全书导航
大模型之路:从图灵、感知机到 ChatGPT · 卷 3

第 23 章:Attention:让模型学会关注

本章问题:模型如何在长句子中找到相关信息?


Seq2Seq 解决了变长转换,但编码器的上下文向量是固定大小的——输入 50 个词和 3 个词,都塞进同一个维度。长句信息必然丢失。这一章引入 Attention,让解码器在每一步动态选择要看输入的哪些位置。

23.1 编码器的瓶颈

上一章 Seq2Seq 的核心矛盾可以用一句话总结:

编码器把整个输入句子读完后,必须在脑子里形成一个固定大小的"意思",然后解码器只能从这个固定大小的意思来生成——无论输入句子是 3 个词还是 50 个词。

当你在翻译一句话时——假设是一个相当长的句子——编码器读到最后时,它的隐藏状态可能已经忘记了句子开头的主语。但它必须在一个固定的 512 维向量里装下一切——句法、主语、宾语、时态和全部语义细节。解码器在生成到半途时,已经得不到关于输入句子前半部分的任何原始信息。

这是固定上下文向量结构性的局限。你当然可以把向量的维度从 512 加到 2048——能缓解一点——但根本的"全部信息必须强行通过一个固定大小的通道"这个问题只会被推迟,不会消失。

更好的方式不是把通道变宽。是让通道可以选择性打开——解码器在每一个生成步,都直接看到整个输入句子的所有位置,然后自己决定这一时刻它最需要关注输入的哪几个位置。

这个机制,就是 Attention


23.2 Attention 的本质:加权平均

不要先理解公式。先理解这个动作:

你有一段输入(比如编码器在每个时间步产生的隐藏状态——共 5 个,对应 5 个输入词)。你在生成完目标句子的第一个词后,要生成第二个词。你手上还有一个查询向量(Query)——当前解码器的隐藏状态,代表着"我现在正在生成什么内容,我需要从输入中寻找什么信息"。

你把这个 Query 和每一个输入隐藏状态分别做一次匹配(计算相似度得分)。得分越高的位置,意味着这个位置的输入和当前解码状态"最相关"。然后你用这些得分作为权重,把所有输入向量做一个加权和——那些得分高的输入位置的向量在求和中贡献大,得分低的贡献小

加权和的结果是一个上下文向量(context vector)——但和 Seq2Seq 中那个唯一的、强塞进编码器最后状态的向量不同,这个上下文向量是动态的——为解码器的每一步单独计算一个、和当前步最相关的信息浓缩版。

然后解码器把当前步特有的这个上下文向量和它自己的当前状态合在一起,生成下一个输出词。

译成日常语言:解码器在生成每一步时,"回头扫一眼原文",找到自己此刻最需要参考的是哪几个词,提取信息,然后继续说。

这就是 Attention。它不是一个复杂的数学玩意儿——它是一个加权平均加上可学习的匹配权重。


23.3 Q、K、V:三个可学习的投影

2015 年,Bahdanau 等人的团队和 Luong 等人的团队几乎同时独立发表了 Attention 在神经翻译中的应用——这是 Attention 第一次被系统地证明能在长句翻译上大幅超越传统的 Seq2Seq。

随后,在 2017 年的 Transformer 论文中,Attention 被推到了其最通用的形式——Query-Key-Value 结构。这个框架现在已经成为所有 Attention 讨论的标准语言。

Q、K、V 不是三个不同种类的向量。它们是由同一个原始向量经过三个不同的线性投影(分别乘以三个可学习的权重矩阵 W_Q、W_K、W_V)后获得的。每一行操作都是一样的——"输入向量 × 权重矩阵 → 输出向量"。

Q(Query,查询):代表"我现在在找什么"。解码器的当前状态在 Attention 层中投影为 Q——"我在生成名词,我需要知道主语;我在生成限定动词,我需要知道时态"。

K(Key,键):代表"我是什么内容"。编码器每个输入位置的向量投影为 K——"我是主语'我';我是谓语'买';我是宾语'牛奶'"。

V(Value,值):代表"如果你需要我,我从数据上拿什么给你"。编码器每个输入位置的向量投影为 V——和 K 从相同的位置产生,但经过不同的权重矩阵投影,携带的"可用信息"不同。

Attention 的计算顺序:

  1. 所有 K 和当前的 Q 做一次相似度计算(通用做法是点积,再除以 √d_k 做数值稳定——叫"缩放点积"),得到注意力分数
  2. 分数通过 softmax 变成注意力权重——每个位置的权重在 0 到 1 之间,所有权重之和为 1。Softmax 倾向于放大高分与低分之间的差距——经过它之后,最相关的那几个位置的权重远高于其他位置。
  3. 权重和 V 做加权平均,得到输出

用一句话串起来:Q 问 K 那里有什么,K 告诉 Q 它有什么,Q 根据相似度给每个 K 分配关注度,然后从 V 那里按比例提取信息。


23.4 为什么叫"缩放点积 Attention"

Attention 的基础得分计算是缩放点积(scaled dot-product):

score(Q, K) = (Q · K) / √d_k

点积(两个向量的内积)衡量两个向量在同方向上的投影重叠程度——方向相近→正大值,方向相反→负大值,正交→接近零。它是一种简单有效的"通用相似度衡量"。

但这里有个数值稳定性的细节:Q 和 K 的维度 d_k 如果很大(比如 64、128),它们的点积可能在绝对值上变得非常大,会导致 softmax 函数被推到饱和区。Softmax 的饱和区梯度接近于零——模型学不到任何新信息。

除以 √d_k(维度大小的平方根)是缩放——把点积的方差重新控制在 1 量级,让 softmax 保持在敏感的学习区内。这个缩放操作尽管简单,但对大规模 Attention 的训练稳定性至关重要——在多头 Attention(下一章)积累成百上千个并行的点积时,如果不缩放,训练就直接崩溃。


23.5 Softmax:把"相关度"变成"注意力分布"

Softmax 是最重要且最短的一个函数:

softmax(x_i) = exp(x_i) / sum(exp(x_j))

它的作用是:把任意实数列(可以有负数,可以很大,可以很小)变成一组具有合理解释的概率权重——每个分量的值在 0 到 1 之间,所有分量的和等于 1。

这个性质正好被用来作为注意力的配分——"我应该给源句子的第一个词多少注意力?给第二个词多少?……"经过 softmax 的分数序列,就是模型自动学出的注意量。


23.6 注意力如何解决 Seq2Seq 的长句退化

回到 Seq2Seq。Attention 的修补方式本身是一个漂亮的软件工程结论——不重建整个架构,而是在解码器和编码器之间加一层:

  • 编码器仍然处理输入句子的每个词,产生每个位置的隐藏状态——但现在,所有位置的隐藏状态都被保留,而不是只在最后一个位置被取用。
  • 解码器在每一个生成步,都会拿自己当前的隐藏状态作为 Q,去和所有编码器位置的 K 做 Attention——得到一个针对当前步特有的上下文向量
  • 解码器用这个动态的上下文来辅助当前步的输出生成,而不是只依赖于一个固定的初始上下文。

效果极其显著。在 Bahdanau 等人的原始实验中,相比固定上下文的 Seq2Seq,加 Attention 的长句 BLEU 得分从快速退化变成与短句几乎持平。解码器学会了在自己需要的时候回看输入的相关片段,而不是"从头到尾靠记忆硬扛"——模型实现了某种程度上的自动对齐,直接跳过了人工做词对齐的时代。


23.7 最小代码:20 行 PyTorch 实现单头 Attention

python
import torchimport torch.nn.functional as Fdef scaled_dot_product_attention(Q, K, V, mask=None):    """    Q: (batch, ..., d_k)   - 查询    K: (batch, seq_len, d_k) - 键    V: (batch, seq_len, d_v) - 值    返回: (batch, ..., d_v)    """    d_k = Q.size(-1)    # 1. 得分: Q 和每一个 K 做点积    scores = torch.matmul(Q, K.transpose(-2, -1)) / (d_k ** 0.5)    # 2. 可选掩码(未来词/填充位不可见)    if mask is not None:        scores = scores.masked_fill(mask == 0, float('-inf'))    # 3. softmax → 注意力权重    attn_weights = F.softmax(scores, dim=-1)    # 4. 权重 × V → 上下文    output = torch.matmul(attn_weights, V)    return output, attn_weights

这就 10 行。完整的单头 Attention 就在这 10 行里——点积得分、√d 缩放、mask(如果有的话)、softmax、加权平均。剩下的只是如何生成 Q、K、V 三个投影矩阵——那是可学习的线性层。


23.8 本章小实验:手算注意力权重

用很简单的数字。

输入句子有三个词。编码器为这三个词分别给出了 K 表示:

K₁ = [1, 0]K₂ = [1, 1]K₃ = [0, 1]

当前解码器的 Q = [1, 0]。

第一步:算得分。Q 和各个 K 做点积:

s₁ = 1×1 + 0×0 = 1s₂ = 1×1 + 0×1 = 1s₃ = 1×0 + 0×1 = 0

第二步:除以 √d_k = √2 ≈ 1.414。(在向量维度很小时,缩放效果不明显,这里演示归一化流程)

第三步:softmax。

e^1 : e^1 : e^0 ≈ 2.718 : 2.718 : 1.000→ 权重 = [0.42, 0.42, 0.15]

模型认为前两个词和当前 Q 最相关(42% 注意力各),第三个词不太重要(15%)。

如果三个位置的 V 是:

V₁ = [0.9, 0.1]  (这个词和"动物"强相关)V₂ = [0.5, 0.5]  (中性)V₃ = [0.1, 0.9]  (和"地点"强相关)

加权和:0.42×V₁ + 0.42×V₂ + 0.15×V₃ = [0.61, 0.39]。

上下文更多携带了"动物"相关特征——这正是 Q 在当前步应该关注的。


23.9 本章地图

text
问题:模型如何在长句子中找到相关信息?方法:Attention——让解码器在每个生成步直接访问编码器的所有位置,通过 Query-Key-Value 匹配计算动态加权上下文。QKV 框架:Q(当前需求)× K(各位置内容)→ 相似度得分 → softmax → 权重 → 加权 V → 上下文。突破:解决了 Seq2Seq 固定上下文向量的瓶颈——长句翻译的 BLEU 不再随着输入长度加长而崩坏。局限:本章只讲了 Encoder-Decoder Attention——解码器看编码器。下一章 Transformer 引入的 Self-Attention 是更彻底地在同一个序列内部让每个词关注所有其他词。今天:Attention 的 QKV 框架是 Transformer 的核心操作——所有大语言模型的计算都被概括为"对序列中的每个位置做一个动态的加权平均"。

23.10 本章结语:回头看,这是 2014-2017 年间最重要的进化

在 Attention 出现之前,处理序列信息只能沿着 RNN 的线性路径——靠隐状态把信息从上一个时刻推到下一个时刻。梯度消失、计算无法并行、长距离依赖被时间步的链结构束缚。

Attention 打开了一种新的可能性:不需要单向的时间步链条。 序列中任意两个词之间可以建立直接的、可微的连接——不管它们之间隔了多少个词。

这个思想在短短三年内直接催生了一个全新的架构——不需要 RNN,纯粹用 Attention 构建的模型。它叫 Transformer。

下一章,我们搭建 Transformer——大模型的发动机。

SECTION §02 · ENGAGE

Discussion

留言区 · GitHub-powered comments via Giscus