ディープラーニング 逆伝播(バックプロパゲーション)完全ガイド – 仕組みから実装まで徹底解説

AI

「逆伝播って聞くけど、何をしているの?」 「数式が難しくて理解できない…」 「どうやって実装すればいいの?」

逆伝播(Backpropagation)は、ディープラーニングの心臓部とも言える重要なアルゴリズムです。これがなければ、ニューラルネットワークは学習することができません。

でも、その仕組みは意外とシンプル。「間違いを遡って、各パラメータの責任を明らかにする」という考え方です。

この記事では、逆伝播について、直感的な理解から数学的な詳細、そして実装まで、段階的に分かりやすく解説していきます。

スポンサーリンク

逆伝播とは?直感的な理解

レストランの例で理解する

逆伝播を理解するために、レストランの例を考えてみましょう。

状況: 料理の味が悪く、お客様から「塩辛い」というクレームが来た

原因究明の流れ(逆伝播):

  1. 最終的な料理が塩辛い(出力の誤差)
  2. シェフの味付けを確認(最後の層)
  3. 下ごしらえ担当の塩加減を確認(中間層)
  4. 仕入れた材料の塩分を確認(入力層)

各担当者の「責任の度合い」を計算して、次回は各自がどれだけ調整すべきかを決める。これが逆伝播の考え方です。

ニューラルネットワークでの逆伝播

import numpy as np
import matplotlib.pyplot as plt

class SimpleNeuralNetwork:
    """逆伝播を理解するためのシンプルなネットワーク"""
    
    def __init__(self):
        # 2入力 → 3隠れ → 1出力
        self.w1 = np.random.randn(2, 3) * 0.5  # 入力層→隠れ層の重み
        self.b1 = np.zeros((1, 3))             # 隠れ層のバイアス
        self.w2 = np.random.randn(3, 1) * 0.5  # 隠れ層→出力層の重み
        self.b2 = np.zeros((1, 1))             # 出力層のバイアス
        
        # 順伝播の中間結果を保存(逆伝播で使用)
        self.z1 = None
        self.a1 = None
        self.z2 = None
        self.a2 = None
    
    def forward(self, X):
        """順伝播:入力から出力を計算"""
        # 入力層 → 隠れ層
        self.z1 = np.dot(X, self.w1) + self.b1
        self.a1 = self.sigmoid(self.z1)
        
        # 隠れ層 → 出力層
        self.z2 = np.dot(self.a1, self.w2) + self.b2
        self.a2 = self.sigmoid(self.z2)
        
        return self.a2
    
    def backward(self, X, y, learning_rate=0.1):
        """逆伝播:誤差から各パラメータの勾配を計算"""
        m = X.shape[0]  # サンプル数
        
        # 出力層の誤差
        dz2 = self.a2 - y  # 誤差 = 予測値 - 正解
        
        # 出力層の重みとバイアスの勾配
        dw2 = (1/m) * np.dot(self.a1.T, dz2)
        db2 = (1/m) * np.sum(dz2, axis=0, keepdims=True)
        
        # 隠れ層への誤差の伝播
        da1 = np.dot(dz2, self.w2.T)
        dz1 = da1 * self.sigmoid_derivative(self.z1)
        
        # 隠れ層の重みとバイアスの勾配
        dw1 = (1/m) * np.dot(X.T, dz1)
        db1 = (1/m) * np.sum(dz1, axis=0, keepdims=True)
        
        # パラメータの更新
        self.w2 -= learning_rate * dw2
        self.b2 -= learning_rate * db2
        self.w1 -= learning_rate * dw1
        self.b1 -= learning_rate * db1
        
        return dw1, db1, dw2, db2
    
    def sigmoid(self, x):
        return 1 / (1 + np.exp(-x))
    
    def sigmoid_derivative(self, x):
        s = self.sigmoid(x)
        return s * (1 - s)

連鎖律:逆伝播の数学的基礎

連鎖律とは

逆伝播の核心は「連鎖律(Chain Rule)」という微分の法則です。

基本的な連鎖律:

もし z = f(g(x)) なら
dz/dx = (dz/dg) × (dg/dx)

つまり、「最終的な結果への影響」は「各段階の影響の掛け算」で表されます。

具体例で理解する連鎖律

def chain_rule_example():
    """連鎖律の具体例"""
    
    # 関数: z = (x + 2)^2
    # これは z = u^2, u = x + 2 の合成関数
    
    x = 3.0
    
    # 順伝播
    u = x + 2       # u = 5
    z = u ** 2      # z = 25
    
    # 逆伝播(連鎖律)
    dz_du = 2 * u   # ∂z/∂u = 2u = 10
    du_dx = 1       # ∂u/∂x = 1
    dz_dx = dz_du * du_dx  # ∂z/∂x = 10 × 1 = 10
    
    print(f"x = {x}")
    print(f"z = {z}")
    print(f"dz/dx = {dz_dx}")
    
    # 検証:直接微分
    # z = (x + 2)^2 を展開すると z = x^2 + 4x + 4
    # dz/dx = 2x + 4 = 2(3) + 4 = 10 ✓

chain_rule_example()

ニューラルネットワークでの連鎖律

class ChainRuleVisualization:
    """ニューラルネットワークでの連鎖律を可視化"""
    
    def __init__(self):
        self.history = []
    
    def simple_network(self, x, w1, w2, b1, b2):
        """
        単純な2層ネットワーク
        x → (w1*x + b1) → ReLU → (w2*h + b2) → y
        """
        # 順伝播
        z1 = w1 * x + b1
        h = max(0, z1)  # ReLU
        z2 = w2 * h + b2
        y = z2
        
        # 各段階の値を保存
        self.history = {
            'x': x, 'w1': w1, 'b1': b1,
            'z1': z1, 'h': h,
            'w2': w2, 'b2': b2,
            'z2': z2, 'y': y
        }
        
        return y
    
    def compute_gradients(self, target):
        """連鎖律で勾配を計算"""
        # 損失関数: L = (y - target)^2 / 2
        y = self.history['y']
        h = self.history['h']
        z1 = self.history['z1']
        x = self.history['x']
        w1 = self.history['w1']
        w2 = self.history['w2']
        
        # 逆伝播
        dL_dy = y - target                    # ∂L/∂y
        dy_dz2 = 1                            # ∂y/∂z2
        dL_dz2 = dL_dy * dy_dz2              # ∂L/∂z2
        
        dz2_dw2 = h                           # ∂z2/∂w2
        dL_dw2 = dL_dz2 * dz2_dw2            # ∂L/∂w2
        
        dz2_db2 = 1                           # ∂z2/∂b2
        dL_db2 = dL_dz2 * dz2_db2            # ∂L/∂b2
        
        dz2_dh = w2                           # ∂z2/∂h
        dL_dh = dL_dz2 * dz2_dh              # ∂L/∂h
        
        dh_dz1 = 1 if z1 > 0 else 0          # ReLUの微分
        dL_dz1 = dL_dh * dh_dz1              # ∂L/∂z1
        
        dz1_dw1 = x                           # ∂z1/∂w1
        dL_dw1 = dL_dz1 * dz1_dw1            # ∂L/∂w1
        
        dz1_db1 = 1                           # ∂z1/∂b1
        dL_db1 = dL_dz1 * dz1_db1            # ∂L/∂b1
        
        return {
            'dL_dw1': dL_dw1, 'dL_db1': dL_db1,
            'dL_dw2': dL_dw2, 'dL_db2': dL_db2
        }
    
    def visualize_chain_rule(self):
        """連鎖律の流れを図示"""
        # 簡単な例で計算
        x = 2.0
        w1, b1 = 1.5, 0.5
        w2, b2 = -0.8, 0.3
        target = 1.0
        
        y = self.simple_network(x, w1, w2, b1, b2)
        grads = self.compute_gradients(target)
        
        print("順伝播の流れ:")
        print(f"  x={x:.2f} → z1={self.history['z1']:.2f} → h={self.history['h']:.2f}")
        print(f"  → z2={self.history['z2']:.2f} → y={y:.2f}")
        print(f"\n目標値: {target:.2f}")
        print(f"誤差: {y - target:.2f}")
        print("\n逆伝播で計算された勾配:")
        for key, value in grads.items():
            print(f"  {key}: {value:.4f}")

# 実行
viz = ChainRuleVisualization()
viz.visualize_chain_rule()

実装:ステップバイステップ

全結合層の逆伝播

class FullyConnectedLayer:
    """全結合層の順伝播と逆伝播"""
    
    def __init__(self, input_size, output_size):
        # Xavierの初期化
        self.W = np.random.randn(input_size, output_size) * np.sqrt(2.0 / input_size)
        self.b = np.zeros((1, output_size))
        
        # 勾配
        self.dW = None
        self.db = None
        
        # 順伝播の入力を保存(逆伝播で使用)
        self.input = None
        self.output = None
    
    def forward(self, X):
        """順伝播"""
        self.input = X
        self.output = np.dot(X, self.W) + self.b
        return self.output
    
    def backward(self, grad_output):
        """逆伝播"""
        batch_size = self.input.shape[0]
        
        # パラメータの勾配
        self.dW = np.dot(self.input.T, grad_output) / batch_size
        self.db = np.sum(grad_output, axis=0, keepdims=True) / batch_size
        
        # 入力に対する勾配(前の層に渡す)
        grad_input = np.dot(grad_output, self.W.T)
        
        return grad_input
    
    def update_params(self, learning_rate):
        """パラメータ更新"""
        self.W -= learning_rate * self.dW
        self.b -= learning_rate * self.db

活性化関数の逆伝播

class ActivationLayers:
    """各種活性化関数の順伝播と逆伝播"""
    
    class ReLU:
        def __init__(self):
            self.mask = None
        
        def forward(self, x):
            self.mask = (x > 0)
            return x * self.mask
        
        def backward(self, grad_output):
            return grad_output * self.mask
    
    class Sigmoid:
        def __init__(self):
            self.output = None
        
        def forward(self, x):
            self.output = 1 / (1 + np.exp(-np.clip(x, -500, 500)))
            return self.output
        
        def backward(self, grad_output):
            return grad_output * self.output * (1 - self.output)
    
    class Tanh:
        def __init__(self):
            self.output = None
        
        def forward(self, x):
            self.output = np.tanh(x)
            return self.output
        
        def backward(self, grad_output):
            return grad_output * (1 - self.output ** 2)
    
    class Softmax:
        def __init__(self):
            self.output = None
        
        def forward(self, x):
            # オーバーフロー対策
            x_exp = np.exp(x - np.max(x, axis=1, keepdims=True))
            self.output = x_exp / np.sum(x_exp, axis=1, keepdims=True)
            return self.output
        
        def backward(self, grad_output):
            # Softmaxの微分は複雑
            batch_size = self.output.shape[0]
            grad_input = np.zeros_like(grad_output)
            
            for i in range(batch_size):
                jacobian = np.diag(self.output[i]) - np.outer(
                    self.output[i], self.output[i]
                )
                grad_input[i] = np.dot(jacobian, grad_output[i])
            
            return grad_input

損失関数の逆伝播

class LossFunctions:
    """各種損失関数とその微分"""
    
    @staticmethod
    def mse_loss(y_pred, y_true):
        """平均二乗誤差"""
        loss = np.mean((y_pred - y_true) ** 2)
        grad = 2 * (y_pred - y_true) / y_true.shape[0]
        return loss, grad
    
    @staticmethod
    def cross_entropy_loss(y_pred, y_true):
        """クロスエントロピー損失(Softmax出力用)"""
        batch_size = y_true.shape[0]
        
        # 数値安定性のためのクリッピング
        y_pred = np.clip(y_pred, 1e-7, 1 - 1e-7)
        
        # One-hotエンコーディングの場合
        if len(y_true.shape) == 2:
            loss = -np.sum(y_true * np.log(y_pred)) / batch_size
            grad = (y_pred - y_true) / batch_size
        # クラスインデックスの場合
        else:
            loss = -np.mean(np.log(y_pred[range(batch_size), y_true]))
            grad = y_pred.copy()
            grad[range(batch_size), y_true] -= 1
            grad /= batch_size
        
        return loss, grad
    
    @staticmethod
    def binary_cross_entropy(y_pred, y_true):
        """二値分類用のクロスエントロピー"""
        y_pred = np.clip(y_pred, 1e-7, 1 - 1e-7)
        
        loss = -np.mean(
            y_true * np.log(y_pred) + 
            (1 - y_true) * np.log(1 - y_pred)
        )
        
        grad = (y_pred - y_true) / (y_pred * (1 - y_pred)) / y_true.shape[0]
        
        return loss, grad

完全なニューラルネットワークの実装

逆伝播を含む完全なネットワーク

class NeuralNetwork:
    """逆伝播を実装した完全なニューラルネットワーク"""
    
    def __init__(self, layer_sizes, activation='relu'):
        """
        layer_sizes: 各層のサイズ [input, hidden1, hidden2, ..., output]
        activation: 活性化関数 ('relu', 'sigmoid', 'tanh')
        """
        self.layers = []
        self.activations = []
        
        # 層を構築
        for i in range(len(layer_sizes) - 1):
            self.layers.append(
                FullyConnectedLayer(layer_sizes[i], layer_sizes[i+1])
            )
            
            # 最後の層以外に活性化関数を追加
            if i < len(layer_sizes) - 2:
                if activation == 'relu':
                    self.activations.append(ActivationLayers.ReLU())
                elif activation == 'sigmoid':
                    self.activations.append(ActivationLayers.Sigmoid())
                elif activation == 'tanh':
                    self.activations.append(ActivationLayers.Tanh())
        
        # 出力層の活性化(分類タスク用)
        self.output_activation = ActivationLayers.Softmax()
    
    def forward(self, X):
        """順伝播"""
        self.layer_outputs = []
        output = X
        
        for i, layer in enumerate(self.layers):
            output = layer.forward(output)
            self.layer_outputs.append(output)
            
            # 最後の層以外は活性化関数を適用
            if i < len(self.layers) - 1:
                output = self.activations[i].forward(output)
                self.layer_outputs.append(output)
        
        # 出力層の活性化
        output = self.output_activation.forward(output)
        
        return output
    
    def backward(self, X, y_true, y_pred, learning_rate=0.01):
        """逆伝播"""
        # 損失関数の勾配
        _, grad = LossFunctions.cross_entropy_loss(y_pred, y_true)
        
        # 出力層の活性化関数の逆伝播
        grad = self.output_activation.backward(grad)
        
        # 各層を逆順に処理
        for i in range(len(self.layers) - 1, -1, -1):
            # 活性化関数の逆伝播(最後の層以外)
            if i < len(self.layers) - 1:
                grad = self.activations[i].backward(grad)
            
            # 全結合層の逆伝播
            grad = self.layers[i].backward(grad)
            
            # パラメータ更新
            self.layers[i].update_params(learning_rate)
    
    def train_step(self, X_batch, y_batch, learning_rate=0.01):
        """1回の学習ステップ"""
        # 順伝播
        y_pred = self.forward(X_batch)
        
        # 損失計算
        loss, _ = LossFunctions.cross_entropy_loss(y_pred, y_batch)
        
        # 逆伝播
        self.backward(X_batch, y_batch, y_pred, learning_rate)
        
        return loss
    
    def train(self, X_train, y_train, epochs=100, batch_size=32, learning_rate=0.01):
        """学習"""
        n_samples = X_train.shape[0]
        losses = []
        
        for epoch in range(epochs):
            epoch_loss = 0
            n_batches = 0
            
            # ミニバッチ学習
            for i in range(0, n_samples, batch_size):
                X_batch = X_train[i:i+batch_size]
                y_batch = y_train[i:i+batch_size]
                
                loss = self.train_step(X_batch, y_batch, learning_rate)
                epoch_loss += loss
                n_batches += 1
            
            avg_loss = epoch_loss / n_batches
            losses.append(avg_loss)
            
            if (epoch + 1) % 10 == 0:
                print(f"Epoch {epoch+1}/{epochs}, Loss: {avg_loss:.4f}")
        
        return losses

勾配消失・爆発問題と対策

勾配消失・爆発の可視化

def visualize_gradient_flow():
    """勾配の流れを可視化"""
    
    # 深いネットワークでの勾配の変化
    n_layers = 10
    
    # Sigmoid活性化の場合(勾配消失しやすい)
    gradients_sigmoid = []
    grad = 1.0
    for _ in range(n_layers):
        # Sigmoidの微分の最大値は0.25
        grad *= 0.25
        gradients_sigmoid.append(grad)
    
    # ReLU活性化の場合
    gradients_relu = []
    grad = 1.0
    for _ in range(n_layers):
        # ReLUの微分は0か1
        grad *= 1.0  # アクティブな場合
        gradients_relu.append(grad)
    
    # Tanh活性化の場合
    gradients_tanh = []
    grad = 1.0
    for _ in range(n_layers):
        # Tanhの微分の最大値は1
        grad *= 0.5  # 平均的な値
        gradients_tanh.append(grad)
    
    # プロット
    plt.figure(figsize=(12, 6))
    
    plt.subplot(1, 2, 1)
    plt.plot(range(1, n_layers+1), gradients_sigmoid, 'o-', label='Sigmoid')
    plt.plot(range(1, n_layers+1), gradients_relu, 's-', label='ReLU')
    plt.plot(range(1, n_layers+1), gradients_tanh, '^-', label='Tanh')
    plt.xlabel('Layer Depth')
    plt.ylabel('Gradient Magnitude')
    plt.title('Gradient Flow in Deep Networks')
    plt.legend()
    plt.yscale('log')
    plt.grid(True)
    
    # 勾配クリッピングの効果
    plt.subplot(1, 2, 2)
    gradients_clipped = []
    grad = 1.0
    clip_value = 1.0
    
    for i in range(n_layers):
        # ランダムな勾配を生成
        random_grad = np.random.randn() * (i + 1)
        # クリッピング
        clipped_grad = np.clip(random_grad, -clip_value, clip_value)
        gradients_clipped.append(abs(clipped_grad))
    
    plt.plot(range(1, n_layers+1), gradients_clipped, 'o-', label='With Clipping')
    plt.axhline(y=clip_value, color='r', linestyle='--', label='Clip Threshold')
    plt.xlabel('Layer Depth')
    plt.ylabel('Gradient Magnitude')
    plt.title('Effect of Gradient Clipping')
    plt.legend()
    plt.grid(True)
    
    plt.tight_layout()
    plt.show()

visualize_gradient_flow()

対策の実装

class GradientStabilization:
    """勾配を安定化させる技術"""
    
    @staticmethod
    def gradient_clipping(gradients, max_norm=5.0):
        """勾配クリッピング"""
        total_norm = 0
        
        # 全勾配のノルムを計算
        for grad in gradients:
            total_norm += np.sum(grad ** 2)
        total_norm = np.sqrt(total_norm)
        
        # クリッピング
        clip_ratio = max_norm / (total_norm + 1e-8)
        if clip_ratio < 1:
            for grad in gradients:
                grad *= clip_ratio
        
        return gradients
    
    @staticmethod
    def batch_normalization(x, gamma, beta, running_mean, running_var, 
                           training=True, momentum=0.9, eps=1e-5):
        """バッチ正規化"""
        if training:
            # バッチの統計量
            batch_mean = np.mean(x, axis=0)
            batch_var = np.var(x, axis=0)
            
            # 正規化
            x_norm = (x - batch_mean) / np.sqrt(batch_var + eps)
            
            # 移動平均の更新
            running_mean = momentum * running_mean + (1 - momentum) * batch_mean
            running_var = momentum * running_var + (1 - momentum) * batch_var
        else:
            # 推論時は移動平均を使用
            x_norm = (x - running_mean) / np.sqrt(running_var + eps)
        
        # スケールとシフト
        out = gamma * x_norm + beta
        
        # 逆伝播用にキャッシュ
        cache = (x, x_norm, batch_mean, batch_var, gamma, beta, eps)
        
        return out, cache, running_mean, running_var
    
    @staticmethod
    def batch_norm_backward(dout, cache):
        """バッチ正規化の逆伝播"""
        x, x_norm, mean, var, gamma, beta, eps = cache
        N = x.shape[0]
        
        # 勾配計算
        dbeta = np.sum(dout, axis=0)
        dgamma = np.sum(dout * x_norm, axis=0)
        
        dx_norm = dout * gamma
        dvar = np.sum(dx_norm * (x - mean) * -0.5 * (var + eps) ** (-1.5), axis=0)
        dmean = np.sum(dx_norm * -1 / np.sqrt(var + eps), axis=0) + \
                dvar * np.mean(-2 * (x - mean), axis=0)
        
        dx = dx_norm / np.sqrt(var + eps) + \
             dvar * 2 * (x - mean) / N + \
             dmean / N
        
        return dx, dgamma, dbeta

高度な逆伝播技術

自動微分の実装

class AutoGrad:
    """簡単な自動微分の実装"""
    
    class Variable:
        def __init__(self, data, requires_grad=True):
            self.data = data
            self.grad = None
            self.requires_grad = requires_grad
            self.grad_fn = None
            self.is_leaf = True
        
        def backward(self, grad=None):
            if not self.requires_grad:
                return
            
            if grad is None:
                grad = np.ones_like(self.data)
            
            if self.grad is None:
                self.grad = grad
            else:
                self.grad += grad
            
            if self.grad_fn is not None:
                self.grad_fn.backward(grad)
        
        def __add__(self, other):
            return Add.apply(self, other)
        
        def __mul__(self, other):
            return Mul.apply(self, other)
    
    class Function:
        @classmethod
        def apply(cls, *args):
            instance = cls()
            output = Variable(instance.forward(*[a.data for a in args]))
            output.grad_fn = instance
            output.is_leaf = False
            instance.saved_tensors = args
            return output
        
        def backward(self, grad_output):
            grad_inputs = self.backward_impl(grad_output)
            if not isinstance(grad_inputs, tuple):
                grad_inputs = (grad_inputs,)
            
            for var, grad in zip(self.saved_tensors, grad_inputs):
                if var.requires_grad:
                    var.backward(grad)
    
    class Add(Function):
        def forward(self, x, y):
            return x + y
        
        def backward_impl(self, grad_output):
            return grad_output, grad_output
    
    class Mul(Function):
        def forward(self, x, y):
            self.x = x
            self.y = y
            return x * y
        
        def backward_impl(self, grad_output):
            return self.y * grad_output, self.x * grad_output

# 使用例
x = AutoGrad.Variable(np.array([2.0]), requires_grad=True)
y = AutoGrad.Variable(np.array([3.0]), requires_grad=True)

z = x * y + x  # z = 2*3 + 2 = 8
z.backward()

print(f"x.grad: {x.grad}")  # dz/dx = y + 1 = 4
print(f"y.grad: {y.grad}")  # dz/dy = x = 2

勾配チェック

def gradient_check(network, X, y, epsilon=1e-7):
    """数値微分で勾配の正確性をチェック"""
    
    # 解析的勾配(逆伝播)を計算
    y_pred = network.forward(X)
    loss, _ = LossFunctions.cross_entropy_loss(y_pred, y)
    network.backward(X, y, y_pred, learning_rate=0)  # 更新はしない
    
    # 各パラメータについて数値微分と比較
    for layer_idx, layer in enumerate(network.layers):
        # 重みの勾配チェック
        analytical_grad = layer.dW.copy()
        
        # 数値微分
        numerical_grad = np.zeros_like(layer.W)
        
        for i in range(layer.W.shape[0]):
            for j in range(layer.W.shape[1]):
                # +epsilon
                layer.W[i, j] += epsilon
                y_pred_plus = network.forward(X)
                loss_plus, _ = LossFunctions.cross_entropy_loss(y_pred_plus, y)
                
                # -epsilon
                layer.W[i, j] -= 2 * epsilon
                y_pred_minus = network.forward(X)
                loss_minus, _ = LossFunctions.cross_entropy_loss(y_pred_minus, y)
                
                # 数値微分
                numerical_grad[i, j] = (loss_plus - loss_minus) / (2 * epsilon)
                
                # 元に戻す
                layer.W[i, j] += epsilon
        
        # 相対誤差を計算
        relative_error = np.abs(analytical_grad - numerical_grad) / \
                        (np.abs(analytical_grad) + np.abs(numerical_grad) + epsilon)
        
        print(f"Layer {layer_idx} - Weight gradient check:")
        print(f"  Max relative error: {np.max(relative_error):.2e}")
        print(f"  Mean relative error: {np.mean(relative_error):.2e}")
        
        if np.max(relative_error) > 1e-5:
            print("  WARNING: Gradient may be incorrect!")

実践例:MNISTでの逆伝播

from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import OneHotEncoder

def train_on_synthetic_data():
    """合成データでニューラルネットワークを学習"""
    
    # データ生成
    X, y = make_classification(
        n_samples=1000,
        n_features=20,
        n_informative=15,
        n_redundant=5,
        n_classes=3,
        random_state=42
    )
    
    # One-hotエンコーディング
    y_onehot = OneHotEncoder(sparse=False).fit_transform(y.reshape(-1, 1))
    
    # データ分割
    X_train, X_test, y_train, y_test = train_test_split(
        X, y_onehot, test_size=0.2, random_state=42
    )
    
    # ネットワーク構築
    network = NeuralNetwork(
        layer_sizes=[20, 64, 32, 3],
        activation='relu'
    )
    
    # 学習
    print("Training neural network with backpropagation...")
    losses = network.train(
        X_train, y_train,
        epochs=50,
        batch_size=32,
        learning_rate=0.01
    )
    
    # 結果をプロット
    plt.figure(figsize=(10, 5))
    
    plt.subplot(1, 2, 1)
    plt.plot(losses)
    plt.xlabel('Epoch')
    plt.ylabel('Loss')
    plt.title('Training Loss')
    plt.grid(True)
    
    # テストデータで評価
    y_pred = network.forward(X_test)
    test_loss, _ = LossFunctions.cross_entropy_loss(y_pred, y_test)
    accuracy = np.mean(np.argmax(y_pred, axis=1) == np.argmax(y_test, axis=1))
    
    plt.subplot(1, 2, 2)
    plt.bar(['Train Loss', 'Test Loss', 'Test Accuracy'], 
            [losses[-1], test_loss, accuracy])
    plt.ylabel('Value')
    plt.title('Final Performance')
    
    plt.tight_layout()
    plt.show()
    
    print(f"\nFinal Results:")
    print(f"  Training Loss: {losses[-1]:.4f}")
    print(f"  Test Loss: {test_loss:.4f}")
    print(f"  Test Accuracy: {accuracy:.2%}")
    
    return network

# 実行
trained_network = train_on_synthetic_data()

逆伝播の最適化とTips

学習率スケジューリング

class LearningRateSchedulers:
    """学習率のスケジューリング"""
    
    @staticmethod
    def step_decay(initial_lr, epoch, drop_rate=0.5, epochs_drop=10):
        """ステップ減衰"""
        return initial_lr * (drop_rate ** (epoch // epochs_drop))
    
    @staticmethod
    def exponential_decay(initial_lr, epoch, decay_rate=0.95):
        """指数減衰"""
        return initial_lr * (decay_rate ** epoch)
    
    @staticmethod
    def cosine_annealing(initial_lr, epoch, total_epochs):
        """コサインアニーリング"""
        return initial_lr * (1 + np.cos(np.pi * epoch / total_epochs)) / 2
    
    @staticmethod
    def warmup(initial_lr, epoch, warmup_epochs=5):
        """ウォームアップ"""
        if epoch < warmup_epochs:
            return initial_lr * (epoch + 1) / warmup_epochs
        return initial_lr

# 可視化
def visualize_lr_schedules():
    epochs = np.arange(50)
    initial_lr = 0.1
    
    plt.figure(figsize=(12, 8))
    
    schedules = {
        'Step Decay': [LearningRateSchedulers.step_decay(initial_lr, e) for e in epochs],
        'Exponential': [LearningRateSchedulers.exponential_decay(initial_lr, e) for e in epochs],
        'Cosine': [LearningRateSchedulers.cosine_annealing(initial_lr, e, 50) for e in epochs],
        'Warmup + Cosine': [
            LearningRateSchedulers.warmup(
                LearningRateSchedulers.cosine_annealing(initial_lr, e, 50), e
            ) for e in epochs
        ]
    }
    
    for name, lr_values in schedules.items():
        plt.plot(epochs, lr_values, label=name, linewidth=2)
    
    plt.xlabel('Epoch')
    plt.ylabel('Learning Rate')
    plt.title('Learning Rate Scheduling Strategies')
    plt.legend()
    plt.grid(True, alpha=0.3)
    plt.show()

visualize_lr_schedules()

まとめ:逆伝播がディープラーニングを可能にする

逆伝播(バックプロパゲーション)について、基礎から実装まで詳しく解説してきました。

重要なポイント:

  1. 逆伝播は連鎖律を使って勾配を効率的に計算
  2. 各層で勾配を計算し、前の層に伝播
  3. 活性化関数の選択が勾配の流れに大きく影響
  4. 勾配消失・爆発問題への対策が重要
  5. 自動微分により複雑なネットワークでも実装可能

逆伝播は一見複雑に見えますが、基本的な考え方は「誤差を遡って責任を配分する」というシンプルなものです。

学習のステップ:

  1. 連鎖律の理解から始める
  2. 簡単なネットワークで手計算
  3. コードで実装して動作確認
  4. 最適化技術を追加
  5. 実際のタスクに適用

この基礎をしっかり理解することで、最新の深層学習モデルも理解できるようになります!

コメント

タイトルとURLをコピーしました