这些年来,Uber 的增长速度是指数级的,如今我们公司每天支持 1400 万次出行,我们的工程师也证明了他们可以实现大规模扩展。这种价值还延伸到了其他领域,其中就包括 Uber ATG(先进技术小组)和他们开发自动驾驶汽车的努力。
这项工作中的一个关键环节,就是创建机器学习(ML)模型来处理诸如传感器输入、对象识别以及预测这些对象可能去向这样的任务。解决这一问题所需的诸多模型,以及致力于解决这些问题的庞大工程师团队本身,催生了管理和版本控制问题。
为了解决这个问题,我们一开始定义了一个五步骤生命周期,以在我们的自动驾驶汽车中训练和部署 ML 模型。这个生命周期从数据摄取开始,一直发展到模型服务,接下来还要确保模型运行良好。这个流程使我们能够有效加快自动驾驶汽车组件的迭代,从而不断改善其表现以实现最高标准。
我们还能进一步自动化这一流程以更好地管理开发涉及的诸多模型。由于自动驾驶领域中 ML 模型的深度依赖性和开发的复杂性,我们开发了 VerCD,这是一套支持 ML 工作流的工具和微服务集合。VerCD 使我们可以使用自动化持续交付(CD)来追踪和管理 ML 工件的依赖项版本。
开发大规模模型的 ML 团队可能会发现,这里提到的 Uber ATG 为自动驾驶技术开发的五步模型生命周期,与 VerCD 的实践和工具可应用于多种用例,从而帮助他们迭代自己的基础设施。
我们的自动驾驶汽车组件有很多都使用 ML 模型,从而使车辆能够安全、准确地行驶。一个组件会包含一个或多个 ML 模型,所有组件在一起构成我们的自动驾驶汽车软件:
每个组件都建立在前一个组件生成的输出的基础上,以将我们的自动驾驶汽车正确地引导到目的地。包含这些组件的 ML 模型都会经过我们的五步迭代流程处理,以确保它们能有最佳的操作。
机器学习模型基于历史数据的训练来预测、展望和估计。对于 Uber ATG 的自动驾驶车辆开发需求,我们使用各种交通状况下从配备传感器(包括 LiDAR、摄像头和雷达)的车辆收集的数据,用作我们的训练数据。我们将这些数据从车辆转移到我们的服务器,然后由标签团队创建标签,这些标签构成了我们希望 ML 模型学习的真值(ground truth)输出。
通常来说,标签团队将标签手动分配给指定场景中的角色。这些标签为场景中的每个角色提供位置、形状和对象类型等属性。我们使用这些标签来训练 ML 应用程序,这样它们以后就能为从传感器捕获的新数据,预测其标签会包含的信息(对象的类型及坐标)。
收集到足够的数据后,我们将使用真值标签处理这些信息来开发 ML 模型,其中包括所有对象类型、坐标及高清地图。我们开发和运行 ML 模型的 ML 技术栈由多层组成,从最高层的 ML 应用程序本身到最底层的硬件部分。中间层包括常见的 ML 库,例如 TensorFlow 和 PyTorch,以及 GPU 加速等。
我们的 ML 模型生命周期(如下图 1 所示)包括五个阶段。
我们让 ML 模型走过这一生命周期的每个阶段,以确保在将其部署到自动驾驶车辆之前,它们能够展现出高质量的模型、系统和硬件指标。
图 1:选择并拆分了用于训练模型的数据(数据摄取)之后,我们会确保信息质量(数据验证),使用经过验证的数据训练模型(模型训练),还会测试我们的模型以保障其性能(模型评估)。如果通过了这一评估,我们会将其部署到自动驾驶汽车(模型服务)。如果在任何阶段遇到问题,我们都可以重新开始
一旦我们收集到了用于 ML 模型训练的数据,它就会被我们的 ML 栈摄取。数据摄取过程会选择我们计划使用的日志,并从中摄取数据。
我们将这些日志数据分为训练数据、测试数据和验证数据。75%的日志用于训练,15%的日志用于测试,10%的日志用于验证。这种比例是为了用大部分数据训练 ML 模型,提高模型的准确性,然后随着训练的进展来做验证。最后,我们在训练过程中没用过的一小部分数据上测试模型效率。在以后的文章中我们将介绍 GeoSplit,这是一个数据管道,用于选择日志,并根据其地理位置分类为训练、测试和验证用途。
划分数据后,我们使用 Uber ATG 的深度学习开源数据访问库 Petastorm,从数据生成日志中摄取数据。我们从日志中摄取的数据包括:
我们将这些信息按日志一个个保存在 HDFS 上。然后使用 ApacheSpark 从多个日志中并行摄取数据。以这种方式摄取数据会将数据保存为优化格式,以便训练管道可以轻松、快速地使用它们。当我们运行训练管道时,并不想等待漫长而繁重的 API 调用来查找某些信息。有了这套系统,我们从 HDFS 加载模型后,就将所有需要的信息(摄取的数据)存储在 GPU 的内存中,以确保管道可以高效地读取训练数据。
下面的图 2 显示了在 CPU 群集上运行,将摄取的数据保存到 HDFS 的提取流程的示例:
图 2:我们的自动驾驶汽车会生成各种日志(此处展示了相机和 LiDAR 信息,此外还有雷达信息和真值标签)。然后我们立即从 CPU 群集上的每个日志中提取这些数据,并将提取到的数据保存到 HDFS,以便管道处理
数据管道选择并提取了用于训练、测试和验证的数据后,我们将运行查询以拉出场景中的帧数和数据集中不同标签类型的出现次数。我们将这些查询的结果与之前的数据集做对比,以了解情况如何变化,变化是否符合预期。例如,如果某个标签类型的出现次数相比其他类型增加得太快,就将触发进一步的分析以理解这种更改及其对模型的影响。
正确选择,提取和验证数据后,我们便拥有了训练模型所需的资源。我们利用 Horovod 的分布式训练,基于提取到的数据快速训练。我们通过数据并行将数据分布在不同的 GPU 上,如下图 3 所示,这意味着要在拥有各个数据部分的不同 GPU 上训练相同的模型。例如,如果我们使用两个 GPU,则将数据分为两部分,并且在具有第一部分数据的第一个 GPU,和具有第二部分数据的第二个 GPU 上训练相同的模型。
图 3:我们使用提取到的数据(包括此处显示的图像以及其他传感器数据)来在 GPU 群集上使用 Horovod 进行分布式训练,并将数据保存到 HDFS
每个过程都独立于其他过程来执行正向和反向传递(正向传递是指为网络所有层的输入计算输出,以及损耗函数的损耗;而反向传递是指计算网络中每个节点损耗变化的速率)。
接下来,我们使用 Horovod 的环减少算法,使 worker 节点能够平均梯度,并将其分散到所有节点,过程中无需参数服务器就能每个过程所学的知识分配给其他所有过程。
利用 TensorFlow 和 PyTorch 打造的 ML 框架,工程师可以通过 TensorBoard 来监视训练作业,以验证训练是否按预期进行。
Uber ATG 使用一种混合方法来处理 ML 计算资源,训练作业既可以在由 GPU 和 CPU 集群支持的本地数据中心中运行,也能在云上运行。
为了在带有 GPU 的本地数据中心内编排训练作业,我们使用了 Peloton(https://eng.uber.com/peloton/),这是 Uber 开发的开源统一资源调度程序。Peloton 将我们的容器部署到群集上的各个进程,从而轻松地将作业扩展到许多 GPU 或 CPU 上。对于基于云的训练,我们使用的是 Kubernetes,它能跨主机群集部署和扩展应用程序容器。
使用选定的,提取的和经过验证的数据训练完机器学习模型后,我们便可以评估它们完成任务的能力,包括识别场景中的物体和预测角色的路径等。
训练了给定的模型后,我们将评估模型本身的性能以及整个系统的性能。我们使用特定于模型的指标和系统指标来测试 ML 模型。我们还评估了硬件指标,以了解我们的模型在最终部署的硬件上的执行速度。
我们在给定的测试集上计算各种特定于模型的指标。例如,对于感知组件中的对象检测模型,我们会计算精度(被证明是正确的检测百分比)和召回率(被模型正确识别的真值对象的比例)这些模型指标。
查看这些指标可为我们提供有关模型性能的重要见解,进而不断提升性能。当我们找到模型表现不佳的场景时,我们会包含类似案例来调整数据管道,从而为模型提供更多可用数据。在这些情况下,有时我们会对那些场景给予更多的权重(这意味着与其他场景相比,这些数据实例将为训练模型做出更大的贡献),从而优化相关模型。
当我们使用更多场景训练模型后,模型在这些场景中的表现就会变好。此外,我们还要确保模型不会在已经表现良好的场景上出现性能退化。
系统指标包括对整个车辆运动的安全性和舒适性测量,这些测量是通过大型测试集执行的。一旦给定模型的特定模型指标显示出良好的结果,我们就会在模型开发的后期阶段评估系统指标。鉴于自动驾驶栈中的不同 ML 模型彼此依赖(例如,我们的预测组件使用感知组件的输出),系统指标为我们提供了一个重要而全面的概览,展示了系统各部分的表现与各个组件版本之间的关系。衡量系统指标,有助于团队更全面地了解新模型是如何影响系统中其他组件的。定期评估系统指标可以让我们发现并修复由于 ML 模型更新,而在其他组件中发生的问题。
Uber ATG 有一套内部基准测试系统,开发人员可以使用它来分析软件的特定部分,例如特定模型的推理,并评估其在自动驾驶汽车硬件上的运行速度。我们使用自动驾驶车辆记录的真实世界数据进行评估,以在部署模型之前了解模型的性能表现。
一旦我们训练好了一个模型,验证了它在孤立状态下是否可以正常运行,并确认它在系统的其余部分上都能正常运行后,我们便将其部署在自动驾驶汽车的推理引擎上。该系统通过训练好的模型运行输入以生成输出。例如,运行感知组件将提供场景对象类型及其坐标,而预测组件将提供对象的未来轨迹。
在部署模型后,我们会通过五步流程不断迭代所有模型来改进它们,重点放在模型需要改进的地方,通常是在模型评估步骤中发现的。
尽管我们将流程简化为从数据集摄取到模型部署的五个步骤,但每个步骤本身都涉及诸多系统,整个端到端的工作流程可以轻松跨越数周时间。流程由人工驱动时存在很多人为错误的可能,因此我们开发了更好的工具和平台,以使工作流程尽可能地自动化。
VerCD 就是这样一个平台,它可以跟踪整个流程中代码、数据集和模型之间的依赖关系,还可以编排这些 ML 工件的创建,所以它成为了流程中的重要组成部分。具体来说,VerCD 涵盖的流程从数据集提取阶段开始,涵盖模型训练,然后以指标计算结束。
由于自动驾驶领域中 ML 模型的深层依赖性和开发复杂性,我们需要一个持续交付(CD)系统,利用敏捷原理专门跟踪和管理 ML 工件的版本化依赖项。尽管诸如 Kubeflow 和 TensorFlow Extended 之类的开源工具提供了高级编排能力来构建数据集和训练模型,但是它们需要大量的集成工作。此外,它们交付单个工作流的表现不佳,并且没有很好地支持持续交付(CD)和持续集成(CI)。
另一方面,市面上也有支持版本控制和 CI/CD 的传统软件工具,例如 Git 和 Jenkins。尽管这些工具无法在我们的自动驾驶软件中处理 ML 工件,但我们从它们中得到了构建 VerCD 的灵感。
多数软件开发人员使用版本控制、依赖项跟踪、持续集成和持续交付,基于敏捷开发流程频繁发布软件。由于这些概念是众所周知的,因此这里我们只关注它们在 ML 工作流领域的应用。
应用于 ML 域的版本控制原则可以分析 ML 工作流某些部分的一项更改对下游依赖的影响。例如,如果同时跟踪代码和 ML 工件版本,则更新特定数据集或模型的 ML 工程师,或用来生成这些工件的支持软件,就可以更好地理解这些更改的影响。在这里,版本控制使我们能够独立于其他开发人员的更改来跟踪特定开发人员的更改的影响,即使他们是并行工作的也不影响。
关于 ML 域中依赖项跟踪的一个例子,就是对标签和原始数据的调整会更改数据集的性质,进而影响训练结果。此外,数据提取或模型训练代码和配置中的更改会影响它们负责构建的工件。因此,先在旧数据集上训练模型然后提取新的数据集是没有意义的,因为数据将与训练好的模型不一致。在这种情况下应先提取数据集,然后再建立模型,这是依赖项约束决定的顺序。
尽管有许多成熟的工具可以实现传统软件代码库的持续交付,但我们发现在同样的成熟度和标准化水平下,如今还不存在用于机器学习的同类工具。与 CD 工作流相比,ML 工作流涉及代码、数据和模型,并且只有第一个由传统软件工程工具处理。下面的图 4 说明了 CD 工作流程中的某些差异,而图 5 说明了构建最终 ML 工件所需的系统范围和复杂性:
图 4:传统的持续交付周期与 ML 的不同之处在于,ML 开发人员不仅要构建并测试代码,还必须构建数据集、训练模型和计算模型指标
图 5:与所有支持系统和代码(例如配置、数据收集、特性提取、数据验证、机器资源管理、分析工具、过程管理工具、服务架构和监控等)相比,ML 工作流的最终结果只是一个很小的工件
通过实施敏捷流程,CD 使工程师能够快速适应不断变化的需求,及早并经常发现错误,并促进所有 ML 块的并行开发,从而提高开发人员的生产率。但是,重度 ML 的组织中常见的频繁更改和并行模型开发需要解决版本控制问题,且由于自动驾驶软件栈中的依赖关系图很深,因此该领域的问题更加棘手,如上所述。这样的依赖图不仅涵盖单个 ML 模型的代码、数据和模型,而且涉及各种 ML 模型之间的依赖关系。
在自动驾驶汽车开发中,依赖图是特别深的。这种深度是自动驾驶软件栈中分层架构的产物,其中每一层都提供了不同的 ML 功能。为了说明这一点,我们在下面的图 6 中展示了顺序连接的三个 ML 模型的高级架构:
图 6:左侧显示的对象检测模型的依赖图,以及右侧显示的其他两个 ML 模型,描述了由版本控制系统处理的代码和配置(绿色),及未由版本控制系统处理的项目(灰色)
在上面的图 6 中,我们的 ML 层包括:
每一个模型都有一个复杂的,涉及代码和工件的依赖图,如图 6 左侧所示。这一层本身还有好几层深,涉及三个工件的生成:
路径预测和计划模型的依赖图都像对象检测模型一样复杂。在某些情况下,当我们查看整个系统时,完全展开的图至少 15 级深。图的深度对 CD 构成了特殊的挑战,因为这些 ML 组件的并行开发极大地增加了 CD 系统必须管理的版本数量。
我们开发了 VerCD,这是一套工具和微服务集合,旨在为 Uber ATG 的自动驾驶车辆软件提供所有 ML 代码和工件的版本控制和持续交付能力。组成 VerCD 的许多组件都是公开可用的,因此我们的大部分工程工作都花在了添加公司特定的集成上,以使现有的编排器能够在整个端到端 ML 流程中与各种异构系统交互。
与传统的版本控制和持续交付系统不同,VerCD 会跟踪每个 ML 组件的所有依赖项,除了代码外通常还包括数据和模型工件。VerCD 提供的元数据服务跟踪依赖关系图,而持续集成编排器用它来定期运行整个 ML 工作流管道,以生成数据集、训练好的模型和指标。对于经常需要将新实验与历史基准做对比,或检查历史构建以跟踪错误的工程师来说,VerCD 确保了 ML 工件始终可重现和可追溯。
在设计 VerCD 时,我们同时结合了实验和生产工作流程。我们不仅需要该系统来支持由编排器驱动的持续集成和持续交付(CI/CD)工作流,而且还希望 VerCD 拥有在实验期间代表工程师构建数据集、训练模型和运行指标的功能。这些要求意味着 VerCD 接口需要同时支持人类和机器的访问。为了获得可重用的接口,我们选择了带有 Python 库绑定的 RESTAPI,如下图 7 和 8 所示。
图 7:VerCD 包括版本和依赖项元数据服务以及编排器服务。我们使用例如Flask、SQLAlchemy、MySQL 和 Jenkins 的常规框架,但通过 ATG 特定的集成来增强其功能
图 8:版本和依赖项元数据服务具有用于数据集构建、模型训练和指标计算的各个端点。REST API 是一个 Flask and SQLAlchemy 应用,由 MySQL 支持以保存依赖项元数据。黄色的 API 处理程序和数据访问层是为 ATG 特定的用例设计的
出于相同的原因,VerCD 被设计为一组单独的微服务,用于数据集构建、模型训练和指标计算。我们选择了基于微服务的架构,这是 Uber 的流行选择,其中每个微服务负责特定的功能,从而允许系统扩展并在服务之间提供隔离。VerCD 的 CI/CD 流程是线性且固定的,而实验流程需要更大的灵活性,通常涉及自定义流程和临时的运行作业,这些工作可能只专注于数据集提取、模型训练或指标评估工作的子集。有了一组单独的微服务,就能获得必要的灵活性,从而在这两种流程之间充分利用相同功能。
为了实现依赖项跟踪,用户提供向 VerCD 注册的任何数据集、模型或指标构建的显式依赖项,然后系统在数据库后端管理这些信息。如下面的图 9 所示,我们使用一个 Jenkins 编排器开始常规构建,并提供连接器和集成代码来增强编排器的功能,以便它可以解析依赖元数据并操作 ATG 特定的系统。
图 9:VerCD 的编排器服务负责管理用于构建数据集、训练模型和计算指标的工作流管道。它由现成的 Jenkins 发行版组成,并增加了我们自己的 ATG 特定集成(黄色),使编排器能够与外部 ATG 系统交互
例如,我们的编排器可以调用这些原语来构建自动驾驶汽车的运行时以进行测试、与我们的代码库交互,还能使用深度学习或 Apache Spark 库创建图像。其他原语包括在数据中心之间以及云之间复制数据集(如果模型在这些位置训练)。
在用户注册新数据集后,VerCD 数据集服务会将依赖项元数据存储在我们的数据库中。对于数据集、模型和指标来说,依赖项将包括存储库的 Git 哈希和代码入口点的路径。根据要构建的工件,还将引用其他版本化的元素(例如标签和源数据的版本化集合,或另一个版本化的数据集 / 模型)。我们期望所有依赖项都是版本化且不可变的;例如数据集这边,依赖项将是来自版本化源的传感器数据的时间序列。
注册的最后一步,元数据服务使用编排器服务启动一个构建。这将启动 Apache Spark 作业来运行代码,监视作业(如有必要就重新启动),最后将数据集复制到管理的存储位置(本地数据中心或云)。然后使用数据复制到的各个位置来更新元数据服务。
我们每种微服务的 API 旨在提供生产和实验流程的编程访问接口。由于系统的目标是确保数据集构建、模型训练和指标运行的可重复性和可追溯性,因此我们要求在注册过程中指定这三个流程的所有版本化不可变依赖项。API 提供了对构建信息(例如生成工件的位置以及构建生命周期等)的访问来确保可追溯性。在尝试调试 ML 工件或性能下降时,此类信息特别重要。
数据集服务负责跟踪用于构建给定数据集的依赖项。REST API 支持以下功能:创建新数据集、读取数据集的元数据、更新数据集的元数据、删除数据集以及获取数据集的工件位置(例如 S3 或 HDFS)。当用户注册新数据集时,后端编排器将立即继续构建和验证数据集。
数据集由名称和版本号唯一标识,而 VerCD 跟踪的依赖项使我们每次都能重现完全相同的数据集工件,这些依赖项包括:
数据集可以使用其当前生命周期状态进行注释,例如何时注册、何时构建失败或中止、何时构建成功且状态良好、何时弃用数据集,最后是数据集的生命周期何时结束。
模型训练服务负责跟踪用于训练给定模型的依赖项。REST API 支持以下功能:训练新模型、读取新模型的元数据、更新已注册模型的元数据,以及升级为生产版本。当用户注册新模型时,后端编排器将立即开始训练它。
模型通过名称和版本唯一标识,VerCD 跟踪的依赖项使我们能够重现相同的训练运行作业,这些依赖项有:
几乎所有新的 ML 模型都是从实验开始的,因此 VerCD 支持验证步骤,以允许在实验模型和生产模型之间平稳过渡。验证步骤对模型训练施加了额外的约束,以确保生产环境所需的可重复性和可追溯性;而在实验过程中,这种约束可能已经以快速开发的方式实现了。如下图 10 所示,一旦 ML 工程师在 VerCD 的模型服务 API 中定义了实验模型,我们的系统就会开始训练它。
图 10:在 VerCD 工作流程中,“实验”和“验证”状态彼此独立,但是在模型可以转换到“生产”状态之前,这两个状态必须是成功的
根据其表现不同,我们将模型指定为失败、中止或成功。如果模型失败或必须中止,则 ML 工程师可以选择使用一组新参数来重建。
VerCD 可以异步启动模型验证。如图 10 所示,我们的实验和验证过程包括相同的基本步骤,不同之处是实验图运行模型训练代码,而验证图运行模型验证代码。我们的验证代码在模型训练管道上执行各种健全性检查,例如验证构建是否成功,输出工件是否存在,以及依赖项是否是不可变和版本化的。具体的检查内容取决于要训练的特定模型。
仅当实验构建成功且验证成功,并且可以在任何时间点以任何顺序调用它们时,才可以将模型提升到生产状态。在模型升级过程中,用户提供命名空间、名称、主要版本和次要版本。然后,端点对训练工件生成快照,并将其与模型训练元数据一起版本化,以备将来参考。在实验阶段,代码和模型的性能经常会出现波动。到开发人员准备升级时,实验输出的结果大部分都应该是正向的,并且系统会批准将结果存档。
VerCD 涉及的机器学习工作流的最后一步,是评估训练模型的指标。与我们其他的微服务一样,为了确保指标的可追溯性和可重复性,该架构的指标服务有一个元数据服务,该元数据服务可跟踪运行指标作业及其结果工件所需的依赖项。与数据集和模型的情况类似,我们会跟踪:
如今,VerCD 在为我们的许多数据集构建、模型训练和指标管道提供定期的日常集成测试。这些频繁的集成测试让我们能知道这些工作流当前的可追溯性和可再现性状态,即使有无数版本的代码、数据和模型工件,以及将整个系统联系在一起的深层依赖图也不在话下。
例如,VerCD 数据集服务已成为 Uber ATG 自动驾驶传感器训练数据的可靠来源。拥有可以持续交付传感器数据的服务之前,无论是在业务流程还是在记录保存方面,我们的数据集生成过程都是非常依赖人工的。将数据集构建流程纳入 VerCD 后,我们将新数据集构建的频率提高了 10 倍以上,显著提高了效率。维护一个常用数据集的清单也提高了 ML 工程师的迭代速度,因为开发人员可以立即继续实验,而无需等待几天时间来构建新的数据集。
此外,我们还为自动驾驶汽车的重点目标检测和路径预测模型提供了每日和每周的训练作业。这种频繁的训练节奏将检测和修复某些错误的时间缩短到了几天时间,而在实现 CICD 训练之前,错误的窗口在很大程度上是未知的,需要许多工程师费心注意。
我们的 ML 模型生命周期流程,以及为简化此流程而构建的工具(如 VerCD)可帮助我们管理所使用的诸多模型,并更快地迭代它们。这些实践和解决方案都是源于我们在开发高水平的自动驾驶汽车系统时对高效率的需求。
我们在 ML 开发中建立多个流程阶段,进而开发诸如 VerCD 的支持系统来管理 ML 流程日益增加的复杂性的过程中已经取得了长足的进步。随着我们技术的不断成熟,以及复杂性和精巧程度的增长,依靠人为干预来管理各个 ML 流程阶段的做法变得越来越不可行。这些工具使工程师能够更快地迭代 ML 组件,从而提升我们自动驾驶汽车的性能表现。
原文链接:
https://eng.uber.com/machine-learning-model-life-cycle-version-control/
领取专属 10元无门槛券
私享最新 技术干货