《TensorFlow:实战Google深度学习框架(第二版)》笔记【1-6章】

读书笔记 同时被 2 个专栏收录
9 篇文章 1 订阅

本书PDF 密码: uj6t
代码:https://github.com/caicloud/tensorflow-tutorial

第一章:深度学习简介

在大部分情况下,在训练数据达到一定数量之前,越多的训练数据可以使逻辑回归算法对未知邮件做出的判断越精准。之所以说在大部分情况下,是因为逻辑回归算法的效果除了依赖于训练数据,也依赖于从数据中提取的特征。假设从邮件中抽取的特征只有邮件发送的时间,那么即使有再多的训练数据,逻辑回归算法也无法很好地利用。这是因为邮件发送的时间和邮件是否为垃圾邮件之间的关联不大,而逻辑回归算法无法从数据中习得更好的特征表达。这也是很多传统机器学习算法的一个共同问题。

如何从实体中提取特征,对于很多传统机器学习算法的性能有巨大影响。下图展示了一个简单的例子。通过笛卡尔坐标系和极角坐标系表示数据。同样的数据使用不同的表达方式会极大地影响解决问题的难度。一旦解决了数据表达和特征提取,很多人工智能任务也就解决了90%。

既然人工的方式无法很好地抽取实体中的特征,那么是否有自动的方式呢?深度学习解决的核心问题之一就是自动的将简单地特征组合成更加复杂的特征,并使用这些组合特征解决问题。下图展示了深度学习和传统机器学习在流程上的差异。深度学习算法可以从数据中学习更加复杂的特征表达,使得最后一步权重学习变得更加简单且有效。

神经科学家们发现,如果将小白鼠的视觉神经连接到听觉中枢,一段时间之后小白鼠可以习得使用听觉中枢“看”世界。这说明虽然哺乳动物大脑分为了很多区域,但这些区域的学习机制却是相似的。

深度学习的发展历程

第一个阶段:模仿大脑的学习机理

感知机模型可以根据样例数据学习特征权重。
不足:感知机无法解决线性不可分问题,且计算能力不足以训练多层神经网络

第二个阶段:分布式知识表达和反向传播算法
分布式知识表达大大加强了模型的表达能力,让神经网络从宽度的方向走向了深度的方向。
反向传播算法大幅降低了训练神经网络所需要的时间。
不足:计算资源不足以训练深层神经网络;数据量无法满足训练需要。

第三个阶段:云计算、GPU和海量数据
解决了计算力和数据的问题,深度学习迎来高潮。

深度学习的应用

  1. 计算机视觉:分类,识别,无人驾驶,图像搜索等
  2. 语音识别:siri,同声传译等
  3. 自然语言处理:语言模型、机器翻译、词性标注、实体识别、情感分析等
    核心技术:单词向量word embedding
  4. 人机博弈:AlphaGo系列

深度学习工具

第二章:TensorFlow环境搭建

  1. 依赖的工具包
  2. 安装方式
  3. 样例程序(略)

TensorFlow的主要依赖包

Protocol Buffer和Bazel

Protocol Buffer

Protocol Buffer的作用是将结构化的数据序列化,并从序列化之后的数据流中还原出原来的结构化数据。

与XML、Json等其他工具相比:

  • 序列化为二进制流,而不是字符串
  • XML、Json格式信息包含在数据流中,Protocol Buffer需要先定义数据的格式
  • Protocol Buffer数据流比XML小3-10倍,解析时间快20-100倍

结构化数据:

Protocol Buffer数据格式示例:

Protocol Buffer数据格式一般保存在.proto文件中。

TensorFlow中的数据基本都是通过Protocol Buffer来组织的。

Bazel

Bazel是从Google开源的自动化构建工具,相比传统的makefile,Ant或者Maven,Bazel在速度、可伸缩性、灵活性以及对不同程序语言和平台的支持上都要更加出色。Tensorflow本身以及Google给出的很多官方样例都是通过Bazel来编译的。

  • WORKSPACE文件:定义了对外部资源的依赖关系
  • BUILD文件:找到需要编译的目标

在编译出来的结果中,bazel-bin目录下存放了编译产生的二进制文件以及运行该二进制文件所需要的所有依赖关系。

Tensorflow安装方式

  • Docker:可移植性最强,但对GPU支持有限,且对本地开发环境的支持不够友好
  • Pip:最方便,但无法修改Tensorflow本身
  • 源码:最灵活,但比较繁琐,一般只有修改tensorflow护着需要支持特殊GPU才会被用到

第三章 Tensorflow入门

本章介绍Tensorflow的计算模型、数据模型和运行模型,以了解Tensorflow的工作原理。并用Tensorflow实现一个神经网络的计算流程。

Tensorflow计算模型:计算图

计算图是Tensorflow中最基本的一个概念,Tensorflow中的所有计算都被被转化为计算图上的节点。

Tensorflow是一个通过计算图的形式来描述计算的编程系统。Tensor即张量,Flow指计算图。Tensorflow中的每一个计算都是计算图上的一个节点,而节点之间的边描述了计算之间的依赖关系。

为了建模方便,tf将常量转化成一种永远输出固定值的运算。

tf自动维护一个默认的计算图,如果没有特意指定,tf会将定义的计算自动转化为默认计算图上的节点。

除了默认的计算图,也可以用tf.Graph生成新的计算图,不同计算图的张量和运算不会共享。

计算图可以通过tf.Graph.device函数指定运行计算的设备。

有效的整理tf程序中的资源也是计算图的一个重要功能。可以通过集合来管理不同类别的资源。体乳通过tf.add_to_collection将资源加入一个或多个集合中,然后通过tf.get_collection获取一个集合里面的所有资源。这里的资源可以是张量、变量或者运行tf程序所需要的队列资源等。tf自动管理了一些最常用的集合:

Tensorflow数据模型:张量

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

tf所有的数据都通过张量的形式来表示,张量可以简单理解为多维数组。但张量在tf的实现并不是直接采用数组的形式,它只是对tf中运算结果的引用。在张量中并没有真正保存数字,它保存的是如何得到这些数字的计算过程。

执行上面的代码,并不会得到加法的结果,而是对结果的一个引用。
tf的计算都可以通过计算图的模型来建立,而计算图上的每一个节点代表了一个计算,计算的结果保存在张量之中。张量对应了计算图上节点代表的计算结果。

一个张量主要保存三个属性:名字(name),维度(shape)和类型(type)。

  • 张量的命名形式可以是“node:src_output"。其中node为节点的名称,src_output表示来自节点的第几个输出。
  • 维度是张量一个很重要的属性,围绕张量的维度tf给出了很多有用的运算。
  • 每个张量都有一个唯一的类型,tf会对参与运算的所有张量进行类型检查,当发现类型不匹配会报错。如

报错:

如果将a指定为实数类型”a=tf.constant([1,2],name=“a”,dtype=tf.float32)",就不会报错了。不指定类型,tf会给出默认的类型,容易导致类型不匹配问题。tf支持14中不同的类型,主要包括实数(tf.float32,tf.float64)、整数(tf.int8,tf.int16,tf.int64,tf.uinit8)、布尔型(tf.bool)和复数(tf.complex64/tf.conplex128)。

Tensorflow运行模型:会话

计算图和张量分别组织运算和数据,而会话(session)用来执行定义好的运算。会话拥有并管理tf程序运行时的所有资源,当所有计算完成之后需要关闭会话帮助系统回收资源,否则可能出现资源泄露的问题。tf中使用会话的模式一般有两种。

使用这种模式需要明确调用Session.close关闭会话并释放资源。当程序因为异常而退出时,关闭函数可能不会执行导致资源泄露。

通过Python上下文管理器机制,退出时自动释放所有资源。

会话和计算图有类似的机制,但tf不会自动生成默认的会话,而是需要手动指定。默认的会话被指定之后可以通过tf.Tensor.eval函数来计算一个张量的取值。

或者

在交互式环境(比如Python脚本或者Jupyter的编辑器)下,通过设置默认会话的方式来获取张量的取值更加方便。所以tf提供了一种直接构建默认会话的函数tf.InteractiveSession。

无论使用哪种方法都可以通过ConfigProto Protocol Buffer来配置需要生成的会话。

通过ConfigProto可以配置类似并行的线程数、GPU分配策略,运算超时时间等参数。在这些参数中,最常使用的有两个。

  • allow_soft_placement:默认值为False。设为True时,可以在以下情况发生时,把GPU的运算放到CPU上。
  1. 运算无法在GPU上执行
  2. 没有GPU资源(比如指定在第二个GPU,但只有一个GPU)
  3. 运算输入包含对CPU结果的引用
  • log_device_placement:True时日志中会记录每个节点被安排在了哪个设备上以方便调试。在生产环境设为False可以减少日志量。

Tensorflow实现神经网络

Tensorflow游乐场是一个通过网页浏览器就可以训练的简单神经网络并实现了可视化训练的工具。

使用这个工具可以看到神经网络解决分类问题主要分为以下步骤:

  1. 提取问题中实体的特征向量作为神经网络的输入。
  2. 定义神经网络的结构,并定义如何从输入得到输出
  3. 通过训练数据来调整神经网络中参数的取值
  4. 使用训练好的神经网络预测未知的数据

前向传播算法简介

本小节用最简单的全连接网络(区别于卷积网络、LSTM等结构)的前向传播算法介绍。首先需要了解神经元(也称为节点)的结构。

一个节点有多个输入和一个输出,最简单的节点的输出就是对所有输入的加权和,不同输入的权重就是节点的参数。而神经网络的优化过程就是优化节点中参数的值的过程(反向传播算法)。下面给出了一个简单的判断零件是否合格的三层全连接神经网络(全连接神经网络是指相邻两层之间所有节点之间都有连接)。

计算神经网络的前向传播结果需要三部分信息。

  1. 神经网络的输入:从实体中提取的特征向量。上图中的零件长度x1和零件质量x2。
  2. 神经网络的连接结构:神经网络由节点组成,神经网络的结构就是不同节点之间输入输出的连接关系。
  3. 神经元的参数:用W表示神经元的参数。W的上标表明神经网络的层数,下标表明连接节点的编号。如W1,2(1)表示连接x1和a12节点的权重。

有了输入、结构和参数,就可以通过前向传播算法计算神经网络的输出,如图。

其中a11的计算过程

输出y的计算过程

前向传播算法可以表示为矩阵乘法,将输入x1,x2表示为1x2的矩阵x=[x1,x2],W(1)表示为2x3的矩阵

这样通过矩阵乘法就可以得到隐藏层三个节点的向量取值:

类似的输出层:

tf中矩阵乘法的实现非常简单。

之后的章节中会继续介绍偏置(bias)、激活函数(activation function)等更复杂的神经元结构,还有卷积神经网络、LSTM等更复杂的神经网络结构。

神经网络参数与Tensorflow变量

本小节介绍tf是如何组织、保存和使用神经网络中的参数的。在tf中,变量的作用是保存和更新神经网络中的参数。tf中的变量需要指定初始值,其中随机初始值最常见。

这段代码调用变量声明函数tf.Variable,在函数中给出了初始化方法。初始值可以是随机数、常数或者通过其他变量的初始值计算得到。tf.random_normal([2,3],stddev=2)会产生一个2x3的矩阵,矩阵的元素是均值为0,标准差为2的随机数。下表列出了tf目前支持的所有随机数生成器。

常量声明方法(比如偏置项:biases=tf.Variable(tf.zeros([3])))

通过其他变量的初始值初始化

以下样例介绍了如何通过变量实现神经网络的参数并实现前向传播的过程。

也可以使用tf.initialize_all_variables函数实现初始化所有变量的过程。

tf中,变量声明函数tf.Variable是一个运算,输出结果是一个张量,所以变量只是一种特殊的张量。下面通过上个案例中计算图中关于w1的操作的可视化结果说明tf.Variable操作在tf中底层是如何实现的。

可以看到w1是一个Variable运算。w1通过一个read操作将值提供给了一个乘法运算(tf.matmul(x,w1)),Assign这个节点的输入为随机数生成函数的输出,输出赋给了变量w1,这样就完成了变量初始化。

所有的变量都会被自动的加入GraphKeys.VARIABLES这个集合,通过tf.all_variables函数可以拿到当前计算图上所有的变量。拿到所有变量有助于持久化计算图的运行状态。可以通过变量声明函数中的trainable参数来区分需要优化的参数(比如神经元参数)和其他参数(比如迭代次数),如果trainable为True,那么这个变量会被加入到GraphKeys.TRAINABLE_VARIABLES集合。可以通过tf.trainable_variables函数得到所有需要优化的参数。tf提供的神经网络优化算法会将GraphKeys.TRAINABLE_VARIABLES集合中的变量作为默认的优化对象。

类似张量,维度和类型也是变量最重要的两个属性。变量类型是不可以改变的,但维度是可能改变的,需要设置参数validate_shape=False,但是更改维度的做法在tf中比较罕见。

通过Tensorflow训练神经网络模型

在神经网络优化算法中,最常用的方法是反向传播算法,本小节主要介绍训练神经网络的整体流程以及Tensorflow对于这个流程的支持。

使用tf实现反向传播算法的第一步是表达一个batch的数据。之前的例子中曾使用常量表达过一个样本,但如果每次迭代都使用一个常量,那么计算图中的常量节点会非常多且利用率很低。为了避免这个问题,tf提供了placeholder机制用于提供输入数据。placeholder相当于定义了一个位置,这个位置中的数据在程序运行时再指定。定义placeholder时,需要指定类型,但是不一定需要维度,因为可以根据提供的数据推导出来。

batch数据例子:

得到前向传播结果后,通过定义好的损失函数计算损失并通过反向传播算法更新神经网络参数。损失函数和反向传播算法将在第四章详细介绍。

完整神经网络样例程序

ipynb github代码

第四章 深层神经网络

本章进一步介绍如何设计和优化神经网络,内容包括深度学习的概念、损失函数、反向传播算法、tf实现反向传播的过程以及神经网络优化中常见的问题。

深度学习与深层神经网络

维基百科对深度学习的定义为“一类通过多层非线性变换对高复杂性数据建模算法的合集”。基本上深度学习就是深层神经网络的代名词。

从定义中可以看出深度学习有两个非常重要的特征:多层和非线性。

线性模型的局限性

线性模型中,模型的输出为输入的加权和。

前面介绍的前向传播算法实现的就是一个线性模型。虽然那个神经网络有两层,但是和单层神经网络的表达能力没有区别,因为它们只有线性变换。然而线性模型解决的问题是有限的,也是为什么深度学习要强调非线性。

我们使用Tensorflow游乐场试验一下,使用Activation为Linear的线性模型得到的效果是:

可以看到模型不能很好的区分不同颜色的点,只能通过直线来划分平面。

使用Activation为Relu的非线性模型的效果是:

可见,对于复杂问题(无法通过直线或者高维平面划分的),非线性模型可以很好的解决。在现实世界中,绝大部分问题都是非线性的。

对于线性可分问题,线性模型就可以很好的区分:

激活函数实现非线性化

上一节提到了Activation(激活函数),如果将前面提到的加权和节点后通过一个非线性函数,那么神经网络模型就是非线性的了。这个非线性函数就是激活函数。

新的结构中加入了偏置项(输出永远为1的节点)和非线性变换。

激活函数的图像都不是一条直线:

新的神经网络结构图:

输出层计算公式

目前tf提供7中不同的非线性激活函数,比如tf.nn.relu,tf.sigmoid和tf.tanh。tf也支持使用自定义的激活函数。以下代码展示了tf实现上图中的前向传播算法:

多层网络解决异或运算

神经网络发展史上,一个重要的问题就是异或问题。感知机模型的提出从数学上完成了对神经网络的建模,感知机可以简单的理解为单层的神经网络,其网络结构就是图4-5。然而感知机被证明无法解决异或运算(如果两个输入的符号相同输出0,否则输出1):

加入隐藏层后,异或问题可以很好地解决。

而且,隐藏层的四个节点中,每个节点都有一个角是黑的。这个节点代表了从输入特征中抽取的更高维的特征,比如第一个节点大致代表两个输入的逻辑与操作的结果。深度神经网络的组合特征提取的功能,对于解决不易提取特征向量的问题(图片识别、语音识别等)有很大帮助。

损失函数定义

神经网络模型的效果以及优化的目标是通过损失函数来定义的,本节讲解适用于分类和回归问题的经典损失函数、自定义损失函数。

经典损失函数

分类问题:交叉熵函数
通过神经网络解决多分类问题最常用的方法是设置n个输出节点,n是分类的个数。理想情况下,如果一个样本属于类别k,那么这个类别对应的输出节点为1,其他节点为0,比如[0,1,0,0,0,0]。怎么判断一个输出向量和期望的向量有多接近呢?交叉熵(cross entropy)是常用的评判方法之一。交叉熵刻画了两个概率分布之间的距离,它是分类问题中使用比较多的损失函数。

给定两个概率分布p和q,通过q来表示p的交叉熵为

交叉熵刻画的是两个概率分布的距离,然而神经网络的输出不一定是一个概率分布(任意事件发生的概率都在0和1之间,且概率总和为1)。Softmax回归是一个常用的将前向传播结果变成概率分布的方法。

交叉熵函数不是对称的,H(p,q)!=H(q,p)

Softmax回归本身可以作为一个学习算法来优化分类结果,但在tf中,Softmax回归的参数被去掉了,只作为一个额外的处理层,将输出变成一个概率分布。

交叉熵值越小,两个概率分布越接近。之前已经用tf实现过交叉熵的计算:

因为交叉熵一般与Softmax回归一起使用,tf提供了tf.nn.softmax_cross_entropy_with_logits函数:

其中y代表输出值,y_代表标准答案。
如果分类只有一个正确答案,还可以使用tf.nn.sparse_softmax_cross_entropy_with_logits函数加速计算过程。

回归问题:均方误差函数
回归问题需要预测的不是一个实现定义好的类别,而是一个任意实数,比如房价、销量等。解决回归问题的神经网络一般只有一个输出节点,最常用的损失函数是均方误差(MSE)

tf实现:

均方误差也是分类问题常用的损失函数

自定义损失函数

为了让神经网络优化的结果更加接近实际问题,我们需要自定义损失函数。

比如商品销量问题,如果预测值较大(大于真实销量),商家损失生产商品的成本。如果预测值较小,损失商品的利润。因为一般成本和利润不相同,使用均方误差不能够很好地最大化利润。比如成本是1元,利润是10元,那么模型应该偏向预测值较大。下面的公式给出了预测值多于或少于真实值时有不同系数的损失函数,通过这个损失函数,模型可能最大化收益。

tf实现:

示例代码github

神经网络优化算法

本节介绍如何通过反向传播算法和梯度下降算法调整神经网络中参数的取值。梯度下降算法主要用于优化单个参数的取值,而反向传播算法给出了一个高效的方式在所有参数上使用梯度下降算法。反向传播算法是训练神经网络的核心算法,可以根据定义好的损失函数优化参数的取值,从而使模型在训练数据集上的损失函数达到一个较小值。神经网络模型参数的优化过程直接决定了模型的质量。

本笔记不对梯度下降和反向传播算法做记录,详情可参考原书或其他资源。

几个概念:

  • 学习率
  • 局部最优
  • 随机梯度下降

在tf中实现神经网络的训练过程大致如下:

神经网络进一步优化

介绍神经网络优化过程中可能遇到的一些问题,以及解决的常用方法。比如指数衰减学习率、过拟合和滑动平均模型。

学习率的设置

tf提供了一种灵活的学习率设置方法——指数衰减法,tf.train.exponential_decay。先使用较大的学习率快速得到一个比较优的解,随着迭代的继续逐步减少学习率,使得模型在训练后期更加稳定。它实现了以下代码的功能:

decayed_learning_rate为每一轮优化时使用的学习率,learning_rate为设定的初始学习率,decay_rate为衰减系数,decay_steps为衰减速度。tf.train.exponential_decay可以通过设置参数staircase选择不同的衰减方式,默认为False,此时学习率衰减趋势如下图灰色曲线。当设置为True时,global_step/decay_steps会被转化成整数,使得学习率成为一个阶梯函数,如黑色曲线。在这样的设置下,decay_steps通常代表完整使用一遍训练数据所需要的迭代轮数,也就是总训练数据/一个batch的样本数。这样可以使得总训练数据中的每一个batch都使用相同的学习率,对模型产生相等的作用。当完整过完一遍训练数据时,学习率就减少一次。

tf代码实现:

初始学习率、衰减系数和衰减速度都是根据经验设置

示例代码github

过拟合问题

正则化的思想是在损失函数中加入刻画模型复杂程度的指标,一般来说模型复杂度只由权重w决定,和偏置b无关。常用的刻画模型复杂度的函数R(w)有两种,L1正则

和L2正则

L1正则化会让参数变得更稀疏,而L2正则不会。其次,L1正则的计算公式不可导,L2正则可导。因为优化时需要计算损失函数的偏导数,所以对L2正则损失函数的优化要更加简洁。优化L1正则更加复杂,而且优化方法也有很多种。也可以将L1和L2一起使用

tf实现带L2正则的损失函数定义

类似的,L1为

当网络结构复杂 之后,定义网络结构的部分和计算损失函数的部分可能不在同一个函数中,这样通过变量这种方式计算损失函数就不方便了。为了解决这个问题,我们可以使用tf的集合。

滑动平均模型

滑动平均模型是可以让模型在测试数据上更鲁棒的方法。tf提供了tf.train.ExponentialMovingAverage来实现滑动平均模型。初始化ExponentialMovingAverage时,需要提供一个衰减率(decay)。ExponentialMovingAverage对每一个变量都会维护一个影子变量(shadow variable),影子变量的初始值就是相应变量的值,而每次运行变量更新时,影子变量的值都会更新为:

decay决定了模型更新的速度,decay越大模型越趋于稳定。实际应用中,decay一般设成非常接近1的数(比如0.999或0.9999)。为了模型在训练前期可以更新的更快,ExponentialMovingAverage还提供了num_updates来动态设置decay的大小:

滑动平均实例代码

第5章会给出在真实应用中使用滑动平均的样例。

第五章 MNIST数字识别问题

第四章介绍了训练神经网络模型需要考虑的主要问题和常用解决方法,这一章将通过一个实际问题验证。并介绍Tensorflow变量重用和命名空间、模型持久化问题。

MNIST数据处理

MNIST是一个非常有名的手写体数字识别数据集。Tensorflow的封装让使用MNIST数据集变得更加方便,tf提供了一个类来处理MNIST数据,这个类会自动下载并转化MNIST数据的格式,将数据从原始的数据包中解析成训练和测试神经网络时使用的格式。

神经网络模型训练及不同模型结果对比

首先给出一个tf程序解决MNIST问题,然后在验证数据集上评价表现,使用第四章介绍的优化方法改进模型。

Tensorflow训练神经网络

使用第四章介绍的训练和优化方法(指数衰减、正则、滑动平均)实现的代码:

从上面的结果可以看出,在训练初期,模型在验证数据集上的表现越来越好。从第4000轮开始,模型的表现开始波动,说明模型已经接近极小值。

程序开始设置了初始学习率、学习率衰减率、隐藏层节点数量、迭代轮数等7种不同的参数。一般从验证数据集评判不同参数取值下模型的表现。交叉验证的方式会花费大量时间,所以在海量数据的情况下,一般会更多地采用验证数据集的形式。

不同模型效果比较

本小节使用神经网络模型在MNIST测试数据集上的正确率作为评价不同优化方法的标准。

从上图可以看到,神经网络的结构对模型的效果有本质性的影响。而滑动平均、指数衰减和正则化带来的提升并不是特别明显。因为滑动平均和指数衰减在一定程度上都是限制神经网络中参数更新的速度,而在MNSIT数据上,模型收敛的速度很快,所以这两种优化影响不大。

可以看到,从4000轮之后,因为梯度本身较小,参数的改变也就缓慢了。于是滑动平均或者指数衰减的作用没有那么突出了。然而,当问题更加复杂时,迭代不会那么快收敛,比如Cifar-10数据集上,使用滑动平均模型可以将错误率降低11%,使用指数衰减可以将错误率降低7%。

相比滑动平均和指数衰减,正则化给模型带来的提升相对显著。下图对比了两个使用不同损失函数的神经网络模型。实线给出了正确率的变化趋势,虚线给出了当前训练batch的交叉熵损失。

可以看到,只优化交叉熵的模型在训练数据上的交叉熵损失要比优化总损失的模型更小。然而在测试数据上,优化总损失的模型却要好于只优化交叉熵的模型。这个原因就是过拟合问题。下图显示了不同模型的损失函数的变化趋势。

左侧随着迭代的进行,正则化损失是在不断加大的,因为MNIST问题相对比较简单,迭代后期的梯度很小,所以正则化损失的增长也不快。如果问题更复杂,总损失会呈现出一个U字型。右侧的正则化损失也可以随着迭代的进行越来越小,从而使得整体的损失呈现一个逐步递减的趋势。

变量管理

在之前计算神经网络前向传播结果的过程抽象成了一个函数,通过这种方式在训练和测试的过程中可以统一调用同一个函数来得到模型的前向传播结果。

def inference(input_tensor, avg_class, weights1, biases1, weights2, biases2):

这个函数的参数中包括了神经网络中的所有参数,当神经网络的结构更加复杂,参数更多时,就需要一个更好的方式来传递和管理神经网络中的参数了。tf提供了通过变量名称创建或者获取一个变量的机制,在不同函数中可以直接通过变量名字来使用变量,而不需要将变量通过参数的形式到处传递。

第四章中介绍了通过tf.Variable函数来创建一个变量。除了tf.Variable,tf还提供了tf.get_variable来创建或者获取变量。tf.get_variable创建变量的功能和tf.Variable基本相同。

其中tf提供的initializer函数和第三章中介绍的随机数以及常量生成函数大部分是一一对应的。

tf.get_variable的变量名称参数是必须的,如果名称已存在,创建变量的程序会报错。

tf.get_variable也可以获取一个已经创建过的变量,需要通过tf.variable_scope生成一个上下文管理器。

当tf.variable_scope使用参数reuse=True时,这个上下文管理器内所有的tf.get_variable函数会直接获取已经创建的变量。如果变量不存在则报错。reuse=False或者None,tf.get_variable将创建新变量。tf.variable_scope是可以嵌套的。

tf.variable_scope也会创建一个tf中的命名空间,在其中创建的变量都会带上这个命名空间作为前缀。

将前面的函数改进一下:

当神经网络更加复杂时,使用这种变量管理方式将大大提高程序的可读性。

Tensorflow模型持久化

持久化即保存训练好的模型,并可以从文件中还原。

持久化代码实现

tf提供了一个非常简单的API保存和还原一个神经网络模型:tf.train.Saver类。

保存计算图:

tf模型一般会存在后缀为.ckpt的文件中,且目录下会出现三个文件,因为tf会将计算图的结构和图上参数取值分开保存。model.ckpt.meta保存计算图的结构,model.ckpt保存tf每一个变量的取值,checkpoint保存目录下所有的模型文件列表。三个文件的具体内容在下小节介绍。

加载模型:

加载模型和保存模型的代码基本相同,唯一的不同是加载模型的代码中没有运行变量的初始化过程,而是将变量的值通过已经保存的模型加载进来。如果不希望重复定义图上的运算,也可以直接加载图。

也可以保存或加载部分变量,在声明tf.train.Saver类时可以提供一个列表来指定。比如saver=tf.train.Saver([v1]),只有v1被加载进来。另外还可以在保存或加载时给变量重命名:

tf通过字典将模型保存时的变量名和需要加载的变量联系起来。

这样做的主要目的之一是方便使用变量的滑动平均值。如果在加载模型时直接将影子变量映射到变量自身,那么在使用训练好的模型时就不需要再调用函数来获取变量的滑动平均值了。

通过重命名读取滑动平均值:

为了方便加载时重命名滑动平均变量,tf.train.ExponentialMovingAverage提供了variables_to_restore函数来生成重命名字典。

使用tf.train.Saver会保存运行tf程序所需要的全部信息,然而在测试或者预测时,只需要知道前向传播得到输出即可,不需要类似于变量初始化、模型保存等辅助节点的信息。tf提供了convert_variables_to_constants函数,将计算图中的变量及其取值通过常量的方式保存,这样整个计算图可以统一存放在同一个文件中。

张量的名称后面有:0,表示某个计算节点的第一个输出。而计算节点本身的名称后是没有:0的。

加载模型并直接得到结果:

持久化原理及数据格式

上小节介绍了当调用saver.save函数时,tf会生成3个文件。这小节将详细介绍这3个文件保存的内容和数据格式。

model.ckpt.meta
tf通过元图(MetaGraph)保存计算图中节点的信息以及元数据。元图是由MetaGraphDef Protocol Buffer定义的。上小节保存的文件就是model.ckpt.meta。

元图中主要记录了5类信息,.meta文件是二进制,无法直接查看。tf提供了export_meta_graph函数,以json格式导出MetaGraphDef Protocol Buffer。

  • meta_info_def
    meta_info_def是通过MetaInfoDef定义的,它记录了tf计算图中的元数据以及tf程序中所有使用到的运算方法的信息。

元数据包括计算图的版本号以及用户指定的一些标签(tags),没有特殊指定默认为空。只有stripped_op_list属性是不为空的。stripped_op_list记录了tf计算图上使用到的所有运算方法的信息。每个方法只会出现一次。
- OpList
stripped_op_list的类型是OpList,OpList是一个OpDef类型的列表。

name定义了运算的名称,也是一个运算的唯一标识符。下面介绍的GraphDef将通过name来引用不同的运算。input_arg和output_arg定义了输入和输出列表(repeated),attr给出了其他的运算参数信息。model.ckpt.meta.json文件中的一个Op为:

名称为Add的运算,有2个输入和1个输出,输入输出都指定了type_attr,值为T。在attr属性中,必须出现name为T的属性。这个样例中,这个属性制定了运算输入输出允许的参数类型(allowed_values)。

  • graph_def
    graph_def记录了tf计算图的节点信息,每一个节点对应了一个运算。meta_info_def包含了运算的具体信息,graph_def关注运算的连接结构。graph_def是通过GraphDef Protocol Buffer定义的,主要包含了一个NodeDef类型的列表。

versions属性存储了tf版本号。NodeDef名称name是节点唯一标识符。tf程序中可以通过节点的名称来获取相应的节点。op属性是使用的运算方法的名称,通过这个名称在meta_info_def属性中找到该运算。
input是字符串列表,取值格式为node:src_output,当src_output为0时可省略。比如node:0也可以写作node。(node的第一个输出)
device指定了处理这个运算的设备。第10章具体介绍如何指定运行设备。device为空时,tf会自动选取一个合适的设备。attr指定了和当前运算相关的配置信息。model.ckpt.meta.json文件一些计算节点如下:

第一个节点是变量定义的运算,名称为v1,运算方法为Variable,attr属性指定了变量的维度和类型。
第二个节点是加法运算。输入为v1/read,v2/read。v1/read(:0)代表的节点可以读取变量v1的值。save/control_dependency是系统在完成模型持久化过程中自动生成的一个运算。versions是tf版本号。

  • saver_def
    saver_def记录了持久化模型需要的一些参数,比如保存文件名、保存和加载操作的名称以及保存频率、清理历史记录等。saver_def类型为SaverDef。

model.ckpt.meta.json文件的内容:

filename_tensor_name给出了保存文件名的张量名称,就是节点save/Const的第一个输出。save_tensor_name是持久化tf模型的运算对应的节点名称,就是graph_def的save/control_dependency节点。加载tf模型的运算名称是restore_op_name。max_to_keep和keep_checkpoint_every_n_hours属性设定了tf.train.Saver类清理之前模型的策略。max_to_keep为5的时候,第6次调用saver.save时,第一次保存的模型就会被删除。keep_checkpoint_every_n_hours,每n小时可以在max_to_keep的基础上多保存一个模型。

  • collection_def
    tf计算图可以维护不同集合,底层实现就是collection_def属性。collection_def是一个从集合名称到集合内容的映射,名称为字符串,内容为CollectionDef Protocol Buffer。

tf可以维护4类不同的集合。NodeList维护节点集合,ByteList维护字符串或者序列化之后的Protocol Buffer集合,比如张量是Protocol Buffer表示,张量集合就是ByteList。Int64List维护整数集合,FloatList维护实数集合。model.ckpt.meta.json文件的内容:

样例程序中维护了两个集合,一个是所有变量的集合,一个是可训练变量的集合。元素都是v1和v2。他们都是系统自动维护的。

model.ckpt

model.ckpt保存了所有变量的取值。这个文件是通过SSTable格式存储的,可以大致理解为就是一个(key,value)列表。
model.ckpt列表的第一行描述了文件的元信息,比如这个文件中存储的变量列表。剩下的每一行保存了一个变量的片段,片段信息通过SavedSlice Protocol Buffer定义,保存了变量名称、当前片段信息以及变量取值。tf提供了tf.train.NewCheckpointReader类查看model.ckpt文件。

checkpoint

这个文件是tf.train.Saver自动生成且维护的。checkpoint文件维护了所有tf模型文件的文件名,当某个模型被删除时,也会从checkpoint文件中删除。checkpoint内容格式为CheckpointState Protocol Buffer。

model_checkpoint_path保存了最新的模型文件,all_model_checkpoint_paths列出了当前所有模型文件。
样例文件:

Tensorflow最佳实践样例程序

本章前面已经给出了一个完整的tf程序解决MNIST问题,然而这个程序的可扩展性并不好。比如,前向传播需要传入所有变量,可读性很差;没有持久化训练好的模型,且需要每隔一段时间保存一次模型训练的中间结果(防止程序退出导致结果丢失)。

结合前面介绍的变量管理和持久化机制,本节介绍一个tf训练神经网络模型的最佳实践。将训练和测试分为两个独立的程序,每一个组件更加灵活。训练神经网络的程序可以持续输出训练好的模型,而测试程序可以每隔一段时间检验最新模型的正确率。将前向传播的过程抽离成一个单独的库函数,方便且保持训练和测试使用的前向传播方法是一致的。

重构之后的代码分为三个程序,mnist_inference.py定义了前向传播的过程以及神经网络中的参数。mnist_train.py定义了神经网络的训练过程。mnist_eval.py定义了测试过程。

mnist_inference.py

mnist_train.py

运行程序得到结果:

新的训练代码中,不再将训练和测试跑在一起。训练过程中,没1000轮输出一次损失函数的大小评估训练的效果。每1000轮保存一次训练好的模型,这样可以通过一个单独的测试程序,更加方便的在滑动平均模型上做测试。

mnist_eval.py

测试程序每10秒运行一次,每次运行都是读取最新保存的模型,并在验证集上计算正确率。运行程序得到结果如下,注意因为训练程序不一定每10秒输出一个新模型,所以有些模型可能被测试多次。一般解决真实问题时,不会这么频繁地运行评测程序。

github code

第六章 图像识别与卷积神经网络

卷积神经网络(CNN)的应用非常广泛,在自然语言处理、医药发现、灾难气候发现甚至围棋人工智能程序中都有应用。本章主要通过卷积神经网络在图像识别上的应用讲解卷积神经网络的基本原理以及如何使用Tensorflow实现。内容包括图像识别领域解决的问题以及经典数据集、卷积神经网络的主体架构、tf实现两个经典的CNN模型以及实现CNN的迁移学习。

图像识别问题简介及经典数据集

图像识别问题希望借助计算机程序处理、分析和理解图片中的内容,使得计算机可以从图片中自动识别不同模式的目标和对象。第五章介绍的MNIST数据集就是识别图片中的手写体数字。下图显示了图像识别的主流技术在MNIST数据集上的错误率随着年份的发展趋势图。

其他经典数据集还有Cifar、ImageNet等。在这些数据集上,使用CNN的算法都超过了人类的表现,给图像识别问题带来了质的飞跃。

卷积神经网络简介

前面的章节介绍的神经网络每两层之间的所有节点都是有边相连的,称为全连接神经网络。卷积神经网络、循环神经网络等是非全连接神经网络。

全连接神经网络一般会将每一层的节点组织成一列,方便显示连接结构。卷积神经网络相邻两层之间只有部分节点相连,为了展示每一层神经元的维度,一般会将每一层卷积层的节点组织成一个三维矩阵。

卷积神经网络和全连接神经网络的唯一区别在于神经网络中相邻两层的连接方式,输入输出、损失函数和训练流程基本一致。

全连接神经网络无法很好地处理图像识别问题的主要原因是参数太多,导致速度减慢,还很容易导致过拟合问题。而卷积神经网络很好的避免了这个问题。

卷积神经网络架构图:

一个卷积神经网络主要由以下5中结构组成:

  1. 输入层
    在处理图像的CNN中,它一般代表了一张图片的像素矩阵,比如32323。从输入层开始,CNN通过不同的神经网络结构将上一层的三维矩阵转化为下一层的三维矩阵,直到最后的全连接层。
  2. 卷积层
    卷积层中每一个节点的输入只是上一层神经网络的一小块,这小块常用的大小是33,55等。CNN将每一小块进行深入地分析从而得到抽象程度更高的特征。一般来说,通过卷积层处理过的节点矩阵会变得更深。
  3. 池化层
    池化层不会改变三维矩阵的额深度,但是可以缩小矩阵的大小。可以认为是将一张高分辨率图片转化为分辨率较低的图片。可以进一步减少参数的数量。
  4. 全连接层
    经过几轮卷积层和池化层的处理之后,可以认为图像中的信息已经被抽象成了信息含量更高的特征。在特征提取之后,仍然需要全连接层来完成分类任务。
  5. Softmax层
    和第四章介绍的作用相同,用于得到不同种类的概率分布。

下面将详细介绍卷积神经网络特殊的两个结构:卷积层和池化层。

卷积神经网络常用结构

卷积层

下图是卷积层神经网络结构中最重要的部分,称为过滤器(filter)或者核(kernel)。过滤器可以将当前层神经网络上的一个子节点矩阵转化为下一层神经网络上的一个单位节点矩阵。单位节点矩阵指的是一个长和宽都是1,深度不限的节点矩阵。

过滤器处理的节点矩阵的尺寸也称之为过滤器的尺寸,其长和宽是人工指定的。常用的有33和55。因为过滤器处理的矩阵深度和当前神经网络节点矩阵的深度一致,所以虽然节点矩阵是三维的,过滤器的尺寸只需要指定长和宽。过滤器中还要指定的一个维度是单位节点矩阵的深度,也称为过滤器的深度(或者过滤器的个数)。

下面展示过滤器的前向传播过程,以一个223的节点矩阵变化为一个115的单位节点矩阵为例。总共需要223*5+5=65个参数,+5为偏置项的个数。单位矩阵中第i个节点的取值g(i)为:

其中,g(0)的计算过程:

卷积层结构的前向传播过程就是通过一个过滤器从神经网络当前层的左上角移动到右下角,并且在移动中计算每一个对应的单位矩阵得到的。

本笔记没有完整记录过滤器的前向传播过程,具体参考原书或其他资料。几个概念:same padding,valid padding,步长(step)。

在卷积神经网络中,每一个卷积层中使用的过滤器中的参数都是一样的。共享过滤器的参数可以使得图像上的内容不受位置的影响。同时大幅度减少参数数量。加入输入层维度为32×32×3,第一层卷积层使用55316的过滤器,那么参数个数为55316+16=1216个。而使用500个隐藏节点的全连接层有1.5百万个参数。另外,卷积层的参数个数和图片的大小无关,它只和过滤器的尺寸、深度以及当前层节点矩阵的深度有关,使得CNN可以很好的扩展到更大的图像数据上。

使用same padding、步长为2的卷积层前向传播流程:

左上角的格子计算过程为:

tf对CNN提供了非常好的支持,下面的程序实现了一个卷积层的前向传播过程。

池化层

和卷积层类似,池化层前向传播的过程也是通过移动一个类似过滤器的结构完成的。过滤器中的计算不是节点的加权和,而是最大值或者平均值运算,分别称为 max pooling和average pooling。同时,池化层的过滤器也需要人工设定过滤器的尺寸、padding方式以及步长等,且这些设置的意义相同。和卷积层移动方式不同的是,池化层的过滤器只影响一个深度上的节点,所以除了在长和宽方向上移动,还需要在深度这个方向上移动。如图所示:

tf实现:

其中ksize(过滤器尺寸)和strides(步长)的数组的第一个和最后一个元素必须为1,因为不能影响节点矩阵的深度。过滤器尺寸常用的有[1,2,2,1]和[1,3,3,1]。tf的另一个池化方式tf.nn.avg_pool使用方式是一致的。

经典卷积网络模型

通过上小节介绍的卷积层和池化层可以组合任意结构的CNN。但怎样结构的CNN才更好的解决真实图像问题呢?本小节介绍一些经典的CNN网络结构:LeNet-5和Inception。

LeNet-5模型

LeNet-5是Yann LeCun提出的,是第一个成功应用于数字识别问题的卷积神经网络。总共有7层:

subsampling即pooling,池化

  • 第一层:卷积层
    输入是原始的图像像素,输入层大小32321。第一个卷积层过滤器尺寸为55,深度6。输出2828*6的feature map。
  • 第二层:池化层
    过滤器大小为2*2,输出为14×14×6。
  • 第三层:卷积层
    过滤器大小55,深度16,输出1010*16
  • 第四层:池化层
    过滤器大小22,输出55*16
  • 第五层:全连接层
    输出节点120
  • 第六层:全连接层
    输出节点84
  • 第七层:全连接层
    输出节点10

tf实现LeNet-5解决MNIST问题:训练过程可以复用第五章中的mnist_train.py,不过因为CNN的输入层是一个三维矩阵,所以需要调整一下输入数据的格式:

修改mnist_inference.py实现LeNet-5模型的前向传播过程:

几个要点:

  1. 训练过程中加入dropout避免过拟合,dropout一般只在全连接层而不是卷积层或者池化层中使用
  2. 只有全连接层的权重需要加入正则化

运行mnist_train.py,得到输出:

相比第五章98.4%的准确率,使用CNN可以达到99.4%。

一种CNN架构不能解决所有问题,比如LeNet-5就无法很好地处理类似ImageNet这样比较大的图像数据集。那么如何设计CNN的架构呢?可以用下面的正则表达式公式总结经典的图片分类问题的CNN架构:

输入层 ->( 卷积层+ -> 池化层? )+ -> 全连接层+

“卷积层+”表示一层或多层卷积层,一般连续使用最多3层。“池化层?”表示没有或者一层池化层,部分论文中发现可以直接通过调整卷积层步长起到和池化层相同的效果。CNN输出之前一般会经过1~2个全连接层。按照这个公式,LeNet-5的架构为:

输入层->卷积层->池化层->卷积层->池化层->全连接层->全连接层->输出层

除此之外,AlexNet、ZF Net、VGGNet等都满足上面的公式,其中VGG论文中介绍的作者尝试过的不同CNN架构如下:

convX-Y表示过滤器边长为X,深度为Y。VGG中过滤器边长一般为3和1,每经过一次池化层之后,过滤器深度都会乘2。卷积层的步长一般为1,最大池化比较常用,池化层的过滤器边长一般为2或者3,步长一般为2或3.

Inception-v3模型

本小节介绍Inception结构以及Inception-v3卷积神经网络模型。在LeNet-5中,不同卷积层通过串联的方式连接在一起,而Inception结构将不同卷积层并联。

前面提到了一个卷积层可以使用边长为1、3或5的过滤器,如何在这些边长中选呢。Inception同时使用所有不同尺寸的过滤器,然后将得到的输出矩阵拼接起来。

上图Inception使用了1/3/5三种过滤器,不同矩阵代表不同计算路径。所有过滤器使用Same Padding且步长为1,得到的输出矩阵大小一致,所以可以拼接成更深的矩阵。Inception-v3架构如下:

详细架构参考论文
Rethinking the Inception Architecture for Computer Vision

Inception-v3总共有46层,由11个Inception模块组成,上图中方框标注的就是一个Inception模块。

Tensorflow-Slim是一个简洁的实现卷积层的工具,相比直接使用tf,代码量减少很多:

下面用tf-slim实现方框中的Inception模块:

卷积神经网络迁移学习

介绍迁移学习的概念以及如何通过tf实现。

迁移学习介绍

迁移学习是将一个问题上训练好的模型通过简单的调整使其适用于一个新的问题。根据论文DeCAF: A Deep Convolutional Activation Feature for Generic Visual Recognition
中的结论,可以保留训练好的Inception-v3模型中所有卷积层的参数,只是替换最后一层全连接层。最后这一层全连接层之前的网络层称之为瓶颈层(bottleneck)。有理由认为瓶颈层输出的节点向量可以被作为任何图像的一个更加精简且表达能力更强的特征向量。于是,在新数据集上,可以直接利用这个训练好的模型进行特征提取,再将这个特征向量作为输入来训练一个新的单层全连接神经网络处理新的分类问题。
一般来说,数据量足够的情况下,迁移学习的效果不如完全重新训练。但是迁移学习所需要的训练时间和训练样本都远远小于完整训练的模型。

Tensorflow实现迁移学习

首先下载数据集

解压之后的文件夹包含5个子文件夹,每个子文件夹的名称为一种花的名称。

下载Google提供的Inception-v3模型

实现代码:

运行之后的结果:

效果还不错。

  • 17
    点赞
  • 9
    评论
  • 42
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值