《Tensorflow技术解析与实战》第四章
Tensorflow基础知识
Tensorflow设计理念
(1)将图的定义和图的运行完全分开,因此Tensorflow被认为是一个“符合主义”的库
(2)Tensorflow中涉及的运算都要放在图中,而图的运行只发生在会话(session)中。开启会话后,就可以用数据去填充节点,进行运算。关闭会话后,就不能继续计算了。因此会话提供了操作运算和Tensor求值的环境
编程模型
边
Tensorflow的边有两种连接关系:数据依赖和控制依赖。其中实线边表示数据依赖,代表数据,即张量。张量在数据流图中从前往后流动一遍就完成了一次前向传播,而残差从后向前流动一编就完成了一次反向传播。
还有一种特殊边,一般画为虚线边,称为控制依赖,可以用于控制操作的运算,这被用来确保happens-before关系,这类边上没有数据流过,但源节点必须在目的节点开始执行前完成执行。
Tensorflow支持的张量数据属性
数据类型 Python类型
DT_FLOAT tf.float32 32位浮点型
DT_DOUBLE tf.float64 64位浮点型
DT_INT64 tf.int64 64位有符号整型
DT_INT32 tf.int32 32位有符号整型
DT_INT16 tf.int16 16位有符号整型
DT_INT8 tf.int8 8位有符号整型
DT_UINT8 tf.uint8 8位无符号整型
DT_STRING tf.string 可变长度的字节数组
DT_BOOL tf.bool 布尔型
DT_COMPLEX64 tf.complex64 由两个32位浮点数组成的复数
DT_QINT32 tf.qint32 用于量化操作的32位有符号整型
DT_QINT8 tf.qint8 用于量化操作的8位有符号整型
DT_QUINT8 tf.quint8 用于量化操作的8位无符号整型
节点
图中的节点又称为算子,它代表一个操作(op),一般用来表示施加的数学运算,也可以表示数据输入(feed in)的起点以及输出(push out)的终点,或者是读取/写入持久变量的终点
下面列举了一些Tensorflow实现的算子
类别 示例
数学运算操作 Add,Sub,Mul,Div,Exp,Log,Greater,Less,Equal
数组运算操作 Concat,Slice,Split,Constant,Rank,Shape,Shuffle
矩阵运算操作 MatMul,MatrixInverse,MatrixDeterminant
有状态的操作 Variable,Assign,AssignAdd
神经网络构建操作 SoftMax,Sigmoid,ReLU,Convolution2D,MaxPool
检查点操作 Save,Restore
队列和同步操作 Enqueue,Dequeue,MutexAcquire,MutexRelease
控制张量流动的操作 Merge,Switch,Enter,Leave,NextIteration
其他概念
除了边和节点,Tensorflow还涉及其他一些概念,如图、会话、设备、变量、内核等。
1.图
把操作任务描述成有向无环图,那么如何构建图呢?构建图的第一步就是创建各个节点
2.会话
启动图的第一步是创建一个Session对象。会话(Session)提供在图中执行操作的一些方法。一般的模式是,建立会话,此时会产生一张空图,在会话中添加节点和边,形成一张图,然后执行
在调用Session对象的run()方法来执行图时,传入一些Tensor,这个过程叫填充(feed);返回的结果类型根据输入的类型而定,这个过程叫取回(fetch)
会话是图交互的一个桥梁,一个会话可以有多个图,会话可以修改图的结构,也可以往图中注入数据进行计算。因此,会话主要有两个API接口:Extend和Run.Extend操作是在Graph中添加节点和边,Run操作是输入计算的节点和填充必要的数据后,进行运算,并输出运算结果。
3.设备
设备(device)是指一块可以用来运算并且拥有自己的地址空间的硬件,如CPU和GPU.Tensorflow为了实现分布式执行操作,充分利用计算资源,可以明确指定操作在哪个设备上执行。
4.变量
变量是一种特殊的数据,它在图中有固定的位置,不像普通张量那样可以流动。例如,创建一个变量张量,使用tf.Variable()构造函数,这个构造函数需要一个初始值
Tensorflow还提供填充机制,可以在构建图时使用tf.placeholder()临时替代任意操作的张量,在调用Session对象的run()去执行图时,使用填充数据作为调用的参数,调用结束后,填充数据就消失。
import tensorflow as tf input1=tf.placeholder(tf.float32) input2=tf.placeholder(tf.float32) output=tf.multiply(input1,input2) with tf.Session() as sess: print(sess.run([output],feed_dict={input1:[7.],input2:[2.]}))
5.内核
我们知道操作是对抽象操作的一个统称,而内核则是能够运行在特定设备(如CPU,GPU)上的一种对操作的实现。因此,同一个操作可能会对应多个内核。
当自定义一个操作时,需要把新操作和内核通过注册的方式添加到系统中。
常用API
图、操作和张量
Tensorflow的计算表现位数据流图,所以tf.Graph类中包含一系列表示计算的操作对象(tf.Operation),以及在操作之间流动的数据——张量对象(tf.Tensor)。与图相关的API均位于tf.Graph类中
tf.Graph._init_() 创建一个空图
tf.Graph.as_default() 将某图设置为默认图,并返回一个上下文管理器。如果不显式添加一个默认图,系统会自动设置一个全局的默认图。所设置的默认图,在模块范围内定义的节点都将默认加入默认图中
tf.Graph.device(device_name_or_function) 定义运行图所使用的设备,并返回一个上下文管理器
tf.Graph.name_scope(name) 为节点创建层次化的名称,并返回一个上下文管理器。
tf.Operation类代表图中的一个节点,用于计算张量数据。该类型由节点构造器产生。
与操作相关的API均位于tf.Operation类中
tf.Operation.name 操作名称
tf.Operation.type 操作类型
tf.Operation.inputs 操作的输入
tf.Operation.outputs 操作的输出
tf.Operation.control_inputs 操作的依赖
tf.Operation.run(feed_dict=None,session=None) 在会话中运行该操作
tf.Operation.get_attr(name) 获取操作的属性值
tf.Tensor类是操作输出的符号句柄,它不包含操作i输出的值,而是提供了一种在tf.Session中计算这些值得方法。这样就可以在操作之间构建一个数据流连接,使Tensorflow能够执行一个表示大量多步计算的图形
tf.Tensor.dtype 张量的数据类型
tf.Tensor.name 张量的名称
tf.Tensor.value_index 张量在操作输出中的索引
tf.Tensor.graph 张量所在的图
tf.Tensor.op 产生该张量的操作
tf.Tensor.consumers() 返回使用该张量的操作列表
tf.Tensor.eval(feed_dict,session=None) 在会话中求张量的值,需要使用sess.as_default()或者eval(session=sess)
tf.Tensor.get_shape() 返回用于表示张量的形状(维度)的类Tensorshape
tf.Tensor.set_shape() 更新张量的形状
tf.Tensor.device 设置计算该张量的设备
可视化
可视化时,需要在程序中给必要的节点添加摘要(summary),摘要会收集该节点的数据,并标记上第几步、时间戳等标识,写入事件文件(event file)中。tf.summary.FileWriter类用于在目录中创建事件文件,并且向文件中添加摘要和事件,用来在TensorBoard中展示
可视化常用API
tf.summary.FileWriter._init_(logdir,graph=None,max_queue=10,flush_secs=120,graph_def=None) 创建FileWriter和事件文件,会在logdir中创建一个新的事件文件
tf.summary.FileWriter.add_summary(summary,global_step=None) 将摘要添加到事件文件
tf.summary.FileWriter.add_event(event) 向事件文件中添加一个事件
tf.summary.FileWriter.add_graph(graph,global_step=None,graph_def=None) 向事件文件中添加一个图
tf.summary.FileWriter.get_logdir 获取事件文件的路径
tf.summary.FileWriter.flush() 将所有事件都写入磁盘
tf.summary.FileWriter.close() 将事件写入磁盘,并关闭文件操作符
tf.summary.scalar(name,tensor,collections=None) 输出包含单个标量值的摘要
tf.summary.histogram(name,values,collections=None) 输出包含直方图的摘要
tf.summary.audio(name,tensor,sample_rate,max_outputs=3,collections=None) 输出包含音频的摘要
tf.summary.image(name,tensor,max_outputs=3,collections=None) 输出包含图片的摘要
tf.summary.merge(inputs,collections=None,name=None) 合并摘要,包含所有输入摘要的值
变量作用域
在Tensorflow中有两个作用域(scope),一个是name_scope,另一个是variable_scope.两者的区别是,variable_scope主要是给variable_name加前缀的,也可以给op_name加前缀;name_scope是给op_name加前缀。
variable_scope变量作用域机制在Tensorflow中主要由两部分组成:
v=tf.get_variable(name,shape,dtype,initializer)#通过所给的名字创建或是返回一个变量
tf.variable_scope(<scope_name>)#为变量指定命名空间
当tf.get_variable_scope().reuse==False时,variable_scope作用域只能用来创建新变量
import tensorflow as tf with tf.variable_scope("foo"): v=tf.get_variable("v",[1]) v2=tf.get_variable("v",[1]) assert(v.name=="foo/v:0")
上面这个程序会报错,因为v这个变量已经被定义过了,但tf.get_variable_scope()。reuse默认为False,所以不能重用
当tf.get_variable_scope().reuse==True时,作用域可以共享变量
import tensorflow as tf with tf.variable_scope("foo") as scope: v=tf.get_variable("v",[1]) with tf.variable_scope("foo",reuse=True): v1=tf.get_variable("v",[1]) assert(v1==v)
1.获取变量作用域
可以直接通过tf.Variable_scope()来获取变量作用域
如果在开启一个变量作用域里使用之前预先定义的一个作用域,则会跳过当前变量的作用域,保持预先存在的作用域不变
import tensorflow as tf with tf.variable_scope("foo") as foo_scope: assert (foo_scope.name=="foo") with tf.variable_scope("bar"): with tf.variable_scope("baz") as other_scope: assert (other_scope.name=="bar/baz") with tf.variable_scope(foo_scope) as foo_scope2: assert(foo_scope2.name=="foo") #保持不变
变量作用域可以默认携带一个初始化器,在这个作用域中的子作用域或变量都可以继承或者重写父作用域初始化器中的值
import tensorflow as tf with tf.variable_scope("foo",initializer=tf.constant_initializer(0.4)): v=tf.get_variable("v",[1]) assert(v.eval()==0.4) w=tf.get_variable("w",[1],initializer=tf.constant_initializer(0.3)) assert(w.eval()==0.3) with tf.variable_scope("bar"): v=tf.get_variable("v",[1]) assert(v.eval()==0.4) with tf.variable_scope("baz",initializer=tf.constant_initializer(0.2)): v=tf.get_variable("v",[1]) assert(v.eval()==0.2)
上面讲的是variable_name,那对于op_name呢?在variable_scope作用域下的操作,也会加上前缀
import tensorflow as tf with tf.variable_scope("foo"): x=1+tf.get_variable("v",[1]) assert x.op.name=="foo/add"
批标准化
批标准化是为了克服神经网络层数加深导致难以训练而诞生的。我们知道,深度神经网络随着网络深度加深,训练起来会越来越困难,收敛速度会很慢,常常会导致梯度弥散。
统计机器学习中有一个ICS理论,这是一个经典假设:源域和目标域的数据分布是一致的。也就是说,训练数据和测试数据是满足相同分布的。这是通过训练数据获得的模型能够在测试集获得好的效果的一个基本保障
covariate shift是指训练集的样本数据和目标样本数据集分布不一致时,训练得到的模型无法很好地泛化。它是分布不一致假设之下地一个分支问题,也就是指源域和目标域的条件概率是一致的,但是其边缘概率不同。的确,对于神经网络的各层输出,在经过了层内操作之后,各层输出分布就会与对应的输入信号分布不同,而且差异会随着网络深度增大而加大,但是每一层所指向的样本标记仍然是不变的。
解决思路一般是根据训练样本和目标样本的比例对训练样本做一个矫正。因此,通过引入批标准化来规范化某些层或者所有层的输入,从而固定每层输入信号的均值与方差
方法
批标准化一般用在非线性映射(激活函数)之前,对x=Wu+b做规范化,使结果(输出信号各个维度)的均值为0,方差为1.让每一层的输入有一个稳定的分布会有利于网络的训练。
优点
批标准化通过规范化让激活函数分布在线性区间,结果就是加大了梯度,让模型更加大胆地进行梯度下降,于是有如下优点:
1.加大探索的步长,加快收敛的速度
2.更容易跳出局部最小值
3.破坏原来的数据分布,一定程度上缓解过拟合
因此,在遇到神经网络收敛速度很慢或者梯度爆炸等无法训练的情况下,都可以尝试用批标准化来解决。
示例:
fc_mean,fc_var=tf.nn.moments(Wx_plus_b,axes=[0],) scale=tf.Variable(tf.ones([out_size])) shift=tf.Variable(tf.zeros([out_size])) epsilon=0.001 Wx_plus_b=tf.nn.batch_normalization(Wx_plus_b,fc_mean,fc_var,shift,scale,epsilon) #也就是在做Wx_plus_b=(Wx_plus_b-fc_mean)/tf.sqrt(fc_var+0.001) #Wx_plus_b=Wx_plus_b*scale+shift
神经元函数及优化方法
激活函数
激活函数运行时激活神经网络中某一部分神经元,将激活系信息向后传入下一层的神经网络。神经网络之所以能解决非线性问题,本质上就是激活函数加入了非线性因素,弥补了线性模型的表达力,把“激活的神经元的特征”通过函数保留并映射到下一层。
因为神经网络的数学基础是处处可微,所以选取的激活函数要能保证数据输入与输出也是可微的。
激活函数不会更改输入数据的维度,也就是输入和输出的维度是相同的。有如下的激活函数:
tf.nn.relu()
tf.nn.sigmoid()
tf.nn.tanh()
tf.nn.elu()
tf.nn.bias_add()
tf.nn.crelu()
tf.nn.relu6()
tf.nn.softplus()
tf.nn.softsign()
dropout函数。一个神经元将以概率keep_prob决定是否被抑制。如果被抑制,该神经元的输出为0,如果不被抑制,那么该神经元的输出值将被放大到原来的1/keep_prob倍
tf.nn.dropout()
卷积函数
卷积函数是构建神经网络的重要支架,是在一批图像上扫描二维过滤器。
tf.nn.convolution(inputs,filter,padding,strides=None,dilation_rate=None,name=None,data_format=None)这个函数计算N维卷积和
tf.nn.conv2d(input,filter,strides,padding,use_cudnn_on_gpu=None,data_format=None,name=None)这个函数的作用是对一个四维的输入数据input和四维的卷积核filter进行操作,然后对输入数据进行一个二维的卷积操作,最后得到卷积之后的结果。
tf.nn.depthwise_conv2d(input,filter,strides,padding,rate=None,name=None,data_format=None)这个函数输入张量的数据维度是[batch,in_height,in_width,in_channels],卷积核的维度是[filter_height,filter_width,in_channels,channel_multiplier],在通道in_channels上面的卷积深度是1,depthwise_conv2d函数将不同的卷积核独立地应用在in_channels的每个通道上(从通道1到channel_multiplier),然后把所有结果进行汇总。最后输出通道的总数是in_channels*channel_multiplier
tf.nn.separable_conv2d(input,depthwise_filter,pointwise_filter,strides,padding,rate=None,name=None,data_format=None)是利用几个分离的卷积核去做卷积。在这个API中,将应用一个二维的卷积核,在每个通道上,以深度channel_multiplier进行卷积。
tf.nn.atrous_conv2d(value,filters,rate,padding,name=None)计算Atrous卷积,又称孔卷积或者扩张卷积
tf.nn.conv2d_transpose(value,filter,output_shape,strides,padding=’SAME’,data_format=’NHWC’,name=None)在解卷积网络中有时称为反卷积,但实际上是conv2d的转置
tf.nn.conv1d(value,filter,stride,padding,use_cudnn_on_gpu=None,data_format=None,name=None)这个函数与二维卷积类似。这个函数是用来计算给定三维的输入核过滤器的情况下的一维卷积。不同的是,它的输入是三维,如[batch,in_width,in_channels]。stride是一个正整数,代表卷积核向右移动每一步的长度。
tf.nn.conv3d(input,filter,strides,padding,name=None)和二维卷积类似,这个函数用来计算给定五维的输入和过滤器的情况下的三维卷积
池化函数
在神经网络中,池化函数一般跟在卷积函数的下一层
tf.nn.avg_pool()计算池化区域中元素的平均值
tf,nn.max_pool()计算池化区域中元素的最大值
tf.nn.max_pool_with_argmax()计算池化区域中元素的最大值和该最大值所在的位置
tf.nn.avg_pool3d()和tf.max_pool3d()分别是在三维下的平均池化和最大池化
tf.nn.fractional_avg_pool()和tf.nn.fractional_max_pool()分别是三维下的平均池化和最大池化
tf.nn.pool()执行一个n维的池化操作
分类函数
Tensorflow中常见的分类函数主要有:sigmoid_cross_entropy_with_logits、softmax、log_softmax、softmax_cross_entropy_with_logits等
tf.nn.sigmoid_cross_entropy_with_logits这个函数的输入要格外注意,如果采用此函数作为损失函数,在神经网络的最后一层不需要进行sigmoid运算。
tf.nn.softmax 计算softmax激活
log_softmax 计算log softmax激活
softmax_cross_entropy_with_logits
优化方法
如何加速神经网络的训练?Tensorflow提供了很多优化器
1.BGD法
批梯度下降,这种方法是利用现有参数对训练集中的每一个输入生成一个估计输出yi,然后跟实际输出yi比较,统计所有误差,求平均以后得平均误差,以此作为更新参数得依据
2.SGD法
随机梯度下降,这种方法是将数据集拆分成一个个批次,随机抽取一个批次来计算并更新参数。(1)由于抽取不可避免地梯度会有误差,需要手动调整学习率,但是选择合适地学习率又比较困难。尤其是在训练时,我们常常想对常出现地特征更新速度快一些,而对不常出现的特征更新速度慢一些,而SGD在更新参数时对所有参数采用一样的学习率,因此无法满足要求。(2)SGD容易收敛到局部最优,并且在某些情况下可能被困在鞍点
3.Momentum法
Momentum是模拟物理学中动量的概念,更新时在一定程度上保留之前的更新方向,利用当前的批次再微调本次的更新参数,因此引入一个新的变量v(速度),作为前几次梯度的累加。因此,Momentum能够更新学习率,再下降初期,前后梯度方向一致时,能够加速学习;在下降的中后期,在局部最小值的附近来回震荡时,能够抑制震荡,加快收敛。
4.Nesterov Momentum法
标准Momentum法首先计算一个梯度,然后在加速更新梯度的方向进行一个大的跳跃;Nesterov项首先在原来加速的梯度方向进行一个大的跳跃,然后再该位置计算梯度值,然后用这个梯度值修正最终的更新方向。
5.Adagrad法
Adagrad法能够自适应地为各个参数分配不同的学习率,能够控制每个维度的梯度方向。这种方法的优点是能够实现学习率的自动更改:如果本次更新时梯度大,学习率就衰减得快一些;如果这次更新时梯度小,学习率衰减得就慢一些。
6.Adadelta法
Adagrad法仍然存在一些问题,其学习率单调递减,在训练的后期学习率非常小,并且需要手动设置一个全局的初始化学习率。Adadelta法用一阶的方法,近似模拟二阶牛顿法,解决了这些问题
7.RMSprop法
RMSprop法与Momentum法类似,通过引入一个衰减系数,使每一回合都衰减一定比例,在实践中,对循环神经网络(RNN)效果很好
8.Adam法
Adam法根据损失函数针对每个参数的梯度的一阶矩估计和二阶矩估计动态调整每个参数的学习率。
模型的存储和加载
训练好一个神经网络后,我们希望能够将其应用在预测数据上。那么,如何把模型存储起来呢?同时,对于一个已经存储起来的模型,在将其应用在预测数据上时又如何加载呢?
Tensorflow的API提供了以下两种方式来存储和加载模型
(1)生成检查点文件,扩展为一般为.ckpt,通过在tf.train.Saver对象上调用Saver.save()生成。它包含权重和其他在程序中定义的变量,不包含图结构。如果需要在另一个程序中使用,需要重新创建图形结构,并告诉Tensorflow如何处理这些权重。
(2)生成图协议文件,这是一个二进制文件,扩展名一般为.pb,用tf.train.write_graph()保存,只包含图形结构,不包含权重,然后使用tf.import_graph_def()来加载图形。
模型的存储与加载
模型存储主要是建立一个tf.train.Saver()来保存变量,并且指定保存的位置,一般模型的扩展名为.ckpt。
存储模型
import os import tensorflow as tf from tensorflow.examples.tutorials.mnist import input_data def init_weight(shape): return tf.Variable(tf.random_normal(shape,stdde=0.01)) def model(X,w_h,w_h2,w_o,p_keep_input,p_keep_hidden): X=tf.nn.dropout(X,p_keep_input) h=tf.nn.relu(tf.matmul(X,w_h)) h=tf.nn.dropout(h,p_keep_hidden) h2=tf.nn.relu(tf.matmul(h,w_h2)) h2=tf.nn.dropout(h2,p_keep_hidden) return tf.matmul(h2,w_o) mnist=input_data.read_data_sets("MNIST_data/",one_hot=True) trX,trY,teX,teY=mnist.train.images,mnist.train.labels,mnist.test.images,mnist.test.labels X=tf.placeholder("float",[None,784]) Y=tf.placeholder("float",[None,10]) w_h=init_weight([784,625]) w_h2=init_weight([625,625]) w_o=init_weight([625,10]) p_keep_input=tf.placeholder("float") p_keep_hidden=tf.placeholder("float") py_x=model(X,w_h,w_h2,w_o,p_keep_input,p_keep_hidden) cost=tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(py_x,Y)) train_op=tf.train.RMSPropOptimizer(0.001,0.9).minimize(cost) predict_op=tf.argmax(py_x,1) ckpt_dir="./ckpt_dir" if not os.path.exists(ckpt_dir): os.makedirs(ckpt_dir) #定义一个计数器,为训练轮数计数 global_step=tf.Variable(0,name='global_step',trainable=False) saver=tf.train.Saver() non_storable_variable=tf.Variable(777) with tf.Session() as sess: tf.initialize_all_variables().run() start=global_step.eval() print("Start from:",start) for i in range(start,100): for start,end in zip(range(0,len(trX),128),range(128,len(trX)+1,128)): sess.run(train_op,feed_dict={X:trX[start:end],Y:trY[start:end],p_keep_input:0.8,p_keep_hidden:0.5}) global_step.assign(i).eval() saver.save(sess,ckpt_dir+"/model.ckpt",global_step=global_step)#存储模型
加载模型
import tensorflow as tf with tf.Session() as sess: tf.initialize_all_variables().run() ckpt=tf.train.get_checkpoint_state(ckpt_dir) if ckpt and ckpt.model_checkpoint_path: print(ckpt.model_checkpoint_path) saver.restore(sess,ckpt.model_checkpoint_path)#加载所有的参数 #从这里开始就可以直接使用模型进行预测,或者接着继续训练
图的存储与加载
当仅保存图模型时,才将图写入二进制协议文件中
import tensorflow as tf v=tf.Variable(0,name='my_variable') sess=tf.Session() tf.train.write_graph(sess.graph_def,'/temp/tfmodel','train.pbtxt') with tf.Session() as _sess: with gfile.FastGFile("/tmp/tfmodel/train.pbtxt","rb") as f: graph_def=tf.GraphDef() graph_def.ParseFromString(f.read()) _sess.graph.as_default() tf.import_graph_def(graph_def,name='tfgraph')
队列和线程
和Tensorflow中的其他组件一样,队列(queue)本身也是图中的一个节点,是一种有状态的节点,其他节点,如入队节点(enqueue)和出队节点(dequeue),可以修改它的内容。
队列
Tensorflow中主要有两种队列,即FIFOQueue和RandomShuffleQueue。
1.FIFOQueue
FIFOQueue创建一个先入先出队列。例如,我们在训练一些语音、文字样本时,使用循环神经网络的网络结构,希望读入的训练样本是有序的,就要用FIFOQueue
import tensorflow as tf 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]) with tf.Session() as sess: sess.run(init) quelen=sess.run(q.size()) for i in range(2):#执行2次操作 sess.run(q_inc) quelen=sess.run(q.size()) for i in range(quelen): print(sess.run(q.dequeue()))#输出队列的值
2.RandomShuffleQueue
RandomShuffleQueue创建一个随机队列,在出队列时,是以随机的顺序产生元素的。例如,我们在训练一些图像样本时,使用CNN的网络结构,希望可以无序地读入训练样本,就要用RandomShuffleQueue,每次随机产生一个训练样本。
RandomShuffleQueue在Tensorflow使用异步计算时非常重要。因为Tensorflow的会话是支持多线程的,我们可以在主线程里执行训练操作,使用RandomShuffleQueue作为训练输入,开多个线程来准备训练样本,将样本压入队列后,主线程会从队列中每次取出mini-batch的样本进行训练
q=tf.RandomShuffleQueue(capacity=10,min_after_dequeue=2,dtypes="float")#队列最大长度为10,出队后最小长度为2 sess=tf.Session() for i in range(0,10):#10次入队 sess.run(q.enqueue(i)) for i in range(0,8):#8次出队 print(sess.run(q.dequeue()))
我们尝试修改入队次数为12次,再运行,发现程序阻断不动,或者我们尝试修改出队此时为10次,即不保留队列最小长度,发现队列输出8次结果后,在终端仍然阻断了。
阻断一般发生在:
1.队列长度等于最小值,执行出队操作
2.队列长度等于最大值,执行入队操作
上面的例子都是在会话的主线程中进行入队操作。当数据量很大时,入队操作从硬盘中读取数据,放入内存中,主线程需要等待入队操作完成,才能进行训练操作。会话中可以运行多个线程,我们使用线程管理器QueueRunner创建一系列的新线程进行入队操作,让主线程继续使用数据,即训练网络和读取数据是异步的,主线程在训练网络,另一个线程在将数据从硬盘读入内存。
队列管理器
我们创建一个含有队列的图:
import tensorflow as tf 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) #启动一个会话,从队列管理器qr中创建线程 with tf.Session() as sess: sess.run(tf.global_variables_initializer()) enqueue_threads=qr.create_threads(sess,start=True)#启动入队线程 for i in range(10): print(sess.run(q.dequeue()))
结果
1.0 1.0 2.0 3.0 5.0 6.0 7.0 8.0 9.0 10.0 ERROR:tensorflow:Exception in QueueRunner: Run call was cancelled ERROR:tensorflow:Exception in QueueRunner: Session has been closed. Exception in thread Thread-22: Traceback (most recent call last): File "C:\Anaconda3\envs\tensorflow-gpu\lib\threading.py", line 914, in _bootstrap_inner self.run() File "C:\Anaconda3\envs\tensorflow-gpu\lib\threading.py", line 862, in run self._target(*self._args, **self._kwargs) File "C:\Anaconda3\envs\tensorflow-gpu\lib\site-packages\tensorflow\python\training\queue_runner_impl.py", line 238, in _run enqueue_callable() File "C:\Anaconda3\envs\tensorflow-gpu\lib\site-packages\tensorflow\python\client\session.py", line 1245, in _single_tensor_run fetch_list_as_strings, [], status, None) File "C:\Anaconda3\envs\tensorflow-gpu\lib\contextlib.py", line 66, in __exit__ next(self.gen) File "C:\Anaconda3\envs\tensorflow-gpu\lib\site-packages\tensorflow\python\framework\errors_impl.py", line 466, in raise_exception_on_not_ok_status pywrap_tensorflow.TF_GetCode(status)) tensorflow.python.framework.errors_impl.CancelledError: Run call was cancelled Exception in thread Thread-23: Traceback (most recent call last): File "C:\Anaconda3\envs\tensorflow-gpu\lib\threading.py", line 914, in _bootstrap_inner self.run() File "C:\Anaconda3\envs\tensorflow-gpu\lib\threading.py", line 862, in run self._target(*self._args, **self._kwargs) File "C:\Anaconda3\envs\tensorflow-gpu\lib\site-packages\tensorflow\python\training\queue_runner_impl.py", line 238, in _run enqueue_callable() File "C:\Anaconda3\envs\tensorflow-gpu\lib\site-packages\tensorflow\python\client\session.py", line 1235, in _single_operation_run target_list_as_strings, status, None) File "C:\Anaconda3\envs\tensorflow-gpu\lib\contextlib.py", line 66, in __exit__ next(self.gen) File "C:\Anaconda3\envs\tensorflow-gpu\lib\site-packages\tensorflow\python\framework\errors_impl.py", line 466, in raise_exception_on_not_ok_status pywrap_tensorflow.TF_GetCode(status)) tensorflow.python.framework.errors_impl.CancelledError: Session has been closed.
能输出结果,但最后会异常:
ERROR:tensorflow:Exception in QueueRunner: Run call was cancelled
ERROR:tensorflow:Exception in QueueRunner: Session has been closed.
我们知道,使用with tf.Session的话,会话执行结束会自动关闭,相当于main函数已经结束,
固也就有 Session has been closed.的错误。
import tensorflow as tf # 创建一个含有队列的图 q = tf.FIFOQueue(1000,"float") # 创建一个长度为1000的队列 counter = tf.Variable(0.0) # 计数器 increment_op = tf.assign_add(counter,tf.constant(1.0)) # 操作:给计数器加1 enqueque_op = q.enqueue(counter) # 操作:计数器值加入队列 # 创建一个队列管理器 QueueRunner,用这两个操作向队列 q 中添加元素,启动一个线程。 qr = tf.train.QueueRunner(q,enqueue_ops=[increment_op,enqueque_op]*1) # 启动一个会话,从队列管理器qr中创建线程 # 主线程 sess = tf.Session() sess.run(tf.global_variables_initializer()) enqueue_threads = qr.create_threads(sess,start=True) # 主线程 for i in range(10): print(sess.run(q.dequeue()))
使用Session就不会自动关闭,也就没有了上面例子中的异常了,虽然没有了异常,但也和我们
设想的会打印顺序的1,2,3,4,5…不一样,而且像第一个例子中还会重复打印1.0,这是为什么呢?
这个本质是+1操作和入队操作是异步的,也就是说如果加1操作执行了很多次之后,才执行一次入队的话,就会出现入队不是按我们预想的顺序那样;反过来,当我执行几次入队之后,才执行一次加1操作就会出现一个数重复入队的情况。
那该怎么解决这个问题呢!下面为几种解决的方法
# 方法1 import tensorflow as tf q = tf.FIFOQueue(1000,"float") counter = tf.Variable(0.0) increment_op = tf.assign_add(counter,tf.constant(1.0)) enqueque_op = q.enqueue(counter) # 把两个操作变成列表中的一个元素 # 原 :qr = tf.train.QueueRunner(q,enqueue_ops=[increment_op,enqueque_op]*1) qr = tf.train.QueueRunner(q,enqueue_ops=[[increment_op,enqueque_op]]*1) sess = tf.Session() sess.run(tf.global_variables_initializer()) enqueue_threads = qr.create_threads(sess,start=True) for i in range(10): print(sess.run(q.dequeue()))
1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0
# 方法2 import tensorflow as tf q = tf.FIFOQueue(1000,"float") counter = tf.Variable(0.0) increment_op = tf.assign_add(counter,tf.constant(1.0)) # 原 enqueque_op = q.enqueue(counter) # 把加一操作变成入队操作的依赖 with tf.control_dependencies([increment_op]): enqueque_op = q.enqueue(counter) # 由于将加1变成了入队的依赖,所以入队操作只需要传入enqueque_op就行了 qr = tf.train.QueueRunner(q,enqueue_ops=[enqueque_op]*1) sess = tf.Session() sess.run(tf.global_variables_initializer()) enqueue_threads = qr.create_threads(sess,start=True) for i in range(10): print(sess.run(q.dequeue()))
1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0
# 方法3 import tensorflow as tf q = tf.FIFOQueue(1000,"float") counter = tf.Variable(0.0) increment_op = tf.assign_add(counter,tf.constant(1.0)) enqueque_op = q.enqueue(counter) # 把两个操作变成空操作的依赖 with tf.control_dependencies([increment_op,enqueque_op]): void_op = tf.no_op() # 由于将两个操作变成了空操作的依赖,所以入队操作只需要传入void_op就行了 qr = tf.train.QueueRunner(q,enqueue_ops=[void_op]*1) sess = tf.Session() sess.run(tf.global_variables_initializer()) enqueue_threads = qr.create_threads(sess,start=True) for i in range(10): print(sess.run(q.dequeue()))
1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0
# 方法4 import tensorflow as tf q = tf.FIFOQueue(1000,"float") counter = tf.Variable(0.0) increment_op = tf.assign_add(counter,tf.constant(1.0)) enqueque_op = q.enqueue(counter) # 原 :qr = tf.train.QueueRunner(q,enqueue_ops=[increment_op,enqueque_op]*1) # 用tf.group()把两个操作组合起来 qr = tf.train.QueueRunner(q,enqueue_ops=[tf.group(increment_op,enqueque_op)]*1) sess = tf.Session() sess.run(tf.global_variables_initializer()) enqueue_threads = qr.create_threads(sess,start=True) for i in range(10): print(sess.run(q.dequeue()))
1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0
线程和协调器
QueueRunner 有一个问题就是:入队线程自顾自地执行,在需要的出队操作完成之后,程序没法结束。这样就要使用 tf.train.Coordinator 来实现线程间的同步,终止其他线程。
import tensorflow as tf q = tf.FIFOQueue(1000,"float") counter = tf.Variable(0.0) increment_op = tf.assign_add(counter,tf.constant(1.0)) enqueque_op = q.enqueue(counter) qr = tf.train.QueueRunner(q,enqueue_ops=[[increment_op,enqueque_op]]*1) # 主线程 sess = tf.Session() sess.run(tf.global_variables_initializer()) #coordinator:协调器,协调线程间的关系可以被当做一种信号量,起同步作用 coord = tf.train.Coordinator() # 启动入队线程,协调器是线程的参数 enqueue_threads = qr.create_threads(sess,coord=coord,start=True) # 主线程 for i in range(0,10): print(sess.run(q.dequeue())) coord.request_stop() # 通知其他线程关闭 # join操作等待其他线程结束,其他所有的线程关闭后,这个函数才能返回 coord.join(enqueue_threads)
在关闭队列线程后,再执行出队操作,就会抛出 tf.errors.OutOfRange 错误。这种情况就需要
使用 tf.errors.OutOfRangeError 来捕捉错误,终止循环:
import tensorflow as tf q = tf.FIFOQueue(1000,"float") counter = tf.Variable(0.0) increment_op = tf.assign_add(counter,tf.constant(1.0)) enqueque_op = q.enqueue(counter) qr = tf.train.QueueRunner(q,enqueue_ops=[[increment_op,enqueque_op]]*1) # 主线程 sess = tf.Session() sess.run(tf.global_variables_initializer()) #coordinator:协调器,协调线程间的关系可以被当做一种信号量,起同步作用 coord = tf.train.Coordinator() # 启动入队线程,协调器是线程的参数 enqueue_threads = qr.create_threads(sess,coord=coord,start=True) coord.request_stop() # 通知其他线程关闭 # 主线程 for i in range(0,10): try: print("i : ",i) print(sess.run(q.dequeue())) except tf.errors.OutOfRangeError: print('finish') break # join操作等待其他线程结束,其他所有的线程关闭后,这个函数才能返回 coord.join(enqueue_threads)
i : 0 1.0 i : 1 finish
说明:从打印出来的信息我们可以看出,将请求线程关闭放置在出队的前面,也就是说我还没有出队之前就请求将线程关闭了,但关闭线程需要一定的时间,所以后来在遍历出队是还是可以执行的线程关闭后,如果不抛异常的话就像上个例子那样会报错,所以这里执行了异常,并打印出了“finish