tf-dev-cert-gd-merge-1

42 阅读1小时+

TensorFlow 开发者认证指南(二)

原文:annas-archive.org/md5/a13e63613764fcdebedea75ef780532d

译者:飞龙

协议:CC BY-NC-SA 4.0

第五章:使用神经网络进行图像分类

到目前为止,我们已经成功地构建了用于解决结构化数据的回归和分类问题的模型。接下来的问题是:我们能否构建能够区分狗和猫,或者汽车和飞机的模型?如今,在TensorFlowPyTorch等框架的帮助下,开发人员可以仅用几行代码构建这样的机器学习解决方案。

在本章中,我们将探索神经网络的构造,并学习如何将它们应用于计算机视觉问题的模型构建。我们将首先了解什么是神经网络,以及多层神经网络的架构。我们还将探讨一些重要的概念,如前向传播、反向传播、优化器、损失函数、学习率和激活函数,以及它们在网络中的作用和位置。

在我们扎实掌握核心基础后,我们将使用 TensorFlow 中的自定义数据集构建图像分类器。在这里,我们将通过 TensorFlow 数据集的端到端过程来构建模型。使用这些自定义数据集的好处是,大部分预处理步骤已经完成,我们可以毫无障碍地对数据进行建模。因此,我们将使用这个数据集,在 TensorFlow 的Keras API 下通过几行代码构建一个神经网络,使我们的模型能够区分包和衬衫,鞋子和外套。

在本章中,我们将涵盖以下主题:

  • 神经网络的构造

  • 使用神经网络构建图像分类器

技术要求

我们将使用python >= 3.8.0,并配合以下可以通过pip install命令安装的包:

  • tensorflow>=2.7.0

  • tensorflow-datasets==4.4.0

  • pillow==8.4.0

  • pandas==1.3.4

  • numpy==1.21.4

  • matplotlib >=3.4.0

本章的代码可以在github.com/PacktPublishing/TensorFlow-Developer-Certificate-Guide/tree/main/Chapter%205找到。此外,所有练习的解答也可以在 GitHub 仓库中找到。

神经网络的构造

在本书的第一部分,我们讨论了模型。我们所讲解和使用的这些模型是神经网络。神经网络是一种深度学习算法,受到人脑功能的启发,但它并不完全像人脑那样运作。它通过分层的方法学习输入数据的有用表示,如图 5.1所示:

图 5.1 – 神经网络

图 5.1 – 神经网络

神经网络非常适合解决复杂问题,因为它们能够识别数据中非常复杂的模式。这使得它们特别适合围绕文本和图像数据(非结构化数据)构建解决方案,而这些是传统机器学习算法难以处理的任务。神经网络通过分层表示,开发规则将输入数据映射到目标或标签。当我们用标记数据训练它们时,它们学习模式,并利用这些知识将新的输入数据映射到相应的标签。

图 5.1中,我们看到输入层的所有神经元都与第一隐藏层的神经元相连接,第一隐藏层的所有神经元都与第二隐藏层的神经元相连接。从第二隐藏层到外层也同样如此。这种每一层的神经元都与下一层的神经元完全连接的网络,被称为全连接神经网络。拥有两个以上隐藏层的神经网络被称为深度神经网络DNN),网络的深度由其层数决定。

让我们深入探讨神经网络架构中的各个层:

  • 输入层:这是我们将输入数据(文本、图像、表格数据)输入网络的层。在这里,我们必须指定正确的输入形状,之前在我们的回归案例研究中,第三章*,TensorFlow 线性回归*,以及在我们的分类案例研究中,第四章*,TensorFlow 分类*中都已经做过这种操作。需要注意的是,输入数据将以数字格式呈现给我们的神经网络。在这一层,不会进行任何计算。这更像是一个将数据传递到隐藏层的通道层。

  • 隐藏层:这是下一个层,位于输入层和输出层之间。之所以称为隐藏层,是因为它对外部系统不可见。在这里,进行大量计算以从输入数据中提取模式。我们在隐藏层中添加的层数越多,我们的模型就会变得越复杂,处理数据所需的时间也越长。

  • 输出层:该层生成神经网络的输出。输出层神经元的数量由当前任务决定。如果我们有一个二分类任务,我们将使用一个输出神经元;而对于多类分类任务,例如我们的案例研究中有 10 个不同的标签,我们将有 10 个神经元,每个标签对应一个神经元。

我们现在知道神经网络的各层,但关键问题是:神经网络是如何工作的,它是如何在机器学习中占据特殊地位的?

神经网络通过前向传播和反向传播的结合解决复杂任务。我们先从前向传播开始。

前向传播

假设我们希望训练神经网络有效地识别图 5.2中的图像。我们将传递大量我们希望神经网络识别的图像代表性样本。这里的想法是,我们的神经网络将从这些样本中学习,并利用所学知识识别样本空间中的新项。比如,假设我们希望模型识别衬衫,我们将传递不同颜色和大小的衬衫。我们的模型将学习衬衫的定义,而不论其颜色、尺寸或样式如何。模型所学到的衬衫核心属性的表示将用于识别新衬衫。

图 5.2 – 来自 Fashion MNIST 数据集的示例图像

图 5.2 – 来自 Fashion MNIST 数据集的示例图像

让我们看一下幕后发生了什么。在我们的训练数据中,我们将图像(X)传入模型 f(x) . . → ˆ y,其中 ˆ y 是模型的预测输出。这里,神经网络随机初始化权重,用于预测输出(ˆ y)。这个过程被称为前向传播前向传递,如图 5.3所示。

注意

权重是可训练的参数,会在训练过程中进行更新。训练完成后,模型的权重会根据其训练数据集进行优化。如果我们在训练过程中适当地调整权重,就能开发出一个表现良好的模型。

当输入数据流经网络时,数据会受到节点的权重和偏置的影响而发生变换,如图 5.3所示,从而产生一组新的信息,这些信息将通过激活函数。如果希望得到新的学习信息,激活函数将触发一个输出信号,作为下一个层的输入。这一过程持续进行,直到在输出层生成输出:

图 5.3 – 神经网络的前向传播

图 5.3 – 神经网络的前向传播

让我们再谈谈激活函数及其在神经网络中的作用。

激活函数

想象一下,你需要从一篮苹果中挑选出好苹果。通过检查这些苹果,你可以挑出好苹果并丢掉坏苹果。这就像激活函数的作用——它充当了一个分隔器,定义了哪些信息会通过,在我们的例子中,这就是它学到的有用表示,并丢弃不必要的数据。从本质上讲,它帮助提取有用信息,就像挑选出好苹果一样,丢弃无用的数据,而在我们的场景中,坏苹果就是无用的数据。现在,激活函数决定了下一层哪个连接的神经元将被激活。它通过数学运算判断一个学习到的表示是否足够有用,能够供下一层使用。

激活函数可以为我们的神经网络添加非线性,这是神经网络学习复杂模式所必需的特性。激活函数有多种选择;对于输出层,激活函数的选择取决于手头任务的类型:

  • 对于二分类问题,我们通常使用 sigmoid 函数,因为它将输入映射到介于 0 和 1 之间的输出值,表示属于某个特定类别的概率。我们通常将阈值设置为 0.5,因此大于此点的值设置为 1,小于此点的值设置为 0。

  • 对于多分类问题,我们使用 Softmax 激活 作为输出层的激活函数。假设我们想要构建一个图像分类器,将四种水果(苹果、葡萄、芒果和橙子)进行分类,如图 5.4 所示。每种水果在输出层分配一个神经元,并且我们会应用 Softmax 激活函数来生成输出属于我们希望预测的水果之一的概率。当我们将苹果、葡萄、芒果和橙子的概率加起来时,结果为 1。对于分类任务,我们选择概率最大的水果类别作为从 Softmax 激活函数生成的概率中的输出标签。在这种情况下,概率最大的输出是橙子:

图 5.4 – SoftMax 激活函数的应用

图 5.4 – SoftMax 激活函数的应用

对于隐藏层,我们将使用 修正线性单元ReLU)激活函数。这个激活函数去除了负值(无用的表示),同时保留了大于 0 的学习表示。ReLU 在隐藏层表现出色,因为它收敛快速并且支持反向传播,这是我们接下来将要讨论的概念。

注意

在二分类问题中,使用 sigmoid 函数更加高效,这时我们只有一个输出神经元,而使用 Softmax 时会有两个输出神经元。此外,当我们阅读代码时,更容易理解我们处理的是二分类问题。

反向传播

当我们开始训练模型时,权重最初是随机的,这使得模型更容易错误地猜测图 5.4 中的水果是橙子。此时神经网络的智能就体现出来了;它会自动修正自己,如图 5.5 所示:

图 5.5 – 神经网络的前向传播与反向传播

图 5.5 – 神经网络的前向传播与反向传播

在这里,神经网络衡量预测输出(ˆy)与真实值(y)的比较结果的准确性。这个损失是通过损失函数计算的,损失函数也可以称为代价函数。这些信息会传递给优化器,其任务是更新神经网络中各层的权重,目的是在接下来的迭代中减少损失,从而使我们的预测更接近真实值。这个过程会持续,直到我们实现收敛。收敛发生在模型训练过程中,损失达到了最小值。

损失函数的应用依赖于当前任务。当我们处理二分类任务时,我们使用二元交叉熵;对于多分类任务,如果目标标签是整数值(例如,0 到 9),我们使用稀疏分类交叉熵,而如果我们决定对目标标签进行独热编码,则使用分类交叉熵。与损失函数类似,我们也有不同类型的优化器;然而,我们将尝试使用随机梯度下降法SGD)和Adam 优化器,它是 SGD 的改进版。因此,我们将使用它作为我们的默认优化器。

学习率

我们现在知道权重是随机初始化的,而优化器的目的是利用关于损失函数的信息来更新权重,从而实现收敛。神经网络使用优化器迭代更新权重,直到损失函数达到最小值,如图 5.6所示。优化器允许你设置一个重要的超参数——学习率,它控制收敛的速度,并且是我们模型学习的方式。为了到达斜率的底部,我们必须朝着底部迈出步伐(见图 5.6):

图 5.6 – 梯度下降

图 5.6 – 梯度下降

我们采取的步伐大小将决定我们到达底部的速度。如果我们走得步伐太小,将需要很长时间才能到达底部,且会导致收敛变慢,甚至存在优化过程可能在到达最小值的过程中卡住的风险。反之,如果步伐太大,则可能会错过最小值,并出现不稳定和异常的训练行为。正确的步伐大小将帮助我们及时到达斜率的底部而不会错过最小点。这里提到的步伐大小就是学习率。

我们现在已经高层次地了解了神经网络的直觉。接下来,让我们进行案例研究,直接应用我们刚刚学到的内容。

用神经网络构建图像分类器

我们回到了虚构的公司,现在我们希望利用神经网络的直觉来构建一个图像分类器。在这里,我们要教计算机识别服装。幸运的是,我们不需要在野外寻找数据;我们有 TensorFlow 数据集,其中包括时尚数据集。在我们的案例研究中,我们的目标是将一个由 28 x 28 灰度图像组成的时尚数据集分类为 10 类(从 0 到 9),每个像素值介于 0 到 255 之间,使用一个广为人知的数据集——Fashion MNIST 数据集。该数据集由 60,000 张训练图像和 10,000 张测试图像组成。我们的数据集中的所有图像都是相同的形状,因此我们几乎不需要做什么预处理。这里的想法是,我们可以快速构建一个神经网络,而不需要复杂的预处理。

为了训练神经网络,我们将传递训练图像,假设我们的神经网络将学习将图像(X)映射到它们相应的标签(y)。在完成训练过程后,我们将使用测试集对模型在新图像上的表现进行评估。同样,目的是让模型根据它在训练过程中学到的知识,正确识别测试图像。让我们开始吧。

加载数据

在这里,我们将首先学习如何使用 TensorFlow 数据集处理图像。在 第七章*,*卷积神经网络进行图像分类,我们将处理需要更多建模工作以使用的真实世界图像;不过,它将基于我们在这里学到的内容。话虽如此,让我们看看如何从 TensorFlow 加载自定义数据集:

  1. 在加载数据之前,我们需要加载必要的库。我们在这里做这件事:

    import tensorflow as tf
    
    from tensorflow import keras
    
    import pandas as pd
    
    import random
    
    import numpy as np
    
    import matplotlib.pyplot as plt #helper libraries
    
    from tensorflow.keras.utils import plot_model
    
  2. 接下来,我们从 TensorFlow 导入 fashion_mnist 数据集,并使用 load_data() 方法创建我们的训练集和测试集:

    #Lets import the fashion mnist
    
    fashion_data = keras.datasets.fashion_mnist
    
    #Lets create of numpy array of training and testing data
    
    (train_images, train_labels), (test_images,
    
        test_labels) = fashion_data.load_data()
    

    如果一切按计划进行,我们应该会得到如 图 5.7 所示的输出:

图 5.7 – 从 TensorFlow 数据集中导入数据

图 5.7 – 从 TensorFlow 数据集中导入数据

  1. 现在,我们不再使用数字标签,而是创建与数据匹配的标签,这样我们可以把一件衣服叫做“衣服”,而不是叫它编号 3。我们将通过创建一个标签列表,并将其映射到相应的数字值来实现这一点:

    #We create a list of the categories
    
    class_names=['Top', 'Trouser','Pullover', 'Dress', 'Coat', 
    
        'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankleboot']
    

现在我们有了数据,让我们探索数据,看看能发现什么。与其盲目接受所说的每一件事,不如探索数据以验证大小、形状和数据分布。

执行探索性数据分析

加载完数据后,下一步是检查数据,了解数据的基本情况。当然,在这个实例中,我们已经从 TensorFlow 获得了一些关于数据分布的基本信息。同时,我们的数据已经以训练集和测试集的形式准备好了。然而,让我们通过代码确认所有细节,并查看我们目标标签的类别分布:

  1. 我们将使用matplotlib库生成索引为i的图像样本,其中i在 60,000 个训练样本中:

    # Display a sample image from the training data (index 7)
    
    plt.imshow(train_images[7])
    
    plt.grid(False)
    
    plt.axis('off')
    
    plt.show()
    

    我们使用索引7运行代码,返回了如图 5.7所示的上衣:

图 5.8 – Fashion MNIST 数据集索引为 7 的一件套头衫照片

图 5.8 – Fashion MNIST 数据集索引为 7 的一件套头衫照片

我们可以切换索引值,查看数据集中其他的服装;不过,这并不是我们这里的目标。所以,让我们继续进行探索性数据分析。

  1. 让我们来看一下我们数据的样本:

    #Lets check the shape of our training images and testing images
    
    train_images.shape, test_images.shape
    

    如预期的那样,我们可以看到训练图像由 60,000 张 28 x 28 的图像组成,而测试图像有 10,000 张,分辨率为 28 x 28:

    ((60000, 28, 28), (10000, 28, 28))
    
  2. 接下来,让我们检查数据的分布情况。最好先了解数据的分布情况,以确保每个我们希望训练模型的服装类别都有足够的样本。让我们在这里进行检查:

    df=pd.DataFrame(np.unique(train_labels,
    
        return_counts=True)).T
    
    dict = {0: ‹Label›,1: ‹Count›}
    
    df.rename(columns=dict,
    
        inplace=True)
    
    df
    

    这将返回如图 5.9所示的DataFrame。我们可以看到所有标签的样本数是相同的:

图 5.9 – 显示标签及其计数的 DataFrame

图 5.9 – 显示标签及其计数的 DataFrame

当然,这种类型的数据更可能出现在受控环境下,比如学术界。

  1. 让我们在这里可视化一些训练数据中的样本图像。让我们来看 16 个来自训练数据的样本:

    plt.figure(figsize=(9,9))
    
    for i in range(16):
    
        plt.subplot(4,4,i+1)
    
        plt.xticks([])
    
        plt.yticks([])
    
        plt.grid(False)
    
        plt.imshow(train_images[i])
    
        plt.title(class_names[train_labels[i]])
    
    plt.show()
    
  2. 当我们运行代码时,我们得到了图 5.10中的图像:

图 5.10 – 从 Fashion MNIST 数据集中随机选取的 16 张图像

图 5.10 – 从 Fashion MNIST 数据集中随机选取的 16 张图像

现在我们已经确认了数据大小、数据分布和形状,并查看了一些样本图像和标签。在我们开始构建和训练图像分类器之前,回顾一下我们的数据由灰度图像组成,值范围从 0 到 255。为了对数据进行归一化并提升模型在训练过程中的表现,我们需要对数据进行归一化处理。我们可以通过简单地将训练数据和测试数据除以 255 来实现这一点:

#it's important that the training and testing set are preprocessed in the same way.
train_images=train_images/255.0
test_images=test_images/255.0

现在我们已经对数据进行了归一化处理,接下来就可以进行建模了。让我们继续构建图像分类器。

构建模型

让我们将到目前为止在本章中学到的所有知识付诸实践:

#Step 1:  Model configuration
model=keras.Sequential([
    keras.layers.Flatten(input_shape=(28,28)),
    keras.layers.Dense(64, activation="relu"),
    keras.layers.Dense(10,activation="softMax")
])
#Here we flatten the data

我们用来构建模型的代码与本书第一部分中使用的代码类似。我们首先使用 Sequential API 创建一个顺序模型,以定义我们想要按顺序连接的层数。如果你是一个细心的观察者,你会注意到我们的第一层是一个展平层。这个层用于将图像数据展平为一个 1D 数组,然后传递给隐藏层。输入层没有神经元,它充当数据预处理层,将数据展平为 1D 数组后传递给隐藏层。

接下来,我们有一个 64 个神经元的隐藏层,并对该隐藏层应用 ReLU 激活函数。最后,我们有一个包含 10 个神经元的输出层——每个输出一个神经元。由于我们处理的是多类分类问题,因此使用 softmax 函数。Softmax 返回的是所有类别的概率结果。如果你还记得激活函数部分,输出概率的总和为 1,概率值最大的输出就是预测标签。

现在,我们完成了模型构建,接下来继续编译模型。

编译模型

下一步是编译模型。我们将使用compile方法来完成这个操作。在这里,我们传入我们希望使用的优化器;在这种情况下,我们使用Adam,它是我们的默认优化器。我们还指定了损失函数和评估指标。由于我们的标签是数字值,因此我们使用稀疏分类交叉熵作为损失函数。对于评估指标,我们使用准确率,因为我们的数据集是平衡的。准确率指标将真实反映我们模型的性能:

#Step 2: Compiling the model, we add the loss, optimizer and evaluation metrics here
model.compile(optimizer='adam',
    loss=›sparse_categorical_crossentropy',
    metrics=[‹accuracy›])

在我们开始拟合模型之前,先来看一下几种可视化模型及其参数的方法。

模型可视化

为了可视化我们的模型,我们使用summary()方法。这将为我们提供一个详细的视觉表示,展示模型的架构、各层、参数数量(可训练和不可训练)以及输出形状:

model.summary()

当我们运行代码时,它将返回模型的详细信息,如图 5.11所示:

图 5.11 – 模型摘要

图 5.11 – 模型摘要

图 5.11中,我们可以看到输入层没有参数,但输出形状为 784,这是将 28 × 28 的图像展平为一维数组的结果。要计算全连接层的参数数量,它是 784 × 64 + 64 = 50240(回想一下,X是输入数据,w是权重,b是偏置)。输出层(dense_1)的形状为 10,其中每个神经元代表一个类别,共有 650 个参数。回想一下,一个层的输出作为下一个层的输入。因此,64 × 10 + 10 = 650,其中 64 是隐藏层的输出形状,也是输出层的输入形状。

另一方面,我们还可以通过以下代码将模型显示为流程图,如图 5.12所示:

plot_model(model, to_file='model_plot.png', show_shapes=True, 
    show_layer_names=True)

图 5.12 – 模型流程图

图 5.12 – 模型流程图

这也让我们对模型的结构有了一个大致了解。我们生成的图表将保存为文件名model_plot.png。在这里,我们将show_shapes设置为true;这将在图中显示每一层的输出形状。我们还将show_layer_name设置为true,以在图中显示各层的名称,正如图 5.12所示。

接下来,让我们将模型拟合到训练数据中。

模型拟合

到现在为止,你应该已经熟悉这个过程。通过一行代码,我们可以使用fit方法来拟合我们的训练图像(X)和训练标签(y):

#Step 3: We fit our data to the model
 history= model.fit(train_images, train_labels, epochs=5)

在这里,我们将数据训练了五个 epoch。我们的模型返回了损失和准确率:

1875/1875 [==============================] – 4s 2ms/step – loss: 0.5206 – accuracy: 0.8183
Epoch 2/5
1875/1875 [==============================] – 4s 2ms/step – loss: 0.3937 – accuracy: 0.8586
Epoch 3/5
1875/1875 [==============================] – 4s 2ms/step – loss: 0.3540 – accuracy: 0.8722
Epoch 4/5
1875/1875 [==============================] – 4s 2ms/step – loss: 0.3301 – accuracy: 0.8790
Epoch 5/5
1875/1875 [==============================] – 4s 2ms/step – loss: 0.3131 – accuracy: 0.8850

我们可以看到,在仅仅五个 epoch 后,我们的模型达到了0.8850的准确率。考虑到我们只训练了非常少的 epoch,这是一个不错的开始。接下来,让我们通过绘制损失和准确率图来观察模型在训练过程中的表现。

训练监控

我们在拟合训练数据时返回一个history对象。在这里,我们使用history对象来创建损失和准确率曲线。以下是绘制图表的代码:

# Plot history for accuracy
plt.plot(history.history['accuracy'])
plt.title('model accuracy')
plt.ylabel('accuracy')
plt.xlabel('epoch')
plt.legend(['Train'], loc='lower right')
plt.show()
# Plot history for loss
plt.plot(history.history['loss'])
plt.title('model loss')
plt.ylabel('loss')
plt.xlabel('epoch')
plt.legend(['Train'], loc='upper right')
plt.show()

当我们运行代码时,我们得到两个图表,如图 5.13所示。我们可以看到,在第五个 epoch 结束时,训练准确率仍在上升,而损失仍在下降,尽管随着接近 0,下降的速度不再那么快:

图 5.13 – 准确率和损失图

图 5.13 – 准确率和损失图

或许如果我们训练更长时间,可能会看到更好的表现。在下一章中,我们将探讨如果我们延长训练时间会发生什么,并且还会查看其他提高模型表现的方法。在这里,目标是理解图表的含义,并获取足够的信息来指导我们接下来的行动。让我们在测试集上评估我们的模型。

评估模型

我们在测试集上评估我们模型的整体表现如下:

test_loss,test_acc =model.evaluate(test_images,test_labels)
print('Test Accuracy: ', test_acc)

我们在测试集上得到了0.8567的准确率。训练准确率和测试准确率之间的差异是机器学习中常见的问题,我们称之为过拟合。过拟合是机器学习中的一个关键问题,我们将在第八章中探讨过拟合及其处理方法,过拟合处理

接下来,让我们用我们训练过的神经网络做一些预测。

模型预测

要对模型进行预测,我们在测试集的未见数据上使用model.predict()方法。让我们看看模型在测试数据的第一个实例上的预测:

predictions=model.predict(test_images)
predictions[0].round(2)

当我们运行代码时,我们得到一个概率数组:

array([0.  , 0.  , 0.  , 0.  , 0.  ,
    0.13, 0.  , 0.16, 0.  , 0.7 ],
    dtype=float32)

如果我们检查概率,会发现在第九个元素的概率最高。因此,这个标签的概率为 70%。我们将使用np.argmax来提取标签,并将其与索引为0的测试标签进行比较:

np.argmax(predictions[0]),test_labels[0]

我们看到预测标签和测试标签的值都是9。我们的模型正确预测了这一点。接下来,让我们绘制 16 张随机图片,并将预测结果与真实标签进行比较。这次,我们不会返回标签的数值,而是返回标签本身,以便更清晰地展示:

# Let us plot 16 random images and compare the labels with the model's prediction
figure = plt.figure(figsize=(9, 9))
for i, index in enumerate(np.random.choice(test_images.shape[0],
size=16, replace=False)):
    ax = figure.add_subplot(4,4,i + 1,xticks=[], yticks=[])
    # Display each image
    ax.imshow(np.squeeze(test_images[index]))
    predict_index = np.argmax(predictions[index])
    true_index = test_labels[index]
    # Set the title for each image
    ax.set_title(f»{class_names[predict_index]} (
    {class_names[true_index]})",color=(
        "green" if predict_index == true_index else «red»))

结果如图 5.14所示。尽管模型能够正确分类 10 个项目,但它在一个样本上失败了,将一件衬衫误分类为套头衫:

图 5.14 – 可视化模型在测试数据上的预测

图 5.14 – 可视化模型在测试数据上的预测

仅用几行代码,我们就训练了一个图像分类器。在五个训练周期内,我们在训练数据上的准确率达到了 88.50%,在测试数据上的准确率为 85.67%。需要注意的是,这是一个用于学习的玩具数据集,尽管它非常适合学习,但实际世界中的图像更为复杂,训练将需要更长时间,且在许多情况下,需要更复杂的模型架构。

在这一章中,我们介绍了许多新概念,这些概念在后续章节以及考试中都会非常有用。

总结

在这一章中,我们讨论了图像分类建模。现在,你应该能够解释什么是神经网络,以及前向传播和反向传播的原理。你应该了解损失函数、激活函数和优化器在神经网络中的作用。此外,你应该能够熟练加载 TensorFlow 数据集中的数据。最后,你应该了解如何构建、编译、拟合和训练一个用于图像分类的神经网络,并评估模型,绘制损失和准确率曲线,解读这些可视化结果。

在下一章中,我们将探讨几种方法,用于提高我们模型的性能。

问题

让我们来测试一下我们在这一章中学到的内容:

  1. 激活函数的作用是什么?

  2. 反向传播是如何工作的?

  3. 输入层、隐藏层和输出层的作用是什么?

  4. 使用 TensorFlow 数据集,加载一个手写数字数据集,然后你将构建、编译、训练并评估一个图像分类器。这与我们的案例研究类似。加油!

进一步阅读

若要了解更多信息,你可以查看以下资源:

  • Amr, T., 2020. 深入学习与 scikit-learn 和科学 Python 工具包的实践。 [S.l.]: Packt Publishing.

  • Vasilev, I., 2019. Python 深度学习进阶。第 1 版。Packt Publishing.

  • Raschka, S. 和 Mirjalili, V., 2019. Python 机器学习。第 3 版。Packt Publishing.

  • Gulli, A., Kapoor, A. 和 Pal, S., 2019. 使用 TensorFlow 2 和 Keras 的深度学习。伯明翰:Packt Publishing.

  • TensorFlow 指南 www.TensorFlow.org/guide

第六章:改进模型

机器学习中建模的目标是确保我们的模型能在未见过的数据上良好泛化。在我们作为数据专业人员构建神经网络模型的过程中,可能会遇到两个主要问题:欠拟合和过拟合。欠拟合是指我们的模型缺乏足够的复杂性,无法捕捉数据中的潜在模式,而过拟合则是在模型过于复杂时,模型不仅学习到模式,还会拾取到训练数据中的噪声和异常值。在这种情况下,我们的模型在训练数据上表现非常好,但在未见过的数据上无法良好泛化。第五章使用神经网络进行图像分类,探讨了神经网络背后的科学原理。在本章中,我们将探索调整神经网络的艺术,以构建在图像分类中表现最优的模型。我们将通过动手实践来探索各种网络设置,了解这些设置(超参数)对模型性能的影响。

除了探索超参数调整的艺术,我们还将探索多种改善数据质量的方法,如数据标准化、数据增强和使用合成数据来提高模型的泛化能力。过去,人们非常注重构建复杂的网络。然而,近年来,越来越多的人开始关注使用以数据为中心的策略来提升神经网络的表现。使用这些以数据为中心的策略并不会削弱精心设计模型的必要性;相反,我们可以将它们看作是互为补充的策略,协同工作以达成目标,从而增强我们构建具有良好泛化能力的最优模型的能力。

在本章中,我们将涵盖以下主题:

  • 数据至关重要

  • 调整神经网络的超参数

到本章结束时,你将能够有效应对过拟合和欠拟合所带来的挑战,通过结合以模型为中心和以数据为中心的思路,构建神经网络模型。

技术要求

我们将使用python >= 3.8.0,并安装以下可以通过pip install命令安装的包:

  • tensorflow>=2.7.0

  • tensorflow-datasets==4.4.0

  • pandas==1.3.4

  • numpy==1.21.4

数据至关重要

在提升神经网络或任何其他机器学习模型的性能时,良好的数据准备工作至关重要,不能过分强调。在第三章使用 TensorFlow 进行线性回归中,我们看到了标准化数据对模型表现的影响。除了数据标准化之外,还有其他数据准备技巧可以在建模过程中产生影响。

如你现在应该已经意识到的,机器学习需要根据手头的问题进行调查、实验和应用不同的技术。为了确保我们拥有最佳性能的模型,我们的旅程应从彻底审视数据开始。我们是否拥有每个目标类别的足够代表性样本?我们的数据是否平衡?我们是否确保了标签的正确性?我们的数据类型是否正确?我们如何处理缺失数据?这些问题是我们在建模阶段之前必须提问并处理的。

提高我们数据的质量是一项多方面的工作,涉及通过应用数据预处理技术(如数据归一化)从现有数据中工程化新特征。当我们处理不平衡数据集时,尤其是当我们缺乏少数类的代表性样本时,合理的做法是收集更多少数类数据;然而,这在所有情况下并不实际。在这种情况下,合成数据可能是一个有效的替代方案。一些初创公司,如Anyverse.aiDatagen.tech,专注于合成数据的开发,从而可以缓解数据不平衡和数据稀缺的问题。然而,合成数据可能会很昂贵,因此在选择这条路线之前,我们需要做一个成本效益分析。

我们可能面临的另一个问题是,当我们收集的样本不足以代表模型正确运行时。例如,你训练模型识别人脸。你收集了成千上万的人脸图像,并将数据分成训练集和测试集。你训练了模型,并且在测试集上预测得非常完美。然而,当你将这个模型作为产品推向市场时,你得到的结果可能像图 6.1所示:

图 6.1 – 数据增强的必要性

图 6.1 – 数据增强的必要性

令人惊讶,对吧?即使你在成千上万的图像上训练了模型,如果轴被垂直或水平翻转,或者以其他方式改变,模型也未能学会识别面孔。为了解决这种问题,我们采用了一种叫做数据增强的技术。数据增强是一种通过某种方式改变现有数据来创建新训练数据的技术,例如随机裁剪、缩放或旋转、翻转初始图像。数据增强背后的基本思想是使我们的模型即使在不可预测的条件下(例如我们在图 6.1 中看到的情况),也能识别图像中的物体。

数据增强在我们希望从有限的训练集获得更多数据样本时非常有用;我们可以使用数据增强来有效地增加数据集的大小,从而为我们的模型提供更多的数据进行学习。此外,由于我们可以模拟各种场景,模型在学习数据中的潜在模式时不太可能发生过拟合,而不是学习数据中的噪声,因为我们的模型通过多种方式学习数据。数据增强的另一个重要好处是它是一种节省成本的技术,可以避免昂贵且有时耗时的数据收集过程。在第八章《处理过拟合》中,我们将应用数据增强技术,在实际的图像分类问题中进行实践。此外,如果将来你需要处理图像或文本数据,数据增强将是一个非常实用的技术。

除了解决数据不平衡和数据多样性问题外,我们可能还希望进一步优化我们的模型,使其足够复杂,以便识别数据中的模式,并且我们希望做到这一点而不导致模型过拟合。在这里,目标是通过调整一个或多个设置来提高模型的质量,例如增加隐藏层的数量、为每层添加更多神经元、改变优化器或使用更复杂的激活函数。这些设置可以通过实验进行调优,直到获得最优的模型。

我们已经讨论了若干提高神经网络性能的思路。现在,让我们看看如何提高在第五章《使用神经网络进行图像分类》中,在 Fashion MNIST 数据集上取得的结果。

神经网络的超参数微调

在进行机器学习改进之前,建立一个基线模型非常重要。基线模型是一个简单的模型,我们可以用它来评估更复杂模型的表现。在第五章,《使用神经网络进行图像分类》中,我们在仅五个训练周期内,训练数据的准确率为 88.50%,测试数据的准确率为 85.67%。为了进一步提高我们模型的表现,我们将继续按照三步流程(构建编译、和训练)来构建神经网络,使用TensorFlow。在构建神经网络的每个步骤中,都有一些需要在训练前配置的设置,这些设置称为超参数。超参数决定了网络如何学习和表现,掌握调整超参数的技巧是构建成功深度学习模型的关键步骤。常见的超参数包括每层神经元的数量、隐藏层的数量、学习率、激活函数以及训练周期数。通过不断尝试这些超参数,我们可以找到最适合我们使用场景的最佳设置。

在构建现实世界的模型时,特别是处理特定领域问题时,专家知识可能会非常有助于定位任务的最佳超参数值。让我们回到笔记本,尝试不同的超参数,看看通过调整一个或多个超参数是否能够超越我们的基线模型。

增加训练周期数

想象一下,你正在教一个孩子乘法表;你与孩子每次的学习互动可以比作机器学习中的一个训练周期。如果你与孩子的学习次数很少,那么他们很可能无法完全理解乘法的概念。因此,孩子将无法尝试基本的乘法问题。在机器学习中,这种情况叫做欠拟合,指的是模型由于训练不足,未能捕捉到数据中的潜在模式。

另一方面,假设你花了很多时间教孩子记住乘法表的某些方面,比如 2 的倍数、3 的倍数和 4 的倍数。孩子在背诵这些乘法表时变得熟练;然而,当遇到类似 10 x 8 这样的乘法题时,孩子却感到困难。这是因为,孩子并没有理解乘法的原理,以至于能够在处理其他数字时应用这个基本的思想,而只是单纯地记住了学习过程中遇到的例子。在机器学习中,这种情况就像过拟合的概念,我们的模型在训练数据上表现良好,但在新情况中无法很好地泛化。在机器学习中,当我们训练模型时,需要找到一个平衡,使得模型足够好地学习数据中的潜在模式,而不是仅仅记住训练数据。

让我们看看延长训练时间对结果的影响。这次,我们选择 40 个训练轮次,观察会发生什么:

#Step 1:  Model configuration
model=keras.Sequential([
    keras.layers.Flatten(input_shape=(28,28)),
    keras.layers.Dense(100, activation=»relu»),
    keras.layers.Dense(10,activation=»softmax»)
])
#Step 2: Compiling the model, we add the loss, optimizer and evaluation metrics here
model.compile(optimizer='adam',
    loss=›sparse_categorical_crossentropy›,
    metrics=[‹accuracy›])
#Step 3: We fit our data to the model
history= model.fit(train_images, train_labels, epochs=40)

在这里,我们将步骤 3中的训练轮次从5改为40,同时保持我们基础模型的其他超参数不变。输出的最后五轮结果如下:

Epoch 36/40
1875/1875 [==============================] - 5s 2ms/step - loss: 0.1356 - accuracy: 0.9493
Epoch 37/40
1875/1875 [==============================] - 5s 2ms/step - loss: 0.1334 - accuracy: 0.9503
Epoch 38/40
1875/1875 [==============================] - 4s 2ms/step - loss: 0.1305 - accuracy: 0.9502
Epoch 39/40
1875/1875 [==============================] - 4s 2ms/step - loss: 0.1296 - accuracy: 0.9512
Epoch 40/40
1875/1875 [==============================] - 4s 2ms/step - loss: 0.1284 - accuracy: 0.9524

请注意,当我们增加训练轮次时,训练模型所需的时间会显著增加。所以,当训练轮次很大时,计算成本可能会变得很高。经过 40 个训练轮次后,我们发现模型的训练准确率达到了0.9524,看起来你可能认为已经找到了解决问题的灵丹妙药。然而,我们的目标是确保模型的泛化能力;因此,检验模型的关键是看它在未见过的数据上表现如何。让我们看看在测试数据上的结果如何:

test_loss, test_acc=model.evaluate(test_images,test_labels)
print('Test Accuracy: ', test_acc)

当我们运行代码时,我们在测试数据上的准确率为0.8692。可以看到,随着模型训练时间的增加,模型在训练数据上的准确率逐渐提高。然而,如果我们训练得太久,模型的效果会出现收益递减的现象,这在比较训练集和测试集准确度的表现差异时尤为明显。找到一个合适的训练轮次非常重要,以确保模型能够学习和提高,但又不会过度拟合训练数据。一种实用的方法是从较少的训练轮次开始,根据需要逐步增加训练轮次。虽然这种方法有效,但也可能比较耗时,因为需要进行多次实验来找到最优的训练轮次。

那么,如果我们能够设定一个规则,在收益递减前停止训练呢?是的,这是可能的。接下来我们来研究这个想法,看看它对结果会有什么影响。

使用回调函数进行早停

早停是一种正则化技术,可以用来防止神经网络在训练时出现过拟合。当我们将训练周期数硬编码到模型中时,我们无法在达到期望的度量标准时停止训练,或者当训练开始退化或不再改进时停止训练。我们在增加训练周期数时遇到了这个问题。然而,为了应对这种情况,TensorFlow 为我们提供了早停回调,使我们可以使用内置的回调函数,或者设计自定义的回调函数。我们可以实时监控实验,并且拥有更多的控制权,从而在模型开始过拟合、训练停止学习,或者符合其他定义的标准时,提前停止训练。早停可以在训练的不同阶段调用,可以在训练开始时、结束时或基于达到特定度量时应用。

使用内置回调实现早停

让我们一起探索 TensorFlow 中内置的早停回调:

  1. 我们将从 TensorFlow 导入早停功能:

    from tensorflow.keras.callbacks import EarlyStopping
    
  2. 接下来,我们初始化早停。TensorFlow 允许我们传入一些参数,我们利用这些参数来创建一个callbacks对象:

    callbacks = EarlyStopping(monitor='val_loss',
    
        patience=5, verbose=1, restore_best_weights=True)
    

让我们解读一下我们在早停函数中使用的一些参数:

  • monitor可以用来跟踪我们想要关注的指标;在我们的情况下,我们希望跟踪验证损失。我们也可以切换为跟踪验证准确率。建议在验证集上监控实验,因此我们将callbacks设置为监控验证损失。

  • patience参数设置为5。这意味着,如果在五个周期后验证损失没有任何进展,训练将结束。

  • 我们添加了restore_best_weight参数并将其设置为True。这使得回调可以监控整个过程,并恢复训练过程中找到的最佳训练周期的权重。如果我们将restore_best_weight设置为False,则使用最后一步训练的模型权重。

  • 当我们将verbose设置为1时,这确保了我们在回调操作发生时得到通知。如果我们将verbose设置为0,训练将停止,但我们不会收到任何输出消息。

这里还有一些其他参数可以使用,但这些参数在应用早停时对许多情况来说已经足够有效。

  1. 我们将继续采用我们的三步法:构建、编译和拟合模型:

    #Step 1:  Model configuration
    
    model=keras.Sequential([
    
        keras.layers.Flatten(input_shape=(28,28)),
    
        keras.layers.Dense(100, activation=»relu»),
    
        keras.layers.Dense(10,activation=»softmax»)
    
    ])
    
    #Step 2: Compiling the model, we add the loss, optimizer and evaluation metrics here
    
    model.compile(optimizer='adam',
    
        loss=›sparse_categorical_crossentropy›,
    
        metrics=[‹accuracy›])
    
    #Step 3: We fit our data to the model
    
    history= model.fit(train_images, train_labels,
    
        epochs=100, callbacks=[callbacks],
    
        validation_split=0.2)
    

第 1 步第 2 步是我们之前实现的相同步骤。在构建模型时,我们进行了更长时间的训练周期。然而,在第 3 步中,我们做了一些调整,以适应我们的验证集拆分和回调。我们将 20%的训练数据用于验证,并将callbacks对象传递给model.fit()。这确保了我们的早停回调在验证损失停止下降时中断训练。输出如下:

Epoch 14/100
1500/1500 [==============================] - 4s 2ms/step - loss: 0.2197 - accuracy: 0.9172 - val_loss: 0.3194 - val_accuracy: 0.8903
Epoch 15/100
1500/1500 [==============================] - 4s 2ms/step - loss: 0.2133 - accuracy: 0.9204 - val_loss: 0.3301 - val_accuracy: 0.8860
Epoch 16/100
1500/1500 [==============================] - 4s 2ms/step - loss: 0.2064 - accuracy: 0.9225 - val_loss: 0.3267 - val_accuracy: 0.8895
Epoch 17/100
1500/1500 [==============================] - 3s 2ms/step - loss: 0.2018 - accuracy: 0.9246 - val_loss: 0.3475 - val_accuracy: 0.8844
Epoch 18/100
1500/1500 [==============================] - 4s 2ms/step - loss: 0.1959 - accuracy: 0.9273 - val_loss: 0.3203 - val_accuracy: 0.8913
Epoch 19/100
1484/1500 [============================>.] - ETA: 0s - loss: 0.1925 - accuracy: 0.9282 Restoring model weights from the end of the best epoch: 14.
1500/1500 [==============================] - 4s 2ms/step - loss: 0.1928 - accuracy: 0.9281 - val_loss: 0.3347 - val_accuracy: 0.8912
Epoch 19: early stopping

因为我们将verbose设置为1,所以可以看到我们的实验在第 19 个 epoch 结束。现在,与其担心我们需要多少个 epoch 才能有效训练,我们可以简单地选择一个较大的 epoch 数并实现早停。接下来,我们还可以看到,因为我们实现了restore_best_weights,最佳权重出现在第 14 个 epoch,此时我们记录了最低的验证损失(0.3194)。通过早停,我们节省了计算时间,并采取了具体措施防止过拟合。

  1. 让我们看看我们的测试准确率如何:

    test_loss, test_acc = model.evaluate(test_images,
    
        test_labels)
    
    print('Test Accuracy: ', test_acc)
    

在这里,我们达到了0.8847的测试准确率。

现在,让我们看看如何编写自定义回调来实现早停。

使用自定义回调实现早停

我们可以通过编写自己的自定义回调来扩展回调的功能,从而实现早停。这为回调增加了灵活性,使我们能够在训练过程中实现一些期望的逻辑。TensorFlow 文档提供了几种实现方法。让我们实现一个简单的回调来跟踪我们的验证准确率:

class EarlyStop(tf.keras.callbacks.Callback):
    def on_epoch_end(self, epoch, logs={}):
        if(logs.get('val_accuracy') > 0.85):
            print("\n\n85% validation accuracy has been reached.")
            self.model.stop_training = True
callback = EarlyStop()

例如,如果我们希望在模型在验证集上超过 85%的准确率时停止训练,我们可以通过编写自己的自定义回调EarlyStop来实现,该回调接受tf.keras.callbacks.Callback参数。然后我们定义一个名为on_epoch_end的函数,该函数返回每个 epoch 的日志。我们设置self.model.stop_training = True,一旦准确率超过 85%,训练结束并显示类似于我们在使用内置回调时将verbose设置为1时所得到的消息。现在,我们可以像使用内置回调一样,将callback传入model.fit()中。然后我们使用我们三步法训练模型:

Epoch 1/100
1490/1500 [============================>.] - ETA: 0s - loss: 0.5325 - accuracy: 0.8134/n/n 85% validation accuracy has been reached
1500/1500 [==============================] - 4s 3ms/step - loss: 0.5318 - accuracy: 0.8138 - val_loss: 0.4190 - val_accuracy: 0.8538

这一次,在第一个 epoch 结束时,我们的验证准确率已经超过了 85%。再次强调,这是通过最小化计算资源的使用来实现期望指标的智能方法。

现在我们已经掌握了如何选择 epoch 并应用早停,让我们把目光转向其他超参数,看看通过调整一个或多个超参数,我们是否能提高 88%的测试准确率。也许我们可以从尝试一个更复杂的模型开始。

让我们看看如果我们向隐藏层添加更多神经元会发生什么。

增加隐藏层的神经元

隐藏层负责神经网络中的重负载,就像我们在讨论神经网络的结构时提到的那样,参见第五章,《使用神经网络进行图像分类》。让我们尝试不同数量的隐藏层神经元。我们将定义一个名为train_model的函数,允许我们尝试不同数量的神经元。train_model函数接受一个名为hidden_neurons的参数,表示模型中隐藏层神经元的数量。此外,该函数还接受训练图像、标签、回调、验证分割和 epoch 数。该函数使用这些参数构建、编译并拟合模型:

def train_model(hidden_neurons, train_images, train_labels, callbacks=None, validation_split=0.2, epochs=100):
    model = keras.Sequential([
        keras.layers.Flatten(input_shape=(28, 28)),
        keras.layers.Dense(hidden_neurons, activation=»relu»),
        keras.layers.Dense(10, activation=»softmax»)
    ])
    model.compile(optimizer=›adam›,
        loss=›sparse_categorical_crossentropy›,
        metrics=[‹accuracy›])
    history = model.fit(train_images, train_labels,
        epochs=epochs, callbacks=[callbacks] if callbacks else None,
        validation_split=validation_split)
    return model, history

为了尝试一组神经元,我们创建了一个for循环来遍历名为neuron_values的神经元列表。然后它应用train_model函数为列表中每个神经元构建并训练一个模型:

neuron_values = [1, 500]
for neuron in neuron_values:
    model, history = train_model(neurons, train_images,
        train_labels, callbacks=callbacks)
    print(f»Trained model with {neurons} neurons in the hidden layer»)

print语句返回一条消息,指示模型已分别使用 1 个和 500 个神经元进行了训练。让我们检查运行该函数时的结果,从只有一个神经元的隐藏层开始:

Epoch 36/40
1500/1500 [==============================] - 3s 2ms/step - loss: 1.2382 - accuracy: 0.4581 - val_loss: 1.2705 - val_accuracy: 0.4419
Epoch 37/40
1500/1500 [==============================] - 2s 1ms/step - loss: 1.2360 - accuracy: 0.4578 - val_loss: 1.2562 - val_accuracy: 0.4564
Epoch 38/40
1500/1500 [==============================] - 2s 1ms/step - loss: 1.2340 - accuracy: 0.4559 - val_loss: 1.2531 - val_accuracy: 0.4507
Epoch 39/40
1500/1500 [==============================] - 2s 1ms/step - loss: 1.2317 - accuracy: 0.4552 - val_loss: 1.2553 - val_accuracy: 0.4371
Epoch 40/40
1500/1500 [==============================] - 2s 1ms/step - loss: 1.2292 - accuracy: 0.4552 - val_loss: 1.2523 - val_accuracy: 0.4401
end of experiment with 1 neuron

从我们的结果来看,隐藏层只有一个神经元的模型不足够复杂,无法识别数据中的模式。这个模型的表现远低于 50%,这是典型的欠拟合案例。接下来,让我们看看使用 500 个神经元的模型结果:

Epoch 11/40
1500/1500 [==============================] - 6s 4ms/step - loss: 0.2141 - accuracy: 0.9186 - val_loss: 0.3278 - val_accuracy: 0.8878
Epoch 12/40
1500/1500 [==============================] - 6s 4ms/step - loss: 0.2057 - accuracy: 0.9220 - val_loss: 0.3169 - val_accuracy: 0.8913
Epoch 13/40
1500/1500 [==============================] - 6s 4ms/step - loss: 0.1976 - accuracy: 0.9258 - val_loss: 0.3355 - val_accuracy: 0.8860
Epoch 14/40
1500/1500 [==============================] - 6s 4ms/step - loss: 0.1893 - accuracy: 0.9288 - val_loss: 0.3216 - val_accuracy: 0.8909
Epoch 15/40
1499/1500 [============================>.] - ETA: 0s - loss: 0.1825 - accuracy: 0.9303Restoring model weights from the end of the best epoch: 10.
1500/1500 [==============================] - 6s 4ms/step - loss: 0.1826 - accuracy: 0.9303 - val_loss: 0.3408 - val_accuracy: 0.8838
Epoch 15: early stopping
end of experiment with 500 neurons

我们可以看到模型在使用更多神经元时出现了过拟合。模型在训练集上的准确率为0.9303,但在测试集上的准确率为0.8838。通常来说,更大的隐藏层能够学习更复杂的模式;然而,它会需要更多的计算资源,并且更容易发生过拟合。在选择隐藏层神经元数量时,考虑训练数据的大小非常重要。如果我们有大量的训练样本,我们可以选择更大的神经元数量。但当训练样本较小时,可能需要考虑使用较少的神经元。正如我们在实验中看到的,更多的神经元可能导致过拟合,而这种架构的表现可能会比拥有较少神经元的模型还要差。

另一个需要考虑的因素是我们所使用的数据类型。当我们处理线性数据时,少量的隐藏层可能就足够了。然而,对于非线性数据,我们需要更复杂的模型来学习数据中的复杂性。最后,我们还必须牢记,拥有更多神经元的模型需要更长的训练时间。重要的是要考虑性能和泛化能力之间的权衡。通常的做法是,从少量神经元开始训练模型。这样训练速度更快,并能避免过拟合。

或者,我们可以通过识别对网络性能影响较小或没有影响的神经元,来优化隐藏层中的神经元数量。这种方法称为剪枝。这超出了考试的范围,所以我们到此为止。

让我们看看向基准架构中添加更多层的影响。到目前为止,我们已经考虑过使模型更加复杂并训练更长时间。那我们试试改变优化器呢?让我们稍微变换一下,看看会发生什么。

更改优化器

我们使用Adam 优化器作为默认优化器;然而,还有其他一些知名的优化器,它们各有优缺点。在本书中,以及为了你的考试,我们将重点讲解 Adam、随机梯度下降SGD)和均方根传播RMSprop)。RMSprop 具有较低的内存需求,并提供自适应学习率;但与 Adam 和 SGD 相比,它的收敛时间要长得多。RMSprop 在训练非常深的网络时表现良好,比如递归神经网络RNN),这将在本书后面讨论。

另一方面,SGD 是另一种流行的优化器;它简单易实现,并且当数据稀疏时效率较高。然而,它收敛较慢,并且需要仔细调整学习率。如果学习率过高,SGD 会发散;如果学习率过低,SGD 会收敛得非常慢。SGD 在各种问题上表现良好,并且在大数据集上比其他优化器收敛得更快,但在训练非常大的神经网络时,有时会收敛得较慢。

Adam 是 SGD 的改进版;它具有较低的内存需求,提供自适应学习率,是一种非常高效的优化器,并且可以比 SGD 或 RMSprop 在更少的迭代次数下收敛到一个良好的解。Adam 也非常适合训练大型神经网络。

让我们试试这三种优化器,看看哪一种在我们的数据集上效果最好。我们已经将优化器从 Adam 更改为 RMSprop 和 SGD,并使用相同的架构和内建回调。我们可以在图 6.2中看到结果:

AdamRMSPropSGD
早停前的训练轮数13939
验证准确率0.88670.87880.8836
测试准确率0.87870.87490.8749

图 6.2 – 不同优化器的性能

尽管 Adam 需要更多的训练轮次,但其结果略微优于其他优化器。当然,任何一种优化器都可以用于此问题。在后续章节中,我们将处理更复杂的真实世界图像,并会再次讨论这些优化器。

在我们结束本章之前,让我们来看一下学习率及其对模型性能的影响。

更改学习率

学习率是一个重要的超参数,它控制着我们的模型在训练过程中学习和改进的效果。一个合适的学习率将确保模型快速且准确地收敛,而一个选择不当的学习率则可能导致各种问题,如收敛缓慢、欠拟合、过拟合或网络不稳定。

要理解学习率的影响,我们需要了解它如何影响模型的训练过程。学习率是达到损失函数最小值所采取的步长。在 图 6*.3(a)* 中,我们看到选择较低学习率时,模型需要太多步骤才能达到最小点。另一方面,当学习率过高时,模型可能会学习得太快,采取较大步长,并可能超过最小点,就像 图 6*.3(c)* 中所示。高学习率可能导致不稳定性和过拟合。然而,当我们像 图 6*.3(b)* 中找到理想学习率时,模型很可能会快速收敛并具有良好的泛化能力:

图 6.3 – 展示低、理想和高学习率的绘图

图 6.3 – 展示低、理想和高学习率的绘图

提出的问题是:我们如何找到最佳的学习率?一种方法是尝试不同的学习率,并根据在验证集上评估模型的表现来确定有效的学习率。另一种方法是使用学习率调度器。这允许我们在训练过程中动态调整学习率。我们将在本书的后面章节探讨这种方法。在这里,让我们尝试几个不同的学习率,看看它们对我们的网络的影响。

让我们编写一个函数,该函数将接受一组不同的学习率。在这个实验中,我们将尝试六种不同的学习率(1、0.1、0.01、0.001、0.0001、0.00001 和 0.000001)。首先,让我们创建一个函数来创建我们的模型:

def learning_rate_test(learning_rate):
    #Step 1:  Model configuration
    model=keras.Sequential([
        keras.layers.Flatten(input_shape=(28,28)),
        keras.layers.Dense(64, activation=»relu»),
        keras.layers.Dense(10,activation=»softmax»)
])
    #Step 2: Compiling the model, we add the loss,
         #optimizer and evaluation metrics here
    model.compile(optimizer=tf.keras.optimizers.Adam(
        learning_rate=learning_rate),
        loss='sparse_categorical_crossentropy',
        metrics=[‹accuracy›])
    #Step 3: We fit our data to the model
    callbacks = EarlyStopping(monitor='val_loss',
        patience=5, verbose=1, restore_best_weights=True)
    history=model.fit(train_images, train_labels,
        epochs=50, validation_split=0.2,
        callbacks=[callbacks])
    score=model.evaluate(test_images, test_labels)
    return score[1]

我们将使用该函数来构建、编译和拟合模型。它还将学习率作为一个变量传递到我们的函数中,并将测试准确率作为结果返回:

# Try out different learning rates
learning_rates = [1, 0.1, 0.01, 0.001, 0.0001, 0.00001,
    0.000001]
# Create an empty list to store the accuracies
accuracies = []
# Loop through the different learning rates
for learning_rate in learning_rates:
    # Get the accuracy for the current learning rate
    accuracy = learning_rate_test(learning_rate)
    # Append the accuracy to the list
    accuracies.append(accuracy)

我们现在已经概述了不同的学习率。在这里,我们想要尝试不同的学习率,从非常高到非常低的学习率。我们创建了一个空列表,并附加了我们的测试集准确率。接下来,让我们以表格形式查看数值。我们使用 pandas 生成一个包含学习率和准确率的 DataFrame:

df = pd.DataFrame(list(zip(learning_rates, accuracies)),
    columns =[‹Learning_rates›, ‹Test_Accuracy›])
df

下面是输出的 DataFrame 截图:

图 6.4 – 不同学习率及其测试准确率

图 6.4 – 不同学习率及其测试准确率

从结果中我们可以看到,当使用非常高的学习率(1.0)时,模型表现很差。随着学习率值的降低,我们看到模型的准确性开始提高;当学习率变得太小时,模型收敛所需时间太长。在选择问题的理想学习率时,并没有银弹。这取决于诸多因素,如模型架构、数据以及应用的优化技术类型。

现在我们已经看到了各种调整模型以提高其性能的方法,本章也已经结束。我们尝试了调整不同的超参数来改善模型的性能;然而,我们的测试准确率停滞在 88%。或许现在是尝试其他方法的好时机,接下来我们将在下一章中进行尝试。休息一下,当你准备好时,让我们看看如何改善这个结果,并尝试使用真实世界的图像。

总结

在本章中,我们讨论了如何提高神经网络的性能。尽管我们使用的是一个轻量级的数据集,但我们已经学到了关于提高模型性能的一些重要概念——这些概念在考试和工作中都会派上用场。你现在知道,数据质量和模型复杂性是机器学习中的两个方面。如果你有高质量的数据,糟糕的模型也会产生不理想的结果;反之,即使是最先进的模型,如果数据不好,也会产生次优的结果。

到目前为止,你应该对微调神经网络有了深入的理解和实践经验。像一个经验丰富的专家一样,你应该能够理解微调超参数的艺术,并将其应用到不同的机器学习问题中,而不仅仅是图像分类。此外,你已经看到,构建模型需要大量实验。没有银弹,但了解各个环节和各种技巧,以及如何和为什么应用它们,这正是明星与普通人之间的区别。

在下一章中,我们将探讨卷积神经网络。我们将看到它们在图像分类任务中为何处于最先进的水平。我们将了解卷积的强大功能,并通过动手操作,深入了解它们与我们迄今为止使用的简单神经网络有何不同。

问题

让我们使用 CIFAR-10 笔记本测试我们在本章中学到的内容:

  1. 使用我们三步法构建神经网络。

  2. 将隐藏层中的神经元数量从 5 增加到 100。

  3. 使用自定义回调函数,当训练准确率达到 90%时停止训练。

  4. 尝试以下学习率:5、0.5、0.01、0.001。你观察到了什么?

进一步阅读

若要了解更多信息,可以查看以下资源:

第七章:卷积神经网络进行图像分类

卷积神经网络CNNs)是进行图像分类时的首选算法。在 1960 年代,神经科学家 Hubel 和 Wiesel 对猫和猴子的视觉皮层进行了研究。他们的工作揭示了我们如何以层次结构处理视觉信息,展示了视觉系统是如何组织成一系列层次的,每一层都负责视觉处理的不同方面。这一发现为他们赢得了诺贝尔奖,但更重要的是,它为 CNN 的构建奠定了基础。CNN 本质上非常适合处理具有空间结构的数据,例如图像。

然而,在早期,由于多种因素的影响,例如训练数据不足、网络架构不成熟、计算资源匮乏,以及缺乏现代技术(如数据增强和丢弃法),CNN 未能获得广泛关注。在 2012 年 ImageNet 大规模视觉识别挑战赛中,一种名为 AlexNet 的 CNN 架构震惊了机器学习ML)社区,它比其他所有方法都超出了一个较大的优势。今天,机器学习从业者通过应用 CNN,在计算机视觉任务(如图像分类、图像分割和目标检测等)中取得了最先进的表现。

在本章中,我们将研究 CNN,看看它们与我们迄今为止使用的全连接神经网络有什么不同。我们将从全连接网络在处理图像数据时面临的挑战开始,接着探索 CNN 的结构。我们将研究 CNN 架构的核心构建模块及其对网络性能的整体影响。接下来,我们将使用 Fashion MNIST 数据集构建一个图像分类器,然后开始构建一个真实世界的图像分类器。我们将处理不同大小的彩色图像,且图像中的目标物体位置不同。

本章结束时,您将对卷积神经网络(CNN)有一个清晰的理解,并了解为什么在图像分类任务中,它们比全连接网络更具优势。此外,您还将能够在真实世界的图像分类问题中,构建、训练、调整和测试 CNN 模型。

本章我们将讨论以下主题:

  • CNN 的结构解析

  • 使用 CNN 进行 Fashion MNIST 分类

  • 真实世界的图像

  • 天气数据分类

  • 应用超参数以提高模型的性能

  • 评估图像分类器

使用全连接网络进行图像识别的挑战

第五章《使用神经网络进行图像分类》中,我们将深度神经网络DNN)应用于时尚 MNIST 数据集。我们看到输入层中的每个神经元都与隐藏层中的每个神经元相连,而隐藏层中的神经元又与输出层中的神经元相连,因此称为全连接。虽然这种架构可以解决许多机器学习问题,但由于图像数据的空间特性,它并不适合用于图像分类任务。假设你正在看一张人脸的照片;人脸特征的位置和朝向使得即便你只专注于某个特定特征(例如眼睛),你也能知道那是一张人脸。你本能地通过人脸各个特征之间的空间关系知道它是一张人脸;然而,DNN 在查看图像时无法看到这种全貌。它们将图像中的每个像素处理为独立的特征,而没有考虑这些特征之间的空间关系。

使用全连接架构的另一个问题是维度灾难。假设我们正在处理一张尺寸为 150 x 150、具有 3 个颜色通道的真实世界图像,红色、绿色和蓝色RGB);我们将有一个 67,500 的输入大小。由于所有神经元都与下一层的神经元相连,如果我们将这些值输入到一个拥有 500 个神经元的隐藏层中,那么参数数量将为 67,500 x 500 = 33,750,000,并且随着我们增加更多层,这个参数数量将呈指数级增长,使得将这种网络应用于图像分类任务变得资源密集。我们可能还会遇到的另一个问题是过拟合;这是由于网络中大量参数的存在。如果我们处理的是更大尺寸的图像,或者我们为网络增加更多神经元,训练的可训练参数数量将呈指数级增长,而训练这样一个网络可能变得不切实际,因为成本和资源需求过高。考虑到这些挑战,迫切需要一种更为复杂的架构,这就是 CNN 的优势所在,它能够揭示空间关系和层次结构,确保无论特征位于图像的哪个位置,都能被识别出来。

注意

空间关系指的是图像中各个特征在位置、距离和朝向上的相对排列方式。

CNN 的结构

在上一节中,我们看到 DNN 在处理视觉识别任务时面临的一些挑战。这些问题包括缺乏空间感知、高维性、计算低效和过拟合的风险。我们如何克服这些挑战呢?这就是 CNN 登场的地方。CNN 天生就特别适合处理图像数据。让我们通过图 7.1来了解 CNN 为何及如何脱颖而出:

图 7.1 – CNN 的结构

图 7.1 – CNN 的结构

让我们来分解图中的不同层:

  1. 卷积层 – 网络的眼睛:我们的旅程从将图像输入卷积层开始;这个层可以看作是我们网络的“眼睛”。它们的主要工作是提取重要特征。与 DNN(深度神经网络)不同,DNN 中的每个神经元都与下一层的每个神经元相连,而 CNN 通过应用滤波器(也叫做卷积核)以分层的方式捕捉图像中的局部模式。滤波器滑过输入图像的一段区域后,所产生的输出称为特征图。如图 7**.2所示,我们可以看到每个特征图突出显示了我们输入到网络中的衬衫特定图案。图像通过 CNN 按层次结构处理,早期层的滤波器擅长捕捉简单的特征,而后续层的滤波器则捕捉更复杂的模式,模仿人类视觉皮层的层级结构。CNN 的另一个重要特点是参数共享——这是因为模式只需要学习一次,然后应用到图像的其他地方。这确保了模型的视觉能力不依赖于特定位置。在机器学习中,我们称这个概念为平移不变性——网络能够检测衬衫,无论它是对齐在图像的左边、右边还是居中。

图 7.2 – 卷积层捕捉的特征可视化

图 7.2 – 卷积层捕捉的特征可视化

  1. 池化层 – 总结器:卷积层之后是池化层。这个层可以看作是 CNN 中的总结器,它专注于压缩特征图的整体维度,同时保留重要特征,如图 7**.3所示。通过系统地对特征图进行下采样,CNN 不仅显著减少了图像处理所需的参数数量,而且提高了 CNN 的整体计算效率。

图 7.3 – 池化操作示例,保留重要细节

图 7.3 – 池化操作示例,保留重要细节

  1. 全连接层 – 决策者:我们的图像通过一系列卷积和池化层,这些层提取特征并减少特征图的维度,最终到达全连接层。这个层可以看作是决策者。这个层提供了高级推理,它将通过各层收集的所有重要细节整合在一起,用来做出最终的分类判断。CNN 的一个显著特点是其端到端的学习过程,它无缝地整合了特征提取和图像分类。这种有条理且分层的学习方法使得 CNN 成为图像识别和分析的理想工具。

我们仅仅触及了卷积神经网络(CNN)如何工作的表面。现在,让我们深入探讨不同层内发生的关键操作,从卷积开始。

卷积

我们现在知道,卷积层应用过滤器,这些过滤器会滑过输入图像的各个区域。典型的 CNN 应用多个过滤器,每个过滤器通过与输入图像的交互来学习特定类型的特征。通过组合检测到的特征,CNN 能够全面理解图像特征,并利用这些详细信息来对输入图像进行分类。从数学上讲,这一卷积过程涉及输入图像的一块区域与过滤器(一个小矩阵)之间的点积运算,如图 7.4所示。这个过程生成了一个输出,称为激活图特征图

图 7.4 – 卷积操作 – 应用过滤器到输入图像生成特征图

图 7.4 – 卷积操作 – 应用过滤器到输入图像生成特征图

当过滤器滑过图像的各个区域时,它为每个点操作生成一个特征图。特征图是输入图像的一个表示,其中某些视觉模式通过过滤器得到增强,如图 7.2所示。当我们将网络中所有过滤器的特征图叠加在一起时,我们就能得到输入图像的丰富多维视图,为后续层提供足够的信息来学习更复杂的模式。

图 7.5 – a(上)和 b(下):点积计算

图 7.5 – a(上)和 b(下):点积计算

图 7.5 a中,我们看到一个点积操作正在进行中,过滤器滑过输入图像的一个区域,得到一个目标像素值 13。如果我们将过滤器向右移动 1 个像素,如图 7.5 b所示,我们将得到下一个目标像素值 14。如果我们继续每次移动 1 个像素,滑过输入图像,就会得到图 7.5 b中显示的完整输出。

我们现在已经了解了卷积操作的工作原理;然而,我们可以在 CNN 中应用多种类型的卷积层。对于图像分类,我们通常使用 2D 卷积层,而对于音频处理则应用 1D 卷积层,视频处理则使用 3D 卷积层。在设计卷积层时,有许多可调的超参数会影响网络的性能,比如滤波器的数量、滤波器的大小、步幅和填充。探讨这些超参数如何影响我们的网络是非常重要的。

让我们通过观察卷积层中过滤器数量的影响,来开始这次探索。

滤波器数量的影响

通过增加卷积神经网络(CNN)中滤波器的数量,我们可以使其学习到输入图像更丰富、更多样化的表示。滤波器越多,学习到的表示越多。然而,更多的滤波器意味着更多的参数需要训练,这不仅会增加计算成本,还可能会减慢训练过程并增加过拟合的风险。在决定为网络应用多少滤波器时,重要的是要考虑所使用数据的类型。如果数据具有较大的变异性,可能需要更多的滤波器来捕捉数据的多样性;而对于较小的数据集,则应该更为保守,以减少过拟合的风险。

滤波器大小的影响

我们现在知道,滤波器是滑过输入图像以生成特征图的小矩阵。我们应用于输入图像的滤波器的大小将决定从输入图像中提取的特征的层次和类型。滤波器大小是指滤波器的维度——即滤波器矩阵的高度和宽度。通常,你会遇到 3x3、5x5 和 7x7 滤波器。较小的滤波器会覆盖输入图像的较小区域,而较大的滤波器则会覆盖输入图像的更广泛部分:

  • 特征的粒度 – 像 3x3 滤波器这样的较小滤波器可以用于捕捉图像中更细致、更局部的细节,如边缘、纹理和角落,而像 7x7 滤波器这样的较大滤波器则可以学习更广泛的模式,如面部形状或物体部件。

  • 计算效率 – 较小的滤波器覆盖输入图像较小的感受野,如图 7**.6所示,这意味着它们需要更多的计算操作。

图 7.6 – 使用 3x3 滤波器的卷积操作

图 7.6 – 使用 3x3 滤波器的卷积操作

另一方面,较大的滤波器覆盖了输入图像的较大部分,如图 7**.7所示。然而,许多现代卷积神经网络(例如,VGG)使用的是 3x3 滤波器。将这些较小的滤波器叠加在一起会增加网络的深度,并增强这些滤波器捕捉更复杂模式的能力,同时相比于使用大滤波器,所需的参数更少,这使得较小的滤波器更容易训练。

图 7.7 – 使用 5x5 滤波器的卷积操作

图 7.7 – 使用 5x5 滤波器的卷积操作

  • 参数数量 – 较大的滤波器通常比较小的滤波器拥有更多的权重;例如,5x5 滤波器会有 25 个参数,而 3x3 滤波器会有 9 个参数。这里为了简便起见,我们忽略了深度。因此,较大的滤波器相对于较小的滤波器,会使模型变得更加复杂。

步幅的影响

步幅是卷积神经网络(CNN)中的一个重要超参数。它决定了滤波器在输入图像上移动的像素数。我们可以将步幅类比为我们走路时的步伐;如果步伐小,达到目的地会花费更长时间,而较大的步伐则可以更快到达目的地。在图 7.8中,我们应用了步幅为 1,这意味着滤波器每次在输入图像上移动 1 个像素。

图 7.8 – 步幅为 1 的卷积操作

图 7.8 – 步幅为 1 的卷积操作

如果我们应用步幅为 2,意味着滤波器每次移动 2 个像素,如图 7.9所示。我们看到,较大的步幅会导致输出特征图的空间维度减小。通过比较这两幅图的输出,我们可以看到这一点。

图 7.9 – 步幅为 2 的卷积操作

图 7.9 – 步幅为 2 的卷积操作

当我们应用较大的步幅时,它可以提高计算效率,但也会降低输入图像的空间分辨率。因此,在为我们的网络选择合适的步幅时,需要考虑这种权衡。接下来,我们来看看边界效应。

边界问题

当滤波器在输入图像上滑动并执行卷积操作时,很快就会到达边界或边缘,此时由于图像边界外缺少像素,难以执行点积操作。由于边缘或边界信息的丢失,这导致输出特征图小于输入图像。这个问题在机器学习中被称为边效应边界问题。在图 7.10中,我们可以看到,由于滤波器的一部分会超出定义的图像边界,我们无法将滤波器集中在突出显示的像素值 3 上,因此无法在左下角执行点积操作。

图 7.10 – 显示边界问题

图 7.10 – 显示边界问题

为了解决边界问题并保持输出特征图的空间维度,我们可能需要应用填充。接下来我们讨论这个概念。

填充的影响

填充是一种可以应用于卷积过程中的技术,通过向边缘添加额外的像素来防止边界效应,如图 7.11所示。

图 7.11 – 进行卷积操作的填充图像

图 7.11 – 进行卷积操作的填充图像

现在,我们可以在边缘的像素上执行点积操作,从而保留边缘的信息。填充也可以应用于保持卷积前后的空间维度。这在具有多个卷积层的深度卷积神经网络(CNN)架构中可能会非常有用。我们来看一下两种主要的填充类型:

  • 有效填充(无填充):这里没有应用填充。当我们希望减少空间维度,尤其是在较深的层时,这种方法非常有用。

  • 相同填充:这里我们设置填充以确保输出特征图和输入图像的尺寸相同。我们在保持空间维度至关重要时使用这种方法。

在我们继续检查池化层之前,让我们将之前讨论过的卷积层的不同超参数组合起来,并看看它们的实际效果。

综合起来

图 7.12中,我们有一个 7x7 的输入图像和一个 3x3 的滤波器。在这里,我们使用步幅为 1 并将填充设置为有效(无填充)。

图 7.12 – 设置超参数

图 7.12 – 设置超参数

为了计算卷积操作的输出特征图,我们可以应用以下公式:

(W − F + 2P _ S) + 1

在这个公式中,以下内容适用:

  • W 代表输入图像的大小

  • F 代表滤波器大小

  • S 代表步幅

  • P 代表填充

当我们将相应的值代入公式时,得到的结果值为 5,这意味着我们将得到一个 5x5 的输出特征图。如果我们更改任何一个值,它都会以某种方式影响输出特征图的大小。例如,如果我们增加步幅大小,输出特征图将变得更小,而如果我们将填充设置为 same,则会增加输出的大小。现在,我们可以从卷积操作转到池化操作。

池化

池化是一个重要的操作,发生在卷积神经网络(CNN)的池化层中。它是一种用于下采样卷积层生成的单个特征图空间维度的技术。让我们来看看一些常见的池化层类型。我们将从最大池化开始,如图 7.13所示。在这里,我们可以看到最大池化操作是如何工作的。池化层简单地从输入数据的每个区域提取最大值。

图 7.13 – 最大池化操作

图 7.13 – 最大池化操作

最大池化具有多个优点,因为它直观且易于实现。它也很高效,因为它只提取区域中的最大值,并且在各类任务中都取得了良好的效果。

平均池化,顾名思义,通过对指定区域取平均值来减少数据的维度,如图 7.14所示。

图 7.14 – 平均池化操作

图 7.14 – 平均池化操作

另一方面,最小池化会提取输入数据指定区域中的最小值。池化减少了输出特征图的空间大小,从而减少了存储中间表示所需的内存。池化对网络是有益的;然而,过度池化可能适得其反,因为这可能导致信息丢失。经过池化层后,我们到达了全连接层,这是我们网络的决策者。

全连接层

我们的 CNN 架构的最后一个组成部分是全连接层。与卷积层不同,在这里,每个神经元都与下一层中的每个神经元相连接。这个层负责决策,例如分类我们的输入图像是衬衫还是帽子。全连接层将从早期层学到的特征映射到相应的标签。现在我们已经在理论上覆盖了 CNN,接下来我们将其应用到我们的时尚数据集上。

Fashion MNIST 2.0

到现在为止,你已经熟悉了这个数据集,因为我们在第五章,《神经网络图像分类》,和第六章,《改进模型》中使用了它。现在,让我们看看 CNN 与我们迄今为止使用的简单神经网络相比如何。我们将继续保持之前的精神,首先导入所需的库:

  1. 我们将导入所需的库以进行预处理、建模和使用 TensorFlow 可视化我们的机器学习模型:

    import tensorflow as tf
    
    import numpy as np
    
    import matplotlib.pyplot as plt
    
  2. 接下来,我们将使用load_data()函数从 TensorFlow 数据集中加载 Fashion MNIST 数据集。此函数返回我们的训练和测试数据,这些数据由 NumPy 数组组成。训练数据包括x_trainy_train,测试数据由x_testy_test组成:

    (x_train,y_train),(x_test,y_test) = tf.keras.datasets.fashion_mnist.load_data()
    
  3. 我们可以通过对训练数据和测试数据使用len函数来确认数据的大小:

    len(x_train), len(x_test)
    

当我们运行代码时,我们得到以下输出:

(60000, 10000)

我们可以看到,我们的训练数据集有 60,000 张图像,测试数据集有 10,000 张图像。

  1. 在 CNN 中, 与我们之前使用的 DNN 不同,我们需要考虑输入图像的颜色通道。目前,我们的训练和测试数据是灰度图像,其形状为(batch_size, height, width),并且只有一个通道。然而,CNN 模型需要一个 4D 输入张量,由batch_sizeheightwidthchannels组成。我们可以通过简单地重塑数据并将元素转换为float32值来修复这个数据不匹配问题:

    # Reshape the images(batch_size, height, width, channels)
    
    x_train = x_train.reshape(x_train.shape[0],
    
        28, 28, 1).astype('float32')
    
    x_test = x_test.reshape(x_test.shape[0],
    
        28, 28, 1).astype('float32')
    

这个预处理步骤在训练机器学习模型之前是标准步骤,因为大多数模型需要浮动点输入。由于我们的图像是灰度图像,因此只有一个颜色通道,这就是我们将数据重塑为包含单一通道维度的原因。

  1. 我们数据的像素值(训练数据和测试数据)范围从 0255,其中 0 代表黑色,255 代表白色。我们通过将像素值除以 255 来规范化数据,从而将像素值缩放到 01 的区间。这样做的目的是让模型更快收敛,并提高其性能:

    # Normalize the pixel values
    
    x_train /= 255
    
    x_test /= 255
    
  2. 我们使用 tf.kerasutils 模块的 to_categorical 函数,将标签(y_trainy_test)中的整数值(09)转换为一维独热编码数组。to_categorical 函数接受两个参数:需要转换的标签和类别的数量;它返回一个一维独热编码数组,如 图 7.15 所示。

图 7.15 – 一维独热编码数组

图 7.15 – 一维独热编码数组

一维独热编码向量的长度为 10,其中在对应标签的索引位置上为 1,其他位置则为 0

# Convert the labels to one hot encoding format
y_train = tf.keras.utils.to_categorical(y_train, 10)
y_test = tf.keras.utils.to_categorical(y_test, 10)
  1. 使用 tf.keras.model 中的顺序模型 API,我们将创建一个卷积神经网络(CNN)架构:

    # Build the Sequential model
    
    model = tf.keras.models.Sequential()
    
    # Add convolutional layer
    
    model.add(tf.keras.layers.Conv2D(64,kernel_size=(3,3),
    
        activation='relu',
    
        input_shape=(28, 28, 1)))
    
    # Add max pooling layer
    
    model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2)))
    
    # Flatten the data
    
    model.add(tf.keras.layers.Flatten())
    
    # Add fully connected layer
    
    model.add(tf.keras.layers.Dense(128,
    
                                    activation='relu'))
    
    # Apply softmax
    
    model.add(tf.keras.layers.Dense(10,
    
                                    activation='softmax'))
    

第一层是一个卷积层,包含 64 个 3x3 的滤波器,用来处理输入图像,输入图像的形状是 28x28 像素,1 个通道(灰度图)。ReLU 被用作激活函数。随后的最大池化层是一个 2D 池化层,应用最大池化对卷积层的输出进行降采样,减少特征图的维度。flatten 层将池化层的输出展平为一维数组,然后由全连接层处理。输出层使用 softmax 激活函数进行多类分类,并包含 10 个神经元,每个类一个神经元。

  1. 接下来,我们将在训练数据上编译并训练模型:

    # Compile and fit the model
    
    model.compile(loss='categorical_crossentropy',
    
                  optimizer='adam', metrics=['accuracy'])
    
    model.fit(x_train, y_train, epochs=10,
    
              validation_split=0.2)
    

compile() 函数有三个参数:损失函数(categorical_crossentropy,因为这是一个多类分类任务)、优化器(adam)和度量标准(accuracy)。编译模型后,我们使用 fit() 函数在训练数据上训练模型。我们将迭代次数设定为 10,并使用 20% 的训练数据进行验证。

在 10 次迭代后,我们得到了训练准确率为 0.9785,验证准确率为 0.9133

Epoch 6/10
1500/1500 [==============================] - 5s 3ms/step - loss: 0.1267 - accuracy: 0.9532 - val_loss: 0.2548 - val_accuracy: 0.9158
Epoch 7/10
1500/1500 [==============================] - 5s 4ms/step - loss: 0.1061 - accuracy: 0.9606 - val_loss: 0.2767 - val_accuracy: 0.9159
Epoch 8/10
1500/1500 [==============================] - 6s 4ms/step - loss: 0.0880 - accuracy: 0.9681 - val_loss: 0.2957 - val_accuracy: 0.9146
Epoch 9/10
1500/1500 [==============================] - 6s 4ms/step - loss: 0.0697 - accuracy: 0.9749 - val_loss: 0.3177 - val_accuracy: 0.9135
Epoch 10/10
1500/1500 [==============================] - 6s 4ms/step - loss: 0.0588 - accuracy: 0.9785 - val_loss: 0.3472 - val_accuracy: 0.9133
  1. summary 函数是一个非常有用的方式,用来快速概览模型架构,并理解每层的参数数量以及输出张量的形状:

    model.summary()
    

输出返回了组成我们当前模型架构的五个层。它还显示了每个层的输出形状和参数数量。总的参数数量是 1,386,506。通过输出,我们可以看到卷积层的输出形状是 26x26,这是由于边缘效应造成的,因为我们没有使用填充。接下来,最大池化层将像素大小减半,然后我们将数据展平并生成预测:

Model: "sequential"
______________________________________________________
 Layer (type)             Output Shape         Param #
======================================================
 conv2d (Conv2D)          (None, 26, 26,64)    640
 max_pooling2d (MaxPooling2D  (None,13,13,64)  0    )
 flatten (Flatten)            (None, 10816)    0
 dense (Dense)                (None, 128)      1384576
 dense_1 (Dense)              (None, 10)       1290
======================================================
Total params: 1,386,506
Trainable params: 1,386,506
Non-trainable params: 0
_________________________________________________________________
  1. 最后,我们将使用 evaluate 函数在测试数据上评估我们的模型。evaluate 函数返回模型在测试数据上的损失和准确度:

    # Evaluate the model
    
    score = model.evaluate(x_test, y_test)
    

我们的模型在测试数据上实现了 0.9079 的准确率,超过了在 第六章*,改进模型* 中使用的架构的性能。我们可以通过调整超参数和应用数据增强来进一步提高模型的性能。让我们把注意力转向现实世界的图像,CNNs 显然比我们以前的模型更出色。

处理现实世界图像

现实世界中的图像提出了不同类型的挑战,因为这些图像通常是彩色图像,具有三个色彩通道(红色、绿色和蓝色),不像我们从时尚 MNIST 数据集中使用的灰度图像。在 图 7*.16* 中,我们看到了一些即将建模的来自天气数据集的实际图像示例,您会注意到这些图像的大小各异。这引入了另一层复杂性,需要额外的预处理步骤,如调整大小或裁剪,以确保我们所有的图像在输入神经网络之前具有统一的尺寸。

图 7.16 – 天气数据集中的图像

图 7.16 – 天气数据集中的图像

在处理现实世界图像时,我们可能会遇到的另一个问题是各种噪声源的存在。例如,我们的数据集中可能有在光线不均匀或意外模糊条件下拍摄的图像。同样,在我们的现实世界数据集中可能会有多个对象或其他意外的背景干扰图像。

要解决这些问题,我们可以应用去噪技术,如去噪,以改善数据的质量。我们还可以使用对象检测技术,如边界框或分割,帮助我们在具有多个对象的图像中识别目标对象。好消息是 TensorFlow 配备了一套完整的工具集,专门处理这些挑战。来自 TensorFlow 的一个重要工具是 tf.image 模块,提供了一系列图像预处理功能,如调整大小、亮度、对比度、色调和饱和度的应用、边界框、裁剪、翻转等等。

然而,这个模块超出了本书及考试的范围。但是,如果你希望深入了解这个模块,可以访问 TensorFlow 文档:www.tensorflow.org/api_docs/python/tf/image。TensorFlow 的另一个工具是ImageDataGenerator,它使我们能够实时执行数据增强操作,提供了在将图像输入训练管道时对其进行预处理和增广(例如旋转和翻转图像)的能力。接下来,我们将使用实际的图像数据集,看看ImageDataGenerator如何发挥作用。

天气数据集分类

在这个案例研究中,我们将作为计算机视觉顾问为一个新兴的初创公司 WeatherBIG 提供支持。你被分配了开发一个图像分类系统的任务,该系统将用于识别不同的天气状况;该任务的数据集可以通过以下链接在 Kaggle 上找到:www.kaggle.com/datasets/rahul29g/weatherdataset。该数据集已被分成三个文件夹,包括训练文件夹、验证文件夹和测试文件夹。每个文件夹下都有各自的天气类别子文件夹。让我们开始这个任务:

  1. 我们首先导入几个库来构建我们的图像分类器:

    import os
    
    import pathlib
    
    import matplotlib.pyplot as plt
    
    import matplotlib.image as mpimg
    
    import random
    
    import numpy as np
    
    from PIL import Image
    
    import tensorflow as tf
    
    from tensorflow import keras
    
    from tensorflow.keras.preprocessing.image import ImageDataGenerator
    

我们在之前的实验中使用了这些库中的几个;然而,接下来我们将介绍一些第一次使用的库的功能。os模块作为我们操作系统的桥梁。它使我们能够读取和写入文件系统,而pathlib提供了一种直观的面向对象的方式来简化文件导航任务。对于图像操作,我们使用PIL,此外还有来自tensorflow.keras.preprocessing.image模块的ImageDataGenerator类,用于我们的数据预处理、批次生成和数据增强步骤。

  1. 你可以从www.kaggle.com/datasets/rahul29g/weatherdataset获取/下载此案例研究的数据集,并将其上传到 Google Drive。完成后,你可以轻松地跟随本节中的代码进行操作。在我的例子中,数据存储在此根目录:/content/drive/MyDrive/weather dataset。在你的情况下,根目录将会不同,所以请确保将目录路径更改为与数据集存储在 Google Drive 中的目录匹配:root_dir = "/content/drive/MyDrive/weather dataset"

  2. 接下来,我们应用os.walk函数来访问根目录,并生成关于所有目录和子目录内容的信息:

    for dirpath, dirnames, filenames in os.walk(root_dir):
    
        print(f"Directory: {dirpath}")
    
        print(f"Number of images: {len(filenames)}")
    
        print()
    

运行代码会返回一个元组,包含每个目录的路径以及每个目录中图片的数量,如图 7.17所示:

图 7.17 – 快照目录及其子目录

图 7.17 – 快照目录及其子目录

我们通过这一步来了解每个目录和子目录的内容。

  1. 我们使用retrieve_labels函数从训练、测试和验证目录中提取并显示标签及其对应的计数。为了实现这个函数,我们使用os模块中的listdir方法,并传入相应的目录路径(train_dirtest_dirval_dir):

    def retrieve_labels(train_dir, test_dir, val_dir):
    
        # Retrieve labels from training directory
    
        train_labels = os.listdir(train_dir)
    
        print(f"Training labels: {train_labels}")
    
        print(f"Number of training labels: {len(train_labels)}")
    
        print()
    
        # Retrieve labels from test directory
    
        test_labels = os.listdir(test_dir)
    
        print(f"Test labels: {test_labels}")
    
        print(f"Number of test labels: {len(test_labels)}")
    
        print()
    
        # Retrieve labels from validation directory
    
        val_labels = os.listdir(val_dir)
    
        print(f"Validation labels: {val_labels}")
    
        print(f"Number of validation labels: {len(val_labels)}")
    
        print()
    
  2. 我们分别在train_dirtest_dirval_dir参数中指定训练、测试和验证目录的路径:

    train_dir = "/content/drive/MyDrive/weather dataset/train"
    
    test_dir = "/content/drive/MyDrive/weather dataset/test"
    
    val_dir = "/content/drive/MyDrive/weather dataset/validation"
    
    retrieve_labels(train_dir, test_dir, val_dir)
    

当我们运行代码时,它将返回训练数据、测试数据、验证数据标签以及标签的数量:

Training labels: ['cloud', 'shine', 'rain', 'sunrise']
Number of training labels: 4
Test labels: ['sunrise', 'shine', 'cloud', 'rain']
Number of test labels: 4
Validation labels: ['shine', 'sunrise', 'cloud', 'rain']
Number of validation labels: 4
  1. 在我们的探索中,创建一个名为view_random_images的函数,从数据集中的子目录中随机访问并显示图片。该函数接受包含子目录的主目录以及我们希望显示的图片数量。我们使用listdir来访问子目录,并引入随机性来选择图片。我们使用random库中的shuffle函数进行打乱并随机选择图片。我们利用 Matplotlib 来显示指定数量的随机图片:

    def view_random_images(target_dir, num_images):
    
      """
    
      View num_images random images from the subdirectories of target_dir as a subplot.
    
      """
    
      # Get list of subdirectories
    
        subdirs = [d for d in os.listdir(
    
            target_dir) if os.path.isdir(os.path.join(
    
                target_dir, d))]
    
      # Select num_images random subdirectories
    
        random.shuffle(subdirs)
    
        selected_subdirs = subdirs[:num_images]
    
      # Create a subplot
    
        fig, axes = plt.subplots(1, num_images, figsize=(15,9))
    
        for i, subdir in enumerate(selected_subdirs):
    
          # Get list of images in subdirectory
    
            image_paths = [f for f in os.listdir(
    
                os.path.join(target_dir, subdir))]
    
          # Select a random image
    
            image_path = random.choice(image_paths)
    
          # Load image
    
            image = plt.imread(os.path.join(target_dir,
    
                subdir, image_path))
    
          # Display image in subplot
    
            axes[i].imshow(image)
    
            axes[i].axis("off")
    
            axes[i].set_title(subdir)
    
        print(f"Shape of image: {image.shape}")    
    
        #width,height, colour chDNNels
    
        plt.show()
    
  2. 让我们通过将num_images设置为4来尝试这个函数,并查看train目录中的一些数据:

    view_random_images(target_dir="/content/drive/MyDrive/weather dataset/train/", num_images=4)
    

这将返回四张随机选择的图片,如下所示:

7.18 – 从天气数据集中随机选择的图片

7.18 – 从天气数据集中随机选择的图片

从显示的数据来看,图片的尺寸(高度和宽度)各不相同,我们需要解决这个预处理问题。我们将使用 TensorFlow 中的ImageDataGenerator类。接下来我们将讨论这个问题。

图片数据预处理

我们在图 7.18中看到,训练图片的大小各不相同。在这里,我们将在训练前调整并规范化数据。此外,我们还希望开发一种有效的方法来批量加载训练数据,确保优化内存使用并与模型的训练过程无缝对接。为了实现这一目标,我们将使用TensorFlow.keras.preprocessing.image模块中的ImageDataGenerator类。在第八章《处理过拟合》中,我们将进一步应用ImageDataGenerator,通过旋转、翻转和缩放等方式生成训练数据的变种,扩大我们的训练数据集。这将有助于我们的模型变得更强大,并减少过拟合的风险。

另一个有助于我们数据预处理任务的有用工具是flow_from_directory方法。我们可以使用此方法构建数据管道。当我们处理大规模、实际数据时,它尤其有用,因为它能够自动读取、调整大小并将图像批量化,以便进行模型训练或推理。flow_from_directory方法接受三个主要参数。第一个是包含图像数据的目录路径。接下来,我们指定在将图像输入神经网络之前,图像的期望大小。然后,我们还需要指定批量大小,以确定我们希望同时处理的图像数量。我们还可以通过指定其他参数,如颜色模式、类别模式和是否打乱数据,来进一步定制该过程。现在,让我们来看一下一个多分类问题的典型目录结构,如图 7.19所示。

图 7.19 – 多分类问题的目录结构

图 7.19 – 多分类问题的目录结构

在应用flow_from_directory方法时,重要的是我们需要将图像组织在一个结构良好的目录中,每个唯一的类别标签都有一个子目录,如图 7.19所示。在这里,我们有四个子目录,每个子目录对应我们的天气数据集中的一个类别标签。一旦所有图像都放入了适当的子目录,我们就可以应用flow_from_directory来设置一个迭代器。这个迭代器是可调的,我们可以定义图像大小、批量大小等参数,并决定是否打乱数据。接下来,我们将这些新想法应用到我们当前的案例研究中:

# Preprocess data (get all of the pixel values between 1 and 0, also called scaling/normalization)
train_datagen = ImageDataGenerator(rescale=1./255)
valid_datagen = ImageDataGenerator(rescale=1./255)
test_datagen = ImageDataGenerator(rescale=1./255)

在这里,我们定义了三个ImageDataGenerator类的实例:一个用于训练,一个用于验证,一个用于测试。我们对每个实例中的图像像素值应用了 1/255 的缩放因子,以便对数据进行归一化:

# Import data from directories and turn it into batches
train_data = train_datagen.flow_from_directory(train_dir,
    batch_size=64, # number of images to process at a time
    target_size=(224,224), # convert all images to be 224 x 224
    class_mode="categorical")
valid_data = valid_datagen.flow_from_directory(val_dir,
    batch_size=64,
    target_size=(224,224),
    class_mode="categorical")
test_data = test_datagen.flow_from_directory(test_dir,
    batch_size=64,
    target_size=(224,224),
    class_mode="categorical")

我们使用flow_from_directory从各自的训练、验证和测试目录中导入图像,结果数据存储在train_datavalid_datatest_data变量中。除了在flow_from_directory方法中指定目录外,您会注意到我们不仅指定了目标大小(224 x 244)和批量大小(64),还指定了我们正在处理的问题类型为categorical,因为我们处理的是一个多分类问题。我们现在已经成功完成了数据预处理步骤。接下来,我们将开始对数据建模:

model_1 = tf.keras.models.Sequential([
    tf.keras.layers.Conv2D(filters=16,
        kernel_size=3, # can also be (3, 3)
        activation="relu",
        input_shape=(224, 224, 3)),
        #(height, width, colour channels)
tf.keras.layers.MaxPool2D(2,2),
    tf.keras.layers.Conv2D(32, 3, activation="relu"),
    tf.keras.layers.MaxPool2D(2,2),
    tf.keras.layers.Conv2D(64, 3, activation="relu"),
    tf.keras.layers.MaxPool2D(2,2),
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(1050, activation="relu"),
    tf.keras.layers.Dense(4, activation="softmax")
    # binary activation output
])
# Compile the model
model_1.compile(loss="CategoricalCrossentropy",
    optimizer=tf.keras.optimizers.Adam(),
    metrics=["accuracy"])
# Fit the model
history_1 = model_1.fit(train_data,
    epochs=10,
    validation_data=valid_data,
    )

在这里,我们使用一个由三组卷积层和池化层组成的 CNN 架构。在第一层卷积层中,我们应用了 16 个 3×3 的过滤器。请注意,输入形状也与我们在预处理步骤中定义的形状匹配。在第一层卷积层之后,我们应用了 2x2 的最大池化。接下来,我们进入第二层卷积层,使用 32 个 3×3 的过滤器,后面跟着另一个 2x2 的最大池化层。最后一层卷积层有 64 个 3×3 的过滤器,后面跟着另一个最大池化层,进一步对数据进行下采样。

接下来,我们进入全连接层。在这里,我们首先将之前层的 3D 输出展平为 1D 数组。然后,我们将数据传递到密集层进行最终分类。接下来,我们编译并拟合我们的模型到数据上。需要注意的是,在我们的compile步骤中,我们使用CategoricalCrossentropy作为loss函数,因为我们正在处理一个多类任务,并将metrics设置为accuracy。最终输出是一个概率分布,表示我们数据集中的四个类别,具有最高概率的类别即为预测标签:

Epoch 6/10
13/13 [==============================] - 8s 622ms/step - loss: 0.1961 - accuracy: 0.9368 - val_loss: 0.2428 - val_accuracy: 0.8994
Epoch 7/10
13/13 [==============================] - 8s 653ms/step - loss: 0.1897 - accuracy: 0.9241 - val_loss: 0.2967 - val_accuracy: 0.9218
Epoch 8/10
13/13 [==============================] - 8s 613ms/step - loss: 0.1093 - accuracy: 0.9671 - val_loss: 0.3447 - val_accuracy: 0.8939
Epoch 9/10
13/13 [==============================] - 8s 604ms/step - loss: 0.1756 - accuracy: 0.9381 - val_loss: 0.6276 - val_accuracy: 0.8324
Epoch 10/10
13/13 [==============================] - 8s 629ms/step - loss: 0.1472 - accuracy: 0.9418 - val_loss: 0.2633 - val_accuracy: 0.9106

我们训练了我们的模型 10 个周期,达到了在训练数据上的 94%训练准确率和在验证数据上的 91%准确率。我们使用summary方法来获取模型中不同层的信息。这些信息包括每层的概述、输出形状以及使用的参数数量(可训练和不可训练):

Model: "sequential"
___________________________________________________________
 Layer (type)                 Output Shape         Param #
===========================================================
 conv2d (Conv2D)              (None, 222, 222, 16) 448
 max_pooling2d (MaxPooling2D) (None, 111, 111, 16) 0
 conv2d_1 (Conv2D)            (None, 109, 109, 32) 4640
 max_pooling2d_1 (MaxPooling  (None, 54, 54, 32)   0
 2D)
 conv2d_2 (Conv2D)            (None, 52, 52, 64)   18496
 max_pooling2d_2 (MaxPooling  (None, 26, 26, 64)   0
 2D)
 flatten (Flatten)            (None, 43264)        0
 dense (Dense)                (None, 1050)         45428250
 dense_1 (Dense)              (None, 4)            4204
===========================================================
Total params: 45,456,038
Trainable params: 45,456,038
Non-trainable params: 0
___________________________________________________________

从模型的总结中,我们看到我们的架构包含三层卷积层(Conv2D),每一层都配有一个池化层(MaxPooling2D)。信息从这些层流入到全连接层,在那里进行最终的分类。让我们深入分析每一层,解读它们提供的信息。第一层卷积层的输出形状为(None, 222, 222, 16)。这里,None表示我们没有硬编码批次大小,这使得我们能够灵活地使用不同的批次大小。接下来,222, 222表示输出特征图的尺寸;如果不应用填充,我们会因为边界效应而丢失 2 个像素的高度和宽度。最后,16表示使用的过滤器或内核的数量,这意味着每个过滤器将输出 16 个不同的特征图。你还会注意到这一层有448个参数。为了计算卷积层中的参数数量,我们使用以下公式:

(过滤器宽度 × 过滤器高度 × 输入通道数 + 1(偏置)) × 过滤器数量 = 卷积层的总参数数量

当我们将数值代入公式时,我们得到(3 × 3 × 3 + 1)× 16 = 448 个参数。

下一个层是第一个池化层,它是一个MaxPooling2D层,用于对卷积层的输出特征图进行降采样。在这里,我们的输出形状为(None, 111, 111, 16)。从输出中可以看到,空间维度已经缩小了一半,同时也要注意,池化层没有参数,正如我们在模型总结中看到的所有池化层一样。

接下来,我们进入第二个卷积层,注意到我们的输出深度已经增加到了32。这是因为我们在该层使用了 32 个过滤器,因此将返回 32 个不同的特征图。同时,由于边界效应,我们的特征图的空间维度再次被减少了两个像素。我们可以通过以下方式轻松计算该层的参数数量:(3 × 3 × 16 + 1)× 32 = 4,640 个参数。

接下来,我们进入第二个池化层,它进一步对特征图进行降采样,输出尺寸为(None, 54, 54, 32)。最后的卷积层使用 64 个过滤器,因此输出形状为(None, 52, 52, 64),有 18,496 个参数。最后的池化层再次将数据维度降至(None, 26, 26, 64)。最后池化层的输出被送入Flatten层,后者将数据从 3D 张量重塑为 1D 张量,尺寸为 26 x 26 x 64 = 43,264。这个数据接着被送入第一个Dense层,其输出形状为(None, 1050)。为了计算Dense层的参数数量,我们使用以下公式:

(输入节点数 + 1) × 输出节点数

当我们输入这些数值时,得到(43,264 + 1) × 1,050 = 45,428,250 个参数。最终的Dense层是输出层,其形状为(None, 4),其中4表示我们要预测的独特类别数。由于连接、偏置和输出神经元数量,该层有(1,050 + 1) × 4 = 4,204 个参数。

接下来,我们使用evaluate方法评估我们的模型:

model_1.evaluate(test_data)

我们在测试数据上达到了 91%的准确率。

让我们将我们的 CNN 架构与两个 DNN 进行比较:

model_2 = tf.keras.Sequential([
    tf.keras.layers.Flatten(input_shape=(224, 224, 3)),
    tf.keras.layers.Dense(1200, activation='relu'),
    tf.keras.layers.Dense(600, activation='relu'),
    tf.keras.layers.Dense(300, activation='relu'),
    tf.keras.layers.Dense(4, activation='softmax')
])
# Compile the model
model_2.compile(loss='categorical_crossentropy',
    optimizer=tf.keras.optimizers.Adam(),
    metrics=["accuracy"])
# Fit the model
history_2 = model_2.fit(train_data,
    epochs=10,
    validation_data=valid_data)

我们构建了一个名为model_2的 DNN,由 4 个Dense层组成,分别有12006003004个神经元。除了输出层使用softmax函数进行分类外,其他所有层都使用 ReLU 作为激活函数。我们与model_1的方式相同,编译并拟合model_2

Epoch 6/10
13/13 [==============================] - 8s 625ms/step - loss: 2.2083 - accuracy: 0.6953 - val_loss: 0.9884 - val_accuracy: 0.7933
Epoch 7/10
13/13 [==============================] - 8s 606ms/step - loss: 2.7116 - accuracy: 0.6435 - val_loss: 2.0749 - val_accuracy: 0.6704
Epoch 8/10
13/13 [==============================] - 8s 636ms/step - loss: 2.8324 - accuracy: 0.6877 - val_loss: 1.7241 - val_accuracy: 0.7430
Epoch 9/10
13/13 [==============================] - 8s 599ms/step - loss: 1.8597 - accuracy: 0.6890 - val_loss: 1.1507 - val_accuracy: 0.7877
Epoch 10/10
13/13 [==============================] - 8s 612ms/step - loss: 1.0902 - accuracy: 0.7813 - val_loss: 0.9915 - val_accuracy: 0.7486

经过 10 个 epoch,我们达到了 74.86%的验证准确率,当我们查看模型的总结时,发现我们总共使用了 181,536,904 个参数,这是我们 CNN 架构参数的 4 倍。

接下来,我们来看另一个 DNN 架构:

model_3 = tf.keras.Sequential([
    tf.keras.layers.Flatten(input_shape=(224, 224, 3)),
    tf.keras.layers.Dense(1000, activation='relu'),
    tf.keras.layers.Dense(500, activation='relu'),
    tf.keras.layers.Dense(500, activation='relu'),
    tf.keras.layers.Dense(4, activation='softmax')
])
# Compile the model
model_3.compile(loss='categorical_crossentropy',
    optimizer=tf.keras.optimizers.Adam(),
    metrics=["accuracy"])
# Fit the model
history_3 = model_3.fit(train_data,
    epochs=10,
    validation_data=valid_data)

我们使用另外一组 4 个Dense层,分别有10005005004个神经元。我们同样为model_3进行了 10 个 epoch 的拟合和编译:

Epoch 6/10
13/13 [==============================] - 9s 665ms/step - loss: 1.6911 - accuracy: 0.6814 - val_loss: 0.5861 - val_accuracy: 0.7877
Epoch 7/10
13/13 [==============================] - 8s 606ms/step - loss: 0.7309 - accuracy: 0.7952 - val_loss: 0.5100 - val_accuracy: 0.8268
Epoch 8/10
13/13 [==============================] - 8s 572ms/step - loss: 0.6797 - accuracy: 0.7863 - val_loss: 0.9520 - val_accuracy: 0.7263
Epoch 9/10
13/13 [==============================] - 8s 632ms/step - loss: 0.7430 - accuracy: 0.7724 - val_loss: 0.5220 - val_accuracy: 0.7933
Epoch 10/10
13/13 [==============================] - 8s 620ms/step - loss: 0.5845 - accuracy: 0.7737 - val_loss: 0.5881 - val_accuracy: 0.7765

在经过 10 个 epoch 后,我们达到了 77.65%的验证准确率,该模型大约有 151,282,004 个参数;结果与我们的 CNN 架构相差较大。接下来,让我们在测试数据上比较三种模型,这是我们评估模型的标准。为此,我们将编写一个函数来生成一个 DataFrame,显示模型的名称、损失和准确率:

def evaluate_models(models, model_names,test_data):
    # Initialize lists for the results
    losses = []
    accuracies = []
    # Iterate over the models
    for model in models:
        # Evaluate the model
        loss, accuracy = model.evaluate(test_data)
        losses.append(loss)
        accuracies.append(accuracy)
       # Convert the results to percentages
    losses = [round(loss * 100, 2) for loss in losses]
    accuracies = [round(accuracy * 100, 2) for accuracy in accuracies]
    # Create a dataframe with the results
    results = pd.DataFrame({"Model": model_names,
        "Loss": losses,
        "Accuracy": accuracies})
    return results

evaluate_models()函数接受一个模型列表、模型名称和测试数据作为输入,并返回一个包含每个模型评估结果(以百分比形式)的 DataFrame:

# Define the models and model names
models = [model_1, model_2, model_3]
model_names = ["Model 1", "Model 2", "Model 3"]
# Evaluate the models
results = evaluate_models(models, model_names,test_data)
# Display the results
results

当我们运行代码时,它会生成如图 7.20所示的表格。

图 7.20 – 显示所有三种模型实验结果的 DataFrame

图 7.20 – 显示所有三种模型实验结果的 DataFrame

从结果中,我们可以清楚地看到模型 1 表现最好。你可能希望尝试更大的 DNN,但很快会遇到内存不足的问题。对于更大的数据集,DNN 的结果可能会大幅下降。接下来,让我们看看模型 1 在训练和验证数据上的表现:

def plot_loss_accuracy(history_1):
  # Extract the loss and accuracy history for both training and validation data
    loss = history_1.history['loss']
    val_loss = history_1.history['val_loss']
    acc = history_1.history['accuracy']
    val_acc = history_1.history['val_accuracy']
  # Create subplots
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9, 6))
  # Plot the loss history
    ax1.plot(loss, label='Training loss')
    ax1.plot(val_loss, label='Validation loss')
    ax1.set_title('Loss history')
    ax1.set_xlabel('Epoch')
    ax1.set_ylabel('Loss')
    ax1.legend()
  # Plot the accuracy history
    ax2.plot(acc, label='Training accuracy')
    ax2.plot(val_acc, label='Validation accuracy')
    ax2.set_title('Accuracy history')
    ax2.set_xlabel('Epoch')
    ax2.set_ylabel('Accuracy')
    ax2.legend()
    plt.show()

我们创建了一个函数,使用 matplotlib 绘制训练和验证的损失与准确率图。我们将history_1传递给这个函数:

# Lets plot the training and validation loss and accuracy
plot_loss_accuracy(history_1)

这将生成以下输出:

图 7.21 – 模型 1 的损失和准确率图

图 7.21 – 模型 1 的损失和准确率图

从图中可以看到,我们的训练准确率稳步上升,但在接近第 10 个 epoch 时,准确率未能达到最高点。同时,我们的验证数据的准确率出现了急剧下降。我们的损失从第 4 个 epoch 开始偏离。

总结

在这一章中,我们见识了 CNN 的强大。我们首先探讨了 DNN 在视觉识别任务中面临的挑战。接着,我们深入了解了 CNN 的构造,重点讲解了卷积层、池化层和全连接层等各个部分。在这里,我们观察了不同超参数的影响,并讨论了边界效应。接下来,我们利用所学知识,构建了一个实际的天气分类器,使用了两个 DNN 和一个 CNN。我们的 CNN 模型优于 DNN,展示了 CNN 在处理基于图像的问题中的优势。同时,我们还讨论并应用了一些 TensorFlow 函数,这些函数可以简化数据预处理和建模过程,特别是在处理图像数据时。

到目前为止,你应该已经很好地理解了 CNN 的结构和操作原理,以及如何使用它们解决实际的图像分类问题,并掌握了利用 TensorFlow 中各种工具有效、高效地预处理图像数据,从而提高模型性能。在下一章中,我们将讨论神经网络中的过拟合问题,并探索各种技术来克服这一挑战,确保我们的模型能很好地泛化到未见过的数据上。

在下一章中,我们将使用一些老技巧,如回调和超参数调整,看看是否能提高模型性能。我们还将实验数据增强和其他新技术,以进一步提高模型的表现。我们将在此任务上暂时画上句号,直到第八章**, 处理过拟合

问题

让我们测试一下本章所学内容:

  1. 一个典型的 CNN 架构有哪些组成部分?

  2. 卷积层在 CNN 架构中是如何工作的?

  3. 什么是池化,为什么在 CNN 架构中使用池化?

  4. 在 CNN 架构中,全连接层的目的是什么?

  5. 填充对卷积操作有什么影响?

  6. 使用 TensorFlow 图像数据生成器的优势是什么?

进一步阅读

要了解更多内容,您可以查看以下资源:

  • Dumoulin, V., & Visin, F. (2016). 《深度学习的卷积算术指南》arxiv.org/abs/1603.07285

  • Gulli, A., Kapoor, A. 和 Pal, S., 2019. 《使用 TensorFlow 2 和 Keras 的深度学习》。伯明翰:Packt Publishing Ltd

  • Kapoor, A., Gulli, A. 和 Pal, S. (2020) 《深度学习与 TensorFlow 和 Keras 第三版:构建和部署监督、非监督、深度和强化学习模型》。Packt Publishing Ltd

  • Krizhevsky, A., Sutskever, I., & Hinton, G. E. (2012). 《使用深度卷积神经网络进行 ImageNet 分类》。《神经信息处理系统进展》(第 1097-1105 页)

  • Zhang, Y., & Yang, H. (2018). 《使用卷积神经网络和多类线性判别分析进行食物分类》。2018 年 IEEE 国际信息重用与集成会议(IRI)(第 1-5 页)。IEEE

  • Zhang, Z., Ma, H., Fu, H., & Zha, C. (2020). 《基于单图像的无场景多类天气分类》。IEEE Access, 8, 146038-146049. doi:10.1109

  • tf.image模块:www.tensorflow.org/api_docs/python/tf/image

第八章:处理过拟合

机器学习(ML)中的一个主要挑战是过拟合。过拟合发生在模型对训练数据的拟合过好,但在未见过的数据上表现不佳,导致性能差。在第六章中,改进模型,我们亲眼见证了过度训练如何将我们的模型推入过拟合的陷阱。在本章中,我们将进一步探讨过拟合的细微差别,努力揭示其警告信号及其潜在原因。同时,我们还将探索可以应用的各种策略,以减轻过拟合对现实世界机器学习应用的危害。通过 TensorFlow,我们将以实践的方式应用这些思想,克服在实际案例中遇到的过拟合问题。通过本章学习结束后,你应该对过拟合的概念以及如何在现实世界的图像分类任务中减少过拟合有一个扎实的理解。

在本章中,我们将讨论以下主题:

  • 机器学习中的过拟合

  • 提前停止

  • 更改模型架构

  • L1 和 L2 正则化

  • Dropout 正则化

  • 数据增强

技术要求

我们将使用 Google Colab 来运行需要 python >= 3.8.0 的编码练习,并且需要安装以下包,可以通过 pip install 命令进行安装:

  • tensorflow>=2.7.0

  • os

  • pillow==8.4.0

  • pandas==1.3.4

  • numpy==1.21.4

  • matplotlib >=3.4.0

本书的代码包可以通过以下 GitHub 链接访问:github.com/PacktPublishing/TensorFlow-Developer-Certificate。此外,所有练习的解答也可以在 GitHub 仓库中找到。

机器学习中的过拟合

从前面的章节中,我们已经知道了什么是过拟合,以及它在未见过的数据上使用时的负面影响。接下来,我们将进一步探讨过拟合的根本原因,如何在构建模型时识别过拟合,以及可以应用的一些重要策略来抑制过拟合。当我们理解了这些内容后,就可以继续构建有效且强大的机器学习模型。

触发过拟合的原因

第六章改进模型中,我们看到通过向隐藏层添加更多神经元,我们的模型变得过于复杂。这使得模型不仅捕捉到了数据中的模式,还捕捉到了数据中的噪声,从而导致了过拟合。另一个导致过拟合的根本原因是数据量不足。如果我们的数据无法真正捕捉到模型在部署后将面临的所有变化,当我们在这样的数据集上训练模型时,它会变得过于专门化,并且在实际应用中无法进行有效的泛化。

除了数据量之外,我们还可能面临另一个问题——噪声数据。与处理经过筛选或静态的数据不同,在构建实际应用时,我们可能会发现数据存在噪声或错误。如果我们使用这些数据开发模型,可能会导致在实际使用时出现过拟合的情况。我们已经讨论了关于过拟合可能发生的一些原因;接下来我们可能想要问的问题是,我们如何检测过拟合?让我们在接下来的子章节中讨论这个问题。

检测过拟合

检测过拟合的一种方法是比较模型在训练数据和验证/测试数据上的准确度。当模型在训练数据上表现出高准确度,而在测试数据上表现不佳时,这种差异表明模型已经记住了训练样本,因此在未见过的数据上的泛化能力较差。另一种有效的发现过拟合的方法是检查训练误差与验证误差。当训练误差随着时间的推移逐渐减小,而验证误差却增加时,这可能表明我们的模型过拟合了,因为模型在验证数据上的表现变差。当模型的验证准确度恶化,而训练准确度却不断提升时,应该引起警觉,可能存在过拟合的风险。

让我们回顾一下来自第七章的案例研究,卷积神经网络的图像分类,以及 WeatherBIG 的天气数据集,并探讨在模型训练过程中如何通过使用验证数据集来监控过拟合。通过使用验证数据集,我们可以准确地追踪模型的表现,防止过拟合。首先,我们将创建一个基准模型。

基准模型

按照构建、编译和拟合的标准三步法,我们将构建一个卷积神经网络CNN)模型,该模型包括两个 Conv2D 和池化层,并配有一个具有 1,050 个神经元的全连接层。输出层由四个神经元组成,表示我们数据集中的四个类别。然后,我们使用训练数据将模型编译并拟合 20 个周期:

#Build
model_1 = tf.keras.models.Sequential([
    tf.keras.layers.Conv2D(filters=16,
        kernel_size=3, # can also be (3, 3)
        activation="relu",
        input_shape=(224, 224, 3)),
        #(height, width, colour channels)
    tf.keras.layers.MaxPool2D(2,2),
    tf.keras.layers.Conv2D(32, 3, activation="relu"),
    tf.keras.layers.MaxPool2D(2,2),
    tf.keras.layers.Conv2D(64, 3, activation="relu"),
    tf.keras.layers.MaxPool2D(2,2),
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(1050, activation="relu"),
    tf.keras.layers.Dense(4, activation="softmax")
])
# Compile the model
model_1.compile(loss="CategoricalCrossentropy",
    optimizer=tf.keras.optimizers.Adam(),
    metrics=["accuracy"])
#fit
history_1 = model_1.fit(train_data,
    epochs=20,
    validation_data=valid_data
)

我们将validation_data参数设置为valid_data。这确保了当我们运行代码时,在每个周期结束后,模型会在验证数据上评估其性能,如图 8.1所示。

图 8.1 – 最后的五个训练周期

图 8.1 – 最后的五个训练周期

这是一种直观的方法,可以比较训练集和验证集之间的损失值。我们可以看到模型能够准确地预测训练集中的每个样本,达到了 100%的准确率。然而,在验证集上,它的准确率为 91%,这表明模型可能存在过拟合问题。观察过拟合的另一种有效方法是使用学习曲线,绘制训练集和验证集的损失和准确度值——如图 8.2所示,两个图之间存在较大的差距,表明模型存在过拟合。

图 8.2 – 显示训练和测试数据的损失和准确度的学习曲线

图 8.2 – 显示训练和测试数据的损失和准确度的学习曲线

在实验开始时,训练损失和验证损失之间的差异较小;然而,进入第四轮时,验证损失开始增加,而训练损失继续下降。类似地,训练和验证的准确度开始时较为接近,但在大约第四轮时,验证准确率达到了 90%左右并保持在该水平,而训练准确率达到了 100%。

构建图像分类器的最终目标是将其应用于现实世界的数据。在完成训练过程后,我们使用保留数据集评估模型。如果在测试中获得的结果与训练过程中取得的结果有显著差异,这可能表明模型存在过拟合。

幸运的是,有几种策略可以用来克服过拟合问题。一些主要的应对过拟合的技术侧重于改进模型本身,以提高其泛化能力。另一方面,检查数据本身同样重要,观察模型在训练和评估过程中忽视的部分。通过可视化错误分类的图像,我们可以洞察模型的不足之处。我们从第七章《卷积神经网络图像分类》开始,首先重新创建我们的基线模型。这次我们将其训练 20 轮,以便观察过拟合问题,如图 8.2所示。接下来,让我们看看如何通过多种策略来抑制过拟合,首先是应用早停法。

早停法

第六章《改进模型》中,我们介绍了早停法的概念,这是一种有效的防止过拟合的方法。它通过在模型性能未能在定义的若干轮次内改善时停止训练,如图 8.3所示,从而避免了过拟合的发生。

图 8.3 – 显示早停法的学习曲线

图 8.3 – 显示早停法的学习曲线

让我们重新创建相同的基准模型,但这次我们将应用一个内置回调,在验证精度未能提高时停止训练。我们将使用与第一个模型相同的构建和编译步骤,然后在拟合模型时添加回调:

#Fit the model
# Add an early stopping callback
callbacks = [tf.keras.callbacks.EarlyStopping(
    monitor="val_accuracy", patience=3,
    restore_best_weights=True)]
history_2 = model_2.fit(train_data,
    epochs=20,
    validation_data=valid_data,
    callbacks=[callbacks])

在这里,我们将周期数指定为20,并添加了验证集来监控模型在训练过程中的表现。之后,我们使用callbacks参数指定了一个回调函数来实现早停。我们使用了一个早停回调,在验证集的精度未能提高时,训练将在三轮后停止。通过将patience参数设置为3来实现这一点。这意味着如果验证精度连续三轮没有进展,早停回调将停止训练。我们还将restore_best_weights参数设置为True;这将在训练结束时恢复训练过程中最好的模型权重。fit函数的信息存储在history_2变量中:

Epoch 8/20
25/25 [==============================] - 8s 318ms/step - loss: 0.0685 - accuracy: 0.9810 - val_loss: 0.3937 - val_accuracy: 0.8827
Epoch 9/20
25/25 [==============================] - 8s 325ms/step - loss: 0.0368 - accuracy: 0.9912 - val_loss: 0.3338 - val_accuracy: 0.9218
Epoch 10/20
25/25 [==============================] - 8s 316ms/step - loss: 0.0169 - accuracy: 0.9987 - val_loss: 0.4322 - val_accuracy: 0.8994
Epoch 11/20
25/25 [==============================] - 8s 297ms/step - loss: 0.0342 - accuracy: 0.9912 - val_loss: 0.2994 - val_accuracy: 0.8994
Epoch 12/20
25/25 [==============================] - 8s 318ms/step - loss: 0.1352 - accuracy: 0.9570 - val_loss: 0.4503 - val_accuracy: 0.8939

从训练过程来看,我们可以看到模型在第九个周期达到了0.9218的最高验证精度,之后训练继续进行了三轮才停止。由于验证精度没有进一步提升,训练被停止,并保存了最佳权重。现在,让我们在测试数据上评估model_2

model_2.evaluate(test_data)

当我们运行代码时,我们看到模型达到了0.9355的精度。在这里,测试集的表现与验证集的表现一致,并且高于我们的基准模型,后者的精度为0.9097。这是我们创建更好模型的第一步。

图 8.4 – 模型总结快照

图 8.4 – 模型总结快照

当我们检查模型总结时,我们可以看到我们的模型有超过 4500 万个参数,这可能导致模型容易在训练数据中拾取噪声,因为模型高度参数化。为了解决这个问题,我们可以通过减少参数数量来简化模型,使得我们的模型对于数据集来说不会过于复杂。接下来,我们将讨论模型简化。

模型简化

为了应对过拟合,你可以考虑重新评估模型的架构。简化模型的架构可能是应对过拟合的有效策略,特别是在模型高度参数化时。然而,重要的是要知道,这种方法并不总能在每种情况下保证更好的表现;事实上,你必须警惕模型过于简化,这可能导致欠拟合的陷阱。因此,重要的是在模型复杂性和简化之间找到合适的平衡,以实现最佳性能,如图 8.5所示,因为模型复杂性与过拟合之间的关系不是线性的。

图 8.5 – 机器学习中的过拟合和欠拟合

图 8.5 – 机器学习中的过拟合和欠拟合

模型简化可以通过多种方式实现——例如,我们可以用更小的滤波器替换大量的滤波器,或者我们还可以减少第一个 Dense 层中的神经元数量。在我们的架构中,你可以看到第一个全连接层有 1050 个神经元。作为模型简化实验的初步步骤,让我们将神经元数量减少到 500

    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(500, activation="relu"),
    tf.keras.layers.Dense(4, activation="softmax")
])

当我们编译并拟合模型时,我们的模型在验证集上达到了 0.9162 的最高准确率:

Epoch 5/50
25/25 [==============================] - 8s 300ms/step - loss: 0.1284 - accuracy: 0.9482 - val_loss: 0.4489 - val_accuracy: 0.8771
Epoch 6/50
25/25 [==============================] - 8s 315ms/step - loss: 0.1122 - accuracy: 0.9659 - val_loss: 0.2414 - val_accuracy: 0.9162
Epoch 7/50
25/25 [==============================] - 8s 327ms/step - loss: 0.0814 - accuracy: 0.9735 - val_loss: 0.2976 - val_accuracy: 0.9050
Epoch 8/50
25/25 [==============================] - 11s 441ms/step - loss: 0.0541 - accuracy: 0.9785 - val_loss: 0.2215 - val_accuracy: 0.9050
Epoch 9/50
25/25 [==============================] - 8s 313ms/step - loss: 0.1279 - accuracy: 0.9621 - val_loss: 0.2848 - val_accuracy: 0.8994

由于我们的验证准确率并没有更好,或许现在是时候尝试一些著名的想法来解决过拟合问题了。让我们在接下来的小节中看一下 L1 和 L2 正则化。我们将讨论它们如何工作,并将其应用到我们的案例研究中。

注意

模型简化的目标不是为了得到更小的模型,而是为了设计出一个能很好地泛化的模型。我们可能只需要减少不必要的层,或者通过改变激活函数,或者重新组织模型层的顺序和排列,以改善信息流动,从而简化模型。

L1 和 L2 正则化

正则化是一组通过向损失函数应用惩罚项来减少模型复杂性,从而防止过拟合的技术。正则化技术使模型对训练数据中的噪声更加抗干扰,从而提高了它对未见数据的泛化能力。正则化技术有多种类型,分别是 L1 和 L2 正则化。L1 和 L2 正则化是两种广为人知的正则化技术;L1 也可以称为 套索回归。在选择 L1 和 L2 时,重要的是要考虑我们所处理数据的类型。

当处理具有大量无关特征的数据时,L1 正则化非常有用。L1 中的惩罚项会导致一些系数变为零,从而减少在建模过程中使用的特征数量;这反过来减少了过拟合的风险,因为模型将基于较少的噪声数据进行训练。相反,当目标是创建具有小权重和良好泛化能力的模型时,L2 是一个非常好的选择。L2 中的惩罚项减少了系数的大小,防止它们变得过大,从而导致过拟合:

    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(1050, activation="relu",
        kernel_regularizer=regularizers.l2(0.01)),
    tf.keras.layers.Dense(4, activation="softmax")
    # binary activation output
])

当我们运行这个实验时,准确率大约为 92%,并没有比其他实验表现得更好。为了尝试 L1 正则化,我们只是将正则化方法从 L2 改为 L1。然而,在这种情况下,我们的结果并不好。因此,让我们尝试另一种叫做 dropout 正则化的正则化方法。

Dropout 正则化

神经网络的一个关键问题是共依赖性。共依赖性是神经网络中一种现象,当一组神经元,特别是同一层中的神经元,变得高度相关,以至于它们过度依赖彼此时,就会发生共依赖性。这可能导致它们放大某些特征,同时无法捕捉到数据中的其他重要特征。由于这些神经元同步工作,我们的模型更容易发生过拟合。为了减轻这一风险,我们可以应用一种称为 dropout 的技术。与 L1 和 L2 正则化不同,dropout 不会添加惩罚项,但顾名思义,在训练过程中我们会随机“丢弃”一部分神经元,如 图 8.6 所示,这有助于减少神经元之间的共依赖性,从而有助于防止过拟合。

图 8.6 – 应用了 dropout 的神经网络

图 8.6 – 应用了 dropout 的神经网络

当我们应用 dropout 技术时,模型被迫学习更鲁棒的特征,因为我们打破了神经元之间的共依赖性。然而,值得注意的是,当我们应用 dropout 时,训练过程可能需要更多的迭代才能达到收敛。让我们将 dropout 应用到我们的基础模型上,观察它的效果:

    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(1050, activation="relu"),
    tf.keras.layers.Dropout(0.6), # added dropout layer
    tf.keras.layers.Dense(4, activation="softmax")])

要在代码中实现 dropout,我们使用 tf.keras.layers.Dropout(0.6) 函数来指定 dropout 层。这会创建一个 dropout 层,dropout 率为 0.6 —— 即在训练过程中我们会关闭 60% 的神经元。值得注意的是,我们可以将 dropout 值设置在 0 和 1 之间:

25/25 [==============================] - 8s 333ms/step - loss: 0.3069 - accuracy: 0.8913 - val_loss: 0.2227 - val_accuracy: 0.9330
Epoch 6/10
25/25 [==============================] - 8s 317ms/step - loss: 0.3206 - accuracy: 0.8824 - val_loss: 0.1797 - val_accuracy: 0.9441
Epoch 7/10
25/25 [==============================] - 8s 322ms/step - loss: 0.2557 - accuracy: 0.9166 - val_loss: 0.2503 - val_accuracy: 0.8994
Epoch 8/10
25/25 [==============================] - 9s 339ms/step - loss: 0.1474 - accuracy: 0.9469 - val_loss: 0.2282 - val_accuracy: 0.9274
Epoch 9/10
25/25 [==============================] - 8s 326ms/step - loss: 0.2321 - accuracy: 0.9241 - val_loss: 0.3958 - val_accuracy: 0.8659

在这个实验中,我们的模型在验证集上达到了 0.9441 的最佳性能,提升了基础模型的表现。接下来,让我们看看调整学习率的效果。

调整学习率

第六章提高模型》中,我们讨论了学习率以及寻找最优学习率的重要性。在这个实验中,我们使用 0.0001 的学习率,这是通过尝试不同的学习率得到的一个良好结果,类似于我们在 第六章提高模型》中做的实验。在 第十三章使用 TensorFlow 进行时间序列、序列和预测》中,我们将研究如何应用自定义和内建的学习率调度器。这里,我们还应用了早停回调,以确保当模型无法再提高时,训练能够终止。让我们编译我们的模型:

# Compile the model
model_7.compile(loss="CategoricalCrossentropy",
    optimizer=tf.keras.optimizers.Adam(learning_rate=0.0001),
    metrics=["accuracy"])

我们将拟合模型并运行它。在七个 epoch 后,我们的模型训练停止,达到了验证集上的最佳性能 0.9274

Epoch 3/10
25/25 [==============================] - 8s 321ms/step - loss: 0.4608 - accuracy: 0.8508 - val_loss: 0.2776 - val_accuracy: 0.8994
Epoch 4/10
25/25 [==============================] - 8s 305ms/step - loss: 0.3677 - accuracy: 0.8824 - val_loss: 0.2512 - val_accuracy: 0.9274
Epoch 5/10
25/25 [==============================] - 8s 316ms/step - loss: 0.3143 - accuracy: 0.8925 - val_loss: 0.4450 - val_accuracy: 0.8324
Epoch 6/10
25/25 [==============================] - 8s 317ms/step - loss: 0.2749 - accuracy: 0.9052 - val_loss: 0.3427 - val_accuracy: 0.8603
Epoch 7/10
25/25 [==============================] - 8s 322ms/step - loss: 0.2241 - accuracy: 0.9279 - val_loss: 0.2996 - val_accuracy: 0.8659

我们已经探索了各种方法来改善我们的模型并克服过拟合问题。现在,让我们将焦点转向数据集本身,看看错误分析如何发挥作用。

错误分析

根据我们目前的结果,我们可以看到模型未能正确地将某些标签分类。为了进一步提高模型的泛化能力,最好检查模型所犯的错误,其背后的思想是揭示误分类数据中的模式,以便我们从查看误分类标签中获得的洞察可以用于改善模型的泛化能力。这种技术称为错误分析。进行错误分析时,我们首先通过识别验证/测试集中的误分类标签开始。接下来,我们将这些错误分组——例如,我们可以将模糊图像或光照条件差的图像归为一组。

基于从收集到的错误中获得的洞察,我们可能需要调整我们的模型架构或调整超参数,特别是当模型未能捕捉到某些特征时。此外,我们的错误分析步骤也可能会指出需要改善数据的大小和质量。解决这一问题的有效方法之一是应用数据增强,这是一种众所周知的技术,用于丰富我们的数据量和质量。接下来,让我们讨论数据增强并将其应用于我们的案例研究。

数据增强

图像数据增强是一种通过应用各种变换(例如旋转、翻转、裁剪和缩放)来增加我们训练集的大小和多样性的技术,从而创建新的合成数据,如图 8.7所示。对于许多实际应用来说,数据收集可能是一个非常昂贵且耗时的过程;因此,数据增强非常有用。数据增强帮助模型学习更具鲁棒性的特征,而不是让模型记住特征,从而提高模型的泛化能力。

图 8.7 – 应用于蝴蝶图像的各种数据增强技术(来源:https://medium.com/secure-and-private-ai-writing-challenge/data-augmentation-increases-accuracy-of-your-model-but-how-aa1913468722)

图 8.7 – 应用于蝴蝶图像的各种数据增强技术(来源:medium.com/secure-and-…

数据增强的另一个重要用途是为了在训练数据集中创建不同类别之间的平衡。如果训练集包含不平衡的数据,我们可以使用数据增强技术来创建少数类的变体,从而构建一个更加平衡的数据集,降低过拟合的可能性。在实施数据增强时,重要的是要牢记可能影响结果的各种因素。例如,使用哪种类型的数据增强取决于我们所处理的数据类型。

在图像分类任务中,诸如随机旋转、平移、翻转和缩放等技术可能会证明是有用的。然而,在处理数字数据集时,对数字应用旋转可能会导致意想不到的结果,比如将数字 6 旋转成 9。再者,翻转字母表中的字母,比如“b”和“d”,也可能带来不良影响。当我们对训练集应用图像增强时,考虑增强的幅度及其对训练数据质量的影响至关重要。过度增强可能导致图像严重失真,从而导致模型性能不佳。为防止这种情况的发生,监控模型的训练过程并使用验证集同样重要。

让我们对案例研究应用数据增强,看看我们的结果会是什么样子。

要实现数据增强,您可以使用 tf.keras.preprocessing.image 模块中的 ImageDataGenerator 类。这个类允许您指定一系列的变换,这些变换只应应用于训练集中的图像,并且它会在训练过程中实时生成合成图像。例如,您可以使用 ImageDataGenerator 类对训练图像应用旋转、翻转和缩放变换,方法如下:

train_datagen = ImageDataGenerator(rescale=1./255,
    rotation_range=25, zoom_range=0.3)
valid_datagen = ImageDataGenerator(rescale=1./255)
# Set up the train, validation, and test directories
train_dir = "/content/drive/MyDrive/weather dataset/train/"
val_dir = "/content/drive/MyDrive/weather dataset/validation/"
test_dir = "/content/drive/MyDrive/weather dataset/test/"
# Import data from directories and turn it into batches
train_data = train_datagen.flow_from_directory(
    train_dir,
    target_size=(224,224), # convert all images to be 224 x 224
    class_mode="categorical")
valid_data = valid_datagen.flow_from_directory(
    val_dir,
    target_size=(224,224),
    class_mode="categorical")
test_data = valid_datagen.flow_from_directory(
    test_dir,
    target_size=(224,224),
    class_mode="categorical",)

使用图像数据增强非常简单;我们为训练集、验证集和测试集创建了 keras.preprocessing.image 模块中的 ImageDataGenerator 类的三个实例。一个关键的区别是,我们在 train_datagen 对象中添加了 rotation_range=25zoom_range=0.3 参数。这样,在训练过程中,图像将随机旋转 25 度并缩放 0.3 倍,其他所有设置保持不变。

接下来,我们将构建、编译并拟合我们的基准模型,并应用早停技术,在增强数据上进行训练:

Epoch 4/20
25/25 [==============================] - 8s 308ms/step - loss: 0.2888 - accuracy: 0.9014 - val_loss: 0.3256 - val_accuracy: 0.8715
Epoch 5/20
25/25 [==============================] - 8s 312ms/step - loss: 0.2339 - accuracy: 0.9115 - val_loss: 0.2172 - val_accuracy: 0.9330
Epoch 6/20
25/25 [==============================] - 8s 320ms/step - loss: 0.1444 - accuracy: 0.9507 - val_loss: 0.2379 - val_accuracy: 0.9106
Epoch 7/20
25/25 [==============================] - 8s 315ms/step - loss: 0.1190 - accuracy: 0.9545 - val_loss: 0.2828 - val_accuracy: 0.9162
Epoch 8/20
25/25 [==============================] - 8s 317ms/step - loss: 0.0760 - accuracy: 0.9785 - val_loss: 0.3220 - val_accuracy: 0.8883

在八个训练周期后,我们的训练结束了。这次,我们在验证集上的得分达到了 0.9330。到目前为止,我们已经运行了七个不同的实验。接下来,让我们在测试集上测试这些模型,看看结果如何。为此,我们将编写一个辅助函数,创建一个 DataFrame,显示前五个模型、每个模型的名称,以及每个模型的损失和准确度,如 图 8.8 所示。

图 8.8 – 显示前五个模型的损失和准确度的 DataFrame

图 8.8 – 显示前五个模型的损失和准确度的 DataFrame

在我们的测试数据中,表现最好的模型是模型 7,它调整了学习率。我们已经讨论了一些在现实世界中用于解决图像分类过拟合问题的想法;然而,结合这些技术可以构建一个更简单但更强大的模型,从而减少过拟合的风险。通常来说,将多种技术结合起来遏制过拟合是一个好主意,因为这可能有助于生成一个更强大且更具泛化能力的模型。然而,重要的是要记住,没有一刀切的解决方案,最好的方法组合将取决于具体的数据和任务,并可能需要多次实验。

总结

在本章中,我们讨论了图像分类中的过拟合问题,并探索了克服它的不同技术。我们首先探讨了什么是过拟合以及为什么会发生过拟合,接着讨论了如何应用不同的技术,如提前停止、模型简化、L1 和 L2 正则化、dropout 以及数据增强来缓解图像分类任务中的过拟合问题。此外,我们还在天气数据集的案例研究中应用了这些技术,并通过实际操作观察了这些技术在案例中的效果。我们还探讨了将这些技术结合起来,以构建一个最优模型的过程。到目前为止,你应该已经对过拟合以及如何在自己的图像分类项目中减轻过拟合有了深入的了解。

在下一章中,我们将深入探讨迁移学习,这是一种强大的技术,能够让你利用预训练的模型来完成特定的图像分类任务,从而节省时间和资源,同时取得令人印象深刻的结果。

问题

让我们来测试一下本章学到的内容:

  1. 图像分类任务中的过拟合是什么?

  2. 过拟合是如何发生的?

  3. 可以使用哪些技术来防止过拟合?

  4. 什么是数据增强,如何利用它来防止过拟合?

  5. 如何通过数据预处理、数据多样性和数据平衡来缓解过拟合?

深入阅读

如需了解更多内容,您可以查看以下资源:

第九章:转移学习

过去十年在机器学习ML)领域最重要的进展之一是转移学习的概念,这一点当之无愧。转移学习是将从源任务中获得的知识应用到目标任务中的过程,目标任务与源任务不同但相关。这种方法不仅在节省训练深度神经网络所需的计算资源方面非常有效,而且在目标数据集较小的情况下也表现出色。转移学习通过重用从预训练模型中学到的特征,使我们能够构建性能更好的模型,并更快地达到收敛。由于其众多的优势,转移学习已经成为一个广泛研究的领域,许多研究探讨了转移学习在不同领域中的应用,如图像分类、目标检测、自然语言处理和语音识别等。

在本章中,我们将介绍转移学习的概念,探讨它是如何工作的,以及转移学习在不同应用场景中的一些最佳实践。我们将借助知名的预训练模型,在现实世界的应用中应用转移学习的概念。我们将看到如何将这些预训练模型作为特征提取器进行应用,并学习如何微调它们以实现最佳结果。在本章结束时,你将对转移学习有一个扎实的理解,并能有效地将其应用于构建现实世界的图像分类器。

在本章中,我们将涵盖以下主题:

  • 转移学习简介

  • 转移学习的类型

  • 使用转移学习构建现实世界的图像分类器

技术要求

我们将使用 Google Colab 进行编码练习,这需要 python >= 3.8.0,并且需要以下软件包,可以通过 pip install 命令进行安装:

  • tensorflow>=2.7.0

  • os

  • matplotlib >=3.4.0

  • pathlib

本书的代码包可以通过以下 GitHub 链接获取:github.com/PacktPublishing/TensorFlow-Developer-Certificate。所有习题的解答也可以在该 GitHub 仓库中找到。

转移学习简介

作为人类,我们很容易将从一个任务或活动中获得的知识转移到另一个任务中。例如,如果你精通 Python(编程语言,不是蛇),并且决定学习 Rust,凭借你在 Python 上的背景知识,你会发现学习 Rust 比没有任何编程语言基础的人要容易得多。这是因为一些概念,如面向对象编程,在不同的编程语言中有相似之处。转移学习遵循相同的原理。

迁移学习是一种技术,我们利用在任务 A上预训练的模型来解决一个不同但相关的任务 B。例如,我们使用在一个任务上训练的神经网络,并将获得的知识转移到多个相关任务中。在图像分类中,我们通常使用已经在非常大的数据集上训练的深度学习模型,比如 ImageNet,它由超过 1,000,000 张图像组成,涵盖 1,000 个类别。这些预训练模型获得的知识可以应用于许多不同的任务,例如在照片中分类不同品种的狗。就像我们因为懂得 Python 而能更快地学习 Rust 一样,迁移学习也适用——预训练的模型可以利用从源任务中获得的信息,并将其应用于目标任务,从而减少训练时间和对大量注释数据的需求,而这些数据可能在目标任务中不可得或难以收集。

迁移学习不仅限于图像分类任务;它还可以应用于其他深度学习任务,例如自然语言处理、语音识别和目标检测。在第十一章《使用 TensorFlow 进行 NLP》中,我们将把迁移学习应用于文本分类。在那里,我们将看到如何对在大规模文本语料库上训练的预训练模型(我们将从 TensorFlow Hub 获取)进行微调,从而进行文本分类。

在经典机器学习中,如在*图 9.1(a)*所示,我们为每个任务从头开始训练模型,正如我们在本书中迄今为止所做的那样。这种方法需要大量的资源和数据。

图 9.1 – 传统机器学习与迁移学习

图 9.1 – 传统机器学习与迁移学习

然而,研究人员发现,模型可以通过学习视觉特征,从一个庞大的数据集(如 ImageNet)中学习低级特征,并将这些特征应用于一个新的、相关的任务,如图 9.1(b)所示——例如,在我们在第八章《处理过拟合》中使用的天气数据集的分类中。通过应用迁移学习,我们可以利用模型在大数据集上训练过程中获得的知识,并有效地将其适应于解决不同但相关的任务。这种方法被证明是有用的,因为它不仅节省了训练时间和资源,还学会了提高性能,即使在目标任务可用数据有限的情况下。

迁移学习的类型

我们可以在卷积神经网络(CNN)中通过两种主要方式应用迁移学习。首先,我们可以将预训练模型作为特征提取器。在这种情况下,我们冻结卷积层的权重,以保留源任务中获得的知识,并添加一个新的分类器,该分类器用于第二任务的分类。这是可行的,因为卷积层是可重用的,它们只学习了低级特征,如边缘、角落和纹理,这些是通用的并且适用于不同的图像,如图 9**.2所示,而全连接层则用于学习高级细节,这些细节用于在照片中分类不同的物体。

图 9.2 – 迁移学习作为特征提取器

图 9.2 – 迁移学习作为特征提取器

迁移学习的第二种应用方法是解冻预训练模型的部分层,并添加一个分类器模型来识别高级特征,如图 9**.3所示。在这里,我们同时训练解冻的层和新的分类器。预训练模型作为新任务的起点,解冻层的权重与分类层一起微调,以使模型适应新任务。

图 9.3 – 迁移学习作为微调模型

图 9.3 – 迁移学习作为微调模型

预训练模型是已经在大数据集上训练过的深度网络。通过利用这些模型已经获得的知识和权重,我们可以将它们用作特征提取器,或通过较小的数据集和更少的训练时间对它们进行微调以适应我们的使用场景。迁移学习为机器学习实践者提供了访问最先进模型的途径,这些模型可以通过 TensorFlow 中的 API 快速、轻松地访问。这意味着我们不必总是从头开始训练我们的模型,从而节省时间和计算资源,因为微调模型比从头开始训练要快。

我们可以将预训练模型应用于相关的使用场景,从而可能提高准确性并加快收敛速度。然而,如果源领域和目标领域不相关,迁移学习可能不仅失败,还可能由于学习到的特征不相关,反而损害目标任务的性能,这种情况称为负迁移。让我们将迁移学习应用于一个真实世界的图像分类任务。我们将探索一些表现最好的预训练模型,如 VGG、Inception、MobileNetV2 和 EfficientNet。这些模型已经为图像分类任务进行了预训练。让我们看看它们在给定任务中的表现如何。

使用迁移学习构建一个真实世界的图像分类器

在这个案例研究中,你的公司获得了一个医疗项目,你被指派负责为 GETWELLAI 构建一个肺炎分类器。你已经获得了超过 5000 张 X 射线 JPEG 图像,包含两类(肺炎和正常)。数据集由专业医生标注,低质量的图像已被移除。让我们看看如何使用我们迄今为止讨论的两种迁移学习技术来解决这个问题。

加载数据

执行以下步骤以加载数据:

  1. 和往常一样,我们首先加载我们项目所需的必要库:

    #Import necessary libraries
    
    import os
    
    import pathlib
    
    import matplotlib.pyplot as plt
    
    import matplotlib.image as mpimg
    
    import random
    
    import numpy as np
    
    from PIL import Image
    
    import pandas as pd
    
    import tensorflow as tf
    
    from tensorflow import keras
    
    from tensorflow.keras.preprocessing.image import ImageDataGenerator
    
    from tensorflow.keras.callbacks import EarlyStopping
    
    from tensorflow.keras import regularizer
    
  2. 接下来,让我们加载 X 射线数据集。为此,我们将使用 wget 命令从指定的 URL 下载文件:

    !wget https://storage.googleapis.com/x_ray_dataset/dataset.zip
    
  3. 下载的文件作为 ZIP 文件保存在我们 Colab 实例的当前工作目录中,文件中包含 X 射线图像的数据集。

  4. 接下来,我们将通过运行以下代码来提取 zip 文件夹的内容:

    !unzip dataset.zip
    

当我们运行代码时,我们将提取一个名为 dataset 的文件夹,其中包含 testvaltrain 子目录,每个子目录中都有正常和肺炎 X 射线图像的数据,如 图 9.4 所示:

图 9.4 – 当前工作目录的快照,包含已提取的 ZIP 文件

图 9.4 – 当前工作目录的快照,包含已提取的 ZIP 文件

  1. 我们将使用以下代码块来提取子目录及其中文件的数量。我们在 第八章*,* 处理过拟合 中也看到了这段代码块:

    root_dir = "/content/dataset"
    
    for dirpath, dirnames, filenames in os.walk(root_dir):
    
        print(f"Directory: {dirpath}")
    
        print(f"Number of images: {len(filenames)}")
    
        print()
    

它为我们提供了每个文件夹中数据的快照,并让我们对数据的分布有一个大致了解。

  1. 接下来,我们将使用 view_random_images 函数从 train 目录显示一些随机图像及其形状:

    view_random_images(
    
        target_dir="/content/dataset/train",num_images=4)
    

当我们运行代码时,结果将类似于 图 9.5

图 9.5 – 从 X 射线数据集的训练样本中随机显示的图像

图 9.5 – 从 X 射线数据集的训练样本中随机显示的图像

  1. 我们将为训练数据和验证数据创建一个 ImageDataGenerator 类的实例。我们将添加 rescale 参数来重新调整图像的大小,确保所有像素值都在 0 到 1 之间。这样做是为了提高稳定性,并增强训练过程中的收敛性。生成的 train_datagenvalid_datagen 对象分别用于生成训练数据和验证数据的批次:

    train_datagen = ImageDataGenerator(rescale=1./255)
    
    valid_datagen = ImageDataGenerator(rescale=1./255)
    
  2. 接下来,我们设置 trainvalidationtest 目录。

    # Set up the train and test directories
    
    train_dir = "/content/dataset/train/"
    
    val_dir = "/content/dataset/val"
    
    test_dir = "/content/dataset/test"
    
  3. 我们使用flow_from_directory()方法从训练目录加载图像。target_size参数用于将所有图像调整为 224 x 224 像素。与我们在第八章**,过拟合处理中使用的代码相比,一个关键的不同是class_mode参数设置为binary,因为我们处理的是二分类问题(即正常和肺炎):

    train_data=train_datagen.flow_from_directory(
    
        train_dir,target_size=(224,224),
    
    # convert all images to be 224 x 224
    
        class_mode="binary")
    
    valid_data=valid_datagen.flow_from_directory(val_dir,
    
        target_size=(224,224),
    
        class_mode="binary",
    
        shuffle=False)
    
    test_data=valid_datagen.flow_from_directory(test_dir,
    
        target_size=(224,224),
    
        class_mode="binary",
    
        shuffle=False)
    

valid_datatest_data生成器与train_data生成器非常相似,因为它们的目标大小也设置为 224 x 224;关键区别在于它们将shuffle设置为false,这意味着图像不会被打乱。如果我们将其设置为true,图像将会被打乱。

建模

我们将从使用在第八章**,过拟合处理中应用的相同模型开始。为了避免重复,我们将专注于全连接层,在该层中,输出层有一个神经元,因为这是一个二分类任务。我们将与使用迁移学习的结果进行比较:

    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(1050, activation="relu"),
    tf.keras.layers.Dense(1, activation="sigmoid")
])
# Compile the model
model_1.compile(loss="binary_crossentropy",
    optimizer=tf.keras.optimizers.Adam(),
    metrics=["accuracy"])
#Fit the model
# Add an early stopping callback
callbacks = [tf.keras.callbacks.EarlyStopping(
    monitor="val_accuracy", patience=3,
    restore_best_weights=True)]
history_1 = model_1.fit(train_data,epochs=20,
    validation_data=valid_data,
    callbacks=[callbacks]

在这种情况下,输出层有一个神经元,并且我们将激活函数更改为 sigmoid 函数,因为我们正在构建一个二分类器。在编译步骤中,我们还将损失函数更改为二元交叉熵;其他部分保持不变。然后,我们进行模型拟合。

训练在第 7 个周期结束,因为验证损失未能进一步下降:

Epoch 4/20
163/163 [==============================] – 53s 324ms/step – loss: 0.0632 – accuracy: 0.9774 – val_loss: 0.0803 – val_accuracy: 1.0000
Epoch 5/20
163/163 [==============================] – 53s 324ms/step – loss: 0.0556 – accuracy: 0.9797 – val_loss: 0.0501 – val_accuracy: 1.0000
Epoch 6/20
163/163 [==============================] – 53s 323ms/step – loss: 0.0412 – accuracy: 0.9854 – val_loss: 0.1392 – val_accuracy: 0.8750
Epoch 7/20
163/163 [==============================] – 54s 334ms/step – loss: 0.0314 – accuracy: 0.9875 – val_loss: 0.2450 – val_accuracy: 0.8750

在第五个周期,模型达到了 100%的验证准确率,看起来很有希望。让我们评估一下模型:

model_1.evaluate(test_data)

当我们在测试数据上评估模型时,我们记录的准确率只有 0.7580。这表明模型可能存在过拟合的迹象。当然,我们可以尝试结合在第八章**,过拟合处理中学到的想法来提高模型的性能,并且鼓励你这么做。然而,让我们学习如何使用预训练模型,看看是否能够将这些模型获得的知识迁移到我们的应用场景中,并且如果可能的话,取得更好的结果。接下来我们就来做这个。

迁移学习建模

在本节中,我们将使用三种广泛应用的预训练 CNN 进行图像分类——VGG16、InceptionV3 和 MobileNet。我们将展示如何通过这些模型作为特征提取器应用迁移学习,接着添加一个全连接层进行标签分类。我们还将学习如何通过解冻部分层来微调预训练模型。在使用这些模型之前,我们需要导入它们。我们可以通过一行代码来实现:

from tensorflow.keras.applications import InceptionV3,
    MobileNet, VGG16, ResNet50

现在我们已经有了模型并准备好开始,让我们从 VGG16 开始。

VGG16

VGG16 是由牛津大学视觉几何组开发的 CNN 架构。它是在 ImageNet 数据集上训练的。VGG16 架构在 2014 年 ImageNet 挑战赛的图像分类类别中获得了第二名。VGG16 由 13 个(3 x 3 卷积核)卷积层,5 个(2x2)最大池化层和 3 个全连接层组成,如图 9.6所示。这使我们得到了 16 层可学习参数;请记住,最大池化层用于降维,它们没有权重。该模型接收 224 x 224 RGB 图像的输入张量。

图 9.6 – VGG16 模型架构(来源:https://medium.com/analytics-vidhya/car-brand-classification-using-vgg16-transfer-learning-f219a0f09765)

图 9.6 – VGG16 模型架构(来源:medium.com/analytics-v…

让我们开始从 Keras 加载 VGG16。我们想加载该模型并使用从 ImageNet 数据集获得的预训练权重。为此,我们将weights参数设置为imagenet;我们还将include_top参数设置为false。这样做是因为我们想将该模型用作特征提取器。通过这种方式,我们可以添加自定义的全连接层用于分类。我们将输入大小设置为(224,224,3),因为这是 VGG16 期望的输入图像大小:

# Instantiate the VGG16 model
vgg16 = VGG16(weights='imagenet', include_top=False,
    input_shape=(224, 224, 3))

下一步使我们能够冻结模型的权重,因为我们想要使用 VGG16 作为特征提取器。当我们冻结所有层时,这使它们变为不可训练,这意味着它们的权重在训练过程中不会更新:

# Freeze all layers in the VGG16 model
for layer in vgg16.layers:
    layer.trainable = False

下一个代码块创建了一个新的顺序模型,使用 VGG 作为其顶层,然后我们添加一个由 1,024 个神经元的密集层、一个 Dropout 层和一个输出层(包含一个神经元)组成的全连接层,并将激活函数设置为 sigmoid,以进行二分类:

# Create a new model on top of VGG16
model_4 = tf.keras.models.Sequential()
model_4.add(vgg16)
model_4.add(tf.keras.layers.Flatten())
model_4.add(tf.keras.layers.Dense(1024, activation='relu'))
model_4.add(tf.keras.layers.Dropout(0.5))
model_4.add(tf.keras.layers.Dense(1, activation='sigmoid'))

我们编译并将模型拟合到数据上:

# Compile the model
model_4.compile(optimizer='adam',
    loss='binary_crossentropy', metrics=['accuracy'])
# Fit the model
callbacks = [tf.keras.callbacks.EarlyStopping(
    monitor='val_accuracy', patience=3,
    restore_best_weights=True)]
history_4 = model_4.fit(train_data,
    epochs=20,
    validation_data=valid_data,
    callbacks=[callbacks]
    )

在四个时期后,我们的模型停止了训练。它达到了 0.9810 的训练准确率,但在验证集上,我们得到了 0.875 的准确率:

Epoch 1/20
163/163 [==============================] - 63s 360ms/step - loss: 0.2737 - accuracy: 0.9375 - val_loss: 0.2021 - val_accuracy: 0.8750
Epoch 2/20
163/163 [==============================] - 57s 347ms/step - loss: 0.0818 - accuracy: 0.9699 - val_loss: 0.4443 - val_accuracy: 0.8750
Epoch 3/20
163/163 [==============================] - 56s 346ms/step - loss: 0.0595 - accuracy: 0.9774 - val_loss: 0.1896 - val_accuracy: 0.8750
Epoch 4/20
163/163 [==============================] - 58s 354ms/step - loss: 0.0556 - accuracy: 0.9810 - val_loss: 0.4209 - val_accuracy: 0.8750

当我们评估模型时,达到了 84.29 的准确率。现在,让我们使用另一个预训练模型作为特征提取器。

MobileNet

MobileNet 是由谷歌的工程师开发的轻量级 CNN 模型。该模型轻巧高效,是开发移动和嵌入式视觉应用的首选模型。与 VGG16 类似,MobileNet 也在 ImageNet 数据集上进行了训练,并能够取得最先进的结果。MobileNet 采用了一种简化的架构,利用了深度可分离卷积。其基本理念是在保持准确率的同时,减少训练所需的参数数量。

为了将 MobileNet 作为特征提取器,步骤与我们刚才使用 VGG16 时类似;因此,让我们来看一下代码块。我们将加载模型,冻结层,并像之前一样添加一个全连接层:

# Instantiate the MobileNet model
mobilenet = MobileNet(weights='imagenet',
    include_top=False, input_shape=(224, 224, 3))
# Freeze all layers in the MobileNet model
for layer in mobilenet.layers:
    layer.trainable = False
# Create a new model on top of MobileNet
model_10 = tf.keras.models.Sequential()
model_10.add(mobilenet)
model_10.add(tf.keras.layers.Flatten())
model_10.add(tf.keras.layers.Dense(1024,activation='relu'))
model_10.add(tf.keras.layers.Dropout(0.5))
model_10.add(tf.keras.layers.Dense(1,activation='sigmoid'))

接下来,我们编译并拟合模型:

# Compile the model
model_10.compile(optimizer='adam',
    loss='binary_crossentropy', metrics=['accuracy'])
# Fit the model
callbacks = [tf.keras.callbacks.EarlyStopping(
    monitor='val_accuracy', patience=3,
    restore_best_weights=True)]
history_10 = model_10.fit(train_data,
    epochs=20,
    validation_data=valid_data,
    callbacks=[callbacks])

仅经过四个周期,模型达到了 87.50%的验证准确率:

Epoch 1/20
163/163 [==============================] - 55s 321ms/step - loss: 3.1179 - accuracy: 0.9402 - val_loss: 1.8479 - val_accuracy: 0.8750
Epoch 2/20
163/163 [==============================] - 51s 313ms/step - loss: 0.3896 - accuracy: 0.9737 - val_loss: 1.1031 - val_accuracy: 0.8750
Epoch 3/20
163/163 [==============================] - 52s 320ms/step - loss: 0.0795 - accuracy: 0.9896 - val_loss: 0.8590 - val_accuracy: 0.8750
Epoch 4/20
163/163 [==============================] - 52s 318ms/step - loss: 0.0764 - accuracy: 0.9877 - val_loss: 1.1536 - val_accuracy: 0.8750

接下来,让我们亲自尝试微调一个预训练模型。

将转移学习作为微调模型

InceptionV3 是 Google 开发的另一种 CNN 架构。它结合了 1x1 和 3x3 的滤波器,以捕捉图像的不同方面。让我们解冻一些层,这样我们就可以训练这些解冻的层以及全连接层。

首先,我们将加载 InceptionV3 模型。我们设置include_top=False以去除 InceptionV3 的分类层,并使用来自 ImageNet 的权重。我们通过将这些层的trainable设置为true来解冻最后 50 层。这使得我们能够在 X 光数据集上训练这些层:

# Load the InceptionV3 model
inception = InceptionV3(weights='imagenet',
    include_top=False, input_shape=(224, 224, 3))
# Unfreeze the last 50 layers of the InceptionV3 model
for layer in inception.layers[-50:]:
    layer.trainable = True

注意:

在小数据集上解冻和微调过多的层并不是一个好策略,因为这可能导致过拟合。

我们将像之前那样创建、拟合和编译模型,并且新模型在第五个周期达到了 100%的验证准确率:

Epoch 5/10
163/163 [==============================] - 120s 736ms/step - loss: 0.1168 - accuracy: 0.9584 - val_loss: 0.1150 - val_accuracy: 1.0000
Epoch 6/10
163/163 [==============================] - 117s 716ms/step - loss: 0.1098 - accuracy: 0.9624 - val_loss: 0.2713 - val_accuracy: 0.8125
Epoch 7/10
163/163 [==============================] - 123s 754ms/step - loss: 0.1011 - accuracy: 0.9613 - val_loss: 0.2765 - val_accuracy: 0.7500
Epoch 8/10
163/163 [==============================] - 120s 733ms/step - loss: 0.0913 - accuracy: 0.9668 - val_loss: 0.2711 - val_accuracy: 0.8125

接下来,让我们使用evaluate_models辅助函数评估这些模型:

图 9.7 – 我们实验的评估结果

图 9.7 – 我们实验的评估结果

图 9.7的结果来看,MobileNet、VGG16 和 InceptionV3 表现最佳。我们可以看到这些模型的表现远远超过了我们的基准模型(模型 1)。我们还报告了来自笔记本的其他一些模型的结果。我们能观察到过拟合的迹象;因此,你可以结合我们在第八章中讨论的处理过拟合的部分来改进结果。

总结

转移学习在深度学习社区中获得了广泛关注,因为它在构建深度学习模型时提高了性能、速度和准确度。我们讨论了转移学习的原理,并探索了将转移学习作为特征提取器和微调模型。我们使用表现最好的预训练模型构建了一些解决方案,并看到它们在应用于 X 光数据集时超越了我们的基准模型。

到目前为止,你应该已经对转移学习及其应用有了扎实的理解。掌握了这些知识后,你应该能够在为各种任务构建现实世界的深度学习解决方案时,将转移学习应用于特征提取器或微调模型。

到此,我们已结束本章及本书的这一部分。在下一章中,我们将讨论自然语言处理NLP),届时我们将使用 TensorFlow 构建令人兴奋的 NLP 应用。

问题

让我们测试一下本章所学内容:

  1. 使用测试笔记本,加载猫狗数据集。

  2. 使用图像数据生成器对图像数据进行预处理。

  3. 使用 VGG16 模型作为特征提取器,并构建一个新的 CNN 模型。

  4. 解冻 InceptionV3 模型的 40 层,并构建一个新的 CNN 模型。

  5. 评估 VGG16 和 InceptionV3 模型。

进一步阅读

要了解更多内容,您可以查看以下资源:

  • Kapoor, A., Gulli, A. 和 Pal, S. (2020) 与 TensorFlow 和 Keras 的深度学习第三版:构建和部署监督学习、无监督学习、深度学习和强化学习模型。Packt Publishing Ltd.

  • 将深度卷积神经网络适应于迁移学习:一项比较研究,由 C. M. B. Al-Rfou、G. Alain 和 Y. Bengio 编写,发表于 arXiv 预印本 arXiv:1511。

  • 非常深的卷积神经网络用于大规模图像识别,由 K. Simonyan 和 A. Zisserman 编写,发表于 2014 年 arXiv 预印本 arXiv:1409.1556。

  • EfficientNet:重新思考卷积神经网络模型缩放,由 M. Tan 和 Q. Le 编写,发表于 2019 年国际机器学习大会

  • MobileNetV2:反向残差和线性瓶颈,由 M. Sandler、A. Howard、M. Zhu、A. Zhmoginov 和 L. Chen 编写,发表于 2018 年 arXiv 预印本 arXiv:1801.04381。

  • DeCAF:一种用于通用视觉识别的深度卷积激活特征,由 Donahue, J., Jia, Y., Vinyals, O., Hoffman, J., Zhang, N., Tzeng, E. 和 Darrell, T.(2014)编写。

  • 利用迁移学习的力量进行医学图像分类,作者:Ryan Burke,Towards Data Sciencetowardsdatascience.com/harnessing-the-power-of-transfer-learning-for-medical-image-classification-fd772054fdc7

第三部分 – 使用 TensorFlow 进行自然语言处理

在本部分中,您将学习如何使用 TensorFlow 构建自然语言处理NLP)应用程序。您将了解如何进行文本处理,并构建文本分类模型。在本部分中,您还将学习如何使用 LSTM 生成文本。

本节包括以下章节:

  • 第十章自然语言处理简介

  • 第十一章使用 TensorFlow 进行 NLP