观文听傑

返回

上一篇把长短期记忆网络(Long Short-Term Memory, LSTM)的保留、写入和遗忘拆成了可学习的门。它让较早的信息更容易到达序列末尾,却仍留下一个容量问题:若机器翻译编码器只把整句源文本交给最后一个状态,十几个实体、修饰关系和词序都要竞争同一个定长向量。

编码器—解码器(Encoder–Decoder)先把输入变成表示,再逐步生成输出。本文只研究它的一项关键改造:Bahdanau 加性注意力(Additive Attention)如何让第 tt 个解码步直接读取全部源位置,而不是每一步都依赖编码器最后一个状态。我们会追踪打分、padding 掩码、softmax 和加权和的完整数据流;多头自注意力留到下一篇。

01 定长上下文究竟丢了什么?#

考虑把「黑猫坐在红垫子上」翻译成英文。无注意力的循环 Encoder–Decoder 可以写成:

hsenc=GRUenc(xs,hs1enc),c=hSench_s^{enc}=\operatorname{GRU}_{enc}(x_s,h_{s-1}^{enc}), \qquad c=h_S^{enc} st=GRUdec([yt1;c],st1),p(yt)=softmax(Wost+bo)s_t=\operatorname{GRU}_{dec}([y_{t-1};c],s_{t-1}), \qquad p(y_t)=\operatorname{softmax}(W_os_t+b_o)

源长度为 SS,目标长度为 TT。不论 S=5S=5 还是 S=500S=500,解码器始终只收到同一个 cRHc\in\mathbb{R}^{H}

源词: 黑猫 ─► 坐在 ─► 红色 ─► 垫子 ─► 上
        enc     enc      enc      enc     enc


                                  c = 最后状态 [N,H]
                                           │ 每一步重复使用同一个 c

目标: <bos> ─► the ─► black ─► cat ─► ... ─► <eos>
text

这不是说最后状态必然“一无所知”,而是所有细节只能通过同一条压缩通道。长句增长时,早期位置到 cc 的路径更长;解码到「mat」时,也无法显式回到源词「垫子」重新取证。

注意力把接口从单个 cc 改为全部编码器状态:

Henc=[h1enc,,hSenc]RN×S×HeH^{enc}=[h_1^{enc},\ldots,h_S^{enc}]\in\mathbb{R}^{N\times S\times H_e}

每个解码步生成自己的上下文 ctc_t。于是容量仍有限,但不再要求一个向量提前猜中所有未来解码步需要的细节。

02 加性注意力怎样回答“现在该看哪里”?#

tt 步已有上一解码状态 st1RN×Hds_{t-1}\in\mathbb{R}^{N\times H_d}。对每个源位置 jj,先计算未归一化能量(Energy):

et,j=vatanh(Whhjenc+Wsst1+ba)e_{t,j}=v_a^\top\tanh(W_hh_j^{enc}+W_ss_{t-1}+b_a)

其中:

变量形状含义
hjench_j^{enc}[N,H_e]jj 个源位置的编码表示
st1s_{t-1}[N,H_d]当前查询所依据的上一解码状态
WhW_h[A,H_e]把源表示投影到对齐空间
WsW_s[A,H_d]把解码状态投影到同一空间
vav_a[A]把对齐特征压成一个标量分数
ete_t[N,S]当前解码步对所有源位置的能量

AA 是注意力内部维度。加性注意力不要求 He=HdH_e=H_d;两个投影负责把不同宽度送入同一空间。

对 padding 位置加 -\infty 后,沿源长度维归一化:

αt,j=exp(et,j)k=1Sexp(et,k),ct=j=1Sαt,jhjenc\alpha_{t,j}=\frac{\exp(e_{t,j})}{\sum_{k=1}^{S}\exp(e_{t,k})}, \qquad c_t=\sum_{j=1}^{S}\alpha_{t,j}h_j^{enc}
encoder states H [N,S,H_e] ───────► W_h ───────────┐
                                                    ├─► tanh ─► v_a ─► energy [N,S]
decoder state s_(t-1) [N,H_d] ─► W_s ─► [N,1,A] ──┘

source_valid [N,S] ─► padding 处填 -inf ──────────────────────────┤

                                                         softmax(dim=source)
                                                                  │ alpha [N,S]

H [N,S,H_e] ──────────────────────────────────────────► 加权和 c_t [N,H_e]
text

能量不是概率;只有掩码后做 softmax 才得到总和为 1 的注意力权重。上下文 ctc_t 是源状态的凸组合,它的宽度仍是 HeH_e,不会因源长度变化。

03 用三个源位置手算一次“重新取证”#

为隔离注意力本体,假设当前解码步已经得到三个标量能量:

et=[0,ln2,ln3]e_t=[0,\ln2,\ln3]

源状态只有二维:

h1=[1,0],h2=[0,2],h3=[2,2]h_1=[1,0],\quad h_2=[0,2],\quad h_3=[2,2]

softmax 的分子为 [1,2,3][1,2,3],所以:

αt=[16,26,36]\alpha_t=\left[\frac16,\frac26,\frac36\right]

上下文为:

ct=16[1,0]+26[0,2]+36[2,2]=[76,53]c_t=\frac16[1,0]+\frac26[0,2]+\frac36[2,2] =\left[\frac76,\frac53\right]

若第三个位置是 padding,就必须在 softmax 之前 把其能量变为 -\infty

αtmasked=[13,23,0],ctmasked=[13,43]\alpha_t^{masked}=\left[\frac13,\frac23,0\right], \qquad c_t^{masked}=\left[\frac13,\frac43\right]

先 softmax 再把 padding 权重乘零会让剩余权重之和小于 1,改变上下文尺度;只把 padding 词向量设为零也不够,因为它仍会从 softmax 分走概率质量。

04 从源 token 到目标 token,数据怎样流?#

本文采用带注意力的循环序列到序列(Sequence-to-Sequence, Seq2Seq)模型:

双向编码器能在离线任务中让每个源位置同时包含左右上下文,所以 He=2HH_e=2H。严格流式输入不能使用反向分支;那是部署条件,不是一个可以被验证分数掩盖的小细节。

训练时,目标序列通常错开一位:

tgt:    <bos>   the   black   cat   <eos>   <pad>
输入:   <bos>   the   black   cat
标签:    the   black   cat   <eos>
text

教师强制(Teacher Forcing)指训练第 tt 步喂入真实的 yt1y_{t-1};推理没有真实未来目标,只能喂入模型上一步预测。这两条数据流必须分别测试。

05 不调用注意力封装,先写出本体#

decoder_state[:, None] 增加源位置轴,通过广播与 [N,S,A] 相加;torch.bmm 接收两个三维 batch 矩阵,[N,1,S] @ [N,S,H_e] 输出 [N,1,H_e]。这两处最容易“形状能广播,语义却错轴”。

06 一个可训练的注意力解码步#

下面只展示核心模块,不把分词、词表和数据管道藏在类里:

损失应直接接收 logits,并忽略目标 padding:

pad_id = 0
logits, alignments = decoder(
    target_input, initial_state, encoder_states, source_valid
)  # [N,T,V], [N,T,S]

loss = nn.functional.cross_entropy(
    logits.reshape(-1, logits.shape[-1]),
    target_output.reshape(-1),
    ignore_index=pad_id,
)
loss.backward()
nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
python

PyTorch 2.13 的 nn.Embedding(..., padding_idx=pad_id) 使该词表行不接收梯度;它不会自动生成源注意力掩码。CrossEntropyLoss(ignore_index=pad_id) 只忽略目标损失位置;它也不会替源端注意力屏蔽 padding。两端 padding 是两份不同契约。

07 推理为什么不能复制训练循环?#

贪心自回归推理从 <bos> 开始,每步选择一个 token,再作为下一步输入:

必须有 max_steps,否则没学会输出 <eos> 的模型会无限生成。batch 内较早结束的样本应冻结或屏蔽状态;上面的最小版本只冻结其 token,生产实现还应避免它的无效后续状态污染指标。束搜索(Beam Search)可以保留多个候选,但不会修复错误词表、掩码或训练—推理分布差异。

08 如何证明模型真的在使用源位置?#

注意力矩阵 alignments 的形状为 [N,T,S],适合画目标—源热力图,但“权重大”不自动等于“因果解释”。至少同时做三种检查:

  1. 数值不变量。 每个有效解码步沿 SS 求和应约等于 1,padding 权重应约等于 0,所有值应有限。
  2. 受控任务。 构造输入 [a,b,c]、目标 [c,b,a] 的反转任务;期望峰值随解码步从右向左移动。
  3. 干预。 删除或替换被认为关键的源 token,或强制屏蔽其位置,观察目标分布是否按任务语义变化。
反转任务的期望对齐:

          source
target      a    b    c
  c        .1   .1   .8
  b        .1   .8   .1
  a        .8   .1   .1
text

若注意力永远盯住最后一个源位置,先检查初始状态是否仍过强、mask 是否反了、源输出是否误用了最终状态广播,以及训练数据是否存在位置捷径。

09 最常见的“能训练,但对齐错了”#

  • softmax 轴写成目标维或 batch 维。 对每个解码步,权重必须沿源位置 SS 归一化。
  • 掩码语义反转。 本文 source_valid=True 表示可见,填 -inf 时使用 ~source_valid;变量名应写进断言。
  • softmax 后再清零 padding。 剩余权重不再归一;掩码必须进入 softmax 之前的能量。
  • 整条源序列全是 padding。 所有分数均为 -\infty 会产生 NaN;数据入口应拒绝零长度样本。
  • 把双向编码器宽度仍写成 HH 拼接方向后是 2H,初始解码状态和注意力投影都要显式适配。
  • 目标输入与标签没有错开。 把当前真实 token 同时作为输入和标签,会形成复制捷径。
  • 训练时总看真值,验证也看真值。 教师强制损失可能很好,自回归生成却会因误差累积崩溃;两种评估都要记录。
  • 在输出后手动 softmax 再送入交叉熵。 官方交叉熵要求未归一化 logits。
  • 把 attention heatmap 当作翻译质量。 对齐清晰不代表 token 正确,权重分散也不必然表示失败。
  • 逐步 Python 循环成为吞吐瓶颈。 循环注意力本身按目标步串行;生产前要用 profiler 区分模型限制与数据管道等待。

10 它与相近方法的边界在哪里?#

方法查询来自哪里键和值来自哪里主要用途
定长 Encoder–Decoder无逐步查询只有编码器末状态短序列基线、接口简单
加性注意力上一解码状态全部编码器状态不同隐宽度下学习非线性对齐
点积交叉注意力解码器表示编码器表示矩阵乘法高效,但通常要求投影到同宽度
自注意力同一序列位置同一序列位置在序列内部交换信息

本文的注意力仍嵌在循环解码器里:第 tt 步依赖第 t1t-1 步,不能并行生成目标。它也没有消除有限词表、暴露偏差、长度偏好和搜索误差。注意力解决的是“每一步能否回到源序列取证”,不是全部生成问题。

11 今天真正需要记住什么?#

  1. 定长 Encoder–Decoder 强迫所有源信息先进入一个末状态;加性注意力把接口改为全部源状态和逐步上下文 ctc_t
  2. 完整数据流是“能量打分 → 源 padding 掩码 → 沿源维 softmax → 对编码状态加权求和”,顺序不能交换。
  3. encoder_states [N,S,H_e]decoder_state [N,H_d]weights [N,S]context [N,H_e] 是最值得写进断言的契约。
  4. 教师强制训练与自回归推理读取不同的上一 token;验证只跑前者会掩盖误差累积。
  5. 注意力图是可诊断的中间变量,不是天然因果解释;应结合反转任务、padding 不变量和输入干预。

12 思考题与小练习#

  1. 将手算例的能量改为 [ln 4, ln 2, 0],分别计算无掩码和“第二位置为 padding”时的权重与上下文。解释为什么不能只把第二个源状态设为零。
  2. AdditiveAttention 写四个测试:不同 He/HdH_e/H_d、不同源长度、含 padding、全 padding。前三者验证权重和与 padding 权重,第四个验证入口断言。
  3. 在序列反转任务上比较定长 Encoder–Decoder 与加性注意力,将源长度从 5 增到 50;同时报告 token 准确率、完整序列准确率、注意力熵和每秒样本数。

相关工作#

13 下一篇预告#

加性注意力允许每个解码步重新读取源序列,但编码器与解码器仍沿时间逐步递推。下一篇将把“查询—键—值”写成矩阵乘法,解释 Transformer 的缩放点积自注意力如何让所有位置并行交换信息,以及位置编码、padding mask 与因果 mask 分别阻止什么错误。

整句话为何不能只压成一个向量?Encoder–Decoder 的加性注意力与对齐
https://zwjcode.cn/blog/seq2seq-additive-attention-alignment
作者
发布于 2026年9月6日
版权协议 CC BY-NC-SA 4.0
评论加载似乎遇到了问题,请尝试刷新页面。