卷积神经网络#

我们之前已经看到,神经网络在处理图像方面表现得相当不错,即使是单层感知机也能够以较高的准确率识别 MNIST 数据集中的手写数字。然而,MNIST 数据集非常特殊,所有数字都被居中放置在图像中,这使得任务变得更简单。

在现实生活中,我们希望能够识别图片中的物体,而不受它们在图像中具体位置的影响。计算机视觉与通用分类不同,因为当我们试图在图片中找到某个特定物体时,我们会扫描图像,寻找一些特定的模式及其组合。例如,当寻找一只猫时,我们可能首先寻找水平线,这些线可能形成胡须,然后某种胡须的组合可以告诉我们这实际上是一张猫的图片。某些模式的相对位置和存在是重要的,而不是它们在图像中的确切位置。

为了提取这些模式,我们将使用卷积滤波器的概念。但首先,让我们加载之前单元中定义的所有依赖项和函数。我们还会导入 tfcv 辅助库,它包含一些实用函数,这些函数我们不想在这个笔记本中定义,以保持代码简洁明了。

In [1]:
import tensorflow as tf
from tensorflow import keras
import matplotlib.pyplot as plt
import numpy as np
from tfcv import *

在这个例子中,我们将重点关注之前看到的MNIST数据集以及图像分类。我们将首先使用Keras内置函数加载数据集。

In [2]:
(x_train,y_train),(x_test,y_test) = keras.datasets.mnist.load_data()
x_train = x_train.astype(np.float32) / 255.0
x_test = x_test.astype(np.float32) / 255.0

卷积滤波器#

卷积滤波器是小型窗口,它会遍历图像的每个像素,并计算邻近像素的加权平均值。

它们由权重系数矩阵定义。让我们来看两个不同的卷积滤波器在我们的 MNIST 手写数字上的应用示例:

In [3]:
plot_convolution(x_train[:5],[[-1.,0.,1.],[-1.,0.,1.],[-1.,0.,1.]],'Vertical edge filter')
plot_convolution(x_train[:5],[[-1.,-1.,-1.],[0.,0.,0.],[1.,1.,1.]],'Horizontal edge filter')
Notebook 输出图像Notebook 输出图像

第一个滤波器称为垂直边缘滤波器,其定义如下矩阵: $$ \left( \begin{matrix} -1 & 0 & 1 \cr -1 & 0 & 1 \cr -1 & 0 & 1 \cr \end{matrix} \right) $$ 当这个滤波器作用于相对均匀的像素区域时,所有值相加为0。然而,当它遇到图像中的垂直边缘时,会产生一个较高的峰值。这就是为什么在上面的图像中,你可以看到垂直边缘以高值和低值的形式表现出来,而水平边缘则被平均化了。

当我们应用水平边缘滤波器时,情况正好相反——水平线被放大,而垂直线被平均化。

在传统的计算机视觉中,会对图像应用多个滤波器以生成特征,然后通过机器学习算法利用这些特征来构建分类器。这些滤波器实际上与某些动物视觉系统中的神经结构非常相似。

然而,在深度学习中,我们构建了能够学习最佳卷积滤波器的网络,以解决分类问题。为此,我们引入了卷积层

卷积层#

为了使卷积层的权重可训练,我们需要将应用卷积滤波窗口到图像的过程简化为矩阵操作,这样才能进行反向传播训练。为此,我们使用了一种巧妙的矩阵变换,称为 im2col

假设我们有一个小图像 $\mathbf{x}$,像素如下:

$$ \mathbf{x} = \left( \begin{array}{ccccc} a & b & c & d & e \ f & g & h & i & j \ k & l & m & n & o \ p & q & r & s & t \ u & v & w & x & y \ \end{array} \right) $$

我们希望应用两个卷积滤波器,其权重如下: $$ W^{(i)} = \left(\begin{array}{ccc} w^{(i)}{00} & w^{(i)}{01} & w^{(i)}{02} \ w^{(i)}{10} & w^{(i)}{11} & w^{(i)}{12} \ w^{(i)}{20} & w^{(i)}{21} & w^{(i)}_{22} \ \end{array}\right) $$

在应用卷积时,结果的第一个像素是通过以下两个矩阵的逐元素相乘得到的: $\left(\begin{array}{ccc} a & b & c \ f & g & h \ k & l & m \ \end{array}\right)$ 和 $W^{(i)}$。第二个像素则是通过以下矩阵相乘得到的: $\left(\begin{array}{ccc} b & c & d \ g & h & i \ l & m & n \ \end{array}\right)$ 和 $W^{(i)}$,以此类推。

为了形式化这个过程,我们将原始图像 $x$ 的所有 $3\times3$ 片段提取到以下矩阵中:

$$ \mathrm{im2col}(x) = \left[ \begin{array}{cccccc} a & b & \ldots & g & \ldots & m \ b & c & \ldots & h & \ldots & n \ c & d & \ldots & i & \ldots & o \ f & g & \ldots & l & \ldots & r \ g & h & \ldots & m & \ldots & s \ h & i & \ldots & n & \ldots & t \ k & l & \ldots & q & \ldots & w \ l & m & \ldots & r & \ldots & x \ m & n & \ldots & s & \ldots & y \ \end{array} \right] $$

这个矩阵的每一列对应原始图像的一个 $3\times3$ 子区域。现在,为了得到卷积的结果,我们只需要将这个矩阵与权重矩阵相乘: $$ \mathbf{W} = \left[ \begin{array}{cccccccc} w^{(0)}{00} & w^{(0)}{01} & w^{(0)}{02} & w^{(0)}{10} & w^{(0)}{11} & \ldots & w^{(0)}{21} & w^{(0)}{22} \ w^{(1)}{00} & w^{(1)}{01} & w^{(1)}{02} & w^{(1)}{10} & w^{(1)}{11} & \ldots & w^{(1)}{21} & w^{(1)}{22} \ \end{array} \right] $$ (这个矩阵的每一行包含第 $i$ 个滤波器的权重,展平成一行)

因此,将卷积滤波器应用到原始图像的过程可以用矩阵乘法替代,而我们已经知道如何通过反向传播处理矩阵乘法: $$ C(x) = W\times\mathbf{im2col}(x) $$

卷积层可以通过 Conv2d 类定义。我们需要指定以下内容:

  • filters - 使用的滤波器数量。我们将使用 9 个不同的滤波器,这样网络就有足够的机会探索哪些滤波器最适合我们的场景。
  • kernel_size - 滑动窗口的大小。通常使用 3x3 或 5x5 的滤波器。

最简单的卷积神经网络(CNN)包含一个卷积层。给定输入大小为 28x28,在应用九个 5x5 滤波器后,我们将得到一个 24x24x9 的张量。空间维度变小了,因为在 28 像素中,只有 24 个位置可以容纳长度为 5 的滑动窗口。

在卷积之后,我们将 24x24x9 的张量展平成一个大小为 5184 的向量,然后添加一个线性层,生成 10 个类别。在层之间,我们还使用 relu 激活函数。

In [4]:
model = keras.models.Sequential([
    keras.layers.Conv2D(filters=9, kernel_size=(5,5), input_shape=(28,28,1),activation='relu'),
    keras.layers.Flatten(),
    keras.layers.Dense(10)
])

model.compile(loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),metrics=['acc'])

model.summary()
Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 conv2d (Conv2D)             (None, 24, 24, 9)         234       
                                                                 
 flatten (Flatten)           (None, 5184)              0         
                                                                 
 dense (Dense)               (None, 10)                51850     
                                                                 
=================================================================
Total params: 52,084
Trainable params: 52,084
Non-trainable params: 0
_________________________________________________________________

你可以看到,这个网络包含大约 50k 个可训练参数,而全连接多层网络中大约有 80k 个。这使得我们即使在较小的数据集上也能取得良好的结果,因为卷积网络的泛化能力要好得多。

Note: 在大多数实际情况下,我们希望将卷积层应用于彩色图像。因此,Conv2D 层期望输入的形状为 $W\times H\times C$,其中 $W$ 和 $H$ 分别是图像的宽度和高度,$C$ 是颜色通道的数量。对于灰度图像,我们需要相同的形状,但 $C=1$。

在开始训练之前,我们需要对数据进行重塑:

In [5]:
x_train_c = np.expand_dims(x_train,3)
x_test_c = np.expand_dims(x_test,3)
hist = model.fit(x_train_c,y_train,validation_data=(x_test_c,y_test),epochs=5)
Epoch 1/5
1875/1875 [==============================] - 15s 7ms/step - loss: 0.2099 - acc: 0.9410 - val_loss: 0.0879 - val_acc: 0.9735
Epoch 2/5
1875/1875 [==============================] - 13s 7ms/step - loss: 0.0858 - acc: 0.9753 - val_loss: 0.0682 - val_acc: 0.9791
Epoch 3/5
1875/1875 [==============================] - 13s 7ms/step - loss: 0.0665 - acc: 0.9808 - val_loss: 0.0553 - val_acc: 0.9829
Epoch 4/5
1875/1875 [==============================] - 15s 8ms/step - loss: 0.0582 - acc: 0.9835 - val_loss: 0.0513 - val_acc: 0.9835
Epoch 5/5
1875/1875 [==============================] - 14s 8ms/step - loss: 0.0527 - acc: 0.9847 - val_loss: 0.0503 - val_acc: 0.9833
In [ ]:
plot_results(hist)
Notebook 输出图像

正如您所见,与上一单元中的全连接网络相比,我们能够实现更高的准确性,并且训练所需的周期数更少。然而,训练过程本身需要更多资源,对于非GPU计算机来说可能会更慢。

可视化卷积层#

我们还可以可视化训练好的卷积层的权重,以更好地理解其内部的工作原理:

In [ ]:
fig,ax = plt.subplots(1,9)
l = model.layers[0].weights[0]
for i in range(9):
    ax[i].imshow(l[...,0,i])
    ax[i].axis('off')
Notebook 输出图像

你可以看到,有些滤波器似乎能够识别一些斜线,而其他的看起来则比较随机。

任务:使用3x3滤波器训练相同的网络并可视化它们。你是否能看到更多熟悉的模式?

多层CNN和池化层#

第一层卷积层会寻找一些基本模式,比如水平线或垂直线,但我们可以在它们之上应用更多的卷积层来寻找更高级的模式,比如基本形状。然后,更多的卷积层可以将这些形状组合成图像的一些部分,最终形成我们试图分类的目标对象。

在这样做时,我们还可以应用一个技巧:减少图像的空间尺寸。一旦我们检测到在滑动的3x3窗口内存在水平线,具体发生在哪个像素点就不那么重要了。因此,我们可以“缩小”图像的尺寸,这可以通过使用一种池化层来实现:

  • 平均池化使用一个滑动窗口(例如2x2像素),并计算窗口内值的平均值。
  • 最大池化用窗口内的最大值替代窗口。最大池化的核心思想是检测滑动窗口内某种模式的存在。

因此,在一个典型的CNN中,会有多个卷积层,并在它们之间插入池化层以减少图像的维度。同时,我们会增加滤波器的数量,因为随着模式变得更加复杂,我们需要寻找的可能的有趣组合也会增多。

展示多个卷积层和池化层的图像。

由于空间维度的减少和特征/滤波器维度的增加,这种架构也被称为金字塔架构

In [ ]:
model = keras.models.Sequential([
    keras.layers.Conv2D(filters=10, kernel_size=(5,5), input_shape=(28,28,1),activation='relu'),
    keras.layers.MaxPooling2D(),
    keras.layers.Conv2D(filters=20, kernel_size=(5,5), activation='relu'),
    keras.layers.MaxPooling2D(),    
    keras.layers.Flatten(),
    keras.layers.Dense(10)
])

model.compile(loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),metrics=['acc'])

model.summary()
Model: "sequential_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            (None, 24, 24, 10)        260       
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 12, 12, 10)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 8, 8, 20)          5020      
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 4, 4, 20)          0         
_________________________________________________________________
flatten_1 (Flatten)          (None, 320)               0         
_________________________________________________________________
dense_1 (Dense)              (None, 10)                3210      
=================================================================
Total params: 8,490
Trainable params: 8,490
Non-trainable params: 0
_________________________________________________________________

注意,可训练参数的数量(约8.5K)比之前的情况显著减少。这是因为卷积层通常具有较少的参数,并且在应用最终的全连接层之前,图像的维度显著降低。较少的参数数量对我们的模型有积极影响,因为它有助于防止即使在较小的数据集规模上也出现过拟合。

In [ ]:
hist = model.fit(x_train_c,y_train,validation_data=(x_test_c,y_test),epochs=5)
Epoch 1/5
1875/1875 [==============================] - 6s 3ms/step - loss: 0.0723 - acc: 0.9780 - val_loss: 0.0423 - val_acc: 0.9861
Epoch 2/5
1875/1875 [==============================] - 6s 3ms/step - loss: 0.0523 - acc: 0.9842 - val_loss: 0.0425 - val_acc: 0.9866
Epoch 3/5
1875/1875 [==============================] - 6s 3ms/step - loss: 0.0448 - acc: 0.9868 - val_loss: 0.0403 - val_acc: 0.9865
Epoch 4/5
1875/1875 [==============================] - 6s 3ms/step - loss: 0.0383 - acc: 0.9886 - val_loss: 0.0323 - val_acc: 0.9888
Epoch 5/5
1875/1875 [==============================] - 6s 3ms/step - loss: 0.0338 - acc: 0.9895 - val_loss: 0.0331 - val_acc: 0.9896
In [ ]:
plot_results(hist)
Notebook 输出图像

我们可以注意到,与仅使用一层相比,我们能够在更少的训练周期(通常只需1到2个周期)内实现更高的准确率。这意味着复杂的网络架构需要更少的数据就能理解发生了什么,并从我们的图像中提取通用模式。然而,训练过程也会更耗时,并且需要使用GPU。

使用CIFAR-10数据集中的真实图像进行实验#

虽然我们的手写数字识别问题看起来像是一个简单的练习,但现在我们已经准备好处理更复杂的问题了。让我们来探索一个更高级的数据集——CIFAR-10,它包含了不同物体的图片。这个数据集名为CIFAR-10,包含了60,000张32x32的图像,分为10个类别。

In [ ]:
(x_train,y_train),(x_test,y_test) = keras.datasets.cifar10.load_data()
x_train = x_train.astype(np.float32) / 255.0
x_test = x_test.astype(np.float32) / 255.0
classes = ('plane', 'car', 'bird', 'cat',
           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
In [ ]:
display_dataset(x_train,y_train,classes=classes)
Notebook 输出图像

一个著名的用于CIFAR-10的架构叫做LeNet,由Yann LeCun提出。它遵循了我们上面概述的相同原则,主要区别是有3个输入颜色通道而不是1个。

In [ ]:
model = keras.models.Sequential([
    keras.layers.Conv2D(filters = 6, kernel_size = 5, strides = 1, activation = 'relu', input_shape = (32,32,3)),
    keras.layers.MaxPooling2D(pool_size = 2, strides = 2),
    keras.layers.Conv2D(filters = 16, kernel_size = 5, strides = 1, activation = 'relu'),
    keras.layers.MaxPooling2D(pool_size = 2, strides = 2),
    keras.layers.Flatten(),
    keras.layers.Dense(120, activation = 'relu'),
    keras.layers.Dense(84, activation = 'relu'),
    keras.layers.Dense(10, activation = 'softmax')])

model.summary()
Model: "sequential_3"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_8 (Conv2D)            (None, 28, 28, 6)         456       
_________________________________________________________________
max_pooling2d_6 (MaxPooling2 (None, 14, 14, 6)         0         
_________________________________________________________________
conv2d_9 (Conv2D)            (None, 10, 10, 16)        2416      
_________________________________________________________________
max_pooling2d_7 (MaxPooling2 (None, 5, 5, 16)          0         
_________________________________________________________________
flatten_3 (Flatten)          (None, 400)               0         
_________________________________________________________________
dense_9 (Dense)              (None, 120)               48120     
_________________________________________________________________
dense_10 (Dense)             (None, 84)                10164     
_________________________________________________________________
dense_11 (Dense)             (None, 10)                850       
=================================================================
Total params: 62,006
Trainable params: 62,006
Non-trainable params: 0
_________________________________________________________________

正确训练这个网络将需要大量时间,最好在支持GPU的计算设备上进行。

In [ ]:
model.compile(optimizer = 'adam', loss = 'sparse_categorical_crossentropy', metrics = ['acc'])
hist = model.fit(x_train,y_train,validation_data=(x_test,y_test),epochs=10)
Epoch 1/10
1563/1563 [==============================] - 6s 4ms/step - loss: 1.6205 - acc: 0.4033 - val_loss: 1.4287 - val_acc: 0.4743
Epoch 2/10
1563/1563 [==============================] - 6s 4ms/step - loss: 1.3435 - acc: 0.5154 - val_loss: 1.2804 - val_acc: 0.5412
Epoch 3/10
1563/1563 [==============================] - 5s 3ms/step - loss: 1.2225 - acc: 0.5645 - val_loss: 1.2164 - val_acc: 0.5600
Epoch 4/10
1563/1563 [==============================] - 5s 4ms/step - loss: 1.1360 - acc: 0.5957 - val_loss: 1.1918 - val_acc: 0.5768
Epoch 5/10
1563/1563 [==============================] - 5s 3ms/step - loss: 1.0776 - acc: 0.6178 - val_loss: 1.1451 - val_acc: 0.5906
Epoch 6/10
1563/1563 [==============================] - 5s 4ms/step - loss: 1.0228 - acc: 0.6370 - val_loss: 1.1178 - val_acc: 0.6098
Epoch 7/10
1563/1563 [==============================] - 5s 4ms/step - loss: 0.9769 - acc: 0.6544 - val_loss: 1.0793 - val_acc: 0.6202
Epoch 8/10
1563/1563 [==============================] - 5s 3ms/step - loss: 0.9340 - acc: 0.6711 - val_loss: 1.0783 - val_acc: 0.6271
Epoch 9/10
1563/1563 [==============================] - 5s 4ms/step - loss: 0.8983 - acc: 0.6824 - val_loss: 1.0952 - val_acc: 0.6203
Epoch 10/10
1563/1563 [==============================] - 6s 4ms/step - loss: 0.8648 - acc: 0.6939 - val_loss: 1.1103 - val_acc: 0.6217
In [ ]:
plot_results(hist)
Notebook 输出图像

我们在短时间内训练了几个周期后所达到的准确率似乎并不算太高。然而,请记住,盲目猜测的准确率只有10%,而且我们的任务实际上比MNIST数字分类要复杂得多。在如此短的训练时间内能够达到超过50%的准确率,已经是一个不错的成就。

收获总结#

在本单元中,我们学习了计算机视觉神经网络的核心概念——卷积神经网络。现实中用于图像分类、目标检测,甚至图像生成的网络架构,都是基于卷积神经网络(CNN)的,只是增加了更多的层数以及一些额外的训练技巧。


免责声明
本文档使用AI翻译服务 Co-op Translator 进行翻译。尽管我们努力确保翻译的准确性,但请注意,自动翻译可能包含错误或不准确之处。应以原始语言的文档作为权威来源。对于关键信息,建议使用专业人工翻译。我们不对因使用此翻译而产生的任何误解或误读承担责任。