用 Python 从零开始创建神经网络(十一):使用样本外数据进行测试
使用样本外数据进行测试
- 引言
引言
到目前为止,我们已经创建了一个模型,该模型在我们生成的测试数据集上的预测准确率似乎达到了98%。这些生成的数据是基于spiral_data
函数中明确规定的一组规则生成的。我们的预期是,一个经过良好训练的神经网络可以学习这些规则的表示,并利用这种表示来预测额外生成数据的类别。
想象一下,你训练了一个神经网络模型来读取车辆的车牌。在这种情况下,经过良好训练的模型的预期是,它能够识别未来的车牌示例,并仍然准确地预测它们(在这种情况下,预测是正确识别车牌上的字符)。
神经网络的复杂性既是它最大的优势,也是它最大的挑战。因为具有大量可调参数,神经网络非常擅长“拟合”数据。这既是天赋,也是诅咒,同时也是我们必须不断努力平衡的问题。如果神经元足够多,一个模型可以轻松记住一个数据集;然而,神经元太少则无法对数据进行泛化。这也是我们为什么不能仅仅通过增加神经元数量或使用更大的模型来解决问题的原因之一。
目前,我们尚不确定我们最新的神经网络达到98%准确率是由于有效地学习了底层数据生成函数的意义表示,还是因为过拟合了数据。到目前为止,我们仅仅调整了超参数以在训练数据上实现尽可能高的准确率,而从未尝试用以前未见过的数据来挑战模型。过拟合实际上只是对数据的记忆,而不对其进行任何理解。过拟合的模型在预测已经见过的数据时表现非常出色,但在处理未见过的数据时往往表现显著较差。
数据上的良好泛化(左)和过度拟合(右)
左图展示了一个泛化的例子。在此例中,模型学会了区分红色和蓝色数据点,即使其中一些可能会被错误预测。造成这种情况的一个原因可能是数据中存在一些“令人困惑”的样本。从图中可以看出,例如,如果某些蓝色点不存在,那么数据质量会更高,也更容易拟合。一个好的数据集是神经网络面临的最大挑战之一。右图展示了一个完全记住数据的模型,该模型完美地拟合了数据,但破坏了泛化能力。
如果无法判断一个模型是否对训练数据过拟合,我们就无法信任该模型的结果。因此,将训练数据和测试数据分开用于不同目的至关重要。
训练数据应仅用于训练模型。测试数据(或称样本外数据)应仅用于在训练完成后验证模型的性能(在本章稍后我们将出于演示目的在训练过程中使用测试数据)。这种做法的理念是,保留一部分数据,不参与训练,用于测试模型的性能。
在许多情况下,可以对可用数据进行随机抽样来训练模型,并将剩余的数据作为测试数据集。但仍需非常小心,避免信息泄漏。一种常见的容易出现问题的情况是时间序列数据。假设有从传感器每秒采集一次的数据,可能收集了数百万条观测值。如果随机选择测试数据,很可能测试数据集中有的样本与训练数据集中的样本时间相隔仅一秒,因此非常相似。这可能导致过拟合“溢出”到测试数据中,使模型在训练和测试数据上都表现良好,但并不意味着它具有良好的泛化能力。
随机分配时间序列数据作为测试数据可能会导致测试数据和训练数据非常相似。为了证明模型的泛化能力,两者必须有足够的差异。在时间序列数据中,更好的方法是从数据中提取多个切片,即整个时间段的块,并将其保留用于测试。
类似这样的其他偏差也可能会潜入测试数据集中,因此必须对此保持警惕,仔细考虑是否发生了数据泄漏,以及如何真正隔离样本外数据。
在我们的案例中,可以使用数据生成函数来创建新的数据,作为样本外测试数据:
# Create test dataset
X_test, y_test = spiral_data(samples=100, classes=3)
鉴于前文提到的过拟合问题,仅仅生成更多数据似乎是不妥的,因为测试数据可能与训练数据相似。直觉和经验在发现样本外数据潜在问题方面都非常重要。通过观察数据的图像表示,可以发现由相同函数生成的另一组数据是合适的。这种方式可以说是获取样本外数据时最安全的方法,因为类在边缘处部分混合(此外,我们实际上使用了“底层函数”来生成更多数据)。
利用这些数据,我们通过执行前向传播、计算损失和准确率等与之前相同的方式评估模型性能:
# Validate the model
# Create test dataset
X_test, y_test = spiral_data(samples=100, classes=3)
# Perform a forward pass of our testing data through this layer
dense1.forward(X_test)
# Perform a forward pass through activation function
# takes the output of first dense layer here
activation1.forward(dense1.output)
# Perform a forward pass through second Dense layer
# takes outputs of activation function of first layer as inputs
dense2.forward(activation1.output)
# Perform a forward pass through the activation/loss function
# takes the output of second dense layer here and returns loss
loss = loss_activation.forward(dense2.output, y_test)
# Calculate accuracy from output of activation2 and targets
# calculate values along first axis
predictions = np.argmax(loss_activation.output, axis=1)
if len(y_test.shape) == 2:
y_test = np.argmax(y_test, axis=1)
accuracy = np.mean(predictions==y_test)
print(f'validation, acc: {accuracy:.3f}, loss: {loss:.3f}')
>>>
...
epoch: 9800, acc: 0.963, loss: 0.075, lr: 0.04975621940303483
epoch: 9900, acc: 0.963, loss: 0.074, lr: 0.049753743844839965
epoch: 10000, acc: 0.967, loss: 0.074, lr: 0.04975126853296942
validation, acc: 0.797, loss: 0.921
虽然79.7%的准确率和0.921的损失并不算糟糕,但与训练数据达到97%的准确率和0.049的损失相比,这表明模型存在过拟合现象。在下图中,训练数据被显示为浅色,而验证数据点则覆盖在相同位置,用于比较泛化良好的模型(左图)和过拟合模型(右图)。
左图——泛化良好的模型预测;右图——过拟合模型的预测错误。
当测试数据结果的趋势开始与训练数据明显分离时,我们可以识别出过拟合的迹象。通常情况下,模型在训练数据上的性能会更好,但如果训练损失与测试性能相差超过约10%,根据经验,这是严重过拟合的常见标志。理想情况下,两组数据的性能应该一致。即使存在轻微差异,也意味着模型没有正确预测部分测试样本,暗示了对训练数据的轻微过拟合。在大多数情况下,适度的过拟合并不是严重问题,但我们希望尽量将其最小化。
让我们再次查看该模型的训练过程,但将训练数据、训练准确率和损失的图表显示为浅色。我们在训练数据的对应图表上叠加测试数据及其损失和准确率的图表,以展示该模型的过拟合现象:
代码可视化:https://nnfs.io/zog
这是一种典型的过拟合现象——验证集的损失在初期下降,但一旦模型开始过拟合后便开始上升。可以观察到验证数据中类别点落在了其他类别的影响区域上。在之前,我们并未意识到这一点,仅仅看到训练数据表现得非常好。这就是为什么通常应在训练后使用测试数据对模型进行验证的原因。目前,该模型被调整为在训练数据上取得尽可能高的分数,但这通常意味着学习率过高、训练轮次过多或模型规模过大。当然,也存在其他可能的原因和解决方法,这些将在后续章节中讨论。总体而言,目标是使测试集损失与训练集损失相同,即使这意味着训练集上的损失更高、准确率更低。两组数据表现相似意味着模型进行了泛化,而不是对训练数据过拟合。
如前所述,防止过拟合的一个方法是调整模型的规模。如果模型完全没有学习,可以尝试更大的模型。如果模型能够学习,但训练数据和测试数据之间出现了较大的差异,则可以尝试更小的模型。一个通用的规则是在选择初始模型超参数时,应找到仍然能够学习的最小模型。
其他避免过拟合的方法包括第14章将讨论的正则化技术以及第15章介绍的Dropout层。通常,训练数据和测试数据的分歧可能需要较长时间才会显现。尝试不同模型设置的过程称为超参数搜索。在初期,你可以快速(通常几分钟内)尝试不同设置(例如,层的大小),以观察模型是否能够学习。如果可以,则对模型进行完全训练,或者至少显著延长训练时间,并比较结果以选择最佳的超参数设置。另一种方法是创建一个不同超参数组合的列表,并在每次使用一个组合时对模型进行训练,最后选择表现最好的组合。
其背后的原因在于,神经元越少,模型记住数据的可能性越小。较少的神经元使得神经网络更容易泛化(实际学习数据的意义)而不是记住数据。反之,神经元数量足够多时,神经网络更容易记住数据。请记住,神经网络的目标是降低训练损失,并遵循“阻力最小的路径”来实现这一目标。而我们的任务作为程序员,就是使泛化的路径成为最容易的路径。这通常意味着,我们实际上需要使模型降低损失的路径变得更具挑战性!
本章的章节代码、更多资源和勘误表:https://nnfs.io/ch11