Keras深度学习——卷积神经网络基本概念详解

1,439 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情

传统神经网络的缺陷

为了了解卷积神经网络 (Convolutional Neural Network, CNN) 的优势,我们首先了解为什么在图像中对象平移或比例改变时前馈神经网络 (Neural Network, NN) 性能欠佳,然后了解 CNN 对比传统前馈神经网络的改进。 我们首先考虑使用以下策略,以了解 NN 模型的缺陷:

  • 建立一个 NN 模型,以预测 MNIST 手写数字标签
  • 获取所有标签为 1 的图像,并取这些图像的均值生成新图像
  • 使用构建的 NN 预测在上一步中生成的均值图像的标签
  • 将均值图像向左或向右平移若干个像素,生成新图像,并使用 NN 模型对生成的新图像进行预测

构建传统神经网络

接下来,根据上述策略,编写代码实现如下。

  1. 加载所需库和 MNIST 数据集:
from keras.datasets import mnist
from keras.layers import Flatten, Dense
from keras.models import Sequential
import matplotlib.pyplot as plt
from keras.utils import np_utils
import numpy as np

(x_train, y_train), (x_test, y_test) = mnist.load_data()
  1. 获取训练集中标签为 1 的数字图片:
x_train1 = x_train[y_train==1]
  1. 将训练数据整形以符合网络输入尺寸要求,并进行数据规范化:
num_pixels = x_train.shape[1] * x_train.shape[2]
x_train = x_train.reshape(x_train.shape[0], num_pixels).astype('float32')
x_test = x_test.reshape(x_test.shape[0], num_pixels).astype('float32')
x_train = x_train / 255.
x_test = x_test / 255.
  1. 对图像标签进行独热编码:
y_train = np_utils.to_categorical(y_train)
y_test = np_utils.to_categorical(y_test)
num_classes = y_train.shape[1]
  1. 然后,构建模型并进行拟合:
model = Sequential()
model.add(Dense(1024, input_dim=num_pixels, activation='relu'))
model.add(Dense(num_classes, activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['acc'])
model.fit(x_train, y_train,
            validation_data=(x_test, y_test),
            epochs=5,
            batch_size=1024,
            verbose=1)
  1. 接下来,使用标签为 1 的所有图像的均值生成新图像,并使用训练后的模型预测此图像的标签。首先,生成图像:
pic = np.zeros((x_train1.shape[1], x_train1.shape[2]))
pic2 = np.copy(pic)

for i in range(x_train1.shape[0]):
    pic2 = x_train1[i,:,:]
    pic = pic + pic2
pic = pic / x_train1.shape[0]
plt.imshow(pic, cmap='gray')
plt.show()

在代码中,我们初始化了一个尺寸为 28 x 28 的空白图像,并通过循环遍历 x_train1 中的所有值,即标签为 1 的所有图像,将图像中各个像素位置值进行加和,求取平均像素值。生成的图像显示如下:

所有标签为1的均值图像

在上图中,像素越白,表示人们在此位置书写的频率就越高;像素越黑的位置表示书写频率越低。可以看到,中间的像素是最白的,这是因为大多数人,都习惯于在中间位置书写数字。

  1. 最后,查看神经网络对于此图像的预测:
p = model.predict(pic.reshape(1, -1)/255.)
print(p)
c = np.argmax(p)
print('神经网络预测结果:', c)

np.argmax() 函数用于返回一个 Numpy 数组中最大值的索引,在以上示例中,最大值的索引就是模型预测概率最大的类别。以上拟合后的模型,输出的预测结果如下:

[[8.6497545e-05 9.2336720e-01 2.6006915e-03 5.4899454e-03 4.6638816e-04
  8.7751285e-04 6.4074027e-04 2.3328003e-03 6.3134938e-02 1.0033193e-03]]
神经网络预测结果: 1

传统神经网络的缺陷

情景 1:创建一个新图像,将上一节由所有标签为 1 的图像生成均值图像向左平移 1 个像素。使用以下代码,我们遍历图像的各列,并将下一列的像素值复制到当前列,从而完成向左平移:

for i in range(pic.shape[0]):
    if i < 20:
        pic[:,i]=pic[:,i+1]

plt.imshow(pic, cmap='gray')
plt.show()

向左平移 1 个像素后的均值图像如下所示:

向左平移 1 个像素

使用训练完成的的模型预测图像的标签:

p = model.predict(pic.reshape(1, -1)/255.)
print(p)
c = np.argmax(p)
print('神经网络预测结果:', c)

该模型对平移后图像的预测如下:

[[2.3171112e-03 5.3161561e-01 2.6453543e-02 8.1305495e-03 5.2826328e-04
  3.4600161e-02 4.3771293e-02 3.4394194e-04 3.5101682e-01 1.2227822e-03]]
神经网络预测结果:1

我们可以看到尽管模型可以将其正确预测为 1,但是其预测概率要比未平移像素时的概率小的多。

情景 2:创建一个新图像,将原始平均图像的像素向右移动了 2 个像素:

pic=np.zeros((x_train1.shape[1], x_train1.shape[2]))
pic2=np.copy(pic)
for i in range(x_train1.shape[0]):
    pic2=x_train1[i,:,:]
    pic=pic+pic2
pic=(pic/x_train1.shape[0])
pic2=np.copy(pic)
for i in range(pic.shape[0]):
    if ((i>6) and (i<26)):
        pic[:,i]=pic2[:,(i-3)]
plt.imshow(pic, cmap='gray')
plt.show()

平移后的平均图像如下所示:

向右平移 2 个像素

然后,对该图像进行预测:

p = model.predict(pic.reshape(1, -1)/255.)
print(p)
c = np.argmax(p)
print('神经网络预测结果:', c)

该模型对平移后图像的预测如下:

[[0.00519334 0.0018531  0.07164755 0.33244154 0.3407778  0.00380969
  0.00090572 0.19745363 0.0096615  0.03625605]]
神经网络预测结果:4

可以看到模型输出了错误的预测结果:4,以上这些问题的存在就是我们需要使用 CNN 的原因。

使用 Python 从零开始构建CNN

在本节中,首先介绍卷积神经网络 (CNN) 的相关概念与组成,以便了解CNN提高平移图像预测准确率的原理。然后,我们将使用 NumPy 从零开始构建 CNN,来了解 CNN 的工作原理。

卷积神经网络的基本概念

我们已经学习了如何构建经典神经网络,在本节中,我们了详细介绍 CNN 中卷积过程的工作原理和相关组件。

卷积

卷积是两个矩阵间的乘法——通常一个矩阵具有较大尺寸,另一个矩阵则较小。要了解卷积,首先讲解以下示例。给定矩阵 A 和矩阵 B 如下:

矩阵

在进行卷积时,我们将较小的矩阵在较大的矩阵上滑动,在上述两个矩阵中,当较小的矩阵 B 需要在较大矩阵 A 的整个区域上滑动时,会得到 9 次乘法运算,过程如下。 在矩阵 A 中从第 1 个元素开始选取与矩阵 B 相同尺寸的子矩阵 [120111332]\left[ \begin{array}{ccc} 1 & 2 & 0\\ 1 & 1 & 1\\ 3 & 3 & 2\\\end{array}\right] 和矩阵 B 相乘并求和:

卷积-1

1×3+2×1+0×1+1×2+1×3+1×1+3×2+3×2+2×3=291\times 3+2\times 1+0\times 1+1\times 2+1\times 3+1\times 1+3\times 2+3\times 2 + 2\times 3=29

然后,向右滑动一个窗口,选择第 2 个与矩阵 B 相同尺寸的子矩阵 [202112321]\left[ \begin{array}{ccc} 2 & 0 & 2\\ 1 & 1 & 2\\ 3 & 2 & 1\\\end{array}\right] 和矩阵 B 相乘并求和:

卷积-2

2×3+0×1+2×1+1×2+1×3+2×1+3×2+2×2+1×3=282\times 3+0\times 1+2\times 1+1\times 2+1\times 3+2\times 1+3\times 2+2\times 2 + 1\times 3=28

然后,再向右滑动一个窗口,选择第 3 个与矩阵 B 相同尺寸的子矩阵 [023120212]\left[ \begin{array}{ccc} 0 & 2 & 3\\ 1 & 2 & 0\\ 2 & 1 & 2\\\end{array}\right] 和矩阵 B 相乘并求和:

卷积-3

0×3+2×1+3×1+1×2+2×3+0×1+2×2+1×2+2×3=250\times 3+2\times 1+3\times 1+1\times 2+2\times 3+0\times 1+2\times 2+1\times 2 + 2\times 3=25

当向右滑到尽头时,向下滑动一个窗口,并从矩阵 A 左边开始,选择第 4 个与矩阵 B 相同尺寸的子矩阵 [111332102]\left[ \begin{array}{ccc} 1 & 1 & 1\\ 3 & 3 & 2\\ 1 & 0 & 2\\\end{array}\right] 和矩阵 B 相乘并求和:

卷积-4

1×3+1×1+1×1+3×2+3×3+2×1+1×2+0×2+2×3=301\times 3+1\times 1+1\times 1+3\times 2+3\times 3+2\times 1+1\times 2+0\times 2 + 2\times 3=30

然后,继续向右滑动,并重复以上过程滑动矩阵窗口,直到滑动到最后一个子矩阵为止,得到最终的结果 [292825303027202434]\left[ \begin{array}{ccc} 29 & 28 & 25\\ 30 & 30 & 27\\ 20 & 24 & 34\\\end{array}\right]

特征图

完整的卷积计算过程如以下动图所示:

卷积

通常,我们把较小的矩阵 B 称为滤波器 (filter) 或卷积核 (kernel),使用 \otimes 表示卷积运算,较小矩阵中的值通过梯度下降被优化学习,卷积核中的值则为网络权重。卷积后得到的矩阵,也称为特征图 (feature map)。

卷积核的通道数与其所乘矩阵的通道数相等。例如,当图像输入形状为 5 x 5 x 3 时(其中 3 为图像通道数),形状为 3 x 3 的卷积核也将具有 3 个通道,以便进行矩阵卷积运算:

三通道卷积

可以看到无论卷积核有多少通道,一个卷积核计算后都只能得到一个通道。多为了捕获图像中的更多特征,通常我们会使用多个卷积核,得到多个通道的特征图,当使用多个卷积核时,计算过程如下:

多卷积核

需要注意的是,卷积并不等同于滤波,最直观的区别在于滤波后的图像大小不变,而卷积会改变图像大小,关于它们之间更详细的计算差异,并非本节重点,因此不再展开介绍。

步幅

在前面的示例中,卷积核每次计算时在水平和垂直方向只移动一个单位,因此可以说卷积核的步幅 (Strides) 为 (1, 1),步幅越大,卷积操作跳过的值越多,例如以下为步幅为 (2, 2) 时的卷积过程:

步幅为2的卷积计算

填充

在前面的示例中,卷积操作对于输入矩阵的不同位置计算的次数并不相同,具体来说对于边缘的数值在卷积时,仅仅使用一次,而位于中心的值则会被多次使用,因此可能导致卷积错过图像边缘的一些重要信息。如果要增加对于图像边缘的考虑,我们将在输入矩阵的边缘周围的填充 (Padding) 零,下图展示了用 0 填充边缘后的矩阵进行的卷积运算,这种填充形式进行的卷积,称为 same 填充,卷积后得到的矩阵大小为 d+s1s\lfloor\frac {d+s-1} s\rfloor,其中 ss 表示步幅。而未进行填充时执行卷积运算,也称为 valid 填充。

填充

激活函数

在传统神经网络中,隐藏层不仅将输入值乘以权重,而且还会对数据应用非线性激活函数,将值通过激活函数传递。CNN 中同样包含激活函数,CNN 支持我们已经学习的所有可用激活函数,包括 SigmoidReLUtanhLeakyReLU 等。

池化

研究了卷积的工作原理之后,我们将了解用于卷积操作之后的另一个常用操作:池化 (Pooling)。假设卷积操作的输出如下,为 2 x 2

[29282024]\left[ \begin{array}{cc} 29 & 28\\ 20 & 24\\\end{array}\right]

假设使用池化块(或者类比卷积核,我们也可以称之为池化核)为 2 x 2 的最大池化,那么将会输出 29 作为池化结果。假设卷积步骤的输出是一个更大的矩阵,如下所示:

[29282529202430262723262724252331]\left[ \begin{array}{cccc} 29 & 28 & 25 & 29\\ 20 & 24 & 30 & 26\\ 27 & 23 & 26 & 27\\ 24 & 25 & 23 & 31\\\end{array}\right]

当池化核为 2 x 2,且步幅为 2 时,最大池化会将此矩阵划分为 2 x 2 的非重叠块,并且仅保留每个块中最大的元素值,如下所示:

[29282529202430262723262724252331]=[29302731]\left[ \begin{array}{ccccc} 29 & 28 & | & 25 & 29\\ 20 & 24 & | & 30 & 26\\ —&—&—&—&—\\ 27 & 23 & | & 26 & 27\\ 24 & 25 & | & 23 & 31\\\end{array}\right]=\left[ \begin{array}{cc} 29 & 30\\ 27 & 31\\\end{array}\right]

从每个池化块中,最大池化仅选择具有最高值的元素。除了最大池化外,也可以使用平均池化,其将输出每个池化块中的平均值作为结果,在实践中,与其他类型的池化相比,最常使用的池化为最大池化。

卷积和池化相比全连接网络的优势

在以上 MNIST 手写数字识别示例中,可以看出传统神经网络的缺点之一是每个像素都具有不同的权重。因此,如果当这些权重用于除原始像素以外的相邻像素,则神经网络得到的输出将不是非常准确(如情景 1 的示例,像素稍微向左侧移动后,准确率就大幅下降)。

使用 CNN 可以解决这一问题,因为图像中的像素共享由卷积核构成的权重。所有像素都乘以构成卷积核的所有权重;在池化层中,仅选择卷积后具有最高值的输出。这样,无论要识别的对象像素是位于中心还是偏离中心,输出通常都是期望值。但是,当要识别的像素距离中心很远时,问题仍然存在,这是由于靠近中心的像素在 CNN 能够会被卷积核更频繁的划过。