机器学习的核心目标是发现数据中的模式,但关键在于这些模式是否具有普遍性,能否“举一反三”。比如,我们希望从患者的基因数据中找到与痴呆状态相关的规律,标签可能是“痴呆”“轻度认知障碍”和“健康”。
然而,由于基因可以唯一标识个体,模型可能会直接 “记住” 数据:“这是鲍勃,我记得他有痴呆症!”这样的模型无法应用到新患者,因为它并没有学到真正的规律。我们需要的是能够泛化的模型,它可以根据训练集的规律,对未见过的样本做出合理判断。
但问题在于,我们通常只能使用有限的数据来训练模型。即使是公开的大型图像数据集,通常也只有约一百万张图像。而在医疗领域,样本可能只有几千甚至更少。在这种情况下,模型容易出现“过拟合”现象:在训练集上表现完美,但在新数据上效果很差。
为了防止过拟合,我们需要用“正则化”等技术来约束模型。或许你已经在实验中看到过这种现象:通过调整神经网络的层数、神经元数量或训练时间,模型可以在训练集上达到100%的精度,但测试集的准确率却下降。这说明仅仅“记住”数据是不够的,模型必须学会真正能泛化的模式,这才是机器学习的关键挑战。
一、训练误差与泛化误差
在机器学习中,我们评估模型表现的两个重要概念是 训练误差 和 泛化误差:
- 训练误差:模型在训练数据上的误差。
- 泛化误差:模型在无限多的、从同一分布中抽样的数据上的误差期望。
由于我们无法获得无限数据,泛化误差通常用一个独立的 测试集 来估计,该测试集由未见过的数据样本组成。
1. 理解训练误差与泛化误差:三个例子
1.1 考试备考的学生
- 一个学生通过背诵往年考题的答案来准备考试,即便训练表现优异(记住考题),也未必能在真正考试中取得好成绩。
- 相比之下,理解题目规律的学生通常泛化能力更强,在新考题中表现更好。
1.2 查表模型的局限性
- 一个简单的查表模型可以记住有限训练样本的输入和输出。但在面对未见过的输入时,模型表现可能和随机猜测无异。
- 例如,灰度图像的可能组合数量极其庞大,即便存储所有可能的输入输出表也不现实。
1.3 硬币分类问题
- 对公平硬币(正面=0,反面=1)分类时,泛化误差是固定的 0.5。但有限样本可能让模型倾向于预测多数类别,导致训练误差低于泛化误差。
- 随着样本量增加,训练误差与泛化误差会逐渐接近。
2. 独立同分布假设(i.i.d. 假设)
机器学习通常假设训练数据和测试数据来自相同分布,且独立采样。但现实中,这一假设可能失效:
- 数据分布不同:如训练数据来自北京的患者,测试数据来自广州的患者。
- 时间相关性:如微博上的话题随新闻周期变化,违反独立性假设。
即便如此,许多实际应用(如人脸识别、语音识别)在一定程度上违背 i.i.d. 假设后仍能表现良好。但极端情况下,模型会失败,例如用大学生的人脸数据训练模型,却用于老年人监测。
3. 模型过拟合问题
训练过程中,模型可能过度拟合训练数据,导致泛化能力不足。例如:
- 在训练集上表现完美,但对测试数据效果差。
- 深度学习中,许多技术(如正则化、早停)被用于控制过拟合。
模型复杂性 是影响泛化的重要因素:
- 可调整参数数量:参数越多,模型越容易过拟合。
- 参数取值范围:参数值范围越大,过拟合风险越高。
- 训练样本数量:小样本数据更容易过拟合,大量样本则需要非常复杂的模型才能完全拟合。
简单模型往往更易泛化,符合统计学中的“可证伪性”原则:模型越容易被测试证明错误,越可能泛化得更好。
二、模型选择:确定最佳模型的方法
在机器学习中,模型选择是一个重要步骤,用于从多个候选模型中选出最优模型。候选模型可能是完全不同的模型(如决策树和线性模型),也可能是同一类模型在不同超参数配置下的变体。
1. 验证集的作用
为了找到最佳模型,我们通常引入验证集来评估模型,而不是直接使用测试集。这是因为:
避免过拟合测试数据:如果在模型选择过程中使用测试集,可能导致模型对测试数据的过拟合。过拟合训练数据尚可通过测试集评估,但如果测试数据也被过拟合,我们将无从判断模型的真实表现。
三分数据法:为了解决这个问题,通常将数据集划分为三部分:
训练集:用于模型训练。
验证集:用于评估模型并调整超参数。
测试集:仅用于最终评估,理想情况下只用一次。
在实践中,由于数据有限,我们可能无法严格区分验证集和测试集。许多实验实际上报告的是验证集的性能,而非严格意义上的测试集性能。
2. 折交叉验证:解决数据稀缺问题
当训练数据不足以同时构建训练集和验证集时,可以使用 折交叉验证(k-fold cross-validation),方法如下:
- 数据划分:将原始训练数据分成 k个不重叠的子集。
- 重复训练和验证:进行 k 次训练,每次用 k−1个子集作为训练数据,剩余的 1 个子集作为验证数据
- 计算平均误差:最终将 k 次验证的结果取平均,作为模型的综合表现指标。
通过折交叉验证,我们可以高效利用有限的数据,减少因数据划分带来的偏差,同时提升模型选择的准确性。
三、欠拟合还是过拟合
在深度学习中,理解欠拟合和过拟合是模型优化的重要步骤。当我们比较训练误差和验证误差时,需关注以下两种典型情形:
欠拟合(Underfitting)
- 现象描述:训练误差和验证误差都很高,并且两者之间的差距很小。
- 原因分析:
模型无法有效降低训练误差,表明模型的表达能力不足,无法捕获数据中的模式。
这种情况通常出现在模型过于简单时。 - 解决思路:可以尝试提高模型复杂性,例如增加模型参数数量、调整网络结构或使用更复杂的模型。
- 总结:欠拟合是由于模型对数据的学习能力不足导致的,通常需要增强模型的表达能力来缓解。
2.过拟合(Overfitting)
现象描述:训练误差远低于验证误差。
原因分析:
模型在训练数据上表现优异,但泛化能力差,难以在未见过的数据上保持良好的性能。
过拟合的模型对训练数据“记得太多”,忽略了数据中的普遍规律。注意事项:
过拟合并非总是有害,尤其在深度学习领域,验证误差优先于训练误差与验证误差的差距。
某些情况下,轻微的过拟合可能导致更好的模型预测性能。解决思路:可通过正则化、数据增强、减少模型复杂度或增加训练数据量来缓解过拟合。
3.综合分析
模型是否欠拟合或过拟合,与以下两大因素密切相关:
- 模型复杂性:决定了模型的表达能力。
- 训练数据集大小:影响模型的泛化能力。 在后续部分中,我们将具体探讨这两个方面。
4.模型复杂性
为直观理解模型复杂性对欠拟合和过拟合的影响,以下通过多项式回归的例子展开说明。
1. 多项式回归的定义
假设我们有一组由单一特征 x和对应标签 𝑦组成的训练数据,尝试使用以下 n 阶多项式来拟合标签:
- 特征:模型的输入变量是 x\mathbf{x}x 的不同次幂。
- 权重:模型的参数为 [w0,w1,⋯ ,wn][w_0,w_1,\cdots,w_n][w0,w1,⋯,wn]。
- 偏置:常数项 𝑏𝑏b。
- 损失函数:采用平方误差,用于衡量模型预测值 𝑦\mathbf{\hat{𝑦}}y 和真实值 𝑦\mathbf{𝑦}y 之间的偏差。
2. 模型复杂性与误差关系
- 高阶多项式:参数数量多,能够表达更复杂的函数,因而具有更强的拟合能力。
-
低阶多项式:参数数量少,拟合能力有限,可能导致欠拟合。
例如,假设数据样本数量为 𝑚,若多项式阶数 𝑛=m−1,则模型可以完美拟合训练数据,但这往往伴随着过拟合风险。在下图中,展示了模型复杂性(多项式阶数)与欠拟合和过拟合之间的关系。
image.png
5.数据集大小
训练数据集的规模同样是决定模型性能的重要因素:
1. 数据集大小对泛化的影响
- 当训练样本较少时,模型更容易过拟合。
- 随着训练数据量的增加,泛化误差通常会减小,因为更多的数据有助于模型学习更普遍的规律。
2. 模型复杂性与数据量的匹配
- 数据量少:简单模型可能更适合,能避免因数据不足导致的严重过拟合。
- 数据量多:可以尝试更复杂的模型,从而挖掘数据中的深层模式。
- 总结:数据量的增加通常是有益的,但需要根据任务需求和数据分布,合理选择模型的复杂性。
3. 深度学习的特殊性
- 深度学习模型往往需要海量数据才能展现其优势。
- 例如,在许多任务中,深度学习模型只有在有数千个训练样本时,才能显著超越传统线性模型。
- 背景支持:深度学习的蓬勃发展得益于廉价存储、互联网设备普及和数字经济带来的大规模数据。
四、多项式回归代码实践
我们现在可以通过多项式拟合来探索这些概念。
import math
import numpy as np
import torch
from torch import nn
from torch.utils.data import DataLoader, TensorDataset
import d2l
1. 生成数据集
为了研究多项式回归模型的拟合效果,我们采用一个三阶多项式生成训练和测试数据标签,公式如下:
其中,噪声
由于在优化过程中需要避免非常大的梯度值或损失值,我们将特征从 𝑥调整为 𝑥/𝑘!。其中 𝑘! 是 𝑘的阶乘)。
我们生成了训练集和测试集各100个样本。以下是数据生成的代码:
# 设置参数
max_degree = 20 # 多项式最大阶数
n_train, n_test = 100, 100 # 训练和测试集大小
true_w = np.zeros(max_degree) # 初始化系数
true_w[:4] = np.array([5, 1.2, -3.4, 5.6]) # 只保留前四项的系数
# 生成特征
# 从标准正态分布中生成 (n_train + n_test, 1) 形状的随机数作为特征。
features = np.random.normal(size=(n_train + n_test, 1))
# 随机打乱生成的特征,以保证训练和测试数据的随机性
np.random.shuffle(features)
# 构造多项式特征矩阵
# 通过将每个特征升幂,生成多项式的各阶特征。poly_features 的形状为 (n_train + n_test, max_degree)
poly_features = np.power(features, np.arange(max_degree).reshape(1, -1))
print(poly_features.shape) # (200, 20)
# 对每一列(对应不同阶数)进行归一化,除以阶数的阶乘(gamma(n) = (n-1)!)
for i in range(max_degree):
poly_features[:, i] /= math.gamma(i + 1) # gamma(n)=(n-1)!
# 生成标签
# 根据多项式方程生成对应的标签值,即对 poly_features 和 true_w 的内积
labels = np.dot(poly_features, true_w)
# 在生成的标签上添加高斯噪声,模拟真实数据中的误差。噪声的标准差为 0.1
labels += np.random.normal(scale=0.1, size=labels.shape)
print(labels.shape) # (200,)
# 转换为张量
# 将 numpy 数据转换为 PyTorch 张量,方便后续使用深度学习框架进行建模和训练
# 数据类型为 float32(单精度浮点数)
true_w, features, poly_features, labels = [
torch.tensor(x, dtype=torch.float32) for x in [true_w, features, poly_features, labels]
]
2. 数据样本示例
我们可以查看生成数据的前两个样本,包括特征、多项式特征以及对应的标签:
print(features[:2], poly_features[:2, :], labels[:2])
输出结果示例:
tensor([[-0.5703],
[-0.8417]]) tensor([[ 1.0000e+00, -5.7029e-01, 1.6262e-01, -3.0913e-02, 4.4074e-03,
-5.0270e-04, 4.7781e-05, -3.8927e-06, 2.7750e-07, -1.7584e-08,
1.0028e-09, -5.1989e-11, 2.4708e-12, -1.0839e-13, 4.4152e-15,
-1.6786e-16, 5.9832e-18, -2.0072e-19, 6.3593e-21, -1.9088e-22],
[ 1.0000e+00, -8.4166e-01, 3.5420e-01, -9.9372e-02, 2.0909e-02,
-3.5197e-03, 4.9374e-04, -5.9366e-05, 6.2458e-06, -5.8409e-07,
4.9161e-08, -3.7615e-09, 2.6383e-10, -1.7081e-11, 1.0269e-12,
-5.7620e-14, 3.0311e-15, -1.5007e-16, 7.0170e-18, -3.1084e-19]]) tensor([3.5675, 2.0859])
3. 模型定义与训练
评估函数 我们定义了一个函数,用于评估模型在给定数据集上的损失:
def evaluate_loss(net, data_iter, loss):
"""评估模型在数据集上的损失"""
metric = d2l.Accumulator(2)#用于累加损失和样本数量
for X, y in data_iter:
out = net(X)#使用模型 net 对输入数据 X 进行预测。
y = y.reshape(out.shape)#将标签 y 的形状调整为与预测输出 out 相同,以便计算损失。
l = loss(out, y)#计算预测输出与真实标签之间的损失。
metric[0] += l.sum().item()#累加当前批次的总损失。
metric[1] += l.numel()#累加当前批次的样本数量。
return metric[0] / metric[1]
训练函数 训练函数如下:
#train_features: 训练集的特征数据。
#test_features: 测试集的特征数据。
#train_labels: 训练集的标签数据。
#test_labels: 测试集的标签数据。
#num_epochs: 训练的轮数,默认为400。
def train(train_features, test_features, train_labels, test_labels, num_epochs=400):
#定义损失函数: loss = nn.MSELoss(reduction='none') 使用均方误差损失函数(MSE),不进行内部求和,以便后续计算梯度。
loss = nn.MSELoss(reduction='none')
#input_shape = train_features.shape[-1] 获取输入特征的维度。
input_shape = train_features.shape[-1]
#net = nn.Sequential(nn.Linear(input_shape, 1, bias=False)) 定义一个线性模型,没有偏置项。
net = nn.Sequential(nn.Linear(input_shape, 1, bias=False))
#设置批量大小,最大为10。
batch_size = min(10, train_labels.shape[0])
#train_iter 和 test_iter 分别为训练集和测试集创建数据加载器。
train_iter = DataLoader(TensorDataset(train_features, train_labels.reshape(-1, 1)), batch_size=batch_size,
shuffle=True)
test_iter = DataLoader(TensorDataset(test_features, test_labels.reshape(-1, 1)), batch_size=batch_size,
shuffle=False)
#使用随机梯度下降(SGD)优化器,学习率为0.01。
trainer = torch.optim.SGD(net.parameters(), lr=0.01)
#对于每个批次,计算损失 l = loss(net(X), y),然后进行反向传播和参数更新。
#每20个epoch或最后一个epoch,评估并打印训练集和测试集的损失。
for epoch in range(num_epochs):
for X, y in train_iter:
l = loss(net(X), y)
trainer.zero_grad()
l.mean().backward()
trainer.step()
if epoch % 20 == 0 or epoch == num_epochs - 1:
train_loss = evaluate_loss(net, train_iter, loss)
test_loss = evaluate_loss(net, test_iter, loss)
print(f"Epoch {epoch + 1}, Train loss: {train_loss:.4f}, Test loss: {test_loss:.4f}")
print("Learned weights:", net[0].weight.data.numpy())
print('Real weights:', true_w[:4])
代码解析
TensorDataset(train_features, train_labels.reshape(-1, 1)) 中 reshape(-1, 1) 的作用是调整 train_labels 的形状,使其符合模型输入和训练的要求。
−1 表示让 NumPy 或 PyTorch 自动计算该维度的大小,以保证数据总元素数量不变。
如果 train_labels 原来的形状是 (100,,即 100 个一维标量,reshape(-1, 1) 会将其转换为(100,1),即 100个一维向量。
print(poly_features[:n_train, :4].shape, poly_features[n_train:, :4].shape)
# 输出:torch.Size([100, 4]) torch.Size([100, 4])
print(labels[:n_train].shape, labels[n_train:].shape)
# 输出:torch.Size([100]) torch.Size([100])
print(poly_features[:10, :4])
"""
tensor([[ 1.0000e+00, 2.0456e-01, 2.0922e-02, 1.4266e-03],
[ 1.0000e+00, -3.5547e-01, 6.3179e-02, -7.4861e-03],
[ 1.0000e+00, 2.8585e-01, 4.0855e-02, 3.8928e-03],
[ 1.0000e+00, -4.3815e-01, 9.5988e-02, -1.4019e-02],
[ 1.0000e+00, -1.1253e+00, 6.3312e-01, -2.3748e-01],
[ 1.0000e+00, -1.4531e+00, 1.0558e+00, -5.1140e-01],
[ 1.0000e+00, 5.3094e-01, 1.4095e-01, 2.4945e-02],
[ 1.0000e+00, 1.2345e+00, 7.6202e-01, 3.1357e-01],
[ 1.0000e+00, 3.1758e-01, 5.0430e-02, 5.3386e-03],
[ 1.0000e+00, 3.4239e-02, 5.8615e-04, 6.6896e-06]])
"""
print(labels[:10].shape) # 输出:torch.Size([10])
print(labels[:10].reshape(-1, 1).shape) # torch.Size([10, 1])
4.不同模型的拟合效果
1.三阶多项式函数(正常)
我们选择前三阶多项式特征,模型能够有效拟合:
train(poly_features[:n_train, :4], poly_features[n_train:, :4], labels[:n_train], labels[n_train:])
示例输出:
Epoch 1, Train loss: 27.4665, Test loss: 32.7723
Epoch 21, Train loss: 1.0210, Test loss: 1.1505
Epoch 41, Train loss: 0.1934, Test loss: 0.1991
Epoch 61, Train loss: 0.0785, Test loss: 0.0748
Epoch 81, Train loss: 0.0400, Test loss: 0.0381
Epoch 101, Train loss: 0.0233, Test loss: 0.0230
Epoch 121, Train loss: 0.0157, Test loss: 0.0164
Epoch 141, Train loss: 0.0123, Test loss: 0.0135
Epoch 161, Train loss: 0.0107, Test loss: 0.0121
Epoch 181, Train loss: 0.0100, Test loss: 0.0115
Epoch 201, Train loss: 0.0097, Test loss: 0.0113
Epoch 221, Train loss: 0.0095, Test loss: 0.0112
Epoch 241, Train loss: 0.0095, Test loss: 0.0111
Epoch 261, Train loss: 0.0094, Test loss: 0.0111
Epoch 281, Train loss: 0.0094, Test loss: 0.0111
Epoch 301, Train loss: 0.0094, Test loss: 0.0111
Epoch 321, Train loss: 0.0094, Test loss: 0.0111
Epoch 341, Train loss: 0.0094, Test loss: 0.0111
Epoch 361, Train loss: 0.0094, Test loss: 0.0111
Epoch 381, Train loss: 0.0094, Test loss: 0.0111
Epoch 400, Train loss: 0.0094, Test loss: 0.0111
Learned weights: [[ 4.9874744 1.1961786 -3.3959963 5.6040487]]
Real weights: tensor([ 5.0000, 1.2000, -3.4000, 5.6000])
2.线性函数(欠拟合)
使用前两维特征(常数和线性项),模型表现欠拟合:
train(poly_features[:n_train, :2], poly_features[n_train:, :2], labels[:n_train], labels[n_train:])
输出:
Epoch 1, Train loss: 18.0453, Test loss: 22.1446
Epoch 21, Train loss: 6.1198, Test loss: 9.4910
Epoch 41, Train loss: 6.1106, Test loss: 9.4209
Epoch 61, Train loss: 6.1106, Test loss: 9.4169
Epoch 81, Train loss: 6.1106, Test loss: 9.4145
Epoch 101, Train loss: 6.1106, Test loss: 9.4129
Epoch 121, Train loss: 6.1106, Test loss: 9.4154
Epoch 141, Train loss: 6.1108, Test loss: 9.4014
Epoch 161, Train loss: 6.1106, Test loss: 9.4089
Epoch 181, Train loss: 6.1107, Test loss: 9.4055
Epoch 201, Train loss: 6.1106, Test loss: 9.4084
Epoch 221, Train loss: 6.1107, Test loss: 9.4060
Epoch 241, Train loss: 6.1106, Test loss: 9.4144
Epoch 261, Train loss: 6.1106, Test loss: 9.4107
Epoch 281, Train loss: 6.1106, Test loss: 9.4182
Epoch 301, Train loss: 6.1106, Test loss: 9.4088
Epoch 321, Train loss: 6.1107, Test loss: 9.4225
Epoch 341, Train loss: 6.1107, Test loss: 9.4194
Epoch 361, Train loss: 6.1106, Test loss: 9.4084
Epoch 381, Train loss: 6.1106, Test loss: 9.4084
Epoch 400, Train loss: 6.1106, Test loss: 9.4101
Learned weights: [[3.6566427 3.2862158]]
Real weights: tensor([ 5.0000, 1.2000, -3.4000, 5.6000])
3.高阶多项式函数(过拟合)
当使用所有特征时,模型容易受到噪声影响,表现出过拟合:
train(poly_features[:n_train, :], poly_features[n_train:, :], labels[:n_train], labels[n_train:])
输出:
Epoch 1, Train loss: 15.5029, Test loss: 25.8490
Epoch 21, Train loss: 1.4607, Test loss: 4.6553
Epoch 41, Train loss: 0.7208, Test loss: 2.5564
Epoch 61, Train loss: 0.4706, Test loss: 1.6853
Epoch 81, Train loss: 0.3304, Test loss: 1.1692
Epoch 101, Train loss: 0.2364, Test loss: 0.8152
Epoch 121, Train loss: 0.1707, Test loss: 0.5680
Epoch 141, Train loss: 0.1244, Test loss: 0.3973
Epoch 161, Train loss: 0.0916, Test loss: 0.2772
Epoch 181, Train loss: 0.0686, Test loss: 0.1941
Epoch 201, Train loss: 0.0522, Test loss: 0.1367
Epoch 221, Train loss: 0.0406, Test loss: 0.0972
Epoch 241, Train loss: 0.0325, Test loss: 0.0699
Epoch 261, Train loss: 0.0267, Test loss: 0.0511
Epoch 281, Train loss: 0.0226, Test loss: 0.0385
Epoch 301, Train loss: 0.0196, Test loss: 0.0302
Epoch 321, Train loss: 0.0176, Test loss: 0.0247
Epoch 341, Train loss: 0.0161, Test loss: 0.0212
Epoch 361, Train loss: 0.0150, Test loss: 0.0191
Epoch 381, Train loss: 0.0142, Test loss: 0.0179
Epoch 400, Train loss: 0.0137, Test loss: 0.0173
Learned weights: [[ 4.9919233 1.342978 -3.378484 5.031814 -0.02400154 1.2797346
0.13397598 0.22811826 0.20582554 -0.16906859 0.17785117 0.2145141
-0.14681216 0.05617441 0.16947077 0.04571209 0.06838381 0.06836328
0.09902996 0.03400499]]
Real weights: tensor([ 5.0000, 1.2000, -3.4000, 5.6000])
小结
- 欠拟合:模型容量不足,无法捕捉数据的模式,表现为训练与测试损失均较高。
- 过拟合:模型容量过大,学习到噪声,训练损失下降而测试损失升高。
- 正常拟合:模型容量与数据复杂度匹配,训练和测试损失都较低。
在接下来的文章中,我们将探讨如何通过正则化技术(如权重衰减和Dropout)来缓解过拟合问题。