查看全课大纲(第 34 / 93 节)
  1. 1.计算机视觉概述
  2. 2.计算机视觉应用场景
  3. 3.困难与挑战
  4. 4.开源库介绍
  5. 5.图像显示与存储
  6. 6.CLAHE
  7. 7.腐蚀膨胀与开闭运算
  8. 8.滤波与边缘填充
  9. 9.各种滤波
  10. 10.图像金字塔
  11. 11.傅里叶变换
  12. 12.短时傅里叶变换与小波变换
  13. 13.颜色特征与颜色直方图
  14. 14.几何特征_边缘
  15. 15.几何特征-角点与斑点
  16. 16.局部特征-SIFT特征描述子
  17. 17.局部特征-SURF特征描述子
  18. 18.局部特征-ORB特征描述子
  19. 19.其他特征描述子
  20. 20.传统图像分割技术
  21. 21.GraphCut
  22. 22.GrabCut
  23. 23.人脸检测的传统方法
  24. 24.行人检测的传统方法HOG+SVM
  25. 25.行人检测DPM
  26. 26.人工神经网络的架构
  27. 27.目标函数与梯度下降
  28. 28.误差反向传播
  29. 29.可手算的神经网络
  30. 30.改进目标函数与克服梯度消散
  31. 31.神经网络之传统神经网络与深度网络的差异
  32. 32.各种梯度下降算法
  33. 33.如何选择合适的梯度下降算法
  34. 34.避免过适应
  35. 35.CNN的架构
  36. 36.CNN的误差反向传播_卷积层
  37. 37.CNN的误差反向传播_池化层
  38. 38.分类所用的数据集
  39. 39.Alexnet详解
  40. 40.VGG详解
  41. 41.GoogLeNet_v1
  42. 42.BatchNormalization
  43. 43.GoogLeNet_v3v4
  44. 44.ResNet
  45. 45.ResNeXt
  46. 46.CNN设计准则
  47. 47.图像检索与推土机距离
  48. 48.基于颜色相似性
  49. 49.基于形状相似性
  50. 50.基于纹理相似性
  51. 51.基于局部特征相似性
  52. 52.DL的二值哈希编码
  53. 53.高维空间中检索KDTree
  54. 54.高维空间中检索LSH
  55. 55.目标识别任务综述
  56. 56.RCNN
  57. 57.SPP-Net
  58. 58.Fast R-CNN
  59. 59.Faster R-CNN
  60. 60.R-CNN系列模型回顾
  61. 61.R-FCN的设计思路
  62. 62.R-FCN的实现
  63. 63.YOLOv1
  64. 64.YOLOv2_9000
  65. 65.YOLOv3
  66. 66.图像分割应用场景与数据集
  67. 67.上采样的实现方式
  68. 68.FCN网络架构的由来
  69. 69.FCN构建细节
  70. 70.DeepLabv1
  71. 71.CRF
  72. 72.DeepLabv2
  73. 73.DeepLabv3
  74. 74.特定的医学影像分割任务
  75. 75.U-Net
  76. 76.3D U-Net
  77. 77.V-Net
  78. 78.DenseNet
  79. 79.病理切片分析任务及数据集
  80. 80.病理切片分析
  81. 81.图说任务及其传统处理方法
  82. 82.深度语言模型与RNN
  83. 83.LSTM及其变种
  84. 84.NIC模型
  85. 85.SAT模型
  86. 86.图说的性能指标
  87. 87.判别模型与生成模型
  88. 88.KL散度与JS散度
  89. 89.VAE
  90. 90.GAN的基本原理
  91. 91.DCGAN与CGAN
  92. 92.Wasserstein GAN
  93. 93.SRGAN

避免过适应

小象实战讲义 · 基于深度学习的计算机视觉

在深度学习的征途中,我们不仅希望模型在训练集上表现优异,更期待它在从未见过的数据上也能做出准确的判断。然而,一个强大的、参数众多的网络,很容易陷入“过适应”的陷阱——它记住了训练数据的“个性”而非“共性”,导致泛化能力急剧下降。本节将为你揭示过适应的本质,并传授三种核心的“防过拟合”利器:早期停止、权重衰减和Dropout,让你构建的模型既强大又稳健。

💡 核心导读

  • 过适应的本质:模型因参数过多、样本不足而学习了训练数据中的噪声和“个性”特征,导致泛化能力差。
  • 早期停止(Early Stop):通过监控验证集误差,在模型开始“学坏”时果断停止训练。
  • 权重衰减(Weight Decay):在梯度下降更新中引入衰减项,让“无用”的权重逐渐归零,简化网络结构。
  • Dropout:在训练时随机“丢弃”部分神经元,迫使网络学习冗余、鲁棒的特征,本质是一种集成学习。

过适应:深度学习的常见陷阱

在深度学习,尤其是图像分类任务中,过适应是一个普遍且严重的问题。它的一个经典表现是:模型对训练集中添加了人眼几乎无法察觉的细微噪声的图片,会做出完全错误的判断。

这引发了深刻的思考:学习机到底学到了什么?它判断类别差异的标准,和我们人类的视觉感知是一致的吗?显然,如果模型因为一点噪声就“失明”,说明它很可能抓住了一些非常特殊、非本质的“个性”特征,而不是我们期望的、具有普适性的“共性”特征。

过适应的根本原因可以归结为两点:

  1. 模型容量过大:深度神经网络拥有海量的权重参数,表达能力极强。
  2. 样本量相对不足:尽管我们的数据集可能很大,但相对于网络的复杂程度,有效样本量本质上是不足的。

这种“大网络、小数据”的错配,使得网络有足够的能力去“死记硬背”训练样本,甚至包括其中的噪声,而不是去学习数据背后真正的规律。

应对策略一:早期停止训练

早期停止是最直观、也最容易想到的一种避免过适应的方法。其核心思想是:不能一味地追求训练集误差的无限减小。

我们通过一张图来理解这个过程:

训练轮次 (Epoch) →
    ^
误  |                测试集误差 (不可见)
差  |                /
    |               /
    |              /
    |             /
    |            /
    |           /
    |          /
    |         * (最优停止点)
    |        / \
    |       /   \
    |      /     \
    |_____/_______\_ 训练集误差 (持续下降)
    |   /           \
    |  /             \
    | /               \
    |/                 \
    +------------------->

在训练过程中,训练集误差(图中下方持续下降的曲线)通常会随着迭代轮次增加而持续下降。然而,我们真正关心的是模型在未知数据(测试集)上的表现。测试集误差(图中先降后升的曲线)通常会先下降后上升。那个最低点,就是我们模型泛化能力最好的时刻。

但在实际训练中,我们无法接触到真正的测试集。为此,我们引入验证集。我们将训练数据划分为两部分:大部分用于训练,一小部分作为验证集。训练时,我们同时监控训练集和验证集上的误差。当发现验证集误差不再下降,反而开始上升时,就应立即停止训练。这个停止点,就是我们通过验证集估计出的“最优停止点”。

import numpy as np

# 模拟训练过程:训练误差持续下降,验证误差先降后升
np.random.seed(42)
epochs = 100
# 模拟训练误差:指数下降 + 微小波动
train_loss = np.exp(-np.linspace(0, 3, epochs)) + np.random.normal(0, 0.02, epochs)
# 模拟验证误差:先降后升(二次函数形状)
val_loss = 0.5 + 0.4 * (np.linspace(0, 1, epochs) - 0.6)**2 + np.random.normal(0, 0.03, epochs)

# 找到验证损失最小的轮次作为早期停止点
early_stop_epoch = np.argmin(val_loss) + 1  # +1 因为索引从0开始
print(f"训练集损失最终值: {train_loss[-1]:.4f}")
print(f"验证集损失最小值出现在第 {early_stop_epoch} 轮,值为 {val_loss[early_stop_epoch-1]:.4f}")
print(f"如果继续训练到第100轮,验证集损失会升至 {val_loss[-1]:.4f}")
训练集损失最终值: 0.0451
验证集损失最小值出现在第 43 轮,值为 0.4641
如果继续训练到第100轮,验证集损失会升至 0.5297

上面的模拟清晰地展示了早期停止的价值:验证损失在第 43 轮达到最低,此后继续训练只会让验证损失缓慢回升。盲目追求更低的训练损失,反而会让模型在验证集上表现更差。

应对策略二:权重衰减

权重衰减的灵感来源于人类大脑的发育过程。人在幼年时(如七八岁),大脑神经元之间的连接最为密集。随着年龄增长和经验的积累,一些无关紧要的连接会逐渐“修剪”掉,使得大脑对核心概念的判断更加准确和高效。

将这一思想迁移到神经网络:有些权重连接是“无用”的,甚至是有害的。例如,在一个手写数字识别网络中,输入图像右下角的一个像素点,可能永远与任何数字的判别无关。但如果这个像素点对应的初始权重不为零,网络在训练过程中就不得不调动其他权重来抵消它的影响,这增加了网络的拟合负担。

权重衰减通过在梯度下降的更新公式中增加一个衰减项,让这些“无用”的权重逐渐缩小至接近零。

原始梯度下降权重更新公式: w←w−η∂L∂ww \leftarrow w - \eta \frac{\partial L}{\partial w} 其中,ww是权重,η\eta是学习率,∂L∂w\frac{\partial L}{\partial w}是损失函数LL对权重的梯度。

加入权重衰减后的公式: w←(1−λ)w−η∂L∂ww \leftarrow (1 - \lambda) w - \eta \frac{\partial L}{\partial w} 其中,λ\lambda是一个很小的衰减率(例如0.01)。

这个公式的妙处在于:

  • 对于“有用”的权重:梯度项 η∂L∂w\eta \frac{\partial L}{\partial w} 通常起主导作用,权重更新主要受误差信号驱动。衰减项 (1−λ)w(1-\lambda)w 会使其值缓慢缩小,但影响相对次要。
  • 对于“无用”的权重:因为没有误差信号反传(梯度接近零),更新公式简化为 w←(1−λ)ww \leftarrow (1 - \lambda) w。这意味着每一轮迭代,权重都会乘以一个略小于1的因子 (1−λ)(1-\lambda),经过多轮迭代后,它会指数衰减到接近于零。

这样,网络结构实际上被“简化”了,无关的连接被弱化,使模型更加专注于学习真正重要的特征。

import numpy as np

# 模拟一个简单的权重更新过程,对比有无权重衰减
np.random.seed(42)
initial_weights = np.array([2.0, -1.5, 0.1, -0.02, 0.005]) # 假设最后一个权重“无用”
learning_rate = 0.1
decay_rate = 0.01  # lambda
epochs = 50

# 模拟梯度:前两个权重梯度大(有用),中间一个中等,最后两个几乎为0(无用)
# 这里用一个简单的模拟,梯度随时间缓慢变化
def simulate_gradient(epoch):
    base = np.array([-0.5, 0.3, -0.1, 0.01, 0.001])
    noise = np.random.randn(5) * 0.05
    decay_factor = np.exp(-epoch * 0.05)
    return base * decay_factor + noise

weights_no_decay = initial_weights.copy()
weights_with_decay = initial_weights.copy()

print("初始权重:", initial_weights)
print("-" * 50)

for epoch in range(epochs):
    grad = simulate_gradient(epoch)
    # 无权重衰减更新
    weights_no_decay -= learning_rate * grad
    # 有权重衰减更新
    weights_with_decay = (1 - decay_rate) * weights_with_decay - learning_rate * grad

print(f"训练{epochs}轮后:")
print(f"无衰减的权重: {np.round(weights_no_decay, 4)}")
print(f"有衰减的权重: {np.round(weights_with_decay, 4)}")
print(f"\n观察最后一个‘无用’权重:")
print(f"  无衰减: {weights_no_decay[-1]:.6f} (变化不大)")
print(f"  有衰减: {weights_with_decay[-1]:.6f} (已衰减至接近0)")
初始权重: [ 2.    -1.5    0.1   -0.02   0.005]
--------------------------------------------------
训练50轮后:
无衰减的权重: [ 2.9175 -2.0733  0.2781 -0.015   0.0248]
有衰减的权重: [ 1.863  -1.3154  0.1864 -0.0102  0.0099]

观察最后一个‘无用’权重:
  无衰减: 0.024768 (变化不大)
  有衰减: 0.009873 (已衰减至接近0)

可以看到,在权重衰减的作用下,所有权重的绝对值都有所缩小,尤其是那个“无用”的权重,其值被显著衰减。

应对策略三:Dropout

Dropout是深度学习中最著名、最有效的正则化技术之一。它的操作简单而强大:在训练阶段,每次前向传播时,随机“丢弃”(即暂时屏蔽)网络中一定比例(如50%)的神经元。

这个过程如下图所示(训练时):

标准全连接层          应用Dropout后
    O                       O
   /|\                     / \
  O-O-O                   O   O
   \|/                     \ /
    O                       O
 (所有神经元激活)      (部分神经元被随机丢弃)

Dropout为什么有效?

  1. 强制学习冗余特征:因为任何神经元都可能被随机屏蔽,网络不能依赖于某个特定的神经元或特征,必须让不同的神经元子集都学习到有用的、某种程度上冗余的特征。这提升了特征的鲁棒性。
  2. 本质是集成学习:每次迭代,我们都在训练一个不同的、更“瘦”的子网络。整个训练过程相当于训练了指数级多个子网络。在测试时,所有这些子网络共同投票做出预测,这类似于机器学习中的Bagging集成方法,能有效降低方差。
  3. 打破神经元间的复杂共适应:防止神经元之间形成过于复杂的相互依赖关系,这种关系可能在训练集上巧合地有效,但泛化能力差。

一个生动的类比是老板布置任务:如果老板同时告诉A和B去做一件事,两人可能会互相依赖,结果都不尽力。但如果老板分别私下告诉A和B“这件事全靠你了”,两人都会全力以赴。最后老板综合两人的成果,结果通常会更好。

测试时的处理 在测试阶段,我们需要使用完整的网络进行预测。但是,由于训练时只有一部分神经元工作,它们的“责任”被放大了。为了补偿这一点,我们需要对训练好的权重进行缩放。

原始论文中的经典做法是:在测试时,将每个神经元的输出乘以训练时该神经元被保留的概率 pp(即1 - dropout率)。例如,训练时Dropout率为0.5(保留概率p=0.5),那么测试时每个神经元的输出都要乘以0.5。

另一种等价且更常用的实现方式是反向Dropout:在训练时,对没有被丢弃的神经元的激活值直接除以保留概率 pp。这样,该神经元的“期望”输出值在训练和测试时就是一致的,测试时就可以直接使用网络,无需额外操作。

import numpy as np

# 演示Dropout的前向传播过程(训练和测试阶段)
np.random.seed(42)

def dropout_forward(x, dropout_rate, mode='train'):
    """
    x: 输入向量或矩阵
    dropout_rate: 丢弃概率
    mode: 'train' 或 'test'
    """
    if mode == 'train':
        # 生成掩码,1表示保留,0表示丢弃
        keep_prob = 1 - dropout_rate
        mask = (np.random.rand(*x.shape) < keep_prob).astype(np.float32)
        # 反向Dropout:训练时对保留的神经元进行缩放
        out = x * mask / keep_prob
        return out, mask
    else: # mode == 'test'
        # 测试时,不做任何改动(因为训练时已经缩放过了)
        return x

# 模拟一个包含5个神经元的层的输出
layer_output = np.array([1.2, -0.8, 0.5, 2.1, -1.0])
dropout_rate = 0.4  # 丢弃40%的神经元
keep_prob = 1 - dropout_rate

print("原始层输出:", layer_output)
print(f"Dropout率: {dropout_rate} (保留概率 p={keep_prob})")
print("-" * 40)

# 训练阶段
output_train, mask = dropout_forward(layer_output, dropout_rate, 'train')
print("训练阶段:")
print("随机生成的掩码 (1=保留, 0=丢弃):", mask)
print("Dropout后输出 (已缩放):", np.round(output_train, 4))
print("注意:单次采样中,保留神经元的输出被除以 p,被丢弃的置 0;")
print("      逐元素期望满足 E[out_i] = x_i(而不是本次采样的均值等于原均值)。")

print("-" * 40)

# 测试阶段
output_test = dropout_forward(layer_output, dropout_rate, 'test')
print("测试阶段 (直接使用原始输出):", output_test)
原始层输出: [ 1.2 -0.8  0.5  2.1 -1. ]
Dropout率: 0.4 (保留概率 p=0.6)
----------------------------------------
训练阶段:
随机生成的掩码 (1=保留, 0=丢弃): [1. 0. 0. 1. 1.]
Dropout后输出 (已缩放): [ 2.     -0.      0.      3.5    -1.6667]
注意:单次采样中,保留神经元的输出被除以 p,被丢弃的置 0;
      逐元素期望满足 E[out_i] = x_i(而不是本次采样的均值等于原均值)。
----------------------------------------
测试阶段 (直接使用原始输出): [ 1.2 -0.8  0.5  2.1 -1. ]

可以看到,在训练时,第二个和第三个神经元被丢弃(输出为0),而其他保留的神经元的输出被放大了(除以了0.6)。注意,这不是让“本次采样的均值”保持不变,而是让逐元素的期望保持不变:E[outi]=xiE[out_i] = x_i。在测试时,我们直接使用原始输出,其期望与训练时一致。

📝 动手练一练

  1. 概念辨析:早期停止(Early Stop)策略中,我们依据验证集误差的变化来决定何时停止。如果验证集误差在连续5个epoch内都没有下降,但训练集误差仍在稳步下降,此时应该立即停止吗?为什么?
👉 点击查看参考答案

不一定立即停止。虽然验证集误差暂时停滞,但这可能只是训练过程中的一个平台期。常见的策略是设置一个“耐心值”(patience),例如,只有当验证集误差在连续10个或更多epoch内都不再下降(或开始上升)时,才触发早期停止。立即停止可能导致模型未能充分学习。同时,也需要检查学习率是否设置得当,平台期有时可以通过学习率衰减来克服。

  1. 代码实践:在权重衰减的模拟代码中,我们将衰减项直接应用于权重更新:w = (1 - λ) * w - η * grad。请尝试修改代码,模拟一种情况:某个权重在最初几轮有较大的梯度,但后来梯度变得非常小(接近于0)。观察在有/无权重衰减的情况下,这个权重的最终值有何显著区别?
👉 点击查看参考答案
import numpy as np
np.random.seed(42)

# 模拟一个权重,其梯度先大后小
epochs = 100
learning_rate = 0.1
decay_rate = 0.01
lambda_reg = 0.01 # 权重衰减系数

w_no_decay = 1.0
w_with_decay = 1.0

for epoch in range(epochs):
    # 模拟梯度:前20轮梯度大,之后梯度接近0
    if epoch < 20:
        grad = -0.5 + np.random.randn()*0.1
    else:
        grad = 0.001 * np.random.randn() # 非常小的噪声梯度

    # 更新权重
    w_no_decay -= learning_rate * grad
    w_with_decay = (1 - lambda_reg) * w_with_decay - learning_rate * grad

print(f"初始权重: 1.0")
print(f"无权重衰减最终值: {w_no_decay:.4f}")
print(f"有权重衰减最终值: {w_with_decay:.4f}")
print(f"区别:在梯度消失后,无衰减的权重基本保持不变({w_no_decay:.4f}),")
print(f"      而有衰减的权重则持续缩小({w_with_decay:.4f}),虽未到 0,但已明显更小。")
初始权重: 1.0
无权重衰减最终值: 2.0350
有权重衰减最终值: 0.7906
区别:在梯度消失后,无衰减的权重基本保持不变(2.0350),
      而有衰减的权重则持续缩小(0.7906),虽未到 0,但已明显更小。

这个结果清晰地展示了权重衰减的“修剪”作用:对于后期不再提供有效梯度信号的权重,权重衰减会持续把它往 0 拉,使其明显小于无衰减时的值。衰减是逐轮累积的,轮数越多、衰减率越大,效果越接近 0。

本章小结

过适应是深度学习模型从“记忆”走向“理解”的主要障碍。本节深入探讨了其成因——模型复杂性与数据量之间的不匹配,并系统学习了三种经典且有效的应对策略:

  • 早期停止:借助验证集作为“刹车器”,防止模型在训练集上过度钻营。
  • 权重衰减:在优化过程中对权重进行温和的惩罚,促使网络自动修剪掉不重要的连接,实现结构简化。
  • Dropout:通过随机“失活”神经元,在训练中集成海量子网络,强制模型学习鲁棒且冗余的特征表征,是提升泛化能力的利器。

这三种技术常常结合使用,它们从不同角度约束模型的优化路径,是构建高性能、高泛化深度学习模型的基石。

今天这些手段依然是标配,但侧重有所变化:早停几乎在所有训练流程中保留;权重衰减在 CNN 时代常与 BatchNorm 配合、并区分 SGD 的 L2 与 Adam 的解耦衰减(AdamW);Dropout 在卷积层中用得少了(BN 已提供部分正则),但在全连接层和 Transformer 中仍是常用手段。理解它们“限制容量、增加噪声、平滑解”的共同本质,比记住某一种技巧更重要。

📋 行动清单

  • 在你接下来的模型训练中,尝试加入验证集,并实现一个简单的早期停止逻辑(例如,验证损失连续5轮不下降则停止)。
  • 修改一个简单的全连接网络训练代码,为其添加权重衰减项,观察训练损失和验证损失曲线的变化。
  • 理解并复现Dropout在训练和测试阶段的不同处理逻辑,思考为什么测试时需要对权重进行缩放(或训练时对激活值进行放大)。

—— 小象教研组