☰
transformer你不学?07|Encoder 块:残差、归一化与前馈网络
2026/10/15 4:11:17 网站建设 项目流程

transformer你不学?07|Encoder 块:残差、归一化与前馈网络

作 者:吴佳浩(Alben)

微某信公某众号:全栈架构师笔记


导读
注意力只是 Encoder 块的"一半零件"。让它真正能堆到几十层深的,是两个不起眼的配角:残差连接和 LayerNorm——没有它们,BERT 堆到第 3 层就训练崩了。
本篇组装出一个可以无限堆叠的标准 Encoder 块,顺便讲清 Pre-LN 和 Post-LN 之争。


本篇你会学到什么

  • 🔸 残差连接为什么是深层网络的"高速公路";
  • 🔸 LayerNorm 与 BatchNorm 的本质区别,为什么 NLP 用前者;
  • 🔸 FFN(前馈网络)在块内扮演什么角色;
  • 🔸 Pre-LN 与 Post-LN 的结构差异与稳定性。

一、为什么要学这个

一个标准 Encoder 块 = 注意力子层 + 前馈子层,每个子层都套着"残差 + 归一化":

x = x + Attention(LN(x)) # 子层1 x = x + FFN(LN(x)) # 子层2

三个角色分工:

  • 🔸Attention:token 之间横向交流(跨位置);
  • 🔸FFN:每个位置独立纵向加工(跨通道,参数量占大头);
  • 🔸残差 + LN:让信号能无损穿透几十层(训练稳定性)。

二、核心理论:三个配角为什么必不可少

残差连接:把输入原样加到输出上,y = x + F(x)。梯度反向传播时走1 + F'(x)的通路——那个"1"保证梯度即使穿过 100 层也不会消失。这是 ResNet 交给 Transformer 的遗产。

LayerNorm:对每个 token 的特征维归一化(均值 0 方差 1,再可学习缩放平移)。NLP 不用 BatchNorm 因为序列长度不一、batch 内统计量不稳定;LN 对单样本自己算统计量,与 batch 无关。

FFN:两层线性 + 激活(论文用 ReLU,现代模型多用 GELU/SwiGLU)。中间层通常扩到 4 倍维度,负责"把注意力聚合来的信息做非线性加工"。GPT-3 的参数大头在 FFN 不在 Attention。

三、Pre-LN vs Post-LN:一个顺序引发的稳定性差异

Post-LN(论文原版): x = LN(x + Attn(x)) # 归一化在残差里面 Pre-LN(现代主流): x = x + Attn(LN(x)) # 归一化在残差外面

Post-LN 每层输出都被归一化,深层梯度信号被反复"重置",BERT 时代需要学习率 warmup 才训得动。Pre-LN 的残差通路无阻塞,几十层直接堆——GPT/LLaMA/Qwen 全是 Pre-LN。

四、流程图

输入 x

LayerNorm

多头注意力

x + Attention输出 (残差)

LayerNorm

FFN: Linear→GELU→Linear

x + FFN输出 (残差)

输出给下一层块

五、真实代码

# 标准 Encoder 块:Pre-LN + 残差 + FFN(现代主流结构)importtorchimporttorch.nnasnn torch.manual_seed(42)classEncoderBlock(nn.Module):def__init__(self,d_model=32,n_heads=4,d_ff=128):super().__init__()# 子层1:注意力(torch 内置 MHA,第 6 篇 的工业版)self.ln1=nn.LayerNorm(d_model)self.attn=nn.MultiheadAttention(d_model,n_heads,batch_first=True)# 子层2:FFN,中间层扩 4 倍self.ln2=nn.LayerNorm(d_model)self.ff=nn.Sequential(nn.Linear(d_model,d_ff),# 扩维: 32→128nn.GELU(),# 现代激活函数nn.Linear(d_ff,d_model),# 缩回: 128→32)defforward(self,x):# Pre-LN 子层1:注意力h=self.ln1(x)a,_=self.attn(h,h,h,need_weights=False)x=x+a# 残差:直通高速公路# Pre-LN 子层2:FFNx=x+self.ff(self.ln2(x))# 残差returnx# 单块验证blk=EncoderBlock()x=torch.randn(2,5,32)out=blk(x)print(f"单块输出 shape:{out.shape}")# 堆叠 10 层验证训练稳定性blocks=nn.Sequential(*[EncoderBlock()for_inrange(10)])deep_out=blocks(x)print(f"10层堆叠输出 shape:{deep_out.shape}")print(f"输出是否有 NaN:{torch.isnan(deep_out).any().item()}")

输出结果怎么看(真实运行输出):

单块输出 shape: torch.Size([2, 5, 32]) 10层堆叠输出 shape: torch.Size([2, 5, 32]) 输出是否有 NaN: False
  • 🔸 随机权重下 10 层堆叠零 NaN:Pre-LN + 残差的稳定性是数值事实,不是理论口号——你可以试着把残差去掉再跑,大概率直接崩;
  • 🔸 shape 进出一致(2,5,32):块与块之间可以任意串联,这是"堆叠"的结构基础;
  • 🔸need_weights=False:不返回注意力矩阵,省一次大张量物化——推理时的微小优化。

六、运行结果应该怎么看

对比一下三组参数量(d_model=32):

Attention (qkv+proj): 32*96+96 + 32*32+32 = 4224 FFN: 32*128+128 + 128*32+32 = 8352 LayerNorm: 2 * (32+32) * 2 = 256

FFN 参数量是 Attention 的近两倍——把比例放大到 GPT-3 级别依然成立。所以"模型容量主要在 FFN"不是一句空话,压缩模型(量化/剪枝)也优先盯 FFN。

七、常见错误与排查

  • 🔸batch_first 忘了设:nn.MultiheadAttention默认(T, B, C),喂(B, T, C)不报错但结果错——最阴险的坑;
  • 🔸残差方向加反:是x + F(LN(x)),不是LN(x + F(x))——后者是 Post-LN,不是错误,但别混用;
  • 🔸FFN 中间维度拍脑袋:4 倍是传统默认,LLaMA 用 SwiGLU 时取 8/3 倍对齐——跟风论文不如理解"扩维-非线性-缩维"的结构意图。

八、练习题

  1. 把两处残差分别注释掉,10 层堆叠再跑一次,观察 NaN 出现情况;
  2. 打印blk各子模块参数量,验证 FFN:Attention ≈ 2:1 的比例;
  3. 把 Pre-LN 改成 Post-LN(交换 LN 和残差位置),堆 20 层对比两种结构的输出方差,解释为什么 Post-LN 需要 warmup。

一句话总结:残差给梯度修高速公路,LayerNorm 稳住每层分布,FFN 承载大部分参数与非线性——注意力负责交流,这三件套负责"能堆 100 层还训得动"。

下一篇:transformer你不学?08|Decoder 块:掩码注意力与自回归生成。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询