机器学习模型在经过训练后必须使用测试集进行评估。我们这样做是为了确保模型没有过拟合,同时确保它们能够处理现实生活中的数据集,这些数据集可能与训练集相比有些微的分布偏差。
但是,为了使你的模型真正健壮,仅仅使用训练/测试划分来评估可能还不够。
例如,假设你有一个数据集包含来自两个类别的样本。你数据集中的前80%大多数样本属于类别A,而剩下的20%大多数样本属于类别B。如果你仅仅采用一个简单的80/20保留划分,那么你的数据集将会有着极为不同的分布——评估结果可能会导致错误的结论。
这是我们想要避免的。因此,在本文中,我们将学习另一种可以应用的技术——K-fold交叉验证。通过在多个折叠中生成训练/测试划分,你可以进行多次训练和测试会话,使用不同的划分。你还将看到如何在PyTorch中使用K-fold交叉验证,PyTorch是目前神经网络领域的领先库之一。
什么是K-fold交叉验证?
假设你的目标是构建一个能够正确分类输入图像的分类器——就像下面的例子一样。你输入代表手写数字的图像,期望的输出是5。
在选择可以采用的模型类型方面,构建这样一个分类器有无数种方式。但哪一种最好呢?你必须评估每个模型以了解它的工作表现如何。
为什么使用训练/测试拆分来评估模型?
在机器学习模型训练完成后,就会进行模型评估。评估确保模型同样可以处理现实世界中的数据,它通过从一个称为测试集的数据集中提供样本来实现,该数据集包含模型以前未见过的样本。
通过将随后的预测结果与为这些样本也提供的真实标签进行比较,我们可以看到模型在该数据集上的表现如何。因此,如果我们在模型评估期间使用了真实世界中的数据,我们也可以看到它在真实世界数据上的表现如何。
然而,在评估我们的模型时,我们必须保持谨慎。我们不能简单地使用我们训练模型时的数据,以避免成为一个给自己打分的学生。
因为如果你使用训练数据进行评估就会发生这样的情况:由于模型已经学会捕捉与该特定数据集相关的模式,如果这些模式是虚假的并且在现实世界数据中不存在,那么模型可能在现实世界数据中表现不佳。特别是对于高变化模型,这可能成为一个问题。
相反,我们使用那个测试集来评估模型,该测试集已被选定,并且包含训练集中不存在的样本。但如何构建这个测试集则是另一个问题。有多种方法可以这样做。让我们先来看一个天真的策略。然后我们将理解为什么我们可能改用K-fold交叉验证。
简单的保留分割
这是一种简单的方法,也叫做简单的保留分割:
使用这种技术,你只需取出原始数据集的一部分,将其分离出来,然后认为这就是测试数据。传统上,这种分割通常按80/20的方式进行,即使用80%的数据来训练模型,而将20%的数据用于评估。
以下是这种做法被认为天真的几个原因:
这就是为什么更强有力地验证你的模型往往是一个更好的主意。我们来看看K-fold交叉验证是如何做到这一点的。
介绍K-fold交叉验证
如果我们能尝试这种训练/测试分割的多种变体呢?我们就会得到一个经过更加严格评估的模型。
而这正是K-fold交叉验证的意义所在。
在K-fold交叉验证中,你设置一个数值k为大于1的任意整数,并将生成k个分割。每个分割会有1/k的样本属于测试数据集,而剩余的数据可用于训练目的。
用PyTorch实现K-fold交叉验证
既然你已经理解了K-fold交叉验证是如何工作的,那么让我们来看看你如何用PyTorch来应用它。使用PyTorch进行K-fold CV涉及以下步骤:
运行代码所需
运行此示例要求你已安装以下依赖项:
让我们打开一个代码编辑器,并创建一个文件,例如,kfold.py。
模型导入
我们做的第一件事是指定模型导入。我们导入这些Python模块:
1. 对于文件输入/输出,我们使用os。
2. 所有PyTorch功能都作为torch导入。我们还有一些子导入:
3. 我们还导入了与计算机视觉相关的特定功能 - 使用torchvision。首先,我们从torchvision.datasets导入了MNIST数据集。我们也从Torch Vision导入transforms,这使我们能够稍后将数据转换为Tensor格式。
最后,我们从sklearn.model_selection导入KFold,以允许我们执行K-fold交叉验证。
import os
import torch
from torch import nn
from torchvision.datasets import MNIST
from torch.utils.data import DataLoader, ConcatDataset
from torchvision import transforms
from sklearn.model_selection import KFold
模型类
让我们定义一个简单的卷积神经网络,即 SimpleConvNet,它利用 nn.Module 基类 - 因此有效地实现了一个 PyTorch 神经网络。
我们可以通过指定__init__构造函数定义和前向传播来实现它。在__init__ 定义中,我们使用 Sequential 层堆栈来指定神经网络。你可以看到,我们使用了一个带有 ReLU 激活的卷积层(Conv2d)和一些负责生成预测的线性层(Linear layers)。由于 MNIST 数据集的简单性,这应该就足够了。我们将堆栈存储在 self.layers 中,我们在前向定义中使用它。在这里,我们只是简单地将数据 - 在 x 中可用 - 传递给层。
class SimpleConvNet(nn.Module):
'''
Simple Convolutional Neural Network
'''
def __init__(self):
super().__init__()
self.layers = nn.Sequential(
nn.Conv2d(1, 10, kernel_size=3),
nn.ReLU(),
nn.Flatten(),
nn.Linear(26 * 26 * 10, 50),
nn.ReLU(),
nn.Linear(50, 20),
nn.ReLU(),
nn.Linear(20, 10)
)
def forward(self, x):
'''Forward pass'''
return self.layers(x)
在此之前class,我们还将添加一个def名为reset_weights. 在折叠过程中,它将用于重置模型的参数。这样,我们确保模型使用(伪)随机初始化的权重进行训练,避免权重泄漏。
def reset_weights(m):
'''
Try resetting model weights to avoid
weight leakage.
'''
for layer in m.children():
if hasattr(layer, 'reset_parameters'):
print(f'Reset trainable parameters of layer = {layer}')
layer.reset_parameters()
运行代码
既然我们已经定义了模型类,现在是时候编写一些运行时代码了。我们的运行时代码涵盖以下几个方面:
准备步骤
下面,我们定义了一些在开始交叉折叠训练过程之前执行的准备步骤。你会看到我们在 main 名称下运行所有代码,这意味着这些代码只有在执行Python文件时才会运行。在这部分,我们做以下几件事:
if __name__ == '__main__':
# Configuration options
k_folds = 5
num_epochs = 1
loss_function = nn.CrossEntropyLoss()
# For fold results
results = {}
# Set fixed random number seed
torch.manual_seed(42)
加载 MNIST 数据集
接下来,我们将加载 MNIST 数据集。
我们简单地将MNIST 数据集的train=True和train=False部分合并在一起,该数据集已经被 PyTorch 的torchvision.
我们不希望这样——回想一下,K-fold交叉验证生成跨K-fold的训练/测试分割,其中 k-1 部分用于训练模型,1 部分用于模型评估。
为了解决这个问题,我们只需加载这两个部分,然后将它们连接到一个ConcatDataset对象中。不用担心数据的混洗——你将看到接下来会处理这个问题。
# Prepare MNIST dataset by concatenating Train/Test part; we split later.
dataset_train_part = MNIST(os.getcwd(), download=True, transform=transforms.ToTensor(), train=True)
dataset_test_part = MNIST(os.getcwd(), download=True, transform=transforms.ToTensor(), train=False)
dataset = ConcatDataset([dataset_train_part, dataset_test_part])
定义K-fold交叉验证器
因为接下来,我们在初始化K-fold交叉验证器时定义了洗牌。在这里,我们设置shuffle=True,意味着在数据被分成批次之前会发生洗牌。k_folds指的是折数,正如你所预期的那样。
# Define the K-fold Cross Validator
kfold = KFold(n_splits=k_folds, shuffle=True)
# Start print
print('--------------------------------')
生成拆分和训练模型的步骤
我们现在可以生成拆分并训练我们的模型。你可以通过定义一个循环来做到这一点,在循环中迭代拆分,指定特定拆分的折叠和训练及测试样本的标识符列表。这些可以用于执行实际的训练过程。
在 for 循环中,我们首先执行一条print语句,指示当前折叠。然后你执行训练过程。这涉及以下步骤:
# K-fold Cross Validation model evaluation
for fold, (train_ids, test_ids) in enumerate(kfold.split(dataset)):
print(f'FOLD {fold}')
print('--------------------------------')
# Sample elements randomly from a given list of ids, no replacement.
train_subsampler = torch.utils.data.SubsetRandomSampler(train_ids)
test_subsampler = torch.utils.data.SubsetRandomSampler(test_ids)
# Define data loaders for training and testing data in this fold
trainloader = torch.utils.data.DataLoader(
dataset,
batch_size=10, sampler=train_subsampler)
testloader = torch.utils.data.DataLoader(
dataset,
batch_size=10, sampler=test_subsampler)
# Init the neural network
network = SimpleConvNet()
# Initialize optimizer
optimizer = torch.optim.Adam(network.parameters(), lr=1e-4)
# Run the training loop for defined number of epochs
for epoch in range(0, num_epochs):
# Print epoch
print(f'Starting epoch {epoch+1}')
# Set current loss value
current_loss = 0.0
# Iterate over the DataLoader for training data
for i, data in enumerate(trainloader, 0):
# Get inputs
inputs, targets = data
# Zero the gradients
optimizer.zero_grad()
# Perform forward pass
outputs = network(inputs)
# Compute loss
loss = loss_function(outputs, targets)
# Perform backward pass
loss.backward()
# Perform optimization
optimizer.step()
# Print statistics
current_loss += loss.item()
if i % 500 == 499:
print('Loss after mini-batch %5d: %.3f' %
(i + 1, current_loss / 500))
current_loss = 0.0
# Process is complete.
print('Training process has finished. Saving trained model.')
折叠评估
在特定折叠中训练模型后,你也必须对其进行评估。接下来我们就要进行这项工作。首先,我们要保存模型——这样以后如果你想要重用它时,它就可以用于生成产出了。然后我们进行模型评估活动——迭代遍历测试加载器(testloader),对折叠分割中测试批次/测试部分的所有样本生成预测。评估后,我们计算准确率,将其显示在屏幕上,并将其添加到该特定折叠的结果字典中。
# Print about testing
print('Starting testing')
# Saving the model
save_path = f'./model-fold-{fold}.pth'
torch.save(network.state_dict(), save_path)
# Evaluation for this fold
correct, total = 0, 0
with torch.no_grad():
# Iterate over the test data and generate predictions
for i, data in enumerate(testloader, 0):
# Get inputs
inputs, targets = data
# Generate outputs
outputs = network(inputs)
# Set total and correct
_, predicted = torch.max(outputs.data, 1)
total += targets.size(0)
correct += (predicted == targets).sum().item()
# Print accuracy
print('Accuracy for fold %d: %d %%' % (fold, 100.0 * correct / total))
print('--------------------------------')
results[fold] = 100.0 * (correct / total)
模型评估
最终,当所有的折叠都完成后,我们得到了每一个折叠的结果。现在,是时候进行全面的模型评估了——我们能够更加稳健地这么做,因为我们拥有了来自所有折叠的信息。以下是如何展示每一个折叠的结果,并且在屏幕上打印出平均值。
它允许你做两件事:
# Print fold results
print(f'K-FOLD CROSS VALIDATION RESULTS FOR {k_folds} FOLDS')
print('--------------------------------')
sum = 0.0
for key, value in results.items():
print(f'Fold {key}: {value} %')
sum += value
print(f'Average: {sum/len(results.items())} %')
完整代码
运行该代码,在每个折叠运行一个时期的情况下,对于5折叠会给你以下结果。
--------------------------------
FOLD 0
--------------------------------
Starting epoch 1
Loss after mini-batch 500: 1.875
Loss after mini-batch 1000: 0.810
Loss after mini-batch 1500: 0.545
Loss after mini-batch 2000: 0.450
Loss after mini-batch 2500: 0.415
Loss after mini-batch 3000: 0.363
Loss after mini-batch 3500: 0.342
Loss after mini-batch 4000: 0.373
Loss after mini-batch 4500: 0.331
Loss after mini-batch 5000: 0.295
Loss after mini-batch 5500: 0.298
Training process has finished. Saving trained model.
Starting testing
Accuracy for fold 0: 91 %
--------------------------------
FOLD 1
--------------------------------
Starting epoch 1
Loss after mini-batch 500: 1.782
Loss after mini-batch 1000: 0.727
Loss after mini-batch 1500: 0.494
Loss after mini-batch 2000: 0.419
Loss after mini-batch 2500: 0.386
Loss after mini-batch 3000: 0.367
Loss after mini-batch 3500: 0.352
Loss after mini-batch 4000: 0.329
Loss after mini-batch 4500: 0.307
Loss after mini-batch 5000: 0.297
Loss after mini-batch 5500: 0.289
Training process has finished. Saving trained model.
Starting testing
Accuracy for fold 1: 91 %
--------------------------------
FOLD 2
--------------------------------
Starting epoch 1
Loss after mini-batch 500: 1.735
Loss after mini-batch 1000: 0.723
Loss after mini-batch 1500: 0.501
Loss after mini-batch 2000: 0.412
Loss after mini-batch 2500: 0.364
Loss after mini-batch 3000: 0.366
Loss after mini-batch 3500: 0.332
Loss after mini-batch 4000: 0.319
Loss after mini-batch 4500: 0.322
Loss after mini-batch 5000: 0.292
Loss after mini-batch 5500: 0.293
Training process has finished. Saving trained model.
Starting testing
Accuracy for fold 2: 91 %
--------------------------------
FOLD 3
--------------------------------
Starting epoch 1
Loss after mini-batch 500: 1.931
Loss after mini-batch 1000: 1.048
Loss after mini-batch 1500: 0.638
Loss after mini-batch 2000: 0.475
Loss after mini-batch 2500: 0.431
Loss after mini-batch 3000: 0.394
Loss after mini-batch 3500: 0.390
Loss after mini-batch 4000: 0.373
Loss after mini-batch 4500: 0.383
Loss after mini-batch 5000: 0.349
Loss after mini-batch 5500: 0.350
Training process has finished. Saving trained model.
Starting testing
Accuracy for fold 3: 90 %
--------------------------------
FOLD 4
--------------------------------
Starting epoch 1
Loss after mini-batch 500: 2.003
Loss after mini-batch 1000: 0.969
Loss after mini-batch 1500: 0.556
Loss after mini-batch 2000: 0.456
Loss after mini-batch 2500: 0.423
Loss after mini-batch 3000: 0.372
Loss after mini-batch 3500: 0.362
Loss after mini-batch 4000: 0.332
Loss after mini-batch 4500: 0.316
Loss after mini-batch 5000: 0.327
Loss after mini-batch 5500: 0.304
Training process has finished. Saving trained model.
Starting testing
Accuracy for fold 4: 90 %
--------------------------------
K-FOLD CROSS VALIDATION RESULTS FOR 5 FOLDS
--------------------------------
Fold 0: 91.87857142857143 %
Fold 1: 91.75 %
Fold 2: 91.85 %
Fold 3: 90.35714285714286 %
Fold 4: 90.82142857142857 %
Average: 91.33142857142857 %
总结
在本文中,我们研究了如何将K-fold交叉验证与PyTorch框架结合应用于深度学习。我们看到,K-fold交叉验证用你的数据集生成了k种不同的情况,称为折叠,其中数据在每个折叠中被分割成k-1个训练批次和1个测试批次。K-fold交叉验证可用于更彻底地评估你的PyTorch模型,让你对性能没有因数据集中的奇怪异常值而受到影响。
除了理论上的东西,我们还提供了一个PyTorch示例,展示了你如何结合Scikit-learn的KFold功能,在框架中应用K-fold交叉验证。