从零开始理解大模型(四):Attention——大模型的"阅读理解"机制

欢迎阅读「从零开始理解大模型」系列 —— 十篇文章,从"下一个词预测"到完整的大模型心智模型。每篇配可运行代码。
第一篇:一切从"猜下一个词"开始 第二篇:Token——大模型眼中的"字"长什么样 第三篇:向量与 Embedding——把文字变成数学 第四篇:Attention——大模型的"阅读理解"机制(本文) 第五篇:Transformer 全景——积木怎么搭成大厦 第六篇:训练——70 亿个参数是怎么"学"出来的 第七篇:推理——你按下回车后的这一秒发生了什么 第八篇:上下文窗口——大模型的"工作记忆" 第九篇:Scaling Law——为什么"大力出奇迹"有效 第十篇:从大模型到 Agent——下一个词预测如何长出手脚 * 本文实操配套代码附件,可在本公众号后台回复“大模型”获取。
作者:十一
上一篇结尾的链路图中间有一个最大的黑盒:
[向量₁', 向量₂', 向量₃'] ← Embedding + 位置编码(第三篇)
│
▼ 进入 Transformer
│ ← 这里面发生了什么?
▼
P("much") = 99.2%
我们知道 Embedding 给每个词发了一张"特征身份证",但也看到了它的局限——"bank" 不管出现在"I deposited money at the bank"还是"The river bank",查出来的初始向量都一样。模型需要一种机制来读上下文,根据周围的词判断 "bank" 是"银行"还是"河岸",修正每个向量的含义。
这个机制就是本篇的主角——Attention(注意力机制)。
▍一、先说结论

一句话总结:Attention 让每个词能"看见"上下文中的所有其他词,并决定该重点关注谁。
▍二、用"图书馆找书"来理解 Attention
想象你走进图书馆,要找关于"量子物理"的信息。
你不会把每本书从头到尾读一遍——你会先扫视书架上每本书的标签,大脑自动给它们打分:《量子力学入门》95 分、《经典力学》40 分、《烹饪指南》0 分。然后你把绝大部分精力花在读高分那本书的内容上。
Attention 做的就是这个过程,对应三个角色:
-
Query(Q):"量子物理"——你带着的搜索需求 -
Key(K):书架上每本书的标签——用来和你的需求做匹配 -
Value(V):每本书的实际内容——匹配成功后你真正要读的东西
完整流程:Q 和每个 K 匹配打分 → 分数高的权重大 → 按权重把所有 V 的内容加起来。
回到大模型:模型在处理 "Thank you very ___" 时,最后一个位置(要预测的位置)带着自己的 Query 去"扫视"前面所有词的 Key,发现 "Thank" 和 "very" 的分数最高,于是把它们的 Value 信息重点融合进来——这样 "much" 就获得了最高的预测概率。
▍三、Q、K、V 怎么来的
每个 token 经过 Embedding 后是一个向量(比如 768 维)。Attention 不直接用这个原始向量,而是通过三个权重矩阵,变换出三个不同角色的向量:
原始向量 (768维)
├── × W_Q ──→ Query (64维) "我在找什么?"
├── × W_K ──→ Key (64维) "我能提供什么索引?"
└── × W_V ──→ Value (64维) "我的实际内容是什么?"
为什么要三个不同的向量?因为同一个词在不同角色下需要展示不同的面。比如 "Thank":作为 Query 时它在找和自己搭配的词;作为 Key 时它广播自己是一个感谢用语;作为 Value 时它提供"感谢"的具体语义。
W_Q、W_K、W_V 就是权重——训练时调整,推理时固定。第三篇提到的"开源权重中 Attention 占约 49%",主要就是这些矩阵。
▍四、Attention 的三步计算
整个过程可以用一个公式概括:
Attention(Q, K, V) = softmax(Q · Kᵀ / √d) · V
别被公式吓到——它只是下面三步的缩写。Q · Kᵀ 是第一步(打分),softmax 是第二步(归一化),· V 是第三步(加权求和),√d 是缩放因子防止数值太大。
以 "Thank you very" 为例,3 个 token 已经通过 Embedding 变成了向量,现在做 Attention。
4.1 第一步:打分(公式中的 Q · Kᵀ / √d)
每个词的 Query 和所有词的 Key 做点积,得到相关度分数:
Key
Thank you very
Query Thank [ 0.8 0.2 0.3 ]
you [ 0.3 0.5 0.4 ]
very [ 0.6 0.2 0.7 ] ← "very" 关注 "Thank" 和自己
分数怎么算的? 以 "very" 对 "Thank" 的 0.6 为例:把 "very" 的 Query 向量和 "Thank" 的 Key 向量逐元素相乘再求和(点积):
Q(very) · K(Thank) = q₁×k₁ + q₂×k₂ + ... + q₆₄×k₆₄ = 0.6
点积越大,说明两个向量方向越一致——Q 要找的和 K 能提供的越匹配。这和第三篇的余弦相似度是同一个直觉。
实际模型还会除以 √d(如 √64 = 8),防止点积数值过大导致 Softmax 输出太极端。
"打分"到底在打什么分? 这个分数同时承载了三层含义:
相似度分。 从向量数学看,点积衡量的是方向一致性。Q(very) 和 K(Thank) 方向接近 → 分数高 → 模型认为当前查询需求和这个位置的信息高度匹配。
贡献度分。 这个分数决定了对应的 Value 应该贡献多少信息。分高 → 这个位置的内容对当前上下文重要;分低 → 是"噪声",在后续加权求和时会被过滤掉。
注意力分配分。 面对一串 token,模型不可能给所有词同等关注。打分就是在回答:"为了理解当前的词,我应该花多少精力去'看'其他词?"——这就是"注意力"这个名字的由来。就像你在图书馆不会平均阅读每本书,而是把精力集中在最相关的那几本上。
4.2 第二步:归一化(公式中的 softmax)
把每一行的分数转成概率(加起来等于 1):
Key(归一化后)
Thank you very
Query Thank [ 0.46 0.24 0.30 ]
you [ 0.27 0.33 0.40 ]
very [ 0.36 0.24 0.40 ] ← 40% 注意力给自己,36% 给 "Thank"
怎么算的? 以 "very" 那行 [0.6, 0.2, 0.7] 为例,Softmax 先对每个分数取 e 的指数,再除以总和:
e^0.6 = 1.82, e^0.2 = 1.22, e^0.7 = 2.01
总和 = 5.05
归一化: [1.82/5.05, 1.22/5.05, 2.01/5.05] = [0.36, 0.24, 0.40]
效果:分数高的被放大,低的被压缩,加起来恰好等于 1——变成了概率分布。这和第一篇预测下一个词时的 Softmax 是同一个操作。
4.3 第三步:加权求和(公式中的 · V)
用注意力分布作为权重,把所有词的 Value 向量加权求和:
"very" 的新向量 = 0.36 × Value(Thank) ← 贡献最大
+ 0.24 × Value(you)
+ 0.40 × Value(very)
结果:"very" 的向量里融合了 "Thank" 的信息。当模型用这个新向量预测下一个词时,它"知道"前面是 "Thank you very"——所以 "much" 的概率被大幅提高。
Attention 的全部运算就是这三步:打分 → 归一化 → 加权求和。 没有循环,没有条件判断,都是矩阵乘法。
▍五、实际看 Attention 分数
用代码可以提取 GPT-2 真实的 Attention 矩阵:
outputs = model(input_ids, output_attentions=True)
attention = outputs.attentions[0][0, 0] # Layer 0, Head 0
# attention.shape: [3, 3] ← 3 个 token 之间的注意力分数
对 "Thank you very" 的实际结果(文末 attention.py 可以复现):
Layer 0, Head 0:
Thank you very
Thank │ 1.000 · · ← 只能看自己
you │ 0.960 0.040 · ← 96% 注意力给了 "Thank"
very │ 0.670 0.180 0.149 ← 67% 注意力给了 "Thank"
几个关键观察:
"Thank" 那行只有一个 1.000。 它是第一个词,只能看自己(因果掩码),所以 100% 注意力在自己身上。
"you" 和 "very" 都重点关注 "Thank"。 这很合理——"Thank" 是这句话的核心语义(感谢),后面的词都需要从它那里获取信息来做出正确预测。
右上角全是 ·(零)。 每个词只能看自己和前面的词,不能看后面——这就是因果掩码(Causal Mask)。
这就是因果掩码(Causal Mask)——推理时后面的词还没生成,模型不能偷看未来。矩阵右上角全是 0:
✓ ✗ ✗ "Thank" 只能看自己
✓ ✓ ✗ "you" 能看前 1 个词
✓ ✓ ✓ "very" 能看所有词
▍六、多头注意力:同时从多个角度看
GPT-2 每一层有 12 个注意力头,每个头独立做一套 Q/K/V 计算。
为什么要多个头?因为一句话需要从多个角度理解:
# 提取所有 12 个头对 "very" 的注意力分布
for head in range(12):
att = outputs.attentions[0][0, head, -1, :] # "very" 的注意力
max_token = tokens[att.argmax()]
print(f"Head {head}: 最关注 '{max_token}'")
不同的头会关注不同的东西——有的头关注语法搭配("very" → "Thank"),有的关注相邻位置("very" → "you"),有的关注自身。12 个头从 12 个角度读同一句话,拼在一起就是全面的理解。
没有人告诉 Head 0 去关注语法、Head 3 去关注位置。这些分工是训练过程中自动形成的——每个头学到了一种对预测下一个词有用的关注模式。
每个头的 Q/K/V 维度是 64(= 768 / 12),12 个头的结果拼接起来还是 768 维:
Multi-Head Attention = Concat(Head₁, Head₂, ..., Head₁₂) × W_O
每个 Headᵢ = Attention(Q_i, K_i, V_i) // 独立计算,64 维
拼接后: 12 × 64 = 768 维
W_O: 输出投影矩阵,768 × 768
完整的多头注意力分析代码见附件 multi_head.py。
* 本系列文章完整实操配套代码,可在公众号后台回复“大模型”获取。
▍七、Attention 在完整链路中的位置
把第四篇学到的内容嵌入之前的链路:
"Thank you very"
│
▼ 分词(第二篇)
[10449, 345, 845]
│
▼ Embedding + 位置编码(第三篇)
[向量₁, 向量₂, 向量₃] ← 初始向量
│
▼ Attention(本篇)—— 打开的黑盒
│ 每个向量生成 Q、K、V
│ Q · Kᵀ / √d → 打分
│ softmax → 注意力分布
│ 加权求和 V → 融合上下文
│ 12 个头并行,结果拼接
▼
[新向量₁', 新向量₂', 新向量₃'] ← 融合了上下文
│
▼ 还有 FFN、残差连接...(第五篇)
▼ 重复 12 层
▼ → softmax → P("much") = 99.2%
但 Attention 只是一层中的一半——另一半是 FFN(前馈网络)和"胶水"组件(残差连接、LayerNorm)。它们一起构成一个完整的 Transformer 层,堆叠几十层才是最终模型。这是下一篇的主题。
▍八、n² 的代价
Attention 的注意力分数矩阵是 n × n(n 为 token 数)。每个词要和所有词算相关度:
输入 100 tokens → 100 × 100 = 10,000 次计算
输入 1,000 tokens → 1,000² = 1,000,000 次
输入 10,000 tokens → 10,000² = 100,000,000 次
长度增 10 倍,计算量增 100 倍。这就是为什么长文本慢、显存占用大、各种"高效 Attention"变体都在想办法降低 n²。第八篇(上下文窗口)会详细讲。
▍九、结语
Attention 不神秘。
它做的事情用一句话就能说清:让每个词看看上下文中的所有词,算出该关注谁,然后把被关注词的信息融合到自己身上。
三步运算——打分、归一化、加权求和。多个头并行,从不同角度做同样的事。因果掩码确保只能看过去,不能偷看未来。
"Attention is all you need." — Vaswani et al., 2017
2017 年那篇论文的标题恰如其分。Attention 让每个词能一次性看到整个上下文,这个简单的改变带来了革命性的效果。
下一篇,我们把 Attention、FFN、残差连接、LayerNorm 组装起来,看一个完整的 Transformer 层长什么样。
本文配套代码:attention.py(Attention 矩阵可视化)、multi_head.py(多头注意力分析)。需要 Python 3.8+、transformers、torch。

扫码回复“大模型”
获取本系列文章完整配套代码
「从零开始理解大模型」是「从零开始理解 Agent」的姊妹系列。Agent 系列讲"四肢",本系列讲"大脑"。建议对照阅读 专栏入口。
今日推荐
- 点赞
- 收藏
- 关注作者
评论(0)