《Tensorflow 实战Google深度学习框架》学习笔记(二)

该书样例代码网址:https://github.com/caicloud/tensorflow-tutorial

第三章 TensorFlow入门

TensorFlow最重要的概念就是Tensor和Flow。Tensor就是张量,可以被简单地理解为多维数组;Flow就是流,直观地表达了张量之间通过计算相互转化的过程。

3.1 TensorFlow计算模型——计算图

3.1.1 计算图的概念

TensorFlow是一个通过计算图的形式来表述计算的编程系统。TensorFlow中的每一个计算都是计算图中的一个节点,而节点之间的边描述了计算之间的依赖关系。一个运算的输入依赖于另一个运算的输出,那么这两个运算有依赖关系。

3.1.2 计算图的使用

TensorFlow程序一般分为两个阶段:第一个阶段,定义计算图中所有的计算,有如下实例代码;第二个阶段,执行计算,在3.3中做详细介绍。

#使用“tf”来代替“tensorflow”作为模块名称,使这个程序更加简洁
import tensorflow as tf
#为了建模的方便,TensorFlow将常量转化成一个永远输出固定值的运算
a = tf.constant([1.0,2.0], name="a")
b = tf.constant([2.0,3.0], name="b")
result = a + b
#通过a.graph可以查看张量所属的计算图,通过tf.get_default_graph()可以获取当前默认的计算图
print(a.graph is tf.get_default_graph())

在TensorFlow程序中,会自动维护一个默认的计算图,在上述代码中,因为没有特意指定,所以a所在计算图等于当前默认的计算图,输出为True。

TensorFlow中的计算图主要有三个功能:
第一,隔离张量和计算:TensorFlow支持通过tf.Graph()函数来生成新的计算图,不同计算图上的张量和运算都不会共享;

第二,管理张量和计算,计算图可以通过tf.Graph().device()函数来指定运行计算的设备,为TensorFlow使用GPU提供了机制,如以下代码:

g = tf.Graph()
#指定计算的设备
with g.device('/gpu:0'):
    result = a + b

第三,有效地整理TensorFlow程序中的资源,这里的资源包括张量、变量或者运行TensorFlow程序所需要的队列资源,在一个计算图中,可以通过集合(collection)来管理不同类别的资源,通过tf.add_to_collection()函数可以将资源加入一个或多个集合当中,通过tf.get_collection()函数可以获得一个集合里的所有资源,TensorFlow也自动管理了一些最常用的集合:
在这里插入图片描述

3.2 TensorFlow数据模型——张量

张量是TensorFlow管理数据的形式。

3.2.1 张量的概念

在TensorFlow程序中,所有的数据都通过张量的形式来表示。张量可以被简单地理解为多维数组,零阶张量表示标量即一个数,一阶张量表示向量即一维数组,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)

从上述代码的运行结果可以看出,一个张量中主要保存了三个属性:名字(name)、维度(shape)和类型(type):

①名字不仅是一个张量的唯一标识符,还描述了这个张量是如何计算出来的。张量的命名可以通过“node:src_output“的形式给出来,其中node为节点的名称,src_output表示当前张量来自节点的第几个输出(编号从0开始)。在计算图上每一个节点代表一个计算,计算的结果就保存在张量中;

②维度描述了张量的维度;

③类型描述了张量的类型,每一个张量会有一个唯一的类型,TensorFlow会对参与计算的所有张量进行类型的检查,当发现类型不匹配时会报错,如以下代码:

import tensorflow as tf
a = tf.constant([1,2], name="a")
b = tf.constant([2.0,3.0], name="b")
result = a + b

运行程序会报类型不匹配的错误:

ValueError: Tensor conversion requested dtype int32 for Tensor with dtype float32: 'Tensor("b:0", shape=(2,), dtype=float32)'

如果将第一个张量指定为实数类型:

a = tf.constant([1,2], name="a",dtype=tf.float32)

那么计算就不会报错了。

如果不指定类型,TensorFlow会给出默认的类型。使用默认类型有可能导致潜在的类型不匹配问题,因此一般通过指定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 张量的使用

张量主要有两种用途:

第一种用途是对中间计算结果的引用,当一个计算包含很多中间结果时,使用张量可以大大提高代码的可读性,比如在构建深层神经网络时就不可避免地要用张量来引用中间结果,同时通过张量来存储中间结果可以方便获取中间结果,比如在卷积神经网络中,卷积层或者池化层有可能改变张量的维度,通过张量.get_shape()函数来获取张量的维度信息可以免去人工计算的麻烦。

第二种用途是当计算图构造完成之后,张量可以用来获得计算结果,也就是真实的数字,虽然张量本身不存储真实的数字,但通过tf.Session().run()函数可以得到计算结果。

3.3 TensorFlow运行模型——会话

会话(Session)拥有并管理TensorFlow程序运行时的所有资源。当所有计算完成之后需要关闭会话来帮助系统回收资源,否则就可能出现资源泄露问题。

TensorFlow中使用会话的模式有两种:

第一种模式需要明确调用会话生成函数和关闭会话函数,这个模式的代码流程如下:

#创建一个会话
sess = tf.Session()
#使用这个创建好的会话来得到关心的运算的结果
sess.run(……)
#关闭会话使得本次运行中使用到的资源可以被释放
sess.close()

使用这种模式时,在所有计算完成之后,需要明确调用close()函数来关闭会话并释放资源,但是当程序因为异常而退出时,关闭会话的函数可能就不会被执行从而导致资源泄露。

为了解决资源泄露这个问题,一般使用第二种模式,通过Python的上下文管理器来使用会话,代码流程如下:

#创建一个会话,并通过Python的上下文管理器来管理这个会话
with tf.Session() as sess:
	#使用这个创建好的会话来得到关心的运算的结果
	sess.run(……)

通过Python的上下文管理器机制,只要将所有的计算放在“with”内部,当上下文管理器退出时就会自动释放所有资源。

TensorFlow会自动生成一个默认的计算图,如果没有特殊指定,计算会自动加到这个计算图当中,但TensorFlow不会自动生成默认的会话,而是需要手动指定,当默认的会话被指定后可以通过张量.eval()函数来计算一个张量的取值,代码示例如下:

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()
with sess.as_default():
    print(result.eval())
print(sess.run(result))
print(result.eval(session=sess))
sess.close()

三个输出结果相同:

[3. 5.]
[3. 5.]
[3. 5.]

在交互环境下(如Python脚本或Jupyter编辑器),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.InteractiveSession()
print(result.eval())
sess.close()

TensorFlow还支持在生成会话时通过tf.ConfigProto()函数配置相应参数,代码如下:

config = ConfigProto(allow_soft_placement=True, log_device_placement=True)
sess1 = tf.InteractiveSession(config=config)
sess2 = tf.Session(config=config)

以上代码中有两个参数:

①allow_soft_placement参数为True:某些计算无法被当前GPU支持时,可以自动调整到CPU上,而不是报错,提高代码的可移植性(默认为False);

②log_device_placement参数为True:日志中会记录每个节点被安排在了哪个设备上以方便调试(参数被设置为False可以减少日志量)。

3.4 TensorFlow实现神经网络

3.4.1 TensorFlow游乐场及神经网络的简介

TensorFlow游乐场是一个通过网页浏览器就可以训练的简单神经网络并实现了可视化训练过程的工具,网址:http://playground.tensorflow.org,网页截图如下:
在这里插入图片描述
网页上有几个部分:

①DATA所示的位置是4个不同的数据集用来测试神经网络。

②FEATURES对应的是实体的特征向量,特征向量是神经网络的输入,一般神经网络的第一层是输入层,代表特征向量中每一个特征的取值,OUTPUT代表的是输出层,在OUTPUT下面的平面是输出的可视化,平面上或深或浅的颜色表示了神经网络模型做出的判断,颜色越深表示神经网络模型对它的判断越有信心,输入层和输出层之间的是HIDDEN LAYERS对应的隐藏层。同一层的节点不会相互连接,而且每一层只和下一层连接,直到最后一层输出层得到输出结果。

③每一个小格子代表神经网络中的一个节点,每一个节点上的颜色代表了这个节点的区分平面,平面上的一个点代表了一种取值,而点的颜色就对应其输出值,输出值的绝对值越大,颜色越深,黄色越深表示负得越大,蓝色越深表示正得越大。

④每一条边代表了神经网络中的一个参数,它可以是任意实数,颜色越深,表示参数值的绝对值越大,黄色越深表示负得越大,蓝色越深表示正得越大,当边的颜色接近白色时,参数取值接近于0。

TensorFlow游乐场简洁明了地展示了使用神经网络解决分类问题的四个步骤:

第一,提取问题中实体的特征向量;

第二,定义神经网络的结构,定义如何从神经网络的输入到输出,设置隐藏层数和节点个数(前向传播);

第三,通过训练数据来调整神经网络中参数的取值,就是训练神经网络的过程(反向传播);

第四,使用训练好的神经网络来预测未知的数据。

3.4.2 前向传播算法简介

以全连接神经网络为例,全连接神经网络是指相邻两层之间任意两个节点之间都有连接,如下所示:
在这里插入图片描述
计算神经网络的前向传播的结果需要三部分的信息:

第一部分是神经网络的输入,这个输入就是从实体中提取的特征向量;

第二部分是神经网络的连接结构,即不同神经元之间输入输出的连接关系;

第三部分是神经网络中神经元之间的边的权重。

设X为某一层结点的值所组成的矩阵,W为该层和下一层之间的边的权重所组成的矩阵,Y为下一层结点的值的矩阵,则Y可通过TensorFlow程序得到:

#实际上是实现了矩阵的乘法
Y = tf.matmul(X,W)

3.4.3 神经网络参数与TensorFlow变量

在TensorFlow中,变量(tf.Variable)的作用就是保存和更新神经网络中的参数。

变量的初始值设置方法有三种:

①使用随机数设置初始值:

TensorFlow支持以下四个随机数生成函数:
在这里插入图片描述
其中tf.random_normal比较常用,如:

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

在上面的样例中,tf.random_normal([2,3],stddev=2)用于产生一个2 x 3的矩阵,矩阵中的元素服从一个平均值mean为0(没有指定时默认为0),标准差stddev为2的正态分布。

②使用常数设置初始值:

在神经网络中,偏置项(bias)通常会使用常数来设置初始值:
在这里插入图片描述
③使用其他变量的初始值设置初始值:

#w1的初始值与weights相同,w2的初始值是weights的两倍
w1 = tf.Variable(weights.initialized_value())
w2 = tf.Variable(weights.initialized_value() * 2.0)

在TensorFlow中,一个变量的值在使用之前,需要明确地调用变量的初始化过程,通常有两种方法:

#第一种方法,直接调用每个变量的初始化过程,比较麻烦
sess.run(weights.initializer)
sess.run(w1.initializer)
sess.run(w2.initializer)
#第二种方法,通过tf.initialize_all_variables函数实现初始化所有变量的过程
init_op = tf.initialize_all_variables()
sess.run(init_op)

实际上,所有的变量都会被自动加入GraphKeys.VARIABLES这个集合。通过tf.all_variables函数可以拿到当前计算图上所有的变量。拿到计算图上所有的变量有助于持久化整个计算图的运行状态。

当构建机器学习模型时,比如神经网络,可以通过变量声明函数中的trainable参数来区分需要优化的参数(比如神经网络中的参数)和其他参数(比如迭代的轮数)。如果声明变量时参数trainable为True(默认为True,若不想变量被GraphKeys.TRAINABLE_VARIABLES集合收集,就设置为False),那么这个变量将会被加入GraphKeys.TRAINABLE_VARIABLES集合。在TensorFlow中可以通过tf.trainable_variables函数得到所有需要优化的参数。TensorFlow中提供的神经网络优化算法会将GraphKeys.TRAINABLE_VARIABLES集合中的变量作为默认的优化对象。

维度(shape)和类型(type)是变量最重要的两个属性。其中,变量的类型是不可改变的,一个变量在构建之后,它的类型就不能再改变了;和类型不大一样的是,维度在程序运行中是有可能改变的,但是需要通过设置参数validate_shape=False(默认为True),如:

w1 = tf.Variable(tf.random_normal([2,3], stddev=2), name="w1")
w2 = tf.Variable(tf.random_normal([2,2], stddev=2), name="w2")
#这一句会报维度不匹配的错误:ValueError: Shapes (2, 3) and (2, 2) are not compatible
tf.assign(w1, w2)
#这一句可以被成功执行
tf.assign(w1, w2, validate_shape=False)

以3.4.2中的前向传播算法示意图为示例编写一个简单的TensorFlow程序:

import tensorflow as tf

with tf.Session() as sess:
    #声明wl、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是一个1*2的矩阵
    x = tf.constant([[0.7, 0.9]])
    #如果像下面这样写,会发生报错
    #x = tf.constant([0.7,0.9])

    a = tf.matmul(x, w1)
    y = tf.matmul(a, w2)

    #初始化变量
    init_op = tf.global_variables_initializer()
    sess.run(init_op)

    #输出结果:[[3.957578]]
    print(sess.run(y))

3.4.4 通过TensorFlow训练神经网络模型

在神经网络优化算法中,最常用的就是反向传播算法。反向传播算法实现了一个迭代的过程:

①在每次迭代的开始,都选取一小部分的数据,这一小部分数据叫做一个batch。

②这个batch的样例会通过前向传播算法得到神经网络模型的预测结果。因为训练数据都是有真实标签的,所以可以计算当前神经网络模型的预测结果和真实结果的差距。

③基于预测结果和真实结果的差距,反向传播算法会相应更新神经网络模型参数的取值,使模型的预测结果在这个batch上更接近真实结果,流程如下图:
在这里插入图片描述
通过TensorFlow实现反向传播算法:

第一步,使用TensorFlow表达一个batch的数据。如果每轮迭代中选取的数据都要通过常量来表示,那么TensorFlow的计算图将会太大。因为每生成一个常量,TensorFlow都会在计算图中增加一个节点。一般来说,一个神经网络的训练过程会需要经过几百万轮甚至几亿轮的迭代,这样计算图就会非常大,而且利用率很低。为了避免这个问题,TensorFlow提供了 placeholder机制用于提供输入数据。placeholder相当于定义了一个位置,这个位置中的数据在程序运行时再指定。这样在程序中就不需要生成大量常量来提供输入数据,而只需要将数据通过placeholder传入TensorFlow计算图。在placeholder定义时,这个位置上的数据类型是需要指定的。和其他张量一样,placeholder的类型也是不可以改变的。placeholder中数据的维度信息可以根据提供的数据推导得出,所以不一定要给出。下面给出了通过placeholder实现前向传播算法的代码:

import tensorflow as tf

with tf.Session() as sess:
    #声明wl、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))
    
    #定义placeholder作为存放输入数据的地方,这里维度也不一定要定义,但如果维度是确定的,那么给出维度可以降低出错的概率
    x = tf.placeholder(tf.float32, shape=[3,2], name="input")

    a = tf.matmul(x, w1)
    y = tf.matmul(a, w2)

    #初始化变量
    init_op = tf.global_variables_initializer()
    sess.run(init_op)

    '''输出结果:
    [[3.957578 ]
     [1.1537654]
     [3.1674924]]'''
    print(sess.run(y, feed_dict={x:[[0.7,0.9], [0.1,0.4], [0.5,0.8]]}))

在这段程序中替换了原来通过常量定义的输入x。在新的程序中计算前向传播结果时,需要提供一个feed_dict来指定x的取值,feed_dict是一个字典(map),在字典中需要给出每个用到的placeholder的取值。如果某个需要的placeholder没有被指定取值,那么程序在运行时将会报错。

第二步,在得到一个batch的前向传播结果之后,需要定义一个损失函数来刻画当前的预测值
和真实值之间的差距:

#定义真实值与预测值之间的交叉熵损失函数,来刻画真实值与预测值之间的差距
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)

TensorFlow支持7种不同的优化器,可以根据具体的应用选择不同的优化算法。比较常用的优化方法有三种:tf.train.GradientDescentOptimizer、tf.train.AdamOptimizer 和tf.train.MomentumOptimizer。在定义了反向传播算法之后,通过运行sess.run(train_step)就可以对所有在GraphKeys.TRAINABLE_VARIABLES集合中的变量进行优化,使得在当前batch下损失函数更小。

3.4.5 完整神经网络样例程序

用神经网络模型解决二分类问题:

import tensorflow as tf

#NumPy是一个科学计算的工具包,这里通过NumPy工具包生成模拟数据集
from numpy.random import RandomState

#声明wl、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))

'''定义placeholder作为存放输入数据的地方,在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)

#定义真实值与预测值之间的交叉熵损失函数,来刻画真实值与预测值之间的差距
cross_entropy = -tf.reduce_mean(y_ * tf.log(tf.clip_by_value(y, 1e-10, 1.0)))

#定义反向传播算法的优化方法
train_step = tf.train.AdamOptimizer(learning_rate=0.001).minimize(cross_entropy)

#设置随机数种子
rdm = RandomState(seed=1)
#设置随机数据集大小
dataset_size = 128
'''设置随机数据集,定义规则来给出样本的标签
在这里所有x1+x2<1的样例都被认为是正样本,而其他为负样本
在这里使用0来表示负样本,1来表示正样本。'''
X = rdm.rand(dataset_size, 2)
Y = [[int(x1 + x2 < 1)] for x1,x2 in X]

#创建会话
with tf.Session() as sess:
    #初始化变量
    init_op = tf.global_variables_initializer()
    sess.run(init_op)
    
    #设置batch训练数据的大小
    batch_size = 8
    #设置训练得轮数
    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]})
        
        '''输出结果:
        After 0 training step(s), cross entropy on all data is 0.0674925
        After 1000 training step(s), cross entropy on all data is 0.0163385
        After 2000 training step(s), cross entropy on all data is 0.00907547
        After 3000 training step(s), cross entropy on all data is 0.00714436
        After 4000 training step(s), cross entropy on all data is 0.00578471
        通过这个结果可以发现随着训练的进行,交叉熵是逐渐变小的,交叉嫡越小说明预测的结果和真实的结果差距越小'''
        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))

代码中有关numpy.random的使用可以参照:https://blog.csdn.net/jieshaoxiansen/article/details/82255191

上面的程序实现了训练神经网络的全部过程,从这段程序可以总结出训练神经网络的过程可以分为以下3个步骤:

第一步,定义神经网络的结构和前向传播的输出结果;

第二步,定义损失函数以及选择反向传播优化的算法;

第三步,生成会话(tf.Session)并且在训练数据上反复运行反向传播优化算法。

发布了11 篇原创文章 · 获赞 0 · 访问量 274

猜你喜欢

转载自blog.csdn.net/qq_44009891/article/details/103961769