遗传算法与深度学习实战(25)——使用Keras构建卷积神经网络
- 0. 前言
- 1. 卷积神经网络基本概念
- 1.1 卷积
- 1.2 步幅
- 1.3 填充
- 1.4 激活函数
- 1.5 池化
- 2. 使用 Keras 构建卷积神经网络
- 3. CNN 层的问题
- 4. 模型泛化
- 小结
- 系列链接
0. 前言
卷积神经网络 (Convolutional Neural Network
, CNN
) 的提出是为了解决传统神经网络的缺陷。即使对象位于图片中的不同位置或其在图像中具有不同占比,CNN
依旧能够正确的处理这些图像,因此在对象分类/检测任务中更加有效。在本节中,我们将使用 Keras
构建卷积神经网络模型进行图像分类,介绍 CNN
的基础知识,并构建 CNN
模型。
1. 卷积神经网络基本概念
在本节中,首先介绍卷积神经网络 (Convolutional Neural Network
, CNN
) 的相关概念与组成,了解 CNN
的工作原理。
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
个通道,以便进行矩阵卷积运算:
可以看到无论卷积核有多少通道,一个卷积核计算后都只能得到一个通道。多为了捕获图像中的更多特征,通常我们会使用多个卷积核,得到多个通道的特征图,当使用多个卷积核时,计算过程如下:
需要注意的是,卷积并不等同于滤波,最直观的区别在于滤波后的图像大小不变,而卷积会改变图像大小,关于它们之间更详细的计算差异,并非本节重点,因此不再展开介绍。
1.2 步幅
在前面的示例中,卷积核每次计算时在水平和垂直方向只移动一个单位,因此可以说卷积核的步幅 (Strides
) 为 (1, 1)
,步幅越大,卷积操作跳过的值越多,例如以下为步幅为 (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
填充。
1.4 激活函数
在传统神经网络中,隐藏层不仅将输入值乘以权重,而且还会对数据应用非线性激活函数,将值通过激活函数传递。CNN
中同样包含激活函数,包括 Sigmoid
,ReLU
,tanh
和 LeakyReLU
等。
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. 使用 Keras 构建卷积神经网络
在本节中,我们在 Fashion-MNIST
数据集上执行图像分类,Fashion-MNIST
是一个基本测试数据集,可以对其进行裁剪,减少用于训练或推理的数据量,以减少进化所需的运行时间。
(1) 加载 Fashion
数据集,对数据进行归一化并将其整形为形状为 (28,28,1)
的张量,其中 1
表示通道,这是因为数据集中的 2D
数组未定义通道,我们从原始数据集中提取前 1,000
个样本进行训练和 100
个用于测试:
import tensorflow as tf
from tensorflow.keras import datasets, layers, models
import numpy as np
import math
import timeimport matplotlib.pyplot as plt
from livelossplot import PlotLossesKerasdataset = datasets.fashion_mnist
(x_train, y_train), (x_test, y_test) = dataset.load_data()# normalize and reshape data
x_train = x_train.reshape(x_train.shape[0], 28, 28, 1).astype("float32") / 255.0
x_test = x_test.reshape(x_test.shape[0], 28, 28, 1).astype("float32") / 255.0x_train = x_train[:1000]
y_train= y_train[:1000]
x_test = x_test[:100]
y_test= y_test[:100]class_names = ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat','Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot']def plot_data(num_images, images, labels):grid = math.ceil(math.sqrt(num_images))plt.figure(figsize=(grid*2,grid*2))for i in range(num_images):plt.subplot(grid,grid,i+1)plt.xticks([])plt.yticks([])plt.grid(False) plt.imshow(images[i].reshape(28,28))plt.xlabel(class_names[labels[i]]) plt.show()plot_data(25, x_train, y_train)
减小数据集规模并非理想方案,但当我们尝试优化数千甚至数万个个体时,这样做可以节省大量时间。
(2) 构建模型, 每个 Conv2D
层定义了应用于输入的卷积操作。在连续应用卷积层的过程中,滤波器或通道的数量从上一层扩展。例如,第一个 Conv2D
层将输入通道从 1
扩展到 64
。然后,后续层将其缩小到 32
,然后到 16
,其中每个卷积层都添加一个 MaxPooling
层,用于总结特征:
model = models.Sequential()
model.add(layers.Conv2D(64, (3, 3), activation='relu', padding="same", input_shape=(28, 28, 1)))
model.add(layers.MaxPooling2D((2, 2), padding="same"))
model.add(layers.Conv2D(32, (3, 3), activation='relu', padding="same"))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(16, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))model.summary()
通过在图像上滑动滤波器块来生成相应的输出,其中每次卷积核滑动操作得到一个输出值,滤波器中的卷积核值或权重/参数是可以学习的。通常卷积操作的输出非常大,每个滤波器都会产生类似图像的输出块。减少数据量可以通过使用池化层,除了最大池化,还可以使用其他变体来获取收集特征的最小值或平均值。设置模型的卷积和最大池化层之后,使用 model.summary()
打印模型摘要。
(3) 展平 CNN
层的输出,并输入到全连接层中,该层包含 10
个神经元(输出为 10
个类别):
model.add(layers.Flatten())
model.add(layers.Dense(128, activation='relu'))
model.add(layers.Dense(10))model.summary()
(4) 训练模型,训练后输出结果如下所示。通常,在此数据集的优化后的性能达到 98%
左右,由于使用完整数据集进行训练非常耗时,我们仅抽取了少量数据样本:
model.compile(optimizer='adam',loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),metrics=['accuracy'])history = model.fit(x_train, y_train, epochs=10, validation_data=(x_test, y_test),callbacks=[PlotLossesKeras()],verbose=0)test_loss, test_acc = model.evaluate(x_test, y_test, verbose=2)
可以看到,在验证数据上的准确率稳定在 81%
左右。由于同一类别中的样本变化很小,在 Fashion-MNIST
数据集上使用简单 CNN
就可以得到较高准确率,但对于像 CIFAR-10
或 CIFAR-100
同一类别中样本变化较大的数据集,情况将有所不同。
观察训练和测试的损失和准确率之间的差异。可以看到,模型在第 3
个 epoch
时在测试数据上的推理能力开始下降。这可能与数据规模缩小或模型构建有关。在之后的学习中,我们将介绍一些经典的 CNN
架构。
3. CNN 层的问题
接下来,我们将进一步探索 CNN
架构,并了解了 CNN
的局限性。当正确使用时,CNN
是一个很好的工具,但如果使用不当,也会引发一系列问题,了解问题有助于更好的理解进化优化。
(1) 构建模型,只使用一个 CNN
层。定义一个具有 64
个滤波器和 3×3
卷积核的层,模型摘要输入如下所示,模型总参数量超过 600
万:
model = models.Sequential()
model.add(layers.Conv2D(64, (3, 3), activation='relu', padding="same", input_shape=(28, 28, 1)))
model.add(layers.Flatten())
model.add(layers.Dense(128, activation='relu'))
model.add(layers.Dense(10))model.summary()
(2) 训练模型,训练结果输出如下。可以看到,模型在训练数据上的表现非常好,但在验证/测试数据上的表现很差。这是因为具有超过 600
万参数的模型会记住训练数据集,无法很好的泛化到模型未见到的数据集上,即过拟合现象。可以看到训练集的准确率接近 100%
,然而在测试/验证集的准确率大幅下降:
model.compile(optimizer='adam',loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),metrics=['accuracy'])history = model.fit(x_train, y_train, epochs=10, validation_data=(x_test, y_test),callbacks=[PlotLossesKeras()],verbose=0)
4. 模型泛化
我们通常希望构建能够泛化的模型,因此,将数据分成训练集和测试集来验证模型的泛化能力。为了提高模型的泛化能力,可以应用例如批归一化和 dropout
等技术。然而,在某些情况下,泛化可能并非最终目标,我们可能希望识别特定的数据集,在这种情况下,就需要模型能够记住数据。
(1) 接下来,我们研究池化对卷积输出的影响,构建包含池化层的卷积神经网络,为了提高模型的泛化能力,还在池化层之间添加了批归一化层:
model = models.Sequential()
model.add(layers.Conv2D(64, (3, 3), activation='relu', padding="same", input_shape=(28, 28, 1)))
model.add(layers.BatchNormalization())
model.add(layers.MaxPooling2D((2, 2), padding="same"))
model.add(layers.Flatten())
model.add(layers.Dense(128, activation='relu'))
model.add(layers.Dense(10))model.summary()
模型摘要如下所示。可以看到,由于添加了池化,此模型的参数量约为上一模型的四分之一。
(2) 对模型进行 10
个 epoch
的训练,输出结果如下所示。虽然该模型仍存在过拟合的现象,但模型已经具有很好的泛化能力,可以看到模型验证准确率得到提升且损失得到降低:
model.compile(optimizer='adam',loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),metrics=['accuracy'])history = model.fit(x_train, y_train, epochs=10, validation_data=(x_test, y_test),callbacks=[PlotLossesKeras()],verbose=0)
我们可以通过调整多个超参数改进模型,例如添加更多的 CNN
层、批归一化层、dropout
、池化层,或修改卷积层的核大小或滤波器数量等。
小结
卷积神经网络 (Convolutional Neural Network
, CNN
) 是一种深度学习模型,特别适用于图像和视频等数据。它的设计灵感来源于生物学中对动物视觉皮层的理解,通过多层次的卷积和池化操作来实现对图像特征的学习和提取。在本节中,我们介绍了 CNN
的基本组件,并使用 Keras
实现了 CNN
模型。
系列链接
遗传算法与深度学习实战(1)——进化深度学习
遗传算法与深度学习实战(2)——生命模拟及其应用
遗传算法与深度学习实战(3)——生命模拟与进化论
遗传算法与深度学习实战(4)——遗传算法(Genetic Algorithm)详解与实现
遗传算法与深度学习实战(5)——遗传算法中常用遗传算子
遗传算法与深度学习实战(6)——遗传算法框架DEAP
遗传算法与深度学习实战(7)——DEAP框架初体验
遗传算法与深度学习实战(8)——使用遗传算法解决N皇后问题
遗传算法与深度学习实战(9)——使用遗传算法解决旅行商问题
遗传算法与深度学习实战(10)——使用遗传算法重建图像
遗传算法与深度学习实战(11)——遗传编程详解与实现
遗传算法与深度学习实战(12)——粒子群优化详解与实现
遗传算法与深度学习实战(13)——协同进化详解与实现
遗传算法与深度学习实战(14)——进化策略详解与实现
遗传算法与深度学习实战(15)——差分进化详解与实现
遗传算法与深度学习实战(16)——神经网络超参数优化
遗传算法与深度学习实战(17)——使用随机搜索自动超参数优化
遗传算法与深度学习实战(18)——使用网格搜索自动超参数优化
遗传算法与深度学习实战(19)——使用粒子群优化自动超参数优化
遗传算法与深度学习实战(20)——使用进化策略自动超参数优化
遗传算法与深度学习实战(21)——使用差分搜索自动超参数优化
遗传算法与深度学习实战(22)——使用Numpy构建神经网络
遗传算法与深度学习实战(23)——利用遗传算法优化深度学习模型
遗传算法与深度学习实战(24)——在Keras中应用神经进化优化