零基础学习机器学习(十二)模型的选择,过拟合,欠拟合及其解决方案

核心概念与核心问题

机器学习的核心目标是让模型发现泛化模式(能对未见过的数据有效预测),而非仅记忆训练数据。关键矛盾在于:模型在有限训练样本上学习时,可能出现 “无法捕捉数据规律” 或 “过度记忆训练噪声” 的问题,即欠拟合与过拟合,需通过合理模型选择平衡泛化能力。

过拟合,欠拟合

训练误差与泛化误差

  1. 定义与区别
    | 误差类型 | 定义 | 特点 |
    | ---- | ---- | ---- |
    | 训练误差 | 模型在训练数据集上计算的误差 | 可直接计算,易通过复杂模型降低 |
    | 泛化误差 | 模型在无限多同分布新数据上的误差期望 | 无法精确计算,需通过独立测试集 / 验证集估计 |

  2. 关键理论支撑

  • 独立同分布假设(i.i.d.):训练数据与测试数据需从同一分布独立抽取,是评估泛化误差的基础。现实中可能存在分布偏移(如加州医院数据训练的模型用于马萨诸塞医院),需警惕其对泛化能力的影响。
  • 统计学习理论:格里文科 - 坎特利定理证明训练误差会收敛到泛化误差;Vapnik 和 Chervonenkis 将其扩展到更广泛函数类,为泛化误差分析提供理论基础。
  1. 影响泛化的核心因素(模型复杂性)
  • 可调整参数数量:参数越多(自由度越高),模型越易过拟合(如高阶多项式 vs 低阶多项式)。
  • 参数取值范围:权重取值过大时,模型易受噪声影响,泛化能力下降。
  • 训练样本数量:样本量越少,越易过拟合;百万级样本需极复杂模型才可能过拟合,足够数据可提升泛化能力。
  • 训练迭代次数:需 “早停” 的模型(少迭代)复杂度低,过度迭代可能加剧过拟合。

模型选择方法

模型选择的核心是避免 “用测试集调参导致过拟合测试数据”,需通过专门数据划分实现:

  1. 验证集划分
  • 数据三分法:将数据分为训练集(训练模型)、验证集(选择超参数 / 模型)、测试集(最终评估泛化能力)。
  • 注意事项:验证集与测试集边界需清晰,书中实验因数据限制,报告的 “测试准确度” 实际为验证集准确度。
  1. K 折交叉验证
  • 适用场景:训练数据稀缺,无法划分出足够大的验证集。
  • 操作步骤:
    • 将原始训练数据分成 K 个不重叠子集;
    • 执行 K 次训练 + 验证:每次用 K-1 个子集训练,1 个子集验证;
    • 取 K 次验证误差的平均值,作为模型泛化能力的估计。

过拟合还是欠拟合

当我们比较训练和验证误差时,我们要注意两种常见的情况。 首先,我们要注意这样的情况:训练误差和验证误差都很严重, 但它们之间仅有一点差距。 如果模型不能降低训练误差,这可能意味着模型过于简单(即表达能力不足), 无法捕获试图学习的模式。 此外,由于我们的训练和验证误差之间的泛化误差很小, 我们有理由相信可以用一个更复杂的模型降低训练误差。 这种现象被称为欠拟合(underfitting)。

另一方面,当我们的训练误差明显低于验证误差时要小心, 这表明严重的过拟合(overfitting)。 注意,过拟合并不总是一件坏事。 特别是在深度学习领域,众所周知, 最好的预测模型在训练数据上的表现往往比在保留(验证)数据上好得多。 最终,我们通常更关心验证误差,而不是训练误差和验证误差之间的差距。

是否过拟合或欠拟合可能取决于模型复杂性和可用训练数据集的大小, 这两个点将在下面进行讨论。

模型复杂度

为了说明一些关于过拟合和模型复杂性的经典直觉,我们给出一个多项式的例子。给定由单个特征xx和对应实数标签yy组成的训练数据,我们试图找到下面的dd阶多项式来估计标签yy

y^=i=0dxiwi\hat{y} = \sum_{i=0}^{d} x^i w_i

这只是一个线性回归问题,我们的特征是xx的幂给出的,模型的权重是wiw_i给出的,偏置是w0w_0给出的(因为对于所有的xx都有x0=1x^0 = 1)。由于这只是一个线性回归问题,我们可以使用平方误差作为我们的损失函数。

高阶多项式函数比低阶多项式函数复杂得多。高阶多项式的参数较多,模型函数的选择范围较广。因此在固定训练数据集的情况下,高阶多项式函数相对于低阶多项式的训练误差应该始终更低(最坏也是相等)。事实上,当数据样本包含了xx的不同值时,函数阶数等于数据样本数量的多项式函数可以完美拟合训练集。在图4.4.1中,我们直观地描述了多项式的阶数和欠拟合与过拟合之间的关系。

数据集大小

另一个重要因素是数据集的大小。 训练数据集中的样本越少,我们就越有可能(且更严重地)过拟合。 随着训练数据量的增加,泛化误差通常会减小。 此外,一般来说,更多的数据不会有什么坏处。 对于固定的任务和数据分布,模型复杂性和数据集大小之间通常存在关系。 给出更多的数据,我们可能会尝试拟合一个更复杂的模型。 能够拟合更复杂的模型可能是有益的。 如果没有足够的数据,简单的模型可能更有用。 对于许多任务,深度学习只有在有数千个训练样本时才优于线性模型。 从一定程度上来说,深度学习目前的生机要归功于 廉价存储、互联设备以及数字化经济带来的海量数据集。

多项式回归

生成数据集

给定xx,我们将使用以下三阶多项式来生成训练和测试数据的标签:

y=5+1.2x3.4x22!+5.6x33!+ϵ where ϵN(0,0.12).(4.4.2)y = 5 + 1.2x - 3.4\frac{x^2}{2!} + 5.6\frac{x^3}{3!} + \epsilon \text{ where } \epsilon \sim \mathcal{N}(0, 0.1^2). \tag{4.4.2}

噪声项ϵ\epsilon 服从均值为0且标准差为0.1的正态分布。在优化的过程中,我们通常希望避免非常大的梯度值或损失值。这就是我们将特征从xix^i 调整为xii!\frac{x^i}{i!} 的原因,这样可以避免很大的ii 带来的特别大的指数值。我们将为训练集和测试集各生成100个样本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
max_degree = 20  # 多项式的最大阶数
n_train, n_test = 100, 100 # 训练和测试数据集大小
true_w = np.zeros(max_degree) # 分配大量的空间
true_w[0:4] = np.array([5, 1.2, -3.4, 5.6])

features = np.random.normal(size=(n_train + n_test, 1))
np.random.shuffle(features)
poly_features = np.power(features, np.arange(max_degree).reshape(1, -1))
for i in range(max_degree):
poly_features[:, i] /= math.gamma(i + 1) # gamma(n)=(n-1)!
# labels的维度:(n_train+n_test,)
labels = np.dot(poly_features, true_w)
labels += np.random.normal(scale=0.1, size=labels.shape)

# NumPy ndarray转换为tensor
true_w, features, poly_features, labels = [torch.tensor(x, dtype=
torch.float32) for x in [true_w, features, poly_features, labels]]

features[:2], poly_features[:2, :], labels[:2]

对模型进行训练和测试

首先让我们实现一个函数来评估模型在给定数据集上的损失。

1
2
3
4
5
6
7
8
9
def evaluate_loss(net, data_iter, loss):  #@save
"""评估给定数据集上模型的损失"""
metric = d2l.Accumulator(2) # 损失的总和,样本数量
for X, y in data_iter:
out = net(X)
y = y.reshape(out.shape)
l = loss(out, y)
metric.add(l.sum(), l.numel())
return metric[0] / metric[1]

现在定义训练函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def train(train_features, test_features, train_labels, test_labels,
num_epochs=400):
loss = nn.MSELoss(reduction='none')
input_shape = train_features.shape[-1]
# 不设置偏置,因为我们已经在多项式中实现了它
net = nn.Sequential(nn.Linear(input_shape, 1, bias=False))
batch_size = min(10, train_labels.shape[0])
train_iter = d2l.load_array((train_features, train_labels.reshape(-1,1)),
batch_size)
test_iter = d2l.load_array((test_features, test_labels.reshape(-1,1)),
batch_size, is_train=False)
trainer = torch.optim.SGD(net.parameters(), lr=0.01)
animator = d2l.Animator(xlabel='epoch', ylabel='loss', yscale='log',
xlim=[1, num_epochs], ylim=[1e-3, 1e2],
legend=['train', 'test'])
for epoch in range(num_epochs):
d2l.train_epoch_ch3(net, train_iter, loss, trainer)
if epoch == 0 or (epoch + 1) % 20 == 0:
animator.add(epoch + 1, (evaluate_loss(net, train_iter, loss),
evaluate_loss(net, test_iter, loss)))
print('weight:', net[0].weight.data.numpy())

三阶多项式函数拟合(正常)

我们将首先使用三阶多项式函数,它与数据生成函数的阶数相同。 结果表明,该模型能有效降低训练损失和测试损失。 学习到的模型参数也接近真实值。

1
2
3
# 从多项式特征中选择前4个维度,即1,x,x^2/2!,x^3/3!
train(poly_features[:n_train, :4], poly_features[n_train:, :4],
labels[:n_train], labels[n_train:])
1
weight: [[ 5.010476   1.2354498 -3.4229028  5.503297 ]]

线性函数拟合(欠拟合)

让我们再看看线性函数拟合,减少该模型的训练损失相对困难。 在最后一个迭代周期完成后,训练损失仍然很高。 当用来拟合非线性模式(如这里的三阶多项式函数)时,线性模型容易欠拟合。

1
2
3
# 从多项式特征中选择前2个维度,即1和x
train(poly_features[:n_train, :2], poly_features[n_train:, :2],
labels[:n_train], labels[n_train:])
1
weight: [[3.4049764 3.9939284]]

高阶多项式函数拟合(过拟合)

现在,让我们尝试使用一个阶数过高的多项式来训练模型。 在这种情况下,没有足够的数据用于学到高阶系数应该具有接近于零的值。 因此,这个过于复杂的模型会轻易受到训练数据中噪声的影响。 虽然训练损失可以有效地降低,但测试损失仍然很高。 结果表明,复杂模型对数据造成了过拟合。

1
2
3
# 从多项式特征中选取所有维度
train(poly_features[:n_train, :], poly_features[n_train:, :],
labels[:n_train], labels[n_train:], num_epochs=1500)

解决方案

权重衰减

前一节我们描述了过拟合的问题,本节我们将介绍一些正则化模型的技术。 我们总是可以通过去收集更多的训练数据来缓解过拟合。 但这可能成本很高,耗时颇多,或者完全超出我们的控制,因而在短期内不可能做到。 假设我们已经拥有尽可能多的高质量数据,我们便可以将重点放在正则化技术上。

  1. 提出目的:作为正则化技术,用于缓解模型过拟合问题。当无法通过收集更多训练数据改善过拟合时,权重衰减通过限制模型复杂度实现优化,尤其适用于高维数据场景(如多变量多项式回归中,阶数增长会导致项数急剧增加,简单限制特征数量难以平衡模型复杂度)。
  2. 核心概念:又称L₂正则化,通过在损失函数中加入权重向量的L₂范数惩罚项,衡量模型函数与“最简单函数(f=0)”的距离,引导权重向量保持较小规模,避免模型过度依赖部分特征。
  3. 数学表达
    • 原始线性回归损失函数:L(w,b)=1ni=1n12(wx(i)+by(i))2L(w, b) = \frac{1}{n}\sum_{i=1}^{n}\frac{1}{2}(w^\top x^{(i)} + b - y^{(i)})^2
    • 加入L₂正则化的损失函数:L(w,b)+λ2w2L(w, b) + \frac{\lambda}{2}\|w\|^2,其中λ\lambda为非负正则化常数(控制惩罚强度,λ=0\lambda=0时恢复原始损失函数),w2\|w\|^2是权重向量的L₂范数平方(便于计算导数)。
  4. 与L₁正则化的区别:L₂正则化(权重衰减)使权重在大量特征上均匀分布,增强模型对观测误差的稳定性;L₁正则化(套索回归)会使部分权重清零,实现特征选择,适用场景不同。
  5. 参数更新逻辑:以小批量随机梯度下降为例,更新公式为w(1ηλ)wηBiBx(i)(wx(i)+by(i))w \leftarrow (1 - \eta\lambda)w - \frac{\eta}{|B|}\sum_{i \in B}x^{(i)}(w^\top x^{(i)} + b - y^{(i)}),每一步训练中通过(1ηλ)(1 - \eta\lambda)项“衰减”权重,限制其增长。通常不对输出层偏置bb进行正则化,不同框架可灵活配置。

暂退法

(一)重新审视过拟合

  1. 线性模型与深度网络的过拟合差异:线性模型在特征多、样本不足时易过拟合,样本多于特征时通常不易过拟合,但无法考虑特征间交互作用;深度神经网络即便样本远多于特征,仍可能过拟合,如2017年研究中,深度网络可在随机标记图像上完美训练,但测试精度难超10%,泛化差距达90%。
  2. 偏差-方差权衡:线性模型偏差高(仅能表示小类函数)、方差低(不同样本上结果相似);深度神经网络位于偏差-方差谱另一端,灵活性高但易过拟合,暂退法是改善其泛化性的实用工具。

(二)扰动的稳健性

  1. “好”模型的标准:除简单性(小维度、参数范数小)外,还需平滑性,即对输入微小变化不敏感,如图像分类中,像素加随机噪声应基本不影响结果。
  2. 暂退法的理论基础:1995年毕晓普证明输入噪声训练等价于Tikhonov正则化;2014年斯里瓦斯塔瓦等人将该想法扩展到网络内部层,提出暂退法,在训练时向每一层注入噪声以增强输入-输出映射的平滑性。
  3. 暂退法的无偏向噪声注入:为保证固定其他层时,每一层期望值与无噪声时一致,标准暂退法中,中间活性值(h)以暂退概率(p)由随机变量(h’)替换,公式为:
    [h’ = \begin{cases}
    0 & 概率为(p) \
    \frac{h}{1-p} & 其他情况
    \end{cases}]
    且(E[h’] = h)。

(三)实践中的暂退法

  1. 操作方式:训练时,每次迭代计算下一层前,将当前层部分节点置零(如含1个隐藏层、5个隐藏单元的多层感知机,可能丢弃部分隐藏单元),使输出层不过度依赖某一隐藏单元;测试时通常不使用暂退法,无需标准化,仅部分研究为估计预测“不确定性”会在测试时使用。
  2. 暂退概率设置技巧:靠近输入层的暂退概率通常设得较低。