17 DeepLearning 誤差逆伝播法の実装

17.1 ニューラルネットワークの学習の全体図

学習手順は下記のようになる。

前提

ニューラルネットワークは、適応可能な重みとバイアスがあり、この重みとバイアスを訓練データに適応するように調整することを「学習」と呼ぶ。ニューラルネットワークの学習は次の4つの手順で行う。

ステップ1(ミニバッチ)

訓練データの中からランダムに一部のデータを選び出す。

ステップ2(勾配の算出)

各重みパラメータに関する損失関数の勾配を求める。

ステップ3(パラメータの更新)

重みパラメータを勾配方向に微小量だけ更新する。

ステップ4(繰り返す)

ステップ1、ステップ2、ステップ3を繰り返す。

誤差逆伝播法を使用するのは、ステップ2の勾配の算出である。数値微分のみを使用する順方向の勾配の算出法は簡単に実装できる反面、計算に多くの時間がかかる。よって、誤差逆伝播法を用いる。

17.2  誤差逆伝播法に対応した
ニューラルネットワークの実装

TwoLayerNet クラスのインスタンス変数

インスタンス変数 説明
params ニューラルネットワークのパラメータを保持するディクショナリ変数。params[‘W1’]は1層目の重み、params[‘b1’]は1層目のバイアス。
params[‘W2’]は2番目の重み、params[‘b2’]は2層目のバイアス。
layers ニューラルネットワークのレイヤを保持する順番付きディクショナリ変数。layers[‘Affine1’],layers[‘Relu1’],layers[‘Affine2’]といったように順番付きディクショナリで各レイヤを保持する。
lastLayer ニューラルネットワークの最後のレイヤ。この例では、SoftMaxWithLossレイヤ。

TwoLayerNet クラスのメソッド

メソッド 説明
__init__(self,input_size, hidden_size, output_size, weight_init_std) ニューラルネットワークのパラメータを保持するディクショナリ変数。params[‘W1’]は1層目の重み、params[‘b1’]は1層目のバイアス。
params[‘W2’]は2番目の重み、params[‘b2’]は2層目のバイアス。
predict(self, x) ニューラルネットワークのレイヤを保持する順番付きディクショナリ変数。layers[‘Affine1’],layers[‘Relu1’],layers[‘Affine2’]といったように順番付きディクショナリで各レイヤを保持する。
loss(self, x, t) 損失関数の値を求める。引数のxは画像データ、tは正解ラベル
accuracy(self, x, t) 認識精度を求める。
numerical_gradient(self, x, t) 重みパラメータに対する勾配を数値微分によって求める。(順伝播 参考として載せるのみで使用しない。)
gradient(self, x, t) 重みパラメータに対する勾配を誤差逆伝播法によって求める。

OrderedDict()についてはPythonのOrderedDictの使い方を参照。

two_layer_net.py

train_neuralnet.py

出力は下図のようになる。

こちらのgithubにフルコードがあります。

マリア様の実装にむけて 日本語の問題

ただのチャットソフトを作るのに、何故このように大量の知識が必要なのか?

たとえば、次の英文があるとする。

We can get an idea of the quality of the learned feature vectors by displaying them in a 2-D map.

(2Dマップを見せることで、学習したベクトルの特徴の品質のアイデアを得ることができる。(曖昧))

下表のように分けられ、次の単語が予想できる。

単語 We can get the learned ?
入力 x1 x2 x3 x^(t-1) x^t x^(t+1)
出力 y1 y2 y^(t-2) y^(t-1) y^t

英語の場合、このように単語と単語の間は”必ず”空白がある。しかし、日本語はそうではない。

入力「こんにちは。今日はいい天気ですね。」

これをどうするのか?

どうやってわけるのか? しかし、そんなことはすでに先人達がやっていた。

意外にあった!?日本語の形態素解析ツールまとめ

python であればjanomeがよさそうである。
これによって、分けられるはずである。(まだやっていない)

処理手順は以下のようになるだろう。

①日本語入力「今日は雨だ。洗濯物が干せない」
②入力をjanomeにかける。
③「今日、は、雨、だ。洗濯物、が、干せ、ない」 のように分けられる
④RNNの誤差逆伝播法と確率的勾配降下法によって、入力に対して学習をさせる(曖昧)

この入力に対して、同じ回答を毎回繰り返すのは要求仕様とは違う。(M$のりんねと同等になってしまう)

なので、回答が複数ひつようである。さらに、ソフト使用者が回答に対して、点数をつけなくてはいけないかもしれない。いいね!かよくないね!のどちらかであろう。

入力に対して、マリア様は、”雨であること”と”洗濯物が干せない”というネガティブなソフト使用者の発言に対して、それに関連した回答をしなくてはいけない。

「ああ、雨はゅぅぅっだわ」が解の人もいるだろうし、「明日は晴れますよ!きっと!」が解の人もいるだろう。

「そういえば田代まさしさんって、昔シャネルズでしたね」が解の人もいるだろう。

すべて田代まさしさんに関する回答で学習させると、過学習がおきるだろうし、その状態のマリア様を元に戻すのはかなりしんどいだろう。ふとした会話で「そういえばTIMESの表紙に乗った時…」などと返されては、昔の男のことは忘れろ!となるかもしれない。

学習については、さらに再考を要する。

16 Deep Learning 逆伝播 2

16.3  リンゴの例

最初に、リンゴなどを買う例の計算グラフを紹介した。リンゴのみに着目すると、
下図のようになる。

この場合の変数は、リンゴの値段、リンゴの個数、消費税である。これら3つの変数それぞれが、最終的な支払金額にどのように影響するかということである。左端の3つの灰色の数字が最終的な支払金額220にどのように影響するか、を表す。「支払金額のリンゴの値段に関する偏微分」、「支払金額のリンゴの個数に関する偏微分」、「支払金額のリンゴの消費税に関する偏微分」を求めることに相当する。これを計算グラフの逆伝播を使って解くと上図のようになる。すなわち、「支払金額のリンゴの値段に関する偏微分」は2.2、「支払金額のリンゴの個数に関する偏微分」は110、「支払金額のリンゴの消費税に関する偏微分」は200である。消費税のスケールとリンゴの値段と個数のスケールは違うので、消費税の影響を表す値が大きくなっている。(消費税の1は100%、リンゴの個数、値段は1個、1円)

前述した通り、乗算ノードの逆伝播では入力信号と”ひっくり返った”積が下流へ流れる。加算ノードはそのまま下流へ流れる。暇な方は下図の空欄を埋めてほしい。

16.4  単純なレイヤの実装

この上のリンゴの買い物の例をPythonで実装する。ここでは、計算グラフの乗算ノードを「乗算レイヤ(MulLayer)」、加算ノードを「加算レイヤ(AddLayer)」という名前で実装する。

16.4.1  乗算レイヤの実装

レイヤはforward()とbackward()という共通のメソッド(関数あるいはインターフェース)を持つように実装する。forward()は順伝播(順方向の伝播)、backward()は逆伝播ん(逆方向の伝播)に対応する。乗算レイヤは下記のように実装できる。

__init__(self)は他言語のコンストラクタに対応し、インスタンス変数の初期化を行う。コンストラクタやインスタンスとは何かとはプログラミングに興味があれば調べてほしい。
forward()では、x、yの2つの引数を受け取り、それらを乗算して出力する。一方、backward()では上流から伝わってきた微分、あるいは信号(dout: d out)に対して、順伝播の”ひっくり返した値”を乗算して下流に流す。
このクラスを使って、下図を実装する。

16_simplelayer.pyとして上記のコードを保存する。その後、コマンドプロンプトで実行すると下記の出力が得られる。

$python 16_simplelayer.py

上図の灰色の数字とdで始まる変数の値が一致していることに注目されたい。

16 Deep Learning 逆伝播

逆伝播とはなにか?なんのために存在するのか?と疑問に思うのは当然である。
結論から言うと、順伝播のほかに、逆伝播が、勾配の更新に必要だからである。
順伝播だけだと、計算量が異常に増えるため、逆伝播が必要だという認識のもと、
この写経を読んでいただきたい所存である。

16.1 加算ノードの逆伝播

数式z=x+yに対して、逆伝播を考える。z=x+yの微分を解析的に計算すると以下のようになる。

\[ \begin{array}{l} \frac{\partial z}{\partial x} = 1 \\ \frac{\partial z}{\partial y} = 1 \tag{16.1} \end{array} \]

上式(16.1)の通り、dz/dxとdz/dyは、ともに1となる。

計算グラフで表すと下図のようになる。

この例では、上流から伝わった微分をdL/dzとしたが、最終的にLを出力する大きな計算グラフを想定している。

分かりにくいので具体例を示す。10+5=15という計算があるとして、上流から1.3の値が流れてくるとする。これを計算グラフで書くと下図のようになる。

加算ノードの逆伝播は入力信号を次のノードへ出力するだけなので、上図のように1.3をそのまま次の(逆方向の)ノードへ流す。

16.2 乗算ノードの逆伝播

乗算ノードの逆伝播について説明する。ここでは、z=xyという式を考える。この式の解析的な微分は、次式(16.2)で表される。

\[ \begin{array}{l} \frac{\partial z}{\partial x} = y \\ \frac{\partial z}{\partial y} = x \tag{16.2} \end{array} \]

乗算の逆伝播の場合は、上流の値に、順伝播の際の入力信号を”ひっくり返した値”を乗算して下流へ流す。

乗算の逆伝播は、入力信号をひっくり返した値を乗算するので、1.3*5 = 6.5、
1.3*10=13とそれぞれ計算できる。

加算の逆伝播では、上流の値をただ下流に流すだけだったので、順伝播の入力信号の値は必要なかったが、一方乗算の逆伝播では、順伝播のときの入力信号の値が必要になる。このため、乗算ノードの実装時には、順伝播の入力信号を保持する。

 

15 Deep Learning 連鎖律 計算グラフの逆伝播

15.1  計算グラフの逆伝播

上図に示すように、逆伝播の計算手順は信号Eに対して、ノードの局所的な微分dy/dxを乗算し、それを次のノードへ伝達していく。
ここで言う、局所的な微分とは順伝播でのy=f(x)の微分dy/dxを求めることになる。
y=f(x)=x^2 のとき dy/dx = 2xになる。
そして、この局所的な微分を上流から伝達された値(上図ではE)に乗算して、前のノードへ渡していく。

これが、逆伝播で行う計算手順だが、この計算を行うことで、目的とする微分の値を効率よく求めることができる。これは後述する連鎖律の原理から説明できる。

15.2 連鎖律

合成関数についてまず説明する。合成関数とは複数の関数によって、構成される関数のことである。たとえば、

\[z = (x+y)^2 \]

上式は式(15.1)のように、2つの式で構成される。

\[ \begin{array}{l} z = t^2 \\ t = x+y \tag{15.1} \end{array} \]

連鎖律とは合成関数の微分についての性質であり、次のように定義される。

ある関数が合成関数で表される場合、その合成関数の微分は、合成関数を構成するそれぞれの関数の微分の積によって表すことができる。

難しく見えるが、単純に言うと分数の積と同様に計算ができるということである。

dz/dx(xに関するzの微分)はdz/dt(tに関するzの微分)とdt/dx(xに関するtの微分)の積によって表すことができる。

\[ \frac{\delta z}{\delta x} = \frac{\delta z}{\delta t} \frac{\delta t}{\delta x} \tag{15.2} \]

上図のように、分数の積の計算のようにできる。

式(15.2)の微分dz/dxを求めるときは、まず局所的な微分(偏微分)を求める。

\[ \begin{array}{l} \frac{\delta z}{\delta t} = 2t \\ \frac{\delta t}{\delta x} = 1 \tag{15.3} \end{array} \]

よって、最終的に求めたいdz/dxは式(15.3)で求めた微分の積によって計算できる。

\[ \frac{\delta z}{\delta x} = \frac{\delta z}{\delta t} \frac{\delta t}{\delta x} = 2t・1 = 2(x + y) \tag{15.4} \]

15.3  連鎖律と計算グラフ

**2というノードは2乗を表すとする。x+y=tとして順方向に伝播する。さらにt^2 = z と順方向に伝播する。dz/dz は先ほどは省略していたが、連鎖律の原理により、dz/dz=1である。上図で注目すべきは、一番左の逆伝播の結果である。

dz/dz dz/dt dt/dx = dz/dx となり、「xに関するzの微分」に対応する。

上図に式(15.3)を代入すると下図のようになる。

dz/dx = 2t = 2(x+y) となる。