多线程输入数据处理
TFRecord输入数据格式
TensorFlow提供一种统一的TFRecord来存储数据,TFRecord文件中的数据都是通过tf.train.Example Protocol Buffer的格式来存储。
message Example { Features features = 1; }; message Features { map<string, Features> features = 1; }; message Feature { oneof kind { BytesList bytes_list = 1; FloatList float_list = 2; Int64List int64_list = 3; } };
tf.train.Example中包含了一个从属性名称到取值的字典,属性的取值可以为字符串(BytesList),实数列表(FloatList)或者整数列表(Int64List)。当数据量大时可以将数据写入多个TFRecord文件。
多线程输入数据处理框架
TF提供tf.train.string_input_producer函数来有效管理原始输入文件列表。
1.队列与多线程
在TF中,队列和变量类似,都是计算图上有状态的节点,其他的计算节点可以修改它们的状态。对于变量,可以通过赋值操作修改变量的取值。对于队列,修改队列状态的操作主要有Enqueue、EnqueueMany和Dequeue。
如下展示如何使用这些函数操作一个队列。
# coding utf-8 import tensorflow as tf # 创建一个先进先出队列,指定队列中可以保存两个元素,并指定类型为整数。 q = tf.FIFOQueue(2, 'int32') # 使用enqueue_many函数来初始化队列中的元素。和变量初始化类似,在使用队列之前 # 需要明确调用这个初始化过程. init = q.enqueue_many(([0, 10],)) # 使用Dequeue函数将队列中的第一个元素出队列。这个元素值,将被存在变量x中。 x = q.dequeue() y = x + 1 # 将加1后的值在重新加入队列中。 q_inc = q.enqueue_many([y]) with tf.Session() as sess: # 运行初始化队列的操作。 init.run() for _ in range(5): # 运行q_inc将执行数据出队列、出队的元素+1,、重新加入队列的整个过程。 v, _ = sess.run([x, q_inc]) # 打印出队元素的值。 print(v)
TensofFlow中提供了FIFOQueue和RandomShuffleQueue两种队列。上面程序展示如何使用FIFOQueue实现一个先进先出队列。RandomShuffleQueue会 将队列中的元素打乱,每次出队列操作得到的是当前队列所有元素中随机选择的一个。
在TensorFlow中,队列不仅仅是一种数据结构,还是异步计算张量取值的一个重要机制。比如多个线程可以同时向一个队列中写元素,或者同时读取一个队列中的元素。
TensorFlow提供了tf.Coordinator和tf.QueueRunner两个类来完成多线程协同的功能。tf.Coordinator主要用于协同多个线程一起停止,并提供了should_stop、request_stop和join三个函数。在启动线程之前需要声明一个tf.Coordinator类,并将这个类传入每一个创建的线程中。启动的线程需要一直查询tf.Coordinatorl类中提供的should_stop函数,当这个函数的返回值为Truez时,则当前线程也需要退出。每一个启动的线程都可以通过调用request_stop函数来通知其他线程退出。当某一个线程调用request_stop函数之后,should_stop函数的返回值将被设置为True,这样其他线程就可以同时终止。
tf.Coordinator展示如下:
# coding utf-8 import tensorflow as tf import numpy as np import threading import time # 线程中运行的程序,这个程序每隔1秒判断是否需要停止并打印自己的ID。 def MyLoop(coord, worker_id): # 使用tf.Coordinator类提供的协同工具判断当前线程是否需要停止并打印自己的ID while not coord.should_stop(): # 随机停止所有线程 if np.random.rand() < 0.1: print('Stoping from id: %d\n' % worker_id) # 调用coord.request_stop()函数来通知其他线程停止 coord.request_stop() else: # 打印当前线程的ID print('Working on id: %d\n' % worker_id) # 暂停1秒 time.sleep(1) # 声明一个tf.train.Coordinator类来协同多个线程 coord = tf.train.Coordinator() # 声明创建5个线程 threads = [threading.Thread(target=MyLoop, args=(coord, i, )) for i in range(5)] # 启动所有的线程 for t in threads: t.start() # 等待所有线程退出 coord.join(threads)
运行结果如下:
当所有线程启动之后,每个线程会打印各自的ID,于是前面5行打印出了他们的ID。然后在暂停1秒后,所有线程又开始第二遍打印ID。这个时候有一个线程退出的条件达到,于
是调用coord.request_stop函数来停止所有其他线程。在打印Stoping from id: 3之后,其他线程仍然在输出,这是因为这些线程已经执行完coord.request_stop的判断,于是仍然会继续输出自己的ID,但在下一轮将退出线程。
tf.QueueRunner主要用于启动多个线程来操作同一个队列,启动的这些线程可以通过上面介绍的tf.Coordinator类来统一管理。以下代码展示如何使用tf.QueueRunner和tf.Coordinator来管理多线程队列操作。
import tensorflow as tf # 声明一个先进先出的队列,队列中最多100个元素,类型为实数 queue = tf .FIFOQueue(100, 'float') # 定义队列的入队操作 enqueue_op = queue.enqueue([tf.random_normal([1])]) # 使用 tf.train.QueueRunner来创建多个线程运行队列的入队操作 # tf.train.QueueRunner给出了被操作的队列,[enqueue_op] * 5 # 表示了需要启动5个线程,每个线程中运行的是enqueue_op操作 qr = tf.train.QueueRunner(queue, [enqueue_op] * 5) # 将定义过的QueueRunner加入TensorFlow计算图上指定的集合 # tf.train.add_queue_runner函数没有指定集合, # 则加入默认集合tf.GraphKeys.QUEUE_RUNNERS。 # 下面的函数就是将刚刚定义的qr加入默认的tf.GraphKeys.QUEUE_RUNNERS结合 tf.train.add_queue_runner(qr) # 定义出队操作 out_tensor = queue.dequeue() with tf.Session() as sess: # 使用tf.train.Coordinator来协同启动的线程 coord = tf.train.Coordinator() # 使用tf.train.QueueRunner时,需要明确调用tf.train.start_queue_runners # 来启动所有线程。否则因为没有线程运行入队操作,当调用出队操作时,程序一直等待 # 入队操作被运行。tf.train.start_queue_runners函数会默认启动 # tf.GraphKeys.QUEUE_RUNNERS中所有QueueRunner.因为这个函数只支持启动指定集合中的QueueRunner, # 所以一般来说tf.train.add_queue_runner函数和tf.train.start_queue_runners函数会指定同一个结合 threads = tf.train.start_queue_runners(sess=sess, coord=coord) # 获取队列中的取值 for _ in range(3): print(sess.run(out_tensor)[0]) # 使用tf.train.Coordinator来停止所有线程 coord.request_stop() coord.join(threads)
上面程序将启动5个线程来执行队列入队操作,其中每一个线程都是将随机数写入队列。于是在每次运行出队操作时,可以得到一个随机数。结果如下:
2.输入文件队列
TensorTlow提供了tf.train.match_filenames_once函数来获取符合一个正则表达式的所有文件,得到的文件列表可以通过tf.train.string_input_producer函数进行有效的管理。
tf.train.string_input_produce会使用初始化时提供的文件列表创建一个输入队列,输入队列中原始的元素为文件列表中的所有文件。创建好的输入队列可以作为文件读取函数的参数。每次调用文件读取函数时,该函数会先判断当前是否已有打开的文件可读,如果没有或者打开的文件读完,这个函数会从输入队列中出队一个文件并从这个文件中读取数据。
通过设置shuffle参数,tf.train.string_input_producer函数支持随机打乱文件列表中文件出队的顺序。当shuffle参数为True时,文件在加入队列之前会被打乱顺序,所以出队的顺序也是随机的。随机的俄打乱文件顺序以及加入输入队列的过程会跑在一个单独的线程上,这样不会影响获取文件的速度。tf.train.string_input_producer生成的输入队列可以同时被多个文件读取线程操作,而且输入队列会将队列中的文件均匀地分给不同的线程,不出现有些文件文件被处理过多次而有些文件还没有被处理过的情况。
当一个输入队列的文件都被处理完后,它会将初始化时提供的文件列表中的文件全部重新加入队列。tf.train.string_input_producer函数可以设置num_epochs参数来限制加载初始文件列表列表的最大轮数。在测试神经网络模型时,因为所有测试数据只需要使用一次,所以可以将num_epochs参数设置为1.这样计算完一轮之后程序将自动停止。如下为简单程序生成样例数据:
import tensorflow as tf # 创建TFRecord文件的帮助函数 def _int64_feature(value): return tf.train.Feature(int64_list=tf.train.Int64List(value=[value])) # 模拟海量数据情况下将数据写入不同的文件。num_shards定义了总共写入多少个文件 # instances_per_shard定义了每个文件有多少个数据 num_sards = 2 instances_per_shard = 2 for i in range(num_sards): # 将数据分为多个文件时,可以将不同文件以类似0000n-of-0000m的后缀区分。其中m表示 # 数据总共被存在了多少个文件,n表示当前文件的编号。式样的方式既方便了通过正则表达式 # 获取文件列表,又在文件名中加入更多的信息。 filename = ('/path/to/data.tfrecords-%.5d-of-%.5d' % (i, num_sards)) writer = tf.python_io.TFRecordWriter(filename) # 将数据封装成Example结构并写入TFRecord文件 for j in range(instances_per_shard): # Example结构仅包含当前样例属于第几个文件以及是当前文件的第几个样本 example = tf.train.Example(features=tf.train.Features(feature={ 'i':_int64_feature(i) 'j':_int64_feature(j) })) writer.write(example.SerializeToString()) writer.close()
程序运行之后会在指定的目录下降生成两个文件:/path/to/data.tfrecords-00000-of-00002和/path/to/data.tfrecords-00001-of-00002。每一个文件中存储了两个样例。在生成了样例数据之后,以下代码展示了tf.train.match_filenames_one函数和tf.train.string_input_producer函数的使用方法:
# coding:utf-8 import tensorflow as tf # 使用tf.train.match_filenames_once函数获取文件列表 files = tf.train.match_filenames_once('dada.tfrecords-*') # 通过tf.train.string_input_producer函数创建输入队列,输入队列中的文件列表为 # tf.train.match_filenames_once函数获取的文件列表。这里将shuffle参数设为False\ # 来避免随机打乱读文件的顺序。但一般在解决真实问题时,会将shuffle参数设置为True filename_queue = tf.train.string_input_producer(files, shuffle=False) # 读取并解析一个样本 reader = tf.TFRecordReader() _, serialized_example = reader.read(filename_queue) features = tf.parse_single_example( serialized_example, features={ 'i':tf.FixedLenFeature([], tf.int64), 'j':tf.FixedLenFeature([], tf.int64), }) with tf.Session() as sess: # 虽然在本段程序中没有声明任何变量,但是使用tf.train.match_filenames_once函数时需 # 要初始化一些变量 tf.initialize_all_variables().run() print(sess.run(files)) # 声明tf.train.Coordinator类来协同不同线程,并启动线程 coord = tf.train.Coordinator() threads = tf.train.start_queue_runners(sess=sess, coord=coord) # 多次执行获取数据的操作 for i in range(6): print(sess.run([features['i'], features['j']])) coord.request_stop() coord.join(threads)
3.组合训练数据
TensorFlow提供了tf.train.batch和tf.train.shuffle_batch函数将单个的样例组成batch的形式输出。这两个函数都会生成一个队列,队列的入队操作是生成单个样例的方法,而每次出队得到的是一个batch的样例,它们唯一区别在于是否会将数据顺序打乱,以下代码展示两个函数使用方法。
# coding:utf-8 import tensorflow as tf # 这里Example结构表示一个样例的 # 特征向量,比如一张图像的像素矩阵,而j表示该样例对应的标签 example, label = features['i'], features['j'] # 一个batch中样例的个数 batch_size = 3 # 组合样例的队列中最多可以存储的样例个数。这个队列如果太大,需要占用很多内存资源: # 如果太小,出队操作可能会因为没有数据而被阻碍(block),从而导致训练效率低。一般 # 这个队列的大小会和每一个batch的大小相关,如下给出设置队列大小的一种方式 capacity = 1000 + 3 * batch_size # 使用tf.train.batch函数来组合样例。[example, label]给出了需要组合的元素, # 一般example和label分别代表训练样本和这个样本对应的正确标签,batch_size参数给出 # 了每个batch中的样例的个数。capacity给出队列的最大容量,当队列长度等于容量时, # TensorFlow将暂停入队操作,而只是等待元素出队。当元素个数小于容量时,TensorFlow # 将自动重新启动入队操作。 example_batch, label_batch = tf.train.batch([example, label], batch_size=batch_size, capacity=capacity) with tf.Session() as sess: tf.initialize_all_variables().run() coord = tf.train.Coordinator() threads = tf.train.start_queue_runners(sess=sess, coord=coord) # 获取并打印组合之后的样例。在真实问题中,这个输出一般会作为神经网络的输入 for i in range(2): cur_example_batch, cur_label_batch = sess.run([example_batch, label_batch]) print(cur_example_batch, label_batch) coord.request_stop() coord.join(threads)
以下代码展示tf.train.shuffle_batch函数的使用方法。
# 和tf.train.batch的样例代码一样产生example和label example, label = features['i'], features['j'] # 使用tf.train,shuffle_batch函数来组合样例。tf.train.shuffle_batch函数 # 的参数大部分都和tf.train.batch函数相似,但是min_after_dequeue参数是 # tf.train.shuffle_batch函数特有的。min_after_dequeue参数限制了出队时队列中 # 元素的最少个数。当队列中元素太少时,随机打乱样侧顺序的作用就不大。所以 # tf.train.shuffle_batch函数提供了限制出队时最少元素的个数来保证随机打乱顺序的 # 作用。当出队函数被调用但是出队中元素不够时,出队操作将等待更多的元素入队才会完成 # 如果min_after_dequeue参数被设定,capacity也应该相应调整来满足性能需求 example_batch, label_batch = tf.train.shuffle_batch( [example, label], batch_size=batch_size, capacity=capacity, min_after_dequeue=30 )
tf.train.batch函数和tf.train.shuffle_batch函数除了可以将单个训练数据整理成输入batch,也提供了并行化处理输入数据的方法。tf.train.batch函数和tf.train.shuffle_batch函数并行化的方式一致。通过设置tf.train.shuffle_batch函数中的num_threads参数,可以指定多个线程同时执行入队操作。tf.train.shuffle_batch函数的入队操作就是数据读取以及预处理的过程。当num_threads参数大于1时,多个线程会同时读取一个文件中的不同样例并进行预处理。如果需要多个线程处理不同文件中的样例时,可以使用tf.train.shuffle_batch_join函数。此函数会从输入文件队列中获取不同的文件分配给不同的线程。一般输入文件队列是通过tf.train.string_input_producer函数生成的,该函数会平均分配文件以保证不同文件中的数据会被尽量平均地使用。
对比tf.train.shuffle_batch函数与tf.train.shuffle_batch_join函数:
相同点:两个函数都可以完成多线程并行的方式来进行数据预处理
不同点:
(1)tf.train.shuffle_batch函数不同线程会读取,同一个文件,如果一个样例比较相似(比如都属于同一个类别),那么神经网络的训练效果有可能会受到影响。所以在使用tf.train.shuffle_batch函数时,需要将同一个TFRecord文件中的样例随机打乱。
(2)tf.train.shuffle_batch_join函数,线程会读取不同文件。如果读取数据的线程数比总文件数还大,那么多个线程可能会读取同一个文件中相近部分的数据。而且多个线程读取多个文件可能导致过多的硬盘寻址,从而使得读取效率降低。