假设文件中有一个图片数据集0001.jpg、0002.jpg、0003.jpg……只需要把它们读取到内存中,然后提供给GPU或是CPU进行计算就可以了。这听起来很容易,但事实远没有那么简单。事实上,必须先读入数据后 才能进行计算,假设读入用时 0.1s,计算用时 0.9s,那么就意味着每 过 1s,GPU 都会有0.1s无事可做,这大大降低了运算的效率。
如何解决这个问题?方法就是将读入数据和计算分别放在两个线程中, 将数据读入内存的一个队列,如图1所示。
读取线程源源不断地将文件系统中的图片读入一个内存的队列中,而负责计算的是另一个线程,计算需要数据时,直接从内存队列中取就可以 了。这样可以解决GPU因为I/O而空闲的问题!
而在TensorFlow 中,为了方便管理,在内存队列前又添加了一层所谓 的“文件名队列”。
为什么要添加这一层文件名队列呢?首先需要了解机器学习中的一个概 念:epoch。对于一个数据集来讲,运行一个 epoch 就是将这个数据集中的图片全部计算一遍。如果一个数据集中有三张图片 A.jpg、B.jpg、 C.jpg,那么运行一个 epoch 就是指对 A、B、C 三张图片都计算一遍。 两个 epoch就是指先对 A、B、C 各计算一遍,然后再全部计算一遍,也就是说每张图片都计算了两遍。
TensorFlow 使用“文件名队列+内存队列”双队列的形式读入文件,可以很好地管理 epoch。下面用图片的形式来说明这个机制的运行方式。如图2所示,还是以数据集A.jpg、B.jpg、C.jpg为例,假定要运行一个 epoch,那么就在文件名队列中把A、B、C各放入一次,并在之后标注队列结束。
程序运行后,内存队列首先读入A(此时A从文件名队列中出队),如 图3所示。
再依次读入B和C,如图4所示。
此时,如果再尝试读入,由于系统检测到了“结束”,就会自动抛出一个 异常(OutOfRange)。外部捕捉到这个异常后就可以结束程序了。这就是TensorFlow中读取数据的基本机制。如果要运行2个epoch而不是1个 epoch,则只要在文件名队列中将A、B、C依次放入两次再标记结束就可以了。
如何在TensorFlow中创建上述的两个队列呢? 对于文件名队列,使用 tf.train.string_input_producer 函数。这个函数需要传入一个文件名list,系统会自动将它转为一个文件名队列。 此外,tf.train.string_input_producer 还有两个重要的参数:一个是 num_epochs,它就是上文中提到的epoch数;另外一个是shuffle,shuffle 是指在一个 epoch 内文件的顺序是否被打乱。若设置 shuffle=False,如图 5所示,每个 epoch 内,数据仍然按照 A、B、C的顺序进入文件名 队列,这个顺序不会改变。
如果设置 shuffle=True,那么在一个 epoch 内,数据的前后顺序就会被 打乱,如图6所示。
在 TensorFlow 中,内存队列不需要自己建立,只需要使用 reader 对象从文件名队列中读取数据就可以了,具体实现可以参考下面的实战代码。
除了 tf.train.string_input_producer 外,还要额外介绍一个函数: tf.train.start_queue_runners。初学者会经常在代码中看到这个函数,但往往很难理解它的用处。有了上面的铺垫后,就可以解释这个函数的作用 了。
在使用 tf.train.string_input_producer 创建文件名队列后,整个系统其实还处于“停滞状态”,也就是说,文件名并没有真正被加入队列中,如图 7所示。如果此时开始计算,因为内存队列中什么也没有,计算单元 就会一直等待,导致整个系统被阻塞。
而使用tf.train.start_queue_runners之后,才会启动填充队列的线程,这时系统就不再“停滞”。此后,计算单元就可以拿到数据并进行计算,整个程序也就运行起来了,这就是函数tf.train.start_queue_runners的用处。 下面用一个具体的例子体会 TensorFlow 中的数据读取。如图8所示,假设在当前文件夹中已经有 A.jpg、B.jpg、C.jpg三张图片,希望读取这三张图片的5个 epoch 并且把读取的结果重新存到read文件夹中。
具体代码test.py
import tensorflow as tf
import os
#判断是否存在read文件夹,不存在的话则建立文件夹
if not os.path.exists('read'):
os.makedirs('read/')
#建立一个Session
with tf.Session() as sess:
#读三幅图片A.jpg, B.jpg, C.jpg
filename = ['A.jpg', 'B.jpg', 'C.jpg']
#tring_input_producer会产生一个文件名队列,第一个参数为文件名列表,
#第二个参数是 num_epochs,也就是对文件中的图片遍历的次数;
#第三个参数是shuffle,shuffle是指在一个 epoch 内文件的顺序是否被打乱True/False。
filename_queue = tf.train.string_input_producer(filename, shuffle=True, num_epochs=5)
#reader从文件名队列中读数据。对应的方法是reader.read
reader = tf.WholeFileReader()
#返回图片和标签值
key, value = reader.read(filename_queue)
#tf.train.string_input_producer定义了一个epoch变量,要对它进行初始化
sess.run(tf.local_variables_initializer())
#使用start_queue_runners之后,才会开始填充队列
threads = tf.train.start_queue_runners(sess=sess)
i = 0
while True:
i += 1
#获取图片数据并保存
image_data = sess.run(value)
with open('read/test_%d.jpg' % i, 'wb') as f:
f.write(image_data)
#程序最后会抛出一个OutOfRangeError,这是epoch跑完,队列关闭的标志
这里使用filename_queue=tf.train.string_input_producer(filename,shuffle=False,num_epochs=5)建立了一个会运行5个epoch的文件名队列。并使用reader读取,reader每次读取一张图片并保存。 运行代码后(程序最后会抛出一个 OutOfRangeError 异常,不必担心, 这就是epoch跑完,队列关闭的标志),得到read文件夹中的图片,正好是按顺序的5个epoch,如图9所示。
如果设置 filename_queue=tf.train.string_input_producer(filename, shuffle=False,num_epochs=5)中的shuffle=True,那么在每个epoch内图像会被打乱,如图10所示。
这里只是用三张图片举例,实际应用中一个数据集肯定不止 3 张图片, 不过涉及的原理都是共通的。