过拟合与正则化

过拟合是深度学习中最常见也最关键的问题——模型在训练集上表现极好但在测试集上性能急剧下降,本质上是模型“记住了噪声而非规律”。应对手段形成了一整套正则化工具箱:L2 正则化(Weight Decay)在损失函数中加入 λ‖w‖² 使权重趋近于零,等价于对参数施加高斯先验,是 Adam 优化器的标配(AdamW 专门解耦了 weight decay);L1 正则化产生稀疏权重,可用于特征选择;Dropout 随机关闭一部分神经元(p=0.1~0.5),强迫网络学习冗余表示——BERT、GPT 中广泛使用;Early Stopping 监控验证集 loss,在开始上升时停止训练,是最简单有效的防过拟合策略;数据增强(Mixup、CutMix、RandAugment)通过合成新样本扩大训练集;Label Smoothing 将 hard label 软化为 (1-ε)·one_hot + ε/K,防止模型对输出过度自信——GPT、ViT 等模型均使用 ε=0.1。本节逐一讲解每种正则化技术的数学原理,并用代码验证其效果。

一、偏差-方差权衡

预测误差的分解:
E[(y - ŷ)²] = Bias² + Variance + σ²_noise

偏差(Bias):模型预测的平均值与真实值的偏离
Bias = E[ŷ] - y_true(模型系统性地偏高或偏低)

方差(Variance):模型预测因训练数据改变而产生的波动
Var = E[(ŷ - E[ŷ])²](不同训练集训练出的模型预测差异多大)

不可约误差:数据本身的随机噪声 σ²,任何模型都无法消除
具体例子——多项式回归拟合:
真实函数:y = sin(x) + 噪声
在区间 [0, 2π] 上有 10 个训练点

模型参数量训练 MSE测试 MSE偏差方差状态
线性 (1 次)20.4520.489欠拟合
3 次多项式40.0890.105刚好
5 次多项式60.0420.098略高轻微过拟合
9 次多项式100.0012.156极低极高严重过拟合
15 次多项式16≈085.3≈0爆炸灾难性过拟合
关键观察:
• 1 次:训练和测试 MSE 都高 → 模型太简单,无法捕捉 sin 曲线
• 3 次:训练 MSE 适中,测试 MSE 接近 → 最佳平衡
• 9 次:训练 MSE 极低但测试暴涨 → 完美拟合了训练点(包括噪声)
• 15 次:参数比数据点还多 → 可以精确穿过每个点,但在点之间剧烈摇摆

二、过拟合的判断方法

学习曲线分析(最直接的方法):

Epoch 1: Train Loss=2.45, Val Loss=2.52 (差距小→正常)
Epoch 10: Train Loss=0.82, Val Loss=0.91 (差距小→正常)
Epoch 30: Train Loss=0.23, Val Loss=0.35 (开始拉开差距)
Epoch 50: Train Loss=0.08, Val Loss=0.42 (差距持续增大)
Epoch 80: Train Loss=0.02, Val Loss=0.58 (验证集损失上升→过拟合!)
Epoch 100: Train Loss=0.01, Val Loss=0.71 (严重过拟合)

判断标准:
① Train Loss 持续下降但 Val Loss 开始上升 → 经典过拟合信号
② Val Loss 停滞不前但 Train Loss 继续降 → 早期过拟合
③ Train Accuracy=99% 而 Val Accuracy=72% → 过拟合(差距>15%要警惕)
④ 训练集与测试集性能差距越大 → 过拟合越严重

三、数据集划分

三集划分:
训练集(Training Set):70-80%,用于训练模型
验证集(Validation Set):10-15%,用于调超参数
测试集(Test Set):10-15%,最终评估(只用一次)

K 折交叉验证(数据量小时):
将数据分为 K 份,轮流用 1 份做验证,其余做训练
最终性能 = K 次结果的平均值
5 折交叉验证完整示例:
数据集:1000 个样本

Fold 1: 训练 [201-1000], 验证 [1-200] → Val Acc = 91.0%
Fold 2: 训练 [1-200,401-1000], 验证 [201-400] → Val Acc = 89.5%
Fold 3: 训练 [1-400,601-1000], 验证 [401-600] → Val Acc = 92.0%
Fold 4: 训练 [1-600,801-1000], 验证 [601-800] → Val Acc = 90.0%
Fold 5: 训练 [1-800], 验证 [801-1000] → Val Acc = 91.5%

平均 Accuracy = (91.0+89.5+92.0+90.0+91.5)/5 = 90.8% ± 0.89%
标准差 = 0.89%(说明不同划分影响不大→模型较稳定)

四、L2 正则化(权重衰减)

L2 正则化目标函数:
J(w) = L(w) + (λ/2) × Σⱼ wⱼ²

梯度更新规则:
∂J/∂w = ∂L/∂w + λw
w ← w - η(∂L/∂w + λw) = (1-ηλ)w - η∂L/∂w

每次更新都将权重缩小为 (1-ηλ) 倍,所以叫"权重衰减"
数值示例——L2 正则化的效果:
学习率 η = 0.01,正则化系数 λ = 0.1
当前权重 w = [2.5, -1.8, 0.3, 3.1, -0.1]
损失梯度 ∂L/∂w = [0.5, -0.3, 0.1, 0.8, -0.05]

无正则化的更新:
w₁_new = 2.5 - 0.01×0.5 = 2.5 - 0.005 = 2.495
w₄_new = 3.1 - 0.01×0.8 = 3.1 - 0.008 = 3.092

L2 正则化的更新:
w₁_new = 2.5 - 0.01×(0.5 + 0.1×2.5) = 2.5 - 0.01×0.75 = 2.5 - 0.0075 = 2.4925
w₂_new = -1.8 - 0.01×(-0.3 + 0.1×(-1.8)) = -1.8 - 0.01×(-0.48) = -1.8 + 0.0048 = -1.7952
w₃_new = 0.3 - 0.01×(0.1 + 0.1×0.3) = 0.3 - 0.01×0.13 = 0.3 - 0.0013 = 0.2987
w₄_new = 3.1 - 0.01×(0.8 + 0.1×3.1) = 3.1 - 0.01×1.11 = 3.1 - 0.0111 = 3.0889
w₅_new = -0.1 - 0.01×(-0.05 + 0.1×(-0.1)) = -0.1 - 0.01×(-0.06) = -0.1 + 0.0006 = -0.0994

观察:
• |w₄|=3.1 最大 → 额外惩罚 0.01×0.31=0.0031 最大(大权重被压得更厉害)
• |w₅|=0.1 最小 → 额外惩罚几乎为零
• L2 不会将权重压缩到 0,但会持续使其变小

五、L1 正则化(稀疏化)

J(w) = L(w) + λ × Σⱼ |wⱼ|
∂J/∂w = ∂L/∂w + λ × sign(w)
其中 sign(w) = +1 (w>0), -1 (w<0), 0 (w=0)
L1 vs L2 对比——特征选择效果:
初始权重 w = [2.5, -1.8, 0.3, 3.1, -0.1],η=0.01, λ=0.1

L1 更新:惩罚项 = λ×sign(w)
w₃: 0.3 - 0.01×(0.1 + 0.1×1) = 0.3 - 0.002 = 0.298
w₅: -0.1 - 0.01×(-0.05 + 0.1×(-1)) = -0.1 - 0.01×(-0.15) = -0.1 + 0.0015 = -0.0985

经过 50~100 步迭代后:
L2: w ≈ [1.2, -0.9, 0.15, 1.5, -0.05](所有权重变小但不为0)
L1: w ≈ [1.4, -1.0, 0.00, 1.8, 0.00](小权重被压成 0→稀疏!)

稀疏的含义:w₃=0 和 w₅=0 意味着模型自动丢弃了第 3 和第 5 个特征→自动特征选择

六、Dropout

训练时:以概率 p 随机"关闭"每个神经元(输出置零)
测试时:所有神经元都打开,输出乘以 (1-p) 或训练时除以 (1-p)

直觉:迫使网络不要过度依赖任何单个特征
概率解释:相当于同时训练 2ⁿ 个子网络的集成
Dropout 前向传播完整计算(Inverted Dropout):

层输入:h = [0.8, -0.5, 1.2, 0.3, -0.9, 0.6](6 个神经元)
Dropout 率 p = 0.5(50% 的神经元被关闭)

训练阶段:
随机生成 mask(每个元素独立 Bernoulli(0.5)):
mask = [1, 0, 1, 0, 1, 0](关闭了第 2,4,6 个神经元)

应用 mask 并缩放(除以保留概率 1-p = 0.5):
h_drop = h × mask / (1-p)
= [0.8×1, -0.5×0, 1.2×1, 0.3×0, -0.9×1, 0.6×0] / 0.5
= [0.8, 0, 1.2, 0, -0.9, 0] / 0.5
= [1.6, 0, 2.4, 0, -1.8, 0]

测试阶段(不做 Dropout):
h_test = [0.8, -0.5, 1.2, 0.3, -0.9, 0.6](原样输出)

期望等价性验证:
训练时 E[h_drop₁] = 0.8 × P(mask=1) / 0.5 = 0.8 × 0.5 / 0.5 = 0.8
测试时 h_test₁ = 0.8
→ 训练和测试的期望输出一致 ✓
Dropout 在不同模型中的使用指南:

位置典型 Dropout 率说明
全连接层之间0.5最常用位置,正则化效果最强
CNN 卷积层之后0.1~0.3卷积本身有权值共享,不需要太多
RNN/LSTM 层间0.2~0.5时间步内不 Dropout,只在层间
Transformer Attention0.1Attention 分数上的 Dropout
输入层0.1~0.2模拟输入噪声/缺失特征
输出层不使用最终预测不应随机

七、Early Stopping(早停)

策略:在验证损失不再下降时停止训练

完整示例(patience=5):
Epoch 1: Val=1.82 best=1.82 wait=0 →保存
Epoch 5: Val=0.95 best=0.95 wait=0 →保存
Epoch 10: Val=0.52 best=0.52 wait=0 →保存
Epoch 15: Val=0.38 best=0.38 wait=0 →保存(最优!)
Epoch 16: Val=0.40 best=0.38 wait=1
Epoch 17: Val=0.39 best=0.38 wait=2
Epoch 18: Val=0.41 best=0.38 wait=3
Epoch 19: Val=0.43 best=0.38 wait=4
Epoch 20: Val=0.45 best=0.38 wait=5 → STOP! 恢复 Epoch 15 的权重

如果继续训练:
Epoch 50: Train=0.01, Val=0.72(已经严重过拟合了)

Early Stopping 节省了 30 个 Epoch 的无用训练,并获得了最佳泛化模型。

八、数据增强

图像数据增强(以 CIFAR-10 为例):

原始:32×32 猫咪图片

增强方法操作数学描述效果
随机水平翻转P=0.5 左右镜像I'(x,y) = I(W-1-x,y)翻倍有效数据量
随机裁剪先 Pad 4px 后裁原大小36×36→随机裁出32×32平移不变性
颜色抖动亮度/对比度/饱和度随机±20%I' = α×I + β光照不变性
随机旋转±15°旋转矩阵变换旋转不变性
随机擦除随机遮挡一块区域区域→灰色/噪声类似 Dropout
Mixup两张图混合I' = λI₁+(1-λ)I₂决策边界平滑
CutMix用另一图的矩形区域替换局部替换信息更完整
Mixup 数值示例:
图片 A(猫)像素 = [0.8, 0.5, 0.3], 标签 = [1, 0]
图片 B(狗)像素 = [0.2, 0.7, 0.6], 标签 = [0, 1]
λ = 0.7(从 Beta(α,α) 分布采样,α 常取 0.2)

混合图片 = 0.7×[0.8,0.5,0.3] + 0.3×[0.2,0.7,0.6] = [0.62, 0.56, 0.39]
混合标签 = 0.7×[1,0] + 0.3×[0,1] = [0.7, 0.3]

九、Batch Normalization 的正则化效果

BN 的意外发现:有正则化效果!

原因:每个 mini-batch 的均值和方差是随机的(因为 batch 的随机采样)
→ 标准化过程引入了噪声 → 类似 Dropout 的效果

对比实验(ResNet-18 on CIFAR-10):
配置Train AccVal Acc差距
无 BN,无 Dropout99.8%91.2%8.6%
有 BN,无 Dropout99.5%93.8%5.7%
无 BN,有 Dropout(0.5)98.2%93.1%5.1%
有 BN + Dropout(0.2)98.8%94.5%4.3%

十、正则化强度的选择

λ(L2/L1 正则化系数)的调参:

以 MNIST 分类(2 层 MLP,256 隐藏单元)为例:
λTrain AccVal Acc权重范数 ||w||₂状态
0(无正则化)99.9%97.2%45.8轻微过拟合
0.000199.7%97.8%32.1改善
0.00199.2%98.1%18.5最佳
0.0197.5%97.3%8.2正则化偏强
0.192.1%91.8%2.1欠拟合
1.085.3%85.0%0.3严重欠拟合
最佳 λ=0.001:Val Acc 最高 98.1%,权重范数适中 18.5
Train-Val 差距仅 1.1%(良好泛化)

十一、正则化方法总结

方法原理超参数常用值适用场景
L2 正则化约束权重大小λ1e-4 ~ 1e-2几乎所有模型
L1 正则化稀疏化权重λ1e-5 ~ 1e-3特征选择
Dropout随机关闭神经元p0.1~0.5全连接层
Early Stopping在过拟合前停止patience5~20所有训练任务
数据增强增加数据多样性增强强度任务相关图像/文本/音频
BatchNorm标准化+噪声momentum0.1CNN/MLP
Label Smoothing软化标签ε0.1分类任务
Label Smoothing 数值示例:

10 分类任务,ε = 0.1:
硬标签:y = [0, 0, 0, 1, 0, 0, 0, 0, 0, 0]
软标签:y' = (1-ε)×y + ε/K = 0.9×y + 0.01
y' = [0.01, 0.01, 0.01, 0.91, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01]

效果:模型不再试图输出极端概率 [0,...,1,...,0]
→ 减少过自信 → 改善泛化 → 特别适合知识蒸馏

十二、代码验证(C# / Rust)

C#(.NET 10)

// dotnet run 即可执行
// ===== L2 / L1 正则化效果 =====
double[] w = { 2.5, -1.8, 0.3, 3.1, -0.1 };
double[] grad = { 0.5, -0.3, 0.1, 0.8, -0.05 };
double eta = 0.01, lam = 0.1;

Console.Write("无正则化: ");
for (int i = 0; i < 5; i++) Console.Write($"{w[i] - eta * grad[i],8:F4}");
Console.WriteLine();

Console.Write("L2 正则化:");
for (int i = 0; i < 5; i++) Console.Write($"{w[i] - eta * (grad[i] + lam * w[i]),8:F4}");
Console.WriteLine();

Console.Write("L1 正则化:");
for (int i = 0; i < 5; i++) Console.Write($"{w[i] - eta * (grad[i] + lam * Math.Sign(w[i])),8:F4}");
Console.WriteLine();

// ===== Dropout 前向传播 =====
double[] h = { 0.8, -0.5, 1.2, 0.3, -0.9, 0.6 };
double pDrop = 0.5;
int[] mask = { 0, 1, 1, 1, 0, 0 }; // 模拟随机掩码 (seed=42)
Console.WriteLine($"\nmask = [{string.Join(", ", mask)}]");
Console.Write("Dropout 输出 = [");
for (int i = 0; i < 6; i++)
{
    if (i > 0) Console.Write(", ");
    Console.Write($"{h[i] * mask[i] / (1 - pDrop):F2}");
}
Console.WriteLine($"]\n期望输出均值 = {h.Average():F3}");

// ===== Early Stopping 模拟 =====
int patience = 5, wait = 0, bestEpoch = 0;
double bestVal = double.MaxValue;
for (int epoch = 0; epoch < 50; epoch++)
{
    double val = 2.5 * Math.Exp(-0.06 * epoch) + 0.1 + 0.005 * Math.Max(0, epoch - 15);
    if (val < bestVal) { bestVal = val; bestEpoch = epoch; wait = 0; }
    else if (++wait >= patience)
    {
        Console.WriteLine($"\nEarly Stopping at epoch {epoch}");
        Console.WriteLine($"Best epoch: {bestEpoch}, Best val loss: {bestVal:F4}");
        break;
    }
}

// ===== Mixup 数据增强 =====
double[] imgA = { 0.8, 0.5, 0.3 }, imgB = { 0.2, 0.7, 0.6 };
double[] labelA = { 1, 0 }, labelB = { 0, 1 };
double lamMix = 0.7;
Console.Write("\nMixup 图片: [");
for (int i = 0; i < 3; i++)
{
    if (i > 0) Console.Write(", ");
    Console.Write($"{lamMix * imgA[i] + (1 - lamMix) * imgB[i]:F2}");
}
Console.Write("]\nMixup 标签: [");
for (int i = 0; i < 2; i++)
{
    if (i > 0) Console.Write(", ");
    Console.Write($"{lamMix * labelA[i] + (1 - lamMix) * labelB[i]:F1}");
}
Console.WriteLine("]");

// ===== Label Smoothing =====
int K = 10; double eps = 0.1;
Console.Write("\n硬标签: [");
for (int i = 0; i < K; i++) { if (i > 0) Console.Write(", "); Console.Write(i == 3 ? "1" : "0"); }
Console.Write("]\n软标签: [");
for (int i = 0; i < K; i++)
{
    if (i > 0) Console.Write(", ");
    double hard = i == 3 ? 1.0 : 0.0;
    Console.Write($"{(1 - eps) * hard + eps / K:F2}");
}
Console.WriteLine("]");

Rust

fn main() {
    // ===== L2 / L1 正则化效果 =====
    let w = [2.5_f64, -1.8, 0.3, 3.1, -0.1];
    let grad = [0.5_f64, -0.3, 0.1, 0.8, -0.05];
    let (eta, lam) = (0.01_f64, 0.1);

    print!("无正则化: ");
    for i in 0..5 { print!("{:8.4}", w[i] - eta * grad[i]); }
    println!();
    print!("L2 正则化:");
    for i in 0..5 { print!("{:8.4}", w[i] - eta * lam.mul_add(w[i], grad[i])); }
    println!();
    print!("L1 正则化:");
    for i in 0..5 { print!("{:8.4}", w[i] - eta * lam.mul_add(w[i].signum(), grad[i])); }
    println!();

    // ===== Dropout 前向传播 =====
    let h = [0.8_f64, -0.5, 1.2, 0.3, -0.9, 0.6];
    let p_drop = 0.5_f64;
    let mask = [0.0_f64, 1.0, 1.0, 1.0, 0.0, 0.0]; // 模拟随机掩码
    println!("\nmask = {:?}", mask.map(|v| v as i32));
    print!("Dropout 输出 = [");
    for i in 0..6 {
        if i > 0 { print!(", "); }
        print!("{:.2}", h[i] * mask[i] / (1.0 - p_drop));
    }
    let h_mean: f64 = h.iter().sum::<f64>() / h.len() as f64;
    println!("]\n期望输出均值 = {h_mean:.3}");

    // ===== Early Stopping 模拟 =====
    let (patience, mut wait, mut best_epoch) = (5_i32, 0_i32, 0_i32);
    let mut best_val = f64::MAX;
    for epoch in 0..50 {
        let val = 2.5 * (-0.06 * epoch as f64).exp()
                  + 0.1 + 0.005 * (epoch - 15).max(0) as f64;
        if val < best_val { best_val = val; best_epoch = epoch; wait = 0; }
        else { wait += 1; if wait >= patience {
            println!("\nEarly Stopping at epoch {epoch} (patience={patience})");
            println!("Best epoch: {best_epoch}, Best val loss: {best_val:.4}");
            break;
        }}
    }

    // ===== Mixup 数据增强 =====
    let img_a = [0.8, 0.5, 0.3_f64];
    let img_b = [0.2, 0.7, 0.6_f64];
    let label_a = [1.0, 0.0_f64];
    let label_b = [0.0, 1.0_f64];
    let lam_mix = 0.7_f64;
    print!("\nMixup 图片: [");
    for i in 0..3 {
        if i > 0 { print!(", "); }
        print!("{:.2}", lam_mix.mul_add(img_a[i], (1.0 - lam_mix) * img_b[i]));
    }
    print!("]\nMixup 标签: [");
    for i in 0..2 {
        if i > 0 { print!(", "); }
        print!("{:.1}", lam_mix.mul_add(label_a[i], (1.0 - lam_mix) * label_b[i]));
    }
    println!("]");

    // ===== Label Smoothing =====
    let (k, eps) = (10, 0.1_f64);
    print!("\n硬标签: [");
    for i in 0..k { if i > 0 { print!(", "); } print!("{}", if i == 3 { 1 } else { 0 }); }
    print!("]\n软标签: [");
    for i in 0..k {
        if i > 0 { print!(", "); }
        let hard = if i == 3 { 1.0 } else { 0.0 };
        print!("{:.2}", (1.0 - eps).mul_add(hard, eps / k as f64));
    }
    println!("]");
}

已阅读当前小节,可返回首页继续浏览其它主题。