1. 順伝播(forward)とは
順伝播は、入力データをネットワークの層に順番に通して出力(予測)を得る1回の流れです。 理論では「線形変換 → 活性化 → 線形変換 → …」と習ったはず。これをNumPyのコードに落とすのがこのレッスンです。
PyTorch では model(x) の一行で順伝播が走りますが、中で何が起きているかを一度自分の手で書いておくと、
PyTorch のコードを読むときに「ここで行列積、ここで活性化」と迷わず追えるようになります。この感覚が、後半でPyTorchのコードを読み解く土台になります。
z = xW + b。W は重み行列、b はバイアス。
活性化関数は非線形性を加える役割でした。ここではその計算そのものを NumPy で確かめます。
2. 全結合層を「行列積」で書く
ミニバッチ(複数サンプルをまとめた入力)X の形を (N, D_in) とします(N=サンプル数、D_in=入力特徴数)。
重み W を (D_in, D_out)、バイアス b を (D_out,) とすると、出力は z = X @ W + b で形は (N, D_out) になります。
@ は行列積(NumPyの matmul)です。
💡 shape の読み方:
行列積 (4, 3) @ (3, 2) は、内側の3が一致していれば計算でき、結果は外側を取って (4, 2)。
バイアス b は形 (2,) ですが、ブロードキャストで各サンプル(各行)に同じ値が足されます。
⚠ よくある shape ミス:
W を (2, 3) の向きで作ると X @ W は 内側が一致せずエラーになります((4,3)@(2,3) は不可)。
「入力の特徴数が W の最初の次元」と覚えましょう。
3. 活性化関数(ReLU・シグモイド)
線形変換だけを重ねても表現力は上がりません。間に非線形な活性化関数を挟みます。 代表的な ReLU とシグモイドを NumPy で実装します。どちらも要素ごとの計算なので、形(shape)は変わりません。
max(0, x)、シグモイドは 1/(1+e^-x)。
PyTorch では torch.relu() や nn.ReLU()、torch.sigmoid() が同じ計算をしてくれます(L05で登場)。
中身は今書いたとおりです。
4. 多層パーセプトロンの順伝播 + ソフトマックス
いよいよ2層のネットワークを順伝播させます。入力 → 全結合(1層目) → ReLU → 全結合(2層目) → ソフトマックスの流れです。 最後のソフトマックスは、出力(ロジット)を合計1の確率に変換します(多クラス分類の定番)。
💡 ソフトマックスの axis=1:
入力が (N, クラス数) のとき、クラス方向(axis=1)で合計1にします。
axis を間違えて縦方向(サンプル方向)で正規化してしまうと、確率の意味が変わってしまうので注意しましょう。
💡 keepdims=True をかみ砕くと:
これは「合計や最大を取ってつぶれた軸を“長さ1”として残す」オプションです。
そのあとの割り算(ブロードキャスト)を成立させるために必要になります。形(shape)で具体的に見てみましょう。
e.shape → (4, 2) # 各行に2クラスの値
e.sum(axis=1) → (4,) # 列がつぶれて 1次元 になる
e.sum(axis=1, keepdims=True) → (4, 1) # 列を「長さ1」で残す
この差が、割り算でそのまま効いてきます。
e / e.sum(axis=1, keepdims=True) # (4,2) ÷ (4,1) → OK
# → (4,1) の1列が「2クラスぶんに自動コピー」され、
# 各行がその行の合計で割られて、行ごとに合計1になる
e / e.sum(axis=1) # (4,2) ÷ (4,) → ValueError!
# → 右端の軸(クラス数2)と長さ4が合わず、割り算できない
イメージは「各行を、その行の合計で割って確率にする」。
そのためには割る相手を (4, 1) という「縦に並んだ1列」の形にしておく必要があり、それを作るのが keepdims=True です。
実際に上のセルから keepdims=True を消すと、エラーになるのを確かめられます。
⚠ オーバーフロー対策:
softmax で x - x.max(...) と最大値を引くのは、exp が巨大な値になってあふれる(inf になる)のを防ぐためです。
数学的な結果は変わりません。PyTorch の softmax も内部で同じ工夫をしています。
5. 練習問題
コードの空欄を埋めて「▶ 実行」しましょう。「↺ リセット」で元に戻せます。
全結合層の出力 shape を合わせる
入力 X が (8, 6) のとき、出力が (8, 4) になるように重み W とバイアス b の形を決め、z = X @ W + b を計算してください。
ヒントを見る(答え+解説)
import numpy as np
np.random.seed(1)
X = np.random.randn(8, 6) # (N=8, D_in=6)
# W と b の形を決めて、z = X @ W + b を計算してください(zの形は (8, 4) になるはず)
W = np.random.randn(6, 4) # (D_in=6, D_out=4)
b = np.zeros(4) # (D_out=4,)
z = X @ W + b
print("z.shape:", z.shape) # (8, 4) になればOK
入力の特徴数 6 が W の最初の次元、欲しい出力数 4 が2番目の次元。
バイアスは出力数ぶんの 4。これで (8,6)@(6,4)=(8,4) になります。
ReLU を通す
配列 z に ReLU を適用して、負の値が0になることを確かめてください。
ヒントを見る(答え+解説)
import numpy as np
z = np.array([[-2.0, 0.5], [1.5, -0.3]])
# ReLU を適用してください(負を0に、正はそのまま)
a = np.maximum(0, z)
print("a =\n", a)
# 期待: [[0. 0.5]
# [1.5 0. ]]
np.maximum(0, z) は z の各要素と0を比べて大きい方を取るので、負の値だけが0になります。形は変わりません。
ソフトマックスで確率にする
ロジット logits(形 (2, 3))を、各行で合計1の確率に変換してください(axis=1 に注意)。
ヒントを見る(答え+解説)
import numpy as np
logits = np.array([[1.0, 2.0, 3.0],
[1.0, 1.0, 1.0]])
# 各行で合計1になるソフトマックスを計算してください
e = np.exp(logits - logits.max(axis=1, keepdims=True))
prob = e / e.sum(axis=1, keepdims=True)
print("prob =\n", np.round(prob, 3))
print("各行の合計:", np.round(prob.sum(axis=1), 3)) # [1. 1.]
2行目は全部同じ値なので、確率は [0.333, 0.333, 0.333] と均等になります。
keepdims=True を外すと形が崩れてブロードキャストに失敗するので注意。
6. まとめ
このレッスンのポイント
- 全結合層は
z = X @ W + b。形は(N, D_in) → (N, D_out) - 行列積は内側の次元が一致すれば計算でき、結果は外側の形になる
- バイアスはブロードキャストで各サンプルに加算される
- 活性化(ReLU・シグモイド)は要素ごとの計算で形を変えない
- ソフトマックスは
axis=1で各行を合計1の確率にする(最大値を引いてオーバーフロー対策) - 順伝播は 線形 → 活性化 → 線形 → ソフトマックス の流れ。PyTorch の
model(x)はこれを自動でやっている
最後に、2層の順伝播を関数にまとめて自由に試してみましょう。
完了するとコース一覧に進捗が記録されます