队列(queue
)是TensorFlow中的重要组成部件。所有队列管理器被默认加入图的tf.GraphKeys.QUEUE_RUNNERS
集合中。
在读入数据的这个例子中,可以在构建完图之后,打印出图中队列的相关信息:
queue_runners = tf.get_collection(tf.GraphKeys.QUEUE_RUNNERS)
for qr in queue_runners:
print(type(qr.queue))
print(qr.queue.name)
for opt in qr.enqueue_ops:
print(type(opt))
print(opt.name)
这个例子中,出现了两个QueueRunner
:第一个由tf.train.string_input_produecer
函数创建,负责管理数据文件名;第二个由tf.train.shuffle_batch
函数创建,负责管理样本。
每个队列管理器拥有一个queue
:可以是先进先出的(FIFOQueue
),或者随机的(RandomShuffleQueue
)。
每个队列管理器还有用一个enqueue_ops
列表,装有入队操作。第一个队列管理器只有一个入队操作;第二个队列管理器,由于设置num_threads=2
,有两个线程向样本队列中添加。
使用样本时,从第二个队列中执行出队操作。
接下来,我们用最简单的例子展示TensorFlow的队列机制。
队列本身也是图中的一个节点。
其他节点(enqueue, dequeue)可以修改队列节点中的内容。
创建一个先进先出队列,以及一个“出队,+1,入队”操作:
q = tf.FIFOQueue(3, "float")
x = q.dequeue()
y = x+1
q_inc = q.enqueue([y])
初始化并执行2次这个操作,之后查看队列内容
sess = tf.Session()
sess.run(q.enqueue_many(([0.1, 0.2, 0.3]),))
for i in range(0,2):
sess.run(q_inc)
for i in range(0,q.size()):
print(sess.run(q.dequeue()))
创建一个随机队列,最大长度为10,出队后的最小长度为2:
q = tf.RandomShuffleQueue(capacity=10, min_after_dequeue=2, dtypes="float")
执行10次入队,并查看内容:
sess = tf.Session()
for i in range(0, 10):
sess.run(q.enqueue(i))
for i in range(0, 8):
print(sess.run(q.dequeue()))
虽然队列长度为10次,但为保持最小长度2,只能输出8个结果。
在以下情况会发生阻断:
- 队列长度=最小值,执行dequeue
- 队列长度=最大值,执行enqueue
直到队列长度满足要求(其他线程执行了enqueue/dequeue操作),才能继续。
要谨慎设置队列的最小最大长度。引例中的两个QueueRunner
都会向tf.GraphKeys.SUMMARIES
集合中添加监测标示队列容量,可以在训练过程中使用TensorBoard观察。
也可以设定session
的运行选项,在一定时间之内解除阻断:
run_options = tf.RunOptions(timeout_in_ms = 10000) # 等待10秒
try:
sess.run(q.dequeue(), options=run_options)
except tf.errors.DeadlineExceededError:
print('out of range')
之前的例子中,入队操作都在主线程中进行。
在数据输入的应用场景中,入队操作从硬盘上读取,速度较慢。
使用QueueRunner
可以创建一系列新的线程进行入队操作,让主线程继续使用数据。
首先创造一个稍微复杂一点的队列作为例子:
q = tf.FIFOQueue(1000, "float")
counter = tf.Variable(0.0) # 计数器
increment_op = tf.assign_add(counter, tf.constant(1.0)) # 操作:给计数器加一
enqueue_op = q.enqueue(counter) # 操作:计数器值加入队列
创建一个队列管理器QueueRunner
,用这两个操作向q
中添加元素。目前我们只使用一个线程:
qr = tf.train.QueueRunner(q, enqueue_ops=[increment_op, enqueue_op] * 1)
从队列管理器中创建线程,并启动:
# 主线程
sess = tf.Session()
sess.run(tf.initialize_all_variables())
enqueue_threads = qr.create_threads(sess, start=True) # 启动入队线程
# 主线程
for i in range(0, 10):
print(sess.run(q.dequeue()))
输出并不是我们期待的自然数列,内部有重复:
1.0
1.0
2.0
3.0
4.0
4.0
5.0
5.0
7.0
8.0
这是因为increment_op
和enqueue_op
作为列表的两个元素传入QueueRunner
,执行是异步的。
有许多修改方法可以实现同步。
【法1】把两个操作变成列表中的一个元素:
qr = tf.train.QueueRunner(q, enqueue_ops=[[enqueue_op, enqueue_op]] * 1)
【法2】把加一操作变成入队操作的dependency:
with tf.control_dependencies([increment_op]):
enqueue_op = q.enqueue(counter)
qr = tf.train.QueueRunner(q, enqueue_ops=[enqueue_op] * 1)
【法3】把两个操作变成空操作的dependency:
with tf.control_dependencies([increment_op, enqueue_op]):
void_op = tf.no_op()
qr = tf.train.QueueRunner(q, enqueue_ops=[void_op] * 1)
【法4】用tf.group()
把两个操作组合起来:
qr = tf.train.QueueRunner(q, [tf.group(increment_op, enqueue_op)] * 1)
出队打印出自然数列:
1.0
2.0
3.0
4.0
5.0
6.0
7.0
8.0
9.0
10.0
当然,可以根据需要,在创建QueueRunner
时使用多个线程进行入队,提高数据读取速度。
在读入数据例子中,使用tf.train.string_input_produecer
和tf.train.shuffle_batch
把两个QueueRunner
添加到全局图中。
由于没有显式地返回QueueRunner
来用create_threads
创建并启动线程,必须这样做:
tf.train.start_queue_runners(sess=sess)
启动tf.GraphKeys.QUEUE_RUNNERS
集合中的所有队列线程。
QueueRunner
的例子有一个问题:由于入队线程自顾自地执行,在需要的出队操作完成之后,程序没法结束。
使用tf.train.Coordinator
来终止其他线程。
# 主线程
sess = tf.Session()
sess.run(tf.initialize_all_variables())
coord = tf.train.Coordinator()
enqueue_threads = qr.create_threads(sess, coord=coord, start=True) # 启动入队线程, Coordinator是线程的参数
# 主线程
for i in range(0, 10):
print(sess.run(q.dequeue()))
coord.request_stop() # 通知其他线程关闭
coord.join(enqueue_threads) # 其他所有线程关闭之后,这一函数才能返回
在队列线程关闭之后,如果再从队列中出队会抛出tf.errors.OutOfRange
错误。
依然参考读取数据例子,在创建文件名队列时,设定num_epoch
:
filename_queue = tf.train.string_input_producer([filename], num_epochs=3)
文件中有2个样本,只能读取3次,batch_size为1时一共能读取6次。主函数修改如下:
# create tensor
a, b, c = read_single_sample('/tmp/data.tfrecord')
a_batch, b_batch, c_batch = tf.train.shuffle_batch([a, b, c], batch_size=1, capacity=200, min_after_dequeue=0, num_threads=1)
# sess
sess = tf.Session()
sess.run(tf.initialize_all_variables())
sess.run(tf.initialize_local_variables()) # epoch计数变量是local variable
tf.train.start_queue_runners(sess=sess)
while True:
try:
print(sess.run([a_batch, b_batch, c_batch]))
except tf.errors.OutOfRangeError: # 文件队列关闭后,终止循环
print('finish')
break
特别要强调的是,string_input_producer
内部创建了一个epoch计数变量,归入tf.GraphKeys.LOCAL_VARIABLES
集合中,必须单独用initialize_local_variables()
初始化。
这里展示的也是TensorFlow循环训练样本的惯例:
- 在文件名队列中设定epoch数量
- 训练时,设定为无穷循环
- 读取数据时,如果捕捉到错误,终止。