原文:
zh.annas-archive.org/md5/c7aabb3a9c13924ec60749e96c9ff05f
译者:飞龙
第八章:元数据和工件存储
本章内容包括:
-
在深度学习背景下理解和管理元数据。
-
设计一个元数据和工件存储来管理元数据
-
介绍两个开源的元数据管理工具:ML Metadata 和 MLflow。
要生成符合业务需求的高质量模型,数据科学家需要尝试各种数据集、数据处理技术和训练算法。为了构建和发布最佳模型,他们花费了大量时间进行这些实验。
模型训练实验产生了各种工件(数据集和模型文件)和元数据。元数据可能包括模型算法、超参数、训练指标和模型版本等,这些对分析模型性能非常有帮助。为了有用,这些数据必须是持久的和可检索的。
当数据科学家需要调查模型性能问题或比较不同的训练实验时,作为工程师,我们是否有什么可以做的来促进这些努力呢?例如,我们是否可以使模型的再现和实验比较更容易?
答案是肯定的。作为工程师,我们可以构建一个系统,保留数据科学家需要再现和比较模型的实验元数据和工件。如果我们设计这个存储和检索系统得当,并进行适当的元数据管理,数据科学家可以轻松地从一系列实验中选择最佳模型,或者快速找出模型退化的根本原因,而不需要深入了解元数据系统。
在前几章中,我们已经学习了设计服务来生成和提供模型。在这里,我们将注意力转向元数据和工件管理系统,这个系统有助于两个更重要的操作:故障排除和比较实验。
我们将从介绍工件和元数据开始这一章,并讨论这些概念在深度学习背景下的含义。然后我们将通过示例和强调设计原则来向您展示如何设计元数据管理系统。最后,我们将讨论两个开源的元数据管理系统:MLMD(ML Metadata)和 MLflow。通过阅读本章,您将清楚地了解如何管理元数据和工件,以便促进实验比较和模型故障排除。
8.1 介绍工件
人们经常假设在深度学习中,工件是模型训练过程产生的模型文件。这在某种程度上是正确的。工件实际上是组成模型训练过程中组件的输入和输出的文件和对象。这是一个关键的区别,如果你想设计一个支持模型再现性的系统,记住这个更广泛的定义是很重要的。
在这个定义下,工件可以包括用于深度学习项目的数据集、模型、代码或任何其他数量的对象。例如,原始输入训练数据、通过标记工具生成的带标签数据集以及数据处理流水线的结果数据都被认为是工件。
此外,为了进行性能比较、可重现性和故障排除,必须将工件与描述其事实和血统的元数据一起保存。在实践中,工件以原始文件的形式存储在文件服务器或云存储服务(例如 Amazon Simple Storage Service 或 Azure Blob Storage)上。我们将工件与其元数据关联在一个独立的存储服务上的元数据存储中。请参见图 8.1,了解此安排通常的外观。
图 8.1 显示了管理工件的常见做法。工件文件保存在文件存储系统中,并将其文件 URL 与其他相关元数据(例如模型训练执行 ID 和模型 ID)一起保存在元数据存储中。这样的设置允许我们 - 或数据科学家 - 在元数据存储中搜索模型,并轻松找到相应模型训练过程的所有输入和输出工件。
图 8.1 工件与其元数据相关联存储在元数据存储中。
8.2 深度学习上下文中的元数据
从一般意义上讲,元数据是结构化的参考数据,提供有关其他数据或对象的信息,例如包装食品上的营养事实标签。然而,在机器学习(ML)和深度学习中,元数据更具体地指的是模型的数据;它是描述模型训练执行(运行)、工作流、模型、数据集和其他工件的数据。
对于任何分布式系统,我们通过日志和度量标准以服务级别跟踪服务元数据。例如,我们可能跟踪 CPU 使用率、活跃用户数量和失败的 Web 请求数量等度量标准。我们使用这些度量标准进行系统/服务监控、故障排除和观察。
在深度学习系统中,除了服务级别指标之外,我们还收集用于模型故障排除、比较和重现的元数据。您可以将深度学习元数据视为系统中用于监视和跟踪每个深度学习活动的一种特殊子集的日志和度量标准。这些活动包括数据解析、模型训练和模型服务。
8.2.1 常见的元数据类别
虽然我们刚刚定义了元数据,但这个术语在某种程度上实际上是相当任意的;没有关于哪些数据应被视为元数据的固定准则。对于深度学习系统的工程师,我们建议将元数据定义分为以下四个类别:模型训练运行、通用工件、模型文件和编排工作流。为了让您对这些类别有一个具体的感觉,让我们来看看每个类别以及每个类别中包含的一些示例元数据。
模型训练运行的元数据
为了重现模型、分析模型性能并促进模型故障排除,我们需要跟踪模型训练运行的所有输入和输出数据和工件。这包括
-
数据集 ID 和版本—用于模型训练的数据集的唯一标识。
-
超参数—训练中使用的超参数,例如学习率和 epoch 数。
-
硬件资源—分配给训练的 CPU、GPU、TPU、内存和磁盘大小以及这些资源的实际消耗。
-
训练代码版本—用于模型训练的训练代码快照的唯一标识。
-
训练代码配置—用于重新创建训练代码执行环境的配置,例如 conda.yml、Dockerfile 和 requirement.txt。
-
训练指标—显示模型训练进展的指标,例如每个训练 epoch 的损失值。
-
模型评估指标—显示模型性能的指标,例如 F 分数和均方根误差(RMSE)。
通用工件的元数据
工件可以是任意文件,例如数据集、模型和预测结果。为了能够在工件存储中找到工件,我们希望跟踪工件的以下元数据:
-
文件位置—工件存储位置的路径,例如 Amazon S3 文件路径或内部文件系统路径
-
文件版本—用于区分不同文件更新的唯一标识
-
描述—用于描述工件文件内容的额外信息
-
审计历史—有关谁创建了工件版本、工件何时创建以及如何创建的信息
模型文件的元数据
模型是一种工件,但由于模型是每个深度学习系统的主要产品,我们建议将模型元数据与其他工件分开跟踪。当我们定义模型元数据时,最好考虑两个视角:模型训练和模型服务。
对于模型训练,为了有模型谱系,我们希望保留模型与产生它的模型训练运行之间的映射。模型谱系对于模型比较和重现很重要。例如,当比较两个模型时,通过具有模型训练运行和模型的链接,数据科学家可以轻松确定模型是如何产生的所有细节,包括输入数据集、训练参数和训练指标。模型训练指标对于了解模型性能非常有用。
对于模型服务,我们希望跟踪模型执行数据以用于未来的模型性能分析。这些执行数据,例如模型响应延迟和预测失误率,对于检测模型性能下降非常有用。
除了前述的通用工件元数据之外,以下是一些推荐的模型元数据类别:
-
资源消耗—用于模型服务的内存、GPU、CPU 和 TPU 消耗
-
模型训练运行——模型训练运行 ID,用于查找创建模型的代码、数据集、超参数和环境
-
实验——跟踪在生产环境中的模型实验活动,例如不同模型版本的用户流量分布
-
生产——模型在生产环境中的使用,例如每秒查询和模型预测统计
-
模型性能——跟踪模型评估指标以检测漂移,如概念漂移和性能漂移
注:不可避免地,一旦模型上线,它们将开始表现得更差。我们称此行为为模型退化。随着目标组的统计分布随时间变化,模型预测变得不太准确。例如,新的流行口号可能会影响语音识别的准确性。
用于流水线的元数据
当我们想要自动化多步模型训练任务时,需要使用流水线或工作流程。例如,我们可以使用工作流管理工具,如 Airflow、Kubeflow 或 Metaflow,自动化包含多个功能步骤的模型训练过程:数据收集、特征提取、数据集增强、训练和模型部署。我们将在下一章中详细讨论工作流程。
对于流水线元数据,我们通常会跟踪流水线执行历史和流水线输入输出。这些数据可以为将来的故障排除提供审计信息。
注:深度学习项目差异很大。语音识别、自然语言处理和图像生成的模型训练代码截然不同。项目特定的因素有很多,例如数据集的大小/类型、ML 模型的类型和输入资产。除了先前提到的样本元数据外,我们建议您基于项目定义和收集元数据。当您寻找有助于模型再现和故障排除的数据时,元数据列表将自然而然地出现在您面前。
8.2.2 为什么要管理元数据?
由于元数据通常是通过日志或指标的形式进行仪表化或记录的,因此您可能会想知道为什么我们需要单独管理深度学习元数据。我们能否简单地从日志文件中提取深度学习元数据?日志管理系统,如 Splunk(www.splunk.com/
)和 Sumo Logic(www.sumologic.com/
),非常方便,因为它们允许开发人员搜索和分析分布式系统生成的日志和事件。
为了更好地解释在深度学习系统中需要专门的元数据管理组件的必要性,我们将讲述一个故事。Julia(数据工程师)、Ravi(数据科学家)和 Jianguo(系统开发人员)一起开发一个深度学习系统,为聊天机器人应用程序开发意图分类模型。Ravi 开发意图分类算法,Julia 负责数据收集和解析,而 Jianguo 则开发和维护深度学习系统。
在项目开发和测试阶段,Julia 和 Ravi 共同合作构建了一个实验性的训练流水线来生成意图模型。模型构建完成后,Ravi 将其传递给 Jianguo,以部署实验模型到预测服务并使用真实客户请求进行测试。
当 Ravi 对实验感到满意时,他会将训练算法从实验性流水线推广到自动化生产训练流水线。该流水线在生产环境中运行,并以客户数据作为输入生成意图模型。该流水线还会自动将模型部署到预测服务。图 8.2 描绘了整个故事背景。
图 8.2 没有元数据管理的模型开发
几周后,当 Ravi 发布了最新的意图分类算法后,一个聊天机器人客户——BestFood 公司向 Ravi 报告了模型性能下降的问题。在调查请求中,BestFood 提到他们的机器人在使用新数据集后的意图分类准确率下降了 10%。
为了排除报告的模型性能下降问题,Ravi 需要验证大量信息。他首先需要检查 BestFood 当前在预测服务中使用的模型版本,然后检查当前模型的模型衍生情况,例如在训练流水线中使用的数据集版本和代码版本。之后,Ravi 还可能需要重新生成模型进行本地调试。他需要比较当前模型和之前的模型,以测试数据分布的影响(当前新数据集 vs. 之前的数据集)。
Ravi 是一个自然语言处理(NLP)专家,但他对于运行其训练代码的深度学习系统了解甚少。为了继续他的调查,他不得不向 Jianguo 和 Julia 请求获取相关的模型、数据集和代码信息。因为每个人对模型训练应用和底层深度学习系统/基础设施的知识只是一知半解,对于每一个模型性能故障排除,Ravi、Julia 和 Jianguo 不得不共同努力来掌握完整的背景,这是耗时且低效的。
当然,这个故事过于简化了。在实践中,深度学习项目开发包含了数据、算法、系统/运行时开发和硬件管理。整个项目由不同的团队负责,并且很少有一个人了解所有内容。在企业环境中依赖跨团队合作来排除与模型相关的问题是不现实的。
在图 8.2 中缺少的关键因素是在一个集中的地方搜索和连接深度学习元数据的有效方法,以便 Julia、Ravi 和 Jianguo 能够轻松获取模型元数据。在图 8.3 中,我们增加了缺失的部分——一个元数据和制品存储(中间的灰色方框)来提高调试能力。
图 8.3 使用元数据管理进行模型故障排除
如果您将图 8.3 与图 8.2 进行比较,您会看到一个新组件(元数据和工件存储)被引入到图 8.3 的中间。我们在第 8.2.1 节中描述的所有深度学习元数据,无论它们来自实验流水线还是生产流水线,都会被收集并存储在此元数据存储中。
元数据存储为深度学习系统中的每个数据科学活动提供了元数据的全面视图。模型、流水线/训练运行和工件的元数据不仅被保存,而且在此存储内部相关联,因此人们可以轻松获取相关信息。例如,由于模型文件和模型训练运行在存储中被链接,人们可以轻松确定给定模型的模型谱系。
现在,数据科学家拉维可以使用元数据存储的用户界面列出系统中的所有模型和训练运行。然后,他可以深入研究元数据存储,找到过去训练运行中使用的输入参数、数据集和训练指标,这对评估模型非常有帮助。更重要的是,拉维可以自行快速完整地检索元数据,而无需了解模型训练和服务的基础架构。
8.3 设计一个元数据和工件存储
在本节中,我们将首先讨论构建元数据和工件存储的设计原则,然后介绍一个遵循这些原则的通用设计方案。即使您更喜欢使用开源技术来管理元数据,本节中的讨论仍将使您受益;了解设计需求和解决方案将帮助您选择适合您需求的正确工具。
注意为了简洁起见,我们在本章中将元数据和工件存储和元数据存储互换使用。当我们提到元数据存储时,它包括工件管理。
8.3.1 设计原则
元数据和工件存储被设计为便于模型性能故障排除和实验比较。它存储各种元数据并将其聚合到模型和训练运行周围,因此数据科学家可以快速获取任意模型的相关模型谱系和模型训练元数据。一个好的元数据存储应该遵循以下四个原则。
原则 1:显示模型谱系和版本控制
当收到模型名称时,元数据存储应该能够确定该模型的版本和每个模型版本的血统,例如,哪个训练运行生成了该模型,输入参数和数据集是什么。模型版本和血统对于模型故障排除至关重要。当客户报告模型的问题,例如模型性能下降时,我们首先问的问题是:模型是何时生成的?训练数据集是否发生了变化?使用了哪个版本的训练代码,以及我们从哪里可以找到训练指标?我们可以在模型血统数据中找到所有答案。
原则二:确保模型可复现性
元数据存储应跟踪重现模型所需的所有元数据,例如训练管道/运行配置、输入数据集文件和算法代码版本。能够重现模型对于模型实验评估和故障排除至关重要。我们需要一个地方来捕获配置、输入参数和工件,以启动模型训练运行以重现相同的模型。元数据存储是保留此类信息的理想地点。
原则三:方便获取打包的模型
元数据存储应该让数据科学家轻松访问模型文件,而不必理解复杂的后端系统。存储库应该具有手动和程序化两种方法,因为数据科学家需要能够运行手动和自动化的模型性能测试。
例如,通过使用元数据存储,数据科学家可以快速识别当前在生产服务中使用的模型文件,并下载它进行调试。数据科学家还可以编写代码从元数据存储中提取任意版本的模型,以自动比较新旧模型版本。
原则四:可视化模型训练跟踪和比较
良好的可视化可以极大地提高模型故障排除过程的效率。数据科学家依赖于大量的指标来比较和分析模型实验,元数据存储需要配备能够处理所有(或任何类型的)元数据查询的可视化工具。
例如,它需要能够显示一组模型训练运行的模型评估指标的差异和趋势行为。它还需要能够显示最新发布的 10 个模型的模型性能趋势。
8.3.2 一个通用的元数据和工件存储设计建议
为了解决第 8.3.1 节中的设计原则,深度学习元数据存储应该是一个度量存储系统,并且需要存储所有类型的元数据及其之间的关系。此类元数据应围绕模型和训练/实验执行聚合,以便在故障排除和性能分析过程中快速找到所有与模型相关的元数据。因此,内部元数据存储的数据架构是元数据存储设计的关键。
尽管元数据存储是一个数据存储系统,但数据扩展通常不是一个问题,因为深度学习系统的元数据量并不高。由于元数据大小取决于模型训练执行和模型的数量,而我们不希望每天进行超过 1,000 次模型训练运行,一个单独的数据库实例应该足够用于元数据存储系统。
为了用户方便,元数据存储应该提供一个 Web 数据摄入接口和日志记录 SDK,这样深度学习元数据就可以以类似应用程序日志和度量的方式被记录。基于设计原则和对系统需求的分析,我们提出了一个示例元数据存储设计供参考。图 8.4 显示了此组件的概述。
图 8.4 通用元数据和工件存储系统设计
在图 8.4 中,示例元数据存储系统由四个组件组成:一个客户端 SDK、一个 Web 服务器、后端存储和一个 Web UI。深度学习工作流程中的每个组件和步骤都使用客户端 SDK 将元数据发送到元数据存储服务器。元数据存储提供一个 RESTful 接口来进行元数据摄入和查询。Web UI 可视化元数据存储服务器的 RESTful 接口。除了基本的元数据和工件组织和搜索外,它还可以可视化各种模型训练运行的模型性能指标和模型差异。
元数据存储服务器位于此设计的中心位置。它有三层——一个 RESTful Web 接口、一个数据聚合器和存储。数据聚合器组件知道元数据如何组织和相互关联,因此它知道在哪里添加新元数据以及如何为不同类型的元数据搜索查询提供服务。在存储方面,我们建议构建一个抽象的元数据和工件存储层。这个抽象层作为一个适配器,封装了实际的元数据和文件存储逻辑。因此,元数据存储可以在不同类型的存储后端上运行,例如云对象存储、本地文件和本地或远程 SQL 服务器。
元数据存储数据模式
现在让我们来看一下元数据存储的数据模式。无论我们是将元数据保存在 SQL 数据库、noSQL 数据库还是纯文件中,我们都需要定义一个数据模式来描述元数据的结构和序列化方式。图 8.4 展示了我们元数据存储的实体关系图。
在图 8.5 中,模型训练运行(Training_Runs
对象)处于实体关系图的中心位置。这是因为模型性能故障排除总是从生成模型的过程(训练运行或工作流程)开始的,所以我们希望有一个专用的数据对象来跟踪生成模型文件的训练执行。
图 8.5 元数据存储数据模式的实体关系图
模型训练的详细元数据保存在Metrics
和Parameters
对象中。Parameters
对象存储训练运行的输入参数,例如数据集 ID、数据集版本和训练超参数。Metrics
对象存储训练期间产生的训练指标,例如模型 F2 分数。
Experiments
对象用于组织和分组模型训练运行。一个实验可以有多个训练运行。例如,我们可以将意图分类模型开发项目定义为一个训练实验,然后将所有意图分类模型训练执行与该实验关联。然后,在 UI 上,我们可以按不同的实验组织训练执行。
Models
对象存储模型文件的元数据,例如模型版本、类型和阶段。一个模型可以有多个阶段,例如测试、预生产和生产,所有这些都可以保留。
还要注意的是,图 8.5 中的每个实体都链接(由图中的线表示)到产生它们的特定训练运行,因此它们都将共享一个公共的training_run_id
。通过利用这个数据链接,你可以从任何训练运行对象开始,找到其输出模型、训练输入数据和模型训练指标。
早些时候,我们说可以将其简称为元数据存储,但也存储工件。那么这个设计中的工件在哪里呢?我们将工件的 URL 作为训练运行的输出存储在Training_Runs
对象中。如果查询模型或训练执行,则会得到工件的 URL。
模型焦点与流水线焦点
设计元数据系统有两种方法:模型焦点和流水线焦点。模型焦点方法将元数据与模型文件相关联,而流水线焦点方法聚合绕流水线/训练运行的元数据,就像我们在图 8.4 中提议的那样。
我们认为模型焦点和流水线焦点对最终用户(数据科学家)同样有用,它们并不是互斥的。我们可以支持两种焦点。
您可以使用流水线焦点方法实现元数据存储的存储层,类似于图 8.5 中的示例,然后在 Web UI 上构建搜索功能,以支持流水线搜索和模型搜索。
8.4 开源解决方案
在本节中,我们将讨论两个广泛使用的元数据管理工具:MLMD 和 MLflow。两个系统都是开源的,可以免费使用。我们首先对每个工具进行概述,然后提供比较,确定哪个工具在什么时候使用。
8.4.1 机器学习元数据
MLMD (github.com/google/ml-metadata
)是一个轻量级库,用于记录和检索与 ML 开发人员和数据科学家工作流相关的元数据。MLMD 是 TensorFlow Extended(TFX; www.tensorflow.org/tfx
)的一个组成部分,但是它设计成可以独立使用。例如,Kubeflow (www.kubeflow.org/
)使用 MLMD 来管理其管道和笔记本服务生成的元数据。有关更多详细信息,请参阅 Kubeflow 元数据文档(mng.bz/Blo1
)。你可以将 MLMD 视为一个日志记录库,并在你的 ML 管道的每个步骤中使用它来记录元数据,以便你可以理解和分析工作流/管道的所有相互连接的部分。
系统概览
使用 MLMD 库进行元数据记录可以设置两种不同的后端:SQL 或 gRPC 服务器。请参阅图 8.6 了解概念。
图 8.6 使用 MLMD 记录元数据的两种不同设置:(A)直接向后端数据库报告元数据,(B)向 gRPC 服务器 DB 报告元数据,数据库。
在图 8.6 中,我们看到 ML 管道/工作流的每个步骤都使用 MLMD 库(MLMD 客户端 API)来记录元数据。在后端,MLMD 将元数据保存在关系型数据库中,例如 mySQL 或 PostgreSQL。
你可以选择让每个 MLMD 库直接与 SQL 服务器通信(图 8.5,A),或者使用 MLMD 库中的服务器设置代码设置一个 gRPC 服务器,让客户端库与服务器通信(图 8.5,B)。方法 A 很简单;你不需要托管一个专用的日志服务器,但是推荐使用方法 B,因为它避免了暴露后端数据库。
您可以查看以下两个文档以获取详细的元数据存储配置:“元数据存储后端和存储连接配置” (mng.bz/dJMo
) 和 “使用 MLMD 与远程 gRPC 服务器” (mng.bz/rd8J
)。
日志记录 API
MLMD 中的元数据存储使用以下数据结构在存储后端记录元数据。一个execution代表工作流程中的一个组件或步骤;一个artifact描述了执行中的输入或输出对象;一个event是描述工件和执行之间关系的记录。一个context是一个逻辑组,用于将工件和执行在同一个工作流中关联在一起。
在这个概念的基础上,让我们来看一些示例元数据记录代码:
# define a dataset metadata
data_artifact = metadata_store_pb2.Artifact() ❶
data_artifact.uri = 'path/to/data'
data_artifact.properties["day"].int_value = 1
data_artifact.properties["split"].string_value = 'train'
data_artifact.type_id = data_type_id
[data_artifact_id] = store ❷
.put_artifacts([data_artifact]) ❷
# define a training run metadata
trainer_run = metadata_store_pb2.Execution() ❸
trainer_run.type_id = trainer_type_id
trainer_run.properties["state"].string_value = "RUNNING"
[run_id] = store.put_executions([trainer_run])
# define a model metadata
model_artifact = metadata_store_pb2.Artifact() ❹
model_artifact.uri = 'path/to/model/file'
model_artifact.properties["version"].int_value = 1
model_artifact.properties["name"].string_value = 'MNIST-v1'
model_artifact.type_id = model_type_id
[model_artifact_id] = store.put_artifacts([model_artifact])
# define an experiment metadata
my_experiment = metadata_store_pb2.Context() ❺
my_experiment.type_id = experiment_type_id
# Give the experiment a name
my_experiment.name = "exp1"
my_experiment.properties["note"].string_value = \
"My first experiment."
[experiment_id] = store.put_contexts([my_experiment])
# declare relationship between model, training run
# and experiment
attribution = metadata_store_pb2.Attribution()
attribution.artifact_id = model_artifact_id
attribution.context_id = experiment_id
association = metadata_store_pb2.Association()
association.execution_id = run_id
association.context_id = experiment_id
# Associate training run and model with the
# same experiment
store.put_attributions_and_associations( \
[attribution], [association]) ❻
一个数据集被记录为一个工件。
将元数据保存到存储中
记录模型训练运行为一个执行。
模型被记录为一个工件。
定义了模型训练元数据的逻辑组
保存元数据之间的关系
查看 MLMD 的“快速入门”文档 (mng.bz/VpWy
) 以获取详细的代码示例和本地设置说明。
搜索元数据
MLMD 没有提供用于显示其存储的元数据的 UI。因此,要查询元数据,我们需要使用其客户端 API。参见以下代码示例:
artifacts = store.get_artifacts() ❶
[stored_data_artifact] = store ❷
.get_artifacts_by_id([data_artifact_id])
artifacts_with_uri = store ❸
.get_artifacts_by_uri(data_artifact.uri)
artifacts_with_conditions = store
.get_artifacts(
list_options=mlmd.ListOptions( ❹
filter_query='uri LIKE "%/data"
AND properties.day.int_value > 0'))
❶ 查询所有注册的 artifacts
❷ 通过 ID 查询 artifact
❸ 通过 URI 查询 artifact
❹ 使用过滤器查询 artifact
MLMD 的“快速入门”文档 (mng.bz/VpWy
) 提供了许多用于获取 artifacts、执行和上下文元数据的查询示例。如果您感兴趣,请参阅。
学习 MLMD 的数据模型的最佳方法是查看其数据库模式。您可以首先创建一个 SQLite 数据库并配置 MLMD 元数据存储以使用该数据库,然后运行 MLMD 示例代码。最后,在本地 SQLite 数据库中创建所有实体和表。通过查看表模式和内容,您将深入了解 MLMD 中的元数据是如何组织的,因此您可以自己在其上构建一个漂亮的 UI。以下示例代码显示了如何配置 MLMD 元数据存储以使用本地 SQLite 数据库:
connection_config = metadata_store_pb2.ConnectionConfig()
connection_config.sqlite.filename_uri =
'{your_workspace}/mlmd-demo/mlmd_run.db' ❶
connection_config.sqlite.connection_mode = 3 ❷
store = metadata_store.MetadataStore(connection_config)
❶ SQLite 数据库的本地文件路径
❷ 允许读取、写入和创建
8.4.2 MLflow
MLflow (mlflow.org/docs/latest/tracking.html
) 是一个开源的 MLOps 平台。它旨在管理 ML 生命周期,包括实验、可重现性、部署和中央模型注册表。
与 MLMD 相比,MLflow 是一个完整的系统,而不是一个库。它由四个主要组件组成:
-
MLflow 跟踪(元数据跟踪服务器) — 用于记录和查询元数据
-
MLflow 项目 — 以可重复使用和可重现的方式打包代码
-
MLflow 模型 — 用于打包可用于不同模型服务工具的 ML 模型
-
MLflow 模型注册表 — 用于通过 UI 管理 MLflow 模型的全生命周期,例如模型谱系、模型版本控制、注释和生产推广
在本节中,我们重点关注跟踪服务器,因为这与元数据管理最相关。
系统概述
MLflow 提供了六种不同的设置方法。例如,MLflow 运行(训练管道)的元数据可以记录到本地文件、SQL 数据库、远程服务器或带有代理存储后端访问的远程服务器。有关这六种不同设置方法的详细信息,您可以查看 MLflow 文档“How Runs and Artifacts Are Recorded” (mlflow.org/docs/latest/tracking.html#id27
)。
在本节中,我们关注最常用的设置方法:使用代理存储访问的远程服务器。请参见图 8.7 以获取系统概述图。
图 8.7 设置 MLflow 跟踪服务器以进行元数据摄取和查询
从图 8.7 可以看出,深度学习流水线(工作流)的每个步骤/动作都使用 MLflow 客户端将元数据和文物注入到 MLflow 跟踪服务器中。跟踪服务器将度量、参数和标签等元数据保存在指定的 SQL 数据库中;文物,如模型、图像和文档,保存在配置的对象存储中,如 Amazon S3。
MLflow 提供了两种上传文物的方式:(1)直接从客户端上传和(2)通过跟踪服务器进行代理上传。在图 8.6 中,我们说明了后一种方法:利用跟踪服务器作为涉及文物的任何操作的代理服务器。优点是最终用户可以直接访问后端远程对象存储,而无需提供访问凭据。
MLflow 的另一个好处是它提供了一个漂亮的用户界面;数据科学家可以通过托管在跟踪服务器上的网站来检查和搜索元数据。该用户界面不仅允许用户从流水线执行的角度查看元数据,还可以直接搜索和操作模型。
记录 API
将元数据发送到 MLflow 跟踪服务器非常简单。我们可以通过创建一个活动运行作为上下文管理器,然后调用 log 函数来记录文物或单个键值参数、度量和标签。参见以下示例代码:
import mlflow
remote_server_uri = "..." ❶
mlflow.set_tracking_uri(remote_server_uri)
mlflow.set_experiment("/my-experiment")
with mlflow.start_run():
mlflow.log_param("parameter_a", 1) ❷
mlflow.log_metric("metric_b", 2) ❷
mlflow.log_artifact("features.txt") ❷
❶ 定义 MLflow 服务器 URL
❷ 在 Python 上下文管理器中由 MLflow ActiveRun 对象创建的日志元数据
自动记录日志
如果您厌倦了手动指定大量元数据,MLflow 支持自动记录日志。通过在训练代码之前调用 mlflow.autolog()
或特定于库的 autolog 函数,如 mlflow.tensorflow.autolog()
、mlflow.keras.autolog()
或 mlflow.pytorch.autolog()
,MLflow 将自动记录元数据,甚至文物,而无需显式的日志语句。如果您想了解更多关于 MLflow 记录日志的信息,请查看 MLflow 记录函数文档(mng.bz/xd1d
)。
搜索元数据
MLflow 跟踪服务器托管的跟踪用户界面允许您可视化、搜索和比较运行,以及下载运行文物或元数据以在其他工具中进行分析。该用户界面包含以下关键功能:基于实验的运行列表和比较,通过参数或度量值搜索运行,可视化运行度量,以及下载运行结果。除了用户界面外,您还可以像以下示例中那样以编程方式实现跟踪用户界面提供的所有操作:
from mlflow.tracking import MlflowClient
client = MlflowClient() ❶
.. .. ..
# Fetch the run metadata from the backend store,
# which contains a list of metadata
active_run = client.get_run(run_id)
print("run_id: {}; lifecycle_stage: {}"\ ❷
.format(run_id, active_run.info.lifecycle_stage))
# Retrieve an experiment by
# experiment_id from the backend store
experiment = client.get_experiment(exp_id)
# Get a specific version of a model
mv = client.get_model_version(model_name, mv_version)
❶ 初始化客户端
❷ 打印出运行的执行阶段
编程式元数据访问不仅在使用分析工具(例如 pandas)查询和比较不同训练运行的模型性能时有帮助,而且对于将模型与模型服务系统集成也很有用,因为它允许你从 MLflow 模型注册表中以编程方式获取模型。有关完整的 MLflowClient
API 用法,请查看 MLflow 跟踪 API 文档 (mng.bz/GRzO
)。
8.4.3 MLflow vs. MLMD
从前面章节的描述中,我们可以看到 MLMD 更像是一个轻量级库,而 MLflow 是一个 MLOps 平台。两者都可以独立运行,提供元数据摄取和搜索功能,并基于模型训练运行跟踪元数据。但是 MLflow 提供的功能更加丰富。
除了 MLMD 的功能之外,MLflow 还支持自动元数据记录和设计良好的 UI 以可视化实验元数据(包括实验比较)、模型注册表、工件管理、代码可复现性、模型打包等。
如果你需要向系统引入一个完整的、新的元数据和工件存储,MFflow 将是你的首选。它得到了一个活跃的开源社区的支持,并且涵盖了大多数用户对 ML 元数据管理的需求。作为一个额外的奖励,MLflow 在 MLOps 上有着很好的支持,比如 MLflow 项目管理和模型部署。
如果你已经拥有一个工件注册表和一个度量可视化网站,并且想要将元数据功能集成到现有系统中,那么 MLMD 是一个不错的选择。MLMD 轻量级、易于使用,并且简单易学。例如,Kubeflow (www.kubeflow.org/
) 深度学习平台将 MLMD 作为元数据跟踪工具集成到其组件中,如 Kubeflow pipeline (www.kubeflow.org/docs/components/pipelines/
)。
总结
-
机器学习元数据可以分为四类:模型训练运行、通用工件、模型工件和流水线。
-
元数据和工件存储设计用于支持模型性能比较、故障排除和复现。
-
一个良好的元数据管理系统可以帮助展示模型血统,实现模型可复现性,并促进模型比较。
-
MLMD 是一个轻量级的元数据管理工具,源自 TensorFlow 流水线,但可以独立使用。例如,Kubeflow 使用 MLMD 在其管道组件中管理 ML 元数据。
-
MLMD 适用于将元数据管理集成到现有系统中。
-
MLflow 是一个 MLOps 平台;它旨在管理机器学习生命周期,包括实验、可复现性、部署和中央模型注册。
-
如果你想要引入一个完全独立的元数据和工件管理系统,那么 MLflow 是适合的选择。
第九章:工作流编排
本章涵盖了
-
定义工作流和工作流编排
-
为什么深度学习系统需要支持工作流
-
设计一个通用工作流编排系统
-
引入三个开源编排系统:Airflow、Argo Workflows 和 Metaflow
在本章中,我们将讨论深度学习系统的最后但至关重要的部分:工作流编排——一个管理、执行和监控工作流自动化的服务。工作流是一个抽象且广泛的概念;它本质上是一系列操作,这些操作是某个更大任务的一部分。如果你可以设计一个带有一组任务的计划来完成一项工作,这个计划就是一个工作流。例如,我们可以为训练机器学习(ML)模型定义一个顺序工作流。这个工作流可以由以下任务组成:获取原始数据、重建训练数据集、训练模型、评估模型和部署模型。
因为工作流是一个执行计划,它可以手动执行。例如,数据科学家可以手动完成我们刚刚描述的模型训练工作流的任务。例如,要完成“获取原始数据”任务,数据科学家可以制作网络请求并将其发送到数据集管理(DM)服务以获取数据集——所有这些都不需要工程师的帮助。
然而,手动执行工作流并不理想。我们希望自动化工作流的执行。当针对不同目的开发了大量工作流时,我们需要一个专门的系统来处理工作流执行的复杂性。我们称这种系统为工作流编排系统。
工作流编排系统被建立来管理工作流的生命周期,包括工作流的创建、执行和故障排除。它不仅提供了使所有预定代码保持运行的脉搏,还为数据科学家提供了一个控制平面,用于管理深度学习系统中的所有自动化。
在本章中,我们将讨论工作流编排系统的设计以及在深度学习领域中使用的最受欢迎的开源编排系统。通过阅读本章,您不仅将对系统要求和设计选项有扎实的理解,还将了解如何选择最适合您自己情况的正确开源编排系统。
9.1 引入工作流编排
在我们深入探讨工作流编排系统设计的细节之前,让我们就工作流编排的基本概念进行快速讨论,特别是关于从深度学习/ML 角度出发的特殊工作流挑战。
注意 由于在深度学习项目和 ML 项目中使用工作流编排的要求几乎相同,因此在本章中我们将深度学习和机器学习这两个词用于交替使用。
9.1.1 什么是工作流?
一般来说,工作流是一系列操作,这些操作是某个较大任务的一部分。工作流可以被视为一种步骤的有向无环图(DAG)。
步骤是描述一个动作的最小可恢复计算单元;这个任务可以是获取数据或触发服务等。一个步骤要么成功,要么失败。在本章中,我们将 任务 和 步骤 这两个词互换使用。
DAG 指定了步骤之间的依赖关系和执行它们的顺序。图 9.1 显示了一个用于训练自然语言处理(NLP)模型的示例工作流。
图 9.1 展示了一个具有多个步骤的示例模型训练工作流的 DAG。椭圆形和菱形都是步骤,但是不同类型。实线箭头表示步骤之间的依赖关系,虚线箭头表示从步骤发送的外部网络请求。
从图 9.1 中的示例 DAG 中,我们看到了一个由许多步骤组成的工作流。每个步骤都依赖于另一个,实线箭头显示了步骤之间的依赖关系。这些箭头和步骤形成了一个没有循环的工作流 DAG。
如果你按照 DAG 中的箭头(从左到右)并完成任务,你可以训练并发布一个 NLP 模型到生产环境。例如,当一个传入的请求触发了工作流时,授权(authorization)步骤将首先被执行,然后数据集构建步骤和嵌入获取步骤将同时被执行。在这两个步骤完成之后,箭头的另一侧的步骤将被执行。
工作流在 IT 行业中随处可见。只要你能够将一个流程定义为单个任务/步骤的 DAG,这个流程就可以被视为工作流。工作流对于深度学习模型的开发至关重要。事实上,在生产环境中,大多数深度学习模型构建活动都被呈现并执行为工作流。
注意:工作流不应该有循环。为了保证工作流在任何情况下都能完成,其执行图必须是一个 DAG,这样可以防止工作流执行陷入死循环。
9.1.2 什么是工作流编排?
一旦我们定义了一个工作流,下一步就是运行工作流。运行工作流意味着根据工作流 DAG 中定义的顺序执行工作流步骤。工作流编排 是我们用来描述工作流的执行和监视的术语。
工作流编排的目标是自动化执行工作流中定义的任务。在实践中,工作流编排的概念通常扩展到整个工作流管理——即以自动化方式创建、调度、执行和监视多个工作流。
深度学习系统为什么需要工作流编排?理想情况下,我们应该能够将整个深度学习项目编码为一个整体。这正是我们在项目的原型阶段所做的,将所有代码放在一个 Jupyter 笔记本中。那么,为什么我们需要将原型代码转换为工作流,并在工作流编排系统中运行它呢?答案有两个方面:自动化和工作共享。为了理解这些原因,让我们看一下图 9.2 中的三个样本训练工作流。
图 9.2 深度学习工作流由许多可重复使用的任务组成。
使用工作流的一个巨大好处是它将大量代码转换为一组可共享和可重用的组件。在图 9.2 中,我们想象三名数据科学家正在进行三个模型训练项目(A、B 和 C)的工作。由于每个项目的训练逻辑不同,数据科学家开发了三个不同的工作流(A、B 和 C)来自动化他们的模型训练流程。尽管每个工作流具有不同的 DAGs,但每个 DAG 中的步骤高度重叠。总共的六个步骤是可共享和可重用的。例如,auth 步骤(步骤 1)是所有三个工作流的第一步。
具有可重复使用步骤可以极大地提高数据科学家的生产率。例如,要从 DM 服务中提取数据(图 9.2 中的第 2 步),数据科学家需要了解 DM Web API 的工作原理。但是如果有人已经将 DM 数据提取方法构建为一个步骤函数,科学家们就可以在他们的工作流中重复使用这个步骤,而不必学习如何与 DM 服务交互。如果每个人都以工作流的形式编写他们的项目,我们将拥有大量可重用的步骤,这将在组织级别节省大量重复的工作!
另一个适应深度学习开发的工作流的原因是它促进了协作。模型开发需要团队合作;一个专门的团队可能负责数据,而另一个团队负责训练算法。通过在工作流中定义复杂的模型构建过程,我们可以将一个大型复杂项目分解为片段(或步骤)并将其分配给不同的团队,同时仍然保持项目有序和组件正确顺序。工作流 DAG 清楚地显示了所有项目参与者可以看到的任务依赖关系。
简而言之,一个好的工作流编排系统鼓励工作共享,促进团队协作,并自动化复杂的开发场景。所有这些优点使工作流编排成为深度学习项目开发的关键组成部分。
9.1.3 深度学习中使用工作流编排的挑战
在前一节中,我们看到工作流系统可以为深度学习项目开发提供许多好处。但有一个注意事项:使用工作流来原型化深度学习算法的想法是很麻烦的。
要了解为什么这样做很麻烦,让我们看一下深度学习开发过程的图表(图 9.3)。这张图表应该为你理解工作流在深度学习背景下提出的挑战奠定基础。
图 9.3 深度学习项目开发的数据科学家视角
在图 9.3 中,我们从数据科学家的角度看到了一个典型的深度学习项目开发过程。该过程可以分为两个阶段:本地孵化阶段和生产阶段。
在本地孵化阶段,数据科学家在本地/开发环境中进行数据探索和模型训练原型。当原型完成并且项目看起来很有前景时,数据科学家开始进行生产上线:将原型代码移到生产系统。
在生产阶段,数据科学家将原型代码转换为工作流程。他们将代码分解为多个步骤,并定义一个工作流 DAG,然后将工作流提交给工作流编排系统。之后,编排系统接管并根据其时间表运行工作流程。
在原型和生产之间存在差距。
如果你问一个在工作流编排系统上工作的工程师他们对图 9.3 中的开发过程的感觉,答案很可能是:还不错!但实际上,这个过程对数据科学家来说是有问题的。
从数据科学家的角度来看,一旦算法在本地测试通过,其原型代码应立即投入生产。但是在图 9.3 中,我们看到原型阶段和生产阶段 不是 顺利连接的。将孵化代码部署到生产并不直接;数据科学家必须额外工作来构建一个工作流程来在生产中运行他们的代码。原型代码和生产工作流之间的差距影响了开发速度,原因有两个:
-
工作流程的构建和调试并不是直接的 —— 数据科学家在编写模型训练工作流程时,通常会面临巨大的学习曲线。学习工作流 DAG 语法、工作流程库、编码范例和故障排除对于数据科学家来说是一个巨大的负担。工作流程的故障排除是最痛苦的部分。大多数编排系统不支持本地执行,这意味着数据科学家必须在远程编排系统中测试他们的工作流程。这很困难,因为工作流环境和工作流执行日志都是远程的,所以数据科学家在工作流程执行出错时无法轻易找出根本原因。
-
工作流构建并非一次性事件,而是频繁发生——一种常见的误解是,由于工作流构建只发生一次,所以如果耗时且繁琐也没关系。但事实是,工作流构建是持续不断的,因为深度学习开发是一个迭代过程。正如图 9.3 所示,数据科学家会迭代地进行原型设计和生产实验,因此工作流需要经常更新,以测试从本地到生产环境的新改进。因此,令人不快且耗时的工作流构建会反复发生,这阻碍了开发速度。
平滑地从原型设计过渡到生产环境
尽管存在差异,图 9.3 中的流程是不错的。数据科学家从一个简单的脚本开始在本地进行原型设计,然后继续完善。如果每次迭代后的结果看起来足够令人满意,那么“简单的本地脚本”将被转换为工作流,并在生产环境中在编排系统中运行。
关键的改进是使从原型代码到生产工作流的过渡步骤变得无缝。如果一个编排系统是为深度学习用例设计的,它应该提供工具,帮助数据科学家用最少的工作量从他们的代码构建工作流。例如,Metaflow 是一个开源库,将在 9.3.3 节中讨论,它允许数据科学家通过编写带有 Python 注解的 Python 代码来授权工作流。数据科学家可以直接从他们的原型代码中获得工作流,而不需要进行任何更改。Metaflow 还在本地和云生产环境之间提供了统一的模型执行用户体验。这消除了工作流测试中的摩擦,因为 Metaflow 在本地和生产环境中以相同的方式运行工作流。
深度学习系统应以人为中心
当我们向深度学习系统引入通用工具——如工作流编排时,不要满足于仅仅启用功能。尽量减少系统中人工的时间。总是可以进行定制工作,以帮助我们的用户更加高效。
Metaflow(9.3.3 节)是一个很好的例子,说明当工程师们不满足于仅仅构建一个用于自动化深度学习工作流的编排系统时会发生什么。相反,他们更进一步优化了工作流构建和管理,以解决数据科学家的工作方式。
9.2 设计工作流编排系统
在本节中,我们将分三个步骤设计工作流编排系统。首先,我们使用一个典型的数据科学家用户场景,展示编排系统从用户角度的工作方式。第二,我们学习通用编排系统设计。第三,我们总结构建或评估编排系统的关键设计原则。通过阅读本节,您将了解编排系统的一般工作方式,从而可以自信地评估或操作任何编排系统。
9.2.1 用户场景
尽管各工作流场景的过程有很大的差异,但数据科学家的用户场景通常非常标准。大多数工作流使用可以分为两个阶段:开发阶段和执行阶段。请参见图 9.4,了解数据科学家(Vena)的工作流用户体验。我们将一步一步地跟随图 9.4 中 Vena 的用户场景。
图 9.4:工作流编排系统的通用深度学习用户场景。
开发阶段
在开发阶段,数据科学家将其训练代码转换为工作流。以下是 Vena 的示例:
-
数据科学家 Vena 在本地环境中使用 Jupyter notebook 或纯 Python 原型开发其模型训练算法。经过本地测试和评估,Vena 认为是时候将代码部署到生产环境中,进行真正的客户数据在线实验了。
-
由于生产环境中的所有内容都是工作流,因此 Vena 需要将其原型代码转换为工作流程。所以,Vena 使用编排系统提供的语法,在 YAML(文本配置)文件中将其工作重建为一个任务的 DAG。例如,数据解析->数据增强->数据集构建->训练->[在线评估、离线评估]->模型发布。
-
然后,Vena 为 DAG 中每个步骤设置输入/输出参数和动作。以训练步骤为例,Vena 将步骤动作设置为 RESTful HTTP 请求。此步将向模型训练服务发送一个 RESTful 请求来启动训练作业。该请求的有效载荷和参数来自步骤输入参数。
-
一旦定义好工作流,Vena 就在 DAG YAML 文件中设置工作流的执行计划。例如,Vena 可以将工作流安排在每个月的第一天运行,还可以将工作流设置为由外部事件触发。
-
Vena 运行工作流本地验证,并将工作流提交给编排服务。
为了让您了解工作流在现实中的含义,以下代码显示了 Vena 的伪工作流(在第 9.3 节,我们将讨论实际的工作流系统):
# define workflow DAG
with DAG(
description='Vena’s sample training workflow',
schedule_interval=timedelta(months=1),
start_date=datetime(2022, 1, 1),
) as dag: ❶
# define execution logic for each step
data_parse_step = BashOperator( .. .. ..)
data_augment_step = BashOperator( .. .. ..) ❷
dataset_building_step = BashOperator( .. .. ..)
training_step = BashOperator( .. .. ..)
# Declares step dependencies
data_parse_step >> data_augment_step ❸
>> dataset_building_step >> training_step ❸
❶ DAG 定义;定义了工作流的主体,包括步骤和依赖项
❷ 执行数据增强的 bash 命令
❸ 顺序执行流
执行阶段
在执行阶段,编排服务执行模型训练工作流,如 Vena 的示例所示:
-
一旦 Vena 的工作流被提交,编排服务就会将工作流 DAG 保存到数据库中。
-
编排服务的调度器组件会检测 Vena 的工作流,并将工作流的任务分派给后端工作者。调度器将确保任务按照工作流 DAG 中定义的顺序执行。
-
Vena 使用编排服务的 Web UI 实时检查工作流的执行进度和结果。
-
如果工作流生成了一个好的模型,Vena 可以将其推广到分期和生产环境。如果不是,Vena 就会开始另一个原型的迭代。
判断一个编排系统是否适合深度学习的一个关键指标是将原型代码转换为工作流的难易程度。在图 9.4 中,我们可以看到,每次 Vena 原型化一个新的想法时,她都需要将其训练代码转换为工作流。我们可以想象如果我们减少将深度学习代码转换为工作流的摩擦会节省多少人力时间。
注意 一个工作流应该总是轻量级的。工作流用于自动化一个过程;其目标是将一系列任务分组并连接起来,并按照定义的顺序执行它们。使用工作流的巨大好处是人们可以共享和重复使用这些任务,因此他们可以更快地自动化他们的流程。因此,工作流本身不应进行任何繁重的计算,真正的工作应由工作流的任务完成。
9.2.2 一个通用的编排系统设计
现在让我们转向一个通用的工作流编排系统。为了帮助您了解编排系统的工作原理以及如何研究开源编排系统,我们准备了一个高级系统设计。通过放大详细的实现并仅保留核心组件,这个设计适用于大多数编排系统,包括将在第 9.3 节讨论的开源系统。请参见图 9.5 以获取设计方案。
图 9.5 通用工作流编排服务的设计概览
一个工作流编排系统通常由以下五个组件组成:
-
Web 服务器—Web 服务器提供了一个 Web 用户界面和一组 Web API,供用户创建、检查、触发和调试工作流的行为。
-
调度器和控制器—调度器和控制器组件有两个功能。首先,调度器监视系统中的每个活动工作流,并在合适的时间安排工作流运行。其次,控制器将工作流任务分派给工作者。尽管调度器和控制器是两个不同的功能单元,但它们通常一起实现,因为它们都与工作流执行相关。
-
元数据数据库 — 元数据数据库存储工作流的配置、DAG、编辑和执行历史,以及任务的执行状态。
-
工作组 — 工作组提供计算资源来运行工作流任务。工作器抽象了基础架构,并且对正在运行的任务不可知。例如,我们可能有不同类型的工作器,例如 Kubernetes 工作器和 Amazon Elastic Compute Cloud(EC2)工作器,但它们都可以执行相同的任务,尽管在不同的基础架构上。
-
对象存储 — 对象存储是所有其他组件的共享文件存储;通常建立在云对象存储之上,例如 Amazon Simple Storage Service(S3)。对象存储的一个用途是任务输出共享。当工作器运行任务时,它从对象存储中读取上一个任务的输出值作为任务输入;工作器还将任务输出保存到对象存储中,供后续任务使用。
对象存储和元数据数据库都可以由编排系统的所有组件访问,包括调度程序、Web 服务器和工作器组件。具有集中式数据存储可以解耦核心组件,因此 Web 服务器、调度程序和工作器可以独立工作。
工作流程是如何执行的?
首先,Vena 为工作流定义了 DAG。在 DAG 内部,Vena 声明了一组任务,并定义了任务执行顺序的控制流。对于每个任务,Vena 要么使用系统的默认运算符,例如 Shell 命令运算符或 Python 运算符,要么构建自己的运算符来执行任务。
第二,Vena 通过 Web UI 或命令行将工作流程(具有依赖代码的 DAG)提交给 Web 服务器。工作流程保存在元数据数据库中。
第三,调度程序定期(每隔几秒或几分钟)扫描元数据数据库并检测新的工作流程;然后在预定时间启动工作流程。为了执行工作流程,调度程序调用控制器组件根据 DAG 中定义的任务顺序将工作流程的任务分派到工作器队列中。
第四,工作人员从共享作业队列中挑选一个任务;它从元数据数据库中读取任务定义,并通过运行任务的运算符执行任务。在执行过程中,工作人员将任务的输出值保存到对象存储中,并将任务的执行状态报告回元数据数据库。
最后但同样重要的是,Vena 使用托管在 Web 服务器组件上的 Web UI 来监视工作流程的执行。因为调度程序/控制器组件和工作器实时向元数据数据库报告状态,所以 Web UI 始终显示最新的工作流程状态。
9.2.3 工作流程编排设计原则
因为我们已经看到了工作流编排系统在内部和外部的工作方式,现在是时候研究使编排系统在深度学习场景中出色的设计原则了。我们希望您可以将这些原则作为指导,来改进您的系统或评估开源方法。
注意 在深度学习系统中,工作流编排系统是最复杂的组件之一,涉及到大量的工程工作。所以,在最初的几个版本中,不必过于担心使您的系统与这些原则完全匹配。
原则 1:重要性
工作流编排本质上是一种作业调度的挑战,所以任何编排系统的底线都是提供一个可靠的工作流执行体验。一个有效的工作流应该总是能够正确、重复地按计划执行。
原则 2:可用性
在深度学习环境中,编排系统的可用性衡量标准是是否优化了数据科学家的工作效率。在一个编排系统中,数据科学家的大部分交互工作都是工作流的创建、测试和监控。因此,一个用户友好的编排系统应该让用户能够轻松地创建、监控和排除故障。
原则 3:可扩展性
为了适应各种深度学习基础设施,人们应该能够轻松定义自己的任务操作符和执行器,而不用担心它们部署在哪里。编排系统应该提供适合您环境的抽象级别,无论是 Amazon EC2 还是 Kubernetes。
原则 4:隔离性
可能发生两种关键的隔离:工作流创建隔离和工作流执行隔离。工作流创建隔离意味着在创建工作流时,人们不能相互干扰。例如,如果 Vena 提交了一个无效的工作流有向无环图(DAG),或者发布了一个在其他工作流中被引用的共享库的新版本,那么现有的工作流不应受到影响。
工作流执行隔离意味着每个工作流在一个独立的环境中运行。工作流之间不应有资源竞争,并且一个工作流的失败不会影响其他工作流的执行。
原则 5:扩展性
一个好的编排系统应该解决以下两个扩展性问题:处理大量同时运行的工作流以及处理大型扩展性工作流。同时运行的工作流扩展性通常指,给定足够的计算资源 —— 例如,向工作组中添加更多的工作节点 —— 编排系统可以满足无限数量的并发工作流执行。此外,系统应始终为每个工作流保持服务级别协议(SLA)。例如,工作流应在其预定时间执行,且不得晚于 2 秒,无论有多少其他工作流正在执行。
对于单一的大型工作流扩展,系统应该鼓励用户不必担心性能,这样他们就可以专注于可读性强、直接明了的代码和简单的操作。当工作流执行达到限制时——例如,训练运算符执行时间过长——编排系统应该提供一些水平并行运算符,例如分布式训练运算符,以解决单个工作流性能问题。
深度学习编排的主要扩展思想是我们应该在系统级别解决性能问题,并避免要求用户考虑可扩展性编写代码。这可能导致可读性下降、调试困难和操作负担增加。
原则 6:人本支持,既适用于原型设计,也适用于生产环境
连接数据科学家本地原型代码到生产工作流的能力是深度学习特有的要求。这是我们用来评估编排系统是否适合深度学习系统的关键指标。
一个为深度学习设计的编排系统将尊重深度学习项目开发是从原型到生产的迭代持续努力的事实。因此,它将不遗余力地帮助数据科学家将他们的本地原型代码转换为生产工作流。
9.3 巡回开源工作流编排系统
在本节中,我们将介绍三种经过实战验证的工作流编排系统:Airflow、Argo Workflows 和 Metaflow。这三个开源系统在 IT 行业得到了广泛应用,并得到了活跃社区的支持。除了一般介绍外,我们还将从深度学习项目开发的角度评估这些工作流系统。
为了进行公正的比较,我们在 Airflow、Argo Workflows 和 Metaflow 中为相同工作流实现伪代码。基本上,如果有新数据,我们首先转换数据并将其保存到数据库的新表中,然后通知数据科学团队。此外,我们希望工作流每天运行。
9.3.1 Airflow
Airflow(airflow.apache.org/docs/apache-airflow/stable/index.html
)于 2014 年在 Airbnb 创建,现在是 Apache 基金会的一部分。Airflow 是一个平台,用于以编程方式编写、调度和监视工作流。Airflow 并不是为深度学习用例设计的;它最初是为了编排越来越复杂的 ETL(抽取、转换、加载)管道(或数据管道)而构建的。但由于 Airflow 具有良好的可扩展性、生产质量和 GUI 支持,它被广泛应用于许多其他领域,包括深度学习。截至本书撰写时,Airflow 是最受欢迎的编排系统。
典型用例
在 Airflow 中构建工作流程需要两个步骤。首先,定义工作流 DAG 和任务。其次,在 DAG 中声明任务之间的依赖关系。Airflow DAG 本质上是 Python 代码。看以下清单,了解我们的示例工作流在 Airflow 中是如何实现的。
清单 9.1 一个示例 Airflow 工作流定义
# declare the workflow DAG.
with DAG(dag_id="data_process_dag",
schedule_interval="@daily",
default_args=default_args,
template_searchpath=[f"{os.environ['AIRFLOW_HOME']}"],
catchup=False) as dag:
# define tasks of the workflow, each code section below is a task
is_new_data_available = FileSensor( ❶
task_id="is_new_data_available",
fs_conn_id="data_path",
filepath="data.csv",
.. .. ..
)
# define data transformation task
transform_data = PythonOperator(
task_id="transform_data",
python_callable=transform_data ❷
)
# define table creation task
create_table = PostgresOperator( ❸
task_id="create_table",
sql='''CREATE TABLE IF NOT EXISTS invoices (
.. .. ..
);''',
postgres_conn_id='postgres',
database='customer_data'
)
save_into_db = PythonOperator(
task_id='save_into_db',
python_callable=store_in_db
)
notify_data_science_team = SlackWebhookOperator(
task_id='notify_data_science_team',
http_conn_id='slack_conn',
webhook_token=slack_token,
message="Data Science Notification \n"
.. .. ..
)
# Step two, declare task dependencies in the workflow
is_new_data_available >> transform_data
transform_data >> create_table >> save_into_db
save_into_db >> notify_data_science_team
save_into_db >> create_report
# The actual data transformation logic, which is referenced
# in the “transform_data” task.
def transform_data(*args, **kwargs):
.. .. ..
❶ 检查是否有新文件到达
❷ 实际逻辑是在 “transform_data” 函数中实现的。
❸ PostgresOperator 是预定义的 Airflow 运算符,用于与 postgres db 交互。
在代码清单 9.1 中,我们看到示例工作流 DAG 包含多个任务,如 create_table
和 save_into_db
。在 Airflow 中,任务被实现为运算符。有许多预定义和社区管理的运算符,例如 MySqlOperator、SimpleHttpOperator 和 Docker 运算符。
Airflow 的预定义运算符帮助用户实现任务而无需编码。您还可以使用 PythonOperator 运行自定义的 Python 函数。一旦工作流 DAG 被构建并且所有代码被部署到 Airflow,我们可以使用 UI 或以下 CLI 命令来检查工作流执行状态;以下是一些示例 shell 命令:
airflow dags list ❶
airflow tasks list data_process_dag ❷
airflow tasks list data_process_dag --tree ❸
❶ 打印所有活动的 DAG
❷ 打印 “data_process_dag” DAG 中任务的列表
❸ 打印 “data_process_dag” DAG 中任务的层次结构
如果您想了解更多关于 Airflow 的信息,您可以查看其架构概述文档和教程 (mng.bz/Blpw
)。
关键功能
Airflow 提供以下关键功能:
-
DAGs — Airflow 通过 DAGs 抽象复杂的工作流程,工作流 DAG 是通过 Python 库实现的。
-
程序化工作流管理 — Airflow 支持动态创建任务,并允许创建复杂的动态工作流。
-
出色的内置运算符帮助构建自动化 — Airflow 提供了许多预定义的运算符,帮助用户实现任务而无需编码。
-
可靠的任务依赖性和执行管理 — Airflow 在每个任务中都内置了自动重试策略,并提供了不同类型的传感器来处理运行时依赖关系,例如检测任务完成、工作流运行状态变更和文件存在。
-
可扩展性 — Airflow 使其传感器、钩子和运算符完全可扩展,这使得它能够从大量社区贡献的运算符中受益。Airflow 还可以通过添加定制运算符轻松集成到不同的系统中。
-
监控和管理界面 — Airflow 提供了一个强大的用户界面,用户可以快速了解工作流/任务执行状态和历史。用户还可以从界面触发和清除任务或工作流运行。
-
生产质量 — Airflow 提供了许多有用的工具,用于在生产环境中维护服务,如任务日志搜索、扩展、报警和 restful API。
限制
尽管 Airflow 是一个出色的工作流编排工具,但在深度学习场景中使用时仍然存在一些缺点:
-
数据科学家入门时的高前期成本 — Airflow 对于实现不受内置运算符支持的任务具有陡峭的学习曲线。此外,没有一种简单的方法进行工作流本地测试。
-
将深度学习原型代码移至生产环境时的高摩擦力 — 当我们将 Airflow 应用于深度学习时,数据科学家必须将他们的本地模型训练代码转换为 Airflow DAG。这是额外的工作,对于数据科学家来说是一种不愉快的体验,特别是考虑到如果我们直接从模型训练代码构建工作流程 DAG,这是可以避免的。
-
在 Kubernetes 上操作时的高复杂性 — 在 Kubernetes 上部署和操作 Airflow 并不简单。如果您希望采用一个编排系统在 Kubernetes 上运行,Argo Workflows 是一个更好的选择。
9.3.2 Argo Workflows
Argo Workflows 是一个开源的、容器原生的工作流引擎,用于在 Kubernetes 上编排并行工作流程/任务。Argo Workflows 解决了与 Airflow 相同的问题,但采用了不同的方式;它采用了 Kubernetes 本地方法。
Argo Workflows 和 Airflow 之间最大的区别在于 Argo Workflows 在 Kubernetes 上本地构建。更具体地说,Argo Workflows 中的工作流程和任务以 Kubernetes 自定义资源定义(CRD)对象实现,并且每个任务(步骤)都作为 Kubernetes pod 执行。请参阅图 9.6 以获得高级系统概述。
图 9.6 Argo Workflows 中的工作流程及其步骤是作为 Kubernetes pod 执行的。
在图 9.6 中,Vena(数据科学家)首先将工作流程及其步骤/任务定义为 Kubernetes CRD 对象,通常表示为 YAML 文件。然后她将工作流提交到 Argo Workflows,其控制器在 Kubernetes 集群内创建 CRD 对象。接下来,Kubernetes pod 动态启动以按工作流程顺序运行工作流程步骤/任务。
您还可以注意到,每个步骤的执行完全由容器和 pod 隔离;每个步骤使用文件来表示其输入和输出值。Argo Workflows 会自动将依赖文件挂载到步骤的容器中。
Kubernetes pod 创建的任务隔离是 Argo Workflows 的一个巨大优势。简单性也是人们选择 Argo Workflows 的另一个原因。如果您了解 Kubernetes,Argo 的安装和故障排除都很简单。我们可以使用 Argo Workflows 命令或标准的 Kubernetes CLI 命令来调试系统。
典型的使用案例
为了更好地理解,让我们看一个 Argo Workflows 的例子。在本节中,我们使用 Argo Workflows 来自动化我们在之前 Airflow 部分看到的相同数据处理工作。工作流程包括首先检查新数据,转换数据,将其保存到数据库中的新表中,然后通过 Slack 通知数据科学家团队。请参阅以下代码清单以查看 Argo Workflows 的定义。
代码清单 9.2 Argo Workflows 的示例工作流程,包含一系列步骤
apiVersion: argoproj.io/v1alpha1
kind: Workflow ❶
metadata:
generateName: data-processing-
spec:
entrypoint: argo-steps-workflow-example
templates:
- name: argo-steps-workflow-example
Steps: ❷
- - name: check-new-data
template: data-checker ❸
- - name: transform-data
template: data-converter
arguments:
artifacts:
- name: data-paths ❹
from: "{{steps.check-new-data.outputs.
artifacts.new-data-paths}}" ❺
- - name: save-into-db
template: postgres-operator
- - name: notify-data-science-team
template: slack-messenger
- name: data-checker ❻
container:
image: docker/data-checker:latest
command: [scan, /datastore/ds/]
outputs:
artifacts:
- name: new-data-paths ❼
path: /tmp/data_paths.txt
- name: data-converter
inputs:
artifacts:
- name: data_paths ❽
path: /tmp/raw_data/data_paths.txt
container:
image: docker/data-checker:latest
command: [data_converter, /tmp/raw_data/data_paths.txt]
- name: save-into-db
.. .. ..
- name: notify-data-science-team
.. .. ..
❶ 将 CRD 对象类型声明为工作流程
❷ 声明工作流程的步骤
❸ 步骤主体被定义为另一个模板,类似于函数。
❹ 声明数据路径工件来自由 check-new-data 步骤生成的新数据路径工件
❺ 这就是步骤传递参数的方式。
❻ 实际步骤定义,类似于函数实现
❼ 声明此步骤的输出工件(生成新的数据路径);工件来自/tmp/data_paths.txt,该工件也可以是一个目录。
❽ 解压缩数据 _paths 输入工件,并将其放置在/tmp/raw_data/data_paths.txt
Argo Workflows 中最基本的概念是工作流程和模板。工作流程对象代表工作流程的单个实例;它包含工作流程的定义和执行状态。我们应该将工作流程视为一个“活动”对象。模板可以被认为是函数;它们定义要执行的指令。entrypoint
字段定义了主函数是什么,意味着将首先执行的模板。
在代码清单 9.2 中,我们看到了一个四步顺序工作流程:check-new-data
-> transform_data
-> save-into-db
-> notify-data-science-team
。每个步骤都可以引用一个模板,并且步骤通过工件(文件)传递参数。例如,check-new-data
引用了data-checker
模板,该模板定义了用于检查是否有新数据的 Docker 镜像。data-checker
模板还声明了步骤输出——新到达的数据文件路径——将被保存到/tmp/data_paths.txt
作为其输出值。
接下来,步骤transform_data
将check-new-data
的输出绑定到 data-converter 模板的输入。这就是变量在步骤和模板之间移动的方式。一旦您提交了工作流程——例如,argo
submit
-n
argo
sample_workflow.yaml
——您可以使用 Argo Workflows UI 或以下命令来查看工作流运行的详细信息:
# list all the workflows
argo list -n argo
# get details of a workflow run
argo get -n argo {workflow_name}
除了使用argo
命令之外,我们还可以使用 Kubernetes CLI 命令来检查工作流的执行,因为 Argo Workflows 在 Kubernetes 上原生运行;请参考以下示例:
# list all argo customer resource definitions
kubectl get crd -n argo
# list all workflows
kubectl get workflows -n argo
# check specific workflow
kubectl describe workflow/{workflow_name} -n argo
要了解更多关于 Argo Workflows 的信息,您可以查看 Argo Workflows 用户指南(mng.bz/WAG0
)和 Argo Workflows 架构图(argoproj.github.io/argo-workflows/architecture
)。
代码 Docker 化:轻松进行生产部署
Argo Workflows 本质上是一个 Kubernetes Pod(Docker 镜像)调度系统。尽管它强迫人们将其代码编写成一系列 Docker 镜像,但它在编排系统内部创建了极大的灵活性和隔离性。因为代码以 Docker 形式存在,所以可以由任何工作节点执行,而不用担心配置工作节点环境。
Argo Workflows 的另一个优点是生产部署成本低。当您在 Docker 中本地测试代码时,Docker 镜像(原型代码)可以直接在 Argo Workflows 中使用。与 Airflow 不同,Argo Workflows 几乎不需要从原型代码转换为生产工作流程的工作量。
关键特性
Argo Workflows 提供以下关键特性:
-
安装和维护成本低—Argo Workflows 在 Kubernetes 上原生运行,因此您可以只使用 Kubernetes 进程来解决任何问题;无需学习其他工具。此外,它的安装非常简单;只需几个
kubectl
命令,您就可以在 Kubernetes 环境中运行 Argo Workflows。 -
稳健的工作流程执行—Kubernetes pod 为 Argo Workflows 任务执行提供了良好的隔离。Argo Workflows 还支持 cron 工作流程和任务重试。
-
模板化和可组合性—Argo Workflows 模板就像函数一样。在构建工作流程时,Argo Workflows 支持组合不同的模板(步骤函数)。这种可组合性鼓励团队之间共享通用工作,从而大大提高了生产率。
-
完整的 UI 功能—Argo Workflows 提供了一个方便的 UI 来管理工作流程的整个生命周期,例如提交/停止工作流程、列出所有工作流程和查看工作流程定义。
-
高度灵活和适用—Argo Workflows 定义了 REST API 来管理系统和添加新功能(插件),并且工作流程任务定义为 Docker 镜像。这些特性使得 Argo Workflows 在许多领域,如 ML、ETL、批处理/数据处理和 CI/CD(持续集成和持续交付/持续部署)中被广泛使用。
-
生产质量—Argo Workflows 设计用于在严肃的生产环境中运行。Kubeflow pipeline 和 Argo CD 是将 Argo Workflows 用于生产环境的绝佳示例。
限制
使用 Argo Workflows 在深度学习系统中的缺点如下:
-
每个人都将编写和维护 YAML 文件—Argo Workflows 要求工作流程定义为 YAML 文件中的 Kubernetes CRD。一个项目的短小 YAML 文件可以管理,但一旦工作流程数量增加并且工作流程逻辑变得更加复杂,YAML 文件可能变得冗长和混乱。Argo Workflows 提供了模板以保持工作流程定义简单,但除非您习惯使用 Kubernetes YAML 配置,否则这仍然不太直观。
-
必须是 Kubernetes 专家—如果您是 Kubernetes 专家,您会觉得这是司空见惯的。但是初学者可能需要花费相当多的时间学习 Kubernetes 的概念和实践。
-
任务执行延迟 —— 在 Argo Workflows 中,对于每个新任务,Argo 将启动一个新的 Kubernetes Pod 来执行它。Pod 的启动可能会为每个单独的任务执行引入秒数或分钟,这限制了 Argo 在支持时间敏感的工作流时的能力。例如,Argoflow 不适用于实时模型预测工作流,该工作流以毫秒级 SLA 运行模型预测请求。
9.3.3 Metaflow
Metaflow 是一个以人为本的 Python 库,专注于 MLOps。它最初是在 Netflix 开发的,并于 2019 年开源。Metaflow 的特点在于它遵循以人为本的设计;它不仅用于自动化工作流程,还旨在减少在深度学习项目开发中花费的人工时间(操作成本)。
在第 9.1.3 节中,我们指出原型代码转换为生产工作流会在 ML 开发中产生很多摩擦。数据科学家必须为每个模型开发迭代构建和测试新版本的工作流。为了弥合原型和生产之间的差距,Metaflow 进行了两项改进:首先,它简化了工作流程的构建;其次,它统一了本地和生产环境之间的工作流程执行体验(参见图 9.7)。
图 9.7 Metaflow 在原型和生产之间提供了统一的开发体验。
在图 9.7 中,我们可以看到 Metaflow 将原型和生产环境都视为一流的执行环境。由于 Metaflow 库提供了一组统一的 API 来抽象实际的基础设施,一个工作流可以在不同的环境中以相同的方式运行。例如,一个工作流可以在本地调度器和生产调度器上运行而无需任何更改。本地调度器在本地执行工作流,而生产调度器集成到其他生产编排系统中,例如 AWS Step Functions 或 Argo Workflows。
Metaflow 允许用户注释 Python 代码 —— 一个 DAG Python 类 —— 来定义工作流程。然后 Metaflow 库会根据 Python 注释自动创建/打包工作流。使用 Metaflow Python 注释,Vena 可以在不更改任何原型代码的情况下构建工作流程。
除了无缝创建和测试工作流之外,Metaflow 还提供其他一些对模型可重复性至关重要的实用功能,如工作流/步骤版本控制和步骤输入/输出保存。要了解更多关于 Metaflow 的信息,您可以查看 Metaflow 的官方网站(docs.metaflow.org/
)和一本名为《Effective Data Science Infrastructure》的精彩 Metaflow 书籍,作者是 Ville Tuulos(Manning,2022;www.manning.com/books/effective-data-science-infrastructure
)。
典型用例
让我们使用 Metaflow 自动化我们在 9.3.1 和 9.3.2 节中看到的相同的数据处理工作。请参见伪代码清单以下示例。
图 9.3 显示了一个 Metaflow 工作流的示例
# define workflow DAG in a python class
class DataProcessWorkflow(FlowSpec):
# define "data source" as an input parameter for the workflow
data_source = Parameter(
"datasource_path", help="the data storage location for data process"
, required=True
)
@step
def start(self):
# The parameter “self.data_source” are available in all steps.
self.newdata_path = dataUtil.fetch_new_data(self.data_source)
self.next(self.transform_data)
@step
def transform_data(self):
self.new_data = dataUtil.convert(self.newdata_path)
# fan out to two parallel branches after data transfer.
self.next(self.save_to_db, self.notify_data_science_team)
@step
def save_to_db(self):
dataUtil.store_data(self.new_data)
self.next(self.join)
@step
def notify_data_science_team(self):
slackUtil.send_notification(messageUtil.build_message(self.new_data))
self.next(self.join)
# join the two parallel branches steps:
# notify_data_science_team and save_to_db
@step
def join(self, inputs):
self.next(self.end)
@step
def end(self, inputs):
# end the flow.
pass
if __name__ == "__main__":
DataProcessWorkflow()
在代码清单 9.3 中,我们看到 Metaflow 通过使用代码注释的新方法构建工作流。通过在函数上注释 @step
并使用 self.next
函数来连接步骤,我们可以轻松地从我们的原型代码构建一个工作流 DAG(图 9.8)。
图 9.8 显示了从图 9.3 构建的工作流 DAG
这里的一大优势在于,我们不需要在一个单独的系统中定义工作流 DAG 并将代码重新打包到不同的格式(比如 Docker 镜像)中。Metaflow 工作流完全融入我们的代码中。工作流开发和原型代码开发发生在同一地方,并且可以从整个 ML 开发周期的开始到结束一起进行测试。
代码准备就绪后,我们可以在本地验证和运行工作流。参见以下示例命令:
# display workflow DAG
python data_process_workflow.py show
# run the workflow locally
python data_process_workflow.py run
一旦我们完成了本地开发和测试,就该将工作流推送到生产环境了,可以通过以下两个命令来实现:
# push the workflow from local to AWS step functions
python data_process_workflow.py --with retry step-functions create
# push the workflow from local to Argo workflows
python data_process_workflow.py --with retry argo-workflows create
这些命令将我们在代码清单 9.3 中定义的数据处理工作流导出到 AWS Step Functions 和 Argo Workflows。然后,您还可以在 AWS Step Functions UI 或 Argo Workflows UI 中按名称搜索流程,从而查看导出的流程。
注意 Metaflow 在本地和生产环境之间提供了统一的开发体验。由于 Metaflow 提供的统一 API,我们在本地和生产环境中测试代码和工作流时拥有无缝的体验。无论使用哪种后端工作流编排系统,无论是 Metaflow 本地调度器、Argo Workflows 还是 AWS Step Functions,工作流开发的 Metaflow 用户体验都是相同的!
关键功能
Metaflow 提供以下关键功能:
-
将代码结构化为工作流 — Metaflow 允许用户通过对 Python 代码进行注释来创建工作流,这极大地简化了工作流的构建。
-
可重复性 — Metaflow 保留了执行每个工作流步骤所需的数据、代码和外部依赖项的不可变快照。Metaflow 还记录了每个工作流执行的元数据。
-
版本控制 — Metaflow 通过对工作流中的所有代码和数据进行哈希处理来解决 ML 项目的版本控制要求。
-
稳健的工作流执行 — 元数据通过使用 @conda 装饰器在工作流级别和步骤级别提供了依赖管理机制。它还提供了任务重试。
-
ML 的可用性设计 — Metaflow 将原型设计和生产视为同等重要。它提供了一组统一的 API 来抽象基础设施,因此相同的代码可以在原型环境和生产环境中运行而无需任何更改。
-
无缝扩展性—Metaflow 集成了 Kubernetes 和 AWS Batch,允许用户轻松定义所需的计算资源,并可以并行执行任意数量的工作流步骤。例如,通过对步骤函数应用像
@batch(cpu=1,
memory=500)
这样的注解,Metaflow 将与 AWS Batch 合作分配所需的资源来计算此步骤。
局限性
在深度学习系统中使用 Metaflow 的缺点如下:
-
没有条件分支支持—Metaflow 步骤注解不支持条件分支(仅在满足条件时执行步骤)。这不是一个红旗,但是这是一个很好的功能。
-
没有作业调度程序—Metaflow 本身不带有作业调度程序,因此无法使用 cron 工作流。这并不是一个大问题,因为 Metaflow 可以与支持作业调度的其他编排系统集成,例如 AWS Step Functions 和 Argo Workflows。
-
与 AWS 紧密耦合—Metaflow 的最重要特性与 AWS 紧密耦合,例如,Amazon S3 和 AWS Batch。幸运的是,Metaflow 是一个开源项目,因此可以将其扩展到非 AWS 替代方案。
9.3.4 何时使用
如果您正在寻找一种用于自动化非 ML 项目工作流执行的编排系统,Airflow 和 Argo Workflows 都是不错的选择。它们拥有出色的社区支持,并且在 IT 行业被广泛使用。如果您的系统在 Kubernetes 上运行,并且您的团队习惯使用 Docker,那么 Argo Workflows 将是一个很好的选择;否则,Airflow 也不会让您失望。
如果您正在寻找一个能够简化 ML 项目开发流程的系统,Metaflow 强烈推荐。Metaflow 不仅是一个编排工具;它是一个 MLOps 工具,旨在节省数据科学家在 ML 开发周期中的时间。由于 Metaflow 抽象了 ML 项目的后端基础设施部分,数据科学家可以专注于模型开发,而无需担心生产转换和部署。
总结
-
工作流是某个更大任务的操作序列。工作流可以看作是步骤的 DAG。步骤是最小的可恢复计算单元,描述了要执行的操作;步骤要么全部成功,要么全部失败。DAG 指定了步骤之间的依赖关系和执行顺序。
-
工作流编排意味着根据工作流的有向无环图(DAG)中定义的顺序执行工作流步骤。
-
采用工作流鼓励工作共享、团队协作和自动化。
-
在深度学习项目中应用工作流的主要挑战是降低工作流构建成本,简化工作流测试和调试。
-
构建/评估工作流编排系统的六个推荐设计原则是关键性、可用性、可扩展性、任务隔离性、可扩展性和以人为中心。
-
在选择非机器学习项目的编排系统时,Airflow 和 Argo Workflows 都是不错的选择。如果项目在 Kubernetes 和 Docker 上运行,Argo Workflows 是更好的选择。
-
在选择机器学习项目的编排系统时,Metaflow 目前是最佳选择。
第十章:生产路径
本章涵盖
-
在生产化深度学习模型之前的初步工作和任务
-
使用深度学习系统生产化深度学习模型
-
用于生产中实验的模型部署策略
对于书的结尾章节,我们认为回到高层次视角并连接前几章的所有内容是有意义的。我们现在已经详细讨论了深度学习系统中的每个服务。在本章中,我们将讨论这些服务如何共同支持我们在第一章介绍的深度学习产品开发周期。如果你还记得的话,该周期将研究和数据科学的努力一直延伸到生产化,最终产品是客户使用的产品。
作为提醒,图 10.1 取自第一章,展示了产品开发周期。本章我们的重点将放在该过程末尾发生的三个阶段上:深度学习研究、原型制作和生产化。这意味着我们将忽略实验、测试、训练和探索的循环,并关注如何将最终产品从研究阶段转化为最终产品,使其准备好发布到公众。
图 10.1 这个深度学习开发周期是将深度学习从研究转化为成品的典型情景。框 (3)、(4) 和 (5) 是本章的重点。
定义 生产化是生产出一个值得用户消费的产品的过程。生产值得性通常被定义为能够服务于客户请求、承受一定水平的请求负载,并优雅地处理诸如格式不正确的输入和请求过载等不利情况。
正如我们所说,本章重点讨论从研究、原型制作到生产化的生产周期路径。让我们将这三个阶段从图 10.1 所示的典型开发周期中拿出来,以便更详细地查看它们。我们将这些阶段放在下一个图表中,图 10.2,并放大它们,以显示每个阶段内的步骤,以及三个阶段之间的连接方式。不要让这个图表的复杂性吓到你!在本章中,我们将带领你走过每个阶段和每个步骤。
图 10.2 生产样品路径中的三个主要阶段。在生产化之前,研究和原型制作会经历许多迭代。
让我们简要回顾一下这张图表,因为它将提供本章的预览。图 10.2 中的前两个阶段是研究和原型制作。这两项工作都需要从模型训练和实验中进行快速迭代和周转。这些阶段(步骤 1–8)中的主要交互点是笔记本环境。使用笔记本,研究人员和数据科学家会调用数据集管理服务来跟踪训练数据集(在步骤 2 和 6 中),并可能使用训练服务和超参数优化库/服务来进行模型训练和实验(在步骤 4 和 8 中)。我们在 10.1 节中详细介绍了这些阶段,直到训练数据形状和代码变得相当稳定并且可以进行产品化。换句话说,团队已经提出了更多或更少的最终版本,并且准备通过最后的步骤将其发布给公众。
在 10.2 节中,我们将从上一节开始,介绍模型的产品化,直到模型被提供给生产推断请求流量的点。
定义 推断请求 是用户或应用程序针对经过训练的模型生成推断的输入。以视觉识别为例。推断请求可以是一张猫的图片。使用经过训练的视觉识别模型,或者推断,可以生成一个形式为 猫 的标签。
这个部分对应于图 10.2 中的第三个也是最后一个阶段。在产品化中,我们系统中的几乎每个服务都会发挥作用。数据集管理服务管理训练数据;工作流管理服务启动和跟踪训练工作流;训练服务执行和管理模型训练作业;元数据和工件存储包含和跟踪代码工件、训练模型及其元数据;模型服务将经过训练的模型提供给推断请求流量。
从产品化转移到部署。在 10.3 节中,我们将研究一些支持在生产中更新模型到新版本的模型部署策略。这些策略还支持在生产中进行实验。这里的主要重点将放在模型服务上,因为这是所有推断请求都被服务的地方。
通过全程跟踪到产品的完整过程,我们希望您能够看到我们在前几章中讨论的第一原则如何影响使用该系统提供深度学习功能的不同方。您从本章中获得的理解应该有助于您将自己的设计适应不同的情况。我们将使用图像识别产品的开发作为示例,以说明所有操作步骤。
10.1 准备产品化
在本节中,我们将研究深度学习模型从诞生前到准备投入生产的过程。在图 10.3 中,我们突出显示了深度学习研究和原型设计的阶段,这些阶段来自更大的深度学习开发循环(如图 10.1 所示)。我们将从深度学习研究阶段开始,在这个阶段,模型训练算法诞生。并不是每个组织都进行深度学习研究,有些使用现成的训练算法,如果这种情况不适用于您,请随意跳过这一步。
图 10.3 深度学习研究和原型设计阶段通往生产的路径摘录
在深度学习研究之后,我们继续进行原型设计。在此阶段,我们假定算法已经准备好用于训练模型。数据探索和实验模型训练的快速迭代过程构成了这一步骤的核心。这一步的目标是找到适当的训练数据形状,并开发一个稳定的模型训练代码库。
10.1.1 研究
通过研究发明了新的深度学习算法,并通过研究改进了现有算法。因为同行评审研究需要可重复的结果,所以模型训练数据需要公开获取。许多公共数据集,例如 ImageNet,都可以供研究团队使用。
笔记本环境,如 JupyterLab,是研究人员原型设计模型培训最流行的选择,因为它的交互性和灵活性。让我们看一下研究人员在模型训练原型设计期间可能采取的一些示例步骤:
-
深度学习研究员 Alice 正在致力于改进视觉识别算法。经过理论探讨后,她准备开始原型设计。
-
Alice 在 JupyterLab 中创建了一个新的笔记本。
-
Alice 想要使用 ImageNet 数据集来训练和基准测试她的算法。她可能
-
编写代码将数据集下载到她的笔记本中,并在数据集管理服务中存储它以供重复使用(第二章)。
-
发现数据集已经存储在数据集管理服务中,并编写代码来直接使用它。
-
-
Alice 开始对现有的视觉识别算法进行改进,直到它能够在笔记本中本地生成实验模型。
-
Alice 尝试更改一些超参数,训练和测试一些实验模型,并比较它们生成的指标。
-
Alice 可以进一步使用超参数优化技术(第五章)自动运行更多实验,以确认她确实对现有算法进行了改进。
-
Alice 发布了她的研究结果,并将她的训练代码改进打包成一个库供他人使用。
通过使用版本化数据集进行训练,爱丽丝确保她所有实验模型训练运行的输入训练数据是相同的。她还使用源控制管理系统,如 Git,以跟踪她的代码,以便所有实验模型都可以追溯到她的代码版本。
注意,在这个阶段,模型训练通常在笔记环境托管的计算节点上进行,因此很有必要为这些节点分配足够的资源。如果训练数据存储在网络上,请确保读取速度不会成为模型训练的瓶颈。
10.1.2 原型制作
原型制作是将研究与实际用例联系起来的实践。它是寻找合适的训练数据、算法、超参数和推断支持的正确深度学习特征的实践,以满足产品需求。
在这个阶段,很常见发现笔记环境仍然是数据科学家和工程师的首选,原因是原型制作的快速迭代性质。期望快速交付。让我们走一遍原型制作的一个可能的场景:
-
模型开发团队收到产品需求,要改善安全摄像头产品的运动检测。
-
基于需求,团队发现爱丽丝的新视觉识别训练算法可能有助于改善运动检测。
-
团队创建一个新的笔记本,并开始探索与他们选择的算法集相关的用于模型训练的数据:
-
如果团队有已收集的数据与正在解决的问题相匹配,他们可能能够使用现有数据进行模型训练。
-
在某些情况下,团队可能需要收集新数据进行训练。
-
-
在大多数情况下,这个阶段应用迁移学习,并且团队会选择一个或多个现有模型作为源模型。
-
团队开发带有算法的建模代码,并使用已收集的数据和源模型训练实验模型。
-
实验模型经过评估,以查看是否产生令人满意的结果。步骤 3 到 6 会重复进行,直到训练数据形状和代码稳定。
我们称步骤 3 到 6 为探索循环。这个循环对应于图 10.3 中原型制作的放大部分的迭代圈。当原型制作开始时,这个循环会迅速迭代。此阶段的重点是缩小训练数据形状和代码。
一旦训练数据形状和代码稳定,它们将准备好进行进一步的调整和优化。这个阶段的目标是收敛到一个状态,使得模型训练和推断代码可以被打包并部署到生产环境。
10.1.3 关键收获
我们已经走过图 10.1 中我们参考的深度学习开发周期中的研究和原型制作阶段。尽管它们有不同的目的,但我们看到它们在如何处理深度学习系统方面有相当大的重叠:
-
笔记本环境是研究和预生产原型设计的常见选择,因为它具有高度的交互性和冗长性。
-
对训练数据的访问应尽可能宽泛和灵活(在合法性和合规性的限制范围内),这有助于加速数据探索过程。
-
应为模型训练分配足够的计算资源,以确保周转时间短。
-
至少使用数据集管理服务和源代码控制管理系统来跟踪实验模型的来源。此外,使用元数据存储来包含指标,并将其与训练数据集和代码关联起来,以进行完整的渊源追踪。
10.2 模型生产化
在深度学习模型能够集成到最终产品之前,它们需要经历生产化过程。对于这个术语肯定有很多解释,但基本上是:
-
模型需要为生产推断请求提供服务,无论是来自最终产品还是最终用户。
-
模型服务应满足预定义的服务水平协议,例如在 50 毫秒内响应或可用时间达到 99.999%。
-
与模型相关的生产问题应易于故障排除。
在本节中,我们将看看深度学习模型如何从一个相当动态的环境,比如笔记本,过渡到一个生产环境,在那里它们会受到各种严苛条件的影响。图 10.4 显示了生产化阶段相对于开发周期的其余部分。让我们回顾一下这个阶段的步骤。
图 10.4 生产化阶段的生产路径摘录
10.2.1 代码组件化
如前一节所示,在原型设计期间,常常将训练数据准备、模型训练和推断代码存在于单个笔记本中。为了将它们生产化为一个深度学习系统,我们需要将它们拆分为单独的组件。拆分组件的一种方法,即代码组件化,如图 10.5 所示。
图 10.5 将代码从单个笔记本组件化为可以单独打包的多个部分。第一个分割发生在训练好的模型是输出的地方。可选的第二次分割发生在训练数据是输出的地方。
让我们将图中的流程付诸实施。在代码中划分的第一条分割线是模型是输出的地方。这应该导致两段代码如下所示:
-
输出模型的模型训练代码
-
模型推断代码,以模型和推断请求作为输入,产生推断作为输出
可选择地,模型训练代码可以分为以下几部分:
-
训练数据转换代码,以原始数据作为输入,并输出可被模型训练代码使用的训练数据
-
模型训练代码,它以训练数据作为输入,并训练出一个模型作为输出
如果你有其他受益于相同类型准备好的数据的模型训练代码,将这些代码分离开是个好主意。如果你的数据准备步骤需要以不同的节奏执行模型训练,分离也是一个好主意。
10.2.2 代码打包
一旦代码组件被清晰地分离,它们就可以被打包部署。为了能够在训练服务(第三章)、模型服务(第六章)和工作流服务(第九章)上运行它们,我们首先需要确保它们遵循这些服务设置的约定。
模型训练代码应修改为从训练服务设置的环境变量指示的位置获取训练数据。其他组件应遵循类似的约定。
模型推理代码应遵循您选择的模型服务策略的约定:
-
如果你使用直接的模型嵌入,需要与嵌入模型的团队合作,确保你的推理代码可以正常工作。
-
如果你计划使用模型服务来提供模型,确保你的推理代码提供了一个接口,使得模型服务可以进行通信。
-
如果你使用模型服务器,只要模型服务器能够正确地提供模型,你可能就不需要模型推理代码。
我们将这些代码组件打包为 Docker 容器,以便它们可以被它们各自的主机服务启动、访问和跟踪。如何做到这一点的示例可以在附录 A 中找到。如果需要特殊的数据转换,我们可以将数据转换代码集成到数据管理服务中。
10.2.3 代码注册
在训练代码和推理代码可以被系统使用之前,它们的包必须被注册并存储到元数据和工件服务中。这提供了训练代码和推理代码之间的必要联系。让我们看看它们是如何相关的(图 10.6)。
图 10.6 在生产深度学习系统中的简单训练和推理执行流程
一旦训练和推理代码被打包为容器(如图中的训练容器和推理容器),它们可以使用一个共同的句柄(例如visual_recognition
,就像图 10.6 中所示的示例一样)注册到元数据和工件存储中。这有助于系统服务在接收到提供相同句柄名称的请求时找到并使用正确的代码容器。我们将在接下来的几个部分继续讲解这个图。
10.2.4 训练工作流程设置
我们建议即使您不经常训练模型,也要设置训练工作流程。 主要原因是在生产中提供相同模型训练流程的可重复性。 当除您之外的其他人需要训练模型并且可以使用您设置的流程时,这非常有帮助。 在某些情况下,生产环境是隔离的,并且通过在生产环境中设置的工作流程可能是在那里生成模型的唯一方法。 在图 10.7 中,我们已将先前图表的模型训练部分放大,以便您可以看到细节。
图 10.7 典型的生产模型训练设置。 工作流服务管理训练流程的何时以及如何运行。 训练服务运行模型训练作业。 元数据和工件存储提供训练代码,存储训练过的模型,并将其与元数据关联起来。
参考图 10.7,一旦设置了 visual_recognition
的训练工作流程,就可以触发训练到训练服务。 训练服务使用句柄从元数据和工件存储中查找要执行的训练代码容器。 一旦模型训练完成,它会使用句柄名称将模型保存到元数据和工件存储中。
在这个阶段,通常也会发现使用超参数优化技术来在模型训练期间找到最佳训练超参数。 如果使用了 HPO 服务,则工作流程将与 HPO 服务而不是直接与训练服务进行通信。 如果您需要提醒 HPO 服务如何工作,请参阅第五章。
10.2.5 模型推理
一旦模型在生产环境中训练并注册,下一步是确保它能够处理系统中进入的推理请求,并在一定的速率内产生推理。 我们可以通过将推理请求发送到模型服务来实现这一点。 当模型服务收到请求时,它会在请求中找到句柄名称 visual_recognition
,并查询元数据和工件存储以获取匹配的模型推理容器和模型文件。 然后,模型服务可以一起使用这些工件来生成推理响应。 您可以在图 10.8 中看到这个过程,再次强调,这是图 10.6 模型服务部分的放大版本。
图 10.8 典型的生产模型服务设置。 当推理请求到达模型服务时,服务使用元数据和工件存储查找推理容器和模型,以产生推理响应。
如果您使用模型服务器,可能需要在其前面加上一层薄膜,以便它知道从哪里获取模型文件。 一些模型服务器实现支持自定义模型管理器实现,也可以用于针对元数据和工件存储进行查询以加载正确的模型。
10.2.6 产品集成
在从模型服务获得适当的推断响应后,就该将模型服务客户端集成到将使用这些推断的产品中。这是生产化的最后一步,我们在推出给最终客户之前需要确保检查几个问题。因为我们正在改进安全摄像头产品的运动检测,所以我们必须将一个模型服务客户端集成到安全摄像头视频处理后端中,以便从新改进的模型请求推断:
-
确保推断响应可被使用它的产品消耗。
-
通过以接近生产流量的速率发送推断请求来进行压力测试推断。
-
通过使用不规范的推断请求进行测试推断,以确保其不会破坏模型推断代码或模型服务。
这只是一个非常基本的检查项目列表。您的组织可能会定义更多生产准备性标准,您需要在集成之前满足这些标准。除了可以告诉我们模型是否正常提供推断请求的系统指标外,我们还应该设置业务指标,这些指标将告诉我们模型是否有助于实现业务用例。
10.3 模型部署策略
在上一节中,我们通过一个从原型设计到生产的示例路径。这个过程假设模型是首次部署,没有现有版本的模型需要替换。一旦模型在生产中使用,除非有维护窗口允许,否则通常需要使用模型部署策略来确保生产推断请求流量不会中断。事实上,这些模型部署策略也可以作为在生产中进行实验的方式,通过使用前一节中设置的业务指标。我们将看下三种策略:金丝雀、蓝绿和多臂赌博机。
10.3.1 金丝雀部署
金丝雀部署(类似 A/B 测试)是指在保留旧模型为大多数请求提供服务的同时,将新模型部署到生产推断请求的一小部分上。示例如图 10.9 所示。这需要模型服务支持将一小部分推断请求流量分段和路由到新模型。
图 10.9 金丝雀部署示例,显示将一小部分流量重定向到新模型的版本
使用这种策略,可能由于部署新模型而产生的任何不利影响都局限在少部分最终用户之内。通过将所有推断请求流量路由回旧模型,回滚变得相当简单。
这种方法的一个缺点是,您只能了解到模型对一小部分最终用户的性能。将新模型发布以服务于所有推断请求流量可能会产生与仅为流量的一小部分提供服务时所观察到的不同效果。
10.3.2 蓝绿部署
在我们的上下文中,蓝绿部署意味着部署一个新模型,将所有推理请求流量路由到新模型,并保持旧模型在线,直到我们确信新模型的性能符合预期。在实现上,它是三种策略中最简单的,因为根本没有流量分割。服务所需做的一切就是内部指向新模型以服务所有推理请求。图 10.10 描绘了蓝绿部署。
图 10.10 蓝绿部署显示所有流量指向旧(蓝色)或新(绿色)模型的方向
这种策略不仅简单,而且在为所有终端用户提供服务时可以全面了解模型的表现。回滚也很简单。只需将模型服务指向旧模型即可。
这种方法的明显缺点是,如果新模型出现问题,将影响所有终端用户。当您基于新模型开发新产品功能时,这种策略可能是合理的。随着您随时间迭代训练更好的模型,您可能希望摆脱这种策略,因为终端用户会根据稳定的体验建立他们的期望。
10.3.3 多臂赌博机部署
多臂赌博机(MAB)是三种策略中最复杂的部署策略。MAB 指的是一种技术,它持续监控多个模型的性能,并随着时间的推移将越来越多的推理请求流量重定向到胜利模型。这使用了模型服务最复杂的实现,因为它要求服务了解模型性能,这取决于您的模型性能指标如何定义。MAB 部署在图 10.11 中说明。
图 10.11 多臂赌博机部署显示了第 0 天和第 1 天的流量模式。注意,模型 v2.0a 在第 1 天的模型性能方面处于领先地位,因为它接收到了最多的流量。
不过,这种策略确实带来了一个优势,因为它在一定的时间范围内最大化了表现最佳模型的利益,而使用金丝雀部署,如果新模型胜过旧模型,您可能只会获得最小的利益。注意,您应确保模型服务报告流量分割随时间的变化。这有助于与模型的性能相关联。
摘要
-
深度学习研究团队发明并改进用于训练模型的深度学习算法。
-
模型开发团队利用现有的算法和可用数据来训练帮助解决深度学习用例的模型。
-
研究和原型制作都需要与代码开发、数据探索和可视化的高度互动。笔记本环境是这些团队的流行选择。
-
数据集管理服务可以在研究和原型制作过程中使用,帮助跟踪用于训练实验模型的训练数据。
-
一旦训练数据和代码足够稳定,投入生产的第一步是打包模型训练代码、模型推理代码和任何源模型。
-
所有深度学习系统的服务都可以使用这些软件包来训练、跟踪和提供模型。
-
一旦模型训练工作流程开始运行并获得令人满意的推理响应,就可以开始与最终用户产品的集成。
-
如果提供推理请求不能中断,则需要一个模型部署策略。
-
多种模型部署策略可供选择,它们可以兼作在生产中进行实验。
附录 A:一个“你好世界”深度学习系统
本书是关于教授构建适合自己情况的深度学习系统的设计原则。但你可能会想知道一个深度学习系统看起来是什么样子。或者人们在实践中如何、为什么以及何时使用这样一个系统。这些都是现阶段很好的问题。
我们相信学习新思想、技能和方法的最佳方法是通过实践——通过获取一些示例并看看你能做些什么。为了帮助你,我们建立了一个迷你深度学习系统和一个代码实验室供你使用。在这个“你好世界”深度学习系统中玩耍应该有助于你建立理解本书介绍的概念和原则的知识库。为了使这个示例系统易于理解,我们侧重于深度学习系统的关键组件,如数据集管理(DM)和模型训练与服务。整个迷你系统可以通过一个 bash 脚本轻松设置在你的本地系统上,并且它的组件在后面的章节中会详细讨论。
在这个附录中,我们将首先参观我们的示例系统,然后进行实验室练习,让你体验深度学习系统中最常见的用户活动,包括数据集摄入、模型训练和模型服务。虽然我们的示例系统非常简化,但它涵盖了深度学习系统的所有基础知识。通过阅读这个附录,你不仅会获得一个关于基本深度学习系统是如何组织和运作的实践理解,而且还会对本书其余部分讨论的示例服务有一个全面的了解。
A.1 介绍“你好世界”深度学习系统
从用户的角度来理解软件系统的最快方法。因此,在这个介绍部分,我们首先将看看深度学习系统的用户:人物角色及其责任。然后,我们将深入探讨系统设计、主要组件和用户工作流程。
A.1.1 人物角色
为了将复杂性降到最低,我们的示例迷你深度学习系统只有四个人物角色,或角色:数据工程师、数据科学家/研究员、系统开发人员和深度学习应用程序开发人员。我们选择了这四个角色,因为它们是保持深度学习系统运行所需的最少人员。在这个“你好世界”系统中,每个角色的角色定义和工作描述在以下各节中列出。
注意 这里描述的角色责任是过于简化的,因为我们想集中在深度学习系统的最基本工作流程上。关于深度学习系统中涉及的人物角色和职责的更详细定义,请参阅第 1.1 节。
A.1.2 数据工程师
数据工程师负责收集、处理和存储用于深度学习训练的原始数据。在这个小系统中,我们有一个 DM 服务用于存储用于模型训练的数据集。数据工程师将使用此服务上传原始训练数据。在我们的实验中,我们准备了一些意图分类数据集,供您体验此过程。
A.1.3 数据科学家/研究人员。
数据科学家或研究人员开发具有满足业务要求的模型训练算法和模型。它们是深度学习系统中模型训练基础设施的“客户”。
我们的示例系统包含一个训练服务,用于运行模型训练代码。在实验中,我们为你预先构建了一个意图分类训练代码,以体验模型训练执行。
A.1.4 系统开发人员。
系统开发人员构建整个深度学习系统并维护它,以确保所有的机器学习活动都正常运行。他们的活动包括数据集上传、模型训练和模型服务。
A.1.5 深度学习应用开发人员。
深度学习应用开发人员利用深度学习模型构建商业产品,如聊天机器人、自动驾驶软件和人脸识别移动应用程序。这些应用程序是任何深度学习系统的最重要的客户,因为它们为系统产生的模型创造业务影响(收入)。在我们的实验中,你将有机会想象自己是聊天机器人的客户,通过运行脚本向预测服务发送请求并分类你的消息。
A.1.6 样例系统概述。
我们的小型深度学习系统由四个服务和一个存储系统组成:
-
数据服务 — 用于存储和获取数据集。
-
模型训练服务 - 用于运行模型训练代码。
-
元数据存储服务 — 用于存储模型元数据,如模型名称、模型版本和模型算法。
-
预测服务 — 设计用于执行模型以处理客户的预测请求。
-
MinIO 存储 — 旨在在您的本地计算机上运行,作为与 Amazon S3 类似的对象存储。
几乎所有这些服务在本书中都有自己的章节,因此我们将能够更详细地研究它们。现在,我们只想提供你需要理解后面的用户场景和实验的高级概述。图 A.1 说明了样例系统(四个服务和存储系统)的主要组成部分及其相互依赖关系。
图 A.1 所示是样例深度学习系统的设计概述。
除了四个服务和存储(用矩形框表示),你会注意到这些框之间有很多有向箭头。这些箭头显示了样例系统内部服务的相互依赖关系。以下是这些依赖关系的解释:
-
DM 服务将数据集保存到 MinIO 存储中。
-
模型训练服务查询 DM 以准备训练数据集并获取训练数据。
-
模型训练服务从 MinIO 下载训练数据。
-
模型训练服务将模型文件保存到 MinIO。
-
模型训练服务将模型元数据保存到元数据存储服务中。
-
预测服务查询元数据存储以确定使用哪个模型。
-
预测服务从 MinIO 下载模型文件以提供预测请求。
A.1.7 用户工作流程
现在,我们已经介绍了人物角色和主要服务,让我们来看看用户工作流程。图 A.2 展示了每个角色的用户工作流程。
图 A.2 中的系统实现了 DM、训练、服务和系统维护四种不同的工作流程。
图 A.2 显示了每个角色使用迷你深度学习系统来执行自己的任务,利用图 A.1 中介绍的服务。让我们回顾一下每个工作流程:
-
场景 A ——数据工程师调用 DM 服务上传原始数据; DM 将数据摄取并保存到 MinIO 存储中的训练数据集格式中。
-
场景 B ——数据科学家首先编写训练代码,然后提交训练请求给训练服务。训练服务执行训练代码并生成模型。然后,它将模型元数据保存到元数据存储中,并将模型文件保存到 MinIO 存储中。
-
场景 C ——应用程序开发人员构建应用程序,调用预测服务来使用在场景 B 中训练的模型。
-
场景 D ——系统开发人员构建和维护此系统。
A.2 实验演示
现在是你开始学习的时候了。在这个实验练习中,你将在本地计算机上参与到 A.1.3 节中提到的用户场景中。为了使这个练习更生动,我们介绍了一些角色,这样你不仅会知道如何使用深度学习系统,还会了解谁负责处理每个不同的工作。虚构的角色包括伊万(一名数据科学家)、风(一名数据工程师)、唐(一名系统开发人员)和约翰娜(一名应用程序开发人员)。
A.2.1 演示步骤
在这个演示场景中,我们将有唐、风、伊万和约翰娜四个人一起合作,训练一个意图分类模型,并使用该模型对任意文本消息进行分类。这个场景模拟了一个典型模型开发工作流程的四个基本步骤:系统设置、数据集构建、模型训练和模型服务。
为了使实验易于运行,我们将所有微服务进行了 Docker 化,并构建了 shell 脚本来自动化实验设置和演示场景。通过按照我们 GitHub 仓库中 README 文件(github.com/orca3/MiniAutoML#lab
)中的说明,运行四个 shell 脚本,你可以完成实验。
注意:你可以在 github.com/orca3/MiniAutoML/tree/main/scripts
的 scripts 文件夹中找到实验演示脚本。该文件夹包含了整本书的演示脚本。以 lab- 开头的文件用于此演示,例如 lab-001-start-all.sh
(地址:mng.bz/zmv1
)可在本地系统上设置。对于未来的更新和成功执行实验脚本,请始终参考 GitHub 仓库中的说明。
第一步是系统设置。运行 scripts/lab-001-start-all.sh
(mng.bz/zmv1
)。
唐(系统开发者)通过运行 scripts/lab-001-start-all.sh
脚本启动迷你深度学习系统。该脚本将下载演示服务的预构建 Docker 镜像并执行它们。
当脚本执行完毕后,迷你深度学习系统已经启动并运行。你可以使用以下命令列出所有本地正在运行的 Docker 容器,以验证所有服务都在运行:
$ docker ps --format="table {{.Names}}\t{{.Image}}"
提供了用于运行实验的 Docker 容器,见下式。
附录 A.1 确认所有系统组件都在运行
NAMES IMAGE
training-service orca3/services:latest
prediction-service orca3/services:latest
intent-classification-torch-predictor pytorch/torchserve:0.5.2-cpu
intent-classification-predictor orca3/intent-classification-predictor
metadata-store orca3/services:latest
data-management orca3/services:latest
minio minio/minio
唐确认所有微服务都在运行后,系统已准备好供使用。他通知伊凡和冯开始他们的工作。
注意:如果你阅读过 lab-001-start-all.sh
脚本,你会发现系统中大多数服务(除了预测器)—例如数据管理和模型训练—都被打包到一个 Docker 镜像中(orca3/services
)。这不是一个推荐的生产用例模式,但由于使用的磁盘空间较少且执行简单,它适用于我们的演示需求。
第二步是构建训练数据集。运行 scripts/lab-002-upload-data.sh
(mng.bz/0yqJ
)。
冯(数据工程师)首先从互联网上下载原始数据,并进行一些训练的修改(参见 scripts/prepare_data.py
,地址:mng.bz/KlKX
)。然后,冯将处理后的数据上传到 DM 服务。数据集上传完成后,DM 服务会返回一个唯一的数据集 ID,以供将来参考。
我们已经将冯的工作自动化在 scripts/lab-002-upload-data.sh
脚本中。执行完此脚本后,将创建一个数据集。你可以在终端中看到 DM 服务打印出一个 JSON 对象。此 JSON 对象表示数据集的元数据,请参考以下示例。
附录 A.2 DM 服务中的样本数据集元数据
# DM returns dataset metadata for a newly created dataset
{
"dataset_id": "1", ❶
"name": "tweet_emotion", ❷
"dataset_type": "TEXT_INTENT", ❸
"last_updated_at": "2022-03-25T01:32:37.493612Z",
"commits": [ ❹
{ ❹
"dataset_id": "1", ❹
"commit_id": "1", ❹
"created_at": "2022-03-25T01:32:38.919357Z", ❹
"commit_message": "Initial commit", ❹
"path": "dataset/1/commit/1", ❹
"statistics": { ❹
"numExamples": "2963", ❹
"numLabels": "3" ❹
} ❹
} ❹
] ❹
}
❶ 数据集标识符
❷ 数据集名称
❸ 数据集类型
❹ 数据集审核历史记录
数据集元数据在第二章中有详细讨论。现在,我们可以忽略元数据 JSON 对象的大部分属性,只关注dataset_id
属性。数据集 ID是数据集的唯一标识符;你需要在第 3 步将此 ID 传递给训练服务以进行模型训练。一旦数据集准备好,Feng 就会通知 Ivan 使用dataset_id="1"
开始模型训练。
*步骤 3 是模型训练。*运行scripts/lab-003-first-training.sh
(mng.bz/vnra
)。
Ivan(数据科学家)首先构建意图分类训练代码(training-code/text-classification
位于github.com/orca3/MiniAutoML/tree/main/training-code/text-classification
)并将其打包成 Docker 镜像(mng.bz/WA5g
)。接下来,Ivan 向模型训练服务提交训练请求以创建模型训练作业。在训练请求中,他指定了在训练中使用的数据集(数据集 ID)和训练算法(Docker 镜像名称)。
注意 在本实验中,我们使用了硬编码的数据集 ID "1"
。要测试其他数据集,请随意将任何其他数据集 ID 设置为训练请求中。
一旦训练服务接收到训练请求,它将启动一个 Docker 容器来运行 Ivan 提供的意图分类训练代码。在我们的演示中,Docker 镜像是orca3/intent-classification
(mng.bz/WA5g
)。请运行实验脚本(scripts/lab-003-first-training.sh
位于mng.bz/916j
)来启动模型训练作业,该脚本设置了依赖项和参数。
列表 A.3 向训练服务提交训练作业
# send gRPC request to kick off a model training in training service.
function start_training() {
grpcurl -plaintext \
-d "{
\"metadata\": {
\"algorithm\":\"intent-classification\", ❶
\"dataset_id\":\"1\", ❷
\"name\":\"test1\",
\"train_data_version_hash\":$2, ❸
\"output_model_name\":\"twitter-model\", ❹
\"parameters\": { ❹
\"LR\":\"4\", ❹
\"EPOCHS\":\"15\", ❹
\"BATCH_SIZE\":\"64\", ❹
\"FC_SIZE\":\"128\" ❹
} ❹
}
}" \
localhost:"6003" training.TrainingService/Train
}
❶ 训练 Docker 镜像名称
❷ 要训练的数据集的 ID
❸ 数据集版本
❹ 训练超参数
一旦训练作业开始,训练服务将持续监视训练执行状态并返回作业 ID 以便跟踪。有了作业 ID,Ivan 可以通过查询训练服务的GetTrainingStatus
API 和元数据存储服务的GetRunStatus
API 获取最新的训练作业状态和训练元数据。查看示例查询请求如下。
列表 A.4 查询模型训练作业状态和模型元数据
# query training job status from training service
grpcurl -plaintext \
-d "{\"job_id\": \"1\"}" \ ❶
localhost:"6003" training.TrainingService/GetTrainingStatus
# query model training metrics from metadata store.
grpcurl -plaintext \
-d "{\"run_id\": \"1\"}" \ ❶
localhost:"6002" metadata_store.MetadataStoreService/GetRunStatus
❶ 模型 ID,也是训练作业 ID
训练服务可以实时返回训练执行状态;查看示例响应如下:
job 1 is currently in "launch" status, check back in 5 seconds
job 1 is currently in "running" status, check back in 5 seconds
job 1 is currently in "succeed" status ❶
❶ 训练完成
由于训练 Docker 容器在训练执行期间向元数据存储报告实时指标,如训练准确性,因此元数据存储服务可以返回实时训练指标。查看来自元数据存储服务的示例模型训练指标如下:
{
"run_info": {
"start_time": "2022-03-25T14:25:44.395619",
"end_time": "2022-03-25T14:25:48.261595",
"success": true, ❶
"message": "test accuracy 0.520", ❷
"run_id": "1", ❸
"run_name": "training job 1",
"tracing": {
"dataset_id": "1", ❹
"version_hash": "hashAg==", ❹
"code_version": "231c0d2"
},
"epochs": { ❺
"0-10": { ❺
"start_time": "2022-03-25T14:25:46.880859", ❺
"end_time": "2022-03-25T14:25:47.054872", ❺
"run_id": "1", ❺
"epoch_id": "0-10", ❺
"metrics": { ❺
"accuracy": "0.4925373134328358" ❺
} ❺
}, ❺
.. .. ..
}
}
❶ 训练状态
❷ 训练容器的最后一条消息
❸ 训练作业 ID,以及模型 ID
❹ 数据集标识符
❺ 每个时期的训练度量
训练完成后,Ivan 通知 Johanna 模型已经准备好使用。在我们的实验室中,他将模型 ID(作业 ID = "1"
)传递给 Johanna,以便她知道要使用哪个模型。
请注意,代码清单 A.3 和 A.4 中描述的所有 API 请求都在 scripts/lab-003-first-training.sh
中自动执行;您可以一次性执行它们。在第三章和第四章中,我们会详细讨论训练服务的工作原理。
步骤 4 是模型服务。 运行 scripts/lab-004-model-serving.sh
(mng.bz/815K
)。
Johanna(应用程序开发人员)正在构建一个聊天机器人,所以她想要使用新训练的意图分类模型来对客户的问题进行分类。当 Ivan 告诉 Johanna 模型已经准备好使用时,Johanna 向预测服务发送一个预测请求来测试新训练的模型。
在预测请求中,Johanna 指定了模型 ID(runId
)和文档,其中文档是正在被分类的文本消息。样本预测服务将自动加载预测请求中请求的模型。您可以在以下清单中看到一个样本 gRPC 预测请求。
清单 A.5 一个样本模型预测 gRPC 请求
grpcurl -plaintext \
-d "{
\"runId\": \"1\", ❶
\"document\": \"You can have a certain arrogance,
and I think that's fine, but what you should never
lose is the respect for the others.\" ❷
}" \
localhost:6001 prediction.PredictionService/Predict
❶ 模型 ID,以及训练作业 ID
❷ 请求体(文本)
在终端中执行查询(代码清单 A.5)或 scripts/lab-004-model-serving.sh
后,您将会看到以下来自模型服务的输出:对于给定文本,意图分类模型预测出的类别(标签)。
{
"response": "{\"result\": \"joy\"}" ❶
}
❶ 预测的类别是“快乐”。
如果在完成实验过程中遇到任何问题,请查看我们 GitHub 仓库 README 文件的实验部分(github.com/orca3/MiniAutoML#lab
)中的最新说明。如果示例系统被修改,我们会尽量保持这些说明的更新。
A.2.2 一个自己完成的练习
现在我们已经向您展示了一个完成的模型开发周期,现在是作业时间。想象一下,在成功发布聊天机器人之后,Johanna 的聊天机器人服务需要支持一个新类别,乐观。这个新需求意味着当前的意图分类模型需要重新训练以识别乐观类型的文本消息。
Feng 和 Ivan 需要共同合作建立一个新的意图分类模型。Feng 需要收集更多带有“乐观”标签的训练数据并将其添加到当前数据集中。虽然 Ivan 不需要更改训练代码,但他确实需要使用更新的数据集触发训练服务中的训练作业来构建一个新模型。
通过按照第 A.2.1 节中的示例查询和脚本,您应该能够完成 Feng 和 Ivan 的任务。如果您想要检查您的结果或者在完成这些任务时需要帮助,您可以在 scripts/lab-005-second-training.sh
文件中找到我们的解决方案。我们鼓励您在检查我们的解决方案之前尝试或者玩弄这个问题。
附录 B:现有解决方案调查
从零开始实施深度学习系统是一项庞大的工作。在某些情况下,特殊需求可能需要额外的努力来从头开始构建深度学习系统。在其他情况下,鉴于有限的资源和时间,使用现有组件,甚至整个系统,并将其定制为符合自己需求可能是有意义的。
本附录的目的是审查几个由不同云供应商和开源社区实施的深度学习系统。这些操作范围从无服务器部署到自定义服务容器部署。通过将它们与我们的参考架构进行比较,并突出它们的相似之处和不同之处,你将了解到哪些操作可以用来设计自己的项目。
如果你想看到我们将要涵盖的每个解决方案的快速摘要比较,请随意跳转到 B.5 节。另外,为了方便起见,在第 1.2.1 节介绍的参考架构在图 B.1 中重新发布。
图 B.1 典型深度学习系统概述,包括支持深度学习开发周期的基本组件。这个参考架构可以作为一个起点,并进一步定制。
B.1 亚马逊 SageMaker
亚马逊 SageMaker 是其人工智能产品系列的总称,可以一起使用形成完整的深度学习系统。在本节中,我们将回顾产品套件,并查看它们与我们的关键组件的比较。正如本节开头所提到的,我们进行这些比较是为了让你了解哪种产品能最好地帮助构建你自己的系统。
B.1.1 数据集管理
亚马逊 SageMaker 没有提供数据集管理组件,该组件提供统一的接口来帮助管理数据准备与深度学习系统不同类型用户的复杂交互。然而,亚马逊提供了一系列数据存储、转换和查询解决方案,可以用来构建数据管理组件。
可以构建一个数据管理组件,用于收集 Amazon S3 的原始数据,这是一款对象存储产品。元数据标记可以由 AWS Glue 数据目录支持,该目录可以由 AWS Glue ETL 用于进一步处理成可用于训练的数据集。阅读第二章后,你应该能够确定如何使用这些亚马逊产品构建自己的数据管理组件。
B.1.2 模型训练
Amazon SageMaker 支持内置算法和提供外部自定义代码进行深度学习模型训练。它还支持用于训练运行的容器。它公开了一个 API,可以通过调用来启动按需训练作业。这与驱动深度学习系统培训组件的计算后端非常相似,本书也对此进行了介绍。要实现训练组件的资源管理部分,您可以使用 Amazon 提供的现有工具,例如为不同的 AWS 身份和访问管理(IAM)用户或角色分配资源限制和策略。如果您的组织需要额外的控制或复杂性,或者已经有身份提供者实现,您可能需要花费更多的时间构建自定义解决方案。阅读第三章和第四章后,您应该能够弄清楚如何使用现有的 Amazon 工具来构建自己的训练组件。
B.1.3 模型服务
在其最基本的形式下,Amazon SageMaker 能够支持将训练后的模型部署为可以通过互联网访问的 Web 服务。为了扩展多个模型的部署而无需将它们部署到单独的端点,SageMaker 提供了一个多模型端点,同时也配备了可配置的模型缓存行为。如果这些工具符合您的需求,它们会很有用。截至本文撰写时,SageMaker 支持多容器端点和串行推理流水线,这与本书中描述的服务架构和 DAG 支持类似。第六章和第七章回顾了模型服务原则,以便您了解现有工具和在遇到现有工具的限制时如何构建自己的工具。
B.1.4 元数据和工件存储
作为以受过训练的模型为中心的组件,云供应商推出了专门的产品是不足为奇的。SageMaker 模型注册表提供了许多元数据和深度学习系统工件存储的关键概念相对应的功能。例如,可以使用模型注册表跟踪模型的训练度量和模型版本等元数据。然而,它并不提供同一组件中的工件存储解决方案。您可以轻松地在模型注册表和其他 Amazon 存储产品之上构建接口,以提供这个组件的工件存储方面。
在工件之间跟踪的另一种重要元数据类型是它们的谱系信息。SageMaker 提供了 ML 谱系跟踪作为一个独立的功能,它会自动跟踪这些信息。
在第八章中,我们将讨论构建元数据和工件存储的关键问题。阅读本章后,您将了解这个组件背后的设计原则以及现有的产品如何帮助您快速构建自己的组件。
B.1.5 工作流编排
-
在亚马逊 SageMaker 上,您可以使用 Model Building Pipelines 产品来管理工作流程或管道(这是 SageMaker 的术语)。使用此产品,您可以按照预定义的顺序以任意方式执行一组操作,例如数据准备步骤、训练步骤和模型验证步骤,作为一个单元。为了允许多种类型的用户共同解决同一个问题,SageMaker 还提供了一个 Project 产品,帮助组织工作流程、代码版本、血统信息以及每种用户类型的不同访问权限之间的关系。
-
在第九章中,我们将介绍如何使用工作流管理器启用不同的训练模式。阅读本章后,您将了解到在深度学习系统中设计和实用工作流管理器的原因,以及其在企业环境中的作用。
- B.1.6 实验
- 亚马逊 SageMaker 提供了一个名为 Experiments 的功能,它使用相关的跟踪信息和指标标记实验运行。事实上,这种跟踪信息也是一种元数据,对于需要评估不同数据输入、训练算法和超参数组合性能的深度学习系统用户来说,这种元数据是很重要的。
- B.2 Google Vertex AI
- Google Vertex AI 是 Google AI 平台提供的一项功能与其 AutoML 产品相结合的产品,提供了一系列可用作深度学习系统的工具和服务。在本节中,我们将回顾其提供的功能,并将其与本书介绍的关键组件进行比较。
- B.2.1 数据集管理
- Google Vertex AI 提供了一个简单的 API 来管理数据集,尽管您必须首先将对象数据上传到 Google Cloud Storage,然后通过 Vertex AI API 上传引用 Google Cloud Storage 中对象数据的元数据和注释文件。数据集 API 在提供给开发人员时提供了统一的体验,尽管不同类型的数据集(图像、文本、视频等)之间的 API 类似。然而,该 API 并未提供版本信息和其他血统跟踪信息。在第二章中,我们探讨了核心数据管理原则。阅读本章后,您将能够比较现有解决方案,并针对自己的需求扩展它们或从头开始构建。
- B.2.2 模型训练
Google Vertex AI 支持使用 Docker 容器进行训练。它为那些不需要进一步定制的用户提供预构建的训练容器,并支持为那些需要比预构建版本提供的内容更多的用户提供自定义构建的训练容器。其训练服务公开了一个接口,允许在单个节点或多个节点上启动训练运行以进行分布式训练。在运行分布式训练时,Vertex AI 提供额外的支持,以加速训练过程。在第三章和第四章中,我们探讨了这些功能及其背后的原理。阅读完这些章节后,您将能够确定可以使用哪些现有产品,如何在需要更多功能时扩展它们,以及如何根据具体需求从头开始构建它们。
B.2.3 模型服务
Google Vertex AI 支持向经过训练的模型提供在线推断请求服务,可以使用预构建推断容器或自定义推断容器。训练过的模型与容器分离,并且必须使用计算资源部署,以形成可以提供在线推断请求服务的端点。Vertex AI 支持将一个模型部署到多个端点,并支持将多个模型部署到单个端点。与支持各种模型类型的其他解决方案不同,在 Vertex AI 中,将多个模型部署到单个端点主要用于使用分流流量模式来执行新模型版本的金丝雀发布。在 Vertex AI 中,如果您训练了一个 Vertex AI 视频模型,则无法使其提供在线推断请求服务。
在第六章和第七章中,我们学习了模型服务背后的基本原理。完成这些章节后,您将对模型服务有很好的理解,并能够决定现有解决方案是否满足您的需求。您将能够构建自己的解决方案,并了解如何高效且规模化地运行模型服务器。
B.2.4 元数据和工件存储
Vertex ML Metadata 是谷歌的元数据存储解决方案,可用于深度学习系统中。它使用图来描述诸如数据集、训练运行和训练模型等工件之间的关系。图中的每个节点和边都可以用一系列键值对标记,以描述任何元数据。当正确使用时,这可以为深度学习系统中的所有内容提供全面的血统信息。
工件不直接存储在 Vertex ML Metadata 中。工件存储在 Google Cloud Storage 中。Vertex ML Metadata 使用 URI 引用指向这些工件。
在第八章中,我们将探讨一种类似的方法,即构建元数据和工件存储,通过单一统一的界面可以管理两者。阅读完本章后,您将能够了解如何利用和扩展现有解决方案以满足您的需求。
B.2.5 工作流编排
使用 Google,您可以使用 Vertex Pipelines 来管理和操作您的深度学习工作流程。您可以将数据准备和训练操作表示为管道中的步骤。在 Vertex Pipelines 中,步骤被组织为有向无环图中的节点。管道的每个步骤由一个容器实现。管道的运行实际上是对容器执行的编排。
在第九章中,我们将介绍如何使用工作流管理器来启用不同的训练模式。阅读该章后,您将了解到在深度学习系统中设计工作流管理器的原因和效用以及它在企业环境中的作用。
实验
Google Vertex AI Experiments 提供了统一的用户界面来创建、跟踪和管理实验。Vertex AI SDK 提供了用于模型训练代码的自动记录支持,以记录超参数、指标和数据衍生关系。与 Vertex ML Metadata 结合使用时,您可以获得所有模型训练实验运行的完整概述。
微软 Azure 机器学习
不同于微软经典的 ML Studio 方案,该方案专注于机器学习的图形用户界面方法,Azure 机器学习是一套新的工具和服务,还支持使用代码和已建立的开源框架进行广泛定制。在本节中,我们将比较它们的功能与本书中描述的关键组件。完成本节后,您将对可直接使用的内容、可扩展的内容以及需要从头开始构建以满足您需求的内容有所了解。
数据集管理
在 Azure 机器学习中,数据集是第一类对象,是数据处理和训练任务的输入和输出。数据集被定义为与数据集的原始数据相关联的元数据集合。数据集通过 URI 引用指向底层数据存储的原始数据。一旦数据集被创建,它就变成了不可变的。然而,底层数据并没有同样的保证,您需要自己管理其不可变性。
一旦数据集被定义,数据处理和训练代码可以通过统一的客户端 API 访问它们。数据可以被下载以供本地访问,也可以被挂载为网络存储以供直接访问。阅读第二章后,您将能够识别出这种范例与本书中描述的范例之间的相似之处。您将学习如何直接使用这个现有产品以及如何根据自己的需求进行扩展。
模型训练
Azure Machine Learning 提供预构建的带有 Python 分发的容器,并允许用户定义符合特定要求的自定义基础镜像。在我们撰写本文时,仅支持使用 Python 定义自定义的训练代码。要启动一个训练过程,您需要指定运行时容器和符合特定约定的训练代码的引用。如果您需要其他设置,则需要构建自己的训练服务。第 3 和第四章将向您展示训练服务的关键原则和一个示例,作为自己训练服务的起点。
B.3.3 模型服务
在 Azure Machine Learning v2 上,可以创建端点来提供在线推断请求。端点可以配置为加载某个模型并使用 Python 脚本产生推断结果,或者配置为使用完全自定义的容器镜像(如 TensorFlow Serving)产生推断结果。Azure Machine Learning 还与 NVIDIA Triton Inference Server 集成,当 GPU 用于产生推断结果时,可以提供额外的性能提升。
如果您需要将多个模型部署到一个端点或在边缘设备上管理模型和推断产量,则需要构建自己的服务。在第 6 和第七章中,我们深入讨论了模型服务。完成这些章节后,您将能够构建自己的模型服务器,以便支持现有提供的功能无法支持的其他功能。
B.3.4 元数据和工件存储
在 Azure Machine Learning 中,元数据可以以标签的形式附加到许多对象上,例如模型、训练运行等等。虽然不是一个独立的产品,但模型注册功能支持在注册模型时添加额外的元数据。在注册期间,接口同时接收元数据和模型文件(工件),相比其他需要先将模型注册到其云存储中的解决方案,需要少一步操作。
我们撰写本文时,一个称为注册表的预览功能可用于将与 ML 相关的元数据集中存储到一个位置。但是,如果您想要跟踪不同工件之间的传承关系,可能需要构建自己的解决方案。
阅读第八章后,您将深入了解元数据和工件存储。您将学习其基础知识,并能够快速构建自己的存储。
B.3.5 工作流编排
Azure Machine Learning 提供了一个称为 ML pipelines 的功能,允许您将数据、训练和其他任务定义为步骤。这些步骤可以通过编程方式组合成管道,并可以根据时间表或触发器定期执行,或仅可手动启动。管道定义时,可以编程配置计算资源、执行环境和访问权限。
在第九章中,我们将回顾如何使用工作流管理器启用不同的训练模式。阅读完本章后,你将理解深度学习系统中工作流管理器的设计理念和实用性,以及它在企业环境中的作用。
B.3.6 实验
Azure 机器学习提供了一个用于定义和跟踪实验的功能。在实验的一部分进行模型训练时,可以从训练代码中记录指标,并通过 Web 界面可视化。它还支持任意标记和实验运行之间的父子关系,以进行层次化组织和查找。
B.4 Kubeflow
Kubeflow 是一个开源工具套件,为构建深度学习系统提供了许多有用的组件,而不会被锁定到特定的云供应商。在本节中,我们列出了本书介绍的关键组件,并将它们与 Kubeflow 提供的类似组件进行了比较。
B.4.1 数据集管理
Kubeflow 的愿景是不重复造轮子,因此不会有数据管理组件也不足为奇,因为其他开源解决方案已经存在。在第二章中,我们审查了一些开源数据管理解决方案,并探讨了它们如何进一步扩展以实现该章节描述的关键原则。
B.4.2 模型训练
Kubeflow 是一个基于 Kubernetes 的工具套件,具有复杂的资源调度器支持。不像云供应商提供预构建的模型训练容器,你必须构建自己的容器并管理它们的启动。在第三章和第四章中,我们讨论了训练服务的原则以及它如何帮助抽象出资源分配和调度中的复杂性。我们将介绍一个参考训练服务,你将学会如何根据自己的需求构建一个。
B.4.3 模型服务
截至目前,Kubeflow 提供了一个名为 KServe 的组件,可用于将训练好的模型部署为推断服务,通过网络提供推断请求。它是一个接口,位于现有的服务框架之上,如 TensorFlow Serving、PyTorch TorchServe 和 NVIDIA Triton 推断服务器。使用 KServe 的主要好处在于额外抽象了操作复杂性,如自动缩放、健康检查和自动恢复。由于它是一个开源解决方案,可以在同一个端点上托管一个或多个模型。在第六章和第七章中,我们将介绍模型服务原理,以便你理解设计流行服务接口的原因以及如何自定义它们以适应自己的需求。
B.4.4 元数据和工件存储
从 Kubeflow 版本 1.3 开始,元数据和工件成为 Kubeflow Pipelines 的一个组成部分。Kubeflow Pipelines 由一组管道组件图组成。在每个组件之间,参数和工件可以传递。类似于本书中的描述,工件封装了深度学习系统的任何类型的数据,例如模型本身、训练指标和数据分布指标等。元数据是描述管道组件和工件的任何数据。有了这些构造,你可以推断出输入训练数据集、训练模型、实验结果和服务推理之间的衍生关系。
在第八章中,我们讨论了构建元数据和工件存储的关键问题。阅读完本章后,你将理解该组件背后的设计原则以及现有产品如何帮助你快速构建自己的存储。
B.4.5 工作流编排
此前的章节中也有描述,Kubeflow Pipelines 可以用于帮助管理深度学习数据准备和训练工作流程。元数据和版本控制已集成到管道中,并且可以使用 Kubernetes 的本机用户和访问权限来限制访问。
在第九章中,我们将回顾工作流程管理器如何实现不同的训练模式。阅读完本章后,你将理解深度学习系统中工作流程管理器设计和实用性背后的原理。
B.4.6 实验
Kubeflow Pipelines 提供了实验结构,其中多个训练运行可以组织成一个逻辑组,在其中为每个实验运行提供了额外的可视化工具以显示差异。这非常适用于离线实验。如果需要进行在线实验,则需要自行解决方案。
B.5 旁边对比
我们认为,提供一个按照我们先前涵盖的组件分组的所有解决方案的摘要概述表格将会很方便。我们希望表格 B.1 能够让你更轻松地选择合适的解决方案。
表格 B.1 旁边对比
亚马逊 SageMaker | 谷歌 Vertex AI | 微软 Azure 机器学习 | Kubeflow | |
---|---|---|---|---|
比较数据集管理解决方案 | AWS 组件,如 S3、Glue 数据目录和 Glue ETL,可用于构建数据集管理组件。 | 用于管理数据集的 API 已经准备就绪。数据内容上传和元数据标记是分开的操作。 | 数据集是一级对象,一旦创建就不可变。提供了统一的客户端 API 用于训练作业访问训练数据集。 | 不提供数据集管理解决方案。其他开源替代方案随时可用。 |
比较模型训练解决方案 | 支持内置算法、外部提供的自定义代码和用于训练的自定义容器。提供一个 API 用于按需启动训练作业。 | 提供可以直接使用的预构建训练容器。支持自定义训练容器。提供支持在多个节点上启动训练容器的 API。 | 提供具有 Python 的预构建训练容器,可进行定制。训练容器必须符合某种约定。 | 具有对 Kubernetes 调度能力的本地访问。不提供预构建的训练容器。 |
比较模型服务解决方案 | 模型可以部署为 Web 端点。可以将多个模型部署到同一端点以实现更好的利用,但使用 GPU 时存在一些限制。可配置的模型缓存行为。 | 模型和推断容器是分离的。它们必须一起部署以形成用于服务的 Web 端点。支持自定义推断容器。主要用于金丝雀测试新版本模型的单个端点上使用多个模型。不支持视频模型。 | 可以部署端点以通过 Web 提供模型服务。端点配置为使用特定模型,使用自定义 Python 脚本生成推断。支持 NVIDIA Triton 推理服务器集成。 | KServe 是 Kubeflow 的组件,用于提供模型服务。它在流行的服务框架(如 TensorFlow Serving、PyTorch TorchServe 和 NVIDIA Triton 推理服务器)之上提供了一个无服务器推断抽象。 |
比较元数据和工件存储解决方案 | SageMaker 模型注册表提供了一个中心化的元数据存储解决方案。工件单独存储在亚马逊的对象存储中。 | Vertex ML 元数据提供了一个中心化的元数据存储解决方案。元数据以能描述复杂关系的图形形式存储。工件存储在谷歌的对象存储中。 | 一个称为注册表的预览功能可用于集中管理 ML 元数据。元数据存在于不同对象(训练运行、模型等)的标签中,并且对象可以是工件。可以使用这些对象标签推断出血统信息。 | 没有中心化的元数据或工件存储库。元数据和工件是 Kubeflow Pipelines 的组成部分。管道中的每个阶段都可以用元数据注释,并且可以跟踪生成的工件。可以从 Pipelines API 检索到的信息中推断出血统信息。 |
比较工作流编排解决方案 | 模型构建流水线可用于构建和管理深度学习工作流。 | Vertex ML Metadata 提供了一个集中的元数据存储解决方案。元数据存储为可以描述复杂关系的图形。工件存储在谷歌的对象存储中。 | 一个名为注册表的预览功能可用于集中化 ML 元数据。元数据存在于不同对象的标签中(训练运行、模型等),对象可以是工件。可以使用这些对象标签推断出谱系信息。 | 没有元数据或工件的中央存储库。元数据和工件是 Kubeflow 流水线的组成部分。管道中的每个阶段都可以用元数据注释,并产生可以被跟踪的工件。可以从可以从流水线 API 中检索的详细信息中推断出谱系信息。 |
比较实验解决方案 | 实验功能提供了对训练运行的分组和跟踪。 | 为 Vertex AI 实验提供跟踪和可视化实验设置和运行结果的功能。 | 提供了定义和跟踪实验的功能。实验可以关联为父子关系。Web 接口支持可视化。 | 提供了一个实验构造,用于逻辑分组 Kubeflow 流水线,这些流水线属于同一个实验组。提供了可视化工具,用于突出显示同一实验中每个流水线运行之间的差异。 |
附录 C:创建使用 Kubeflow Katib 的 HPO 服务
我们将向您介绍一个开源的超参数优化(HPO)服务——Kubeflow Katib,它满足了我们在第五章讨论的几乎所有 HPO 要求。我们强烈建议您在构建自己的 HPO 服务之前考虑采用 Katib。除了向您展示如何使用 Katib 外,我们还将介绍其系统设计和代码库,以使您对这个开源服务感到舒适。
作为 Kubeflow 家族的一员,Katib 是一个云原生、可扩展且可投入生产的超参数优化系统。此外,Katib 不关心机器学习框架或编程语言。此外,Katib 是用 Go 编写的,采用 Kubernetes 本地方法,可以在 Kubernetes 集群中独立运行。除了支持具有早期停止支持的超参数优化外,Katib 还支持神经架构搜索(NAS)。
Katib 有许多优点,包括其支持多租户和分布式训练的能力、其云原生性以及其可扩展性,所有这些都使其与其他系统有所区别。无论您是在云中还是在本地服务器上使用 Kubernetes 管理服务器集群,Katib 都是最佳选择。在本章中,我们将以以下五个步骤介绍 Katib:Katib 概述、如何使用 Katib、Katib 系统设计和代码阅读、加速 HPO 执行以及向 Katib 添加自定义 HPO 算法。
C.1 Katib 概览
Katib 以黑盒方式管理 HPO 实验和计算资源,因此 Katib 用户只需要提供训练代码并定义 HPO 执行计划,然后 Katib 就会处理其余事务。图 C.1 显示了 Katib 的系统概述。
图 C.1 Katib 系统概述。Katib 组件作为 Kubernetes 本地服务运行,并且 Katib 支持三种类型的用户界面:UI、API 和 SDK。
在图 C.1 中,我们看到 Katib 为用户的方便性提供了三种类型的用户界面:一个 Web UI,一个 Python SDK,以及一组 API。用户可以通过网页、Jupyter 笔记本、Kubernetes 命令和 HTTP 请求来运行 HPO。
从用户的角度来看,Katib 是一个远程系统。要运行 HPO,用户需要向 Katib 提交一个实验请求,然后 Katib 为他们执行 HPO 实验。要构建实验请求,用户需要做两件事:首先,将训练代码制作成 Docker 镜像,并将要优化的超参数暴露为外部变量;其次,创建一个实验对象,定义 HPO 实验的规范,如 HPO 算法、试验预算或超参数及其值搜索空间。一旦实验对象在 Katib 中创建完成,Katib 将分配计算资源以启动 HPO 执行。
Katib 在 Kubernetes 集群内部运行。Katib 服务本身并不消耗大量内存或磁盘空间;它启动 Kubernetes pod 来运行模型训练作业(HPO 试验)以测试不同的超参数建议。Katib 可以在不同的命名空间为不同的用户运行训练作业,以创建资源隔离。
C.2 使用 Katib 入门
在本节中,我们将看一下如何操作 Katib。首先,我们在本地安装 Katib,然后解释术语,最后,我们向您展示一个 Katib 的端到端使用案例。
为什么在设计书中谈论 Katib 操作和安装呢?
理想情况下,我们不希望在设计书中包含软件的安装和用户指南,因为这些信息在书出版后可能会过时,并且我们可以在官方网站上找到实时的文档。以下是我们违反规则的两个原因。
首先,因为我们建议您使用 Katib 而不是构建您自己的服务,我们有责任向您展示完整的用户体验,既从 Katib 用户(数据科学家)的角度,又从 Katib 运营者(工程师)的角度。其次,为了理解 Katib 的设计并学习如何阅读其代码库,最好先解释其术语和典型用户工作流程。一旦你理解了 Katib 的工作原理,阅读其代码就会更容易。
C.2.1 第一步:安装
如果您安装了 Kubeflow 系统 (mng.bz/WAp4
),那么 Katib 已包含在内。但如果您只对 HPO 感兴趣,您可以单独安装 Katib。Katib 正在积极发展和得到良好维护,所以请查看其官方安装文档 “Getting Started with Katib: Installing Katib” (mng.bz/81YZ
),获取最新的安装提示。
C.2.2 第二步:理解 Katib 术语
对于 Katib 用户来说,实验、建议和试验是需要熟悉的三个最重要的实体/概念。定义如下。
实验
一个实验是单一的优化运行;它是一个端到端的 HPO 过程。一个实验配置包含以下主要组件:用于训练代码的 Docker 镜像,一个我们要优化的客观指标(也称为目标值),需要调整的超参数,以及一个值搜索空间和 HPO 算法。
建议
一个建议是一个由 HPO 算法提出的一组超参数值。Katib 创建一个试验作业来评估建议的值集。
试验
试验是实验的一次迭代。一次试验会接受一个建议,执行一个训练过程(一个试验作业)以产生一个模型,并评估模型的性能。
每个实验都运行一个试验循环。实验会持续调度新的试验,直到达到客观目标或者配置的最大试验数。您可以在 Katib 官方文档 “Introduction to Katib” (mng.bz/ElBo
) 中看到更多的 Katib 概念解释。
C.2.3 第三步:将训练代码打包为 Docker 镜像
与超参数优化库的方法相比(第 5.4 节),最大的区别在于优化服务方法要求我们将模型训练代码打包成一个 Docker 镜像。这是因为优化服务需要在远程集群中运行优化训练实验,而 Docker 镜像是在远程运行模型训练代码的理想方法。
在准备 Docker 镜像时,有两个需要注意的地方:将超参数定义为训练代码的命令行参数,以及将训练指标报告给 Katib。让我们来看一个例子。
首先,我们在训练代码中将需要优化的超参数定义为命令行参数。因为 Katib 需要为不同的超参数值执行训练代码作为一个 docker 容器,所以训练代码需要从命令行参数中获取超参数值。在下面的代码示例中,我们定义了两个要调整的超参数:lr(学习率)和批量大小。在优化过程中,Katib 将在训练容器启动时传入这些值;请参见以下代码:
def main():
parser = argparse.ArgumentParser( \
description="PyTorch MNIST Example")
parser.add_argument("--batch-size", \ ❶
type=int, default=64, metavar="N", \
help="input batch size for training (default: 64)")
parser.add_argument("--lr", \
type=float, default=0.01, metavar="LR", \ ❶
help="learning rate (default: 0.01)")
❶ 从命令行参数解析超参数值
其次,我们让训练代码报告训练指标,尤其是目标指标,给 Katib,这样它就可以跟踪每个试验执行的进度和结果。Katib 可以从以下三个位置收集指标:stdout(操作系统的标准输出位置),任意文件和 TensorFlow 事件。如果你有特殊的指标收集或存储要求,也可以编写自己的指标收集容器。
最简单的选项是将评估(目标)指标打印到训练代码的标准输出(stdout)中,并使用 Katib 的标准指标收集器收集它们。例如,如果我们将目标指标定义为Validation-accuracy
,并希望优化过程找到最小化此值的最优超参数,我们可以将以下日志写入到 stdout 中。Katib 的标准指标收集器将在 stdout 中检测到Validation-accuracy=0.924463
,并将解析该值。如下所示是标准输出样本:
2022-01-23T05:19:53Z INFO Epoch[5] Train-accuracy=0.932769
2022-01-23T05:19:53Z INFO Epoch[5] Time cost=31.698
2022-01-23T05:19:54Z INFO Epoch[5] Validation-accuracy=0.924463
2022-01-23T05:19:58Z INFO Epoch[6] Batch [0-100] Speed: 1749.16 ..
Katib 默认使用的正则表达式格式来解析日志中的目标指标是([\w|-]+)\s*=\s*([+-]?\d*(\.\d+)?([Ee][+-]?\d+)?)
。您可以在实验配置文件的.source.filter.metricsFormat
中定义自己的正则表达式格式。更多详细信息,请参阅 Katib 文档“运行实验”的指标收集器部分(mng.bz/NmvN
)。
为了帮助你入门,Katib 提供了一系列示例训练代码和示例 Docker 镜像文件,以展示如何打包你的训练代码。这些示例是为不同的训练框架编写的,如 TensorFlow、PyTorch、MXNet 等。你可以在 Katib 的 GitHub 仓库中找到这些示例(mng.bz/DZln
)。
C.2.4 第四步:配置一个实验
现在你已经准备好了训练代码,我们可以开始在 Katib 中准备一个 HPO 实验。我们只需要在 Katib 中创建一个实验 CRD(customer resource definition)对象。
通过使用 Kubernetes API 或kubectl
命令,我们可以通过指定一个 YAML 配置来创建实验 CRD。以下是一个示例配置。为了便于阅读,我们将示例配置分成了三个部分。我们逐个部分讨论一下。
第一部分:目标
第一部分是定义 HPO 实验的目标,并确定如何衡量每个试验(训练执行)的性能。Katib 使用objectiveMetric
和additionalMetric
的值作为目标值,以监控建议的超参数与模型的配合情况。如果一个试验的目标值达到了目标,Katib 将将建议的超参数标记为最佳值,并停止进一步的试验。
对于以下配置,目标指标被设置为Validation-accuracy
,目标值为0.99
:
apiVersion: kubeflow.org/v1beta1
kind: Experiment
metadata:
namespace: kubeflow
name: bayesian-optimization
spec:
Objective:
type: maximize
goal: 0.99
objectiveMetricName: Validation-accuracy ❶
additionalMetricNames:
- Train-accuracy
❶ 定义了目标指标
第二部分:HPO 算法和超参数
设置完 HPO 的目标后,我们可以配置 HPO 算法并声明它们的搜索空间以及需要调整的超参数。我们分别来看这些配置。
algorithm config指定了我们希望 Katib 在实验中使用的 HPO 算法。在当前的示例中,我们选择了贝叶斯优化算法(mng.bz/lJw6
)。Katib 支持许多最新的 HPO 算法,你可以在 Katib 官方文档的“Running an Experiment”一节的 Search Algorithm in Detail 中看到它们(mng.bz/BlV0
)。你还可以将自己的 HPO 算法添加到 Katib 中,我们将在 C.5 节中讨论。
ParallelTrialCount
、maxTrialCount
和maxFailedTrialCount
:根据它们的名称,可以自解释地定义试验如何安排实验。在这个示例中,我们并行运行了三个试验,共进行了 12 个试验。如果有三个试验失败,实验将停止。
parameters config定义了要调整的超参数及其值的搜索空间。Katib 根据你指定的超参数调整算法在搜索空间中选择超参数的值。请参考以下代码:
algorithm:
algorithmName: bayesianoptimization ❶
algorithmSettings:
- name: "random_state"
value: "10"
parallelTrialCount: 3
maxTrialCount: 12
maxFailedTrialCount: 3
Parameters: ❷
- name: lr
parameterType: double
feasibleSpace:
min: "0.01"
max: "0.03"
- name: num-layers
parameterType: int
feasibleSpace:
min: "2"
max: "5"
- name: optimizer
parameterType: categorical
feasibleSpace:
list:
- sgd
- adam
- ftrl
❶ 使用了 Katib 提供的贝叶斯优化算法
❷ 定义了要优化的超参数及其值的搜索空间
最后一节:试验配置
在这个trial template config中,我们定义了要执行的训练代码(Docker 镜像)和要传递给训练代码的超参数。Katib 为几乎每个模型训练框架(如 TensorFlow、PyTorch、MXNet 等)都内置了作业,它负责在 Kubernetes 中执行实际的训练。
例如,如果我们想要在 PyTorch 训练代码的 HPO 试验中运行分布式训练,需要设置一个分布式组,我们可以将试验定义为 PyTorch 作业类型。Katib 将为您运行分布式训练。
在以下示例中,我们将试验定义为默认的作业类型 Kubernetes Job
。在实验过程中,Katib 将以 Kubernetes Pod 的形式运行试验作业,无需对训练代码进行任何特殊的自定义配置;请参阅以下代码:
trialTemplate:
primaryContainerName: training-container
trialParameters: ❶
- name: learningRate
description: Learning rate for the training model
reference: lr
- name: numberLayers
description: Number of training model layers
reference: num-layers
- name: optimizer
description: Training model optimizer (sdg, adam or ftrl)
reference: optimizer
trialSpec: ❷
apiVersion: batch/v1
kind: Job
spec:
template:
spec:
containers:
- name: training-container
image: docker.io/kubeflowkatib/mxnet-mnist:latest
command: ❸
- "python3"
- "/opt/mxnet-mnist/mnist.py"
- "--batch-size=64"
- "--lr=${trialParameters.learningRate}"
- "--num-layers=${trialParameters.numberLayers}"
- "--optimizer=${trialParameters.optimizer}"
restartPolicy: Never
❶ 为训练代码声明超参数
❷ 配置训练容器
❸ 配置如何执行训练代码
Katib 为其支持的每个 HPO 算法提供了示例实验配置文件;您可以在 Katib GitHub 仓库中找到它们:katib/examples/v1beta1/hp-tuning/
(mng.bz/dJVN
)
C.2.5 步骤 5:开始实验
一旦我们定义了实验配置并将其保存在 YAML 文件中,我们可以运行以下命令来启动实验:
% kubectl apply -f bayesian-optimization.yaml
experiment.kubeflow.org/bayesian-optimization created
% kubectl get experiment -n kubeflow
NAME TYPE STATUS AGE
bayesian-optimization Created True 46s
从 kubectl get experiment -n kubeflow
的返回消息中,我们看到实验 bayesian-optimization
被创建为 Experiment CRD 资源。从现在开始,Katib 将完全拥有 HPO 实验,直到获得结果。
请注意,Katib 完全依赖于 Kubernetes CRD 对象来管理 HPO 实验和试验。它还使用 CRD 对象来存储其 HPO 活动的指标和状态,因此我们说 Katib 是一个 Kubernetes 原生应用程序。
除了上述 kubectl
命令之外,我们还可以使用 Katib SDK、其 Web UI 或发送 HTTP 请求来启动实验。
C.2.6 步骤 6:查询进度和结果
您可以使用以下命令来检查实验的运行状态:
% kubectl describe experiment bayesian-optimization -n kubeflow
kubectl describe
命令将返回有关实验的所有信息,例如其配置、元数据和状态。从进度跟踪的角度来看,我们主要关注状态部分。请参阅以下示例:
Status:
Completion Time: 2022-01-23T05:27:19Z
Conditions: ❶
.. .. ..
Message: Experiment is created
Type: Created
.. .. ..
Message: Experiment is running
Reason: ExperimentRunning
Status: False
Type: Running
.. .. ..
Message: Experiment has succeeded because max trial count has reached
Reason: ExperimentMaxTrialsReached
Status: True
Type: Succeeded
Current Optimal Trial: ❷
Best Trial Name: bayesian-optimization-9h25bvq9
Observation:
Metrics: ❸
Latest: 0.979001
Max: 0.979001
Min: 0.955713
Name: Validation-accuracy
Latest: 0.992621
Max: 0.992621
Min: 0.906333
Name: Train-accuracy
Parameter Assignments: ❹
Name: lr
Value: 0.014183662191100063
Name: num-layers
Value: 3
Name: optimizer
Value: sgd
Start Time: 2022-01-23T05:13:00Z
Succeeded Trial List: ❺
.. .. ..
bayesian-optimization-5s59vfwc
bayesian-optimization-r8lnnv48
bayesian-optimization-9h25bvq9
.. .. ..
Trials: 12
Trials Succeeded: 12
❶ 实验历史
❷ 当前最佳试验的元数据
❸ 当前最佳试验的目标度量
❹ 当前最佳试验中使用的超参数值
❺ 已完成试验列表
以下是对前面示例响应的几点解释:
-
状态/条件—显示当前和以前的状态。在前面的示例中,我们看到实验经历了三个状态:创建、运行和成功。从消息中,我们知道实验已完成,因为它用完了训练预算——最大试验计数。
-
当前最佳试验—显示当前的“最佳”试验以及试验使用的超参数值。它还显示目标度量的统计信息。随着实验的进行,这些值将不断更新,直到实验中的所有试验都完成,然后我们将
status.currentOptimalTrial.parameterAssignment
(超参数值分配)作为最终结果。 -
成功的试验列表/失败的试验列表/试验—通过列出实验执行的所有试验来显示实验的进展情况。
C.2.7 步骤 7:故障排除
如果有失败的试验,我们可以运行以下命令来检查失败的试验作业的错误消息。参见以下失败的 HPO 示例:
-> % k describe trial bayesian-optimization-mnist-pytorch-88c9rdjx \
-n kubeflow
Name: bayesian-optimization-mnist-pytorch-88c9rdjx
.. .. ..
Kind: Trial
Spec:
.. .. ..
Parameter Assignments:
Name: lr
Value: 0.010547476197421666
Name: num-layers
Value: 3
Name: optimizer
Value: ftrl
Status:
Completion Time: 2022-01-23T06:23:50Z
Conditions:
.. .. ..
Message: Trial has failed. Job message: Job has reached the specified backoff limit
Reason: TrialFailed. Job reason: BackoffLimitExceeded ❶
.. .. ..
❶ 失败消息
从返回的数据中,我们可以看到试验中使用的超参数值以及相关的错误消息。
除了 describe
命令的错误消息外,我们还可以通过检查训练容器的日志来找到根本原因。如果选择使用 Katib 标准度量收集器,Katib 将在同一 pod 中的训练代码容器中运行 metrics-logger-and-collector
容器。该度量收集器捕获来自训练容器的所有 stdout 日志;您可以使用以下命令检查这些日志:kubectl logs ${trial_pod} -c metrics-logger-and-collector -n kubeflow
。参见以下示例命令:
% kubectl logs bayesian-optimization-mkqgq6nm--1-qnbtt -c \
metrics-logger-and-collector -n kubeflow
logs
命令输出大量有价值的信息,例如训练过程的初始参数,数据集下载结果和模型训练指标。在以下示例日志输出中,我们可以看到 Validation-accuracy
和 Train-accuracy
。Katib 度量收集器将解析这些值,因为它们在实验配置中被定义为目标度量:
Trial Name: bayesian-optimization-mkqgq6nm ❶
2022-01-23T05:17:17Z INFO start with arguments Namespace( ❷
add_stn=False, batch_size=64, disp_batches=100,
dtype='float32', gc_threshold=0.5, gc_type='none', gpus=None,
image_shape='1, 28, 28', ... warmup_epochs=5,
warmup_strategy='linear', wd=0.0001)
I0123 05:17:20.159784 16 main.go:136] 2022-01-23T05:17:20Z INFO
downloaded http:/ /data.mxnet.io/data/mnist/t10k-labels-idx1-ubyte.gz
➥ into t10k-labels-idx1-ubyte.gz successfully ❸
.. .. ..
I0123 05:17:26.711552 16 main.go:136] 2022-01-23T05:17:26Z INFO Epoch[0] Train-accuracy=0.904084 ❹
.. .. ..
I0123 05:17:26.995733 16 main.go:136] 2022-01-23T05:17:26Z INFO Epoch[0] Validation-accuracy=0.920482 ❺
I0123 05:17:27.576586 16 main.go:136] 2022-01-23T05:17:27Z INFO Epoch[1] Batch [0-100] Speed: 20932.09 samples/sec accuracy=0.926825
I0123 05:17:27.871579 16 main.go:136] 2022-01-23T05:17:27Z INFO Epoch[1] Batch [100-200] Speed: 21657.24 samples/sec accuracy=0.930937
❶ 试验名称
❷ 训练试验的初始参数
❸ 数据集下载
❹ 附加度量值
❺ 目标度量值
C.3 加速 HPO
HPO 是一项耗时且昂贵的操作。Katib 提供了三种方法来加速该过程:并行试验,分布式训练和提前停止。
C.3.1 并行试验
通过在实验配置中指定 parallelTrialCount
,您可以并行运行试验。我们应该注意的一件事是,一些 HPO 算法不支持并行试验执行。因为这种类型的算法对试验执行顺序有线性要求,下一个试验需要等到当前试验完成。
C.3.2 分布式试验(训练)作业
为了更快完成试验作业,Katib 允许我们为运行训练代码启用分布式训练。正如我们在 C.2(步骤 4)中所解释的那样,Katib 为不同的训练框架(如 PyTorch、TensorFlow 和 MXNet)在 trialTemplate
中定义了不同的作业类型。
以下是如何为 Katib 实验中的 PyTorch 训练代码启用分布式训练(一个主节点,两个工作节点)的示例:
trialTemplate:
primaryContainerName: pytorch
trialParameters: ❶
- name: learningRate
description: Learning rate for the training model
reference: lr
- name: momentum
description: Momentum for the training model
reference: momentum
trialSpec:
apiVersion: kubeflow.org/v1
kind: PyTorchJob ❷
spec:
pytorchReplicaSpecs:
Master: ❸
replicas: 1 ❸
restartPolicy: OnFailure
template:
spec:
containers:
- name: pytorch
image: docker.io/kubeflowkatib/pytorch-mnist:latest
command:
- "python3"
- "/opt/pytorch-mnist/mnist.py"
- "--epochs=1"
- "--lr=${trialParameters.learningRate}"
- "--momentum=${trialParameters.momentum}"
Worker:
replicas: 2 ❹
restartPolicy: OnFailure
template:
spec:
containers:
- name: pytorch
image: docker.io/kubeflowkatib/pytorch-mnist:latest
command:
- "python3"
- "/opt/pytorch-mnist/mnist.py"
- "--epochs=1"
- "--lr=${trialParameters.learningRate}"
- "--momentum=${trialParameters.momentum}"
❶ 将学习率和动量声明为超参数
❷ 将试验作业类型设置为 PyTorch
❸ 配置主训练器
❹ 配置工作训练器
在前面的示例中,与第 C.2 节中的非分布式实验配置相比(步骤 4),唯一的区别是 trialSpec
部分。作业类型现在变为 PyTorchJob
,并且具有主训练器和工作训练器的副本数等单独的设置。您可以在以下两个 GitHub 仓库中找到 Katib 训练操作符及其配置示例的详细信息:Kubeflow 训练操作符(github.com/kubeflow/training-operator
)和 Katib 操作符配置示例(mng.bz/rdgB
)。
C.3.3 早期停止
Katib 提供的另一个有用技巧是早期停止。当试验的目标指标不再改善时,早期停止结束试验。它通过终止不太有希望的试验来节省计算资源并减少执行时间。
在 Katib 中使用早期停止的优点是我们只需要更新实验配置文件,而不需要修改训练代码。只需在 .spec.algorithm
部分中定义 .earlyStopping.algorithmName
和 .earlyStopping.algorithmSettings
,您就可以开始使用了。
Katib 当前支持的早期停止算法是中位数停止规则,如果试验的最佳目标值比所有其他已完成试验的目标报告到相同步骤的运行平均值的中位数值更差,则停止试验。请在 Katib 官方文档“使用早期停止”中阅读更多详细信息。
C.4 Katib 系统设计
最后,我们可以谈谈我们最喜欢的话题——系统设计。通过阅读第 C.2 节和第 C.3 节,您应该对 Katib 如何从用户角度解决 HPO 问题有一个清晰的了解。这为理解 Katib 的系统设计打下了良好的基础。
正如我们所见,Katib 不仅解决了 HPO 问题,而且还以生产质量来解决它。通常,这样一个功能强大的系统具有庞大而复杂的代码库,但是 Katib 是一个例外。因为核心 Katib 组件都是按照一个简单的设计模式——Kubernetes 控制器/操作器模式来实现的,如果您理解一个组件,您几乎就理解了整个系统。通过在本节中阅读我们的介绍,阅读 Katib 源代码对您来说将会很简单。
C.4.1 Kubernetes 控制器/操作器模式
我们已经在第 3.4.2 节中讨论了控制器设计模式。但是,为了帮助您记住,我们在此将图 3.10 重新发布为图 C.2。如果图 C.2 看起来不太熟悉,请回顾第 3.4.2 节。
图 C.2 Kubernetes 控制器/操作器模式运行一个无限控制循环,监视特定 Kubernetes 资源的实际状态(右侧)和期望状态(左侧),并尝试将其实际状态移动到期望状态。
C.4.2 Katib 系统设计和工作流程
图 C.2 说明了 Katib 的内部组件及其交互方式。系统有三个核心组件:实验控制器(标记为 A)、建议控制器(标记为 B)和试验控制器(标记为 C)。
实验控制器管理整个 HPO 实验的生命周期,例如为实验安排 HPO 试验并更新其状态。建议控制器运行 HPO 算法,为给定的超参数提供建议值。试验控制器运行给定超参数集的实际模型训练。
从这些核心组件的名称可以知道,它们的实现都遵循 Kubernetes 控制器模式。除了控制器外,Katib 还定义了一组 CRD 对象(spec)来与这三个控制器一起工作。例如,实验规范 是一种 CRD 类型,用于定义 HPO 实验的期望状态,并作为输入请求传递给实验控制器。
如图 C.3 所示,数据科学家 Alex 在与 Katib 交互时可能会遵循典型的工作流程。主要步骤列在以下各节中。
图 C.3 显示了 Katib 系统设计图和用户工作流程
第 1 步:创建实验请求
在第 1 步中,Alex 使用客户端工具(如 Katib SDK、Katib web UI 或 kubectl
命令)创建了一个实验 CRD 对象。这个实验对象包含了所有 HPO 实验的定义,如训练算法、超参数及其搜索空间、HPO 算法和试验预算。
实验控制器(组件 A)定期扫描所有实验 CRD 对象。对于每个实验 CRD 对象,它创建声明的建议 CRD 对象和试验 CRD 对象。简而言之,实验控制器生成实际资源,以实现实验 CRD 中定义的所需状态。此外,它会在实验 CRD 对象中更新实验的运行状态,因此 Alex 可以实时查看试验超参数和实验的执行状态。
一旦在第 1 步中创建了 Alex 的实验对象,Katib 就会为 Alex 的实验部署一个 HPO 算法建议服务(组件 D),以便运行所需的 HPO 算法。在这个建议服务中,实验 CRD 对象中定义的 HPO 搜索算法(库)被加载并通过 gRPC 接口公开,允许建议控制器与其通信并请求建议的超参数。
第 2 步:获取下一个试验的超参数
当实验控制器在第 2 步中发现了 Alex 的实验 CRD 对象时,它会创建一个建议 CRD 对象作为建议控制器(组件 B)的输入请求。在此建议 CRD 对象中指定了超参数及其值,以及搜索算法和建议的数量。
随后,建议控制器调用在第 1 步创建的建议算法服务来计算建议的超参数值。此外,建议控制器在建议 CRD 对象中维护建议的超参数值的历史记录。
第 3 步:创建试验请求
作为第 3 步的一部分,在建议控制器提供一组试验超参数值后,实验控制器(组件 A)创建一个试验 CRD 对象来启动模型训练试验。试验使用建议服务(组件 D)计算出的超参数值集来训练模型。
第 4 步:启动训练作业
在第 4 步中,试验控制器(组件 C)读取新创建的试验 CRD 对象(在第 3 步创建),并创建一个 TrialJob CRD 对象。有几种类型的 TrialJob CRD 对象,包括 Kubernetes 作业、PyTorch 作业、TF 作业和 MXNet 作业。对于每种作业类型,Kubeflow(www.kubeflow.org/docs/components/training/
)提供了一个专用的训练运算符来执行它,如 PyTorch 训练运算符或 TensorFlow 训练运算符(组件 E)。
在检测到其类型中新创建的 TrialJob CRD 对象后,训练运算符(组件 E)会根据试验作业中定义的超参数创建 Kubernetes Pod 来执行训练图像。Alex 的 HPO 实验的训练试验将由 PyTorch 训练运算符运行,因为他的训练代码是用 PyTorch 编写的。
第 5 步:返回试验结果
当模型试训练开始时,度量收集器边车(一个位于 Kubernetes 训练 Pod 中的 Docker 容器)在第 5 步收集训练指标,并将其报告给 Katib 指标存储(一个 MySQL 数据库)。使用这些指标,试验控制器(组件 C)将试验执行状态更新到试验 CRD 对象上。当实验控制器注意到试验 CRD 对象的最新更改时,它读取更改并使用最新的试验执行信息更新实验 CRD 对象,以便实验对象具有最新状态。最新状态以这种方式聚合到实验对象中。
HPO 工作流本质上是一个试验循环。为了在 Katib 上处理 Alex 的 HPO 请求,此工作流中的第 2、3、4 和 5 步会重复进行,直到满足退出标准。Alex 可以在 HPO 执行过程中始终检查实验 CRD 对象,以获取 HPO 的及时执行状态,其中包括已完成或失败的试验数量、模型训练指标和当前最佳超参数值。
注意:使用 CRD 对象存储 HPO 执行数据有两个主要优点:简单性和可靠性。首先,可以轻松访问实验的最新状态信息。例如,你可以使用 Kubernetes 命令,如 kubectl
describe
experiment|trial|suggestion
,在几秒钟内获取实验、试验和建议的中间数据和最新状态。其次,CRD 对象有助于提高 HPO 实验的可靠性。当 Katib 服务关闭或训练操作员失败时,我们可以从失败的地方恢复 HPO 执行,因为这些 CRD 对象保留了 HPO 执行历史记录。
C.4.3 Kubeflow 训练操作员集成分布式训练
Katib 的默认训练操作员——Kubernetes 作业操作员——只支持单 pod 模型训练;它为实验中的每个试验启动一个 Kubernetes pod。为了支持分布式训练,Katib 与 Kubeflow 训练操作员合作(www.kubeflow.org/docs/components/training/
)。你可以在图 C.4 中看到这是如何运作的。
图 C.4 Katib 创建不同的试验作业以触发训练操作员为不同的训练框架运行分布式训练。
HPO 实验由试验组成。Katib 为每个试验创建一个试验 CRD 对象和一个 TrialJob CRD 对象。试验 CRD 包含 HPO 试验的元数据,例如建议的超参数值、工作人员数量和退出条件。在 TrialJob CRD 中,试验元数据被重新格式化,以便 Kubeflow 训练操作员能够理解它。
PyTorchJob
和 TFJob
是两种最常用的 TrialJobs CRD 类型。它们可以被 TensorFlow 训练操作员和 PyTorch 训练操作员处理,每个操作员都支持分布式训练。当 Alex 在实验 CRD 对象中将工作人员数量设置为三时,Katib 会创建一个 PyTorchJob 试验 CRD 对象,PyTorch 训练器可以在这个实验上进行分布式训练。
这个例子还说明了 Kubernetes 控制器模式是多么的灵活和可扩展。如果它们都被实现为控制器,两个应用程序 Katib 和 KubeFlow 训练操作员可以轻松集成。
注意:我们在第 3.4.3 节讨论了 Kubeflow 训练操作员设计,请如果你想了解更多内容,请重新阅读。
C.4.4 代码阅读
虽然 Katib 有一个庞大的代码库(github.com/kubeflow/katib
),但阅读和调试其代码并不太困难。
从哪里开始阅读代码
所有 Katib 核心组件都采用了控制器模式:experiment_controller
、trial_controller
和suggestion_controller
。控制器的工作是确保对于任何给定的对象,Kubernetes 世界的实际状态与对象中的期望状态相匹配。我们称这个过程为reconciling。例如,在experiment_controller
中的 reconcile 函数会读取实验对象的集群状态,并根据所读取的状态进行更改(建议、试验)。通过遵循这个思路,我们可以从每个控制器类的 reconcile 函数开始理解其核心逻辑。
你可以在pkg/controller.v1beta1/experiment/experiment_controller.go
找到实验控制器,建议控制器在pkg/controller.v1beta1/suggestion/suggestion_controller.go
,试验控制器在pkg/controller.v1beta1/trial/trial_controller.go
。记得从这些文件的 reconcile 函数开始。
调试
Katib 核心应用(katib-controller)作为控制台应用程序运行。这个控制台应用程序中没有 UI 或者 Web 代码,只有纯逻辑代码,因此它的本地调试设置很简单。要调试 Katib,首先设置好你的本地 Kubernetes 集群,然后通过断点在本地运行 katib-controller,接着你可以通过创建一个测试实验请求,例如kubectl apply -f {test_experiment.yaml}
来启动 HPO 过程。reconcile 函数中的断点会被触发,你可以从那里开始调试和探索代码。
要设置一个本地开发环境,请按照 Katib 的开发者指南操作(mng.bz/VpzP
)。katib-controller 的入口点在 cmd/katib-controller/v1beta1/main.go。
注意 Katib 是一个生产质量的 HPO 工具。它以高可靠性和稳定性运行。但是要在日常操作中使用它,我们需要阅读其源代码以了解其行为,这样我们就知道在 HPO 执行偏离脚本时该如何引导它。通过遵循图 C.2 中的工作流程并阅读每个控制器的 reconcile 函数,你将在几个小时内对 Katib 有很好的理解。
C.5 添加新算法
从图 C.2 中我们知道 Katib 将不同的 HPO 算法作为独立的建议/算法服务运行。一旦创建了一个实验,Katib 就会为所选的 HPO 算法创建一个建议服务。这个机制使得向 Katib 添加新算法并让新添加的算法与现有算法一致变得很容易。要向 Katib 添加一个新算法,我们需要执行以下三个步骤。
C.5.1 第 1 步:用新算法实现 Katib 建议 API
首先,我们需要实现 Katib 的Suggestion
接口。这个接口在 gRPC 中定义,所以你可以用你喜欢的任何语言来实现它。这个接口的详细定义可以在mng.bz/xdzW
找到;请看以下代码:
service Suggestion {
rpc GetSuggestions(GetSuggestionsRequest)
returns (GetSuggestionsReply);
rpc ValidateAlgorithmSettings(ValidateAlgorithmSettingsRequest)
returns (ValidateAlgorithmSettingsReply);
}
以下代码片段是实现 Suggestion
接口的一个示例。超参数及其值搜索空间在 request
变量中定义。过去的试验及其指标也可以在 request
变量中找到,因此您可以运行您的算法来计算下一个建议,方法是在 GetSuggestions
方法中使用这些输入数据;请参阅以下代码:
class NewAlgorithmService( ❶
api_pb2_grpc.SuggestionServicer, ❶
HealthServicer): ❶
def ValidateAlgorithmSettings(self, request, context):
# Optional, it is used to validate
# algorithm settings defined by users.
Pass
def GetSuggestions(self, request, context): ❷
search_space = HyperParameterSearchSpace.convert(request.experiment)
trials = Trial.convert(request.trials) ❸
# Implement the logic to use your algorithm
# to generate new assignments
# for the given current request number.
list_of_assignments = your_logic(search_space, ❹
trials, request.current_request_number) ❹
return api_pb2.GetSuggestionsReply(
trials=Assignment.generate(list_of_assignments))
❶ 定义一个新的算法服务,并实现 GetSuggestions 接口
❷ Suggestion 函数为每个试验提供超参数。
❸ 获取过去的试验
❹ 实现了实际的 HPO 算法,提供候选值。
C.5.2 步骤 2:将算法代码作为 GRPC 服务 Docker 化
一旦我们实现了 Suggestion
接口,我们需要构建一个 gRPC 服务器来将此 API 暴露给 Katib,并将其 Docker 化,以便 Katib 可以通过发送 gRPC 调用来启动算法服务并获取超参数建议。代码如下所示:
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
service = YourNewAlgorithmService()
api_pb2_grpc.add_SuggestionServicer_to_server(service, server)
health_pb2_grpc.add_HealthServicer_to_server(service, server)
server.add_insecure_port(DEFAULT_PORT)
print("Listening...")
server.start()
C.5.3 步骤 3:将算法注册到 Katib
最后一步是将新算法注册到 Katib 的起始配置中。在 Katib 服务配置的 suggestion
部分添加一个新条目;以下是一个示例:
suggestion: |-
{
"tpe": {
"image": "docker.io/kubeflowkatib/suggestion-hyperopt"
},
"random": {
"image": "docker.io/kubeflowkatib/suggestion-hyperopt"
},
+ "<new-algorithm-name>": {
+ "image": "new algorithm image path"
+ }
C.5.4 示例和文档
大部分前文来自 Katib GitHub 仓库(github.com/kubeflow/katib
)的 readme 文件,“关于如何在 Katib 中添加新算法的文档”(mng.bz/Alrz
)— 这是一份非常详细和写得很好的文档,我们强烈建议您阅读。
因为 Katib 的所有预定义 HPO 算法都遵循相同的 HPO 算法注册模式,您可以将它们用作示例。此示例代码可在 katib/cmd/suggestion(mng.bz/ZojP
)找到。
C.6 进一步阅读
恭喜您到达这里!这是需要消化的大量内容,但您已经走了这么远。虽然我们已经涵盖了 Katib 的很大一部分,但由于页面限制,我们仍然没有讨论到一些重要的部分。如果您想进一步进行,我们列出了一些有用的阅读材料供您探索。
-
要了解 Katib 设计背后的思维过程,请阅读“一个可扩展的云原生超参数调整系统”(
arxiv.org/pdf/2006.02085.pdf
)。 -
要检查功能更新、教程和代码示例,请访问 Katib 官方网站(
www.kubeflow.org/docs/components/katib/
)和 Katib GitHub 仓库(github.com/kubeflow/katib
)。 -
要直接从 Jupyter 笔记本中使用 Python SDK 运行 HPO,请阅读 SDK API 文档(
mng.bz/RlpK
)和 Jupyter 笔记本示例(mng.bz/2aY0
)。
C.7 使用它的时机
从这次讨论中我们可以看到,Katib 满足了 HPO 服务的所有设计原则。它对训练框架和训练代码是不可知的;它可以扩展以纳入不同的 HPO 算法和不同的指标收集器;并且由于 Kubernetes 的存在,它是可移植和可扩展的。如果你正在寻找一个生产级别的 HPO 服务,Katib 是最佳选择。
对于 Katib 唯一的注意事项是它的前期成本很高。你需要建立一个 Kubernetes 集群,安装 Katib,并将训练代码 Docker 化才能开始。你需要了解 Kubernetes 命令来排查故障。这需要专门的工程师来操作和维护系统,因为这些都是非常重要的任务。
对于生产场景,这些挑战并不是主要问题,因为通常模型训练系统的设置与 Kubernetes 中的 Katib 相同。只要工程师有操作模型训练系统的经验,就可以轻松管理 Katib。但对于小团队或原型项目来说,如果你更喜欢简单的东西,像 Ray Tune 这样的 HPO 库方法更合适。