交叉注意力与解码器

解码器一边回顾自己写过的词,一边盯着原文,拼出 Transformer 全图。

知识课 · 约 30 分钟 · 更新于 2026-08-14

写每个词,都得回头看原文

解码器已经会用掩码自注意力「回顾自己写过的词」了。但翻译 I love you 时,生成每个词都还得 盯着原文 :写到第二个词,该重点对齐 love 还是 you?

「该重点看谁」,这正是注意力一直在回答的问题。机制可以原样复用, 只是关注的对象变了:从「自己句子里的词」换成 编码器的输出 。

交叉注意力:Q 来自我,K、V 来自原文

交叉注意力(cross-attention,论文里也叫 encoder-decoder attention) 的全部改动就一句话:

Query 的个数由解码器正在生成的句子决定,Key、Value 的个数由原文决定,两者通常不等长。

还记得「多头注意力」里那个看似多余的设计吗? forward(query, key, value) 接收三个独立入参,原因就在这里:

一行都不用改 ,换个传参方式,注意力就「跨」起来了。

两种注意力对照着看:

组装解码器层

解码器层(DecoderLayer) 由三个子层串联,每个都套 Add & Norm,全部是已经实现过的模块:

两个注意力子层戴的掩码不同:自注意力用因果掩码( tgt_mask )挡住还没生成的词; 交叉注意力面对的是原文整句,不存在「未来」,因此 不戴因果掩码 ,传入的 src_mask 只用来屏蔽原文里的填充符。

堆叠 N 层、配上目标侧的词嵌入与位置编码,就是完整的解码器:

最后一步:把向量变成「下一个词」

解码器输出的还是 维向量,可我们要的是「下一个词是什么」。最后接一个线性层,把每个位置的向量映射成 词表上每个词的分数(logits)。序列长度记作 、词表大小记作 ,每个位置都得到 个分数,整句堆成一张 的表:

softmax 之后就是「下一个词」的概率分布。眼熟吗?这与「Softmax 回归」的多分类输出 完全同构 ,只不过「类别」从 10 个数字变成了词表里的几万个词。预测下一个词,本质上就是一次超多类别的分类。

这里把整句「我 爱 你」当成已知输入,三个位置一次性各预测了下一个词。这和前面「一词一词地写」看似矛盾:为什么所有位置能一起算?这个问题留到本课末尾点明,《训练与推理通路》中解答。

合体:完整的 Transformer

编码器、解码器、输出层三部分都实现完了。其中输出层就是上一节那个线性层,它不在解码器内部, 而是装在整个模型的顶端。把三者接到一起:

class Transformer(nn.Module): def __init__(self, src_vocab, tgt_vocab, d_model=512, n_layers=6, n_heads=8, d_ff=2048, dropout=0.1): super().__init__() self.encoder = Encoder(src_vocab, d_model, n_layers, n_heads, d_ff, dropout) self.decoder = Decoder(tgt_vocab, d_model, n_layers, n_heads, d_ff, dropout) self.out_proj = nn.Linear(d_model, tgt_vocab) # 输出层 def forward(self, src, tgt, src_mask=None, tgt_mask=None): enc_out = self.encoder(src, src_mask) dec_out = self.decoder(tgt, enc_out, src_mask, tgt_mask) return self.out_proj(dec_out) # (B, L_tgt, tgt_vocab) 的 logits

对照 Transformer 论文的架构图回顾一下:图上的每一个方块(Input Embedding、 Positional Encoding、Multi-Head Attention、Masked Multi-Head Attention、Add & Norm、Feed Forward、Linear、Softmax) 都是你亲手实现过的模块 , 这张架构图现在对应的正是你写过的代码。

架构齐了,两个问题没答

模型的整体结构现在完整了,但有两件事我们一直含糊着:

之后的《训练与推理通路》会把这两条通路走通。