TensorFlow官方教程翻译3:读取数据

原文地址:https://www.tensorflow.org/programmers_guide/reading_data
这篇教程已经从官网程序员指引中移除,官方给出的说法是,此类多线程基于队列的读取数据的方式,从TensorFlow 1.2版本以后被抛弃,后续不再提供支持更新,从1.2版本之后,提供DataSet以及tf.contrib.data来支持数据的读取,相关的教程请见https://www.tensorflow.org/programmers_guide/datasets
有三种主要方法用来将数据导入到TensorFlow的程序中:

  • 供给:运行每一步,Python代码来提供数据。
  • 从文件读取:在TensorFlow的图开始,一个输入管道从文件中读取数据。
  • 预先载入数据:TensorFlow的图中的一个常量或者变量保存所有数据(对于小数据集而言)。

供给

TensorFlow的供给机制让你可以将数据注入到计算图中的任何张量中。因此,一个python的计算可以直接供给数据到图中。
通过给run()或者eval()传递feed_dict参数来提供供给数据,以启动运算。

with tf.Session():
  input = tf.placeholder(tf.float32)
  classifier = ...
  print(classifier.eval(feed_dict={input: my_python_preprocessing_fn()}))

当你用供给数据提供任意的张量(包括变量和常量)时,最好的做法是使用tf.placeholder节点。Placeholder的存在的目标就是为了供给数据。它不会被初始化,并且没有包含任何数据。如果placeholder在没有数据的情况下运行,那么它会产生错误,因此你不要忘记供给它数据。
使用placeholder,并供给MNIST数据来训练的例子可以在tensorflow/examples/tutorials/mnist/fully_connected_feed.py中找到,并且在MNIST教程中有介绍。

从文件读取

一个典型的从文件读取记录的管道有如下步骤:

  1. 文件列表
  2. 可选文件名打乱
  3. 可选代次限制
  4. 文件名队列
  5. 对应文件格式的阅读程序
  6. 对于阅读程序读取的记录的解码器
  7. 可选预处理
  8. 样本队列

文件名列表,乱序和代次限制

对于文件列表,使用常量字符串张量(像[”file0”,”file1”]或者[(”file%d”%i for i in range(2))],或者tf.train.match_filenames_once函数。
传递一个文件名列表给tf.train.string_input_producer函数,那么这个函数会创建一个先进先出队列来保存文件名,直到文件阅读程序需要它们。
string_input_producer有选项设置乱序和设置代数的最大数目。队列运行器在每一代都将整个文件名列表添加到队列中一次,如果shuffle=True,那么会在一代中打乱一次文件名。这个程序提供统一的对文件的采样,因此样本对于彼此之间,不会出现欠采样或者过采样。

队列运行器在一个不同于文件阅读器从队列中取出文件名的线程上工作,因此打乱和入队操作不会阻塞文件阅读器。

文件格式

选择匹配你输入文件格式的文件阅读器,然后将文件名队列传给阅读器的read方法。read方法会输出一个标识文件和记录(如果你有些很奇怪的记录,这个对于调试有帮助)的关键字(key),以及一个字符串。使用一个(或多个)解码器,然后进行转换操作将这个字符串转换成组成一个样本的张量。

CSV文件

为了读取在逗号分隔数值格式的文本文件,使用tf.TextLineReader,并配合tf.decode_csv操作。例如:

filename_queue = tf.train.string_input_producer(["file0.csv", "file1.csv"])

reader = tf.TextLineReader()
key, value = reader.read(filename_queue)

# Default values, in case of empty columns. Also specifies the type of the
# decoded result.
record_defaults = [[1], [1], [1], [1], [1]]
col1, col2, col3, col4, col5 = tf.decode_csv(
    value, record_defaults=record_defaults)
features = tf.stack([col1, col2, col3, col4])

with tf.Session() as sess:
  # Start populating the filename queue.
  coord = tf.train.Coordinator()
  threads = tf.train.start_queue_runners(coord=coord)

  for i in range(1200):
    # Retrieve a single instance:
    example, label = sess.run([features, col5])

  coord.request_stop()
  coord.join(threads)

每次运行read都从文件中读取单独的一行数据。然后,decode_csv操作将结果解析成一个张量的列表。record_defaults参数确定了结果张量的类型,以及如果在输入的字符串中有值缺损,那么这个参数将被用来设置缺损项。

你必须在你调用run或者eval来运行read操作前,调用tf.train.start_queue_runners来填充队列,否则的话read操作将会在等待从队列中获取文件名的时候被阻塞。

固定长度的记录

为了读取二进制文件,二进制文件中每条记录都是固定的字节数:标签占一个字节,后面跟着3072个字节的图片数据。一旦你有了一个unit8类型的张量,标准操作能够根据需要切分每一块,然后重组格式。关于CIFAR-10数据集,你可以在tensorflow_models/tutorials/image/cifar10/cifar10_input.py 里看到如何读取并解码数据,并且代码在这个教程中描述了。

标准的TensorFlow格式

另外一种方法是,将你有的不论什么样的数据,转换成支持的格式。这种方式使得将数据集和网络结构混合以及匹配变得容易。TensorFlow的推荐格式是TFRecord文件,这个文件包含tf.train.Example协议缓存(协议包含特征作为字段)。你可以写很少的代码来获取你的数据,将其填充到一个样本协议缓冲中,然后把协议缓冲序列化成一个字符串,接着使用tf.python_io.TFRecordWriter将字符串写进一个TFRecord文件中。举例说明,tensorflow/examples/how_tos/reading_data/convert_to_records.py将MNIST数据转换成了这种格式。

使用tf.TFRecordReader配合tf.parse_single_example解码器来读取TFRecorder文件。tf.parse_single_example操作将样本协议缓冲解码成张量。一个使用由convert_to_records产生的数据MNIST的实例可以在tensorflow/examples/how_tos/reading_data/fully_connected_reader.py找到,你可以将这个实例与fully_connected_feed的版本进行比较。

预处理

你可以对这些样本做任意你想要进行的预处理。这可以任何不依赖于训练参数的处理。比如正则化你的数据,随机切片,或者增加噪声或者扭曲等等。参见例子tensorflow_models/tutorials/image/cifar10/cifar10_input.py

批处理

在输入管道的末端,我们使用另外一个队列来将样本分成批次用以训练,评估和推理。为此,我们使用一个采用了tf.train_shuffle_batch来随机化打乱样本顺序的队列。
例子:

def read_my_file_format(filename_queue):
  reader = tf.SomeReader()
  key, record_string = reader.read(filename_queue)
  example, label = tf.some_decoder(record_string)
  processed_example = some_processing(example)
  return processed_example, label

def input_pipeline(filenames, batch_size, num_epochs=None):
  filename_queue = tf.train.string_input_producer(
      filenames, num_epochs=num_epochs, shuffle=True)
  example, label = read_my_file_format(filename_queue)
  # min_after_dequeue defines how big a buffer we will randomly sample
  #   from -- bigger means better shuffling but slower start up and more
  #   memory used.
  # capacity must be larger than min_after_dequeue and the amount larger
  #   determines the maximum we will prefetch.  Recommendation:
  #   min_after_dequeue + (num_threads + a small safety margin) * batch_size
  min_after_dequeue = 10000
  capacity = min_after_dequeue + 3 * batch_size
  example_batch, label_batch = tf.train.shuffle_batch(
      [example, label], batch_size=batch_size, capacity=capacity,
      min_after_dequeue=min_after_dequeue)
  return example_batch, label_batch

如果你需要更多文件中的样本的并行化或者乱序,那么采用调用了tf.train.shuffle_batch_join函数的多个文件阅读器的例子。举例说明:

def read_my_file_format(filename_queue):
  # Same as above

def input_pipeline(filenames, batch_size, read_threads, num_epochs=None):
  filename_queue = tf.train.string_input_producer(
      filenames, num_epochs=num_epochs, shuffle=True)
  example_list = [read_my_file_format(filename_queue)
                  for _ in range(read_threads)]
  min_after_dequeue = 10000
  capacity = min_after_dequeue + 3 * batch_size
  example_batch, label_batch = tf.train.shuffle_batch_join(
      example_list, batch_size=batch_size, capacity=capacity,
      min_after_dequeue=min_after_dequeue)
  return example_batch, label_batch

你依旧只使用了一个文件名队列,但是这个队列被多个文件阅读器共享。我们保证采用这样的方法,不同的文件阅读器使用的同一代次中的不同文件,直到该代次的所有文件都被启用。(通常只需要单个线程填充文件名队列即可。)

另一种方法是通过调用num_threads参数大于1的tf.train.shuffle_batch函数来使用单个阅读器。这样,同时同一时间只从一个文件中读取数据(但是比一个线程快),而不是一次读取N个文件。这很重要:

  • 如果你拥有的读取线程比输入文件多,这就避免了你有彼此靠近的两个线程会从同一个文件中读取相同的样本的危险。
  • 或者并行读取N个文件导致过多的磁盘寻找。

你需要多少个线程?tf.train.shuffle_batch函数向图中添加了一个概要,这个概要表明了样本队列有多满。如果你有足够的读取线程,这个概要数据将保持在0以上。你可以使用Tensorboard来查看你训练进程的概要。

使用QueueRunner对象创建线程预读取文件
简而言之:上面提到的很多tf.train的函数都在你的图中添加了tf.train.QueueRunner的对象。这就要求你在运行任何训练和推理步骤前,都调用tf.train.start_queue_runners,否则队列会被永久的挂起。这个操作会启动线程来运行输入管道,进而填充样本队列,这样获取样本的出队操作才会成功。这最好结合tf.train.Coordinator一起使用,确保在线程发生错误的时候,利落的关闭这些线程。如果你设置了代次的限制,它会使用一个需要被初始化的代次计数器。推荐的代码模式是:

# Create the graph, etc.
init_op = tf.global_variables_initializer()

# Create a session for running operations in the Graph.
sess = tf.Session()

# Initialize the variables (like the epoch counter).
sess.run(init_op)

# Start input enqueue threads.
coord = tf.train.Coordinator()
threads = tf.train.start_queue_runners(sess=sess, coord=coord)

try:
    while not coord.should_stop():
        # Run training steps or whatever
        sess.run(train_op)

except tf.errors.OutOfRangeError:
    print('Done training -- epoch limit reached')
finally:
    # When done, ask the threads to stop.
    coord.request_stop()

# Wait for threads to finish.
coord.join(threads)
sess.close()

旁白:这里发生了什么?

首先我们创建了图。图会有一些用队列连接的管道阶段。第一阶段会产生需要读取的文件名,然后将它们放入文件名队列。第二阶段消耗文件名队列(使用阅读器),产生样本,然后将它们放入样本队列。你可能会有一些独立的第二阶段的拷贝,这取决于你如何设置,因此你可以平行的从多文件中读取数据。最后是入队操作,这个操作入队元素到下一阶段用以出队的队列中。我们想启动多个线程来运行这些入队操作,因而我们的训练循环可以从样本队列中出队获得样本。

tf.train中的辅助工具创建了这些队列和入队操作,并使用tf.train.add_queue_runner函数将tf.train.QueueRunner添加到图中。每个QueueRunner负责一个阶段,并且保存了需要在线程中运行的入队操作的列表。一旦图被创建,tf.train.start_queue_runners函数就会要求图中的每个QueueRunner启动其线程运行入队操作。

如果一切运行良好,那么现在你可以运行你的训练步骤,同时队列将在后台线程中被填充。如果你设置了代次限制,有时候出队样本的尝试会获得一个tf.errors.OutOfRangeErrore的异常。这是TensorFlow对应的文件结尾(EOF)——这意味着达到了代次限制,不再有样本可用。

最后一部分就是tf.train.Coordinator。如果任何东西告知需要停止,那么它负责让所有的线程知道。通常这是因为抛出了一个异常,比如一个线程运行一些操作的时候发生了错误(或者一个普通的Python异常)。

更多关于线程,队列,QueueRunners和Coordinators见此处。

旁白:当达到限制代次安全关闭是如何工作的
想象你有个设置了训练代次限制的模型。这意味着,在产生一个OutOfRange错误之前,产生文件名的线程只能运行一定的次数。QueueRunner会捕获这个错误,然后关闭文件名队列,最后退出线程。关闭队列做了两件事情:

  • 任何之后的入队文件名队列的尝试都会产生错误。此时不应该有任何线程尝试这么做,但是对于队列因为其他错误而关闭,这样的信息很有帮助。
  • 任何现在或者将来的出队操作,会立即成功(如果队列中有足够剩余的元素),或者立即失败(抛出OutOfRange错误)。他们不会因为等待更多的元素入队而被阻塞,因为根据此前的情况,这是不会发生的。

问题是当文件名队列被关闭,可能还会有一些文件名在队列中,因此管道的下一阶段(有阅读器和其他的预处理)可能会继续运行一段时间。一旦文件名队列用尽,下一次尝试出队一个文件名(例如从一个已经结束的阅读器的一个正在工作的文件)将会触发OutOfRange错误。不过在这种情况下,你可能有多个线程与同一个QueueRunner联系在一起。如果这不是QueueRunner中的最后一个线程,那么OutOfRange错误仅仅会导致一个线程退出。这使得那些仍然在读取它们上一个文件的线程,会一直处理到它们完成读取为止。(假设你使用tf.train.Coordinator,那么其他类型的错误会导致所有进程的终止。)一旦所有的阅读器线程触发OutOfRange错误,直到那时,首先是下一个队列,即样本队列,将被关闭。

而且样本队列将会有一些元素在队列中,因此训练还会继续,直到这些元素被消耗用尽。如果样本队列是一个tf.RandomShuffleQueue的对象,比如说你使用了shuffle_batch或者shuffle_batch_join,它通常在任何时候都会避免少于它的min_after_dequeue属性个元素被缓存。但是,一旦队列被关闭,那么限制会被取消,而队列最终会被清空。在这种情况下,当目前的训练进程尝试从样本队列中出队元素时,将会开始得到OutOfRange的错误并退出。一旦所有的线程已经完成,那么tf.train.Coordinator.join将会返回,此时你可以安全的退出。

过虑记录或者每个记录产生多个样本

除了获得形如[x,y,z]的样本,你还可以产生一批次有着形状为[batch,x,y,z]的样本。如果你想过滤掉这个记录(它可能是保留集合?),那么batch的大小可以为0;如果你想为每个记录产生多个样本,那么batch的大小可以大于1。那么只需要在调用任意一种批处理函数(比如shuffle_batch或者shuffle_batch_join)的时候,设置enqueue_many=True就可以了。

稀疏的输入数据

稀疏张量不适合队列。如果你使用稀疏张量,你必须得在批处理之后,是欧诺个tf.parse_example解码字符串记录。(而不是在批处理之前使用tf.parse_single_example)。

预先载入数据

这仅仅用于可以被完整载入内存的小数据集。有两种方式:

  • 保存数据在常量中
  • 保存数据在变量中,然后你自己初始化,接着它们的值不再变化
    使用常量更为简单,但是占用了更多内存(因为常量被保存在图的内部数据结构中,可能会被复制多次)。
training_data = ...
training_labels = ...
with tf.Session():
  input_data = tf.constant(training_data)
  input_labels = tf.constant(training_labels)
  ...
取而代之可以使用变量,你也需要在图构建之后,对其进行初始化。

training_data = ...
training_labels = ...
with tf.Session() as sess:
  data_initializer = tf.placeholder(dtype=training_data.dtype,
                                    shape=training_data.shape)
  label_initializer = tf.placeholder(dtype=training_labels.dtype,
                                     shape=training_labels.shape)
  input_data = tf.Variable(data_initializer, trainable=False, collections=[])
  input_labels = tf.Variable(label_initializer, trainable=False, collections=[])
  ...
  sess.run(input_data.initializer,
           feed_dict={data_initializer: training_data})
  sess.run(input_labels.initializer,
           feed_dict={label_initializer: training_labels})

设置trainable=False保证这个变量不会在图的GraphKeys.TRAINABLE_VARIABLES集合中,因此我们不会尝试在训练过程中更新它。设置Collections=[]保证这个变量不会在GraphKeys.GLOBAL_VARIABLES集合中,这个集合的数据作为中断点被保存和还原。
不管怎样,tf.train.slice_input_producer每次可以被用来产生一个切片。这打乱了样本在整个一代中的顺序,因此当批处理的时候不需要近一步的打乱样本。因此,我们使用简单的tf.train.batch函数来代替shuffle_batch函数。为了使用多个预处理线程,将num_threads参数的值设置为大于1的数值。
使用常量预先载入数据的MNIST例子可以在tensorflow/examples/how_tos/reading_data/fully_connected_preloaded.py找到,而使用变量预先载入数据的例子可以在tensorflow/examples/how_tos/reading_data/fully_connected_preloaded_var.py找到,你可以将这些例子与上面fully_connected_feed以及fully_connected_reader版本进行比较。

多输入管道

一般你会再一个数据集上训练,在另一个数据集上测试(或者“eval”)。一种完成这样需求的方式是拥有两个独立的进程

  • 悬链进程读取训练输入的数据,并周期性的将训练的变量写入中断点文件中。
  • 评估进程从中断点文件中还原出推断模型,并读取验证数据

这就是 the example CIFAR-10 model中所做的。
这样做有两个好处:

  • eval运行在训练变量的一个快照上
  • 你可以在训练完成并且推出以后运行eval

你可以使得训练和eval在同一个图的同一个进程中,并共享它们的训练变量。参见 the shared variables tutorial.

如果觉得本文对你有帮助的话,不妨扫码请笔者喝杯茶

TensorFlow官方教程翻译3:读取数据_第1张图片
image

你可能感兴趣的:(TensorFlow官方教程翻译3:读取数据)