数据准备、载入及加速

在命令式编程模式(动态图)下,使用PaddlePaddle Fluid准备数据相比声明式编程模式(静态图)较为简洁,总体分为三个步骤:

Step 1: 自定义Reader生成训练/预测数据

首先,自定义一个生成器类型的Reader数据源,通过yield的方式顺序输出训练/预测数据,其生成的数据类型可以为Numpy Array或Tensor。

根据Reader返回的数据形式的不同,Fluid支持配置三种不同的数据生成器,以满足不同的用户需求,下面举例说明。

以常见的图像类模型输入需求为例,一个Sample的输入通常是 (image, label),即(处理图像,真实标签),对此Fluid支持的三种不同Reader分别为:

  1. Sample(样本)级的Reader:每次生成的数据为 (image, label)
  2. Sample List 级的Reader:每次生成的数据为 [(image, label), (image, label), (image, label), …]
  3. Batch 级的Reader:每次生成的数据为 ([BATCH_SIZE, image], [BATCH_SIZE, label])

如果您的数据是Sample形式的数据,但是想要在外部以Batch为单位组件数据,并对整个Batch的数据进行预处理,我们提供了相关的工具,详细内容请参见: 数据预处理工具 ,此处仅通过简单的例子说明如下:

  1. import paddle
  2. mnist_train = paddle.dataset.mnist.train()
  3. mnist_train_batch_reader = paddle.batch(mnist_train, 128) # 128 为 batch size

在上面例子中,mnist_train生成的数据是Sample为单位的,经过 paddle.batch 处理后,会以一个Batch(包含128个Sample)为单位生成数据。

Step 2. 创建DataLoader并设置自定义Reader

在命令式编程模式(动态图)下,我们推荐使用DataLoader进行数据载入,DataLoader默认使用线程进行异步加速,使用也更加简便直观。

  1. 创建DataLoader

命令式编程模式(动态图)下,创建DataLoader对象的方式为:

  1. import paddle.fluid as fluid
  2. data_loader = fluid.io.DataLoader.from_generator(capacity=10)

其中,

  • capacity为DataLoader对象的缓存区大小,单位为batch数量。

该值根据实际需求设定即可,一般对于单卡训练的场景,设置5到20即可满足需求,特别是对于一个Batch数据较大的情况,不宜设置过大,可能会消耗过多内存资源,反而降低效率。 如果模型消耗训练或预测数据较慢的话,缓存队列设置过大也没有收益,但对于模型训练速度非常快,数据载入是训练速度瓶颈的情况,适当扩大队列能够提高训练速度。

对于其他参数,我们推荐使用默认设置,无需再额外配置,具体可参见官方文档 DataLoader

  1. 设置DataLoader对象的数据源

上文中讲到,DataLoader支持配置三种不同的自定义Reader,配置这三种Reader的方法分别为: set_sample_generator()set_sample_list_generatorset_batch_generator() 。 这三个方法均接收Python生成器 generator 作为参数,其区别在于:

  • set_sample_generator() 要求 generator 返回的数据格式为(image_1, label_1),其中image1和label_1为单个样本的Numpy Array类型数据。
  • set_sample_list_generator() 要求 generator 返回的数据格式为[(image_1, label_1), (image_2, label_2), …, (image_n, label_n)],其中image_i和label_i均为每个样本的Numpy Array类型数据,n为batch size。
  • set_batch_generator() 要求 generator 返回的数据的数据格式为([BATCH_SIZE, image], [BATCH_SIZE, label]),其中[BATCH_SIZE, image]和[BATCH_SIZE, label]为batch级的Numpy Array或Tensor类型数据。

此处我们构建三个不同的示例生成器,对应上述三个接口:

  1. import numpy as np
  2. BATCH_NUM = 10
  3. BATCH_SIZE = 16
  4. MNIST_IMAGE_SIZE = 784
  5. MNIST_LABLE_SIZE = 1
  6. # 伪数据生成函数,服务于下述三种不同的生成器
  7. def get_random_images_and_labels(image_shape, label_shape):
  8. image = np.random.random(size=image_shape).astype('float32')
  9. label = np.random.random(size=label_shape).astype('int64')
  10. return image, label
  11. # 每次生成一个Sample,使用set_sample_generator配置数据源
  12. def sample_generator_creator():
  13. def __reader__():
  14. for _ in range(BATCH_NUM * BATCH_SIZE):
  15. image, label = get_random_images_and_labels([MNIST_IMAGE_SIZE], [MNIST_LABLE_SIZE])
  16. yield image, label
  17. return __reader__
  18. # 每次生成一个Sample List,使用set_sample_list_generator配置数据源
  19. def sample_list_generator_creator():
  20. def __reader__():
  21. for _ in range(BATCH_NUM):
  22. sample_list = []
  23. for _ in range(BATCH_SIZE):
  24. image, label = get_random_images_and_labels([MNIST_IMAGE_SIZE], [MNIST_LABLE_SIZE])
  25. sample_list.append([image, label])
  26. yield sample_list
  27. return __reader__
  28. # 每次生成一个Batch,使用set_batch_generator配置数据源
  29. def batch_generator_creator():
  30. def __reader__():
  31. for _ in range(BATCH_NUM):
  32. batch_image, batch_label = get_random_images_and_labels([BATCH_SIZE, MNIST_LABLE_SIZE], [BATCH_SIZE, MNIST_LABLE_SIZE])
  33. yield batch_image, batch_label
  34. return __reader__

然后,可以根据需求为DataLoader配置不同的数据源,此处完整的创建DataLoader及相应配置为:

  1. import paddle.fluid as fluid
  2. BATCH_SIZE = 16
  3. place = fluid.CPUPlace() # 或者 fluid.CUDAPlace(0)
  4. fluid.enable_imperative(place)
  5. # 使用sample数据生成器作为DataLoader的数据源
  6. data_loader1 = fluid.io.DataLoader.from_generator(capacity=10)
  7. data_loader1.set_sample_generator(sample_generator_creator(), batch_size=BATCH_SIZE, places=place)
  8. # 使用sample list数据生成器作为DataLoader的数据源
  9. data_loader2 = fluid.io.DataLoader.from_generator(capacity=10)
  10. data_loader2.set_sample_list_generator(sample_list_generator_creator(), places=place)
  11. # 使用batch数据生成器作为DataLoader的数据源
  12. data_loader3 = fluid.io.DataLoader.from_generator(capacity=10)
  13. data_loader3.set_batch_generator(batch_generator_creator(), places=place)

此处有两点值得注意:

  1. DataLoader的使用需要在命令式编程模式(动态图)下,即提前通过 fluid.enable_imperative() 进入命令式编程模式(动态图)。
  2. 命令式编程模式(动态图)下DataLoader配置数据源,需要在 set_XXX_generator 时执行place,一般为当前执行的place。(该点后续可能会优化,在这里默认使用动态图当前place,而无需用户指定)

Step 3. 使用DataLoader进行模型训练和预测

下面我们通过一个完整的例子来说明命令式编程模式(动态图)下DataLoader在训练/预测时的使用:

  1. 构建命令式编程模式(动态图)模型

此处我们构建一个简单的命令式编程模式(动态图)网络。

  1. 创建网络执行对象,配置DataLoader,进行训练或预测
  1. import paddle.fluid as fluid
  2. EPOCH_NUM = 4
  3. BATCH_SIZE = 16
  4. MNIST_IMAGE_SIZE = 784
  5. MNIST_LABLE_SIZE = 1
  6. # 1. 构建命令式编程模式(动态图)网络
  7. class MyLayer(fluid.dygraph.Layer):
  8. def __init__(self):
  9. super(MyLayer, self).__init__()
  10. self.linear = fluid.dygraph.nn.Linear(MNIST_LABLE_SIZE, 10)
  11. def forward(self, inputs, label=None):
  12. x = self.linear(inputs)
  13. if label is not None:
  14. loss = fluid.layers.cross_entropy(x, label)
  15. avg_loss = fluid.layers.mean(loss)
  16. return x, avg_loss
  17. else:
  18. return x
  19. # 2. 创建网络执行对象,配置DataLoader,进行训练或预测
  20. place = fluid.CPUPlace() # 或者 fluid.CUDAPlace(0)
  21. fluid.enable_imperative(place)
  22. # 创建执行的网络对象
  23. my_layer = MyLayer()
  24. # 添加优化器
  25. adam = fluid.optimizer.AdamOptimizer(
  26. learning_rate=0.001, parameter_list=my_layer.parameters())
  27. # 配置DataLoader
  28. train_loader = fluid.io.DataLoader.from_generator(capacity=10)
  29. train_loader.set_sample_list_generator(sample_list_generator_creator(), places=place)
  30. # 执行训练/预测
  31. for _ in range(EPOCH_NUM):
  32. for data in train_loader():
  33. # 拆解载入数据
  34. image, label = data
  35. # 执行前向
  36. x, avg_loss = my_layer(image, label)
  37. # 执行反向
  38. avg_loss.backward()
  39. # 梯度更新
  40. adam.minimize(avg_loss)
  41. mnist.clear_gradients()

异步数据读取加速

在命令式编程模式(动态图)下,DataLoader默认使用子线程进行异步数据读取加速,但由于python GIL(全局解释器锁)的限制,在数据载入开销比较大的场景下,仅使用线程进行加速的效果并不能满足训练速度需求,因此我们提供了使用子进程加速的方式,进一步提升数据读取的效率。

配置使用子进程加速,仅需要在DataLoader创建时设置 use_multiprocess=True 即可,此参数默认为False,例如

  1. import paddle.fluid as fluid
  2. data_loader = fluid.io.DataLoader.from_generator(capacity=10, use_multiprocess=True)

其他使用方式均与前文中的示例一致。

关于配置此选项带来的加速效果,此处列出一些测试数据供参考。表中数据为单个Epoch的训练耗时,单位为秒(s),模型名后括号内为模型训练所使用的Batch Size。

模型 (Batch Size)DataLoader默认模式耗时 (s)DataLoader+子进程模式耗时 (s)加速比例
Mnist (64)10.166.44+57.8%
ResNet (32)83.5653.75+55.5%
Ptb (20)108.56108.27+0.2%
MobileNet V1 (256)5041.773249.51+55.2%

从表中可以看出,在图像类训练任务这种数据载入开销较大的情况下,配置子进程加速的效果是比较明显的,但在一个Batch数据很小的训练任务中,没有明显提升,在这种情况下,数据载入的开销不会是训练速度的瓶颈,使用默认模式即可。

注解

命令式编程模式(动态图)下DataLoader多进程方式采用共享内存机制实现Tensor的进程间传输,使用时需要保证机器上或Docker共享内存空间足够大,需要大于 DataLoader.capacity * Batch Size * 单个Sample的数据大小。在物理机或者虚拟机上训练一般不会有问题,但在docker中可能出现共享内存不足的情况,因为docker中/dev/shm目录空间不够大(默认64M),若内存空间不足,建议配置较大的共享内存空间,或者仍使用单进程。