3、Transformer架构拆解:Encoder-Decoder结构、LayerNorm与残差连接、Feed-Forward Network、PyTorch从零搭建

好,咱们进入正题。这一章我打算手把手带你拆解Transformer的核心组件。说实话,很多初学者一上来就看论文里的公式,结果越看越懵。我的建议是——先别管数学,先把代码跑通,再回头理解公式,会轻松很多。

3.1 整体架构:Encoder-Decoder 到底在干嘛?

Transformer最初是为机器翻译设计的。它的结构分两半:左边是Encoder(编码器),右边是Decoder(解码器)。

Encoder 负责把输入句子“看懂”。比如输入“I love you”,Encoder会把它变成一组向量,这些向量包含了每个词的上下文信息。

Decoder 负责“生成”输出句子。它一边看Encoder的输出,一边看自己已经生成的词,逐步预测下一个词。

我刚开始做自动驾驶感知时,觉得这跟我的领域没啥关系。后来发现,很多多模态融合模型(比如把相机图像和激光雷达点云对齐)用的就是Encoder-Decoder的思路。说白了,Encoder负责提取特征,Decoder负责生成结果——这个思想在AI里无处不在。

核心要点:Encoder把输入变成特征表示,Decoder根据特征表示生成输出。两者通过交叉注意力(Cross-Attention)连接。

3.2 LayerNorm:为什么不用BatchNorm?

Transformer里用的不是BatchNorm,而是LayerNorm。为什么?

BatchNorm是对一个batch里所有样本的同一特征维度做归一化。但NLP任务里,每个句子的长度不一样,BatchNorm的效果就不稳定。你想想看,如果batch里有一个句子特别长,其他句子特别短,那归一化的统计量就会偏掉。

LayerNorm是对每个样本的所有特征维度做归一化。不管句子多长,它只关心当前样本内部的特征分布。这样训练更稳定。

我在项目中遇到过一个问题:用BatchNorm训练一个序列模型,loss死活降不下去。换成LayerNorm后,一个epoch就收敛了。嗯,这里要注意——如果你做的是图像任务,BatchNorm通常更好;但只要是序列数据(文本、时间序列、点云序列),优先考虑LayerNorm。

import torch
import torch.nn as nn

# 手动实现LayerNorm
class LayerNorm(nn.Module):
    def __init__(self, d_model, eps=1e-12):
        super().__init__()
        self.gamma = nn.Parameter(torch.ones(d_model))
        self.beta = nn.Parameter(torch.zeros(d_model))
        self.eps = eps

    def forward(self, x):
        # x shape: (batch, seq_len, d_model)
        mean = x.mean(-1, keepdim=True)
        var = x.var(-1, keepdim=True, unbiased=False)
        out = (x - mean) / torch.sqrt(var + self.eps)
        return self.gamma * out + self.beta

小技巧:PyTorch官方提供了nn.LayerNorm,直接调用就行。但我建议你手动实现一次,能加深理解。

3.3 残差连接:解决梯度消失的利器

Transformer里每个子层(Self-Attention、FFN)后面都跟着一个残差连接。公式很简单:输出 = LayerNorm(x + Sublayer(x))

为什么要加这个?因为Transformer堆了6层甚至更多层。如果没有残差连接,梯度在反向传播时会越传越小,最后前面的层根本学不到东西。

残差连接让梯度可以“抄近道”直接传到前面。说白了,就是给梯度开了一条高速公路。

我曾经调试一个12层的Transformer模型,发现前几层的参数几乎没变化。加上残差连接后,问题立刻解决了。所以——如果你自己搭Transformer,千万别忘了残差连接。

class TransformerBlock(nn.Module):
    def __init__(self, d_model, n_heads, d_ff, dropout=0.1):
        super().__init__()
        self.attention = nn.MultiheadAttention(d_model, n_heads, dropout=dropout)
        self.ffn = FeedForwardNetwork(d_model, d_ff, dropout)
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        # 残差连接 + LayerNorm
        attn_out, _ = self.attention(x, x, x)
        x = self.norm1(x + self.dropout(attn_out))

        # 残差连接 + LayerNorm
        ffn_out = self.ffn(x)
        x = self.norm2(x + self.dropout(ffn_out))
        return x

3.4 Feed-Forward Network:两层全连接,不简单

FFN(前馈神经网络)是Transformer里容易被忽视的部分。它由两个全连接层组成,中间加一个ReLU激活函数:

FFN(x) = max(0, xW1 + b1)W2 + b2

第一层把维度从d_model扩展到d_ff(通常是4倍),第二层再压缩回来。为什么要扩维再压缩?我个人的理解是:让模型在高维空间里做特征变换,再投影回原空间。这样能学到更丰富的非线性特征。

在自动驾驶场景里,FFN的作用类似于对每个位置的特征做“精加工”。比如,Self-Attention捕捉了全局关系,FFN再对每个位置的特征做一次非线性变换,让特征更有区分度。

class FeedForwardNetwork(nn.Module):
    def __init__(self, d_model, d_ff, dropout=0.1):
        super().__init__()
        self.linear1 = nn.Linear(d_model, d_ff)
        self.linear2 = nn.Linear(d_ff, d_model)
        self.dropout = nn.Dropout(dropout)
        self.relu = nn.ReLU()

    def forward(self, x):
        # d_model -> d_ff -> d_model
        x = self.relu(self.linear1(x))
        x = self.dropout(x)
        x = self.linear2(x)
        return x

避坑指南:我曾经把d_ff设得太大(比如8倍d_model),结果模型参数量暴增,训练速度慢得离谱。建议从4倍开始尝试,根据任务调整。

3.5 从零搭建一个完整的Transformer Block

好了,现在我们把所有组件拼起来。下面是一个完整的Transformer Encoder Block代码:

class TransformerEncoderBlock(nn.Module):
    def __init__(self, d_model=512, n_heads=8, d_ff=2048, dropout=0.1):
        super().__init__()
        # 多头自注意力
        self.self_attn = nn.MultiheadAttention(d_model, n_heads, dropout=dropout)
        # 前馈网络
        self.ffn = FeedForwardNetwork(d_model, d_ff, dropout)
        # 层归一化
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        # Dropout
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, mask=None):
        # 子层1:多头自注意力 + 残差连接
        attn_out, _ = self.self_attn(x, x, x, attn_mask=mask)
        x = self.norm1(x + self.dropout(attn_out))

        # 子层2:前馈网络 + 残差连接
        ffn_out = self.ffn(x)
        x = self.norm2(x + self.dropout(ffn_out))

        return x

这个Block就是Transformer的基本单元。你可以把它堆叠6层、12层甚至更多。每一层的输出都是下一层的输入。

Decoder Block稍微复杂一点,多了一个交叉注意力层(Cross-Attention),用来关注Encoder的输出。但核心思想是一样的:残差连接 + LayerNorm + 子层。

总结一下:Transformer的核心就三样东西——注意力机制(捕捉关系)、残差连接(稳定训练)、LayerNorm(加速收敛)。FFN是锦上添花,但也很重要。

下一章我们会深入多头注意力的实现细节,以及位置编码到底是怎么工作的。到时候我会用自动驾驶中的实际案例来讲解,敬请期待。