Learning Chainer with Examples 〜深層学習への入門〜

はじめに

深層学習フレームワークPyTorchのチュートリアルの中に「Learning PyTorch with Examples」というページがある。隠れ層がひとつの簡単なネットワーク(多層パーセプトロン)をnumpyだけで実装し、少しずつPyTorchのAPIで置き換えていく内容である。個々のAPIが何をしているのかが直感的に分かるとても良い入門記事となっている。本ブログでは、同じことをChainerで行う。本題に入る前に、ここで取り上げるネットワーク構造を用いて、誤差逆伝播法について手短に説明する。

動作環境

  • macOS Sierra
  • Python 2.7.14
  • Chainer 2.1.0

対象とするネットワークの構造

以下のネットワークを扱う。
ネットワーク
入力層、隠れ層、出力層の3層から成るネットワークである。それぞれのユニット数は、N_{\rm I}N_{\rm H}N_{\rm O}である。\vec{x}は入力データ(N_{\rm I}次元の列ベクトル)、\vec{y}はそれに対応する教師データ(N_{\rm O}次元の列ベクトル)を表す。\vec{x}を入力し、その出力を教師データ\vec{y}と比較することにより訓練を行う。W_1は入力層と隠れ層の間の重みを表すN_{\rm H}\times N_{\rm I}行列、W_2は隠れ層と出力層の間の重みを表すN_{\rm O}\times N_{\rm H}行列である。

誤差関数

入力データ\vec{x}に対する出力\vec{y}_pは次式で与えられる。

(1)    \begin{eqnarray*} \vec{y}_p &=& W_2\;\vec{h}_r \\ \vec{h}_r &=& \vec{g}(\vec{h}) \\ \vec{h}   &=& W_1\;\vec{x} \end{eqnarray*}

ここで、\vec{g}(\vec{h})は次式で定義される活性化関数Rectified Linear Unit(ReLU)である。

(2)    \begin{eqnarray*} \vec{g}(\vec{h})^T&=&\left(f(h_1),\cdots,f(h_{N_{\rm H}})\right) \\ f(h_i)&=& \left \{ \begin{array}{ll} h_i & (h_i\geq 0) \\ 0   & (\mbox{otherwise}) \end{array} \right. \end{eqnarray*}

\vec{y}_pと教師データ\vec{y}を比較するため次の誤差関数を定義する。

(3)    \begin{equation*} L(W_1,W_2)=\frac{1}{N_{\rm O}}\|\vec{y}_p-\vec{y}\|^{2} \end{equation*}

\vec{y}_p\vec{y}の各成分の差の2乗和の平均値である。訓練により、L(W_1,W_2)が最小となるように重みW_1W_2を最適化する。

誤差逆伝播法と勾配降下法

L(W_1,W_2)W_1W_2で偏微分する。W_1,W_2の成分をw_{1,ij},w_{2,ij}\vec{y}_{p},\vec{h}_r, \vec{h}の成分をy_{p,i},h_{r,i},h_{i}と書くことにする。微分の連鎖律を用いて、

(4)    \begin{eqnarray*} \frac{\partial L}{\partial w_{2,ij}} &=&\sum_{m}\frac{\partial L}{\partial y_{p,m}}\frac{\partial y_{p,m}}{\partial w_{2,ij}} \\ &=&\sum_{m,n}\frac{\partial L}{\partial y_{p,m}}\frac{\partial}{\partial w_{2,ij}}\left( w_{2,mn}h_{r,n} \rith) \\ &=&\sum_{m,n}\frac{\partial L}{\partial y_{p,m}}\delta_{mi}\delta_{nj}h_{r,n} \\ &=&\frac{\partial L}{\partial y_{p,i}}h_{r,j} \end{eqnarray*}

(5)    \begin{eqnarray*} \frac{\partial L}{\partial w_{1,ij}}&=&\sum_{m}\frac{\partial L}{\partial h_m}\frac{\partial h_m}{\partial w_{1,ij}} \\ &=&\sum_{m,n}\frac{\partial L}{\partial h_m}\frac{\partial}{\partial w_{1,ij}}\left(w_{1,mn}x_n\right) \\ &=&\sum_{m,n}\frac{\partial L}{\partial h_m}\delta_{mi}\delta_{nj}x_n \\ &=&\frac{\partial L}{\partial h_i}x_j \end{eqnarray*}

を得る。式(5)の最後の式に現れる\frac{\partial L}{\partial h_i}は以下のように変形できる。

(6)    \begin{eqnarray*} \frac{\partial L}{\partial h_i}&=&\sum_j \frac{\partial L}{\partial y_{p,j}}\frac{\partial y_{p,j}}{\partial h_{i}} \\ &=&\sum_{j,m} \frac{\partial L}{\partial y_{p,j}} \frac{\partial}{\partial h_i}\left(w_{2,jm}f(h_m)\right) \\ &=&\sum_{j,m} \frac{\partial L}{\partial y_{p,j}} w_{2,jm}f'(h_m) \delta_{mi} \\ &=&\sum_{j}  \frac{\partial L}{\partial y_{p,j}} w_{2,ji}f'(h_i) \\ &=&f'(h_i)\sum_{j} \frac{\partial L}{\partial y_{p,j}}w_{2,ji} \end{eqnarray*}

すなわち、\frac{\partial L}{\partial h_i}は式(4)の最後の式に現れる\frac{\partial L}{\partial y_{p,i}}を使って計算することができる。\frac{\partial L}{\partial y_{p,i}}は、最上層(出力層)の出力\vec{y}_pについての微分であり、これさえ計算できれば最下層(入力層)の出力\vec{h}についての微分\frac{\partial L}{\partial h_i}を求めることができる。いま、

(7)    \begin{equation*} \frac{\partial L}{\partial y_{p,i}}=\frac{1}{N_{\rm O}}2(y_{p,i}-y_{i}) \end{equation*}

である。これはネットワークの出力と教師データの差分、すなわち、誤差を表す。誤差を下層に向かって伝播することになる(誤差逆伝播法)。式(7)を用いて、

(8)    \begin{eqnarray*} \frac{\partial L}{\partial w_{2,ij}}&=&\frac{\partial L}{\partial y_{p,i}}h_{r,j} \\ &=&\frac{1}{N_{\rm O}}2(y_{p,i}-y_{i})h_{r,j} \end{eqnarray*}

を得る。行列の形で書けば

(9)    \begin{equation*} \frac{\partial L}{\partial W_2}&=&\frac{1}{N_{\rm O}}2(\vec{y}_p-\vec{y})\;\vec{h}_r^T \end{equation*}

となる。ここで、Tは転置を表す。同様にして、

(10)    \begin{eqnarray*} \frac{\partial L}{\partial w_{1,ij}}&=&\frac{\partial L}{\partial h_i}x_j \\ &=&f'(h_i)\sum_{m} \frac{\partial L}{\partial y_{p,m}}w_{2,mi}\;x_j \\ &=&\frac{1}{N_{\rm O}}f'(h_i)\sum_{m} 2(y_{p,m}-y_{m})w_{2,mi}\;x_j \\ &=&\frac{1}{N_{\rm O}}f'(h_i)\sum_{m}w_{2,mi}\;2(y_{p,m}-y_{m})\;x_j \end{eqnarray*}

を得る。ただし、f'(x)x<0のとき0、それ以外のときは1となることに注意する。行列の形で書けば、

(11)    \begin{equation*} \frac{\partial L}{\partial W_1}&=&\frac{1}{N_{\rm O}}\left[W_2^T\;2(\vec{y}_p-\vec{y})\right]\;\vec{x}^T \end{equation*}

である。ただし、\vec{h}<0のときは0であることに注意する。これらを用いて、W_1W_2を以下のように更新する。

(12)    \begin{eqnarray*} W_1 &\leftarrow& W_1 - \eta\;\frac{\partial L}{\partial W_1} \\ W_2 &\leftarrow& W_2 - \eta\;\frac{\partial L}{\partial W_2} \end{eqnarray*}

ここで、\etaは学習率(正の微小量)である。上の更新を繰り返すことにより、Lを最小値に近づけていく(勾配降下法)。

上の議論は1つのデータの組み(\vec{x},\vec{y})に対するものである。M個の組みを扱う場合、式(9),(11)は以下のように拡張される。

(13)    \begin{eqnarray*} \frac{\partial L}{\partial W_2}&=&\frac{1}{N_{\rm O}}2(Y_p-Y)\;H_r^T \\ \frac{\partial L}{\partial W_1}&=&\frac{1}{N_{\rm O}}\left[W_2^T\;2(Y_p-Y)\right]\;X^T \end{eqnarray*}

ここで、新たに以下の行列を導入した。

(14)    \begin{eqnarray*} X&=&\left(\vec{x}^{\;(1)},\cdots,\vec{x}^{\;(M)}\right) \\ Y&=&\left(\vec{y}^{\;(1)},\cdots,\vec{y}^{\;(M)}\right) \\ Y_p&=&\left(\vec{y}_p^{\;(1)},\cdots,\vec{y}_p^{\;(M)}\right) \\ H_r&=&\left(\vec{h}_r^{\;(1)},\cdots,\vec{h}_r^{\;(M)}\right) \end{eqnarray*}

実際のコードでは、計算効率のため、ここまでに定義した全ての行列の行と列を転置したものを扱う。後で示すコードと一致させるため、式(13)をさらに変形しておく。式(13)の両辺の転置を取って、

(15)    \begin{eqnarray*} \left(\frac{\partial L}{\partial W_2}\right)^T &=&\left(\frac{1}{N_{\rm O}}2(Y_p-Y)\;H_r^T\right)^T \\ &=&\frac{1}{N_{\rm O}}H_r\;2(Y_p-Y)^T \\ \left(\frac{\partial L}{\partial W_1}\right)^T &=&\left(\frac{1}{N_{\rm O}}\left[W_2^T\;2(Y_p-Y)\right]\;X^T\right)^T \\ &=&\frac{1}{N_{\rm O}}X \left[2(Y_p-Y)^T\;W_2\right] \end{eqnarray*}

行と列を入れ替えた行列を同じ表記のまま改めて定義し直すと次式を得る。

(16)    \begin{eqnarray*} \frac{\partial L}{\partial W_2} &=&\frac{1}{N_{\rm O}}H_r^T\;2(Y_p-Y) \\ \frac{\partial L}{\partial W_1} &=&\frac{1}{N_{\rm O}}X^T\left[2(Y_p-Y)\;W_2^T\right] \end{eqnarray*}

numpyによる実装

最初にChainerを使わずnumpyだけを用いて実装したものを示す(PyTorchの記事と同じ)。

  • 24-25行目:\vec{x}\vec{y}に適当な値を設定する。
  • 28-29行目:W_1W_2を適当な値で初期化する。
  • 32行目以降:勾配降下法を行うルーチンである。
  • 34-36行目:\vec{y}_pを計算する(forward計算)。
  • 39行目:Lを計算する。
  • 43-44行目:\frac{\partial L}{\partial W_2}を計算する(backward計算)。
  • 47-50行目:\frac{\partial L}{\partial W_1}を計算する(backward計算)。
  • 53-54行目:W_1W_2を更新する。

理論式と一対一対応していることに注意する。

chainer.Variableの導入

上のコードでは微分計算を全て書き下ろした。chainer.Variableを使うことで、微分計算を自動化することができる。書き換えの手順は以下の通りである。

  • numpy.arrayからVariable変数を作る(26,27,30,31行目)。
  • numpy.array間の演算を行う関数をchainer.functions内の関数に置き換える(35,36,37,40行目)。
  • lossを求めた(35-40行目)あと、loss.backwardを実行する(49行目)。ここで誤差逆伝播法が実行される。
  • 微分値は、w1.gradとw2.gradで得ることができる(52-53行目)。
  • 適当なタイミングでw1とw2の勾配のゼロ初期化が必要である(44-45行目)。

Variableとchainer.functionsを使うことで、背後で自動微分が実行される。

chainer.Chainの導入

オリジナルなネットワークを、chainer.Chainを継承したクラスとして実装することができる。forward計算をメソッド__call__内で定義する(34-38行目)。

chainer.optimizerの導入

ここまでの例では重みの更新式を露わに書いてきた。chainer.optimizersを使うことで、この煩雑さをなくすことできる(71行目)。 今回の最適化手法は(確率的)勾配降下法であるが(51行目)、chainer.optimizersは様々な最適化手法を提供している。

ここまでの置き換えで、ループの中の処理をかなりクリアにするこができた。

chainer.trainingの導入

ここまでの例では、勾配降下法を行うループを露わに書いてきた。chainer.trainingなどを使うことでループをなくすことができる。

上のコードは、numpyによるコードと異なり、何をするのかを宣言するコードとなっている。これが、Chainerによる抽象化の恩恵である。もちろん、これら5つのスクリプトで計算した誤差(loss)とエポック(epoch)の関係は(ほぼ)一致する。
誤差(loss)とエポック(epoch)の関係
epoch数が増えるにつれて誤差は小さくなることが分かる。

まとめ

Chainerの提供するAPIを使うことにより、深層学習の背後にある煩雑さ(誤差逆伝播法、勾配降下法をはじめとする各種最適化手法などの詳細)を隠蔽して、ネットワーク構造やその訓練過程を実装することができる。今回は触れなかったが、数行追加するだけでGPUとCPUのどちらでも動作するコードに仕立て上げることも可能である。
これまでに大変多くの深層学習フレームワークが公開されている。どれか一つに固執することなく複数のフレームワークを使うことをお薦めしたい。各フレームワークの長所・短所を知ることで深層学習についての理解も深まるためである。

Kumada Seiya

Kumada Seiya

仕事であろうとなかろうと勉強し続ける、その結果”中身”を知ったエンジニアになれる

最近の記事

  • 関連記事
  • おすすめ記事
  • 特集記事

アーカイブ

カテゴリー

PAGE TOP