8.1 Bahdanau Attention:从信息压缩到动态检索

Author

jshn9515

Published

2026-04-09

Modified

2026-04-11

在前面的序列模型里,我们已经见过一种很自然的思路:先用一个编码器读完整个输入序列,把它总结成一个向量,然后再让解码器根据这个向量一步一步生成输出。以机器翻译为例,编码器读入源语言句子,最后得到一个上下文向量;解码器再根据这个上下文向量生成目标语言句子。

这种做法看起来很合理。既然输入是一句话,那我们把整句话压缩成一个表示,再用这个表示去生成另一句话,似乎就是一个标准的“编码-解码”过程。

但问题也正出在这里:整个输入序列的信息都必须被压进一个固定长度的向量里。

对于短句子来说,这个问题可能还不明显。可是当句子变长以后,固定长度向量就很容易成为信息瓶颈。编码器最后的隐藏状态既要记住句子的整体语义,又要保留局部词语、语法结构和长距离依赖。我们强迫编码器把所有信息都挤在一个向量里,导致解码器在生成每个目标词时都只能依赖同一个压缩表示。

这就像我们让一个人先读完整篇文章,然后只能用一句话概括出来,之后别人再根据这句话逐字还原原文。对于非常短的句子,这也许还可以;但对于长句子,很多细节必然会丢失。

Bahdanau attention 的出发点,就是为了解决这个固定长度上下文向量带来的瓶颈。

8.1.1 固定长度上下文向量的瓶颈

先看传统 seq2seq 的结构。

假设待翻译的句子有 \(T_x\) 个 token,编码器依次读入这些 token,并得到一串隐藏状态:

\[ h_1, h_2, \dots, h_{T_x} \]

在最简单的 seq2seq 模型中,编码器通常只把最后一个隐藏状态作为整个输入句子的表示:

\[ c = h_{T_x} \]

这里的 \(c\) 就是上下文向量(context vector)。解码器之后生成每一个目标 token 时,都依赖这个相同的 \(c\)。也就是说,不管当前要生成的是目标句子的第一个词,还是第十个词,解码器看到的都是同一个固定向量。

这会带来两个问题。

第一个问题是信息压缩。源句越长,越难把所有信息都压进一个固定长度向量里。即使 LSTM、GRU 这类模型比普通 RNN 更擅长保留长期信息,这个瓶颈仍然存在。

第二个问题是缺少动态性。翻译一句话时,生成不同目标词通常需要关注源句中的不同部分。比如生成一个动词时,模型可能需要看源句里的谓语;生成一个名词时,模型可能需要看源句里的某个人、地点或东西。可是传统 seq2seq 给解码器的上下文向量是固定的,它没有办法在不同生成时刻灵活地选择现在更应该看哪里。

那么,如果让我们来做翻译,我们会怎么做呢?

我们肯定不会先读完整句话,然后只记住这句话的一个总结,再只根据这个总结去写翻译。我们会在读句子的过程中,记住每个词的含义和位置信息。当我们开始翻译时,我们会动态地回过头去看看源句的不同位置,选择最相关的信息来帮助决定下一个词是什么。

所以,一个更自然的想法就是:模仿人类的行为,不要把整句话一次性压成一个向量,而是保留编码器在每个位置上的隐藏状态。解码器每生成一个词,就根据当前需要,从这些隐藏状态里动态地取信息。

这就是 Bahdanau attention 的核心思想。

8.1.2 Bahdanau Attention:生成时再决定看哪里

Bahdanau attention 的关键变化是:编码器不再只给解码器一个固定的上下文向量,而是把所有时间步的隐藏状态都保留下来。

\[ h_1, h_2, \dots, h_{T_x} \]

这些隐藏状态可以理解为源句中每个位置的上下文表示。注意,它们不是简单的词向量,而是编码器读到对应位置后得到的隐藏状态,因此已经包含了一定的上下文信息。

然后,在解码器生成第 \(t\) 个目标词时,模型不会直接使用同一个固定的 \(c\),而是为当前时刻重新计算一个上下文向量 \(c_t\)。这个 \(c_t\) 不是凭空来的,而是编码器所有隐藏状态的加权和:

\[ c_t = \sum_{i=1}^{T_x} \alpha_{t,i} h_i \]

这里的 \(\alpha_{t,i}\) 代表当解码器生成第 \(t\) 个目标词时,它应该把多少注意力分配给源句的第 \(i\) 个位置。例如,如果 \(\alpha_{t,3}\) 很大,就说明模型在当前生成时刻更关注源句第 3 个位置;如果 \(\alpha_{t,7}\) 很小,就说明源句第 7 个位置对当前生成不太重要。

这样一来,解码器每生成一个词,都会得到一个专门为当前时刻计算出来的上下文向量:

\[ c_1, c_2, \dots, c_{T_y} \]

这和传统 seq2seq 只有一个固定的 \(c\) 很不一样。Attention 让上下文向量变成了动态的:

当前要生成什么,就决定当前要看哪里。

这也是 attention 最重要的直觉。它不是要求模型在编码阶段一次性记住所有信息,而是允许模型在解码过程中反复回看源句,并根据当前生成需求选择不同的信息。

8.1.3 注意力权重从哪里来?

有了 attention 的直觉,现在的问题就变成了:这些权重 \(\alpha_{t,i}\) 是怎么得到的?

在生成第 \(t\) 个目标词时,解码器会有一个当前状态。这个状态可以理解为模型当前的生成需求:它已经生成了前面的词,现在需要决定下一个词应该是什么。

假设解码器当前的隐藏状态为 \(s_{t-1}\),编码器第 \(i\) 个位置的隐藏状态为 \(h_i\)。一种想法是,用一个打分函数来衡量它们之间的相关性:

\[ e_{t,i} = a(s_{t-1}, h_i) \]

这里的 \(e_{t,i}\) 是一个未归一化的注意力分数。它表示在生成第 \(t\) 个目标词时,源句第 \(i\) 个位置和当前解码状态有多相关。

Bahdanau attention 使用的是一个小型前馈神经网络来计算这个分数:

\[ e_{t,i} = v_a^\top \tanh(W_a s_{t-1} + U_a h_i) \]

不用太纠结这个公式的来源。直观地说,它做的事情就是:把当前解码状态 \(s_{t-1}\) 和某个编码器隐藏状态 \(h_i\) 放到一起,经过一个可学习的函数,输出一个相关性分数。

然后,对所有源句位置的分数做 softmax,得到注意力权重:

\[ \alpha_{t,i} = \frac{\exp(e_{t,i})}{\sum_{j=1}^{T_x} \exp(e_{t,j})} \]

最后,模型用这些权重对所有编码器隐藏状态加权求和,得到当前时刻的上下文向量:

\[ c_t = \sum_{i=1}^{T_x} \alpha_{t,i} h_i \]

所以,Bahdanau attention 的计算过程可以概括成三步:

  1. 用当前解码状态和每个编码器隐藏状态计算相关性分数。
  2. 对这些分数做 softmax,得到注意力权重。
  3. 用注意力权重对编码器隐藏状态加权求和,得到当前时刻的上下文向量。

这个过程本质上就是一次动态的信息检索。当前解码状态提出一个需求,编码器的所有隐藏状态作为候选信息,attention 根据相关性分配权重,然后把最相关的信息聚合回来。

8.1.4 为什么叫软对齐?

Bahdanau attention 最早是在机器翻译中提出的。机器翻译里有一个非常自然的问题:目标语言中的某个词,通常会对应源语言中的某个词或某几个词。比如英文句子翻译成中文时,生成某个中文词时,模型可能主要参考英文句子里的某几个单词。传统机器翻译里,这类对应关系通常叫做对齐(alignment)。

Attention 的权重 \(\alpha_{t,i}\) 正好可以被看作一种对齐关系。

对于目标句子的第 \(t\) 个位置,\(\alpha_{t,i}\) 表示它和源句第 \(i\) 个位置之间的关联程度。如果我们把所有 \(\alpha_{t,i}\) 画成一个矩阵,就会得到一个类似对齐图的东西:每一行对应一个目标词,每一列对应一个源词,颜色越深表示注意力权重越大。

但这种对齐不是硬性的。硬对齐会说,当前目标词只对应源句中的某一个位置,比如第 3 个目标词只对齐第 5 个源词。而 Bahdanau attention 做的是软对齐。它不会强制模型只选择一个位置,而是允许模型对多个源位置分配连续权重:

\[ \alpha_{t,1}, \alpha_{t,2}, \dots, \alpha_{t,T_x} \]

这些权重加起来等于 1,但每个位置都可以有一定贡献。也就是说,当前目标词可以主要参考某个源词,同时也少量参考其他相关位置。这就是“软”的含义。

软对齐的好处是,它是连续的、可微的。模型不需要额外的标注告诉它哪个词应该对齐哪个词,也不需要先训练一个独立的对齐模型。它可以在翻译任务的训练过程中,通过反向传播自动学出这种对齐关系。

换句话说,模型不是先学会对齐,再学会翻译;而是在学习翻译的过程中,顺便学会了生成当前词时应该看哪里。这也是 Bahdanau 论文标题里 “jointly learning to align and translate” 的含义:对齐和翻译是一起学出来的。

8.1.5 Bahdanau Attention 改变了什么?

Bahdanau attention 并不是简单地给 seq2seq 加了一个小模块,它改变的是编码器和解码器之间传递信息的方式。

在传统 seq2seq 里,编码器和解码器之间只有一个固定长度的通道。编码器必须把所有输入信息压进一个向量,解码器之后只能依赖这个向量生成完整输出。

在加入 attention 之后,编码器和解码器之间的通信变得更加灵活。编码器把每个位置的隐藏状态都保留下来,解码器在每个生成时刻都可以重新查看这些隐藏状态,并根据当前需要动态地聚合信息。

因此,attention 至少带来了三个重要变化。

首先,它缓解了固定长度向量的信息瓶颈。源句的信息不再只通过最后一个隐藏状态传给解码器,而是通过所有编码器隐藏状态共同提供。其次,它让上下文向量变成了可以动态检索的。不同目标词可以对应不同的 \(c_t\),模型可以在生成不同词时关注源句的不同部分。最后,它提供了一种可解释的中间结构。虽然 attention 权重不应该被简单等同于完整解释,但在机器翻译场景下,它确实可以展示模型在生成某个目标词时更关注源句中的哪些位置。

从这个角度看,attention 的核心不是某个具体公式,而是一种思想:

不要提前把所有信息压成一个固定表示,而是在需要的时候,根据当前任务动态检索信息。

这个思想后来被不断推广。最开始,它主要用于 RNN seq2seq 中的编码器-解码器架构;后来,它发展成更通用的 query、key、value 形式;再后来,Self-Attention 进一步把这种动态信息检索机制应用到同一个序列内部,最终成为 Transformer 的核心组件。

8.1.6 Bahdanau Attention 和现代 Attention 的关系

从今天的角度看,Bahdanau attention 可以被理解为现代 attention 的一个早期版本。

在现代 attention 里,我们经常使用 query、key、value 这三个概念:query 表示当前想找什么,key 表示每个候选项用什么来匹配,value 表示真正要取回的信息。

如果用这个视角回看 Bahdanau attention,那么解码器的隐藏状态 \(s_{t-1}\) 就类似于 query,编码器的隐藏状态 \(h_i\) 同时扮演了 key 和 value 的角色。

它像 key,是因为模型用 \(h_i\)\(s_{t-1}\) 计算相关性分数:

\[ e_{t,i} = a(s_{t-1}, h_i) \]

它也像 value,是因为最后被加权求和、真正传给解码器的也是这些 \(h_i\)

\[ c_t = \sum_{i=1}^{T_x} \alpha_{t,i} h_i \]

也就是说,在 Bahdanau attention 里,匹配用的信息和取回的信息还没有明确分开。到了后来的现代 attention 中,模型通常会通过不同的线性变换显式得到 \(Q\)\(K\)\(V\),从而让“用什么匹配”和“取回什么内容”变成两个可以分别学习的表示空间。不过,核心思想并没有变:模型先根据当前需求和候选信息计算相关性,再用相关性权重对信息进行加权聚合。

这也是为什么 Bahdanau attention 很适合作为理解 Transformer 的起点。它让我们先在一个具体的 seq2seq 翻译场景里看清楚 attention 要解决什么问题:固定长度表示太死板,生成过程需要动态地回看输入。

一旦理解了这一点,后面的 cross-attention、self-attention 和 multi-head attention 就不会显得那么突然。它们本质上都是在回答同一个问题:

当模型处理一个位置时,它应该如何从一组候选信息中,动态地找出当前最相关的部分?

8.1.7 本章小结

这一节我们从传统 seq2seq 的固定长度上下文向量出发,介绍了 Bahdanau attention 的核心思想。

传统 seq2seq 把整个源句压缩成一个固定向量,这会造成信息瓶颈,也让解码器在生成不同目标词时缺少动态选择信息的能力。Bahdanau attention 选择保留编码器所有时间步的隐藏状态,让解码器在每个生成时刻都重新计算注意力权重,并根据这些权重得到当前专属的上下文向量。

从直觉上说,attention 就是在生成时动态决定现在应该看哪里。从机器翻译角度看,它又可以被理解为一种软对齐:模型不再硬性选择某一个源词,而是对所有源位置分配连续权重,并通过端到端训练自动学出这种对齐关系。

这一节的重点不是记住某个具体打分函数,而是理解 attention 解决的问题和带来的变化。它让模型从把输入一次性压成一个表示,转向在需要时动态检索相关信息。下一节我们会把这个思想进一步抽象成更现代的形式,讨论 cross-attention 和 self-attention,并正式引入 query、key、value 这套表示方式。