正则化深度实战:从权重衰减、Dropout 到早停、数据增强与 Stochastic Depth 的训练工程全解
正则化是机器学习从"记住训练集"走向"学会泛化"的唯一桥梁,也是模型在有限数据上获得可部署精度的系统工程内核。它被每一个训练脚本默认开启,却极少有文章从权重衰减与 AdamW 的解耦、Dropout 的训练/推理语义、早停与权重衰减的对偶性、数据增强的隐式正则视角、以及"哪些参数不该加权重衰减"等生产视角做工程化展开。本文与本站《RMSNorm》《相对位置编码》《交叉注意力》《残差连接》《学习率调度》《梯度累积》《词嵌入》《激活函数》《损失函数》共同构成 Transformer 内部机制与训练工程深度解系列,补全"泛化控制"这一环。
一、第一性原理:为什么需要正则化
监督学习在有限样本 $S=\{(x_i,y_i)\}_{i=1}^N$ 上最小化经验风险 $\hat R(f)=\frac{1}{N}\sum_i \ell(f(x_i),y_i)$,而真正关心的是期望风险 $R(f)=\mathbb{E}_{(x,y)\sim\mathcal{D}}[\ell(f(x),y)]$。泛化间隙(generalization gap) $R(f)-\hat R(f)$ 由模型容量(capacity)与样本量共同决定:容量越高、样本越少,过拟合越严重。
正则化的统一框架是把无约束优化改成约束/惩罚优化:
$$\min_f \hat R(f) \quad \longrightarrow \quad \min_f \hat R(f) + \lambda\, \Omega(f)$$
其中 $\Omega(f)$ 是对"复杂假设"的惩罚,$\lambda$ 是权衡系数。这等价于在参数空间施加约束 $\|w\|\le C$(Tikhonov 正则化的 Lagrange 对偶)。理解这一点,就能解释为什么所有正则方法本质上都在做同一件事——压缩假设空间的容量,迫使模型在低复杂度的解里寻找可分界面。
| 正则手段 | 作用域 | 机制 | 超参 | 典型场景 |
|---|---|---|---|---|
| L2 / 权重衰减 | 参数 | 惩罚平方范数 | $\lambda$ | 通用默认 |
| L1 | 参数 | 惩罚绝对值 | $\lambda$ | 稀疏、特征选择 |
| Dropout | 激活 | 随机置零(集成近似) | $p$ | 全连接/注意力 |
| 早停 | 优化过程 | 截断迭代 | patience | 任意 |
| 数据增强 | 数据 | 隐式扩大分布支撑 | 策略 | 视觉/语音 |
| Stochastic Depth | 结构 | 随机跳过残差分支 | $p$ | 深层 ResNet |
二、权重衰减与 L2:被误解最深的默认项
2.1 数学本质与"解耦"陷阱
标准 L2 正则在损失上叠加 $\frac{\lambda}{2}\|w\|^2$,梯度多了一项 $\lambda w$,于是 SGD 更新变成:
$$w_{t+1}=(1-\eta\lambda)w_t - \eta \nabla \hat R(w_t)$$
系数 $(1-\eta\lambda)$ 就是"衰减"的字面含义——每步把权重向零收缩一点。然而当优化器从 SGD 换成带动量的 Adam 时,L2 与权重衰减不再等价:Adam 用二阶动量 $v_t$ 缩放梯度,L2 项 $\lambda w$ 也被缩放,导致衰减强度随梯度历史漂移;而真正的权重衰减应当无差别地作用于所有权重。
Loshchilov 等人(2017)提出的 AdamW 把衰减项从损失里拿出来、直接作用在权重更新上:
# ❌ 经典 Adam + L2:惩罚被 Adam 的 v_t 缩放,衰减效果失真
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=0.01)
# ✅ AdamW:权重衰减与梯度解耦,直接作用到权重更新
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=0.01)
经验上,AdamW 容许比 Adam+L2 大得多的 weight_decay(如 0.1)而不损害收敛,这正是现代 LLM 预训练(如 Llama、GPT 系列)默认 weight_decay=0.1 的原因。
2.2 哪些参数不该加权重衰减
这是生产环境最常被忽略、也最容易踩坑的规则。对所有参数无差别地施加权重衰减是有害的:
- BatchNorm / LayerNorm 的增益与偏置:缩放因子 $\gamma$ 与偏置 $\beta$ 没有"幅度过大即过拟合"的风险,加衰减只会无谓地压缩它们,反而损害表达力。
- 偏置(bias):偏置项维度低、不易过拟合,通常排除。
- Embedding 表:部分实践对 embedding 不加衰减(尤其词表巨大时),避免压制稀有词向量。
def configure_optimizer(model, lr=1e-3, wd=0.1):
# 标准做法:把"不该衰减"的参数拆出来单独给 weight_decay=0
decay, no_decay = [], []
for name, p in model.named_parameters():
if not p.requires_grad:
continue
if len(p.shape) == 1 or name.endswith('.bias') \
or 'norm' in name.lower() or 'bn' in name.lower():
no_decay.append(p)
else:
decay.append(p)
groups = [
{'params': decay, 'weight_decay': wd},
{'params': no_decay, 'weight_decay': 0.0},
]
return torch.optim.AdamW(groups, lr=lr)
这条"norm 与 bias 不衰减"的规则被 HuggingFace get_linear_schedule_with_warmup 的官方示例、以及 Llama/GPT-NeoX 训练脚本普遍采用。违反它(如对所有参数统一 weight_decay=0.1)会观察到 loss 曲线异常、norm 层被压垮。
三、Dropout:随机集成与它的训练/推理鸿沟
3.1 从 Bernoulli 掩码到"反转 Dropout"
Dropout 在训练时以概率 $p$ 将神经元输出随机置零,等价于对 $2^n$ 个子网络做几何平均集成(Srivastava 等,2014)。朴素的"训练期乘掩码、推理期乘 $(1-p)$"会引入推理/训练的分布不一致,因此工业实现一律采用反转 Dropout(inverted dropout):训练期对保留的神经元除以 $(1-p)$ 做期望补偿,推理期什么都不做。
import torch, torch.nn as nn
class InvertedDropout(nn.Module):
def __init__(self, p=0.1):
super().__init__()
self.p = p
def forward(self, x):
if self.training:
# 训练期:保留神经元按 1/(1-p) 放大,保证期望不变
mask = (torch.rand_like(x) > self.p).float()
return x * mask / (1.0 - self.p)
return x # 推理期:恒等,无需缩放
关键陷阱:model.eval() 必须被调用。若忘记切到 eval 模式,推理时 Dropout 仍在随机置零,导致预测结果每次不同、线上服务抖动且精度暴跌。这是上线事故的高频来源。
3.2 Dropout 与 BatchNorm 的冲突
Dropout 制造激活值的方差抖动,而 BatchNorm 依赖 mini-batch 的均值/方差估计。二者叠加会放大 BN 在推理期使用的滑动统计量误差,尤其在 Batch Size 较小时。实践中:深层 CNN/Transformer 通常只在全连接或大矩阵乘法后使用 Dropout,而 BN/LN 层后不再挂 Dropout;现代大模型训练甚至完全弃用 Dropout,靠权重衰减 + 数据规模 + 早停来控制泛化。
四、早停:与权重衰减对偶的"免费午餐"
早停在验证损失开始回升的拐点截断训练。Ng(2004)证明:早停等价于一种隐式的 L2 正则,其有效惩罚强度随训练步数单调增长——训练越久,等效权重衰减越强。因此早停与权重衰减存在对偶关系:在二者之间取平衡,往往比单纯堆 $\lambda$ 更省心。
class EarlyStopper:
def __init__(self, patience=5, min_delta=1e-4, restore_best=True):
self.patience = patience; self.min_delta = min_delta
self.restore_best = restore_best; self.counter = 0
self.best_loss = float('inf'); self.best_state = None
def __call__(self, model, val_loss):
if val_loss < self.best_loss - self.min_delta:
self.best_loss = val_loss; self.counter = 0
if self.restore_best:
self.best_state = {k: v.clone() for k, v in model.state_dict().items()}
else:
self.counter += 1
return self.counter >= self.patience
def restore(self, model):
if self.best_state:
model.load_state_dict(self.best_state)
注意 restore_best=True:早停的精髓不是"在最后一步停",而是回滚到验证损失最低的那一步的权重。丢弃坏权重这一步,经常能比继续训练多拿 0.5~1 个点的泛化精度。
五、数据增强:隐式正则化的第一现场
数据增强不改变损失,而是改变经验分布的支撑集,逼迫模型在"插值出来的新样本"上也保持稳健——它是最强、最廉价的隐式正则。
| 方法 | 机制 | 标签处理 | 风险 |
|---|---|---|---|
| 几何/颜色抖动 | 保持语义的变换 | 不变 | 过度抖动扭曲语义 |
| Mixup | 样本与标签线性插值 | 软标签 | 破坏硬边界 |
| CutMix | 区域切块互换 | 按面积比例软标签 | 小目标被覆盖 |
| RandAugment | 随机策略组合 | 不变 | 搜索成本 |
# Mixup:把标签也线性插值,等价于"软正则"
def mixup_data(x, y, alpha=0.2):
lam = torch.distributions.Beta(alpha, alpha).sample().to(x.device)
idx = torch.randperm(x.size(0))
mixed_x = lam * x + (1 - lam) * x[idx]
return mixed_x, y, y[idx], lam
# 损失端:两端标签按 lam 加权
loss = lam * criterion(logits, y_a) + (1 - lam) * criterion(logits, y_b)
数据增强与标签平滑存在天然桥接:二者都通过"软化目标分布"降低模型过自信。但也要注意,Mixup/CutMix 产生的软标签与知识蒸馏的软标签语义不同(前者是凸组合、后者是教师分布),混用时需明确目标来源,避免梯度互相打架。
六、更进阶的正则:Stochastic Depth、EMA 与 SWA
6.1 Stochastic Depth(DropPath)
深层残差网络里,随机在训练时"跳过"某些残差分支(以概率 $p$ 直接走恒等映射),等效于对浅层子网络做集成,并缓解梯度消失。Huang 等(2016)的随机深度是现代视觉骨干(ConvNeXt、Swin)的标配。
class DropPath(nn.Module):
def __init__(self, p=0.1):
super().__init__(); self.p = p
def forward(self, x):
if not self.training or self.p == 0:
return x
keep = (torch.rand(x.shape[0], 1, 1, 1, device=x.device) > self.p).float()
return x * keep / (1.0 - self.p) # 同样用 inverted 补偿
6.2 模型 EMA 与 SWA
- 指数移动平均(EMA):用滑动平均的权重做推理,平滑训练后期的权重抖动,几乎零成本提升泛化,是生成模型(扩散、GAN)与半监督训练的标配。
- 随机权重平均(SWA):在训练末期沿 SGDR 周期收集多个 checkpoint 求算术平均,收敛到损失曲面的更宽极小值,泛化更稳。
七、正则手段的协同与冲突清单
正则不是越多越好,叠加时存在真实冲突:
- 权重衰减 × BatchNorm:对 norm 层加衰减有害(见 2.2),务必拆分参数组。
- Dropout × BatchNorm:方差抖动放大 BN 估计误差,深层模型宜二选一或仅在大矩阵后 Dropout。
- 数据增强 × 归一化统计量:增强改变输入分布,若 BN 用训练期统计量、却在不增强的测试集上推理,误差被放大——需保证推理走滑动平均统计量。
- 强增强 × 小学习率:增强相当于加大有效 batch 噪声,配过小学习率会欠拟合。
- 权重衰减 × 学习率预热:预热期梯度大,若衰减系数随 lr 线性缩放(如
wd * lr),需显式实现"衰减随 lr 归一"以避免预热早期过度压缩权重。
八、生产陷阱清单(12 项)
- 忘记
model.eval():Dropout/BN 训练态泄漏到推理,预测抖动、精度暴跌。 - 全参数无差别
weight_decay:norm/bias 被错误衰减,训练不稳。 - Adam 当 AdamW 用:L2 被二阶动量缩放,衰减失真。
- 早停未回滚最佳权重:停在过拟合点而非最低验证点。
- Dropout 概率在推理期残留:未做 inverted dropout,分布错位。
- 增强策略与测试分布偏移过大:训练看增强图、推理看原图,BN 统计失准。
- Mixup 软标签与蒸馏目标冲突:两端梯度互相抵消。
- 权重衰减系数随 lr 缩放却未归一:预热期过度压缩。
- EMA 衰减率设错:过大(如 0.9999)导致历史权重稀释当前,过小则无平滑收益。
- DropPath 概率在小 batch 下过高:子网络过浅,训练信号缺失。
- L1 未做特征归一化:不同量纲特征的稀疏化被尺度主导,特征选择失效。
- 早停 patience 与学习率调度错配:cosine 末段 loss 自然抖动,patience 过小会过早停在最优点之前。
九、可复现的 PyTorch 正则工具箱
import torch, torch.nn as nn, torch.nn.functional as F
def build_groups(model, wd=0.1):
decay, no_decay = [], []
for n, p in model.named_parameters():
if not p.requires_grad or len(p.shape) == 1 or n.endswith('.bias') \
or 'norm' in n.lower():
no_decay.append(p)
else:
decay.append(p)
return [{'params': decay, 'weight_decay': wd},
{'params': no_decay, 'weight_decay': 0.0}]
class EMA:
def __init__(self, model, decay=0.9999):
self.shadow = {k: v.clone() for k, v in model.state_dict().items()}
self.decay = decay
@torch.no_grad()
def update(self, model):
for k, v in model.state_dict().items():
self.shadow[k].mul_(self.decay).add_(v, alpha=1 - self.decay)
def apply(self, model):
model.load_state_dict(self.shadow)
# 训练主循环骨架(融合 AdamW + 参数分组 + EMA + 早停)
model = MyModel().train()
opt = torch.optim.AdamW(build_groups(model), lr=3e-4)
ema, stopper = EMA(model), EarlyStopper(patience=6)
for epoch in range(100):
for x, y in loader:
opt.zero_grad()
loss = F.cross_entropy(model(x), y)
loss.backward(); opt.step(); ema.update(model)
ema.apply(model)
if stopper(model, val_loss):
stopper.restore(model); break
正则化从来不是"调一个 magic number",而是对容量—样本—优化轨迹三者关系的系统性工程控制。把权重衰减、Dropout、早停、数据增强看成一把可拆解、可协同、也可冲突的工具箱,而非孤立的超参,才能在有限数据上稳定地逼出模型的泛化上限。

发表评论 取消回复