PyTorch 中的数据增强

Data Augmentation in PyTorch

我对 PyTorch 中执行的数据扩充有点困惑。现在,据我所知,当我们执行数据扩充时,我们会保留原始数据集,然后添加它的其他版本(翻转、裁剪等)。但这似乎不会发生在 PyTorch 中。据我从参考资料中了解到,当我们在 PyTorch 中使用 data.transforms 时,它会一一应用它们。例如:

data_transforms = {
    'train': transforms.Compose([
        transforms.RandomResizedCrop(224),
        transforms.RandomHorizontalFlip(),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
    'val': transforms.Compose([
        transforms.Resize(256),
        transforms.CenterCrop(224),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
}

在这里,为了进行训练,我们首先随机裁剪图像并将其调整为 (224,224) 的形状。然后我们拍摄这些 (224,224) 图像并水平翻转它们。因此,我们的数据集现在只包含水平翻转的图像,所以在这种情况下我们的原始图像丢失了。

我说得对吗?这种理解是否正确?如果不是,那么我们在上面的这段代码(取自官方文档)中告诉 PyTorch 保留原始图像并将其调整为预期形状的位置 (224,224)?

谢谢

transforms 操作会在每个批次生成时应用于您的原始图像。所以你的数据集保持不变,每次迭代只复制和转换批处理图像。

混淆可能来自这样一个事实,就像在您的示例中一样,transforms 通常用于数据准备(resizing/cropping 到预期尺寸、规范化值等)和数据增强(随机化 resizing/cropping、随机翻转图像等)。


你的data_transforms['train']所做的是:

  • 随机调整提供的图像大小并随机裁剪它以获得(224, 224)补丁
  • 对该补丁应用或不应用随机水平翻转,有 50/50 的机会
  • 将其转换为 Tensor
  • 根据您提供的均值和偏差值对结果进行标准化 Tensor

你的data_transforms['val']所做的是:

  • 将图片大小调整为 (256, 256)
  • 中心裁剪调整大小的图像以获得 (224, 224) 补丁
  • 将其转换为 Tensor
  • 根据您提供的均值和偏差值对结果进行标准化 Tensor

(即训练数据的随机 resizing/cropping 被验证数据的固定操作所取代,以获得可靠的验证结果)


如果您不希望训练图像以 50/50 的几率水平翻转,只需删除 transforms.RandomHorizontalFlip() 行。

同样,如果您希望图像始终居中裁剪,请将 transforms.RandomResizedCrop 替换为 transforms.Resizetransforms.CenterCrop,就像 data_transforms['val'] 所做的那样。

我假设您是在问这些数据增强变换(例如 RandomHorizo​​ntalFlip)是否实际上 也增加了数据集的大小 ,或者 它们是否应用于每个数据集数据集中的一项一项,而不是增加数据集的大小

运行 下面的简单代码片段我们可以观察到 后者是正确的,即如果你有一个包含 8 个图像的数据集,并创建一个 PyTorch 数据集对象对于此数据集,当您遍历数据集时,将在每个数据点上调用转换,并返回转换后的数据点。因此,例如,如果您有随机翻转,一些数据点将作为原始数据点返回,一些将作为翻转返回(例如 4 个翻转和 4 个原始数据点)。 换句话说,通过对数据集项的一次迭代,您将获得 8 个数据点(有些翻转,有些没有翻转)。 [这与扩充数据集的传统理解不一致(例如,在这种情况下,扩充数据集中有 16 个数据点)]

class experimental_dataset(Dataset):

    def __init__(self, data, transform):
        self.data = data
        self.transform = transform

    def __len__(self):
        return len(self.data.shape[0])

    def __getitem__(self, idx):
        item = self.data[idx]
        item = self.transform(item)
        return item

    transform = transforms.Compose([
        transforms.ToPILImage(),
        transforms.RandomHorizontalFlip(),
        transforms.ToTensor()
    ])

x = torch.rand(8, 1, 2, 2)
print(x)

dataset = experimental_dataset(x,transform)

for item in dataset:
    print(item)

结果:(浮点数的细微差别是转pil图来回造成的)

原始虚拟数据集:

tensor([[[[0.1872, 0.5518],
          [0.5733, 0.6593]]],


    [[[0.6570, 0.6487],
      [0.4415, 0.5883]]],


    [[[0.5682, 0.3294],
      [0.9346, 0.1243]]],


    [[[0.1829, 0.5607],
      [0.3661, 0.6277]]],


    [[[0.1201, 0.1574],
      [0.4224, 0.6146]]],


    [[[0.9301, 0.3369],
      [0.9210, 0.9616]]],


    [[[0.8567, 0.2297],
      [0.1789, 0.8954]]],


    [[[0.0068, 0.8932],
      [0.9971, 0.3548]]]])

转换后的数据集:

tensor([[[0.1843, 0.5490],
     [0.5725, 0.6588]]])
tensor([[[0.6549, 0.6471],
     [0.4392, 0.5882]]])
tensor([[[0.5647, 0.3255],
         [0.9333, 0.1216]]])
tensor([[[0.5569, 0.1804],
         [0.6275, 0.3647]]])
tensor([[[0.1569, 0.1176],
         [0.6118, 0.4196]]])
tensor([[[0.9294, 0.3333],
         [0.9176, 0.9608]]])
tensor([[[0.8549, 0.2275],
         [0.1765, 0.8941]]])
tensor([[[0.8902, 0.0039],
         [0.3529, 0.9961]]])

是的,数据集大小在转换后没有改变。每个图像都传递给转换并返回,因此大小保持不变。

如果您希望将原始数据集与转换后的数据集结合使用。

例如increased_dataset = torch.utils.data.ConcatDataset([transformed_dataset,original])

TLDR :

  • 变换操作以一定的概率对循环中的输入批次应用一堆变换。因此,模型现在在多个 epoch 的过程中接触到更多示例。

  • 就个人而言,当我在自己的数据集上训练音频分类模型时,在增强之前,我的模型似乎总是以 72% 的准确率收敛。我使用了扩充和增加的训练时期数,这将测试集中的验证准确率提高到了 89%。

在 PyTorch 中,有一些裁剪类型会改变数据集的大小。这些是 FiveCrop and TenCrop:

CLASS torchvision.transforms.FiveCrop(size)

Crop the given image into four corners and the central crop.

This transform returns a tuple of images and there may be a mismatch in the number of inputs and targets your Dataset returns. See below for an example of how to deal with this.

Example:

>>> transform = Compose([
>>>    TenCrop(size), # this is a list of PIL Images
>>>    Lambda(lambda crops: torch.stack([ToTensor()(crop) for crop in crops])) # returns a 4D tensor
>>> ])
>>> #In your test loop you can do the following:
>>> input, target = batch # input is a 5d tensor, target is 2d
>>> bs, ncrops, c, h, w = input.size()
>>> result = model(input.view(-1, c, h, w)) # fuse batch size and ncrops
>>> result_avg = result.view(bs, ncrops, -1).mean(1) # avg over crops

TenCrop同上加上五个patch的翻转版本(默认使用水平翻转)。

数据增强的目的是增加训练数据集的多样性。

虽然data.transforms并没有改变数据集的大小,但是,我们每次调用数据集的epoch,都会执行transforms操作,然后得到不同的数据。

我稍微更改了@Ashkan372 代码以输出多个时期的数据:

import torch
from torchvision import transforms
from torch.utils.data import TensorDataset as Dataset
from torch.utils.data import DataLoader

class experimental_dataset(Dataset):
  def __init__(self, data, transform):
    self.data = data
    self.transform = transform

  def __len__(self):
    return self.data.shape[0]

  def __getitem__(self, idx):
    item = self.data[idx]
    item = self.transform(item)
    return item

transform = transforms.Compose([
  transforms.ToPILImage(),
  transforms.RandomHorizontalFlip(),
  transforms.ToTensor()
])

x = torch.rand(8, 1, 2, 2)
print('the original data: \n', x)

epoch_size = 3
batch_size = 4

dataset = experimental_dataset(x,transform)
for i in range(epoch_size):
  print('----------------------------------------------')
  print('the epoch', i, 'data: \n')
  for item in DataLoader(dataset, batch_size, shuffle=False):
    print(item)

输出为:

the original data: 
 tensor([[[[0.5993, 0.5898],
          [0.7365, 0.5472]]],


        [[[0.1878, 0.3546],
          [0.2124, 0.8324]]],


        [[[0.9321, 0.0795],
          [0.4090, 0.9513]]],


        [[[0.2825, 0.6954],
          [0.3737, 0.0869]]],


        [[[0.2123, 0.7024],
          [0.6270, 0.5923]]],


        [[[0.9997, 0.9825],
          [0.0267, 0.2910]]],


        [[[0.2323, 0.1768],
          [0.4646, 0.4487]]],


        [[[0.2368, 0.0262],
          [0.2423, 0.9593]]]])
----------------------------------------------
the epoch 0 data: 

tensor([[[[0.5882, 0.5961],
          [0.5451, 0.7333]]],


        [[[0.3529, 0.1843],
          [0.8314, 0.2118]]],


        [[[0.9294, 0.0784],
          [0.4078, 0.9490]]],


        [[[0.6941, 0.2824],
          [0.0863, 0.3725]]]])
tensor([[[[0.7020, 0.2118],
          [0.5922, 0.6235]]],


        [[[0.9804, 0.9961],
          [0.2902, 0.0235]]],


        [[[0.2314, 0.1765],
          [0.4627, 0.4471]]],


        [[[0.0235, 0.2353],
          [0.9569, 0.2392]]]])
----------------------------------------------
the epoch 1 data: 

tensor([[[[0.5882, 0.5961],
          [0.5451, 0.7333]]],


        [[[0.1843, 0.3529],
          [0.2118, 0.8314]]],


        [[[0.0784, 0.9294],
          [0.9490, 0.4078]]],


        [[[0.2824, 0.6941],
          [0.3725, 0.0863]]]])
tensor([[[[0.2118, 0.7020],
          [0.6235, 0.5922]]],


        [[[0.9804, 0.9961],
          [0.2902, 0.0235]]],


        [[[0.2314, 0.1765],
          [0.4627, 0.4471]]],


        [[[0.0235, 0.2353],
          [0.9569, 0.2392]]]])
----------------------------------------------
the epoch 2 data: 

tensor([[[[0.5882, 0.5961],
          [0.5451, 0.7333]]],


        [[[0.3529, 0.1843],
          [0.8314, 0.2118]]],


        [[[0.0784, 0.9294],
          [0.9490, 0.4078]]],


        [[[0.6941, 0.2824],
          [0.0863, 0.3725]]]])
tensor([[[[0.2118, 0.7020],
          [0.6235, 0.5922]]],


        [[[0.9961, 0.9804],
          [0.0235, 0.2902]]],


        [[[0.2314, 0.1765],
          [0.4627, 0.4471]]],


        [[[0.0235, 0.2353],
          [0.9569, 0.2392]]]])

不同的epoch我们得到不同的输出!