2. 模型过拟合与欠拟合:偏差与方差权衡、正则化技术(L1/L2)、Dropout、早停法

做模型训练,说白了就是在跟两个「敌人」打架——欠拟合和过拟合。我刚开始带项目那会儿,总觉得模型在训练集上跑得越准越好,结果一上线就被现实狠狠教育了一顿。今天咱们就把这两个问题彻底聊透。

2.1 偏差与方差:模型的两副面孔

先看一张图,你马上就能明白。

偏差与方差四象限 低偏差 · 低方差 ✅ 理想模型 训练好 · 泛化好 低偏差 · 高方差 ⚠️ 过拟合 训练好 · 测试差 高偏差 · 低方差 ⚠️ 欠拟合 训练差 · 测试差 高偏差 · 高方差 ❌ 最差情况 哪哪都不行 偏差 → 低 ← 高 方差 → 低 ← 高

偏差衡量的是模型预测的「准不准」,方差衡量的是模型在不同数据集上「稳不稳」。你想想看,一个模型如果偏差高,说明它连训练数据都没学好;如果方差高,说明它学得太死,换个数据就翻车。

核心口诀:

  • 欠拟合 → 偏差高 → 模型太简单,没学到规律
  • 过拟合 → 方差高 → 模型太复杂,记住了噪声

我记得有一次做工业缺陷检测,用了个很深的 ResNet,训练集准确率 99.8%,我高兴得不行。结果一上产线,新样本准确率直接掉到 72%。这就是典型的低偏差高方差——模型把训练集里的光照、角度变化全背下来了,反而没学会真正的缺陷特征。

2.2 正则化技术:给模型戴上「紧箍咒」

正则化说白了就是给损失函数加点惩罚,让模型不敢把权重学得太大。我个人的习惯是,只要模型参数超过 10 万,第一件事就是加正则化。

2.2.1 L1 正则化(Lasso)

L1 在损失函数后面加的是权重的值之和:

Loss = 原始损失 + λ * Σ|w_i|

它有个特别有意思的特性——会让部分权重直接变成 0。为什么?因为 L1 的梯度是常数,每次更新都在往 0 方向拉,拉到 0 就卡住了。

我的经验:L1 适合做特征选择。之前做一个用户画像项目,原始特征 2000 维,用 L1 跑完只剩 120 维,模型小了 90%,推理速度翻倍,精度反而没掉。

2.2.2 L2 正则化(Ridge)

L2 加的是权重的平方和:

Loss = 原始损失 + λ * Σw_i²

L2 不会把权重推到 0,而是让所有权重都变小、变均匀。它的梯度是 2λw,权重越大惩罚越狠,所以大权重会被压下来。

对比项 L1 正则化 L2 正则化
惩罚形式 权重值之和 权重平方和
权重结果 部分权重变为 0 权重整体变小
适用场景 特征选择、稀疏模型 防止过拟合、权重平滑
边缘端优势 模型体积小、速度快 数值稳定、推理一致

避坑指南:λ 值别拍脑袋设。我曾经在一个项目里把 λ 设成 0.1,结果模型直接废了——所有输出都接近 0。建议从 1e-4 开始试,每次乘 10 往上调,观察验证集损失的变化。

2.3 Dropout:随机「开除」神经元

Dropout 的思路特别巧妙——训练时随机让一部分神经元不干活。你想想看,如果每个神经元都知道自己随时可能被「开除」,它就不敢过度依赖别人,必须自己学会提取有效特征。

# 一个简单的 Dropout 实现示意
import numpy as np

def dropout_forward(x, keep_prob=0.5):
    mask = np.random.binomial(1, keep_prob, size=x.shape) / keep_prob
    return x * mask

注意那个 / keep_prob,这叫「缩放补偿」。训练时只保留了 keep_prob 比例的神经元,推理时所有神经元都工作,如果不缩放,输出值会翻倍。

参数经验值:

  • 全连接层:keep_prob 通常设 0.5
  • 卷积层:keep_prob 可以设 0.7~0.9(卷积层参数少,不需要太强正则)
  • 输入层:keep_prob 设 0.8 以上,别丢太多原始信息

我做过一个对比实验:同样的 CNN 网络,不加 Dropout 时验证集准确率 83%,加 0.5 Dropout 后升到 89%。而且有意思的是,训练时间几乎没增加——Dropout 的计算量可以忽略不计。

2.4 早停法:见好就收

早停法是我在项目里用得最多的技巧,没有之一。它的逻辑很简单:训练过程中监控验证集损失,一旦连续 N 轮不下降,就停下来。

# 早停法伪代码
patience = 10
best_loss = inf
wait = 0

for epoch in range(max_epochs):
    train_loss = train_one_epoch()
    val_loss = validate()
    
    if val_loss < best_loss:
        best_loss = val_loss
        save_checkpoint()
        wait = 0
    else:
        wait += 1
        if wait >= patience:
            print("触发早停,加载最佳模型")
            load_checkpoint()
            break

参数怎么设?

  • patience(耐心值):小数据集设 5~10,大数据集设 10~20
  • min_delta(最小改善阈值):设 1e-4,避免微小波动干扰
  • restore_best_weights:一定要设为 True,否则最后保存的可能是过拟合的模型

我曾经犯过一个低级错误——没保存最佳权重,早停后直接用了最后一轮的模型。结果验证集损失虽然没涨,但模型已经开始过拟合了。从那以后,我每次都在代码里加 restore_best_weights=True,成了肌肉记忆。

2.5 实战组合拳:怎么选、怎么搭

在实际项目中,我不会只用一种方法。下面是我常用的组合策略:

  1. 先加 L2 正则化,λ 从 1e-4 开始试
  2. 再加 Dropout,全连接层用 0.5,卷积层用 0.8
  3. 开启早停,patience 设 10,监控验证集损失
  4. 如果模型太大,考虑用 L1 做特征选择,压缩模型体积

边缘端部署特别提醒:

  • L1 正则化能产生稀疏权重,配合剪枝可以进一步压缩模型
  • Dropout 在推理时一定要关闭(大部分框架自动处理)
  • 早停法在边缘端训练时尤其重要——设备资源有限,不能无限跑