導入
部品がそろいました。仕上げに、直線では分けられないデータを、numpy 製の小さな NN で分類してみます。ここまでの順伝播・逆伝播・勾配降下法が、1つの「学習するプログラム」として動く瞬間です。
説明
題材は有名な XOR 問題。(0,0)と(1,1)はクラス0、(0,1)と(1,0)はクラス1。1本の直線ではどう引いても分けられません。隠れ層のある NN なら、非線形な境界を学習して解けます。
flowchart LR in["入力 (x1, x2)"] --> hid["隠れ層(4)<br/>sigmoid"] hid --> out["出力(1)<br/>sigmoid = クラス1の確率"] out --> loss["損失で誤差を測り<br/>逆伝播で全重み更新"] loss -. 繰り返す .-> in
やってみよう
下のコードは、隠れ層4つの NN を逆伝播で訓練し、XOR を解きます。実行すると損失が下がり、最後に4つの入力すべてを正しく分類できるようになります。直線では不可能な問題を、自作 NN が解く瞬間を見てください。
import numpy as np
def sigmoid(z):
return 1 / (1 + np.exp(-z))
# XOR データ
X = np.array([[0,0],[0,1],[1,0],[1,1]], dtype=float)
Y = np.array([[0],[1],[1],[0]], dtype=float)
np.random.seed(42)
W1 = np.random.randn(2, 4) # 入力2 → 隠れ4
b1 = np.zeros(4)
W2 = np.random.randn(4, 1) # 隠れ4 → 出力1
b2 = np.zeros(1)
lr = 0.5
for epoch in range(5000):
# 順伝播
A1 = sigmoid(X @ W1 + b1)
P = sigmoid(A1 @ W2 + b2)
# 逆伝播
dP = (P - Y) * P * (1 - P)
dW2 = A1.T @ dP
db2 = dP.sum(axis=0)
dA1 = (dP @ W2.T) * A1 * (1 - A1)
dW1 = X.T @ dA1
db1 = dA1.sum(axis=0)
# 更新
W2 -= lr * dW2; b2 -= lr * db2
W1 -= lr * dW1; b1 -= lr * db1
if epoch % 1000 == 0:
loss = np.mean((P - Y) ** 2)
print(f"epoch {epoch:4d}: loss = {loss:.4f}")
pred = sigmoid(sigmoid(X @ W1 + b1) @ W2 + b2)
print("\n最終予測(0に近い=クラス0, 1に近い=クラス1):")
for xi, pi in zip(X, pred):
print(f" 入力 {xi} → {pi[0]:.3f} → 判定クラス {int(pi[0] > 0.5)}")
演習
隠れ層のニューロン数を 4 から 2 に減らして実行してみましょう。学習が難しくなり、損失の下がりが悪くなる(うまく解けないことがある)のを観察してください。表現力=隠れニューロン数の大切さを体感します。
ヒント1を見る
W1 を np.random.randn(2, 2)、b1 を np.zeros(2)、W2 を np.random.randn(2, 1) に変えます。
ヒント2を見る
ニューロンが少ないと表現力が足りず、XORのような問題は解けたり解けなかったりします。乱数シードを変えると結果が変わることも確かめてみましょう。
まとめ
- 隠れ層のある NN は、**直線では分けられない問題(XOR)**も学習で解ける。
- 順伝播・逆伝播・勾配降下法を組み合わせれば、それが「学習するプログラム」になる。
- ここまで numpy で自作した一連の処理を、次章から PyTorch が自動化してくれる。