使用PyTorch进行K-fold交叉验证

2023年12月06日 由 alex 发表 828 0

机器学习模型在经过训练后必须使用测试集进行评估。我们这样做是为了确保模型没有过拟合,同时确保它们能够处理现实生活中的数据集,这些数据集可能与训练集相比有些微的分布偏差。


但是,为了使你的模型真正健壮,仅仅使用训练/测试划分来评估可能还不够。


例如,假设你有一个数据集包含来自两个类别的样本。你数据集中的前80%大多数样本属于类别A,而剩下的20%大多数样本属于类别B。如果你仅仅采用一个简单的80/20保留划分,那么你的数据集将会有着极为不同的分布——评估结果可能会导致错误的结论。


这是我们想要避免的。因此,在本文中,我们将学习另一种可以应用的技术——K-fold交叉验证。通过在多个折叠中生成训练/测试划分,你可以进行多次训练和测试会话,使用不同的划分。你还将看到如何在PyTorch中使用K-fold交叉验证,PyTorch是目前神经网络领域的领先库之一。


什么是K-fold交叉验证?


假设你的目标是构建一个能够正确分类输入图像的分类器——就像下面的例子一样。你输入代表手写数字的图像,期望的输出是5。


在选择可以采用的模型类型方面,构建这样一个分类器有无数种方式。但哪一种最好呢?你必须评估每个模型以了解它的工作表现如何。


6


为什么使用训练/测试拆分来评估模型?


在机器学习模型训练完成后,就会进行模型评估。评估确保模型同样可以处理现实世界中的数据,它通过从一个称为测试集的数据集中提供样本来实现,该数据集包含模型以前未见过的样本。


通过将随后的预测结果与为这些样本也提供的真实标签进行比较,我们可以看到模型在该数据集上的表现如何。因此,如果我们在模型评估期间使用了真实世界中的数据,我们也可以看到它在真实世界数据上的表现如何。


然而,在评估我们的模型时,我们必须保持谨慎。我们不能简单地使用我们训练模型时的数据,以避免成为一个给自己打分的学生。


因为如果你使用训练数据进行评估就会发生这样的情况:由于模型已经学会捕捉与该特定数据集相关的模式,如果这些模式是虚假的并且在现实世界数据中不存在,那么模型可能在现实世界数据中表现不佳。特别是对于高变化模型,这可能成为一个问题。


相反,我们使用那个测试集来评估模型,该测试集已被选定,并且包含训练集中不存在的样本。但如何构建这个测试集则是另一个问题。有多种方法可以这样做。让我们先来看一个天真的策略。然后我们将理解为什么我们可能改用K-fold交叉验证。


简单的保留分割


这是一种简单的方法,也叫做简单的保留分割:


7


使用这种技术,你只需取出原始数据集的一部分,将其分离出来,然后认为这就是测试数据。传统上,这种分割通常按80/20的方式进行,即使用80%的数据来训练模型,而将20%的数据用于评估。


以下是这种做法被认为天真的几个原因:


  1. 数据代表性:所有数据集本质上都是样本,必须尽可能代表总体中的模式。当你从一个样本中生成样本(即从完整的数据集中)时,这一点变得尤为重要。例如,如果你数据集的前半部分是冰淇淋的图片,而后半部分只代表浓咖啡,那么如果你按照上面显示的方式生成分割,就一定会出现问题。随机洗牌可能有助于你解决这些问题。
  2. 时间的箭头:如果你拥有一个时间序列数据集,你的数据集很可能是按时间顺序排列的。如果你随机洗牌,然后进行简单的保留验证,那么你实际上会“[根据过去预测未来]”(Chollet, 2017)。这种时间泄漏并不利于模型性能。
  3. 数据冗余:如果一些样本出现不止一次,一个简单的随机洗牌后的保留分割可能会在训练和测试数据集之间引入冗余。也就是说,相同的样本属于这两个数据集。这也是有问题的,因为用于训练的数据隐式地泄露到了测试数据集中。


这就是为什么更强有力地验证你的模型往往是一个更好的主意。我们来看看K-fold交叉验证是如何做到这一点的。


介绍K-fold交叉验证


如果我们能尝试这种训练/测试分割的多种变体呢?我们就会得到一个经过更加严格评估的模型。


而这正是K-fold交叉验证的意义所在。


8


在K-fold交叉验证中,你设置一个数值k为大于1的任意整数,并将生成k个分割。每个分割会有1/k的样本属于测试数据集,而剩余的数据可用于训练目的。


用PyTorch实现K-fold交叉验证


既然你已经理解了K-fold交叉验证是如何工作的,那么让我们来看看你如何用PyTorch来应用它。使用PyTorch进行K-fold CV涉及以下步骤:


  1. 确保你的依赖项是最新的。
  2. 声明模型导入。
  3. 定义你的神经网络的nn.Module类,以及一个权重重置函数。
  4. 在你的运行时代码中添加准备步骤。
  5. 加载你的数据集。
  6. 定义K折交叉验证器,并生成分割。
  7. 迭代每个分割,训练并评估另一个模型实例。
  8. 平均所有分割以得到最终性能。


运行代码所需


运行此示例要求你已安装以下依赖项:


  • Python,来运行一切。确保安装了3.12+,尽管它也可以在略微旧的版本中运行。
  • PyTorch,这是你用来训练模型的深度学习库。
  • Scikit-learn,用于生成分割。


让我们打开一个代码编辑器,并创建一个文件,例如,kfold.py。


模型导入


我们做的第一件事是指定模型导入。我们导入这些Python模块:


1. 对于文件输入/输出,我们使用os。


2. 所有PyTorch功能都作为torch导入。我们还有一些子导入:


  • 神经网络功能作为nn导入。
  • 我们从torch.utils.data导入的DataLoader用于将数据传递给神经网络。
  • ConcatDataset将用于连接MNIST数据集的训练和测试部分,我们将用它来训练模型。K-fold CV意味着你自己生成分割,所以你不希望PyTorch为你做这个 - 因为你会实际上丢失数据。


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文件时才会运行。在这部分,我们做以下几件事:


  1. 我们设置了配置选项。我们将生成5个折叠(通过设置(k = 5),我们训练1个epoch(通常,这个值要高得多,但在这里我们只是想说明K-fold交叉验证的工作原理),并且我们设定了nn.CrossEntropyLoss作为我们的损失函数。
  2. 我们定义了一个字典,用以储存每个折叠的结果。
  3. 我们设置了一个固定的随机数种子,意味着我们所有的伪随机数初始化器将使用相同的初始化令牌进行初始化。


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=Truetrain=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语句,指示当前折叠。然后你执行训练过程。这涉及以下步骤:


  • train_idstest_ids中采样实际元素SubsetRandomSampler。采样器可用于DataLoader仅使用特定样本;在这种情况下基于标识符,因为SubsetRandomSampler从列表中随机采样元素,没有替换。换句话说,你创建两个子采样器,它们遵循循环中指定的分割for


  • 使用数据加载器,你实际上将从完整的dataset. 你可以使用适合内存的任何批处理大小,但批处理大小 10 几乎适用于所有情况。


  • 为该特定折叠准备好数据集后,你可以通过初始化类来初始化神经网络 - 使用SimpleConvNet().


  • 然后,当神经网络初始化时,你可以初始化此特定训练会话的优化器 - 在本例中,我们使用 Adam,并具有1e-4学习率。


  • 在 PyTorch 中,你必须定义自己的训练循环。它相对简单:迭代 epoch 的数量;在一个时期内,在小批量上;每个小批量,你执行前向传递、后向传递和后续优化。这就是这里正在发生的事情。


 # K-fold Cross Validation model evaluation
  for fold, (train_ids, test_ids) in enumerate(kfold.split(dataset)):
    
    # Print
    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)


模型评估


最终,当所有的折叠都完成后,我们得到了每一个折叠的结果。现在,是时候进行全面的模型评估了——我们能够更加稳健地这么做,因为我们拥有了来自所有折叠的信息。以下是如何展示每一个折叠的结果,并且在屏幕上打印出平均值。


它允许你做两件事:


  1. 查看你的模型是否在所有折叠中都表现良好;如果每一个折叠的准确性并没有太大的偏差,这就是真的。
  2. 如果它们确实有所偏差,你就会知道在哪一个折叠中出现问题,并且可以更仔细地查看数据,看看那里发生了什么。


# 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交叉验证。

文章来源:https://medium.com/ai-mind-labs/k-fold-cross-validation-with-pytorch-d24b62d95efc
欢迎关注ATYUN官方公众号
商务合作及内容投稿请联系邮箱:bd@atyun.com
评论 登录
热门职位
Maluuba
20000~40000/月
Cisco
25000~30000/月 深圳市
PilotAILabs
30000~60000/年 深圳市
写评论取消
回复取消