TF学习(一)

TensorFlow™(以下简称TF)是一个采用数据流图(data flow graphs),用于数值计算的开源软件库。节点(Nodes)在图中表示数学操作,图中的线(edges)则表示在节点间相互联系的多维数据数组,即张量(tensor)。它灵活的架构让你可以在多种平台上展开计算,例如台式计算机中的一个或多个CPU(或GPU),服务器,移动设备等等。

(本博客主要介绍TF1.5. 而TF2.x与TF1.x间并不兼容)

Tensorflow官网:https://tensorflow.google.cn/

Tensorflow中文社区:http://tensorfly.cn/

基础知识

数据流图(Data Flow Graph)

数据流图用“结点”(nodes)和“线”(edges)的有向图来描述数学计算。“节点” 一般用来表示施加的数学操作,但也可以表示数据输入(feed in)的起点/输出(push out)的终点,或者是读取/写入持久变量(persistent
variable)的终点。“线”表示“节点”之间的输入/输出关系。这些数据“线”可以输运“size可动态调整”的多维数据数组,即“张量”(tensor)。张量从图中流过的直观图像是这个工具取名为“TF”的原因。一旦输入端的所有张量准备好,节点将被分配到各种计算设备完成异步并行地执行运算。

avatar

设计理念

  1. 将图的定义与图的运行完全分开,采用符号式编程。符号式计算一般是先定义各种变量,然后建立一个数据流图,在数据流图中规定各个变量之间的计算关系,最后需要对数据流图进行编译,但此时的数据流图还是一个空壳儿,里面没有任何实际数据,只有把需要运算的输入放进去后,才能在整个模型中形成数据流,从而形成输出值。

  2. 将涉及的运算都存放在图中,而图的运行只发生在会话中(session)中。当开启会话后,就可以用数据去填充节点,进行运算;关闭会话后,就不能进行计算了。因此,会话提供了操作运行和Tensor求值的环境。

TensorFlow的边有两种连接关系:数据依赖和控制依赖。

实线边表示数据依赖,代表数据,即张量。任意维度的数据统称为张量。在机器学习算法中,张量在数据流图中从前往后流动一遍就完成了一次前向传播 (forword propagation),而残差从后向前流动一遍就完成了一次反向传播
(backword propagation)。

还有一种特殊边,一般画为虚线边,称为控制依赖 (control dependency),可以用于控制操作的运行,这被用来确保happens-before关系,这类边上没有数据流过,但源节点必须在目的节点开始执行前完成执行。

节点

图中的节点又称为算子它代表一个操作(operation,OP),一般用来表示施加的数学运算,也可以表示数据输入 (feed in)的起点以及输出 (push out)的终点,或者是读取/写入持久变量 (persistent
variable)的终点。算子支持张量的各种数据属性,并且需要在建立图的时候确定下来。

构建图的第一步是创建各个节点。

1
2
3
4
5
6
7
8
import tensorflow as tf
# 创建一个常量运算操作,产生一个 1×2 矩阵
matrix1 = tf.constant([[3., 3.]])
# 创建另外一个常量运算操作,产生一个 2×1 矩阵
matrix2 = tf.constant([[2.],[2.]])
# 创建一个矩阵乘法运算 ,把matrix1和matrix2作为输入
# 返回值product代表矩阵乘法的结果
product = tf.matmul(matrix1, matrix2)

会话

启动图的第一步是创建一个Session对象。会话(session)提供在图中执行操作的一些方法。一般的模式是,建立会话,此时会生成一张空图,在会话中添加节点和边,形成一张图,然后执行。

要创建一张图并运行操作的类,在Python的API中使用tf. Session,在C++ 的API中使用tensorflow:: Session。示例如下:

1
2
3
4
with tf.Session() as sess:
result = sess.run([product])
print (result)

在调用Session对象的run()方法来执行图时,传入一些Tensor,这个过程叫填充 (feed);返回的结果类型根据输入的类型而定,这个过程叫取回 (fetch)。

设备

设备 (device)是指一块可以用来运算并且拥有自己的地址空间的硬件,如GPU和CPU。TensorFlow为了实现分布式执行操作,充分利用计算资源,可以明确指定操作在哪个设备上执行。具体如下:

1
2
3
4
5
6
with tf.Session() as sess:
# 指定在第二个gpu上运行
with tf.device("/gpu:1"):
matrix1 = tf.constant([[3., 3.]])
matrix2 = tf.constant([[2.],[2.]])
product = tf.matmul(matrix1, matrix2)

变量

变量 (variable)是一种特殊的数据,它在图中有固定的位置,不像普通张量那样可以流动。

1
2
3
4
# 创建一个变量,初始化为标量0
state = tf.Variable(0, name="counter")
#创建一个常量张量
input1 = tf.constant(3.0)

TensorFlow 还提供了填充机制,可以在构建图时使用tf.placeholder()临时替代任意操作的 张量,在调用Session对象的run()方法去执行图时,使用填充数据作为调用的参数,调用结束 后,填充数据就消失。代码示例如下:

1
2
3
4
5
6
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.]})
# 输出 [array([ 14.], dtype=float32)]

内核

我们知道操作 (operation)是对抽象操作(如matmul或者add)的一个统称,而内核
(kernel)则是能够运行在特定设备(如CPU、GPU)上的一种对操作的实现。因此,同一个操作可能会对应多个内核。当自定义一个操作时,需要把新操作和内核通过注册的方式添加到系统中。

常用API

图(tf. Graph)

tf. Graph类中包含一系列表示计算的操作对象(tf. Operation),以及在操作之间流动的数据——张量对象(tf. Tensor)

avatar

操作对象/节点(tf. Operation)

用于计算张量数据,由节点构造器(如tf.matmul()或者Graph.create_op())产生.

avatar

张量对象(tf. Tensor)

tf. Tensor类是操作输出的符号句柄,它不包含操作输出的值,而是提供了一种在tf.
Session中计算这些值的方法。这样就可以在操作之间构建一个数据流连接,使TensorFlow能够执行一个表示大量多步计算的图形。与张量相关的API均位于tf. Tensor类中

avatar

可视化

可视化时,需要在程序中给必要的节点添加摘要 (summary),摘要会收集该节点的数据,并标记上第几步、时间戳等标识,写入事件文件 (event file)中。tf.summary.
FileWriter类用于在目录中创建事件文件,并且向文件中添加摘要和事件,用来在TensorBoard中展示。

avatar

变量作用域

在TensorFlow中有两个作用域 (scope),一个是name_scope,另一个是variable_scope。

variable_scope主要是给variable_name加前缀,也可以给op_name加前缀;name_scope是给op_name加前缀。

variable_scope

1
2
v = tf.get_variable(name, shape, dtype, initializer) # 通过所给的名字创建或是返回一个变量
tf.variable_scope(<scope_name>) # 为变量指定命名空间

当tf.get_variable_scope().reuse == False时,variable_scope作用域只能用来创建新变量

当tf.get_variable_scope().reuse == True时,作用域可以共享变量

1. 获取变量作用域

可以直接通过tf.variable_scope()来获取变量作用域

如果在开启的一个变量作用域里使用之前预先定义的一个作用域,则会跳过当前变量的作用域,保持预先存在的作用域不变。

1
2
3
4
5
6
7
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" # 保持不变
2. 变量作用域的初始化

变量作用域可以默认携带一个初始化器,在这个作用域中的子作用域或变量都可以继承或者重写父作用域初始化器中的值。

1
2
3
4
5
6
7
8
9
10
11
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 # 重写父作用域的初始化器的值
op_name

那对于op_name, 在variable_scope作用域下的操作,也会被加上前缀:

1
2
3
with tf.variable_scope("foo"):
x = 1.0 + tf.get_variable("v", [1])
assert x.op.name == "foo/add"

name_scope

ensorFlow中常常会有数以千计的节点,在可视化的过程中很难一下子展示出来,因此用name_scope为变量划分范围,在可视化中,这表示在计算图中的一个层级。name_scope会影响op_name,不会影响用get_variable()
创建的变量,而会影响通过Variable()创建的变量。

1
2
3
4
5
6
7
8
with tf.variable_scope("foo"):
with tf.name_scope("bar"):
v = tf.get_variable("v", [1])
b = tf.Variable(tf.zeros([1]), name='b')
x = 1.0 + v
assert v.name == "foo/v:0"
assert b.name == "foo/bar/b:0"
assert x.op.name == "foo/bar/add"

批标准化(BN)

ICS(Internal Covariate Shift)理论源域(source domain)和目标域 (target domain)的数据分布 是一致的。

Covariate Shift是指训练集的样本数据和目标样本集分布不一致时,训练得到的模型无法很好地泛化
(generalization)。它是分布不一致假设之下的一个分支问题,也就是指源域和目标域的条件概率是一致的,但是其边缘概率不同。的确,对于神经网络的各层输出,在经过了层内操作后,各层输出分布就会与对应的输入信号分布不同,而且差异会随着网络深度增大而加大,但是每一层所指向的样本标记
(label)仍然是不变的。(常常会导致梯度弥散问题 (vanishing gradient problem)。使训练起来会越来越困难,收敛速度会很慢)

解决思路一般是根据训练样本和目标样本的比例对训练样本做一个矫正。因此,通过引入批标准化来规范化某些层或者所有层的输入,从而固定每层输入信号的均值与方差:一般用在非线性映射(激活函数)之前,对x =Wu +b
做规范化,使结果(输出信号各个维度)的均值为0,方差为1。让每一层的输入有一个稳定的分布会有利于网络的训练。

示例

1
2
3
4
5
6
7
8
9
10
11
12
# 对每层的Wx_plus_b进行批标准化,这个步骤放在激活函数之前

# 计算Wx_plus_b的均值和方差,其中axes=[0]表示想要标准化的维度
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

神经元函数及优化方法

激活函数(activation function)

激活函数运行时激活神经网络中某一部分神经元,将激活信息向后传入下一层的神经网络。

神经网络的数学基础是处处可微的,所以选取的激活函数要能保证数据输入与输出也是可微的。同时,激活函数不会更改输入数据的维度,也就是输入和输出的维度是相同的。TF中的激活函数如下:

1
2
3
4
5
6
7
8
9
10
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()
tf.nn.dropout() # 防止过拟合,用来舍弃某些神经元

sigmoid函数

1
2
3
a = tf.constant([[1.0, 2.0], [1.0, 2.0], [1.0, 2.0]])
sess = tf.Session()
print sess.run(tf.sigmoid(a))

avatar

sigmoid函数的优点在于,它的输出映射在(0, 1)内,单调连续,非常适合用作输出层,并且求导比较容易。但是,它也有缺点,因为软饱和性,一旦输入落入饱和区,f ‘ (x )就会变得接近于0,很容易产生梯度消失 。

tanh函数

avatar

tanh函数也具有软饱和性。因为它的输出以0为中心,收敛速度比sigmoid要快。但是仍无法解决梯度消失的问题。

relu函数

relu:f (x )=max(x , 0)

softplus:f (x )=log(1+exp(x ))

avatar

relu在x < 0时硬饱和。由于x>0时导数为1,所以,relu能够在x >
0时保持梯度不衰减,从而缓解梯度消失问题,还能够更快地收敛,并提供了神经网络的稀疏表达能力。但是,随着训练的进行,部分输入会落到硬饱和区,导致对应的权重无法更新,称为“神经元死亡”。

dropout函数

一个神经元将以概率keep_prob决定是否被抑制。如果被抑制,该神经元的输出就为0;如果不被抑制,那么该神经元的输出值将被放大到原来的1/keep_prob倍。

在默认情况下,每个神经元是否被抑制是相互独立的。但是否被抑制也可以通过noise_shape来调节。

当noise_shape[i] == shape(x)[i]时,x中的元素是相互独立的。如果shape(x)= [k, l, m, n],x中的维度的顺序分别为批、行、列和通道,如果noise_shape = [k, 1, 1, n]
,那么每个批和通道都是相互独立的,但是每行和每列的数据都是关联的,也就是说,要不都为0,要不都还是原来的值。

1
2
3
4
5
6
a = tf.constant([[-1.0, 2.0, 3.0, 4.0]])
with tf.Session() as sess:
b = tf.nn.dropout(a, 0.5, noise_shape = [1,4])
print (sess.run(b))
b = tf.nn.dropout(a, 0.5, noise_shape = [1,1])
print (sess.run(b))

激活函数的选择

当输入数据特征相差明显时,用tanh的效果会很好,且在循环过程中会不断扩大特征效果并显示出来。

当特征相差不明显时,sigmoid效果比较好。

同时,用sigmoid和tanh作为激活函数时,需要对输入进行规范化,否则激活后的值全部都进入平坦区,隐层的输出会全部趋同,丧失原有的特征表达。

而relu会好很多,有时可以不需要输入规范化来避免上述情况。

因此,现在大部分的卷积神经网络都采用relu作为激活函数。大概有85%~90%的神经网络会采用ReLU,10%~15%的神经网络会采用tanh,尤其用在自然语言处理上。

avarat

卷积函数

卷积函数是构建神经网络的重要支架,是在一批图像上扫描的二维过滤器。

tf.nn.convolution(input, filter, padding, strides=None, dilation_rate=None, name=None, data_format =None)

计算N维卷积的和

输入:

  • input:一个Tensor。数据类型必须是float32或者float64
  • filter:一个Tensor。数据类型必须是input相同
  • strides: strides: Optional. Sequence of N ints >= 1. Specifies the output stride. Defaults to [1]*N. If any value of
    strides is > 1, then all values of dilation_rate must be 1.
  • padding:一个字符串,取值为SAME或者VALID;padding=’SAME’:仅适用于全尺寸操作,即输入数据维度和输出数据维度相同;padding=’VALID:适用于部分窗口,即输入数据维度和输出数据维度不同
  • name:(可选)为这个操作取一个名字

输出:一个Tensor,数据类型是input相同

tf.nn.conv2d(input, filter, strides, padding, use_cudnn_on_gpu=None, data_format=None, name=None)

对一个四维的输入数据input和四维的卷积核filter进行操作,然后对输入数据进行一个二维的卷积操作,最后得到卷积之后的结果

输入:

  • strides:一个长度是4的一维整数类型数组,每一维度对应的是input中每一维的对应移动步数,比如,strides[1]对应input[1]的移动步数
  • use_cudnn_on_gpu:一个可选布尔值,默认情况下是True

输出:一个Tensor,数据类型是input相同

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)

应用一个二维的卷积核,在每个通道上,以深度channel_multiplier进行卷积。

输入:

  • depthwise_filter:一个张量。数据维度是四维[filter_height, filter_width, in_channels, channel_multiplier]。其中,in_channels的卷积深度是1
  • pointwise_filter:一个张量。数据维度是四维[1, 1, channel_multiplier * in_channels, out_channels]
    。其中,pointwise_filter是在depthwise_filter卷积之后的混合卷积

tf.nn.atrous_conv2d(value, filters, rate, padding, name=None)

计算Atrous卷积,又称孔卷积或者扩张卷积

输入:

  • rate:正整数int32。我们跨height和跨width维度采样输入值的跨度。等效地,我们通过在height和 width维度上插入零来对滤波器值进行升采样的速率。

tf.nn.conv2d_transpose(value, filter, output_shape, strides, padding=’SAME’, data_format=’NHWC’, name=None)

在解卷积网络(deconvolutional network)中有时称为“反卷积”,但实际上是conv2d的转置,而不是实际的反卷积。

输入:

  • output_shape:一维的张量,表示反卷积运算后输出的形状

输出:和value一样维度的Tensor

)tf.nn.conv1d(value, filters, stride, padding, use_cudnn_on_gpu=None, data_format=None, name=None)

计算给定三维的输入和过滤器的情况下的一维卷积。

输入:

  • value:[batch, in_width, in_channels]。
  • filter: 卷积核的维度也是三维,少了一维filter_height,如 [filter_width, in_channels, out_channels]。
  • stride: 正整数,代表卷积核向右移动每一步的长度。

tf.nn.conv3d(input, filter, strides, padding, name=None)

计算给定五维的输入和过滤器的情况下的三维卷积

输入:(与二维卷积相对比)

  • input的shape中多了一维in_depth,形状为Shape[batch, in_depth, in_height, in_width, in_channels];
  • filter的shape中多了一维filter_depth,由filter_depth, filter_height, filter_width构成了卷积核的大小;
  • strides中多了一维,变为[strides_batch, strides_depth, strides_height, strides_width, strides_channel],必须保证strides[0] =
    strides[4] = 1

tf.nn.conv3d_transpose(value, filter, output_shape, strides, padding=’SAME’, name=None)

与二维反卷积类似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
>>> input_data = tf.Variable( np.random.rand(10,9,9,3), dtype = np.float32 )
>>> filter_data = tf.Variable( np.random.rand(2, 2, 3, 2), dtype = np.float32)

>>> y = tf.nn.convolution(input_data,filter_data,padding='SAME',strides=[1,1])
>>> print(y)
Tensor("convolution_4:0", shape=(10, 9, 9, 2), dtype=float32)

>>> y = tf.nn.conv2d(input_data,filter_data,padding='SAME',strides=[1,1,1,1])
>>> print(y)
Tensor("Conv2D_1:0", shape=(10, 9, 9, 2), dtype=float32)

>>> y = tf.nn.depthwise_conv2d(input_data,filter_data,padding='SAME',strides=[1,1,1,1])
>>> print(y)
Tensor("depthwise_1:0", shape=(10, 9, 9, 6), dtype=float32)

>>> input_data = tf.Variable( np.random.rand(10, 9, 9, 3), dtype = np.float32 )
>>> depthwise_filter = tf.Variable( np.random.rand(2, 2, 3, 5), dtype = np.float32)
>>> pointwise_filter = tf.Variable( np.random.rand(1, 1, 15, 20), dtype = np.float32)
>>> # out_channels >= channel_multiplier * in_channels
>>> y = tf.nn.separable_conv2d(input_data, depthwise_filter, pointwise_filter,strides = [1, 1, 1, 1], padding = 'SAME')
>>> print(y)
Tensor("separable_conv2d_1:0", shape=(10, 9, 9, 20), dtype=float32)

>>> input_data = tf.Variable( np.random.rand(1,5,5,1), dtype = np.float32 )
>>> filters = tf.Variable( np.random.rand(3,3,1,1), dtype = np.float32)
>>> y = tf.nn.atrous_conv2d(input_data, filters, 2, padding='SAME')
>>> print(y)
Tensor("convolution_6/BatchToSpaceND:0", shape=(1, 5, 5, 1), dtype=float32)

>>> x = tf.random_normal(shape=[1,3,3,1])
>>> kernel = tf.random_normal(shape=[2,2,3,1])
>>> y = tf.nn.conv2d_transpose(x,kernel,output_shape=[1,5,5,3],strides=[1,2,2,1],padding="SAME")
>>> print(y)
Tensor("conv2d_transpose:0", shape=(1, 5, 5, 3), dtype=float32)

池化函数

池化操作是利用一个矩阵窗口在张量上进行扫描,将每个矩阵窗口中的值通过取最大值或平均值来减少元素个数。每个池化操作的矩阵窗口大小是由ksize指定的,并且根据步长strides决定移动步长。

tf.nn.avg_pool(value, ksize, strides, padding, data_format=’NHWC’, name=None)

计算池化区域中元素的平均值

输入:

  • value:一个四维的张量。数据维度是[batch, height, width, channels]
  • ksize:一个长度不小于4的整型数组。每一位上的值对应于输入数据张量中每一维的窗口对应值
  • strides:一个长度不小于4的整型数组。该参数指定滑动窗口在输入数据张量每一维上的步长
  • padding:一个字符串,取值为SAME或者VALID
  • data_format: ‘NHWC’代表输入张量维度的顺序,N为个数,H为高度,W为宽度,C为通道数(RGB三通道或者灰度单通道)
  • name(可选):为这个操作取一个名字

输出:一个张量,数据类型和value相同

1
2
3
4
5
6
7
>>> input_data = tf.Variable( np.random.rand(10,6,6,3), dtype = np.float32 )
>>> filter_data = tf.Variable( np.random.rand(2, 2, 3, 10), dtype = np.float32)
>>> y = tf.nn.conv2d(input_data, filter_data, strides = [1, 1, 1, 1], padding = 'SAME')
>>> output = tf.nn.avg_pool(value = y, ksize = [1, 2, 2, 1], strides = [1, 1, 1, 1],padding ='SAME')
>>> print(output)
Tensor("AvgPool:0", shape=(10, 6, 6, 10), dtype=float32)
#计算输出维度的方法是:shape(output)= (shape(value) - ksize + 1) / strides。

tf.nn.max_pool(value, ksize, strides, padding, data_format=’NHWC’, name=None)

计算池化区域中元素的最大值

1
2
3
4
5
6
7
>>> input_data = tf.Variable( np.random.rand(10,6,6,3), dtype = np.float32 )
>>> filter_data = tf.Variable( np.random.rand(2, 2, 3, 10), dtype = np.float32)
>>> y = tf.nn.conv2d(input_data, filter_data, strides = [1, 1, 1, 1], padding = 'SAME')
>>> output = tf.nn.max_pool(value = y, ksize = [1, 2, 2, 1], strides = [1, 1, 1, 1],
... padding ='SAME')
>>> print(output)
Tensor("MaxPool:0", shape=(10, 6, 6, 10), dtype=float32)

tf.nn.max_pool_with_argmax(input, ksize, strides, padding, Targmax = None, name=None)

计算池化区域中元素的最大值和该最大值所在的位置

该函数只能在GPU下运行,在CPU下没有对应的函数实现

1
2
3
4
5
input_data = tf.Variable( np.random.rand(10,6,6,3), dtype = tf.float32 )
filter_data = tf.Variable( np.random.rand(2, 2, 3, 10), dtype = np.float32)
y = tf.nn.conv2d(input_data, filter_data, strides = [1, 1, 1, 1], padding = 'SAME')
output, argmax = tf.nn.max_pool_with_argmax(input = y, ksize = [1, 2, 2, 1],strides = [1, 1, 1, 1], padding = 'SAME')
#返回结果是一个张量组成的元组(output, argmax),output表示池化区域的最大值;argmax的数据类型是Targmax,维度是四维

tf.nn.avg_pool3d()和tf.nn.max_pool3d()

三维下的平均池化和最大池化

tf.nn.fractional_avg_pool()和tf.nn.fractional_max_pool()

三维下的平均池化和最大池化。

tf.nn.pool(input, window_shape, pooling_type, padding, dilation_rate=None, strides=None, name=None, data_format=None)

执行一个N维的池化操作

分类函数

tf.nn.sigmoid_cross_entropy_with_logits(logits, targets, name=None)

输入:

  • logits:[batch_size, num_classes], targets:[batch_size, size].logits用最后一层的输入即可

最后一层不需要进行sigmoid运算,此函数内部进行了sigmoid操作

输出:loss [batch_size, num_classes]

tf.nn.softmax(logits, dim=-1, name=None)

计算Softmax激活,也就是softmax=exp(logits)/reduce_sum(exp(logits), dim)

tf.nn.log_softmax(logits, dim=-1, name=None)

计算log softmax激活,也就是logsoftmax =logits - log(reduce_sum(exp(logits), dim))

tf.nn.softmax_cross_entropy_with_logits(_sentinel=None, labels=None, logits=None, dim=-1, name =None)

输入:

  • logits and labels 均为[batch_size, num_classes]

输出:loss [batch_size],里面保存是batch中每个样本的交叉熵

tf.nn.sparse_softmax_cross_entropy_with_logits(logits, labels, name=None)

输入:

  • logits: [batch_size, num_classes] labels: [batch_size],必须在[0, num_classes]

logits是神经网络最后一层的结果

输出:loss [batch_size],里面保存是batch中每个样本的交叉熵

优化方法

重点介绍以下8个优化器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 梯度下降法(BGD和SGD)
class tf.train.GradientDescentOptimizer

# Adadelta法
class tf.train.AdadeltaOptimizer

# Adagrad法(Adagrad和AdagradDAO)
class tf.train.AdagradOptimizer
class tf.train.AdagradDAOptimizer

# Momentum法(Momentum和Nesterov Momentum)
class tf.train.MomentumOptimizer

# Adam法
class tf.train.AdamOptimizer

# Ftrl法
class tf.train.FtrlOptimizer

# RMSProp法
class tf.train.RMSPropOptimizer

BGD、SGD、Momentum和Nesterov Momentum是手动指定学习率的,其余算法能够自动调节学习率。

BGD法

BGD的全称是batch gradient
descent,即批梯度下降。这种方法是利用现有参数对训练集中的每一个输入生成一个估计输出yi,然后跟实际输出yi比较,统计所有误差,求平均以后得到平均误差,以此作为更新参数的依据。它的迭代过程为:

  • (1)提取训练集中的所有内容{x 1 , …, x n },以及相关的输出yi;

  • (2)计算梯度和误差并更新参数。

这种方法的优点是,使用所有训练数据计算,能够保证收敛,并且不需要逐渐减少学习率;缺点是,每一步都需要使用所有的训练数据,随着训练的进行,速度会越来越慢。

SGD法

SGD的全称是stochastic gradient descent,即随机梯度下降。因为这种方法的主要思想是将数据集拆分成一个个批次(batch),随机抽取一个批次来计算并更新参数,所以也称为MBGD(minibatch gradient
descent)。SGD在每一次迭代计算mini-batch的梯度,然后对参数进行更新。

与BGD相比,SGD在训练数据集很大时,仍能以较快的速度收敛。

但是,它仍然会有下面两个缺点:

  • (1)由于抽取不可避免地梯度会有误差,需要手动调整学习率 (learning
    rate),但是选择合适的学习率又比较困难。尤其在训练时,我们常常想对常出现的特征更新速度快一些,而对不常出现的特征更新速度慢一些,而SGD在更新参数时对所有参数采用一样的学习率,因此无法满足要求。

  • (2)SGD容易收敛到局部最优,并且在某些情况下可能被困在鞍点。

Momentum法

Momentum是模拟物理学中动量的概念,更新时在一定程度上保留之前的更新方向,利用当前的批次再微调本次的更新参数,因此引入了一个新的变量v(速度),作为前几次梯度的累加。因此,Momentum能够更新学习率,在下降初期,前后梯度方向一致时,能够加速学习;在下降的中后期,在局部最小值的附近来回震荡时,能够抑制震荡,加快收敛。

Nesterov Momentum法

标准Momentum法首先计算一个梯度(短的1号线),然后在加速更新梯度的方向进行一个大的跳跃(长的1号线);Nesterov项首先在原来加速的梯度方向进行一个大的跳跃(2号线),然后在该位置计算梯度值(3号线),然后用这个梯度值修正最终的更新方向(4号线)。

avarat

Adagrad法

Adagrad法能够自适应地为各个参数分配不同的学习率,能够控制每个维度的梯度方向。这种方法的优点是能够实现学习率的自动更改:如果本次更新时梯度大,学习率就衰减得快一些;如果这次更新时梯度小,学习率衰减得就慢一些。

Adadelta法

Adagrad法仍然存在一些问题:其学习率单调递减,在训练的后期学习率非常小,并且需要手动设置一个全局的初始学习率。Adadelta法用一阶的方法,近似模拟二阶牛顿法,解决了这些问题。

RMSprop法

RMSProp法与Momentum法类似,通过引入一个衰减系数,使每一回合都衰减一定比例。在实践中,对循环神经网络(RNN)效果很好。

Adam法

Adam的名称来源于自适应矩估计(adaptive moment estimation)。Adam法根据损失函数针对每个参数的梯度的一阶矩估计和二阶矩估计动态调整每个参数的学习率。

各个方法的比较

在不怎么调整参数的情况下,Adagrad法比SGD法和Momentum法更稳定,性能更优;精调参数的情况下,精调的SGD法和Momentum法在收敛速度和准确性上要优于Adagrad法

各个优化器的损失值比较结果

avarat

各个优化器的测试准确率比较

avarat

各个优化器的训练准确率比较

avarat

模型的存储与加载

TensorFlow的API提供了以下两种方式来存储和加载模型。

  • (1)生成检查点文件 (checkpoint file),扩展名一般为.ckpt,通过在tf.train. Saver对象上调用Saver.save()
    生成。它包含权重和其他在程序中定义的变量,不包含图结构。如果需要在另一个程序中使用,需要重新创建图形结构,并告诉TensorFlow如何处理这些权重。

  • (2)生成图协议文件(graph proto file),这是一个二进制文件,扩展名一般为.pb,用tf.trainwrite_graph()保存,只包含图形结构,不包含权重,然后使用tf.import_graph_def()
    来加载图形。

训练模型及存储模型过程

1. 我们定义一个存储路径,这里就用当前路径下的ckpt_dir目录

1
2
3
ckpt_dir = "./ckpt_dir"
if not os.path.exists(ckpt_dir):
os.makedirs(ckpt_dir)

2. 定义一个计数器,为训练轮数计数

1
2
# 计数器变量,设置它的trainable=False,不需要被训练
global_step = tf.Variable(0, name='global_step', trainable=False)

3. 当定义完所有变量后,调用tf.train. Saver()来保存和提取变量,其后面定义的变量将不会被存储

1
2
3
4
# 在声明完所有变量后,调用tf.train.Saver
saver = tf.train.Saver()
# 位于tf.train.Saver之后的变量将不会被存储
non_storable_variable = tf.Variable(777)

4. 训练模型并存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
with tf.Session() as sess:
tf.initialize_all_variables().run()

start = global_step.eval() # 得到global_step的初始值
print("Start from:", start)

for i in range(start, 100):
# 以128作为batch_size
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) # 存储模型

在训练的过程中,ckpt_dir下会出现16个文件,其中有5个model.ckpt-{n}.data-00000-of-00001文件,是训练过程中保存的模型,5个model.ckpt-{n}.meta文件,是训练过程中保存的元数据(TensorFlow默认只保存最近5个模型和元数据,删除前面没用的模型和元数据),5个model.ckpt-{n}.index文件,{n}代表迭代次数,以及1个检查点文本文件,里面保存着当前模型和最近的5个模型,内容如下:

model_checkpoint_path: "model.ckpt-60"
all_model_checkpoint_paths: "model.ckpt-56"
all_model_checkpoint_paths: "model.ckpt-57"
all_model_checkpoint_paths: "model.ckpt-58"
all_model_checkpoint_paths: "model.ckpt-59"
all_model_checkpoint_paths: "model.ckpt-60"

加载模型

1
2
3
4
5
6
7
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) # 加载所有的参数
# 从这里开始就可以直接使用模型进行预测,或者接着继续训练了

图的存储与加载

当仅保存图模型时,才将图写入二进制协议文件中

1
2
3
4
v = tf.Variable(0, name='my_variable')
sess = tf.Session()
tf.train.write_graph(sess.graph_def, '/tmp/tfmodel', 'train.pbtxt')

当读取时,又从协议文件中读取出来:

1
2
3
4
5
6
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')

队列和线程

队列

1.FIFOQueue

FIFOQueue创建一个先入先出队列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import tensorflow as tf
# 创建一个先入先出队列,初始化队列插入0.1、0.2、0.3三个数字
q = tf.FIFOQueue(3, "float")
init = q.enqueue_many(([0.1, 0.2, 0.3],))

# 定义出队、+1、入队操作
x = q.dequeue()
y = x + 1
q_inc = q.enqueue([y])

# 然后开启一个会话,执行2次q_inc操作,随后查看队列的内容:
with tf.Session() as sess:
sess.run(init)
quelen = sess.run(q.size())
for i in range(2):
sess.run(q_inc) # 执行2次操作,队列中的值变为0.3,1.1,1.2
quelen = sess.run(q.size())
for i in range(quelen):
print (sess.run(q.dequeue())) # 输出队列的值

最终结果如下:0.3, 1.1, 1.2

2.RandomShuffleQueue

RandomShuffleQueue创建一个随机队列,在出队列时,是以随机的顺序产生元素的。

RandomShuffleQueue在TensorFlow使用异步计算时非常重要。因为TensorFlow的会话是支 持多线程的,我们可以在主线程里执行训练操作,使用RandomShuffleQueue作为训练输入,开
多个线程来准备训练样本,将样本压入队列后,主线程会从队列中每次取出mini-batch的样本 进行训练。

1
2
3
4
5
6
7
8
9
10
# 创建一个随机队列,队列最大长度为10,出队后最小长度为2:
q = tf.RandomShuffleQueue(capacity=10, min_after_dequeue=2, dtypes="float")
# 开启一个会话,执行10次入队操作,8次出队操作:
sess = tf.Session()
for i in range(0, 10): #10次入队
sess.run(q.enqueue(i))
for i in range(0, 3): # 8次出队
print(sess.run(q.dequeue()))

# 输出结果:8.0,4.0,9.0(乱序输出)

阻断一般发生在:

  • 队列长度等于最小值,执行出队操作;
  • 队列长度等于最大值,执行入队操作。

队列管理器

会话中可以运行多个线程,我们使用线程管理器QueueRunner创建一系列的新线程进行入队操作,让主线程继续使用数据,即训练网络和读取数据是异步的,主线程在训练网络,另一个线程在将数据从硬盘读入内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 创建一个含有队列的图:
q = tf.FIFOQueue(1000, "float")
counter = tf.Variable(0.0) # 计数器
increment_op = tf.assign_add(counter, tf.constant(1.0)) # 操作:给计数器加1
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()))

线程和协调器

使用协调器(coordinator)来管理线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 主线程
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(sess.run(q.dequeue()))
# 使用tf.errors.OutOfRangeError来捕捉错误,终止循环
except tf.errors.OutOfRangeError:
break

coord.join(enqueue_threads

加载数据

TensorFlow作为符号编程框架,需要先构建数据流图,再读取数据,随后进行模型训练。

1. 预加载数据

1
2
3
x1 = tf.constant([2, 3, 4])
x2 = tf.constant([4, 0, 1])
y = tf.add(x1, x2)

这种方式的缺点在于,将数据直接嵌在数据流图中,当训练数据较大时,很消耗内存。

2. 填充数据

使用sess.run()中的feed_dict参数,将Python产生的数据填充给后端。

1
2
3
4
5
6
7
8
9
10
11
import tensorflow as tf
# 设计图
a1 = tf.placeholder(tf.int16)
a2 = tf.placeholder(tf.int16)
b = tf.add(x1, x2)
# 用Python产生数据
li1 = [2, 3, 4]
li2 = [4, 0, 1]
# 打开一个会话,将数据填充给后端
with tf.Session() as sess:
print sess.run(b, feed_dict={a1: li1, a2: li2})

3. 从文件读取数据

1. 生成TFRecords文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 主函数main:给训练、验证、测试数据集做转换
def main(unused_argv):
# 获取数据
data_sets = mnist.read_data_sets(FLAGS.directory,dtype=tf.uint8,reshape=False,validation_size=FLAGS.validation_size) # 注意,这里的编码是uint8

# 将数据转换为tf.train.Example类型,并写入TFRecords文件
convert_to(data_sets.train, 'train')
convert_to(data_sets.validation, 'validation')
convert_to(data_sets.test, 'test')

# 转换函数convert_to: 将数据填入到tf.train.Example的协议缓冲区 (protocolbuffer)中,将协议缓冲区序列化为一个字符串,通过tf.python_io.TFRecordWriter 写入TFRecords文件。

def convert_to(data_set, name):
images = data_set.images
labels = data_set.labels
num_examples = data_set.num_examples # 55000个训练数据,5000个验证数据,10000个测试数据
if images.shape[0] != num_examples:
raise ValueError('Images size %d does not match label size %d.' %(images.shape[0], num_examples))
rows = images.shape[1] # 28
cols = images.shape[2] # 28
depth = images.shape[3] # 1,是黑白图像,所以是单通道

filename = os.path.join(FLAGS.directory, name + '.tfrecords')
print('Writing', filename)
writer = tf.python_io.TFRecordWriter(filename)

for index in range(num_examples):
image_raw = images[index].tostring()

# 写入协议缓冲区中,height、width、depth、label编码成int64类型,image_raw编码成二进制
example = tf.train.Example(features=tf.train.Features(feature={'height': _int64_feature(rows),'width': _int64_feature(cols),'depth': _int64_feature(depth),'label': _int64_feature(int(labels[index])),'image_raw': _bytes_feature(image_raw)}))

writer.write(example.SerializeToString()) # 序列化为字符串
writer.close()

# 编码函数:

def _int64_feature(value):
return tf.train.Feature(int64_list=tf.train.Int64List(value=[value]))

def _bytes_feature(value):
return tf.train.Feature(bytes_list=tf.train.BytesList(value=[value]))

运行结束后,在/tmp/data下生成3个文件,即train.tfrecords、validation.tfrecords和test.tfrecords。

2. 从队列中读取

一旦生成了TFRecords文件,接下来就可以使用队列读取数据了。主要分为3步:

  • (1)创建张量,从二进制文件读取一个样本;

  • (2)创建张量,从二进制文件随机读取一个mini-batch;

  • (3)把每一批张量传入网络作为输入节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# 首先我们定义从文件中读取并解析一个样本:
def read_and_decode(filename_queue): # 输入文件名队列
reader = tf.TFRecordReader()
_, serialized_example = reader.read(filename_queue)
features = tf.parse_single_example( # 解析example
serialized_example,
# 必须写明features里面的key的名称
features={
'image_raw': tf.FixedLenFeature([], tf.string), # 图片是string类型
'label': tf.FixedLenFeature([], tf.int64), # 标记是int64类型
})
# 对于BytesList,要重新进行解码,把string类型的0维Tensor变成uint8类型的一维Tensor
image = tf.decode_raw(features['image_raw'], tf.uint8)
image.set_shape([mnist.IMAGE_PIXELS])
# Tensor("input/DecodeRaw:0", shape=(784,), dtype=uint8)

# image张量的形状为:Tensor("input/sub:0", shape=(784,), dtype=float32)
image = tf.cast(image, tf.float32) * (1./ 255) - 0.5

# 把标记从uint8类型转换为int32类型
# label张量的形状为Tensor("input/Cast_1:0", shape=(), dtype=int32)
label = tf.cast(features['label'], tf.int32)

return image, label

# 接下来使用tf.train.shuffle_batch将前面生成的样本随机化,获得一个最小批次的张量:
def inputs(train, batch_size, num_epochs):
# 输入参数:
# train: 选择输入训练数据/验证数据
# batch_size: 训练的每一批有多少个样本
# num_epochs: 过几遍数据,设置为0/None表示永远训练下去
"""
返回结果:A tuple (images, labels)
* images: 类型float, 形状[batch_size, mnist.IMAGE_PIXELS],范围[-0.5, 0.5].
* labels: 类型int32,形状[batch_size],范围 [0, mnist.NUM_CLASSES]
注意tf.train.QueueRunner 必须用tf.train.start_queue_runners()来启动线程
"""
if not num_epochs: num_epochs = None
# 获取文件路径,即/tmp/data/train.tfrecords, /tmp/data/validation.records
filename = os.path.join(FLAGS.train_dir,
TRAIN_FILE if train else VALIDATION_FILE)

with tf.name_scope('input'):
# tf.train.string_input_producer返回一个QueueRunner,里面有一个FIFOQueue
filename_queue = tf.train.string_input_producer([filename], num_epochs=num_epochs) # 如果样本量很大,可以分成若干文件,把文件名列表传入

image, label = read_and_decode(filename_queue)
# 随机化example,并把它们规整成batch_size大小
# tf.train.shuffle_batch生成了RandomShuffleQueue,并开启两个线程
images, sparse_labels = tf.train.shuffle_batch(
[image, label], batch_size=batch_size, num_threads=2,
capacity=1000 + 3 * batch_size,
min_after_dequeue=1000) # 留下一部分队列,来保证每次有足够的数据做随机打乱

return images, sparse_labels

# 最后,我们把生成的batch张量作为网络的输入,进行训练:
def run_training():
with tf.Graph().as_default():
# 输入images和labels
images, labels = inputs(train=True, batch_size=FLAGS.batch_size,num_epochs=FLAGS.num_epochs)

# 构建一个从推理模型来预测数据的图
logits = mnist.inference(images,
FLAGS.hidden1,
FLAGS.hidden2)

loss = mnist.loss(logits, labels) # 定义损失函数

# Add to the Graph operations that train the model.
train_op = mnist.training(loss, FLAGS.learning_rate)

# 初始化参数,特别注意:string_input_producer内部创建了一个epoch计数变量,
# 归入tf.GraphKeys.LOCAL_VARIABLES集合中,必须单独用initialize_local_variables()初始化
init_op = tf.group(tf.global_variables_initializer(),
tf.local_variables_initializer())

sess = tf.Session()

sess.run(init_op)

# Start input enqueue threads.
coord = tf.train.Coordinator()
threads = tf.train.start_queue_runners(sess=sess, coord=coord)

try:
step = 0
while not coord.should_stop(): # 进入永久循环
start_time = time.time()
_, loss_value = sess.run([train_op, loss])
duration = time.time() - start_time

# 每100次训练输出一次结果
if step % 100 == 0:
print('Step %d: loss = %.2f (%.3f sec)' % (step, loss_value, duration))
step += 1
except tf.errors.OutOfRangeError:
print('Done training for %d epochs, %d steps.' % (FLAGS.num_epochs, step))
finally:
coord.request_stop() # 通知其他线程关闭

coord.join(threads)
sess.close()

如上所述,我们总结出TensorFlow使用TFRecords文件训练样本的步骤:

  • (1)在生成文件名队列中,设定epoch数量;

  • (2)训练时,设定为无穷循环;

  • (3)在读取数据时,如果捕捉到错误,终止。

实现自定义操作

谢谢你请我吃糖果
0%