演習:ニューラルネットワークの動作を理解しよう
田中VPoE:「ここまでパーセプトロン、活性化関数、逆伝播、ネットワーク設計を学んだ。ここでは NumPy だけを使って、ニューラルネットワークの順伝播と逆伝播をスクラッチで実装してみよう。」
あなた:「フレームワークに頼らず、内部の動きを手で追うということですね。」
田中VPoE:「そうだ。ブラックボックスを分解して理解しておくと、後で PyTorch を使うときにトラブルシューティングが格段に楽になる。」
ミッション概要
NumPy を使って、ニューラルネットワークの基本動作を手動で実装し、学習の仕組みを体験します。
Mission 1: 活性化関数を実装する
各活性化関数とその導関数を実装してください。
要件
- Sigmoid、ReLU、Tanh の順方向と導関数を実装する
- 入力値
z = [-3, -1, 0, 1, 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問題を解いてください。
要件
- 入力: 2次元、隠れ層: 4ユニット(ReLU)、出力: 1ユニット(Sigmoid)
- 順伝播を実装する
- Binary Cross-Entropy 損失を計算する
- 逆伝播で勾配を計算する
- パラメータを更新して学習させる
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 問題を学習し、収束の違いを観察してください。
要件
- 学習率 0.01, 0.1, 0.5, 2.0 の4パターンで学習する
- 各学習率での損失曲線をプロットする
- どの学習率が最適かを考察する
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分