训练集表现很好,换到验证集却下降,可能是过拟合,也可能是两组数据分布不同。先检查切分和数据质量,再判断是否需要限制模型容量。下面介绍常见正则化方法,并说明应该观察哪些信号来判断它们是否有效。
这篇把主流正则技术一次性讲透:每种方法「限制了什么、为什么有效、什么时候用」。
前置阅读:建议先读 线性回归实战、神经网络基础、ML 数学基础。
偏差-方差权衡:过拟合的本质图景
模型误差可以拆成三块:偏差(模型太简单,系统性学偏)+ 方差(模型太灵活,数据抖一下预测就跟着抖)+ 不可约噪声。
欠拟合 ←————————→ 过拟合
高偏差 高方差
训练差测试差 训练好测试差
最优复杂度在中间:测试误差的 U 型谷底
诊断是第一课:训练误差和验证误差画在同一张图上(学习曲线)——两个都高是欠拟合(加容量/减正则),训练低验证高是过拟合(加正则/加数据)。不诊断就动手是瞎调。
L2 与 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 微调比全量微调更不容易在小数据上过拟合、也不容易遗忘预训练知识。
我的调参决策流程
面对过拟合症状,按这个顺序动:
- 先诊断:学习曲线确认是过拟合(训练/验证大 gap)而非其他问题。
- 数据侧优先:能加数据加数据,能上增强上增强——不动模型容量的方法没有代价。
- 早停打开:零成本,先开了再说。
- weight decay 网格:深度学习首选 L2,从 1e-4 起对数网格搜。
- Dropout:全连接层 0.2~0.5,CNN/Transformer 慎用或用小值。
- 砍容量:以上都试了还不够,说明模型相对数据太大了,换小模型。
- 集成收尾:单模型正则到极限后,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 没用」的真相。
练习
- 在 MNIST 上固定小训练集(2000 张),分别加 {无正则, Dropout, L2, 增强} 训同一 CNN,画四条「训练/验证准确率」曲线对比。
- 用 sklearn 的
Lasso在 diabetes 数据集上扫 alpha,画出「系数路径图」(每个特征的系数随 alpha 的变化),观察特征逐个归零的顺序。 - 实现带早停的训练循环,对比「保存最优权重」和「用最终权重」在测试集上的差异。
- 复现 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 稀疏激活)。不变的是偏差-方差权衡的框架本身——只是现在天平两端是「算力-数据-参数」的三角。
相关阅读
- 线性回归实战——L1/L2 最直观的舞台
- 集成学习:随机森林与 XGBoost——方差缩减的另一条路线
- 图像数据增强——数据即正则的完整展开
- LLM 微调:LoRA 与 QLoRA——低秩约束作为正则
- ML 数学基础——正则项的数学细节
正则化的哲学:最好的模型不是最聪明的,而是知道什么时候该保持简单的。Occham 剃刀在 ML 里不是美学,是数学。
💬 评论
这套方案在哪一步卡住了?也欢迎补充你的实现方式或踩坑经验。