深層学習の学習プロセスは、順伝播(Forward Propagation) と 誤差逆伝播(Backpropagation) の2つのフェーズで構成されます。順伝播で予測値を計算し、誤差逆伝播で損失関数に対する各パラメータの勾配を計算して、勾配降下法でパラメータを更新します。
本記事では、全結合ニューラルネットワークの順伝播と誤差逆伝播を行列形式で丁寧に導出し、Pythonでスクラッチ実装します。
本記事の内容
- 順伝播の行列形式での定式化
- 誤差逆伝播法の連鎖律による勾配導出
- 各層のパラメータに対する勾配の計算
- Pythonでのスクラッチ実装
前提知識
この記事を読む前に、以下の概念を押さえておくと理解が深まります。
- 微分の連鎖律(合成関数の微分)
- 行列の微分
順伝播の行列形式
$L$ 層のニューラルネットワークを考えます。第 $l$ 層の順伝播は以下の通りです。
$$ \bm{z}^{(l)} = \bm{W}^{(l)}\bm{a}^{(l-1)} + \bm{b}^{(l)} $$
$$ \bm{a}^{(l)} = g^{(l)}(\bm{z}^{(l)}) $$
ここで、
- $\bm{a}^{(l-1)} \in \mathbb{R}^{n_{l-1}}$: 第 $l-1$ 層の出力($\bm{a}^{(0)} = \bm{x}$: 入力)
- $\bm{W}^{(l)} \in \mathbb{R}^{n_l \times n_{l-1}}$: 重み行列
- $\bm{b}^{(l)} \in \mathbb{R}^{n_l}$: バイアスベクトル
- $g^{(l)}$: 活性化関数
- $\bm{z}^{(l)}$: 活性化前の値(pre-activation)
- $\bm{a}^{(l)}$: 活性化後の値(post-activation)
ミニバッチの場合
ミニバッチサイズ $m$ のデータ $\bm{X} \in \mathbb{R}^{n_0 \times m}$ に対して、
$$ \bm{Z}^{(l)} = \bm{W}^{(l)}\bm{A}^{(l-1)} + \bm{b}^{(l)}\bm{1}^T $$
$$ \bm{A}^{(l)} = g^{(l)}(\bm{Z}^{(l)}) $$
損失関数
二乗誤差損失の場合:
$$ L = \frac{1}{2m}\sum_{i=1}^{m}\|\bm{a}_i^{(L)} – \bm{y}_i\|^2 $$
交差エントロピー損失の場合:
$$ L = -\frac{1}{m}\sum_{i=1}^{m}\sum_{k=1}^{K} y_{ik}\log a_{ik}^{(L)} $$
誤差逆伝播の導出
出力層の勾配
損失関数の $\bm{z}^{(L)}$ に対する勾配を $\bm{\delta}^{(L)}$ と定義します。
$$ \bm{\delta}^{(L)} = \frac{\partial L}{\partial \bm{z}^{(L)}} $$
二乗誤差 + 恒等関数(出力層)の場合:
$$ \bm{\delta}^{(L)} = \bm{a}^{(L)} – \bm{y} $$
交差エントロピー + softmax(出力層)の場合も:
$$ \bm{\delta}^{(L)} = \bm{a}^{(L)} – \bm{y} $$
中間層の勾配(連鎖律)
第 $l$ 層の $\bm{\delta}^{(l)}$ は、第 $l+1$ 層の $\bm{\delta}^{(l+1)}$ から逆伝播で計算されます。
$$ \bm{\delta}^{(l)} = \frac{\partial L}{\partial \bm{z}^{(l)}} = \frac{\partial \bm{z}^{(l+1)}}{\partial \bm{a}^{(l)}} \cdot \frac{\partial \bm{a}^{(l)}}{\partial \bm{z}^{(l)}} \cdot \bm{\delta}^{(l+1)} $$
$\bm{z}^{(l+1)} = \bm{W}^{(l+1)}\bm{a}^{(l)} + \bm{b}^{(l+1)}$ なので、
$$ \frac{\partial \bm{z}^{(l+1)}}{\partial \bm{a}^{(l)}} = (\bm{W}^{(l+1)})^T $$
したがって、
$$ \bm{\delta}^{(l)} = (\bm{W}^{(l+1)})^T \bm{\delta}^{(l+1)} \odot g’^{(l)}(\bm{z}^{(l)}) $$
ここで $\odot$ は要素積(Hadamard積)、$g’$ は活性化関数の導関数です。
パラメータに対する勾配
重みとバイアスに対する勾配は以下の通りです。
$$ \frac{\partial L}{\partial \bm{W}^{(l)}} = \frac{1}{m}\bm{\delta}^{(l)}(\bm{A}^{(l-1)})^T $$
$$ \frac{\partial L}{\partial \bm{b}^{(l)}} = \frac{1}{m}\sum_{i=1}^{m}\bm{\delta}_i^{(l)} $$
ReLU活性化関数の導関数
$$ g(z) = \max(0, z), \quad g'(z) = \begin{cases} 1 & (z > 0) \\ 0 & (z \leq 0) \end{cases} $$
Pythonでのスクラッチ実装
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
class NeuralNetwork:
"""全結合ニューラルネットワーク(スクラッチ実装)"""
def __init__(self, layer_dims):
"""
layer_dims: 各層のユニット数のリスト [入力, 隠れ1, ..., 出力]
"""
self.L = len(layer_dims) - 1
self.params = {}
for l in range(1, self.L + 1):
# He初期化
self.params[f'W{l}'] = np.random.randn(layer_dims[l], layer_dims[l-1]) * \
np.sqrt(2.0 / layer_dims[l-1])
self.params[f'b{l}'] = np.zeros((layer_dims[l], 1))
def relu(self, z):
return np.maximum(0, z)
def relu_derivative(self, z):
return (z > 0).astype(float)
def forward(self, X):
"""順伝播"""
self.cache = {'A0': X}
A = X
for l in range(1, self.L + 1):
W = self.params[f'W{l}']
b = self.params[f'b{l}']
Z = W @ A + b
self.cache[f'Z{l}'] = Z
if l < self.L:
A = self.relu(Z) # 中間層: ReLU
else:
A = Z # 出力層: 恒等関数(回帰の場合)
self.cache[f'A{l}'] = A
return A
def compute_loss(self, Y_hat, Y):
"""MSE損失"""
m = Y.shape[1]
loss = (1 / (2 * m)) * np.sum((Y_hat - Y) ** 2)
return loss
def backward(self, Y):
"""誤差逆伝播"""
m = Y.shape[1]
grads = {}
# 出力層のデルタ
delta = self.cache[f'A{self.L}'] - Y # (n_L, m)
for l in range(self.L, 0, -1):
A_prev = self.cache[f'A{l-1}']
# パラメータの勾配
grads[f'dW{l}'] = (1 / m) * delta @ A_prev.T
grads[f'db{l}'] = (1 / m) * np.sum(delta, axis=1, keepdims=True)
if l > 1:
# 前の層へのデルタの逆伝播
W = self.params[f'W{l}']
Z_prev = self.cache[f'Z{l-1}']
delta = (W.T @ delta) * self.relu_derivative(Z_prev)
return grads
def update(self, grads, lr):
"""パラメータの更新"""
for l in range(1, self.L + 1):
self.params[f'W{l}'] -= lr * grads[f'dW{l}']
self.params[f'b{l}'] -= lr * grads[f'db{l}']
def train(self, X, Y, lr=0.01, n_epochs=1000):
"""学習ループ"""
losses = []
for epoch in range(n_epochs):
Y_hat = self.forward(X)
loss = self.compute_loss(Y_hat, Y)
losses.append(loss)
grads = self.backward(Y)
self.update(grads, lr)
return losses
# --- データ生成 ---
n = 200
X = np.random.uniform(-3, 3, (1, n))
Y = np.sin(X) + 0.3 * np.cos(3 * X)
# --- 学習 ---
nn_model = NeuralNetwork([1, 64, 32, 1])
losses = nn_model.train(X, Y, lr=0.01, n_epochs=2000)
# --- 予測と可視化 ---
X_test = np.linspace(-3.5, 3.5, 300).reshape(1, -1)
Y_pred = nn_model.forward(X_test)
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 予測結果
ax1 = axes[0]
ax1.scatter(X.ravel(), Y.ravel(), c='blue', s=10, alpha=0.3, label='Data')
ax1.plot(X_test.ravel(), Y_pred.ravel(), 'r-', linewidth=2, label='NN Prediction')
ax1.plot(X_test.ravel(), np.sin(X_test.ravel()) + 0.3 * np.cos(3 * X_test.ravel()),
'k--', alpha=0.5, label='True Function')
ax1.set_xlabel('x')
ax1.set_ylabel('y')
ax1.set_title('Neural Network Regression (Scratch)')
ax1.legend()
ax1.grid(True, alpha=0.3)
# 損失曲線
ax2 = axes[1]
ax2.plot(losses, 'b-')
ax2.set_xlabel('Epoch')
ax2.set_ylabel('MSE Loss')
ax2.set_title('Training Loss')
ax2.set_yscale('log')
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print(f"最終損失: {losses[-1]:.6f}")
まとめ
本記事では、順伝播と誤差逆伝播を行列形式で導出し、スクラッチ実装しました。
- 順伝播は $\bm{z}^{(l)} = \bm{W}^{(l)}\bm{a}^{(l-1)} + \bm{b}^{(l)}$, $\bm{a}^{(l)} = g(\bm{z}^{(l)})$ で層ごとに計算
- 誤差逆伝播は連鎖律により $\bm{\delta}^{(l)} = (\bm{W}^{(l+1)})^T\bm{\delta}^{(l+1)} \odot g'(\bm{z}^{(l)})$ で出力層から入力層に向かって勾配を伝播
- 重みの勾配は $\partial L / \partial \bm{W}^{(l)} = \bm{\delta}^{(l)}(\bm{a}^{(l-1)})^T / m$ で計算
- 行列形式で書くことで、ミニバッチ全体の計算を効率的に行える
次のステップとして、以下の記事も参考にしてください。