在深度学习一节中,我们使用 Keras
构建并训练了全连接网络以解决 CIFAR-10
数据集分类问题,但模型性能远未达到预期效果。全连接网络之所以未能达到理想状态的原因之一是由于全连接神经网络没有考虑输入图像的空间结构。在全连接网络中,首先需要将图像展平为一个一维向量,以便将其传递给第一个全连接层。为了考虑图像的空间结构,需要使用卷积神经网络 (Convolutional Neural Network
, CNN
) 。本节中,将介绍卷积神经网络的优势及其基本组件,并使用 Keras
构建卷积神经网络。
卷积神经网络 (Convolutional Neural Network
, CNN
) 是一种非常强大的深度学习模型,广泛应用于图像分析、目标检测、图像生成等任务中。CNN
的核心思想是卷积操作和参数共享,卷积操作通过滑动滤波器(也称为卷积核)在输入数据上进行元素级的乘积和求和运算,从而提取局部特征。通过多个滤波器的组合,CNN
可以学习到不同层次的特征表示,从低级到高级的抽象特征。接下来,我们首次介绍卷积神经网络的基本组件。
首先,我们需要了解深度学习中卷积的含义。下图中显示了两个不同的3×3×1
灰度图像区域与一个 3×3×1
滤波器(卷积核)执行卷积运算的过程。卷积是通过将滤波器逐像素地与部分图像区域逐像素相乘并求和来执行的。当图像区域与滤波器相似时,输出结果就可能为正数;当图像区域与滤波器相反时,输出结果可能是负数。下图中,上侧的图像区域与滤波器相似度较高,因此得到一个较大的正值,下侧的图像区域与滤波器的相似性,因此卷积结果接近零值。
如果我们将滤波器从左到右、从上到下在整个图像上滑动,并记录滑动过程中每个卷积的输出,将会得到一个新的数组,它能够根据滤波器中的值选择出输入的特定特征。卷积层可以拥有多个卷积核,下图展示了用于检测水平和垂直边缘的两个不同的滤波器:
如果使用彩色图像,则每个滤波器将拥有三个通道(即滤波器的形状为 3x3x3
),以匹配图像的三个通道(红色,绿色,蓝色)。
卷积层只是一组滤波器,其中存储神经网络通过训练学习到的权重。最初这些权重是随机的,随着网络训练的进行,滤波器会调整其权重以检测图像中的特定特征,如边缘或特定的颜色组合。
在 Keras
中,使用 Conv2D
层将卷积应用于具有两个空间维度的输入张量(如图像),使用以下代码可以构建一个包含两个滤波器的卷积层:
input_layer = layers.Input(shape=(64,64,1))
conv_layer_1 = layers.Conv2D(filters = 2,
kernel_size = (3,3),
strides = 1,
padding = "same")(input_layer)
接下来,我们详细介绍 Conv2D
层中的两个关键参数:步幅 (strides
) 和填充 (padding
)。
strides
strides
参数是卷积核在输入图像上滑动的步幅。因此,增加步幅将减小输出张量的大小。例如,当步幅 strides= 2
时,输出张量的高度和宽度将变为输入张量的一半。通常,使用该参数减小张量通过网络时的空间大小,同时增加通道数量。
padding
输入参数 padding= "same"
表示在输入数据周围填充 0
,以便当 strides = 1
时,卷积层的输出大小与输入大小完全相同。
下图展示了一个 3×3
的卷积核在一个 5×5
的输入图像上滑动的过程,其中 padding= "same"
且 strides= 1
。该卷积层的输出大小也是 5×5
,因为使用填充使卷积核在滑动时可以扩展到图像边缘以外,以便在横竖两个方向上都可以计算五次。如果没有使用填充,该卷积核只能在每个方向上计算三次,输出大小为 3×3
。
设置 padding= "same"
可以确保在使用许多卷积层时能够轻松跟踪张量的大小。使用 padding= "same"
的卷积层的输出形状为:(输入高度/步幅,输入宽度/步幅,滤波器数量)
Conv2D
层的输出是一个形状为 (batch_size, height, width, filters)
的四维张量,因此我们可以通过将多个 Conv2D
层堆叠在一起以增加神经网络的深度,提高神经网络性能。接下来,将 Conv2D
层应用于 CIFAR-10
数据集,构建一个简单的卷积神经网络预测给定图像的标签。与在全连接网络中使用的单通道(灰度)图像不同,我们将使用三个输入通道(红色,绿色和蓝色):
input_layer = layers.Input(shape=(32,32,3))
conv_layer_1 = layers.Conv2D(filters = 10,
kernel_size = (4,4),
strides = 2,
padding = 'same')(input_layer)
conv_layer_2 = layers.Conv2D(filters = 20,
kernel_size = (3,3),
strides = 2,
padding = 'same')(conv_layer_1)
flatten_layer = layers.Flatten()(conv_layer_2)
output_layer = layers.Dense(units=10, activation = 'softmax')(flatten_layer)
model = models.Model(input_layer, output_layer)
可视化以上卷积神经网络:
由于我们正在处理彩色图像,第一个卷积层中的每个滤波器的通道数为 3
,而不是 1
(即,每个滤波器的形状为 4×4×3
,而非 4×4×1
),这是为了匹配输入图像的三个通道(红色,绿色,蓝色)。同理,为了匹配第一个卷积层输出的 10
个通道,因此第二个卷积层中卷积核的通道数为 10
。通常情况下,卷积层中滤波器的通道数始终等于前一层输出的通道数。
了解数据从一个卷积层传递到下一个卷积层时张量的形状的变化情况非常重要。我们可以使用 model.summary()
方法来检查张量在网络中传递时的形状。
逐层遍历以上网络,并记录张量的形状:
(None, 32, 32, 3)
——Keras
使用 None
来表示可以同时通过神经网络传递任意数量的图像。由于网络只执行张量代数运算,不需要逐个地通过网络传递图像,而是可以将所有图像作为一批数据一起传递进去4 × 4 × 3
,这是因为我们选择的滤波器的高度和宽度为 4
(kernel_size = (4,4)
),并且在输入图像中有三个通道(红色、绿色和蓝色)。因此,该层中的参数(或权重)数量为 (4 × 4 × 3 + 1) × 10 = 490
,其中 +1
是由于每个滤波器还包含一个偏置项。每个滤波器的输出是滤波器权重和它所覆盖的图像的 4 × 4 × 3
部分的逐像素乘积。由于 strides = 2
且 padding = "same"
,因此输出的宽度和高度都减半为 16
,由于有 10
个滤波器,第一层输出的批张量中,每个张量的形状为 [16, 16, 10]
3 × 3
,且通道数为 10
,以匹配前一层的输出通道数。由于这一层有 20
个滤波器,所以参数(权重)的数量为 (3 × 3 × 10 + 1) × 20 = 1,820
。同样,使用 strides = 2
和 padding = "same"
,所以宽度和高度都减半,整体的输出形状为 (None, 8, 8, 20)
Conv2D
层后,使用 Keras
的 Flatten
层来展平张量,得到一组包含 8 × 8 × 20 = 1,280
个单元的集合。在 Flatten
层中没有要学习的参数,因为该操作只是对张量进行重新排列10
个单元的 Dense
层,并应用 softmax
激活函数,以表示 10
个类别的分类任务中每个类别的概率。该层包含 1,280 × 10 = 12,810
个参数(权重)需要学习。以上示例演示了如何将卷积层链接在一起创建卷积神经网络。在比较其准确性率与全连接神经网络的差异之前,我们将介绍另外两种可以提高神经网络性能的技术:批归一化和 dropout
。
在训练深度神经网络时,一个常见的问题是需要确保网络的权重保持在合理的范围内——如果权重值变得过大,意味着网络可能遇到了梯度爆炸问题。当误差反向传播到网络的较浅层(靠近输入的层)时,较浅层计算出的梯度有时会呈指数增加,导致权重值出现剧烈波动波动。
如果损失函数开始返回 NaN
,那么很可能是网络权重已经变得过大,引发了溢出错误。
这种现象不一定会在开始训练网络时立即发生。有时候,神经网络可以非常顺利地训练数小时,然后突然损失函数返回 NaN
,神经网络就会完全崩溃了。为了防止这种情况发生,需要了解梯度爆炸问题的根本原因。
对神经网络输入数据进行缩放的原因之一是确保在神经网络开始训练的前几次迭代中保持稳定。由于网络的权重最初是随机的,未经缩放的输入可能导致得到较大的激活值,从而引发梯度爆炸。例如,我们通常将像素值从 0-255
缩放到 [-1,1]
之间后传递到输入层。
由于输入被缩放,所以我们期望所有网络层的激活也会被缩放,最初神经网络可以按此预期执行,但随着神经网络训练的进行,网络权重逐渐开始远离其随机初始值,激活也将逐渐偏离预期,这种现象称为协变量漂移 (Covariate Shift
)。
协变量漂移类似积木块的堆叠,为了防止积木堆的坍塌,我们需要把新的积木块放置在积木堆倾斜的相反一侧,但是随着积木块的堆叠,整个积木堆会变得更加不稳定,最终导致积木堆的坍塌,这就类似协变量偏移现象。
类似的,在神经网络中每一次训练为了保持稳定,当网络更新权重时,每一层都隐含地假设其输入分布在迭代过程中与下一层之间保持一致。但是,由于神经网络中并没有任何机制能够阻止激活分布在某个方向上发生偏移,因此随着训练的进行可能导致权值失控和整个网络的崩溃。
批归一化 (Batch Normalization
) 是可以用于缓解协变量偏移问题。在训练过程中,批归一化层计算批数据中每个输入通道的均值和标准差,并通过减去均值后除以标准差实现归一化。然后,再为每个通道添加两个可学习的参数:
γ
\gamma
γ (gamma
) 和
β
\beta
β (beta
)。输出就是归一化后的输入乘以
γ
γ
γ 后加上
β
β
β,算法流程如下所示:
可以在全连接层或卷积层之后放置批归一化层,以对输出进行归一化。使用批归一化类似在积木堆两侧加上一些约束,用于确保积木块的位置不会发生较大幅度的变化。
在神经网络测试时,我们可能只需要预测一个输入观测样本,因此没有批数据可用于计算均值和标准差。为了解决这个问题,在训练过程中,批归一化层还会计算每个通道的均值和标准差的移动平均值,并将该值作为该批归一化层的一部分存储下来,在模型测试时使用。
对于上一层中的每个通道,批归一化层都需要学习两个可训练权重 (trainable parameters
):
γ
\gamma
γ 和
β
\beta
β。此外,还需要计算每个通道的移动平均值和标准差,但由于它们是从通过该层的数据中计算得到的,而不是通过反向传播训练得到的,所以称为不可训练参数 (nontrainable parameters
)。总的而言,对于上一层中的每个通道,批归一化层给出了四个参数,其中两个是可训练的,两个是不可训练的。在 Keras
中,可以使用 BatchNormalization
层实现批归一化:
from keras import layers
layers.BatchNormalization(momentum = 0.9)
其中,momentum
参数是在计算移动平均值和移动标准差时赋予前一个值的权重。
在备考考试时,我们通常会使用过去的试卷和样题来提高对学科知识的理解。一些同学试图记住这些问题的答案,但在考试中会因为没有真正理解学科内容而遇到困难。而另一些同学则利用这些练习材料来进一步提高他们对整体知识的理解,这样他们在面对从未见过的新问题时仍能正确回答。
人类在进行学习时,如果对于知识点并不理解,而仅仅是死记硬背时,往往无法将理论知识应用于实际问题中。
在机器学习中也同样如此,任何性能优秀的机器学习算法都必须确保其能够对未见过的数据进行处理,而不仅仅是记住训练数据集。如果一个算法在训练数据集上表现良好,但在测试数据集上表现不佳,则说明算法出现了过拟合。为了解决这个问题,需要使用正则化技术,以确保模型在开始过拟合时受到惩罚,提高模型在未见过的数据上的泛化能力。
机器学习算法中有许多正则化技术,对于深度学习来说,Dropout
层是最常用的方法之一。Dropout
层在训练过程中,每个 Dropout
层都会从前一层中随机选择一组单元,并将它们的输出设置为 0
:
仅仅使用如此简单的网络层,就可以确保网络不过度依赖某些单元或单元组(过度依赖实际上相当于神经网络只记住了训练集中的观测样本),从而大幅减少了过拟合现象。如果使用 Dropout
层,网络就不能过度依赖任何一个单元,因此知识更加均匀地分布在整个网络中。
这使得模型在处理未见过的数据时更加出色,因为网络已经被训练出能够在不熟悉的条件下(例如通过随机丢弃单元而引发的情况)依然能够得到准确预测的能力。在 Dropout
层内部没有需要学习的权重,因为要丢弃的单元是随机决定的。在预测时,Dropout
层不会丢弃任何单元,因此整个神经网络都被用于进行预测。
这就像人类学习了缺少某些中间过程的知识,因此必须通过理解该知识的核心原理来掌握该知识,因此在实际中,能够处理超越该知识点本身而未见过的问题。
在 Keras
中,使用 Dropout
层实现该功能,其中的 rate
参数指定了从前一层丢弃的单元比例:
from keras import layers
layers.Dropout(rate = 0.25)
Dropout
层常用在全连接层之后,因为全连接层由于权重数较多更容易产生过拟合,当然,也可以在卷积层之后使用 Dropout
层。
实践表明,批归一化也可以用于减少过拟合,因此许多现代深度学习架构不再使用 Dropout
,而仅依靠批归一化进行正则化。大多数深度学习原则并没有适用于每种情况的黄金法则,实践中确定最佳性能的方式是测试不同的架构,以观察哪种架构能够在测试数据集上获得最佳表现。
接下来,我们综合使用 Conv2D
、BatchNormalization
和 Dropout
构建一个卷积神经网络 (Convolutional Neural Network
, CNN
),并观察其在 CIFAR-10
数据集上的表现。
(1) 使用 Keras
构建 CNN
架构:
input_layer = layers.Input((32, 32, 3))
x = layers.Conv2D(filters=32, kernel_size=3, strides=1, padding="same")(
input_layer
)
x = layers.BatchNormalization()(x)
x = layers.LeakyReLU()(x)
x = layers.Conv2D(filters=32, kernel_size=3, strides=2, padding="same")(x)
x = layers.BatchNormalization()(x)
x = layers.LeakyReLU()(x)
x = layers.Conv2D(filters=64, kernel_size=3, strides=1, padding="same")(x)
x = layers.BatchNormalization()(x)
x = layers.LeakyReLU()(x)
x = layers.Conv2D(filters=64, kernel_size=3, strides=2, padding="same")(x)
x = layers.BatchNormalization()(x)
x = layers.LeakyReLU()(x)
x = layers.Flatten()(x)
x = layers.Dense(128)(x)
x = layers.BatchNormalization()(x)
x = layers.LeakyReLU()(x)
x = layers.Dropout(rate=0.5)(x)
x = layers.Dense(NUM_CLASSES)(x)
output_layer = layers.Activation("softmax")(x)
model = models.Model(input_layer, output_layer)
以上代码中,使用了四个堆叠的 Conv2D
层,每个 Conv2D
层后面紧跟一个 BatchNormalization
层和一个 LeakyReLU
层。在将结果展平后,将数据传递给包含 128
个单元的 Dense
层,再经过一个 BatchNormalization
层和一个 LeakyReLU
层以及一个用于正则化的 Dropout
层,网络最后使用包含 10
个单元 Dense
层作为输出。
批归一化层和激活层的使用顺序并无定论,通常,批归一化层放在激活层之前,但有些性能优秀的架构则恰好相反。
模型摘要打印结果如下:
(2) 编译和训练模型,并调用 evaluate
方法评估该模型在测试数据集上的准确率:
opt = optimizers.Adam(learning_rate=0.0005)
model.compile(loss="categorical_crossentropy", optimizer=opt, metrics=["accuracy"])
model.fit(
x_train,
y_train,
batch_size=32,
epochs=10,
shuffle=True,
validation_data=(x_test, y_test),
)
model.evaluate(x_test, y_test, batch_size=1000)
如上所示,该模型的准确率可以达到 73.79%
,相比全连接神经网络的 51.0%
有了明显的提升。下图展示了使用卷积神经网络进行测试的一些预测结果。
尽管 (Convolutional Neural Network
, CNN
) 中的层数相比全连接网络更多,但参数数量实际上比全连接网络更少。因此,在模型设计中进行实验并熟悉不同类型的网络层非常重要。当构建生成模型时,了解模型的内部工作原理更加重要,因为我们最感兴趣的恰是网络的中间层捕捉到的高级特征。在本节中,我们综合使用 Conv2D
、BatchNormalization
和 Dropout
构建了一个卷积神经网络。
AIGC实战——生成模型简介
AIGC实战——深度学习 (Deep Learning, DL)
AIGC实战——自编码器(Autoencoder)
AIGC实战——变分自编码器(Variational Autoencoder, VAE)
AIGC实战——使用变分自编码器生成面部图像
AIGC实战——生成对抗网络(Generative Adversarial Network, GAN)
AIGC实战——WGAN(Wasserstein GAN)