import matplotlib.pyplot as plt
import torch
plt.rc('font', family='Microsoft YaHei')
%config InlineBackend.figure_format = 'retina'
print('PyTorch version:', torch.__version__)PyTorch version: 2.11.0+xpu
jshn9515
2026-04-09
2026-04-09
在今天回头看 Attention 时,我们很容易直接想到 Transformer、self-attention、multi-head attention。可如果把时间往前拨,attention 最初真正变得重要,并不是因为它一开始就被写成了 Q、K、V 的矩阵形式,而是因为它首先解决了一个非常具体、也非常现实的问题:Seq2Seq 模型里的固定长度瓶颈。
这也是 Bahdanau、Cho 和 Bengio 在经典论文 (Bahdanau et al. 2016) 里切入 attention 的方式。
在这篇论文里,作者关注的是神经机器翻译。我们知道,最早的 Seq2Seq 模型通常会先把源句编码成一个固定长度向量,再让解码器仅凭这个向量逐词生成目标句。这就会产生一个问题:
把整句压缩成单个向量,本身就可能构成性能瓶颈。
尤其当句子较长时,这个固定长度的向量必须同时容纳很多信息:词义、顺序、局部依赖、长距离关系、语法结构,等等。这件事本身就非常吃力。因为翻译并不是先读完整句,再仅凭一句摘要机械地输出结果,而是在生成每个目标词时,都可能需要参考输入序列中的不同部分。如果解码器自始至终只能依赖同一个固定长度的上下文向量,那么它就难以灵活地利用源句中这些细粒度的信息,从而影响生成质量。
于是,attention 最早的价值,是让解码器在生成每个词时,不必只依赖一个全局摘要,而是能够动态地回头看输入序列中不同的位置。这其实也是我们人类在做翻译时的自然习惯:我们在翻译一个句子时,并不是先把它完全读完,然后再输出结果,而是边读边翻译,随时回头参考输入中的不同部分。
这一节我们就先把这个最原始、也最重要的 attention 直觉讲清楚。
PyTorch version: 2.11.0+xpu
先看最基本的 encoder-decoder 翻译框架。给定源句:
\[ x_1, x_2, \dots, x_{T_x} \]
编码器先把整句读完,压缩成一个固定长度向量 \(c\);然后解码器基于这个 \(c\),逐步生成目标句:
\[ y_1, y_2, \dots, y_{T_y} \]
这个结构的问题在于,它把所有源句信息都塞进了同一个瓶子里。句子越长、依赖越复杂,这个瓶子就越容易装不下。这也就是为什么长句翻译质量往往不如短句的原因。
更重要的是,翻译本来就是一个逐步对齐的过程。人在翻译时,并不会先把整句压成一团,然后机械地往外吐词。相反,我们在翻译每个词时,往往会回头看源句中最相关的位置。例如,翻译主语时,可能更依赖源句前面的名词;翻译谓语时,可能更依赖源句中间的动作。
所以,真正的问题不是固定向量太小,而是:
解码器在每一步都被迫依赖同一个全局表示,而不能按需读取输入的不同部分。
那么,既然我们人类可以边读边翻译,那么有没有什么办法,让模型也能在生成每个词时,动态地回头看输入序列中不同的位置呢?这就是 Bahdanau attention,也是现代 attention 机制的雏形。
Bahdanau 等人的做法并不是完全抛弃 encoder-decoder,而是在它的基础上引入一种动态检索机制。编码器不再只输出一个固定向量,而是输出一串隐藏状态:
\[ h_1, h_2, \dots, h_{T_x} \]
然后,解码器在生成第 \(t\) 个目标词时,不再只看一个固定上下文,而是先根据当前解码器的隐藏状态 \(s_{t-1}\),去和所有编码器的隐藏状态 \(h_i\) 计算相关性分数:
\[ e_{t,i} = f(s_{t-1}, h_i) \]
这里的 \(f\) 是一个打分函数,用来衡量当前解码状态与源句第 \(i\) 个位置之间的相关程度。在 Bahdanau attention 里,它通常由一个小型前馈网络来实现;后来的现代 attention 则更常使用 Query 和 Key 的点积来打分。虽然形式不同,但核心思想是一致的:先判断当前该关注哪里,再去聚合这些位置上的信息。
把这些分数做 softmax,得到对源句各位置的注意力权重:
\[ \alpha_{t,i} = \text{softmax}(e_{t,i}) \]
最后,把编码器隐藏状态按这些权重加权求和,得到当前步专属的上下文向量:
\[ c_t = \sum_i \alpha_{t,i} h_i \]
到这里,attention 这一步还没有结束。得到 \(c_t\) 之后,解码器会再把这个当前步专属的上下文向量,与当前输入一起用于更新自己的状态,并生成第 \(t\) 个目标词。也就是说,attention 直接参与了当前这一步的生成过程。
从计算流程上看,我们可以把 Bahdanau attention 总结成下面四步:
这就是最早的 attention。不是提前把整句压成一个唯一摘要,而是在生成每个目标词时,动态地对源句做一次软检索,生成一个当前步专属的上下文向量 \(c_t\),让解码器可以按需参考输入的不同部分。
source_tokens = ['I', 'love', 'deep', 'learning']
target_tokens = ['我', '喜欢', '深度', '学习']
# An illustrative soft alignment matrix, not a training result
A = torch.tensor(
[
[0.55, 0.30, 0.10, 0.05],
[0.20, 0.45, 0.25, 0.10],
[0.05, 0.20, 0.50, 0.25],
[0.05, 0.10, 0.30, 0.55],
]
)
fig = plt.figure(1, figsize=(4, 3))
ax = fig.add_subplot(1, 1, 1)
im = ax.imshow(A, cmap='Blues')
ax.set_xticks(range(len(source_tokens)), source_tokens)
ax.set_yticks(range(len(target_tokens)), target_tokens)
ax.set_xlabel('source position')
ax.set_ylabel('target position')
ax.set_title('Illustrative Attention Alignment')
fig.colorbar(im)
plt.show()
上面这个矩阵只是 attention alignment 的一个示例。它想表达的是:在生成不同目标词时,模型会对源句中不同位置分配不同的权重。真实训练得到的 attention 往往不会这么整齐,也不一定是一一对应的。
因此,这个机制经常被描述为一种 软对齐(soft alignment)。它不像传统对齐那样,把当前目标词硬性对应到某个源位置;相反,模型会对多个源位置分配连续权重,并按照这些权重聚合信息。于是,模型在生成当前词时,可以同时参考多个位置的表示,只是关注程度有所不同。
“soft”这个词在这里很重要。它表明这些注意力权重是连续且可微的,因此整个对齐过程可以直接纳入神经网络,通过反向传播与翻译目标一起优化,而不需要额外提供离散的对齐标签。这也正是论文标题里“jointly learning to align and translate”的含义:模型不仅在学习如何翻译,也在学习生成当前词时应该关注源句中的哪些位置。有了 attention,对齐不再是系统外部的预处理步骤,而是端到端训练的一部分。
从今天回头看,这样的设计似乎顺理成章;但在当时,它其实体现了一次非常重要的观念转变。
看到这里,我们可以进一步问一个问题:Bahdanau attention 到底只是 Seq2Seq 里的一个技巧,还是已经包含了更一般的 attention 思想?
从它最初的提出背景来看,它显然还没有脱离 encoder-decoder 框架。它面对的仍然是机器翻译中的那个具体问题:当解码器生成当前目标词时,应该去源句的哪些位置寻找最相关的信息?
但如果用今天更抽象的视角来理解,这个过程其实已经和后来的 Query、Key、Value 框架很接近了:
所以,Bahdanau attention 虽然还没有被写成今天统一的 Q、K、V 形式,但它已经把 attention 最核心的机制表达出来了:给定当前需求,在一组候选表示中找到更相关的信息,再按权重把这些信息聚合起来。
如果借用今天的术语来描述,这种结构其实更接近后来的 cross-attention。因为 Query 来自解码器,而 Key / Value 来自编码器,三者并不完全来自同一组表示。再往后,当 Query、Key、Value 都来自同一个序列时,self-attention 的形式才会自然出现。
也正是在这个抽象过程中,attention 才逐渐从翻译里的软对齐机制,发展成一个更一般的表示学习框架。
这一节里,我们先回到了 attention 的历史起点,也就是 attention-based Seq2Seq。它的思路可以大概总结成下面几点:
从今天的角度往回看,Bahdanau attention 虽然仍然属于 encoder-decoder,但已经可以和后来的 Query、Key、Value 框架形成清晰类比。接下来我们就沿着这条线,进入更一般的 attention 表述。