注意力就三步:算查询和每个 key 的分数、softmax 成权重、按权重加权求和。 权重加起来是 1,所以输出是这些 key 的一个凸组合。 实测手算例子:点积 1、0、2、1,除根号 3 后 0.5774、0、1.1547、0.5774, softmax 权重 0.2303、0.1293、0.4102、0.2303。 加到那个 5 位反转任务上, 准确率反而低了一点(0.9300 对 0.9900)——因为 5 个字母本来就装得进那个向量。 换成 10 位序列,权重矩阵的峰值逐行左移,对齐关系是从梯度里自己长出来的。
6.5 的瓶颈是「只留一个向量」。注意力不压缩,把编码器每一步的状态都留着。 第 5 章的卷积是「在固定邻域里加权求和」, 注意力是「在整个序列上加权求和,权重由内容决定」——形式很像,邻域从固定变成了学出来的。 这一节是全书最后一节,2017 年之后的 Transformer 整个就是堆注意力。
一、为什么要回头挑
翻译一句话,输出第 3 个词时,最相关的往往只是输入里的某两三个词。其余的词这一刻根本用不上。 朴素 Seq2Seq 只能靠那一个向量, 它被迫把「所有词的摘要」都塞进去,而且塞的时候不知道后面要问什么。 注意力换了个做法: 输入每一步的隐状态都留着,解码到第 t 步时,拿当前状态当查询,去和每一个输入状态算相似度。 相似度高就多拿一点,低就少拿。 这就是「回头挑重点」。关键在于「挑」这件事本身是算出来的,不是人定的。
二、三步
第一步算分数。最常用的是点积:查询和 key 的内积,越大说明方向越接近。 第二步除根号 d。 维度一大点积的方差就大,softmax 会变得极端,梯度几乎为 0。除根号 d 把方差拉回 1 左右。 第三步 softmax 成权重,再对 value 加权求和。 权重非负且和为 1,所以输出是 value 的凸组合,一定落在它们围成的范围里。 这里 key 和 value 常是同一个东西(就是编码器的隐状态)。 分开是后来的做法,Transformer 里才明确分成两套投影。 还有一个细节:softmax 永远不会给出绝对的 0。就算某个 key 完全不相关,它也会拿到一点权重。 这是好事——梯度还能流过去。
三、手算一遍
| 第几个 key | 点积 | 除根号 d 后 | softmax 权重 |
|---|---|---|---|
| 1 | 1.0000 | 0.5774 | 0.2303 |
| 2 | 0.0000 | 0.0000 | 0.1293 |
| 3 | 2.0000 | 1.1547 | 0.4102 |
| 4 | 1.0000 | 0.5774 | 0.2303 |
查询是(1, 0, 1)。第 3 个 key 和它一模一样,点积最大 2,除根号 3 后 1.1547,拿 0.41 的权重。 第 2 个 key 是(0, 1, 0),和查询完全正交,点积 0,只拿 0.13。 但它不是 0,因为 softmax 不会给出 0。 加权求和的结果是(+0.8707, +0.3595, +0.4102)。 三个分量都落在四个 key 的对应分量范围里——这就是凸组合的意思。 这一条很重要:注意力不会「创造出」新的向量,它只能在已有的里面调配。想让它表达新东西, 得在前后加线性层。
四、加到反转任务上:一个诚实的结果
| 做法 | 参数 | 整串全对 | 逐位正确 |
|---|---|---|---|
| 不用注意力 | 838 | 0.9900 | 0.9980 |
| 用注意力 | 934 | 0.9300 | 0.9860 |
加注意力反而低了一点。这个结果要说实话,不能圆。 原因很清楚:5 个字母的信息本来就装得进 16 维,瓶颈不紧,注意力没有东西可救, 反而多了 96 个参数要训。 这是一条通用的经验:结构要对着瓶颈用。瓶颈不紧的时候,加容量只是加噪声。 96 个参数是怎么来的,值得算一遍: 这一版是最朴素的点积注意力,没有 Wq、Wk、Wv 三套投影——查询就是当前隐状态, key 与 value 就是编码器每一步的隐状态。 唯一的改动在输出层:它原来吃 16 维,现在吃「隐状态拼上上下文」的 32 维, Wo 从 16×6 变成 32×6,正好多 96 个。 也就是说,这一版注意力的开销几乎全落在输出层,打分本身是零参数。 Transformer 里那三套投影才是参数大户,那是后来的事。
五、长序列上才看得见结构
把序列拉到 10 位,同一个 16 维编码器就压不住了。3000 轮后逐位正确率 0.6820,这个规模它学不完。 但注意力权重已经有结构:最大值 0.25,均匀是 0.10;而且每一行的峰值在往左移。 逐行读那张矩阵: 输出第 1 位的峰值在第 9 列,第 2 位在第 8 列,第 5 位在第 5 列。这正是反转任务需要的反对角线。 没有人告诉它该看哪儿。这个对齐是从梯度里自己长出来的。这是这一节最核心的一句话。 权重还不够尖锐(0.25 对 0.10,只差两倍半),因为模型本身也没学完。训练充分之后, 这些行会锐化成一条清晰的对角线。
六、从这里往前看
2017 年的 Transformer 把循环整个去掉,只留注意力和前馈层,而且让所有位置互相看,那叫自注意力。 第 5 章的卷积、这一章的循环, 在 Transformer 里都变成了「注意力的一个特例或补充」。卷积的局部性被重新加回去(比如 Swin), 位置信息靠位置编码补。 回头看这六章,走的是一条很清楚的线:每一步都在补前一步的具体缺陷。全连接装不下空间结构, 卷积补上;卷积要求固定长度,循环补上;循环记不住远处,注意力补上。 所以下次再遇到一个新结构,先问它补的是哪个缺陷。这个问题比记公式有用得多—— 因为缺陷是有限的,花样是无限的。
七、动手 Demo:每次解码时自己回头挑重点
先手算一个小例子:一个查询对四个 key,看点积、softmax、加权求和各自算出什么。 再把注意力加到 6.5 那个反转任务上,跑同样 2000 轮,和不带注意力的版本对比。 最后把序列拉到 10 位,看注意力权重矩阵长什么样。
import numpy as np
q = np.array([1.0, 0.0, 1.0])
K = np.array([[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[1.0, 0.0, 1.0],
[1.0, 1.0, 0.0]])
sc = K @ q / np.sqrt(3.0) # 第一步:点积,再除根号 d
al = np.exp(sc - sc.max()) # 第二步:softmax 成权重
al = al / al.sum()
ctx = al @ K # 第三步:按权重加权求和
print("权重", " ".join("%.4f" % v for v in al))
print("结果", " ".join("%+.4f" % v for v in ctx))
# 放进解码器:第 t 步拿当前隐状态当查询,看编码器每一步
sc = hs_enc @ ht / np.sqrt(d) # hs_enc 形状 T×H,ht 形状 H
al = softmax(sc) # T 个权重,和为 1
ctx = al @ hs_enc # 凸组合,形状 H
实跑输出:
--------------------------------------------------------------------
Attention:每次解码时自己回头挑重点
--------------------------------------------------------------------
先说机制。注意力就是三步:算分数、softmax 成权重、加权求和。手算一个小例子。
第几个 key 点积 除根号 d 后 softmax 权重
------------------------------------------------------
1 1.0000 0.5774 0.2303
2 0.0000 0.0000 0.1293
3 2.0000 1.1547 0.4102
4 1.0000 0.5774 0.2303
加权求和 = +0.8707 +0.3595 +0.4102
**权重加起来正好是 1,所以输出是四个 key 的一个「凸组合」——它落在这些 key 围成的范围里,**
**不会跑到外面去。**
第 3 个 key 和查询最像,拿了 0.41 的权重;第 2 个完全不像,只拿 0.13。
把注意力加到上一个 Demo 那个反转任务上,跑同样 2000 轮:
做法 参数 整串全对 逐位正确
----------------------------------------------
不用注意力 838 0.9900 0.9980
用注意力 934 0.9300 0.9860
**这一行要说实话:加注意力反而低了一点。**
原因是 5 个字母的信息本来就装得进那个 16 维向量,瓶颈不紧,注意力没东西可救,
反而多了一批参数要训。
注意力真正解决问题的地方是「装不下」的时候。把序列从 5 位拉到 10 位,
同一个编码器就压不住了——这时再看注意力权重有没有学到东西。
10 位序列,同样 3000 轮。这个规模下它学不完(逐位正确率 0.6820),
但注意力权重已经能看出结构了:
1 2 3 4 5 6 7 8 9 10
输出 1 0.08 0.11 0.10 0.06 0.10 0.08 0.07 0.08 0.18 0.15
输出 2 0.11 0.11 0.05 0.10 0.08 0.09 0.07 0.20 0.12 0.06
输出 3 0.09 0.08 0.11 0.06 0.06 0.08 0.25 0.11 0.09 0.07
输出 4 0.09 0.08 0.09 0.06 0.09 0.21 0.13 0.07 0.06 0.13
输出 5 0.08 0.07 0.07 0.09 0.22 0.15 0.08 0.04 0.10 0.09
输出 6 0.08 0.08 0.09 0.16 0.17 0.08 0.06 0.06 0.12 0.10
输出 7 0.09 0.10 0.13 0.17 0.09 0.05 0.07 0.10 0.11 0.10
输出 8 0.11 0.12 0.13 0.08 0.07 0.07 0.10 0.13 0.11 0.09
输出 9 0.11 0.11 0.11 0.07 0.07 0.11 0.12 0.08 0.10 0.11
输出 10 0.10 0.09 0.11 0.10 0.12 0.10 0.08 0.07 0.11 0.12
**每一行的峰值在往左移:输出第 1 位看输入第 9 位,第 2 位看第 8 位,第 5 位看第 5 位……**
这正是反转任务需要的对齐关系。
**没有人告诉它该看哪儿,这个对齐是从梯度里自己长出来的。**这就是 6-6 节最核心的一句话。
权重还不尖锐(最大值 0.25,均匀是 0.10),因为模型本身也没学完。
**训练充分之后,这些行会锐化成一条反对角线。**
手算那张表的四列要连着看: 点积 1、0、2、1 除根号 3 之后差距被压小了(1 变 0.58),再 softmax 又拉开(0.13 到 0.41)。 除根号 d 不是可选项。维度是 3 时它只把分数压了 1.7 倍; 维度是 512 时它压 22.6 倍——不除的话 softmax 直接饱和成 one-hot。 加权求和那一行(+0.8707, +0.3595, +0.4102)是四个 key 的凸组合。 注意第一个分量 0.87 比任何单个 key 的第一个分量(最大是 1)都小,凸组合永远不会超出边界。 对比表那两行是这一节最该记住的:838 个参数 0.9900,934 个参数 0.9300。多了 96 个参数, 反而低了 6 个点。 这不是注意力不好用,是这里没有瓶颈给它修。5 个字母、16 维向量,信息装得下, 注意力就是多余的路径。 10 位那张矩阵要横着读: 第 1 行最大值 0.18 在第 9 列,第 2 行 0.20 在第 8 列,第 3 行 0.25 在第 7 列, 第 4 行 0.21 在第 6 列,第 5 行 0.22 在第 5 列。 峰值一路往左走,正是「倒序」这个任务要求的对齐。第 6 行之后开始散(0.16、0.17 在第 4、5 列), 因为后半段它还没学会。 这张矩阵就是注意力最迷人的地方:你喂它一个任务,它自己长出一张对齐表。 没有任何一行代码告诉它「输出第 1 位要去看输入第 10 位」。
30 秒实验
- 为什么点积要除以根号 d,而不是除以 d?把 d 从 3 换成 300 试试看差别。
- 如果把 softmax 换成「只保留最大的那个」,会失去什么?梯度会变成什么样?
- 5 位任务上注意力没帮上忙。这个实验能说明「注意力没用」吗?
常见误区
- 以为注意力权重接近 1 才叫「学到了」。softmax 不会给 0, 均匀是 0.10 的时候,0.25 已经是两倍半。
- 以为加了注意力一定涨点。实测在 5 位任务上是降的,0.9300 对 0.9900。 瓶颈不紧时它只是多余参数。
- 以为注意力权重就是「模型的理由」。权重高说明这一步从这里取的信息多, 不等于模型就是靠它下的决定,多头之后更难这样读。
自测三问(点开对答案)
1. 注意力有哪三步? —— 用查询和每个 key 算分数(一般用点积),除以根号 d 之后 softmax 成权重, 最后按权重对 value 加权求和。 2. 为什么要除以根号 d? —— 维度越大点积的方差越大,softmax 会饱和成近似 one-hot,梯度几乎消失。 除根号 d 把方差拉回 1 左右。 3. 为什么这个 Demo 里加注意力反而降了? —— 5 个字母的信息本来就装得进 16 维隐状态,瓶颈不紧,注意力没有可救的东西, 反而多了一批参数要训练。全书到这里结束。从第 1 章的「什么是深度学习」到这里的注意力,六章走的是同一条线: 每一步都在补前一步的具体缺陷。全连接装不下空间结构,卷积补上;卷积要求固定长度,循环补上; 循环记不住远处,注意力补上。下次再遇到新结构,先问它补的是哪个缺陷。
- ☐ (全书完)