行列積によるニューラルネットワークの順伝播

\(y = Wx + b\) を手書きし、異なる形状の行列を入力して、次元がどのように変化・対応するかを観察します。

このケースを学ぶと、以下が理解できるようになります:ニューラルネットワークのトレーニング中に shape mismatch エラーが発生するのは、本質的には行列積の次元が一致していないことが原因です。


身近な例から

宅配便の仕分けセンター

コンベアから運ばれてきた荷物(入力)が、仕分け機によって異なるトラック(隠れ層)に振り分けられ、最終的に各エリア(出力)へ配送されます。

各仕分け機は1つの「ニューロン」です。すべての荷物を受け取り、ルールに従ってどの荷物が自分の経路を通るかを決定します。仕分け機のルールこそが「重み」です。ある方向の荷物は多く分け、ある方向の荷物は少なく分けます。

仕分けセンター全体の仕組みは、次のとおりです:行列積 + 活性化関数 + バイアス。


直感的な理解

1層のニューラルネットワーク = 3つのステップを、1行のコードで実行:

\( z = X @ W + b \)

各部分には明確な役割があります:

X
入力データ
(batch, in)
W
重み行列
(in, out)
b
バイアス
(out,)
z
出力
(batch, out)
次元一致の鉄則:Aの列数はBの行数と等しくなければなりません!\(A_{(m \times n)} \cdot B_{(n \times p)} = C_{(m \times p)}\)

入力特徴数(in_features)は重み行列の行数と等しくなければなりません。出力特徴数(out_features)は重み行列の列数と等しくなります。batch_size はプロセス全体を通して変わりません。


数学的定義

単層の順伝播

\[ z_j = \sum_{i=1}^{n} x_i \cdot W_{ij} + b_j \]

各出力 \(z_j\) は、入力ベクトルと重み行列の第 \(j\) 列とのドット積に、バイアスを加えたものです。

2層ネットワークの直列接続

\[ \begin{aligned} h &= \text{ReLU}(X W_1 + b_1) \quad & (m \times 4) \cdot (4 \times 6) &= (m \times 6) \\ o &= \text{Sigmoid}(h W_2 + b_2) \quad & (m \times 6) \cdot (6 \times 2) &= (m \times 2) \end{aligned} \]

注意:第1層の出力特徴数(6)は、第2層の重み行列の行数(6)と等しくなければなりません。これが「層と層の間の次元のつなぎ目」です。


Pythonで実際にやってみる

汎用の dense_layer 関数を使って、単一サンプル・バッチ処理・2層ネットワークの3つのシナリオをデモンストレーションします。

実例

import numpy as np

def dense_layer(X, W, b, activation=None):
    """
1層全結合ネットワークの順伝播
X: 入力行列 (batch_size, in_features)
W: 重み行列 (in_features, out_features)
b: バイアスベクトル (out_features,)
    """

    z = X @ W + b   # 行列積 + ブロードキャスト加算
    if activation == "relu":
        return np.maximum(0, z)          # ReLU: 負の値を0にする
    if activation == "sigmoid":
        return 1 / (1 + np.exp(-z))      # Sigmoid: (0,1)に圧縮
    return z

print("=" * 55)
print("サンプル シナリオ 1:単一サンプル、4次元 -> 3次元")
print("=" * 55)
X = np.array([[1.0, 2.0, 0.5, -1.0]])     # (1, 4)
W = np.random.randn(4, 3) * 0.5            # (4, 3)
b = np.zeros(3)                            # (3,)
y = dense_layer(X, W, b, activation="relu")
print("X.shape:", X.shape, "  W.shape:", W.shape,
      "  ->  y.shape:", y.shape)

print("\n" + "=" * 55)
print("サンプル シナリオ 2:8サンプルのバッチ、同じ重み")
print("=" * 55)
X_batch = np.random.randn(8, 4)            # (8, 4)
y_batch = dense_layer(X_batch, W, b, activation="relu")
print("X_batch.shape:", X_batch.shape,
      "  ->  y_batch.shape:", y_batch.shape)
print("バッチサイズ8のまま、特徴次元は 4 -> 3")

print("\n" + "=" * 55)
print("サンプル シナリオ 3:2層ネットワークを重ねる")
print("=" * 55)
# 第1層:4次元 -> 6次元 + ReLU
W1 = np.random.randn(4, 6) * 0.5
b1 = np.zeros(6)
# 第2層:6次元 -> 2次元 + Sigmoid
W2 = np.random.randn(6, 2) * 0.5
b2 = np.zeros(2)

h = dense_layer(X_batch, W1, b1, activation="relu")
out = dense_layer(h, W2, b2, activation="sigmoid")

print(f"入力層: {X_batch.shape}")
print(f"隠れ層: {h.shape} (4->6, W1=(4,6))")
print(f"出力層: {out.shape} (6->2, W2=(6,2))")

# 検証:次元が一致しないとどうなるか?
print("\n" + "=" * 55)
print("サンプル シナリオ 4:次元が一致しないとエラーになる")
print("=" * 55)
try:
    W_wrong = np.random.randn(5, 3) * 0.5  # 行数が合わない
    dense_layer(X_batch, W_wrong, b)
except ValueError as e:
    print("エラーの原因:X_batch には4列ありますが、"
          "W_wrong には5行ある -> 乗算できません")
    print(f" 正しい方法:W の行数 ({W_wrong.shape
          f" は X の列数 ({X_batch.shape)
=======================================================
サンプル 场景 1:单样本,4 维 -> 3 维
=======================================================
X.shape: (1, 4)   W.shape: (4, 3)   ->  y.shape: (1, 3)

=======================================================
サンプル 场景 2:8 个样本批量,同权重
=======================================================
X_batch.shape: (8, 4)   ->  y_batch.shape: (8, 3)
批大小 8 不变,特征维度 4 -> 3

=======================================================
サンプル 场景 3:堆叠两层网络
=======================================================
输入层: (8, 4)
隐藏层: (8, 6)  (4->6, W1=(4,6))
输出层: (8, 2)  (6->2, W2=(6,2))

=======================================================
サンプル 场景 4:维度不匹配会报错
=======================================================
报错原因:X_batch 有 4 列,W_wrong 有 5 行 -> 无法相乘
  正确做法:W 的行数 (5) 应等于 X 的列数 (4)

AIでの応用シーン

AI シナリオ次元変化の例説明
全結合層(32, 784) @ (784, 128) = (32, 128)MNIST分類:28x28=784 ピクセル → 128次元特徴
Transformer の QKV(B, L, D) @ (D, D) = (B, L, D)自己注意:入力と出力のトークン次元は変わらない
Embedding ルックアップテーブル(B, L) → (B, L, D)単語IDシーケンス → 単語ベクトルシーケンス
分類ヘッド(B, 768) @ (768, 10) = (B, 10)BERT の最終層の分類出力
その他の拡張