深度学习系统设计(一)

153 阅读1小时+

深度学习系统设计(一)

原文:zh.annas-archive.org/md5/c7aabb3a9c13924ec60749e96c9ff05f

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

序言

如果一个深度学习系统能够连接两个不同的世界——研究和原型设计与生产运营,那么可以假定它是有效的。设计这种系统的团队必须能够与这两个世界中的从业者进行沟通,并处理来自每个世界的不同要求和约束。这需要对深度学习系统中的组件是如何设计的,以及它们预期如何协同工作有一个原则性的理解。现有文献很少涵盖深度学习工程的这一方面。当初级软件工程师入职并期望成为有效的深度学习工程师时,这种信息差距就会成为一个问题。

多年来,工程团队通过使用他们获得的经验并从文献中挖掘出他们需要了解的知识来填补这一空白。他们的工作帮助传统软件工程师在相对较短的时间内构建、设计和扩展深度学习系统。所以当我得知曾领导过深度学习工程团队的 Chi 和 Donald 采取了非常重要的倡议,将这些知识整合并以书籍的形式分享时,我感到非常兴奋。

我们早就需要一本全面的书籍,介绍如何构建支持将深度学习从研究和原型设计转化为生产的系统。设计深度学习系统终于填补了这一需求。

该书以高层次的介绍开始,描述了深度学习系统是什么以及其作用。随后的章节详细讨论了每个系统组件,并就各种设计选择的利弊提供了动机和见解。每一章都以分析结束,帮助读者评估最适合和最相关的选项以满足他们自己的用例。作者在结尾进行了深入讨论,汇总了之前所有章节的内容,探讨了从研究和原型设计到生产的艰难之路。为了帮助工程师将所有这些想法付诸实践,他们创建了一个示例深度学习系统,提供了完全功能的代码,以说明核心概念,并为那些刚刚进入这个领域的人提供一些体验。

总的来说,读者会发现这本书易于阅读和导航,同时将他们对如何策划、设计和实现深度学习系统的理解提升到一个全新的层次。对于所有专业水平的从业者,他们对设计有效的深度学习系统感兴趣,将会将这本书视为一个宝贵的资源和参考。他们会读一遍以了解全局,然后在构建系统、设计组件和做出关键选择以满足使用系统的所有团队时,一遍又一遍地返回阅读。

—Silvio Savarese,Salesforce 执行副总裁,首席科学家

—Caiming Xiong,Salesforce 副总裁

前言

十多年前,我们有幸开发了一些早期的面向最终用户的产品功能,这些功能由人工智能驱动。这是一项巨大的工程。在那个时候,收集和整理适合模型训练的数据并不是一种常见的做法。很少有机器学习算法被打包成可直接使用的库。进行实验需要手动运行管理,并构建自定义的工作流和可视化界面。定制服务器用来为每种类型的模型提供服务。除了那些资源密集型的科技公司外,几乎每一个新的人工智能产品功能都是从零开始构建的。智能应用程序有朝一日成为一种商品是一个遥不可及的梦想。

在使用了一些人工智能应用程序后,我们意识到每次我们都在重复类似的仪式,对我们来说,设计一种系统化的方式,通过原型设计,为交付人工智能产品功能至生产提供帮助,显得更有意义。这一努力的成果是 PredictionIO,这是一套开源的框架软件套件,汇集了最先进的软件组件,用于数据收集和检索、模型训练和模型服务。通过其 API 完全可定制,并通过几个命令即可部署为服务,它有助于缩短每个阶段所需的时间,从运行数据科学实验到训练和部署生产就绪的模型。我们很高兴地得知,世界各地的开发人员能够使用 PredictionIO 来制作他们自己的人工智能应用程序,为他们的业务带来一些令人惊叹的提升。PredictionIO 后来被 Salesforce 收购,以解决一个更大规模的相似问题。

我们决定撰写本书的时候,整个行业都在蓬勃发展,拥有一个健康的人工智能软件生态系统。许多算法和工具已经可以处理不同的用例。一些云服务提供商,如亚马逊、谷歌和微软甚至提供完整的托管系统,使团队可以在一个集中的位置共同进行实验、原型设计和生产部署。无论您的目标是什么,现在您有许多选择,以及许多种方法将它们组合在一起。

在我们与团队合作交付由深度学习驱动的产品功能时,出现了一些反复出现的问题。为什么我们的深度学习系统设计成这个样子?对于其他特定用例来说,这是最好的设计吗?我们注意到,初级软件工程师最经常提出这些问题,我们采访了其中的一些人,想知道为什么。他们透露,他们传统的软件工程训练并没有让他们有效地与深度学习系统合作。当他们寻找学习资源时,他们发现只有零星和零散的关于特定系统组件的信息,几乎没有资源讨论软件组件的基础知识,为什么它们被组合在一起,以及它们是如何共同工作形成完整系统的。

为了解决这个问题,我们开始建立一个知识库,最终演变成了类似手册的学习材料,解释了每个系统组件的设计原则、设计决策的利弊以及从技术和产品角度的理由。我们被告知,我们的材料帮助新团队成员快速上手,并让没有建立深度学习系统经验的传统软件工程师迅速掌握。我们决定与更大的读者群分享这些学习材料,以书的形式。我们联系了 Manning,剩下的就是历史。

致谢

写一本书确实需要很多孤独的努力,但是没有以下个人的帮助,这本书是不可能完成的。

在 Salesforce Einstein 团队(Einstein 平台、E.ai、Hawking)的不同团队中工作,构成了本书的大部分基础。这些杰出而有影响力的团队成员包括(按字母顺序排列)Sara Asher、Jimmy Au、John Ball、Anya Bida、Gene Becker、Yateesh Bhagavan、Jackson Chung、Himay Desai、Mehmet Ezbiderli、Vitaly Gordon、Indira Iyer、Arpeet Kale、Sriram Krishnan、Annie Lange、Chan Lee、Eli Levine、Daphne Liu、Leah McGuire、Ivaylo Mihov、Richard Pack、Henry Saputra、Raghu Setty、Shaun Senecal、Karl Skucha、Magnus Thorne、Ted Tuttle、Ian Varley、Yan Yang、Marcin Zieminski 和 Leo Zhu。

我们也想借此机会感谢我们的开发编辑 Frances Lefkowitz。她不仅是一位提供出色写作指导和内联编辑的优秀编辑,还是一位在整个写书过程中指导我们的优秀导师。没有她,这本书就不会达到目前的质量,也不会按计划完成。

我们感谢 Manning 团队在整本书的写作过程中给予的指导。我们非常感谢通过 Manning 早期访问计划(MEAP)在书写的早期阶段获得读者意见的机会。

致所有审阅者——亚历克斯·布兰克、阿米特·库马尔、阿尤什·托马尔、巴格万·科马迪、迪卡尔·朱亚尔、埃斯雷夫·杜尔纳、戈拉夫·苏德、吉伦姆·阿利昂、哈马德·阿尔沙德、杰米·沙弗、贾普尼特·辛格、杰里米·陈、若昂·迪尼斯·费雷拉、凯蒂娅·帕特金、基思·金、拉里·蔡、玛利亚·安娜、米凯尔·多特雷、尼克·德克鲁斯、尼科尔·康宁斯坦、诺亚·弗林、奥利弗·科尔滕、奥马尔·埃尔·马拉克、普兰杰尔·兰詹、赛义德·艾希-查迪、桑迪普·迪、桑凯特·沙玛、萨特杰·库马尔·沙胡、萨亚克·保罗、希薇塔·乔希、西蒙·斯瓜扎、斯里兰·马查拉、苏米特·巴塔查里亚、厄辛·斯特劳斯、维迪亚·维纳伊和韦伊·罗——感谢你们的建议帮助了这本书更加优秀。

我要感谢我的妻子吴佩,她在写作这本书的过程中给予了我无条件的爱和巨大的支持。在新冠疫情困难的时刻,佩始终是一个宁静祥和的角落,让这本书在一个繁忙的家庭中有两个可爱的小孩——凯瑟琳和天成的情况下得以完成。

另外,我还要感谢许彦,一个才华横溢的 10 倍开发者,他几乎写了整个代码实验室。他的帮助使得代码实验室不仅质量高,而且易于学习。许彦的妻子,董,全心全意地支持着他,这样许彦就能专心致志于书实验室。

我还要感谢的另一个人是黛安·西伯尔德,Salesforce 一位富有才华和经验丰富的技术作家。黛安用她自己的写作经历启发了我,并鼓励我开始写作。

——王弛

共同创办 PredictionIO(后来被 Salesforce 收购)让我学到了关于构建开源机器学习开发者产品的宝贵经验。这一冒险而有回报的旅程不可能没有彼此之间的巨大信任。他们是(按字母顺序排列):肯尼思·陈、汤姆·陈、帕特·费雷尔、伊莎贝尔·李、保罗·李、亚历克斯·梅里特、托马斯·斯通、马可·维维罗和贾斯汀·叶。

西蒙·陈值得特别一提。陈共同创办了 PredictionIO,我也有幸与他在他之前的创业努力中一起工作和学习。当我们在香港的同一所中学(九龙华仁书院)上学时,他是第一个正式向我介绍编程的人。学校的其他鼓舞人心的人包括(按字母顺序排列):唐纳德·陈、杰森·陈、哈姆雷特·朱、郭嘉权、杰弗里·侯、方锦鸿、埃里克·刘、刘金、雷莱克斯·李、凯文·雷、丹尼·辛格、诺曼·苏、史蒂文·唐和罗博·黄。

我非常感激我的父母和我的哥哥罗纳德。他们让我早早接触到计算机。他们的持续支持在我渴望成为一名计算机工程师的成长年代起着至关重要的作用。

我的儿子,斯宾塞,是为什么生物深度神经网络是世界上最令人惊奇的事物的活生证明。他是一份美好的礼物,每天都向我展示,我始终可以成长并变得更好。

无法用言语表达我妻子 Vicky 在我心中的重要性。她总是能够让我在困难时期振作起来,使我变得更好。她是我所能请求的最好的伴侣。

—Donald Szeto

关于本书

本书的目的是为工程师提供设计、构建或设置有效机器学习系统并将这些系统定制为他们可能遇到的任何需求和情况的能力。他们开发的系统将促进、自动化并加速机器学习(尤其是深度学习)项目在各个领域的发展。

在深度学习领域,模型是吸引所有注意力的主角。考虑到从这些模型中开发出的新应用程序经常进入市场——如能够检测人类的安防摄像机、行为像真正人类一样的互联网视频游戏虚拟角色、可以编写代码解决任意问题的程序以及可实现完全自主驾驶的高级驾驶辅助系统,这样的做法或许是正确的。在很短的时间内,深度学习领域充满了巨大的激动和有待完全实现的潜能。

但是,模型并不是孤军奋战。为了将产品或服务推向成熟阶段,需要将模型置于支持其各种服务和存储的系统或平台(我们可以互换使用这些术语)中。例如,它需要一个 API、一个数据集管理器以及用于存储工件和元数据的存储空间,等等。因此,在深度学习模型开发团队的背后,需要有一支非深度学习开发人员的团队,负责创建容纳模型和所有其他组件的基础设施。

我们观察到在这个行业中存在的问题是,通常负责设计深度学习系统和组件的开发人员只具有浅显的深度学习知识。他们不理解深度学习需要从系统工程方面满足的具体要求,因此倾向于在构建系统时遵循通用方法。例如,他们可能会选择将与深度学习模型开发所有工作的抽象化交给数据科学家,并仅关注自动化。因此,它们所构建的系统依赖于传统的作业调度系统或商业智能数据分析系统,这些系统并未针对深度学习训练作业的运行方式或深度学习特定的数据访问模式进行优化。结果,该系统难以用于模型开发,而模型发货速度缓慢。基本上,这些缺乏深刻理解的工程师被要求构建支持深度学习模型的系统。因此,这些工程系统效率低下,不适合深度学习系统。

针对数据科学家的观点已经写了很多关于深度学习模型开发的内容,涵盖了数据收集和数据集增强、编写训练算法等。但是很少有书籍,甚至是博客,涉及支持所有这些深度学习活动的系统和服务。

在这本书中,我们从软件开发者的角度讨论构建和设计深度学习系统。我们的方法是首先整体描述一个典型的深度学习系统,包括其主要组件及其连接方式;然后我们在单独的章节中深入探讨每个主要组件。我们始终通过讨论需求来开始每个组件章节。然后我们介绍设计原则和样例服务/代码,并最终评估开源解决方案。

因为我们无法涵盖每一个现有的深度学习系统(供应商或开源),我们在书中专注于讨论需求和设计原则(带有示例)。在学习了这些原则,尝试了本书的示例服务,并阅读了我们对开源选项的讨论后,我们希望读者能够进行自己的研究,找到最适合他们的方案。

谁应该阅读这本书?

这本书的主要受众是软件工程师(包括最近毕业的计算机科学学生),他们希望快速转入深度学习系统工程领域,比如那些希望在深度学习平台上工作或将一些人工智能功能(例如模型服务)集成到他们的产品中的人。

数据科学家、研究人员、经理和任何其他使用机器学习解决实际问题的人也会发现这本书很有用。在了解了基础架构(或系统)之后,他们将能够为改善模型开发过程的效率提供精确的反馈给工程团队。

这是一本工程书,您不需要机器学习背景,但您应该熟悉基本的计算机科学概念和编码工具,比如微服务、gRPC 和 Docker,以运行实验室并理解技术材料。无论您的背景如何,您仍然可以从本书的非技术性材料中受益,帮助您更好地理解机器学习和深度学习系统是如何将产品和服务从想法转化为生产的。

通过阅读这本书,您将能够理解深度学习系统的工作原理以及如何开发每个组件。您还将了解何时从用户那里收集需求,将需求转化为系统组件设计选择,并集成组件以创建一个有助于用户快速开发和交付深度学习功能的连贯系统。

这本书的组织方式:一张路线图

这本书共有 10 章和三个附录(包括一个实验室附录)。第一章解释了深度学习项目开发周期是什么,以及基本的深度学习系统是什么样子。接下来的章节深入探讨了参考深度学习系统的每个功能组件。最后一章讨论了模型如何被部署到生产环境中。附录包含一个实验室环节,让读者可以尝试样本深度学习系统。

第一章描述了深度学习系统是什么,系统的不同利益相关者以及他们如何与之交互以提供深度学习功能。我们称这种交互为深度学习开发周期。此外,你将概念化一个深度学习系统,称为参考架构,它包含所有必要的元素,并可以根据你的要求进行调整。

第 2 至 9 章涵盖了参考深度学习系统架构的每个核心组件,例如数据集管理服务、模型训练服务、自动超参数优化服务和工作流编排服务。

第十章描述了如何将最终产品从研究或原型阶段推向发布给公众使用的阶段。附录 A 介绍了样本深度学习系统并演示了实验室练习,附录 B 对现有解决方案进行了调查,附录 C 讨论了 Kubeflow Katib。

关于代码

我们相信学习的最佳方式是通过实践和尝试。为了演示本书中解释的设计原则并提供实践经验,我们创建了一个样本深度学习系统和代码实验室。样本深度学习系统的所有源代码、设置说明和实验室脚本都可以在 GitHub 上找到(github.com/orca3/MiniAutoML)。你也可以从本书的 liveBook(在线)版本(livebook.manning.com/book/software-engineers-guide-to-deep-learning-system-design)和 Manning 网站(www.manning.com)获取可执行的代码片段。

附录 A 中的“hello world”实验室包含一个完整但简化的迷你深度学习系统,具有最基本的组件(数据集管理、模型训练和服务)。我们建议你在阅读本书第一章之后尝试“hello world”实验室,或在尝试本书中的样本服务之前进行尝试。此实验室还提供了 shell 脚本和所有您需要开始的资源的链接。

除了代码实验室外,本书还包含许多源代码示例,以编号列表和与普通文本一致的方式显示。在这两种情况下,源代码都采用固定宽度 字体 格式 显示,以便与普通文本分开。有时,代码也会以**加粗**的形式显示,以突出显示与本章前一步骤中的代码不同的代码,例如当新功能添加到现有代码行时。

在许多情况下,原始源代码已经重新格式化;我们已添加了换行符并重新排列了缩进,以适应书中可用的页面空间。在极少数情况下,即使这样还不够,列表也包括行续标记(➥)。此外,当在文本中描述代码时,源代码中的注释通常已从列表中删除。代码注释伴随许多列表,突出显示重要概念。

liveBook 讨论论坛

购买设计深度学习系统包括免费访问 liveBook,Manning 的在线阅读平台。使用 liveBook 的独家讨论功能,您可以将评论附加到全书或特定部分或段落。为自己做笔记,提出和回答技术问题,并从作者和其他用户那里获得帮助,都是易如反掌的。要访问论坛,请转到livebook.manning.com/book/software-engineers-guide-to-deep-learning-system-design。您还可以了解有关 Manning 论坛和行为规则的更多信息,请访问livebook.manning.com/discussion

Manning 致力于为读者提供一个场所,使个人读者之间以及读者与作者之间能够进行有意义的对话。这不是对作者参与的任何特定数量的承诺,作者对论坛的贡献仍然是自愿的(且未获报酬的)。我们建议您尝试向作者提出一些具有挑战性的问题,以免他们失去兴趣!只要书还在出版中,论坛和以前的讨论档案将可以从出版商的网站上访问。

关于作者

王琦是 Salesforce Einstein 团队的首席软件开发人员,在那里他构建了数百万 Salesforce 客户使用的深度学习平台。此前,他曾在 Microsoft Bing 和 Azure 工作,构建大规模分布式系统。王琦已申请了六项专利,其中大多与深度学习系统有关,并最近完成了斯坦福大学的人工智能研究生证书项目。

Donald Szeto 是 PredictionIO 的联合创始人和首席技术官,这是一家旨在帮助普及和加速机器学习应用的初创公司。PredictionIO 被 Salesforce 收购后,他继续在机器学习和深度学习系统上工作。Donald 是 Aftermint 的创始人,其目标是搭建 Web2 和 Web3 之间的桥梁。他还投资、提供建议,并指导技术初创公司。

关于封面插图

《设计深度学习系统》 封面上的人物是来自 Jacques Grasset de Saint-Sauveur 收藏的“黑森林人”,摘自 1797 年出版。每幅插图都是精细绘制并手工上色的。

在那些日子里,仅凭着服饰就能轻易识别人们居住的地方以及他们的职业或社会地位。Manning 通过基于数个世纪前区域文化的丰富多样性而创作的书籍封面,庆祝了计算机业务的创造力和主动性,这些文化被像这样的收藏品中的图片重新呈现出来。

第一章:深度学习系统简介

本章内容包括

  • 定义深度学习系统

  • 产品开发周期及深度学习系统如何支持其

  • 基本深度学习系统及其组成部分概述

  • 搭建深度学习系统与开发模型之间的差异

本章将为您提供一个深度学习系统的全貌思维模型。我们将回顾一些定义,并提供一个参考系统架构设计和该架构的完整示例实现。我们希望这个思维模型能够让您看到其他章节如何详细介绍每个系统组件,并将其融入整体图景。

在开始本章之前,我们将讨论一个超越深度学习系统的更大图景:我们称之为深度学习开发周期。这个周期概述了基于深度学习的产品推向市场所涉及的各种角色和阶段。模型和平台并不是孤立存在的;它们受产品管理、市场调研、生产和其他阶段的影响,也会影响这些阶段。我们相信,当工程师了解这个周期以及每个团队的工作内容和所需时,他们设计的系统会更好。

在 1.2 节中,我们将从典型系统的示例架构开始讨论深度学习系统设计。本节描述的组件将在各自的章节中进行更详细的探讨。最后,我们将强调开发模型与开发深度学习系统之间的区别。这种区别常常是一个让人困惑的焦点,所以我们想要立即澄清。

在阅读完这个入门章节之后,您将对深度学习的概况有了扎实的理解。您还将能够开始创建自己的深度学习系统设计,并理解现有设计以及如何使用和扩展它们,这样您就不必从头开始构建一切。随着您继续阅读本书,您将看到一切是如何连接和共同作为一个深度学习系统运作的。

术语

在我们继续本章(以及本书的其余部分)之前,让我们定义和澄清本书中始终使用的一些术语。

深度学习与机器学习的比较

深度学习是机器学习的一种,但被认为是机器学习的一种演变。机器学习按定义是人工智能的一种应用,包括解析数据、从数据中学习,然后应用所学内容做出明智决策的算法。深度学习是机器学习的一种特殊形式,它使用可编程神经网络作为算法,从数据中学习并做出准确的决策。

尽管本书主要关注于教授如何构建系统或基础设施来促进深度学习开发(所有示例都是神经网络算法),但我们讨论的设计和项目开发概念在机器学习中也适用。因此,在本书中,我们有时将术语深度学习机器学习互换使用。例如,本章介绍的深度学习开发周期和第二章介绍的数据管理服务也适用于机器学习上下文。

深度学习用例

深度学习用例是指利用深度学习技术解决问题的场景,换句话说,是您想使用深度学习解决的问题。例如:

  • 聊天机器人—用户可以在客户支持网站上与虚拟代理进行基于文本的对话。虚拟代理使用深度学习模型理解用户输入的句子,并像真正的人类一样与用户进行对话。

  • 自动驾驶汽车—驾驶员可以将汽车置于辅助驾驶模式,根据道路标线自动转向。车载多个摄像头捕捉标线,利用基于深度学习的计算机视觉技术形成对道路的感知。

模型、预测和推断,以及模型服务

这三个术语的描述如下:

  • 模型—深度学习模型可以被视为包含算法(模型架构)和进行预测所需数据的可执行程序。

  • 预测和推断—模型预测和模型推断都是指使用给定数据执行模型以获得一组输出。由于在模型服务的上下文中广泛使用预测和推断,它们在本书中可以互换使用。

  • 模型服务(预测服务)—本书将模型服务描述为在 Web 应用程序(在云端或本地)中托管机器学习模型,并允许深度学习应用程序通过 API 将模型功能集成到其系统中。模型服务 Web 程序通常称为预测服务或模型服务。

深度学习应用

深度学习应用是利用深度学习技术解决问题的软件。它通常不执行任何计算密集型任务,例如数据处理、深度学习模型训练和模型服务(除了在边缘托管模型,例如自动驾驶汽车)。例如:

  • 提供 UI 或 API 以接受用户的自然句子作为输入,解释它们,采取行动并向用户提供有意义的响应的聊天机器人应用程序。基于深度学习系统中计算的模型输出(来自模型服务),聊天机器人做出响应并采取行动。

  • 自动驾驶软件从多个传感器接收输入,如视频摄像头、接近传感器和激光雷达,借助深度学习模型形成对汽车周围环境的感知,并相应地驾驶汽车。

平台 vs. 系统 vs. 基础设施

在本书中,术语 深度学习平台深度学习系统深度学习基础设施 具有相同的含义:为高效构建深度学习应用程序提供所有必要支持的基础系统。我们倾向于最常使用 系统,但在本书的上下文中,所有三个术语都具有相同的含义。

现在我们对术语有了共识,让我们开始吧!

1.1 深度学习开发周期

正如我们所说,深度学习系统是深度学习 项目开发 高效进行所必需的基础设施。因此,在深入探讨深度学习系统的结构之前,审视一下深度学习系统所启用的开发范式是明智的。我们称这个范式为 深度学习开发周期

你可能会想,在一本技术书中,为什么我们要强调像产品开发这样与技术无关的东西。事实上,大多数深度学习工作的目标最终是将产品或服务推向市场。然而,许多工程师并不熟悉产品开发的其他阶段,就像许多产品开发者不了解工程或建模一样。从我们构建深度学习系统的经验中,我们已经了解到,说服公司中多个角色的人员采用系统主要取决于该系统是否实际上能解决他们的特定问题。我们相信,概述深度学习开发周期中的各个阶段和角色有助于表达、解决、沟通和最终解决每个人的痛点。

了解这一周期也能解决其他一些问题。在过去的十年里,许多新的深度学习软件包已经被开发出来,以解决不同的领域。其中一些处理模型训练和服务,而另一些处理模型性能跟踪和实验。数据科学家和工程师每次需要解决特定应用程序或用例时都会组合这些工具;这被称为 MLOps(机器学习运维)。随着这些应用程序数量的增长,为新的应用程序每次从头开始组合这些工具变得重复和耗时。同时,随着这些应用程序的重要性增长,对其质量的期望也在增加。这两个问题都需要一种一致的方法来快速可靠地开发和交付深度学习功能。这种一致的方法始于所有人都在同一深度学习开发范式或周期下工作。

深度学习系统如何适应深度学习周期?一个良好构建的深度学习系统应该支持产品开发周期,并使执行周期变得轻松、快速和可靠。理想情况下,数据科学家可以使用深度学习系统作为基础设施完成整个深度学习周期,而无需学习底层复杂系统的所有工程细节。

因为每个产品和组织都是独特的,对系统构建者来说,理解各种角色的独特需求以构建成功的系统至关重要。所谓的“成功”,是指帮助利益相关者高效协作,快速交付深度学习特性的系统。在本书中,当我们讨论深度学习系统的设计原则,并查看每个组件的工作方式时,你对利益相关者需求的理解将帮助你调整这些知识,形成自己的系统设计。在讨论技术细节时,我们将指出在设计系统时需要注意某些类型的利益相关者。深度学习开发周期将作为指导框架,帮助我们考虑深度学习系统的每个组件的设计要求。

让我们从一张图片开始。图 1.1 展示了典型周期的样貌。它展示了机器学习(特别是深度学习)的开发进度逐个阶段的过程。正如你所见,跨职能协作几乎在每一步都发生。我们将在接下来的两个部分讨论此图中涉及的每个阶段和角色。

图 1.1 将深度学习从研究带入产品的典型场景。我们称之为深度学习开发周期

1.1.1 深度学习产品开发周期中的阶段

深度学习开发周期通常从一个业务机会开始,并由产品计划及其管理驱动。之后,周期通常经历四个阶段:数据探索、原型制作、产品化(投入生产)和应用集成。让我们逐一查看这些阶段。然后我们将查看所有涉及的角色(在图 1.1 中以人物图标表示)。

注意:每个后续小节旁边括号中的数字与图 1.1 中的相同数字对应。

产品启动(1)

首先,业务利益相关者(产品所有者或项目经理)分析业务,并确定可以通过机器学习解决的潜在业务机会或问题。

数据探索(2)

当数据科学家清楚了解业务需求时,他们开始与数据工程师合作,尽可能收集、标记数据并构建数据集。数据收集可以包括搜索公开可用数据和探索内部来源。数据清理也可能会发生。数据标记可以外包或在内部执行。

与以下阶段相比,数据探索的早期阶段是非结构化的,通常是随意进行的。它可能是一个 Python 脚本或 shell 脚本,甚至是数据的手动复制。数据科学家经常使用基于 Web 的数据分析应用程序,例如 Jupyter Notebook(开源;jupyter.org)、Amazon SageMaker Data Wrangler(aws.amazon.com/sagemaker/data-wrangler)和 Databricks(www.databricks.com)来分析数据。不需要构建正式的数据收集管道。

数据探索不仅重要,而且对深度学习项目的成功至关重要。可用的相关数据越多,建立有效和高效深度学习模型的可能性就越高。

研究和原型设计(3, 4)

原型设计的目标是找到最可行的算法/方法,以解决给定数据的业务需求(来自产品所有者)。在此阶段,数据科学家可以与 AI 研究人员合作,提出并评估来自前期数据探索阶段构建的不同训练算法。数据科学家通常在此阶段尝试多种想法,并构建概念验证(POC)模型来评估它们。

尽管新发布的算法通常会受到考虑,但大多数算法都不会被采纳。算法的准确性不是唯一要考虑的因素;在评估算法时还必须考虑计算资源需求、数据量和算法实现成本。最实用的方法通常是获胜者。

请注意,由于资源限制,研究人员并不总是参与原型设计阶段。经常情况下,数据科学家既做研究工作,又构建 POC。

您还可能注意到,在图 1.1 中,大型开发周期中有一个内部循环(循环 A):产品启动 > 数据探索 > 深度学习研究 > 原型设计 > 模型 > 产品启动。该循环的目的是通过构建 POC 模型在早期阶段获得产品反馈。我们可能会多次执行此循环,直到所有利益相关者(数据科学家、产品所有者)就将用于满足业务需求的算法和数据达成一致意见。

多次痛苦的教训最终教会了我们,在开始昂贵的生产过程——构建生产数据和训练管道以及托管模型之前,我们必须与产品团队或客户(甚至更好)审查解决方案。深度学习项目的目的与任何其他软件开发项目并无不同:解决业务需求。在早期阶段与产品团队审查方法将防止在后期重新制定方法的昂贵和令人沮丧的过程。

生产化(也称为 MLOps)(5)

生产化,也称为“投入生产”,是使产品具备生产价值并准备好被用户消费的过程。生产价值通常定义为能够服务客户请求,承受一定程度的请求负载,并优雅地处理诸如格式错误的输入和请求超载等不利情况。生产价值还包括后期工作,如持续的模型指标监控和评估、反馈收集和模型重新训练。

生产化是开发周期中最需要工程投入的部分,因为我们将把原型实验转化为严肃的生产流程。生产化的非详尽待办事项列表可以包括

  • 建立一个数据管道,重复从不同的数据源中提取数据,并使数据集版本化和更新。

  • 建立数据管道对数据集进行预处理,例如数据增强或增强和与外部标记工具集成。

  • 重构和将原型代码 docker 化为生产质量的模型训练代码。

  • 通过版本控制和跟踪输入和输出使训练和服务代码的结果可再现。例如,我们可以使训练代码报告训练元数据(训练日期和时间、持续时间、超参数)和模型元数据(性能指标、使用的数据和代码),以确保对每次模型训练运行的完全可追溯性。

  • 设置持续集成(Jenkins、GitLab CI)和持续部署以自动化代码构建、验证和部署。

  • 建立连续的模型训练和评估管道,以便模型训练可以自动使用最新的数据集,并以可重复、可审计和可靠的方式生成模型。

  • 建立一个模型部署管道,自动发布通过质量门的模型,以便模型服务组件可以访问它们;根据业务需求可以执行async或实时模型预测。模型服务组件托管模型并通过 Web API 公开它。

  • 建立持续监控管道,定期评估数据集、模型和模型服务性能,以检测数据集的潜在特征漂移(数据分布变化)或模型性能下降(概念漂移)并警告开发人员或重新训练模型。

如今,生产化步骤有一个新的热门别名:MLOps(机器学习运营),这是一个模糊的术语,对研究人员和专业人员的定义模糊不清。我们解释 MLOps 的含义是弥合模型开发(实验)和生产环境运营(Ops)之间的鸿沟,以促进机器学习项目的生产化。例如,简化将机器学习模型推向生产的过程,然后对其进行监视和维护。

MLOps 是一种根植于 DevOps 原则的范式,应用了类似的原则到软件开发中。它利用了三个学科:机器学习、软件工程(特别是运维)和数据工程。查看图 1.2,了解通过 MLOps 视角看深度学习。

图 1.2 MLOps 在深度学习的产品化阶段应用了 DevOps 方法,当模型被推向生产时。(来源:Machine Learning Engineering in Action,作者 Ben Wilson,Manning 出版社,2022 年,图 2.7)

因为这本书是关于构建支持 ML 运营的机器学习系统,所以我们不会详细介绍图 1.2 中所示的实践。但是,正如你所看到的,支持将机器学习模型开发到生产环境中的工程工作量是巨大的。与数据科学家在数据探索和模型原型阶段所做的工作相比,工具(软件)、工程标准和流程已经发生了巨大变化,并变得更加复杂。

为什么将模型部署到生产环境很困难?

庞大的基础设施(工具、服务、服务器)和团队间的密集合作是将模型部署到生产环境的两个最大障碍。这个关于产品化(又称 MLOps)的部分建立在一个事实上,即数据科学家需要与数据工程师、平台开发人员、DevOps 工程师和机器学习工程师一起工作,并且要了解庞大的基础设施(深度学习系统),才能将算法/模型从原型推向生产。难怪产品化模型需要花费如此多的时间。

为了解决这些挑战,我们需要在设计和构建深度学习系统时,将复杂性从数据科学家那里抽象出来。就像建造汽车一样,我们希望让数据科学家坐在驾驶座上,但不要求他们对汽车本身了解太多。

现在,回到开发周期,你可能会注意到图 1.1 中还有一个另一个内部循环(循环 B),从产品化(方框 5)到模型到产品启动(方框 1)。这是在我们将模型推理与 AI 应用集成之前与产品团队进行的第二次审查。

我们的第二次审查(循环 B)在原型和生产之间比较模型和数据。我们要确保模型性能和可扩展性(例如,模型服务容量)符合业务需求。

注意:以下两篇论文是推荐的;如果你想了解更多关于 MLOps 的内容,它们是很好的起点:“Operationalizing Machine Learning: An Interview Study”(arXiv:2209.09125)和“Machine Learning Operations (MLOps): Overview, Definition, and Architecture”(arXiv:2205.02302)。

应用集成(6)

产品开发周期的最后一步是将模型预测集成到 AI 应用中。常见的模式是将模型托管在深度学习系统的模型服务服务中,并通过互联网发送模型预测请求将业务应用逻辑与模型集成。

作为一个示例用户场景,一个聊天机器人用户通过键入或发声问题与聊天机器人用户界面进行交互。当聊天机器人应用程序接收到来自客户的输入时,它调用远程模型服务服务来运行模型预测,然后根据模型预测结果采取行动或回应客户。

除了将模型服务与应用逻辑集成外,此阶段还涉及评估对产品重要的指标,如点击率和流失率。良好的 ML 特定指标(良好的精确度-召回率曲线)并不总是能保证满足业务需求。因此,业务利益相关者通常在此阶段进行客户访谈和产品指标评估。

1.1.2 开发周期中的角色

因为您现在对典型开发周期中的每个步骤有了清晰的了解,让我们来看看在这个周期中合作的关键角色。每个角色的定义、职称和职责可能因组织而异。所以确保您澄清了您的组织中谁做什么,并相应调整您系统的设计。

业务利益相关者(产品所有者)

许多组织将利益相关者角色分配给多个职位,如产品经理、工程经理和高级开发人员。业务利益相关者定义产品的业务目标,并负责产品开发周期的沟通和执行。以下是他们的责任:

  • 从深度学习研究中获得灵感,讨论在产品中应用深度学习特性的潜在应用,并驱动推动模型开发的产品需求

  • 拥有产品!与客户沟通,确保工程解决方案符合业务需求并产生结果

  • 协调不同角色和团队之间的跨职能协作

  • 运行项目开发执行;在整个开发周期内提供指导或反馈,以确保深度学习特性为产品的客户提供真正的价值

  • 评估产品指标(如用户流失率和功能使用情况)—而不是模型指标(精度或准确性)—并推动模型开发、产品化或产品集成的改进

研究人员

机器学习研究人员研究和开发新颖的神经网络架构。他们还开发提高模型准确性和训练模型效率的技术。这些架构和技术可以在模型开发过程中使用。

注:机器学习研究员角色通常与 Google、Microsoft 和 Salesforce 等大型科技公司相关联。在许多其他公司,数据科学家扮演相同的角色。

数据科学家

数据科学家可能会扮演研究员的角色,但大多数情况下,他们会将业务问题转化为机器学习问题,并使用机器学习方法来实现。数据科学家受产品需求的驱动,并将研究技术应用于生产数据,而不是标准基准数据集。除了研究模型算法外,数据科学家的职责还可能包括

  • 将不同研究中的多个深度学习神经网络架构和/或技术结合到一个解决方案中。有时,他们除了纯深度学习外还应用其他机器学习技术。

  • 探索可用数据,确定哪些数据是有用的,并决定如何在供训练之前对其进行预处理。

  • 原型不同方法(编写实验性代码)来解决业务问题。

  • 将模型原型代码转换为生产代码,并进行工作流自动化。

  • 遵循工程流程,通过使用深度学习系统将模型部署到生产环境。

  • 根据需要迭代可能有助于模型开发的任何额外数据。

  • 在生产环境中持续监控和评估数据和模型性能。

  • 排查与模型相关的问题,如模型退化。

数据工程师

数据工程师帮助收集数据,并建立连续数据摄入和处理的数据管道,包括数据转换、丰富和标记。

MLOps 工程师/ML 工程师

MLOps 工程师在多个领域扮演多种角色,包括数据工程师、DevOps(运维)工程师、数据科学家和平台工程师。除了设置和运行机器学习基础设施(系统和硬件),他们还管理自动化管道以创建数据集并训练和部署模型。MLOps 工程师还监控 ML 基础设施和用户活动,如训练和服务。

如你所见,MLOps 很困难,因为它需要人们掌握一套跨越软件开发、运维、维护和机器学习开发的实践方法。MLOps 工程师的目标是确保机器学习模型的创建、部署、监控和维护高效可靠。

深度学习系统/平台工程师

深度学习系统工程师构建和维护机器学习基础设施的主要组件——本书的主要关注点——以支持数据科学家、数据工程师、MLOps 工程师和 AI 应用的所有机器学习开发活动。机器学习系统的组成部分包括数据仓库、计算平台、工作流编排服务、模型元数据和工件存储、模型训练服务、模型服务等。

应用工程师

应用工程师构建面向客户的应用程序(前端和后端),以满足给定的业务需求。应用程序逻辑将根据给定客户请求的模型预测做出决策或采取行动。

注意:未来,随着机器学习系统(基础设施)的成熟,深度学习开发周期中涉及的角色将合并为越来越少。最终,数据科学家将能够独自完成整个周期。

1.1.3 深度学习开发周期步骤详解

通过给出一个例子,我们可以以更具体的方式展示角色和过程。假设你被分配了构建一个关于公司产品线自动回答问题的客户支持系统的任务。以下步骤将指导您完成将该产品推向市场的过程:

  1. 产品要求是构建一个客户支持应用程序,提供一个菜单,让客户可以浏览以找到常见问题的答案。随着问题数量的增加,菜单变得越来越大,有许多层次的导航。分析显示,许多客户在尝试找到答案时对导航系统感到困惑,并放弃了浏览菜单。

  2. 拥有该产品的产品经理(PM)受到改善用户保留率和体验(快速找到答案)的动机。在与客户进行了一些研究后,产品经理发现,大多数客户希望在不复杂的菜单系统中获得答案,最好是像在他们的自然语言中提问一样简单。

  3. 产品经理联系机器学习研究人员寻求潜在解决方案。结果表明,深度学习可能会有所帮助。专家认为这项技术已经足够成熟,可以用于这个用例,并建议几种基于深度学习模型的方法。

  4. 产品经理编写产品规格,指示应用程序一次从客户那里接收一个问题,从问题中识别意图,并与相关答案匹配。

  5. 数据科学家收到产品需求并开始原型化符合需求的深度学习模型。他们首先开始数据探索,收集可用的训练数据,并与研究人员商讨算法的选择。然后数据科学家开始编写原型代码以生成实验模型。最终,他们得到了一些数据集、几种训练算法和多个模型。经过仔细评估,从各种实验中选择了一个自然语言处理模型。

  6. 然后,项目经理组建了一个平台工程师、MLOps 工程师和数据工程师团队,与数据科学家一起工作,将在第 5 步中创建的原型代码投入生产。这项工作包括构建连续的数据处理管道和连续的模型训练、部署和评估管道,以及设置模型服务功能。项目经理还确定了每秒预测次数和所需的延迟。

  7. 一旦生产设置完成,应用工程师将客户支持服务的后端与模型服务服务(在第 6 步中构建)集成起来,因此当用户输入问题时,服务将根据模型预测返回答案。项目经理还定义了产品指标,例如平均查找答案所花费的时间,以评估最终结果,并将其用于推动下一轮改进。

1.1.4 项目开发的扩展

正如您在 1.1.2 节中所看到的,我们需要填补七种不同的角色才能完成一个深度学习项目。这些角色之间的跨职能协作几乎在每一个步骤都会发生。例如,数据工程师、平台开发人员和数据科学家共同致力于将项目投入生产。任何参与过需要许多利益相关方的项目的人都知道,为了推动这样一个项目前进,需要多少沟通和协调。

这些挑战使得深度学习开发难以扩展,因为我们要么没有资源来填补所有所需角色,要么由于沟通成本和减速而无法满足产品时间表。为了减少巨大的运营工作量、沟通和跨团队协调成本,公司正在投资于机器学习基础设施,并减少构建机器学习项目所需的人员数量和知识范围。深度学习基础设施堆栈的目标不仅是自动化模型构建和数据处理,还要使技术角色合并为可能,使数据科学家能够在项目中独立地处理所有这些功能。

深度学习系统的一个关键成功指标是看模型投产过程能否顺利进行。有了良好的基础设施,不会期望数据科学家突然成为专家级的 DevOps 或数据工程师,他们应该能够以可扩展的方式实现模型、建立数据管道,并独立地在生产环境中部署和监控模型。

通过使用高效的深度学习系统,数据科学家将能够以最小的额外开销完成开发周期——需要较少的沟通和等待他人的时间,并专注于最重要的数据科学任务,如理解数据和尝试算法。扩展深度学习项目开发能力是深度学习系统的真正价值所在。

1.2 深度学习系统设计概述

在考虑到第 1.1 节的背景下,让我们深入了解本书的重点:深度学习系统本身。设计一个系统——任何系统——都是在一组对你的情况独特的约束条件下实现目标的艺术。深度学习系统也不例外。例如,假设你有几个需要同时提供服务的深度学习模型,但是你的预算不允许你运行一台具有足够内存同时容纳所有模型的机器。你可能需要设计一个缓存机制来在内存和磁盘之间交换模型。然而,交换会增加推断延迟。这种解决方案是否可行将取决于延迟要求。另一个可能性是为每个模型运行多个较小的机器,如果你的模型大小和预算允许的话。

或者,举个例子,想象一下你公司的产品必须符合某些认证标准。它可能会规定数据访问政策,对希望访问公司产品收集的数据的任何人施加重大限制。你可能需要设计一个框架,以符合标准地允许数据访问,以便研究人员、数据科学家和数据工程师可以在你的深度学习系统中解决问题并开发需要这种数据访问的新模型。

正如你所看到的,有许多可以调整的旋钮。达到尽可能满足多个要求的设计肯定是一个迭代的过程。但为了缩短迭代过程,最好从尽可能接近最终状态的设计开始。

在本节中,我们首先提出了一个仅具有基本组件的深度学习系统设计,然后解释了每个组件的责任和用户工作流程。根据我们设计和定制深度学习系统的经验,几个关键组件在不同的设计中是共同的。我们认为它们可以作为你设计的合理起点。我们称之为 参考系统架构

你可以为你的设计项目制作一份此参考副本,列出你的目标和约束条件,然后开始识别每个组件中可以根据需要调整的旋钮。因为这不是一个权威的体系结构,所以你还应该评估是否所有组件都真的是必需的,并根据需要添加或删除组件。

1.2.1 参考系统架构

图 1.3 显示了参考深度学习系统的高层概述。深度学习系统有两个主要部分。第一个是系统的应用程序编程接口(API;盒子 A),位于图表中间。第二个是深度学习系统的组件集合,由所有矩形框表示,位于大框内,用虚线轮廓标出,占据图表的下半部分。这些框每个代表一个系统组件:

  • API(框 A)

  • 数据集管理器(框 B)

  • 模型训练器(框 C)

  • 模型服务(框 D)

  • 元数据和工件存储(框 E)

  • 工作流编排(框 F)

  • 交互式数据科学环境(框 G)

在本书中,我们假设这些系统组件是微服务

图 1.3 典型深度学习系统的概览,包括支持深度学习开发周期的基本组件。这个参考架构可以作为一个起点,进行进一步的定制。在后面的章节中,我们将详细讨论每个组件,并解释它们如何融入这一大局。

定义:对于微服务,没有单一的定义。在这里,我们将使用该术语来指代使用 HTTP 或 gRPC 协议与网络通信的进程。

这一假设意味着我们可以合理地期望这些组件能够安全地支持具有不同角色的多个用户,并且可以方便地通过网络或互联网访问。(然而,本书将不涵盖微服务的所有工程方面的设计或构建。我们将重点讨论与深度学习系统相关的具体内容。)

注意:你可能会想知道,你是否需要自己设计、构建和托管所有深度学习系统组件。实际上,有开源(Kubeflow)和托管的替代方案(Amazon SageMaker)可供选择。我们希望在你学习了每个组件的基本知识、它们如何融入整体架构以及不同角色如何使用后,你能为你的使用场景做出最佳决策。

1.2.2 关键组件

现在让我们详细介绍我们认为对基本深度学习系统至关重要的关键组件,如图 1.3 所示。你可能希望根据自己的需求添加其他组件或进一步简化。

应用程序编程接口

我们深度学习系统的入口点(图 1.3 中的框 A)是一个通过网络访问的 API。我们选择 API 是因为系统不仅需要支持研究人员、数据科学家、数据工程师等使用的图形用户界面,还需要支持应用程序和可能来自合作伙伴组织的数据仓库等其他系统。

虽然在概念上 API 是系统的唯一入口点,但完全有可能将 API 定义为每个组件提供的所有 API 的总和,而没有额外的层将所有内容聚合在单一服务端点下。在本书中,我们将直接使用每个组件提供的所有 API 的总和,并跳过聚合以简化问题。

注意您应该使用集中式还是分布式深度学习系统 API?在参考架构(图 1.3)中,深度学习系统 API 显示为一个单独的框。应该将其解释为深度学习系统 API 的完整集合的逻辑容器,无论它是在单个(例如,代理所有组件的 API 网关)还是多个服务端点(直接与每个组件交互)上实现的。每种实现都有其优点和缺点,您应该与团队合作找出哪种方法最有效。如果从一个小的用例和团队开始,直接与每个组件交互可能会更容易。

数据集管理器

深度学习是基于数据的。毫无疑问,数据管理组件是深度学习系统的核心组成部分。每个学习系统都是垃圾进,垃圾出的系统,因此确保良好的数据质量对于学习至关重要。良好的数据管理组件应该提供解决此问题的解决方案。它使收集、组织、描述和存储数据成为可能,从而使数据可以被探索、标记和用于训练模型。

在图 1.3 中,我们至少可以看到数据管理器(盒子 B)与其他方面的四种关系:

  • 数据收集器将原始数据推送到数据集管理器以创建或更新数据集。

  • 工作流编排服务(盒子 F)执行数据处理管道,从数据管理器中提取数据以增强训练数据集或转换数据格式,并将结果推送回去。

  • 数据科学家、研究人员和数据工程师使用 Jupyter Notebook(盒子 G)从数据管理器中提取数据进行数据探索和检查。

  • 模型训练服务(盒子 C)从数据管理器中提取训练数据进行模型训练。

在第二章中,我们将深入讨论数据集管理。在整本书中,我们使用术语数据集作为可能相关的收集数据的单位。

模型训练器

模型训练器(又称模型训练服务;盒子 C)响应以提供基础计算资源,如 CPU、RAM 和 GPU,并提供作业管理逻辑来运行模型训练代码并生成模型文件。在图 1.3 中,我们可以看到工作流编排服务(盒子 F)告诉模型训练器执行模型训练代码。训练器从数据集管理器(盒子 B)获取输入训练数据并生成模型。然后,它将模型与训练指标和元数据一起上传到元数据和工件存储(盒子 E)中。

通常需要对大型数据集进行密集计算,以产生可以进行准确预测的高质量深度学习模型。采用新的算法和训练库/框架也是关键要求。这些要求在几个层面上产生挑战:

  • 减少模型训练时间的能力——尽管训练数据的规模和模型架构的复杂性不断增长,但训练系统必须保持训练时间合理。

  • 水平扩展性——一个有效的生产训练系统应该能够同时支持来自不同应用程序和用户的多个训练请求。

  • 采用新技术的成本——深度学习社区充满活力,不断更新和改进算法和工具(SDK、框架)。训练系统应该足够灵活,能够轻松地适应新的创新,而不会干扰现有的工作负载。

在第三章中,我们将研究解决上述问题的不同方法。我们不会在本书中深入探讨训练算法的理论方面,因为它们不会影响我们如何设计系统。在第四章中,我们将研究如何分发训练以加速该过程。在第五章中,我们将探讨几种不同的方法来优化训练超参数。

模型服务

模型可以在各种设置中使用,例如用于实时预测的在线推理或用于批量预测的离线推理,使用大量的输入数据。这就是模型服务的地方—当系统托管模型、接受输入预测请求、运行模型预测并将预测返回给用户时。有几个关键问题需要回答:

  • 您的推理请求是来自网络?还是来自需要本地服务的传感器?

  • 什么是可接受的延迟?推理请求是临时的还是流式的?

  • 有多少模型正在提供服务?每个模型是单独提供某种推理请求,还是一组模型这样做?

  • 模型的大小有多大?您需要预算多少内存容量?

  • 需要支持哪些模型架构?是否需要 GPU?您需要多少计算资源才能生成推断?是否有其他支持服务的组件—例如嵌入、归一化、聚合等?

  • 是否有足够的资源保持模型在线?还是需要一种置换策略(例如在内存和磁盘之间移动模型)?

从图 1.3 中,模型服务的主要输入和输出(方框 D)分别是推理请求和返回的预测。为了生成推理,模型是从元数据和工件存储中检索出来的(方框 E)。一些请求及其响应可能会被记录并发送到模型监控和评估服务(图 1.3 中未显示或本书未涉及),该服务从这些数据中检测异常并生成警报。在第六章和第七章中,我们将深入探讨模型服务架构,探讨这些关键方面,并讨论它们的解决方案。

元数据和工件存储

想象一下作为一个人的团队在一个简单的深度学习应用上工作,你只需处理几个数据集并训练并部署一种类型的模型。你可能可以追踪数据集、训练代码、模型、推理代码和推理之间的关系。这些关系对于模型开发和故障排除至关重要,因为你需要能够将某些观察追溯到原因。

现在想象增加更多应用、更多人员和更多模型类型。这些关系的数量将呈指数级增长。在一个为多种用户服务的深度学习系统中,这些用户在不同阶段处理多个数据集、代码和模型,存在对一个跟踪关系网络的组件的需求。深度学习系统中的元数据和工件存储正是为此而设计。工件包括训练模型和生成推理的代码,以及任何生成的数据,如训练模型、推理和指标。元数据是描述工件或工件之间关系的任何数据。一些具体的例子是

  • 训练代码的作者和版本

  • 经过训练的模型的输入训练数据集和训练环境的参考

  • 经过训练的模型的训练指标,例如训练日期和时间、持续时间以及训练任务的所有者

  • 特定于模型的指标,如模型版本、模型血统(训练中使用的数据和代码)以及性能指标

  • 用于生成某一推断的模型、请求和推理代码

  • 工作流历史,跟踪模型训练和数据处理流水线的每个步骤

这些只是基准元数据和工件存储可以帮助跟踪的一些例子。你应该根据你的团队或组织的需求来定制这个组件。

图 1.3 中生成元数据和工件的每一个其他组件都会流入元数据和工件存储(箱体 E)。该存储在模型服务中也扮演着重要角色,因为它提供模型文件及其元数据给模型服务服务(箱体 D)。虽然图中未显示,但通常在用户界面层构建自定义工具来追踪血统和故障排除,这些工具由元数据和工件存储提供动力。

当我们在第八章进行时,我们将会查看一个基准元数据和工件存储。这个存储通常是深度学习系统用户界面的核心组件。

工作流协调

工作流编排(图 1.3,框 F)在许多系统中是无处不在的,它有助于根据编程条件自动启动计算任务。在机器学习系统的上下文中,工作流编排是所有在深度学习系统内运行的自动化的驱动力。它允许人们定义工作流程或管道—有向无环图(DAGs)—将单个任务以执行顺序粘合在一起。工作流编排组件编排这些工作流的任务执行。一些典型的示例是

  • 在构建新数据集时启动模型训练

  • 监控上游数据源,增补新数据,转换其格式,通知外部标记者,并将新数据合并到现有数据集中

  • 如果通过了一些可接受的标准,则将训练好的模型部署到模型服务器

  • 持续监控模型性能指标并在检测到性能下降时提醒开发人员

您将学习如何在第九章中构建或设置工作流编排系统。

交互式数据科学环境

由于合规性和安全性原因,无法从生产环境将客户数据和模型下载到本地工作站。为了让数据科学家交互式地探索数据,在工作流编排中排查管道执行问题以及调试模型,需要一个位于深度学习系统内的远程交互式数据科学环境(图 1.3,框 G)。

公司通常会使用开源 Jupyter Notebooks (jupyter.org/) 或利用云供应商的基于 JupyterLab 的解决方案,如 Amazon SageMaker Studio (aws.amazon.com/sagemaker/studio/) 来建立自己的可信数据科学环境。

典型的交互式数据科学环境应提供以下功能:

  • 数据探索—为数据科学家提供对客户数据的便捷访问,但保持其安全和合规性;没有数据泄漏,并且任何未经授权的数据访问将被拒绝。

  • 模型原型—为数据科学家提供了必要的工具,可以在深度学习系统内快速开发 POC 模型。

  • 故障排除—使工程师能够调试发生在深度学习系统内的任何活动,例如下载模型并对其行为进行分析,或者检查失败管道中的所有输入/输出工件(中间数据集或配置)。

1.2.3 主要用户场景

为了更好地理解深度学习系统在开发周期中的使用方式(图 1.1),我们准备了说明它们如何被使用的示例场景。让我们从编程消费者开始,如图 1.4 所示。将数据推送到系统的数据收集器通常会通过 API 最终到达数据管理服务,该服务收集和组织原始数据用于模型训练。

图 1.4 数据从来源或收集器推送,通过 API 到数据管理服务,数据在那里进一步组织和存储为更适合模型训练的格式。

深度学习应用通常会访问模型推断服务,从训练模型中获取推理结果,这些结果被用于支持最终用户消费的深度学习功能。图 1.5 显示了这种交互的顺序。脚本,甚至是完整的管理服务,也可以是程序化的消费者。因为它们是可选的,我们简化了图表,没有将它们包括在内。

图 1.5 深度学习应用通过 API 请求推理。模型推断服务接受并处理对训练模型的请求,并产生返回给应用程序的推理结果。

人类消费者和 API 之间通常还有一个额外的层——用户界面。界面可以是基于 web 的,也可以是基于命令行的。一些高级用户甚至可以跳过这个界面直接使用 API。让我们逐个角色讨论一下。

研究人员使用系统的典型场景如图 1.6 所示。研究人员可以查找可用数据来尝试他们的新建模技术。他们访问用户界面,并访问数据探索和可视化部分,从数据管理服务中提取数据。可能会涉及大量手动数据处理,将其处理成可以被新的训练技术使用的形式。一旦研究人员确定了一种技术,他们可以将其打包为库供其他人使用。

图 1.6 研究人员使用场景序列,他对查看可用于研究和开发新建模技术的数据感兴趣。研究人员与 API 和幕后的数据管理支持的用户界面进行交互。

数据科学家和工程师可以通过首先查看可用数据来研究用例,类似于上一段中研究人员最初要做的事情。这将得到数据管理服务的支持。他们做出假设,并将数据处理和训练技术组合成代码。这些步骤可以结合成一个工作流,使用工作流管理服务。

当工作流管理服务执行工作流的运行时,它联系数据管理服务和模型训练服务来执行实际任务并跟踪它们的进展。每个服务和训练代码都将超参数、代码版本、模型训练度量和测试结果存储到元数据和工件存储中。

通过用户界面,数据科学家和工程师可以比较实验运行并推断出训练模型的最佳方法。前述场景如图 1.7 所示。

图 1.7 数据科学家定义模型训练工作流程、运行它并审查结果的使用序列

产品经理还可以通过用户界面查看和查询整个系统的各种指标。指标数据可以由元数据和工件存储提供。

1.2.4 推导您自己的设计

现在,我们已经讨论了参考系统架构的所有方面,让我们讨论一些定制您自己版本的指南。

收集目标和需求

设计任何成功系统设计的第一步是具有一组清晰的目标和要求,以便进行工作。这些理想情况下应该来自您系统的用户,直接或间接通过产品管理团队或工程管理团队。这个简短的目标和要求清单将帮助您形成您的系统将会是什么样子的愿景。这个愿景,反过来又应该是您在系统设计和开发阶段的指导方针。

注意 有时工程师被要求开发一个支持一个或多个已经存在的深度学习应用程序的系统。在这种情况下,您可以首先确定这些应用程序中的一组共同需求,以及您的系统如何设计来快速为这些应用程序带来创新。

要收集系统的目标和需求,您需要确定系统的不同类型的用户和利益相关者,或者系统的人物角色。(这是一个通用概念,可以应用于大多数系统设计问题。)毕竟,是用户将帮助您阐明系统的目标和需求。

我们的建议是,如果您不确定一个好的起点,请从用例或应用需求开始。以下是一些示例问题,您可以向用户提出:

  • 给数据工程师和产品经理—系统是否允许应用程序收集用于训练的数据?系统是否需要处理流输入数据?正在收集多少数据?

  • 给数据科学家和工程师—我们如何处理和标记数据?系统是否需要为外部供应商提供标注工具?我们如何评估模型?我们如何处理测试数据集?数据科学工作是否需要交互式笔记本用户界面?

  • 给研究人员和数据科学家—模型训练需要多大量的数据?模型训练的平均时间是多少?研究和数据科学需要多少计算和数据容量?系统应该支持哪些实验?需要收集哪些元数据和指标来评估不同的实验?

  • 给产品经理和软件工程师—模型服务是在远程服务器上完成还是在客户端上完成的?它是实时模型推断还是脱机批量预测?是否有延迟要求?

  • 对产品经理 ——我们在组织中试图解决什么问题?我们的商业模式是什么?我们将如何评估我们的实施效果?

  • 对安全团队 ——您的系统需要什么级别的安全性?数据访问是完全开放还是严格限制/隔离?是否有审计要求?是否有一定级别的合规性或认证(例如,通用数据保护条例,系统和组织控制 2 等)需要系统达到?

定制参考架构

设计要求和范围明确后,我们可以开始定制图 1.3 中的参考架构。首先,我们可以决定是否需要添加或删除任何组件。例如,如果需求仅仅是在远程服务器群中管理模型训练,我们可以删除工作流管理组件。如果数据科学家想要有效评估生产数据的模型性能,他们也可以添加一个实验管理组件。这个组件允许数据科学家使用系统中已经存在的全量数据进行训练和验证,并对生产流量进行在线 A/B 测试,使用以前未见过的数据。

第二步是根据您的特定需求设计和实现每个关键组件套件。根据要求,您可能会从数据集管理服务中排除数据流 API,并添加分布式训练支持,如果训练速度是一个问题的话。您可以从头开始构建每个关键组件,也可以使用开源软件。在本书的其余部分,我们涵盖了每个关键组件的这两种选项,以确保您知道该做什么。

提示 保持系统设计简单和用户友好。创建如此庞大的深度学习系统的目的是提高深度学习开发的生产力,所以请记住这一点。我们希望使数据科学家能够构建高质量的模型,而不需要了解底层系统的运行情况。

1.2.5 在 Kubernetes 之上构建组件

我们已经介绍了一系列实现为服务的关键组件。有了这么多服务,您可能希望在基础架构层面使用一个复杂的系统来管理它们,例如 Kubernetes。

Kubernetes 是一个用于自动化部署、扩展和管理容器化应用程序的开源系统,这些应用程序在隔离的运行时环境中运行,例如 docker 容器。我们已经看到了一些构建在 Kubernetes 之上的深度学习系统。一些人学习如何使用 Kubernetes,却从未知道为什么要用它来运行深度学习服务,所以我们想解释它背后的思想。如果您熟悉 Kubernetes,请随意跳过本节。

注意 Kubernetes 是一个复杂的平台,需要一本书的篇幅来进行教学,所以我们只讨论它在深度学习系统中的优点。如果你想学习 Kubernetes,我们强烈推荐你阅读 Kubernetes in Action(Manning,2018),作者是 Marko Lukša。

管理计算资源的挑战

在远程服务器上执行一个 Docker 容器似乎是一个简单的任务,但在 30 个不同的服务器上运行 200 个容器就是另外一回事了。存在许多挑战,例如监视所有远程服务器以确定在哪个上运行容器,需要将容器故障转移到健康的服务器,当容器卡住时重新启动容器,跟踪每个容器运行并在完成时收到通知等。为了解决这些挑战,我们必须自己监视硬件、操作系统进程和网络。这不仅在技术上具有挑战性,而且工作量巨大。

Kubernetes 如何帮助

Kubernetes 是一个开源的容器编排平台,用于调度和自动化部署、管理和扩展容器化应用程序。一旦你设置了 Kubernetes 集群,你的服务器组的操作(部署、打补丁、更新)和资源就变得可管理了。这里有一个部署示例:你可以告诉 Kubernetes 运行一个带有 16GB 内存和 1 个 GPU 的 Docker 镜像,Kubernetes 将为你分配资源来运行这个 Docker 镜像。

这对软件开发人员来说是一个巨大的好处,因为并不是每个人都有丰富的硬件和部署经验。通过 Kubernetes,我们只需要声明集群的最终状态,Kubernetes 就会实际完成工作以达到我们的目标。

除了容器部署的好处之外,以下是一些其他关键的 Kubernetes 功能,对于管理我们的训练容器至关重要:

  • 自动缩放功能 — 根据工作负载,Kubernetes 自动调整集群中节点的数量。这意味着如果有突然的用户请求增加,Kubernetes 将自动增加容量,这被称为 弹性计算管理

  • 自愈能力 — 当 Pod 失败或节点死亡时,Kubernetes 会重新启动、替换或重新调度 Pod。它还会终止不响应用户定义的健康检查的 Pod。

  • 资源利用和隔离 — Kubernetes 负责计算资源饱和;它确保每个服务器都得到充分利用。在内部,Kubernetes 在 Pod 中启动应用程序容器。每个 Pod 都是一个带有计算资源保证的隔离环境,并且运行一个功能单元。在 Kubernetes 中,只要多个 Pod 的组合资源需求(CPU、内存、磁盘)不超过节点的限制,多个 Pod 就可以在一个节点(服务器)中,因此服务器可以轻松地被不同的功能单元共享,并保证隔离。

  • 命名空间——Kubernetes 支持将物理集群划分为不同的虚拟集群。这些虚拟集群称为命名空间。您可以为每个命名空间定义资源配额,这使您可以通过将它们分配给不同的命名空间来为不同的团队隔离资源。

另一方面,这些好处是有代价的——它们也会消耗资源。当您运行一个 Kubernetes pod 时,pod 本身会占用一定量的系统资源(CPU、内存)。这些资源是在运行 pod 内部的容器所需资源之上消耗的。在许多情况下,Kubernetes 的开销似乎是合理的;例如,根据 Lally Singh 和 Ashwin Venkatesan(2021 年 2 月)在文章“我们如何将 Kubernetes 的开销最小化在我们的作业系统中”中发表的实验,每个 pod 的 CPU 开销约为每秒 10 毫秒。

注意我们建议您查看附录 B,了解现有深度学习系统与本章介绍的概念之间的关系。在该附录中,我们将 1.2.1 节描述的参考架构与 Amazon SageMaker、Google Vertex AI、Microsoft Azure Machine Learning 和 Kubeflow 进行了比较。

1.3 构建深度学习系统与开发模型

在我们开始之前的最后一项准备工作是:我们认为强调构建深度学习系统开发深度学习模型之间的区别至关重要。在本书中,我们将开发深度学习模型的实践定义为解决问题的过程

  • 探索可用数据以及如何将其转换为训练数据

  • 确定用于解决问题的有效训练算法

  • 训练模型并开发推理代码以针对未见数据进行测试

请记住,深度学习系统不仅应支持模型开发所需的所有任务,还应支持其他角色执行的任务,并使这些角色之间的协作无缝。在构建深度学习系统时,您不是在开发深度学习模型;您正在构建一个支持深度学习模型开发的系统,使该过程更加高效和可扩展。

我们发现已有大量关于构建模型的材料发布。但是,我们几乎没有看到有关设计和构建支持这些模型的平台或系统的资料。这就是为什么我们写了这本书。

摘要

  • 典型的机器学习项目开发经历以下循环:产品启动、数据探索、模型原型制作、生产化和生产集成。

  • 深度学习项目开发涉及七种不同的角色:产品经理、研究人员、数据科学家、数据工程师、MLOps 工程师、机器学习系统工程师和应用工程师。

  • 深度学习系统应该降低深度学习开发周期中的复杂性。

  • 在深度学习系统的帮助下,数据科学家不需要突然成为专业的 DevOps 或数据工程师,但应该能够以可扩展的方式实现模型,建立数据管道,独立地部署和监控模型。

  • 高效的深度学习系统应该让数据科学家专注于有趣且重要的数据科学任务。

  • 高层次的参考架构(如图 1.3 所示)可以帮助您快速开始一个新的设计。首先,复制一份并收集目标和需求。最后,根据需要添加、修改或减少组件及其关系。

  • 基础的深度学习系统由以下关键组件组成:数据集管理器、模型训练器、模型服务、元数据和存储容器、工作流编排和数据科学环境。

  • 数据管理组件帮助收集、组织、描述和存储数据作为可用于训练的数据集。它还支持数据探索活动并跟踪数据集之间的血统。第二章将详细讨论数据管理。

  • 模型训练组件负责处理多个训练请求,并在给定有限的计算资源的情况下高效地运行它们。第三章和第四章将回顾模型训练组件。

  • 模型服务组件处理传入的推断请求,使用模型生成推断结果,并将其返回给请求者。章节 6 和 7 将介绍这部分内容。

  • 元数据和存储容器组件记录元数据并存储来自系统其余部分的工件。系统产生的任何数据都可以视为工件。其中大多数将是模型,其附带的元数据将存储在同一组件中。这提供了完整的血统信息,以支持实验和故障排除。我们将在第八章中讨论这个组件。

  • 工作流管理组件存储链式定义,连接数据处理和模型训练的不同步骤。它负责触发周期性工作流运行,并跟踪正在其他组件上执行的每个运行步骤的进度—例如,在模型训练服务上执行的模型训练步骤。在第九章中,我们将介绍该组件的实例。

  • 深度学习系统应支持深度学习开发周期,并使多个角色之间的协作变得简单。

  • 构建深度学习系统与开发深度学习模型是不同的。系统是支持深度学习模型开发的基础设施。

第二章:数据集管理服务

本章包括

  • 理解数据集管理

  • 使用设计原则构建数据集管理服务

  • 构建一个样本数据集管理服务

  • 使用开源方法进行数据集管理

在我们对深度学习系统做一般讨论之后,我们已经准备好了章节的其他部分,这些部分专注于这些系统中的特定组件。我们首先介绍数据集管理,不仅因为深度学习项目是数据驱动的,还因为我们希望提醒您在构建其他服务之前考虑数据管理有多重要。

在深度学习模型开发过程中,数据集管理(DM)往往被忽视,而数据处理、模型训练和服务则吸引了最多的注意力。数据工程中的一个普遍观点是,好的数据处理流程,比如 ETL(抽取、转换和加载)流程,已经足够了。但是,如果在项目进行过程中避免管理数据集,你的数据收集和数据集消耗逻辑将变得越来越复杂,模型性能改进会变得困难,最终整个项目也会变慢。一个好的 DM 系统可以通过解耦训练数据的收集和消耗加快模型的开发;它还可以通过对训练数据进行版本控制来实现模型的可重复性。

我们保证您将感谢自己明智的决定,构建或至少设置一个数据集管理组件,以补充现有的数据处理流程。并且在着手训练和服务组件之前构建它。您的深度学习项目开发将会更快,长远来看可以产生更好的结果和更简单的模型。因为 DM 组件将上游数据的复杂性屏蔽在您的模型训练代码之外,您的模型算法开发和数据开发可以并行运行。

本章涉及为您的深度学习项目构建数据集管理功能。由于深度学习算法、数据流程和数据源的多样性,数据集管理是深度学习行业中经常讨论的话题。目前还没有统一的方法,而且似乎永远不会有一个。因此,为了在实践中对您有益,我们将专注于教授设计原则,而不是倡导单一方法。我们在本章构建的样本数据集管理服务展示了实施这些原则的一种可能方法。

在第 2.1 节,您将了解到为什么需要数据集管理,它应该解决哪些挑战以及它在深度学习系统中所扮演的关键角色。我们还将介绍其关键设计原则,为下一节的具体示例做好准备。

在第 2.2 节中,我们将基于第 2.1 节介绍的概念和设计原则演示一个数据集管理服务。首先,我们将在您的本地机器上设置该服务并进行实验。其次,我们将讨论内部数据集存储和数据模式、用户场景、数据摄取 API 和数据集提取 API,以及提供设计和用户场景的概述。在这个过程中,我们还将讨论在服务设计中作出的一些重要决策的优点和缺点。

在第 2.3 节,我们将看两种开源方法。如果你不想要自行构建的数据集管理服务,你可以使用已经构建好、可用和可适应的组件。例如,如果您的现有数据流水线是基于 Apache Spark 构建的,您可以使用 Delta Lake 与 Petastorm 进行数据集管理。或者,如果您的数据直接来自于云对象存储,例如 AWS 简单存储服务(S3)或 Azure Blob,您可以选择采用 Pachyderm。我们以图像数据集准备为例,展示这两种方法如何在实践中处理非结构化数据。在本章结束时,您将对数据集管理的内在特性和设计原则有深入的了解,这样您可以自己构建数据集管理服务或改进工作中的现有系统。

2.1 理解数据集管理服务

数据集管理组件或服务是一个专门的数据存储,用于组织数据以支持模型训练和模型性能故障排除。它处理来自上游数据源的原始数据,并以一种明确定义的结构(数据集)返回用于模型训练的训练数据。图 2.1 显示了数据集管理服务提供的核心价值。在图中,我们可以看到数据集管理组件将原始数据转换为有利于模型训练的一致的数据格式,因此下游模型训练应用程序只需关注算法开发。

图 2.1 数据集管理服务是一个专门的数据存储;它使用自己的原始数据格式将数据导入其内部存储。在训练期间,它将原始数据转换为一致的数据格式,以便于模型训练。

2.1.1 深度学习系统为什么需要数据集管理

在我们开始查看示例数据集管理服务之前,让我们花一点时间解释为什么 DM 是任何深度学习系统的重要组成部分。这一部分很重要,因为根据我们的经验,除非你完全理解为什么,否则无法设计解决实际问题的系统。

对于为什么这个问题,有两个答案。第一,DM 可以通过将训练数据的收集使用分离来加快模型的开发。第二,一个设计良好的 DM 服务通过对训练数据集进行版本跟踪来支持模型的可复现性。让我们详细讨论这两个观点。

解耦训练数据收集与消费

如果你完全独自开发深度学习项目,项目开发工作流程是以下步骤的迭代循环:数据收集、数据集预处理、训练和评估(见图 2.2)。虽然如果你在数据收集组件中更改数据格式,可能会破坏下游数据集预处理代码或训练代码,但这不是一个大问题。因为你是唯一的代码所有者,你可以自由更改;没有其他人受到影响。

图 2.2 单人深度学习项目开发的工作流程是一系列线性步骤的迭代循环。

当我们正在构建一个面向数十个不同深度学习项目并向多人和团队开放的严肃深度学习平台时,简单的数据流程图将迅速扩张到令人困惑的 3D 图(图 2.3)。

图 2.3 企业中的深度学习模型开发在多个维度上运行。多个团队合作以在不同阶段完成项目。每个团队专注于工作流程的一个步骤,同时还在多个项目上工作。

图 2.3 显示了企业深度学习开发环境的复杂性。在这种情况下,每个人只负责一个步骤而不是整个工作流程,并且他们为多个项目开发他们的工作。理想情况下,这个过程是有效的,因为人们通过专注于一个特定问题来建立他们的专业知识。但这里有个问题:通信成本经常被忽视。

当我们将工作流程的步骤(图 2.2)分配给多个团队时,需要数据模式进行握手。没有数据合同,下游团队不知道如何读取上游团队发送的数据。让我们回到图 2.3。想象一下,如果有四个团队并行开发的 10 个项目,尤其是每个团队处理工作流程的不同步骤,我们需要多少数据模式来在团队之间进行通信。

现在,如果我们想要向训练数据集添加一个新特征或属性(如文本语言),我们需要召集每个团队,在新数据格式上达成共识,并实施更改。这是一项巨大的工作,因为公司内部的跨团队协作是复杂的。通常需要几个月的时间来做出一个小改变;因为每个团队都有自己的优先事项,你必须等待他们的待办事项清单。

更糟糕的是,深度学习模型开发是一个迭代过程。它要求不断调整训练数据集(包括上游数据管道)以提高模型准确性。这需要数据科学家、数据开发人员和平台开发人员高频率地进行交互,但由于跨团队工作流程的设置,数据迭代发生缓慢,这是在生产环境中模型开发如此缓慢的原因之一。

另一个问题是,当我们同时开发多种类型的项目(图像、视频和文本)时,数据模式的数量将会激增。如果让每个团队自由定义新的数据模式,并且不进行适当管理,那么保持系统向后兼容几乎是不可能的。新数据的更新将变得越来越困难,因为我们必须花费额外的时间来确保新数据更新不会破坏过去构建的项目。因此,项目开发速度将会显著减慢。

为解决迭代缓慢和数据模式管理问题,我们可以构建一个数据集管理服务。让我们看一下图 2.4,以帮助确定引入数据集管理服务后项目开发工作流程的变化。

在图 2.4 中,我们看到一个数据集管理服务将模型开发工作流程分成了两个独立的空间:数据开发者空间和数据科学家空间。长迭代循环(图 2.2)现在被分成了两个小循环(图 2.4),每个循环由一个团队拥有,因此数据开发者和数据科学家可以分别迭代数据收集和模型训练;因此,深度学习项目可以更快地迭代。

图 2.4 一个数据集管理组件通过为训练数据收集和消耗定义强类型模式,为两者之间创建了良好的分离,这使得数据开发和模型算法开发可以在自己的循环中迭代,从而加快了项目的开发速度。

你可能也注意到,现在我们把所有的数据模式都放在了一个地方:一个数据集管理服务,它管理着两种强类型的数据模式——每种类型的数据集都有一个摄取数据模式和一个训练数据模式。通过在数据转换过程中在 DM 内部进行数据摄取和训练的两个单独的数据模式,你可以确保上游数据收集中的数据更改不会破坏下游的模型训练。由于数据模式是强类型的,未来的数据升级可以轻松地保持向后兼容。

为项目定义强类型数据集可能并不是一个好主意,因为我们仍在探索各种数据选项。因此,我们还建议定义一种特殊的无模式数据集类型,例如GENERIC类型,它没有强类型模式限制。对于此数据集类型中的数据,DM 只接受原样数据,并且不执行数据验证和转换(有关详细示例,请参见第 2.2.6 节)。从数据处理管道中收集的数据可以直接由训练流程使用。虽然整个工作流程可能会很脆弱,但自由数据集类型满足了在早期阶段项目需要灵活性的需求。一旦项目成熟,我们可以创建强类型模式并为它们定义数据集类型。

总结本节,管理数据集类型的两个数据架构是解耦数据科学家和数据开发者的秘密武器。在第 2.2.6 节,我们将展示如何在我们的示例数据集管理服务中实现这些架构。

实现模型可重现性

一个设计良好的数据集管理服务通过在训练数据集上进行版本跟踪来支持模型可重现性,例如,使用版本字符串来获取在以前模型训练运行中使用的确切训练文件。相对于数据科学家(模型算法开发),模型可重现性的优势在于,你可以重复在某个数据集上运行深度学习算法(例如 NLP 中的自注意力变换器),并获得相同或相似质量的结果。这被称为算法可重现性

从深度学习系统开发者的角度来看,模型可重现性是算法可重现性的超集。它要求数据集管理系统能够复现其输出物件(数据集)。例如,我们需要获取确切的训练数据和训练配置来复现过去训练过的模型。

模型可重现性对于机器学习项目至关重要,有两个主要原因。第一个是信任。可重现性为生成模型的系统创造了信任和可信度。对于任何系统,如果输出无法复现,人们简单地不会信任该系统。这在机器学习项目中非常相关,因为应用程序将根据模型输出做出决策——例如,聊天机器人将根据用户意图预测将用户呼叫转接到适当的服务部门。如果我们无法复现模型,构建在模型之上的应用程序是不确定性的和不可信的。

第二个原因是模型可重现性有助于性能故障排除。在检测到模型性能退化时,人们首先想要找出训练数据集和训练算法代码发生了什么变化。如果不支持模型可重现性,性能故障排除将非常困难。

2.1.2 数据集管理设计原则

在我们开始构建之前,我们想要概述 DM 的五个设计原则。

注意:我们认为这五个原则是本章最重要的元素。对于数据应用程序,我们在设计中遵循的原则比实际设计更重要。因为数据可能是任何形式的任何东西,在一般情况下,没有适用于所有数据处理用例的数据存储范式,也没有适用于所有数据处理用例的标准设计。因此,在实践中,我们通过遵循某些通用原则来构建我们自己的数据应用程序。因此,这些原则至关重要。

这里的五个原则将为您建立新的 DM 服务或改进现有的 DM 服务提供明确的设计目标。

原则 1:支持数据集可重现性以重现模型

数据集的可重现性意味着 DM 总是返回过去返回的完全相同的训练示例。例如,当训练团队开始训练模型时,DM 提供了一个带有版本字符串的数据集。每当训练团队——或任何其他团队——需要检索相同的训练数据时,它都可以使用此版本字符串查询 DM 以检索相同的训练数据。

我们相信所有的 DM 系统都应该支持数据集的可重现性。更好的是还能提供数据差异功能,这样我们就可以轻松地看到两个不同数据集版本之间的数据差异。这对故障排除非常方便。

原则 2:在不同类型的数据集上提供统一的 API

深度学习的数据集可能是结构化的(文本,如销售记录或用户对话的文字稿)或非结构化的(图像、语音记录文件)。无论 DM 系统如何在内部处理和存储这些不同形式的数据,它都应该为上传和获取不同类型的数据集提供统一的 API 接口。API 接口还将数据源与数据使用者抽象出来;无论发生什么情况,比如数据解析更改和内部存储格式更改,下游使用者都不应受到影响。

因此,我们的用户,包括数据科学家和数据开发人员,只需要学习一个 API 就能处理所有不同类型的数据集。这使系统简单易用。此外,由于我们只公开一个公共 API,代码维护成本将大大降低。

原则 3:采用强类型数据模式

强类型数据模式是避免由数据更改引起的意外故障的关键。通过数据模式强制执行,DM 服务可以保证其摄取的原始数据和生成的训练数据与我们的规范一致。

强类型数据模式充当安全防护,以确保下游模型训练代码不受上游数据收集更改的影响,并确保 DM 的上游和下游客户的向后兼容性。如果没有数据模式保护,则数据集使用者——下游模型训练代码——很容易受到上游数据更改的影响。

数据模式也可以进行版本控制,但这会增加管理的复杂性。另一个选项是每个数据集只有一个模式。在引入新的数据更改时,确保模式更新是向后兼容的。如果新的数据需求需要破坏性更改,则创建一个具有新模式的新数据集类型,而不是更新现有的数据集。

原则 4:确保 API 一致性并在内部处理扩展

深度学习领域的当前趋势是,随着数据集不断增大,模型架构也变得越来越复杂。例如,GPT-3(一个用于语言理解的生成预训练转换器语言模型)使用超过 250 TB 的文本材料,其中包含数百亿个单词;在特斯拉中,自动驾驶模型消耗了海量的数据,达到了 PB 级。另一方面,对于一些简单的窄领域任务(如客户支持工单分类),我们仍然使用小型数据集(约 50 MB)。数据集管理系统应该在内部解决数据扩展的挑战,并且向用户(数据开发者和数据科学家)提供的 API 对于大型和小型数据集应该是一致的。

原则 5:保证数据持久性

理想情况下,用于深度学习训练的数据集应该以不可变的方式存储,以便复现训练数据和进行故障排查。数据删除应该是软删除,只有极少数例外情况才需要进行硬删除,例如当客户选择退出或取消账户时永久删除客户数据。

2.1.3 数据集的矛盾性

为了结束我们关于数据集管理的概念讨论,我们想要澄清数据集一个模糊的方面。我们见过许多设计不良的数据集管理系统在这一点上失败。

数据集具有矛盾的特性:它既是动态的又是静态的。从数据科学家的角度来看,数据集是静态的:它是一组带有注释(也称为标签)的固定文件。从数据开发者的角度来看,数据集是动态的:它是一个远程存储中的文件保存目的地,我们不断向其添加数据。

因此,从数据管理的角度来看,数据集应该是一个逻辑文件组,同时满足数据收集和数据训练的需求。为了帮助您对数据集的动态和静态特性有具体的理解,让我们看一下图 2.5。

图 2.5 数据集是一个逻辑文件组:它既是动态的又是静态的,对于数据收集来说是可编辑的,但对于模型训练来说是固定的。

我们可以从数据摄入和数据获取两个角度来阅读图 2.5。首先,从数据摄入的角度来看,我们看到数据收集管道(图中左侧)不断地注入新数据,例如文本话语和标签。例如,在时间 T0,数据集中创建了一个示例数据批次(示例批次 T0)——T1、T2 和 T3 时间也是如此;随着时间的推移,我们总共创建了四个数据批次。因此,从数据开发者的角度来看,这个数据集是可变的,因为管道不断向其中添加数据。

其次,在训练数据获取方面(从图的顶部),我们可以看到在获取训练数据时,DM 同时读取数据集中的所有当前数据。我们可以看到数据以静态的版本快照方式返回,该快照具有一个版本字符串,用于唯一标识从数据集中选择的实际数据。例如,当我们从时间点 T2 的数据集中获取训练数据时,数据集有三个数据批次(批次 T0、批次 T1 和批次 T2)。我们将这三个数据批次打包成一个快照,分配一个版本字符串(“version1”)并将其作为训练数据返回。

从模型训练的角度来看,从 DM 获取的数据集是数据集的静态快照——一个经过时间过滤和客户逻辑过滤的数据集。静态快照对于模型的可复制性至关重要,因为它代表了训练过程中使用的确切训练文件。当我们需要重新构建模型时,我们可以使用快照版本字符串来找到过去模型训练中使用的快照。

我们对理论介绍已经进行了全面的介绍,您应该能够掌握数据集管理组件的需求、目标和独特特性。下一节是如何设计数据集管理服务的具体示例。

2.2 浏览示例数据集管理服务

在本节中,我们将带您了解一个示例 DM 服务。我们构建了这个示例,以便让您了解第 2.1.2 节中介绍的原理如何实施。我们首先在本地运行服务,与之互动,然后查看其 API 设计和内部实现。

2.2.1 与示例服务交互

为了方便您操作,我们构建了七个 shell 脚本来自动化整个 DM 实验室。这些 shell 脚本是本节演示场景的推荐方式,因为它们不仅自动配置本地服务,还会处理环境变量设置、准备示例数据和初始化本地网络。

您可以在github.com/orca3/MiniAutoML/tree/main/scripts找到这些脚本,从搜索词“dm”开始。我们 GitHub 仓库中的“功能演示”文档(github.com/orca3/MiniAutoML/tree/main/data-management)提供了完成实验以及这些脚本的示例输出的详细说明。

在运行功能演示之前,请确保已满足系统要求。请参考github.com/orca3/MiniAutoML#system-requirements

这个实验室分为三个部分:首先,运行示例数据集管理服务;其次,创建一个数据集并上传数据;再次,从刚创建的数据集中获取训练数据。

在本地设置服务

示例服务是用 Java 11 编写的。它使用 MinIO 作为文件 Blob 服务器来模拟云对象存储(如 Amazon S3),因此我们可以在本地运行而无需任何远程依赖。如果您在附录 A 中设置了实验,您可以在终端中的脚本文件夹的根目录运行以下命令(列表 2.1)来启动服务。

注意在运行 DM demo 脚本之前,强烈建议从干净的设置开始。您可以执行 ./scripts/lab-999-tear-down.sh 来清除之前的实验。

列表 2.1 在本地启动服务

# (1) Start minio server
./scripts/dm-001-start-minio.sh     

# (2) start dataset management service, it will build 
➥ the dm image and run the container.
./scripts/dm-002-start-server.sh

为了保持服务的最简设置,我们将所有数据集记录保存在内存中,以避免使用数据库。请注意,如果重新启动数据集管理服务,你将丢失所有数据集。

创建和更新语言意图数据集

我们的示例 DM 服务为用户提供了三种 API 方法来创建/更新数据集并检查结果。这些 API 方法是 CreateDatasetUpdateDatasetGetDatasetSummary。我们将在接下来的几节中详细讨论它们。

在此示例场景中,我们首先调用数据管理服务上的 CreateDataset API 方法来创建一个新的语言意图数据集;然后我们使用 UpdateDataset API 方法向数据集追加更多数据。最后,我们使用 GetDatasetSummary API 方法获取数据集的统计信息和提交(数据更改)历史记录。

注意脚本 dm-003-create-dataset.sh 和 dm-004-add-commits.sh 自动化了之前的步骤。请使用它们来运行演示场景。请注意,以下代码列表仅供说明目的。

现在让我们运行实验。首先,我们将使用以下列表创建一个数据集。

列表 2.2 创建语言意图数据集

mc -q cp data-management/src/test/resources/datasets/test.csv  ❶
➥ myminio/"${MINIO_DM_BUCKET}"/upload/001.csv

grpcurl -v -plaintext \                                        ❷
 -d '{"name": "dataset-1", \
      "dataset_type": "LANGUAGE_INTENT", \                     ❸
      "bucket": "mini-automl", \                               ❹
      "path": "{DATA_URL_IN_MINIO}"}' \                        ❹
 ${DM_SERVER}:${DM_PORT} \
 data_management.DataManagementService/CreateDataset           ❺

❶ 将原始数据(upload/001.csv)上传到云存储

❷ 创建数据集的 gRPC 请求

❸ 数据集类型

❹ MinIO 中原始数据的数据 URL,例如,upload/001.csv

❺ API 名称

应注意,CreateDataset API 预期用户在 gRPC 请求中提供可下载的 URL,而不是实际数据,这就是为什么我们首先将 001.csv 文件上传到本地 MinIO 服务器的原因。数据集创建完成后,CreateDataset API 将返回一个包含数据摘要和数据集历史提交的 JSON 对象。以下是一个示例结果:

{
 "datasetId": "1", 
  "name": "dataset-1",
 "dataset_type": "TEXT_INTENT",
  "last_updated_at": "2021-10-09T23:44:00",
  "commits": [                                ❶
    {       
      "dataset_id": "1",
      "commit_id": "1",                       ❷
      "created_at": "2021-10-09T23:44",
 "commit_message": "Initial commit",
      "tags": [                               ❸
        {
          "tag_key": "category",
          "tag_value": "test set"
        }
      ],
      "path": "dataset/1/commit/1",
      "statistics": {                         ❹
        "numExamples": "5500",
        "numLabels": "151"
      }
    }
  ]
}

❶ 提交是数据集更新的快照。

❷ 提交 ID;此提交捕获了来自 upload/001.csv 的数据。

❸ 提交标签用于在构建训练数据集时过滤提交。

❹ 提交的数据摘要

创建数据集后,你可以通过追加更多数据来持续更新它;请参见以下的数据集更新 gRPC 请求。

列表 2.3 更新语言意图数据集

mc -q cp data-management/src/test/resources/datasets/train.csv  myminio/"${MINIO_DM_BUCKET}"/upload/002.csv        ❶

grpcurl -v -plaintext \                                 ❷
 -d '{"dataset_id": "1", \                              ❸
      "commit_message": "More training data", \
      "bucket": "mini-automl", \                        ❹
      "path": "upload/002.csv", \                       ❹
      "tags": [{ \
        "tag_key": "category", \
        "tag_value": "training set\"}]}' \ 
${DM_SERVER}:${DM_PORT} \
data_management.DataManagementService/UpdateDataset     ❺

❶ 将原始数据(upload/002.csv)上传到云存储

❷ 请求追加更多数据(upload/002.csv)

❸ 用从 CreateDataset API 返回的值替换数据集 ID。

❹ 原始数据的数据 URL,由原始数据上传创建

❺ 更新数据集的 API 名称

一旦数据集更新完成,UpdateDataset API 会以与 CreateDataset API 相同的方式返回一个数据摘要 JSON 对象;请参考以下示例响应对象:

{
  "datasetId": "1",
  "name": "dataset-1",
  "dataset_type": "TEXT_INTENT",
  "last_updated_at": "2021-10-09T23",
  "commits": [
    {
      "commit_id": "1",        ❶
       .. .. ..
    },
    {
      "dataset_id": "1",
      "commit_id": "2",        ❷
      "created_at": "2021-10-09T23:59:17",
      "commit_message": "More training data",
      "tags": [
        {
          "tag_key": "category",     
          "tag_value": "training set" 
        }
      ],
      "path": "dataset/1/commit/2",
      "statistics": {
        "numExamples": "7600",
        "numLabels": "151"
      }
    }
  ]
}

❶ 由创建数据集请求创建的提交

❷ 提交 ID;此提交捕获来自 upload/002.csv 的数据。

你还可以使用 GetDatasetSummary API 来获取数据集的数据摘要和提交历史。请参考以下示例的 gRPC 请求:

grpcurl -v -plaintext 
  -d '{"datasetId": "1"}' \     ❶
${DM_SERVER}:${DM_PORT} \
data_management.DataManagementService/GetDatasetSummary

❶ 要查询的数据集的 ID

获取训练数据集

现在我们已经创建了一个数据集(ID = 1),包含原始数据;让我们尝试从中构建一个训练数据集。在我们的示例服务中,这是一个两步骤的过程。

我们首先调用 PrepareTrainingDataset API 开始数据集构建过程。然后,我们使用 FetchTrainingDataset API 查询数据集准备进度,直到请求完成。

注意,脚本 dm-005-prepare-dataset.sh、dm-006-prepare-partial-dataset.sh 和 dm-007-fetch-dataset-version.sh 自动化了接下来的步骤。请尝试使用它们来运行代码清单 2.4 和 2.5 中的示例数据集获取演示。

要使用 PrepareTrainingDataset API,我们只需提供一个数据集 ID。如果你只想让部分数据进入训练数据集,你可以在请求中使用 tag 作为过滤器。请参考以下的示例请求。

清单 2.4 准备训练数据集

grpcurl -plaintext \                   ❶
 -d “{“dataset_id”: “1”}” \            ❶
 ${DM_SERVER}:${DM_PORT} \             ❶
data_management.DataManagementService/PrepareTrainingDataset

grpcurl -plaintext \                   ❷
 -d “{“dataset_id”: “1”, \             ❷
 “Tags”:[ \                            ❷
   {“tag_key”:”category”, \            ❸
    “tag_value”:”training set”}]}” \   ❸
 ${DM_SERVER}:${DM_PORT} data_management.DataManagementService/PrepareTrainingDataset

❶ 准备包含所有数据提交的训练数据集的请求

❷ 通过定义过滤标签,准备包含部分数据提交的训练数据集的请求

❸ 数据过滤器

一旦数据准备的 gRPC 请求成功,它将返回一个如下的 JSON 对象:

{
  "dataset_id": "1",
  "name": "dataset-1",
  "dataset_type": "TEXT_INTENT",
  "last_updated_at": "2021-10-09T23:44:00",
  "version_hash": "hashDg==",     ❶
  "commits": [
    {                             ❷
      "commit_id": "1",           ❷
      .. .. ..                    ❷
    },                            ❷
    {                             ❷
      "commit_id": "2",           ❷
      .. .. ..                    ❷
    }                             ❷
  ]
}

❶ 训练数据集快照的 ID

❷ 原始数据集的选定数据提交

PrepareTrainingDataset API 返回的数据中包含 "version_hash" 字符串。它用于识别该 API 生成的数据快照。使用这个哈希作为 ID,我们可以调用 FetchTrainingDatasetc API 来跟踪训练数据集建立的进度;请参考以下示例。

清单 2.5 检查数据集准备进度

grpcurl -plaintext \
 -d "{"dataset_id": "1", \
      "version_hash":          ❶
      "hashDg=="}" \           ❶
${DM_SERVER}:${DM_PORT} 
data_management.DataManagementService/FetchTrainingDataset

❶ 训练数据集快照的 ID

FetchTrainingDatasetc API 返回一个描述训练数据集的 JSON 对象。它告诉我们背景数据集构建过程的状态:RUNNINGREADYFAILED。如果训练数据已准备好供使用,响应对象将显示训练数据的可下载 URL 列表。在这个演示中,URL 指向本地的 MinIO 服务器。请参考以下的示例响应:

{
  "dataset_id": "1",
  "version_hash": "hashDg==",
  "state": "READY",                                          ❶
  "parts": [
    {                                                        ❷
      "name": "examples.csv",                                ❷
      "bucket": "mini-automl-dm",                            ❷
      "path": "versionedDatasets/1/hashDg==/examples.csv"    ❷
    },                                                       ❷
    {                                                        ❷
      "name": "labels.csv",                                  ❷
      "bucket": "mini-automl-dm",                            ❷
      "path": "versionedDatasets/1/hashDg==/labels.csv"      ❷
    }                                                        ❷
  ],                                                         ❷
  "statistics": {
    "numExamples": "16200",
    "numLabels": "151"
  }
}

❶ 训练数据集的状态

❷ 训练数据的数据 URL

做得好!你刚刚体验了我们示例数据集管理服务提供的所有主要数据 API。通过尝试自己上传数据和构建训练数据集,我们希望你能感受到这项服务的用途。在接下来的几个部分,我们将查看用户场景、服务架构概览以及我们的示例数据集管理服务的代码实现。

如果在运行上述脚本时遇到任何问题,请参考我们 GitHub 仓库中“function demo”文档中的说明。另外,如果您想尝试第三章和第四章的实验,请保持容器运行,因为它们是模型训练实验的先决条件。

2.2.2 用户、用户场景和整体情况

在设计后端服务时,我们发现非常有用的方法是从外部到内部思考。首先,弄清楚用户是谁,服务将提供什么价值,以及客户将如何与服务进行交互。然后,内部逻辑和存储布局应该自然而然地出现。对于浏览此示例 DM 服务,我们将使用相同的方法展示给您。因此,让我们首先看看我们的用户和用户场景。

注意我们之所以首先考虑用例,是因为我们认为任何系统设计都应该最大程度地考虑用户。如果我们确定了客户如何使用系统,那么我们的效率和可扩展性的方法将自然而然地出现。如果设计是以相反的顺序进行的(首先考虑技术,其次考虑可用性),那么系统通常很难使用,因为它是为技术而设计的,而不是为客户设计的。

用户和用户场景

我们的示例 DM 服务是为两个虚构用户而构建的:建国,一名数据工程师,和朱莉娅,一名数据科学家。他们共同合作训练语言意图分类模型。

建国负责训练数据收集。他不断地从不同的数据源(如解析用户活动日志和进行客户调查)中收集数据并对其进行标记。建国使用 DM 数据摄取 API 创建数据集,将新数据附加到现有数据集中,并查询数据集的摘要和状态。

朱莉娅使用建国构建的数据集来训练意图分类模型(通常使用 PyTorch 或 TensorFlow 编写)。在训练时,朱莉娅的训练代码将首先调用 DM 服务的获取训练数据 API 从 DM 获取训练数据集,然后开始训练过程。

服务的整体架构

我们的示例 DM 服务分为三层:数据摄取层、数据集获取层和数据集内部存储层。数据摄取 API 集是为了让建国能够上传新的训练数据并查询数据集状态而构建的。数据集获取 API 是为了让朱莉娅能够获取训练数据集。有关整体情况,请参见图 2.6 和图 2.7。

图 2.6 示例数据集管理服务的系统概述。该示例服务包含三个主要组件,即数据摄取 API、内部存储和数据集获取 API。

图 2.6 的中心大框显示了我们样本数据集管理服务的整体设计。它有一个内部数据集存储系统和两个面向公众的接口:一个用于数据摄入的数据摄入 API 和一个用于数据集获取的数据集获取 API——一个用于数据摄入,另一个用于数据集获取。该系统支持强类型架构数据集(文本和图像类型)和非架构数据集(GENERIC类型)。

图 2.7 用于存储数据集的内部存储结构

图 2.7 显示了样本 DM 服务用于存储数据集的整体数据结构。提交是由数据摄入 API 创建的,版本化的快照是由数据获取 API 创建的。提交和版本化快照的概念被引入以应对数据集的动态和静态特性。我们将在第 2.2.5 节详细讨论存储。

在接下来的小节中,我们将逐个详细介绍前两个图表中的每个细节,从 API 开始,然后转向内部存储和数据架构。

2.2.3 数据摄入 API

数据摄入 API 允许在样本数据集管理服务中创建、更新和查询数据集。图 2.8 中的灰色框显示了数据摄入层中支持将数据摄入到 DM 中的四种服务方法的定义。它们的名称不言自明;让我们在列表 2.6 中查看它们的 gRPC 方法定义。

图 2.8 支持数据摄入的四种方法:创建数据集、更新数据集、获取数据集摘要和列出数据集

注意:为了减少样板代码,我们选择了 gRPC 来实现样本 DM 服务的公共接口。这并不意味着 gRPC 是数据集管理服务的最佳方法,但与 RESTful 接口相比,gRPC 的简洁编码风格非常适合演示我们的想法,而不会让您接触到不必要的 Spring 框架细节。

数据摄入方法定义

让我们看看我们的样本数据摄入 API 是什么样子的。

列表 2.6 数据摄入 API 服务定义

# create a new dataset and save data into it
rpc CreateDataset (CreateDatasetRequest) returns (DatasetSummary);

# add new data to an existing dataset 
rpc UpdateDataset (CreateCommitRequest) returns (DatasetSummary);

# get summary and history of a given dataset
rpc GetDatasetSummary (DatasetPointer) returns (DatasetSummary);

# list all existing datasets’ summary
rpc ListDatasets (ListQueryOptions) returns (stream DatasetSummary);

message CreateDatasetRequest {
 string name = 1;
 string description = 2;
 DatasetType dataset_type = 3;   ❶
 string bucket = 4;              ❷
 string path = 5;                ❷
 repeated Tag tags = 6;          ❸
}

❶ 定义数据集类型,为"TEXT_INTENT"或"GENERIC"

❷ 定义了在 MinIO 服务器中上传数据的文件 URL。

❸ 通过使用标签设置数据过滤器。

注意:本示例服务不涵盖数据删除和修改的主题,但该服务可以轻松扩展以支持这些功能。

数据 URL 与数据流

在我们的 API 设计中,您可能会注意到我们要求用户提供数据 URL 作为原始数据输入,而不是直接将文件上传到我们的服务。在第 2.2.4 节中,我们还选择将数据 URL 作为训练数据集返回,而不是通过流式传输端点直接返回文件。主要原因是我们希望将文件传输工作卸载到云对象存储服务(如 Amazon S3 或 Azure Blob)。这样做有两个好处:首先,它节省了网络带宽,因为客户端和服务之间没有实际的文件传递;其次,它降低了代码复杂性,因为在文件较大且 API 使用量较高时,保持数据流工作的高可用性可能会更加复杂。

创建新的数据集

让我们来看看 gRPC CreateDataset 方法是如何实现的。在调用 DM(createDataset API)创建数据集之前,用户(Jianguo)需要准备一个可下载的 URL,用于上传他们想要的数据(步骤 1 和 2);这个 URL 可以是云对象存储服务中的可下载链接,如 Amazon S3 或 Azure Blob。在我们的示例服务中,我们使用 MinIO 服务器在本地模拟 Amazon S3。Jianguo 还可以在数据集创建请求中命名数据集并分配标签。清单 2.7 突出显示了代码(dataManagement/DataManagementService .java)的关键部分,它实现了图 2.9 中所示的工作流程。

清单 2.7 是新数据集创建的实现

public void createDataset(CreateDatasetRequest request) {

  int datasetId = store.datasetIdSeed.incrementAndGet();      ❶

  Dataset dataset = new Dataset(                              ❷
    datasetId, request.getName(),                             ❷
    request.getDescription(),                                 ❷
    request.getDatasetType());                                ❷
  int commitId = dataset.getNextCommitId();                   ❷

  CommitInfo.Builder builder = DatasetIngestion               ❸
    .ingest(minioClient, datasetId, commitId,                 ❸
    request.getDatasetType(), request.getBucket(),            ❸
    request.getPath(), config.minioBucketName);               ❸

  store.datasets.put(Integer.toString(datasetId), dataset);   ❹
  dataset.commits.put(commitId, builder                       ❹
    .setCommitMessage("Initial commit")                       ❹
    .addAllTags(request.getTagsList()).build());              ❹

  responseObserver.onNext(dataset.toDatasetSummary());        ❺
  responseObserver.onCompleted();                             ❺
}

❶接收数据集创建请求(步骤 3)

❷使用用户请求中的元数据创建数据集对象(步骤 4a)

❸从 URL 下载数据并将其上传到 DM 的云存储(步骤 4b)

❹将具有下载数据的数据集保存为初始提交(步骤 5)

❺将数据集摘要返回给客户端(步骤 6 和 7)

图 2.9 是创建新数据集的七个步骤的高级概述:(1)将数据上传到云对象存储;(2)获取数据链接;(3)调用createDataset API,并将数据链接作为有效负载;(4)DM 首先从数据链接下载数据,然后找到正确的数据集转换器(IntentTextTransformer)来进行数据解析和转换;(5)DM 保存转换后的数据;(6 和 7)DM 将数据集摘要(ID,提交历史,数据统计)返回给用户。

DatasetIngestion.ingest()的具体实现细节将在第 2.2.5 节中讨论。

更新现有数据集

深度学习模型的开发是一个持续的过程。一旦我们为模型训练项目创建了一个数据集,数据工程师(比如 Jianguo)将继续向其中添加数据。为了满足这个需求,我们提供了UpdateDataset API。

要使用UpdateDataset API,我们需要为新数据准备一个数据 URL。我们还可以传递提交消息和一些客户标签来描述数据的更改;这些元数据对于数据历史记录和数据过滤非常有用。

数据集更新工作流程与数据集创建工作流程几乎相同(图 2.9)。它使用给定数据创建一个新的提交,并将提交附加到数据集的提交列表中。唯一的区别是数据集更新工作流程不会创建新的数据集,而是在现有数据集上工作。请参阅以下代码清单。

注意 每次数据集更新都保存为提交,如果建国错误地将一些标记错误的数据上传到数据集中,我们可以很容易地使用一些数据集管理 API 删除或软删除这些提交。由于空间限制,这些管理 API 没有讨论。

列表 2.8 数据集更新实现

public void updateDataset(CreateCommitRequest request) {

  String datasetId = request.getDatasetId();   ❶

  Dataset dataset = store.datasets             ❷
    .get(datasetId);                           ❷
  String commitId = Integer.toString(dataset   ❷
    .getNextCommitId());                       ❷

 // the rest code are the same as listing 2.7
  .. .. .. 
}

❶ 接收数据集创建请求(步骤 3)

❷ 查找现有数据集并创建新的提交对象(步骤 4a)

在第 2.2.3 节,我们将更多地讨论提交的概念。目前,你只需知道每个数据集更新请求都会创建一个新的提交对象。

注意 为什么要将数据更新保存在提交中?我们能否将新数据与当前数据合并,以便只存储最新状态?在我们的更新数据集实现中,每次调用UpdateDataset API 时,我们都会创建一个新的提交。我们要避免就地数据合并有两个原因:首先,就地数据合并可能导致不可逆转的数据修改和悄悄的数据丢失。其次,为了重现过去使用的训练数据集,我们需要确保 DM 接收的数据批次是不可变的,因为它们是我们随时用来创建训练数据集的源数据。

列出数据集并获取数据集摘要

除了CreateDatasetUpdateDataset API 外,我们的用户还需要方法来列出现有数据集并查询数据集的概述,例如数据集的示例数和标签数以及其审计历史记录。为满足这些需求,我们构建了两个 API:ListDatasetsGetDatasetSummary。第一个 API 可以列出所有现有的数据集,而第二个 API 提供了有关数据集的详细信息,例如提交历史记录、示例和标签计数以及数据集 ID 和类型。这两个 API 的实现很简单;你可以在我们的 Git 仓库中找到它们(miniAutoML/DataManagementService.java)

2.2.4 训练数据集获取 API

在本节中,我们将看一下数据集获取层,它在图 2.10 中被标记为灰色方框。为了构建训练数据,我们设计了两个 API。数据科学家(朱莉娅)首先调用PrepareTrainingDataset API 发出训练数据准备请求;我们的 DM 服务将启动一个后台线程来开始构建训练数据,并返回一个版本字符串作为训练数据的参考句柄。接下来,朱莉娅可以调用FetchTrainingDataset API 来获取训练数据,如果后台线程已完成。

图 2.10 数据集获取层中支持数据集获取的两种方法:PrepareTrainingDatasetFetchTrainingDataset

数据集获取方法的定义

首先,让我们看一下 gRPC 服务方法定义(grpc-contract/src/main/proto/ data_management.proto)中的两个数据集获取方法——PrepareTrainingDatasetFetchTrainingDataset

列表 2.9 训练数据集获取服务定义

rpc PrepareTrainingDataset (DatasetQuery)     ❶
  returns (DatasetVersionHash);               ❶

rpc FetchTrainingDataset (VersionHashQuery)   ❷
  returns (VersionHashDataset);               ❷

message DatasetQuery {                        ❸
 string dataset_id = 1;                       ❹
 string commit_id = 2;                        ❺
 repeated Tag tags = 3;                       ❻
}

message VersionHashQuery {                    ❼
 string dataset_id = 1; 
 string version_hash = 2;                     ❽
}

❶ 准备训练数据集 API

❷ 获取训练数据集 API

❸ 数据集准备 API 的有效载荷

❹ 指定要构建训练数据的数据集

❺ 指定要构建训练数据的数据集的提交,可选

❻ 按提交标签过滤数据,可选

❼ 训练数据集获取 API 的有效载荷

❽ 版本哈希字符串代表训练数据集快照。

为什么我们需要两个 API(两个步骤)来获取数据集

如果我们只发布一个用于获取训练数据的 API,则调用者需要在后端数据准备完成后等待 API 调用,以获取最终的训练数据。如果数据准备需要很长时间,则此请求将超时。

深度学习数据集通常很大(以 GB 为单位);进行网络 I/O 数据传输和本地数据聚合可能需要几分钟或几小时。因此,获取大型数据的常见解决方案是提供两个 API——一个用于提交数据准备请求,另一个用于查询数据状态,并在请求完成时拉取结果。通过这种方式,数据集获取 API 的性能始终如一,无论数据集的大小如何。

发送准备训练数据集请求

现在让我们看一下 PrepareTrainingDataset API 的代码工作流程。图 2.11 显示了我们的示例服务如何处理 Julia 的准备训练数据集请求。

图 2.11 对应数据集构建请求的八个步骤的高层概述:(1)用户提交具有数据过滤器的数据集准备请求;(2)DM 从满足数据过滤器的提交中选择数据;(3 和 4)DM 生成表示训练数据的版本字符串;以及(5-8)DM 启动后台作业以生成训练数据。

当 DM 收到数据集准备请求(图 2.11,步骤 1)时,它执行三个动作:

  • 尝试使用给定的数据集 ID 在其存储中查找数据集。

  • 将给定的数据过滤器应用于从数据集中选择提交。

  • 创建 versionedSnapshot 对象以跟踪内部存储中的训练数据(versionHashRegistry)。versionedSnapshot 对象的 ID 是从所选提交的 ID 列表生成的哈希字符串。

versionedSnapshot 对象是 Julia 想要的训练数据集;它是从所选提交中的不可变静态文件组成的。在步骤 3 返回哈希字符串(快照 ID)后,Julia 可以使用它来查询数据集准备状态,并在训练数据集准备好时获取数据可下载的 URL。使用此版本字符串,Julia 可以始终从将来的任何时间获取相同的训练数据(versionedSnapshot),这就是支持数据集可重现性的方式。

versionedSnapshot的一个副产品是它可以在不同的PrepareTrainingDataset API 调用之间用作缓存。如果快照 ID(一系列提交的哈希字符串)已经存在,我们将返回现有的versionedSnapshot而不重建相同的数据,这可以节省计算时间和网络带宽。

注意 在我们的设计中,数据过滤发生在提交级别,而不是在个别示例级别;例如,在准备请求中使用过滤标记"DataType=Training"表明用户只希望从标记为"DataType=Training"的提交中获取数据。

第 3 步之后,DM 将生成一个后台线程来构建训练数据集。在后台作业中,DM 将从 MinIO 服务器下载每个数据集提交的文件到本地,将其聚合并压缩成一个预定义格式的文件,并将其上传回 MinIO 服务器的不同存储桶中(步骤 6 和 7)。接下来,DM 将在versionedSnapshot对象中放置实际训练数据的数据 URL,并将其状态更新为"READY"(步骤 8)。现在 Julia 可以从返回的versionedSnapshot对象中找到数据 URL 并开始下载训练数据。

我们还没有涉及的是数据模式。在数据集管理服务中,我们将摄取的数据(commit)和生成的训练数据(versionedSnapshot)保存在两种不同的数据格式中。数据合并操作(图 2.11,步骤 6 和 7)将原始摄取的数据(所选提交)聚合并将其转换为意图分类训练数据模式中的训练数据。我们将在 2.2.6 节详细讨论数据模式。列表 2.10 突出显示了图 2.11 中实现的代码。

列表 2.10 准备训练数据请求 API

public void prepareTrainingDataset(DatasetQuery request) {
 # step 1, receive dataset preparation request
  Dataset dataset = store.datasets.get(datasetId);
  String commitId;
  .. .. ..
  # step 2, select data commits by checking tag filter
  BitSet pickedCommits = new BitSet();
  List<DatasetPart> parts = Lists.newArrayList();
  List<CommitInfo> commitInfoList = Lists.newLinkedList();
  for (int i = 1; i <= Integer.parseInt(commitId); i++) {
    CommitInfo commit = dataset.commits.get(Integer.toString(i));
    boolean matched = true;
    for (Tag tag : request.getTagsList()) {
      matched &= commit.getTagsList().stream().anyMatch(k -> k.equals(tag));
    }
    if (!matched) {
      continue;
    }
    pickedCommits.set(i);
    commitInfoList.add(commit);
    .. .. ..
  }

 # step 3, generate version hash from the selected commits list
  String versionHash = String.format("hash%s", 
    Base64.getEncoder().encodeToString(pickedCommits.toByteArray()));

  if (!dataset.versionHashRegistry.containsKey(versionHash)) {
    dataset.versionHashRegistry.put(versionHash,      ❶
      VersionedSnapshot.newBuilder()                  ❶
        .setDatasetId(datasetId)                      ❶
        .setVersionHash(versionHash)                  ❶
        .setState(SnapshotState.RUNNING).build());    ❶

 # step 5,6,7,8, start a background thread to aggregate data 
 # from commits to the training dataset     
    threadPool.submit(
      new DatasetCompressor(minioClient, store, datasetId,
        dataset.getDatasetType(), parts, versionHash, config.minioBucketName));
   }

 # step 4, return hash version string to customer
  responseObserver.onNext(responseBuilder.build());
  responseObserver.onCompleted();
}

❶ 创建 VersionedSnapshot 对象以表示训练数据集

获取训练数据集

一旦 DM 服务收到了prepareTrainingDataset API 上的训练数据准备请求,它将生成一个后台作业来构建训练数据,并返回一个version_hash字符串用于跟踪目的。Julia 可以使用FetchTrainingDataset API 和version_hash字符串来查询数据集构建进度,并最终获取训练数据。图 2.12 展示了 DM 中如何处理数据集获取请求。

图 2.12 数据集获取请求服务的三个步骤的高级概述:(1)用户使用数据集 ID 和版本字符串调用FetchTrainingDataset API;(2)DM 将在其内部存储中搜索数据集的versionHashRegistry并返回一个versionedSnapshot对象;(3)当数据准备作业完成时,versionedSnapshot对象将具有一个下载 URL。

获取训练数据集本质上是查询训练数据准备请求的状态。对于每个数据集,DM 服务都会创建一个versionedSnapshot对象来跟踪prepareTrainingDataset请求生成的每个训练数据集。

当用户发送获取数据集查询时,我们只需在请求中使用哈希字符串来搜索其对应的versionedSnapshot对象在数据集的训练快照(versionHashRegistry)中是否存在,如果存在则将其返回给用户。versionedSnapshot对象将由后台训练数据处理作业(图 2.11,步骤 5–8)不断更新。当作业完成时,它将训练数据 URL 写入versionedSnapshot对象;因此,用户最终获取训练数据。请参见以下清单中的代码实现。

清单 2.11 准备训练数据请求 API

public void fetchTrainingDataset(VersionQuery request) {
  String datasetId = request.getDatasetId(); 
  Dataset dataset = store.datasets.get(datasetId); 

  if (dataset.versionHashRegistry.containsKey(   ❶
      request.getVersionHash())) {               ❶

    responseObserver.onNext(

      dataset.versionHashRegistry.get(           ❷
 request.getVersionHash()));                ❷
    responseObserver.onCompleted();
  } 
  .. .. .. 
}

❶ 在数据集的训练快照中搜索versionedSnapshot

❷ 返回versionedSnapshot;其中包含了数据集准备的最新进展。

2.2.5 内部数据集存储

示例服务的内部存储仅是一组内存中的数据集对象。之前我们讨论了数据集既可以是动态的又可以是静态的。一方面,数据集是一个逻辑文件组,随着不断地从各种来源吸收新数据而动态变化。另一方面,它是静态的且可重现的用于训练。

为了展示这个概念,我们设计了每个数据集,其中包含提交列表和版本化快照列表。提交代表动态摄入的数据:通过数据摄入调用(CreateDatasetUpdateDataset)添加的数据;提交还具有标签和注释目的的消息。版本化快照代表静态训练数据,由准备训练数据集请求(PrepareTrainingDataset)产生,从所选提交列表转换而来。每个快照都与一个版本相关联;一旦构建了训练数据集,您可以使用该版本字符串随时获取相应的训练数据(FetchTrainingDataset)以供重用。图 2.13 可视化了数据集的内部存储结构。

图 2.13 内部数据集存储概述。数据集存储两种类型的数据:用于摄入原始数据的提交和用于训练数据集的版本快照。数据集元数据和数据 URL 存储在数据集管理服务中,实际数据存储在云对象存储服务中。

注意虽然不同类型的数据集的各个训练示例可以采用不同的形式,例如图像、音频和文本句子,但数据集的操作(创建、更新和查询数据集摘要)以及其动态/静态特性是相同的。由于我们在所有数据集类型上设计了统一的 API 集,我们可以使用统一的存储结构来存储所有不同类型的数据集。

在我们的存储中,实际文件(提交数据、快照数据)存储在云对象存储(如 Amazon S3)中,我们只在我们的 DM 系统中保留数据集元数据(稍后解释)。通过卸载文件存储工作并仅跟踪文件链接,我们可以专注于组织数据集并跟踪其元数据,例如编辑历史、数据统计、训练快照和所有权。

数据集元数据

我们将数据集元数据定义为除了实际数据文件以外的所有内容,例如数据集 ID、数据所有者、变更历史(审计)、训练快照、提交、数据统计等等。

为了演示目的,我们将数据集的元数据存储在一个内存字典中,以 ID 作为键,并将所有数据文件放入 MinIO 服务器。但您可以扩展它以使用数据库或 NoSQL 数据库来存储数据集的元数据。

到目前为止,我们已经讨论了数据集存储概念,但实际的数据集写入和读取是如何工作的呢?我们如何序列化不同数据集类型(例如GENERICTEXT_INTENT类型)的提交和快照?

在存储后端实现中,我们使用简单的继承概念来处理不同数据集类型的文件操作。我们定义了一个DatasetTransformer接口如下:ingest()函数将输入数据保存到内部存储作为提交,compress()函数将来自选定提交的数据合并为版本快照(训练数据)。

更具体地说,对于“TEXT_INTENT”类型的数据集,我们有IntentTextTransformer来应用文件转换的强类型模式。对于“GENERIC”类型的数据集,我们有GenericTransformer将数据保存在原始格式中,没有任何检查或格式转换。图 2.14 说明了这些。

图 2.14 实现DatasetTransformer接口来处理不同的数据集类型;实现 ingest 函数将原始输入数据保存为提交;实现 compress 函数将提交数据聚合为训练数据。

从图 2.14 可以看出,通过数据摄取 API(第 2.2.3 节)保存的原意图分类数据由IntentTextTransformer:Ingest()保存为提交;通过训练数据集提取 API(第 2.2.4 节)生成的意图分类训练数据由IntentTextTransformer:Compress()保存为版本化的快照。因为它们是纯 Java 代码,我们留给您自己去发现;您可以在我们的 Git 存储库中找到实现代码(org/orca3/miniAutoML/dataManagement/transformers/IntentTextTransformer.java)。

2.2.6 数据模式

到目前为止,我们已经看到了所有的 API、工作流程和内部存储结构。现在让我们来考虑 DM 服务中的数据是什么样子的。对于每一种强类型数据集,例如“TEXT_INTENT”数据集,我们定义了两种数据模式:一种用于数据摄取,一种用于训练数据提取(图 2.15)。

图 2.15 每一种类型的数据集都有两个数据模式:摄取数据模式和训练数据模式。这两个模式将确保我们接受的数据和我们生成的数据都遵循我们的数据规范。

图 2.15 显示了 DM 服务如何使用两个数据模式来实现其数据合同。步骤 1 使用摄取数据模式验证原始输入数据;步骤 2 使用训练数据模式将原始数据转换为训练数据格式;步骤 3 将转换后的数据保存为一个提交;步骤 4 在构建训练数据集时将选定的提交合并为一个版本化的快照,但仍然遵循训练数据模式。

这两个不同的数据模式是 DM 服务提供给我们两个不同用户(Jianguo 和 Julia)的数据合同。无论 Jianguo 如何收集数据,它都需要转换为摄入数据格式以插入到 DM 中。或者,由于 DM 保证输出的训练数据遵循训练数据模式,Julia 可以放心地使用数据集,而不用担心 Jianguo 所做的数据收集更改会影响到她。

一个数据摄入模式

我们已经了解了数据模式的概念,现在让我们来看看我们为TEXT_INTENT数据集定义的摄入数据模式:

>> TEXT_INTENT dataset ingestion data schema
<text utterance>, <label>,<label>,<label>, ...

为了简化起见,我们的数据摄入模式要求TEXT_INTENT数据集的所有输入数据必须以 CSV 文件格式提供。第一列是文本话语,其余列是标签。请参考以下示例 CSV 文件:

“I am still waiting on my credit card”, activate_my_card      ❶
➥ ;card_arrival                                              ❶
“I couldn’t purchase gas in Costco”, card_not_working

❶ 标签

训练数据集的模式

对于TEXT_INTENT训练数据,我们的模式将输出数据定义为一个包含两个文件的压缩文件:examples.csv 和 labels.csv。labels.csv 定义了标签名称到标签 ID 的映射,而 examples.csv 定义了训练文本(话语)到标签 ID 的映射。请参考以下示例:

examples.csv: <text utterance>, <label_id>,<label_id>, ...
“I am still waiting on my credit card”, 0;1
“I couldn’t purchase gas in Costco”, 2

Labels.csv: <label_id>, <label_name>
0, activate_my_card
1, card_arrival
2, card_not_working

为什么我们使用自定义的数据结构

我们使用自定义的数据模式来构建TEXT_INTENT,而不是使用 PyTorch 或 Tensorflow 数据集格式(如 TFRecordDataset)来创建与模型训练框架的抽象。

选择一个特定于框架的数据集格式,你的训练代码也需要用该框架编写,这并不理想。引入自定义的中间数据集格式可以使 DM 框架中立,因此不需要特定于框架的训练代码。

一个数据集中有两个强类型的数据模式的好处

通过在数据集中使用两个强类型的数据模式,并且让 DM 将数据从摄取的数据格式转换为训练数据格式,我们可以并行开发数据收集和训练代码开发。例如,当 Jianguo 想要向TEXT_INTENT数据集添加一个新特征——“文本语言”时,他可以与 DM 服务开发人员合作更新数据摄入模式以添加一个新的数据字段。

Julia 不会受到影响,因为训练数据模式没有改变。当 Julia 有带宽来消费她训练代码中的新功能时,她可能会后来向我们更新训练数据模式。关键是,Jianguo 和 Julia 不必同步工作来引入新的数据集增强;他们可以独立工作。

注意:为了简单起见和演示目的,我们选择使用 CSV 文件来存储数据。使用纯 CSV 文件的问题在于它们缺乏向后兼容性支持和数据类型验证支持。在生产环境中,我们建议使用 Parquet、Google protobuf 或 Avro 来定义数据模式和存储数据。它们带有一组用于数据验证、数据序列化和模式向后兼容性支持的库。

通用数据集:没有模式的数据集

尽管我们在多个地方强调定义强类型数据集模式对数据集管理服务是基础性的,但在这里我们将例外情况添加了一个自由格式的数据集类型——GENERIC数据集。与强类型 TEXT_ INENT 数据集不同,GENERIC类型数据集没有数据模式验证。我们的服务将任何原始输入数据保存原样,并在构建训练数据时,服务只是将所有原始数据按其原始格式打包到训练数据集中。

一个GENERIC数据集类型听起来可能不是一个好主意,因为我们基本上将来自上游数据源的任何数据传递给下游训练应用程序,这很容易破坏训练代码中的数据解析逻辑。这绝对不是一个生产选项,但它为实验项目提供了所需的灵活性。

尽管强类型数据模式提供了良好的数据类型安全保护,但需要付出维护的代价。当您不得不在 DM 服务中频繁进行模式更改以采用新的实验所需的新数据格式时,这是相当烦人的。

在深度学习项目开始时,有很多事情是不确定的,比如哪种深度学习算法效果最好,我们可以收集到什么样的数据,以及我们应该选择什么样的数据模式。为了解决所有这些不确定性,我们需要一种灵活的方式来处理任意数据,以启用模型训练实验。这就是GENERIC数据集类型设计的目的。

一旦业务价值得到证明,并选择了深度学习算法,我们现在清楚了训练数据的样子;然后是时候在数据集管理服务中定义一个强类型数据集了。在接下来的部分中,我们将讨论如何添加一个新的强类型数据集。

2.2.7 添加新的数据集类型(IMAGE_CLASS)

让我们假设有一天,朱莉娅(平台开发者之一)要求我们将她的实验性图像分类项目提升为正式项目。朱莉娅和她的团队正在使用 GENERIC 数据集开发图像分类模型,并且因为他们取得了良好的结果,现在他们想要定义一个强类型数据集(IMAGE_CLASS)来稳定原始数据收集和训练数据消费的数据模式。这将保护训练代码免受未来数据集更新的影响。

要添加一个新的数据集类型——IMAGE_CLASS,我们可以按照三个步骤进行。首先,我们必须定义训练数据格式。在与朱莉娅讨论后,我们决定由 FetchTrainingDataset API 生成的训练数据将是一个 zip 文件;它将包含以下三个文件:

>> examples.csv: <image filename>,<label id>
“imageA.jpg”, 0
“imageB.jpg”, 1
“imageC.jpg”, 0

>> labels.csv: <label id>,<label name>
0, cat
1, dog

>> examples/ - folder
imageA.jpg
imageB.jpg
imageC.jpg

examples.csv 和 labels.csv 文件是定义每个训练图像标签的清单文件。实际图像文件存储在 examples 文件夹中。

其次,定义摄取数据格式。我们需要与收集图像并为其标记标签的数据工程师建国讨论摄取数据架构。我们一致同意,每个 CreateDatasetUpdateDataset 请求的有效负载数据也是一个 zip 文件;其目录如下所示:zip 文件应该是只包含子目录的文件夹。根文件夹下的每个子目录代表一个标签;其下的图像属于此标签。子目录应只包含图像,而不包含任何嵌套目录:

├── cat
│   ├── catA.jpg
│   ├── catB.jpg
│   └── catC.jpg
└── dog
    ├── dogA.jpg
    ├── dogB.jpg
    └── dogC.jpg

最后一步是代码更改。在心中有两种数据模式之后,我们需要创建一个实现了 DatasetTransformer 接口的 ImageClassTransformer 类来构建数据读取和写入逻辑。

我们首先实现 ImageClassTransformer.ingest() 函数。逻辑需要使用第 2 步中定义的输入数据格式来解析数据集创建和更新请求中的输入数据,然后将输入数据转换为训练数据格式并将其保存为数据集的提交。

然后,我们实现 ImageClassTransformer.compress() 函数,它首先通过匹配数据过滤器选择提交,然后将匹配的提交合并为单个训练快照。最后一步,我们将 ImageClassTransformer.ingest() 函数注册到 DatasetIngestion.ingestion() 函数中,类型为 IMAGE_CLASS,并将 ImageClassTransformer.compress() 注册到 DatasetCompressor.run() 函数中,类型为 IMAGE_CLASS

如你所见,通过合适的数据集结构,我们只需添加几个新的代码片段就能支持新的数据集类型。现有的数据集类型和公共数据摄取及获取 API 不会受到影响。

2.2.8 服务设计总结

让我们回顾一下这个示例数据集管理服务是如何满足第 2.1.2 节介绍的五项设计原则的:

  • 原则 1—支持数据集可重现性。我们的示例 DM 服务将所有生成的训练数据保存为带有版本哈希字符串的版本化快照,用户可以随时应用该版本字符串来获取训练数据快照。

  • 原则 2—为不同的数据集类型提供统一的体验。数据摄取 API 和训练数据获取 API 对所有数据集类型和大小的工作方式相同。

  • 原则 3—采用强类型数据架构。我们的示例 TEXT_INENT 类型和 IMAGE_CLASS 类型数据集对原始摄取数据和训练数据都应用自定义数据架构。

  • 原则 4—确保 API 一致性并在内部处理扩展。尽管我们在示例代码中将所有数据集的元数据保存在内存中(为了简单起见),但我们可以轻松地在云对象存储中实现数据集存储结构;理论上,它具有无限的容量。此外,我们要求数据 URL 用于发送数据和返回数据,因此无论数据集有多大,我们的 API 都保持一致。

  • 原则 5—保证数据持久性。每个数据集创建请求和更新请求都会创建一个新的提交;每个训练数据准备请求都会创建一个版本化的快照。提交和快照都是不可变的,并且不受数据到期限制的持久存在。

注意 我们从示例数据集管理服务中删除了许多重要功能,以保持简单性。管理 API,例如允许您删除数据,还原数据提交以及查看数据审计历史记录。欢迎 fork 该存储库并尝试实现它们。

2.3 开源方法

如果您有兴趣采用开源方法来设置数据集管理功能,我们为您选择了两种方法:Delta Lake 和 Pachyderm。让我们分别来看看它们。

2.3.1 Delta Lake 和 Petastorm 与 Apache Spark 家族

在这种方法中,我们建议将数据保存在 Delta Lake 表中,并使用 Petastorm 库将表数据转换为 PyTorch 和 Tensorflow 数据集对象。数据集可以在训练代码中无缝消耗。

Delta Lake

Delta Lake 是一个存储层,为 Apache Spark 和其他云对象存储(例如 Amazon S3)带来可扩展的、ACID(原子性、一致性、隔离性、持久性)事务。Delta Lake 由 Databricks,一个备受尊敬的数据和人工智能公司,作为开源项目开发。

云存储服务,例如 Amazon S3,是 IT 行业中最具可扩展性和成本效益的存储系统之一。它们是构建大型数据仓库的理想场所,但其键值存储设计使得难以实现 ACID 事务和高性能。元数据操作(例如列出对象)昂贵,并且一致性保证有限。

Delta Lake 的设计旨在填补前面讨论的空白。它作为一个文件系统工作,将批处理和流处理数据存储在对象存储中(例如亚马逊 S3)。此外,Delta Lake 管理表结构和模式强制执行的元数据、缓存和索引。它提供了 ACID 属性、时间旅行和针对大型表格数据集的显著更快的元数据操作。请参见图 2.16 了解 Delta Lake 概念图。

图 2.16 Delta Lake 数据摄入和处理工作流程。流数据和批数据都可以保存为 Delta Lake 表,并且 Delta Lake 表存储在云对象存储中,例如亚马逊 S3。

Delta Lake 表是系统的核心概念。在使用 Delta Lake 时,您通常正在处理 Delta Lake 表。它们就像 SQL 表一样;您可以查询、插入、更新和合并表内容。Delta Lake 中的模式保护是其优势之一。它支持在表写入时对模式进行验证,从而防止数据污染。它还跟踪表历史,因此您可以将表回滚到其过去的任何阶段(称为时间旅行)。

对于构建数据处理管道,Delta Lake 建议将表命名为三个类别:铜(bronze)、银(silver)和金(gold)。首先,我们使用铜表存储来自不同来源的原始输入(其中一些可能不太干净)。然后,数据不断从铜表流向银表,经过数据清洗和转换(ETL)。最后,我们执行数据过滤和净化,并将结果保存到金表中。每个表都处于机器学习状态;它们是可重现的,并且类型安全。

为什么 Delta Lake 是深度学习数据集管理的良好选择

以下是使 Delta Lake 成为管理深度学习项目数据集的良好选择的三个功能。

首先,Delta Lake 支持数据集的可重现性。它具有“时间旅行”功能,可以使用数据版本控制查询数据在特定时间点的状态。想象一下,您已经设置了一个持续运行的 ETL 管道来保持您的训练数据集(gold table)的最新状态。因为 Delta Lake 将表更新跟踪为快照,每个操作都会被自动版本化,当管道写入数据集时。这意味着所有训练数据的快照都是免费的,您可以轻松浏览表更新历史并回滚到过去的阶段。以下列表提供了一些示例命令。

列表 2.12 Delta Lake 时间旅行命令

pathToTable = "/my/sample/text/intent/dataset/A"

deltaTable = DeltaTable.forPath(spark, pathToTable)    ❶
fullHistoryDF = deltaTable.history()                   ❷
lastOperationDF = deltaTable.history(1)                ❸

df = spark.read.format("delta")                        ❹
       .option("timestampAsOf", "2021-07-01")          ❹
       .load(pathToTable)                              ❹

df = spark.read.format("delta")                        ❺
      .option("versionAsOf", "12")                     ❺
      .load(pathToTable)                               ❺

❶ 在 Delta Lake 中查找数据集

❷ 列出数据的完整历史

❸ 获取数据集上的最后一个操作

❹ 根据时间戳回滚数据集

❺ 根据版本回滚数据集

其次,Delta Lake 支持持续的流式数据处理。它的表可以无缝处理来自历史和实时流数据源的连续数据流。例如,数据管道或流数据源可以在查询数据表的同时不断向 Delta Lake 表中添加数据。这样可以节省编写代码将新数据与现有数据合并的额外步骤。

其次,Delta Lake 提供模式强制执行和演化功能。它在写入时应用模式验证。它将确保新的数据记录与表的预定义模式匹配;如果新数据与表的模式不兼容,Delta Lake 将引发异常。在写入时进行数据类型验证要比在读取时进行更好,因为如果数据被污染,清理数据将变得困难。

除了强大的模式强制执行功能之外,Delta Lake 还允许您在现有数据表中添加新列而不会引起破坏性更改。对于深度学习项目来说,数据集模式强制执行和调整(演化)能力至关重要。这些功能可以保护训练数据免受意外数据写入污染,并提供安全的数据更新。

Petastorm

Petastorm 是 Uber ATG(高级技术组)开发的开源数据访问库。它可以直接从 Apache Parquet 格式的数据集中单机或分布式进行深度学习模型的训练和评估。(Apache Parquet 是一种为高效数据存储和检索而设计的数据文件格式。)

Petastorm 可以轻松地将 Delta Lake 表转换为 Tensorflow 和 PyTorch 格式的数据集,并且还支持分布式训练数据分区。使用 Petastorm,可以简单地从 Delta Lake 表中消耗训练数据,而不必担心特定训练框架的数据转换细节。它还在数据集格式和训练框架(Tensorflow、PyTorch 和 PySpark)之间创建了良好的隔离。图 2.17 可视化了数据转换过程。

图 2.17 Petastorm 将 Delta Lake 表转换为可以被 PyTorch 或 Tensorflow 框架读取的数据集。

图 2.17 描述了 Petastorm 的数据转换工作流。您可以创建一个 Petastorm spark 转换器,将 Delta Lake 表作为 Parquet 文件读取到其缓存中,并生成 Tensorflow 或 Pytorch 数据集。

例子:为花朵图像分类准备训练数据

现在我们对 Delta Lake 和 Petastorm 有了一个大致的了解,让我们看一个具体的模型训练示例。接下来的代码片段——代码列表 2.13 和 2.14——展示了一个端到端的图像分类模型训练工作流程的两个步骤。首先,它们定义了一个图像处理 ETL 管道,将一组图像文件解析为 Delta Lake 表作为图像数据集。然后,它们使用 Petastorm 将 Delta Lake 表转换为可以直接加载到 PyTorch 框架中进行模型训练的数据集。

让我们首先查看代码清单 2.13 中的四步 ETL 数据处理管道。您还可以在 mng.bz/JVPz 找到完整的代码。

在管道的开始步骤中,我们将图像从文件夹 flower_photos 加载到 Spark 中作为二进制文件。其次,我们定义提取函数以从每个图像文件中获取元数据,如标签名称、文件大小和图像大小。第三,我们使用提取函数构建数据处理管道,然后将图像文件传递给管道,管道将生成一个数据框。数据框的每一行代表一个图像文件及其元数据,包括文件内容、标签名称、图像大小和文件路径。在最后一步中,我们将此数据框保存为 Delta Lake 表—gold_table_training_dataset。您还可以在以下代码清单的末尾查看此 Delta Lake 表的数据模式。

清单 2.13 Delta Lake 中创建图像数据集的 ETL

## Step 1: load all raw images files
path_labeled_rawdata = “datacollablab/flower_photos/”

images = spark.read.format("binary")                     ❶
 .option("recursiveFileLookup", "true")                  ❶
 .option("pathGlobFilter", "*.jpg")                      ❶
 .load(path_labeled_rawdata)                             ❶
 .repartition(4)                                         ❶

## Step 2: define ETL extract functions
def extract_label(path_col):                             ❷
 """Extract label from file path using built-in SQL functions."""
 return regexp_extract(path_col, "flower_photos/([^/]+)", 1)

def extract_size(content):                               ❸
 """Extract image size from its raw content."""
 image = Image.open(io.BytesIO(content))
 return image.size 

@pandas_udf("width: int, height: int")
def extract_size_udf(content_series):                    ❸
 sizes = content_series.apply(extract_size)
 return pd.DataFrame(list(sizes))
## Step 3: construct and execute ETL to generate a data frame 
## contains label, image, image size and path for each image. 
df = images.select(
 col("path"),
 extract_size_udf(col("content")).alias("size"),
 extract_label(col("path")).alias("label"),
 col("content"))

## Step 4: save the image dataframe produced 
# by ETL to a Delta Lake table
gold_table_training_dataset = “datacollablab.flower_train_binary”
spark.conf.set("spark.sql.parquet.compression.codec", "uncompressed")
df.write.format(“delta”).mode(“overwrite”)
  .saveAsTable(gold_table_training_dataset)

>>> 
ColumnName: path: string                                ❹
ColumnName: label: string                               ❹
ColumnName: labelIndex: bigint                          ❹
ColumnName: size: struct<width:int, length:int>         ❹
ColumnName: content: binary                             ❹

❶ 将图像作为 binaryFile 读取

❷ 从图像的子目录名称提取标签

❸ 提取图像尺寸

❹ Delta Lake 表—gold_table_training_dataset 的数据模式

注意:演示中使用的原始数据是 TensorFlow 团队的 flowers 数据集。它包含存储在五个子目录下的花朵照片,每个子目录对应一个类别。子目录名称是其中包含的图像的标签名称。

现在我们在 Delta Lake 表中构建了一个图像数据集,我们可以开始使用 Petastorm 的帮助来训练一个 PyTorch 模型。在代码清单 2.14 中,我们首先读取由代码清单 2.13 中定义的 ETL 管道生成的 Delta Lake 表 gold_table_training_dataset,然后将数据分成两个数据框架:一个用于训练,一个用于验证。接下来,我们将这两个数据框加载到两个 Petastorm Spark 转换器中;数据将在转换器内转换为 Parquet 文件。最后,我们使用 Petastorm API make_torch_dataloader 从 PyTorch 中读取训练示例以进行模型训练。请参阅以下代码以了解整个三步过程。您还可以在以下链接找到完整的示例代码:mng.bz/wy4B

清单 2.14 使用 Petastorm 在 PyTorch 中消耗 Delta Lake 图像数据集

## Step 1: Read dataframe from Delta Lake table
df = spark.read.format("delta")
  .load(gold_table_training_dataset) 
 .select(col("content"), col("label_index")) 
 .limit(100)
num_classes = df.select("label_index").distinct().count()

df_train, df_val = df                                         ❶
  .randomSplit([0.9, 0.1], seed=12345)                        ❶

## (2) Load dataframes into Petastorm converter 
spark.conf.set(SparkDatasetConverter.PARENT_CACHE_DIR_URL_CONF,  
  "file:///dbfs/tmp/petastorm/cache")
converter_train = make_spark_converter(df_train)
converter_val = make_spark_converter(df_val)

## (3) Read training data in PyTorch by using 
## Petastorm converter
def train_and_evaluate(lr=0.001):
 device = torch.device("cuda")
  model = get_model(lr=lr)
    .. .. .. 

  with converter_train.make_torch_dataloader(                 ❷
         transform_spec=get_transform_spec(is_train=True),
         batch_size=BATCH_SIZE) as train_dataloader,
 converter_val.make_torch_dataloader(                   ❷
         transform_spec=get_transform_spec(is_train=False),
         batch_size=BATCH_SIZE) as val_dataloader:

 train_dataloader_iter = iter(train_dataloader)
   steps_per_epoch = len(converter_train) // BATCH_SIZE

 val_dataloader_iter = iter(val_dataloader)
   validation_steps = max(1, len(converter_val) // BATCH_SIZE)

   for epoch in range(NUM_EPOCHS):
     .. .. 
     train_loss, train_acc = train_one_epoch(
        model, criterion, optimizer,  
        exp_lr_scheduler,
 train_dataloader_iter,                                ❸
        steps_per_epoch, epoch,device)

     val_loss, val_acc = evaluate(
        model, criterion, 
 val_dataloader_iter,                                  ❸
        validation_steps, device)
 return val_loss

❶ 将 Delta Lake 表数据分成两个数据框:训练和验证

❷ 使用 Petastorm 转换器创建 PyTorch 数据加载器进行训练和评估

❸ 在训练迭代中使用训练数据

何时使用 Delta Lake

有关 Delta Lake 的一个普遍误解是,它只能处理结构化文本数据,如销售记录和用户配置文件。但前面的示例表明它也可以处理像图像和音频文件这样的非结构化数据;您可以将文件内容作为字节列写入带有其他文件属性的表中,并从中构建数据集。

如果您已经使用 Apache Spark 构建数据管道,那么 Delta Lake 是进行数据集管理的绝佳选择;它支持结构化和非结构化数据。而且它也是经济实惠的,因为 Delta Lake 将数据存储在云对象存储中(例如 Amazon S3、Azure Blob),Delta Lake 的数据架构强制执行和实时数据更新表支持机制简化了您的 ETL 管道的开发和维护。最重要的是,时间旅行功能会自动跟踪所有表更新,因此您可以放心地进行数据更改并回滚到以前版本的训练数据集。

Delta Lake 的局限性

使用 Delta Lake 的最大风险是技术锁定和其陡峭的学习曲线。Delta Lake 将表存储在其自己的机制中:基于 Parquet 的存储、事务日志和索引的组合,这意味着它只能由 Delta 集群写入/读取。您需要使用 Delta ACID API 进行数据摄入和使用 Delta JDBC 运行查询;因此,如果将来决定远离 Delta Lake,数据迁移成本将会很高。另外,由于 Delta Lake 需要与 Spark 配合使用,如果您是 Spark 的新手,那么您将需要大量的学习时间。

关于数据摄入性能,Delta Lake 将数据存储到底层的云对象存储中,当使用对象存储操作(例如表创建和保存)时,很难实现低延迟流式处理(毫秒级)。此外,Delta Lake 需要为每个 ACID 事务更新索引;与一些执行仅追加数据写入的 ETL 相比,它还引入了延迟。但在我们看来,深度学习项目的数据摄入延迟在秒级并不是一个问题。如果您不熟悉 Spark,并且不想要设置 Spark 和 Delta Lake 集群的重任,我们为您提供了另一种轻量级方法——Pachyderm。

2.3.2 使用云对象存储的 Pachyderm

在这一部分,我们想提出一个基于轻量级 Kubernetes 的工具——Pachyderm——来处理数据集管理。我们将向您展示如何使用 Pachyderm 来完成图像数据处理和标记的两个示例。但在此之前,让我们先了解一下 Pachyderm 是什么。

Pachyderm

Pachyderm 是一个用于构建版本控制、自动化、端到端数据科学数据管道的工具。它运行在 Kubernetes 上,并由您选择的对象存储支持(例如 Amazon S3)。您可以编写自己的 Docker 镜像用于数据爬取、摄入、清理、整理和处理,并使用 Pachyderm 管道将它们链接在一起。一旦定义了管道,Pachyderm 将处理管道的调度、执行和扩展。

Pachyderm 提供了数据集版本控制和来源追溯(数据血统)管理。它将每个数据更新(创建、写入、删除等)视为一个提交,并且还跟踪生成数据更新的数据源。因此,您不仅可以查看数据集的变更历史,还可以将数据集回滚到过去的版本,并查找更改的数据来源。图 2.18 展示了 Pachyderm 的工作原理的高级视图。

图 2.18 Pachyderm 平台由两种对象组成——管道和版本控制数据。管道是计算组件,数据是版本控制基元。在“原始数据集”中的数据更改可能会触发管道作业,以处理新数据并将结果保存到“成熟数据集”中。

在 Pachyderm 中,数据以 Git 风格进行版本控制。每个数据集在 Pachyderm 中都是一个仓库(repo),它是最高级别的数据对象。一个仓库包含提交、文件和分支。Pachyderm 仅在内部保留元数据(例如审计历史和分支),并将实际文件存储在云对象存储中。

Pachyderm 管道执行各种数据转换。管道执行用户定义的代码片段,例如一个 Docker 容器,以执行操作并处理数据。每个执行都称为一个作业。清单 2.15 显示了一个简单的管道定义。这个“edges”管道监视一个“images”数据集。当在图像数据集中添加了新的图像时,管道将启动一个作业,运行 "pachyderm/opencv" Docker 镜像解析图像,并将其边缘图片保存到 edges 数据集中。

清单 2.15 Pachyderm 管道定义

{
  "pipeline": {
    "name": "edges"        ❶
  },
  "description": "A pipeline that performs image \
     edge detection by using the OpenCV library.",
  "transform": {
    "cmd": [ "python3", "/edges.py" ],
    "image": "pachyderm/opencv"
  },
  "input": {
    "pfs": {
      "repo": "images",    ❷
      "glob": "/*"
    }
  }
}

❶ 一个 Pachyderm 管道

❷ 一个 Pachyderm 数据集

版本和数据来源

在 Pachyderm 中,对数据集和管道所做的任何更改都会自动进行版本管理,您可以使用 Pachyderm 命令工具 pachctl 连接到 Pachyderm 工作区,查看文件历史记录,甚至还可以回滚这些更改。查看以下示例,了解如何使用 pachctl 命令来查看 edges 数据集的变更历史和变更来源。首先,我们运行 pachctllist 命令来列出 edges 数据集的所有提交。在我们的示例中,对 edges 数据集进行了三次变更(提交):

$ pachctl list commit edges #A
REPO  BRANCH COMMIT                           FINISHED
edges master 0547b62a0b6643adb370e80dc5edde9f 3 minutes ago 
edges master eb58294a976347abaf06e35fe3b0da5b 3 minutes ago 
edges master f06bc52089714f7aa5421f8877f93b9c 7 minutes ago 

要获取数据更改的来源,我们可以使用 pachctl inspect 命令来检查提交情况。例如,我们可以使用以下命令来检查提交的数据来源。

“eb58294a976347abaf06e35fe3b0da5b”. 
$ pachctl inspect commit edges@eb58294a976347abaf06e35fe3b0da5b \
       --full-timestamps

从以下回应中,我们可以看到 edges 数据集的提交 eb58294a976347abaf06e35fe3b0da5b 是由 images 数据集的提交 66f4ff89a017412090dc4a542d9b1142 计算得出的:

Commit: edges@eb58294a976347abaf06e35fe3b0da5b
Original Branch: master
Parent: f06bc52089714f7aa5421f8877f93b9c
Started: 2021-07-19T05:04:23.652132677Z
Finished: 2021-07-19T05:04:26.867797209Z
Size: 59.38KiB
Provenance:  __spec__@91da2f82607b4c40911d48be99fd3031 (edges)  ❶
images@66f4ff89a017412090dc4a542d9b1142 (master)                ❶

❶ 数据来源

数据来源功能非常适用于数据集的可重现性和故障排除,因为您始终可以找到过去使用的确切数据以及创建它的数据处理代码。

示例:使用 Pachyderm 对图像数据集进行标注和训练

看完了 Pachyderm 是如何工作的,现在让我们看一个设计提案,使用 Pachyderm 来构建一个自动化目标检测训练管道。对于目标检测模型训练,我们首先需要通过在每个图像上用一个边界框标记目标对象来准备训练数据集,然后将数据集——边界框标签文件和图像——发送给训练代码开始模型训练。图 2.19 展示了使用 Pachyderm 自动化这一工作流程的过程。

图 2.19

图 2.19 在 Pachyderm 中自动化的目标检测模型训练。当标记了新图像时,训练过程会自动开始。

在此设计中,我们使用了两个流水线,标记流水线和训练流水线,以及两个数据集来构建这个训练工作流程。在第 1 步,我们将图像文件上传到“原始图像数据集”。在第 2 步中,我们启动标记流水线,启动一个标记应用程序,为用户打开一个 UI 界面,通过在图像上绘制边界框来标记对象;这些图像是从原始图像数据集中读取的。一旦用户完成了标记工作,图像和生成的标签数据将被保存到“标记数据集”。在第 3 步中,我们向已标记的数据集添加新的训练数据,这将触发训练流水线启动训练容器并开始模型训练。在第 4 步中,我们保存模型文件。

除了自动化之外,包括原始图像数据集、标记数据集和模型文件在内的所有数据都会被 Pachyderm 自动进行版本控制。此外,通过利用数据溯源功能,我们可以确定任何给定模型文件使用的标记数据集的版本,以及用于训练此训练数据的原始图像数据集的版本。

什么时候使用 Pachyderm

Pachyderm 是一个轻量级的方法,帮助您轻松构建数据工程流水线,并提供类似 Git 的数据版本支持。它以数据科学家为中心,易于使用。Pachyderm 基于 Kubernetes,并使用云对象存储作为数据存储,因此对于小团队来说成本效益高,设置简单,易于维护。我们建议数据科学团队拥有自己基础设施的情况下使用 Pachyderm,而不要使用 Spark。Pachyderm 在处理非结构化数据(如图像和音频文件)方面表现非常出色。

Pachyderm 的局限性

Pachyderm 缺少的是模式保护和数据分析效率。Pachyderm 将所有东西都视为文件;它为每个文件版本保留快照,但不关心文件内容。在数据写入或读取时没有数据类型验证;它完全依赖于管道来保护数据一致性。

缺乏模式意识和保护为任何持续运行的深度学习训练流水线引入了很多风险,因为在上游数据处理代码中做任何代码更改可能会破坏下游数据处理或训练代码。此外,没有了解数据的模式,实现数据集比较就变得很困难。

总结

  • 数据集管理的主要目标是持续从各种数据源接收新鲜数据,并在支持训练可重现性(数据版本跟踪)的同时,向模型训练交付数据集。

  • 拥有数据集管理组件可以通过将模型算法开发和数据工程开发并行化来加速深度学习项目的开发。

  • 设计数据集管理服务的原则如下:支持数据集可重现性;采用强类型数据模式;设计统一的 API,并保持 API 在不同数据集类型和大小上的一致行为;保证数据持久性。

  • 数据集管理系统至少应支持(训练)数据集版本控制,这对模型的可重现性和性能故障排除至关重要。

  • 数据集是深度学习任务的逻辑文件组;从模型训练的角度来看是静态的,从数据收集的角度来看是动态的。

  • 示例数据集管理服务由三层组成——数据摄入层、内部数据集存储层和训练数据集获取层。

  • 在示例数据集管理服务中,我们为每种数据集类型定义了两种数据模式,一种用于数据摄入,一种用于数据集获取。每次数据更新都被存储为一个提交,而每个训练数据集都被存储为一个带版本的快照。用户可以使用版本哈希字符串随时获取相关的训练数据(数据集可重现性)。

  • 示例数据集管理服务支持一种特殊的数据集类型——GENERIC数据集。GENERIC数据集没有模式和数据验证,并且用户可以自由上传和下载数据,因此非常适合原型化新算法。一旦训练代码和数据集要求变得成熟,数据集格式就可以升级为强类型数据集。

  • Delta Lake 和 Petastorm 可以共同用于为基于 Spark 的深度学习项目建立数据集管理服务。

  • Pachyderm 是一个基于 Kubernetes 的轻量级数据平台,支持类似 Git 的数据版本控制,并允许轻松设置流水线。流水线由 Docker 容器组成;它可以用于自动化数据处理工作流程和深度学习项目的训练工作流程。

第三章:模型训练服务

这一章涵盖了

  • 建立训练服务的设计原则

  • 解释深度学习训练代码模式

  • 参观一个示例训练服务

  • 使用开源训练服务,如 Kubeflow

  • 决定何时使用公共云训练服务

机器学习中的模型训练任务不是研究人员和数据科学家的专属责任。是的,他们对算法的训练工作至关重要,因为他们定义了模型架构和训练计划。但就像物理学家需要一个软件系统来控制电子-正电子对撞机来测试他们的粒子理论一样,数据科学家需要一个有效的软件系统来管理昂贵的计算资源,如 GPU、CPU 和内存,以执行训练代码。这个管理计算资源和执行训练代码的系统被称为模型训练服务

构建高质量的模型不仅取决于训练算法,还取决于计算资源和执行训练的系统。一个好的训练服务可以使模型训练速度更快、更可靠,同时还可以降低平均模型构建成本。当数据集或模型架构非常庞大时,使用训练服务来管理分布式计算是你唯一的选择。

在这一章中,我们首先考察了训练服务的价值主张和设计原则,然后我们遇到了我们的示例训练服务。这个示例服务不仅向你展示了如何将设计原则应用于实践,还教你训练服务如何与任意训练代码交互。接下来,我们介绍了几个开源训练应用程序,你可以用它们快速建立自己的训练服务。最后,我们讨论了何时使用公共云训练系统。

本章重点讨论了从软件工程师的角度而不是数据科学家的角度设计和构建有效的训练服务。因此,我们不希望你熟悉任何深度学习理论或框架。第 3.2 节关于深度学习算法代码模式,是你理解本章训练代码所需的全部准备工作。训练代码并不是我们在这里的主要关注点;我们只是为了演示目的而编写了它,所以我们有东西可以演示示例训练服务。

模型训练的话题经常让工程师感到害怕。一个常见的误解是,模型训练全部都是关于训练算法和研究的。通过阅读这一章,我希望你不仅能学会如何设计和构建训练服务,还能吸收到这样一条信息:模型训练的成功建立在两个支柱上,即算法和系统工程。组织中的模型训练活动如果没有良好的训练系统,就无法扩展。因此,作为软件工程师,我们有很多可以为这个领域做出的贡献。

3.1 模型训练服务:设计概述

在企业环境中,深度学习模型训练涉及两种角色:开发模型训练算法的数据科学家(使用 TensorFlow、PyTorch 或其他框架),以及构建和维护在远程和共享服务器群中运行模型训练代码的系统的平台工程师。我们称这个系统为模型训练服务。

模型训练服务作为一个训练基础设施,在专用环境中执行模型训练代码(算法);它处理训练作业调度和计算资源管理。图 3.1 展示了一个高级工作流程,其中模型训练服务运行模型训练代码以生成一个模型。

图 3.1 通过训练服务执行模型训练的高级工作流程。在步骤 1 中,数据科学家向训练服务提交带有训练代码的训练请求,该服务将在作业队列中创建一个作业。在步骤 2 中,模型训练服务分配计算资源来执行训练作业(训练代码)。在步骤 3 中,当训练执行完成时,作业产生一个模型。

关于这个组件最常见的问题是为什么我们需要编写一个服务来进行模型训练。对许多人来说,似乎更容易编写一个简单的 bash 脚本在本地或远程执行训练代码(算法),比如在 Amazon 弹性云计算(Amazon EC2)实例上。然而,构建训练服务的理由不仅仅是启动训练计算。我们将在下一节详细讨论它。

3.1.1 为什么使用模型训练的服务?

想象一下,你领导一个数据科学团队,你需要明智地为团队成员 Alex、Bob 和 Kevin 分配团队宝贵的计算资源。计算资源需要以一种所有团队成员都能在时间限制和预算内完成他们的模型训练任务的方式分配。图 3.2 展示了分配计算资源的两种方法:专用和共享。

图 3.2 不同的计算资源分配策略:专用 vs. 共享

第一个选项,专用,是为每个团队成员独家分配一台强大的工作站。这是最简单的方法,但显然不是经济的,因为当 Alex 不运行他的训练代码时,他的服务器处于空闲状态,Bob 和 Kevin 都无法使用它。因此,在这种方法中,我们的预算被低效利用。

专用方法的另一个问题是它无法扩展。当 Alex 需要训练一个大模型或者一个数据集庞大的模型时,他将需要多台机器。而且训练机器通常很昂贵;由于深度学习模型架构的复杂性,即使是一个体量适中的神经网络也需要具有较大内存的 GPU。在这种情况下,我们必须为 Alex 分配更多专用服务器,这加剧了资源分配效率低下的问题。

第二个选项,共享计算资源,是建立一个弹性服务器组(组大小可调整)并与所有成员共享。这种方法显然更经济,因为我们使用更少的服务器来实现相同的结果,从而最大化了我们的资源利用率。

选择共享策略并不是一个困难的决定,因为它大大降低了我们训练集群的成本。但是共享方法需要适当的管理,例如如果突然出现大量的训练请求,则排队用户请求,监控每个训练执行并在必要时进行干预(重新启动或中止)(训练进度停滞),并根据实时系统使用情况扩展或缩减我们的集群。

脚本与服务

现在让我们重新审视之前关于脚本与服务的讨论。在模型训练的背景下,训练脚本 是指使用 shell 脚本在共享服务器集群中编排不同的训练活动。训练服务是一个远程过程,它通过 HTTP(超文本传输协议)或 gRPC(gRPC 远程过程调用)进行网络通信。作为数据科学家,Alex 和 Bob 向服务发送训练请求,而服务则编排这些请求并管理共享服务器上的训练执行。

使用脚本方法可能适用于单人场景,但在共享资源环境中会变得困难。除了执行训练代码之外,我们还需要关注其他重要因素,比如设置环境、确保数据合规性以及排除模型性能问题。例如,环境设置要求在开始模型训练之前,在训练服务器上正确安装训练框架和训练代码的库依赖项。数据合规性要求对敏感的训练数据(用户信用卡号、支付记录)进行受限访问的保护。性能故障排除要求对训练中使用的所有内容进行跟踪,包括数据集 ID 和版本、训练代码版本以及超参数,以便进行模型再现。

很难想象用 shell 脚本解决这些要求,并且以可靠、可重复和可扩展的方式执行模型训练。这就是为什么如今大多数在生产中训练的模型都是通过深思熟虑设计的模型训练服务生成的原因。

模型训练服务的好处

从前面的讨论中,我们可以想象一个模型训练服务的价值主张如下:

  • 饱和计算资源并降低模型训练成本

  • 通过以快速(可用资源更多)和可靠的方式构建模型来加快模型开发

  • 通过在受限环境中执行训练来强制执行数据合规性

  • 促进模型性能故障排除

3.1.2 模型训练服务设计原则

在查看我们的示例训练服务之前,让我们看一下可以用来评估模型训练系统的四个设计原则。

原则 1:提供统一的 API,不关心实际的训练代码

只有一个公共 API 来训练不同种类的训练算法使得训练服务易于使用。无论是目标检测训练、语音识别训练还是文本意图分类训练,我们都可以使用示例 API 触发模型训练执行。未来算法性能的 A/B 测试也可以通过一个单一的训练 API 轻松实现。

训练代码不易装配意味着训练服务定义了一种执行训练算法(代码)的清晰机制或协议。例如,它确定了服务如何将变量传递给训练代码/进程,训练代码如何获取训练数据集,以及训练后的模型和指标上传到何处。只要训练代码遵循这个协议,无论它是如何实现的、其模型架构是什么或使用哪些训练库,都不会有任何问题。

原则 2:构建具有高性能和低成本的模型

一个好的训练服务应该将成本效益作为优先考虑。成本效益可以提供缩短模型训练执行时间和提高计算资源利用率的方法。例如,一种现代的训练服务可以通过支持各种分布式训练方法、提供良好的作业调度管理来饱和服务器群,以及在训练过程偏离原始计划时向用户发出警报,从而降低时间和硬件成本。

原则 3:支持模型可重现性

一个服务如果给出相同的输入应该会产生相同的模型。这不仅对调试和性能故障排除很重要,而且还建立了系统的可信度。记住,我们将根据模型预测结果构建业务逻辑。例如,我们可以使用分类模型来预测用户的信用并根据此作出贷款批准决策。除非我们能够反复产生相同质量的模型,否则就无法信任整个贷款批准申请。

原则 4:支持鲁棒、隔离和弹性计算管理

现代深度学习模型,如语言理解模型,需要很长时间的训练(超过一周)。如果训练过程在中途被中断或因某些随机操作系统故障而中止,所有的时间和计算费用都会白白浪费。一个成熟的训练服务应该处理训练工作的鲁棒性(故障转移、故障恢复)、资源隔离和弹性资源管理(能够调整资源数量),以确保其训练作业可以在各种情况下成功完成执行。

在讨论了所有重要的抽象概念之后,让我们来解决如何设计和构建模型训练服务。在接下来的两节中,我们将学习深度学习代码的一般代码模式以及模型训练服务的示例。

3.2 深度学习训练代码模式

深度学习算法可能对工程师来说复杂且常常令人望而生畏。幸运的是,作为设计深度学习系统平台的软件工程师,我们不需要掌握这些算法来进行日常工作。但是,我们需要熟悉这些算法的一般代码模式。通过对模型训练代码模式的高层次理解,我们可以将模型训练代码视为黑盒子。在本节中,我们将向您介绍一般模式。

3.2.1 模型训练工作流程

简而言之,大多数深度学习模型通过迭代学习过程进行训练。该过程在许多迭代中重复相同的计算步骤,并且在每次迭代中,它试图更新神经网络的权重和偏差,以使算法输出(预测结果)更接近数据集中的训练目标。

为了衡量神经网络模拟给定数据的能力并使用它来更新神经网络的权重以获得更好的结果,定义了一个损失函数来计算算法输出与实际结果之间的偏差。损失函数的输出称为 LOSS。

因此,您可以将整个迭代训练过程视为不断努力减少损失值。最终,当损失值达到我们的训练目标或无法进一步减少时,训练完成。训练输出是神经网络及其权重,但我们通常简称为模型。

图 3.3 深度学习模型训练工作流程的一般步骤

图 3.3 说明了一般的模型训练步骤。由于神经网络由于内存限制无法一次性加载整个数据集,因此我们通常在训练开始之前将数据集重新分组成小批量(mini-batches)。在步骤 1 中,将小批量示例馈送到神经网络中,并且网络计算每个示例的预测结果。在步骤 2 中,我们将预测结果和期望值(训练标签)传递给损失函数以计算损失值,该损失值表示当前学习与目标数据模式之间的偏差。在步骤 3 中,一个称为反向传播的过程计算出每个神经网络参数的梯度与损失值。这些梯度用于更新模型参数,以便模型可以在下一个训练循环中获得更好的预测准确性。在步骤 4 中,选择的优化算法(如随机梯度下降及其变种)更新神经网络的参数(权重和偏差)。梯度(来自步骤 3)和学习率是优化算法的输入参数。模型更新步骤后,模型准确性应该会提高。最后,在步骤 5 中,训练完成,网络及其参数保存为最终模型文件。训练在以下两种情况下完成:完成预期的训练运行或达到预期的模型准确度。

尽管有不同类型的模型架构,包括循环神经网络(RNNs)、卷积神经网络(CNNs)和自编码器,但它们的模型训练过程都遵循相同的模式;只有模型网络不同。此外,将模型训练代码抽象为先前重复的一般步骤是进行分布式训练的基础。这是因为,无论模型架构如何不同,我们都可以使用共同的训练策略对它们进行训练。我们将在下一章详细讨论分布式训练。

3.2.2:将模型训练代码 Docker 化为黑匣子

在之前讨论的训练模式的基础上,我们可以将深度学习训练代码视为一个黑匣子。无论训练代码实现了什么样的模型架构和训练算法,我们都可以在训练服务中以相同的方式执行它。为了在训练集群中的任何位置运行训练代码并为每个训练执行创建隔离,我们可以将训练代码及其依赖库打包到一个 Docker 镜像中,并将其作为容器运行(见图 3.4)。

图 3.4:一个训练服务启动一个 Docker 容器来执行模型训练,而不是直接运行训练代码作为一个进程。

在图 3.4 中,通过将训练代码 Docker 化,训练服务可以通过简单地启动一个 Docker 容器来执行模型训练。因为服务对容器内部的内容无所知,所以训练服务可以以这种标准方法执行所有不同的代码。这比让训练服务生成一个进程来执行模型训练要简单得多,因为训练服务需要为每个训练代码设置各种环境和依赖包。Docker 化的另一个好处是它将训练服务和训练代码解耦,这使得数据科学家和平台工程师可以分别专注于模型算法开发和训练执行性能。

如果你对训练服务如何与训练代码通信而对彼此不知情感到奇怪,那么关键在于定义通信协议;该协议界定了训练服务传递给训练代码的参数及其数据格式。这些参数包括数据集、超参数、模型保存位置、指标保存位置等等。我们将在下一节看到一个具体的例子。

3.3 一个样本模型训练服务

如今我们知道,大多数深度学习训练代码遵循相同的模式(图 3.3),它们可以以统一的方式进行 Docker 化和执行(图 3.4)。让我们仔细看一个具体的例子。

为了演示我们迄今介绍的概念和设计原则,我们构建了一个示例服务,实现了模型训练的基本生产场景——接收训练请求,在 Docker 容器中启动训练执行,并跟踪其执行进度。虽然这些场景相当简单——几百行代码——但它们展示了我们在前几节中讨论的关键概念,包括使用统一的 API、Docker 化的训练代码以及训练服务和训练容器之间的通信协议。

为了清晰地展示关键部分,该服务以精简的方式构建。模型训练元数据(如运行任务和等待任务)被跟踪在内存中而不是数据库中,并且训练任务直接在本地 Docker 引擎中执行。通过删除许多中间层,您将直接查看到两个关键区域:训练任务管理和训练服务与训练代码(Docker 容器)之间的通信。

3.3.1 与服务交互

在我们看服务设计和实现之前,让我们看看我们如何操作它。

请按照 GitHub 说明运行此实验。我们仅强调了运行示例服务的主要步骤和关键命令,以避免冗长的代码页面和执行输出,以便清晰地演示概念。要运行此实验,请按照 orca3/MiniAutoML Git 存储库中的“单个训练器演示”文档 (training-service/single_trainer_demo.md) 中的说明操作,该文档还包含所需的输出。

首先,我们使用 scripts/ts-001-start-server.sh 启动服务:

docker build -t orca3/services:latest -f services.dockerfile .
docker run --name training-service -v 
  ➥ /var/run/docker.sock:/var/run/docker.sock 
  ➥ --network orca3 --rm -d -p "${TS_PORT}":51001
  ➥ orca3/services:latest training-service.jar

在启动训练服务 Docker 容器后,我们可以发送一个 gRPC 请求来启动模型训练执行(scripts/ts-002-start-run.sh <dataset id>)。请参见以下示例 gRPC 请求。

图 3.1 调用训练服务 API:提交训练作业

grpcurl -plaintext 
 -d "{
  "metadata": 
    { "algorithm":"intent-classification",    ❶
      "dataset_id":"1",
      "name":"test1",
      "train_data_version_hash":"hashBA==",   ❷
      "parameters":                           ❸
        {"LR":"4","EPOCHS":"15",
         "BATCH_SIZE":"64",
         "FC_SIZE":"128"}}
}" 
"${TS_SERVER}":"${TS_PORT}" 
training.TrainingService/Train

❶ 训练算法;也是训练 Docker 镜像的名称

❷ 训练数据集的版本哈希值。

❸ 训练超参数

一旦作业成功提交,我们就可以使用从 train API 返回的作业 ID 来查询训练执行的进度(scripts/ts-003-check-run.sh <job id>);请参见以下示例:

grpcurl -plaintext \
 -d "{"job_id\": "$job_id"}" \     ❶
"${TS_SERVER}":"${TS_PORT}" 
training.TrainingService/GetTrainingStatus

❶ 使用由训练 API 返回的作业 ID。

正如您所见,通过调用两个 gRPC API,我们可以启动深度学习训练并跟踪其进度。现在,让我们来看看这个示例训练服务的设计和实现。

注意 如果您遇到任何问题,请查看附录 A。A.2 节的脚本自动化了数据集准备和模型训练。如果您想看到一个工作模型训练示例,请阅读该部分的实验部分。

3.3.2 服务设计概述

让我们以 Alex(一位数据科学家)和 Tang(一位开发人员)来展示服务的功能。要使用训练服务来训练一个模型,Alex 需要编写训练代码(例如,一个神经网络算法)并将代码构建成一个 Docker 镜像。这个 Docker 镜像需要发布到一个 artifact 存储库,以便训练服务可以拉取镜像并将其作为容器运行。在 Docker 容器内部,训练代码将由一个 bash 脚本执行。

为了提供一个示例,我们用 PyTorch 编写了一个样本意图分类训练代码,将代码构建成一个 Docker 镜像,并将其推送到 Docker hub(hub.docker.com/u/orca3)。我们将在第 3.3.6 节再次解释它。

注意 在实际场景中,训练 Docker 镜像的创建、发布和消费都是自动完成的。一个示例场景可能如下:第一步,Alex 将他的训练代码提交到 Git 存储库;第二步,一个预配置的程序——例如 Jenkins 流水线——被触发以从这个存储库构建一个 Docker 镜像;第三步,流水线还将 Docker 镜像发布到 Docker 镜像工厂,例如 JFrog Artifactory;第四步,Alex 发送一个训练请求,然后训练服务从工厂拉取训练镜像并开始模型训练。

当 Alex 完成培训代码开发后,他可以开始使用服务运行他的培训代码。整个工作流程如下:步骤 1.a,Alex 向我们的样本培训服务提交培训请求。请求定义了培训代码——一个 Docker 镜像和标签。当培训服务收到培训请求时,它会在队列中创建一个作业,并将作业 ID 返回给 Alex 以供将来跟踪作业;步骤 1.b,Alex 可以查询培训服务以实时获取培训进度;步骤 2,服务以 Docker 容器的形式在本地 Docker 引擎中启动一个训练作业来执行模型训练;步骤 3,Docker 容器中的培训代码在训练期间上传培训指标到元数据存储以及在培训完成时上传最终模型。

注意 模型评估是我们在前述模型训练工作流程中未提及的步骤。在模型训练完成后,Alex(数据科学家)将查看培训服务报告的培训指标,以验证模型的质量。为了评估模型质量,Alex 可以检查预测失败率、梯度和损失值图。由于模型评估通常是数据科学家的责任,所以我们不会在本书中涉及此内容,但我们会在第八章中讨论模型训练指标是如何收集和存储的。

整个培训工作流程是自助式的;Alex 可以完全自己管理模型训练。Tang 开发了培训服务并维护系统,但系统对 Alex 开发的培训代码是不可知的。Tang 的重点不是模型的准确性,而是系统的可用性和效率。请参见图 3.5,了解我们刚刚描述的用户工作流程。

图片

图 3.5 高级服务设计和用户工作流程:用户培训请求被排队,Docker 作业跟踪器从队列中提取作业,并启动 Docker 容器来运行模型训练。

看到了用户工作流程,让我们看看两个关键组件:内存存储和 Docker 作业跟踪器。内存存储使用以下四种数据结构(映射)来组织请求(作业):作业队列、启动列表、运行列表和完成列表。这些映射中的每一个都代表了不同运行状态的作业。我们之所以在内存中实现作业跟踪存储,只是为了简单起见;理想情况下,我们应该使用数据库。

Docker 作业跟踪器处理 Docker 引擎中的实际作业执行;它定期监视内存存储中的作业队列。当 Docker 引擎有空闲容量时,跟踪器将从作业队列中启动一个 Docker 容器,并继续监视容器的执行。在我们的示例中,我们使用本地 Docker 引擎,所以服务可以在您的本地运行。但它也可以很容易地配置到远程 Docker 引擎上。

启动培训容器后,基于执行状态,Docker 作业跟踪器将作业对象从作业队列移动到其他作业列表,如作业启动列表、运行列表和finalizedJobs列表。在第 3.4.4 节中,我们将详细讨论这个过程。

注意 考虑到训练时间,可能会在培训容器(在培训时)中分割数据集。在数据集构建或模型训练期间拆分数据集都是有效的,但两个过程都有各自的优点和缺点。但无论哪种方式,都不会对训练服务的设计产生重大影响。为简单起见,在此示例培训服务中,我们假设算法代码将数据集拆分为训练集、验证集和测试集。

3.3.3 培训服务 API

在了解了概述后,让我们深入了解公共 gRPC API (grpc-contract/ src/main/proto/training_service.proto),以更深入地理解该服务。培训服务中有两个 API:TrainGetTrainingStatusTrain API 用于提交培训请求,而GetTrainingStatus API 用于获取培训执行状态。请参见以下清单中的 API 定义。

清单 3.2 模型培训服务 gRPC 接口

service TrainingService {
 rpc Train(TrainRequest) returns (TrainResponse);
 rpc GetTrainingStatus(GetTrainingStatusRequest) 
   returns (GetTrainingStatusResponse);
}

message TrainingJobMetadata {           ❶
 string algorithm = 1;                  ❶
 string dataset_id = 2;                 ❶
 string name = 3;                       ❶
 string train_data_version_hash = 4;    ❶
 map<string, string> parameters = 5;    ❶
}                                       ❶

message GetTrainingStatusResponse {
 TrainingStatus status = 1;
 int32 job_id = 2;
 string message = 3;
 TrainingJobMetadata metadata = 4;
 int32 positionInQueue = 5;
}

❶ 定义了模型构建请求的数据集、训练算法和额外参数

从清单 3.2 的 gRPC 接口中,为使用Train API,我们需要提供以下信息作为TrainingJobMetadata

  • dataset_id—数据集管理服务中的数据集 ID

  • train_data_version_hash—用于培训的数据集的散列版本

  • name—培训作业名称

  • algorithm—指定使用哪个培训算法来训练数据集。该算法字符串需要是我们预定义算法之一。在内部,培训服务将查找与该算法关联的 Docker 镜像以执行培训。

  • parameters—我们直接传递给训练容器的训练超参数,如训练轮数、批量大小等。

一旦Train API 收到一个培训请求,服务将把请求放入作业队列,并返回一个 ID (job_id)供调用者引用该作业。这个job_id可以与GetTrainingStatus API 一起使用,以检查培训状态。现在我们已经看到了 API 定义,让我们在接下来的两个章节中看看它们的具体实现。

3.3.4 启动新的培训作业

当用户调用Train API 时,培训请求将被添加到内存存储的作业队列中,然后 Docker 作业跟踪器会在另一个线程中处理实际的作业执行。这个逻辑将在接下来的三个清单(3.3-3.5)中解释。

接收培训请求

首先,一个新的培训请求将被添加到作业等待队列中,并分配一个作业 ID 供将来参考;参见代码(training-service/src/main/ java/org/orca3/miniAutoML/training/TrainingService.java)如下。

3.3 提交培训请求的实现代码

public void train(                                      ❶
  TrainRequest request, 
  StreamObserver<TrainResponse> responseObserver) {

   int jobId = store.offer(request);                    ❷
   responseObserver.onNext(TrainResponse
     .newBuilder().setJobId(jobId).build());            ❸
   responseObserver.onCompleted();
}

public class MemoryStore {
   // jobs are waiting to pick up
   public SortedMap<Integer, TrainingJobMetadata>       ❹
     jobQueue = new TreeMap<>();                        ❹
   // jobs’ docker container is in launching state  
   public Map<Integer, ExecutedTrainingJob>             ❹
     launchingList = new HashMap<>();                   ❹
   // jobs’ docker container is in running state
   public Map<Integer, ExecutedTrainingJob>             ❹
     runningList = new HashMap<>();                     ❹
   // jobs’ docker container is completed
   public Map<Integer, ExecutedTrainingJob>             ❹
     finalizedJobs = new HashMap<>();                   ❹
   // .. .. ..

   public int offer(TrainRequest request) {
       int jobId = jobIdSeed.incrementAndGet();         ❺
       jobQueue.put(jobId, request.getMetadata());      ❻
       return jobId;
   }
}

❶ 实现了训练 API

❷ 将训练请求加入队列

❸ 返回作业 ID

❹ 跟踪工作状态的四个不同作业列表

❺ 生成作业 ID

❻ 在等待队列中启动作业

启动训练作业(容器)

一旦作业在等待队列中,当本地 Docker 引擎有足够的系统资源时,Docker 作业跟踪器将处理它。图 3.6 展示了整个过程。Docker 作业跟踪器监控作业等待队列,并在本地 Docker 引擎有足够容量时挑选出第一个可用作业(图 3.6 中的第 1 步)。然后,Docker 作业跟踪器通过启动 Docker 容器执行模型训练作业(步骤 2)。容器成功启动后,跟踪器将作业对象从作业队列移动到启动列表队列(步骤 3)。图 3.6 的代码实现(training-service/src/main/java/org/orca3/miniAutoML/training/ tracker/DockerTracker.java)在清单 3.4 中突出显示。

图 3.6

图 3.6 训练作业启动工作流程:当 Docker 作业跟踪器具有足够的容量时,它会从作业队列中启动训练容器。

清单 3.4 使用 DockerTracker 启动训练容器

public boolean hasCapacity() {                           ❶
  return store.launchingList.size()
    + store.runningList.size() == 0;
}

public String launch(                                    ❷
  int jobId, TrainingJobMetadata metadata, 
  VersionedSnapshot versionedSnapshot) {

    Map<String, String> envs = new HashMap<>();          ❸
    .. .. ..                                             ❸
    envs.put("TRAINING_DATA_PATH",                       ❸
    versionedSnapshot.getRoot());                        ❸
    envs.putAll(metadata.getParametersMap());            ❸
    List<String> envStrings = envs.entrySet()            ❸
           .stream()                                     ❸
           .map(kvp -> String.format("%s=%s", 
             kvp.getKey(), kvp.getValue()))
           .collect(Collectors.toList());

   String containerId = dockerClient                     ❹
    .createContainerCmd(metadata.getAlgorithm())         ❺
           .. .. ..
           .withCmd("server", "/data")
           .withEnv(envStrings)                          ❻
           .withHostConfig(HostConfig.newHostConfig()
             .withNetworkMode(config.network))
           .exec().getId();

   dockerClient.startContainerCmd(containerId).exec();   ❼
   jobIdTracker.put(jobId, containerId);
   return containerId;
}

❶ 检查系统的容量

❷ 启动训练 Docker 容器

❸ 将训练参数转换为环境变量

❹ 构建 Docker 启动命令

❺ 设置 Docker 镜像名称;其值来自算法名称参数。

❻ 将训练参数作为环境变量传递给 Docker 容器

❼ 运行 Docker 容器

需要注意的是,在代码清单 3.4 中,launch函数将train API 请求中定义的训练参数作为环境变量传递给训练容器(训练代码)。

跟踪训练进度

在最后一步,Docker 作业跟踪器通过监控其容器的执行状态继续跟踪每个作业。当它检测到容器状态发生变化时,作业跟踪器将容器的作业对象移到内存存储中相应的作业列表中。

作业跟踪器将查询 Docker 运行时以获取容器的状态。例如,如果作业的 Docker 容器开始运行,作业跟踪器将检测到此更改,并将作业放入“运行中作业列表”;如果作业的 Docker 容器完成,则作业跟踪器将作业移动到“已完成的作业列表”。作业跟踪器将在将作业放置在“已完成的作业列表”后停止检查作业状态,这意味着训练已完成。图 3.7 描述了此作业跟踪工作流程。清单 3.5 突出显示了此作业跟踪过程的实现。

图 3.7

图 3.7 Docker 作业跟踪器监视 Docker 容器的执行状态并更新作业队列。

清单 3.5 DockerTracker 监控 Docker 并更新作业状态

public void updateContainerStatus() {
  Set<Integer> launchingJobs = store.launchingList.keySet();
  Set<Integer> runningJobs = store.runningList.keySet();

  for (Integer jobId : launchingJobs) {               ❶

    String containerId = jobIdTracker.get(jobId);
    InspectContainerResponse.ContainerState state =   ❷
        dockerClient.inspectContainerCmd(             ❷
          containerId).exec().getState();             ❷
    String containerStatus = state.getStatus();

    // move the launching job to the running 
    // queue if the container starts to run. 
       .. .. ..
   }

   for (Integer jobId : runningJobs) {                ❸
      // move the running job to the finalized 
      // queue if it completes (success or fail).
       .. .. ..
   }
}

❶ 检查启动作业列表中所有作业的容器状态

❷ 查询容器的执行状态

❸ 检查运行中作业列表中所有作业的容器状态

3.3.5 更新和获取作业状态

现在您已经看到了训练请求在训练服务中是如何执行的,让我们继续前往代码之旅的最后一站:获取训练执行状态。启动训练作业后,我们可以查询GetTrainingStatus API 来获取训练状态。作为提醒,我们将图 3.5 重新发布,呈现为图 3.8,显示了服务的高级设计,如下所示。

图 3.8 高级服务设计和用户工作流程

根据图 3.8,我们可以看到获取训练状态只需要一步,即 1.b。此外,通过查找哪个作业列表(在内存存储中)包含jobId,可以确定训练作业的最新状态。请参阅以下代码以查询训练作业/请求的状态(training-service/src/main/java/org/orca3/miniAutoML/training/TrainingService.java)。

清单 3.6 训练状态实现

public void getTrainingStatus(GetTrainingStatusRequest request) {
  int jobId = request.getJobId();
  .. .. ..  
  if (store.finalizedJobs.containsKey(jobId)) {           ❶
    job = store.finalizedJobs.get(jobId);
    status = job.isSuccess() ? TrainingStatus.succeed 
        : TrainingStatus.failure;

  } else if (store.launchingList.containsKey(jobId)) {    ❷
    job = store.launchingList.get(jobId);
    status = TrainingStatus.launch;

  } else if (store.runningList.containsKey(jobId)) {      ❸
    job = store.runningList.get(jobId);
    status = TrainingStatus.running;
  } else {                                                ❹
    TrainingJobMetadata metadata = store.jobQueue.get(jobId);
    status = TrainingStatus.waiting;
       .. .. ..
   }
   .. .. ..
}

❶ 在已完成的作业列表中搜索作业

❷ 在启动作业列表中搜索作业

❸ 在运行中的作业列表中搜索作业

❹ 作业仍在等待作业队列中。

因为 Docker 作业跟踪器实时将作业移动到相应的作业列表中,我们可以依靠使用作业队列类型来确定训练作业的状态。

3.3.6 意图分类模型训练代码

到目前为止,我们一直在处理训练服务代码。现在让我们看看最后一部分,模型训练代码。请不要被这里的深度学习算法吓到。这个代码示例的目的是向您展示一个具体的示例,说明训练服务如何与模型训练代码交互。图 3.9 描绘了示例意图分类训练代码的工作流程。

图 3.9 意图分类训练代码工作流程首先从环境变量中读取所有输入参数,然后下载数据集,处理数据集,并启动训练循环。最后,它上传输出模型文件。

我们的示例训练代码训练一个三层神经网络以执行意图分类。首先,它从由我们的训练服务传递的环境变量中获取所有输入参数(请参阅第 3.3.4 节)。输入参数包括超参数(epoch 数、学习率等)、数据集下载设置(MinIO 服务器地址、数据集 ID、版本哈希)和模型上传设置。接下来,训练代码下载和解析数据集,并开始迭代学习过程。在最后一步中,代码将生成的模型和训练指标上传到元数据存储中。以下代码清单突出显示了前面提到的主要步骤(train-service/text-classification/train.pytrain-service/text-classification/Dockerfile)。

清单 3.7 意图分类模型训练代码和 Docker 文件

# 1\. read all the input parameters from 
# environment variables, these environment 
# variables are set by training service - docker job tracker.
EPOCHS = int_or_default(os.getenv('EPOCHS'), 20)
.. .. ..
TRAINING_DATA_PATH = os.getenv('TRAINING_DATA_PATH')

# 2\. download training data from dataset management
client.fget_object(TRAINING_DATA_BUCKET, 
  TRAINING_DATA_PATH + "/examples.csv", "examples.csv")
client.fget_object(TRAINING_DATA_BUCKET, 
  TRAINING_DATA_PATH + "/labels.csv", "labels.csv")

# 3\. prepare dataset
.. .. ..
train_dataloader = DataLoader(split_train_, batch_size=BATCH_SIZE,
                             shuffle=True, collate_fn=collate_batch)
valid_dataloader = DataLoader(split_valid_, batch_size=BATCH_SIZE,
                             shuffle=True, collate_fn=collate_batch)
test_dataloader = DataLoader(split_test_, batch_size=BATCH_SIZE,
                            shuffle=True, collate_fn=collate_batch)

# 4\. start model training
for epoch in range(1, EPOCHS + 1):
   epoch_start_time = time.time()
   train(train_dataloader)
   .. .. ..

print('Checking the results of test dataset.')
accu_test = evaluate(test_dataloader)
print('test accuracy {:8.3f}'.format(accu_test))

# 5\. save model and upload to metadata store.
.. .. ..
client.fput_object(config.MODEL_BUCKET, 
  config.MODEL_OBJECT_NAME, model_local_path)
artifact = orca3_utils.create_artifact(config.MODEL_BUCKET, 
  config.MODEL_OBJECT_NAME)
.. .. ..

注意 我们希望我们的示例训练代码演示了深度学习训练代码遵循常见模式。通过容器化以及传递参数的清晰协议,训练服务可以执行各种训练代码,而不论训练框架或模型架构。

3.3.7 训练作业管理

在第 3.1.2 节中,我们提到良好的训练服务应该解决计算隔离并提供按需计算资源(原则 4)。这种隔离有两重含义:训练过程的执行隔离和资源消耗隔离。由于我们使用 Docker 对训练过程进行容器化,所以这个执行隔离是由 Docker 引擎保证的。但是资源消耗隔离仍然需要我们自己处理。

想象一下,来自不同团队的三个用户(A、B 和 C)向我们的训练服务提交训练请求。如果用户 A 提交了 100 个训练请求,然后用户 B 和 C 都各自提交了一个请求,那么用户 B 和 C 的请求将在等待作业队列中等待一段时间,直到用户 A 的所有训练请求完成。当我们将训练集群视为每个人的游戏场时,这就是发生的情况:一个重度使用情况会主导作业调度和资源消耗。

为了解决资源竞争问题,我们需要在训练集群内为不同团队和用户设置边界。我们可以在训练集群内创建机器池以实现资源消耗隔离。每个团队或用户可以分配到一个专用的机器池,每个池都有自己的 GPU 和机器,池的大小取决于项目需求和训练使用情况。此外,每个机器池可以有一个专门的作业队列,因此重度用户不会影响其他用户。图 3.9 展示了这种方法的运作方式。

注意 资源隔离方法,像我们刚刚提到的服务器池方法,在资源利用方面可能不够高效。例如,服务器池 A 可能非常忙,而服务器池 B 可能处于空闲状态。可以定义每个服务器池的大小为一个范围,而不是一个固定数字,例如最小 5 台服务器、最大 10 台服务器,以提高资源利用率。然后可以应用额外的逻辑,以在服务器之间进行移动或提供新服务器。

实现图 3.10 的理想方法是使用 Kubernetes。Kubernetes 允许您创建由相同物理集群支持的多个虚拟集群,称为命名空间。Kubernetes 命名空间是一个消耗非常少系统资源的轻量级机器池。

图 3.10 在训练集群内创建机器池,以为不同用户设置资源消耗边界。

如果您正在使用 Kubernetes 管理服务环境和计算集群,那么设置此类隔离非常容易。首先,您需要创建一个拥有资源配额的命名空间,例如 CPU 数量、内存大小和 GPU 数量;然后,在训练服务中定义用户及其命名空间的映射关系。

现在,当用户提交一个训练请求时,训练服务首先通过检查请求中的用户信息找到正确的命名空间,然后调用 Kubernetes API 将训练可执行文件放置在该命名空间中。由于 Kubernetes 实时跟踪系统的使用情况,它知道一个命名空间是否有足够的容量,如果命名空间已满,它将拒绝作业启动请求。

正如您所见,通过使用 Kubernetes 管理训练集群,我们可以将资源容量跟踪和资源隔离管理从训练服务中卸载出来。这是选择 Kubernetes 构建深度学习训练集群管理的一个原因。

3.3.8 故障排除指标

在此示例服务中,我们没有演示指标的使用。通常,指标是用于评估、比较和跟踪性能或生产的定量评估的常见度量。对于深度学习训练,我们通常定义两种类型的指标:模型训练执行指标和模型性能指标。

模型训练执行指标包括资源饱和率、训练作业执行可用性、平均训练作业执行时间和作业失败率。我们检查这些指标,以确保训练服务健康运行,并且用户的日常活动正常。例如,我们期望服务可用性超过 99.99% ,训练作业失败率小于 0.1% 。

模型性能指标衡量模型学习的质量。它包括每个训练迭代(epoch)的损失值和评估分数,以及最终的模型评估结果,如准确率、精度和 F1 分数。

对于与模型性能相关的指标,我们需要以更有组织的方式存储这些指标,以便能够使用统一的方法轻松搜索信息并比较不同训练运行之间的性能。我们将在第八章对此进行更详细的讨论。

3.3.9 支持新算法或新版本

现在让我们讨论如何将更多的训练代码接入到我们的示例训练服务中。在当前的实现中,我们使用请求中的 algorithm 变量来定义用户训练请求与训练代码之间的映射关系,使用 algorithm 变量在请求中找到对应的训练镜像。底层规则是 algorithm 变量必须等于 Docker 镜像名称,否则训练服务无法找到正确的镜像来运行模型训练。

以我们的意图分类训练为例。首先,我们需要将意图训练 Python 代码 Docker 化为 Docker 镜像并将其命名为“intent-classification”。然后,当用户使用algorithm = 'intent-classification'参数发送训练请求时,Docker 作业跟踪器将使用算法名称(即 intent-classification)在本地 Docker 仓库中搜索“intent-classification”训练镜像并将镜像运行为训练容器。

这种方法肯定过于简化了,但它演示了我们如何与数据科学家一起定义将用户训练请求映射到实际训练代码的正式协议。在实践中,训练服务应该提供一组 API,允许数据科学家以自助方式注册训练代码。

一种可能的方法是在数据库中定义算法名称和训练代码映射,添加一些 API 来管理这个映射。建议的 API 可以是

  • createAlgorithmMapping(string algorithmName, string image, string version)

  • updateAlgorithmVersion(string algorithmName, string image, string version)

如果数据科学家想要添加新的算法类型,他们可以调用createAlgorithmMapping API 来向训练服务注册新的训练图像和新的算法名称。我们的用户只需要在训练请求中使用这个新的算法名称即可使用这个新的算法开始模型训练。

如果数据科学家想要发布现有算法的新版本,他们可以调用updateAlgorithmVersion API 来更新映射。我们的用户仍然会有相同的算法名称(如意图分类),发送请求,但他们不会意识到训练代码在幕后升级到不同的版本。同时,值得指出的是,服务的公共 API 不会受到添加新训练算法的影响;只有一个新参数值被使用。

3.4. Kubeflow 训练操作符:开源方法

看完我们的示例训练服务后,让我们看看一个开源的训练服务。在本节中,我们将讨论来自 Kubeflow 项目的一组开源训练操作符。这些训练操作符可立即使用,并且可以在任何 Kubernetes 集群中独立设置。

Kubeflow 是一个成熟、开源的机器学习系统,适用于生产环境。我们在附录 B.4 中简要介绍了它,以及亚马逊 SageMaker 和 Google Vertex AI。我们推荐使用 Kubeflow、可扩展、可分布式和稳健性高的训练操作符。我们将首先讨论高级系统设计,然后讨论如何将这些训练操作符集成到自己的深度学习系统中。

什么是 Kubeflow?

Kubeflow 是一个开源的机器学习平台(源自谷歌),用于开发和部署生产级别的机器学习模型。你可以将 Kubeflow 视为 Amazon SageMaker 的开源版本,但它原生运行在 Kubernetes 上,因此它是云无关的。Kubeflow 将完整的机器学习功能集成到一个系统中——从 Notebooks 和管道到训练和服务。

即使你不打算使用它,我强烈建议你关注 Kubeflow 项目。Kubeflow 是一个设计良好且相当先进的深度学习平台;它的功能列表涵盖了整个机器学习生命周期。通过审查其用例、设计和代码,你将深入了解现代深度学习平台。

此外,因为 Kubeflow 是在 Kubernetes 的原生基础上构建的,你可以轻松地在你的本地或生产环境中设置整个系统。如果你不感兴趣借鉴整个系统,你也可以移植其中一些组件——如训练操作器或超参数优化服务——它们可以自行在任何 Kubernetes 环境中开箱即用。

3.4.1 Kubeflow 训练操作器

Kubeflow 提供了一组训练操作器,例如 TensorFlow 操作器、PyTorch 操作器、MXNet 操作器和 MPI 操作器。这些操作器涵盖了所有主要训练框架。每个操作器都有知识可以启动和监视用特定类型的训练框架编写的训练代码(容器)。

如果你计划在 Kubernetes 集群中运行模型训练,并希望设置自己的训练服务以减少操作成本,Kubeflow 训练操作器是完美的选择。以下是三个原因:

  • 轻松安装和低维护——Kubeflow 操作器开箱即用;通过几行 Kubernetes 命令,你可以使它们在你的集群中工作。

  • 兼容大多数训练算法和框架——只要你将训练代码容器化,就可以使用 Kubeflow 操作器执行它。

  • 易于集成到现有系统——由于 Kubeflow 训练操作器遵循 Kubernetes 操作器设计模式,因此你可以使用 Kubernetes 的声明性 HTTP API 提交训练作业请求并检查作业运行状态和结果。你也可以使用 RESTful 查询与这些操作器交互。

3.4.2 Kubernetes 操作器/控制器模式

Kubeflow 训练操作器遵循 Kubernetes 操作器(控制器)设计模式。如果我们理解了此模式,那么运行 Kubeflow 训练操作器并阅读其源代码就很简单了。图 3.11 显示了控制器模式的设计图。

图 3.11 Kubernetes 操作器/控制器模式运行无限控制循环,观察某些 Kubernetes 资源的实际状态(在右侧)和期望状态(在左侧),并尝试将实际状态移动到期望状态。

Kubernetes 中的所有内容都围绕着资源对象和控制器构建。Kubernetes 的资源对象,如 Pods、Namespaces 和 ConfigMaps,是持久化实体(数据结构),代表着集群的状态(期望的和当前的)。控制器是一个控制循环,它对实际的系统资源进行更改,以将您的集群从当前状态带到更接近期望状态,这在资源对象中定义。

注意 Kubernetes pod 是您可以在 Kubernetes 中创建和管理的最小部署单元。Pod 可以被视为运行一个或多个 Docker 容器的“逻辑主机”。有关 Kubernetes 概念的详细解释,例如 Namespaces 和 ConfigMaps,可以在官方网站找到:kubernetes.io/docs/concepts/

例如,当用户应用 Kubernetes 命令来创建一个 pod 时,它将在集群中创建一个 pod 资源对象(一个数据结构),其中包含所需的状态:两个 Docker 容器和一个磁盘卷。当控制器检测到这个新的资源对象时,它将在集群中提供实际的资源,并运行两个 Docker 容器并附加磁盘。接下来,它将更新 pod 资源对象的最新实际状态。用户可以查询 Kubernetes API 来获取此 pod 资源对象的更新信息。当用户删除此 pod 资源对象时,控制器将删除实际的 Docker 容器,因为所需状态已更改为零。

为了轻松扩展 Kubernetes,Kubernetes 允许用户定义自定义资源定义(CRD)对象,并注册定制的控制器来处理这些 CRD 对象,称为操作器。如果您想了解更多关于控制器/操作器的信息,可以阅读 “Kubernetes/sample-controller” GitHub 存储库,该存储库实现了用于监视 CRD 对象的简单控制器。这个示例控制器代码可以帮助您理解操作器/控制器模式,这种理解对于阅读 Kubeflow 训练操作器源代码非常有用。

注意:在本节中,“控制器”和“操作器”这两个术语是可以互换使用的。

3.4.3 Kubeflow 训练操作器设计

Kubeflow 训练操作器(TensorFlow 操作器、PyTorch 操作器、MPI 操作器)遵循 Kubernetes 操作器设计。每个训练操作器都会监视其自己类型的客户资源定义对象 —— 例如 TFJobPyTorchJobMPIJob —— 并创建实际的 Kubernetes 资源来运行训练。

例如,TensorFlow 操作器处理在集群中生成的任何 TFJob CRD 对象,并根据 TFJob 规范创建实际的服务/ pod。它将 TFJob 对象的资源请求与实际的 Kubernetes 资源(例如服务和 pod)同步,并不断努力使观察到的状态与期望的状态匹配。在图 3.12 中可以看到一个视觉工作流程。

图 3.12 Kubeflow 训练操作器的工作流程。用户首先创建一个 TFJob CRD 对象,该对象定义了一个训练请求,然后 TensorFlow 操作器检测到此对象,并创建实际的 pod 来执行 TensorFlow 训练图像。TensorFlow 操作器还监视 pod 的状态,并将其状态更新到 TFJob CRD 对象中。相同的工作流程也适用于 PyTorch 操作器。

每个操作器都可以为其自己类型的训练框架运行训练 pod。例如,TensorFlow 操作器知道如何为 TensorFlow 编写的训练代码设置分布式训练 pod 组。操作器从 CRD 定义中读取用户请求,创建训练 pod,并将正确的环境变量和命令行参数传递给每个训练 pod/container。您可以查看每个操作器代码中的 reconcileJobsreconcilePods 函数以了解更多详细信息。

每个 Kubeflow 操作器还处理作业队列管理。因为 Kubeflow 操作器遵循 Kubernetes 操作器模式,并在 pod 级别创建 Kubernetes 资源,所以训练 pod 的故障切换处理得很好。例如,当一个 pod 意外失败时,当前 pod 数量会减少一个,小于 CRD 对象中定义的期望 pod 数量。在这种情况下,操作器中的 reconcilePods 逻辑将在集群中创建一个新的 pod,以确保实际的 pod 数量等于 CRD 对象中定义的期望数量,从而解决故障切换问题。

注意 在编写本书时,TensorFlow 操作器正在成为全能的 Kubeflow 操作器。它旨在简化在 Kubernetes 上运行分布式或非分布式 TensorFlow/PyTorch/MXNet/XGBoost 作业。无论最终的结果如何,它都是基于我们在这里提到的设计构建的,只是使用起来更加方便。

3.4.4 如何使用 Kubeflow 训练操作器

在本节中,我们将以 PyTorch 操作器作为示例,分四个步骤训练一个 PyTorch 模型。因为所有的 Kubeflow 训练操作器都遵循相同的使用模式,这些步骤也适用于其他操作器。

首先,在您的 Kubernetes 集群中安装独立的 PyTorch 操作器和 PyTorchJob CRD。您可以在 PyTorch 操作器的 Git 存储库的开发人员指南中找到详细的安装说明。安装完成后,您可以在您的 Kubernetes 集群中找到一个正在运行的训练操作器 pod,并创建一个 CRD 定义。查看如下的 CRD 查询命令:

$ kubectl get crd                                       ❶

NAME                              CREATED AT
...
pytorchjobs.kubeflow.org        2021-09-15T18:33:58Z    ❷
...

❶ 列出所有的 CRD 定义

❷ Kubernetes 中创建了 PyTorchJob CRD。

注意 训练运算符的安装可能会令人困惑,因为 README 建议你安装整个 Kubeflow 来运行这些运算符,但这并非必须。每个训练运算符都可以单独安装,这是我们推荐的处理方式。请查阅开发指南或设置脚本

接下来,更新你的训练容器以从环境变量和命令行参数中读取参数输入。你可以稍后在 CRD 对象中传递这些参数。

第三步,创建一个PyTorchJob CRD 对象来定义我们的训练请求。你可以通过首先编写一个 YAML 文件(例如,pytorchCRD.yaml),然后在你的 Kubernetes 集群中运行kubectl create -f pytorchCRD.yaml来创建这个 CRD 对象。PT-operator 将检测到这个新创建的 CRD 对象,将其放入控制器的作业队列中,并尝试分配资源(Kubernetes pod)来运行训练。清单 3.8 显示了一个样例PyTorchJob CRD。

清单 3.8 一个样例 PyTorch CRD 对象

kind: PyTorchJob                 ❶
metadata:
  name: pytorch-demo             ❷
spec:
  pytorchReplicaSpecs:           ❸
    Master:
      replicas: 1                ❹
      restartPolicy: OnFailure
      containers:
          .. .. ..
    Worker:
      replicas: 1                ❺
      .. .. ..
        spec:
          containers:            ❻
            - name: pytorch
              .. .. ..
              env:               ❼
                - name: credentials
                  value: "/etc/secrets/user-gcp-sa.json"
              command:           ❽
                - "python3"
                - “-m”
                - "/opt/pytorch-mnist/mnist.py"
                - "--epochs=20"
                - “--batch_size=32

❶ CRD 的名称

❷ 训练作业的名称

❸ 定义训练组规格

❹ 主节点 pod 的数量

❺ 训练工作负载的数量

❻ 定义训练容器配置

❼ 为每个训练 pod 定义环境变量

❽ 定义命令行参数

最后一步是监控。你可以使用kubectl get -o yaml pytorchjobs命令获取训练状态,它将列出所有pytorchjobs类型的 CRD 对象的详细信息。因为 PyTorch 运算符的控制器将持续更新最新的训练信息到 CRD 对象中,所以我们可以从中读取当前状态。例如,以下命令将创建一个名为pytorch-demoPyTorchJob类型的 CRD 对象:

kubectl get -o yaml pytorchjobs pytorch-demo -n kubeflow

注意 在前面的示例中,我们使用 Kubernetes 命令kubectl与 PyTorch 运算符进行交互。但我们也可以向集群的 Kubernetes API 发送 RESTful 请求来创建训练作业 CRD 对象并查询其状态。然后,新创建的 CRD 对象将触发控制器中的训练操作。这意味着 Kubeflow 训练运算符可以轻松集成到其他系统中。

3.4.5 如何将这些运算符集成到现有系统中

从第 3.4.3 节我们可以看到,运算符的 CRD 对象充当了触发训练操作的网关 API,并且是训练状态的真实来源。因此,我们可以通过在运算符 CRD 对象之上构建一个 Web 服务将这些训练运算符集成到任何系统中。这个包装服务有两个责任:首先,它将你系统中的训练请求转换为 CRD 对象(训练作业)上的 CRUD(创建、读取、更新和删除)操作;其次,它通过读取 CRD 对象来查询训练状态。请参见图 3.13 中的主要工作流程。

图 3.13 将 Kubeflow 训练运算符集成到现有深度学习系统中作为训练后端。包装器服务可以将训练请求转换为 CRD 对象,并从 CRD 对象中获取训练状态。

在图 3.13 中,现有系统的前端部分保持不变,例如前门网站。在计算后端,我们更改了内部组件,并与包装器训练服务对话以执行模型训练。包装器服务有三个功能:首先,它管理作业队列;其次,它将训练请求从现有格式转换为 Kubeflow 训练运算符的 CRD 对象;第三,它从 CRD 对象中获取训练状态。通过这种方法,通过添加包装器服务,我们可以轻松地将 Kubeflow 训练运算符作为任何现有深度学习平台/系统的训练后端。

从零开始构建一个生产质量的训练系统需要大量的努力。你不仅需要了解各种训练框架的微妙之处,还需要了解如何处理工程方面的可靠性和可扩展性挑战。因此,如果你决定在 Kubernetes 中运行模型训练,我们强烈建议采用 Kubeflow 训练运算符。这是一个开箱即用的解决方案,可以轻松移植到现有系统中。

3.5 何时使用公有云

主要的公有云供应商,如亚马逊、谷歌和微软,提供了他们的深度学习平台,如亚马逊 SageMaker、谷歌 Vertex AI 和 Azure 机器学习工作室,一应俱全。所有这些系统声称提供全面托管的服务,支持整个机器学习工作流程,以便快速训练和部署机器学习模型。事实上,它们不仅涵盖模型训练,还包括数据处理和存储、版本控制、故障排除、操作等方面。

在本节中,我们不打算讨论哪种云解决方案是最好的;相反,我们想分享一下何时使用它们的想法。当我们提出在公司内部构建服务,如训练服务或超参数调整服务时,我们经常会听到诸如“我们可以使用 SageMaker 吗?我听说他们有一个功能……”或“你能在 Google Vertex AI 之上构建一个包装器吗?我听说……”这样的问题。这些问题有时是有效的,有时不是。你能负担得起什么真的取决于你的业务阶段。

3.5.1 何时使用公有云解决方案

如果你经营一家初创公司或想要快速验证你的业务理念,使用公有云 AI 平台是一个不错的选择。它处理所有底层基础设施管理,并为你提供了一个标准的工作流程供你遵循。只要预定义的方法对你有效,你就可以专注于开发你的业务逻辑、收集数据和实现模型算法。真正的好处在于节省了建立自己基础设施的时间,这样你就可以“早期失败,快速学习”。

使用公共云 AI 平台的另一个原因是您只有少数深度学习场景,并且它们很好地适用于公共云的标准用例。在这种情况下,为仅几个应用程序构建复杂的深度学习系统并不值得消耗资源。

3.5.2 构建自己的训练服务的时机

现在,让我们谈谈何时需要构建自己的训练方法的情况。如果您的系统具有以下五个要求之一,构建自己的训练服务是正确的选择。

云无关性

如果您希望您的应用程序具有云无关性,您就不能使用亚马逊 SageMaker 或 Google Vertex AI 平台,因为这些系统是特定于云的。当您的服务存储客户数据时,拥有云无关性是重要的,因为一些潜在客户对于他们不希望将数据放入的云有特定要求。您希望您的应用程序能够在各种云基础设施上无缝运行。

在公共云上构建云无关系统的常见方法是仅仅使用基础服务,例如虚拟机(VM)和存储,并在其上构建您的应用程序逻辑。以模型训练为例,当使用亚马逊网络服务时,我们首先通过使用亚马逊 EC2 服务设置一个 Kubernetes 集群(Amazon 弹性 Kubernetes 服务(Amazon EKS))来管理计算资源,然后使用 Kubernetes 接口构建我们自己的训练服务来启动训练任务。通过这种方式,当我们需要迁移到谷歌云(GCP)时,我们可以简单地将我们的训练服务应用到 GCP Kubernetes 集群(Google Kubernetes Engine)而不是 Amazon EKS,并且大部分服务保持不变。

降低基础设施成本

使用云服务提供商的人工智能平台相比自行运营服务将会花费更多的资金。在原型设计阶段,您可能不太在意账单,但产品发布后,您肯定应该关心。

以亚马逊 SageMaker 为例,在撰写本书时(2022 年),SageMaker 为 m5.2xlarge 类型(八个虚拟 CPU,32GB 内存)的机器每小时收费 0.461 美元。如果直接在此硬件规格上启动亚马逊 EC2 实例(VM),则每小时收费 0.384 美元。通过构建自己的训练服务并直接在亚马逊 EC2 实例上运行,您平均可以节省近 20%的模型构建成本。如果一家公司有多个团队每天进行模型训练,那么自建训练系统将使您在竞争中处于领先地位。

定制

尽管云 AI 平台为您提供了许多工作流配置选项,但它们仍然是黑匣子方法。因为它们是一刀切的方法,这些 AI 平台专注于最常见的场景。但总会有需要为您的业务定制的例外情况;当选择不多时,这不会是一种好的体验。

云端 AI 平台的另一个问题是在采用新技术方面总是有所延迟。例如,您必须等待 SageMaker 团队决定是否支持某种训练方法以及何时支持它,而有时该决定可能不符合您的意愿。深度学习是一个快速发展的领域。构建自己的训练服务可以帮助您采用最新的研究并快速转变,从而使您在激烈的竞争中获得优势。

通过合规审计

要有资格运行某些业务,您需要获得符合合规法律法规的证书,例如 HIPAA(医疗保险流动性和责任法)或 CCPA(加州消费者隐私法)。这些认证要求您不仅提供证据证明您的代码符合这些要求,还要提供您的应用程序运行的基础设施符合要求。如果您的应用程序是基于 Amazon SageMaker 和 Google Vertex AI 平台构建的,则它们也需要符合要求。由于云供应商是黑盒,通过合规检查清单并提供证据是一项不愉快的任务。

身份验证和授权

将身份验证和授权功能集成到云端 AI 平台和内部身份验证服务(内部部署)需要付出很大的努力。许多公司都有自己的版本身份验证服务来验证和授权用户请求。如果我们采用 SageMaker 作为 AI 平台并将其暴露给不同的内部服务以满足不同的业务目的,将 SageMaker 的身份验证管理与内部用户身份验证管理服务连接起来并不容易。相反,构建内部部署的训练服务要容易得多,因为我们可以自由更改 API 并简单地将其集成到现有的身份验证服务中。

总结

  • 训练服务的主要目标是管理计算资源和训练执行。

  • 一种先进的训练服务遵循四个原则:通过统一接口支持各种模型训练代码;降低训练成本;支持模型可复现性;具有高可伸缩性和可用性,并处理计算隔离。

  • 了解常见的模型训练代码模式可以让我们从训练服务的角度将代码视为黑盒。

  • 容器化是处理深度学习训练方法和框架多样性的关键。

  • 通过将训练代码 Docker 化并定义明确的通信协议,训练服务可以将训练代码视为黑盒并在单个设备或分布式环境中执行训练。这也使得数据科学家可以专注于模型算法开发,而不必担心训练执行。

  • Kubeflow 训练 operators 是一组基于 Kubernetes 的开源训练应用程序。这些 operators 可以开箱即用,并且可以轻松地集成到任何现有系统中作为模型训练后端。Kubeflow 训练 operators 支持分布式和非分布式训练。

  • 使用公共云训练服务可以帮助快速构建深度学习应用程序。另一方面,建立自己的训练服务可以减少训练操作成本,提供更多的定制选项,并保持云无关。