【TensorFlow动手玩】队列

引子

队列(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动手玩】队列_第1张图片

使用样本时,从第二个队列中执行出队操作。

接下来,我们用最简单的例子展示TensorFlow的队列机制。

Queue

队列本身也是图中的一个节点
其他节点(enqueue, dequeue)可以修改队列节点中的内容。

FIFOQue

创建一个先进先出队列,以及一个“出队,+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()))

RandomShuffleQueue

创建一个随机队列,最大长度为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

之前的例子中,入队操作都在主线程中进行。
在数据输入的应用场景中,入队操作从硬盘上读取,速度较慢。
使用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_openqueue_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_produecertf.train.shuffle_batch把两个QueueRunner添加到全局图中。

由于没有显式地返回QueueRunner来用create_threads创建并启动线程,必须这样做:

tf.train.start_queue_runners(sess=sess)

启动tf.GraphKeys.QUEUE_RUNNERS集合中的所有队列线程。

Coordinator

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数量
- 训练时,设定为无穷循环
- 读取数据时,如果捕捉到错误,终止。

你可能感兴趣的:(DL框架)