TensorFlow(五)队列与线程

 

深度学习的模型训练过程往往需要大量的数据,而将这些数据一次性的读入和预处理需要大量的时间开销,所以通常采用队列与多线程的思想解决这个问题,而且TensorFlow为我们提供了完善的函数。 

TensorFlow提供了整套实现队列的函数和方法,在TensorFlow中,队列和变量类似,都是计算图上有状态的节点。操作队列的函数主要有:

  • FIFOQueue():创建一个先入先出(FIFO)的队列

  • RandomShuffleQueue():创建一个随机出队的队列

  • enqueue_many():初始化队列中的元素

  • dequeue():出队

  • enqueue():入队

import tensorflow as tf
with tf.Session() as sess:
    q = tf.FIFOQueue(3 , "float")
    
    #进行三步操作
    init = q.enqueue_many(([0.1 , 0.2 , 0.3],)) #创建队列
    init2 = q.dequeue() #出队列
    init3 = q.enqueue(1) #将1入队
    
    #执行
    sess.run(init)
    sess.run(init2)
    sess.run(init3)

    #将所有队列元素出队打印    
    quelen = sess.run(q.size())
    for i in range(quelen):
        print(sess.run(q.dequeue()))

         

同时需要注意的是队列会由于一些例如 队列满进入队列 、 数据未能弹出 等原因堵塞 , 直到堵塞的原因解除。

入队操作都在主线程中进行,Session中可以多个线程一起运行。 在数据输入的应用场景中,入队操作从硬盘上读取,入队操作是从硬盘中读取输入,放到内存当中,速度较慢。 使用QueueRunner可以创建一系列新的线程进行入队操作,让主线程继续使用数据。如果在训练神经网络的场景中,就是训练网络和读取数据是异步的,主线程在训练网络,另一个线程在将数据从硬盘读入内存。同时tf提供了 QueueRunner 函数 用来解决异步问题 , 可以创建一系列线程同时进入主线程操作 , 数据的读取和操作是同步的 , 即主线程在进行模型的训练同时将数据读入。

 

import tensorflow as tf
with tf.Session() as sess:
    
    q = tf.FIFOQueue(1000 , "float32")
    
    counter = tf.Variable(0.0)
    
    #第一个任务 counter为计数器 不断计数
    add_op = tf.assign_add(counter , tf.constant(1.0))
    
    #第二个任务 将计数结果不断入队
    enqueueData_op = q.enqueue(counter)
    
    #创建四个线程完成 计数入队的操作
    qr = tf.train.QueueRunner(q , enqueue_ops = [add_op , enqueueData_op] * 4)
    sess.run(tf.global_variables_initializer())
    
    #启动入队线程
    enqueue_threads = qr.create_threads(sess , start = True)
    
    for i in range(10):
        #出队
        print(sess.run(q.dequeue()))

此时会出现报错 错误:tensorflow:QueueRunner中的异常:会话已关闭。

当main循环结束后,该Session就会自动关闭,导致线程无法继续。

import tensorflow as tf
with tf.Session() as sess:
    
    q = tf.FIFOQueue(1000 , "float32")
    
    counter = tf.Variable(0.0)
    
    #第一个任务 counter为计数器 不断计数
    add_op = tf.assign_add(counter , tf.constant(1.0))
    
    #第二个任务 将计数结果不断入队
    enqueueData_op = q.enqueue(counter)
    
    #启动Session
    sess = tf.Session()
    #创建四个线程完成 计数入队的操作
    qr = tf.train.QueueRunner(q , enqueue_ops = [add_op , enqueueData_op] * 4)
    sess.run(tf.global_variables_initializer())
    
    #启动入队线程
    enqueue_threads = qr.create_threads(sess , start = True)
    
    for i in range(10):
        #出队
        print(sess.run(q.dequeue()))

而先将sess初始化为Session,便不会报错主要原因是因为tensorflow是在图上进行计算,要驱动一张图进行计算,必须要送入数据,如果说数据没有送进去,那么sess.run(),就无法执行,tf也不会主动报错,提示没有数据送进去,其实tf也不能主动报错,因为tf的训练过程和读取数据的过程其实是异步的。tf会一直挂起,等待数据准备好。现象就是tf的程序不报错,但是一直不动,跟挂起类似。 
多个线程时由于一个线程与另一个线程会有冲突的操作,导致会话出现错误,为了解决同步问题,提供Coordinator和QueueRunner函数来对线程进行控制和协调。来共同协作停止线程

import tensorflow as tf
with tf.Session() as sess:
    
    q = tf.FIFOQueue(1000 , "float32")
    
    counter = tf.Variable(0.0)
    
    #第一个任务 counter为计数器 不断计数
    add_op = tf.assign_add(counter , tf.constant(1.0))
    
    #第二个任务 将计数结果不断入队
    enqueueData_op = q.enqueue(counter)
    
    #启动Session
    sess = tf.Session()
    #创建四个线程完成 计数入队的操作
    qr = tf.train.QueueRunner(q , enqueue_ops = [add_op , enqueueData_op] * 4)
    sess.run(tf.global_variables_initializer())
    
    #启动入队线程
    enqueue_threads = qr.create_threads(sess , start = True)
    
    #线程协调器 协调线程之间的关系
    coord = tf.train.Coordinator()
    enqueue_threads = qr.create_threads(sess , coord = coord , start = True)
    
    for i in range(10):
        #出队
        print(sess.run(q.dequeue()))
        
    coord.request_stop()
    coord.join(enqueue_threads)

首先需要思考的一个问题是,什么是数据读取?以图像数据为例,读取数据的过程可以用下图来表示:


 

假设我们的硬盘中有一个图片数据集0001.jpg,0002.jpg,0003.jpg……我们只需要把它们读取到内存中,然后提供给GPU或是CPU进行计算就可以了。这听起来很容易,但事实远没有那么简单。事实上,我们必须要把数据先读入后才能进行计算,假设读入用时0.1s,计算用时0.9s,那么就意味着每过1s,GPU都会有0.1s无事可做,这就大大降低了运算的效率。

如何解决这个问题?方法就是将读入数据和计算分别放在两个线程中,将数据读入内存的一个队列,如下图所示:

 

 

读取线程源源不断地将文件系统中的图片读入到一个内存的队列中,而负责计算的是另一个线程,计算需要数据时,直接从内存队列中取就可以了。这样就可以解决GPU因为IO而空闲的问题!

而在tensorflow中,为了方便管理,在内存队列前又添加了一层所谓的“文件名队列”。

为什么要添加这一层文件名队列?我们首先得了解机器学习中的一个概念:epoch。对于一个数据集来讲,运行一个epoch就是将这个数据集中的图片全部计算一遍。如一个数据集中有三张图片A.jpg、B.jpg、C.jpg,那么跑一个epoch就是指对A、B、C三张图片都计算了一遍。两个epoch就是指先对A、B、C各计算一遍,然后再全部计算一遍,也就是说每张图片都计算了两遍。

tensorflow使用文件名队列+内存队列双队列的形式读入文件,可以很好地管理epoch。下面我们用图片的形式来说明这个机制的运行方式。如下图,还是以数据集A.jpg, B.jpg, C.jpg为例,假定我们要跑一个epoch,那么我们就在文件名队列中把A、B、C各放入一次,并在之后标注队列结束。

程序运行后,内存队列首先读入A(此时A从文件名队列中出队):

 

再依次读入B和C:

 

此时,如果再尝试读入,系统由于检测到了“结束”,就会自动抛出一个异常(OutOfRange)。外部捕捉到这个异常后就可以结束程序了。这就是tensorflow中读取数据的基本机制。如果我们要跑2个epoch而不是1个epoch,那只要在文件名队列中将A、B、C依次放入两次再标记结束就可以了。

典型的文件数据读取会包含下面这些步骤:

(1)文件名列表

可以使用字符串张量(比如["file0", "file1"][("file%d" % i) for i in range(2)], [("file%d" % i) for i in range(2)]) 或者tf.train.match_filenames_once ()函数来产生文件名列表。

filenames = [os.path.join(data_dir, 'data_batch_%d.bin' % i)
                 for i in xrange(1, 6)]

(2)文件名队列

对于文件名队列,我们使用tf.train.string_input_producer()函数。这个函数需要传入一个文件名list,系统会自动将它转为一个先入先出的文件名队列, 文件阅读器会需要它来读取数据。

# 同时打开多个文件,显示创建Queue,同时隐含了QueueRunner的创建
filename_queue = tf.train.string_input_producer(filenames)

(3)可配置的 文件名乱序(shuffling),可配置的最大训练迭代数(epoch limit)

tf.train.string_input_producer还有两个重要的参数,一个是num_epochs,它就是我们上文中提到的epoch数。另外一个就是shuffle,shuffle是指在一个epoch内文件的顺序是否被打乱。若设置shuffle=False,如下图,每个epoch内,数据还是按照A、B、C的顺序进入文件名队列,这个顺序不会改变:

如果设置shuffle=True,那么在一个epoch内,数据的前后顺序就会被打乱,如下图所示:

在tensorflow中,内存队列不需要我们自己建立,我们只需要使用reader对象从文件名队列中读取数据就可以了。

(4)针对输入文件格式的阅读器

根据你的文件格式, 选择对应的文件阅读器, 然后将文件名队列提供给阅读器的read()方法。阅读器的read()方法会输出一个key来表征输入的文件和其中的纪录(对于调试非常有用),同时得到一个字符串标量, 这个字符串标量可以被一个或多个解析器,或者转换操作将其解码为张量并且构造成为样本。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值