コンテンツにスキップ

6.1.3 ニューロンから多層パーセプトロンへ

ニューロンから MLP への構造図

この節では、小さな PyTorch 実験を実行します。

  • 人工ニューロンを手で計算する;
  • sigmoidReLU を比較する;
  • 小さな MLP で XOR を解く;
  • 1 つの線形層だけでは足りない理由を説明する。

中心となる流れは次です。

featuresweighted sum zactivation alayermultilayer network

ニューロンの線形スコアと活性化ゲート図

パーセプトロンが人々を興奮させたのは、機械がデータからルールを学べることを示したからです。その後、XOR のような単純な非線形パターンを単層パーセプトロンが解けないことが明らかになりました。

この歴史から学ぶべきことは次です。

ニューロン自体は単純。表現力を生むのは、非線形活性化を持つ層の積み重ね。

XOR における単層パーセプトロンの限界図

Terminal window
python -m pip install -U torch

コードでは安定した PyTorch API を使います:torch.Tensornn.Modulenn.Sequentialnn.Linear、活性化関数、loss、optimizer です。

neuron_mlp_lab.py を作成します。

import torch
import torch.nn as nn
torch.manual_seed(42)
x = torch.tensor([[0.8, 0.3, 0.5]])
w = torch.tensor([[0.2], [-0.4], [0.6]])
b = torch.tensor([0.1])
z = x @ w + b
print("single_neuron")
print("z=", round(float(z.item()), 3))
print("sigmoid=", round(float(torch.sigmoid(z).item()), 3))
print("relu=", round(float(torch.relu(z).item()), 3))
xor_x = torch.tensor([[0., 0.], [0., 1.], [1., 0.], [1., 1.]])
xor_y = torch.tensor([[0.], [1.], [1.], [0.]])
class TinyMLP(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(2, 4),
nn.Tanh(),
nn.Linear(4, 1),
nn.Sigmoid(),
)
def forward(self, x):
return self.net(x)
model = TinyMLP()
loss_fn = nn.BCELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.1)
for step in range(2000):
pred = model(xor_x)
loss = loss_fn(pred, xor_y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
with torch.no_grad():
prob = model(xor_x)
pred = (prob >= 0.5).float()
print("xor_mlp")
for row, p, y_hat in zip(xor_x.tolist(), prob.squeeze().tolist(), pred.squeeze().tolist()):
print(f"x={row} prob={p:.3f} pred={int(y_hat)}")
print("final_loss=", round(float(loss.item()), 4))

実行します。

Terminal window
python neuron_mlp_lab.py

期待される出力:

Terminal window
single_neuron
z= 0.44
sigmoid= 0.608
relu= 0.44
xor_mlp
x=[0.0, 0.0] prob=0.000 pred=0
x=[0.0, 1.0] prob=1.000 pred=1
x=[1.0, 0.0] prob=1.000 pred=1
x=[1.0, 1.0] prob=0.000 pred=0
final_loss= 0.0001

ニューロンと XOR 実験結果図

最初の部分は次を計算しています。

z = x @ w + b

出力では:

z= 0.44
sigmoid= 0.608
relu= 0.44

重み付きスコア z はまだ線形です。活性化関数が、信号をどう次へ渡すかを変えます。

活性化することよく使う場面
Sigmoid0-1 に押し込む二値分類の確率出力
Tanh-1 から 1 に押し込む小さなデモ、一部の系列モデル
ReLU正の値を残し、負の値を 0 にする隠れ層の一般的な既定選択

線形層だけを積み重ねても、全体としては 1 つの大きな線形層と等価です。非線形活性化があるから、層を重ねたネットワークは曲がった境界を表現できます。

そのため、この MLP は次を使います。

nn.Linear(2, 4),
nn.Tanh(),
nn.Linear(4, 1),
nn.Sigmoid(),

隠れ層の Tanh が非線形の表現力を与えます。最後の Sigmoid は、二値分類向けの確率らしい値に変換します。

XOR が古典的なテストである理由

Section titled “XOR が古典的なテストである理由”

XOR は 4 行だけです。

x1x2y
000
011
101
110

直線 1 本では、このラベルを分けられません。だから単層パーセプトロンは失敗します。小さな MLP が成功するのは、最終判断の前に中間の隠れ特徴を作れるからです。

次の小さな結果カードを残します。

単一ニューロン
z = x @ w + b、活性化がシグナルを変える
xor結果
小さな MLP で [0, 1, 1, 0] を復元
中核理由
非線形な隠れ層が中間特徴を作る
失敗プローブ
隠れ活性を取り除いて final_loss を比較する

重要なのは、toy model が 4 行を覚えたことではありません。非線形性によって、層を重ねたモデルが表現できる形が変わることです。

症状よくある原因修正
loss が下がらない学習率が高すぎる/低すぎる、loss の組み合わせが違うLR を下げ、出力活性化と loss の組み合わせを確認する
確率がすべて 0.5 付近モデルが学習していない長く訓練し、勾配を見て、hidden size を変える
output shape エラーtarget shape と prediction が違うこの二値例では target を [batch, 1] にする
nan が出る学習が不安定学習率を下げ、入力を確認する
訓練データは解けるが実データで弱い訓練データを記憶しているtrain/validation split と正則化を使う
  1. 隠れユニットを 4 から 2 に変えてください。XOR は安定して学習できますか?
  2. nn.Tanh()nn.ReLU() に置き換えてください。結果は変わりますか?
  3. 200 step ごとに loss を表示し、学習曲線を見てください。
  4. 隠れ層の活性化関数を外し、なぜ弱くなるか説明してください。
  5. 隠れ層をもう 1 つ追加し、final loss を比較してください。
参考実装と解説
  1. 隠れユニットを 2 にすると表現力が下がり、XOR の学習は不安定になりやすいです。複数回実行して seed による差も確認してください。
  2. ReLU でも学習できることがありますが、小さな XOR では初期値や学習率の影響を受けやすく、Tanh より曲線が荒れる場合があります。
  3. 200 step ごとの loss は、学習が進んでいるか、途中で止まっているかを確認するための最小ログです。
  4. 活性化関数を外すと、線形層を重ねても全体は線形変換のままです。XOR のような非線形境界を表しにくくなります。
  5. 隠れ層を追加すると容量は増えますが、小さなデータでは必ず改善するとは限りません。final loss と安定性を両方見ます。

次を説明できれば、この節はクリアです。

  • ニューロンは x @ w + b を計算し、その後に活性化を適用する;
  • 活性化関数は非線形性を加える;
  • 単層パーセプトロンは XOR を解けない;
  • MLP は層を積み重ねて中間特徴を作る;
  • PyTorch モデルは通常 nn.Module、loss、optimizer、backward()step() を組み合わせる。