2.2 Learn and use ML | 学习使用机器学习

本节内容受到 Deep Learning with Python 一书的启发。教程使用 Tensorflow 中的高级 Python API tf.keras 来构建和训练深度学习模型。学习如何利用 Keras 使用 Tensorflow,见 TensorFlow Keras Guide

Deep Learning with Python 介绍了利用 Python 语言和强大的 Keras 库进行深度学习。本书作者是 Keras 的创始人和 Google AI 研究员 François Chollet,本书通过直观的解释和实际的案例帮助你构建对深度学习的理解。

学习机器学习的基础概念,可参考 Machine Learning Crash Course 课程。额外的 Tensorflow 和机器学习资源罗列在 Next Steps 中。

2.2.1 Basic classification | 基础分类问题

训练你的第一个的神经网络模型:基础分类问题

本节将训练一个神经网络模型用于服装图片的分类,例如运动鞋和衬衫。你可以不必对所有的细节都理解,这是一个完整的 Tensorflow 程序的快速概述以及对其细节的说明。

本节将使用 Tensorflow 的高级 API tf.keras 构建和训练模型。

2.2.1.1 Import the Fashion MNIST dataset | 导入 Fashion MNIST 数据集

本节使用的 Fashion MNIST 数据集包含 70,000 个共 10 个类别的灰度图像。这些图像以 28x28 的低分辨率显示不同类型的服装,如图 1.1 所示:

Fashion-MNIST samples (by Zalando, MIT License).

图 1.1: Fashion-MNIST samples (by Zalando, MIT License).

传统的 MNIST 数据集包含了手写数字 (0,1,2 等) 通常作为计算机视觉领域机器学习的 Hello, World 被使用。Fashion MNIST 数据集作为其替代品,保留了与其相同的数据格式。

本节使用 Fashion MNIST 数据集是因为其相比传统的 MNIST 数据集更具有挑战性。两个数据集都相对比较小,适用于验证一个算法是否能够正常工作,也适合我们对代码进行测试和调试。

我们将利用 60,000 张图片用于训练并利用 10,000 张图片用于验证我们的神经网络的分类准确性。你可以直接从 Tensorflow 中访问 Fashion MNIST 数据集,仅需要导入并加载数据即可:

载入的数据以 Numpy 的数组形式返回

  • train_imagestrain_labels 数组为 训练集 的图片和对应的类别。
  • test_imagestest_labels 数组为 测试集 的图片和对应的类别。

每张图片为 28x28 的 Numpy 数组,每个像素点的值介于 0 和 255 之间。标签 labels 为整形数值的数组,值介于 0 和 9 之间。这些便签值对应的衣服的类型 class 如表 1.1 所示:

表 1.1: Fashion MNIST 数据集标签和类别对应关系
标签 类别 类别 (中文)
0 T-shirt/top T恤/短衫
1 Trouser 裤子
2 Pullover 套衫
3 Dress 裙子
4 Coat 大衣
5 Sandal 凉鞋
6 Shirt 衬衫
7 Sneaker 运动鞋
8 Bag
9 Ankle boot 短靴

每张图片被映射到一个标签上。由于类型名称 class names 并没有包含在数据集中,我们需要将其保存在变量中用于后续的绘图:

2.2.1.2 Explore the data | 探索数据

在开始训练模型之前,让我们先对数据集的格式进行初步探索。如下代码显示了在训练集中共包含 60,000 张图片,每张图片由 28x28 个像素构成:

类似的,训练集中共有 60,000 个标签:

每个标签是一个介于 0 和 9 之间的一个整数:

测试集中共包含 10,000 张图片,每张图片由 28x28 个像素构成:

训练集中共有 10,000 个标签:

2.2.1.3 Preprocess the data | 数据预处理

在训练神经网络之前,数据必须进行预处理。如果你查看训练集中的第一个图像,你会发现其像素值介于 0 和 255 之间:

Fashion MNIST 训练集中第一张图片

图 1.2: Fashion MNIST 训练集中第一张图片

在将其喂给神经网络模型之前,我们将其值缩放到 0 和 1 之间。将其类型转换为 float 型,再除以 255,如下为对图片的预处理。特别注意的是我们需要对训练集和测试集均进行相同的预处理:

显示训练集中的前 25 张图片,并将其类型的名称显示在每张图片的下面。验证我们的数据是否格式正确,以便于构建和训练我们的模型。

Fashion MNIST 训练集中前 25 张图片

图 1.3: Fashion MNIST 训练集中前 25 张图片

2.2.1.4 Build the model | 构建模型

构建神经网络模型需要设置网络中的每一层,之后再编译整个模型。

2.2.1.4.1 Setup the layers | 设置网络层级

构建一个神经网络模型的基础单元是层 (layer)。每个层从喂给他们的数据中提取信息,同时我们希望这些信息对于当前的问题是更有意义的。

大多数的深度学习模型是由一系列的 layer 构成的链条。对于大多数的 layer,例如 tf.keras.layers.Dense 包含了在训练过程中可被学习的参数。

网络中的第一个 layer,tf.keras.layers.Flatten 将原始的图片由一个 2d-array (28x28 像素) 转换成一个 1d-array (28 * 28 = 784)。这一层可以看做是将图像中的像素一行一行的连接起来,这一层没有参数,其目的仅是转换数据的格式。

将所有像素平坦化 (flattened) 后,后面连接了两个 tf.keras.layers.Dense 层。这两个层称之为全连接层 (densely-connected / fully-connected layer)。第一个全连接层包含 128 个神经元。第二层是一个由 10 个节点构成的 softmax 层,该层将返回一个和为 1 的包含 10 个概率值的数组。其中,每个节点的数值表示当前图片属于该类别的概率。

2.2.1.4.2 Compile the model | 编译模型

在对模型进行训练之前,我们还需要一些额外的设置。这些设置将在模型的编译 (compile) 阶段添加:

  • 损失函数 (loss function):它用于衡量训练过程中模型的准确度。我们希望通过最小化损失函数来控制 (steer) 模型朝着正确的方向优化。
  • 优化器 (optimizer):它控制模型如何利用输入的数据和损失函数对模型的参数进行更新。
  • 度量 (metrics):它用于模型训练和测试阶段的监控。下面的例子中我们使用的是准确率 (accuracy),即被正确分类的图像的占比。

2.2.1.5 Train the model | 训练模型

训练一个神经网络模型需要如下步骤:

  1. 将训练数据喂给模型,在本例中,即为 train_imagestrain_labels 数组。
  2. 模型学习图像和标签之间的关系。
  3. 利用训练好的模型对测试数据进行预测,在本例中,即为 test_images。我们通过预测值和测试集中的标签 test_labels 进行验证。

我们通过调用 model.fit 函数开始模型的训练,模型将会拟合 (fit) 训练集:

随着模型的训练,其损失 (loss) 和准确率 (accuray) 两个度量不断被更新。最终,模型在训练集上达到 89% 的准确率。

2.2.1.6 Evaluate accuracy | 评估准确率

下面,我们利用测试集来评估模型的性能:

可以看出,在测试集上的准确率要比在训练集上的准确率略小。这两者之间的差别即为过拟合 (overfitting),过拟合可以理解为一个机器学习模型在新的数据集上的性能要比在训练集上差的情况。

2.2.1.7 Make predictions | 进行预测

利用训练好的模型,我们可以对一些图片进行预测。

模型对测试集中的所有图片都进行了预测,在此,我们看一下第一张图片的预测结果:

预测结果是由一个包含 10 个数字的数组。其描述了模型对图片属于不同类型判别的概率 (confidence),其中具有最大概率值的标签为:

所以模型得到最可信的结果分类为 ankle boot,即 class_name[9]。我们可以检查测试集的标签看预测是否正确:

让我们画一些真实值和模型预测结果的对比图,我们将正确的预测结果标注为绿色,错误的预测结果标注为红色。

Fashion MNIST 训练集中前 25 张图片预测结果

图 1.4: Fashion MNIST 训练集中前 25 张图片预测结果

最后,我们可以利用训练好的模型对一张图片进行预测。

tf.keras 模型对于一批 (batch) 数据的预测进行了优化,因此即使我们仅对一张图片进行预测,我们也需要将其添加到一个 list 中:

之后再进行预测:

model.predict 返回一个包含 list 的 list,其中每一个为一批数据中的一张图像的预测结果。获取上文中一批图像 (仅 1 张) 的预测结果:

结果同之前的一样,模型对其预测结果为标签 9。

2.2.2 Text classification | 文本分类

电影评论问分类:二分类问题

本节将利用电影评论文本的内容对该电影评论的正向或负向进行分类。这是一个二分类 (binary / two-class classification) 任务的案例,二分类任务是机器学习领域广泛适用的问题。

我们经使用 IMDB 数据集中 50,000 条电影评论数据构建分类模型。该数据集中包含 25,000 条训练数据和 25,000 条测试数据,训练数据和测试数据是均衡的,两者包含相同数量的正向和负向评论。

本节将使用 Tensorflow 的高级 API tf.keras 构建和训练模型。

2.2.2.1 Download the IMDB dataset | 下载 IMDB 数据集

IMDB 数据集已经被打包在 Tensorflow 中,同时评论 (文本序列) 已经被预处理为整型数值的序列,每个整型数值代表词典中一个具体的词。

如下代码可以将 IMDB 数据集下载到你的机器上 (或者你也可以使用已经下载好的缓存副本):

参数 num_words=10000 表示仅保留训练数据中出现频率最高的 10,000 个词。为了保证数据的易控性,我们将不常见的词进行了舍弃处理。

2.2.2.2 Explore the data | 探索数据

让我们首先来了解一下数据的格式。数据是经过预处理的,每个样本是一个整型数值的数组,表示电影评论中的文本。每个标签是 0 或 1 的整型数值,0 表示是一个负向的评论,1 表示是一个正向的评论。

评论的文本已经被转换成了数值类型,每个整型数值表示词典中一个具体的词。我们来看一下第一条评论长什么样子:

电影评论的长度各有不同。如下代码展示了第一条评论和第二条评论的长度。由于输入到神经网络模型的数据必须有相同的长度,因此我们需要在后续对其进行处理。

2.2.2.2.1 Convert the integers back to words | 将整型数值转换回文本

知道如何将整型数值转换回文本将对于我们的理解有所帮助。在此,我们构建一个工具函数 (helper function) 用于查询一个包含从整型数值到文本的映射词典。

接下来我们就可以利用 decode_review 函数来显示我们第一条评论的原始文本了:

2.2.2.3 Prepare the data | 准备数据

评论数据 (整型数组) 在喂给神经网络之前需要先转换成张量 (Tensors),这种转换可以通过多种方式实现:

  • 独热编码 (one-hot-encode) 可以经他们转换成由 0 和 1 构成的向量。例如,序列 [3, 5] 将会变成一个 10,000 维的向量,其中除了 3 和 5 的位置的值为 1,其他位置的值均为 0。我们将编码后的数据作为网络的第一层,一个可以处理浮点向量数据的全连接层 (Dense Layer)。但这种方式的编码会占用大量的内存空间,因为其需要存储 num_words * num_reviews 大小的矩阵。
  • 另一种方式,我们首先需要填补数组使得所有的数组具有相同的长度。之后,我们构建一个形状为 num_examples * max_length 的整型张量。我们使用一个能够处理这个形状的嵌入层 (Embedding Layer) 作为网络的第一层。

在本节中,我们使用第二种方法进行转换。

因为需要保证所有的电影评论的长度相同,在此我们使用 pad_sequences 函数来统一长度:

处理之后,我们看一下不同样本的长度:

经过填补的第一条评论如下:

2.2.2.4 Build the model | 构建模型

一个神经网络是由多层神经元构成的,因此在构建一个神经网络的时候,对于网络架构我们主要需确定如下两个问题:

  • 网络有多少层?
  • 每一层中有多少个神经元?

在示例中,输入数据是由词在词典中的位置构成的数组,待预测的标签为 0 或 1,针对这个问题,我们构建如下模型:

所有层以序列形式的堆叠组成了最终的分类器:

  1. 第一层是一个嵌入层 (Embedding Layer)。这一层接受整型编码的词汇表,对每个整型编码的词通过查询得到其对应的嵌入向量。这些词向量通过模型的训练可以学得。这些向量输出一维信息到输出数组中,其维度为:(batch, sequence, embedding)
  2. 接下来,一个 GlobalAveragePooling1D 层对不同维度的值计算均值返回一个固定长度的输出向量。该操作是一种最简单的处理可变长度输入的方法。
  3. 一个固定长度的输出向量被接到了一个包含 16 个隐含节点的全连接层 (Dense Layer)。
  4. 最后一层是一个连接到最终一个输出节点的全连接层,使用 sigmoid 作为激活函数,最后的输出结果是一个介于 0 和 1 之间的浮点数,代表概率或置信水平。
2.2.2.4.1 Hidden units | 隐含节点

上面的模型中,在输入层和输出层之间有两个中间层或隐含层 (Hidden Layer)。输出的数量 (units, nodes 或 neurons) 即为该层表征空间 (Representational Space) 的维度。换言之,即为网络在学习内部表征时所允许的自由度。

如果一个神经网络拥有更多的隐含节点 (一个高维的表征空间),或更多的隐含层,那么该网络就能够学习到更复杂的表征。当然,这也可能使得神经网络变得复杂和不易优化,或是学习到一些不该学到的模式。这些不该学到的模式往往使得模型在训练数据上有很好的效果,但是在测试集上表现不佳,这种情况我们称之为过拟合 (Overfitting),我们将在后面讨论这个问题。

2.2.2.4.2 Loss function and optimizer | 损失函数和优化器

一个模型需要一个损失函数和一个优化器用于训练。因为这是一个二分类问题,同时模型输出一个概率 (一个单节点的,以 sigmoid 函数为激活函数的层),因此我们使用 binary_crossentropy 作为损失函数。

当然,这并不是唯一的选择,除此之外我们也可以选择 mean_squared_error 损失函数。不过,一般情况下,binary_crossentropy 损失函数在处理概率的时候效果更好。binary_crossentropy 衡量了两个概率分布之间的距离,在我们的示例中,即为真实情况的分布和预测分布之间的距离。

后面,我们将探索回归 (Regression) 问题 (例如预测房屋的价格),我们将展示如果使用另一种名为均方根误差 (Mean Suqared Error) 的损失函数。

现在,我们为模型添加一个优化器和损失函数:

2.2.2.5 Create a validation set | 构造一个验证集

在训练模型的过程中,我们希望检查模型在未知数据上的准确率。因此我们需要从原始的训练数据的 10,000 个样本中划分出来一些构成验证集。(为什么不使用测试集呢?因为我们的目标是仅利用训练数据来优化我们的模型,最后利用测试数据一次性评估模型的准确率)。

2.2.2.7 Evaluate the model | 评估模型

现在让我们评估一下模型的性能如何。评估将返回两个值,一个是损失 (Loss,代表了模型的误差,数值越小越好),另一个是准确率。

这种比较简单的做法最终的准确率在 87%,配合一些更高级的方法,模型的准确率可以提升至接近 95%。

2.2.2.8 Create a graph of accuracy and loss over time | 绘制准确率和损失的变化图

model.fit() 可以返回一个历史 (History) 对象,一个包含了训练过程中所发生事件的词典。

其中包含了 4 种条目,每一种代表了在训练和验证过程中被记录的度量值。我们可以使用这些数据绘制训练和验证过程中损失和准确率的变化图:

训练和验证的损失

图 1.5: 训练和验证的损失

训练和验证的准确率

图 1.6: 训练和验证的准确率

在上图中,为训练数据的损失和准确率,实线为验证数据的损失和准确率。

可以看出训练数据的损失随着每一轮的训练不断减小,准确率随着每一轮训练不断增加。这正是我们利用梯度下降算法进行优化时所期望的,每一轮都会最小化设置的损失。

验证数据的损失和准确率的变化略有不同,在 20 轮后似乎达到了峰值。这是过拟合的一个典型的例子,模型在训练数据上的表现要比在未见过的数据上表现更好。此时,模型过度优化并学习到了仅在训练数据上才有的模式,而这些模式并不能够在测试数据上有很好的泛化能力。

对于这个例子,我们可以通过在 20 轮后停止训练避免过拟合。后面,我们将展示如何利用一个 callback 来自动化处理这个问题。

2.2.3 Regression | 回归

在回归 (Regression) 问题中,我们的目标是预测一个连续的值,例如价格或概率。相对的,一个分类 (Classification) 问题则是预测一个离散的标签 (例如:一个图片是包含包含一个苹果还是一个橙子)。

本节我们将构建一个模型预测二十世纪 70 年代波士顿郊区的房价。为此,我们将利用一些其他的信息用于构建模型,例如:犯罪率和本地的财产税等。

本节将使用 Tensorflow 的高级 API tf.keras 构建和训练模型。

2.2.3.1 The Boston Housing Prices dataset | 波士顿房价数据

波士顿房价数据集已经被打包在 Tensorflow 中,下载并打乱数据:

2.2.3.1.1 Examples and features | 样本和特征

波士顿房价数据集要远比我们已经处理过的数据集要小,共包括了 506 个样本,其中训练样本 404 条,测试样本 102 条:

数据包含的 13 个特征如下:

  1. 人均犯罪率
  2. 超过 25,000 平方英尺的居住地的占比
  3. 每个城镇非零售业面积的占比
  4. Charles 河流的呀变量 (如果是河流边界则为 1,否则为 2)
  5. 氮氧化物浓度
  6. 每个住宅的平均房间数量
  7. 1940 年以前建造的自由住房占比
  8. 5 个波士顿就业中心的加权距离
  9. 高速公路的便捷性指数
  10. 每万美元的全额财产税税率
  11. 每个城镇小学教师的比率
  12. \(1000 \times (\text{Bk} - 0.63)\),其中 Bk 为城镇黑人占比
  13. 较差生活状态的人口占比

每个特征的数值范围不尽相同,有些特征是一个介于 0 和 1 之间的占比值,有些特征则是介于 1 和 12 或 0 到 100 之间。现实生活的数据往往就是这样,所以对数据的探索性分析和清洗就十分重要。

作为一个建模和开发人员,我们需要考虑如何使用这些数据以及据此构建的模型的预测的潜在收益和危害,例如本例中的模型可能会加剧社会偏见和差异。一个特征是否与你在解决的问题相关,或其是否会引入偏置?想了解更多的信息,请参见 ML fairness

利用 pandas 库可以以漂亮的格式来显示数据的前几行:

表 1.2: 波士顿房价数据 (前 5 行)
CRIM ZN INDUS CHAS NOX RM AGE DIS RAD TAX PTRATIO B LSTAT
1 0.07875 45 3.44 0 0.437 6.782 41.1 3.7886 5 398 15.2 393.87 6.68
2 4.55587 0 18.10 0 0.718 3.561 87.9 1.6132 24 666 20.2 354.70 7.12
3 0.09604 40 6.41 0 0.447 6.854 42.8 4.2673 4 254 17.6 396.90 2.98
4 0.01870 85 4.15 0 0.429 6.516 27.7 8.5353 4 351 17.9 392.43 6.36
5 0.52693 0 6.20 0 0.504 8.725 83.0 2.8944 8 307 17.4 382.00 4.63
2.2.3.1.2 Labels | 标签

标签为以千美元为单位的房价。(需要注意的是此处为二十世纪七十年代中期的价格)

2.2.3.2 Normalize features | 规范化特征

在使用不同量纲和大小的特征的时候,我们建议对其进行规范化。对于每个特征,我们可以减去其均值,然后再除以其标准差:

尽管不进行特征规范化模型也可能收敛,但这会让训练变得比较慢,同时也会让结果模型过多的依赖对于出入节点的选择。

2.2.3.4 Train the model | 训练模型

我们利用训练数据对模型训练 500 轮,训练和验证的准确率被保存在 history 对象中。

利用 history 对象中保存的统计数据对训练过程进行可视化。我们希望据此确定训练模型花费了多长的时间。

波士顿房价预测模型 MAE 变化曲线

图 1.7: 波士顿房价预测模型 MAE 变化曲线

从图中可以看出,模型在 200 轮训练后几乎就不再有改进。让我们更新 model.fit 方法,使得当验证效果不再有改进时自动停止训练。我们将使用一个回调 (callback) 来在每轮训练中测试训练,如果若干轮训练未带来改进,则自动停止训练。

你可以在 这里 获得更多关于回调的信息。

波士顿房价预测模型 MAE 变化曲线

图 1.8: 波士顿房价预测模型 MAE 变化曲线

上图显示了平均的误差为 2,500 美元,效果如何?其实 2,500 美元并不是一个小数目,尤其是一些标签数据仅为 15,000 美元。

2.2.3.6 Conclusion | 结论

本案介绍了处理回归问题的一些方法。

  • 均方根误差 (Mean Squared Error, MSE) 是一种常用于回归问题的损失函数。
  • 类似的,回归问题的平均指标也与分类问题不同,常用的回归问题的评价指标为平均绝对误差 (Mean Absolute Error, MAE)。
  • 当输入数据的特征有不同的量纲和取值范围时,每个格正都应该被单独规范化。
  • 当没有大量的训练数据时,建议选择一个包含少量隐含层的小网络来避免过拟合。
  • 早停 (Early Stopping) 是一种实用的避免过拟合的方法。

2.2.4 Overfitting and underfitting | 过拟合和欠拟合

同样,本例也将使用 tf.keras API,你可以在 TensorFlow Kears Guide 中获取更多相关信息。

在我们之前的 IDMB 电影评论分类和波士顿房价预测的案例中,可以看出验证集的准确率在一定轮数后达到一个峰值,之后便开始下降。换言之,我们的模型在训练数据上发生了过拟合 (Overfitting),因此学习如何处理过拟合将十分重要。尽管我们能够在训练数据上取得很高的准确率,但是我们真正想得到的是一个能够在测试数据 (未见过的数据) 上有更好泛化能力的模型。

过拟合的对立面是欠拟合 (Undefitting),欠拟合是指如果继续进行训练,模型在测试数据上任有改进的空间。也就是说我们的模型还并没有学习到训练数据中所有有关的模式。

当我们训练模型过长时间后,模型将开始产生过拟合,其将会学习到尽在训练集中存在并无法在测试数据上有很好的泛化能力的模式。因此我们需要寻找一个平衡点,下面我们将探讨如何训练适当的轮数,这将是一项很重要的技能。

为了方式过拟合的发生,最好的解决方案就是使用更多的训练数据,一个利用更多数据训练的模型往往能够有更好的泛化能力。但当这种情况不太现实的时候,次优的方案就是使用想正则化等类似技术,这将限制模型所存储信息的数量和类型。如果一个模型仅能够记住少量的模式,那么在优化的过程中将会促使其更多的关注那些更为有用的模式,进而使得模型能够有机会获得更好的泛化能力。

在本例中,我们将探索两种常用的正则化技术:权重正则化和 Dropout,我们将使用这两种技术来改进 IMDB 电影评论分类模型。

2.2.4.1 Download the IMDB dataset | 下载 IMDB 数据集

不同于之前示例中使用的 Embedding 的方法,本例中我们使用句子级别的 Multi-Hot 编码。这样的话,模型将会很快在训练数据上出现过拟合,因此我们将用其作为过拟合的演示示例,并介绍如何解决这个问题。

Multi-Hot 编码是指将数据转化成仅由 0 和 1 构成的向量。也就是说对于 [3, 5] 这样一个序列,我们会将其转换成一个 10,000 维度的向量,该向量中除了第 3 和 5 位值为 1 外,其他的值均为 0。

我们来看一下转换后的的一个 Multi-Hot 编码的向量,因为词典中词的下标是按照词频逆序排序的,因此下图中可以看出在词下标 0 附近有更多的值。

IMDB 电影评论第一条数据 Multi-Hot 编码

图 1.9: IMDB 电影评论第一条数据 Multi-Hot 编码

2.2.4.2 Demonstrate overfitting | 演示过拟合问题

解决过拟合问题的最简单的方式是减小模型的大小,例如模型中可学习的参数个数 (在神经网络中由网络层的个数和每层中节点的个数决定)。在深度学习中,一个模型的可学习的参数通常称之为模型的容量 (cpcity)。直观上,一个拥有更多参数的模型会有更好的记忆能力 (memorization capacity),这样就能够比较容易的学习一个从训练样本到其目标的完美的字典式 (dictionary-like) 的映射。但这种映射没有任何泛化能力,因此将无法对之前未遇见过的数据上进行预测。

要牢记:深度学习模型倾向于更好的拟合训练数据,但真正的挑战是泛化,而非拟合。

换言之,当网络仅有有限的记忆资源时,将无法很容易的学习到映射模式。当最小化损失时,网络就必须学习具有更强预测能力的压缩表示 (compressed representations)。与此同时,当模型过小时,网络将很难拟合训练数据,因此我们需要在“容量过大” (too much capacity) 和“容量不足” (not enough capacity) 之间进行权衡。

不幸的是,并没有一个完美的方案去确定一个模型的大小和结构 (例如网络的层数和每层的节点数),因此需要进行大量的实验测试不同的网络结构。

为了得到一个合适的模型大小,建议以一些相对较少的网络层数和参数开始。之后在添加新的网络的层和层中节点的过程中观察验证数据上的损失是否减小。让我们在 IMDB 电影评论分类神经网络上实验一下。

我们仅用 Dense 层构建一个简单的模型,之后再构建一个更小的模型并比较他们。

2.2.4.2.1 Create a baseline model | 构建一个基线模型
2.2.4.2.2 Create a smaller model | 构建一个小模型

让我们构建一个具有较少隐含节点的网络,并同我们刚才构建的基线模型进行比较:

然后我们利用相同的数据对其进行训练:

2.2.4.2.3 Create a bigger model | 构建一个大模型

作为一个练习,你可以构建一个足够大的模型来观察其迅速的出现过拟合现象。接下来,让我们将一个要远比问题的空间要大得多的容量的模型加入到我们的对比测试中。

依旧使用相同的数据对其进行训练:

2.2.4.2.4 Plot the training and validation loss | 绘制训练和验证的损失

下图中,实线表示的是训练集的损失,虚线表示的是验证集的损失 (请记住,越小的验证集损失表示模型越好)。从图中可以看出,较小的模型要迟于基线模型发生过拟合,同时在发生过拟合后其性能的下降程度也相对缓慢。

IMDB 电影评论分类模型训练集和测试集损失对比 (基线模型,大模型,小模型)

图 1.10: IMDB 电影评论分类模型训练集和测试集损失对比 (基线模型,大模型,小模型)

可以发现,大的网络在一开始便发生了过拟合,在之后其过拟合的更为严重。当网络的容量越大时,其也容易拟合训练数据 (即训练集的损失很快变小),但越容易发生过拟合 (即验证集和训练集之间的损失差别增大)。

2.2.4.3 Strategies | 应对策略

2.2.4.3.1 Add weight regularization | 添加权重正则项

你也许熟悉奥卡姆剃刀原则:如果对于一个事务有两种解释,往往是“最简单”的,也就是有更少假设的那个往往是正确的。这对于通过神经网络学习到的模型同样适用:给定一些训练数据和一个网络结构,有很多种组合的权重 (即很多个模型) 可以解释数据中的模式,那么越简单的模型相比于复杂的模型越不容易发生过拟合现象。

在这里,一个“简单的模型”的含义就是模型参数值的分布的熵比较小 (或者说一个模型具有更少的参数)。因此,减轻过拟合的一种常见方法是通过添加一个强制权重仅能取到一些小的值的约束,这样做将会使得权重的分布更加的“正则” (Regular)。这种做法称之为权重正则化 (Weight Regularization),其具体做法是在网络的损失函数中添加和权重大小相关的代价。如下为两种常用的形式:

  • L1 正则,增加的代价与权重系数的绝对值成正比 (即与权重的 L1 范数成正比)。
  • L2 正则,增加的代价与权重系数的平方项成正比 (即与权重的 L2 范数成正比)。L2 正则化在神经网络中也称之为权重衰减。不要让不同的叫法把你弄混,权重衰减和 L2 正则化是完全相同的概念。

tf.keras 中,权重正则化是通过给没一层添加一个权重正则化实例实现的。现在,让我们为模型添加 L2 权重正则化。

l2(0.001) 表示权重矩阵中所有的系数都将添加 0.001 * weight_coefficient_value 的值到网络的整体损失中。注意,这种惩罚项尽在训练过程中添加,所以网络在训练阶段要比测试时有更大的损失值。

下面是 L2 正则化惩罚的影响:

IMDB 电影评论分类模型训练集和测试集损失对比 (基线模型,带有 L2 正则化的模型)

图 1.11: IMDB 电影评论分类模型训练集和测试集损失对比 (基线模型,带有 L2 正则化的模型)

从图中可以看出,带有 L2 正则化的模型要比基线模型能够更好的抑制过拟合的发生,尽管两个模型的参数数量是相同的。

2.2.4.3.2 Add dropout | 添加 Dropout

Dropout 是在神经网络中一种最有效和最常用的正则化技术,其是由 Hinton 和他在多伦多大学的学生一同发明的。Dropout 会在其应用的层上在训练时随机的删除 (Dropping Out) 一些输出的特征。例如,一个给定的层训练时对于一个给定的输入返回的结果为 [0.2, 0.5, 1.3, 0.8, 1.1],当添加 Dropout 后,这个向量将会有随机分布的零值,例如:[0, 0.5, 1.3, 0, 1.1]。删除比例 (Dropout Rate) 即为特征被零值化的占比,通常我们会将其设置在 0.2 到 0.5 之间。在测试阶段,将不会有节点的值被删除,而是会一个与删除比例相同的因子进行缩放来平衡其比训练阶段有更多的激活节点。

tf.kears 中,我们可以通过添加一个 Dropout 层来引入 Dropout,其将被添加在没一层输出的前面。

让我们为 IMDB 网络添加两个 Dropout 层,在来观察一下过拟合的抑制情况:

IMDB 电影评论分类模型训练集和测试集损失对比 (基线模型,带有 Dropout 的模型)

图 1.12: IMDB 电影评论分类模型训练集和测试集损失对比 (基线模型,带有 Dropout 的模型)

添加 Dropout 后较之基线模型有明显的改进。

以下是防止神经网络过拟合的常用方法:

  • 获取更多的数据。
  • 减少网络的容量。
  • 添加权重正则化。
  • 添加 Dropout。

以及本节为介绍的数据扩张 (data-augmentation) 和批标准化 (batch normalization)。

2.2.5 Save and resotre models | 保存和恢复模型

在模型的训练中和训练后均可以对其进行保存,这就意味着你可以复用一个训练好的模型,而无需重新对其进行训练。保存同样意味着你可以将其分享给他人用于复现你的工作。当我们发表研究成果的时候,大多数的机器学习实践者会分享:

  • 用于构建模型的代码
  • 训练好的模型权重和参数

这些数据的共享能够帮助其他人更好的理解你的模型是如何工作的,并使用他们自己的数据进行新的尝试。

小心处理不信任的代码,Tensorflow 模型也是代码,更多信息请参考 Using Tensorflow Securely

2.2.5.1 Options | 选项

根据使用的 API 的不同,有多种保存 Tensorflow 模型的方法。本节将继续使用 tf.keras 高级 API 构建和训练 Tensorflow 模型。对于其他的情况,请参见 Save and RestoreSaving in eager

2.2.5.2 Setup | 设置

2.2.5.2.1 Install and imports | 安装和导入

安装和导入 Tensorflow 及其依赖:

pip install -q h5py pyyaml 

2.2.5.3 Save checkpoints during training | 保存检查点

最基本的使用方法就是在模型训练过程中结束时自动保存检查点 (checkpoints)。这样的话你就可以复用一个训练好的模型而无需重新训练它,或者从上次训练中断的地方继续训练。

tf.kears.callbacks.ModelCheckpoint 是一个完成此项任务的回调方法 (callback)。这个回调方法接受一系列参数来配置检查点。

2.2.5.3.1 Checkpoint callback usage | 检查点回调使用

训练模型并将 ModelCheckpoint 回调传给训练函数:

这将产生一些列的 Tensorflow 检查点文件,并在每一轮训练结束时更新他们:

ls {checkpoint_dir}
checkpoint  cp.ckpt.data-00000-of-00001  cp.ckpt.index

构建一个新的未训练的模型,当仅用权重恢复一个模型的时候,你必须保证新的模型与原来的模型有相同的架构了。正因为具有相同的模型架构,我们才能够在不同的实例中共享这些权重。

现在构建一个新的未训练的模型,并利用测试集对其进行测试。一个未训练的模型仅能表现出一个随机的性能水平 (大约 10% 的准确率):

接下来,我们从检查点载入权重,并重新测试:

2.2.5.3.2 Checkpoint callback options | 检查点回调选项

回调函数提供了多个选项用于指定检查点的唯一名称,调整保存检查点的频率等。

构建一个新的模型,每 5 轮保存一个单独的检查点文件:

现在,我们来看一下保存的检查点文件 (以修改日期进行排序):

Tensorflow 默认仅保存最近的 5 个检查点文件。

让我们重置模型,并载入最后一个检查点文件进行测试:

2.2.5.4 What are these files? | 这些是什么文件

上面的代码将权重信息保存到了一系列检查点格式的二进制文件中,其中仅包含了训练权重。检查点文件包括:

  • 一个或多个包含模型权重的分片文件
  • 一个包含了权重分布在不同分片文件中的索引文件

如果你尽在单台机器上进行训练,则仅有一个分片文件,其文件名将以 .data-00000-of-00001 结尾。

2.2.5.6 Save the entire model | 保存整个模型

我们还可以将整个模型保存到一个文件中,包括权重,模型的配置信息,甚至是优化器的配置信息。这将有助于我们在没有原始代码的情况下,保存模型的检查点和后续的继续训练。

保存一个包含全部信息的模型在 Keras 中十分有用,我们可以在 Tensorflow.js 中载入训练好的模型并在浏览器中进行训练和测试。

Keras 提供了一种名为 HDF5 的简单存储格式标准。之于我们的目标,我们可以将保存的模型视为一个单一的二进制文件。

现在我们利用这个文件来恢复我们的模型:

检查一下模型的准确性:

利用这种方法,我们可以保存:

  • 模型的权重信息
  • 模型的配置信息 (架构)
  • 优化器的配置信息

Keras 通过检查整个架构来保存模型。目前还不能够保存 Tensorflow 的优化器 (tf.train),如果使用了其中的代码,需要在模型载入后重新对其进行编译,这将丢失掉优化器的状态信息。

2.2.5.7 What’s Next | 接下来

本节我们简单介绍了 tf.keras 中如何保存和载入。

  • tf.keras guide 中可以找到更多有关 tf.keras 保存和载入的介绍。
  • Saving in eager 中可以找到有关在动态图中保存和载入的介绍。
  • Save and Restore 中可以找到更底层的 Tensorflow 保存和载入的介绍。