1. 概览
本教程针对 TensorFlow 2.2 进行了更新!
在此 Codelab 中,您将学习如何构建和训练识别手写数字的神经网络。在此过程中,随着您增强神经网络以实现 99% 的准确率,您还会发现深度学习专业人士用于高效训练其模型的各种工具。
此 Codelab 使用 MNIST 数据集,该数据集包含 60,000 个已加标签的数字,为几代博士的辛勤付出提供了近 20 年的时间。您将使用不超过 100 行的 Python / TensorFlow 代码来解决问题。
学习内容
- 神经网络是什么以及如何训练它
- 如何使用 tf.keras 构建基本的 1 层神经网络
- 如何添加更多图层
- 如何设置学习速率时间表
- 如何构建卷积神经网络
- 如何使用正则化技术:丢弃、批量归一化
- 什么是过拟合
所需条件
只要一个浏览器,此研讨会完全可以通过 Google Colaboratory 开展。
反馈
如果您发现此实验中存在错误,或者您认为需要改进,请告诉我们。我们通过 GitHub 问题 [反馈链接] 处理反馈。
2. Google Colaboratory 快速入门
本实验使用 Google Colaboratory,无需您进行任何设置。您可以从 Chromebook 运行该应用。请打开下面的文件,然后执行其中的单元,以熟悉 Colab 笔记本。
其他说明如下:
选择 GPU 后端
在 Colab 菜单中,选择运行时 >更改运行时类型,然后选择 GPU。首次执行时会自动连接到运行时,您也可以使用“Connect”按钮。
笔记本执行
通过点击单元格并使用 Shift-ENTER 一次执行一个单元格。您还可以使用 Runtime >运行全部
目录
所有笔记本都有一个目录。您可以使用左侧的黑色箭头将其打开。
隐藏单元格
部分单元格将仅显示标题。这是 Colab 特有的笔记本功能。您可以双击它们来查看其中的代码,但通常不是很有趣。通常是支持函数或可视化函数。您仍然需要运行这些单元才能定义其中的函数。
3. 训练神经网络
我们首先来看神经网络训练。请打开下面的笔记本,并运行所有单元。先不要关注代码,我们稍后会开始解释。
在执行笔记本时,请专注于可视化图表。有关说明,请参阅下文。
训练数据
我们有一个带标签的手写数字数据集,这样我们就知道每张图片代表什么,即 0 到 9 之间的一个数字。在笔记本中,您将看到一段摘录:
我们要构建的神经网络将手写数字分成 10 个类别(0、..、9)。它基于那些需要具有正确值的内部参数才能正常运行。这个“正确的值”是通过训练过程学习的,这需要“已加标签的数据集”并附上图片和相关正确答案。
我们如何知道经过训练的神经网络性能是否良好?使用训练数据集测试网络是一种作弊行为。它在训练期间已经多次见过该数据集,而且绝对性能非常出色。我们需要另一个训练期间从未见过的有标签数据集来评估“现实世界”网络性能。它称为“验证数据集”
训练
随着训练的推进,每次处理一批训练数据,内部模型参数会不断更新,模型在识别手写数字方面的能力也会越来越好。您可以在训练图上查看它:
右侧的“准确率”只是正确识别的数字所占的百分比。它会随着训练的进展而增加,这是很好的。
在左侧,我们可以看到 "loss"。为了推动训练,我们将定义一个“损失”函数,它表示系统识别数字的难易程度,并尝试将其最小化。在这里,您可以看到,随着训练的进行,训练数据和验证数据的损失逐渐降低,这很好。这意味着神经网络正在学习。
X 轴表示“周期”的数量对整个数据集进行迭代或迭代。
预测
模型经过训练后,我们可以使用它来识别手写数字。下一个可视化结果展示了它在分别通过本地字体(第一行)渲染的几位数以及验证数据集的 10,000 位数渲染效果时的表现。预测的类别会显示在每个数字下方,如果输入的数字不正确,则会以红色显示。
如您所见,这个初始模型不是很好,但仍然能正确识别一些数字。它的最终验证准确率约为 90%,这对于我们最初的简单模型来说还算不错,但这仍然意味着它遗漏了 10, 000 位中的 1,000 位验证数字。可显示的内容远远多了,因此看起来所有答案都不正确(红色)。
张量
数据存储在矩阵中。将 28x28 像素灰度图片放入 28x28 二维矩阵中。但对于彩色图片,我们需要更多尺寸。每个像素有 3 个颜色值(红、绿、蓝),因此需要尺寸为 [28、28、3] 的三维表格。要存储一批 128 色图像,需要一个尺寸为 [128, 28, 28, 3] 的四维表格。
这些多维表称为“张量”,其维度列表即为其"形状"。
4. [INFO]:神经网络 101
简述
如果你已经知道下一段落中以粗体显示的所有术语,可以接着进行下一个练习。如果您刚开始接触深度学习,欢迎继续阅读。
对于构建为一系列层的模型,Keras 可提供 Sequential API。例如,使用 3 个密集层的图片分类器可以在 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, ... )
单个密集层
MNIST 数据集中的手写数字是 28x28 像素的灰度图像。要对其进行分类,最简单的方法是使用 28x28=784 像素作为单层神经网络的输入。
神经网络中的每个“神经元”都会对其所有输入值进行加权和,然后添加一个称为“偏差”的常数然后通过一些非线性“激活函数”来馈送结果。"weights" 和 "biases" 是将通过训练确定的参数。它们最初使用随机值初始化。
上图显示了一个包含 10 个输出神经元的 1 层神经网络,因为我们想要将数字分为 10 个类别(0 到 9)。
使用矩阵乘法
下方展示了处理图片集合的神经网络层如何用矩阵乘法表示:
我们使用权重矩阵 W 中的第一列权重,计算第一张图片的所有像素的加权和。此总和对应于第一个神经元。使用第二列权重,我们对第二个神经元执行相同的操作,依此类推,直到第 10 个神经元。然后,我们可以对其余 99 张图片重复这一操作。如果我们将 X 这个矩阵称为包含我们 100 张图像的矩阵,那么基于 100 张图像计算的 10 个神经元的所有加权和只是 X.W,即矩阵乘法。
每个神经元现在必须加上其偏差(常数)。由于我们有 10 个神经元,因此有 10 个偏差常量。我们将这个包含 10 个值的矢量称为 b。必须将其添加到之前计算的矩阵的每一行中。使用“广播”功能我们要用一个简单的加号
最后,我们应用激活函数,例如“softmax”(在下文中说明)并获取用于描述 1 层神经网络的公式,并将其应用于 100 张图像:
在 Keras 中
使用 Keras 等高级神经网络库时,我们无需实现此公式。但请务必注意,神经网络层只是一系列乘法和加法运算。在 Keras 中,密集层的编写方式如下:
tf.keras.layers.Dense(10, activation='softmax')
深入了解
链接神经网络层非常简单。第一层计算像素的加权和。后续层会计算前一层输出的加权和。
除了神经元数量之外,唯一的区别在于激活函数的选择。
激活函数:relu、softmax 和 sigmoid 函数
您通常可以使用“relu”激活函数。在分类器中,最后一层将使用“softmax”。
“神经元”计算所有输入的加权和,然后添加一个名为“偏差”的值并通过激活函数馈送结果。
最常用的激活函数称为修正线性单元 "RELU"。如上图所示,这是一个非常简单的函数。
神经网络中的传统激活函数为“sigmoid”,但使用“relu”被证明在几乎所有地方都有更好的收敛属性,现在是首选。
用于分类的 Softmax 激活
神经网络的最后一层有 10 个神经元,因为我们想要将手写数字分为 10 个类别 (0,..9)。它应输出 0 到 1 之间的 10 个数字,表示该数字为 0、1、2 等的概率。为此,在最后一层,我们将使用一个名为 "softmax" 的激活函数。
对向量应用 Softmax 的方法是,取每个元素的指数,然后归一化向量,通常用其除以其“L1”范数(即绝对值之和),以使归一化值加起来等于 1,可解释为概率。
激活前最后一层的输出有时称为 "logits"。如果此矢量为 L = [L0, L1, L2, L3, L4, L5, L6, L7, L8, L9],则:
交叉熵损失
现在,我们的神经网络可以根据输入图片生成预测,接下来我们需要衡量预测的好坏,即网络告诉我们的信息与正确答案之间的距离,通常称为“标签”。请记住,数据集中的所有图片都有正确的标签。
任何距离都有效,但对于分类问题而言,所谓的“交叉熵距离”是最有效的。我们将这种情况称为错误或“损失”。函数:
梯度下降法
“训练”神经网络实际上意味着使用训练图片和标签来调整权重和偏差,以便最大限度地降低交叉熵损失函数的值。其运作方式如下。
交叉熵是训练图片的权重、偏差、像素及其已知类别的函数。
如果我们计算交叉熵相对于所有权重和所有偏置的偏导数,就会得到一个“梯度”,该梯度针对给定图像、标签以及权重和偏差的现值进行了计算。请记住,我们可以有数百万个权重和偏差,因此计算梯度声就像是大量的工作。幸运的是,TensorFlow 可以帮我们搞定。渐变的数学属性是它指向“向上”。由于我们想去交叉熵较低的地方,因此会前往相反的方向。我们以梯度的分数更新权重和偏差。然后,我们在训练循环中使用下一批训练图片和标签重复执行相同的操作。我们希望这可以收敛到交叉熵极低的位置,但无法保证此最小值是唯一的。
小批量和动量
您可以仅对一张样本图片计算梯度,并立即更新权重和偏差,但对一批 128 张图片(例如 128 张图片)执行此操作将产生一个梯度,该梯度可以更好地表示不同样本图片施加的限制,因此可能会更快地收敛于解决方案。小批次的大小是一个可调整的参数。
这种技术有时称为“随机梯度下降”还有一个更实际的好处:使用批量处理还意味着使用较大的矩阵,这些矩阵通常更易于在 GPU 和 TPU 上进行优化。
不过,收敛可能仍然有点混乱,即使梯度矢量都为零,收敛也可能会停止。这是否意味着我们找到了一个最低值?不一定。梯度分量可以为零的最小值或最大值。对于包含数百万个元素的梯度矢量,如果这些元素全部为零,则每个零对应于一个最小值而没有一个对应于最大点的概率非常小。在具有很多维度的空间中,马鞍点很常见,我们不想就此止步。
插图:马鞍点。梯度为 0,但并非在所有方向上的最小值。(图片出处 维基媒体:Nicoguaro - 自创内容,CC BY 3.0)
解决方法是为优化算法增加一些动力,使其不停地驶过马鞍点。
术语库
batch 或 mini-batch:始终对批量训练数据和标签执行训练。这样做有助于算法收敛。“批次”维度通常是数据张量的第一个维度。例如,形状为 [100, 192, 192, 3] 的张量包含 100 张 192x192 像素的图片,每个像素有三个值 (RGB)。
交叉熵损失:分类器中常用的一种特殊损失函数。
密集层:一个神经元层,其中每个神经元都与前一层中的所有神经元连接。
特征:神经网络的输入有时称为“特征”。弄清楚将数据集的哪些部分(或部分组合)馈送到神经网络以获得良好预测结果的过程称为“特征工程”。
labels:“类”的另一种名称在监督式分类问题中,
学习速率:权重和偏差在训练循环的每次迭代中更新时的梯度百分比。
logits:在应用激活函数之前,一层神经元的输出称为“logits”。这个术语来自“逻辑函数”也称为“S 型函数”这曾是最常用的激活函数。"逻辑函数之前的中子输出"已简化为“logits”。
loss:将神经网络输出与正确答案进行比较的误差函数
神经元:计算其输入的加权和,添加偏差并通过激活函数馈送结果。
独热编码:第 3 类(共 5 类)编码为 5 个元素(第三个元素为 1 除外)的向量。
relu:修正的线性单元。一种热门的神经元激活函数。
S 型函数:另一种过去很流行的激活函数,在特殊情况下仍然有用。
softmax:一个作用于向量的特殊激活函数,用于增大最大分量与所有其他分量之间的差值,同时对该向量进行归一化,使其总和为 1,以便将其解释为概率向量。用作分类器中的最后一步。
tensor:“张量”类似于矩阵,但维度数量没有限制。一维张量是一个向量。二维张量就是一个矩阵。然后,您可以得到具有 3 个、4 个、5 个或更多维度的张量。
5. 下面我们看一下代码
返回研究笔记本,这一次,我们来阅读代码。
我们来浏览一下此笔记本中的所有单元。
单元格“Parameters”
这里定义了批次大小、训练周期数和数据文件的位置。数据文件托管在 Google Cloud Storage (GCS) 存储分区中,因此它们的地址以 gs://
开头
单元格“Imports”
这里导入了所有必要的 Python 库,包括 TensorFlow 和用于可视化的 matplotlib。
单元格“可视化实用程序 [RUN ME]****”
此单元格包含无趣的可视化代码。该窗格默认处于收起状态,但您可以将其打开,并在有时间时双击它查看代码。
单元格“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
将多个图片和标签合并到一个小批次中。最后,在 GPU 上训练当前批次时,.prefetch
可以使用 CPU 准备下一个批次。
验证数据集以类似的方式准备。现在,我们可以定义模型并使用此数据集进行训练了。
单元“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)
使用 model.compile
函数在 Keras 中配置模型。在这里,我们使用基本优化器 'sgd'
(随机梯度下降)。分类模型需要交叉熵损失函数,在 Keras 中称为 'categorical_crossentropy'
。最后,我们要求模型计算 'accuracy'
指标,即正确分类的图片所占的百分比。
Keras 提供了非常实用的 model.summary()
实用程序,可以输出你所创建的模型的详细信息。您的 Kind 教师已添加 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 中,可以使用回调在训练期间添加自定义行为。这就是本次研讨会中采用动态更新训练图的方式。
单元格“Visualize Prediction”(可视化预测结果)
模型训练完成后,我们可以通过调用 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% 的数字了。还不错,但是您现在将显著改善这一点。
6. 添加图层
为了提高识别准确性,我们将向神经网络添加更多层。
我们将 softmax 保留为最后一层的激活函数,因为 softmax 函数最适用于分类。不过,在中间层,我们会使用最经典的激活函数:S 型函数:
例如,您的模型可能如下所示(不要忘记英文逗号,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 倍!但由于某些原因,它不是 ...
损失似乎也激增。出错了,
7. 针对深度网络的特别关注
你刚刚体验到神经网络,80、90 年代人们曾经设计过神经网络。难怪他们放弃了这个想法,来到了所谓的“AI 冬季”。事实上,随着层数的增加,收敛的神经网络的难度越来越大。
事实证明,具有许多层(目前为 20、50 甚至 100 层)的深度神经网络可以非常有效地运行,只需使用几个粗略的数学技巧来使其收敛。这些简单的技巧的发现是深度学习在 2010 年代复兴的原因之一。
RELU 激活
实际上,S 型激活函数在深度网络中存在问题。它会合并 0 到 1 之间的所有值,当您反复执行此操作时,神经元输出及其梯度可能会完全消失。提及它是因为历史原因,但现代影音平台使用 RELU(修正线性单元),如下所示:
另一方面,relu 的导数为 1,至少在其右侧是这样。使用 RELU 激活后,即使来自某些神经元的梯度可能为零,也始终存在其他神经元提供明显的非零梯度,并且训练可以继续以良好的速度继续。
更好的优化器
在像这里这样的非常高维空间中,我们有大约 10000 个权重和偏差的顺序,即“马鞍点”频率。这些点不是局部最小值,但梯度值为零,并且梯度下降法优化器一直卡在那里。TensorFlow 拥有一系列可用的优化器,其中一些优化器可以承受一定的惯性,并且能够安全地越过马鞍点。
随机初始化
在训练之前初始化权重偏差这一艺术,这本身就是一个研究领域,有许多有关该主题的论文发表过。点击此处可查看 Keras 中提供的所有初始化程序。幸运的是,Keras 在默认情况下会做正确的事,并使用 'glorot_uniform'
初始化程序,这在几乎所有情况下都是最好的。
您不需要执行任何操作,因为 Keras 已经正确执行了操作。
NaN ???
交叉熵公式涉及对数,而 log(0) 为非数字(如果你喜欢 NaN,也称作数字崩溃)。交叉熵的输入值是否可以为 0?输入来自 softmax,其本质上是一个指数,一个指数从来都不是 0。所以我们很安全!
真的吗?在美好的数学世界中,我们可以放心,但在计算机世界中,以 float32 格式表示的 exp(-150) 为零,而交叉熵会崩溃。
幸运的是,您在这里也无需执行任何操作,因为 Keras 会处理这一点,并以特别谨慎的方式计算 softmax 和交叉熵,以确保数值稳定性并避免可怕的 NaN。
成功了吗?
现在您应该可以达到 97% 的准确率。本次研讨会的目标是要显著高于 99%,让我们继续加油。
如果您遇到问题,请参阅以下解决方案:
8. 学习速率衰减
或许我们可以试试提高训练速度?Adam 优化器中的默认学习速率为 0.001。我们来试着增加一个。
提高速度似乎并不能带来太大帮助,这些噪音都是什么呢?
训练曲线非常嘈杂,我们看看两条验证曲线:它们上下跳动。也就是说,我们的操作速度太快了。我们可以恢复到之前的速度,但还有更好的方法。
较好的解决方案是快速起步,然后以指数方式衰减学习速率。在 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% 以上。
9. 丢弃、过拟合
模型现在似乎很好地收敛。让我们尝试进行更深入的探索。
是否有用?
实际上,准确率仍然在 98%,再看看验证损失。它正在上升!学习算法仅处理训练数据,并相应优化训练损失。它从不看到验证数据,因此一段时间后其工作不再对验证损失产生影响也就不足为奇了,验证损失会停止下降,有时甚至反弹。
这不会立即影响模型的实际识别能力,但会阻止您运行多次迭代,这通常表明训练不再产生积极影响。
这种脱节通常称为“过拟合”看到这些数据后,您可以尝试应用一种叫做“丢弃”的正则化技术。丢弃技术会在每次训练迭代时发射随机神经元。
效果如何?
再次出现噪音(根据漏失的工作原理,出乎意料)。验证损失似乎不再起伏不定,但整体而言比没有丢弃的损失要高。验证准确率也略有下降。这个结果相当令人失望。
丢弃似乎不是正确的解决方案,或者可能是“过拟合”是一个更复杂的概念,其中一些原因不适合“丢弃”修复?
什么是“过拟合”?当神经网络学习“糟糕”时,就会发生过拟合,学习方式对训练样本有效,但在真实数据上效果不佳。有些正则化技术(如丢弃)可以迫使它以更好的方式进行学习,但过拟合也有更深的根源。
当神经网络当前解决问题的自由度过高时,就会发生基本的过拟合。想象一下我们有这么多的神经元,以至于网络可以在其中存储所有训练图像,然后通过模式匹配来识别它们。它在处理真实数据时将完全失败。神经网络必须在一定程度上受到限制,这样它才能被迫在训练期间泛化所学到的内容。
如果您的训练数据非常少,即使是小型网络也可以凭心学习,您将看到“过拟合”。一般来说,您总是需要大量数据来训练神经网络。
最后,如果您已经按照书中的流程完成了所有工作,尝试了不同规模的网络以确保其自由度受到约束,应用了丢弃,并使用大量数据进行训练,那么,您可能仍会陷入似乎无力改进的性能水平。这意味着,当前的神经网络不能从数据中提取更多信息,正如我们这里所介绍的那样。
还记得我们是如何使用平面化成单个矢量的图像的吗?这真是个坏主意。手写数字由形状组成,我们在扁平化像素时舍弃了形状信息。不过,有一种能够利用形状信息的神经网络:卷积网络。我们来试试看。
如果您遇到问题,请参阅以下解决方案:
10. [INFO] 卷积网络
简述
如果你已经知道下一段落中以粗体显示的所有术语,可以接着进行下一个练习。如果您刚开始使用卷积神经网络,请继续阅读。
插图:使用两个由 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')
])
在卷积网络中的一个层中,一个“神经元”会对紧邻其上的像素进行加权和,只对其上的一小块区域进行加权。它会添加偏差并通过激活函数向和馈送总和,就像常规密集层中的神经元一样。然后,使用相同的权重对整张图片重复此操作。请记住,在密集层中,每个神经元都有自己的权重。在这里,一个“补丁”的权重在图像上两个方向滑动(“卷积”)。输出的值与图像中的像素数一样多(不过边缘部分需要一些内边距)。它是一种过滤操作。在上图中,它使用了权重为 4x4x3=48 的过滤器。
但是,48 个权重并不够。为了增加自由度,我们使用一组新权重重复同一操作。这样会生成一组新的过滤器输出。我们称之为“频道”通过与输入图像中的 R、G、B 通道类比来比较输出结果。
通过添加新维度,将两组(或更多)权重求和为一个张量。这为我们确定了卷积层权重张量的通用形状。由于输入和输出通道的数量是参数,因此我们可以开始堆叠和链接卷积层。
图解:卷积神经网络转换“立方体”将数据转移到其他“立方体”数据。
步进卷积、最大池化
通过以步长 2 或 3 执行卷积,我们还可以在水平维度上缩小生成的数据立方体。您可以采用以下两种常见方式:
- 步进卷积:如上所示的滑动过滤器,但步长 >1
- 最大池化:应用 MAX 操作的滑动窗口(通常采用 2x2 图块,每 2 像素重复一次)
图解:将计算窗口滑动 3 个像素会减少输出值。步长卷积或最大池化(以 2x2 窗口的步长 2 滑动的最大值)是在水平维度上缩小数据立方体的方法。
最后一层
在最后一个卷积层之后,数据将采用“立方体”的形式。有两种方法可以将其馈送到最后一个密集层。
第一种方法是将数据立方体扁平化为向量,然后将其馈送到 softmax 层。有时,您甚至可以在 softmax 层之前添加一个密集层。就权重数量而言,这往往非常昂贵。卷积网络末尾的密集层可以包含超过整个神经网络权重的一半。
除了使用昂贵的密集层之外,我们还可以拆分传入数据“立方体”分成尽可能多的部分,然后取平均值,然后用 softmax 激活函数输入这些值。这种构建分类头部的方法的权重为 0。在 Keras 中,有一个用于填充的层:tf.keras.layers.GlobalAveragePooling2D()
。
跳至下一部分即可构建卷积网络来解决当前问题。
11. 卷积网络
我们来构建一个用于手写数字识别的卷积网络。我们将在顶部使用三个卷积层,在底部使用传统的 softmax 读取层,并将它们与一个全连接层连接:
请注意,第二个和第三个卷积层的步长为 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”激活卷积层的语法为:
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% 的准确率障碍?非常接近...不过还是看看验证损失曲线。这会响铃吗?
还要查看预测结果。第一次,您应该会发现 10,000 个测试数字中大多数现在都被正确识别了。仅剩余约 41⁄2 行错误检测(10,000 位数中有大约 110 位数)
如果您遇到问题,请参阅以下解决方案:
12. 再次退出
之前的训练有明显的过拟合迹象(并且准确率依然低于 99%)。我们是否应该再次尝试丢弃?
这次成功了?
这次丢弃似乎解决了问题。验证损失不再上升,最终准确率应远高于 99%。恭喜!
首次尝试应用丢弃时,我们以为存在过拟合问题,而实际上问题出在神经网络的架构上。如果没有卷积层,我们无法更进一步,而丢弃层也无能为力。
这一次,过拟合似乎是导致问题的原因,而丢弃实际上有所帮助。请记住,有很多因素会导致训练损失曲线与验证损失曲线之间出现脱节,并且验证损失不断上升。过拟合(自由度过高,被网络滥用)只是其中之一。如果数据集过小或者神经网络的架构不充分,您可能会在损失曲线上看到类似行为,但丢弃不会有所帮助。
13. 批量归一化
最后,我们来尝试添加批量归一化。
从理论上说,在实践中,只需记住几条规则即可:
现在,我们按照这本书中的说明,在除最后一层之外的每个神经网络层上添加一个批量正则层。不要将其添加到最后一个“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'),
现在的准确率如何?
稍微调整一下(BATCH_SIZE=64,学习速率衰减参数 0.666,密集层 0.3 上的丢弃率),运气好,可以达到 99.5%。按照“最佳实践”进行了学习速率和丢弃调整使用批量归一化:
- 批次范数有助于神经网络收敛,并且通常可以提高训练速度。
- 批次归一化是一种正则化器。通常,您可以减少丢弃的数量,甚至完全不使用丢弃。
解决方案笔记本的训练运行率为 99.5%:
14. 在强大的硬件上在云端训练:AI Platform
您可以在 GitHub 上的 mlengine 文件夹中找到代码支持云的版本,以及有关在 Google Cloud AI Platform 上运行该代码的说明。在运行此部分之前,您必须先创建 Google Cloud 账号并启用结算功能。完成本实验所需的资源应该不到几美元(假设在一个 GPU 上训练 1 小时)。要准备好您的账号,请按以下步骤操作:
- 创建 Google Cloud Platform 项目 ( http://cloud.google.com/console)。
- 启用结算功能。
- 安装 GCP 命令行工具(点击此处可查看 GCP SDK)。
- 创建一个 Google Cloud Storage 存储分区(放入区域
us-central1
)。它将用于暂存训练代码并存储经过训练的模型。 - 启用必要的 API 并请求必要的配额(运行训练命令一次,您应该会收到错误消息,告知您要启用的内容)。
15. 恭喜!
您已经构建了第一个神经网络,并一直训练它的准确率达到 99%。在此过程中学到的技术并非特定于 MNIST 数据集,实际上它们在处理神经网络时被广泛使用。要送你临别礼物,《悬崖音符》实验卡片(卡通版)。您可以用它来记住您学到的内容:
后续步骤
- 了解完全连接和卷积网络之后,建议您了解一下循环神经网络。
- 为了在分布式基础架构上在云端运行训练或推理,Google Cloud 提供了 AI Platform。
- 最后,欢迎您提供反馈。如果您发现此实验中存在错误,或者您认为需要改进,请告诉我们。我们通过 GitHub 问题 [反馈链接] 处理反馈。
|
本实验室版权中包含所有卡通图片:alexpokusay / 123RF 图库照片