一句话总结:残差连接在子层旁边留出一条恒等路径,让信息和梯度不必只走变换分支,因此深层网络更容易优化;Dropout 则在训练时随机把一部分激活值置零,作为一种可选的正则化手段。
13.1 回顾:Transformer Block 的结构
在深入残差连接和 Dropout 之前,让我们先把一个 Pre-Norm decoder block 写成最紧凑的形式:
u = X + Dropout(Attention(LayerNorm(X)))
y = u + Dropout(FFN(LayerNorm(u)))
Attention 和 FFN 各有一条残差路径,所以这里有两次相加。图中的两次 Dropout 是一种常见训练配置,并不是 Transformer 的硬性规定:有些模型还会对 attention weights 或输入嵌入使用 Dropout,也有些大模型把 Dropout 率设为 0。GPT-2 这类 Pre-Norm 模型还会在整叠 Block 之后再放一个最终 Norm;它不属于单个 Block 内的这两次相加。
13.2 残差连接:信息的"高速公路"
13.2.1 深度网络的问题
网络变深以后,优化会变得困难。反向传播要连续乘上许多层的 Jacobian;这些乘积可能让梯度变得很小,也可能让它变得很大。深层网络还可能出现“多加几层,训练误差反而更高”的退化问题。残差连接不能包治所有这些问题,但它给优化器多留了一条简单路径。
想象信息从第 1 层传到第 12 层:
Layer 1 → Layer 2 → Layer 3 → ... → Layer 12
如果每一层都只有一条变换路径,信息和梯度就必须连续穿过所有变换。层数一多,这条乘法链就会很难训练。
13.2.2 残差连接的解决方案
残差连接的核心思想非常简单:让输入可以直接"跳过"某些层。
输入 X ─────────────────────┐
↓ │
Layer │(跳跃连接)
↓ │
输出 ←────────────────────┘ + X
公式表示:
输出 = Layer(X) + X
不是只输出 Layer(X),而是加上原始输入 X。
13.2.3 图解残差连接
看图中右侧的教学数值(用来说明逐元素相加,并不是某个已发布模型的运行日志):
Attention 输出(经过 Dropout 后):
[4, 16, 512] 的张量
第一个值:-0.07005, 0.09600, 0.03522, ...
原始输入 X:
[4, 16, 512] 的张量
第一个值:0.50748, -1.96800, 5.14941, ...
残差连接后:
输出 = Attention输出 + X
= [-0.07005 + 0.50748, 0.09600 + (-1.96800), ...]
= [0.43743, -1.87200, ...]
就是简单的逐元素相加!
13.2.4 为什么残差连接有效?
1. 梯度直通
设 ,用向量写法,反向传播是:
这里的 是恒等路径带来的直接项, 是变换分支的 Jacobian。它通常会改善梯度传播;但它不保证梯度永不消失或永不爆炸,因为两条分支仍可能相互抵消,很多层的乘积也仍然重要。
2. 恒等映射
如果某一层"不知道该学什么",它可以学习成恒等映射:
Layer(X) ≈ 0
输出 = 0 + X = X
相当于这一层"什么都不做",信息原封不动传过去。这比学习一个完美的恒等变换容易多了。
3. 信息保留
每个 Block 都把子层输入直接加到更新量上,因此模型不必在每一层都重新构造恒等映射。这提供了信息的直通路径,但不意味着最初的输入在任意深度都能被完整还原——后续更新仍会不断修改 residual stream。
13.2.5 在 Transformer 中的位置
图中左侧的架构图清楚地标注了两个残差连接的位置:
第一个残差连接:在 Attention 之后
X → LayerNorm → Attention → Dropout → (+X) → 输出1
第二个残差连接:在 FFN 之后
输出1 → LayerNorm → FFN → Dropout → (+输出1) → 输出2
13.2.6 PyTorch 实现
from torch import nn
class TransformerBlock(nn.Module):
def __init__(self, d_model, num_heads, d_ff, dropout=0.1):
super().__init__()
self.attention = 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.dropout1 = nn.Dropout(dropout)
self.dropout2 = nn.Dropout(dropout)
def forward(self, x, mask=None):
# 第一个残差连接
attn_output = self.attention(self.norm1(x), mask)
x = x + self.dropout1(attn_output) # 残差连接!
# 第二个残差连接
ffn_output = self.ffn(self.norm2(x))
x = x + self.dropout2(ffn_output) # 残差连接!
return x
这里沿用前面章节定义的 MultiHeadAttention 和 FeedForward。核心就是那两行 x = x + ...;两个 Dropout 实例也让配置和调试更清楚。
13.3 Dropout:用随机置零做正则化
13.3.1 过拟合问题
神经网络有一个常见问题:过拟合。
过拟合是指模型在训练数据上表现很好,但在新数据上表现很差。就像一个学生:
- 把所有考试题都背下来了(训练数据)
- 但遇到新题就不会做(测试数据)
模型"记住"了训练数据的细节,而不是学到了通用的规律。
13.3.2 Dropout 的解决方案
Dropout 的想法很有趣:训练时随机把激活张量中的一些元素置零。
每次 forward pass 都会重新采样掩码。这里没有删除权重,也没有永久关闭某个神经元;下一次 forward pass,被置零的位置通常会不同。
13.3.3 Dropout 的直觉
为什么随机置零一部分激活值可能缓解过拟合?
类比:团队协作
想象一个公司:
- 没有 Dropout:某个员工特别能干,所有工作都依赖他
- 有 Dropout:每天随机让一些员工休假
结果:
- 没有 Dropout:如果那个能干的员工离职,公司就瘫痪了
- 有 Dropout:每个员工都学会了多种技能,团队更健壮
在神经网络中,这个类比想表达的是:Dropout 会扰动固定的协作关系,抑制过强的 co-adaptation。它是一种随机正则化,不保证每个神经元真的“独立工作”,也不保证在所有数据集和模型上都能改善泛化。
13.3.4 Dropout 的数学
Dropout 的操作很简单:
设丢弃率为 ,保留率为 。PyTorch 使用的是 inverted Dropout:
例如,dropout_rate = 0.1。下面五个位置里恰好丢了一个;这是一次随机抽样,并不是说每一小组都必须精确丢 10%:
input = [0.5, 0.3, 0.8, 0.2, 0.6]
mask = [1, 1, 0, 1, 1 ] # 随机生成,0.8 被丢弃
output = [0.5, 0.3, 0, 0.2, 0.6] / 0.9
= [0.56, 0.33, 0, 0.22, 0.67]
推理时:
output = input # 不做任何丢弃
13.3.5 为什么要缩放?
你可能注意到训练时有个 / (1 - dropout_rate) 的缩放。
原因:对每个固定输入元素,保持期望值不变。
假设 dropout_rate = 0.1:
- 训练时:每个激活值平均有 90% 的概率保留;保留下来的值放大 1/0.9 ≈ 1.11
- 推理时:所有激活值直接通过,不再缩放
因此 。这里保持的是逐元素条件期望,不是保证每次抽样的向量范数都不变。
13.3.6 在 Transformer 中的位置
在本章这套简化的 residual Dropout 配置里,位置是:
- Attention 之后:
Attention → Dropout → 残差连接 - FFN 之后:
FFN → Dropout → 残差连接
也就是先扰动子层的更新量,再与 residual stream 相加。原始 Transformer 还对 embedding 与位置编码之和使用 Dropout;不少实现也会对 attention weights 使用另一种 Dropout。还有一些模型完全不用 Dropout,所以不能把“总是在这里”当成所有 Transformer 的普遍定律。
13.3.7 PyTorch 实现
import torch
import torch.nn as nn
x = torch.tensor([0.5, 0.3, 0.8, 0.2, 0.6])
dropout = nn.Dropout(p=0.1)
dropout.train()
train_output = dropout(x) # 随机掩码;保留下来的值除以 0.9
dropout.eval()
eval_output = dropout(x) # 与 x 相同
如果 Dropout 是某个 model 的子模块,调用 model.train() / model.eval() 会递归切换它的模式;单独拿一个 Dropout 层演示时,则像上面这样直接切换它。
13.4 Pre-Norm vs Post-Norm
13.4.1 两种 LayerNorm 位置
一个重要的架构选择,是 Norm 放在 residual addition 的哪一边:
Post-Norm(原始 Transformer,2017):
u = LayerNorm(X + Dropout(Attention(X)))
y = LayerNorm(u + Dropout(FFN(u)))
LayerNorm 在残差连接之后。
Pre-Norm(GPT-2 使用的布局):
u = X + Dropout(Attention(LayerNorm(X)))
y = u + Dropout(FFN(LayerNorm(u)))
LayerNorm 在残差连接之前(在每个子层的输入处)。
GPT-2 还在整叠 Block 的最后加了一个最终 LayerNorm。LLaMA 延续的是 Pre-Norm 的位置关系,但把 LayerNorm 换成了 RMSNorm。
13.4.2 为什么 Pre-Norm 常见?
Pre-Norm 的恒等路径不经过 Norm。理论分析和实践都表明,它在初始化时通常有更规整的梯度,对很深的堆叠更容易优化,也能降低对精细 warmup 的依赖。
但这不是“Pre-Norm 在任何意义上都更好”。Post-Norm 配合合适的初始化、warmup 或其他改造也能训练得很好;Pre-Norm 自己也可能出现 residual stream 尺度随深度增长等问题。现代模型还有 RMSNorm、并行分支和其他 Norm 布局。因此更准确的说法是:Pre-Norm 是现代 decoder-only LLM 的常见起点,而不是唯一标准答案。
13.5 残差连接 + Dropout 的协同作用
13.5.1 完整的数据流
让我们追踪一次完整的数据流:
输入 X [4, 16, 512]
↓
LayerNorm(X) # 标准化
↓
Attention(LayerNorm(X)) # 注意力计算
↓
Dropout(Attention(...)) # 随机丢弃(训练时)
↓
X + Dropout(...) # 残差连接
↓
输出 [4, 16, 512] # 形状不变
13.5.2 为什么这个组合有效?
| 技术 | 解决的问题 | 机制 |
|---|---|---|
| 残差连接 | 深层优化困难 | 提供恒等信息与梯度路径 |
| Dropout | 过拟合风险 | 用随机掩码做可选正则化 |
| Norm | 激活尺度与训练动态 | 在每个 token 的特征维度上控制尺度 |
三者配合:
- LayerNorm 稳定输入
- Attention/FFN 学习特征
- 如果配置了 Dropout,它对更新量增加随机正则化
- 残差连接把更新量加回 residual stream
这里真正接近架构必需品的是 residual path 和某种稳定化方案;Dropout 是否有益要看模型、数据和训练目标,不能把三者写成“缺一不可”。
13.6 实际训练中的参数
13.6.1 常见的 Dropout 率
| 公开训练配方 | Dropout 率 | 备注 |
|---|---|---|
| 原始 Transformer base | 0.1 | residual 与 embedding/position 的和 |
| BERT | 0.1 | 论文中的预训练与微调配置 |
| LLaMA | 0.0 | 参考实现的 Block 不使用 Dropout |
这几行不能推出“模型越大,Dropout 一定越低”。Dropout 率是训练配方的一部分,受数据规模与重复度、训练时长、任务、其他正则化以及优化稳定性共同影响。大规模自回归预训练有时会选择 0,但微调或较小数据集完全可能需要重新调参。
13.6.2 残差连接的变体
有些模型会在残差连接上做一些变化:
缩放 residual branch(示意):
x = x + alpha * sublayer(x)
门控残差:
gate = sigmoid(linear(x))
x = x + gate * sublayer(x) # 学习残差的权重
不同方法会固定、初始化或学习 alpha,门控形式也各不相同。原始 Transformer 使用直接相加;后来的模型不一定都坚持这一种形式,所以这里的代码只用来说明设计空间。
13.7 本章总结
13.7.1 核心概念
| 概念 | 作用 | 公式 |
|---|---|---|
| 残差连接 | 改善深层优化,提供恒等路径 | output = x + F(x) |
| Dropout | 可选的随机正则化 | 训练时随机置零并按 keep rate 缩放 |
| Pre-Norm | 常见、较易优化的布局 | Norm 在子层之前,整叠之后通常还有最终 Norm |
13.7.2 在 Transformer Block 中的位置
输入 X
↓
LayerNorm ──→ Attention ──→ Dropout ──→ (+X) ──→ 输出1
↑
残差连接
输出1
↓
LayerNorm ──→ FFN ──→ Dropout ──→ (+输出1) ──→ 输出2
↑
残差连接
13.7.3 核心认知
残差连接让每个子层学习“在现有 residual stream 上加什么”,而不是每次都从头重写全部表示;恒等路径通常让深层网络更容易优化。Dropout 则是在训练配方需要时,对激活施加随机掩码来正则化。它们作用不同,而且 Dropout 不是每个 Transformer 都必须有。
本章交付物
学完这一章,你应该能够:
- 解释残差连接为什么通常改善梯度传播,以及它为什么不是绝对保证
- 理解 inverted Dropout 的随机掩码、缩放和训练/推理差异
- 说出残差连接和 Dropout 在 Transformer Block 中的位置
- 区分 Pre-Norm 和 Post-Norm 的区别
下一章预告
每个 Block 的残差连接加回的是该子层当前的输入,不是永远加回最初的词嵌入。不过在第一个 Block 之前,residual stream 的起点确实来自 词嵌入(Embedding) 和 位置编码(Positional Encoding) 的结合。
下一章,我们来深入理解词嵌入和位置信息是如何结合的,以及这种设计背后的深层逻辑。