卷积神经网络实践指南-全-
卷积神经网络实践指南(全)
原文:
annas-archive.org/md5/b8bd6c69e42dba1814f4d08d925abff3译者:飞龙
前言
卷积神经网络(CNN)正在彻底改变多个应用领域,如视觉识别系统、自动驾驶汽车、医学发现、创新电子商务等。本书将带你从 CNN 的基本构建块开始,同时引导你通过最佳实践来实现实际的 CNN 模型和解决方案。你将学习如何为图像和视频分析创造创新的解决方案,以解决复杂的机器学习和计算机视觉问题。
本书从深度神经网络的概述开始,举了一个图像分类的例子,并引导你构建第一个 CNN 模型。你将学习到迁移学习和自编码器等概念,这些概念将帮助你构建非常强大的模型,即使是使用有限的监督(标注图像)训练数据。
后续我们将在这些学习的基础上,构建更高级的与视觉相关的算法和解决方案,用于目标检测、实例分割、生成(对抗)网络、图像描述、注意力机制以及视觉中的递归注意力模型。
除了让你获得与最具挑战性的视觉模型和架构的实践经验外,本书还深入探讨了卷积神经网络和计算机视觉领域的前沿研究。这样,读者可以预测该领域的未来,并通过先进的 CNN 解决方案快速启动创新之旅。
本书结束时,你应该能够在专业项目或个人计划中实施先进、有效且高效的 CNN 模型,尤其是在处理复杂的图像和视频数据集时。
本书适合人群
本书适用于数据科学家、机器学习与深度学习从业者,以及认知与人工智能爱好者,他们希望在构建 CNN 模型上更进一步。通过对极大数据集和不同 CNN 架构的实践,你将构建高效且智能的卷积神经网络模型。本书假设你已有一定的深度学习概念基础和 Python 编程语言知识。
本书内容概述
第一章,深度神经网络概述,快速回顾了深度神经网络的科学原理和不同的框架,这些框架可以用来实现这些网络,并介绍了它们背后的数学原理。
第二章,卷积神经网络简介,它将向读者介绍卷积神经网络,并展示如何利用深度学习从图像中提取见解。
第三章,构建第一个 CNN 并进行性能优化,从零开始构建一个简单的 CNN 图像分类模型,并解释如何调整超参数、优化训练时间和提升 CNN 的效率与准确性。
第四章,流行的 CNN 模型架构,展示了不同流行(并获奖)CNN 架构的优势和工作原理,分析它们的差异,并讲解如何使用它们。
第五章,迁移学习,教你如何使用已有的预训练网络,并将其适应到新的数据集上。书中还有一个使用名为迁移学习的技术来解决实际应用的自定义分类问题。
第六章,卷积神经网络的自编码器,介绍了一种名为自编码器的无监督学习技术。我们将通过不同的自编码器应用案例来讲解其在 CNN 中的应用,例如图像压缩。
第七章,使用 CNN 进行目标检测与实例分割,讲解了目标检测、实例分割和图像分类之间的区别。接着,我们学习了使用 CNN 进行目标检测与实例分割的多种技术。
第八章,生成对抗网络(GAN)—使用 CNN 生成新图像,探索了生成性 CNN 网络,并将它们与我们学习到的判别性 CNN 网络结合,利用 CNN/GAN 生成新图像。
第九章,CNN 和视觉模型中的注意力机制,讲解了深度学习中注意力机制的直觉,并学习了基于注意力的模型如何用于实现一些高级解决方案(如图像描述和 RAM)。我们还理解了不同类型的注意力及强化学习在硬注意力机制中的作用。
最大化利用本书
本书专注于使用 Python 编程语言构建 CNN。我们使用 Python 2.7 版本(2x)构建了多种应用程序和开源、企业级专业软件,使用的工具包括 Python、Spyder、Anaconda 和 PyCharm。许多示例也兼容 Python 3x。作为良好的实践,我们鼓励用户在实现这些代码时使用 Python 虚拟环境。
我们专注于如何最好地利用各种 Python 和深度学习库(Keras、TensorFlow 和 Caffe)来构建真实世界的应用程序。在这方面,我们尽力使所有代码尽可能友好和易读。我们认为,这将帮助读者轻松理解代码,并在不同的场景中灵活运用。
下载示例代码文件
你可以从你的账户下载本书的示例代码文件,网址为 www.packtpub.com。如果你在其他地方购买了本书,可以访问 www.packtpub.com/support 并注册,以便直接通过邮件获得文件。
你可以按照以下步骤下载代码文件:
-
登录或注册,访问 www.packtpub.com。
-
选择“支持”标签。
-
点击“代码下载与勘误”。
-
在搜索框中输入书名,并按照屏幕上的指示操作。
一旦文件下载完成,请确保使用最新版本的工具来解压或提取文件夹:
-
WinRAR/7-Zip for Windows
-
Zipeg/iZip/UnRarX for Mac
-
7-Zip/PeaZip for Linux
本书的代码包也托管在 GitHub 上,地址为github.com/PacktPublishing/Practical-Convolutional-Neural-Networks。如果代码有更新,GitHub 上的现有仓库将会进行更新。
我们还有其他的代码包,来自我们丰富的书籍和视频目录,您可以在github.com/PacktPublishing/找到它们。快来看看吧!
下载彩色图像
我们还提供了一份 PDF 文件,其中包含了本书中使用的截图/图表的彩色图像。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/PracticalConvolutionalNeuralNetworks_ColorImages.pdf。
使用的约定
本书中使用了许多文本约定。
CodeInText:表示文本中的代码字、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号。例如:"将下载的WebStorm-10*.dmg磁盘映像文件挂载为您系统中的另一个磁盘。"
代码块如下所示:
import tensorflow as tf
#Creating TensorFlow object
hello_constant = tf.constant('Hello World!', name = 'hello_constant')
#Creating a session object for execution of the computational graph
with tf.Session() as sess:
当我们希望您关注代码块中的某一部分时,相关行或项目会以粗体显示:
x = tf.subtract(1, 2,name=None) # -1
y = tf.multiply(2, 5,name=None) # 10
粗体:表示新术语、重要词汇或您在屏幕上看到的词汇。例如,菜单或对话框中的词汇将在文本中以此方式显示。举个例子:“从管理面板中选择系统信息。”
警告或重要提示将以此格式出现。
提示和技巧将以此格式显示。
联系我们
我们始终欢迎读者的反馈。
一般反馈:通过电子邮件feedback@packtpub.com联系我们,并在邮件主题中注明书名。如果您对本书的任何部分有疑问,请通过questions@packtpub.com与我们联系。
勘误:虽然我们已尽全力确保内容的准确性,但错误仍然可能发生。如果您在本书中发现任何错误,我们将非常感激您能向我们报告。请访问www.packtpub.com/submit-errata,选择您的书籍,点击“勘误提交表单”链接,并输入详细信息。
盗版:如果您在互联网上发现我们的任何作品的非法副本,请提供相关的地址或网站名称。请通过copyright@packtpub.com联系我们,并附上该资料的链接。
如果你有兴趣成为作者:如果你在某个领域具有专业知识,并且有兴趣撰写或参与编写书籍,请访问authors.packtpub.com。
评价
请留下评价。在你阅读并使用本书后,为什么不在你购买该书的网站上留下评价呢?潜在读者可以通过你的公正意见做出购买决策,我们在 Packt 也能了解你对我们产品的看法,而我们的作者也能看到你对其书籍的反馈。谢谢!
欲了解更多关于 Packt 的信息,请访问packtpub.com。
第一章:深度神经网络 - 概述
在过去的几年中,我们在人工智能(深度学习)领域取得了显著进展。今天,深度学习是许多先进技术应用的基石,从自动驾驶汽车到艺术和音乐创作。科学家们旨在帮助计算机不仅能理解语音,还能用自然语言进行对话。深度学习是一种基于数据表示学习的方法,而不是任务特定的算法。深度学习使计算机能够从更简单、更小的概念构建复杂的概念。例如,深度学习系统通过将较低层次的边缘和角点组合起来,并以分层方式将它们组合成身体部位,从而识别一个人的图像。那一天已经不远了,深度学习将扩展到能够让机器独立思考的应用中。
在本章中,我们将涵盖以下主题:
-
神经网络的构建模块
-
TensorFlow 简介
-
Keras 简介
-
反向传播
神经网络的构建模块
神经网络由许多人工神经元组成。它是大脑的表现形式,还是某些知识的数学表示呢?在这里,我们将简单地尝试理解神经网络在实践中的应用。卷积神经网络(CNN)是一种非常特殊的多层神经网络。CNN 旨在直接从图像中识别视觉模式,且处理过程最小。该网络的图示如下所示。神经网络领域最初受到建模生物神经系统目标的启发,但此后它已朝着不同方向发展,并成为机器学习任务中工程学和获得良好结果的关键。
人工神经元是一个接受输入并产生输出的函数。所使用的神经元数量取决于当前任务的需求。它可能少至两个神经元,也可能多达几千个神经元。连接人工神经元以创建卷积神经网络(CNN)的方式有很多种。常用的拓扑结构之一是前馈网络:

每个神经元从其他神经元接收输入。每条输入线对神经元的影响由权重控制。权重可以是正数也可以是负数。整个神经网络通过理解语言来学习执行有用的计算以识别物体。现在,我们可以将这些神经元连接成一个被称为前馈网络的网络。这意味着每一层的神经元将它们的输出传递到下一层,直到得到最终输出。可以写成如下形式:


上述前向传播的神经元可以通过以下方式实现:
import numpy as np
import math
class Neuron(object):
def __init__(self):
self.weights = np.array([1.0, 2.0])
self.bias = 0.0
def forward(self, inputs):
""" Assuming that inputs and weights are 1-D numpy arrays and the bias is a number """
a_cell_sum = np.sum(inputs * self.weights) + self.bias
result = 1.0 / (1.0 + math.exp(-a_cell_sum)) # This is the sigmoid activation function
return result
neuron = Neuron()
output = neuron.forward(np.array([1,1]))
print(output)
TensorFlow 简介
TensorFlow 基于图计算。以以下数学表达式为例:
c=(a+b), d = b + 5,
*e = c * d *
在 TensorFlow 中,这表示为计算图,如下所示。之所以强大,是因为计算可以并行执行:

安装 TensorFlow
有两种简单的方法可以安装 TensorFlow:
-
使用虚拟环境(推荐并在此处描述)
-
使用 Docker 镜像
对于 macOS X/Linux 变体
以下代码片段创建一个 Python 虚拟环境,并在该环境中安装 TensorFlow。运行此代码之前,您应先安装 Anaconda:
#Creates a virtual environment named "tensorflow_env" assuming that python 3.7 version is already installed.
conda create -n tensorflow_env python=3.7
#Activate points to the environment named "tensorflow"
source activate tensorflow_env
conda install pandas matplotlib jupyter notebook scipy scikit-learn
#installs latest tensorflow version into environment tensorflow_env
pip3 install tensorflow
请查看官方 TensorFlow 页面上的最新更新,www.tensorflow.org/install/。
尝试在 Python 控制台中运行以下代码以验证您的安装。如果 TensorFlow 已安装并正常工作,控制台应打印 Hello World!:
import tensorflow as tf
#Creating TensorFlow object
hello_constant = tf.constant('Hello World!', name = 'hello_constant')
#Creating a session object for execution of the computational graph
with tf.Session() as sess:
#Implementing the tf.constant operation in the session
output = sess.run(hello_constant)
print(output)
TensorFlow 基础
在 TensorFlow 中,数据不是以整数、浮点数、字符串或其他基本类型存储的。这些值被封装在一个名为 张量 的对象中。它由一组基本值构成,这些值被塑形为任意维度的数组。张量的维度数量称为其 秩。在前面的示例中,hello_constant 是一个秩为零的常量字符串张量。以下是一些常量张量的示例:
# A is an int32 tensor with rank = 0
A = tf.constant(123)
# B is an int32 tensor with dimension of 1 ( rank = 1 )
B = tf.constant([123,456,789])
# C is an int32 2- dimensional tensor
C = tf.constant([ [123,456,789], [222,333,444] ])
TensorFlow 的核心程序基于计算图的概念。计算图是由以下两部分组成的有向图:
-
构建计算图
-
运行计算图
计算图在 会话 中执行。TensorFlow 会话是计算图的运行时环境。它分配 CPU 或 GPU,并保持 TensorFlow 运行时的状态。以下代码使用 tf.Session 创建一个名为 sess 的会话实例。然后,sess.run() 函数计算张量并返回存储在 output 变量中的结果。最后输出 Hello World!:
with tf.Session() as sess:
# Run the tf.constant operation in the session
output = sess.run(hello_constant)
print(output)
使用 TensorBoard,我们可以可视化计算图。要运行 TensorBoard,请使用以下命令:
tensorboard --logdir=path/to/log-directory
让我们创建一段简单的加法代码,如下所示。创建一个常量整数 x,其值为 5,然后将 5 加到一个新变量 y 中并打印结果:
constant_x = tf.constant(5, name='constant_x')
variable_y = tf.Variable(x + 5, name='variable_y')
print (variable_y)
区别在于,variable_y并没有像 Python 代码中那样给出x + 5的当前值。它实际上是一个方程式;也就是说,当计算variable_y时,会取x在那个时间点的值,并加上5。在前面的代码中,variable_y的值并没有实际计算出来。这段代码实际上属于典型 TensorFlow 程序中的计算图构建部分。运行这段代码后,你会得到类似<tensorflow.python.ops.variables.Variable object at 0x7f074bfd9ef0>的内容,而不是variable_y实际的值10。为了解决这个问题,我们必须执行计算图的代码部分,代码如下:
#initialize all variables
init = tf.global_variables_initializer()
# All variables are now initialized
with tf.Session() as sess:
sess.run(init)
print(sess.run(variable_y))
这里是一些基本数学函数的执行示例,如加法、减法、乘法和除法。更多数学函数,请参考文档:
访问 TensorFlow 数学函数,请查看www.tensorflow.org/versions/r0.12/api_docs/python/math_ops/basic_math_functions。
使用 TensorFlow 进行基本的数学运算
tf.add()函数接受两个数字、两个张量或每种一个,并返回它们的和作为一个张量:
Addition
x = tf.add(1, 2, name=None) # 3
这是一个包含减法和乘法的示例:
x = tf.subtract(1, 2,name=None) # -1
y = tf.multiply(2, 5,name=None) # 10
如果我们想使用非常量值该怎么办?如何将输入数据集传递给 TensorFlow?为此,TensorFlow 提供了一个 API,tf.placeholder(),并使用feed_dict。
placeholder是一个变量,数据稍后会在tf.session.run()函数中分配给它。通过这个方法,我们可以在不需要数据的情况下创建操作,并构建计算图。之后,这些数据通过feed_dict参数被传递到计算图中,从而设置placeholder张量。在以下示例中,在会话运行之前,张量x被设置为字符串Hello World:
x = tf.placeholder(tf.string)
with tf.Session() as sess:
output = sess.run(x, feed_dict={x: 'Hello World'})
也可以使用feed_dict设置多个张量,如下所示:
x = tf.placeholder(tf.string)
y = tf.placeholder(tf.int32, None)
z = tf.placeholder(tf.float32, None)
with tf.Session() as sess:
output = sess.run(x, feed_dict={x: 'Welcome to CNN', y: 123, z: 123.45})
占位符还可以在多维的帮助下存储数组。请参见以下示例:
import tensorflow as tf
x = tf.placeholder("float", [None, 3])
y = x * 2
with tf.Session() as session:
input_data = [[1, 2, 3],
[4, 5, 6],]
result = session.run(y, feed_dict={x: input_data})
print(result)
当传递给feed_dict参数的数据类型与张量类型不匹配且无法转换时,将抛出一个错误,例如ValueError: invalid literal for...。
tf.truncated_normal()函数返回一个从正态分布中生成的随机值的张量。它主要用于网络中的权重初始化:
n_features = 5
n_labels = 2
weights = tf.truncated_normal((n_features, n_labels))
with tf.Session() as sess:
print(sess.run(weights))
TensorFlow 中的 softmax
softmax 函数将其输入值(称为logit或logit 分数)转换为 0 到 1 之间的值,并将输出值标准化,使其总和为 1。换句话说,softmax 函数将 logits 转换为概率。数学上,softmax 函数的定义如下:

在 TensorFlow 中,softmax 函数已经实现。它接受 logits 并返回与输入 logits 类型和形状相同的 softmax 激活,如下图所示:

以下代码用于实现这一点:
logit_data = [2.0, 1.0, 0.1]
logits = tf.placeholder(tf.float32)
softmax = tf.nn.softmax(logits)
with tf.Session() as sess:
output = sess.run(softmax, feed_dict={logits: logit_data})
print( output )
我们在数学上表示标签的方式通常称为 独热编码。每个标签由一个向量表示,正确标签的值为 1.0,其它标签的值为 0.0。这种方式在大多数问题中都非常有效。然而,当问题中有数百万个标签时,独热编码就不太高效,因为大部分向量元素都是零。我们通过测量两个概率向量之间的相似度来衡量其距离,这个度量被称为 交叉熵,用 D 表示。
交叉熵是非对称的。这意味着:D(S,L) != D(L,S)
在机器学习中,我们通常通过一个数学函数来定义模型的“坏”与“好”。这个函数叫做 损失、代价 或 目标 函数。用来衡量模型损失的一个非常常见的函数是 交叉熵损失。这一概念来源于信息论(关于这一点的更多内容,请参考colah.github.io/posts/2015-09-Visual-Information/)。直观地,如果模型在训练数据上的分类效果差,损失就会很高;反之,如果分类效果好,损失会很低,如下所示:

交叉熵损失函数
在 TensorFlow 中,我们可以使用 tf.reduce_sum() 来编写交叉熵函数;它接受一个数字数组并返回其和作为一个张量(见下方代码块):
x = tf.constant([[1,1,1], [1,1,1]])
with tf.Session() as sess:
print(sess.run(tf.reduce_sum([1,2,3]))) #returns 6
print(sess.run(tf.reduce_sum(x,0))) #sum along x axis, prints [2,2,2]
但在实际操作中,在计算 softmax 函数时,由于指数的存在,某些中间项可能会非常大。因此,除法操作可能在数值上不稳定。我们应该使用 TensorFlow 提供的 softmax 和交叉熵损失 API。以下代码片段手动计算交叉熵损失,并使用 TensorFlow API 打印出相同的结果:
import tensorflow as tf
softmax_data = [0.1,0.5,0.4]
onehot_data = [0.0,1.0,0.0]
softmax = tf.placeholder(tf.float32)
onehot_encoding = tf.placeholder(tf.float32)
cross_entropy = - tf.reduce_sum(tf.multiply(onehot_encoding,tf.log(softmax)))
cross_entropy_loss = tf.nn.softmax_cross_entropy_with_logits(logits=tf.log(softmax), labels=onehot_encoding)
with tf.Session() as session:
print(session.run(cross_entropy,feed_dict={softmax:softmax_data, onehot_encoding:onehot_data} ))
print(session.run(cross_entropy_loss,feed_dict={softmax:softmax_data, onehot_encoding:onehot_data} ))
MNIST 数据集简介
这里我们使用 MNIST(修改后的国家标准与技术研究院数据集),它包含手写数字的图像及其标签。自 1999 年发布以来,这个经典数据集被用来作为分类算法的基准。
数据文件 train.csv 和 test.csv 包含从 0 到 9 的手绘数字,以灰度图像的形式展示。数字图像是一个数学函数,形式为 f(x,y)=像素 值。图像是二维的。
我们可以对图像执行任何数学函数。通过计算图像的梯度,我们可以测量像素值变化的速度和变化的方向。为了简化图像识别,我们将图像转换为灰度图像,并且只有一个颜色通道。RGB图像由三个颜色通道组成,红色、蓝色和绿色。在 RGB 颜色模式中,一幅图像是由三张图像(红色、蓝色和绿色)叠加而成的。在灰度颜色模式中,颜色不重要。彩色图像在计算上更难分析,因为它们在内存中占用更多空间。强度是图像明暗的度量,对于识别物体非常有用。在某些应用中,例如自动驾驶汽车应用中的车道线检测,颜色非常重要,因为需要区分黄色车道和白色车道。灰度图像无法提供足够的信息来区分白色和黄色车道线。
任何灰度图像都会被计算机解读为一个矩阵,每个图像像素对应一个条目。每幅图像的高度和宽度为 28 x 28 像素,总共有 784 个像素。每个像素都有一个单独的像素值与之相关联。这个像素值表示该像素的明暗程度。该像素值是一个从 0 到 255 的整数,其中零表示最暗,255 表示最亮,灰色像素的值在 0 和 255 之间。
最简单的人工神经网络
以下图像表示一个简单的两层神经网络:

简单的两层神经网络
第一层是输入层,最后一层是输出层。中间层是隐藏层。如果有多个隐藏层,那么这样的网络就是深度神经网络。
每个隐藏层中神经元的输入和输出与下一层中的每个神经元相连。每层的神经元数量可以根据问题的不同而有所不同。我们来看一个例子。一个简单的例子是你可能已经知道的流行手写数字识别,它用于检测一个数字,比如 5。该网络会接受一个数字 5 的图像,并输出 1 或 0。1 表示该图像确实是一个 5,0 则表示不是。网络一旦创建,就需要进行训练。我们可以先用随机权重初始化,然后输入已知的样本数据集(training dataset)。对于每个输入样本,我们检查输出,计算误差率,然后调整权重,使得每当它看到 5 时输出 1,而对其他任何数字输出 0。这种训练方法叫做监督学习,而调整权重的方法则叫做反向传播。在构建人工神经网络模型时,主要的考虑因素之一是如何选择隐藏层和输出层的激活函数。三种最常用的激活函数是 sigmoid 函数、双曲正切函数和修正线性单元(ReLU)。sigmoid 函数的优点在于它的导数可以在z上计算,其计算公式为z乘以 1 减去z。这意味着:
- dy/dx =σ(x)(1−σ(x)) *
这有助于我们以一种便捷的方式有效地计算神经网络中使用的梯度。如果某一层的逻辑函数的前馈激活值保存在内存中,那么该层的梯度可以通过简单的乘法和减法进行计算,而无需实现和重新计算 sigmoid 函数,因为这需要额外的指数运算。下图展示了 ReLU 激活函数,当x < 0时其值为零,而当x > 0时其值呈线性关系,斜率为 1:

ReLU 是一个非线性函数,其计算公式为f(x)=max(0, x)。这意味着对于负输入,ReLU 的值为 0,而对于所有x >0的输入,其值为x。这意味着激活函数的阈值设定在零(见前面左侧的图像)。TensorFlow 通过tf.nn.relu()实现 ReLU 函数:

反向传播("backward propagation of errors"的缩写)是训练人工神经网络的一种常用方法,通常与优化方法如梯度下降法结合使用。该方法计算损失函数相对于网络中所有权重的梯度。优化方法会获取梯度并利用它更新权重,以减少损失函数。
使用 TensorFlow 构建单层神经网络
让我们一步一步地用 TensorFlow 构建一个单层神经网络。在这个例子中,我们将使用 MNIST 数据集。这个数据集包含 28 x 28 像素的手写数字灰度图像。数据集包括 55,000 条训练数据,10,000 条测试数据和 5,000 条验证数据。每个 MNIST 数据点包含两部分:一个手写数字的图像和一个对应的标签。以下代码块加载数据。one_hot=True 表示标签是 one-hot 编码的向量,而不是标签的实际数字。例如,如果标签是 2,你将看到 [0,0,1,0,0,0,0,0,0,0]。这使得我们可以直接在网络的输出层中使用它:
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)
设置占位符和变量的方法如下:
# All the pixels in the image (28 * 28 = 784)
features_count = 784
# there are 10 digits i.e labels
labels_count = 10
batch_size = 128
epochs = 10
learning_rate = 0.5
features = tf.placeholder(tf.float32, [None,features_count])
labels = tf.placeholder(tf.float32, [None, labels_count])
#Set the weights and biases tensors
weights = tf.Variable(tf.truncated_normal((features_count, labels_count)))
biases = tf.Variable(tf.zeros(labels_count),name='biases')
让我们在 TensorFlow 中设置优化器:
loss,
optimizer = tf.train.GradientDescentOptimizer(learning_rate).minimize(loss)
在开始训练之前,让我们设置变量初始化操作和一个用于衡量预测准确度的操作,如下所示:
# Linear Function WX + b
logits = tf.add(tf.matmul(features, weights),biases)
prediction = tf.nn.softmax(logits)
# Cross entropy
cross_entropy = -tf.reduce_sum(labels * tf.log(prediction), reduction_indices=1)
# Training loss
loss = tf.reduce_mean(cross_entropy)
# Initializing all variables
init = tf.global_variables_initializer()
# Determining if the predictions are accurate
is_correct_prediction = tf.equal(tf.argmax(prediction, 1), tf.argmax(labels, 1))
# Calculating prediction accuracy
accuracy = tf.reduce_mean(tf.cast(is_correct_prediction, tf.float32))
现在我们可以开始训练模型,代码如下所示:
#Beginning the session
with tf.Session() as sess:
# initializing all the variables
sess.run(init)
total_batch = int(len(mnist.train.labels) / batch_size)
for epoch in range(epochs):
avg_cost = 0
for i in range(total_batch):
batch_x, batch_y = mnist.train.next_batch(batch_size=batch_size)
_, c = sess.run([optimizer,loss], feed_dict={features: batch_x, labels: batch_y})
avg_cost += c / total_batch
print("Epoch:", (epoch + 1), "cost =", "{:.3f}".format(avg_cost))
print(sess.run(accuracy, feed_dict={features: mnist.test.images, labels: mnist.test.labels}))
Keras 深度学习库概述
Keras 是一个 Python 中的高级深度神经网络 API,运行在 TensorFlow、CNTK 或 Theano 之上。
这里是一些你需要了解的 Keras 核心概念。TensorFlow 是一个用于数值计算和机器智能的深度学习库。它是开源的,并使用数据流图进行数值计算。数学操作由节点表示,多维数据数组(即张量)由图的边表示。这个框架极其技术性,因此对于数据分析师来说可能比较困难。Keras 使得深度神经网络编码变得简单,同时在 CPU 和 GPU 机器上运行也非常顺畅。
模型 是 Keras 的核心数据结构。顺序模型是最简单的模型类型,它由一系列按顺序堆叠的层组成。它提供了常见的功能,如 fit()、evaluate() 和 compile()。
你可以通过以下代码行创建一个顺序模型:
from keras.models import Sequential
#Creating the Sequential model
model = Sequential()
Keras 模型中的层
Keras 层就像神经网络层一样。有全连接层、最大池化层和激活层。可以使用模型的 add() 函数向模型中添加层。例如,一个简单的模型可以表示为以下内容:
from keras.models import Sequential
from keras.layers.core import Dense, Activation, Flatten
#Creating the Sequential model
model = Sequential()
#Layer 1 - Adding a flatten layer
model.add(Flatten(input_shape=(32, 32, 3)))
#Layer 2 - Adding a fully connected layer
model.add(Dense(100))
#Layer 3 - Adding a ReLU activation layer
model.add(Activation('relu'))
#Layer 4- Adding a fully connected layer
model.add(Dense(60))
#Layer 5 - Adding an ReLU activation layer
model.add(Activation('relu'))
Keras 将自动推断第一层之后所有层的形状。这意味着你只需要为第一层设置输入维度。前面代码片段中的第一层 model.add(Flatten(input_shape=(32, 32, 3))) 将输入维度设置为 (32, 32, 3),输出维度设置为 (3072=32 x 32 x 3)。第二层接收第一层的输出,并将输出维度设置为 (100)。这种将输出传递给下一层的链式过程一直持续到最后一层,即模型的输出。
使用 Keras 和 MNIST 进行手写数字识别
一个典型的数字识别神经网络可能有 784 个输入像素,连接到 1000 个隐藏层神经元,隐藏层再连接到 10 个输出目标——每个数字对应一个输出。每一层都与上一层完全连接。下面是这个网络的图形表示,其中x是输入,h是隐藏层神经元,y是输出类别变量:

在这个笔记本中,我们将构建一个神经网络来识别从 0 到 9 的手写数字。
我们正在构建的神经网络类型在许多现实世界应用中得到了应用,例如识别电话号码和按地址排序邮政邮件。为了构建这个网络,我们将使用MNIST数据集。
我们将按照以下代码开始,导入所有必需的模块,然后加载数据,最后构建网络:
# Import Numpy, keras and MNIST data
import numpy as np
import matplotlib.pyplot as plt
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers.core import Dense, Dropout, Activation
from keras.utils import np_utils
获取训练数据和测试数据
MNIST 数据集已经包含了训练数据和测试数据。训练数据有 60,000 个数据点,测试数据有 10,000 个数据点。如果您在'~/.keras/datasets/' +路径下没有数据文件,可以通过这个位置下载。
每个 MNIST 数据点都有:
-
一张手写数字的图像
-
一个对应的标签,是一个从 0 到 9 的数字,用于帮助识别图像
图像将被调用,并作为我们神经网络的输入,X;其对应的标签是y。
我们希望标签是独热向量。独热向量是由许多零和一个组成的向量。通过一个例子来看最为直观。数字 0 表示为[1, 0, 0, 0, 0, 0, 0, 0, 0, 0],数字 4 表示为[0, 0, 0, 0, 1, 0, 0, 0, 0, 0],这是一个独热向量。
扁平化数据
在这个示例中,我们将使用扁平化数据,或者也可以使用 MNIST 图像的一维表示,而非二维。这样,每个 28 x 28 像素的数字图像将被表示为一个 784 像素的一维数组。
通过扁平化数据,图像的二维结构信息会丢失;然而,我们的数据得到了简化。借助这一点,所有的训练数据都可以包含在一个形状为(60,000,784)的数组中,其中第一维表示训练图像的数量,第二维表示每张图像中的像素数。这样结构的数据可以用简单的神经网络进行分析,如下所示:
# Retrieving the training and test data
(X_train, y_train), (X_test, y_test) = mnist.load_data()
print('X_train shape:', X_train.shape)
print('X_test shape: ', X_test.shape)
print('y_train shape:',y_train.shape)
print('y_test shape: ', y_test.shape)
可视化训练数据
以下函数将帮助您可视化 MNIST 数据。通过传入训练样本的索引,show_digit函数将显示该训练图像及其对应的标签:
# Visualize the data
import matplotlib.pyplot as plt
%matplotlib inline
#Displaying a training image by its index in the MNIST set
def display_digit(index):
label = y_train[index].argmax(axis=0)
image = X_train[index]
plt.title('Training data, index: %d, Label: %d' % (index, label))
plt.imshow(image, cmap='gray_r')
plt.show()
# Displaying the first (index 0) training image
display_digit(0)
X_train = X_train.reshape(60000, 784)
X_test = X_test.reshape(10000, 784)
X_train = X_train.astype('float32')
X_test = X_test.astype('float32')
X_train /= 255
X_test /= 255
print("Train the matrix shape", X_train.shape)
print("Test the matrix shape", X_test.shape)
#One Hot encoding of labels.
from keras.utils.np_utils import to_categorical
print(y_train.shape)
y_train = to_categorical(y_train, 10)
y_test = to_categorical(y_test, 10)
print(y_train.shape)
构建网络
对于这个例子,您将定义以下内容:
-
输入层,您应该预期每一条 MNIST 数据都包含此层,因为它告诉网络输入的数量
-
隐藏层,它们识别数据中的模式,并将输入层与输出层连接起来
-
输出层,因为它定义了网络如何学习,并为给定图像提供标签输出,如下所示:
# Defining the neural network
def build_model():
model = Sequential()
model.add(Dense(512, input_shape=(784,)))
model.add(Activation('relu')) # An "activation" is just a non-linear function that is applied to the output
# of the above layer. In this case, with a "rectified linear unit",
# we perform clamping on all values below 0 to 0.
model.add(Dropout(0.2)) #With the help of Dropout helps we can protect the model from memorizing or "overfitting" the training data
model.add(Dense(512))
model.add(Activation('relu'))
model.add(Dropout(0.2))
model.add(Dense(10))
model.add(Activation('softmax')) # This special "softmax" activation,
#It also ensures that the output is a valid probability distribution,
#Meaning that values obtained are all non-negative and sum up to 1.
return model
#Building the model
model = build_model()
model.compile(optimizer='rmsprop',
loss='categorical_crossentropy',
metrics=['accuracy'])
训练网络
现在我们已经构建了网络,我们将数据输入并进行训练,如下所示:
# Training
model.fit(X_train, y_train, batch_size=128, nb_epoch=4, verbose=1,validation_data=(X_test, y_test))
测试
当你对训练输出和准确率感到满意时,你可以在测试数据集上运行网络,以衡量其性能!
请记住,只有在完成训练并对结果感到满意后,再执行此操作。
一个好的结果会获得高于 95%的准确率。已知一些简单的模型甚至可以达到 99.7%的准确率!我们可以测试模型,如下所示:
# Comparing the labels predicted by our model with the actual labels
score = model.evaluate(X_test, y_test, batch_size=32, verbose=1,sample_weight=None)
# Printing the result
print('Test score:', score[0])
print('Test accuracy:', score[1])
理解反向传播
在这一部分,我们将理解反向传播的直觉。这是一种使用链式法则计算梯度的方法。理解这个过程及其细节对于你能够理解并有效地开发、设计和调试神经网络至关重要。
一般来说,给定一个函数f(x),其中x是输入的向量,我们想要计算f在x处的梯度,记作∇(f(x))。这是因为在神经网络的情况下,函数f基本上是一个损失函数(L),而输入x是权重和训练数据的组合。符号∇的发音为nabla:
(xi, yi ) i = 1......N
为什么我们要对权重参数计算梯度?
给定训练数据通常是固定的,而参数是我们可以控制的变量。我们通常计算参数的梯度,以便可以使用它来更新参数。梯度∇f是部分导数的向量,也就是说:
∇f = [ df/dx, df/dy] = [y,x]
总的来说,反向传播将包括:
-
执行前向传播操作
-
将模型的输出与期望的输出进行比较
-
计算误差
-
运行反向传播操作(反向传播),将误差传播到每个权重上
-
使用这个来更新权重,得到更好的模型
-
持续进行,直到我们得到一个良好的模型
我们将构建一个识别从 0 到 9 的数字的神经网络。这类网络应用被用于按邮政编码排序邮件、从图像中识别电话号码和门牌号、从包裹图像中提取包裹数量等等。
在大多数情况下,反向传播是在框架中实现的,例如 TensorFlow。然而,仅仅通过添加任意数量的隐藏层,反向传播并不总是会神奇地作用于数据集。事实上,如果权重初始化不当,这些非线性函数可能会饱和并停止学习。也就是说,训练损失会变得平稳并拒绝下降。这就是梯度消失问题。
如果你的权重矩阵W初始化得过大,那么矩阵乘法的输出可能也会有非常大的范围,这会导致向量z中的所有输出几乎都是二进制的:要么是 1,要么是 0。然而,如果是这种情况,那么z(1-z),即 sigmoid 非线性函数的局部梯度,将会在两种情况下都变为零(消失),这将导致x和W*的梯度也为零。由于链式法则中的乘法,接下来的反向传播从这一点开始也会输出全零。
另一个非线性激活函数是 ReLU,它将神经元的值阈值化为零,如下所示。使用 ReLU 的全连接层的前向和反向传播核心包括:
z = np.maximum(0, np.dot(W, x)) #Representing forward pass
dW = np.outer(z > 0, x) #Representing backward pass: local gradient for W
如果你观察一段时间,你会发现,如果一个神经元在前向传播中被限制为零(即z = 0,它不激活),那么它的权重将获得一个零梯度。这可能会导致所谓的死 ReLU问题。这意味着,如果一个 ReLU 神经元初始化时恰好从未激活,或者在训练过程中其权重因大的更新而进入这种状态,那么这个神经元将永远处于“死”状态。就像永久性的、不可恢复的脑损伤一样。有时,你甚至可以将整个训练集通过一个已训练的网络,最终意识到有很大一部分(大约 40%)的神经元一直是零。
在微积分中,链式法则用于计算两个或多个函数的复合函数的导数。也就是说,如果我们有两个函数f和g,那么链式法则表示它们复合函数f ∘ g的导数。该函数将x映射到f(g(x)),其导数可以通过f和g的导数及函数的乘积表示如下:

有一种更明确的方式可以通过变量来表示这一点。设F = f ∘ g,或者等价地,F(x) = f(g(x)) 对所有的x都成立。然后也可以写作:
F'(x) = f'(g(x))g'(x)。
链式法则可以借助莱布尼茨符号以以下方式写出。如果变量z依赖于变量y,而y又依赖于变量x(如此y和z为依赖变量),那么z也通过中介变量y依赖于x。链式法则则表示:

z = 1/(1 + np.exp(-np.dot(W, x))) # 前向传播
dx = np.dot(W.T, z(1-z))* # 反向传播:x的局部梯度
dW = np.outer(z(1-z), x)* # 反向传播:W的局部梯度
下图左侧的前向传播通过输入变量 x 和 y,计算函数 f(x,y) 得到 z。图右侧表示反向传播。接收 dL/dz(即损失函数关于 z 的梯度),通过链式法则计算出损失函数对 x 和 y 的梯度,如下图所示:

总结
在本章中,我们奠定了神经网络的基础,并通过最简单的人工神经网络进行了讲解。我们学习了如何使用 TensorFlow 构建单层神经网络。
我们研究了 Keras 模型中各层的差异,并使用 Keras 和 MNIST 演示了著名的手写数字识别。
最后,我们理解了什么是反向传播,并使用 MNIST 数据集构建我们的网络,进行数据的训练和测试。
在下一章,我们将介绍卷积神经网络(CNN)。
第二章:卷积神经网络简介
卷积神经网络(CNNs)无处不在。在过去五年中,由于引入了深度架构用于特征学习和分类,视觉识别系统的性能得到了显著提升。CNN 在多个领域取得了良好的表现,如自动语音理解、计算机视觉、语言翻译、自动驾驶汽车以及类似 Alpha Go 的游戏。因此,CNN 的应用几乎是无限的。DeepMind(来自谷歌)最近发布了 WaveNet,这是一种利用 CNN 生成模仿任何人类声音的语音的技术(deepmind.com/blog/wavenet-generative-model-raw-audio/)。
在本章中,我们将涵盖以下主题:
-
CNN 的历史
-
CNN 概述
-
图像增强
CNN 的历史
几十年来,机器识别图像的尝试层出不穷。模仿人脑的视觉识别系统在计算机中是一项挑战。人类的视觉是大脑中最难模仿且最复杂的感知认知系统。我们在这里不会讨论生物神经元,即初级视觉皮层,而是专注于人工神经元。物理世界中的物体是三维的,而这些物体的图像是二维的。在本书中,我们将介绍神经网络,而不借用大脑类比。1963 年,计算机科学家 Larry Roberts,也被称为计算机视觉之父,在他的研究论文《BLOCK WORLD》中描述了从物体的二维透视图中提取三维几何信息的可能性。这是计算机视觉领域的第一个突破。全球许多研究者在机器学习和人工智能领域跟随这项工作,并在 BLOCK WORLD 的背景下研究计算机视觉。人类能够识别物体,不论物体的朝向或光照发生何种变化。在这篇论文中,他指出,理解图像中的简单边缘形状是很重要的。他从方块中提取这些边缘形状,以使计算机理解这两个方块无论朝向如何都是相同的:

视觉始于一个简单的结构。这是计算机视觉作为工程模型的起点。MIT 计算机视觉科学家 David Mark 给了我们下一个重要的概念,那就是视觉是层次化的。他写了一本非常有影响力的书,名为VISION。这是一本简单的书。他说,一幅图像由几个层次组成。这两个原则构成了深度学习架构的基础,尽管它们并没有告诉我们该使用哪种数学模型。
在 1970 年代,第一种视觉识别算法——广义圆柱模型,来自斯坦福大学的 AI 实验室。这个模型的思想是,世界由简单的形状构成,任何现实世界的物体都是这些简单形状的组合。同时,SRI Inc. 发布了另一种模型——图像结构模型。其概念与广义圆柱模型相同,但这些部分通过弹簧连接,因此引入了可变性概念。第一个视觉识别算法在 2006 年由富士胶片公司在数码相机中使用。
卷积神经网络
CNN 或者 ConvNet 与常规神经网络非常相似。它们仍然由带有权重的神经元组成,这些权重可以通过数据学习得到。每个神经元接收一些输入并进行点积运算。它们仍然在最后的全连接层上使用损失函数。它们仍然可以使用非线性激活函数。我们在上一章学到的所有技巧和方法对于 CNN 仍然有效。正如我们在上一章中看到的,常规神经网络将输入数据作为一个单一的向量,经过一系列隐藏层。每个隐藏层由一组神经元组成,每个神经元与前一层的所有神经元完全连接。在单一层内,每个神经元是完全独立的,它们之间没有任何连接。最后一个全连接层,也称为输出层,在图像分类问题中包含类别得分。一般来说,一个简单的 ConvNet 包括三个主要层:卷积层、池化层和全连接层。我们可以在下图中看到一个简单的神经网络:

一个常规的三层神经网络
那么,什么改变了呢?由于 CNN 主要以图像作为输入,这使我们能够在网络中编码一些特性,从而减少了参数的数量。
在实际的图像数据中,CNN 比多层感知机(MLP)表现更好。其原因有两个:
-
在上一章中,我们看到,为了将图像输入到 MLP 中,我们将输入矩阵转换为一个简单的数值向量,这个向量没有空间结构。它无法理解这些数字是如何在空间上排列的。因此,CNN 正是为了解决这个问题而设计的,旨在揭示多维数据中的模式。与 MLP 不同,CNN 理解图像中彼此距离较近的像素之间关系比远离的像素之间的关系更强:
CNN = 输入层 + 隐藏层 + 全连接层
-
CNN 与 MLP 在模型中包含的隐藏层类型上有所不同。一个 ConvNet 将其神经元按三维方式排列:宽度、高度和深度。每一层使用激活函数将其三维输入体积转换为三维输出体积。例如,在下图中,红色输入层包含图像。因此,它的宽度和高度是图像的维度,深度为三,因为有红色、绿色和蓝色通道:

ConvNet 是深度神经网络,它们在空间上共享参数。
计算机如何解读图像?
本质上,每张图像可以表示为一个像素值矩阵。换句话说,图像可以看作是一个函数(f),它从R²映射到R。
f(x, y) 给出位置(x, y)处的强度值。实际上,函数的值范围仅从0到255。类似地,一张彩色图像可以表示为三个函数的堆叠。我们可以将其写为一个向量:
f(x, y) = [ r(x,y) g(x,y) b(x,y)]
或者我们可以将其写成一个映射:
f: R x R --> R3
因此,一张彩色图像也是一个函数,但在这种情况下,每个(x,y)位置的值不是一个单一的数字。相反,它是一个向量,包含三种不同的光强度,对应于三个颜色通道。以下是查看图像细节作为计算机输入的代码。
用于可视化图像的代码
让我们看一下如何使用以下代码来可视化一张图像:
#import all required lib
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
from skimage.io import imread
from skimage.transform import resize
# Load a color image in grayscale
image = imread('sample_digit.png',as_grey=True)
image = resize(image,(28,28),mode='reflect')
print('This image is: ',type(image),
'with dimensions:', image.shape)
plt.imshow(image,cmap='gray')
我们得到以下结果图像:

def visualize_input(img, ax):
ax.imshow(img, cmap='gray')
width, height = img.shape
thresh = img.max()/2.5
for x in range(width):
for y in range(height):
ax.annotate(str(round(img[x][y],2)), xy=(y,x),
horizontalalignment='center',
verticalalignment='center',
color='white' if img[x][y]<thresh else 'black')
fig = plt.figure(figsize = (12,12))
ax = fig.add_subplot(111)
visualize_input(image, ax)
得到以下结果:

在前一章中,我们使用基于 MLP 的方法来识别图像。该方法存在两个问题:
-
它增加了参数的数量
-
它只接受向量作为输入,也就是说,将矩阵展平为向量
这意味着我们必须找到一种新的处理图像的方法,在这种方法中,二维信息不会完全丢失。CNN 解决了这个问题。此外,CNN 接受矩阵作为输入。卷积层保留空间结构。首先,我们定义一个卷积窗口,也叫做滤波器,或卷积核;然后将其在图像上滑动。
Dropout
神经网络可以被看作是一个搜索问题。神经网络中的每个节点都在搜索输入数据与正确输出数据之间的相关性。
Dropout 在前向传播时随机关闭节点,从而帮助防止权重收敛到相同的位置。完成此操作后,它会打开所有节点并进行反向传播。同样,我们也可以在前向传播时将某些层的值随机设置为零,以便对该层执行 dropout。
仅在训练过程中使用 dropout。在运行时或测试数据集上不要使用它。
输入层
输入层保存图像数据。在下图中,输入层由三个输入组成。在全连接层中,两个相邻层之间的神经元是完全连接的,但在同一层内的神经元之间没有连接。换句话说,这一层的神经元与上一层的所有激活都有全连接。因此,它们的激活可以通过简单的矩阵乘法计算,可能还需要加上偏置项。全连接层与卷积层的区别在于,卷积层中的神经元仅连接到输入的局部区域,并且它们还共享参数:

卷积层
卷积在与 ConvNet 相关的目标是从输入图像中提取特征。这一层在 ConvNet 中进行大部分计算。我们在这里不会深入讲解卷积的数学细节,而是了解它在图像上的工作原理。
ReLU 激活函数在 CNN 中非常有用。
Keras 中的卷积层
在 Keras 中创建卷积层之前,你必须首先导入所需的模块,如下所示:
from keras.layers import Conv2D
然后,你可以使用以下格式创建卷积层:
Conv2D(filters, kernel_size, strides, padding, activation='relu', input_shape)
你必须传入以下参数:
-
filters:过滤器的数量。 -
kernel_size:指定卷积窗口(方形)的高度和宽度的数字。你还可以调整一些额外的可选参数。 -
strides:卷积的步幅。如果你没有指定,默认为 1。 -
padding:这可以是valid或same。如果没有指定,填充默认为valid。 -
activation:通常是relu。如果没有指定,则不会应用激活函数。强烈建议你在每个卷积层中添加 ReLU 激活函数。
kernel_size和strides都可以表示为数字或元组。
当将卷积层用作模型中的第一层(位于输入层之后)时,你必须提供一个额外的input_shape参数——input_shape。它是一个元组,指定输入的高度、宽度和深度(按此顺序)。
如果卷积层不是你网络中的第一层,请确保不包含input_shape参数。
你可以设置许多其他可调参数,以改变卷积层的行为:
- 示例 1:为了构建一个接受 200 x 200 像素灰度图像的输入层的 CNN。在这种情况下,下一层将是一个具有 16 个过滤器的卷积层,宽度和高度为 2。随着卷积的进行,我们可以设置过滤器每次跳跃 2 个像素。因此,我们可以使用以下代码构建一个不填充零的卷积层:
Conv2D(filters=16, kernel_size=2, strides=2, activation='relu', input_shape=(200, 200, 1))
- 示例 2:在构建完 CNN 模型后,我们可以在模型中加入下一层,通常是卷积层。这个层会有 32 个滤波器,宽度和高度均为 3,它会将前面示例中构建的层作为输入。在进行卷积操作时,我们设置滤波器每次跳跃一个像素,以便卷积层能够查看前一层的所有区域。通过以下代码,我们可以构建这样一个卷积层:
Conv2D(filters=32, kernel_size=3, padding='same', activation='relu')
- 示例 3:你还可以在 Keras 中构建大小为 2 x 2 的卷积层,使用 64 个滤波器和 ReLU 激活函数。在这里,卷积操作采用步幅为 1,填充方式为
valid,其他参数均使用默认值。可以使用以下代码来构建这样一个卷积层:
Conv2D(64, (2,2), activation='relu')
池化层
如我们所见,卷积层是由一堆特征图组成的,每个滤波器对应一个特征图。更多的滤波器会增加卷积的维度,而维度越高表示参数越多。因此,池化层通过逐渐减小表示的空间大小来控制过拟合,从而减少参数和计算量。池化层通常以卷积层作为输入。最常用的池化方法是最大池化。除了最大池化,池化单元还可以执行其他功能,比如平均池化。在 CNN 中,我们可以通过指定每个滤波器的大小和滤波器的数量来控制卷积层的行为。为了增加卷积层中的节点数,我们可以增加滤波器的数量;为了增大模式的大小,我们可以增大滤波器的尺寸。此外,还有一些其他的超参数可以调整,其中之一是卷积的步幅。步幅是滤波器在图像上滑动的步长。步幅为 1 时,滤波器会水平和垂直移动 1 个像素。在这种情况下,卷积的输出尺寸将与输入图像的宽度和深度相同。步幅为 2 时,卷积层的输出宽度和高度将为输入图像的一半。如果滤波器超出了图像边界,我们可以选择忽略这些未知值,或者用零来填充它们,这被称为填充。在 Keras 中,如果可以接受丢失少量数据,我们可以设置 padding = 'valid';否则,设置 padding = 'same':

一个非常简单的卷积网络如下所示:

实际示例 – 图像分类
卷积层有助于检测图像中的区域模式。卷积层之后的最大池化层有助于减少维度。这里是一个使用我们在前面几节中学习的所有原则进行图像分类的例子。一个重要的概念是,在做其他操作之前,首先要将所有图像调整为标准大小。第一个卷积层需要一个额外的input.shape()参数。在这一节中,我们将训练一个 CNN 来分类来自 CIFAR-10 数据库的图像。CIFAR-10 是一个包含 60,000 张 32 x 32 大小的彩色图像的数据集。这些图像被标注为 10 个类别,每个类别有 6,000 张图像。这些类别是飞机、汽车、鸟、猫、狗、鹿、青蛙、马、船和卡车。让我们看看如何通过以下代码来实现:
import keras
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
fig = plt.figure(figsize=(20,5))
for i in range(36):
ax = fig.add_subplot(3, 12, i + 1, xticks=[], yticks=[])
ax.imshow(np.squeeze(x_train[i]))from keras.datasets import cifar10
# rescale [0,255] --> [0,1]
x_train = x_train.astype('float32')/255
from keras.utils import np_utils
# one-hot encode the labels
num_classes = len(np.unique(y_train))
y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)
# break training set into training and validation sets
(x_train, x_valid) = x_train[5000:], x_train[:5000]
(y_train, y_valid) = y_train[5000:], y_train[:5000]
# print shape of training set
print('x_train shape:', x_train.shape)
# printing number of training, validation, and test images
print(x_train.shape[0], 'train samples')
print(x_test.shape[0], 'test samples')
print(x_valid.shape[0], 'validation samples')x_test = x_test.astype('float32')/255
from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout
model = Sequential()
model.add(Conv2D(filters=16, kernel_size=2, padding='same', activation='relu',
input_shape=(32, 32, 3)))
model.add(MaxPooling2D(pool_size=2))
model.add(Conv2D(filters=32, kernel_size=2, padding='same', activation='relu'))
model.add(MaxPooling2D(pool_size=2))
model.add(Conv2D(filters=64, kernel_size=2, padding='same', activation='relu'))
model.add(MaxPooling2D(pool_size=2))
model.add(Conv2D(filters=32, kernel_size=2, padding='same', activation='relu'))
model.add(MaxPooling2D(pool_size=2))
model.add(Dropout(0.3))
model.add(Flatten())
model.add(Dense(500, activation='relu'))
model.add(Dropout(0.4))
model.add(Dense(10, activation='softmax'))
model.summary()
# compile the model
model.compile(loss='categorical_crossentropy', optimizer='rmsprop',
metrics=['accuracy'])
from keras.callbacks import ModelCheckpoint
# train the model
checkpointer = ModelCheckpoint(filepath='model.weights.best.hdf5', verbose=1,
save_best_only=True)
hist = model.fit(x_train, y_train, batch_size=32, epochs=100,
validation_data=(x_valid, y_valid), callbacks=[checkpointer],
verbose=2, shuffle=True)
图像增强
在训练 CNN 模型时,我们不希望模型根据图像的大小、角度和位置改变任何预测。图像被表示为像素值的矩阵,因此,大小、角度和位置对像素值有很大的影响。为了使模型对尺寸不变性更强,我们可以将不同尺寸的图像添加到训练集中。同样,为了使模型对旋转不变性更强,我们可以添加具有不同角度的图像。这个过程被称为图像数据增强。这还有助于避免过拟合。过拟合发生在模型仅接触到非常少的样本时。图像数据增强是减少过拟合的一种方法,但可能还不够,因为增强后的图像之间仍然存在相关性。Keras 提供了一个图像增强类,叫做ImageDataGenerator,它定义了图像数据增强的配置。它还提供了其他功能,例如:
-
样本标准化和特征标准化
-
随机旋转、平移、剪切和缩放图像
-
水平和垂直翻转
-
ZCA 白化
-
维度重排序
-
将更改保存到磁盘
可以按照以下方式创建一个增强型图像生成器对象:
imagedatagen = ImageDataGenerator()
该 API 通过实时数据增强生成批量张量图像数据,而不是在内存中处理整个图像数据集。这个 API 旨在在模型拟合过程中创建增强的图像数据。因此,它减少了内存开销,但为模型训练增加了一些时间成本。
创建并配置完毕后,您必须对数据进行拟合。这将计算进行图像数据转换所需的任何统计信息。通过在数据生成器上调用fit()函数并将其传递给训练数据集,可以完成此操作,如下所示:
imagedatagen.fit(train_data)
可以配置批量大小,准备数据生成器,并通过调用flow()函数接收批量图像:
imagedatagen.flow(x_train, y_train, batch_size=32)
最后,调用fit_generator()函数,而不是在模型上调用fit()函数:
fit_generator(imagedatagen, samples_per_epoch=len(X_train), epochs=200)
让我们看一些示例来理解 Keras 中的图像增强 API 是如何工作的。在这些示例中,我们将使用 MNIST 手写数字识别任务。
让我们首先来看一下训练数据集中前九张图像:
#Plot images
from keras.datasets import mnist
from matplotlib import pyplot
#loading data
(X_train, y_train), (X_test, y_test) = mnist.load_data()
#creating a grid of 3x3 images
for i in range(0, 9):
pyplot.subplot(330 + 1 + i)
pyplot.imshow(X_train[i], cmap=pyplot.get_cmap('gray'))
#Displaying the plot
pyplot.show()
以下代码片段从 CIFAR-10 数据集中创建了增强图像。我们将这些图像添加到上一例中的训练集,并观察分类准确率如何提高:
from keras.preprocessing.image import ImageDataGenerator
# creating and configuring augmented image generator
datagen_train = ImageDataGenerator(
width_shift_range=0.1, # shifting randomly images horizontally (10% of total width)
height_shift_range=0.1, # shifting randomly images vertically (10% of total height)
horizontal_flip=True) # flipping randomly images horizontally
# creating and configuring augmented image generator
datagen_valid = ImageDataGenerator(
width_shift_range=0.1, # shifting randomly images horizontally (10% of total width)
height_shift_range=0.1, # shifting randomly images vertically (10% of total height)
horizontal_flip=True) # flipping randomly images horizontally
# fitting augmented image generator on data
datagen_train.fit(x_train)
datagen_valid.fit(x_valid)
总结
我们通过简要回顾卷积神经网络(CNN)的历史开始了这一章。我们向你介绍了可视化图像的实现方法。
我们通过一个实际的例子学习了图像分类,运用了本章中学到的所有原理。最后,我们了解了图像增强如何帮助我们避免过拟合,并研究了图像增强提供的其他各种功能。
在下一章中,我们将学习如何从零开始构建一个简单的图像分类 CNN 模型。
第三章:构建你的第一个 CNN 并优化性能
卷积神经网络(CNN)是一种前馈神经网络(FNN),其神经元之间的连接模式受到动物视觉皮层的启发。近年来,CNN 在图像搜索服务、自动驾驶汽车、自动视频分类、语音识别和自然语言处理(NLP)中展现出了超越人类的表现。
考虑到这些动机,在本章中,我们将从零开始构建一个简单的 CNN 图像分类模型,并介绍一些理论方面的内容,如卷积操作和池化操作。然后,我们将讨论如何调整超参数并优化 CNN 的训练时间,以提高分类准确性。最后,我们将通过考虑一些最佳实践来构建第二个 CNN 模型。简而言之,本章将涵盖以下主题:
-
CNN 架构与 DNN 的缺点
-
卷积操作与池化层
-
创建并训练 CNN 进行图像分类
-
模型性能优化
-
创建一个优化性能的改进版 CNN
CNN 架构与 DNN 的缺点
在第二章,《卷积神经网络简介》中,我们讨论了常规的多层感知器对于小图像(例如,MNIST 或 CIFAR-10)效果良好。然而,对于较大图像,它会因为所需的参数数量庞大而崩溃。例如,一张 100 × 100 的图像有 10,000 个像素,如果第一层只有 1,000 个神经元(这已经严重限制了传递到下一层的信息量),这意味着 10 百万个连接;而这仅仅是第一层。
CNN 通过使用部分连接的层来解决这个问题。由于连续层仅部分连接,并且由于它大量重用权重,CNN 比全连接的 DNN 具有更少的参数,这使得它的训练速度更快,减少了过拟合的风险,并且需要更少的训练数据。此外,当 CNN 学会了一个可以检测特定特征的核时,它可以在图像的任何位置检测到该特征。相比之下,当 DNN 在某个位置学会了一个特征时,它只能在那个特定位置检测到该特征。
由于图像通常具有非常重复的特征,卷积神经网络(CNN)在图像处理任务(如分类)中能比深度神经网络(DNN)更好地进行泛化,并且使用更少的训练样本。重要的是,DNN 对像素如何组织没有先验知识;它并不知道相邻的像素是接近的。CNN 的架构则嵌入了这种先验知识。较低层通常在图像的小区域内识别特征,而较高层将低层特征合并成更大的特征。这对大多数自然图像有效,赋予了 CNN 在与 DNN 的比较中决定性的优势:

图 1:常规 DNN 与 CNN 的对比,其中每一层的神经元按 3D 排列
例如,在图 1中,左侧展示了一个常规的三层神经网络。右侧则是一个卷积神经网络(ConvNet),它将神经元排列在三维空间(宽度、高度和深度)中,正如其中一层所示。ConvNet 的每一层都将 3D 输入体积转化为 3D 输出体积的神经元激活。红色的输入层包含图像,因此它的宽度和高度就是图像的维度,而深度则是三(红色、绿色和蓝色通道)。因此,我们所讨论的所有多层神经网络都是由一长列神经元组成的,我们需要将输入的图像或数据扁平化为一维,再输入到神经网络中。
然而,一旦你尝试直接输入一张 2D 图像会发生什么呢?答案是,在 CNN 中,每一层都是以 2D 形式表示的,这使得神经元和其对应的输入更容易匹配。我们将在接下来的章节中看到这个例子的应用。另一个重要的事实是,特征图中的所有神经元共享相同的参数,因此大大减少了模型中的参数数量;但更重要的是,这意味着一旦 CNN 学会在一个位置识别某个模式,它就能在任何其他位置识别该模式。
相比之下,一旦常规的 DNN 学习到识别一个位置的模式,它只能在该特定位置进行识别。在多层网络(如 MLP 或 DBN)中,输入层所有神经元的输出都会连接到隐藏层中的每个神经元,然后输出将再次作为输入传递到全连接层。在 CNN 网络中,定义卷积层的连接方式则大不相同。卷积层是 CNN 中的主要层类型,其中每个神经元都连接到输入区域的某一特定区域,这个区域被称为感受野。
在典型的 CNN 架构中,几个卷积层是以级联的方式连接的。每一层后面都跟着一个修正线性单元(ReLU)层,接着是一个池化层,然后是一个或多个卷积层(+ReLU),再接一个池化层,最后是一个或多个全连接层。根据问题类型,网络的深度可能不同。每个卷积层的输出是由单个卷积核生成的一组对象,称为特征图。然后,这些特征图可以作为输入定义传递到下一层。
CNN 网络中的每个神经元都会产生一个输出,后跟一个激活阈值,该阈值与输入成正比且没有限制。

图 2:CNN 的概念性架构
如图 2所示,池化层通常位于卷积层之后(例如,在两个卷积层之间)。池化层将卷积区域划分为子区域。然后,使用最大池化或平均池化技术选择一个代表性值,从而减少后续层的计算时间。这样,卷积神经网络(CNN)可以被看作是一种特征提取器。为了更清晰地理解这一点,请参见下图:

通过这种方式,特征相对于其空间位置的鲁棒性也得到了提高。更具体来说,当特征图作为图像属性并通过灰度图像时,它在网络中逐步变小;但它通常会变得越来越深,因为将添加更多的特征图。
我们已经讨论过这种前馈神经网络(FFNN)的局限性——也就是说,即使在一个浅层架构中,由于图像的输入规模非常大,其中每个像素都是一个相关变量,因此需要大量的神经元。卷积操作为这个问题提供了解决方案,因为它减少了自由参数的数量,使得网络可以更深,且参数更少。
卷积操作
卷积是一种数学运算,它将一个函数滑动到另一个函数上,并测量它们逐点乘积的积分。它与傅里叶变换和拉普拉斯变换有着深厚的联系,并且在信号处理领域中被广泛使用。卷积层实际上使用的是互相关,这与卷积非常相似。
在数学中,卷积是对两个函数进行的数学操作,产生一个第三个函数——即原始函数的修改版(卷积版)。结果函数给出了两个函数逐点乘积的积分,作为其中一个原始函数平移量的函数。感兴趣的读者可以参考此网址获取更多信息:en.wikipedia.org/wiki/Convolution。
因此,卷积神经网络(CNN)最重要的构建块是卷积层。第一卷积层中的神经元并不是与输入图像中的每个像素相连接(就像前馈神经网络(FNN)——例如多层感知机(MLP)和深度信念网络(DBN)那样),而是仅与其感受野中的像素相连接。请参见图 3。反过来,第二卷积层中的每个神经元仅与第一层中位于小矩形内的神经元相连接:

图 3:每个卷积神经元只处理其感受野中的数据
在第二章《卷积神经网络简介》中,我们已经看到所有的多层神经网络(例如,MLP)都由许多神经元组成的层构成,我们需要将输入的图像展平为 1D,然后才能输入神经网络。相反,在 CNN 中,每一层都是以 2D 的形式表示,这使得将神经元与其对应的输入匹配变得更加容易。
感受野的概念被 CNN 用来通过在相邻层的神经元之间强制局部连接模式,从而利用空间局部性。
这种架构允许网络在第一隐藏层集中处理低级特征,然后在下一隐藏层将其组合成更高级的特征,依此类推。这种层次结构在真实世界的图像中很常见,这也是 CNN 在图像识别中表现良好的原因之一。
最后,它不仅需要较少的神经元,而且显著减少了可训练参数的数量。例如,无论图像大小如何,构建大小为 5 x 5 的区域,每个区域使用相同的共享权重,仅需要 25 个可学习参数。通过这种方式,它解决了在使用反向传播训练传统多层神经网络时出现的梯度消失或爆炸问题。
池化、步幅和填充操作
一旦你理解了卷积层的工作原理,池化层就很容易理解。池化层通常独立地对每个输入通道进行处理,因此输出深度与输入深度相同。你也可以在深度维度上进行池化,正如我们接下来将看到的那样,在这种情况下,图像的空间维度(例如,高度和宽度)保持不变,但通道的数量减少。我们来看一下来自著名 TensorFlow 网站的池化层的正式定义:
“池化操作通过一个矩形窗口在输入张量上滑动,对每个窗口执行一个归约操作(平均、最大或带有 argmax 的最大值)。每个池化操作使用称为 ksize 的矩形窗口,窗口之间的偏移量由步幅决定。例如,如果步幅为 1,则使用每个窗口;如果步幅为 2,则每个维度使用每隔一个窗口,依此类推。”
因此,总结来说,就像卷积层一样,池化层中的每个神经元都连接到前一层中一小部分神经元的输出,这些神经元位于一个小的矩形感受野内。然而,我们必须定义其大小、步幅和填充类型。因此,总结一下,输出可以通过以下方式计算:
output[i] = reduce(value[strides * i:strides * i + ksize]),
在这里,索引还考虑了填充值。
池化神经元没有权重。因此,它所做的就是使用聚合函数(如最大值或均值)聚合输入。
换句话说,使用池化的目的是对输入图像进行子采样,以减少计算负担、内存使用量和参数数量。这有助于避免训练阶段的过拟合。减少输入图像的大小还使得神经网络能够容忍一定程度的图像偏移。卷积操作的空间语义依赖于所选择的填充方案。
填充是一种增加输入数据大小的操作。在一维数据的情况下,你只需在数组前后添加一个常数;在二维数据的情况下,你会在矩阵的周围添加这些常数。在 n 维数据中,你会在 n 维超立方体的四周添加常数。在大多数情况下,这个常数是零,称为零填充:
-
VALID 填充:仅丢弃最右侧的列(或最底部的行)
-
SAME 填充:尽量将左右填充均匀,但如果需要添加的列数为奇数,则会将多余的列添加到右侧,正如这个例子中所示。
让我们通过以下图形来直观地解释前面的定义。如果我们希望某一层与前一层具有相同的高度和宽度,通常会在输入周围添加零,如图所示。这被称为SAME或零填充。
SAME 这个术语表示输出特征图与输入特征图具有相同的空间维度。
另一方面,零填充被引入以使形状根据需要匹配,并且在输入图上每一侧的填充量相等。VALID 意味着没有填充,仅丢弃最右侧的列(或最底部的行):

图 4:CNN 中的 SAME 与 VALID 填充对比
在下面的示例(图 5)中,我们使用一个 2 × 2 的池化核,步长为 2,并且没有填充。每个池化核中的最大输入值进入下一层,因为其他输入会被丢弃(稍后我们将看到这一点):

图 5:使用最大池化的示例,即子采样
全连接层
在堆栈的顶部,添加了一个常规的全连接层(也称为FNN或密集层);它的作用类似于多层感知器(MLP),该网络可能由若干个全连接层(加 ReLU 激活函数)组成。最后一层输出(例如 softmax)为预测结果。一个例子是一个 softmax 层,它输出用于多分类任务的估计类别概率。
全连接层将一层中的每个神经元与另一层中的每个神经元连接。尽管全连接的前馈神经网络(FNN)可以用于学习特征和分类数据,但将这种架构应用于图像并不实际。
TensorFlow 中的卷积和池化操作
现在我们已经理论上了解了卷积和池化操作的执行方式,接下来我们来看看如何在 TensorFlow 中实际操作这些操作。让我们开始吧。
在 TensorFlow 中应用池化操作
使用 TensorFlow 时,子采样层通常通过保持该层初始参数来表示为max_pool操作。对于max_pool,它在 TensorFlow 中的签名如下:
tf.nn.max_pool(value, ksize, strides, padding, data_format, name)
现在让我们学习如何创建一个利用前面签名的函数,返回一个类型为tf.float32的张量,即最大池化输出张量:
import tensorflow as tf
def maxpool2d(x, k=2):
return tf.nn.max_pool(x,
ksize=[1, k, k, 1],
strides=[1, k, k, 1],
padding='SAME')
在前面的代码段中,参数可以描述如下:
-
value:这是一个 4D 的float32张量,形状为(批次长度,高度,宽度和通道数)。 -
ksize:一个整数列表,表示每个维度上的窗口大小 -
strides:每个维度上滑动窗口的步长 -
data_format:支持NHWC、NCHW和NCHW_VECT_C -
ordering:NHWC或NCHW -
padding:VALID或SAME
然而,根据 CNN 中的层次结构,TensorFlow 支持其他的池化操作,如下所示:
-
tf.nn.avg_pool:返回一个包含每个窗口平均值的缩小张量 -
tf.nn.max_pool_with_argmax:返回max_pool张量及其最大值的扁平化索引张量 -
tf.nn.avg_pool3d:执行一个类似立方体的avg_pool操作 -
窗口;输入增加了深度
-
tf.nn.max_pool3d:执行与(...)相同的功能,但应用最大操作
现在让我们看一个具体示例,看看填充在 TensorFlow 中的作用。假设我们有一个形状为[2, 3]并且只有一个通道的输入图像x。现在我们想看看VALID和SAME填充的效果:
-
valid_pad:使用 2 x 2 的内核,步幅为 2,且采用VALID填充的最大池化操作 -
same_pad:使用 2 x 2 的内核,步幅为 2,且采用SAME填充的最大池化操作
让我们看看如何在 Python 和 TensorFlow 中实现这一点。假设我们有一个形状为[2, 4]的输入图像,且只有一个通道:
import tensorflow as tf
x = tf.constant([[2., 4., 6., 8.,],
[10., 12., 14., 16.]])
现在让我们给出tf.nn.max_pool接受的形状:
x = tf.reshape(x, [1, 2, 4, 1])
如果我们想要应用使用 2 x 2 内核、步幅为 2 的最大池化,并采用VALID填充:
VALID = tf.nn.max_pool(x, [1, 2, 2, 1], [1, 2, 2, 1], padding='VALID')
另一方面,使用 2 x 2 内核、步幅为 2 并采用SAME填充的最大池化:
SAME = tf.nn.max_pool(x, [1, 2, 2, 1], [1, 2, 2, 1], padding='SAME')
对于VALID填充,由于没有填充,输出形状为[1, 1]。然而,对于SAME填充,由于我们将图像填充为形状[2, 4](使用-inf),然后应用最大池化,输出形状为[1, 2]。让我们验证它们:
print(VALID.get_shape())
print(SAME.get_shape())
>>>
(1, 1, 2, 1)
(1, 1, 2, 1)
TensorFlow 中的卷积操作
TensorFlow 提供了多种卷积方法。经典的形式是通过conv2d操作来应用。让我们看看这个操作的用法:
conv2d(
input,
filter,
strides,
padding,
use_cudnn_on_gpu=True,
data_format='NHWC',
dilations=[1, 1, 1, 1],
name=None
)
我们使用的参数如下:
-
input:该操作将应用于这个原始张量。它有四个维度的确定格式,默认的维度顺序如下所示。 -
filter:这是一个表示内核或滤波器的张量。它有一个非常通用的方法:(filter_height,filter_width,in_channels和out_channels)。 -
strides:这是一个包含四个int类型张量的数据列表,表示每个维度的滑动窗口。 -
padding:可以是SAME或VALID。SAME会尽量保持初始张量维度不变,而VALID则允许其在输出大小和填充计算的情况下增长。稍后我们将看到如何在池化层中执行填充操作。 -
use_cudnn_on_gpu:这表示是否使用CUDA GPU CNN库来加速计算。 -
data_format:指定数据组织的顺序(NHWC或NCWH)。 -
dilations:这表示一个可选的ints列表,默认为 (1, 1, 1, 1)。长度为 4 的 1D 张量,表示每个输入维度的扩张因子。如果设置为 k > 1,则在该维度的每个滤波器元素之间会有 k-1 个跳过的单元。维度的顺序由data_format的值决定;有关详情,请参见前面的代码示例。批处理和深度维度的扩张因子必须为 1。 -
name:操作的名称(可选)。
以下是一个卷积层的示例。它将卷积操作连接起来,添加一个偏置参数的和,最后返回我们为整个层选择的激活函数(在本例中为 ReLU 操作,这是一个常用的操作):
def conv_layer(data, weights, bias, strides=1):
x = tf.nn.conv2d(x,
weights,
strides=[1, strides, strides, 1],
padding='SAME')
x = tf.nn.bias_add(x, bias)
return tf.nn.relu(x)
在这里,x 是 4D 张量输入(批处理大小、高度、宽度和通道)。TensorFlow 还提供了其他几种卷积层。例如:
-
tf.layers.conv1d()创建一个用于 1D 输入的卷积层。例如,在自然语言处理(NLP)中,句子可以表示为一个 1D 的单词数组,感受野覆盖几个相邻的单词。 -
tf.layers.conv3d()创建一个用于 3D 输入的卷积层。 -
tf.nn.atrous_conv2d()创建了一个空洞卷积层(a trous 是法语中“带孔”的意思)。这相当于使用一个常规卷积层,并通过插入零行和零列来扩展滤波器。例如,一个 1 × 3 的滤波器(1, 2, 3)可以通过扩张率为 4 来扩展,得到扩张后的滤波器(1, 0, 0, 0, 2, 0, 0, 0, 3)。这使得卷积层可以在不增加计算量和额外参数的情况下拥有更大的感受野。 -
tf.layers.conv2d_transpose()创建了一个转置卷积层,有时也称为 反卷积层,它用于上采样图像。它通过在输入之间插入零来实现,因此可以将其视为一个使用分数步幅的常规卷积层。 -
tf.nn.depthwise_conv2d()创建了一个深度卷积层,它将每个滤波器独立应用于每个输入通道。因此,如果有 f[n] 个滤波器和 f[n][′] 个输入通道,那么这将输出 f[n ]× f[n][′] 个特征图。 -
tf.layers.separable_conv2d()创建了一个可分离卷积层,首先像深度卷积层一样工作,然后对结果特征图应用 1 × 1 的卷积层。这使得可以将滤波器应用于任意输入通道的集合。
训练 CNN
在前一节中,我们已经看到如何构建 CNN 并在其不同层上应用不同操作。现在,当涉及训练 CNN 时,由于需要考虑控制这些操作(如应用适当的激活函数、权重和偏置初始化,当然还有智能使用优化器),这变得更加棘手。
还有一些高级考虑因素,如优化的超参数调整。然而,这将在下一节讨论。我们首先从权重和偏置初始化开始我们的讨论。
权重和偏置初始化
在训练 DNN 中,最常见的初始化技术之一是随机初始化。使用随机初始化的想法是从输入数据集的正态分布中抽样每个权重,具有低偏差。低偏差可以使网络偏向简单的 0 解决方案。
但这意味着什么呢?事实是,初始化可以完成,而不会造成将权重初始化为 0 的坏影响。其次,Xavier 初始化经常用于训练 CNN。它类似于随机初始化,但通常效果要好得多。现在让我解释一下原因:
-
想象一下,您随机初始化网络权重,但它们却开始太小。然后,信号通过每一层时会收缩,直到变得太微小而无用。
-
另一方面,如果网络中的权重开始过大,则信号在通过每一层时会增长,直到变得太大而无用。
好处在于使用 Xavier 初始化确保权重恰到好处,通过许多层保持信号在合理范围内的值。总结一下,它可以根据输入和输出神经元的数量自动确定初始化的比例。
有兴趣的读者应参考这篇论文获取详细信息:Xavier Glorot 和 Yoshua Bengio,《理解训练深度前馈神经网络的困难》,第 13 届人工智能和统计学会议(AISTATS)2010 年,位于意大利撒丁岛的 Chia Laguna Resort。JMLR 的第 9 卷:W&CP。
最后,您可能会问一个聪明的问题,在训练常规的 DNN(例如 MLP 或 DBN)时,我不能摆脱随机初始化吗?嗯,最近,一些研究人员提到了随机正交矩阵初始化,这种初始化比单纯的任意随机初始化效果更好。
- 当涉及初始化偏置时,将偏置初始化为零是可能且常见的,因为权重中的小随机数提供了不对称性破坏。将所有偏置设置为一个小常数值,如 0.01,可以确保所有 ReLU 单元可以传播一些梯度。然而,它既表现不佳,也没有持续改进。因此,建议坚持使用零值。
正则化
有多种方法可以控制 CNN 的训练,以防止在训练阶段出现过拟合。例如,L2/L1 正则化、最大范数约束和 dropout:
-
L2 正则化:这可能是最常见的正则化形式。它可以通过在目标函数中直接惩罚所有参数的平方大小来实现。例如,使用梯度下降更新参数时,L2 正则化最终意味着每个权重都以线性方式衰减:W += -lambda * W 向零靠拢。
-
L1 正则化:这是另一种相对常见的正则化形式,对于每个权重 w,我们将项 λ∣w∣ 加入到目标函数中。然而,也可以将 L1 正则化与 L2 正则化结合起来:λ1∣w∣+λ2w2,这通常被称为 弹性网正则化。
-
最大范数约束:另一种正则化形式是对每个神经元的权重向量的绝对值设定上限,并使用投影梯度下降法来强制实施这一约束。
最后,dropout 是正则化的一种高级变体,稍后将在本章中讨论。
激活函数
激活操作提供了不同类型的非线性函数,用于神经网络中。这些包括平滑的非线性函数,如sigmoid、tanh、elu、softplus和softsign。另一方面,也可以使用一些连续但在某些点不可导的函数,如relu、relu6、crelu和relu_x。所有激活操作都是逐元素应用,并产生与输入张量形状相同的张量。现在,让我们看看如何在 TensorFlow 语法中使用一些常见的激活函数。
使用 sigmoid
在 TensorFlow 中,签名 tf.sigmoid(x, name=None) 按元素计算 x 的 sigmoid 函数,使用 y = 1 / (1 + exp(-x)),并返回一个与 x 类型相同的张量。下面是参数的描述:
-
x:一个张量。它必须是以下类型之一:float32、float64、int32、complex64、int64或qint32。 -
name:操作的名称(可选)。
使用 tanh
在 TensorFlow 中,签名 tf.tanh(x, name=None) 按元素计算 x 的双曲正切,并返回一个与 x 类型相同的张量。下面是参数的描述:
-
x:一个张量或稀疏张量。它的类型可以是float、double、int32、complex64、int64或qint32。 -
name:操作的名称(可选)。
使用 ReLU
在 TensorFlow 中,签名 tf.nn.relu(features, name=None) 计算使用 max(features, 0) 的修正线性函数,并返回一个与 features 类型相同的张量。下面是参数的描述:
-
features:一个张量。它必须是以下类型之一:float32、float64、int32、int64、uint8、int16、int8、uint16和half。 -
name:操作的名称(可选)。
关于如何使用其他激活函数,请参考 TensorFlow 官网。到目前为止,我们已经具备了构建第一个 CNN 网络进行预测的最基础理论知识。
构建、训练和评估我们的第一个 CNN
在下一节中,我们将探讨如何基于原始图像对狗和猫进行分类和区分。我们还将学习如何实现我们的第一个 CNN 模型,以处理具有三个通道的原始彩色图像。这个网络设计和实现并不简单;我们将使用 TensorFlow 的低级 API 来实现。然而,不用担心;在本章的后面,我们将看到如何使用 TensorFlow 的高级 contrib API 实现 CNN 模型。正式开始之前,先简单介绍一下数据集。
数据集描述
在这个例子中,我们将使用 Kaggle 提供的狗与猫数据集,它用于著名的“狗与猫分类”问题,这是一个提供内核支持的竞赛数据集。数据集可以从www.kaggle.com/c/dogs-vs-cats-redux-kernels-edition/data下载。
训练文件夹包含 25,000 张狗和猫的图像。该文件夹中的每个图像的标签是文件名的一部分。测试文件夹包含 12,500 张图像,文件名是数字 ID。对于测试集中的每个图像,您应该预测该图像是狗的概率(1 = 狗,0 = 猫);也就是说,这是一个二分类问题。对于这个例子,有三个 Python 脚本。
步骤 1 – 加载所需的包
在这里,我们导入所需的包和库。请注意,您的导入可能会根据平台不同而有所不同:
import time
import math
import random
import os
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
import Preprocessor
import cv2
import LayersConstructor
from sklearn.metrics import confusion_matrix
from datetime import timedelta
from sklearn.metrics.classification import accuracy_score
from sklearn.metrics import precision_recall_fscore_support
步骤 2 – 加载训练/测试图像以生成训练/测试集
我们将图像的颜色通道数量设置为 3。在前面的章节中,我们已经看到,对于灰度图像,它的通道数量应为 1:
num_channels = 3
为了简化问题,我们假设图像的尺寸应该是正方形的。我们将尺寸设置为128:
img_size = 128
现在我们已经有了图像的大小(即 128)和通道的数量(即 3),图像展开为一维时,图像的大小将是图像尺寸与通道数量的乘积,如下所示:
img_size_flat = img_size * img_size * num_channels
请注意,在后续步骤中,我们可能需要对图像进行重塑,以便适应最大池化层和卷积层,因此我们需要对图像进行重塑。对于我们的情况,它将是一个包含图像高度和宽度的元组,用于重塑数组:
img_shape = (img_size, img_size)
由于我们只有原始彩色图像,并且这些图像没有像其他数字机器学习数据集那样带有标签,因此我们应该明确地定义标签(即类)。让我们如下明确定义类信息:
classes = ['dogs', 'cats']
num_classes = len(classes)
我们需要定义稍后在 CNN 模型上训练的批量大小:
batch_size = 14
请注意,我们还可以定义训练集的哪一部分将作为验证集。为了简便起见,假设使用 16%的数据:
validation_size = 0.16
一个重要的设置是,在验证损失停止改善后,等待多长时间再终止训练。如果我们不想实现早停,应该使用none:
early_stopping = None
现在,下载数据集后,你需要手动做一件事:将狗和猫的图片分开并放入两个不同的文件夹中。具体来说,假设你将训练集放在路径/home/DoG_CaT/data/train/下。在 train 文件夹中,创建两个单独的文件夹dogs和cats,但只显示DoG_CaT/data/train/的路径。我们还假设我们的测试集位于/home/DoG_CaT/data/test/目录中。此外,你可以定义检查点目录,在该目录中将写入日志和模型检查点文件:
train_path = '/home/DoG_CaT/data/train/'
test_path = '/home/DoG_CaT/data/test/'
checkpoint_dir = "models/"
然后我们开始读取训练集并为 CNN 模型做准备。处理测试集和训练集时,我们有另一个脚本Preprocessor.py。不过,最好也准备好测试集:
data = Preprocessor.read_train_sets(train_path, img_size, classes, validation_size=validation_size)
上述代码行读取猫和狗的原始图像并创建训练集。read_train_sets()函数的定义如下:
def read_train_sets(train_path, image_size, classes, validation_size=0):
class DataSets(object):
pass
data_sets = DataSets()
images, labels, ids, cls = load_train(train_path, image_size, classes)
images, labels, ids, cls = shuffle(images, labels, ids, cls)
if isinstance(validation_size, float):
validation_size = int(validation_size * images.shape[0])
validation_images = images[:validation_size]
validation_labels = labels[:validation_size]
validation_ids = ids[:validation_size]
validation_cls = cls[:validation_size]
train_images = images[validation_size:]
train_labels = labels[validation_size:]
train_ids = ids[validation_size:]
train_cls = cls[validation_size:]
data_sets.train = DataSet(train_images, train_labels, train_ids, train_cls)
data_sets.valid = DataSet(validation_images, validation_labels, validation_ids, validation_cls)
return data_sets
在前面的代码段中,我们使用了load_train()方法来加载图像,它是DataSet类的一个实例:
def load_train(train_path, image_size, classes):
images = []
labels = []
ids = []
cls = []
print('Reading training images')
for fld in classes:
index = classes.index(fld)
print('Loading {} files (Index: {})'.format(fld, index))
path = os.path.join(train_path, fld, '*g')
files = glob.glob(path)
for fl in files:
image = cv2.imread(fl)
image = cv2.resize(image, (image_size, image_size), cv2.INTER_LINEAR)
images.append(image)
label = np.zeros(len(classes))
label[index] = 1.0
labels.append(label)
flbase = os.path.basename(fl)
ids.append(flbase)
cls.append(fld)
images = np.array(images)
labels = np.array(labels)
ids = np.array(ids)
cls = np.array(cls)
return images, labels, ids, cls
DataSet类用于生成训练集的批次,其定义如下:
class DataSet(object):
def next_batch(self, batch_size):
"""Return the next `batch_size` examples from this data set."""
start = self._index_in_epoch
self._index_in_epoch += batch_size
if self._index_in_epoch > self._num_examples:
# Finished epoch
self._epochs_completed += 1
start = 0
self._index_in_epoch = batch_size
assert batch_size <= self._num_examples
end = self._index_in_epoch
return self._images[start:end], self._labels[start:end], self._ids[start:end], self._cls[start:end]
然后,类似地,我们从混合的测试图像(狗和猫)中准备测试集:
test_images, test_ids = Preprocessor.read_test_set(test_path, img_size)
我们有read_test_set()函数来简化此过程,代码如下:
def read_test_set(test_path, image_size):
images, ids = load_test(test_path, image_size)
return images, ids
现在,和训练集类似,我们有一个专门的函数load_test()来加载测试集,代码如下:
def load_test(test_path, image_size):
path = os.path.join(test_path, '*g')
files = sorted(glob.glob(path))
X_test = []
X_test_id = []
print("Reading test images")
for fl in files:
flbase = os.path.basename(fl)
img = cv2.imread(fl)
img = cv2.resize(img, (image_size, image_size), cv2.INTER_LINEAR)
X_test.append(img)
X_test_id.append(flbase)
X_test = np.array(X_test, dtype=np.uint8)
X_test = X_test.astype('float32')
X_test = X_test / 255
return X_test, X_test_id
做得好!现在我们可以看到一些随机选取的图像。为此,我们有一个辅助函数plot_images();它创建一个包含 3 x 3 子图的图形。总共会绘制九张图像,并显示它们的真实标签。其代码如下:
def plot_images(images, cls_true, cls_pred=None):
if len(images) == 0:
print("no images to show")
return
else:
random_indices = random.sample(range(len(images)), min(len(images), 9))
images, cls_true = zip(*[(images[i], cls_true[i]) for i in random_indices])
fig, axes = plt.subplots(3, 3)
fig.subplots_adjust(hspace=0.3, wspace=0.3)
for i, ax in enumerate(axes.flat):
# Plot image.
ax.imshow(images[i].reshape(img_size, img_size, num_channels))
if cls_pred is None:
xlabel = "True: {0}".format(cls_true[i])
else:
xlabel = "True: {0}, Pred: {1}".format(cls_true[i], cls_pred[i])
ax.set_xlabel(xlabel)
ax.set_xticks([])
ax.set_yticks([])
plt.show()
让我们从训练集中随机获取一些图像及其标签:
images, cls_true = data.train.images, data.train.cls
最后,我们使用前面代码中的辅助函数绘制图像和标签:
plot_images(images=images, cls_true=cls_true)
上述代码行生成从训练集中随机选取的图像的真实标签:

图 6:从训练集中随机选取的图像的真实标签
最后,我们可以打印数据集的统计信息:
print("Size of:")
print(" - Training-set:tt{}".format(len(data.train.labels)))
print(" - Test-set:tt{}".format(len(test_images)))
print(" - Validation-set:t{}".format(len(data.valid.labels)))
>>>
Reading training images
Loading dogs files (Index: 0)
Loading cats files (Index: 1)
Reading test images
Size of:
- Training-set: 21000
- Test-set: 12500
- Validation-set: 4000
第 3 步 - 定义 CNN 超参数
现在我们有了训练集和测试集,是时候在开始构建 CNN 模型之前定义超参数了。在第一层和第二层卷积层中,我们定义了每个滤波器的宽度和高度,即3,而滤波器的数量是32:
filter_size1 = 3
num_filters1 = 32
filter_size2 = 3
num_filters2 = 32
第三层卷积层的维度相同,但滤波器数量是原来的两倍;也就是64个滤波器:
filter_size3 = 3
num_filters3 = 64
最后两层是全连接层,指定神经元的数量:
fc_size = 128
现在让我们通过设置较低的学习率来使训练变慢,以进行更为密集的训练,如下所示:
learning_rate=1e-4
第 4 步 – 构建 CNN 层
一旦我们定义了 CNN 的超参数,下一步就是实现 CNN 网络。正如你所猜测的,我们的任务将构建一个包含三个卷积层、一个展平层和两个全连接层的 CNN 网络(参见LayersConstructor.py)。此外,我们还需要定义权重和偏置。此外,我们还会有隐式的最大池化层。首先,让我们定义权重。在下面,我们有new_weights()方法,它需要图像形状并返回截断正态形状:
def new_weights(shape):
return tf.Variable(tf.truncated_normal(shape, stddev=0.05))
接着我们使用new_biases()方法来定义偏置:
def new_biases(length):
return tf.Variable(tf.constant(0.05, shape=[length]))
现在我们定义一个方法,new_conv_layer(),用于构建卷积层。该方法接受输入批次、输入通道数、滤波器大小和滤波器数量,并且它还使用最大池化(如果为真,则使用 2 x 2 的最大池化)来构建新的卷积层。该方法的工作流程如下:
-
定义卷积的滤波器权重的形状,这由 TensorFlow API 决定。
-
创建具有给定形状和新偏置的新权重(即滤波器),每个滤波器一个偏置。
-
创建卷积的 TensorFlow 操作,其中步幅在所有维度上都设置为 1。第一个和最后一个步幅必须始终为 1,因为第一个是为了图像编号,最后一个是为了输入通道。例如,strides= (1, 2, 2, 1)意味着滤波器在图像的x轴和y轴上各移动两个像素。
-
将偏置添加到卷积的结果中。然后将偏置值添加到每个滤波器通道中。
-
然后,它使用池化来下采样图像分辨率。这是 2 x 2 的最大池化,意味着我们考虑 2 x 2 的窗口,并在每个窗口中选择最大的值。然后我们将窗口移动两个像素。
-
然后使用 ReLU 来计算每个输入像素x的max(x, 0)。如前所述,ReLU 通常在池化之前执行,但由于
relu(max_pool(x)) == max_pool(relu(x)),我们可以通过先进行最大池化来节省 75%的 ReLU 操作。 -
最后,它返回结果层和滤波器权重,因为我们稍后会绘制权重。
现在我们定义一个函数来构建要使用的卷积层:
def new_conv_layer(input, num_input_channels, filter_size, num_filters, use_pooling=True):
shape = [filter_size, filter_size, num_input_channels, num_filters]
weights = new_weights(shape=shape)
biases = new_biases(length=num_filters)
layer = tf.nn.conv2d(input=input,
filter=weights,
strides=[1, 1, 1, 1],
padding='SAME')
layer += biases
if use_pooling:
layer = tf.nn.max_pool(value=layer,
ksize=[1, 2, 2, 1],
strides=[1, 2, 2, 1],
padding='SAME')
layer = tf.nn.relu(layer)
return layer, weights
下一步是定义展平层:
-
获取输入层的形状。
-
特征数量为
img_height * img_width * num_channels。get_shape()函数在 TensorFlow 中用于计算这一点。 -
然后,它将重塑该层为(
num_images和num_features)。我们只需将第二维的大小设置为num_features,而第一维的大小设置为-1,这意味着在该维度中的大小会被计算出来,以便重塑后张量的总大小不变。 -
最后,它返回展平层和特征数量。
以下代码与之前描述的defflatten_layer(layer)完全相同:
layer_shape = layer.get_shape()
num_features = layer_shape[1:4].num_elements()
layer_flat = tf.reshape(layer, [-1, num_features])
return layer_flat, num_features
最后,我们需要构建全连接层。以下函数new_fc_layer()接受输入批次、批次数和输出数量(即预测的类别)。它使用 ReLU,然后基于我们之前定义的方法创建权重和偏置。最后,它通过输入与权重的矩阵乘法计算该层,并加上偏置值:
def new_fc_layer(input, num_inputs, num_outputs, use_relu=True):
weights = new_weights(shape=[num_inputs, num_outputs])
biases = new_biases(length=num_outputs)
layer = tf.matmul(input, weights) + biases
if use_relu:
layer = tf.nn.relu(layer)
return layer
第 5 步 – 准备 TensorFlow 图
现在我们为 TensorFlow 图创建占位符:
x = tf.placeholder(tf.float32, shape=[None, img_size_flat], name='x')
x_image = tf.reshape(x, [-1, img_size, img_size, num_channels])
y_true = tf.placeholder(tf.float32, shape=[None, num_classes], name='y_true')
y_true_cls = tf.argmax(y_true, axis=1)
第 6 步 – 创建 CNN 模型
现在我们有了输入;即x_image,它已经准备好输入到卷积层。我们正式创建卷积层,后接最大池化:
layer_conv1, weights_conv1 =
LayersConstructor.new_conv_layer(input=x_image,
num_input_channels=num_channels,
filter_size=filter_size1,
num_filters=num_filters1,
use_pooling=True)
我们必须有第二个卷积层,其中输入是第一个卷积层layer_conv1,后接最大池化:
layer_conv2, weights_conv2 =
LayersConstructor.new_conv_layer(input=layer_conv1,
num_input_channels=num_filters1,
filter_size=filter_size2,
num_filters=num_filters2,
use_pooling=True)
现在我们有了第三个卷积层,其中输入是第二个卷积层的输出,即layer_conv2,后面接着最大池化:
layer_conv3, weights_conv3 =
LayersConstructor.new_conv_layer(input=layer_conv2,
num_input_channels=num_filters2,
filter_size=filter_size3,
num_filters=num_filters3,
use_pooling=True)
一旦第三个卷积层实例化,我们接着实例化平坦化层,如下所示:
layer_flat, num_features = LayersConstructor.flatten_layer(layer_conv3)
一旦我们将图像进行平坦化处理,它们就可以输入到第一个全连接层。我们使用 ReLU:
layer_fc1 = LayersConstructor.new_fc_layer(input=layer_flat,
num_inputs=num_features,
num_outputs=fc_size,
use_relu=True)
最后,我们需要第二个也是最后一个全连接层,其中输入是第一个全连接层的输出:
layer_fc2 = LayersConstructor.new_fc_layer(input=layer_fc1,
num_inputs=fc_size,
num_outputs=num_classes,
use_relu=False)
第 7 步 – 运行 TensorFlow 图以训练 CNN 模型
接下来的步骤用于执行训练。代码与我们之前示例中使用的代码一样,易于理解。我们使用 softmax 通过与真实类别进行比较来预测类别:
y_pred = tf.nn.softmax(layer_fc2)
y_pred_cls = tf.argmax(y_pred, axis=1)
cross_entropy = tf.nn.softmax_cross_entropy_with_logits_v2(logits=layer_fc2, labels=y_true)
我们定义cost函数,然后是优化器(在这里使用 Adam 优化器)。接着我们计算准确率:
cost_op= tf.reduce_mean(cross_entropy)
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cost_op)
correct_prediction = tf.equal(y_pred_cls, y_true_cls)
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
然后我们使用 TensorFlow 的global_variables_initializer()函数初始化所有操作:
init_op = tf.global_variables_initializer()
然后我们创建并运行 TensorFlow 会话,执行跨张量的训练:
session = tf.Session()
session.run(init_op)
然后我们输入训练数据,将批量大小设置为 32(参见步骤 2):
train_batch_size = batch_size
我们保持两个列表来跟踪训练和验证的准确性:
acc_list = []
val_acc_list = []
然后我们计算到目前为止已执行的总迭代次数,并创建一个空列表来跟踪所有迭代:
total_iterations = 0
iter_list = []
我们通过调用optimize()函数正式开始训练,该函数需要指定若干迭代次数。它需要两个参数:
-
x_batch是训练样本,其中包含一批图像 -
y_true_batch,这些图像的真实标签
它将每张图像的形状从(num个示例,行,列,深度)转换为(num个示例,平坦化图像形状)。之后,我们将批次放入 TensorFlow 图的dict占位符变量中。接下来,我们在训练数据批次上运行优化器。
然后,TensorFlow 将 feed_dict_train 中的变量分配给占位符变量。接着执行优化器,在每个 epoch 结束时打印状态。最后,更新我们已执行的总迭代次数:
def optimize(num_iterations):
global total_iterations
best_val_loss = float("inf")
patience = 0
for i in range(total_iterations, total_iterations + num_iterations):
x_batch, y_true_batch, _, cls_batch = data.train.next_batch(train_batch_size)
x_valid_batch, y_valid_batch, _, valid_cls_batch = data.valid.next_batch(train_batch_size)
x_batch = x_batch.reshape(train_batch_size, img_size_flat)
x_valid_batch = x_valid_batch.reshape(train_batch_size, img_size_flat)
feed_dict_train = {x: x_batch, y_true: y_true_batch}
feed_dict_validate = {x: x_valid_batch, y_true: y_valid_batch}
session.run(optimizer, feed_dict=feed_dict_train)
if i % int(data.train.num_examples/batch_size) == 0:
val_loss = session.run(cost, feed_dict=feed_dict_validate)
epoch = int(i / int(data.train.num_examples/batch_size))
acc, val_acc = print_progress(epoch, feed_dict_train, feed_dict_validate, val_loss)
acc_list.append(acc)
val_acc_list.append(val_acc)
iter_list.append(epoch+1)
if early_stopping:
if val_loss < best_val_loss:
best_val_loss = val_loss
patience = 0
else:
patience += 1
if patience == early_stopping:
break
total_iterations += num_iterations
我们将在下一部分展示我们的训练进展。
步骤 8 – 模型评估
我们已经完成了训练。现在是时候评估模型了。在开始评估模型之前,我们先实现一些辅助函数,用于绘制示例错误并打印验证准确率。plot_example_errors() 接受两个参数,第一个是 cls_pred,它是一个数组,包含测试集中所有图像的预测类别编号。
第二个参数 correct 是一个 boolean 数组,用于预测每个测试集中的图像的预测类别是否与 true 类别相等。首先,它获取测试集中分类错误的图像。然后,它获取这些图像的预测类别和真实类别,最后绘制前九张图像及其类别(即,预测类别与真实标签的对比):
def plot_example_errors(cls_pred, correct):
incorrect = (correct == False)
images = data.valid.images[incorrect]
cls_pred = cls_pred[incorrect]
cls_true = data.valid.cls[incorrect]
plot_images(images=images[0:9], cls_true=cls_true[0:9], cls_pred=cls_pred[0:9])
第二个辅助函数叫做 print_validation_accuracy(),它打印验证准确率。该函数为预测类别分配一个数组,这些预测类别将在批次中计算并填充到此数组中,接着计算每个批次的预测类别:
def print_validation_accuracy(show_example_errors=False, show_confusion_matrix=False):
num_test = len(data.valid.images)
cls_pred = np.zeros(shape=num_test, dtype=np.int)
i = 0
while i < num_test:
# The ending index for the next batch is denoted j.
j = min(i + batch_size, num_test)
images = data.valid.images[i:j, :].reshape(batch_size, img_size_flat)
labels = data.valid.labels[i:j, :]
feed_dict = {x: images, y_true: labels}
cls_pred[i:j] = session.run(y_pred_cls, feed_dict=feed_dict)
i = j
cls_true = np.array(data.valid.cls)
cls_pred = np.array([classes[x] for x in cls_pred])
correct = (cls_true == cls_pred)
correct_sum = correct.sum()
acc = float(correct_sum) / num_test
msg = "Accuracy on Test-Set: {0:.1%} ({1} / {2})"
print(msg.format(acc, correct_sum, num_test))
if show_example_errors:
print("Example errors:")
plot_example_errors(cls_pred=cls_pred, correct=correct)
现在我们已经有了辅助函数,可以开始优化。在第一步,先让我们对微调进行 10,000 次迭代,看看性能如何:
optimize(num_iterations=1000)
经过 10,000 次迭代后,我们观察到以下结果:
Accuracy on Test-Set: 78.8% (3150 / 4000)
Precision: 0.793378626929
Recall: 0.7875
F1-score: 0.786639298213
这意味着测试集上的准确率大约为 79%。此外,让我们看看我们的分类器在一张示例图像上的表现如何:

图 7:在测试集上的随机预测(经过 10,000 次迭代)
之后,我们进一步将优化迭代至 100,000 次,并观察到更好的准确率:

图 8:在测试集上的随机预测(经过 100,000 次迭代)
>>>
Accuracy on Test-Set: 81.1% (3244 / 4000)
Precision: 0.811057239265
Recall: 0.811
F1-score: 0.81098298755
所以它的改进不大,但整体准确率提高了 2%。现在是时候对我们的模型进行单张图片的评估了。为了简单起见,我们将随机选取一只狗和一只猫的图片,看看我们的模型的预测能力:

图 9:待分类的猫和狗的示例图像
首先,我们加载这两张图片,并相应地准备测试集,正如我们在本示例的前面步骤中所看到的:
test_cat = cv2.imread('Test_image/cat.jpg')
test_cat = cv2.resize(test_cat, (img_size, img_size), cv2.INTER_LINEAR) / 255
preview_cat = plt.imshow(test_cat.reshape(img_size, img_size, num_channels))
test_dog = cv2.imread('Test_image/dog.jpg')
test_dog = cv2.resize(test_dog, (img_size, img_size), cv2.INTER_LINEAR) / 255
preview_dog = plt.imshow(test_dog.reshape(img_size, img_size, num_channels))
然后,我们有以下函数来进行预测:
def sample_prediction(test_im):
feed_dict_test = {
x: test_im.reshape(1, img_size_flat),
y_true: np.array([[1, 0]])
}
test_pred = session.run(y_pred_cls, feed_dict=feed_dict_test)
return classes[test_pred[0]]
print("Predicted class for test_cat: {}".format(sample_prediction(test_cat)))
print("Predicted class for test_dog: {}".format(sample_prediction(test_dog)))
>>>
Predicted class for test_cat: cats
Predicted class for test_dog: dogs
最后,当我们完成后,通过调用 close() 方法关闭 TensorFlow 会话:
session.close()
模型性能优化
由于卷积神经网络(CNN)与传统的分层结构不同,它们有不同的要求和调优标准。那么,如何知道哪种超参数组合最适合你的任务呢?当然,你可以使用网格搜索和交叉验证来找到线性机器学习模型的最佳超参数。
然而,对于 CNN,存在许多需要调优的超参数,而且在大数据集上训练神经网络需要大量时间,因此你只能在合理的时间内探索超参数空间的一小部分。以下是一些可以遵循的见解。
隐藏层的数量
对于许多问题,你可以从一个隐藏层开始,通常就能得到合理的结果。实际上,研究表明,仅有一个隐藏层的多层感知机(MLP)只要神经元足够多,甚至可以建模最复杂的函数。长时间以来,这些事实让研究人员相信不需要进一步研究更深层的神经网络。然而,他们忽略了深层网络比浅层网络具有更高的参数效率;深层网络能用指数级更少的神经元建模复杂的函数,使得训练速度大大加快。
需要注意的是,这并非总是适用。然而,总的来说,对于许多问题,你可以从一个或两个隐藏层开始。使用相同数量的神经元,两个隐藏层通常也能取得良好的效果,而且大致相同的训练时间。对于更复杂的问题,你可以逐步增加隐藏层的数量,直到开始过拟合训练集。非常复杂的任务,如大规模图像分类或语音识别,通常需要数十层的网络和大量的训练数据。
每个隐藏层的神经元数量
显然,输入层和输出层的神经元数量由任务所需的输入和输出类型决定。例如,如果你的数据集形状为 28 x 28,它应该有 784 个输入神经元,而输出神经元数量应等于要预测的类别数。至于隐藏层,一种常见的做法是将它们的规模设计成漏斗形状,每一层的神经元数量逐渐减少,理由是许多低级特征可以合并成更少的高级特征。然而,现在这种做法已经不再那么常见,你也可以简单地为所有隐藏层使用相同的大小。
如果有四个卷积层,每个卷积层有 256 个神经元,那么就只有一个超参数需要调优,而不是每个层都调优一个。就像隐藏层的数量一样,你可以逐渐增加神经元数量,直到网络开始过拟合。另一个重要的问题是:你何时需要添加最大池化层,而不是使用步幅相同的卷积层?问题在于,最大池化层没有任何参数,而卷积层有很多参数。
有时,添加一个局部响应归一化层,可以使得最强激活的神经元抑制在同一位置但在相邻特征图中的神经元,从而鼓励不同的特征图进行专业化,并推动它们分开,迫使它们探索更广泛的特征范围。通常,它在较低层使用,以便有更多的低级特征供上层构建。
批量归一化
批量归一化(BN)是一种在训练常规 DNN 时减少内部协变量偏移的方法。这也适用于 CNN。由于归一化,BN 进一步防止了参数的小变化被放大,从而允许更高的学习率,使网络更快:

这个想法是在各层之间加入一个额外的步骤,其中前一层的输出会被规范化。更具体来说,在非线性操作的情况下(例如,ReLU),必须对非线性操作应用 BN 变换。通常,整个过程的工作流如下:
-
将网络转换为 BN 网络(见图 1)
-
然后训练新的网络
-
将批量统计数据转换为总体统计数据
这样,BN 可以充分参与反向传播过程。如图 1所示,BN 在应用该层网络的其他过程之前就已经执行。然而,任何类型的梯度下降(例如,随机梯度下降(SGD)及其变种)都可以应用于训练 BN 网络。
感兴趣的读者可以参考原始论文以获取更多信息:Ioffe, Sergey, 和 Christian Szegedy. Batch normalization: Accelerating deep network training by reducing internal covariate shift. arXiv 预印本 arXiv:1502.03167 (2015).
现在一个合理的问题是:BN 层应该放在哪里?好吧,为了知道答案,可以快速评估 BatchNorm 层在 ImageNet-2012 上的表现(github.com/ducha-aiki/caffenet-benchmark/blob/master/batchnorm.md),得到如下基准测试结果:

从前面的表格可以看出,将 BN 放置在非线性操作之后是正确的做法。第二个问题是:BN 层应该使用什么激活函数?好吧,从相同的基准测试中,我们可以看到以下结果:

从前面的表格来看,我们可以假设使用 ReLU 或其变种会是一个更好的选择。现在,另一个问题是如何在深度学习库中使用这些方法。在 TensorFlow 中,它是:
training = tf.placeholder(tf.bool)
x = tf.layers.dense(input_x, units=100)
x = tf.layers.batch_normalization(x, training=training)
x = tf.nn.relu(x)
一般警告:将此设置为True用于训练,设置为False用于测试。然而,前面的添加会引入额外的操作,这些操作会在图中更新其均值和方差变量,以确保它们不会成为训练操作的依赖项。为此,我们可以像下面这样单独运行这些操作:
extra_update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
sess.run([train_op, extra_update_ops], ...)
高级正则化与避免过拟合
如前一章节所述,在训练大规模神经网络时观察到的主要缺点之一是过拟合,即对于训练数据生成了非常好的近似,但对于单个点之间的区域则发出噪声。减少或甚至避免这个问题有几种方法,比如 dropout、提前停止和限制参数数量。
在过拟合的情况下,模型被特别调整以适应训练数据集,因此它不能用于泛化。因此,尽管它在训练集上表现良好,但在测试数据集及随后的测试中表现较差,因为它缺乏泛化能力。

图 10:使用 dropout 与不使用 dropout 的对比
这种方法的主要优势在于,它避免了同步地优化层中所有神经元的权重。通过随机分组进行的这种适应性调整,防止了所有神经元趋向于相同的目标,从而去相关化了适应的权重。在 dropout 应用中发现的第二个特点是,隐藏单元的激活变得稀疏,这也是一个理想的特性。
在前面的图中,我们展示了一个原始的全连接多层神经网络及其与 dropout 连接的相关网络。因此,大约一半的输入被置为零(此示例选择了这个情况,以展示概率并不总是给出预期的四个零)。一个可能让你感到意外的因素是,应用于未被丢弃元素的缩放因子。
该技术用于保持相同的网络,在训练时将其恢复为原始架构,并使用dropout_keep_prob设置为 1。使用 dropout 的一个主要缺点是,它对卷积层没有相同的效果,因为卷积层中的神经元不是全连接的。为了解决这个问题,可以应用一些技术,如 DropConnect 和随机池化:
-
DropConnect 与 dropout 类似,它在模型中引入了动态稀疏性,但不同之处在于,稀疏性作用于权重,而不是层的输出向量。关键是,带有 DropConnect 的全连接层变成了一个稀疏连接层,其中连接在训练阶段是随机选择的。
-
在随机池化中,常规的确定性池化操作被一个随机过程所替代,在这个过程中,每个池化区域内的激活值都是根据池化区域内的活动,通过多项分布随机选择的。这种方法不依赖于超参数,可以与其他正则化方法结合使用,如 dropout 和数据增强。
随机池化与标准最大池化: 随机池化等价于标准的最大池化,但它使用了多个输入图像副本,每个副本具有小的局部变形。
其次,防止网络过拟合的最简单方法之一是简单地在过拟合有机会发生之前停止训练。这样做的缺点是学习过程会被暂停。第三,限制参数的数量有时是有帮助的,有助于避免过拟合。对于 CNN 训练来说,滤波器大小也会影响参数的数量。因此,限制这类参数直接限制了网络的预测能力,减少了它对数据执行的函数的复杂性,从而限制了过拟合的程度。
在 TensorFlow 中应用 dropout 操作
如果我们对一个样本向量应用 dropout 操作,它将作用于将 dropout 传递给所有依赖于架构的单元。为了应用 dropout 操作,TensorFlow 实现了 tf.nn.dropout 方法,其工作原理如下:
tf.nn.dropout (x, keep_prob, noise_shape, seed, name)
其中 x 是原始张量。keep_prob 表示保留神经元的概率,以及剩余节点被乘的因子。noise_shape 表示一个四个元素的列表,用于确定某个维度是否会独立应用零化操作。让我们看一下这段代码:
import tensorflow as tf X = [1.5, 0.5, 0.75, 1.0, 0.75, 0.6, 0.4, 0.9]
drop_out = tf.nn.dropout(X, 0.5)
sess = tf.Session() with sess.as_default():
print(drop_out.eval())
sess.close()
[ 3\. 0\. 1.5 0\. 0\. 1.20000005 0\. 1.79999995]
在前面的示例中,你可以看到将 dropout 应用到 x 变量的结果,零的概率为 0.5;在没有发生的情况下,值被加倍(乘以 1/1.5,dropout 概率)。
使用哪个优化器?
在使用 CNN 时,由于目标函数之一是最小化评估的成本,我们必须定义一个优化器。使用最常见的优化器,如 SGD,学习率必须按 1/T 的比例缩放才能收敛,其中 T 是迭代次数。Adam 或 RMSProp 试图通过自动调整步长来克服这一限制,从而使步长与梯度的尺度相同。此外,在前面的示例中,我们使用了 Adam 优化器,它在大多数情况下表现良好。
然而,如果你正在训练一个神经网络并且计算梯度是必需的,使用RMSPropOptimizer函数(该函数实现了RMSProp算法)是一个更好的选择,因为它是小批量设置下学习的更快速方法。研究人员还建议在训练深度 CNN 或 DNN 时使用动量优化器。技术上,RMSPropOptimizer是梯度下降的高级形式,它通过一个指数衰减的梯度平方均值来调整学习率。建议的衰减参数设置值是 0.9,而学习率的一个良好默认值是 0.001。例如,在 TensorFlow 中,tf.train.RMSPropOptimizer()帮助我们轻松使用此方法:
optimizer = tf.train.RMSPropOptimizer(0.001, 0.9).minimize(cost_op)
内存调优
在本节中,我们尝试提供一些见解。我们从一个问题及其解决方案开始;卷积层需要大量的内存,尤其是在训练期间,因为反向传播的反向传递需要保留在前向传递中计算的所有中间值。在推理期间(即对新实例进行预测时),一个层占用的内存可以在下一个层计算完成后立即释放,因此你只需要两层连续层所需的内存。
然而,在训练期间,前向传递中计算的所有内容需要保留以供反向传递使用,因此所需的内存量(至少)是所有层所需的总内存。如果在训练 CNN 时 GPU 内存不足,这里有五个你可以尝试解决问题的方法(除了购买更大内存的 GPU):
-
减小小批量大小
-
使用更大的步幅在一层或多层中降低维度
-
删除一层或多层
-
使用 16 位浮点数而不是 32 位
-
将 CNN 分布到多个设备上(更多内容请见
www.tensorflow.org/deploy/distributed)
合适的层次放置
另一个重要问题是:你何时希望添加最大池化层,而不是具有相同步幅的卷积层?问题在于,最大池化层完全没有参数,而卷积层有相当多的参数。
即使添加一个局部响应归一化层,有时也会导致最强激活的神经元抑制相同位置但在邻近特征图上的神经元,这鼓励不同的特征图进行专业化并将它们推开,迫使它们探索更广泛的特征。它通常用于低层,以便提供更大的低级特征池,供高层构建。
通过将所有内容组合起来构建第二个 CNN
现在我们知道如何通过添加 dropout、BN 和偏置初始化器(如 Xavier)来优化 CNN 中的分层结构。让我们尝试将这些应用于一个较简单的 CNN。在这个例子中,我们将看到如何解决一个实际的分类问题。更具体地说,我们的 CNN 模型将能够从一堆图像中分类交通标志。
数据集描述和预处理
为此,我们将使用比利时交通数据集(比利时交通标志分类数据集,裁剪图像)。这个数据集可以从 btsd.ethz.ch/shareddata/ 下载。以下是比利时交通标志的概览:
-
比利时的交通标志通常使用荷兰语和法语。这是一个值得了解的信息,但对于你将要处理的数据集来说,这一点并不是特别重要!
-
比利时的交通标志分为六类:警告标志、优先标志、禁止标志、强制标志、与停车和路面停靠相关的标志,以及最后的标识性标志。
一旦我们下载了上述数据集,我们将看到以下的目录结构(左侧为训练集,右侧为测试集):

这些图像是 .ppm 格式的;否则我们可以使用 TensorFlow 内置的图像加载器(例如 tf.image.decode_png)。不过,我们可以使用 skimage Python 包。
在 Python 3 中,执行 $ sudo pip3 install scikit-image 来安装 skimage 包并使用它。所以让我们从显示如下的目录路径开始:
Train_IMAGE_DIR = "<path>/BelgiumTSC_Training/"
Test_IMAGE_DIR = ""<path>/BelgiumTSC_Testing/"
接着,让我们写一个使用 skimage 库的函数,读取图像并返回两个列表:
-
images: 一个包含 Numpy 数组的列表,每个数组表示一张图像 -
labels: 一个数字列表,表示图像的标签
def load_data(data_dir):
# All subdirectories, where each folder represents a unique label
directories = [d for d in os.listdir(data_dir)if os.path.isdir(os.path.join(data_dir, d))]
# Iterate label directories and collect data in two lists, labels and images.
labels = []
images = []
for d in directories:label_dir = os.path.join(data_dir, d)
file_names = [os.path.join(label_dir, f)
for f in os.listdir(label_dir) if f.endswith(".ppm")]
# For each label, load it's images and add them to the images list.
# And add the label number (i.e. directory name) to the labels list.
for f in file_names:images.append(skimage.data.imread(f))
labels.append(int(d))
return images, labels
上面的代码块很直观,并包含内联注释。如何显示与图像相关的统计信息呢?不过,在此之前,让我们调用上面的函数:
# Load training and testing datasets.
train_data_dir = os.path.join(Train_IMAGE_DIR, "Training")
test_data_dir = os.path.join(Test_IMAGE_DIR, "Testing")
images, labels = load_data(train_data_dir)
然后让我们查看一些统计信息:
print("Unique classes: {0} \nTotal Images: {1}".format(len(set(labels)), len(images)))
>>>
Unique classes: 62
Total Images: 4575
因此,我们有 62 个类别需要预测(也就是一个多类别图像分类问题),而且我们也有许多图像,这应该足以满足一个较小的 CNN。现在让我们来直观地查看类别分布:
# Make a histogram with 62 bins of the `labels` data and show the plot:
plt.hist(labels, 62)
plt.xlabel('Class')
plt.ylabel('Number of training examples')
plt.show()

因此,从前面的图示中,我们可以看到类别非常不平衡。然而,为了简化问题,我们不打算处理这一点,接下来,我们可以通过可视化检查一些文件,例如显示每个标签的第一张图片:
def display_images_and_labels(images, labels):
unique_labels = set(labels)
plt.figure(figsize=(15, 15))
i = 1
for label in unique_labels:
# Pick the first image for each label.
image = images[labels.index(label)]
plt.subplot(8, 8, i) # A grid of 8 rows x 8 column
splt.axis('off')
plt.title("Label {0} ({1})".format(label, labels.count(label)))
i += 1
_= plt.imshow(image)
plt.show()
display_images_and_labels(images, labels)

现在你可以从前面的图示中看到,这些图像的大小和形状各不相同。此外,我们还可以通过 Python 代码看到这一点,如下所示:
for img in images[:5]:
print("shape: {0}, min: {1}, max: {2}".format(img.shape, img.min(), img.max()))
>>>
shape: (87, 84, 3), min: 12, max: 255
shape: (289, 169, 3), min: 0, max: 255
shape: (205, 76, 3), min: 0, max: 255
shape: (72, 71, 3), min: 14, max: 185
shape: (231, 228, 3), min: 0, max: 255
因此,我们需要对每张图像进行一些预处理,比如调整大小、重塑等等。假设每张图像的大小为 32 x 32:
images32 = [skimage.transform.resize(img, (32, 32), mode='constant')
for img in images]for img in images32[:5]:
print("shape: {0}, min: {1}, max: {2}".format(img.shape, img.min(), img.max()))
>>>
shape: (32, 32, 3), min: 0.06642539828431372, max: 0.9704350490196079
shape: (32, 32, 3), min: 0.0, max: 1.0
shape: (32, 32, 3), min: 0.03172870710784261, max: 1.0
shape: (32, 32, 3), min: 0.059474571078431314, max: 0.7036305147058846
shape: (32, 32, 3), min: 0.01506204044117481, max: 1.0
现在,我们的所有图像都有相同的大小。接下来的任务是将标签和图像特征转换为 numpy 数组:
labels_array = np.array(labels)
images_array = np.array(images32)
print("labels: ", labels_array.shape, "nimages: ", images_array.shape)
>>>
labels: (4575,)
images: (4575, 32, 32, 3)
太棒了!接下来的任务将是创建我们的第二个 CNN,但这次我们将使用 TensorFlow contrib 包,这是一个支持层操作的高级 API。
创建 CNN 模型
我们即将构建一个复杂的网络。然而,它具有简单的架构。首先,我们使用 Xavier 初始化网络。初始化网络偏置后,输入层后跟卷积层(卷积层 1),然后是 BN 层(即 BN 层 1)。然后是步幅为两个、核大小为两个的池化层。接着第二个卷积层后跟另一个 BN 层。接下来是步幅为两个、核大小为两个的第二个池化层。然后是最大池化层,后面是一个将输入从(None,高度,宽度,通道)扁平化为(None,高度 * 宽度 * 通道)==(None,3072)的扁平化层。
扁平化完成后,输入被送入第一个全连接层 1。然后第三个 BN 被应用为正常化函数。然后在将轻网络馈送到生成大小为(None,62)的 logits 的第二个全连接层 2 之前,我们将有一个 dropout 层。太多了吗?别担心,我们将一步步看到它。让我们从创建计算图开始编码,创建特征和标签占位符:
graph = tf.Graph()
with graph.as_default():
# Placeholders for inputs and labels.
images_X = tf.placeholder(tf.float32, [None, 32, 32, 3]) # each image's 32x32 size
labels_X = tf.placeholder(tf.int32, [None])
# Initializer: Xavier
biasInit = tf.contrib.layers.xavier_initializer(uniform=True, seed=None, dtype=tf.float32)
# Convolution layer 1: number of neurons 128 and kernel size is 6x6.
conv1 = tf.contrib.layers.conv2d(images_X, num_outputs=128, kernel_size=[6, 6],
biases_initializer=biasInit)
# Batch normalization layer 1: can be applied as a normalizer
# function for conv2d and fully_connected
bn1 = tf.contrib.layers.batch_norm(conv1, center=True, scale=True, is_training=True)
# Max Pooling (down sampling) with strides of 2 and kernel size of 2
pool1 = tf.contrib.layers.max_pool2d(bn1, 2, 2)
# Convolution layer 2: number of neurons 256 and kernel size is 6x6.
conv2 = tf.contrib.layers.conv2d(pool1, num_outputs=256, kernel_size=[4, 4], stride=2,
biases_initializer=biasInit)
# Batch normalization layer 2:
bn2 = tf.contrib.layers.batch_norm(conv2, center=True, scale=True, is_training=True)
# Max Pooling (down-sampling) with strides of 2 and kernel size of 2
pool2 = tf.contrib.layers.max_pool2d(bn2, 2, 2)
# Flatten the input from [None, height, width, channels] to
# [None, height * width * channels] == [None, 3072]
images_flat = tf.contrib.layers.flatten(pool2)
# Fully connected layer 1
fc1 = tf.contrib.layers.fully_connected(images_flat, 512, tf.nn.relu)
# Batch normalization layer 3
bn3 = tf.contrib.layers.batch_norm(fc1, center=True, scale=True, is_training=True)
# apply dropout, if is_training is False, dropout is not applied
fc1 = tf.layers.dropout(bn3, rate=0.25, training=True)
# Fully connected layer 2 that generates logits of size [None, 62].
# Here 62 means number of classes to be predicted.
logits = tf.contrib.layers.fully_connected(fc1, 62, tf.nn.relu)
到目前为止,我们已经成功生成了大小为(None, 62)的 logits。然后,我们需要将 logits 转换为标签索引(int),形状为(None),即一个长度为 batch_size 的 1D 向量:predicted_labels = tf.argmax(logits, axis=1)。然后我们定义交叉熵作为 loss 函数,这对于分类是一个不错的选择:
loss_op = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits, labels=labels_X))
现在,其中一个最重要的部分是更新操作并创建优化器(在我们的案例中是 Adam):
update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
with tf.control_dependencies(update_ops):
# Create an optimizer, which acts as the training op.train =
tf.train.AdamOptimizer(learning_rate=0.10).minimize(loss_op)
最后,我们初始化所有操作:
init_op = tf.global_variables_initializer()
训练和评估网络
我们开始创建一个会话来运行我们创建的图形。请注意,为了更快地训练,我们应该使用 GPU。但如果您没有 GPU,只需设置 log_device_placement=False:
session = tf.Session(graph=graph, config=tf.ConfigProto(log_device_placement=True))
session.run(init_op)
for i in range(300):
_, loss_value = session.run([train, loss_op], feed_dict={images_X: images_array, labels_X:
labels_array})
if i % 10 == 0:
print("Loss: ", loss_value)
>>>
Loss: 4.7910895
Loss: 4.3410876
Loss: 4.0275432
...
Loss: 0.523456
训练完成后,让我们随机挑选 10 张图片,看看我们模型的预测能力:
random_indexes = random.sample(range(len(images32)), 10)
random_images = [images32[i]
for i in random_indexes]
random_labels = [labels[i]
for i in random_indexes]
然后让我们运行 predicted_labels op:
predicted = session.run([predicted_labels], feed_dict={images_X: random_images})[0]
print(random_labels)
print(predicted)
>>>
[38, 21, 19, 39, 22, 22, 45, 18, 22, 53]
[20 21 19 51 22 22 45 53 22 53]
我们可以看到,一些图片被正确分类了,而一些则错误分类了。然而,视觉检查可能会更有帮助。因此,让我们显示预测和实际情况:
fig = plt.figure(figsize=(5, 5))
for i in range(len(random_images)):
truth = random_labels[i]
prediction = predicted[i]
plt.subplot(5, 2,1+i)
plt.axis('off')color='green'
if truth == prediction
else
'red'plt.text(40, 10, "Truth: {0}nPrediction: {1}".format(truth, prediction), fontsize=12,
color=color)
plt.imshow(random_images[i])
>>>

最后,我们可以使用测试集来评估我们的模型。为了看到预测能力,我们计算准确率:
# Load the test dataset.
test_X, test_y = load_data(test_data_dir)
# Transform the images, just as we did with the training set.
test_images32 = [skimage.transform.resize(img, (32, 32), mode='constant')
for img in test_X]
display_images_and_labels(test_images32, test_y)
# Run predictions against the test
setpredicted = session.run([predicted_labels], feed_dict={images_X: test_images32})[0]
# Calculate how many matches
match_count = sum([int(y == y_) for y, y_ in zip(test_y, predicted)])
accuracy = match_count / len(test_y)print("Accuracy: {:.3f}".format(accuracy))
>>
Accuracy: 87.583
就准确性而言,并不算糟糕。除此之外,我们还可以计算其他性能指标,如精确度、召回率、F1 值,并将结果可视化为混淆矩阵,以展示预测与实际标签的计数。不过,通过调整网络和超参数,我们仍然可以提高准确性。但这些工作留给读者来完成。
最后,我们完成了任务,接下来让我们关闭 TensorFlow 会话:
session.close()
总结
在本章中,我们讨论了如何使用 CNN(卷积神经网络),它是一种前馈式人工神经网络,神经元之间的连接模式受到动物视觉皮层组织的启发。我们学习了如何级联一组层来构建 CNN,并在每一层执行不同的操作。接着我们讲解了如何训练 CNN。随后,我们讨论了如何优化 CNN 的超参数和优化方法。
最后,我们构建了另一个 CNN,并在其中应用了所有优化技术。我们的 CNN 模型未能达到出色的准确率,因为我们只对这两个 CNN 进行了几次迭代,甚至没有使用网格搜索技术;这意味着我们没有寻找超参数的最佳组合。因此,收获的经验是:需要在原始图像中应用更强大的特征工程,使用最佳超参数进行更多轮的训练,并观察性能表现。
在下一章中,我们将学习如何使用一些更深层次且流行的 CNN 架构,例如 ImageNet、AlexNet、VGG、GoogLeNet 和 ResNet。我们将了解如何利用这些训练好的模型进行迁移学习。
第四章:流行的 CNN 模型架构
在本章中,我们将介绍 ImageNet 图像数据库,并讨论以下流行 CNN 模型的架构:
-
LeNet
-
AlexNet
-
VGG
-
GoogLeNet
-
ResNet
ImageNet 介绍
ImageNet 是一个包含超过 1500 万张手工标注的高分辨率图像的数据库,涵盖大约 22,000 个类别。该数据库的组织结构类似于 WordNet 层次结构,其中每个概念也被称为 同义集(即 synset)。每个同义集都是 ImageNet 层次结构中的一个节点。每个节点包含超过 500 张图像。
ImageNet 大规模视觉识别挑战赛 (ILSVRC) 于 2010 年成立,旨在大规模提升物体检测和图像分类的最先进技术:

在概述了 ImageNet 之后,我们将进一步介绍各种 CNN 模型架构。
LeNet
2010 年,ImageNet 发起了一项挑战(称为 ILSVRC 2010),该挑战使用了 Yann Lecun 构建的 CNN 架构 LeNet 5。该网络以 32 x 32 的图像作为输入,图像经过卷积层(C1),然后进入子采样层(S2)。今天,子采样层已被池化层替代。接着,又有一系列卷积层(C3),然后是一个池化(即子采样)层(S4)。最后,网络有三个全连接层,其中包括最后的 OUTPUT 层。该网络曾用于邮局的邮政编码识别。从那时起,每年都有不同的 CNN 架构通过这一竞赛得以推出:

LeNet 5 – 来自 Yann Lecun 1998 年文章的 CNN 架构
因此,我们可以得出以下几点结论:
-
该网络的输入为一张灰度 32 x 32 的图像
-
实现的架构是一个 CONV 层,接着是 POOL 层和一个全连接层
-
CONV 滤波器为 5 x 5,以步幅为 1 应用
AlexNet 架构
CNN 架构的第一个突破发生在 2012 年。这一获奖的 CNN 架构被称为 AlexNet,由多伦多大学的 Alex Krizhevsky 和他的教授 Jeffry Hinton 开发。
在第一次运行时,该网络使用了 ReLU 激活函数和 0.5 的 dropout 来抵抗过拟合。如以下图所示,架构中使用了一个归一化层,但由于使用了大量的数据增强技术,这在实际应用中已不再使用。尽管如今已有更为精确的网络,但由于其相对简单的结构和较小的深度,AlexNet 仍然在今天被广泛使用,尤其是在计算机视觉领域:

AlexNet 使用两个独立的 GPU 在 ImageNet 数据库上进行训练,可能是由于当时 GPU 间连接的处理限制,正如下图所示:

使用 AlexNet 的交通标志分类器
在这个例子中,我们将使用迁移学习进行特征提取,并使用一个德国交通标志数据集来开发分类器。这里使用的是 Michael Guerzhoy 和 Davi Frossard 实现的 AlexNet,AlexNet 的权重来自伯克利视觉与学习中心。完整的代码和数据集可以从 这里下载。
AlexNet 期望输入的是 227 x 227 x 3 像素的图像,而交通标志图像的尺寸是 32 x 32 x 3 像素。为了将交通标志图像输入到 AlexNet 中,我们需要将图像调整为 AlexNet 所期望的尺寸,即 227 x 227 x 3:
original_image = tf.placeholder(tf.float32, (None, 32, 32, 3))
resized_image = tf.image.resize_images(original_imag, (227, 227))
我们可以借助 TensorFlow 的 tf.image.resize_images 方法来实现。另一个问题是,AlexNet 是在 ImageNet 数据集上训练的,该数据集有 1,000 个类别的图像。因此,我们将用一个 43 神经元的分类层替换这一层。为此,首先计算最后一个全连接层输出的大小;由于这是一个全连接层,因此它的输出是一个 2D 形状,最后一个元素就是输出的大小。fc7.get_shape().as_list()[-1] 完成了这个任务;然后将其与交通标志数据集的类别数结合,得到最终全连接层的形状:shape = (fc7.get_shape().as_list()[-1], 43)。其余代码只是 TensorFlow 中定义全连接层的标准方式。最后,通过 softmax 计算概率:
#Refer AlexNet implementation code, returns last fully connected layer
fc7 = AlexNet(resized, feature_extract=True)
shape = (fc7.get_shape().as_list()[-1], 43)
fc8_weight = tf.Variable(tf.truncated_normal(shape, stddev=1e-2))
fc8_b = tf.Variable(tf.zeros(43))
logits = tf.nn.xw_plus_b(fc7, fc8_weight, fc8_b)
probs = tf.nn.softmax(logits)
VGGNet 架构
2014 年 ImageNet 挑战赛的亚军是来自牛津大学视觉几何小组的 VGGNet。这个卷积神经网络架构简单而优雅,错误率为 7.3%。它有两个版本:VGG16 和 VGG19。
VGG16 是一个 16 层神经网络,不包括最大池化层和 softmax 层。因此,它被称为 VGG16。VGG19 由 19 层组成。Keras 中有一个预训练模型,适用于 Theano 和 TensorFlow 后端。
这里的关键设计考虑因素是网络的深度。通过增加更多的卷积层来增加网络的深度,这样做是因为所有层中的卷积滤波器大小都为 3 x 3。该模型的默认输入图像大小为 224 x 224 x 3。图像通过一系列卷积层进行处理,步幅为 1 个像素,填充为 1。整个网络使用 3 x 3 卷积。最大池化在一个 2 x 2 像素窗口中进行,步幅为 2,然后是另一组卷积层,接着是三个全连接层。前两个全连接层每个有 4,096 个神经元,第三个全连接层负责分类,包含 1,000 个神经元。最后一层是 softmax 层。与 AlexNet 的 11 x 11 卷积窗口相比,VGG16 使用了更小的 3 x 3 卷积窗口。所有隐藏层都使用 ReLU 激活函数。网络架构如下所示:

VGG16 网络架构
由于小型的 3 x 3 卷积滤波器,VGGNet 的深度得以增加。该网络的参数数量大约为 1.4 亿,主要来自于第一个全连接层。在后来的架构中,VGGNet 的全连接层被全局平均池化(GAP)层替代,以减少参数数量。
另一个观察是,随着图像尺寸的减小,滤波器的数量会增加。
VGG16 图像分类代码示例
Keras 应用模块包含了预训练的神经网络模型,以及基于 ImageNet 训练的预训练权重。这些模型可以直接用于预测、特征提取和微调:
#import VGG16 network model and other necessary libraries
from keras.applications.vgg16 import VGG16
from keras.preprocessing import image
from keras.applications.vgg16 import preprocess_input
import numpy as np
#Instantiate VGG16 and returns a vgg16 model instance
vgg16_model = VGG16(weights='imagenet', include_top=False)
#include_top: whether to include the 3 fully-connected layers at the top of the network.
#This has to be True for classification and False for feature extraction. Returns a model instance
#weights:'imagenet' means model is pre-training on ImageNet data.
model = VGG16(weights='imagenet', include_top=True)
model.summary()
#image file name to classify
image_path = 'jumping_dolphin.jpg'
#load the input image with keras helper utilities and resize the image.
#Default input size for this model is 224x224 pixels.
img = image.load_img(image_path, target_size=(224, 224))
#convert PIL (Python Image Library??) image to numpy array
x = image.img_to_array(img)
print (x.shape)
#image is now represented by a NumPy array of shape (224, 224, 3),
# but we need to expand the dimensions to be (1, 224, 224, 3) so we can
# pass it through the network -- we'll also preprocess the image by
# subtracting the mean RGB pixel intensity from the ImageNet dataset
#Finally, we can load our Keras network and classify the image:
x = np.expand_dims(x, axis=0)
print (x.shape)
preprocessed_image = preprocess_input(x)
preds = model.predict(preprocessed_image)
print('Prediction:', decode_predictions(preds, top=2)[0])
当第一次执行上述脚本时,Keras 会自动下载并将架构权重缓存到磁盘中的~/.keras/models目录。随后的运行会更快。
GoogLeNet 架构
2014 年,在 ILSVRC 比赛中,Google 发布了自己的网络,名为GoogLeNet。它的表现比 VGGNet 略好;GoogLeNet 的表现为 6.7%,而 VGGNet 的表现为 7.3%。GoogLeNet 的主要吸引力在于它运行非常快速,因为引入了一种叫做inception 模块的新概念,从而将参数数量减少到仅为 500 万;这是 AlexNet 的 12 倍还少。它的内存使用和功耗也更低。
它有 22 层,因此是一个非常深的网络。添加更多层会增加参数数量,并且可能导致网络过拟合。计算量也会增加,因为滤波器数量的线性增长会导致计算量的平方增长。因此,设计者使用了 inception 模块和 GAP。网络末端的全连接层被 GAP 层替代,因为全连接层通常容易导致过拟合。GAP 没有需要学习或优化的参数。
架构洞察
与以前的架构选择特定滤波器大小不同,GoogLeNet 设计者将 1 x 1、3 x 3 和 5 x 5 三种不同大小的滤波器应用到同一图像块上,再通过 3 x 3 最大池化和连接操作将它们合并为一个输出向量。
使用 1 x 1 卷积可以减少在计算量增加的地方,替代了昂贵的 3 x 3 和 5 x 5 卷积。在昂贵的 3 x 3 和 5 x 5 卷积之前,使用了带有 ReLU 激活函数的 1 x 1 卷积。
在 GoogLeNet 中,inception 模块一个接一个地堆叠。这种堆叠方式使得我们可以修改每个模块,而不会影响后面的层。例如,你可以增加或减少任何层的宽度:

GoogLeNet 架构
深度网络也面临着所谓的梯度消失问题,尤其是在反向传播时。通过向中间层添加辅助分类器,可以避免这一问题。此外,在训练过程中,将中间损失与折扣因子 0.3 相加,纳入总损失。
由于全连接层容易发生过拟合,因此被替换为 GAP 层。平均池化并不排除使用 dropout,这是一种用于克服深度神经网络中过拟合的正则化方法。GoogLeNet 在 60 层后添加了一个线性层,使用 GAP 层帮助其他人通过迁移学习技术为自己的分类器进行优化。
Inception 模块
以下图像展示了一个 Inception 模块的例子:

ResNet 架构
在达到一定深度后,向前馈卷积网络添加额外的层会导致训练误差和验证误差增加。增加层数时,性能只会在一定深度内提升,然后会迅速下降。在ResNet(残差网络)论文中,作者认为这种欠拟合不太可能是由于梯度消失问题引起的,因为即使使用批量归一化技术,这种情况仍然会发生。因此,他们提出了一个新的概念——残差块。ResNet 团队添加了可以跳过层的连接:
ResNet 使用标准卷积神经网络,并添加了可以一次跳过几个卷积层的连接。每个跳跃都形成一个残差块。

残差块
在 2015 年 ImageNet ILSVRC 竞赛中,微软的 ResNet 以 3.57%的错误率赢得了冠军。ResNet 在某种意义上类似于 VGG,因为它的结构会不断重复,从而使网络变得更深。与 VGGNet 不同,ResNet 有不同的深度变种,例如 34 层、50 层、101 层和 152 层。相比于 AlexNet 的 8 层、VGGNet 的 19 层和 GoogLeNet 的 22 层,它有着惊人的 152 层。ResNet 架构是由残差块堆叠而成。其主要思想是通过向神经网络添加连接来跳过某些层。每个残差块包含 3x3 的卷积层。最后的卷积层后面添加了一个 GAP 层。只有一个全连接层用于分类 1000 个类别。它有不同的深度变种,例如 34 层、50 层、101 层或 152 层,用于 ImageNet 数据集。对于更深的网络,比如超过 50 层的网络,使用了瓶颈特征概念来提高效率。这个网络没有使用 dropout。
其他需要注意的网络架构包括:
-
网络中的网络
-
超越 ResNet
-
FractalNet,一个没有残差的超深神经网络
摘要
本章中,我们学习了不同的 CNN 架构。这些模型是预训练的现有模型,在网络架构上有所不同。每个网络都是为了针对其架构特定的问题而设计的。因此,我们在此描述了它们的架构差异。
我们还理解了我们自己定义的 CNN 架构(在上一章中提到的)与这些先进架构之间的区别。
在下一章,我们将学习如何将这些预训练模型用于迁移学习。
第五章:迁移学习
在上一章中,我们学习了卷积神经网络(CNN)由多个层组成。我们还研究了不同的 CNN 架构,调整了不同的超参数,并确定了步幅、窗口大小和填充的值。然后我们选择了一个合适的损失函数并进行了优化。我们用大量图像训练了这个架构。那么,问题来了,我们如何利用这些知识处理不同的数据集呢?与其从头构建 CNN 架构并进行训练,不如使用一个现有的预训练网络,通过一种叫做迁移学习的技术将其适配到新的不同数据集上。我们可以通过特征提取和微调来实现这一点。
迁移学习是将已经训练好的网络的知识复制到新网络中,以解决类似问题的过程。
在本章中,我们将讨论以下主题:
-
特征提取方法
-
迁移学习示例
-
多任务学习
特征提取方法
在特征提取方法中,我们只训练网络的顶层;其余部分保持不变。当新的数据集相对较小且与原始数据集相似时,可以考虑采用特征提取方法。在这种情况下,从原始数据集中学到的高层次特征应能很好地迁移到新数据集。
当新的数据集较大且与原始数据集相似时,可以考虑微调方法。修改原始权重是安全的,因为网络不太可能会对新的、大型数据集发生过拟合。
让我们考虑一个预训练的卷积神经网络,如下图所示。通过这个示例,我们可以研究如何在不同情况下使用知识迁移:

我们什么时候使用迁移学习?迁移学习可以根据以下情况进行应用:
-
新的(目标)数据集的大小
-
原始数据集和目标数据集之间的相似性
主要有四种使用场景:
-
案例 1:新的(目标)数据集较小,并且与原始训练数据集相似
-
案例 2:新的(目标)数据集较小,但与原始训练数据集不同
-
案例 3:新的(目标)数据集较大,并且与原始训练数据集相似
-
案例 4:新的(目标)数据集较大,并且与原始训练数据集不同
现在让我们在接下来的章节中详细讲解每个案例。
目标数据集较小,并且与原始训练数据集相似
如果目标数据集较小且与原始数据集相似:
-
在这种情况下,用一个新的全连接层替换最后一个全连接层,使其与目标数据集的类别数量匹配
-
用随机权重初始化旧的权重
-
训练网络以更新新的全连接层的权重:

转移学习可以作为一种避免过拟合的策略,特别是在数据集较小的情况下。
目标数据集较小,但与原始训练数据集不同
如果目标数据集较小,但与原始数据集类型不同——例如,原始数据集是狗的图像,而新的(目标)数据集是花卉的图像——那么应执行以下操作:
-
切割网络的大部分初始层
-
在其余的预训练层中添加一个新的全连接层,该层的节点数与目标数据集的类别数相匹配
-
随机化新全连接层的权重,并冻结预训练网络的所有权重
-
训练网络以更新新全连接层的权重
由于数据集较小,过拟合在这里仍然是一个问题。为了解决这个问题,我们将保持原始预训练网络的权重不变,只更新新全连接层的权重:

只需微调网络的高层部分。这是因为开始的层是用来提取更通用的特征的。通常,卷积神经网络的第一层并不特定于某个数据集。
目标数据集较大并且与原始训练数据集相似
由于数据集很大,我们不需要担心过拟合。因此,在这种情况下,我们可以重新训练整个网络:
-
移除最后一个全连接层,并用一个与目标数据集类别数匹配的全连接层替换它
-
随机初始化新添加的全连接层的权重
-
使用预训练的权重初始化其余的权重
-
训练整个网络:

目标数据集较大,并且与原始训练数据集不同
如果目标数据集较大并且与原始数据集不同:
-
移除最后一个全连接层,并用一个与目标数据集类别数匹配的全连接层替换它
-
从头开始训练整个网络,并随机初始化权重:

Caffe 库有 ModelZoo,可以在其中共享网络权重。
当数据集很大并且与原始数据集完全不同时,考虑从头开始训练。在这种情况下,我们有足够的数据来从头开始训练,而不必担心过拟合。然而,即便如此,使用预训练权重初始化整个网络并在新数据集上进行微调可能还是有益的。
转移学习示例
在这个例子中,我们将采用预训练的 VGGNet,并使用迁移学习训练一个 CNN 分类器,该分类器根据狗的图像预测狗的品种。Keras 包含许多预训练模型,并提供加载和可视化这些模型的代码。另一个是可以在这里下载的花卉数据集。狗品种数据集有 133 个狗品种类别和 8,351 张狗的图像。请在这里下载狗品种数据集并将其复制到你的文件夹中。VGGNet 从头到尾包含 16 层卷积池化层,以及三个全连接层,后接一个softmax函数。它的主要目标是展示网络深度如何带来最佳性能。它来自牛津的视觉几何组(VGG)。他们表现最佳的网络是 VGG16。狗品种数据集相对较小,并与imageNet数据集有些重叠。所以我们可以去除卷积层之后的最后一个全连接层,并用我们自己的层替换它。卷积层的权重保持不变。输入图像通过卷积层并停留在第 16 层:

VGGNet 架构
我们将使用预训练的 VGG16 网络的瓶颈特征 —— 该网络已经从imageNet数据集中学习了特征。由于imageNet数据集已经包含了一些狗的图像,VGG16 网络模型已学到了用于分类的关键特征。类似地,其他预训练的 CNN 架构也可以作为解决其他图像分类任务的练习。
在此下载 VGG16 的bottleneck_features,将其复制到你自己的文件夹中,然后加载:
bottleneck_features = np.load('bottleneck_features/DogVGG16Data.npz')
train_vgg16 = bottleneck_features['train']
valid_vgg16 = bottleneck_features['valid']
test_vgg16 = bottleneck_features['test']
现在定义模型架构:
from keras.layers import GlobalAveragePooling2D
model = Sequential()
model.add(GlobalAveragePooling2D(input_shape=(7, 7, 512)))
model.add(Dense(133, activation='softmax'))
model.summary()
Layer (type) Output Shape Param # Connected to
=================================================================================================
globalaveragepooling2d_1 (Global (None, 512) 0 globalaveragepooling2d_input_1[0]
_________________________________________________________________________________________________
dense_2 (Dense) (None, 133) 68229 globalaveragepooling2d_1[0][0]
=================================================================================================
Total params: 68,229
Trainable params: 68,229
Non-trainable params: 0
_________________________________________________________________________________________________
编译模型并训练:
model.compile(loss='categorical_crossentropy', optimizer='rmsprop',
metrics=['accuracy'])
from keras.callbacks import ModelCheckpoint
# train the model
checkpointer = ModelCheckpoint(filepath='dogvgg16.weights.best.hdf5', verbose=1,
save_best_only=True)
model.fit(train_vgg16, train_targets, nb_epoch=20, validation_data=(valid_vgg16, valid_targets),
callbacks=[checkpointer], verbose=1, shuffle=True)
加载模型并计算测试集上的分类准确度:
# load the weights that yielded the best validation accuracy
model.load_weights('dogvgg16.weights.best.hdf5')
# get index of predicted dog breed for each image in test set
vgg16_predictions = [np.argmax(model.predict(np.expand_dims(feature, axis=0)))
for feature in test_vgg16]
# report test accuracy
test_accuracy = 100*np.sum(np.array(vgg16_predictions)==
np.argmax(test_targets, axis=1))/len(vgg16_predictions)
print('\nTest accuracy: %.4f%%' % test_accuracy)
多任务学习
在多任务学习中,迁移学习是从一个预训练模型到多个任务的同时迁移。例如,在自动驾驶汽车中,深度神经网络同时检测交通标志、行人和前方的其他车辆。语音识别同样受益于多任务学习。
总结
在某些特定情况下,训练于图像上的卷积神经网络架构允许我们在新网络中重用已学到的特征。当基础任务和目标任务差异较大时,特征迁移的性能提升会减小。令人惊讶的是,几乎任何层数的卷积神经网络初始化,若采用转移过来的特征,在微调到新数据集后都能提升泛化性能。
第六章:自编码器在卷积神经网络(CNN)中的应用
在本章中,我们将覆盖以下主题:
-
自编码器介绍
-
卷积自编码器
-
自编码器的应用
-
一个压缩的例子
自编码器介绍
自编码器是一个普通的神经网络,是一种无监督学习模型,它接受输入并在输出层产生相同的输入。因此,训练数据中没有相关标签。一般来说,自编码器由两个部分组成:
-
编码器网络
-
解码器网络
它从无标签的训练数据中学习所有必需的特征,这被称为低维特征表示。在下图中,输入数据(x)通过编码器传递,编码器生成输入数据的压缩表示。从数学角度来看,在方程式中,z = h(x),z是特征向量,通常比x的维度更小。
然后,我们将从输入数据中生成的特征传递通过解码器网络,以重建原始数据。
编码器可以是一个全连接神经网络,也可以是一个卷积神经网络(CNN)。解码器也使用与编码器相同类型的网络。在这里,我们通过卷积神经网络(ConvNet)解释并实现了编码器和解码器功能:

损失函数:||x - x||²
在这个网络中,输入层和输出层的尺寸相同。
卷积自编码器
卷积自编码器是一个神经网络(无监督学习模型的特殊情况),经过训练后能够在输出层重建输入图像。图像通过编码器传递,编码器是一个卷积神经网络(ConvNet),它生成图像的低维表示。解码器是另一个样本卷积神经网络,它接收这个压缩后的图像并重建原始图像。
编码器用于压缩数据,解码器用于重建原始图像。因此,自编码器可以用于数据压缩。压缩逻辑是特定于数据的,这意味着它是从数据中学习的,而不是预定义的压缩算法,如 JPEG、MP3 等。自编码器的其他应用包括图像去噪(从损坏的图像中生成更清晰的图像)、降维和图像搜索:

这与普通的卷积神经网络(ConvNets)或神经网络的不同之处在于,输入尺寸和目标尺寸必须相同。
应用
自编码器用于降维或数据压缩,以及图像去噪。降维反过来有助于提高运行时性能并减少内存消耗。图像搜索在低维空间中可以变得非常高效。
一个压缩的例子
网络架构包括一个编码器网络,这是一个典型的卷积金字塔。每个卷积层后面跟着一个最大池化层;这减少了层的维度。
解码器将输入从稀疏表示转换为宽度较大的重建图像。网络的示意图如下所示:

编码器层的输出图像大小为 4 x 4 x 8 = 128。原始图像大小为 28 x 28 x 1 = 784,因此压缩后的图像向量大约是原始图像大小的 16%。
通常,你会看到使用反卷积(transposed convolution)层来增加层的宽度和高度。它们的工作方式与卷积层几乎完全相同,只不过是反向的。输入层的步幅(stride)在反卷积层中会变得更大。例如,如果你有一个 3 x 3 的卷积核,输入层的 3 x 3 区域在卷积层中将被缩小为一个单位。相比之下,输入层的一个单位在反卷积层中将被扩展成一个 3 x 3 的区域。TensorFlow API 为我们提供了一个简单的方式来创建这些层:tf.nn.conv2d_transpose,点击这里,www.tensorflow.org/api_docs/python/tf/nn/conv2d_transpose。
总结
我们以简短的自编码器介绍开始了本章,并在卷积神经网络(ConvNets)的帮助下实现了编码器和解码器功能。
然后,我们转向卷积自编码器,并学习它们与常规卷积神经网络和神经网络的不同之处。
我们通过一个例子详细讲解了自编码器的不同应用,并展示了自编码器如何提高低维空间中图像搜索的效率。
在下一章中,我们将研究使用卷积神经网络(CNNs)进行物体检测,并了解物体检测与物体分类的区别。
第七章:基于 CNN 的物体检测和实例分割
到目前为止,在本书中,我们主要使用卷积神经网络(CNNs)进行分类。分类将整张图像分类为具有最高检测概率的实体的类别。但如果图像中不仅有一个实体,而是多个感兴趣的实体,我们希望能够关联所有这些实体的图像该怎么办?一种方法是使用标签而不是类别,这些标签是倒数第二个 Softmax 分类层中具有高于给定阈值的概率的所有类别。然而,这里检测概率会根据实体的大小和位置有很大的差异,从下图中我们实际上可以问,模型有多自信,认为识别出的实体确实是声明的那个?假设我们非常自信,图像中确实有一个实体,比如狗,但它在图像中的尺度和位置不如它的主人,人类实体那样显眼。那么,多类别标签是一个有效的方式,但并不是最好的选择:

在本章中,我们将涵盖以下主题:
-
物体检测和图像分类的区别
-
传统的非 CNN 物体检测方法
-
基于区域的 CNN 及其特点
-
Fast R-CNN
-
Faster R-CNN
-
Mask R-CNN
物体检测和图像分类的区别
让我们举另一个例子。假设你正在观看电影《101 忠犬》,你想知道在电影的某个场景中,实际能数到多少只达尔马提亚犬。图像分类在最佳情况下可以告诉你至少有一只狗或者一只达尔马提亚犬(具体取决于你为分类器训练的级别),但无法准确告诉你它们有多少只。
基于分类的模型的另一个问题是,它们无法告诉你图像中识别出的实体在哪里。很多时候,这一点非常重要。举个例子,假设你看到邻居的狗正在和他(人类)以及他的猫玩耍。你拍了一张他们的照片,想从中提取出狗的图像,以便在网上搜索它的品种或类似的狗。然而,问题是,搜索整张图像可能无法成功,而且如果没有从图像中识别出单独的对象,你就不得不手动进行裁剪-提取-搜索的工作,如下图所示:

因此,你实际上需要一种技术,它不仅能识别图像中的实体,还能告诉你它们在图像中的位置。这就是所谓的物体检测。物体检测为图像中所有识别出的实体提供边界框和类别标签(以及检测的概率)。该系统的输出可以用于支持多个高级用例,这些用例依赖于特定类别的检测对象。
举个例子,像 Facebook、Google Photos 以及许多其他类似应用中的面部识别功能。在这些功能中,在你识别图像中是谁参加了派对之前,你需要先检测图像中的所有面部;然后,你可以将这些面部通过你的面部识别/分类模块来获取/分类出他们的名字。所以,物体检测中的“物体”命名不仅限于语言实体,还包括任何具有明确边界并且有足够数据来训练系统的事物,如下图所示:

现在,如果你想知道参加你派对的客人中有多少人实际上在享受派对,你甚至可以进行一个微笑脸的物体检测,或者使用一个微笑检测器。目前有非常强大和高效的物体检测模型,适用于大多数可检测的人体部位(眼睛、面部、上半身等)、常见的人的表情(例如微笑)以及许多其他常见物体。所以,下次你使用智能手机上的微笑快门功能时(该功能会在场景中大多数面孔被检测为微笑时自动拍照),你就知道是什么驱动了这个功能。
为什么物体检测比图像分类更加具有挑战性?
根据我们目前对 CNN 和图像分类的理解,让我们尝试理解如何解决物体检测问题,这应该会逻辑地引导我们发现其潜在的复杂性和挑战。假设我们为了简化问题,处理的是单色图像。
任何高级的物体检测都可以被视为两个任务的组合(我们稍后会反驳这一点):
-
获取正确的边界框(或者获取足够多的边界框以便后续过滤)
-
在该边界框内对物体进行分类(同时返回分类效果用于过滤)
因此,物体检测不仅要解决图像分类(第二个目标)的所有挑战,还面临着寻找正确或尽可能多的边界框这一新的挑战。我们已经知道如何使用 CNN 进行图像分类,以及相关的挑战,现在我们可以集中精力处理第一个任务,并探讨我们的方案在效果(分类精度)和效率(计算复杂度)方面的有效性——或者更确切地说,探讨这一任务将会有多么具有挑战性。
所以,我们从随机生成图像中的边界框开始。即使我们不担心生成这么多候选框的计算负载,技术上称为区域提议(我们发送作为分类物体的提议的区域),我们仍然需要有某种机制来寻找以下参数的最佳值:
-
用于提取/绘制候选边界框的起始(或中心)坐标
-
候选边界框的长度
-
候选边界框的宽度
-
跨越每个轴(从一个起始位置到另一个位置的* x *-水平轴和 * y *-垂直轴的距离)
假设我们可以生成一个算法,给出这些参数的最优值。那么,这些参数的一个值在大多数情况下适用吗?实际上,在某些一般情况下适用吗?根据我们的经验,我们知道每个物体的尺度不同,因此我们知道,对于这些框,L 和 W 的一个固定值是行不通的。此外,我们还可以理解,相同的物体,比如狗,在不同的图像中可能以不同的比例/尺度和位置出现,正如我们之前的一些例子所示。所以这证实了我们的信念:我们需要的是不同尺度而且不同大小的框。
假设从前面的类比中修正过来,我们希望从图像中的每个起始坐标提取 N 个候选框,其中 N 包含大多数可能适合我们分类问题的尺寸/尺度。尽管这似乎是一个相当具有挑战性的任务,但假设我们已经有了那个魔法数字,而它远不是 L[1,l-image] x W[1,w-image](所有 L 和 W 的组合,其中长度是实际图像的所有整数集合,宽度是从 1 到图像宽度);这将导致每个坐标有 lw* 个框:

接下来,另一个问题是我们需要在图像中访问多少个起始坐标,从这些位置提取每个 N 个框,或者说步长。使用一个非常大的步长会导致我们提取到的子图像本身,而不是可以有效分类并用于实现我们之前示例中某些目标的单一同质物体。相反,步长过短(比如每个方向上 1 个像素)可能意味着会有大量候选框。
从前面的示例中,我们可以理解,即使假设放宽大部分约束条件,我们仍然无法制造出可以装进智能手机、实时检测微笑自拍或甚至明亮面孔的系统(实际上即便是一个小时也不行)。我们的机器人和自动驾驶汽车也无法在移动时识别物体(并通过避开它们来导航)。这种直觉应该帮助我们理解物体检测领域的进展,以及为什么这是一个如此具有影响力的工作领域。
传统的非 CNN 物体检测方法
像 OpenCV 这样的库以及其他一些库在智能手机、机器人项目和许多其他软件包中迅速被纳入,以提供特定物体(如面部、微笑等)的检测能力,以及计算机视觉等相关功能,尽管即便是在 CNN 广泛采用之前,也存在一些约束。
基于 CNN 的目标检测和实例分割领域的研究为该领域提供了许多进展和性能提升,不仅使这些系统的大规模部署成为可能,还为许多新解决方案开辟了道路。但在我们计划深入探讨基于 CNN 的进展之前,了解在前一部分中提到的挑战是如何得到解决的,以使得目标检测在各种限制条件下仍能得以实现,将是一个不错的主意。接着,我们将按逻辑开始讨论不同的研究人员以及如何将 CNN 应用于解决传统方法仍然存在的其他问题。
Haar 特征、级联分类器和 Viola-Jones 算法
与 CNN 或深度学习不同,后者以能够自动生成更高层次概念特征而著称,这些特征反过来可以大大提升分类器的性能,在传统的机器学习应用中,这些特征需要由领域专家手工设计。
正如我们从使用基于 CPU 的机器学习分类器的经验中也可以理解的那样,它们的性能受数据高维度和可应用于模型的特征数量过多的影响,尤其是对于一些非常流行且复杂的分类器,如支持向量机(SVM),它曾被认为是最先进的技术,直到不久前。
在本节中,我们将了解一些创新的想法,这些想法来自不同科学和数学领域的启发,最终解决了上面提到的一些挑战,从而使得非 CNN 系统中的实时目标检测得以实现。
Haar 特征
Haar 或类似 Haar 特征是具有不同像素密度的矩形形式。Haar 特征通过在检测区域内特定位置的相邻矩形区域中求和像素强度,根据各区域像素强度总和之间的差异,分类图像的不同子区域。
类 Haar 特征的名称源自数学术语 Haar 小波,它是一系列重新缩放的方形函数,这些函数共同形成了一个小波族或基底。
由于 Haar-like 特征是基于区域间像素强度差异工作的,因此它们在单色图像上效果最佳。这也是为什么前面使用的图像以及本节中的图像是单色的,以便更好地直观理解。
这些类别可以分为三个主要组别,如下所示:
-
两个矩形特征
-
三个矩形特征
-
四个矩形特征

类 Haar 特征
通过一些简单的技巧,图像中不同区域的像素强度计算变得非常高效,并可以实时以非常高的速度处理。
级联分类器
即使我们能够非常快速地从特定区域提取 Haar 特征,这也不能解决从图像中许多不同位置提取这些特征的问题;这时,级联特征的概念就能发挥作用。观察到在分类中,只有 1/10,000 的子区域会被判定为面部,但我们必须提取所有特征并在所有区域运行整个分类器。进一步观察到,通过仅使用少数几个特征(级联第一层中的两个特征),分类器可以消除大量区域(级联第一层中 50%的区域)。此外,如果样本仅包含这些减少的区域样本,那么只需稍多的特征(级联第二层中的 10 个特征)就能让分类器排除更多的情况,以此类推。因此,我们按层次进行分类,从需要非常低计算能力的分类器开始,逐步增加剩余子集所需的计算负载,依此类推。
Viola-Jones 算法
在 2001 年,Paul Viola 和 Michael Jones 提出了一个解决方案,可以很好地应对一些前述挑战,但也有一些约束。尽管这是一个近二十年前的算法,但到目前为止,甚至直到最近,许多流行的计算机视觉软件仍然以某种形式将其嵌入其中。这一事实使得在我们转向基于 CNN 的区域提议方法之前,理解这个非常简单而强大的算法变得非常重要。
OpenCV 是最流行的计算机视觉软件库之一,使用级联分类器作为物体检测的主要模式,Haar 特征类似的级联分类器在 OpenCV 中非常流行。为此,已有许多预训练的 Haar 分类器可供使用,涵盖多种类型的常见物体。
该算法不仅能够提供高TPR(真正率)和低FPR(假正率)的检测结果,还能在实时条件下工作(每秒至少处理两帧)。
高 TPR 结合低 FPR 是确定算法鲁棒性的一个非常重要的标准。
他们提出的算法的约束条件如下:
-
该算法仅适用于检测面部,而非识别面部(尽管他们提出的算法是针对面部的,但同样可以用于许多其他物体)。
-
面部必须出现在图像中并且是正面视角,其他视角无法被检测到。
该算法的核心是 Haar(类似)特征和级联分类器。Haar 特征将在后面的一个小节中介绍。Viola-Jones 算法使用 Haar 特征的一个子集来确定面部的一般特征,例如:
-
眼睛(通过一个由两个矩形特征(水平)确定,其中一个较暗的水平矩形在眼睛上方形成眉毛,接着是一个较亮的矩形位于下方)
-
鼻子(三个矩形特征(垂直),鼻子中心为浅矩形,两侧各有一个较暗的矩形,形成太阳穴),等等。
这些快速提取的特征可以用于创建一个分类器,以检测(区分)人脸(与非人脸)。
Haar 特征通过一些技巧,计算速度非常快。

Viola-Jones 算法和 Haar-like 特征用于检测人脸
这些 Haar-like 特征随后被用于级联分类器,以加快检测问题的处理速度,同时不失检测的鲁棒性。
Haar 特征和级联分类器促成了前一代一些非常鲁棒、高效且快速的单目标检测器。然而,训练这些级联以适应新的目标仍然非常耗时,并且存在许多限制,正如前面提到的。这就是新一代基于 CNN 的物体检测器发挥作用的地方。
在本章中,我们仅介绍了 Haar 级联或 Haar 特征的基础(属于非 CNN 范畴),因为它们长期占据主导地位,并且是许多新类型的基础。我们鼓励读者也探索一些后来更有效的基于 SIFT 和 HOG 的特征/级联(相关论文见参考文献部分)。
R-CNN - 带 CNN 特征的区域
在“为什么物体检测比图像分类更具挑战性?”这一节中,我们使用了非 CNN 方法来生成区域提议,并用 CNN 进行分类,我们意识到这样做效果不佳,因为生成的区域并没有经过优化,且输入到 CNN 中的区域并不理想。R-CNN 或带 CNN 特征的区域,顾名思义,完全颠覆了这个示例,使用 CNN 来生成特征,然后采用一种叫做支持向量机(SVM)的非 CNN 技术进行分类。
R-CNN 使用滑动窗口方法(就像我们之前讨论的那样,选择一些L x W的窗口和步幅)生成约 2000 个感兴趣区域,然后将它们转换为 CNN 特征进行分类。记得我们在迁移学习章节中讨论的内容——最后一层平展层(分类或 Softmax 层之前)可以提取出来,进行迁移学习,从在通用数据上训练的模型中进行学习,并进一步训练它们(与使用领域特定数据从头开始训练的相似性能模型相比,通常需要的数据量要少得多),从而构建领域特定的模型。R-CNN 也使用类似的机制来提高其在特定目标检测上的效果:

R-CNN - 工作原理
R-CNN 原始论文声称,在 PASCAL VOC 2012 数据集上,它相较于以前的最佳结果提高了平均精度(mAP)超过 30%,并且达到了 53.3%的 mAP。
我们在对 ImageNet 数据集进行图像分类练习(使用 CNN)时,得到了非常高精度的结果。不要将该结果与此处给出的对比统计数据一起使用,因为使用的数据集不仅不同(因此无法进行比较),而且任务本身(分类与目标检测)也完全不同,目标检测比图像分类要更具挑战性。
PASCAL VOC(视觉目标挑战):每个研究领域都需要某种标准化的数据集和标准的 KPI,以便在不同的研究和算法之间进行结果比较。我们用于图像分类的数据集 ImageNet 不能作为目标检测的标准化数据集,因为目标检测不仅需要对对象类别进行标注,还需要标注对象的位置。ImageNet 并未提供这种数据。因此,在大多数目标检测研究中,我们可能会看到使用标准化的目标检测数据集,如 PASCAL VOC。PASCAL VOC 数据集目前有 4 个版本:VOC2007、VOC2009、VOC2010 和 VOC2012。VOC2012 是其中最新(也是最丰富)的版本。
另一个我们遇到的难点是兴趣区域的不同尺度(和位置),使用区域进行识别。这就是所谓的定位挑战;在 R-CNN 中,它通过使用不同范围的感受野来解决这个问题,从高达 195 x 195 像素和 32 x 32 步长的区域开始,到较小的区域逐渐减小。
这种方法被称为使用区域进行识别。
等一下!这是不是让你想起了什么?我们曾说过将使用 CNN 从这个区域生成特征,但 CNN 使用的是固定大小的输入来生成固定大小的平坦层。我们确实需要固定大小的特征(扁平化的向量大小)作为 SVM 的输入,但这里输入区域的大小是变化的。那么这如何实现呢?R-CNN 使用了一种流行的技术,叫做仿射图像变换,通过这种技术,可以从每个区域提议中计算出固定大小的 CNN 输入,无论区域的形状如何。
在几何学中,仿射变换是指在仿射空间之间的变换函数,这种变换保持点、直线和平面的不变。仿射空间是一种结构,它在保留与平行性和相应尺度相关的性质的同时,推广了欧几里得空间的性质。
除了我们已经讨论过的挑战,还有一个值得提到的挑战。我们在第一步中生成的候选区域(在第二步中进行分类)并不非常准确,或者它们缺乏围绕识别对象的紧密边界。因此,我们在这种方法中加入了第三阶段,通过运行回归函数(称为边界框回归器)来提高边界框的准确性,以识别分隔的边界。
与早期的端到端非 CNN 方法相比,R-CNN 证明非常成功。但它仅使用 CNN 将区域转换为特征。如我们所知,CNN 在图像分类中也非常强大,但由于我们的 CNN 仅对输入的区域图像工作,而不是对展平后的区域特征进行操作,因此无法直接使用它。在下一节中,我们将看到如何克服这一障碍。
从理解 CNN 在目标检测中的背景使用角度来看,R-CNN 非常重要,因为它是从所有非 CNN 方法中迈出的巨大一步。但由于 CNN 在目标检测中的进一步改进,正如我们接下来会讨论的那样,R-CNN 现在不再被积极开发,代码也不再维护。
Fast R-CNN – 快速区域卷积神经网络
Fast R-CNN,或称快速区域卷积神经网络方法,是对先前的 R-CNN 的改进。具体来说,与 R-CNN 相比,它的改进统计数据如下:
-
训练速度提升 9 倍
-
在评分/服务/测试时快 213 倍(每张图片处理 0.3 秒),不包括区域提议所花费的时间
-
在 PASCAL VOC 2012 数据集上具有更高的 mAP,达 66%
在 R-CNN 使用较小的(五层)CNN 时,Fast R-CNN 使用更深的 VGG16 网络,这也提高了其准确性。此外,R-CNN 之所以慢,是因为它对每个对象提议执行一次卷积神经网络的前向传播,而没有共享计算:

Fast R-CNN:工作原理
在 Fast R-CNN 中,深度 VGG16 CNN 为所有阶段提供了必要的计算,即:
-
感兴趣区域(RoI)计算
-
对区域内容进行分类(对象或背景)
-
回归以增强边界框
在这种情况下,CNN 的输入不是来自图像的原始(候选)区域,而是完整的实际图像;输出不是最后的展平层,而是之前的卷积(映射)层。从生成的卷积映射中,使用 RoI 池化层(最大池化的变体)来生成对应每个目标提议的展平固定长度 RoI,这些 RoI 随后会通过一些全连接(FC)层。
RoI 池化是最大池化的一种变体(我们在本书的初始章节中使用过),其中输出大小是固定的,输入矩形是一个参数。
RoI 池化层使用最大池化,将任何有效感兴趣区域中的特征转换为一个具有固定空间范围的小特征图。
来自倒数第二个全连接层的输出将用于以下两项:
-
分类(SoftMax 层),类别数与目标提议的数量相同,额外+1 个类别用于背景(区域中未找到的任何类别)
-
一组回归器,产生四个数字(两个数字表示该物体框的左上角的 x、y 坐标,接下来的两个数字对应于该区域内物体的高度和宽度),这些数字对于每个物体提议都是必需的,以便为该物体提供精确的边界框。
使用 Fast R-CNN 所取得的结果非常出色。更为出色的是,利用强大的 CNN 网络为我们需要克服的所有三个挑战提供了非常有效的特征。但仍然存在一些缺点,正如我们在下一节关于 Faster R-CNN 的内容中将了解的那样,仍然有进一步改进的空间。
Faster R-CNN – 基于更快区域提议网络的 CNN
我们在前一节中看到,Fast R-CNN 大幅减少了评分(测试)图像所需的时间,但这种减少忽略了生成区域提议所需的时间,这一过程使用了一个独立的机制(尽管是从 CNN 的卷积图中提取的),并继续形成瓶颈。此外,我们观察到,尽管所有三个挑战在 Fast R-CNN 中都使用了来自卷积图的共同特征来解决,但它们使用了不同的机制/模型。
Faster R-CNN 改进了这些缺点,并提出了区域提议网络(RPNs)的概念,将评分(测试)时间减少到每张图像 0.2 秒,即使包括了区域提议的时间。
Fast R-CNN 在每张图像的评分(测试)上用了 0.3 秒,这还不包括区域提议过程所需的时间。

Faster R-CNN: 工作原理 - 区域提议网络作为注意力机制
如前图所示,VGG16(或其他)CNN 直接作用于图像,生成一个卷积图(类似于在 Fast R-CNN 中所做的)。从这里开始有所不同,现在有两个分支,一个进入 RPN,另一个进入检测网络。这再次是相同 CNN 的扩展,用于预测,形成了全卷积网络(FCN)。RPN 作为注意力机制并且与检测网络共享完整图像的卷积特征。此外,现在由于网络中的所有部分都可以使用高效的基于 GPU 的计算,因此减少了总体所需的时间:

Faster R-CNN: 工作原理 - 区域提议网络作为注意力机制
要更好地理解注意力机制,请参考本书中关于 CNN 的注意力机制章节。
RPN 通过滑动窗口机制工作,其中一个窗口(类似 CNN 滤波器)在共享卷积层的最后一个卷积图上滑动。每次滑动时,滑动窗口会产生k (k=N[Scale] × N[Size])个锚框(类似候选框),其中N[Scale]是每个size的N[Size]大小(长宽比)框的尺度数,这些框从滑动窗口的中心提取,就像下图所示。
RPN 输出进入一个展平的全连接(FC)层。然后,输出进入两个网络,一个用于预测每个k框的四个数字(确定框的坐标、长宽,如同 Fast R-CNN 中一样),另一个进入一个二项分类模型,确定该框内是否包含目标物体的可能性。来自 RPN 的输出进入检测网络,检测每个 k 框中所包含的具体物体类别,给定框的位置及其物体性。

Faster R-CNN:工作原理 - 提取不同尺度和大小
这个架构中的一个问题是两个网络的训练,分别是区域提议网络(Region Proposal)和检测网络。我们了解到,CNN 是通过反向传播训练的,反向传播遍历所有层,并在每次迭代时减少每层的损失。但由于架构分成了两个不同的网络,我们每次只能对一个网络进行反向传播。为了解决这个问题,训练是通过在每个网络中迭代进行的,同时保持另一个网络的权重不变。这有助于两个网络快速收敛。
RPN 架构的一个重要特性是,它对两个函数具有平移不变性,一个生成锚点,另一个为锚点生成属性(其坐标和物体性)。由于平移不变性,反向操作或根据锚点图的向量图生成图像的部分是可行的。
由于平移不变性,我们可以在 CNN 中任意方向移动,即从图像到(区域)提议,从提议到图像的相应部分。
Mask R-CNN - 使用 CNN 进行实例分割
Faster R-CNN 是目前目标检测领域的最先进技术。但在目标检测的相关领域,Faster R-CNN 无法有效解决一些问题,这就是 Mask R-CNN——Faster R-CNN 的进化版本——能够提供帮助的地方。
本节介绍了实例分割的概念,它结合了本章描述的标准目标检测问题与语义分割的挑战。
在语义分割中,应用于图像时,目标是将每个像素分类到一个固定的类别集中,而不区分物体实例。
还记得我们在直觉部分中提到的计算图像中狗的数量的例子吗?我们能够轻松地数出狗的数量,因为它们彼此相隔很远,没有重叠,因此基本上只需数对象的数量即可完成任务。现在,以以下这张图片为例,使用目标检测数番茄的数量。这将是一项艰巨的任务,因为边界框(Bounding Boxes)重叠严重,很难区分番茄实例与框的关系。
所以,本质上,我们需要进一步深入,超越边界框,进入像素层面,以便获得这种级别的分离和识别。就像我们在目标检测中用物体名称来分类边界框一样,在实例分割中,我们对每个像素进行分割/分类,不仅标出具体的物体名称,还要标出物体实例。
目标检测和实例分割可以被视为两个不同的任务,一个逻辑上引导另一个,正如我们在目标检测中发现的那样,任务是查找区域提议和分类。但是,正如在目标检测中,尤其是使用像 Fast/Faster R-CNN 这样的技术时,我们发现如果能够同时进行这些任务,同时还能够利用大量计算和网络资源来完成任务,这将更加高效,从而使这些任务无缝衔接。

实例分割 – 直觉
Mask R-CNN 是 Faster R-CNN 的一种扩展,前者在之前的网络中已经覆盖,并且使用了 Faster R-CNN 中的所有技术,唯一的新增部分是——在网络中增加了一个额外的路径,用于并行生成每个检测到的对象实例的分割掩码(或对象掩码)。此外,由于这种方法主要利用现有网络,因此它对整个处理过程仅增加了最小的开销,并且其评分(测试)时间几乎等同于 Faster R-CNN。它在所有单模型解决方案中,尤其是在应用于 COCO2016 挑战(使用 COCO2015 数据集)时,具有最好的准确度之一。
类似于 PASCAL VOC,COCO 是另一个大规模的标准数据集(由微软提供)。除了目标检测,COCO 还用于分割和图像描述。COCO 比许多其他数据集更为广泛,最近在目标检测方面的很多比较都是基于 COCO 数据集进行的。COCO 数据集有三个版本,分别是 COCO 2014、COCO 2015 和 COCO 2017。
在 Mask R-CNN 中,除了有两个分支分别生成每个锚框或 RoI 的目标性(objectness)和定位信息外,还有一个第三个全卷积网络(FCN),它接受 RoI 并以逐像素的方式为给定的锚框预测一个分割掩码。
但是仍然存在一些挑战。尽管 Faster R-CNN 确实展示了变换不变性(也就是说,我们可以从 RPN 的卷积图追踪到实际图像的像素图),但卷积图的结构与实际图像像素的结构不同。因此,网络输入和输出之间没有像素级的对齐,这对于我们通过该网络提供像素到像素的遮罩非常重要。为了解决这个问题,Mask R-CNN 使用了一个无量化层(在原文中称为 RoIAlign),它有助于对齐精确的空间位置。这个层不仅提供了精确的对齐,还大大提高了精度,因此 Mask R-CNN 能够超越许多其他网络:

Mask R-CNN – 实例分割遮罩(示例输出)
实例分割的概念非常强大,它能够实现很多使用物体检测单独无法完成的有影响力的应用场景。
我们甚至可以使用实例分割来估计同一框架中的人体姿势并将其消除。
代码中的实例分割
现在是时候将我们学到的内容付诸实践了。我们将使用 COCO 数据集及其 API 来获取数据,并使用 Facebook Research 的 Detectron 项目(链接见参考文献),该项目提供了许多前面讨论的技术的 Python 实现,遵循 Apache 2.0 许可协议。该代码适用于 Python2 和 Caffe2,因此我们需要一个带有指定配置的虚拟环境。
创建环境
可以按照 References 部分中 Caffe2 仓库链接中的 caffe2 安装说明来创建带有 Caffe2 安装的虚拟环境。接下来,我们将安装依赖。
安装 Python 依赖(Python2 环境)
我们可以按照以下代码块中的方式安装 Python 依赖:
Python 2X 和 Python 3X 是 Python 的两种不同版本(或者更准确地说是 CPython),并不是一个传统意义上的版本升级,因此一个版本的库可能与另一个版本不兼容。在这一部分中,请使用 Python 2X。
当我们提到(解释型的)编程语言 Python 时,我们需要使用特定的解释器(因为它是解释型语言,而不是像 Java 那样的编译型语言)。我们通常所提到的 Python 解释器(例如从 Python.org 下载的版本或与 Anaconda 捆绑的版本)技术上叫做 CPython,它是 Python 的默认字节码解释器,使用 C 语言编写。但是,也有其他 Python 解释器,例如 Jython(基于 Java 构建)、PyPy(用 Python 本身编写——有点不直观吧?)、IronPython(.NET 实现的 Python)。
pip install numpy>=1.13 pyyaml>=3.12 matplotlib opencv-python>=3.2 setuptools Cython mock scipy
下载并安装 COCO API 和 detectron 库(操作系统命令行命令)
然后我们将下载并安装 Python 依赖项,如以下代码块所示:
# COCO API download and install
# COCOAPI=/path/to/clone/cocoapi
git clone https://github.com/cocodataset/cocoapi.git $COCOAPI
cd $COCOAPI/PythonAPI
make install
# Detectron library download and install
# DETECTRON=/path/to/clone/detectron
git clone https://github.com/facebookresearch/detectron $DETECTRON
cd $DETECTRON/lib && make
或者,我们可以下载并使用该环境的 Docker 镜像(需要 Nvidia GPU 支持):
# DOCKER image build
cd $DETECTRON/docker docker build -t detectron:c2-cuda9-cudnn7.
nvidia-docker run --rm -it detectron:c2-cuda9-cudnn7 python2 tests/test_batch_permutation_op.py
准备 COCO 数据集文件夹结构
现在我们将看到准备 COCO 数据集文件夹结构的代码,如下所示:
# We need the following Folder structure: coco [coco_train2014, coco_val2014, annotations]
mkdir -p $DETECTRON/lib/datasets/data/coco
ln -s /path/to/coco_train2014 $DETECTRON/lib/datasets/data/coco/
ln -s /path/to/coco_val2014 $DETECTRON/lib/datasets/data/coco/
ln -s /path/to/json/annotations $DETECTRON/lib/datasets/data/coco/annotations
在 COCO 数据集上运行预训练模型
我们现在可以在 COCO 数据集上实现预训练模型,如以下代码片段所示:
python2 tools/test_net.py \
--cfg configs/12_2017_baselines/e2e_mask_rcnn_R-101-FPN_2x.yaml \
TEST.WEIGHTS https://s3-us-west-2.amazonaws.com/detectron/35861858/12_2017_baselines/e2e_mask_rcnn_R-101- FPN_2x.yaml.02_32_51.SgT4y1cO/output/train/coco_2014_train:coco_2014_valminusminival/generalized_rcnn/model_final.pkl \
NUM_GPUS 1
参考文献
-
Paul Viola 和 Michael Jones,《使用增强级联简单特征进行快速目标检测》,计算机视觉与模式识别会议,2001 年。
-
Paul Viola 和 Michael Jones,《鲁棒的实时目标检测》,国际计算机视觉杂志,2001 年。
-
Itseez2015opencv,OpenCV,《开源计算机视觉库》,Itseez,2015 年。
-
Ross B. Girshick,Jeff Donahue,Trevor Darrell,Jitendra Malik,《准确目标检测和语义分割的丰富特征层次》,CoRR,arXiv:1311.2524,2013 年。
-
Ross Girshick,Jeff Donahue,Trevor Darrell,Jitendra Malik,《准确目标检测和语义分割的丰富特征层次》,计算机视觉与模式识别,2014 年。
-
M. Everingham, L. VanGool, C. K. I. Williams, J. Winn, A. Zisserman,《PASCAL 视觉目标类别挑战赛 2012》,VOC2012,结果。
-
D. Lowe,《基于尺度不变关键点的独特图像特征》,IJCV,2004 年。
-
N. Dalal 和 B. Triggs,《用于人类检测的方向梯度直方图》,CVPR,2005 年。
-
Ross B. Girshick,Fast R-CNN,CoRR,arXiv:1504.08083,2015 年。
-
Rbgirshick,fast-rcnn,GitHub,
github.com/rbgirshick/fast-rcnn,2018 年 2 月。 -
Shaoqing Ren, Kaiming He, Ross B. Girshick, Jian Sun, Faster R-CNN: 《基于区域提议网络的实时目标检测》,CoRR,arXiv:1506.01497,2015 年。
-
Shaoqing Ren 和 Kaiming He 和 Ross Girshick 和 Jian Sun,Faster R-CNN:《基于区域提议网络的实时目标检测》,神经信息处理系统 (NIPS),2015 年。
-
Rbgirshick,py-faster-rcnn,GitHub,
github.com/rbgirshick/py-faster-rcnn,2018 年 2 月。 -
Ross Girshick,Ilija Radosavovic,Georgia Gkioxari,Piotr Dollar,Kaiming He,
Detectron,GitHub,
github.com/facebookresearch/Detectron,2018 年 2 月。 -
Tsung-Yi Lin, Michael Maire, Serge J. Belongie, Lubomir D. Bourdev, Ross B. Girshick, James Hays, Pietro Perona, Deva Ramanan, Piotr Dollar, C. Lawrence Zitnick,《Microsoft COCO:上下文中的常见物体》,CoRR,arXiv:1405.0312,2014 年。
-
Kaiming He, Georgia Gkioxari, Piotr Dollar, Ross B. Girshick,Mask R-CNN,CoRR,arXiv:1703.06870,2017 年。
-
Liang-Chieh Chen, Alexander Hermans, George Papandreou, Florian Schroff, Peng Wang, Hartwig Adam, MaskLab: 通过语义和方向特征优化目标检测的实例分割,CoRR,arXiv:1712.04837,2017。
-
Anurag Arnab, Philip H. S. Torr,使用动态实例化网络的像素级实例分割,CoRR,arXiv:1704.02386,2017。
-
Matterport,Mask_RCNN,GitHub,
github.com/matterport/Mask_RCNN,2018 年 2 月。 -
CharlesShang, FastMaskRCNN,GitHub,
github.com/CharlesShang/FastMaskRCNN,2018 年 2 月。 -
Caffe2,Caffe2,GitHub,
github.com/caffe2/caffe2,2018 年 2 月。
总结
在本章中,我们从目标检测任务背后的简单直觉入手,然后逐步介绍了更为先进的概念,例如实例分割,这是当今的研究热点。目标检测在零售、媒体、社交媒体、移动性和安全领域的创新中占据核心地位;这些技术具有巨大的潜力,可以为企业和社会消费创造具有深远影响和盈利潜力的功能。
从算法的角度来看,本章从传奇的 Viola-Jones 算法及其底层机制开始,诸如 Haar 特征和级联分类器。基于这些直觉,我们开始探索卷积神经网络(CNN)在目标检测中的应用,涉及的算法包括 R-CNN、Fast R-CNN,一直到最先进的 Faster R-CNN。
在本章中,我们还奠定了基础,并介绍了一个非常新颖且具有深远影响的研究领域——实例分割。我们还讨论了基于 Mask R-CNN 等方法的先进深度 CNN,用于实现实例分割的简便且高效的实现。
第八章:GAN:使用 CNN 生成新图像
通常,神经网络需要带标签的示例才能有效学习。无监督学习方法从未标注的数据中学习的效果并不好。生成对抗网络,简称GAN,是一种无监督学习方法,但基于可微分的生成器网络。GAN 最初由 Ian Goodfellow 等人于 2014 年发明。从那时起,它们变得非常流行。这是基于博弈论的,有两个参与者或网络:生成器网络和判别器网络,它们相互竞争。这种基于双网络的博弈论方法大大改善了从未标注数据中学习的过程。生成器网络生成伪造数据并传递给判别器。判别器网络也看到真实数据并预测它收到的数据是假的还是真的。因此,生成器被训练成可以轻松生成非常接近真实数据的数据,从而欺骗判别器网络。判别器网络被训练成分类哪些数据是真实的,哪些数据是假的。所以,最终,生成器网络学会生成非常非常接近真实数据的数据。GAN 将在音乐和艺术领域广泛流行。
根据 Goodfellow 的说法,"你可以把生成模型看作是赋予人工智能一种想象力的形式。"
以下是一些 GAN 的示例:
-
Pix2pix
-
CycleGAN
Pix2pix - 图像到图像翻译 GAN
该网络使用条件生成对抗网络(cGAN)来学习图像的输入和输出之间的映射。以下是原始论文中可以完成的一些示例:

Pix2pix 的 cGAN 示例
在手袋示例中,网络学习如何为黑白图像上色。在这里,训练数据集中的输入图像是黑白的,目标图像是彩色版。
CycleGAN
CycleGAN 也是一种图像到图像翻译器,但没有输入/输出对。例如,从画作中生成照片,将马的图像转换成斑马图像:

在判别器网络中,使用 dropout 非常重要。否则,它可能会产生较差的结果。
生成器网络以随机噪声作为输入,并产生一个真实感的图像作为输出。对不同类型的随机噪声运行生成器网络会产生不同种类的真实图像。第二个网络,称为判别器网络,与常规的神经网络分类器非常相似。该网络在真实图像上进行训练,尽管训练 GAN 与监督训练方法有很大不同。在监督训练中,每个图像在显示给模型之前都会先被标注。例如,如果输入是一张狗的图像,我们会告诉模型这是狗。而在生成模型中,我们会向模型展示大量图像,并要求它从相同的概率分布中生成更多类似的图像。实际上,第二个判别器网络帮助生成器网络实现这一目标。
判别器输出图像是真实的还是生成器生成的假的概率。换句话说,它试图给真实图像分配一个接近 1 的概率,而给假的图像分配一个接近 0 的概率。与此同时,生成器则做相反的事情。它被训练成输出能被判别器判定为接近 1 的图像。随着时间的推移,生成器会生成更真实的图像,从而欺骗判别器:

训练 GAN 模型
在前几章中解释的大多数机器学习模型都是基于优化的,也就是说,我们在其参数空间中最小化代价函数。生成对抗网络(GAN)则不同,因为它包含了两个网络:生成器 G 和判别器 D。每个网络都有自己的代价函数。一个简单的方式来理解 GAN 是,判别器的代价函数是生成器代价函数的负值。在 GAN 中,我们可以定义一个值函数,生成器需要最小化,而判别器需要最大化。生成模型的训练过程与监督训练方法大不相同。GAN 对初始权重非常敏感,因此我们需要使用批量归一化(batch normalization)。批量归一化不仅能提高性能,还能使模型更加稳定。在这里,我们同时训练两个模型:生成模型和判别模型。生成模型 G 捕捉数据分布,而判别模型 D 估计一个样本来自训练数据的概率,而不是来自 G。
GAN – 代码示例
在以下示例中,我们使用 MNIST 数据集并利用 TensorFlow 构建和训练一个 GAN 模型。这里,我们将使用一种特殊版本的 ReLU 激活函数,称为Leaky ReLU。输出是一个新的手写数字类型:
Leaky ReLU 是 ReLU 激活函数的一种变体,其公式为f(x) = max(α∗x, x**)。因此,x为负值时,输出为alpha * x,而x为正值时,输出为x。
#import all necessary libraries and load data set
%matplotlib inline
import pickle as pkl
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets('MNIST_data')
为了构建这个网络,我们需要两个输入,一个是生成器的输入,一个是判别器的输入。在下面的代码中,我们为判别器创建real_input的占位符,为生成器创建z_input的占位符,输入尺寸分别为dim_real和dim_z:
#place holder for model inputs
def model_inputs(dim_real, dim_z):
real_input = tf.placeholder(tf.float32, name='dim_real')
z_input = tf.placeholder(tf.float32, name='dim_z')
return real_input, z_input
这里,输入z是一个随机向量传入生成器,生成器将这个向量转化为图像。然后我们添加一个隐藏层,这是一个带有泄漏的 ReLU 层,以允许梯度向后传播。泄漏 ReLU 就像普通的 ReLU(对负值输出零)一样,除了对于负输入值,输出有一个小的非零值。生成器使用tanh和sigmoid函数表现更好。生成器的输出是tanh输出。因此,我们必须将 MNIST 图像重新缩放到-1 到 1 之间,而不是 0 到 1 之间。通过这些知识,我们可以构建生成器网络:
#Following code builds Generator Network
def generator(z, out_dim, n_units=128, reuse=False, alpha=0.01):
''' Build the generator network.
Arguments
---------
z : Input tensor for the generator
out_dim : Shape of the generator output
n_units : Number of units in hidden layer
reuse : Reuse the variables with tf.variable_scope
alpha : leak parameter for leaky ReLU
Returns
-------
out:
'''
with tf.variable_scope('generator', reuse=reuse) as generator_scope: # finish this
# Hidden layer
h1 = tf.layers.dense(z, n_units, activation=None )
# Leaky ReLU
h1 = tf.nn.leaky_relu(h1, alpha=alpha,name='leaky_generator')
# Logits and tanh output
logits = tf.layers.dense(h1, out_dim, activation=None)
out = tf.tanh(logits)
return out
判别器网络与生成器相同,只是输出层使用的是sigmoid函数:
def discriminator(x, n_units=128, reuse=False, alpha=0.01):
''' Build the discriminator network.
Arguments
---------
x : Input tensor for the discriminator
n_units: Number of units in hidden layer
reuse : Reuse the variables with tf.variable_scope
alpha : leak parameter for leaky ReLU
Returns
-------
out, logits:
'''
with tf.variable_scope('discriminator', reuse=reuse) as discriminator_scope:# finish this
# Hidden layer
h1 = tf.layers.dense(x, n_units, activation=None )
# Leaky ReLU
h1 = tf.nn.leaky_relu(h1, alpha=alpha,name='leaky_discriminator')
logits = tf.layers.dense(h1, 1, activation=None)
out = tf.sigmoid(logits)
return out, logits
要构建网络,使用以下代码:
#Hyperparameters
# Size of input image to discriminator
input_size = 784 # 28x28 MNIST images flattened
# Size of latent vector to generator
z_size = 100
# Sizes of hidden layers in generator and discriminator
g_hidden_size = 128
d_hidden_size = 128
# Leak factor for leaky ReLU
alpha = 0.01
# Label smoothing
smooth = 0.1
我们希望在真实数据和假数据之间共享权重,因此需要重用变量:
#Build the network
tf.reset_default_graph()
# Create our input placeholders
input_real, input_z = model_inputs(input_size, z_size)
# Build the model
g_model = generator(input_z, input_size, n_units=g_hidden_size, alpha=alpha)
# g_model is the generator output
d_model_real, d_logits_real = discriminator(input_real, n_units=d_hidden_size, alpha=alpha)
d_model_fake, d_logits_fake = discriminator(g_model, reuse=True, n_units=d_hidden_size, alpha=alpha)
计算损失
对于判别器,总损失是对真实图像和假图像损失的总和。损失将是 sigmoid 交叉熵损失,我们可以使用 TensorFlow 的tf.nn.sigmoid_cross_entropy_with_logits得到。然后我们计算批次中所有图像的均值。因此,损失将如下所示:
tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=logits, labels=labels))
为了帮助判别器更好地泛化,可以通过例如使用平滑参数,将labels从 1.0 稍微减少到 0.9。 这被称为标签平滑,通常与分类器一起使用以提高性能。假的数据的判别器损失类似。logits是d_logits_fake,它是通过将生成器输出传递给判别器得到的。这些假的logits与全为零的labels一起使用。记住,我们希望判别器对真实图像输出 1,对假图像输出 0,因此我们需要设置损失函数来反映这一点。
最后,生成器的损失使用的是d_logits_fake,即假的图像logits。但现在labels全为 1。生成器试图欺骗判别器,因此它希望判别器对假的图像输出 1:
# Calculate losses
d_loss_real = tf.reduce_mean(
tf.nn.sigmoid_cross_entropy_with_logits(logits=d_logits_real,
labels=tf.ones_like(d_logits_real) * (1 - smooth)))
d_loss_fake = tf.reduce_mean(
tf.nn.sigmoid_cross_entropy_with_logits(logits=d_logits_fake,
labels=tf.zeros_like(d_logits_real)))
d_loss = d_loss_real + d_loss_fake
g_loss = tf.reduce_mean(
tf.nn.sigmoid_cross_entropy_with_logits(logits=d_logits_fake,
labels=tf.ones_like(d_logits_fake)))
添加优化器
我们需要分别更新生成器和判别器的变量。因此,首先获取图中的所有变量,然后如前所述,我们可以仅从生成器作用域获取生成器变量,类似地从判别器作用域获取判别器变量:
# Optimizers
learning_rate = 0.002
# Get the trainable_variables, split into G and D parts
t_vars = tf.trainable_variables()
g_vars = [var for var in t_vars if var.name.startswith('generator')]
d_vars = [var for var in t_vars if var.name.startswith('discriminator')]
d_train_opt = tf.train.AdamOptimizer(learning_rate).minimize(d_loss, var_list=d_vars)
g_train_opt = tf.train.AdamOptimizer(learning_rate).minimize(g_loss, var_list=g_vars)
要训练网络,使用:
batch_size = 100
epochs = 100
samples = []
losses = []
# Only save generator variables
saver = tf.train.Saver(var_list=g_vars)
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for e in range(epochs):
for ii in range(mnist.train.num_examples//batch_size):
batch = mnist.train.next_batch(batch_size)
# Get images, reshape and rescale to pass to D
batch_images = batch[0].reshape((batch_size, 784))
batch_images = batch_images*2 - 1
# Sample random noise for G
batch_z = np.random.uniform(-1, 1, size=(batch_size, z_size))
# Run optimizers
_ = sess.run(d_train_opt, feed_dict={input_real: batch_images, input_z: batch_z})
_ = sess.run(g_train_opt, feed_dict={input_z: batch_z})
# At the end of each epoch, get the losses and print them out
train_loss_d = sess.run(d_loss, {input_z: batch_z, input_real: batch_images})
train_loss_g = g_loss.eval({input_z: batch_z})
print("Epoch {}/{}...".format(e+1, epochs),
"Discriminator Loss: {:.4f}...".format(train_loss_d),
"Generator Loss: {:.4f}".format(train_loss_g))
# Save losses to view after training
losses.append((train_loss_d, train_loss_g))
# Sample from generator as we're training for viewing afterwards
sample_z = np.random.uniform(-1, 1, size=(16, z_size))
gen_samples = sess.run(
generator(input_z, input_size, n_units=g_hidden_size, reuse=True, alpha=alpha),
feed_dict={input_z: sample_z})
samples.append(gen_samples)
saver.save(sess, './checkpoints/generator.ckpt')
# Save training generator samples
with open('train_samples.pkl', 'wb') as f:
pkl.dump(samples, f)
一旦模型训练并保存后,你可以可视化生成的数字(代码不在此处,但可以下载)。
半监督学习与 GAN
到目前为止,我们已经看到 GAN 如何用于生成逼真的图像。在本节中,我们将看到 GAN 如何用于分类任务,尤其是在标签数据较少的情况下,但仍希望提高分类器的准确性。这里我们仍然使用相同的 街景房屋号码(SVHN)数据集来对图像进行分类。如前所述,我们这里也有两个网络,生成器 G 和判别器 D。在这种情况下,判别器被训练成一个分类器。另一个变化是,判别器的输出将传递给 softmax 函数,而不是早期看到的 sigmoid 函数。softmax 函数返回标签的概率分布:

现在我们将网络建模为:
总成本 = 有标签数据的成本 + 无标签数据的成本
为了获取有标签数据的成本,我们可以使用 cross_entropy 函数:
cost of labeled data = cross_entropy ( logits, labels)
cost of unlabeled data = cross_entropy ( logits, real)
然后我们可以计算所有类别的总和:
real prob = sum (softmax(real_classes))
正常的分类器作用于有标签数据。然而,基于 GAN 的半监督分类器作用于有标签数据、真实未标注数据和假图像。这种方法非常有效,即使我们在训练过程中有较少的标注数据,分类错误也较少。
特征匹配
特征匹配的思想是,在生成器的成本函数中添加一个额外的变量,以惩罚测试数据和训练数据中的绝对误差之间的差异。
使用 GAN 示例进行半监督分类
在本节中,我们将解释如何使用 GAN 来构建一个采用半监督学习方法的分类器。
在监督学习中,我们有一个包含输入 X 和类别标签 y 的训练集。我们训练一个模型,该模型以 X 作为输入并输出 y。
在半监督学习中,我们的目标仍然是训练一个模型,该模型以 X 作为输入并生成 y 作为输出。然而,并非所有的训练示例都有标签 y。
我们使用 SVHN 数据集。我们将 GAN 判别器转变为一个 11 类判别器(0 到 9 以及一个假图像标签)。它将识别真实 SVHN 数字的 10 个不同类别,以及来自生成器的第 11 类假图像。判别器将能在真实标注图像、真实未标注图像和假图像上进行训练。通过利用三种数据来源,而不仅仅是单一来源,它将在测试集上表现得比传统的仅在单一数据源上训练的分类器更好:
def model_inputs(real_dim, z_dim):
inputs_real = tf.placeholder(tf.float32, (None, *real_dim), name='input_real')
inputs_z = tf.placeholder(tf.float32, (None, z_dim), name='input_z')
y = tf.placeholder(tf.int32, (None), name='y')
label_mask = tf.placeholder(tf.int32, (None), name='label_mask')
return inputs_real, inputs_z, y, label_mask
添加生成器:
def generator(z, output_dim, reuse=False, alpha=0.2, training=True, size_mult=128):
with tf.variable_scope('generator', reuse=reuse):
# First fully connected layer
x1 = tf.layers.dense(z, 4 * 4 * size_mult * 4)
# Reshape it to start the convolutional stack
x1 = tf.reshape(x1, (-1, 4, 4, size_mult * 4))
x1 = tf.layers.batch_normalization(x1, training=training)
x1 = tf.maximum(alpha * x1, x1)
x2 = tf.layers.conv2d_transpose(x1, size_mult * 2, 5, strides=2, padding='same')
x2 = tf.layers.batch_normalization(x2, training=training)
x2 = tf.maximum(alpha * x2, x2)
x3 = tf.layers.conv2d_transpose(x2, size_mult, 5, strides=2, padding='same')
x3 = tf.layers.batch_normalization(x3, training=training)
x3 = tf.maximum(alpha * x3, x3)
# Output layer
logits = tf.layers.conv2d_transpose(x3, output_dim, 5, strides=2, padding='same')
out = tf.tanh(logits)
return out
添加判别器:
def discriminator(x, reuse=False, alpha=0.2, drop_rate=0., num_classes=10, size_mult=64):
with tf.variable_scope('discriminator', reuse=reuse):
x = tf.layers.dropout(x, rate=drop_rate/2.5)
# Input layer is 32x32x3
x1 = tf.layers.conv2d(x, size_mult, 3, strides=2, padding='same')
relu1 = tf.maximum(alpha * x1, x1)
relu1 = tf.layers.dropout(relu1, rate=drop_rate)
x2 = tf.layers.conv2d(relu1, size_mult, 3, strides=2, padding='same')
bn2 = tf.layers.batch_normalization(x2, training=True)
relu2 = tf.maximum(alpha * x2, x2)
x3 = tf.layers.conv2d(relu2, size_mult, 3, strides=2, padding='same')
bn3 = tf.layers.batch_normalization(x3, training=True)
relu3 = tf.maximum(alpha * bn3, bn3)
relu3 = tf.layers.dropout(relu3, rate=drop_rate)
x4 = tf.layers.conv2d(relu3, 2 * size_mult, 3, strides=1, padding='same')
bn4 = tf.layers.batch_normalization(x4, training=True)
relu4 = tf.maximum(alpha * bn4, bn4)
x5 = tf.layers.conv2d(relu4, 2 * size_mult, 3, strides=1, padding='same')
bn5 = tf.layers.batch_normalization(x5, training=True)
relu5 = tf.maximum(alpha * bn5, bn5)
x6 = tf.layers.conv2d(relu5, 2 * size_mult, 3, strides=2, padding='same')
bn6 = tf.layers.batch_normalization(x6, training=True)
relu6 = tf.maximum(alpha * bn6, bn6)
relu6 = tf.layers.dropout(relu6, rate=drop_rate)
x7 = tf.layers.conv2d(relu5, 2 * size_mult, 3, strides=1, padding='valid')
# Don't use bn on this layer, because bn would set the mean of each feature
# to the bn mu parameter.
# This layer is used for the feature matching loss, which only works if
# the means can be different when the discriminator is run on the data than
# when the discriminator is run on the generator samples.
relu7 = tf.maximum(alpha * x7, x7)
# Flatten it by global average pooling
features = raise NotImplementedError()
# Set class_logits to be the inputs to a softmax distribution over the different classes
raise NotImplementedError()
# Set gan_logits such that P(input is real | input) = sigmoid(gan_logits).
# Keep in mind that class_logits gives you the probability distribution over all the real
# classes and the fake class. You need to work out how to transform this multiclass softmax
# distribution into a binary real-vs-fake decision that can be described with a sigmoid.
# Numerical stability is very important.
# You'll probably need to use this numerical stability trick:
# log sum_i exp a_i = m + log sum_i exp(a_i - m).
# This is numerically stable when m = max_i a_i.
# (It helps to think about what goes wrong when...
# 1\. One value of a_i is very large
# 2\. All the values of a_i are very negative
# This trick and this value of m fix both those cases, but the naive implementation and
# other values of m encounter various problems)
raise NotImplementedError()
return out, class_logits, gan_logits, features
计算损失:
def model_loss(input_real, input_z, output_dim, y, num_classes, label_mask, alpha=0.2, drop_rate=0.):
"""
Get the loss for the discriminator and generator
:param input_real: Images from the real dataset
:param input_z: Z input
:param output_dim: The number of channels in the output image
:param y: Integer class labels
:param num_classes: The number of classes
:param alpha: The slope of the left half of leaky ReLU activation
:param drop_rate: The probability of dropping a hidden unit
:return: A tuple of (discriminator loss, generator loss)
"""
# These numbers multiply the size of each layer of the generator and the discriminator,
# respectively. You can reduce them to run your code faster for debugging purposes.
g_size_mult = 32
d_size_mult = 64
# Here we run the generator and the discriminator
g_model = generator(input_z, output_dim, alpha=alpha, size_mult=g_size_mult)
d_on_data = discriminator(input_real, alpha=alpha, drop_rate=drop_rate, size_mult=d_size_mult)
d_model_real, class_logits_on_data, gan_logits_on_data, data_features = d_on_data
d_on_samples = discriminator(g_model, reuse=True, alpha=alpha, drop_rate=drop_rate, size_mult=d_size_mult)
d_model_fake, class_logits_on_samples, gan_logits_on_samples, sample_features = d_on_samples
# Here we compute `d_loss`, the loss for the discriminator.
# This should combine two different losses:
# 1\. The loss for the GAN problem, where we minimize the cross-entropy for the binary
# real-vs-fake classification problem.
# 2\. The loss for the SVHN digit classification problem, where we minimize the cross-entropy
# for the multi-class softmax. For this one we use the labels. Don't forget to ignore
# use `label_mask` to ignore the examples that we are pretending are unlabeled for the
# semi-supervised learning problem.
raise NotImplementedError()
# Here we set `g_loss` to the "feature matching" loss invented by Tim Salimans at OpenAI.
# This loss consists of minimizing the absolute difference between the expected features
# on the data and the expected features on the generated samples.
# This loss works better for semi-supervised learning than the tradition GAN losses.
raise NotImplementedError()
pred_class = tf.cast(tf.argmax(class_logits_on_data, 1), tf.int32)
eq = tf.equal(tf.squeeze(y), pred_class)
correct = tf.reduce_sum(tf.to_float(eq))
masked_correct = tf.reduce_sum(label_mask * tf.to_float(eq))
return d_loss, g_loss, correct, masked_correct, g_model
添加优化器:
def model_opt(d_loss, g_loss, learning_rate, beta1):
"""
Get optimization operations
:param d_loss: Discriminator loss Tensor
:param g_loss: Generator loss Tensor
:param learning_rate: Learning Rate Placeholder
:param beta1: The exponential decay rate for the 1st moment in the optimizer
:return: A tuple of (discriminator training operation, generator training operation)
"""
# Get weights and biases to update. Get them separately for the discriminator and the generator
raise NotImplementedError()
# Minimize both players' costs simultaneously
raise NotImplementedError()
shrink_lr = tf.assign(learning_rate, learning_rate * 0.9)
return d_train_opt, g_train_opt, shrink_lr
构建网络模型:
class GAN:
"""
A GAN model.
:param real_size: The shape of the real data.
:param z_size: The number of entries in the z code vector.
:param learnin_rate: The learning rate to use for Adam.
:param num_classes: The number of classes to recognize.
:param alpha: The slope of the left half of the leaky ReLU activation
:param beta1: The beta1 parameter for Adam.
"""
def __init__(self, real_size, z_size, learning_rate, num_classes=10, alpha=0.2, beta1=0.5):
tf.reset_default_graph()
self.learning_rate = tf.Variable(learning_rate, trainable=False)
inputs = model_inputs(real_size, z_size)
self.input_real, self.input_z, self.y, self.label_mask = inputs
self.drop_rate = tf.placeholder_with_default(.5, (), "drop_rate")
loss_results = model_loss(self.input_real, self.input_z,
real_size[2], self.y, num_classes,
label_mask=self.label_mask,
alpha=0.2,
drop_rate=self.drop_rate)
self.d_loss, self.g_loss, self.correct, self.masked_correct, self.samples = loss_results
self.d_opt, self.g_opt, self.shrink_lr = model_opt(self.d_loss, self.g_loss, self.learning_rate, beta1)
训练并保存模型:
def train(net, dataset, epochs, batch_size, figsize=(5,5)):
saver = tf.train.Saver()
sample_z = np.random.normal(0, 1, size=(50, z_size))
samples, train_accuracies, test_accuracies = [], [], []
steps = 0
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for e in range(epochs):
print("Epoch",e)
t1e = time.time()
num_examples = 0
num_correct = 0
for x, y, label_mask in dataset.batches(batch_size):
assert 'int' in str(y.dtype)
steps += 1
num_examples += label_mask.sum()
# Sample random noise for G
batch_z = np.random.normal(0, 1, size=(batch_size, z_size))
# Run optimizers
t1 = time.time()
_, _, correct = sess.run([net.d_opt, net.g_opt, net.masked_correct],
feed_dict={net.input_real: x, net.input_z: batch_z,
net.y : y, net.label_mask : label_mask})
t2 = time.time()
num_correct += correct
sess.run([net.shrink_lr])
train_accuracy = num_correct / float(num_examples)
print("\t\tClassifier train accuracy: ", train_accuracy)
num_examples = 0
num_correct = 0
for x, y in dataset.batches(batch_size, which_set="test"):
assert 'int' in str(y.dtype)
num_examples += x.shape[0]
correct, = sess.run([net.correct], feed_dict={net.input_real: x,
net.y : y,
net.drop_rate: 0.})
num_correct += correct
test_accuracy = num_correct / float(num_examples)
print("\t\tClassifier test accuracy", test_accuracy)
print("\t\tStep time: ", t2 - t1)
t2e = time.time()
print("\t\tEpoch time: ", t2e - t1e)
gen_samples = sess.run(
net.samples,
feed_dict={net.input_z: sample_z})
samples.append(gen_samples)
_ = view_samples(-1, samples, 5, 10, figsize=figsize)
plt.show()
# Save history of accuracies to view after training
train_accuracies.append(train_accuracy)
test_accuracies.append(test_accuracy)
saver.save(sess, './checkpoints/generator.ckpt')
with open('samples.pkl', 'wb') as f:
pkl.dump(samples, f)
return train_accuracies, test_accuracies, samples
深度卷积 GAN
深度卷积生成对抗网络,也称为 DCGAN,用于生成彩色图像。在这里,我们在生成器和判别器中使用了卷积层。我们还需要使用批量归一化来确保 GAN 能够正常训练。我们将在《深度神经网络性能提升》章节中详细讨论批量归一化。我们将在 SVHN 数据集上训练 GAN;以下是一个小示例。训练后,生成器将能够创建几乎与这些图像相同的图像。你可以下载这个示例的代码:

Google 街景房屋号码视图
批量归一化
批量归一化是一种提高神经网络性能和稳定性的技术。其思想是对层输入进行归一化,使其均值为零,方差为 1。批量归一化最早由 Sergey Ioffe 和 Christian Szegedy 于 2015 年在论文 Batch Normalization is Necessary to Make DCGANs Work 中提出。其思路是,与其仅对网络输入进行归一化,不如对网络中各个层的输入进行归一化。之所以称之为 批量 归一化,是因为在训练过程中,我们使用当前小批量中的均值和方差来对每一层的输入进行归一化。
概述
在这一章中,我们看到了 GAN 模型如何真正展示 CNN 的强大功能。我们学习了如何训练自己的生成模型,并看到了一个实际的 GAN 示例,它能够将画作转化为照片,将马变成斑马。
我们理解了 GAN 与其他判别模型的区别,并学会了为什么生成模型更受青睐。
在下一章中,我们将从头开始学习深度学习软件的比较。
第九章:CNN 和视觉模型中的注意力机制
并非图像或文本中的每一部分——或者一般来说,任何数据——从我们需要获得的洞察角度来看都是同等相关的。例如,考虑一个任务,我们需要预测一个冗长陈述序列中的下一个单词,如 Alice and Alya are friends. Alice lives in France and works in Paris. Alya is British and works in London. Alice prefers to buy books written in French, whereas Alya prefers books in _____.
当这个例子给人类时,即使是语言能力相对较好的孩子也能很好地预测下一个词很可能是 English。从数学角度出发,在深度学习的背景下,我们同样可以通过创建这些单词的向量嵌入,并使用向量数学计算结果,来得出类似的结论,具体如下:

在这里,V(Word) 是所需单词的向量嵌入;类似地,V(French)、V(Paris) 和 V(London) 分别是单词 French、Paris 和 London 的向量嵌入。
嵌入通常是低维且密集(数值型)的向量表示,用于表示输入或输入的索引(对于非数值数据);在此情况下为文本。
诸如 Word2Vec 和 glove 等算法可以用来获得单词嵌入。这些模型的预训练变种可在流行的基于 Python 的 NLP 库中找到,如 SpaCy、Gensim 等,也可以使用大多数深度学习库(如 Keras、TensorFlow 等)进行训练。
嵌入(embeddings)这一概念与视觉和图像领域的相关性与文本领域同样重要。
可能没有现成的向量与我们刚刚得到的向量完全匹配,如
;但是,如果我们尝试找到最接近的现有向量,如
,并使用反向索引查找代表词,那么这个词很可能与我们人类之前想到的词相同,即 English。
诸如余弦相似度等算法可以用来找到与计算出的向量最接近的向量。
对于实现来说,找到最接近向量的计算效率更高的方式是 近似最近邻 (ANN),可以通过 Python 的 annoy 库来实现。
尽管我们通过认知和深度学习方法帮助得出了相同的结果,但两者的输入并不相同。对于人类,我们给出的句子与计算机相同,但对于深度学习应用,我们仔细选择了正确的单词(French、Paris 和 London)及其在方程中的正确位置以获得结果。想象一下,我们如何能轻松地意识到需要关注的正确单词,以理解正确的上下文,从而得出结果;但在当前的形式下,我们的深度学习方法无法做到这一点。
现在,使用不同变体和架构的 RNN(如 LSTM 和 Seq2Seq)的语言建模算法已经相当复杂。这些算法本来可以解决这个问题并得到正确的答案,但它们在较短且直接的句子中最为有效,例如巴黎对于法语来说,就像伦敦对于 _____ 一样。为了正确理解一个长句并生成正确的结果,重要的是要有一种机制来教导架构,使得在一长串单词中需要特别关注某些特定的词。这就是深度学习中的注意力机制,它适用于许多类型的深度学习应用,但方式略有不同。
RNN代表循环神经网络,用于表示深度学习中的时间序列数据。由于梯度消失问题,RNN 很少被直接使用;相反,其变体,如LSTM(长短期记忆网络)和GRU(门控循环单元)在实际应用中更为流行。Seq2Seq代表序列到序列模型,由两个 RNN(或其变体)网络组成(因此被称为Seq2Seq,其中每个 RNN 网络表示一个序列);一个作为编码器,另一个作为解码器。这两个 RNN 网络可以是多层或堆叠的 RNN 网络,并通过一个思维或上下文向量连接。此外,Seq2Seq 模型可以使用注意力机制来提高性能,特别是对于更长的序列。
事实上,更准确地说,即使我们需要分层处理前面的信息,首先要理解最后一句话是关于 Alya 的。然后我们可以识别并提取 Alya 所在的城市,接着是 Alice 的城市,依此类推。人类这种分层思维方式类似于深度学习中的堆叠,因此在类似的应用中,堆叠架构非常常见。
若想更了解堆叠在深度学习中的运作方式,特别是在基于序列的架构中,可以探讨一些话题,如堆叠 RNN 和堆叠注意力网络。
本章将涵盖以下主题:
-
图像字幕生成的注意力机制
-
注意力机制的类型(硬注意力和软注意力)
-
使用注意力机制改善视觉模型
- 视觉注意力的递归模型
图像字幕生成的注意力机制
从介绍中,到目前为止,您一定已经清楚注意力机制是在一系列对象上工作的,为每个序列中的元素分配一个特定迭代所需输出的权重。随着每一步的推进,不仅序列会改变,注意力机制中的权重也会发生变化。因此,基于注意力的架构本质上是序列网络,最适合使用 RNN(或其变体)在深度学习中实现。
现在的问题是:我们如何在静态图像上实现基于序列的注意力,尤其是在卷积神经网络(CNN)中表示的图像上?好吧,让我们通过一个介于文本和图像之间的例子来理解这个问题。假设我们需要根据图像内容为其生成标题。
我们有一些带有人工提供的图像和标题作为训练数据,基于这些数据,我们需要创建一个系统,能够为任何新图像生成合适的标题,且这些图像此前未曾被模型见过。如前所述,我们可以举个例子,看看作为人类我们如何理解这个任务,以及需要在深度学习和卷积神经网络(CNN)中实现的类似过程。让我们考虑以下这张图像,并为其构思一些合理的标题。我们还将通过人工判断对这些标题进行启发式排序:

一些可能的标题(从最可能到最不可能的顺序)是:
-
女人在雪林中看着狗
-
棕色狗在雪中
-
一位戴帽子的人在森林和白雪覆盖的土地上
-
狗、树、雪、人、阳光
这里需要注意的一点是,尽管女人在图像中是中心对象,而狗并不是图像中最大的物体,但我们寻求的标题显然集中在它们及其周围环境。这是因为我们认为它们在此处是重要的实体(假设没有其他上下文)。作为人类,我们如何得出这个结论的过程如下:我们首先快速浏览了整张图像,然后将焦点集中在女人身上,并以高分辨率呈现她,同时将背景的其他部分(假设是散景效果)置于低分辨率。我们为此部分确定了标题,然后将狗呈现为高分辨率,背景其他部分则保持低分辨率;接着我们补充了与狗相关的标题部分。最后,我们对周围环境做了相同处理并为之添加了标题部分。
所以,本质上,我们通过以下顺序得出第一个标题:

图像 1:先快速浏览图像

图像 2:聚焦于女人

图像 3:聚焦于狗

图像 4:聚焦于雪

图像 5:聚焦于森林
从注意力或焦点的权重来看,在快速浏览图像之后,我们会将注意力集中在图像中最重要的第一个物体上:这里是女人。这个过程类似于在创建一个心理框架,在这个框架中,我们将包含女人的图像部分置于高分辨率,而图像的其余部分则保持低分辨率。
在深度学习的参考中,注意力序列将会为表示女人概念的向量(嵌入)分配最高的权重。在输出/序列的下一步中,权重将更多地向代表狗的向量偏移,依此类推。
为了直观理解这一点,我们将以 CNN 形式表示的图像转换为一个扁平化的向量或其他类似的结构;然后,我们创建图像的不同切片或具有不同部分和不同分辨率的序列。同时,正如我们在第七章中讨论的那样,使用 CNN 进行目标检测与实例分割,我们必须拥有需要检测的相关部分,并且这些部分也必须在不同的尺度下进行有效的检测。这个概念在这里同样适用,除了分辨率,我们还会改变尺度;但为了直观理解,我们暂时忽略尺度部分,保持简单。
这些图像切片或序列现在充当一系列单词,就像我们之前的例子一样,因此它们可以在 RNN/LSTM 或类似的基于序列的架构中进行处理,用于注意力机制。这样做的目的是在每次迭代中获得最合适的单词作为输出。因此,序列的第一次迭代会得到 woman(来自表示Woman的序列在Image 2中的权重)→ 然后下一次迭代得到→ seeing(来自表示Woman背面的序列,如Image 2)→ Dog(来自Image 3中的序列)→ in(来自所有模糊像素生成的序列,生成填充词,从实体过渡到环境)→ Snow(来自Image 4中的序列)→ Forest(来自Image 5中的序列)。
填充词如in和动作词如seeing也可以通过在人类生成的字幕与多张图像之间进行最佳图像切片/序列映射时自动学习。但在更简单的版本中,像Woman、Dog、Snow和Forest这样的字幕也能很好地描述图像中的实体和周围环境。
注意力的类型
注意力机制有两种类型。它们如下:
-
硬性注意力
-
软性注意力
接下来,我们将详细了解每一种类型。
硬性注意力
实际上,在我们最近的图像描述示例中,会选择更多的图片,但由于我们用手写字幕进行训练,这些图片的权重永远不会更高。然而,重要的是要理解系统如何理解所有像素(或者更精确地说,是它们的 CNN 表示),系统聚焦于这些像素,以绘制不同方面的高分辨率图像,并且如何选择下一个像素以重复这一过程。
在前面的例子中,点是从分布中随机选择的,并且该过程会重复进行。而且,哪些像素会获得更高的分辨率是由注意力网络内部决定的。这种类型的注意力被称为硬性注意力。
硬注意力存在所谓的可微性问题。我们花些时间来理解这个问题。我们知道,在深度学习中,网络需要训练,而训练它们的方式是通过遍历训练批次来最小化损失函数。我们可以通过沿着最小值的梯度方向改变权重,从而最小化损失函数,这样就可以得到最小值,而这个过程是通过对损失函数进行微分得到的。*
这一过程,即从最后一层到第一层在深度网络中最小化损失,是反向传播。
在深度学习和机器学习中使用的一些可微损失函数示例包括对数似然损失函数、平方误差损失函数、二项式和多项式交叉熵等。
然而,由于在每次迭代中硬注意力是随机选择点的——而且这种随机像素选择机制不是一个可微函数——我们本质上无法训练这种注意力机制,正如前文所解释的。这个问题可以通过使用强化学习(RL)或切换到软注意力来解决。
强化学习涉及解决两个问题的机制,可能是分别解决,也可能是结合解决。第一个问题叫做控制问题,它决定了在给定状态下,代理在每一步应该采取的最优动作;第二个问题叫做预测问题,它决定了状态的最优值。
软注意力
正如在前面的硬注意力小节中介绍的那样,软注意力使用强化学习逐步训练并确定下一步寻找的地方(控制问题)。
使用硬注意力和强化学习(RL)结合来实现所需目标存在两个主要问题:
-
将强化学习和训练基于强化学习的代理及递归神经网络(RNN)/深度网络分开处理会稍显复杂。
-
策略函数的梯度方差不仅很高(如A3C模型),而且其计算复杂度为O(N),其中N是网络中单元的数量。这大大增加了此类方法的计算负担。此外,由于注意力机制在过长的序列(无论是词语还是图像嵌入片段)中能提供更多价值——而且训练涉及更长序列的网络需要更大的内存,因此需要更深的网络——这种方法在计算上并不高效。
强化学习中的策略函数,表示为Q(a,s),是用来确定最优策略或在给定状态(s)下应该采取的动作(a),以最大化奖励的函数。
那么,替代方案是什么呢?正如我们所讨论的,问题的出现是因为我们为注意力机制选择的方式导致了一个不可微的函数,因此我们不得不使用强化学习(RL)。所以让我们在这里采取不同的方式。通过类比我们在前面提到的语言建模问题示例(如在Attention 机制 - 直觉*部分),我们假设我们已经有了注意力网络中对象/词语的向量。此外,在同一向量空间中(比如嵌入超空间),我们将特定序列步骤中的查询所需的对象/词语的标记引入。采用这种方法,找到注意力网络中标记的正确注意力权重与查询空间中的标记之间的关系,就像计算它们之间的向量相似度一样简单;例如,计算余弦距离。幸运的是,大多数向量距离和相似度函数都是可微的;因此,使用这种向量距离/相似度函数在该空间中推导出的损失函数也是可微的,我们的反向传播可以在这种情况下正常工作。
两个向量之间的余弦距离,例如
和
,在一个多维(此例为三维)向量空间中的计算公式为:
使用可微损失函数训练注意力网络的方法被称为软注意力。
使用注意力来改善视觉模型
正如我们在之前的注意力机制 - 直觉部分的 NLP 示例中发现的,注意力确实帮助我们在实现新的用例方面取得了巨大进展,这些用例是传统 NLP 无法高效实现的,同时也大大提高了现有 NLP 机制的性能。在 CNN 和视觉模型中,注意力的使用也是类似的。
在前一章第七章,基于 CNN 的目标检测与实例分割中,我们发现了如何使用类似注意力机制的区域提议网络(如 Faster R-CNN 和 Mask R-CNN),大大增强和优化了提议区域,并生成分割掩码。这对应于讨论的第一部分。在本节中,我们将讨论第二部分,我们将使用“注意力”机制来提高我们 CNN 的性能,即使在极端条件下。
视觉 CNN 模型性能不佳的原因
通过采用适当的调优和设置机制,CNN 网络的性能可以在一定程度上得到改善,这些机制包括:数据预处理、批归一化、权重的最佳预初始化;选择正确的激活函数;使用正则化技术来避免过拟合;使用最佳的优化函数;以及使用大量(优质)数据进行训练。
除了这些训练和架构相关的决策外,还有与图像相关的细节,这些细节可能影响视觉模型的表现。即便在控制了上述训练和架构因素后,传统的基于 CNN 的图像分类器在以下一些与底层图像相关的条件下表现不佳:
-
非常大的图像
-
包含多个分类实体的高度杂乱的图像
-
非常嘈杂的图像
让我们尝试理解在这些条件下性能不佳的原因,然后我们将从逻辑上理解可能修复问题的方案。
在传统的基于 CNN 的模型中,即便是经过层间下采样,计算复杂度仍然相当高。实际上,复杂度是以
为量级,其中 L 和 W 是图像的长度和宽度(以英寸为单位),PPI 是每英寸的像素数(像素密度)。这意味着计算复杂度与图像中总像素数(P)线性相关,即 O(P)。这直接回答了挑战中的第一个问题;对于更高的 L、W 或 PPI,我们需要更高的计算能力和时间来训练网络。
操作如最大池化、平均池化等有助于大幅减少计算负担,相比于在所有层中对实际图像进行的所有计算。
如果我们可视化 CNN 中每一层形成的模式,我们将理解 CNN 工作原理背后的直觉,并且明白为什么它需要是深层的。在每一层中,CNN 训练更高层次的概念特征,这些特征逐层帮助更好地理解图像中的物体。所以,在 MNIST 的情况下,第一层可能只识别边界,第二层识别基于边界的对角线和直线形状,以此类推:

在 CNN 的不同(初始)层中形成的 MNIST 相关的概念特征

MNIST 是一个简单的数据集,而现实生活中的图像则相当复杂;这需要更高层次的概念特征来区分它们,因此需要更复杂且更深的网络。此外,在 MNIST 中,我们试图区分相似类型的物体(所有都是手写数字)。而在现实生活中,物体可能差异很大,因此需要的不同特征类型也非常多:

这引出了我们的第二个挑战。一个包含过多物体的杂乱图像需要一个非常复杂的网络来建模所有这些物体。此外,由于需要识别的物体太多,图像分辨率需要足够高才能正确提取和映射每个物体的特征,这也意味着图像大小和像素数量需要足够高,以便进行有效的分类。这反过来会通过结合前两个挑战,成倍增加复杂性。
在 ImageNet 挑战赛中使用的流行 CNN 架构的层数,以及因此而增加的复杂性,近年来不断增加。一些例子包括:VGG16(2014)有 16 层,GoogLeNet(2014)有 19 层,ResNet(2015)有 152 层。
并非所有图像都具有完美的单反相机(SLR)质量。通常,由于低光照、图像处理、低分辨率、缺乏稳定性等原因,图像中可能会引入大量噪声。这只是噪声的一种形式,是比较容易理解的一种。从卷积神经网络(CNN)的角度来看,噪声的另一种形式可能是图像过渡、旋转或变换:

没有噪声的图像

添加噪声后的同一图像
在前面的图像中,试着在带噪声和不带噪声的图像中阅读报纸标题Business,或者在两张图像中识别手机。带噪声的图像中很难做到这一点,对吧?这就像在噪声图像的情况下,CNN 的检测/分类挑战一样。
即使经过大量训练,完美的超参数调整,以及诸如丢弃法等技术,这些现实中的挑战依然会降低 CNN 网络的图像识别准确性。现在我们已经理解了导致 CNN 准确性和性能不足的原因和直觉,让我们探讨一些使用视觉注意力来缓解这些挑战的方法和架构。
视觉注意力的递归模型
视觉注意力的递归模型可以用来解决我们在前面部分提到的一些挑战。这些模型使用硬注意力方法,正如在之前的(注意力类型)部分中所讲述的那样。在这里,我们使用的是一种流行的视觉注意力递归模型变体——递归注意力模型(RAM)。
如前所述,硬注意力问题是不可微分的,因此必须使用强化学习(RL)来解决控制问题。因此,RAM 使用强化学习来进行此优化。
视觉注意力的递归模型不会一次性处理整个图像,甚至不会处理基于滑动窗口的边界框。它模仿人眼的工作方式,基于图像中不同位置的注视,并结合每次注视所获得的重要信息,逐步建立起图像场景的内部表示。它使用递归神经网络(RNN)以顺序方式进行处理。
模型根据 RL 智能体的控制策略选择下一个要固定的位置信息,以最大化基于当前状态的奖励。当前状态又是所有过去信息和任务需求的函数。因此,它找到下一个固定坐标,以便在已经收集的信息基础上(通过 RNN 的记忆快照和先前访问的坐标)最大化奖励(任务需求)。
大多数 RL 机制使用马尔可夫决策过程(MDP),其中下一个动作仅由当前状态决定,而与之前访问的状态无关。在这里使用 RNN,能够将来自先前固定视点的重要信息结合到当前状态中。
上述机制解决了 CNN 在前面部分中强调的最后两个问题。此外,在 RAM 中,参数的数量和计算量可以独立于输入图像的大小进行控制,从而也解决了第一个问题。
在噪声 MNIST 样本上应用 RAM
为了更详细地理解 RAM 的工作原理,让我们尝试创建一个包含一些早期部分所提到的问题的 MNIST 样本:

更大的噪声和失真的 MNIST 图像
上图展示了一个较大的图像/拼贴,使用了一个实际且略微噪声化的 MNIST 图像(数字2),以及其他一些失真和部分样本的片段。此外,实际的数字2并未居中。此示例代表了之前所述的所有问题,但足够简单,便于理解 RAM 的工作原理。
RAM 使用瞥视传感器的概念。RL 智能体将目光固定在特定的坐标(l)和特定的时间(t-1)。在时间 t-1 时刻,坐标l[t-1]和图像x[t]的内容,通过瞥视传感器提取以l[t-1]为中心的类似视网膜的多分辨率图像补丁。这些在时间t-1提取的表示 collectively 被称为p(x[t], l[t-1]):

瞥视传感器的概念
;
这些图像展示了我们的图像在两个固定视点下使用瞥视传感器的表示。
从瞥视传感器获得的表示经过'瞥视网络'处理,表示会在两个阶段被展平。在第一阶段,瞥视传感器和瞥视网络的表示分别被展平(
),然后它们合并为一个单一的展平层(
),以生成时间t的输出表示g[t]:

Glimpse 网络的概念
这些输出表示然后传递通过 RNN 模型架构。下一步的固定点由 RL 代理决定,以最大化来自此架构的奖励:

模型架构(RNN)
如直观理解所示,Glimpse 传感器捕捉了跨越注视点的重要信息,这有助于识别重要的概念。例如,我们第二个示例图像中表示的 Fixation 处的多个分辨率(此处为 3)具有标记的三种分辨率(按分辨率递减顺序为红色、绿色和蓝色)。如图所示,即使这些被直接使用,我们依然能够检测到由这一噪声拼贴表示的正确数字:


Glimpse 传感器代码
如前一节所讨论,Glimpse 传感器是一个强大的概念。结合 RNN 和 RL 等其他概念,如前所述,它是提高视觉模型性能的核心。
让我们在这里更详细地查看。代码的每一行都有注释,方便理解,并且自解释:
import tensorflow as tf
# the code is in tensorflow
import numpy as np
def glimpseSensor(image, fixationLocation):
'''
Glimpse Sensor for Recurrent Attention Model (RAM)
:param image: the image xt
:type image: numpy vector
:param fixationLocation: cordinates l for fixation center
:type fixationLocation: tuple
:return: Multi Resolution Representations from Glimpse Sensor
:rtype:
'''
img_size=np.asarray(image).shape[:2]
# this can be set as default from the size of images in our dataset, leaving the third 'channel' dimension if any
channels=1
# settings channels as 1 by default
if (np.asarray(img_size).shape[0]==3):
channels=np.asarray(image).shape[-1]
# re-setting the channel size if channels are present
batch_size=32
# setting batch size
loc = tf.round(((fixationLocation + 1) / 2.0) * img_size)
# fixationLocation coordinates are normalized between -1 and 1 wrt image center as 0,0
loc = tf.cast(loc, tf.int32)
# converting number format compatible with tf
image = tf.reshape(image, (batch_size, img_size[0], img_size[1], channels))
# changing img vector shape to fit tf
representaions = []
# representations of image
glimpse_images = []
# to show in window
minRadius=img_size[0]/10
# setting the side size of the smallest resolution image
max_radius=minRadius*2
offset = 2 * max_radius
# setting the max side and offset for drawing representations
depth = 3
# number of representations per fixation
sensorBandwidth = 8
# sensor bandwidth for glimpse sensor
# process each image individually
for k in range(batch_size):
imageRepresentations = []
one_img = image[k,:,:,:]
# selecting the required images to form a batch
one_img = tf.image.pad_to_bounding_box(one_img, offset, offset, max_radius * 4 + img_size, max_radius * 4 + img_size)
# pad image with zeros for use in tf as we require consistent size
for i in range(depth):
r = int(minRadius * (2 ** (i)))
# radius of draw
d_raw = 2 * r
# diameter
d = tf.constant(d_raw, shape=[1])
# tf constant for dia
d = tf.tile(d, [2])
loc_k = loc[k,:]
adjusted_loc = offset + loc_k - r
# location wrt image adjusted wrt image transformation and pad
one_img2 = tf.reshape(one_img, (one_img.get_shape()[0].value, one_img.get_shape()[1].value))
# reshaping image for tf
representations = tf.slice(one_img2, adjusted_loc, d)
# crop image to (d x d) for representation
representations = tf.image.resize_bilinear(tf.reshape(representations, (1, d_raw, d_raw, 1)), (sensorBandwidth, sensorBandwidth))
# resize cropped image to (sensorBandwidth x sensorBandwidth)
representations = tf.reshape(representations, (sensorBandwidth, sensorBandwidth))
# reshape for tf
imageRepresentations.append(representations)
# appending the current representation to the set of representations for image
representaions.append(tf.stack(imageRepresentations))
representations = tf.stack(representations)
glimpse_images.append(representations)
# return glimpse sensor output
return representations
参考文献
-
Kelvin Xu, Jimmy Ba, Ryan Kiros, Kyunghyun Cho, Aaron C. Courville, Ruslan Salakhutdinov, Richard S. Zemel, Yoshua Bengio, Show, Attend and Tell: 基于视觉注意力的神经图像描述生成,CoRR,arXiv:1502.03044,2015 年。
-
Karl Moritz Hermann, Tom's Kocisk, Edward Grefenstette, Lasse Espeholt, Will Kay, Mustafa Suleyman, Phil Blunsom, 教机器阅读与理解,CoRR,arXiv:1506.03340,2015 年。
-
Volodymyr Mnih, Nicolas Heess, Alex Graves, Koray Kavukcuoglu, 视觉注意力的递归模型,CoRR,arXiv:1406.6247,2014 年。
-
Long Chen, Hanwang Zhang, Jun Xiao, Liqiang Nie, Jian Shao, Tat-Seng Chua, SCA-CNN: 卷积网络中的空间与通道注意力用于图像描述,CoRR,arXiv:1611.05594,2016 年。
-
Kan Chen, Jiang Wang, Liang-Chieh Chen, Haoyuan Gao, Wei Xu, Ram Nevatia, ABC-CNN: 一种基于注意力的卷积神经网络用于视觉问答,CoRR,arXiv:1511.05960,2015 年。
-
Wenpeng Yin, Sebastian Ebert, Hinrich Schutze, 基于注意力的卷积神经网络用于机器理解,CoRR,arXiv:1602.04341,2016 年。
-
Wenpeng Yin, Hinrich Schutze, Bing Xiang, Bowen Zhou, ABCNN: 基于注意力的卷积神经网络用于建模句子对,CoRR,arXiv:1512.05193,2015 年。
-
Zichao Yang, Xiaodong He, Jianfeng Gao, Li Deng, Alexander J. Smola, 用于图像问答的堆叠注意力网络,CoRR,arXiv:1511.02274,2015 年。
-
Y. Chen, D. Zhao, L. Lv 和 C. Li,一种基于视觉注意力的卷积神经网络用于图像分类,2016 年第 12 届世界智能控制与自动化大会(WCICA),桂林,2016 年,第 764-769 页。
-
H. Zheng,J. Fu,T. Mei 和 J. Luo,学习多注意力卷积神经网络用于细粒度图像识别,2017 年 IEEE 国际计算机视觉会议(ICCV),威尼斯,2017 年,5219-5227 页。
-
肖天俊、徐一冲、杨奎远、张家兴、彭宇欣、张正,两级注意力模型在深度卷积神经网络中的应用:用于细粒度图像分类,CoRR,arXiv:1411.6447,2014 年。
-
Jlindsey15,循环注意力模型的 TensorFlow 实现,GitHub,
github.com/jlindsey15/RAM,2018 年 2 月。 -
QihongL,循环注意力模型的 TensorFlow 实现,GitHub,
github.com/QihongL/RAM,2018 年 2 月。 -
Amasky,循环注意力模型,GitHub,
github.com/amasky/ram,2018 年 2 月。
摘要
注意力机制是当今深度学习中最热门的话题,被认为是当前研究中大多数前沿算法的核心,并且在未来的应用中也可能处于中心地位。像图像描述、视觉问答等问题,已经通过这种方法得到了很好的解决。事实上,注意力机制不仅限于视觉任务,早期也被应用于神经机器翻译等复杂的自然语言处理问题。因此,理解注意力机制对于掌握许多高级深度学习技术至关重要。
卷积神经网络(CNN)不仅用于视觉任务,还在许多应用中与注意力机制结合,用于解决复杂的自然语言处理问题,如建模句子对和机器翻译。本章介绍了注意力机制及其在一些自然语言处理问题中的应用,以及图像描述和循环视觉模型。在 RAM 中,我们没有使用 CNN,而是将 RNN 和注意力机制应用于从 Glimpse 传感器获得的图像缩小表示。然而,最近的研究也开始将注意力机制应用于基于 CNN 的视觉模型。
强烈建议读者参考文献中的原始论文,并探索使用注意力机制的高级概念,如多层次注意力、堆叠注意力模型以及使用 RL 模型(例如异步优势行为者-批评家(A3C)模型解决硬注意力控制问题)。


浙公网安备 33010602011771号