模型在训练集上越来越准,验证集却没什么变化。这时候该加数据、换模型,还是调正则化(regularization)?先别急着选。曲线告诉你发生了什么,但不一定告诉你为什么。

这一篇把过拟合(overfitting)、欠拟合、数据泄漏和分布变化分开看,再算清 L1、L2 和 Dropout 实际改了什么。需要先了解 loss 和梯度;没接触过可以从线性模型到神经网络读起。

先确认比较的是同一件事

下面是教学用的分类错误率,不是某次真实实验:

情况 训练错误率 验证错误率 第一件值得检查的事
A 2% 18% 切分、泄漏、分布差异,再检查过拟合
B 24% 25% 训练是否跑通,特征是否有信号,模型是否欠拟合
C 1% 1% 是真的简单,还是重复样本跨了集合?
D 8% 5% 训练开了数据增强 / Dropout,验证没有?

泛化差距(generalization gap)不是病因诊断。 训练只见过白天照片,验证全是夜景,不能仅凭两条曲线就断言模型记住了噪声。训练 loss 包含正则项、验证 loss 不包含,也不能直接作差。

比较时先统一任务指标、样本权重、模型模式和预处理;训练目标本身可以另外记录。model.eval() 会改变 Dropout / BatchNorm 的行为,但不等于关闭自动求导。

怎么区分过拟合、欠拟合和没训好

过拟合(overfitting)指模型对训练样本的适应没有转化成同分布新样本上的表现。欠拟合(underfitting)则是现有模型或训练设置连数据里的规律都没学好。它们不是“大模型”和“小模型”的别名。

一个实用的排查顺序:

  1. 先让一小批干净数据被记住。 暂时关增强和强正则,检查标签、loss、梯度、optimizer 里的参数是否正确。记不住先查实现,不立刻加宽网络。
  2. 确认切分符合要回答的问题。 想预测未来就按时间切;想泛化到新用户,就避免同一个人同时进入训练与验证。
  3. 再画训练曲线和学习曲线。 前者横轴是更新次数,后者横轴是训练数据量;两者回答不同问题。
  4. 只改变一个主要因素。 多训练、增加容量、加数据、调正则分别试,保持评估集合不动。

小 batch 能被记住只是训练链路的检查,不是泛化证明。更多数据也不保证有用:一万份重复记录未必比一千份独立、有代表性的记录提供更多信息。

验证集负责选择,测试集负责最后检查

Early stopping 根据验证指标选择 checkpoint;交叉验证(cross-validation)估计不同切分下的表现、帮助选超参数。轮流换一折留作验证,本身并不会让某一个模型变好。

如果每次看到测试分数不好都回来改方案,这个测试集实际上也参与了选择(公开 benchmark 上的同类问题,见测试时计算的污染与饱和)。需要另留最终检验的数据;样本相关时还要使用分组或时间切分,而不是机械地随机分成 K 份。

预处理也属于学习过程。假设训练特征是 [0, 2],未来样本是 100:训练均值为 1,若提前把未来样本一起算进去,均值会变成 34。即使没读取未来标签,训练流程也用到了本来不可见的分布信息。应该只在训练部分 fit,再用同一变换 transform 验证与测试。交叉验证时,每个 fold 也要单独拟合预处理。scikit-learn 的数据泄漏说明给出了相同原则。

L1 和 L2:用一个参数就能看出区别

把数据损失简化成 $\frac12(w-a)^2$,其中 $a$ 是不加正则时偏好的参数值。

平方 L2 惩罚写成 $\frac\lambda2 w^2$,最优解是:

$$w_{L2}=\frac{a}{1+\lambda}.$$

它把参数往 0 拉,但有限的 $\lambda$ 通常不会把非零的 $a$ 直接变成 0。注意这里是平方范数;$\lambda\lVert w\rVert_2$ 是另一个目标,不能混写。

L1 惩罚为 $\lambda|w|$。在 0 处用次梯度(subgradient)处理,可得软阈值(soft thresholding):

$$w_{L1}=\operatorname{sign}(a)\max(|a|-\lambda,0).$$

例如 $a=0.4,\lambda=0.5$:L1 得到 0,平方 L2 得到约 0.267。这解释了 L1 为什么可能产生精确的零;它不代表被删掉的特征在业务上一定没用,相关特征之间也可能互相替代。

动画 L1 为什么能停在 0

这张图需要启用 JavaScript。

import math

def regularized_scalar(target, strength, kind):
    if not math.isfinite(target) or not math.isfinite(strength) or strength < 0:
        raise ValueError("finite target and nonnegative strength required")
    if kind == "l1":
        return math.copysign(max(abs(target) - strength, 0.0), target)
    if kind == "squared_l2":
        return target / (1.0 + strength)
    raise ValueError("unknown penalty")

特征单位会影响惩罚:同样的预测函数,把一个特征放大 100 倍,相应权重可以缩小 100 倍。正则参数相同,不代表约束相同。先明确尺度、截距是否参与惩罚,以及哪些参数组不做 weight decay。

Dropout:保持期望,不是保持每次输出

常见 inverted dropout 在训练时令 $m\sim\operatorname{Bernoulli}(1-p)$,输出为

$$\tilde h=\frac{m h}{1-p},\qquad 0\le p<1.$$

当 $h=4,p=0.5$,输出一半时候是 0,一半时候是 8,均值仍为 4。推理时直接用 4,不再额外乘保留概率。PyTorch Dropout采用这种缩放约定。

但后面如果接平方函数,训练时的期望是 $(0^2+8^2)/2=32$,推理时是 $4^2=16$。所以“保留激活期望”不等于“完整非线性网络等于所有子网络输出的精确平均”。它是训练噪声和约束,不是免费得到一个严格等价的 ensemble。

Dropout 也不是永久删除神经元;每次训练调用重新采样。是否有帮助要看数据和模型,不是越大越稳。强 Dropout、小数据 SFT 和原有预训练(pre-training)分布之间的取舍,需要实际验证。

预处理和网络里的 Norm 别混在一起

操作 统计从哪里来 需要留意什么
Min–max scaling 训练特征的最小 / 最大值 新样本可以超出原来的 [0,1];常数列需要单独处理
Z-score 训练特征的均值与标准差 除以标准差,不是方差(variance);不会把任意分布变成高斯
BatchNorm 训练 batch 统计;默认推理使用 running statistics 统计量不是靠 optimizer 学出来的 gamma / beta
LayerNorm / RMSNorm 当前样本指定维度的统计 不是一组对所有输入固定的线性变换

非退化的固定仿射变换可以保留一维分布的形状,但数值、均值、方差已经变了,不能说“分布完全不变”。模型中的 Norm 还有输入依赖的统计量,含义更不一样。详细轴与公式见归一化。

回到一个真正的训练决定

发现验证差距后,不必把所有修法一起堆上去。先固定合理切分和指标,做一个小对照:基线(baseline)、只加数据、只调正则、只改容量。记录每项的训练与验证表现、计算成本及波动。

如果只在某类样本上失败,先看覆盖与标签;如果训练本身不下降,再去看激活与初始化和优化器。这样每一次改动都有要检验的猜想,而不是看到 gap 就默认“再加一点 Dropout”。

本页术语 5
正则化 regularization
通过约束、惩罚或训练方式减少过拟合,让模型更好地处理新数据
过拟合 overfitting
训练集越来越好,没见过的数据反而变差
预训练 pre-training
先用大量数据学习通用表示和能力,再适配具体任务
方差 variance
估计量的波动幅度;重要性加权最容易在这里爆炸
基线 baseline
用来比较新方法的参考方法或配置