持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情
传统神经网络的缺陷
为了了解卷积神经网络 (Convolutional Neural Network
, CNN
) 的优势,我们首先了解为什么在图像中对象平移或比例改变时前馈神经网络 (Neural Network
, NN
) 性能欠佳,然后了解 CNN
对比传统前馈神经网络的改进。
我们首先考虑使用以下策略,以了解 NN
模型的缺陷:
- 建立一个
NN
模型,以预测MNIST
手写数字标签 - 获取所有标签为
1
的图像,并取这些图像的均值生成新图像 - 使用构建的
NN
预测在上一步中生成的均值图像的标签 - 将均值图像向左或向右平移若干个像素,生成新图像,并使用
NN
模型对生成的新图像进行预测
构建传统神经网络
接下来,根据上述策略,编写代码实现如下。
- 加载所需库和
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
的数字图片:
x_train1 = x_train[y_train==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.
- 对图像标签进行独热编码:
y_train = np_utils.to_categorical(y_train)
y_test = np_utils.to_categorical(y_test)
num_classes = y_train.shape[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
的所有图像的均值生成新图像,并使用训练后的模型预测此图像的标签。首先,生成图像:
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
的所有图像,将图像中各个像素位置值进行加和,求取平均像素值。生成的图像显示如下:
在上图中,像素越白,表示人们在此位置书写的频率就越高;像素越黑的位置表示书写频率越低。可以看到,中间的像素是最白的,这是因为大多数人,都习惯于在中间位置书写数字。
- 最后,查看神经网络对于此图像的预测:
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
个像素后的均值图像如下所示:
使用训练完成的的模型预测图像的标签:
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()
平移后的平均图像如下所示:
然后,对该图像进行预测:
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
相同尺寸的子矩阵 和矩阵 B
相乘并求和:
然后,向右滑动一个窗口,选择第 2
个与矩阵 B
相同尺寸的子矩阵 和矩阵 B
相乘并求和:
然后,再向右滑动一个窗口,选择第 3
个与矩阵 B
相同尺寸的子矩阵 和矩阵 B
相乘并求和:
当向右滑到尽头时,向下滑动一个窗口,并从矩阵 A
左边开始,选择第 4
个与矩阵 B
相同尺寸的子矩阵 和矩阵 B
相乘并求和:
然后,继续向右滑动,并重复以上过程滑动矩阵窗口,直到滑动到最后一个子矩阵为止,得到最终的结果 :
完整的卷积计算过程如以下动图所示:
通常,我们把较小的矩阵 B
称为滤波器 (filter
) 或卷积核 (kernel
),使用 表示卷积运算,较小矩阵中的值通过梯度下降被优化学习,卷积核中的值则为网络权重。卷积后得到的矩阵,也称为特征图 (feature map
)。
卷积核的通道数与其所乘矩阵的通道数相等。例如,当图像输入形状为 5 x 5 x 3
时(其中 3
为图像通道数),形状为 3 x 3
的卷积核也将具有 3
个通道,以便进行矩阵卷积运算:
可以看到无论卷积核有多少通道,一个卷积核计算后都只能得到一个通道。多为了捕获图像中的更多特征,通常我们会使用多个卷积核,得到多个通道的特征图,当使用多个卷积核时,计算过程如下:
需要注意的是,卷积并不等同于滤波,最直观的区别在于滤波后的图像大小不变,而卷积会改变图像大小,关于它们之间更详细的计算差异,并非本节重点,因此不再展开介绍。
步幅
在前面的示例中,卷积核每次计算时在水平和垂直方向只移动一个单位,因此可以说卷积核的步幅 (Strides
) 为 (1, 1)
,步幅越大,卷积操作跳过的值越多,例如以下为步幅为 (2, 2)
时的卷积过程:
填充
在前面的示例中,卷积操作对于输入矩阵的不同位置计算的次数并不相同,具体来说对于边缘的数值在卷积时,仅仅使用一次,而位于中心的值则会被多次使用,因此可能导致卷积错过图像边缘的一些重要信息。如果要增加对于图像边缘的考虑,我们将在输入矩阵的边缘周围的填充 (Padding
) 零,下图展示了用 0
填充边缘后的矩阵进行的卷积运算,这种填充形式进行的卷积,称为 same
填充,卷积后得到的矩阵大小为 ,其中 表示步幅。而未进行填充时执行卷积运算,也称为 valid
填充。
激活函数
在传统神经网络中,隐藏层不仅将输入值乘以权重,而且还会对数据应用非线性激活函数,将值通过激活函数传递。CNN 中同样包含激活函数,CNN 支持我们已经学习的所有可用激活函数,包括 Sigmoid
,ReLU
,tanh
和 LeakyReLU
等。
池化
研究了卷积的工作原理之后,我们将了解用于卷积操作之后的另一个常用操作:池化 (Pooling
)。假设卷积操作的输出如下,为 2 x 2
:
假设使用池化块(或者类比卷积核,我们也可以称之为池化核)为 2 x 2
的最大池化,那么将会输出 29
作为池化结果。假设卷积步骤的输出是一个更大的矩阵,如下所示:
当池化核为 2 x 2
,且步幅为 2
时,最大池化会将此矩阵划分为 2 x 2
的非重叠块,并且仅保留每个块中最大的元素值,如下所示:
从每个池化块中,最大池化仅选择具有最高值的元素。除了最大池化外,也可以使用平均池化,其将输出每个池化块中的平均值作为结果,在实践中,与其他类型的池化相比,最常使用的池化为最大池化。
卷积和池化相比全连接网络的优势
在以上 MNIST
手写数字识别示例中,可以看出传统神经网络的缺点之一是每个像素都具有不同的权重。因此,如果当这些权重用于除原始像素以外的相邻像素,则神经网络得到的输出将不是非常准确(如情景 1 的示例,像素稍微向左侧移动后,准确率就大幅下降)。
使用 CNN
可以解决这一问题,因为图像中的像素共享由卷积核构成的权重。所有像素都乘以构成卷积核的所有权重;在池化层中,仅选择卷积后具有最高值的输出。这样,无论要识别的对象像素是位于中心还是偏离中心,输出通常都是期望值。但是,当要识别的像素距离中心很远时,问题仍然存在,这是由于靠近中心的像素在 CNN
能够会被卷积核更频繁的划过。