过拟合与正则化专题:L1、L2、Dropout、早停——让模型学会适可而止

· 约 7 分钟 ·
本文目录(11 节)

训练集表现很好,换到验证集却下降,可能是过拟合,也可能是两组数据分布不同。先检查切分和数据质量,再判断是否需要限制模型容量。下面介绍常见正则化方法,并说明应该观察哪些信号来判断它们是否有效。

这篇把主流正则技术一次性讲透:每种方法「限制了什么、为什么有效、什么时候用」。

前置阅读:建议先读 线性回归实战、神经网络基础、ML 数学基础。

偏差-方差权衡:过拟合的本质图景

模型误差可以拆成三块:偏差(模型太简单,系统性学偏)+ 方差(模型太灵活,数据抖一下预测就跟着抖)+ 不可约噪声。

欠拟合 ←————————→ 过拟合
高偏差              高方差
训练差测试差        训练好测试差
    最优复杂度在中间:测试误差的 U 型谷底

诊断是第一课:训练误差和验证误差画在同一张图上(学习曲线)——两个都高是欠拟合(加容量/减正则),训练低验证高是过拟合(加正则/加数据)。不诊断就动手是瞎调。

L2 与 L1:权重上的两种镣铐

在损失函数上加惩罚项,限制权重的大小:

L=Ldata+λ∑wi2(L2)L=Ldata+λ∑∣wi∣(L1)L = L_{data} + \lambda \sum w_i^2 \quad (L2) \qquad L = L_{data} + \lambda \sum |w_i| \quad (L1)

L2(权重衰减 / Ridge):惩罚权重的平方。梯度更新变成 w ← w(1−ηλ) − η·grad——每步先把权重缩小一点再学习,所以深度学习里叫 weight decay。效果:权重普遍变小、趋近于 0 但不等于 0,模型变「平滑」,不依赖任何单个特征的极端权重。

L1(Lasso):惩罚绝对值。几何解释最直观:L1 的约束区域是菱形,角在坐标轴上——损失等高线最容易在角点相切,而角点意味着某些权重恰好为 0。所以 L1 产生稀疏解,自带特征选择效果;L2 的约束区域是圆形,无角可切,权重只会趋近零。

from sklearn.linear_model import Lasso, Ridge, LogisticRegression

# 看 L1 的稀疏化效果
lasso = Lasso(alpha=0.1).fit(X, y)
print(f"L1 后非零系数: {(lasso.coef_ != 0).sum()} / {len(lasso.coef_)}")

# 逻辑回归的正则方向是反的:C = 1/λ,C 越小正则越强
lr = LogisticRegression(C=0.1, penalty="l1", solver="liblinear")

λ(或 1/C)是旋钮:太大模型被勒死(欠拟合),太小等于没戴。用交叉验证选。

Dropout:深度学习的随机正则

训练时每个神经元以概率 p(常 0.5/0.2)随机「失活」,每次前向都是一个不同的子网络。两个经典解释:

  • 集成视角:2^n 个共享权重的子网络在轮流训练,推理时用全体(激活缩放),等效于巨大集成模型的平均——集成为什么抗过拟合,集成学习篇讲过。
  • 防共适应视角:神经元不能指望某个特定队友在场,被迫学「独立有用」的特征,而不是小团体内部的暗号。
import torch.nn as nn

model = nn.Sequential(
    nn.Linear(784, 256), nn.ReLU(),
    nn.Dropout(p=0.5),          # 训练随机失活,eval() 自动关闭
    nn.Linear(256, 10))

现代实践的变化:大模型时代 Dropout 用得更少了(LLM 甚至不用)——因为数据量大到一定程度,过拟合风险本身就降了,Dropout 是数据不足时的补偿手段。CNN 里它部分被 BatchNorm 取代(BN 自带轻微正则效果),Transformer 里它仍在 attention 和 FFN 里以 0.1 的小概率存在。

早停:最便宜的正则

监控验证集指标,连续 N 轮不提升就停——训练时长本身就是模型容量:训得越久,记忆越精。早停把「容量」控制在泛化最好的那一刻。

# 手写早停(PyTorch)
best_val, patience, counter = float("inf"), 10, 0
for epoch in range(1000):
    train_one_epoch()
    val_loss = evaluate()
    if val_loss < best_val:
        best_val, counter = val_loss, 0
        torch.save(model.state_dict(), "best.pt")   # 存最优而非最后
    else:
        counter += 1
        if counter >= patience:
            break
model.load_state_dict(torch.load("best.pt"))   # 恢复最优权重

XGBoost/LightGBM 内置 early_stopping_rounds。三个坑:保存的是验证最优点的权重,不是最后一步;patience 要给学习率衰减留空间(衰减后常有第二春,patience 太小会误杀);验证集太小导致指标噪声大时,早停会被噪声触发。

数据即正则:增强与更多数据

正则化不只惩罚,还有「喂」:

  • 数据增强是最优雅的正则——不限制模型,而是告诉它「这些变换不改变标签」:图像翻转裁剪(增强专题)、文本同义替换、音频加噪。本质是把先验知识注入数据。
  • 增加数据是否有帮助,可以先看学习曲线。验证误差仍随样本量下降时,补充同分布、标注可靠的数据值得尝试;如果主要问题是分布偏移、系统性错标或模型欠拟合,仅增加数量可能作用有限。训练与验证误差也不能不加条件地保证汇合。
  • 标签平滑:把硬标签 [0,1,0] 改成 [0.05,0.9,0.05],惩罚「过度自信」,分类任务几乎免费的 +0.5%。

其他常用正则速览

方法限制了什么场景
BatchNorm层输入分布(顺带正则)CNN 标配
梯度裁剪单步更新幅度RNN/Transformer 防梯度爆炸
参数共享/小模型直接砍容量部署受限
Stochastic Depth随机丢整层ResNet 系
Mixup样本间线性插值图像分类
约束参数量(LoRA)只训低秩增量LLM 微调——微调场景的正则

LoRA 值得一提:冻结全部预训练权重、只训千分之一的低秩矩阵——这本身就是一种强正则,所以 LoRA 微调比全量微调更不容易在小数据上过拟合、也不容易遗忘预训练知识。

我的调参决策流程

面对过拟合症状,按这个顺序动:

  1. 先诊断:学习曲线确认是过拟合(训练/验证大 gap)而非其他问题。
  2. 数据侧优先:能加数据加数据,能上增强上增强——不动模型容量的方法没有代价。
  3. 早停打开:零成本,先开了再说。
  4. weight decay 网格:深度学习首选 L2,从 1e-4 起对数网格搜。
  5. Dropout:全连接层 0.2~0.5,CNN/Transformer 慎用或用小值。
  6. 砍容量:以上都试了还不够,说明模型相对数据太大了,换小模型。
  7. 集成收尾:单模型正则到极限后,bagging/集成再榨一层(集成篇)。

踩坑排查

现象原因解法
加了 Dropout 训练 loss 下不去了p 太大 + 模型小降 p 到 0.1~0.2,或先加容量
早停后复现不了最优指标存了最后权重没存最优checkpoint 存 best_val 对应权重
L1 把重要特征也清零了λ 过大对数网格搜 λ,看系数路径
验证集上早停,测试集效果差验证集被「用旧了」(隐性过拟合验证集)早停用 val,最终评估用没碰过的 test
weight decay 和 Adam 一起用没效果Adam 的自适应梯度会缩放 decay 项换 AdamW(decay 与梯度解耦)
训练/验证都差还狂加正则方向反了,这是欠拟合减正则加容量,先看诊断

最后一个坑单独强调:Adam + L2 不等于 AdamW。Adam 里 L2 项会被自适应学习率缩放,正则效果失真;AdamW 把权重衰减从梯度里拆出来独立施加——现代深度学习默认用 AdamW,这是很多「调了 weight decay 没用」的真相。

练习

  1. 在 MNIST 上固定小训练集(2000 张),分别加 {无正则, Dropout, L2, 增强} 训同一 CNN,画四条「训练/验证准确率」曲线对比。
  2. 用 sklearn 的 Lasso 在 diabetes 数据集上扫 alpha,画出「系数路径图」(每个特征的系数随 alpha 的变化),观察特征逐个归零的顺序。
  3. 实现带早停的训练循环,对比「保存最优权重」和「用最终权重」在测试集上的差异。
  4. 复现 Adam vs AdamW 差异:同一网络用两种优化器加相同 weight decay,对比测试准确率和权重范数。

面试常问

Q:L1 和 L2 的几何区别?为什么 L1 稀疏? L1 约束区域是菱形(坐标轴上有尖角),损失等高线与可行域的切点容易落在角上——角即某些坐标为 0。L2 是球面,切点一般不在轴上,权重只能趋近 0。补充:L1 的次梯度在 0 附近恒定(±λ),小权重会被恒定的力推过 0;L2 的梯度 ∝w,权重越小推力越弱,永远差一口气到 0。

Q:Dropout 训练和推理为什么不一致?怎么处理? 训练时每个神经元以 p 概率失活,期望激活是 (1−p)·x;推理时全部激活,数值期望变大 1/(1−p) 倍。两种对齐法:inverted dropout(训练时把存活神经元的激活除以 (1−p),推理不动,PyTorch 的做法);或推理时乘 (1−p)。model.eval() 必须记得调,否则推理也在随机失活,结果不可复现。

Q:BatchNorm 为什么有正则效果? 每个 batch 的均值方差是带噪声的统计量——样本在训练时经历的是「被 batch 同伴抖动过的归一化」,相当于激活上加了和数据相关的噪声,模型不能过度依赖精确的激活值。副作用:小 batch 时统计噪声过大,正则变成伤害——小 batch 场景换 GroupNorm/LayerNorm。

Q:怎么判断该加数据还是加正则? 画学习曲线(训练集大小 vs 训练/验证误差):曲线右端验证误差仍在明显下降 → 数据瓶颈,加数据收益大;验证误差已平稳且与训练误差有稳定 gap → 容量/正则问题,加正则或砍容量。学习曲线是这个判断的唯一可靠依据,拍脑袋不行。

Q:大模型时代正则化变了吗? 变了优先级:数据规模成了主要正则(万亿 token 下记忆不划算),Dropout 淡出,weight decay 保留,早停依然标配;新增「架构正则」(LoRA 低秩约束、MoE 稀疏激活)。不变的是偏差-方差权衡的框架本身——只是现在天平两端是「算力-数据-参数」的三角。

相关阅读

正则化的哲学:最好的模型不是最聪明的,而是知道什么时候该保持简单的。Occham 剃刀在 ML 里不是美学,是数学。

💬 评论

这套方案在哪一步卡住了?也欢迎补充你的实现方式或踩坑经验。

正在连接评论服务,首次访问可能需要几秒…