第 6 章:Attention 机制到底在做什么

开篇问题

如果只能用一句话解释 Attention,可以这样说:

Attention 让模型在处理当前位置时,动态决定应该从上下文里的哪些位置拿信息,以及各拿多少。

这句话听起来简单,但它是理解 Transformer 的核心。大模型之所以能在一段文本中追踪指代、组合条件、引用前文、保持代码变量关系,都离不开 Attention 这种“按相关性取信息”的机制。

本章会比前几章更细。你不需要先懂线性代数,我们会用“搜索”“索引”“加权汇总”这些开发者熟悉的概念,把 Q、K、V 和多头注意力拆开。

本章你会理解什么

读完本章,你应该能理解:

  • Attention 为什么可以看成一次软查询。
  • Q、K、V 分别扮演什么角色。
  • 注意力分数如何变成权重。
  • 为什么 Attention 的输出是 Value 的加权和。
  • 多头注意力为什么不是简单重复,而是让模型从多个关系角度看上下文。
  • 因果掩码如何避免生成模型偷看未来 Token。

核心概念

Attention 是“按相关性读取上下文”

假设你在读一句话:

小李把笔记本放进背包,因为它太重了。

读到“它”时,你会自然回看前文。你可能重点关注“笔记本”和“背包”,再结合“太重”判断“它”更可能指笔记本。

Attention 做的事情类似:当前位置会对上下文中的每个位置计算相关性,然后把相关位置的信息汇总回来。

更程序化地说:

query 当前需要什么
keys  每个位置能被怎样匹配
values 每个位置实际提供的信息

当前位置拿自己的 Query 去和所有位置的 Key 比较,得到一组分数。分数越高,说明那个位置越值得参考。最后按这些分数对应的权重,对所有位置的 Value 做加权汇总。

Q、K、V 是同一份 Token 表示的三种投影

每个 Token 在进入 Attention 前都有一个向量表示。Attention 会把这个表示分别变换成三种向量:

  • Query,简称 Q:我现在想找什么信息。
  • Key,简称 K:我这里有什么特征,别人可以怎样找到我。
  • Value,简称 V:如果别人关注我,我能提供什么内容。

它们不是三份原始数据,而是同一个 Token 表示经过三组不同参数变换得到的结果。

token representation
-> Q
-> K
-> V

为什么要拆成三种?因为“用于匹配的信息”和“最终被取走的信息”不一定应该完全相同。

在数据库里,索引字段和返回字段也可以不同。你可能用 userId 查一行记录,但最终拿走的是 nameemailrole。Q/K 更像查询和索引,V 更像被读取的数据。

注意力不是硬选择,而是软加权

Attention 通常不会只选一个位置,而是给上下文中每个位置一个权重:

Token A: 0.05
Token B: 0.70
Token C: 0.20
Token D: 0.05

然后把所有位置的 Value 按权重加起来。权重高的位置贡献更多,权重低的位置贡献更少。

这叫软注意力。它的好处是可微分,训练时可以通过梯度下降优化;也更适合语言这种模糊关系,因为很多时候一个词确实需要同时参考多个位置。

机制拆解

一个最小例子:处理“它”

看这个句子:

程序员把服务器日志发给同事,因为它记录了错误原因。

当模型处理“它”时,可能需要判断“它”指代什么。上下文里有几个候选:

程序员
服务器日志
同事

记录
错误原因

直觉上,“记录了错误原因”的更可能是“服务器日志”。Attention 的目标不是显式输出“指代服务器日志”,而是让“它”这个位置的表示吸收“服务器日志”的相关信息,以便后续预测更合理。

流程可以写成:

1. 为“它”生成 Q:当前位置需要寻找可指代对象。
2. 为每个 Token 生成 K:每个位置声明自己有哪些可匹配特征。
3. 用“它”的 Q 和每个位置的 K 计算匹配分数。
4. 对分数做 softmax,得到权重。
5. 用权重加权汇总每个位置的 V。
6. 得到“它”的新表示。

这个新表示不再只是“它”这个字本身,而是包含了从上下文取回的信息。

第一步:从 Token 表示生成 Q、K、V

假设某个 Token 的向量是 x。Attention 层会用三组可学习参数把它变成:

q = x * Wq
k = x * Wk
v = x * Wv

这里的 WqWkWv 是训练得到的参数矩阵。你可以暂时把它们理解成三个不同的特征提取器:

  • Wq 提取“我要查什么”。
  • Wk 提取“我能被什么查到”。
  • Wv 提取“我被查到后提供什么”。

对一整段序列来说,每个位置都会产生自己的 Q、K、V。

第二步:用 Q 和 K 计算注意力分数

当前位置的 Q 会和每个位置的 K 做相似度计算。常见做法是点积:

score(i, j) = Q_i · K_j

这表示第 i 个位置在处理自己时,对第 j 个位置的关注程度。

如果 Q_iK_j 在向量空间里方向相近,点积就较大,说明匹配度较高。如果方向不相近,分数就较低。

从开发者视角看,这像一次向量检索:

用当前位置的 query 向量
去匹配上下文中每个 token 的 key 向量
得到相关性分数

但 Attention 不是外部检索系统。所有 Q/K/V 都在模型内部即时计算,并参与端到端训练。

第三步:缩放分数,避免数值过大

Transformer 里的 scaled dot-product attention 会把点积分数除以一个缩放因子,通常是 Key 维度的平方根:

score = (Q · K) / sqrt(d_k)

直觉上,向量维度越高,点积值越容易变大。如果分数过大,softmax 后可能变得过于极端,训练不稳定。缩放的作用是让分数落在更适合优化的范围里。

你不需要记住公式细节,但要记住:缩放是为了稳定训练,不是为了改变 Attention 的核心逻辑。

第四步:softmax 把分数变成权重

原始分数可能是任意数字:

程序员: 1.1
服务器日志: 4.8
同事: 0.7
因为: -0.2

softmax 会把它们变成一组总和为 1 的权重:

程序员: 0.03
服务器日志: 0.88
同事: 0.02
因为: 0.01
...

这样模型就能用权重表示“各参考多少”。权重不是人工规则,而是由参数和当前上下文共同计算出来的。

第五步:对 Value 做加权汇总

得到权重后,Attention 会对所有位置的 V 做加权求和:

output_i = weight_i1 * V_1
         + weight_i2 * V_2
         + weight_i3 * V_3
         + ...

注意这里加权的是 Value,不是 Key。Key 负责匹配,Value 负责提供内容。

如果“它”对“服务器日志”的权重很高,那么“服务器日志”的 Value 就会大量进入“它”的新表示。后续层再基于这个新表示继续计算。

第六步:每个位置都做同样的事情

不是只有“它”会做 Attention。序列里的每个位置都会生成自己的 Query,并对上下文里的 Key/Value 做一次读取。

如果序列长度是 n,那么可以想象有一个 n x n 的注意力矩阵:

第 1 个 Token 关注所有 Token 的权重
第 2 个 Token 关注所有 Token 的权重
第 3 个 Token 关注所有 Token 的权重
...

这就是为什么标准 Attention 的计算成本会随序列长度快速增长。上下文越长,位置之间的两两关系越多。

因果掩码:生成时不能偷看未来

语言模型生成文本时,当前位置只能依赖已经出现的 Token,不能看到未来答案。

训练时我们通常一次把整段文本送进模型,为了并行计算,模型会同时处理很多位置。此时必须加上因果掩码,让第 i 个位置只能关注第 i 个位置及其之前的位置。

例如:

位置 1:只能看 1
位置 2:只能看 1, 2
位置 3:只能看 1, 2, 3
位置 4:只能看 1, 2, 3, 4

如果没有这个限制,模型训练时就可以直接看答案,loss 会变低,但学到的是作弊规则,生成时无法成立。

自注意力:Q、K、V 都来自同一段序列

Transformer 语言模型中常说的 self-attention,意思是 Q、K、V 都来自同一个序列。

同一段文本
-> 生成 Q
-> 生成 K
-> 生成 V
-> 序列内部互相读取

“自”不是说每个 Token 只看自己,而是说序列内部的 Token 彼此建立注意力关系。

在其他模型结构里,也可以有 cross-attention。比如一个序列的 Q 去读另一个序列的 K/V。机器翻译、图文模型中都可能出现这种结构。但本书关注大语言模型原理,先抓住 self-attention 即可。

多头注意力:从多个角度同时看

为什么一个注意力头不够

一句话里同时存在多种关系:

小王昨天提交了补丁,因为他发现缓存模块在高并发下会返回旧数据。

处理“他”时,模型需要关注“小王”。处理“缓存模块”时,可能要关注“返回旧数据”。处理“高并发”时,又可能要和“缓存模块”的行为建立条件关系。

如果只有一个 Attention 头,它必须把所有关系压进一套 Q/K/V 匹配空间。多头注意力的想法是:让模型拥有多组不同的 Q/K/V 投影,每组关注不同类型的关系。

多头不是复制多份结果

多头注意力不是把同一个 Attention 计算重复几遍。每个头都有自己的参数:

Head 1: Wq1, Wk1, Wv1
Head 2: Wq2, Wk2, Wv2
Head 3: Wq3, Wk3, Wv3
...

因为参数不同,每个头会把同一份 Token 表示投影到不同空间里。它们可能学到不同关注模式:

  • 某些头更关注相邻词搭配。
  • 某些头更关注主语和谓语。
  • 某些头更关注括号、引号、缩进等结构。
  • 某些头更关注指代关系。
  • 某些头可能承担很难用人类语言命名的统计模式。

这些说法是帮助理解的类比,不代表每个头都有稳定、单一、可解释的职责。

多头注意力的简单流程

多头注意力可以拆成四步:

1. 把每个 Token 的表示分别投影到多个头的 Q/K/V。
2. 每个头独立执行一次 Attention。
3. 把所有头的输出拼接起来。
4. 再经过一次线性变换,得到该层的最终输出。

用更直观的方式表示:

同一句话
-> Head 1 看一种关系
-> Head 2 看另一种关系
-> Head 3 看第三种关系
-> 拼接多种视角
-> 合成新的 Token 表示

这像做代码审查时让不同角色同时看同一段代码:有人关注安全,有人关注性能,有人关注可读性,有人关注边界条件。最后把这些视角合并,得到更完整的判断。

多头让表示更丰富,但也更难解释

多头提供了并行的关系建模能力。它使模型不必在一个注意力分布里表达所有关系,而是可以在不同头中分配不同模式。

但这也意味着模型内部更难解释。某一头的注意力图可能看起来有意义,也可能只是整体计算中的一个中间信号。不要把单个头的可视化当成模型完整推理过程。

一个开发者版伪代码

下面的伪代码忽略了批处理、矩阵维度和工程优化,只保留 Attention 的核心逻辑:

type Vector = number[];

function attention(currentQ: Vector, allKeys: Vector[], allValues: Vector[]) {
  const scores = allKeys.map((key) => dot(currentQ, key));
  const weights = softmax(scores);

  let output = zeroVector();
  for (let i = 0; i < allValues.length; i++) {
    output = add(output, multiply(allValues[i], weights[i]));
  }

  return output;
}

对 self-attention 来说,这个过程会对序列中的每个位置执行一次。真实 Transformer 会把这些操作组织成大矩阵计算,从而在硬件上高效并行。

开发者常见误解

误解一:Q、K、V 是人工设计的字段

Q、K、V 不是开发者手写的语义字段,也不是数据集中天然存在的列。它们是模型通过参数矩阵从 Token 表示中投影出来的向量。

训练会决定这些投影如何工作。我们用“查询、索引、内容”来类比,是为了理解机制,不是说模型内部真的有命名字段。

误解二:Attention 权重最高的位置就是最终答案来源

高权重说明某一层某一头在汇总信息时更关注该位置。但模型最终输出还会经过多头合并、残差连接、前馈网络、多层堆叠和输出层。

所以 Attention 权重可以作为观察窗口,但不能当成完整因果解释。

误解三:多头越多一定越好

更多头可以提供更多关系子空间,但也会增加计算和参数组织复杂度。效果取决于模型整体规模、训练数据、隐藏维度、层数和优化方式。

模型设计不是简单把头数调大。

误解四:Attention 本身就等于理解

Attention 提供了一种上下文信息路由机制。它让模型能把相关信息聚合到当前位置。但“理解”还依赖训练目标、数据分布、层间组合和后续预测。

Attention 是关键机制,不是全部能力的唯一来源。

误解五:Attention 只关注语义相近的词

Attention 学到的是对任务有用的关系,不一定是人类直觉中的语义相似。括号匹配、代码变量引用、否定词、格式边界、列表编号,都可能成为重要关注对象。

有时两个 Token 语义不相近,但结构关系很强,Attention 仍然可能把它们连接起来。

小实验/思考实验

实验一:手动做一次注意力分配

读下面句子:

张工把配置文件发给测试同学,因为它包含新的开关。

当你读到“它”时,给每个候选词分配权重:

张工:?
配置文件:?
测试同学:?
新的开关:?

大多数人会给“配置文件”较高权重,因为“包含新的开关”更符合配置文件的属性。Attention 做的事类似,只是权重由向量匹配计算出来,而不是由人手动填写。

实验二:把 Q/K/V 映射到搜索系统

想象一个搜索系统:

  • Query:用户输入的检索意图。
  • Key:每篇文档建立索引时抽取的可匹配特征。
  • Value:文档真正返回给用户的内容。

如果 Query 和某个 Key 匹配度高,该文档的 Value 就更可能被读取。

Attention 与搜索系统的差异也很重要:

  • Attention 在模型内部对每层每个位置即时发生。
  • Q/K/V 都是训练出来的连续向量。
  • 输出不是返回某篇文档,而是加权混合多个 Value。

这个差异正是 Attention 能端到端训练的原因。

实验三:观察“不能偷看未来”

给定训练文本:

Redis 是一个内存数据库。

预测“内存”时,模型输入只能看到:

Redis 是 一个

它不能看到后面的“数据库”。如果训练时允许当前位置关注未来 Token,模型就可以靠答案本身完成预测。这会让训练目标失效。

因果掩码保证模型学习的是“根据过去预测未来”,而不是“根据未来抄答案”。

本章小结

Attention 的核心是按相关性读取上下文。每个 Token 的表示会被投影成 Q、K、V:Q 表示当前位置想找什么,K 表示每个位置如何被匹配,V 表示每个位置能提供什么信息。模型用 Q 和 K 计算分数,通过 softmax 得到权重,再用权重对 V 做加权汇总。

多头注意力让模型可以从多个关系空间同时观察同一段文本,再把这些视角合并成更丰富的表示。因果掩码则确保生成式语言模型在训练和生成时都只能使用当前位置之前的信息。

理解 Attention,就抓住了 Transformer 最关键的信息流动方式:不是把上下文压成一个固定状态,而是让每个位置动态地从其他位置取回有用信息。

自测题

  1. 用自己的话解释 Q、K、V 分别是什么。
  2. 为什么 Attention 的输出是 Value 的加权和,而不是 Key 的加权和?
  3. softmax 在 Attention 中起什么作用?
  4. 多头注意力相比单头注意力带来了什么能力?
  5. 因果掩码为什么对生成式语言模型必要?
  6. 为什么不能把 Attention 权重直接等同于模型的完整推理过程?