第 2 章:矩阵与维度
这一章是理解所有代码的基础中的基础。如果你对矩阵感到陌生,请认真读完这一章再继续。
2.1 为什么要用矩阵?
先回顾第 1 章的单个神经元:
输入: x = [x₁, x₂, x₃, ..., x₇₈₄] (一张图片的 784 个像素)
权重: w = [w₁, w₂, w₃, ..., w₇₈₄]
计算: z = w₁x₁ + w₂x₂ + ... + w₇₈₄x₇₈₄ + b隐藏层有 50 个节点,每个节点都要做这样的计算。如果一个一个算,需要算 50 次。
更糟的是,我们训练时不是一张图片一张图片地处理,而是一次处理 100 张(批量处理)。那就需要 100 × 50 = 5000 次这样的计算。
矩阵乘法可以把这 5000 次计算用一行代码完成,而且 numpy 会在底层高度并行优化,速度极快。
2.2 基本概念回顾
标量(Scalar)
一个单独的数字。比如 loss = 0.35。在代码中就是 Python 的 float。
向量(Vector)
一列数字。可以理解为一维数组。
x = [0.0, 0.1, 0.8, 0.9, 0.0] # 长度为 5 的向量在 numpy 中:x.shape = (5,)
矩阵(Matrix)
二维数组,有行和列。
第1列 第2列 第3列
第1行 [0.1, 0.2, 0.3]
第2行 [0.4, 0.5, 0.6]
第3行 [0.7, 0.8, 0.9]
第4行 [1.0, 1.1, 1.2]这个矩阵有 4 行、3 列,在 numpy 中:shape = (4, 3)
记住这个约定:shape = (行数, 列数)
2.3 矩阵乘法规则
这是最关键的规则:
矩阵 A 的 shape = (m, n)
矩阵 B 的 shape = (n, p)
A @ B 的 shape = (m, p)
↑ ↑
这两个数必须相等(才能相乘)验证方法: 写成括号的形式,相邻的两个数必须相同,消掉后剩下的就是结果的形状:
(m, n) @ (n, p) = (m, p)
↑ ↑
相同,可以相乘具体例子
A shape = (3, 4)
B shape = (4, 2)
A @ B 的 shape = (3, 2) ✓ 因为中间的 4 和 4 相同A shape = (3, 4)
B shape = (3, 2)
A @ B = 错误! ✗ 因为 4 ≠ 3,无法相乘矩阵乘法的计算过程
设 A 是 (2, 3) 的矩阵,B 是 (3, 2) 的矩阵:
A = [[1, 2, 3], B = [[7, 8],
[4, 5, 6]] [9, 10],
[11, 12]]C = A @ B,结果是 (2, 2):
C[0,0] = 1×7 + 2×9 + 3×11 = 7 + 18 + 33 = 58
C[0,1] = 1×8 + 2×10 + 3×12 = 8 + 20 + 36 = 64
C[1,0] = 4×7 + 5×9 + 6×11 = 28 + 45 + 66 = 139
C[1,1] = 4×8 + 5×10 + 6×12 = 32 + 50 + 72 = 154
C = [[58, 64],
[139, 154]]结果的第 i 行第 j 列 = A 的第 i 行 与 B 的第 j 列 的点积(逐元素相乘再求和)
2.4 批量处理:为什么要用矩阵的真正原因
单样本情况
假设只处理一张图片:
- 输入 x 是一个向量:shape =
(784,) - 第一层权重 W1:shape =
(784, 50) - 计算:
a1 = x @ W1 + b1
x @ W1 = a1
(784,) @ (784, 50) = ???等等,这里有问题:向量 (784,) 和矩阵 (784, 50) 怎么相乘?
numpy 会把 (784,) 当成行向量 (1, 784) 来处理,结果是 (50,) ——隐藏层 50 个节点的值。
批量处理情况(实际使用)
实际训练时,我们一次处理 100 张图片(batch_size = 100):
- 输入 X 是一个矩阵:shape =
(100, 784)← 100 行,每行是一张图片 - 第一层权重 W1:shape =
(784, 50) - 计算:
a1 = X @ W1 + b1
X @ W1 = a1
(100, 784) @ (784, 50) = (100, 50)
↑ ↑
相同(784),消掉
剩下 (100, 50)结果 a1 的 shape = (100, 50):
- 100 行对应 100 张图片
- 50 列对应隐藏层的 50 个节点
- 一次矩阵乘法就算出了 100 张图片在 50 个节点上的值!
这就是批量处理的威力——用矩阵乘法一次计算多个样本。
2.5 广播(Broadcasting):加偏置时发生了什么
计算 a1 = X @ W1 + b1 中,+ b1 这一步是怎么做的?
X @ W1的 shape =(100, 50)b1的 shape =(50,)
(100, 50) + (50,) — 形状不同,怎么加?
numpy 的广播机制自动把 b1 的 (50,) 扩展成 (100, 50),即把同一行 b1 复制 100 次,再逐元素相加:
b1 = [b₁, b₂, ..., b₅₀] shape: (50,)
扩展后变成:
[[b₁, b₂, ..., b₅₀],
[b₁, b₂, ..., b₅₀], shape: (100, 50)
[b₁, b₂, ..., b₅₀],
...(共100行)
[b₁, b₂, ..., b₅₀]]直觉: 偏置 b1 是这一层的"固有偏好",对所有样本一视同仁,所以每个样本加的偏置是一样的。
2.6 转置(Transpose)
转置就是把矩阵的行和列互换,记作 A.T 或 Aᵀ:
A = [[1, 2, 3], A.T = [[1, 4],
[4, 5, 6]] [2, 5],
[3, 6]]
A 的 shape: (2, 3)
A.T 的 shape: (3, 2)反向传播时经常用到转置,因为梯度的传递方向和前向传播相反,维度也需要相应翻转。
2.7 逐元素运算(Element-wise)
除了矩阵乘法,还有一种运算:对应位置的元素各自做运算,结果形状不变。
A = [[1, 2], B = [[10, 20],
[3, 4]] [30, 40]]
A * B(逐元素乘)= [[1×10, 2×20], = [[10, 40],
[3×30, 4×40]] [90, 160]]激活函数就是逐元素运算:sigmoid(A) 就是对 A 里每个元素各自计算 sigmoid,形状不变。
2.8 本项目完整维度速查表
这是整个前向传播过程中,每个变量的 shape:
| 变量 | 含义 | Shape |
|---|---|---|
X | 输入数据(一批图片) | (n, 784) |
W1 | 第一层权重 | (784, 50) |
b1 | 第一层偏置 | (50,) |
a1 = X @ W1 + b1 | 第一层线性输出 | (n, 50) |
z1 = sigmoid(a1) | 第一层激活输出 | (n, 50) |
W2 | 第二层权重 | (50, 10) |
b2 | 第二层偏置 | (10,) |
a2 = z1 @ W2 + b2 | 第二层线性输出 | (n, 10) |
y = softmax(a2) | 输出概率分布 | (n, 10) |
t | 真实标签 | (n,) |
L | 损失值 | 标量 |
其中 n = batch_size = 100(训练时),或训练/测试集总样本数(评估时)。
2.9 维度推导练习
让我们一步步验证每个矩阵乘法是否合法:
第一层:
X @ W1 = ?
(n, 784) @ (784, 50) = (n, 50) ✓ 中间的 784 = 784 相同第二层:
z1 @ W2 = ?
(n, 50) @ (50, 10) = (n, 10) ✓ 中间的 50 = 50 相同反向传播(预习):
dy @ W2.T = ?
(n, 10) @ (10, 50) = (n, 50) ✓ W2.T 把 (50,10) 变成 (10,50)
X.T @ da1 = ?
(784, n) @ (n, 50) = (784, 50) ✓ 和 W1 的 shape 一样!(梯度和参数形状相同)重要直觉: 参数的梯度和参数本身形状始终相同。这是必然的——梯度 ∂L/∂W 告诉我们 W 中每个元素该如何调整,所以形状必须和 W 一致。
2.10 小结
| 概念 | 关键记忆 |
|---|---|
矩阵乘法 (m,n) @ (n,p) | 中间数必须相等,结果是 (m,p) |
| 批量处理 | X 是 (n,784) 而不是 (784,),n 是批量大小 |
| 广播 | (n,50) + (50,) → 偏置自动复制成 n 行 |
| 逐元素运算 | 激活函数不改变 shape |
| 转置 | A.T 把 shape (m,n) 变成 (n,m) |
下一章,我们来学习激活函数——为什么要用它,以及 Sigmoid、ReLU、Softmax 的完整推导。
← 第 1 章 | 返回目录 | 第 3 章:激活函数 →