数式をnumpyに落としこむコツ

Post on 12-Nov-2014

20.178 views 3 download

Tags:

description

Tokyo.SciPy #2 にて発表した、数式(あるいは数式入りのアルゴリズム)から実装に落とす場合、何に気をつけるのか、どう考えればいいのか、というお話。 対象は、どうやって数式をプログラムすればいいかよくわからない人、ちょっとややこしい数式になると四苦八苦してしまい、コードに落とすのにすごく時間がかかってしまう人、など。 ここでは実行速度についてはひとまずおいといて、簡潔で間違いにくい、ちゃんと動くコードを書くことを目標にしています。

Transcript of 数式をnumpyに落としこむコツ

数式を numpy に落としこむコツ ~機械学習を題材に~

2011/10/15

中谷 秀洋@サイボウズ・ラボ

@shuyo / id:n_shuyo

「機械学習の手法を実装」って どうするの?

機械学習の手法いろいろ

数式! 数式! 数式!!!

numpy で実装

今回のターゲット

機械学習の手法いろいろ

数式! 数式! 数式!!!

numpy で実装

ここは対象外

ここをやっつけます

!!

「数式→実装」は共通

機械学習

数式! 数式! 数式!!!

numpy で実装

ここは共通

数値解析 統計処理

数式から実装まで

数式! 数式! 数式!!!

numpy で実装

数式見てすぐ実装? ムリムリ!

小さいステップに分解

数式! 数式! 数式!!!

numpy で実装

数式から 行間の情報を読み解く

「逐語訳」できる形に 数式を書き換える

今日のポイント

この後の流れ

1. 数式が降ってきた!

– 「式はどうやって出てきたか」は無視!

2. (必要なら)数式を読み解こう

3. (必要なら)数式を書き換えよう

4. 数式を「逐語訳」で実装しよう

「数式」と言っても いろいろある

対象とする「数式」

• 数式の例は「パターン認識と機械学習」

(以降 PRML)から引く

• 主に行列やその要素の掛け算が出てくる数式

– 掛け算は基本中の基本!

• コンピュータで実装したい数式は、行列を

使って表されているものも多い

– 機械学習は典型例の1つ、かな?

– 他の分野は……あまり知りません(苦笑

おことわり

• Python/numpyの基本機能は説明しません

– Python の文法とか

– 行列やベクトルの四則演算とか

• ラムダ式とリスト内包はちょろっと紹介

• 線形代数の基本的な知識も説明しません

– 四則演算とか、転置とか、逆行列とかとか

• 行列式や固有値なんかは出てこないので安心して

記法

• 数式

– ベクトルは太字の小文字

– 行列は太字の大文字

• コード

– import numpy は省略

– import numpy as np はしない

– numpy.matrix は使わず ndarray で

• 行列積と要素積が紛らわしくなるとかいろいろ嫌いw

ネームスペースを 省略するの嫌い~

C++ の using namespace も 使ったことないしw

書き換え不要なパターン

まずは一番簡単なパターンから

𝒘 = 𝚽𝑇𝚽 −1𝚽𝑇𝒕 (PRML 3.15 改)

• 線形回帰のパラメータ推定の式

– この式がどこから降ってきたかは気にしな

い!

ちなみに「線形回帰」って?

• 回帰:与えられた点を(だいたい)通る曲線

(関数)を見つけること

– 「回帰」って何が戻ってくるの? というの

は突っ込んではいけないお約束

• 線形回帰:∑𝒘𝑇𝝓(𝒙)という線形結合の形

の中で点を通るものを探す

– 線形の関数(つまり直線)を求めているわけで

はありません

一応紹介してみたけど、気にしなくていいですw

数式の「読み解き」

𝒘 = 𝚽𝑇𝚽 −1𝚽𝑇𝒕 (PRML 3.15 改)

• 𝚽:N×M次元の特徴行列

– 中身は気にしない

– N×M次元の行列が与えられているだけ!

• t:N次のベクトル(正解データ)

– 中身は気にしない(以下同様)

• w はベクトル? 行列? 何次の?

※特徴行列の作り方は後の「おまけ」で出てきます

掛け算した行列のサイズの求め方

𝒘 = 𝚽𝑇𝚽 −1𝚽𝑇𝒕

M×1 ← (M×N N×M) M×N N×1

隣接する行列の列数と行数は一致。 そうでなければ必ずどこか間違ってる

各行列のサイズ。 ベクトルは

1列の行列として

「数式がわからない」というとき この段階で間違っていることも少なくない

両端の行数・列数が 行列(ベクトル)のサイズ。 列数が1ならベクトル

numpy に「逐語訳」

𝒘 = 𝚽𝑇𝚽 −1𝚽𝑇𝒕 (PRML 3.15 改)

# PHI = N×M次元の特徴行列 # t = N次のベクトル(正解データ) w = numpy.linalg.solve(numpy.dot(PHI.T, PHI), numpy.dot(PHI.T, t))

※ 逆行列のところで inv() を使ってもいいですが、

solve() の方がコードが短いし、速度もかなり速いです

numpy.dot(PHI.T, PHI) numpy.dot(PHI.T, t)

𝑨−1𝒃 = numpy.linalg.solve(𝑨, 𝒃)

いつもこんなにかんたんとは 限りませんよね

書き換えが必要になるパターン

多クラスロジスティック回帰の 誤差関数の勾配

𝛻𝒘𝑘𝐸 𝑾 = 𝑦𝑛𝑘 − 𝑡𝑛𝑘 𝝓𝑛

𝑁

𝑛=1

(k = 1,⋯ , 𝐾)

(PRML 4.109 改)

• 𝒀 = 𝑦𝑛𝑘 : N×K 次行列(予測値)

• 𝑻 = 𝑡𝑛𝑘 : N×K 次行列(1-of-K 表現)

• 𝑾 = 𝒘1, … ,𝒘𝐾 = (𝑤𝑚𝑘) : M×K 次行列

• 𝚽 = 𝜙𝑛𝑚 = 𝝓1, ⋯ ,𝝓𝑁𝑇 : N×M 次行列

– 𝝓𝑛 = 𝝓 𝒙𝑛 = 𝜙𝑚 𝒙𝑛𝑇: M 次ベクトル

与えられている情報

「ロジスティック回帰」って?

「誤差関数」って?

「勾配」って?

式がどこから降ってきたかは 気にしない!

さすがに「勾配」は 必要なんじゃあないの?

𝛻𝒘𝑘𝐸 𝑾 = 𝑦𝑛𝑘 − 𝑡𝑛𝑘 𝝓𝑛

𝑁

𝑛=1

• 右辺は M 次ベクトル

– 𝑦𝑛𝑘 − 𝑡𝑛𝑘 はただのスカラー

– 一般には先ほどの方法で次元を読み解けばいい

• それが k=1,……,K 個あるだけ

– つまり求めるのは「M×K次元の行列」と読み解く

• ∴「勾配」は実装になんの関係もない!

これ

求めるものは 読み解けたが

どうすれば実装できるか まだよくわからない

「逐語訳」できる形に書き換える

• 掛けて行列になるパターンは大きく3通り

– 上から要素積、行列積、直積

𝑐𝑖𝑗 = 𝑎𝑖𝑗𝑏𝑖𝑗 ⇔ C = A * B

𝑐𝑖𝑗 = ∑ 𝑎𝑖𝑘𝑏𝑘𝑗𝑘 ⇔ C=numpy.dot(A, B)

𝑐𝑖𝑗 = 𝑎𝑖𝑏𝑗 ⇔ C=numpy.outer(a, b)

数式を左の形に書き換えれば、 右の numpy コードに「逐語訳」できる

※「外積」もあるが、使う人やシーンが限られるので略

式を書き換える (1)

𝛻𝒘𝑘𝐸 𝑾 = 𝑦𝑛𝑘 − 𝑡𝑛𝑘 𝝓𝑛

𝑁

𝑛=1

• 行列の要素の式になおす

𝛻𝐸 𝑾𝑚𝑘= 𝑦𝑛𝑘 − 𝑡𝑛𝑘 𝜙𝑛𝑚

𝑁

𝑛=1

(𝑚 = 1,⋯ ,𝑀; 𝑘 = 1,⋯ ,𝐾)

– 𝛻𝐸 𝑾 は「求める行列」としてひとかたまりで扱う

式を書き換える (2)

𝛻𝐸 𝑾𝑚𝑘= 𝑦𝑛𝑘 − 𝑡𝑛𝑘 𝜙𝑛𝑚

𝑁

𝑛=1

• 注:右辺の添え字に未解決のものは残らない

– 左辺に現れる : m, k

– 右辺で解決 : n (総和で消える)

• 3種類の積のどれかに帰着するよう変形

– この場合、総和があるので 𝑐𝑖𝑗 = ∑ 𝑎𝑖𝑘𝑏𝑘𝑗𝑘 に

式を書き換える (3)

𝑨 = 𝑎𝑛𝑘 = 𝑦𝑛𝑘 − 𝑡𝑛𝑘 とおくと(𝑁 × 𝐾 行列)

𝛻𝐸 𝑾𝑚𝑘= 𝑎𝑛𝑘𝜙𝑛𝑚

𝑁

𝑛=1

= Φ𝑇 𝑚𝑛 𝐴 𝑛𝑘

𝑁

𝑛=1

• 右辺を Σn○mn○nk の形に調整

– 左辺が○mk & 右辺は n で和を取っている

– 添え字の順序を逆にしたければ転置でOK

• 𝛻𝐸 𝑾 = 𝚽𝑇𝑨 であることがわかる

– 難しくて実装できなさそうだった式が かんたんに!

内側は 同じ添え字同士

numpyに「逐語訳」

• 𝑨 = 𝒀 − 𝑻, 𝛻𝐸 𝑾 = 𝚽𝑇𝑨 を実装

– うわあ、かんたんすぎ

• 元の数式と見比べてみよう

𝛻𝒘𝑘𝐸 𝑾 = 𝑦𝑛𝑘 − 𝑡𝑛𝑘 𝝓𝑛 (k = 1,⋯ , 𝐾)

𝑁

𝑛=1

# PHI = N×M 次元の特徴行列 # Y, T = N×K 次元の行列 gradient_E = numpy.dot(PHI.T, Y - T)

まとめ

• 数式から条件を読み解こう

– この段階で間違っていると、絶対うまく行かない

– さぼらず紙と鉛筆で確認するのが一番賢い

• 「逐語訳」できる数式なら実装かんたん

– 基本機能の呼び出しで完成!

– 難しい数式は「逐語訳」できる形に書き換え

– さぼらず紙と鉛筆(ry

(おまけ) 「リスト内包」を使いこなして楽しよう

特徴行列(先ほどの 𝚽)

𝚽 =

𝜙1 𝒙1 𝜙1 𝒙2 ⋯ 𝜙1 𝒙𝑁𝜙2 𝒙1 𝜙2 𝒙2 ⋯ 𝜙2 𝒙𝑁⋮ ⋮ ⋱ ⋮

𝜙𝑀 𝒙1 𝜙𝑀 𝒙2 ⋯ 𝜙𝑀 𝒙𝑁

• 関数 𝝓 𝒙 = 𝜙1 𝒙 ,⋯ , 𝜙𝑀 𝒙 と、

• データ 𝑿 = (𝒙1, ⋯ , 𝒙𝑁) から作る行列

– カーネル法のグラム行列も似たような作り

特徴行列の作り方 (1) # X = N×D 次元の行列(今回は D=1) phi = [ lambda x: 1, lambda x: x, # φ:特徴関数の列 lambda x: x ** 2, # lambda ってなに? lambda x: x ** 3 ] N = len(X) M = len(phi) PHI = numpy.zeros((N, M)) # Φ:N×M行列の入れ物を用意 for n in xrange(N): for m in xrange(M): PHI[n, m] = phi[m](X[n]) # φ_m(x_n)

‘lambda’ ってなに?

ぷちPython講座:ラムダ式

• lambda : その場で関数を作る

– def を書かなくていい

f = lambda x: x ** 3

def f(x): return x ** 3

だいたい同じ

※厳密には def と lambda はいろいろ違うわけだけど、

ここでは細かいことは気にしない

つまりラムダ式のところは

• 実はこの数式の実装でした

𝜙𝑚 𝑥 = 𝑥𝑚 (𝑚 = 0,⋯ ,𝑀 − 1)

• 繰り返しなんだから、もっとかんたんに

できそう

phi = [ lambda x: 1, # φ_0(x) = 1 lambda x: x, # φ_1(x) = x lambda x: x ** 2, # φ_2(x) = x^2 lambda x: x ** 3 # φ_3(x) = x^3 ]

ぷちPython講座:リスト内包

• リスト内包 : ルールから配列を作る

– for ループを書かなくていい

– R の apply() 系の関数に相当

a = [] for x in xrange(10): a.append(x * x)

a = [x * x for x in xrange(10)]

リスト内包なら簡潔!

※厳密にはいろいろ(ry

「リスト内包」を使えば……

• かんたんになったね!

phi = [ lambda x: 1, lambda x: x, lambda x: x ** 2, lambda x: x ** 3 ]

phi = [lambda x: x ** m for m in xrange(M)]

𝜙𝑚 𝑥 = 𝑥𝑚 (𝑚 = 0,⋯ ,𝑀 − 1)

こう書ける気がする

だめでした……

• 𝜙0 2 , 𝜙1 2 , 𝜙2 2 , 𝜙3 2 を表示してみる

– “1 2 4 8” と出力されることを期待

• ところがこれの実行結果は “8 8 8 8”

– って、全部同じ!? なんで???

M = 4 phi = [lambda x: x ** m for m in xrange(M)] print phi[0](2), phi[1](2), phi[2](2), phi[3](2)

うまくいかない理由は……

• 「レキシカルスコープ」がどうとか

– ちょっとややこしい

• 回避する裏技もあるけど……

– もっとややこしい

M = 4 phi = [lambda x, c=m: x ** c for m in xrange(M)] print phi[0](2), phi[1](2), phi[2](2), phi[3](2) # => “1 2 4 8” と表示される(ドヤ

結論

• リスト内包の中では lambda を使わない

ようにしよう!(ぇ

– これで同種の問題はだいたい避けられる

• かんたんに書く他の方法を考えてみる

特徴行列の作り方 (2)

• phi を「ベクトルを返す関数」として定義

– 𝜙𝑚のリストではなく,𝝓 = (𝜙𝑚)を扱う

– lambda を書かなくていい

– 関数の呼び出し回数も減って高速化

• 行列の生成にもリスト内包を使う

– numpy.array(リスト内包) は頻出!

def phi(x): return [x ** m for m in xrange(4)] PHI = numpy.array([phi(x) for x in X])

numpy の機能の一部と言っても いいくらい

まとめ

• リスト内包は超便利

– 憶えましょう

– 憶えてなかったら Python 使ってる意味ない

と言い切ってしまっていいくらい

• ラムダ式も便利

– でもリスト内包の中で使うとハマることがあ

るので避けましょう

よだん

• numpy.fromfunction() を使って特徴行

列を作る方法もあるよ。あるけど……

– なんかいろいろひどい

• take とか dtype=int とか

– ダメな numpy の見本

PHI = numpy.fromfunction( lambda n, m: X.take(n) ** m, (N, M), dtype=int)