之前介绍了深度学习--多层感知机,然而,模型训练可能存在欠拟合或者过拟合现象。因此,今天首先介绍模型误差的概念,如何进行模型选择以及过拟合、欠拟合问题,然后用一个例子进行拟合实验,最后介绍过拟合的解决方案。
0
1模型误差
训练误差(training error): 指模型在训练数据集上表现出的误差;泛化误差(generalization error): 指模型在任意一个测试数据样本上表现出的误差的期望,并常常通过测试数据集上的误差来近似。
训练误差和泛化误差可以使用之前介绍过的损失函数,例如线性回归用到的平方损失函数和softmax回归用到的交叉熵损失函数来计算。
0 2模型选择
- 测试集一般只能在所有超参数和模型参数确定后使用一次。不可以使用测试数据选择模型,如调参。由于无法从训练误差估计泛化误差,因此也不应只依赖训练数据选择模型。鉴于此,我们可以预留一部分在训练数据集和测试数据集以外的数据来进行模型选择。这部分数据被称为验证数据集,简称验证集(validation set)。例如,我们可以从给定的训练集中随机选取一小部分作为验证集,而将剩余部分作为真正的训练集。
- 由于验证数据集不参与模型训练,当训练数据不够用时,预留大量的验证数据显得太奢侈。一种改善的方法是K折交叉验证(K-fold cross-validation)。在K折交叉验证中,我们把原始训练数据集分割成K个不重合的子数据集,然后我们做K次模型训练和验证。每一次,我们使用一个子数据集验证模型,并使用其他K-1个子数据集来训练模型。在这K次训练和验证中,每次用来验证模型的子数据集都不同。最后,我们对这K次训练误差和验证误差分别求平均。
0 3过拟合和欠拟合
欠拟合(underfitting): 模型无法得到较低的训练误差;过拟合(overfitting): 模型的训练误差远小于它在测试数据集上的误差。
模型过拟合和欠拟合一般存在两种因素:1.模型复杂度:
当模型过度复杂,则容易出现过拟合;模型太简单,则处于欠拟合。
2.训练数据集大小:
如果训练数据集中样本数过少,特别是比模型参数数量(按元素计)更少时,过拟合更容易发生。此外,泛化误差不会随训练数据集里样本数量增加而增大。因此,在计算资源允许的范围之内,训练数据集应该大一些,特别是在模型复杂度较高时。
0 4实验
此处采用一个多项式拟合来模拟过拟合、欠拟合等现象。
import torchimport numpy as npimport matplotlib.pyplot as pltimport sys# 模型训练并可视化损失函数值def train_and_plot(net, train_features, test_features, train_labels, test_labels, loss, num_epochs, batch_size, params=None, lr=None, optimizer=None, **args): dataset = torch.utils.data.TensorDataset(train_features, train_labels) # 设置数据集 train_iter = torch.utils.data.DataLoader(dataset, batch_size, shuffle = True) # 设置获取数据方式 dataset = torch.utils.data.TensorDataset(test_features, test_labels) # 设置数据集 test_iter = torch.utils.data.DataLoader(dataset, batch_size) # 设置获取数据方式 train_ls, test_ls = [], [] for epoch in range(num_epochs): train_l_sum, train_acc_sum, n = 0.0, 0.0, 0 for X, y in train_iter: if params is not None: y_hat = net(X, params) else: y_hat = net(X) if "l2_Reg" in args: l2_Reg = args["l2_Reg"] lambd = args["lambd"] if params is not None: W = params[0] else: W = net.weight l = loss(y_hat, y.view(-1, 1)) + lambd * l2_Reg(W) else: l = loss(y_hat, y.view(-1, 1)) l = l.sum() # 梯度清零 if optimizer is not None: optimizer.zero_grad() elif params is not None and params[0].grad is not None: for param in params: param.grad.data.zero_() l.backward() # 后向传播计算梯度 if optimizer is None: sgd(params, lr, batch_size) # 参数更新 else: optimizer.step() if params is not None: train_ls.append(loss(net(train_features, params), train_labels.view(-1, 1)).mean().item()) # 将训练损失保存到train_ls中 test_ls.append(loss(net(test_features, params), test_labels.view(-1, 1)).mean().item()) # 将测试损失保存到test_ls中 else: train_ls.append(loss(net(train_features), train_labels.view(-1, 1)).item()) # 将训练损失保存到train_ls中 test_ls.append(loss(net(test_features), test_labels.view(-1, 1)).item()) # 将测试损失保存到test_ls中 print('epoch %d, train loss %.3f, test loss %.3f' % (epoch + 1, train_ls[-1], test_ls[-1])) show_loss(range(1, num_epochs + 1), train_ls, 'epochs', 'loss', range(1, num_epochs + 1), test_ls, ['train', 'test'])def main(): lr = 0.01 batch_size = 10 # 批量大小 n_train = 5 # 训练数据集大小 n_test = 100 # 测试数据集大小 num_epochs = 100 train_features, test_features, train_labels, test_labels = generateData(n_train, n_test) batch_size = min(10, train_labels.shape[0]) net = torch.nn.Linear(train_features.shape[-1], 1) # 初始化网络模型 loss = torch.nn.MSELoss() # 损失函数 默认情况下, reduce = True,则l.sum()和l.mean()一样 optimizer = torch.optim.SGD(net.parameters(), lr = lr) # 设置优化函数,使用的是随机梯度下降优化 train_and_plot(net, train_features, test_features, train_labels, test_labels, loss, num_epochs, batch_size, params = None, lr = lr, optimizer = optimizer) print('weight:', net.weight.data, '\nbias:', net.bias.data)if __name__ == '__main__': main()
欠拟合: 模型训练不足
# 欠拟合 lr = 0.00001 n_train = 100
# final epoch: train loss 295.9873352050781 test loss 237.28042602539062
# weight: tensor([[-0.0926, 0.3235, 1.6614]])
# bias: tensor([-0.4215])
过拟合: 训练数据不足
# 过拟合 lr = 0.01 n_train = 5
# final epoch: train loss 0.9944426417350769 test loss 174.0299072265625
# weight: tensor([[1.7811, 1.9922, 2.2059]])
# bias: tensor([2.7676])
0 3解决方案
对于解决过拟合,主要有权重衰减(L2范数正则化)和丢弃法。
权重衰减:
权重衰减: 等价于
范数正则化(regularization)。正则化通过为模型损失函数添加惩罚项使学出的模型参数值较小,是应对过拟合的常用手段。以线性回归中的线性回归损失函数为例,其损失函数为:
那么带有
范数惩罚项的新损失函数为:
其中,
范数惩罚项指的是模型权重参数每个元素的平方和与一个正的常数的乘积;
。当权重参数均为0时,惩罚项最小。当
较大时,惩罚项在损失函数中的比重较大,这通常会使学到的权重参数的元素较接近0。加上惩罚项后,线性回归权重更新公式可以写为:
范数正则化令权重
和
先自乘小于1的数,再减去不含惩罚项的梯度。因此,
范数正则化又叫权重衰减。权重衰减通过惩罚绝对值较大的模型参数为需要学习的模型增加了限制,这可能对过拟合有效。
# L2惩罚项def l2_penalty(W): return (W**2).sum() / 2def main_l2(): lr = 0.003 lambd = 3 batch_size = 1 # 批量大小 n_train = 100 # 训练数据集大小 n_test = 100 # 测试数据集大小 num_inputs = 200 num_epochs = 100 train_features, test_features, train_labels, test_labels = generateData1(n_train, n_test, num_inputs) # 自己实现 params = init_parameters(num_inputs) net = model # 部分函数见之前的推送 loss = torch.nn.MSELoss() # 损失函数 optimizer = None l2_Reg = l2_penalty train_and_plot(net, train_features, test_features, train_labels, test_labels, loss, num_epochs, batch_size, params = params, lr = lr, optimizer = optimizer, l2_Reg = l2_penalty, lambd = lambd) print('L2 norm of w:', params[0].norm().item())
结果:
# lambd = 0
# epoch 100, train loss 0.000, test loss 0.026
# L2 norm of w: 0.14004801213741302
# lambd = 3
# epoch 100, train loss 0.003, test loss 0.015
# L2 norm of w: 0.06013459712266922
当不使用
范数惩罚项,即上面实验结果
,当使用
范数惩罚项时,即
,可以看出使用
范数惩罚项对过拟合有一定效果。
丢弃法
对于包含隐含层的多层感知机,当对该隐藏层使用丢弃法时,该层的隐藏单元将有一定概率被丢弃掉。假设丢弃概率为
,那么
有
的概率会被清0,有
的概率会除以
被拉伸,即
具体来说,假设随机变量
为0和1的概率分别为
和
,那么
,所以
即丢弃法不改变其输入的期望值。由于在训练中隐藏层神经元的丢弃是随机的,在反向传播时,与被丢弃单元相关的权重的梯度均为0。下一层的输出无法过度依赖上一隐含层的任何一个隐含单元,从而在训练模型时起到正则化的作用,并可以用来应对过拟合。在测试模型时,我们为了拿到更加确定性的结果,一般不使用丢弃法。
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.rand(X.shape) < keep_prob).float() return mask * X / keep_probnet = nn.Sequential( util.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) )
此处使用多层感知机为例,使用FashionMNIST数据验证丢弃法对过拟合的效果。具体的可自行改变drop_prob1和drop_prob2参数进行验证。当drop_prob1=drop_prob2=0时即没有采用丢弃法。