この連載では毎回、「自動で学習する」というボタンを押してきました。直線が点に吸い付き、境界線が青と赤を分け、曲がった塗り分けがXORを解く。そのたびに「誤差が減る向きへ、つまみを少しずつ回している」と説明してきましたが、肝心のことをまだ話していません。誤差が減る向き、つまり勾配を、機械はどうやって知るのか。今回はその中身をついに開けます。ここは現代のAI訓練の心臓部で、この連載でいちばん大事な回です。
測り方その1: 少し動かして、差を見る
じつは、勾配を測るだけなら誰にでもできます。知りたいのは「このつまみを少し増やしたら、誤差は増えるのか減るのか、どれくらい変わるのか」でした。なら、実際に少しだけ動かして、誤差を測り直せばいい。
ここで言葉を一つ、高校数学と接続しておきます。勾配とは傾きのことで、これは高校で習った微分の「傾き」と同じ意味です。ただし横軸が違います。時間や x の代わりに、横軸につまみの値、縦軸に誤差をとったグラフを思い浮かべてください。つまみを回すと誤差が上下する一本の曲線が描けます。その曲線の、いまのつまみ位置での傾きが勾配です。
図のとおり、左右でやっていることは同じで、軸の名前が x と y から「つまみの値」と「誤差」に変わっただけです。傾きがプラスなら、つまみを右に回すと誤差が増える(だから左へ回すべき)。マイナスなら右へ。学習に傾きが必要なのは、それが下り坂の向きを教えてくれるからでした。
式で書くと、つまみ $w$ の勾配はこう近似できます。$h$ はごく小さい数(たとえば0.0001)です。
$$\text{勾配} \approx \frac{L(w+h) - L(w)}{h} \qquad (L\text{ は誤差})$$
見覚えのある形のはずです。高校で習う微分係数の定義
$$f^{\prime}(x) = \lim_{h \to 0} \frac{f(x+h) - f(x)}{h}$$
の、$h$ を「限りなく0に近づける」代わりに小さい数で止めたものがこれです。つまりこの測り方は、微分の定義を素朴にそのまま実行しているだけ。ただの割り算です。これを数値微分と呼びます。コードにしても3行で書けます。
function numericGrad(k) { // k番目のつまみの勾配
const h = 1e-4, base = loss(P); // いまの誤差
P[k] += h; // 少しだけ動かして
const g = (loss(P) - base) / h; // 誤差の変化 ÷ 動かした量
P[k] -= h; // 元に戻す
return g;
}
これは正真正銘、ちゃんと学習できる方法です。全つまみをこの方法で測って回せば、これまでのデモと同じようにネットワークは賢くなります。素朴で、確実で、そして重大な欠点が1つあります。
問題は、遅すぎること
数値微分は、つまみ1個の勾配を測るために誤差の計算(=データ全部に対する予測)を1回余分に必要とします。つまみが N 個なら、基準の1回とあわせて N+1 回。第2回 のネットワークはつまみ13個だったので14回、これはまだ笑って済みます。
しかし本物のAIは笑えません。いまの大規模言語モデルはつまみ(パラメータ)を数十億から数兆個持っていて、学習ではそれを何百万ステップも回します。1ステップごとに数十億回の予測計算をやり直すのは、どんな計算機でも不可能です。つまみを増やせば賢くなると分かっていても、この測り方のままでは、そもそも訓練が終わらない。ディープラーニングが長いあいだ離陸できなかった理由の一つが、まさにここでした。
測り方その2: 誤差への「責任」を後ろへ配る
そこで登場するのが誤差逆伝播(バックプロパゲーション)です。発想を変えます。つまみを1個ずつつついて外から反応を見るのではなく、誤差がどの経路を通って生まれたかを、計算の流れを逆にたどって配りなおすのです。
出力の誤差は、まず出力の直前にいたつまみたちのせいです。では隠れ層のニューロンに責任はないかというと、あります。ただし全員が同罪ではなく、出力に強くつながっていた(重みの絶対値が大きかった)ニューロンほど、誤差への影響も大きかったはずです。だから、責任はそのつながりの重みを掛けて渡す。重みが大きいほど強く伝わり、重みがマイナスのつながりなら、伝わる責任の向き(符号)も反転します。責任を受け取った隠れニューロンは、こんどは自分の入力側のつまみたちへ、同じルールでまた責任を配る。
これを出力から入力まで一巡させると、すべてのつまみに「あなたはこの誤差にこれだけ加担した」という数が行き渡ります。この数こそ勾配です。かかる手間は、前向きの計算を1回と、責任配りを1回。前向きの計算とは、前回 で数字を鎖に流したあの計算、入力を左から右へ通して予測と誤差を出すところまでのことです。責任配りの「逆流」と区別するために、この普段どおりの向きを前向きと呼ぶのでした。前向き1回と逆流1回、たった一往復で、つまみが13個だろうと数十億個だろうと、全部の勾配が同時にそろいます。
ここで大事なことを言っておくと、逆伝播は数値微分と別の何かを求めているのではありません。求めているものは同じ、さきほどの「つまみに対する誤差のグラフの傾き」です。違うのは求め方だけ。誤差は「入力 → 重み付き和 → 活性化 → … → 誤差」と関数が入れ子になった合成関数なので、その傾きは、高校数学の合成関数の微分
$$\lbrace f(g(x)) \rbrace^{\prime} = f^{\prime}(g(x)) \cdot g^{\prime}(x)$$
つまり各段の傾きの掛け算で、厳密に計算できます。この規則を連鎖律と呼びます。
「責任を配る」は、この掛け算の比喩です。以下では「〜の感度」という言い方をしますが、これは「そこを少し動かすと誤差が何倍動くか」、つまりいま見た傾きのことで、新しい概念ではありません。第0回 では「傾き」をつまみの名前としても使ったので、区別のために呼び分けます(教科書では勾配・デルタ・誤差信号などと書かれます)。
そして肝心なのは、この掛け算に要る材料が、すでに全部そろっているということです。前回 見たとおり、ネットワークの中身は掛け算・足し算・活性化関数という単純な部品の鎖でした。単純な部品は、感度が公式で厳密に分かります。「$w$ を掛ける」部品なら、入力を1動かせば出力は $w$ だけ動くので、感度は $w$ の値そのもの。第1回 のシグモイドなら感度は $y(1-y)$ で、前向きで出した $y$ を代入するだけ。どの公式も「いまの値」さえあれば計算できて、その値は前向きのときに全部メモしてあります。
ここが逆伝播のいちばんの勘所です。前向きで一度通ったときのメモが残っている以上、あとは合成関数の微分に従って公式を掛け合わせるだけで、すべてのつまみの傾きが一度に出てしまう。測り直す必要は1回もありません。
図で見てみましょう。前回 とまったく同じニューロン1個で、入力は大きさ $x_1 = 2$・甘さ $x_2 = 1$、重み $w_1 = 0.5$・$w_2 = 1$、バイアス $b = -1$。そして $t$ は正解を表す数で、この果物は「りんごである」が正解なので $t = 1$ とします。誤差は予測 $y$ がこの $t$ からどれだけ離れているか、$(y-t)^2$ で測ります。
上の段が前向きで、途中の値をメモしていくところ。下の段が感度の逆流で、誤差から出発して、右から左へ公式を掛けていくだけです。数字は図で追ってください。追ってほしいのは個々の計算より、掛けている $0.20$ も $2$ も新しく測った値ではなく、上のメモから降りてきているという一点です。
出てきた $w_1$ の勾配は $-0.21$。マイナスなので、$w_1$ を少し増やせば誤差が減ると分かります。そして $w_2$ と $b$ の勾配は、途中で出た「$z$ の感度 $-0.11$」に $x_2$ と $1$ を掛けるだけで済みます。1個測るごとにネットワーク全体を計算し直していた数値微分と違い、共通の部分は一度しか計算しません。感度は幹から枝へ、枝から葉へと一度ずつ流れるだけなので、全体でちょうど一往復分で終わります。
正確さの種明かしも同じところにあります。各部品の感度は「少し動かして止めた近似」ではなく微分の公式そのものなので、厳密な数を掛け合わせている以上、答えも厳密です。
比喩を外せば、逆伝播とは「合成関数の微分を、前向き計算で保存しておいた現在値を使って、ネットワークの後ろから前へ掛け進める計算手順」のことです。
2つの測り方を、並べて確かめる
本当に同じ答えが出るのか。第2回 のXORのネットワーク(つまみ13個)で、2つの測り方を並べられるデモを用意しました。
うまく表示されないときは、デモを別タブで開く 。
まず「数値微分で測る」を押すと、13個のつまみを1個ずつつついて、オレンジの棒(勾配)がひとつずつ埋まっていきます。予測計算の回数がかさんでいくのも見えます。次に「逆伝播で測る」を押すと、青い棒が一瞬で13本全部そろいます。2色の棒はほぼぴったり重なり、最大差は0.00001以下。まったく別の手順が同じ答えに着地しますが、これは偶然ではありません。定義どおりに測るか、合成関数の微分で計算するかの違いだけで、どちらも同じ傾きを求めているのだから、一致するのが当然なのです。あとは「1ステップ進める」で、その勾配のとおりに全つまみを回せば、誤差が下がっていきます。
実際に動いているコード
これまでのデモの「自動で学習」の裏で動いていた逆伝播は、次のコードです。責任を表す変数を d と書いています。
function backprop() {
const g = new Array(13).fill(0); // 全つまみの勾配
for (const [x1, x2, t] of DATA) {
const { o, hh } = forward(x1, x2); // 前向きに1回計算
const d2 = 2 * (o - t) * o * (1 - o); // 出力の責任(誤差から出発)
g[12] += d2; // 出力のバイアスへ
for (let j = 0; j < 3; j++) {
g[9 + j] += d2 * hh[j]; // 出力側の重みへ
const d1 = d2 * v[j] * hh[j] * (1 - hh[j]); // 隠れjへ。重みv[j]とシグモイドの感度を掛けて配る
g[j*3] += d1 * x1; // 隠れjの入力側の重みへ
g[j*3+1] += d1 * x2;
g[j*3+2] += d1; // 隠れjのバイアスへ
}
}
return g.map(v => v / DATA.length); // データ全体の平均にそろえる
}
見てほしいのは d1 = d2 * v[j] * … の行です。出力の感度 d2 に、つながりの重み v[j](=掛け算部品の感度)を掛けて、隠れニューロン j へ渡していく。まさに「重みを掛けて責任を配る」が、そのまま1行になっています。途中に挟まっている o*(1-o) や hh*(1-hh) は、どちらもさきほど公式で紹介したシグモイドの感度で、代入されているのは前向き計算(forward)でメモした出力 o や隠れ層の値 hh の現在値です。
これが現代AIの心臓
PyTorch や TensorFlow といった、AI開発で実際に使われているフレームワークの中核機能は自動微分と呼ばれますが、その正体はいま見た責任配りを、どんな形のネットワークにも適用できるよう一般化したものです。大規模言語モデルの訓練も、毎ステップやっていることは、前向きに1回予測し、誤差の責任を数十億個のつまみへ逆向きに配り、全員を少しずつ回す。この一往復の繰り返しです。
ここまでで、機械が学ぶからくりの土台はひととおりそろいました。ここまでの5回を並べておきます。
- 第0回 AIは結局どうやって学んでいるのか ── 学習とは、誤差が減る向きへつまみを回すこと
- 第1回 ニューロン1個は何をしているのか ── 部品は、重みで混ぜて境界線を1本引くだけ
- 第2回 1本の線では割れない問題 ── 重ねると境界が曲がり、表現力が生まれる
- 第3回 ネットワークの中は、数字が流れる鎖 ── 前向きに計算すると、数字が左から右へ流れる
- 第4回(今回)誤差逆伝播 ── その鎖を逆にたどると、全つまみの回す向きが一往復で分かる
理屈の骨格は、もう全部手のなかにあります。次回はこの部品だけを使って、いよいよ実物を作ります。画面に手で描いた数字を、自作のニューラルネットワークに読ませてみましょう。
連載「手で動かすAI」
← 前回 ネットワークの中は、数字が流れる鎖
