Python 深度学习第二版(GPT 重译)(三)(3)https://developer.aliyun.com/article/1485271
8.2.5 使用数据增强
过拟合是由于样本量太少,导致无法训练出能够泛化到新数据的模型。如果有无限的数据,你的模型将暴露于手头数据分布的每一个可能方面:你永远不会过拟合。数据增强采取生成更多训练数据的方法,通过一些随机转换增强样本,生成看起来可信的图像。目标是,在训练时,你的模型永远不会看到完全相同的图片。这有助于让模型接触数据的更多方面,从而更好地泛化。
在 Keras 中,可以通过在模型开头添加一些数据增强层来实现。让我们通过一个示例开始:下面的 Sequential 模型链接了几个随机图像转换。在我们的模型中,我们会在Rescaling
层之前包含它。
列表 8.14 定义要添加到图像模型中的数据增强阶段
data_augmentation = keras.Sequential( [ layers.RandomFlip("horizontal"), layers.RandomRotation(0.1), layers.RandomZoom(0.2), ] )
这些只是一些可用的层(更多内容,请参阅 Keras 文档)。让我们快速浏览一下这段代码:
RandomFlip("horizontal")
—对通过它的随机 50%图像应用水平翻转RandomRotation(0.1)
—将输入图像旋转一个在范围[–10%,+10%]内的随机值(这些是完整圆的分数—以度为单位,范围将是[–36 度,+36 度])RandomZoom(0.2)
—通过在范围[-20%,+20%]内的随机因子放大或缩小图像
让我们看一下增强后的图像(参见图 8.10)。
列表 8.15 显示一些随机增强的训练图像
plt.figure(figsize=(10, 10)) for images, _ in train_dataset.take(1): # ❶ for i in range(9): augmented_images = data_augmentation(images) # ❷ ax = plt.subplot(3, 3, i + 1) plt.imshow(augmented_images[0].numpy().astype("uint8")) # ❸ plt.axis("off")
❶ 我们可以使用 take(N)仅从数据集中取样 N 批次。这相当于在第 N 批次后的循环中插入一个中断。
❷ 将增强阶段应用于图像批次。
❸ 显示输出批次中的第一张图像。对于九次迭代中的每一次,这是同一图像的不同增强。
图 8.10 通过随机数据增强生成一个非常好的男孩的变化
如果我们使用这个数据增强配置训练一个新模型,那么模型将永远不会看到相同的输入两次。但是它看到的输入仍然高度相关,因为它们来自少量原始图像—我们无法产生新信息;我们只能重新混合现有信息。因此,这可能不足以完全消除过拟合。为了进一步对抗过拟合,我们还将在密集连接分类器之前向我们的模型添加一个Dropout
层。
关于随机图像增强层,还有一件事你应该知道:就像Dropout
一样,在推断时(当我们调用predict()
或evaluate()
时),它们是不活动的。在评估期间,我们的模型的行为将与不包括数据增强和 dropout 时完全相同。
列表 8.16 定义一个包含图像增强和 dropout 的新卷积神经网络
inputs = keras.Input(shape=(180, 180, 3)) x = data_augmentation(inputs) x = layers.Rescaling(1./255)(x) x = layers.Conv2D(filters=32, kernel_size=3, activation="relu")(x) x = layers.MaxPooling2D(pool_size=2)(x) x = layers.Conv2D(filters=64, kernel_size=3, activation="relu")(x) x = layers.MaxPooling2D(pool_size=2)(x) x = layers.Conv2D(filters=128, kernel_size=3, activation="relu")(x) x = layers.MaxPooling2D(pool_size=2)(x) x = layers.Conv2D(filters=256, kernel_size=3, activation="relu")(x) x = layers.MaxPooling2D(pool_size=2)(x) x = layers.Conv2D(filters=256, kernel_size=3, activation="relu")(x) x = layers.Flatten()(x) x = layers.Dropout(0.5)(x) outputs = layers.Dense(1, activation="sigmoid")(x) model = keras.Model(inputs=inputs, outputs=outputs) model.compile(loss="binary_crossentropy", optimizer="rmsprop", metrics=["accuracy"])
让我们使用数据增强和 dropout 来训练模型。因为我们预计过拟合会在训练期间发生得更晚,所以我们将训练三倍的时期—一百个时期。
列表 8.17 训练正则化的卷积神经网络
callbacks = [ keras.callbacks.ModelCheckpoint( filepath="convnet_from_scratch_with_augmentation.keras", save_best_only=True, monitor="val_loss") ] history = model.fit( train_dataset, epochs=100, validation_data=validation_dataset, callbacks=callbacks)
让我们再次绘制结果:参见图 8.11。由于数据增强和 dropout,我们开始过拟合的时间要晚得多,大约在 60-70 个时期(与原始模型的 10 个时期相比)。验证准确性最终稳定在 80-85%的范围内—相比我们的第一次尝试,这是一个很大的改进。
图 8.11 使用数据增强的训练和验证指标
让我们检查测试准确性。
列表 8.18 在测试集上评估模型
test_model = keras.models.load_model( "convnet_from_scratch_with_augmentation.keras") test_loss, test_acc = test_model.evaluate(test_dataset) print(f"Test accuracy: {test_acc:.3f}")
我们获得了 83.5%的测试准确性。看起来不错!如果你在使用 Colab,请确保下载保存的文件(convnet_from_scratch_with_augmentation.keras),因为我们将在下一章中用它进行一些实验。
通过进一步调整模型的配置(例如每个卷积层的滤波器数量,或模型中的层数),我们可能能够获得更高的准确性,可能高达 90%。但是,仅通过从头开始训练我们自己的卷积神经网络,要想获得更高的准确性将会变得困难,因为我们的数据量太少。为了提高这个问题上的准确性,我们将不得不使用一个预训练模型,这是接下来两节的重点。
8.3 利用预训练模型
一种常见且高效的小图像数据集深度学习方法是使用预训练模型。预训练模型是先前在大型数据集上训练过的模型,通常是在大规模图像分类任务上。如果原始数据集足够大且足够通用,那么预训练模型学习到的空间特征层次结构可以有效地充当视觉世界的通用模型,因此,其特征对许多不同的计算机视觉问题都可能有用,即使这些新问题可能涉及与原始任务完全不同的类别。例如,您可以在 ImageNet 上训练一个模型(其中类别主要是动物和日常物品),然后将这个训练好的模型重新用于识别图像中的家具物品等远程任务。与许多较旧的、浅层学习方法相比,学习到的特征在不同问题之间的可移植性是深度学习的一个关键优势,这使得深度学习在小数据问题上非常有效。
在这种情况下,让我们考虑一个在 ImageNet 数据集上训练的大型卷积网络(140 万标记图像和 1000 个不同类别)。ImageNet 包含许多动物类别,包括不同品种的猫和狗,因此您可以期望它在狗与猫的分类问题上表现良好。
我们将使用 VGG16 架构,这是由 Karen Simonyan 和 Andrew Zisserman 于 2014 年开发的。虽然这是一个较老的模型,远非当前技术水平,并且比许多其他最新模型要重,但我选择它是因为其架构类似于您已经熟悉的内容,并且没有引入任何新概念。这可能是您第一次遇到这些可爱的模型名称之一——VGG、ResNet、Inception、Xception 等;如果您继续进行计算机视觉的深度学习,您将经常遇到它们。
使用预训练模型有两种方法:特征提取和微调。我们将涵盖这两种方法。让我们从特征提取开始。
8.3.1 使用预训练模型进行特征提取
特征提取包括使用先前训练模型学习到的表示来从新样本中提取有趣的特征。然后,这些特征通过一个新的分类器,该分类器是从头开始训练的。
正如您之前看到的,用于图像分类的卷积网络由两部分组成:它们从一系列池化和卷积层开始,然后以一个密集连接的分类器结束。第一部分被称为模型的卷积基础。在卷积网络的情况下,特征提取包括获取先前训练网络的卷积基础,将新数据通过它运行,并在输出之上训练一个新的分类器(参见图 8.12)。
图 8.12 在保持相同卷积基础的情况下交换分类器
为什么只重用卷积基?我们能否也重用密集连接的分类器?一般来说,应该避免这样做。原因是卷积基学习到的表示可能更通用,因此更具重用性:卷积网络的特征图是图片上通用概念的存在图,这些概念可能无论面临什么计算机视觉问题都有用。但分类器学习到的表示必然是特定于模型训练的类集合的——它们只包含关于整个图片中这个或那个类别存在概率的信息。此外,密集连接层中的表示不再包含有关对象在输入图像中位置的信息;这些层摆脱了空间的概念,而对象位置仍然由卷积特征图描述。对于需要考虑对象位置的问题,密集连接特征基本上是无用的。
请注意,特定卷积层提取的表示的泛化程度(因此可重用性)取决于模型中该层的深度。模型中较早的层提取局部、高度通用的特征图(如视觉边缘、颜色和纹理),而较高层提取更抽象的概念(如“猫耳”或“狗眼”)。因此,如果您的新数据集与原始模型训练的数据集差异很大,您可能最好只使用模型的前几层进行特征提取,而不是使用整个卷积基。
在这种情况下,因为 ImageNet 类别集包含多个狗和猫类别,重用原始模型的密集连接层中包含的信息可能是有益的。但我们选择不这样做,以涵盖新问题的类别集与原始模型的类别集不重叠的更一般情况。让我们通过使用在 ImageNet 上训练的 VGG16 网络的卷积基从猫和狗图片中提取有趣的特征,然后在这些特征之上训练一个狗与猫的分类器来实践这一点。
VGG16 模型,以及其他模型,已经预先打包在 Keras 中。您可以从 keras.applications
模块导入它。许多其他图像分类模型(都在 ImageNet 数据集上预训练)都作为 keras.applications
的一部分可用:
- Xception
- ResNet
- MobileNet
- EfficientNet
- DenseNet
- 等等。
让我们实例化 VGG16 模型。
列表 8.19 实例化 VGG16 卷积基
conv_base = keras.applications.vgg16.VGG16( weights="imagenet", include_top=False, input_shape=(180, 180, 3))
我们向构造函数传递三个参数:
weights
指定了初始化模型的权重检查点。include_top
指的是是否包括(或不包括)网络顶部的密集连接分类器。默认情况下,这个密集连接分类器对应于 ImageNet 的 1,000 个类。因为我们打算使用我们自己的密集连接分类器(只有两个类:cat
和dog
),所以我们不需要包含它。input_shape
是我们将馈送到网络的图像张量的形状。这个参数是完全可选的:如果我们不传递它,网络将能够处理任何大小的输入。在这里,我们传递它,以便我们可以可视化(在下面的摘要中)随着每个新的卷积和池化层特征图的大小如何缩小。
这是 VGG16 卷积基架构的详细信息。它类似于您已经熟悉的简单卷积网络:
>>> conv_base.summary() Model: "vgg16" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= input_19 (InputLayer) [(None, 180, 180, 3)] 0 _________________________________________________________________ block1_conv1 (Conv2D) (None, 180, 180, 64) 1792 _________________________________________________________________ block1_conv2 (Conv2D) (None, 180, 180, 64) 36928 _________________________________________________________________ block1_pool (MaxPooling2D) (None, 90, 90, 64) 0 _________________________________________________________________ block2_conv1 (Conv2D) (None, 90, 90, 128) 73856 _________________________________________________________________ block2_conv2 (Conv2D) (None, 90, 90, 128) 147584 _________________________________________________________________ block2_pool (MaxPooling2D) (None, 45, 45, 128) 0 _________________________________________________________________ block3_conv1 (Conv2D) (None, 45, 45, 256) 295168 _________________________________________________________________ block3_conv2 (Conv2D) (None, 45, 45, 256) 590080 _________________________________________________________________ block3_conv3 (Conv2D) (None, 45, 45, 256) 590080 _________________________________________________________________ block3_pool (MaxPooling2D) (None, 22, 22, 256) 0 _________________________________________________________________ block4_conv1 (Conv2D) (None, 22, 22, 512) 1180160 _________________________________________________________________ block4_conv2 (Conv2D) (None, 22, 22, 512) 2359808 _________________________________________________________________ block4_conv3 (Conv2D) (None, 22, 22, 512) 2359808 _________________________________________________________________ block4_pool (MaxPooling2D) (None, 11, 11, 512) 0 _________________________________________________________________ block5_conv1 (Conv2D) (None, 11, 11, 512) 2359808 _________________________________________________________________ block5_conv2 (Conv2D) (None, 11, 11, 512) 2359808 _________________________________________________________________ block5_conv3 (Conv2D) (None, 11, 11, 512) 2359808 _________________________________________________________________ block5_pool (MaxPooling2D) (None, 5, 5, 512) 0 ================================================================= Total params: 14,714,688 Trainable params: 14,714,688 Non-trainable params: 0 _________________________________________________________________
最终的特征图形状为(5, 5, 512)
。这是我们将在其上放置一个密集连接分类器的特征图。
在这一点上,我们可以有两种方式继续:
- 运行卷积基在我们的数据集上,将其输出记录到磁盘上的 NumPy 数组中,然后使用这些数据作为输入到一个独立的、与本书第四章中看到的类似的密集连接分类器。这种解决方案运行快速且成本低,因为它只需要为每个输入图像运行一次卷积基,而卷积基是整个流程中最昂贵的部分。但出于同样的原因,这种技术不允许我们使用数据增强。
- 通过在
conv_base
顶部添加Dense
层来扩展我们的模型,并在输入数据上端对端地运行整个模型。这将允许我们使用数据增强,因为每个输入图像在模型看到时都会经过卷积基。但出于同样的原因,这种技术比第一种要昂贵得多。
我们将涵盖这两种技术。让我们逐步了解设置第一种技术所需的代码:记录conv_base
在我们的数据上的输出,并使用这些输出作为新模型的输入。
无数据增强的快速特征提取
我们将通过在训练、验证和测试数据集上调用conv_base
模型的predict()
方法来提取特征作为 NumPy 数组。
让我们迭代我们的数据集以提取 VGG16 特征。
列表 8.20 提取 VGG16 特征和相应标签
import numpy as np def get_features_and_labels(dataset): all_features = [] all_labels = [] for images, labels in dataset: preprocessed_images = keras.applications.vgg16.preprocess_input(images) features = conv_base.predict(preprocessed_images) all_features.append(features) all_labels.append(labels) return np.concatenate(all_features), np.concatenate(all_labels) train_features, train_labels = get_features_and_labels(train_dataset) val_features, val_labels = get_features_and_labels(validation_dataset) test_features, test_labels = get_features_and_labels(test_dataset)
重要的是,predict()
只期望图像,而不是标签,但我们当前的数据集产生的批次包含图像和它们的标签。此外,VGG16
模型期望使用keras.applications.vgg16.preprocess_input
函数预处理输入,该函数将像素值缩放到适当的范围。
提取的特征目前的形状为(samples,
5,
5,
512)
:
>>> train_features.shape (2000, 5, 5, 512)
在这一点上,我们可以定义我们的密集连接分类器(注意使用了 dropout 进行正则化),并在我们刚刚记录的数据和标签上对其进行训练。
列表 8.21 定义和训练密集连接分类器
inputs = keras.Input(shape=(5, 5, 512)) x = layers.Flatten()(inputs) # ❶ x = layers.Dense(256)(x) x = layers.Dropout(0.5)(x) outputs = layers.Dense(1, activation="sigmoid")(x) model = keras.Model(inputs, outputs) model.compile(loss="binary_crossentropy", optimizer="rmsprop", metrics=["accuracy"]) callbacks = [ keras.callbacks.ModelCheckpoint( filepath="feature_extraction.keras", save_best_only=True, monitor="val_loss") ] history = model.fit( train_features, train_labels, epochs=20, validation_data=(val_features, val_labels), callbacks=callbacks)
❶ 注意在将特征传递给密集层之前使用了 Flatten 层。
训练非常快,因为我们只需要处理两个Dense
层——即使在 CPU 上,一个时代也不到一秒。
让我们在训练过程中查看损失和准确率曲线(见图 8.13)。
图 8.13 普通特征提取的训练和验证指标
列表 8.22 绘制结果
import matplotlib.pyplot as plt acc = history.history["accuracy"] val_acc = history.history["val_accuracy"] loss = history.history["loss"] val_loss = history.history["val_loss"] epochs = range(1, len(acc) + 1) plt.plot(epochs, acc, "bo", label="Training accuracy") plt.plot(epochs, val_acc, "b", label="Validation accuracy") plt.title("Training and validation accuracy") plt.legend() plt.figure() plt.plot(epochs, loss, "bo", label="Training loss") plt.plot(epochs, val_loss, "b", label="Validation loss") plt.title("Training and validation loss") plt.legend() plt.show()
我们达到了约 97%的验证准确率——比我们在前一节使用从头开始训练的小模型取得的结果要好得多。然而,这有点不公平的比较,因为 ImageNet 包含许多狗和猫实例,这意味着我们预训练的模型已经具有了完成任务所需的确切知识。当您使用预训练特征时,情况并不总是如此。
然而,图表也表明我们几乎从一开始就过拟合了——尽管使用了相当大的 dropout 率。这是因为这种技术没有使用数据增强,而数据增强对于防止小图像数据集过拟合是至关重要的。
结合数据增强的特征提取
现在让我们回顾一下我提到的第二种特征提取技术,这种技术速度较慢,成本较高,但允许我们在训练过程中使用数据增强:创建一个将conv_base
与新的密集分类器连接起来的模型,并在输入上端对端地进行训练。
为了做到这一点,我们首先要冻结卷积基。冻结一层或一组层意味着在训练过程中阻止它们的权重被更新。如果我们不这样做,卷积基先前学到的表示将在训练过程中被修改。因为顶部的Dense
层是随机初始化的,非常大的权重更新会通过网络传播,有效地破坏先前学到的表示。
在 Keras 中,通过将其trainable
属性设置为False
来冻结一个层或模型。
列表 8.23 实例化和冻结 VGG16 卷积基
conv_base = keras.applications.vgg16.VGG16( weights="imagenet", include_top=False) conv_base.trainable = False
将trainable
设置为False
会清空层或模型的可训练权重列表。
列表 8.24 在冻结前后打印可训练权重列表
>>> conv_base.trainable = True >>> print("This is the number of trainable weights " "before freezing the conv base:", len(conv_base.trainable_weights)) This is the number of trainable weights before freezing the conv base: 26 >>> conv_base.trainable = False >>> print("This is the number of trainable weights " "after freezing the conv base:", len(conv_base.trainable_weights)) This is the number of trainable weights after freezing the conv base: 0
现在我们可以创建一个新模型,将
- 一个数据增强阶段
- 我们冻结的卷积基础
- 一个密集分类器
列表 8.25 向卷积基添加数据增强阶段和分类器
data_augmentation = keras.Sequential( [ layers.RandomFlip("horizontal"), layers.RandomRotation(0.1), layers.RandomZoom(0.2), ] ) inputs = keras.Input(shape=(180, 180, 3)) x = data_augmentation(inputs) # ❶ x = keras.applications.vgg16.preprocess_input(x) # ❷ x = conv_base(x) x = layers.Flatten()(x) x = layers.Dense(256)(x) x = layers.Dropout(0.5)(x) outputs = layers.Dense(1, activation="sigmoid")(x) model = keras.Model(inputs, outputs) model.compile(loss="binary_crossentropy", optimizer="rmsprop", metrics=["accuracy"])
❶ 应用数据增强。
❷ 应用输入值缩放。
使用这种设置,只有我们添加的两个Dense
层的权重将被训练。总共有四个权重张量:每层两个(主要权重矩阵和偏置向量)。请注意,为了使这些更改生效,您必须首先编译模型。如果在编译后修改权重的可训练性,那么您应该重新编译模型,否则这些更改将被忽略。
让我们训练我们的模型。由于数据增强,模型开始过拟合的时间会更长,所以我们可以训练更多的 epochs——让我们做 50 个。
注意 这种技术足够昂贵,只有在您可以访问 GPU(例如 Colab 中提供的免费 GPU)时才应尝试——在 CPU 上无法实现。如果无法在 GPU 上运行代码,则应采用前一种技术。
callbacks = [ keras.callbacks.ModelCheckpoint( filepath="feature_extraction_with_data_augmentation.keras", save_best_only=True, monitor="val_loss") ] history = model.fit( train_dataset, epochs=50, validation_data=validation_dataset, callbacks=callbacks)
让我们再次绘制结果(参见图 8.14)。正如您所看到的,我们达到了超过 98%的验证准确率。这是对先前模型的一个强大改进。
图 8.14 使用数据增强进行特征提取的训练和验证指标
让我们检查测试准确率。
列表 8.26 在测试集上评估模型
test_model = keras.models.load_model( "feature_extraction_with_data_augmentation.keras") test_loss, test_acc = test_model.evaluate(test_dataset) print(f"Test accuracy: {test_acc:.3f}")
我们得到了 97.5%的测试准确率。与先前的测试准确率相比,这只是一个适度的改进,考虑到验证数据的强大结果,有点令人失望。模型的准确性始终取决于您评估的样本集!某些样本集可能比其他样本集更难,对一个样本集的强大结果不一定会完全转化为所有其他样本集。
8.3.2 微调预训练模型
用于模型重用的另一种广泛使用的技术,与特征提取相辅相成,即微调(参见图 8.15)。微调包括解冻用于特征提取的冻结模型基础的顶部几层,并同时训练模型的这部分新添加部分(在本例中是全连接分类器)和这些顶部层。这被称为微调,因为它略微调整了被重用模型的更抽象的表示,以使它们对手头的问题更相关。
图 8.15 微调 VGG16 网络的最后一个卷积块
我之前说过,为了能够在顶部训练一个随机初始化的分类器,需要冻结 VGG16 的卷积基。出于同样的原因,只有在顶部的分类器已经训练好后,才能微调卷积基的顶层。如果分类器尚未训练好,那么在训练过程中通过网络传播的误差信号将会太大,并且之前由微调层学到的表示将被破坏。因此,微调网络的步骤如下:
- 在已经训练好的基础网络上添加我们的自定义网络。
- 冻结基础网络。
- 训练我们添加的部分。
- 解冻基础网络中的一些层。(请注意,不应解冻“批量归一化”层,在这里不相关,因为 VGG16 中没有这样的层。有关批量归一化及其对微调的影响,将在下一章中解释。)
- 同时训练这两个层和我们添加的部分。
在进行特征提取时,您已经完成了前三个步骤。让我们继续进行第四步:我们将解冻我们的conv_base
,然后冻结其中的各个层。
作为提醒,这是我们的卷积基的样子:
>>> conv_base.summary() Model: "vgg16" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= input_19 (InputLayer) [(None, 180, 180, 3)] 0 _________________________________________________________________ block1_conv1 (Conv2D) (None, 180, 180, 64) 1792 _________________________________________________________________ block1_conv2 (Conv2D) (None, 180, 180, 64) 36928 _________________________________________________________________ block1_pool (MaxPooling2D) (None, 90, 90, 64) 0 _________________________________________________________________ block2_conv1 (Conv2D) (None, 90, 90, 128) 73856 _________________________________________________________________ block2_conv2 (Conv2D) (None, 90, 90, 128) 147584 _________________________________________________________________ block2_pool (MaxPooling2D) (None, 45, 45, 128) 0 _________________________________________________________________ block3_conv1 (Conv2D) (None, 45, 45, 256) 295168 _________________________________________________________________ block3_conv2 (Conv2D) (None, 45, 45, 256) 590080 _________________________________________________________________ block3_conv3 (Conv2D) (None, 45, 45, 256) 590080 _________________________________________________________________ block3_pool (MaxPooling2D) (None, 22, 22, 256) 0 _________________________________________________________________ block4_conv1 (Conv2D) (None, 22, 22, 512) 1180160 _________________________________________________________________ block4_conv2 (Conv2D) (None, 22, 22, 512) 2359808 _________________________________________________________________ block4_conv3 (Conv2D) (None, 22, 22, 512) 2359808 _________________________________________________________________ block4_pool (MaxPooling2D) (None, 11, 11, 512) 0 _________________________________________________________________ block5_conv1 (Conv2D) (None, 11, 11, 512) 2359808 _________________________________________________________________ block5_conv2 (Conv2D) (None, 11, 11, 512) 2359808 _________________________________________________________________ block5_conv3 (Conv2D) (None, 11, 11, 512) 2359808 _________________________________________________________________ block5_pool (MaxPooling2D) (None, 5, 5, 512) 0 ================================================================= Total params: 14,714,688 Trainable params: 14,714,688 Non-trainable params: 0 _________________________________________________________________
我们将微调最后三个卷积层,这意味着所有层直到block4_pool
应该被冻结,而层block5_conv1
、block5_conv2
和block5_conv3
应该是可训练的。
为什么不微调更多层?为什么不微调整个卷积基?你可以。但你需要考虑以下几点:
- 较早的卷积基层编码更通用、可重复使用的特征,而较高层编码更专业化的特征。对更专业化的特征进行微调更有用,因为这些特征需要在新问题上重新利用。微调较低层会有快速减少的回报。
- 您训练的参数越多,过拟合的风险就越大。卷积基有 1500 万个参数,因此在您的小数据集上尝试训练它是有风险的。
因此,在这种情况下,只微调卷积基的前两三层是一个好策略。让我们从前一个示例中结束的地方开始设置这个。
列表 8.27 冻结直到倒数第四层的所有层
conv_base.trainable = True for layer in conv_base.layers[:-4]: layer.trainable = False
现在我们可以开始微调模型了。我们将使用 RMSprop 优化器,使用非常低的学习率。使用低学习率的原因是我们希望限制对我们正在微调的三层表示所做修改的幅度。更新过大可能会损害这些表示。
列表 8.28 微调模型
model.compile(loss="binary_crossentropy", optimizer=keras.optimizers.RMSprop(learning_rate=1e-5), metrics=["accuracy"]) callbacks = [ keras.callbacks.ModelCheckpoint( filepath="fine_tuning.keras", save_best_only=True, monitor="val_loss") ] history = model.fit( train_dataset, epochs=30, validation_data=validation_dataset, callbacks=callbacks)
最终我们可以在测试数据上评估这个模型:
model = keras.models.load_model("fine_tuning.keras") test_loss, test_acc = model.evaluate(test_dataset) print(f"Test accuracy: {test_acc:.3f}")
在这里,我们获得了 98.5% 的测试准确率(再次强调,您自己的结果可能在一个百分点内)。在围绕这个数据集的原始 Kaggle 竞赛中,这将是顶尖结果之一。然而,这并不是一个公平的比较,因为我们使用了预训练特征,这些特征已经包含了关于猫和狗的先前知识,而竞争对手当时无法使用。
积极的一面是,通过利用现代深度学习技术,我们成功地仅使用了比比赛可用的训练数据的一小部分(约 10%)就达到了这个结果。在能够训练 20,000 个样本和 2,000 个样本之间存在巨大差异!
现在您已经掌握了一套处理图像分类问题的工具,特别是处理小数据集。
总结
- 卷积神经网络是计算机视觉任务中最好的机器学习模型类型。即使在一个非常小的数据集上,也可以从头开始训练一个,并取得不错的结果。
- 卷积神经网络通过学习一系列模块化的模式和概念来表示视觉世界。
- 在一个小数据集上,过拟合将是主要问题。数据增强是处理图像数据时对抗过拟合的强大方式。
- 通过特征提取,可以很容易地在新数据集上重用现有的卷积神经网络。这是处理小图像数据集的有价值的技术。
- 作为特征提取的补充,您可以使用微调,这会使现有模型先前学习的一些表示适应新问题。这会稍微提高性能。
¹ Karen Simonyan 和 Andrew Zisserman,“Very Deep Convolutional Networks for Large-Scale Image Recognition”,arXiv(2014),arxiv.org/abs/1409.1556
。