深層学習における順伝播と誤差逆伝播を行列形式で理解する

深層学習の学習プロセスは、順伝播(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$ で計算
  • 行列形式で書くことで、ミニバッチ全体の計算を効率的に行える

次のステップとして、以下の記事も参考にしてください。