卷 III · 肚子里CH 09深度 9/20

注意力:每个位置自己查一次表

第 6 章那台小网络只能看固定的 8 个字符,把它们拼成一长条硬塞进去。GPT-2 要看前面 1023 个 token,而哪一个重要,每一句话都不一样。解决办法叫注意力:让每个位置自己决定回头去读谁、读多少。这一章打开 GPT-2 的 144 个注意力头,看几个真的:一个只盯着上一个 token 的,一个替你找出「谁该拿到那杯饮料」的——以及一个看得最认真、却在唱反调的。

99.4% 只看上一个144 个头看得多 ≠ 帮得多
◉ 这一章在 GPT-2 的哪一段

▷ 先猜一下

GPT-2 有 12 层,每层 12 个注意力头,一共 144 个。每个头在每个位置,都会把「注意力」分配给它前面的所有 token,加起来是 100%。

第 4 层的第 11 个头,在一篇它从没见过的文章(第 4 章那篇)的 1024 个 token 上,平均把多少注意力分给了「紧挨着的上一个 token」?

A 10% 左右。上一个只是众多前文之一 B 三分之一左右。最近的总是最重要的,但也会看远处 C 70% 左右。大部分给上一个,剩下的看情况 D 99% 以上。它几乎只看上一个

答案在本章末尾。

为什么需要「回头看」

看这句话:When Mary and John went to the store, John gave a drink to。下一个词该是谁?人一眼就知道是 Mary——John 是给东西的人,两人里另一个才是接收者。可要做出这个判断,读到最后那个 to 的时候,得回头找到 12 个 token 之前的 ␣Mary,还得知道 ␣John 出现过两次。

第 6 章那种「把前面几个 token 拼起来」的做法在这里行不通:拼 1023 个向量,网络会大到没法训练,而且第 3 个位置上的 Mary 和第 10 个位置上的 Mary 会被当成两回事。需要的是一种不管距离多远、不管在第几个位置,都能按内容找到要的东西的机制。

一次「模糊」的查表

注意力的做法,像是在每个位置上做一次数据库查询,只不过是模糊的:

  1. 每个位置把自己的向量变换出三样东西:一个查询(query,「我在找什么」)、一个键(key,「我这儿有什么」)、一个值(value,「如果你要我,我给你这个」)。三个变换都是学出来的矩阵。
  2. 当前位置拿自己的查询,和前面每个位置的键做点积——第 5 章说过,点积量的是方向有多像。越像,分越高。
  3. 把这些分数过一次 softmax,变成一组加起来是 1 的权重。
  4. 按权重把前面各位置的值加权混合,取回来。

和真正的数据库查询比:数据库是「键完全匹配就返回那一行」,注意力是「每一行都按像不像打个分,返回所有行的加权平均」。像的多拿,不像的少拿。整个过程每一步都可以求导,所以「该查什么、该亮出什么」全都能用第 6 章的反向传播训出来。

GPT-2 每层有 12 个这样的查询器,叫 12 个头(head),各自有自己的三套矩阵、各查各的——一个可能负责「找上一个词」,另一个负责「找主语」。每个头只用 64 维(768 ÷ 12),12 个头的结果拼回 768 维,再写回去。12 层 × 12 个头 = 144 个头。下面挑几个看。

一个最简单的头:只看上一个

把第 4 章那篇文章的前 1024 个 token 送进去,量每个头平均把多少注意力分给了「紧挨着的上一个 token」:

头(层.编号,从 0 数)分给上一个 token 的注意力
4.1199.4%
2.255.5%
3.738.3%
2.933.5%
3.233.3%

第 4 层第 11 个头,在一千多个位置上平均把 99.4% 的注意力放在上一个 token 身上。它是一个近乎固定的「搬运工」:把上一个 token 的信息搬到当前位置来。这种事为什么需要专门一个头?因为每个位置的向量一开始只知道自己是谁,不知道前面是谁;有了这个头,第 t 个位置上就写着「我前面那个是 X」。第 12 章会看到,这一点点信息,是 GPT-2 「现学现卖」能力的一半零件。

一个聪明的头:谁该拿到那杯饮料

回到那句话。GPT-2 small 的候选表:

…John gave a drink to ▍ … ␣store , ␣John ␣gave ␣a ␣drink ␣to 下一个 token 概率 意外 ' Mary' 44.60% 1.16 比特 ' them' 21.15% 2.24 比特 ' the' 7.11% 3.81 比特 ' John' 6.00% 4.06 比特 ' her' 4.50% 4.47 比特 ' his' 1.24% 6.33 比特 熵 3.40 比特

Mary 44.60%,排第一;John 只有 6.00%。它做对了。是谁做的?在最后那个 ␣to 的位置上,看各个头把注意力分给了谁:

读到最后那个 to 时,两个头把注意力分给了谁(颜色越深,分得越多;整行加起来是 100%) 10.7 号头 When 9% ␣Mary 58% ␣and 3% ␣John 14% ␣went ␣to ␣the ␣store , 2% ␣John 9% ␣gave ␣a ␣drink ␣to 2% 9.6 号头 When 33% ␣Mary 49% ␣and ␣John 12% ␣went ␣to ␣the ␣store , ␣John 6% ␣gave ␣a ␣drink ␣to
头看 Mary看两个 John
10.758.2%23.3%
11.1052.9%31.3%
9.649.2%17.3%

好几个后面几层的头,都从 ␣to 这个位置把目光投向了 Mary,而且明显多于两个 John 加起来。2022 年 Redwood Research 的一篇论文(《Interpretability in the Wild》)专门拆过 GPT-2 small 里解这类句子的整套电路,把这些头叫做「搬名字的头」:它们找出「出现过但还没当过主语的那个名字」,把它搬到当前位置,直接推高它的概率。

但这张表里藏着一个陷阱。下一章会把最终的那个判断——「Mary 比 John 高多少」——拆回到每个头身上,逐个算账。拆出来的结果是:9.6 号头确实在帮 Mary(+1.16),而看 Mary 看得最多的 10.7 号头,在给 Mary 减分(−1.03);11.10 号也一样(−0.57)。那篇论文给它们起了个名字:反向搬名字的头——它们去看 Mary,是为了把 Mary 往下压一压,像是在给其他头的过度自信踩刹车。一个头看向哪里,和它往哪个方向使劲,是两回事。

它天生不知道顺序

注意力有一个结构上的盲点:它只按内容查找,不看位置。把一组输入的顺序打乱,送进一层注意力(不加遮罩、不加位置信息),输出会跟着同样打乱,而每个向量本身一点都不变——本机实测,打乱前后对应位置的最大差值是 0。对它来说,「dog bites man」和「man bites dog」是同一袋词。

所以 GPT-2 在第一步查完词嵌入之后,还要给每个位置加上一个位置向量:第 0 个位置一个、第 1 个位置一个……一共 1024 个,也是学出来的(这张表占了 786432 个参数)。拿掉它会怎样?同一篇文章:

正常                      4.53 比特/token
拿掉位置向量             ⟦10.79 比特/token⟧

意外翻了一倍多。模型的每一层都默认「位置信息已经混在向量里了」,突然抽掉,它连哪个词在前都分不清。第 19 章会看到,今天的模型换了一种注入位置的方式(叫 RoPE,直接把位置「转」进查询和键里),但「注意力本身不知道顺序、必须另外告诉它」这一点没变。

注意力里的一个怪现象:倒在第一个 token 上

还有一件事是在量这些头的时候顺便发现的。看每一层的头平均把多少注意力分给了整段文字的第一个 token(从第 50 个位置往后算,免得被开头几个位置干扰):

层     0     1     2     3     4     5     6     7     8     9    10    11
第一个  0%    1%   11%   31%   35%   53%   48%  ⟦58%⟧  48%   49%   43%   34%

中间几层,平均一半左右的注意力落在了第一个 token 上——而那个 token 只是文章的第一个词「Chat」,和第 800 个位置在想什么毫无关系。这是什么?

答案在 softmax 里:权重必须加起来是 1,一个头不能选择「这一次什么都不看」。当一个头在某个位置上没什么可找的,它需要一个地方把注意力「倒掉」。训练学会了用第一个位置当垃圾桶:它的值向量被学成了几乎不影响结果的东西,倒在那里就等于没看。这个现象后来被叫做注意力汇(attention sink),2023 年的一篇论文(StreamingLLM)还利用它做了一个工程技巧:长对话里永远保留最开头那几个 token,模型就不会崩。

它很贵

最后一笔账。1024 个 token,每个头要给每一对(位置,前文位置)打一次分:1024 × 1024 = 1048576 个分数(因果遮罩挡掉其中一半),144 个头都要算。上下文长度翻一倍,这一项就翻四倍。注意力的代价随上下文长度平方增长——这是「长上下文」一直贵的原因,第 15 章算这笔账。

⌨ 自己跑一遍

让 transformers 把每一层的注意力矩阵交出来(要用 eager 实现,默认的快速实现不保留这些矩阵):

import torch
from transformers import AutoTokenizer, AutoModelForCausalLM

tok = AutoTokenizer.from_pretrained("gpt2")
model = AutoModelForCausalLM.from_pretrained("gpt2", attn_implementation="eager").eval()
x = torch.tensor([tok.encode(open("chatgpt-2026-10-21.txt").read())[:1024]])
with torch.no_grad():
    att = model(x, output_attentions=True).attentions      # 12 层,每层 (1, 12 个头, 1024, 1024)

T = x.shape[1]
prev = torch.stack([a[0, :, torch.arange(1, T), torch.arange(T - 1)].mean(-1) for a in att])
for i in prev.flatten().topk(3).indices.tolist():
    L, h = divmod(i, 12)
    print(f"第 {L} 层第 {h} 个头:平均 {prev[L, h]:.1%} 的注意力给了「上一个 token」")

first = [a[0, :, 50:, 0].mean().item() for a in att]       # 从第 50 个位置往后,平均有多少落在第一个 token 上
print("各层落在第一个 token 上的注意力:", " ".join(f"{v:.0%}" for v in first))

本机输出:

第 4 层第 11 个头:平均 99.4% 的注意力给了「上一个 token」
第 2 层第 2 个头:平均 55.5% 的注意力给了「上一个 token」
第 3 层第 7 个头:平均 38.3% 的注意力给了「上一个 token」
各层落在第一个 token 上的注意力: 0% 1% 11% 31% 35% 53% 48% 58% 48% 49% 43% 34%

想看那句 Mary 和 John 的注意力,把输入换成那句话,看 att[10][0, 7, -1]——第 10 层第 7 个头在最后一个位置上的那一行。

pip install torch transformers && python 09.py

可视化工具:BertViz 能在 Jupyter 里把每个头的注意力画成连线图;Colab 里装好就能用。

▸ 在现实里
  • 「注意力热力图」常被当成模型的解释——「你看,它关注了这几个词,所以它是这么想的」。这一章的两个现象(倒在第一个 token 上的那一半、看得最多却在唱反调的头)都说明:注意力权重只告诉你信息从哪里被搬过来,不告诉你它被拿去干了什么。
  • 长上下文里「中间的内容容易被忽略」,在实际使用中被反复观察到。把最关键的指令放在开头或结尾,是一条朴素但有效的经验。
  • FlashAttention 这类优化不改变注意力算的是什么,只是重新安排计算顺序,避免把那个 1024 × 1024(或更大)的矩阵整个写进显存——平方级的东西,省内存比省计算更要紧。
✗ 这个直觉是错的

「注意力权重高的地方,就是模型认为重要的地方,也是它推高的那个答案。」

两件事都不成立。第一,中间几层平均有一半左右的注意力倒在第一个 token 上(第 7 层是 58%),那个 token 什么内容都不提供——高权重可能只是「没地方去」。

第二,在 Mary 那句话里,看 Mary 看得最多的 10.7 号头(58.2%),对「Mary 胜过 John」的贡献是 −1.03:它在压低 Mary。注意力只决定从哪里取信息;取回来之后往哪个方向写,由这个头的另一组矩阵决定。要知道一个部件在帮谁,得像下一章那样拆账。

◇ 揭晓

正确答案是 D:4.11 号头平均把 99.4% 的注意力给了上一个 token。
排第二的 2.2 号头是 55.5%,再往后就只剩三成多了。

A 「上一个只是众多前文之一」——对大多数头来说确实如此。但 144 个头里会分化出专职的头,有的就只干这一件事。 B 「最近的最重要,但也会看远处」——这是对「整个模型」的合理直觉,用在单个头上就错了。分工发生在头与头之间:有的头只看近处,有的头专找远处的名字。 C 「大部分给上一个」——2.2 号头大概是这样(55.5%)。4.11 号头更极端:它几乎是一根硬接线。

这一章的一句话

注意力是一次模糊的查表:每个位置用自己的查询去比前面所有位置的键,按相似度把它们的值混合着取回来。每个头学一种查法;它们只按内容找、天生不知道顺序;而一个头看向哪里,不等于它往哪个方向使劲。

注意力头把信息取回来之后,写到哪里去?下一章看 GPT-2 的「总线」:每一层都只是往同一条线上加一笔。所以你可以整层删掉中间某一层——删掉第 1 层(层号从 0 数),每个 token 的意外只从 4.53 比特变成 4.56 比特,多了 0.6%。