Skip to content

第 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.TAᵀ

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 章:激活函数 →

基于 Kaggle MNIST 数据集,使用纯 numpy 从零实现