如何将自定义数据集分成训练集和测试集?

123
import pandas as pd
import numpy as np
import cv2
from torch.utils.data.dataset import Dataset

class CustomDatasetFromCSV(Dataset):
    def __init__(self, csv_path, transform=None):
        self.data = pd.read_csv(csv_path)
        self.labels = pd.get_dummies(self.data['emotion']).as_matrix()
        self.height = 48
        self.width = 48
        self.transform = transform

    def __getitem__(self, index):
        pixels = self.data['pixels'].tolist()
        faces = []
        for pixel_sequence in pixels:
            face = [int(pixel) for pixel in pixel_sequence.split(' ')]
            # print(np.asarray(face).shape)
            face = np.asarray(face).reshape(self.width, self.height)
            face = cv2.resize(face.astype('uint8'), (self.width, self.height))
            faces.append(face.astype('float32'))
        faces = np.asarray(faces)
        faces = np.expand_dims(faces, -1)
        return faces, self.labels

    def __len__(self):
        return len(self.data)

这是我通过使用其他存储库的参考所能完成的。然而,我想将此数据集拆分为训练集和测试集。

我应该如何在这个类中实现它?还是说我需要创建一个单独的类来完成这项工作?

8个回答

206

从PyTorch 0.4.1开始,您可以使用random_split函数:

train_size = int(0.8 * len(full_dataset))
test_size = len(full_dataset) - train_size
train_dataset, test_dataset = torch.utils.data.random_split(full_dataset, [train_size, test_size])

1
我按照你的答案操作,但在遍历分割的 train_loader 时出现了这个问题:https://dev59.com/3LDma4cB1Zd3GeqPALOS - joe
4
AttributeError: 'Subset' object has no attribute 'targets' how can I access targets of only one of the subsets? I want to print something like this for train and test data separately {0: 111, 1: 722, 2: 813, 3: 175, 4: 283, 5: 2846, 6: 290, 7: 106} - Amin Bashiri
1
对于其他人:如果你遇到了 TypeError 'DataLoader' object is not subscriptable 的错误,你可能也想看一下 https://dev59.com/3rnoa4cB1Zd3GeqPLSlT#60150673 - trujello
有没有办法包含空间重采样策略? - Sheykhmousa

158

使用Pytorch的SubsetRandomSampler

import torch
import numpy as np
from torchvision import datasets
from torchvision import transforms
from torch.utils.data.sampler import SubsetRandomSampler

class CustomDatasetFromCSV(Dataset):
    def __init__(self, csv_path, transform=None):
        self.data = pd.read_csv(csv_path)
        self.labels = pd.get_dummies(self.data['emotion']).as_matrix()
        self.height = 48
        self.width = 48
        self.transform = transform

    def __getitem__(self, index):
        # This method should return only 1 sample and label 
        # (according to "index"), not the whole dataset
        # So probably something like this for you:
        pixel_sequence = self.data['pixels'][index]
        face = [int(pixel) for pixel in pixel_sequence.split(' ')]
        face = np.asarray(face).reshape(self.width, self.height)
        face = cv2.resize(face.astype('uint8'), (self.width, self.height))
        label = self.labels[index]

        return face, label

    def __len__(self):
        return len(self.labels)


dataset = CustomDatasetFromCSV(my_path)
batch_size = 16
validation_split = .2
shuffle_dataset = True
random_seed= 42

# Creating data indices for training and validation splits:
dataset_size = len(dataset)
indices = list(range(dataset_size))
split = int(np.floor(validation_split * dataset_size))
if shuffle_dataset :
    np.random.seed(random_seed)
    np.random.shuffle(indices)
train_indices, val_indices = indices[split:], indices[:split]

# Creating PT data samplers and loaders:
train_sampler = SubsetRandomSampler(train_indices)
valid_sampler = SubsetRandomSampler(val_indices)

train_loader = torch.utils.data.DataLoader(dataset, batch_size=batch_size, 
                                           sampler=train_sampler)
validation_loader = torch.utils.data.DataLoader(dataset, batch_size=batch_size,
                                                sampler=valid_sampler)

# Usage Example:
num_epochs = 10
for epoch in range(num_epochs):
    # Train:   
    for batch_index, (faces, labels) in enumerate(train_loader):
        # ...

num_train是什么? - nirvair
1
我的错,已经恰当地更名为(dataset_size)。 - benjaminplanche
1
Dataset.__getitem__() 应该返回单个样本和标签,而不是整个数据集。我编辑了我的帖子,给你一个例子,它应该是这样的。 - benjaminplanche
1
@AnaClaudia: batch_size 定义了每个训练迭代向神经网络传递的堆叠在一起的样本数,形成一个 _mini-batch_。请参阅 Dataloader documentation 或此 Cross-Validated thread 以了解更多信息。 - benjaminplanche
@benjaminplanche 感谢您的回答!我的问题是,如何在像数据集这样分割CV数据集时考虑空间自相关性。因此,我指的是将训练、评估和测试进行空间上不相交的分割。 - Sheykhmousa
显示剩余6条评论

26

当前的答案会进行随机分割,这样做的缺点是不能保证每个类别的样本数量都是平衡的。当你想让每个类别只有少量的样本时,这是尤其有问题的。例如,MNIST有60000个样例,即每个数字有6000个样例。假设你只想在训练集中有每个数字30个样例,在这种情况下,随机分割可能会导致类别之间的不平衡(某些数字拥有比其他数字更多的训练数据)。因此,你需要确保每个数字只有30个标签。这被称为分层抽样

一种实现这个目标的方法是使用Pytorch中的采样器接口,示例代码在这里

另一种方法是通过编程进行简单实现。例如,以下是针对MNIST的简单实现,其中ds是MNIST数据集,k是每个类别所需的样本数。

def sampleFromClass(ds, k):
    class_counts = {}
    train_data = []
    train_label = []
    test_data = []
    test_label = []
    for data, label in ds:
        c = label.item()
        class_counts[c] = class_counts.get(c, 0) + 1
        if class_counts[c] <= k:
            train_data.append(data)
            train_label.append(torch.unsqueeze(label, 0))
        else:
            test_data.append(data)
            test_label.append(torch.unsqueeze(label, 0))
    train_data = torch.cat(train_data)
    for ll in train_label:
        print(ll)
    train_label = torch.cat(train_label)
    test_data = torch.cat(test_data)
    test_label = torch.cat(test_label)

    return (TensorDataset(train_data, train_label), 
        TensorDataset(test_data, test_label))
你可以像这样使用这个函数:

def main():
    train_ds = datasets.MNIST('../data', train=True, download=True,
                       transform=transforms.Compose([
                           transforms.ToTensor()
                       ]))
    train_ds, test_ds = sampleFromClass(train_ds, 3)

24
如果您想确保分割后的数据类别平衡,您可以使用 sklearn 中的 train_test_split 方法。假设您已经将数据封装在一个 自定义数据集对象 中:
from torch.utils.data import DataLoader, Subset
from sklearn.model_selection import train_test_split

TEST_SIZE = 0.1
BATCH_SIZE = 64
SEED = 42

# generate indices: instead of the actual data we pass in integers instead
train_indices, test_indices, _, _ = train_test_split(
    range(len(data)),
    data.targets,
    stratify=data.targets,
    test_size=TEST_SIZE,
    random_state=SEED
)

# generate subset based on indices
train_split = Subset(data, train_indices)
test_split = Subset(data, test_indices)

# create batches
train_batches = DataLoader(train_split, batch_size=BATCH_SIZE, shuffle=True)
test_batches = DataLoader(test_split, batch_size=BATCH_SIZE)

7
这是PyTorch中的Subset类,其中包含random_split方法。请注意,该方法是SubsetRandomSampler的基础。

enter image description here

如果我们使用random_split对MNIST进行划分:

loader = DataLoader(
  torchvision.datasets.MNIST('/data/mnist', train=True, download=True,
                             transform=torchvision.transforms.Compose([
                               torchvision.transforms.ToTensor(),
                               torchvision.transforms.Normalize(
                                 (0.5,), (0.5,))
                             ])),
  batch_size=16, shuffle=False)

print(loader.dataset.data.shape)
test_ds, valid_ds = torch.utils.data.random_split(loader.dataset, (50000, 10000))
print(test_ds, valid_ds)
print(test_ds.indices, valid_ds.indices)
print(test_ds.indices.shape, valid_ds.indices.shape)

我们得到:
torch.Size([60000, 28, 28])
<torch.utils.data.dataset.Subset object at 0x0000020FD1880B00> <torch.utils.data.dataset.Subset object at 0x0000020FD1880C50>
tensor([ 1520,  4155, 45472,  ..., 37969, 45782, 34080]) tensor([ 9133, 51600, 22067,  ...,  3950, 37306, 31400])
torch.Size([50000]) torch.Size([10000])

我们的 test_ds.indicesvalid_ds.indices 将从范围 (0, 600000) 中随机选择。但是,如果我想要从(0, 49999)(50000, 59999)中获取索引序列,很遗憾目前无法做到,除非使用this方法。

如果您运行 MNIST基准测试,这将非常方便,因为预定义了哪些数据应该是测试集和验证集。


显然是最简单的方法 - Valentin
3
为什么代码是截图形式?请避免使用这种方式。 - rbaleksandar

3

补充Fábio Perez的回答,您可以为随机拆分提供分数。请注意,您首先要拆分数据集,而不是数据加载器。

train_dataset, val_dataset, test_dataset = torch.utils.data.random_split(full_dataset, [0.8, 0.1, 0.1])

1
如果你想在训练数据集中每个类别最多使用X个样本,你可以使用以下代码:
def stratify_split(dataset: Dataset, train_samples_per_class: int):
        import collections
        train_indices = []
        val_indices = []
        TRAIN_SAMPLES_PER_CLASS = 10
        target_counter = collections.Counter()
        for idx, data in enumerate(dataset):
            target = data['target']
            target_counter[target] += 1
            if target_counter[target] <= train_samples_per_class:
                train_indices.append(idx)
            else:
                val_indices.append(idx)
        train_dataset = Subset(dataset, train_indices)
        val_dataset = Subset(dataset, val_indices)
        return train_dataset, val_dataset

0
请注意,大多数规范示例已经被分割。例如,在this page上,您将找到MNIST。一个普遍的误解是它有60,000张图片。砰!错了!其中有70,000张图片,其中60,000张是训练图像,10,000张是验证(测试)图像。
因此,对于规范数据集,PyTorch的风格是为您提供已经分割好的数据集。
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader, Dataset, TensorDataset
from torch.optim import *
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import os
import numpy as np
import random

bs=512

t = transforms.Compose([
                       transforms.ToTensor(),
                       transforms.Normalize(mean=(0), std=(1))]
                       )

dl_train = DataLoader( torchvision.datasets.MNIST('/data/mnist', download=True, train=True, transform=t), 
                batch_size=bs, drop_last=True, shuffle=True)
dl_valid = DataLoader( torchvision.datasets.MNIST('/data/mnist', download=True, train=False, transform=t), 
                batch_size=bs, drop_last=True, shuffle=True)

在我看来,流程应该是先加载数据,然后拆分再进行转换 - 特别是在您的情况下,您已经将输入硬编码到了Normalize中。通常,这些应该仅从训练数据集中确定,但是使用pytorch时,变换似乎总是应用于整个数据集。 - David Waterworth
从你拥有的数据中,理想情况下应该创建训练、验证和测试数据集(TRAVALTES)。训练用于训练,验证用于检查是否过度拟合/欠拟合。您可以计算准确率分数或其他分数(f1...)以获取一些线索,并在出现分类问题时理想地创建混淆矩阵。所以我的这篇文章很糟糕。我今天稍后会改进它。 - prosti
1
是的,我的评论更多地涉及到大多数标准pytorch示例似乎将特征的均值/标准差硬编码为Transform的输入,通常使用预分割测试/验证数据。这似乎有点循环,因为实际上你想要从训练集中拆分数据并计算转换器参数,然后应用于验证(和/或测试)。但是,DataSet / Transformer的设计不像sklearn那么简单。有时我会想 scaling 是否应该由nn层执行,因此成为可学习的参数 - 但我猜这可能会影响收敛。 - David Waterworth
我更新了文章。如果你从头开始训练,大多数情况下将均值设置为0,标准差设置为1。对于预训练模型,只需遵循提供的归一化参数即可(transforms.Normalize)。你可以使用相同的归一化转换来处理训练集和测试集。@DavidWaterworth。是的,我知道有些实践者在模型的最开始使用BN层进行归一化。 - prosti
在上面的示例中,使用均值=(0),标准差=(1)时,我在简单手工ResNet上获得了99.3%的验证准确率。当我们经常看到这个例子时,使用均值=(0.5),标准差=(0.5)也是一样的。 - prosti

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接