LESSON

演習:ニューラルネットワークの動作を理解しよう

田中VPoE:「ここまでパーセプトロン、活性化関数、逆伝播、ネットワーク設計を学んだ。ここでは NumPy だけを使って、ニューラルネットワークの順伝播と逆伝播をスクラッチで実装してみよう。」

あなた:「フレームワークに頼らず、内部の動きを手で追うということですね。」

田中VPoE:「そうだ。ブラックボックスを分解して理解しておくと、後で PyTorch を使うときにトラブルシューティングが格段に楽になる。」

ミッション概要

NumPy を使って、ニューラルネットワークの基本動作を手動で実装し、学習の仕組みを体験します。


Mission 1: 活性化関数を実装する

各活性化関数とその導関数を実装してください。

要件

  1. Sigmoid、ReLU、Tanh の順方向と導関数を実装する
  2. 入力値 z = [-3, -1, 0, 1, 3] に対して出力と導関数を計算する
  3. 結果を表形式で表示する
import numpy as np

def sigmoid(z):
    # ここに実装
    pass

def sigmoid_derivative(z):
    # ここに実装
    pass

def relu(z):
    # ここに実装
    pass

def relu_derivative(z):
    # ここに実装
    pass
解答例
import numpy as np

def sigmoid(z):
    return 1 / (1 + np.exp(-z))

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

def relu(z):
    return np.maximum(0, z)

def relu_derivative(z):
    return (z > 0).astype(float)

def tanh_func(z):
    return np.tanh(z)

def tanh_derivative(z):
    return 1 - np.tanh(z) ** 2

# テスト
z = np.array([-3, -1, 0, 1, 3], dtype=float)

print(f"{'z':>5} | {'Sigmoid':>8} {'dSigmoid':>9} | {'ReLU':>6} {'dReLU':>6} | {'Tanh':>7} {'dTanh':>7}")
print("-" * 70)
for i in range(len(z)):
    print(f"{z[i]:5.1f} | {sigmoid(z)[i]:8.4f} {sigmoid_derivative(z)[i]:9.4f} | "
          f"{relu(z)[i]:6.2f} {relu_derivative(z)[i]:6.2f} | "
          f"{tanh_func(z)[i]:7.4f} {tanh_derivative(z)[i]:7.4f}")

Mission 2: 2層ニューラルネットワークを実装する

NumPy で2層(入力→隠れ層→出力)のニューラルネットワークを実装し、XOR問題を解いてください。

要件

  1. 入力: 2次元、隠れ層: 4ユニット(ReLU)、出力: 1ユニット(Sigmoid)
  2. 順伝播を実装する
  3. Binary Cross-Entropy 損失を計算する
  4. 逆伝播で勾配を計算する
  5. パラメータを更新して学習させる
import numpy as np

# XOR データ
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y = np.array([[0], [1], [1], [0]])

np.random.seed(42)

# パラメータ初期化
W1 = np.random.randn(2, 4) * 0.5
b1 = np.zeros((1, 4))
W2 = np.random.randn(4, 1) * 0.5
b2 = np.zeros((1, 1))

learning_rate = 0.5

# ここに学習ループを実装
解答例
import numpy as np

def sigmoid(z):
    return 1 / (1 + np.exp(-z))

def relu(z):
    return np.maximum(0, z)

# XOR データ
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y = np.array([[0], [1], [1], [0]])

np.random.seed(42)

# パラメータ初期化
W1 = np.random.randn(2, 4) * 0.5
b1 = np.zeros((1, 4))
W2 = np.random.randn(4, 1) * 0.5
b2 = np.zeros((1, 1))

learning_rate = 0.5
losses = []

for epoch in range(5000):
    # === 順伝播 ===
    z1 = X @ W1 + b1          # 隠れ層の入力
    a1 = relu(z1)             # 隠れ層の出力(ReLU)
    z2 = a1 @ W2 + b2         # 出力層の入力
    a2 = sigmoid(z2)          # 出力層の出力(Sigmoid)

    # === 損失計算 ===
    epsilon = 1e-7
    loss = -np.mean(y * np.log(a2 + epsilon) + (1 - y) * np.log(1 - a2 + epsilon))
    losses.append(loss)

    # === 逆伝播 ===
    m = X.shape[0]

    # 出力層の勾配
    dz2 = a2 - y                          # (4, 1)
    dW2 = (a1.T @ dz2) / m                # (4, 1)
    db2 = np.mean(dz2, axis=0, keepdims=True)

    # 隠れ層の勾配
    da1 = dz2 @ W2.T                      # (4, 4)
    dz1 = da1 * (z1 > 0).astype(float)    # ReLU の導関数
    dW1 = (X.T @ dz1) / m                 # (2, 4)
    db1 = np.mean(dz1, axis=0, keepdims=True)

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

    if epoch % 1000 == 0:
        print(f"Epoch {epoch:4d}: Loss = {loss:.6f}")

# 最終予測
print(f"\n=== 学習結果 ===")
print(f"入力 → 予測 (正解)")
for i in range(len(X)):
    pred = a2[i, 0]
    print(f"  {X[i]}{pred:.4f} ({y[i, 0]})")
print(f"\n最終損失: {losses[-1]:.6f}")

Mission 3: 学習率の影響を実験する

学習率を変えて XOR 問題を学習し、収束の違いを観察してください。

要件

  1. 学習率 0.01, 0.1, 0.5, 2.0 の4パターンで学習する
  2. 各学習率での損失曲線をプロットする
  3. どの学習率が最適かを考察する
import matplotlib.pyplot as plt

learning_rates = [0.01, 0.1, 0.5, 2.0]
# ここに実験コードを書く
解答例
import numpy as np
import matplotlib.pyplot as plt

def sigmoid(z):
    return 1 / (1 + np.exp(-np.clip(z, -500, 500)))

def train_xor(learning_rate, epochs=5000):
    X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
    y = np.array([[0], [1], [1], [0]])
    np.random.seed(42)
    W1 = np.random.randn(2, 4) * 0.5
    b1 = np.zeros((1, 4))
    W2 = np.random.randn(4, 1) * 0.5
    b2 = np.zeros((1, 1))
    losses = []

    for epoch in range(epochs):
        z1 = X @ W1 + b1
        a1 = np.maximum(0, z1)
        z2 = a1 @ W2 + b2
        a2 = sigmoid(z2)
        epsilon = 1e-7
        loss = -np.mean(y * np.log(a2 + epsilon) + (1 - y) * np.log(1 - a2 + epsilon))
        losses.append(loss)
        m = X.shape[0]
        dz2 = a2 - y
        dW2 = (a1.T @ dz2) / m
        db2 = np.mean(dz2, axis=0, keepdims=True)
        da1 = dz2 @ W2.T
        dz1 = da1 * (z1 > 0).astype(float)
        dW1 = (X.T @ dz1) / m
        db1 = np.mean(dz1, axis=0, keepdims=True)
        W2 -= learning_rate * dW2
        b2 -= learning_rate * db2
        W1 -= learning_rate * dW1
        b1 -= learning_rate * db1
    return losses

fig, ax = plt.subplots(figsize=(10, 6))
for lr in [0.01, 0.1, 0.5, 2.0]:
    losses = train_xor(lr)
    ax.plot(losses, label=f'lr={lr}')

ax.set_xlabel('Epoch')
ax.set_ylabel('Loss')
ax.set_title('学習率による収束の違い')
ax.legend()
ax.set_ylim(0, 1.0)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

print("考察:")
print("  lr=0.01: 学習が遅く、5000エポックでは収束しない")
print("  lr=0.1:  安定して収束するが、やや時間がかかる")
print("  lr=0.5:  高速に収束する(この問題では適切)")
print("  lr=2.0:  不安定で発散する可能性がある")

達成度チェック

  • Sigmoid、ReLU、Tanh の順方向と導関数を実装できた
  • 2層ニューラルネットワークの順伝播と逆伝播を手動で実装できた
  • XOR 問題を学習させ、正しい出力が得られた
  • 学習率の違いによる収束挙動の変化を確認した

推定所要時間: 60分