ブログ投稿の続きとして、スライドに書けなかったTheanoの細かい部分についてもう少しまとめておこうと思います。
まず、Theano 解説 はTheano特徴を簡潔に表現されているので、一読をオススメします。
ここでも書かれていますが、Theanoの特徴として、
- 実行時にCコードを生成してコンパイル
- GPUでの実行のサポート(要CUDA)
- 自動微分
などがあげられると思います。
Theanoの超簡略チュートリアル
http://deeplearning.net/software/theano/tutorial/index.html#tutorial
の乱暴な要約です。
まず常にImportしておく3つ
import numpy
import theano
import theano.tensor as T
この3つはお約束です。
これだけ知っておけば概ね大丈夫
以下の事柄がだいたい理解できれば、Deep Learningの実装を読んで理解したり、変更を加えたりすることができると思います。
- T.scalar, T.vector, T.matrix: 変数(シンボル)の話
- シンボルの演算結果はシンボルである
- function: 関数周りの話
- T.grad: 微分周りの話
- shared: 共有変数
- T.gradと共有変数とupdates: 典型的な勾配法の実装パターン
変数(シンボル)の話
Theanoで扱う変数は「テンソル(tensor)」という概念で扱われています。
テンソル周りの型や演算はtheano.tensor (as T) 以下にだいたい定義されています。
私はいまいちテンソルとか理解してないのですが、とりあえず
「T. 以下には、変数の型や主要な汎用数学関数(expやlogやsinとか)がある*」
ということです。
ここで「変数の型」というのは
- 「スカラー、ベクトル、行列」のような次元的(テンソル用語では「階数」と呼ぶらしい)な意味
- 要素の型(整数、実数など)の意味
があります。これらを組み合わせて、変数の型(テンソルの型)を表しています。
名前としては、
- scalar: スカラー(階数=0)
- vector: ベクトル (階数=1)
- matrix: 行列(階数=2)
であり、
- d: double
- l: long
などです(他にもあります)。
これらを組み合わせて、
- 実数(float64)の行列→T.dmatrix()
- 整数(int64)のベクトル→T.lvector()
などと表します。
例えば以下のようになります。
x = T.lscalar("x")
m = T.dmatrix()
これら、x,m は「記号(シンボル)」であって実際の値を持っていません。
この辺が普通のPythonの変数とは少し異なるところです。
変数の生成についてより詳しくは、下記をご覧ください。
http://deeplearning.net/software/theano/library/tensor/basic.html#libdoc-basic-tensor
シンボルの演算結果はシンボルである
例えば、
x = T.lscalar("x")
y = x*2
z = T.exp(x)
と実行したとします。
x は 値を持たないシンボルですので、yにも値が入ることはなく、yも「x*2」を意味するシンボルになります。
zも同様に exp(x) を表すシンボルです。(実際にはPythonのObject)
ニューラルネットワークを構成する計算も、実際に値が与えられるまでは、このシンボル同士の演算の塊(要するに式)として扱われます。
式のまま扱うので、人間にとっても理解しやすいですし、後述する自動微分などが可能になったり、実行時の最適化が可能になるんだと思います。
function: 関数周り
実際に計算をさせるには、「関数」を定義する必要があります。
例えば、 「f(x) = x*2 」を作りたければ、
f = theano.function([x], x*2)
とか
y = x*2
f = theano.function([x], y)
などとします。
f は一つの関数になり、呼び出すと
>>> f(3)
array(6)
となります。
functionは
- 第一引数に「引数のシンボルのリスト」
- キーワード inputs として指定も可能
- 第二引数に「計算する式(シンボル同士の演算)」
- キーワード outputs として指定も可能
を指定します。
この時点で関数がコンパイルされるようで、複雑な関数でも高速に実行されたりします。
functionには、givens というキーワードがあります。
givensにはその名の通り「式の中のシンボルを別のシンボルや値で置き換える」ような働きがあります。
例えば、
>>> x = T.dscalar()
>>> y = T.dscalar()
>>> c = T.dscalar()
>>> ff = theano.function([c], x*2+y, givens=[(x, c*10), (y,5)])
>>> ff(2)
array(45)
ということができます。
本来計算する値は 「x*2+y」なのですが、関数自体の引数は「c」というシンボルを取ることになっています。
実際には、xやyが与えられないと計算できないわけですが、このgivensの部分でxやyの値を与えるという形でも計算できます。
これは、今後のTutorialの中で機械学習でデータを部分的に利用する際に使われたりしています。
T.grad: 微分周り
Theanoの目玉機能の一つがこの微分機能です。自動微分と呼ばれる「式を分析して微分した式を求める」ことができます。
例えば、
x, y = T.dscalars("x", "y") # ※まとめて宣言する書き方
z = (x+2*y)**2
という式を x について微分したら、 dz/dx = 2(x+2*y) になりますが、それを
gx = T.grad(z, x)
で式変換できます。
yについての微分ならば、 dz/dy = 4(x+2*y) ですが、それを
gy = T.grad(z, y)
で式変換できます。
実際に値を求めるときは、やはり関数化が必要で、
fgy = theano.function([x,y], gy)
>>> fgy(1,2)
array(20.0)
などとします。
shared: 共有変数
変数=theano.shared(object)
という形で上記functionの中で参照可能な共有型のデータを宣言できます。
例えば、
>>> x = T.dscalar("x")
>>> b = theano.shared(numpy.array([1,2,3,4,5]))
>>> f = theano.function([x], b * x)
>>> f(2)
array([ 2., 4., 6., 8., 10.])
などと使うことができます。
shared変数の値を参照したりセットしたりするには、
>>> b.get_value()
array([1,2,3,4,5])
>>> b.set_value([10,11,12])
などとします。
先ほど定義した関数にも即座に反映されもう一度 f(2) を実行すると結果が変わっていることがわかります。
>>> f(2)
array([ 20., 22., 24.])
T.gradと共有変数とupdates: 典型的な勾配法の実装パターン
function にはupdatesというキーワード引数があり、これによって共有変数を更新することができます。
例えば、cを共有変数として、関数fが実行される度に 1 増やすようにするには以下のように記述します。
c = theano.shared(0)
f = theano.function([], c, updates= {c: c+1})
updated={c: c+1} という部分が、 c = c + 1 というプログラミング言語ではお馴染みの値の更新を表しています。
これを実行すると以下のようになります。
>>> f()
array(0)
>>> f()
array(1)
>>> f()
array(2)
これらを使うと勾配法を実装することができます。
例えば、データx=[1,2,3,4,5] に対して y=sum((x-c)^2) が最小となるようなcを求めたいとします。
コードとしては、例えば下記のようになります。
x = T.dvector("x") # input
c = theano.shared(0.) # これを更新していく。初期値はとりあえず0.
y = T.sum((x-c)**2) # y=最小化したい値
gc = T.grad(y, c) # y を c について偏微分
d2 = theano.function([x], y, updates={c: c - 0.05*gc}) # 実行する度に c を更新して、現時点のyを返す
として、**d2()に[1,2,3,4,5]**を何度か与えると、
>>> d2([1,2,3,4,5])
array(55.0)
>>> c.get_value()
1.5
>>> d2([1,2,3,4,5])
array(21.25)
>>> c.get_value()
2.25
>>> d2([1,2,3,4,5])
array(12.8125)
>>> c.get_value()
2.625
となります。yが徐々に小さくなり、cが徐々に「3」に近づいていくのがわかると思います。
ロジスティック回帰の実装
この辺りまで理解できれば、以下のチュートリアルのロジスティック回帰がどうのように動くかわかってくるのではないかと思います。
(え、最初からわかるって?^^;)