TensorFlow深度学习实战(8)------卷积神经网络
-
- [0. 前言](#0. 前言)
- [1. 全连接网络的缺陷](#1. 全连接网络的缺陷)
- [2. 卷积神经网络](#2. 卷积神经网络)
-
- [2.1 卷积神经网络的基本概念](#2.1 卷积神经网络的基本概念)
- [2.2 TensorFlow 中的卷积层](#2.2 TensorFlow 中的卷积层)
- [2.3 TensorFlow 中的池化层](#2.3 TensorFlow 中的池化层)
- [2.4 卷积神经网络总结](#2.4 卷积神经网络总结)
- [3. 构建卷积神经网络](#3. 构建卷积神经网络)
-
- [3.1 LeNet](#3.1 LeNet)
- [3.2 使用 TensorFlow 实现 LeNet](#3.2 使用 TensorFlow 实现 LeNet)
- [4. 深度卷积神经网络优势](#4. 深度卷积神经网络优势)
- 小结
- 系列链接
0. 前言
卷积神经网络 (Convolutional Neural Network
, CNN
) 是一种非常强大的深度学习模型,广泛应用于图像分析、目标检测、图像生成等任务中。CNN
的核心思想是卷积操作和参数共享,卷积操作通过滑动滤波器(也称为卷积核)在输入数据上进行元素级的乘积和求和运算,从而提取局部特征。通过多个滤波器的组合,CNN
可以学习到不同层次的特征表示,从低级到高级的抽象特征。本节从传统全连接神经网络的缺陷为切入点,介绍了卷积神经网络的优势及其基本组件,并使用 TensorFlow
构建卷积神经网络。
1. 全连接网络的缺陷
在全连接网络中每一网络层的神经元都与相邻层的神经元相连接,在使用全连接网络分类 MNIST 手写数字数据集中,输入图像中的每个像素都输入到一个神经元中,共有 784
个( 28 x 28
像素)输入神经元。然而,这种网络未能利用图像之间的空间结构和关系。由于全连接会将每个手写数字图像转换为一个扁平向量,从而移除了局部的空间结构,导致失去了重要的信息:
python
X_train = X_train.reshape(60000, 784)
X_test = X_test.reshape(10000, 784)
卷积神经网络能够利用空间信息,因此非常适合用于图像分类。这类网络借鉴了人类视觉系统的特性,生物学研究表明,人类的视觉系统基于多个层次,能够识别越来越多的结构化信息。首先观察到单个像素,然后从中识别简单的几何形状,然后是更复杂的元素,如物体、面孔、人体、动物等等。
卷积神经网络已经在多个领域(从文本到视频再到语音)具备了超出人类水平的性能表现。接下来,我们将介绍卷积神经网络的原理,并使用 Tensorflow
构建卷积神经网络。
2. 卷积神经网络
卷积神经网络 (Convolutional Neural Network
, CNN
) 由多个神经网络层构成,通常交替使用两种不同类型的层:卷积层和池化层,最后通常由一个或多个全连接层组成。
2.1 卷积神经网络的基本概念
2.1.1 卷积
如果我们希望保留图像(或其他形式数据)的空间信息,那么就需要用像素矩阵表示每个图像。这种情况下,编码局部结构的简单方法是将相邻输入神经元的子矩阵连接成下一层中的一个隐藏神经元,单个隐藏神经元代表一个局部感知野,这一操作称为卷积,这也是卷积神经网络名称的由来。
卷积是两个矩阵间的乘法------通常一个矩阵具有较大尺寸,另一个矩阵则较小。要了解卷积,首先讲解以下示例。给定矩阵 A
和矩阵 B
如下:

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

1 × 3 + 2 × 1 + 0 × 1 + 1 × 2 + 1 × 3 + 1 × 1 + 3 × 2 + 3 × 2 + 2 × 3 = 29 1\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 1×3+2×1+0×1+1×2+1×3+1×1+3×2+3×2+2×3=29
然后,向右滑动一个窗口,选择第 2
个与矩阵 B
相同尺寸的子矩阵 [ 2 0 2 1 1 2 3 2 1 ] \left[ \begin{array}{ccc} 2 & 0 & 2\\ 1 & 1 & 2\\ 3 & 2 & 1\\\end{array}\right] 213012221 和矩阵 B
相乘并求和:

2 × 3 + 0 × 1 + 2 × 1 + 1 × 2 + 1 × 3 + 2 × 1 + 3 × 2 + 2 × 2 + 1 × 3 = 28 2\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 2×3+0×1+2×1+1×2+1×3+2×1+3×2+2×2+1×3=28
然后,再向右滑动一个窗口,选择第 3
个与矩阵 B
相同尺寸的子矩阵 [ 0 2 3 1 2 0 2 1 2 ] \left[ \begin{array}{ccc} 0 & 2 & 3\\ 1 & 2 & 0\\ 2 & 1 & 2\\\end{array}\right] 012221302 和矩阵 B
相乘并求和:

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

1 × 3 + 1 × 1 + 1 × 1 + 3 × 2 + 3 × 3 + 2 × 1 + 1 × 2 + 0 × 2 + 2 × 3 = 30 1\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 1×3+1×1+1×1+3×2+3×3+2×1+1×2+0×2+2×3=30
然后,继续向右滑动,并重复以上过程滑动矩阵窗口,直到滑动到最后一个子矩阵为止,得到最终的结果 [ 29 28 25 30 30 27 20 24 34 ] \left[ \begin{array}{ccc} 29 & 28 & 25\\ 30 & 30 & 27\\ 20 & 24 & 34\\\end{array}\right] 293020283024252734 :

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

通常,我们把较小的矩阵 B
称为滤波器 (filter
) 或卷积核 (kernel
),使用 ⊗ \otimes ⊗ 表示卷积运算,较小矩阵中的值通过梯度下降被优化学习,卷积核中的值则为网络权重。卷积后得到的矩阵,也称为特征图 (feature map
)。通过这种方式,每一层将学习一组与位置无关的潜在特征,每一层由一组并行的卷积核组成,每个卷积核只学习一个特征。
卷积核的通道数与其所乘矩阵的通道数相等。例如,当图像输入形状为 5 x 5 x 3
时(其中 3
为图像通道数),形状为 3 x 3
的卷积核也将具有 3
个通道,以便进行矩阵卷积运算:

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

需要注意的是,卷积并不等同于滤波,最直观的区别在于滤波后的图像大小不变,而卷积会改变图像大小。
2.1.2 步幅
在前面的示例中,卷积核每次计算时在水平和垂直方向只移动一个单位,因此可以说卷积核的步幅 (Strides
) 为 (1, 1)
,步幅越大,卷积操作跳过的值越多,例如以下为步幅为 (2, 2)
时的卷积过程:

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

2.1.4 激活函数
在传统神经网络中,隐藏层不仅将输入值乘以权重,而且还会对数据应用非线性激活函数,将值通过激活函数传递。CNN 中同样包含激活函数,CNN 支持我们已经学习的所有可用激活函数,包括 Sigmoid
,ReLU
,tanh
和 LeakyReLU
等。
2.1.5 池化
研究了卷积的工作原理之后,我们将了解用于卷积操作之后的另一个常用操作:池化 (Pooling
)。假设卷积操作的输出如下,为 2 x 2
:
[ 29 28 20 24 ] \left[ \begin{array}{cc} 29 & 28\\ 20 & 24\\\end{array}\right] [29202824]
假设使用池化块(或者类比卷积核,我们也可以称之为池化核)为 2 x 2
的最大池化,那么将会输出 29
作为池化结果。假设卷积步骤的输出是一个更大的矩阵,如下所示:
[ 29 28 25 29 20 24 30 26 27 23 26 27 24 25 23 31 ] \left[ \begin{array}{cccc} 29 & 28 & 25 & 29\\ 20 & 24 & 30 & 26\\ 27 & 23 & 26 & 27\\ 24 & 25 & 23 & 31\\\end{array}\right] 29202724282423252530262329262731
当池化核为 2 x 2
,且步幅为 2
时,最大池化会将此矩阵划分为 2 x 2
的非重叠块,并且仅保留每个块中最大的元素值,如下所示:
[ 29 28 ∣ 25 29 20 24 ∣ 30 26 --- --- --- --- --- 27 23 ∣ 26 27 24 25 ∣ 23 31 ] = [ 29 30 27 31 ] \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] 2920---27242824---2325∣∣---∣∣2530---26232926---2731 =[29273031]
从每个池化块中,最大池化仅选择具有最高值的元素。除了最大池化外,也可以使用平均池化,其将输出每个池化块中的平均值作为结果,在实践中,与其他类型的池化相比,最常使用的池化为最大池化。
2.2 TensorFlow 中的卷积层
在 TensorFlow
中,如果要添加一个具有 32
个并行特征(将得到 32
个特征图)和大小为 3 x 3
的卷积核的卷积层,可以写为:
python
import tensorflow as tf
from tensorflow.keras import datasets, layers, models
model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)))
这表示在 28 x 28
的图像上应用了 32
个大小为 3 x 3
的卷积,输入通道数为 1
,产生 32
个输出通道。
2.3 TensorFlow 中的池化层
在 TensorFlow
中,如果要定义一个大小为 2 x 2
的最大池化层,可以写为:
python
model.add(layers.MaxPooling2D((2, 2)))
2.4 卷积神经网络总结
我们已经学习了卷积神经网络 (Convolutional Neural Network
, CNN
) 的基本概念。CNN
在一维上对音频和文本数据沿时间维度进行卷积和池化操作,在二维上对图像沿(高度 x 宽度
)维度进行卷积和池化操作,在三维上对视频沿(高度 x 宽度 x 时间
)维度进行卷积和池化操作。
换句话说,一个 CNN
包含多个堆叠在一起的卷积核,这些卷积核学习识别特定的视觉特征,而不受图像中位置的影响。这些视觉特征在网络的初始层中往往较简单,在网络的更深层中变得越来越复杂。训练 CNN
需要确定每个卷积核的权重值,以便当输入通过多个网络层时,激活最后一层的适当神经元,从而预测正确的值。
3. 构建卷积神经网络
3.1 LeNet
Yann LeCun
提出的 LeNet
是用于识别 MNIST
手写数字的卷积神经网络,对简单的几何变换和扭曲具有鲁棒性。LeNet
的核心思想是在较低层交替使用卷积操作和最大池化操作。然后,较高层基于传统的多层感知器,输出层使用 Softmax
激活函数。

3.2 使用 TensorFlow 实现 LeNet
使用 Convolution2D
类定义 LeNet
,tf.keras.layers.Conv2D
是 tf.keras.layers.Convolution2D
的别名,因此可以互换使用:
python
layers.Convolution2D(20, (5, 5), activation='relu', input_shape=input_shape)
其中第一个参数是卷积中卷积核的数量,下一个元组是每个卷积核的尺寸。padding
是可选参数,包含两个值,padding='valid'
表示卷积仅在输入和卷积核完全重叠的地方计算,因此输出比输入小;而 padding='same'
表示输出与输入的大小相同,输入矩阵周围用零值填充。
此外,需要使用 MaxPooling2D
类:
python
layers.MaxPooling2D(pool_size=(2, 2), strides=(2, 2))
其中 pool_size=(2, 2)
是一个包含两个整数的元组,表示图像在垂直和水平方向上缩小的比例。因此,(2, 2)
会在每个维度上将图像缩小一半,strides=(2, 2)
是用于池化的步幅。
接下来,实现 LeNet
模型。
(1) 首先,导入所需库:
python
import tensorflow as tf
from tensorflow.keras import datasets, layers, models, optimizers
# network and training
EPOCHS = 5
BATCH_SIZE = 128
VERBOSE = 1
OPTIMIZER = tf.keras.optimizers.Adam()
VALIDATION_SPLIT=0.90
IMG_ROWS, IMG_COLS = 28, 28 # input image dimensions
INPUT_SHAPE = (IMG_ROWS, IMG_COLS, 1)
NB_CLASSES = 10 # number of outputs = number of digits
(2) 定义 LeNet
网络:
python
#define the convnet
class LeNet:
@staticmethod
def build(input_shape, classes):
model = models.Sequential()
第一个卷积层带有 ReLU
激活函数,接着是最大池化层。第一个卷积层包含 20
个卷积核,每个卷积核大小为 5 x 5
。输出形状与输入形状相同,因此为 28 x 28
。需要注意的是,模型的第一层还需要定义其输入形状 input_shape
。
最大池化操作使用滑动窗口在输入上滑动,并在由池化核定义的区域内取最大值:
python
# CONV => RELU => POOL
model.add(layers.Convolution2D(20, (5, 5), activation='relu',
input_shape=input_shape))
model.add(layers.MaxPooling2D(pool_size=(2, 2), strides=(2, 2)))
定义第二个带 ReLU
激活的卷积层,以及第二个最大池化层。第二个卷积层的卷积核数量增加为 50
,逐渐增加卷积核的数量是卷积神经网络中常见的技巧之一:
python
# CONV => RELU => POOL
model.add(layers.Convolution2D(50, (5, 5), activation='relu'))
model.add(layers.MaxPooling2D(pool_size=(2, 2), strides=(2, 2)))
在展平层后添加一个包含 500
个神经元的全连接层,最后是一个使用 softmax
激活函数且包含 10
个神经元的全连接层(分类器):
python
# Flatten => RELU layers
model.add(layers.Flatten())
model.add(layers.Dense(500, activation='relu'))
# a softmax classifier
model.add(layers.Dense(classes, activation="softmax"))
return model
(3) 训练网络,并打印损失:
python
# data: shuffled and split between train and test sets
(X_train, y_train), (X_test, y_test) = datasets.mnist.load_data()
# reshape
X_train = X_train.reshape((60000, 28, 28, 1))
X_test = X_test.reshape((10000, 28, 28, 1))
# normalize
X_train, X_test = X_train / 255.0, X_test / 255.0
# cast
X_train = X_train.astype('float32')
X_test = X_test.astype('float32')
print(X_train.shape[0], 'train samples')
print(X_test.shape[0], 'test samples')
# convert class vectors to binary class matrices
y_train = tf.keras.utils.to_categorical(y_train, NB_CLASSES)
y_test = tf.keras.utils.to_categorical(y_test, NB_CLASSES)
# initialize the optimizer and model
model = LeNet.build(input_shape=INPUT_SHAPE, classes=NB_CLASSES)
model.compile(loss="categorical_crossentropy", optimizer=OPTIMIZER,
metrics=["accuracy"])
model.summary()
# use TensorBoard, princess Aurora!
callbacks = [
# Write TensorBoard logs to `./logs` directory
tf.keras.callbacks.TensorBoard(log_dir='./logs')
]
# fit
history = model.fit(X_train, y_train,
batch_size=BATCH_SIZE, epochs=EPOCHS,
verbose=VERBOSE, validation_split=VALIDATION_SPLIT,
callbacks=callbacks)
score = model.evaluate(X_test, y_test, verbose=VERBOSE)
print("\nTest score:", score[0])
print('Test accuracy:', score[1])
运行代码。可以看到,模型在训练集上的准确率为 100%
,在验证集上的准确率为 97.83%
:

绘制模型训练过程中的准确率和损失变化情况,可以看到,只需 20
个 epoch
模型即可达到大约 98.25%
的准确率:

查看 MNIST
图像样本,以便了解 98.25%
准确率究竟如何。例如,人们书写数字 9
有许多种方式,如下所示。数字 3
、7
、4
和 5
也是如此,而下图中第 2
列第 3
行的数字 5
非常难以识别,即使是人类也可能会有困扰:

可以采用多种神经网络优化技术改进该简单 CNN
,例如使用不同优化器、添加 Dropout
层、使用批归一化等,在不同模型上手写数字识别情况如下所示。简单的全连接网络只有 90.71%
的准确率,这意味着大约每 100
个手写字符中就有 9
个被错误识别。然后,通过使用深度学习架构,模型获得了 8%
的提升,达到了 98.25%
的准确率,这意味着每 100
个手写字符中大约只有 1
个被错误识别。

4. 深度卷积神经网络优势
为了更好地理解深度学习和卷积神经网络的性能,我们进行另一项测试,通过减少训练集的大小观察性能的衰减情况。将包含 50,000
个样本的训练集分割为两个不同的集合:
- 用于训练模型的训练集逐渐减小,分别包含
5,900
、3,000
、1,800
、600
和300
个样本 - 使用数据集中的剩余样本作为验证集测试模型训练效果
- 测试集始终包含
10000
个样本不变
将上一小节定义的深度神经网络与简单神经网络进行比较,可以看到,当训练的可用数据增加时,深度神经网络优于简单网络。当训练样本为 5,900
个时,深度神经网络的准确率为 97.23%
,而简单网络的准确率为 94%
。
简单而言,深度神经网络需要更多的训练数据才能充分展现其潜力:

在 MNIST
数据集上训练的一系列深度学习模型性能能够在线查看。截至目前,最佳模型的错误率为 0.21%
。

小结
卷积神经网络 (Convolutional Neural Network
, CNN
) 是一种广泛应用的深度学习模型。通过参数共享、局部感知和空间结构等优势,能够更好地处理图像数据,并在图像识别、目标检测和图像生成等任务中展现出强大的能力。在本节中,介绍了卷积的计算方法以及卷积神经网络的基本组件,并使用 TensorFlow
构建卷积神经网络 LeNet
以深入了解其工作原理。
系列链接
TensorFlow深度学习实战(1)------神经网络与模型训练过程详解
TensorFlow深度学习实战(2)------使用TensorFlow构建神经网络
TensorFlow深度学习实战(3)------深度学习中常用激活函数详解
TensorFlow深度学习实战(4)------正则化技术详解
TensorFlow深度学习实战(5)------神经网络性能优化技术详解
TensorFlow深度学习实战(6)------回归分析详解
TensorFlow深度学习实战(7)------分类任务详解