《实战Google深度学习框架第二版》之TensorFlow入门

3.1 TensorFlow计算模型——计算图

TensorFlow 的名字中己经说明了它最重要的两个概念一一Tensor 和 Flow。Tensor 就是张量。在 TensorFlow 中,张量可以被简单地理解为多维数组。如果说 TensorFlow 的第一个词 Tensor 表明了它的数据结构,那么 Flow 则 体现了它的计算模型。Flow 翻译成中文就是“流”,它直观地表达了张量之间通过计算相 互转化的过程。 TensorFlow 是一个通过计算图的形式来表述计算的编程系统。TensorFlow 中的每一个计算都是计算图上的一个节点,而节点之间的边描述了计算之间的依赖关系。图 3-1 展示了通过 TensorBoard画出来的第 2 章中两个向量相加样例的计算图。

图 3 -1 中的每一个节点都是一个运算,而每一条边代表了计算之间的依赖关系。 如果一个运算的输入依赖于另一个运算的输出,那么这两个运算有依赖关系。在图 3-1 中, a 和 b 这两个常量不依赖任何其他计算。 而 add 计算则依赖读取两个常量的取值。 于是在 图 3- 1 中可以看到有一条从 a 到 add 的边和一条从 b 到 add 的边。在图 3-1 中,没有任何计 算依赖 add 的结果,于是代表加法的节点 add 没有任何指向其他节点的边。所有 TensorFlow 的程序都可以通过类似图 3-1 所示的计算图的形式来表示,这就是 TensorFlow 的基本计算 模型。

3.1.2 计算图的使用

TensorFlow 程序一般可以分为两个阶段。在第一个阶段需要定义计算图中所有的计算。比如在第 2 章的向量加法样例程序中首先定义了两个输入,然后定义了一个计算来得到它 们的和。第二个阶段为执行计算。以下代码给出了计算定义 阶段的样例。

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 程序中,系统会自动维护一个默认的计算图,通过 tf.get_default_graph 函数可以获取当前默认的计算图。以下 代码示意了如何获取默认计算图以及如何查看一个运算所属的计算图 。

#通过 a . graph 可以查看张-111:所属的计算圈。因为没有特意指定,所以这个计算图应该等于 

#当前默认的计算圈。所以下面这个操作输出值为 True 。 

print(a . graph is tf. get default graph())

除了使用默认的计算图, TensorFlow 支持通过 tf.Graph 函数来生成新的计算图。不同计 算图上的张量和运算都不会共享。以下代码示意了如何在不同计算图上定义和使用变量。 

import tensorflow as tf
gl = tf . Graph() 
with gl. as_default () : 
    #在计算阁 gl 中定义变量“v”,并设置初始值为 0 。
    v = tf. get_variable( 
        ” v ”, initializer=tf. zeros_initializer (shape=[1]))
g2 = tf. Graph() 
with g2.as_default() : 
    #在计算阁 g2 中定义变量“v飞并设置初始值为 l 。
    v = tf. get_variable( 
        ” v ”, initializer=tf. ones_ initializer (shape= [1]))
#在计算图 gl 中读取变盘“v”的取值。 
with tf. Session(graph=gl) as sess : 
    tf . global_variables_initializer (). run () 
    with tf. variable_scope (””, r euse=True ) : 
        #在计算图 gl 中,变量“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,所以下面这行会输出[ l. ]。 
        print(sess . run(tf.get_variable (”v ”)))

以上代码产生了两个计算图,每个计算图中定义了一个名字为“v”的变量。在计算图g1 中,将 v 初始化为 0;在计算图 g2 中,将 v 初始化为1 。可以看到当运行不同计算图时, 变量 v 的值也是不一样的。 

3.2 TensorFlow 数据模型一一张量

张量是 TensorFlow 管理数据的形式,在 3.2.1 节中将介绍张 量的一些基本属性。然后在 3.2.2 节中将介绍如何通过张量来保存和获取 TensorFlow 计算 的结果。

3.2.1 张量的概念

从 TensorFlow 的名字就可以看出张量 (tensor)是一个很重要的概念。 在 TensorFlow程序中,所有的数据都通过张量的形式来表示。从功能的角度上看,张量可以被简单理解 为多维数组。其中零阶张量表示标量(scalar ) ,也就是一个数: 第一阶张量为向量 (vector), 也就是一个一维数组;第 n 阶张量可以理解为一个 n 维数组。但张量在 TensorFlow 中的实现并不是直接采用数组的形式,它只是对 TensorFlow 中运算结果的引用。在张量中并没有 真正保存数字,它保存的是如何得到这些数字的计算过程。还是以向量加法为例,当运行 如下代码时,并不会得到加法的结果,而会得到对结果的一个引用。

import tensorflow as tf 
# tf.constant 是一个计算,这个计算的结果为一个张量,保存在变量 a 中。
a = tf. constant([1 . O, 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 )。

张量的第一个属性名字不仅是一个张量的唯一标识符, 它同样也给出了这个张量是如 何计算出来的。在 3 . 1.2 节中介绍了 TensorFlow 的计算都可以通过计算图的模型来建立, 而计算图上的每一个节点代表了一个计算,计算的结果就保存在张量之中。所以张量和计算图上节点所代表的计算结果是对应的。这样张量的命名就可以通过 “ node:src_output”的 形式来给出。其中 node 为节点的名称, src_output 表示当前张量来自节点的第几个输出。 比如上面代码打出来的“add :0”就说明了 result 这个张量是计算节点“add” 输出的第一个 结果(编号从0开始)。

张量的第二个属性是张量的维度( shape)。这个属性描述了一个张量的维度信息。比如上面样例中 shape=(2,)说明了张量 result 是一个一维数组, 这个数组的长度为 2。维度是 张量一个很重要的属性, 围绕张量的维度 TensorFlow 也给出了很多有用的运算。

张量的第三个属性是类型(type ),每一个张量会有一个唯一的类型。 TensorFlow 会对 参与运算的所有张量进行类型的检查, 当发现类型不匹配时会报错。 比如运行以下程序时 就会得到类型不匹配的错误:

import tensorf low 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 支持 14 种不同的类型, 主要包括了实数(tf.float32 、 tf.float64)、整 数(tf.int8 、 tf.int16 、 tf.int32 、 tf.int64 、 tf.uint8 )、布尔型 (tf.bool) 和复数(tf.complex64 、 tf.complex128 ) 。

3.2.2 张量的使用

和 TensorFlow 的计算模型相比, TensorFlow 的数据模型相对比较简单。 张量使用主要 可以总结为两大类。 第一类用途是对中间计算结果的引用 。 当一个计算包含很多中间结果时,使用张量可以大大提高代码的可读性。以下为使用张量和不使用张量记录中间结果来完成向量相加的 功能的代码对比。

#使用张量记录中间结果 
a = tf. constant ( [1.0, 2.0], name=” a ”)
b = tf. constant ( [2.0, 3.0], name=” b ”) 
result = a + b

#直接计算向量的和,这样可读性会比较差。
result= tf.constant([l.O, 2.0] ,name=” a ”)+
        tf.constant([2.0, 3.0] ,name=” b ”)

从上面的样例程序可以看到, a 和 b 其实就是对常量生成这个运算结果的引用,这样在做加法时就可以直接使用这两个变量,而不需要再去生成这些常量。 当计算的复杂度增 加时(比如在构建深层神经网络时〉通过张量来引用计算的中间结果可以使代码的可阅读 性大大提升。 同时,通过张量来存储中间结果可以方便获取中间结果。 

使用张量的第二类情况是当计算图构造完成之后,张量可以用来获得计算结果,也就是得到真实的数字。虽然张量本身没有存储具体的数字,但是通过 3.3 节中介绍的会话,就可以得到这些具体的数字。 比如在上述代码中,可以使用 tf. Session().run( result)语句得到 计算结果。

3.3 TensorFlow 运行模型一一会话

前面的两节介绍了 TensorFlow 是如何组织数据和运算的。本节将介绍如何使用TensorFlow 中的会话( session)来执行定义好的运算。会话拥有并管理 TensorFlow 程序运 行时的所有资源。所有计算完成之后需要关闭会话来帮助系统回收资源,否则就可能出现 资源泄漏的问题。 TensorFlow 中使用会话的模式一般有两种,第一种模式需要明确调用会 话生成函数和关闭会话函数,这种模式的代码流程如下。

#创建一个会话。 
sess = tf.Session()
#使用这个创建好的会话来得到关心的运算的结果。比如可以调用 sess.run(result) , 
#来得到 3.1 节样例中张量 result 的取值。 
sess.run (...)
#关闭会话使得本次运行中使用到的资源可以被释放。 
sess.close ()

使用这种模式时,在所有计算完成之后,需要明确调用 Session.close 函数来关闭会话 并释放资源。然而,当程序因为异常而退出时,关闭会话的函数可能就不会被执行从而导 致资源泄漏。 为了解决异常退出时资源释放的问题, TensorFlow 可以通过 Python 的上下文 管理器来使用会话。以下代码展示了如何使用这种模式。

#创建一个会话,并通过 Python 中的上下文管理器来管理这个会话。
with tf. Session() as sess : 
    #使用创建好的会话来计算关心的结果。 
    sess.run(...) 
#不需要再调用“Session.close()”函数来关闭会话, 
#当上下文退出时会话关闭和资源释放也自动完成了。

通过 Python 上下文管理器的机制,只要将所有的计算放在 “ with”的内部就可以。 当 上下文管理器退出时候会自动释放所有资源。这样既解决了因为异常退出时资源释放的问 题,同时也解决了忘记调用 Session.close 函数而产生的资源泄漏。

3.1 节介绍过 TensorFlow 会自动生成一个默认的计算图,如果没有特殊指定,运算会自动加入这个计算图中。 TensorFlow 中的会话也有类似的机制,但 TensorFlow 不会自动生成默认的会话,而是需要手动指定。 当默认的会话被指定之后可以通过 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 脚本或者 Jupyter 的编辑器下),通过设置默认会话的方式来获取张量的取值更加方便。 所以 TensorFlow 提供了一种在交互式环境下直接构建默认会话的函数。这个函数就是 tf.lnteractiveSession。使用这个函数会自动将生成的会话注册为默认会话。以下代码展示了tf.InteractiveSession 函数的用法。

sess = tf.InteractiveSession() 
prirt(result.eval())
sess.close ()

通过 tf.InteractiveSession 函数可以省去将产生的会话注册为默认会话的过程。无论使 用哪种方法都可以通过 ConfigProto Protocol BufferCD来配置需要生成的会话。 下面给出了通 过 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实现神经网络

3.4.3 神经网络参数与TensorFlow变量

下面一段代码给出了一种在 TensorFlow 中声明一个 2*3 的矩阵变量的方法:

weights= tf.Variable(tf. random_normal([2, 3], stddev=2))

这段代码调用了 TensorFlow 变量的声明函数 tf. Variable。在变量声明函数中给出了初始化这个变量的方法。 TensorFlow 中变量的初始值可以设置成随机数、 常数或者是通过其 他变量的初始值计算得到。 在上面的样例中, tf.random_ normal([2, 3], stddev=2)会产生一个 2*3 的矩阵,矩阵中的元素是均值为 0,标准差为 2 的随机数。 

在神经网络中,偏置项(bias)通常会使用常数来设置初始值。 以下代码给出了一个样例。

biases= tf.Variable(tf.zeros([3]))

以上代码将会生成一个初始值全部为 0 且长度为 3 的变量。除了使用随机数或者常数,TensorFlow 也支持通过其他变量的初始值来初始化新的变量。 以下代码给出了具体的方法。

w2 = tf.Variable(weights.initialized_value()) 
w3 = tf.Variable(weights.initialized_value()*2.0)

以上代码中, w2 的初始值被设置成了与 weights 变量相同。 w3的初始值则是 weights初始值的两倍。在 TensorFlow 中, 一个变量的值在被使用之前,这个变量的初始化过程需要 被明确地调用。 以下样例介绍了如何通过变量实现神经网络的参数并实现前向传播的过程。

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((3, 1),stddev=1, seed=1))
#暂时将输入的特征向量定义为一个常量。注意这里 x 是一个 1x2 的矩阵。
x = tf.constant([[0.7, 0.9)))
#通过 3.4.2 节描述的前向传播算法获得神经网络的输出。 
a = tf.matmul(x, w1) 
y = tf.matmul(a, w2)

sess = tf.Session()
#与 3.4.2 中的计算不同,这里不能直接通过 sess . run (y)来获取 y 的取值, 
#因为 wl 和 w2 都还没有运行初始化过程。以下两行分别初始化了 w1 和 w2 两个变量。 sess.run(w1.initializer) #初始化 w1. 
sess.run(w2.initializer) #初始化 w2 。 
#输出[( 3.95757794 ))。 
print(sess.run(y)) 
sess.close ()

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

3.4.5 完整神经网络样例程序

# -*- coding: utf-8 -*-
"""
Created on Sun Apr 21 14:18:53 2019

@author: Lenovo
"""
import tensorflow as tf
# NumPy 是一个科学计算的工具包,这里通过 NumPy工具包生成模拟数据袋 。
from numpy.random import RandomState
#定义训练数据 batch的大小 。
batch_size = 8
#定义神经网络的参数,这里还是沿用 3.4.2 小节中给出的神经网络结构 。
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 大小。在训练时需要把数据分
#成比较小的 batch ,但是在测试时,可以一次性使用全部的数据。当数据集比较小时这样比较
#方便测试,但数据集比较大时,将大量数据放入一个 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)
#定义规则来给出样本的标签。在这里所有 x1+x2<1 的样例都被认为是正样本( 比如i零件合格) ,
#而其他为负样本( 比如零件不合格)。和 TensorFlow 游乐场中的表示法不大一样的地方是,
#在这里使用 0来表示负样本 ,1来表示正样本。大部分解决分类问题的神经网络都会采用
# 0 和 1 的表示方法。
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))
    '''
    在训练之前神经网络参数的值:
    wl = [ [-0.81131822 , 1 . 48459876 , 0 . 06532937)
    [-2 . 44270396 , 0 . 0992484 , 0 . 59122431))
    w2 = [ [-0 . 81131822) , [1.48459876) , [0 . 06532937 ) )
    '''
    
    #设定训练的轮数。
    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%1000 == 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))
            '''
            输出结果 :
            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
            After 3000 training step (s), cross entropy on all data is 0.615096
            After 4000 training step (s), cross entropy on all data is 0.610309
            通过这个结果可以发现随着训练的进行,交叉熵是逐渐变小的。交叉熵越小说明
            预测的结果和真实的结果差距越小。
            '''
            print(sess.run(w1))
            print(sess.run(w2))
            '''
            在训练之后神经网络参数的值 :
            w1 = [[0 . 02476984 , 0 . 5694868 , 1 . 69219422]
            [- 2 . 19773483 , - 0 . 23668921 , 1.11438966] J
            w2 = [[“ 0 . 45544702 ], (0 . 49110931 ], (- 0 . 9811033]]
        可以发现这两个参数的取值已经发生了变化,这个变化就是训练的结果。
        它使得这个神经网络能更好地拟合提供的训练数据。
        '''

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章