Create your own
Lesson illustration

手算向量点积与小型矩阵乘法

你好。上一课我们建立了“形状语言”:线性层将输入的最后一维从 变为 ,而矩阵乘法要求相邻的中间维匹配。今天把这些规则落实到具体算术上:向量点积小型矩阵乘法

它们并不是脱离后训练的基础题。Transformer 的线性投影、注意力分数、奖励模型头,以及后续反向传播中的大量计算,底层都由“对应元素相乘并求和”组成。学完后,你应能手工算出一个线性层输出的来源,并在阅读 x @ W.TQ @ K.transpose(-1, -2) 这类代码时知道每个数在做什么。


点积:把两组对应特征汇总成一个分数

设两个同长度向量为:

它们的点积定义为:

点积的输入是两个向量,输出却是一个标量,也就是一个单独的数。计算时必须满足两个向量长度相同;否则无法一一对应地相乘。

先看一个三维例子:

逐项相乘:

然后把结果求和:

这里最容易混淆的是:逐元素相乘和点积不是同一个操作。

操作计算结果输出形状
逐元素相乘 向量,长度为 3
点积 标量

在 PyTorch 中,这一区别也非常直接:

import torch

a = torch.tensor([2.0, -1.0, 3.0])
b = torch.tensor([4.0, 5.0, -2.0])

elementwise = a * b       # tensor([ 8., -5., -6.])
dot = torch.dot(a, b)     # tensor(-3.)

a * b 只做逐元素乘法;torch.dot(a, b) 才会把乘积加总为一个数。对一维向量,a @ b 也会得到同一个点积结果。

下面这段 Khan Academy 视频用两个二维、三维例子说明“对应项相乘再相加”,并强调结果为什么是标量。

Vector dot product and vector length | Vectors and spaces | Linear Algebra | Khan Academy

观看 Khan Academy 的《Vector dot product and vector length》。它用最直接的方式建立点积的计算规则,适合先把“向量得到一个数”的操作感固定下来。

先看 点积定义,关注两个向量必须按相同位置配对、结果是标量这两点;接着看 手算例子,留意负数项和零项如何参与最终求和。

点积在模型里意味着什么?

把向量看成“特征的有序列表”时,点积可理解为:用另一组数对各个特征加权,再把贡献汇总成一个分数。

例如,某个 token 的隐藏表示是:

某个输出单元对应的权重是:

则该输出单元在加上 bias 前的值为:

其中每个权重决定对应输入特征对输出的贡献方式:

  • 权重为正,会保留同号输入的正向贡献;
  • 权重为负,会反转该输入特征的贡献方向;
  • 权重为零,该特征不影响这个输出;
  • 最终不是看某一维,而是看所有维度的加权和。

实际大模型中,隐藏维度可能是 或更高。手工当然不会逐维计算,但计算结构没有变化:一个输出特征就是输入隐藏向量与一行权重的点积,再加上 bias。


从一个点积到整张矩阵:每个输出位置都是“行点列”

矩阵乘法只是把点积系统地重复很多次。

设:

则矩阵积 存在,且:

其中 的列数 必须等于 的行数 。上一课称它为“中间维”;在手算中,它对应每个点积里需要相乘并累加的项数。

输出矩阵中第 行、第 列的元素为:

不用急着记住下标。更实用的语言是:

结果的第 个元素,等于左矩阵第 行与右矩阵第 列的点积。

这张图准确展示了这一规则:左侧矩阵 的每一行,与右侧矩阵 的每一列分别配对;每次配对产生结果矩阵 的一个元素。

左侧 \(2 \times 2\) 矩阵 \(A\) 的每一行,分别与右侧矩阵 \(B\) 的每一列做点积,构成结果矩阵 \(C\) 对应位置的元素。

在开始算数字前,先做形状检查。图中的两个矩阵是:

它们都是 矩阵。中间维都是 ,因此乘法合法,输出也会是

现在按“行点列”逐格计算。

第一行、第一列

的第一行与 的第一列:

所以:

第一行、第二列

所以:

第二行、第一列

所以:

第二行、第二列

所以最终结果为:

手算矩阵乘法时,不要把“同位置元素相乘”误当成矩阵乘法。下面这个计算:

是逐元素乘法,不是矩阵乘法。它的形状恰好也是 ,因此只看 shape 无法分辨;必须看操作符和数学含义。

下面的视频完整演示了“先检查中间维,再逐格做行点列”的过程。

Multiplying a matrix by a matrix | Matrices | Precalculus | Khan Academy

观看 Khan Academy 的《Multiplying a matrix by a matrix》。它把一个矩阵积拆成四个点积,正好对应手算时应执行的动作。

先看 形状条件,确认为什么左矩阵列数必须等于右矩阵行数。然后从 逐格计算 看完:重点不是记住视频中的数字,而是追踪每个结果元素如何取左边的一行和右边的一列。


矩阵乘法的顺序不能随意交换

普通数满足:

但矩阵一般不满足:

对刚才的两个矩阵,反过来计算

它与之前的结果不同:

这不是偶然,而是矩阵乘法的基本性质。因为 中,左边矩阵提供“行”,右边矩阵提供“列”;交换顺序以后,参与每个点积的数和组合方式都变了。

放到模型代码中,顺序尤其关键。例如,设输入 batch 为 ,权重为 。若每行代表一个样本或 token 表示,常见计算是:

而不是任意交换成:

两者即使在形状碰巧允许时,也代表不同的计算。


线性层就是一组并行的点积

回忆上一课:PyTorch 中

layer = nn.Linear(in_features=H, out_features=O)

权重保存为:

若输入向量为:

线性层输出为:

这里的 行,每一行都表示一个输出特征对应的一套权重。于是:

  • 的第一行与 做点积,得到 的第一个分量;
  • 的第二行与 做点积,得到第二个分量;
  • 重复 次,就得到 维输出。

看一个输入维度为 3、输出维度为 2 的小例子:

因为 存储为 ,实际矩阵乘法使用其转置:

于是:

第一个输出元素来自 第一行的点积:

第二个输出元素来自 第二行的点积:

因此,加 bias 后:

对应 PyTorch 验证代码如下:

import torch

x = torch.tensor([1.0, 2.0, -1.0])

W = torch.tensor([
    [2.0, -1.0, 0.0],
    [1.0, 3.0, 4.0],
])

b = torch.tensor([0.5, -1.0])

y = x @ W.T + b
print(y)  # tensor([0.5000, 2.0000])

对于语言模型中的 [B, T, H] 隐藏状态,这个逻辑逐 token 重复发生。线性层并不把 batch 中不同样本混在一起,也不会把不同位置的 token 自动相加;它对每个位置上的 维向量独立地做同一套点积计算,最终得到 [B, T, O]


手算与读代码时的检查清单

今后遇到一个小型矩阵计算,或者试图理解某个投影层时,可以固定按以下顺序处理:

  1. 先写形状。
    ,则 合法,结果为

  2. 确认中间维。
    点积中有多少个对应项,就应有多少个相乘项。 时,每个输出元素都恰好累加 项。

  3. 定位输出元素。
    结果的第 个元素,固定取左侧第 行、右侧第 列。

  4. 分开处理乘法与求和。
    先写每一对元素的乘积,再相加。包含负数时尤其不要跳步。

  5. 最后检查输出 shape 与语义。
    输出行数来自左矩阵,输出列数来自右矩阵。若是线性层,还要确认是否需要加 bias。

常见错误包括:

  • 将逐元素 * 当作矩阵乘法 @
  • 只检查两个矩阵“大小差不多”,却没检查左列数和右行数;
  • 计算某个输出位置时取了错误的行或列;
  • 忘记转置 PyTorch 的 nn.Linear.weight
  • 得到数值结果后,没有检查其 shape 是否仍符合预期。

小结

这一课的核心可以浓缩为两句话:

  • 点积:对应元素相乘,再求和;输入是等长向量,输出是标量。
  • 矩阵乘法:结果中每个元素都是“左矩阵一行”与“右矩阵一列”的点积。

进一步地,线性层的每一个输出特征,本质上都是输入特征与一行权重的点积,加上一个 bias。理解这一点,后面学习注意力中的 、语言模型 logits,以及梯度计算时,就不会只把它们看成黑箱张量操作。

下一课将转向导数与偏导数:从“如何算出一个输出”进一步走到“参数变化一点点,输出会变化多少”。

Can't find a good explanation? Sign up and we'll make it for you

Sign up