7. 解码器层(DecoderLayer):Transformer的“目标序列生成器”
解码器层是Transformer生成目标序列的核心单元——整个解码器由N个完全相同的解码器层堆叠而成(原论文N=6),它的任务是:接收目标序列(如翻译任务中的中文句子)和编码器输出的上下文特征(memory),通过“自约束+跨交互+细加工”逐步生成高质量的目标序列特征,最终传给线性层预测每个位置的词。
相比于编码器层(2个子层),解码器层多了1个子层——这是因为它要处理两个关键关系:目标序列内部的依赖(需防“偷看”) 和 目标序列与输入序列的关联(需“对齐”)。
一、解码器层的“定位与核心差异”
先明确解码器层在整个Transformer中的位置和作用:
「目标序列→嵌入层+位置编码→解码器层1→解码器层2→…→解码器层6→线性层→预测词」
同时,每个解码器层还需要接收编码器的输出(memory) ——这是解码器与编码器交互的唯一途径。
解码器层与编码器层的核心差异:
对比维度 | 编码器层(EncoderLayer) | 解码器层(DecoderLayer) |
---|---|---|
子层数量 | 2个(自注意力+前馈网络) | 3个(掩码自注意力+交叉注意力+前馈网络) |
注意力类型 | 仅自注意力(无掩码) | 2种(掩码自注意力+交叉注意力) |
依赖外部输入 | 无(仅依赖自身输入x) | 有(依赖编码器的memory) |
核心任务 | 提取输入序列特征 | 生成目标序列特征(需对齐输入) |
二、代码逐行解析:从初始化到前向传播
class DecoderLayer(nn.Module):def __init__(self, d_model, self_atten, cross_atten, feed_forward, dropout):super(DecoderLayer, self).__init__()# 1. 接收3个核心模块实例(适配3个子层)self.self_atten = self_atten # 👉 掩码自注意力(防止偷看未来词)self.cross_atten = cross_atten # 👉 交叉注意力(连接编码器与解码器)self.feed_forward = feed_forward # 👉 前馈网络(局部特征加工)# 2. 3个AddNorm模块:对应3个子层,确保每层训练稳定(防梯度消失+稳分布)self.sublayer = nn.ModuleList([AddNorm(d_model, dropout), # 对应「掩码自注意力子层」AddNorm(d_model, dropout), # 对应「交叉注意力子层」AddNorm(d_model, dropout) # 对应「前馈网络子层」])
1. __init__方法关键参数解读
参数名 | 作用说明 |
---|---|
d_model | 词向量维度(如512),确保所有模块维度一致,数据顺畅流动 |
self_atten | 多头自注意力实例(同编码器的MultiHeadAttention),但需传入未来掩码,防止偷看 |
cross_atten | 多头注意力实例(同MultiHeadAttention),用于“解码器→编码器”的交叉交互 |
feed_forward | 前馈网络实例(同EncoderLayer的FeedForward),逐位置加工特征 |
dropout | Dropout概率(如0.1),传给AddNorm防止过拟合 |
⚠️ 注意点1:为什么需要两个注意力模块?
self_atten
:处理目标序列内部的依赖(如“猫吃鱼”中“吃”与“猫”“鱼”的关系),但必须加“未来掩码”,避免生成时“偷看”后面的词;cross_atten
:处理解码器与编码器的跨序列依赖(如“吃”对应输入序列的“eats”),是两者信息交互的唯一通道。
def forward(self, x, memory, src_mask, tag_mask):# x:解码器输入(目标序列的嵌入+位置编码,或上一层解码器输出),形状[batch_size, tag_seq_len, d_model]# memory:编码器输出的上下文特征,形状[batch_size, src_seq_len, d_model]# src_mask:源序列掩码(遮挡输入序列的<PAD>),形状[batch_size, 1, 1, src_seq_len]# tag_mask:目标序列掩码(遮挡目标序列的<PAD> + 未来词),形状[batch_size, 1, tag_seq_len, tag_seq_len]# 子层1:掩码自注意力(处理目标序列内部依赖,防偷看未来词)x = self.sublayer[0](x, lambda y: self.self_atten(y, y, y, tag_mask))# 子层2:交叉注意力(连接编码器,对齐输入与目标序列)x = self.sublayer[1](x, lambda y: self.cross_atten(y, memory, memory, src_mask))# 子层3:前馈网络(局部特征细加工)x = self.sublayer[2](x, lambda y: self.feed_forward(y))return x # 输出形状不变:[batch_size, tag_seq_len, d_model],特征更适配目标序列生成
2. forward方法核心逻辑拆解(结合翻译例子)
我们用英语→中文翻译任务举例(输入序列:“cat eats fish”,目标序列:“猫吃鱼”),逐个解释每个步骤:
(1)先明确4个输入的作用(小白必懂)
输入参数 | 具体例子(batch_size=1) | 核心作用 |
---|---|---|
x | 目标序列“猫吃鱼”的嵌入+位置编码,形状[1,3,512] | 解码器当前要处理的目标序列特征 |
memory | 编码器处理“cat eats fish”后的输出,形状[1,3,512] | 输入序列的上下文特征,供解码器对齐使用 |
src_mask | 若输入无PAD,则全1,形状[1,1,1,3] | 遮挡输入序列的,避免交叉注意力关注无效词 |
tag_mask | 未来掩码+PAD掩码,形状[1,1,3,3](下三角全1,上三角0) | ① 遮挡目标序列的;② 防止生成时偷看未来词 |
(2)子层1:掩码自注意力——“目标序列自己关注自己,但不能偷看”
这是解码器层最特殊的子层,核心是**“掩码”**,解决“自回归生成”的关键问题:
- 自回归生成:解码器生成目标序列时,是“逐词生成”的(先生成“猫”,再生成“吃”,最后生成“鱼”),生成第i个词时,只能用前i-1个词的信息,不能“偷看”第i个及以后的词。
📌 重点:掩码如何工作?
以目标序列“猫吃鱼”(长度3)为例,tag_mask
的形状是[1,1,3,3],值为:
[[[[1, 0, 0], # 生成“猫”(第0个词)时,只能看自己(第0个),不能看“吃”“鱼”[1, 1, 0], # 生成“吃”(第1个词)时,能看“猫”“吃”,不能看“鱼”[1, 1, 1]]]]# 生成“鱼”(第2个词)时,能看所有前词
当计算注意力分数时,tag_mask
中为0的位置会被设为负无穷,softmax后权重为0,相当于“完全不关注”。
同时,self.self_atten(y, y, y, tag_mask)
中query=key=value=y
(y是归一化后的x),说明是“目标序列自注意力”,但通过tag_mask
限制了关注范围。
lambda函数的作用和编码器层一致:适配AddNorm的接口,将“归一化后的y”传入自注意力,并带上tag_mask
。
(3)子层2:交叉注意力——“解码器向编码器‘提问’,找对应关系”
这是解码器与编码器“沟通”的唯一桥梁,核心是**“query来自解码器,key/value来自编码器”**,实现“目标序列与输入序列的对齐”。
用翻译例子解释:
- query:解码器当前的特征y(归一化后的x,如“吃”的向量)——相当于“解码器的提问:我现在处理‘吃’,输入序列里哪个词和我对应?”;
- key:编码器的memory(如“eats”的向量)——相当于“编码器的回答候选:这是输入里的‘eats’,你看看是不是对应?”;
- value:编码器的memory(和key相同)——相当于“编码器的详细信息:如果‘吃’对应‘eats’,就用我的向量来更新你的特征”;
- src_mask:遮挡输入序列的(如输入无PAD,全1)——确保解码器只关注输入序列中的有效词(“cat”“eats”“fish”)。
通过交叉注意力,“吃”的向量会被更新为“融合了‘eats’上下文的特征”,实现“输入与目标的对齐”——这就是翻译任务中“词对齐”的核心原理。
(4)子层3:前馈网络——“细加工对齐后的特征”
和编码器层的前馈网络功能完全一致:逐位置独立处理每个词的向量,通过“升维(512→2048)→ReLU激活→降维(2048→512)”提炼细节特征。
比如经过交叉注意力后,“吃”的向量已经对齐了“eats”的信息,前馈网络会进一步加工这个向量,提取更细的特征(如“‘吃’是动词,对应输入的‘eats’,主语是‘猫’,宾语是‘鱼’”),为后续预测词做准备。
三、关键注意点(换色突出,小白必记)
1. 两个掩码的区别:不要混淆src_mask和tag_mask!
掩码类型 | 作用范围 | 遮挡内容 | 形状示例(seq_len=3) |
---|---|---|---|
src_mask | 源序列(输入) | 仅遮挡符号 | [1,1,1,3](全1,无PAD时) |
tag_mask | 目标序列(输出) | ① 遮挡;② 遮挡未来词 | [1,1,3,3](下三角1,上三角0) |
核心目的 | 保护输入有效信息 | ① 保护目标有效信息;② 保证自回归 | —— |
2. 三个子层的顺序不能乱!必须是“掩码自注意力→交叉注意力→前馈网络”
顺序设计的逻辑:
- 先处理目标序列内部依赖(掩码自注意力):先搞清楚“猫”和“吃”的关系,再去对齐输入;
- 再对齐输入与目标的关系(交叉注意力):基于内部依赖的结果,找输入里对应的词,避免对齐混乱;
- 最后细加工特征(前馈网络):在对齐的基础上提炼细节,确保特征质量。
如果顺序颠倒(如先交叉注意力再掩码自注意力),会导致“目标序列内部关系没理清就去对齐输入”,生成的序列逻辑混乱(如翻译出“吃猫鱼”)。
3. 交叉注意力的query/key/value来源是固定的!
永远是:query=解码器特征(y),key=编码器memory,value=编码器memory。
这是因为:解码器需要“主动向编码器提问”(用自己的query找编码器的key),而不是编码器向解码器提问——如果key/value来自解码器,就失去了“对齐输入”的意义。
四、知识拓展:为什么解码器层也要堆叠N=6层?
和编码器层类似,单层解码器只能处理简单的依赖和对齐:
- 底层(1-2层):只能捕捉目标序列的局部依赖(如“猫”和“吃”),对齐输入的局部词(如“猫”对应“cat”);
- 中层(3-4层):能捕捉目标序列的短语级依赖(如“猫吃”),对齐输入的短语(如“猫吃”对应“cat eats”);
- 高层(5-6层):能捕捉目标序列的全局依赖(如“猫吃鱼”的整体逻辑),对齐输入的全局语义(如“猫吃鱼”对应“cat eats fish”)。
N=6层是“生成质量”和“计算效率”的平衡——层数太少,生成的序列可能逻辑混乱(如“鱼吃猫”);层数太多,训练慢且容易过拟合。
五、总结:解码器层的核心价值
解码器层通过“掩码自注意力(约束自身)+ 交叉注意力(对齐输入)+ 前馈网络(加工特征) ”的三步流程,实现了“在不偷看未来词的前提下,结合输入序列信息,逐步生成高质量目标序列特征”的核心任务。
用一句话记住:解码器层=“自约束”(掩码自注意力)+“跨对齐”(交叉注意力)+“细加工”(前馈网络),堆叠起来就是Transformer的“目标序列生成引擎”。