AI 数学课程 · 第 20 课,共 180

用于 AI 的矩阵-矩阵乘积:复合变换与批处理

检查矩阵形状,计算一个小型矩阵乘积,并将同一个缩并运算理解为复合映射或批量稠密层计算。

分享这篇文章

稠密层对一个输入或同时对许多输入执行的是同一类计算。对于单个输入,矩阵-向量乘积将一行权重与特征向量结合起来。对于一个批次,输入矩阵的各行会与每条输出规则结合。矩阵-矩阵乘法正是让这种复用成为可能的紧凑记号。

学完本课后,你将能够检查形状是否兼容,计算一个小型矩阵乘积,解释输出维度,并将同一个乘积理解为复合变换或一批矩阵-向量乘积。

**学习成果:**检查兼容的形状,计算一个小型矩阵乘积,解释输出维度,并将同一个乘积理解为复合变换或一批矩阵-向量乘积。

第 19 课:作为加权和的矩阵-向量乘积逐次处理一个输入。官方的 Mathematics for Machine Learning companion 在本书第 2.2 节中介绍了矩阵乘积。这里的示例、代码、可视化和评估题都是基于该定义的原创应用。

内部维度决定乘积是否存在

设

A∈Rm×n,B∈Rn×p.A\in\mathbb{R}^{m\times n}, \qquad B\in\mathbb{R}^{n\times p}.

由于 AA 的列数等于 BB 的行数,乘积 C=ABC=AB 有定义。它保留两个外部维度作为形状:

A⏟m×nB⏟n×p=C=AB⏟m×p.\underbrace{A}_{m\times n} \underbrace{B}_{n\times p} = \underbrace{C=AB}_{m\times p}.

共享维度 nn 告诉我们,每个输出元素要将多少对对齐的数相乘并相加。外部维度 mm 和 pp 告诉我们结果有多少行和多少列。换句话说,输出形状是 (m,p)(m,p),而不是 (m,n)(m,n),也不是 (n,p)(n,p)。

这个规则是在共享轴上进行缩并。它并不是把相同位置的元素直接相乘。这个区别在代码中很重要:NumPy 将 numpy.matmul 记录为矩阵乘积,并在普通二维情形下使用 @ 作为运算符简写(文档)。

形状流模拟器让轴保持可见

下面的可视化从一个完整且兼容的乘积开始。选择其他预设,可以看到其他有效的外部形状;在直接的“行乘列”读法与批处理读法之间切换;也可以使用原生控件追踪一个输出元素。每种状态下方都有语义表,因此即使没有 JavaScript、颜色或动画,计算仍然清晰可读。

探索兼容的形状、一个点积,以及将同一缩并运算理解为批处理。
探索形状流

每个选项都有匹配的内部维度;外部维度决定输出形状。

将同一乘积理解为

使用指针或键盘移动滑块。表格始终作为完整的静态参考保留。

正在追踪 C11:第 1 行,第 1 列;已包含 3 个乘积中的 3 个。

左矩阵 × 右矩阵 → 输出矩阵

行乘列追踪

C11 = 1 × 2 + (-1) × 3 + 2 × 0 = -1

累计值: -1 · 完整点积

语义表:每个输出元素、对应的左行、右列,以及以下乘积的最终点积: A × B = C
输出元素左行右列点积
C11(第 1 行,第 1 列)[1, -1, 2][2, 3, 0]-1
C12(第 1 行,第 2 列)[1, -1, 2][1, -1, 4]10
C21(第 2 行,第 1 列)[2, 0, 1][2, 3, 0]4
C22(第 2 行,第 2 列)[2, 0, 1][1, -1, 4]6
语义表:将同一乘法理解为按行批处理: Bᵀ × Aᵀ = Cᵀ
批次行输入行(右矩阵的一列)输出行
1[2, 3, 0][-1, 4]
2[1, -1, 4][10, 6]

这些表格会随课程一起渲染,并且在没有 JavaScript 时仍保持可见。聚焦控件、文字标签和高亮星号为交互状态提供了不依赖颜色的提示。

服务器渲染的初始状态会在可选的客户端交互显示之前展示完整乘积。滑块显示的是部分和,而不是新的数学内容:它只决定当前显示同一个点积中的多少项。在批处理模式中,显示的行都是完整示例,而表格是它们永久的回退内容。

一个小型乘积揭示四个点积

考虑原始矩阵

A=[1−12201],B=[213−104].A= \begin{bmatrix} 1 & -1 & 2\\ 2 & 0 & 1 \end{bmatrix}, \qquad B= \begin{bmatrix} 2 & 1\\ 3 & -1\\ 0 & 4 \end{bmatrix}.

先检查形状:

(2×3)(3×2)⟶(2×2).(2\times3)(3\times2)\longrightarrow(2\times2).

两个内部维度都是 33,所以乘积存在。两个外部维度预测出四个输出元素,每个元素都由三次标量乘法形成。

每个答案所在的位置也是结果的一部分。例如,c12c_{12} 并不是扁平列表中的“第二次乘法”;它是第 11 行第 22 列的元素,并且始终将 AA 的第 11 行与 BB 的第 22 列配对。

同一个乘积按固定顺序复合变换

矩阵形状也描述定义域和值域。在这里,BB 将 R2\mathbb{R}^{2} 映射到 R3\mathbb{R}^{3},而 AA 将 R3\mathbb{R}^{3} 映射到 R2\mathbb{R}^{2}:

R2→  B  R3→  A  R2.\mathbb{R}^{2} \xrightarrow{\;B\;} \mathbb{R}^{3} \xrightarrow{\;A\;} \mathbb{R}^{2}.

因此,ABAB 是“先应用 BB,再应用 AA”这一过程对应的单个矩阵。设

x=[12],\boldsymbol{x}=\begin{bmatrix}1\\2\end{bmatrix},

中间向量为

Bx=[2⋅1+1⋅23⋅1+(−1)⋅20⋅1+4⋅2]=[418].B\boldsymbol{x} = \begin{bmatrix} 2\cdot1+1\cdot2\\ 3\cdot1+(-1)\cdot2\\ 0\cdot1+4\cdot2 \end{bmatrix} = \begin{bmatrix}4\\1\\8\end{bmatrix}.

接着应用 AA 得到

A(Bx)=[1⋅4+(−1)⋅1+2⋅82⋅4+0⋅1+1⋅8]=[1916].A(B\boldsymbol{x}) = \begin{bmatrix} 1\cdot4+(-1)\cdot1+2\cdot8\\ 2\cdot4+0\cdot1+1\cdot8 \end{bmatrix} = \begin{bmatrix}19\\16\end{bmatrix}.

矩阵乘积无需实际构造中间向量,就能得到同一个终点:

Cx=[−11046][12]=[1916].C\boldsymbol{x} = \begin{bmatrix}-1&10\\4&6\end{bmatrix} \begin{bmatrix}1\\2\end{bmatrix} = \begin{bmatrix}19\\16\end{bmatrix}.

顺序不是装饰。反向乘积在这里是有定义的,但它的形状和含义不同:

BA=[4−251−35804]∈R3×3.BA= \begin{bmatrix} 4 & -2 & 5\\ 1 & -3 & 5\\ 8 & 0 & 4 \end{bmatrix}\in\mathbb{R}^{3\times3}.

BABA 将 R3\mathbb{R}^{3} 映射到 R3\mathbb{R}^{3},因此不能在两个坐标的向量 x\boldsymbol{x} 上替代 ABAB。本书第 2.2 节也强调矩阵乘法通常不满足交换律;定义域和值域的图景解释了为什么交换顺序会改变计算(书籍 PDF)。

这个缩并运算是一批矩阵-向量乘积

假设有三个样本,每个样本有三个特征。将每个样本放在一行中:

X=[120012−101]∈R3×3.X= \begin{bmatrix} 1&2&0\\ 0&1&2\\ -1&0&1 \end{bmatrix}\in\mathbb{R}^{3\times3}.

使用前面的 AA 作为稠密层权重矩阵,并将它记为 WW。它的形状是 (2,3)(2,3):三个输入特征和两个输出分数。按行组织的批计算需要转置 WTW^{\mathsf T}:

XWT=[120012−101][12−1021]=[−12321−1].XW^{\mathsf T} = \begin{bmatrix} 1&2&0\\ 0&1&2\\ -1&0&1 \end{bmatrix} \begin{bmatrix} 1&2\\ -1&0\\ 2&1 \end{bmatrix} = \begin{bmatrix} -1&2\\ 3&2\\ 1&-1 \end{bmatrix}.

形状计算是

(3×3)(3×2)⟶(3×2).(3\times3)(3\times2)\longrightarrow(3\times2).

第一个维度 33 是样本数。第二个维度 22 是每个样本的输出分数数目。将偏置向量

b=[1−1]\boldsymbol{b}=\begin{bmatrix}1&-1\end{bmatrix}

加到每一行,得到

Y=XWT+b=[01412−2].Y=XW^{\mathsf T}+\boldsymbol{b} = \begin{bmatrix} 0&1\\ 4&1\\ 2&-2 \end{bmatrix}.

NumPy 的广播规则允许形状为 (2,)(2,) 的偏置与每个形状为 (3,2)(3,2) 的行的最后一个轴对齐(广播文档)。矩阵乘积本身仍然是缩并;偏置相加是单独的逐元素运算。

这种按行批处理的约定也是 torch.nn.Linear 所采用的约定:当 in_features=3 且 out_features=2 时,PyTorch 存储形状为 (2,3)(2,3) 的权重,并计算 y=xWT+by=xW^{\mathsf T}+b。因此,形状为 (3,3)(3,3) 的输入会返回形状为 (3,2)(3,2) 的结果。转置连接了数学上的列向量约定与实现中每个样本为一行的约定。对于含有更多批次维度的张量,torch.matmul 会保留矩阵乘积的最后两个轴,并广播兼容的前导批次轴。

还可以用另一种方式理解同一个结果。BB 的列是映射 AA 的两个三坐标输入。将 BB 转置,把这些输入放入各行:

BTAT=[2301−14][12−1021]=[−14106]=CT.B^{\mathsf T}A^{\mathsf T} = \begin{bmatrix} 2&3&0\\ 1&-1&4 \end{bmatrix} \begin{bmatrix} 1&2\\ -1&0\\ 2&1 \end{bmatrix} = \begin{bmatrix} -1&4\\ 10&6 \end{bmatrix} =C^{\mathsf T}.

BTB^{\mathsf T} 的每一行都是 BB 的一列,而 CTC^{\mathsf T} 的每一行都是 AA 对相应输入的输出。恒等式 (AB)T=BTAT(AB)^{\mathsf T}=B^{\mathsf T}A^{\mathsf T} 因此连接了“每列是一个独立输入”和“各行组成一个批次”这两种表示。

NumPy 让这个缩并运算可测试

下面的小程序显式写出三个嵌套循环,并将结果与 NumPy 对照。它还检查批处理恒等式,并将逐元素运算符清楚地分开。NumPy 的快速入门介绍了逐元素运算与矩阵乘积之间的同一区别。

import numpy as np

A = np.array([[1, -1, 2], [2, 0, 1]])
B = np.array([[2, 1], [3, -1], [0, 4]])

def matrix_product_by_loops(left, right):
    if left.shape[1] != right.shape[0]:
        raise ValueError("inner dimensions do not match")

    result = np.zeros((left.shape[0], right.shape[1]), dtype=int)
    for i in range(left.shape[0]):
        for j in range(right.shape[1]):
            for ell in range(left.shape[1]):
                result[i, j] += left[i, ell] * right[ell, j]
    return result

C_by_loops = matrix_product_by_loops(A, B)
C = A @ B
np.testing.assert_array_equal(C_by_loops, C)
np.testing.assert_array_equal(B.T @ A.T, C.T)

print(C)
print("elementwise A * B.T has shape", (A * B.T).shape)

try:
    A @ np.ones((2, 2), dtype=int)
except ValueError as error:
    print(type(error).__name__, "for incompatible inner dimensions")

程序输出

[[-1 10]
 [ 4  6]]
elementwise A * B.T has shape (2, 3)
ValueError for incompatible inner dimensions

由于 AA 有三列,而第二个操作数有两行,无效调用会失败。逐元素表达式 A * B.T 在这里不会失败,因为两个数组的形状都是 (2,3)(2,3);它会产生

A∗BT=[2−30204],A\mathbin{*}B^{\mathsf T} = \begin{bmatrix}2&-3&0\\2&0&4\end{bmatrix},

其中没有按行与按列配对后对三个项求和的过程。NumPy 将 multiply 记录为逐元素乘法(参考)。两个运算符接受相同操作数,并不能证明它们表示同一个数学运算。

单位结果与零结果是有用的边界情况

两个边界案例可以进一步明确形状规则。首先,兼容的单位矩阵不会改变数值:

I2C=C=CI2,I2=[1001].I_2C=C=CI_2, \qquad I_2=\begin{bmatrix}1&0\\0&1\end{bmatrix}.

这里左侧的单位矩阵必须有两行两列,因为 CC 是 (2,2)(2,2)。一般来说,对于 A∈Rm×nA\in\mathbb{R}^{m\times n},有 ImA=AI_mA=A;当维度匹配时,AIn=AAI_n=A。单位矩阵保留有效乘积,但不能修复无效乘积。

其次,有效乘积可能因为各项抵消而全为零。设

P=[1−100],Q=[3333].P=\begin{bmatrix}1&-1\\0&0\end{bmatrix}, \qquad Q=\begin{bmatrix}3&3\\3&3\end{bmatrix}.

那么

PQ=[1⋅3+(−1)⋅31⋅3+(−1)⋅300]=[0000].PQ=\begin{bmatrix}1\cdot3+(-1)\cdot3&1\cdot3+(-1)\cdot3\\0&0\end{bmatrix} =\begin{bmatrix}0&0\\0&0\end{bmatrix}.

内部维度匹配,所以零结果是一次计算,而不是形状错误。调试稠密层时,要同时检查这两类证据:形状契约告诉你运算是否合法,而实际数值告诉你输出是否由抵消、零权重或数据问题造成。

可复用的习惯很简短:写出左形状,写出右形状,匹配内部维度,保留外部维度,然后从左行和右列计算每个输出元素。这个习惯可以从小型演算示例扩展到 X @ W.T + b、学习映射的复合,以及更大的批量张量运算。

检查你的理解

问题 1

如果 L 的形状是 4 × 3,R 的形状是 3 × 5,那么 LR 的形状是什么?完整乘积需要多少次标量乘法?

显示分步解答

**步骤 1:检查共享轴。**左矩阵有 3 列,右矩阵有 3 行,因此内部维度匹配。

**步骤 2:保留外部轴。**乘积的形状是 (4,5)(4,5)。

**步骤 3:计算工作量。**共有 4⋅5=204\cdot5=20 个输出元素。每个元素使用长度为 3 的点积,因此直接计算需要 20⋅3=6020\cdot3=60 次标量乘法。

问题 2

计算 P Q,其中 P = [[1, 0, −2], [2, 1, 1]],Q = [[3, 1], [0, −2], [4, 2]]。

显示分步解答

**步骤 1:预测形状。**形状是 (2,3)(2,3) 和 (3,2)(3,2),所以结果必须是 (2,2)(2,2)。

**步骤 2:计算第 1 行。**其元素是 1⋅3+0⋅0+(−2)⋅4=−51\cdot3+0\cdot0+(-2)\cdot4=-5 和 1⋅1+0⋅(−2)+(−2)⋅2=−31\cdot1+0\cdot(-2)+(-2)\cdot2=-3。

**步骤 3:计算第 2 行。**其元素是 2⋅3+1⋅0+1⋅4=102\cdot3+1\cdot0+1\cdot4=10 和 2⋅1+1⋅(−2)+1⋅2=22\cdot1+1\cdot(-2)+1\cdot2=2。

因此

PQ=[−5−3102].PQ=\begin{bmatrix}-5&-3\\10&2\end{bmatrix}.

问题 3

一个稠密层存储形状为 (2, 3) 的 W,接收形状为 (4, 3) 的 X,并有形状为 (2,) 的偏置 b。应使用什么乘积,输出形状是什么?

显示分步解答

**步骤 1:连接两种约定。**由于样本按行排列,要转置权重矩阵:WTW^{\mathsf T} 的形状是 (3,2)(3,2)。

步骤 2:计算批次乘积。XWTXW^{\mathsf T} 的形状是 (4,3)(3,2)→(4,2)(4,3)(3,2)\to(4,2)。

**步骤 3:按行加上偏置。**偏置有两个元素,与两个输出特征匹配,因此 XWT+bXW^{\mathsf T}+b 仍为 (4,2)(4,2)。开头的 4 表示样本数,而不是学习到的权重形状的一部分。

问题 4

对于本课的 A 和 B,为什么 Bᵀ Aᵀ 等于 (A B)ᵀ?Bᵀ 的每一行表示什么?

显示分步解答

**步骤 1:以相反顺序转置因子。**转置 ABAB 得到 (AB)T=BTAT(AB)^{\mathsf T}=B^{\mathsf T}A^{\mathsf T}。

步骤 2:识别各行。BTB^{\mathsf T} 的每一行都是 BB 的一列,因此是映射 AA 的一个三坐标输入。

步骤 3:读取输出。BTATB^{\mathsf T}A^{\mathsf T} 的每个对应行,都是按行批处理约定对该输入得到的输出。数值上,

BTAT=[−14106]=[−11046]T.B^{\mathsf T}A^{\mathsf T} =\begin{bmatrix}-1&4\\10&6\end{bmatrix} =\begin{bmatrix}-1&10\\4&6\end{bmatrix}^{\mathsf T}.

问题 5

使用本课的 A、B 和 x = [1, 2]ᵀ,在 (A B)x 中哪个映射先作用?最终向量是什么?

显示分步解答

**步骤 1:读取顺序。**最右侧的因子先作用,因此要先计算 BxB\boldsymbol{x},再应用 AA。

步骤 2:计算中间向量。

Bx=[418].B\boldsymbol{x}=\begin{bmatrix}4\\1\\8\end{bmatrix}.

步骤 3:应用第二个映射。

A(Bx)=[1916].A(B\boldsymbol{x}) =\begin{bmatrix}19\\16\end{bmatrix}.

因此,ABAB 表示“先 BB,再 AA”,最终向量是 [19,16]T[19,16]^{\mathsf T}。

问题 6

为什么 A * Bᵀ 与 A B 不是同一种运算,尽管对于本课的数组这两个表达式都能被接受?

显示分步解答

步骤 1:比较形状。AA 和 BTB^{\mathsf T} 的形状都是 (2,3)(2,3)。

步骤 2:识别运算符。* 是逐元素乘法,因此它将六对对齐的元素相乘并保持形状 (2,3)(2,3)。它不会将一行与一列配对并对三个项求和。

步骤 3:对比矩阵乘积。A @ B 使用形状为 (2,3)(2,3) 和 (3,2)(3,2) 的操作数,缩并共享维度,并返回 (2,2)(2,2)。对这些数值而言,A∗BT=[2−30204]A*B^{\mathsf T}=\begin{bmatrix}2&-3&0\\2&0&4\end{bmatrix},而 AB=[−11046]AB=\begin{bmatrix}-1&10\\4&6\end{bmatrix}。

问题 7

A 的形状是 (2, 3),D 的形状是 (2, 2)。为什么 A @ D 会失败?D @ A 是否有效?

显示分步解答

**步骤 1:检验 ADA D。**内部维度将是 3 和 2。它们不同,因此无法按照矩阵乘积规则将 AA 的一行与 DD 的完整一列配对;A @ D 无效。

**步骤 2:检验 DAD A。**此时内部维度是 2 和 2,所以乘积有效,外部形状是 (2,3)(2,3)。

**步骤 3:保持顺序。**反转顺序可能改变合法性,也总会改变所表示的映射。不能把交换因子当作让形状碰巧匹配的一般修复办法。

问题 8

对于 P = [[1, −1], [0, 0]] 和 Q = [[3, 3], [3, 3]],为什么 P Q 虽然有效却可能是零矩阵?I₂ C 做了什么?

显示分步解答

**步骤 1:检查乘积。**两个矩阵都是 (2,2)(2,2),因此内部维度匹配。

**步骤 2:解释零结果。**第一行的每个元素都是 1⋅3+(−1)⋅3=01\cdot3+(-1)\cdot3=0,第二行直接贡献零。因此 PQPQ 为零是因为抵消和零元素,而不是因为形状不兼容。

**步骤 3:应用单位矩阵。**令 I2=[1001]I_2=\begin{bmatrix}1&0\\0&1\end{bmatrix},且 CC 的形状为 (2,2)(2,2),则 I2C=CI_2C=C。单位矩阵保留兼容的乘积;它既不改变数值,也不能修复不匹配。

问题 9

对于 torch.nn.Linear(3, 2),一个批次的形状是 (5, 3)。请写出权重形状、偏置形状、输出形状和按行批处理公式。

显示分步解答

步骤 1:读取层参数。in_features=3 和 out_features=2 表示学习到的权重形状为 (2,3)(2,3),偏置形状为 (2,)(2,)。

**步骤 2:为按行输入进行转置。**五行批次的形状是 (5,3)(5,3),因此与 WTW^{\mathsf T} 相乘时使用 (5,3)(3,2)(5,3)(3,2)。

**步骤 3:写出结果。**输出形状为 (5,2)(5,2),公式为 Y=XWT+bY=XW^{\mathsf T}+b。5 保持为批次维度;每一行接收两个输出特征,并且两元素偏置会加到每一行上。

资料来源

  1. Mathematics for Machine Learning companion website
  2. Mathematics for Machine Learning book PDF
  3. NumPy documentation: numpy.matmul
  4. NumPy documentation: numpy.multiply
  5. NumPy quickstart: elementwise and matrix products
  6. NumPy documentation: broadcasting
  7. PyTorch documentation: torch.nn.Linear
  8. PyTorch documentation: torch.matmul