数据加载与处理

在网络训练与测试中,数据的加载和预处理往往会耗费大量的精力。MegEngine 提供了一系列接口来规范化这些处理工作。

利用 Dataset 封装一个数据集

数据集是一组数据的集合,例如 MNIST、Cifar10等图像数据集。Dataset 是 MegEngine 中表示数据集的抽象类。我们自定义的数据集类应该继承 Dataset 并重写下列方法:

  • init() :一般在其中实现读取数据源文件的功能。也可以添加任何其它的必要功能;

  • getitem() :通过索引操作来获取数据集中某一个样本,使得可以通过 for 循环来遍历整个数据集;

  • len() :返回数据集大小;

下面是一个简单示例。我们根据下图所示的二分类数据,创建一个 Dataset 。每个数据是一个二维平面上的点,横坐标和纵坐标在 [-1, 1] 之间。共有两个类别标签(图1中的蓝色 * 和红色 +),标签为0的点处于一、三象限;标签为1的点处于二、四象限。

../_images/dataset.png 图1

该数据集的创建过程如下:

  • init() 中利用 NumPy 随机生成 ndarray 作为数据;

  • getitem() 中返回 ndarray 中的一个样本;

  • len() 中返回整个数据集中样本的个数;

  1. import numpy as np
  2. from typing import Tuple
  3.  
  4. # 导入需要被继承的 Dataset 类
  5. from megengine.data.dataset import Dataset
  6.  
  7. class XORDataset(Dataset):
  8. def __init__(self, num_points):
  9. """
  10. 生成如图1所示的二分类数据集,数据集长度为 num_points
  11. """
  12. super().__init__()
  13.  
  14. # 初始化一个维度为 (50000, 2) 的 NumPy 数组。
  15. # 数组的每一行是一个横坐标和纵坐标都落在 [-1, 1] 区间的一个数据点 (x, y)
  16. self.data = np.random.rand(num_points, 2).astype(np.float32) * 2 - 1
  17. # 为上述 NumPy 数组构建标签。每一行的 (x, y) 如果符合 x*y < 0,则对应标签为1,反之,标签为0
  18. self.label = np.zeros(num_points, dtype=np.int32)
  19. for i in range(num_points):
  20. self.label[i] = 1 if np.prod(self.data[i]) < 0 else 0
  21.  
  22. # 定义获取数据集中每个样本的方法
  23. def __getitem__(self, index: int) -> Tuple:
  24. return self.data[index], self.label[index]
  25.  
  26. # 定义返回数据集长度的方法
  27. def __len__(self) -> int:
  28. return len(self.data)
  29.  
  30. np.random.seed(2020)
  31. # 构建一个包含 30000 个点的训练数据集
  32. xor_train_dataset = XORDataset(30000)
  33. print("The length of train dataset is: {}".format(len(xor_train_dataset)))
  34.  
  35. # 通过 for 遍历数据集中的每一个样本
  36. for cor, tag in xor_train_dataset:
  37. print("The first data point is: {}, {}".format(cor, tag))
  38. break;
  39.  
  40. print("The second data point is: {}".format(xor_train_dataset[1]))

输出:

  1. The length of train dataset is: 30000
  2. The first data point is: [0.97255366 0.74678389], 0
  3. The second data point is: (array([ 0.01949105, -0.45632857]), 1)

MegEngine 中也提供了一些已经继承自 Dataset 的数据集类,方便我们使用,比如 ArrayDatasetArrayDataset 允许通过传入单个或多个 NumPy 数组,对它进行初始化。其内部实现如下:

  • init() :检查传入的多个 NumPy 数组的长度是否一致;不一致则无法成功创建;

  • getitem() :将多个 NumPy 数组相同索引位置的元素构成一个 tuple 并返回;

  • len() :返回数据集的大小;

以图1所示的数据集为例,我们可以通过坐标数据和标签数据的数组直接构造 ArrayDataset ,无需用户自己定义数据集类。

  1. from megengine.data.dataset import ArrayDataset
  2.  
  3. # 准备 NumPy 形式的 data 和 label 数据
  4. np.random.seed(2020)
  5. num_points = 30000
  6. data = np.random.rand(num_points, 2).astype(np.float32) * 2 - 1
  7. label = np.zeros(num_points, dtype=np.int32)
  8. for i in range(num_points):
  9. label[i] = 1 if np.prod(data[i]) < 0 else 0
  10.  
  11. # 利用 ArrayDataset 创建一个数据集类
  12. xor_dataset = ArrayDataset(data, label)

通过 Sampler 从 Dataset 中采样

Dataset 仅能通过一个固定的顺序(其 getitem 实现)访问所有样本,而 Sampler 使得我们可以以所期望的方式从 Dataset 中采样,生成训练和测试的批(minibatch)数据。Sampler 本质上是一个数据集中数据索引的迭代器,它接收 Dataset 的实例 和批大小(batch_size)来进行初始化。

MegEngine 中提供各种常见的采样器,如 RandomSampler (通常用于训练)、 SequentialSampler (通常用于测试) 等。下面我们以它们为例,来熟悉 Sampler 的基本用法:

  1. # 导入 MegEngine 中采样器
  2. from megengine.data import RandomSampler
  3.  
  4. # 创建一个随机采样器
  5. random_sampler = RandomSampler(dataset=xor_dataset, batch_size=4)
  6.  
  7. # 获取迭代sampler时每次返回的数据集索引
  8. for indices in random_sampler:
  9. print(indices)
  10. break;

输出:

  1. [19827, 2614, 8788, 8641]

可以看到,在 batch_size 为4时,每次迭代 sampler 返回的是长度为4的列表,列表中的每个元素是随机采样出的数据索引。

如果你创建的是一个序列化采样器 SequentialSampler ,那么每次返回的就是顺序索引。

  1. from megengine.data import SequentialSampler
  2.  
  3. sequential_sampler = SequentialSampler(dataset=xor_dataset, batch_size=4)
  4.  
  5. # 获取迭代sampler时返回的数据集索引信息
  6. for indices in sequential_sampler:
  7. print(indices)
  8. break;

输出:

  1. [0, 1, 2, 3]

用户也可以继承 Sampler 自定义采样器,这里不做详述。

用 DataLoader 生成批数据

MegEngine 中,DataLoader 本质上是一个迭代器,它通过 DatasetSampler 生成 minibatch 数据。

下列代码通过 for 循环获取每个 minibatch 的数据。

  1. from megengine.data import DataLoader
  2.  
  3. # 创建一个 DataLoader,并指定数据集和顺序采样器
  4. xor_dataloader = DataLoader(
  5. dataset=xor_dataset,
  6. sampler=sequential_sampler,
  7. )
  8. print("The length of the xor_dataloader is: {}".format(len(xor_dataloader)))
  9. # 从 DataLoader 中迭代地获取每批数据
  10. for idx, (cor, tag) in enumerate(xor_dataloader):
  11. print("iter %d : " % (idx), cor, tag)
  12. break;

输出:

  1. The length of the xor_dataloader is: 7500
  2. iter 0 : [[ 0.97255366 0.74678389]
  3. [ 0.01949105 -0.45632857]
  4. [-0.32616254 -0.56609147]
  5. [-0.44704571 -0.31336881]] [0 1 0 0]

DataLoader 中的数据变换(Transform)

在深度学习模型的训练中,我们经常需要对数据进行各种转换,比如,归一化、各种形式的数据增广等。Transform 是数据变换的基类,其各种派生类提供了常见的数据转换功能。DataLoader 构造函数可以接收一个 Transform 参数,在构建 minibatch 时,对该批数据进行相应的转换操作。

接下来通过 MNIST 数据集(MegEngine 提供了 MNIST Dataset)来熟悉 Transform 的使用。首先我们构建一个不做 Transform 的 MNIST DataLoader,并可视化第一个 minibatch 数据。

  1. # 从 MegEngine 中导入 MNIST 数据集
  2. from megengine.data.dataset import MNIST
  3.  
  4. # 若你是一次下载 MNIST 数据集,download 需设置成 True
  5. # 若你已经下载 MNIST 数据集,通过 root 指定 MNIST数据集 raw 路径
  6. # 通过 设置 train=True/False 获取训练集或测试集
  7. mnist_train_dataset = MNIST(root="./dataset/MNIST", train=True, download=True)
  8. # mnist_test_dataset = MNIST(root="./dataset/MNIST", train=False, download=True)
  9. sequential_sampler = SequentialSampler(dataset=mnist_train_dataset, batch_size=4)
  10.  
  11. mnist_train_dataloader = DataLoader(
  12. dataset=mnist_train_dataset,
  13. sampler=sequential_sampler,
  14. )
  15.  
  16. for i, batch_sample in enumerate(mnist_train_dataloader):
  17. batch_image, batch_label = batch_sample[0], batch_sample[1]
  18. # 下面可以将 batch_image, batch_label 传递给网络做训练,这里省略
  19. # trainging code ...
  20. # 中断
  21. break
  22.  
  23. print("The shape of minibatch is: {}".format(batch_image.shape))
  24.  
  25. # 导入可视化 Python 库,若没有请安装
  26. import matplotlib.pyplot as plt
  27.  
  28. def show(batch_image, batch_label):
  29. for i in range(4):
  30. plt.subplot(1, 4, i+1)
  31. plt.imshow(batch_image[i][:,:,-1], cmap='gray')
  32. plt.xticks([])
  33. plt.yticks([])
  34. plt.title("label: {}".format(batch_label[i]))
  35. plt.show()
  36.  
  37. # 可视化数据
  38. show(batch_image, batch_label)

输出:

  1. The shape of minibatch is: (4, 28, 28, 1)

可视化第一批 MNIST 数据:

../_images/mnist_batch.png 图2

然后,我们构建一个做 RandomResizedCrop transform 的 MNIST DataLoader,并查看此时第一个 minibatch 的图片。

  1. # 导入 MegEngine 已支持的一些数据增强操作
  2. from megengine.data.transform import RandomResizedCrop
  3.  
  4. dataloader = DataLoader(
  5. mnist_train_dataset,
  6. sampler=sequential_sampler,
  7. # 指定随机裁剪后的图片的输出size
  8. transform=RandomResizedCrop(output_size=28),
  9. )
  10.  
  11. for i, batch_sample in enumerate(dataloader):
  12. batch_image, batch_label = batch_sample[0], batch_sample[1]
  13. break;
  14.  
  15. show(batch_image, batch_label)

可视化第一个批数据:

../_images/mnist_aug.png 图3

可以看到,此时图片经过了随机裁剪并 resize 回原尺寸。

组合变换(Compose Transform)

我们经常需要做一系列数据变换。比如:

  • 数据归一化:我们可以通过 Transform 中提供的 Normalize 类来实现;

  • Pad:对图片的每条边补零以增大图片尺寸,通过 Pad 类来实现;

  • 维度转换:将 (Batch-size, Hight, Width, Channel) 维度的 minibatch 转换为 (Batch-size, Channel, Hight, Width)(因为这是 MegEngine 支持的数据格式),通过 ToMode 类来实现;

  • 其他的转换操作

为了方便使用,MegEngine 中的 Compose 类允许我们组合多个 Transform 并传递给 DataLoader 的 transform 参数。

接下来我们通过 Compose 类将之前的 RandomResizedCrop 操作与 NormalizePadToMode 操作组合起来,实现多种数据转换操作的混合使用。运行如下代码查看转换 minibatch 的维度信息。

  1. from megengine.data.transform import RandomResizedCrop, Normalize, ToMode, Pad, Compose
  2.  
  3. # 利用 Compose 组合多个 Transform 操作
  4. dataloader = DataLoader(
  5. mnist_train_dataset,
  6. sampler=sequential_sampler,
  7. transform=Compose([
  8. RandomResizedCrop(output_size=28),
  9. # mean 和 std 分别是 MNIST 数据的均值和标准差,图片数值范围是 0~255
  10. Normalize(mean=0.1307*255, std=0.3081*255),
  11. Pad(2),
  12. ToMode('CHW'),
  13. ])
  14. )
  15.  
  16. for i, batch_sample in enumerate(dataloader):
  17. batch_image, batch_label = batch_sample[0], batch_sample[1]
  18. break;
  19.  
  20. print("The shape of the batch is now: {}".format(batch_image.shape))

输出:

  1. The shape of the batch is now: (4, 1, 32, 32)

可以看到此时 minibatch 数据的 channel 维换了位置,且图片尺寸变为32。

DataLoader 中其他参数的用法请参考 DataLoader 文档。