Tech カテゴリのイメージ

誤差逆伝播とは?二つの重みに損失の原因を振り分ける

一つの重みなら、損失をその重みで微分すれば更新方向が分かります。では重みが何層にも並ぶニューラルネットワークでは、最後に出た誤差を途中の重みへどう届けるのでしょうか。誤差逆伝播は、出力側で分かった損失の傾きを、計算のつながりを逆にたどって各重みへ伝える方法です。

「逆伝播」という名前から、正解の数値そのものを入力まで送り返す印象を受けるかもしれません。実際に伝えるのは、各途中結果を少し変えたとき損失がどれだけ変わるかという勾配です。

二つの重みでも、誤差は最後に一つだけ

入力を x=3、二つの重みを w1=2 と w2=4 とします。途中結果 h=w1×x は6、予測 p=w2×h は24です。正解を20とし、損失を (p−20)^2 とすると16になります。

x=3 → [w1=2] → h=6 → [w2=4] → p=24 → 損失=16

損失16だけを見ても、w1 と w2 のどちらをどれだけ動かせばよいかは分かりません。そこで、最後の計算から順に傾きを求めます。

連鎖律で傾きを後ろから渡す

損失 L=(p−y)^2 を予測 p で微分すると dL/dp=2(p−y)=8 です。予測は p=w2×h なので、w2 に関する傾きは dL/dw2=(dL/dp)×h=8×6=48 になります。

前の重みへ進むには、まず途中結果 h の傾きを求めます。dL/dh=(dL/dp)×w2=8×4=32。さらに h=w1×x なので、dL/dw1=(dL/dh)×x=32×3=96 です。

dL/dp = 8
dL/dw2 = 8 × 6 = 48
dL/dh = 8 × 4 = 32
dL/dw1 = 32 × 3 = 96

この掛け算の連続が連鎖律です。出力から入力側へ進むとき、すでに求めた傾きを再利用します。重みごとに最初から全計算をやり直す必要がないのが重要です。

手計算をPythonで照合する

次のコードは標準Pythonだけで動きます。解析的に求めた勾配と、重みをわずかに動かして測った数値微分を比較します。

x, y = 3.0, 20.0
w1, w2 = 2.0, 4.0
def loss(a, b):
prediction = (a * x) * b
return (prediction - y) ** 2
h = w1 * x
p = w2 * h
dL_dp = 2 * (p - y)
dL_dw2 = dL_dp * h
dL_dh = dL_dp * w2
dL_dw1 = dL_dh * x
eps = 1e-5
numeric_w1 = (loss(w1 + eps, w2) - loss(w1 - eps, w2)) / (2 * eps)
numeric_w2 = (loss(w1, w2 + eps) - loss(w1, w2 - eps)) / (2 * eps)
print(f"予測={p:.1f}, 損失={loss(w1, w2):.1f}")
print(f"w1の勾配: 逆伝播={dL_dw1:.1f}, 数値微分={numeric_w1:.1f}")
print(f"w2の勾配: 逆伝播={dL_dw2:.1f}, 数値微分={numeric_w2:.1f}")

出力:

予測=24.0, 損失=16.0
w1の勾配: 逆伝播=96.0, 数値微分=96.0
w2の勾配: 逆伝播=48.0, 数値微分=48.0

数値微分は照合用です。小さな変化を前後で試すため、パラメータが大量にある学習で全重みに毎回行う方法としては非効率です。実際の学習では計算の履歴を使い、逆向きに勾配を伝えます。

勾配を求めることと、重みを更新することは別

ここで求めた 96 と 48 は更新後の重みではありません。勾配降下法なら、それぞれの重みから「学習率×勾配」を引きます。誤差逆伝播は更新に必要な勾配を求める段階で、更新量を決めて適用するのは最適化手法の役割です。

PyTorchでは計算のつながりを自動微分が記録し、loss.backward() で各パラメータの勾配を計算します。optimizer.step() はその勾配を使って更新します。勾配が蓄積される挙動もあるため、学習ループでは適切なタイミングで初期化します。

LLMの層はこの二つの掛け算よりはるかに複雑ですが、「順方向に予測と損失を計算し、逆方向に連鎖律で勾配を伝える」という構造は変わりません。次にトークンや埋め込みを学ぶと、LLMがどんな入力をこの計算へ渡しているのかも具体化できます。

講座の前後記事

参照資料