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是锦上添花,但也很重要。
下一章我们会深入多头注意力的实现细节,以及位置编码到底是怎么工作的。到时候我会用自动驾驶中的实际案例来讲解,敬请期待。