TensorFlow6:图像识别与卷积神经网路

在前一章中,通过MNIST数据集验证了前面介绍的神经网络设计和优化的方法。从实验的结果可以看出,神经网络的结构会对神经网络的准确率产生巨大的影响。本章将介绍一个非常常用的神经网络结构-——卷积神经网络(Convolutional Neural NetWork),我们将介绍:
1.图像识别领域解决的问题以及图像识别领域中经典的数据集。
2.介绍卷积神经网络的主题思想和整体架构。
3.详细讲解卷积层和池化层的网络结构,以及TensorFlow对这些网络结构的支持。
4.通过两个经典的卷积神经网络模型来介绍如何设计卷积神经网络的架构以及如何设置每一层神经网络的配置。将通过TensorFlow实现LeNet-5模型,并介绍TensorFlow-Slim来实现更加复杂的Inception-v3模型中的Inception模块。
5.最后将介绍如何通过TensorFlow实现卷积神经网络的迁移学习。

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

视觉是人类认识世界非常重要的一种知觉。对于人类来说,通过视觉来识别手写体数字、识别图片中的物体或者找出图片中人脸的轮廓都是非常简单的任务。然而对于计算机而言,让计算机识别图片中的内容就不是一件容易的事情了。图像识别问题希望借助计算机程序来处理、分析和理解图片中的内容,使得计算机可以从图片中自动识别各种不同模式的目标和对象。比如在第5章中介绍的MNIST数据集就是通过计算机来识别图片中的手写体数字。图像识别问题作为人工智能的一个重要领域,在最近几年已经取得了很多突破性的进展。本章将要介绍的卷积神经网络就是这些突破性进展背后的最主要技术支持。下图中显示了图像识别的主流技术在MNIST数据集上的错误率随着年份的发展趋势图。

不同算法在MNIST数据集上最好表现变化趋势图

上图中最下方的虚线表示人工标注的错误率,其他不同的线段表示了不同算法的错误率。而且通过卷积神经网络达到的错误率已经非常接近人工标注的错误率了。在MNIST数据集的一万个测试数据上,最好的深度学习算法只会比人工识别多错一张图片。
MNIST手写体识别数据集是一个相对简单的数据集,在其他更加复杂的图像识别数据集上,卷积神经网络有更加突出的表现。Cifar数据集就是一个影响力很大的图像分类数据集。Cifar数据集分为了Cifar-10和Cifar-100两个问题,他们都是图像词典项目(Visual Dictionary)中800万张图片的一个子集。Cifar数据集中的图片为3232的彩色图片,这些图片是由Alex Krizhevsky教授、Vinod Nair博士和Geoffrey Hinton教授整理的。
Cifar-10数据集样例图片

Cifar-10问题收集了来自10个不同种类的60000张图片。上图左侧显示了Cifar-10数据集中的每一个种类中的一些样例图片以及这些种类的类别名称,上图的右侧给出了Cifar-10中一张飞机的图像。因为图像的像素仅为32
32,所以放大之后图片是比较模糊的,但隐约还是可以看出飞机的轮廓。Cifar官网http://www.cs.toronto.edu/~kriz/cifar.html
提供了不同格式的Cifar数据集下载。
和MNIST数据集类似,Cifar-10中的图片大小都是固定的且每一张图片中仅包含一个种类的实体,但和MNIST相比,Cifar数据集最大的区别在于图片由黑白变成的彩色,且分类的难度也相对更高。在Cifar-10数据集上,人工标注的正确率大概为94%,这比 MNIST数据集上的人工表现要低很多。下图给出了MNIST和Cifar-10数据集中比较难以分类的图片样例:
MNIST和Cifar-10数据集中分类难度较高的样例

在上图中的左侧的四张图片给出了Cifar-10数据集中比较难分类的图片,直接从图片上看,人类也很难判断图片上实体的类别。上图右侧的四张图片给出了MNIST数据集中难度较高的图片。在这些难度较高的图片上,人类还是可以有一个比较准确的猜测。目前在Cifar-10数据集上最好的图像识别算法正确率为95.59%,达到这个正确率的算法同样使用了卷积神经网络。
无论是MNIST数据集还是Cifar数据集,相比真实环境下的图像识别问题,有2个最大的问题。第一,现实生活中的图片分辨率要远高于32*32,而且图像的分辨率也不会是固定的。第二,现实生活中的物体类别很多,无论是10种还是100种都远远不够,而且一张图片中不会只出现一个种类的物品。为了更加贴近真实环境下的图像识别问题,由斯坦福大学的李飞飞教授带头整理的ImageNet很大程度地解决了这两个问题。
ImageNet是一个基于WordNet的大型图像数据库。在ImageNet中,将近1500万图片被关联到了WordNet的大约20000个名词同义词集上。目前每一个与ImageNet相关的WordNet同义词集都代表了现实世界中的一个实体,可以被认为是分类问题中的一个类别。ImageNet中的图片都是从互联网上爬取下来的,并且通过亚马逊的人工标注服务将图片分类到WordNet的同义词集上。在ImageNet的图片中,一张图片中可能出现多个同义词集所代表的的实体。
下图展示了ImageNet中的一张图片,在这张图片上用几个矩形框除了不同实体的轮廓。在物体识别问题中,一般将用于框出实体的矩形称为bounding box。下图中总共可以找到四个实体,其中有两把椅子、一个人和一条狗。类似下图所示,ImageNet的部分图片中的实体轮廓也被标注了出来,以用于更加精确的图像识别。
ImageNet样例图片以及标注出来的实体轮廓

ImageNet每年都举办图像识别相关的竞赛(ImageNet Large Scale Visual Recognition Challenge, ILSVRC),而且每年的竞赛都会有一些不同的问题,这些问题基本涵盖了图像识别的主要研究方向。ImageNet的官网http://www.image-net.org/challenges/LSVRC列出了历届ILSVRC竞赛的题目和数据集。不同年份的ImageNet比赛提供了不同的数据集,本书将着重介绍使用的最多的ILSVRC2012图像分类数据集中的图片是直接从互联网上爬去得到的,所以图片的大小从几千字节到几百万字节不等。
下图给出了不同算法在ImageNet图像分类数据集上的top-5正确率:
不同算法在ImageNet ILSVRC2012图像分类数据集上的正确率

top-N正确率指的是图像识别算法给出前N个答案中有一个是正确的概率。在图像分类问题上,很多学术论文都将前N个答案的正确率作为比较的方法,其中N的取值一般为3或5.从下图中可以看出,在更加复杂的ImageNet问题上,基于卷积神经网络的图像识别算法可以远远超过人类的表现。在下图的左侧对比了传统算法与深度学习算法的正确率。从图中可以看出,深度学习,特别是卷积神经网络,给图像识别问题带来了质的飞跃。2013年之后,基本上所有的研究都集中到了深度学习算法上。从下一节开始将具体介绍卷积神经网络的基本原理,以及如何通过TensorFlow实现卷积神经网络。

2.卷积神经网络简介

在上一节中介绍图像识别问题时,已经多次提到了卷积神经网络。卷积神经网络在上一节中介绍的所有图像分类数据集上有非常突出的表现。在前面的章节中所介绍的神技能网络每两层之间的所有节点都是有边相连的,所以称这种网络结构为全连接层网络结构。为了将只包含全连接层的神经网络与卷积神经网络、循环神经网络区分开,这里将只包含全连接层的神经网络称之为全连接神经网络。在前面介绍的神经网络都为全连接神经网络。在这一节中将讲解卷积神经网络与全连接神经网络的差异,并介绍组成一个卷积神经网络的基本网络结构。下图显示了全连接神经网络与卷积神经网络的结构对比图:

全连接神经网络与卷积神经网络结构示意图

虽然上图中显示的全连接神经网络和卷积神经网络的结构直观上差异比较大,但实际上它们的整体架构是非常相似的。从上图可以看出,卷积神经网络也是通过一层一层的节点组织起来的。和全连接神经网络一样,卷积神经网络中每一个节点都是一个神经元。在全连接神经网络中,每相邻两层之间的节点都有边相连,于是一般会将每一层全连接层中的节点组织成一列,这样方便显示连接结构。而对于卷积神经网络,相邻两层之间只有部分节点相连,为了展示每一层神经元的维度,一般会将每一层卷积层的节点组织称一个三维矩阵。
除了结构相似,卷积神经网络的输入输出以及训练流程与全连接神经网络也基本一致。以图像分类为例,卷积神经网络的输入层就是图像的原始像素,而输出层中的每一个节点代表了不同类别的可信度。这和全连接神经网络的输入输出是一致的。类似的,前面第4章介绍的损失函数以及参数的优化过程也都适用于卷积神经网络。在后面的章节中会看到,在TensorFlow中训练一个卷积神经网络的流程和训练一个全连接神经网络没有任何区别。卷积神经网络和全连接神经网络的唯一区别就在于神经网络中相邻两层的连接方式。在进一步介绍卷积神经网络的连接结构之前,本节将先介绍为什么全连接神经网络无法很好地处理图像数据。
使用全连接神经网络处理图像的最大问题在于全连接层的参数太多。对于MNIST数据,每一张图片的大小是28281,其中2828为图片的大小,1表示图像是黑白的,只有一个色彩通道。假设第一层隐藏层的节点数为500个,那么一个全连接层的神经网络将有2828500+500=392500个参数。当图片更大时,比如在Cifar-10数据集中,图片的大小为32323,其中3232表示图片的大小,3表示图片是通过红绿蓝三个色彩通道表示的。这样输入层就有3072个节点,如果第一层全连接层仍然是500个节点,那么这一层全连接神经网络将有3072500+500约等于150万个参数。参数增多除了导致计算速度减慢,还很容易导致过拟合问题。所以需要一个更合理的神经网络结构来有效地减少神经网络中参数个数。卷积神经网络就可以达到这个目的。
下面给出了一个更加具体的卷积神经网络架构图:
用于图像分类问题的一种卷积神经网络架构图

在卷积神经网络的前几层中,每一层的节点都被组织成一个三维矩阵。比如处理Cifar-10数据集中的图片时,可以将输入层组织成一个32
323的三维矩阵。上图中虚线部分展示了卷积神经网络的一个连接示意图,从图中可以看出卷积神经网络中前几层中每一个节点只和上一层中部分的节点相连。卷积神经网络的具体连接方式将在下节中介绍。一个卷积神经网络主要有以下5中结构组成:
1.输入层:输入层是整个神经网络的输入,在处理图像的卷积神经网络中,它一般代表了一张图片的像素矩阵。比如在上图中,最左侧的三维矩阵就可以代表一张图片。其中三维矩阵的长和宽代表了图像的大小,而三维矩阵的深度代表了图像的色彩通道。比如黑白图片的深度为1,而RGB色彩模式下,图像的深度为3.从输入层开始,卷积神经网络通过不同的神经网络结构将上一层的三维矩阵转化为下一层的三维矩阵,直到最后的全连接层。
2.卷积层:从名字就可以看出,卷积层是一个卷积神经网络中最重要的部分。和传统全连接不同,卷积层中每一个节点的输入只是上一层神经网络的一小块,这个小块常用的大小有3
3或者5*5.卷积层试图将神经网络中的每一小块进行更加深入地分析从而得到抽象程度更高的特征。一般来说,通过卷积层处理过的节点矩阵会变得更深,所以在上图中可以看到经过卷积层之后的节点矩阵的深度会增加。
3.池化层:池化层神经网络不会改变三维矩阵的深度,但是它可以缩小矩阵的大小。池化操作可以认为是将一张分辨率较高的图片转化为分辨率较低的图片。通过池化层,可以进一步缩小最后全连接层中节点的个数,从而达到减少整个神经网络中参数的目的。
4.全连接层:如上图所示,在经过多伦卷积层和池化层的处理之后,在卷积神经网络的最后一般会是有1到2个全连接层来给出最后的分类结果。经过几轮卷积层和池化层的处理之后,可以认为图像中的信息已经被抽象成了信息含量更高的特征。我们将卷积层和池化层看成自动图像特征提取的过程。在特征提取完之后,仍然需要使用全连接层来完成分类任务。
5.Softmax层:和第4章中介绍的一样,Softmax层主要用于分类问题。通过Softmax层,可以得到当前样例属于不同种类的概率分布情况。
下一节将详细介绍卷积神经网络中特殊的两个网络结构——卷积层和池化层。

3.卷积神经网络常用结构

上一节已经大致介绍了卷积层和池化层的概念,在本节中将具体介绍这两种网络结构。在下面的两个小节中将分别介绍卷积层和池化层的网络结构以及前向传播的过程,并通过TensorFlow实现这些网络结构。

3.1卷积层

本小节将详细介绍卷积层的结构及其前向传播的算法。下图显示了卷积层神经网络中最重要的部分,这个部分被称之为过滤器或者内核。以内TensorFlow文档中将这个结构称之为过滤器,所以在这里将统称为过滤器。

卷积层过滤器

如上图,过滤器可以将当前层神经网络上的一个子节点矩阵转化为下一层神经网络上的一个单位节点矩阵。单位节点矩阵指的是一个长和宽都为1,但深度不限的节点矩阵。
在一个卷积层中,过滤器所处理的节点矩阵的长和宽都是由人工指定的,这个节点矩阵的尺寸也被称之为过滤器的尺寸。常用的过滤器尺寸有33或55.因为过滤器处理的矩阵深度和当前层神经网络节点矩阵的深度是一致的,所以虽然节点矩阵是三维的,但过滤器的尺寸只需要指定两个维度。过滤器中另外一个需要人工智能指定的设置是处理得到的单位节点矩阵的深度,这个设置称为过滤器的深度。如上图所示,左侧小矩阵的尺寸为过滤器的尺寸,而右侧单位矩阵的深度为过滤器的深度。下节将通过一些经典卷积神经网络结构来了解如何设置每一层卷积层过滤器的尺寸和深度。
如上图所示,过滤器的前向传播过程就是通过左侧小矩阵中的节点计算出右侧单位矩阵中节点的过程。为了直观地解释过滤器的前向传播过程,在下面的篇幅中将给出一个具体的样例。在这个样例中将展示如何通过过滤器将一个223的节点矩阵变化为一个115的单位节点矩阵。一个过滤器的前向传播过程和全连接相似,它总共需要2235+5=65个参数,其中最后的+5为偏置项参数的个数。假设使用w^i_{x,y,z}来表示对于输出单位节点矩阵中的第i个节点,过滤器输入节点(x,y,z)的权重,使用b^i表示第i个输出节点对应的偏置项参数,那么单位矩阵中的第i个节点的取值g(i)为:
g(i) = f(\sum^2_{x=1}\sum^2_{y=1}\sum^3_{z=1}a_{x,y,z} * w^i_{x,y,z})+b^i
其中a_{x,y,z}为过滤器中节点(x,y,z)的取值,f为激活函数。下图展示了在给定a,w^0和b^0的情况下,使用ReLU作为激活函数时g(0)的计算过程:
使用过滤器计算g(0)取值的过程

在图的左侧给出了a和w^0的取值,这里通过3个二维矩阵来表示一个三维矩阵的取值,其中每一个二维矩阵表示三维矩阵在某一个深度上的取值。图中圆点.表示点积,也就是矩阵中对应元素乘积的和。图的右侧显示了g(0)的计算过程。如果给出w^1到w^4和b^1到b^4,那么也可以类似地计算出g(1)到g(4)的取值。如果将a和w^i组织成两个向量,那么一个过滤器的计算过程完全可以通过第3章中介绍的向量乘法来完成。
上面的样例已经介绍了在卷积层中计算一个过滤器的前向传播过程。卷积层结构的前向传播过程就是通过将一个过滤器从神经网络当前层的左上角移动到右下角,并且在移动中计算每一个对应的单位矩阵得到的。下图展示了卷积层结构前向传播的过程:
卷积层前向传播过程

为了更好地可视化过滤器的移动过程,图中使用的节点矩阵深度都为1.在图中,展示了在3
3矩阵上的使用22过滤器的卷积层前向传播过程。在这个过程中,最后到右下角矩阵。过滤器每移动一次,可以计算得到一个值(当深度为看、时会计算出k个值)。讲这些数值拼接成一个新的矩阵,就完成了卷积层前向传播的过程。图的右侧显示了过滤器在移动过程中计算得到的结果与新矩阵中节点的对应关系。
当过滤器的大小不为1
1时,卷积层前向传播得到的矩阵的尺寸要小于当前矩阵的尺寸。如上图所示,当前层矩阵的大小为33(左侧矩阵),而通过卷积层前向传播算法之后,得到的矩阵大小为22(右侧矩阵)。为了避免尺寸的变化,可以在当前层矩阵的边界上加入全0填充。这样可以使得卷积层前向传播结果矩阵的大小和当前层矩阵保持一致。
下图显示了使用全0填充后卷积层前向传播过程:
使用了全0填充的卷积层前向传播

从图中可以看出,加入一层全0填充后,得到的结构矩阵大小就为33了。
除了使用全0填充,还可以通过设置过滤器移动的步长来调整结果矩阵的大小。在上面两个图中,过滤器每次都只移动一格。
下图显示了当移动步长为2且使用全0填充时,卷积层前向传播的过程:
过滤器移动步长为2且使用全0填充时卷积层前向传播过程

从上图可以看出,当长和宽的步长均为2时,过滤器每隔2步计算一次结果,所以得到的结果矩阵的长和宽都只有原来的一半。下面的公式给出了在同时使用全0填充时结果矩阵的大小:
out_{length}=[in_{length / stride_{length}}]
out_{width} = [in_{width}/stride_{width}]
其中
out_{length}
表示输出层矩阵的长度,它等于输入层矩阵长度除以长度方向上的步长的向上取整值。类似的,out_{width}表示输出层矩阵的宽度,它等于输入层矩阵宽度除以宽度方向上的步长的向上取整值。如果不使用全0填充,下面的公式给出了结果矩阵的大小:
out_{length}=[(in_{length} -filter_{length}+1)/ stride_{length}]
out_{width} = [(in_{width}-filter_{width}+1)/stride_{width}]
前面三张图只讲解了移动过滤器的方式,没有涉及到过滤器中的参数如何设定,所以在这些图片中结果矩阵中并没有填上具体的值。在卷积神经网络中,每一个卷积层中使用的过滤器中的参数都是一样的。这是卷积神经网络一个非常重要的性质。从直观上理解,共享过滤器的参数可以使得图像上的内容不受位置的影响。以MNIST手写体数字识别为例,无论数字“1”出现在左上角还是右下角,图片的种类都是不变的。因为在左上角和右下角使用的过滤器参数相同,所以通过卷积层之后无论数字在图像上的哪个位置,得到的结果都一样。
共享每一个卷积层中过滤器中的参数可以巨幅减少神经网络上的参数。以Cifar-10问题为例,输入层矩阵的维度是32
323.假设第一层卷积层是用尺寸为55,深度为16的过滤器,那么这个卷积层的参数个数为553*16+16=1216个。前面小节提到过,使用500个隐藏节点的全连接层将有1.5百万个参数。相比之下,卷积层的参数个数要远远小于全连接层。而且卷积层的参数个数和图片的大小无关,它只和过滤器的尺寸、深度以及当前层节点矩阵的深度有关。这使得卷积神经网络可以很好地扩展到更大的图像数据上。
结合过滤器的使用方法和参数共享的机制,下图给出了使用全0填充、步长为2的卷积层前向传播的计算流程:
卷积层前向传播过程样例图

上图给出了过滤器上权重的取值以及偏置项的取值,通过上图的计算方法,可以得到每一个格子的具体取值。下面的公式给出了左上角格子取值的计算方法,其他格子可以以此类推.

TensorFlow对卷积神经网络提供了非常好的支持,下面的程序实现了一个卷积层的前向传播过程,以下代码可以看出,通过TensorFlow实现卷积层是非常方便的:

import tensorflow as tf

# 通过tf.get_variable的方式创建过滤器的权重变量和偏置项变量。上面介绍了卷积层的参数个数
# 只和过滤器的尺寸、深度以及当前层节点矩阵的深度有关,所以这里声明的参数变量是一个四维矩阵,
# 前面两个维度代表了过滤器的尺寸,第三个维度表示当前层的深度,第四个维度表示过滤器的深度。
filter_weight = tf.get_variable('weights', [5,5,3,16], initializer=tf.truncated_normal_initializer(stddev=0.1))
filter_weight

#和卷积层的权重类似,当前层矩阵上不同位置的偏置项也是共享的,所以总共有下一层深度各不同的偏置项。
# 本例代码中16为过滤器的深度,也是神经网络中下一层节点矩阵的深度。
biases = tf.get_variable('biases', [16], initializer=tf.constant_initializer(0.1))
# tf.nn.conv2d提供了一个非常方便的函数来实现卷积层前向传播的算法。这个函数的第一个输入为当前层的节点矩阵。
# 注意这个矩阵是一个四维矩阵,后面三个维度对应一个节点矩阵,第一维对应一个输入batch。比如在输入层,input[0,:,:,:]
# 表示第一张图片,input[1,:,:,:]表示第二张图片,以此内推。tf.nn.conv2d第二个参数提供了卷积层的权重,
# 第三个参数为不同维度上的步长。虽然第三个参数提供的是一个长度为4的数组,但是第一维和最后一维的数字要求一定是1.
# 这是因为卷积层的步长只对矩阵的长和宽有效。最后一个参数是填充的方法,TensorFlow中提供SAME或是VALID两种选择。其中
# SAME表示添加全0填充,“VALID”表示不添加。
conv = tf.nn.conv2d(input, filter_weight, strides=[1,1,1,1],padding='SAME')
# tf.nn.bias_add提供了一个方便的函数给每一个节点加上偏置项。注意这里不能直接使用加法,因为矩阵上不同位置上的节点
# 都需要加上同样的偏置项。
bias = tf.nn.bias_add(conv,biases)
# 将计算结果通过ReLU激活函数完成去线性化
actived_conv = tf.nn.relu(bias)
3.2池化层

上节介绍过卷积神经网络的大致结构。从第一张图中可以看出,在卷积层之间往往会加上一个池化层。池化层可以非常有效地缩小矩阵的尺寸,从而减少最后全连接层中的参数。使用池化层既可以加快计算速度也有防止过拟合问题的作用。

和3.1小节中介绍的卷积层类似,池化层前向传播的过程也是通过移动一格类似过滤器的结构完成的。不过池化层过滤器中的计算不是节点的加权和,而是采用更加简单的最大值或者平均值运算。使用最大值操作的池化层被称之为最大池化层,这是被使用得最多的池化层结构。使用平均值操作的池化层被称之为平均池化层(average pooling)。其他池化层在实践中使用的比较少,本书不做过多的介绍。
与卷积层的过滤器类似,池化层的过滤器也需要人工设定过滤器的尺寸、是否使用全0填充以及过滤器移动的步长等设置,而且这些设置的意义也是一样的。卷积层和池化层中过滤器移动的方式是相似的,唯一的区别在于卷积层使用的过滤器是横跨整个深度的,而池化层使用的过滤器只影响一个深度上的节点。所以池化层的过滤除了在长和宽两个维度移动之外,它还需要在深度这个维度移动。下图展示了一个最大池化层前向传播计算过程:


3*3*2节点矩阵经过全0填充且步长为2的最大池化层前向传播过程

在上图中,不同颜色或者不同阶段代表了不同的池化层过滤器。从图中可以看粗,池化层的过滤除了在长和宽的维度上移动,它还需要在深度的维度上移动。下面的TensorFlow程序实现了最大池化层的前向传播算法:

# tf.nn.max_pool实现了最大池化层的前向传播过程,它的参数和tf.nn.conv2d函数类似。
# ksize提供了过滤器的尺寸,strides提供了步长信息,padding提供了是否使用全0填充。
pool = tf.nn.max_pool(actived_conv, ksize = [1, 3, 3, 1], strides = [1, 2, 2, 1], padding='SAME')

对比池化层和卷积层前向传播在TensorFlow中的实现,可以发现函数的参数形式是相似的。在tf.nn.max_pool函数中,首先需要传入当前层的节点矩阵,这个矩阵是一个四维矩阵,格式和tf.nn.conv2d函数中的第一个参数一致。第二个参数为过滤器的尺寸。虽然给出的是一个长度为4的一维数组,但是这个数组的第一个和最后一个数必须为1.这意味着池化层的过滤器是不可以跨不同输入样例或者节点矩阵深度的。在实际应用中使用的最多的池化层过滤器尺寸为[1,2,2,1]或者[1,3,3,1].
tf.nn.max_pool函数的第三个参数为步长,它和tf.nn.conv2d函数中步长的意义是一样的,而且第一维和最后一维也只能为1。这意味着在TensorFlow中,池化层不能减少节点矩阵的深度或者输入样例的个数。tf.nn.max_pool函数的最后一个参数指定了是否使用全0填充。这个参数也只有两种取值——VALID或者SAME,其中VALID表示不使用全0填充,SAME表示使用全0填充。SAME表示使用全0填充。TensorFlow还提供了tf.nn.avg_pool来实现平均池化层。tf.nn.avg_pool函数的调用格式和tf.nn.max_pool函数是一致的。

4.经典卷积网络模型

在上一小节中介绍了卷积神经网络特有的两种网络结构——卷积层和池化层。然而,通过这些网络结构任意组合得到的神经网络有无限多种,怎样的神经网络更有可能解决真实的图像处理问题呢?这一节将介绍一些经典的卷积神经网络的网络结构。通过这些经典的卷积神经网络结构可以总结出卷积神经网络结构设计的一些模式。这节将介绍:
1.具体介绍LeNet-5模型,并给出一个完整的TensorFlow程序来实现LeNet-5模型。通过这个模型,将给出卷积神经网络结构设计的一个通用模式。
2.将介绍设计卷积神经网络结构的另外一种思路——Inception模型。这个小节将简单介绍TensorFlow-Slim工具,并通过这个工具实现谷歌提出的Inception-v3模型中的一个模块。

4.1LeNet-5模型

LeNet-5模型是Yann LeCun教授于1998年在论文Gradient-based learning applied to document recognition中提出的,它是第一个成功应用于数字识别问题的卷积神经网络。在MNIST数据集上,LeNet-5模型可以达到大约99.2%的正确率。LeNet-5模型总共有7层,下图展示了LetNet-5模型的架构:


LeNet-5模型结构图

在下面的篇幅中将详细介绍LeNet-5模型每一层的结构

第一层,卷积层

这一层的输入就是原始的图像像素,LeNet-5模型接受的输入层大小为32321.第一个卷积层过滤器的尺寸为55,深度为6,不使用全0填充,步长为1,。因为没有使用全0填充,所以这一层的输出的尺寸为32-5+1=28,深度为6.这一个卷积层总共有5516+6=156个参数,其中6个为偏置项参数。因为下一层的节点矩阵有28286=4704个节点,每个节点和55=25个当前层节点相连,所以本层卷积层总共有4704(25+1)=122304个连接。

第二层,池化层

这一层的输入为第一层的输出,是一个28286的节点矩阵。本层采用的过滤器大小为22,长和宽的步长均为2,所以本层的输出矩阵大小为1414*6.原始的LeNet-5模型中使用的过滤器和前面3.2节介绍的有些细微差别。

第三层,卷积层

本层的输入矩阵大小为14146,使用的过滤器大小为55,深度为16.本层不使用0填充,步长为1.本层的输出矩阵大小为101016.按照标准的卷积层,本层应该有55616+16=2416个参数,101016*(25+1)= 41600个连接。

第四层,池化层

本层的输入矩阵大小为101016,才用的过滤器大小为22,步长为2.本层的输出矩阵大小为55*16.

第五层,全连接层

本层的输入矩阵大小为5516,在LeNet-5模型的论文中将这一层称为卷积层,但是因为过滤器的大小就是55,所以和全连接层没有区别,在之后的TensorFlow程序实现中也会将这一层看成全连接层。如果将5516矩阵中的节点拉成一个向量,那么这一层和在前面2章中介绍的全连接层输入就不一样了。本层的输出节点个数为120,总共有5516120+120=48120个参数。

第六层,全连接层

本层的输入节点个数为120个,输出节点个数为84个,总共参数为120*84+84=10164个。

第七层,全连接层

本层的输入节点个数为84个,输出节点个数为10个,总共参数为84*10+10=850个。
上面介绍了LeNet-5模型每一层结构和设置,下面给出一个TensorFlow的程序来实现一个类似LeNet-5模型的卷积神经网络来解决MNIST数字识别问题。通过TensorFlow训练卷积神经网络的过程和第5章中介绍的训练全连接神经网络是完全一样的。损失函数的计算、反向传播过程的实现都可以复用前面给出的mnist_train.py程序。唯一的区别在于因为卷积神经网络的输入层为一个三维矩阵,所以需要调整一下输入数据的格式

# 根据输入数据placeholder的格式,输入一个四维矩阵
x = tf.placeholder(tf.float32, 
                   [BATCH_SIZE,    # 第一维表示一个batch中样例的个数。
                    mnist_inference.IMAGE_SIZE,# 第二维和第三维表示图片的尺寸。
                    mnist_inference.IMAGE_SIZE,
                    mnist_inference.NUM_CHANNELS], # 第四维表示图片的深度,对于RBG格式的图片,深度为5.
      name='x-input')

# 类似地将输入的训练数据格式调整为一个四维矩阵,并将这个调整后的数据传入sess.run过程。
reshaped_xs = np.reshape(xs, (BATCH_SIZE,
                              mnist_inference.IMAGE_SIZE,
                              mnist_inference.IMAGE_SIZE,
                              mnist_inference.NUM_CHANNELS))

在调整完输入格式之后,只需要在程序mnist_inference.py中实现类似LeNet-5模型结构的前向传播过程即可。下面给出了修改后的mnist_inference.py程序:

import tensorflow as tf

# 配置神经网络的参数
INPUT_NODE = 784
OUTPUT_NODE = 10

IMAGE_SIZE = 28
NUM_CHANNELS = 1
NUM_LABELS = 10

# 第一层卷积层的尺寸和深度
CONV1_DEEP = 32
CONV1_SIZE = 5

# 第二层卷积层的尺寸和深度
CONV2_DEEP = 64
CONV2_SIZE = 5
# 全连接层的节点个数
FC_SIZE = 512

# 定义卷积神技能网络的前向传播过程。这里添加了一个新的参数train,用于区分训练过程和测试过程。
# 在这个程序中将用到dropout方法,dropout可以进一步提升模型可靠性并防止过拟合。
# dropout过程只在训练时使用。


def inference(input_tensor, train, regularizer):
    # 声明第一层卷积层的变量并实现前向传播过程。这个过程和3.1小节中介绍的一致。
    # 通过使用不同的命名空间来隔离不同层的变量,这可以让每一层中的变量命名只需要考虑
    # 在当前层的作用,而不需要担心重命名问题。和标准的LeNet-5模型不大一样,这里定义的
    # 卷积层输入为28*28*1的原始MNIST图片像素。因为卷积层中使用了全0填充,所以输出为
    # 28*28*32的矩阵。
    with tf.variable_scope('layer1-conv1'):
        conv1_weights = tf.get_variable(
            "weight", [CONV1_SIZE, CONV1_SIZE, NUM_CHANNELS, CONV1_DEEP],
            initializer=tf.truncated_normal_initializer(stddev=0.1))
        conv1_biases = tf.get_variable("bias", [CONV1_DEEP], initializer=tf.constant_initializer(0.0))
        # 使用边长为5,深度为32的过滤器,过滤器移动的步长为1,且使用全0填充。
        conv1 = tf.nn.conv2d(input_tensor, conv1_weights, strides=[1, 1, 1, 1], padding='SAME')
        relu1 = tf.nn.relu(tf.nn.bias_add(conv1, conv1_biases))

    # 实现第二层池化层的前向传播过程。这里选用最大池化层,池化层过滤器的边长为2,使用全0填充且移动的步长为2.
    # 这一层的输入是上一层的输出,也就是28*28*32
    # 的矩阵。输出为14*14*32的矩阵。
    with tf.name_scope("layer2-pool1"):
        pool1 = tf.nn.max_pool(relu1, ksize = [1,2,2,1],strides=[1,2,2,1],padding="SAME")

    # 声明第三层卷积层的变量并实现前向传播过程。这一层输入为14*14*32的矩阵。
    # 输出为14*14*64的矩阵
    with tf.variable_scope("layer3-conv2"):
        conv2_weights = tf.get_variable(
            "weight", [CONV2_SIZE, CONV2_SIZE, CONV1_DEEP, CONV2_DEEP],
            initializer=tf.truncated_normal_initializer(stddev=0.1))
        conv2_biases = tf.get_variable("bias", [CONV2_DEEP], initializer=tf.constant_initializer(0.0))
        # 使用边长为5,深度为64的过滤器,过滤器移动的步长为1,且使用全0填充。
        conv2 = tf.nn.conv2d(pool1, conv2_weights, strides=[1, 1, 1, 1], padding='SAME')
        relu2 = tf.nn.relu(tf.nn.bias_add(conv2, conv2_biases))

    # 实现第四层池化层的前向传播过程。这一层和第二层的结构是一样的。这一层的输入为
    # 14*14*64的矩阵,输出为7*7*64的矩阵。
    with tf.name_scope("layer4-pool2"):
        pool2 = tf.nn.max_pool(relu2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
        # 将第四层池化层的输出转化为第五层全连接层的输入格式。第四层的输出为7*7*64的矩阵,
        # 然而第五层全连接层需要的输入格式为向量,所以在这里需要将这个7*7*64的矩阵拉直成一个向量
        # pools.get_shape函数可以得到第四层输出矩阵的维度而不需要手工计算。注意因为每一层神经网络
        # 的输入输出都为一个batch的矩阵,所以这里得到的维度也包含了一个batch中数据的个数。
        pool_shape = pool2.get_shape().as_list()
        # 计算将矩阵拉直成向量的长度,这个长度就是矩阵长宽以及深度的乘积。注意这里pool_shape[0]
        # 为一个batch中数据的个数
        nodes = pool_shape[1] * pool_shape[2] * pool_shape[3]
        # 通过tf.reshape函数将第四层的输出变成一个batch的向量。
        reshaped = tf.reshape(pool2, [pool_shape[0], nodes])

    # 声明第五层全连接层的变量并实现前向传播过程。这一层的输入是拉直之后的一组向量,向量长度为3136.
    # 输出是一组长度为512的向量。这一层和之前在第5章中介绍的基本一致,唯一的区别就是引入了dropout的概念。
    # dropout在训练时会随机将部分节点的输出改成0.dropout可以避免过拟合问题,从而使得模型在测试数据上的效果
    # 更好。dropout一般只在全连接层而不是卷积层或者池化层使用。
    with tf.variable_scope('layer5-fc1'):
        fc1_weights = tf.get_variable("weight", [nodes, FC_SIZE],
                                      initializer=tf.truncated_normal_initializer(stddev=0.1))
        # 只有全连接层的权重需要加入正则化。
        if regularizer != None:
            tf.add_to_collection('losses', regularizer(fc1_weights))
        fc1_biases = tf.get_variable("bias", [FC_SIZE], initializer=tf.constant_initializer(0.1))

        fc1 = tf.nn.relu(tf.matmul(reshaped, fc1_weights) + fc1_biases)
        if train: fc1 = tf.nn.dropout(fc1, 0.5)

    # 声明第六层全连接层的变量并实现前向传播过程。这一层的输入为一组长度为512的向量,
    # 输出为一组长度为10的向量。这一层的输出通过Softmax之后就得到了最后的分类结果。
    with tf.variable_scope('layer6-fc2'):
        fc2_weights = tf.get_variable("weight", [FC_SIZE, NUM_LABELS],
                                      initializer=tf.truncated_normal_initializer(stddev=0.1))
        if regularizer != None: tf.add_to_collection('losses', regularizer(fc2_weights))
        fc2_biases = tf.get_variable("bias", [NUM_LABELS], initializer=tf.constant_initializer(0.1))
        logit = tf.matmul(fc1, fc2_weights) + fc2_biases

    # 返回第六层的输出
    return logit

运行修改后的mnist_train.py,可以得到类似上一章中的输出:


运行mnist_train.py程序的输出

类似地修改上一章中给出的mnist_eval.py程序输入部分,就可以测试整个卷积神经网络在MNIST数据集上的正确率了。在MNIST测试数据上,上面给出的卷积神经网络可以达到99.4%的正确率。相比上一章中最高的98.4%的正确率,卷积神经网络可以巨幅提高神经网络在MNIST数据集上的正确率。
然而一种卷积神经网络架构不能解决所有问题。比如LeNet-5模型就无法很好地处理类似ImageNet这样比较大的图向数据集。那么如何设计卷积神经网络的架构呢?下面的正则表达式公式总结了一些经典的用于图片分类问题的卷积神经网络架构:
然而一种卷积神经网络不能解决所有问题。比如LeNet-5模型就无法很好地处理类似ImageNet这样比较大的图向数据集。那么如何设计卷积神经网络的架构呢?下面的正则表达式公式总结了一些经典的用于图片分类问题的卷积神经网络架构:
输入层——>(卷积层+——>池化层?)+——>全连接层+
在上面的公式中,“卷积层+”表示一层或者多层卷积层。大部分卷积神经网络中一般最多连续使用三层卷积层。“池化层?”表示没有或者一层池化层。池化层虽然可以起到减少参数防止过拟合问题,但是在部分论文中也发现可以直接通过调整卷积层步长来完成。所以有些卷积神经网络中没有池化层。在多轮卷积层和池化层之后,卷积神经网络在输出前一般会经过1~2个全连接层。比如LeNet-5模型就可以表示为下面的结构。
输入层 ——>卷积层 ——>池化层 ——>卷积层 ——>池化层 ——>全连接层 ——>全连接层 ——>输出层
除了LeNet-5模型,2012年ImageNet ILSVRC图像分类挑战的第一名AlexNet模型、2013年ILSVRC第一名ZF Net模型以及2014年第二名VGGNet模型的架构都满足上面介绍的正则表达式。下表给出了作者尝试过的不同卷积神经网络架构:

image.png

其中conv表示卷积层,maxpool表示池化层,FC-表示全连接层,soft-max为softmax结构
从上表可以看出这些卷积神经网络架构都满足介绍的正则表达式。
有了卷积神经网络架构,那么每一层卷积层或者池化层中的配置需要如何设置呢?上表也提供了很多线索。在上表中,convX-Y表示过滤器的边长为X,深度为Y。比如conv3-64表示过滤器的长和宽都为3,深度为64.从表中可以看出,VGG Net中的过滤器边长一般为3或者1. 在LeNet-5模型中,也使用了边长为5的过滤器。一般卷积层的过滤器边长不会超过5,但有些卷积神经网络结构中,处理输入的卷积层中使用了边长我7甚至是11的过滤器。
在过滤器的深度上,大部分卷积神经网络都采用逐层递增的方式。比如在表中可以看到,每经过一次池化层之后,卷积层过滤器的深度会乘以2.虽然不同的模型会选择使用不同的数字,但是逐层递增是比较普遍的模式。卷积层的步长一般为1,但是在有些模型中也会使用2,或者3作为步长。池化层的配置相对简单,用的最多的是最大池化层。池化层的过滤器边长一般为2或者3,步长也一般为2或者3.

4.2.INception-v3模型

在上一小节中通过介绍LeNet-5模型整理出了一类经典的卷积神经网络架构设计。在这一小节中将介绍Inception结构以及Inception-v3卷积神经网络模型。Inception结构是一种和LeNet-5结构完全不同的卷积神经网络结构。在LeNet-5模型中,不同卷积层通过串联的方式连接在一起,而Inception-v3模型中的Inception结构是将不同的卷积层通过并联的方式结合在一起,而Inception-v3模型中的Inception结构是将不同的卷积层通过并联的方式结合在一起。在下面的篇幅中将具体介绍Inception结构,并通过TensorFlow-Slim工具来实现Inception-v3模型中的一个模块。
上一小节提到了一个卷积层可以使用边长为1、3或者5的过滤器,那么如何在这些边长中选呢?Inception模块给出了一个方案,那就是同时使用所有不同尺寸的过滤器,然后再将得到的矩阵拼接起来。下图给出了Inception模块的一个单元结构示意图:


Inception模块示意图

从上图可以看出,Inception模块会首先使用不同尺寸的过滤器处理输入矩阵。在图中,最上方矩阵为使用了边长为1的过滤器的卷积层前向传播的结果。类似的,中间矩阵使用的过滤器边长为3,下方矩阵使用的过滤器边长为5.不同的矩阵代表了Inception模块中的一条计算路径。虽然过滤器的大小不同,但如果所有的过滤器都使用了全0填充且步长为1,那么前向传播得到的结果矩阵的长和宽都与输入矩阵一致。这样经过不同过滤器处理的结果矩阵可以拼接成一个更深的矩阵。如上图所示,可以将它们在深度这个维度上组合起来。
上图所示的Inception模块得到的结果矩阵的长和宽与输入一样,深度为红黄蓝三个矩阵深度的和。图中展示的是Inception模块的核心思想,真正在Inception-v3模型中使用的Inception模块要更加复杂且多样。下图给出了Inception-v3模型的架构图:


Inception-v3模型架构图

Inception-v3模型总共有46层,由11个Inception模块组成。图中方框标注出来的结构就是一个Inception模块。在Inception-v3模型中有96个卷积层,如果将上一小节的程序直接搬过来,那么一个卷积层就需要5行代码,于是总共需要480行代码来实现所有的卷积层。这样使得代码的可读性非常差。为了更好地实现类似Inception-v3模型这样的复杂卷积神经网络,在下面将先介绍TensorFlow-Slim工具来更加简洁地实现一个卷积层。以下代码对比了直接使用TensorFlow实现一个卷积层和TensorFlow-Slim实现同样结构的神经网络的代码量:
# 直接使用TensorFlow原始API实现卷积层
with tf.variable_score(scope_name):
    weights = tf.get_variable("weight",...)
    biases = tf.get_variable("bias",...)
    conv = tf.nn.conv2d(...)
relu = tf.nn.relu(tf.nn.bias_add(conv,biases))
# 使用TensorFlow-Slim实现卷积层,通过TensorFlow-Slim可以在一行中实现一个卷积层的前向传播算法。slim.conv2d
# 函数的有3个参数是必填的。第一个参数为输入节点矩阵,第二个参数是当前卷积层过滤器的深度,第三个参数是
# 过滤器的尺寸。可选的参数有过滤器移动的步长、是否使用全0填充、激活函数的选择以及变量的命名空间等。
net = slim.conv2d(input,32,[3,3])

因为完整的Inception-v3模型比较长,所以在本书中仅提供Inception-v3模型中结构相对复杂的一个Inception模块的代码实现。以下代码实现了图中黑色方框中的Inception模块:

# slim.arg_scope函数可以用于设置默认的参数取值。slim.arg_scope函数的第一个参数是
# 一个函数列表,在这个列表中的函数将使用默认的参数取值。比如通过下面的定义,调用
# slim.conv2d(net,320,[1,1])函数时会自动加上stride=1和padding='SAME'的参数。如果
# 在函数调用时指定了stride,那么这里设置的默认值就不会再使用。通过这种方式可以进一步
# 减少冗余的代码。
with slim.arg_scope([slim.conv2d, slim.max_pool2d,slim.avg_pool2d], stride=1, padding='SAME'):
    ...
    # 此处省略了Inception-v3模型中其他的网络结构而直接实现最后面红色方框中的。Inception结构。
    # 假设输入图片经过之前的神经网络前向传播的结果保存在变量net中。
    net = 上一层的输出节点矩阵
    # 为一个Inception模块声明一个统计的变量命名空间。
    with tf.variable_scope('Brach_0'):
        #实现一个过滤器边长为1,深度为320的卷积层。
        branch_0 = slim.conv2d(net,320,[1,1],scope='Conv2d_0a_1*1')
    # Inception模块中第二条路径。这条计算路径上的结构本身也是一个Inception结构。
    with tf.variable_scope('Branch_1'):
        branch_1 = slim.conv2d(net,384,[1,1],scope='Conv2d_0a_1*1')
        # tf.concat函数可以将多个矩阵拼接起来。tf.concat函数的第一个参数指定了拼接的维度,这里
        # 给出的“3”代表了矩阵是在深度这个维度上进行拼接。上图展示了在深度上拼接矩阵的方式。
        branch_1 = tf.concat(3,[
            # 如上面的图中,此处2层卷积层的输入都是batch_1而不是net
            slim.conv2d(branch_1,384,[1,3],scope='Conv2d_0b_1*3')
            slim.conv2d(branch_1,384,[3,1],scope='Conv2d_0c_3*1')
        ])
    # Inception模块中第三条路径。刺计算路径也是一个Inception结构
    with tf.varibale_scope('Branch_2'):
        branch_2 = slim.conv2d(
            net, 448, [1,1], scope='Conv2d_0a_1*1')
        branch_2 = slim.conv2d(
            branch_2,384,[3,3],scope='Conv2d_0b_3*3')
        branch_2 = tf.concat(3,[
            slim.conv2d(branch_2, 384,[1,3],scope='Conv2d_0c_1*3'),
            slim.conv2d(branch_2, 384,[3,1],scope='Conv2d_od_3*1')])
    
    # Inception模块中第四条路径
    with tf.variable_scope('Branch_3'):
        branch_3 = slim.avg_pool2d(
            net, [3,3], scope='AvgPool_0a_3*3')
        branch_3 = slim.conv2d(
            branch_3, 192, [1,1], scope='Conv2d_0b_1*1')
    
    # 当前Inception模块的最后输出是由上面四个计算结果拼接得到的
    net = tf.concat(3, [branch_0, branch_1, branch_2, branch_3])

5.卷积神经网络迁移学习

在本节中将介绍迁移学习的概念以及如何通过TensorFlow来实现迁移学习。
1.讲解迁移学习的动机
2.介绍如何将一个数据集上训练好的卷积神经网络快速转移到另外一个数据集上。
3.将给出一个具体的TensorFlow程序将ImageNet上训练好的Inception-v3模型转移到另外一个图像分类数据集上。

5.1迁移学习介绍

在上一节中介绍了LeNet-5模型和Inception-v3模型。对比这两个模型可以发现,卷积神经网络模型的层数和复杂度都发生了巨大的变化。下表给出了从2012年到2015年ILSVRC(Large Scale Visual Recognition Challenge)第一名模型的层数以及前五个答案的错误率。


ILSVRC第一名模型信息列表

从表中可以看到,随着模型层数及复杂度的增加,模型在ImageNet上的错误率也随之降低。然而,训练复杂的卷积神经网络需要非常多的标注数据。如第一节提到的,ImageNet图像分类数据集中有120万标注图片,所以才能将152层的ResNet的模型训练到大约96.5%的正确率。在真实的应用中,很难收集到如此多的的标注数据。即使可以收集到,也需要花费大量人力物力。而且即使有海量的训练数据,要训练一个复杂的卷积神经网络也需要几天甚至几周的时间。为了解决标注性数据和训练时间的问题,可以使用本节将要介绍的迁移学习。
所谓迁移学习,就是将一个问题上训练好的模型通过简单的调整使其适用于一个新的问题。本小节将介绍如何利用ImageNet数据集上训练好的Inception-v3模型来解决一个新的图像分类问题。根据论文DeCAF:ADeep Convolutional Activation Feature For Generic Visual Recongnition中的结论,可以保留训练好的Inception-v3模型中所有卷积层的参数,只是替换最后一层全连接层。在最后这一层全连接层之前的网络称之为瓶颈层。
将新的图像通过训练好的卷积神经网络直到瓶颈层的过程可以看成是对图像进行特征提取的过程。在训练好的Inception-v3模型中,因为将瓶颈层的输出再通过一个单层的全连接层神经网络可以很好地区分1000种类别的图像,所以有理由认为瓶颈层输出的节点向量可以直接利用这个训练好的神经网络对图像进行特征提取,然后再将提取得到的特征向量作为输入来训练一个新的单层全连接神经网络处理新的分类问题。
一般来说,在数据量足够的情况下,迁移学习的效果不如完全重新训练,但是迁移学习所需要的训练时间和训练样本要远远小于训练完整的模型。在没有GPU的普通台式机或者笔记本电脑上,下一小节中给出的TensorFlow训练过程只需要大约5分钟,而且可以达到概率90%的正确率。

5.2TensorFlow实现迁移学习

本小节将给出一个完整的TensorFlow程序来介绍如何通过TensorFlow来实现迁移学习。以下代码给出了如何下载这一小节将要用到的数据集。

tar xzf flower_photos.tgz

解压之后的文件夹包含了5个子文件夹,每一个子文件夹的名称为一种花的名称,代表了不同的类别。平均每一种花有734张图片,每一张图片都是RGB色彩模式的,大小也不相同。和之前的样例不同,在这一小节中给出的程序将直接处理没有整理过的图像数据。同时,通过下面的命名可以下载谷歌提供的训练好的Inception-v3模型。
wget https://storage.googoleapis.com/download.tensorflow.org/models/inception_dec_2015.zip
uzip tensorflow/examples/label_image/data/inception_dec_2015.zip
当新的数据集和已经训练好的模型都准备好之后,可以通过以下代码来完成迁移学习的过程:

  1. 定义需要使用到的常量
import glob
import os.path
import random
import numpy as np
import tensorflow as tf
from tensorflow.python.platform import gfile

2.模型和样本路径的设置

BOTTLENECK_TENSOR_SIZE = 2048
BOTTLENECK_TENSOR_NAME = 'pool_3/_reshape:0'
JPEG_DATA_TENSOR_NAME = 'DecodeJpeg/contents:0'


MODEL_DIR = '../../datasets/inception_dec_2015'
MODEL_FILE= 'tensorflow_inception_graph.pb'

CACHE_DIR = '../../datasets/bottleneck'
INPUT_DATA = '../../datasets/flower_photos'

VALIDATION_PERCENTAGE = 10
TEST_PERCENTAGE = 10

3.神经网络参数的设置

LEARNING_RATE = 0.01
STEPS = 4000
BATCH = 100

4.把样本中所有的图片列表并按训练、验证、测试数据分开

def create_image_lists(testing_percentage, validation_percentage):

    result = {}
    sub_dirs = [x[0] for x in os.walk(INPUT_DATA)]
    is_root_dir = True
    for sub_dir in sub_dirs:
        if is_root_dir:
            is_root_dir = False
            continue

        extensions = ['jpg', 'jpeg', 'JPG', 'JPEG']

        file_list = []
        dir_name = os.path.basename(sub_dir)
        for extension in extensions:
            file_glob = os.path.join(INPUT_DATA, dir_name, '*.' + extension)
            file_list.extend(glob.glob(file_glob))
        if not file_list: continue

        label_name = dir_name.lower()
        
        # 初始化
        training_images = []
        testing_images = []
        validation_images = []
        for file_name in file_list:
            base_name = os.path.basename(file_name)
            
            # 随机划分数据
            chance = np.random.randint(100)
            if chance < validation_percentage:
                validation_images.append(base_name)
            elif chance < (testing_percentage + validation_percentage):
                testing_images.append(base_name)
            else:
                training_images.append(base_name)

        result[label_name] = {
            'dir': dir_name,
            'training': training_images,
            'testing': testing_images,
            'validation': validation_images,
            }
    return result

5.定义函数通过类别名称、所属数据集和图片编号获取一张图片的地址

def get_image_path(image_lists, image_dir, label_name, index, category):
    label_lists = image_lists[label_name]
    category_list = label_lists[category]
    mod_index = index % len(category_list)
    base_name = category_list[mod_index]
    sub_dir = label_lists['dir']
    full_path = os.path.join(image_dir, sub_dir, base_name)
    return full_path

6.定义函数获取Inception-v3模型处理之后的特征向量的文件地址

def get_bottleneck_path(image_lists, label_name, index, category):
    return get_image_path(image_lists, CACHE_DIR, label_name, index, category) + '.txt'

7.定义函数使用加载的训练好的Inception-v3模型处理一张图片,得到这个图片的特征向量

def run_bottleneck_on_image(sess, image_data, image_data_tensor, bottleneck_tensor):

    bottleneck_values = sess.run(bottleneck_tensor, {image_data_tensor: image_data})

    bottleneck_values = np.squeeze(bottleneck_values)
    return bottleneck_values

8.定义函数会先试图寻找已经计算且保存下来的特征向量,如果找不到则先计算这个特征向量,然后保存到文件

def get_or_create_bottleneck(sess, image_lists, label_name, index, category, jpeg_data_tensor, bottleneck_tensor):
    label_lists = image_lists[label_name]
    sub_dir = label_lists['dir']
    sub_dir_path = os.path.join(CACHE_DIR, sub_dir)
    if not os.path.exists(sub_dir_path): os.makedirs(sub_dir_path)
    bottleneck_path = get_bottleneck_path(image_lists, label_name, index, category)
    if not os.path.exists(bottleneck_path):

        image_path = get_image_path(image_lists, INPUT_DATA, label_name, index, category)

        image_data = gfile.FastGFile(image_path, 'rb').read()

        bottleneck_values = run_bottleneck_on_image(sess, image_data, jpeg_data_tensor, bottleneck_tensor)

        bottleneck_string = ','.join(str(x) for x in bottleneck_values)
        with open(bottleneck_path, 'w') as bottleneck_file:
            bottleneck_file.write(bottleneck_string)
    else:

        with open(bottleneck_path, 'r') as bottleneck_file:
            bottleneck_string = bottleneck_file.read()
        bottleneck_values = [float(x) for x in bottleneck_string.split(',')]

    return bottleneck_values

9.这个函数随机获取一个batch的图片作为训练数据。

def get_random_cached_bottlenecks(sess, n_classes, image_lists, how_many, category, jpeg_data_tensor, bottleneck_tensor):
    bottlenecks = []
    ground_truths = []
    for _ in range(how_many):
        label_index = random.randrange(n_classes)
        label_name = list(image_lists.keys())[label_index]
        image_index = random.randrange(65536)
        bottleneck = get_or_create_bottleneck(
            sess, image_lists, label_name, image_index, category, jpeg_data_tensor, bottleneck_tensor)
        ground_truth = np.zeros(n_classes, dtype=np.float32)
        ground_truth[label_index] = 1.0
        bottlenecks.append(bottleneck)
        ground_truths.append(ground_truth)

    return bottlenecks, ground_truths

10.这个函数获取全部的测试数据,并计算正确率

def get_test_bottlenecks(sess, image_lists, n_classes, jpeg_data_tensor, bottleneck_tensor):
    bottlenecks = []
    ground_truths = []
    label_name_list = list(image_lists.keys())
    for label_index, label_name in enumerate(label_name_list):
        category = 'testing'
        for index, unused_base_name in enumerate(image_lists[label_name][category]):
            bottleneck = get_or_create_bottleneck(sess, image_lists, label_name, index, category,jpeg_data_tensor, bottleneck_tensor)
            ground_truth = np.zeros(n_classes, dtype=np.float32)
            ground_truth[label_index] = 1.0
            bottlenecks.append(bottleneck)
            ground_truths.append(ground_truth)
    return bottlenecks, ground_truths

11.定义主函数。

def main():
    image_lists = create_image_lists(TEST_PERCENTAGE, VALIDATION_PERCENTAGE)
    n_classes = len(image_lists.keys())
    
    # 读取已经训练好的Inception-v3模型。
    with gfile.FastGFile(os.path.join(MODEL_DIR, MODEL_FILE), 'rb') as f:
        graph_def = tf.GraphDef()
        graph_def.ParseFromString(f.read())
    bottleneck_tensor, jpeg_data_tensor = tf.import_graph_def(
        graph_def, return_elements=[BOTTLENECK_TENSOR_NAME, JPEG_DATA_TENSOR_NAME])

    # 定义新的神经网络输入
    bottleneck_input = tf.placeholder(tf.float32, [None, BOTTLENECK_TENSOR_SIZE], name='BottleneckInputPlaceholder')
    ground_truth_input = tf.placeholder(tf.float32, [None, n_classes], name='GroundTruthInput')
    
    # 定义一层全链接层
    with tf.name_scope('final_training_ops'):
        weights = tf.Variable(tf.truncated_normal([BOTTLENECK_TENSOR_SIZE, n_classes], stddev=0.001))
        biases = tf.Variable(tf.zeros([n_classes]))
        logits = tf.matmul(bottleneck_input, weights) + biases
        final_tensor = tf.nn.softmax(logits)
        
    # 定义交叉熵损失函数。
    cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=ground_truth_input)
    cross_entropy_mean = tf.reduce_mean(cross_entropy)
    train_step = tf.train.GradientDescentOptimizer(LEARNING_RATE).minimize(cross_entropy_mean)
    
    # 计算正确率。
    with tf.name_scope('evaluation'):
        correct_prediction = tf.equal(tf.argmax(final_tensor, 1), tf.argmax(ground_truth_input, 1))
        evaluation_step = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

    with tf.Session() as sess:
        init = tf.global_variables_initializer()
        sess.run(init)
        # 训练过程。
        for i in range(STEPS):
 
            train_bottlenecks, train_ground_truth = get_random_cached_bottlenecks(
                sess, n_classes, image_lists, BATCH, 'training', jpeg_data_tensor, bottleneck_tensor)
            sess.run(train_step, feed_dict={bottleneck_input: train_bottlenecks, ground_truth_input: train_ground_truth})

            if i % 100 == 0 or i + 1 == STEPS:
                validation_bottlenecks, validation_ground_truth = get_random_cached_bottlenecks(
                    sess, n_classes, image_lists, BATCH, 'validation', jpeg_data_tensor, bottleneck_tensor)
                validation_accuracy = sess.run(evaluation_step, feed_dict={
                    bottleneck_input: validation_bottlenecks, ground_truth_input: validation_ground_truth})
                print('Step %d: Validation accuracy on random sampled %d examples = %.1f%%' %
                    (i, BATCH, validation_accuracy * 100))
            
        # 在最后的测试数据上测试正确率。
        test_bottlenecks, test_ground_truth = get_test_bottlenecks(
            sess, image_lists, n_classes, jpeg_data_tensor, bottleneck_tensor)
        test_accuracy = sess.run(evaluation_step, feed_dict={
            bottleneck_input: test_bottlenecks, ground_truth_input: test_ground_truth})
        print('Final test accuracy = %.1f%%' % (test_accuracy * 100))

if __name__ == '__main__':
    main()

运行主函数,得到如下结果:
Step 0: Validation accuracy on random sampled 100 examples = 47.0%
Step 100: Validation accuracy on random sampled 100 examples = 83.0%
Step 200: Validation accuracy on random sampled 100 examples = 88.0%
Step 300: Validation accuracy on random sampled 100 examples = 86.0%
Step 400: Validation accuracy on random sampled 100 examples = 89.0%
Step 500: Validation accuracy on random sampled 100 examples = 84.0%
Step 600: Validation accuracy on random sampled 100 examples = 91.0%
Step 700: Validation accuracy on random sampled 100 examples = 90.0%
Step 800: Validation accuracy on random sampled 100 examples = 92.0%
Step 900: Validation accuracy on random sampled 100 examples = 84.0%
Step 1000: Validation accuracy on random sampled 100 examples = 88.0%
Step 1100: Validation accuracy on random sampled 100 examples = 92.0%
Step 1200: Validation accuracy on random sampled 100 examples = 90.0%
Step 1300: Validation accuracy on random sampled 100 examples = 86.0%
Step 1400: Validation accuracy on random sampled 100 examples = 91.0%
Step 1500: Validation accuracy on random sampled 100 examples = 90.0%
Step 1600: Validation accuracy on random sampled 100 examples = 89.0%
Step 1700: Validation accuracy on random sampled 100 examples = 92.0%
Step 1800: Validation accuracy on random sampled 100 examples = 96.0%
Step 1900: Validation accuracy on random sampled 100 examples = 91.0%
Step 2000: Validation accuracy on random sampled 100 examples = 85.0%
Step 2100: Validation accuracy on random sampled 100 examples = 87.0%
Step 2200: Validation accuracy on random sampled 100 examples = 92.0%
Step 2300: Validation accuracy on random sampled 100 examples = 93.0%
Step 2400: Validation accuracy on random sampled 100 examples = 91.0%
Step 2500: Validation accuracy on random sampled 100 examples = 94.0%
Step 2600: Validation accuracy on random sampled 100 examples = 90.0%
Step 2700: Validation accuracy on random sampled 100 examples = 94.0%
Step 2800: Validation accuracy on random sampled 100 examples = 90.0%
Step 2900: Validation accuracy on random sampled 100 examples = 88.0%
Step 3000: Validation accuracy on random sampled 100 examples = 95.0%
Step 3100: Validation accuracy on random sampled 100 examples = 94.0%
Step 3200: Validation accuracy on random sampled 100 examples = 91.0%
Step 3300: Validation accuracy on random sampled 100 examples = 98.0%
Step 3400: Validation accuracy on random sampled 100 examples = 96.0%
Step 3500: Validation accuracy on random sampled 100 examples = 88.0%
Step 3600: Validation accuracy on random sampled 100 examples = 90.0%
Step 3700: Validation accuracy on random sampled 100 examples = 96.0%
Step 3800: Validation accuracy on random sampled 100 examples = 92.0%
Step 3900: Validation accuracy on random sampled 100 examples = 93.0%
Step 3999: Validation accuracy on random sampled 100 examples = 90.0%
Final test accuracy = 92.2%
从上面的结果可以看到,模型在新的数据集上很快能够收敛,并达到不错的分类效果。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343