第 26 章 注意力机制怎样工作
文章目录
- [第 26 章 注意力机制怎样工作](#第 26 章 注意力机制怎样工作)
-
- [🌈 关于《AI 零基础 36 讲》](#🌈 关于《AI 零基础 36 讲》)
- 从一句话到四行输入
- [一个输入为何要变成 Q、K、V 三份](#一个输入为何要变成 Q、K、V 三份)
- 把"它"这一行的匹配分数算出来
- 分数怎样变成一组权重
- [用权重汇总 V](#用权重汇总 V)
- 两个头为什么能给出不同结果
- 生成文字时,为何不能看到后面的位置
- 两个排错点,以及图能说明到哪里
"小猫吃鱼,它饱了。"读到"它"时,前面的"小猫"提供了理解这个代词的重要信息。上一章把词向量取平均,每个词使用相同权重,许多差别会消失。注意力增加了一步,根据当前位置和其他位置的向量计算权重,再按权重汇总信息。
本章只算四个位置、两个维度。需要第 25 章的 Token、向量和余弦知识,以及前面数组与矩阵的基础。句子、切分和参数全是人工教学设定,没有训练模型。我们会逐项计算 Query、Key、Value,核对矩阵形状,代码和运行入口见GitHub仓库。

🌈 关于《AI 零基础 36 讲》
🧠 零基础也能开始学 AI
《AI 零基础 36 讲》是一套面向初学者的 AI 入门教程。课程从认识和使用 AI 开始,逐步学习
Python、数据处理、机器学习与深度学习,再进入大模型应用开发,最后完成个人知识库助手的综合实践。
📖 本篇是正式讲解文章
通过具体例子、原创图示和操作结果讲清概念。涉及编程的章节提供可运行代码,方便你边读边试。
🎯 读完以后,动手检验
建议按专栏顺序学习,并完成对应的课后训练,检查自己能否独立运用本章知识。
博客专栏:《AI 零基础 36 讲》
从一句话到四行输入
为缩小计算,把句子人为分成"小猫、吃鱼、它、饱了"四个 Token,暂时省去标点。输入 X 为四行四列,每个位置用一个不同的独热向量表示,即一行中一个位置为 1,其余为 0。
python
import numpy as np
from examples.attention_core import load_data
data = load_data()
x = np.array(data["x"], dtype=float)
print(data["tokens"])
print(x)
print(x.shape)
输出为四个 Token、4×4 单位矩阵和 (4, 4)。行数是 Token 数,列数是输入特征维度。独热向量在这里仅区分位置对应的 Token,不包含学到的语义和额外位置编码。完整模型通常使用训练得到的表示,下一章再把各层连接起来。
一个输入为何要变成 Q、K、V 三份
Query 简写为 Q,是当前位置参与匹配的向量;Key 简写为 K,是每个候选位置用于匹配的向量;Value 简写为 V,是各位置将被汇总的信息向量。
它们来自不同的线性投影。投影是把原向量乘以一组参数矩阵,得到另一种表示。本章用 Q=XWq、K=XWk、V=XWv,三组参数均为 4×2,因此三份结果都是 4×2。这个机制与矩阵乘法直接相连,不需要把网络描述成有人在脑中挑选词语。
下面是第一个头的实际结果。一个头表示一套投影和一次注意力计算,后面再讨论多个头。
| Token | Q | K | V |
|---|---|---|---|
| 小猫 | 1,0 | 2,0 | 1,0 |
| 吃鱼 | 0,1 | 0,1 | 0,1 |
| 它 | 2,0 | 0,0 | 0,0 |
| 饱了 | 0,1 | 1,1 | 1,1 |
python
from examples.attention_core import load_data, run_heads
heads, concatenated, output = run_heads(load_data())
for name in ["q", "k", "v"]:
print(name, heads[0][name].shape)
print(heads[0]["q"][2])
三份形状都为 (4, 2),第 2 行 Q 为 [2,0],行编号从 0 开始,正好对应"它"。因为 X 是单位矩阵,本章投影结果恰好等于参数矩阵的对应行,这使数值容易复核。真实输入不必是单位矩阵,Q、K、V 也不必相同。
Q 与 K 决定匹配权重,V 提供汇总内容。如果保持 Q 和 K 不变,仅把所有 V 乘以 2,权重不会变化,但汇总输出会乘以 2。这个差别解释了为什么三组名称不能随意互换。
把"它"这一行的匹配分数算出来
用"它"的 Q [2,0] 分别和四行 K 做点积,得到 [4,0,0,2]。比如"小猫"的分数为 2×2+0×0=4,"饱了"为 2×1+0×1=2。
本章采用缩放点积注意力。点积要除以 K 的维度的平方根,这里是根号 2,得到约 [2.828427,0,0,1.414214]。这是 Transformer 原论文的缩放点积规则。它与上一章的余弦不同,没有再除以 Q、K 各自长度。
缩放的目的涉及分数大小和 Softmax 的训练行为。维度增大时,点积的幅度在一定分布假设下也会增大,过大的分数差容易使 Softmax 集中到很少的位置。
分数怎样变成一组权重
Softmax 对各分数取指数,再除以同一行指数之和。输出为非负数,每行总和为 1。本章"它"这一行的结果如下。
| K、V 位置 | 原点积 | 缩放后分数 | 权重 |
|---|---|---|---|
| 小猫 | 4 | 约 2.828427 | 约 0.734577 |
| 吃鱼 | 0 | 0 | 约 0.043418 |
| 它 | 0 | 0 | 约 0.043418 |
| 饱了 | 2 | 约 1.414214 | 约 0.178588 |
python
from examples.attention_core import load_data, run_heads
head = run_heads(load_data())[0][0]
print(head["raw"][2])
print(head["scaled"][2].round(6))
print(head["weights"][2].round(6))
print(head["weights"].sum(axis=1))
前三行输出对应上表,最后为 [1,1,1,1]。显示时四舍五入可能使打印数值的和略有差异,程序核对使用完整精度。
计算指数时还有一个容易忽略的细节。代码先减去每行最大分数,再做指数,避免很大的正数溢出。所有分数一起减去同一个值,不会改变 Softmax 的比例。例如 [1,2,3] 与 [10001,10002,10003] 得到同样权重,这一点也已实际测试。
权重 0.734577 表示在这次人工计算中,"小猫"对应的 V 被赋予这个系数。它不是代词指代正确率,也没有测量语言模型理解句子的水平。数值较高的原因就在我们给定的 Q、K 中,不能绕过参数来源把它解释成模型自行学会了语法。
用权重汇总 V
"它"这一行的输出为四个 V 的加权和。
text
0.734577 × [1,0]
+ 0.043418 × [0,1]
+ 0.043418 × [0,0]
+ 0.178588 × [1,1]
≈ [0.913165, 0.222006]
输出仍为二维,因为每个 V 都是二维。"小猫"贡献第一维,"吃鱼"贡献第二维,"饱了"同时贡献两维。计算过程见逐项贡献表。

连线从当前 Q 位置指向提供 K、V 的位置,粗细表示权重。图用一个位置展开全部计算。要同时处理四个 Q,就把它们排成矩阵,运算写成 softmax(QK.T / sqrt(dk)) @ V,其中 Softmax 按每一行执行。
| 步骤 | 矩阵形状 |
|---|---|
| Q、K、V | 各 4×2 |
| K 转置 | 2×4 |
| Q 与 K 转置相乘 | 4×4 |
| 行内 Softmax | 4×4 |
| 权重矩阵与 V 相乘 | 4×2 |
权重矩阵的行是接收信息的位置,列是提供信息的位置。拿某一列求和,不要求等于 1。把 Softmax 做在列上会变成另一种计算,不能与本章公式混用。
两个头为什么能给出不同结果
本章另外给出第二套投影。对同一个"它",第二头权重约为 [0.043418,0.043418,0.178588,0.734577],最大的系数对应"饱了"。两套不同参数对同一输入得到不同匹配关系,这就是多头机制可以展示的核心计算差别。

两个头各自产生 4×2 输出,沿特征方向拼成 4×4,再乘本章给定的 4×4 输出投影 Wo,结果仍为 4×4。拼接保留每个头的输出分量,后面的投影再组合这些分量。原论文的多头注意力部分给出了相应计算结构。
python
from examples.attention_core import load_data, run_heads
heads, joined, projected = run_heads(load_data())
print(heads[0]["output"].shape, heads[1]["output"].shape)
print(joined.shape, projected.shape)
print(heads[1]["weights"][2].round(6))
输出两次 (4,2),随后两次 (4,4),最后对应第二头的权重。真实模型会训练各头参数,各头可能获得不同关系,也可能有冗余。不能事先规定第一头永远负责主语、第二头永远负责动作;本章这些差异只是人工设置的可见计算。
生成文字时,为何不能看到后面的位置
前面的计算允许全部位置互相提供信息,称为不加因果限制的自注意力。自注意力表示 Q、K、V 都来自同一输入序列的不同投影。生成下一个 Token 时,则要限制当前计算只使用已经可见的位置。
在方形的因果自注意力矩阵中,将当前行后面位置的分数设为负无穷,再做 Softmax,那些位置的指数变为零。"它"是第 2 行,可以使用"小猫、吃鱼、它",不能使用第 3 行的"饱了"。本章因果版本权重变为约 [0.894285,0.052857,0.052857,0]。
python
from examples.attention_core import load_data, run_heads
head = run_heads(load_data(), causal=True)[0][0]
print(head["weights"][2].round(6))
print(head["weights"][0])
第二个输出为 [1,0,0,0],因为第一个位置在这个示例中只能使用自己。屏蔽之后,剩余权重会重新归一化,不能在原权重上直接删除最后一项而不调整其他项。
两个排错点,以及图能说明到哪里
如果直接写 Q @ K,两个 4×2 矩阵无法相乘,程序报维度不匹配。修复版使用 K.T,得到 4×4 分数矩阵。另一个问题是在某行把所有位置都屏蔽,负无穷分数无法产生有效的归一化权重。本章主动报错,修复版保留一个合法位置,得到 [1,0]。两组真实结果见错误与修复。
注意力图是这一步计算的系数图。后面还有 V 的内容、其他头、投影、残差和更多层,不能凭一条粗线就解释最终答案的全部原因。关于把注意力直接当解释的限制,可参考研究论文 Attention is not Explanation。
此外,当前演示未加入位置编码。如果同时重排 Q、K、V 的行,不加因果掩码时,输出会按同样顺序重排。注意力核心计算本身没有自动提供文字先后关系。下一章会把位置表示、残差、归一化和前馈网络接起来。先完成【零基础学AI】第 26 章课后练习与答案,试着修改一个 K,重新算出权重和输出的变化。