《神经网络和深度学习》系列文章四十五:卷积神经网络在实际中的应用
出处: Michael Nielsen的《Neural Network and Deep Learning》,点击末尾“阅读原文”即可查看英文原文。
声明:我们将在每周四连载该书的中文翻译。
本节译者:朱小虎 、张广宇。转载已获得译者授权,禁止二次转载。
使用神经网络识别手写数字
反向传播算法是如何工作的
改进神经网络的学习方法
神经网络可以计算任何函数的可视化证明
为什么深度神经网络的训练是困难的
深度学习
介绍卷积网络
卷积神经网络在实际中的应用
卷积网络的代码
图像识别领域中的近期进展
其他的深度学习模型
神经网络的未来
我们现在已经明白了卷积神经网络后面的核心思想。让我们通过实现一些卷积网络,并将它们应用于 MNIST 数字分类问题,来看看它们如何在实践中工作。我们将使用的程序是 network3.py,它是前面章节开发的 network.py 和 network2.py 的强化版本[1]。如果你想跟着学,代码可以从 GitHub 上下载。注意我们将在下一节中解决 network3.py 需要的代码。在这一节中,我们将把 network3.py 作为库来构建卷积网络。
程序 network.py 和 network2.py 是用 Python 和矩阵库 Numpy 实现的。这些程序从最初的原理工作,并致力于反向传播、随即梯度下降等细节。但是现在我们已经理解了这些细节,对于 network3.py 我们打算使用一个称为 Theano 的机器学习库[2]。使用 Theano 使得实现针对卷积神经网络的反向传播很容易,因为它自动计算涉及到的映射。Theano 也比我们前面代码更快(那些代码是为了容易理解,不是为了运行速度),这使它可实际用于训练更复杂的网络。特别地,Theano 的一个非常好的特性是它能够运行于 CPU 或者,如果可以,GPU 上。运行于 GPU 上可以提供显著的增速,而且,有助于实际用于更复杂的网络。
如果你想要跟着学,你需要可运行在你的系统上的 Theano。按照项目主页上的说明来安装 Theano。接下来的例子使用 Theano 0.6[3] 运行过。有些在没有 GPU 支持的 Mac OS X Yosemite 运行过。有些在有 NVIDIA GPU 支持的 Ubuntu 14.04 中运行过。有些实验在两个系统中都运行过。为了让 networks3.py 运行,你需要(适当地)把 networks3.py 源码中的 GPU 标志设置为 True 或者 False。此外,为了让 Theano 运行于 GPU 上,你可能会发现这份指导说明有帮助。互联网上也有教程,很容易用 Google 搜索到,同样能帮助你让 Theano 工作。如果你手上的系统没有可用的 GPU,那么你可能想要看下 Amazon Web Services EC2 G2 实例类型。注意即使有 GPU 支持,代码仍然需要一些时间执行。许多实验要花费从几分钟到几个小时的时间来运行。在 CPU 上可能需要花费数天时间来运行最复杂的实验。正如前面章节里说的,我建议让程序运行着,同时继续阅读,偶尔回来检查下代码的输出。如果你用的是 CPU,你可能需要对更复杂的实验减少训练迭代期的数量,或者整个忽略它们。
为了取得一个基线,我们将从一个浅层架构开始,它仅仅使用一个隐藏层,包含
>>> import network3
>>> from network3 import Network
>>> from network3 import ConvPoolLayer, FullyConnectedLayer, SoftmaxLayer
>>> training_data, validation_data, test_data = network3.load_data_shared()
>>> mini_batch_size = 10
>>> net = Network([
FullyConnectedLayer(n_in=784, n_out=100),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(training_data, 60, mini_batch_size, 0.1,
validation_data, test_data)
我获得的一个最好的分类准确率是
这个
然而,在之前的网络中有两个不同的地方。首先,我们规范化了之前的网络,来帮助降低过度拟合带来的影响。规范化当前的网络确实可以提高准确率,但是得到的只是很小,所以我
们将推迟到后面再来惦记规范化。第二,虽然之前的网络中的最终层使用了S型激活值和交叉熵代价函数,当前网络使用一个柔性最大值的最终层,以及对数似然代价函数。正如第三
章中解释的,这不是一个大的改变。我没有为了任何特别深刻的原因来做出这样的改变 —— 主要是因为柔性最大值和对数似然代价在现代的图像分类网络中很常见。
我们能用一个更深的网络架构来做得比这些结果更好吗?
让我们从在网络开始位置的右边插入一个卷积层开始。我们将使用
在这个架构中,我们可以把卷积和混合层看作是在学习输入训练图像中的局部感受野,而后面的全连接层则在一个更抽象的层次学习,从整个图像整合全局信息。这是一种常见的卷积
神经网络模式。
让我们训练这样的一个网络,看看它表现怎样[6]:
>>> net = Network([
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2)),
FullyConnectedLayer(n_in=20*12*12, n_out=100),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(training_data, 60, mini_batch_size, 0.1,
validation_data, test_data)
我们得到了
在指定网络结构时,我把卷积和混合层作为一个单一层对待。不管他们是被视为分开的层还是作为一个单一的层在一定程度上是一个个人喜好的问题。network3.py 视他们为单个层,因为它使得 network3.py 的代码更紧凑。然而,如果需要的话,很容易修改 network3.py 使得这些层可以单独指定。
练习
如果你删除了全连接层,只使用卷积-混合层和柔性最大值层,你得到了什么样的分类准确率?全连接层的加入有帮助吗?
我们能改进
让我们试着插入第二个卷积–混合层。把它插在已有的卷积–混合层和全连接隐藏层之间。我们再次使用一个
>>> net = Network([
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2)),
ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
filter_shape=(40, 20, 5, 5),
poolsize=(2, 2)),
FullyConnectedLayer(n_in=40*4*4, n_out=100),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(training_data, 60, mini_batch_size, 0.1,
validation_data, test_data)
再一次,我们得到了改善:现在我们达到了
在这里有两个很自然想到的问题。第一个问题是:应用第二个卷积–混合层意味着什么?实际上,你可以认为第二个卷积–混合层输入
这是一个令然满意的观点,但是引出了第二个问题。从前面层的输出涉及
问题
使用 tanh 激活函数 在本书前面我已经几次提起过 tanh 函数可以是一个比 S型函数更好的激活函数。我们还没有实际采用过这些建议,因为我们已经用 S 型取得了大量进展。但现在让我们试试一些用 tanh 作为我们激活函数的实验。试着训练卷积和全连接层中具有 tanh 激活值的网络[8]。开始时使用 S 型网络中使用的相同的超参数,但是训练
个迭代期,而不是 个。你的网络表现得怎么样?如果你继续训练到 个迭代期会怎样?试着将tanh和 S型网络的每个迭代期的验证准确率都绘制出来,都绘制到 个迭代期。如果你的结果和我的相似,你会发现 tanh 网络训练得稍微快些,但是最终的准确率非常相似。你能否解释为什么 tanh 网络可以训练得更快?你能否用 S型取得一个相似的训练速度,也许通过改变学习速率,或者做些调整[9]?试着用五六个迭代学习超参数和网络架构,寻找 tanh 优于 S 型的方面。注意:这是一个开放式问题。就我个人而言,我并没有找到太多切换为 tanh 的优势,虽然我没全面地做过实验,也许你会找到一个方法。无论如何,我们马上会发现切换到修正线性激活函数的一个优势,所以我们不会去深入使用 tanh 函数。
使用修正线性单元: 到现在为止,我们开发的网络实际上是一篇开创性的 1998 论文[10]中使用的众多网络中一种的变化形式,这个网络被称为 LeNet-5,并引入了 MNIST 问题。这为进一步实验并构筑理解和直观感受打下很好的基础。特别是,有很多种我们可以改变网络来改善结果的方式。
作为开始,让我们改变我们的神经元,我们使用修正线性单元而不是 S 型激活函数。确切地说,我们将使用激活函数
>>> from network3 import ReLU
>>> net = Network([
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
filter_shape=(40, 20, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
FullyConnectedLayer(n_in=40*4*4, n_out=100, activation_fn=ReLU),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(training_data, 60, mini_batch_size, 0.03,
validation_data, test_data, lmbda=0.1)
我得到一个 99.23% 的分类准确率。它稍微超过了 S 型的结果(99.06)。然而,在我所有实验中我发现基于修正线性单元的网络,其性能始终优于基于 S 型激活函数的网络。似乎对于这个问题切换到修正线性单元确实有收益。
是什么使得修正线性激活函数好于 S 型或者 tanh 函数?目前,我们对这个问题的答案有一个很差的理解。实际上,修正线性单元只在过去几年才开始被广泛使用。最近才采用的原因是以经验为依据的:一些人经常基于直觉或者启发式的理由试着用修正线性单元[11]。他们在分类基准数据集时取得了很好的结果,并且其实践传播开了。在一个理想的世界中,我们有一个理论告诉我们为什么样的应用选择什么样的激活函数。但目前我们我们离这样的理想世界还有一条很长的路。如果通过选择一个更好的激活函数来取得了进一步的重大改进,我一点也不会感到惊讶,我还期待在未来的几十年里,一个强大的激活函数理论将被开发。今天,我们仍然不得不依靠单凭经验的不足的理解。
扩展训练数据:另一种我们可能希望改进结果的方法是以算法形式扩展训练数据。扩展训练数据的一个简单的方法是将每个训练图像由一个像素来代替,无论是上一个像素,一个像素,左边一个像素,或右边一个像素。我们可以通过在 shell 提示符中运行程序 expand_mnist.py 来这样做[12]:
$ python expand_mnist.py
运行这个程序取得
>>> expanded_training_data, _, _ = network3.load_data_shared(
"../data/mnist_expanded.pkl.gz")
>>> net = Network([
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
filter_shape=(40, 20, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
FullyConnectedLayer(n_in=40*4*4, n_out=100, activation_fn=ReLU),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(expanded_training_data, 60, mini_batch_size, 0.03,
validation_data, test_data, lmbda=0.1)
使用扩展后的训练数据我取得了一个 99.37% 的训练准确率。所以这个几乎是微不足道的改变在分类准确率上给出了一个显著的改进。事实上,正如我们前面所讨论的,这种以算法形式扩展数据的想法可以更进一步。提醒你一些早期讨论的结果:在 2003 年,Simard,Steinkraus 和 Platt[13] 使用一个神经网络改进了他们的 MNIST 性能,达到了
问题
卷积层的想法是以一种横跨图像不变的方式作出反应。它看上去令人惊奇,然而,当我们做完所有输入数据的转换,网络能学习得更多。你能否解释为什么这实际上很合理?
插入一个额外的全连接层:我们还能做得更好吗?一种可能性是使用和上面完全相同的程序,但是扩展全连接层的规模。我试过
增加一个额外的全连接层怎样?让我们试着插入一个全连接层,这样我们就有两个
>>> net = Network([
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
filter_shape=(40, 20, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
FullyConnectedLayer(n_in=40*4*4, n_out=100, activation_fn=ReLU),
FullyConnectedLayer(n_in=100, n_out=100, activation_fn=ReLU),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(expanded_training_data, 60, mini_batch_size, 0.03,
validation_data, test_data, lmbda=0.1)
这样做我取得一个 99.43% 的测试准确率。再一次,扩展后的网络并没有帮助太多。运行类似的试验,用包含
这里发生了什么事?扩展的,或者额外的全连接层真的对 MNIST 没帮助吗?或者说,我们的网络有能力做得更好,但我们在用错误的方式学习?例如,也许我们可以用更强有力的规
范化技术来减小过度拟合的趋势。一种可能性是第三章介绍的弃权技术。回想弃权的基本思想是在训练网络时随机地移除单独的激活值。这使得模型对单独依据的丢失更为强劲,因此不太可能依赖于训练数据的特质。让我们试着应用弃权到最终的全连接层:
>>> net = Network([
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
filter_shape=(40, 20, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
FullyConnectedLayer(
n_in=40*4*4, n_out=1000, activation_fn=ReLU, p_dropout=0.5),
FullyConnectedLayer(
n_in=1000, n_out=1000, activation_fn=ReLU, p_dropout=0.5),
SoftmaxLayer(n_in=1000, n_out=10, p_dropout=0.5)],
mini_batch_size)
>>> net.SGD(expanded_training_data, 40, mini_batch_size, 0.03,
validation_data, test_data)
使用它,我们取得了
有两个值得注意的变化。
首先,我减少了训练迭代期的数量到
其次,全连接隐藏层有
使用一个组合的网络:一个简单的进一步提高性能的方法是创建几个神经网络,然后让它们投票来决定最好的分类。例如,假设我们使用上述的方式训练了
这听上去太好了,不像是真的,但是这种组合的方式是神经网络和其它机器学习技术都惯用的伎俩。而它确实产生了更进一步的改善:我们最终得到了 99.67% 的准确率。换句话说,
我们的网络组合正确分类了除了
剩余的测试集中的错误显示在下面。右上角的标签是按照 NMIST 数据的正确的分类,而右下角的标签是我们组合网络的输出。
值得去仔细看看这些。开头两个数字,一个 6 和一个 5,是我们的组合犯的真正的错误。然而,它们也是可以理解的错误,人类也会犯。那个 6 确实看上去更像一个 0,而那个 5看上去更像一个 3。第三幅图像,据称是一个 8,在我看来实际上更像一个 9。所以这里我站在网络组合这边:我认为它比最初画出这些数字的人做得更好。另一方面,第四幅图像,那个 6,确实看上去是我们网络分类错了。
如此等等。在大多数情况下我们网络的选择看上去至少是合理的,而在一些情况下我们比最初写这些数字的人做得更好。总体而言,我们的网络提供卓越的性能,特别是当你认为它们
正确分类的 9,967 张图片,这是没有显示。在这种背景下,这里的不清晰的错误似乎是可以理解的。甚至一个细心的人也会偶尔犯错误。因此我认为只有一个非常细心和有条理的人
才会做得更好。我们的网络正在接近人类的性能。
为什么我们只对全连接层应用弃权:如果你仔细看上面的代码,你会注意到我们只在网络的全链接部分应用了弃权,而不是卷积层。原则上我们可以在卷积层上应用一个类似的程序。但是,实际上那没必要:卷积层有相当大的先天的对于过度拟合的抵抗。原因是共享权重意味着卷积滤波器被强制从整个图像中学习。这使他们不太可能去选择在训练数据中的局部特质。于是就很少有必要来应用其它规范化,例如弃权。
进一步:仍然有可能在 MNIST 上提高性能。Rodrigo Benenson 汇编了一份信息汇总页面,显示这几年的进展,提供了论文的链接。这些论文许多使用深度卷积网络,与我们已经使用的网络相似。如果你深入挖掘这些论文你会发现许多有趣的技术,并且你可能乐于实现其中一些。如果你这样做,明智的做法是从一个简单的能被快速训练的网络开始实现,这将有助于你更快地了解正在发生的事。
我不会概述这份近期成果的大部分内容。但是我忍不住有一个例外。它是一篇 Cireșan、Meier、 Gambardella、 和 Schmidhuber 所著的 2010 年论文[14]。我喜欢这篇论文的地方是它是如此简单。其中的网络是一个许多层的神经网络,仅使用全连接层(没有卷积层)。他们最成功的网络有分别包含有
为什么我们能够训练? 我们在上一章看到了深的、多层的神经网络中的基本障碍。特别是,我们看到的梯度往往是相当不稳定的:当我们从输出层移动到前面层,梯度趋于消失(消失的梯度问题)或爆炸(爆炸的梯度问题)。由于梯度是我们用来训练的动机,这会导致问题。
我们如何避免这些结果?
当然,答案是我们没有回避这些结果。相反,我们已经做了一些事情,帮助我们继续进行。特别地:(1)使用卷积层极大地减少了这些层中的参数的数目,使学习的问题更容易;(2)使用更多强有力的规范化技术(尤其是弃权和卷积层)来减少过度拟合,否则它在更复杂的网络中是更多的问题;(3)使用修正线性单元而不是 S 型神经元,来加速训练 —— 依据经验通常是
你的反应可能是:“就这样?这就是我们为了训练深度网络所要做的全部事情?为什么要小题大做?”
当然,我们也已经使用了其它主意:利用充分大的数据集(为了避免过度拟合);使用正确的代价函数(为了避免学习减速);使用好的权重初始化(也是为了避免因为神经元饱和引起的学习减速);以算法形式扩展训练数据。我们在前面章节中讨论了这些和其它想法,并在本章中的大部分已经可以重用这些想法了,而不需要太多注解。
有了这样说法,这真的是一套相当简单的想法。在组合使用时简单,但功能强大。入门深度学习变得非常容易!
这些网络有多深? 把卷积–混合层算作一个层,我们最终的架构有
一些按部就班的话:在这一节中,我们从单个隐藏层的浅层网络顺利转换到多层卷积网络。这一切似乎很容易!我们做了一个改变,其中大部分,我们得到了改进。如果你开始尝试,我可以保证事情不会总是那么顺利。原因是,我呈现的是一个清理过的叙述,省略了许多实验 —— 包括许多失败的实验。这个清理过的叙述,希望能帮助你清楚认识基本思想。但它也有风险,传达着不完整的感觉。取得一个好的,可工作的网络会涉及到大量的试验和错误,偶尔有挫折。在实践中,你应该预计会处理相当多的实验。为了加快这一进程,你可能会发现回顾第三章关于如何选择一个神经网络的超参数的讨论会有帮助,或许也看一些那一小节中的进一步阅读的建议。
注:
1.注意 network3.py 包含了源自 Theano 库文档中关于卷积神经网络(尤其是 LeNet-5 (http://deeplearning.net/tutorial/lenet.html) 的实现),Misha Denil 的 弃权的实现 (https://github.com/mdenil/dropout),以及 Chris Olah (http://colah.github.io/) 的概念。
2.参见 Theano: A CPU and GPU Math Expression Compiler in Python (http://www.iro.umontreal.ca/~lisa/pointeurs/theano_scipy2010.pdf),作者为 James Bergstra, Olivier Breuleux, Frederic Bastien, Pascal Lamblin, fRavzan Pascanu, Guillaume Desjardins, Joseph Turian, David Warde-Farley, 和 Yoshua Bengio (2010)。 Theano 也是流行的 Pylearn2 (http://deeplearning.net/software/pylearn2/) 和 Keras (http://keras.io/) 神经网络库的基础。其它在本文写作时流行的神经网路库包括 Caffe (http://caffe.berkeleyvision.org/) 和 Torch (http://torch.ch/) 。
3.当我发布这一章时,Theano 的当前版本变成了 0.7。我实际上已经在 Theano 0.7 版本中重新运行过这些例子并取得了和文中非常相似的结果。
4.本节中的实验代码可以在 https://github.com/mnielsen/neural-networks-and-deep-learning/blob/master/src/conv.py 这个脚本中找到。注意,脚本中的代码只是简单地重复并相对于本节中的讨论。
5.实际上,在这个实验中我其实对这个架构的网络运行了三次独立的训练。然后我从这三次运行中报告了对应于最佳验证准确率的测试准确率。利用多次运行有助于减少结果中的变动,
它在比较许多个架构时是很有用的,正如我们正在做的。除非明确指出,我在下面已经遵循了这个程序。在实践中,它对于所获得的结果不会带来什么区别。
6.这里我继续使用一个大小为 10 的小批量数据。正如我们前面讨论过的,使用更大的小批量数据可能提高训练速度。我继续使用相同的小批量数据,主要是为了和前面章节中的实验保持一致。
7.如果输入图像是有颜色的,这个问题会在第一层中出现。在这种情况下,对于每一个像素我们会有 3个输入特征,对应于输入图像中的红色、绿色和蓝色通道。因此我们将允许特征检测器可访问所有颜色信息,但仅仅在一个给定的局部感受野中。
8.注意你可以将 activation_fn=tanh 作为一个参数传递给 ConvPoolLayer 和 FullyConnectedLayer 类。
9.你也许可以回想
10.“Gradient-based learning applied to document recognition”(http://yann.lecun.com/exdb/publis/pdf/lecun-98.pdf),作者为 Yann LeCun, Léon Bottou, Yoshua Bengio, 和 Patrick Haffner (1998)。细节上有很多不同,但大体上讲,我们的网络和论文中描述的网络非常相似。
11.一个通常的理由是 max(0,z) 在 z 取最大极限时不会饱和,不像 S 型神经元,而这有助于修正线性单元持续学习。到目前为止,这一辩解很好,但不是一个详细的理由,更多的是一个“就这样”的故事。注意我们在第二章里讨论过饱和的问题。
12. expand_mnist.py 的代码可以从 https://github.com/mnielsen/neural-networks-and-deep-learning/blob/master/src/expand_mnist.py 这里获取。
13. Best Practices for Convolutional Neural Networks Applied to Visual Document Analysis (http://dx.doi.org/10.1109/ICDAR.2003.1227801),作者为 Patrice Simard, Dave Steinkraus, 和 John Platt (2003)。
14. Deep, Big, Simple Neural Nets Excel on Handwritten Digit Recognition (http://arxiv.org/abs/1003.0358),作者为 Dan Claudiu Cireșan, Ueli Meier,Luca Maria Gambardella, 和 Jürgen Schmidhuber (2010)。
“哈工大SCIR”公众号
编辑部:郭江,李家琦,施晓明,张文博,赵得志
本期编辑:赵得志
长按下图并点击 “识别图中二维码”,即可关注哈尔滨工业大学社会计算与信息检索研究中心微信公共号:”哈工大SCIR” 。点击左下角“阅读原文”,即可查看原文。