在机器学习项目中构建输入管道总是漫长而痛苦的,并且比构建实际模型需要更多的时间。在本教程中,我们将学习如何使用TensorFlow的数据集模块tf.data为图像和文本构建有效的管道。
官方资源
- API docs for
tf.data
- API docs for
tf.contrib.data
: new features still in beta mode. Contains useful functions that will soon be added to the maintf.data
- Datasets Quick Start: gentle introduction to
tf.data
- Programmer’s guide: more advanced and detailed guide to the best practices when using Datasets in TensorFlow
- Performance guide: advanced guide to improve performance of the data pipeline
- Official blog post introducing Datasets and Estimators. We don’t use Estimators in our code examples so you can safely ignore them for now.
- Slides from the creator of tf.data explaining the API, best practices (don’t forget to read the speaker notes below the slides)
- Origin github issue for Datasets: a bit of history on the origin of
tf.data
- Stackoverflow tag for the Datasets API
新建file.txt文件,包含语句
I use Tensorflow You use PyTorch Both are great
使用tf.dataAPI读取文件:
dataset = tf.data.TextLineDataset('file.txt')
dataset是Tensorflow的图节点,其包含着读取文件的指令。如果我们想要读取文件,我们需要初始化图并在会话中评估这个节点。尽管这个听起来很复杂,实际上恰恰相反。现在甚至数据集对象也是图的一部分,因此你不需要担心如何将数输入模型。
我们需要增加一些额外的代码,来完成工作。首先,我们创建一个基于整个数据集的迭代器对象。
iterator = dataset.make_one_shot_iterator()
next_element = iterator.get_next()
The
one_shot_iterator
method creates an iterator that will be able to iterate once over the dataset. In other words, once we reach the end of the dataset, it will stop yielding elements and raise an Exception.
现在,next_element 是图的节点。每一次执行,它将包含着迭代器的下一个元素。执行如下
with tf.Session() as sess:
for i in range(3):
print(sess.run(next_element))
既然已经了解tf.data API的基本原理,下面介绍一些先进的技巧。
例如
import tensorflow as tf
print(tf.__version__)
dataset = tf.data.TextLineDataset('file.txt')
#the value in dataet :‘I use Tensorflow’->['I', 'use', 'Tensorflow']
#'You use PyTorch'->['You', 'use', 'PyTorch']
#基于分隔符分割输入的每个元素
#map函数映射输入函数到整个数据集
#values为SparseTensor属性,表示取值
dataset = dataset.map(lambda string: tf.string_split([string]).values)
#将数据集中连续的元素以batch_size为单位集合成批次
dataset = dataset.batch(2)
#预取数据,即它总是使得一个批次的数据准备被加载。
dataset = dataset.prefetch(1)
#创建基于整个数据集的迭代器
iterator = dataset.make_one_shot_iterator()
#使用get_next()方法取出元素,每次执行,next_element保存迭代器的下一个元素
next_element = iterator.get_next()
with tf.Session() as sess:
print(sess.run(next_element))
通过初始化节点,相当于重新加载数据(make_one_shot仅执行一个epoch),我们可以选择从头开始训练。这对于我们执行多次epoch操作,极为有利。
dataset = tf.data.TextLineDataset('file.txt')
iterator = dataset.make_initializable_iterator()
next_element = iterator.get_next()
init_op = iterator.initializer
with tf.Session() as sess:
#初始化迭代器
sess.run(init_op)
print(sess.run(next_element))
print(sess.run(next_element))
#移动迭代器到最初
sess.run(init_op)
print(sess.run(next_element))
假设我们已经有了一个包含所有JPEG图像名称的列表和一个与之对应的标签列表。
通道建立步骤如下:
tf.data.Dataset的方法的输入为其内部的数据。
dataset = tf.data.Dataset.from_tensor_slices((filenames, labels))
dataset = dataset.shuffle(len(filenames))
dataset = dataset.map(parse_function, num_parallel_calls=4)
dataset = dataset.map(train_preprocess, num_parallel_calls=4)
dataset = dataset.batch(batch_size)
dataset = dataset.prefetch(1)
parse_function功能如下:
def parse_function(filename, label):
image_string = tf.read_file(filename)
#Don't use tf.image.decode_image, or the output shape will be undefined.
image = tf.image.decode_jepg(image_string, channels)
#This will convert to float values in [0, 1]
image = tf.image.convert_image_dtype(image, tf.float32)
image = tf.image.resize_images(image, [64, 64])
return resized_image, label
函数 train_preprocess(optionally)可用于执行数据扩增。
def train_preprocess(image, label):
image tf.image.random_flip_left_right(image)
image = tf.image.random_brightness(image, max_delta=32)
image = tf.image.random_saturation(image, lowe=0.5, upper=1.5)
#Make sure the image is still in [0, 1]
image = tf.clip_by_value(image, 0.0, 1.0)
return image, label
参考https://www.tensorflow.org/guide/performance/overview#input_pipeline
一般情况下,将所有的数据处理通道放在cpu上,以此保证gpu仅用于训练深度神经网络模型。
wtih tf.device('/cpu:0'):
dataset = ....
当在一个数据集上训练时,我们常需要重复循环多个epochs并打乱重组。
一个特别需要注意的地方时,当重组时候,保证buffer_size参数需要足够的大,一般为整个数据集大小。此参数值越大,它在初始时候将花费更多的时间加载数据。然而一个较小的buffer_size可能对于训练产生糟糕的影响。参考解释
最好避免这种错误的方法是提前将数据集分为train/dev/test并打乱。参考解释
另一个建议点是,最好在数据通道建立初始就打乱和重复。例如,假设数据集输入是一个文件名列表,如果直接打乱,之后tf.data.Dataset.shuffle()将只包含文件名,这只占用很小的内存资源。
When choosing the ordering between shuffle and repeat, you may consider two options:
- shuffle then repeat: we shuffle the dataset in a certain way, and repeat this shuffling for multiple epochs (ex:
[1, 3, 2, 1, 3, 2]
for 2 epochs with 3 elements in the dataset)- repeat then shuffle: we repeat the dataset for multiple epochs and then shuffle (ex:
[1, 2, 1, 3, 3, 2]
for 2 epochs with 3 elements in the dataset)The second method provides a better shuffling, but you might wait multiple epochs without seeing an example. The first method makes sure that you always see every element in the dataset at each epoch. You can also use
tf.contrib.data.shuffle_and_repeat()
to perform shuffle and repeat.
tf.data模块运行时,使用多线程进行数据通道处理,从而实现并行,这种操作几乎是透明的。我们只需要添加一个num_parallel_calls参数到每一个dataset.map()call中,
num_threads = 4
dataset = dataset.map(parse_function, num_parallel_calls=num_threads)
如果使用值tf.data.experimental.AUTOTUNE,则根据可用的CPU动态设置并行调用的数量。
当GPU执行在当前批次执行前向或者后向传播时,我们希望CPU处理下一个批次的数据,以便于数据批次能够迅速被GPU使用。我们希望GPU被完全、时刻用于训练。我们称这种机制为消费者/生产者重叠,消费者是GPU,生产者是CPU。
使用tf.data,你可以轻易的做到只一点,只需要在通道末尾调用dataset.prefetch(1)。这将总是预取一个批次的数据,并且保证总有一个数据准备好被消耗。
dataset = dataset.batch(64)
dataset = dataset.prefetch(1)
特殊情况下,预取超过一个批次的数据会更有用。例如,当预处理时间较长,预取10个批次的数据将会优于取10次。
更具体的例子,加入10%的批次需要10s计算,90%需要1s计算。如何GPU花费2s在一个batch上训练,通过预取多个批次,我们永远不需要等待那些稀缺的花费时间更长的批次。
总结,不同转换数据的方式执行顺序如下:
更多https://towardsdatascience.com/how-to-use-dataset-in-tensorflow-c758ef9e4428