第三章:TensorFlow入门
* 3.1
TensorFlow计算模型 - 计算图
* 3.1.1
计算图的概念
* 3.1.2
计算图的使用
* 3.2
TensorFlow数据模型 - 张量
* 3.2.1
张量的概念
* 3.2.2
张量的使用
* 3.3
TensorFlow运行模型 - 会话
* 3.4
TensorFlow实现神经网络
* 3.4.1
TensorFlow游乐场及神经网络简介
* 3.4.2
前向传播算法简介
* 3.4.3
神经网络参数&TensorFlow变量
* 3.4.4
通过TensorFlow训练神经网络模型
* 3.4.5
完整神经网络样例程序
-3.1- TensorFlow计算模型 - 计算图
计算图是 TensorFlow 中最基本的 一个概念, TensorFlow 中的所有计算都会被转化为计 算图上的节点 。
-3.1.1- 计算图的概念
TensorFlow 的名字中己经说明了它最重要的两个概念一一Tensor 和 Flow。
Tensor
就是张量。
张量这个概念在数学或者物理学中可以有不同的解释 , 但在本书中并不强调它本身 的含义 。
在 TensorFlow 中,张量可以被简单地理解为多维数组 , 在 3.2 节中将对张量做更 加详细的介绍 。
如果说 TensorFlow 的第一个词 Tensor表明了它的数据结构,那么 Flow 则 体现了它的计算模型。
Flow
翻译成中文就是“流”,它直观地表达了张量之 间通过计算相 互转化的过程。
TensorFlow 是一个通过计算图 的形式来表述计算的编程系统 。
TensorFlow 中的每一个计算都是计算图上 的一个节点,而节点之间的边描述了计算之间的依赖关系。
tf的图Graph中:
*
每一个节点
都是一个运算
;
*
每一条边
可以理解为被运算节点驱使流动起来的Tensor
,代表了计算之间的依赖关系 ,如果一个运算的输入依赖于另一个运算的输出,那么这两个运算有依赖关系。
上图展示了通过 TensorBoard1画出来的第 2 章中两个向量相加样例的计算图。
在上图中:
*
a节点
和b节点
是两个常量(赋值运算)节点
,赋值运算不需要依赖输入Tensor,不依赖任何其他计算(输出的Tensor),所以没有输入边;
*
而add节点
这个向量相加运算节点
则需要指定输入的两个Tensor,于是图中的add节点
有两个输入边:一条从a到add的边 & 一条从b到add的边。
*
没有任何其他计算节点
依赖 add 的结果,于是代表加法的节点 add 没有任何指向其他节点的边。
所有 TensorFlow 的程序都可以通过类似图 3-1 所示的计算图的形式来表示,这就是 TensorFlow 的基本计算 模型。
-3.1.2- 计算图的使用
TensorFlow程序一般可以分为两个阶段。
*
在第一个阶段需要定义计算图中所有的计算
。 比如在第 2 章的向量加法样例程序中首先定义了两个输入,然后定义了 一个计算来得到它 们的和。
*
第二个阶段为执行计算图中所有的计算
,这个阶段将在 3.3 节中介绍。以下代码给出了计算定义阶段的样例。
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
* 载入 TensorFlow
在 Python 中一般会采用“impo口tensorflowas tf”的形式来载入 TensorFlow,这样:
*
可以使用“tf”来代替“tensorflow”作为模块名称,使得整个程序更加简洁。这是 TensorFlow 中非常常用的技巧,在本书后面的章节中将会全部采用这种加载万式 。
在这个过程中,TensorFlow 会自动
将定义的计算转化为
计算图上的节点 。在 TensorFlow 程序中,系统会自动维护
一个默认的计算图,通过 tf.get_default_graph 函数可以获取当前默认的计算图。
以下代码示意了如何获取默认计算图以及如何查看一个运算所属的计算图 。
print(a.graph is tf.get_default_graph())
# 判断张量a所属的计算图是否是当前载入TensorFlow后默认的计算图
# 该步输出将会为True
通过a.graph可以查看张量a所属的计算图
因为没有特意指定,所以这个计算图应该等于当前默认的计算图
所以上面的操作输出值为True。
* 指定新的计算图
除了使用默认的计算图, TensorFlow 支持通过
tf.Graph 函数
来生成新的计算图
。不同计 算图上的张量和运算都不会共享。
以下代码示意了如何在不同计算图上定义和使用变量。
import tensorflow as tf
g1 = tf.Graph()
with g1.as_default():
# 在计算图g1中定义变量"v",并设置初始值为0。
v = tf.get_variable(
"v",
initializer=tf.zeros_initializer(shape=[1])
)
g2 = tf.Graph()
with g2.as_default():
# 在计算图g2中定义变量"v",并设置初始值为1。
v = tf.get_variable(
"v",
initializer=tf.ones_initializer(shape=[1])
)
with tf.Session(graph=g1) as sess:
# 在计算图g1中读取变量v的取值
tf.global_variables_initializer().run()
with tf.variable_scope("", reuse=True):
# 在计算图g1中,变量v的取值应该是0
# 所以下面这行会输出[0. ]
print(sess.run(tf.get_variable("v")))
with tf.Session(graph=g2) as sess:
# 在计算图g2中读取变量v的取值
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
使用GPU的具体方法将在第12章详述。
有效地整理 TensorFlow程序中的
资源
也是计算图的一个重要功能。
在一个计算图中,可以通过集合 Ccollection)
来管理不同类别的资 源。
比如通过tf.add_to_collection()
函数可以将资源加入一个或多个集合中,
然后通过tf.get_collection()
获取一个集合里面的所有资源。
这里的资源
可以是:
*
张量
*
变量
*
运行 TensorFlow 程序所需要的队列资源
*
等等
为了方便使用,TensorFlow 也自动管理了一些 最常用的集合 , 表3-1总结了最常用的几个自动维护的集合。
集合名称 | 集合内容 | 使用场景 |
---|---|---|
tf.GraphKeys.VARIABLES | 所有变量 | 持久化TensorFlow |
tf.GraphKeys.TRAINABLE_VARIABLE | 可学习的、待训练的参数 | 模型训练、生成模型可视化内容 |
tf.GraphKeys.SUMMERIES | 日志生成相关的张量 | TensorFlow计算可视化 |
tf.GraphKeys.QUEUE_RUNNERS | 处理输入的QueueRunner | 输入处理 |
tf.GraphKeys.MOVING_AVERAGE_VARIABLES | 所有计算了滑动平均值的变量 | 计算变量的滑动平均值 |
-3.2- TensorFlow数据模型 - 张量
这一节将介绍 TensorFlow 中另外一个基础概念一一张量 。
张量是 TensorFlow 管理数据的形式。
-3.2.1- 张量的概念
⚠️*
Tensor
是对运算节点
的计算结果/输出边
的引用
⚠️
在 TensorFlow 程序中,所有的数据都通过
张量
的形式来表示。
从功能的角度上看,张量可以被简单理解 为多维数组
。
*
其中零阶张量
表示标量(scalar),也就是一个数
;
*
第一阶张量
为向量(vector), 也就是一个一维数组
;
*
第n阶张量
可以理解为一个n维数组
。
但
张量
在 TensorFlow 中的实现并不是
直接采用数组
的形式,
它只是对TensorFlow 中运算结果的引用
。
在张量中并没有真正保存数字,它保存的是如何得到这些数字的计算过程
。
⚠️TensorFlow中的张量
本身,就是对计算节点
的结果
的引用。
还是以向量加法为例,当运行 如下代码时,并不会得到加法的结果,而会得到对结果的一个引用。
import tensorflow as tf
# tf.constant本身是一个计算
# 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计算的结果不是一个具体的数字,而是一个张量的结构。
从上面代码的运行结果可以看出,一个张量主要保存了三个属性:
*
名字(name)
*
维度(shape)
*
类型(type)
* 张量的第一个属性:名字(name)
张量的第一个属性
名字(name)
:
*
不仅是一个张量的唯一标识符
,
*
它同样也给出了这个张量是如何计算出来的
。
*
TensorFlow的计算都可以通过计算图的模型来建立, 计算图上的每一个节点代表了一个计算;计算节点计算完的结果就保存在张量之中。
*
所以张量
和计算图上计算节点
所代表的计算结果
是对应的。
这样张量的命名就可 以通过 “node:src_output”的形式来给出。其中:
*
node 为节点的名称
*
src_output表示当前张量来自节点的第几个输出。
比如上面代码打出来的“add:0”就说明了 result这个张量是计算节点“add” 输出的第一个结果(编号从 0 开始)。
* 张量的第二个属性:维度(shape)
张量的第二个属性是张量的维度( shape)。这个属性描述了一个张量的维度信息。比 如上面样例中 shape=(2,)说明了张量 result是一个一维数组, 这个数组的长度为 2。维度是 张量一个很重要的属性 , 围绕张量的维度 TensorFlow 也给出了很多有用的运算,在这里先 不 一一 列举,在后面的章节中将使用到部分运算。
* 张量的第三个属性:类型(type)
张量的第三个属性是类型( 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 的类型为实数,这样程序会报类型不匹配的错误 :
ValueError: Tensor conversion requested dtype int32 for Tensor
with dtype float32 : 'Tensor ("b : 0", shape=(2, ), dtype=float32 )'
如果将第一个加数指定成实数类型
a= tf.constant([l, 2], name="a”, dtype=tf.float32)
, 那么两个加数的类型相同就不会报错了。
如果不指定类型, TensorFlow会给出默认的类型,比如不带小数点的数会被默认为 int32,带小数点的会默认为 float32。
因为使用默认类型有可能会导致潜在的类型不匹配问题,所以一般建议通过指定 dtype 来明确指出变量或者常量的类型。
* TensorFlow支持的数据类型
TensorFlow支持 14种不同的类型, 主要包括了:
*
实数(tf.float32、 tf.float64)、
*
整数(tf.int8、 tf.int16、 tf.int32、 tf.int64、 tf.uint8)、
*
布尔型(tf.bool)
*
复数(tf.complex64、 tf.complex128 )
-3.2.2- 张量的使用
张量使用主要可以总结为两大类 :
*
第一类用途是对中间计算结果的引用
*
使用张量的第二类情况是当计算图构造完成之后,张量可以用来获得计算结果
,也就是得到真实的数字
* 张量的使用1: 对中间计算结果的引用
当一个计算包含很多中间结果时,使用张量可以大大提高代码的可读性。以下为使用张量和不使用张量记录中间结果
来完成向量相加的功能的代码对比:
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
# 直接计算向量的和,这样可读性会比较差
result = tf.constant([1.0, 2.0], name="a") + tf.constant([2.0, 3.0], name="b")
从上面的样例程序可以看到,a和b其实就是对
常量生成
这个运算的结果
的引用,
*
这样在做加法时就可以直接使用
这两个变量,而不需要再去生成这些常量。 当计算的复杂度增加时(比如在构建深层神经网络时),通过张量来引用计算的中间结果可以使代码的可阅读性
大大提升 。
*
同时,通过张量来存储中间结果可以方便获取中间结果
。 比如在卷积神经网络中,卷积层或者池化层有可能改变张量的维度,通过result.get_shape() 函数来获取结果张量的维度信息可以免去人工计算的麻烦。
* 张量的使用2: 获得计算结果
使用张量的第二类情况是:当计算图构造完成之后,张量可以用来获取计算结果,也就是得到真实的数字。虽然张量本身没有存储具体的数字,但是通过3.3节中介绍的会话,就可以得到这些具体的数字。比如在上述代码中使用tf.Session().run(result)
语句得到计算结果。
-3.3- TensorFlow运行模型 - 会话
前面的两节介绍了 TensorFlow 是如何
组织数据
和定义运算
的。本节将介绍如何使用 TensorFlow 中的会话( session)来执行
定义好的运算。
会话拥有并管理 TensorFlow 程序运行时的所有资源。所有计算完成之后需要关闭会话来帮助系统回收资源,否则就可能出现资源泄漏的问题。 TensorFlow 中使用会话
的模式一般有两种:
*
明确调用会话生成函数
和关闭会话函数
*
通过Python的with关键字
和上下文管理器
来使用会话
* 明确调用会话生成函数和关闭会话函数
import tensorflow as tf
# 创建一个会话
sess = tf.Session()
# 使用创建好的会话来得到关心的运算的结果
# 比如可以调用sess.run(result)
# 来得到3.1节样例中张量result的取值
sess.run(...)
# 关闭会话使得本次运行中使用到的资源可以背释放
sess.close()
使用这种模式,在所有计算完成之后,需要明确调用Session.close()函数来关闭会话并释放资源。
* 通过Python的with关键字和上下文管理器来使用会话
然而,当程序因为异常而退出时,关闭会话的函数可能就不会被执行从而导致资源泄漏 。
为了解决异常退出时资源释放的问题, TensorFlow 可以通过 Python 的上下文 管理器来使用会话。
import tensorflow as tf
# 创建一个会话
# 并通过Pythonx的with关键字和上下文管理器来管理这个会话
with tf.Session() as sess:
# 通过创建好的会话来计算关心的结果
sess.run(...)
# 不需要调用sess.close()函数来关闭会话
# 当上下文退出时,会话关闭和资源释放也自动完成了
通过 Python 上下文管理器的机制,只要将所有的计算放在 “with”的内部就可以 。 当上下文管理器退出时候会自动释放所有资源。这样
*
既解决了因为异常退出时资源释放的问题,
*
同时也解决了忘记调用 Session.close() 函数而产生的资源泄漏。
⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
* 默认的计算图 & 默认的会话
3.1 节介绍过 TensorFlow在被载入的时候,会自动生成一个默认的
计算图
,如果没有特殊指定,运算会自动加入这个计算图中。
TensorFlow 中的会话
也有类似的机制,但 TensorFlow 不会自动生成默认的会话,而是需要手动指定 。
⚠️当通过tf.Session()初始化一个会话对象
之后,需要将其指定
为默认的会话
;
⚠️在指定
完默认的会话
之后,才可以通过tf.Tensor.eval()
函数来计算一个张量的取值。
以下代码展示了,通过设定默认会话
,计算张量的取值:
sess = tf.Session()
with sess.as_default():
print(result.eval())
以下代码也可以完成相似的功能:
sess = tf.Session()
# 以下两个命令有相似的功能
print(sess.run(result))
print(result.eval(session=sess))
* Python 交互式环境
在交互式环境下(比如 Python 脚本或者Jupyter的编辑器下),通过设置
默认会话
的方式来获取张量的取值
更加方便。
所以TensorFlow提供了一种在交互式环境下直接构建默认会话的函数。这个函数就是tf.lnteractiveSession()
。
使用这个函数会自动将生成的会话注册为默认会话。
以下代码展示了 tf.InteractiveSession() 函数的用法:
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.InteractivateSession()
print(result.eval())
sess.close()
通过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)
-3.4- TensorFlow实现神经网络
在这一节中,将结合神经网络的功能进一步介绍如何通过TensorFlow来实现神经网络。
*
3.4.1节:TensorFlow游乐场 & 神经网络
*
3.4.2 节:前向传播( Forward-Propagation)
算法
*
3.4.3 节:TensorFlow中的变量
& 神经网络参数的表达
*
3.4.4 节:神经网络反向传播(Back-Propagation)
算法
*
3.4.5 节:完整的TensorFlow程序在随机的数据上训练一个简单的神经网络
-3.4.1- TensorFlow游乐场及神经网络简介
TensorFlow游乐场:http://playground.tensorflow.org
上图中可以先着重关注两个框:
*
Feature框:输入每条样本(x,y)
的各种特征:
、、、、、、
*
隐藏层:由神经网络各个节点
和他们之间的带权重的连线
组成
节点
:可以在某种意义上理解为特征提取
带权重的连线
:对一层特征提取节点的各结果进行有权重处理
-3.4.2- 前向传播算法简介
前向传播算法可以很容易地转化成矩阵乘法
的形式,在TensorFlow中矩阵乘法是非常容易实现的。
以下TensorFlow 程序实现了图 3-5 所示神经网络的前向传播过程:
import tensorflow as tf
a = tf.matmul(x, w1) # 向量x和权重矩阵w1相乘,得到向量a
y = tf.matmul(a, w2) # 向量a和权重矩阵w2相乘,得到向量y
其中
tf.matmul()
实现了矩阵乘法的功能。
到此为止己经详细地介绍了神经网络的前向传 播算法,并且给出了TensorFlow程序来实现这个过程 。
在之后的章节中会继续介绍更加复杂的
神经元
结构:
*
偏置 (bias)
*
激活函数 (activation缸nction)
等
也会介绍更加复杂的神经网络结构
:
*
卷积神经网络
*
LSTM 结构
等
对于这些更加复杂的神经网络, TensorFlow 也提 供了很好的支持,后面的章节中再详细介绍。
-3.4.3- 神经网络参数&TensorFlow变量
神经网络中的
待训练参数
是神经网络实现分类或者回归问题中重要的部分。
本节将更加具体地介绍TensorFlow是如何组织、保存以及使用神经网络中的待训练参数的。
在TensorFlow中,变量(tf.Variable)
的作用就是保存和更新神经网络中的(待训练)参数
。
* 变量初始化 & 随机值
和其他编程语言类似, TensorFlow中的变量也需要指定
初始值
。
因为在神经网络中,给参数赋予随机初始值
最为常见,所以 一般也使用随机数给TensorFlow中的变量初始化。
下面一段代码给出了一种在TensorFlow 中声明一个的矩阵变量的方法:
import tensorflow as tf
weights = tf.Variable(
tf.random_normal([2, 3], stddev=2)
)
*
这是一个变量声明
运算节点
*
该运算节点的输出Tensor的名称为weights
这段代码调用了TensorFlow的
变量声明函数tf.Variable()
。
在变量声明函数中给出了初始化这个变量
的方法。
TensorFlow 中变量的初始值可以设置成随机数、 常数或者是通过其 他变量的初始值计算得到。
在上面的样例中,tf.random_normal([2, 3], stddev=2)
会产生一个2*3的矩阵,矩阵中的元素
是均值为0
,标准差为2
的随机数。 tf.random normal()函数可以通过参数mean
来指定平均值,在没有指定时默认为0。
通过满足正态分布的随机数来初始化神经网络中的参数是一个非常常用的方法。 除了正态分布的随机数, TensorFlow 还提供了一些其他的随机数生成器, 表3-2列出了TensorFlow目前支持的所有随机数生成器。
函数名称 | 随机数分布 | 主要参数 |
---|---|---|
tf.random_normal() |
正态分布 | 平均值、标准差、取值类型 |
tf.truncated_normal() |
正态分布,但是如果随机出来的数值 偏离平均值超过2这个标准差, 那么这个数将会被重新随机生成 |
平均值、标准差、取值类型 |
tf.random_uniform() |
均匀分布 | 平均值、标准差、取值类型 |
tf.random_gamma() |
Gamma分布 | 平均值、标准差、取值类型 |
TensorFlow也支持通过常数来初始化一个变量。表 3-3给出了 TensorFlow 中常用的常 量声 明方法 。
函数名称 | 功能 | 样例 | 结果 |
---|---|---|---|
tf.zeros() |
产生一个全0的数组?
|
tf.zeros([2,3], int32) | [[0, 0, 0], [0, 0, 0]] |
tf.ones() |
产生一个全1的数组?
|
tf.ones([2,3], int32) | [[1, 1, 1], [1, 1, 1]] |
tf.fill() |
产生一个元素全部为指定数字的数组?
|
tf.fill([2,3], 9) | [[9, 9, 9], [[9, 9, 9]] |
tf.constant() |
产生一个给定值的常量
|
tf.constant([1,2,3]) | [1, 2, 3] |
神经网络中的偏置项(bias)
,通常会使用常数
来初始化,以下代码给出一个样例:
import tensorflow as tf
bias = tf.Variable(
tf.zeros([3])
)
以上代码是一个
声明变量
的运算节点,该运算节点的输出的是名为"bias
"的Tensor;
在声明变量的同时,进行了初始化
,初值为长度为3、元素值全为0的向量。
除了使用随机数或者常数,TensorFlow也支持通过其他变量的初始值
来初始化新的变量。 以下代码给出了具体的方法:
import tensorflow as tf
weights = tf.Variable(tf.random_normal([2, 3], stddev=2))
w1 = tf.Variable(weights.initialized_value())
w2 = tf.Variable(weights.initialized_value () * 2.0)
在以上的代码中,w2的初值被置为了与weights变量相同,w3的初值则是weights初值的两倍。
* 变量初始化
在TensorFlow中,一个
变量定义运算节点
的输出Tensor
的值在被使用之前,这个变量定义的初始化过程
需要被明确地调用
。
即:
变量定义运算节点
→明确初始化
→使用该节点的输出Tensor
以下样例介绍了如何通过变量实现神经网络的参数并实现前向传播的过程:
import tensorflow as tf
# 声明w1、w2两个变量
# 这里还通过seed参数设置了随机种子
# 这样可以保证每次运行得到的结果是一样的
w1 = tf.Variable(tf.random_normal((2,3), stddev=1, seed=1))
w2 = tf.Variable(tf.random_normal((2,3), stddev=1, seed=1))
# 暂时将输入的特征向量定义为一个常量
# 注意这里x是一个1*2维的矩阵
x = tf.constant([[0.7, 0.9]])
# 通过3.4.2节描述的前向传播算法获得神经网络的输出
a = tf.matmul(x, w1)
y = tf.matmul(a, w2)
sess = td.Session()
# 与3.4.2节的计算不同:
# ⚠️这里不能⚠️直接通过sess.run(y)来计算y的取值
# 因为之前定义了w1、w2这两个⚠️变量⚠️
# 但是却没有明确对其初始化
# 以下两行分别明确地初始化了w1、w2两个变量
sess.run(w1.initializer) # 初始化w1
sess.run(w2.initializer) # 初始化w2
print(sess.run(y)) # 输出[[3.95757794]]
sess.close()
以上程序实现了神经网络的前向传播过程。
从这段代码可以看到,当声明了变量w1、w2之后,可以通过w1和w2来定义神经网络的前向传播过程
,并得到中间结果a和最后答案y。
在 TensorFlow 程序的第二步会声明一个会话( session)
,并通过会话计算结果
。
在上面 的样例中, 当会话定义完成之后就可以开始真正运行定义好的计算了。
但在计算 y 之前,需要将所有
用到的变量
初始化
。
也就是说,虽然在变量定义时给出了变量初始化的方法, 但这个方法并没有被真正运行。
所以在计算 y 之前,需要通过运行 w1.initializer 和 w2. initializer 来给变量赋值。
虽然直接调用每个变量的初始化过程是一个可行的方案,
但是当变量数目增多,或者变量之间存在依赖关系时,单个调用的方案就比较麻烦了 。
为了解决这个问题, TensorFlow提供了一种更加便捷的方式来完成变量初始化过程。
以下程序展示了 通过 tf.global_variables_initializer 函数实现初始化所有变量的过程 :
import tensorflow as tf
init_op = tf.global_variables_initializer()
sess.run(init_op)
通过 tf.global_variables_initializer 函数,就不需要将
变量
一个一个初始化了 。
这个函数 也会自动处理变量之间
的依赖关系。
下面的章节都将使用这个函数来完成变量
的初始化过程。
* 变量和张量的关系
*
定义变量
是一个运算节点
*
该运算节点的输出是一个张量
*
下面将进一步介绍 tf.Variable 操作在 TensorFlow 中底层是如何实现的。
图 3-7 给出了神经网络前向传播样例程序的 TensorFlow 计算图的一个部分,这个部分显示了和变量w1相关的
操作。
*
图中左上角的黑色椭圆代表了w1的变量定义运算节点,可以看出这是一个Variable定义运算节点及其输出Tensor
*
图中右下角是read操作
和matmul乘法运算操作
;
*
初始化
变量 w1 的操作是通过Assign 操作
完成的。
在图 3-7 上可以看到Assign这个节点的输入,是随机数生成函数
的输出
输出赋值给了变量w1,这样就完成了变量初始化的过程
3.1.2 节介绍了 TensorFlow 中集合( collection)的概念,所有的变量都会被自动地加入到
GraphKeys.VARIABLES
这个集合中。
通过tf.global_variables()
函数可以拿到当前计算图 上所有的变量 。
拿到计算图上所有的变量有助于持久化整个计算图的运行状态
,在第 5 章 中将更加详细地介绍 。
* trainable - 待优化的变量
当构建机器学习模型时,比如神经网络,可以通过变量声明函数中的
trainable 参数
来区分需要优化的参数(比如神经网络中的待训练参数矩阵)和其他参数(比如选代的轮数)。
如果声明变量时参数 trainable
为True,那么这个变量将会被加入到GraphKeys.TRAINABLE_VARIABLES 集合
。
在TensorFlow中可以通过tf.trainable_variables()
函数得到所有需要优化的参数 。
TensorFlow中提供的神经网络优化算法会将GraphKeys.TRAINABLE_VARIABLES集合
中的变量作为默认的优化对象。
⚠️前面说过,变量是变量定义运算节点
的输出Tensor,即:变量是一种特殊的张量;
⚠️前面说过,张量有三个属性:Name、Shape、Type
⚠️变量作为一种特殊的张量,除了变量名之外,变量的Shape、Type也是其非常重要的属性
* 变量的类型
和大部分程序语言类似,
变量的类型
是不可改变
的。
TF中,一个变量在构建之后,它的类型就不能再改变了 。
比如在上面给出的前向传播样例中,w1的类型为randomnormal()结果的默认类型tf.float32
, 那么它将不能被赋予其他类型的值。
以下代码将会报出类型不匹配的错误:
w1 = tf.Variable(tf.random_normal([2, 3], stddev=2), name="calculate_node_w1")
w2 = tf.Variable(tf.random_normal([2, 3], stddev=2, dtype=tf.float64), name="calculate_node_w2")
w1.assign(w2) # 明确调用变量的初始化操作
* 变量的维度
维度是变量另一个重要的属性。
和类型不大一样的是,维度在程序运行中是有可能改变
的,
但是需要通过设置参数validate_shape=False
。
下面给出了一段示范代码:
import tensorflow as tf
w1 = tf.Variable(tf.random_normal([2, 3], stddev=2), name="calculate_node_w1")
w2 = tf.Variable(tf.random_normal([2, 3], stddev=2), name="calculate_node_w2")
tf.assign(w1, w2)
# 会报错
# ValueError: Dimension 1 in both shapes must be equal, but are 3 and 2
# for 'Assign_1' (op: 'Assign') with input shapes : [2 , 3], [2,2]. tf.assign(w1, w2)
tf.assign(w1, w2, validate_shape=False)
# 不会报错
# 以为有设置参数 validate_shape=False
⚠️虽然 TensorFlow支持更改变量的维度,但是这种用法在实践中比较罕见。
-3.4.4- 通过TensorFlow训练神经网络模型
* 监督学习 & 参数(矩阵) & 训练过程
这一节将简单介绍使用
监督学习
的方式来更合理地设置参数取值,
同时也将给出 TensorFlow程序来完成这个过程。
学习更新神经网络参数(矩阵)
的过程就是神经网络的训练
过程。
只有经过有效训练的神经网络模型才可以真正地解决分类或者回归问题。
图 3-8 对比了训练之前和训练之后神经网络模型的分类效果:
从图中可以看出,模型在训练之前是 完全无法区分黑色点和灰色点的,
但经过训练之后区分效果已经很好了 。
* 标注好的训练数据集
* 监督学习: 预测结果接近真实的答案
监督学习最重要的思想就是,在己知答案的标注数据集上 ,模型给出的预测结果要尽 量接近真实的答案。
通过调整神经网络中的参数对 训练数据进行拟合,可以使得模型对未 知的样本提供预测的能力。
* 反向传播算法(Back-Propagation)
* 迭代相关的概念
*
:样本集的总样本量
*
:全部样本传入模型参与训练的轮数
*
:一次迭代计算loss时的传入样本量
*
:迭代次数/计算loss次数/更新参数矩阵次数
通过 TensorFlow实现反向传播算法的第一步是使用 TensorFlow表达一个 batch的数据。
在 3.4.3 节中尝试过使用常量来表达过一个样例
:
import tensorflow as tf
x = tf.constant([[0.7, 0.9]])
但如果每轮迭代选取数据时都通过常量来表示,那么TensorFlow的计算图将会
太大
。
因为每生成一个常量, TensorFlow都会在计算图中增加一个节点。
一般来说,一个神经网络的训练过程会需要经过几百万轮甚至几亿轮的迭代,
这样计算图就会非常大,而且利用率很低。
* placeholder
为了避免这个问题, TensorFlow提供了placeholder机制用于提供输入数据。
placeholder 相当于定义了一个位置,这个位置中的数据在程序运行时再指定。
这样在程序 中就不需要生成大量常量来提供输入数据,而只需要将数据通过 placeholder 传入 TensorFlow 计算图 。
在 placeholder定义时,这个位置上的数据类型是需要指定的。
和其他 张量一样, placeholder 的类型也是不可以改变的。
placeholder 中数据的维度信息可以根据 提供的数据推导得出,所以不一定要给出。
下面给出了通过 placeholder实现前向传播算法 的代码:
import tensorflow as tf
w1 = tf.Variable(tf.random_normal([2, 3], stddev=1), name="calculate_node_w1")
w2 = tf.Variable(tf.random_normal([3, 1], stddev=1), name="calculate_node_w2")
# 定义placeholder作为存放输入数据的占位符
# placeholder的维度不强制定义
# 但是如果维度已经确定,可以通过指定维度,降低出错的可能性
x = tf.placeholder(tf.float32, shape=(1, 2), name="calculate_node_placeholder_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))
# 会报错
# InvalidArgumentError: You must feed a value for placeholder
# tensor 'Input_1' with dtype float and shape [1,2]
print(sess.run(
y ,
feed_dict={x: [[0.7,0.9]]}
))
# 不会报错
在这段代码中,使用placeholder替代了之前代码中常量定义的x。
在新的程序进行前向传播计算时,需要提供一个feed_dict来指定x的取值。
feed_dict是一个字典(map),在字典中需要给出 每个用到的 placeholder 的取值。
如果某个需要的 placeholder 没有被指定取值,那么程序在 运行时将会报错。
* Placeholder & Batch
以上程序只计算了一个样例的前向传播结果,但如图3-9所示,在训练神经网络时需要每次提供一个batch的训练样例。
对于这样的需求,placeholder也可以很好地支持。
在上面的样例程序中,如果将输入的矩阵改为的矩阵,那么就可以得到个样例的前向传播结果了。
其中的矩阵的每一行
为一条样本
。
这样前向传播的结果
为的矩阵,
这个矩阵的每一行
就代表了一条样本的前向传播结果
。
以下代码给出了 一个示例:
import tensorflow as tf
x = tf.placeholder(tf.float32, shape=(3,2), name="calculate_node_placeholder_x")
... # 中间代码可上面的一样
# 因为x在定义时,指定了n为3
# 所以在运行前向传播过程时,需要提供3条样本
print(sess.run(
y,
feed_dict={
x:[
[0.7, 0.9],
[0.1, 0.4],
[0.5, 0.8]
]
}
))
# 输出结果为:
# [
# [ 3.95757794 ]
# [ 1.15376544 ]
# [ 3.16749191 ]
# ]
以上样例展示了一次性计算多个样例的前向传播结果 。
在运行时,需要将三个样例 [0.7, 0.9]、[0.1, 0.4]和[0.5, 0.8]组成一个的矩阵传入placeholder。
计算得到的结果为的(结果)矩阵。
其中:
第一行 3.95757794为样例[0.7,0.9] 的前向传播结果
1.15376544为样例[0.1,0.4] 的前向传播结果
3.16749191 为样例[0.5,0.8]的前向传播结果
* Loss & 反向传播
在得到一个batch的前向传播结果之后,
需要定义一个损失函数
来刻画当前的预测值和真实答案之间的差距。
然后通过反向传播算法来迭代神经网络参数的取值
使得差距可以被缩小 。
损失函数和反向传播算法将在第 4 章 中更加详细地介绍 。
以下代码定义了一个简单的损失函数,并通过TensorFlow定义了反向传播的算法:
import tensorflow as tf
# 使用sigmoid函数,将y转换为0~1之间的数值
# 转换后:
# y 代表是预测为正样本的概率
# 1-y 代表是预测为负样本的概率
y = tf.sigmoid(y)
# ?? 此处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 (l-y, 1e-10, 1.0))
)
# 定义学习率,在地4章中将有更加具体的介绍
learning_rate = 0.001
# 定义反向传播算法来优化神经网络中的参数
train_step = tf.train.AdamOptimizer(learning_rate).minimize (cross_entropy)
在以上代码中,cross_entropy定义了预测值(0.01.0)和真实值(0.01.0)之间的交叉熵,这是分类问题中,常用的一个损失函数
第二行,train_step定义了反向传播的优化方法
目前 TensorFlow 支持 10 种不同的优化器,读者可以根据具体的应用选择不同 的优化算法。 比较 常用的优化方法有 三 种:
*
tf.train.GradientDescentOptimizer
*
tf.train.AdamOptimizer
*
tf.train.MomentumOptimizer
在定义了反向传播算法之后,通过运行 sess.run(train_step)就可以对所有在 GraphKeys.TRAINABLE_VARIABLES 集合中 的变量进行优化,使得在当前 batch 下损失函数更小 。
下面的 3.4.5 节将给出 一个完整 的训练神 经 网络样例程序 。
-3.4.5- 完整神经网络样例程序
#! /usr/bin/env python
# -*- coding:utf-8 -*-
import tensorflow as tf
from numpy.random import RandomState
# 定义训练数据的大小
batch_size = 8
# 定义神经网络的待训练参数矩阵
w1 = tf.Variable(tf.random_normal([2, 3], stddev=1, seed=1), name="calculate_node_w1")
w2 = tf.Variable(tf.random_normal([3, 1], stddev=1, seed=1), name="calculate_node_w2")
# 在shape的【第一个维度上】使用None可以方便使用不同的batch大小
# 在训练时,需要把数据分成比较小的batch
# 但是在测试时,可以一次性使用全部的数据
# 当数据集比较小时,这样做可以比较方便测试
# 但数据集比较大时,将大量数据放入一个batch可能会导致内存溢出
x = tf.placeholder(tf.float32, shape=[None, 2], name="calculate_node_placeholder_x_input")
y_ = tf.placeholder(tf.float32, shape=[None, 1], name="calculate_node_placeholder_y_input") # 真实标签
# 定义神经网络前向传播的过程
a = tf.matmul(x, w1)
y = tf.matmul(a, w2)
# 定义损失函数和反响传播算法
y = tf.sigmoid(y) # 此处的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)
# 自定义规则,来给出样本标签
# 这里所有 x1 + x2 < 1的样本都被认为是正样本(比如零件合格)
# 而其他的被认为是负样本(零件不合格)
# 和TensorFlow游乐场中+1、-1不一样的是:
# 这里用1、0来分别表示正负样本
# 大部分解决分类问题的神经网络,都会采用1、0的表示方法
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))
"""
在训练开始之前,待训练参数矩阵你w1、w2的初始化值:
w1 = [
[-0.81131822, 1.48459876, 0.06532937]
[-2.44270396 , 0.0992484 , 0.59122431]
]
w2 = [
[-0.81131822],
[1.48459876],
[0 . 06532937],
]
"""
# 设定训练的轮数(N_iteration)
STEPS = 5000
for i in rang(STEPS):
# 每次选取batch_size个样本进行训练
start = (i * batch_size) % dataset_size
end = min(start + batch_size, dataset_size)
# 通过本batch的样本,训练神经网络并更新参数矩阵
sess.run(train_step,
feed_dict={
x: X[start:end],
y_: Y[start:end]
}
)
if i % 1000 == 0:
# 每迭代1000次,计算在所有数据上的交叉熵,并输出
total_cross_entropy = sess.run(cross_entropy,
feed_dict={
x: X,
y_: Y
}
)
print("After {train_i} training step(s), cross_entropy on all data is {total_cross_entropy};".format(
train_i=i,
total_cross_entropy= total_cross_entropy
))
"""
输出结果:
After 0 training step(s), cross_entropy on all data is 1.89805;
After 1000 training step(s), cross_entropy on all data is 0.655075;
After 2000 training step(s), cross_entropy on all data is 0.626172;
"""
print(sess.run(w1))
print(sess.run(w2))
"""
在训练之后的神经网络参数的值
w1 = [
[0.02476984, 0.5694868, 1.69219422]
[-2.19773483 , -0.23668921 , 1.11438966]
]
w2 = [
[0.45544702],
[0.49110931],
[-0.9811033]
]
可以发现,这两个参数的取值,已经发生了变化
这个变化,就是训练过程中一次次迭代更新后的结果
迭代后的参数矩阵使得这个神经网络能更好地拟合提供的训练数据
"""
以上程序实现了训练神经网络的全部过程。
从这段程序可以总结出训练神经网络的过程可以分为以下三个步骤:
*
定义神经网络的结构以及前向传播的输出预测结果
*
定义损失函数以及反向传播传播更新参数矩阵的方法
*
生成会话,并在训练数据上反复运行前向传播
、计算loss
、反向传播迭代参数
无论神经网络的结构如何变化,这三个步骤是不变的。