在本章中,将详细介绍
1.TensorFlow基本概念
2.TensorFlow的计算模型
3.数据模型、运行模型
4.简单介绍神经网络的主要计算流程和如何通过TensorFlow来实现这些计算
1.TensorFlow计算模型计算图
1.1计算图的概念
TensorFlow的名字中已经说明了它最重要的两个概念:Tensor和Flow。Tensor就是张量。张量这个概念在数学或物理中可以有不同的解释,而在TensorFlow中,张量可以被简单理解为多维数组。如果说TensorFlow的第一个词Tensor表明了它的数据结构,那么Flow则体现了它的计算模型。Flow翻译成中卫就是“流”,它直观地表达了张量之间通过计算相互转化的过程。TensorFlow是一个通过计算图的形式来表述计算的编程系统。TensorFlow中的每一个计算都是计算图上的一个节点,而节点之间的边描述了计算之间的依赖关系。下图展示了通过TennsorBoard画出来的两个向量相加样例的计算图。
上图中的每一个节点一个运算,而每一条边代表了计算之间的依赖关系。如果一个运算的输入依赖于另一个运算的输出,那么这两个运算有依赖关系。上图中,a和b这两个常量不依赖于任何其他计算。而add依赖于读取两个常量的取值。于是在图中可以看到有一条从a到add的边和一条从b到add的边。在上图中,没有任何计算依赖add的结果,于是代表加法的节点add没有任何指向其他节点的边。所有TensorFlow的程序都可以通过类似上图的形式来表达,这就是TensorFlow的基本计算模型。
1.2计算图的使用
TensorFlow程序一般分为两个阶段。第一个阶段需要定义计算图中所有的计算。比如在前面的章节的向量加法的例子中,首先定义了两个输入,然后定义了一个计算来得到他的和。第二个阶段为执行计算
定义计算阶段代码如下:
#加载TensorFlow
import tensorflow as tf
a = tf.constant([1.0,2.0],name="a")
b = tf.constant([2.0,3.0],name="b")
result = a + b
计算阶段代码如下:
#生成一个会话,通过这个会话来计算结果
sess = tf.Session()
sess.run(result)
一般载入TensorFlow的形式:import tensorflow as tf
在这个过程中,TensorFlow会自动将定义的计算转化为计算图上的节点。在TensorFlow程序中,系统会自动维护一个默认的计算图,通过tf.get_default_graph函数可以获取当前默认的计算图。一下代码示意了如何获取默认计算图以及如何查看一个运算所属的计算图。
print(a.graph is tf.get_default_graph())
运行代码,得到如下所示的结果:
True
结果分析:过a.graph可以查看张量所属的计算图。因为没有特意指定,所以这个计算图应该等于当前默认的计算图。所以下面这个操作输出值为True
除了使用默认的计算图,TensorFlow支持通过tf.Graph函数来生成新的计算图。不同计算图上的张量和运算都不会共享,一下代码示意了如何在不同计算图上定义和使用变量:
import tensorflow as tf
g1 = tf.Graph()
with g1.as_default():
#在计算图g1中定义变量“v”,并设置初始值为0
v = tf.get_variable("v",shape=[1],initializer=tf.zeros_initializer)
g2 = tf.Graph()
with g2.as_default():
#在计算图g2中定义变量“v”,并设置初始值为1
v = tf.get_variable("v",shape=[1],initializer=tf.ones_initializer)
#在计算图g1中读取变量“v”的取值
with tf.Session(graph=g1) as sess:
tf.global_variables_initializer().run()
with tf.variable_scope("",reuse=True):
#在计算图g1中,变量“v”的取值应该为0,所以下面这行会输出[0.]
print(sess.run(tf.get_variable("v")))
#在计算图g2中读取变量“v”的取值
with tf.Session(graph=g2) as sess:
tf.global_variables_initializer().run()
with tf.variable_scope("",reuse=True):
#在计算图g2中,变量“v”的取值应该为1,所以下面这行会输出[1.]
print(sess.run(tf.get_variable("v")))
运行代码:
结果分析:以上代码产生了两个计算图,每个计算图中定义了一个名为“v”的变量。在计算图g1中,将v初始化为0;在计算图g2中,将v初始化为1,。可以看到当运行不同计算图时,变量v的值是不同的。TensorFlow中的计算图不仅仅可以用来隔离张量和计算,它还提供了管理张量和计算的机制。计算图可以通过tf.graph.device函数来指定运行计算的设备。这位TensorFlow使用GPU提供了机制。以下程序可以将加法计算跑在GPU上:
g = tf.Graph()
#指定计算运行的设备
with g.device('/gpu:0'):
result = a+b
有效地整理TensorFlow程序中的资源也是计算图的一个重要功能。在一个计算图中,可以通过集合来管理不同类别的资源。比如通过tf.add_to_collection函数可以将资源加入一个或多个集合中,然后通过tf.get_collection获取一个集合的所有资源。这里的资源可以是张量、变量或者运行TensorFlow程序所需的队列资源,等等。为了方便使用,TensorFlow也自动管理了一些最常用集合。下表总结了常用几个自动维护的集合:
2.TensorFlow数据模型
上一小节介绍了使用计算图的模型来描述TensorFlow中的计算。这一节将介绍TensorFlow中另外一个基础概念——张量。张量是TensorFlow管理数据的形式,我们将介绍:
1.张量的一些基本属性
2.如何通过张量保存和获取TensorFlow计算的结果
2.1张量的概念
从TensorFlow的名字就可以看出张量(Tensor)是一个很重要的概念。在TensorFlow程序中,所有的数据都是通过张量的形式来表示。从功能的角度上看,张量可以被简单理解为多维数组。其中0阶张量表示标量(scalar),也就是一个数;第一阶张量为向量(vector),也就是一个一维数组;第n阶张量可以理解为一个n维数组。但张量在TensorFlow中 的实现并不是直接采用数组的形式,它只是对TensorFlow运算结果的引用。在张量中并没有真正保存数字,它保存的是如何得到这些数字的计算过程。还是以向量加法为例,当运行如下代码时,并不会得到加法的结果,而会得到对结果的一个引用:
import tensorflow as tf
#tf.constant是一个计算,这个计算的结果为一个张量,保存在变量a中。
a = tf.constant([1.0,2.0],name="a")
b = tf.constant([2.0,3.0],name="b")
result = tf.add(a,b,name="add")
print(result)
运行结果:
Tensor("add:0", shape=(2,), dtype=float32)
结果分析:
从代码中可以看出TensorFlow中的张量和Numpy中的数组不同,TensorFlow计算的结果不是一个具体的数字,而是一个张量的结构。从结果中可以看出,一个张量中主要保存了3个属性:名字(name)、维度(shape)和类型(type)
我们发现,当运行第二遍的时候,结果会变成:
Tensor("add_1:0", shape=(2,), dtype=float32),说明每次运行,名称都会递增改变。
张量的第一个属性名字不仅是一个张量的唯一标识符,它同样也给出了这个张量是如何计算出来的。在上一节中介绍了TensorFlow的计算都可以通过计算图的模型来建立,而计算图上的每一个节点代表了一个计算,计算的结果就保存在张量之中。所以张量和计算图上节点所代表的的计算结果是对应的。这样张量的命名就可以通过“node:src_output”的形式来给出。其中node为节点的名称,src_output表示当前张量来自节点的第几个输出。比如上面代码打出来的“add:0”就说明了result这个张量是计算节点“add”输出的第一个结果(编号从0开始)。
张量的第二个属性是张量的维度(shape)。这个属性描述了一个张量的维度信息。比如上面样例中shape=(2,)说明了张量result是一个一维数组,这个数组的长度是2。维度是张量一个很重要的属性,围绕张量的维度TensorFlow也给出了很多有用的运算,这里先不一一列举了。在后面的章节中将使用部分运算。
张量的第三个属性是类型(type),每一个张量会有一个唯一的类型。TensorFlow会对参与运算的所有张量进行类型的检查,当发现类型不匹配时会报错。比如运行一下程序时就会得到类型不匹配的错误:
import tensorflow as tf
a = tf.constant([1,2],name="a")
b = tf.constant([2.0,3.0],name="b")
result = a+b
运行结果:
结果分析:
这段程序和上面演示过的样例基本一样,唯一不同的是把其中一个加数的小数点去掉了。这会使得a的类型为整数而加b的类型为实数,这样程序就会报类型不匹配的错误。
思考:那么除了加上小数点还有什么解决办法?
如果将第一个家属指定为实数类型“a = tf.constant([1,2],name="a",dtype=tf.float32)”,那么两个加数的类型相同就不会报错了。如果不指定类型,TensorFlow会给出默认的类型,比如不带小数点的数会被默认为int32,带小数点的会默认为float32.因为使用默认类型有可能会导致潜在的类型不匹配问题,所以一般建议通过制定dtype来明确之处变量或者常量的类型。
TensorFlow支持的14种不同的类型,主要包括了实数(tf.float32、tf.float64)、整数(tf.int8、tf.int16、tf.int32、tf.int64、tf.uint8)、布尔型(tf.bool)和复数(tf.complex64、tf.complex128)
2.2张量的使用
和TensorFlow的计算模型相比,TensorFlow的数据模型相对比较简单。张量使用主要可以总结为两大类。
1.第一类用途是对中间计算结果的引用。当一个计算包含很多中间结果时,使用张量可以大大提高代码的可读性。以下为使用张量和不适用张量记录中间结果来完成向量相加的功能的代码对比:
#使用张量记录中间结果
a = tf.constant([1.0,2.0],name="a")
b = tf.constant([2.0,3.0],name="b")
result = a + b
#使用计算向量的和,这样可读性会比较差
result = tf.constant([1.0,2.0],name="a") + tf.constant([2.0,3.0],name="b")
从上面的样例程序可以看到,a和b其实就是对常量生成的运算结果的引用,这样在做加法时就可以直接使用这两个变量,而不需要再去生成这些常量。
主要在两点:
1.当计算的复杂度增加时(比如在构建深层神经网络时)通过张量来引用计算的中间结果可以是使代码的可阅读性大大提升。
2.通过张量来存储中间结果可以方便获取中间结果。比如在卷积神经网络中,卷积层或者池化层有可能改变张量的维度,通过result.get_shape函数来获取结果张量维度信息可以免去人工计算的麻烦。
3.还有一种情况是当计算图构造完成之后,张量可以用来获得计算结果,也就是得到真正的数字。虽然张量本身没有存储具体的数字,但通过下一节介绍的会话,就可以得到这些具体的数字,比如在上述代码中,可以使用tf.Session().run(result)语句得到计算结果。
3.TensorFlow运行模型会话
前面的两节中介绍了TensorFlow是如何组织数据和运算的。本节将介绍如何使用TensorFlow中的会话(session)来执行定义好的运算。会话拥有并管理TensorFlow程序运行时的所有资源。所有计算完成之后需要关闭会话来帮助系统回收资源,否则就可能出现资源泄露问题。TensorFlow中使用会话的模式一般有两种,第一种模式需要明确调用会话生成函数和关闭会话函数,这种模式的代码流程如下:
#创建一个会话
sess = tf.Session()
#使用这个创建好的会话来得到关心的运算的结果。比如可以调用sess.run(result),来得到样例中张量result的取值。
sess.run(...)
#关闭会话使得本次运行中使用到的资源可以被释放
sess.close()
使用这种模式时,在所有计算完成之后,需要明确调用Session.close函数来关闭会话并释放资源。然而,当程序因为异常而退出时,关闭会话的函数可能就不会被执行从而导致资源泄露。为了解决异常退出时资源释放的问题,TensorFlow可以通过python的上下文管理器来使用会话。以下代码展示了如何使用这种模式:
#创建一个会话,并通过python中的上下文管理器来管理这个会话。
with tf.Session() as sess:
#使用创建好的会话来计算关心的结果。
sess.run(...)
#不需要再调用“session.close()”函数来关闭会话,当上下文退出时会话关闭和资源释放也自动完成了。
通过python上下文管理器的机制,只要将所有的计算放在“with”的内部就可以。当上下文管理器退出时候就会自动释放所有资源。这样既解决了因为异常退出时资源释放的问题,也同时解决了忘记调用Session.close函数而产生的资源遗漏。
前2节中介绍了TensorFlow会自动生成一个默认的计算图,如果没有特殊指定,运算会自动加入这个计算图中。TensorFlow的会话也有类似的机制,但TensorFlow不会自动生成默认的会话,而是需要手动指定。当默认的会话被指定之后就可以通过tfTensor.eval函数来计算一个张量的取值。以下代码展示了通过设定默认会话来计算张量的取值:
sess = tf.Session()
with sess.as_default():
print(result.eval())
以下代码也可以完成相同的功能:
sess = tf.Session()
print(sess.run(result))
print(result.eval(session=sess))
第一段代码运行结果:
[3. 5.]
第二段代码运行结果:
[3. 5.]
[3. 5.]
TensorFlow提供了一种在交互式环境下直接构建默认会话的函数。这个函数就是tf.InteractiveSession。使用这个函数会自动将生成的会话注册为默认会话,以下代码展示了tfInteractiveSession函数的用法:
sess = tf.InteractiveSession()
print(result.eval())
sess.close()
运行代码结果:
[3. 5.]
通过 tf.InteractiveSession()可有省去将产生的会话注册为默认会话的过程,无论使用哪种方法都可以通过ConfigProto Protocol Buffer来配置需要生成的会话。下面给出了通过ConfigProto配置会话的方法:
config = tf.ConfigProto(allow_soft_placement=True,log_device_placement=True)
sess1 = tf.InteractiveSession(config=config)
sess2 = tf.Session(config=config)
通过ConfigProto可以配置类似并行的线程数、GPU分配策略、运算超时时间等参数。在这些参数中,最常用的是两个。第一个是allow_soft_placement,这是一个布尔类型的参数,当它为True时,在最下任意一个条件成立时,GPU上的运算可以放到CPU上进行:
1.运算无法在GPU上执行
2.没有GPU资源
3.运算输入包含对CPU计算结果的引用。
这个参数设置为False,但是为了使得代码的可移植性更强,在GPU的环境下这个参数一般会被设置为True,不同的GPU驱动版本可能对计算机的支持有略微的区别,通过将allow_soft_placement参数设为true,当某些运算无法被当前的GPU支持时,可以自动调整到CPU上,而不是报错。类似地通过将这个参数设置为True,可以让程序在拥有不同数量的GPU机器上顺利运行。
第二个使用比较多的参数是log_device_placement。这是一个布尔类型的参数,当它为True时,日志中将会记录每个节点被安排在哪个设备上以方便调试。而在生产环境中将这个参数设置为False可以减少日志量。
4.TensorFlow实现神经网络
本节将结合神经网络的功能进一步介绍如何通过TensorFlow来实现神经网络。本节具体介绍:
1.通过TensorFlow游乐场来简单介绍神经网络的主要功能以及计算流程
2.神经网络的前向传播算法,并给出使用TensorFlow的代码实现
3.神经网络反向传播算法的原理以及TensorFlow对反向传播算法的支持
4.将给出一个完整的TensorFlow程序在随机的数据上训练一个简单的神经网络
4.1TensorFlow游乐场及神经网络简介
这一小节将通过TensorFlow游乐场来快速介绍神经网络的主要功能。TensorFlow游乐场(http://playground.tensorflow.org)是一个通过网页浏览器就可以训练的简单神经网络并实现了可视化训练过程的工具。下图给出了TensorFlow游乐场默认设置的截图:
从上图可以看出,TensorFlow游乐场的左侧提供了4个不同数据集来测试神经网络。默认的数据为左上角被框出来的那个。被选中的数据也会显示在上图最右边的“OUTPUT”栏下。在这个数据中,可以看到一个二维平面上有蓝色或者橙色的点,每一个小点代表了一个样例,而点的颜色代表了样例的标签。因为点的颜色只有两种,所以这是一个二分类的问题。在这里举一个例子来说明这个数据可以代表的实际问题。假设需要判断某个工厂生产的零件是否合格,那么橙色的点可以表示所有合格的零件而蓝色的表示不合格的零件。这样判断一个零件是否合格就变成了区分点的颜色。
为了将一个实际问题对应到平面上不同颜色点的划分,还需要将实际问题中的实体,比如上述例子中的零件,变成平面上的一个点。这就是特征提取解决的问题。还是以零件为例,可以用零件的长度来大致描述一个零件。这样一个物理意义上的零件就可以被转化成长度和质量这两个数字。在机器学习汇总,所有由于描述实体的数字的组合就是一个实体的特征向量(feature vector)。特征向量的提取对机器学习的效果至关重要。通过特征提取,就可以将实际问题中的实体转化为空间中的点。假设使用长度和质量作为一个零件的特征向量,那么每个零件就是二维平面上的一个点。TensorFlow游乐场中FEATURES一栏对应了特征向量。在样例中,可以忍为x1代表一个零件的长度,而x2代表零件的质量。
特征向量是神经网络的输入,神经网络的主体结构显示了在上图中间的位置。目前主流的神经网络都是分层的结构,第一层是输入层,代表特征向量中每一个特征的取值。
在输入和输出层之间的神经网络叫做隐藏层,一般一个神经网络的隐藏层越多,这个神经网络越“深”。而所谓的深度学习中的这个“深度”和神经网络的层数也是密切相关的。在TensorFlow游乐场中可以通过点击“+”或者“-”来增加或减少隐藏层的数量。除了可以选择神经网络的深度,TensorFlow游乐场也支持选择神经网络每一层的节点数以及学习率、激活函数、正则化。当所有配置都选好之后,可以通过左上角的开始标志播放按钮来训练这个神经网络。下图给出了迭代训练122轮之后的情况:
图分析:在图中,一个小格子代表神经网络中的一个节点,而边代表节点之间的连接。每一个节点和边都被涂上了或深或浅的颜色,但边上的颜色和格子中的颜色含义有略微的区别。每一条边代表了神经网络中的一个参数,它可以是任意实数。神经网络就是通过对参数的合理设置来解决分类或者回归问题的。边上的颜色体现了这个参数的取值,当边的颜色越深时,这个参数取值的绝对值越大;当边的颜色越接近白色时,这个参数的取值越接近于0
每一个节点上的颜色代表了这个节点的区分平面。具体来说,如果把这个平面当成一个卡迪尔坐标系,这个平面上的每一个点就代表了(x1,x2)的一种取值。而这个点的颜色就体现了x1,x2在这种取值下这个节点的输出值。和边类似,当节点的输出值的绝对值越大时,颜色越深。下面将具体解读输入层x1所代表的节点。从上图可以看到x1这个节点的区分平面就是y轴,因为这个节点的输出就是x1本身的值,所以当x1小于0时,这个节点的输出就是负数,而x1大于0时输出的就是正数。于是y轴的左侧都为橙色,而右侧都是蓝色。上图中其他节点可以类似的解读。唯一特殊的是最右边OUTPUT栏下的输出节点。这个节点中除了显示区分平面之外,还显示了训练数据,也就是希望通过神经网络区分的数据点。从图上可以看到经过两层的隐藏层,输出节点的区分平面已经可以完全区分不同颜色的数据点。
综上所述,使用神经网络解决分类问题主要可以分为以下个步骤:
1.提取问题中实体的特征向量作为神经网络的输入。
2.定义神经网络的结构,并定义如何从神经网络的输入得到输出。这个过程就是神经网络的前向传播算法。
3.通过训练数据来调整神经网络中参数的取值,这就是训练神经网络的过程。
4.使用训练好的神经网络来预测未知的数据
4.2前向传播算法简介
前面介绍了神经网络可以将输入的特征向量经过层层推导得到最后的输出,并通过这些输出解决分类或者回归问题。那么神经网络的输出是如何得到的?在这一小节将详细介绍解决这个问题
的算法——前向传播算法。不同的神经网络结构前向传播的方式也不一样,本小节将介绍最简单的全连接网络结构的前向传播算法,并将展示如何通过TensorFlow实现这个算法。
为了介绍神经网络的前向传播算法,需要先了解神经元的结构,神经元是构成一个神经网络的最小单元,下图显示了一个最简单的神经元结构:
图分析:从图中可以看出,一个神经元有多个输入和一个输出,每个神经元的输入既可以是其他神经元的输出,也可以是整个神经网络的输入。所谓神经网络的结构就是指的不同神经元之间的连接结构。如上图所示,一个最简单的神经元结构的输出就是所有输入的加权和,而不同输入的权重就是神经元的参数。神经网络的优化过程就是优化神经元中参数取值的过程。
下图给出了一个简单的判断零件是否合格的三层全连接神经网络:
图分析:之所以称之为全连接神经网络是因为相邻两层之间任意两个节点之间都有连接。这是全连接结构,后面还会介绍全基层、LSTM的结构。
计算神经网络的前向传播结果需要三部分信息。第一个部分是神经网络的输入,这个输入就是从实体中提取的特征向量。比如上图中有两个输入x1,x2.第二个部分为神经网络的连接结构。神经网络是由神经元构成的,神经网络的结构给出不同神经元之间输入输出的连接关系。神经网络中的神经元也可以称之为节点。上图中,a11节点有两个输入,分别是x1,x2.而a11的输出节点则是节点y的输入。最后一个部分是神经元中的参数。在上图中用W来表示神经元中的参数。W的上标表明了神经网络的层数,比如表示第一层节点的参数,而表示第二层节点的参数。W的下标表明了连接节点编号,如果表示连接x1和a12节点的边上的权重。给定神经网络的输入,结构以及边上的权重,就可以通过前向传播算法来计算出神经网络的输出,如下图展示了这个神经网络的前线传播过程:
图分析:上图给出了输入层的取值x1=0.7和x2=0.9.从输入层开始一层一层地使用前向传播算法。首先隐藏层中有3个节点,每一个节点的取值都是输入层取值的加权和。下面给出节点a11取值的详细计算过程:
和也可以通过类似的方法就计算得到,上图也给出了具体的计算公式。在得到第一层节点的取值之后,可以进一步推导得到输出层的取值。类似的,输出层中节点的取值就是第一层的加权和:
因为这个输出值大于阈值0,所以在这个样例中最后给出的答案又是:这个产品是合格的。这就是整个前向传播的算法。前向传播算法可以表示为矩阵乘法。将输入x1,x2组织成一个1*2的矩阵x = [x1,x2],而组织成一个2 * 3的矩阵:
这样通过矩阵乘法可以得到隐藏层三个节点所组成的向量取值:
类似的输出层可以表示为:
这样就将前向传播算法通过矩阵乘法的方式表达出来了。在TensorFlow中矩阵乘法是非常容易实现的。以下TensorFlow程序实现了上图神经网络的前向传播过程:
a = tf.matmul(x,w1)
y = tf.matmul(a,w2)
其中tf.matmul实现了矩阵乘法的功能。到此为止已经详细介绍了神经网络的前向传播算法,并且给出了TensorFlow程序来实现这个过程。
4.3神经网络参数与TensorFlow变量
神经网络中的参数是神经网络实现分类或者回归问题中重要的部分。本小节将更加具体地介绍TensorFlow是如何组织、保存以及使用神经网络中的参数的。在TensorFlow中,变量(tf.variable)的作用就是保存和更新神经网络中的参数。和其他编程语言类似,TensorFlow中的变量也需要指定初始值。因为在神经网络中,给参数赋予随机初始值最为常见,所以一般也使用随机数给TensorFlow中的变量初始化。
下面一段代码给出了一种在TensorFlow中声明一个2*3的矩阵变量的方法:
weights = tf.Variable(tf.random_normal([2,3],stddev=2))
分析:这段代码调用了TensorFlow变量的声明函数tf.Variable。在变量声明函数中给出了初始化这个变量的方法。TensorFlow中变量的初始值可以设置成随机数、常数或者是通过其他变量的初始值计算得到。在上面的样例中,会产生一个2*3的矩阵,矩阵中的元素是均值为0,标准差为2的随机数。tf.random_normal函数可以通过参数mean来指定平均值,在没有指定时默认为0.通过满足正态分布的随机数来初始化神经网络中的参数是一个非常常用的方法。除了正态分布的随机数,TensorFlow还提供了一些其他的随机数生成器,下面列出了TensorFlow目前支持的所有随机数生成器:
TensorFlow也支持通过常数来初始化一个变量,下面给出了TensorFlow常用的常量声明方法:
在神经网络中,偏置项(bias)通常会使用常数来设置初始值。代码样例如下:
biases = tf.Variable(tf.zeros([3]))
这段代码将会生成一个初始值全部为0,且长度为3的变量。除了使用随机数或者常数,TensorFlow也支持通过其他变量的初始值来初始化新的变量。代码样例如下:
w2 = tf.Variable(weights.initialized_value())
w3 = tf.Variable(weights.initalized_value()*2.0)
以上代码中,w2的初始值被设置成了与weights变量相同。w3的初始值则是weights初始值的两倍。以下样例介绍了如何通过变量实现神经网络的参数并实现前向传播的过程:
import tensorflow as tf
w1 = tf.Variable(tf.random_normal([2,3], stddev = 1, seed=1))
w2 = tf.Variable(tf.random_normal([3,1], stddev = 1, seed=1))
x = tf.constant([[0.7,0.9]])
a = tf.matmul(x,w1)
y = tf.matmul(a,w2)
sess = tf.Session()
sess.run(w1.initializer)
sess.run(w2.initializer)
print(sess.run(y))
sess.close()
运行代码,打印结果如下:
[[ 3.95757794]]
上面这段代码实现了神经网络的前向传播过程,从代码中可以看到,当声明了变量w1、w2之后,可以通过w1和w2来定义神经网络的前向传播过程并得到中间结果a和最后答案y。定义w1、w2、a和y的过程对应了前面介绍的TensorFlow程序的第一步。这一步定义了TensorFlow计算图中所有的计算,但这些被定义的计算在这一步中并不真正的运行。当需要运行这些计算并得到具体数字时,需要进入TensorFlow程序的第二步。
在TensorFlow程序的第二步会声明一个会话(session),通过这个会话计算结果。在上面样例中,当会话定义完成之后就可以开始真正运行定义好的计算了。但在计算y之前,需要将所有用到的变量初始化。所以在计算y之前,需要通过运行w1.initializer和w2.initializer来给变量赋值。虽然直接调用每个变量的初始化过程是一个可行的方案,但是当变量数目增多,或者变量之间存在依赖关系时,单个调用的方案就比较麻烦了。为了解决这个问题,TensorFlow提供了一种更加便捷的方式来完成变量初始化过程。下面程序展示了通过tf.initializer_all_vairables函数实现初始化所有变量的过程。
init_op = tf.initialize_all_variables()
sess.run(init_op)
通过tf.initialize_all_variables函数,就不需要将变量一个一个初始化了。这个函数也会自动处理变量之间的依赖关系。
变量和张量的关系:
在TensorFlow中,变量的声明函数tf.Variable是一个运算。这个运算的输出结果就是一个张量,这个张量也就是本节中介绍的变量。所以变量只是一种特殊的张量。
下面将进一步介绍tf.Variable操作在TensorFlow中底层是如何实现的。下图给出了神经网络前向传播样例程序的TensorFlow计算图的一个部分,这个部分显示了和变量w1相关的操作
图分析:在上图中黑色的椭圆代表了变量w1,可以看到w1是一个Variable运算。在这张图的下方可以看到w1通过一个read操作将值提供给了一个乘法运算,这个乘法操作就是tf.matmul(x,w1)。初始化变量w1的操作是通过Assign操作完成的。在上图中可以看到Assign这个节点的输入为随机数生成函数的输出,而且输出赋给了变量w1.这样就完成了初始化的过程
前面小节介绍了TensorFlow集合的概念,所有的变量都会被自动的加入GraphKeys.VARIABLES这个集合。通过tf.all_variables函数可以拿到当前计算图上所有的变量。拿到计算图上所有的变量有助于持久化整个计算图的运行状态。
类似张量,维度(shape)和类型(type)也是变量最重要的两个属性。和大部分程序语言类似,变量的类型是不可改变的。一个变量在构建之后,它的类型就不能再改变了。比如在上面给出的前向传播样例中,w1的类型为random_normal结果的默认类型tf.float32,那么它将不能被赋予其他类型的值,以下代码将会报出类型不匹配的错误:
w1 = tf.Variable(tf.random_normal([2,3],stddev=1),name="w1")
w2 = tf.Variable(tf.random_normal([2,3],dtype=tf.float64,stddev=1),name="w2")
w1.assign(w2)
运行代码,报错如下:
维度是变量另一个重要的属性。和类型不大一样的是,维度在程序运行中是有可能改变的,但是需要通过设置参数validate_shape=False。代码示例如下:
w1 = tf.Variable(tf.random_normal([2,3],stddev=1),name="w1")
w2 = tf.Variable(tf.random_normal([2,2],stddev=1),name="w2")
tf.assign(w1,w2,validate_shape=False)
执行代码,结果如下:
<tf.Tensor 'Assign:0' shape=(2, 2) dtype=float32_ref>
虽然TensorFlow支持更改变量的维度,但是这种用法在实践中比较罕见。
4.4通过TensorFlow训练神经网络模型
前面介绍了如何通过TensorFlow变量来表示神经网络中的参数,并给出了一个样例程序来完成神经网络的前向传播过程。在这个样例程序中,所有变量的取值都是随机的。在使用神经网络解决实际的分类或者回归问题时(比如判断一个零件是否合格)需要更好地设置参数取值。这一小节将简单介绍使用监督学习的方式来更合理地设置参数取值,同时也将给出TensorFlow程序来完成这个过程。设置神经网络参数的过程就是神经网络的训练过程。只有经过有效训练的神经网络模型才可以真正的解决分类或者回归问题。
下图对比了训练之前和训练之后神经网络模型的分类效果。从图中可以看出,模型在训练之前是完全无法区分黑色点和灰色点的,但是经过训练之后区分效果已经很好了:
监督学习最重要的思想就是,在已知答案的标注数据集上,模型给出的预测结果要尽量接近真实的答案。通过调整神经网络中的参数对训练数据进行拟合,可以使得模型对未知的样本提供预测的能力。
在神经网络优化算法中,最常用的方法是反向传播算法(backpropagation)。下图展示了使用反向传播算法训练神经网络的流程图:
图分析:从上图中可以看出,反向传播算法实现了一个迭代的过程。在每次迭代的开始,先需要选取一小部分训练数据,这一小部分数据叫做一个batch。然后,这个batch的样例会通过前向传播算法得到神经网络模型的预测结果。因为训练数据都是有正确答案标注的,所以可以计算出当前神经网络模型的预测答案与正确答案之间的差距。最后基于这个预测值和真实值之间的差距,反向传播算法会相应更新神经网络参数的取值,使得在这个batch上神经网络模型的预测结果和真实答案更加接近。
通过TensorFlow实现反向传播算法的第一步是TensorFlow表达一个batch的数据,在前面尝试过使用常量来表达过一个样例:
x = tf.constant([[0.7,0.9]])
但如果每轮迭代中选取的数据都要通过常量来表示,那么TensorFlow的计算图将会太大。因为没生成一个常量,TensorFlow都会在计算图中增加一个节点。一般来说,一个神经网络的训练过程会需要经过几百万轮甚至几亿轮的迭代,这样计算图就会非常大,而且利用率很低。为了避免这个问题,TensorFlow提供了placeholder机制用于提供输入数据。placeholder相当于定义了一个位置,这个位置中的数据在程序运行时再指定。这样在程序中就不需要生成大量常量来提供输入数据,而只需要将数据通过placeholder传入TensorFlow计算图。在placeholder定义时,这个位置上的数据类型是需要指定的。和其他张量一样,placeholder的类型也是不可以改变的。placeholder中数据的维度信息可以根据提供的数据推导得出,所以不一定要给出。下面给出了通过placeholder实现前向传播算法的代码:
w1 = tf.Variable(tf.random_normal([2,3],stddev=1))
w2 = tf.Variable(tf.random_normal([3,1],stddev=1))
x = tf.placeholder(tf.float32,shape=(1,2),name="input")
a = tf.matmul(x,w1)
y = tf.matmul(a,w2)
sess = tf.Session()
init_op = tf.global_variables_initializer()
sess.run(init_op)
# print(sess.run(y))
print(sess.run(y,feed_dict={x:[[0.7,0.9]]}))
运行代码,结果如下:
[[ 1.26965725]]
在这段程序中替换了原来通过常量定义的输入x。在新的程序中计算前向传播结果时,需要提供一个feed_dict里指定x的取值。feed_dict是一个字段(map),在字典中需要给出每个用到的placeholder的取值。如果某个需要的placeholder没有被指定取值,那么程序在运行时将会报错。
上面的程序只计算了一个样例的前向传播结果,但如上图所示,在训练神经网络时需要每次提供一个batch的训练样例。对于这样的需求,placeholder也可以很好地支持。在上面的样例程序中,如果将输入的12矩阵改成n2的矩阵,那么就可以得到n个样例的前向传播结果了。其中n2的矩阵的每一行为一个样例数据。这样前向传播的结果为n1的矩阵,这个矩阵的每一行就代表了一个样例的前向传播结果,下面的程序给出了一个示例:
w1 = tf.Variable(tf.random_normal([2,3],stddev=1))
w2 = tf.Variable(tf.random_normal([3,1],stddev=1))
x = tf.placeholder(tf.float32,shape=(3,2),name="input")
a = tf.matmul(x,w1)
y = tf.matmul(a,w2)
sess = tf.Session()
init_op = tf.global_variables_initializer()
sess.run(init_op)
print(sess.run(y,feed_dict={x:[[0.7,0.9],[0.1,0.4],[0.5,0.8]]}))
运行代码,得到结果如下:
[[ 3.83821535]
[ 1.10681784]
[ 3.06492543]]
上面的样例展示了一次性计算多个样例的前向传播结果。在运行时,需要将3个样例[0.7,0.9],[0.1,0.4],[0.5,0.8]组成一个32的矩阵传入placeholder。计算得到的结果为31的矩阵。第n行的样例对应din行样例的前向传播结果。
在得到一个batch的前向传播结果之后,需要定义一个损失函数来刻画当前的预测值和真实答案之间的差距。然后通过反向传播算法来调整神经网络参数的取值使得差距可以被缩小。一下代码定义了一个简单的损失函数,并通过TensorFlow定义了反向传播的算法:
#定义损失函数来刻画预测值与真实值的差距
cross_entropy = -tf.reduce_mean(y_*tf.log(tf.clip_by_value(y,1e-10,1.0)))
# 定义学习率
learning_rate = 0.001
# 定义反向传播算法来优化神经网络中的参数
train_step = tf.train.AdamOptimizer(learning_rate).minimize(cross_entropy)
在上面的代码中,cross_entropy 定义了真实值和预测值之间的交叉熵,这是分类问题中一个常用的损失函数。第二行train_step定义了反向传播的优化方法。目前TensorFlow支持7种不同的优化器,读者可以根据具体的应用选择不同的优化算法。比较常用的优化方法有三种:tf.train.AdamOptimizer和tf.train.MomentumOptimizer。在定义了反向传播算法之后,通过运行sess.run(train_step)就可以对所有在GraphKeys.TRAINABLE_VARIABLES集合中的变量进行优化,使得在当前batch下损失函数更小。
4.5完整神经网络样例程序
下面给出一个完整的程序来训练神经网络解决二分类问题:
import tensorflow as tf
# 通过numpy工具生成模拟数据集
from numpy.random import RandomState
# 定义训练数据batch的大小
batch_size = 8
# 定义神经网络的参数,这里还是沿用前面小节给出的神经网络结构
w1 = tf.Variable(tf.random_normal([2,3], stddev=1, seed=1))
w2 = tf.Variable(tf.random_normal([3,1], stddev=1, seed=1))
# 在shape的一个维度上使用None可以方便使用不同的batch大小。
x = tf.placeholder(tf.float32, shape=(None,2),name = 'x-input')
y_ = tf.placeholder(tf.float32, shape=(None,1), name='y-input')
# 定义神经网络前向传播的过程
a = tf.matmul(x, w1)
y = tf.matmul(a, w2)
# 定义损失函数和反向传播算法
y = tf.sigmoid(y)
cross_entropy = -tf.reduce_mean(
y_*tf.log(tf.clip_by_value(y, 1e-10, 1.0)) + (1-y_)*tf.log(tf.clip_by_value(1-y, 1e-10, 1.0)))
train_step = tf.train.AdamOptimizer(0.001).minimize(cross_entropy)
# 通过随机数生成一个模拟数据集
rdm = RandomState(1)
dataset_size = 128
X = rdm.rand(dataset_size, 2)
# 定义规则来给出样本的标签。
Y = [[int(x1+x2<1)] for (x1,x2) in X]
# 创建一个会话来运行TensorFlow程序
with tf.Session() as sess:
init_op = tf.global_variables_initializer()
# 初始化变量
sess.run(init_op)
print(sess.run(w1))
print(sess.run(w2))
#设定训练的轮数
STEPS = 5000
for i in range(STEPS):
# 每次选取batch_size个样本进行训练
start = (i * batch_size) % dataset_size
end = min(start+batch_size, dataset_size)
# 通过选取的样本训练神经网络并更新参数
sess.run(train_step, feed_dict = {x:X[start:end],y_:Y[start:end]})
if i % 100 == 0:
# 每隔一段时间计算在所有数据上的交叉熵并输出
total_cross_entropy = sess.run(cross_entropy, feed_dict = {x:X,y_:Y})
print("After %d training step(s), cross entropy on all data is %g" %(i, total_cross_entropy))
print(sess.run(w1))
print(sess.run(w2))
运行代码:得到结果如下:
[[-0.8113182 1.4845988 0.06532937]
[-2.4427042 0.0992484 0.5912243 ]]
[[-0.8113182 ]
[ 1.4845988 ]
[ 0.06532937]]
After 0 training step(s), cross entropy on all data is 1.89805
After 100 training step(s), cross entropy on all data is 1.62943
After 200 training step(s), cross entropy on all data is 1.40099
After 300 training step(s), cross entropy on all data is 1.19732
After 400 training step(s), cross entropy on all data is 1.02375
After 500 training step(s), cross entropy on all data is 0.887612
After 600 training step(s), cross entropy on all data is 0.790222
After 700 training step(s), cross entropy on all data is 0.727325
After 800 training step(s), cross entropy on all data is 0.689437
After 900 training step(s), cross entropy on all data is 0.667623
After 1000 training step(s), cross entropy on all data is 0.655075
After 1100 training step(s), cross entropy on all data is 0.647813
After 1200 training step(s), cross entropy on all data is 0.643196
After 1300 training step(s), cross entropy on all data is 0.639896
After 1400 training step(s), cross entropy on all data is 0.637246
After 1500 training step(s), cross entropy on all data is 0.635031
After 1600 training step(s), cross entropy on all data is 0.633027
After 1700 training step(s), cross entropy on all data is 0.631151
After 1800 training step(s), cross entropy on all data is 0.629368
After 1900 training step(s), cross entropy on all data is 0.627724
After 2000 training step(s), cross entropy on all data is 0.626172
After 2100 training step(s), cross entropy on all data is 0.624697
After 2200 training step(s), cross entropy on all data is 0.623293
After 2300 training step(s), cross entropy on all data is 0.622006
After 2400 training step(s), cross entropy on all data is 0.620801
After 2500 training step(s), cross entropy on all data is 0.619664
After 2600 training step(s), cross entropy on all data is 0.618592
After 2700 training step(s), cross entropy on all data is 0.617622
After 2800 training step(s), cross entropy on all data is 0.616723
After 2900 training step(s), cross entropy on all data is 0.615883
After 3000 training step(s), cross entropy on all data is 0.615096
After 3100 training step(s), cross entropy on all data is 0.614397
After 3200 training step(s), cross entropy on all data is 0.613756
After 3300 training step(s), cross entropy on all data is 0.61316
After 3400 training step(s), cross entropy on all data is 0.612608
After 3500 training step(s), cross entropy on all data is 0.612126
After 3600 training step(s), cross entropy on all data is 0.611688
After 3700 training step(s), cross entropy on all data is 0.611285
After 3800 training step(s), cross entropy on all data is 0.610913
After 3900 training step(s), cross entropy on all data is 0.610594
After 4000 training step(s), cross entropy on all data is 0.610309
After 4100 training step(s), cross entropy on all data is 0.610046
After 4200 training step(s), cross entropy on all data is 0.609804
After 4300 training step(s), cross entropy on all data is 0.609603
After 4400 training step(s), cross entropy on all data is 0.609423
After 4500 training step(s), cross entropy on all data is 0.609258
After 4600 training step(s), cross entropy on all data is 0.609106
After 4700 training step(s), cross entropy on all data is 0.608983
After 4800 training step(s), cross entropy on all data is 0.608874
After 4900 training step(s), cross entropy on all data is 0.608772
[[ 0.02476984 0.5694868 1.6921942 ]
[-2.1977348 -0.23668921 1.1143897 ]]
[[-0.45544702]
[ 0.4911093 ]
[-0.9811033 ]]
以上程序实现了训练神经网络的全部过程。从这段程序中可以总结出训练神经网络的过程可以分为以下三个步骤:
1.定义神经网络的结构和前向传播的输出结果
2.定义损失函数以及选择反向传播优化的算法
3.生成会话(tf.Session)并且在训练数据上反复运行反向传播优化算法。
无论神经网络的结构如何变化,这三个步骤是不变的。