矩阵乘法实现神经网络前向传播
手写 \(y = Wx + b\),输入不同形状的矩阵,观察维度如何变化和匹配。
学完本案例你将理解:神经网络训练时报错 shape mismatch,本质就是矩阵乘法维度对不上。
生活引入
快递分拣中心
一批包裹从传送带进来(输入),经过分拣机分流到不同的货车(隐藏层),最终配送到各个小区(输出)。
每台分拣机就是一个「神经元」——它接收所有包裹,按规则决定哪些包裹走自己的路。分拣机的规则就是「权重」:某个方向的包裹多分一些,某个方向的少分一些。
整个分拣中心的运作就是:矩阵乘法 + 激活函数 + 偏置。
直观理解
一层神经网络 = 三个步骤,一行代码搞定:
\( z = X @ W + b \)
其中每个部分都有明确的角色:
X
输入数据
(batch, in)
(batch, in)
W
权重矩阵
(in, out)
(in, out)
b
偏置
(out,)
(out,)
z
输出
(batch, out)
(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\) 列的点积,再加上偏置。
两层网络串联
\[ \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} \]注意:第一层输出的特征数(6)必须等于第二层权重矩阵的行数(6)。这就是「层与层之间的维度衔接」。
Python 动手实践
用一个通用的 dense_layer 函数,演示单样本、批处理、两层网络三种场景。
实例
import numpy as np
def dense_layer(X, W, b, activation=None):
"""
一层全连接网络的前向传播
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("RUNOOB 场景 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("RUNOOB 场景 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("RUNOOB 场景 3:堆叠两层网络")
print("=" * 55)
# 第一层:4 维 -> 6 维 + ReLU
W1 = np.random.randn(4, 6) * 0.5
b1 = np.zeros(6)
# 第二层: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("RUNOOB 场景 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[0]})"
f" 应等于 X 的列数 ({X_batch.shape[1]})")
def dense_layer(X, W, b, activation=None):
"""
一层全连接网络的前向传播
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("RUNOOB 场景 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("RUNOOB 场景 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("RUNOOB 场景 3:堆叠两层网络")
print("=" * 55)
# 第一层:4 维 -> 6 维 + ReLU
W1 = np.random.randn(4, 6) * 0.5
b1 = np.zeros(6)
# 第二层: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("RUNOOB 场景 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[0]})"
f" 应等于 X 的列数 ({X_batch.shape[1]})")
======================================================= RUNOOB 场景 1:单样本,4 维 -> 3 维 ======================================================= X.shape: (1, 4) W.shape: (4, 3) -> y.shape: (1, 3) ======================================================= RUNOOB 场景 2:8 个样本批量,同权重 ======================================================= X_batch.shape: (8, 4) -> y_batch.shape: (8, 3) 批大小 8 不变,特征维度 4 -> 3 ======================================================= RUNOOB 场景 3:堆叠两层网络 ======================================================= 输入层: (8, 4) 隐藏层: (8, 6) (4->6, W1=(4,6)) 输出层: (8, 2) (6->2, W2=(6,2)) ======================================================= RUNOOB 场景 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) | 自注意力:输入和输出的 token 维度不变 |
| Embedding 查表 | (B, L) → (B, L, D) | 词 ID 序列 → 词向量序列 |
| 分类头 | (B, 768) @ (768, 10) = (B, 10) | BERT 最后一层的分类输出 |
