1.基本概念
图(graphs):来表示计算任务;
在会话(session)的上下文(context)执行图;
使用Tensor表示数据;
通过变量(variable)维护状态;(是一种状态节点)
使用feed和fetch为任意的操作赋值或者从中提取数据;(feed:赋值,fetch取数据?Fetch可以同时运行多个op)
Tensorflow是一个编程系统。用graphs来表示计算任务,graphs中的每个节点称为op(operation),一个op中包含0或n个Tensor,也可以产生0或n个Tensor.Tensor可以看成一个n维数组或者列表。Graph必须在session中被启动。
tensorflow结构如下:
最外面是一个Session,session下面有两个graph,graph1里面有两个节点,即两个op,可以是加减乘除等等(图中的矩形)。图中的圆形是数据。Graph2中输入x,乘以权重w(matmul是矩阵相乘op),相乘的结果再加上偏执b(add是相加的op),最终送入一个relu激活函数得到结果。
2.tensorflow数据读取机制
2.1 首先需要思考的一个问题是,什么是数据读取?
以图像数据为例,读取数据的过程可以用下图来表示:
假设我们的硬盘中有一个图片数据集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依次放入两次再标记结束就可以了。
2.2 Tensorfow数据读取机制对应的函数
如何在TensorFlow中创建上述的两个队列呢?
对于文件名队列,我们使用tf.train.string_input_producer函数。这个函数需要传入一个文件名list,系统会自动将它转为一个文件名队列。
此外tf.train.string_input_producer还有两个重要的参数,一个是num_epochs,它就是我们上文中提到的epoch数。另外一个就是shuffle,shuffle是指在一个epoch内文件的顺序是否被打乱。
若设置shuffle=False,如下图,每个epoch内,数据还是按照A、B、C的顺序进入文件名队列,这个顺序不会改变:
如果设置shuffle=True,那么在一个epoch内,数据的前后顺序就会被打乱,如下图所示:(先打乱,再入队)
在TensorFlow中,内存队列不需要我们自己建立,我们只需要使用reader对象从文件名队列中读取数据就可以了,具体实现可以参考下面的实战代码。
除了tf.train.string_input_producer外,我们还要额外介绍一个函数:tf.train.start_queue_runners。初学者会经常在代码中看到这个函数,但往往很难理解它的用处,在这里,有了上面的铺垫后,我们就可以解释这个函数的作用了。
在我们使用tf.train.string_input_producer创建文件名队列后,整个系统其实还是处于“停滞状态”的,也就是说,我们文件名并没有真正被加入到队列中(如下图所示)。此时如果我们开始计算,因为内存队列中什么也没有,计算单元就会一直等待,导致整个系统被阻塞。
而使用tf.train.start_queue_runners之后,才会启动填充队列的线程,这时系统就不再“停滞”。此后计算单元就可以拿到数据并进行计算,整个程序也就跑起来了,这就是函数tf.train.start_queue_runners的用处。
3.队列与线程
3.1 基本概念名词
线程、队列、异步(多线程)、协调器(Coordinator)、QueueRunner
3.2 线程与队列使用综述
队列可以看成是variable节点,通过入队(enqueue)和出队(dequeue)来维持一种状态。
用队列来存放训练数据时:
- 多线程的准备训练数据,然后将数据push到队列里面;
- 训练线程生成一个训练op,来从队列中取出mini-batch个数据。
Session是多线程的,可以并行运行,多线程三个条件:①所有线程能同时停止②能捕捉和报告异常③队列能在停止的时候适当关闭
tensorflow提供两个类用于协调线程:tf.Coordinator和tf.train.QueueRunner。这两个类同时使用,其中Coordinator用于多线程停止和异常捕捉,tf.train.QueueRunner用于Tensor入队。
3.3 常用的队列类
主要有两个,分别是:tf.FIFOQueue和tf.RandomShuffleQueue
这两个队列中第一个队列是顺序队列,第二个队列是随机队列。两个队列都有的操作是enqueue、enqueue_many和dequeue,但是没有dequeue_many的操作。最常见的操作是一次性将数据存入(读入)队列(即入队),然后一个一个取出来(出队)。
如果想一次性取出多个数据,可以使用tf.train.batch或者tf.train.shuffle_batch
这里还有一个在sequence-to-sequence上比较常用的tf.paddingFIFOQueue,也就是带变量填充的队列,支持dequeue_many。
实际应用中,我们一般不直接使用队列本身,而是使用string_input_producer来读取数据。