Python 深度学习第二版(GPT 重译)(二)

发布于:2024-03-21 ⋅ 阅读:(117) ⋅ 点赞:(0)

四、入门神经网络:分类和回归

本章涵盖

  • 您的第一个真实世界机器学习工作流示例

  • 处理矢量数据上的分类问题

  • 处理矢量数据上的连续回归问题

本章旨在帮助您开始使用神经网络解决实际问题。您将巩固从第二章和第三章中获得的知识,并将所学应用于三个新任务,涵盖神经网络的三种最常见用例 — 二元分类、多类分类和标量回归:

  • 将电影评论分类为正面或负面(二元分类)

  • 根据主题对新闻线进行分类(多类分类)

  • 给定房地产数据估计房屋价格(标量回归)

这些示例将是您与端到端机器学习工作流的第一次接触:您将介绍数据预处理、基本模型架构原则和模型评估。

分类和回归术语表

分类和回归涉及许多专门术语。您在早期示例中已经遇到了一些,您将在未来章节中看到更多。它们具有精确的、机器学习特定的定义,您应该熟悉它们:

  • Sampleinput — 进入您的模型的一个数据点。

  • Predictionoutput — 您的模型输出的内容。

  • Target — 真相。根据外部数据源,您的模型理想情况下应该预测的内容。

  • Prediction errorloss value — 您的模型预测与目标之间距离的度量。

  • Classes — 在分类问题中可供选择的可能标签集。例如,当对猫和狗图片进行分类时,“狗”和“猫”是两个类别。

  • Label — 分类问题中类别注释的特定实例。例如,如果图片 #1234 被注释为包含“狗”类,则“狗”是图片 #1234 的一个标签。

  • Ground-truthannotations — 数据集中的所有目标,通常由人类收集。

  • Binary classification — 一个分类任务,其中每个输入样本应该被分类到两个互斥的类别中。

  • Multiclass classification — 一个分类任务,其中每个输入样本应该被分类到两个以上的类别中:例如,分类手写数字。

  • Multilabel classification — 一个分类任务,其中每个输入样本可以被分配多个标签。例如,给定图像可能同时包含猫和狗,并且应该同时用“猫”标签和“狗”标签进行注释。每个图像的标签数量通常是可变的。

  • Scalar regression — 目标是一个连续标量值的任务。预测房价是一个很好的例子:不同的目标价格形成一个连续空间。

  • Vector regression — 目标是一组连续值的任务:例如,一个连续的矢量。如果您正在针对多个值进行回归(例如图像中边界框的坐标),那么您正在进行矢量回归。

  • Mini-batchbatch — 模型同时处理的一小组样本(通常在 8 到 128 之间)。样本数量通常是 2 的幂,以便在 GPU 上进行内存分配。在训练时,一个小批量用于计算应用于模型权重的单个梯度下降更新。

通过本章结束时,您将能够使用神经网络处理矢量数据上的简单分类和回归任务。然后,您将准备好在第五章开始构建更有原则、理论驱动的机器学习理解。

4.1 电影评论分类:一个二元分类示例

二元分类,或二元分类,是最常见的机器学习问题之一。在这个示例中,您将学习根据评论的文本内容将电影评论分类为正面或负面。

4.1.1 IMDB 数据集

您将使用 IMDB 数据集:来自互联网电影数据库的 50,000 条高度极化评论。它们被分为 25,000 条用于训练和 25,000 条用于测试的评论,每组评论包含 50% 的负面评论和 50% 的正面评论。

就像 MNIST 数据集一样,IMDB 数据集已经打包到 Keras 中。它已经经过预处理:评论(单词序列)已经转换为整数序列,其中每个整数代表字典中的特定单词。这使我们能够专注于模型构建、训练和评估。在第十一章中,您将学习如何从头开始处理原始文本输入。

以下代码将加载数据集(第一次运行时,将下载约 80 MB 的数据到您的计算机)。

列表 4.1 加载 IMDB 数据集

from tensorflow.keras.datasets import imdb
(train_data, train_labels), (test_data, test_labels) = imdb.load_data(
    num_words=10000)

参数num_words=10000表示您只会保留训练数据中出现频率最高的前 10,000 个单词。罕见单词将被丢弃。这使我们可以处理可管理大小的向量数据。如果我们不设置这个限制,我们将使用训练数据中的 88,585 个独特单词,这是不必要的庞大数量。其中许多单词只在一个样本中出现,因此无法有意义地用于分类。

变量train_datatest_data是评论列表;每个评论是一个单词索引列表(编码为单词序列)。train_labelstest_labels是 0 和 1 的列表,其中 0 代表负面,1 代表正面

>>> train_data[0]
[1, 14, 22, 16, ... 178, 32]
>>> train_labels[0]
1

因为我们限制自己只使用前 10,000 个最常见的单词,所以没有单词索引会超过 10,000:

>>> max([max(sequence) for sequence in train_data])
9999

为了好玩,这里是如何快速将其中一个评论解码回英文单词。

列表 4.2 将评论解码回文本

word_index = imdb.get_word_index()                                # ❶
reverse_word_index = dict(
    [(value, key) for (key, value) in word_index.items()])        # ❷
decoded_review = " ".join(
    [reverse_word_index.get(i - 3, "?") for i in train_data[0]])  # ❸

❶ word_index 是一个将单词映射到整数索引的字典。

❷ 将其反转,将整数索引映射到单词

❸ 解码评论。请注意,索引偏移了 3,因为 0、1 和 2 是“填充”、“序列开始”和“未知”保留索引。

4.1.2 准备数据

您不能直接将整数列表输入神经网络。它们的长度各不相同,但神经网络期望处理连续的数据批次。您必须将列表转换为张量。有两种方法可以做到这一点:

  • 填充列表,使它们的长度相同,将它们转换为形状为(samples, max_length)的整数张量,并从能够处理这种整数张量的层开始构建模型(Embedding层,我们稍后会详细介绍)。

  • 多热编码您的列表以将它们转换为 0 和 1 的向量。这意味着,例如,将序列[8, 5]转换为一个 10,000 维的向量,除了索引 8 和 5 外,其他都是 0,而索引 8 和 5 是 1。然后,您可以使用一个Dense层,能够处理浮点向量数据,作为模型中的第一层。

让我们选择后一种解决方案来对数据进行向量化,这样您可以最大程度地清晰地进行操作。

列表 4.3 通过多热编码对整数序列进行编码

import numpy as np 
def vectorize_sequences(sequences, dimension=10000): 
    results = np.zeros((len(sequences), dimension))   # ❶
    for i, sequence in enumerate(sequences):
        for j in sequence:
            results[i, j] = 1.                        # ❷
    return results
x_train = vectorize_sequences(train_data)             # ❸
x_test = vectorize_sequences(test_data)               # ❹

❶ 创建一个形状为(len(sequences), dimension)的全零矩阵

❷ 将结果[i]的特定索引设置为 1

❸ 向量化训练数据

❹ 向量化测试数据

现在样本看起来是这样的:

>>> x_train[0]
array([ 0.,  1.,  1., ...,  0.,  0.,  0.])

您还应该对标签进行向量化,这很简单:

y_train = np.asarray(train_labels).astype("float32")
y_test = np.asarray(test_labels).astype("float32")

现在数据已准备好输入神经网络。

4.1.3 构建您的模型

输入数据是向量,标签是标量(1 和 0):这是您可能会遇到的最简单的问题设置之一。在这样的问题上表现良好的模型类型是具有relu激活的一堆密集连接(Dense)层。

对于这样一堆Dense层,有两个关键的架构决策:

  • 要使用多少层

  • 选择每层使用多少个单元

在第五章,你将学习指导你做出这些选择的正式原则。目前,你将不得不相信我做出以下架构选择:

  • 两个中间层,每个有 16 个单元

  • 第三层将输出关于当前评论情感的标量预测

[外链图片转存中…(img-uZL0ZtEe-1710946537839)]

图 4.1 三层模型

图 4.1 展示了模型的外观。以下代码展示了 Keras 实现,类似于你之前看到的 MNIST 示例。

代码清单 4.4 模型定义

from tensorflow import keras 
from tensorflow.keras import layers

model = keras.Sequential([
    layers.Dense(16, activation="relu"),
    layers.Dense(16, activation="relu"),
    layers.Dense(1, activation="sigmoid")
])

传递给每个 Dense 层的第一个参数是层中的单元数:层的表示空间的维度。你从第二章和第三章记得,每个具有 relu 激活的 Dense 层实现以下张量操作链:

output = relu(dot(input, W) + b)

有 16 个单元意味着权重矩阵 W 的形状为 (input_dimension, 16):与 W 的点积将把输入数据投影到一个 16 维表示空间(然后你将添加偏置向量 b 并应用 relu 操作)。你可以直观地理解表示空间的维度为“模型在学习内部表示时允许的自由度有多大”。拥有更多单元(更高维的表示空间)允许你的模型学习更复杂的表示,但会使模型在计算上更昂贵,并可能导致学习不需要的模式(这些模式会提高训练数据的性能,但不会提高测试数据的性能)。

中间层使用 relu 作为它们的激活函数,最后一层使用 sigmoid 激活以输出一个概率(介于 0 和 1 之间的分数,指示样本有多大可能具有目标“1”:评论有多大可能是积极的)。relu(线性整流单元)是一个用于将负值归零的函数(参见图 4.2),而 sigmoid “压缩”任意值到 [0, 1] 区间(参见图 4.3),输出可以解释为概率。

[外链图片转存中…(img-CF16i01A-1710946537840)]

图 4.2 线性整流单元函数

最后,你需要选择一个损失函数和一个优化器。因为你面临的是一个二元分类问题,你的模型的输出是一个概率(你的模型以具有 sigmoid 激活的单单元层结束),最好使用 binary_crossentropy 损失。这并不是唯一可行的选择:例如,你可以使用 mean_squared_error。但是当你处理输出概率的模型时,交叉熵通常是最佳选择。交叉熵是信息论领域的一种量,用于衡量概率分布之间的距离,或者在这种情况下,地面实况分布和你的预测之间的距离。

[外链图片转存中…(img-hDTsCUY5-1710946537840)]

图 4.3 Sigmoid 函数

激活函数是什么,为什么它们是必要的?

没有像 relu 这样的激活函数(也称为非线性),Dense 层将由两个线性操作组成——点积和加法:

output = dot(input, W) + b

该层只能学习输入数据的线性变换(仿射变换):该层的假设空间将是将输入数据转换为 16 维空间的所有可能线性变换的集合。这样的假设空间太受限制,不会受益于多层表示,因为深度堆叠的线性层仍然实现线性操作:增加更多层不会扩展假设空间(正如你在第二章中看到的)。

为了获得一个更丰富的假设空间,从而受益于深度表示,你需要一个非线性或激活函数。relu 是深度学习中最流行的激活函数,但还有许多其他候选项,它们都有类似奇怪的名称:preluelu 等等。

至于优化器的选择,我们将选择rmsprop,这通常是几乎任何问题的一个很好的默认选择。

这是我们使用rmsprop优化器和binary_crossentropy损失函数配置模型的步骤。请注意,我们还将在训练过程中监视准确性。

列表 4.5 编译模型

model.compile(optimizer="rmsprop",
              loss="binary_crossentropy",
              metrics=["accuracy"])

4.1.4 验证您的方法

正如您在第三章中学到的,深度学习模型永远不应该在其训练数据上进行评估——在训练过程中使用验证集来监视模型的准确性是标准做法。在这里,我们将通过从原始训练数据中分离出 10,000 个样本来创建一个验证集。

列表 4.6 设置一个验证集

x_val = x_train[:10000]
partial_x_train = x_train[10000:]
y_val = y_train[:10000]
partial_y_train = y_train[10000:]

现在我们将在 512 个样本的小批量中对模型进行 20 个时代(对训练数据中的所有样本进行 20 次迭代)的训练。同时,我们将通过将验证数据作为validation_data参数传递来监视我们分离出的 10,000 个样本上的损失和准确性。

列表 4.7 训练您的模型

history = model.fit(partial_x_train,
                    partial_y_train,
                    epochs=20,
                    batch_size=512,
                    validation_data=(x_val, y_val))

在 CPU 上,每个时代不到 2 秒——训练在 20 秒内结束。在每个时代结束时,模型会在验证数据的 10,000 个样本上计算其损失和准确性,会有一个轻微的暂停。

请注意,对model.fit()的调用会返回一个History对象,就像您在第三章中看到的那样。这个对象有一个成员history,它是一个包含训练过程中发生的一切数据的字典。让我们来看一下:

>>> history_dict = history.history
>>> history_dict.keys()
[u"accuracy", u"loss", u"val_accuracy", u"val_loss"]

字典包含四个条目:每个在训练和验证期间监视的指标一个。在接下来的两个列表中,让我们使用 Matplotlib 将训练和验证损失并排绘制出来(参见图 4.4),以及训练和验证准确性(参见图 4.5)。请注意,由于模型的不同随机初始化,您自己的结果可能会略有不同。

[外链图片转存中…(img-Qps8vmZl-1710946537840)]

图 4.4 训练和验证损失

[外链图片转存中…(img-cSK9fPIj-1710946537841)]

图 4.5 训练和验证准确性

列表 4.8 绘制训练和验证损失

import matplotlib.pyplot as plt
history_dict = history.history
loss_values = history_dict["loss"]
val_loss_values = history_dict["val_loss"]
epochs = range(1, len(loss_values) + 1)
plt.plot(epochs, loss_values, "bo", label="Training loss")        # ❶
plt.plot(epochs, val_loss_values, "b", label="Validation loss")   # ❷
plt.title("Training and validation loss")
plt.xlabel("Epochs")
plt.ylabel("Loss")
plt.legend()
plt.show()

❶ “bo"代表"蓝色点”。

❷ “b"代表"实线蓝色线”。

列表 4.9 绘制训练和验证准确性

plt.clf()                           # ❶
acc = history_dict["accuracy"]
val_acc = history_dict["val_accuracy"]
plt.plot(epochs, acc, "bo", label="Training acc")
plt.plot(epochs, val_acc, "b", label="Validation acc")
plt.title("Training and validation accuracy")
plt.xlabel("Epochs")
plt.ylabel("Accuracy")
plt.legend()
plt.show()

❶ 清除图形

正如您所看到的,训练损失随着每个时代的进行而减少,而训练准确性则随着每个时代的进行而增加。这是在运行梯度下降优化时您所期望的情况——您试图最小化的量应该在每次迭代中都减少。但验证损失和准确性并非如此:它们似乎在第四个时代达到峰值。这是我们之前警告过的一个例子:在训练数据上表现更好的模型不一定会在以前从未见过的数据上表现更好。准确来说,您所看到的是过拟合:在第四个时代之后,您过度优化了训练数据,最终学习到的表示是特定于训练数据的,无法推广到训练集之外的数据。

在这种情况下,为了防止过拟合,您可以在四个时代后停止训练。一般来说,您可以使用一系列技术来减轻过拟合,我们将在第五章中介绍。

让我们从头开始训练一个新模型四个时代,然后在测试数据上评估它。

列表 4.10 从头开始重新训练模型

model = keras.Sequential([
    layers.Dense(16, activation="relu"),
    layers.Dense(16, activation="relu"),
    layers.Dense(1, activation="sigmoid")
])
model.compile(optimizer="rmsprop",
              loss="binary_crossentropy",
              metrics=["accuracy"])
model.fit(x_train, y_train, epochs=4, batch_size=512)
results = model.evaluate(x_test, y_test)

最终结果如下:

>>> results
[0.2929924130630493, 0.88327999999999995]    # ❶

❶ 第一个数字 0.29 是测试损失,第二个数字 0.88 是测试准确性。

这种相当天真的方法实现了 88%的准确性。使用最先进的方法,您应该能够接近 95%。

4.1.5 使用训练好的模型在新数据上生成预测

在训练完模型后,您会想要在实际环境中使用它。您可以使用predict方法生成评论为正面的可能性,就像您在第三章中学到的那样:

>>> model.predict(x_test)
array([[ 0.98006207]
       [ 0.99758697]
       [ 0.99975556]
       ...,
       [ 0.82167041]
       [ 0.02885115]
       [ 0.65371346]], dtype=float32)

正如您所看到的,模型对某些样本非常自信(0.99 或更高,或 0.01 或更低),但对其他样本不太自信(0.6、0.4)。

4.1.6 进一步的实验

以下实验将帮助您确信您所做的架构选择都是相当合理的,尽管仍有改进的空间:

  • 在最终分类层之前,您使用了两个表示层。尝试使用一个或三个表示层,看看这样做如何影响验证和测试准确性。

  • 尝试使用更多单元或更少单元的层:32 个单元,64 个单元等等。

  • 尝试使用mse损失函数而不是binary_crossentropy

  • 尝试使用tanh激活(这是早期神经网络中流行的激活函数)而不是relu

4.1.7 总结

这是您应该从这个示例中了解到的内容:

  • 通常,您需要对原始数据进行大量预处理,以便能够将其(作为张量)馈送到神经网络中。单词序列可以编码为二进制向量,但也有其他编码选项。

  • 具有relu激活的Dense层堆叠可以解决各种问题(包括情感分类),您可能经常会使用它们。

  • 在二元分类问题(两个输出类别)中,您的模型应该以一个具有一个单元和sigmoid激活的Dense层结束:您的模型的输出应该是一个介于 0 和 1 之间的标量,编码为概率。

  • 在二元分类问题上,具有标量 S 形输出的损失函数应该使用binary_crossentropy

  • rmsprop优化器通常是一个足够好的选择,无论您的问题是什么。这是您无需担心的一件事。

  • 随着神经网络在训练数据上变得更好,最终会开始过拟合,并且在从未见过的数据上获得越来越糟糕的结果。一定要始终监视在训练集之外的数据上的性能。

4.2 新闻线分类:一个多类别分类示例

在前一节中,您看到了如何使用密集连接的神经网络将向量输入分类为两个互斥类别。但是当您有两个以上的类别时会发生什么?

在本节中,我们将构建一个模型,将路透社新闻线分类为 46 个互斥主题。因为我们有很多类别,所以这个问题是多类别分类的一个实例,因为每个数据点应该被分类为一个类别,所以这个问题更具体地是单标签多类别分类的一个实例。如果每个数据点可以属于多个类别(在这种情况下是主题),我们将面临一个多标签多类别分类问题。

4.2.1 路透社数据集

您将使用路透社数据集,这是路透社在 1986 年发布的一组简短新闻线及其主题。这是一个简单、广泛使用的文本分类玩具数据集。有 46 个不同的主题;一些主题比其他主题更有代表性,但每个主题在训练集中至少有 10 个示例。

与 IMDB 和 MNIST 一样,路透社数据集作为 Keras 的一部分打包提供。让我们来看看。

列表 4.11 加载路透社数据集

from tensorflow.keras.datasets import reuters
(train_data, train_labels), (test_data, test_labels) = reuters.load_data(
    num_words=10000)

与 IMDB 数据集一样,参数num_words=10000将数据限制为数据中出现频率最高的 10,000 个单词。

您有 8,982 个训练示例和 2,246 个测试示例:

>>> len(train_data)
8982
>>> len(test_data)
2246

与 IMDB 评论一样,每个示例都是一个整数列表(单词索引):

>>> train_data[10]
[1, 245, 273, 207, 156, 53, 74, 160, 26, 14, 46, 296, 26, 39, 74, 2979,
3554, 14, 46, 4689, 4329, 86, 61, 3499, 4795, 14, 61, 451, 4329, 17, 12]

如果您感兴趣,这是如何将其解码回单词的方法。

列表 4.12 将新闻线解码回文本

word_index = reuters.get_word_index()
reverse_word_index = dict(
    [(value, key) for (key, value) in word_index.items()])
decoded_newswire = " ".join(
    [reverse_word_index.get(i - 3, "?") for i in     train_data[0]])   # ❶

❶ 请注意,索引偏移了 3,因为 0、1 和 2 是“填充”、“序列开始”和“未知”保留索引。

与示例相关联的标签是介于 0 和 45 之间的整数—一个主题索引:

>>> train_labels[10]
3

4.2.2 准备数据

您可以使用与前一个示例中完全相同的代码对数据进行向量化。

列表 4.13 对输入数据进行编码

x_train = vectorize_sequences(train_data)      # ❶
x_test = vectorize_sequences(test_data)        # ❷

❶ 向量化训练数据

❷ 向量化测试数据

要将标签向量化,有两种可能性:你可以将标签列表转换为整数张量,或者你可以使用独热编码。独热编码是一种广泛使用的分类数据格式,也称为分类编码。在这种情况下,标签的独热编码包括将每个标签嵌入为一个全零向量,其中标签索引的位置为 1。下面的列表显示了一个示例。

列表 4.14 编码标签

def to_one_hot(labels, dimension=46):
    results = np.zeros((len(labels), dimension))
    for i, label in enumerate(labels):
        results[i, label] = 1. 
    return results
y_train = to_one_hot(train_labels)     # ❶
y_test = to_one_hot(test_labels)       # ❷

❶ 向量化训练标签

❷ 向量化测试标签

请注意,Keras 中有一种内置的方法可以做到这一点:

from tensorflow.keras.utils import to_categorical
y_train = to_categorical(train_labels)
y_test = to_categorical(test_labels)

4.2.3 构建你的模型

这个主题分类问题看起来与之前的电影评论分类问题相似:在这两种情况下,我们都试图对短文本进行分类。但是这里有一个新的约束:输出类别的数量从 2 个增加到了 46 个。输出空间的维度大得多。

在像我们一直使用的Dense层堆叠中,每一层只能访问前一层输出中存在的信息。如果一层丢失了与分类问题相关的一些信息,这些信息将永远无法被后续层恢复:每一层都可能成为信息瓶颈。在前面的例子中,我们使用了 16 维的中间层,但 16 维的空间可能太有限,无法学习区分 46 个不同的类别:这样的小层可能充当信息瓶颈,永久丢失相关信息。

出于这个原因,我们将使用更大的层。让我们选择 64 个单元。

列表 4.15 模型定义

model = keras.Sequential([
    layers.Dense(64, activation="relu"),
    layers.Dense(64, activation="relu"),
    layers.Dense(46, activation="softmax")
])

还有两件事情你应该注意关于这个架构。

首先,我们用一个大小为 46 的Dense层结束模型。这意味着对于每个输入样本,网络将输出一个 46 维的向量。这个向量中的每个条目(每个维度)将编码一个不同的输出类别。

其次,最后一层使用了softmax激活函数。你在 MNIST 示例中看到了这种模式。这意味着模型将输出 46 个不同输出类别的概率分布,对于每个输入样本,模型将产生一个 46 维的输出向量,其中output[i]是样本属于类别i的概率。这 46 个分数将总和为 1。

在这种情况下使用的最佳损失函数是categorical_crossentropy。它衡量两个概率分布之间的距离:在这里,模型输出的概率分布与标签的真实分布之间的距离。通过最小化这两个分布之间的距离,你训练模型输出尽可能接近真实标签。

列表 4.16 编译模型

model.compile(optimizer="rmsprop",
              loss="categorical_crossentropy",
              metrics=["accuracy"])

4.2.4 验证你的方法

让我们在训练数据中留出 1,000 个样本作为验证集使用。

列表 4.17 设置一个验证集

x_val = x_train[:1000]
partial_x_train = x_train[1000:]
y_val = y_train[:1000]
partial_y_train = y_train[1000:]

现在,让我们训练模型 20 个周期。

列表 4.18 训练模型

history = model.fit(partial_x_train,
                    partial_y_train,
                    epochs=20,
                    batch_size=512,
                    validation_data=(x_val, y_val))

最后,让我们展示其损失和准确率曲线(见图 4.6 和 4.7)。

[外链图片转存中…(img-GIIbdlCe-1710946537841)]

图 4.6 训练和验证损失

[外链图片转存中…(img-zeZsDaxy-1710946537841)]

图 4.7 训练和验证准确率

列表 4.19 绘制训练和验证损失

loss = history.history["loss"]
val_loss = history.history["val_loss"]
epochs = range(1, len(loss) + 1)
plt.plot(epochs, loss, "bo", label="Training loss")
plt.plot(epochs, val_loss, "b", label="Validation loss")
plt.title("Training and validation loss")
plt.xlabel("Epochs")
plt.ylabel("Loss")
plt.legend()
plt.show()

列表 4.20 绘制训练和验证准确率

plt.clf()                          # ❶
acc = history.history["accuracy"]
val_acc = history.history["val_accuracy"]
plt.plot(epochs, acc, "bo", label="Training accuracy")
plt.plot(epochs, val_acc, "b", label="Validation accuracy")
plt.title("Training and validation accuracy")
plt.xlabel("Epochs")
plt.ylabel("Accuracy")
plt.legend()
plt.show()

❶ 清除图表

模型在九个周期后开始过拟合。让我们从头开始训练一个新模型,训练九个周期,然后在测试集上评估它。

列表 4.21 从头开始重新训练模型

model = keras.Sequential([
    layers.Dense(64, activation="relu"),
    layers.Dense(64, activation="relu"),
    layers.Dense(46, activation="softmax")
])
model.compile(optimizer="rmsprop",
              loss="categorical_crossentropy",
              metrics=["accuracy"])
model.fit(x_train,
          y_train,
          epochs=9,
          batch_size=512)
results = model.evaluate(x_test, y_test)

这里是最终结果:

>>> results
[0.9565213431445807, 0.79697239536954589]

这种方法达到了约 80%的准确率。对于一个平衡的二元分类问题,一个纯随机分类器达到的准确率将是 50%。但在这种情况下,我们有 46 个类别,它们可能不会被平等地表示。一个随机基线的准确率会是多少呢?我们可以尝试快速实现一个来进行经验性检查:

>>> import copy
>>> test_labels_copy = copy.copy(test_labels)
>>> np.random.shuffle(test_labels_copy)
>>> hits_array = np.array(test_labels) == np.array(test_labels_copy)
>>> hits_array.mean()
0.18655387355298308

正如你所看到的,一个随机分类器的分类准确率约为 19%,所以从这个角度看,我们模型的结果似乎相当不错。

4.2.5 在新数据上生成预测

在新样本上调用模型的predict方法会返回每个样本的 46 个主题的类概率分布。让我们为所有测试数据生成主题预测:

predictions = model.predict(x_test)

“predictions”中的每个条目都是长度为 46 的向量:

>>> predictions[0].shape
(46,)

这个向量中的系数总和为 1,因为它们形成一个概率分布:

>>> np.sum(predictions[0])
1.0

最大的条目是预测的类别——具有最高概率的类别:

>>> np.argmax(predictions[0])
4

4.2.6 处理标签和损失的另一种方式

我们之前提到另一种编码标签的方式是将它们转换为整数张量,就像这样:

y_train = np.array(train_labels)
y_test = np.array(test_labels)

这种方法唯一改变的是损失函数的选择。列表 4.21 中使用的损失函数categorical_crossentropy期望标签遵循分类编码。对于整数标签,你应该使用sparse_categorical_crossentropy

model.compile(optimizer="rmsprop",
              loss="sparse_categorical_crossentropy",
              metrics=["accuracy"])

这个新的损失函数在数学上仍然与categorical_crossentropy相同;它只是有一个不同的接口。

4.2.7 拥有足够大的中间层的重要性

我们之前提到,由于最终的输出是 46 维的,你应该避免中间层的单元远远少于 46。现在让我们看看当我们引入信息瓶颈时会发生什么,即通过具有明显低于 46 维的中间层,例如 4 维:

列表 4.22 具有信息瓶颈的模型

model = keras.Sequential([
    layers.Dense(64, activation="relu"),
    layers.Dense(4, activation="relu"),
    layers.Dense(46, activation="softmax")
])
model.compile(optimizer="rmsprop",
              loss="categorical_crossentropy",
              metrics=["accuracy"])
model.fit(partial_x_train,
          partial_y_train,
          epochs=20,
          batch_size=128,
          validation_data=(x_val, y_val))

现在模型的验证准确率达到了约 71%,绝对下降了 8%。这种下降主要是因为我们试图将大量信息(足以恢复 46 个类别的分离超平面的信息)压缩到一个过低维度的中间空间中。模型能够将大部分必要信息压缩到这些四维表示中,但并非全部。

4.2.8 进一步实验

就像前面的例子一样,我鼓励你尝试以下实验,以培养你对这类模型需要做出的配置决策的直觉:

  • 尝试使用更大或更小的层:32 个单元,128 个单元等。

  • 在最终的 softmax 分类层之前使用了两个中间层。现在尝试使用一个单独的中间层,或者三个中间层。

4.2.9 总结

这个例子给我们的启示是:

  • 如果你试图在N个类别中对数据点进行分类,你的模型应该以大小为NDense层结束。

  • 在单标签多类分类问题中,你的模型应该以softmax激活结束,这样它将输出关于N个输出类别的概率分布。

  • 对于这类问题,几乎总是应该使用分类交叉熵作为损失函数。它最小化了模型输出的概率分布与目标的真实分布之间的距离。

  • 在多类分类中有两种处理标签的方式:

    • 通过分类编码(也称为独热编码)对标签进行编码,并使用categorical_crossentropy作为损失函数

    • 将标签编码为整数并使用sparse_categorical_crossentropy损失函数

  • 如果你需要将数据分类到大量类别中,你应该避免由于中间层太小而在模型中创建信息瓶颈。

4.3 预测房价:回归示例

之前的两个例子被视为分类问题,目标是预测输入数据点的单个离散标签。另一种常见的机器学习问题是回归,它包括预测连续值而不是离散标签:例如,根据气象数据预测明天的温度,或者根据规格说明预测软件项目完成所需的时间。

注意不要混淆回归逻辑回归算法。令人困惑的是,逻辑回归并不是一个回归算法,而是一个分类算法。

4.3.1 波士顿房价数据集

在本节中,我们将尝试预测上世纪 70 年代中期波士顿郊区房屋的中位价格,根据当时有关该郊区的数据点,例如犯罪率、当地财产税率等。我们将使用的数据集与前两个示例有一个有趣的区别。它的数据点相对较少:仅有 506 个,分为 404 个训练样本和 102 个测试样本。输入数据中的每个特征(例如犯罪率)具有不同的比例。例如,一些值是比例,取值介于 0 和 1 之间,其他值介于 1 和 12 之间,其他值介于 0 和 100 之间,依此类推。

列表 4.23 加载波士顿房屋数据集

from tensorflow.keras.datasets import boston_housing
(train_data, train_targets), (test_data, test_targets) = (
    boston_housing.load_data())

让我们看一下数据:

>>> train_data.shape
(404, 13)
>>> test_data.shape
(102, 13)

正如你所看到的,我们有 404 个训练样本和 102 个测试样本,每个样本有 13 个数值特征,例如人均犯罪率、每个住宅的平均房间数、高速公路的可达性等。

目标值是占有住房的中位数值,以千美元为单位:

>>> train_targets
[ 15.2,  42.3,  50\. ...  19.4,  19.4,  29.1]

价格通常在$10,000 和$50,000 之间。如果听起来很便宜,请记住这是上世纪 70 年代中期,这些价格没有考虑通货膨胀。

4.3.2 准备数据

将取值范围差异很大的值输入神经网络可能会有问题。模型可能能够自动适应这种异质数据,但这肯定会使学习变得更加困难。处理这种数据的一种广泛最佳实践是进行特征归一化:对于输入数据中的每个特征(输入数据矩阵中的一列),我们减去该特征的均值并除以标准差,使得该特征以 0 为中心,具有单位标准差。这在 NumPy 中很容易实现。

列表 4.24 归一化数据

mean = train_data.mean(axis=0)
train_data -= mean
std = train_data.std(axis=0)
train_data /= std
test_data -= mean
test_data /= std

请注意,用于归一化测试数据的量是使用训练数据计算的。你绝对不应该在工作流程中使用在测试数据上计算的任何量,即使是像数据归一化这样简单的操作也不行。

4.3.3 构建你的模型

由于可用样本很少,我们将使用一个非常小的模型,其中包含两个中间层,每个层有 64 个单元。一般来说,训练数据越少,过拟合就会越严重,使用一个小模型是缓解过拟合的一种方法。

列表 4.25 模型定义

def build_model():
    model = keras.Sequential([              # ❶
        layers.Dense(64, activation="relu"),
        layers.Dense(64, activation="relu"),
        layers.Dense(1)
    ])
    model.compile(optimizer="rmsprop", loss="mse", metrics=["mae"])
    return model

❶ 因为我们需要多次实例化相同的模型,所以我们使用一个函数来构建它。

模型以一个单元结束,没有激活函数(它将是一个线性层)。这是标量回归的典型设置(一种回归,你试图预测一个单一连续值)。应用激活函数会限制输出的范围;例如,如果在最后一层应用sigmoid激活函数,模型只能学习预测 0 到 1 之间的值。在这里,因为最后一层是纯线性的,模型可以自由地学习预测任何范围内的值。

请注意,我们使用mse损失函数来编译模型—均方误差,即预测值与目标值之间的差的平方。这是回归问题中广泛使用的损失函数。

在训练过程中,我们还监控一个新的指标:平均绝对误差(MAE)。它是预测值与目标值之间的差的绝对值。例如,在这个问题上的 MAE 为 0.5 意味着你的预测平均偏差为$500。

4.3.4 使用 K 折验证验证你的方法

在我们继续调整模型参数(例如用于训练的时代数)的同时评估我们的模型,我们可以将数据分割为训练集和验证集,就像我们在之前的示例中所做的那样。但是由于数据点很少,验证集最终会变得非常小(例如,约 100 个示例)。因此,验证分数可能会根据我们选择用于验证和训练的数据点而变化很大:验证分数可能在验证拆分方面具有很高的方差。这将阻止我们可靠地评估我们的模型。

在这种情况下,最佳做法是使用K 折交叉验证(参见图 4.8)。

[外链图片转存中…(img-9MDZybmg-1710946537841)]

图 4.8 K=3 的 K 折交叉验证

它包括将可用数据分割为K个分区(通常K=4 或 5),实例化K个相同的模型,并在K-1 个分区上训练每个模型,同时在剩余分区上进行评估。然后使用的模型的验证分数是获得的K个验证分数的平均值。在代码方面,这很简单。

列表 4.26 K 折交叉验证

k = 4 
num_val_samples = len(train_data) // k
num_epochs = 100 
all_scores = [] 
for i in range(k):
    print(f"Processing fold #{i}")
    val_data = train_data[i * num_val_samples: (i + 1) * num_val_samples]   # ❶
    val_targets = train_targets[i * num_val_samples: (i + 1) * num_val_samples]
    partial_train_data = np.concatenate(                                    # ❷
        [train_data[:i * num_val_samples],
         train_data[(i + 1) * num_val_samples:]],
        axis=0)
    partial_train_targets = np.concatenate(
        [train_targets[:i * num_val_samples],
         train_targets[(i + 1) * num_val_samples:]],
        axis=0)
    model = build_model()                                                   # ❸
    model.fit(partial_train_data, partial_train_targets,                    # ❹
              epochs=num_epochs, batch_size=16, verbose=0)
    val_mse, val_mae = model.evaluate(val_data, val_targets, verbose=0)     # ❺
    all_scores.append(val_mae)

❶ 准备验证数据:来自分区#k 的数据

❷ 准备训练数据:来自所有其他分区的数据

❸ 构建 Keras 模型(已编译)

❹ 训练模型(静默模式,verbose = 0)

❺ 在验证数据上评估模型

使用num_epochs = 100运行此操作将产生以下结果:

>>> all_scores
[2.112449, 3.0801501, 2.6483836, 2.4275346]
>>> np.mean(all_scores)
2.5671294

不同的运行确实显示了相当不同的验证分数,从 2.1 到 3.1。平均值(2.6)比任何单个分数更可靠—这就是 K 折交叉验证的全部意义。在这种情况下,我们平均偏差为$2,600,考虑到价格范围为$10,000 到$50,000,这是一个显著的差距。

让我们尝试将模型训练更长一点:500 个时代。为了记录模型在每个时代的表现如何,我们将修改训练循环以保存每个折叠的每个时代验证分数日志。

列表 4.27 保存每个折叠的验证日志

num_epochs = 500 
all_mae_histories = [] 
for i in range(k):
    print(f"Processing fold #{i}")
    val_data = train_data[i * num_val_samples: (i + 1) * num_val_samples]   # ❶
    val_targets = train_targets[i * num_val_samples: (i + 1) * num_val_samples]
    partial_train_data = np.concatenate(                                    # ❷
        [train_data[:i * num_val_samples],
         train_data[(i + 1) * num_val_samples:]],
        axis=0)
    partial_train_targets = np.concatenate(
        [train_targets[:i * num_val_samples],
         train_targets[(i + 1) * num_val_samples:]],
        axis=0)
    model = build_model()                                                   # ❸
    history = model.fit(partial_train_data, partial_train_targets,          # ❹
                        validation_data=(val_data, val_targets),
                        epochs=num_epochs, batch_size=16, verbose=0)
    mae_history = history.history["val_mae"]
    all_mae_histories.append(mae_history)

❶ 准备验证数据:来自分区#k 的数据

❷ 准备训练数据:来自所有其他分区的数据

❸ 构建 Keras 模型(已编译)

❹ 训练模型(静默模式,verbose=0)

然后我们可以计算所有折叠的每个时代 MAE 分数的平均值。

列表 4.28 构建连续平均 K 折验证分数的历史

average_mae_history = [
    np.mean([x[i] for x in all_mae_histories]) for i in range(num_epochs)]

让我们绘制这个;参见图 4.9。

[外链图片转存中…(img-Khgg7THg-1710946537842)]

图 4.9 按时代划分的验证 MAE

列表 4.29 绘制验证分数

plt.plot(range(1, len(average_mae_history) + 1), average_mae_history)
plt.xlabel("Epochs")
plt.ylabel("Validation MAE")
plt.show()

由于缩放问题,可能有点难以阅读图表:前几个时代的验证 MAE 远高于后续数值。让我们省略前 10 个数据点,这些数据点与曲线的其余部分处于不同的比例尺。

列表 4.30 绘制验证分数,不包括前 10 个数据点

truncated_mae_history = average_mae_history[10:]
plt.plot(range(1, len(truncated_mae_history) + 1), truncated_mae_history)
plt.xlabel("Epochs")
plt.ylabel("Validation MAE")
plt.show()

正如您在图 4.10 中所看到的,验证 MAE 在 120-140 个时代后停止显着改善(这个数字包括我们省略的 10 个时代)。在那之后,我们开始过拟合。

[外链图片转存中…(img-VGII4j6P-1710946537842)]

图 4.10 按时代划分的验证 MAE,不包括前 10 个数据点

一旦您完成调整模型的其他参数(除了时代数,您还可以调整中间层的大小),您可以使用最佳参数在所有训练数据上训练最终的生产模型,然后查看其在测试数据上的表现。

列表 4.31 训练最终模型

model = build_model()                                # ❶
model.fit(train_data, train_targets,                 # ❷
          epochs=130, batch_size=16, verbose=0)
test_mse_score, test_mae_score = model.evaluate(test_data, test_targets)

❶ 获取一个新的、已编译的模型

❷ 在所有数据上对其进行训练

这是最终结果:

>>> test_mae_score
2.4642276763916016

我们仍然有一点不到$2,500 的差距。这是一个进步!就像前两个任务一样,您可以尝试改变模型中的层数或每层的单元数,看看是否可以减少测试误差。

4.3.5 在新数据上生成预测

在我们的二元分类模型上调用predict()时,我们为每个输入样本检索到介于 0 和 1 之间的标量分数。对于我们的多类分类模型,我们为每个样本检索到所有类别的概率分布。现在,对于这个标量回归模型,predict()返回模型对样本价格的猜测,单位为千美元:

>>> predictions = model.predict(test_data)
>>> predictions[0]
array([9.990133], dtype=float32)

测试集中的第一栋房子预测价格约为$10,000。

4.3.6 总结

从这个标量回归示例中,您应该得出以下结论:

  • 回归使用不同的损失函数进行,与我们用于分类的不同。均方误差(MSE)是回归常用的损失函数。

  • 同样,用于回归的评估指标与用于分类的评估指标不同;自然地,准确性的概念不适用于回归。常见的回归指标是平均绝对误差(MAE)。

  • 当输入数据中的特征具有不同范围的值时,每个特征应作为预处理步骤独立缩放。

  • 当数据量很少时,使用 K 折验证是可靠评估模型的好方法。

  • 当可用的训练数据很少时,最好使用只有少数中间层(通常只有一个或两个)的小型模型,以避免严重过拟合。

摘要

  • 在向量数据上,机器学习任务的三种最常见类型是二元分类、多类分类和标量回归。

    • 本章前面的“总结”部分总结了您对每个任务学到的重要知识点。

    • 回归使用不同的损失函数和不同的评估指标,与分类不同。

  • 在将原始数据输入神经网络之前,通常需要对其进行预处理。

  • 当您的数据具有不同范围的特征时,作为预处理的一部分,应独立缩放每个特征。

  • 随着训练的进行,神经网络最终开始过拟合,并在以前未见过的数据上获得更糟糕的结果。

  • 如果您没有太多的训练数据,可以使用只有一个或两个中间层的小型模型,以避免严重过拟合。

  • 如果您的数据被分成许多类别,如果将中间层设置得太小,可能会导致信息瓶颈。

  • 当您处理少量数据时,K 折验证可以帮助可靠评估您的模型。

五、机器学习的基础知识

本章涵盖

  • 理解泛化和优化之间的紧张关系,这是机器学习中的基本问题

  • 机器学习模型的评估方法

  • 改进模型拟合的最佳实践

  • 实现更好泛化的最佳实践

在第四章中的三个实际例子之后,你应该开始熟悉如何使用神经网络解决分类和回归问题,并且见证了机器学习的核心问题:过拟合。本章将把你对机器学习的一些新直觉形式化为一个坚实的概念框架,强调准确模型评估的重要性以及训练和泛化之间的平衡。

5.1 泛化:机器学习的目标

在第四章中提出的三个例子——预测电影评论、主题分类和房价回归——我们将数据分为训练集、验证集和测试集。很快就明显看到了不在训练数据上评估模型的原因:在几个周期后,从未见过的数据的性能开始与训练数据的性能分歧,而训练数据的性能始终随着训练的进行而改善。模型开始过拟合。过拟合在每个机器学习问题中都会发生。

机器学习中的基本问题是优化和泛化之间的紧张联系。优化指的是调整模型以在训练数据上获得最佳性能的过程(机器学习中的学习),而泛化指的是训练好的模型在从未见过的数据上的表现。当然,游戏的目标是获得良好的泛化,但你无法控制泛化;你只能将模型拟合到其训练数据。如果你做得太好,过拟合就会发生,泛化就会受到影响。

但是是什么导致了过拟合?我们如何实现良好的泛化?

5.1.1 欠拟合和过拟合

对于你在上一章中看到的模型,在保留验证数据上的性能随着训练的进行而改善,然后在一段时间后必然达到顶峰。这种模式(如图 5.1 所示)是普遍存在的。你会在任何模型类型和任何数据集中看到这种情况。

[外链图片转存中…(img-64pmf2uK-1710946537842)]

图 5.1 典型的过拟合行为

在训练开始时,优化和泛化是相关的:在训练数据上的损失越低,测试数据上的损失也越低。当这种情况发生时,你的模型被称为欠拟合:仍然有进步的空间;网络尚未对训练数据中的所有相关模式进行建模。但在对训练数据进行一定数量的迭代后,泛化停止改善,验证指标停滞然后开始恶化:模型开始过拟合。也就是说,它开始学习训练数据特定的模式,但这些模式在新数据方面是误导性的或无关的。

过拟合在数据存在噪声、不确定性或包含稀有特征时特别容易发生。让我们看看具体的例子。

嘈杂的训练数据

在现实世界的数据集中,一些输入无效是相当常见的。例如,一个 MNIST 数字可能是一张全黑的图片,或者像图 5.2 那样的东西。

[外链图片转存中…(img-BGRV5tvV-1710946537842)]

图 5.2 一些相当奇怪的 MNIST 训练样本

这些是什么?我也不知道。但它们都是 MNIST 训练集的一部分。然而,更糟糕的是,有些完全有效的输入最终被错误标记,就像图 5.3 中的那些一样。

[外链图片转存中…(img-EpfJ9bQZ-1710946537842)]

图 5.3 错标的 MNIST 训练样本

如果一个模型竭尽全力地纳入这些异常值,其泛化性能将会下降,就像图 5.4 中所示的那样。例如,一个看起来非常接近图 5.3 中错误标记的 4 的 4 可能最终被分类为 9。

[外链图片转存中…(img-vc51nbXQ-1710946537843)]

图 5.4 处理异常值:鲁棒拟合 vs. 过拟合

模糊特征

并非所有数据噪声都来自不准确性,即使是完全干净和整洁标记的数据,在涉及不确定性和模糊性的问题时也可能存在噪声。在分类任务中,通常情况下,输入特征空间的某些区域同时与多个类相关联。假设你正在开发一个模型,该模型接收香蕉的图像并预测香蕉是未熟、成熟还是腐烂。这些类别没有客观的界限,因此同一张图片可能会被不同的人类标记者分类为未熟或成熟。同样,许多问题涉及随机性。你可以使用大气压力数据来预测明天是否会下雨,但完全相同的测量有时可能会导致下雨,有时可能会导致晴天,具有一定的概率。

一个模型可能会对这种概率性数据过拟合,对特征空间中模糊区域过于自信,就像图 5.5 中那样。更鲁棒的拟合会忽略个别数据点,看到更大的图景。

[外链图片转存中…(img-nS3zSmgH-1710946537843)]

图 5.5 鲁棒拟合 vs. 过拟合给出特征空间中的模糊区域

稀有特征和虚假相关性

如果你一生中只见过两只橘色虎斑猫,而且它们都恰好非常不合群,你可能会推断橘色虎斑猫通常可能是不合群的。这就是过拟合:如果你接触到更多种类的猫,包括更多橘色的猫,你会发现猫的颜色与性格并没有很好的相关性。

同样,训练在包含稀有特征值的数据集上的机器学习模型极易过拟合。在情感分类任务中,如果训练数据中的单词“cherimoya”(一种原产于安第斯山脉的水果)只出现在一篇文本中,并且这篇文本恰好是负面情感的,一个调节不好的模型可能会对这个词赋予很高的权重,并且总是将提到 cherimoyas 的新文本分类为负面,然而,客观上,cherimoya 并没有什么负面的东西。

重要的是,一个特征值不需要只出现几次就会导致虚假相关性。考虑一个在你的训练数据中出现 100 次的单词,它与积极情感相关的概率为 54%,与消极情感相关的概率为 46%。这种差异很可能是一个完全的统计偶然,然而你的模型很可能会学会利用这个特征来进行分类任务。这是过拟合的最常见来源之一。

这里有一个引人注目的例子。以 MNIST 为例。通过将 784 个白噪声维度连接到现有数据的 784 个维度上,创建一个新的训练集,因此一半的数据现在是噪声。为了比较,还创建一个通过连接 784 个全零维度而得到的等效数据集。我们连接的无意义特征完全不影响数据的信息内容:我们只是在添加一些东西。人类分类准确度不会受到这些转换的影响。

列表 5.1 向 MNIST 添加白噪声通道或全零通道

from tensorflow.keras.datasets import mnist 
import numpy as np

(train_images, train_labels), _ = mnist.load_data()
train_images = train_images.reshape((60000, 28 * 28))
train_images = train_images.astype("float32") / 255 

train_images_with_noise_channels = np.concatenate(
    [train_images, np.random.random((len(train_images), 784))], axis=1)

train_images_with_zeros_channels = np.concatenate(
    [train_images, np.zeros((len(train_images), 784))], axis=1)

现在,让我们在这两个训练集上训练第二章中的模型。

列表 5.2 在带有噪声通道或全零通道的 MNIST 数据上训练相同的模型

from tensorflow import keras 
from tensorflow.keras import layers

def get_model():
    model = keras.Sequential([
        layers.Dense(512, activation="relu"),
        layers.Dense(10, activation="softmax")
    ])
    model.compile(optimizer="rmsprop",
                  loss="sparse_categorical_crossentropy",
                  metrics=["accuracy"])
    return model

model = get_model()
history_noise = model.fit(
    train_images_with_noise_channels, train_labels,
    epochs=10,
    batch_size=128,
    validation_split=0.2)

model = get_model()
history_zeros = model.fit(
    train_images_with_zeros_channels, train_labels,
    epochs=10,
    batch_size=128,
    validation_split=0.2)

让我们比较每个模型的验证准确性随时间的演变。

列表 5.3 绘制验证准确性比较

import matplotlib.pyplot as plt
val_acc_noise = history_noise.history["val_accuracy"]
val_acc_zeros = history_zeros.history["val_accuracy"]
epochs = range(1, 11)
plt.plot(epochs, val_acc_noise, "b-",
         label="Validation accuracy with noise channels")
plt.plot(epochs, val_acc_zeros, "b--",
         label="Validation accuracy with zeros channels")
plt.title("Effect of noise channels on validation accuracy")
plt.xlabel("Epochs")
plt.ylabel("Accuracy")
plt.legend()

尽管数据在两种情况下都包含相同的信息,但通过虚假相关性的影响,使用噪声通道训练的模型的验证准确性最终会降低约一个百分点(参见图 5.6)——纯粹是通过虚假相关性的影响。你添加的噪声通道越多,准确性就会进一步下降。

[外链图片转存中…(img-HOAYiCRs-1710946537843)]

图 5.6 噪声通道对验证准确性的影响

嘈杂的特征不可避免地导致过拟合。因此,在你不确定所拥有的特征是信息性的还是干扰性的情况下,通常在训练之前进行特征选择是很常见的。例如,将 IMDB 数据限制为最常见的前 10000 个单词就是一种粗糙的特征选择。进行特征选择的典型方法是为每个可用特征计算一些有用性评分——衡量特征相对于任务的信息性的度量,比如特征与标签之间的互信息——并且只保留高于某个阈值的特征。这样做将过滤掉前面示例中的白噪声通道。

5.1.2 深度学习中泛化性质的本质

深度学习模型的一个显著特点是,只要具有足够的表征能力,它们就可以被训练来拟合任何东西。

不相信?试着洗牌 MNIST 标签并在此基础上训练一个模型。尽管输入和洗牌标签之间没有任何关系,训练损失仍然可以很好地下降,即使是使用相对较小的模型。当然,由于在这种情况下没有泛化的可能性,验证损失不会随时间改善。

列表 5.4 使用随机洗牌标签拟合 MNIST 模型

(train_images, train_labels), _ = mnist.load_data()
train_images = train_images.reshape((60000, 28 * 28))
train_images = train_images.astype("float32") / 255 

random_train_labels = train_labels[:]
np.random.shuffle(random_train_labels)

model = keras.Sequential([
    layers.Dense(512, activation="relu"),
    layers.Dense(10, activation="softmax")
])
model.compile(optimizer="rmsprop",
              loss="sparse_categorical_crossentropy",
              metrics=["accuracy"])
model.fit(train_images, random_train_labels,
          epochs=100,
          batch_size=128,
          validation_split=0.2)

实际上,你甚至不需要用 MNIST 数据来做这个实验——你可以只生成白噪声输入和随机标签。只要模型有足够的参数,你也可以在这上面拟合一个模型。它最终只会记住特定的输入,就像一个 Python 字典一样。

如果是这样的话,那么深度学习模型到底是如何泛化的呢?它们难道不应该只是学习训练输入和目标之间的临时映射,就像一个高级dict一样吗?我们有什么期望这种映射会适用于新的输入呢?

事实证明,深度学习中的泛化性质与深度学习模型本身关系不大,而与现实世界中信息的结构有很大关系。让我们看看这里到底发生了什么。

流形假设

MNIST 分类器的输入(预处理之前)是一个 28×28 的整数数组,取值范围在 0 到 255 之间。因此,可能的输入值总数是 256 的 784 次方——远远大于宇宙中的原子数量。然而,这些输入中很少有看起来像有效 MNIST 样本的:实际手写数字只占据了所有可能的 28×28 uint8数组父空间中的一个微小子空间。而且,这个子空间不仅仅是在父空间中随机分布的一组点:它具有高度结构化。

首先,有效手写数字的子空间是连续的:如果你拿一个样本并稍微修改它,它仍然可以被识别为相同的手写数字。此外,所有有效子空间中的样本都通过平滑路径连接在一起。这意味着如果你拿两个随机的 MNIST 数字 A 和 B,存在一个“中间”图像序列,将 A 变形为 B,使得两个连续数字非常接近彼此(见图 5.7)。也许在两个类之间边界附近会有一些模糊的形状,但即使这些形状看起来仍然非常像数字。

[外链图片转存中…(img-licfhwXU-1710946537843)]

图 5.7 不同的 MNIST 数字逐渐变形成彼此,显示手写数字空间形成了一个“流形”。此图像是使用第十二章的代码生成的。

从技术角度来说,你会说手写数字形成了一个流形,位于可能的 28×28 uint8数组空间中。这是一个大词,但概念相当直观。一个“流形”是某个父空间中的低维子空间,局部类似于线性(欧几里得)空间。例如,在平面上的平滑曲线是 2D 空间中的 1D 流形,因为对于曲线的每个点,你都可以画出一个切线(曲线可以在每个点处用一条直线来近似)。在 3D 空间中的平滑曲面是 2D 流形。依此类推。

更一般地,流形假设认为所有自然数据都位于编码它的高维空间中的低维流形上。这是关于宇宙中信息结构的一个非常强烈的陈述。据我们所知,这是准确的,也是深度学习有效的原因。这对于 MNIST 数字是正确的,但也适用于人脸、树形态、人声和甚至自然语言。

流形假设意味着

  • 机器学习模型只需要适应潜在输入空间(潜在流形)中相对简单、低维、高度结构化的子空间。

  • 在这些流形中的一个中,总是可以在两个输入之间插值,也就是说,通过一个连续路径将一个变形为另一个,路径上的所有点都落在流形上。

在深度学习中,插值样本之间的能力是理解泛化的关键。

插值作为泛化的来源

如果你处理可以插值的数据点,你可以通过将它们与流形上靠近的其他点联系起来,开始理解以前从未见过的点。换句话说,你可以通过插值来填补空白,从而理解空间的整体

请注意,潜在流形上的插值与父空间中的线性插值是不同的,如图 5.8 所示。例如,在两个 MNIST 数字之间的像素的平均值通常不是一个有效的数字。

[外链图片转存中…(img-HklXaYOZ-1710946537843)]

图 5.8 线性插值和潜在流形上的插值之间的差异。数字的潜在流形上的每个点都是一个有效的数字,但两个数字的平均值通常不是。

至关重要的是,虽然深度学习通过在学习的数据流形上进行插值来实现泛化,但假设插值就是泛化的全部是错误的。这只是冰山一角。插值只能帮助你理解与之前看到的非常接近的事物:它实现了局部泛化。但值得注意的是,人类总是处理极端的新颖性,并且做得很好。你不需要事先在无数例子上接受训练,以便应对你将要遇到的每种情况。你每一天都与以往任何一天都不同,也与自人类诞生以来的任何一天都不同。你可以在纽约待一周,上海待一周,班加罗尔待一周,而无需为每个城市进行数千次的学习和排练。

人类能够进行极端泛化,这是由于插值之外的认知机制所实现的:抽象、世界的符号模型、推理、逻辑、常识、对世界的内在先验——我们通常称之为理性,与直觉和模式识别相对。后者在很大程度上是插值性质的,但前者不是。这两者对智能都是至关重要的。我们将在第十四章中更多地讨论这个问题。

为什么深度学习有效

还记得第二章中的揉皱纸球的比喻吗?一张纸代表了 3D 空间中的 2D 流形(见图 5.9)。深度学习模型是一种展开纸球的工具,也就是说,是为了解开潜在流形。

[外链图片转存中…(img-0O5k4SQH-1710946537844)]

图 5.9 展开复杂的数据流形

一个深度学习模型基本上是一个非常高维的曲线—一条平滑连续的曲线(受模型架构先验的额外约束),因为它需要是可微的。这条曲线通过梯度下降逐渐和增量地拟合到数据点。深度学习的本质是关于取一个大的、复杂的曲线—一个流形—并逐渐调整其参数,直到它拟合一些训练数据点。

曲线涉及足够多的参数,可以拟合任何东西——实际上,如果你让你的模型训练足够长的时间,它最终将纯粹地记忆其训练数据,根本无法泛化。然而,你要拟合的数据并不是由稀疏分布在基础空间中的孤立点组成。你的数据在输入空间内形成了一个高度结构化的低维流形—这就是流形假设。由于随着梯度下降的进行,将模型曲线拟合到这些数据中是逐渐平稳进行的,因此在训练过程中会有一个中间点,此时模型大致近似于数据的自然流形,正如你在图 5.10 中所看到的。

[外链图片转存中…(img-LpBm5HIJ-1710946537844)]

图 5.10 从随机模型到过拟合模型,以及作为中间状态实现稳健拟合

沿着模型在那一点学习的曲线移动将接近沿着数据的实际潜在流形移动—因此,模型将能够通过在训练输入之间进行插值来理解以前从未见过的输入。

除了它们具有足够的表征能力这一显而易见的事实外,深度学习模型具有一些特性使它们特别适合学习潜在流形:

  • 深度学习模型实现了从输入到输出的平滑连续映射。它必须是平滑和连续的,因为它必须是可微的,这是必然的(否则你无法进行梯度下降)。这种平滑性有助于近似潜在流形,这些流形具有相同的特性。

  • 深度学习模型往往以与其训练数据中信息“形状”相似的方式进行结构化(通过架构先验)。这特别适用于图像处理模型(在第八章和第九章讨论)和序列处理模型(第十章)。更一般地说,深度神经网络以分层和模块化的方式构建其学习表示,这与自然数据组织方式相呼应。

训练数据至关重要

虽然深度学习确实非常适合流形学习,但泛化的能力更多地是由数据的自然结构而不是模型的任何属性决定的。只有当你的数据形成一个可以进行插值的流形时,你才能进行泛化。你的特征越具信息性,噪声越少,你就越能进行泛化,因为你的输入空间将更简单、更有结构。数据筛选和特征工程对泛化至关重要。

此外,由于深度学习是曲线拟合,为了使模型表现良好,它需要在其输入空间上进行密集采样训练。在这种情况下,“密集采样”意味着训练数据应该密集覆盖整个输入数据流形(参见图 5.11)。这在决策边界附近尤为重要。通过足够密集的采样,可以通过在过去的训练输入之间进行插值来理解新的输入,而无需使用常识、抽象推理或关于世界的外部知识—这些是机器学习模型无法访问的东西。

[外链图片转存中…(img-St7XNyUO-1710946537844)]

图 5.11 为了学习一个能够准确泛化的模型,需要对输入空间进行密集采样。

因此,您应始终牢记改进深度学习模型的最佳方法是在更多或更好的数据上训练它(当然,添加过于嘈杂或不准确的数据将损害泛化能力)。输入数据流形的更密集覆盖将产生更好泛化的模型。您永远不应期望深度学习模型执行比其训练样本之间的粗略插值更多的操作,因此您应尽一切可能使插值变得更容易。您在深度学习模型中找到的唯一东西就是您放入其中的东西:编码在其架构中的先验和训练数据。

当无法获取更多数据时,下一个最佳解决方案是调节模型允许存储的信息量,或者对模型曲线的平滑性添加约束。如果一个网络只能记住少量模式,或者非常规律的模式,优化过程将迫使其专注于最突出的模式,这些模式更有可能泛化良好。这种通过这种方式对抗过拟合的过程称为正则化。我们将在第 5.4.4 节深入讨论正则化技术。

在开始调整模型以帮助其更好地泛化之前,您需要一种评估当前模型表现的方法。在接下来的部分中,您将学习如何在模型开发过程中监控泛化:模型评估。

5.2 评估机器学习模型

您只能控制您能观察到的内容。由于您的目标是开发能够成功泛化到新数据的模型,因此能够可靠地衡量模型泛化能力至关重要。在本节中,我将正式介绍您可以评估机器学习模型的不同方法。您在上一章中已经看到了其中大部分的应用。

5.2.1 训练、验证和测试集

评估模型总是归结为将可用数据分为三组:训练、验证和测试。您在训练数据上训练模型,并在验证数据上评估模型。一旦您的模型准备投入实际使用,您将最后一次在测试数据上测试它,这些数据应尽可能与生产数据相似。然后您可以将模型部署到生产环境中。

您可能会问,为什么它只有两组:一个训练集和一个测试集?您可以在训练数据上训练,并在测试数据上评估。简单得多!

原因在于,开发模型总是涉及调整其配置:例如,选择层数或层的大小(称为模型的超参数,以区别于参数,即网络的权重)。您通过使用模型在验证数据上的性能作为反馈信号来进行这种调整。本质上,这种调整是一种学习:在某个参数空间中寻找良好配置。因此,基于模型在验证集上的性能调整模型的配置可能很快导致过拟合验证集,即使您的模型从未直接在其上进行训练。

这种现象的核心是信息泄漏的概念。每当您根据模型在验证集上的性能调整模型的超参数时,一些关于验证数据的信息就会泄漏到模型中。如果您只这样做一次,针对一个参数,那么泄漏的信息量将很少,您的验证集将保持可靠,用于评估模型。但是,如果您多次重复这个过程——运行一个实验,在验证集上评估,并根据结果修改模型——那么您将泄漏越来越多关于验证集的信息到模型中。

最终,您将得到一个在验证数据上表现良好的模型,因为这是您优化的目标。您关心的是在全新数据上的表现,而不是在验证数据上的表现,因此您需要使用一个完全不同的、以前从未见过的数据集来评估模型:测试数据集。您的模型不应该有关于测试集的任何信息,甚至间接的。如果模型的任何部分基于测试集的性能进行调整,那么您的泛化度量将是有缺陷的。

将数据分成训练、验证和测试集可能看起来很简单,但在数据有限时,有一些高级方法可以派上用场。让我们回顾三种经典的评估方法:简单留出验证、K 折验证和具有洗牌功能的迭代 K 折验证。我们还将讨论使用常识基线来检查您的训练是否有所进展。

简单留出验证

将一部分数据作为测试集。在剩余数据上进行训练,并在测试集上进行评估。正如您在前面的部分中看到的,为了防止信息泄漏,您不应该根据测试集调整模型,因此您还应该保留一个验证集。

从示意图 5.12 的示意图上看,留出验证看起来像是。列表 5.5 显示了一个简单的实现。

[外链图片转存中…(img-FY0qTnrF-1710946537844)]

图 5.12 简单留出验证分割

列表 5.5 留出验证(为简单起见省略了标签)

num_validation_samples = 10000 
np.random.shuffle(data)                                   # ❶
validation_data = data[:num_validation_samples]           # ❷
training_data = data[num_validation_samples:]             # ❸
model = get_model()                                       # ❹
model.fit(training_data, ...)                             # ❹
validation_score = model.evaluate(validation_data, ...)   # ❹

...                                                       # ❺

model = get_model()                                       # ❻
model.fit(np.concatenate([training_data,                  # ❻
                          validation_data]), ...)         # ❻
test_score = model.evaluate(test_data, ...)               # ❻

❶ 通常适合对数据进行洗牌。

❷ 定义验证集

❸ 定义训练集

❹ 在训练数据上训练模型,并在验证数据上评估

❺ 在这一点上,您可以调整您的模型,重新训练它,评估它,再次调整它。

❻ 一旦调整了超参数,通常会从头开始在所有非测试数据上训练最终模型。

这是最简单的评估协议,但存在一个缺陷:如果可用的数据很少,那么您的验证和测试集可能包含的样本太少,无法统计代表手头的数据。这很容易识别:如果在分割之前对数据进行不同的随机洗牌轮次导致模型性能的度量值非常不同,那么您就会遇到这个问题。K 折验证和具有洗牌功能的迭代 K 折验证是解决这个问题的两种方法,接下来将讨论。

K 折验证

使用这种方法,将数据分成K个大小相等的分区。对于每个分区i,在剩余的K - 1个分区上训练模型,并在分区i上评估。然后,您的最终得分是获得的 K 个分数的平均值。当您的模型的性能根据训练-测试分割显示出显著变化时,这种方法是有帮助的。与留出验证一样,这种方法并不免除您使用一个不同的验证集进行模型校准。

从图 5.13 的示意图上看,K 折交叉验证看起来像是。列表 5.6 显示了一个简单的实现。

[外链图片转存中…(img-x8hgiGI5-1710946537844)]

图 5.13 K 折交叉验证,K=3

列表 5.6 K 折交叉验证(为简单起见省略了标签)

k = 3 
num_validation_samples = len(data) // k
np.random.shuffle(data)
validation_scores = [] 
for fold in range(k):
    validation_data = data[num_validation_samples * fold:        # ❶
                           num_validation_samples * (fold + 1)]  # ❶
    training_data = np.concatenate(                              # ❷
        data[:num_validation_samples * fold],                    # ❷
        data[num_validation_samples * (fold + 1):])              # ❷
    model = get_model()                                          # ❸
    model.fit(training_data, ...)
    validation_score = model.evaluate(validation_data, ...)
    validation_scores.append(validation_score)
validation_score = np.average(validation_scores)                 # ❹
model = get_model()                                              # ❺
model.fit(data, ...)                                             # ❺
test_score = model.evaluate(test_data, ...)                      # ❺

❶ 选择验证数据分区

❷ 使用剩余的数据作为训练数据。请注意,+ 运算符表示列表连接,而不是求和。

❸ 创建一个全新的模型实例(未经训练)

❹ 验证分数:k 个折叠的验证分数的平均值

❺ 在所有非测试数据上训练最终模型

具有洗牌功能的迭代 K 折验证

这个是用于在可用数据相对较少且需要尽可能精确评估模型的情况下。我发现在 Kaggle 竞赛中非常有帮助。它包括多次应用 K 折验证,在每次将数据随机洗牌后将其分成K份。最终得分是在每次 K 折验证运行中获得的得分的平均值。请注意,你最终会训练和评估P * K个模型(其中P是你使用的迭代次数),这可能非常昂贵。

5.2.2 打败常识基线

除了你可以使用的不同评估协议之外,你还应该了解的最后一件事是使用常识基线。

训练深度学习模型有点像按下一个按钮,在另一个平行世界中发射火箭。你听不到也看不到。你无法观察到流形学习过程—它发生在一个有数千维度的空间中,即使你将其投影到 3D,你也无法解释它。你唯一的反馈是你的验证指标—就像你看不见的火箭上的高度计。

能够判断你是否有所进展特别重要。你开始时的高度是多少?你的模型似乎有 15%的准确率—这算好吗?在开始处理数据集之前,你应该始终选择一个微不足道的基线来尝试超越。如果你超过了这个阈值,你就知道你做对了:你的模型实际上正在利用输入数据中的信息进行泛化预测,你可以继续前进。这个基线可以是随机分类器的性能,或者你能想象到的最简单的非机器学习技术的性能。

例如,在 MNIST 数字分类示例中,一个简单的基准是验证准确率大于 0.1(随机分类器);在 IMDB 示例中,它将是验证准确率大于 0.5。在 Reuters 示例中,由于类别不平衡,它将在 0.18-0.19 左右。如果你有一个二元分类问题,其中 90%的样本属于 A 类,10%属于 B 类,那么总是预测 A 的分类器在验证准确率方面已经达到 0.9,你需要做得比这更好。

在开始解决以前没有人解决过的问题时,拥有一个可以参考的常识基线是至关重要的。如果你无法击败一个微不足道的解决方案,你的模型是毫无价值的—也许你使用的是错误的模型,或者你正在处理的问题根本无法用机器学习方法解决。是时候重新审视问题了。

5.2.3 关于模型评估需要记住的事情

在选择评估协议时,请注意以下事项:

  • 数据代表性—你希望你的训练集和测试集都能代表手头的数据。例如,如果你试图对数字图像进行分类,并且从一个按类别排序的样本数组开始,将数组的前 80%作为训练集,剩下的 20%作为测试集,将导致你的训练集只包含类别 0-7,而测试集只包含类别 8-9。这似乎是一个荒谬的错误,但这种情况出奇地常见。因此,你通常应该在将数据拆分为训练集和测试集之前随机洗牌你的数据。

  • 时间的箭头—如果你试图根据过去来预测未来(例如,明天的天气、股票走势等),在将数据拆分之前不要随机洗牌,因为这样做会造成时间泄漏:你的模型实际上是在未来的数据上进行训练的。在这种情况下,你应该始终确保测试集中的所有数据都晚于训练集中的数据。

  • 数据中的冗余——如果你的数据中有一些数据点出现两次(在真实世界数据中很常见),那么对数据进行洗牌并将其分成训练集和验证集将导致训练集和验证集之间存在冗余。实际上,你将在部分训练数据上进行测试,这是你能做的最糟糕的事情!确保你的训练集和验证集是不相交的。

有一个可靠的方法来评估模型性能是你如何能够监控机器学习中的核心张力——在优化和泛化、欠拟合和过拟合之间。

5.3 改善模型拟合

要实现完美拟合,你必须首先过拟合。由于你事先不知道边界在哪里,你必须越过它找到它。因此,你在开始解决问题时的初始目标是获得一个显示一定泛化能力并能够过拟合的模型。一旦你有了这样的模型,你将专注于通过对抗过拟合来完善泛化。

在这个阶段你会遇到三个常见问题:

  • 训练无法开始:你的训练损失不会随时间降低。

  • 训练开始得很顺利,但你的模型并没有有意义地泛化:你无法击败你设定的常识基线。

  • 随着时间的推移,训练和验证损失都在下降,你可以击败你的基线,但似乎无法过拟合,这表明你仍然欠拟合。

让我们看看如何解决这些问题,以实现机器学习项目的第一个重要里程碑:获得具有一定泛化能力的模型(能够击败一个简单的基线)并且能够过拟合。

5.3.1 调整关键梯度下降参数

有时训练无法开始,或者过早停滞。你的损失停滞不前。这总是可以克服的:记住你可以将模型拟合到随机数据上。即使你的问题毫无意义,你仍然应该能够训练出一些东西——即使只是通过记忆训练数据。

当这种情况发生时,通常是由于梯度下降过程的配置问题:你选择的优化器、模型权重的初始值分布、学习率或批量大小。所有这些参数是相互依赖的,因此通常只需调整学习率和批量大小,同时保持其他参数不变即可。

让我们看一个具体的例子:让我们用值为 1 的不合适大学习率训练第二章的 MNIST 模型。

列表 5.7 使用不正确高学习率训练 MNIST 模型

(train_images, train_labels), _ = mnist.load_data()
train_images = train_images.reshape((60000, 28 * 28))
train_images = train_images.astype("float32") / 255 

model = keras.Sequential([
    layers.Dense(512, activation="relu"),
    layers.Dense(10, activation="softmax")
])
model.compile(optimizer=keras.optimizers.RMSprop(1.),
              loss="sparse_categorical_crossentropy",
              metrics=["accuracy"])
model.fit(train_images, train_labels,
          epochs=10,
          batch_size=128,
          validation_split=0.2)

该模型很快达到了 30%–40% 的训练和验证准确率,但无法超越这一范围。让我们尝试将学习率降低到一个更合理的值1e-2

列表 5.8 具有更合适学习率的相同模型

model = keras.Sequential([
    layers.Dense(512, activation="relu"),
    layers.Dense(10, activation="softmax")
])
model.compile(optimizer=keras.optimizers.RMSprop(1e-2),
              loss="sparse_categorical_crossentropy",
              metrics=["accuracy"])
model.fit(train_images, train_labels,
          epochs=10,
          batch_size=128,
          validation_split=0.2)

现在模型能够训练。

如果你发现自己处于类似情况,尝试

  • 降低或增加学习率。学习率过高可能导致更新远超适当拟合,就像前面的例子中一样,而学习率过低可能使训练过于缓慢,看起来停滞不前。

  • 增加批量大小。具有更多样本的批次将导致更具信息性和更少噪声的梯度(方差更低)。

最终,你会找到一个能够开始训练的配置。

5.3.2 利用更好的架构先验知识

你有一个适合的模型,但由于某种原因你的验证指标根本没有改善。它们仍然不比随机分类器获得的好:你的模型训练了,但泛化能力不强。发生了什么?

这可能是你会遇到的最糟糕的机器学习情况。这表明你的方法在根本上有问题,而且可能不容易判断。以下是一些建议。

首先,可能是你使用的输入数据根本不包含足够的信息来预测目标:问题的表述是不可解的。这就是之前当我们尝试拟合一个 MNIST 模型时发生的情况,其中标签被洗牌:模型训练得很好,但验证准确率停留在 10%,因为用这样的数据集明显不可能泛化。

也可能是你使用的模型类型不适合当前的问题。例如,在第十章中,你会看到一个时间序列预测问题的例子,其中一个密集连接的架构无法击败一个微不足道的基准线,而一个更合适的循环架构确实能够很好地泛化。使用对问题做出正确假设的模型对于实现泛化是至关重要的:你应该利用正确的架构先验知识。

在接下来的章节中,你将学习到用于各种数据模态(图像、文本、时间序列等)的最佳架构。一般来说,你应该始终确保阅读关于你正在攻击的任务类型的架构最佳实践——很可能你不是第一个尝试的人。

5.3.3 增加模型容量

如果你成功得到一个适合的模型,其中验证指标在下降,并且似乎至少达到了一定程度的泛化能力,恭喜你:你已经接近成功了。接下来,你需要让你的模型开始过拟合。

考虑以下的小模型——一个简单的逻辑回归——在 MNIST 像素上训练。

列表 5.9 在 MNIST 上的简单逻辑回归

model = keras.Sequential([layers.Dense(10, activation="softmax")])
model.compile(optimizer="rmsprop",
              loss="sparse_categorical_crossentropy",
              metrics=["accuracy"])
history_small_model = model.fit(
    train_images, train_labels,
    epochs=20,
    batch_size=128,
    validation_split=0.2)

你会得到类似于图 5.14 的损失曲线:

import matplotlib.pyplot as plt
val_loss = history_small_model.history["val_loss"]
epochs = range(1, 21)
plt.plot(epochs, val_loss, "b--",
         label="Validation loss")
plt.title("Effect of insufficient model capacity on validation loss")
plt.xlabel("Epochs")
plt.ylabel("Loss")
plt.legend()

[外链图片转存中…(img-pbgxiNbc-1710946537845)]

图 5.14 不足模型容量对损失曲线的影响

验证指标似乎停滞不前,或者改善非常缓慢,而不是达到峰值然后逆转。验证损失降至 0.26,然后停在那里。你可以拟合,但你无法明显过拟合,即使在对训练数据进行多次迭代后。你在职业生涯中很可能经常遇到类似的曲线。

记住,总是可以过拟合的。就像训练损失不下降的问题一样,这是一个总是可以解决的问题。如果你似乎无法过拟合,很可能是你的模型的表征能力的问题:你需要一个更大的模型,一个具有更多容量的模型,也就是说,能够存储更多信息的模型。你可以通过添加更多层、使用更大的层(具有更多参数的层)或使用更适合问题的层来增加表征能力(更好的架构先验)。

让我们尝试训练一个更大的模型,一个具有两个中间层,每个层有 96 个单元:

model = keras.Sequential([
    layers.Dense(96, activation="relu"),
    layers.Dense(96, activation="relu"),
    layers.Dense(10, activation="softmax"),
])
model.compile(optimizer="rmsprop",
              loss="sparse_categorical_crossentropy",
              metrics=["accuracy"])
history_large_model = model.fit(
    train_images, train_labels,
    epochs=20,
    batch_size=128,
    validation_split=0.2)

现在验证曲线看起来正是应该的:模型快速拟合,并在 8 个时期后开始过拟合(见图 5.15)。

[外链图片转存中…(img-MyYWoFFZ-1710946537845)]

图 5.15 具有适当容量的模型的验证损失

5.4 改善泛化能力

一旦你的模型展现出一定的泛化能力并且能够过拟合,就是时候将注意力转移到最大化泛化上了。

5.4.1 数据集整理

你已经学到了深度学习中泛化是源自数据的潜在结构。如果你的数据使得在样本之间平滑插值成为可能,你将能够训练一个泛化的深度学习模型。如果你的问题过于嘈杂或基本上是离散的,比如说,列表排序,深度学习将无法帮助你。深度学习是曲线拟合,而不是魔术。

因此,确保你正在使用一个合适的数据集是至关重要的。在数据收集上投入更多的精力和金钱几乎总是比在开发更好的模型上投入相同的精力和金钱产生更大的回报。

  • 确保你有足够的数据。记住你需要对输入-输出空间进行密集采样。更多的数据将产生更好的模型。有时,一开始看起来不可能的问题会随着更大的数据集而变得可解。

  • 最小化标记错误——可视化你的输入以检查异常,并校对你的标签。

  • 清洁你的数据并处理缺失值(我们将在下一章中介绍)。

  • 如果你有很多特征,而不确定哪些是真正有用的,那就进行特征选择。

提高数据泛化潜力的一个特别重要的方法是特征工程。对于大多数机器学习问题,特征工程是成功的关键因素。让我们来看看。

5.4.2 特征工程

特征工程是利用你对数据和手头的机器学习算法(在本例中是神经网络)的知识,通过在数据进入模型之前应用硬编码(非学习的)转换来使算法更好地工作的过程。在许多情况下,期望机器学习模型能够从完全任意的数据中学习是不合理的。数据需要以一种使模型工作更轻松的方式呈现给模型。

让我们看一个直观的例子。假设你正在开发一个模型,可以接受时钟的图像作为输入,并输出一天中的时间(见图 5.16)。

[外链图片转存中…(img-S5RJVWF9-1710946537845)]

图 5.16 读取时钟上时间的特征工程

如果你选择将图像的原始像素作为输入数据,那么你将面临一个困难的机器学习问题。你将需要一个卷积神经网络来解决它,并且需要耗费相当多的计算资源来训练网络。

但是如果你已经在高层次上理解了问题(你了解人类如何读取时钟面上的时间),你可以为机器学习算法想出更好的输入特征:例如,编写一个五行的 Python 脚本来跟踪时钟指针的黑色像素,并输出每个指针尖端的(x, y)坐标。然后一个简单的机器学习算法可以学会将这些坐标与适当的时间关联起来。

你甚至可以更进一步:进行坐标变换,将(x, y)坐标表示为相对于图像中心的极坐标。你的输入将变为每个时钟指针的角度theta。此时,你的特征使问题变得如此简单,以至于不需要机器学习;一个简单的四舍五入操作和字典查找就足以恢复大致的时间。

这就是特征工程的本质:通过以更简单的方式表达问题来使问题变得更容易。使潜在流形更加平滑、简单、更有组织。通常这需要深入理解问题。

在深度学习之前,特征工程曾经是机器学习工作流程中最重要的部分,因为经典的浅层算法没有足够丰富的假设空间来自动学习有用的特征。你向算法呈现数据的方式对其成功至关重要。例如,在卷积神经网络在 MNIST 数字分类问题上取得成功之前,解决方案通常基于硬编码的特征,如数字图像中的循环次数、图像中每个数字的高度、像素值的直方图等。

幸运的是,现代深度学习消除了大部分特征工程的需求,因为神经网络能够自动从原始数据中提取有用的特征。这是否意味着只要使用深度神经网络,你就不必担心特征工程了?不,有两个原因:

  • 优秀的特征仍然可以让您更优雅地解决问题,同时使用更少的资源。例如,使用卷积神经网络解决读取时钟面的问题是荒谬的。

  • 优秀的特征让您可以用更少的数据解决问题。深度学习模型学习特征的能力依赖于有大量的训练数据可用;如果只有少量样本,那么它们的特征中的信息价值就变得至关重要。

5.4.3 使用早停法

在深度学习中,我们总是使用远远超参数化的模型:它们的自由度远远超过拟合数据的潜在流形所需的最小自由度。这种过度参数化并不是问题,因为你永远不会完全拟合一个深度学习模型。这样的拟合根本不会泛化。你总是会在达到最小可能的训练损失之前中断训练。

找到训练过程中达到最具泛化性拟合的确切点——欠拟合曲线和过拟合曲线之间的确切边界——是改善泛化的最有效的事情之一。

在上一章的例子中,我们会先训练我们的模型比需要的时间更长,以找出产生最佳验证指标的时期数量,然后我们会重新训练一个新模型,确切地达到那个时期数量。这是相当标准的,但它要求你做冗余的工作,有时可能很昂贵。当然,你可以在每个时期结束时保存你的模型,一旦找到最佳时期,就重用你最接近的已保存模型。在 Keras 中,通常使用EarlyStopping回调来实现这一点,它会在验证指标停止改善时立即中断训练,同时记住已知的最佳模型状态。你将在第七章学习如何使用回调。

5.4.4 正则化您的模型

正则化技术是一组最佳实践,积极阻碍模型完美拟合训练数据的能力,目的是使模型在验证期间表现更好。这被称为“正则化”模型,因为它倾向于使模型更简单,更“规则”,其曲线更平滑,更“通用”;因此,它对训练集不那么特定,更能够通过更接近地逼近数据的潜在流形来泛化。

请记住,正则化模型是一个应该始终由准确的评估程序指导的过程。只有通过测量,您才能实现泛化。

让我们回顾一些最常见的正则化技术,并在实践中应用它们来改进第四章的电影分类模型。

减小网络的大小

您已经学到,一个太小的模型不会过拟合。缓解过拟合的最简单方法是减小模型的大小(模型中可学习参数的数量,由层数和每层单元的数量确定)。如果模型的记忆资源有限,它将无法简单地记住其训练数据;因此,为了最小化损失,它将不得不求助于学习具有关于目标的预测能力的压缩表示——这正是我们感兴趣的表示类型。同时,请记住,您应该使用具有足够参数的模型,以便它们不会欠拟合:您的模型不应该缺乏记忆资源。在容量过大容量不足之间需要找到一个折衷。

不幸的是,没有一个神奇的公式可以确定正确的层数或每个层的正确大小。你必须评估一系列不同的架构(当然是在验证集上,而不是在测试集上)以找到适合你数据的正确模型大小。找到合适模型大小的一般工作流程是从相对较少的层和参数开始,并增加层的大小或添加新层,直到看到验证损失的收益递减。

让我们尝试在电影评论分类模型上进行这个操作。以下列表显示了我们的原始模型。

列表 5.10 原始模型

from tensorflow.keras.datasets import imdb
(train_data, train_labels), _ = imdb.load_data(num_words=10000)

def vectorize_sequences(sequences, dimension=10000):
    results = np.zeros((len(sequences), dimension))
    for i, sequence in enumerate(sequences):
        results[i, sequence] = 1. 
    return results
train_data = vectorize_sequences(train_data)

model = keras.Sequential([
    layers.Dense(16, activation="relu"),
    layers.Dense(16, activation="relu"),
    layers.Dense(1, activation="sigmoid")
])
model.compile(optimizer="rmsprop",
              loss="binary_crossentropy",
              metrics=["accuracy"])
history_original = model.fit(train_data, train_labels,
                             epochs=20, batch_size=512, validation_split=0.4)

现在让我们尝试用这个较小的模型替换它。

列表 5.11 具有较低容量的模型版本

model = keras.Sequential([
    layers.Dense(4, activation="relu"),
    layers.Dense(4, activation="relu"),
    layers.Dense(1, activation="sigmoid")
])
model.compile(optimizer="rmsprop",
              loss="binary_crossentropy",
              metrics=["accuracy"])
history_smaller_model = model.fit(
    train_data, train_labels,
    epochs=20, batch_size=512, validation_split=0.4)

图 5.17 显示了原始模型和较小模型的验证损失的比较。

[外链图片转存中…(img-DR9yPI1H-1710946537845)]

图 5.17 IMDB 评论分类中原始模型与较小模型的比较

正如你所看到的,较小的模型开始过拟合的时间比参考模型晚(在六个周期而不是四个周期之后),一旦开始过拟合,其性能下降速度更慢。

现在,让我们在基准模型中添加一个容量更大的模型——远远超出问题所需的容量。虽然通常使用远超参数化的模型来学习是标准做法,但确实存在记忆容量过大的情况。如果你的模型开始过拟合并且其验证损失曲线看起来波动较大(尽管波动的验证指标也可能是使用不可靠的验证过程的症状,比如验证分割太小),那么你会知道你的模型太大了。

列表 5.12 具有更高容量的模型版本

model = keras.Sequential([
    layers.Dense(512, activation="relu"),
    layers.Dense(512, activation="relu"),
    layers.Dense(1, activation="sigmoid")
])
model.compile(optimizer="rmsprop",
              loss="binary_crossentropy",
              metrics=["accuracy"])
history_larger_model = model.fit(
    train_data, train_labels,
    epochs=20, batch_size=512, validation_split=0.4)

图 5.18 显示了较大模型与参考模型的比较。

[外链图片转存中…(img-jN2k8PVM-1710946537846)]

图 5.18 原始模型与 IMDB 评论分类中更大模型的比较

较大模型几乎立即开始过拟合,仅经过一个周期,过拟合程度更严重。其验证损失也更加嘈杂。它非常快地将训练损失降至接近零。模型容量越大,就越能快速对训练数据进行建模(导致训练损失较低),但也越容易过拟合(导致训练和验证损失之间的差异较大)。

添加权重正则化

你可能熟悉奥卡姆剃刀原理:对于某事的两种解释,最有可能正确的解释是最简单的解释——即做出更少假设的解释。这个想法也适用于神经网络学习的模型:在给定一些训练数据和网络架构的情况下,多组权重值(多个模型)可以解释数据。简单模型比复杂模型更不容易过拟合。

在这种情况下,简单模型是指参数值分布熵较低的模型(或者是参数较少的模型,正如你在前一节中看到的)。因此,减轻过拟合的常见方法是通过对模型的复杂性施加约束,强制其权重只取小值,这使得权重值的分布更规则。这被称为权重正则化,通过向模型的损失函数添加与权重较大相关的成本来实现。这个成本有两种形式:

  • L1 正则化—添加的成本与权重系数的绝对值成比例(权重的L1 范数)。

  • L2 正则化—添加的成本与权重系数的平方值成比例(权重的L2 范数)。在神经网络的背景下,L2 正则化也被称为权重衰减。不要让不同的名称使你困惑:数学上,权重衰减与 L2 正则化是相同的。

在 Keras 中,通过将权重正则化器实例作为关键字参数传递给层来添加权重正则化。让我们向我们最初的电影评论分类模型添加 L2 权重正则化。

清单 5.13 向模型添加 L2 权重正则化

from tensorflow.keras import regularizers
model = keras.Sequential([
    layers.Dense(16,
                 kernel_regularizer=regularizers.l2(0.002),
                 activation="relu"),
    layers.Dense(16,
                 kernel_regularizer=regularizers.l2(0.002),
                 activation="relu"),
    layers.Dense(1, activation="sigmoid")
])
model.compile(optimizer="rmsprop",
              loss="binary_crossentropy",
              metrics=["accuracy"])
history_l2_reg = model.fit(
    train_data, train_labels,
    epochs=20, batch_size=512, validation_split=0.4)

在前面的清单中,l2(0.002)表示层的权重矩阵中的每个系数将会为模型的总损失增加 0.002 * weight_coefficient_value ** 2。请注意,因为这种惩罚仅在训练时添加,所以该模型的损失在训练时会比在测试时高得多。

图 5.19 显示了 L2 正则化惩罚的影响。正如您所看到的,具有 L2 正则化的模型比参考模型更加抵抗过拟合,尽管两个模型具有相同数量的参数。

[外链图片转存中…(img-8FSyoNIx-1710946537846)]

图 5.19 L2 权重正则化对验证损失的影响

作为 L2 正则化的替代,您可以使用以下 Keras 权重正则化器之一。

清单 5.14 Keras 中可用的不同权重正则化器

from tensorflow.keras import regularizers
regularizers.l1(0.001)                    # ❶
regularizers.l1_l2(l1=0.001, l2=0.001)    # ❷

❶ L1 正则化

❷ 同时使用 L1 和 L2 正则化

请注意,权重正则化更常用于较小的深度学习模型。大型深度学习模型往往过于参数化,对权重值施加约束对模型容量和泛化能力的影响不大。在这些情况下,更倾向于使用不同的正则化技术:丢弃。

添加丢弃

Dropout 是神经网络中最有效和最常用的正则化技术之一;它是由杰夫·辛顿及其多伦多大学的学生开发的。应用于层的 Dropout 在训练期间会随机丢弃(设置为零)层的一些输出特征。假设给定层在训练期间对于给定输入样本会返回一个向量 [0.2, 0.5, 1.3, 0.8, 1.1]。应用 Dropout 后,这个向量将随机分布一些零条目:例如,[0, 0.5, 1.3, 0, 1.1]丢弃率是被置零的特征的分数;通常设置在 0.2 和 0.5 之间。在测试时,没有单位被丢弃;相反,层的输出值会按照与丢弃率相等的因子进行缩放,以平衡训练时更多单位处于活动状态的事实。

考虑一个包含层输出 layer_output 的 NumPy 矩阵,形状为 (batch_size, features)。在训练时,我们随机将矩阵中的一部分值置零:

layer_output *= np.random.randint(0, high=2, size=layer_output.shape)  # ❶

❶ 在训练时,输出中的单位有 50% 被丢弃

在测试时,我们通过丢弃率缩小输出。在这里,我们缩小了 0.5(因为之前我们丢弃了一半的单位):

layer_output *= 0.5    # ❶

❶ 在测试时

请注意,这个过程可以通过在训练时执行这两个操作并在测试时保持输出不变来实现,这通常是实践中的实现方式(参见图 5.20):

layer_output *= np.random.randint(0, high=2, size=layer_output.shape)   # ❶
layer_output /= 0.5                                                     # ❷

❶ 在训练时

❷ 请注意,在这种情况下我们是放大而不是缩小。

[外链图片转存中…(img-SB9MOjut-1710946537846)]

图 5.20 在训练时应用丢弃到激活矩阵,训练期间进行重新缩放。在测试时,激活矩阵保持不变。

这种技术可能看起来奇怪而武断。为什么这有助于减少过拟合?辛顿说,他受到了银行使用的防欺诈机制的启发,其中包括其他事物。他自己的话是:“我去了我的银行。出纳员经常变动,我问其中一个原因。他说他不知道,但他们经常换岗。我想这一定是因为需要员工之间的合作才能成功欺诈银行。这让我意识到,随机地在每个示例中删除不同的神经元子集将防止阴谋,从而减少过拟合。” 核心思想是在层的输出值中引入噪声可以打破不重要的偶然模式(辛顿称之为阴谋),如果没有噪声,模型将开始记忆。

在 Keras 中,您可以通过Dropout层在模型中引入辍学,该层应用于其前一层的输出。让我们在 IMDB 模型中添加两个Dropout层,看看它们在减少过拟合方面的效果如何。

列表 5.15 向 IMDB 模型添加辍学

model = keras.Sequential([
    layers.Dense(16, activation="relu"),
    layers.Dropout(0.5),
    layers.Dense(16, activation="relu"),
    layers.Dropout(0.5),
    layers.Dense(1, activation="sigmoid")
])
model.compile(optimizer="rmsprop",
              loss="binary_crossentropy",
              metrics=["accuracy"])
history_dropout = model.fit(
    train_data, train_labels,
    epochs=20, batch_size=512, validation_split=0.4)

图 5.21 显示了结果的图表。这明显比参考模型有所改进——它似乎也比 L2 正则化效果更好,因为达到的最低验证损失有所改善。

[外链图片转存中…(img-RxG2hQMH-1710946537846)]

图 5.21 辍学对验证损失的影响

总结一下,以下是在神经网络中最大化泛化并防止过拟合的最常见方法:

  • 获取更多训练数据,或更好的训练数据。

  • 开发更好的特征。

  • 减少模型的容量。

  • 添加权重正则化(适用于较小的模型)。

  • 添加辍学。

总结

  • 机器学习模型的目的是泛化:在以前未见过的输入上准确执行。这比看起来更难。

  • 深度神经网络通过学习一个能够成功插值训练样本之间的参数模型来实现泛化——这样的模型可以说已经学会了训练数据的“潜在流形”。这就是为什么深度学习模型只能理解与训练时非常接近的输入。

  • 机器学习中的基本问题是优化和泛化之间的紧张关系:要实现泛化,您必须首先对训练数据进行良好拟合,但随着时间的推移,改进模型对训练数据的拟合将不可避免地开始损害泛化。每一个深度学习最佳实践都涉及管理这种紧张关系。

  • 深度学习模型泛化的能力来自于它们设法学习逼近其数据的潜在流形,因此可以通过插值理解新的输入。

  • 在开发模型时,准确评估模型的泛化能力至关重要。您可以使用各种评估方法,从简单的留出验证到 K 折交叉验证和带有洗牌的迭代 K 折交叉验证。请记住,始终保留一个完全独立的测试集用于最终模型评估,因为您的验证数据可能会泄漏到模型中。

  • 当您开始处理模型时,您的目标首先是实现具有一定泛化能力且可能过拟合的模型。实现此目标的最佳实践包括调整学习率和批量大小,利用更好的架构先验,增加模型容量,或者简单地延长训练时间。

  • 当您的模型开始过拟合时,您的目标转为通过模型正则化来改善泛化。您可以减少模型的容量,添加辍学或权重正则化,并使用提前停止。自然地,更大或更好的数据集始终是帮助模型泛化的首要方法。


¹ 马克·吐温甚至称之为“人类所知最美味的水果”。

六、机器学习的通用工作流程

本章涵盖

  • 制定机器学习问题的步骤

  • 开发一个可工作模型的步骤

  • 部署模型到生产环境并进行维护的步骤

我们先前的示例假设我们已经有一个标记的数据集可以开始训练模型。在现实世界中,这通常不是这样。您不是从数据集开始,而是从问题开始。

想象一下,您正在开设自己的机器学习咨询公司。您注册公司,建立一个漂亮的网站,通知您的网络。项目开始涌入:

  • 为图片分享社交网络设计的个性化照片搜索引擎——输入“婚礼”并检索您在婚礼上拍摄的所有照片,无需任何手动标记。

  • 在新兴聊天应用的帖子中标记垃圾邮件和冒犯性文本内容。

  • 为在线广播用户构建音乐推荐系统。

  • 为电子商务网站检测信用卡欺诈。

  • 预测显示广告的点击率,以决定在特定时间向特定用户提供哪个广告。

  • 在饼干制造线上的传送带上标记异常的饼干。

  • 使用卫星图像预测尚未知晓的考古遗址的位置。

伦理注意事项

有时您可能会被提供道德上可疑的项目,比如“构建一个从某人面部照片中评估其信任度的 AI”。首先,该项目的有效性存疑:不清楚为什么信任度会反映在某人的脸上。其次,这样的任务打开了各种道德问题的大门。为这个任务收集数据集将等同于记录标记图片的人的偏见和成见。您将在这些数据上训练的模型只会将这些偏见编码到一个黑盒算法中,给予它们一层薄薄的合法性外衣。在我们这样一个技术素养较低的社会中,“AI 算法说这个人不可信”似乎比“约翰·史密斯说这个人不可信”更具权威性和客观性,尽管前者只是对后者的学习近似。您的模型将在规模上洗钱和操作化人类判断的最糟糕的方面,对真实人们的生活产生负面影响。

技术从来都不是中立的。如果您的工作对世界产生任何影响,这种影响具有道德方向:技术选择也是伦理选择。始终要谨慎选择您希望您的工作支持的价值观。

如果您可以从keras.datasets导入正确的数据集并开始拟合一些深度学习模型将会非常方便。不幸的是,在现实世界中,您将不得不从头开始。

在本章中,您将学习一个通用的逐步蓝图,您可以使用它来处理和解决任何机器学习问题,就像前面列表中的问题一样。这个模板将汇集和巩固您在第四章和第五章中学到的一切,并为您提供更广泛的背景,以便扎实您将在接下来的章节中学到的内容。

机器学习的通用工作流程大致分为三个部分:

  1. 定义任务—理解问题领域和客户要求背后的业务逻辑。收集数据集,了解数据代表什么,并选择如何衡量任务的成功。

  2. 开发模型—准备数据以便机器学习模型处理,选择一个模型评估协议和一个简单的基准来超越,训练一个具有泛化能力且可以过拟合的第一个模型,然后正则化和调整您的模型,直到实现最佳的泛化性能。

  3. 部署模型—向利益相关者展示您的工作,将模型部署到 Web 服务器、移动应用程序、网页或嵌入式设备,监控模型在实际环境中的性能,并开始收集构建下一代模型所需的数据。

让我们深入了解。

6.1 定义任务

没有对您正在做的事情的背景有深刻的理解,您无法做出好的工作。您的客户为什么要解决这个特定的问题?他们将从解决方案中获得什么价值——您的模型将如何使用,它将如何融入客户的业务流程?有哪些可用的数据,或者可以收集哪些数据?什么样的机器学习任务可以映射到业务问题?

6.1.1 界定问题

构建机器学习问题通常需要与利益相关者进行许多详细的讨论。以下是您应该牢记的问题:

  • 您的输入数据将是什么?您试图预测什么?只有在有训练数据可用的情况下,您才能学会预测某些东西:例如,只有在有电影评论和情感注释可用时,您才能学会对电影评论的情感进行分类。因此,在这个阶段,数据可用性通常是限制因素。在许多情况下,您将不得不自己收集和注释新的数据集(我们将在下一节中介绍)。

  • 您面临的是什么类型的机器学习任务?是二元分类?多类分类?标量回归?向量回归?多类别、多标签分类?图像分割?排名?还是其他类型,如聚类、生成或强化学习?在某些情况下,可能机器学习甚至不是理解数据的最佳方式,您应该使用其他方法,比如传统的统计分析。

    • 照片搜索引擎项目是一个多类别、多标签分类任务。

    • 垃圾邮件检测项目是一个二元分类任务。如果将“具有攻击性内容”设置为单独的类别,则它是一个三分类任务。

    • 音乐推荐引擎最终最好不是通过深度学习来处理,而是通过矩阵分解(协同过滤)。

    • 信用卡欺诈检测项目是一个二元分类任务。

    • 点击率预测项目是一个标量回归任务。

    • 异常饼干检测是一个二元分类任务,但它还需要一个对象检测模型作为第一阶段,以正确裁剪原始图像中的饼干。请注意,被称为“异常检测”的一组机器学习技术在这种情况下并不适用!

    • 从卫星图像中寻找新的考古遗址项目是一个图像相似度排名任务:您需要检索看起来最像已知考古遗址的新图像。

  • 现有解决方案是什么样的?也许您的客户已经有一个手工制作的算法来处理垃圾邮件过滤或信用卡欺诈检测,其中有很多嵌套的if语句。也许目前是一个人手动处理正在考虑的过程——监控饼干工厂的传送带并手动移除坏饼干,或者制作歌曲推荐播放列表以发送给喜欢特定艺术家的用户。您应该确保了解已经存在的系统以及它们是如何工作的。

  • 您是否需要处理特定的约束?例如,您可能会发现您正在为其构建垃圾邮件检测系统的应用程序严格端到端加密,因此垃圾邮件检测模型将不得不存在于最终用户的手机上,并且必须在外部数据集上进行训练。也许饼干过滤模型有延迟约束,因此它将需要在工厂的嵌入式设备上运行,而不是在远程服务器上运行。您应该了解您的工作将适用的完整背景。

一旦你完成了研究,你应该知道你的输入是什么,你的目标是什么,以及问题映射到什么类型的机器学习任务。在这个阶段要注意你所做的假设:

  • 你假设可以根据输入来预测目标。

  • 你假设可用的数据(或者你即将收集的数据)足够信息丰富,可以学习输入和目标之间的关系。

在你有一个可用的模型之前,这些只是等待验证或无效的假设。并不是所有问题都可以通过机器学习解决;仅仅因为你已经收集了输入 X 和目标 Y 的示例并不意味着 X 包含足够的信息来预测 Y。例如,如果你试图根据股票市场的最近价格历史来预测股票的走势,你很可能不会成功,因为价格历史并不包含太多预测信息。

6.1.2 收集数据集

一旦你了解了任务的性质,知道你的输入和目标将是什么,就是数据收集的时候了——大多数机器学习项目中最费力、耗时和昂贵的部分。

  • 图片搜索引擎项目要求你首先选择你想要分类的标签集——你选择了 1 万个常见的图像类别。然后你需要手动为数十万张过去用户上传的图片打上这个标签集中的标签。

  • 对于聊天应用的垃圾信息检测项目,由于用户聊天是端到端加密的,你无法使用其内容来训练模型。你需要获得一个包含数万条未经过滤的社交媒体帖子的独立数据集,并手动将其标记为垃圾信息、冒犯性内容或可接受内容。

  • 对于音乐推荐引擎,你可以只使用用户的“喜欢”。不需要收集新数据。同样对于点击率预测项目:你有过去多年广告的点击率记录。

  • 对于饼干标记模型,你需要在传送带上方安装摄像头,收集数万张图片,然后需要有人手动标记这些图片。目前知道如何做这件事的人在饼干工厂工作,但似乎并不太困难。你应该能够训练人员来做这件事。

  • 卫星图像项目将需要一个考古学家团队收集一个现有感兴趣地点的数据库,对于每个地点,你需要找到在不同天气条件下拍摄的现有卫星图像。为了得到一个好的模型,你需要成千上万个不同的地点。

你在第五章学到,模型的泛化能力几乎完全来自于它所训练的数据的属性——你拥有的数据点数量,标签的可靠性,特征的质量。一个好的数据集是值得关注和投资的资产。如果你有额外的 50 个小时用于项目,最有效的分配方式很可能是收集更多数据,而不是寻找渐进的建模改进。

数据比算法更重要的观点最著名的是由谷歌研究人员在 2009 年发表的一篇名为“数据的不合理有效性”的论文中提出的(标题是对尤金·维格纳 1960 年文章“数学在自然科学中的不合理有效性”进行的改编)。这是在深度学习流行之前,但令人惊讶的是,深度学习的兴起只使数据的重要性更加突出。

如果你正在进行监督学习,那么一旦你收集了输入(如图片),你将需要为它们提供注释(如这些图片的标签)—你将训练模型来预测的目标。有时,注释可以自动获取,比如音乐推荐任务或点击率预测任务的注释。但通常你需要手动为数据进行注释。这是一个劳动密集型的过程。

投资于数据标注基础设施

你的数据标注流程将决定你的目标的质量,进而决定你的模型的质量。仔细考虑你可用的选项:

  • 你应该自己标注数据吗?

  • 你应该使用类似 Mechanical Turk 这样的众包平台来收集标签吗?

  • 是否应该使用专门的数据标注公司的服务?

外包可能会节省你时间和金钱,但会带走控制权。使用类似 Mechanical Turk 这样的东西可能会便宜且扩展性好,但你的标注可能会变得很嘈杂。

要选择最佳选项,考虑你正在处理的限制条件:

  • 数据标注员是否需要是专业领域专家,还是任何人都可以标注数据?猫狗图像分类问题的标签可以由任何人选择,但狗品种分类任务的标签需要专业知识。同时,标注骨折的 CT 扫描几乎需要医学学位。

  • 如果标注数据需要专业知识,你能培训人员来做吗?如果不能,你如何获得相关专家的帮助?

  • 你自己是否了解专家如何进行标注?如果不了解,你将不得不将数据集视为黑匣子,你将无法进行手动特征工程—这并非关键,但可能会有限制。

如果决定在内部标注数据,问问自己你将使用什么软件记录标注。你可能需要自己开发这个软件。高效的数据标注软件会为你节省大量时间,因此在项目早期投资于此是值得的。

警惕非代表性数据

机器学习模型只能理解与之前看到的输入类似的内容。因此,训练使用的数据应该代表生产数据。这个问题应该是所有数据收集工作的基础。

假设你正在开发一个应用,用户可以拍摄一盘食物的照片以找出菜名。你使用来自食客常用的图片分享社交网络的图片来训练模型。在部署时,愤怒用户的反馈开始涌入:你的应用在 10 次中有 8 次答错!怎么回事?你在测试集上的准确率远远超过 90%!快速查看用户上传的数据发现,随机餐馆的随机菜品用随机手机拍摄的移动图片看起来与你训练模型的专业质量、光线充足、诱人的图片完全不同:你的训练数据不代表生产数据。这是一个致命的错误—欢迎来到机器学习地狱。

如果可能的话,直接从模型将要使用的环境中收集数据。电影评论情感分类模型应该用于新的 IMDB 评论,而不是 Yelp 餐厅评论,也不是 Twitter 状态更新。如果你想评价一条推文的情感,首先收集和标注来自与你预期在生产中的用户类似的一组用户的实际推文。如果无法在生产数据上进行训练,那么确保你充分了解你的训练和生产数据的差异,并积极纠正这些差异。

你应该注意的一个相关现象是概念漂移。你几乎会在所有涉及用户生成数据的真实世界问题中遇到概念漂移。概念漂移发生在生产数据的属性随时间变化,导致模型准确性逐渐下降的情况下。2013 年训练的音乐推荐引擎可能在今天效果不佳。同样,你使用的 IMDB 数据集是在 2011 年收集的,基于它训练的模型可能在处理 2020 年的评论时表现不如处理 2012 年的评论,因为词汇、表达和电影类型随时间演变。概念漂移在像信用卡欺诈检测这样的对抗性环境中尤为严重,那里的欺诈模式几乎每天都在变化。处理快速概念漂移需要不断进行数据收集、标注和模型重新训练。

请记住,机器学习只能用于记忆训练数据中存在的模式。你只能识别以前见过的内容。使用过去数据训练的机器学习来预测未来是假设未来会像过去一样。这通常并非如此。

抽样偏差问题

一个特别阴险且常见的非代表性数据案例是抽样偏差。当你的数据收集过程与你试图预测的内容相互作用,导致测量结果出现偏差时,就会发生抽样偏差。一个著名的历史例子发生在 1948 年的美国总统选举中。选举当晚,芝加哥论坛报刊登了“杜威击败杜鲁门”的头条新闻。第二天早上,杜鲁门被宣布为胜者。论坛报的编辑信任了一项电话调查的结果——但 1948 年的电话用户并不是选民群体的随机代表样本。他们更有可能是富裕、保守派的人,更有可能投票给共和党候选人杜威。

[外链图片转存中…(img-ZlcagxVJ-1710946537846)]

“杜威击败杜鲁门”:抽样偏差的一个著名例子

如今,每次电话调查都会考虑到抽样偏差。这并不意味着在政治民意调查中抽样偏差已经成为过去的事情——相反,与 1948 年不同,民意调查员现在已经意识到这一点并采取措施加以纠正。

6.1.3 理解你的数据

将数据集视为黑匣子是相当糟糕的做法。在开始训练模型之前,你应该探索和可视化数据,以获取关于数据预测性的见解,这将为特征工程提供信息,并筛选潜在问题。

  • 如果你的数据包含图像或自然语言文本,请直接查看一些样本(及其标签)。

  • 如果你的数据包含数值特征,绘制特征值的直方图是个好主意,以了解取值范围和不同值的频率。

  • 如果你的数据包含位置信息,请在地图上绘制出来。是否出现了明显的模式?

  • 一些样本是否缺少某些特征的值?如果是,你在准备数据时需要处理这个问题(我们将在下一节介绍如何处理)。

  • 如果你的任务是一个分类问题,请打印出数据中每个类别的实例数。这些类别是否大致平均表示?如果不是,你将需要考虑这种不平衡。

  • 检查目标泄漏:数据中存在的特征提供了关于目标的信息,这些信息在生产中可能不可用。如果你正在训练一个模型来预测未来某人是否会接受癌症治疗,并且记录包括“这个人被诊断患有癌症”这个特征,那么你的目标就会人为地泄漏到你的数据中。始终要问自己,你的数据中的每个特征是否都是在生产中以相同形式可用的?

6.1.4 选择成功的衡量标准

要控制某物,你需要能够观察它。要在项目上取得成功,你必须首先定义你所理解的成功。准确率?精确率和召回率?客户保留率?你对成功的度量标准将指导你在整个项目中做出的所有技术选择。它应直接与你的更高级别目标(例如客户的业务成功)保持一致。

对于平衡分类问题,每个类别等可能出现的情况下,准确率和受试者工作特征曲线(ROC)下的面积,简称为 ROC AUC,是常见的度量标准。对于类别不平衡的问题、排名问题或多标签分类问题,可以使用精确率和召回率,以及加权准确率或 ROC AUC 的形式。而且,定义自己的成功度量标准也并不罕见。要了解机器学习成功度量标准的多样性以及它们与不同问题领域的关系,浏览 Kaggle(kaggle.com)上的数据科学竞赛是很有帮助的;它们展示了各种问题和评估指标。

6.2 开发模型

一旦你知道如何衡量你的进展,就可以开始模型开发。大多数教程和研究项目假设这是唯一的步骤——跳过问题定义和数据集收集,这些被假定已经完成,并跳过模型部署和维护,这些被假定由其他人处理。事实上,模型开发只是机器学习工作流程中的一个步骤,如果你问我,这并不是最困难的步骤。机器学习中最困难的事情是明确问题并收集、注释和清理数据。所以振作起来——接下来的事情相对容易!

6.2.1 准备数据

正如你之前学到的,深度学习模型通常不会直接处理原始数据。数据预处理旨在使手头的原始数据更适合神经网络。这包括向量化、归一化或处理缺失值。许多预处理技术是领域特定的(例如,特定于文本数据或图像数据);在接下来的章节中,当我们在实际示例中遇到它们时,我们将介绍这些技术。目前,我们将回顾所有数据领域通用的基础知识。

向量化

神经网络中的所有输入和目标通常必须是浮点数据张量(或在特定情况下是整数或字符串张量)。无论你需要处理的数据是声音、图像、文本,你都必须首先将其转换为张量,这一步称为数据向量化。例如,在第四章的两个文本分类示例中,我们开始时将文本表示为整数列表(代表单词序列),然后使用独热编码将其转换为float32数据张量。在分类数字和预测房价的示例中,数据以向量化形式呈现,因此我们可以跳过这一步。

值归一化

在第二章的 MNIST 数字分类示例中,我们开始时将图像数据编码为 0-255 范围内的整数,表示灰度值。在将这些数据馈送到网络之前,我们必须将其转换为float32并除以 255,以便最终得到 0-1 范围内的浮点值。类似地,在预测房价时,我们开始时的特征具有各种范围——一些特征具有较小的浮点值,而其他特征具有相当大的整数值。在将这些数据馈送到网络之前,我们必须独立地对每个特征进行归一化,使其具有标准差为 1 和均值为 0。

一般来说,将相对较大的值(例如,多位整数,远大于网络权重初始值的值)或异构数据(例如,一个特征在 0-1 范围内,另一个在 100-200 范围内)输入神经网络是不安全的。这样做可能会触发大的梯度更新,阻止网络收敛。为了让你的网络学习更容易,你的数据应具有以下特征:

  • 取小值—通常,大多数值应该在 0-1 范围内。

  • 保持同质性—所有特征的取值应该在大致相同的范围内。

此外,以下更严格的归一化实践是常见的,可以帮助,尽管并不总是必要的(例如,在数字分类示例中我们没有这样做):

  • 将每个特征独立归一化为均值为 0。

  • 将每个特征独立归一化为标准差为 1。

这在 NumPy 数组中很容易实现:

x -= x.mean(axis=0)      # ❶
x /= x.std(axis=0)

❶ 假设 x 是一个形状为(样本数,特征数)的 2D 数据矩阵

处理缺失值

有时你的数据中可能会有缺失值。例如,在房价示例中,第一个特征(数据中索引为 0 的列)是人均犯罪率。如果这个特征并非所有样本都有呢?那么你的训练或测试数据中就会有缺失值。

你可以选择完全丢弃该特征,但不一定必须这样做。

  • 如果特征是分类的,创建一个新类别表示“该值缺失”是安全的。模型将自动学习这对目标意味着什么。

  • 如果特征是数值的,避免输入像"0"这样的任意值,因为它可能会在特征形成的潜在空间中创建不连续,使得模型更难泛化。相反,考虑用数据集中该特征的平均值或中位数替换缺失值。你也可以训练一个模型,根据其他特征的值来预测该特征的值。

请注意,如果你预期测试数据中会有缺失的分类特征,但网络是在没有任何缺失值的数据上训练的,那么网络就不会学会忽略缺失值!在这种情况下,你应该人为生成带有缺失条目的训练样本:多次复制一些训练样本,并丢弃你预计在测试数据中可能缺失的一些分类特征。

6.2.2 选择评估协议

正如你在前一章中学到的,模型的目的是实现泛化,你在整个模型开发过程中做出的每个建模决策都将由寻求衡量泛化性能的验证指标指导。你的验证协议的目标是准确估计你选择的成功指标(如准确率)在实际生产数据上的表现。这个过程的可靠性对于构建一个有用的模型至关重要。

在第五章中,我们回顾了三种常见的评估协议:

  • 保持留出验证集—当你有大量数据时,这是一个好方法。

  • 进行 K 折交叉验证—当你的样本量太少,无法依靠留出验证来进行可靠评估时,这是正确的选择。

  • 进行迭代的 K 折验证—这是在数据有限的情况下进行高精度模型评估的方法。

选择其中之一。在大多数情况下,第一个就足够好了。但是要记住,始终要注意你的验证集的代表性,并小心不要在训练集和验证集之间有重复的样本。

6.2.3 击败基准

当你开始着手模型本身时,你的初始目标是实现统计功效,就像你在第五章看到的那样:也就是说,开发一个能够击败简单基准的小模型。

在这个阶段,你应该专注于以下三件最重要的事情:

  • 特征工程—过滤掉无信息的特征(特征选择)并利用你对问题的了解开发可能有用的新特征。

  • 选择正确的架构先验—你将使用什么类型的模型架构?密集连接网络、卷积网络、循环神经网络、Transformer?深度学习是否是解决任务的好方法,还是应该使用其他方法?

  • 选择足够好的训练配置—你应该使用什么损失函数?什么批量大小和学习率?

选择正确的损失函数

往往不可能直接优化衡量问题成功的指标。有时没有简单的方法将指标转化为损失函数;毕竟,损失函数需要仅通过一个小批量数据就能计算(理想情况下,损失函数应该能够计算一个数据点)并且必须可微分(否则,你无法使用反向传播来训练你的网络)。例如,广泛使用的分类指标 ROC AUC 不能直接优化。因此,在分类任务中,通常会优化 ROC AUC 的代理指标,如交叉熵。一般来说,你可以希望交叉熵越低,ROC AUC 就越高。

以下表格可以帮助你为几种常见问题类型选择最后一层激活函数和损失函数。

为你的模型选择正确的最后一层激活函数和损失函数

问题类型 最后一层激活函数 损失函数
二元分类 sigmoid binary_crossentropy
多类单标签分类 softmax categorical_crossentropy
多类多标签分类 sigmoid binary_crossentropy

对于大多数问题,你可以从现有的模板开始。你不是第一个尝试构建垃圾邮件检测器、音乐推荐引擎或图像分类器的人。确保你研究先前的技术以识别在你的任务上表现良好的特征工程技术和模型架构。

注意,并非总是能够达到统计功效。如果在尝试多个合理的架构后仍无法超越简单的基准线,可能是你所问的问题在输入数据中没有答案。记住,你正在提出两个假设:

  • 你假设可以根据输入预测输出。

  • 你假设可用数据足够信息丰富,可以学习输入和输出之间的关系。

很可能这些假设是错误的,如果是这样,你必须回到起点。

6.2.4 扩展规模:开发一个过拟合的模型

一旦你获得了具有统计功效的模型,问题就变成了,你的模型是否足够强大?它是否有足够的层和参数来正确地建模手头的问题?例如,逻辑回归模型在 MNIST 上具有统计功效,但不足以很好地解决问题。记住,机器学习中的普遍张力在于优化和泛化之间。理想的模型是一个恰好处于欠拟合和过拟合、欠容量和过容量之间的模型。要弄清楚这个边界在哪里,首先你必须跨越它。

要弄清楚你需要多大的模型,你必须开发一个过拟合的模型。这相当容易,正如你在第五章学到的那样:

  1. 添加层。

  2. 让层变得更大。

  3. 训练更多的 epochs。

始终监控训练损失和验证损失,以及你关心的任何指标的训练和验证值。当你看到模型在验证数据上的表现开始下降时,你已经过拟合了。

6.2.5 正则化和调整你的模型

一旦您达到了统计功效并且能够过度拟合,您就知道自己走在正确的道路上。此时,您的目标是最大化泛化性能。

这个阶段将花费最多的时间:您将反复修改您的模型,训练它,在验证数据上评估(此时不是测试数据),再次修改它,并重复,直到模型达到最佳状态。以下是您应该尝试的一些事项:

  • 尝试不同的架构;添加或删除层。

  • 添加丢弃(dropout)。

  • 如果您的模型很小,添加 L1 或 L2 正则化。

  • 尝试不同的超参数(例如每层的单元数或优化器的学习率)以找到最佳配置。

  • 可选地,迭代数据整理或特征工程:收集和注释更多数据,开发更好的特征,或删除看起来不具信息量的特征。

可以通过使用自动化超参数调整软件(如 KerasTuner)来自动化大部分工作。我们将在第十三章中介绍这个内容。

要注意以下事项:每次您使用验证过程的反馈来调整模型时,都会泄漏有关验证过程的信息到模型中。这样做几次是无害的;但如果在许多迭代中系统地重复这样做,最终会导致您的模型过度拟合验证过程(即使没有任何模型直接在任何验证数据上训练)。这会使评估过程变得不太可靠。

一旦您开发出令人满意的模型配置,您可以在所有可用数据(训练和验证)上训练最终的生产模型,并最后一次在测试集上评估它。如果测试集上的性能明显低于在验证数据上测量的性能,这可能意味着您的验证程序毕竟不可靠,或者在调整模型参数时开始过拟合验证数据。在这种情况下,您可能需要切换到更可靠的评估协议(如迭代 K 折验证)。

6.3 部署模型

您的模型已成功通过了测试集上的最终评估,它已准备好部署并开始其生产生活。

6.3.1 向利益相关者解释您的工作并设定期望

成功和客户信任在于始终满足或超越人们的期望。您交付的实际系统只是这个画面的一半;另一半是在推出前设定适当的期望。

非专业人士对人工智能系统的期望往往是不切实际的。例如,他们可能期望系统“理解”其任务,并能在任务背景下行使类似人类常识的能力。为了解决这个问题,您应该考虑展示一些模型的失败模式的示例(例如,展示被错误分类的样本是什么样子,特别是那些误分类看起来令人惊讶的样本)。

他们可能还期望人类水平的表现,特别是对以前由人类处理的流程。大多数机器学习模型,因为它们(不完美地)训练来逼近人类生成的标签,并没有真正达到那里。您应该清楚地传达模型表现的期望。避免使用抽象的陈述,如“模型的准确率为 98%”(大多数人心理上会四舍五入到 100%),而更倾向于谈论假阴性率和假阳性率。您可以说,“使用这些设置,欺诈检测模型将有 5%的假阴性率和 2.5%的假阳性率。每天,平均有 200 笔有效交易会被标记为欺诈并送去人工审核,平均会漏掉 14 笔欺诈交易。平均会正确捕捉 266 笔欺诈交易。”清楚地将模型的性能指标与业务目标联系起来。

你还应该确保与利益相关者讨论关键启动参数的选择——例如,应该在哪个概率阈值下标记交易(不同的阈值会产生不同的假阴性和假阳性率)。这些决策涉及权衡,只有深刻理解业务背景才能处理。

6.3.2 发布推理模型

当你到达一个可以保存训练模型的 Colab 笔记本时,一个机器学习项目并没有结束。你很少会将在训练过程中操作的完全相同的 Python 模型对象投入生产。

首先,你可能希望将模型导出为除 Python 之外的其他形式:

  • 你的生产环境可能根本不支持 Python——例如,如果是移动应用程序或嵌入式系统。

  • 如果应用程序的其余部分不是用 Python 编写的(可能是 JavaScript、C++ 等),使用 Python 来提供模型可能会引入显著的开销。

其次,由于你的生产模型只用于输出预测(一个称为推理的阶段),而不是用于训练,你有机会进行各种优化,使模型更快速并减少其内存占用。

让我们快速看一下你可以使用的不同模型部署选项。

将模型部署为 REST API

这可能是将模型转化为产品的常见方式:在服务器或云实例上安装 TensorFlow,并通过 REST API 查询模型的预测。你可以使用类似 Flask(或任何其他 Python Web 开发库)的东西构建自己的服务应用程序,或者使用 TensorFlow 的自己的库来将模型作为 API 进行部署,称为TensorFlow Servingwww.tensorflow.org/tfx/guide/serving)。使用 TensorFlow Serving,你可以在几分钟内部署一个 Keras 模型。

当你需要使用这种部署设置时

  • 将消费模型预测的应用程序将可靠地访问互联网(显然)。例如,如果你的应用程序是一个移动应用程序,从远程 API 提供预测意味着应用程序在飞行模式或低连接环境下无法使用。

  • 应用程序没有严格的延迟要求:请求、推理和答复往返通常需要大约 500 毫秒。

  • 发送用于推理的输入数据并不高度敏感:数据需要以解密形式在服务器上可用,因为模型需要查看它(但请注意,应该使用 SSL 加密进行 HTTP 请求和答复)。

例如,图像搜索引擎项目、音乐推荐系统、信用卡欺诈检测项目和卫星图像项目都非常适合通过 REST API 进行服务。

将模型部署为 REST API 时一个重要的问题是,你是想自己托管代码,还是想使用完全托管的第三方云服务。例如,Google 的产品 Cloud AI Platform 允许你简单地将你的 TensorFlow 模型上传到 Google Cloud Storage(GCS),并提供一个 API 端点来查询它。它会处理许多实际细节,如批处理预测、负载平衡和扩展。

在设备上部署模型

有时,你可能需要让你的模型与使用它的应用程序运行在同一设备上——也许是智能手机、机器人上的嵌入式 ARM CPU,或者微型设备上的微控制器。你可能见过一款相机,能够自动检测你对准的场景中的人和面孔:那很可能是一款直接在相机上运行的小型深度学习模型。

当你需要使用这种设置时

  • 你的模型具有严格的延迟约束或需要在低连接环境中运行。如果你正在构建一款沉浸式增强现实应用程序,查询远程服务器不是一个可行的选择。

  • 您的模型可以足够小,以便在目标设备的内存和功率约束下运行。您可以使用 TensorFlow 模型优化工具包来帮助实现此目标(www.tensorflow.org/model_optimization)。

  • 获得最高可能的准确性对您的任务并非至关重要。运行时效率和准确性之间总是存在权衡,因此内存和功率约束通常要求您部署一个不如在大型 GPU 上运行的最佳模型好的模型。

  • 输入数据非常敏感,因此不应在远程服务器上解密。

我们的垃圾邮件检测模型将需要在最终用户的智能手机上运行,作为聊天应用程序的一部分,因为消息是端到端加密的,因此无法由远程托管模型读取。同样,坏 cookie 检测模型具有严格的延迟约束,并且需要在工厂中运行。幸运的是,在这种情况下,我们没有任何功率或空间约束,因此我们实际上可以在 GPU 上运行模型。

要在智能手机或嵌入式设备上部署 Keras 模型,您的首选解决方案是 TensorFlow Lite(www.tensorflow.org/lite)。这是一个用于在设备上高效进行深度学习推断的框架,可在 Android 和 iOS 智能手机以及基于 ARM64 的计算机、树莓派或某些微控制器上运行。它包括一个转换器,可以直接将您的 Keras 模型转换为 TensorFlow Lite 格式。

在浏览器中部署模型

深度学习经常用于基于浏览器或桌面的 JavaScript 应用程序。虽然通常可以让应用程序通过 REST API 查询远程模型,但在浏览器中直接运行模型(利用 GPU 资源(如果可用))可能具有关键优势。

当以下情况发生时,请使用此设置

  • 您希望将计算卸载到最终用户,这可以大大降低服务器成本。

  • 输入数据需要保留在最终用户的计算机或手机上。例如,在我们的垃圾邮件检测项目中,Web 版本和桌面版聊天应用程序(作为用 JavaScript 编写的跨平台应用程序实现)应使用本地运行的模型。

  • 您的应用程序具有严格的延迟约束。虽然在最终用户的笔记本电脑或智能手机上运行的模型可能比在您自己服务器上的大型 GPU 上运行的模型慢,但您没有额外的 100 毫秒的网络往返时间。

  • 在模型已被下载并缓存后,您需要确保您的应用程序在没有连接的情况下继续工作。

只有在您的模型足够小,不会占用用户笔记本电脑或智能手机的 CPU、GPU 或 RAM 时,才应选择此选项。此外,由于整个模型将被下载到用户设备上,因此您应确保模型的任何内容都不需要保密。请注意,鉴于训练过的深度学习模型,通常可以恢复有关训练数据的一些信息:如果模型是在敏感数据上训练的,则最好不要将训练过的模型公开。

要在 JavaScript 中部署模型,TensorFlow 生态系统包括 TensorFlow.js(www.tensorflow.org/js),这是一个用于深度学习的 JavaScript 库,几乎实现了所有 Keras API(最初以 WebKeras 为工作名称开发)以及许多较低级别的 TensorFlow API。您可以轻松地将保存的 Keras 模型导入到 TensorFlow.js 中,以便在基于浏览器的 JavaScript 应用程序或桌面 Electron 应用程序中查询它。

推断模型优化

在部署在具有严格可用功率和内存约束(智能手机和嵌入式设备)或具有低延迟要求的环境中时,优化您的模型以进行推断尤为重要。在将模型导入到 TensorFlow.js 或导出到 TensorFlow Lite 之前,您应始终寻求优化您的模型。

有两种流行的优化技术可以应用:

  • 权重剪枝—权重张量中的每个系数并不都对预测产生相同的贡献。通过仅保留最重要的系数,可以显著降低模型层中的参数数量。这减少了模型的内存和计算占用,但在性能指标上略有损失。通过决定要应用多少剪枝,你可以控制大小和准确性之间的权衡。

  • 权重量化—深度学习模型是使用单精度浮点(float32)权重训练的。然而,可以将权重量化为 8 位有符号整数(int8),以获得一个仅用于推断的模型,其大小是原始模型的四分之一,但保持接近原始模型的准确性。

TensorFlow 生态系统包括一个权重剪枝和量化工具包(www.tensorflow.org/model_optimization),它与 Keras API 深度集成。

6.3.3 在实际环境中监控你的模型

你已经导出了一个推断模型,将其集成到你的应用程序中,并在生产数据上进行了干扰测试—模型的行为与你预期的完全一致。你已经编写了单元测试以及日志记录和状态监控代码—完美。现在是时候按下大红按钮,部署到生产环境了。

即使这还不是结束。一旦部署了模型,你需要继续监控其行为,对新数据的性能,与应用程序其他部分的交互以及对业务指标的最终影响。

  • 在部署新的音乐推荐系统后,你的在线广播的用户参与度是上升还是下降?切换到新的点击率预测模型后,平均广告点击率是否增加?考虑使用随机 A/B 测试来分离模型本身的影响和其他变化:一部分案例应该经过新模型,而另一部分控制案例应该继续使用旧流程。一旦处理了足够多的案例,两者之间的结果差异很可能归因于模型。

  • 如果可能的话,定期对模型在生产数据上的预测进行手动审核。通常可以重复使用与数据标注相同的基础设施:将一部分生产数据发送到手动标注,然后将模型的预测与新的标注进行比较。例如,你应该绝对为图像搜索引擎和恶意 cookie 标记系统做这个工作。

  • 当无法进行手动审核时,考虑替代的评估途径,比如用户调查(例如,在垃圾邮件和有害内容标记系统的情况下)。

6.3.4 维护你的模型

最后,没有模型能永远持续。你已经了解了概念漂移:随着时间的推移,你的生产数据的特征将发生变化,逐渐降低模型的性能和相关性。你的音乐推荐系统的寿命将以周计算。对于信用卡欺诈检测系统,将以天计算。最好情况下,图像搜索引擎的寿命为几年。

一旦你的模型启动,你应该准备好训练下一个将取代它的新一代。因此,

  • 注意生产数据的变化。是否有新特征可用?是否应该扩展或编辑标签集?

  • 继续收集和标注数据,并随着时间的推移改进你的标注流水线。特别要注意收集那些对当前模型分类困难的样本—这些样本最有可能帮助提高性能。

这就结束了机器学习的通用工作流程——需要记住的事情很多。成为专家需要时间和经验,但不要担心,您现在比几章前聪明多了。您现在熟悉了整体情况——机器学习项目所涉及的整个范围。虽然本书的大部分内容将集中在模型开发上,但您现在意识到这只是整个工作流程的一部分。始终牢记整体情况!

摘要

  • 当您开始新的机器学习项目时,首先定义手头的问题:

    • 理解您要做的事情的更广泛背景——最终目标是什么,有哪些约束条件?

    • 收集和注释数据集;确保深入了解数据。

    • 选择如何衡量问题的成功——您将在验证数据上监视哪些指标?

  • 一旦您理解了问题并拥有适当的数据集,就开发一个模型:

    • 准备您的数据。

    • 选择评估协议:留出验证?K 折验证?应该使用数据的哪一部分进行验证?

    • 实现统计功效:击败一个简单的基准。

    • 扩展:开发一个可以过拟合的模型。

    • 正则化您的模型并调整其超参数,基于验证数据的性能。许多机器学习研究往往只关注这一步,但要牢记整体情况。

  • 当您的模型准备就绪并在测试数据上表现良好时,就是部署的时候了:

    • 首先,确保与利益相关者设定适当的期望。

    • 优化最终模型以进行推断,并将模型部署到选择的部署环境——Web 服务器、移动设备、浏览器、嵌入式设备等。

    • 监控模型在生产中的性能,并继续收集数据,以便开发下一代模型。

Python 深度学习第二版(GPT 重译)(二)

四、入门神经网络:分类和回归

本章涵盖

  • 您的第一个真实世界机器学习工作流示例

  • 处理矢量数据上的分类问题

  • 处理矢量数据上的连续回归问题

本章旨在帮助您开始使用神经网络解决实际问题。您将巩固从第二章和第三章中获得的知识,并将所学应用于三个新任务,涵盖神经网络的三种最常见用例 — 二元分类、多类分类和标量回归:

  • 将电影评论分类为正面或负面(二元分类)

  • 根据主题对新闻线进行分类(多类分类)

  • 给定房地产数据估计房屋价格(标量回归)

这些示例将是您与端到端机器学习工作流的第一次接触:您将介绍数据预处理、基本模型架构原则和模型评估。

分类和回归术语表

分类和回归涉及许多专门术语。您在早期示例中已经遇到了一些,您将在未来章节中看到更多。它们具有精确的、机器学习特定的定义,您应该熟悉它们:

  • Sampleinput — 进入您的模型的一个数据点。

  • Predictionoutput — 您的模型输出的内容。

  • Target — 真相。根据外部数据源,您的模型理想情况下应该预测的内容。

  • Prediction errorloss value — 您的模型预测与目标之间距离的度量。

  • Classes — 在分类问题中可供选择的可能标签集。例如,当对猫和狗图片进行分类时,“狗”和“猫”是两个类别。

  • Label — 分类问题中类别注释的特定实例。例如,如果图片 #1234 被注释为包含“狗”类,则“狗”是图片 #1234 的一个标签。

  • Ground-truthannotations — 数据集中的所有目标,通常由人类收集。

  • Binary classification — 一个分类任务,其中每个输入样本应该被分类到两个互斥的类别中。

  • Multiclass classification — 一个分类任务,其中每个输入样本应该被分类到两个以上的类别中:例如,分类手写数字。

  • Multilabel classification — 一个分类任务,其中每个输入样本可以被分配多个标签。例如,给定图像可能同时包含猫和狗,并且应该同时用“猫”标签和“狗”标签进行注释。每个图像的标签数量通常是可变的。

  • Scalar regression — 目标是一个连续标量值的任务。预测房价是一个很好的例子:不同的目标价格形成一个连续空间。

  • Vector regression — 目标是一组连续值的任务:例如,一个连续的矢量。如果您正在针对多个值进行回归(例如图像中边界框的坐标),那么您正在进行矢量回归。

  • Mini-batchbatch — 模型同时处理的一小组样本(通常在 8 到 128 之间)。样本数量通常是 2 的幂,以便在 GPU 上进行内存分配。在训练时,一个小批量用于计算应用于模型权重的单个梯度下降更新。

通过本章结束时,您将能够使用神经网络处理矢量数据上的简单分类和回归任务。然后,您将准备好在第五章开始构建更有原则、理论驱动的机器学习理解。

4.1 电影评论分类:一个二元分类示例

二元分类,或二元分类,是最常见的机器学习问题之一。在这个示例中,您将学习根据评论的文本内容将电影评论分类为正面或负面。

4.1.1 IMDB 数据集

您将使用 IMDB 数据集:来自互联网电影数据库的 50,000 条高度极化评论。它们被分为 25,000 条用于训练和 25,000 条用于测试的评论,每组评论包含 50% 的负面评论和 50% 的正面评论。

就像 MNIST 数据集一样,IMDB 数据集已经打包到 Keras 中。它已经经过预处理:评论(单词序列)已经转换为整数序列,其中每个整数代表字典中的特定单词。这使我们能够专注于模型构建、训练和评估。在第十一章中,您将学习如何从头开始处理原始文本输入。

以下代码将加载数据集(第一次运行时,将下载约 80 MB 的数据到您的计算机)。

列表 4.1 加载 IMDB 数据集

from tensorflow.keras.datasets import imdb
(train_data, train_labels), (test_data, test_labels) = imdb.load_data(
    num_words=10000)

参数num_words=10000表示您只会保留训练数据中出现频率最高的前 10,000 个单词。罕见单词将被丢弃。这使我们可以处理可管理大小的向量数据。如果我们不设置这个限制,我们将使用训练数据中的 88,585 个独特单词,这是不必要的庞大数量。其中许多单词只在一个样本中出现,因此无法有意义地用于分类。

变量train_datatest_data是评论列表;每个评论是一个单词索引列表(编码为单词序列)。train_labelstest_labels是 0 和 1 的列表,其中 0 代表负面,1 代表正面

>>> train_data[0]
[1, 14, 22, 16, ... 178, 32]
>>> train_labels[0]
1

因为我们限制自己只使用前 10,000 个最常见的单词,所以没有单词索引会超过 10,000:

>>> max([max(sequence) for sequence in train_data])
9999

为了好玩,这里是如何快速将其中一个评论解码回英文单词。

列表 4.2 将评论解码回文本

word_index = imdb.get_word_index()                                # ❶
reverse_word_index = dict(
    [(value, key) for (key, value) in word_index.items()])        # ❷
decoded_review = " ".join(
    [reverse_word_index.get(i - 3, "?") for i in train_data[0]])  # ❸

❶ word_index 是一个将单词映射到整数索引的字典。

❷ 将其反转,将整数索引映射到单词

❸ 解码评论。请注意,索引偏移了 3,因为 0、1 和 2 是“填充”、“序列开始”和“未知”保留索引。

4.1.2 准备数据

您不能直接将整数列表输入神经网络。它们的长度各不相同,但神经网络期望处理连续的数据批次。您必须将列表转换为张量。有两种方法可以做到这一点:

  • 填充列表,使它们的长度相同,将它们转换为形状为(samples, max_length)的整数张量,并从能够处理这种整数张量的层开始构建模型(Embedding层,我们稍后会详细介绍)。

  • 多热编码您的列表以将它们转换为 0 和 1 的向量。这意味着,例如,将序列[8, 5]转换为一个 10,000 维的向量,除了索引 8 和 5 外,其他都是 0,而索引 8 和 5 是 1。然后,您可以使用一个Dense层,能够处理浮点向量数据,作为模型中的第一层。

让我们选择后一种解决方案来对数据进行向量化,这样您可以最大程度地清晰地进行操作。

列表 4.3 通过多热编码对整数序列进行编码

import numpy as np 
def vectorize_sequences(sequences, dimension=10000): 
    results = np.zeros((len(sequences), dimension))   # ❶
    for i, sequence in enumerate(sequences):
        for j in sequence:
            results[i, j] = 1.                        # ❷
    return results
x_train = vectorize_sequences(train_data)             # ❸
x_test = vectorize_sequences(test_data)               # ❹

❶ 创建一个形状为(len(sequences), dimension)的全零矩阵

❷ 将结果[i]的特定索引设置为 1

❸ 向量化训练数据

❹ 向量化测试数据

现在样本看起来是这样的:

>>> x_train[0]
array([ 0.,  1.,  1., ...,  0.,  0.,  0.])

您还应该对标签进行向量化,这很简单:

y_train = np.asarray(train_labels).astype("float32")
y_test = np.asarray(test_labels).astype("float32")

现在数据已准备好输入神经网络。

4.1.3 构建您的模型

输入数据是向量,标签是标量(1 和 0):这是您可能会遇到的最简单的问题设置之一。在这样的问题上表现良好的模型类型是具有relu激活的一堆密集连接(Dense)层。

对于这样一堆Dense层,有两个关键的架构决策:

  • 要使用多少层

  • 选择每层使用多少个单元

在第五章,你将学习指导你做出这些选择的正式原则。目前,你将不得不相信我做出以下架构选择:

  • 两个中间层,每个有 16 个单元

  • 第三层将输出关于当前评论情感的标量预测

图 4.1 三层模型

图 4.1 展示了模型的外观。以下代码展示了 Keras 实现,类似于你之前看到的 MNIST 示例。

代码清单 4.4 模型定义

from tensorflow import keras 
from tensorflow.keras import layers

model = keras.Sequential([
    layers.Dense(16, activation="relu"),
    layers.Dense(16, activation="relu"),
    layers.Dense(1, activation="sigmoid")
])

传递给每个 Dense 层的第一个参数是层中的单元数:层的表示空间的维度。你从第二章和第三章记得,每个具有 relu 激活的 Dense 层实现以下张量操作链:

output = relu(dot(input, W) + b)

有 16 个单元意味着权重矩阵 W 的形状为 (input_dimension, 16):与 W 的点积将把输入数据投影到一个 16 维表示空间(然后你将添加偏置向量 b 并应用 relu 操作)。你可以直观地理解表示空间的维度为“模型在学习内部表示时允许的自由度有多大”。拥有更多单元(更高维的表示空间)允许你的模型学习更复杂的表示,但会使模型在计算上更昂贵,并可能导致学习不需要的模式(这些模式会提高训练数据的性能,但不会提高测试数据的性能)。

中间层使用 relu 作为它们的激活函数,最后一层使用 sigmoid 激活以输出一个概率(介于 0 和 1 之间的分数,指示样本有多大可能具有目标“1”:评论有多大可能是积极的)。relu(线性整流单元)是一个用于将负值归零的函数(参见图 4.2),而 sigmoid “压缩”任意值到 [0, 1] 区间(参见图 4.3),输出可以解释为概率。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4.2 线性整流单元函数

最后,你需要选择一个损失函数和一个优化器。因为你面临的是一个二元分类问题,你的模型的输出是一个概率(你的模型以具有 sigmoid 激活的单单元层结束),最好使用 binary_crossentropy 损失。这并不是唯一可行的选择:例如,你可以使用 mean_squared_error。但是当你处理输出概率的模型时,交叉熵通常是最佳选择。交叉熵是信息论领域的一种量,用于衡量概率分布之间的距离,或者在这种情况下,地面实况分布和你的预测之间的距离。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4.3 Sigmoid 函数

激活函数是什么,为什么它们是必要的?

没有像 relu 这样的激活函数(也称为非线性),Dense 层将由两个线性操作组成——点积和加法:

output = dot(input, W) + b

该层只能学习输入数据的线性变换(仿射变换):该层的假设空间将是将输入数据转换为 16 维空间的所有可能线性变换的集合。这样的假设空间太受限制,不会受益于多层表示,因为深度堆叠的线性层仍然实现线性操作:增加更多层不会扩展假设空间(正如你在第二章中看到的)。

为了获得一个更丰富的假设空间,从而受益于深度表示,你需要一个非线性或激活函数。relu 是深度学习中最流行的激活函数,但还有许多其他候选项,它们都有类似奇怪的名称:preluelu 等等。

至于优化器的选择,我们将选择rmsprop,这通常是几乎任何问题的一个很好的默认选择。

这是我们使用rmsprop优化器和binary_crossentropy损失函数配置模型的步骤。请注意,我们还将在训练过程中监视准确性。

列表 4.5 编译模型

model.compile(optimizer="rmsprop",
              loss="binary_crossentropy",
              metrics=["accuracy"])

4.1.4 验证您的方法

正如您在第三章中学到的,深度学习模型永远不应该在其训练数据上进行评估——在训练过程中使用验证集来监视模型的准确性是标准做法。在这里,我们将通过从原始训练数据中分离出 10,000 个样本来创建一个验证集。

列表 4.6 设置一个验证集

x_val = x_train[:10000]
partial_x_train = x_train[10000:]
y_val = y_train[:10000]
partial_y_train = y_train[10000:]

现在我们将在 512 个样本的小批量中对模型进行 20 个时代(对训练数据中的所有样本进行 20 次迭代)的训练。同时,我们将通过将验证数据作为validation_data参数传递来监视我们分离出的 10,000 个样本上的损失和准确性。

列表 4.7 训练您的模型

history = model.fit(partial_x_train,
                    partial_y_train,
                    epochs=20,
                    batch_size=512,
                    validation_data=(x_val, y_val))

在 CPU 上,每个时代不到 2 秒——训练在 20 秒内结束。在每个时代结束时,模型会在验证数据的 10,000 个样本上计算其损失和准确性,会有一个轻微的暂停。

请注意,对model.fit()的调用会返回一个History对象,就像您在第三章中看到的那样。这个对象有一个成员history,它是一个包含训练过程中发生的一切数据的字典。让我们来看一下:

>>> history_dict = history.history
>>> history_dict.keys()
[u"accuracy", u"loss", u"val_accuracy", u"val_loss"]

字典包含四个条目:每个在训练和验证期间监视的指标一个。在接下来的两个列表中,让我们使用 Matplotlib 将训练和验证损失并排绘制出来(参见图 4.4),以及训练和验证准确性(参见图 4.5)。请注意,由于模型的不同随机初始化,您自己的结果可能会略有不同。

图 4.4 训练和验证损失

图 4.5 训练和验证准确性

列表 4.8 绘制训练和验证损失

import matplotlib.pyplot as plt
history_dict = history.history
loss_values = history_dict["loss"]
val_loss_values = history_dict["val_loss"]
epochs = range(1, len(loss_values) + 1)
plt.plot(epochs, loss_values, "bo", label="Training loss")        # ❶
plt.plot(epochs, val_loss_values, "b", label="Validation loss")   # ❷
plt.title("Training and validation loss")
plt.xlabel("Epochs")
plt.ylabel("Loss")
plt.legend()
plt.show()

❶ “bo"代表"蓝色点”。

❷ “b"代表"实线蓝色线”。

列表 4.9 绘制训练和验证准确性

plt.clf()                           # ❶
acc = history_dict["accuracy"]
val_acc = history_dict["val_accuracy"]
plt.plot(epochs, acc, "bo", label="Training acc")
plt.plot(epochs, val_acc, "b", label="Validation acc")
plt.title("Training and validation accuracy")
plt.xlabel("Epochs")
plt.ylabel("Accuracy")
plt.legend()
plt.show()

❶ 清除图形

正如您所看到的,训练损失随着每个时代的进行而减少,而训练准确性则随着每个时代的进行而增加。这是在运行梯度下降优化时您所期望的情况——您试图最小化的量应该在每次迭代中都减少。但验证损失和准确性并非如此:它们似乎在第四个时代达到峰值。这是我们之前警告过的一个例子:在训练数据上表现更好的模型不一定会在以前从未见过的数据上表现更好。准确来说,您所看到的是过拟合:在第四个时代之后,您过度优化了训练数据,最终学习到的表示是特定于训练数据的,无法推广到训练集之外的数据。

在这种情况下,为了防止过拟合,您可以在四个时代后停止训练。一般来说,您可以使用一系列技术来减轻过拟合,我们将在第五章中介绍。

让我们从头开始训练一个新模型四个时代,然后在测试数据上评估它。

列表 4.10 从头开始重新训练模型

model = keras.Sequential([
    layers.Dense(16, activation="relu"),
    layers.Dense(16, activation="relu"),
    layers.Dense(1, activation="sigmoid")
])
model.compile(optimizer="rmsprop",
              loss="binary_crossentropy",
              metrics=["accuracy"])
model.fit(x_train, y_train, epochs=4, batch_size=512)
results = model.evaluate(x_test, y_test)

最终结果如下:

>>> results
[0.2929924130630493, 0.88327999999999995]    # ❶

❶ 第一个数字 0.29 是测试损失,第二个数字 0.88 是测试准确性。

这种相当天真的方法实现了 88%的准确性。使用最先进的方法,您应该能够接近 95%。

4.1.5 使用训练好的模型在新数据上生成预测

在训练完模型后,您会想要在实际环境中使用它。您可以使用predict方法生成评论为正面的可能性,就像您在第三章中学到的那样:

>>> model.predict(x_test)
array([[ 0.98006207]
       [ 0.99758697]
       [ 0.99975556]
       ...,
       [ 0.82167041]
       [ 0.02885115]
       [ 0.65371346]], dtype=float32)

正如您所看到的,模型对某些样本非常自信(0.99 或更高,或 0.01 或更低),但对其他样本不太自信(0.6、0.4)。

4.1.6 进一步的实验

以下实验将帮助您确信您所做的架构选择都是相当合理的,尽管仍有改进的空间:

  • 在最终分类层之前,您使用了两个表示层。尝试使用一个或三个表示层,看看这样做如何影响验证和测试准确性。

  • 尝试使用更多单元或更少单元的层:32 个单元,64 个单元等等。

  • 尝试使用mse损失函数而不是binary_crossentropy

  • 尝试使用tanh激活(这是早期神经网络中流行的激活函数)而不是relu

4.1.7 总结

这是您应该从这个示例中了解到的内容:

  • 通常,您需要对原始数据进行大量预处理,以便能够将其(作为张量)馈送到神经网络中。单词序列可以编码为二进制向量,但也有其他编码选项。

  • 具有relu激活的Dense层堆叠可以解决各种问题(包括情感分类),您可能经常会使用它们。

  • 在二元分类问题(两个输出类别)中,您的模型应该以一个具有一个单元和sigmoid激活的Dense层结束:您的模型的输出应该是一个介于 0 和 1 之间的标量,编码为概率。

  • 在二元分类问题上,具有标量 S 形输出的损失函数应该使用binary_crossentropy

  • rmsprop优化器通常是一个足够好的选择,无论您的问题是什么。这是您无需担心的一件事。

  • 随着神经网络在训练数据上变得更好,最终会开始过拟合,并且在从未见过的数据上获得越来越糟糕的结果。一定要始终监视在训练集之外的数据上的性能。

4.2 新闻线分类:一个多类别分类示例

在前一节中,您看到了如何使用密集连接的神经网络将向量输入分类为两个互斥类别。但是当您有两个以上的类别时会发生什么?

在本节中,我们将构建一个模型,将路透社新闻线分类为 46 个互斥主题。因为我们有很多类别,所以这个问题是多类别分类的一个实例,因为每个数据点应该被分类为一个类别,所以这个问题更具体地是单标签多类别分类的一个实例。如果每个数据点可以属于多个类别(在这种情况下是主题),我们将面临一个多标签多类别分类问题。

4.2.1 路透社数据集

您将使用路透社数据集,这是路透社在 1986 年发布的一组简短新闻线及其主题。这是一个简单、广泛使用的文本分类玩具数据集。有 46 个不同的主题;一些主题比其他主题更有代表性,但每个主题在训练集中至少有 10 个示例。

与 IMDB 和 MNIST 一样,路透社数据集作为 Keras 的一部分打包提供。让我们来看看。

列表 4.11 加载路透社数据集

from tensorflow.keras.datasets import reuters
(train_data, train_labels), (test_data, test_labels) = reuters.load_data(
    num_words=10000)

与 IMDB 数据集一样,参数num_words=10000将数据限制为数据中出现频率最高的 10,000 个单词。

您有 8,982 个训练示例和 2,246 个测试示例:

>>> len(train_data)
8982
>>> len(test_data)
2246

与 IMDB 评论一样,每个示例都是一个整数列表(单词索引):

>>> train_data[10]
[1, 245, 273, 207, 156, 53, 74, 160, 26, 14, 46, 296, 26, 39, 74, 2979,
3554, 14, 46, 4689, 4329, 86, 61, 3499, 4795, 14, 61, 451, 4329, 17, 12]

如果您感兴趣,这是如何将其解码回单词的方法。

列表 4.12 将新闻线解码回文本

word_index = reuters.get_word_index()
reverse_word_index = dict(
    [(value, key) for (key, value) in word_index.items()])
decoded_newswire = " ".join(
    [reverse_word_index.get(i - 3, "?") for i in     train_data[0]])   # ❶

❶ 请注意,索引偏移了 3,因为 0、1 和 2 是“填充”、“序列开始”和“未知”保留索引。

与示例相关联的标签是介于 0 和 45 之间的整数—一个主题索引:

>>> train_labels[10]
3

4.2.2 准备数据

您可以使用与前一个示例中完全相同的代码对数据进行向量化。

列表 4.13 对输入数据进行编码

x_train = vectorize_sequences(train_data)      # ❶
x_test = vectorize_sequences(test_data)        # ❷

❶ 向量化训练数据

❷ 向量化测试数据

要将标签向量化,有两种可能性:你可以将标签列表转换为整数张量,或者你可以使用独热编码。独热编码是一种广泛使用的分类数据格式,也称为分类编码。在这种情况下,标签的独热编码包括将每个标签嵌入为一个全零向量,其中标签索引的位置为 1。下面的列表显示了一个示例。

列表 4.14 编码标签

def to_one_hot(labels, dimension=46):
    results = np.zeros((len(labels), dimension))
    for i, label in enumerate(labels):
        results[i, label] = 1. 
    return results
y_train = to_one_hot(train_labels)     # ❶
y_test = to_one_hot(test_labels)       # ❷

❶ 向量化训练标签

❷ 向量化测试标签

请注意,Keras 中有一种内置的方法可以做到这一点:

from tensorflow.keras.utils import to_categorical
y_train = to_categorical(train_labels)
y_test = to_categorical(test_labels)

4.2.3 构建你的模型

这个主题分类问题看起来与之前的电影评论分类问题相似:在这两种情况下,我们都试图对短文本进行分类。但是这里有一个新的约束:输出类别的数量从 2 个增加到了 46 个。输出空间的维度大得多。

在像我们一直使用的Dense层堆叠中,每一层只能访问前一层输出中存在的信息。如果一层丢失了与分类问题相关的一些信息,这些信息将永远无法被后续层恢复:每一层都可能成为信息瓶颈。在前面的例子中,我们使用了 16 维的中间层,但 16 维的空间可能太有限,无法学习区分 46 个不同的类别:这样的小层可能充当信息瓶颈,永久丢失相关信息。

出于这个原因,我们将使用更大的层。让我们选择 64 个单元。

列表 4.15 模型定义

model = keras.Sequential([
    layers.Dense(64, activation="relu"),
    layers.Dense(64, activation="relu"),
    layers.Dense(46, activation="softmax")
])

还有两件事情你应该注意关于这个架构。

首先,我们用一个大小为 46 的Dense层结束模型。这意味着对于每个输入样本,网络将输出一个 46 维的向量。这个向量中的每个条目(每个维度)将编码一个不同的输出类别。

其次,最后一层使用了softmax激活函数。你在 MNIST 示例中看到了这种模式。这意味着模型将输出 46 个不同输出类别的概率分布,对于每个输入样本,模型将产生一个 46 维的输出向量,其中output[i]是样本属于类别i的概率。这 46 个分数将总和为 1。

在这种情况下使用的最佳损失函数是categorical_crossentropy。它衡量两个概率分布之间的距离:在这里,模型输出的概率分布与标签的真实分布之间的距离。通过最小化这两个分布之间的距离,你训练模型输出尽可能接近真实标签。

列表 4.16 编译模型

model.compile(optimizer="rmsprop",
              loss="categorical_crossentropy",
              metrics=["accuracy"])

4.2.4 验证你的方法

让我们在训练数据中留出 1,000 个样本作为验证集使用。

列表 4.17 设置一个验证集

x_val = x_train[:1000]
partial_x_train = x_train[1000:]
y_val = y_train[:1000]
partial_y_train = y_train[1000:]

现在,让我们训练模型 20 个周期。

列表 4.18 训练模型

history = model.fit(partial_x_train,
                    partial_y_train,
                    epochs=20,
                    batch_size=512,
                    validation_data=(x_val, y_val))

最后,让我们展示其损失和准确率曲线(见图 4.6 和 4.7)。

图 4.6 训练和验证损失

图 4.7 训练和验证准确率

列表 4.19 绘制训练和验证损失

loss = history.history["loss"]
val_loss = history.history["val_loss"]
epochs = range(1, len(loss) + 1)
plt.plot(epochs, loss, "bo", label="Training loss")
plt.plot(epochs, val_loss, "b", label="Validation loss")
plt.title("Training and validation loss")
plt.xlabel("Epochs")
plt.ylabel("Loss")
plt.legend()
plt.show()

列表 4.20 绘制训练和验证准确率

plt.clf()                          # ❶
acc = history.history["accuracy"]
val_acc = history.history["val_accuracy"]
plt.plot(epochs, acc, "bo", label="Training accuracy")
plt.plot(epochs, val_acc, "b", label="Validation accuracy")
plt.title("Training and validation accuracy")
plt.xlabel("Epochs")
plt.ylabel("Accuracy")
plt.legend()
plt.show()

❶ 清除图表

模型在九个周期后开始过拟合。让我们从头开始训练一个新模型,训练九个周期,然后在测试集上评估它。

列表 4.21 从头开始重新训练模型

model = keras.Sequential([
    layers.Dense(64, activation="relu"),
    layers.Dense(64, activation="relu"),
    layers.Dense(46, activation="softmax")
])
model.compile(optimizer="rmsprop",
              loss="categorical_crossentropy",
              metrics=["accuracy"])
model.fit(x_train,
          y_train,
          epochs=9,
          batch_size=512)
results = model.evaluate(x_test, y_test)

这里是最终结果:

>>> results
[0.9565213431445807, 0.79697239536954589]

这种方法达到了约 80%的准确率。对于一个平衡的二元分类问题,一个纯随机分类器达到的准确率将是 50%。但在这种情况下,我们有 46 个类别,它们可能不会被平等地表示。一个随机基线的准确率会是多少呢?我们可以尝试快速实现一个来进行经验性检查:

>>> import copy
>>> test_labels_copy = copy.copy(test_labels)
>>> np.random.shuffle(test_labels_copy)
>>> hits_array = np.array(test_labels) == np.array(test_labels_copy)
>>> hits_array.mean()
0.18655387355298308

正如你所看到的,一个随机分类器的分类准确率约为 19%,所以从这个角度看,我们模型的结果似乎相当不错。

4.2.5 在新数据上生成预测

在新样本上调用模型的predict方法会返回每个样本的 46 个主题的类概率分布。让我们为所有测试数据生成主题预测:

predictions = model.predict(x_test)

“predictions”中的每个条目都是长度为 46 的向量:

>>> predictions[0].shape
(46,)

这个向量中的系数总和为 1,因为它们形成一个概率分布:

>>> np.sum(predictions[0])
1.0

最大的条目是预测的类别——具有最高概率的类别:

>>> np.argmax(predictions[0])
4

4.2.6 处理标签和损失的另一种方式

我们之前提到另一种编码标签的方式是将它们转换为整数张量,就像这样:

y_train = np.array(train_labels)
y_test = np.array(test_labels)

这种方法唯一改变的是损失函数的选择。列表 4.21 中使用的损失函数categorical_crossentropy期望标签遵循分类编码。对于整数标签,你应该使用sparse_categorical_crossentropy

model.compile(optimizer="rmsprop",
              loss="sparse_categorical_crossentropy",
              metrics=["accuracy"])

这个新的损失函数在数学上仍然与categorical_crossentropy相同;它只是有一个不同的接口。

4.2.7 拥有足够大的中间层的重要性

我们之前提到,由于最终的输出是 46 维的,你应该避免中间层的单元远远少于 46。现在让我们看看当我们引入信息瓶颈时会发生什么,即通过具有明显低于 46 维的中间层,例如 4 维:

列表 4.22 具有信息瓶颈的模型

model = keras.Sequential([
    layers.Dense(64, activation="relu"),
    layers.Dense(4, activation="relu"),
    layers.Dense(46, activation="softmax")
])
model.compile(optimizer="rmsprop",
              loss="categorical_crossentropy",
              metrics=["accuracy"])
model.fit(partial_x_train,
          partial_y_train,
          epochs=20,
          batch_size=128,
          validation_data=(x_val, y_val))

现在模型的验证准确率达到了约 71%,绝对下降了 8%。这种下降主要是因为我们试图将大量信息(足以恢复 46 个类别的分离超平面的信息)压缩到一个过低维度的中间空间中。模型能够将大部分必要信息压缩到这些四维表示中,但并非全部。

4.2.8 进一步实验

就像前面的例子一样,我鼓励你尝试以下实验,以培养你对这类模型需要做出的配置决策的直觉:

  • 尝试使用更大或更小的层:32 个单元,128 个单元等。

  • 在最终的 softmax 分类层之前使用了两个中间层。现在尝试使用一个单独的中间层,或者三个中间层。

4.2.9 总结

这个例子给我们的启示是:

  • 如果你试图在N个类别中对数据点进行分类,你的模型应该以大小为NDense层结束。

  • 在单标签多类分类问题中,你的模型应该以softmax激活结束,这样它将输出关于N个输出类别的概率分布。

  • 对于这类问题,几乎总是应该使用分类交叉熵作为损失函数。它最小化了模型输出的概率分布与目标的真实分布之间的距离。

  • 在多类分类中有两种处理标签的方式:

    • 通过分类编码(也称为独热编码)对标签进行编码,并使用categorical_crossentropy作为损失函数

    • 将标签编码为整数并使用sparse_categorical_crossentropy损失函数

  • 如果你需要将数据分类到大量类别中,你应该避免由于中间层太小而在模型中创建信息瓶颈。

4.3 预测房价:回归示例

之前的两个例子被视为分类问题,目标是预测输入数据点的单个离散标签。另一种常见的机器学习问题是回归,它包括预测连续值而不是离散标签:例如,根据气象数据预测明天的温度,或者根据规格说明预测软件项目完成所需的时间。

注意不要混淆回归逻辑回归算法。令人困惑的是,逻辑回归并不是一个回归算法,而是一个分类算法。

4.3.1 波士顿房价数据集

在本节中,我们将尝试预测上世纪 70 年代中期波士顿郊区房屋的中位价格,根据当时有关该郊区的数据点,例如犯罪率、当地财产税率等。我们将使用的数据集与前两个示例有一个有趣的区别。它的数据点相对较少:仅有 506 个,分为 404 个训练样本和 102 个测试样本。输入数据中的每个特征(例如犯罪率)具有不同的比例。例如,一些值是比例,取值介于 0 和 1 之间,其他值介于 1 和 12 之间,其他值介于 0 和 100 之间,依此类推。

列表 4.23 加载波士顿房屋数据集

from tensorflow.keras.datasets import boston_housing
(train_data, train_targets), (test_data, test_targets) = (
    boston_housing.load_data())

让我们看一下数据:

>>> train_data.shape
(404, 13)
>>> test_data.shape
(102, 13)

正如你所看到的,我们有 404 个训练样本和 102 个测试样本,每个样本有 13 个数值特征,例如人均犯罪率、每个住宅的平均房间数、高速公路的可达性等。

目标值是占有住房的中位数值,以千美元为单位:

>>> train_targets
[ 15.2,  42.3,  50\. ...  19.4,  19.4,  29.1]

价格通常在$10,000 和$50,000 之间。如果听起来很便宜,请记住这是上世纪 70 年代中期,这些价格没有考虑通货膨胀。

4.3.2 准备数据

将取值范围差异很大的值输入神经网络可能会有问题。模型可能能够自动适应这种异质数据,但这肯定会使学习变得更加困难。处理这种数据的一种广泛最佳实践是进行特征归一化:对于输入数据中的每个特征(输入数据矩阵中的一列),我们减去该特征的均值并除以标准差,使得该特征以 0 为中心,具有单位标准差。这在 NumPy 中很容易实现。

列表 4.24 归一化数据

mean = train_data.mean(axis=0)
train_data -= mean
std = train_data.std(axis=0)
train_data /= std
test_data -= mean
test_data /= std

请注意,用于归一化测试数据的量是使用训练数据计算的。你绝对不应该在工作流程中使用在测试数据上计算的任何量,即使是像数据归一化这样简单的操作也不行。

4.3.3 构建你的模型

由于可用样本很少,我们将使用一个非常小的模型,其中包含两个中间层,每个层有 64 个单元。一般来说,训练数据越少,过拟合就会越严重,使用一个小模型是缓解过拟合的一种方法。

列表 4.25 模型定义

def build_model():
    model = keras.Sequential([              # ❶
        layers.Dense(64, activation="relu"),
        layers.Dense(64, activation="relu"),
        layers.Dense(1)
    ])
    model.compile(optimizer="rmsprop", loss="mse", metrics=["mae"])
    return model

❶ 因为我们需要多次实例化相同的模型,所以我们使用一个函数来构建它。

模型以一个单元结束,没有激活函数(它将是一个线性层)。这是标量回归的典型设置(一种回归,你试图预测一个单一连续值)。应用激活函数会限制输出的范围;例如,如果在最后一层应用sigmoid激活函数,模型只能学习预测 0 到 1 之间的值。在这里,因为最后一层是纯线性的,模型可以自由地学习预测任何范围内的值。

请注意,我们使用mse损失函数来编译模型—均方误差,即预测值与目标值之间的差的平方。这是回归问题中广泛使用的损失函数。

在训练过程中,我们还监控一个新的指标:平均绝对误差(MAE)。它是预测值与目标值之间的差的绝对值。例如,在这个问题上的 MAE 为 0.5 意味着你的预测平均偏差为$500。

4.3.4 使用 K 折验证验证你的方法

在我们继续调整模型参数(例如用于训练的时代数)的同时评估我们的模型,我们可以将数据分割为训练集和验证集,就像我们在之前的示例中所做的那样。但是由于数据点很少,验证集最终会变得非常小(例如,约 100 个示例)。因此,验证分数可能会根据我们选择用于验证和训练的数据点而变化很大:验证分数可能在验证拆分方面具有很高的方差。这将阻止我们可靠地评估我们的模型。

在这种情况下,最佳做法是使用K 折交叉验证(参见图 4.8)。

图 4.8 K=3 的 K 折交叉验证

它包括将可用数据分割为K个分区(通常K=4 或 5),实例化K个相同的模型,并在K-1 个分区上训练每个模型,同时在剩余分区上进行评估。然后使用的模型的验证分数是获得的K个验证分数的平均值。在代码方面,这很简单。

列表 4.26 K 折交叉验证

k = 4 
num_val_samples = len(train_data) // k
num_epochs = 100 
all_scores = [] 
for i in range(k):
    print(f"Processing fold #{i}")
    val_data = train_data[i * num_val_samples: (i + 1) * num_val_samples]   # ❶
    val_targets = train_targets[i * num_val_samples: (i + 1) * num_val_samples]
    partial_train_data = np.concatenate(                                    # ❷
        [train_data[:i * num_val_samples],
         train_data[(i + 1) * num_val_samples:]],
        axis=0)
    partial_train_targets = np.concatenate(
        [train_targets[:i * num_val_samples],
         train_targets[(i + 1) * num_val_samples:]],
        axis=0)
    model = build_model()                                                   # ❸
    model.fit(partial_train_data, partial_train_targets,                    # ❹
              epochs=num_epochs, batch_size=16, verbose=0)
    val_mse, val_mae = model.evaluate(val_data, val_targets, verbose=0)     # ❺
    all_scores.append(val_mae)

❶ 准备验证数据:来自分区#k 的数据

❷ 准备训练数据:来自所有其他分区的数据

❸ 构建 Keras 模型(已编译)

❹ 训练模型(静默模式,verbose = 0)

❺ 在验证数据上评估模型

使用num_epochs = 100运行此操作将产生以下结果:

>>> all_scores
[2.112449, 3.0801501, 2.6483836, 2.4275346]
>>> np.mean(all_scores)
2.5671294

不同的运行确实显示了相当不同的验证分数,从 2.1 到 3.1。平均值(2.6)比任何单个分数更可靠—这就是 K 折交叉验证的全部意义。在这种情况下,我们平均偏差为$2,600,考虑到价格范围为$10,000 到$50,000,这是一个显著的差距。

让我们尝试将模型训练更长一点:500 个时代。为了记录模型在每个时代的表现如何,我们将修改训练循环以保存每个折叠的每个时代验证分数日志。

列表 4.27 保存每个折叠的验证日志

num_epochs = 500 
all_mae_histories = [] 
for i in range(k):
    print(f"Processing fold #{i}")
    val_data = train_data[i * num_val_samples: (i + 1) * num_val_samples]   # ❶
    val_targets = train_targets[i * num_val_samples: (i + 1) * num_val_samples]
    partial_train_data = np.concatenate(                                    # ❷
        [train_data[:i * num_val_samples],
         train_data[(i + 1) * num_val_samples:]],
        axis=0)
    partial_train_targets = np.concatenate(
        [train_targets[:i * num_val_samples],
         train_targets[(i + 1) * num_val_samples:]],
        axis=0)
    model = build_model()                                                   # ❸
    history = model.fit(partial_train_data, partial_train_targets,          # ❹
                        validation_data=(val_data, val_targets),
                        epochs=num_epochs, batch_size=16, verbose=0)
    mae_history = history.history["val_mae"]
    all_mae_histories.append(mae_history)

❶ 准备验证数据:来自分区#k 的数据

❷ 准备训练数据:来自所有其他分区的数据

❸ 构建 Keras 模型(已编译)

❹ 训练模型(静默模式,verbose=0)

然后我们可以计算所有折叠的每个时代 MAE 分数的平均值。

列表 4.28 构建连续平均 K 折验证分数的历史

average_mae_history = [
    np.mean([x[i] for x in all_mae_histories]) for i in range(num_epochs)]

让我们绘制这个;参见图 4.9。

图 4.9 按时代划分的验证 MAE

列表 4.29 绘制验证分数

plt.plot(range(1, len(average_mae_history) + 1), average_mae_history)
plt.xlabel("Epochs")
plt.ylabel("Validation MAE")
plt.show()

由于缩放问题,可能有点难以阅读图表:前几个时代的验证 MAE 远高于后续数值。让我们省略前 10 个数据点,这些数据点与曲线的其余部分处于不同的比例尺。

列表 4.30 绘制验证分数,不包括前 10 个数据点

truncated_mae_history = average_mae_history[10:]
plt.plot(range(1, len(truncated_mae_history) + 1), truncated_mae_history)
plt.xlabel("Epochs")
plt.ylabel("Validation MAE")
plt.show()

正如您在图 4.10 中所看到的,验证 MAE 在 120-140 个时代后停止显着改善(这个数字包括我们省略的 10 个时代)。在那之后,我们开始过拟合。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4.10 按时代划分的验证 MAE,不包括前 10 个数据点

一旦您完成调整模型的其他参数(除了时代数,您还可以调整中间层的大小),您可以使用最佳参数在所有训练数据上训练最终的生产模型,然后查看其在测试数据上的表现。

列表 4.31 训练最终模型

model = build_model()                                # ❶
model.fit(train_data, train_targets,                 # ❷
          epochs=130, batch_size=16, verbose=0)
test_mse_score, test_mae_score = model.evaluate(test_data, test_targets)

❶ 获取一个新的、已编译的模型

❷ 在所有数据上对其进行训练

这是最终结果:

>>> test_mae_score
2.4642276763916016

我们仍然有一点不到$2,500 的差距。这是一个进步!就像前两个任务一样,您可以尝试改变模型中的层数或每层的单元数,看看是否可以减少测试误差。

4.3.5 在新数据上生成预测

在我们的二元分类模型上调用predict()时,我们为每个输入样本检索到介于 0 和 1 之间的标量分数。对于我们的多类分类模型,我们为每个样本检索到所有类别的概率分布。现在,对于这个标量回归模型,predict()返回模型对样本价格的猜测,单位为千美元:

>>> predictions = model.predict(test_data)
>>> predictions[0]
array([9.990133], dtype=float32)

测试集中的第一栋房子预测价格约为$10,000。

4.3.6 总结

从这个标量回归示例中,您应该得出以下结论:

  • 回归使用不同的损失函数进行,与我们用于分类的不同。均方误差(MSE)是回归常用的损失函数。

  • 同样,用于回归的评估指标与用于分类的评估指标不同;自然地,准确性的概念不适用于回归。常见的回归指标是平均绝对误差(MAE)。

  • 当输入数据中的特征具有不同范围的值时,每个特征应作为预处理步骤独立缩放。

  • 当数据量很少时,使用 K 折验证是可靠评估模型的好方法。

  • 当可用的训练数据很少时,最好使用只有少数中间层(通常只有一个或两个)的小型模型,以避免严重过拟合。

摘要

  • 在向量数据上,机器学习任务的三种最常见类型是二元分类、多类分类和标量回归。

    • 本章前面的“总结”部分总结了您对每个任务学到的重要知识点。

    • 回归使用不同的损失函数和不同的评估指标,与分类不同。

  • 在将原始数据输入神经网络之前,通常需要对其进行预处理。

  • 当您的数据具有不同范围的特征时,作为预处理的一部分,应独立缩放每个特征。

  • 随着训练的进行,神经网络最终开始过拟合,并在以前未见过的数据上获得更糟糕的结果。

  • 如果您没有太多的训练数据,可以使用只有一个或两个中间层的小型模型,以避免严重过拟合。

  • 如果您的数据被分成许多类别,如果将中间层设置得太小,可能会导致信息瓶颈。

  • 当您处理少量数据时,K 折验证可以帮助可靠评估您的模型。

五、机器学习的基础知识

本章涵盖

  • 理解泛化和优化之间的紧张关系,这是机器学习中的基本问题

  • 机器学习模型的评估方法

  • 改进模型拟合的最佳实践

  • 实现更好泛化的最佳实践

在第四章中的三个实际例子之后,你应该开始熟悉如何使用神经网络解决分类和回归问题,并且见证了机器学习的核心问题:过拟合。本章将把你对机器学习的一些新直觉形式化为一个坚实的概念框架,强调准确模型评估的重要性以及训练和泛化之间的平衡。

5.1 泛化:机器学习的目标

在第四章中提出的三个例子——预测电影评论、主题分类和房价回归——我们将数据分为训练集、验证集和测试集。很快就明显看到了不在训练数据上评估模型的原因:在几个周期后,从未见过的数据的性能开始与训练数据的性能分歧,而训练数据的性能始终随着训练的进行而改善。模型开始过拟合。过拟合在每个机器学习问题中都会发生。

机器学习中的基本问题是优化和泛化之间的紧张联系。优化指的是调整模型以在训练数据上获得最佳性能的过程(机器学习中的学习),而泛化指的是训练好的模型在从未见过的数据上的表现。当然,游戏的目标是获得良好的泛化,但你无法控制泛化;你只能将模型拟合到其训练数据。如果你做得太好,过拟合就会发生,泛化就会受到影响。

但是是什么导致了过拟合?我们如何实现良好的泛化?

5.1.1 欠拟合和过拟合

对于你在上一章中看到的模型,在保留验证数据上的性能随着训练的进行而改善,然后在一段时间后必然达到顶峰。这种模式(如图 5.1 所示)是普遍存在的。你会在任何模型类型和任何数据集中看到这种情况。

图 5.1 典型的过拟合行为

在训练开始时,优化和泛化是相关的:在训练数据上的损失越低,测试数据上的损失也越低。当这种情况发生时,你的模型被称为欠拟合:仍然有进步的空间;网络尚未对训练数据中的所有相关模式进行建模。但在对训练数据进行一定数量的迭代后,泛化停止改善,验证指标停滞然后开始恶化:模型开始过拟合。也就是说,它开始学习训练数据特定的模式,但这些模式在新数据方面是误导性的或无关的。

过拟合在数据存在噪声、不确定性或包含稀有特征时特别容易发生。让我们看看具体的例子。

嘈杂的训练数据

在现实世界的数据集中,一些输入无效是相当常见的。例如,一个 MNIST 数字可能是一张全黑的图片,或者像图 5.2 那样的东西。

图 5.2 一些相当奇怪的 MNIST 训练样本

这些是什么?我也不知道。但它们都是 MNIST 训练集的一部分。然而,更糟糕的是,有些完全有效的输入最终被错误标记,就像图 5.3 中的那些一样。

图 5.3 错标的 MNIST 训练样本

如果一个模型竭尽全力地纳入这些异常值,其泛化性能将会下降,就像图 5.4 中所示的那样。例如,一个看起来非常接近图 5.3 中错误标记的 4 的 4 可能最终被分类为 9。

图 5.4 处理异常值:鲁棒拟合 vs. 过拟合

模糊特征

并非所有数据噪声都来自不准确性,即使是完全干净和整洁标记的数据,在涉及不确定性和模糊性的问题时也可能存在噪声。在分类任务中,通常情况下,输入特征空间的某些区域同时与多个类相关联。假设你正在开发一个模型,该模型接收香蕉的图像并预测香蕉是未熟、成熟还是腐烂。这些类别没有客观的界限,因此同一张图片可能会被不同的人类标记者分类为未熟或成熟。同样,许多问题涉及随机性。你可以使用大气压力数据来预测明天是否会下雨,但完全相同的测量有时可能会导致下雨,有时可能会导致晴天,具有一定的概率。

一个模型可能会对这种概率性数据过拟合,对特征空间中模糊区域过于自信,就像图 5.5 中那样。更鲁棒的拟合会忽略个别数据点,看到更大的图景。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5.5 鲁棒拟合 vs. 过拟合给出特征空间中的模糊区域

稀有特征和虚假相关性

如果你一生中只见过两只橘色虎斑猫,而且它们都恰好非常不合群,你可能会推断橘色虎斑猫通常可能是不合群的。这就是过拟合:如果你接触到更多种类的猫,包括更多橘色的猫,你会发现猫的颜色与性格并没有很好的相关性。

同样,训练在包含稀有特征值的数据集上的机器学习模型极易过拟合。在情感分类任务中,如果训练数据中的单词“cherimoya”(一种原产于安第斯山脉的水果)只出现在一篇文本中,并且这篇文本恰好是负面情感的,一个调节不好的模型可能会对这个词赋予很高的权重,并且总是将提到 cherimoyas 的新文本分类为负面,然而,客观上,cherimoya 并没有什么负面的东西。

重要的是,一个特征值不需要只出现几次就会导致虚假相关性。考虑一个在你的训练数据中出现 100 次的单词,它与积极情感相关的概率为 54%,与消极情感相关的概率为 46%。这种差异很可能是一个完全的统计偶然,然而你的模型很可能会学会利用这个特征来进行分类任务。这是过拟合的最常见来源之一。

这里有一个引人注目的例子。以 MNIST 为例。通过将 784 个白噪声维度连接到现有数据的 784 个维度上,创建一个新的训练集,因此一半的数据现在是噪声。为了比较,还创建一个通过连接 784 个全零维度而得到的等效数据集。我们连接的无意义特征完全不影响数据的信息内容:我们只是在添加一些东西。人类分类准确度不会受到这些转换的影响。

列表 5.1 向 MNIST 添加白噪声通道或全零通道

from tensorflow.keras.datasets import mnist 
import numpy as np

(train_images, train_labels), _ = mnist.load_data()
train_images = train_images.reshape((60000, 28 * 28))
train_images = train_images.astype("float32") / 255 

train_images_with_noise_channels = np.concatenate(
    [train_images, np.random.random((len(train_images), 784))], axis=1)

train_images_with_zeros_channels = np.concatenate(
    [train_images, np.zeros((len(train_images), 784))], axis=1)

现在,让我们在这两个训练集上训练第二章中的模型。

列表 5.2 在带有噪声通道或全零通道的 MNIST 数据上训练相同的模型

from tensorflow import keras 
from tensorflow.keras import layers

def get_model():
    model = keras.Sequential([
        layers.Dense(512, activation="relu"),
        layers.Dense(10, activation="softmax")
    ])
    model.compile(optimizer="rmsprop",
                  loss="sparse_categorical_crossentropy",
                  metrics=["accuracy"])
    return model

model = get_model()
history_noise = model.fit(
    train_images_with_noise_channels, train_labels,
    epochs=10,
    batch_size=128,
    validation_split=0.2)

model = get_model()
history_zeros = model.fit(
    train_images_with_zeros_channels, train_labels,
    epochs=10,
    batch_size=128,
    validation_split=0.2)

让我们比较每个模型的验证准确性随时间的演变。

列表 5.3 绘制验证准确性比较

import matplotlib.pyplot as plt
val_acc_noise = history_noise.history["val_accuracy"]
val_acc_zeros = history_zeros.history["val_accuracy"]
epochs = range(1, 11)
plt.plot(epochs, val_acc_noise, "b-",
         label="Validation accuracy with noise channels")
plt.plot(epochs, val_acc_zeros, "b--",
         label="Validation accuracy with zeros channels")
plt.title("Effect of noise channels on validation accuracy")
plt.xlabel("Epochs")
plt.ylabel("Accuracy")
plt.legend()

尽管数据在两种情况下都包含相同的信息,但通过虚假相关性的影响,使用噪声通道训练的模型的验证准确性最终会降低约一个百分点(参见图 5.6)——纯粹是通过虚假相关性的影响。你添加的噪声通道越多,准确性就会进一步下降。

图 5.6 噪声通道对验证准确性的影响

嘈杂的特征不可避免地导致过拟合。因此,在你不确定所拥有的特征是信息性的还是干扰性的情况下,通常在训练之前进行特征选择是很常见的。例如,将 IMDB 数据限制为最常见的前 10000 个单词就是一种粗糙的特征选择。进行特征选择的典型方法是为每个可用特征计算一些有用性评分——衡量特征相对于任务的信息性的度量,比如特征与标签之间的互信息——并且只保留高于某个阈值的特征。这样做将过滤掉前面示例中的白噪声通道。

5.1.2 深度学习中泛化性质的本质

深度学习模型的一个显著特点是,只要具有足够的表征能力,它们就可以被训练来拟合任何东西。

不相信?试着洗牌 MNIST 标签并在此基础上训练一个模型。尽管输入和洗牌标签之间没有任何关系,训练损失仍然可以很好地下降,即使是使用相对较小的模型。当然,由于在这种情况下没有泛化的可能性,验证损失不会随时间改善。

列表 5.4 使用随机洗牌标签拟合 MNIST 模型

(train_images, train_labels), _ = mnist.load_data()
train_images = train_images.reshape((60000, 28 * 28))
train_images = train_images.astype("float32") / 255 

random_train_labels = train_labels[:]
np.random.shuffle(random_train_labels)

model = keras.Sequential([
    layers.Dense(512, activation="relu"),
    layers.Dense(10, activation="softmax")
])
model.compile(optimizer="rmsprop",
              loss="sparse_categorical_crossentropy",
              metrics=["accuracy"])
model.fit(train_images, random_train_labels,
          epochs=100,
          batch_size=128,
          validation_split=0.2)

实际上,你甚至不需要用 MNIST 数据来做这个实验——你可以只生成白噪声输入和随机标签。只要模型有足够的参数,你也可以在这上面拟合一个模型。它最终只会记住特定的输入,就像一个 Python 字典一样。

如果是这样的话,那么深度学习模型到底是如何泛化的呢?它们难道不应该只是学习训练输入和目标之间的临时映射,就像一个高级dict一样吗?我们有什么期望这种映射会适用于新的输入呢?

事实证明,深度学习中的泛化性质与深度学习模型本身关系不大,而与现实世界中信息的结构有很大关系。让我们看看这里到底发生了什么。

流形假设

MNIST 分类器的输入(预处理之前)是一个 28×28 的整数数组,取值范围在 0 到 255 之间。因此,可能的输入值总数是 256 的 784 次方——远远大于宇宙中的原子数量。然而,这些输入中很少有看起来像有效 MNIST 样本的:实际手写数字只占据了所有可能的 28×28 uint8数组父空间中的一个微小子空间。而且,这个子空间不仅仅是在父空间中随机分布的一组点:它具有高度结构化。

首先,有效手写数字的子空间是连续的:如果你拿一个样本并稍微修改它,它仍然可以被识别为相同的手写数字。此外,所有有效子空间中的样本都通过平滑路径连接在一起。这意味着如果你拿两个随机的 MNIST 数字 A 和 B,存在一个“中间”图像序列,将 A 变形为 B,使得两个连续数字非常接近彼此(见图 5.7)。也许在两个类之间边界附近会有一些模糊的形状,但即使这些形状看起来仍然非常像数字。

图 5.7 不同的 MNIST 数字逐渐变形成彼此,显示手写数字空间形成了一个“流形”。此图像是使用第十二章的代码生成的。

从技术角度来说,你会说手写数字形成了一个流形,位于可能的 28×28 uint8数组空间中。这是一个大词,但概念相当直观。一个“流形”是某个父空间中的低维子空间,局部类似于线性(欧几里得)空间。例如,在平面上的平滑曲线是 2D 空间中的 1D 流形,因为对于曲线的每个点,你都可以画出一个切线(曲线可以在每个点处用一条直线来近似)。在 3D 空间中的平滑曲面是 2D 流形。依此类推。

更一般地,流形假设认为所有自然数据都位于编码它的高维空间中的低维流形上。这是关于宇宙中信息结构的一个非常强烈的陈述。据我们所知,这是准确的,也是深度学习有效的原因。这对于 MNIST 数字是正确的,但也适用于人脸、树形态、人声和甚至自然语言。

流形假设意味着

  • 机器学习模型只需要适应潜在输入空间(潜在流形)中相对简单、低维、高度结构化的子空间。

  • 在这些流形中的一个中,总是可以在两个输入之间插值,也就是说,通过一个连续路径将一个变形为另一个,路径上的所有点都落在流形上。

在深度学习中,插值样本之间的能力是理解泛化的关键。

插值作为泛化的来源

如果你处理可以插值的数据点,你可以通过将它们与流形上靠近的其他点联系起来,开始理解以前从未见过的点。换句话说,你可以通过插值来填补空白,从而理解空间的整体

请注意,潜在流形上的插值与父空间中的线性插值是不同的,如图 5.8 所示。例如,在两个 MNIST 数字之间的像素的平均值通常不是一个有效的数字。

图 5.8 线性插值和潜在流形上的插值之间的差异。数字的潜在流形上的每个点都是一个有效的数字,但两个数字的平均值通常不是。

至关重要的是,虽然深度学习通过在学习的数据流形上进行插值来实现泛化,但假设插值就是泛化的全部是错误的。这只是冰山一角。插值只能帮助你理解与之前看到的非常接近的事物:它实现了局部泛化。但值得注意的是,人类总是处理极端的新颖性,并且做得很好。你不需要事先在无数例子上接受训练,以便应对你将要遇到的每种情况。你每一天都与以往任何一天都不同,也与自人类诞生以来的任何一天都不同。你可以在纽约待一周,上海待一周,班加罗尔待一周,而无需为每个城市进行数千次的学习和排练。

人类能够进行极端泛化,这是由于插值之外的认知机制所实现的:抽象、世界的符号模型、推理、逻辑、常识、对世界的内在先验——我们通常称之为理性,与直觉和模式识别相对。后者在很大程度上是插值性质的,但前者不是。这两者对智能都是至关重要的。我们将在第十四章中更多地讨论这个问题。

为什么深度学习有效

还记得第二章中的揉皱纸球的比喻吗?一张纸代表了 3D 空间中的 2D 流形(见图 5.9)。深度学习模型是一种展开纸球的工具,也就是说,是为了解开潜在流形。

图 5.9 展开复杂的数据流形

一个深度学习模型基本上是一个非常高维的曲线—一条平滑连续的曲线(受模型架构先验的额外约束),因为它需要是可微的。这条曲线通过梯度下降逐渐和增量地拟合到数据点。深度学习的本质是关于取一个大的、复杂的曲线—一个流形—并逐渐调整其参数,直到它拟合一些训练数据点。

曲线涉及足够多的参数,可以拟合任何东西——实际上,如果你让你的模型训练足够长的时间,它最终将纯粹地记忆其训练数据,根本无法泛化。然而,你要拟合的数据并不是由稀疏分布在基础空间中的孤立点组成。你的数据在输入空间内形成了一个高度结构化的低维流形—这就是流形假设。由于随着梯度下降的进行,将模型曲线拟合到这些数据中是逐渐平稳进行的,因此在训练过程中会有一个中间点,此时模型大致近似于数据的自然流形,正如你在图 5.10 中所看到的。

图 5.10 从随机模型到过拟合模型,以及作为中间状态实现稳健拟合

沿着模型在那一点学习的曲线移动将接近沿着数据的实际潜在流形移动—因此,模型将能够通过在训练输入之间进行插值来理解以前从未见过的输入。

除了它们具有足够的表征能力这一显而易见的事实外,深度学习模型具有一些特性使它们特别适合学习潜在流形:

  • 深度学习模型实现了从输入到输出的平滑连续映射。它必须是平滑和连续的,因为它必须是可微的,这是必然的(否则你无法进行梯度下降)。这种平滑性有助于近似潜在流形,这些流形具有相同的特性。

  • 深度学习模型往往以与其训练数据中信息“形状”相似的方式进行结构化(通过架构先验)。这特别适用于图像处理模型(在第八章和第九章讨论)和序列处理模型(第十章)。更一般地说,深度神经网络以分层和模块化的方式构建其学习表示,这与自然数据组织方式相呼应。

训练数据至关重要

虽然深度学习确实非常适合流形学习,但泛化的能力更多地是由数据的自然结构而不是模型的任何属性决定的。只有当你的数据形成一个可以进行插值的流形时,你才能进行泛化。你的特征越具信息性,噪声越少,你就越能进行泛化,因为你的输入空间将更简单、更有结构。数据筛选和特征工程对泛化至关重要。

此外,由于深度学习是曲线拟合,为了使模型表现良好,它需要在其输入空间上进行密集采样训练。在这种情况下,“密集采样”意味着训练数据应该密集覆盖整个输入数据流形(参见图 5.11)。这在决策边界附近尤为重要。通过足够密集的采样,可以通过在过去的训练输入之间进行插值来理解新的输入,而无需使用常识、抽象推理或关于世界的外部知识—这些是机器学习模型无法访问的东西。

图 5.11 为了学习一个能够准确泛化的模型,需要对输入空间进行密集采样。

因此,您应始终牢记改进深度学习模型的最佳方法是在更多或更好的数据上训练它(当然,添加过于嘈杂或不准确的数据将损害泛化能力)。输入数据流形的更密集覆盖将产生更好泛化的模型。您永远不应期望深度学习模型执行比其训练样本之间的粗略插值更多的操作,因此您应尽一切可能使插值变得更容易。您在深度学习模型中找到的唯一东西就是您放入其中的东西:编码在其架构中的先验和训练数据。

当无法获取更多数据时,下一个最佳解决方案是调节模型允许存储的信息量,或者对模型曲线的平滑性添加约束。如果一个网络只能记住少量模式,或者非常规律的模式,优化过程将迫使其专注于最突出的模式,这些模式更有可能泛化良好。这种通过这种方式对抗过拟合的过程称为正则化。我们将在第 5.4.4 节深入讨论正则化技术。

在开始调整模型以帮助其更好地泛化之前,您需要一种评估当前模型表现的方法。在接下来的部分中,您将学习如何在模型开发过程中监控泛化:模型评估。

5.2 评估机器学习模型

您只能控制您能观察到的内容。由于您的目标是开发能够成功泛化到新数据的模型,因此能够可靠地衡量模型泛化能力至关重要。在本节中,我将正式介绍您可以评估机器学习模型的不同方法。您在上一章中已经看到了其中大部分的应用。

5.2.1 训练、验证和测试集

评估模型总是归结为将可用数据分为三组:训练、验证和测试。您在训练数据上训练模型,并在验证数据上评估模型。一旦您的模型准备投入实际使用,您将最后一次在测试数据上测试它,这些数据应尽可能与生产数据相似。然后您可以将模型部署到生产环境中。

您可能会问,为什么它只有两组:一个训练集和一个测试集?您可以在训练数据上训练,并在测试数据上评估。简单得多!

原因在于,开发模型总是涉及调整其配置:例如,选择层数或层的大小(称为模型的超参数,以区别于参数,即网络的权重)。您通过使用模型在验证数据上的性能作为反馈信号来进行这种调整。本质上,这种调整是一种学习:在某个参数空间中寻找良好配置。因此,基于模型在验证集上的性能调整模型的配置可能很快导致过拟合验证集,即使您的模型从未直接在其上进行训练。

这种现象的核心是信息泄漏的概念。每当您根据模型在验证集上的性能调整模型的超参数时,一些关于验证数据的信息就会泄漏到模型中。如果您只这样做一次,针对一个参数,那么泄漏的信息量将很少,您的验证集将保持可靠,用于评估模型。但是,如果您多次重复这个过程——运行一个实验,在验证集上评估,并根据结果修改模型——那么您将泄漏越来越多关于验证集的信息到模型中。

最终,您将得到一个在验证数据上表现良好的模型,因为这是您优化的目标。您关心的是在全新数据上的表现,而不是在验证数据上的表现,因此您需要使用一个完全不同的、以前从未见过的数据集来评估模型:测试数据集。您的模型不应该有关于测试集的任何信息,甚至间接的。如果模型的任何部分基于测试集的性能进行调整,那么您的泛化度量将是有缺陷的。

将数据分成训练、验证和测试集可能看起来很简单,但在数据有限时,有一些高级方法可以派上用场。让我们回顾三种经典的评估方法:简单留出验证、K 折验证和具有洗牌功能的迭代 K 折验证。我们还将讨论使用常识基线来检查您的训练是否有所进展。

简单留出验证

将一部分数据作为测试集。在剩余数据上进行训练,并在测试集上进行评估。正如您在前面的部分中看到的,为了防止信息泄漏,您不应该根据测试集调整模型,因此您还应该保留一个验证集。

从示意图 5.12 的示意图上看,留出验证看起来像是。列表 5.5 显示了一个简单的实现。

图 5.12 简单留出验证分割

列表 5.5 留出验证(为简单起见省略了标签)

num_validation_samples = 10000 
np.random.shuffle(data)                                   # ❶
validation_data = data[:num_validation_samples]           # ❷
training_data = data[num_validation_samples:]             # ❸
model = get_model()                                       # ❹
model.fit(training_data, ...)                             # ❹
validation_score = model.evaluate(validation_data, ...)   # ❹

...                                                       # ❺

model = get_model()                                       # ❻
model.fit(np.concatenate([training_data,                  # ❻
                          validation_data]), ...)         # ❻
test_score = model.evaluate(test_data, ...)               # ❻

❶ 通常适合对数据进行洗牌。

❷ 定义验证集

❸ 定义训练集

❹ 在训练数据上训练模型,并在验证数据上评估

❺ 在这一点上,您可以调整您的模型,重新训练它,评估它,再次调整它。

❻ 一旦调整了超参数,通常会从头开始在所有非测试数据上训练最终模型。

这是最简单的评估协议,但存在一个缺陷:如果可用的数据很少,那么您的验证和测试集可能包含的样本太少,无法统计代表手头的数据。这很容易识别:如果在分割之前对数据进行不同的随机洗牌轮次导致模型性能的度量值非常不同,那么您就会遇到这个问题。K 折验证和具有洗牌功能的迭代 K 折验证是解决这个问题的两种方法,接下来将讨论。

K 折验证

使用这种方法,将数据分成K个大小相等的分区。对于每个分区i,在剩余的K - 1个分区上训练模型,并在分区i上评估。然后,您的最终得分是获得的 K 个分数的平均值。当您的模型的性能根据训练-测试分割显示出显著变化时,这种方法是有帮助的。与留出验证一样,这种方法并不免除您使用一个不同的验证集进行模型校准。

从图 5.13 的示意图上看,K 折交叉验证看起来像是。列表 5.6 显示了一个简单的实现。

图 5.13 K 折交叉验证,K=3

列表 5.6 K 折交叉验证(为简单起见省略了标签)

k = 3 
num_validation_samples = len(data) // k
np.random.shuffle(data)
validation_scores = [] 
for fold in range(k):
    validation_data = data[num_validation_samples * fold:        # ❶
                           num_validation_samples * (fold + 1)]  # ❶
    training_data = np.concatenate(                              # ❷
        data[:num_validation_samples * fold],                    # ❷
        data[num_validation_samples * (fold + 1):])              # ❷
    model = get_model()                                          # ❸
    model.fit(training_data, ...)
    validation_score = model.evaluate(validation_data, ...)
    validation_scores.append(validation_score)
validation_score = np.average(validation_scores)                 # ❹
model = get_model()                                              # ❺
model.fit(data, ...)                                             # ❺
test_score = model.evaluate(test_data, ...)                      # ❺

❶ 选择验证数据分区

❷ 使用剩余的数据作为训练数据。请注意,+ 运算符表示列表连接,而不是求和。

❸ 创建一个全新的模型实例(未经训练)

❹ 验证分数:k 个折叠的验证分数的平均值

❺ 在所有非测试数据上训练最终模型

具有洗牌功能的迭代 K 折验证

这个是用于在可用数据相对较少且需要尽可能精确评估模型的情况下。我发现在 Kaggle 竞赛中非常有帮助。它包括多次应用 K 折验证,在每次将数据随机洗牌后将其分成K份。最终得分是在每次 K 折验证运行中获得的得分的平均值。请注意,你最终会训练和评估P * K个模型(其中P是你使用的迭代次数),这可能非常昂贵。

5.2.2 打败常识基线

除了你可以使用的不同评估协议之外,你还应该了解的最后一件事是使用常识基线。

训练深度学习模型有点像按下一个按钮,在另一个平行世界中发射火箭。你听不到也看不到。你无法观察到流形学习过程—它发生在一个有数千维度的空间中,即使你将其投影到 3D,你也无法解释它。你唯一的反馈是你的验证指标—就像你看不见的火箭上的高度计。

能够判断你是否有所进展特别重要。你开始时的高度是多少?你的模型似乎有 15%的准确率—这算好吗?在开始处理数据集之前,你应该始终选择一个微不足道的基线来尝试超越。如果你超过了这个阈值,你就知道你做对了:你的模型实际上正在利用输入数据中的信息进行泛化预测,你可以继续前进。这个基线可以是随机分类器的性能,或者你能想象到的最简单的非机器学习技术的性能。

例如,在 MNIST 数字分类示例中,一个简单的基准是验证准确率大于 0.1(随机分类器);在 IMDB 示例中,它将是验证准确率大于 0.5。在 Reuters 示例中,由于类别不平衡,它将在 0.18-0.19 左右。如果你有一个二元分类问题,其中 90%的样本属于 A 类,10%属于 B 类,那么总是预测 A 的分类器在验证准确率方面已经达到 0.9,你需要做得比这更好。

在开始解决以前没有人解决过的问题时,拥有一个可以参考的常识基线是至关重要的。如果你无法击败一个微不足道的解决方案,你的模型是毫无价值的—也许你使用的是错误的模型,或者你正在处理的问题根本无法用机器学习方法解决。是时候重新审视问题了。

5.2.3 关于模型评估需要记住的事情

在选择评估协议时,请注意以下事项:

  • 数据代表性—你希望你的训练集和测试集都能代表手头的数据。例如,如果你试图对数字图像进行分类,并且从一个按类别排序的样本数组开始,将数组的前 80%作为训练集,剩下的 20%作为测试集,将导致你的训练集只包含类别 0-7,而测试集只包含类别 8-9。这似乎是一个荒谬的错误,但这种情况出奇地常见。因此,你通常应该在将数据拆分为训练集和测试集之前随机洗牌你的数据。

  • 时间的箭头—如果你试图根据过去来预测未来(例如,明天的天气、股票走势等),在将数据拆分之前不要随机洗牌,因为这样做会造成时间泄漏:你的模型实际上是在未来的数据上进行训练的。在这种情况下,你应该始终确保测试集中的所有数据都晚于训练集中的数据。

  • 数据中的冗余——如果你的数据中有一些数据点出现两次(在真实世界数据中很常见),那么对数据进行洗牌并将其分成训练集和验证集将导致训练集和验证集之间存在冗余。实际上,你将在部分训练数据上进行测试,这是你能做的最糟糕的事情!确保你的训练集和验证集是不相交的。

有一个可靠的方法来评估模型性能是你如何能够监控机器学习中的核心张力——在优化和泛化、欠拟合和过拟合之间。

5.3 改善模型拟合

要实现完美拟合,你必须首先过拟合。由于你事先不知道边界在哪里,你必须越过它找到它。因此,你在开始解决问题时的初始目标是获得一个显示一定泛化能力并能够过拟合的模型。一旦你有了这样的模型,你将专注于通过对抗过拟合来完善泛化。

在这个阶段你会遇到三个常见问题:

  • 训练无法开始:你的训练损失不会随时间降低。

  • 训练开始得很顺利,但你的模型并没有有意义地泛化:你无法击败你设定的常识基线。

  • 随着时间的推移,训练和验证损失都在下降,你可以击败你的基线,但似乎无法过拟合,这表明你仍然欠拟合。

让我们看看如何解决这些问题,以实现机器学习项目的第一个重要里程碑:获得具有一定泛化能力的模型(能够击败一个简单的基线)并且能够过拟合。

5.3.1 调整关键梯度下降参数

有时训练无法开始,或者过早停滞。你的损失停滞不前。这总是可以克服的:记住你可以将模型拟合到随机数据上。即使你的问题毫无意义,你仍然应该能够训练出一些东西——即使只是通过记忆训练数据。

当这种情况发生时,通常是由于梯度下降过程的配置问题:你选择的优化器、模型权重的初始值分布、学习率或批量大小。所有这些参数是相互依赖的,因此通常只需调整学习率和批量大小,同时保持其他参数不变即可。

让我们看一个具体的例子:让我们用值为 1 的不合适大学习率训练第二章的 MNIST 模型。

列表 5.7 使用不正确高学习率训练 MNIST 模型

(train_images, train_labels), _ = mnist.load_data()
train_images = train_images.reshape((60000, 28 * 28))
train_images = train_images.astype("float32") / 255 

model = keras.Sequential([
    layers.Dense(512, activation="relu"),
    layers.Dense(10, activation="softmax")
])
model.compile(optimizer=keras.optimizers.RMSprop(1.),
              loss="sparse_categorical_crossentropy",
              metrics=["accuracy"])
model.fit(train_images, train_labels,
          epochs=10,
          batch_size=128,
          validation_split=0.2)

该模型很快达到了 30%–40% 的训练和验证准确率,但无法超越这一范围。让我们尝试将学习率降低到一个更合理的值1e-2

列表 5.8 具有更合适学习率的相同模型

model = keras.Sequential([
    layers.Dense(512, activation="relu"),
    layers.Dense(10, activation="softmax")
])
model.compile(optimizer=keras.optimizers.RMSprop(1e-2),
              loss="sparse_categorical_crossentropy",
              metrics=["accuracy"])
model.fit(train_images, train_labels,
          epochs=10,
          batch_size=128,
          validation_split=0.2)

现在模型能够训练。

如果你发现自己处于类似情况,尝试

  • 降低或增加学习率。学习率过高可能导致更新远超适当拟合,就像前面的例子中一样,而学习率过低可能使训练过于缓慢,看起来停滞不前。

  • 增加批量大小。具有更多样本的批次将导致更具信息性和更少噪声的梯度(方差更低)。

最终,你会找到一个能够开始训练的配置。

5.3.2 利用更好的架构先验知识

你有一个适合的模型,但由于某种原因你的验证指标根本没有改善。它们仍然不比随机分类器获得的好:你的模型训练了,但泛化能力不强。发生了什么?

这可能是你会遇到的最糟糕的机器学习情况。这表明你的方法在根本上有问题,而且可能不容易判断。以下是一些建议。

首先,可能是你使用的输入数据根本不包含足够的信息来预测目标:问题的表述是不可解的。这就是之前当我们尝试拟合一个 MNIST 模型时发生的情况,其中标签被洗牌:模型训练得很好,但验证准确率停留在 10%,因为用这样的数据集明显不可能泛化。

也可能是你使用的模型类型不适合当前的问题。例如,在第十章中,你会看到一个时间序列预测问题的例子,其中一个密集连接的架构无法击败一个微不足道的基准线,而一个更合适的循环架构确实能够很好地泛化。使用对问题做出正确假设的模型对于实现泛化是至关重要的:你应该利用正确的架构先验知识。

在接下来的章节中,你将学习到用于各种数据模态(图像、文本、时间序列等)的最佳架构。一般来说,你应该始终确保阅读关于你正在攻击的任务类型的架构最佳实践——很可能你不是第一个尝试的人。

5.3.3 增加模型容量

如果你成功得到一个适合的模型,其中验证指标在下降,并且似乎至少达到了一定程度的泛化能力,恭喜你:你已经接近成功了。接下来,你需要让你的模型开始过拟合。

考虑以下的小模型——一个简单的逻辑回归——在 MNIST 像素上训练。

列表 5.9 在 MNIST 上的简单逻辑回归

model = keras.Sequential([layers.Dense(10, activation="softmax")])
model.compile(optimizer="rmsprop",
              loss="sparse_categorical_crossentropy",
              metrics=["accuracy"])
history_small_model = model.fit(
    train_images, train_labels,
    epochs=20,
    batch_size=128,
    validation_split=0.2)

你会得到类似于图 5.14 的损失曲线:

import matplotlib.pyplot as plt
val_loss = history_small_model.history["val_loss"]
epochs = range(1, 21)
plt.plot(epochs, val_loss, "b--",
         label="Validation loss")
plt.title("Effect of insufficient model capacity on validation loss")
plt.xlabel("Epochs")
plt.ylabel("Loss")
plt.legend()

图 5.14 不足模型容量对损失曲线的影响

验证指标似乎停滞不前,或者改善非常缓慢,而不是达到峰值然后逆转。验证损失降至 0.26,然后停在那里。你可以拟合,但你无法明显过拟合,即使在对训练数据进行多次迭代后。你在职业生涯中很可能经常遇到类似的曲线。

记住,总是可以过拟合的。就像训练损失不下降的问题一样,这是一个总是可以解决的问题。如果你似乎无法过拟合,很可能是你的模型的表征能力的问题:你需要一个更大的模型,一个具有更多容量的模型,也就是说,能够存储更多信息的模型。你可以通过添加更多层、使用更大的层(具有更多参数的层)或使用更适合问题的层来增加表征能力(更好的架构先验)。

让我们尝试训练一个更大的模型,一个具有两个中间层,每个层有 96 个单元:

model = keras.Sequential([
    layers.Dense(96, activation="relu"),
    layers.Dense(96, activation="relu"),
    layers.Dense(10, activation="softmax"),
])
model.compile(optimizer="rmsprop",
              loss="sparse_categorical_crossentropy",
              metrics=["accuracy"])
history_large_model = model.fit(
    train_images, train_labels,
    epochs=20,
    batch_size=128,
    validation_split=0.2)

现在验证曲线看起来正是应该的:模型快速拟合,并在 8 个时期后开始过拟合(见图 5.15)。

图 5.15 具有适当容量的模型的验证损失

5.4 改善泛化能力

一旦你的模型展现出一定的泛化能力并且能够过拟合,就是时候将注意力转移到最大化泛化上了。

5.4.1 数据集整理

你已经学到了深度学习中泛化是源自数据的潜在结构。如果你的数据使得在样本之间平滑插值成为可能,你将能够训练一个泛化的深度学习模型。如果你的问题过于嘈杂或基本上是离散的,比如说,列表排序,深度学习将无法帮助你。深度学习是曲线拟合,而不是魔术。

因此,确保你正在使用一个合适的数据集是至关重要的。在数据收集上投入更多的精力和金钱几乎总是比在开发更好的模型上投入相同的精力和金钱产生更大的回报。

  • 确保你有足够的数据。记住你需要对输入-输出空间进行密集采样。更多的数据将产生更好的模型。有时,一开始看起来不可能的问题会随着更大的数据集而变得可解。

  • 最小化标记错误——可视化你的输入以检查异常,并校对你的标签。

  • 清洁你的数据并处理缺失值(我们将在下一章中介绍)。

  • 如果你有很多特征,而不确定哪些是真正有用的,那就进行特征选择。

提高数据泛化潜力的一个特别重要的方法是特征工程。对于大多数机器学习问题,特征工程是成功的关键因素。让我们来看看。

5.4.2 特征工程

特征工程是利用你对数据和手头的机器学习算法(在本例中是神经网络)的知识,通过在数据进入模型之前应用硬编码(非学习的)转换来使算法更好地工作的过程。在许多情况下,期望机器学习模型能够从完全任意的数据中学习是不合理的。数据需要以一种使模型工作更轻松的方式呈现给模型。

让我们看一个直观的例子。假设你正在开发一个模型,可以接受时钟的图像作为输入,并输出一天中的时间(见图 5.16)。

图 5.16 读取时钟上时间的特征工程

如果你选择将图像的原始像素作为输入数据,那么你将面临一个困难的机器学习问题。你将需要一个卷积神经网络来解决它,并且需要耗费相当多的计算资源来训练网络。

但是如果你已经在高层次上理解了问题(你了解人类如何读取时钟面上的时间),你可以为机器学习算法想出更好的输入特征:例如,编写一个五行的 Python 脚本来跟踪时钟指针的黑色像素,并输出每个指针尖端的(x, y)坐标。然后一个简单的机器学习算法可以学会将这些坐标与适当的时间关联起来。

你甚至可以更进一步:进行坐标变换,将(x, y)坐标表示为相对于图像中心的极坐标。你的输入将变为每个时钟指针的角度theta。此时,你的特征使问题变得如此简单,以至于不需要机器学习;一个简单的四舍五入操作和字典查找就足以恢复大致的时间。

这就是特征工程的本质:通过以更简单的方式表达问题来使问题变得更容易。使潜在流形更加平滑、简单、更有组织。通常这需要深入理解问题。

在深度学习之前,特征工程曾经是机器学习工作流程中最重要的部分,因为经典的浅层算法没有足够丰富的假设空间来自动学习有用的特征。你向算法呈现数据的方式对其成功至关重要。例如,在卷积神经网络在 MNIST 数字分类问题上取得成功之前,解决方案通常基于硬编码的特征,如数字图像中的循环次数、图像中每个数字的高度、像素值的直方图等。

幸运的是,现代深度学习消除了大部分特征工程的需求,因为神经网络能够自动从原始数据中提取有用的特征。这是否意味着只要使用深度神经网络,你就不必担心特征工程了?不,有两个原因:

  • 优秀的特征仍然可以让您更优雅地解决问题,同时使用更少的资源。例如,使用卷积神经网络解决读取时钟面的问题是荒谬的。

  • 优秀的特征让您可以用更少的数据解决问题。深度学习模型学习特征的能力依赖于有大量的训练数据可用;如果只有少量样本,那么它们的特征中的信息价值就变得至关重要。

5.4.3 使用早停法

在深度学习中,我们总是使用远远超参数化的模型:它们的自由度远远超过拟合数据的潜在流形所需的最小自由度。这种过度参数化并不是问题,因为你永远不会完全拟合一个深度学习模型。这样的拟合根本不会泛化。你总是会在达到最小可能的训练损失之前中断训练。

找到训练过程中达到最具泛化性拟合的确切点——欠拟合曲线和过拟合曲线之间的确切边界——是改善泛化的最有效的事情之一。

在上一章的例子中,我们会先训练我们的模型比需要的时间更长,以找出产生最佳验证指标的时期数量,然后我们会重新训练一个新模型,确切地达到那个时期数量。这是相当标准的,但它要求你做冗余的工作,有时可能很昂贵。当然,你可以在每个时期结束时保存你的模型,一旦找到最佳时期,就重用你最接近的已保存模型。在 Keras 中,通常使用EarlyStopping回调来实现这一点,它会在验证指标停止改善时立即中断训练,同时记住已知的最佳模型状态。你将在第七章学习如何使用回调。

5.4.4 正则化您的模型

正则化技术是一组最佳实践,积极阻碍模型完美拟合训练数据的能力,目的是使模型在验证期间表现更好。这被称为“正则化”模型,因为它倾向于使模型更简单,更“规则”,其曲线更平滑,更“通用”;因此,它对训练集不那么特定,更能够通过更接近地逼近数据的潜在流形来泛化。

请记住,正则化模型是一个应该始终由准确的评估程序指导的过程。只有通过测量,您才能实现泛化。

让我们回顾一些最常见的正则化技术,并在实践中应用它们来改进第四章的电影分类模型。

减小网络的大小

您已经学到,一个太小的模型不会过拟合。缓解过拟合的最简单方法是减小模型的大小(模型中可学习参数的数量,由层数和每层单元的数量确定)。如果模型的记忆资源有限,它将无法简单地记住其训练数据;因此,为了最小化损失,它将不得不求助于学习具有关于目标的预测能力的压缩表示——这正是我们感兴趣的表示类型。同时,请记住,您应该使用具有足够参数的模型,以便它们不会欠拟合:您的模型不应该缺乏记忆资源。在容量过大容量不足之间需要找到一个折衷。

不幸的是,没有一个神奇的公式可以确定正确的层数或每个层的正确大小。你必须评估一系列不同的架构(当然是在验证集上,而不是在测试集上)以找到适合你数据的正确模型大小。找到合适模型大小的一般工作流程是从相对较少的层和参数开始,并增加层的大小或添加新层,直到看到验证损失的收益递减。

让我们尝试在电影评论分类模型上进行这个操作。以下列表显示了我们的原始模型。

列表 5.10 原始模型

from tensorflow.keras.datasets import imdb
(train_data, train_labels), _ = imdb.load_data(num_words=10000)

def vectorize_sequences(sequences, dimension=10000):
    results = np.zeros((len(sequences), dimension))
    for i, sequence in enumerate(sequences):
        results[i, sequence] = 1. 
    return results
train_data = vectorize_sequences(train_data)

model = keras.Sequential([
    layers.Dense(16, activation="relu"),
    layers.Dense(16, activation="relu"),
    layers.Dense(1, activation="sigmoid")
])
model.compile(optimizer="rmsprop",
              loss="binary_crossentropy",
              metrics=["accuracy"])
history_original = model.fit(train_data, train_labels,
                             epochs=20, batch_size=512, validation_split=0.4)

现在让我们尝试用这个较小的模型替换它。

列表 5.11 具有较低容量的模型版本

model = keras.Sequential([
    layers.Dense(4, activation="relu"),
    layers.Dense(4, activation="relu"),
    layers.Dense(1, activation="sigmoid")
])
model.compile(optimizer="rmsprop",
              loss="binary_crossentropy",
              metrics=["accuracy"])
history_smaller_model = model.fit(
    train_data, train_labels,
    epochs=20, batch_size=512, validation_split=0.4)

图 5.17 显示了原始模型和较小模型的验证损失的比较。

图 5.17 IMDB 评论分类中原始模型与较小模型的比较

正如你所看到的,较小的模型开始过拟合的时间比参考模型晚(在六个周期而不是四个周期之后),一旦开始过拟合,其性能下降速度更慢。

现在,让我们在基准模型中添加一个容量更大的模型——远远超出问题所需的容量。虽然通常使用远超参数化的模型来学习是标准做法,但确实存在记忆容量过大的情况。如果你的模型开始过拟合并且其验证损失曲线看起来波动较大(尽管波动的验证指标也可能是使用不可靠的验证过程的症状,比如验证分割太小),那么你会知道你的模型太大了。

列表 5.12 具有更高容量的模型版本

model = keras.Sequential([
    layers.Dense(512, activation="relu"),
    layers.Dense(512, activation="relu"),
    layers.Dense(1, activation="sigmoid")
])
model.compile(optimizer="rmsprop",
              loss="binary_crossentropy",
              metrics=["accuracy"])
history_larger_model = model.fit(
    train_data, train_labels,
    epochs=20, batch_size=512, validation_split=0.4)

图 5.18 显示了较大模型与参考模型的比较。

图 5.18 原始模型与 IMDB 评论分类中更大模型的比较

较大模型几乎立即开始过拟合,仅经过一个周期,过拟合程度更严重。其验证损失也更加嘈杂。它非常快地将训练损失降至接近零。模型容量越大,就越能快速对训练数据进行建模(导致训练损失较低),但也越容易过拟合(导致训练和验证损失之间的差异较大)。

添加权重正则化

你可能熟悉奥卡姆剃刀原理:对于某事的两种解释,最有可能正确的解释是最简单的解释——即做出更少假设的解释。这个想法也适用于神经网络学习的模型:在给定一些训练数据和网络架构的情况下,多组权重值(多个模型)可以解释数据。简单模型比复杂模型更不容易过拟合。

在这种情况下,简单模型是指参数值分布熵较低的模型(或者是参数较少的模型,正如你在前一节中看到的)。因此,减轻过拟合的常见方法是通过对模型的复杂性施加约束,强制其权重只取小值,这使得权重值的分布更规则。这被称为权重正则化,通过向模型的损失函数添加与权重较大相关的成本来实现。这个成本有两种形式:

  • L1 正则化—添加的成本与权重系数的绝对值成比例(权重的L1 范数)。

  • L2 正则化—添加的成本与权重系数的平方值成比例(权重的L2 范数)。在神经网络的背景下,L2 正则化也被称为权重衰减。不要让不同的名称使你困惑:数学上,权重衰减与 L2 正则化是相同的。

在 Keras 中,通过将权重正则化器实例作为关键字参数传递给层来添加权重正则化。让我们向我们最初的电影评论分类模型添加 L2 权重正则化。

清单 5.13 向模型添加 L2 权重正则化

from tensorflow.keras import regularizers
model = keras.Sequential([
    layers.Dense(16,
                 kernel_regularizer=regularizers.l2(0.002),
                 activation="relu"),
    layers.Dense(16,
                 kernel_regularizer=regularizers.l2(0.002),
                 activation="relu"),
    layers.Dense(1, activation="sigmoid")
])
model.compile(optimizer="rmsprop",
              loss="binary_crossentropy",
              metrics=["accuracy"])
history_l2_reg = model.fit(
    train_data, train_labels,
    epochs=20, batch_size=512, validation_split=0.4)

在前面的清单中,l2(0.002)表示层的权重矩阵中的每个系数将会为模型的总损失增加 0.002 * weight_coefficient_value ** 2。请注意,因为这种惩罚仅在训练时添加,所以该模型的损失在训练时会比在测试时高得多。

图 5.19 显示了 L2 正则化惩罚的影响。正如您所看到的,具有 L2 正则化的模型比参考模型更加抵抗过拟合,尽管两个模型具有相同数量的参数。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5.19 L2 权重正则化对验证损失的影响

作为 L2 正则化的替代,您可以使用以下 Keras 权重正则化器之一。

清单 5.14 Keras 中可用的不同权重正则化器

from tensorflow.keras import regularizers
regularizers.l1(0.001)                    # ❶
regularizers.l1_l2(l1=0.001, l2=0.001)    # ❷

❶ L1 正则化

❷ 同时使用 L1 和 L2 正则化

请注意,权重正则化更常用于较小的深度学习模型。大型深度学习模型往往过于参数化,对权重值施加约束对模型容量和泛化能力的影响不大。在这些情况下,更倾向于使用不同的正则化技术:丢弃。

添加丢弃

Dropout 是神经网络中最有效和最常用的正则化技术之一;它是由杰夫·辛顿及其多伦多大学的学生开发的。应用于层的 Dropout 在训练期间会随机丢弃(设置为零)层的一些输出特征。假设给定层在训练期间对于给定输入样本会返回一个向量 [0.2, 0.5, 1.3, 0.8, 1.1]。应用 Dropout 后,这个向量将随机分布一些零条目:例如,[0, 0.5, 1.3, 0, 1.1]丢弃率是被置零的特征的分数;通常设置在 0.2 和 0.5 之间。在测试时,没有单位被丢弃;相反,层的输出值会按照与丢弃率相等的因子进行缩放,以平衡训练时更多单位处于活动状态的事实。

考虑一个包含层输出 layer_output 的 NumPy 矩阵,形状为 (batch_size, features)。在训练时,我们随机将矩阵中的一部分值置零:

layer_output *= np.random.randint(0, high=2, size=layer_output.shape)  # ❶

❶ 在训练时,输出中的单位有 50% 被丢弃

在测试时,我们通过丢弃率缩小输出。在这里,我们缩小了 0.5(因为之前我们丢弃了一半的单位):

layer_output *= 0.5    # ❶

❶ 在测试时

请注意,这个过程可以通过在训练时执行这两个操作并在测试时保持输出不变来实现,这通常是实践中的实现方式(参见图 5.20):

layer_output *= np.random.randint(0, high=2, size=layer_output.shape)   # ❶
layer_output /= 0.5                                                     # ❷

❶ 在训练时

❷ 请注意,在这种情况下我们是放大而不是缩小。

图 5.20 在训练时应用丢弃到激活矩阵,训练期间进行重新缩放。在测试时,激活矩阵保持不变。

这种技术可能看起来奇怪而武断。为什么这有助于减少过拟合?辛顿说,他受到了银行使用的防欺诈机制的启发,其中包括其他事物。他自己的话是:“我去了我的银行。出纳员经常变动,我问其中一个原因。他说他不知道,但他们经常换岗。我想这一定是因为需要员工之间的合作才能成功欺诈银行。这让我意识到,随机地在每个示例中删除不同的神经元子集将防止阴谋,从而减少过拟合。” 核心思想是在层的输出值中引入噪声可以打破不重要的偶然模式(辛顿称之为阴谋),如果没有噪声,模型将开始记忆。

在 Keras 中,您可以通过Dropout层在模型中引入辍学,该层应用于其前一层的输出。让我们在 IMDB 模型中添加两个Dropout层,看看它们在减少过拟合方面的效果如何。

列表 5.15 向 IMDB 模型添加辍学

model = keras.Sequential([
    layers.Dense(16, activation="relu"),
    layers.Dropout(0.5),
    layers.Dense(16, activation="relu"),
    layers.Dropout(0.5),
    layers.Dense(1, activation="sigmoid")
])
model.compile(optimizer="rmsprop",
              loss="binary_crossentropy",
              metrics=["accuracy"])
history_dropout = model.fit(
    train_data, train_labels,
    epochs=20, batch_size=512, validation_split=0.4)

图 5.21 显示了结果的图表。这明显比参考模型有所改进——它似乎也比 L2 正则化效果更好,因为达到的最低验证损失有所改善。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5.21 辍学对验证损失的影响

总结一下,以下是在神经网络中最大化泛化并防止过拟合的最常见方法:

  • 获取更多训练数据,或更好的训练数据。

  • 开发更好的特征。

  • 减少模型的容量。

  • 添加权重正则化(适用于较小的模型)。

  • 添加辍学。

总结

  • 机器学习模型的目的是泛化:在以前未见过的输入上准确执行。这比看起来更难。

  • 深度神经网络通过学习一个能够成功插值训练样本之间的参数模型来实现泛化——这样的模型可以说已经学会了训练数据的“潜在流形”。这就是为什么深度学习模型只能理解与训练时非常接近的输入。

  • 机器学习中的基本问题是优化和泛化之间的紧张关系:要实现泛化,您必须首先对训练数据进行良好拟合,但随着时间的推移,改进模型对训练数据的拟合将不可避免地开始损害泛化。每一个深度学习最佳实践都涉及管理这种紧张关系。

  • 深度学习模型泛化的能力来自于它们设法学习逼近其数据的潜在流形,因此可以通过插值理解新的输入。

  • 在开发模型时,准确评估模型的泛化能力至关重要。您可以使用各种评估方法,从简单的留出验证到 K 折交叉验证和带有洗牌的迭代 K 折交叉验证。请记住,始终保留一个完全独立的测试集用于最终模型评估,因为您的验证数据可能会泄漏到模型中。

  • 当您开始处理模型时,您的目标首先是实现具有一定泛化能力且可能过拟合的模型。实现此目标的最佳实践包括调整学习率和批量大小,利用更好的架构先验,增加模型容量,或者简单地延长训练时间。

  • 当您的模型开始过拟合时,您的目标转为通过模型正则化来改善泛化。您可以减少模型的容量,添加辍学或权重正则化,并使用提前停止。自然地,更大或更好的数据集始终是帮助模型泛化的首要方法。


¹ 马克·吐温甚至称之为“人类所知最美味的水果”。

六、机器学习的通用工作流程

本章涵盖

  • 制定机器学习问题的步骤

  • 开发一个可工作模型的步骤

  • 部署模型到生产环境并进行维护的步骤

我们先前的示例假设我们已经有一个标记的数据集可以开始训练模型。在现实世界中,这通常不是这样。您不是从数据集开始,而是从问题开始。

想象一下,您正在开设自己的机器学习咨询公司。您注册公司,建立一个漂亮的网站,通知您的网络。项目开始涌入:

  • 为图片分享社交网络设计的个性化照片搜索引擎——输入“婚礼”并检索您在婚礼上拍摄的所有照片,无需任何手动标记。

  • 在新兴聊天应用的帖子中标记垃圾邮件和冒犯性文本内容。

  • 为在线广播用户构建音乐推荐系统。

  • 为电子商务网站检测信用卡欺诈。

  • 预测显示广告的点击率,以决定在特定时间向特定用户提供哪个广告。

  • 在饼干制造线上的传送带上标记异常的饼干。

  • 使用卫星图像预测尚未知晓的考古遗址的位置。

伦理注意事项

有时您可能会被提供道德上可疑的项目,比如“构建一个从某人面部照片中评估其信任度的 AI”。首先,该项目的有效性存疑:不清楚为什么信任度会反映在某人的脸上。其次,这样的任务打开了各种道德问题的大门。为这个任务收集数据集将等同于记录标记图片的人的偏见和成见。您将在这些数据上训练的模型只会将这些偏见编码到一个黑盒算法中,给予它们一层薄薄的合法性外衣。在我们这样一个技术素养较低的社会中,“AI 算法说这个人不可信”似乎比“约翰·史密斯说这个人不可信”更具权威性和客观性,尽管前者只是对后者的学习近似。您的模型将在规模上洗钱和操作化人类判断的最糟糕的方面,对真实人们的生活产生负面影响。

技术从来都不是中立的。如果您的工作对世界产生任何影响,这种影响具有道德方向:技术选择也是伦理选择。始终要谨慎选择您希望您的工作支持的价值观。

如果您可以从keras.datasets导入正确的数据集并开始拟合一些深度学习模型将会非常方便。不幸的是,在现实世界中,您将不得不从头开始。

在本章中,您将学习一个通用的逐步蓝图,您可以使用它来处理和解决任何机器学习问题,就像前面列表中的问题一样。这个模板将汇集和巩固您在第四章和第五章中学到的一切,并为您提供更广泛的背景,以便扎实您将在接下来的章节中学到的内容。

机器学习的通用工作流程大致分为三个部分:

  1. 定义任务—理解问题领域和客户要求背后的业务逻辑。收集数据集,了解数据代表什么,并选择如何衡量任务的成功。

  2. 开发模型—准备数据以便机器学习模型处理,选择一个模型评估协议和一个简单的基准来超越,训练一个具有泛化能力且可以过拟合的第一个模型,然后正则化和调整您的模型,直到实现最佳的泛化性能。

  3. 部署模型—向利益相关者展示您的工作,将模型部署到 Web 服务器、移动应用程序、网页或嵌入式设备,监控模型在实际环境中的性能,并开始收集构建下一代模型所需的数据。

让我们深入了解。

6.1 定义任务

没有对您正在做的事情的背景有深刻的理解,您无法做出好的工作。您的客户为什么要解决这个特定的问题?他们将从解决方案中获得什么价值——您的模型将如何使用,它将如何融入客户的业务流程?有哪些可用的数据,或者可以收集哪些数据?什么样的机器学习任务可以映射到业务问题?

6.1.1 界定问题

构建机器学习问题通常需要与利益相关者进行许多详细的讨论。以下是您应该牢记的问题:

  • 您的输入数据将是什么?您试图预测什么?只有在有训练数据可用的情况下,您才能学会预测某些东西:例如,只有在有电影评论和情感注释可用时,您才能学会对电影评论的情感进行分类。因此,在这个阶段,数据可用性通常是限制因素。在许多情况下,您将不得不自己收集和注释新的数据集(我们将在下一节中介绍)。

  • 您面临的是什么类型的机器学习任务?是二元分类?多类分类?标量回归?向量回归?多类别、多标签分类?图像分割?排名?还是其他类型,如聚类、生成或强化学习?在某些情况下,可能机器学习甚至不是理解数据的最佳方式,您应该使用其他方法,比如传统的统计分析。

    • 照片搜索引擎项目是一个多类别、多标签分类任务。

    • 垃圾邮件检测项目是一个二元分类任务。如果将“具有攻击性内容”设置为单独的类别,则它是一个三分类任务。

    • 音乐推荐引擎最终最好不是通过深度学习来处理,而是通过矩阵分解(协同过滤)。

    • 信用卡欺诈检测项目是一个二元分类任务。

    • 点击率预测项目是一个标量回归任务。

    • 异常饼干检测是一个二元分类任务,但它还需要一个对象检测模型作为第一阶段,以正确裁剪原始图像中的饼干。请注意,被称为“异常检测”的一组机器学习技术在这种情况下并不适用!

    • 从卫星图像中寻找新的考古遗址项目是一个图像相似度排名任务:您需要检索看起来最像已知考古遗址的新图像。

  • 现有解决方案是什么样的?也许您的客户已经有一个手工制作的算法来处理垃圾邮件过滤或信用卡欺诈检测,其中有很多嵌套的if语句。也许目前是一个人手动处理正在考虑的过程——监控饼干工厂的传送带并手动移除坏饼干,或者制作歌曲推荐播放列表以发送给喜欢特定艺术家的用户。您应该确保了解已经存在的系统以及它们是如何工作的。

  • 您是否需要处理特定的约束?例如,您可能会发现您正在为其构建垃圾邮件检测系统的应用程序严格端到端加密,因此垃圾邮件检测模型将不得不存在于最终用户的手机上,并且必须在外部数据集上进行训练。也许饼干过滤模型有延迟约束,因此它将需要在工厂的嵌入式设备上运行,而不是在远程服务器上运行。您应该了解您的工作将适用的完整背景。

一旦你完成了研究,你应该知道你的输入是什么,你的目标是什么,以及问题映射到什么类型的机器学习任务。在这个阶段要注意你所做的假设:

  • 你假设可以根据输入来预测目标。

  • 你假设可用的数据(或者你即将收集的数据)足够信息丰富,可以学习输入和目标之间的关系。

在你有一个可用的模型之前,这些只是等待验证或无效的假设。并不是所有问题都可以通过机器学习解决;仅仅因为你已经收集了输入 X 和目标 Y 的示例并不意味着 X 包含足够的信息来预测 Y。例如,如果你试图根据股票市场的最近价格历史来预测股票的走势,你很可能不会成功,因为价格历史并不包含太多预测信息。

6.1.2 收集数据集

一旦你了解了任务的性质,知道你的输入和目标将是什么,就是数据收集的时候了——大多数机器学习项目中最费力、耗时和昂贵的部分。

  • 图片搜索引擎项目要求你首先选择你想要分类的标签集——你选择了 1 万个常见的图像类别。然后你需要手动为数十万张过去用户上传的图片打上这个标签集中的标签。

  • 对于聊天应用的垃圾信息检测项目,由于用户聊天是端到端加密的,你无法使用其内容来训练模型。你需要获得一个包含数万条未经过滤的社交媒体帖子的独立数据集,并手动将其标记为垃圾信息、冒犯性内容或可接受内容。

  • 对于音乐推荐引擎,你可以只使用用户的“喜欢”。不需要收集新数据。同样对于点击率预测项目:你有过去多年广告的点击率记录。

  • 对于饼干标记模型,你需要在传送带上方安装摄像头,收集数万张图片,然后需要有人手动标记这些图片。目前知道如何做这件事的人在饼干工厂工作,但似乎并不太困难。你应该能够训练人员来做这件事。

  • 卫星图像项目将需要一个考古学家团队收集一个现有感兴趣地点的数据库,对于每个地点,你需要找到在不同天气条件下拍摄的现有卫星图像。为了得到一个好的模型,你需要成千上万个不同的地点。

你在第五章学到,模型的泛化能力几乎完全来自于它所训练的数据的属性——你拥有的数据点数量,标签的可靠性,特征的质量。一个好的数据集是值得关注和投资的资产。如果你有额外的 50 个小时用于项目,最有效的分配方式很可能是收集更多数据,而不是寻找渐进的建模改进。

数据比算法更重要的观点最著名的是由谷歌研究人员在 2009 年发表的一篇名为“数据的不合理有效性”的论文中提出的(标题是对尤金·维格纳 1960 年文章“数学在自然科学中的不合理有效性”进行的改编)。这是在深度学习流行之前,但令人惊讶的是,深度学习的兴起只使数据的重要性更加突出。

如果你正在进行监督学习,那么一旦你收集了输入(如图片),你将需要为它们提供注释(如这些图片的标签)—你将训练模型来预测的目标。有时,注释可以自动获取,比如音乐推荐任务或点击率预测任务的注释。但通常你需要手动为数据进行注释。这是一个劳动密集型的过程。

投资于数据标注基础设施

你的数据标注流程将决定你的目标的质量,进而决定你的模型的质量。仔细考虑你可用的选项:

  • 你应该自己标注数据吗?

  • 你应该使用类似 Mechanical Turk 这样的众包平台来收集标签吗?

  • 是否应该使用专门的数据标注公司的服务?

外包可能会节省你时间和金钱,但会带走控制权。使用类似 Mechanical Turk 这样的东西可能会便宜且扩展性好,但你的标注可能会变得很嘈杂。

要选择最佳选项,考虑你正在处理的限制条件:

  • 数据标注员是否需要是专业领域专家,还是任何人都可以标注数据?猫狗图像分类问题的标签可以由任何人选择,但狗品种分类任务的标签需要专业知识。同时,标注骨折的 CT 扫描几乎需要医学学位。

  • 如果标注数据需要专业知识,你能培训人员来做吗?如果不能,你如何获得相关专家的帮助?

  • 你自己是否了解专家如何进行标注?如果不了解,你将不得不将数据集视为黑匣子,你将无法进行手动特征工程—这并非关键,但可能会有限制。

如果决定在内部标注数据,问问自己你将使用什么软件记录标注。你可能需要自己开发这个软件。高效的数据标注软件会为你节省大量时间,因此在项目早期投资于此是值得的。

警惕非代表性数据

机器学习模型只能理解与之前看到的输入类似的内容。因此,训练使用的数据应该代表生产数据。这个问题应该是所有数据收集工作的基础。

假设你正在开发一个应用,用户可以拍摄一盘食物的照片以找出菜名。你使用来自食客常用的图片分享社交网络的图片来训练模型。在部署时,愤怒用户的反馈开始涌入:你的应用在 10 次中有 8 次答错!怎么回事?你在测试集上的准确率远远超过 90%!快速查看用户上传的数据发现,随机餐馆的随机菜品用随机手机拍摄的移动图片看起来与你训练模型的专业质量、光线充足、诱人的图片完全不同:你的训练数据不代表生产数据。这是一个致命的错误—欢迎来到机器学习地狱。

如果可能的话,直接从模型将要使用的环境中收集数据。电影评论情感分类模型应该用于新的 IMDB 评论,而不是 Yelp 餐厅评论,也不是 Twitter 状态更新。如果你想评价一条推文的情感,首先收集和标注来自与你预期在生产中的用户类似的一组用户的实际推文。如果无法在生产数据上进行训练,那么确保你充分了解你的训练和生产数据的差异,并积极纠正这些差异。

你应该注意的一个相关现象是概念漂移。你几乎会在所有涉及用户生成数据的真实世界问题中遇到概念漂移。概念漂移发生在生产数据的属性随时间变化,导致模型准确性逐渐下降的情况下。2013 年训练的音乐推荐引擎可能在今天效果不佳。同样,你使用的 IMDB 数据集是在 2011 年收集的,基于它训练的模型可能在处理 2020 年的评论时表现不如处理 2012 年的评论,因为词汇、表达和电影类型随时间演变。概念漂移在像信用卡欺诈检测这样的对抗性环境中尤为严重,那里的欺诈模式几乎每天都在变化。处理快速概念漂移需要不断进行数据收集、标注和模型重新训练。

请记住,机器学习只能用于记忆训练数据中存在的模式。你只能识别以前见过的内容。使用过去数据训练的机器学习来预测未来是假设未来会像过去一样。这通常并非如此。

抽样偏差问题

一个特别阴险且常见的非代表性数据案例是抽样偏差。当你的数据收集过程与你试图预测的内容相互作用,导致测量结果出现偏差时,就会发生抽样偏差。一个著名的历史例子发生在 1948 年的美国总统选举中。选举当晚,芝加哥论坛报刊登了“杜威击败杜鲁门”的头条新闻。第二天早上,杜鲁门被宣布为胜者。论坛报的编辑信任了一项电话调查的结果——但 1948 年的电话用户并不是选民群体的随机代表样本。他们更有可能是富裕、保守派的人,更有可能投票给共和党候选人杜威。

“杜威击败杜鲁门”:抽样偏差的一个著名例子

如今,每次电话调查都会考虑到抽样偏差。这并不意味着在政治民意调查中抽样偏差已经成为过去的事情——相反,与 1948 年不同,民意调查员现在已经意识到这一点并采取措施加以纠正。

6.1.3 理解你的数据

将数据集视为黑匣子是相当糟糕的做法。在开始训练模型之前,你应该探索和可视化数据,以获取关于数据预测性的见解,这将为特征工程提供信息,并筛选潜在问题。

  • 如果你的数据包含图像或自然语言文本,请直接查看一些样本(及其标签)。

  • 如果你的数据包含数值特征,绘制特征值的直方图是个好主意,以了解取值范围和不同值的频率。

  • 如果你的数据包含位置信息,请在地图上绘制出来。是否出现了明显的模式?

  • 一些样本是否缺少某些特征的值?如果是,你在准备数据时需要处理这个问题(我们将在下一节介绍如何处理)。

  • 如果你的任务是一个分类问题,请打印出数据中每个类别的实例数。这些类别是否大致平均表示?如果不是,你将需要考虑这种不平衡。

  • 检查目标泄漏:数据中存在的特征提供了关于目标的信息,这些信息在生产中可能不可用。如果你正在训练一个模型来预测未来某人是否会接受癌症治疗,并且记录包括“这个人被诊断患有癌症”这个特征,那么你的目标就会人为地泄漏到你的数据中。始终要问自己,你的数据中的每个特征是否都是在生产中以相同形式可用的?

6.1.4 选择成功的衡量标准

要控制某物,你需要能够观察它。要在项目上取得成功,你必须首先定义你所理解的成功。准确率?精确率和召回率?客户保留率?你对成功的度量标准将指导你在整个项目中做出的所有技术选择。它应直接与你的更高级别目标(例如客户的业务成功)保持一致。

对于平衡分类问题,每个类别等可能出现的情况下,准确率和受试者工作特征曲线(ROC)下的面积,简称为 ROC AUC,是常见的度量标准。对于类别不平衡的问题、排名问题或多标签分类问题,可以使用精确率和召回率,以及加权准确率或 ROC AUC 的形式。而且,定义自己的成功度量标准也并不罕见。要了解机器学习成功度量标准的多样性以及它们与不同问题领域的关系,浏览 Kaggle(kaggle.com)上的数据科学竞赛是很有帮助的;它们展示了各种问题和评估指标。

6.2 开发模型

一旦你知道如何衡量你的进展,就可以开始模型开发。大多数教程和研究项目假设这是唯一的步骤——跳过问题定义和数据集收集,这些被假定已经完成,并跳过模型部署和维护,这些被假定由其他人处理。事实上,模型开发只是机器学习工作流程中的一个步骤,如果你问我,这并不是最困难的步骤。机器学习中最困难的事情是明确问题并收集、注释和清理数据。所以振作起来——接下来的事情相对容易!

6.2.1 准备数据

正如你之前学到的,深度学习模型通常不会直接处理原始数据。数据预处理旨在使手头的原始数据更适合神经网络。这包括向量化、归一化或处理缺失值。许多预处理技术是领域特定的(例如,特定于文本数据或图像数据);在接下来的章节中,当我们在实际示例中遇到它们时,我们将介绍这些技术。目前,我们将回顾所有数据领域通用的基础知识。

向量化

神经网络中的所有输入和目标通常必须是浮点数据张量(或在特定情况下是整数或字符串张量)。无论你需要处理的数据是声音、图像、文本,你都必须首先将其转换为张量,这一步称为数据向量化。例如,在第四章的两个文本分类示例中,我们开始时将文本表示为整数列表(代表单词序列),然后使用独热编码将其转换为float32数据张量。在分类数字和预测房价的示例中,数据以向量化形式呈现,因此我们可以跳过这一步。

值归一化

在第二章的 MNIST 数字分类示例中,我们开始时将图像数据编码为 0-255 范围内的整数,表示灰度值。在将这些数据馈送到网络之前,我们必须将其转换为float32并除以 255,以便最终得到 0-1 范围内的浮点值。类似地,在预测房价时,我们开始时的特征具有各种范围——一些特征具有较小的浮点值,而其他特征具有相当大的整数值。在将这些数据馈送到网络之前,我们必须独立地对每个特征进行归一化,使其具有标准差为 1 和均值为 0。

一般来说,将相对较大的值(例如,多位整数,远大于网络权重初始值的值)或异构数据(例如,一个特征在 0-1 范围内,另一个在 100-200 范围内)输入神经网络是不安全的。这样做可能会触发大的梯度更新,阻止网络收敛。为了让你的网络学习更容易,你的数据应具有以下特征:

  • 取小值—通常,大多数值应该在 0-1 范围内。

  • 保持同质性—所有特征的取值应该在大致相同的范围内。

此外,以下更严格的归一化实践是常见的,可以帮助,尽管并不总是必要的(例如,在数字分类示例中我们没有这样做):

  • 将每个特征独立归一化为均值为 0。

  • 将每个特征独立归一化为标准差为 1。

这在 NumPy 数组中很容易实现:

x -= x.mean(axis=0)      # ❶
x /= x.std(axis=0)

❶ 假设 x 是一个形状为(样本数,特征数)的 2D 数据矩阵

处理缺失值

有时你的数据中可能会有缺失值。例如,在房价示例中,第一个特征(数据中索引为 0 的列)是人均犯罪率。如果这个特征并非所有样本都有呢?那么你的训练或测试数据中就会有缺失值。

你可以选择完全丢弃该特征,但不一定必须这样做。

  • 如果特征是分类的,创建一个新类别表示“该值缺失”是安全的。模型将自动学习这对目标意味着什么。

  • 如果特征是数值的,避免输入像"0"这样的任意值,因为它可能会在特征形成的潜在空间中创建不连续,使得模型更难泛化。相反,考虑用数据集中该特征的平均值或中位数替换缺失值。你也可以训练一个模型,根据其他特征的值来预测该特征的值。

请注意,如果你预期测试数据中会有缺失的分类特征,但网络是在没有任何缺失值的数据上训练的,那么网络就不会学会忽略缺失值!在这种情况下,你应该人为生成带有缺失条目的训练样本:多次复制一些训练样本,并丢弃你预计在测试数据中可能缺失的一些分类特征。

6.2.2 选择评估协议

正如你在前一章中学到的,模型的目的是实现泛化,你在整个模型开发过程中做出的每个建模决策都将由寻求衡量泛化性能的验证指标指导。你的验证协议的目标是准确估计你选择的成功指标(如准确率)在实际生产数据上的表现。这个过程的可靠性对于构建一个有用的模型至关重要。

在第五章中,我们回顾了三种常见的评估协议:

  • 保持留出验证集—当你有大量数据时,这是一个好方法。

  • 进行 K 折交叉验证—当你的样本量太少,无法依靠留出验证来进行可靠评估时,这是正确的选择。

  • 进行迭代的 K 折验证—这是在数据有限的情况下进行高精度模型评估的方法。

选择其中之一。在大多数情况下,第一个就足够好了。但是要记住,始终要注意你的验证集的代表性,并小心不要在训练集和验证集之间有重复的样本。

6.2.3 击败基准

当你开始着手模型本身时,你的初始目标是实现统计功效,就像你在第五章看到的那样:也就是说,开发一个能够击败简单基准的小模型。

在这个阶段,你应该专注于以下三件最重要的事情:

  • 特征工程—过滤掉无信息的特征(特征选择)并利用你对问题的了解开发可能有用的新特征。

  • 选择正确的架构先验—你将使用什么类型的模型架构?密集连接网络、卷积网络、循环神经网络、Transformer?深度学习是否是解决任务的好方法,还是应该使用其他方法?

  • 选择足够好的训练配置—你应该使用什么损失函数?什么批量大小和学习率?

选择正确的损失函数

往往不可能直接优化衡量问题成功的指标。有时没有简单的方法将指标转化为损失函数;毕竟,损失函数需要仅通过一个小批量数据就能计算(理想情况下,损失函数应该能够计算一个数据点)并且必须可微分(否则,你无法使用反向传播来训练你的网络)。例如,广泛使用的分类指标 ROC AUC 不能直接优化。因此,在分类任务中,通常会优化 ROC AUC 的代理指标,如交叉熵。一般来说,你可以希望交叉熵越低,ROC AUC 就越高。

以下表格可以帮助你为几种常见问题类型选择最后一层激活函数和损失函数。

为你的模型选择正确的最后一层激活函数和损失函数

问题类型 最后一层激活函数 损失函数
二元分类 sigmoid binary_crossentropy
多类单标签分类 softmax categorical_crossentropy
多类多标签分类 sigmoid binary_crossentropy

对于大多数问题,你可以从现有的模板开始。你不是第一个尝试构建垃圾邮件检测器、音乐推荐引擎或图像分类器的人。确保你研究先前的技术以识别在你的任务上表现良好的特征工程技术和模型架构。

注意,并非总是能够达到统计功效。如果在尝试多个合理的架构后仍无法超越简单的基准线,可能是你所问的问题在输入数据中没有答案。记住,你正在提出两个假设:

  • 你假设可以根据输入预测输出。

  • 你假设可用数据足够信息丰富,可以学习输入和输出之间的关系。

很可能这些假设是错误的,如果是这样,你必须回到起点。

6.2.4 扩展规模:开发一个过拟合的模型

一旦你获得了具有统计功效的模型,问题就变成了,你的模型是否足够强大?它是否有足够的层和参数来正确地建模手头的问题?例如,逻辑回归模型在 MNIST 上具有统计功效,但不足以很好地解决问题。记住,机器学习中的普遍张力在于优化和泛化之间。理想的模型是一个恰好处于欠拟合和过拟合、欠容量和过容量之间的模型。要弄清楚这个边界在哪里,首先你必须跨越它。

要弄清楚你需要多大的模型,你必须开发一个过拟合的模型。这相当容易,正如你在第五章学到的那样:

  1. 添加层。

  2. 让层变得更大。

  3. 训练更多的 epochs。

始终监控训练损失和验证损失,以及你关心的任何指标的训练和验证值。当你看到模型在验证数据上的表现开始下降时,你已经过拟合了。

6.2.5 正则化和调整你的模型

一旦您达到了统计功效并且能够过度拟合,您就知道自己走在正确的道路上。此时,您的目标是最大化泛化性能。

这个阶段将花费最多的时间:您将反复修改您的模型,训练它,在验证数据上评估(此时不是测试数据),再次修改它,并重复,直到模型达到最佳状态。以下是您应该尝试的一些事项:

  • 尝试不同的架构;添加或删除层。

  • 添加丢弃(dropout)。

  • 如果您的模型很小,添加 L1 或 L2 正则化。

  • 尝试不同的超参数(例如每层的单元数或优化器的学习率)以找到最佳配置。

  • 可选地,迭代数据整理或特征工程:收集和注释更多数据,开发更好的特征,或删除看起来不具信息量的特征。

可以通过使用自动化超参数调整软件(如 KerasTuner)来自动化大部分工作。我们将在第十三章中介绍这个内容。

要注意以下事项:每次您使用验证过程的反馈来调整模型时,都会泄漏有关验证过程的信息到模型中。这样做几次是无害的;但如果在许多迭代中系统地重复这样做,最终会导致您的模型过度拟合验证过程(即使没有任何模型直接在任何验证数据上训练)。这会使评估过程变得不太可靠。

一旦您开发出令人满意的模型配置,您可以在所有可用数据(训练和验证)上训练最终的生产模型,并最后一次在测试集上评估它。如果测试集上的性能明显低于在验证数据上测量的性能,这可能意味着您的验证程序毕竟不可靠,或者在调整模型参数时开始过拟合验证数据。在这种情况下,您可能需要切换到更可靠的评估协议(如迭代 K 折验证)。

6.3 部署模型

您的模型已成功通过了测试集上的最终评估,它已准备好部署并开始其生产生活。

6.3.1 向利益相关者解释您的工作并设定期望

成功和客户信任在于始终满足或超越人们的期望。您交付的实际系统只是这个画面的一半;另一半是在推出前设定适当的期望。

非专业人士对人工智能系统的期望往往是不切实际的。例如,他们可能期望系统“理解”其任务,并能在任务背景下行使类似人类常识的能力。为了解决这个问题,您应该考虑展示一些模型的失败模式的示例(例如,展示被错误分类的样本是什么样子,特别是那些误分类看起来令人惊讶的样本)。

他们可能还期望人类水平的表现,特别是对以前由人类处理的流程。大多数机器学习模型,因为它们(不完美地)训练来逼近人类生成的标签,并没有真正达到那里。您应该清楚地传达模型表现的期望。避免使用抽象的陈述,如“模型的准确率为 98%”(大多数人心理上会四舍五入到 100%),而更倾向于谈论假阴性率和假阳性率。您可以说,“使用这些设置,欺诈检测模型将有 5%的假阴性率和 2.5%的假阳性率。每天,平均有 200 笔有效交易会被标记为欺诈并送去人工审核,平均会漏掉 14 笔欺诈交易。平均会正确捕捉 266 笔欺诈交易。”清楚地将模型的性能指标与业务目标联系起来。

你还应该确保与利益相关者讨论关键启动参数的选择——例如,应该在哪个概率阈值下标记交易(不同的阈值会产生不同的假阴性和假阳性率)。这些决策涉及权衡,只有深刻理解业务背景才能处理。

6.3.2 发布推理模型

当你到达一个可以保存训练模型的 Colab 笔记本时,一个机器学习项目并没有结束。你很少会将在训练过程中操作的完全相同的 Python 模型对象投入生产。

首先,你可能希望将模型导出为除 Python 之外的其他形式:

  • 你的生产环境可能根本不支持 Python——例如,如果是移动应用程序或嵌入式系统。

  • 如果应用程序的其余部分不是用 Python 编写的(可能是 JavaScript、C++ 等),使用 Python 来提供模型可能会引入显著的开销。

其次,由于你的生产模型只用于输出预测(一个称为推理的阶段),而不是用于训练,你有机会进行各种优化,使模型更快速并减少其内存占用。

让我们快速看一下你可以使用的不同模型部署选项。

将模型部署为 REST API

这可能是将模型转化为产品的常见方式:在服务器或云实例上安装 TensorFlow,并通过 REST API 查询模型的预测。你可以使用类似 Flask(或任何其他 Python Web 开发库)的东西构建自己的服务应用程序,或者使用 TensorFlow 的自己的库来将模型作为 API 进行部署,称为TensorFlow Servingwww.tensorflow.org/tfx/guide/serving)。使用 TensorFlow Serving,你可以在几分钟内部署一个 Keras 模型。

当你需要使用这种部署设置时

  • 将消费模型预测的应用程序将可靠地访问互联网(显然)。例如,如果你的应用程序是一个移动应用程序,从远程 API 提供预测意味着应用程序在飞行模式或低连接环境下无法使用。

  • 应用程序没有严格的延迟要求:请求、推理和答复往返通常需要大约 500 毫秒。

  • 发送用于推理的输入数据并不高度敏感:数据需要以解密形式在服务器上可用,因为模型需要查看它(但请注意,应该使用 SSL 加密进行 HTTP 请求和答复)。

例如,图像搜索引擎项目、音乐推荐系统、信用卡欺诈检测项目和卫星图像项目都非常适合通过 REST API 进行服务。

将模型部署为 REST API 时一个重要的问题是,你是想自己托管代码,还是想使用完全托管的第三方云服务。例如,Google 的产品 Cloud AI Platform 允许你简单地将你的 TensorFlow 模型上传到 Google Cloud Storage(GCS),并提供一个 API 端点来查询它。它会处理许多实际细节,如批处理预测、负载平衡和扩展。

在设备上部署模型

有时,你可能需要让你的模型与使用它的应用程序运行在同一设备上——也许是智能手机、机器人上的嵌入式 ARM CPU,或者微型设备上的微控制器。你可能见过一款相机,能够自动检测你对准的场景中的人和面孔:那很可能是一款直接在相机上运行的小型深度学习模型。

当你需要使用这种设置时

  • 你的模型具有严格的延迟约束或需要在低连接环境中运行。如果你正在构建一款沉浸式增强现实应用程序,查询远程服务器不是一个可行的选择。

  • 您的模型可以足够小,以便在目标设备的内存和功率约束下运行。您可以使用 TensorFlow 模型优化工具包来帮助实现此目标(www.tensorflow.org/model_optimization)。

  • 获得最高可能的准确性对您的任务并非至关重要。运行时效率和准确性之间总是存在权衡,因此内存和功率约束通常要求您部署一个不如在大型 GPU 上运行的最佳模型好的模型。

  • 输入数据非常敏感,因此不应在远程服务器上解密。

我们的垃圾邮件检测模型将需要在最终用户的智能手机上运行,作为聊天应用程序的一部分,因为消息是端到端加密的,因此无法由远程托管模型读取。同样,坏 cookie 检测模型具有严格的延迟约束,并且需要在工厂中运行。幸运的是,在这种情况下,我们没有任何功率或空间约束,因此我们实际上可以在 GPU 上运行模型。

要在智能手机或嵌入式设备上部署 Keras 模型,您的首选解决方案是 TensorFlow Lite(www.tensorflow.org/lite)。这是一个用于在设备上高效进行深度学习推断的框架,可在 Android 和 iOS 智能手机以及基于 ARM64 的计算机、树莓派或某些微控制器上运行。它包括一个转换器,可以直接将您的 Keras 模型转换为 TensorFlow Lite 格式。

在浏览器中部署模型

深度学习经常用于基于浏览器或桌面的 JavaScript 应用程序。虽然通常可以让应用程序通过 REST API 查询远程模型,但在浏览器中直接运行模型(利用 GPU 资源(如果可用))可能具有关键优势。

当以下情况发生时,请使用此设置

  • 您希望将计算卸载到最终用户,这可以大大降低服务器成本。

  • 输入数据需要保留在最终用户的计算机或手机上。例如,在我们的垃圾邮件检测项目中,Web 版本和桌面版聊天应用程序(作为用 JavaScript 编写的跨平台应用程序实现)应使用本地运行的模型。

  • 您的应用程序具有严格的延迟约束。虽然在最终用户的笔记本电脑或智能手机上运行的模型可能比在您自己服务器上的大型 GPU 上运行的模型慢,但您没有额外的 100 毫秒的网络往返时间。

  • 在模型已被下载并缓存后,您需要确保您的应用程序在没有连接的情况下继续工作。

只有在您的模型足够小,不会占用用户笔记本电脑或智能手机的 CPU、GPU 或 RAM 时,才应选择此选项。此外,由于整个模型将被下载到用户设备上,因此您应确保模型的任何内容都不需要保密。请注意,鉴于训练过的深度学习模型,通常可以恢复有关训练数据的一些信息:如果模型是在敏感数据上训练的,则最好不要将训练过的模型公开。

要在 JavaScript 中部署模型,TensorFlow 生态系统包括 TensorFlow.js(www.tensorflow.org/js),这是一个用于深度学习的 JavaScript 库,几乎实现了所有 Keras API(最初以 WebKeras 为工作名称开发)以及许多较低级别的 TensorFlow API。您可以轻松地将保存的 Keras 模型导入到 TensorFlow.js 中,以便在基于浏览器的 JavaScript 应用程序或桌面 Electron 应用程序中查询它。

推断模型优化

在部署在具有严格可用功率和内存约束(智能手机和嵌入式设备)或具有低延迟要求的环境中时,优化您的模型以进行推断尤为重要。在将模型导入到 TensorFlow.js 或导出到 TensorFlow Lite 之前,您应始终寻求优化您的模型。

有两种流行的优化技术可以应用:

  • 权重剪枝—权重张量中的每个系数并不都对预测产生相同的贡献。通过仅保留最重要的系数,可以显著降低模型层中的参数数量。这减少了模型的内存和计算占用,但在性能指标上略有损失。通过决定要应用多少剪枝,你可以控制大小和准确性之间的权衡。

  • 权重量化—深度学习模型是使用单精度浮点(float32)权重训练的。然而,可以将权重量化为 8 位有符号整数(int8),以获得一个仅用于推断的模型,其大小是原始模型的四分之一,但保持接近原始模型的准确性。

TensorFlow 生态系统包括一个权重剪枝和量化工具包(www.tensorflow.org/model_optimization),它与 Keras API 深度集成。

6.3.3 在实际环境中监控你的模型

你已经导出了一个推断模型,将其集成到你的应用程序中,并在生产数据上进行了干扰测试—模型的行为与你预期的完全一致。你已经编写了单元测试以及日志记录和状态监控代码—完美。现在是时候按下大红按钮,部署到生产环境了。

即使这还不是结束。一旦部署了模型,你需要继续监控其行为,对新数据的性能,与应用程序其他部分的交互以及对业务指标的最终影响。

  • 在部署新的音乐推荐系统后,你的在线广播的用户参与度是上升还是下降?切换到新的点击率预测模型后,平均广告点击率是否增加?考虑使用随机 A/B 测试来分离模型本身的影响和其他变化:一部分案例应该经过新模型,而另一部分控制案例应该继续使用旧流程。一旦处理了足够多的案例,两者之间的结果差异很可能归因于模型。

  • 如果可能的话,定期对模型在生产数据上的预测进行手动审核。通常可以重复使用与数据标注相同的基础设施:将一部分生产数据发送到手动标注,然后将模型的预测与新的标注进行比较。例如,你应该绝对为图像搜索引擎和恶意 cookie 标记系统做这个工作。

  • 当无法进行手动审核时,考虑替代的评估途径,比如用户调查(例如,在垃圾邮件和有害内容标记系统的情况下)。

6.3.4 维护你的模型

最后,没有模型能永远持续。你已经了解了概念漂移:随着时间的推移,你的生产数据的特征将发生变化,逐渐降低模型的性能和相关性。你的音乐推荐系统的寿命将以周计算。对于信用卡欺诈检测系统,将以天计算。最好情况下,图像搜索引擎的寿命为几年。

一旦你的模型启动,你应该准备好训练下一个将取代它的新一代。因此,

  • 注意生产数据的变化。是否有新特征可用?是否应该扩展或编辑标签集?

  • 继续收集和标注数据,并随着时间的推移改进你的标注流水线。特别要注意收集那些对当前模型分类困难的样本—这些样本最有可能帮助提高性能。

这就结束了机器学习的通用工作流程——需要记住的事情很多。成为专家需要时间和经验,但不要担心,您现在比几章前聪明多了。您现在熟悉了整体情况——机器学习项目所涉及的整个范围。虽然本书的大部分内容将集中在模型开发上,但您现在意识到这只是整个工作流程的一部分。始终牢记整体情况!

摘要

  • 当您开始新的机器学习项目时,首先定义手头的问题:

    • 理解您要做的事情的更广泛背景——最终目标是什么,有哪些约束条件?

    • 收集和注释数据集;确保深入了解数据。

    • 选择如何衡量问题的成功——您将在验证数据上监视哪些指标?

  • 一旦您理解了问题并拥有适当的数据集,就开发一个模型:

    • 准备您的数据。

    • 选择评估协议:留出验证?K 折验证?应该使用数据的哪一部分进行验证?

    • 实现统计功效:击败一个简单的基准。

    • 扩展:开发一个可以过拟合的模型。

    • 正则化您的模型并调整其超参数,基于验证数据的性能。许多机器学习研究往往只关注这一步,但要牢记整体情况。

  • 当您的模型准备就绪并在测试数据上表现良好时,就是部署的时候了:

    • 首先,确保与利益相关者设定适当的期望。

    • 优化最终模型以进行推断,并将模型部署到选择的部署环境——Web 服务器、移动设备、浏览器、嵌入式设备等。

    • 监控模型在生产中的性能,并继续收集数据,以便开发下一代模型。

本文含有隐藏内容,请 开通VIP 后查看