MXNet 深度学习秘籍(三)
原文:
annas-archive.org/md5/22b009d5e2069762a5538fce4fc0f3fd译者:飞龙
第七章:使用迁移学习与微调优化模型
随着模型规模的增大(每层的深度和处理模块数量),训练它们所需的时间呈指数增长,通常为了达到最佳性能,需要更多的训练轮次(epoch)。
因此,MXNet通过GluonCV和GluonNLP库提供了最先进的预训练模型。正如我们在前几章中所见,当我们的最终数据集与所选模型的预训练数据集相似时,这些模型可以帮助我们解决各种问题。
然而,有时候这还不够,最终数据集可能存在一些微妙的差异,预训练模型并未能捕捉到这些差异。在这种情况下,将预训练模型所存储的知识与我们的最终数据集相结合是理想的做法。这就是迁移学习,我们将预训练模型的知识转移到一个新的任务(最终数据集)上。
本章我们将学习如何使用 MXNet Gluon 库中的 GluonCV 和 GluonNLP,分别针对计算机视觉(CV)和自然语言处理(NLP)。我们还将学习如何从它们的模型库中获取预训练模型,并通过迁移这些预训练模型的学习成果来优化我们自己的网络。
具体来说,我们将在本章中涵盖以下主题:
-
理解迁移学习与微调
-
提升图像分类性能
-
提升图像分割性能
-
提升从英语翻译到德语的性能
技术要求
除了前言中指定的技术要求外,以下技术要求适用:
-
确保你已经完成了第一章的食谱,安装 MXNet、Gluon、GluonCV 和 GluonNLP,第一章,开始使用 MXNet。
-
确保你已经完成了第五章,使用计算机视觉分析图像,以及第六章,理解自然语言处理中的文本。
本章的代码可以在以下 GitHub 链接中找到:github.com/PacktPublishing/Deep-Learning-with-MXNet-Cookbook/tree/main/ch07。
此外,你可以直接从 Google Colab 访问每个食谱;例如,本章的第一个食谱可以在这里找到:colab.research.google.com/github/PacktPublishing/Deep-Learning-with-MXNet-Cookbook/blob/main/ch07/7_1_Understanding_Transfer_Learning_and_Fine_Tuning.ipynb。
理解迁移学习与微调
在之前的章节中,我们看到如何利用 MXNet、GluonCV 和 GluonNLP 来检索特定数据集(如 ImageNet、MS COCO 和 IWSLT2015)中的预训练模型,并将它们应用于我们的特定任务和数据集。
在本教程中,我们将介绍一种称为迁移学习的方法,它允许我们结合预训练模型(在通用知识数据集上)的信息和新领域的信息(来自我们希望解决的任务数据集)。这种方法有两个主要显著优势。一方面,预训练数据集通常是大规模的(ImageNet-22k 有 1400 万张图像),使用预训练模型可以节省训练时间。另一方面,我们不仅用我们的特定数据集进行评估,还用它来训练模型,在所需的场景中提高其性能。正如我们将发现的那样,这并不总是一种容易的方式来实现,因为它需要能够获得一个可观的数据集,或者甚至一种正确的方式,因为它可能不会产生预期的结果。我们还将探讨迁移学习之后的可选下一步,称为微调,我们将尝试使用我们的特定数据集进一步修改模型参数。我们将对这两种技术进行测试。
准备就绪
和之前的章节一样,在本教程中,我们将使用一些矩阵运算和线性代数,但这一点一点也不难。
如何做到…
在本教程中,我们将关注以下步骤:
-
引入迁移学习
-
描述迁移学习的优势及其使用时机
-
理解表示学习的基础知识
-
专注于实际应用
让我们深入了解每一个步骤。
引入迁移学习
在之前的章节中,我们学习了如何从头开始训练深度学习神经网络,探索计算机视觉和自然语言处理中的问题。正如在第三章中介绍的那样,解决回归问题,深度学习神经网络试图模仿我们大脑中的生物网络。一个有趣的观点是,当我们(及我们的大脑)学习新任务时,我们以非常强大的方式利用先前获得的知识。例如,一个非常优秀的网球选手会在几个小时的比赛中成为相对优秀的壁球选手。迁移学习是一个研究领域,其中包含不同的技术,以达到类似于这个例子的结果。
图 7.1 – 传统机器学习(ML)与迁移学习之间的比较
在图 7.1中,我们可以看到两种范式的对比,其中迁移学习解决任务 2 的方法利用了在解决任务 1 时获得的知识。然而,这意味着要解决单个目标任务(任务 2),我们需要训练两次模型(分别为任务 1 和任务 2)。实际上,正如我们接下来的步骤所示,我们将使用来自 MXNet 的 GluonCV 和 GluonNLP 模型库中的预训练模型,因此我们只需为任务 2 训练一次模型。
描述迁移学习的优势以及何时使用它
使用迁移学习具有多种优势,原因有很多:
-
更快:通过利用来自模型库的预训练模型,训练过程会比从头开始训练更快收敛,所需的训练轮次和时间都会大大减少。
-
更通用:通常,预训练模型是使用大规模数据集(如 ImageNet)进行训练的,因此学习到的参数(权重)具有广泛的适用性,能够重用于大量任务。这个目标是通过使用大规模数据集训练得到的、既通用又不依赖特定领域的特征提取部分(也称为表示)来实现的。
-
需要更少的数据:为了将预训练模型适配到新的任务上,所需的数据量远低于从头开始训练该模型架构的数量。这是因为表示(如前述)可以被重用。
-
更环保:由于迁移学习所需的训练时间、数据集和计算资源远低于从头开始训练,训练模型所需的污染也大大减少。
-
性能提升:已有研究证明(例如,
www.cv-foundation.org/openaccess/content_cvpr_2014/papers/Oquab_Learning_and_Transferring_2014_CVPR_paper.pdf)迁移学习在小规模数据集上能带来显著的性能提升,在大规模数据集上,迁移学习能比从头训练更快地达到相同的性能水平。
在图 7.2中,分析了计算表示的不同方法,尽管专用网络可以达到更好的性能,但这只有在拥有大规模数据集、高端计算资源和更长训练时间的情况下才能实现。
图 7.2 – 比较不同的表示学习方法
在更广泛的场景下,迁移学习有多种实现方法,如下图所示:
图 7.3 – 不同类型的迁移学习
在图 7.3中,我们可以看到不同类型的迁移学习,这取决于源领域和目标领域的相似性以及源领域和目标领域数据的可用性。在本章中,我们将探讨在与我们目标任务相似的领域中使用预训练模型的常见设置(源领域和目标领域相同),并且任务会稍有不同,同时在目标领域有一定量的标注数据(归纳 迁移学习)。
百度首席科学家、Google Brain 的联合创始人 Andrew Ng 在 2016 年 NIPS 的一个教程中说:“在未来几年,我们将看到通过迁移学习带来大量具体的价值,”他是对的。
理解表示学习的基本原理
在本节中,我们将从更理论的角度回答如何使用迁移学习以及它为何有效的问题。在第五章,使用计算机视觉分析图像,和第六章,使用自然语言处理理解文本中,我们介绍了使用 GluonCV 提取图像特征的表示概念,以及使用 GluonNLP 提取文本中单词/句子的表示概念。
我们可以在图 7.4中回顾 CNN 架构的常见结构:
图 7.4 – 卷积神经网络(CNNs)的复习
在图 7.5中,我们可以回顾 Transformer 架构的常见结构:
图 7.5 – Transformer 架构的复习(左侧是编码器,右侧是解码器)
这一基本思想在两个领域中都是共通的;例如,CNN 的特征提取部分和 Transformer 中的编码器就是表示,而这些网络部分的训练被称为表示学习,这是一项积极的研究领域,因为它具备在监督和无监督设置下训练这些网络的能力。
迁移学习背后的主要思想是将一个任务中学习到的表示迁移到另一个任务中;因此,我们通常会遵循以下步骤:
-
从 MXNet 的模型库中获取预训练模型(GluonCV 或 GluonNLP)。
-
移除最后的层(通常是分类器)。将其他层的参数冻结(在训练过程中不可更新)。
-
添加新的层(新的分类器),以适应新任务
-
使用目标数据训练更新后的模型(只有新添加的层是可更新的,其他冻结的层在训练期间不可更新)。
如果我们有足够的标注数据来解决我们想要解决的任务(目标任务),另一个步骤(可以在前一步之后执行,也可以替代前一步)叫做微调。
微调考虑到原始学习的表示可能无法完美适应目标任务,因此,通过更新也能得到改进。在这种情况下,步骤如下:
-
解冻表示网络的权重。
-
使用目标数据重新训练网络,通常使用较小的学习率,因为表示应该接近(同一领域)。
两个过程(迁移学习和微调)在图 7.6中有直观总结。
图 7.6 – 迁移学习与微调
两个过程可以按顺序应用,每个过程都有适当的超参数。
重点关注实际应用
在本节中,我们将使用到目前为止所学的表示学习内容,并将其应用于一个实际的示例:检测猫和狗。
为此,我们将从GluonCV 模型库中检索一个模型;我们将去除分类器(最后的几层),保留特征提取阶段。然后,我们将分析猫和狗的表示是如何被学习的。要加载模型,我们可以使用以下代码片段:
alexnet = gcv.model_zoo.get_model("resnet152_v2", pretrained=True, ctx=ctx)
在前面的代码片段中,对于pretrained参数,我们已将其赋值为True,表示我们希望加载预训练权重(而不仅仅是模型的架构)。
当正确训练时,CNN 会学习训练数据集中图像特征的层次化表示,每一层逐渐学习越来越复杂的模式。因此,当图像被处理时(在连续的层中进行处理),网络能够计算与网络相关的更复杂的模式。
现在,我们可以使用一个新的 MXNet 库,MXBoard(请参阅安装说明中的食谱),使用此模型来评估狗图像经过的不同步骤,并查看一些预训练模型计算其表示的示例:
图 7.7 – 猫与狗的表示 – 卷积滤波器
在图 7.7中,我们可以看到对应于 ResNet152 预训练网络(在 ImageNet 上)的第一层卷积层的卷积滤波器。请注意这些滤波器如何专注于简单的模式,如特定的形状(垂直和水平线、圆形等)和特定的颜色(红色斑点)。
让我们用一张特定的图像来分析结果:
图 7.8 – 一只狗的示例图像
我们从Dogs vs. Cats数据集中选择一张图像,例如图 7.8中描绘的狗。当将这张图像通过我们的网络时,我们将得到类似以下的结果:
图 7.9 – 卷积滤波器输出
在图 7.9中,我们可以看到我们狗的示例在图 7.7中的过滤器输出。注意不同的输出如何突出显示简单的形状,比如眼睛或腿部(较大的值,接近白色)。
最后,随着图像在网络中传播,它的特征会越来越压缩,最终得到(对于 ResNet152)一个包含 2,048 个元素的向量。这个向量可以通过使用 MXNet 的模型库轻松计算:
resnet152.features(summary_image.as_in_context(ctx))
这段代码示例会输出以下结果:
[[2.5350871e-04 2.8519407e-01 1.6196619e-03 ... 7.2884483e-05
2.9618644e-07 7.8995163e-03]]
<NDArray 1x2048 @gpu(0)>
如我们所见,我们得到了一个2048元素。
它是如何工作的...
在本篇食谱中,我们介绍了迁移学习和微调的概念。我们解释了何时使用这两种不同技术及其优点。
我们还探讨了这些技术何时有用及其与表征学习的关系,解释了在使用这些技术时,表征在知识转移中的重要作用。我们使用了一个新的库,MXBoard,来生成表征的可视化。
此外,我们直观且实践地展示了如何将这些技术应用于计算机视觉和自然语言处理任务,并为一个具体示例计算了表征。
还有更多...
迁移学习,包括微调,是一个活跃的研究领域。在这个食谱中,我们仅涵盖了深度学习中最有用的场景——归纳迁移学习。想了解更全面但仍易于阅读的介绍,我推荐阅读迁移学习:友好的介绍,可以在以下网址找到:journalofbigdata.springeropen.com/articles/10.1186/s40537-022-00652-w。
此外,知识从一个系统转移到另一个系统的概念并不新颖,早在 1995 年就有学习如何学习和知识转移等概念的引用,那时曾有一个关于这个主题的神经信息处理系统(NeurIPS)研讨会。该研讨会的总结可以在这里找到:socrates.acadiau.ca/courses/com…
此外,正如 21 年后在同一场合中介绍的,Andrew Ng 正确预见了迁移学习的重要性。他的 2016 年 NeurIPS 教程可以在这里找到(跳转到 1 小时 37 分钟查看迁移学习相关内容):www.youtube.com/watch?v=F1ka6a13S9I。
提升图像分类的性能
在上一篇食谱中介绍了迁移学习和微调后,在本篇中,我们将其应用于图像分类,这是一个计算机视觉任务。
在第二个配方中,使用 MXNet 进行图像分类——GluonCV 模型库,AlexNet 和 ResNet,在第五章,使用计算机视觉分析图像,我们已经看到如何使用 GluonCV 检索预训练模型,并直接用于图像分类任务。在第一次的实例中,我们展示了如何从零开始训练模型,实际上仅利用预训练模型的架构,而没有利用任何包含在预训练权重中的过去知识,这些权重已被重新初始化,删除了任何历史信息。之后,预训练模型直接用于任务,实际上也利用了模型的权重/参数。
在这个配方中,我们将把模型的权重/参数与目标数据集结合,应用本章介绍的技术——迁移学习和微调。用于预训练的数据集是Dogs vs Cats数据集。
准备工作
和前面的章节一样,在这个配方中,我们将使用一些矩阵运算和线性代数,但一点也不难。
此外,我们还将处理文本数据集;因此,我们将重新审视在第二个配方中已看到的一些概念,使用 MXNet 进行图像分类:GluonCV 模型库,AlexNet 和 ResNet,在第五章,使用计算机视觉分析图像。
如何操作...
在这个配方中,我们将查看以下步骤:
-
重新审视ImageNet-1k和Dogs vs. Cats数据集
-
从头开始训练一个ResNet模型,使用Dogs vs Cats数据集
-
使用预训练的 ResNet 模型,通过迁移学习从ImageNet-1k到Dogs vs Cats优化性能
-
对Dogs vs Cats数据集上的预训练 ResNet 模型进行微调
接下来,我们将详细介绍这些步骤。
重新审视 ImageNet-1k 和 Dogs vs Cats 数据集
ImageNet-1k和Dogs vs Cats都是图像分类数据集;然而,它们有很大的不同。ImageNet-1k是一个大规模数据集,包含约 120 万张图像,按 1000 个类别进行标签,广泛用于研究和学术界的基准测试。Dogs vs Cats是一个小规模数据集,包含 1400 张描绘狗或猫的图像,其知名度主要来自于 2013 年启动的 Kaggle 竞赛。
MXNet GluonCV 不提供直接下载数据集的方法。然而,我们不需要ImageNet-1k数据集(它的大小约为 133GB),只需要我们选择的模型的预训练参数。预训练模型可以直接从 MXNet GluonCV 模型库下载,我们在前面的章节中见过例子,在本章中也将再次使用它们。
这是一些来自ImageNet-1k的示例:
图 7.10 – ImageNet-1k 示例
上图的来源是cs.stanford.edu/people/karpathy/cnnembed/。
对于猫狗数据集,关于如何获取数据集的所有信息都可以在第二个配方中找到,即使用 MXNet 进行图像分类:GluonCV 模型动物园、AlexNet 和 ResNet,在第五章,使用计算机视觉分析图像。根据该配方的代码作为参考,我们可以显示一些示例:
图 7.11 – 猫狗数据集
在图 7**.10和图 7**.11中,我们可以看到ImageNet-1k中的一些图像与猫狗数据集中的一些图像相似。
从头开始训练一个 ResNet 模型,使用猫狗数据集
如第二个配方中所述,使用 MXNet 进行图像分类:GluonCV 模型动物园、AlexNet 和 ResNet,在第五章,使用计算机视觉分析图像,我们将使用softmax 交叉熵作为损失函数,以及accuracy和混淆矩阵作为评估指标。
我们使用 ResNet 模型进行训练的演变如下:
图 7.12 – ResNet 训练演变(训练损失和验证损失,以及验证精度)– 从头开始训练
此外,在最佳迭代中,测试集中得到的accuracy值如下:
('accuracy', 0.75)
混淆矩阵如下:
图 7.13 – 从头开始训练的 ResNet 模型在猫狗数据集中的混淆矩阵
在经过多次训练(例如,100 次)后,获得的准确度值(75%)和图 7**.13显示出相当平均的性能。鼓励您运行自己的实验,尝试不同的超参数设置。
定性上,我们还可以检查我们的模型在一个示例图像中的表现。在我们的案例中,我们选择了以下内容:
图 7.14 – 猫狗定性示例,具体为猫
我们可以通过以下代码片段运行这张图像,检查我们模型的输出:
# Qualitative Evaluation
# Qualitative Evaluation
# Expected Output
print("Expected Output:", example_label)
# Model Output
example_output = resnet50_ft(example_image_preprocessed)
class_output = np.argmax(example_output, axis=1).asnumpy()[0]
print("Class Output:", class_output)
assert class_output == 0 # Cat 0
这些代码语句将给出以下输出:
Expected Output: 0
Class Output: 0
如结果所示,图像已正确分类为猫。
使用预训练的 ResNet 模型,通过从 ImageNet-1k 到猫狗数据集的迁移学习来优化性能
在前面的配方中,我们使用我们的数据集从头开始训练了一个新模型。然而,这有两个重要的缺点:
-
从头开始训练需要大量的数据。
-
由于数据集的大尺寸和模型学习任务所需的周期数,训练过程可能需要很长时间。
因此,在这个食谱中,我们将采取不同的方法:我们将使用来自 MXNet GluonCV 的预训练模型来解决任务。这些模型已经在 ImageNet-1k 数据集上训练过,该数据集包含我们感兴趣的类别(猫和狗);因此,我们可以利用这些学到的特征,并轻松将其迁移到 Dogs vs Cats(相同领域)。
对于 ResNet 模型,使用以下代码:
# ResNet50 from Model Zoo (This downloads v1d)
resnet50 = gcv.model_zoo.get_model("resnet50_v1d", pretrained=True, ctx=ctx)
如我们在前面的代码段中看到的,按照本章第一个食谱《理解迁移学习和微调》中的讨论,对于 pretrained 参数,我们已将其值设置为 True,表示我们希望获取预训练权重(而不仅仅是模型的架构)。
为了充分评估迁移学习带来的改进,我们将在应用迁移学习到 Dogs vs Cats 数据集之前和之后,直接评估我们的预训练模型(源任务是 ImageNet-1k)。因此,使用我们当前的预训练模型,我们得到如下结果:
('accuracy', 0.925)
混淆矩阵如下:
图 7.15 – 在预训练 ResNet 模型下,Dogs vs Cats 数据集的混淆矩阵
如我们所见,我们的预训练 Transformer 模型在同一领域下已经显示出良好的表现;然而,单纯使用预训练模型并不能比从零开始训练获得更好的表现。使用预训练模型的巨大优势在于节省时间,因为加载它只需要几行代码。
我们还可以通过相同的图像示例检查模型的定性表现。注意代码与之前的定性图像摘录有所不同,因为现在我们需要将 ImageNet 类别(即我们的预训练 ResNet50 模型的输出)转换为我们的类别(0 代表猫,1 代表狗)。新的代码如下所示:
# Qualitative Evaluation
# Expected Output
print("Expected Output:", example_label)
# Model Output
example_output = resnet50(example_image_preprocessed)
class_output = model.CLASSES_DICT[np.argmax(example_output, axis=1).asnumpy()[0]]
print("Class Output:", class_output)
assert class_output == 0 # Cat
这些代码语句将会给我们以下输出:
Expected Output: 0
Class Output: 0
从结果中可以看出,该图像已被正确分类为猫。
现在我们有了一个基准进行比较,让我们将迁移学习应用到我们的任务中。从第一个食谱《理解迁移学习和微调》中,第一步是从 MXNet 模型库(GluonCV 或 GluonNLP)中获取一个预训练的模型,这一步我们已经完成。
第二步是移除最后一层(通常是分类器),保持其他层的参数被冻结(在训练过程中不可更新),所以让我们开始吧!
我们可以用以下代码段替换分类器:
# Replace the classifier (with gradients activated)
resnet50_tl.fc = mx.gluon.nn.Dense(2)
resnet50_tl.fc.initialize(ctx=ctx)
我们可以通过以下代码段来冻结 ResNet 特征提取层:
for param in resnet50_tl.collect_params().values():
param.grad_req = 'null'
我们可以用以下代码段替换分类器:
# Replace the classifier (with gradients activated)
resnet50_tl.fc = mx.gluon.nn.Dense(2)
resnet50_tl.fc.initialize(ctx=ctx)
现在,我们可以应用通常的训练过程来处理 Dogs vs Cats 数据集,并且我们在使用 ResNet 模型的训练中有了以下进展:
图 7.16 – ResNet 训练演变(训练损失和验证损失)– 迁移学习
此外,对于最佳迭代,测试集上的准确率如下:
('accuracy', 0.985)
混淆矩阵如下:
图 7.17 – 使用迁移学习的 ResNet 模型在狗与猫数据集上的混淆矩阵
与我们之前从头开始训练的实验相比,这个实验的表现大大提高,并且我们只用了几分钟就让这个模型开始在我们期望的任务中表现良好,而之前的实验需要几个小时,并且需要多次调整超参数,这可能会转化为几天的工作。
我们还可以使用相同的图像示例和代码来定性地检查我们的模型表现。输出结果如下:
Expected Output: 0
Class Output: 0
从结果可以看出,图像已经被正确分类为猫。
在狗与猫数据集上微调我们的预训练 ResNet 模型
在之前的步骤中,我们冻结了编码器层中的参数。然而,由于我们当前使用的数据集(狗与猫)样本足够多,我们可以解冻这些参数并训练模型,从而有效地让新的训练过程更新表示(在迁移学习中,我们直接使用了为ImageNet-1k学到的表示)。这个过程叫做微调。
微调有两种变体:
-
通过冻结层并在之后解冻(迁移学习后的微调)来应用迁移学习
-
直接应用微调,而没有冻结层的预处理步骤(直接微调)
让我们计算这两个实验并通过比较结果得出结论。
对于第一个实验,我们可以取之前步骤中获得的网络,解冻层并重新开始训练。在 MXNet 中,要解冻编码器参数,我们可以运行以下代码片段:
# Un-freeze weights
for param in resnet50_ft.collect_params().values():
if param.name in updated_params:
param.grad_req = 'write'
现在,我们可以应用常规的训练过程与狗与猫数据集,并且我们在使用 ResNet 模型进行训练时,得到了以下演变:
图 7.18 – ResNet 训练演变(训练损失和验证损失)– 迁移学习后的微调
此外,对于最佳迭代,测试集上的准确率如下:
('accuracy', 0.90255)
混淆矩阵如下:
图 7.19 – 使用迁移学习后微调的 ResNet 模型在狗与猫数据集上的混淆矩阵
与我们之前的迁移学习实验相比,这次实验的表现更差。这是由于数据集的大小和选择的超参数组合。鼓励你尝试自己的实验。
我们还可以使用相同的图像示例和代码,定性地检查我们的模型表现如何。输出如下所示:
Expected Output: 0
Class Output: 0
从结果可以看出,图像已被正确分类为猫。
现在让我们继续进行第二次微调实验,这次我们不采用迁移学习,而是直接对整个模型进行微调(没有冻结层)。
我们需要再次获取为ImageNet-1k预训练的 ResNet 模型,MXNet GluonCV 的代码片段如下:
# ResNet50 from Model Zoo (This downloads v1d)
resnet50 = gcv.model_zoo.get_model("resnet50_v1d", pretrained=True, ctx=ctx)
现在,我们可以在没有冻结层的情况下应用训练过程,这将更新我们 ResNet 模型的所有层,得到如下的损失曲线:
图 7.20 – ResNet 训练过程(训练损失与验证损失)– 微调且未冻结层
此外,对于最佳迭代,在测试集上获得的准确率如下:
('accuracy', 0.98)
这个值与之前迁移学习实验的结果相似。对于混淆矩阵,结果如下:
图 7.21 – ResNet 模型在狗与猫分类中的混淆矩阵,采用微调且未冻结层
如前所述,与我们之前的微调实验相比,我们可以看到这次实验的性能更高。根据经验,这已被证明是一个可重复的结果,且被认为是因为最初冻结编码器允许解码器(使用编码器表示)学习当前的新任务。从信息流的角度来看,在这一步骤中,知识从特征提取阶段转移到了分类器。在第二步中,当特征提取阶段解冻时,分类器中学到的参数执行辅助的迁移学习——这次是从分类器到特征提取阶段。
我们还可以使用相同的图像示例和代码,定性地检查我们的模型表现如何。输出如下所示:
Expected Output: 0
Class Output: 0
从结果可以看出,图像已被正确分类为猫。
它的工作原理是…
在这个配方中,我们将第一个章节开头介绍的迁移学习和微调技术应用于图像分类任务,这个任务在第二个配方中也有呈现,名为使用 MXNet 分类图像:GluonCV 模型库、AlexNet 和 ResNet,第五章,计算机视觉下的图像分析。
我们重新访问了两个已知的数据集,ImageNet-1k 和 Dogs vs Cats,并打算结合这两个数据集,基于前者数据集的知识转移,并用后者对该知识进行微调。此外,这是通过利用 MXNet GluonCV 提供的工具实现的:
-
为 ImageNet-1k 提供的预训练 ResNet 模型
-
便于访问的工具,用于狗 与猫 数据集
此外,我们继续使用用于图像分类的损失函数和指标,包括 softmax 交叉熵、准确率和混淆矩阵。
有了 MXNet 和 GluonCV 中这些随时可用的工具,我们只需几行代码就能运行以下实验:
-
从零开始训练 Dogs vs Cats 中的模型
-
使用预训练模型,通过从 ImageNet-1k 到 Dogs vs Cats 的转移学习优化性能
-
在 Dogs vs Cats 数据集上微调我们的预训练模型(包括和不包括冻结层)
在进行不同实验后,我们直接获得了转移学习与微调之间的有效联系(准确率分别为 0.985 和 0.98)。实际运行这些实验时,结果可能会根据模型架构、数据集和选择的超参数有所不同,因此建议大家尝试不同的技术和变种。
还有更多…
转移学习,包括微调,是一个活跃的研究领域。一篇 2022 年发布的论文探讨了图像分类领域的最新进展。该论文标题为《深度转移学习用于图像分类:一项综述》,可以在此查阅:www.researchgate.net/publication/360782436_Deep_transfer_learning_for_image_classification_a_survey。
关于计算机视觉应用案例的更一般方法,最近发布了一篇论文《转移学习方法作为解决小数据集计算机视觉任务的新方法》,该论文评估了小数据集的问题,并应用这些技术解决医学影像任务。可以在此查阅:www.researchgate.net/publication/344943295_Transfer_Learning_Methods_as_a_New_Approach_in_Computer_Vision_Tasks_with_Small_Datasets。
提高图像分割性能
在本配方中,我们将应用转移学习和微调来进行语义分割,这是一个计算机视觉任务。
在第四个配方中,使用 MXNet 进行图像对象分割:PSPNet 与 DeepLab-v3,在第五章 使用计算机视觉分析图像 中,我们展示了如何使用 GluonCV 直接获取预训练模型并将其用于语义分割任务,通过使用预训练模型的架构和权重/参数,充分利用过去的知识。
在本教程中,我们将继续利用模型的权重/参数,用于一个任务,该任务包括使用语义分割模型在一组 21 类图像中对图像进行分类。用于预训练的数据集是MS COCO(源任务),我们将运行多个实验来评估我们的模型在一个新的(目标)任务中的表现,使用Penn-Fudan Pedestrian数据集。在这些实验中,我们还将包含来自目标数据集的知识,以提高我们的语义分类性能。
准备工作
至于之前的章节,在本教程中,我们将使用一些矩阵操作和线性代数,但这一点也不难。
此外,我们将使用文本数据集;因此,我们将重新讨论第四篇教程中已经见过的一些概念,MXNet 中图像分割:PSPNet 和 DeepLab-v3,在第五章,计算机视觉中的图像分析。
如何做…
在本教程中,我们将看到以下步骤:
-
重新审视MS COCO和Penn-Fudan Pedestrian数据集
-
使用Penn-Fudan Pedestrian从头开始训练DeepLab-v3模型
-
使用预训练的 DeepLab-v3 模型通过从MS COCO到Penn-Fudan Pedestrian的迁移学习来优化性能。
-
在Penn-Fudan Pedestrian上微调我们预训练的 DeepLab-v3 模型
让我们详细看一下接下来的步骤。
重新审视 MS COCO 和 Penn-Fudan Pedestrian 数据集
MS COCO和Penn-Fudan Pedestrian都是目标检测和语义分割数据集;然而,它们有很大的不同。MS COCO是一个大规模数据集,包含约 150,000 张图像,标记为 80 类(21 个主要类),并且在研究和学术界广泛应用于基准测试。Penn-Fudan Pedestrian是一个小规模数据集,包含 170 张图像和 423 名行人。在本教程中,我们将专注于语义分割任务。
MXNet GluonCV 不提供直接下载任何数据集的方法。然而,我们不需要MS COCO数据集(其大小约为 19 GB),只需要选择模型的预训练参数。
这里有一些MS COCO的例子:
图 7.22 – MS COCO 示例
对于Penn-Fudan Pedestrian,关于如何获取数据集的所有信息可以在第四篇教程中找到,MXNet 中图像分割:PSPNet 和 DeepLab-v3,在第五章,计算机视觉中的图像分析。参考该篇教程的代码,我们可以展示一些例子:
图 7.23 – Penn-Fudan Pedestrian 数据集示例
从图 7.22和图 7.23,我们可以看到一些MS COCO图像与Penn-Fudan Pedestrian的图像相似。
使用 Penn-Fudan Pedestrian 从头开始训练 DeepLab-v3 模型
正如在第四个示例中所述,使用 MXNet 对图像进行分割:PSPNet 和 DeepLab-v3,在 第五章,使用计算机视觉分析图像 中,我们将使用 softmax 交叉熵作为损失函数,并使用像素准确率和 平均交并比(mIoU)作为评估指标。
按照我们示例中的代码,我们得到了以下从头开始训练 DeepLab-v3 模型时的演变:
图 7.24 – DeepLab-v3 训练演变(训练损失和验证损失)– 从头开始训练
此外,对于最佳迭代,测试集中的像素准确率和 mIoU 值如下:
PixAcc: 0.8454046875
mIoU : 0.6548404063890942
即使训练了 40 个周期,评估结果仍未显示出强劲的性能(mIoU 值仅为 0.65)。
定性地,我们还可以通过一个示例图像来检查模型的表现。在我们的案例中,我们选择了以下图像:
图 7.25 – Penn-Fudan 行人数据集的图像示例,用于定性结果
我们可以通过以下代码片段将此图像传入模型来检查模型的输出:
# Compute and plot prediction
transformed_image = gcv.data.transforms.presets.segmentation.test_transform(test_image, ctx)
output = deeplab_ts(transformed_image)
filtered_output = mx.nd.argmax(output[0], 1)
masked_output = gcv.utils.viz.plot_mask(test_image, filtered_output)
axes = fig.add_subplot(1, 2, 2)
axes.set_title("Prediction", fontsize=16, y=-0.3)
axes.axis('off')
axes.imshow(masked_output);
上述代码片段展示了真实标签的分割结果和我们模型的预测结果:
图 7.26 – 从头开始训练的 DeepLab-v3 的真实标签与预测
从结果可以看出,行人只有在较晚阶段才开始被正确分割。为了改善结果,我们需要训练更多的周期和/或调整超参数。然而,一个更好、更快、更简单的方法是使用迁移学习并进行微调。
使用预训练的 DeepLab-v3 模型,通过从 MS COCO 到 Penn-Fudan 行人的迁移学习优化性能
在前一个示例中,我们使用自己的数据集从头开始训练了一个新模型。然而,这种方法有三个重要的缺点:
-
从头开始训练需要大量的数据。
-
由于数据集的庞大和模型需要的训练周期数,训练过程可能会耗时很长。
-
所需的计算资源可能会非常昂贵或难以获取。
因此,在本示例中,我们将采用不同的方法。我们将使用来自 MXNet GluonCV 模型库的预训练模型来解决任务。这些模型已经在 MS COCO 数据集上训练过,包含我们感兴趣的类别(在本案例中是 person);因此,我们可以使用这些学到的表示,并轻松地将它们迁移到 Penn-Fudan 行人(同一领域)。
对于 DeepLab-v3 模型,我们有以下内容:
# DeepLab-v3 from Model Zoo
deeplab_pt
gcv.model_zoo.get_model('deeplab_resnet101_coco'
pretrained=True, ctx=ctx)
正如我们在前面的代码片段中看到的,按照本章第一个食谱中讨论的内容,理解迁移学习和微调,对于 pretrained 参数,我们已将其值设置为 True,表示我们希望获取预训练的权重(而不仅仅是模型的架构)。
为了充分评估迁移学习带来的改进,我们将直接在我们的目标任务中评估预训练模型(任务源自MS COCO),然后再将迁移学习应用到Penn-Fudan Pedestrian,并对比应用前后的结果。因此,使用我们当前的预训练模型,我们获得了以下结果:
PixAcc: 0.9640322916666667
mIoU : 0.476540873665686
如我们所见,我们的预训练 Transformer 模型在相同领域中已经显示出良好的性能值。此外,使用预训练模型的巨大优势是节省时间,因为加载预训练模型只需要几行代码。
我们还可以通过相同的图像示例和代码检查模型的定性表现。输出结果如下:
图 7.27 – DeepLab-v3 预训练模型的真实值与预测值
从结果中可以看出,行人已被正确地分割。请注意,使用预训练模型的一个附加优点见于图 7.27:在真实值图像中,背景中的人没有被分割,但预训练模型正确地识别了它们(这可能解释了较低的 mIoU 值)。
现在我们有了一个比较基准,让我们将迁移学习应用于我们的任务。在第一个食谱中,理解迁移学习和微调,第一步是从 MXNet Model Zoo(GluonCV 或 GluonNLP)中获取预训练模型,而这一部分我们已经完成。
第二步是移除最后的几层(通常是分类器),并保持其余层的参数冻结(在训练过程中不可更新),所以让我们开始吧!
我们可以通过以下代码片段冻结DeepLab-v3的特征提取层:
for param in deeplab_tl.collect_params().values():
param.grad_req = 'null'
此外,我们还需要替换分割任务头。以前,它支持来自MS COCO的 21 类。对于我们的实验,两个类别就足够了,background 和 person。这可以通过以下代码片段完成:
# Replace the last layers
deeplab_tl.head = gcv.model_zoo.deeplabv3._DeepLabHead(2)
deeplab_tl.head.initialize(ctx=ctx)
deeplab_tl.head.collect_params().setattr('lr_mult', 10)
现在,我们可以使用Penn-Fudan Pedestrian应用常规的训练过程,并且我们使用DeepLab-v3模型时,训练演化如下:
图 7.28 – DeepLab-v3 训练演化(训练损失和验证损失)– 迁移学习
此外,对于最佳迭代,测试集中的评估指标如下所示:
PixAcc: 0.9503427083333333
mIoU : 0.8799470898171042
与我们之前从头开始训练和预训练的实验相比,这个实验的表现略好,而且我们花费了几分钟就让这个模型开始在我们预定的任务上工作,而从头开始训练的实验则花费了数小时,并且需要多次尝试调整超参数,最终耗费了几天的时间。
我们还可以通过相同的图像示例和代码检查模型的定性表现。输出结果如下:
图 7.29 – 经过转移学习的 DeepLab-v3 预训练模型的真实标签与预测结果
从结果可以看出,行人已经被正确地分割。
在 Penn-Fudan 行人数据集上对我们预训练的 DeepLab-v3 模型进行微调
在之前的配方中,我们冻结了编码器层中的参数。然而,在我们当前使用的数据集(Penn-Fudan 行人数据集)中,我们可以解冻这些参数并训练模型,从而有效地使新的训练过程更新表示(在转移学习中,我们直接使用了为 MS COCO 学习到的表示)。如本章所介绍的,这个过程被称为微调。
微调有两种变体:
-
通过冻结层并随后解冻层来应用转移学习。
-
直接应用微调,而不进行冻结层的预备步骤。
让我们计算两个实验并通过比较结果得出结论。
对于第一个实验,我们可以使用之前配方中获得的网络,解冻层并重新开始训练。在 MXNet 中,要解冻编码器参数,可以运行以下代码片段:
for param in deeplab_ft.collect_params().values():
param.grad_req = 'write'
现在,我们可以应用常规的训练过程来训练 Penn-Fudan 行人数据集,并且在使用 DeepLab-v3 模型时,我们有以下训练演变:
图 7.30 – DeepLab-v3 训练过程演变(训练损失和验证损失)– 转移学习后的微调
此外,对于最佳迭代,在测试集上获得的评估指标如下:
PixAcc: 0.9637550347222222
mIoU : 0.9091450223893902
与我们之前在转移学习中的实验相比,这个实验在 mIoU 上的表现提高了约 3%,考虑到投入的训练时间较少,这是一个非常好的提升。
我们还可以通过相同的图像示例和代码检查模型的定性表现。输出结果如下:
图 7.31 – 经过微调后的 DeepLab-v3 预训练模型的真实标签与预测结果
从结果可以看出,行人已经被正确地分割。
现在让我们继续进行第二次微调实验,在此实验中,我们不应用迁移学习(没有冻结层),而是直接对整个模型应用微调。
我们需要检索用于MS COCO的预训练DeepLab-v3模型,以下是 MXNet GluonCV 的代码片段:
# DeepLab-v3 from Model Zoo
deeplab_ft_direct = gcv.model_zoo.get_model("deeplab_resnet101_coco", pretrained=True, ctx=ctx)
现在,我们可以在不冻结的情况下应用训练过程,这将更新我们DeepLab-v3模型的所有层:
图 7.32 – DeepLab-v3 训练演变(训练损失和验证损失) – 未冻结的微调
此外,对于最佳迭代,在测试集上获得的评估指标如下:
PixAcc: 0.9639182291666667
mIoU : 0.9095065032946663
与我们之前的微调实验相比,我们可以看到这些实验表现出非常相似的性能。根据经验,已经证明这个微调实验可能会略微降低结果,因为最初冻结编码器使解码器能够使用编码器表示学习当前任务。从某种角度看,在这一步,知识从编码器传递到解码器。在第二步中,当编码器被解冻时,解码器中学习到的参数执行辅助迁移学习,这次是从解码器到编码器。
我们还可以通过相同的图像示例和代码来检查我们的模型在定性上的表现。输出如下:
图 7.33 – DeepLab-v3 预训练模型的真实标签与预测(未冻结的微调)
从结果可以看出,行人已经被正确分割,尽管如前所述,如果我们看右侧的那个人,靠近左侧那个人的手臂可能会被更好地分割。如前所讨论,有时这种微调版本的结果可能会略低于其他方法。
它是如何工作的……
在这个方案中,我们将本章开头介绍的迁移学习和微调技术应用于图像分类任务,这在之前的第四个方案中也有呈现,使用 MXNet 进行图像对象分割:PSPNet 和 DeepLab-v3,位于第五章,使用计算机视觉分析图像。
我们重新访问了两个已知的数据集,MS COCO和Penn-Fudan Pedestrian,我们打算通过基于前者数据集的知识迁移,并利用后者对该知识进行优化来将它们结合起来。此外,MXNet GluonCV 提供了以下内容:
-
用于MS COCO的预训练DeepLab-v3模型
-
用于便捷访问Penn-Fudan Pedestrian的工具
此外,我们继续使用为语义分割引入的损失函数和指标,如 softmax 交叉熵、像素准确率和 mIoU。
在 MXNet 和 GluonCV 中,所有这些工具都已经预先准备好,这使得我们只需几行代码就能进行以下实验:
-
使用Penn-Fudan Pedestrian从零开始训练模型
-
使用预训练模型通过迁移学习从MS COCO到Penn-Fudan Pedestrian优化性能
-
在Penn-Fudan Pedestrian上微调我们的预训练模型(包括冻结和不冻结层的情况)
经过不同实验的运行,并结合定性结果和定量结果,迁移学习(像素准确率为 0.95,mIoU 为 0.88)已经成为我们任务中最佳的实验方法。实际运行这些实验时获得的结果可能会因为模型架构、数据集和所选择的超参数而有所不同,因此我们鼓励你尝试不同的技术和变化。
还有更多……
迁移学习,包括微调,是一个活跃的研究领域。2022 年发布的一篇论文探讨了图像分类的最新进展。该论文题为Deep Transfer Learning for Image Classification: A survey,可以在这里找到:www.researchgate.net/publication/360782436_Deep_transfer_learning_for_image_classification_a_survey。
一篇有趣的论文将迁移学习和语义分割结合起来,题为Semantic Segmentation with Transfer Learning for Off-Road Autonomous Driving,在这篇论文中,也通过使用合成数据研究了领域的变化。可以在这里找到:www.researchgate.net/publication/333647772_Semantic_Segmentation_with_Transfer_Learning_for_Off-Road_Autonomous_Driving。
这篇论文提供了更一般的概述:Learning Transferable Knowledge for Semantic Segmentation with Deep Convolutional Neural Network,该论文被计算机视觉与模式识别(CVPR)大会于 2016 年接收。可以在这里找到:openaccess.thecvf.com/content_cvpr_2016/papers/Hong_Learning_Transferrable_Knowledge_CVPR_2016_paper.pdf。
改进英德翻译的性能
在之前的示例中,我们已经看到如何利用预训练模型和新数据集进行迁移学习和微调,应用于计算机视觉任务。在本示例中,我们将采用类似的方法,但针对一个自然语言处理任务,即从英语翻译成德语。
在第四个食谱中,从越南语翻译到英语,来自第六章,理解文本与自然语言处理,我们看到如何使用 GluonNLP 直接检索预训练的模型,并将它们用于翻译任务,从头开始训练,实际上只是通过使用预训练模型的架构来有效地利用过去的知识。
在本食谱中,我们还将利用模型的权重/参数,这些权重/参数是通过机器翻译模型用于将英文文本翻译成德文的任务获得的。我们将使用WMT2014数据集(任务来源)进行预训练,并将进行多个实验,使用WMT2016数据集(增加了约 20%的德英对词汇和句子)评估我们在新目标任务中的模型。
准备工作
和之前的章节一样,在本食谱中,我们将使用一些矩阵运算和线性代数,但这并不难。
此外,我们将处理文本数据集;因此,我们将重新审视在第四个食谱中已经看到的一些概念,理解文本数据集——加载、管理和可视化 Enron 邮件数据集,来自第二章,使用 MXNet 和可视化数据集:Gluon 和 DataLoader。
如何操作...
在本食谱中,我们将查看以下步骤:
-
介绍WMT2014和WMT2016数据集
-
从头开始训练一个 Transformer 模型,使用WMT2016数据集
-
使用预训练的 Transformer 模型,通过迁移学习将性能从WMT2014优化到WMT2016
-
在WMT2016上微调我们预训练的 Transformer 模型
接下来我们将详细查看这些步骤。
介绍 WMT2014 和 WMT2016 数据集
WMT2014和WMT2016是多模态(多语言)翻译数据集,包括中文、英语和德语语料库。WMT2014首次在 2014 年《第九届统计机器翻译研讨会论文集》上介绍,作为翻译模型评估活动的一部分。该研讨会在 2016 年升级为自己的会议,并在《第一次机器翻译会议论文集》中介绍了WMT2016,作为翻译模型评估活动的一部分。这两个数据集非常相似,都是从新闻来源中提取信息,最大的区别在于语料库(两个数据集所用词汇量的大小)。WMT2014 大约包含14 万个不同的词,而 WMT2016 略大,包含15 万个词,特别是在德英对中,增加了约 20%的词汇和句子。
MXNet GluonNLP 提供了这些数据集的现成版本。在我们的案例中,我们将使用WMT2016,它仅包含train和test划分。我们将进一步拆分测试集,获得validation和test划分。以下是加载数据集的代码:
# WMT2016 Dataset (Train, Validation and Test)
# Dataset Parameters
src_lang, tgt_lang = "en", "de"
src_max_len, tgt_max_len = 50, 50
wmt2016_train_data = nlp.data.WMT2016BPE(
'train',
src_lang=src_lang,
tgt_lang=tgt_lang)
wmt2016_val_data = nlp.data.WMT2016BPE(
'newstest2016',
src_lang=src_lang,
tgt_lang=tgt_lang)
wmt2016_test_data = nlp.data.WMT2016BPE(
'newstest2016',
src_lang=src_lang,
tgt_lang=tgt_lang)
以下是生成validation和test划分的代码:
# Split Val / Test sets
val_length = 1500
test_length = len(wmt2016_test_text) - val_length
wmt2016_val_data._data[0] = wmt2016_val_data._data[0][:val_length]
wmt2016_val_data._data[1] = wmt2016_val_data._data[1][:val_length]
wmt2016_val_data._length = val_length
wmt2016_val_text._data[0] = wmt2016_val_text._data[0][:val_length]
wmt2016_val_text._data[1] = wmt2016_val_text._data[1][:val_length]
wmt2016_val_text._length = val_length
wmt2016_test_data._data[0] = wmt2016_test_data._data[0][-test_length:]
wmt2016_test_data._data[1] = wmt2016_test_data._data[1][-test_length:]
wmt2016_test_data._length = test_length
在分割后,我们的WMT2016数据集提供以下数据:
Length of train set: 4500966
Length of val set : 1500
Length of test set : 1499
从每个数据集上大量的实例中,我们可以确认这些数据集适合用于我们的实验。
在 WMT2016 中从零开始训练一个 Transformer 模型
如第四章所述,从越南语翻译到英语,在第六章,使用自然语言处理理解文本中,我们将使用困惑度进行每批次计算,并使用BLEU进行每轮计算,这些将展示我们的训练过程演变,作为典型的训练和验证损失的一部分。我们还将它们用于定量评估,对于定性评估,我们将选择一个句子(也可以使用任何你想出的其他句子)。
我们在使用 Transformer 模型进行训练时有以下演变:
图 7.34 – Transformer 训练演变(训练损失)– 从零开始训练
此外,对于最佳迭代,测试集中的损失、困惑度和 BLEU 得分(乘以 100)如下:
WMT16 test loss: 3.01; test bleu score: 14.50
当前最先进的技术(SOTA)模型的 BLEU 得分可以超过 30 分;我们在 10 个 epoch 后大约达到一半,在大约 30 个 epoch 后达到 SOTA 性能。
从质量上讲,我们也可以通过一个句子示例检查我们的模型表现如何。在我们的案例中,我们选择了:"I learn new things every day",可以通过以下代码进行验证:
print("Qualitative Evaluation: Translating from English to German")
# From Google Translate
expected_tgt_seq = " Ich lerne jeden Tag neue Dinge."
print("Expected translation:")
print(expected_tgt_seq)
src_seq = "I learn new things every day."
print("In English:")
print(src_seq)
translation_out = nmt.utils.translate(
transformer_ts_translator,
src_seq,
wmt_src_vocab,
wmt_tgt_vocab,
ctx)
print("The German translation is:")
print(" ".join(translation_out[0]))
这些代码语句将给我们以下输出:
Qualitative Evaluation: Translating from English to German
Expected translation:
Ich lerne jeden Tag neue Dinge.
In English:
I learn new things every day.
The German translation is:
Ich halte es für so , dass es hier so ist.
这句德语意味着我认为这里是这种情况;因此,从这个结果可以看出,文本没有正确地从英语翻译成德语,我们需要投入更多的时间来训练,才能获得正确的结果。
使用预训练的 Transformer 模型,通过将 WMT2014 转移到 WMT2016 进行优化,从而提升性能。
在前一章中,我们使用自己的数据集从零开始训练了一个新模型。然而,这有两个重要的缺点:
-
从零开始训练需要大量的数据。
-
由于数据集的庞大规模和模型需要的训练轮数,训练过程可能会非常漫长。
因此,在本章中,我们将采用不同的方法。我们将使用 MXNet GluonNLP 中的预训练模型来解决该任务。这些模型已经在与WMT2014非常相似的数据集上进行训练,因此为该任务学习到的表示可以很容易地迁移到WMT2016(同一领域)。
对于一个 Transformer 模型,我们有如下内容:
wmt_model_name = 'transformer_en_de_512'
wmt_transformer_model_pt, wmt_src_vocab, wmt_tgt_vocab = nlp.model.get_model(
wmt_model_name,
dataset_name='WMT2014',
pretrained=True,
ctx=ctx)
print('Source Vocab:', len(wmt_src_vocab), ', Target Vocab:', len(wmt_tgt_vocab))
输出展示了WMT2014数据集的词汇表大小(预训练的英德翻译任务):
Source Vocab: 36794 , Target Vocab: 36794
这是WMT2014数据集的一个子集。正如我们在前面的代码片段中所看到的,按照本章第一个配方理解迁移学习和微调中的讨论,对于pretrained参数,我们已将其值设置为True,表示我们希望检索预训练的权重(而不仅仅是模型的架构)。
为了充分评估迁移学习带来的改进,我们将直接评估我们的预训练模型(任务源是WMT2014)在应用迁移学习到WMT2016之前和之后的表现。因此,直接使用我们的预训练模型,我们得到以下结果:
WMT16 test loss: 1.59; test bleu score: 29.76
如我们所见,我们的预训练 Transformer 模型已经显示出非常好的性能,因为它属于相同的领域;然而,单纯使用预训练模型并不能达到 SOTA(最先进的)性能,这只能通过从零开始训练来实现。使用预训练模型的巨大优势在于节省时间和计算资源,因为加载一个预训练模型只需要几行代码。
我们还可以通过相同的句子示例和代码检查模型的定性表现。输出结果如下:
Qualitative Evaluation: Translating from English to German
Expected translation:
Ich lerne jeden Tag neue Dinge.
In English:
I learn new things every day.
The German translation is:
Ich lerne neue Dinge, die in jedem Fall auftreten.
这句德语的意思是我学习在每个案例中出现的新事物;因此,从结果中可以看出,文本尚未正确地从英语翻译成德语,但这一次,比我们之前的实验更接近了。
现在我们有了一个比较的基准,让我们将迁移学习应用到我们的任务中。在第一个配方中,理解迁移学习和微调,第一步是从 MXNet 模型库(GluonCV 或 GluonNLP)中检索一个预训练的模型,这一步我们已经完成了。
第二步是去掉最后一层(通常是分类器),保持其余层的参数冻结(在训练过程中不可更新),让我们来做吧!
我们可以用以下代码冻结除分类器之外的所有参数,保持这些参数被冻结(我们将在后续实验中解冻它们):
updated_params = []
for param
wmt_transformer_model_tl.collect_params().values():
if param.grad_req == "write":
param.grad_req = "null"
updated_params += [param.name]
现在,我们可以使用WMT2016应用常规的训练过程,接着我们可以看到在使用 Transformer 模型训练时的演变:
图 7.35 – Transformer 训练演变(训练损失)– 迁移学习
此外,对于最佳迭代,测试集中的损失、困惑度和 BLEU 得分(乘以 100)如下:
WMT16 test loss: 1.20; test bleu score: 27.78
与我们之前的实验相比,这次实验的数值表现略低;然而,我们花了几分钟就让这个模型开始在我们预定的任务中发挥作用,而之前的实验从头开始训练则花了几个小时,并且需要多次尝试调整超参数,总共花费了几天的时间。
我们还可以通过相同的句子示例和代码检查模型的定性表现。输出如下:
Qualitative Evaluation: Translating from English to German
Expected translation:
Ich lerne jeden Tag neue Dinge.
In English:
I learn new things every day.
The German translation is:
Ich erlerne jedes Mal neue Dinge
这句德语句子意味着我每次都学到新东西;因此,从结果可以看出,文本几乎已正确地从英语翻译成德语,相较于我们之前的实验(预训练模型),有所改进,尽管(更好的)定量结果显示了不同的趋势。
在 WMT2016 上微调我们预训练的 Transformer 模型
在之前的方案中,我们冻结了所有参数,除了分类器。然而,由于我们目前使用的数据集(WMT2016)有足够的数据样本,我们可以解冻这些参数并训练模型,有效地让新的训练过程更新表示(通过迁移学习,我们直接使用了为 WMT2014 学到的表示)。这个过程,正如我们所知,叫做微调。
微调有两种变体:
-
通过冻结层并在之后解冻它们来应用迁移学习。
-
直接应用微调,而不需要冻结层的预备步骤。
让我们计算两个实验,并通过比较结果得出结论。
对于第一个实验,我们可以获取在之前方案中获得的网络,解冻层并重新开始训练。在 MXNet 中,要解冻编码器参数,我们可以运行以下代码片段:
for param in wmt_transformer_model_ft.collect_params().values():
if param.name in updated_params:
param.grad_req = 'write'
现在,我们可以应用通常的训练过程,使用 WMT2016,我们得到了 Transformer 模型训练中的以下演变:
图 7.36 – Transformer 训练演变(训练损失)– 迁移学习后微调
此外,对于最佳迭代,测试集中的损失、困惑度和 BLEU 分数(乘以 100)如下:
WMT16 test loss: 1.23; test bleu score: 26.05
与我们之前的迁移学习实验相比,本次实验的定量表现略差。
从定性上讲,我们还可以通过一个句子示例来检查我们的模型表现如何。在我们的例子中,我们选择了 "I learn new things every day",得到的输出如下:
Qualitative Evaluation: Translating from English to German
Expected translation:
Ich lerne jeden Tag neue Dinge.
In English:
I learn new things every day.
The German translation is:
Ich lerne jedes Mal Neues.
这句德语句子意味着我每次都学到新东西;因此,从结果可以看出,文本几乎已正确地从英语翻译成德语。
现在让我们继续进行第二个微调实验,在这个实验中,我们不应用迁移学习(没有冻结层),而是直接对整个模型应用微调。
我们需要重新获取预训练的 Transformer 模型,使用以下 MXNet GluonNLP 代码片段:
wmt_model_name = 'transformer_en_de_512'
wmt_transformer_model_ft_direct, _, _ = nlp.model.get_model(
wmt_model_name,
dataset_name='WMT2014',
pretrained=True,
ctx=ctx)
现在,在不冻结的情况下,我们可以应用训练过程,这将更新我们 Transformer 模型的所有层:
图 7.37 – Transformer 训练演化(训练损失)– 不冻结层进行微调
此外,对于最佳迭代,测试集上获得的损失、困惑度和 BLEU 分数(乘以 100)如下:
WMT16 test loss: 1.22; test bleu score: 26.75
与我们之前的微调实验相比,我们可以看到这个实验的性能略有提升。然而,实际操作中,我们原本预计会得到相反的结果(即该实验性能会略有下降)。这已被证明是一个可重复的结果,因为最初冻结编码器可以使解码器学习(使用编码器的表示)当前的任务。从某种角度来看,在这一步,知识从编码器转移到了解码器。在随后的步骤中,当编码器解冻时,解码器学习到的参数执行辅助的迁移学习——这次是从解码器到编码器。
从定性角度来看,我们还可以通过一个句子示例来检查模型的表现。在我们的例子中,我们选择了 "I learn new things every day",得到的输出结果如下:
Qualitative Evaluation: Translating from English to German
Expected translation:
Ich lerne jeden Tag neue Dinge.
In English:
I learn new things every day.
The German translation is:
Ich lerne jedes Mal neue Dinge
这句德语的意思是 I learn new things every time;因此,从结果可以看出,文本几乎正确地从英语翻译成了德语。
它是如何工作的…
在这篇教程中,我们将第一个章节开头介绍的迁移学习和微调技术应用于机器翻译任务,该任务也在前一篇教程 从越南语翻译到英语 中呈现过,详见 第六章,通过自然语言处理理解文本。
我们探索了两个新的数据集,WMT2014 和 WMT2016,这些数据集除了支持其他语言对的翻译外,还支持德语和英语之间的翻译。此外,MXNet GluonNLP 提供了以下内容:
-
针对 WMT2014 的预训练 Transformer 模型
-
一个准备好与 WMT2016 一起使用的数据加载器
此外,我们继续使用了为机器翻译引入的评估指标:困惑度(perplexity)和 BLEU。
MXNet 和 GluonNLP 提供的所有这些工具使我们能够通过几行代码轻松运行以下实验:
-
从头开始训练 WMT2016 模型
-
使用预训练模型通过迁移学习优化性能,从 WMT2014 到 WMT2016
-
在 WMT2016 上对预训练模型进行微调(有层冻结和没有冻结层的情况)
我们比较了结果,并得出了最佳方法,即先应用迁移学习,之后进行微调。
还有更多…
在这篇教程中,我们介绍了两个新的数据集,WMT2014 和 WMT2016。这些数据集是在 统计机器翻译研讨会(WMT)会议上作为挑战引入的。2014 年和 2016 年的结果如下:
-
2014 年统计机器翻译研讨会的发现: aclanthology.org/W14-3302.pd…
-
2016 年机器翻译会议(WMT16)研究成果:
aclanthology.org/W16-2301.pdf
机器翻译中的迁移学习,包括微调,是一个活跃的研究领域。一篇发表于 2020 年的论文探讨了其应用,题为神经机器翻译中的迁移学习转移了什么?,可以在这里找到:aclanthology.org/2020.acl-main.688.pdf。
针对更一般的自然语言处理应用,最近有一篇论文发布,题为自然语言处理中的迁移学习综述,可以在这里找到:www.researchgate.net/publication/342801560_A_Survey_on_Transfer_Learning_in_Natural_Language_Processing。
第八章:使用 MXNet 改进训练性能
在之前的章节中,我们利用 MXNet 的功能解决了计算机视觉和 GluonCV、GluonNLP 的问题。我们通过不同的方法训练了这些模型:从头开始、迁移学习和微调。在本章中,我们将重点关注如何提高训练过程的性能,并加速我们如何获得这些结果。
为了实现优化训练循环性能的目标,MXNet 提供了多种功能。我们已经简要使用了一些这些功能,例如 延迟计算 的概念,该概念在第一章中介绍过。我们将在本章中再次探讨这一点,并结合自动并行化来使用。此外,我们还将优化如何高效访问数据,利用 Gluon DataLoaders 在不同的环境(CPU、GPU)中执行数据转换。
此外,我们将探索如何结合多个 GPU 加速训练,利用诸如数据并行化等技术来获得最佳性能。我们还将探讨如何使用不同的数据类型与 MXNet 配合,以动态优化不同的数据格式。
最后,利用书中已探讨的问题,我们将通过示例应用所有这些技术。对于我们的计算机视觉任务,我们将选择图像分割,而对于 NLP 任务,我们将选择翻译英语到德语的文本。
具体来说,本章的结构包含以下食谱:
-
介绍训练优化功能
-
为图像分割优化训练
-
为英语到德语的文本翻译优化训练
技术要求
除了前言中指定的技术要求外,以下技术要求适用:
-
确保你已经完成了安装 MXNet、Gluon、GluonCV 和 GluonNLP的食谱。
-
确保你已经完成了第五章和第六章。
-
确保你已经完成了第七章。
本章的代码可以在以下 GitHub URL 找到:github.com/PacktPublishing/Deep-Learning-with-MXNet-Cookbook/tree/main/ch08。
此外,你可以直接从 Google Colab 访问每个食谱。例如,本章第一个食谱的代码可以在此找到:colab.research.google.com/github/PacktPublishing/Deep-Learning-with-MXNet-Cookbook/blob/main/ch08/8_1_Introducing_training_optimization_features.ipynb。
介绍训练优化功能
在前几章中,我们展示了如何利用MXNet、GluonCV和GluonNLP来检索特定数据集(如ImageNet、MS COCO或IWSLT2015)中的预训练模型,并将其用于我们的特定任务和数据集。此外,我们还使用了迁移学习和微调技术来提高这些任务/数据集上的性能。
在本教程中,我们将介绍(并重温)几个概念和特性,这些将优化我们的训练循环,之后我们将分析其中的权衡。
准备工作
类似于前几章,在本教程中,我们将使用一些矩阵操作和线性代数,但这不会很困难,因为你会发现许多示例和代码片段来帮助你学习。
如何操作...
在本教程中,我们将通过以下步骤进行操作:
-
使用懒评估和自动并行化
-
优化 DataLoader:GPU 预处理和 CPU 线程
-
使用
Float32、Float16和自动混合精度进行训练 -
使用多个 GPU 和数据并行化进行训练
让我们深入了解这些步骤。
使用懒评估和自动并行化
在第一章的NumPy 和 MXNet NDArrays教程中,我们介绍了懒评估,MXNet 在计算操作时采用的策略。这种策略对于大计算负载来说是最优的,因为实际的计算会被延迟,直到这些值真正需要时才会计算。
此外,MXNet 通过推迟操作计算,直到它们真正需要时,能够并行化一些计算,这意味着涉及的数据不会按顺序处理。这一过程是自动完成的,对于在多个硬件资源(如 CPU 和 GPU)之间共享数据时非常有用。
作为一个示例,我们可以进行一些矩阵乘法实验。我们的第一个实验将生成四个矩阵,然后进行它们之间的乘法组合。在每次计算后,我们将强制计算完成(通过添加wait_to_read()函数调用)。我们将计算两种配置下的结果。初始配置将强制 MXNet 使用一个线程(NaiveEngine)。在这种配置下,计算花费的时间是:
Time (s): 134.3672107196594
第二种配置是测试 MXNet 的常规默认配置(ThreadedEnginePerDevice,四个 CPU 线程)。在这种配置下,计算花费的时间是:
Time (s): 135.26983547210693
如我们所见,强制每次计算在进入下一步之前完成(通过调用wait_to_read())在多线程配置中是适得其反的。
我们的第二次实验将非常相似;但是,这一次,我们将删除所有对wait_to_read()函数的调用。我们将只确保在计算时间之前,所有矩阵乘法的计算都已完成。对于初始配置(NaiveEngine),计算所需的时间如下:
Time (s): 134.47382940625321
正如预期的那样,这个时长与仅使用一个线程时非常相似,因为所有计算都是按顺序进行的。
使用我们的第二种配置(ThreadedEnginePerDevice,具有四个 CPU 线程),第二次实验的计算时间如下:
Time (s): 111.36750531196594
结果表明,使用多个线程(MXNet 的默认自动配置)时,我们获得了约 20%的提升(在更适合多线程的工作负载下,提升可能更高)。
重要提示
请注意,在代码中,我们使用了mx.nd.waitall()函数,以确保所有计算在计算操作所花费的时间之前都已严格完成。
优化 DataLoader——GPU 预处理与 CPU 线程
在第二章的理解图像数据集——加载、管理和可视化时尚 MNIST 数据集一节中,我们介绍了Gluon DataLoader,这是一种高效的机制,用于生成批次大小,供我们的模型用于训练和评估。
DataLoader 在我们的数据预处理过程中扮演着两个重要角色。首先,正如我们在前面的章节中探讨过的,我们的模型是为并行数据处理进行了优化,这意味着我们可以在同一时间处理多个样本(例如,图像分割任务中的图像),这些样本会在同一个批次中并行处理,且由GPU进行处理。这个参数称为批次大小。另一方面,样本通常需要进行预处理,以最大化模型的性能(例如,图像被调整大小,并将其值从[0, 255]映射到[0, 1])。这些操作耗时,优化这些操作可以节省大量时间和计算资源。
让我们分析一下将数据预处理放在 GPU 上与使用 CPU 的常规默认行为之间的效果。作为基准,我们计算仅使用 CPU 加载数据集所需的时间。我们选择分割数据集的验证集,结果如下:
Time (s): 24.319150686264038
然而,在加载数据集时,我们通常会应用某些transform操作,以最大化网络性能。常见的转换操作包括图像缩放、裁剪、转化为张量以及归一化,可以通过以下代码在 MXNet 中定义:
input_transform_fn = mx.gluon.data.vision.transforms.Compose([
mx.gluon.data.vision.transforms.Resize(image_size, keep_ratio=True),
mx.gluon.data.vision.transforms.CenterCrop(image_size), mx.gluon.data.vision.transforms.ToTensor(),
mx.gluon.data.vision.transforms.Normalize([.485, .456, .406], [.229, .224, .225])
])
在仅使用 CPU 处理分割数据集的验证分割时,应用这些转换操作后的处理时间如下:
Time (s): 38.973774433135986
正如我们所看到的,处理时间增加了超过 50%,从大约 24 秒增加到大约 39 秒。然而,当我们利用 GPU 进行数据预处理时,处理时间如下:
Time (s): 25.39602303504944
正如我们所看到的,基于 GPU 的预处理操作几乎没有额外开销(<5%)。
此外,在 GPU 上执行预处理还有另一个优势:数据可以保存在 GPU 中供我们的模型处理,而使用 CPU 进行预处理时,我们需要将数据复制到 GPU 内存中,这可能会占用大量时间。如果我们实际测量端到端的预处理流水线,将数据预处理与复制操作到 GPU 内存结合起来,得到的结果如下:仅使用 CPU 时,端到端处理时间如下:
Time (s): 67.73443150520325
正如我们所看到的,复制时间非常长,整个流水线需要超过 1 分钟。然而,使用 GPU 时的结果如下:
Time (s): 23.22727918624878
这表明完整预处理所需的时间有了显著改善(<40%)。总的来说,这是由于两个因素:首先,预处理操作在 GPU 上更快;其次,数据需要在过程结束时复制到 GPU,这样我们的模型(也存储在 GPU 中)才能高效地处理数据。
这种方法最主要的缺点是需要将整个数据集保存在 GPU 中。通常,GPU 内存空间是为你在训练或推理中使用的每个批次进行优化的,而不是为整个数据集进行优化。这就是为什么这种方法通常会以将处理后的数据从 GPU 内存空间复制回 CPU 内存空间的方式结束。
然而,有些情况下,将数据保存在 GPU 内存空间可能是正确的做法——例如,当你在实验不同的数据集时,可能会加载多个数据集并测试不同的预处理流水线。在这种情况下,你希望实验能快速完成,因此速度是需要优化的变量。此外,有时你并不是在处理数据集的完整训练/验证/测试集,而只是其中的一部分(例如,为了实验)。在这种情况下,优化速度也是合理的。
对于其他更面向生产的环境,正确的方法是在 GPU 内存空间中进行预处理,但将数据(回复制)保留在 CPU 内存空间。在这种情况下,结果略有不同:
Time (s): 34.58254957199097
正如我们所看到的,即使考虑到必要的数据移动(从 CPU 到 GPU,再从 GPU 回到 CPU),在 GPU 中进行预处理仍然能显著提高性能(约 50%)。
现在,我们将深入探讨如何利用 Gluon DataLoader 作为输入的两个重要参数:工作线程数和批量大小。工作线程数是 DataLoader 将并行启动的线程数量(多线程)用于数据预处理。批量大小,如前所述,是将并行处理的样本数量。
这些参数与 CPU 的核心数量直接相关,并且可以通过优化使用可用的硬件来实现最大性能。为了了解 CPU 的核心数,Python 提供了一个非常简单的 API:
import multiprocessing
multiprocessing.cpu_count()
在所选环境中,显示的可用核心数如下:
4
通过结合使用 CPU 和 GPU,我们可以计算最佳性能,考虑不同的工作线程数和批量大小值。为所选环境计算的结果如下:
图 8.1 – 不同计算模式下(CPU/GPU 和工作线程数)运行时间与批量大小的关系
从图 8.1中,我们可以得出以下三个重要结论:
-
GPU 预处理管道(数据处理加内存存储)要快得多(+50% 的运行时提升),即使是将数据复制回 CPU 时也是如此。
-
当结合使用 GPU 和 CPU 时,由于在这个环境下我们只使用一个 GPU,因此当将数据复制回 CPU 时会遇到瓶颈,因为数据是逐个样本复制的(而不是按批次)。
-
如果仅使用 CPU,增加工作线程可以改善处理时间。然而,限制因素是线程的数量。添加的工作线程数超过线程数(在我们的例子中为四个)将不会提高性能。增加批量大小能够提升性能,直到达到某一数量(在我们的例子中为 8),超过该数量后,性能不会进一步提高。
重要提示
使用 GPU 时,MXNet Gluon DataLoader 仅支持工作线程数为0(零)的值。
使用 Float32、Float16 和自动混合精度进行训练
在之前的示例中,我们已经看到了如何通过不同的方式优化训练循环,以最大限度地提高给定模型的 CPU 和 GPU 性能。在本示例中,我们将探讨如何计算我们的数据输入、模型参数及其周围的各种算术运算,并了解如何优化它们。
首先,让我们了解计算是如何进行的。数据输入和模型参数的默认数据类型是Float32,可以通过(参见示例代码)验证这一点,产生以下输出:
Input data type: <class 'numpy.float32'> Model Parameters data type: <class 'numpy.float32'>
该输出结果如预期所示,表明我们的数据输入和模型参数的数据类型是Float32(单精度)。但这意味着什么呢?
Float32 表示两件事:一方面,它是一种支持使用浮动小数点表示十进制数的数据类型;另一方面,它使用 32 位来存储单个数字。此格式的最重要特性如下:
-
能够表示大数值,从 10^-45 到 10^+38
-
可变精度
使用 Float32 作为数据类型有很多优点,主要与其可变精度有关。然而,训练过程是一个迭代的优化过程,其中许多计算并不需要 Float32 数据类型的精度。如果能以受控的方式牺牲一些精度来加速训练过程,那是可以接受的。我们可以通过 Float16 数据类型(半精度)实现这种平衡的权衡。与 Float32 类似,Float16 的最重要特性如下:
-
能够表示大数值,从 2^-24 到 2^+16
-
可变精度
作为精度丧失的示例,我们可以通过以下代码片段显示 1/3 的近似值,以两种格式呈现:
a = mx.nd.array([1/3], dtype=np.float32)
b = a.astype(np.float16)
print("1/3 as Float32: {0:.30f}".format(a.asscalar()))
print("1/3 as Float16: {0:.30f}".format(b.asscalar()))
结果如下:
1/3 as Float32: 0.333333343267440795898437500000
1/3 as Float16: 0.333251953125000000000000000000
如我们所见,所有表示都不是精确的,Float32 如预期一样提供了更高的精度,而 Float16 的精度更有限,但对于某些应用场景(如模型训练)可能足够,稍后我们会证明这一点。
如前所述,这种精度丧失是一种权衡,我们在训练循环中获得了巨大的速度提升。为了在训练循环中启用 Float16(半精度),我们需要对代码进行一些更改。首先,我们需要将模型参数更新为 Float16,这一操作只需一行简单的代码:
deeplab_ft_direct_f16.cast('float16')
之后,当我们的模型处理数据和真实标签时,这些也需要更新为 Float16,因此在我们的训练循环中,我们加入了以下几行:
data = data.astype('float16', copy=False)
label = label.astype('float16', copy=False)
通过这些更改,我们现在可以运行一个实验,比较两种训练循环的性能。例如,我们将微调一个 DeepLabv3 预训练模型,进行图像分割任务(参见 第七章 中的 改善图像分割性能 配方)。对于 Float32,我们得到以下结果:
Training time for 10 epochs: 594.4833037853241 / Best validation loss: 0.6800425
对于 Float16,我们获得了以下结果:
Training time for 10 epochs: 199.80901980400085 / Best validation loss: nan
不幸的是,对于 Float16,尽管我们的训练时间比 Float32 的训练循环少了约三分之一,但它并未收敛。这是由于几个原因:
-
对大数值的支持有限,因为任何大于
65519的整数都表示为无穷大 -
对小数值的支持有限,因为任何小于
1e-7的正十进制数都表示为0(零)
幸运的是,MXNet 提供了一个解决方案,能够自动结合两者的优点:
-
在必要的地方应用
Float32(单精度) -
在没有使用的地方应用
Float16(半精度),以优化运行时
这种方法被称为自动混合精度(AMP),为了启用它,我们只需要在代码中进行一些更改。首先,在创建模型之前,我们需要初始化库:
amp.init()
然后,在初始化训练器/优化器之后,我们需要将其与 AMP 链接:
amp.init_trainer(trainer)
最后,为了防止下溢或溢出,我们需要启用Float16数据类型。这在训练循环中非常方便地实现:
with amp.scale_loss(loss, trainer) as scaled_loss:
mx.autograd.backward(scaled_loss)
当我们应用这些变化并为Float16(现在启用了 AMP)重复之前的实验时,得到了以下结果:
Training time for 10 epochs: 217.64903020858765 / Best validation loss: 0.7082735
如我们所见,我们在更短的时间内获得了非常相似的验证损失结果(约 33%)。
由于我们的训练循环的内存占用大约是之前的一半,我们通常可以将模型的大小翻倍(更多的层和更大的分辨率),或者将批量大小翻倍,因为 GPU 内存的消耗在这种情况下与完整的Float32训练循环相比是相同的。使用双倍批量大小运行相同的实验得到以下结果:
Training time for 10 epochs: 218.82141995429993 / Best validation loss: 0.18198483
如我们所见,增加批量大小对训练循环的性能有着非常好的影响,验证损失大大降低,并且训练时间也显著缩短(约 33%)。
然而,通常作为机器学习工程师(MLE)或数据科学家(DS),我们将处理大量数据和大型模型,运行训练循环,预计需要持续数小时或数天。因此,在工作中,MLEs/DSs 通常会在工作日结束前启动训练循环,留下训练在后台运行,并在下一个工作日回来分析和评估结果。在这种环境下,实际上优化预期训练时间以提升性能是一种更好的策略。使用 MXNet,我们也可以为此优化训练参数。例如,我们可以通过将训练轮数翻倍来调整训练时间。在这种情况下,实验得到了以下结果:
Training time for 10 epochs: 645.7392318248749 / Best validation loss: 0.16439788
与标准的Float32训练循环相比,这些结果非常好。然而,我们不要忘记,实际结果取决于特定任务、数据集、模型、超参数等。建议您在玩具训练循环中尝试不同的选项和超参数,以找到每种情况的最佳解决方案。
使用多个 GPU 和数据并行化训练
在这个方案中,我们将利用环境中多个 GPU 进一步优化训练。MXNet 和 Gluon 让我们可以非常轻松地更新训练循环以包含多个 GPU。
从高层次来看,利用多个 GPU 有两种范式:
-
模型并行化:将模型分割成多个部分,并将每个部分部署到特定的 GPU 上。当模型无法适配单个 GPU 时,这种范式非常有用。
-
数据并行化:数据批次被拆分成多个部分,每个部分会被分配到一个特定的 GPU 上,该 GPU 能够完全使用这些数据进行前向和反向传播。
我们将只使用数据并行化,因为它是最常见的用例,能带来很高的加速,并且由于其方法的简单性,它也最为便捷。
为了应用数据并行化,我们需要对训练循环进行如下修改:
-
设置上下文:上下文现在是一个列表,每个元素是一个特定的 GPU 上下文。
-
在这些上下文中初始化我们的模型:在数据并行化中,每个 GPU 都会存储一份所有模型参数的副本。
-
调整超参数:批量大小通常设置为尽可能大的值,而不填满 GPU 内存。当多个 GPU 并行工作时,这个数字通常可以乘以上下文中 GPU 的数量。然而,这也会对学习率产生副作用,必须将学习率乘以相同的数字,以保持梯度更新在相同的范围内。
-
分配数据:每个 GPU 必须拥有每个批次的一部分,并使用它进行前向和反向传播。
-
计算损失并更新梯度:每个 GPU 会计算与其批次切片相关的损失。MXNet 会自动结合这些损失并计算梯度,然后将其分发到每个 GPU,以更新它们的模型副本。
-
显示结果:训练损失和验证损失等统计信息通常会在每个批次中计算并积累,并在每个周期结束时进行可视化。
让我们看一些如何应用这些步骤的例子。
例如,在一个包含四个 GPU 的环境中设置上下文非常简单,使用 MXNet 只需要一行代码:
ctx_list = [mx.gpu(0), mx.gpu(1), mx.gpu(2), mx.gpu(3)]
初始化模型和自定义层就这么简单。对于我们的环境,以下是如何初始化带有 ResNet-101 主干的 Deeplabv3 网络:
deeplab_ft_direct_f32 = gcv.model_zoo.get_model('deeplab_resnet101_coco', pretrained=True, ctx=ctx_list)
[...]
deeplab_ft_direct_f32.head.initialize(ctx=ctx_list)
为了更新超参数,我们只需要计算上下文中的 GPU 数量,并更新之前计算的批量大小和学习率。对于我们的示例,这意味着只需添加或修改几行代码:
num_gpus = len(ctx_list)
[...]
batch_size_per_gpu = 4
batch_size = len(ctx_list) * batch_size_per_gpu
[...]
trainer = mx.gluon.Trainer(deeplab_ft_direct_f32.collect_params(), "sgd", {"learning_rate": 0.5})
为了将数据均匀地分配到每个 GPU 上,MXNet 和 Gluon 提供了一个非常方便的函数 split_and_load(),它会根据上下文中的 GPU 数量自动分配数据。在我们的环境中,操作如下:
data_list = mx.gluon.utils.split_and_load(data, ctx_list=ctx_list)
label_list = mx.gluon.utils.split_and_load(label, ctx_list=ctx_list)
为了计算损失并更新梯度,分布在每个 GPU 上的数据会通过循环并行处理。由于 MXNet 提供了自动并行化,这些调用是非阻塞的,每个 GPU 独立计算其输出和损失。此外,MXNet 会将这些损失结合起来生成完整的梯度更新,并将其重新分配给每个 GPU,所有这些操作都是自动完成的。我们只需要几行代码即可完成这一切:
with mx.autograd.record():
outputs = [model(data_slice) for data_slice in data_list]
losses = [loss_fn(output[0], label_slice) for output, label_slice in zip(outputs, label_list)]
for loss in losses:
loss.backward()
trainer.step(batch_size)
最后,为了显示损失计算,需要处理每个 GPU 的损失并将其组合。使用自动并行化,可以通过一行代码轻松实现这一点:
current_loss = sum([l.sum().asscalar() for l in losses])
通过这些简单的步骤,我们已经能够修改我们的训练循环以支持多个 GPU,并且现在可以测量这些变化带来的性能提升。
作为提醒,使用一个 GPU,我们达到了以下性能(批处理大小为四):
Training time for 10 epochs: 647.753002166748 / Best validation loss: 0.0937674343585968
在我们的环境中,使用 4 个 GPU,我们可以将批处理大小增加到 16,其结果如下:
Training time for 10 epochs: 177.23532104492188 / Best validation loss: 0.082047363743186
如预期的那样,我们已经能够将训练时间减少到约 25%(从 1 个 GPU 到 4 个 GPU 时的预期减少量,由于数据分布的预期损失而稍微改善了验证分数)。
工作原理如下…
在这个配方中,我们深入探讨了如何利用 MXNet 和 Gluon 优化我们的训练循环。我们利用我们的硬件(CPU 和 GPU)来处理训练循环中的每一个步骤:
-
我们重新审视了惰性评估和自动并行化机制如何共同作用以优化所有基于 MXNet 的流程。
-
我们利用所有的 CPU 线程来加载数据,并通过 GPU 中的预处理进一步优化该过程。我们还比较了速度和内存优化之间的权衡。
-
我们分析了不同的数据类型,并在可能的情况下将
Float32的精度与Float16的加速结合起来,使用 AMP。 -
我们通过使用多个 GPU(假设我们的硬件有这些设备可用)提升了训练循环的性能。
我们通过运行两个实验比较了每种场景,在特定优化之前和之后的性能,并强调了在使用这些优化时需要考虑的潜在权衡。在接下来的配方中,我们将同时应用所有这些优化技术,优化两个熟悉的任务:图像分割和文本翻译。
还有更多内容…
此配方中展示的所有优化特性都已在研究文献中进行了详细描述。以下是一些入门链接,以深入了解每个特性:
-
Gluon DataLoaders: https://mxnet.apache.org/versions/master/api/python/docs/tutorials/getting-started/crash-course/5-datasets.html
-
AMP:
medium.com/apache-mxnet/simplify-mixed-precision-training-with-mxnet-amp-dc2564b1c7b0 -
使用多个 GPU 进行训练:
mxnet.apache.org/versions/1.7/api/python/docs/tutorials/getting-started/crash-course/6-use_gpus.html
优化图像分割的训练
在之前的食谱中,我们展示了如何利用 MXNet 和 Gluon 通过各种技术来优化模型的训练。我们了解了如何联合使用懒惰求值和自动并行化来进行并行处理。我们看到如何通过结合在 CPU 和 GPU 上进行预处理来提高 DataLoader 的性能,以及如何使用半精度(Float16)与 AMP 结合来减少训练时间。最后,我们探索了如何利用多个 GPU 进一步减少训练时间。
现在,我们可以重新审视一个贯穿全书的课题:图像分割。我们在前几章的食谱中曾处理过这个任务。在第五章中的使用 MXNet Model Zoo 进行语义化物体分割——PSPNet 和 DeepLabv3食谱中,我们学习了如何使用 GluonCV Model Zoo 中的预训练模型,并介绍了我们将在本食谱中使用的任务和数据集:MS COCO和Penn-Fudan Pedestrian数据集。此外,在第七章中的提高图像分割性能食谱中,我们比较了处理目标数据集时可以采取的不同方法——是从头开始训练模型,还是利用预训练模型的现有知识并通过不同的迁移学习和微调方式进行调整。
在本食谱中,我们将应用所有这些优化技术,以训练图像分割模型为具体任务。
准备工作
与之前的章节类似,在本食谱中,我们将使用一些矩阵运算和线性代数,但这并不难,因为你会发现很多示例和代码片段来帮助你学习。
如何做...
在本食谱中,我们将探讨以下步骤:
-
重新审视我们当前的预处理和训练流程
-
应用训练优化技术
-
分析结果
让我们深入了解每个步骤。
重新审视我们当前的预处理和训练流程
在第七章中的提高图像分割性能食谱中,我们使用以下方法处理数据:
-
将数据从存储加载到CPU 内存空间
-
使用CPU预处理数据
-
使用默认参数在训练过程中处理数据
这种方法是一个有效的途径,用来比较我们可用的不同训练方案(从头开始训练、预训练模型、迁移学习和微调),而无需为实验增加复杂性。例如,这种方法在直接引入并评估微调技术时效果很好。
按照上述方法,在为此食谱选择的数据集(Penn-Fudan Pedestrian)上,基于 CPU 的预处理花费了以下时间:
Pre-processing time (s): 0.12470602989196777
此外,当与必要的步骤结合使用,如将数据批量重新加载并复制到 GPU 时,我们获得了以下性能:
Data-Loading in GPU time (s): 0.4085373878479004
在预处理之后,下一步是训练过程。如前所述,我们将通过直接使用微调技术来评估训练优化的效果。结合这种方法,我们将使用以下超参数:
# Epochs & Batch Size
epochs = 10
batch_size = 4
# Define Optimizer and Hyper Parameters
trainer = mx.gluon.Trainer(deeplab_ft_direct_naive.collect_params(), "sgd", {"learning_rate": 0.1})
在这些条件下,训练过程的持续时间和所达到的性能如下:
Training time for 10 epochs (s): 638.9948952198029 / Best validation loss: 0.09416388
如我们所见,在 10 分钟多一点的时间内,我们获得了优秀的验证性能(约为 0.09)。
每个 epoch 中训练损失和验证损失的演变如下:
图 8.2 – 回顾训练:训练损失与验证损失
从图 8.2中,我们可以看到训练损失和验证损失的演变。正如本书各章节所探讨的那样,我们选择提供最小验证损失的模型(在这种情况下,这是在最后一个 epoch,即 epoch 10 中实现的)。
在训练完成后,我们可以验证数据集测试集上的整体性能。从定量角度来看,以下是我们获得的结果:
PixAcc: 0.9627800347222222
mIoU : 0.9070747450272697
正如预期的那样,通过仅训练有限的 epoch 数(此处为 10),我们获得了优异的结果。
从定性角度来看,结果如下:
图 8.3 – 回顾训练:GroundTruth 示例和训练后的预测
正如预期的那样,结果展示了模型如何学会将焦点集中在前景中的人身上,避免背景中的人。
应用训练优化技术
在本章开头的引入训练优化功能食谱中,我们展示了不同的优化技术如何提高训练机器学习模型过程中各个步骤的性能,包括数据预处理、模型训练和评估。
在本节中,我们将展示如何通过使用 MXNet 和 Gluon,仅用几行代码,我们可以轻松应用所有我们已经介绍过的技术。
如本章第一个示例所示,MXNet 默认应用最佳策略(ThreadedEnginePerDevice)来优化惰性求值和自动并行化,考虑到可用的 CPU 线程数,因此我们无需在此进行任何更改(请注意,当使用多个 GPU 时,这项技术也会自动应用)。
我们还展示了如何通过结合使用 CPU 线程和 GPU 来优化数据预处理管道,考虑到每种设备的数量,并据此进行优化。为了进行此实验,选择了具有以下特征的特定硬件:
Number of CPUs: 16
Number of GPUs: 4
为了使用这种优化技术,我们需要对代码做一些更改。具体来说,我们需要定义可供使用的 GPU:
# Context variable is now a list,
# with each element corresponding to a GPU device
ctx_list = [mx.gpu(0), mx.gpu(1), mx.gpu(2), mx.gpu(3)]
num_gpus = len(ctx_list)
此外,在我们的预处理管道中,我们现在需要一个特定的步骤,将数据从 CPU 内存空间复制到 GPU 内存空间:
p_train_gpu = mx.gluon.data.SimpleDataset(
[(data.as_in_context(ctx_list[idx % num_gpus]), label.as_in_context(ctx_list[idx % num_gpus]))
for idx, (data, label) in enumerate(pedestrian_train_dataset)])
p_val_gpu = mx.gluon.data.SimpleDataset(
[(data.as_in_context(ctx_list[idx % num_gpus]), label.as_in_context(ctx_list[idx % num_gpus]))
for idx, (data, label) in enumerate(pedestrian_val_dataset)])
p_test_gpu = mx.gluon.data.SimpleDataset(
[(data.as_in_context(ctx_list[idx % num_gpus]), label.as_in_context(ctx_list[idx % num_gpus]))
for idx, (data, label) in enumerate(pedestrian_test_dataset)])
p_train_opt = p_train_gpu.transform(train_val_transform, lazy=False)
p_val_opt = p_val_gpu.transform(train_val_transform, lazy=False)
p_test_opt = p_test_gpu.transform(test_transform, lazy=False)
如本章第一个示例所讨论的,在典型的面向生产的环境中,我们不希望将数据保留在 GPU 中,以免占用宝贵的 GPU 内存。通常会根据 GPU 可用内存优化批量大小,并使用MXNet Gluon DataLoaders将数据从 CPU 内存空间批量加载到 GPU 内存空间。因此,为了使我们的基于 GPU 的预处理管道完整,我们需要一个最终步骤,将数据复制回 CPU 内存空间:
to_cpu_fn = lambda x: x.as_in_context(mx.cpu())
通过这些代码更改,我们的最佳预处理管道已经准备就绪,可以继续进行下一个优化技术:应用Float16优化,包括 AMP。
如本章第一个示例所示,为了启用这项技术,我们只需要对代码进行一些更改。首先,我们初始化库:
# AMP
amp.init()
其次,我们将训练器/优化器附加到库中:
amp.init_trainer(trainer)
最后,由于Float16数据类型的局限性,存在梯度溢出/下溢的风险;因此,我们需要根据情况调整(缩放)损失,这可以通过以下几行代码自动完成:
with amp.scale_loss(losses, trainer) as scaled_losses: mx.autograd.backward(scaled_losses)
通过这三项简单的更改,我们已经更新了训练循环,使其能够有效地使用Float16数据类型(在适当的情况下)。
请注意在前面的代码片段中,我们现在正使用一个损失列表,而不是单一的实例。这是由于我们的下一个也是最后一个训练优化技术:使用多个 GPU。
正如我们将看到的,优化地使用多个 GPU 意味着将它们并行工作,因此,需要并行计算损失并执行训练的反向传播,从而生成前述段落中描述的损失列表。
为了并行使用多个 GPU,我们需要将新的上下文定义为一个列表(之前在预处理部分出现过,这里为了方便再次展示):
# Context variable is now a list,
# with each element corresponding to a GPU device
ctx_list = [mx.gpu(0), mx.gpu(1), mx.gpu(2), mx.gpu(3)
num_gpus = len(ctx_list)
由于现在我们有多个 GPU,我们可以增加批量大小,以便最佳利用可用的 GPU 内存空间:
batch_size = len(ctx_list) * batch_size_per_gpu
此外,在从 Gluon DataLoader 读取数据时,我们需要将数据批次分配到多个 GPU 上。幸运的是,Gluon 还提供了一个简化该操作的功能。我们只需要添加以下几行代码(对于每个训练和验证批次):
data_list = mx.gluon.utils.split_and_load(data , ctx_list=ctx_list)
label_list = mx.gluon.utils.split_and_load(label, ctx_list=ctx_list)
如前所述,这种跨 GPU 的划分使我们能够并行计算模型输出及与这些输出相关的损失(衡量实际输出与预期输出之间的差异)。这可以通过以下几行代码实现:
outputs = [model(data_slice) for data_slice in data_list]
losses = [loss_fn(output[0], label_slice) for output, label_slice in zip(outputs, label_list)]
最后,我们计算用于更新模型权重的反向传播过程(结合 AMP 的缩放损失):
with amp.scale_loss(losses, trainer) as scaled_losses:
mx.autograd.backward(scaled_losses)
通过这些最小的代码更改,我们现在拥有了一个最佳的预处理和训练管道,可以运行实验以分析性能变化。
分析结果
在前面的部分,我们回顾了预处理和训练管道的先前性能,并回顾了我们如何应用必要的更改以实现训练优化技术,特别是针对我们的图像分割任务。
我们的预处理管道步骤现在如下:
-
从存储中加载数据到 CPU 内存空间。
-
使用 GPU 预处理数据。
-
将数据复制回 CPU 内存空间。
-
使用优化的参数在训练过程中处理数据。
对于我们的实验,我们将直接使用微调技术。
将之前描述的方法应用于为本方案选择的数据集(Penn-Fudan Pedestrian),预处理的时间如下:
Pre-processing time (s): 0.10713815689086914
端到端的预处理管道必须考虑使用Gluon DataLoader加载数据的批处理过程——在我们的情况下,将数据加载到多个 GPU 中,如下所示:
Data-Loading in GPU time (s): 0.18216562271118164
与本方案的初始部分相比(当时预处理需要0.4秒),我们可以看到,即使在将数据复制回 CPU 内存空间的额外开销下,我们仍然将预处理性能提高了>2 倍。
在预处理之后,下一步是训练过程。正如前面所描述的,我们将直接使用微调技术来评估我们训练优化的效果。结合这种方法,我们使用以下超参数:
# Epochs & Batch Size
epochs = 10
batch_size_per_gpu = 4
batch_size = len(ctx_list) * batch_size_per_gpu
# Define Optimizer and Hyper Parameters
trainer = mx.gluon.Trainer(deeplab_ft_direct_opt.collect_params(), "sgd", {"learning_rate": 0.5})
请注意,通过将多个 GPU 添加到训练过程中,我们可以增加批量大小(乘以 GPU 的数量),还可以增加学习率(从 0.1 增加到 0.5)。在这些条件下,训练过程的持续时间和实现的性能如下:
Training time for 10 epochs: 59.86336851119995 / Best validation loss: 0.08904324161509672
如图所示,我们在不到 1 分钟的时间内就得到了优秀的验证表现(约 0.09)。与配方中获得的结果相比,我们可以看到损失的减少非常小(这将通过我们的性能分析进一步确认),但迄今为止最大的一项改进是训练时间减少了>10 倍。这个改进归功于我们应用的所有训练优化技术。简而言之,每项优化都提供了以下改进:
-
使用 4 个 GPU:提供了 4 倍的时间缩短
-
使用 Float16 和 AMP:提供了 2 倍的时间减少(合计 8 倍)
-
预处理数据集:提供了 1.25 倍的时间减少(合计>10 倍)
每个 epoch 中的训练损失和验证损失的变化如下:
图 8.4 – 优化训练:训练损失与验证损失
从图 8.4中,我们可以看到训练损失和验证损失的变化。正如本章至今所探讨的,我们选择提供最小验证损失的模型(在这种情况下,在最后一个 epoch,即第 10 个 epoch 中取得)。
训练完成后,我们可以在数据集的测试分割上验证整体性能。从定量的角度来看,我们获得的结果如下:
PixAcc: 0.9679262152777778
mIoU : 0.9176786683400912
正如预期的那样,仅通过训练有限的 epoch(本例中为 10 个 epoch),我们就得到了优秀的结果。我们还可以确认,验证损失的最小改善带来了测试指标的微小改进(与我们初始实验中的 0.96/0.91 相比)。
从定性的角度来看,我们得到了以下结果:
图 8.5 – 优化训练:GroundTruth 示例与训练后预测
正如预期的那样,结果显示模型已经学会了将注意力集中在前景中的不同人物上,避免了背景中的人物。
工作原理...
在本配方中,我们应用了本章第一部分中的不同训练优化技术,利用我们的硬件(CPU 和 GPU)来解决训练循环中的每个步骤:
-
我们重新审视了惰性评估和自动并行化机制如何协同工作,以优化所有基于 MXNet 的流程。
-
我们利用所有 CPU 线程加载数据,并通过在 GPU 上进行预处理进一步优化了该过程。我们还比较了速度和内存优化之间的权衡。
-
我们分析了不同的数据类型,并结合了
Float32的准确性与Float16的加速效果(在可能的情况下),并使用了 AMP。 -
我们通过使用多个 GPU 提高了训练循环的性能(假设我们的硬件有这些设备)。
我们将这些场景分别应用于图像分割任务,并进行了两次实验。在第一次实验中,我们没有应用前面章节中描述的任何训练优化技术,而是遵循了书中前几章提到的方法。在第二次实验中,我们并行应用了所有技术,尽可能进行优化。
这一方法非常有用,提供了类似的算法性能,同时将训练时间提高了 10 倍(从 10 分钟缩短到 1 分钟)。这主要得益于使用了多个 GPU(减少了 4 倍),利用Float16 AMP(减少了 2 倍)以及优化的预处理(减少了 1.25 倍)。
还有更多内容……
我们已经描述、实现、执行并评估了几种训练优化技术。然而,还有更多先进的技术可以用来实现最佳的训练循环。
其中一种技术是学习率调度。在本书中,我们一直使用常数学习率。然而,使用动态调整的学习率有多个优点,其中一些如下:
-
预热:在使用预训练模型时,不建议从较大的学习率开始。初始的几个 epoch 必须用于梯度的调整。这可以看作是将模型从源任务调整到目标任务的方式,保留并利用来自前一个任务的知识,因此推荐使用较小的学习率。
-
衰减:在最佳训练循环中,当模型学习到输入到输出的预期表示时,训练的目标是产生越来越精细的改进。较小的学习率在这些阶段能获得更好的性能(更小、更稳定的权重更新)。因此,经过几个 epoch 后,衰减学习率是首选。
Dive into Deep Learning 书中提供了关于如何在 MXNet 中实现这些技术的深入见解:d2l.ai/chapter_optimization/lr-scheduler.html.
优化训练以将文本从英语翻译为德语
在本章的第一个示例中,我们展示了如何利用 MXNet 和 Gluon 优化我们的模型训练,应用不同的技术。我们理解了如何联合使用惰性计算和自动并行化进行并行处理,并通过将预处理分配到 CPU 和 GPU 上提高了 DataLoader 的性能。我们还看到,结合使用半精度(Float16)和 AMP 可以将训练时间缩短一半,并探索了如何利用多个 GPU 进一步缩短训练时间。
现在,我们可以重新审视我们在整本书中一直在处理的问题,即从英语到德语的翻译。我们在之前的章节中已经处理了翻译任务。在第六章中的从越南语到英语的翻译示例中,我们介绍了翻译任务,并学习了如何使用来自 GluonCV 模型库的预训练模型。此外,在第七章中的提高从英语到德语翻译性能示例中,我们介绍了本示例中将要使用的数据集:WMT2014和WMT2016,并比较了我们在处理目标数据集时可以采取的不同方法:从头开始训练我们的模型,或利用预训练模型的过去知识并进行调整,采用不同的迁移学习和微调策略。
因此,在本示例中,我们将应用所有这些优化技术,专门用于训练一个英语到德语的文本 翻译模型。
准备工作
与之前的章节一样,在本示例中我们将使用一些矩阵运算和线性代数,但理解起来一点也不难。
如何实现...
在本示例中,我们将按以下步骤进行操作:
-
重新审视我们当前的数据预处理和训练流程
-
应用训练优化技术
-
分析结果
让我们深入了解每一步。
重新审视我们当前的数据预处理和训练流程
在第七章中的提高从英语到德语翻译性能的示例中,我们使用以下方法处理数据:
-
将数据从存储加载到 CPU 内存中
-
使用 CPU 对数据进行了预处理
-
在训练过程中使用了默认参数来处理数据
这是一个有效的方法,可以比较我们可用的不同训练选择(从头开始训练、预训练模型、迁移学习和微调),而不会增加实验的复杂性。例如,这种方法非常适合介绍和评估微调技术,这是我们在本示例中选择的技术。
在应用前面描述的方法到本示例所选数据集(WMT2016)时,基于 CPU 的预处理需要以下时间:
Pre-processing time (s): 2.697735548019409
此外,当与必要的批量重新加载数据并将其复制到 GPU 的步骤结合时,我们将获得以下性能:
Data-Loading in GPU time (s): 27.328779935836792
预处理完成后,下一步是训练过程。如前所述,我们将直接评估使用微调技术对训练优化的影响。结合这种方法,我们使用以下超参数:
# Epochs & Batch Size
hparams.epochs = 5
hparams.lr = 0.00003
# hparam.batch_size = 256
在这些条件下,训练过程的持续时间和所取得的性能如下:
Training time for 5 epochs: 11406.558312892914 / Best validation loss: 1.4029905894300159
如我们所见,在训练时间大约为 3 小时的情况下,我们获得了优秀的验证性能(~1.4)。
每轮训练损失和验证损失的变化情况如下所示:
图 8.6 – 重新审视训练:训练损失与验证损失
从图 8.6中,我们可以看到训练损失和验证损失的变化。正如在各章节中探讨的那样,我们选择提供最小验证损失的模型(在这个案例中,最小验证损失出现在第一轮训练,即第 1 轮)。
训练完成后,我们可以在数据集的测试分割中验证整体性能。从定量角度来看,以下是我们获得的结果:
WMT16 test loss: 1.28; test bleu score: 27.05
正如预期的那样,通过仅仅训练有限的时期(在此为 10 轮),我们就获得了优异的结果。
从定性角度来看,我们还可以通过测试一个示例句子来检查模型的表现。在我们的案例中,我们选择了I learn new things every day,并且得到的输出如下:
Qualitative Evaluation: Translating from English to German
Expected translation:
Ich lerne neue Dinge jeden Tag.
In English:
I learn new things every day.
The German translation is:
Immer wieder erfährt ich Neues.
输出结果中的德语句子(Immer wieder erfährt ich Neues)的意思是我总是学习新东西,因此,从结果中可以看出,文本几乎已从英语完美翻译成德语。
应用训练优化技术
在本章开头的引入训练优化特性配方中,我们展示了不同的优化技术如何提高我们在训练机器学习模型时所采取的不同步骤的性能,包括数据预处理、训练和评估模型。
在本节中,我们将展示如何通过使用 MXNet 和 Gluon 以及仅仅几行代码,轻松应用我们已介绍的所有技术。
如本章的第一种配方所示,MXNet 默认应用最佳策略(ThreadedEnginePerDevice)来优化懒评估和自动并行化,考虑到可用的 CPU 线程数,因此我们无需在此处进行任何修改(请注意,当使用多个 GPU 时,这项技术也会自动应用)。
我们已经展示了如何通过结合使用 CPU 线程和 GPU,优化我们的数据预处理管道,考虑到每个设备的可用数量并进行相应优化。对于这次实验,选择了具有以下特征的特定硬件:
Number of CPUs: 16
Number of GPUs: 4
为了应用这种优化技术,我们不得不对代码进行一些修改。具体来说,我们定义了可用的 GPU:
# Context variable is now a list,
# with each element corresponding to a GPU device
ctx_list = [mx.gpu(0), mx.gpu(1), mx.gpu(2), mx.gpu(3)]
num_gpus = len(ctx_list)
此外,在我们的预处理管道中,我们现在需要一个特定的步骤,将数据从 CPU 内存空间复制到 GPU 内存空间:
wmt2016_train_data_processed_gpu = mx.gluon.data.SimpleDataset([(mx.nd.array(data).as_in_context(ctx_list[idx % num_gpus]), mx.nd.array(label).as_in_context(ctx_list[idx % num_gpus])) for idx, (data, label) in enumerate(wmt2016_train_data_processed)])
wmt2016_train_data_processed_gpu = mx.gluon.data.SimpleDataset([(mx.nd.array(data).as_in_context(ctx_list[idx % num_gpus]), mx.nd.array(label).as_in_context(ctx_list[idx % num_gpus])) for idx, (data, label) in enumerate(wmt2016_train_data_processed)])
wmt2016_val_data_processed_gpu = mx.gluon.data.SimpleDataset([(mx.nd.array(data).as_in_context(ctx_list[idx % num_gpus]), mx.nd.array(label).as_in_context(ctx_list[idx % num_gpus])) for idx, (data, label) in enumerate(wmt2016_val_data_processed)])
wmt2016_ test _data_processed_gpu = mx.gluon.data.SimpleDataset([(mx.nd.array(data).as_in_context(ctx_list[idx % num_gpus]), mx.nd.array(label).as_in_context(ctx_list[idx % num_gpus])) for idx, (data, label) in enumerate(wmt2016_ test _data_processed)])
如本章第一个例子所讨论的,在典型的生产环境中,我们并不希望将数据保留在 GPU 中,因为它会占用宝贵的 GPU 内存。通常会根据 GPU 可用内存优化批次大小,并通过 MXNet Gluon DataLoaders 从 CPU 内存空间批量加载数据到 GPU 内存空间。因此,为了使我们的基于 GPU 的预处理管道完整,我们需要一个最终步骤,将数据复制回 CPU 内存空间。正如在第七章中的提高英德翻译性能一节中介绍的,我们使用的是来自 MXNet GluonNLP 库的 ShardedDataLoader 类。这个类会自动执行数据的回传到 CPU 内存空间。
然而,正如我们在实验中将看到的,当使用多个 GPU 时,直接使用 MXNet Gluon DataLoaders 会更高效,因为它们设计上可以在后续进行最佳并行化。
通过这些代码修改,我们的最佳预处理管道已经准备好,接下来可以继续进行下一个优化技术:应用 Float16 优化,包括 AMP。
正如本章第一个例子所示,为了启用该技术,我们只需要在代码中做几个修改。首先,我们初始化库:
# AMP
amp.init()
其次,我们将训练器/优化器附加到库中:
amp.init_trainer(trainer)
在前面的例子中,当处理图像时,我们描述了由于梯度可能出现过度/欠流动的问题,因此需要相应地调整(缩放)损失。这在我们的用例中并不必要,因此我们在这里不进行损失 缩放。
通过这两个简单的修改,我们已更新训练循环,以便在适当时使用 Float16 数据类型高效工作。
最后,我们可以应用下一个也是最后一个训练优化技术:使用多个 GPU。
正如我们将看到的,优化地使用多个 GPU 意味着并行处理它们,因此并行计算损失并执行训练的反向传递,从而得到上一段描述的损失列表。
为了在多个 GPU 上并行工作,我们需要将新上下文定义为一个列表(之前在预处理时见过,这里再次展示以便于参考):
# Context variable is now a list,
# with each element corresponding to a GPU device
ctx_list = [mx.gpu(0), mx.gpu(1), mx.gpu(2), mx.gpu(3)]
num_gpus = len(ctx_list)
由于我们现在有了多个 GPU,我们可以增加批次大小,以最优化使用可用的 GPU 内存空间:
batch_size = len(ctx_list) * batch_size_per_gpu
此外,当从 Gluon DataLoaders 中读取数据时,我们需要将数据批次分配到各个 GPU 上。幸运的是,Gluon 也提供了一个简化该操作的函数。我们只需为每个训练和验证批次添加以下几行代码:
src_seq_list = mx.gluon.utils.split_and_load(src_seq, ctx_list=ctx_list, even_split=False)
tgt_seq_list = mx.gluon.utils.split_and_load(tgt_seq, ctx_list=ctx_list, even_split=False)
src_valid_length_list = mx.gluon.utils.split_and_load(src_valid_length, ctx_list=ctx_list, even_split=False)
tgt_valid_length_list = mx.gluon.utils.split_and_load(tgt_valid_length, ctx_list=ctx_list, even_split=False)
如前所述,GPU 之间的分割使我们能够并行计算模型的输出及其相关的损失(即实际输出与预期输出之间的差异度量)。这可以通过以下几行代码实现:
out_slice, _ = wmt_transformer_model_ft_direct_opt(
src_seq_slice,
tgt_seq_slice[:, :-1],
src_valid_length_slice,
tgt_valid_length_slice - 1)
loss = loss_function(out_slice, tgt_seq_slice[:, 1:], tgt_valid_length_slice - 1)
通常,为了使我们的更新能够在训练循环中与多个 GPU 一起工作,我们需要对损失缩放进行进一步修改。然而,正如前面所讨论的,对于我们的使用案例,这是不必要的。
通过这些最小的代码更改,我们现在拥有了一个最佳的预处理和训练管道,可以运行所需的实验来分析性能变化。
分析结果
在前面的部分中,我们回顾了我们预处理和训练管道的先前性能,并回顾了如何为我们的训练优化技术应用必要的更改,特别是针对我们将英文翻译成德文的任务。
我们的预处理管道步骤现在如下:
-
将数据从存储加载到 CPU 内存空间。
-
使用 GPU 预处理数据(尽管正如我们将看到的,我们会将其改为 CPU)。
-
将数据复制回 CPU 内存空间(此操作不必要)。
-
在训练过程中使用优化的参数处理数据。
对于我们的实验,我们将直接使用微调技术。
按照前述方法,在为本食谱选择的数据集(WMT2016)上,基于 GPU 的预处理花费了以下时间:
Pre-processing time (s): 50.427586793899536
端到端的预处理管道必须考虑使用 Gluon DataLoader 进行批处理的过程(在我们的案例中,将数据加载到多个 GPU 中),从而为我们提供以下性能:
Data-Loading in GPU time (s): 72.83465576171875
与本食谱的初始部分(预处理花费了 27 秒)相比,我们可以看到,在这种情况下,GPU 上的预处理效果并不显著。这是由于文本数据的特性,它不像图像那样容易并行化。
在这种情况下,基于 CPU 的预处理管道是最佳选择,避免使用 Gluon NLPShardedDataLoader 类,而改用 Gluon DataLoader 类(它更适合并行化)。应用此管道后,我们得到了以下结果:
Data-Loading in CPU with Gluon DataLoaders time (s): 24.988255500793457
这为我们提供了一个最小的优势(2 秒),但如前所述,这是使用 Gluon DataLoader 及其并行化功能时我们能得到的最佳结果。
经过预处理后,下一步是训练过程。如前所述,我们将使用微调技术直接评估我们训练优化的效果。结合这种方法,我们使用以下超参数:
# Epochs & Batch Size
hparams.epochs = 5
hparams.lr = 0.0001
# hparams.batch_size = num_gpus * 256
请注意,通过在训练过程中增加多个 GPU,我们可以增加批处理大小(乘以 GPU 数量),并且还可以增加学习率(从 0.00003 增加到 0.0001)。在这些条件下,训练过程的持续时间和达到的性能如下:
Training time for 5 epochs: 1947.1244320869446 / Best validation loss: 1.2199710432327155
如我们所见,在训练时间约为 3 小时的情况下,我们获得了出色的验证表现(约 1.4)。与本食谱初始部分获得的结果相比,我们可以看到损失有了最小的下降(这是一个积极的变化,我们将在接下来的性能分析中确认),但迄今为止最大的改进是训练时间减少了 5.5 倍。这个改进归功于我们应用的所有训练优化技术。简而言之,每个优化提供了以下改进:
-
使用 4 个 GPU:提供了 4 倍的降低(如预期)。
-
Float16在不影响算法性能的情况下使用。 -
预处理数据集:在这种情况下,改进几乎可以忽略不计。
每一轮训练中的训练损失和验证损失的演变如下所示:
图 8.7 – 优化训练:训练损失与验证损失
从图 8.7中,我们可以看到训练损失和验证损失的变化过程。如本书各章节所述,我们选择了提供最小验证损失的模型(在这种情况下,这是在第一轮训练时实现的)。
训练完成后,我们可以在数据集的测试分区中验证整体性能。从量化角度来看,这些是我们获得的结果:
WMT16 test loss: 1.27; test bleu score: 28.20
正如预期的那样,仅通过训练有限数量的轮次(在本例中为 5 次),我们就获得了优异的结果。我们还可以确认,验证损失的最小改进为我们的测试指标提供了最小的提升(与最初获得的 27.05 相比)。
从定性角度来看,我们也可以通过用一个示例句子测试模型来检查它的表现。在我们的例子中,我们选择了 I learn new things every day,得到的输出如下:
Qualitative Evaluation: Translating from English to German
Expected translation:
Ich lerne neue Dinge.
In English:
I learn new things every day.
The German translation is:
Ich lerne jedes Mal Neues.
输出中得到的德语句子(Ich lerne jedes Mal Neues)的意思是 I learn something new every time,因此从结果来看,文本几乎已经被完美地从英语翻译成德语。
它是如何工作的...
在本食谱中,我们应用了本章第一个食谱中看到的不同训练优化技术,利用我们的硬件(CPU 和 GPU)来解决训练循环中的每个步骤:
-
我们重新审视了懒惰求值和自动并行化机制如何协同工作,以优化所有基于 MXNet 的流程。
-
我们利用了所有 CPU 线程来加载数据,并通过在 GPU 上进行预处理进一步优化了该过程。在这种情况下,展示了结合 Gluon DataLoader 的基于 CPU 的预处理管道是最优方案。
-
我们分析了不同的数据类型,并结合了
Float32的准确性和精度,以及Float16的加速效果,并在可能的情况下,使用了 AMP。 -
我们通过使用多个 GPU(假设我们的硬件具备这些设备)提高了训练循环的性能。
我们将每种具体应用于将英文文本翻译成德文的情景进行了比较,进行了两项实验。在第一项实验中,我们没有应用书中描述的任何训练优化技术,而是采用了之前章节中的方法。在第二项实验中,我们同时应用了所有技术,试图尽可能优化。
这证明非常有用,提供了类似的算法性能,训练时间缩短了 5.5 倍(从 3 小时缩短到 30 分钟)。这主要是由于使用了多个 GPU(减少了 4 倍)和利用了Float16和 AMP(减少了 1.4 倍),而优化的预处理提供了微不足道的改进。
还有更多内容…
我们描述、实施、执行和评估了几种训练优化技术。然而,还有更高级的技术可以利用,以实现最佳的训练循环。
其中一种技术是人类反馈强化学习(RLHF),引入了人在回路中的过程。在这个过程中,模型训练完成后,会向人员展示模型的不同输出选项(例如,不同的潜在翻译),并根据这些人员对哪个最好地表达原始句子进行排序。这些人类输入然后用于训练一个评分模型,评分模型会对模型的输出进行评分,并选择分数最高的输出。这种技术已被证明非常强大。例如,OpenAI利用RLHF在GPT-3语言模型之上开发了ChatGPT。
要了解更多关于ChatGPT和RLHF的信息,推荐阅读以下文章:huyenchip.com/2023/05/02/rlhf.html。
第九章:使用 MXNet 提升推理性能
在之前的章节中,我们利用 MXNet 的功能解决了计算机视觉和自然语言处理任务。这些章节的重点是从预训练模型中获得最大性能,利用 GluonCV 和 GluonNLP 的模型库API。我们使用从头开始的不同方法训练这些模型,包括迁移学习和微调。在上一章中,我们探索了如何利用一些高级技术优化训练过程。最后,在本章中,我们将重点提高推理过程本身的性能,加速从我们的模型中获得结果,并讨论与边缘****AI 计算相关的多个主题。
为了实现优化推理管道性能的目标,MXNet 包含了不同的功能。我们已经简要讨论过其中的一些功能,例如在前一章中介绍的自动混合精度(AMP),它可以提高训练性能,同时也可以用来提升推理性能。在本章中,我们将重新讨论这一点,以及其他功能,如混合化。此外,我们还将进一步优化如何有效利用数据类型,借助量化中的INT8数据类型加速推理过程。
此外,我们将探索我们的模型在操作方面的工作原理,了解它们如何在MXNet 分析器的帮助下内部运行。然后,我们将借助 MXNet GluonCV 模型库,进一步学习如何将我们的模型导出为ONNX格式,使用该格式,我们可以将模型应用于不同的框架,例如将我们的模型部署到 NVIDIA 硬件平台上,如NVIDIA Jetson系列产品。
最后,我们将结合应用所有这些技术,选择书中已经探讨过的问题作为例子。对于计算机视觉任务,我们将选择图像分割;对于自然语言处理任务,我们将选择将英文文本翻译成德文。
本章具体包含以下食谱:
-
介绍推理优化功能
-
优化图像分割的推理
-
优化将英文文本翻译为德文时的推理
技术要求
除了《前言》中指定的技术要求外,以下内容适用:
-
确保你已经完成了第一章中的食谱 1,安装 MXNet。
-
确保你已经完成了第五章,使用计算机视觉分析图像,以及第六章,利用自然语言处理理解文本。
-
确保你已经完成了第七章,通过迁移学习与微调优化模型。
本章的代码可以在以下 GitHub 网址找到:github.com/PacktPublishing/Deep-Learning-with-MXNet-Cookbook/tree/main/ch09。
此外,你可以直接从 Google Colab 访问每个配方,例如,本章第一个配方:github.com/PacktPublishing/Deep-Learning-with-MXNet-Cookbook/blob/main/ch09/9_1_Introducing_inference_optimization_features.ipynb。
引入推理优化功能
在之前的章节中,我们已经看到如何利用 MXNet、GluonCV 和 GluonNLP 从特定数据集(如 ImageNet、MS COCO 或 IWSLT2015)中获取预训练模型,并将其应用于我们的特定任务和数据集。此外,我们还使用了迁移学习和微调技术来提高这些任务/数据集的算法性能。
在这个配方中,我们将介绍(并重温)几个概念和功能,这些内容将优化我们的推理循环,以提高运行时性能,同时分析其中的权衡。
做好准备
与之前的章节一样,在这个配方中,我们将使用一些矩阵运算和线性代数,但这完全不难。
如何操作...
在这个配方中,我们将执行以下步骤:
-
混合我们的模型
-
使用 float16 和 AMP 进行推理
-
使用 INT8 进行量化
-
对我们的模型进行性能分析
让我们深入了解每个步骤。
混合我们的模型
在最初的章节中,我们在探索 MXNet 的特性时,重点介绍了命令式编程。如果你以前用过 Java、C/C++或 Python 等语言编程,那么你很可能使用过命令式编程。这是一种常见的编码方式,因为它更灵活。
在命令式编程中,通常期待代码中的语句按顺序逐步执行。例如,在我们的评估路径中,通常会逐步执行这些语句,通常是在一个循环内部:
-
从数据加载器中加载新样本。
-
转换输入和预期输出,以便它们可以被我们的模型和指标计算所使用。
-
将输入传递给模型以计算输出。
-
将模型输出与预期输出进行比较,并更新相应的指标。
在这种编程范式中,每个语句按顺序执行,输出可以在每一步完成后进行检查或调试(因为 MXNet 使用惰性求值)。
使用不同的编程范式,称为符号编程,其中使用的是符号,符号本质上是操作的抽象,直到定义的某一点(通常称为编译步骤)才会进行实际计算。这对于深度学习尤其有用,因为所有模型都可以定义为图,使用这个图作为符号,优化底层图中的操作路径,并仅在需要时运行优化后的计算。
然而,由于计算尚未发生,因此每个步骤的输出无法检查或调试,这使得查找和修复问题变得更加困难。另一方面,由于图优化的能力,符号编程需要更少的内存且速度更快。
幸运的是,使用 MXNet,我们可以充分利用两者的优势。我们可以使用命令式编程定义模型,进行测试、调试和修复(通过print语句、测试、调试等机制)。当我们准备好进行优化时,我们只需要调用hybridize函数,它会处理底层的一切工作,与我们的图形在符号编程中一起工作。这种方法被称为混合编程,是 MXNet 的最佳优势之一。此外,这个特性没有硬件限制,可以用于 CPU 和 GPU 计算。
作为一个玩具示例,我们可以进行一些实验,通过推理模型并比较不同配置的不同结果。具体来说,这些是我们将测试的配置:
-
CPU:
-
使用命令式执行
-
使用符号执行和默认参数
-
使用符号执行和特定后端
-
使用符号执行、特定后端和静态内存分配
-
使用符号执行、特定后端、静态内存分配和不变输入形状
-
-
GPU:
-
使用命令式执行
-
使用符号执行和默认参数
-
使用符号执行和静态内存分配
-
使用符号执行、静态内存分配和不变输入形状
-
请注意,为了正确验证计算时间,我们添加了对mx.nd.waitall()函数的调用。选择的方法是使用ADE20K验证集(数据集可通过 MXNet GluonCV 获得),并使用DeepLabv3模型进行处理。我们将使用批量大小为 4:
-
对于初始的 CPU 计算配置,使用命令式执行时,模型对数据集的处理时间如下:
Time (s): 115.22693085670471 -
对于第二种 CPU 计算配置,我们只需利用 MXNet 混合编程模型并使用以下方式转换我们的模型:
deeplab_pt_cpu_hybrid.hybridize() Time (s): 64.75840330123901正如我们所看到的,所做的优化将计算时间减少了近一半。
-
对于第三种 CPU 计算配置,我们只需稍微修改我们的混合化调用以定义特定的后端。我们将利用我们的 Intel CPU 架构,使用
MKLDNN后端,并使用以下方式转换我们的模型:deeplab_pt_cpu_hybrid.hybridize(backend = "MKLDNN") Time (s): 55.860424757003784正如我们所见,特定的后端进一步将计算时间减少了约 20%。
-
对于第四个 CPU 计算配置,我们只需要稍微修改我们的混合化调用,定义我们希望使用静态内存分配。我们可以使用以下方式更新我们的调用:
deeplab_pt_cpu_hybrid.hybridize(backend = "MKLDNN", static_alloc=True) Time (s): 53.905478715896606正如我们所见,静态内存分配使我们能够将计算时间再减少约 4%。
-
对于第五个 CPU 计算配置,我们只需要稍微修改我们的混合化调用,定义我们希望利用不变的输入形状(我们已经预处理了数据,使其具有相同的输入形状,
480x480)。我们可以使用以下方式更新我们的调用:deeplab_pt_cpu_hybrid.hybridize(backend = "MKLDNN", static_alloc=True, static_shape=True) Time (s): 52.464826822280884正如我们所见,不变输入形状约束使我们能够将计算时间再减少约 2%。
-
对于初始的 GPU 计算配置,采用命令式执行,模型处理数据集的时间如下:
Time (s): 13.315197944641113 -
对于第二个 GPU 计算配置,我们只需要利用 MXNet 混合编程模型,并用以下方式转换我们的模型:
deeplab_pt_gpu_hybrid.hybridize() Time (s): 12.873461246490479正如我们所见,当在 GPU 上执行优化时,由于 GPU 已经针对这些类型的计算进行了内部优化,因此几乎没有提高计算时间。
-
对于 GPU 计算,没有需要选择的特定后端。因此,对于第三个 GPU 计算配置,我们只需要稍微修改我们的混合化调用,定义我们希望使用静态内存分配。我们可以使用以下方式更新我们的调用:
deeplab_pt_gpu_hybrid.hybridize(static_alloc=True) Time (s): 12.752988815307617正如我们所见,静态内存分配在 GPU 上带来了另一个微不足道的改进。
-
对于第四个 GPU 计算配置,我们只需要稍微修改我们的混合化调用,定义我们希望利用不变的输入形状(我们已经预处理了数据,使其具有相同的输入形状,480x480)。我们可以使用以下方式更新我们的调用:
deeplab_pt_gpu_hybrid.hybridize(static_alloc=True, static_shape=True) Time (s): 12.583650827407837正如我们所见,不变输入形状约束在 GPU 上带来了另一个微不足道的改进。
结果显示,当使用 CPU 时,我们可以将推理时间减少到原始时间的一半,这是一个显著的改进。使用 GPU 时,由于内部优化,改进几乎可以忽略不计。
重要提示
请注意,在代码中我们是如何使用mx.nd.waitall()函数来验证所有计算是否已经严格完成,然后才计算这些操作所花费的时间。
应用 float16 和 AMP 进行推理
在上一章,第八章,使用 MXNet 提升训练性能,我们介绍了float16数据类型和 AMP 优化,这是一种极为简单的方式,仅在最有用时才使用这一半精度数据类型。
在食谱 1,介绍训练优化特性,上一章中,我们比较了单精度(float32)和半精度(float16)数据类型,理解它们的特性和内存/速度折衷。如果你还没有复习这个食谱,建议你回顾一下,因为它与本主题非常相关。
正如大多数概念之前已介绍的那样,本节将重点讨论如何将 AMP 应用于推理过程。像往常一样,MXNet 为此操作提供了一个非常简单的接口,只需调用amp.convert_hybrid_block()函数即可。
该优化可以应用于 CPU 和 GPU 环境,因此让我们来运行这些实验。
要修改我们的 CPU 模型以使用 AMP,我们只需要以下一行代码:
deeplab_pt_cpu_hybrid_amp = amp.convert_hybrid_block(deeplab_pt_cpu_hybrid, ctx=mx.cpu())
使用这个修改后的模型,处理数据集的时间如下:
Time (s): 56.16465926170349
正如我们所见,AMP 在 CPU 上几乎没有产生改善。这是因为最大的收益出现在训练时所需的反向传递过程中,但在推理过程中并不需要。此外,CPU 通常没有专门的电路来直接处理 float16,这限制了改进效果。
要修改 GPU 模型以使用 AMP,我们只需要以下一行代码:
deeplab_pt_gpu_hybrid_amp = amp.convert_hybrid_block(deeplab_pt_gpu_hybrid, ctx=mx.gpu())
使用这个修改后的模型,处理数据集的时间如下:
Time (s): 3.371366024017334
正如我们所见,AMP 在 GPU 上产生了优秀的结果,将推理时间减少了大约~25%。这是因为 GPU 具有专门的电路来直接处理 float16,极大地改善了结果。
重要说明
amp.convert_hybrid_block()函数接受不同的参数。鼓励你尝试不同的选项(如cast_optional_params)以找到最佳配置。
使用 Int8 进行量化
在前面的章节中,我们看到如何通过使用不同的方法来优化推理循环,优化如何使用 CPU 和 GPU 以获得最大性能,给定一个模型。我们还探讨了如何利用单精度(float32)和半精度(float16)数据类型。在本节中,我们将探讨如何通过一种新的数据类型 Int8 来优化我们的数据输入、模型参数以及它们之间的不同算术运算。
这种数据类型的修改比精度变化更有深远的影响。我们还将底层表示从浮点数修改为整数,这样可以减少内存和计算要求。让我们分析一下这种数据类型。
Int8 表示两件事:它是一种仅支持整数数字(没有浮动小数点)的数据类型,并且在这种格式下存储单个数字所用的位数是 8 位。此格式的最重要特性如下:
-
能表示从-128 到 127,或从 0 到 255 的整数(取决于它是有符号还是无符号类型)
-
常数精度(每个连续的数字相差恰好 1)
为了解释Int8量化的核心思想,并展示精度损失,我们可以用以下代码片段显示数字 1/3(即三分之一)在Float32和Int8两种格式下的近似值:
a = mx.nd.array([1/3], dtype=mx.np.float32)
int_value = 85
scaling_factor = 255
b = int_value / scaling_factor
print("1/3 as 0.333... (Float32): {0:.30f}".format(a.asscalar())))
print("1/3 as 85/255 (Int8) : {0:.30f}".format(b))
这产生了以下结果:
1/3 as 0.333... (Float32): 0.333333343267440795898437500000
1/3 as 85/255 (Int8) : 0.333333333333333314829616256247
如我们所见,所有表示方式都不是完全精确的,float32表现出了非常高的精度,符合预期。使用Int8时,我们做了一个小的简化;我们使用了两个 8 位整数,85和255,并用其中一个作为缩放因子。这个缩放因子通常会同时应用于多个数字集。它可以是整个模型的相同缩放因子(不太可能),也可以是每层的缩放因子,等等。这个缩放因子不需要以Int8表示,它可以是float32。
重要提示
对于这个特定的例子,选择的Int8表示比目标数值更精确,但这只是巧合。在常见的场景中,存在精度损失,进而导致性能损失。
为了最小化性能损失,通常量化调优技术会要求一个校准数据集。然后,使用该数据集来计算减少性能损失的参数。
除了使用校准数据集外,还有一些技术可以优化最准确的Int8值的计算,而 MXNet 提供了一个非常简单的 API 来促进我们网络的优化。通过简单调用mx.contrib.quantization.quantize_net_v2()函数,我们将更新我们的网络为Int8。
特别是,对于我们的实验,这就是我们使用的调用:
deeplab_pt_cpu_q_hybrid = quantization.quantize_net_v2(
deeplab_pt_cpu,
quantized_dtype='auto',
exclude_layers=None,
exclude_layers_match=None,
calib_data=ade20k_cal_loader_gpu_cpu,
calib_mode='entropy',
logger=logger,
ctx=mx.cpu())
重要提示
Int8量化是一个复杂的过程,针对特定应用的定制需要深入的分析和一些反复试验。关于涉及的参数,建议阅读以下函数文档:github.com/apache/mxnet/blob/v1.9.1/python/mxnet/contrib/quantization.py#L825。
使用这个修改后的基于 CPU 的模型,处理数据集所需的时间如下:
Time (s): 36.10324692726135
正如我们所看到的,Int8在 CPU 上产生了显著的提升,几乎减少了约 50%的运行时间。
不幸的是,对于 GPU,这个特性无法引入。尽管最近的 GPU 有专门的Int8电路,但这还是一个比较新的发展,MXNet 尚不支持这些操作符。
对我们的模型进行分析
在这个配方中,我们已经看到如何使用不同的技术来优化推理循环。然而,有时即使引入了这些优化技术,我们的模型仍可能无法达到我们预期的运行时性能。这可能是由于以下几个原因:
-
架构并不适合边缘计算。
-
操作符尚未得到充分优化。
-
组件之间的数据传输。
-
内存泄漏。
为了验证我们的模型内部是如何工作的,检查需要进一步优化的地方和/或调查模型性能不佳的可能原因,MXNet 提供了一种低级分析工具,称为 MXNet 性能分析器。
MXNet 性能分析器在后台运行,实时记录模型中发生的所有操作和数据传输。它也非常轻量,占用的资源非常少。最重要的是,它极易配置和使用。
为了分析一组语句,我们需要采取两个步骤:
-
配置性能分析器。
-
在要进行性能分析的语句之前和之后启动与停止性能分析器。
要配置性能分析器,我们只需要一行代码,如下所示:
mx.profiler.set_config(
profile_all=True,
aggregate_stats=True,
continuous_dump=True,
filename='profile_output_cpu.json')
要启动和停止性能分析器,我们需要在要分析的语句的开始和结束处添加以下几行代码:
mx.profiler.set_state('run')
[... code statements to analyze ...]
# Wait until all operations have completed
mx.nd.waitall()
# Stop recording
mx.profiler.set_state('stop')
# Log results
mx.profiler.dump()
请注意,我们需要三条语句来停止录制:完成所有指令、停止录制,并转储在第一步中配置的文件的信息。
重要说明
请注意代码中我们如何使用 mx.nd.waitall() 函数来验证所有计算已严格完成,然后再计算这些操作所花费的时间。
如前所述的指令会生成一个 JSON 文件,随后可以通过追踪应用程序进行分析。我推荐使用 Google Chrome 中包含的 Tracing 应用程序,因为它非常易于使用和访问。只需在地址栏中输入以下内容:chrome://tracing。
为了验证 MXNet 性能分析器的功能,我们以广泛使用的ResNet架构为例,这些架构在我们的图像分割任务中被广泛应用,作为我们所使用的 DeepLabv3 网络的骨干。
ResNet 网络的典型架构如下:
图 9.1 – ResNet50 模型架构
请注意,在图 9.1中,初始步骤(阶段 1)是卷积、批量归一化和激活。
从我们分析过的模型中,Google Chrome Tracing 应用程序提供了以下屏幕:
图 9.2 – 性能分析 ResNet
在图 9.2中,我们可以看到模型的一般执行情况。放大早期层,我们可以看到如下:
图 9.3 – 性能分析 ResNet: 放大
在图 9.3中,我们可以看到阶段 1 的卷积、批量归一化和激活步骤被清晰地展示出来。我们还可以非常清楚地看到,批量归一化操作所需的时间大约是卷积和激活步骤的 4 倍,这可能表明了一个优化的方向。
它的工作原理……
在本节中,我们深入探讨了 MXNet 和 Gluon 如何帮助我们优化推理循环。我们通过解决推理循环中的每一个步骤,充分利用了我们的硬件(CPU 和 GPU):
-
重用了上一章中数据加载的工作
-
通过混合化优化图计算
-
分析了不同的数据类型,并在可能的情况下,使用 AMP 结合
float16的加速与float32的精度,利用 GPU 的特定电路 -
通过使用 Int8 量化迈出了进一步的步伐
-
使用 MXNet 分析器分析了低级性能
我们通过运行多个实验,比较了每个特性的效果,比较了在特定优化前后的性能,强调了使用这些优化时需要考虑的潜在权衡。总结来说,结果如下:
| 特性 | CPU 上的结果(毫秒) | GPU 上的结果(毫秒) |
|---|---|---|
| 标准 | 115 | 13.3 |
| 混合化 / 默认 | 65 | 12.9 |
| 混合化 / MKLDNN | 56 | N/A |
| 混合化 / MKLDNN + 静态分配 | 54 | 12.8 |
| 混合化 / MKLDNN + 静态分配 + 不变形状 | 52 | 12.6 |
| AMP | 54 | 3.5 |
| Int8 量化 | 36 | N/A |
表 9.1 – CPU 和 GPU 的特性及结果汇总
在接下来的食谱中,我们将同时应用所有这些优化技术,针对 CPU(MKL-DNN + 静态分配 + 不变形状 + Int8 量化)和 GPU(静态分配 + 不变形状 + 自动混合精度)优化两项常见任务:图像分割和文本翻译。
还有更多…
本食谱中展示的所有优化特性都在文献中进行了详细描述。在本节中,我们分享了一些入门链接,以便深入理解每个特性:
-
混合化:
mxnet.apache.org/versions/1.9.1/api/python/docs/tutorials/packages/gluon/blocks/hybridize.html -
自动混合精度(AMP):
mxnet.apache.org/versions/1.9.1/api/python/docs/tutorials/packages/gluon/blocks/hybridize.html -
Int8 量化:
mxnet.apache.org/versions/1.9.1/api/python/docs/tutorials/packages/gluon/blocks/hybridize.html -
MXNet 分析器:
mxnet.apache.org/versions/1.9.1/api/python/docs/tutorials/packages/gluon/blocks/hybridize.html
优化图像分割的推理
在之前的食谱中,我们展示了如何利用 MXNet 和 Gluon 来优化模型推理,应用了不同的技术,例如使用混合化提高运行时性能;如何结合 AMP 使用半精度(float16)显著减少推理时间;以及如何利用 Int8 量化等数据类型进一步优化。
现在,我们可以重新审视本书中一直在处理的一个问题:图像分割。我们在前几章的食谱中已经处理过这个任务。在食谱 4,《使用 MXNet Model Zoo 进行语义图像分割—PSPNet 和 DeepLabv3》中,来自第五章《使用计算机视觉分析图像》,我们介绍了这个任务以及我们将在本食谱中使用的数据集,MS COCO 和 Penn-Fudan Pedestrian,并学习了如何使用来自 GluonCV Model Zoo 的预训练模型。
此外,在食谱 3,《提升图像分割性能》中,来自第七章《通过迁移学习和微调优化模型》中,我们比较了处理目标数据集时可以采取的不同方法,是否从头开始训练我们的模型,或者利用预训练模型的先验知识并针对我们的任务进行调整,使用不同的迁移学习和微调方式。最后,在食谱 2,《优化图像分割训练》中,来自第八章《通过 MXNet 提升训练性能》中,我们应用了不同的技术来提升训练循环的运行时性能。
因此,在本食谱中,我们将应用所有介绍的优化技术,专注于优化图像分割模型的推理任务。
准备工作
和之前的章节一样,在本食谱中,我们将使用一些矩阵运算和线性代数,但这绝对不难。
如何操作...
在这个食谱中,我们将使用以下步骤:
-
应用推理优化技术
-
可视化和分析我们的模型
-
将我们的模型导出到 ONNX 和 TensorRT
让我们深入探讨每个步骤。
应用推理优化技术
在食谱 1,《介绍推理优化功能》中,我们展示了不同的优化技术如何提高推理过程中各个步骤的性能,包括混合化、AMP 和 Int8 量化。
在这一部分,我们将展示如何仅通过几行代码,在 MXNet 和 Gluon 中轻松应用我们介绍的每一项技术,并验证每项技术的结果。
如果不应用这些优化技术,作为基准,以下是使用 CPU 获取的定量结果:
PixAcc: 0.9602144097222223
mIoU : 0.4742364603465315
Time (s): 27.573920726776123
我们可以显示图像以获取定性结果:
图 9.4 – 定性结果:CPU 基线
从定量指标可以预见,图 9.4也显示了出色的结果。
正如之前的食谱所总结的,对于 CPU 上的最大性能,最佳方法是:
-
使用混合化:使用 Intel MKL-DNN 后端,结合静态内存分配和不变输入形状。
-
不要使用 AMP。
-
使用 Int8 量化。
让我们将这些技术应用于我们当前的特定任务——图像分割。
对于混合化,我们只需要一行代码(其中包括 Intel MKLDNN后端所需的参数,结合静态内存分配和不变输入形状):
deeplab_pt_cpu_q_hybrid.hybridize(backend="MKLDNN", static_alloc=True, static_shape=True)
我们不需要添加 AMP 步骤,因为它在 CPU 工作负载中未能提供任何好处。
对于Int8量化,我们需要两个独立的步骤。一方面,我们需要定义校准数据集。这可以通过少量代码实现:
# Dataset Loading & Transforming
# Limit to 10 samples (last ones)
max_samples = 10
samples = range(0, max_samples)
p_cal_cpu_pre = mx.gluon.data.SimpleDataset([(pedestrian_val_dataset[-i][0], pedestrian_val_dataset[-i][1]) for i in tqdm(samples)])
p_cal_gpu_cpu = p_cal_cpu_pre.transform_first(input_transform_fn_gpu_cpu, lazy=False)
# DataLoader for Calibration
# For CPU, Pre-processed in GPU, copied back to CPU memory space)
num_workers = 0
batch_size = 4
p_cal_loader_gpu_cpu = mx.gluon.data.DataLoader(
p_cal_gpu_cpu, batch_size=batch_size,
num_workers=num_workers,
last_batch="discard")
然后,为了应用Int8量化,并使用校准数据集进行优化,只需再加一行代码:
deeplab_pt_cpu_q_hybrid = quantization.quantize_net_v2(
deeplab_pt_cpu,
quantized_dtype='auto',
exclude_layers=None,
exclude_layers_match=None,
calib_data=p_cal_loader_gpu_cpu,
calib_mode='entropy',
logger=logger,
ctx=mx.cpu())
应用这些优化技术,得到的优化 CPU 推理定量结果如下:
PixAcc: 0.9595597222222222
mIoU : 0.47379941937958425
Time (s): 8.355125904083252
正如我们所见,性能差异(0.959对比0.960和0.473对比0.474)可以忽略不计。然而,通过这些推理优化技术,我们已经将推理运行时减少了 4 倍(8.4 秒对比 27.6 秒),这是一个令人印象深刻的结果。
我们也可以显示一张图像来展示定性结果:
图 9.5 – 定性结果:CPU 优化推理
从定量指标可以预见,图 9.5也显示了出色的结果,差异几乎可以忽略不计(如果有的话)。
那么,基于 GPU 的推理怎么样呢?让我们按照相同的步骤进行:
-
如果不应用这些优化技术,作为基线,这些是使用 GPU 获得的定量结果:
PixAcc: 0.9602144097222223 mIoU : 0.4742364603465315 Time (s): 13.068315982818604正如预期的那样,与 CPU 基线相比,算法性能没有变化。GPU 的推理运行时确实是 CPU 的两倍快(
13.1秒对比27.6秒)。 -
我们可以显示一张图像来展示定性结果:
图 9.6 – 定性结果:GPU 基线
从定量指标可以预见,图 9.6也显示了出色的结果。
正如之前的食谱所总结的,对于 GPU 上的最大性能,最佳方法是:
-
使用混合化:使用静态内存分配和不变输入形状。不要使用 Intel MKL-DNN 后端。
-
使用 AMP**。**
-
不要使用 Int8 量化(不支持)。
让我们将这些技术应用于我们当前的特定任务——图像分割。
对于混合化,我们只需要一行代码(其中包括静态内存分配和不变输入形状所需的参数):
deeplab_pt_gpu_hybrid.hybridize(static_alloc=True, static_shape=True)
对于 AMP,我们需要遵循两个简单的步骤,即前向传播和模型的转换,如下所示:
deeplab_pt_gpu_hybrid(single_sample_gpu);
deeplab_pt_gpu_hybrid_amp = amp.convert_hybrid_block(deeplab_pt_gpu_hybrid, ctx=mx.gpu())
不需要进一步的步骤。
通过应用这些优化技术,我们获得了优化 GPU 推断的量化结果:
PixAcc: 0.9602565972222222
mIoU : 0.4742640561133744
Time (s): 0.8551054000854492
正如我们所看到的,性能上的差异(0.960与0.960,以及0.474与0.474)是不存在的。此外,通过这些推断优化技术,我们已经成功将推断运行时间缩短了 15 倍(0.85 秒与 13.1 秒),这是一个令人印象深刻的成果。
我们还可以显示一张图片来展示定性结果:
图 9.7 – 定性结果:GPU 优化推断
正如定量指标所预期的那样,图 9.7也显示了出色的结果,与图 9.6中的结果几乎没有(如果有的话)可忽略的差异。
可视化和剖析我们的模型
在前几节中,我们看到了可以应用于优化推断循环的不同技术,以及这些技术所取得的结果。但是,这些技术究竟是如何工作的?为什么它们更快?
我们将使用 MXNet 提供的两个工具来达到这个目的:
-
模型可视化
-
模型剖析
模型可视化为我们提供了一种直观的方式来看待不同层之间的交互。对于使用 ResNet 骨干(例如我们在本文档中用于图像分割的DeepLabv3),这尤为重要,因为残差通过层进行传递。
用 MXNet 可视化我们的模型架构非常简单。当使用符号模型时,只需一行代码即可。在我们的情况下,由于我们使用 Gluon 模型,需要以下几行代码:
deeplab_pt_cpu_q_hybrid.export('deeplab_pt_cpu_q_hybrid_sym')
sym, arg_params, aux_params = mx.model.load_checkpoint('deeplab_pt_cpu_q_hybrid_sym', 0)
mx.visualization.plot_network(sym)
正如前一篇文章所示,基于 ResNet 的网络由 ResNet 块组成,其中包括卷积、批量归一化和激活步骤。
对于我们的 CPU 优化模型(混合化和Int8量化),以下是这些块之间部分连接的样子:
图 9.8 – ResNet 块的 GraphViz(CPU 优化)
正如我们在图 9.8中所看到的,预期的 ResNet 块操作没有单独的块;它们都是执行所有计算的单个块的一部分。这些操作的组合称为运算符融合,其中尽可能多的操作被融合在一起,而不是计算一个操作,然后是下一个操作(通常发生数据传输)。最大的好处在于融合的操作可以在相同的内存空间中进行。这是混合化执行的优化之一,因为一旦网络的图形完成,很容易找到候选融合操作。
好的,模型可视化告诉我们这些优化会发生,但我们如何验证它们实际上正在发生呢?这正是模型性能分析擅长的地方,也可以帮助我们了解运行时发生的问题。
如食谱中所述,引入推理优化功能,以及“对我们模型进行性能分析”一节中提到,模型性能分析的输出是一个 JSON 文件,可以使用 Google Chrome Tracing 应用等工具进行可视化。对于一个未优化的 CPU 工作负载,我们的 DeepLabv3 模型显示了以下的时间配置文件:
图 9.9 – DeepLabv3 性能分析:未优化的 CPU 工作负载
在 图 9.9 中,我们可以看到以下特征:
-
几乎所有任务都由单个进程处理。
-
在操作开始约 80 毫秒后,所有任务已被派发,并且控制已返回,操作继续进行(延迟评估和
mx.nd.waitall)。 -
所有任务在操作开始约 80 毫秒时已被派发,并且控制已返回,操作继续进行(延迟评估和 mx.nd.waitall)。
-
所有操作都是原子操作,并逐个执行。
-
完整的操作大约需要 800 毫秒。
对于一个 CPU 优化的工作负载,我们的 DeepLabv3 模型显示了以下的时间配置文件:
图 9.10 – DeepLabv3 性能分析:优化后的 CPU 工作负载
在 图 9.10 中,我们可以看到以下特征:
-
几乎所有任务都由单个进程处理,类似于未优化的版本。
-
在操作开始约 5 毫秒后,所有任务已被派发,并且控制已返回,操作继续进行(延迟评估和
mx.nd.waitall),比未优化版本要快得多。 -
内存的使用是同步/结构化的,这与未优化的版本形成鲜明对比。
-
所有操作都被融合在一起,这与未优化的版本形成鲜明对比。
-
完整的操作大约需要 370 毫秒。
总结来说,对于基于 CPU 的优化,我们可以清晰地看到效果:
-
混合化将所有操作符融合在一起,基本上在一个操作中执行几乎全部工作负载。
-
MKL-DNN 后端和 Int8 量化通过加速操作改善了这些操作。
对于我们的 GPU 优化模型(混合化并使用 AMP),以下是一些 ResNet 模块之间连接的情况:
图 9.11 – ResNet 模块的 GraphViz(GPU 优化)
如 图 9.11 所示,这与 CPU 优化后的可视化完全不同,因为所有预期的 ResNet 模块操作的独立块都可以被识别出来。正如本章第一个食谱中所提到的,混合化对 GPU 的影响非常有限。
那么,性能加速是从哪里来的呢?让我们借助模型性能分析来解答。
对于未优化的 GPU 工作负载,我们的 DeepLabv3 模型显示出以下时间轮廓:
图 9.12 – 深度分析 DeepLabv3:未优化的 GPU 工作负载
在图 9.12中,我们可以看到以下特点:
-
几乎所有任务都由两个 GPU 进程处理。
-
在操作开始约 40 毫秒时,所有任务已被发送进行调度,控制返回以继续操作(懒评估和
mx.nd.waitall)。 -
内存的异步/非结构化使用。
-
所有操作都是原子性的并单独执行。
-
完整操作大约需要 150 毫秒。
对于优化过的 GPU 工作负载,我们的 DeepLabv3 模型显示出以下时间轮廓:
图 9.13 – 深度分析 DeepLabv3:优化过的 GPU 工作负载
在图 9.13中,我们可以看到以下特点:
-
几乎所有任务都由两个进程处理,类似于未优化的版本。
-
在操作开始约 4 毫秒时,所有任务已被发送进行调度,控制返回以继续操作(懒评估和
mx.nd.waitall),比未优化的版本快得多。 -
内存的同步/结构化使用,与未优化的版本形成鲜明对比。
-
所有操作都是原子性的并单独执行;与未优化的版本类似,只是速度更快。例如,大型卷积操作在 GPU 未优化的情况下大约需要 1 毫秒,而在 GPU 优化的情况下只需要三分之一的时间(约 0.34 毫秒)。
-
完整操作大约需要 55 毫秒(为未优化时间的三分之一)。
总结来说,对于基于 GPU 的优化,我们可以清晰地看到效果:
-
如预期的那样,混合化没有效果,也没有发现操作融合。
-
如果 GPU 具有专用的 float16 电路,AMP 使得操作运行得更快。
将我们的模型导出到 ONNX 和 TensorRT
MXNet 和 GluonCV 也提供了将我们的模型导出到外部的工具。这对于优化运行时计算时间(推理)最为有用,可能需要:
-
MXNet/GluonCV 可能不支持的特定算法
-
在特定硬件平台上的部署和优化
在这一部分,我们将研究每个类别的一个示例。
对于特定的算法,我们将把我们的模型导出为 ONNX 格式。ONNX代表开放神经网络交换格式,它是一个开放的格式,描述了深度学习模型如何存储和共享。这对于利用特定工具执行高度专业化任务极为有用。例如,ONNX Runtime拥有强大的推理工具,包括量化(例如,ONNX Runtime 支持基于 GPU 的 INT8 量化)。因此,我们可以将模型导出为 ONNX 格式,并直接开始使用 ONNX Runtime 进行工作。
和往常一样,MXNet 只需几行代码就能帮助我们完成这个任务。我们需要执行两个步骤。首先,我们需要将模型从 Gluon 格式转换为符号格式(先混合,再导出):
deeplab_pt_gpu_hybrid.hybridize(static_alloc=True, static_shape=True)
deeplab_pt_gpu_hybrid(single_sample_gpu)
# Need to be exported externally for the symbols to be loaded
deeplab_pt_gpu_hybrid_filename = "deeplab_resnet101_coco_pt_gpu_hybrid"
deeplab_pt_gpu_hybrid.export(deeplab_pt_gpu_hybrid_filename)
接下来,我们可以将符号模型转换为 ONNX:
# Files exported
sym_filename = deeplab_pt_gpu_hybrid_filename + "-symbol.json"
params_filename = deeplab_pt_gpu_hybrid_filename + "-0000.params"
in_shapes = [single_sample_gpu.shape]
in_types = [mx.np.float32]
onnx_model_path = mx.onnx.export_model(
sym_filename,
params_filename,
in_shapes,
in_types,
onnx_file_name)
ONNX 还提供了一个检查器,用于验证我们的模型是否正确导出。可以使用以下代码行来完成:
# Model Verification
import onnx
# Load the ONNX model
onnx_model = onnx.load_model(onnx_model_path)
# Check the ONNX graph
onnx.checker.check_graph(onnx_model.graph)
就这样!按照这些指示,我们将把 ONNX 模型存储在文件中(在我们的示例中为 'deeplab_resnet101_coco_pt_gpu_hybrid.onnx'),并准备好在任何接受 ONNX 模型作为输入的工具中使用。
另一方面,有时我们希望在特定硬件平台上部署和/或优化我们的模型,例如 NVIDIA 系列产品(例如,Nvidia Jetson 平台)。具体来说,Nvidia 提供了一个特定的机器学习框架,用于在其硬件上运行推理。这个框架叫做TensorRT。
尽管 MXNet 提供了直接的 TensorRT 集成,但默认情况下并未启用,需要从源代码直接构建 MXNet,并启用特定参数来支持 TensorRT 集成。更为直接的是,我们可以利用刚才描述的 ONNX 导出,生成一个支持 TensorRT 的模型。
为了实现这一点,只需写几行代码:
import tensorrt as trt
trt_file_name = "deeplab_resnet101_coco_pt_gpu_hybrid.trt"
TRT_LOGGER = trt.Logger(trt.Logger.INFO)
builder = trt.Builder(TRT_LOGGER)
config = builder.create_builder_config()
explicit_batch = 1 << (int) (trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)
deeplab_pt_gpu_hybrid_trt = builder.create_network(explicit_batch)
with open(onnx_file_name, 'rb') as model:
with trt.OnnxParser(deeplab_pt_gpu_hybrid_trt, TRT_LOGGER) as parser:
assert parser.parse(model.read()) == True
deeplab_pt_gpu_hybrid_engine_serialized = builder.build_serialized_network(deeplab_pt_gpu_hybrid_trt, config=config)
with open(trt_file_name, 'wb') as f:
f.write(bytearray(deeplab_pt_gpu_hybrid_engine_serialized))
有了这个,我们将编写一个序列化的 TensorRT 可用模型。
我们可以通过反序列化和读取模型来验证它是否可以被正确读取。我们可以使用以下代码行做到这一点:
# Check it can be read back
runtime = trt.Runtime(TRT_LOGGER)
with open(trt_file_name, 'rb') as f:
deeplab_pt_gpu_hybrid_engine_deserialized = runtime.deserialize_cuda_engine(f.read())
就这样!在这一节中,我们已经成功地编写了 ONNX 和 TensorRT 模型。恭喜!
它是如何工作的……
在本教程中,我们应用了本章第一节中提到的不同推理优化技术,利用我们的硬件(CPU 和 GPU)通过以下方式优化模型的运行时性能:
-
混合模型
-
利用 AMP
-
使用 INT8 数据类型进行量化以加速推理
此外,我们还学习了如何使用模型可视化工具(由 GraphViz 提供支持)和 MXNet 分析器,并利用这些工具从低级角度分析推理优化。
最后,我们学会了如何将模型导出到特定场景和目的,使用 ONNX 和 TensorRT 库。
还有更多……
在本教程中,我们从训练后角度提出了推理优化问题。我们拿到了一个(预)训练的模型,并尽可能挖掘它的性能。
然而,还有另一条可以探索的途径,那就是从机器学习模型设计的角度开始思考如何最大化推理性能。这就是所谓的模型压缩,它是一个活跃的研究领域,定期会发布很多改进。近期的活跃研究课题包括:
-
量化感知训练:
arxiv.org/pdf/1712.05877.pdf
优化从英语翻译到德语的推理
在最初的食谱中,我们展示了如何利用 MXNet 和 Gluon 优化模型的推理过程,应用了不同的技术:通过混合化提高运行时性能;如何结合 AMP 使用半精度(float16)显著减少推理时间;以及如何通过数据类型(如 Int8 量化)进一步优化。
现在,我们可以重新审视本书中一直在讨论的一个问题:将英语翻译成德语。我们在前几章的食谱中处理过翻译任务。在第六章《利用自然语言处理理解文本》的食谱 4《将越南语文本翻译成英语》中,我们介绍了翻译文本的任务,同时学习了如何使用来自 GluonCV Model Zoo 的预训练模型。
此外,在第七章《通过迁移学习和微调优化模型》的食谱 4《提高英语到德语翻译的性能》中,我们介绍了在本食谱中将使用的数据集:WMT 2014 和 WMT 2016。我们还比较了处理目标数据集时可以采用的不同方法:从零开始训练我们的模型,或利用预训练模型的过去知识,并根据我们的任务进行调整,使用不同的迁移学习和微调方式。最后,在第八章《通过 MXNet 提高训练性能》的食谱 3《优化英语到德语翻译的训练》中,我们应用了不同的技术来提高训练循环的运行时性能。
因此,在本食谱中,我们将应用所有介绍过的优化技术,专门用于优化从英语到德语翻译的推理过程。
做好准备
与前几章一样,在本食谱中我们将使用一些矩阵运算和线性代数,但这并不难。
如何操作...
在本食谱中,我们将进行以下步骤:
-
应用推理优化技术
-
对我们的模型进行分析
-
导出我们的模型
让我们深入探讨这些步骤。
应用推理优化技术
在本章开始的食谱 1《引入推理优化功能》中,我们展示了不同的优化技术如何改善机器学习模型推理过程中各个步骤的性能,包括混合化、AMP 和 Int8 量化。
在本节中,我们将展示如何通过 MXNet 和 Gluon,仅用几行代码,我们就能轻松应用每个介绍过的技术,并验证每项技术的结果。
如果不应用这些优化技术,作为基准,以下是使用 CPU 获得的定量结果:
WMT16 test loss: 1.53; test bleu score: 26.40
Time (s): 373.5446252822876
从定性角度来看,我们还可以检查模型在一个句子示例上的表现。在我们的案例中,我们选择了 I learn new things every day,并且获得的输出如下:
Qualitative Evaluation: Translating from English to German
Expected translation:
Ich lerne neue Dinge.
In English:
I learn new things every day.
The German translation is:
Ich lerne neue Dinge, die in jedem Fall auftreten.
输出中获得的德语句子(Ich lerne neue Dinge, die in jedem Fall auftreten)的意思是 我学习在每种情况下都会出现的新事物,因此,正如结果所示,文本几乎被完美地从英语翻译成了德语。
正如前一个方案中总结的,为了在 CPU 上实现最大性能,最佳的方法如下:
-
使用混合化:使用 Intel MKL-DNN 后端,结合静态内存分配和不变输入形状。
-
不要使用 AMP。
-
使用 Int8 量化。
很遗憾,我们无法使用 Int8 量化,因为它不支持 GluonNLP 模型。
让我们为当前特定任务应用每一项技术,即从英语翻译成德语。
对于混合化,我们只需要几行代码(包括所需的参数,这些参数是针对 Intel MKL-DNN 后端的,结合静态内存分配和不变输入形状,同时对损失函数进行混合化):
wmt_transformer_pt_cpu_hybrid.hybridize(backend="MKLDNN", static_alloc=True, static_shape=True)
loss_function = nlp.loss.MaskedSoftmaxCELoss()
loss_function.hybridize(backend="MKLDNN", static_alloc=True, static_shape=True)
我们不需要添加与 AMP 相关的任何步骤,因为已经证明它对基于 CPU 的工作负载没有益处。同样,GluonNLP 不支持 Int8 量化,因此我们不需要对代码做任何进一步的修改。
应用这些优化技术后,以下是优化后的 CPU 推理所获得的定量结果:
WMT16 test loss: 1.53; test bleu score: 26.40
Time (s): 312.5660226345062
正如我们所看到的,性能差异(损失函数为 1.53 与 1.53,BLEU 分数为 26.40 与 26.40)几乎可以忽略不计。然而,使用这些推理优化技术,我们成功将推理运行时间缩短了 20%(313 秒比 374 秒),这是一个非常好的结果。
从定性角度来看,我们还可以检查模型在一个句子示例上的表现。在我们的案例中,我们选择了 I learn new things every day,并且获得的输出如下:
Qualitative Evaluation: Translating from English to German
Expected translation:
Ich lerne neue Dinge.
In English:
I learn new things every day.
The German translation is:
Ich lerne neue Dinge, die in jedem Fall auftreten.
输出中获得的德语句子(Ich lerne neue Dinge, die in jedem Fall auftreten)的意思是 我学习在每种情况下都会出现的新事物,因此,正如结果所示,文本几乎被完美地从英语翻译成了德语。此外,结果与未优化的情况相同(如预期)。
那么,基于 GPU 的推理如何呢?我们来按照相同的步骤操作:
-
如果不应用这些优化技术,作为基准,以下是使用 GPU 获得的定量结果:
WMT16 test loss: 1.53; test bleu score: 26.40 Time (s): 61.67868137359619正如预期的那样,相对于 CPU 基线,算法性能没有变化。推理运行时间在 GPU 上确实是 CPU 的六倍快(61.7 秒对比 374 秒)。
-
从定性角度来看,我们还可以通过一个句子示例检查我们的模型表现如何。在我们的例子中,我们选择了 I learn new things every day,输出结果如下:
Qualitative Evaluation: Translating from English to German Expected translation: Ich lerne neue Dinge. In English: I learn new things every day. The German translation is: Ich lerne neue Dinge, die in jedem Fall auftreten.输出中获得的德语句子(Ich lerne neue Dinge, die in jedem Fall auftreten)的意思是 我学习每天都出现的新事物,因此从结果来看,文本几乎被完美地从英语翻译成了德语(并且与两个 CPU 情况相等)。
正如在前面的步骤中总结的那样,为了在 GPU 上获得最大性能,最佳方法如下:
-
使用混合化:使用静态内存分配和不变输入形状。不要使用 Intel MKL-DNN 后端。
-
使用 AMP。
-
不使用 Int8 量化(不支持)。
不幸的是,由于 GluonNLP 模型不支持 AMP,我们将无法使用 AMP。
让我们将这些技术应用于我们当前的具体任务,从英语翻译成德语。
对于混合化,我们只需要几行代码(包括静态内存分配和不变输入形状所需的参数,以及损失函数的混合化):
wmt_transformer_pt_gpu_hybrid.hybridize(static_alloc=True, static_shape=True)
loss_function = nlp.loss.MaskedSoftmaxCELoss()
loss_function.hybridize(static_alloc=True, static_shape=True)
我们不需要添加与 AMP 或 Int8 量化相关的任何步骤,因为 GluonNLP 不支持这些功能。因此,不需要进一步的步骤。
通过应用这些优化技术,以下是优化 GPU 推理后的定量结果:
WMT16 test loss: 1.53; test bleu score: 26.40
Time (s): 56.29795598983765
正如我们所看到的,性能差异(损失为 1.53 与 1.53,BLEU 得分为 26.40 与 26.40)是微不足道的。然而,通过这些推理优化技术,我们已经能够将推理运行时间减少了 10%(56.3 秒对比 61.7 秒),这是一个非常好的结果。
从定性角度来看,我们还可以通过一个句子示例检查我们的模型表现如何。在我们的例子中,我们选择了 I learn new things every day,输出结果如下:
Qualitative Evaluation: Translating from English to German
Expected translation:
Ich lerne neue Dinge.
In English:
I learn new things every day.
The German translation is:
Ich lerne neue Dinge, die in jedem Fall auftreten.
输出中获得的德语句子(Ich lerne neue Dinge, die in jedem Fall auftreten)的意思是 我学习每天都出现的新事物,因此从结果来看,文本几乎被完美地从英语翻译成了德语。而且,结果与非优化情况(如预期)相当。
对我们的模型进行分析
在前面的章节中,我们看到了可以应用于优化推理循环的不同技术,以及这些技术所取得的结果。然而,这些技术究竟是如何工作的?为什么它们更快?
在本节中,我们将使用 MXNet 分析器,这有助于我们理解在运行时发生的问题。
如初始部分所述,模型性能分析的输出是一个 JSON 文件,可以使用诸如 Google Chrome Tracing 应用等工具进行可视化。对于未优化的 CPU 工作负载,我们的 Transformer 模型显示以下时间分析:
图 9.14 – Transformer 性能分析:未优化的 CPU 工作负载
在图 9.14中,我们可以看到以下特点:
-
几乎所有的任务都由两个进程处理。
-
几乎没有等待时间(惰性计算和
mx.nd.waitall)。 -
内存的同步/结构化使用。
-
所有操作都是原子操作,并且逐个执行。
-
完整操作大约需要 1,200 毫秒。
对于经过 CPU 优化的工作负载,我们的 Transformer 模型显示以下时间分析:
图 9.15 – Transformer 性能分析:优化后的 CPU 工作负载
在图 9.15中,我们可以看到以下特点:
-
几乎所有的任务都由两个进程处理,类似于未优化的情况。
-
几乎没有等待时间(惰性计算和
mx.nd.waitall),与未优化的情况类似。 -
与未优化的情况相比,内存以更异步/结构化的方式使用。
-
一些操作被融合在一起。尽管可视化不太清晰,但操作符融合(混合化)似乎在起作用,且大部分时间都花费在融合操作上。
-
完整操作大约需要 720 毫秒。
让我们仔细观察其中一个操作符融合步骤:
图 9.16 – Transformer 性能分析:优化后的 CPU 工作负载(聚焦于 OperatorFusion)
在图 9.16中,我们可以看到操作符融合如何将多个不同的操作融合在一起,包括嵌入、层归一化、全连接层和 MKL-DNN 加速的层。
总结来说,对于基于 CPU 的优化,我们可以清楚地看到以下效果:
-
混合化已经将大多数操作符融合在一起,尽管可视化较为困难,而且这种情况发生了多次。
-
MKL-DNN 后端通过加速的操作符改进了这些操作。
现在让我们讨论 GPU 情况。
对于未优化的 GPU 工作负载,我们的 Transformer 模型显示以下时间分析:
图 9.17 – Transformer 性能分析:未优化的 GPU 工作负载
在图 9.17中,我们可以看到以下特点:
-
任务主要由几个(三个)GPU 进程处理。
-
几乎没有等待时间(惰性计算和
mx.nd.waitall)。 -
内存逐渐增加。
-
所有操作都是原子操作,并且逐个执行。
-
有多个从/到 CPU 的拷贝,这似乎没有降低性能。
-
完整操作大约需要 580 毫秒。
对于 GPU 优化的工作负载,我们的 Transformer 模型展示了以下时间性能:
图 9.18 – 性能分析 Transformer:优化后的 GPU 工作负载
在图 9.18中,我们可以看到以下特点:
-
几乎所有任务都由三个进程处理,类似于未优化的版本。
-
几乎没有等待时间(懒加载和
mx.nd.waitall),与未优化版本类似。 -
与未优化版本相比,内存的异步/非结构化使用更多。
-
一些操作被融合在一起。尽管可视化不够清晰,但操作融合(混合化)似乎有效,绝大部分时间都花费在了融合的操作上。
-
从/到 CPU 的数据复制操作似乎不会影响性能,尽管有几个操作。
-
整个操作大约需要 260 毫秒。
让我们详细看一下其中一步操作融合的过程:
图 9.19 – 性能分析 Transformer:优化后的 GPU 工作负载(聚焦操作融合)
在图 9.19中,我们可以看到操作融合如何将几个不同的操作融合在一起,包括嵌入、层归一化和全连接层。
总结来说,对于基于 GPU 的优化,我们可以清楚地看到混合化的效果,所有操作都已被融合在一起,尽管可视化较难解读,而且这种情况发生了很多次。
导出我们的模型
MXNet 和 GluonNLP 也提供了导出模型的工具。然而,这些工具主要是为 MXNet/Gluon 的内部使用而设计。原因是 GluonNLP 主要处理 save() 函数。
这个函数可以很容易地调用:
wmt_transformer_pt_gpu_hybrid.save('transformer_pt_gpu_hybrid')
我们可以通过以下命令验证与模型相关的文件,参数(.params 扩展名)和架构(.json 扩展名)是否已被保存:
assert os.path.exists("transformer_pt_gpu_hybrid-model.params")
assert os.path.exists("transformer_pt_gpu_hybrid-model.json")
完成了!在这一节中,我们成功地导出了我们的 Transformer 模型。恭喜!
它是如何工作的...
在这个食谱中,我们应用了本章第一个食谱中看到的不同推理优化技术,利用我们的硬件(CPU 和 GPU)通过混合化模型来优化模型的运行性能。
此外,我们已经学习了如何使用 MXNet 性能分析器从低级别的角度分析推理优化。
最后,我们已经学会了如何使用 MXNet 内部库导出我们的模型。
还有更多…
在这个食谱中,我们从训练后的角度展示了推理优化问题。我们得到一个(预)训练好的模型,并尝试从中挤出尽可能多的性能。
然而,还有另一条可以探索的途径,这条途径从机器学习模型设计的角度思考如何最大化推理性能。已经发布了几种改进方法,展示了如何在没有大量计算工作负载的情况下使用大型语言模型(LLM),例如以下几种:
-
低排名适应(LORA):
arxiv.org/pdf/2012.13255.pdf -
LORA 与剪枝:
arxiv.org/pdf/2305.18403.pdf -
GPT4All(量化):
gpt4all.io -
Int4 量化:
arxiv.org/pdf/2301.12017.pdf