查看全课大纲(第 34 / 93 节)
- 1.计算机视觉概述
- 2.计算机视觉应用场景
- 3.困难与挑战
- 4.开源库介绍
- 5.图像显示与存储
- 6.CLAHE
- 7.腐蚀膨胀与开闭运算
- 8.滤波与边缘填充
- 9.各种滤波
- 10.图像金字塔
- 11.傅里叶变换
- 12.短时傅里叶变换与小波变换
- 13.颜色特征与颜色直方图
- 14.几何特征_边缘
- 15.几何特征-角点与斑点
- 16.局部特征-SIFT特征描述子
- 17.局部特征-SURF特征描述子
- 18.局部特征-ORB特征描述子
- 19.其他特征描述子
- 20.传统图像分割技术
- 21.GraphCut
- 22.GrabCut
- 23.人脸检测的传统方法
- 24.行人检测的传统方法HOG+SVM
- 25.行人检测DPM
- 26.人工神经网络的架构
- 27.目标函数与梯度下降
- 28.误差反向传播
- 29.可手算的神经网络
- 30.改进目标函数与克服梯度消散
- 31.神经网络之传统神经网络与深度网络的差异
- 32.各种梯度下降算法
- 33.如何选择合适的梯度下降算法
- 34.避免过适应
- 35.CNN的架构
- 36.CNN的误差反向传播_卷积层
- 37.CNN的误差反向传播_池化层
- 38.分类所用的数据集
- 39.Alexnet详解
- 40.VGG详解
- 41.GoogLeNet_v1
- 42.BatchNormalization
- 43.GoogLeNet_v3v4
- 44.ResNet
- 45.ResNeXt
- 46.CNN设计准则
- 47.图像检索与推土机距离
- 48.基于颜色相似性
- 49.基于形状相似性
- 50.基于纹理相似性
- 51.基于局部特征相似性
- 52.DL的二值哈希编码
- 53.高维空间中检索KDTree
- 54.高维空间中检索LSH
- 55.目标识别任务综述
- 56.RCNN
- 57.SPP-Net
- 58.Fast R-CNN
- 59.Faster R-CNN
- 60.R-CNN系列模型回顾
- 61.R-FCN的设计思路
- 62.R-FCN的实现
- 63.YOLOv1
- 64.YOLOv2_9000
- 65.YOLOv3
- 66.图像分割应用场景与数据集
- 67.上采样的实现方式
- 68.FCN网络架构的由来
- 69.FCN构建细节
- 70.DeepLabv1
- 71.CRF
- 72.DeepLabv2
- 73.DeepLabv3
- 74.特定的医学影像分割任务
- 75.U-Net
- 76.3D U-Net
- 77.V-Net
- 78.DenseNet
- 79.病理切片分析任务及数据集
- 80.病理切片分析
- 81.图说任务及其传统处理方法
- 82.深度语言模型与RNN
- 83.LSTM及其变种
- 84.NIC模型
- 85.SAT模型
- 86.图说的性能指标
- 87.判别模型与生成模型
- 88.KL散度与JS散度
- 89.VAE
- 90.GAN的基本原理
- 91.DCGAN与CGAN
- 92.Wasserstein GAN
- 93.SRGAN
避免过适应
小象实战讲义 · 基于深度学习的计算机视觉
在深度学习的征途中,我们不仅希望模型在训练集上表现优异,更期待它在从未见过的数据上也能做出准确的判断。然而,一个强大的、参数众多的网络,很容易陷入“过适应”的陷阱——它记住了训练数据的“个性”而非“共性”,导致泛化能力急剧下降。本节将为你揭示过适应的本质,并传授三种核心的“防过拟合”利器:早期停止、权重衰减和Dropout,让你构建的模型既强大又稳健。
💡 核心导读
- 过适应的本质:模型因参数过多、样本不足而学习了训练数据中的噪声和“个性”特征,导致泛化能力差。
- 早期停止(Early Stop):通过监控验证集误差,在模型开始“学坏”时果断停止训练。
- 权重衰减(Weight Decay):在梯度下降更新中引入衰减项,让“无用”的权重逐渐归零,简化网络结构。
- Dropout:在训练时随机“丢弃”部分神经元,迫使网络学习冗余、鲁棒的特征,本质是一种集成学习。
过适应:深度学习的常见陷阱
在深度学习,尤其是图像分类任务中,过适应是一个普遍且严重的问题。它的一个经典表现是:模型对训练集中添加了人眼几乎无法察觉的细微噪声的图片,会做出完全错误的判断。
这引发了深刻的思考:学习机到底学到了什么?它判断类别差异的标准,和我们人类的视觉感知是一致的吗?显然,如果模型因为一点噪声就“失明”,说明它很可能抓住了一些非常特殊、非本质的“个性”特征,而不是我们期望的、具有普适性的“共性”特征。
过适应的根本原因可以归结为两点:
- 模型容量过大:深度神经网络拥有海量的权重参数,表达能力极强。
- 样本量相对不足:尽管我们的数据集可能很大,但相对于网络的复杂程度,有效样本量本质上是不足的。
这种“大网络、小数据”的错配,使得网络有足够的能力去“死记硬背”训练样本,甚至包括其中的噪声,而不是去学习数据背后真正的规律。
应对策略一:早期停止训练
早期停止是最直观、也最容易想到的一种避免过适应的方法。其核心思想是:不能一味地追求训练集误差的无限减小。
我们通过一张图来理解这个过程:
训练轮次 (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 轮达到最低,此后继续训练只会让验证损失缓慢回升。盲目追求更低的训练损失,反而会让模型在验证集上表现更差。
应对策略二:权重衰减
权重衰减的灵感来源于人类大脑的发育过程。人在幼年时(如七八岁),大脑神经元之间的连接最为密集。随着年龄增长和经验的积累,一些无关紧要的连接会逐渐“修剪”掉,使得大脑对核心概念的判断更加准确和高效。
将这一思想迁移到神经网络:有些权重连接是“无用”的,甚至是有害的。例如,在一个手写数字识别网络中,输入图像右下角的一个像素点,可能永远与任何数字的判别无关。但如果这个像素点对应的初始权重不为零,网络在训练过程中就不得不调动其他权重来抵消它的影响,这增加了网络的拟合负担。
权重衰减通过在梯度下降的更新公式中增加一个衰减项,让这些“无用”的权重逐渐缩小至接近零。
原始梯度下降权重更新公式: 其中,是权重,是学习率,是损失函数对权重的梯度。
加入权重衰减后的公式: 其中,是一个很小的衰减率(例如0.01)。
这个公式的妙处在于:
- 对于“有用”的权重:梯度项 通常起主导作用,权重更新主要受误差信号驱动。衰减项 会使其值缓慢缩小,但影响相对次要。
- 对于“无用”的权重:因为没有误差信号反传(梯度接近零),更新公式简化为 。这意味着每一轮迭代,权重都会乘以一个略小于1的因子 ,经过多轮迭代后,它会指数衰减到接近于零。
这样,网络结构实际上被“简化”了,无关的连接被弱化,使模型更加专注于学习真正重要的特征。
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为什么有效?
- 强制学习冗余特征:因为任何神经元都可能被随机屏蔽,网络不能依赖于某个特定的神经元或特征,必须让不同的神经元子集都学习到有用的、某种程度上冗余的特征。这提升了特征的鲁棒性。
- 本质是集成学习:每次迭代,我们都在训练一个不同的、更“瘦”的子网络。整个训练过程相当于训练了指数级多个子网络。在测试时,所有这些子网络共同投票做出预测,这类似于机器学习中的Bagging集成方法,能有效降低方差。
- 打破神经元间的复杂共适应:防止神经元之间形成过于复杂的相互依赖关系,这种关系可能在训练集上巧合地有效,但泛化能力差。
一个生动的类比是老板布置任务:如果老板同时告诉A和B去做一件事,两人可能会互相依赖,结果都不尽力。但如果老板分别私下告诉A和B“这件事全靠你了”,两人都会全力以赴。最后老板综合两人的成果,结果通常会更好。
测试时的处理 在测试阶段,我们需要使用完整的网络进行预测。但是,由于训练时只有一部分神经元工作,它们的“责任”被放大了。为了补偿这一点,我们需要对训练好的权重进行缩放。
原始论文中的经典做法是:在测试时,将每个神经元的输出乘以训练时该神经元被保留的概率 (即1 - dropout率)。例如,训练时Dropout率为0.5(保留概率p=0.5),那么测试时每个神经元的输出都要乘以0.5。
另一种等价且更常用的实现方式是反向Dropout:在训练时,对没有被丢弃的神经元的激活值直接除以保留概率 。这样,该神经元的“期望”输出值在训练和测试时就是一致的,测试时就可以直接使用网络,无需额外操作。
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)。注意,这不是让“本次采样的均值”保持不变,而是让逐元素的期望保持不变:。在测试时,我们直接使用原始输出,其期望与训练时一致。
📝 动手练一练
- 概念辨析:早期停止(Early Stop)策略中,我们依据验证集误差的变化来决定何时停止。如果验证集误差在连续5个epoch内都没有下降,但训练集误差仍在稳步下降,此时应该立即停止吗?为什么?
👉 点击查看参考答案
不一定立即停止。虽然验证集误差暂时停滞,但这可能只是训练过程中的一个平台期。常见的策略是设置一个“耐心值”(patience),例如,只有当验证集误差在连续10个或更多epoch内都不再下降(或开始上升)时,才触发早期停止。立即停止可能导致模型未能充分学习。同时,也需要检查学习率是否设置得当,平台期有时可以通过学习率衰减来克服。
- 代码实践:在权重衰减的模拟代码中,我们将衰减项直接应用于权重更新:
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在训练和测试阶段的不同处理逻辑,思考为什么测试时需要对权重进行缩放(或训练时对激活值进行放大)。
—— 小象教研组