TensorFlow开发实践经验(一):优化数据获取的效率
本文整理了TensorFlow中的数据读取方法,在TensorFlow中主要有三种方法读取数据:
1. 供给数据(Feeding):在TensorFlow程序运行的每一步, 让Python代码来供给数据。
2. 预加载数据(Preloaded data):在TensorFlow图中定义常量或变量来保存所有数据(仅适用于数据量比较小的情况)。
3. 从文件读取数据(Reading from files):在TensorFlow图的起始, 让一个输入管线从文件中读取数据。
对于数据量较小而言,可能一般选择直接将数据加载进内存,然后再分batch
输入网络进行训练(tip:使用这种方法时,结合yield
使用更为简洁,大家自己尝试一下吧,我就不赘述了)。但是,如果数据量较大,这样的方法就不适用了,因为太耗内存,所以这时最好使用tensorflow提供的队列queue
,也就是第3种方法 从文件读取数据。
供给数据(Feeding)
我们一般用tf.placeholder
节点来feed
数据,该节点不需要初始化也不包含任何数据,我们在执行run()
或者eval()
指令时通过feed_dict
参数把数据传入graph
中来计算。如果在运行过程中没有对tf.placeholder
节点传入数据,程序会报错。例如:
import tensorflow as tf # 设计Graph x1 = tf.placeholder(tf.int16) x2 = tf.placeholder(tf.int16) y = tf.add(x1, x2) # 用Python产生数据 li1 = [2, 3, 4] li2 = [4, 0, 1] # 打开一个session --> 喂数据 --> 计算y with tf.Session() as sess: print sess.run(y, feed_dict={x1: li1, x2: li2})
预加载数据(Preloaded data)
预加载数据方法仅限于用在可以完全加载到内存中的小数据集上,主要有两种方法:
- 把数据存在常量(constant)中。
- 把数据存在变量(variable)中,我们初始化并且永不改变它的值。
用常量更简单些,但会占用更多的内存,因为常量存储在graph
数据结构内部。例如:
training_data = ... training_labels = ... with tf.Session(): input_data = tf.constant(training_data) input_labels = tf.constant(training_labels) ...
如果用变量的话,我们需要在graph
构建好之后初始化该变量。例如:
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.VARIABLES
把它收集后做为保存和恢复的中断点。
无论哪种方式,我们可以用tf.train.slice_input_producer
函数每次产生一个切片。这样就会让样本在整个迭代中被打乱,所以在使用批处理的时候不需要再次打乱样本。所以我们不使用shuffle_batch
函数,取而代之的是纯tf.train.batch
函数。 如果要使用多个线程进行预处理,需要将num_threads
参数设置为大于1的数字。
从文件读取数据(Reading from files)
从文件中读取数据一般包含以下步骤:
- 文件名列表
- 文件名随机排序(可选的)
- 迭代控制(可选的)
- 文件名队列
- 针对输入文件格式的阅读器
- 记录解析器
- 预处理器(可选的)
- 样本队列
TFRecords文件
TFRecords其实是一种二进制文件,虽然它不如其他格式好理解,但是它能更好的利用内存,更方便复制和移动,并且不需要单独的标签文件。总而言之,这样的文件格式好处多多。
TFRecords文件包含了tf.train.Example
协议内存块(protocol buffer)(协议内存块包含了字段 Features
)。我们可以写一段代码获取你的数据, 将数据填入到Example
协议内存块(protocol buffer),将协议内存块序列化为一个字符串, 并且通过tf.python_io.TFRecordWriter
写入到TFRecords文件。
从TFRecords文件中读取数据, 可以使用tf.TFRecordReader
的tf.parse_single_example
解析器。这个操作可以将Example
协议内存块(protocol buffer)解析为张量。
生成TFRecords文件
我们使用tf.train.Example
来定义我们要填入的数据格式,然后使用tf.python_io.TFRecordWriter
来写入。
import os
import tensorflow as tf
from PIL import Image
cwd = os.getcwd()
'''
此处我加载的数据目录如下:
0 -- img1.jpg
img2.jpg
img3.jpg
...
1 -- img1.jpg
img2.jpg
...
2 -- ...
这里的0, 1, 2...就是类别,也就是下文中的classes
classes是我根据自己数据类型定义的一个列表,大家可以根据自己的数据情况灵活运用
...
'''
writer = tf.python_io.TFRecordWriter("train.tfrecords")
for index, name in enumerate(classes):
class_path = cwd + name + "/"
for img_name in os.listdir(class_path):
img_path = class_path + img_name
img = Image.open(img_path)
img = img.resize((224, 224))
img_raw = img.tobytes() #将图片转化为原生bytes
example = tf.train.Example(features=tf.train.Features(feature={
"label": tf.train.Feature(int64_list=tf.train.Int64List(value=[index])),
'img_raw': tf.train.Feature(bytes_list=tf.train.BytesList(value=[img_raw]))
}))
writer.write(example.SerializeToString()) #序列化为字符串
writer.close()
关于Example
Feature
的相关定义和详细内容,我推荐去官网查看相关API。
基本的,一个Example
中包含Features
,Features
里包含Feature
(这里没s)的字典。最后,Feature
里包含有一个 FloatList
, 或者ByteList
,或者Int64List
就这样,我们把相关的信息都存到了一个文件中,所以前面才说不用单独的label文件。而且读取也很方便。
接下来是一个简单的读取小例子:
for serialized_example in tf.python_io.tf_record_iterator("train.tfrecords"):
example = tf.train.Example()
example.ParseFromString(serialized_example)
image = example.features.feature['image'].bytes_list.value
label = example.features.feature['label'].int64_list.value
# 可以做一些预处理之类的
print image, label
使用队列读取
一旦生成了TFRecords文件,为了高效地读取数据,TF中使用队列(queue
)读取数据。
def read_and_decode(filename): #根据文件名生成一个队列 filename_queue = tf.train.string_input_producer([filename]) reader = tf.TFRecordReader() _, serialized_example = reader.read(filename_queue) #返回文件名和文件 features = tf.parse_single_example(serialized_example, features={ 'label': tf.FixedLenFeature([], tf.int64), 'img_raw' : tf.FixedLenFeature([], tf.string), }) img = tf.decode_raw(features['img_raw'], tf.uint8) img = tf.reshape(img, [224, 224, 3]) img = tf.cast(img, tf.float32) * (1. / 255) - 0.5 label = tf.cast(features['label'], tf.int32) return img, label
之后我们可以在训练的时候这样使用
img, label = read_and_decode("train.tfrecords") #使用shuffle_batch可以随机打乱输入 img_batch, label_batch = tf.train.shuffle_batch([img, label], batch_size=30, capacity=2000, min_after_dequeue=1000) init = tf.initialize_all_variables() with tf.Session() as sess: sess.run(init) threads = tf.train.start_queue_runners(sess=sess) for i in range(3): val, l= sess.run([img_batch, label_batch]) #我们也可以根据需要对val, l进行处理 #l = to_categorical(l, 12) print(val.shape, l)
几个注意事项:
第一,tensorflow里的graph能够记住状态(state
),这使得TFRecordReader
能够记住tfrecord
的位置,并且始终能返回下一个。而这就要求我们在使用之前,必须初始化整个graph,这里我们使用了函数tf.initialize_all_variables()
来进行初始化。
第二,tensorflow中的队列和普通的队列差不多,不过它里面的operation
和tensor
都是符号型的(symbolic
),在调用sess.run()
时才执行。
第三, TFRecordReader
会一直弹出队列中文件的名字,直到队列为空。
第四,在我们使用tf.train.string_input_producer创建文件名队列后,整个系统其实还是处于“停滞状态”的,也就是说,我们文件名并没有真正被加入到队列中。此时如果我们开始计算,因为内存队列中什么也没有,计算单元就会一直等待,导致整个系统被阻塞。而使用tf.train.start_queue_runners之后,才会启动填充队列的线程,这时系统就不再“停滞”。此后计算单元就可以拿到数据并进行计算,整个程序也就跑起来了,这就是函数tf.train.start_queue_runners的用处。
文件名、随机排序和迭代控制
我们首先要有个文件名列表,为了产生文件名列表,我们可以手动用Python输入字符串,例如:
["file0", "file1"]
[("file%d" % i) for i in range(2)]
[("file%d" % i) for i in range(2)]
我们也可以用tf.train.match_filenames_once
函数来生成文件名列表。
有了文件名列表后,我们需要把它送入tf.train.string_input_producer
函数中生成一个先入先出的文件名队列,文件阅读器需要从该队列中读取文件名。
string_input_producer( string_tensor, num_epochs=None, shuffle=True, seed=None, capacity=32, shared_name=None, name=None, cancel_op=None )
这个QueueRunner
的工作线程独立于文件阅读器的线程, 因此随机排序和将文件名送入到文件名队列这些过程不会阻碍文件阅读器的运行。一个QueueRunner
每次会把每批次的所有文件名送入队列中,可以通过设置string_input_producer
函数的shuffle
参数来对文件名随机排序,或者通过设置num_epochs
来决定对string_tensor
里的文件使用多少次,类型为整型,如果想要迭代控制则需要设置了num_epochs
参数,同时需要添加tf.local_variables_initializer()
进行初始化,如果不初始化会报错。
文件格式
根据不同的文件格式, 应该选择对应的文件阅读器, 然后将文件名队列提供给阅读器的read
方法。阅读器每次从队列中读取一个文件,它的read
方法会输出一个key
来表征读入的文件和其中的纪录(对于调试非常有用),同时得到一个字符串标量, 这个字符串标量可以被一个或多个解析器,或者转换操作将其解码为张量并且构造成为样本。
根据不同的文件类型,有三种不同的文件阅读器:
tf.TextLineReader
tf.FixedLengthRecordReader
tf.TFRecordReader
它们分别用于单行读取(如CSV文件)、固定长度读取(如CIFAR-10的.bin二进制文件)、TensorFlow标准格式读取。
根据不同的文件阅读器,有三种不同的解析器,它们分别对应上面三种阅读器:
tf.decode_csv
tf.decode_raw
tf.parse_single_example
和tf.parse_example
CSV文件
当我们读入CSV格式的文件时,我们可以使用tf.TextLineReader
阅读器和tf.decode_csv
解析器。例如:
filename_queue = tf.train.string_input_producer(["file0.csv", "file1.csv"]) # 创建一个Filename Queue # 该例csv文件中共有5列数据,前四列为features,最后一列为label 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) #启动QueueRunner, 此时文件名队列已经进队。 for i in range(1200): # Retrieve a single instance: example, label = sess.run([features, col5]) coord.request_stop() coord.join(threads)
record_defaults = [[1], [1], [1], [1], [1]]
代表了解析的摸版,默认用,
隔开,是用于指定矩阵格式以及数据类型的,CSV文件中的矩阵是NXM的,则此处为1XM,例如上例中M=5。[1]表示解析为整型,如果矩阵中有小数,则应为float型,[1]应该变为[1.0],[‘null’]解析为string类型。每次read
的执行都会从文件中读取一行内容, decode_csv
操作会解析这一行内容并将其转为张量列表。在调用run
或者eval
去执行read
之前, 必须先调用tf.train.start_queue_runners
来将文件名填充到队列。否则read
操作会被阻塞到文件名队列中有值为止。
col1, col2, col3, col4, col5 = tf.decode_csv(value, record_defaults = record_defaults)
, 矩阵中有几列,这里就要写几个参数,比如5列,就要写到col5,不管你到底用多少。否则报错。
固定长度记录
我们也可以从二进制文件(.bin)中读取固定长度的数据,使用的是tf.FixedLengthRecordReader
阅读器和tf.decode_raw
解析器。decode_raw
节点会把string
转化为uint8
类型的张量。
例如CIFAR-10数据集就采用的固定长度的数据,1字节的标签,后面跟着3072字节的图像数据。使用uint8
类型张量的标准操作可以把每个图像的片段截取下来并且按照需要重组。下面有一个例子:
reader = tf.FixedLengthRecordReader(record_bytes = record_bytes) key, value = reader.read(filename_queue) record_bytes = tf.decode_raw(value, tf.uint8) label = tf.cast(tf.slice(record_bytes, [0], [label_bytes]), tf.int32) image_raw = tf.slice(record_bytes, [label_bytes], [image_bytes]) image_raw = tf.reshape(image_raw, [depth, height, width]) image = tf.transpose(image_raw, (1,2,0)) # 图像形状为[height, width, channels] image = tf.cast(image, tf.float32)
这里介绍上述代码中出现的函数:tf.slice()
slice( input_, begin, size, name=None )
从一个张量input
中提取出长度为size
的一部分,提取的起点由begin
定义。size
是一个向量,它代表着在每个维度提取出的tensor
的大小。begin
表示提取的位置,它表示的是input
的起点偏离值,也就是从每个维度第几个值开始提取。
begin
从0开始,size
从1开始,如果size[i]
的值为-1,则第i个维度从begin
处到余下的所有值都被提取出来。
例如:
# 'input' is [[[1, 1, 1], [2, 2, 2]], # [[3, 3, 3], [4, 4, 4]], # [[5, 5, 5], [6, 6, 6]]] tf.slice(input, [1, 0, 0], [1, 1, 3]) ==> [[[3, 3, 3]]] tf.slice(input, [1, 0, 0], [1, 2, 3]) ==> [[[3, 3, 3], [4, 4, 4]]] tf.slice(input, [1, 0, 0], [2, 1, 3]) ==> [[[3, 3, 3]], [[5, 5, 5]]]
标准TensorFlow格式
我们也可以把任意的数据转换为TensorFlow所支持的格式, 这种方法使TensorFlow的数据集更容易与网络应用架构相匹配。这种方法就是使用TFRecords文件,TFRecords文件包含了tf.train.Example
的protocol buffer(里面包含了名为 Features
的字段)。你可以写一段代码获取你的数据, 将数据填入到Example
的protocol buffer,将protocol buffer序列化为一个字符串, 并且通过tf.python_io.TFRecordWriter
类写入到TFRecords文件。
从TFRecords文件中读取数据, 可以使用tf.TFRecordReader
阅读器以及tf.parse_single_example
解析器。parse_single_example
操作可以将Example
protocol buffer解析为张量。 具体可以参考如下例子,把MNIST数据集转化为TFRecords格式:
SparseTensors这种稀疏输入数据类型使用队列来处理不是太好。如果要使用SparseTensors你就必须在批处理之后使用tf.parse_example
去解析字符串记录 (而不是在批处理之前使用tf.parse_single_example
) 。
预处理
我们可以对输入的样本数据进行任意的预处理, 这些预处理不依赖于训练参数, 比如数据归一化, 提取随机数据片,增加噪声或失真等等。具体可以参考如下对CIFAR-10处理的例子:
批处理
经过了之前的步骤,在数据读取流程的最后, 我们需要有另一个队列来批量执行输入样本的训练,评估或者推断。根据要不要打乱顺序,我们常用的有两个函数:
tf.train.batch()
tf.train.shuffle_batch()
下面来分别介绍:
tf.train.batch()
tf.train.batch( tensors, batch_size, num_threads=1, capacity=32, enqueue_many=False, shapes=None, dynamic_pad=False, allow_smaller_final_batch=False, shared_name=None, name=None )
该函数将会使用一个队列,函数读取一定数量的tensors
送入队列,然后每次从中选取batch_size
个tensors
组成一个新的tensors
返回出来。
capacity
参数决定了队列的长度。
num_threads
决定了有多少个线程进行入队操作,如果设置的超过一个线程,它们将从不同文件不同位置同时读取,可以更加充分的混合训练样本。
如果enqueue_many
参数为False
,则输入参数tensors
为一个形状为[x, y, z]
的张量,输出为一个形状为[batch_size, x, y, z]
的张量。如果enqueue_many
参数为True
,则输入参数tensors
为一个形状为[*, x, y, z]
的张量,其中所有*
的数值相同,输出为一个形状为[batch_size, x, y, z]
的张量。
当allow_smaller_final_batch
为True
时,如果队列中的张量数量不足batch_size
,将会返回小于batch_size
长度的张量,如果为False
,剩下的张量会被丢弃。
tf.train.shuffle_batch()
tf.train.shuffle_batch( tensors, batch_size, capacity, min_after_dequeue, num_threads=1, seed=None, enqueue_many=False, shapes=None, allow_smaller_final_batch=False, shared_name=None, name=None )
该函数类似于上面的tf.train.batch()
,同样创建一个队列,主要区别是会首先把队列中的张量进行乱序处理,然后再选取其中的batch_size
个张量组成一个新的张量返回。但是新增加了几个参数。
capacity
参数依然为队列的长度,建议capacity
的取值如下:
min_after_dequeue + (num_threads + a small safety margin) * batch_size
min_after_dequeue
这个参数的意思是队列中,做dequeue(取数据)的操作后,线程要保证队列中至少剩下min_after_dequeue
个数据。如果min_after_dequeue
设置的过少,则即使shuffle
为True
,也达不到好的混合效果。
假设你有一个队列,现在里面有m个数据,你想要每次随机从队列中取n个数据,则代表先混合了m个数据,再从中取走n个。
当第一次取走n个后,队列就变为m-n个数据;当你下次再想要取n个时,假设队列在此期间入队进来了k个数据,则现在的队列中有(m-n+k)个数据,则此时会从混合的(m-n+k)个数据中随机取走n个。
如果队列填充的速度比较慢,k就比较小,那你取出来的n个数据只是与周围很小的一部分(m-n+k)个数据进行了混合。
因为我们的目的肯定是想尽最大可能的混合数据,因此设置
min_after_dequeue
,可以保证每次dequeue后都有足够量的数据填充尽队列,保证下次dequeue时可以很充分的混合数据。但是
min_after_dequeue
也不能设置的太大,这样会导致队列填充的时间变长,尤其是在最初的装载阶段,会花费比较长的时间。
其他参数和tf.train.batch()
相同。
这里我们使用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 越大意味着随机效果越好但是也会占用更多的时间和内存 # capacity 必须比 min_after_dequeue 大 # 建议capacity的取值如下: # 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
class cifar10_data(object): def __init__(self, filename_queue): self.height = 32 self.width = 32 self.depth = 3 self.label_bytes = 1 self.image_bytes = self.height * self.width * self.depth self.record_bytes = self.label_bytes + self.image_bytes self.label, self.image = self.read_cifar10(filename_queue) def read_cifar10(self, filename_queue): reader = tf.FixedLengthRecordReader(record_bytes = self.record_bytes) key, value = reader.read(filename_queue) record_bytes = tf.decode_raw(value, tf.uint8) label = tf.cast(tf.slice(record_bytes, [0], [self.label_bytes]), tf.int32) image_raw = tf.slice(record_bytes, [self.label_bytes], [self.image_bytes]) image_raw = tf.reshape(image_raw, [self.depth, self.height, self.width]) image = tf.transpose(image_raw, (1,2,0)) image = tf.cast(image, tf.float32) return label, image def inputs(data_dir, batch_size, train = True, name = 'input'): with tf.name_scope(name): if train: filenames = [os.path.join(data_dir,'data_batch_%d.bin' % ii) for ii in range(1,6)] for f in filenames: if not tf.gfile.Exists(f): raise ValueError('Failed to find file: ' + f) filename_queue = tf.train.string_input_producer(filenames) read_input = cifar10_data(filename_queue) images = read_input.image images = tf.image.per_image_standardization(images) labels = read_input.label image, label = tf.train.shuffle_batch( [images,labels], batch_size = batch_size, min_after_dequeue = 20000, capacity = 20192) return image, tf.reshape(label, [batch_size]) else: filenames = [os.path.join(data_dir,'test_batch.bin')] for f in filenames: if not tf.gfile.Exists(f): raise ValueError('Failed to find file: ' + f) filename_queue = tf.train.string_input_producer(filenames) read_input = cifar10_data(filename_queue) images = read_input.image images = tf.image.per_image_standardization(images) labels = read_input.label image, label = tf.train.shuffle_batch( [images,labels], batch_size = batch_size, min_after_dequeue = 20000, capacity = 20192) return image, tf.reshape(label, [batch_size])
这里介绍下函数tf.image.per_image_standardization(image)
,该函数对图像进行线性变换使它具有零均值和单位方差,即规范化。其中参数image
是一个3-D的张量,形状为[height, width, channels]
。一个具体的例子如下,该例采用了CIFAR-10数据集,采用了固定长度读取的tf.FixedLengthRecordReader
阅读器和tf.decode_raw
解析器,同时进行了数据预处理操作中的标准化操作,最后使用tf.train.shuffle_batch
函数批量执行数据的乱序处理。
多个样本和多个阅读器
下面讲分别展示三个不同Reader数目和不同样本数的代码示例:
文件准备
$ echo -e "Alpha1,A1\nAlpha2,A2\nAlpha3,A3" > A.csv $ echo -e "Bee1,B1\nBee2,B2\nBee3,B3" > B.csv $ echo -e "Sea1,C1\nSea2,C2\nSea3,C3" > C.csv $ cat A.csv Alpha1,A1 Alpha2,A2 Alpha3,A3
单个Reader,单个样本
import tensorflow as tf # 生成一个先入先出队列和一个QueueRunner filenames = ['A.csv', 'B.csv', 'C.csv'] filename_queue = tf.train.string_input_producer(filenames, shuffle=False) # 定义Reader reader = tf.TextLineReader() key, value = reader.read(filename_queue) # 定义Decoder example, label = tf.decode_csv(value, record_defaults=[['null'], ['null']]) # 运行Graph with tf.Session() as sess: coord = tf.train.Coordinator() #创建一个协调器,管理线程 threads = tf.train.start_queue_runners(coord=coord) #启动QueueRunner, 此时文件名队列已经进队。 for i in range(10): print example.eval() #取样本的时候,一个Reader先从文件名队列中取出文件名,读出数据,Decoder解析后进入样本队列。 coord.request_stop() coord.join(threads) # outpt Alpha1 Alpha2 Alpha3 Bee1 Bee2 Bee3 Sea1 Sea2 Sea3 Alpha1
单个Reader,多个样本
import tensorflow as tf filenames = ['A.csv', 'B.csv', 'C.csv'] filename_queue = tf.train.string_input_producer(filenames, shuffle=False) reader = tf.TextLineReader() key, value = reader.read(filename_queue) example, label = tf.decode_csv(value, record_defaults=[['null'], ['null']]) # 使用tf.train.batch()会多加了一个样本队列和一个QueueRunner。Decoder解后数据会进入这个队列,再批量出队。 # 虽然这里只有一个Reader,但可以设置多线程,通过在tf.train.batch()中添加“num_threads=",相应增加线程数会提高读取速度,但并不是线程越多越好。 example_batch, label_batch = tf.train.batch( [example, label], batch_size=5) with tf.Session() as sess: coord = tf.train.Coordinator() threads = tf.train.start_queue_runners(coord=coord) for i in range(10): print example_batch.eval() coord.request_stop() coord.join(threads) # output # ['Alpha1' 'Alpha2' 'Alpha3' 'Bee1' 'Bee2'] # ['Bee3' 'Sea1' 'Sea2' 'Sea3' 'Alpha1'] # ['Alpha2' 'Alpha3' 'Bee1' 'Bee2' 'Bee3'] # ['Sea1' 'Sea2' 'Sea3' 'Alpha1' 'Alpha2'] # ['Alpha3' 'Bee1' 'Bee2' 'Bee3' 'Sea1'] # ['Sea2' 'Sea3' 'Alpha1' 'Alpha2' 'Alpha3'] # ['Bee1' 'Bee2' 'Bee3' 'Sea1' 'Sea2'] # ['Sea3' 'Alpha1' 'Alpha2' 'Alpha3' 'Bee1'] # ['Bee2' 'Bee3' 'Sea1' 'Sea2' 'Sea3'] # ['Alpha1' 'Alpha2' 'Alpha3' 'Bee1' 'Bee2']
多个Reader,多个样本
import tensorflow as tf filenames = ['A.csv', 'B.csv', 'C.csv'] filename_queue = tf.train.string_input_producer(filenames, shuffle=False) reader = tf.TextLineReader() key, value = reader.read(filename_queue) record_defaults = [['null'], ['null']] example_list = [tf.decode_csv(value, record_defaults=record_defaults) for _ in range(2)] # Reader设置为2 # 使用tf.train.batch_join(),可以使用多个reader,并行读取数据。每个Reader使用一个线程。 example_batch, label_batch = tf.train.batch_join( example_list, batch_size=5) with tf.Session() as sess: coord = tf.train.Coordinator() threads = tf.train.start_queue_runners(coord=coord) for i in range(10): print example_batch.eval() coord.request_stop() coord.join(threads) # output # ['Alpha1' 'Alpha2' 'Alpha3' 'Bee1' 'Bee2'] # ['Bee3' 'Sea1' 'Sea2' 'Sea3' 'Alpha1'] # ['Alpha2' 'Alpha3' 'Bee1' 'Bee2' 'Bee3'] # ['Sea1' 'Sea2' 'Sea3' 'Alpha1' 'Alpha2'] # ['Alpha3' 'Bee1' 'Bee2' 'Bee3' 'Sea1'] # ['Sea2' 'Sea3' 'Alpha1' 'Alpha2' 'Alpha3'] # ['Bee1' 'Bee2' 'Bee3' 'Sea1' 'Sea2'] # ['Sea3' 'Alpha1' 'Alpha2' 'Alpha3' 'Bee1'] # ['Bee2' 'Bee3' 'Sea1' 'Sea2' 'Sea3'] # ['Alpha1' 'Alpha2' 'Alpha3' 'Bee1' 'Bee2']
注意
tf.train.batch
与tf.train.shuffle_batch
函数是单个Reader读取,但是可以多线程,通过设置num_threads
参数来设置多线程。tf.train.batch_join
与tf.train.shuffle_batch_join
可设置多Reader读取,每个Reader使用一个线程。至于两种方法的效率,单Reader时,2个线程就达到了速度的极限。多Reader时,2个Reader就达到了极限。所以并不是线程越多越快,甚至更多的线程反而会使效率下降。
上述两种方法,前者相比于后者的好处是:
- 避免了两个不同的线程从同一个文件中读取同一个样本。
- 避免了过多的磁盘搜索操作。
那么具体需要多少个读取线程呢? 函数tf.train.shuffle_batch*
为graph
提供了获取文件名队列中的元素个数之和的方法。 如果你有足够多的读取线程, 文件名队列中的元素个数之和应该一直是一个略高于0的数。具体可以参考TensorBoard的教程。
创建线程并使用QueueRunner对象来获取
我们要添加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()
如上图所示,每个QueueRunner
负责一个阶段,处理那些需要在线程中运行的入队操作的列表。一旦数据流图构造成功,tf.train.start_queue_runners
函数就会要求数据流图中每个QueueRunner
去开始它的线程运行入队操作。
如果一切顺利的话,我们可以执行训练步骤,同时队列也会被后台线程来填充。如果设置了最大训练迭代数,在某些时候,样本出队的操作可能会得到一个tf.OutOfRangeError
的错误。这其实是TensorFlow的“文件结束”(EOF)——这就意味着已经达到了最大训练迭代数,已经没有更多可用的样本了。
最后一个因素是Coordinator
。这是负责在收到任何关闭信号的时候,让所有的线程都知道。最常见的情况是在发生异常时,比如说其中一个线程在运行某些操作时出现错误(或一个普通的Python异常)。
疑问:在达到最大训练迭代数的时候如何关闭线程?
想象一下,我们有一个模型并且设置了最大训练迭代数。这意味着,生成文件的那个线程只会在产生OutOfRange
错误之前运行。QueueRunner
会捕获该错误,并且关闭文件名的队列,最后退出线程。关闭队列做了两件事情:
- 如果试着对文件名队列执行入队操作将发生错误。
- 当前或将来的出队操作要么成功(如果队列中还有足够的元素)或立即失败(发生
OutOfRange
错误)。它们不会等待更多的元素被添加到队列中,因为上面的一点已经保证了这种情况不会发生。
关键是,当在文件名队列被关闭时候,有可能还有许多文件名在该队列中,这样下一阶段的流水线(包括reader和其它预处理)还可以继续运行一段时间。 一旦文件名队列空了之后,如果后面的流水线还要尝试从文件名队列中取出一个文件名,这将会触发OutOfRange
错误。在这种情况下,即使你可能有一个QueueRunner
关联着多个线程,如果该出错线程不是QueueRunner
中最后的那个线程,那么OutOfRange
错误只会使得这一个线程退出。而其他那些正处理自己的最后一个文件的线程继续运行,直至他们完成为止。(但如果你使用的是tf.train.Coordinator
来管理所有的线程,那么其他类型的错误将导致所有线程停止)。一旦所有的reader线程触发OutOfRange
错误,样本队列才会被关闭。
同样,样本队列中会有一些已经入队的元素,所以样本训练将一直持续直到样本队列中再没有样本为止。如果样本队列是一个RandomShuffleQueue
,因为你使用了shuffle_batch
或者 shuffle_batch_join
,所以通常不会出现以往那种队列中的元素会比min_after_dequeue
定义的更少的情况。 然而,一旦该队列被关闭,min_after_dequeue
设置的限定值将失效,最终队列将为空。在这一点来说,当实际训练线程尝试从样本队列中取出数据时,将会触发OutOfRange
错误,然后训练线程会退出。一旦所有的训练线程完成,tf.train.Coordinator.join
会返回,你就可以正常退出了。
参考
- Reading data | TensorFlow
- Custom Data Readers | TensorFlow
- TF Boys (TensorFlow Boys ) 养成记(二): TensorFlow 数据读取 - Charles-Wan - 博客园
- tf.train.string_input_producer | TensorFlow
- tf.train.batch | TensorFlow
- tf.train.shuffle_batch | TensorFlow
- honggang.io
- 数据读取 - TensorFlow 官方文档中文版 - 极客学院Wiki
- TensorFlow官网教程Convolutional Neural Networks 难点详解 - 玛莎鱼的博客 - CSDN博客