Theano で Deep Learning <1> : MNIST データをロジスティック回帰で判別する
概要
ここ数年 Deep Learning 勢の隆盛いちじるしい。自分が学生の頃は ニューラルネットワークはオワコン扱いだったのに、、、どうしてこうなった?自分もちょっと触ってみようかな、と記事やらスライドやら読んでみても、活性化関数が〜 とか、 制約付き何とかマシンが〜(聞き取れず。何か中ボスっぽい名前)とか、何言っているのかよくわからん。
巷には 中身がわかっていなくてもある程度 使えるパッケージもいくつかあるようだが、せっかくなので少しは勉強したい。
Python 使って できんかな?と思って探してみると、すでに Theano
というPython パッケージの開発チームが作った DeepLearning Documentation 0.1 という大部の聖典 (バイブル) があった。
当然だがこの文書では Theano
の機能をいろいろ使っているため、ぱっと見では 何をやってんだかよくわからない。ということで、Theano
の仕様を調べたり numpy
で翻訳したりしながら読み解いてみることにした。
聖典の目次
かなり楽観的な見通しだが、 1章/週 のペースで進めても 3ヶ月近くかかるな、、。
第一回 MNIST データをロジスティック回帰で判別する (今回) | 英 |
第二回 多層パーセプトロン | 英 |
第三回 畳み込みニューラルネットワーク | 英 |
第四回 Denoising オートエンコーダ | 英 |
第五回 多層 Denoising オートエンコーダ | 英 |
第六回の準備1 networkx でマルコフ確率場 / 確率伝搬法を実装する | - |
第六回の準備2 ホップフィールドネットワーク | - |
第六回 制約付きボルツマンマシン | 英 |
Deep Belief Networks | 英 |
Hybrid Monte-Carlo Sampling | 英 |
Recurrent Neural Networks with Word Embeddings | 英 |
LSTM Networks for Sentiment Analysis | 英 |
Modeling and generating sequences of polyphonic music with the RNN-RBM | 英 |
全体の流れ / スクリプトは元文書を参照。以下の各セクション名には対応するセクションへのリンクを貼っている。
Theano
とは
こんなことができます。
- シンボル(変数名) / 値 (スカラー, ベクトル, 行列) / 式を 数式表現そのもの (構文木) として扱える。
- 式の定義 / 途中計算結果の再利用が楽
- 機械学習で頻出する勾配計算を解析的に実行できる
- 高速な計算実行
インストール
pip install theano
準備
import numpy import theano import theano tensor as T
モデル
ロジスティック回帰が 入力をどのようにクラス分類するか?という方法が書いてある。ロジスティック回帰 そのものについてはこちらでまとめた。
プログラム中で まず 目に付くのは以下のような theano.shared
表現。Theano
では以下 二つのメモリ空間を明示的にわけて扱う。
- Python 空間 ( user space ) : Python スクリプト上で定義された普通の変数が存在する。
Theano
空間 :theano.shared
によって定義された変数 (以降、共有変数) が存在する。物理的に Python 空間とは別に確保され、Theano
の式表現(後述)から参照できる。また、GPU へのデータ転送を高速にする。
theano.shared
で宣言された変数は 実データとして numpy
のデータをもつ。
# この表現で作られるデータは W = theano.shared( value=numpy.zeros( (n_in, n_out), dtype=theano.config.floatX ), name='W', borrow=True ) # numpy だとこちらと同じ W = numpy.zeros((n_in, n_out), dtype=numpy.float64)
theano.shared
による共有変数の定義
上でも指定されている引数 borrow
は Python 空間上で定義されたデータの実体を 共有変数でも共有するかどうかを決める。
# Python 空間で変数定義 x = numpy.array([1, 1, 1]) # theano.shared で 共有変数化 xs = theano.shared(x) # 返り値は int32 / vector の TensorSharedVariable 型 xs # <TensorType(int32, vector)> type(xs) # theano.tensor.sharedvar.TensorSharedVariable # 内部の値を得るためには get_value xs.get_value() # array([1, 1, 1]) # デフォルトでは 対象オブジェクトのコピーが 共有変数の実体になるため、 # Python 空間の変数を変更しても 共有変数の実体には影響しない x[1] = 2 x # array([1, 2, 1]) xs.get_value() # array([1, 1, 1]) # 同じ実体を共有したい場合は borrow=True xs = theano.shared(x, borrow=True) xs.get_value() # array([1, 2, 1]) # Python 空間での変更が 共有変数へも反映される x[1] = 3 x # array([1, 3, 1]) xs.get_value() # array([1, 3, 1])
theano.tensor
上の関数の呼び出し
Theano
では シンボル(変数名) / スカラー / ベクトル / 行列などはテンソル型として抽象化される。テンソル型に対する処理は theano.tensor
上で定義された関数を使って行う。
例えば、多項ロジスティック回帰では 入力があるクラスに分類される確率はソフトマックス関数を用いて書けた。Theano
にはソフトマックス関数を計算する theano.tensor.nnet.softmax
があって、
d = theano.shared(value=numpy.matrix([[0.1, 0.2, 0.3], [0.3, 0.2, 0.1], [0.1, 0.2, 0.3]], dtype=theano.config.floatX), name='d', borrow=True) sm = T.nnet.softmax(d) # 返り値は TensorVariable 型 type(sm) # <class 'theano.tensor.var.TensorVariable'> # TensorVariable から結果を取り出す (関数を評価する) には eval() sm.eval() # [[ 0.30060961 0.33222499 0.3671654 ] # [ 0.3671654 0.33222499 0.30060961] # [ 0.30060961 0.33222499 0.3671654 ]]
ロジスティック回帰の予測値は上記の確率を最大にするラベルになる。値が最大となるラベルをとる処理は theano.tensor.argmax
。これは numpy.argmax
のテンソル版。以下の通り、theano.tensor
の関数は通常 (共有変数化されていない) の numpy.array
も受け取ることができる。その場合も返り値は TensorVariable
型になる。
# 3 番目の要素 ( index でみて 2 ) が最も大きい am = T.argmax(numpy.array([1, 2, 3, 2, 1])) type(am) # theano.tensor.var.TensorVariable am.eval() # array(2L, dtype=int64)
損失関数の定義
これはそのまま。すべて theano.tensor
の関数であることには注意。
LogisticRegression
のクラス作成
LogisticRegression.errors
で誤差 (ここでの誤差は真のクラスと 判別結果の差)を取っている。論理演算 !=
についても テンソル版である theano.tensor.neq
が定義されているのでそちらを使う。
# 真のクラス y = numpy.array([1, 0, 1, 1, 1]) # 予測値が2つ誤判別しているとする y_pred = numpy.array([1, 1, 1, 0, 1]) # theano.tensor T.mean(T.neq(y_pred, y)).eval() # 0.4 # numpy numpy.mean(y_pred != y) # 0.4
モデルの学習
ロジスティック回帰の学習について。勾配 (Gradient) の計算は Theano
だと theano.grad
を使って簡単にできるようだ。ここからの一連の処理が今回のキモ。
Theano
での勾配の計算 (微分)
Theano
での勾配の計算については公式ドキュメントに 詳細 がある。Theano
では、定義された式にあらわれるシンボル / 定数を構文木として保持している。そのため、勾配計算の際は 構文木解析によって導関数を求めて実行することができる。とりあえず を定義して微分してみると、
# シンボル x を宣言 (この時点で x に値はない) x = T.dscalar('x') # y = x ** 2 という式を宣言 # シンボルに対して演算子から式を作っていけるところがカッコイイ y = x ** 2 # y を x について微分 (まだ値はない) gy = T.grad(y, x) # gy の定義 (導関数) を確認 theano.pp(gy) # '((fill((x ** TensorConstant{2}), TensorConstant{1.0}) * TensorConstant{2}) * (x ** (TensorConstant{2} - TensorConstant{1})))'
TensorConstant
は {}
内の値をもつスカラー。また、fill
は 第一引数を 第二引数で埋める、という意味になる。順番に数値を埋めてみると、、、おお、、、ちゃんと になっている。
# '((fill((x ** TensorConstant{2}), TensorConstant{1.0}) * TensorConstant{2}) * (x ** (TensorConstant{2} - TensorConstant{1})))' # = '((fill((x ** 2), 1.0) * 2) * (x ** (2 - 1)))' # = '(2 * x)'
続けて、式から関数を作成し、実際の値を渡して計算 (式の評価) を行う。え、式にそのまま引数渡せないの?と思うが、どうも function
の実行時に式表現をコンパイルしているのでダメらしい。
# 式から関数を作る。function の第一引数は関数に与える引数, 第二引数に関数化する式 f = function([x], gy) # x = 4 のときの x ** 2 の傾きを求める f(4) # array(8.0) # x = 8 のときの x ** 2 の傾きを求める f(8) # array(16.0)
さて、元の文書にもどってロジスティック回帰を定義 / 学習を始めるところをみてみる。ここでのテンソル型の式から学習器を作っていく流れは格好いい。
# ミニバッチ確率勾配降下法で指定する index (スカラー)を入れるシンボル index = T.lscalar() # 入力データの行列を示すシンボル x = T.matrix('x') # ラベルのベクトルを示すシンボル y = T.ivector('y') # ロジスティック回帰の処理も theano.tensor の関数で定義されているため、 # シンボル x が実データなしで渡せる classifier = LogisticRegression(input=x, n_in=28 * 28, n_out=10) # 引数 y もシンボル。返り値 cost では負の対数尤度を計算する式が返ってくる cost = classifier.negative_log_likelihood(y) # w, b の勾配を計算する式 g_W = T.grad(cost=cost, wrt=classifier.W) g_b = T.grad(cost=cost, wrt=classifier.b)
で、最後にこれらを theano.function
で関数化する。引数の意味は、
inputs
: 関数への入力 (引数)。outputs
: 関数化される式。updates
: 共有変数を更新する式。givens
: 引数 (inputs
) -> 共有変数へのマッピングを行う辞書。
つまり、
# w, b の更新を定義する式 updates = [(classifier.W, classifier.W - learning_rate * g_W), (classifier.b, classifier.b - learning_rate * g_b)] train_model = theano.function( inputs=[index], outputs=cost, updates=updates, givens={ x: train_set_x[index * batch_size: (index + 1) * batch_size], y: train_set_y[index * batch_size: (index + 1) * batch_size] } )
上の定義は
index
を引数として受け取り、givens
の指定によりindex
の位置のデータを切り出して シンボルx
,y
に入れ、x
,y
をoutputs
の式 (負の対数尤度) に与えて計算結果を得て、updates
で指定された更新式によって共有変数を更新する
という一連の処理を行う関数を作っている。うーん、これは訓練されていないと ちゃっと読めないな、、、。
そして、この時点においても train_model
では ロジスティック回帰の学習を 関数として定義しただけで、実計算はまだ行われていない。実際の学習 ( 続く while
ループ中 ) では この定義に対して index
だけを渡してやれば、 必要な値が計算 / 関連する値が自動的に更新されていく。
全部まとめて
以降は特になし。MNISTデータをローカルに保存した後、まとめ にあるスクリプトをコピペして実行すればよい。
11/30追記: つづきはこちら。
- 作者: 岡谷貴之
- 出版社/メーカー: 講談社
- 発売日: 2015/04/08
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (4件) を見る