畳みこみニューラルネットを0から実装する (第2回)

2 Comments

こんにちはtatsyです。

畳みこみニューラルネットを0から実装する記事の第2回です。前回の第1回ではMNISTから画像とラベルのデータを読み込みました。第1回の記事のご確認は以下のリンクからお願いします。

畳みこみニューラルネットを0から実装する (第1回)
 

第2回の記事の内容


第2回となる今回はシグモイド関数を使った通常のニューラルネットについて解説します。

今回の記事のポイントはニューラルネットの考え方と学習に使われるバック・プロパゲーション (back propagation) というアルゴリズムです。
 

ニューラルネットの概要


詳しい解説はその他の専門書にゆずるとして、ニューラルネットとはベクトル値ベクトル関数 (ベクトルをとってベクトルを返す関数) の合成だと考えることができます。

例えばMNISTの例ならニューラルネットの入力は画像で、出力はラベルです。

入力の画像は画素値を要素として画素数と同じだけの要素数を持つベクトルとみなすことができます。出力のラベルは(学習の都合上ですが)十次元のベクトルとみなせます。正解は0から9までの数字ですから、正解をxとするとx番号目の要素だけが1でそれ以外が0の十次元ベクトルです。

関数の合成だ、といったのはニューラルネットが複数のレイヤーから構成されるためです。ここでいうレイヤーからレイヤーへの遷移が上で述べたベクトル値ベクトル関数というわけです。

つまり、入力層、隠れ層、出力層からなる三層のニューラルネットの場合には入力層から隠れ層への関数と隠れ層から出力層への関数という2つの関数の合成になります。
 

ニューラルネットの学習


ニューラルネットはベクトル値ベクトル関数の合成だと述べましたが、ネットワーク全体もまたベクトル値ベクトル関数とみなせます。

ですから、ニューラルネットの学習は学習の画像をニューラルネットに入れたときの出力と与えられた学習用ラベルの差が小さくなれば良いということになります。

さて、ここからは隠れ層を1つもつ三層のニューラルネットを考えます。今、入力のベクトルを\mathbf{x}とします。

まず、入力層から隠れ層への関数をf_1とします。すると、隠れ層で受け取る出力は、

\mathbf{h} = f_1(\mathbf{x})

です。

同じように隠れ層から出力層への関数をf_2とすると出力層で受け取るベクトルは、

\mathbf{y} = f_2(\mathbf{h}) = f_2(f_1(\mathbf{x}))

となります。

ということは、単純に右辺と左辺の差をとって、

 E = \frac{1}{2} \| \mathbf{y} - f_2(f_1(\mathbf{x})) \|^2

を最小化すればよさそうです。


とはいえ、今回学習するのはニューラルネットのパラメータなので、Eをこれらのパラメータについて最小化する必要があります。

そこで、関数f_1f_2をもう少し具体的な形で書き下してみます。

細かい説明は省くとして、今回はシグモイド関数\sigma(\mathbf{x})を使って、

 f_i (\mathbf{x}) = \sigma(\mathbf{W}_i \mathbf{x} + \mathbf{b}_i)

念のため補足ですが、シグモイド関数は通常、

 \sigma(x) = \frac{\displaystyle 1}{\displaystyle 1 + \exp (x)}

のような形をした関数です。上でベクトルをとるように書いたものはベクトルの各要素に対して、上記の関数を適用したものだと考えてください。


あらためてニューラルネットの関数を書き直してみると、

 \mathbf{y} = \sigma(\mathbf{W}_2 \sigma(\mathbf{W}_1 \mathbf{x} + \mathbf{b}_1) + \mathbf{b}_2)

となります。これを使って、先ほどのコスト関数E\mathbf{W}_i\mathbf{b}_iで偏微分して、それが0になるようにすれば良さそうです。

実際の最適化は、最急降下法というアルゴリズムを使うことにします。

オリジナルの畳みこみニューラルネットの論文[LeCun98]ではルーベンバーグ・マルカート法 (Levenberg Marquardt method) という、もう少し発展的なアルゴリズムを使っているのですが、実装が複雑になるので、今回はシンプルな最急降下法でやることにします (最急降下法についてはWikipediaなどを参照してください)。

というわけで上記のエネルギー関数をレイヤーが上のものから順に微分していきます。まずは\mathbf{W}_2です。

 \frac{\displaystyle \partial E}{\displaystyle \partial \mathbf{W}_2} = \frac{\displaystyle \partial}{\displaystyle \partial \mathbf{W}_2} \left( \frac{1}{2} \| \mathbf{y} - \sigma(\mathbf{W}_2 \mathbf{h} + \mathbf{b}_2) \|^2 \right) = \left( (\mathbf{y} - \sigma(\mathbf{W}_2 \mathbf{h} + \mathbf{b}_2)) \circ \sigma'(\mathbf{W}_2 \mathbf{h} + \mathbf{b}_2) \right) \mathbf{h}^T

なお、この式での\circはベクトルの要素ごとの積を表します。続いて\mathbf{b}_2です。

 \frac{\displaystyle \partial E}{\displaystyle \partial \mathbf{b}_2} = \frac{\displaystyle \partial}{\displaystyle \partial \mathbf{b}_2} \left( \frac{1}{2} \| \mathbf{y} - \sigma(\mathbf{W}_2\mathbf{h} + \mathbf{b}_2) \|^2 \right) = ((\mathbf{y} - \sigma(\mathbf{W}_2 \mathbf{h} + \mathbf{b}_2)) \circ \sigma'(\mathbf{W}_2 \mathbf{h} + \mathbf{b}_2)

この様な形で、一番上のレイヤーに関する微分は簡単にもとまります。ですが、これを下へ下へと微分していくのはなかなか骨が折れそうです。

ただし、この場合には実は下のレイヤーでのパラメータでの偏微分は上のレイヤーでのパラメータの偏微分の一定の関係を持っており、この関係を用いて各レイヤーでの偏微分を簡単に計算しようというのがバック・プロパゲーションの考え方です。

詳細な説明はPRML本 (外部ページに飛びます)などに譲りますが、実際、

 \boldsymbol\delta_1 = \left( \mathbf{y} - \sigma(\mathbf{W}_2 \mathbf{h} + \mathbf{b}_2) \right) \circ \sigma'(\mathbf{W}_2 \mathbf{h} + \mathbf{b}_2)

と置くと、コスト関数E\mathbf{W}_1および\mathbf{b}_1での偏微分は、

 \frac{\displaystyle \partial E}{\displaystyle \partial \mathbf{W}_1} = \left( ( \mathbf{W}_2^T \boldsymbol\delta_1 ) \circ \sigma'(\mathbf{W}_1 \mathbf{x} + \mathbf{b}_1) \right) \mathbf{x}^T

 \frac{\displaystyle \partial E}{\displaystyle \partial \mathbf{b}_1} = \left( ( \mathbf{W}_2^T \boldsymbol\delta_1 ) \circ \sigma'(\mathbf{W}_1 \mathbf{x} + \mathbf{b}_1) \right)

となります。

今回の実装では、各レイヤーに自分のレイヤーの入力と出力だけを持たせる都合上、各レイヤーの出力をシグモイド関数の微分に通す部分をはじいたものをerrという変数にして、次のように実装しています。

このソースコードで、etaという変数は繰り返し計算ごとのステップ幅を表しています。続いてmomentumという変数ですが、これは次に説明するバッチ学習という方法による学習の分散を和らげるために使う変数です。

とりあえずのイメージとしては、最急降下方向が急に変わらないようにmomentumを使って、前の繰り返し計算での最急降下方向と今の繰り返し計算で得られた勾配との線形和を取っているという感じです。

このあたりのバック・プロパゲーションがニューラルネットの一番ややこしい部分でもありますが、ネットを検索すると結構良い解説が多数見つかると思うので、そちらを見つつソースコードを眺めていただければ嬉しいです。
 

バッチ学習


最も単純には、レイヤーの持つパラメータ (現在の場合は\mathbf{W}\mathbf{b}) を更新するときの勾配は、全ての学習データに対して計算される勾配の平均を使うことになります。

ですが、このやり方は、第一に勾配を計算するのに時間がかかりますし、それほどパラメータ更新の効率もよくありません。

そこで、入力の学習データをいくつかに分けて(その1つ1つをバッチと呼ぶ)、バッチごとに勾配を求めるということをします。

この方法は、学習データをどういう風にバッチに分けるかで多少、学習の効率が変化するのですが、学習データが十分に多く、バッチに含まれるデータに十分なランダム性があれば、学習がうまくいきます。

実際には、バッチ内にどのようにデータが含まれるかに影響をうけるため、たまたま偏ったデータがバッチに多く含まれた場合の影響を少なくするためにモーメンタム(momentum)というものを使います。

モーメンタムは直前の繰り返し計算でパラメータの更新に使った勾配と新しく求まった勾配から、新しい勾配方向を決定する際の重みのようなものです。

上記のソースコードからもわかるように、勾配更新のステップ幅を\eta、モーメンタムを\mu、t回目の繰り返し計算でパラメータ更新に用いる勾配を\Delta_t、現在のバッチから求まる勾配を\deltaとして、

 \Delta_{t+1} = \mu \Delta_{t} + \eta \delta

という風に勾配方向を更新します。
 

実験


それでは、MNISTからとってきたデータを実際に学習してみます。

MNISTに含まれる画像は28×28の大きさですので、入力は784次元のベクトルとみなせます。

一方で、出力されるものは、10次元のベクトルで、入力画像が表す数字に対応する次元だけが1になっています。

そこで、今回は入力層が784次元、隠れ層が300次元、出力層が10次元の三層ニューラルネットで学習を行います。

 

私の手元のマシン(Corei7 3.6GHz, 16GB RAM)で走らせたところ、およそ6分学習させて、94%の正解率がでました。

学習時間が少ないことを考えれば普通のニューラルネットでも十分な正解率が出ていることが分かります。

ちなみに学習の際にはMNISTの60000個のデータを50個ずつのバッチに切って、20回ずつ学習を行いました。

neural_result

 

まとめ


今回は畳み込みニューラルネットに行く前段階として普通のニューラルネットで学習を行ってみました。

前回と同じく、実験でつかったコードは私のGitHubからご覧いただけます (随時更新中)。

https://github.com/tatsy/educnn

次回は畳み込みニューラルネットの実装について解説と実験を行いたいと思います。

今回も最後までお読み頂き、ありがとうございました。
 

参考文献


[LeCun98] Y. LeCun et al., “Gradient-based learning applied to document recognition.” Proceedings of the IEEE, vol.86, No.11, pp.2278-2324, 1998.

2 thoughts on “畳みこみニューラルネットを0から実装する (第2回)

  1. 機械学習に興味がありこのサイトを見させていただいております。
    gitにあるファイルで実行してみたところEpochの値が最初の一回以外nan、-nan(ind)となってしまいます。どうしてこのようになってしまうのか教えていただきたいです。

    • ご興味をお持ちいただきありがとうございます。
      いただいた情報だけだと何が問題かは分かりかねるのですが、先ほど自分の環境 (Windows 10, 64bit) でテストをしたところ問題なく動作をしておりました。

      もう少し、動作を試みた環境や試した手順などをお話しいただければ、的確な助言をさせていただけるかもしれません。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です