• tensorflow基本知识学完了,但是看源码时,很多代码都用比较高封装的用法…所以准备开个系列,整个贯通一遍。
  • TensorFlow程序读取数据一共有3种方法:
  1. 供给数据(Feeding): 在TensorFlow程序运行的每一步, 让Python代码来供给数据。
  2. 从文件读取数据: 在TensorFlow图的起始, 让一个输入管线从文件中读取数据。
  3. 预加载数据: 在TensorFlow图中定义常量或变量来保存所有数据(仅适用于数据量比较小的情况)。

预加载数据

  • 个人目前唯一见过是对应NLP任务中的:embedding层,可能会将pre-train的embedding向量静态加载进来,其他情况还真没怎么用过。
  • 一句话形容就是:将数据直接内嵌到图中
  • 最最简单的方式类似与下面:
import tensorflow as tf
x1 = tf.constant([2,3,4])
x2 = tf.constant([4,0,1])

y = tf.add(x1,x2)

with tf.Session() as sess:
    print(sess.run(y))

Feeding

  • 这个数据的读入方式应该是像我这样的小白最常用,或者最习惯用的。但是却是大佬们不常用的。
  • 就是读取数据和处理数据全部用python程序写好,然后feed进对应的节点。
  • 简单用代码举一个例子:
import tensorflow as tf
x1 = tf.placeholder(tf.int32)
x2 = tf.placeholder(tf.int32)
#用python产生数据
v1 = [2,3,4]
v2 = [4,0,1]

y = tf.add(x1,x2)

with tf.Session() as sess:
    print(sess.run(y,feed_dict={x1:v1,x2:v2}))

以上两种方法都很方便,但是遇到大型数据的时候就会很吃力,即使是Feed_dict,中间环节的增加也是不小的开销,因为数据量大的时候,TensorFlow程序运行的每一步,我们都需要使用python代码去从文件中读取数据,并对读取到的文件数据进行解码。最优的方案就是在图中定义好文件读取的方法,让TF自己从文件中读取数据,并解码成可用的样本集。

从流水线(pipeline)说起

最早用到的TensorFlow读数据的方法就是把数据集中所有的数据都读到内存中。但是有些数据集很大,没法一次读入内存。我们可以按batch读数据,一次读入一个batch。如果是纯串行的操作,即“读数据->计算->读数据”,这样的计算效率就相当低。

  • 一个可行的改进就是“流水线”(pipeline)。流水线的一个简明的解释
  • 流水线(pipeline)是将组合逻辑进行分割,能让任务以类似并行方式处理,提高系统频率,提高吞吐量(throughput),使各模块利用率达到最高。
  • 放到tensorflow里就是:TensorFlow中,读取数据的线程负责把数据从硬盘读到内存中的队列里面,计算的线程从内存中的队列得到数据进行计算。也就是说,读数据的线程只管把数据读到内存中,计算的线程只管从内存中取数据。两者都不会空闲下来等对方。

TensorFlow中的队列机制

TensorFlow提供了一个队列机制,通过多线程将读取数据与计算数据分开。因为在处理海量数据集的训练时,无法把数据集一次全部载入到内存中,需要一边从硬盘中读取,一边进行训练,为了加快训练速度,我们可以采用多个线程读取数据,一个线程消耗数据。

  1. Queue,队列,本身也是图中的一个节点。入队和出队的操作(enqueue, dequeue)也是图中的节点,可以修改Queue节点中的内容。类似Variable,用来存放数据。
  2. 如果Queue中的数据满了,那么enqueue操作将会阻塞,如果Queue是空的,那么dequeue操作就会阻塞。如果操作不当,可能会出现程序卡住的问题,我就遇到过这个情况。
  3. 在常用环境中,一般是有多个enqueue线程同时像Queue中放数据,有一个dequeue操作从Queue中取数据。一般来说enqueue线程就是准备数据的线程,dequeue线程就是训练数据的线程.

明确概念

  • 其实概念只有三个:
  1. Queue是TF队列和缓存机制的实现
  2. QueueRunner是TF中对操作Queue的线程的封装
  3. Coordinator是TF中用来协调线程运行的工具
  • 虽然它们经常同时出现,但这三样东西在TensorFlow里面是可以单独使用的,不妨先分开来看待。

Queue

据实现的方式不同(出队的方式不同),分成具体的几种类型,例如:

  • tf.FIFOQueue :按入列顺序出列的队列
  • tf.RandomShuffleQueue :随机顺序出列的队列
  • tf.PaddingFIFOQueue :以固定长度批量出列的队列
  • tf.PriorityQueue :带优先级出列的队列
  • … …
  • 这些类型的Queue除了自身的性质不太一样外,创建、使用的方法基本是相同的。
  • Queue主要包含入列(enqueue)和出列(dequeue)两个操作。队列本身也是图中的一个节点。其他节点(enqueue, dequeue)可以修改队列节点中的内容。enqueue操作返回计算图中的一个Operation节点,dequeue操作返回一个Tensor值。Tensor在创建时同样只是一个定义(或称为“声明”),需要放在Session中运行才能获得真正的数值。
  • Enqueue、 EnqueueMany和Dequeue都是特殊的节点。他们需要获取队列指针,而非普通的值,如此才能修改队列内容。我们建议您将它们看作队列的方法。

下面是一个单独使用Queue的例子:参考:tensorflow中关于队列使用的实验

#创建的图:一个先入先出队列,以及初始化,出队,+1,入队操作  
q = tf.FIFOQueue(3, "float")  
init = q.enqueue_many(([0.1, 0.2, 0.3],))  

x = q.dequeue()  
y = x + 1  
q_inc = q.enqueue([y])  
  
#开启一个session,session是会话,会话的潜在含义是状态保持,各种tensor的状态保持  
with tf.Session() as sess: 
    
    # 初始化节点
    sess.run(init)  
    
    # 出队改变
    for i in range(2):  
        sess.run(q_inc) 
    
    # 队列长度
    quelen =  sess.run(q.size())  
    
    for i in range(quelen):  
            print (sess.run(q.dequeue()))

0.3
1.1
1.2

QueueRunner

  • 之前的例子中,入队操作都在主线程中进行,Session中可以多个线程一起运行。 在数据输入的应用场景中,入队操作从硬盘上读取,入队操作是从硬盘中读取输入,放到内存当中,速度较慢。 使用QueueRunner可以创建一系列新的线程进行入队操作,让主线程继续使用数据。如果在训练神经网络的场景中,就是训练网络和读取数据是异步的,主线程在训练网络,另一个线程在将数据从硬盘读入内存。

代码参考:tensorflow中关于队列使用的实验

'''
'''
QueueRunner()的使用
'''

q = tf.FIFOQueue(10, "float")  

counter = tf.Variable(0.0)  #计数器

# 给计数器加一
# 返回的是一个op
increment_op = tf.assign_add(counter, 1.0)

# 将计数器加入队列
enqueue_op = q.enqueue(counter)

# 创建QueueRunner
# 用多个线程向队列添加数据
# 这里实际创建了4个线程,两个增加计数,两个执行入队

qr = tf.train.QueueRunner(q, enqueue_ops=[increment_op, enqueue_op] * 2)


#主线程  

sess =  tf.Session()  
    
# 初始化变量
sess.run(tf.initialize_all_variables())  

#启动入队线程  
enqueue_threads = qr.create_threads(sess, start=True)  

#主线程  
for i in range(10):              
    print (sess.run(q.dequeue()))

2.0
2.0
2.0
5.0
4.0
7.0
7.0
10.0
10.0
12.0

  • 并不是我们设想的1,2,3,4,本质原因是增加计数的进程会不停的后台运行,执行入队的进程会先执行10次(因为队列长度只有10),然后主线程开始消费数据,当一部分数据消费被后,入队的进程又会开始执行。最终主线程消费完10个数据后停止,但其他线程继续运行,程序不会结束。
  • 经验:因为tensorflow是在图上进行计算,要驱动一张图进行计算,必须要送入数据,如果说数据没有送进去,那么sess.run(),就无法执行,tf也不会主动报错,提示没有数据送进去,其实tf也不能主动报错,因为tf的训练过程和读取数据的过程其实是异步的。tf会一直挂起,等待数据准备好。现象就是tf的程序不报错,但是一直不动,跟挂起类似。

Coordinator

  • 最后一个因素是Coordinator。这是负责在收到任何关闭信号的时候,让所有的线程都知道。最常用的是在发生异常时这种情况就会呈现出来,比如说其中一个线程在运行某些操作时出现错误(或一个普通的Python异常)。
  • 个人尝试代码后发现代码有些问题,所以此处暂时保留意见。

从文件中读取数据(只是读取方式,这里没有加上面说的多线程)

  • 注意下面读取文件的函数都是tf的内置函数,而不是我们平常python IO文件Open那些函数。

字典

  • 1.从字典结构的数据文件读取(python数据格式)
    其实就是pickle的使用

详情看:此博文

从bin文件读取

  • tf.FixedLengthRecordReader 类来每次读取固定长度的字节,正好对应一个样本存储的字节(包括label)。
  • 用tf.decode_raw进行解析。

从CSV(TXT)文件读取

  • tf.TextLineReader 类来每次读取一行,并使用tf.decode_csv来对每一行进行解析

从TFRecord文件读取

  • TFrecord是Tensorflow推荐的数据集格式,与Tensorflow框架紧密结合。在TensorFlow中提供了一系列接口可以访问TFRecord格式。

多线程读取文件

QueueRunner和Coordinator经典结合方式一

  • 显式的创建QueueRunner,然后调用它的create_threads方法启动线程
  • 但我们一般都是IO文件
import numpy as np
# 1000个4维输入向量,每个数取值为1-10之间的随机数
data = 10 * np.random.randn(1000, 4) + 1
# 1000个随机的目标值,值为0或1
target = np.random.randint(0, 2, size=1000)

# 创建Queue,队列中每一项包含一个输入数据和相应的目标值
queue = tf.FIFOQueue(capacity=50, dtypes=[tf.float32, tf.int32], shapes=[[4], []])

# 批量入列数据(这是一个Operation)
enqueue_op = queue.enqueue_many([data, target])
# 出列数据(这是一个Tensor定义)
data_sample, label_sample = queue.dequeue()

# 创建包含4个线程的QueueRunner
qr = tf.train.QueueRunner(queue, [enqueue_op] * 4)

with tf.Session() as sess:
    # 创建Coordinator
    coord = tf.train.Coordinator()
    # 启动QueueRunner管理的线程
    enqueue_threads = qr.create_threads(sess, coord=coord, start=True)
    # 主线程,消费100个数据
    for step in range(100):
        if coord.should_stop():
            break
        data_batch, label_batch = sess.run([data_sample, label_sample])
        print(data_batch)
        print(label_batch)
        
    # 主线程计算完成,停止所有采集数据的进程
    coord.request_stop()
    coord.join(enqueue_threads)

QueueRunner和Coordinator经典结合方式二

  • 隐式创建线程的方法。
  • 在讲解具体代码之前,我们需要先来讲解关于TensorFlow中的文件队列机制和内存队列机制。
  • 具体可以参考:
  • 十图讲解tensorflow内存读取机制
  • 针对不同的文件,其实就是解析方式不同,代码可以参考
  • 上面那篇论可以结合这篇文章

例如我们要读取CSV:单个reader,多个样本

import tensorflow as tf
filenames = ['A.csv', 'B.csv', 'C.csv']
filename_queue = tf.train.string_input_producer(filenames, shuffle=False)
reader = tf.TextLineReader()
key, value = reader.read(filename_queue)
example, label = tf.decode_csv(value, record_defaults=[['null'], ['null']])

# 使用tf.train.batch()会多加了一个样本队列和一个QueueRunner。Decoder解后数据会进入这个队列,再批量出队。
# 虽然这里只有一个Reader,但可以设置多线程,相应增加线程数会提高读取速度,但并不是线程越多越好。

example_batch, label_batch = tf.train.batch( [example, label], batch_size=5)

with tf.Session() as sess:
	coord = tf.train.Coordinator()
    threads = tf.train.start_queue_runners(coord=coord)
    
    for i in range(10):
    	print(example_batch.eval())
    coord.request_stop()
    coord.join(threads)
# output
# ['Alpha1' 'Alpha2' 'Alpha3' 'Bee1' 'Bee2']
# ['Bee3' 'Sea1' 'Sea2' 'Sea3' 'Alpha1']
# ['Alpha2' 'Alpha3' 'Bee1' 'Bee2' 'Bee3']
# ['Sea1' 'Sea2' 'Sea3' 'Alpha1' 'Alpha2']
# ['Alpha3' 'Bee1' 'Bee2' 'Bee3' 'Sea1']
# ['Sea2' 'Sea3' 'Alpha1' 'Alpha2' 'Alpha3']
# ['Bee1' 'Bee2' 'Bee3' 'Sea1' 'Sea2']
# ['Sea3' 'Alpha1' 'Alpha2' 'Alpha3' 'Bee1']
 # ['Bee2' 'Bee3' 'Sea1' 'Sea2' 'Sea3']
 # ['Alpha1' 'Alpha2' 'Alpha3' 'Bee1' 'Bee2']

tf.train.batch(tensors, batch_size, num_threads=1, capacity=32, enqueue_many=False, shapes=None, dynamic_pad=False, allow_smaller_final_batch=False, shared_name=None, name=None)

对比,多reader

tensorflow恢复检查点_tensorflow恢复检查点

tf.train.batch与tf.train.shuffle_batch函数是单个Reader读取,但是可以多线程。tf.train.batch_join与tf.train.shuffle_batch_join可设置多Reader读取,每个Reader使用一个线程。至于两种方法的效率,单Reader时,2个线程就达到了速度的极限。多Reader时,2个Reader就达到了极限。所以并不是线程越多越快,甚至更多的线程反而会使效率下降。