前文回顾:多层感知机、详细代码实现
首先,我们学习模型选择,如何选择超参数。
例子: 根据模考成绩来预测未来考试分数。
- 在过去的考试中表现很好(训练误差),不代表未来考试一定会好(泛化误差)。
过拟合和欠拟合的一个简单例子如下:
欠拟合 | 正常 | 过拟合 |
---|---|---|
我们需要根据数据的简单和复杂程度来选择模型容量。
通常,处理简单的数据需要比较低的模型容量,来获得一个比较正常的结果。如果在简单的数据上使用了较高的模型容量,就容易产生过拟合。
而对于复杂的数据,我们需要使用较高的模型容量。过低的模型容量可能会导致无法很好地拟合复杂数据,从而导致欠拟合。
随着模型容量的增加,模型的训练误差逐渐下降,模型会记住更多数据中的信息。但数据中存在噪音,因此泛化误差一开始随模型容量的增加而下降,但到一定程度会反而会上升。因此,模型容量存在一个最优值。
我们难以在不同种类的算法之间比较模型容量,例如:树模型和神经网络。
对于一个给定的模型种类,有两个主要因素影响他的模型容量:参数的个数和参数值的选择范围。
我们通过代码来研究一下模型选择和欠拟合、过拟合之间的关系。
import math
import numpy as np
import torch
from torch import nn
from d2l import torch as d2l
我们通过多项式拟合来探索这些概念,使用一下三阶多项式来生成训练和测试数据的标签:
y = 5 + 1.2 x − 3.4 x 2 2 ! + 5.6 x 3 3 ! + ϵ w h e r e ϵ ∼ N ( 0 , 0. 1 2 ) y=5+1.2x-3.4\frac{x^2}{2!}+5.6\frac{x^3}{3!}+ \epsilon \; \mathop{where} \; \epsilon \sim N(0, 0.1^2) y=5+1.2x−3.42!x2+5.63!x3+ϵwhereϵ∼N(0,0.12)
# 使用三阶多项式来生成训练和测试数据的标签
max_degree = 40
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)
labels = np.dot(poly_features, true_w)
labels += np.random.normal(scale=0.1, size=labels.shape)
这里的
n_test
是验证集的数量
上述代码中,我们创建了一个长为 40
的向量 true_w
,它只有前4个值为有意义的数值,后36位全为0(噪音)。此外,我们使用 np.random.normal()
方法添加噪音。
我们可以观察一些前2个样本:
# 看一下前两个样本
true_w, features, poly_features, labels = [
torch.tensor(x, dtype=torch.float32)
for x in [true_w, features, poly_features, labels]
]
实现一个函数来评估模型在给定数据集上的损失。
def evaluate_loss(net, data_iter, loss):
""" 评估给定数据集上模型的损失 """
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]
接下来,我们定义一个训练函数。
# 定义训练函数
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)))
d2l.plt.show()
print('weight: ', net[0].weight.data.numpy())
我们在训练模型的过程中,使用验证集进行三阶多项式函数拟合。
正态: 我们使用前4位有效信息训练模型,可以得到比较好的拟合效果。因为我们之前构造人工数据集的时候只有前4位是有效的数据,所以数据与模型是匹配的。
# 拟合 正态
train(poly_features[:n_train, :4], poly_features[n_train:, :4],
labels[:n_train], labels[n_train:])
线性函数拟合(欠拟合): 我们只使用前两位有效信息进行拟合,由于少使用了一半的有效数据,所以产生了比较严重的欠拟合。
# 拟合 欠拟合
train(poly_features[:n_train, :2], poly_features[n_train:, :2],
labels[:n_train], labels[n_train:])
高阶多项式函数拟合(过拟合): 我们使用全部的40维数据训练模型,由于后36维饱含噪音,所以产生过拟合。我们可以观察到在 epoch = 300 左右的地方,损失值略有增加。
# 拟合 过拟合
train(poly_features[:n_train, :], poly_features[n_train:, :],
labels[:n_train], labels[n_train:], num_epochs=1500)