Skip to content

前馈网络与残差连接

注意力负责在位置之间搬运、混合信息;而 前馈网络(FFN) 负责在每个位置内部做非线性加工;残差连接 + 层归一化 则像钢筋和水泥,让几十层能稳稳叠起来不塌。这三件套,构成了 Transformer 每一层。

1. 逐位置前馈网络(Position-wise FFN)

Attention 的输出里,每个位置还是一个 d_model 维向量。FFN 对每个位置独立、相同地套一个两层全连接网络:

[ \text{FFN}(x) = \max(0,; xW_1 + b_1),W_2 + b_2 ]

   d_model=512                d_ff=2048              d_model=512
   向量 ──Linear(升维)──► 中间向量 ──ReLU──► 中间向量 ──Linear(降维)──► 向量
                         (先"展开")        (非线性)          (再"压回")

为什么先升维再降维?

  • 升到一个更宽的空间(d_ff 通常是 d_model 的 4 倍,如 512→2048),能存更多「知识」、做更复杂的变换。
  • 再压回 d_model,保证维度不变,能顺畅地和残差相加、传给下一层。
  • 中间的 ReLU/GELU 提供非线性——没有它,两层线性等于一层线性,白搭。

有研究者把 FFN 看作模型的「键值记忆(key-value memory)」:注意力负责「从哪里取信息」,FFN 负责「对取到的信息做知识加工」。经验上,模型参数的大头都在 FFN,它是模型「记住事实」的重要场所。

「逐位置」是什么意思

句子 [w1, w2, w3] 各自过一个"相同"的 FFN:
   FFN(w1)  FFN(w2)  FFN(w3)      ← 三份用同一套权重,互不串门

FFN 不 mixing 位置(那是注意力的活),因此天然可并行。

python
class FeedForward(nn.Module):
    def __init__(self, d_model, d_ff):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(d_model, d_ff),
            nn.GELU(),                 # 现代模型多用 GELU,原始论文用 ReLU
            nn.Linear(d_ff, d_model),
        )
    def forward(self, x):              # x: [B, T, d_model]
        return self.net(x)             # 输出同形状 [B, T, d_model]

2. 残差连接(Residual / Skip Connection)

每个子层(注意力、FFN)的输出,都会加上它的输入

      输入 x ───────────────┐        ← 一条"捷径"直接连到后面
        │                   │
   [子层: Attention/FFN]    │        子层只需学"增量" f(x)
        │                   │
        └────► f(x) ──► ( + ) ──► x + f(x)

为什么不可或缺

没有残差有残差
梯度要穿过每一层,深网络易消失/爆炸梯度可走「捷径」直达底层,深层可训
每层必须彻底改写表示,信息易丢失每层只需学「在原有基础上补充什么」
堆到十几层就难收敛轻松堆到几十上百层

残差让每个子层从「重写全文」变成「在原稿上批注」,训练稳定得多,这也是 Transformer 能加深的前提。

3. 层归一化(LayerNorm)

把每个位置的向量做标准化,稳定数值分布、加速收敛:

[ \text{LayerNorm}(x) = \gamma \cdot \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} + \beta ]

  • (\mu, \sigma^2):该位置向量内部各维度的均值与方差
  • (\gamma, \beta):可学习的缩放与偏移

LayerNorm vs BatchNorm

BatchNorm: 跨"样本"维统计 → 依赖 batch,对变长序列/推理不友好
LayerNorm: 只在"特征"维内部统计 → 与 batch 大小无关,天然适合 NLP 序列

所以 Transformer 全线使用 LayerNorm

4. Pre-Norm vs Post-Norm

规范化和残差相加的先后顺序,有两种摆法:

Post-Norm(原始论文):        Pre-Norm(现代主流):
  x = LayerNorm(x + Sublayer(x))   x = x + Sublayer(LayerNorm(x))
        │                                │
  深层训练不太稳定                   训练更稳,可堆更深,大模型几乎都用它
Post-NormPre-Norm
出处原始《Attention Is All You Need》GPT-2 及之后几乎所有大模型
训练稳定性层数深时需小心(配合 warmup)更稳,易收敛
极限质量调得好略高基本持平,工程上更省心

本教程的 NanoGPT 实战会采用 Pre-Norm

5. 把三件套拼成一个「Block」

现在一个标准的 Transformer 层就长这样:

        输入 x

   ┌──────┴───────┐
   │ 多头注意力    │
   └──────┬───────┘
      Dropout

     ( + )  ◄── 残差: 加回输入 x

      LayerNorm

   ┌──────┴───────┐
   │   FFN        │
   └──────┬───────┘
      Dropout

     ( + )  ◄── 残差: 再加回

      LayerNorm

        输出 → 送往下一个 Block
python
class TransformerBlock(nn.Module):
    def __init__(self, d_model, num_heads, d_ff, dropout=0.1):
        super().__init__()
        self.attn = MultiHeadAttention(d_model, num_heads)   # 见上一页
        self.ffn  = FeedForward(d_model, d_ff)
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.drop  = nn.Dropout(dropout)

    def forward(self, x, mask=None):
        # Pre-Norm 风格: 先 norm 再进子层, 子层输出走残差
        x = x + self.drop(self.attn(self.norm1(x), self.norm1(x), self.norm1(x), mask))
        x = x + self.drop(self.ffn(self.norm2(x)))
        return x
  • Dropout:训练时随机丢弃部分神经元,防过拟合;推理时关闭。
  • 把这样的 Block 堆 N 层(原始论文 N=6),就得到 Encoder 或 Decoder 的骨干。

6. 分工总览

┌───────────┬──────────────────┬───────────────────┐
│  组件      │  干什么           │  一句话记忆         │
├───────────┼──────────────────┼───────────────────┤
│ 注意力     │ 跨位置混合信息     │ "谁该看谁"          │
│ FFN       │ 位置内非线性加工   │ "把看到的想明白"     │
│ 残差连接   │ 稳住深层训练       │ "在原稿上批注"       │
│ LayerNorm │ 稳定数值分布       │ "每层先归零再出发"   │
│ Dropout   │ 防过拟合          │ "偶尔随机偷懒"       │
└───────────┴──────────────────┴───────────────────┘

小结

  • FFN:逐位置的两层网络(升维→激活→降维),提供非线性与「知识加工」。
  • 残差x + f(x),让梯度直达、深层可训,是加深的基石。
  • LayerNorm:在特征维归一化,比 BatchNorm 更适合变长序列。
  • Pre-Norm 更稳,是现代大模型默认选择。
  • 三件套组成一个 Block,堆 N 层即成骨干。

下一步

所有零件都认全了。下一页把它们组装成完整的 Transformer,看看整台机器如何协同 → 完整架构