6. 编码器层(EncoderLayer):Transformer编码器的“基本功能单元”
编码器层是Transformer编码器的核心组成部分——整个编码器就是由N个完全相同的编码器层“堆叠”而成(原论文N=6)。它的核心任务是:接收输入序列(如英语句子),通过“全局交互+局部加工”提取上下文特征,为后续解码器提供高质量的输入特征。
一、先明确编码器层的“角色定位”
在整个Transformer中,编码器层的位置和作用如下:
「输入序列→嵌入层+位置编码→编码器层1→编码器层2→…→编码器层6→输出上下文特征(memory)→传给解码器」
每个编码器层做的事情本质上一样:在前面层的基础上,进一步优化特征——比如第一层可能只捕捉“相邻词的关系”,第六层就能捕捉“句子前后的长距离依赖”(如“小明去公园,他很开心”中“他”和“小明”的关联)。
二、编码器层的核心结构:“2个子层+2个AddNorm”
每个编码器层只包含两个核心子层,且每个子层都被AddNorm(残差连接+层归一化) 包裹(之前讲过AddNorm,这里直接复用逻辑):
- 自注意力子层:负责“全局交互”——让每个词关注输入序列中所有相关的词(比如“它”关注“猫”或“狗”);
- 前馈网络子层:负责“局部加工”——对每个词的向量单独做非线性变换(比如从“猫”的向量中提炼“动物”“哺乳动物”等细节特征)。
两者互补:自注意力抓“全局关系”,前馈网络抓“局部细节”,共同提升特征质量。
三、代码逐行解析:从初始化到前向传播
class EncoderLayer(nn.Module):def __init__(self, d_model, self_atten, feed_forward, dropout):super(EncoderLayer, self).__init__()# 1. 接收外部传入的核心模块实例(依赖注入,灵活复用)self.self_atten = self_atten # 👉 已初始化好的「多头自注意力」实例(之前讲过的MultiHeadAttention)self.feed_forward = feed_forward # 👉 已初始化好的「前馈网络」实例(之前讲过的FeedForward)# 2. 定义两个AddNorm模块,分别包裹自注意力和前馈网络# 每个AddNorm对应一个子层,确保子层训练稳定(防梯度消失、稳分布)self.sublayer = nn.ModuleList([AddNorm(d_model, dropout), # 对应「自注意力子层」的AddNormAddNorm(d_model, dropout) # 对应「前馈网络子层」的AddNorm])
1. __init__方法关键参数解释
参数名 | 作用说明 |
---|---|
d_model | 词向量维度(如512),确保所有模块维度一致,数据能顺畅流动 |
self_atten | 多头自注意力实例(比如MultiHeadAttention(h=8, d_model=512) 的输出),负责全局交互 |
feed_forward | 前馈网络实例(比如FeedForward(d_model=512, d_ff=2048) 的输出),负责局部加工 |
dropout | dropout概率(如0.1),防止过拟合,传给AddNorm模块 |
⚠️ 注意点1:依赖注入的优势
这里没有在EncoderLayer内部重新定义MultiHeadAttention或FeedForward,而是通过参数传入实例——这样做的好处是“灵活复用”:比如想换一种注意力实现,只需传入新的注意力实例,不用修改EncoderLayer的代码,符合“低耦合”设计原则。
def forward(self, x, mask):# x:输入张量,形状[batch_size, seq_len, d_model](如32个样本,每个10个词,512维)# mask:源序列掩码(仅Padding掩码),形状[batch_size, 1, 1, seq_len],遮挡无意义的<PAD>符号# 第一个子层:自注意力(关注输入序列内部的依赖关系)# lambda y: ... :包装自注意力函数,适配AddNorm的接口(AddNorm需要接收“输入为归一化后张量”的函数)x = self.sublayer[0](x, lambda y: self.self_atten(y, y, y, mask))# 第二个子层:前馈网络(对每个词的特征做非线性加工)x = self.sublayer[1](x, lambda y: self.feed_forward(y))return x # 输出形状不变:[batch_size, seq_len, d_model],特征更丰富
2. forward方法核心逻辑拆解
(1)输入x和mask的作用
- x:上一层编码器层的输出(或嵌入层+位置编码的结果),包含了前面层提取的特征;
- mask:源序列Padding掩码,作用是遮挡句子中的
<PAD>
符号(无实际语义),防止自注意力关注这些无效信息。比如句子“我[PAD][PAD]”,mask会把后两个位置设为0,自注意力不会关注它们。
(2)第一个子层:自注意力(全局交互)
这是编码器层的“核心交互模块”,关键在于**“自”**——即query=key=value
都来自同一输入y
(y
是AddNorm归一化后的x),意味着“序列自己关注自己”,捕捉序列内部的依赖关系。
📌 重点:lambda函数的作用(不是多余的!)
AddNorm的forward
方法要求传入的sublayer
是一个“接收归一化后张量的函数”(回顾AddNorm的代码:def forward(self, x, sublayer): return x + self.dropout(sublayer(self.norm(x)))
)。
这里lambda y: self.self_atten(y, y, y, mask)
就是定义这样的函数:
- 输入
y
:AddNorm归一化后的张量; - 输出:
self.self_atten(y, y, y, mask)
——即多头自注意力的结果,query=key=value=y
,并传入mask遮挡PAD。
(3)第二个子层:前馈网络(局部加工)
自注意力捕捉了“全局关系”(如“他”和“小明”的关联),但每个词的特征还需要“深度加工”。前馈网络的作用就是逐位置独立处理每个词的向量(不涉及词与词的交互),通过“升维→激活→降维”提炼细节特征(比如把“小明”的向量加工成包含“人名、男性、主语”等信息的向量)。
这里同样用lambda y: self.feed_forward(y)
包装,原因和自注意力一致:适配AddNorm的接口,输入是归一化后的y
,输出是前馈网络的结果。
(4)输出x的特点
输出x的形状和输入x完全一致([batch_size, seq_len, d_model]
),但特征更丰富:既包含了序列内部的全局依赖(自注意力的贡献),又包含了每个词的细节特征(前馈网络的贡献)。
四、关键注意点(小白必看)
1. 自注意力的“自” vs 后续解码器的“交叉注意力”
编码器层的自注意力是“自交互”(query=key=value
),而解码器层的交叉注意力是“跨序列交互”(query
来自解码器,key=value
来自编码器)——这是两者的核心区别,不要混淆!
2. 为什么要堆叠N=6层?(原论文的选择)
层数太少(如1-2层):只能捕捉简单的局部依赖(比如相邻词的关系),无法处理长句子的长距离依赖(如“小明昨天去公园,他今天还想去”中“他”和“小明”的关联);
层数太多(如10层以上):虽然能捕捉更复杂的特征,但计算成本急剧增加,且容易过拟合(模型记住训练数据的细节,泛化能力差);
N=6层:是原论文在“特征提取能力”和“计算效率”之间找到的最佳平衡点,后续大多数Transformer类模型(如BERT)也沿用类似的层数设计。
3. 所有子层都保持d_model一致,为什么?
从嵌入层到编码器层,再到解码器层,所有模块的输入输出维度都严格保持d_model
(如512)一致——这是为了保证数据流动的顺畅性,避免维度不匹配导致的错误。比如自注意力的输出是512维,前馈网络的输入输出也是512维,AddNorm的输入输出还是512维,这样才能层层堆叠。
五、知识拓展:编码器层堆叠的“特征进化”
当N=6层编码器层堆叠时,不同层提取的特征是“逐步进化”的:
- 底层(1-2层):关注局部特征,比如相邻词的语法关系(如“追”和“猫”的动宾关系);
- 中层(3-4层):关注短语级特征,比如“猫追狗”这个短语的语义;
- 高层(5-6层):关注全局特征,比如整个句子的逻辑关系(如“猫追狗,它跑得很快”中“它”指代“猫”或“狗”)。
这种“从局部到全局”的特征提取,让编码器能全面理解输入序列的语义,为解码器的翻译/生成任务提供坚实的上下文基础。
六、总结:编码器层的核心价值
编码器层是Transformer处理输入序列的“最小功能单元”,通过“自注意力(全局交互)+ 前馈网络(局部加工)+ AddNorm(稳定训练) ”的组合,实现了“既懂全局关系,又懂局部细节”的特征提取。堆叠6层后,就能捕捉输入序列的复杂上下文信息,为后续解码器生成准确的目标序列(如翻译结果)提供关键支持。
用一句话记住:编码器层=全局交互(自注意力)+ 局部加工(前馈网络)+ 稳定保障(AddNorm),堆叠起来就是强大的输入特征提取器。