TensorFlow、Keras 和深度学习(无需拥有博士学位)

1. 概览

本教程已更新为适用于 TensorFlow 2.2!

74f6fbd758bf19e6.png

在此 Codelab 中,您将学习如何构建和训练可识别手写数字的神经网络。在不断增强神经网络以达到 99% 准确率的过程中,您还将发现深度学习专业人士用来高效训练模型的工具。

此 Codelab 使用 MNIST 数据集,该数据集包含 60,000 个带标签的数字,在近 20 年的时间里,让一代又一代的博士生忙碌不已。您将使用不到 100 行 Python / TensorFlow 代码解决该问题。

学习内容

  • 什么是神经网络以及如何训练神经网络
  • 如何使用 tf.keras 构建基本的单层神经网络
  • 如何添加更多图层
  • 如何设置学习速率安排
  • 如何构建卷积神经网络
  • 如何使用正则化技术:dropout、批次归一化
  • 什么是过拟合

所需条件

只需一个浏览器。本研讨会完全可以在 Google Colaboratory 中运行。

反馈

如果您发现此实验有任何问题,或者认为此实验应进行改进,请告诉我们。我们通过 GitHub 问题处理反馈 [反馈链接]。

2. Google Colaboratory 快速入门

本实验使用 Google Colaboratory,无需您进行任何设置。您可以在 Chromebook 上运行该应用。请打开下面的文件,并执行相应单元格,以便熟悉 Colab 笔记本。

c3df49e90e5a654f.png Welcome to Colab.ipynb

其他说明如下:

选择 GPU 后端

hsy7H7O5qJNvKcRnHRiZoyh0IznlzmrO60wR1B6pqtfdc8Ie7gLsXC0f670zsPzGsNy3QAJuZefYv9CwTHmjiMyywG2pTpnMCE6Slkf3K1BeVmfpsYVw6omItm1ZneqdE31F8re-dA

在 Colab 菜单中,依次选择运行时 > 更改运行时类型,然后选择 GPU。首次执行时,系统会自动连接到运行时,您也可以使用右上角的“连接”按钮。

笔记本执行

evlBKSO15ImjocdEcsIo8unzEe6oDGYnKFe8CoHS_7QiP3sDbrs2jB6lbyitEtE7Gt_1UsCdU5dJA-_2IgBWh9ofYf4yVDE740PwJ6kiQwuXNOLkgktzzf0E_k5VN5mq29ZXI5wb7Q

点击某个单元格,然后使用 Shift-Enter 逐个执行单元格。您还可以通过依次选择运行时 > 全部运行来运行整个笔记本

目录

OXeYYbtKdLCNnw_xovSMeMwSdD7CL_w25EfhnpRhhhO44bYp3zZpU72J5tKaSuo8wpas0GK5B2sTBlIMiFmdGxFRQ9NmwJ7JIRYy5XtpWKQCPdxQVRPy_0J_LshGIKjtw8P9fXozaA

所有笔记本都有目录。您可以使用左侧的黑色箭头打开该菜单。

隐藏单元格

GXTbXUO8xpPFKiGc6Q-cFwFHxHvOa105hHg3vk77EDpStyhU4AQMN3FYenbiBusHXUSk-yGXbRDcK-Cwx18XbDtyqB5WRr3_2jhnLvFxW8a7H_4cGvVDKrEMto_QxhfTeO0hwmrfng

部分单元格将仅显示其标题。这是 Colab 特有的笔记本功能。您可以双击它们来查看其中的代码,但通常不会很有趣。通常是支持或可视化函数。您仍需运行这些单元格,以便定义其中的函数。

3. 训练神经网络

我们将首先观看神经网络的训练过程。请打开下面的笔记本,并运行所有单元格。暂时不要关注代码,我们稍后会开始讲解。

c3df49e90e5a654f.png keras_01_mnist.ipynb

执行笔记本时,请重点关注可视化图表。有关说明,请参阅下文。

训练数据

我们有一个手写数字数据集,其中包含已添加标签的手写数字,因此我们知道每张图片代表什么,即 0 到 9 之间的数字。在笔记本中,您会看到一段摘录:

ad83f98e56054737.png

我们将构建的神经网络会将手写数字分类到 10 个类别(0 到 9)中。它会根据内部参数进行分类,这些参数需要具有正确的值,才能使分类功能正常运行。此“正确值”是通过训练过程学习的,该过程需要包含图片和相关正确答案的“已标记数据集”。

如何判断训练后的神经网络性能是否良好?使用训练数据集来测试网络属于作弊行为。在训练期间,模型已经多次看到该数据集,因此在该数据集上的表现肯定非常出色。我们需要另一个在训练期间从未见过的带标签数据集,以评估网络的“实际”性能。它被称为“验证数据集

训练

随着训练的进行,模型会一次处理一批训练数据,内部模型参数会得到更新,模型识别手写数字的能力也会越来越好。您可以在训练图表中看到它:

3f7b405649301ea.png

在右侧,“准确率”只是正确识别的数字所占的百分比。随着训练的进行,该值会上升,这是好事。

在左侧,我们可以看到损失。为了推动训练,我们将定义一个“损失”函数,该函数表示系统识别数字的错误程度,并尝试将其最小化。您会看到,随着训练的进行,训练数据和验证数据的损失都在下降:这是好事。这意味着神经网络正在学习。

X 轴表示“周期”数或遍历整个数据集的迭代次数。

预测

模型训练完毕后,我们就可以使用它来识别手写数字了。下一个可视化图表显示了模型在本地字体渲染的几个数字(第一行)以及验证数据集的 10,000 个数字上的表现。预测的类别显示在每个数字下方,如果预测错误,则以红色显示。

c0699216ba0effdb.png

如图所示,这个初始模型效果不太好,但仍能正确识别一些数字。其最终验证准确率约为 90%,对于我们开始使用的简单模型来说还不错,但仍意味着它在 1 万个验证数字中遗漏了 1,000 个。这远远超出了可显示的范围,因此看起来好像所有答案都是错误的(红色)。

张量

数据存储在矩阵中。28x28 像素的灰度图片可放入 28x28 的二维矩阵中。但对于彩色图片,我们需要更多维度。每个像素有 3 个颜色值(红色、绿色、蓝色),因此需要一个维度为 [28, 28, 3] 的三维表。若要存储一批 128 张彩色图片,则需要一个维度为 [128, 28, 28, 3] 的四维表。

这些多维表格称为“张量”,其维度的列表称为“形状”

4. [信息]:神经网络 101

简而言之

如果您已经了解下一段中所有粗体字词,则可以继续进行下一个练习。如果您刚刚开始学习深度学习,欢迎您,请继续阅读。

witch.png

对于构建为层序列的模型,Keras 提供了 Sequential API。例如,使用三个密集层的图片分类器可以在 Keras 中编写为:

model = tf.keras.Sequential([
    tf.keras.layers.Flatten(input_shape=[28, 28, 1]),
    tf.keras.layers.Dense(200, activation="relu"),
    tf.keras.layers.Dense(60, activation="relu"),
    tf.keras.layers.Dense(10, activation='softmax') # classifying into 10 classes
])

# this configures the training of the model. Keras calls it "compiling" the model.
model.compile(
  optimizer='adam',
  loss= 'categorical_crossentropy',
  metrics=['accuracy']) # % of correct answers

# train the model
model.fit(dataset, ... )

688858c21e3beff2.png

单个密集层

MNIST 数据集中的手写数字是 28x28 像素的灰度图片。对这些图片进行分类的最简单方法是将 28x28=784 像素用作单层神经网络的输入。

Screen Shot 2016-07-26 at 12.32.24.png

神经网络中的每个“神经元”都会对其所有输入进行加权求和,然后加上一个称为“偏差”的常量,最后通过某个非线性“激活函数”馈送结果。“权重”“偏差”是将在训练过程中确定的参数。它们最初会使用随机值进行初始化。

上图表示一个单层神经网络,其中包含 10 个输出神经元,因为我们要将数字分为 10 个类别(0 到 9)。

使用矩阵乘法

下面展示了如何通过矩阵乘法表示处理一组图片的神经网络层:

matmul.gif

使用权重矩阵 W 中的第一列权重,计算第一个图像的所有像素的加权和。此总和对应于第一个神经元。使用第二列权重,我们对第二个神经元执行相同的操作,依此类推,直到第 10 个神经元。然后,我们可以对剩余的 99 张图片重复执行该操作。如果我们用 X 表示包含 100 张图片的矩阵,那么对 100 张图片计算出的 10 个神经元的全部加权和就是 X.W,即矩阵乘法。

每个神经元现在都必须添加其偏差(一个常数)。由于我们有 10 个神经元,因此有 10 个偏置常数。我们将这个包含 10 个值的向量称为 b。必须将其添加到之前计算的矩阵的每一行中。借助一种称为“广播”的技巧,我们可以使用简单的加号来编写此代码。

最后,我们应用激活函数(例如“softmax”,如下所述),并获得描述应用于 100 张图片的单层神经网络的公式:

Screen Shot 2016-07-26 at 16.02.36.png

在 Keras 中

借助 Keras 等高级神经网络库,我们无需实现此公式。不过,请务必了解神经网络层只是一堆乘法和加法运算。在 Keras 中,密集层将编写为:

tf.keras.layers.Dense(10, activation='softmax')

深入了解

链接神经网络层非常简单。第一层计算像素的加权和。后续层会计算前几层输出的加权和。

fba0638cc213a29.png

除了神经元数量之外,唯一的区别在于激活函数的选择。

激活函数:relu、softmax 和 sigmoid

通常,您会为除最后一层之外的所有层使用“relu”激活函数。分类器中的最后一层会使用“softmax”激活函数。

644f4213a4ee70e5.png

同样,“神经元”会计算所有输入的加权和,添加一个称为“偏差”的值,并通过激活函数馈送结果。

最常用的激活函数是“RELU”(修正线性单元)。如上图所示,这是一个非常简单的函数。

神经网络中的传统激活函数是 “sigmoid”,但“relu”在几乎所有地方都表现出更好的收敛特性,因此现在更受青睐。

41fc82288c4aff5d.png

用于分类的 Softmax 激活函数

我们神经网络的最后一层有 10 个神经元,因为我们希望将手写数字分为 10 个类别(0 到 9)。它应该输出 10 个介于 0 到 1 之间的数字,分别表示相应数字为 0、1、2 等的概率。为此,我们将在最后一层使用一个名为 “softmax”的激活函数。

对向量应用 softmax 函数的方法是,先计算每个元素的指数,然后对向量进行归一化处理,通常是将其除以“L1”范数(即绝对值之和),这样一来,归一化后的值总和为 1,可以解释为概率。

激活前的最后一层的输出有时称为 “logits”。如果此向量为 L = [L0, L1, L2, L3, L4, L5, L6, L7, L8, L9],则:

ef0d98c0952c262d.png d51252f75894479e.gif

交叉熵损失

现在,我们的神经网络可以根据输入图片生成预测结果,我们需要衡量这些结果的准确性,即网络给出的结果与正确答案(通常称为“标签”)之间的距离。请注意,我们已为数据集中的所有图片添加了正确的标签。

任何距离都可以,但对于分类问题,所谓的“交叉熵距离”是最有效的。我们将此函数称为误差或“损失”函数:

6dbba1bce3cadc36.png

梯度下降法

“训练”神经网络实际上是指使用训练图片和标签来调整权重和偏差,以最大限度地减少交叉熵损失函数。具体运作方式如下。

交叉熵是权重、偏差、训练图像的像素及其已知类别的函数。

如果我们计算相对于所有权重和所有偏差的交叉熵偏导数,就会得到一个“梯度”,该梯度是针对给定的图片、标签以及当前权重和偏差值计算得出的。请注意,我们可能有数百万个权重和偏差,因此计算梯度听起来像是一项非常繁重的工作。幸运的是,TensorFlow 可以帮我们完成这项工作。梯度的数学属性是它指向“上方”。由于我们希望前往交叉熵较低的位置,因此我们朝相反的方向前进。我们通过梯度的一小部分来更新权重和偏差。然后,我们在训练循环中,使用下一批训练图片和标签反复执行相同的操作。希望这会收敛到交叉熵最小的位置,尽管无法保证此最小值是唯一的。

gradient descent2.png

小批量和动量

您可以仅根据一张示例图片计算梯度并立即更新权重和偏差,但如果根据一批(例如 128 张)图片计算梯度,则得到的梯度能更好地表示不同示例图片施加的限制,因此更有可能更快地收敛到解决方案。小批次的大小是一个可调整的参数。

这种有时称为“随机梯度下降法”的技术还有另一个更实用的好处:处理批次也意味着处理更大的矩阵,而这些矩阵通常更易于在 GPU 和 TPU 上进行优化。

不过,收敛可能仍然有点混乱,如果梯度向量全为零,甚至可能会停止。这是否意味着我们找到了最小值?不一定。梯度分量在最小值或最大值处可以为零。如果梯度向量有数百万个元素,并且这些元素全部为零,那么每个零都对应一个最小值,而没有一个零对应最大值的概率非常小。在多维空间中,鞍点非常常见,我们不希望停留在鞍点。

cc544924671fa208.png

图示:鞍点。梯度为 0,但它在所有方向上都不是最小值。(图片提供方信息 维基媒体:Nicoguaro - 自制作品,CC BY 3.0

解决方案是为优化算法增加一些动量,使其能够顺利通过鞍点而不停止。

术语库

批次小批次:始终基于批次的训练数据和标签进行训练。这样做有助于算法收敛。“批次”维度通常是数据张量的第一个维度。例如,形状为 [100, 192, 192, 3] 的张量包含 100 张 192x192 像素的图片,每张图片包含三个值(RGB)。

交叉熵损失:一种常用于分类器的特殊损失函数。

密集层:一种神经元层,其中每个神经元都与上一层中的所有神经元相连。

特征:神经网络的输入有时称为“特征”。确定将数据集的哪些部分(或部分组合)馈送到神经网络以获得良好预测结果的技巧称为“特征工程”。

标签:监督式分类问题中“类别”或正确答案的另一种名称

学习速率:在训练循环的每次迭代中,权重和偏差更新所依据的梯度分数。

logits:在应用激活函数之前,神经元层的输出称为“logits”。该术语源自“logistic 函数”(也称为“S 型函数”),后者曾经是最热门的激活函数。“Neuron outputs before logistic function”(逻辑函数之前的神经元输出)缩短为“logits”。

损失:将神经网络输出与正确答案进行比较的误差函数

神经元:计算输入的加权和,添加偏差,并通过激活函数馈送结果。

独热编码:5 个类别中的第 3 个类别编码为包含 5 个元素的向量,其中第 3 个元素为 1,其余元素均为 0。

relu:修正线性单元。一种常用的神经元激活函数。

sigmoid:另一种曾经很受欢迎的激活函数,在特殊情况下仍然有用。

softmax:一种特殊的激活函数,可作用于向量,增大最大分量与所有其他分量之间的差值,还会将向量归一化为总和为 1,以便将其解读为概率向量。用作分类器的最后一步。

张量:“张量”类似于矩阵,但具有任意数量的维度。一维张量是向量。二维张量是矩阵。然后,您可以拥有 3 维、4 维、5 维或更多维度的张量。

5. 我们来直接查看代码

返回到学习笔记本,这次我们来阅读代码。

c3df49e90e5a654f.png keras_01_mnist.ipynb

我们来浏览一下此笔记本中的所有单元格。

“参数”单元格

在此处定义批次大小、训练周期数和数据文件的位置。数据文件托管在 Google Cloud Storage (GCS) 存储分区中,因此其地址以 gs:// 开头

“导入”单元格

此处导入了所有必需的 Python 库,包括 TensorFlow 和用于可视化的 matplotlib。

单元格“可视化实用程序 [运行我]****”

此单元格包含不重要的可视化代码。该部分默认处于收起状态,但您可以双击它,在有时间时打开并查看代码。

单元格“tf.data.Dataset:解析文件并准备训练和验证数据集

此单元格使用 tf.data.Dataset API 从数据文件加载 MNIST 数据集。无需在此单元格上花费过多时间。如果您对 tf.data.Dataset API 感兴趣,可以参阅以下教程:TPU 速度数据流水线。目前,基本知识包括:

MNIST 数据集中的图片和标签(正确答案)存储在 4 个文件中的固定长度记录中。可以使用专用固定记录函数加载文件:

imagedataset = tf.data.FixedLengthRecordDataset(image_filename, 28*28, header_bytes=16)

现在,我们有了一个图片字节数据集。需要将它们解码为图片。为此,我们定义了一个函数。由于图片未经过压缩,因此该函数无需解码任何内容(decode_raw 基本上不执行任何操作)。然后,将图片转换为介于 0 到 1 之间的浮点值。我们可以在此处将其重塑为 2D 图片,但实际上,我们将其保留为大小为 28*28 的扁平像素数组,因为这是初始密集层所期望的。

def read_image(tf_bytestring):
    image = tf.io.decode_raw(tf_bytestring, tf.uint8)
    image = tf.cast(image, tf.float32)/256.0
    image = tf.reshape(image, [28*28])
    return image

我们使用 .map 将此函数应用于数据集,并获得一个图片数据集:

imagedataset = imagedataset.map(read_image, num_parallel_calls=16)

我们对标签进行相同的读取和解码,并一起处理 .zip 图片和标签:

dataset = tf.data.Dataset.zip((imagedataset, labelsdataset))

现在,我们有了一个包含(图片、标签)对的数据集。这是我们模型所期望的。我们尚未准备好在训练函数中使用它:

dataset = dataset.cache()
dataset = dataset.shuffle(5000, reshuffle_each_iteration=True)
dataset = dataset.repeat()
dataset = dataset.batch(batch_size)
dataset = dataset.prefetch(tf.data.experimental.AUTOTUNE)

tf.data.Dataset API 具有用于准备数据集的所有必需的实用函数:

.cache 将数据集缓存在 RAM 中。这是一个很小的数据集,因此可以正常运行。.shuffle 使用 5000 个元素的缓冲区对其进行混排。请务必对训练数据进行充分的随机化处理。.repeat 会循环播放数据集。我们将多次(多个周期)对其进行训练。.batch 将多个图片和标签拉取到一起,形成一个小批次。最后,.prefetch 可以使用 CPU 来准备下一批数据,同时在 GPU 上训练当前批次的数据。

验证数据集的准备方式类似。现在,我们可以定义模型并使用此数据集对其进行训练了。

单元格“Keras 模型”

我们的所有模型都将是层级的直线序列,因此我们可以使用 tf.keras.Sequential 样式来创建它们。最初,这里是一个密集层。它有 10 个神经元,因为我们要将手写数字分为 10 类。它使用“softmax”激活函数,因为它是分类器中的最后一层。

Keras 模型还需要知道其输入的形状。您可以使用 tf.keras.layers.Input 来定义它。在此示例中,输入向量是长度为 28*28 的像素值扁平向量。

model = tf.keras.Sequential(
  [
    tf.keras.layers.Input(shape=(28*28,)),
    tf.keras.layers.Dense(10, activation='softmax')
  ])

model.compile(optimizer='sgd',
              loss='categorical_crossentropy',
              metrics=['accuracy'])

# print model layers
model.summary()

# utility callback that displays training curves
plot_training = PlotTraining(sample_rate=10, zoom=1)

在 Keras 中,可以使用 model.compile 函数配置模型。这里我们使用基本优化器 'sgd'(随机梯度下降法)。分类模型需要交叉熵损失函数,在 Keras 中称为 'categorical_crossentropy'。最后,我们要求模型计算 'accuracy' 指标,即正确分类的图片所占的百分比。

Keras 提供了一个非常实用的 model.summary() 实用程序,用于输出您创建的模型的详细信息。您好心的指导员添加了 PlotTraining 实用程序(在“可视化实用程序”单元格中定义),该程序将在训练期间显示各种训练曲线。

“训练和验证模型”代码单元格

通过调用 model.fit 并传入训练数据集和验证数据集,即可在此处进行训练。默认情况下,Keras 会在每个周期结束时运行一轮验证。

model.fit(training_dataset, steps_per_epoch=steps_per_epoch, epochs=EPOCHS,
          validation_data=validation_dataset, validation_steps=1,
          callbacks=[plot_training])

在 Keras 中,可以使用回调在训练期间添加自定义行为。这就是本次研讨会中动态更新训练图的实现方式。

“直观呈现预测结果”单元格

模型训练完毕后,我们可以通过调用 model.predict() 从中获取预测结果:

probabilities = model.predict(font_digits, steps=1)
predicted_labels = np.argmax(probabilities, axis=1)

我们在此处准备了一组使用本地字体渲染的打印数字,以供测试。请注意,神经网络会通过最终的“softmax”返回一个包含 10 个概率的向量。为了获得标签,我们必须找出哪个概率最高。numpy 库中的 np.argmax 可实现此目的。

如需了解为何需要 axis=1 参数,请记住我们已处理一批 128 张图片,因此模型会返回 128 个概率向量。输出张量的形状为 [128, 10]。我们正在计算每张图片返回的 10 个概率的 argmax,因此为 axis=1(第一个轴为 0)。

这个简单的模型已经可以识别 90% 的数字。这个分数不算差,但您现在可以大幅提高它。

396c54ef66fad27f.png

6. 添加图层

godeep.png

为了提高识别准确率,我们将向神经网络添加更多层。

Screen Shot 2016-07-27 at 15.36.55.png

我们将 softmax 保留为最后一层的激活函数,因为这是最适合分类的函数。不过,在中间层,我们将使用最经典的激活函数:sigmoid:

41fc82288c4aff5d.png

例如,您的模型可能如下所示(请勿忘记逗号,tf.keras.Sequential 接受以逗号分隔的层列表):

model = tf.keras.Sequential(
  [
      tf.keras.layers.Input(shape=(28*28,)),
      tf.keras.layers.Dense(200, activation='sigmoid'),
      tf.keras.layers.Dense(60, activation='sigmoid'),
      tf.keras.layers.Dense(10, activation='softmax')
  ])

查看模型的“摘要”。现在,它的形参数量至少增加了 10 倍。效果应该好 10 倍!但由于某些原因,它不是 ...

5236f91ba6e07d85.png

亏损似乎也大幅增加。出了点问题。

7. 深度网络的特殊注意事项

您刚刚体验了 20 世纪 80 年代和 90 年代人们设计神经网络的方式。难怪他们放弃了这个想法,迎来了所谓的“AI 寒冬”。事实上,随着层数的增加,神经网络越来越难以收敛。

事实证明,只要采用一些数学上的“脏招”来使它们收敛,具有许多层(如今为 20 层、50 层,甚至 100 层)的深度神经网络就可以发挥出非常好的效果。这些简单技巧的发现是 2010 年代深度学习复兴的原因之一。

RELU 激活

relu.png

在深度网络中,sigmoid 激活函数实际上存在相当大的问题。它会将所有值压缩到 0 到 1 之间,如果重复执行此操作,神经元输出及其梯度可能会完全消失。之所以提及它,是因为历史原因,但现代网络使用 ReLU(修正线性单元),其外观如下所示:

1abce89f7143a69c.png

另一方面,relu 的导数为 1(至少在其右侧是这样)。使用 RELU 激活函数时,即使某些神经元传来的梯度可能为零,也总会有其他神经元提供清晰的非零梯度,训练可以以良好的速度继续进行。

更好的优化器

在非常高维的空间中(例如这里,我们有大约 1 万个权重和偏差),“鞍点”很常见。这些点不是局部最小值,但梯度为零,梯度下降法优化器会卡在这些点上。TensorFlow 提供了各种优化器,包括一些具有一定惯性的优化器,可安全地通过鞍点。

随机初始化

在训练之前初始化权重偏差是一门艺术,也是一个研究领域,有大量论文发表于此主题。您可以点击此处查看 Keras 中所有可用的初始值设定器。幸运的是,Keras 默认情况下会执行正确的操作,并使用 'glorot_uniform' 初始化程序,该初始化程序在几乎所有情况下都是最佳选择。

由于 Keras 已经采取了正确的做法,因此您无需执行任何操作。

NaN ???

交叉熵公式涉及对数,而 log(0) 是非数字 (NaN,如果您愿意,也可以称之为数值崩溃)。交叉熵的输入可以是 0 吗?输入来自 softmax,而 softmax 本质上是指数函数,指数函数永远不会为零。所以我们是安全的!

真的吗?在美好的数学世界中,我们是安全的,但在计算机世界中,以 float32 格式表示的 exp(-150) 几乎为零,因此交叉熵会崩溃。

幸运的是,您在此处也无需执行任何操作,因为 Keras 会处理此问题,并以特别谨慎的方式计算 softmax,然后计算交叉熵,以确保数值稳定性并避免出现可怕的 NaN。

成功?

e1521c9dd936d9bc.png

现在,准确率应该可以达到 97%。本次研讨会的目标是显著高于 99%,因此让我们继续。

如果您卡住了,请尝试以下解决方案:

c3df49e90e5a654f.png keras_02_mnist_dense.ipynb

8. 学习速率衰减

或许我们可以尝试更快地训练模型?Adam 优化器中的默认学习速率为 0.001。我们来尝试增加它。

加快速度似乎没有太大帮助,而且这些噪音是什么?

d4fd66346d7c480e.png

训练曲线非常嘈杂,并且两个验证曲线都在上下跳动。这意味着我们走得太快了。我们可以恢复到之前的速度,但还有更好的方法。

放慢速度。

一个不错的解决方案是快速开始,然后以指数方式衰减学习速率。在 Keras 中,您可以使用 tf.keras.callbacks.LearningRateScheduler 回调来实现此目的。

可供复制粘贴的实用代码:

# lr decay function
def lr_decay(epoch):
  return 0.01 * math.pow(0.6, epoch)

# lr schedule callback
lr_decay_callback = tf.keras.callbacks.LearningRateScheduler(lr_decay, verbose=True)

# important to see what you are doing
plot_learning_rate(lr_decay, EPOCHS)

请勿忘记使用您创建的 lr_decay_callback。将其添加到 model.fit 中的回调列表中:

model.fit(...,  callbacks=[plot_training, lr_decay_callback])

这一小小的变化带来了惊人的效果。您会看到,大部分噪声都消失了,测试准确率现在持续高于 98%。

8c1ae90976c4a0c1.png

9. Dropout、过拟合

模型现在似乎正在顺利收敛。让我们尝试更深入地了解一下。

是否有帮助?

e36c09a3088104c6.png

并非如此,准确率仍停留在 98%,请看验证损失率。正在上涨!学习算法仅处理训练数据,并相应地优化训练损失。它永远不会看到验证数据,因此过一段时间后,它的工作不再对验证损失产生影响,验证损失停止下降,有时甚至会反弹,这并不令人意外。

这不会立即影响模型的实际识别能力,但会阻止您运行多次迭代,并且通常表明训练不再产生积极效果。

dropout.png

这种不一致通常称为“过拟合”,当您发现这种情况时,可以尝试应用一种称为“dropout”的正则化技术。在每次训练迭代中,dropout 技术都会随机舍弃神经元。

效果如何?

43fd33801264743f.png

噪声会重新出现(考虑到 dropout 的工作方式,这并不令人意外)。验证损失似乎不再缓慢上升,但总体上高于不使用 Dropout 的情况。验证准确率略有下降。这个结果相当令人失望。

看来 dropout 并不是正确的解决方案,或者“过拟合”是一个更复杂的概念,其某些原因并不适合使用“dropout”修复?

什么是“过拟合”?当神经网络“学习”得不好时,就会发生过拟合,即学习方式虽然适用于训练示例,但在真实世界的数据上效果不佳。有一些正则化技术(例如 dropout)可以强制模型以更好的方式学习,但过拟合也有更深层次的原因。

overfitting.png

当神经网络对于手头的问题具有过多的自由度时,就会发生基本的过拟合。假设我们有非常多的神经元,以至于网络可以将所有训练图片存储在其中,然后通过模式匹配来识别它们。在真实世界的数据上会完全失败。神经网络必须受到一定的限制,这样才能被迫泛化在训练期间学到的知识。

如果您只有极少的训练数据,即使是小型网络也能将其背得滚瓜烂熟,从而导致“过拟合”。一般来说,您始终需要大量数据来训练神经网络。

最后,如果您已按部就班地完成所有操作,尝试了不同大小的网络以确保其自由度受到限制,应用了 dropout,并使用大量数据进行了训练,但仍停留在似乎无法提高的性能水平,这意味着,您的神经网络目前无法从数据中提取更多信息,就像我们这里的情况一样。

还记得我们如何将图片展平为单个向量吗?这真是个糟糕的主意。手写数字由形状组成,我们在展平像素时舍弃了形状信息。不过,有一种神经网络可以利用形状信息,那就是卷积网络。让我们来试试这些功能。

如果您卡住了,请尝试以下解决方案:

c3df49e90e5a654f.png keras_03_mnist_dense_lrdecay_dropout.ipynb

10. [信息] 卷积网络

简而言之

如果您已经了解下一段中所有粗体字词,则可以继续进行下一个练习。如果您刚刚开始使用卷积神经网络,请继续阅读。

convolutional.gif

示例:使用两个连续的滤波器(每个滤波器包含 4x4x3=48 个可学习的权重)过滤图片。

下面展示了如何在 Keras 中实现一个简单的卷积神经网络:

model = tf.keras.Sequential([
    tf.keras.layers.Reshape(input_shape=(28*28,), target_shape=(28, 28, 1)),
    tf.keras.layers.Conv2D(kernel_size=3, filters=12, activation='relu'),
    tf.keras.layers.Conv2D(kernel_size=6, filters=24, strides=2, activation='relu'),
    tf.keras.layers.Conv2D(kernel_size=6, filters=32, strides=2, activation='relu'),
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(10, activation='softmax')
])

688858c21e3beff2.png

在卷积网络的某一层中,一个“神经元”仅对上方一小块图像区域中的像素进行加权求和。它会添加偏差,并通过激活函数馈送总和,就像常规密集层中的神经元一样。然后,使用相同的权重对整个图片重复此操作。请注意,在密集层中,每个神经元都有自己的权重。在这里,单个权重“补丁”在图像上沿两个方向滑动(即“卷积”)。输出值的数量与图片中的像素数量相同(不过边缘需要一些填充)。这是一项过滤操作。在上图中,它使用了 4x4x3=48 个权重的过滤条件。

不过,48 个权重是不够的。为了增加自由度,我们使用一组新的权重重复相同的操作。这会生成一组新的过滤输出。我们不妨将其称为输出“通道”,这与输入图像中的 R、G、B 通道类似。

Screen Shot 2016-07-29 at 16.02.37.png

通过添加新维度,可以将两组(或更多组)权重汇总为一个张量。这样,我们就得到了卷积层权重张量的通用形状。由于输入和输出通道的数量是形参,我们可以开始堆叠和链接卷积层。

d1b557707bcd1cb9.png

图示:卷积神经网络将数据“立方体”转换为其他数据“立方体”。

步幅卷积、最大池化

通过以 2 或 3 的步幅执行卷积,我们还可以缩小所得数据立方体的水平维度。您可以通过以下两种常见方式来完成此操作:

  • 步幅卷积:与上述滑动过滤器的不同之处在于,步幅大于 1
  • 最大池化:应用 MAX 操作的滑动窗口(通常在 2x2 块上,每 2 个像素重复一次)

2b2d4263bb8470b.gif

图示:将计算窗口滑动 3 个像素会减少输出值。步幅卷积或最大池化(以 2 为步长滑动 2x2 窗口的最大值)是一种在水平维度上缩小数据立方体的方法。

最后一层

在最后一个卷积层之后,数据以“立方体”的形式呈现。有两种方式可将其馈送到最终的密集层。

第一种方法是将数据立方体展平为向量,然后将其馈送到 softmax 层。有时,您甚至可以在 softmax 层之前添加一个密集层。就权重数量而言,这种方法往往成本较高。卷积网络末端的密集层可能包含整个神经网络一半以上的权重。

除了使用开销较大的密集层之外,我们还可以将传入的数据“立方体”拆分为与类别数量相同的多个部分,计算这些部分的平均值,然后通过 softmax 激活函数馈送这些平均值。这种构建分类头的方式不需要任何权重。在 Keras 中,有一个专门用于此目的的层:tf.keras.layers.GlobalAveragePooling2D()

a44aa392c7b0e32a.png

跳到下一部分,为当前问题构建卷积网络。

11. 卷积网络

让我们构建一个用于手写数字识别的卷积网络。我们将使用顶部的三个卷积层、底部的传统 softmax 读取层,并通过一个全连接层将它们连接起来:

e1a214a170957da1.png

请注意,第二个和第三个卷积层的步幅为 2,这说明了为什么它们会将输出值的数量从 28x28 减少到 14x14,然后再减少到 7x7。

我们来编写 Keras 代码。

在第一个卷积层之前需要特别注意。实际上,它需要一个 3D 数据“立方体”,但我们的数据集目前已设置为密集层,并且图像的所有像素都已展平为向量。我们需要将它们重新调整为 28x28x1 图片(灰度图片为 1 个通道):

tf.keras.layers.Reshape(input_shape=(28*28,), target_shape=(28, 28, 1))

您可以使用此行代码来代替您之前使用的 tf.keras.layers.Input 层。

在 Keras 中,采用“relu”激活函数的卷积层的语法如下:

140f80336b0e653b.png

tf.keras.layers.Conv2D(kernel_size=3, filters=12, padding='same', activation='relu')

对于步幅卷积,您可以编写以下代码:

tf.keras.layers.Conv2D(kernel_size=6, filters=24, padding='same', activation='relu', strides=2)

将数据立方体展平为向量,以便密集层可以使用该向量:

tf.keras.layers.Flatten()

对于密集层,语法没有变化:

tf.keras.layers.Dense(200, activation='relu')

您的模型是否突破了 99% 的准确率?差不多了…但请看验证损失曲线。这是否让您想起了什么?

ecc5972814885226.png

还要查看预测结果。您应该会看到,现在 10,000 个测试数字中的大多数都能正确识别。误检的行数仅剩约 4½ 行(约 110 个数字,总共 10,000 个)

37e4cbd3f390c89e.png

如果您卡住了,请尝试以下解决方案:

c3df49e90e5a654f.png keras_04_mnist_convolutional.ipynb

12. 再次漏失

之前的训练显示出明显的过拟合迹象(并且准确率仍低于 99%)。我们是否应再次尝试 dropout?

这次怎么样?

63e0cc982cee2030.png

这次的 dropout 似乎奏效了。验证损失不再增加,最终准确率应远高于 99%。恭喜!

我们第一次尝试应用 dropout 时,以为遇到了过拟合问题,但实际上问题出在神经网络的架构上。如果没有卷积层,我们就无法继续,而 Dropout 对此也无能为力。

这次,过拟合似乎是问题的原因,而 dropout 确实有帮助。请注意,有很多因素可能会导致训练损失曲线和验证损失曲线之间出现差异,并导致验证损失逐渐增加。过拟合(自由度过高,被网络错误地使用)只是其中一种。如果数据集过小或神经网络的架构不够完善,您可能会在损失曲线上看到类似的行为,但 Dropout 无法提供帮助。

13. 批次归一化

oggEbikl2I6_sOo7FlaX2KLdNeaYhJnVSS8GyG8FHXid75PVJX73CRiOynwpMZpLZq6_xAy69wgyez5T-ZlpuC2XSlcmjk7oVcOzefKKTFhTEoLO3kljz2RDyKcaFtHvtTey-I4VpQ

最后,我们尝试添加批次归一化。

以上是理论,在实践中,只需记住以下几条规则:

我们现在先按部就班地操作,在每个神经网络层(最后一层除外)上添加一个批次归一化层。请勿将其添加到最后一个“softmax”层。这在上述情况下并不适用。

# Modify each layer: remove the activation from the layer itself.
# Set use_bias=False since batch norm will play the role of biases.
tf.keras.layers.Conv2D(..., use_bias=False),
# Batch norm goes between the layer and its activation.
# The scale factor can be turned off for Relu activation.
tf.keras.layers.BatchNormalization(scale=False, center=True),
# Finish with the activation.
tf.keras.layers.Activation('relu'),

现在的准确度如何?

ea48193334c565a1.png

稍作调整(BATCH_SIZE=64,学习速率衰减参数 0.666,密集层的 dropout 率 0.3),再加上一点运气,您就可以达到 99.5% 的准确率。学习速率和 dropout 调整是按照使用批次归一化的“最佳实践”进行的:

  • 批归一化有助于神经网络收敛,通常可让您更快地进行训练。
  • 批次归一化是一种正则化方法。您通常可以减少所用的 dropout 量,甚至完全不使用 dropout。

解决方案笔记本的训练运行成功率为 99.5%:

c3df49e90e5a654f.png keras_05_mnist_batch_norm.ipynb

14. 在云端使用强大的硬件进行训练:AI Platform

d7d0282e687bdad8.png

您可以在 GitHub 上的 mlengine 文件夹中找到可用于云端的代码版本,以及在 Google Cloud AI Platform 上运行该代码的说明。在运行此部分之前,您必须先创建 Google Cloud 账号并启用结算功能。完成实验所需的资源应不超过几美元(假设在一个 GPU 上训练 1 小时)。准备账号:

  1. 创建 Google Cloud Platform 项目 ( http://cloud.google.com/console)。
  2. 启用结算功能。
  3. 安装 GCP 命令行工具(此处为 GCP SDK)。
  4. 创建一个 Google Cloud Storage 存储分区(放在 us-central1 区域中)。它将用于暂存训练代码并存储经过训练的模型。
  5. 启用必要的 API 并申请必要的配额(运行一次训练命令,您应该会收到错误消息,其中会告知您需要启用哪些 API)。

15. 恭喜!

您已构建了首个神经网络,并将其训练到 99% 的准确率。在此过程中学到的技巧并非仅适用于 MNIST 数据集,实际上,它们在处理神经网络时得到了广泛应用。作为临别礼物,我们为您准备了实验的“速记卡片”(卡通版)。您可以使用它来记住所学内容:

cliffs notes tensorflow lab.png

后续步骤

  • 在学习了全连接网络和卷积网络之后,您应该了解一下循环神经网络
  • 如需在云端分布式基础架构上运行训练或推理,Google Cloud 提供了 AI Platform
  • 最后,我们非常欢迎您提供反馈。如果您发现此实验有任何问题,或者认为此实验应进行改进,请告诉我们。我们通过 GitHub 问题处理反馈 [反馈链接]。

HR.png

Martin Görner ID small.jpg作者:Martin GörnerTwitter:@martin_gorner

本实验中的所有卡通图片版权归以下方所有:alexpokusay / 123RF 图库照片