Python深度学习篇五《深度学习用于计算机视觉》

pyDeeplearn 虚幻 1570℃ 0评论

前言

前期回顾:Python深度学习篇四《机器学习基础》
上面这篇里面写了关于向量数据最常见的机器学习任务。

好,接下来切入正题。

本章包括以下内容:

  • 理解卷积神经网络(convnet)
  • 使用数据增强来降低过拟合
  • 使用预训练的卷积神经网络进行特征提取
  • 微调预训练的卷积神经网络
  • 将卷积神经网络学到的内容及其如何做出分类决策可视化

本章将介绍卷积神经网络,也叫 convnet,它是计算机视觉应用几乎都在使用的一种深度学习模型。你将学到将卷积神经网络应用于图像分类问题,特别是那些训练数据集较小的问题。 如果你工作的地方并非大型科技公司,这也将是你最常见的使用场景。

5.1 卷积神经网络简介

我们将深入讲解卷积神经网络的原理,以及它在计算机视觉任务上为什么如此成功。但在此之前,我们先来看一个简单的卷积神经网络示例,即使用卷积神经网络对 MNIST 数字进行分 类,这个任务我们在第 2 章用密集连接网络做过(当时的测试精度为 97.8%)。虽然本例中的卷积神经网络很简单,但其精度肯定会超过第 2 章的密集连接网络。

下列代码将会展示一个简单的卷积神经网络。它是 Conv2D 层和 MaxPooling2D 层的堆叠。 很快你就会知道这些层的作用。

代码清单 5-1 实例化一个小型的卷积神经网络

from keras import layers
from keras import models
model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))

重要的是,卷积神经网络接收形状为 (image_height, image_width, image_channels) 的输入张量(不包括批量维度)。本例中设置卷积神经网络处理大小为 (28, 28, 1) 的输入张量, 这正是 MNIST 图像的格式。我们向第一层传入参数 input_shape=(28, 28, 1) 来完成此设置。

我们来看一下目前卷积神经网络的架构。

>>> model.summary()
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d_1 (Conv2D) (None, 26, 26, 32) 320
_________________________________________________________________
max_pooling2d_1 (MaxPooling2D) (None, 13, 13, 32) 0
_________________________________________________________________
conv2d_2 (Conv2D) (None, 11, 11, 64) 18496
_________________________________________________________________
max_pooling2d_2 (MaxPooling2D) (None, 5, 5, 64) 0
_________________________________________________________________
conv2d_3 (Conv2D) (None, 3, 3, 64) 36928
=================================================================
Total params: 55,744
Trainable params: 55,744
Non-trainable params: 0

可以看到,每个 Conv2D 层和 MaxPooling2D 层的输出都是一个形状为 (height, width, channels) 的 3D 张量。宽度和高度两个维度的尺寸通常会随着网络加深而变小。通道数量由传 入 Conv2D 层的第一个参数所控制(32 或 64)。

下一步是将最后的输出张量[大小为 (3, 3, 64)]输入到一个密集连接分类器网络中, 即 Dense 层的堆叠,你已经很熟悉了。这些分类器可以处理 1D 向量,而当前的输出是 3D 张量。 首先,我们需要将 3D 输出展平为 1D,然后在上面添加几个 Dense 层。

代码清单 5-2 在卷积神经网络上添加分类器

model.add(layers.Flatten())
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(10, activation='softmax'))

我们将进行 10 类别分类,最后一层使用带 10 个输出的 softmax 激活。现在网络的架构如下。

>>> model.summary()
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d_1 (Conv2D) (None, 26, 26, 32) 320
_________________________________________________________________
max_pooling2d_1 (MaxPooling2D) (None, 13, 13, 32) 0
_________________________________________________________________
conv2d_2 (Conv2D) (None, 11, 11, 64) 18496
_________________________________________________________________
max_pooling2d_2 (MaxPooling2D) (None, 5, 5, 64) 0
_________________________________________________________________
conv2d_3 (Conv2D) (None, 3, 3, 64) 36928 
_________________________________________________________________
flatten_1 (Flatten) (None, 576) 0
_________________________________________________________________
dense_1 (Dense) (None, 64) 36928
_________________________________________________________________
dense_2 (Dense) (None, 10) 650
=================================================================
Total params: 93,322
Trainable params: 93,322
Non-trainable params: 0

如你所见,在进入两个 Dense 层之前,形状 (3, 3, 64) 的输出被展平为形状 (576,) 的 向量。

下面我们在 MNIST 数字图像上训练这个卷积神经网络。我们将复用第 2 章 MNIST 示例中 的很多代码。

代码清单 5-3 在 MNIST 图像上训练卷积神经网络

from keras.datasets import mnist
from keras.utils import to_categorical
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
train_images = train_images.reshape((60000, 28, 28, 1))
train_images = train_images.astype('float32') / 255
test_images = test_images.reshape((10000, 28, 28, 1))
test_images = test_images.astype('float32') / 255
train_labels = to_categorical(train_labels)
test_labels = to_categorical(test_labels)
model.compile(optimizer='rmsprop',
 loss='categorical_crossentropy',
 metrics=['accuracy'])
model.fit(train_images, train_labels, epochs=5, batch_size=64)

我们在测试数据上对模型进行评估。

>>> test_loss, test_acc = model.evaluate(test_images, test_labels)
>>> test_acc
0.99080000000000001

第 2 章密集连接网络的测试精度为 97.8%,但这个简单卷积神经网络的测试精度达到了 99.3%,我们将错误率降低了 68%(相对比例)。相当不错!

与密集连接模型相比,为什么这个简单卷积神经网络的效果这么好?要回答这个问题,我 们来深入了解 Conv2D 层和 MaxPooling2D 层的作用。

5.1.1 卷积运算

密集连接层和卷积层的根本区别在于,Dense 层从输入特征空间中学到的是全局模式

(比如对于 MNIST 数字,全局模式就是涉及所有像素的模式),而卷积层学到的是局部模式(见图 5-1),对于图像来说,学到的就是在输入图像的二维小窗口中发现的模式。在上面的例子中, 这些窗口的大小都是 3×3。

20201210210835557 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-1 图像可以被分解为局部模式,如边缘、纹理等

这个重要特性使卷积神经网络具有以下两个有趣的性质。

  • 卷积神经网络学到的模式具有平移不变性(translation invariant)。卷积神经网络在图像 右下角学到某个模式之后,它可以在任何地方识别这个模式,比如左上角。对于密集连 接网络来说,如果模式出现在新的位置,它只能重新学习这个模式。这使得卷积神经网 络在处理图像时可以高效利用数据(因为视觉世界从根本上具有平移不变性),它只需 要更少的训练样本就可以学到具有泛化能力的数据表示。
  • 卷积神经网络可以学到模式的空间层次结构(spatial hierarchies of patterns),见图 5-2。 第一个卷积层将学习较小的局部模式(比如边缘),第二个卷积层将学习由第一层特征 组成的更大的模式,以此类推。这使得卷积神经网络可以有效地学习越来越复杂、越来 越抽象的视觉概念(因为视觉世界从根本上具有空间层次结构)。

对于包含两个空间轴(高度和宽度)和一个深度轴(也叫通道轴)的 3D 张量,其卷积也叫特征图(feature map)。对于 RGB 图像,深度轴的维度大小等于 3,因为图像有 3 个颜色通道: 红色、绿色和蓝色。对于黑白图像(比如 MNIST 数字图像),深度等于 1(表示灰度等级)。卷积运算从输入特征图中提取图块,并对所有这些图块应用相同的变换,生成输出特征图(output feature map)。该输出特征图仍是一个 3D 张量,具有宽度和高度,其深度可以任意取值,因为 输出深度是层的参数,深度轴的不同通道不再像 RGB 输入那样代表特定颜色,而是代表过滤器 (filter)。过滤器对输入数据的某一方面进行编码,比如,单个过滤器可以从更高层次编码这样 一个概念:“输入中包含一张脸。”

20201210210852580 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-2 视觉世界形成了视觉模块的空间层次结构:超局部的边缘组合成局部的对象, 比如眼睛或耳朵,这些局部对象又组合成高级概念,比如“猫”

MNIST 示例中,第一个卷积层接收一个大小为 (28, 28, 1) 的特征图,并输出一个大 小为 (26, 26, 32) 的特征图,即它在输入上计算 32 个过滤器。对于这 32 个输出通道,每个 通道都包含一个 26×26 的数值网格,它是过滤器对输入的响应图(response map),表示这个过 滤器模式在输入中不同位置的响应(见图 5-3)。这也是特征图这一术语的含义:深度轴的每个 维度都是一个特征(或过滤器),而 2D 张量 output[:, :, n] 是这个过滤器在输入上的响应 的二维空间图(map)。

20201210210918707 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-3 响应图的概念:某个模式在输入中的不同位置是否存在的二维图

卷积由以下两个关键参数所定义。

  • 从输入中提取的图块尺寸:这些图块的大小通常是 3×3 或 5×5。本例中为 3×3,这是 很常见的选择。
  • 输出特征图的深度:卷积所计算的过滤器的数量。本例第一层的深度为 32,最后一层的 深度是 64。

对于 Keras 的 Conv2D 层,这些参数都是向层传入的前几个参数:Conv2D(output_depth, (window_height, window_width))。

卷积的工作原理:在 3D 输入特征图上滑动(slide)这些 3×3 或 5×5 的窗口,在每个可能 的位置停止并提取周围特征的 3D 图块[形状为 (window_height, windowwidth, input depth)]。然后每个 3D 图块与学到的同一个权重矩阵[叫作卷积核(convolution kernel)]做 张量积,转换成形状为 (output_depth,) 的 1D 向量。然后对所有这些向量进行空间重组, 使其转换为形状为 (height, width, output_depth) 的 3D 输出特征图。输出特征图中的 每个空间位置都对应于输入特征图中的相同位置(比如输出的右下角包含了输入右下角的信 息)。举个例子,利用 3×3 的窗口,向量 output[i, j, :] 来自 3D 图块 input[i-1:i+1, j-1:j+1, :]。整个过程详见图 5-4。

20201210211135363 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-4 卷积的工作原理

注意,输出的宽度和高度可能与输入的宽度和高度不同。不同的原因可能有两点。

  • 边界效应,可以通过对输入特征图进行填充来抵消。
  • 使用了步幅(stride),稍后会给出其定义。

我们来深入研究一下这些概念。

  1. 理解边界效应与填充

假设有一个 5×5 的特征图(共 25 个方块)。其中只有 9 个方块可以作为中心放入一个 3×3 的窗口,这 9 个方块形成一个 3×3 的网格(见图 5-5)。因此,输出特征图的尺寸是 3×3。 它比输入尺寸小了一点,在本例中沿着每个维度都正好缩小了 2 个方块。在前一个例子中你也 可以看到这种边界效应的作用:开始的输入尺寸为 28×28,经过第一个卷积层之后尺寸变为 26×26。

20201210211155818 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-5 在 5×5 的输入特征图中,可以提取 3×3 图块的有效位置

如果你希望输出特征图的空间维度与输入相同,那么可以使用填充(padding)。填充是在 输入特征图的每一边添加适当数目的行和列,使得每个输入方块都能作为卷积窗口的中心。对 于 3×3 的窗口,在左右各添加一列,在上下各添加一行。对于 5×5 的窗口,各添加两行和两 列(见图 5-6)。

20201210211213543 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-6 对 5×5 的输入进行填充,以便能够提取出 25 个 3×3 的图块

对于 Conv2D 层,可以通过 padding 参数来设置填充,这个参数有两个取值:"valid" 表 示不使用填充(只使用有效的窗口位置);"same" 表示“填充后输出的宽度和高度与输入相同”。 padding 参数的默认值为 "valid"。

  1. 理解卷积步幅

影响输出尺寸的另一个因素是步幅的概念。目前为止,对卷积的描述都假设卷积窗口的中心方块都是相邻的。但两个连续窗口的距离是卷积的一个参数,叫作步幅,默认值为 1。也可 以使用步进卷积(strided convolution),即步幅大于 1 的卷积。在图 5-7 中,你可以看到用步幅 为 2 的 3×3 卷积从 5×5 输入中提取的图块(无填充)。

20201210211238512 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-7 2×2 步幅的 3×3 卷积图块

步幅为 2 意味着特征图的宽度和高度都被做了 2 倍下采样(除了边界效应引起的变化)。虽 然步进卷积对某些类型的模型可能有用,但在实践中很少使用。熟悉这个概念是有好处的。

为了对特征图进行下采样,我们不用步幅,而是通常使用最大池化(max-pooling)运算, 你在第一个卷积神经网络示例中见过此运算。下面我们来深入研究这种运算。

5.1.2 最大池化运算

在卷积神经网络示例中,你可能注意到,在每个 MaxPooling2D 层之后,特征图的尺寸都 会减半。例如,在第一个 MaxPooling2D 层之前,特征图的尺寸是 26×26,但最大池化运算将 其减半为 13×13。这就是最大池化的作用:对特征图进行下采样,与步进卷积类似。

最大池化是从输入特征图中提取窗口,并输出每个通道的最大值。它的概念与卷积类似, 但是最大池化使用硬编码的 max 张量运算对局部图块进行变换,而不是使用学到的线性变换(卷 积核)。最大池化与卷积的最大不同之处在于,最大池化通常使用 2×2 的窗口和步幅 2,其目 的是将特征图下采样 2 倍。与此相对的是,卷积通常使用 3×3 窗口和步幅 1。

为什么要用这种方式对特征图下采样?为什么不删除最大池化层,一直保留较大的特征图? 我们来这么做试一下。这时模型的卷积基(convolutional base)如下所示。

model_no_max_pool = models.Sequential()
model_no_max_pool.add(layers.Conv2D(32, (3, 3), activation='relu',
 input_shape=(28, 28, 1)))
model_no_max_pool.add(layers.Conv2D(64, (3, 3), activation='relu'))
model_no_max_pool.add(layers.Conv2D(64, (3, 3), activation='relu'))

该模型的架构如下。

>>> model_no_max_pool.summary()
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d_4 (Conv2D) (None, 26, 26, 32) 320
_________________________________________________________________
conv2d_5 (Conv2D) (None, 24, 24, 64) 18496
_________________________________________________________________
conv2d_6 (Conv2D) (None, 22, 22, 64) 36928
=================================================================
Total params: 55,744
Trainable params: 55,744
Non-trainable params: 0

这种架构有什么问题?有如下两点问题。

  • 这种架构不利于学习特征的空间层级结构。第三层的 3×3 窗口中只包含初始输入的 7×7 窗口中所包含的信息。卷积神经网络学到的高级模式相对于初始输入来说仍然很小, 这可能不足以学会对数字进行分类(你可以试试仅通过 7 像素×7 像素的窗口观察图像 来识别其中的数字)。我们需要让最后一个卷积层的特征包含输入的整体信息。
  • 最后一层的特征图对每个样本共有 22×22×64=30 976 个元素。这太多了。如果你将其展平并在上面添加一个大小为 512 的 Dense 层,那一层将会有 1580 万个参数。这对于 这样一个小模型来说太多了,会导致严重的过拟合。

简而言之,使用下采样的原因,一是减少需要处理的特征图的元素个数,二是通过让连续 卷积层的观察窗口越来越大(即窗口覆盖原始输入的比例越来越大),从而引入空间过滤器的层 级结构。

注意,最大池化不是实现这种下采样的唯一方法。你已经知道,还可以在前一个卷积层中 使用步幅来实现。此外,你还可以使用平均池化来代替最大池化,其方法是将每个局部输入图 块变换为取该图块各通道的平均值,而不是最大值。但最大池化的效果往往比这些替代方法更好。 简而言之,原因在于特征中往往编码了某种模式或概念在特征图的不同位置是否存在(因此得 名特征图),而观察不同特征的最大值而不是平均值能够给出更多的信息。因此,最合理的子采 样策略是首先生成密集的特征图(通过无步进的卷积),然后观察特征每个小图块上的最大激活, 而不是查看输入的稀疏窗口(通过步进卷积)或对输入图块取平均,因为后两种方法可能导致 错过或淡化特征是否存在的信息。

现在你应该已经理解了卷积神经网络的基本概念,即特征图、卷积和最大池化,并且也知 道如何构建一个小型卷积神经网络来解决简单问题,比如 MNIST 数字分类。下面我们将介绍更 加实用的应用。

5.2 在小型数据集上从头开始训练一个卷积神经网络

使用很少的数据来训练一个图像分类模型,这是很常见的情况,如果你要从事计算机视觉方面的职业,很可能会在实践中遇到这种情况。“很少的”样本可能是几百张图像,也可能是几 万张图像。来看一个实例,我们将重点讨论猫狗图像分类,数据集中包含 4000 张猫和狗的图像 (2000 张猫的图像,2000 张狗的图像)。我们将 2000 张图像用于训练,1000 张用于验证,1000 张用于测试。

本节将介绍解决这一问题的基本策略,即使用已有的少量数据从头开始训练一个新模型。 首先,在 2000 个训练样本上训练一个简单的小型卷积神经网络,不做任何正则化,为模型目标设定一个基准。这会得到 71% 的分类精度。此时主要的问题在于过拟合。然后,我们会介绍数据增强(data augmentation),它在计算机视觉领域是一种非常强大的降低过拟合的技术。使用数据增强之后,网络精度将提高到 82%。

5.3 节会介绍将深度学习应用于小型数据集的另外两个重要技巧:用预训练的网络做特征提 取(得到的精度范围在 90%~96%),对预训练的网络进行微调(最终精度为 97%)。总而言之, 这三种策略——从头开始训练一个小型模型、使用预训练的网络做特征提取、对预训练的网络 进行微调——构成了你的工具箱,未来可用于解决小型数据集的图像分类问题。

5.2.1 深度学习与小数据问题的相关性

有时你会听人说,仅在有大量数据可用时,深度学习才有效。这种说法部分正确:深度学 习的一个基本特性就是能够独立地在训练数据中找到有趣的特征,无须人为的特征工程,而这只在拥有大量训练样本时才能实现。对于输入样本的维度非常高(比如图像)的问题尤其如此。

但对于初学者来说,所谓“大量”样本是相对的,即相对于你所要训练网络的大小和深度 而言。只用几十个样本训练卷积神经网络就解决一个复杂问题是不可能的,但如果模型很小, 并做了很好的正则化,同时任务非常简单,那么几百个样本可能就足够了。由于卷积神经网络 学到的是局部的、平移不变的特征,它对于感知问题可以高效地利用数据。虽然数据相对较少, 但在非常小的图像数据集上从头开始训练一个卷积神经网络,仍然可以得到不错的结果,而且 无须任何自定义的特征工程。本节你将看到其效果。

此外,深度学习模型本质上具有高度的可复用性,比如,已有一个在大规模数据集上训练 的图像分类模型或语音转文本模型,你只需做很小的修改就能将其复用于完全不同的问题。特别是在计算机视觉领域,许多预训练的模型(通常都是在 ImageNet 数据集上训练得到的)现在都可以公开下载,并可以用于在数据很少的情况下构建强大的视觉模型。这是 5.3 节的内容。 我们先来看一下数据。

5.2.2 下载数据

本节用到的猫狗分类数据集不包含在 Keras 中。它由 Kaggle 在 2013 年末公开并作为一项 计算视觉竞赛的一部分,当时卷积神经网络还不是主流算法。你可以从 https://www.kaggle.com/ c/dogs-vs-cats/data 下载原始数据集(如果没有 Kaggle 账号的话,你需要注册一个,别担心,很简单)。

这些图像都是中等分辨率的彩色 JPEG 图像。图 5-8 给出了一些样本示例。

20201210211308873 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-8 猫狗分类数据集的一些样本。没有修改尺寸:样本在尺寸、外观等方面是不一样的

不出所料,2013 年的猫狗分类 Kaggle 竞赛的优胜者使用的是卷积神经网络。最佳结果达到 了 95% 的精度。本例中,虽然你只在不到参赛选手所用的 10% 的数据上训练模型,但结果也和 这个精度相当接近(见下一节)。

这个数据集包含 25 000 张猫狗图像(每个类别都有 12 500 张),大小为 543MB(压缩后)。 下载数据并解压之后,你需要创建一个新数据集,其中包含三个子集:每个类别各 1000 个样本 的训练集、每个类别各 500 个样本的验证集和每个类别各 500 个样本的测试集。

创建新数据集的代码如下所示。

代码清单 5-4 将图像复制到训练、验证和测试的目录

import os, shutil
original_dataset_dir = '/Users/fchollet/Downloads/kaggle_original_data'
base_dir = '/Users/fchollet/Downloads/cats_and_dogs_small'
os.mkdir(base_dir)
train_dir = os.path.join(base_dir, 'train')
os.mkdir(train_dir)
validation_dir = os.path.join(base_dir, 'validation')
os.mkdir(validation_dir)
test_dir = os.path.join(base_dir, 'test')
os.mkdir(test_dir)
train_cats_dir = os.path.join(train_dir, 'cats')
os.mkdir(train_cats_dir)
train_dogs_dir = os.path.join(train_dir, 'dogs')
os.mkdir(train_dogs_dir)
validation_cats_dir = os.path.join(validation_dir, 'cats')
os.mkdir(validation_cats_dir)
validation_dogs_dir = os.path.join(validation_dir, 'dogs')
os.mkdir(validation_dogs_dir)
test_cats_dir = os.path.join(test_dir, 'cats')
os.mkdir(test_cats_dir)
test_dogs_dir = os.path.join(test_dir, 'dogs')
os.mkdir(test_dogs_dir)
fnames = ['cat.{}.jpg'.format(i) for i in range(1000)]
for fname in fnames:
 src = os.path.join(original_dataset_dir, fname)
 dst = os.path.join(train_cats_dir, fname)
 shutil.copyfile(src, dst)
fnames = ['cat.{}.jpg'.format(i) for i in range(1000, 1500)]
for fname in fnames:
 src = os.path.join(original_dataset_dir, fname)
 dst = os.path.join(validation_cats_dir, fname)
 shutil.copyfile(src, dst)
fnames = ['cat.{}.jpg'.format(i) for i in range(1500, 2000)]
for fname in fnames:
 src = os.path.join(original_dataset_dir, fname)
 dst = os.path.join(test_cats_dir, fname)
 shutil.copyfile(src, dst)
fnames = ['dog.{}.jpg'.format(i) for i in range(1000)]
for fname in fnames:
 src = os.path.join(original_dataset_dir, fname)
 dst = os.path.join(train_dogs_dir, fname)
 shutil.copyfile(src, dst)
fnames = ['dog.{}.jpg'.format(i) for i in range(1000, 1500)]
for fname in fnames:
 src = os.path.join(original_dataset_dir, fname)
 dst = os.path.join(validation_dogs_dir, fname)
 shutil.copyfile(src, dst)
fnames = ['dog.{}.jpg'.format(i) for i in range(1500, 2000)]
for fname in fnames:
 src = os.path.join(original_dataset_dir, fname)
 dst = os.path.join(test_dogs_dir, fname)
 shutil.copyfile(src, dst)

我们来检查一下,看看每个分组(训练 / 验证 / 测试)中分别包含多少张图像。

>>> print('total training cat images:', len(os.listdir(train_cats_dir)))
total training cat images: 1000
>>> print('total training dog images:', len(os.listdir(train_dogs_dir)))
total training dog images: 1000
>>> print('total validation cat images:', len(os.listdir(validation_cats_dir)))
total validation cat images: 500
>>> print('total validation dog images:', len(os.listdir(validation_dogs_dir)))
total validation dog images: 500
>>> print('total test cat images:', len(os.listdir(test_cats_dir)))
total test cat images: 500
>>> print('total test dog images:', len(os.listdir(test_dogs_dir)))
total test dog images: 500

所以我们的确有 2000 张训练图像、1000 张验证图像和 1000 张测试图像。每个分组中两个 类别的样本数相同,这是一个平衡的二分类问题,分类精度可作为衡量成功的指标。

5.2.3 构建网络

在前一个 MNIST 示例中,我们构建了一个小型卷积神经网络,所以你应该已经熟悉这 种网络。我们将复用相同的总体结构,即卷积神经网络由 Conv2D 层(使用 relu 激活)和 MaxPooling2D 层交替堆叠构成。

但由于这里要处理的是更大的图像和更复杂的问题,你需要相应地增大网络,即再增加一 个 Conv2D+MaxPooling2D 的组合。这既可以增大网络容量,也可以进一步减小特征图的尺寸, 使其在连接 Flatten 层时尺寸不会太大。本例中初始输入的尺寸为 150×150(有些随意的选 择),所以最后在 Flatten 层之前的特征图大小为 7×7。

注意 网络中特征图的深度在逐渐增大(从 32 增大到 128),而特征图的尺寸在逐渐减小(从 150×150 减小到 7×7)。这几乎是所有卷积神经网络的模式。

你面对的是一个二分类问题,所以网络最后一层是使用 sigmoid 激活的单一单元(大小为 1 的 Dense 层)。这个单元将对某个类别的概率进行编码。

代码清单 5-5 将猫狗分类的小型卷积神经网络实例化

from keras import layers
from keras import models
model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu',
 input_shape=(150, 150, 3)))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Flatten())
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

我们来看一下特征图的维度如何随着每层变化。

>>> model.summary()
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d_1 (Conv2D) (None, 148, 148, 32) 896
_________________________________________________________________
max_pooling2d_1 (MaxPooling2D) (None, 74, 74, 32) 0
_________________________________________________________________
conv2d_2 (Conv2D) (None, 72, 72, 64) 18496
_________________________________________________________________
max_pooling2d_2 (MaxPooling2D) (None, 36, 36, 64) 0
_________________________________________________________________
conv2d_3 (Conv2D) (None, 34, 34, 128) 73856
_________________________________________________________________
max_pooling2d_3 (MaxPooling2D) (None, 17, 17, 128) 0
_________________________________________________________________
conv2d_4 (Conv2D) (None, 15, 15, 128) 147584
_________________________________________________________________
max_pooling2d_4 (MaxPooling2D) (None, 7, 7, 128) 0
_________________________________________________________________
flatten_1 (Flatten) (None, 6272) 0
_________________________________________________________________
dense_1 (Dense) (None, 512) 3211776
_________________________________________________________________
dense_2 (Dense) (None, 1) 513
=================================================================
Total params: 3,453,121
Trainable params: 3,453,121
Non-trainable params: 0

在编译这一步,和前面一样,我们将使用 RMSprop 优化器。因为网络最后一层是单一 sigmoid 单元,所以我们将使用二元交叉熵作为损失函数(提醒一下,表 4-1 列出了各种情况下应该使 用的损失函数)。

代码清单 5-6 配置模型用于训练

from keras import optimizers
model.compile(loss='binary_crossentropy',
 optimizer=optimizers.RMSprop(lr=1e-4),
 metrics=['acc'])

5.2.4 数据预处理

你现在已经知道,将数据输入神经网络之前,应该将数据格式化为经过预处理的浮点数张量。 现在,数据以 JPEG 文件的形式保存在硬盘中,所以数据预处理步骤大致如下。

  1. 读取图像文件。
  2. 将 JPEG 文件解码为 RGB 像素网格。
  3. 将这些像素网格转换为浮点数张量。
  4. 将像素值(0~255 范围内)缩放到 [0, 1] 区间(正如你所知,神经网络喜欢处理较小的输 入值)。

这些步骤可能看起来有点吓人,但幸运的是,Keras 拥有自动完成这些步骤的工具。Keras 有一个图像处理辅助工具的模块,位于 keras.preprocessing.image。特别地,它包含 ImageDataGenerator 类,可以快速创建 Python 生成器,能够将硬盘上的图像文件自动转换 为预处理好的张量批量。下面我们将用到这个类。

代码清单 5-7 使用 ImageDataGenerator 从目录中读取图像

from keras.preprocessing.image import ImageDataGenerator
train_datagen = ImageDataGenerator(rescale=1./255)
test_datagen = ImageDataGenerator(rescale=1./255)
train_generator = train_datagen.flow_from_directory(
 train_dir,
 target_size=(150, 150),
 batch_size=20,
 class_mode='binary')
validation_generator = test_datagen.flow_from_directory(
 validation_dir,
 target_size=(150, 150),
 batch_size=20,
 class_mode='binary')

理解 Python 生成器

Python 生成器(Python generator)是一个类似于迭代器的对象,一个可以和 for ... in 运算符一起使用的对象。生成器是用 yield 运算符来构造的。 下面一个生成器的例子,可以生成整数。

def generator():
 i=0
 while True:
 i += 1
 yield i
for item in generator():
 print(item)
 if item > 4:
 break
输出结果如下。
1
2
3
4
5

我们来看一下其中一个生成器的输出:它生成了 150×150 的 RGB 图像[形状为 (20, 150, 150, 3)]与二进制标签[形状为 (20,)]组成的批量。每个批量中包含 20 个样本(批 量大小)。注意,生成器会不停地生成这些批量,它会不断循环目标文件夹中的图像。因此,你 需要在某个时刻终止(break)迭代循环。

>>> for data_batch, labels_batch in train_generator:
>>> print('data batch shape:', data_batch.shape)
>>> print('labels batch shape:', labels_batch.shape)
>>> break
data batch shape: (20, 150, 150, 3)
labels batch shape: (20,)

利用生成器,我们让模型对数据进行拟合。我们将使用 fit_generator 方法来拟合,它 在数据生成器上的效果和 fit 相同。它的第一个参数应该是一个 Python 生成器,可以不停地生 成输入和目标组成的批量,比如 train_generator。因为数据是不断生成的,所以 Keras 模型 要知道每一轮需要从生成器中抽取多少个样本。这是 steps_per_epoch 参数的作用:从生成 器中抽取 steps_per_epoch 个批量后(即运行了 steps_per_epoch 次梯度下降),拟合过程 将进入下一个轮次。本例中,每个批量包含 20 个样本,所以读取完所有 2000 个样本需要 100 个批量。

使用 fit_generator 时,你可以传入一个 validation_data 参数,其作用和在 fit 方法中类似。值得注意的是,这个参数可以是一个数据生成器,但也可以是 Numpy 数组组成的元 组。如果向 validation_data 传入一个生成器,那么这个生成器应该能够不停地生成验证数据批量,因此你还需要指定 validation_steps 参数,说明需要从验证生成器中抽取多少个批次用于评估。

代码清单 5-8 利用批量生成器拟合模型

history = model.fit_generator(
 train_generator,
 steps_per_epoch=100,
 epochs=30,
 validation_data=validation_generator,
 validation_steps=50)

始终在训练完成后保存模型,这是一种良好实践。

代码清单 5-9 保存模型

model.save('cats_and_dogs_small_1.h5')

我们来分别绘制训练过程中模型在训练数据和验证数据上的损失和精度(见图 5-9 和图 5-10)。

代码清单 5-10 绘制训练过程中的损失曲线和精度曲线

import matplotlib.pyplot as plt
acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']
epochs = range(1, len(acc) + 1)
plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()
plt.figure()
plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()
plt.show()

20201210211343646 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-9 训练精度和验证精度

20201210211359389 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-10 训练损失和验证损失

从这些图像中都能看出过拟合的特征。训练精度随着时间线性增加,直到接近 100%,而验 证精度则停留在 70%~72%。验证损失仅在 5 轮后就达到最小值,然后保持不变,而训练损失则 一直线性下降,直到接近于 0。

因为训练样本相对较少(2000 个),所以过拟合是你最关心的问题。前面已经介绍过几种 降低过拟合的技巧,比如 dropout 和权重衰减(L2 正则化)。现在我们将使用一种针对于计算 机视觉领域的新方法,在用深度学习模型处理图像时几乎都会用到这种方法,它就是数据增强 (data augmentation)。

5.2.5 使用数据增强

过拟合的原因是学习样本太少,导致无法训练出能够泛化到新数据的模型。如果拥有无限的数据,那么模型能够观察到数据分布的所有内容,这样就永远不会过拟合。数据增强是从现 有的训练样本中生成更多的训练数据,其方法是利用多种能够生成可信图像的随机变换来增加 (augment)样本。其目标是,模型在训练时不会两次查看完全相同的图像。这让模型能够观察到数据的更多内容,从而具有更好的泛化能力。

Keras 中,这可以通过对 ImageDataGenerator 实例读取的图像执行多次随机变换来实 现。我们先来看一个例子。

代码清单 5-11 利用 ImageDataGenerator 来设置数据增强

datagen = ImageDataGenerator(
 rotation_range=40,
 width_shift_range=0.2,
 height_shift_range=0.2,
 shear_range=0.2,
 zoom_range=0.2,
 horizontal_flip=True,
 fill_mode='nearest')

这里只选择了几个参数(想了解更多参数,请查阅 Keras 文档)。我们来快速介绍一下这些 参数的含义。

  • rotation_range 是角度值(在 0~180 范围内),表示图像随机旋转的角度范围。
  • width_shift 和 height_shift 是图像在水平或垂直方向上平移的范围(相对于总宽 度或总高度的比例)。
  • shear_range 是随机错切变换的角度。
  • zoom_range 是图像随机缩放的范围。
  • horizontal_flip 是随机将一半图像水平翻转。如果没有水平不对称的假设(比如真 实世界的图像),这种做法是有意义的。
  • fill_mode是用于填充新创建像素的方法,这些新像素可能来自于旋转或宽度/高度平移。 我们来看一下增强后的图像(见图 5-11)。

20201210211426218 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-11 通过随机数据增强生成的猫图像

代码清单 5-12 显示几个随机增强后的训练图像

from keras.preprocessing import image
fnames = [os.path.join(train_cats_dir, fname) for
 fname in os.listdir(train_cats_dir)]
 img_path = fnames[3]
img = image.load_img(img_path, target_size=(150, 150))
x = image.img_to_array(img)
x = x.reshape((1,) + x.shape)
i = 0
for batch in datagen.flow(x, batch_size=1):
 plt.figure(i)
 imgplot = plt.imshow(image.array_to_img(batch[0]))
 i += 1
 if i % 4 == 0:
 break
plt.show()

如果你使用这种数据增强来训练一个新网络,那么网络将不会两次看到同样的输入。但网 络看到的输入仍然是高度相关的,因为这些输入都来自于少量的原始图像。你无法生成新信息, 而只能混合现有信息。因此,这种方法可能不足以完全消除过拟合。为了进一步降低过拟合, 你还需要向模型中添加一个 Dropout 层,添加到密集连接分类器之前。

代码清单 5-13 定义一个包含 dropout 的新卷积神经网络

model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu',
 input_shape=(150, 150, 3)))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Flatten())
model.add(layers.Dropout(0.5))
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
model.compile(loss='binary_crossentropy',
 optimizer=optimizers.RMSprop(lr=1e-4),
 metrics=['acc'])

我们来训练这个使用了数据增强和 dropout 的网络。

代码清单 5-14 利用数据增强生成器训练卷积神经网络

train_datagen = ImageDataGenerator(
 rescale=1./255,
 rotation_range=40,
 width_shift_range=0.2,
 height_shift_range=0.2,
 shear_range=0.2,
 zoom_range=0.2,
 horizontal_flip=True,)
test_datagen = ImageDataGenerator(rescale=1./255)
train_generator = train_datagen.flow_from_directory(
 train_dir,
 target_size=(150, 150),
 batch_size=32,
 class_mode='binary')
validation_generator = test_datagen.flow_from_directory(
 validation_dir,
 target_size=(150, 150),
 batch_size=32,
 class_mode='binary')
history = model.fit_generator(
 train_generator,
 steps_per_epoch=100,
 epochs=100,
 validation_data=validation_generator,
 validation_steps=50)

我们把模型保存下来,你会在 5.4 节用到它。

代码清单 5-15 保存模型

model.save('cats_and_dogs_small_2.h5')

我们再次绘制结果(见图 5-12 和图 5-13)。使用了数据增强和 dropout 之后,模型不再过拟合: 训练曲线紧紧跟随着验证曲线。现在的精度为 82%,比未正则化的模型提高了 15%(相对比例)。

20201210211458479 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-12 采用数据增强后的训练精度和验证精度

2020121021151258 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-13 采用数据增强后的训练损失和验证损失

通过进一步使用正则化方法以及调节网络参数(比如每个卷积层的过滤器个数或网络中的层数),你可以得到更高的精度,可以达到86%或87%。但只靠从头开始训练自己的卷积神经网络, 再想提高精度就十分困难,因为可用的数据太少。想要在这个问题上进一步提高精度,下一步 需要使用预训练的模型,这是接下来两节的重点。

5.3 使用预训练的卷积神经网络

想要将深度学习应用于小型图像数据集,一种常用且非常高效的方法是使用预训练网络。 预训练网络(pretrained network)是一个保存好的网络,之前已在大型数据集(通常是大规模图像分类任务)上训练好。如果这个原始数据集足够大且足够通用,那么预训练网络学到的特征 的空间层次结构可以有效地作为视觉世界的通用模型,因此这些特征可用于各种不同的计算机视觉问题,即使这些新问题涉及的类别和原始任务完全不同。举个例子,你在 ImageNet 上训练了一个网络(其类别主要是动物和日常用品),然后将这个训练好的网络应用于某个不相干的任 务,比如在图像中识别家具。这种学到的特征在不同问题之间的可移植性,是深度学习与许多 早期浅层学习方法相比的重要优势,它使得深度学习对小数据问题非常有效。

本例中,假设有一个在 ImageNet 数据集(140 万张标记图像,1000 个不同的类别)上训练好的大型卷积神经网络。ImageNet 中包含许多动物类别,其中包括不同种类的猫和狗,因此可以认为它在猫狗分类问题上也能有良好的表现。

我们将使用 VGG16 架构,它由 Karen SimonyanAndrew Zisserman 在 2014 年开发 a。对于 ImageNet,它是一种简单而又广泛使用的卷积神经网络架构。虽然 VGG16 是一个比较旧的模 型,性能远比不了当前最先进的模型,而且还比许多新模型更为复杂,但我之所以选择它,是因为它的架构与你已经熟悉的架构很相似,因此无须引入新概念就可以很好地理解。这可能是你第一次遇到这种奇怪的模型名称——VGG、ResNet、Inception、Inception-ResNet、Xception 等。 你会习惯这些名称的,因为如果你一直用深度学习做计算机视觉的话,它们会频繁出现。

使用预训练网络有两种方法:特征提取(feature extraction)和微调模型(fine-tuning)。两种方法我们都会介绍。首先来看特征提取。

5.3.1 特征提取

特征提取是使用之前网络学到的表示来从新样本中提取出有趣的特征。然后将这些特征输入一个新的分类器,从头开始训练。

如前所述,用于图像分类的卷积神经网络包含两部分:首先是一系列池化层和卷积层,最后是一个密集连接分类器。第一部分叫作模型的卷积基(convolutional base)。对于卷积神经网络而言,特征提取就是取出之前训练好的网络的卷积基,在上面运行新数据,然后在输出上面训练一个新的分类器(见图 5-14)。

20201210211523367 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-14 保持卷积基不变,改变分类器

为什么仅重复使用卷积基?我们能否也重复使用密集连接分类器?一般来说,应该避免这么做。原因在于卷积基学到的表示可能更加通用,因此更适合重复使用。卷积神经网络的特征图表示通用概念在图像中是否存在,无论面对什么样的计算机视觉问题,这种特征图都可能很有用。但是,分类器学到的表示必然是针对于模型训练的类别,其中仅包含某个类别出现在整 张图像中的概率信息。此外,密集连接层的表示不再包含物体在输入图像中的位置信息。密集 连接层舍弃了空间的概念,而物体位置信息仍然由卷积特征图所描述。如果物体位置对于问题 很重要,那么密集连接层的特征在很大程度上是无用的。

注意,某个卷积层提取的表示的通用性(以及可复用性)取决于该层在模型中的深度。模型中更靠近底部的层提取的是局部的、高度通用的特征图(比如视觉边缘、颜色和纹理),而更靠近顶部的层提取的是更加抽象的概念(比如“猫耳朵”或“狗眼睛”)。a 因此,如果你的新数据集与原始模型训练的数据集有很大差异,那么最好只使用模型的前几层来做特征提取,而不是使用整个卷积基。

本例中,由于 ImageNet 的类别中包含多种狗和猫的类别,所以重复使用原始模型密集连接层中所包含的信息可能很有用。但我们选择不这么做,以便涵盖新问题的类别与原始模型的类 别不一致的更一般情况。我们来实践一下,使用在 ImageNet 上训练的 VGG16 网络的卷积基从 猫狗图像中提取有趣的特征,然后在这些特征上训练一个猫狗分类器。

VGG16 等模型内置于 Keras 中。你可以从 keras.applications 模块中导入。下面是 keras.applications 中的一部分图像分类模型(都是在 ImageNet 数据集上预训练得到的):

‰ Xception
‰ Inception V3
‰ ResNet50
‰ VGG16
‰ VGG19
‰ MobileNet

我们将 VGG16 模型实例化。

代码清单 5-16 将 VGG16 卷积基实例化

from keras.applications import VGG16
conv_base = VGG16(weights='imagenet',
 include_top=False,
 input_shape=(150, 150, 3))

这里向构造函数中传入了三个参数。

  • weights 指定模型初始化的权重检查点。
  • include_top 指定模型最后是否包含密集连接分类器。默认情况下,这个密集连接分类器对应于 ImageNet 的 1000 个类别。因为我们打算使用自己的密集连接分类器(只有两个类别:cat 和 dog),所以不需要包含它。
  • input_shape 是输入到网络中的图像张量的形状。这个参数完全是可选的,如果不传入这个参数,那么网络能够处理任意形状的输入。

VGG16 卷积基的详细架构如下所示。它和你已经熟悉的简单卷积神经网络很相似。

>>> conv_base.summary()
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) (None, 150, 150, 3) 0 
_________________________________________________________________
block1_conv1 (Conv2D) (None, 150, 150, 64) 1792
_________________________________________________________________
block1_conv2 (Conv2D) (None, 150, 150, 64) 36928
_________________________________________________________________
block1_pool (MaxPooling2D) (None, 75, 75, 64) 0
_________________________________________________________________
block2_conv1 (Conv2D) (None, 75, 75, 128) 73856
_________________________________________________________________
block2_conv2 (Conv2D) (None, 75, 75, 128) 147584
_________________________________________________________________
block2_pool (MaxPooling2D) (None, 37, 37, 128) 0
_________________________________________________________________
block3_conv1 (Conv2D) (None, 37, 37, 256) 295168
_________________________________________________________________
block3_conv2 (Conv2D) (None, 37, 37, 256) 590080
_________________________________________________________________
block3_conv3 (Conv2D) (None, 37, 37, 256) 590080
_________________________________________________________________
block3_pool (MaxPooling2D) (None, 18, 18, 256) 0
_________________________________________________________________
block4_conv1 (Conv2D) (None, 18, 18, 512) 1180160
_________________________________________________________________
block4_conv2 (Conv2D) (None, 18, 18, 512) 2359808
_________________________________________________________________
block4_conv3 (Conv2D) (None, 18, 18, 512) 2359808
_________________________________________________________________
block4_pool (MaxPooling2D) (None, 9, 9, 512) 0
_________________________________________________________________
block5_conv1 (Conv2D) (None, 9, 9, 512) 2359808
_________________________________________________________________
block5_conv2 (Conv2D) (None, 9, 9, 512) 2359808
_________________________________________________________________
block5_conv3 (Conv2D) (None, 9, 9, 512) 2359808
_________________________________________________________________
block5_pool (MaxPooling2D) (None, 4, 4, 512) 0
=================================================================
Total params: 14,714,688
Trainable params: 14,714,688
Non-trainable params: 0

最后的特征图形状为 (4, 4, 512)。我们将在这个特征上添加一个密集连接分类器。 接下来,下一步有两种方法可供选择。

  • 在你的数据集上运行卷积基,将输出保存成硬盘中的 Numpy 数组,然后用这个数据作 为输入,输入到独立的密集连接分类器中(与本书第一部分介绍的分类器类似)。这种 方法速度快,计算代价低,因为对于每个输入图像只需运行一次卷积基,而卷积基是目 前流程中计算代价最高的。但出于同样的原因,这种方法不允许你使用数据增强。
  • 在顶部添加 Dense 层来扩展已有模型(即 conv_base),并在输入数据上端到端地运行 整个模型。这样你可以使用数据增强,因为每个输入图像进入模型时都会经过卷积基。 但出于同样的原因,这种方法的计算代价比第一种要高很多。

这两种方法我们都会介绍。首先来看第一种方法的代码:保存你的数据在 conv_base 中的 输出,然后将这些输出作为输入用于新模型。

  1. 不使用数据增强的快速特征提取

首先,运行 ImageDataGenerator 实例,将图像及其标签提取为 Numpy 数组。我们需要 调用 conv_base 模型的 predict 方法来从这些图像中提取特征。

代码清单 5-17 使用预训练的卷积基提取特征

import os
import numpy as np
from keras.preprocessing.image import ImageDataGenerator
base_dir = '/Users/fchollet/Downloads/cats_and_dogs_small'
train_dir = os.path.join(base_dir, 'train')
validation_dir = os.path.join(base_dir, 'validation')
test_dir = os.path.join(base_dir, 'test')
datagen = ImageDataGenerator(rescale=1./255)
batch_size = 20
def extract_features(directory, sample_count):
 features = np.zeros(shape=(sample_count, 4, 4, 512))
 labels = np.zeros(shape=(sample_count))
 generator = datagen.flow_from_directory(
 directory,
 target_size=(150, 150),
 batch_size=batch_size,
 class_mode='binary')
 i = 0
 for inputs_batch, labels_batch in generator:
 features_batch = conv_base.predict(inputs_batch)
 features[i * batch_size : (i + 1) * batch_size] = features_batch
 labels[i * batch_size : (i + 1) * batch_size] = labels_batch
 i += 1
 if i * batch_size >= sample_count:
 break
 return features, labels
train_features, train_labels = extract_features(train_dir, 2000)
validation_features, validation_labels = extract_features(validation_dir, 1000)
test_features, test_labels = extract_features(test_dir, 1000)

目前,提取的特征形状为 (samples, 4, 4, 512)。我们要将其输入到密集连接分类器中, 所以首先必须将其形状展平为 (samples, 8192)。

train_features = np.reshape(train_features, (2000, 4 * 4 * 512))
validation_features = np.reshape(validation_features, (1000, 4 * 4 * 512))
test_features = np.reshape(test_features, (1000, 4 * 4 * 512))

现在你可以定义你的密集连接分类器(注意要使用 dropout 正则化),并在刚刚保存的数据 和标签上训练这个分类器。

代码清单 5-18 定义并训练密集连接分类器

from keras import models
from keras import layers
from keras import optimizers
model = models.Sequential()
model.add(layers.Dense(256, activation='relu', input_dim=4 * 4 * 512))
model.add(layers.Dropout(0.5))
model.add(layers.Dense(1, activation='sigmoid'))
model.compile(optimizer=optimizers.RMSprop(lr=2e-5),
 loss='binary_crossentropy',
 metrics=['acc'])
history = model.fit(train_features, train_labels,
 epochs=30,
 batch_size=20,
 validation_data=(validation_features, validation_labels))

训练速度非常快,因为你只需处理两个 Dense 层。即使在 CPU 上运行,每轮的时间也不 到一秒钟。

我们来看一下训练期间的损失曲线和精度曲线(见图 5-15 和图 5-16)。

代码清单 5-19 绘制结果

import matplotlib.pyplot as plt
acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']
epochs = range(1, len(acc) + 1)
plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()
plt.figure()
plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()
plt.show()

20201210211604508 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-15 简单特征提取的训练精度和验证精度

20201210211617519 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-16 简单特征提取的训练损失和验证损失

我们的验证精度达到了约 90%,比上一节从头开始训练的小型模型效果要好得多。但从图中也可以看出,虽然 dropout 比率相当大,但模型几乎从一开始就过拟合。这是因为本方法没有使用数据增强,而数据增强对防止小型图像数据集的过拟合非常重要。

  1. 使用数据增强的特征提取

下面我们来看一下特征提取的第二种方法,它的速度更慢,计算代价更高,但在训练期间可以使用数据增强。这种方法就是:扩展 conv_base 模型,然后在输入数据上端到端地运行模型。

注意 本方法计算代价很高,只在有 GPU 的情况下才能尝试运行。它在 CPU 上是绝对难以运行的。如果你无法在 GPU 上运行代码,那么就采用第一种方法。

模型的行为和层类似,所以你可以向 Sequential 模型中添加一个模型(比如 conv_base), 就像添加一个层一样。

代码清单 5-20 在卷积基上添加一个密集连接分类器

from keras import models
from keras import layers
model = models.Sequential()
model.add(conv_base)
model.add(layers.Flatten())
model.add(layers.Dense(256, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

现在模型的架构如下所示。

>>> model.summary()
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
vgg16 (Model) (None, 4, 4, 512) 14714688
_________________________________________________________________
flatten_1 (Flatten) (None, 8192) 0
_________________________________________________________________
dense_1 (Dense) (None, 256) 2097408
_________________________________________________________________
dense_2 (Dense) (None, 1) 257
=================================================================
Total params: 16,812,353
Trainable params: 16,812,353
Non-trainable params: 0

如你所见,VGG16 的卷积基有 14 714 688 个参数,非常多。在其上添加的分类器有 200 万 个参数。

在编译和训练模型之前,一定要“冻结”卷积基。冻结(freeze)一个或多个层是指在训练 过程中保持其权重不变。如果不这么做,那么卷积基之前学到的表示将会在训练过程中被修改。 因为其上添加的 Dense 层是随机初始化的,所以非常大的权重更新将会在网络中传播,对之前学到的表示造成很大破坏。

在 Keras 中,冻结网络的方法是将其 trainable 属性设为 False。

>>> print('This is the number of trainable weights '
 'before freezing the conv base:', len(model.trainable_weights))
This is the number of trainable weights before freezing the conv base: 30
>>> conv_base.trainable = False
>>> print('This is the number of trainable weights '
 'after freezing the conv base:', len(model.trainable_weights))
This is the number of trainable weights after freezing the conv base: 4

如此设置之后,只有添加的两个 Dense 层的权重才会被训练。总共有 4 个权重张量,每层 2 个(主权重矩阵和偏置向量)。注意,为了让这些修改生效,你必须先编译模型。如果在编译 之后修改了权重的 trainable 属性,那么应该重新编译模型,否则这些修改将被忽略。

现在你可以开始训练模型了,使用和前一个例子相同的数据增强设置。

代码清单 5-21 利用冻结的卷积基端到端地训练模型

from keras.preprocessing.image import ImageDataGenerator
from keras import optimizers
train_datagen = ImageDataGenerator(
 rescale=1./255,
 rotation_range=40,
 width_shift_range=0.2,
 height_shift_range=0.2,
 shear_range=0.2,
 zoom_range=0.2,
 horizontal_flip=True,
 fill_mode='nearest')
test_datagen = ImageDataGenerator(rescale=1./255)
train_generator = train_datagen.flow_from_directory(
 train_dir,
 target_size=(150, 150),
 batch_size=20,
 class_mode='binary')
validation_generator = test_datagen.flow_from_directory(
 validation_dir,
 target_size=(150, 150),
 batch_size=20,
 class_mode='binary')
model.compile(loss='binary_crossentropy',
 optimizer=optimizers.RMSprop(lr=2e-5),
 metrics=['acc'])
history = model.fit_generator(
 train_generator,
 steps_per_epoch=100,
 epochs=30,
 validation_data=validation_generator,
 validation_steps=50)

我们来再次绘制结果(见图 5-17 和图 5-18)。如你所见,验证精度约为 96%。这比从头开 始训练的小型卷积神经网络要好得多。

20201210211639565 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-17 带数据增强的特征提取的训练精度和验证精度

20201210211656738 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-18 带数据增强的特征提取的训练损失和验证损失

5.3.2 微调模型

另一种广泛使用的模型复用方法是模型微调(fine-tuning),与特征提取互为补充。对于用 于特征提取的冻结的模型基,微调是指将其顶部的几层“解冻”,并将这解冻的几层和新增加的 部分(本例中是全连接分类器)联合训练(见图 5-19)。之所以叫作微调,是因为它只是略微调 整了所复用模型中更加抽象的表示,以便让这些表示与手头的问题更加相关。

20201210211713533 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-19 微调 VGG16 网络的最后一个卷积块

前面说过,冻结 VGG16 的卷积基是为了能够在上面训练一个随机初始化的分类器。同理, 只有上面的分类器已经训练好了,才能微调卷积基的顶部几层。如果分类器没有训练好,那么训练期间通过网络传播的误差信号会特别大,微调的几层之前学到的表示都会被破坏。因此, 微调网络的步骤如下。

  1. 在已经训练好的基网络(base network)上添加自定义网络。
  2. 冻结基网络。
  3. 训练所添加的部分。
  4. 解冻基网络的一些层。
  5. 联合训练解冻的这些层和添加的部分。

你在做特征提取时已经完成了前三个步骤。我们继续进行第四步:先解冻 conv_base,然 后冻结其中的部分层。

提醒一下,卷积基的架构如下所示。

>>> conv_base.summary()
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) (None, 150, 150, 3) 0
_________________________________________________________________
block1_conv1 (Conv2D) (None, 150, 150, 64) 1792
_________________________________________________________________
block1_conv2 (Conv2D) (None, 150, 150, 64) 36928
_________________________________________________________________
block1_pool (MaxPooling2D) (None, 75, 75, 64) 0
_________________________________________________________________
block2_conv1 (Conv2D) (None, 75, 75, 128) 73856
_________________________________________________________________
block2_conv2 (Conv2D) (None, 75, 75, 128) 147584
_________________________________________________________________
block2_pool (MaxPooling2D) (None, 37, 37, 128) 0
_________________________________________________________________
block3_conv1 (Conv2D) (None, 37, 37, 256) 295168
_________________________________________________________________
block3_conv2 (Conv2D) (None, 37, 37, 256) 590080
_________________________________________________________________
block3_conv3 (Conv2D) (None, 37, 37, 256) 590080
_________________________________________________________________
block3_pool (MaxPooling2D) (None, 18, 18, 256) 0
_________________________________________________________________
block4_conv1 (Conv2D) (None, 18, 18, 512) 1180160
_________________________________________________________________
block4_conv2 (Conv2D) (None, 18, 18, 512) 2359808
_________________________________________________________________
block4_conv3 (Conv2D) (None, 18, 18, 512) 2359808
_________________________________________________________________
block4_pool (MaxPooling2D) (None, 9, 9, 512) 0
_________________________________________________________________
block5_conv1 (Conv2D) (None, 9, 9, 512) 2359808
_________________________________________________________________
block5_conv2 (Conv2D) (None, 9, 9, 512) 2359808
_________________________________________________________________
block5_conv3 (Conv2D) (None, 9, 9, 512) 2359808 
_________________________________________________________________
block5_pool (MaxPooling2D) (None, 4, 4, 512) 0
=================================================================
Total params: 14,714,688
Trainable params: 14,714,688
Non-trainable params: 0

我们将微调最后三个卷积层,也就是说,直到 block4_pool 的所有层都应该被冻结,而 block5_conv1、block5_conv2 和 block5_conv3 三层应该是可训练的。

为什么不微调更多层?为什么不微调整个卷积基?你当然可以这么做,但需要考虑以下几点。

  • 卷积基中更靠底部的层编码的是更加通用的可复用特征,而更靠顶部的层编码的是更专业化的特征。微调这些更专业化的特征更加有用,因为它们需要在你的新问题上改变用途。微调更靠底部的层,得到的回报会更少。
  • 训练的参数越多,过拟合的风险越大。卷积基有 1500 万个参数,所以在你的小型数据集上训练这么多参数是有风险的。

因此,在这种情况下,一个好策略是仅微调卷积基最后的两三层。我们从上一个例子结束 的地方开始,继续实现此方法。

代码清单 5-22 冻结直到某一层的所有层

conv_base.trainable = True
set_trainable = False
for layer in conv_base.layers:
 if layer.name == 'block5_conv1':
 set_trainable = True
 if set_trainable:
 layer.trainable = True
 else:
 layer.trainable = False

现在你可以开始微调网络。我们将使用学习率非常小的 RMSProp 优化器来实现。之所以让 学习率很小,是因为对于微调的三层表示,我们希望其变化范围不要太大。太大的权重更新可能会破坏这些表示。

代码清单 5-23 微调模型

model.compile(loss='binary_crossentropy',
 optimizer=optimizers.RMSprop(lr=1e-5),
 metrics=['acc'])
history = model.fit_generator(
 train_generator,
 steps_per_epoch=100,
 epochs=100,
 validation_data=validation_generator,
 validation_steps=50)

我们用和前面一样的绘图代码来绘制结果(见图 5-20 和图 5-21)。

2020121021174152 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-20 微调模型的训练精度和验证精度

20201210211814385 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-21 微调模型的训练损失和验证损失

这些曲线看起来包含噪声。为了让图像更具可读性,你可以将每个损失和精度都替换为指数 移动平均值,从而让曲线变得平滑。下面用一个简单的实用函数来实现(见图 5-22 和图 5-23)。

代码清单 5-24 使曲线变得平滑

def smooth_curve(points, factor=0.8):
 smoothed_points = []
 for point in points:
 if smoothed_points:
 previous = smoothed_points[-1]
 smoothed_points.append(previous * factor + point * (1 - factor))
 else:
 smoothed_points.append(point)
 return smoothed_points
plt.plot(epochs,
 smooth_curve(acc), 'bo', label='Smoothed training acc')
 plt.plot(epochs,
 smooth_curve(val_acc), 'b', label='Smoothed validation acc')
plt.title('Training and validation accuracy')
plt.legend()
plt.figure()
plt.plot(epochs,
 smooth_curve(loss), 'bo', label='Smoothed training loss')
plt.plot(epochs,
 smooth_curve(val_loss), 'b', label='Smoothed validation loss')
plt.title('Training and validation loss')
plt.legend()
plt.show()

20201210211839580 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-22 微调模型的训练精度和验证精度的平滑后曲线

20201210211849200 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-23 微调模型的训练损失和验证损失的平滑后曲线

验证精度曲线变得更清楚。可以看到,精度值提高了 1%,从约 96% 提高到 97% 以上。

注意,从损失曲线上看不出与之前相比有任何真正的提高(实际上还在变差)。你可能感到 奇怪,如果损失没有降低,那么精度怎么能保持稳定或提高呢?答案很简单:图中展示的是逐 点(pointwise)损失值的平均值,但影响精度的是损失值的分布,而不是平均值,因为精度是 模型预测的类别概率的二进制阈值。即使从平均损失中无法看出,但模型也仍然可能在改进。

现在,你可以在测试数据上最终评估这个模型。

test_generator = test_datagen.flow_from_directory(
 test_dir,
 target_size=(150, 150),
 batch_size=20,
 class_mode='binary')
test_loss, test_acc = model.evaluate_generator(test_generator, steps=50)
print('test acc:', test_acc)

我们得到了 97% 的测试精度。在关于这个数据集的原始 Kaggle 竞赛中,这个结果是最佳 结果之一。但利用现代深度学习技术,你只用一小部分训练数据(约 10%)就得到了这个结果。 训练 20 000 个样本与训练 2000 个样本是有很大差别的!

5.3.3 小结

下面是你应该从以上两节的练习中学到的要点。

  • 卷积神经网络是用于计算机视觉任务的最佳机器学习模型。即使在非常小的数据集上也 可以从头开始训练一个卷积神经网络,而且得到的结果还不错。
  • 在小型数据集上的主要问题是过拟合。在处理图像数据时,数据增强是一种降低过拟合的强大方法。
  • 利用特征提取,可以很容易将现有的卷积神经网络复用于新的数据集。对于小型图像数据集,这是一种很有价值的方法。
  • 作为特征提取的补充,你还可以使用微调,将现有模型之前学到的一些数据表示应用于新问题。这种方法可以进一步提高模型性能。 现在你已经拥有一套可靠的工具来处理图像分类问题,特别是对于小型数据集。

5.4 卷积神经网络的可视化

人们常说,深度学习模型是“黑盒”,即模型学到的表示很难用人类可以理解的方式来提取和呈现。虽然对于某些类型的深度学习模型来说,这种说法部分正确,但对卷积神经网络来说 绝对不是这样。卷积神经网络学到的表示非常适合可视化,很大程度上是因为它们是视觉概念的表示。自 2013 年以来,人们开发了多种技术来对这些表示进行可视化和解释。我们不会在书中全部介绍,但会介绍三种最容易理解也最有用的方法。

  • 可视化卷积神经网络的中间输出(中间激活):有助于理解卷积神经网络连续的层如何 对输入进行变换,也有助于初步了解卷积神经网络每个过滤器的含义。
  • 可视化卷积神经网络的过滤器:有助于精确理解卷积神经网络中每个过滤器容易接受的视觉模式或视觉概念。
  • 可视化图像中类激活的热力图:有助于理解图像的哪个部分被识别为属于某个类别,从而可以定位图像中的物体。

对于第一种方法(即激活的可视化),我们将使用 5.2 节在猫狗分类问题上从头开始训练的小型卷积神经网络。对于另外两种可视化方法,我们将使用 5.3 节介绍的 VGG16 模型。

5.4.1 可视化中间激活

可视化中间激活,是指对于给定输入,展示网络中各个卷积层和池化层输出的特征图(层的输出通常被称为该层的激活,即激活函数的输出)。这让我们可以看到输入如何被分解为网络学到的不同过滤器。我们希望在三个维度对特征图进行可视化:宽度、高度和深度(通道)。每个通道都对应相对独立的特征,所以将这些特征图可视化的正确方法是将每个通道的内容分别绘制成二维图像。我们首先来加载 5.2 节保存的模型。

>>> from keras.models import load_model
>>> model = load_model('cats_and_dogs_small_2.h5')
>>> model.summary() # 作为提醒
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d_5 (Conv2D) (None, 148, 148, 32) 896
_________________________________________________________________
max_pooling2d_5 (MaxPooling2D) (None, 74, 74, 32) 0
_________________________________________________________________
conv2d_6 (Conv2D) (None, 72, 72, 64) 18496
_________________________________________________________________
max_pooling2d_6 (MaxPooling2D) (None, 36, 36, 64) 0
_________________________________________________________________
conv2d_7 (Conv2D) (None, 34, 34, 128) 73856
_________________________________________________________________
max_pooling2d_7 (MaxPooling2D) (None, 17, 17, 128) 0
_________________________________________________________________
conv2d_8 (Conv2D) (None, 15, 15, 128) 147584
_________________________________________________________________
max_pooling2d_8 (MaxPooling2D) (None, 7, 7, 128) 0
_________________________________________________________________
flatten_2 (Flatten) (None, 6272) 0
_________________________________________________________________
dropout_1 (Dropout) (None, 6272) 0
_________________________________________________________________
dense_3 (Dense) (None, 512) 3211776
_________________________________________________________________
dense_4 (Dense) (None, 1) 513
=================================================================
Total params: 3,453,121
Trainable params: 3,453,121
Non-trainable params: 0

接下来,我们需要一张输入图像,即一张猫的图像,它不属于网络的训练图像。

代码清单 5-25 预处理单张图像

img_path = '/Users/fchollet/Downloads/cats_and_dogs_small/test/cats/cat.1700.jpg'
from keras.preprocessing import image
import numpy as np
img = image.load_img(img_path, target_size=(150, 150))
img_tensor = image.img_to_array(img)
img_tensor = np.expand_dims(img_tensor, axis=0)
img_tensor /= 255.
# 其形状为 (1, 150, 150, 3)
print(img_tensor.shape)

我们来显示这张图像(见图 5-24)。

代码清单 5-26 显示测试图像

import matplotlib.pyplot as plt
plt.imshow(img_tensor[0])
plt.show()

20201210211918689 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-24 测试的猫图像

为了提取想要查看的特征图,我们需要创建一个 Keras 模型,以图像批量作为输入,并输出所有卷积层和池化层的激活。为此,我们需要使用 Keras 的 Model 类。模型实例化需要两个参数:一个输入张量(或输入张量的列表)和一个输出张量(或输出张量的列表)。得到的类是一个 Keras 模型,就像你熟悉的 Sequential 模型一样,将特定输入映射为特定输出。Model 类允许模型有多个输出,这一点与 Sequential 模型不同。想了解 Model 类的更多信息,请参见 7.1 节。

代码清单 5-27 用一个输入张量和一个输出张量列表将模型实例化

from keras import models
layer_outputs = [layer.output for layer in model.layers[:8]]
activation_model = models.Model(inputs=model.input, outputs=layer_outputs) 

输入一张图像,这个模型将返回原始模型前 8 层的激活值。这是你在本书中第一次遇到的多输出模型,之前的模型都是只有一个输入和一个输出。一般情况下,模型可以有任意个输入和输出。这个模型有一个输入和 8 个输出,即每层激活对应一个输出。

代码清单 5-28 以预测模式运行模型

activations = activation_model.predict(img_tensor)
例如,对于输入的猫图像,第一个卷积层的激活如下所示。
>>> first_layer_activation = activations[0]
>>> print(first_layer_activation.shape)
(1, 148, 148, 32)

它是大小为 148×148 的特征图,有 32 个通道。我们来绘制原始模型第一层激活的第 4 个通道(见图 5-25)。

代码清单 5-29 将第 4 个通道可视化

import matplotlib.pyplot as plt
plt.matshow(first_layer_activation[0, :, :, 4], cmap='viridis')

20201210211932667 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-25 对于测试的猫图像,第一层激活的第 4 个通道

这个通道似乎是对角边缘检测器。我们再看一下第 7 个通道(见图 5-26)。但请注意,你的 通道可能与此不同,因为卷积层学到的过滤器并不是确定的。

代码清单 5-30 将第 7 个通道可视化

plt.matshow(first_layer_activation[0, :, :, 7], cmap='viridis')

20201210211946855 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-26 对于测试的猫图像,第一层激活的第 7 个通道

这个通道看起来像是“鲜绿色圆点”检测器,对寻找猫眼睛很有用。下面我们来绘制网络中所有激活的完整可视化(见图 5-27)。我们需要在 8 个特征图中的每一个中提取并绘制每一个通道,然后将结果叠加在一个大的图像张量中,按通道并排。

代码清单 5-31 将每个中间激活的所有通道可视化

layer_names = []
for layer in model.layers[:8]:
 layer_names.append(layer.name)
images_per_row = 16
for layer_name, layer_activation in zip(layer_names, activations):
 n_features = layer_activation.shape[-1]
 size = layer_activation.shape[1]
 n_cols = n_features // images_per_row
 display_grid = np.zeros((size * n_cols, images_per_row * size))
 for col in range(n_cols):
 for row in range(images_per_row):
 channel_image = layer_activation[0,
 :, :,
 col * images_per_row + row]
 channel_image -= channel_image.mean()
 channel_image /= channel_image.std()
 channel_image *= 64
 channel_image += 128
 channel_image = np.clip(channel_image, 0, 255).astype('uint8')
 display_grid[col * size : (col + 1) * size,
 row * size : (row + 1) * size] = channel_image
 scale = 1. / size
 plt.figure(figsize=(scale * display_grid.shape[1],
 scale * display_grid.shape[0]))
 plt.title(layer_name)
 plt.grid(False)
 plt.imshow(display_grid, aspect='auto', cmap='viridis')

20201210212525889 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-27 对于测试的猫图像,每个层激活的所有通道

这里需要注意以下几点。

  • 第一层是各种边缘探测器的集合。在这一阶段,激活几乎保留了原始图像中的所有信息。
  • 随着层数的加深,激活变得越来越抽象,并且越来越难以直观地理解。它们开始表示更 高层次的概念,比如“猫耳朵”和“猫眼睛”。层数越深,其表示中关于图像视觉内容 的信息就越少,而关于类别的信息就越多。
  • 激活的稀疏度(sparsity)随着层数的加深而增大。在第一层里,所有过滤器都被输入图 像激活,但在后面的层里,越来越多的过滤器是空白的。也就是说,输入图像中找不到 这些过滤器所编码的模式。

我们刚刚揭示了深度神经网络学到的表示的一个重要普遍特征:随着层数的加深,层所提取的特征变得越来越抽象。更高的层激活包含关于特定输入的信息越来越少,而关于目标的信息越来越多(本例中即图像的类别:猫或狗)。深度神经网络可以有效地作为信息蒸馏管道 (information distillation pipeline),输入原始数据(本例中是 RGB 图像),反复对其进行变换, 将无关信息过滤掉(比如图像的具体外观),并放大和细化有用的信息(比如图像的类别)。

这与人类和动物感知世界的方式类似:人类观察一个场景几秒钟后,可以记住其中有哪些 抽象物体(比如自行车、树),但记不住这些物体的具体外观。事实上,如果你试着凭记忆 画一辆普通自行车,那么很可能完全画不出真实的样子,虽然你一生中见过上千辆自行车(见 图 5-28)。你可以现在就试着画一下,这个说法绝对是真实的。你的大脑已经学会将视觉输入完 全抽象化,即将其转换为更高层次的视觉概念,同时过滤掉不相关的视觉细节,这使得大脑很 难记住周围事物的外观。

20201210212622168 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-28 (左图)试着凭记忆画一辆自行车;(右图)自行车示意图

5.4.2 可视化卷积神经网络的过滤器

想要观察卷积神经网络学到的过滤器,另一种简单的方法是显示每个过滤器所响应的视觉模式。这可以通过在输入空间中进行梯度上升来实现:从空白输入图像开始,将梯度下降应用 于卷积神经网络输入图像的值,其目的是让某个过滤器的响应最大化。得到的输入图像是选定过滤器具有最大响应的图像。

这个过程很简单:我们需要构建一个损失函数,其目的是让某个卷积层的某个过滤器的值最大化;然后,我们要使用随机梯度下降来调节输入图像的值,以便让这个激活值最大化。例如, 对于在ImageNet上预训练的VGG16网络,其block3_conv1层第0个过滤器激活的损失如下所示。

代码清单 5-32 为过滤器的可视化定义损失张量

from keras.applications import VGG16
from keras import backend as K
model = VGG16(weights='imagenet',
 include_top=False)
layer_name = 'block3_conv1'
filter_index = 0
layer_output = model.get_layer(layer_name).output
loss = K.mean(layer_output[:, :, :, filter_index])

为了实现梯度下降,我们需要得到损失相对于模型输入的梯度。为此,我们需要使用 Keras 的 backend 模块内置的 gradients 函数。

代码清单 5-33 获取损失相对于输入的梯度

grads = K.gradients(loss, model.input)[0] 

为了让梯度下降过程顺利进行,一个非显而易见的技巧是将梯度张量除以其 L2 范数(张量 中所有值的平方的平均值的平方根)来标准化。这就确保了输入图像的更新大小始终位于相同的范围。

代码清单 5-34 梯度标准化技巧

grads /= (K.sqrt(K.mean(K.square(grads))) + 1e-5) 

现在你需要一种方法:给定输入图像,它能够计算损失张量和梯度张量的值。你可以定义 一个 Keras 后端函数来实现此方法:iterate 是一个函数,它将一个 Numpy 张量(表示为长度 为 1 的张量列表)转换为两个 Numpy 张量组成的列表,这两个张量分别是损失值和梯度值。

代码清单 5-35 给定 Numpy 输入值,得到 Numpy 输出值

iterate = K.function([model.input], [loss, grads])
import numpy as np
loss_value, grads_value = iterate([np.zeros((1, 150, 150, 3))])

现在你可以定义一个 Python 循环来进行随机梯度下降。

代码清单 5-36 通过随机梯度下降让损失最大化

input_img_data = np.random.random((1, 150, 150, 3)) * 20 + 128. 
step = 1.
for i in range(40):
 loss_value, grads_value = iterate([input_img_data])
 input_img_data += grads_value * step 

得到的图像张量是形状为 (1, 150, 150, 3) 的浮点数张量,其取值可能不是 [0, 255] 区 间内的整数。因此,你需要对这个张量进行后处理,将其转换为可显示的图像。下面这个简单 的实用函数可以做到这一点。

代码清单 5-37 将张量转换为有效图像的实用函数

def deprocess_image(x):
 x -= x.mean()
 x /= (x.std() + 1e-5)
 x *= 0.1
 x += 0.5
 x = np.clip(x, 0, 1)
 x *= 255
 x = np.clip(x, 0, 255).astype('uint8')
 return x

接下来,我们将上述代码片段放到一个Python函数中,输入一个层的名称和一个过滤器索引, 它将返回一个有效的图像张量,表示能够将特定过滤器的激活最大化的模式。

代码清单 5-38 生成过滤器可视化的函数

def generate_pattern(layer_name, filter_index, size=150):
 layer_output = model.get_layer(layer_name).output
 loss = K.mean(layer_output[:, :, :, filter_index])
 grads = K.gradients(loss, model.input)[0]
 grads /= (K.sqrt(K.mean(K.square(grads))) + 1e-5)
 iterate = K.function([model.input], [loss, grads])
 input_img_data = np.random.random((1, size, size, 3)) * 20 + 128.
 step = 1.
 for i in range(40):
 loss_value, grads_value = iterate([input_img_data])
 input_img_data += grads_value * step
 img = input_img_data[0]
 return deprocess_image(img)

我们来试用一下这个函数(见图 5-29)。

>>> plt.imshow(generate_pattern('block3_conv1', 0))

20201210212638379 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-29 block3_conv1 层第 0 个通道具有最大响应的模式

看起来,block3_conv1 层第 0 个过滤器响应的是波尔卡点(polka-dot)图案。下面来 看有趣的部分:我们可以将每一层的每个过滤器都可视化。为了简单起见,我们只查看每一 层的前 64 个过滤器,并只查看每个卷积块的第一层(即 block1_conv1、block2_conv1、 block3conv1、block4 conv1、block5_conv1)。我们将输出放在一个 8×8 的网格中, 每个网格是一个 64 像素×64 像素的过滤器模式,两个过滤器模式之间留有一些黑边(见 图 5-30 ~ 图 5-33)。

代码清单 5-39 生成某一层中所有过滤器响应模式组成的网格

layer_name = 'block1_conv1'
size = 64
margin = 5
results = np.zeros((8 * size + 7 * margin, 8 * size + 7 * margin, 3))
for i in range(8):
 for j in range(8):
 filter_img = generate_pattern(layer_name, i + (j * 8), size=size)
 horizontal_start = i * size + i * margin
 horizontal_end = horizontal_start + size
 vertical_start = j * size + j * margin
 vertical_end = vertical_start + size
 results[horizontal_start: horizontal_end,
 vertical_start: vertical_end, :] = filter_img
plt.figure(figsize=(20, 20))
plt.imshow(results)

20201210212653182 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-30 block1_conv1 层的过滤器模式

20201210212712131 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-31 block2_conv1 层的过滤器模式

2020121021272288 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-32 block3_conv1 层的过滤器模式

20201210212729837 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-33 block4_conv1 层的过滤器模式

这些过滤器可视化包含卷积神经网络的层如何观察世界的很多信息:卷积神经网络中每一 层都学习一组过滤器,以便将其输入表示为过滤器的组合。这类似于傅里叶变换将信号分解为一 组余弦函数的过程。随着层数的加深,卷积神经网络中的过滤器变得越来越复杂,越来越精细。

  • 模型第一层(block1_conv1)的过滤器对应简单的方向边缘和颜色(还有一些是彩色 边缘)。
  • block2_conv1 层的过滤器对应边缘和颜色组合而成的简单纹理。
  • 更高层的过滤器类似于自然图像中的纹理:羽毛、眼睛、树叶等。

5.4.3 可视化类激活的热力图

我还要介绍另一种可视化方法,它有助于了解一张图像的哪一部分让卷积神经网络做出了最终的分类决策。这有助于对卷积神经网络的决策过程进行调试,特别是出现分类错误的情况下。 这种方法还可以定位图像中的特定目标。

这种通用的技术叫作类激活图(CAM,class activation map)可视化,它是指对输入图像生成类激活的热力图。类激活热力图是与特定输出类别相关的二维分数网格,对任何输入图像的每个位置都要进行计算,它表示每个位置对该类别的重要程度。举例来说,对于输入到猫狗分类卷积神经网络的一张图像,CAM 可视化可以生成类别“猫”的热力图,表示图像的各个部分 与“猫”的相似程度,CAM 可视化也会生成类别“狗”的热力图,表示图像的各个部分与“狗” 的相似程度。

我们将使用的具体实现方式是“Grad-CAM: visual explanations from deep networks via gradientbased localization”a 这篇论文中描述的方法。这种方法非常简单:给定一张输入图像,对于一个 卷积层的输出特征图,用类别相对于通道的梯度对这个特征图中的每个通道进行加权。直观上 来看,理解这个技巧的一种方法是,你是用“每个通道对类别的重要程度”对“输入图像对不 同通道的激活强度”的空间图进行加权,从而得到了“输入图像对类别的激活强度”的空间图。

我们再次使用预训练的 VGG16 网络来演示此方法。

代码清单 5-40 加载带有预训练权重的 VGG16 网络

from keras.applications.vgg16 import VGG16
model = VGG16(weights='imagenet') 

图 5-34 显示了两只非洲象的图像(遵守知识共享许可协议),可能是一只母象和它的小 象,它们在大草原上漫步。我们将这张图像转换为 VGG16 模型能够读取的格式:模型在大小为 224×224 的图像上进行训练,这些训练图像都根据 keras.applications.vgg16.preprocess_ input 函数中内置的规则进行预处理。因此,我们需要加载图像,将其大小调整为 224×224, 然后将其转换为 float32 格式的 Numpy 张量,并应用这些预处理规则。

2020121021274562 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-34 非洲象的测试图像

代码清单 5-41 为 VGG16 模型预处理一张输入图像

from keras.preprocessing import image
from keras.applications.vgg16 import preprocess_input, decode_predictions
import numpy as np
img_path = '/Users/fchollet/Downloads/creative_commons_elephant.jpg'
img = image.load_img(img_path, target_size=(224, 224))
x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
x = preprocess_input(x)

现在你可以在图像上运行预训练的 VGG16 网络,并将其预测向量解码为人类可读的格式。

>>> preds = model.predict(x)
>>> print('Predicted:', decode_predictions(preds, top=3)[0])
Predicted:', [(u'n02504458', u'African_elephant', 0.92546833),
(u'n01871265', u'tusker', 0.070257246),
(u'n02504013', u'Indian_elephant', 0.0042589349)]

对这张图像预测的前三个类别分别为:

  • 非洲象(African elephant,92.5% 的概率)
  • 长牙动物(tusker,7% 的概率)
  • 印度象(Indian elephant,0.4% 的概率)

网络识别出图像中包含数量不确定的非洲象。预测向量中被最大激活的元素是对应“非洲象” 类别的元素,索引编号为 386。

>>> np.argmax(preds[0])
386

为了展示图像中哪些部分最像非洲象,我们来使用 Grad-CAM 算法。

代码清单 5-42 应用 Grad-CAM 算法

african_elephant_output = model.output[:, 386]
last_conv_layer = model.get_layer('block5_conv3')
grads = K.gradients(african_elephant_output, last_conv_layer.output)[0]
pooled_grads = K.mean(grads, axis=(0, 1, 2))
iterate = K.function([model.input],
 [pooled_grads, last_conv_layer.output[0]])
pooled_grads_value, conv_layer_output_value = iterate([x])
for i in range(512):
 conv_layer_output_value[:, :, i] *= pooled_grads_value[i]
heatmap = np.mean(conv_layer_output_value, axis=-1) 

为了便于可视化,我们还需要将热力图标准化到 0~1 范围内。得到的结果如图 5-35 所示。

代码清单 5-43 热力图后处理

heatmap = np.maximum(heatmap, 0)
heatmap /= np.max(heatmap)
plt.matshow(heatmap)

20201210212841195 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-35 测试图像的“非洲象”类激活热力图

最后,我们可以用 OpenCV 来生成一张图像,将原始图像叠加在刚刚得到的热力图上(见 图 5-36)。

代码清单 5-44 将热力图与原始图像叠加

import cv2
img = cv2.imread(img_path)
heatmap = cv2.resize(heatmap, (img.shape[1], img.shape[0]))
heatmap = np.uint8(255 * heatmap)
heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)
superimposed_img = heatmap * 0.4 + img
cv2.imwrite('/Users/fchollet/Downloads/elephant_cam.jpg', superimposed_img)

20201210212920713 - Python深度学习篇五《深度学习用于计算机视觉》

图 5-36 将类激活热力图叠加到原始图像上
这种可视化方法回答了两个重要问题:

  • 网络为什么会认为这张图像中包含一头非洲象?

  • 非洲象在图像中的什么位置?

尤其值得注意的是,小象耳朵的激活强度很大,这可能是网络找到的非洲象和印度象的不 同之处。

转载请注明:虚坏叔叔 » Python深度学习篇五《深度学习用于计算机视觉》

喜欢 (4)

您必须 登录 才能发表评论!