Tips
前言
1.本系列是对《动手学深度学习2.0.0》书中代码的复现,这本书由阿斯顿·张,李沐等编写,其介绍了深度学习领域的一些基本的背景知识,概念逻辑,使用PyTorch框架实现了一些基本的神经网络结构。
2.更详细的书籍介绍,代码展示以及视频讲解可以在https://zh.d2l.ai中查看,或者可以点击文末的阅读原文查看详细信息。
无论是对于机器的学习还是对于其它学科规律的探索,最终的目的都是期望发现或者挖掘一种潜在的模式,其可以在最大的程度上概括某一种事物或者现象在时间域、空间域或者频率域等中的分布特征,但是这些规律根据它们的适用范围的不同也有不同的特点,借用机器学习中的术语表示即存在一定的欠拟合或者过拟合等状况。
其中过拟合(overfitting)表示的是找到的规律在训练数据上的表现比潜在的分布更好,用于对抗过拟合的技术称为正则化(Regularization)。而有的时候找到的规律对于训练数据的表现较为糟糕,不能很好地概括数据的分布特点,这种现象称为欠拟合(underfitting)。
训练误差(training error)是指:模型在训练数据集上计算得到的误差。 泛化误差(generalization error)是指:模型应用在同样从原始样本的分布中抽取的无限多数据样本时,模型误差的期望。
但是在实际的问题中对于泛化误差的求解是不可能的,因为无限多的数据样本是一个虚构的对象。因此,在实际的操作过程中,通常都是将训练得到的模型应用于一个独立的测试集来估计泛化误差。
1.1 统计学习理论
这里需要着重介绍的就是在进行机器学习的时候对于训练数据集的一个十分严苛的规定——所有的训练数据都需要满足独立同分布的条件,其本质目的是为了保证模型的泛化性能。但是实际上这种假设在实际的训练情形中十分难以达到,因为数据与数据之间不可能完全独立或者满足相同的分布。因此,很多在进行机器学习分析数据的时候大多都会在一定的程度上违反独立同分布的假设。虽然使用深层神经网络在一定程度上能解决这种问题,但是其提升模型的泛化性能的理论基础仍待进一步研究。
1.2 模型的复杂性
模型的复杂性由什么构成是一个十分复杂的问题,一个模型是否能够很好地泛化取决于许多的影响因素。在统计学上,一般认为能够轻松解释任意事实的模型是复杂的,而表达能力有限但仍然能够很好地解释数据的模型可能更具有现实用途。具体来说,如果一个理论能够拟合数据并且由具体的测试可以用来证明它是错误的,那么这个模型就是一个好的模型。简单而言,影响一个模型泛化地因素主要包括以下部分:
(1)可调整的参数数量:当自由度很大的时候,模型往往更容易拟合。
(2)参数采用值:当权重的取值范围比较大的时候,模型更容易过拟合。
(3)训练模型使用的数据量:即使模型很简单,也很容易过拟合一个只有一到两个样本的数据集,但是当过拟合一个有数百万样本的数据集则需要一个十分灵活的模型,此时模型的复杂程度相对较高。
2.1 验证集
原则上,在确定所有的超参数之前,一般情况下都不希望用到测试集,因为如果在模型选择的过程中使用了测试数据那么就会存在过拟合测试数据的风险。在实际应用中,情况变得更加复杂。 虽然理想情况下我们只会使用测试数据一次, 以评估最好的模型或比较一些模型效果,但现实是测试数据很少在使用一次后被丢弃。 我们很少能有充足的数据来对每一轮实验采用全新测试集。
解决这种问题的常见做法是将数据分成了三份,除了训练和测试数据之外,还增加了一个验证数据集,也叫验证集(validation set)。但是在实际实验中,验证数据和测试数据之间的边界十分模糊。
2.2 K折交叉验证
当用于训练的数据十分稀缺的时候,可能无法提供一个足够的数据来构成一个合适的验证集。这种问题常见的解决方案就是使用K折交叉验证。
其中,原始的训练数据被分成了K个不重叠的子集,然后执行K次模型训练与验证,每次在K-1个原有的数据集上进行训练,并在剩余的一个子集上进行验证,最后通过K次实验的结果取平均来估计训练误差和验证误差。
3.1 生成数据集
给定x,可以使用三阶多项式来生成训练和测试数据的标签:
其中噪声项?服从均值为0且标准差为0.1的正态分布。在优化的过程中,通常会希望避免非常大的梯度值或者损失值。因此在这里调整了多项式的取值,使之除以了一个阶乘。为了方便进行多项式的回归操作,这里首先需要针对训练集和测试集各生成100个数据样本。
import math
import numpy as np
import torch
from torch import nn
from d2l import torch as d2l
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)
同样,存储在poly_features中的单项式由gamma函数重新缩放。从生成的数据集中查看前两个样本,第一个值是与偏置相对应的常量特征。
# 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]
查看结果:
(tensor([[-0.2521],
[-0.4423]]),
tensor([[ 1.0000e+00, -2.5208e-01, 3.1772e-02, -2.6697e-03, 1.6825e-04,
-8.4824e-06, 3.5637e-07, -1.2834e-08, 4.0439e-10, -1.1326e-11,
2.8552e-13, -6.5430e-15, 1.3745e-16, -2.6652e-18, 4.7989e-20,
-8.0648e-22, 1.2706e-23, -1.8841e-25, 2.6386e-27, -3.5007e-29],
[ 1.0000e+00, -4.4235e-01, 9.7835e-02, -1.4426e-02, 1.5953e-03,
-1.4113e-04, 1.0405e-05, -6.5750e-07, 3.6355e-08, -1.7868e-09,
7.9040e-11, -3.1785e-12, 1.1716e-13, -3.9867e-15, 1.2596e-16,
-3.7147e-18, 1.0270e-19, -2.6722e-21, 6.5669e-23, -1.5289e-24]]),
tensor([4.6088, 4.1797]))
3.2 模型训练
首先需要实现一个函数用于评估模型在给定数据集上的损失,计算损失的函数形式如下:
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]
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())
3.3 三阶多项式拟合
首先使用三阶多项式进行拟合,它与数据生成函数的阶数相同。结果表明,该模型能够有效降低训练损失和测试损失。模型所学习的的参数也更加接近真实值,模型的权重值w设置为[5,1.2,-3.4,5.6]。
# 从多项式特征中选择前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:])
输出结果:
weight: [[ 5.012135 1.2095892 -3.4422364 5.5343533]]
3.4 一阶线性多项式拟合(欠拟合)
当采用线性函数进行拟合的时候,减少该模型的训练损失相对困难。在最后一个迭代周期完成之后,训练的损失仍然比较高。当用来拟合非线性模式的时候,线性模型容易欠拟合。
# 从多项式特征中选择前2个维度,即1和x
train(poly_features[:n_train, :2], poly_features[n_train:, :2],
labels[:n_train], labels[n_train:])
输出结果:
weight: [[3.6877737 4.1118197]]
3.5 高阶多项式函数拟合(过拟合)
最后,使用一个阶数相对较高的多项式来训练模型。在这种情况下,没有足够的数据用于学到高阶系数中应该具有接近于零的值。因此,这个过于复杂的模型会轻易受到训练数据中噪声的影响。虽然训练损失可以有效地降低,但是测试损失仍然相对较高。结果表明,复杂模型对数据造成了过拟合。
# 从多项式特征中选取所有维度
train(poly_features[:n_train, :], poly_features[n_train:, :],
labels[:n_train], labels[n_train:], num_epochs=1500)
输出结果:
weight: [[ 5.001528 1.271765 -3.3871417 5.184045 -0.13694283 0.9155818 0.31472698 -0.12597603 0.12591372 -0.18864107 -0.07109772 0.15500595 0.164931 0.08582269 -0.04007841 -0.05222602 0.12040162 0.05486707 -0.129653 0.02811539]]
在多项式的回归中,可以通过拟合多项式的阶数来限制模型的容量。实际上,限制特征的数量是用来缓解过拟合的一种常用技术。但是,简单地丢弃特征对于模型训练来说是不合适的,因此需要由一个更细粒度的工具来调整函数的复杂性,以使其达到一个合适的相对平衡的位置。
在训练参数化机器学习模型的时候,权重衰减(weight decay)是最为广泛使用的正则化技术之一,也被称为L2正则化。要保证权重向量比较小,最常用的方法就是将其范数作为惩罚项添加到最小化损失的问题中。将原来的训练目标调整为最小化预测损失和惩罚项之和。
在之前的线性回归中,使用的损失函数为:
添加惩罚项也即正则化之后的损失函数为:
L2正则化线性模型构成了经典的岭回归算法(ridge regression),L1正则化是统计学中常用的模型,通常被称为套索回归(lasso resgression)。用L2范数的一个原因是它对权重向量的大分量施加了巨大的惩罚。 这使得学习算法偏向于在大量特征上选择均匀分布权重的模型。 在实践中,这可能使它们对单个变量中的观测误差更为稳定。 相比之下,L1惩罚会导致模型将权重集中在一小部分特征上, 而将其他权重清除为零。
4.1 生成数据
首先,还是需要生成一些用于进行分析的数据,生成的公式如下:
这里选择标签是关于输入的线性函数。其中标签同时被均值为0,标准差为0.01的高斯噪声破坏。为了使过拟合的效果更加明显,可以将问题的维数增加到d=200,并使用一个只包含20个样本的小训练集。
# 生成一系列数据,然后使用线性回归模型来拟合这些数据
%matplotlib inline
import torch
from torch import nn
from d2l import torch as d2l
n_train, n_test, num_inputs, batch_size = 20, 100, 200, 5
true_w, true_b = torch.ones((num_inputs, 1)) * 0.01, 0.05 # 真实的权重和偏置
train_data = d2l.synthetic_data(true_w, true_b, n_train)
train_iter = d2l.load_array(train_data, batch_size)
test_data = d2l.synthetic_data(true_w, true_b, n_test)
test_iter = d2l.load_array(test_data, batch_size, is_train=False)
4.2 初始化模型参数
def init_params():
w = torch.normal(0, 1, size=(num_inputs, 1), requires_grad=True)
b = torch.zeros(1, requires_grad=True) # 初始化偏置
return [w, b]
4.3 定义L2范数惩罚项
# L2范数最简单的操作就是对所有的参数进行平方然后求和
def l2_penalty(w):
return torch.sum(w.pow(2)) / 2
4.4 训练代码实现
# 训练的几个参数主要包括权重,偏置,训练的模型,损失函数,训练数据,测试数据,迭代次数,学习率,批量大小
def train(lambd):
w, b = init_params()
net, loss = lambda X: d2l.linreg(X, w, b), d2l.squared_loss
num_epochs, lr = 100, 0.003
animator = d2l.Animator(xlabel='epochs', ylabel='loss', yscale='log', xlim=[5, num_epochs], legend=['train', 'test'])
for epoch in range(num_epochs):
for X, y in train_iter:
with torch.enable_grad():
l = loss(net(X), y) + lambd * l2_penalty(w)
l.sum().backward() # 计算梯度,自动求导
d2l.sgd([w, b], lr, batch_size) # 更新参数
if (epoch + 1) % 5 == 0:
animator.add(epoch + 1, (d2l.evaluate_loss(net, train_iter, loss), d2l.evaluate_loss(net, test_iter, loss)))
print('L2 norm of w:', torch.norm(w).item())
4.5 忽略正则化直接训练
在这里训练误差有了一定的减少,但是在测试集的误差中,仍然处于比较高的水平。
train(lambd=0) # 没有正则化
训练得到的权重结果如下:
norm of w: 13.296727180480957
4.6 使用L2正则化训练
从结果中可以看到训练误差增大,但是测试误差减小,这正是期望从正则化中得到的结果。
由于权重衰减在神经网络中经常使用,为了便于使用这个正则化技术,一般的深度学习框架都会将权重衰减集成到优化算法中。这种集成允许在不增加任何额外开销的情况下向算法中添加权重衰减。
def train_concise(wd):
net = nn.Sequential(nn.Linear(num_inputs, 1))
for param in net.parameters():
param.data.normal_()
loss = nn.MSELoss()
num_epochs, lr = 100, 0.003
# 带有权重衰减的优化器
trainer = torch.optim.SGD([{'params': net.parameters(), 'weight_decay': wd}], lr=lr)
animator = d2l.Animator(xlabel='epochs', ylabel='loss', yscale='log', xlim=[5, num_epochs], legend=['train', 'test'])
for epoch in range(num_epochs):
for X, y in train_iter:
with torch.enable_grad():
trainer.zero_grad()
l = loss(net(X), y)
l.backward()
trainer.step()
if (epoch + 1) % 5 == 0:
animator.add(epoch + 1, (d2l.evaluate_loss(net, train_iter, loss), d2l.evaluate_loss(net, test_iter, loss)))
print('L2 norm of w:', net[0].weight.norm().item())
忽略L2正则化技术(左)与添加L2正则化技术(右)得到的结果如下图: