引言
当我们观察我在博文【深度学习】多层感知机(二)MXNet实现双层感知机中的实验结果(下图)时,可以发现:当模型在训练数据集上更加准确时(迭代至第20次时,测试准确率已经接近99%),它在测试数据集上却不一定更加准确(测试数据集准确率不到98%)。这是为什么呢?
图 1
训练误差与泛化误差
下面引用周志华老师经典的西瓜书《机器学习》中的一段话阐述一下训练误差和泛化误差的基本含义(原书23页,2.1节):
通常我们将分类错误的样本数占样本总数的比例称为“错误率(error rate)”,即如果 m m m个样本中有 a a a个样本分类错误,则错误率 E = a / m E=a/m E=a/m。相应的, 1 − a / m 1-a/m 1−a/m称为精度(accuracy),即“精度=1-错误率”。更一般地,将学习器的实际预测输出与样本真实输出之间的差异称为“误差(error)”,学习器在训练数据集上的误差称为“训练误差(training error)”或“经验误差(empirical error)”,学习器在新样本上的误差称之为“泛化误差(generalization error)”。
通俗来讲,训练误差指的是模型在训练数据上表现出的误差,泛化误差指模型在任意一个测试数据样本上表现出的误差的期望,并常常通过测试数据集上的误差来近似。误差可以通过损失函数来进行量化。
《动手学深度学习》一书举了一个很形象的例子:
让我们以高考为例来直观地解释训练误差和泛化误差这两个概念。训练误差可以认为是做往年高考试题(训练题)时的错误率,泛化误差则可以通过真正参加高考(测试题)时的答题错误率来近似。假设训练题和测试题都随机采样于一个未知的依照相同考纲的巨大试题库。如果让一名未学习中学知识的小学生去答题,那么测试题和训练题的答题错误率可能很相近。但如果换成一名反复练习训练题的高三备考生答题,即使在训练题上做到了错误率为 0,也不代表真实的高考成绩会如此。
现在的机器学习模型基本都遵从一个假设:训练数据集和测试数据集里的每一个样本都是从同一个概率分布中相互独立地生成的。基于这个同分布假设,当给定一个机器学习模型,它的训练误差期望和泛化误差都是一样的。然而,我们知道模型的参数是从训练数据集上学习到的,所以训练误差的期望应该小于或者等于泛化误差。
换句话说,由训练数据集学习到的模型参数会使模型在训练数据集上的表现优于或等于测试数据集上的表现。由于无法从训练误差估计泛化误差,一味降低训练误差并不意味着泛化误差一定会降低。然而,从前面的讨论中可以看到:机器学习模型应该着眼于降低泛化误差
过拟合和欠拟合
当模型把训练数据集学习得“太好”之后,很有可能把训练样本的一些特点当做所有潜在样本都会具有的一般性质,导致模型泛化性能降低,这种现象称之为“过拟合(overfitting)”,与之对应的是“欠拟合(underfitting)”。从误差的角度来看,过拟合指训练误差远小于模型在测试数据集上的误差,欠拟合指无法得到较低的训练误差。
一般而言,为了应对过拟合和欠拟合问题,我们一般重点关注两个因素:模型复杂度和训练数据集大小。
- 给定训练数据集,如果模型的复杂度太低,很容易出现欠拟合;如果模型复杂度过高,很容易出现过拟合;
- 一般来说,如果训练数据集中样本数过少,特别是比模型参数数量(按元素计)更少时,过拟合更容易发生。此外,泛化误差不会随训练数据集里样本数量增加而增大。
欠拟合比较容易克服,例如在决策树学习中扩展分支数量、在神经网络学习中增加迭代的次数等,过拟合是机器学习所面临的关键障碍。
模型选择
在现实任务中,我们通常需要评估若干候选模型的表现并从中选择模型,这一过程称之为“模型选择(model selection)”。
通常我们通过实验来对模型的泛化误差进行评估并且选择较优的模型,为此需要“测试数据集”来测试模型对新样本的判别能力,以此估计模型的泛化性能。值得注意的是,在划分数据集时,应该使测试集和训练集尽可能互斥,即测试样本不出现在训练样本中。
下面是三种常用的数据集划分方法。
留出法
留出法(hold-out)直接将数据集划分为两个互斥的集合,其中一个作为训练集,另外一个作为测试集。
单次使用留出法得到的结果往往不够稳定和可靠,故而一般采用若干次随机划分、重复进行实验评估后取均值作为留出法的评估结果。
交叉验证法
验证数据集
严格意义上,测试集只能在所有超参数和模型参数选定后使用一次。我们不可以使用测试数据选择模型,例如调参。由于我们无法从训练误差估计泛化误差,因此也不应只依赖训练数据选择模型。有鉴于此,我们可以预留一部分在训练数据集和测试数据集以外的数据来进行模型选择。这部分数据被称为验证数据集,简称验证集(validation set)。例如,我们可以从给定的训练集中随机选取一小部分作为验证集,而将剩余部分作为真正的训练集。
在实际应用中,由于数据不容易获取,测试数据极少只使用一次就丢弃。因此,实践中验证数据集和测试数据集的界限可能比较模糊。
个人理解,平常我们所说的测试数据集其实就是验证数据集,我们利用它来进行调参,而测试数据集本质上是这个模型实际应用中的数据集。所以除非明确说明,一般使用的测试集就是验证集,实验报告的测试精度就是验证精度。
K折交叉验证
由于验证数据集不参与模型训练,当训练数据不够用时,预留大量的验证数据显得太奢侈。一个改善的方法是 K 折交叉验证( K -fold cross-validation)。在 K 折交叉验证中,我们把原始训练数据集分割成 K 个不重合的子数据集。然后我们做 K 次模型训练和验证。每一次,我们使用一个子数据集验证模型,并使用其他 K−1 个子数据集来训练模型。在这 K 次训练和验证中,每次用来验证模型的子数据集都不同。最后,我们对这 K 次训练误差和验证误差分别求平均。
留一法
假设数据集中包含 m m m个样本,我们令 k = m k=m k=m,则得到了交叉验证法的特例,留一法(leave one out)。相比较于一般地交叉验证法,由于对数据集只有 k = m k=m k=m一种划分方式,避免了随机样本划分的影响。但是当数据集过大时,训练 m m m个模型的计算代价将相当庞大。
自助法
自助法(bootstrapping)以自助采样(bootstrap sampling)为基础。给定数据集 D D D,我们使用如下方式获得数据集 D ′ D' D′:每次随机从 D D D中挑选一个样本放入 D ′ D' D′中,然后将此样本放回 D D D中,使得该样本在下次采样中仍有可能被采到。如此重复 m m m次后,得到一个大小为 m m m的数据集 D ′ D' D′。我们将 D ′ D' D′用作训练集,将 D − D ′ D-D' D−D′作为测试集。
自助法特别适用于数据集较小、难以有效划分训练和测试集的场合。缺点是改变了初始数据集的分布,将引入估计偏差。
过拟合实验
这里使用我前一篇博文【深度学习】多层感知机(二)MXNet实现双层感知机中的实验为例,调整迭代次数,画出“loss-epochs”关系图,观察模型的过拟合现象。
代码
# coding=utf-8
# author: BebDong
# 2018/12/19
# 过拟合实验:双层感知机在MNIST手写数据集
from mxnet.gluon import data as gdata
from mxnet.gluon import loss as gloss
from mxnet.gluon import nn
from mxnet import init, gluon, autograd, nd
from matplotlib import pyplot as plt
from IPython import display
import pylab
# 作图
def semilogy(x_vals, y_vals, x_label, y_label, x2_vals=None, y2_vals=None,
legend=None, figsize=(3.5, 2.5)):
# 矢量图显示并设置大小
display.set_matplotlib_formats('svg')
plt.rcParams['figure.figsize'] = figsize
# 画图
plt.xlabel(x_label)
plt.ylabel(y_label)
plt.semilogy(x_vals, y_vals)
if x2_vals and y2_vals:
plt.semilogy(x2_vals, y2_vals, linestyle=":")
plt.legend(legend)
pylab.show()
# 加载数据,第一次运行将自动下载
mnist_train = gdata.vision.FashionMNIST(train=True)
mnist_test = gdata.vision.FashionMNIST(train=False)
# 将样本的属性值和标签值分开
train_features, train_labels = mnist_train[:]
test_features, test_labels = mnist_test[:]
# 将数据值映射到0~1之间并保持对象类型为mxnet.ndarray
train_features = nd.array(train_features.asnumpy() / 255.0)
test_features = nd.array(test_features.asnumpy() / 255.0)
train_labels = nd.array(train_labels)
test_labels = nd.array(test_labels)
# 按批读取训练数据
batch_size = 256
train_iter = gdata.DataLoader(gdata.ArrayDataset(train_features, train_labels), batch_size, shuffle=True)
# 定义模型
net = nn.Sequential()
net.add(nn.Dense(256, activation='relu'), nn.Dense(10))
# 初始化参数
net.initialize(init.Normal(sigma=0.01))
# 定义损失函数:交叉熵
loss_function = gloss.SoftmaxCrossEntropyLoss()
# 定义优化算法:小批量随机梯度下降
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.3})
# 训练
epochs = 10
train_loss_lst, test_loss_lst = [], []
for epoch in range(epochs):
for feature, label in train_iter:
with autograd.record():
los = loss_function(net(feature), label)
los.backward()
trainer.step(batch_size)
# 记录每次迭代的训练误差和测试误差
train_loss = loss_function(net(train_features), train_labels).mean().asscalar()
test_loss = loss_function(net(test_features), test_labels).mean().asscalar()
train_loss_lst.append(train_loss)
test_loss_lst.append(test_loss)
# 打印信息
print('epoch %d, train loss %.4f, test loss %.4f' % (epoch + 1, train_loss, test_loss))
# 画出loss-epochs关系图
semilogy(range(1, epochs + 1), train_loss_lst, 'epochs', 'loss',
range(1, epochs + 1), test_loss_lst, ['train', 'test'])
结果
设置迭代次数epochs = 10
,得到如下实验结果:
图 2
设置迭代次数epochs = 30
,得到如下实验结果:
图 3
设置迭代次数epochs = 50
,得到如下实验结果:
图 4
结果分析
观察图2可以发现,迭代次数epochs = 10
时,模型的训练误差和在测试数据集上的误差都较低,二者接近,并且呈现持续降低的趋势,这时可以认为模型欠拟合。
观察图3可以发现,当迭代次数epochs = 30
时,模型的训练误差随着迭代次数的增加持续减小,但是当迭代次数超过17次时,在测试数据集上的误差不再明显减小,这时认为模型拟合最佳。这个结果从图1的准确率当中也可以看出来。
观察图4可以发现,当迭代次数epochs = 50
时,模型的训练误差随着迭代次数的增加持续减小,但是当迭代次数超过40次时,在测试数据集上的误差反而开始增加,这时认为模型过拟合。