Dropout を実装する
9月に入ったとたん仕事があれこれ重なって「ああもう夏休み終わったんやなぁ…」ってのと,雨がちであんまりてくてくしてへんのとで,ブルーないしブラックな感じのたかたかです (^^;
さて,これまで( Convolutional Neural Net で CIFAR-10 を識別してみる - まんぼう日記 とか 大石東バイパスとTheanoでGPGPU - まんぼう日記 とか)ずっと後回しにしてきた dropout を実装しました.参考にした文献はこちら:
dropout を入れるついでに,今までのプログラムを大幅に書き直し.んで,MNIST 使って実験してみました.ソースと実験結果の生データが
https://gist.github.com/takatakamanbou/4a35d1e0f1a7f6db3639
にあります.
実験結果
実験条件は…面倒なのでおおざっぱに (^^;
- 入力 - ReLu 1000個 - ReLu 1000個 - softmax という3層
- 慣性項ありのSGD,重みのL2-norm正則化はなし(weight decayなし)
- Theano で Tesla K20 使ってGPGPU
で,まずは dropout なしの場合の結果.学習は 50epoch 繰り返すことにして,重みの初期値を生成する乱数の種を3通り変えて実行してみました.CE は Cross Entropy, ER は Error Rate, L, V, T はそれぞれ学習,検査,テスト用データを表します.右端は実行に要した時間(実時間).
CE(L) ER(L) CE(V) ER(V) CE(T) ER(T) seed = 1 50 | 0.0000 0.00 | 0.0925 1.45 | 0.0857 1.46 real 2m2.908s seed = 2 50 | 0.0000 0.00 | 0.0949 1.45 | 0.0834 1.41 real 2m2.661s seed = 3 50 | 0.0000 0.00 | 0.1022 1.55 | 0.0882 1.47 real 2m2.639s
次は,dropout あり.dropout の確率(ニューロンがオンになる確率)は,入力が 0.8,2つの隠れ層はともに 0.5 としました.dropout すると学習が収束するまでにより多くの繰り返しが必要となるので,こちらは100epoch学習させてます.学習係数や慣性項の係数は dropout なしと同じ.
CE(L) ER(L) CE(V) ER(V) CE(T) ER(T) seed = 1 100 | 0.0003 0.01 | 0.0594 1.18 | 0.0513 1.12 real 4m13.466s seed = 2 100 | 0.0003 0.01 | 0.0588 1.10 | 0.0507 1.21 real 4m14.241s seed = 3 100 | 0.0002 0.00 | 0.0642 1.23 | 0.0471 1.16 real 4m15.080s
ご覧のとおり,テスト誤識別率を下げることができています.繰り返し回数が2倍になった分実行時間も2倍になってるだけで,同じ繰り返し数で考えれば追加の計算コストも無視できるようです.
細かい話
dropout を実装する際に,「何も考えずに各ニューロンの出力を確率的に 0 にするだけでええのんか?」がとっても気になったので,ちょっとその話を.
まずこんな感じで記号を定義します(「深層学習」Chapter 4 と同じのようで添字の順番その他微妙に違います).第 \( \ell \) 層の \( j \) 番目のニューロンへの入力の総和 \( y_j^{(\ell)} \) と出力 \( z_j^{(\ell)}\) を次式で表します.
\[ \begin{align} y_j^{(\ell)} & = \sum_{i} w_{ij}^{(\ell)}z_{i}^{(\ell)} \\ z_j^{(\ell)} & = m_{j}^{(\ell)}f(y_{j}^{(\ell)}) \end{align} \]
\( f \) は活性化関数,\( m_{j}^{(\ell)} \) ってのは dropout するしないを表す変数で,\( 0 \) か \( 1 \) をとります.バイアス項が見当たりませんが,恒等的に 1 を出すニューロンがいて,そいつとの結合重みで表現してると考えましょう(ただしそいつは dropout しないものとします).
誤差逆伝播学習知ってる人には言わずもがな(かつ知らない人にはわけわかめな話でアレなん)ですが,学習のコスト関数を \( E \) とおいて,
\[ \delta_j^{(\ell)} = \frac{\partial E}{\partial y_j^{(\ell)}} \]
というものを定義します.すると,
\[ \delta_j^{(\ell)} = \sum_{k}\frac{\partial E}{\partial y_k^{(\ell + 1)}} \frac{\partial y_k^{(\ell + 1)}}{\partial y_j^{(\ell)}} = \sum_{k} \delta_k^{(\ell +1)} \frac{\partial y_k^{(\ell + 1)}}{\partial y_j^{(\ell)}} \]
であり,また
\[ \begin{align} \frac{\partial y_k^{(\ell + 1)}}{\partial y_j^{(\ell)}} &= \frac{\partial}{\partial y_j^{(\ell)}} \sum_j w_{jk}^{(\ell + 1)}m_j^{(\ell)}f(y_j^{(\ell)}) \\ &= w_{jk}^{(\ell + 1)}m_j^{(\ell)}f'(y_j^{(\ell)}) \end{align} \]
なので,
\[ \delta_j^{(\ell)} = \left( \sum_{k} \delta_k^{(\ell +1)}w_{jk}^{(\ell + 1)} \right) m_j^{(\ell)}f'(y_j^{(\ell)}) \]
と書けます.したがって,
\[ \begin{align} \frac{\partial E}{\partial w_{ij}^{(\ell)}} &= \frac{\partial E}{\partial y_j^{(\ell)}} \frac{\partial y_j^{(\ell)}}{\partial w_{ij}^{(\ell)}} = \delta_j^{(\ell)}z_{i}^{(\ell - 1)} \\ &= \left( \sum_{k} \delta_k^{(\ell +1)}w_{jk}^{(\ell + 1)} \right) m_j^{(\ell)}f'(y_j^{(\ell)}) m_i^{(\ell - 1)}f(y_i^{(\ell - 1)}) \end{align} \]
となります.この式から,\( w_{ij}^{(\ell)} \) がつないでいる2つのニューロンのいずれか一方がオフになれば,この重みの修正量は \( 0 \) になることがわかります.
うん,そやから Theano で実装するときは変数 \( m_{j}^{(\ell)} \) を導入して,後は Theano に微分を任せるだけでおけ,な気がします.でも,それでOKなんは慣性項も何もないときなわけで.
慣性項を入れて重みを次式のように更新する場合を考えます.
\[ \begin{align} \Delta w(t+1) &= -\eta \frac{\partial E}{\partial w}(t) + \mu \Delta w(t) \\ w(t+1) &= w(t) + \Delta w(t+1) \end{align} \]
この場合,\( \frac{\partial E}{\partial w}(t) \) の値が \( 0 \) でも慣性があるので,重みは修正されちゃいます.dropout は「確率的に各ニューロンをいなかったものとみなす」という話ですから,厳密にはこれはちゃうんちゃうかと.\( \Delta w, w \) ともに更新しないで1ステップ前の値を維持するようにするべきなんやないかと.面倒なのでここでは省略しましたが,weight decay の実装の仕方次第でやっぱり同じこと考えなあかんし.
ちうわけで,重みの更新式にも変数 \( m_{j}^{(\ell)} \) 入れて上記のことを実現したプログラムも作って実験してみました.結果は,わざわざそんなことしても変わらへん,ということに….まあそうでしょうな.というわけで,Theano で dropout 実装するなら,出力に 0,1 のマスクをかけるだけでOKのようです.上の実験結果は,そのバージョンの結果です.
…また無駄なことに時間を使ってまいましたな (^^;