训练误差和泛化误差
训练误差:模型在训练数据集上表现出的误差
泛化误差:模型在任意一个测试数据样本上表现出的误差的期望,并常常通过测试数据集上的误差来近似
计算训练误差和泛化误差可以使用之前介绍过的损失函数,例如线性回归用到的平方损失函数和softmax回归用到的交叉熵损失函数。
以⾼考为例来直观地解释训练误差和泛化误差这两个概念。训练误差可以认为是做往年⾼考试题 (训练题)时的错误率,泛化误差则可以通过真正参加⾼考(测试题)时的答题错误率来近似。假设训 练题和测试题都随机采样于⼀个未知的依照相同考纲的巨⼤试题库。如果让⼀名未学习中学知识的⼩学 ⽣去答题,那么测试题和训练题的答题错误率可能很相近。但如果换成⼀名反复练习训练题的⾼三备考 ⽣答题,即使在训练题上做到了错误率为0,也不代表真实的⾼考成绩会如此。
在机器学习⾥,我们通常假设训练数据集(训练题)和测试数据集(测试题)⾥的每⼀个样本都是从同 ⼀个概率分布中相互独⽴地⽣成的。基于该独⽴同分布假设,给定任意⼀个机器学习模型(含参数), 它的训练误差的期望和泛化误差都是⼀样的。例如,如果我们将模型参数设成随机值(⼩学⽣),那么 训练误差和泛化误差会⾮常相近。但我们从前⾯⼏节中已经了解到,模型的参数是通过在训练数据集上 训练模型⽽学习出的,参数的选择依据了最⼩化训练误差(⾼三备考⽣)。所以,训练误差的期望⼩于 或等于泛化误差。也就是说,⼀般情况下,由训练数据集学到的模型参数会使模型在训练数据集上的表 现优于或等于在测试数据集上的表现。由于⽆法从训练误差估计泛化误差,⼀味地降低训练误差并不意 味着泛化误差⼀定会降低。
机器学习模型应关注降低泛化误差。
模型选择
在机器学习中,通常需要评估若⼲候选模型的表现并从中选择模型。这⼀过程称为模型选择(model selection)。可供选择的候选模型可以是有着不同超参数的同类模型。以多层感知机为例,可以选择隐藏层的个数以及每个隐藏层中隐藏单元个数和激活函数。为了得到有效的模型,我们通常要在模 型选择上下⼀番功夫。下⾯,我们来描述模型选择中经常使⽤的验证数据集(validation data set)。
- 验证数据集
从严格意义上讲,测试集只能在所有超参数和模型参数选定后使⽤⼀次。不可以使⽤测试数据选择模 型,如调参。由于⽆法从训练误差估计泛化误差,因此也不应只依赖训练数据选择模型。鉴于此,我们 可以预留⼀部分在训练数据集和测试数据集以外的数据来进⾏模型选择。这部分数据被称为验证数据 集,简称验证集(validation set)。例如,我们可以从给定的训练集中随机选取⼀⼩部分作为验证 集,⽽将剩余部分作为真正的训练集。
在实际应⽤中,由于数据不容易获取,测试数据极少只使⽤⼀次就丢弃。因此,实践中验证数据集 和测试数据集的界限可能⽐较模糊。
- K折交叉验证
由于验证数据集不参与模型训练,当训练数据不够用时,预留大量的验证数据显得太奢侈。一种改善的方法是K折交叉验证(K-fold cross-validation)。在K折交叉验证中,我们把原始训练数据集分割成K个不重合的子数据集,然后我们做K次模型训练和验证。每一次,我们使用一个子数据集验证模型,并使用其他K-1个子数据集来训练模型。在这K次训练和验证中,每次用来验证模型的子数据集都不同。最后,我们对这K次训练误差和验证误差分别求平均。
欠拟合和过拟合
模型训练中经常出现的两类典型问题:⼀类是模型⽆法得到较低的训练误差,我们 将这⼀现象称作⽋拟合(underfitting);另⼀类是模型的训练误差远⼩于它在测试数据集上的误差, 我们称该现象为过拟合(overfitting)。在实践中,我们要尽可能同时应对⽋拟合和过拟合。虽然有很 多因素可能导致这两种拟合问题,这⾥重点讨论两个因素:模型复杂度和训练数据集⼤⼩。
- 模型复杂度
为了解释模型复杂度,我们以多项式函数拟合为例。给定⼀个由标量数据特征 和对应的标量标签 组成 的训练数据集,多项式函数拟合的⽬标是找⼀个 K阶多项式函数:
来近似 。在上式中,
是模型的权重参数,
是偏差参数。与线性回归相同,多项式函数拟合也使⽤ 平⽅损失函数。特别地,⼀阶多项式函数拟合⼜叫线性函数拟合.。
⾼阶多项式函数模型参数更多,模型函数的选择空间更⼤,所以⾼阶多项式函数⽐低阶多项式函数 的复杂度更⾼。因此,⾼阶多项式函数⽐低阶多项式函数更容易在相同的训练数据集上得到更低的训练 误差。给定训练数据集,模型复杂度和误差之间的关系通常如下图所示。给定训练数据集,如果模型 的复杂度过低,很容易出现⽋拟合;如果模型复杂度过⾼,很容易出现过拟合。应对⽋拟合和过拟合的 ⼀个办法是针对数据集选择合适复杂度的模型。
- 训练数据集⼤⼩
⼀般来说,如果训练数据集中样本数过 少,特别是⽐模型参数数量(按元素计)更少时,过拟合更容易发⽣。此外,泛化误差不会随训练数据 集⾥样本数量增加⽽增⼤。因此,在计算资源允许的范围之内,我们通常希望训练数据集⼤⼀些,特别 是在模型复杂度较⾼时,例如层数较多的深度学习模型。
多项式函数拟合实现
#导入包
import torch
import numpy as np
import sys
sys.path.append("/home/kesci/input")
import d2lzh1981 as d2l
print(torch.__version__)
权重衰减
权重衰减(weight decay)使对过拟合问题的常⽤⽅法。
权᯿衰减等价于 范数正则化(regularization)。正则化通过为模型损失函数添加惩罚项使学出的 模型参数值较⼩,是应对过拟合的常⽤⼿段。我们先描述
范数正则化,再解释它为何⼜称权重衰减。
范数正则化在模型原损失函数基础上添加
范数惩罚项,从⽽得到训练所需要最⼩化的函数。
范 数惩罚项指的是模型权重参数每个元素的平⽅和与⼀个正的常数的乘积。以(线性回归)中的线性回归损失函数:
其中w1,w2是权重参数,b是偏差参数,样本i的输入为,
,标签为
,样本数为n。将权重参数用向量w=[w1,w2]表示,带有L2范数惩罚项的新损失函数为
其中超参数λ>0。当权重参数均为0时,惩罚项最小。当λ较大时,惩罚项在损失函数中的比重较大,这通常会使学到的权重参数的元素较接近0。当λ设为0时,惩罚项完全不起作用。上式中L2范数平方|w|2展开后得到w12+w22。 有了L2范数惩罚项后,在小批量随机梯度下降中,将线性回归一节中权重w1和w2的迭代方式更改为
可⻅, 范数正则化令权重
和
先⾃乘⼩于1的数,再减去不含惩罚项的梯度。因此,
范数正则 化⼜叫权重衰减。权重衰减通过惩罚绝对值较⼤的模型参数为需要学习的模型增加了限制,这可能对过 拟合有效。实际场景中,我们有时也在惩罚项中添加偏差元素的平⽅和。
⾼维线性回归实验
下⾯以⾼维线性回归为例来引⼊⼀个过拟合问题,并使⽤权重衰减来应对过拟合。设数据样本特 征的维度为p 。对于训练数据集和测试数据集中特征为 的任⼀样本,我们使⽤如下的线性 函数来⽣成该样本的标签:
其中噪声项 服从均值为0、标准差为0.01的正态分布。为了较容易地观察过拟合,我们考虑⾼维线性回 归问题,如设维度p ;同时特意把训练数据集的样本数设低,如20。
import torch
import torch.nn as nn
import numpy as np
import sys
sys.path.append("..")
import d2lzh_pytorch as d2l
n_train, n_test, num_inputs = 20, 100, 200
true_w, true_b = torch.ones(num_inputs, 1) * 0.01, 0.05
features = torch.randn((n_train + n_test, num_inputs))
labels = torch.matmul(features, true_w) + true_b
labels += torch.tensor(np.random.normal(0, 0.01,
size=labels.size()), dtype=torch.float)
train_features, test_features = features[:n_train, :],
features[n_train:, :]
train_labels, test_labels = labels[:n_train], labels[n_train:]
从零开始实现权重衰减的⽅法。我们通过在⽬标函数后添加 范数惩罚项来实现权᯿衰减。
# -*- coding: utf-8 -*-
"""
⾼维线性回归实验
使用权重衰减来处理过拟合问题
从零开始实现
"""
#初始化模型
def init_params():
w = torch.randn((num_inputs, 1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
return [w, b]
#定义L2惩罚范数项
def l2_penalty(w):
return (w**2).sum() / 2
#定义和训练测试
batch_size, num_epochs, lr = 1, 100, 0.003
net, loss = d2l.linreg, d2l.squared_loss
dataset = torch.utils.data.TensorDataset(train_features, train_labels)
train_iter = torch.utils.data.DataLoader(dataset, batch_size, shuffle=True)
def fit_and_plot(lambd):
w, b = init_params()
train_ls, test_ls = [], []
for _ in range(num_epochs):
for X, y in train_iter:
# 添加了L2范数惩罚项
l = loss(net(X, w, b), y) + lambd * l2_penalty(w)
l = l.sum()
if w.grad is not None:
w.grad.data.zero_()
b.grad.data.zero_()
l.backward()
d2l.sgd([w, b], lr, batch_size)
train_ls.append(loss(net(train_features, w, b), train_labels).mean().item())
test_ls.append(loss(net(test_features, w, b), test_labels).mean().item())
d2l.semilogy(range(1, num_epochs + 1), train_ls, 'epochs', 'loss', range(1, num_epochs + 1), test_ls, ['train', 'test'])
print('L2 norm of w:', w.norm().item())
#观察过拟合
fit_and_plot(lambd=0)
#使用权重衰减
fit_and_plot(lambd=3)
简洁实现
直接在构造优化器实例时通过 weight_decay 参数来指定权᯿衰减超参数。默认下,PyTorch 会对权᯿和偏差同时衰减。可以分别对权重和偏差构造优化器实例,从⽽只对权重衰减。
"""
简洁实现
"""
def fit_and_plot_pytorch(wd):
# 对权᯿参数衰减。权᯿名称⼀般是以weight结尾
net = nn.Linear(num_inputs, 1)
nn.init.normal_(net.weight, mean=0, std=1)
nn.init.normal_(net.bias, mean=0, std=1)
optimizer_w = torch.optim.SGD(params=[net.weight], lr=lr, weight_decay=wd) # 对权᯿参数衰减
optimizer_b = torch.optim.SGD(params=[net.bias], lr=lr) # 不对偏差参数衰减
train_ls, test_ls = [], []
for _ in range(num_epochs):
for X, y in train_iter:
l = loss(net(X), y).mean()
optimizer_w.zero_grad()
optimizer_b.zero_grad()
l.backward()
# 对两个optimizer实例分别调⽤step函数,从⽽分别更新权᯿和偏差
optimizer_w.step()
optimizer_b.step()
train_ls.append(loss(net(train_features), train_labels).mean().item())
test_ls.append(loss(net(test_features), test_labels).mean().item())
d2l.semilogy(range(1, num_epochs + 1), train_ls, 'epochs', 'loss', range(1, num_epochs + 1), test_ls, ['train', 'test'])
print('L2 norm of w:', net.weight.data.norm().item())
fit_and_plot_pytorch(0)
fit_and_plot_pytorch(3)
丢弃法
除了前⼀节介绍的权重衰减以外,深度学习模型常常使⽤丢弃法(dropout)[1] 来应对过拟合问题。 丢弃法有⼀些不同的变体。本节中提到的丢弃法特指倒置丢弃法(inverted dropout)。
多层感知机中神经网络图描述了一个单隐藏层的多层感知机。其中输入个数为4,隐藏单元个数为5,且隐藏单元hi(i=1,…,5)的计算表达式为
这里ϕ是激活函数,x1,…,x4是输入,隐藏单元i的权重参数为w1i,…,w4i,偏差参数为bi。当对该隐藏层使用丢弃法时,该层的隐藏单元将有一定概率被丢弃掉。设丢弃概率为p,那么有p的概率hi会被清零,有1−p的概率hi会除以1−p做拉伸。丢弃概率是丢弃法的超参数。具体来说,设随机变量ξi为0和1的概率分别为p和1−p。使用丢弃法时我们计算新的隐藏单元hi′
由于,因此
丢弃法不改变其输入的期望值。让我们对之前多层感知机的神经网络中的隐藏层使用丢弃法,一种可能的结果如图所示,其中h2和h5被清零。这时输出值的计算不再依赖h2和h5,在反向传播时,与这两个隐藏单元相关的权重的梯度均为0。由于在训练中隐藏层神经元的丢弃是随机的,即h1,…,h5都有可能被清零,输出层的计算无法过度依赖h1,…,h5中的任一个,从而在训练模型时起到正则化的作用,并可以用来应对过拟合。在测试模型时,我们为了拿到更加确定性的结果,一般不使用丢弃法。
从零开始实现
根据丢弃法的定义,我们可以很容易地实现它。下⾯的 dropout 函数将以 drop_prob 的概率丢弃 X 中的元素。
# -*- coding: utf-8 -*-
"""
丢弃法
从零开始实现
"""
import torch
import torch.nn as nn
import numpy as np
import sys
sys.path.append("..")
import d2lzh_pytorch as d2l
def dropout(X, drop_prob):
X = X.float()
assert 0 <= drop_prob <= 1
keep_prob = 1 - drop_prob
# 这种情况下把全部元素都丢弃
if keep_prob == 0:
return torch.zeros_like(X)
mask = (torch.randn(X.shape) < keep_prob).float()
return mask * X / keep_prob
X = torch.arange(16).view(2, 8)
#其中丢弃概率分别为0、0.5和1。
dropout(X, 0)
dropout(X, 0.5)
dropout(X, 1.0)
#定义模型参数
#的Fashion-MNIST数据集。我们将定义⼀个包含两个隐藏层的多层感知机,其中两个隐藏层的输出个数都是256。
num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256
W1 = torch.tensor(np.random.normal(0, 0.01, size=(num_inputs, num_hiddens1)), dtype=torch.float, requires_grad=True)
b1 = torch.zeros(num_hiddens1, requires_grad=True)
W2 = torch.tensor(np.random.normal(0, 0.01, size=(num_hiddens1, num_hiddens2)), dtype=torch.float, requires_grad=True)
b2 = torch.zeros(num_hiddens2, requires_grad=True)
W3 = torch.tensor(np.random.normal(0, 0.01, size=(num_hiddens2, num_outputs)), dtype=torch.float, requires_grad=True)
b3 = torch.zeros(num_outputs, requires_grad=True)
params = [W1, b1, W2, b2, W3, b3]
#定义模型
#下⾯定义的模型将全连接层和激活函数ReLU串起来,并对每个激活函数的输出使⽤丢弃法。分别设置各个层的丢弃概率。通常的建议是把靠近输⼊层的丢弃概率设得⼩⼀点。在这个实验中,我们把
#第⼀个隐藏层的丢弃概率设为 0.2 ,把第⼆个隐藏层的丢弃概率设为 0.5 。可以通过参数 is_training 函数来判断运⾏模式为训练还是测试,并只需在训练模式下使⽤丢弃法。
drop_prob1, drop_prob2 = 0.2, 0.5
def net(X, is_training=True):
X = X.view(-1, num_inputs)
H1 = (torch.matmul(X, W1) + b1).relu()
if is_training: # 只在训练模型时使⽤丢弃法
H1 = dropout(H1, drop_prob1) # 在第⼀层全连接后添加丢弃层
H2 = (torch.matmul(H1, W2) + b2).relu()
if is_training:
H2 = dropout(H2, drop_prob2) # 在第⼆层全连接后添加丢弃层
return torch.matmul(H2, W3) + b3
# 本函数已保存在d2lzh_pytorch
def evaluate_accuracy(data_iter, net):
acc_sum, n = 0.0, 0
for X, y in data_iter:
if isinstance(net, torch.nn.Module):
net.eval() # 评估模式, 这会关闭dropout
acc_sum += (net(X).argmax(dim=1) == y).float().sum().item()
net.train() # 改回训练模式
else: # ⾃定义的模型
if('is_training' in net.__code__.co_varnames): # 如果有is_training这个参数
# 将is_training设置成False
acc_sum += (net(X, is_training=False).argmax(dim=1) == y).float().sum().item()
else:
acc_sum += (net(X).argmax(dim=1) == y).float().sum().item()
n += y.shape[0]
return acc_sum / n
#训练和测试模型
num_epochs, lr, batch_size = 5, 100.0, 256
loss = torch.nn.CrossEntropyLoss()
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, params, lr)
简洁实现
在PyTorch中,我们只需要在全连接层后添加 Dropout 层并指定丢弃概率。在训练模型时, Dropout层将以指定的丢弃概率随机丢弃上⼀层的输出元素;在测试模型时(即 model.eval()后), Dropout 层并不发挥作⽤。
下面代码用到的数据集和上面的一样。
"""
简洁实现
"""
net = nn.Sequential(
d2l.FlattenLayer(),
nn.Linear(num_inputs, num_hiddens1),
nn.ReLU(),
nn.Dropout(drop_prob1),
nn.Linear(num_hiddens1, num_hiddens2),
nn.ReLU(),
nn.Dropout(drop_prob2),
nn.Linear(num_hiddens2, 10)
)
for param in net.parameters():
nn.init.normal_(param, mean=0, std=0.01)
#训练并测试
optimizer = torch.optim.SGD(net.parameters(), lr=0.5)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, optimizer)