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

AI 中的矩阵与向量乘积:全连接层计算的两种视角

按行和按列计算矩阵与向量的乘积,在 NumPy 中实现两种方法,并排查全连接层的形状和运算符错误。

分享这篇文章

三个特征值进入一个全连接层,输出两个分数。该层的代码可以只有一行:

scores = weights @ features

这个 @ 隐藏了两个完整的加权和。它还隐藏了另一种同样正确的理解方式:每个输入特征缩放权重矩阵的一列,缩放后的列相加得到同一个输出向量。掌握这两种视角,就能把简洁的运算符还原成可手算、可调试的过程。

学完本课,你将能够按行和按列计算矩阵与向量的乘积,解释两种方法为何一致,用 NumPy 实现它们,并发现维度不匹配或误用逐元素乘法的问题。

学习目标: 按行和按列计算矩阵与向量的乘积。

第 17 课:AI 中的线性组合说明了缩放后的向量如何相加。第 18 课:AI 中的矩阵把每条输出规则放在一行,把每个输入特征放在一列。矩阵与向量相乘将这两个概念结合起来。官方的 Mathematics for Machine Learning 配套网站及教材第 2.2 节介绍了维度相容的矩阵乘法与列向量组合的视角。下文的排序示例、计算、代码、可视化和测验均为原创。

相同的内维度规定了计算如何进行

设 W∈Rm×nW\in\mathbb{R}^{m\times n} 是一个有 mm 行、nn 列的实矩阵。设 x∈Rn\boldsymbol{x}\in\mathbb{R}^{n} 是一个有 nn 个坐标的向量。由于矩阵的列数等于向量的坐标数,乘积 WxW\boldsymbol{x} 有定义:

W⏟m×nx⏟n×1=y⏟m×1.\underbrace{W}_{m\times n} \underbrace{\boldsymbol{x}}_{n\times1} = \underbrace{\boldsymbol{y}}_{m\times1}.

两个相同的 nn 描述了乘法内部需要完成的运算。剩下的 mm 就是输出的长度。

这并不意味着可以像处理普通数字那样约去相同的维度。维度记录的是对齐关系:xjx_j 必须与第 jj 列对应,使权重 wijw_{ij} 和输入 xjx_j 出现在同一项中。

一个输入按行产生两个分数

假设一个小型排序层使用三个标准化的输入特征:

  • x1=2x_1=2:主题匹配度比其参考值高两个尺度单位;
  • x2=1x_2=1:时效性比其参考值高一个尺度单位;
  • x3=−1x_3=-1:来源质量比其参考值低一个尺度单位。

特征向量和两行的权重矩阵为

x=[21−1],W=[0.8−0.40.20.10.6−0.5].\boldsymbol{x}= \begin{bmatrix}2\\1\\-1\end{bmatrix}, \qquad W= \begin{bmatrix} 0.8 & -0.4 & 0.2\\ 0.1 & 0.6 & -0.5 \end{bmatrix}.

根据维度可以预知答案有两个坐标:

(2×3)(3×1)⟶(2×1).(2\times3)(3\times1)\longrightarrow(2\times1).

负输入 x3=−1x_3=-1 使负权重 w23=−0.5w_{23}=-0.5 对 y2y_2 贡献正的 0.50.5。单看权重的符号,无法判断它会提高还是降低输出;输入的符号同样重要。

同一个乘积的两种读法。选择一种视角,再逐项显示。两条路径最终都得到 y = [1, 1.3]ᵀ。
探索计算过程

使用方向键操作滑块。第 3 步显示完整乘积。

每个输出行已包含前 3 项。

权重矩阵 W
0.8-0.40.20.10.6-0.5
×
输入 x
21-1
=
当前累积输出
11.3

第 1 行: 1.6 + (-0.4) + (-0.2) = 1

第 2 行: 0.2 + 0.6 + 0.5 = 1.3

数值后备表格:每个缩放后的列及其对两个输出坐标的贡献。
列贡献对 y₁ 的贡献对 y₂ 的贡献缩放后的列
x₁w₁ (x₁ = 2)1.60.22 × [0.8, 0.1]ᵀ = [1.6, 0.2]ᵀ
x₂w₂ (x₂ = 1)-0.40.61 × [-0.4, 0.6]ᵀ = [-0.4, 0.6]ᵀ
x₃w₃ (x₃ = −1)-0.20.5-1 × [0.2, -0.5]ᵀ = [-0.2, 0.5]ᵀ
总计11.3[1, 1.3]ᵀ

完整表格会保留在服务器渲染的课程中,因此即使没有 JavaScript 或动态效果,六项标量乘积及其两个总数仍然可见。

模拟器初始就会呈现完整的按行计算。列视角并未引入另一种乘积,而是把同样的六次标量乘法重新分为三个向量贡献。即使交互尚未加载或用户减少了动态效果,表格也保留所有精确数值。

从列看出每个输入的贡献

将 WW 的各列写为向量 w1,w2,w3∈R2\boldsymbol{w}_1,\boldsymbol{w}_2,\boldsymbol{w}_3\in\mathbb{R}^{2}:

w1=[0.80.1],w2=[−0.40.6],w3=[0.2−0.5].\boldsymbol{w}_1= \begin{bmatrix}0.8\\0.1\end{bmatrix}, \qquad \boldsymbol{w}_2= \begin{bmatrix}-0.4\\0.6\end{bmatrix}, \qquad \boldsymbol{w}_3= \begin{bmatrix}0.2\\-0.5\end{bmatrix}.

输入坐标成为这些列向量的系数:

Wx=x1w1+x2w2+x3w3.W\boldsymbol{x} =x_1\boldsymbol{w}_1+x_2\boldsymbol{w}_2+x_3\boldsymbol{w}_3.

对于同一个例子,

Wx=2[0.80.1]+1[−0.40.6]−1[0.2−0.5]=[1.60.2]+[−0.40.6]+[−0.20.5]=[1.01.3].\begin{aligned} W\boldsymbol{x} &=2\begin{bmatrix}0.8\\0.1\end{bmatrix} +1\begin{bmatrix}-0.4\\0.6\end{bmatrix} -1\begin{bmatrix}0.2\\-0.5\end{bmatrix}\\[4pt] &=\begin{bmatrix}1.6\\0.2\end{bmatrix} +\begin{bmatrix}-0.4\\0.6\end{bmatrix} +\begin{bmatrix}-0.2\\0.5\end{bmatrix}\\[4pt] &=\begin{bmatrix}1.0\\1.3\end{bmatrix}. \end{aligned}

行视角回答:“输出 ii 如何计算?”列视角回答:“输入 jj 对每个输出贡献了什么?”调试全连接层时,这两个问题通常都需要回答。

为什么两种视角一定一致

可以逐个坐标证明这一等式。列向量组合的第 ii 个坐标是

(x1w1+⋯+xnwn)i=x1wi1+⋯+xnwin=∑j=1nwijxj.\left(x_1\boldsymbol{w}_1+\cdots+x_n\boldsymbol{w}_n\right)_i =x_1w_{i1}+\cdots+x_nw_{in} =\sum_{j=1}^{n}w_{ij}x_j.

最后一个和式恰好是 (Wx)i(W\boldsymbol{x})_i 的按行定义。全部 mm 个坐标都相同,所以得到的向量相等。这个论证只用了有限个实数的普通乘法和加法;两幅示意图之间没有暗含新的假设。

NumPy 能展示简写背后的运算

NumPy 当前的 matmul 文档将 @ 定义为矩阵乘法,并说明二维数组乘一维数组时的常见结果。先检查形状,再使用简洁的运算符:

import numpy as np

weights = np.array([
    [0.8, -0.4,  0.2],
    [0.1,  0.6, -0.5],
], dtype=float)
features = np.array([2.0, 1.0, -1.0])

if weights.shape[1] != features.shape[0]:
    raise ValueError("each matrix row needs one weight per input feature")

direct = weights @ features
by_rows = np.array([
    sum(weight * value for weight, value in zip(row, features))
    for row in weights
])
by_columns = sum(
    (features[j] * weights[:, j] for j in range(weights.shape[1])),
    start=np.zeros(weights.shape[0]),
)

print("direct:", direct)
print("by rows:", by_rows)
print("by columns:", by_columns)
print("all equal:", np.allclose(direct, by_rows)
      and np.allclose(direct, by_columns))

已验证的输出:

direct: [1.  1.3]
by rows: [1.  1.3]
by columns: [1.  1.3]
all equal: True

显式写出两种算法有助于教学和调试。在一般数值计算中,weights @ features 直接表达预期的线性代数运算,也让 NumPy 使用其优化实现。

输出形状值得留意。weights 的形状是 (2, 3),features 的形状是 (3,)。NumPy 将一维的第二个操作数视为矩阵与向量乘法中的向量,返回形状 (2,),而不是 (2, 1)。数值与列向量记法一致;软件表示只是省去了长度为一的轴。

全连接层在乘积之后加上偏置

矩阵与向量的乘积是全连接层的线性部分。带偏置的层采用仿射规则

z=Wx+b,b∈Rm.\boldsymbol{z}=W\boldsymbol{x}+\boldsymbol{b}, \qquad \boldsymbol{b}\in\mathbb{R}^{m}.

在上述例子中,若 b=[0.2,−0.1]T\boldsymbol{b}=[0.2,-0.1]^{\mathsf T},则

z=[1.01.3]+[0.2−0.1]=[1.21.2].\boldsymbol{z} =\begin{bmatrix}1.0\\1.3\end{bmatrix} +\begin{bmatrix}0.2\\-0.1\end{bmatrix} =\begin{bmatrix}1.2\\1.2\end{bmatrix}.

如果模型使用激活函数,它会作用于仿射运算的结果之后。激活函数不属于 WxW\boldsymbol{x} 的定义。

当前的 torch.nn.Linear 文档以 (out_features, in_features) 的形状存储权重,与这里每个输出占一行的排列相同。PyTorch 通常接受最后一轴为特征的输入,并把规则写成 xAT+b\boldsymbol{x}A^{\mathsf T}+\boldsymbol{b}。对于写成行向量的单个输入,这与列向量方程 Wx+bW\boldsymbol{x}+\boldsymbol{b} 互为转置形式;并没有产生另一组权重。

两种乘法符号可能得到两种不同对象

这里最危险的错误可能不会引发异常。NumPy 将 multiply 和 * 说明为支持广播的逐元素乘法。因此,下面的代码

print(weights * features)

并不会算出有两个坐标的层输出。长度为三的向量会广播到两行,产生

[[ 1.6 -0.4 -0.2]
 [ 0.2  0.6  0.5]]

其形状为 (2, 3)。这六个单元格是每行求和之前的贡献。调用 sum(axis=1) 可以完成这个特定计算,但 @ 能直接表明预期的缩并运算。NumPy 快速入门也区分了这两种运算符:对于数组,* 是逐元素乘法,@ 执行矩阵乘法。

输入维度不相容时,失败的原因则不同:

bad_features = np.array([2.0, 1.0])
weights @ bad_features

矩阵有三列,向量却只有两个坐标。NumPy 文档规定的 matmul 约束会拒绝这种内维度不匹配。随意添加第三个数来消除错误,比报错更糟;必须先查明缺少的特征及其坐标含义。

下一个乘积仍遵循相同的对齐规则

矩阵与向量的乘积将多个输出公式压缩在一起,却没有抹去其结构。按行阅读,每个输出对应一个加权和;按列阅读,每个输入对应一个输出向量贡献。形状对齐保证两种读法都将同一个 wijw_{ij} 与同一个 xjx_j 配对。

第 20 课会把多个输入向量放在同一个权重矩阵旁边。运算将变为矩阵与矩阵相乘,但核心原则不变:先匹配内维度,再确定结果保留哪些外轴。Math for AI 课程页面保留了这条先修路径。

检查你的理解

Question 1

设 A 的形状为 4 × 3,x 的长度为 3。Ax 是否有定义?输出长度是多少?每个输出坐标需要将多少项标量乘积相加?

Show the step-by-step solution

矩阵有三列,向量有三个坐标。两个内维度相同,所以乘积有定义:

(4×3)(3×1)⟶(4×1).(4\times3)(3\times1)\longrightarrow(4\times1).

余下的四行决定了四个输出坐标。对于每个输出,一行提供三个权重。每个权重乘以对齐的输入坐标,再将三项标量乘积相加。因此 AxA\boldsymbol{x} 的长度为 44,每个坐标由三项乘积构成。

Question 2

按行计算 Bv,其中 B = [[2, −1], [0.5, 3]],v = [4, 2]ᵀ。写出两个加权和。

Show the step-by-step solution

BB 的两列与 v\boldsymbol{v} 的两个坐标相匹配,所以乘积有定义,并且有两个输出。

对于第 1 行,

y1=(2)(4)+(−1)(2)=8−2=6.y_1=(2)(4)+(-1)(2)=8-2=6.

对于第 2 行,

y2=(0.5)(4)+(3)(2)=2+6=8.y_2=(0.5)(4)+(3)(2)=2+6=8.

按顺序将各行结果组成向量,得到

Bv=[68].B\boldsymbol{v}=\begin{bmatrix}6\\8\end{bmatrix}.

Question 3

把第 2 题的乘积重新计算为 B 的各列的加权和。

Show the step-by-step solution

首先分离各列:

b1=[20.5],b2=[−13].\boldsymbol{b}_1=\begin{bmatrix}2\\0.5\end{bmatrix}, \qquad \boldsymbol{b}_2=\begin{bmatrix}-1\\3\end{bmatrix}.

v=[4,2]T\boldsymbol{v}=[4,2]^{\mathsf T} 的坐标成为它们的系数:

Bv=4b1+2b2=4[20.5]+2[−13]=[82]+[−26]=[68].\begin{aligned} B\boldsymbol{v} &=4\boldsymbol{b}_1+2\boldsymbol{b}_2\\ &=4\begin{bmatrix}2\\0.5\end{bmatrix} +2\begin{bmatrix}-1\\3\end{bmatrix}\\ &=\begin{bmatrix}8\\2\end{bmatrix} +\begin{bmatrix}-2\\6\end{bmatrix}\\ &=\begin{bmatrix}6\\8\end{bmatrix}. \end{aligned}

结果与按行计算一致,因为两种视角包含相同的四项标量乘积,只是分组方式不同。

Question 4

在本课的乘积中,令 x₂ 为零,同时保持 x₁ = 2、x₃ = −1。用列视角计算新结果,并说明哪些对齐关系保持不变。

Show the step-by-step solution

新的输入是 [2,0,−1]T[2,0,-1]^{\mathsf T}。其第二个坐标将第二列缩放为零,但这个坐标仍处于位置 2:

Wx=2[0.80.1]+0[−0.40.6]−1[0.2−0.5]=[1.60.2]+[00]+[−0.20.5]=[1.40.7].\begin{aligned} W\boldsymbol{x} &=2\begin{bmatrix}0.8\\0.1\end{bmatrix} +0\begin{bmatrix}-0.4\\0.6\end{bmatrix} -1\begin{bmatrix}0.2\\-0.5\end{bmatrix}\\ &=\begin{bmatrix}1.6\\0.2\end{bmatrix} +\begin{bmatrix}0\\0\end{bmatrix} +\begin{bmatrix}-0.2\\0.5\end{bmatrix}\\ &=\begin{bmatrix}1.4\\0.7\end{bmatrix}. \end{aligned}

第 2 列仍对应时效性,第 3 列仍对应来源质量。若直接删除这个零坐标,向量会变短,后面每个特征都会错位。

Question 5

开发者对形状为 (2, 3) 和 (3,) 的 weights 与 features 执行 weights * features。代码为什么能运行?返回什么形状?还缺少哪一步运算?

Show the step-by-step solution

NumPy 的 * 运算符执行逐元素乘法。长度为三的数组可以广播到矩阵的两行,因此运算成功,并返回包含六项乘积、形状为 (2, 3) 的数组。

对于本课的数值,这两行为

[1.6−0.4−0.20.20.60.5].\begin{bmatrix} 1.6 & -0.4 & -0.2\\ 0.2 & 0.6 & 0.5 \end{bmatrix}.

全连接层的输出每行只需要一个数。还必须对每一行求和:

[1.6−0.4−0.20.2+0.6+0.5]=[1.01.3].\begin{bmatrix} 1.6-0.4-0.2\\ 0.2+0.6+0.5 \end{bmatrix} =\begin{bmatrix}1.0\\1.3\end{bmatrix}.

使用 weights @ features 同时表达了乘法和所需的求和。

Question 6

对于相容的矩阵 A,比较 A0 与带偏置层 A0 + b 的输出。为什么结果不同?

Show the step-by-step solution

零向量的每个坐标都为零。从列视角看,每一列因此都被零缩放:

A0=0a1+⋯+0an=0.A\boldsymbol{0} =0\boldsymbol{a}_1+\cdots+0\boldsymbol{a}_n =\boldsymbol{0}.

带偏置的全连接层在矩阵乘积之后还执行一次向量加法:

A0+b=0+b=b.A\boldsymbol{0}+\boldsymbol{b} =\boldsymbol{0}+\boldsymbol{b} =\boldsymbol{b}.

答案不同,是因为 AxA\boldsymbol{x} 是线性映射,而加上非零偏置使完整规则成为仿射映射。偏置不属于矩阵与向量的乘积。

Question 7

存储的权重数组形状为 (3, 2),但预期层有三个输入和两个输出。为什么对于长度为三的输入,W @ x 会失败?转置 W 之前必须核实什么?

Show the step-by-step solution

按每个输出占一行的约定,预期矩阵形状是 (2, 3):两行输出、三列输入。实际的 (3, 2) 数组只有两列,其内维度无法匹配长度为三的输入:

(3×2)(3×1)(3\times2)(3\times1)

没有定义,因为 2≠32\ne3。

转置后形状会变成 (2, 3),但形状本身还不够。转置前,应确认实际的行确实代表三个输入特征,列确实代表两条输出规则,而且它们的顺序与特征和输出的元数据一致。若数组本身采用另一种语义布局,盲目转置可能保留错误的含义。

Question 8

本课的乘积为 [1, 1.3]ᵀ。全连接层加上 b = [0.2, −0.1]ᵀ,再应用将负坐标替换为零的 ReLU。计算激活前的结果和最终输出。

Show the step-by-step solution

先逐个坐标加上偏置:

z=Wx+b=[1.01.3]+[0.2−0.1]=[1.21.2].\boldsymbol{z} =W\boldsymbol{x}+\boldsymbol{b} =\begin{bmatrix}1.0\\1.3\end{bmatrix} +\begin{bmatrix}0.2\\-0.1\end{bmatrix} =\begin{bmatrix}1.2\\1.2\end{bmatrix}.

此时 z\boldsymbol{z} 是激活前的结果。ReLU 对每个坐标分别作用,规则是 ReLU⁡(t)=max⁡(0,t)\operatorname{ReLU}(t)=\max(0,t)。两个坐标都为正,因此均不改变:

ReLU⁡(z)=[max⁡(0,1.2)max⁡(0,1.2)]=[1.21.2].\operatorname{ReLU}(\boldsymbol{z}) =\begin{bmatrix}\max(0,1.2)\\\max(0,1.2)\end{bmatrix} =\begin{bmatrix}1.2\\1.2\end{bmatrix}.

顺序很重要:先计算矩阵与向量的乘积,加上偏置,最后才应用激活函数。

资料来源

  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. PyTorch documentation: torch.nn.Linear