学習の考え方は、第4章とまったく同じ
前のレッスンでは、XORを解く重みを人間が考えて与えました。実際のニューラルネットワークでは、重みはデータから 学習 で決めます。その考え方は、第4章で線形回帰に使ったものとまったく同じです。
- 今の重みで予測する(順伝播)
- 予測と正解のズレを、損失 として1つの数値にする
- それぞれの重みについて 勾配(少し動かしたときの損失の変化の割合)を求める
- 勾配と逆向きに、学習率の分だけ重みを動かす(勾配降下法)
- これを繰り返す
XORを解く、隠れ層2つ・出力1つのネットワーク(活性化関数はシグモイド関数)には、重みとバイアスが合わせて9個あります。第4章と同じように、9個それぞれについて「0.0001だけ動かしたときの損失の変化の割合」を計算し、勾配降下法で動かしてみます。
for step in range(3000):
base = loss(params)
grad = np.zeros(9)
for i in range(9): # 9個の数値を1つずつ
q = params.copy()
q[i] += 0.0001 # ほんの少しだけ動かして
grad[i] = (loss(q) - base) / 0.0001 # 変化の割合を求める
params = params - 5.0 * grad # 勾配と逆向きに動く
このコードは、考え方が見えるように要点だけを抜き出したものです。params は9個の数値(重みとバイアス)を並べた配列、loss(params) はその数値で予測したときの損失を返す関数、np.zeros(9) は0が9個並んだ配列、.copy() は配列の複製を作る命令です。細かい書き方は読み飛ばして構いません。見てほしいのは、内側のループが第4章レッスン6の「少しだけ動かして、変化の割合を測る」とまったく同じ形をしていることです。
学習率が5.0と大きいのは、このネットワークでは勾配がとても小さくなるためです。シグモイド関数は、入力が1増えても出力は最大で0.25しか増えないので、それを2段通ると変化の割合が小さくなります。第4章で見たとおり、ちょうどよい学習率はモデルによって違い、試しながら決めます。
乱数で決めた適当な重みから始めると、損失は次のように下がっていきます。
3000回繰り返した後の出力は、4つの入力に対して [0.015, 0.987, 0.987, 0.013] です。正解 [0, 1, 1, 0] にほぼ一致しており、人間が重みを考えなくても、ネットワークが自分でXORの解き方を見つけた ことになります。
変化の割合を、掛け算でつなぐ
前のスライドの方法には、大きな弱点があります。重み1つの勾配を求めるたびに、ネットワーク全体の計算(順伝播)をやり直していることです。重みとバイアスが9個なら、1回の更新に必要な計算は「元の損失の計算1回+9個それぞれを動かした計算9回」の10回で済みます。しかし重みが1億個あれば、1回の更新だけで1億回以上の計算が必要になり、現実的な時間では終わりません。
そこで使われるのが 誤差逆伝播(ごさぎゃくでんぱ)という方法です。考え方の核は、中学で習う「割合」の掛け算です。
ある重み w を少し動かすと、損失は次のように 連鎖して 変わります。
w → z(ニューロンの合計) → 出力 → 損失
それぞれの段階の変化の割合が、次のようだったとします。
- w が1増えると、z は3増える(変化の割合 3)
- z が1増えると、出力は0.2増える(変化の割合 0.2)
- 出力が1増えると、損失は4増える(変化の割合 4)
このとき、w が1増えると損失は 3×0.2×4=2.4 増えます。つまり w の勾配は、途中の変化の割合を掛け算でつないだもの です。「Bの変化はAの変化の3倍、Cの変化はBの変化の0.2倍なら、Cの変化はAの変化の 3×0.2=0.6 倍」という、割合の考え方と同じです。
では、この掛け算を使うと、なぜ計算が速くなるのでしょうか。次のスライドで、重みが2つある例で確かめます。
後ろから計算すると、途中の結果を使い回せる
1つのニューロンに、2つの入力 x1=3 と x2=5 が入っているとします。このニューロンの合計は次の式です。
z=w1×3+w2×5+b
一次関数の傾きと同じ考え方で、w1 が1増えると z は3増え、w2 が1増えると z は5増え、b が1増えると z は1増えます。z から先は前のスライドと同じで、「z → 出力」の変化の割合が0.2、「出力 → 損失」の変化の割合が4だとします。
ここからは、レッスンの最初に見た「少しだけ動かして測る」方法ではなく、前のスライドの「変化の割合を掛け算でつなぐ」方法を使います。同じ掛け算の方法でも、計算する順番によって手間が変わります。
前から1つずつ計算すると
どの行でも、後ろ半分の 0.2×4 は同じです。同じ計算を3回繰り返しています。
後ろから計算すると
- まず出力側から、「z が1増えると、損失はいくつ増えるか」を 1回だけ 求めます。0.2×4=0.8 です。
- あとは、それぞれの重みについて「自分の変化の割合 × 0.8」を計算するだけです。w1 は 3×0.8=2.4、w2 は 5×0.8=4、b は 1×0.8=0.8 になります。
この例では、使い回して省けたのは掛け算2回だけです。しかし実際のネットワークでは、z から損失までの道のりが何層にもわたって長く、1つのニューロンに何百本もの重みがつながっています。後ろから計算すれば、長い道のりの掛け算はニューロン1つにつき1回で済み、それぞれの重みの勾配は掛け算1回で求まります。
隠れ層のニューロンについても同じです。出力層のニューロンで求めた「z が1増えると損失はいくつ増えるか」を使って、その1つ手前のニューロンの分を求め、さらにその手前へ、と出力側から入力側へ順番に計算していきます。
※細かい補足: 1つのニューロンが次の層の複数のニューロンにつながっている場合は、それぞれの道の分を足し合わせます。今は読み流して構いません。
こうして、重みがいくつあっても、全部の重みの勾配を順伝播の数倍程度の手間でまとめて求められます。損失(誤差)の情報を後ろへ伝えていくので、誤差逆伝播 と呼ばれます。
よくある誤解と振り返り
誤解:「誤差逆伝播は、勾配降下法とは別の学習方法?」
2つは役割が違い、組み合わせて使います。誤差逆伝播は「勾配を効率よく求める方法」、勾配降下法は「求めた勾配を使って重みを動かす方法」 です。学習の手順は第4章のときから変わっておらず、「勾配を求める」部分を速くしたのが誤差逆伝播です。
実際の変化の割合は、場所によって変わる
前の2枚のスライドでは分かりやすくするため、変化の割合を3や0.2のような決まった数にしました。実際のネットワークでは、シグモイド関数のように曲がった関数を通るため、変化の割合はその時点の値によって変わります(放物線の傾きが場所によって違ったのと同じです)。誤差逆伝播は、今いる場所での変化の割合を使って計算します。
この「変化の割合を掛け算でつなぐ」考え方は、高校・大学の数学では 連鎖律(微分の連鎖律)と呼ばれます。PyTorchなどのライブラリは、この計算を自動で行ってくれます(自動微分)。
振り返り
- ニューラルネットワークの学習も、損失を勾配降下法で小さくするという点で第4章と同じ
- 勾配は「変化の割合」として1つずつ求められるが、重みが多いと計算が膨大になる
- 誤差逆伝播は、途中の変化の割合を後ろから掛け算でつなぎ、全部の重みの勾配を効率よく求める
- 速くなる理由は、「このニューロンの合計が1増えると損失はいくつ増えるか」を出力側から1回ずつ求め、手前の重みで使い回すから