三个特征值进入一个全连接层,输出两个分数。该层的代码可以只有一行:
scores = weights @ features
这个 @ 隐藏了两个完整的加权和。它还隐藏了另一种同样正确的理解方式:每个输入特征缩放权重矩阵的一列,缩放后的列相加得到同一个输出向量。掌握这两种视角,就能把简洁的运算符还原成可手算、可调试的过程。
学完本课,你将能够按行和按列计算矩阵与向量的乘积,解释两种方法为何一致,用 NumPy 实现它们,并发现维度不匹配或误用逐元素乘法的问题。
学习目标: 按行和按列计算矩阵与向量的乘积。
第 17 课:AI 中的线性组合说明了缩放后的向量如何相加。第 18 课:AI 中的矩阵把每条输出规则放在一行,把每个输入特征放在一列。矩阵与向量相乘将这两个概念结合起来。官方的 Mathematics for Machine Learning 配套网站及教材第 2.2 节介绍了维度相容的矩阵乘法与列向量组合的视角。下文的排序示例、计算、代码、可视化和测验均为原创。
相同的内维度规定了计算如何进行
设 W∈Rm×n 是一个有 m 行、n 列的实矩阵。设 x∈Rn 是一个有 n 个坐标的向量。由于矩阵的列数等于向量的坐标数,乘积 Wx 有定义:
m×nWn×1x=m×1y.
两个相同的 n 描述了乘法内部需要完成的运算。剩下的 m 就是输出的长度。
这并不意味着可以像处理普通数字那样约去相同的维度。维度记录的是对齐关系:xj 必须与第 j 列对应,使权重 wij 和输入 xj 出现在同一项中。
一个输入按行产生两个分数
假设一个小型排序层使用三个标准化的输入特征:
- x1=2:主题匹配度比其参考值高两个尺度单位;
- x2=1:时效性比其参考值高一个尺度单位;
- x3=−1:来源质量比其参考值低一个尺度单位。
特征向量和两行的权重矩阵为
x=21−1,W=[0.80.1−0.40.60.2−0.5].
根据维度可以预知答案有两个坐标:
(2×3)(3×1)⟶(2×1).
负输入 x3=−1 使负权重 w23=−0.5 对 y2 贡献正的 0.5。单看权重的符号,无法判断它会提高还是降低输出;输入的符号同样重要。
同一个乘积的两种读法。选择一种视角,再逐项显示。两条路径最终都得到 y = [1, 1.3]ᵀ。权重矩阵 W0.8-0.40.20.10.6-0.5
×=第 1 行: 1.6 + (-0.4) + (-0.2) = 1
第 2 行: 0.2 + 0.6 + 0.5 = 1.3
数值后备表格:每个缩放后的列及其对两个输出坐标的贡献。| 列贡献 | 对 y₁ 的贡献 | 对 y₂ 的贡献 | 缩放后的列 |
|---|
| x₁w₁ (x₁ = 2) | 1.6 | 0.2 | 2 × [0.8, 0.1]ᵀ = [1.6, 0.2]ᵀ |
|---|
| x₂w₂ (x₂ = 1) | -0.4 | 0.6 | 1 × [-0.4, 0.6]ᵀ = [-0.4, 0.6]ᵀ |
|---|
| x₃w₃ (x₃ = −1) | -0.2 | 0.5 | -1 × [0.2, -0.5]ᵀ = [-0.2, 0.5]ᵀ |
|---|
| 总计 | 1 | 1.3 | [1, 1.3]ᵀ |
|---|
完整表格会保留在服务器渲染的课程中,因此即使没有 JavaScript 或动态效果,六项标量乘积及其两个总数仍然可见。
模拟器初始就会呈现完整的按行计算。列视角并未引入另一种乘积,而是把同样的六次标量乘法重新分为三个向量贡献。即使交互尚未加载或用户减少了动态效果,表格也保留所有精确数值。
从列看出每个输入的贡献
将 W 的各列写为向量 w1,w2,w3∈R2:
w1=[0.80.1],w2=[−0.40.6],w3=[0.2−0.5].
输入坐标成为这些列向量的系数:
Wx=x1w1+x2w2+x3w3.
对于同一个例子,
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].
行视角回答:“输出 i 如何计算?”列视角回答:“输入 j 对每个输出贡献了什么?”调试全连接层时,这两个问题通常都需要回答。
为什么两种视角一定一致
可以逐个坐标证明这一等式。列向量组合的第 i 个坐标是
(x1w1+⋯+xnwn)i=x1wi1+⋯+xnwin=j=1∑nwijxj.
最后一个和式恰好是 (Wx)i 的按行定义。全部 m 个坐标都相同,所以得到的向量相等。这个论证只用了有限个实数的普通乘法和加法;两幅示意图之间没有暗含新的假设。
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.
在上述例子中,若 b=[0.2,−0.1]T,则
z=[1.01.3]+[0.2−0.1]=[1.21.2].
如果模型使用激活函数,它会作用于仿射运算的结果之后。激活函数不属于 Wx 的定义。
当前的 torch.nn.Linear 文档以 (out_features, in_features) 的形状存储权重,与这里每个输出占一行的排列相同。PyTorch 通常接受最后一轴为特征的输入,并把规则写成 xAT+b。对于写成行向量的单个输入,这与列向量方程 Wx+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 约束会拒绝这种内维度不匹配。随意添加第三个数来消除错误,比报错更糟;必须先查明缺少的特征及其坐标含义。
下一个乘积仍遵循相同的对齐规则
矩阵与向量的乘积将多个输出公式压缩在一起,却没有抹去其结构。按行阅读,每个输出对应一个加权和;按列阅读,每个输入对应一个输出向量贡献。形状对齐保证两种读法都将同一个 wij 与同一个 xj 配对。
第 20 课会把多个输入向量放在同一个权重矩阵旁边。运算将变为矩阵与矩阵相乘,但核心原则不变:先匹配内维度,再确定结果保留哪些外轴。Math for AI 课程页面保留了这条先修路径。
检查你的理解
Question 1
设 A 的形状为 4 × 3,x 的长度为 3。Ax 是否有定义?输出长度是多少?每个输出坐标需要将多少项标量乘积相加?
Show the step-by-step solution
矩阵有三列,向量有三个坐标。两个内维度相同,所以乘积有定义:
(4×3)(3×1)⟶(4×1).余下的四行决定了四个输出坐标。对于每个输出,一行提供三个权重。每个权重乘以对齐的输入坐标,再将三项标量乘积相加。因此 Ax 的长度为 4,每个坐标由三项乘积构成。
Question 2
按行计算 Bv,其中 B = [[2, −1], [0.5, 3]],v = [4, 2]ᵀ。写出两个加权和。
Show the step-by-step solution
B 的两列与 v 的两个坐标相匹配,所以乘积有定义,并且有两个输出。
对于第 1 行,
y1=(2)(4)+(−1)(2)=8−2=6.对于第 2 行,
y2=(0.5)(4)+(3)(2)=2+6=8.按顺序将各行结果组成向量,得到
Bv=[68].
Question 3
把第 2 题的乘积重新计算为 B 的各列的加权和。
Show the step-by-step solution
首先分离各列:
b1=[20.5],b2=[−13].v=[4,2]T 的坐标成为它们的系数:
Bv=4b1+2b2=4[20.5]+2[−13]=[82]+[−26]=[68].结果与按行计算一致,因为两种视角包含相同的四项标量乘积,只是分组方式不同。
Question 4
在本课的乘积中,令 x₂ 为零,同时保持 x₁ = 2、x₃ = −1。用列视角计算新结果,并说明哪些对齐关系保持不变。
Show the step-by-step solution
新的输入是 [2,0,−1]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].第 2 列仍对应时效性,第 3 列仍对应来源质量。若直接删除这个零坐标,向量会变短,后面每个特征都会错位。
Question 5
开发者对形状为 (2, 3) 和 (3,) 的 weights 与 features 执行 weights * features。代码为什么能运行?返回什么形状?还缺少哪一步运算?
Show the step-by-step solution
NumPy 的 * 运算符执行逐元素乘法。长度为三的数组可以广播到矩阵的两行,因此运算成功,并返回包含六项乘积、形状为 (2, 3) 的数组。
对于本课的数值,这两行为
[1.60.2−0.40.6−0.20.5].全连接层的输出每行只需要一个数。还必须对每一行求和:
[1.6−0.4−0.20.2+0.6+0.5]=[1.01.3].使用 weights @ features 同时表达了乘法和所需的求和。
Question 6
对于相容的矩阵 A,比较 A0 与带偏置层 A0 + b 的输出。为什么结果不同?
Show the step-by-step solution
零向量的每个坐标都为零。从列视角看,每一列因此都被零缩放:
A0=0a1+⋯+0an=0.带偏置的全连接层在矩阵乘积之后还执行一次向量加法:
A0+b=0+b=b.答案不同,是因为 Ax 是线性映射,而加上非零偏置使完整规则成为仿射映射。偏置不属于矩阵与向量的乘积。
Question 7
存储的权重数组形状为 (3, 2),但预期层有三个输入和两个输出。为什么对于长度为三的输入,W @ x 会失败?转置 W 之前必须核实什么?
Show the step-by-step solution
按每个输出占一行的约定,预期矩阵形状是 (2, 3):两行输出、三列输入。实际的 (3, 2) 数组只有两列,其内维度无法匹配长度为三的输入:
(3×2)(3×1)没有定义,因为 2=3。
转置后形状会变成 (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].此时 z 是激活前的结果。ReLU 对每个坐标分别作用,规则是 ReLU(t)=max(0,t)。两个坐标都为正,因此均不改变:
ReLU(z)=[max(0,1.2)max(0,1.2)]=[1.21.2].顺序很重要:先计算矩阵与向量的乘积,加上偏置,最后才应用激活函数。