Tensorflow读取数据的方式

  |   0 评论   |   0 浏览

背景

学习TensorFlow读取数据的方式。

四种读取方式

简介

TensorFlow输入数据的方法有四种:

  • tf.data API:可以很容易的构建一个复杂的输入通道(pipeline)(首选数据输入方式)(Eager模式必须使用该API来构建输入通道)
  • Feeding:使用Python代码提供数据,然后将数据feeding到计算图中。
  • QueueRunner:基于队列的输入通道(在计算图计算前从队列中读取数据)
  • Preloaded data:用一个constant常量将数据集加载到计算图中(主要用于小数据集)

QueueRunner细节

首先,我们创建整个图。

它的input pipeline将有几个阶段,这些阶段通过Queue连在一起。

这里写图片描述

第一个阶段将会产生要读取的文件的文件名,并将文件名enqueue到filename queue。

第二个阶段使用一个Reader来dequeue文件名并读取,产生example,并将example enqueue到一个example queue。根据你的设置,你可能有很多第二阶段(并行),所以你可以从并行地读取多个文件。

最后一个阶段是一个enqueue操作,将example enqueue成一个queue,然后等待下一步操作。我们想要开启多个线程运行着些enqueue操作,所以我们的训练loop能够从example queue中dequeue examples。

结论:

  1. Preloaded data:适用于小数据集,可以全部预加载到内存中。
  2. QueueRunner:基于队列的读取,比如读取TFRecords文件。
  3. Feeding:只用于小数据集和调试,数据输入效率最低的方式。
  4. tf.data API:基于C++的多线程及队列,彻底提高了效率。不建议再使用 QueueRunner 了,使用tf.data API简单、高效。

具体见 TensorFlow 数据读取方法总结

导入数据(Reading data)

TensorFlow tf.data 导入数据(tf.data官方教程)

使用 tf.data API 可以轻松处理大量数据、不同的数据格式以及复杂的转换。

tf.data.Dataset:表示一系列元素,其中每个元素包含一个或多个 Tensor 对象。例如,在图片管道中,一个元素可能是单个训练样本,具有一对表示图片数据和标签的张量。可以通过两种不同的方式来创建数据集。

tf.data.Iterator:这是从数据集中提取元素的主要方法。Iterator.get_next() 指令会在执行时生成 Dataset 的下一个元素,并且此指令通常充当输入管道和模型之间的接口。最简单的迭代器是“单次迭代器”,它会对处理好的 Dataset 进行单次迭代。要实现更复杂的用途,您可以通过 Iterator.initializer 指令使用不同的数据集重新初始化和参数化迭代器,这样一来,您就可以在同一个程序中对训练和验证数据进行多次迭代(举例而言)。

tf.data API

TensorFlow版本:1.12.0。

使用GPU、TPU可以减少训练中每个step所需的时间。

然而要提高吞吐能力,也需要在下个step进行前,预先准备好下个step其所需要的数据。这正是tf.data API的优势。

数据输入管道结构

TensorFlow数据输入管道可以被抽象为一个 ETL 过程(Extract,Transform,Load):

  • Extract:从硬盘上读取数据 ------ 可以是本地(HDD 或 SSD),也可以是网盘(GCS 或 HDFS)
  • Transform:使用 CPU 去解析、预处理数据 ------ 比如:图像解码、数据增强、变换(比如:随机裁剪、翻转、颜色变换)、打乱、batching。
  • Load:将 Transform 后的数据加载到 计算设备 ------ 例如:GPU、TPU 等设备。

上述的数据输入管道使用 CPU 来进行数据的 ETL 过程,从而让 GPU、TPU 等设备专心进行模型的训练过程(提高了设备的利用率)。另外,将数据输入管道抽象为 ETL 过程,有利于我们对数据输入管道进行优化。

tf.data优化数据输入管道

ET和L解耦,即prefetch过程

这里写图片描述

Pipelining将训练 step 中的 数据准备模型执行 “并行”。当计算设备在执行第 N 个训练 step 时,CPU 为第 N+1 个训练 step 准备数据。通过两个过程的重叠,单个训练 step 的时间等于 CPU 准备数据的时间 和 计算设备执行训练 step 的时间中较大值。

这里写图片描述

数据变换(T)的并行化 ------ 并行map,融合mapbatch

使用 tf.data.Dataset.map,我们可以很方便地对数据集中的各个元素进行预处理。因为输入元素之间时独立的,所以可以在多个 CPU 核心上并行地进行预处理。map 变换提供了一个 num_parallel_calls 参数去指定并行的级别。例如,下图为 num_parallel_calls=2map 变换的示意图:

这里写图片描述

数据读取(E)的并行化------并行地读取并解析多个数据文件

在实际应用中,输入数据可能被存储在网盘(例如,GCS 或 HDFS)(要么因为输入数据不适合本地,要么因为训练是分布式的,在每台机器上复制输入数据是没有意义的)。另外,在本地能够很好的读取数据的数据输入管道也可能会卡在 I/O 瓶颈上,因为 本地 和 远程存储 有以下区别:

  • Time-to-first-byte(读取第一个bytes的时间):从远程存储读取文件的第一个字节的时间比本地存储长一个数量级。
  • Read throughput(读取吞吐量):虽然远程存储通常提供大的聚合带宽,但是读取单个文件可能仅能利用该带宽的一小部分。

另外,一旦原始字节被读取到内存中,也可能需要对数据进行反序列化或解密(例如:protobuf),这将导致额外的负载。不管数据是本地存储还是远程存储,该开销都存在,但如果数据未被高效地预加载,则远程情况下可能更糟。

为了减轻各种数据读取(E)开销的影响,tf.data 提供了 tf.contrib.data.parallel_interleave 函数。该函数可以并行地从多个文件中提取并解析数据。同时读取的文件数可以通过参数 cycle_length 来指定。

下图说明了将 parallel_interleave 中的 cycle_length=2 时的效果:

这里写图片描述

数据操作顺序

Map and Batch: map开销很小时,batch形式的map更高效。

将用户自定义的函数传给 map 函数 会产生调度、执行用户自定义函数的负载。一般情况下,这个负载与自定义函数的计算量相比很小。但是,如果 map 的函数的计算量很小,这个负载将是主要开销。在这种情况下,我们推荐使用向量化的自定义函数(它一次对一个batch进行变换),并且在 map 变换前使用 batch 变换。

Map and Cache ------ 通过cache进一步加速

tf.data.Dataset.cache 变化能够在内存或本地存储器上缓存一个数据集。如果传递给 map 变换的用户自定义函数的计算量很大,只要得到的数据集仍然适合内存或本地存储,就可以在 map 转换之后应用 cache 转换。

如果用户定义函数导致存储数据集需要的空间超过了 cache 的容量,考虑提前对数据集进行预处理,以减少资源的使用。

注意:cache将数据集进行缓存能够有效地提高数据输入管道的性能,但是,cache位置放置错误时,会导致模型性能下降。

Map and Interleave / Prefetch / Shuffle ------ 变换的顺序对内存使用量的影响

因为各个变换函数(包括 interleave,prefetch,shuffle)都有自己的内部缓存,所以如果传给 map 变换的 用户自定义函数 改变了元素的 size,那么 map 变换的次序影响内存的使用量。通常情况下,我们建议选择内存使用量更低的次序,除非不同的次序能够产生性能上的提高(例如,为了使用融合的 tf.contrib.data.map_and_batch)。

Repeat and Shuffle ------ repeat在前性能优,shuffle在前次序强

tf.data.Dataset.repeat 变换重复输入数据有限次(或无限次);数据的每一次重复称为一个 epoch。tf.data.Dataset.shuffle 变换随机打乱数据集 example 的次序。

如果 repeat 变换被放在 shuffle 变换之前,那么 epoch 边界将变得模糊。也就是说,某些元素可以在其他元素出现一次之前重复。另一方面,如果在 repeat 变换之前应用 shuffle 变换,那么在每个 epoch 开始时,性能可能会下降(因为这时,也需要进行 shuffle 变化的初始化)。换句话说,将 repeat 放置在 shuffle 之前,提供了更好的性能,将 shuffle 放置在 repeat 之前,提供了更强的次序保证。

当可能时,我们推荐使用融合op:tf.contrib.data.shuffle_and_repeat 变换,这个变换在性能和更强的次序保证上都是最好的(good performance and strong ordering guarantees)。否则,我们推荐在 repeat 之前使用 shuffle

数据输入管道最佳实践

  • 使用 prefetch 函数去重叠 数据读取器 和 数据消耗器的工作。我们尤其推荐在输入管道的末端添加 prefetch(n) (n是batch size),以重叠 CPU 上的变换 及 GPU/TPU设备上的训练。[2.1]
  • 通过设置 num_parallel_calls 参数,来并行 map 变换。我们建议使用将该参数设置为 CPU 的核心数。[2.2]
  • 如果你使用 batch 变换来将预处理好的元素 batching,我们建议使用融合op:map_and_batch 变换;尤其是你如果使用大的batch size。[2.2]
  • 如果你的数据存在远程存储上,(且有时需要解析),我们建议使用 parallel_interleave 来并行数据的读取和解析。[2.3]
  • 将简单的用户自定义函数进行向量化,然后传递给 map 变换去分摊 用户自定义函数有关的调用、执行的负载。[3.1]
  • 如果你的数据能够加载到内存,使用 cache 变化去在训练的第一个 epoch 将数据集缓存到内存,所以能避免后来的 epoch 读取、解析、变换数据的负载。[3.2]
  • 如果你的预处理会增加你数据的 size,我们建议你首先使用 interleaveprefetchshuffle 变换去减少内存使用量(如果可能)。[3.3]
  • 我们建议在 repeat 变换之前使用 shuffle 变换,最好使用融合op: shuffle_and_repeat 变换。[3.4]

具体看 TensorFlow 高性能数据输入管道设计指南

参考