跳转到内容

第 15 章 自注意力与 Q/K/V

第 14 章中,解码器可以查看多个保存下来的源状态,不必只依赖最后一个。当前查询决定了各个状态在混合结果中占多少。现在,假设我们只有一条序列:red key .。为了预测后面的内容,希望每个位置的表示都能用上其他位置的信息。各个位置的查询从哪里来?拿什么去比较?比较完又接收什么?

最简单的办法,是让一个位置现有的向量包办这三件事。计算能做,但三种选择被绑在了一起:一旦改变它与别的位置匹配的方式,它传出去的内容也跟着变。我们将为这三项工作分别安排一次矩阵乘法,再逐个追踪算出的数。矩阵和 Softmax 的约定沿用第 5—6 章。阅读算例不需要 Python;动手验证以前,先完成 Primer-PY。

15.1 一份输入,算出三份不同的结果

Section titled “15.1 一份输入,算出三份不同的结果”

例 15.1:三个位置查看同一条序列。 位置顺序固定为 red、key、.,每个位置用两个坐标表示,依次排成下面矩阵的三行。这是本章新给定的教学数值,既不是第 14 章的递归状态,也不是实测词嵌入。两个坐标并不天然表示“红色程度”或“钥匙程度”。所有数值都没有物理单位。

X=[100111],WQ=[1001],WK=[1101],WV=[1012].X=\begin{bmatrix}1&0\\0&1\\1&1\end{bmatrix},\qquad W_Q=\begin{bmatrix}1&0\\0&1\end{bmatrix},\quad W_K=\begin{bmatrix}1&1\\0&1\end{bmatrix},\quad W_V=\begin{bmatrix}1&0\\1&2\end{bmatrix}.

X 的 shape 是 [3,2],即三个位置、两个输入坐标。每个 W 的 shape 都是 [2,2]:行对应输入坐标,列对应输出坐标。W 是模型保存的参数矩阵,各个位置共用同一份;本次前向计算中,参数保持不变。三次乘法的结果也都是 [3,2]:

Q=XWQ=[100111],K=XWK=[110112],V=XWV=[101222].Q=XW_Q=\begin{bmatrix}1&0\\0&1\\1&1\end{bmatrix},\qquad K=XW_K=\begin{bmatrix}1&1\\0&1\\1&2\end{bmatrix},\qquad V=XW_V=\begin{bmatrix}1&0\\1&2\\2&2\end{bmatrix}.

例如,Q 第三行的第一个坐标来自 1×1+1×0=11\times1+1\times0=1;K 第三行的第二个坐标来自 1×1+1×1=21\times1+1\times1=2;V 第二行的第二个坐标来自 0×0+1×2=20\times0+1\times2=2。三次乘法目前都只组合了 同一个位置内部 的坐标,还没有哪个位置读取另一个位置。这种变换称为 投影(projection)。这里指可学习的线性变换,不一定是几何中的垂直投影,也不一定降低维度。

三份结果的分工现在清楚了。Q 的一行叫 查询(query),接收信息的位置用它去比较可能的信息来源。K 的一行叫 键(key),是比较时的另一方。与该键对应的 V 行叫 值(value),决定这个来源实际送入混合结果的内容。名称容易让人想到查数据库,但类比只覆盖“匹配后接收内容”这层关系。这里没有精确键查找,没有取回事实的保证,通常也不是只选一条记录。Q、K、V 都是算出来的数组,W 矩阵才是保存的参数。

三份数组都来自同一条序列,所以称为 自注意力(self-attention)。第 14 章的源序列和解码序列不同,因此是交叉注意力。“自”说的是信息来自同一条序列,与模型有没有自我意识无关。

15.2 让每个接收位置与每个来源位置比较

Section titled “15.2 让每个接收位置与每个来源位置比较”

先拿第一条查询 [1,0],分别与三个键 [1,1]、[0,1]、[1,2] 求点积,得到 1、0、1。第二条查询 [0,1] 得到 1、1、2;第三条查询 [1,1] 得到 2、1、3。把九个结果按行排好:

D=QKT=[101112213].D=QK^{\mathsf T}=\begin{bmatrix}1&0&1\\1&1&2\\2&1&3\end{bmatrix}.

D 的 shape 是 [3,3]。行对应 接收信息的查询位置,列对应 提供信息的键位置。第三行第二列的数来自 1×0+1×1=11\times0+1\times1=1,表示查询 3 与键 2 的比较结果。为什么要转置 K,也就有了答案:K 原来一行存一个键,把这些行转成列,矩阵乘法才能逐行查询、逐个键地比较。[3,2] @ [2,3] 把两个对应坐标相乘后相加,保留两条位置轴。这里若写成 Q @ K,中间维度甚至对不上。

D 还不是下一个 token 的概率表。它的列是位置,不是词表候选,行内的数也没有归一化。比如第三个位置对自己的分数是 3,对第一个位置的分数是 2;现在仍不知道最终各用多少。

15.3 避免分数只因坐标变多就过于悬殊

Section titled “15.3 避免分数只因坐标变多就过于悬殊”

当然可以立刻对 D 使用第 6 章的 Softmax。但点积累加的项越多,分数的波动就可能越大,即使比较本身没有更明确的偏好。为了理解这一点,设想各坐标相互独立,正负值围绕零分布,方差都是 1。方差衡量偏离平均值的平方有多大。在所需的独立性假设下,两个查询与键坐标乘积相加,方差是 2;64 个相加,方差就是 64。典型波动幅度随平方根增大,分别约为 1.414 和 8。除以坐标数的平方根,可以抵消这一原因带来的波动增长。

这就引出了 缩放点积注意力(scaled dot-product attention)。本例的查询和键各有两个坐标,因此把 D 的每个数都除以 2\sqrt{2}。分母不是三个位置的数量,不是值向量的宽度,也不是批大小。记 s=1/2≈0.707107s=1/\sqrt{2}\approx0.707107:

S=D/2=[s0sss2s2ss3s].S=D/\sqrt{2}=\begin{bmatrix}s&0&s\\s&s&2s\\2s&s&3s\end{bmatrix}.

S 仍是 [3,3],行列含义不变。例如,最后一个数是 3/2≈2.1213203/\sqrt{2}\approx2.121320。本例中,缩放缩小了分数差距,所得概率也就没那么集中。在高维情形下,按论文的假设,这有助于避免 Softmax 过早接近饱和、梯度过小,但不能保证一定避免:训练后的坐标未必独立,方差也未必仍是 1。除以平方根既没有给分数规定上限,也不能替代指数计算前减去最大值的数值处理。Vaswani 等,3.2.1 节。

每个接收位置都需要自己的一份混合比例,所以要 沿来源位置逐行归一化。记 r=es≈2.028115r=e^s\approx2.028115。第一行取指数后是 [r,1,r],总和为 2r+1≈5.0562302r+1\approx5.056230;分别除以这个总和,就得到第一行权重。第二行的 [r,r,r²] 可以在分子、分母中同时约去 r;第三行的 [r²,r,r³] 也可以这样处理。三行的精确表达式为:

A1=[r,1,r]2r+1,A2=[1,1,r]2+r,A3=[r,1,r2]r+1+r2.A_1=\frac{[r,1,r]}{2r+1},\qquad A_2=\frac{[1,1,r]}{2+r},\qquad A_3=\frac{[r,1,r^2]}{r+1+r^2}.

表 15.1:每条查询分配的权重总和都是 1。 表中保留六位小数,实际计算使用未舍入的数值。

接收位置给位置 1 的权重给位置 2 的权重给位置 3 的权重
10.4011120.1977760.401112
20.2482550.2482550.503490
30.2839950.1400290.575975

A 的 shape 是 [3,3]。如果改为按列归一化,含义就变成了不同接收位置围绕同一个来源分配份额,一般不能再保证每个接收位置拿到总和为 1 的混合权重。这里并不要求一列的总和为 1;多条查询完全可以同时大量使用同一个来源。

第一个输出给 [1,0] 分配 0.401112,给 [1,2] 分配 0.197776,给 [2,2] 分配 0.401112。把每一整行值乘以相应权重,再把对应坐标相加。使用未舍入的权重:

O1,1=r×1+1×1+r×22r+1≈1.401112,O1,2=r×0+1×2+r×22r+1≈1.197776.O_{1,1}=\frac{r\times1+1\times1+r\times2}{2r+1}\approx1.401112, \qquad O_{1,2}=\frac{r\times0+1\times2+r\times2}{2r+1}\approx1.197776.

分母仍是第一条查询的行内归一化总和,不会变成值向量的坐标数。三个输出位置都按这个规则计算:

O=AV≈[1.4011121.1977761.5034901.5034901.5759751.432009].O=AV\approx\begin{bmatrix} 1.401112&1.197776\\ 1.503490&1.503490\\ 1.575975&1.432009 \end{bmatrix}.

shape 的变化是 [3,3] @ [3,2] → [3,2]。这次乘法沿来源位置求和,保留接收位置和值坐标。值向量中可以有负数,也可以有大于 1 的数;O 是表示向量,不是概率分布。由于权重非负且总和为 1,输出的每个坐标都落在 V 对应坐标的最小值与最大值之间。后续若再做投影或残差相加,就未必还满足这个范围。

分开三种作用
  1. 一组数字X [3, 2]
  2. 模型保存的数值W_Q、W_K、W_V:各 [2, 2]
  3. 一组数字Q、K、V:各 [3, 2]

投影沿输入坐标求和,各位置仍分行保存。

比较与归一化
  1. 处理步骤Q [3, 2] @ Kᵀ [2, 3] → D [3, 3]
  2. 一组数字第 1 行:[1, 0, 1] / √2
  3. 处理步骤取指数 → [r, 1, r];除以 2r+1

r = exp(1/√2)。每一接收行都用自己的分母。

混合值向量
  1. 一组数字A₁ = [0.401112, 0.197776, 0.401112]
  2. 一组数字V 各行:[1, 0];[1, 2];[2, 2]
  3. 处理步骤A [3, 3] @ V [3, 2] → O [3, 2]
  4. 一组数字O₁ = [1.401112, 1.197776]

混合沿来源位置求和。图中小数保留六位。

图 15.1 — 一条查询产生三个权重,得到两个输出坐标。 本书原创教学计算图,CC BY-SA 4.0。输入和参数为给定值,结果由声明的计算规则推得。

图 15.1 把保存的矩阵与临时算出的数组分开了。沿着第一条查询看:三个分数最后变成两个输出坐标。接收位置一直保留着,但每一步求和的对象不同:投影时沿输入坐标求和,混合时沿来源位置求和。

完整算过一遍之后,可以把它压缩成常见的公式:

Attention⁡(Q,K,V)=softmax⁡keys ⁣(QKTdk)V.\operatorname{Attention}(Q,K,V)=\operatorname{softmax}_{\text{keys}}\!\left(\frac{QK^{\mathsf T}}{\sqrt{d_k}}\right)V.

对于一个注意力头,Q 的 shape 是 [T,d_k],K 是 [T,d_k],V 是 [T,d_v]。T 表示位置数,正整数 dkd_k 表示用于比较的坐标数,dvd_v 表示值坐标数,两种宽度可以不同。输出是 [T,d_v]。Softmax 下标中的 “keys” 明确了归一化的轴。代入 T=3、两种宽度都为 2,就回到了前面的全部矩阵。若一次处理 B 条样本,只需增加最前面的批轴,各条样本分别做这些计算,不能把样本混在一起。

15.5 同一张权重图,可以传递不同的内容

Section titled “15.5 同一张权重图,可以传递不同的内容”

例 15.2:比例不变,换掉流过来的值。 固定 X、W_Q、W_K,把 W_V 换成全零矩阵。Q、K、D、S 和表 15.1 都不变。但 V 变为零,所有输出也都变成 [0,0]。即使用很醒目的颜色绘制注意力权重图,两张图也会完全相同,实际传来的内容却已经消失了。

还有一个不那么极端、但同样能精确判断的改动:给每一行 V 都加上 [2,-1],每一行 O 就也加上 [2,-1],因为该行权重的总和为 1。这里是有意只改动 V,不能据此断言修改原始文本也只会改变 V。改 X 通常会同时改变三种投影。注意力权重说明的是这次混合用了哪些比例;仅凭它,既不能充分解释整个模型为何给出某个回答,也不能判断回答是否真实。

完成 Primer-PY 后,可以用下面这段完整代码核对正文。axis=-1 指最后一条轴;keepdims=True 让每行总和仍保留一列,从而用各行自己的总和去除。这里 K 只有两个维度,因此可以使用 .T。

import numpy as np
x = np.array([[1., 0.], [0., 1.], [1., 1.]])
wq = np.eye(2)
wk = np.array([[1., 1.], [0., 1.]])
wv = np.array([[1., 0.], [1., 2.]])
q, k, v = x @ wq, x @ wk, x @ wv
scores = (q @ k.T) / np.sqrt(q.shape[-1])
e = np.exp(scores - scores.max(axis=-1, keepdims=True))
weights = e / e.sum(axis=-1, keepdims=True)
output = weights @ v

共享程序 code/part-iv/run.py 会把 Q、K、V、点积、权重和输出的 每一个元素,与 data/part-iv/attention.json 对照。预期值来自前面的整数展开和以 r 表示的精确表达式,独立于 NumPy 的矩阵实现。在已记录的 CPU、NumPy 2.0.2 运行中,整数乘积完全一致;权重的最大绝对误差为 0.000000000000000111,输出为 0.000000000000000222,均小于规定的绝对容差 0.000000000001。这测量的是计算是否一致,不是模型质量。完整记录在 data/part-iv/numpy-run.json,环境和复跑命令在 code/part-iv/README.md。

每个位置现在都能产生查询,与序列中的键比较,并接收由当前查询决定的值向量混合结果。W 矩阵规定三种变换,注意力权重则是依赖本次输入的临时结果。我们已经算清了机制,还没有训练这些矩阵。

这仍只是一个注意力头,不是完整的 Transformer 块:它还缺少残差路径、归一化、位置机制和逐位置的非线性网络。眼前更紧迫的问题是,第一个位置已经读到了第二、第三个位置。如果它的任务是在第二个 token 出现以前预测它,刚才的计算就已经偷看了答案。第 16 章要把“允许使用哪些信息”写进计算规则。

  1. 把第三条查询改为 [0,0],K、V 不变。计算它的三个权重和两个输出坐标。为什么查询为零,输出通常不为零?
  2. 一个程序返回 [3,3] 矩阵,每一 列 的和都是 1。这足以复现本章的注意力头吗?请给出一个列和为 1、但第一行不构成有效混合比例的具体矩阵。
  3. 固定 Q、K,给 V 的每一行都加上 [2,-1]。从表 15.1 对应的未舍入输出出发,计算新的第一行输出。如果只改 V 的第一行,同一个结论还成立吗?
  4. 一个头的 Q、K 都是 [5,4],V 是 [5,3]。写出缩放分母、分数 shape、输出 shape,以及 Softmax 归一化的轴。为什么除以 5 是另一种操作?
  5. 一份报告看到 red 上的权重很大,就声称模型能可靠遵循红钥匙指令。用例 15.2 说明它缺了什么证据,并提出更有帮助的受控比较。
  1. 所有点积都是零,取指数都为 1,归一化分母为 3,所以权重为 [1/3,1/3,1/3]。把 [1,0]、[1,2]、[2,2] 平均,得到 [4/3,4/3]。查询没有给不同来源拉开分数差距,只会使它们权重相同,不会清除值向量。
  2. 不足以。三行分别为 [1,1,1]、[0,0,0]、[0,0,0] 的矩阵,每列和都是 1,第一行的和却为 3。它乘以 V 后,第一个位置得到的是直接求和,而非权重总和为 1 的混合。本章要求每条接收查询各自对应一行归一化权重。
  3. 新输出是 [3.401112092679786,0.1977758146404283]。额外贡献等于共同增加的向量乘以三个权重之和,而这个和为 1。如果只改 V 第一行,额外贡献就只是 [2,-1] 乘以 0.4011120926797859,不再是整个新增向量。
  4. 分母是 4=2\sqrt{4}=2,分数是 [5,5],输出是 [5,3]。每条查询沿五个键位置归一化。4 是每次点积累加的项数,5 是来源位置数;缩放的动机针对前者,而非后者。
  5. 值投影和后续计算可能丢弃或改变这些信息。V 全为零的反例保留了原权重,却没有传入信息。更有帮助的做法是固定一组红、蓝钥匙配对提示,控制任务和其他措辞,检查实际输出的成功与失败。这样的测试也只支持已测条件下的行为判断;一张权重图既不能证明可靠性,也不是完整的因果解释。
  • Vaswani 等(2017),Attention Is All You Need。3.2.1—3.2.3 节支持缩放点积、投影分工以及自注意力与交叉注意力的区别。论文训练出的网络与本章给定的小矩阵是两回事。
  • NumPy,matmul,2.0 版文档。可核对矩阵与批量矩阵乘法的行为;实践记录另外固定了实际安装的补丁版本。