ニューラルネットワークの学習では、損失関数を小さくする方向へパラメータを少しずつ動かします。そのためには 損失をすべてのパラメータで偏微分した値(勾配) が必要です。パラメータが数百万あっても、この勾配を順伝播と同程度の計算量で求められるのが バックプロパゲーション(誤差逆伝播法) です。
この記事では、記法を決めて連鎖律を適用し、4つの基本式を導きます。最後に、手計算できる数値例と NumPy による実装で確かめます。
L 層の全結合ニューラルネットワークを考えます。第 l 層(l=1,…,L)では、次の計算を行います。
z(l)a(l)=W(l)a(l−1)+b(l)=σ(z(l))| 記号 | 意味 |
|---|
| a(0)=x | 入力 |
| W(l),b(l) | 第 l 層の重み行列とバイアス |
| z(l) | 活性化関数に入る前の値(重み付き入力) |
| σ | 活性化関数(成分ごとに適用) |
| a(L)=y^ | ネットワークの出力 |
| L(y^,y) | 損失関数(y は正解) |
順伝播(実線)で信号を前へ送り、逆伝播(破線)で誤差 δ を後ろへ送る
成分で書くと zj(l)=∑kWjk(l)ak(l−1)+bj(l) です。Wjk(l) は「前の層のユニット k から、この層のユニット j への重み」を表します。
目標は、すべての l について次の2つを求めることです。
∂Wjk(l)∂L,∂bj(l)∂Lδj(l) は「ユニット j の入力 zj(l) を少し動かしたとき、損失がどれだけ変わるか」を表します。バックプロパゲーションは、この δ を 出力層から入力層へ向かって順番に 計算していく手法です。
多変数の連鎖律を思い出しておきます。L が中間変数 u1,…,un を通して v に依存しているとき、次が成り立ちます。
∂v∂L=i=1∑n∂ui∂L∂v∂uiニューラルネットワークでは、ある層の値は 次の層のすべてのユニットを通して 損失に影響します。そのため、和の形の連鎖律が自然に現れます。
aj(L)=σ(zj(L)) なので、zj(L) は aj(L) だけを通して損失に影響します。
δj(L)=∂aj(L)∂Lσ′(zj(L))ベクトルで書くと、⊙ を成分ごとの積(アダマール積)として次のようになります。
δ(L)=∇a(L)L⊙σ′(z(L))(BP1)zj(l) は、次の層のすべての zk(l+1) を通して損失に影響します。連鎖律より、次が成り立ちます。
δj(l)=k∑∂zk(l+1)∂L∂zj(l)∂zk(l+1)=k∑δk(l+1)∂zj(l)∂zk(l+1)ここで zk(l+1)=∑jWkj(l+1)σ(zj(l))+bk(l+1) なので、次のようになります。
∂zj(l)∂zk(l+1)=Wkj(l+1)σ′(zj(l))これを代入すると、次が得られます。
δj(l)=(k∑Wkj(l+1)δk(l+1))σ′(zj(l))∑kWkj(l+1)δk(l+1) は、転置行列 (W(l+1))⊤ とベクトル δ(l+1) の積の第 j 成分です。したがって、次のようにまとめられます。
δ(l)=((W(l+1))⊤δ(l+1))⊙σ′(z(l))(BP2)順伝播では W を掛けて信号を前へ送りました。逆伝播では W⊤ を掛けて誤差を後ろへ送ります。これが「逆伝播」という名前の由来です。
∂zj(l)/∂bj(l)=1 なので、次が成り立ちます。
∂bj(l)∂L=δj(l)⟺∇b(l)L=δ(l)(BP3)∂zj(l)/∂Wjk(l)=ak(l−1) なので、次が成り立ちます。
∂Wjk(l)∂L=δj(l)ak(l−1)⟺∇W(l)L=δ(l)(a(l−1))⊤(BP4)重みの勾配は「その重みの 出口側の誤差 × 入口側の活性」です。入力側のユニットがほとんど発火していなければ(ak(l−1)≈0)、その重みはほとんど学習されません。
(BP1) は、出力層の活性化関数と損失関数の組み合わせによって簡単な形になります。
| 出力層 | 損失関数 | δ(L) |
|---|
| 恒等関数 | 二乗誤差 21∥y^−y∥2 | y^−y |
| シグモイド | 二値交差エントロピー | y^−y |
| ソフトマックス | 交差エントロピー −∑iyilogy^i | y^−y |
| シグモイド | 二乗誤差 | (y^−y)⊙σ′(z(L)) |
ソフトマックスと交差エントロピーの組み合わせは、ソフトマックスが成分ごとの関数ではないので (BP1) をそのままは使えません。それでも、直接計算すると δ(L)=y^−y という非常に簡単な形になります。y^i=ezi/∑mezm より ∂y^i/∂zj=y^i(1[i=j]−y^j) なので、次のように計算できます。
∂zj∂L=−i∑y^iyiy^i(1[i=j]−y^j)=−yj+y^ji∑yi=y^j−yj最後の等号では、one-hot ベクトルなので ∑iyi=1 であることを使いました。
ここまでの4つの式をまとめておきます。
バックプロパゲーションの基本式
δ(L)δ(l)∇b(l)L∇W(l)L=∇a(L)L⊙σ′(z(L))=((W(l+1))⊤δ(l+1))⊙σ′(z(l))=δ(l)=δ(l)(a(l−1))⊤(BP1)(BP2)(BP3)(BP4)- 順伝播:a(0)=x から始め、各層の z(l),a(l) を計算して 保存 しておく。
- 出力層の誤差:(BP1) で δ(L) を求める。
- 逆伝播:l=L−1,…,1 の順に (BP2) で δ(l) を求める。
- 勾配:(BP3) と (BP4) で各層の勾配を求める。
- 更新:学習率 η で勾配降下する。
W(l)←W(l)−η∇W(l)L,b(l)←b(l)−η∇b(l)Lミニバッチ(m 個のサンプル)で損失を平均する場合は、各サンプルの勾配を平均します。
入力1つ、隠れユニット1つ、出力1つの最小のネットワークで計算してみます。
- 隠れ層:z1=w1x+b1、a1=σ(z1)(シグモイド)
- 出力層:y^=z2=w2a1+b2(恒等関数)
- 損失:L=21(y^−y)2
値は x=1、y=1、w1=0.5、b1=0、w2=2、b2=0 とします。
順伝播
z1a1y^L=0.5×1+0=0.5=σ(0.5)≈0.62246=2×0.62246+0≈1.24492=21(1.24492−1)2≈0.029993逆伝播
出力層は恒等関数と二乗誤差の組み合わせなので、δ2=y^−y≈0.24492 です。シグモイドの微分は σ′(z)=σ(z)(1−σ(z)) なので、σ′(0.5)≈0.62246×0.37754≈0.23500 となります。
∂w2∂L∂b2∂Lδ1∂w1∂L∂b1∂L=δ2a1≈0.24492×0.62246≈0.15245=δ2≈0.24492=w2δ2σ′(z1)≈2×0.24492×0.23500≈0.11511=δ1x≈0.11511=δ1≈0.11511(BP4)(BP3)(BP2)(BP4)(BP3)更新
学習率 η=0.5 で1回更新すると、パラメータは w1≈0.44244、b1≈−0.05756、w2≈1.92377、b2≈−0.12246 になります。損失は 0.029993 から約 0.000248 へ下がりました。
実装した勾配が正しいかは、数値微分(中心差分) と比べて確かめられます。
∂θ∂L≈2εL(θ+ε)−L(θ−ε)上の例で ε=10−6 として計算すると、∂L/∂w1≈0.115114、∂L/∂w2≈0.152452 となり、逆伝播の結果と一致します。
ただし、数値微分はパラメータ1つにつき損失を2回計算する必要があります。パラメータ数を P とすると、順伝播を 2P 回行うことになります。一方バックプロパゲーションは、順伝播1回と逆伝播1回(順伝播の定数倍の計算量) で全パラメータの勾配が求まります。P が数百万〜数十億になる現代のネットワークでは、この差が決定的です。
XOR(2入力の排他的論理和)を、隠れ層4ユニット(シグモイド)と出力層(ソフトマックス + 交差エントロピー)で学習します。サンプルは列ベクトルとして横に並べています。
backprop_xor.py
import numpy as np
rng = np.random.default_rng(0)
def sigmoid(z):
return 1.0 / (1.0 + np.exp(-z))
def softmax(z):
z = z - z.max(axis=0, keepdims=True) # オーバーフロー対策
e = np.exp(z)
return e / e.sum(axis=0, keepdims=True)
# データは列ベクトルを横に並べる:X は (入力次元, m)、Y は one-hot で (クラス数, m)
X = np.array([[0, 0, 1, 1],
[0, 1, 0, 1]], dtype=float)
Y = np.array([[1, 0, 0, 1], # クラス0:XOR が 0
[0, 1, 1, 0]], # クラス1:XOR が 1
dtype=float)
n_in, n_hidden, n_out = 2, 4, 2
params = {
"W1": rng.normal(0, 1, (n_hidden, n_in)),
"b1": np.zeros((n_hidden, 1)),
"W2": rng.normal(0, 1, (n_out, n_hidden)),
"b2": np.zeros((n_out, 1)),
}
def forward(p, X):
Z1 = p["W1"] @ X + p["b1"]
A1 = sigmoid(Z1)
Z2 = p["W2"] @ A1 + p["b2"]
A2 = softmax(Z2)
return A1, A2
def loss(A2, Y):
m = Y.shape[1]
return -np.sum(Y * np.log(A2 + 1e-12)) / m
def backward(p, X, Y, A1, A2):
m = X.shape[1]
d2 = (A2 - Y) / m # (BP1) softmax + 交差エントロピー
d1 = (p["W2"].T @ d2) * A1 * (1 - A1) # (BP2) σ'(z) = σ(z)(1 - σ(z))
return {
"W2": d2 @ A1.T, # (BP4)
"b2": d2.sum(axis=1, keepdims=True), # (BP3) バッチ方向に和をとる
"W1": d1 @ X.T,
"b1": d1.sum(axis=1, keepdims=True),
}
eta = 1.0
for step in range(5001):
A1, A2 = forward(params, X)
grads = backward(params, X, Y, A1, A2)
for name in params:
params[name] -= eta * grads[name]
if step % 1000 == 0:
print(f"step {step:4d} loss = {loss(A2, Y):.4f}")
_, A2 = forward(params, X)
print("予測クラス:", A2.argmax(axis=0)) # 正解は [0 1 1 0]
実行結果は次のとおりです(NumPy 2.5、乱数シード 0)。
実行結果
step 0 loss = 0.8358
step 1000 loss = 0.0085
step 2000 loss = 0.0034
step 3000 loss = 0.0021
step 4000 loss = 0.0015
step 5000 loss = 0.0012
予測クラス: [0 1 1 0]
同じコードで中心差分による勾配チェックを行うと、最大相対誤差は約 1.7×10−10 でした。逆伝播の実装が正しいことが確認できます。
損失を m 個の平均にしているので、d2 を m で割っています。こうすると (BP3) と (BP4) の和が、自動的にサンプルごとの勾配の平均になります。
- 1970年:リンナインマーが、今日のリバースモード自動微分にあたる方法を修士論文で示しました。
- 1974年:ワーボスが、博士論文でこの方法をニューラルネットワークの学習に応用することを論じました。
- 1986年:ラメルハート、ヒントン、ウィリアムズが Nature 誌の論文で、多層ネットワークが逆伝播によって有用な内部表現を学習できることを示しました。これにより手法が広く知られるようになります。
現在の PyTorch や JAX などのフレームワークは、ここで導いた計算を 計算グラフ上のリバースモード自動微分 として一般化し、自動で行っています。
- バックプロパゲーションは、連鎖律を 出力側から効率よく 適用する方法です。
- 誤差 δ(l)=∂L/∂z(l) を導入すると、すべてが4つの式 (BP1)〜(BP4) にまとまります。
- 逆伝播では W⊤ を掛けて誤差を後ろへ送り、各重みの勾配は「出口の誤差 × 入口の活性」になります。
- 計算量は順伝播の定数倍で、数値微分よりはるかに効率的です。実装したら勾配チェックで確かめましょう。