3、残差连接在Transformer中的位置:Self-Attention子层后的残差连接、Feed-Forward子层后的残差连接、Add & Norm层的结构
好,咱们接着往下聊。上一节我们把LayerNorm的细节掰开揉碎了讲了一遍,这一节我打算聊聊残差连接在Transformer里到底是怎么安放的。
说实话,我第一次看Transformer论文里的那个“Add & Norm”时,心里想的是:这不就是加一下再归一化吗?有啥好说的?后来自己动手写代码,才发现这里面的门道比想象中多得多。
残差连接:为什么非加不可?
先问一个问题:一个网络如果堆到12层、24层,会发生什么?
梯度消失。说白了就是,反向传播的时候,越靠近输入层的梯度越小,小到几乎为零。那前面的层还学个啥?
残差连接就是来解决这个问题的。它让梯度可以“抄近道”直接流回前面的层。你想想看,原本信息要经过层层变换才能往后传,现在多了一条直达通道,是不是稳多了?
核心公式:
output = LayerNorm(x + Sublayer(x))
这里的 x 是子层的输入,Sublayer(x) 是子层(Self-Attention或FFN)的输出。两者相加,再做归一化。
Self-Attention子层后的残差连接
先看第一个位置:Multi-Head Self-Attention 子层后面。
流程是这样的:
- 输入
x进入 Self-Attention 子层,得到attn_output x和attn_output做逐元素相加:x + attn_output- 对相加结果做 LayerNorm
这里有个细节我特别想强调:残差连接是在子层内部做的,还是在子层外部做的?
嗯,答案是外部。你看代码就明白了:
class TransformerEncoderLayer(nn.Module):
def __init__(self, d_model, nhead, dim_feedforward, dropout=0.1):
super().__init__()
self.self_attn = nn.MultiheadAttention(d_model, nhead, dropout=dropout)
self.linear1 = nn.Linear(d_model, dim_feedforward)
self.linear2 = nn.Linear(dim_feedforward, d_model)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, src):
# Self-Attention 子层 + 残差连接 + LayerNorm
attn_output, _ = self.self_attn(src, src, src)
src = self.norm1(src + self.dropout(attn_output))
# Feed-Forward 子层 + 残差连接 + LayerNorm
ff_output = self.linear2(self.dropout(torch.relu(self.linear1(src))))
src = self.norm2(src + self.dropout(ff_output))
return src
注意看 src + self.dropout(attn_output) 这一行。残差连接是在子层输出之后、LayerNorm之前做的。我习惯把这个顺序叫做“先加后归一”。
我的经验: 有些实现会把 Dropout 放在残差连接之前,有些放在之后。我个人建议放在子层输出之后、相加之前。这样残差路径上的信息更干净,不容易被 Dropout 干扰。
Feed-Forward子层后的残差连接
第二个位置:Feed-Forward Network(FFN)子层后面。
流程和 Self-Attention 子层完全对称:
- 经过 Self-Attention 子层处理后的
src进入 FFN 子层,得到ff_output src和ff_output相加- 再做 LayerNorm
你可能会问:为什么两个子层后面都要加残差连接?
我当初也有这个疑问。后来在实践中发现,如果不加残差,12层的Transformer训练到第6层就开始梯度不稳定了。加了之后,24层、36层都能稳定训练。说白了,残差连接就是给深度网络装了个“安全气囊”。
注意: 两个子层的残差连接是独立的。Self-Attention 子层的残差只影响它自己的输出,FFN 子层的残差只影响 FFN 的输出。不要搞混了。
Add & Norm 层的结构
现在我们把两个部分合起来看:Add & Norm 到底长什么样?
其实很简单,就是两个操作:
- Add: 残差连接,把子层输入和子层输出相加
- Norm: LayerNorm,对相加结果做归一化
但这里有一个容易踩的坑:Norm 的位置。
Transformer 原始论文用的是 Post-Norm(先加后归一)。但后来很多工作发现 Pre-Norm(先归一后加)效果更好,训练更稳定。
我整理了一下两种方式的区别:
| 方式 | 顺序 | 特点 |
|---|---|---|
| Post-Norm | 子层 → Add → Norm | 原始论文方式,深层训练不稳定 |
| Pre-Norm | Norm → 子层 → Add | 训练更稳定,但表达能力略弱 |
我曾经在一个项目里用了 Post-Norm,结果训练到第8层时 loss 直接炸了。换成 Pre-Norm 后,一路平稳收敛。嗯,从那以后我默认都用 Pre-Norm。
总结一下:
- Transformer 中有两个残差连接位置:Self-Attention 子层后、FFN 子层后
- 每个残差连接都跟着一个 LayerNorm,组成 Add & Norm 结构
- Add & Norm 的顺序有 Post-Norm 和 Pre-Norm 两种,推荐 Pre-Norm
下一节我会聊聊位置编码。说实话,位置编码是 Transformer 里最容易被忽视但又极其关键的部分。到时候我会分享一些我在实际项目中踩过的坑,保证让你少走弯路。