誤差逆伝播法(バックプロパゲーション)

2024/10/26に公開

誤差逆伝播法(Backpropagation)

誤差逆伝播法(Backpropagation)は,ニューラルネットワークの学習において,重みとバイアスを更新するための効率的な方法です.このアルゴリズムは,ネットワークの出力と期待される出力との間の「誤差」を計算し,その誤差を逆方向に伝播させて,各層のパラメータの勾配を求めます.

以下では,具体的な数式とPythonコードを用いて,誤差逆伝播法の仕組みを詳しく解説します.

誤差逆伝播法と数式

まずはシンプルな,「入力層」「隠れ層」「出力層」のニューラルネットワークを考えます.

  • 入力層:\mathbf{x}
  • 隠れ層:\mathbf{h} = f(\mathbf{W}^{(1)}\mathbf{x} + \mathbf{b}^{(1)})
  • 出力層:\mathbf{\hat{y}} = g(\mathbf{W}^{(2)}\mathbf{h} + \mathbf{b}^{(2)})

ここで,\mathbf{W}\mathbf{b}はぞれぞれ重みとバイアスのベクトル(数字の集合)を表し,右肩の数字(1)は「入力層→隠れ層」(2)は「隠れ層→出力層」の経路ということを示しています.fは隠れ層の活性化関数,gは出力層の活性化関数です.代表的な活性化関数については別記事でまとめています.(https://zenn.dev/minoda_kohei/articles/64e41b2c6d5be3)

順伝播(フォーワードパス)

ニューラルネットのシンプルな順伝播を具体的に見ると以下のように分解できます.

  • 入力層:\mathbf{x}
  • 隠れ層への入力:\mathbf{z}^{(1)} = \mathbf{W}^{(1)} \mathbf{x} + \mathbf{b}^{(1)}
  • 隠れ層の出力:\mathbf{h} = f(\mathbf{z}^{(1)})
  • 出力層への入力:\mathbf{z}^{(2)} = \mathbf{W}^{(2)} \mathbf{h} + \mathbf{b}^{(2)}
  • 出力層の出力:\hat{\mathbf{y}} = g(\mathbf{z}^{(2)})

\hat{\mathbf{y}}がモデルの予測値となります.

損失関数(Loss Function)

「損失関数」そのものの具体的な説明はこの記事(https://zenn.dev/minoda_kohei/articles/9a92faa9c8955c) に任せて,今回は二乗誤差を採用します.

L = \frac{1}{2} \| \hat{\mathbf{y}} - \mathbf{y} \|^2

ここで,\mathbf{y}は正解ラベルです.

逆伝播(バックワードパス)

損失関数Lを各パラメータで微分し,勾配を求めます.

まず損失関数の各入力\mathbf{z}^{(1)}, \mathbf{z}^{(2)} に関する勾配を\deltaを用いて以下のように設定します.(厳密な解析解を求めるには複雑な式変形が必要ですが,Pythonでは既存のモジュールを使用することで求めることができるため,厳密な解析解はまた今度記事にします.)

\delta^{(1)} = \frac{\partial L}{\partial \mathbf{z}^{(1)}}
\delta^{(2)} = \frac{\partial L}{\partial \mathbf{z}^{(2)}}

第一層について,微分の連鎖律を用いて少し変形し,損失関数Lのパラメーター\mathbf{W}^{(1)}, \mathbf{b}^{(1)}に関する勾配を求めます.

\frac{\partial L}{\partial \mathbf{W}^{(1)}} = \frac{\partial L}{\partial \mathbf{z}^{(1)}} \cdot \frac{\partial \mathbf{z}^{(1)}}{\partial \mathbf{W}^{(1)}} = \delta^{(1)} \mathbf{x}^T
\frac{\partial L}{\partial \mathbf{b}^{(1)}} = \frac{\partial L}{\partial \mathbf{z}^{(1)}} \cdot \frac{\partial \mathbf{z}^{(1)}}{\partial \mathbf{b}^{(1)}}

\mathbf{x}に関して転置を作用させていますが,転置をしなかった場合,列ベクトル\delta^{(1)}と列ベクトル\mathbf{x}のスケールが合わないベクトル積となってしまうので注意してください.

第二層についても同様に計算します.

\frac{\partial L}{\partial \mathbf{W}^{(2)}} = \frac{\partial L}{\partial \mathbf{z}^{(2)}} \cdot \frac{\partial \mathbf{z}^{(2)}}{\partial \mathbf{W}^{(2)}} = \delta^{(2)} \mathbf{h}^T
\frac{\partial L}{\partial \mathbf{b}^{(2)}} = \frac{\partial L}{\partial \mathbf{z}^{(2)}} \cdot \frac{\partial \mathbf{z}^{(2)}}{\partial \mathbf{b}^{(2)}}

以上によって計算された各勾配と学習率\etaを用いて各パラメータを更新します.

\mathbf{W}^{(1)} \leftarrow \mathbf{W}^{(1)} - \eta \frac{\partial L}{\partial \mathbf{W}^{(1)}}
\mathbf{b}^{(1)} \leftarrow \mathbf{b}^{(1)} - \eta \frac{\partial L}{\partial \mathbf{b}^{(1)}}
\mathbf{W}^{(2)} \leftarrow \mathbf{W}^{(2)} - \eta \frac{\partial L}{\partial \mathbf{W}^{(2)}}
\mathbf{b}^{(2)} \leftarrow \mathbf{b}^{(2)} - \eta \frac{\partial L}{\partial \mathbf{b}^{(2)}}

これによって予測値と正解ラベルの「誤差」から各パラメータを更新することができました.

Pythonによる実装

以下に,単純な二層ニューラルネットワークの誤差逆伝播法をPythonで実装した例を示します.

import numpy as np

# 活性化関数とその導関数
def sigmoid(z):
    return 1 / (1 + np.exp(-z))

def sigmoid_derivative(z):
    return sigmoid(z) * (1 - sigmoid(z))

# パラメータの初期化
np.random.seed(0)
input_size = 2
hidden_size = 2
output_size = 1

W1 = np.random.randn(hidden_size, input_size)
b1 = np.zeros((hidden_size, 1))
W2 = np.random.randn(output_size, hidden_size)
b2 = np.zeros((output_size, 1))

# トレーニングデータ(例)
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]]).T  # 入力データ
Y = np.array([[0, 1, 1, 0]])  # XOR問題のラベル

# ハイパーパラメータ
learning_rate = 0.1
epochs = 10000

# トレーニングループ
for epoch in range(epochs):
    # 順伝播
    Z1 = np.dot(W1, X) + b1
    A1 = sigmoid(Z1)
    Z2 = np.dot(W2, A1) + b2
    A2 = sigmoid(Z2)

    # 損失の計算
    loss = np.mean((A2 - Y) ** 2)

    # 逆伝播
    dZ2 = (A2 - Y) * sigmoid_derivative(Z2)
    dW2 = np.dot(dZ2, A1.T) / X.shape[1]
    db2 = np.sum(dZ2, axis=1, keepdims=True) / X.shape[1]

    dA1 = np.dot(W2.T, dZ2)
    dZ1 = dA1 * sigmoid_derivative(Z1)
    dW1 = np.dot(dZ1, X.T) / X.shape[1]
    db1 = np.sum(dZ1, axis=1, keepdims=True) / X.shape[1]

    # パラメータの更新
    W2 -= learning_rate * dW2
    b2 -= learning_rate * db2
    W1 -= learning_rate * dW1
    b1 -= learning_rate * db1

    # 進捗の表示
    if epoch % 1000 == 0:
        print(f"Epoch {epoch}, Loss: {loss}")

# テスト
Z1 = np.dot(W1, X) + b1
A1 = sigmoid(Z1)
Z2 = np.dot(W2, A1) + b2
A2 = sigmoid(Z2)
print("Predictions:")
print(A2)

実行結果

Epoch 0, Loss: 0.2772226743503602
Epoch 1000, Loss: 0.2394590439446847
Epoch 2000, Loss: 0.22243462354320356
Epoch 3000, Loss: 0.2049611513259233
Epoch 4000, Loss: 0.1889131359501558
Epoch 5000, Loss: 0.17595266619727007
Epoch 6000, Loss: 0.16604337947664854
Epoch 7000, Loss: 0.15856117551800525
Epoch 8000, Loss: 0.1528860272321115
Epoch 9000, Loss: 0.14853441549571722
Predictions:
[[0.13084326 0.5080402  0.82550843 0.53945658]]

果たして人間のニューロンはこのようなことをしているのだろうか,,,

LIFe Is LIKe A BOAt

Discussion