面向机器学习的-Kubeflow-全-

面向机器学习的 Kubeflow(全)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

多年来,有时人们会问我,在技术领域,哪些技能最受欢迎。十年前,我会告诉他们学习机器学习,因为它能以前所未有的方式扩展自动化决策能力。然而,如今我的答案不同了:机器学习工程。

即使仅仅几年前,如果你懂得机器学习并开始在一个组织中工作,你可能会是唯一掌握这种技能集的人,从而可以产生巨大影响。然而,由于大量的书籍、教程、电子课程和训练营(其中一些是我自己写的),教授整整一代技术人员所需的技能,机器学习现在被数以万计的公司和组织广泛使用。

如今,更有可能的情况是,当你步入新工作时,你会发现一个组织在本地使用机器学习,但无法将其部署到生产环境,或者能够部署模型但无法有效地管理它们。在这种情况下,最有价值的技能不是训练模型的能力,而是能够管理所有这些模型,并以最大化影响的方式部署它们。

在这本书中,Trevor Grant、Holden Karau、Boris Lublinsky、Richard Liu 和 Ilan Filonenko 组织起来,我认为这是数据科学家和机器学习工程师教育中的一个重要基石。在可预见的未来,开源 Kubeflow 项目将成为组织在培训、管理和部署机器学习模型中常用的工具之一。这本书代表了大量知识的系统化,这些知识以前分散在内部文档、会议演示和博客文章中。

如果你像我一样相信,机器学习的强大取决于我们如何使用它,那么这本书适合你。

Chris Albon

机器学习总监,

维基媒体基金会

https://chrisalbon.com

前言

我们为那些正在构建希望投入生产的机器学习系统/模型的数据工程师和数据科学家而写此书。如果您曾经训练出了一个优秀的模型,却不知如何将其部署到生产环境或在部署后如何保持更新,那么这本书适合您。我们希望这本书能帮助您用相对可靠的方式替换Untitled_5.ipynb

本书不适合作为您第一次接触机器学习的入门书籍。下一节将指出一些资源,如果您刚开始机器学习之旅可能会有所帮助。

我们对您的假设

本书假设您要么已经了解如何在本地训练模型,要么正在与了解此过程的人合作。如果以上都不是,那么有许多出色的机器学习入门书籍可以帮助您入门,包括Aurélien Géron 的《Python 机器学习实战》第二版,使用 Scikit-Learn、Keras 和 TensorFlow(O’Reilly)。

我们的目标是教会您如何以可重复的方式进行机器学习,并自动化您的模型训练和部署。这个目标涉及到一系列广泛的主题,您可能不太熟悉其中的所有内容是非常合理的。

由于我们无法深入研究每个主题,因此我们希望为您提供一个我们最喜欢的入门指南的简短列表:

当然,还有许多其他资源,但这些可以帮助你入门。请不要被这个清单吓倒—你并不需要成为这些主题的专家来有效地部署和管理 Kubeflow。实际上,Kubeflow 存在的目的就是简化这些任务。

容器和 Kubernetes 是一个广泛且快速发展的领域。如果你想深入了解 Kubernetes,我们建议查看以下内容:

作为从业者的责任

本书帮助你将机器学习模型投入生产解决现实世界的问题。用机器学习解决现实世界的问题很棒,但在应用你的技能时,请记得考虑其影响。

首先,确保你的模型足够准确非常重要,在 Kubeflow 中有很多强大的工具,这在“训练和部署模型”章节有详细介绍。即使是最好的工具也不能保证你免于所有错误—例如,在同一数据集上进行超参数调整以报告最终交叉验证结果。

即使具有显著预测能力的模型在常规的训练评估阶段可能不会显示出意外的效果和偏见。意外的偏见可能很难发现,但有许多故事(例如,亚马逊基于机器学习的招聘引擎后来被发现存在严重偏见,决定只招聘男性)显示了我们工作的深远潜在影响。早期不解决这些问题可能会导致不得不放弃整个工作,就像IBM 决定停止其面部识别程序以及在警方手中的面部识别中种族偏见影响明显后,行业中类似暂停一样。

即使表面上没有偏见的数据,如原始购买记录,最终也可能存在严重偏见,导致不正确的推荐甚至更糟糕的情况。公开并广泛可用的数据集并不意味着它没有偏见。众所周知的word embeddings做法已被证明包含许多种类的偏见,包括性别歧视、反 LGBTQ 和反移民。在查看新数据集时,查找数据中偏见的例子并尽可能减少这些偏见至关重要。对于最流行的公开数据集,研究中经常讨论各种技术,您可以借鉴这些来指导自己的工作。

虽然本书没有解决偏见的工具,但我们鼓励您在投入生产之前,对系统中潜在的偏见进行批判性思考,并探索解决方案。如果您不知道从哪里开始,请查看 Katharine Jarmul 的出色的入门讲座。IBM 在其AI 公正性 360 开源工具包中收集了工具和示例,这可能是开始探索的好地方。减少模型中偏见的关键步骤之一是拥有一个多样化的团队,以便及早发现潜在问题。正如Jeff Dean所说:“AI 充满了承诺,有潜力彻底改变现代社会的许多不同领域。为了实现其真正的潜力,我们的领域需要对所有人都友好。但事实并非如此。我们的领域在包容性方面存在问题。”

提示

需要注意的是,在你的结果中消除偏见或验证准确性并不是一蹴而就的事情;模型性能可能会随时间降低,甚至会引入偏见——即使你个人没有做任何改变。¹

本书使用的约定

本书使用以下排版约定:

斜体

表示新术语、网址、电子邮件地址、文件名和文件扩展名。

等宽字体

用于程序清单,以及段落内部引用程序元素,如变量或函数名称、数据库、数据类型、环境变量、语句和关键字。

固定宽度粗体

显示用户应该按字面意义输入的命令或其他文本。

固定宽度斜体

显示应用用户提供的值或由上下文确定的值的文本。

提示

此元素表示提示或建议。

注意

此元素表示一般提示。

警告

此元素表示警告或注意事项。

我们将使用警告指出任何可能导致结果管道不可移植的情况,并呼吁您可以使用的可移植替代方法。

代码示例

可下载补充材料(例如代码示例等),网址为https://oreil.ly/Kubeflow_for_ML。这些代码示例根据 Apache 2 许可或下一节中描述的许可证提供。

有其他示例在它们自己的各自许可证下,您可能会发现它们有用。Kubeflow 项目有一个示例仓库,在撰写本文时可根据 Apache 2 许可获取。Canonical 还为 MicroK8s 用户提供了一套资源,可在此处找到。

使用代码示例

如果您有技术问题或使用代码示例时遇到问题,请发送电子邮件至bookquestions@oreilly.com

本书旨在帮助您完成工作。通常,如果本书提供了示例代码,您可以在您的程序和文档中使用它。除非您重复使用了本书中的大量代码片段,否则无需联系我们请求许可。出售或分发奥莱利图书的示例代码需要许可。引用本书并引用示例代码回答问题无需许可。将本书中大量示例代码合并到产品文档中需要许可。

更多许可细节可以在存储库中找到。

我们感谢但通常不要求署名。署名通常包括标题、作者、出版商和 ISBN。例如:“Kubeflow for Machine Learning by Holden Karau, Trevor Grant, Boris Lublinsky, Richard Liu, and Ilan Filonenko (O’Reilly). Copyright 2021 Holden Karau, Trevor Grant, Boris Lublinsky, Richard Liu, and Ilan Filonenko, 978-1-492-05012-4.”

如果您觉得您使用的代码示例超出了合理使用范围或上述许可,请随时通过permissions@oreilly.com与我们联系。

如何联系作者

欲提供反馈,请发送邮件至intro-to-ml-kubeflow@googlegroups.com。偶尔也会关于 Kubeflow 的随意言论,请在线关注我们:

特雷弗

霍尔登

勃里斯

理查德

伊兰

致谢

作者们想要感谢 O’Reilly Media 的所有人,特别是我们的编辑 Amelia Blevins 和 Deborah Baker,以及 Kubeflow 社区使这本书的出版成为可能。来自 Seldon 的 Clive Cox 和 Alejandro Saucedo 在 第八章 中做出了重要贡献,否则这本书将缺少关键部分。我们要感谢 Google Cloud Platform 提供的资源,使我们能够确保示例在 GCP 上运行。也许最重要的是,我们要感谢我们的审阅者,没有他们,这本书就不会以目前的形式存在。这包括 Taka Shinagawa,Pete MacKinnon,Kevin Haas,Chris Albon,Hannes Hapke 等人。感谢所有早期读者和书籍审阅者,感谢你们的贡献。

Holden

想要感谢她的女友 Kris Nóva,在调试她的第一个 Kubeflow PR 方面给予的帮助,以及整个 Kubeflow 社区对她的热情欢迎。她还要感谢她的妻子 Carolyn DeSimone,她的小狗 Timbit DeSimone-Karau(见图 P-1),以及她的玩偶们给予的写作支持。她要感谢 SF General 和 UCSF 的医生,让她能够完成这本书的写作(尽管她希望手不再疼痛),以及在医院和护理院探望她的每一个人。特别感谢 Ann Spencer,第一位教会她如何享受写作的编辑。最后,她要感谢她的约会伴侣 Els van Vessem,在她意外后的康复中给予的支持,尤其是通过阅读故事和提醒她对写作的热爱。

Timbit

图 P-1. Timbit the dog

Ilan

想要感谢所有在 Bloomberg 工作的同事们,他们花时间审阅、指导和鼓励他参与开源贡献。名单包括但不限于:Kimberly Stoddard,Dan Sun,Keith Laban,Steven Bower 和 Sudarshan Kadambi。他还要感谢他的家人—Galia,Yuriy 和 Stan,给予他无条件的爱和支持。

Richard

想要感谢 Google Kubeflow 团队,包括但不限于:Jeremy Lewi,Abhishek Gupta,Thea Lamkin,Zhenghui Wang,Kunming Qu,Gabriel Wen,Michelle Casbon 和 Sarah Maddox—没有他们的支持,这一切都不可能实现。他还想感谢他的猫 Tina(见图 P-2)在 COVID-19 期间的支持和理解。

Tina

图 P-2. Tina the cat

Boris

想要感谢他在 Lightbend 的同事们,特别是 Karl Wehden,在书写过程中给予的支持,对初版文本的建议和校对,以及他的妻子 Marina,在他长时间写作时的支持和供应。

Trevor

Trevor 想要感谢他的办公室同事 Apache 和 Meowska(见 图 P-3),提醒他午睡的重要性,以及去年聆听他关于 Kubeflow 的演讲的所有人(特别是那些听了糟糕版本,尤其尤其是听了糟糕版本但现在依然在阅读本书的人——你们是最棒的)。他还想要感谢他的妈妈、姐姐和弟弟多年来对他各种古怪行为的包容。

Apache 和 Meowska

图 P-3. Apache 和 Meowska

抱怨

作者还要感谢 API 变更带来的挑战,这使得写作本书变得如此令人沮丧。如果你也遇到 API 变更的困扰,要知道你并不孤单;它们几乎让每个人都感到恼火。

Holden 还想要感谢 Timbit DeSimone-Karau 曾经在她工作时捣乱挖坑的时刻。我们有一个特别的抱怨要向那个撞到 Holden 的人宣泄,导致本书发布进度减慢。

Trevor 有一个抱怨要向他的女朋友发表,她一直在这个项目中坚持要他求婚,而他一直在“努力”——如果在这本书出版前他还没向她求婚的话:凯蒂,你愿意嫁给我吗?

¹ 记得那个通过强化学习成为新纳粹的 Twitter 机器人吗?

第一章:Kubeflow:是什么以及谁会用它

如果您是试图将模型投入生产的数据科学家,或者是试图使模型可扩展和可靠的数据工程师,Kubeflow 提供了帮助的工具。Kubeflow 解决了如何将机器学习从研究转向生产的问题。尽管有常见的误解,Kubeflow 不仅仅是 Kubernetes 和 TensorFlow,您可以用它来处理各种机器学习任务。我们希望 Kubeflow 是适合您的正确工具,只要您的组织在使用 Kubernetes。“Kubeflow 的替代方案”介绍了一些您可能希望探索的选项。

本章旨在帮助您决定 Kubeflow 是否适合您的用例。我们将介绍您可以从 Kubeflow 中期待的好处,一些相关的成本,以及一些替代方案。在本章结束后,我们将深入探讨如何设置 Kubeflow 并构建端到端的解决方案,以帮助您熟悉基础知识。

模型开发生命周期

机器学习或模型开发基本上遵循路径:数据 → 信息 → 知识 → 洞察力。从数据中生成洞察力的这一路径可以用图 1-1 来形象描述。

模型开发生命周期(MDLC)是一个常用术语,用于描述训练和推断之间的流程。图 1-1 是这种连续交互的可视化表示,当触发模型更新时,整个周期重新开始。

模型开发生命周期

图 1-1. 模型开发生命周期

Kubeflow 的定位在哪里?

Kubeflow 是一套云原生工具,涵盖了 MDLC 的所有阶段(数据探索、特征准备、模型训练/调整、模型服务、模型测试和模型版本控制)。Kubeflow 还具有允许这些传统上独立工具无缝协作的工具。其工具的一个重要部分是管道系统,允许用户构建集成的端到端管道,连接其 MDLC 的所有组件。

Kubeflow 适用于既是数据科学家又是数据工程师,希望构建生产级机器学习实现的人士。Kubeflow 可以在您的开发环境中本地运行,也可以在生产集群上运行。通常在本地开发并在管道准备就绪后迁移管道。Kubeflow 提供了一个统一的系统,利用 Kubernetes 进行容器化和可扩展性,以实现管道的可移植性和可重复性。

为什么要容器化?

容器提供的隔离使得机器学习阶段具有可移植性和可重现性。容器化应用程序与您的机器其他部分隔离,并包含所有的需求(从操作系统开始)。¹ 容器化意味着不再需要包含“在我的机器上可行”或“哦,是的,我们忘记了一个,你需要这个额外的包”的对话。

容器是由可组合的层构建的,允许你使用另一个容器作为基础。例如,如果你想使用一个新的自然语言处理(NLP)库,你可以将其添加到现有容器之上,而不必每次从头开始。这种可组合性允许你重复使用一个通用的基础;例如,我们使用的 R 和 Python 容器都共享一个基础 Debian 容器。

关于使用容器的一个普遍担忧是开销问题。容器的开销取决于你的实现方式,但 IBM 的一篇论文² 发现容器的开销非常低,并且通常比虚拟化更快。在 Kubeflow 中,安装了一些可能不会使用的运算符会增加一些额外开销。在生产集群上这种开销可以忽略不计,但在笔记本电脑上可能会有所感觉。

提示

具有 Python 经验的数据科学家可以将容器视为一个功能强大的虚拟环境。除了虚拟环境中常见的功能外,容器还包括操作系统、软件包等一切。

为什么选择 Kubernetes?

Kubernetes 是一个用于自动化部署、扩展和管理容器化应用程序的开源系统。它允许我们的流水线在保持可移植性的同时实现可扩展性,从而避免被锁定在特定的云提供商中。³ 除了能够从单一机器切换到分布式集群外,你的机器学习流水线的不同阶段可以请求不同数量或类型的资源。例如,你的数据准备步骤可能更适合在多台机器上运行,而你的模型训练可能更适合在 GPU 或张量处理单元(TPU)上计算。在云环境中,这种灵活性尤为重要,你可以在需要时使用昂贵的资源来降低成本。

当然,你可以在 Kubernetes 上构建自己的容器化机器学习流水线,而不使用 Kubeflow;然而,Kubeflow 的目标是标准化这一过程,使其变得更加简单高效。⁴ Kubeflow 提供了一个通用接口,覆盖了你在机器学习实现中可能使用的工具。它还使得配置你的实现以使用像 TPU 这样的硬件加速器变得更加容易,而无需修改代码。

Kubeflow 的设计和核心组件

在机器学习领域,存在着多样化的库、工具集和框架选择。Kubeflow 不寻求重复发明轮子或提供“一刀切”的解决方案——相反,它允许机器学习从业者根据特定需求组合和定制自己的堆栈。它旨在简化规模化构建和部署机器学习系统的过程。这使得数据科学家可以将精力集中在模型开发上,而不是基础设施上。

Kubeflow 致力于通过三个特性来简化机器学习的问题:可组合性、可移植性和可扩展性。

可组合性

Kubeflow 的核心组件来自于数据科学工具,这些工具对于机器学习从业者已经非常熟悉。它们可以独立使用以促进机器学习的特定阶段,或者组合在一起形成端到端的流水线。

可移植性

通过基于容器的设计,并利用 Kubernetes 及其云原生架构,Kubeflow 不要求您锚定到任何特定的开发环境。您可以在笔记本电脑上进行实验和原型设计,并轻松部署到生产环境。

可扩展性

通过使用 Kubernetes,Kubeflow 可以根据集群的需求动态扩展,通过更改底层容器和机器的数量和大小。⁵

这些功能对于 MDLC 的不同部分至关重要。随着数据集的增长,可扩展性变得越来越重要。可移植性对于避免供应商锁定至关重要。可组合性使您可以自由地混合和匹配最适合工作的工具。

让我们快速浏览一下 Kubeflow 的一些组件及其如何支持这些功能。

使用笔记本进行数据探索

MDLC 始终从数据探索开始——绘图、分割和操作数据,以理解可能存在的见解。一个强大的工具提供了这些数据探索工具和环境,即 Jupyter。Jupyter 是一个开源的 Web 应用程序,允许用户创建和共享数据、代码片段和实验。由于其简单性和可移植性,Jupyter 在机器学习从业者中很受欢迎。

在 Kubeflow 中,您可以启动与集群及其其他组件直接交互的 Jupyter 实例,如图 1-2 所示。例如,您可以在笔记本电脑上编写 TensorFlow 分布式训练代码片段,并仅需点击几下即可启动训练集群。

在 Kubeflow 中运行的 Jupyter 笔记本

图 1-2. 在 Kubeflow 中运行的 Jupyter 笔记本

数据/特征准备

机器学习算法需要良好的数据才能有效,通常需要特殊工具来有效地提取、转换和加载数据。通常情况下,人们会对输入数据进行过滤、归一化和准备,以从结构化嘈杂的数据中提取深刻的特征。Kubeflow 支持几种不同的工具来完成这些任务:

  • Apache Spark(最流行的大数据工具之一)

  • TensorFlow Transform(与 TensorFlow Serving 集成,用于更轻松的推断)

这些独特的数据准备组件可以处理各种格式和数据大小,并设计为与您的数据探索环境兼容。⁶

注意

在 Kubeflow 管道中支持 Apache Beam 和 Apache Flink 是一个正在积极开发的领域。

训练

一旦准备好您的特征,您就可以构建和训练您的模型了。Kubeflow 支持多种分布式训练框架。截至撰写本文时,Kubeflow 支持:

在第七章中,我们将详细讨论 Kubeflow 如何训练 TensorFlow 模型,而第九章将探索其他选项。

超参数调整

如何优化您的模型架构和训练?在机器学习中,超参数是控制训练过程的变量。例如,模型的学习率应该是多少?神经网络中应该有多少隐藏层和神经元?这些参数不是训练数据的一部分,但它们对训练模型的性能有重大影响。

使用 Kubeflow,用户可以从一个他们不确定的训练模型开始,定义超参数搜索空间,Kubeflow 将处理其余的事情——使用不同的超参数启动训练作业,收集指标,并将结果保存到模型数据库中,以便比较它们的性能。

模型验证

在将模型投入生产之前,了解其可能的性能非常重要。用于超参数调整的同一工具可以执行模型验证的交叉验证。当您更新现有模型时,像 A/B 测试和多臂赌博机可以用于模型推断,以在线验证您的模型。

推断/预测

训练完模型后,下一步是在集群中部署模型以处理预测请求。Kubeflow 可以让数据科学家轻松将机器学习模型部署到生产环境中,并进行规模化部署。目前,Kubeflow 提供了一个多框架组件用于模型服务(KFServing),除了 TensorFlow Serving 和 Seldon Core 等现有解决方案。

在 Kubeflow 上提供多种类型的模型相对比较简单。在大多数情况下,无需自行构建或定制容器——只需将 Kubeflow 指向存储模型的位置,服务器就可以准备好服务请求。

一旦模型提供服务,就需要监视其性能,并可能进行更新。通过 Kubeflow 的云原生设计,可以实现这种监控和更新,并且将在第八章进一步扩展。

管道

现在我们已完成 MDLC 的所有方面,希望能够实现这些实验的可重复使用性和治理。为此,Kubeflow 将 MDLC 视为机器学习管道,并将其实现为一个图形,其中每个节点是工作流程中的一个阶段,如图 1-3 所示。Kubeflow Pipelines 是一个允许用户轻松组合可重复使用工作流程的组件。其功能包括:

  • 多步骤工作流程的编排引擎

  • 一个 SDK 用于与管道组件交互

  • 一个用户界面,允许用户可视化和跟踪实验,并与合作者分享结果

一个 Kubeflow 管道

图 1-3. 一个 Kubeflow 管道

组件概览

正如您所见,Kubeflow 内置了 MDLC 的所有部分的组件:数据准备、特征准备、模型训练、数据探索、超参数调整和模型推断,以及用于协调所有事务的管道。但您不仅限于作为 Kubeflow 一部分提供的组件。您可以在这些组件的基础上构建,甚至替换它们。对于偶尔替换组件来说,这可能没问题,但如果您发现自己想要替换 Kubeflow 的许多部分,您可能需要探索一些可用的替代方案。

Kubeflow 的替代方案

在研究社区内,存在许多提供与 Kubeflow 功能迥然不同的替代方案。最近的研究集中在模型开发和训练方面,基础设施、理论和系统方面取得了大幅进展。

另一方面,预测和模型服务接收到的关注相对较少。因此,数据科学从业者通常会将一系列关键系统组件混合在一起,这些组件被集成以支持跨多种工作负载的服务和推断,并持续演进的框架。

针对持续可用性和水平可扩展性的需求,像 Kubeflow 和其他各种解决方案正在行业中获得广泛认可,作为强大的架构抽象工具和令人信服的研究范围。

Clipper(RiseLabs)

一个有趣的替代 Kubeflow 的选择是 Clipper,一个由 RiseLabs 开发的通用低延迟预测服务系统。为了简化部署、优化和推断,Clipper 采用了分层架构系统。通过各种优化和模块化设计,Clipper 实现了与 TensorFlow Serving 相媲美的低延迟和高吞吐量预测,适用于三种不同推断成本的 TensorFlow 模型。

Clipper 分为两个抽象层,分别命名为 模型选择模型抽象 层。模型选择层非常复杂,使用自适应在线模型选择策略和各种集成技术。由于模型在应用程序生命周期内持续学习反馈,模型选择层可以在无需直接与策略层交互的情况下自我校准失败的模型。

Clipper 的模块化架构和与 Kubeflow 类似的容器化方法,使得可以跨框架共享缓存和批处理机制,同时获得可扩展性、并发性和灵活性增加新的模型框架的好处。

从理论上升级到功能端到端系统,Clipper 在科学界获得了认可,并且其架构设计的各个部分已被最近引入的机器学习系统采纳。尽管如此,我们还没有看到它是否会在工业上被大规模采用。

MLflow(Databricks)

MLflow 是由 Databricks 开发的开源机器学习开发平台。MLflow 的架构利用了与 Clipper 相同的架构范式,包括其与框架无关的特性,同时专注于其称为 Tracking、Projects 和 Models 的三个主要组件。

MLflow Tracking 作为一个 API,配合一个 UI,用于记录参数、代码版本、指标和输出文件。在机器学习中,跟踪参数、指标和工件非常重要,这一点非常强大。

MLflow Projects 提供了一个标准格式,用于打包可重复使用的数据科学代码,由一个 YAML 文件定义,可以利用 Anaconda 进行源代码控制和依赖管理。项目格式使得共享可重复生成的数据科学代码变得容易,因为对于机器学习从业者来说,可重复性非常重要。

MLflow Models 是打包多种格式的机器学习模型的约定。每个 MLflow Model 保存为一个包含任意文件和一个 MLmodel 描述文件的目录。MLflow 还提供模型注册表,显示部署模型和其创建元数据之间的衍生关系。

像 Kubeflow 一样,MLflow 仍在积极开发中,并拥有一个活跃的社区。

其他

由于机器学习开发中存在的挑战,许多组织已经开始建立内部平台来管理其机器学习生命周期。例如:Bloomberg、Facebook、Google、Uber 和 IBM 分别构建了数据科学平台、FBLearner Flow、TensorFlow Extended、Michelangelo 和 Watson Studio,以管理数据准备、模型训练和部署。⁷

机器学习基础设施格局不断演变和成熟,我们对开源项目如 Kubeflow 如何为机器学习开发带来亟需的简化和抽象感到兴奋。

我们的案例研究介绍

机器学习可以使用许多不同类型的数据,您使用的方法和工具可能会有所不同。为了展示 Kubeflow 的能力,我们选择了具有非常不同数据和最佳实践的案例研究。在可能的情况下,我们将使用这些案例研究的数据来探索 Kubeflow 及其某些组件。

修改过的国家标准技术研究所

在机器学习中,修改过的国家标准技术研究所(MNIST)通常指用于分类的手写数字数据集。手写数字的相对较小的数据大小,以及其作为示例的常见用途,使我们可以探索各种工具。在某些方面,MNIST 已成为机器学习的标准“hello world”示例之一。我们将 MNIST 作为第一个例子,以展示 Kubeflow 的端到端过程。

邮件列表数据

知道如何提出好问题是一种艺术。您是否曾在邮件列表上发布求助信息,却无人回应?什么是不同类型的问题?我们将查看一些公共 Apache 软件基金会的邮件列表数据,并尝试创建一个预测消息是否会被回复的模型。通过选择我们想要查看的项目和时间段来缩放这个示例,我们可以使用多种工具来解决它。

产品推荐

推荐系统是机器学习中最常见和易于理解的应用之一,有许多例子,从亚马逊的产品推荐到 Netflix 的电影建议。大多数推荐实现都基于协同过滤——一种假设,即如果 A 和 B 在一组问题上有相同的观点,那么 A 更有可能在其他问题上与 B 分享相同的观点,而不是随机选择的第三人。这种方法建立在一个成熟的算法上,有相当多的实现,包括 TensorFlow/Keras 实现。⁸

基于评分模型的问题之一是它们不能轻松地为具有非标度目标值的数据进行标准化,例如购买或频率数据。这篇优秀的 Medium 文章展示了如何将这类数据转换为可用于协同过滤的评分矩阵。我们的例子利用了Data Driven Investor 的数据和代码,以及Piyushdharkar 的 GitHub上描述的代码。我们将使用这个示例来探讨如何在 Jupyter 中构建初始模型,并继续构建生产管道。

CT 扫描

在我们编写本书时,全世界正在经历 COVID-19 大流行。AI 研究人员被要求应用方法和技术帮助医疗提供者理解这种疾病。一些研究显示,CT 扫描比传统的 RT-PCR 检测更有效进行早期检测。然而,诊断性 CT 扫描使用低剂量的辐射,因此“噪音”较多,也就是说,当使用更多辐射时,CT 扫描更加清晰。

一篇新的论文提出了一种开源解决方案,用于使用完全来自开源项目的现成方法去噪 CT 扫描(而不是专利的 FDA 批准解决方案)。我们实现这种方法来说明如何从学术文章走向实际解决方案,展示 Kubeflow 在创建可重现和可共享研究方面的价值,并为任何希望为抗击 COVID-19 贡献力量的读者提供起点。

结论

我们非常高兴您决定使用本书开始您的 Kubeflow 冒险之旅。本介绍应该让您对 Kubeflow 及其功能有所了解。然而,像所有的冒险一样,可能会有时候您的指南不足以带您通行。幸运的是,有一个社区资源收集,您可以与其他走同一条路的人进行交流。我们鼓励您加入 Kubeflow Slack 工作区,这是一个比较活跃的讨论区域之一。还有一个 Kubeflow 讨论邮件列表。同时也有一个 Kubeflow 项目页面

提示

如果您希望快速了解 Kubeflow 的端到端操作,可以参考一些Google codelabs

在第二章,我们将安装 Kubeflow 并使用它来训练和部署一个相对简单的机器学习模型,以便让您了解基础知识。

¹ 更多关于容器的信息,请参阅这个 Google 云资源。在涉及 GPU 或 TPU 的情况下,隔离细节变得更加复杂。

² W. Felter 等人在 2015 年 IEEE 国际系统与软件性能分析研讨会(ISPASS)上发表了《虚拟机和 Linux 容器的更新性能比较》,详见 doi: 10.1109/ISPASS.2015.7095802。

³ Kubernetes 通过提供容器编排层来实现这一点。关于 Kubernetes 的更多信息,请查看其文档

⁴ Spotify 能够将实验速率提高了约 7 倍;可以参考这篇Spotify Engineering 的博客文章

⁵ 像 Minikube 这样的本地集群仅限于一台机器,但大多数云集群可以根据需要动态更改机器的种类和数量。

⁶ 虽然还需要一些设置工作来使其运行,我们会在第五章中详细介绍。

⁷ 如果你想了解更多这些工具,可以参考Ian Hellstrom 2020 年的博客文章Austin Kodra 2019 年的文章,都是很好的概述。

⁸ 例如,可以查看Piyushdharkar 的 GitHub

第二章:你好,Kubeflow

欢迎来到激动人心的 Kubeflow 世界的第一步!

首先,我们将在您的机器上或云服务商上设置 Kubeflow。然后,我们将深入一个全面的例子。这个例子的目标是尽快训练模型并开始提供服务。在第一部分的某些部分,可能会让人觉得我们只是让您机械地输入命令。虽然我们希望您跟着操作,但我们强烈建议您在完成本书后重新阅读本章,反思您输入的命令,并考虑在阅读过程中您的理解有多大提升。

我们将为在本地机器上设置和测试我们的示例提供说明,并提供在真实集群上执行相同操作的说明链接。虽然我们会指向驱动所有这些的配置文件和 OCI 容器,但它们不是本章的重点;它们将在后续章节中详细讨论。本章的重点是一个您可以在家里跟随的端到端示例。

在未来的章节中,我们将深入探讨我们所做一切的“为什么”。

现在,只需享受这段旅程。

使用 Kubeflow 进行设置

Kubeflow 建立在 Kubernetes 之上的伟大之处之一是可以在本地进行初始开发和探索,随后转向更强大和分布式的工具。您的同一流水线可以在本地开发,然后迁移到集群中去。

提示

虽然您可以在本地开始使用 Kubeflow,但并非必须如此。您也可以选择在云提供商或本地 Kubernetes 集群上进行初始工作。

使用 Google 云平台(GCP)上的点击部署应用程序是开始使用 Kubeflow 的较快方法之一。如果您急于开始,请查看此 Kubeflow 文档页面

安装 Kubeflow 及其依赖项

在接近 Kubeflow 的最大需求——访问 Kubernetes 集群之前,让我们先设置好工具。Kubeflow 相当自包含,但确实需要kubectl。其余依赖项位于容器内,因此您无需担心安装它们。

提示

无论您使用本地还是远程的 Kubernetes 集群,将开发工具安装在本地将简化您的生活。

无论你使用的是哪种集群,你都需要安装 Kubeflow 的核心依赖项 kubectl,用于与 Kubernetes 进行通信。kubectl 被广泛打包,不同的安装选项在 Kubernetes 文档 中有详细介绍。如果你想使用软件包管理器安装 kubectl,Ubuntu 用户可以使用 snap(参见 示例 2-1),Mac 用户可以使用 Homebrew(参见 示例 2-2);其他安装选项也在 Kubernetes 文档 中有涵盖。kubectl 也可以作为一个本地二进制文件从这个 Kubernetes 文档页面 安装。

示例 2-1. 使用 snap 安装 kubectl
sudo snap install kubectl --classic
示例 2-2. 使用 Homebrew 安装 kubectl
brew install kubernetes-cli

一旦你安装了最低限度的依赖项,你现在可以从 这个 GitHub 仓库 安装 Kubeflow,就像在 示例 2-3 中描述的那样。

示例 2-3. 安装 Kubeflow
PLATFORM=$(uname) # Either Linux or Darwin
export PLATFORM
mkdir -p ~/bin
#Configuration
export KUBEFLOW_TAG=1.0.1
# ^ You can also point this to a different version if you want to try
KUBEFLOW_BASE="https://api.github.com/repos/kubeflow/kfctl/releases"
# Or just go to https://github.com/kubeflow/kfctl/releases
KFCTL_URL=$(curl -s ${KUBEFLOW_BASE} |\
	      grep http |\
	      grep "${KUBEFLOW_TAG}" |\
	      grep -i "${PLATFORM}" |\
	      cut -d : -f 2,3 |\
	      tr -d '\" ' )
wget "${KFCTL_URL}"
KFCTL_FILE=${KFCTL_URL##*/}
tar -xvf "${KFCTL_FILE}"
mv ./kfctl ~/bin/
rm "${KFCTL_FILE}"
# It's recommended that you add the scripts directory to your path
export PATH=$PATH:~/bin

现在你应该已经在你的机器上安装了 Kubeflow。为了确保它已安装成功,请运行 kfctl version 并检查其返回的版本是否符合预期。现在让我们介绍一些可选的工具,可以帮助你简化未来使用 Kubeflow 的过程。

设置本地 Kubernetes

能够在本地和生产环境中运行相同的软件是 Kubeflow 的一个巨大优势之一。为了支持这一点,你需要安装一个本地版本的 Kubernetes。虽然有几种选择,但我们发现 Minikube 是最简单的。Minikube 是一个本地版本的 Kubernetes,允许你在本地计算机上模拟一个集群。另外两个常见的本地 Kubeflow 版本选择是 microk8s,支持多种 Linux 平台,以及使用 Vagrant 启动 VM 的 MiniKF,用于在 Kubernetes 上运行 Kubeflow。

提示

严格来说,不是必须安装本地 Kubernetes 集群,但许多数据科学家和开发人员发现拥有一个本地集群进行测试非常有帮助。

Minikube

Minikube 是一个可以运行 Kubeflow 的本地 Kubernetes 版本。Minikube 的安装指南可以在 主 Kubernetes 文档页面Kubeflow 专用页面 找到。

Minikube 自动设置中最常见的失败是缺少虚拟化程序或 Docker。无论你使用的是哪种操作系统,你都可以使用 VirtualBox;不过,其他选项如 Linux 上的 KVM2、Windows 上的 Hyper-V,以及 macOS 上的 HyperKit 也同样适用。

提示

在启动 Minikube 时,请确保为其分配足够的内存和磁盘空间,例如,minikube start --cpus 16 --memory 12g --disk-size 15g。注意:你并不需要 16 个 CPU 核心来运行它;这只是 Minikube 将使用的虚拟 CPU 数量。

设置你的 Kubeflow 开发环境

Kubeflow 的流水线系统是用 Python 构建的,本地安装 SDK 可以让您更快地构建流水线。然而,如果您无法在本地安装软件,仍可以使用 Kubeflow 的 Jupyter 环境来构建您的流水线。

设置 Pipeline SDK

要开始设置 Pipeline SDK,您需要安装Python。许多人发现为其不同项目创建隔离的虚拟环境非常有用;请参阅示例 2-4 了解如何操作。

示例 2-4. 创建虚拟环境
virtualenv kfvenv --python python3
source kfvenv/bin/activate

现在您可以使用 pip 命令安装 Kubeflow Pipelines 包及其要求,如示例 2-5 所示。

示例 2-5. 安装 Kubeflow Pipeline SDK
URL=https://storage.googleapis.com/ml-pipeline/release/latest/kfp.tar.gz
pip install "${URL}" --upgrade

如果您使用虚拟环境,您需要在每次使用 Pipeline SDK 时激活它。

除了 SDK 外,Kubeflow 还提供了许多组件。检出标准组件的固定版本,例如示例 2-6,可以创建更可靠的流水线。

示例 2-6. 克隆 Kubeflow Pipelines 存储库
  git clone --single-branch --branch 0.3.0 https://github.com/kubeflow/pipelines.git

设置 Docker

Docker是最低要求的重要组成部分,允许您定制和添加库和其他功能到您自己的自定义容器中。我们将在第三章详细介绍 Docker。在 Linux 中,您可以通过标准软件包管理器或在 macOS 上使用 Homebrew 来安装 Docker。

除了安装 Docker 外,您还需要一个存储容器映像的地方,称为容器注册表。容器注册表将被您的 Kubeflow 集群访问。Docker 背后的公司提供Docker Hub,RedHat 提供Quay,这是一个云中立的平台,您可以使用。或者,您还可以使用您的云提供商的容器注册表。¹ 云供应商特定的容器注册表通常提供更高的图像存储安全性,并可以自动配置您的 Kubernetes 集群以获取这些图像所需的权限。在我们的示例中,我们假设您通过环境变量$CONTAINER_REGISTRY设置了您的容器注册表。

提示

如果您使用的注册表不在 Google Cloud Platform 上,您需要按照Kaniko 配置指南配置 Kubeflow Pipelines 容器构建器,以便访问您的注册表。

确保你的 Docker 安装已正确配置,你可以编写一行命令 Dc 并将其推送到你的注册表中。对于 Dockerfile,我们将使用 FROM 命令来指示我们基于 Kubeflow 的 TensorFlow 笔记本容器镜像构建,就像在 示例 2-7 中展示的一样(我们将在 第九章 中详细讨论这个)。当你推送一个容器时,需要指定 tag,它确定了镜像名称、版本以及存储位置,就像在 示例 2-8 中展示的一样。

示例 2-7. 指定新容器是基于 Kubeflow 的容器
FROM gcr.io/kubeflow-images-public/tensorflow-2.1.0-notebook-cpu:1.0.0
示例 2-8. 构建新容器并推送到注册表以供使用
IMAGE="${CONTAINER_REGISTRY}/kubeflow/test:v1"
docker build  -t "${IMAGE}" -f Dockerfile .
docker push "${IMAGE}"

有了这个设置,你现在可以开始定制 Kubeflow 中的容器和组件以满足你的需求。我们将在 第九章 中深入讨论如何从头开始构建容器。随着我们在未来章节的深入,我们将使用这种模式在需要时添加工具。

编辑 YAML

虽然 Kubeflow 在很大程度上抽象了 Kubernetes 的细节,但在查看或修改配置时仍然有用。大部分 Kubernetes 配置都以 YAML 形式表示,因此设置工具以便轻松查看和编辑 YAML 将是有益的。大多数集成开发环境(IDE)都提供了某种用于编辑 YAML 的工具,但你可能需要单独安装这些工具。

提示

对于 IntelliJ,有一个 YAML 插件。对于 emacs,有许多可用于 YAML 编辑的模式,包括 yaml-mode(可以从 Milkypostman’s Emacs Lisp Package Archive (MELPA) 安装)。Atom 作为一个包括语法高亮的插件 YAML。如果你使用不同的 IDE,在探索可用的插件之前,不要因为更好的 YAML 编辑而抛弃它。无论使用何种 IDE,你也可以使用 YAMLlint 网站 来检查你的 YAML 文件。

创建我们的第一个 Kubeflow 项目

首先,我们需要创建一个 Kubeflow 项目来工作。要创建 Kubeflow 部署,我们使用 kfctl 程序。² 在使用 Kubeflow 时,你需要指定一个配置文件清单,配置构建内容以及构建方式,不同的云服务提供商有不同的清单文件。

我们将从使用基本配置的示例项目开始,就像在 示例 2-9 中展示的那样。在这个项目中,我们将为我们的 MNIST 示例构建一个简单的端到端流水线。我们选择这个示例,因为它是机器学习中的标准“Hello World”。

示例 2-9. 创建第一个示例项目
# Pick the correct config file for your platform from
# https://github.com/kubeflow/manifests/tree/[version]/kfdef
# You can download and edit the configuration at this point if you need to.
# For generic Kubernetes with Istio:
MANIFEST_BRANCH=${MANIFEST_BRANCH:-v1.0-branch}
export MANIFEST_BRANCH
MANIFEST_VERSION=${MANIFEST_VERSION:-v1.0.1}
export MANIFEST_VERSION

KF_PROJECT_NAME=${KF_PROJECT_NAME:-hello-kf-${PLATFORM}}
export KF_PROJECT_NAME
mkdir "${KF_PROJECT_NAME}"
pushd "${KF_PROJECT_NAME}"

manifest_root=https://raw.githubusercontent.com/kubeflow/manifests/
# On most environments this will create a "vanilla" Kubeflow install using Istio.
FILE_NAME=kfctl_k8s_istio.${MANIFEST_VERSION}.yaml
KFDEF=${manifest_root}${MANIFEST_BRANCH}/kfdef/${FILE_NAME}
kfctl apply -f $KFDEF -V
echo $?

popd

示例 2-9 假设你正在使用一个现有的 Kubernetes 集群(如本地 Minikube)。当你运行 kfctl apply 时,你将看到很多状态消息,甚至可能会看到一些错误消息。只要它最后输出 0,你可以安全地忽略大多数错误,因为它们会自动重试。

警告

这个部署过程可能需要30 分钟

如果您决定直接使用云服务提供商,请参阅Kubeflow 安装指南了解如何开始。

警告

在 Kubeflow 完全部署之前,Kubeflow 用户界面可能会出现,并在这时访问可能意味着您没有正确的命名空间。为确保 Kubeflow 准备就绪,请运行 kubectl get pods --all-namespaces -w 并等待所有的 pod 变为 RUNNING 或 COMPLETED。如果您看到 pod 被抢占,请确保您启动了足够的 RAM 和磁盘空间的集群。如果您无法在本地启动足够大的集群,请考虑使用云服务提供商。(Ilan 和 Holden 目前正在撰写有关此主题的博客文章。)

训练和部署模型

在传统的机器学习文本中,训练阶段通常受到最多关注,只有一些简单的部署示例,而对模型管理的处理非常少。本书假设您是一名了解如何选择正确模型/算法或与了解此领域的人合作的数据科学家。我们比传统的机器学习文本更注重部署和模型管理。

训练和监控进度

接下来的步骤是使用 Kubeflow Pipeline 训练模型。我们将使用一个预先创建的训练容器³来下载训练数据并训练模型。对于示例 2-10,我们在 train_pipeline.py 中有一个预先构建的工作流,在本书 GitHub 示例存储库的 ch2 文件夹中训练一个 RandomForestClassifier

示例 2-10. 创建训练工作流示例
dsl-compile --py train_pipeline.py --output job.yaml

如果在这里遇到问题,您应该查看Kubeflow 故障排除指南

Kubeflow 用户界面,如图 2-1 所示,可以通过几种不同的方式访问。对于本地部署,快速端口转发是最简单的开始方式:只需运行 kubectl port-forward svc/istio-ingressgateway -n istio-system 7777:80 然后访问 localhost:7777。如果您在 GCP 上部署了 Kubeflow,您应该访问 https://<deployment_name>.endpoints.<project_name>.cloud.goog。否则,您可以通过运行 kubectl get ingress -n istio-system 来获取网关服务的地址。

kubeflow-ui

图 2-1. Kubeflow Web 用户界面

点击流水线,或者在根 URL 中添加 _/pipeline/,您应该能够看到流水线 Web 用户界面,就像图 2-2 所示。

argo-ui

图 2-2. 流水线 Web 用户界面

从这里,我们可以上传我们的流水线。一旦上传了流水线,我们可以使用同样的 Web 用户界面来创建流水线的运行。点击上传的流水线后,您将能够创建一个运行,如图 2-3 所示。

pipeline-detail

图 2-3. 流水线详细页面

测试查询

最后,让我们查询我们的模型并监控结果。 "理智检查"是一个简单的测试,用于确保我们的模型做出的预测在理论上是合理的。例如 - 我们试图猜测写的是什么数字。如果我们的模型返回像77橙色果味饮料错误这样的答案,那都不会通过理智检查。我们希望看到的是 0 到 9 之间的数字。在投入生产之前对模型进行理智检查总是一个明智的选择。

Web UI 和模型服务通过相同的 Istio 网关公开。因此,该模型将在http://<WEBUI_URL>/seldon<mnist-classifier/api<v0.1/predictions处可用。如果您使用 Google IAP,您可能会发现 iap_curl 项目有助于发出请求。

有一个 Python脚本可用,用于从 MNIST 数据集中提取图像,将其转换为向量,显示图像,并将其发送到模型。将图像转换为向量通常是预测转换的一部分;我们将在第八章中介绍更多内容。例子 2-11 是一个相当清晰的 Python 示例,演示了如何查询模型。该模型返回了 10 个数字和提交的向量表示特定数字的概率的 JSON。具体来说,我们需要一张手写数字的图像,我们可以将其转换为一系列值。

例 2-11. 模型查询示例
import requests
import numpy as np

from tensorflow.examples.tutorials.mnist import input_data
from matplotlib import pyplot as plt

def download_mnist():
    return input_data.read_data_sets("MNIST_data/", one_hot=True)

def gen_image(arr):
    two_d = (np.reshape(arr, (28, 28)) * 255).astype(np.uint8)
    plt.imshow(two_d, cmap=plt.cm.gray_r, interpolation='nearest')
    return plt
mnist = download_mnist()
batch_xs, batch_ys = mnist.train.next_batch(1)
chosen = 0
gen_image(batch_xs[chosen]).show()
data = batch_xs[chosen].reshape((1, 784))
features = ["X" + str(i + 1) for i in range(0, 784)]
request = {"data": {"names": features, "ndarray": data.tolist()}}
deploymentName = "mnist-classifier"
uri = "http://" + AMBASSADOR_API_IP + "/seldon/" + \
    deploymentName + "/api/v0.1/predictions"

response = requests.post(uri, json=request)

例如,查看图 2-4 中手写的3

kfml 0204

图 2-4. 手写 3

这将返回以下内容:

{'data': {'names': ['class:0',
		    'class:1',
		    'class:2',
		    'class:3',
		    'class:4',
		    'class:5',
		    'class:6',
		    'class:7',
		    'class:8',
		    'class:9'],
	  'ndarray':[[0.03333333333333333,
		      0.26666666666666666,
		      0.03333333333333333,
		      0.13333333333333333, ## It was actually this
		      0.1,
		      0.06666666666666667,
		      0.1,
		      0.26666666666666666,
		      0.0,
		      0.0]]},
 'meta': {'puid': 'tb02ff58vcinl82jmkkoe80u4r', 'routing': {}, 'tags': {}}}

我们可以看到,即使我们写的是一个相当清晰的 3,但模型最佳猜测是 17。也就是说,RandomForestClassifier对手写识别来说是一个糟糕的模型 - 所以这并不是一个令人惊讶的结果。我们之所以使用RandomForestClassifier有两个原因:首先,为了在第八章中说明模型的可解释性,其次,让您尝试一个更合理的模型并比较性能。

注意

尽管我们在此处部署了端到端示例,但您应该始终在真实生产之前进行验证。

超越本地部署

你们中的一些人一直在本地 Kubernetes 部署上尝试这个。Kubeflow 的一大优势是能够利用 Kubernetes 进行扩展。Kubernetes 可以在单台计算机或许多计算机上运行,并且一些环境可以根据需要动态添加更多资源。尽管 Kubernetes 是一个行业标准,但根据您的提供商不同,Kubeflow 的设置步骤可能会有所不同。Kubeflow 入门指南提供了 GCP、AWS、Azure、IBM Cloud 和 OpenShift 的安装说明。一旦 Kubeflow 安装在您的 Kubernetes 集群上,您可以再次尝试相同的示例,看看相同的代码如何运行,或者相信我们并转向更有趣的问题。

提示

在云提供商上部署时,Kubeflow 可以创建不仅仅是 Kubernetes 资源,也应该删除的其他资源。例如,在 Google 上,您可以通过访问部署管理器来删除辅助服务。

结论

在本章中,您第一次真正体验了 Kubeflow。您现在已经正确配置了开发环境,并且拥有了一个可以在本书其余部分中使用的 Kubeflow 部署。我们介绍了一个简单的端到端示例,使用标准的 MNIST 数据集,让您看到了 Kubeflow 不同核心组件的实际运行情况。我们介绍了流水线,它将所有 Kubeflow 组件联系在一起,并且您使用它来训练模型。在第三章中,我们将探索 Kubeflow 的设计并设置一些可选组件。理解设计将帮助您选择合适的组件。

¹ 只需搜索“cloudname”加上容器注册表名称以获取文档。

² 不要将其与传统的kfctl.sh脚本混淆。

³ 该容器来源于此 GitHub 仓库

第三章:Kubeflow 设计:进阶内容

您已经通过了两章。做得好。到目前为止,您已经决定学习 Kubeflow 并完成了一个简单的示例。现在,我们希望退后一步,详细查看每个组件。图 3-1 显示了主要的 Kubeflow 组件及其在整体架构中的角色。

Kubeflow 架构

图 3-1. Kubeflow 架构

Essentially, we’ll look at the core elements that make up our example deployment as well as the supporting pieces. In the chapters that follow, we will dig into each of these sections in greater depth.

话虽如此,让我们开始吧。

环顾中央仪表板

您与 Kubeflow 的主要交互界面是中央仪表板(参见 图 3-2),它允许您访问大多数 Kubeflow 组件。根据您的 Kubernetes 提供程序,您的入口可能需要多达半小时才能变得可用。

中央仪表板

图 3-2. 中央仪表板
Note

虽然它应该是自动的,但如果您没有为您的工作创建命名空间,请按照Kubeflow 的“手动配置文件创建”说明操作。

从中央仪表板的主页,您可以访问 Kubeflow 的流水线、笔记本、Katib(超参数调优)和 artifact 存储。接下来我们将介绍这些组件的设计及其使用方法。

Notebooks (JupyterHub)

大多数项目的第一步是某种形式的原型设计和实验。Kubeflow 用于此目的的工具是JupyterHub——一个多用户中心,可以生成、管理和代理多个单用户Jupyter 笔记本实例。Jupyter 笔记本支持整个计算过程:开发、文档编写、代码执行以及结果通信。

要访问 JupyterHub,请转到主 Kubeflow 页面并单击笔记本按钮。在笔记本页面上,您可以连接到现有服务器或创建一个新服务器。

要创建一个新服务器,您需要指定服务器名称和命名空间,选择一个镜像(从 CPU 优化、GPU 优化或您可以创建的自定义镜像中选择),并指定资源需求——CPU/内存、工作空间、数据卷、自定义配置等等。一旦服务器创建完成,您可以连接到它并开始创建和编辑笔记本。

为了允许数据科学家在不离开笔记本环境的情况下进行集群操作,Kubeflow 在提供的笔记本镜像中添加了kubectl,这使开发人员可以使用笔记本创建和管理 Kubernetes 资源。Jupyter 笔记本 pod 运行在特殊的服务账户 default-editor 下,该账户在命名空间范围内对以下 Kubernetes 资源有权限:

  • Pods

  • Deployments

  • Services

  • Jobs

  • TFJobs

  • PyTorchJobs

你可以将此帐户绑定到自定义角色,以限制/扩展笔记本服务器的权限。这允许笔记本开发人员在不离开笔记本环境的情况下执行所有(由角色允许的)Kubernetes 命令。例如,可以通过在 Jupyter 笔记本中直接运行以下命令来创建一个新的 Kubernetes 资源:

!kubectl create -f myspec.yaml

你的 yaml 文件的内容将决定创建的资源。如果你不习惯创建 Kubernetes 资源,不用担心——Kubeflow 的管道包含工具,可以为你创建它们。

为了进一步增强 Jupyter 的功能,Kubeflow 还在笔记本中提供了对管道和元数据管理的支持(稍后在 “元数据” 中描述)。Jupyter 笔记本还可以直接启动分布式训练作业。

训练操作符

JupyterHub 是进行数据初步实验和原型化 ML 作业的好工具。但是,当转向在生产环境中进行训练时,Kubeflow 提供了多个训练组件来自动执行机器学习算法,包括:

在 Kubeflow 中,分布式训练作业由应用程序特定的控制器管理,称为操作符。这些操作符扩展了 Kubernetes API 来创建、管理和操作资源的状态。例如,要运行一个分布式 TensorFlow 训练作业,用户只需提供描述期望状态的规范(工作节点数和参数服务器等),TensorFlow 操作符组件将处理其余并管理训练作业的生命周期。

这些操作符允许重要部署概念如可伸缩性、可观察性和故障转移的自动化。它们还可以被管道用来链式执行系统其他组件的执行。

Kubeflow 管道

除了提供实施特定功能的专用参数外,Kubeflow 还拥有 Pipelines,允许你编排机器学习应用的执行。这一实现基于 Argo Workflows,一个面向 Kubernetes 的开源容器本地工作流引擎。Kubeflow 安装所有 Argo 组件。

在高层次上,管道的执行包含以下 组件

Python SDK

你可以使用 Kubeflow 管道的 领域特定语言(DSL)创建组件或指定管道。

DSL 编译器

DSL 编译器 将你的管道的 Python 代码转换为静态配置(YAML)。

Pipeline 服务

Pipeline 服务从静态配置创建管道运行。

Kubernetes 资源

流水线服务调用 Kubernetes API 服务器来创建必要的 Kubernetes 自定义资源定义(CRD)来运行流水线。

编排控制器

一组编排控制器执行完成由 Kubernetes 资源(CRD)指定的流水线执行所需的容器。这些容器在虚拟机上的 Kubernetes Pod 中执行。一个示例控制器是 Argo Workflow 控制器,它编排任务驱动的工作流程。

工件存储

Kubernetes Pod 存储两种类型的数据:

元数据

实验、作业、运行、单一标量指标(通常用于排序和过滤目的的汇总指标),等等。Kubeflow Pipelines 将元数据存储在 MySQL 数据库中。

工件

流水线包、视图、如时间序列等大规模指标(通常用于调查单个运行的性能和调试),等等。Kubeflow Pipelines 将工件存储在类似于 MinIO 服务器Google Cloud Storage (GCS)Amazon S3 的工件存储中。

Kubeflow Pipelines 允许您使您的机器学习作业可重复,并处理新数据。它提供了一个直观的 Python DSL 来编写流水线。然后将您的流水线编译为现有的 Kubernetes 工作流引擎(目前是 Argo Workflows)。Kubeflow 的流水线组件使得使用和协调构建端到端机器学习项目所需的不同工具变得简单。此外,Kubeflow 可以跟踪数据和元数据,改进我们理解作业的方式。例如,在 第五章 中,我们使用这些工件来理解模式。流水线可以暴露出底层机器学习算法的参数,使得 Kubeflow 能够执行调整。

超参数调整

为您的训练模型找到合适的超参数集合可能是一项具有挑战性的任务。传统的方法如网格搜索可能耗时且相当乏味。大多数现有的超参数系统与一个机器学习框架绑定,并且在搜索参数空间时只有几个选项。

Kubeflow 提供了一个组件(称为 Katib),允许用户在 Kubernetes 集群上轻松执行超参数优化。Katib 受到 Google Vizier 的启发,这是一个黑盒优化框架。它利用高级搜索算法如贝叶斯优化来找到最优的超参数配置。

Katib 支持 超参数调整,可以与包括 TensorFlow、MXNet 和 PyTorch 在内的任何深度学习框架一起运行。

如同 Google Vizier,Katib 基于四个主要概念:

实验

在可行空间上进行的单次优化运行。每个实验包含描述可行空间的配置,以及一组试验。假设客观函数f(x)在实验过程中不会改变。

试验

一组参数值,x,将导致f(x)的单次评估。一个试验可以“完成”,这意味着它已经被评估并且客观值f(x)已经被分配,否则它是“挂起”的。一个试验对应一个作业。

作业

负责评估挂起试验并计算其客观值的过程。

建议

构建参数集的算法。目前,Katib 支持以下探索算法:

利用这些核心概念,您可以提高模型的性能。由于 Katib 不限于一种机器学习库,因此您可以在几乎不进行修改的情况下探索新的算法和工具。

模型推断

Kubeflow 使得在生产环境中大规模部署机器学习模型变得容易。它提供了多种模型服务选项,包括TFServingSeldon servingPyTorch servingTensorRT。它还提供了一个总体实现,KFServing,它通用化了模型推断的自动扩展、网络、健康检查和服务器配置等问题。

整体实现基于利用Istio(稍后详述)和Knative serving——基于 Kubernetes 的无服务器容器。正如在Knative 文档中定义的那样,Knative serving 项目提供中间件原语,使以下功能成为可能:

  • 无服务器容器的快速部署

  • 自动扩展至零和缩减

  • Istio 组件的路由和网络编程

由于模型服务本质上是尖锐的,快速的扩展和缩减至零至关重要。Knative serving 简化了对连续模型更新的支持,通过自动将请求路由到较新的模型部署中。这需要将未使用的模型缩减至零(最小化资源利用),同时保持可供回滚使用。由于 Knative 是云原生的,它从其基础设施堆栈中受益,并因此提供了所有存在于 Kubernetes 中的监控功能,例如日志记录、跟踪和监控。KFServing 还利用Knative 事件提供可选支持可插拔事件源。

类似于 Seldon,每个 KFServing 部署都是一个编排者,将以下组件连接在一起:

预处理器

负责将输入数据转换为模型服务所需的内容/格式的可选组件

预测器

负责实际模型服务的必需组件

后处理器

负责将模型服务结果转换/丰富为输出所需内容/格式的可选组件

其他组件可以增强整体模型服务的实现,但不属于主要执行流水线。例如异常检测和模型可解释性工具可以在此环境中运行,而不会减慢整体系统速度。

尽管这些独立组件和技术已存在很长时间,但将它们集成到 Kubeflow 的服务系统中可以减少将新模型投入生产中的复杂性。

除了直接支持 ML 操作的组件外,Kubeflow 还提供几个支持组件。

元数据

Kubeflow 的一个重要组件是元数据管理,提供捕获和跟踪模型创建信息的能力。许多组织每天构建数百个模型,但很难管理所有与模型相关的信息。ML Metadata 是记录和检索与 ML 开发人员和数据科学家工作流相关的元数据的基础设施和库。可以在元数据组件中注册的信息包括:

  • 用于模型创建的数据来源

  • 组件/流水线步骤生成的工件

  • 这些组件/步骤的执行

  • 流水线和相关的谱系信息

ML Metadata 跟踪 ML 工作流中所有组件和步骤的输入和输出及其谱系。这些数据支持表 3-1 中列出的几个重要功能,并显示在图 3-3 中。

表 3-1. ML Metadata 操作示例

操作 示例
列出特定类型的所有工件。 所有已经训练的模型。
比较相同类型的两个工件。 比较两个实验的结果。
显示所有相关执行及其输入和输出工件的 DAG。 可视化实验的工作流以进行调试和发现。
显示工件的创建方式。 查看用于模型的数据;执行数据保留计划。
标识所有使用特定工件创建的工件。 用有问题数据标记从特定数据集训练的所有模型。
确定执行是否已在相同输入上运行过。 确定组件/步骤是否已完成相同工作,以便可以重用先前的输出。
记录和查询工作流运行的上下文。 跟踪工作流运行的所有者和变更;按实验分组谱系;按项目管理工件。

Metadata Diagram

图 3-3. 元数据图

组件摘要

Kubeflow 的魔力在于使所有这些传统上不同的组件协同工作。虽然 Kubeflow 当然不是唯一一个将机器学习领域不同部分集成在一起的系统,但它在支持各种组件方面的灵活性是独一无二的。除此之外,由于它在标准 Kubernetes 上运行,您可以根据需要添加自己的组件。大部分工具集成的魔力发生在 Kubeflow 的管道内部,但一些支持组件对于让这些工具相互交互非常重要。

支持组件

虽然这些组件并未明确暴露在 Kubeflow 之外,但它们在整个 Kubeflow 生态系统中扮演着重要角色。让我们简要讨论每一个。我们也鼓励您自行研究它们。

MinIO

流水线架构的基础是共享存储。今天的常见做法是将数据存储在外部存储中。不同的云提供商有不同的解决方案,如 Amazon S3、Azure 数据存储、Google Cloud Storage 等。这种多样化的解决方案使得从一个云提供商迁移到另一个云提供商变得复杂。为了最小化这种依赖性,Kubeflow 附带了 MinIO,一个专为大规模私有云基础设施设计的高性能分布式对象存储服务器。MinIO 不仅适用于私有云,还可以作为公共 API 的一致性网关。

MinIO 可以以多种不同的配置部署。在 Kubeflow 中的默认配置是单容器模式,当 MinIO 在一个容器中使用 Kubernetes 内置的持久存储时。分布式 MinIO 允许将多个卷汇集到一个单一的对象存储服务中。¹它还可以承受多个节点故障,并确保完全的数据保护(故障数取决于您的复制配置)。MinIO 网关在 Azure Blob 存储、Google Cloud 存储、Gluster 或 NAS 存储上提供了 S3 API。网关选项最灵活,允许您创建无缩放限制的云独立实现。

虽然 Kubeflow 的默认 MinIO 设置可以使用,但您可能希望进一步配置它。Kubeflow 安装了 MinIO 服务器和 UI。您可以访问 MinIO UI 并探索存储的内容,如图 3-4 中所示,通过端口转发(如示例 3-1)或暴露入口。您可以使用 Kubeflow 的默认 minio/minio123 用户登录。

示例 3-1. 设置端口转发
kubectl port-forward -n kubeflow svc/minio-service 9000:9000 &

Minio 仪表板

图 3-4. MinIO 仪表板

此外,您还可以安装MinIO CLI (mc)来使用工作站上的命令访问 MinIO 安装。对于 macOS,使用 Homebrew,如示例 3-2 中所示。对于 Linux Ubuntu,使用 snap,如示例 3-3 中所示。

示例 3-2. 在 Mac 上安装 MinIO
brew install minio/stable/minio
示例 3-3. 在 Linux 上安装 MinIO
pushd ~/bin
wget https://dl.min.io/client/mc/release/linux-amd64/mc
chmod a+x mc

你需要配置 MinIO 以与正确的端点进行通信,如示例 3-4 所示。

示例 3-4. 配置 MinIO 客户端与 Kubeflow 的 MinIO 对话
mc config host add minio http://localhost:9000 minio minio123

一旦你配置了命令行,你可以像示例 3-5 那样创建新的存储桶,或者改变你的设置。

示例 3-5. 使用 MinIO 创建一个存储桶
mc mb minio/kf-book-examples

MinIO 提供本地和与 S3 兼容的 API。由于我们的大多数软件可以与 S3 对话,所以与 S3 兼容的 API 是最重要的。

警告

使用构建在 Hadoop 之上的系统(主要是基于 Java 的)需要 Hadoop 2.8 或更高版本。

Kubeflow 安装将 MinIO 凭据硬编码为 minio/minio123,你可以直接在你的应用程序中使用,但通常最好使用密钥,特别是如果你可能会切换到常规的 S3。Kubernetes 密钥为你提供了一种在集群上存储凭据的方式,与你的应用程序分开。² 要为 MinIO 或 S3 设置一个密钥,可以创建一个类似于 示例 3-6 的密钥文件。在 Kubernetes 中,ID 和密钥的密钥值必须进行 base64 编码。要编码一个值,请运行命令 echo -n *xxx* | base64

示例 3-6. MinIO 的示例密钥
apiVersion: v1
kind: Secret
metadata:
  name: minioaccess
  namespace: mynamespace
data:
  AWS_ACCESS_KEY_ID: xxxxxxxxxx
  AWS_SECRET_ACCESS_KEY: xxxxxxxxxxxxxxxxxxxxx

将这个 YAML 文件保存为 minioaccess.yaml,并使用命令 kubectl apply -f minioaccess.yaml 部署这个密钥。现在我们理解了管道阶段之间的数据通信,让我们努力理解组件之间的网络通信。

Istio

Kubeflow 的另一个支持组件是Istio,一个服务网格,提供诸如服务发现、负载均衡、故障恢复、指标、监控、速率限制、访问控制和端到端认证等重要功能。Istio 作为一个服务网格,透明地层叠在 Kubernetes 集群之上。它可以集成到任何日志平台、遥测或策略系统,并推广一种统一的方式来保护、连接和监控微服务。Istio 的实现将每个服务实例与一个旁路网络代理(sidecar)共存。所有来自单个服务实例的网络流量(HTTP、REST、gRPC 等)都通过其本地旁路代理流向适当的目标。因此,服务实例并不知晓整个网络,它只知道其本地代理。实际上,分布式系统网络已经被从服务程序员的视角中抽象出来。

Istio 的实现在逻辑上分为数据平面和控制平面。数据平面由一组智能代理组成。这些代理中介和控制所有 pod 之间的网络通信。控制平面管理和配置代理以路由流量。

Istio 的主要组件包括:

Envoy

Istio 数据平面基于 Envoy 代理,提供故障处理(例如健康检查和有界重试)、动态服务发现和负载均衡等功能。Envoy 具有许多内置功能,包括:

  • 动态服务发现

  • 负载均衡

  • TLS 终止

  • HTTP/2 和 gRPC 代理

  • 断路器

  • 健康检查

  • 阶段性的推出,通过基于百分比的流量分割

  • 故障注入

  • 丰富的度量指标

Mixer

Mixer 强制执行跨服务网格的访问控制和使用策略,并从 Envoy 代理和其他服务收集遥测数据。代理提取请求级属性,并将其发送到 Mixer 进行评估。

Pilot

Pilot 为 Envoy Sidecar 提供服务发现和流量管理功能,如智能路由(例如 A/B 测试、金丝雀发布)和可靠性(超时、重试、断路器等)。通过将控制流量行为的高级路由规则转换为 Envoy 特定的配置,并在运行时传播给 Sidecar,Pilot 抽象了特定于平台的服务发现机制,并将它们合成为符合 Envoy 数据平面 API 的标准格式。

Galley

Galley 是 Istio 的配置验证、接收、处理和分发组件。它负责保护 Istio 其他组件免受从底层平台获取用户配置的详细信息。

Citadel

Citadel 通过提供身份和凭据管理来实现强大的服务到服务和端用户认证。它允许在服务网格中升级未加密的流量。使用 Citadel,运营商可以基于服务标识而不是相对不稳定的第 3 层或第 4 层网络标识符来执行策略。

Istio 的整体架构如图 3-5 所示。

Istio 架构

图 3-5. Istio 架构

Kubeflow 使用 Istio 提供代理给 Kubeflow UI,并适当且安全地路由请求。Kubeflow 的 KFServing 利用 Knative,需要像 Istio 这样的服务网格。

Knative

Kubeflow 另一个不为人知的支持组件是 Knative。我们将从描述最重要的部分开始:Knative Serving。建立在 Kubernetes 和 Istio 上,Knative Serving 支持部署和提供无服务器应用程序服务。Knative Serving 项目提供的中间件原语使以下功能成为可能:

  • 快速部署无服务器容器

  • 自动缩放到零和向上缩放

  • Istio 组件的路由和网络编程

  • 部署代码和配置的时序快照

Knative Serving 实现为一组 Kubernetes CRD。这些对象用于定义和控制无服务器工作负载的行为:

Service

service.serving.knative.dev 资源整体管理工作负载。它编排其他对象的创建和执行,以确保应用程序在每次服务更新时都有配置、路由和新的修订版本。服务可以定义为始终将流量路由到最新的修订版或指定的修订版。

路由

route.serving.knative.dev 资源将网络端点映射到一个或多个修订版本。这允许多种流量管理方法,包括分数流量和命名路由。

配置

configuration.serving.knative.dev 资源维护部署的期望状态。它在代码和配置之间提供清晰的分离,并遵循 Twelve-Factor App 方法论。修改配置会创建一个新的修订版本。

修订版

revision.serving.knative.dev 资源是每次对工作负载进行的代码和配置修改的时间点快照。修订版是不可变对象,可以保留尽可能长的时间。Knative Serving 修订版可以根据传入的流量自动扩展和缩减。

Knative 的整体架构在图 3-6 中有所体现。

Knative 架构

图 3-6. Knative 架构

Apache Spark

在 Kubeflow 中更显著的支持组件是 Apache Spark。从 Kubeflow 1.0 开始,Kubeflow 提供了用于运行 Spark 作业的内置 Spark 运算符。除了 Spark 运算符外,Kubeflow 还提供了用于使用 Google 的 Dataproc 和 Amazon 的 Elastic Map Reduce(EMR)两种托管云服务运行 Spark 的集成。这些组件和运算符专注于生产使用,不适合用于探索。对于探索,您可以在 Jupyter 笔记本中使用 Spark。

Apache Spark 允许您处理更大的数据集并解决无法放在单个计算机上的问题。虽然 Spark 有自己的机器学习库,但更常见的是作为数据或特征准备的机器学习流水线的一部分使用。我们在第五章中更详细地讨论了 Spark。

Kubeflow 多用户隔离

Kubeflow 的最新版本引入了多用户隔离,允许在不同团队和用户之间共享同一资源池。多用户隔离为用户提供了一种可靠的方法来隔离和保护自己的资源,避免意外查看或更改彼此的资源。这种隔离的关键概念包括:

管理员

管理员是创建和维护 Kubeflow 集群的人员。此人有权授予他人访问权限。

用户

用户是具有对集群中某些资源集的访问权限的人。用户需要管理员授予访问权限。

档案

档案是用户拥有的所有 Kubernetes 命名空间和资源的分组。

从版本 1.0 开始,Kubeflow 的 Jupyter 笔记本服务是第一个完全与多用户隔离集成的应用程序。笔记本及其创建由管理员或配置文件所有者设置的配置文件访问策略控制。笔记本创建的资源(例如训练作业和部署)也将继承相同的访问权限。默认情况下,Kubeflow 在首次登录时为经过身份验证的用户提供自动配置文件创建,这会创建一个新的命名空间。或者,用户的配置文件可以通过手动方式创建。这意味着每个用户都可以在其自己的命名空间中独立工作,并使用其自己的 Jupyter 服务器和笔记本。要与他人共享对您的服务器/笔记本的访问权限,请转到管理贡献者页面并添加您的合作者的电子邮件。

结论

现在您已了解 Kubeflow 的不同组件及其如何相互配合。Kubeflow 的中央仪表板为您提供访问其 Web 组件的权限。您已经看到 JupyterHub 如何促进模型开发的探索阶段。我们涵盖了 Kubeflow 的不同内置训练操作符。我们重新审视了 Kubeflow 流水线,讨论了它如何将所有 Kubeflow 的其他组件联系在一起。我们介绍了 Katib,Kubeflow 的用于管道的超参数调整工具。我们讨论了使用 Kubeflow 提供的不同模型服务选项(包括 KF Serving 和 Seldon)。我们讨论了 Kubeflow 的跟踪机器学习元数据和工件的系统。然后我们总结了一些支持 Kubeflow 其余部分的组件,例如 Knative 和 Istio。通过了解 Kubeflow 的不同部分以及总体设计,您现在应该能够开始看到如何将您的机器学习任务和工作流转化为 Kubeflow。

接下来的几章将帮助您深入了解这些组件以及如何将它们应用到您的使用案例中。

¹ 这可以在多台服务器上运行,同时暴露一个一致的端点。

² 将凭证存储在应用程序内部可能导致安全漏洞。

³ 为了使用户能够登录,他们应被授予最小的权限范围,允许他们连接到 Kubernetes 集群。例如,对于 GCP 用户,他们可以被授予 IAM 角色:Kubernetes Engine Cluster Viewer 和 IAP-secured Web App User。

第四章:Kubeflow Pipelines

在上一章中,我们介绍了Kubeflow Pipelines,这是 Kubeflow 中用于编排机器学习应用程序的组件。编排是必要的,因为典型的机器学习实现使用一系列工具来准备数据,训练模型,评估性能和部署。通过在代码中形式化步骤及其顺序,流水线允许用户正式捕捉所有数据处理步骤,确保其可重现性和可审计性,以及训练和部署步骤。

我们将从查看 Pipelines UI 开始本章,并展示如何开始使用 Python 编写简单的流水线。我们将探讨如何在各个阶段之间传输数据,然后继续探讨如何利用现有应用作为流水线的一部分。我们还将查看底层工作流引擎——Argo Workflows,这是 Kubeflow 用来运行流水线的标准 Kubernetes 流水线工具。理解 Argo Workflows 的基础知识可以帮助您更深入地了解 Kubeflow Pipelines,并有助于调试。接下来,我们将展示 Kubeflow Pipelines 在 Argo 上的增强功能。

我们将展示如何在 Kubeflow Pipelines 中实现条件执行以及如何按计划运行流水线执行,来完成 Kubeflow Pipelines 的总结。流水线的任务特定组件将在各自的章节中进行详细介绍。

开始使用流水线

Kubeflow Pipelines 平台包括:

  • 有一个用于管理和跟踪流水线及其执行的 UI。

  • 用于调度流水线执行的引擎

  • 用 Python 定义、构建和部署流水线的 SDK

  • 笔记本支持使用 SDK 和流水线执行

熟悉流水线的最简单方法是查看预装的示例。

探索预打包的示例流水线

为帮助用户理解流水线,Kubeflow 预装了一些示例流水线。您可以在 Pipeline Web UI 中找到这些预打包流水线,如图 4-1 中所示。请注意,在撰写时,只有从基本到条件执行的流水线是通用的,而其余流水线仅在 Google Kubernetes Engine(GKE)上运行。如果您尝试在非 GKE 环境中运行它们,它们将失败。

Kubeflow Pipelines UI - 预打包的流水线

图 4-1. Kubeflow pipelines UI: 预打包的流水线

单击特定流水线将显示其执行图或源代码,如在图 4-2 中所示。

Kubeflow Pipelines UI - Pipeline Graph View

图 4-2. Kubeflow pipelines UI: 流水线图视图

单击源代码选项卡将显示流水线的编译代码,这是一个 Argo YAML 文件(详细内容请参见“Argo: Pipelines 的基础”)。

您可以在此领域进行实验,运行流水线以更好地了解其执行和流水线 UI 的功能。

要调用特定流水线,只需点击它;这将显示如图 4-3 中所示的流水线视图。

Kubeflow 流水线 UI - 流水线视图

图 4-3. Kubeflow 流水线 UI:流水线视图

要运行流水线,请单击“创建运行”按钮,然后按照屏幕上的说明操作。

提示

运行流水线时,必须选择一个实验。这里的实验只是流水线执行(运行)的便利分组。您始终可以使用 Kubeflow 安装创建的“默认”实验。此外,选择“一次性”作为运行类型以执行一次流水线。我们将在“定期运行流水线”中讨论定期执行。

使用 Python 构建简单流水线

我们已经看到如何执行预编译的 Kubeflow 流水线,现在让我们来研究如何编写我们自己的新流水线。Kubeflow 流水线存储为 YAML 文件,由名为 Argo 的程序执行(参见“Argo: the Foundation of Pipelines”)。幸运的是,Kubeflow 提供了一个 Python 领域特定语言(DSL) 用于编写流水线。DSL 是对在 ML 工作流中执行的操作的 Python 表示,并专门针对 ML 工作负载构建。DSL 还允许使用一些简单的 Python 函数作为流水线阶段,而无需显式构建容器。

提示

本书第四章的示例可以在此书的 GitHub 仓库的笔记本中找到

流水线本质上是容器执行的图形。除了指定容器应按顺序运行外,它还允许用户将参数传递给整个流水线和参与容器之间。

对于每个容器(使用 Python SDK 时),我们必须:

  • 创建容器——可以是简单的 Python 函数,也可以是任何 Docker 容器(在第 9 章中了解更多)。

  • 创建引用该容器以及要传递给容器的命令行参数、数据挂载和变量的操作。

  • 顺序化操作,定义哪些操作可以并行进行,哪些必须在继续进行下一步之前完成。¹

  • 将此在 Python 中定义的流水线编译成 Kubeflow Pipelines 可以消费的 YAML 文件。

流水线是 Kubeflow 的一个关键功能,你将在整本书中多次看到它们。在本章中,我们将展示一些最简单的示例,以说明流水线的基本原理。这不会感觉像“机器学习”,这是有设计意图的。

对于我们的第一个 Kubeflow 操作,我们将使用一种称为轻量级 Python 函数的技术。然而,我们不应该让轻量级这个词愚弄我们。在轻量级 Python 函数中,我们定义一个 Python 函数,然后让 Kubeflow 负责将该函数打包到一个容器中并创建一个操作。

为了简单起见,让我们声明最简单的函数作为回显。这是一个接受单个输入(整数)并返回该输入的函数。

让我们从导入 kfp 并定义我们的函数开始:

import kfp
def simple_echo(i: int) -> int:
    return i
警告

注意我们使用 snake_case,而不是 camelCase 作为我们的函数名称。在撰写本文时,存在一个错误(特性?),即使用驼峰命名(例如:将我们的函数命名为 simpleEcho)会导致错误。

接下来,我们想要将我们的函数 simple_echo 包装到一个 Kubeflow 流水线操作中。有一个很好的方法可以做到这一点:kfp.components.func_to_container_op。此方法返回一个工厂函数,具有强类型签名:

simpleStronglyTypedFunction =
  kfp.components.func_to_container_op(deadSimpleIntEchoFn)

当我们在下一步创建流水线时,工厂函数将构造一个 ContainerOp,该操作将在容器中运行原始函数(echo_fn):

foo = simpleStronglyTypedFunction(1)
type(foo)
Out[5]: kfp.dsl._container_op.ContainerOp
提示

如果您的代码可以通过 GPU 加速,那么很容易将一个阶段标记为使用 GPU 资源;只需将 .set_gpu_limit(NUM_GPUS) 添加到您的 ContainerOp 中。

现在让我们将 ContainerOp(s)(只有一个)按顺序排列到一个流水线中。这个流水线将接受一个参数(我们将要回显的数字)。此流水线还带有一些与其关联的元数据。虽然回显数字可能是参数使用的一个微不足道的例子,但在实际用例中,您将包括稍后可能想要调整的变量,如机器学习算法的超参数。

最后,我们将我们的流水线编译成一个压缩的 YAML 文件,然后我们可以将其上传到 Pipelines UI。

@kfp.dsl.pipeline(
  name='Simple Echo',
  description='This is an echo pipeline. It echoes numbers.'
)
def echo_pipeline(param_1: kfp.dsl.PipelineParam):
  my_step = simpleStronglyTypedFunction(i= param_1)

kfp.compiler.Compiler().compile(echo_pipeline,
  'echo-pipeline.zip')
提示

还可以直接从笔记本运行流水线,我们将在下一个示例中这样做。

只有一个组件的流水线并不是非常有趣。对于我们的下一个示例,我们将自定义轻量级 Python 函数的容器。我们将创建一个新的流水线,安装和导入额外的 Python 库,从指定的基础镜像构建,并在容器之间传递输出。

我们将创建一个流水线,将一个数字除以另一个数字,然后再加上第三个数字。首先让我们创建我们简单的 add 函数,如 示例 4-1 中所示。

示例 4-1. 一个简单的 Python 函数
def add(a: float, b: float) -> float:
   '''Calculates sum of two arguments'''
   return a + b

add_op = comp.func_to_container_op(add)

接下来,让我们创建一个稍微复杂一些的函数。此外,让我们让这个函数需要从一个非标准的 Python 库 numpy 进行导入。这必须在函数内完成。这是因为从笔记本进行的全局导入不会打包到我们创建的容器中。当然,确保我们的容器安装了我们导入的库也是很重要的。

为此,我们将传递我们想要用作基础镜像的特定容器到 .func_to_container(,就像 示例 4-2 中一样。

示例 4-2. 一个稍复杂的 Python 函数
from typing import NamedTuple
def my_divmod(dividend: float, divisor:float) -> \
       NamedTuple('MyDivmodOutput', [('quotient', float), ('remainder', float)]):
    '''Divides two numbers and calculate  the quotient and remainder'''
    #Imports inside a component function:
    import numpy as np ![1](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/1.png)

    #This function demonstrates how to use nested functions inside a
    # component function:
    def divmod_helper(dividend, divisor): ![2](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/2.png)
	return np.divmod(dividend, divisor)

    (quotient, remainder) = divmod_helper(dividend, divisor)

    from collections import namedtuple
    divmod_output = namedtuple('MyDivmodOutput', ['quotient', 'remainder'])
    return divmod_output(quotient, remainder)

divmod_op = comp.func_to_container_op(
                my_divmod, base_image='tensorflow/tensorflow:1.14.0-py3') ![3](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/3.png)

1

在函数内部导入库。

2

轻量级 Python 函数内部的嵌套函数也是可以的。

3

调用特定的基础容器。

现在我们将构建一个流水线。示例 4-3 中的流水线使用之前定义的函数 my_divmodadd 作为阶段。

示例 4-3. 一个简单的流水线
@dsl.pipeline(
   name='Calculation pipeline',
   description='A toy pipeline that performs arithmetic calculations.'
)
def calc_pipeline(
   a='a',
   b='7',
   c='17',
):
    #Passing pipeline parameter and a constant value as operation arguments
    add_task = add_op(a, 4) #Returns a dsl.ContainerOp class instance.

    #Passing a task output reference as operation arguments
    #For an operation with a single return value, the output
    # reference can be accessed using `task.output`
    # or `task.outputs['output_name']` syntax
    divmod_task = divmod_op(add_task.output, b) ![1](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/1.png)

    #For an operation with multiple return values, the output references
    # can be accessed using `task.outputs['output_name']` syntax
    result_task = add_op(divmod_task.outputs['quotient'], c) ![1](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/1.png)

1

传递在容器之间传递的值。操作顺序可从此推断。

最后,我们使用客户端提交流水线进行执行,返回执行和实验的链接。实验将执行组合在一起。如果您更喜欢,也可以使用 kfp.compiler.Compiler().compile 并上传 zip 文件,就像在第一个示例中一样:

client = kfp.Client()
#Specify pipeline argument values
# arguments = {'a': '7', 'b': '8'} #whatever makes sense for new version
#Submit a pipeline run
client.create_run_from_pipeline_func(calc_pipeline, arguments=arguments)

跟随 create_run_from_pipeline_func 返回的链接,我们可以进入执行的 Web UI,显示流水线本身和中间结果,如 图 4-4 中所示。

流水线执行

图 4-4. 流水线执行

正如我们所见,轻量级轻量级 Python 函数 中指的是我们流程中这些步骤的易于完成,而不是函数本身的功能强大。我们可以使用自定义导入、基础镜像,并学习如何在容器之间传递小结果。

在接下来的部分,我们将展示如何通过为容器挂载卷来传递更大的数据文件。

在步骤之间存储数据

在前面的示例中,容器之间传递的数据很小,是原始类型(如数值、字符串、列表和数组)。然而,在实际中,我们很可能传递更大的数据(例如,整个数据集)。在 Kubeflow 中,有两种主要的方法可以做到这一点——Kubernetes 集群内的持久卷和云存储选项(如 S3),尽管每种方法都有其固有的问题。

持久卷抽象了存储层。根据供应商不同,持久卷的配置可能较慢,并且具有 IO 限制。请检查供应商是否支持读写多种存储类,以允许多个 Pod 访问存储,这对于某些类型的并行性是必需的。存储类可以是以下之一。²

ReadWriteOnce

卷可以被单个节点以读写方式挂载。

ReadOnlyMany

卷可以被多个节点以只读方式挂载。

ReadWriteMany

卷可以被多个节点以读写方式挂载。

您的系统/集群管理员可能能够添加读写多支持。³ 另外,许多云服务提供商包括他们专有的读写多实现,例如在 GKE 上查看 动态配置,但请确保询问是否存在单节点瓶颈。

Kubeflow Pipelines 的 VolumeOp 允许您创建自动管理的持久卷,如 Example 4-4 所示。要将卷添加到操作中,只需调用 add_pvolumes,并传递一个挂载点到卷的字典,例如 download_data_op(year).add_pvolumes({"/data_processing": dvop.volume})

Example 4-4. 邮件列表数据准备
dvop = dsl.VolumeOp(name="create_pvc",
                    resource_name="my-pvc-2",
                    size="5Gi",
                    modes=dsl.VOLUME_MODE_RWO)

尽管在 Kubeflow 示例中不太常见,但在某些情况下,使用对象存储解决方案可能更合适。MinIO 通过作为现有对象存储引擎的网关或独立运行,提供云原生对象存储。⁴ 我们在 第三章 中介绍了如何配置 MinIO。

Kubeflow 的内置 file_output 机制可以在流水线步骤之间自动传输指定的本地文件到 MinIO。要使用 file_output,请在容器中将文件写入本地,并在 ContainerOp 中指定参数,如 Example 4-5 所示。

Example 4-5. 文件输出示例
    fetch = kfp.dsl.ContainerOp(name='download',
                                image='busybox',
                                command=['sh', '-c'],
                                arguments=[
                                    'sleep 1;'
                                    'mkdir -p /tmp/data;'
                                    'wget ' + data_url +
                                    ' -O /tmp/data/results.csv'
                                ],
                                file_outputs={'downloaded': '/tmp/data'})
    # This expects a directory of inputs not just a single file

如果您不想使用 MinIO,您也可以直接使用您提供商的对象存储,但这可能会影响一些可移植性。

挂载数据到本地是任何机器学习流水线中的基本任务。我们在此简要概述了多种方法,并提供了每种方法的示例。

Kubeflow Pipelines 组件介绍

Kubeflow Pipelines 基于 Argo Workflows,这是一个针对 Kubernetes 的开源、容器本地的工作流引擎。在本节中,我们将描述 Argo 的工作原理、其功能,以及 Kubeflow Pipeline 如何补充 Argo 以便数据科学家更容易使用。

Argo:流水线的基础

Kubeflow 安装了所有 Argo 组件。虽然在您的计算机上安装 Argo 不是使用 Kubeflow Pipelines 的必要条件,但使用 Argo 命令行工具可以更轻松地理解和调试您的流水线。

Tip

默认情况下,Kubeflow 配置 Argo 使用 Docker 执行器。如果您的平台不支持 Docker API,则需要将执行器切换为兼容的执行器。这可以通过在 Argo params 文件中更改 containerRuntimeExecutor 值来完成。有关权衡的详细信息,请参见 附录 A。本书中的大多数示例使用 Docker 执行器,但可以调整为其他执行器。

在 macOS 上,您可以使用 Homebrew 安装 Argo,如 Example 4-6 所示。⁵

Example 4-6. 安装 Argo
#!/bin/bash
# Download the binary
curl -sLO https://github.com/argoproj/argo/releases/download/v2.8.1/argo-linux-amd64

# Make binary executable
chmod +x argo-linux-amd64

# Move binary to path
mv ./argo-linux-amd64 ~/bin/argo

You can verify your Argo installation by running the Argo examples with the command-line tool in the Kubeflow namespace: follow these Argo instructions. When you run the Argo examples the pipelines are visible with the argo command, as in Example 4-7.

Example 4-7. Listing Argo executions
$ argo list -n kubeflow
NAME                STATUS      AGE   DURATION
loops-maps-4mxp5    Succeeded   30m   12s
hello-world-wsxbr   Succeeded   39m   15s

Since pipelines are implemented with Argo, you can use the same technique to check on them as well. You can also get information about specific workflow execution, as shown in Example 4-8.

Example 4-8. Getting Argo execution details
$ argo get hello-world-wsxbr -n kubeflow  ![1](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/1.png)
Name:                hello-world-wsxbr
Namespace:           kubeflow
ServiceAccount:      default
Status:              Succeeded
Created:             Tue Feb 12 10:05:04 -0600 (2 minutes ago)
Started:             Tue Feb 12 10:05:04 -0600 (2 minutes ago)
Finished:            Tue Feb 12 10:05:23 -0600 (1 minute ago)
Duration:            19 seconds

STEP                  PODNAME            DURATION  MESSAGE
 ✔ hello-world-wsxbr  hello-world-wsxbr  18s

1

hello-world-wsxbr is the name that we got using argo list -n kubeflow above. In your case the name will be different.

We can also view the execution logs by using the command in Example 4-9.

Example 4-9. Getting the log of Argo execution
$ argo logs hello-world-wsxbr -n kubeflow

This produces the result shown in Example 4-10.

Example 4-10. Argo execution log
< hello world >
 -------------
    \
     \
      \
		    ##        .
	      ## ## ##       ==
	   ## ## ## ##      ===
       /""""""""""""""""___/ ===
  ~~~ {~~ ~~~~ ~~~ ~~~~ ~~ ~ /  ===- ~~~
       \______ o          __/
	\    \        __/
	  \____\______/

You can also delete a specific workflow; see Example 4-11.

Example 4-11. Deleting Argo execution
$ argo delete hello-world-wsxbr -n kubeflow

Alternatively, you can get pipeline execution information using the Argo UI, as seen in Figure 4-5.

Argo UI for pipelines execution

Figure 4-5. Argo UI for pipeline execution

You can also look at the details of the flow execution graph by clicking a specific workflow, as seen in Figure 4-6.

Argo UI - Execution Graph

Figure 4-6. Argo UI execution graph

For any Kubeflow pipeline you run, you can also view that pipeline in the Argo CLI/UI. Note that because ML pipelines are using the Argo CRD, you can also see the result of the pipeline execution in the Argo UI (as in Figure 4-7).

Viewing Kubeflow Pipelines in Argo UI

Figure 4-7. Viewing Kubeflow pipelines in Argo UI
Tip

Currently, the Kubeflow community is actively looking at alternative foundational technologies for running Kubeflow pipelines, one of which is Tekton. The paper by A. Singh et al., “Kubeflow Pipelines with Tekton”, gives “initial design, specifications, and code for enabling Kubeflow Pipelines to run on top of Tekton.” The basic idea here is to create an intermediate format that can be produced by pipelines and then executed using Argo, Tekton, or other runtimes. The initial code for this implementation is found in this Kubeflow GitHub repo.

What Kubeflow Pipelines Adds to Argo Workflow

Argo 是工作流执行的基础;然而,直接使用它需要您做一些笨拙的事情。首先,您必须在 YAML 中定义工作流,这可能很困难。其次,您必须将您的代码容器化,这可能很繁琐。KF Pipelines 的主要优势在于,您可以使用 Python API 定义/创建管道,这自动化了工作流定义的大部分 YAML 样板,并且非常适合数据科学家/Python 开发人员。Kubeflow Pipelines 还添加了用于机器学习特定组件的构建块的钩子。这些 API 不仅生成 YAML,还可以简化容器创建和资源使用。除了 API 外,Kubeflow 还添加了一个定期调度程序和用于配置和执行的 UI。

使用现有镜像构建管道

直接从 Python 构建管道阶段提供了一个简单的入口点。尽管如此,这限制了我们的实现仅限于 Python。Kubeflow Pipelines 的另一个特性是能够编排执行多语言实现,利用预构建的 Docker 镜像(见 第 9 章)。

除了之前的导入外,我们还希望导入 Kubernetes 客户端,这使我们可以直接从 Python 代码中使用 Kubernetes 函数(见 示例 4-12)。

示例 4-12. 导出 Kubernetes 客户端
from kubernetes import client as k8s_client

再次,我们创建一个客户端和实验来运行我们的管道。正如前面提到的,实验将管道运行分组。您只能创建给定实验一次,因此 示例 4-13 展示了如何创建新实验或使用现有实验。

示例 4-13. 获取管道实验
client = kfp.Client()
exp = client.get_experiment(experiment_name ='mdupdate')

现在我们创建我们的管道(见 示例 4-14)。所使用的镜像需要是可访问的,并且我们正在指定完整名称,以便解析。由于这些容器是预构建的,我们需要为我们的管道配置它们。

我们正在使用的预构建容器通过 MINIO_* 环境变量配置其存储。因此,我们通过调用 add_env_variable 来配置它们以使用我们的本地 MinIO 安装。

除了在各阶段之间传递参数时自动生成的依赖关系外,您还可以使用 after 指定某个阶段需要前一个阶段。当存在外部副作用(例如更新数据库)时,这是非常有用的。

示例 4-14. 示例推荐管道
@dsl.pipeline(
  name='Recommender model update',
  description='Demonstrate usage of pipelines for multi-step model update'
)
def recommender_pipeline():
    # Load new data
  data = dsl.ContainerOp(
      name='updatedata',
      image='lightbend/recommender-data-update-publisher:0.2') \
    .add_env_variable(k8s_client.V1EnvVar(name='MINIO_URL',
        value='http://minio-service.kubeflow.svc.cluster.local:9000')) \
    .add_env_variable(k8s_client.V1EnvVar(name='MINIO_KEY', value='minio')) \
    .add_env_variable(k8s_client.V1EnvVar(name='MINIO_SECRET', value='minio123'))
    # Train the model
  train = dsl.ContainerOp(
      name='trainmodel',
      image='lightbend/ml-tf-recommender:0.2') \
    .add_env_variable(k8s_client.V1EnvVar(name='MINIO_URL',
            value='minio-service.kubeflow.svc.cluster.local:9000')) \
    .add_env_variable(k8s_client.V1EnvVar(name='MINIO_KEY', value='minio')) \
    .add_env_variable(k8s_client.V1EnvVar(name='MINIO_SECRET', value='minio123'))
  train.after(data)
    # Publish new model
  publish = dsl.ContainerOp(
      name='publishmodel',
      image='lightbend/recommender-model-publisher:0.2') \
    .add_env_variable(k8s_client.V1EnvVar(name='MINIO_URL',
            value='http://minio-service.kubeflow.svc.cluster.local:9000')) \
    .add_env_variable(k8s_client.V1EnvVar(name='MINIO_KEY', value='minio')) \
    .add_env_variable(k8s_client.V1EnvVar(name='MINIO_SECRET', value='minio123')) \
    .add_env_variable(k8s_client.V1EnvVar(name='KAFKA_BROKERS',
            value='cloudflow-kafka-brokers.cloudflow.svc.cluster.local:9092')) \
    .add_env_variable(k8s_client.V1EnvVar(name='DEFAULT_RECOMMENDER_URL',
            value='http://recommendermodelserver.kubeflow.svc.cluster.local:8501')) \
    .add_env_variable(k8s_client.V1EnvVar(name='ALTERNATIVE_RECOMMENDER_URL',
            value='http://recommendermodelserver1.kubeflow.svc.cluster.local:8501'))
  publish.after(train)

由于管道定义本质上是代码,您可以通过使用循环设置 MinIO 参数来使其更加紧凑,而不是在每个阶段都这样做。

与之前一样,我们需要编译管道,可以使用 compiler.Compiler().compile 明确编译,也可以使用 create_run_from_pipeline_func 隐式编译。现在,继续运行管道(如 图 4-8)。

推荐管道执行示例

图 4-8. 推荐管道执行示例

Kubeflow 管道组件

除了我们刚讨论过的容器操作外,Kubeflow Pipelines 还公开了使用组件的其他操作。组件公开了不同的 Kubernetes 资源或外部操作(如 dataproc)。Kubeflow 组件允许开发人员打包机器学习工具,同时抽象掉容器或 CRD 的具体细节。

我们已经相对直接地使用了 Kubeflow 的构建模块,并且使用了 func_to_container 组件。⁶ 一些组件,如 func_to_container,以普通的 Python 代码形式提供,并且可以像普通的库一样导入。其他组件使用 Kubeflow 的 component.yaml 系统来指定,并需要加载。在我们看来,使用 Kubeflow 组件的最佳方式是下载仓库的特定标签,允许我们使用 load_component_from_file,如示例 4-15 所示。

示例 4-15. 管道发布
wget https://github.com/kubeflow/pipelines/archive/0.2.5.tar.gz
tar -xvf 0.2.5.tar.gz
警告

有一个 load_component 函数,它接受组件的名称并尝试解析它。我们不建议使用此函数,因为它默认搜索路径包括从 Github 获取 pipelines 库的主分支,这是不稳定的。

在下一章中,我们深入探讨数据准备组件;然而,让我们快速看一个文件获取组件的例子。在本章的推荐器示例中,我们使用了一个特殊的预构建容器来获取我们的数据,因为它不在持久卷中。相反,我们可以使用 Kubeflow GCS 组件 google-cloud/storage/download/ 来下载我们的数据。假设您已经像 示例 4-15 中所示下载了管道发布,您可以使用 load_component_from_file 如同 示例 4-16 中所示加载组件。

示例 4-16. 加载 GCS 下载组件
gcs_download_component = kfp.components.load_component_from_file(
    "pipelines-0.2.5/components/google-cloud/storage/download/component.yaml")

当加载组件时,它会返回一个函数,调用该函数将生成一个管道阶段。大多数组件接受参数以配置它们的行为。通过在加载的组件上调用 help 或查看 component.yaml,您可以获取组件选项的列表。GCS 下载组件要求我们使用 gcs_path 配置下载内容,如 示例 4-17 所示。

示例 4-17. 从相对路径和网络链接加载管道存储组件
    dl_op = gcs_download_component(
        gcs_path=
        "gs://ml-pipeline-playground/tensorflow-tfx-repo/tfx/components/testdata/external/csv"
    )  # Your path goes here

在 第五章 中,我们深入探讨了更常见的 Kubeflow 管道数据和特征准备组件。

管道的高级主题

到目前为止,我们展示的所有示例都是纯顺序执行的。还有一些情况下,我们需要能够检查条件并相应地更改管道的行为。

管道阶段的条件执行

Kubeflow Pipelines 允许通过 dsl.Condition 进行条件执行。让我们看一个非常简单的示例,在这个示例中,根据变量的值执行不同的计算。

一个简单的笔记本实现了这个例子。它从示例 4-18 中必需的导入开始。

示例 4-18. 导入所需组件
import kfp
from kfp import dsl
from kfp.components import func_to_container_op, InputPath, OutputPath

一旦导入完成,我们可以实现几个简单的函数,如示例 4-19 所示。

示例 4-19. 函数实现
@func_to_container_op
def get_random_int_op(minimum: int, maximum: int) -> int:
    """Generate a random number between minimum and maximum (inclusive)."""
    import random
    result = random.randint(minimum, maximum)
    print(result)
    return result

@func_to_container_op
def process_small_op(data: int):
    """Process small numbers."""
    print("Processing small result", data)
    return

@func_to_container_op
def process_medium_op(data: int):
    """Process medium numbers."""
    print("Processing medium result", data)
    return

@func_to_container_op
def process_large_op(data: int):
    """Process large numbers."""
    print("Processing large result", data)
    return

我们直接使用 Python 实现所有函数(与前面的示例相同)。第一个函数生成一个介于 0 和 100 之间的整数,接下来的三个函数构成了实际处理的简单框架。管道的实现如示例 4-20 中所示。

示例 4-20. 管道实现
@dsl.pipeline(
    name='Conditional execution pipeline',
    description='Shows how to use dsl.Condition().'
)
def conditional_pipeline():
    number = get_random_int_op(0, 100).output ![1](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/1.png)
    with dsl.Condition(number < 10): ![2](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/2.png)
	process_small_op(number)
    with dsl.Condition(number > 10 and number < 50): ![2](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/2.png)
	process_medium_op(number)
    with dsl.Condition(number > 50): ![2](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/2.png)
	process_large_op(number)

kfp.Client().create_run_from_pipeline_func(conditional_pipeline, arguments={}) ![3](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/3.png)

1

根据这里我们得到的数字…

2

我们将继续进行其中一个操作。

3

注意这里我们正在指定空参数——必需参数。

最后,执行图表,如图 4-9 所示。

执行条件管道示例

图 4-9. 执行条件管道示例

从这个图表中,我们可以看到管道确实分成了三个分支,并且在此运行中选择了处理大操作。为了验证这一点,我们查看执行日志,如图 4-10 所示。

查看条件管道日志

图 4-10. 查看条件管道日志

在这里我们可以看到生成的数字是 67。这个数字大于 50,这意味着应执行process_large_op分支。⁷

在计划上运行管道

我们手动运行了我们的管道。这对于测试很好,但通常不足以满足生产环境的需求。幸运的是,您可以按计划运行管道,如thisKubeflow documentation page所述。首先,您需要上传管道定义并指定描述。完成后,您可以通过创建一个运行并选择“重复”运行类型来创建定期运行,并按照屏幕上的说明操作,如图 4-11 所示。

在此图中,我们正在设置每天运行一次的管道。

警告

创建定期运行时,我们正在指定管道运行的频率,而不是运行时间。在当前实现中,执行时间是在创建运行时定义的。一旦创建,它会立即执行,然后按照定义的频率执行。例如,如果每天上午 10 点创建一个每天运行,它将每天上午 10 点执行。

设置管道的定期执行是一个重要的功能,允许您完全自动化管道的执行。

设置定期运行管道

图 4-11. 设置管道的周期性执行

结论

您现在应该掌握了如何构建、调度和运行一些简单管道的基础知识。您还学习了管道工具在调试时的使用。我们展示了如何将现有软件集成到管道中,如何在管道内实现条件执行,以及如何按计划运行管道。

在我们的下一章中,我们将看看如何使用管道进行数据准备,并提供一些示例。

¹ 当将一个管道阶段的结果作为其他管道的输入时,通常可以自动推断出这一点。您还可以手动指定额外的依赖关系。

² Kubernetes 持久卷可以提供不同的 访问模式

³ 通用的读写多实现是 NFS 服务器

⁴ 如果需要确保解决方案在多个云提供商之间具有可移植性,则可以使用云原生访问存储。

⁵ 如果需要在另一个操作系统上安装 Argo Workflow,请参考 这些 Argo 指令

⁶ 许多标准组件都在 这个 Kubeflow GitHub 仓库 中。

⁷ 在 这个 GitHub 网站 上可以找到更复杂的条件处理示例(包括嵌套条件)。

第五章:数据和特征准备

机器学习算法的好坏取决于它们的训练数据。获取用于训练的良好数据涉及数据和特征准备。

数据准备 是获取数据并确保其有效性的过程。这是一个多步骤的过程¹,可以包括数据收集、增强、统计计算、模式验证、异常值修剪以及各种验证技术。数据量不足可能导致过拟合,错过重要的相关性等问题。在数据准备阶段投入更多精力收集更多的记录和每个样本的信息可以显著提高模型的效果。²

特征准备(有时称为特征工程)是将原始输入数据转换为机器学习模型可以使用的特征的过程。³ 糟糕的特征准备可能会导致丢失重要的关系,例如线性模型未展开非线性项,或者深度学习模型中图像方向不一致。

数据和特征准备的微小变化可能导致显著不同的模型输出。迭代方法对于特征和数据准备都是最佳选择,随着对问题和模型理解的深入,需要重新访问它们。Kubeflow Pipelines 使我们能够更容易地迭代我们的数据和特征准备。我们将探讨如何使用超参数调整在第十章中迭代。

在本章中,我们将涵盖数据和特征准备的不同方法,并演示如何通过使用流水线使它们可重复。我们假设您已经熟悉本地工具。因此,我们将从如何为流水线构建本地代码开始,并转向更可扩展的分布式工具。一旦我们探索了这些工具,我们将根据“介绍我们的案例研究”中的示例将它们组合成一个流水线。

决定正确的工具

有各种各样的数据和特征准备工具。⁴ 我们可以将它们分类为分布式和本地工具。本地工具在单台机器上运行,并提供很大的灵活性。分布式工具在多台机器上运行,因此可以处理更大更复杂的任务。在工具选择上做出错误决策可能需要后续大幅修改代码。

如果输入数据规模相对较小,单台机器可以提供你所需要的所有工具。更大的数据规模往往需要整个流水线或仅作为抽样阶段的分布式工具。即使是对于较小的数据集,像 Apache Spark、Dask 或 TFX with Beam 这样的分布式系统也可能更快,但可能需要学习新的工具。⁵

不必为所有数据和特征准备活动使用相同的工具。在处理使用相同工具会很不方便的不同数据集时,使用多个工具尤为常见。Kubeflow Pipelines 允许您将实现拆分为多个步骤并连接它们(即使它们使用不同的语言),形成一个连贯的系统。

本地数据和特征准备

在本地工作限制了数据的规模,但提供了最全面的工具范围。实施数据和特征准备的常见方法是使用 Jupyter 笔记本。在第四章中,我们介绍了如何将笔记本的部分转换为管道,这里我们将看看如何结构化我们的数据和特征准备代码,使其变得简单易行。

使用笔记本进行数据准备可以是开始探索数据的好方法。笔记本在这个阶段特别有用,因为我们通常对数据了解最少,而使用可视化工具来理解我们的数据可能非常有益。

获取数据

对于我们的邮件列表示例,我们使用来自互联网公共档案的数据。理想情况下,您希望连接到数据库、流或其他数据存储库。然而,即使在生产中,获取网络数据也可能是必要的。首先,我们将实现我们的数据获取算法,该算法获取 Apache 软件基金会(ASF)项目的电子邮件列表位置以及要获取消息的年份。示例 5-1 返回所获取记录的路径,因此我们可以将其用作下一个管道阶段的输入。

注意

函数下载至多一年的数据,并在调用之间休眠。这是为了防止过度使用 ASF 邮件存档服务器。ASF 是一家慈善组织,请在下载数据时注意这一点,不要滥用此服务。

示例 5-1. 下载邮件列表数据
def download_data(year: int) -> str:

  # The imports are inline here so Kubeflow can serialize the function correctly.
  from datetime import datetime
  from lxml import etree
  from requests import get
  from time import sleep

 import json

  def scrapeMailArchives(mailingList: str, year: int, month: int):
      #Ugly xpath code goes here. See the example repo if you're curious.

   datesToScrape =  [(year, i) for i in range(1,2)]

   records = []
   for y,m in datesToScrape:
     print(m,"-",y)
     records += scrapeMailArchives("spark-dev", y, m)
   output_path = '/data_processing/data.json'
   with open(output_path, 'w') as f:
     json.dump(records, f)

   return output_path

此代码下载给定年份的所有邮件列表数据,并将其保存到已知路径。在本例中,需要挂载持久卷,以便在制作管道时使数据在各个阶段之间流动。

作为机器学习管道的一部分,您可能会有数据转储,或者可能会由不同的系统或团队提供。对于 GCS 或 PV 上的数据,您可以使用内置组件 google-cloud/storage/downloadfilesystem/get_subdirectory 来加载数据,而不是编写自定义函数。

数据清理:过滤掉垃圾

现在我们加载了数据,是时候进行一些简单的数据清理了。本地工具更为常见,因此我们将首先专注于它们。尽管数据清理通常取决于领域专业知识,但也有标准工具可用于协助常见任务。首先的步骤可以是通过检查模式验证输入记录。也就是说,我们检查字段是否存在并且类型正确。

要检查邮件列表示例中的模式,我们确保发送者、主题和正文都存在。为了将其转换为独立组件,我们将使我们的函数接受输入路径的参数,并返回已清理记录的文件路径。实现这个功能所需的代码量相对较小,如示例 5-2 所示。

示例 5-2. 数据清理
def clean_data(input_path: str) -> str:
    import json
    import pandas as pd

    print("loading records...")
    with open(input_path, 'r') as f:
        records = json.load(f)
    print("records loaded")

    df = pd.DataFrame(records)
    # Drop records without a subject, body, or sender
    cleaned = df.dropna(subset=["subject", "body", "from"])

    output_path_hdf = '/data_processing/clean_data.hdf'
    cleaned.to_hdf(output_path_hdf, key="clean")

    return output_path_hdf

除了丢弃缺失字段之外,还有许多其他标准数据质量技术。其中两种较受欢迎的是补全缺失数据⁶和分析并移除可能是不正确测量结果的离群值。无论您决定执行哪些附加的通用技术,您都可以简单地将它们添加到您的数据清理函数中。

领域特定的数据清理工具也可能是有益的。在邮件列表示例中,我们数据中的一个潜在噪声来源可能是垃圾邮件。解决此问题的一种方法是使用 SpamAssassin。我们可以像示例 5-3 中所示那样将此包添加到我们的容器中。在笔记本镜像之上添加不由 pip 管理的系统软件有点更加复杂,因为权限问题。大多数容器以 root 用户身份运行,可以简单地安装新的系统软件包。然而,由于 Jupyter,笔记本容器以较低权限用户身份运行。像这样安装新包需要切换到 root 用户然后再切换回去,这在其他 Dockerfile 中并不常见。

示例 5-3. 安装 SpamAssassin
ARG base
FROM $base
# Run as root for updates
USER root
# Install SpamAssassin
RUN apt-get update && \
    apt-get install -yq spamassassin spamc && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* && \
    rm -rf /var/cache/apt
# Switch back to the expected user
USER jovyan

创建完这个 Dockerfile 后,你会希望构建并将生成的镜像推送到 Kubeflow 集群可以访问的某个地方,例如示例 2-8。

推送一个新的容器还不足以让 Kubeflow 知道我们想要使用它。当使用 func_to_container_op 构建管道阶段时,您需要在 func_to_container_op 函数调用中指定 base_image 参数。我们将在示例 5-35 中将此示例作为管道展示。

这里我们再次看到容器的强大。您可以在 Kubeflow 提供的基础上添加我们需要的工具,而不是从头开始制作一切。

数据清理完成后,就该确保数据充足,或者如果不充足,探索增加数据。

格式化数据

正确的格式取决于您用于特征准备的工具。如果您继续使用用于数据准备的同一工具,则输出可以与输入相同。否则,您可能会发现这是更改格式的好地方。例如,当使用 Spark 进行数据准备并使用 TensorFlow 进行训练时,我们经常在这里实现转换为 TFRecords。

特征准备

如何进行特征准备取决于问题的性质。在邮件列表示例中,我们可以编写各种文本处理函数,并将它们组合成特征,如示例 5-4 所示。

示例 5-4. 将文本处理函数编写并组合为特征
    df['domains'] = df['links'].apply(extract_domains)
    df['isThreadStart'] = df['depth'] == '0'

    # Arguably, you could split building the dataset away from the actual witchcraft.
    from sklearn.feature_extraction.text import TfidfVectorizer

    bodyV = TfidfVectorizer()
    bodyFeatures = bodyV.fit_transform(df['body'])

    domainV = TfidfVectorizer()

    def makeDomainsAList(d):
        return ' '.join([a for a in d if not a is None])

    domainFeatures = domainV.fit_transform(
        df['domains'].apply(makeDomainsAList))

    from scipy.sparse import csr_matrix, hstack

    data = hstack([
        csr_matrix(df[[
            'containsPythonStackTrace', 'containsJavaStackTrace',
            'containsExceptionInTaskBody', 'isThreadStart'
        ]].to_numpy()), bodyFeatures, domainFeatures
    ])

到目前为止,示例代码结构使您可以将每个函数转换为单独的管道阶段;然而,还有其他选项。我们将查看如何将整个笔记本作为管道阶段使用,在“将其放入管道中”中。

当然,除了笔记本和 Python 之外还有其他数据准备工具。笔记本并不总是最好的工具,因为它们在版本控制方面可能存在困难。Python 并不总是具有您所需的库(或性能)。因此,我们现在将看看如何使用其他可用工具。

定制容器

管道不仅仅限于笔记本,甚至不限于特定语言。⁷ 根据项目的不同,您可能有一个常规的 Python 项目、定制工具、Python 2,甚至是 FORTRAN 代码作为一个重要组成部分。

例如,在第九章中,我们将使用 Scala 来执行管道中的一个步骤。此外,在“使用 RStats”中,我们讨论如何开始使用一个 RStats 容器。

有时候您可能无法找到一个与我们这里所需如此相符的容器。在这些情况下,您可以采用一个通用的基础镜像并在其上构建,我们将在第九章中更详细地讨论这一点。

除了需要定制容器之外,您可能选择放弃笔记本的另一个原因是探索分布式工具。

分布式工具

使用分布式平台可以处理大型数据集(超出单个机器内存)并可以提供显著更好的性能。通常,当我们的问题已经超出初始笔记本解决方案时,我们需要开始使用分布式工具。

Kubeflow 中的两个主要数据并行分布式系统是 Apache Spark 和 Google 的 Dataflow(通过 Apache Beam)。Apache Spark 拥有更大的安装基础和支持的格式和库的种类。Apache Beam 支持 TensorFlow Extended(TFX),这是一个端到端的 ML 工具,可以与 TFServing 集成以进行模型推断。由于其集成性最强,我们将首先探索在 Apache Beam 上使用 TFX,然后继续使用更标准的 Apache Spark。

TensorFlow Extended

TensorFlow 社区为从数据验证到模型服务的一切创建了一套优秀的集成工具。目前,TFX 的数据工具都是基于 Apache Beam 构建的,这是 Google Cloud 上分布式处理支持最多的工具。如果您想使用 Kubeflow 的 TFX 组件,目前仅限于单节点;这在未来的版本中可能会改变。

注意

Apache Beam 在 Google Cloud Dataflow 之外的 Python 支持尚不成熟。TFX 是一个 Python 工具,因此其扩展取决于 Apache Beam 的 Python 支持。您可以通过使用仅 GCP 的 Dataflow 组件来扩展作业。随着 Apache Beam 对 Apache Flink 和 Spark 的支持改进,可能会添加对可移植方式扩展 TFX 组件的支持。⁸

Kubeflow 在其管道系统中包含许多 TFX 组件。TFX 还有其自己的管道概念。这些与 Kubeflow 管道是分开的,在某些情况下,TFX 可以作为 Kubeflow 的替代方案。在这里,我们将重点放在数据和特征准备组件上,因为这些是与 Kubeflow 生态系统的其他部分最简单配合使用的组件。

保持数据质量:TensorFlow 数据验证

确保数据质量不会随时间而下降至关重要。数据验证允许我们确保数据的架构和分布仅以预期的方式发展,并在它们变成生产问题之前捕捉数据质量问题。TensorFlow 数据验证(TFDV)使我们能够验证我们的数据。

为了使开发过程更加简单,您应该在本地安装 TFX 和 TFDV。虽然代码可以在 Kubeflow 内部评估,但在本地拥有库会加快开发工作的速度。安装 TFX 和 TFDV 只需使用 pip install 命令,如 Example 5-5 所示。

Example 5-5. 安装 TFX 和 TFDV
pip3 install tfx tensorflow-data-validation

现在让我们看看如何在 Kubeflow 的管道中使用 TFX 和 TFDV。第一步是加载我们想要使用的相关组件。正如我们在前一章中讨论的,虽然 Kubeflow 确实有一个load_component函数,但它自动解析在主分支上,因此不适合生产用例。因此,我们将使用load_component_from_file以及从 Example 4-15 下载的 Kubeflow 组件的本地副本来加载我们的 TFDV 组件。我们需要加载的基本组件包括:示例生成器(即数据加载器)、模式、统计生成器和验证器本身。加载组件的示例如 Example 5-6 所示。

Example 5-6. 加载组件
tfx_csv_gen = kfp.components.load_component_from_file(
    "pipelines-0.2.5/components/tfx/ExampleGen/CsvExampleGen/component.yaml")
tfx_statistic_gen = kfp.components.load_component_from_file(
    "pipelines-0.2.5/components/tfx/StatisticsGen/component.yaml")
tfx_schema_gen = kfp.components.load_component_from_file(
    "pipelines-0.2.5/components/tfx/SchemaGen/component.yaml")
tfx_example_validator = kfp.components.load_component_from_file(
    "pipelines-0.2.5/components/tfx/ExampleValidator/component.yaml")

除了组件之外,我们还需要我们的数据。当前的 TFX 组件通过 Kubeflow 的文件输出机制在管道阶段之间传递数据。这将输出放入 MinIO,自动跟踪与管道相关的工件。为了在推荐示例的输入上使用 TFDV,我们首先使用标准容器操作下载它,就像在 Example 5-7 中所示。

Example 5-7. 下载推荐数据
    fetch = kfp.dsl.ContainerOp(name='download',
                                image='busybox',
                                command=['sh', '-c'],
                                arguments=[
                                    'sleep 1;'
                                    'mkdir -p /tmp/data;'
                                    'wget ' + data_url +
                                    ' -O /tmp/data/results.csv'
                                ],
                                file_outputs={'downloaded': '/tmp/data'})
    # This expects a directory of inputs not just a single file
Tip

如果我们的数据在持久卷上(比如说,在之前的阶段获取的数据),我们可以使用filesystem/get_file组件。

一旦数据加载完成,TFX 有一组称为示例生成器的工具来摄取数据。这些支持几种不同的格式,包括 CSV 和 TFRecord。还有其他系统的示例生成器,包括 Google 的 BigQuery。与 Spark 或 Pandas 支持的各种格式不同,可能需要使用另一工具预处理记录。¹⁰ 在我们的推荐示例中,我们使用了 CSV 组件,如示例 5-8 所示。

示例 5-8. 使用 CSV 组件
    records_example = tfx_csv_gen(input_base=fetch.output)

现在我们有了示例的通道,可以将其作为 TFDV 的输入之一。创建模式的推荐方法是使用 TFDV 推断模式。为了能够推断模式,TFDV 首先需要计算数据的一些摘要统计信息。示例 5-9 展示了这两个步骤的管道阶段。

示例 5-9. 创建模式
    stats = tfx_statistic_gen(input_data=records_example.output)
    schema_op = tfx_schema_gen(stats.output)

如果每次都推断模式,我们可能无法捕捉模式变化。相反,你应该保存模式,并在将来的运行中重复使用它进行验证。流水线的运行网页有指向 MinIO 中模式的链接,你可以使用另一个组件或容器操作,获取或复制它到其他地方。

不管你将模式持久化到何处,你都应该检查它。要检查模式,你需要导入 TFDV 库,如示例 5-10 所示。在开始使用模式验证数据之前,你应该先检查模式。要检查模式,请在本地下载模式(或者笔记本上)并使用 TFDV 的display_schema函数,如示例 5-11 所示。

示例 5-10. 在本地下载模式
import tensorflow_data_validation as tfdv
示例 5-11. 显示模式
schema = tfdv.load_schema_text("schema_info_2")
tfdv.display_schema(schema)

如果需要,可以从TensorFlow GitHub repo下载schema_util.py脚本,提供修改模式的工具(不论是为了演化或者纠正推断错误)。

现在我们知道我们正在使用正确的模式,让我们验证我们的数据。验证组件接受我们生成的模式和统计数据,如示例 5-12 所示。在生产时,你应该用它们的输出替换模式和统计生成组件。

示例 5-12. 验证数据
    tfx_example_validator(stats=stats.outputs['output'],
                          schema=schema_op.outputs['output'])
提示

在推送到生产之前,检查被拒绝记录的大小。你可能会发现数据格式已经改变,需要使用模式演化指南,并可能更新其余的流水线。

TensorFlow Transform,与 TensorFlow Extended 一起使用 Beam

用于进行特征准备的 TFX 程序称为 TensorFlow Transform(TFT),并集成到 TensorFlow 和 Kubeflow 生态系统中。与 TFDV 一样,Kubeflow 的 TensorFlow Transform 组件目前无法扩展到单节点处理之外。TFT 的最大好处是其集成到 TensorFlow 模型分析工具中,简化了推断过程中的特征准备。

我们需要指定 TFT 要应用的转换。我们的 TFT 程序应该在一个与管道定义分离的文件中,虽然也可以作为字符串内联。首先,我们需要一些标准的 TFT 导入,如示例 5-13 所示。

示例 5-13. TFT 导入
import tensorflow as tf
import tensorflow_transform as tft
from tensorflow_transform.tf_metadata import schema_utils

现在我们已经导入了所需的模块,是时候为组件创建入口点了,如示例 5-14 所示。

示例 5-14. 创建入口点
def preprocessing_fn(inputs):

在这个函数内部,我们进行数据转换以生成我们的特征。并非所有特征都需要转换,这就是为什么还有一个复制方法来将输入镜像到输出,如果你只是添加特征的话。在我们的邮件列表示例中,我们可以计算词汇表,如示例 5-15 所示。

示例 5-15. 计算词汇表
    outputs = {}
    # TFT business logic goes here
    outputs["body_stuff"] = tft.compute_and_apply_vocabulary(inputs["body"],
                                                             top_k=1000)
    return outputs

此函数不支持任意的 Python 代码。所有的转换必须表达为 TensorFlow 或 TensorFlow Transform 操作。TensorFlow 操作一次操作一个张量,但在数据准备中,我们通常希望对所有输入数据进行某些计算,而 TensorFlow Transform 的操作使我们能够实现这一点。请参阅TFT Python 文档或调用help(tft)以查看一些起始操作。

一旦您编写了所需的转换,就可以将它们添加到管道中了。这样做的最简单方法是使用 Kubeflow 的tfx/Transform组件。加载该组件与其他 TFX 组件相同,如示例 5-6 所示。使用此组件的独特之处在于需要将转换代码作为上传到 S3 或 GCS 的文件传递给它。它还需要数据,您可以使用 TFDV 的输出(如果使用了 TFDV),或者像我们为 TFDV 所做的那样加载示例。使用 TFT 组件的示例如示例 5-16 所示。

示例 5-16. 使用 TFT 组件
    transformed_output = tfx_transform(
        input_data=records_example.output,
        schema=schema_op.outputs['output'],
        module_file=module_file)  # Path to your TFT code on GCS/S3

现在,您拥有一个包含特征准备及在服务时间转换请求的机器学习管道的关键工件。TensorFlow Transform 的紧密集成可以使模型服务变得不那么复杂。TensorFlow Transform 与 Kubeflow 组件结合使用并不能为所有项目提供足够的能力,因此我们将在接下来看看分布式特征准备。

使用 Apache Spark 进行分布式数据处理

Apache Spark 是一个开源的分布式数据处理工具,可以在各种集群上运行。Kubeflow 通过几个不同的组件支持 Apache Spark,以便您可以访问特定于云的功能。由于您可能对 Spark 不太熟悉,我们将在数据和特征准备的背景下简要介绍 Spark 的 Dataset/Dataframe API。如果想要超越基础知识,我们推荐Learning SparkSpark: The Definitive GuideHigh Performance Spark作为提升 Spark 技能的资源。

注意

在我们的代码中,为了进行所有特征和数据准备,我们将其结构化为单一阶段,因为一旦达到规模,写入和加载数据之间的步骤是昂贵的。

Kubeflow 中的 Spark 操作者

一旦您超越了实验阶段,使用 Kubeflow 的本地 Spark operator EMR 或 Dataproc 是最佳选择。最具可移植性的操作者是本地 Spark operator,它不依赖于任何特定的云。要使用任何操作者,您需要打包 Spark 程序并将其存储在分布式文件系统(如 GCS、S3 等)中或将其放入容器中。

如果您使用 Python 或 R,我们建议构建一个 Spark 容器以便安装依赖项。对于 Scala 或 Java 代码来说,这不那么关键。如果将应用程序放入容器中,可以使用local:///来引用它。您可以使用 gcr.io/spark-operator/spark-py:v2.4.5 容器作为基础,或者构建您自己的容器——遵循 Spark 在 Kubernetes 上的说明,或查看第九章。示例 5-19 展示了如何安装任何依赖项并复制应用程序。如果决定更新应用程序,仍可以使用容器,只需配置主资源为分布式文件系统。

我们在第九章中还涵盖了如何构建自定义容器。

示例 5-19. 安装要求并复制应用程序
# Use the Spark operator image as base
FROM gcr.io/spark-operator/spark-py:v2.4.5
# Install Python requirements
COPY requirements.txt /
RUN pip3 install -r /requirements.txt
# Now you can reference local:///job/my_file.py
RUN mkdir -p /job
COPY *.py /job

ENTRYPOINT ["/opt/entrypoint.sh"]

在 Kubeflow 中运行 Spark 的两个特定于云的选项是 Amazon EMR 和 Google Dataproc 组件。然而,它们各自接受不同的参数,这意味着您需要调整您的流水线。

EMR 组件允许您设置集群、提交作业以及清理集群。两个集群任务组件分别是aws/emr/create_cluster用于启动和aws/emr/delete_cluster用于删除。用于运行 PySpark 作业的组件是aws/emr/submit_pyspark_job。如果您不是在重用外部集群,则无论 submit_pyspark_job 组件是否成功,触发删除组件都非常重要。

尽管它们具有不同的参数,但 Dataproc 集群的工作流程与 EMR 类似。组件的命名类似,使用 gcp/dataproc/create_cluster/gcp/dataproc/delete_cluster/ 来管理生命周期,并使用 gcp/dataproc/submit_pyspark_job/ 运行我们的作业。

与 EMR 和 Dataproc 组件不同,Spark 运算符没有组件。对于没有组件的 Kubernetes 运算符,您可以使用 dsl.ResourceOp 调用它们。示例 5-20 展示了使用 ResourceOp 启动 Spark 作业。

示例 5-20. 使用 ResourceOp 启动 Spark 作业
resource = {
    "apiVersion": "sparkoperator.k8s.io/v1beta2",
    "kind": "SparkApplication",
    "metadata": {
        "name": "boop",
        "namespace": "kubeflow"
    },
    "spec": {
        "type": "Python",
        "mode": "cluster",
        "image": "gcr.io/boos-demo-projects-are-rad/kf-steps/kubeflow/myspark",
        "imagePullPolicy": "Always",
        # See the Dockerfile OR use GCS/S3/...
        "mainApplicationFile": "local:///job/job.py",
        "sparkVersion": "2.4.5",
        "restartPolicy": {
            "type": "Never"
        },
        "driver": {
            "cores": 1,
            "coreLimit": "1200m",
            "memory": "512m",
            "labels": {
                "version": "2.4.5",
            },
            # also try spark-operatoroperator-sa
            "serviceAccount": "spark-operatoroperator-sa",
        },
        "executor": {
            "cores": 1,
            "instances": 2,
            "memory": "512m"
        },
        "labels": {
            "version": "2.4.5"
        },
    }
}

@dsl.pipeline(name="local Pipeline", description="No need to ask why.")
def local_pipeline():

    rop = dsl.ResourceOp(
        name="boop",
        k8s_resource=resource,
        action="create",
        success_condition="status.applicationState.state == COMPLETED")
警告

Kubeflow 对 ResourceOp 请求不执行任何验证。例如,在 Spark 中,作业名称必须能够用作有效 DNS 名称的开头,而在容器操作中,容器名称会被重写,但 ResourceOps 只是直接通过请求。

读取输入数据

Spark 支持多种数据源,包括(但不限于):Parquet、JSON、JDBC、ORC、JSON、Hive、CSV、ElasticSearch、MongoDB、Neo4j、Cassandra、Snowflake、Redis、Riak Time Series 等¹¹。加载数据非常简单,通常只需要指定格式。例如,在我们的邮件列表示例中,读取我们数据准备阶段的 Parquet 格式输出就像 示例 5-25 中所示。

示例 5-25. 读取我们数据的 Parquet 格式输出
initial_posts = session.read.format("parquet").load(fs_prefix +
                                                    "/initial_posts")
ids_in_reply = session.read.format("parquet").load(fs_prefix + "/ids_in_reply")

如果它被格式化为 JSON,我们只需要将 “parquet” 更改为 “JSON”¹²。

验证架构的有效性

我们通常认为我们了解数据的字段和类型。Spark 可以快速发现数据的架构,当我们的数据是自描述格式(如 Parquet)时。在其他格式(如 JSON)中,直到 Spark 读取记录时,架构才会知道。无论数据格式如何,指定架构并确保数据匹配是良好的做法,如 示例 5-26 中所示。与在模型部署期间出现的错误相比,数据加载期间的错误更容易调试。

示例 5-26. 指定架构
ids_schema = StructType([
    StructField("In-Reply-To", StringType(), nullable=True),
    StructField("message-id", StringType(), nullable=True)
])
ids_in_reply = session.read.format("parquet").schema(ids_schema).load(
    fs_prefix + "/ids_in_reply")

您可以配置 Spark 处理损坏和不符合规范的记录,通过删除它们、保留它们或停止过程(即失败作业)。默认为宽容模式,保留无效记录同时设置字段为空,允许我们使用相同的技术处理缺失字段来处理架构不匹配。

处理缺失字段

在许多情况下,我们的数据中会有一些缺失。您可以选择删除缺少字段的记录,回退到次要字段,填充平均值,或者保持原样。Spark 的内置工具用于这些任务在 DataFrameNaFunctions 中。正确的解决方案取决于您的数据和最终使用的算法。最常见的是删除记录并确保我们没有过多地筛选记录,这在邮件列表数据中使用 示例 5-27 进行了说明。

示例 5-27. 删除记录
initial_posts_count = initial_posts.count()
initial_posts_cleaned = initial_posts.na.drop(how='any',
                                              subset=['body', 'from'])
initial_posts_cleaned_count = initial_posts_cleaned.count()

过滤掉坏数据

检测不正确的数据可能是具有挑战性的。然而,如果不进行至少一些数据清洗,模型可能会在噪声中训练。通常,确定坏数据取决于从业者对问题的领域知识。

在 Spark 中支持的常见技术是异常值移除。然而,简单应用这一技术可能会移除有效记录。利用你的领域经验,你可以编写自定义验证函数,并使用 Spark 的filter函数删除任何不符合条件的记录,就像我们在示例 5-28 中的邮件列表示例中所示。

示例 5-28. 过滤掉坏数据
def is_ok(post):
    # Your special business logic goes here
    return True

spark_mailing_list_data_cleaned = spark_mailing_list_data_with_date.filter(
    is_ok)

保存输出

当数据准备好后,是时候保存输出了。如果你将使用 Apache Spark 进行特征准备,现在可以跳过此步骤。

如果你想返回到单机工具,将数据保存到持久存储通常是最简单的。为此,通过调用toPandas()将数据带回主程序,就像在示例 5-30 中展示的那样。现在你可以按照下一个工具期望的格式保存数据了。

示例 5-30. 保存到持久存储
initial_posts.toPandas()

如果数据量大,或者你想使用对象存储,Spark 可以写入多种不同的格式(就像它可以从多种不同的格式加载一样)。正确的格式取决于你打算用于特征准备的工具。在示例 5-31 中展示了写入 Parquet 格式的方法。

示例 5-31. 写入 Parquet 格式
initial_posts.write.format("parquet").mode('overwrite').save(fs_prefix +
                                                             "/initial_posts")
ids_in_reply.write.format("parquet").mode('overwrite').save(fs_prefix +
                                                            "/ids_in_reply")

现在你已经看到了各种可以用来获取和清理数据的工具。我们已经看到了本地工具的灵活性,分布式工具的可扩展性以及来自 TensorFlow Extended 的集成。数据形状已经到位,现在让我们确保正确的特征可用,并以可用于机器学习模型的格式获取它们。

使用 Apache Spark 进行分布式特征准备

Apache Spark 拥有大量内置的特征准备工具,在pyspark.ml.feature中,你可以使用这些工具生成特征。你可以像在数据准备阶段一样使用 Spark。你可能会发现使用 Spark 自带的 ML 管道是将多个特征准备阶段组合在一起的一种简便方法。

对于 Spark 邮件列表示例,我们有文本输入数据。为了能够训练多种模型,将其转换为词向量是我们首选的特征准备形式。这涉及首先使用 Spark 的分词器对数据进行分词。一旦有了这些标记,我们可以训练一个 Word2Vec 模型并生成我们的词向量。示例 5-32 展示了如何使用 Spark 为邮件列表示例准备特征。

示例 5-32. 准备邮件列表的特征
tokenizer = Tokenizer(inputCol="body", outputCol="body_tokens")
body_hashing = HashingTF(inputCol="body_tokens",
                         outputCol="raw_body_features",
                         numFeatures=10000)
body_idf = IDF(inputCol="raw_body_features", outputCol="body_features")
body_word2Vec = Word2Vec(vectorSize=5,
                         minCount=0,
                         numPartitions=10,
                         inputCol="body_tokens",
                         outputCol="body_vecs")
assembler = VectorAssembler(inputCols=[
    "body_features", "body_vecs", "contains_python_stack_trace",
    "contains_java_stack_trace", "contains_exception_in_task"
],
                            outputCol="features")

通过这个最终的分布式特征准备示例,你可以准备好扩展以处理更大的数据量(如果它们碰巧出现)。如果你处理的是更小的数据,你已经看到了如何使用容器化的简单技术继续使用你喜欢的工具。无论哪种方式,你几乎准备好进入机器学习管道的下一阶段。

在管道中将它放在一起

我们已经展示了如何解决数据和特征准备中的个别问题,但现在我们需要把它们整合起来。在我们的本地示例中,我们编写了带有类型和返回参数的函数,以便轻松地放入管道中。由于我们在每个阶段返回输出路径,我们可以使用函数输出来为我们创建依赖关系图。将这些函数放入管道中的示例在 示例 5-33 中说明。

示例 5-33. 将函数放在一起
@kfp.dsl.pipeline(name='Simple1', description='Simple1')
def my_pipeline_mini(year: int):
    dvop = dsl.VolumeOp(name="create_pvc",
                        resource_name="my-pvc-2",
                        size="5Gi",
                        modes=dsl.VOLUME_MODE_RWO)
    tldvop = dsl.VolumeOp(name="create_pvc",
                          resource_name="tld-volume-2",
                          size="100Mi",
                          modes=dsl.VOLUME_MODE_RWO)
    download_data_op = kfp.components.func_to_container_op(
        download_data, packages_to_install=['lxml', 'requests'])
    download_tld_info_op = kfp.components.func_to_container_op(
        download_tld_data,
        packages_to_install=['requests', 'pandas>=0.24', 'tables'])
    clean_data_op = kfp.components.func_to_container_op(
        clean_data, packages_to_install=['pandas>=0.24', 'tables'])

    step1 = download_data_op(year).add_pvolumes(
        {"/data_processing": dvop.volume})
    step2 = clean_data_op(input_path=step1.output).add_pvolumes(
        {"/data_processing": dvop.volume})
    step3 = download_tld_info_op().add_pvolumes({"/tld_info": tldvop.volume})

kfp.compiler.Compiler().compile(my_pipeline_mini, 'local-data-prep-2.zip')

您可以看到这里的特征准备步骤遵循了所有本地组件的相同一般模式。然而,我们用于特征准备的库有些不同,所以我们已经将 packages_to_install 的值更改为安装 Scikit-learn,如 示例 5-34 所示。

示例 5-34. 安装 Scikit-learn
    prepare_features_op = kfp.components.func_to_container_op(
        prepare_features,
        packages_to_install=['pandas>=0.24', 'tables', 'scikit-learn'])
提示

当你开始探索一个新的数据集时,你可能会发现,像往常一样使用笔记本会更容易,而不使用管道组件。在可能的情况下,遵循与管道相同的一般结构将使得将你的探索工作投入到生产中更加容易。

这些步骤没有指定要使用的容器。对于你刚刚构建的 SpamAssassin 容器,你可以按照 示例 5-35 的方式编写。

示例 5-35. 指定一个容器
clean_data_op = kfp.components.func_to_container_op(
    clean_data,
    base_image="{0}/kubeflow/spammassisan".format(container_registry),
    packages_to_install=['pandas>=0.24', 'tables'])

有时候,在各个阶段之间写入数据的成本太高。在我们的推荐系统示例中,与邮件列表示例不同,我们选择将数据和特征准备放在一个单一的管道阶段中。在我们的分布式邮件列表示例中,我们也构建了一个单一的 Spark 作业。在这些情况下,我们迄今为止的整个工作只是一个阶段。使用单一阶段可以避免在中间写文件,但可能会增加调试的复杂性。

将整个笔记本作为数据准备管道阶段

如果你不想将数据准备笔记本的各个部分转换为管道,你可以将整个笔记本作为一个阶段。你可以使用 JupyterHub 使用的相同容器来以编程方式运行笔记本。

要做到这一点,您需要制作一个新的 Dockerfile,指定它基于另一个容器使用 FROM,然后添加一个 COPY 指令将笔记本打包到新容器中。由于人口普查数据示例中有一个现成的笔记本,这就是我们在 示例 5-36 中采取的方法。

示例 5-36. 将整个笔记本作为数据准备
FROM gcr.io/kubeflow-images-public/tensorflow-1.6.0-notebook-cpu

COPY ./ /workdir /

如果您需要额外的 Python 依赖项,可以使用RUN指令来安装它们。将依赖项放入容器可以加快流水线速度,特别是对于复杂的包。对于我们的邮件列表示例,Dockerfile 将如示例 5-37 所示。

示例 5-37. 使用 RUN 命令将 Python 依赖项添加到容器中
RUN pip3 install --upgrade lxml pandas

我们可以像在第四章中的推荐器示例中一样,在流水线中使用dsl.ContainerOp处理此容器。现在你有两种方法可以在 Kubeflow 中使用笔记本,接下来我们将介绍笔记本以外的选项。

提示

笔记本是否需要 GPU 资源?在指定dsl.ContainerOp时,调用set_gpu_limit并指定所需的 GPU 类型,可以满足您的需求。

结论

现在您已经准备好数据来训练模型。我们已经看到,在特征和数据准备方面,没有一种适合所有情况的方法;我们的不同示例需要不同的工具支持。我们还看到了如何在同一个问题中可能需要修改方法,例如我们在扩展邮件列表示例的范围以包含更多数据时。特征的数量和质量,以及产生它们的数据,对机器学习项目的成功至关重要。您可以通过使用较小的数据集运行示例并比较模型来测试这一点。

还要记住,数据和特征准备不是一劳永逸的活动,您可能希望在开发模型时重新审视此步骤。您可能会发现有些功能是您希望拥有的,或者您认为性能良好的功能实际上暗示了数据质量问题。在接下来的章节中,当我们训练和服务模型时,请随时重新审视数据和特征准备的重要性。

¹ 如果您对数据准备还不熟悉,可以参考TFX 文档进行详细了解。

² 使用更多数据进行训练的积极影响在 A. Halevy 等人的文章“数据的非合理有效性”中已经清晰地表现出来,《IEEE 智能系统》24 卷 2 期(2009 年 3-4 月):8-12 页,https://oreil.ly/YI820,以及 T. Schnoebelen 的“更多数据胜过更好的算法”,Data Science Central,2016 年 9 月 23 日,https://oreil.ly/oLe1R

³ 更多详细定义,请参见“使用数据准备掌握机器学习的六个步骤”

⁴ 这里涵盖了太多工具,但这篇博文包含了很多信息。

⁵ 数据集往往随着时间增长而不是减少,因此从分布式工具开始可以帮助您扩展工作规模。

⁶ 查看这篇关于缺失数据填充的博客文章

⁷ 有一些 VB6 代码需要运行吗?查看第九章,探索超越 TensorFlow 的内容,并做出一点点放弃红酒的牺牲。

⁸ 在此 Apache 页面上有一个兼容性矩阵,尽管目前 Beam 的 Python 支持需要启动额外的 Docker 容器,使得在 Kubernetes 上的支持更加复杂。

⁹ 虽然 TFX 自动安装了 TFDV,但如果你使用的是旧版本且没有指定tensorflow-data-validation,可能会出现Could not find a version that satisfies the requirement的错误,因此我们在这里明确说明需要同时安装两者。

¹⁰ 虽然严格来说不是文件格式,由于 TFX 可以接受 Pandas 数据帧,常见的模式是首先使用 Pandas 加载数据。

¹¹ 虽然没有明确的列表,但许多供应商在此 Spark 页面上列出了它们的格式。

¹² 当然,由于大多数格式存在轻微差异,如果默认设置不起作用,它们具有配置选项。

第六章:物件和元数据存储

机器学习通常涉及处理大量原始和中间(转换后)数据,其最终目标是创建和部署模型。为了理解我们的模型,必须能够探索用于其创建和转换的数据集(数据谱系)。收集这些数据集及其应用的转换称为我们模型的元数据。¹

在机器学习中,模型元数据对于可重现性至关重要;² 可重现性对于可靠的生产部署至关重要。捕捉元数据使我们能够在重新运行作业或实验时理解变化。理解变化对于迭代开发和改进我们的模型是必要的。它还为模型比较提供了坚实的基础。正如 Pete Warden 在这篇文章中定义的那样:

要复现结果,需要准确记录代码、训练数据和整个平台。

对于其他常见的 ML 操作,如模型比较、可重现模型创建等,也需要相同的信息。

有许多不同的选项可以用来跟踪模型的元数据。Kubeflow 内置了一个称为Kubeflow ML Metadata的工具用于此目的。该工具的目标是帮助 Kubeflow 用户通过跟踪和管理工作流程产生的元数据来理解和管理其 ML 工作流。我们可以集成到我们的 Kubeflow 管道中的另一个跟踪元数据的工具是 MLflow Tracking。它提供 API 和 UI,用于在运行机器学习代码时记录参数、代码版本、指标和输出文件,并在稍后可视化结果。

在本章中,我们将讨论 Kubeflow 的 ML Metadata 项目的能力,并展示其如何使用。我们还将考虑此实现的一些缺点,并探索使用其他第三方软件的可能性:MLflow⁴。

Kubeflow ML Metadata

Kubeflow ML Metadata 是一个用于记录和检索与模型创建相关的元数据的库。在当前实现中,Kubeflow Metadata 仅提供 Python API。要使用其他语言,您需要实现特定于语言的 Python 插件才能使用该库。为了理解其工作原理,我们将从一个简单的人工示例开始,展示 Kubeflow Metadata 的基本功能,使用一个非常简单的笔记本(基于这个demo)⁵。

Kubeflow Metadata 的实现从所需的导入开始,如示例 6-1 所示。

示例 6-1。所需导入
from kfmd import metadata
import pandas
from datetime import datetime

在 Kubeflow Metadata 中,所有信息都按照工作空间、运行和执行进行组织。您需要定义一个工作空间,以便 Kubeflow 可以跟踪和组织记录。示例 6-2 中的代码展示了如何实现这一点。

示例 6-2. 定义工作空间
ws1 = metadata.Workspace(
    # Connect to metadata-service in namespace kubeflow.
    backend_url_prefix="metadata-service.kubeflow.svc.cluster.local:8080",
    name="ws1",
    description="a workspace for testing",
    labels={"n1": "v1"})
r = metadata.Run(
    workspace=ws1,
    name="run-" + datetime.utcnow().isoformat("T") ,
    description="a run in ws_1",
)
exec = metadata.Execution(
    name = "execution" + datetime.utcnow().isoformat("T") ,
    workspace=ws1,
    run=r,
    description="execution example",
)
提示

可以在同一或不同的应用程序中多次定义工作空间、运行和执行。如果它们不存在,将被创建;如果它们已存在,则将被使用。

Kubeflow 不会自动跟踪应用程序使用的数据集。它们必须在代码中显式注册。按照经典的 MNIST 示例,元数据中数据集的注册应实现如示例 6-3 所示。

示例 6-3. 元数据示例
data_set = exec.log_input(
        metadata.DataSet(
            description="an example data",
            name="mytable-dump",
            owner="owner@my-company.org",
            uri="file://path/to/dataset",
            version="v1.0.0",
            query="SELECT * FROM mytable"))

除了数据外,Kubeflow Metadata 还允许您存储有关模型及其指标的信息。其实现代码显示在示例 6-4 中。

示例 6-4. 另一个元数据示例
model = exec.log_output(
    metadata.Model(
            name="MNIST",
            description="model to recognize handwritten digits",
            owner="someone@kubeflow.org",
            uri="gcs://my-bucket/mnist",
            model_type="neural network",
            training_framework={
                "name": "tensorflow",
                "version": "v1.0"
            },
            hyperparameters={
                "learning_rate": 0.5,
                "layers": [10, 3, 1],
                "early_stop": True
            },
            version="v0.0.1",
            labels={"mylabel": "l1"}))
metrics = exec.log_output(
    metadata.Metrics(
            name="MNIST-evaluation",
            description="validating the MNIST model to recognize handwritten digits",
            owner="someone@kubeflow.org",
            uri="gcs://my-bucket/mnist-eval.csv",
            data_set_id=data_set.id,
            model_id=model.id,
            metrics_type=metadata.Metrics.VALIDATION,
            values={"accuracy": 0.95},
            labels={"mylabel": "l1"}))

这些代码片段将实现用于存储模型创建元数据的所有主要步骤:

  1. 定义工作空间、运行和执行。

  2. 存储有关用于模型创建的数据资产的信息。

  3. 存储有关创建的模型的信息,包括其版本、类型、训练框架及用于创建的超参数。

  4. 存储有关模型评估指标的信息。

在实际实现中,这些代码片段应用于实际代码中,用于捕获用于数据准备、机器学习等的元数据。参见第七章以了解在哪里以及如何捕获此类信息的示例。

收集元数据仅在有方法查看它时才有用。Kubeflow Metadata 提供两种查看方式——通过编程方式和使用元数据 UI。

编程查询

可以通过编程查询以下功能。

首先,我们列出工作空间中的所有模型,如示例 6-5 所示。

示例 6-5. 列出所有模型
pandas.DataFrame.from_dict(ws1.list(metadata.Model.ARTIFACT_TYPE_NAME))

在我们的代码中,我们只创建了一个单一模型,这是此查询结果的返回(参见表 6-1)。

表 6-1. 模型列表

id workspace run create_time description model_type
0 2 ws1 run-2020-01-10T22:13:20.959882 2020-01-10T22:13:26.324443Z 用于识别手写数字的模型 神经网络
name owner version uri training_framework
--- --- --- --- ---
MNIST someone@kubeflow.org v0.0.1 gcs://my-bucket/mnist {name: tensorflow, version: v1.0}

接下来,我们获取基本谱系(参见示例 6-6)。在我们的情况下,我们创建了一个单一模型,因此返回的谱系将仅包含此模型的 ID。

示例 6-6. 基本谱系
print("model id is " + model.id) ![1](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/1.png)

1

返回model id is 2

然后我们找到产生该模型的执行。在我们的示例应用程序中,我们创建了一个单独的执行。这个执行的 ID 作为这个查询的结果返回,如 示例 6-7 所示。

Example 6-7. 找到执行
output_events = ws1.client.list_events2(model.id).events
execution_id = output_events[0].execution_id
print(execution_id) ![1](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/1.png)

1

返回 1.

最后,在 示例 6-8 中说明,我们找到与该执行相关的所有事件。

Example 6-8. 获取所有相关事件
all_events = ws1.client.list_events(execution_id).events
assert len(all_events) == 3
print("\nAll events related to this model:")
pandas.DataFrame.from_dict([e.to_dict() for e in all_events])

在我们的情况下,我们使用了单个输入来创建模型和指标。因此,这个查询的结果如 表格 6-2 所示。

Table 6-2. 以表格形式显示的查询结果

artifact_id execution_id path type milliseconds_since_epoch
0 1 1 None INPUT 1578694406318
1 2 1 None OUTPUT 1578694406338
2 3 1 None OUTPUT 1578694406358

Kubeflow Metadata UI

除了提供用于编写分析元数据的 API 外,Kubeflow Metadata 工具还提供一个 UI,允许您在不编写代码的情况下查看元数据。通过主 Kubeflow UI 访问元数据 UI,如 图 6-1 所示。

Metadata UI 截图

Figure 6-1. 访问 Metadata UI

一旦您点击艺术品存储,您应该看到可用艺术品(已记录的元数据事件)的列表,如 图 6-2 所示。

艺术品存储 UI 截图

Figure 6-2. 艺术品存储 UI 中的艺术品列表

从这个视图中,我们可以点击单个艺术品并查看其详细信息,如 图 6-3 所示。

艺术品视图截图

Figure 6-3. 艺术品视图

Kubeflow Metadata 提供了一些基本功能来存储和查看机器学习元数据;然而,其功能非常有限,特别是在查看和操作存储的元数据方面。MLflow 提供了一个更强大的机器学习元数据管理实现。虽然 MLflow 不是 Kubeflow 分发的一部分,但很容易将其部署在 Kubeflow 旁边,并从基于 Kubeflow 的应用程序中使用,正如下一节所述。

使用 MLflow 的元数据工具与 Kubeflow

MLflow 是一个开源平台,用于管理端到端的机器学习生命周期。它包括三个主要功能:

MLflow 追踪

跟踪实验以记录和比较参数和结果

MLflow 项目

将 ML 代码打包成可重用、可再现的形式,以便与其他数据科学家共享或转移到生产环境

MLflow 模型

管理和部署来自各种 ML 库的模型到各种模型服务和推理平台

对于我们的 Kubeflow 元数据讨论目的,我们只讨论 MLflow 跟踪组件的部署和使用 —— 用于记录参数、代码版本、指标和输出文件的 API 和 UI,以及在运行机器学习代码并可视化结果时使用。MLflow 跟踪允许您使用 PythonRESTRJava API 记录和查询实验,这显著扩展了 API 的覆盖范围,允许您存储和访问来自不同 ML 组件的元数据。

MLflow 跟踪围绕运行的概念组织,这些运行是某些数据科学代码的执行。每次运行记录以下信息:

代码版本

用于运行的 Git 提交哈希,如果是从 MLflow 项目运行的话

开始和结束时间

运行的开始和结束时间

启动运行的文件名称,或者如果是从 MLflow 项目运行的话,项目名称和入口点

参数

您选择的键-值输入参数。键和值都是字符串。

指标

键-值指标,其中值是数值。每个指标可以在运行过程中更新(例如,跟踪模型损失函数如何收敛),而 MLflow 记录并允许您可视化指标的完整历史记录。

工件

任何格式的输出文件。在这里,您可以记录图像(例如 PNG 文件)、模型(例如,一个序列化的 Scikit-learn 模型)和数据文件(例如,一个 Parquet 文件)作为工件。

大多数 MLflow 示例 使用本地 MLflow 安装,这对我们的目的不合适。对于我们的实施,我们需要基于集群的安装,允许我们从不同的 Docker 实例写入元数据并集中查看它们。遵循项目 MLflow Tracking Server based on Docker and AWS S3 中概述的方法,展示了这种 MLflow 跟踪组件部署的整体架构,如 图 6-4 所示。

MLflow 组件部署的整体架构

图 6-4. MLflow 组件部署的整体架构

此架构的主要组件包括:

  • 已经作为 Kubeflow 安装的一部分的 MinIO 服务器

  • MLflow 跟踪服务器 — MLflow UI 组件 — 需要添加到 Kubeflow 安装中以支持 MLflow 使用的额外组件

  • 用户代码,如笔记本、Python、R 或 Java 应用程序

创建和部署 MLflow 跟踪服务器

MLflow 跟踪服务器允许您将 MLflow 运行记录到本地文件、到与 SQLAlchemy 兼容的数据库,或者远程到跟踪服务器。在我们的实施中,我们使用的是远程服务器。

MLflow 跟踪服务器有两个存储组件:后端存储和 artifact 存储。后端存储用于存储实验和运行元数据以及运行的参数、指标和标签。MLflow 支持两种类型的后端存储:文件存储和数据库支持的存储。为了简单起见,我们将使用文件存储。在我们的部署中,这个文件存储是 Docker 镜像的一部分,这意味着在服务器重启的情况下,这些数据会丢失。如果您需要长期存储,可以使用外部文件系统,如 NFS 服务器,或者数据库。Artifact 存储是一个适合大数据的位置(例如 S3 桶或共享的 NFS 文件系统),客户端在这里记录他们的 artifact 输出(例如模型)。为了使我们的部署与云无关,我们决定使用 MinIO(Kubeflow 的一部分)作为 artifact 存储。基于这些决策,用于构建 MLflow 跟踪服务器的 Docker 文件看起来像是 示例 6-9(与 此 GitHub 仓库 中的实现类似)。

示例 6-9. MLflow 跟踪服务器
FROM python:3.7

RUN pip3 install --upgrade pip && \
   pip3 install mlflow --upgrade && \
   pip3 install awscli --upgrade  && \
   pip3 install boto3 --upgrade

ENV PORT 5000
ENV AWS_BUCKET bucket
ENV AWS_ACCESS_KEY_ID aws_id
ENV AWS_SECRET_ACCESS_KEY aws_key
ENV FILE_DIR /tmp/mlflow

RUN mkdir -p /opt/mlflow
COPY run.sh /opt/mlflow
RUN chmod -R 777 /opt/mlflow/

ENTRYPOINT ["/opt/mlflow/run.sh"]

在这里,我们首先加载 MLflow 代码(使用 pip),设置环境变量,然后复制并运行启动脚本。这里使用的启动脚本看起来像是 示例 6-10。⁶

示例 6-10. MLflow 启动脚本
#!/bin/sh
mkdir -p $FILE_DIR

mlflow server \
   --backend-store-uri file://$FILE_DIR \
   --default-artifact-root s3://$AWS_BUCKET/mlflow/artifacts \
   --host 0.0.0.0 \
   --port $PORT

此脚本设置一个环境,然后验证所有必需的环境变量是否已设置。一旦验证成功,将启动一个 MLflow 服务器。一旦 Docker 创建完成,可以使用 示例 6-11 中的 Helm 命令(Helm 图表位于 本书的 GitHub 仓库)来安装服务器。

示例 6-11. 使用 Helm 安装 MLflow 服务器
helm install <location of the Helm chart>

这个 Helm 图表安装了三个主要组件,实现了 MLflow 跟踪服务器:

部署

部署 MLflow 服务器本身(单个副本)。这里的重要参数包括环境,包括 MinIO 端点、凭据和用于存储 artifact 的桶。

服务

创建一个暴露 MLflow 部署的 Kubernetes 服务

虚拟服务

通过 Kubeflow 使用的 Istio 入口网关向用户公开 MLflow 服务

一旦服务器部署完成,我们可以访问 UI,但此时它会显示没有可用的实验。现在让我们看看如何使用这个服务器来捕获元数据。⁷

记录运行数据

作为记录数据的示例,让我们看一下一些简单的代码。⁸ 我们将从安装所需的包开始,如示例 6-11 和 6-12 所示。

示例 6-12. 安装所需内容
!pip install pandas --upgrade --user
!pip install mlflow --upgrade --user ![1](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/1.png)
!pip install joblib --upgrade --user
!pip install numpy --upgrade --user
!pip install scipy --upgrade --user
!pip install scikit-learn --upgrade --user
!pip install boto3 --upgrade --user ![1](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/1.png)

1

这里 mlflowboto3 是用于元数据记录的包,而其余的则用于机器学习本身。

一旦安装了这些包,我们可以像 示例 6-13 中所示定义所需的导入。

示例 6-13. 导入所需库
import time
import json
import os
from joblib import Parallel, delayed

import pandas as pd
import numpy as np
import scipy

from sklearn.model_selection import train_test_split, KFold
from sklearn.metrics import mean_squared_error, mean_absolute_error
from sklearn.metrics import r2_score, explained_variance_score
from sklearn.exceptions import ConvergenceWarning

import mlflow
import mlflow.sklearn
from mlflow.tracking import MlflowClient

from warnings import simplefilter
simplefilter(action='ignore', category = FutureWarning)
simplefilter(action='ignore', category = ConvergenceWarning)

这里再次,os 和最后三个导入项是用于 MLflow 日志记录的必需,而其余的则用于机器学习。现在我们需要定义环境变量(参见 示例 6-14),以便正确访问用于存储工件的 MinIO 服务器。

示例 6-14. 设置环境变量
os.environ['MLFLOW_S3_ENDPOINT_URL'] = \
     'http://minio-service.kubeflow.svc.cluster.local:9000'
os.environ['AWS_ACCESS_KEY_ID'] = 'minio'
os.environ['AWS_SECRET_ACCESS_KEY'] = 'minio123'

请注意,除了跟踪服务器本身之外,MLFLOW_S3_ENDPOINT_URL 不仅在跟踪服务器定义中定义,而且在实际捕获元数据的代码中也有定义。这是因为,正如我们之前提到的,用户代码直接写入到工件存储中,绕过服务器。

这里我们跳过了大部分代码(完整代码可以在 本书的 GitHub 仓库 中找到),仅集中在与 MLflow 日志记录相关的部分。下一步(参见 示例 6-15)是连接到跟踪服务器并创建实验。

示例 6-15. 创建实验
remote_server_uri = "http://mlflowserver.kubeflow.svc.cluster.local:5000"
mlflow.set_tracking_uri(remote_server_uri)
experiment_name = "electricityconsumption-forecast"
mlflow.set_experiment(experiment_name)

一旦连接到服务器并创建(选择)实验,我们就可以开始记录数据。例如,让我们看一下存储 KNN 回归器 信息的代码,在 示例 6-16 中。

示例 6-16. 样本 KNN 模型
def train_knnmodel(parameters, inputs, tags, log = False):
    with mlflow.start_run(nested = True):

……………………………………………….
        # Build the model
        tic = time.time()
        model = KNeighborsRegressor(parameters["nbr_neighbors"],
                                weights = parameters["weight_method"])
        model.fit(array_inputs_train, array_output_train)
        duration_training = time.time() - tic

        # Make the prediction
        tic1 = time.time()
        prediction = model.predict(array_inputs_test)
        duration_prediction = time.time() - tic1

        # Evaluate the model prediction
        metrics = evaluation_model(array_output_test, prediction)

        # Log in mlflow (parameter)
        mlflow.log_params(parameters)

        # Log in mlflow (metrics)
        metrics["duration_training"] = duration_training
        metrics["duration_prediction"] = duration_prediction
        mlflow.log_metrics(metrics)

        # Log in mlflow (model)
        mlflow.sklearn.log_model(model, f"model")

        # Save model
        #mlflow.sklearn.save_model(model,
                         f"mlruns/1/{uri}/artifacts/model/sklearnmodel")

        # Tag the model
        mlflow.set_tags(tags)

在此代码片段中,我们可以看到有关模型创建和预测测试统计数据的不同类型数据是如何记录的。这里的信息与 Kubeflow Metadata 捕获的信息非常相似,包括输入、模型和指标。

最后,类似于 Kubeflow Metadata,MLflow 允许您以编程方式访问此元数据。MLflow 提供的主要 API 包括您在 示例 6-17 中看到的内容。

示例 6-17. 获取给定实验的运行
df_runs = mlflow.search_runs(experiment_ids="0") ![1](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/1.png)
print("Number of runs done : ", len(df_runs))

df_runs.sort_values(["metrics.rmse"], ascending = True, inplace = True) ![2](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/2.png)
df_runs.head()

1

获取给定实验的运行

2

根据特定参数对运行进行排序

MLflow 将根据均方根误差(rmse)对运行进行排序,并显示最佳运行。

要了解程序化运行查询的额外功能,请参阅 MLflow 文档

通过运行程序化查询的所有功能来评估运行元数据的最强大方法是通过 MLflow UI,我们将在下一步中介绍它。

使用 MLflow UI

MLflow 中的跟踪 UI 允许您可视化、搜索和比较运行,并下载运行工件或元数据以在其他工具中进行分析。由于 MLflow 不是 Kubeflow 的一部分,因此 Kubeflow UI 不提供其访问权限。根据提供的虚拟服务,MLflow UI 可通过 /mlflow 访问。

图 6-5 显示了运行描述的结果。可以使用搜索框来过滤结果。例如,如果我们只想看到 KNN 模型的结果,则可以使用搜索条件 tags.model="knn"。还可以使用更复杂的过滤器,如 tags.model="knn"metrics.duration_prediction < 0.002,这将返回预测持续时间小于 0.002 秒的 KNN 模型的结果。

MLFlow 主页截图

图 6-5. MLflow 主页

点击单独运行,我们可以查看其详细信息,如 图 6-6 所示。

单独运行的截图

图 6-6. 单独运行的视图

或者,我们可以通过选择多个运行并点击比较按钮来比较它们,如 图 6-7 所示。

运行比较截图

图 6-7. 运行比较视图

我们还可以查看多个运行的指标比较,如 图 6-8 所示。⁹

运行指标比较视图截图

图 6-8. 运行指标比较视图

结论

本章节展示了 Kubeflow 部署中 Kubeflow Metadata 组件支持存储和查看 ML 元数据的方式。我们还讨论了这种实现的缺点,包括仅支持 Python 和界面弱等问题。最后,我们介绍了如何通过补充 Kubeflow 与类似功能的组件,如 MLflow,来增强其额外功能。

在 第七章 中,我们探讨了使用 Kubeflow 与 TensorFlow 进行模型训练和服务化的内容。

¹ 欲了解有关机器学习元数据的背景信息和要捕获内容的概述,请参阅 Luigi Patruno 撰写的这篇博客文章

² 欲了解更多,请参阅 Jennifer Villa 和 Yoav Zimmerman 撰写的这篇博客文章

³ 请注意,Kubeflow ML Metadata 与 ML Metadata(TFX 的一部分)是不同的。

⁴ MLflow 最初由 Databricks 开发,并且目前是 Linux 基金会 的一部分。

⁵ 本笔记本的完整代码位于本书的 GitHub 仓库

⁶ 这是一个简化的实现。要获取完整的实现,请查阅本书的 GitHub 仓库

⁷ 在这里,我们展示了使用 Python API 的用法。要了解更多 API(如 R、Java、REST),请参阅MLflow 文档

⁸ 这里的代码改编自 Jean-Michel Daignan 的这篇文章

⁹ 也请查看MLflow 文档,了解更多的 UI 功能。

第七章:训练一个机器学习模型

在 第五章 中,我们学习了如何准备和清理我们的数据,这是机器学习流程中的第一步。现在让我们深入探讨如何利用我们的数据来训练一个机器学习模型。

训练通常被认为是机器学习中的“大部分”工作。我们的目标是创建一个函数(即“模型”),能够准确预测它之前没有见过的结果。直观地说,模型训练非常像人类学习新技能的方式——我们观察、练习、纠正错误,并逐渐改进。在机器学习中,我们从一个可能不太擅长其工作的初始模型开始。然后,我们将模型通过一系列的训练步骤,将训练数据馈送给模型。在每个训练步骤中,我们将模型产生的预测结果与真实结果进行比较,并查看模型的表现如何。然后,我们调整这个模型的参数(例如,通过改变每个特征所赋予的权重),试图提高模型的准确性。一个好的模型是能够在不过度拟合特定输入集的情况下进行准确预测的模型。

在本章中,我们将学习如何使用两种不同的库——TensorFlow 和 Scikit-learn 来训练机器学习模型。TensorFlow 在 Kubeflow 中有原生的一流支持,而 Scikit-learn 则没有。但正如我们在本章中所看到的,这两个库都可以很容易地集成到 Kubeflow 中。我们将演示如何在 Kubeflow 的笔记本中对模型进行实验,以及如何将这些模型部署到生产环境中。

使用 TensorFlow 构建推荐系统

让我们首先来了解 TensorFlow——这是一个由 Google 开发的机器学习开源框架。它目前是实现深度学习的最流行库之一,特别是在机器学习驱动的应用程序中。TensorFlow 对于包括 CPU、GPU 和 TPU 在内的各种硬件的计算任务有很好的支持。我们选择 TensorFlow 进行这个教程是因为它的高级 API 用户友好,并且抽象了许多复杂的细节。

让我们通过一个简单的教程来熟悉 TensorFlow。在 第一章 中,我们介绍了我们的案例研究之一,即面向客户的产品推荐系统。在本章中,我们将使用 TensorFlow 来实现这个系统。具体来说,我们将做两件事情:

  1. 使用 TensorFlow 来训练一个产品推荐模型。

  2. 使用 Kubeflow 将训练代码封装并部署到生产集群中。

TensorFlow 的高级 Keras API 使得实现我们的模型相对容易。事实上,大部分模型可以用不到 50 行 Python 代码来实现。

提示

Keras 是用于深度学习模型的高级 TensorFlow API。它具有用户友好的界面和高可扩展性。此外,Keras 还预置了许多常见的神经网络实现,因此您可以立即启动一个模型。

让我们首先选择我们的推荐系统模型。我们从一个简单的假设开始——如果两个人(Alice 和 Bob)对一组产品有相似的意见,那么他们在其他产品上的看法也更可能相似。换句话说,Alice 更有可能与 Bob 拥有相同的偏好,而不是随机选择的第三个人。因此,我们可以仅使用用户的购买历史来构建推荐模型。这就是协同过滤的理念——我们从许多用户(因此称为“协同”)那里收集偏好信息,并使用这些数据进行选择性预测(因此称为“过滤”)。

要构建这个推荐模型,我们需要几件事:

用户的购买历史

我们将使用来自此 GitHub 仓库的示例输入数据

数据存储

为了确保我们的模型能够跨不同的平台工作,我们将使用 MinIO 作为存储系统。

训练模型

我们使用的实现基于Github 上的 Keras 模型

我们将首先使用 Kubeflow 的笔记本服务器对该模型进行实验,然后使用 Kubeflow 的 TFJob API 将训练工作部署到我们的集群上。

入门

让我们从下载先决条件开始。您可以从本书的 GitHub 仓库下载笔记本。要运行笔记本,您需要一个运行中包含 MinIO 服务的 Kubeflow 集群。请查看“支持组件”来配置 MinIO。确保还安装了 MinIO 客户端(“mc”)。

我们还需要准备数据以便进行训练:您可以从这个 GitHub 站点下载用户购买历史数据。然后,您可以使用 MinIO 客户端创建存储对象,如示例 7-1 中所示。

示例 7-1. 设置先决条件
# Port-forward the MinIO service to http://localhost:9000
kubectl port-forward -n kubeflow svc/minio-service 9000:9000 &

# Configure MinIO host
mc config host add minio http://localhost:9000 minio minio123

# Create storage bucket
mc mb minio/data

# Copy storage objects
mc cp go/src/github.com/medium/items-recommender/data/recommend_1.csv \\
        minio/data/recommender/users.csv
mc cp go/src/github.com/medium/items-recommender/data/trx_data.csv \\
        minio/data/recommender/transactions.csv

启动新的笔记本会话

现在让我们通过创建一个新的笔记本开始。您可以通过在 Kubeflow 仪表板的“笔记本服务器”面板中导航,然后点击“New Server”并按照说明操作来完成此操作。对于本示例,我们使用tensorFlow-1.15.2-notebook-cpu:1.0镜像。¹

当笔记本服务器启动时,请点击右上角的“上传”按钮并上传Recommender_Kubeflow.ipynb文件。单击文件以启动新会话。

代码的前几部分涉及导入库并从 MinIO 中读取训练数据。然后,我们对输入数据进行归一化处理,以便准备开始训练。这个过程称为特征准备,我们在第五章中讨论过。在本章中,我们将专注于练习的模型训练部分。

TensorFlow 训练

现在我们的笔记本已设置并准备好数据,我们可以创建一个 TensorFlow 会话,如示例 7-2 所示。²

示例 7-2. 创建 TensorFlow 会话
# Create TF session and set it in Keras
sess = tf.Session()
K.set_session(sess)
K.set_learning_phase(1)

对于模型类,我们使用协作过滤的示例 7-3 中的代码。

示例 7-3. 深度协同过滤学习
class DeepCollaborativeFiltering(Model):
   def__init__(self, n_customers, n_products, n_factors, p_dropout = 0.2):
      x1 = Input(shape = (1,), name="user")

      P = Embedding(n_customers, n_factors, input_length = 1)(x1)
      P = Reshape((n_factors,))(P)

      x2 = Input(shape = (1,), name="product")

      Q = Embedding(n_products, n_factors, input_length = 1)(x2)
      Q = Reshape((n_factors,))(Q)

      x = concatenate([P, Q], axis=1)
      x = Dropout(p_dropout)(x)

      x = Dense(n_factors)(x)
      x = Activation('relu')(x)
      x = Dropout(p_dropout)(x)

      output = Dense(1)(x)

      super(DeepCollaborativeFiltering, self).__init__([x1, x2], output)

   def rate(self, customer_idxs, product_idxs):
      if (type(customer_idxs) == int and type(product_idxs) == int):
          return self.predict([np.array(customer_idxs).reshape((1,)),\
                  np.array(product_idxs).reshape((1,))])

      if (type(customer_idxs) == str and type(product_idxs) == str):
          return self.predict( \
                 [np.array(customerMapping[customer_idxs]).reshape((1,)),\
                 np.array(productMapping[product_idxs]).reshape((1,))])

      return self.predict([
         np.array([customerMapping[customer_idx] \
                for customer_idx in customer_idxs]),
            np.array([productMapping[product_idx] \
                for product_idx in product_idxs])
      ])

这是我们模型类的基础。它包括一个构造函数,其中包含一些用于使用 Keras API 实例化协作过滤模型的代码,以及一个“rate”函数,我们可以使用我们的模型进行预测——即客户对特定产品的评分。

我们可以像示例 7-4 那样创建一个模型实例。

示例 7-4. 模型创建
model = DeepCollaborativeFiltering(n_customers, n_products, n_factors)
model.summary()

现在我们准备开始训练我们的模型。我们可以通过设置一些超参数来实现这一点,如示例 7-5 所示。

示例 7-5. 设置训练配置
bs = 64
val_per = 0.25
epochs = 3

这些是控制训练过程的超参数。它们通常在训练开始之前设置,不像模型参数那样是从训练过程中学习的。设置正确的超参数值可以显著影响模型的有效性。目前,让我们为它们设置一些默认值。在第十章中,我们将学习如何使用 Kubeflow 调整超参数。

现在我们已准备好运行训练代码。查看示例 7-6。

示例 7-6. 拟合模型
model.compile(optimizer = 'adam', loss = mean_squared_logarithmic_error)
model.fit(x = [customer_idxs, product_idxs], y = ratings,
        batch_size = bs, epochs = epochs, validation_split = val_per)
print('Done training!')

训练完成后,你应该能看到类似于示例 7-7 中的结果。

示例 7-7. 模型训练结果
Train on 100188 samples, validate on 33397 samples
Epoch 1/3
100188/100188 [==============================]
- 21s 212us/step - loss: 0.0105 - val_loss: 0.0186
Epoch 2/3
100188/100188 [==============================]
- 20s 203us/step - loss: 0.0092 - val_loss: 0.0188
Epoch 3/3
100188/100188 [==============================]
- 21s 212us/step - loss: 0.0078 - val_loss: 0.0192
Done training!

恭喜你:你已成功在 Jupyter 笔记本中训练了一个 TensorFlow 模型。但我们还没有完成——为了以后能够使用我们的模型,我们应该先导出它。你可以通过设置使用 MinIO Client 的导出目的地来完成此操作,如示例 7-8 所示。

示例 7-8. 设置导出目的地
directorystream = minioClient.get_object('data', 'recommender/directory.txt')
directory = ""
for d in directorystream.stream(32*1024):
    directory += d.decode('utf-8')
arg_version = "1"
export_path = 's3://models/' + directory + '/' + arg_version + '/'
print ('Exporting trained model to', export_path)

一旦设置了导出目的地,你可以像示例 7-9 那样导出模型。

示例 7-9. 导出模型
# Inputs/outputs
tensor_info_users = tf.saved_model.utils.build_tensor_info(model.input[0])
tensor_info_products = tf.saved_model.utils.build_tensor_info(model.input[1])
tensor_info_pred = tf.saved_model.utils.build_tensor_info(model.output)

print ("tensor_info_users", tensor_info_users.name)
print ("tensor_info_products", tensor_info_products.name)
print ("tensor_info_pred", tensor_info_pred.name)

# Signature
prediction_signature = (tf.saved_model.signature_def_utils.build_signature_def(
        inputs={"users": tensor_info_users, "products": tensor_info_products},
        outputs={"predictions": tensor_info_pred},
        method_name=tf.saved_model.signature_constants.PREDICT_METHOD_NAME))
# Export
legacy_init_op = tf.group(tf.tables_initializer(), name='legacy_init_op')
builder = tf.saved_model.builder.SavedModelBuilder(export_path)
builder.add_meta_graph_and_variables(
      sess, [tf.saved_model.tag_constants.SERVING],
      signature_def_map={
        tf.saved_model.signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY:
          prediction_signature,
      },
      legacy_init_op=legacy_init_op)
builder.save()

现在我们准备使用这个模型来提供预测,正如我们将在第八章中学到的那样。但在此之前,让我们看看如何使用 Kubeflow 部署这个训练作业。

部署 TensorFlow 训练任务

到目前为止,我们已经使用 Jupyter 笔记本进行了一些 TensorFlow 训练,这是原型设计和实验的好方法。但很快我们可能会发现我们的原型不足——也许我们需要使用更多数据来完善模型,或者我们需要使用专用硬件来训练模型。有时候,我们甚至可能需要持续运行训练作业,因为我们的模型在不断发展。或许更重要的是,我们的模型必须能够部署到生产环境中,以便为实际的客户请求提供服务。

为了处理这些需求,我们的训练代码必须易于打包和部署到不同的环境中。实现这一点的一种方法是使用 TFJob——一个 Kubernetes 自定义资源(使用 Kubernetes 操作者 tf-operator 实现),您可以使用它在 Kubernetes 上运行 TensorFlow 训练作业。

我们将首先将我们的推荐器部署为单容器 TFJob。由于我们已经有一个 Python 笔记本,将其导出为 Python 文件非常简单——只需选择“文件”,然后选择“另存为”,然后选择“Python”。这将保存您的笔记本作为一个可以立即执行的 Python 文件。

接下来的步骤是将训练代码打包到容器中。可以通过 Dockerfile 完成,就像在示例 7-10 中看到的那样。

示例 7-10. TFJob Dockerfile
FROM tensorflow/tensorflow:1.15.2-py3
RUN pip3 install --upgrade pip
RUN pip3 install pandas --upgrade
RUN pip3 install keras --upgrade
RUN pip3 install minio --upgrade
RUN mkdir -p /opt/kubeflow
COPY Recommender_Kubeflow.py /opt/kubeflow/
ENTRYPOINT ["python3", "/opt/kubeflow/Recommender_Kubeflow.py"]

接下来,我们需要将此容器及其所需的库一起构建,并将容器映像推送到存储库:

docker build -t kubeflow/recommenderjob:1.0 .
docker push kubeflow/recommenderjob:1.0

完成后,我们准备创建 TFJob 的规范,就像在示例 7-11 中所示。

示例 7-11. 单容器 TFJob 示例
apiVersion: "kubeflow.org/v1"   ![1](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/1.png)
kind: "TFJob"                   ![2](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/2.png)
metadata:
  name: "recommenderjob"        ![3](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/3.png)
spec:
  tfReplicaSpecs:               ![4](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/4.png)
    Worker:
      replicas: 1
    restartPolicy: Never
    template:
      spec:
        containers:
        - name: tensorflow image: kubeflow/recommenderjob:1.0

1

apiVersion字段指定您正在使用的 TFJob 自定义资源的版本。需要在您的 Kubeflow 集群中安装相应的版本(在本例中为 v1)。

2

kind字段标识自定义资源的类型——在本例中是 TFJob。

3

metadata字段适用于所有 Kubernetes 对象,并用于在集群中唯一标识对象——您可以在此处添加名称、命名空间和标签等字段。

4

架构中最重要的部分是tfReplicaSpecs。这是您的 TensorFlow 训练集群及其期望状态的实际描述。在此示例中,我们只有一个工作节点副本。在接下来的部分中,我们将进一步检查这个字段。

您的 TFJob 还有一些其他可选配置,包括:

activeDeadlineSeconds

在系统可以终止作业之前保持作业活动的时间。如果设置了此项,则系统将在截止日期到期后终止作业。

backoffLimit

将作业标记为失败之前重试此作业的次数。例如,将其设置为 3 意味着如果作业失败 3 次,系统将停止重试。

cleanPodPolicy

配置是否在作业完成后清理 Kubernetes pods。设置此策略可以用于保留用于调试的 pods。可以设置为 All(清理所有 pods)、Running(仅清理运行中的 pods)或 None(不清理 pods)。

现在将 TFJob 部署到您的集群中,就像示例 7-12 中所示。

示例 7-12. 部署 TFJob
kubectl apply -f recommenderjob.yaml

您可以使用以下命令监视 TFJob 的状态,如示例 7-13。

示例 7-13. 查看 TFJob 的状态
kubectl describe tfjob recommenderjob

这应该显示类似于示例 7-14 的内容。

示例 7-14. TF 推荐作业描述
Status:
  Completion Time:  2019-05-18T00:58:27Z
  Conditions:
    Last Transition Time:  2019-05-18T02:34:24Z
    Last Update Time:      2019-05-18T02:34:24Z
    Message:               TFJob recommenderjob is created.
    Reason:                TFJobCreated
    Status:                True
    Type:                  Created
    Last Transition Time:  2019-05-18T02:38:28Z
    Last Update Time:      2019-05-18T02:38:28Z
    Message:               TFJob recommenderjob is running.
    Reason:                TFJobRunning
    Status:                False
    Type:                  Running
    Last Transition Time:  2019-05-18T02:38:29Z
    Last Update Time:      2019-05-18T02:38:29Z
    Message:               TFJob recommenderjob successfully completed.
    Reason:                TFJobSucceeded
    Status:                True
    Type:                  Succeeded
  Replica Statuses:
    Worker:
      Succeeded:  1

注意,状态字段包含一系列作业条件,表示作业转换到每个状态的时间。这对调试非常有用——如果作业失败,作业失败的原因将在此处显示。

到目前为止,我们使用了一些训练样本数量适中的相对简单和直接的模型进行了训练。在实际生活中,学习更复杂的模型可能需要大量训练样本或模型参数。这样的模型可能过大且计算复杂,无法由单台机器处理。这就是分布式训练发挥作用的地方。

分布式训练

到目前为止,我们已经在 Kubeflow 上部署了一个单工作节点的 TensorFlow 作业。它被称为“单工作节点”,因为从托管数据到执行实际训练步骤的所有工作都在单台机器上完成。然而,随着模型变得更复杂,单台机器通常不够用——我们可能需要将模型或训练样本分布到多台联网机器上。TensorFlow 支持分布式训练模式,其中训练在多个工作节点上并行执行。

分布式训练通常有两种方式:数据并行和模型并行。在数据并行中,训练数据被分成多个块,每个块上运行相同的训练代码。在每个训练步骤结束时,每个工作节点向所有其他节点通信其更新。模型并行则相反——所有工作节点使用相同的训练数据,但模型本身被分割。在每个步骤结束时,每个工作节点负责同步模型的共享部分。

TFJob 接口支持多工作节点分布式训练。在概念上,TFJob 是与训练作业相关的所有资源的逻辑分组,包括podsservices。在 Kubeflow 中,每个复制的工作节点或参数服务器都计划在自己的单容器 pod 上。为了使副本能够相互同步,每个副本都需要通过端点暴露自己,这是一个 Kubernetes 内部服务。将这些资源在父资源(即 TFJob)下逻辑地分组允许这些资源一起协同调度和垃圾回收。

在本节中,我们将部署一个简单的 MNIST 示例,并进行分布式训练。TensorFlow 训练代码已为您提供,可以在 此 GitHub 仓库 中找到。

让我们看一下分布式 TensorFlow 作业的 YAML 文件,位于 示例 7-15 中。

示例 7-15. 分布式 TFJob 示例
apiVersion: "kubeflow.org/v1"
kind: "TFJob"
metadata:
  name: "mnist"
  namespace: kubeflow
spec:
  cleanPodPolicy: None
  tfReplicaSpecs:
    Worker:
      replicas: 2
      restartPolicy: Never
      template:
        spec:
          containers:
            - name: tensorflow
              image: gcr.io/kubeflow-ci/tf-mnist-with-summaries:1.0
              command:
                - "python"
                - "/var/tf_mnist/mnist_with_summaries.py"
                - "--log_dir=/train/logs"
                - "--learning_rate=0.01"
                - "--batch_size=150"
              volumeMounts:
                - mountPath: "/train"
                  name: "training"
          volumes:
            - name: "training"
              persistentVolumeClaim:
                claimName: "tfevent-volume"

注意,tfReplicaSpecs 字段现在包含几种不同的副本类型。在典型的 TensorFlow 训练集群中,有几种可能的情况:

主节点

负责编排计算任务、发出事件并对模型进行检查点处理

参数服务器

为模型参数提供分布式数据存储

工作节点

这是实际进行计算和训练的地方。当主节点未明确定义(如前述示例中),其中一个工作节点充当主节点。

评估者

评估者可以用于在训练模型时计算评估指标。

还要注意,副本规范包含多个属性,描述其期望的状态:

replicas

应为此副本类型生成多少副本

template

描述每个副本要创建的 pod 的 PodTemplateSpec

restartPolicy

确定 pod 退出时是否重新启动。允许的值如下:

Always

意味着 pod 将始终重新启动。这个策略适用于参数服务器,因为它们永不退出,应在失败事件中始终重新启动。

OnFailure

意味着如果 pod 由于失败而退出,将重新启动 pod。非零退出代码表示失败。退出代码为 0 表示成功,pod 将不会重新启动。这个策略适用于主节点和工作节点。

ExitCode

意味着重新启动行为取决于 TensorFlow 容器的退出代码如下:

  • 0 表示进程成功完成,将不会重新启动。

  • 1–127 表示永久错误,容器将不会重新启动。

  • 128–255 表示可重试错误,容器将被重新启动。这个策略适用于主节点和工作节点。

Never

意味着终止的 pod 将永远不会重新启动。这个策略应该很少使用,因为 Kubernetes 可能会因各种原因(如节点变得不健康)而终止 pod,并且这会阻止作业恢复。

编写完 TFJob 规范后,将其部署到您的 Kubeflow 集群:

kubectl apply -f dist-mnist.yaml

监控作业状态类似于单容器作业:

kubectl describe tfjob mnist

这应该输出类似于 示例 7-16 的内容。

示例 7-16. TFJob 执行结果
Status:
  Completion Time:  2019-05-12T00:58:27Z
  Conditions:
    Last Transition Time:  2019-05-12T00:57:31Z
    Last Update Time:      2019-05-12T00:57:31Z
    Message:               TFJob dist-mnist-example is created.
    Reason:                TFJobCreated
    Status:                True
    Type:                  Created
    Last Transition Time:  2019-05-12T00:58:21Z
    Last Update Time:      2019-05-12T00:58:21Z
    Message:               TFJob dist-mnist-example is running.
    Reason:                TFJobRunning
    Status:                False
    Type:                  Running
    Last Transition Time:  2019-05-12T00:58:27Z
    Last Update Time:      2019-05-12T00:58:27Z
    Message:               TFJob dist-mnist-example successfully completed.
    Reason:                TFJobSucceeded
    Status:                True
    Type:                  Succeeded
  Replica Statuses:
    Worker:
      Succeeded:  2

注意,Replica Statuses 字段显示了每个副本类型的状态细分。当所有工作节点都完成时,TFJob 将成功完成。如果有任何工作节点失败,则 TFJob 的状态也将失败。

使用 GPU

GPU 是由许多较小且专业的核心组成的处理器。最初设计用于渲染图形,GPU 越来越多地用于大规模并行计算任务,如机器学习。与 CPU 不同,GPU 非常适合在其多个核心上分发大型工作负载并同时执行。

要使用 GPU 进行训练,您的 Kubeflow 集群需要预先配置以启用 GPU。请参考您的云服务提供商的文档以启用 GPU 使用。在集群上启用 GPU 后,您可以通过修改命令行参数在训练规范中的特定副本类型上启用 GPU,例如示例 7-17。

示例 7-17. TFJob with GPU example
    Worker:
      replicas: 4
      restartPolicy: Never
      template:
        spec:
          containers:
            - name: tensorflow
              image: kubeflow/tf-dist-mnist-test:1.0
              args:
            - python
            - /var/tf_dist_mnist/dist_mnist.py
            - --num_gpus=1

使用其他框架进行分布式训练

Kubeflow 被设计为一个多框架的机器学习平台。这意味着分布式训练的模式可以轻松扩展到其他框架。截至本文撰写时,已编写了许多运算符,为其他框架(包括 PyTorch 和 Caffe2)提供一流支持。

示例 7-18 展示了 PyTorch 训练作业规范的样子。

示例 7-18. Pytorch Distributed Training Example
apiVersion: "kubeflow.org/v1"
kind: "PyTorchJob"
metadata:
  name: "pytorch-dist"
spec:
  pytorchReplicaSpecs:
    Master:
      replicas: 1
      restartPolicy: OnFailure
      template:
        spec:
          containers:
            - name: pytorch
              image: gcr.io/kubeflow-ci/pytorch-dist-sendrecv-test:1.0
    Worker:
      replicas: 3
      restartPolicy: OnFailure
      template:
        spec:
          containers:
            - name: pytorch
              image: gcr.io/kubeflow-ci/pytorch-dist-sendrecv-test:1.0

如您所见,其格式与 TFJobs 非常相似。唯一的区别在于副本类型。

使用 Scikit-Learn 训练模型

到目前为止,我们已经看到如何使用 Kubeflow 中的内置运算符来训练机器学习模型。然而,还有许多框架和库没有 Kubeflow 运算符。在这些情况下,您仍然可以在 Jupyter 笔记本³或自定义 Docker 镜像中使用您喜欢的框架。

Scikit-learn 是一个基于 NumPy 构建的用于机器学习的开源 Python 库,用于高性能线性代数和数组操作。该项目起源于 scikits.learn,由 David Cournapeau 在 Google Summer of Code 项目中开发。其名称源于它是一个“SciKit”(SciPy 工具包),是一个单独开发和分发的第三方扩展到 SciPy。Scikit-learn 是 GitHub 上最受欢迎的机器学习库之一,也是维护最好的之一。在 Kubeflow 中,使用 Scikit-learn 训练模型作为通用的 Python 代码支持,无需专门的分布式训练运算符。

该库支持最先进的算法,如 KNN、XGBoost、随机森林和 SVM。Scikit-learn 在 Kaggle 竞赛和知名技术公司中被广泛使用。Scikit-learn 在预处理、降维(参数选择)、分类、回归、聚类和模型选择方面提供帮助。

在本节中,我们将通过使用 Scikit-learn 在1994 年美国人口普查数据集上训练模型来探索 Kubeflow。该示例基于用于收入预测的 Anchor 解释的这个实现,并利用了从 1994 年人口普查数据集中提取的数据。数据集包括多个分类变量和连续特征,包括年龄、教育、婚姻状况、职业、工资、关系、种族、性别、原籍国家以及资本收益和损失。我们将使用随机森林算法——一种集成学习方法,用于分类、回归以及其他任务,在训练时通过构建大量决策树并输出类的众数(分类)或个体树的平均预测(回归)来运行。

你可以从本书的 GitHub 存储库下载这个笔记本。

开始一个新的笔记本会话

让我们从创建一个新的笔记本开始。类似于 TensorFlow 训练,你可以通过导航到你的 Kubeflow 仪表板中的“笔记本服务器”面板,然后点击“新建服务器”,按照指示操作。例如,我们可以使用tensorFlow-1.15.2-notebook-cpu:1.0 image

提示

在 Kubeflow 中工作时,利用 GPU 资源加速你的 Scikit 模型的一种简单方法是切换到 GPU 类型。

当笔记本服务器启动时,在右上角点击“上传”按钮并上传IncomePrediction.ipynb文件。点击文件以启动一个新的会话。

数据准备

笔记本的前几个部分涉及导入库和读取数据。然后我们继续进行特征准备。⁴ 为了特征转换,我们使用 Scikit-learn 的管道。管道使得将一致数据输入模型变得更加容易。

对于我们的随机森林训练,我们需要定义有序(标准化数据)和分类(独热编码)特征,就像示例 7-19 中那样。

示例 7-19. 特征准备
ordinal_features = [x for x in range(len(feature_names))
                if x not in list(category_map.keys())]
ordinal_transformer = Pipeline(steps=[
    ('imputer',  SimpleImputer(strategy='median')),
    ('scaler', StandardScaler())])

categorical_features = list(category_map.keys())
categorical_transformer = Pipeline(steps=[('imputer',
    SimpleImputer(strategy='median')),
    ('onehot', OneHotEncoder(handle_unknown='ignore'))])
提示

许多真实世界的数据集包含缺失值,这些值被数据特定的占位符(如空白和 NaN)编码。这种数据集通常与假设所有值为数值的 Scikit-learn 估计器不兼容。有多种策略可以处理这种缺失数据。一个基本的策略是丢弃包含缺失值的整行和/或列,这样做的代价是丢失数据。一个更好的策略是填补缺失值——通过从已知数据部分推断出它们。Simple imputer是一个 Scikit-learn 类,允许你通过用指定的预定义值替换 NaN 值来处理预测模型数据集中的缺失数据。

一旦定义了特征,我们可以使用列转换器将它们组合,如示例 7-20 所示。

示例 7-20. 使用列转换器组合列
preprocessor = ColumnTransformer(transformers=[
    ('num', ordinal_transformer, ordinal_features),
    ('cat', categorical_transformer, categorical_features)])
preprocessor.fit(X_train)
提示

Scikit-learn 的独热编码用于将分类特征编码为独热数字数组。编码器将整数或字符串数组转换,用分类(离散)特征替换值。使用独热编码方案对特征进行编码,为每个类别创建一个二进制列,并返回一个稀疏矩阵或密集数组(取决于 sparse 参数)。

转换器本身看起来像示例 7-21。

示例 7-21. 数据变换器
ColumnTransformer(n_jobs=None, remainder='drop', sparse_threshold=0.3,
  transformer_weights=None,
  transformers=[('num',
    Pipeline(memory=None,
      steps=[
        ('imputer', SimpleImputer(add_indicator=False,
          copy=True,
          fill_value=None,
          missing_values=nan,
          strategy='median',
          verbose=0)),
        ('scaler', StandardScaler(copy=True,
          with_mean=True,
          with_std=True))],
        verbose=False),
      [0, 8, 9, 10]),
    ('cat',
     Pipeline(memory=None,
       steps=[('imputer', SimpleImputer(add_indicator=False,
         copy=True,
         fill_value=None,
         missing_values=nan,
         strategy='median',
         verbose=0)),
       ('onehot', OneHotEncoder(categories='auto',
         drop=None,
         dtype=<class 'numpy.float64'>,
         handle_unknown='ignore',
         sparse=True))],
       verbose=False),
       [1, 2, 3, 4, 5, 6, 7, 11])],
    verbose=False)

由于这种转换,我们的数据以特征的形式准备好进行训练。

Scikit-Learn 训练

一旦我们准备好特征,就可以开始训练。这里我们将使用由 Scikit-learn 库提供的RandomForestClassifier,如示例 7-22 所示。

示例 7-22. 使用 RandomForestClassifier
np.random.seed(0)
clf = RandomForestClassifier(n_estimators=50)
clf.fit(preprocessor.transform(X_train), Y_train)
提示

机器学习算法的集合和特定特征是选择特定机器学习实现框架的主要驱动因素之一。即使在不同框架中相同算法的实现提供略有不同的特性,这些特性可能(或可能不)对您的特定数据集很重要。

一旦预测完成,我们可以评估训练结果,如示例 7-23 所示。

示例 7-23. 评估训练结果
predict_fn = lambda x: clf.predict(preprocessor.transform(x))
print('Train accuracy: ', accuracy_score(Y_train, predict_fn(X_train)))
print('Test accuracy: ', accuracy_score(Y_test, predict_fn(X_test)))

该部分将返回示例 7-24 中的结果。

示例 7-24. 训练结果
Train accuracy:  0.9655333333333334
Test accuracy:  0.855859375

在这一点上,模型已创建并可以通过导出直接使用(参见下一节)。模型的一个最重要的属性是其可解释性。虽然模型的可解释性主要用于模型服务,但对于模型创建也很重要,有两个主要原因:

  • 如果在模型创建期间模型服务中的可解释性很重要,我们经常需要验证所创建的模型是否可解释。

  • 许多模型解释方法要求在模型创建过程中进行额外的计算。

基于此,我们将展示如何在模型创建过程中实现模型可解释性⁵。

解释模型

对于模型解释,我们使用锚点,这是Seldon's Alibi project的一部分。

该算法提供了适用于应用于图像、文本和表格数据的分类模型的模型无关(黑盒)和人类可解释的解释。连续特征被离散化为分位数(例如,分位数),因此它们变得更易解释。在候选锚点中,保持特征不变(相同类别或分箱为离散化特征),同时从训练集中对其他特征进行抽样,如示例 7-25 所示。

示例 7-25. 定义表格锚点
explainer = AnchorTabular(
    predict_fn, feature_names, categorical_names=category_map, seed=1)
explainer.fit(X_train, disc_perc=[25, 50, 75])

这创建了表格锚点(示例 7-26)。

示例 7-26. 表格锚点
AnchorTabular(meta={
    'name': 'AnchorTabular',
    'type': ['blackbox'],
    'explanations': ['local'],
    'params': {'seed': 1, 'disc_perc': [25, 50, 75]}
})

现在我们可以为测试集中第一条观测的预测获取一个锚点。锚点是一个充分条件——即,当锚点成立时,预测应与示例 7-27 中此实例的预测相同。

示例 7-27. 预测计算
idx = 0
class_names = adult.target_names
print('Prediction: ', class_names[explainer.predictor( \
                X_test[idx].reshape(1, -1))[0]])

返回如示例 7-28 所示的预测计算结果。

示例 7-28. 预测计算结果
Prediction:  <=50K

我们将精度阈值设置为0.95。这意味着在锚定条件成立的观察中,预测至少 95%的时间将与解释实例的预测相同。现在我们可以为这个预测获取解释(示例 7-29)。

示例 7-29. 模型解释
explanation = explainer.explain(X_test[idx], threshold=0.95)
print('Anchor: %s' % (' AND '.join(explanation.anchor)))
print('Precision: %.2f' % explanation.precision)
print('Coverage: %.2f' % explanation.coverage)

返回如示例 7-30 所示的模型解释结果。

示例 7-30. 模型解释结果
Anchor: Marital Status = Separated AND Sex = Female
Precision: 0.95
Coverage: 0.18

这告诉我们,决策的主要因素是婚姻状况(Separated)和性别(Female)。并非所有点都可以找到锚点。让我们尝试为测试集中的另一条观测获取一个锚点——一个预测为>50K的示例,如示例 7-31 所示。

示例 7-31. 模型解释
idx = 6
class_names = adult.target_names
print('Prediction: ', class_names[explainer.predictor( \
                X_test[idx].reshape(1, -1))[0]])

explanation = explainer.explain(X_test[idx], threshold=0.95)
print('Anchor: %s' % (' AND '.join(explanation.anchor)))
print('Precision: %.2f' % explanation.precision)
print('Coverage: %.2f' % explanation.coverage)

返回如示例 7-32 所示的模型解释结果。

示例 7-32. 模型解释结果
Prediction:  >50K
Could not find a result satisfying the 0.95 precision constraint.
Now returning the best non-eligible result.
Anchor: Capital Loss > 0.00 AND Relationship = Husband AND
    Marital Status = Married AND Age > 37.00 AND
    Race = White AND Country = United-States AND Sex = Male
Precision: 0.71
Coverage: 0.05

由于数据集不平衡(大约 25:75 高:低收入者比例),在采样阶段与低收入者对应的特征范围将被过度采样。因此,在这种情况下找不到锚点。这是一个特征,因为它可以指出数据集不平衡,但也可以通过生成平衡数据集来修复,以便为任何类找到锚点。

模型导出

为了将创建的模型用于服务,我们需要导出模型。这可以通过 Scikit-learn 功能完成,如示例 7-33 所示。

示例 7-33. 导出模型
dump(clf, '/tmp/job/income.joblib')

这将以 Scikit-learn 格式导出一个模型,例如,可以由 Scikit-learn 服务器用于推断。

集成到流水线中

无论您想使用哪个基于 Python 的机器学习库,如果 Kubeflow 没有相应的操作符,您可以按照正常方式编写您的代码,然后将其容器化。要将本章中构建的笔记本作为流水线阶段使用,请参阅“将整个笔记本用作数据准备流水线阶段”。在这里,我们可以使用file_output将生成的模型上传到我们的工件跟踪系统,但您也可以使用持久卷机制。

结论

在本章中,我们看了如何使用两种非常不同的框架(TensorFlow 和 Scikit-learn)在 Kubeflow 中训练机器学习模型。

我们学习了如何使用 TensorFlow 构建协同过滤推荐系统。我们使用 Kubeflow 创建了一个笔记本会话,在那里我们使用了 Keras API 原型化了一个 TensorFlow 模型,然后使用 TFJob API 将我们的训练作业部署到了 Kubernetes 集群。最后,我们看了如何使用 TFJob 进行分布式训练。

我们还学习了如何使用 Scikit-learn 训练通用的 Python 模型,这是 Kubeflow 不原生支持的框架。第九章讨论了如何集成不受支持的非 Python 机器学习系统,这有点复杂。尽管 Kubeflow 的一方面训练操作员可以简化您的工作,但重要的是要记住您并不受此限制。

在第八章中,我们将探讨如何提供在本章中训练的模型。

¹ 当前,Kubeflow 提供了带有 TensorFlow 1.15.2 和 2.1.0 的 CPU 和 GPU 镜像,或者您可以使用自定义镜像。

² 本章中的示例使用了 TensorFlow 1.15.2。您可以在这个 Kubeflow GitHub 网站找到使用 TensorFlow 2.1.0 的示例。

³ 目前 Jupyter 笔记本支持的语言包括 Python、R、Julia 和 Scala。

⁴ 请参阅第五章详细讨论特征准备。

⁵ 有关模型可解释性的更多信息,请参阅Rui Aguiar 的这篇博客文章

第八章:模型推断

注意

我们要感谢 Clive Cox 和 Alejandro Saucedo 来自Seldon对本章的巨大贡献。

机器学习中大部分关注点都集中在算法开发上。然而,模型并不是为了创建而创建的,它们被创建出来是为了投入生产。通常情况下,当人们谈论将模型“投入生产”时,他们指的是执行推断。如第一章所介绍的并在图 1-1 中有所说明,一个完整的推断解决方案旨在提供服务、监控和更新功能。

模型服务

将训练好的模型置于可以处理预测请求的服务后面

模型监控

监控模型服务器性能异常以及底层模型的准确性

模型更新

完全管理模型的版本控制,并简化版本之间的推广和回滚

本章将探讨每个核心组件,并定义其功能的期望。在确定具体期望之后,我们将列出理想推断解决方案需满足的要求清单。最后,我们将讨论 Kubeflow 支持的推断方案及如何使用它们满足您的推断需求。

模型服务

模型推断的第一步是模型服务,即将您的模型托管在一个可以与之交互的服务后面。模型服务有两种基本方法:嵌入式,其中模型直接部署到应用程序中,以及模型作为服务(MaaS),其中一个专用于模型服务的独立服务可从企业中的任何应用程序中使用。表 8-1 提供了这些方法的比较。

表 8-1 比较嵌入式和 MaaS

服务类型 优点 缺点
嵌入式
  • 提供最大性能

  • 提供最简单的基础设施

  • 无需计划异常用户行为

|

  • 必须在每个使用它的应用中部署模型

  • 当模型类型变化时需要应用更新

  • 所有部署策略,例如蓝绿部署,必须明确实施

|

模型作为服务(MaaS)
  • 简化与其他技术和组织流程的集成

  • 在多个流处理应用中重复使用模型部署

  • 允许在低功率设备(例如手机)上进行模型服务,无法运行复杂模型

  • 允许为来自多个客户端的请求进行小批量处理

  • 更容易提供内置功能,包括模型更新、可解释性、漂移检测等

  • 允许高级模型部署策略,如集成和多臂赌博机,需要与应用程序解耦

  • 允许应用和模型服务器之间的分别扩展,或在不同设备(如 CPU 和 GPU)上运行它们

|

  • 额外的网络跳跃会降低性能。

  • 与模型服务器紧密的时间耦合可能会影响整体服务级别协议。

|

Kubeflow 只支持 MaaS 方法。因此,我们将不讨论本书中的模型嵌入。¹

实施 MaaS 的两种主要方法:模型即代码模型即数据。模型即代码直接在服务实现中使用模型代码。模型即数据则使用中间模型格式(如 PMMLPFAONNXTensorFlow 的原生格式)驱动的通用实现。这两种方法在 Kubeflow 的不同模型服务器实现中使用。确定要使用哪种实现时,我们建议使用模型即数据,因为它允许在服务实例之间标准化模型交换,从而提供系统间的可移植性和通用模型服务解决方案的实现。

大多数常见的服务实现,如 TFServing、ONNX Runtime、Triton 和 TorchServe,使用模型即数据方法,并利用中间模型格式。其中一些实现仅支持一个框架,而其他实现则支持多个框架。不幸的是,每种解决方案都使用不同的模型格式并暴露独特的专有服务 API。这些接口无法满足所有人的需求。这些 API 接口的复杂性和分歧导致了用户体验的差异化,以及在不同模型框架之间切换时增加了摩擦力。

有一些强大的行业领导者试图统一模型服务器的开源社区,减少在切换模型框架时的摩擦力。Seldon 正在通过 Seldon Core 探索图推理;Bloomberg 和 IBM 正在使用类似 Knative 的解决方案进行无服务器模型服务的研究;而 Google 则正在进一步加固其用于 TensorFlow 模型的服务实现。

在 “Kubeflow 中的模型推理” 中,我们将讨论 Kubeflow 提供的服务解决方案以及将这些解决方案统一到单一接口的工作。

模型服务要求

模型服务需要你理解和管理开发运维(DevOps),以及处理模型的分析、实验和治理。这个范围广泛、复杂,并且在数据科学家中普遍存在。我们现在将开始明确你可能希望从服务解决方案中获得的期望。

首先,您需要框架的灵活性。像 Kubeflow 这样的解决方案允许您的训练与具体实现无关(即 TensorFlow 对比 PyTorch)。如果您编写了一个图像分类推断服务,那么底层模型是使用 PyTorch、Scikit-learn 还是 TensorFlow 训练的都不重要,服务接口应共享,以保持用户的 API 一致性。

其次,您希望能够利用与算法需求匹配的硬件优化器。有时完全拟合和调整的神经网络非常深,这意味着即使在评估阶段,您也会从像 GPU 或 TPU 这样的硬件优化器中受益,以推断模型。

第三,您的模型服务器应无缝与推断图中的其他组件交互。推断图可以包括特征转换器、预测器、解释器和漂移检测器,我们稍后会详细介绍。

第四,您还应该有选择在生产中扩展您的服务实例的选项,无论基础硬件如何,即推断的成本、延迟。这尤为重要且困难,因为 GPU 的自动缩放依赖于多种因素,包括 GPU/CPU 利用率指标、工作周期等,而确定用于自动缩放的指标并不明显。此外,推断图中每个组件的扩展应分别进行,因为它们的算法需求不同。

第五,您希望服务实例公开表现状态传输(REST)请求或通用远程过程调用(gRPC)。如果您有流式输入,可能希望支持像 Kafka 这样的流式接口。

模型监控

一旦您部署了模型,就必须监视生产中的模型服务器。当我们谈论模型服务器的监控时,不仅仅是指模型服务的洞察力,还包括适用于任何基于 Kubernetes 的应用程序的一般监控,包括内存、CPU、网络等。我们将在“监控您的模型”中更详细地探讨模型监控和模型洞察。

模型准确性、漂移和可解释性

在生成模型服务洞察时,监控的最常见的 ML 属性是模型准确性、模型漂移和可解释性。模型准确性 是指您训练数据的验证准确性。但随着实时数据分布开始偏离原始训练数据,这往往会导致模型漂移。换句话说,当发送到模型的数据的特征分布开始与训练模型时使用的数据显著不同时,模型会表现不佳。ML 洞察系统实施有效的技术来分析和检测可能发生在您的输入数据中的变化——概念漂移,这对于运行在生产系统中的模型至关重要。

另一种越来越受关注的模型洞察形式是模型可解释性,即解释为什么会产生某个特定结果的能力。更确切地说,它回答以下问题:

  • 模型在数据中认为哪些特征最重要?

  • 对于模型的任何单个预测,数据中的每个特征如何影响该特定预测?

  • 特征之间的交互对模型预测有何最大影响?

除了模型洞察之外,应用程序监控传统上与网络可观察性或遥测相关,包括日志聚合和服务网格相关的指标收集。这些工具在从活动服务实例中捕获数据方面非常有用。该基础设施暴露了足够的可查询信息,用于故障排除和警报,以防在可达性、利用率或延迟方面出现问题。

模型监控要求

监控模型准确性和模型漂移是困难的。幸运的是,这是一个非常活跃的研究领域,有各种开源解决方案。² 您的推断解决方案应使您能够插入提供所需功能的解决方案。现在,我们将看看您可能希望从您的模型监控组件中得到的内容。

首先,您希望您的推断服务能够在微服务架构中提供 ML 洞察,并能够简化漂移检测和模型解释解决方案的实验。

其次,您希望启用服务的监控、日志记录和跟踪。它还应支持像 Prometheus、Kibana 和 Zipkin 这样的解决方案,但也能无缝地支持它们的替代方案。

模型更新

如果您希望更新模型并发布新版本或回滚到以前的版本,您将希望部署和运行此更新版本。然而,当前部署与新部署之间的关系可以用多种方式定义。当您的推断系统引入多个版本的模型服务实例时,您可以使用影子模型或竞争模型:

影子模型

在考虑在生产中替换模型时,这些是有用的。您可以在当前模型旁边部署新模型,并发送相同的生产流量以收集关于影子模型表现的数据,然后再提升它。

竞争模型

这些是稍微复杂的场景,您正在尝试在生产环境中使用多个模型版本,通过诸如 A/B 测试之类的工具来找出哪一个更好。

让我们讨论三种主要的部署策略:

蓝绿部署

通过仅有一个生产流量的活动环境,可以减少与版本发布相关的停机时间和风险。

金丝雀部署

这些通过允许在版本之间进行基于百分比的流量切换,实现了逐步发布。

固定部署

这些允许您将实验流量暴露给新版本,同时将生产流量保持在当前版本。

金丝雀和固定与蓝绿相比增加了基础设施和路由规则的复杂性,以确保流量被重定向到正确的模型。有了这种启用,您可以开始收集数据,以便在何时开始移动流量时做出统计显著的决策。流量移动的一种统计方法是 A/B 测试。评估多个竞争模型的另一种流行方法是multi-armed bandits,其中需要您为每个模型定义一个分数或奖励,并根据它们各自的分数推广模型。

模型更新要求

模型升级必须简单,因此您用于升级的部署策略应易于配置和更改(即从固定到金丝雀)。您的推断解决方案还应在其设计中提供更复杂的图推断。我们将详细阐述您从推断解决方案中所需的内容:

首先,部署策略的切换——即从固定到金丝雀——应该是微不足道的。您可以通过抽象服务平面来实现抽象化的流量级别路由,该路由将在“无服务器和服务平面”中定义。

第二,应在升级之前测试和验证版本更改,并记录相应的升级。

第三,底层堆栈应使您能够配置常见于图推断文献中的更复杂的部署策略。

推断需求摘要

满足模型服务、监控和更新的要求后,您现在拥有一个完整的推断解决方案,完成了模型开发生命周期(MDLC)的故事。这使您可以将模型从实验室直接推广到生产,并且甚至可以处理此模型的更新,以便调整或修改其结构。现在我们将讨论 Kubeflow 提供的推断解决方案。

提示

有些机器学习实践者认为,持续学习(CL)对于他们的生产机器学习系统至关重要。CL 是模型不断从流数据中学习的能力。实质上,随着新数据的进入,模型将自主学习和适应生产环境。有些人甚至称之为 AutoML。通过一个完整的 MDLC 解决方案,该解决方案能够实现管道和金丝雀部署,您可以利用 Kubeflow 中可用的工具来设计这样的系统。

Kubeflow 中的模型推断

在推断中进行模型服务、监控和更新可能非常棘手,因为您需要一种管理所有这些期望的解决方案,以便为首次用户提供抽象,并为高级用户提供可定制性。

Kubeflow 为模型推断解决方案提供多种 选项。在本节中,我们将描述其中一些,包括 TensorFlow Serving、Seldon Core 和 KFServing。表 8-2 提供了这些解决方案的快速比较。

表 8-2. 比较不同的模型推断方法

解决方案 方法
TensorFlow Serving
  • 仅支持单一模型类型(TensorFlow)

  • 对监控指标(Prometheus)提供部分支持。

  • 通过模型版本标签支持版本 2.3 的金丝雀发布

  • 最简单的基础设施依赖

|

Seldon Core
  • 针对流行库(如 TensorFlow、H2O、XGBoost、MXNet 等)优化的 Docker 容器。

  • 将 Python 文件或 Java JAR 转换为完整的微服务的语言包装器

  • 支持由模型、转换器、合并器和路由器组成的推断流水线

  • 对监控指标和可审计请求日志的支持

  • 支持高级部署技术 —— 金丝雀发布、蓝绿部署等。

  • 支持高级 ML 洞察:解释器、异常检测器和对抗攻击检测器。

  • 更复杂的基础设施依赖

|

KFServing
  • 将 Seldon Core 添加无服务器(Knative)和标准化推断体验,同时为其他模型服务器提供可扩展性。

  • 最复杂的基础设施依赖

|

TensorFlow Serving

最受欢迎的服务实现之一是 TensorFlow Serving(TFServing),这是基于 TensorFlow 导出格式 的模型服务实现。TFServing 实现了一个灵活高效的 ML 模型服务系统,专为生产环境设计。TFServing 的架构如 图 8-1 所示。

TFServing 架构

图 8-1. TFServing 架构

TFServing 使用导出的 TensorFlow 模型作为输入,并支持使用 HTTP 或 gRPC 运行预测。TFServing 可以配置为使用以下之一:

  • 单一(最新)版本的模型

  • 多个具体版本的模型。

TensorFlow 可以在本地³和 Kubernetes 中⁴使用。Kubeflow 中的典型 TFServing 实现包括以下组件:

  • 运行所需副本数的 Kubernetes 部署。

  • 提供访问部署的 Kubernetes 服务。

  • 通过 Istio 虚拟服务,通过 Istio 入口网关公开服务

  • 一个 Istio DestinationRule,定义路由到服务的流量策略(这些规则可以指定负载均衡、连接池大小和异常检测设置,以便检测和清除负载均衡池中的不健康主机。)

我们将通过扩展我们的推荐示例来演示这些组件的实现方式。为了简化您的初始推断服务,您的示例 TFServing 实例将被限定为一个部署和一个允许 HTTP 访问的服务。该示例的 Helm 图表可以在本书的 GitHub 存储库 中找到。

该图表定义了一个 Kubernetes 部署和服务。部署使用了“标准”TFServing Docker 镜像,并在其配置规范中指向了一个在 S3 源位置的序列化模型。这个 S3 存储桶由本地 MinIO 实例管理。该服务在 Kubernetes 集群内部公开了这个部署。

可以使用以下命令部署该图表(假设您正在运行 Helm 3):

helm install <chart_location>

现在您已经部署了图表,需要一种与推断解决方案进行接口交互的方式。一种方法是将您的服务进行端口转发,以便流量可以重定向到本地主机进行测试。您可以使用示例 8-1 进行服务的端口转发。

示例 8-1. TFServing 服务的端口转发
kubectl port-forward service/recommendermodelserver 8501:8501

结果流量将被重新路由到 localhost:8051

现在您已准备好与 TFServing 推断解决方案交互。首先,您应该通过请求来自您的服务的模型部署信息来验证部署:

curl http://localhost:8501/v1/models/recommender/versions/1

预期输出显示在示例 8-2 中。

示例 8-2. TFServing 推荐模型版本状态
{
 "model_version_status": [
  {
   "version": "1",
   "state": "AVAILABLE",
   "status": {
    "error_code": "OK",
    "error_message": ""
   }
  }
 ]
}

您还可以通过发出以下 curl 命令获取模型的元数据,包括其签名定义:

curl http://localhost:8501/v1/models/recommender/versions/1/metadata

现在您的模型可用,并且具有正确的签名定义,您可以使用示例 8-3 中看到的命令预测服务。

示例 8-3. 向您的 TFServing 推荐服务发送请求
curl -X POST http://localhost:8501/v1/models/recommender/versions/1:predict\
-d '{"signature_name":"serving_default","inputs":\
{"products": [[1],[2]],"users" : [[25], [3]]}}'

执行示例 8-3 的结果显示在示例 8-4 中。

示例 8-4. 您的 TFServing 推荐服务的输出
{
    "outputs": {
        "model-version": [
            "1"
        ],
        "recommendations": [
            [
                0.140973762
            ],
            [
                0.0441606939
            ]
        ]
    }
}

您的 TensorFlow 模型现在位于一个实时推断解决方案之后。TFServing 使得部署新的 TensorFlow 算法和实验变得轻松,同时保持相同的服务器架构和 API。但旅程并未结束。首先,这些部署说明创建了一个服务,但未启用集群外的访问。⁵ 现在我们将进一步探讨这个特定解决方案对您的推断需求的所有能力。

回顾

如果您希望使用最低的基础设施要求部署 TensorFlow 模型,TFServing 是您的解决方案。但是,在考虑您的推断需求时,这也有其局限性。

模型服务

由于 TFServing 仅对 TensorFlow 提供生产级支持,因此并不具备您从框架无关推断服务中期望的灵活性。但它支持 REST、gRPC、GPU 加速、小批量处理以及适用于边缘设备的“精简”版本。尽管如此,无论底层硬件如何,这种支持都不涵盖流式输入或内置自动扩展功能。⁶ 此外,以一流方式支持扩展推断图——超越公平指标——以包含更高级 ML 洞察并不得到支持。尽管为 TensorFlow 模型提供基本的服务和模型分析功能,但这种推断解决方案并未满足您更高级的服务需求。

模型监控

TFServing 通过其与 Prometheus 的集成支持传统监控。这公开了系统信息(如 CPU、内存和网络)以及 TFServing 特定的指标;不幸的是,有关文档极为匮乏(请参阅最佳来源,位于TensorFlow 网站)。此外,它与像 Kibana 这样的数据可视化工具或像 Jaeger 这样的分布式跟踪库的一流集成并不存在。因此,TFServing 未提供您期望的托管网络可观察性能力。

关于包括模型漂移和可解释性在内的高级模型服务洞察,某些内容在TensorFlow 2.0 中可用。此外,对专有服务解决方案的供应商锁定使得模型洞察组件的可插拔性变得复杂。由于 TFServing 的部署策略使用 Kubeflow 的基础设施堆栈,它采用了微服务方法。这使得 TFServing 部署可以轻松地与辅助 ML 组件耦合。

模型更新

TFServing 在使能够金丝雀、固定和甚至回滚部署策略方面相当先进。⁷ 然而,这些策略仅限于对现有模型版本的手动标记,并不包括对在飞行中模型版本的支持。因此,版本推广并不具有安全推出的保证。最后,这些策略嵌入在服务器中,不支持其他可能存在于 TFServing 之外的部署策略的可扩展性。

总结

TFServing 为 TensorFlow 模型提供了极其高效和复杂的即插即用集成,但在启用更高级功能(如框架可扩展性、高级遥测和可插入式部署策略)方面存在不足。鉴于这些要求尚未得到满足,我们现在将看看 Seldon Core 如何填补这些空白。

Seldon Core

Seldon Core 不仅仅可以在端点后面提供单个模型,还可以将数据科学家的机器学习代码或工件转换为微服务,从而组成复杂的运行时推断图。如在图 8-2 中可视化的推断图可以由以下组件组成:

模型

为一个或多个 ML 模型运行推断可执行文件

路由器

将请求路由到子图,例如启用 A/B 测试或多臂赌博机

组合器

合并来自子图的响应,例如模型集成

转换器

转换请求或响应,即转换特征请求

Seldon 推断图

图 8-2. Seldon 推断图示例

要理解 Seldon 如何实现这一点,我们将探讨其核心组件和功能集:

预打包模型服务器

优化的 Docker 容器,适用于流行库如 TensorFlow、XGBoost、H2O 等,可以加载和提供模型工件/二进制文件

语言包装器

工具可以使用一组 CLI 实现更多定制的机器学习模型包装,允许数据科学家将 Python 文件或 Java JAR 文件转换为成熟的微服务

标准化 API

可以是 REST 或 gRPC 的开箱即用的 API

开箱即用的可观察性

监控指标和可审计的请求日志

高级机器学习洞察

将复杂的 ML 概念(如解释器、异常检测器和对抗攻击检测器)抽象为基础组件,可以在需要时进行扩展。

使用所有这些组件,我们将详细介绍如何使用 Seldon 设计推断图。

设计 Seldon 推断图

首先,您需要决定推断图包含哪些组件。它只是一个模型服务器,还是会向模型服务器添加一组转换器、解释器或异常检测器?幸运的是,根据需要添加或移除组件非常容易,所以我们将从一个简单的模型服务器开始。

其次,您需要将处理步骤容器化。您可以使用模型作为数据模型作为代码构建推断图中的每个步骤。对于模型作为数据,您可以使用预打包模型服务器加载您的模型工件/二进制文件,并避免在模型更改时每次构建 Docker 容器。对于模型作为代码,您将根据自定义实现构建自己的预打包模型服务器。您的实现通过语言包装器实现,通过暴露高级接口到模型逻辑来容器化您的代码。这可以用于更复杂的情况,甚至是可能需要定制的特定操作系统或外部系统依赖的用例。

接下来,您需要测试您的实现。您可以在本地运行您的实现,利用 Seldon 工具验证其正确性。本地开发得益于 Kubernetes 的可移植性和 Seldon 与 Kubeflow 基础设施堆栈的兼容性。

接下来,您可以启用 Seldon Core 扩展。一些扩展包括:Jaeger 跟踪集成、ELK 请求日志集成、Seldon Core 分析集成或 Istio/Ambassador 入口集成等⁸。

启用扩展后,您可以将本地图部署推广为针对活动 Kubernetes 集群托管。

最后,您可以将推断图连接到持续集成/持续交付(CI/CD)流水线中。Seldon 组件允许您无缝集成到 CI/CD 工作流程中,从而可以使用您喜欢的 CI 工具将模型源连接到 Seldon Core。

现在您已经确定了一个相当强大的推断图,我们将在设置好 Seldon 在您的 Kubeflow 集群之后,通过一些示例进行演示。

设置 Seldon Core

Seldon Core 1.0 预先打包了 Kubeflow,因此它应该已经对您可用。Seldon Core 安装将创建一个 Kubernetes 操作员,该操作员将监视描述推断图的 SeldonDeployment 资源。但是,您可以按照安装说明安装自定义版本的 Seldon Core,如 示例 8-5 所示。

示例 8-5. 自定义 Seldon Core 版本的 Helm 安装
helm install seldon-core-operator \
    --repo https://storage.googleapis.com/seldon-charts  \
    --namespace default \
    --set istio.gateway=istio-system/seldon-gateway \
    --set istio.enabled=true

您必须确保用于提供模型的命名空间具有 Istio 网关和 InferenceServing 命名空间标签。示例标签应用如下:

kubectl label namespace kubeflow serving.kubeflow.org/inferenceservice=enabled

在 示例 8-6 中显示了一个 Istio 网关示例。

示例 8-6. Seldon Core Istio 网关
kind: Gateway
metadata:
  name: seldon-gateway
  namespace: istio-system
spec:
  selector:
    istio: ingressgateway
  servers:
  - hosts:
    - '*'
    port:
      name: http
      number: 80
      protocol: HTTP

您应该将 示例 8-6 保存到文件中,并使用 kubectl 应用它。

打包您的模型

如前所述,要使用 Seldon Core 运行模型,您可以使用预打包的模型服务器⁹ 或语言包装器¹⁰ 打包它。

创建一个 SeldonDeployment

打包您的模型后,需要定义一个推断图,将一组模型组件连接成一个单一的推断系统。模型组件可以是以下两种选项之一,详见 “打包您的模型”。

一些示例图在 图 8-3 中展示。

Seldon 推断图示例

图 8-3. Seldon 图示例

以下列表详细说明了示例推断图 (a) 到 (e),如图 8-3 图 所示:

  • (a) 单个模型

  • (b) 两个模型按顺序排列。第一个模型的输出将被馈送到第二个模型的输入。

  • (c) 具有输入和输出转换器的模型:先调用输入转换器,然后通过模型和响应进行输出转换

  • (d) 一个路由器,将决定是将数据发送到模型 A 还是模型 B

  • (e) 一个组合器,将模型 A 和 B 的响应合并为单个响应¹¹

此外,SeldonDeployment 可以为每个组件指定方法。当您的 SeldonDeployment 部署时,Seldon Core 将添加一个服务协调器来管理请求和响应流通过您的图。

一个示例 SeldonDeployment,用于推理图(a)在 Figure 8-3 中,出现在 Example 8-7 中,作为预打包模型服务器的示例。

示例 8-7. 简单的 Seldon Core 预打包模型服务器
apiVersion: machinelearning.seldon.io/v1
kind: SeldonDeployment
metadata:
 name: seldon-model
spec:
 name: test-deployment
 predictors:
 - componentSpecs:
   graph:
     name: classifier
     type: SKLEARN_SERVER
     modelUri: gs://seldon-models/sklearn/income/model
     children: []
   name: example
   replicas: 1

在示例中,您可以看到 SeldonDeployment 拥有一个 predictors 列表,每个描述一个推理图。每个预测器都有一些核心字段:

componentSpecs

一个 Kubernetes PodSpecs 列表,每个将用于 Kubernetes 部署。

推理图

包含推理图表示,其中包含每个组件的名称、类型以及它遵循的协议。每个名称必须与 componentSpecs 部分的一个容器名称匹配,除非它是预打包模型服务器(参见后续示例)。

名称

预测器的名称。

副本

每个预测器部署中要创建的副本数量。

类型

它是预打包模型服务器还是自定义语言包装器模型的详细信息。

modelUri

存储模型二进制文件或权重的 URL,这对于相应的预打包模型服务器很重要。

另一个示例是 SeldonDeployment,用于示例 Example 8-8,在本例中使用了自定义语言包装器模型。

示例 8-8. 简单的 Seldon Core 自定义语言包装器
apiVersion: machinelearning.seldon.io/v1
kind: SeldonDeployment
metadata:
 name: seldon-model
spec:
 name: test-deployment
 predictors:
 - componentSpecs:
   - spec:
       containers:
       - image: seldonio/mock_classifier_rest:1.3
         name: classifier
   graph:
     children: []
     endpoint:
       type: REST
     name: classifier
     type: MODEL
   name: example
   replicas: 1

在这个示例中,您有一小组新的章节:

容器

这是您的 Kubernetes 容器定义,在这里您可以提供有关容器详细信息的覆盖,以及您的 Docker 镜像和标签。

端点

在这种情况下,您可以指定模型端点是 REST 还是 gRPC。

您的推理图定义现已完成。我们现在将讨论如何在集群上单独或联合地测试您的组件。

测试您的模型

为了测试您的组件,您必须使用某些请求输入与每个接口。您可以直接使用 curlgrpcurl 或类似的实用程序发送请求,也可以使用 Python 的 SeldonClient SDK。

在部署之前,有几种测试模型的选项。

直接使用 Python 客户端运行您的模型

这允许在集群外进行简单的本地测试。

将模型作为 Docker 容器运行

这可以用于所有语言包装器,但不适用于预打包推理服务器,以测试您的图像是否具有所需的依赖项并表现如您所期望的那样。

在 Kubernetes 开发客户端(如 KIND)中运行您的 SeldonDeployment 示例。

这可用于任何模型,并且是确保您的模型将按预期运行的最终测试的一部分。

Python 客户端用于 Python 语言封装模型

您可以在名为 MyModel.py 的文件中定义您的 Python 模型,如 示例 8-9 所示。

示例 8-9. Seldon Core Python 模型类
class MyModel:
    def __init__(self):
      pass
    def predict(*args, **kwargs):
      return ["hello, "world"]

您可以通过运行由 Python 模块 提供的 microservice CLI 来测试您的模型。一旦安装了 Python 的 seldon-core 模块,您就可以使用以下命令运行模型:

> seldon-core-microservice MyModel REST --service-type MODEL
...
2020-03-23 16:59:17,366 - werkzeug:_log:122
- INFO: * Running on http://0.0.0.0:5000/
(Press CTRL+C to quit)

现在您的模型微服务正在运行,您可以使用 curl 发送请求,如 示例 8-10 所示。

示例 8-10. 向您的 Seldon Core 自定义微服务发送请求
> curl -X POST \
> 	-H 'Content-Type: application/json' \
> 	-d '{"data": { "ndarray": [[1,2,3,4]]}}' \
>     	http://localhost:5000/api/v1.0/predictions
{"data":{"names":[],"ndarray":["hello","world"]},"meta":{}}

您可以看到模型的输出是通过 API 返回的。¹²

使用 Docker 进行本地测试

如果您使用其他包装器构建语言模型,您可以通过本地 Docker 客户端运行构建的容器。一个用于从源代码构建 Docker 容器的好工具是 S2I。为此,您只需使用 示例 8-11 中的命令运行 Docker 客户端。

示例 8-11. 在本地 Docker 客户端中公开 Seldon Core 微服务
docker run --rm --name mymodel -p 5000:5000 mymodel:0.1

运行该模型并在端口 5000 上导出它,现在您可以使用 curl 发送请求,如 示例 8-12 所示。

示例 8-12. 向本地 Seldon Core 微服务发送请求
> curl -X POST \
> 	-H 'Content-Type: application/json' \
> 	-d '{"data": { "ndarray": [[1,2,3,4]]}}' \
>     	http://localhost:5000/api/v1.0/predictions

{"data":{"names":[],"ndarray":["hello","world"]},"meta":{}}

使用这个环境,您可以快速原型设计并有效地测试,然后在实时集群中提供您的模型。

提供请求

Seldon Core 支持两个入口网关,Istio 和 Ambassador。由于 Kubeflow 的安装使用 Istio,我们将重点介绍 Seldon Core 如何与 Istio Ingress Gateway 配合工作。我们假设 Istio 网关位于 *<istioGateway>*,在命名空间 *<namespace>* 中具有 SeldonDeployment 名称 *<deploymentName>*。这意味着将在以下 REST 端点公开:

http://<istioGateway>/seldon/<namespace>/<deploymentName>/api/v1.0/predictions.

将在 *<istioGateway>* 公开一个 gRPC 端点,并且您应该在请求中发送头部元数据:

  • seldon 和值 *<deploymentName>*

  • namespace 和值 *<namespace>*

这些请求的载荷将是一个 SeldonMessage。¹³

例如,一个简单的 ndarray 表示的 SeldonMessage,如 示例 8-13 所示。

示例 8-13. SeldonMessage 包含一个 ndarray
{
   "data": {
   "ndarray":[[1.0, 2.0, 5.0]]
   }
}

负载还可以包括简单的张量、TFTensors,以及二进制、字符串或 JSON 数据。一个包含 JSON 数据的示例请求如 示例 8-14 所示。

示例 8-14. SeldonMessage 包含 JSON 数据
{
   "jsonData": {
     "field1": "some text",
     "field2": 3
   }
}

现在您的推理图已经定义、测试并运行,您将希望从中获取预测,并且可能还想在生产环境中监控它,以确保它按预期运行。

监控您的模型

在 Seldon Core 的设计中,部署机器学习模型的方式与部署传统应用程序的方式并无二致。一旦部署完成,监控和治理也是一样的处理方式。通过在 Grafana 中暴露 Prometheus 指标提供传统应用程序监控指标,如请求延迟、负载和状态码分布。¹⁴

然而,作为数据科学家,我们更感兴趣的是模型的表现如何——来自实时数据和模型训练数据的关系,以及特定预测背后的原因。

为了解决这些问题,Seldon Core 提供了额外的开源项目 Alibi:ExplainAlibi:Detect,这两个项目专注于高级机器学习洞察。这两个项目分别实现了模型可解释性、异常检测、数据漂移和对抗攻击检测的核心算法。现在,我们将通过其集成 Alibi:Explain 和 Alibi:Detect 的示例来详细介绍 Seldon Core 如何实现模型可解释性和漂移检测。

模型可解释性

模型可解释性算法旨在回答:“为什么我的模型在这个实例上做出这个预测?”答案可以有多种形式,例如对模型预测贡献最重要的特征或导致不同预测所需的特征最小变化。

可解释性算法还因其对底层模型的访问程度而有所不同。在光谱的一端是“黑盒”算法,它们只能访问模型预测端点,而不能访问其他内容。相比之下,“白盒”算法具有对内部模型架构的全面访问权限,并允许进行更深入的洞察(例如取梯度)。然而,在生产场景中,“黑盒”案例更为突出,因此我们将在此处重点讨论这一点。

在讨论示例之前,我们将描述黑盒解释算法的集成模式。这些算法通常通过生成许多与待解释实例相似的实例,并向模型发送批处理和顺序请求来描绘出模型在原始实例周围做出决策的过程。因此,在计算解释时,解释器组件将与底层模型进行通信。Figure 8-4 展示了该模式的实现方式。一个配置为 SeldonDeployment 的模型与一个解释器组件并排存在,解释器组件有其自己的端点。当内部调用解释器端点时,解释器将与模型通信以生成解释。

Seldon 解释器组件

Figure 8-4. Seldon 解释器组件
警告

在图 8-4 中,解释器直接与生产模型通信。然而,在更现实的情况下,底层模型将是一个单独但相同的部署(即在分段),以确保对解释器的调用不会降低生产推断系统的性能。

为了说明这些技术,我们将展示几个示例。

情感预测模型

我们的第一个例子是一个训练有关康奈尔大学主办的电影评论数据的情感预测模型。您可以像在示例 8-15 中使用 SeldonDeployment 同样配置一个相关的解释器来启动这个模型。

示例 8-15. 带有锚解释器的 SeldonDeployment
apiVersion: machinelearning.seldon.io/v1
kind: SeldonDeployment
metadata:
  name: movie
spec:
  name: movie
  annotations:
    seldon.io/rest-timeout: "10000"
  predictors:
  - graph:
      children: []
      implementation: SKLEARN_SERVER
      modelUri: gs://seldon-models/sklearn/moviesentiment
      name: classifier
    explainer:
      type: AnchorText
    name: default
    replicas: 1

一旦部署,可以像在示例 8-16 中一样通过 Istio 入口查询该模型。然后,您可以将简单的评论“这部电影有很棒的演员”发送给模型。

示例 8-16. 向您的 Seldon Core 电影情感模型发送预测请求
curl -d '{"data": {"ndarray":["This film has great actors"]}}' \
   -X POST http://<istio-ingress>/seldon/seldon/movie/api/v1.0/predictions \
   -H "Content-Type: application/json"

在示例 8-16 中对预测请求的响应在示例 8-17 中可见。

示例 8-17. 来自您的 Seldon Core 电影情感模型的预测响应
{
  "data": {
    "names": ["t:0","t:1"],
    "ndarray": [[0.21266916924914636,0.7873308307508536]]
  },
  "meta": {}
}

该模型是一个分类器,并且以 78%的准确率预测这是一个正面评价,这是正确的。现在,您可以尝试解释请求,就像在示例 8-18 中所见。

示例 8-18. 向您的 Seldon Core 电影情感模型发送解释请求
curl -d '{"data": {"ndarray":["This movie has great actors"]}}' \
   -X POST http://<istio-ingress>/seldon/seldon/movie/explainer/api/v1.0/explain \
   -H "Content-Type: application/json"

在示例 8-18 中解释请求的响应在示例 8-19 中可见(截断示例部分)。

示例 8-19. 来自您的 Seldon Core 电影情感模型的解释响应
{
  "names": [
    "great"
  ],
  "precision": 1,
  "coverage": 0.5007,
  ...
  "instance": "This movie has great actors",
  "prediction": 1
  },
  "meta": {
    "name": "AnchorText"
  }
}

该示例的关键要素在于解释器已经确定了词语great作为模型预测积极情感的原因,并建议如果句子包含词语great,这种情况将在这个模型中始终发生(由精度值反映)。

美国人口普查收入预测模型示例

这是第二个例子,训练于1996 年美国人口普查数据,预测一个人是否有高收入或低收入。¹⁵ 对于这个例子,您还需要有一个 Alibi 解释器对输入数据集进行采样并识别分类特征,以使解释器能够提供更直观的结果。有关配置 Alibi 解释器的详细信息可以在Alibi 文档中找到,以及对以下数据科学示例的深入审查。

SeldonDeployment 资源在示例 8-20 中定义。

示例 8-20. 用于收入预测的 SeldonDeployment
apiVersion: machinelearning.seldon.io/v1
kind: SeldonDeployment
metadata:
  name: income
spec:
  name: income
  annotations:
    seldon.io/rest-timeout: "100000"
  predictors:
  - graph:
      children: []
      implementation: SKLEARN_SERVER
      modelUri: gs://seldon-models/sklearn/income/model
      name: classifier
    explainer:
      type: AnchorTabular
      modelUri: gs://seldon-models/sklearn/income/explainer
    name: default
    replicas: 1

部署后,您可以使用 curl 请求来请求预测,见示例 8-21。

Example 8-21. 发送预测请求到您的 Seldon Core 收入预测模型
curl -d '{"data": {"ndarray":[[39, 7, 1, 1, 1, 1, 4, 1, 2174, 0, 40, 9]]}}' \
   -X POST http://<istio-ingress>/seldon/seldon/income/api/v1.0/predictions \
   -H "Content-Type: application/json"

对示例 8-21 中的预测请求的响应见于示例 8-22。

Example 8-22. 来自您的 Seldon Core 收入预测模型的预测响应
{
    "data": {
      "names":["t:0","t:1"],
      "ndarray":[[1.0,0.0]]
     },
     "meta":{}
 }

该模型预测此人的收入较低。您现在可以通过示例 8-23 请求该预测的解释。

Example 8-23. 发送解释请求到您的 Seldon Core 收入预测模型
curl -d '{"data": {"ndarray":[[39, 7, 1, 1, 1, 1, 4, 1, 2174, 0, 40, 9]]}}' \
   -X POST http://<istio-ingress>/seldon/seldon/income/explainer/api/v1.0/explain \
   -H "Content-Type: application/json"

对示例 8-23 中的解释请求的响应见于示例 8-24,我们已经缩短了不显示所有返回示例。

Example 8-24. 来自您的 Seldon Core 收入预测模型的解释响应
{
  "names": [
    "Marital Status = Never-Married",
    "Occupation = Admin",
    "Relationship = Not-in-family"
  ],
  "precision": 0.9766081871345029,
  "coverage": 0.022,
  ...
}

关键要点是,如果输入特征为"婚姻状况 = 从未结过婚""职业 = 管理员""关系 = 非家庭关系",该模型将在 97%的时间内预测低收入分类。因此,这些是影响模型的输入关键特征。

异常值和漂移检测

机器学习模型在训练数据分布之外通常无法很好地外推,这会影响模型漂移。为了信任并可靠地应用模型预测,您必须通过不同类型的检测器监控传入请求的分布。异常检测器旨在标记不符合原始训练分布的单个实例。对抗检测器试图识别并纠正精心设计的攻击,以欺骗模型。漂移检测器检查传入请求的分布何时与参考分布(例如训练数据的分布)偏离。

如果发生数据漂移,模型性能可能会下降,应该重新训练。在现实应用中使用之前,应验证我们看到的任何检测器标记的实例的机器学习模型预测。检测器通常在实例甚至特征级别返回异常分数。如果分数超过预定义的阈值,则标记该实例。

异常值和漂移检测通常是异步进行实际预测请求的。在 Seldon Core 中,您可以激活有效负载日志记录,并将请求发送到外部服务,该服务将在主要请求/响应流程之外进行异常值和漂移检测。在图 8-5 中展示了一个示例架构,其中 Seldon Core 的有效负载记录器将请求传递给处理它们的组件,这些组件以异步方式处理和警报。处理和警报的组件通过 Knative Eventing 进行管理,Knative Eventing 在“Knative Eventing”中有描述。此处使用 Knative Eventing 是为了提供后绑定事件源和事件消费者,实现异步处理。结果可以传递给警报系统。

使用 Seldon Core + Knative 对模型进行数据科学监控

图 8-5. 使用 Seldon Core 和 Knative 进行数据科学模型监控
注意

以下是一些利用图 8-5 架构进行异常值和漂移检测的示例:

回顾

在构建推理图并希望同时实现模型服务、监视和更新保证时,选择 Seldon Core 作为推理解决方案是一个稳固的选择。它在填补 TFServing 的大多数空白同时,允许数据科学家根据其用例变得更复杂而有机地扩展其推理图。此外,它还允许许多超出本概述范围的功能,如CanariesShadows和强大的多阶段推理管道。¹⁶

然而,我们将看看它如何满足您的推理要求。

模型服务

Seldon Core 显然提供了扩展推理图和支持高级机器学习洞察力的功能。其架构也足够灵活,可以利用其托管外的其他高级机器学习洞察力。并且 Seldon Core 非常灵活,提供了预期的服务灵活性,因为它与框架无关。它支持 REST 和 gRPC,并且支持 GPU 加速。它还可以使用 Knative Eventing 接口流式输入。然而,由于 SeldonDeployment 作为裸 Kubernetes 部署运行,它不提供我们期望的硬件无关自动缩放功能。

模型监控

Seldon Core 似乎满足了您所有的模型监控需求。Seldon Core 的部署策略还使用 Kubeflow 的基础设施堆栈,因此它采用了微服务方法。这在 Seldon Core 将解释器和检测器表示为灵活推理图中的单独微服务时尤为明显。Seldon Core 通过支持 Prometheus 和Zipkin使监控成为一流,启用了监控、日志记录和跟踪。

模型更新

Seldon Core 在支持包括金丝雀发布、固定版本和甚至多臂老丨虎丨机在内的多种部署策略方面非常先进。然而,与 TFServing 类似,版本或版本管理并没有以一流的方式管理。这意味着版本推广没有安全的发布保证。最后,正如您可以通过 Figure 8-3 中提供的图推断选项看到的那样,Seldon Core 在增长推断图以支持更复杂的部署策略方面提供了完全的灵活性。

总结

Seldon Core 通过提供可扩展性和复杂推理图支持的即插即用功能来填补空白。但是,它在 GPU 的自动扩展、零缩放能力以及安全模型更新的版本管理方面存在不足,这些功能对于无服务器应用程序是常见的。我们现在将探讨 KFServing 如何通过增加一些最近的 Kubernetes 增强功能(由 Knative 提供),为 TFServing、Seldon Core 以及许多其他服务解决方案实现无服务器工作流程,来填补这一空白。

KFServing

如同在 TFServing 和 Seldon Core 中所见,ML 模型的生产级服务并不是任何一个研究团队或公司的独特问题。不幸的是,这意味着每个内部解决方案将使用不同的模型格式,并公开独特的专有服务 API。TFServing 和 Seldon Core 面临的另一个问题是缺乏像版本管理和更复杂的自动缩放形式等无服务器基元。这些缺陷也存在于大多数推理服务中。为了统一模型服务器的开源社区,同时填补每个模型服务器存在的空白,Seldon、Google、Bloomberg 和 IBM 参与开源社区的协作开发 KFServing。

KFServing 是一个无服务器推断解决方案,为常见的 ML 框架(如 TensorFlow、XGBoost、Scikit-learn、PyTorch 和 ONNX)提供高性能、高抽象度的接口。通过将 Knative 置于 Kubeflow 的云原生堆栈之上,KFServing 封装了自动扩展、网络、健康检查和服务器配置的复杂性,并引入了 GPU 自动扩展、零缩放以及金丝雀发布等前沿的服务功能到 ML 预测服务中。这使得 ML 工程师能够专注于关键的数据科学相关工具,如预测服务、转换器、可解释性和漂移检测器。

无服务器和服务平面

KFServing 的设计主要借鉴了无服务器 Web 开发。无服务器允许您构建和运行应用程序和服务,而无需规划、扩展或管理任何服务器。这些服务器配置通常称为服务平面或控制平面。

自然而然,无服务器抽象带来了部署简易性和流动性,因为基础设施管理有限。然而,无服务器架构严重依赖基于事件的触发器来扩展其副本,关于这点我们将在“逃生舱口”中讨论。这使你可以专注于你的应用程序代码。

KFServing 的主要宗旨之一是将无服务器应用程序开发扩展到模型服务领域。这对数据科学家尤为有利,因为你希望专注于你正在开发的 ML 模型及其产生的输入和输出层。

数据平面

KFServing 定义了 数据平面,它将所有标准模型服务组件连接在一起,并使用 Knative 为服务平面提供无服务器抽象。数据平面是一个协议,用于指导如何从一个接口转发数据包和请求,同时提供服务发现、健康检查、路由、负载均衡、认证/授权等功能。KFServing 的数据平面架构包括一个组件的静态图,类似于 Seldon Core 的 InferenceGraph,用于协调对单个模型的请求。高级功能如集成、A/B 测试和多臂老丨虎丨机连接这些服务,再次从 Seldon Core 的部署可扩展性中获得灵感。

为了理解数据平面的静态图,让我们回顾一下图 8-6 中使用的一些术语。

KFServing 数据平面

图 8-6. KFServing 数据平面

端点

KFServing 实例分为两个端点:默认金丝雀。这些端点允许用户使用 pinnedcanary 的滚动策略安全地进行更改。金丝雀部署完全是可选的,使用户可以简单地针对默认端点使用蓝绿部署策略。

组件

每个端点都有多个组件:预测器解释器转换器

唯一必需的组件是预测器,它是系统的核心。随着 KFServing 的发展,它可以无缝地增加支持的组件数量,以支持诸如 Seldon Core 的异常检测等用例。如果你愿意,甚至可以引入自己的组件,并利用 Knative 抽象的强大功能将它们连接在一起。

预测器

预测器是 KFServing 实例的工作核心。它只是一个模型和一个模型服务器,可以在网络端点上使用。

解释器

解释器允许可选的替代数据平面,除了预测外还提供模型解释。用户可以定义自己的解释容器,KFServing 将配置相关环境变量,如预测端点。对于常见用例,KFServing 提供了像 Seldon Core 的 Alibi:Explain 这样的开箱即用解释器,我们之前已经了解过。

转换器

变换器允许用户在预测和解释工作流之前定义预处理和后处理步骤。与解释器类似,它配置了相关的环境变量。

数据平面的最后部分是 KFServing 使用的预测协议¹⁷。KFServing 定义了一组必须由符合推理/预测服务实现的 HTTP/REST 和 gRPC API。值得注意的是,KFServing 标准化了这个预测工作流程,详细描述见 表 8-3,跨所有模型框架都适用。

表 8-3. KFServing V1 数据平面

API Verb 路径 负载
可用性 GET

/v1/models/<model_name>

|

{
  Response:{"name":<model_name>,"ready": true/false}
}

|

预测 POST

/v1/models/<model_name>:predict

|

{
  Request:{"instances": []},
  Response:{"predictions": []}
}

|

解释 POST

/v1/models/<model_name>:explain

|

{
  Request:{"instances": []},
  Response:{"predictions": [],"explanations": []}
}

|

示例演示

数据平面定义完成后,我们将通过一个示例演示如何与由 KFServing 提供的模型进行接口交互。

配置 KFServing

KFServing 提供了 InferenceService,这是一个无服务器推理资源,通过提供用于在任意框架上提供 ML 模型服务的 Kubernetes CRD 来描述您的静态图。KFServing 预装于 Kubeflow 中,因此应该已经可用。KFServing 的安装¹⁸ 将在 kubeflow 命名空间中创建一个 Kubernetes 操作器,该操作器将监视 InferenceService 资源。

警告

因为 Kubeflow 的 Kubernetes 最低要求为1.14,不支持对象选择器,Kubeflow 安装时默认启用了 ENABLE_WEBHOOK_NAMESPACE_SELECTOR。如果您使用 Kubeflow 的仪表板或配置控制器创建用户命名空间,标签会自动添加以便 KFServing 部署模型。如果您手动创建命名空间,您需要运行以下命令:

kubectl label namespace \
my-namespace serving.kubeflow.org/inferenceservice=enabled

以允许 KFServing 在命名空间 my-namespace 中部署 InferenceService,例如。

要检查 KFServing 控制器是否正确安装,请运行以下命令:

kubectl get pods -n kubeflow | grep kfserving

您可以通过查看处于 Running 状态的 Pod 来确认控制器是否正在运行。您还可以在 这个 Kubeflow GitHub 站点 找到详细的故障排除指南。

简单性和可扩展性

KFServing 的设计旨在对初学者简单易用,同时对有经验的数据科学家可定制化。这得益于 KFServing 所设计的接口。

现在我们将查看三个 InferenceService 的示例。

示例 8-25 适用于 sklearn

示例 8-25. 简单的 sklearn KFServing 推理服务
apiVersion: "serving.kubeflow.org/v1alpha2"
kind: "InferenceService"
metadata:
  name: "sklearn-iris"
spec:
  default:
    predictor:
      sklearn:
        storageUri: "gs://kfserving-samples/models/sklearn/iris"

示例 8-26 适用于 tensorflow

示例 8-26. 简单的 TensorFlow KFServing 推理服务
apiVersion: "serving.kubeflow.org/v1alpha2"
kind: "InferenceService"
metadata:
  name: "flowers-sample"
spec:
  default:
    predictor:
      tensorflow:
        storageUri: "gs://kfserving-samples/models/tensorflow/flowers"

示例 8-27 适用于 pytorch

示例 8-27. 简单的 PyTorch KFServing 推理服务
apiVersion: "serving.kubeflow.org/v1alpha2"
kind: "InferenceService"
metadata:
  name: "pytorch-cifar10"
spec:
  default:
    predictor:
      pytorch:
        storageUri: "gs://kfserving-samples/models/pytorch/cifar10/"
        modelClassName: "Net"

这些示例每个都将为您提供一个服务实例,具有 HTTP 端点,可以使用请求的框架服务器类型提供模型。在这些示例中,storageUri指向序列化的资产。接口在不同模型之间基本保持一致。区别在于框架规范,如tensorflowpytorch。这些框架规范足够普遍,它们共享像storageUri和 Kubernetes 资源请求这样的信息,但也可以通过启用特定于框架的信息(如 PyTorch 的ModelClassName)进行扩展。

显然,这个接口足够简单,可以很容易地开始使用,但在向更复杂的部署配置和策略迈进时,它的可扩展性如何?示例 8-28 展示了 KFServing 提供的一些功能特性。

Example 8-28. Sophisticated Canary KFServing InferenceService
apiVersion: "serving.kubeflow.org/v1alpha2"
kind: "InferenceService"
metadata:
  name: "my-model"
spec:
  default:
    predictor:
      # 90% of traffic is sent to this model
      tensorflow:
        storageUri: "gs://kfserving-samples/models/tensorflow/flowers"
        serviceAccount: default
        minReplicas: 2
        maxReplicas: 10
        resources:
          requests:
            cpu: 1
            gpu: 1
            memory: 8Gi
  canaryTrafficPercent: 10
  canary:
    predictor:
      # 10% of traffic is sent to this model
      tensorflow:
        storageUri: "gs://kfserving-samples/models/tensorflow/flowers-2"
        serviceAccount: default
        minReplicas: 1
        maxReplicas: 5
        resources:
          requests:
            cpu: 1
            gpu: 1
            memory: 8Gi

第一个扩展是ServiceAccount,用于身份验证,采用托管标识的形式。如果您希望对S3进行身份验证,因为您的S3不应该是公共的,您需要将身份附加到您的InferenceService,以便验证您作为用户的身份。KFServing 允许您通过容器上挂载的身份信息,并通过ServiceAccount以托管方式连接凭据。例如,假设您尝试访问可能存储在Minio上的模型。您将使用您的Minio身份信息事先创建一个密钥,然后将其附加到服务帐户。如果您还记得,我们在 MinIO 中创建了一个密钥在“MinIO”,所以我们只需要包含像示例 8-29 中的 KFServing 相关注释即可。

Example 8-29. KFServing-annotated MinIO secret
apiVersion: v1
data:
 awsAccessKeyID: xxxx
 awsSecretAccessKey: xxxxxxxxx
kind: Secret
metadata:
 annotations:
   serving.kubeflow.org/s3-endpoint: minio-service.kubeflow.svc.cluster.local:9000
   serving.kubeflow.org/s3-verifyssl: "0"
   serving.kubeflow.org/s3-usehttps: "0"
   serving.kubeflow.org/s3-region: us-east-1
 name: minioaccess
 namespace: my-namespace

并将其附加到服务帐户,就像在示例 8-30 中看到的那样。

Example 8-30. Service Account with attached MinIO secret
apiVersion: v1
kind: ServiceAccount
metadata:
  name: default
  namespace: my-namespace
secrets:
- name: default-token-rand6
- name: minioaccess

第二个要注意的扩展是最小和最大副本数。您可以使用这些来控制配额,以便满足需求,既不会丢弃请求,也不会过度分配。

第三个扩展是资源请求,它们具有预设的默认值,几乎总是需要根据您的模型进行定制。正如您所见,此接口支持使用硬件加速器,如 GPU。

最后一个扩展展示了 KFServing 用于启用金丝雀部署的机制。这种部署策略假定您只想专注于双向流量分割,而不是n路流量分割。为了定制您的部署策略,请执行以下操作:

  • 如果您只使用默认设置,就像在您的初始模板中一样,您将获得一个带有 Kubernetes 部署资源的标准blue-green部署。

  • 如果您包含一个金丝雀版本,其中 canaryTrafficPercent == 0,您将获得一个固定部署,其中您有一个可寻址的 defaultcanary 终端。如果您希望将实验流量发送到新的终端,同时保持生产流量指向旧的终端,这将非常有用。

  • 如果您包含金丝雀,其中 canaryTrafficPercent > 0,您将获得一个金丝雀部署,可以透明地逐步增加到金丝雀部署的流量。在前面的示例中,您正在尝试 flowers-2,并且随着您逐渐增加这个 canaryTrafficPercentage,您可以确信您的新模型不会破坏当前用户。¹⁹ 最终,您将到达 100,从而翻转金丝雀和默认值,然后您应该删除旧版本。

现在我们了解了一些 KFServing 提供的强大抽象功能,让我们使用 KFServing 来托管您的产品推荐示例。

推荐示例

我们现在将您的产品推荐示例从 “使用 TensorFlow 构建推荐系统” 放在一个 InferenceService 后面。

警告

因为 kubeflow 命名空间是一个系统命名空间,您无法在 kubeflow 命名空间中创建 InferenceService。因此,您必须在另一个命名空间中部署您的 InferenceService

首先,您将使用以下 11 行 YAML 定义您的 InferenceService,如 示例 8-31 所示。

示例 8-31. KFServing 推荐器 InferenceService
apiVersion: "serving.kubeflow.org/v1alpha2"
kind: "InferenceService"
metadata:
 name: "recommender"
 namespace: my-namespace
spec:
 default:
   predictor:
     tensorflow:
       serviceAccount: default
       storageUri: "s3://models/recommender"

运行 kubectl apply 并等待直到您的 InferenceService 处于 Ready 状态后,您应该看到:

$ kubectl get inferenceservices -n my-namespace
NAME        URL                                                               READY DEFAULT
recommender http://recommender.my-namespace.example.com/v1/models/recommender True  100

您可以像在 示例 8-32 中那样 curl 您的 InferenceService

示例 8-32. 向您的 KFServing 推荐器 InferenceService 发送预测请求
kubectl port-forward --namespace istio-system \
 $(kubectl get pod --namespace istio-system \
 --selector="app=istio-ingressgateway" \
 --output jsonpath='{.items[0].metadata.name}') \
 8080:80

curl -v -H "Host: recommender.my-namespace.example.com" \
http://localhost:8080/v1/models/recommender:predict -d \
'{"signature_name":"serving_default",
 "inputs": {"products": [[1],[2]],"users" : [[25], [3]]}}'
警告

如果您的 curl 返回 404 Not Found 错误,则这是 Kubeflow 1.0.x 中存在的已知 Istio 网关问题。我们建议您使用 Kubeflow 1.1 或更高版本。此问题的可能解决方法在此 GitHub 问题 中描述。

作为 curl 的替代方法,您还可以使用 KFServing 的 PythonSDK 在 Python 中发送请求。²⁰ 除了 HTTP 终端外,这个简单的接口还提供了与 Kubeflow 堆栈和 Knative 一起提供的所有无服务器功能,其中包括:

  • 缩放到零

  • GPU 自动缩放

  • 修订管理(安全发布)

  • 优化的容器

  • 网络策略和身份验证

  • 追踪

  • 指标

因此,只需几行 YAML,KFServing 就提供了生产 ML 功能,同时允许数据科学家将其部署扩展到未来。但是,KFServing 如何以这种抽象的方式实现这些功能呢?

现在我们将看看 KFServing 的基础设施堆栈,了解它如何推广无服务器,其层如何进一步定制,以及存在的其他功能。

揭开底层基础设施

通过解剖其基础设施堆栈,您可以看到 KFServing 如何在实现无服务器 ML 的同时,还能教育您如何调试推理解决方案。KFServing 以云原生方式构建,就像 Kubeflow 一样。它受益于其下每一层的特性。如图 8-7 所示,KFServing 建立在与 Kubeflow 相同的堆栈上,但是它是少数几个强烈利用 IstioKnative 功能的 Kubeflow 解决方案之一。

我们现在将详细讨论每个组件的角色,比我们在前几章节中所做的更详细,看看 KFServing 利用了这些层次的哪些部分。

KFServing 基础设施堆栈

图 8-7. KFServing 基础设施堆栈

逐层分析

运行计算集群的硬件是所有上层堆栈的基本构建块。您的集群可以运行各种硬件设备,包括 CPU、GPU 或甚至 TPU。上层层次的责任是简化硬件类型的切换,并尽可能地抽象复杂性。

Kubernetes 是关键层,位于计算集群之上,管理、编排和部署各种资源,成功地将底层硬件抽象化。我们将重点关注的主要资源是部署、水平 Pod 自动缩放器(HPA)和入口。由于 Kubernetes 抽象了底层硬件,使得您可以在堆栈的上层使用 GPU 等硬件优化器。

在本书中,一直提到 Istio,但我们将讨论几个特别与 KFServing 相关的功能。Istio 是一个开源服务网格,可透明地层叠在 Kubernetes 集群之上。它集成到任何日志平台、遥测系统或策略系统中,并促进了一种统一的方式来安全地连接和监视微服务。但服务网格是什么呢?传统上,每个服务实例与一个旁路网络代理共同部署。所有来自单个服务实例的网络流量(HTTPRESTgRPC 等)都通过其本地旁路代理流向适当的目标。因此,服务实例不知道整个网络,只知道其本地代理。实际上,分布式系统网络已从服务程序员的视角抽象出来。主要上,Istio 扩展了 Kubernetes 资源,如入口,以提供服务网格基础设施,例如:

  • 认证/访问控制

  • 入口和出口策略管理

  • 分布式跟踪

  • 通过多集群入口和路由进行联合管理

  • 智能流量管理

这些工具对于需要管理、安全和监控的生产推理应用程序至关重要。

KFServing 基础架构堆栈的最后一个组件是 Knative,它利用 Istio 提供的抽象。KFServing 项目主要借鉴了 Knative Serving 和 Eventing,后者将在“Knative Eventing”中进一步扩展。正如我们在“Knative”中描述的那样,Knative Serving 建立在 Kubernetes 和 Istio 之上,以支持部署和提供无服务器应用程序。通过构建在 Kubernetes 资源(如部署和 HPA)和 Istio 资源(如虚拟服务)之上,Knative Serving 提供了支持。

  • 一个抽象的服务网格

  • CPU/GPU 自动缩放(每秒查询(QPS)或基于度量)

  • 安全发布的修订管理和金丝雀/固定部署策略

这些服务对希望将精力和精力限制在模型开发上,并在托管方式下处理扩展和版本控制的数据科学家非常有吸引力。

逃生舱

KFServing 的可扩展性功能可以逃离其堆栈的底层。通过将逃生舱口内置到InferenceService CRD 中,数据科学家可以进一步调整其生产推理服务,以便在 Istio 层面进行安全性和在 Knative 层面进行性能优化。

现在我们将通过一个示例来演示如何利用这些应急通道,通过调整你的推理服务的自动缩放。

要了解如何使用这个应急通道,您需要了解 Knative 如何启用自动缩放。在 Knative Serving Pods 中有一个名为队列代理的代理,负责执行请求队列参数(并发限制),并向自动缩放器报告并发客户端指标。自动缩放器反过来通过增加和减少 Pod 来对这些指标做出反应。每秒钟,队列代理发布该时间段内观察到的并发请求数。KFServing 默认将目标并发(每个 Pod 的平均正在处理的请求数)设置为一。如果我们将服务负载与五个并发请求,自动缩放器将尝试扩展到五个 Pod。您可以通过添加示例注释autoscaling.knative.dev/target来自定义目标并发。

让我们再次看看您的推理服务,来自示例 8-31。

apiVersion: "serving.kubeflow.org/v1alpha2"
kind: "InferenceService"
metadata:
 name: "recommender"
 namespace: my-namespace
spec:
 default:
   predictor:
     tensorflow:
       serviceAccount: default
       storageUri: "s3://models/recommender"

如果您通过每 30 秒发送的流量测试此服务,并保持 5 个正在处理的请求,您将看到自动缩放器将您的推理服务扩展到 5 个 Pod。²¹

注意

由于最初生成 Pod 并下载模型,再准备提供服务,将会产生冷启动时间成本。如果图像未缓存在调度 Pod 的节点上,冷启动可能需要更长时间(拉取服务图像)。

通过应用注释autoscaling.knative.dev/target,如在示例 8-33 中所见,目标并发将设置为五。

示例 8-33. 通过注解在 KFServing 推理服务中设置自定义目标并发量
apiVersion: "serving.kubeflow.org/v1alpha2"
kind: "InferenceService"
metadata:
 name: "recommender"
 namespace: my-namespace
 annotations:
   autoscaling.knative.dev/target: "5"
spec:
 default:
   predictor:
     tensorflow:
       serviceAccount: default
       storageUri: "s3://models/recommender"

如果你用五个并发请求加载服务,你会发现,你只需要一个 pod 来作为你的推理服务。

调试推理服务

通过完全抽象的接口,InferenceService 在最小化暴露底层复杂性的同时,启用了许多功能。为了正确调试你的推理服务,让我们看一下请求击中你的推理服务时的请求流程。

当请求击中你的推理服务时的请求流程,如图 8-8 所示,如下:

  1. 当流量是外部时,通过 Istio 入口网关到达;当流量是内部时,通过 Istio 集群本地网关到达。

  2. KFServing 创建了一个 Istio VirtualService 来指定其所有组件的顶层路由规则。因此,流量会从网关路由到那个顶层 VirtualService。

  3. Knative 创建了一个 Istio 虚拟服务来配置网关,将用户流量路由到期望的修订版。打开目标规则后,你会看到目标是最新就绪的 Knative 修订版的 Kubernetes 服务。

  4. 一旦修订版的 pod 准备就绪,Kubernetes 服务将请求发送到队列代理。

    • 如果队列代理处理的请求超过其处理能力,基于 KFServing 容器的并发性,那么自动扩展器将创建更多的 pod 来处理额外的请求。
  5. 最后,队列代理将流量发送到 KFServing 控制器。

KFServing 请求流程

图 8-8. KFServing 请求流程

这在哪里派上用场?比如说你创建了你的推理服务,但是Ready状态是false

kubectl get inferenceservice -n my-namespace recommender
NAME          URL   READY   DEFAULT TRAFFIC   CANARY TRAFFIC   AGE
recommender         False                                      3s

你可以逐步查看在请求流程中创建的资源,并查看它们各自的状态对象,以理解阻碍的原因。²²

调试性能

如果你部署了你的推理服务但性能不符合你的期望,KFServing 提供了各种仪表板和工具来帮助调查这些问题。使用 Knative,KFServing 在其“调试性能问题”指南中拥有许多资源。你也可以参考这个 Knative 指南来访问 Prometheus 和 Grafana。最后,你可以使用请求跟踪,也被称为分布式跟踪,查看 KFServing 请求流程的每个步骤花费了多少时间,详见图 8-8。你可以使用这个 Knative 指南来访问请求跟踪。

Knative Eventing

通过将 Knative 整合到其堆栈中,KFServing 通过 Knative Serving 启用了无服务器模式,并通过 Knative Eventing 使用事件源和事件消费者。²³ 我们将看一下 Knative Eventing 的工作原理,以及如何通过事件源扩展您的推理服务。

Knative Eventing 强制执行事件源和事件消费者的 Lambda 风格架构,遵循以下设计原则:

  • Knative Eventing 服务松散耦合。

  • 事件生产者和事件消费者是独立的。任何生产者或源都可以在有活跃事件消费者监听之前生成事件。任何事件消费者都可以在有创建这些事件的生产者之前对事件表达兴趣。

  • 其他服务可以连接到任何事件系统,包括:

    • 创建新的应用程序而不修改事件生成器或事件消费者。

    • 选择并针对其生产者的特定子集事件。

Knative Eventing 提供两种事件交付方式:直接从源到单个服务的直接交付,以及通过通道和订阅从源到多个端点的扇出交付。

在安装 Knative Eventing 时,提供了多种即插即用的源²⁴,其中之一是 KafkaSource²⁵。如果你查看示例 8-34,你将看到如何使用 KafkaSource 发送事件,通过 Kafka 接收到你的推荐器示例。

示例 8-34. KafkaSource 发送事件到 KFServing 推荐器推理服务
apiVersion: sources.knative.dev/v1alpha1
kind: KafkaSource
metadata:
  name: kafka-source
spec:
  consumerGroup: knative-group
  # Broker URL. Replace this with the URLs for your Kafka cluster, which
  # is in the format of my-cluster-kafka-bootstrap.my-kafka-namespace:9092.
  bootstrapServers: my-cluster-kafka-bootstrap.my-kafka-namespace:9092.
  topics: recommender
  sink:
    ref:
      apiVersion: serving.kubeflow.org/v1alpha2
      kind: InferenceService
      name: recommender

正如你所看到的这个规范的简单性,设置好你的 Kafka 资源后,将 Kafka 链接到你的 InferenceService 就像是 13 行 YAML 那样简单。你可以在这个 Kubeflow GitHub 网址找到一个更高级的端到端示例,其中包括 MinIO 和 Kafka。

其他功能

KFServing 包含一系列功能,不断进行改进。你可以在此 GitHub 网址找到其功能的详细列表。

API 文档

要了解更多有关 API 的信息,请参考KFServing Kubernetes APIs 的参考资料和 KFServing Python KFServing Python APIs 的参考资料

回顾

在 Seldon Core 的图推理之上构建无服务器应用,KFServing 提供了一个完整的推理解决方案,充分填补了 TFServing 和 Seldon Core 的所有空白。KFServing 通过将模型服务器作为 Knative 组件运行,致力于统一整个模型服务器社区。通过其所有的功能和承诺,我们将看看 KFServing 如何满足所有你的推理需求。

模型服务

KFServing 使得图推理和先进的机器学习洞察力成为头等大事,同时定义了一个数据平面,极易扩展以插入式组件。这种灵活性使得数据科学家能够专注于机器学习洞察力,而无需为如何将其包含在图中而苦恼。

KFServing 不仅多才多艺,为多种框架提供了灵活的服务,而且还通过规范化数据平面来减少在不同框架之间切换模型服务器的复杂性。它通过将请求批处理、日志记录和管道化等常用功能移动到 Sidecar 中,从而简化了模型服务器,并实现了关注点的分离,没有这些功能的模型服务可以立即从部署到 KFServing 中受益。它还支持RESTgRPC和 GPU 加速,并且可以使用 Knative Eventing 与流输入进行接口连接。最后,得益于 Knative Serving,KFServing 提供了 GPU 自动缩放,这是您从硬件无关自动缩放中所期望的。

模型监控

通过借鉴 Seldon Core 及其基础设施堆栈,KFServing 满足了您的模型监控需求。KFServing 以一流的方式利用了 Seldon Core 的复杂模型解释器和漂移检测器,同时也为开发人员提供了定义其自己的监控组件的高度灵活而强大的数据平面。

此外,通过在基础设施堆栈中使用 Istio 和 Knative 的所有网络功能,KFServing 提供了可扩展的网络监控和支持 Prometheus、Grafana、Kibana、Zipkin 和 Jaeger 等的遥测。这些都满足了您监控 Kubernetes 指标(内存/CPU 容器限制)和服务器指标(每秒查询和分布式跟踪)的需求。

模型更新

KFServing 对 Knative 的使用在提供复杂的模型更新功能方面是策略性的。因此,KFServing 满足了关于部署策略和版本发布的所有要求。

通过利用 Istio 的虚拟服务和抽象的 CRD 的简易性,KFServing 简化了部署策略的切换。只需改变几行 YAML,便可以轻松实现从蓝绿→固定→金丝雀的流程。此外,借助其底层堆栈的多样化和不断扩展的功能,KFServing 很容易扩展以支持更复杂的部署策略,如多臂赌博机。²⁶

通过使用 Knative Serving,KFServing 采用了使 Kubernetes 部署不可变的修订管理。这确保了在将流量切换到新的修订版之前,通过对新修订版的 Pod 进行健康检查来实现安全的部署。修订版可以实现:

  • 自动化和安全的部署

  • 对先前创建的所有修订版本进行簿记

  • 回滚到已知的良好配置

这充分满足了您在开发、飞行和生产中对模型版本的要求。

总结

KFServing 已经开发了一个复杂的推理解决方案,将其复杂性抽象为日常用户的第一天使用,同时也使强大的用户能够利用其多样的功能集。构建云原生,KFServing 无缝地坐落在 Kubeflow 之上,并通过其推理解决方案完成了 MDLC。

结论

在本章中,我们调查了可以在 Kubeflow 中使用的各种推理解决方案。

根据您希望优先考虑的推理需求以及您希望您的基础架构堆栈有多深,描述的每个解决方案都具有独特的优势。在详细审查了每个提供的内容之后,重新考虑表 8-2 可能是值得的,看看哪种推理解决方案适合您的用例:

  • TFServing 为 TensorFlow 模型提供了极其高效和复杂的即插即用集成。

  • Seldon Core 提供了复杂推理图和模型洞察力的可扩展性和复杂的即插即用支持。

  • KFServing 提供了一个更简单且具有无服务器能力的部署定义。

然而,技术和开发在所有这些项目之间是共享的,并展望未来,Seldon Core 将甚至支持新的 KFServing 数据平面,目标是提供易于互操作和转换。KFServing 可期待的其他令人兴奋的功能包括多模型服务、渐进式部署以及更先进的图推理技术,如管道和多臂赌博机。

现在您已经完成了 MDLC 故事的最后一步,我们将看看如何在下一章中进一步定制 Kubeflow 以实现更高级的功能。

¹ 如果您有兴趣了解模型嵌入更多信息,建议阅读服务机器学习模型 by Boris Lublinsky(O'Reilly)。

² 一些参考文献包括:“大声失败:检测数据集漂移的方法的实证研究”“使用黑盒预测器检测和校正标签偏移”“核二样本检验”,以及“生产中模型的监控和可解释性”

³ 有关在本地使用 TFServing 的详细信息,请参阅TensorFlow 文档

⁴ 有关在 Kubernetes 上使用 TFServing 的详细信息,请参阅TensorFlow 文档

⁵ 如果您正在使用 Istio 作为服务网格,请按照这些说明添加虚拟服务

⁶ 您当然可以通过更改部署实例的数量来手动扩展它。

⁷ 有关更多信息,请参阅 TFServing 的部署策略配置

⁸ 有关与PrometheusELKJaeger集成的 Seldon 文档,请参阅此处。

⁹ 目前支持的预打包服务器包括 MLflow 服务器、SKLearn 服务器、TensorFlow 服务和 XGBoost 服务器。

¹⁰ 目前支持 Python 的语言服务器。Java、R、NodeJS 和 Go 正在孵化中。

¹¹ 因为 Seldon 将计算结构实现为树形结构,所以合并器以反向顺序执行,以合并所有子节点的输出。

¹² 您还可以使用 Python 客户端发送请求。

¹³ 可以将 SeldonMessage 定义为OpenAPI 规范协议缓冲定义

¹⁴ 有关如何启用此功能的详细信息,请参阅此Seldon 文档页面

¹⁵ 有关此模型及其构建方式的更多信息,请参阅“使用 Scikit-Learn 训练模型”。

¹⁶ 有关更多详细信息,请参阅Seldon Core 文档

¹⁷ KFServing 不断发展,其协议也在不断演进。您可以在此Kubeflow GitHub 网站上预览 V2 协议。数据平面协议的第二个版本解决了 V1 数据平面协议中发现的几个问题,包括性能和跨大量模型框架和服务器的普适性。

¹⁸ KFServing 还支持独立安装,无需 Kubeflow。实际上,大多数 KFServing 的生产用户都将其作为独立安装运行。

¹⁹ 您仍然可以通过在请求中传递 Host-Header 来针对特定版本进行预测。有关部署的更多信息,请参阅此 GitHub 仓库。

²⁰ 您可以通过运行pip install kfserving来安装 SDK。您可以在此 GitHub 网站上获取 KFServing SDK 文档和示例,用于创建、发布、推广和删除InferenceService

²¹ 您可以在此 Kubeflow GitHub 网站上进一步探索负载测试。两个优秀的负载测试框架是HeyVegeta

²² 可在Kubeflow GitHub 网站找到详细的调试指南。

²³ 想了解更多关于 Knative Eventing 的信息,请查看文档

²⁴ Knative 列出了事件源的非详尽列表

²⁵ 想了解更多关于 KafkaSource 的信息,请查看文档

²⁶ 查看如何使用 ML Graph 构建 ML 组件复杂图形的示例,访问Seldon GitHub 网站

第九章:使用多个工具的案例研究

在本章中,我们将讨论如果您需要使用“其他”工具来处理特定的数据科学流水线时应该怎么做。Python 拥有丰富的工具来处理各种数据格式。RStats 拥有大量高级数学函数的仓库。Scala 是大数据处理引擎(如 Apache Spark 和 Apache Flink)的默认语言。在任何一种语言中都存在成本高昂且难以复制的旧程序。

Kubeflow 的一个非常重要的好处是用户不再需要选择哪种语言最适合他们的整个流水线,而是可以针对每个作业使用最佳语言(只要语言和代码可以容器化)。

我们将通过一个全面的例子演示这些概念,即去噪 CT 扫描。低剂量 CT 扫描允许临床医生通过传递辐射剂量的一小部分来使用扫描作为诊断工具——然而,这些扫描通常受到白噪声增加的影响。CT 扫描以 DICOM 格式呈现,并且我们将使用一个包含名为pydicom的专用库的容器来加载和处理数据为一个numpy矩阵。

存在多种去噪 CT 扫描方法;然而,它们通常侧重于数学理论,而非实际实现。我们将介绍一种开源方法,该方法使用奇异值分解(SVD)将图像分解为组件,其中“最不重要”的部分通常是噪声。我们使用 Apache Spark 和 Apache Mahout 库进行奇异值分解。最后,我们再次使用 Python 对 CT 扫描进行去噪并可视化结果。

去噪 CT 扫描示例

计算机断层扫描(CT 扫描)被用于广泛的医疗目的。这些扫描通过从多个角度获取 X 射线,并形成图像“切片”,然后可以堆叠以创建人体内部的三维图像。在美国,健康专家建议一个人在一生中接受的辐射不超过 100 毫西弗(mSv),相当于约 25 次胸部 CT 扫描(每次约 7 毫西弗)。

在二十世纪末和二十一世纪初,对所谓的“低剂量” CT 扫描进行了大量研究。低剂量胸部 CT 扫描仅释放 1 至 2 毫西弗(mSv)的辐射,但代价是图像更加嘈杂,这可能会增加阅读难度。这些扫描是习惯性吸烟者筛查肺癌的常用工具。

这种低剂量 CT 扫描的成本是生成图像质量较低或更嘈杂。在 2000 年代,对去噪这些低剂量 CT 扫描进行了大量研究。大多数论文仅呈现方法和结果(无代码)。此外,FDA 限制了可用于去噪 CT 扫描的方法,这导致几乎所有解决方案都是专有且昂贵的。去噪旨在通过去除这些低剂量 CT 扫描中经常存在的白噪声来提高图像质量。

在撰写本书时,被广为人知的新型冠状病毒 COVID-19 已经升级为全球大流行。已经证明,胸部 CT 扫描比逆转录聚合酶链式反应(RT-PCR)检测更具敏感性,尤其是在感染的早期阶段。

随着多个 CT 扫描库的上线,并请求 AI 研究人员协助抗击大流行病,我们致力于基于现成的开源组件添加一种去噪 CT 扫描的方法。我们将使用 Python、Apache Spark、Apache Mahout(一个专门用于分布式线性代数的 Spark 库)和 Kubeflow。

我们不会深入讨论这里正在做的数学内容,但我们强烈建议您参考这篇论文。¹

在这个例子中,我们将专注于使用 Kubeflow 执行此技术的“如何”,并鼓励读者在管道的末尾添加自己的步骤,然后可以自由地与其他研究人员分享。

使用 Python 进行数据准备

CT 扫描图像通常以 DICOM 格式存储。在这种格式中,图像的每个“切片”都存储在自己的文件中,同时包含一些关于图像的元数据,例如像素之间的间距和切片之间的间距。我们希望读取所有这些文件并创建一个像素值的 3D 张量。然后,我们希望将该张量“展平”为一个二维矩阵,以便进行奇异值分解。

有几个地方可以获取 DICOM 文件集。在本文中,我们从 https://coronacases.org 获取了一些(尽管下载 DICOM 可能有点棘手)。您可以在其他地方找到 DICOM 文件,例如来自医生的 CT 扫描光盘、以及其他在线地点。² 重要的是,我们需要一个包含单个 CT 扫描所有 DICOM 文件的目录。我们假设在目录 /data/dicom 中存在 某些 DICOM 文件集组成的单个 CT 扫描。

如果你已经准备好了正确的依赖项,将 DICOM 图像转换为张量实际上非常简单。我们将使用 pydicom,这是一个与 DICOM 图像工作的良好支持的 Python 接口。不幸的是,pydicom Docker 镜像不包括 Grassroots DICOM(GDCM),后者是将 DICOM 转换为像素数组所必需的。我们解决这个问题的方法是使用 pydicom Docker 容器作为基础镜像,然后构建一个兼容的 GDCM 版本。我们得到的镜像我们命名为 rawkintrevo/covid-prep-dicom。有了 pydicom 和 GDCM,将 DICOM 图像转换为张量就很容易;我们将使用一个轻量级 Python 函数来完成剩余的工作(参见 示例 9-1)。

示例 9-1. 轻量级 Python 函数将 DICOM 转换为张量
def dicom_to_matrix(input_dir: str, output_file: str) -> output_type:
    import pydicom ![1](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/1.png)
    import numpy as np

    def dicom_to_tensor(path): ![2](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/2.png)
        dicoms = [pydicom.dcmread(f"{path}/{f}") for f in listdir(path)]
        slices = [d for d in dicoms if hasattr(d, "SliceLocation")]
        slices = sorted(slices, key=lambda s: s.SliceLocation)

        img_shape = list(slices[0].pixel_array.shape)
        img_shape.append(len(slices))
        img3d = np.zeros(img_shape)

        for i, s in enumerate(slices):
            img2d = s.pixel_array
            img3d[:, :, i] = img2d

        return {"img3d": img3d, "img_shape": img_shape}

    m = dicom_to_tensor(f"{input_dir}")
    np.savetxt(output_file, m['img3d'].reshape((-1,m['img_shape'][2])), delimiter=",") ![3](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/3.png)
    return None

dicom_to_matrix_op = comp.func_to_container_op(
        dicom_to_matrix,
        base_image='rawkintrevo/covid-prep-dicom:0.8.0.0')

1

我们的导入必须在函数内进行(而不是全局的)。

2

此函数读取“切片”列表,它们本身是 2D 图像,并将它们堆叠成 3D 张量。

3

我们使用numpy将 3D 张量重塑为 2D 矩阵。

接下来,让我们考虑使用 Apache Spark 和 Apache Mahout 对我们的 CT 扫描进行去噪处理。

使用 Apache Spark 进行 DS-SVD

分布式随机奇异值分解(DS-SVD)背后的数学远远超出了本书的范围;然而,我们建议您在Apache Mahout: Beyond MapReduce中,或者在Apache Mahout 网站或前述论文中进一步了解。

我们希望将 CT 扫描分解为一组特征,然后丢弃最不重要的特征,因为这些可能是噪音。因此,让我们使用 Apache Spark 和 Apache Mahout 来分解 CT 扫描。

Apache Mahout 的一个显著特性是其“R-Like”领域特定语言,它使得用 Scala 编写的数学代码易于阅读。在示例 9-2 中,我们将数据加载到 Spark RDD 中,将 RDD 包装在 Mahout 分布式行矩阵(DRM)中,并对矩阵执行 DS-SVD,从而得到三个矩阵,然后我们将它们保存。

示例 9-2. 使用 Spark 和 Mahout 分解 CT 扫描
val pathToMatrix = "gs://covid-dicoms/s.csv" ![1](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/1.png)

val voxelRDD:DrmRdd[Int]  = sc.textFile(pathToMatrix)
  .map(s => dvec( s.split(",")
  .map(f => f.toDouble)))
  .zipWithIndex
  .map(o => (o._2.toInt, o._1))

val voxelDRM = drmWrap(voxelRDD) ![2](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/2.png)

// k, p, q should all be cli parameters // k is rank of the output, e.g., the number of eigenfaces we want out. // p is oversampling parameter, // and q is the number of additional power iterations // Read https://mahout.apache.org/users/dim-reduction/ssvd.html val k = args(0).toInt
val p = args(1).toInt
val q = args(2).toInt

val(drmU, drmV, s) = dssvd(voxelDRM.t, k, p, q) ![3](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/3.png)

val V = drmV.checkpoint().rdd.saveAsTextFile("gs://covid-dicoms/drmV")
val U = drmU.t.checkpoint().rdd.saveAsTextFile("gs://covid-dicoms/drmU")

sc.parallelize(s.toArray,1).saveAsTextFile("gs://covid-dicoms/s") ![4](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/4.png)

1

加载数据。

2

将 RDD 包装在 DRM 中。

3

执行 DS-SVD。

4

保存输出。

所以只需几行 Scala 代码,我们就能执行一个基于外存的奇异值分解。

可视化

在 R 和 Python 中有许多用于可视化的优秀库,我们希望使用其中之一来可视化我们的去噪 DICOM。我们还希望将最终的图像保存到比持久卷容器(PVC)更持久的位置,以便稍后查看我们的图像。

管道的这个阶段将有三个步骤:

  1. 下载由 DS-SVD 生成的 DRM。

  2. 将矩阵重新组合成 DICOM,通过将矩阵s的一些对角线值设为零来去噪。

  3. 视觉化以图形方式呈现去噪 DICOM 的切片。

注意

可视化可以轻松在 R 或 Python 中完成。我们将在 Python 中进行,但使用 R 中的oro.dicom包。我们选择 Python 是因为 Google 正式支持用 Python API 与 Cloud Storage 进行交互。

下载 DRM

回想一下,DRM 实际上只是 RDD 的包装器。在云存储桶中,它将被表示为一个“部分”矩阵的目录。为了下载这些文件,我们使用了示例 9-3 中展示的辅助函数。

示例 9-3. 下载 GCS 目录的辅助函数
def download_folder(bucket_name = 'your-bucket-name',
                    bucket_dir = 'your-bucket-directory/',
                    dl_dir= "local-dir/"):
    storage_client = storage.Client()
    bucket = storage_client.get_bucket(bucket_name)
    blobs = bucket.list_blobs(prefix=bucket_dir)  # Get list of files
    for blob in blobs:
        filename = blob.name.replace('/', '_')
        blob.download_to_filename(dl_dir + filename)  # Download

在撰写本文时,Mahout 与 Python 的集成很少(这段代码没有 PySpark 的等效代码)。

此外,没有用于将 Mahout DRMs 读入 Python NumPy 数组的辅助函数,因此我们必须编写另一个辅助函数来帮助我们完成(在示例 9-4 中显示)。

示例 9-4. 辅助函数,用于将 Mahout DRMs 读入 NumPy 矩阵
def read_mahout_drm(path):
    data = {}
    counter = 0
    parts = [p for p in os.listdir(path) if "part"] ![1](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/1.png)
    for p in parts:
        with open(f"{path}/{p}", 'r') as f:
            lines = f.read().split("\n")
            for l in lines[:-1]:
                counter +=1
                t = literal_eval(l)
                arr = np.array([t[1][i] for i in range(len(t[1].keys()))])
                data[t[0]] = arr
    print(f"read {counter} lines from {path}")
    return data

1

请记住,大多数 Mahout DRMs 将在文件的“部分”中,因此我们必须迭代这些部分来重构矩阵。

重新构建矩阵以生成去噪图像

在奇异值分解中,奇异值的对角矩阵通常用σ表示。然而,在我们的代码中,我们使用字母s。按照惯例,这些值通常从最重要到最不重要排序,并且幸运的是,Mahout 实现遵循了这一惯例。为了去噪图像,我们只需将对角线的最后几个值设为零。这个想法是,最不重要的基础向量可能代表我们希望消除的噪声(参见示例 9-5)。

示例 9-5. 用于写入多个图像的循环
percs = [0.001, 0.01, 0.05, 0.1, 0.3]

for p in range(len(percs)):
    perc = percs[p]
    diags = [diags_orig[i]
             if i < round(len(diags) - (len(diags) * perc))
             else 0
             for i in range(len(diags))] ![1](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/1.png)
    recon = drmU_p5 @ np.diag(diags) @ drmV_p5.transpose() ![2](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/2.png)
    composite_img = recon.transpose().reshape((512,512,301)) ![3](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/3.png)
    a1 = plt.subplot(1,1,1)
    plt.imshow(composite_img[:, :, 150], cmap=plt.cm.bone) ![4](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/4.png)
    plt.title(f"{perc*100}% denoised.  (k={len(diags)}, oversample=15, power_iters=2)")
    a1.set_aspect(1.0)
    plt.axis('off')
    fname = f"{100-(perc*100)}%-denoised-img.png"
    plt.savefig(f"/tmp/{fname}")
    upload_blob(bucket_name, f"/tmp/{fname}", f"/output/{fname}") ![5](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/5.png)

1

将最后p%的奇异值设为零。

2

@ 是“矩阵乘法”操作符。

3

我们假设我们的原始图像是 512 x 512 x 301 个切片,这可能对您的情况正确或不正确。

4

取第 150 个切片。

5

我们将在下一节讨论此函数。

现在在我们的存储桶中,我们将在/output/文件夹中有几张图像,命名为去噪的百分比。

我们的输出是 DICOM 一个切片的图像。相反,我们可以输出几个完整的 DICOM 文件(每个去噪级别一个),然后可以在 DICOM 查看器中查看,尽管完整的示例有点复杂,超出了本文的范围。如果您对此输出感兴趣,我们建议阅读pydicom的文档

CT 扫描去噪管道

要创建我们的管道,我们将首先为我们的 Spark 作业创建一个清单,该清单将指定要使用的图像,要使用的密钥以挂载哪些存储桶,以及各种其他信息。然后,我们将使用我们之前步骤中的容器和我们定义的清单创建一个管道,该管道将输出 DICOM 图像的一个切片的 PNG 格式,去除不同程度的噪音。

Spark 操作清单

Spark 从 GCS 读取/写入文件,因为它与 ReadWriteOnce(RWO)PVC 存在问题。我们需要从 GCS 下载输出,然后上传。

Apache Spark 操作员不喜欢从 ReadWriteOnce PVC 读取。如果您的 Kubernetes 使用这些操作员,并且无法请求 ReadWriteMany(例如在 GCP 上的情况),那么您将需要使用其他存储来存储将要分解的原始矩阵。

到目前为止,我们的大部分容器都使用了 ContainerOp。由于 Spark 作业实际上可能包含多个容器,我们将使用一个更通用的ResourceOp。定义 ResourceOp 给了我们更多的力量和控制,但这也以 Python API 不那么美观为代价。要定义一个 ResourceOp,我们必须定义一个清单(参见 示例 9-6),并将其传递给 ResourceOp 的创建(请参阅下一节)。

示例 9-6. Spark 操作清单
container_manifest = {
    "apiVersion": "sparkoperator.k8s.io/v1beta2",
    "kind": "SparkApplication",
    "metadata": {
        "name": "spark-app", ![1](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/1.png)
        "namespace": "kubeflow"
    },
    "spec": {
        "type": "Scala",
        "mode": "cluster",
        "image": "docker.io/rawkintrevo/covid-basis-vectors:0.2.0",
        "imagePullPolicy": "Always",
        "hadoopConf": { ![2](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/2.png)
            "fs.gs.project.id": "kubeflow-hacky-hacky",
            "fs.gs.system.bucket": "covid-dicoms",
            "fs.gs.impl" : "com.google.cloud.hadoop.fs.gcs.GoogleHadoopFileSystem",
            "google.cloud.auth.service.account.enable": "true",
            "google.cloud.auth.service.account.json.keyfile": "/mnt/secrets/user-gcp-sa.json",
        },
        "mainClass": "org.rawkintrevo.covid.App",
        "mainApplicationFile": "local:///covid-0.1-jar-with-dependencies.jar",
        # See the Dockerfile
        "arguments": ["245", "15", "1"],
        "sparkVersion": "2.4.5",
        "restartPolicy": {
            "type": "Never"
        },
        "driver": {
            "cores": 1,
            "secrets":  ![2
                {"name": "user-gcp-sa",
                 "path": "/mnt/secrets",
                 "secretType": "GCPServiceAccount"
                 }
            ],

            "coreLimit": "1200m",
            "memory": "512m",
            "labels": {
                "version": "2.4.5",
            },
            "serviceAccount": "spark-operatoroperator-sa", # also try spark-operatoroperator-sa
        },
        "executor": {
            "cores": 1,
            "secrets":  ![2
                {"name": "user-gcp-sa",
                 "path": "/mnt/secrets",
                 "secretType": "GCPServiceAccount"
                 }
            ],
            "instances": 4, ![3](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/3.png)
            "memory": "4084m"
        },
        "labels": {
            "version": "2.4.5"
        },

    }
}

1

应用程序的名称:您可以使用kubectl logs spark-app-driver在控制台上查看进度。

2

不同的云提供商在这里使用略有不同的配置。

3

我们在一个非常大的矩阵上进行分解,如果有余力,可能需要提供更多资源。

注意

因为我们正在访问 GCP,所以我们需要基于gcr.io/spark-operator/spark:v2.4.5-gcs-prometheus这个镜像,它包含了用于访问 GCP 的额外的 JAR 包(否则我们将使用gcr.io/spark-operator/spark:v2.4.5)。

尽管这是为 GCP 调整的,但在配置上进行非常少量的更改,特别是在密钥周围,这很容易转移到 AWS 或 Azure。

如果您熟悉 Kubernetes,您可能已经习惯于将清单表示为 YAML 文件。在这里,我们创建了一个包含 Python 字典的清单。接下来,我们将在我们的流水线定义中使用这个字典来创建一个ResourceOp

流水线

最后,我们拥有了所有必要的组件。我们将创建一个将它们串联在一起的流水线,以便为我们创建一个可重复操作。

回顾一下,示例 9-7 进行以下操作:

  • 从 GCP 下载 CT 扫描到本地 PVC。

  • 将 CT 扫描(DICOM 文件)转换为矩阵(s.csv)。

  • Spark 作业进行分布式随机奇异值分解,并将输出写入 GCP。

  • 将分解的矩阵重新组合,其中一些奇异值被设为零,从而去噪图像。

示例 9-7. CT 扫描去噪流水线
from kfp.gcp import use_gcp_secret
@kfp.dsl.pipeline(
    name="Covid DICOM Pipe v2",
    description="Visualize Denoised CT Scans"
)
def covid_dicom_pipeline():
    vop = kfp.dsl.VolumeOp(
        name="requisition-PVC",
        resource_name="datapvc",
        size="20Gi", #10 Gi blows up...
        modes=kfp.dsl.VOLUME_MODE_RWO
    )
    step1 = kfp.dsl.ContainerOp( ![1](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/1.png)
        name="download-dicom",
        image="rawkintrevo/download-dicom:0.0.0.4",
        command=["/run.sh"],
        pvolumes={"/data": vop.volume}
    )
    step2 = kfp.dsl.ContainerOp( ![2](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/2.png)
        name="convert-dicoms-to-vectors",
        image="rawkintrevo/covid-prep-dicom:0.9.5",
        arguments=[
            '--bucket_name', "covid-dicoms",
        ],
        command=["python", "/program.py"],
        pvolumes={"/mnt/data": step1.pvolume}
    ).apply(kfp.gcp.use_gcp_secret(secret_name='user-gcp-sa')) ![5](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/5.png)
    rop = kfp.dsl.ResourceOp( ![3](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/3.png)
        name="calculate-basis-vectors",
        k8s_resource=container_manifest,
        action="create",
        success_condition="status.applicationState.state == COMPLETED"
    ).after(step2)
    pyviz = kfp.dsl.ContainerOp( ![4](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/4.png)
        name="visualize-slice-of-dicom",
        image="rawkintrevo/visualize-dicom-output:0.0.11",
        command=["python", "/program.py"],
        arguments=[
            '--bucket_name', "covid-dicoms",
        ],
    ).apply(kfp.gcp.use_gcp_secret(secret_name='user-gcp-sa')).after(rop)

kfp.compiler.Compiler().compile(covid_dicom_pipeline,"dicom-pipeline-2.zip")
client = kfp.Client()

my_experiment = client.create_experiment(name='my-experiments')
my_run = client.run_pipeline(my_experiment.id, 'my-run1', 'dicom-pipeline-2.zip')

1

此容器未进行讨论,但其简单地从 GCP 存储桶下载图像到我们的本地 PVC。

2

在此,我们将 DICOM 转换为矩阵并上传到指定的 GCP 存储桶。

3

这是计算奇异值分解的 Spark 作业。

4

这就是 DICOM 图像重建的地方。

5

对于 GCP 我们 use_gcp_secret,但是 Azure 和 AWS 也有类似的功能。

为了说明,图 9-1 到 9-3 是 DICOM 图像在不同去噪水平上的切片。由于我们不是放射学专家,我们不会试图指出质量变化或最佳选择,除了指出在 10% 去噪时我们可能做得太过头了,在 30% 时毫无疑问是如此。

原始 DICOM 切片

图 9-1. 原始 DICOM 切片

1% 和 5% 去噪

图 9-2. 1% 去噪 DICOM 切片(左);5% 去噪 DICOM 切片(右)

10% 和 30% 去噪

图 9-3. 10% 去噪 DICOM 切片(左);.5% 去噪 DICOM 切片(右)

再次看到,尽管此流水线现在是为 GCP 硬编码的,但只需更新几行代码即可将其更改为与 AWS 或 Azure 兼容;具体来说,是如何将秘密挂载到容器中的。这样做的一个重要优势是我们能够安全地将密码与代码解耦。

分享流水线

Kubeflow 的另一个重要优势是实验的可重现性。尽管在学术界经常被强调,但在商业环境中,可重现性也是一个重要的概念。通过容器化流水线步骤,我们可以消除隐藏的依赖项,使程序不再只能在一个设备上运行,或者换句话说,可重现性可以防止您开发一个只能在某人的机器上运行的算法。

我们在这里呈现的流水线应该可以在任何 Kubeflow 部署上运行。³ 这也允许快速迭代。任何读者都可以将此流水线用作基础,例如可以创建一个最终步骤,在此步骤中对去噪图像和原始图像执行一些深度学习,以比较去噪的效果。

结论

现在我们已经看到如何通过利用包含大部分甚至所有所需依赖项的容器来创建非常易于维护的流水线。这不仅消除了必须维护具有所有这些依赖项的系统的技术债务,还使得程序更易于转移,并且我们的研究更易于转移和重现。

存在着一个庞大且令人兴奋的 Docker 容器星系,很可能您已经在现有容器中 Dockerize 了一些步骤。能够利用这些容器作为 Kubeflow 流水线步骤的一部分,无疑是 Kubeflow 的最大优势之一。

¹ 完整的论文可以在 这里 找到。

² 北美放射学会 希望尽快发布 COVID-19 CT 扫描的存储库。

³ 针对不使用 GCE 部署的微调。

第十章:超参数调整和自动化机器学习

在前几章中,我们已经看到 Kubeflow 如何帮助完成机器学习的各个阶段。但是,了解每个阶段该做什么——无论是特征准备、训练还是部署模型——都需要一定的专业知识和实验。根据“无免费午餐”定理,没有单一的模型适用于每个机器学习问题,因此每个模型必须经过精心构建。如果每个阶段需要大量人工输入,那么完全构建一个性能优异的模型可能会非常耗时和昂贵。

自然地,人们可能会想:是否可能自动化机器学习过程的部分或整体?我们是否可以减少数据科学家的工作量,同时保持高质量的模型?

在机器学习中,解决这些问题的总称是自动化机器学习(AutoML)。这是一个不断发展的研究领域,并已在实际应用中找到其位置。AutoML 试图通过减少在机器学习的更加耗时和迭代的阶段中的手动交互的需求,简化机器学习过程,无论是对专家还是非专家。

在本章中,我们将看到 Kubeflow 如何用于自动化超参数搜索和神经架构搜索,这是 AutoML 的两个重要子领域。

自动机器学习(AutoML)概述

AutoML 指的是自动化机器学习过程中的各种流程和工具。在高层次上,AutoML 涉及解决以下一个或多个问题的算法和方法:

数据预处理

机器学习需要数据,原始数据可以来自不同来源且格式各异。为了使原始数据有用,通常需要人类专家逐行查看数据,规范化数值,删除错误或损坏的数据,并确保数据一致性。

特征工程

使用过少的输入变量(或“特征”)训练模型可能导致模型不准确。然而,使用过多的特征也可能会导致问题;学习过程会变慢且消耗资源,可能会出现过拟合问题。找到合适的特征集可能是构建机器学习模型中最耗时的部分。自动特征工程可以加速特征提取、选择和转换的过程。

模型选择

一旦您拥有了所有的训练数据,就需要为您的数据集选择合适的训练模型。理想的模型应尽可能简单,同时又能提供良好的预测精度。

超参数调整

大多数学习模型有一些与模型外部相关的参数,例如学习率、批大小和神经网络中的层数。我们称这些参数为超参数,以区别于由学习过程调整的模型参数。超参数调整是自动化搜索这些参数的过程,以提高模型的准确性。

神经架构搜索。

与超参数调整相关的一个领域是神经架构搜索(NAS)。NAS 不是选择每个超参数值的固定范围,而是进一步实现自动化,生成一个整个神经网络,优于手工设计的架构。NAS 的常见方法包括强化学习和进化算法。

本章重点讨论后两个问题——超参数调整和神经架构搜索。由于它们相关,可以使用类似的方法来解决。

使用 Kubeflow Katib 进行超参数调整。

在第七章中提到,我们需要设置几个超参数。在机器学习中,超参数是在训练过程开始之前设置的参数(与从训练过程中学习的模型参数相对)。超参数的示例包括学习率、决策树数量、神经网络中的层数等。

超参数优化的概念非常简单:选择导致最佳模型性能的一组超参数值。超参数调整框架就是做这件事的工具。通常,这样的工具的用户会定义几件事情:

  • 超参数及其有效值范围的列表(称为搜索空间)。

  • 用于衡量模型性能的度量标准。

  • 用于搜索过程的方法论。

Kubeflow 打包了Katib,一个用于超参数调整的通用框架。在类似的开源工具中,Katib 有几个显著的特点:

它是 Kubernetes 原生的。

这意味着 Katib 实验可以在 Kubernetes 运行的任何地方移植。

它具有多框架支持。

Katib 支持许多流行的学习框架,并提供 TensorFlow 和 PyTorch 分布式训练的一流支持。

它是与语言无关的。

训练代码可以用任何语言编写,只要它构建为 Docker 镜像。

注意。

Katib 这个名字在阿拉伯语中意为“秘书”或“书记”,是对启发其最初版本的 Vizier 框架的致敬(“vizier” 在阿拉伯语中是大臣或高级官员的意思)。

在本章中,我们将看看 Katib 如何简化超参数优化。

Katib 概念。

让我们从定义几个对 Katib 工作流程至关重要的术语开始(如图 10-1 所示):

实验。

一个实验是一个端到端的过程,它涉及一个问题(例如,调整手写识别训练模型)、一个目标指标(最大化预测准确率)和一个搜索空间(超参数的范围),并生成最终的最优超参数值集合。

建议

建议是我们试图解决的问题的一个可能解决方案。因为我们试图找到导致最佳模型性能的超参数值组合,一个建议将是指定搜索空间中的一组超参数值。

试验

试验是实验的一次迭代。每个试验接受一个建议,并执行一个生成评估指标的工作进程(通过 Docker 打包)。Katib 的控制器然后基于先前的指标计算出下一个建议,并生成新的试验。

kfml 1001

图 10-1. Katib 系统工作流程
注意

在 Katib 中,实验、建议和试验都是自定义资源。这意味着它们存储在 Kubernetes 中,并且可以使用标准 Kubernetes API 进行操作。

超参数调优的另一个重要方面是如何找到下一组参数。截至本文撰写时,Katib 支持以下搜索算法:

网格搜索

也称为参数扫描,网格搜索是最简单的方法——穷举指定搜索空间中的所有可能参数值。虽然资源密集型,但网格搜索具有高并行性的优势,因为任务是完全独立的。

随机搜索

与网格搜索类似,随机搜索中的任务是完全独立的。随机搜索尝试通过随机选择生成参数值,而不是枚举每个可能的值。当需要调整的超参数很多(但只有少数对模型性能有显著影响时),随机搜索可以大大优于网格搜索。当离散参数的数量很高时,使得网格搜索不可行时,随机搜索也很有用。

贝叶斯优化

这是一种使用概率和统计来寻找更好参数的强大方法。贝叶斯优化构建了一个针对目标函数的概率模型,找到在模型上表现良好的参数值,然后根据试验运行期间收集的指标迭代更新模型。直观地说,贝叶斯优化通过做出明智的猜测来改进模型。这种优化方法依赖于先前的迭代来找到新的参数,并且可以并行化。虽然试验并非像网格搜索或随机搜索那样独立,但贝叶斯优化能够通过较少的总试验次数找到结果。

超带

这是一种相对新的方法,它随机选择配置值。 但与传统的随机搜索不同,Hyperband 仅对每个试验进行少量迭代的评估。 然后,它采用表现最佳的配置,并更长时间地运行它们,重复此过程直到达到所需的结果。 由于它与随机搜索的相似性,任务可以高度并行化。

其他实验性算法

这些包括使用Goptuna优化框架实现的 Parzen 估计树(TPE)和协方差矩阵适应进化策略(CMA-ES)。

Katib 中拼图的最后一部分是指标收集器。 这是在每次试验后收集和解析评估指标并将它们推送到持久数据库中的过程。 Katib 通过一个 sidecar 容器实现指标收集,该容器与 pod 中的主容器并行运行。

总的来说,Katib 的设计使其具有高度可扩展性、可移植性和可扩展性。 由于它是 Kubeflow 平台的一部分,Katib 本身原生支持与 Kubeflow 的许多其他训练组件集成,如 TFJob 和 PyTorch 运算符。 Katib 还是第一个支持多租户的超参数调整框架,使其非常适合云托管环境。

安装 Katib

默认情况下安装了 Katib。 要将 Katib 安装为独立服务,您可以使用 Kubeflow GitHub 存储库中的以下脚本:

git clone https://github.com/kubeflow/katib
bash ./katib/scripts/v1beta1/deploy.sh

如果您的 Kubernetes 集群不支持动态卷配置,您还将创建一个持久卷:

pv_path=https://raw.githubusercontent.com/kubeflow/katib/master/manifests\
/v1beta1/pv/pv.yaml
kubectl apply -f pv_path

安装完 Katib 组件后,您可以导航到 Katib 仪表板以验证其运行状态。 如果您通过 Kubeflow 安装了 Katib 并且有一个端点,只需导航到 Kubeflow 仪表板并在菜单中选择“Katib”。 否则,您可以设置端口转发以测试部署:

kubectl port-forward svc/katib-ui -n kubeflow 8080:80

然后导航到:

http://localhost:8080/katib/

运行您的第一个 Katib 实验

现在,Katib 已在您的集群中运行起来了,让我们看看如何运行一个实际的实验。 在本节中,我们将使用 Katib 来调整一个简单的 MNist 模型。 您可以在Katib 的 GitHub 页面上找到源代码和所有配置文件。

准备您的训练代码

第一步是准备您的训练代码。 由于 Katib 运行试验评估的训练作业,每个训练作业都需要打包为 Docker 容器。 Katib 是语言无关的,因此您如何编写训练代码并不重要。 但是,为了与 Katib 兼容,训练代码必须满足一些要求:

  • 超参数必须公开为命令行参数。 例如:
python mnist.py --batch_size=100 --learning_rate=0.1
  • 指标必须以与指标收集器一致的格式公开。 Katib 目前通过标准输出、文件、TensorFlow 事件或自定义支持指标收集。 最简单的选项是使用标准指标收集器,这意味着评估指标必须以以下格式写入 stdout:
metrics_name=metrics_value

我们将使用的示例训练模型代码可以在 此 GitHub 网站 找到。

准备好训练代码后,将其简单打包为 Docker 镜像即可使用。

配置实验

一旦你准备好训练容器,下一步就是为你的实验编写规范。Katib 使用 Kubernetes 自定义资源来表示实验。可以从 这个 GitHub 页面 下载 示例 10-1。

示例 10-1. 示例实验规范
apiVersion: "kubeflow.org/v1beta1"
kind: Experiment
metadata:
  namespace: kubeflow
  labels:
    controller-tools.k8s.io: "1.0"
  name: random-example
spec:
  objective:               ![1](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/1.png)
    type: maximize
    goal: 0.99
    objectiveMetricName: Validation-accuracy
    additionalMetricNames:
      - Train-accuracy
  algorithm:               ![2](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/2.png)
    algorithmName: random
  parallelTrialCount: 3    ![3](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/3.png)
  maxTrialCount: 12
  maxFailedTrialCount: 3
  parameters:              ![4](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/4.png)
    - name: --lr
      parameterType: double
      feasibleSpace:
        min: "0.01"
        max: "0.03"
    - name: --num-layers
      parameterType: int
      feasibleSpace:
        min: "2"
        max: "5"
    - name: --optimizer
      parameterType: categorical
      feasibleSpace:
        list:
        - sgd
        - adam
        - ftrl
  trialTemplate:           ![5](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/5.png)
    goTemplate:
        rawTemplate: |-
          apiVersion: batch/v1
          kind: Job
          metadata:
            name: {{.Trial}}
            namespace: {{.NameSpace}}
          spec:
            template:
              spec:
                containers:
                - name: {{.Trial}}
                  image: docker.io/kubeflowkatib/mxnet-mnist
                  command:
                  - "python3"
                  - "/opt/mxnet-mnist/mnist.py"
                  - "--batch-size=64"
                  {{- with .HyperParameters}}
                  {{- range .}}
                  - "{{.Name}}={{.Value}}"
                  {{- end}}
                  {{- end}}
                restartPolicy: Never

这是相当多的内容需要跟进。让我们仔细查看 spec 部分的每个部分:

1

目标。 这里是你配置如何衡量训练模型性能以及实验目标的地方。在这个实验中,我们试图最大化验证准确率指标。如果达到 0.99(99% 准确率)的目标,我们会停止实验。additionalMetricsNames 表示从每个试验中收集的指标,但不用于评估试验。

2

算法。 在这个实验中,我们使用随机搜索;有些算法可能需要额外的配置。

3

预算配置。 这里是我们配置实验预算的地方。在这个实验中,我们将并行运行 3 个试验,总共运行 12 个试验。如果有三个试验失败,我们也会停止实验。这最后一部分也被称为 错误预算 —— 在维护生产级系统正常运行时间方面是一个重要的概念。

4

参数。 这里我们定义想要调整的参数以及每个参数的搜索空间。例如,学习率 参数在训练代码中以 --lr 暴露,是一个双精度浮点数,搜索空间在 0.01 到 0.03 之间。

5

试验模板。 实验规范的最后部分是配置每个试验的模板。对于本例子,唯一重要的部分是:

    image: docker.io/kubeflowkatib/mxnet-mnist
    command:
      - "python3"
      - "/opt/mxnet-mnist/mnist.py"
      - "--batch-size=64"

这应该指向您在前一步中构建的 Docker 镜像,其命令行入口点用于运行代码。

运行实验

一切配置完成后,应用资源以启动实验:

kubectl apply -f random-example.yaml

您可以通过运行以下命令来检查实验的状态:

kubectl -n kubeflow describe experiment random-example

在输出中,你应该看到类似 示例 10-2 的内容。

示例 10-2. 示例实验输出
Name:         random-example
Namespace:    kubeflow
Labels:       controller-tools.k8s.io=1.0
Annotations:  <none>
API Version:  kubeflow.org/v1beta1
Kind:         Experiment
Metadata:
  Creation Timestamp:  2019-12-22T22:53:25Z
  Finalizers:
    update-prometheus-metrics
  Generation:        2
  Resource Version:  720692
  Self Link:         /apis/kubeflow.org/v1beta1/namespaces/kubeflow/experiments/random-example
  UID:               dc6bc15a-250d-11ea-8cae-42010a80010f
Spec:
  Algorithm:
    Algorithm Name:        random
    Algorithm Settings:    <nil>
  Max Failed Trial Count:  3
  Max Trial Count:         12
  Metrics Collector Spec:
    Collector:
      Kind:  StdOut
  Objective:
    Additional Metric Names:
      accuracy
    Goal:                   0.99
    Objective Metric Name:  Validation-accuracy
    Type:                   maximize
  Parallel Trial Count:     3
  Parameters:
    Feasible Space:
      Max:           0.03
      Min:           0.01
    Name:            --lr
    Parameter Type:  double
    Feasible Space:
      Max:           5
      Min:           2
    Name:            --num-layers
    Parameter Type:  int
    Feasible Space:
      List:
        sgd
        adam
        ftrl
    Name:            --optimizer
    Parameter Type:  categorical
  Trial Template:
    Go Template:
      Raw Template:  apiVersion: batch/v1
kind: Job
metadata:
  name: {{.Trial}}
  namespace: {{.NameSpace}}
spec:
  template:
    spec:
      containers:
      - name: {{.Trial}}
        image: docker.io/kubeflowkatib/mxnet-mnist-example
        command:
        - "python"
        - "/mxnet/example/image-classification/train_mnist.py"
        - "--batch-size=64"
        {{- with .HyperParameters}}
        {{- range .}}
        - "{{.Name}}={{.Value}}"
        {{- end}}
        {{- end}}
      restartPolicy: Never
Status:                                       ![1](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/1.png)
  Conditions:
    Last Transition Time:  2019-12-22T22:53:25Z
    Last Update Time:      2019-12-22T22:53:25Z
    Message:               Experiment is created
    Reason:                ExperimentCreated
    Status:                True
    Type:                  Created
    Last Transition Time:  2019-12-22T22:55:10Z
    Last Update Time:      2019-12-22T22:55:10Z
    Message:               Experiment is running
    Reason:                ExperimentRunning
    Status:                True
    Type:                  Running
  Current Optimal Trial:                      ![2](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/2.png)
    Observation:
      Metrics:
        Name:   Validation-accuracy
        Value:  0.981091
    Parameter Assignments:
      Name:          --lr
      Value:         0.025139701133432946
      Name:          --num-layers
      Value:         4
      Name:          --optimizer
      Value:         sgd
  Start Time:        2019-12-22T22:53:25Z
  Trials:            12                       ![3](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/3.png)
  Trials Running:    2
  Trials Succeeded:  10
Events:              <none>

输出中的一些有趣部分包括:

1

状态. 在这里,您可以看到实验的当前状态及其先前的状态。

2

当前最佳试验. 这是目前为止的“最佳”试验,即通过我们预定义的度量确定的表现最佳的试验。您还可以查看该试验的参数和指标。

3

Trials Succeeded/Running/Failed. 在本节中,您可以查看实验的进展情况。

Katib 用户界面

或者,您可以使用 Katib 的用户界面(UI)提交和监视您的实验。如果您有 Kubeflow 部署,可以通过在导航面板中单击“Katib”,然后在主页面中单击“超参数调整”来访问 Katib UI,如 图 10-2 所示。

kfml 1002

图 10-2. Katib UI 主页面

让我们提交我们的随机搜索实验(参见 图 10-3)。您可以简单地在此处粘贴一个 YAML,或者通过 UI 生成一个 YAML。要执行此操作,请单击“参数”选项卡。

kfml 1003

图 10-3. 配置新实验,第一部分

您应该看到一个面板,类似于 图 10-4。在此页面上输入必要的配置参数;定义运行预算和验证指标。

kfml 1004

图 10-4. 配置新实验,第二部分

然后,滚动页面并通过配置搜索空间和试验模板完成其余的实验。对于后者,您可以保持默认模板不变。完成后,单击“部署”。

现在实验正在运行中,您可以监视其状态,并查看进度的可视图表(参见 图 10-5)。通过在 Katib 仪表板中导航到下拉菜单,然后选择“UI”,然后选择“监视”,可以查看正在运行和已完成的实验。

kfml 1005

图 10-5. 用于实验的 Katib UI

在此图表下方,您将看到每个试验的详细分析(如 图 10-6 所示),每个试验的超参数值以及最终的度量值。这对比较某些超参数对模型性能的影响非常有用。

kfml 1006

图 10-6. 实验的 Katib 指标

由于我们还在实时收集验证指标,因此实际上可以为每个试验绘制图表。单击一行以查看模型如何在给定的超参数值下随时间表现(如 图 10-7 所示)。

kfml 1007

图 10-7. 每次试验的度量指标

调整分布式训练工作

在第七章中,我们看到了使用 Kubeflow 来编排分布式训练的示例。如果我们想要使用 Katib 来调整分布式训练工作的参数,该怎么办?

好消息是,Katib 原生支持与 TensorFlow 和 PyTorch 分布式训练的集成。TensorFlow 的 MNIST 示例可以在这个Katib GitHub 页面找到。这个示例使用了我们在第七章中看到的相同的 MNIST 分布式训练示例,并直接集成到 Katib 框架中。在示例 10-3 中,我们将启动一个实验来调整分布式 TensorFlow 作业的超参数(学习率和批次大小)。

示例 10-3. 分布式训练示例
apiVersion: "kubeflow.org/v1beta1"
kind: Experiment
metadata:
  namespace: kubeflow
  name: tfjob-example
spec:
  parallelTrialCount: 3             ![1](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/1.png)
  maxTrialCount: 12
  maxFailedTrialCount: 3
  objective:                        ![2](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/2.png)
    type: maximize
    goal: 0.99
    objectiveMetricName: accuracy_1
  algorithm:
    algorithmName: random
  metricsCollectorSpec:             ![3](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/3.png)
    source:
      fileSystemPath:
        path: /train
        kind: Directory
    collector:
      kind: TensorFlowEvent
  parameters:                       ![4](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/4.png)
    - name: learning_rate
      parameterType: double
      feasibleSpace:
        min: "0.01"
        max: "0.05"
    - name: batch_size
      parameterType: int
      feasibleSpace:
        min: "100"
        max: "200"
  trialTemplate:
    trialParameters:
      - name: learningRate
        description: Learning rate for the training model
        reference: learning_rate
      - name: batchSize
        description: Batch Size
        reference: batch_size
    trialSpec:
      apiVersion: "kubeflow.org/v1"
      kind: TFJob
      spec:
        tfReplicaSpecs:             ![5](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/5.png)
          Worker:
            replicas: 2
            restartPolicy: OnFailure
            template:
              spec:
                containers:
                  - name: tensorflow
                    image: gcr.io/kubeflow-ci/tf-mnist-with-summaries:1.0
                    imagePullPolicy: Always
                    command:
                      - "python"
                      - "/var/tf_mnist/mnist_with_summaries.py"
                      - "--log_dir=/train/metrics"
                      - "--learning_rate=${trialParameters.learningRate}"
                      - "--batch_size=${trialParameters.batchSize}"

1

总试验和并行试验计数与之前的实验类似。在这种情况下,它们指的是要运行的总分布式训练作业数和并行数。

2

目标规范也类似——在这种情况下,我们希望最大化accuracy度量。

3

度量收集器规范看起来略有不同。这是因为这是一个 TensorFlow 作业,我们可以直接使用 TensorFlow 输出的 TFEvents。通过使用内置的TensorFlowEvent收集器类型,Katib 可以自动解析 TensorFlow 事件并填充度量数据库。

4

参数配置完全相同——在这种情况下,我们调整模型的学习率和批次大小。

5

如果你读过第七章,那么试验模板应该很熟悉——这是我们之前运行过的相同分布式训练示例规范。这里的重要区别在于,我们已经将learning_ratebatch_size的输入参数化了。

所以现在你已经学会了如何使用 Katib 来调整超参数。但请注意,你仍然需要自己选择模型。我们能进一步减少人工工作量吗?关于 AutoML 的其他子领域呢?在接下来的部分中,我们将看看 Katib 如何支持整个人工神经网络的生成。

神经架构搜索

神经结构搜索(NAS)是自动化机器学习中不断发展的一个子领域。与超参数调整不同,后者选择了模型,我们的目标是通过调整少量旋钮来优化其性能;而 NAS 则试图生成网络架构本身。最近的研究表明,NAS 在图像分类、目标检测和语义分割等任务上可以胜过手工设计的神经网络。¹

大多数 NAS 方法可以归类为 生成 方法或 变异 方法。在 生成 方法中,算法会在每次迭代中提出一个或多个候选架构。这些提议的架构随后会在下一次迭代中进行评估和优化。在 变异 方法中,首先提出一个过度复杂的架构,随后的迭代会尝试修剪模型。

Katib 目前支持两种 NAS 实现方法:可微结构搜索(DARTS)² 和 高效神经结构搜索(ENAS)³。DARTS 通过将搜索空间从离散变为连续来实现 NAS 的可扩展性,并利用梯度下降优化架构。ENAS 则采用不同的方法,观察到大多数 NAS 算法在训练每个子模型时会出现瓶颈。ENAS 强制每个子模型共享参数,从而提高整体效率。

Katib 中 NAS 的一般工作流程与超参数搜索类似,额外增加了构建模型架构的步骤。Katib 的一个内部模块,称为 模型管理器,负责接收拓扑配置和变异参数,并构建新模型。然后,Katib 使用相同的试验和指标概念来评估模型的性能。

例如,可以查看使用 DARTS 进行 NAS 实验的规范,在 示例 10-4 中。

示例 10-4. 示例 NAS 实验规范
apiVersion: "kubeflow.org/v1beta1"
kind: Experiment
metadata:
  namespace: kubeflow
  name: darts-example-gpu
spec:
  parallelTrialCount: 1
  maxTrialCount: 1
  maxFailedTrialCount: 1
  objective:
    type: maximize
    objectiveMetricName: Best-Genotype
  metricsCollectorSpec:
    collector:
      kind: StdOut
    source:
      filter:
        metricsFormat:
          - "([\\w-]+)=(Genotype.*)"
  algorithm:
    algorithmName: darts
    algorithmSettings:
      - name: num_epochs
        value: "3"
  nasConfig:                     ![1](https://github.com/OpenDocCN/ibooker-ml-zh/raw/master/docs/kbfl-ml/img/1.png)
    graphConfig:
      numLayers: 3
    operations:
      - operationType: separable_convolution
        parameters:
          - name: filter_size
            parameterType: categorical
            feasibleSpace:
              list:
                - "3"
      - operationType: dilated_convolution
        parameters:
          - name: filter_size
            parameterType: categorical
            feasibleSpace:
              list:
                - "3"
                - "5"
      - operationType: avg_pooling
        parameters:
          - name: filter_size
            parameterType: categorical
            feasibleSpace:
              list:
                - "3"
      - operationType: max_pooling
        parameters:
          - name: filter_size
            parameterType: categorical
            feasibleSpace:
              list:
                - "3"
      - operationType: skip_connection
  trialTemplate:
    trialParameters:
      - name: algorithmSettings
        description: Algorithm settings of DARTS Experiment
        reference: algorithm-settings
      - name: searchSpace
        description: Search Space of DARTS Experiment
        reference: search-space
      - name: numberLayers
        description: Number of Neural Network layers
        reference: num-layers
    trialSpec:
      apiVersion: batch/v1
      kind: Job
      spec:
        template:
          spec:
            containers:
              - name: training-container
                image: docker.io/kubeflowkatib/darts-cnn-cifar10
                imagePullPolicy: Always
                command:
                  - python3
                  - run_trial.py
                  - --algorithm-settings="${trialParameters.algorithmSettings}"
                  - --search-space="${trialParameters.searchSpace}"
                  - --num-layers="${trialParameters.numberLayers}"
                resources:
                  limits:
                    nvidia.com/gpu: 1
            restartPolicy: Never

1

NAS 实验的一般结构与超参数搜索实验类似。规范的大部分内容应该非常熟悉;最重要的区别是添加了 nasConfig。这里可以配置想要创建的神经网络的规范,如层数、每层的输入和输出以及操作类型。

Katib 相比其他框架的优势

有许多类似的开源系统用于超参数搜索,其中包括 NNIOptunaRay TuneHyperopt。此外,Katib 的原始设计灵感来源于 Google Vizier。虽然这些框架在配置并行超参数扫描时具有许多与 Katib 类似的功能,例如使用多种算法,但 Katib 的几个特性使其独特:

设计旨在同时服务用户和管理员

大多数调整框架旨在为用户——进行调整实验的数据科学家提供服务。Katib 也被设计为使系统管理员的生活更轻松,他负责维护基础设施、分配计算资源和监控系统健康状态。

云原生设计

其他框架(例如 Ray Tune)可能支持与 Kubernetes 的集成,但通常需要额外的设置集群的工作。相比之下,Katib 是第一个完全基于 Kubernetes 设计的超参数搜索框架;其每一个资源都可以通过 Kubernetes API 进行访问和操作。

可扩展和可移植

由于 Katib 使用 Kubernetes 作为其编排引擎,因此很容易扩展实验。您可以在笔记本电脑上运行相同的实验进行原型设计,并在生产集群上部署作业,只需对规格进行最少的更改。相比之下,其他框架则需要根据硬件可用性进行额外的安装和配置工作。

可扩展的

Katib 提供了灵活和可插拔的接口,用于其搜索算法和存储系统。大多数其他框架都带有预设的算法列表,并且具有硬编码的指标收集机制。在 Katib 中,用户可以轻松实现自定义搜索算法,并将其集成到框架中。

本地支持

Katib 原生支持高级功能,如分布式训练和神经架构搜索。

结论

在本章中,我们快速概述了 AutoML,并学习了如何通过自动化超参数搜索等技术加速机器学习模型的开发。通过自动化的超参数调整技术,您可以在保持高模型质量的同时扩展模型的开发。

我们随后使用了 Katib——来自 Kubeflow 平台的 Kubernetes 原生调整服务——来配置和执行超参数搜索实验。我们还展示了如何使用 Katib 的仪表板提交、跟踪和可视化您的实验。

我们还探讨了 Katib 如何处理神经架构搜索(NAS)。Katib 目前支持 NAS 的两种方法——DARTS 和 ENAS,并将继续进行更多开发。

希望这些内容能为您提供一些关于如何利用 Katib 来减少机器学习工作流中工作量的见解。Katib 仍然是一个不断发展的项目,您可以在Katib GitHub 页面上关注最新的发展动态。

感谢您加入我们一起学习 Kubeflow 的旅程。我们希望 Kubeflow 能够满足您的需求,并帮助您利用机器学习的能力为您的组织带来价值。为了了解最新的 Kubeflow 变化,请加入Kubeflow Slack 工作空间和邮件列表

¹ T. Elsken, J. H. Metzen, F. Hutter, “神经架构搜索:一项调查,” 机器学习研究杂志 20 (2019), https://oreil.ly/eO-CV, pp. 1-21.

² H. Liu, K. Simonyan, and Y. Tang, “可微架构搜索(DARTS),” https://oreil.ly/JSAIX.

³ H. Pham 等人,“通过参数共享实现高效的神经架构搜索”,https://oreil.ly/SQPxn

附录 A. Argo 执行器配置和权衡

直到最近,所有 Kubernetes 实现都支持 Docker API。最初的 Argo 实现依赖于它们。随着 OpenShift 4 的推出,不再支持 Docker API,情况发生了变化。为了支持没有 Docker API 的情况,Argo 引入了几个新的执行器:Docker、Kubelet 和 Kubernetes API。在 Argo 参数文件中,containerRuntimeExecutor 配置值控制使用哪个执行器。根据这里的信息,每个执行器的优缺点总结在 表 A-1 中。这张表应该帮助您选择正确的 Argo 执行器值。

表 A-1. Argo 和 Kubernetes API

执行器 Docker Kubelet Kubernetes API PNC
优点 支持所有工作流示例。最可靠、经过充分测试,非常可扩展。与 Docker 守护程序进行沟通,用于处理重型任务。 安全性高。无法绕过 Pod 的服务账号权限。中等可扩展性。日志检索和容器轮询针对 Kubelet 执行。 安全性高。无法绕过 Pod 的服务账号权限。无需额外配置。 安全性高。无法绕过服务账号权限。可以从基础镜像层收集构件。可扩展性强:进程轮询通过 procfs 完成,而非 kubelet/k8s API。
缺点 安全性最低。需要挂载主机的 docker.sock(通常被 OPA 拒绝)。 可能需要额外的 kubelet 配置。只能在卷(例如 emptyDir)中保存参数/构件,而不能保存基础镜像层(例如 /tmp)。 可扩展性最低。日志检索和容器轮询针对 k8s API 服务器执行。只能在卷(例如 emptyDir)中保存参数/构件,而不能保存基础镜像层(例如 /tmp)。 进程不再以 pid 1 运行。对于完成过快的容器,可能会导致构件收集失败。无法从挂载在其下的卷中捕获基础镜像层的构件目录。尚未成熟。
Argo 配置 docker kubelet k8sapi pns

附录 B. 特定于云的工具和配置

特定于云的工具可以加速您的开发,但也可能导致供应商锁定。

Google Cloud

由于 Kubeflow 起源于 Google,因此在 Google Cloud 上运行时会有一些额外的功能可用。我们将快速指出如何使用 TPUs 和 Dataflow 加速您的机器学习流水线,以及更多与 Google 相关的组件可以在 Kubeflow GitHub 仓库 中找到。

TPU 加速实例

机器学习过程的不同部分不仅可以从不同数量的机器中受益,还可以从不同类型的机器中受益。最常见的例子是模型服务:通常,大量低内存的机器可以表现得相当不错,但对于模型训练,高内存或 TPU 加速的机器可以提供更大的好处。虽然使用 GPU 有一个方便的内置缩写,但对于 TPU,您需要显式地 import kfp.gcp as gcp。一旦导入了 kfp 的 gcp,您可以通过在容器操作中添加 .apply(gcp.use_tpu(tpu_cores=cores, tpu_resource=version, tf_version=tf_version)) 的方式,向任何容器操作添加 TPU 资源。

警告

TPU 节点仅在特定地区可用。请查看 此 Google Cloud 页面 以获取支持的地区列表。

Dataflow 用于 TFX

在 Google Cloud 上,您可以配置 Kubeflow 的 TFX 组件使用 Google 的 Dataflow 进行分布式处理。为此,您需要指定一个分布式输出位置(因为工作节点之间没有共享持久卷),并配置 TFX 使用 Dataflow 运行器。展示这一点最简单的方法是重新查看 示例 5-8; 要使用 Dataflow,我们会将其更改为 示例 B-1。

示例 B-1. 将管道更改为使用 Dataflow
generated_output_uri = root_output_uri + kfp.dsl.EXECUTION_ID_PLACEHOLDER
beam_pipeline_args = [
    '--runner=DataflowRunner',
    '--project=' + project_id,
    '--temp_location=' + root_output_uri + '/tmp'),
    '--region=' + gcp_region,
    '--disk_size_gb=50', # Adjust as needed
]

records_example = tfx_csv_gen(
    input_uri=fetch.output, # Must be on distributed storage
    beam_pipeline_args=beam_pipeline_args,
    output_examples_uri=generated_output_uri)

正如您所见,将管道更改为使用 Dataflow 相对简单,并且可以打开更大规模的数据进行处理。

虽然特定于云的加速可以带来好处,但要小心权衡,看看是否值得为了未来可能需要更换提供商而带来的额外麻烦。

附录 C. 在应用程序中使用模型服务

在第八章中,您学习了 Kubeflow 提供的暴露模型服务器的不同方法。正如那里所描述的,Kubeflow 提供多种部署训练模型和提供运行模型推断的 REST 和 gRPC 接口的方式。然而,它在为自定义应用程序使用这些模型提供支持方面还不足。在这里,我们将介绍一些利用 Kubeflow 暴露的模型服务器构建应用程序的方法。

在涉及利用模型推断的应用程序时,它们可以大致分为两类:实时和批处理应用程序。在实时/流式应用程序模型中,推断是直接在生成或接收的数据上进行的。在这种情况下,通常一次只有一个请求可用,并且可以在其到达时用于推理。在批处理场景中,所有数据都是一开始就可用的,并且可以顺序或并行用于推理。我们将从流式使用案例开始,然后看看可能的批处理实现。

利用模型服务构建流应用程序

如今大多数流应用程序利用Apache Kafka作为系统的数据骨干。实现流应用程序本身的两种可能选项是:使用流处理引擎和使用流处理库。

流处理引擎和库

如《定义流处理引擎执行语义》一文所定义的¹,现代流处理引擎基于将计算组织成块并利用集群架构。²将计算分成块可以实现执行并行性,其中不同块在同一台机器的不同线程上运行,或者在不同的机器上运行。它还通过将执行块从失败的机器移动到健康机器来实现故障转移。此外,现代引擎支持的检查点进一步提高了基于集群的执行的可靠性。

反之,流处理库是带有特定领域语言的库,提供一组构造来简化构建流应用程序。这些库通常不支持分发和/或集群——这通常留给开发者来实现。

由于这些选项听起来相似,它们通常可以互换使用。实际上,正如 Jay Kreps 在他的博客中概述的,流处理引擎和流处理库是两种构建流应用程序的非常不同的方法,选择其中之一是权衡功能和简易性。如前所述,流处理引擎提供更多功能,但需要开发人员遵循它们的编程模型和部署要求。他们通常也需要更陡峭的学习曲线来掌握其功能。另一方面,流处理库通常更易于使用,提供更多灵活性,但需要特定的部署、扩展性和负载平衡实现。

当今最流行的流处理引擎包括以下内容:

最受欢迎的流处理库有:

所有这些都可以作为构建流应用程序的平台,包括模型服务。³

数据艺术家(现在是 Vervetica)和 Confluent 团队联合完成的流处理引擎(Flink)和流处理库(Kafka streams)的比较也强调了流处理引擎和库之间的另一个区别:企业所有权。流处理引擎通常由企业范围的单元集中拥有和管理,而流处理库通常由个别开发团队监管,这通常使其采纳变得更加简单。流处理引擎非常适合需要这些引擎提供的开箱即用功能的应用程序,包括跨集群的并行扩展性和高吞吐量,事件时间语义,检查点功能,内置支持监控和管理,以及流和批处理混合处理。使用引擎的缺点是您受制于它们提供的编程和部署模型。

相反,流处理库提供的编程模型允许开发人员按照符合其精确需求的方式构建应用程序或微服务,并将它们部署为简单的独立的 Java 应用程序。但在这种情况下,他们需要自己实施扩展性、高可用性和监控解决方案(基于 Kafka 的实现通过利用 Kafka 支持其中的一些解决方案)。

介绍 Cloudflow

实际上,大多数流应用程序实现需要使用多个引擎和库来构建单个应用程序,这会导致额外的集成和维护复杂性。通过使用像Cloudflow这样的开源项目,可以显著减轻这些问题,它允许您快速开发、编排和操作基于 Kubernetes 的分布式流应用程序。Cloudflow 支持将流应用程序构建为一组小型、可组合的组件,这些组件通过基于模式的契约与 Kafka 连接并相互连接。这种方法可以显著提高重用性,并允许您极大地加速流应用程序的开发。在撰写本文时,此类组件可以使用 Akka Streams 实现;Flink 和 Spark 流支持 Kafka Streams 正在即将到来。Cloudflow 的整体架构在图 C-1 中展示。

Cloudflow 架构

图 C-1. Cloudflow 架构

Cloudflow 的核心是 Cloudflow 操作员,负责部署/撤销、管理和扩展管道和单个 streamlet。操作员还利用现有的FlinkSpark操作员来管理 Flink 和 Spark streamlet。提供的一组 Helm 图表支持操作员和支持组件的简单安装。

构建流式应用程序时的一个常见挑战是在进入生产之前将所有组件连接在一起并进行端到端测试。Cloudflow 通过允许您验证组件之间的连接并在开发期间本地运行应用程序来解决此问题,以避免部署时的意外。

在 Cloudflow 中,一切都是在应用程序的上下文中完成的,该应用程序代表一个由数据流通过 Kafka 连接的自包含分布式系统(图形)的数据处理服务。

Cloudflow 支持:

开发

通过生成大量样板代码,使开发人员可以专注于业务逻辑。

构建

它提供了从业务逻辑到可部署 Docker 镜像的所有工具。

部署

它提供了 Kubernetes 工具,可以通过单个命令部署您的分布式应用程序。

操作

它为您的分布式流式应用程序提供了获取洞察力、可观察性和生命周期管理的所有工具。Cloudflow 直接支持的另一个重要操作关注点是能够扩展流的各个组件。

当使用 Cloudflow 实现流式应用程序时,模型服务器调用通常由基于动态控制流模式的单独 streamlet⁴实现。

在 图 C-2 中,一个实现包含一个状态,状态是指在使用模型服务进行推断时模型服务服务器的 URL。⁵ 在这种情况下的实际数据处理是通过调用模型服务器来获取推断结果。此调用可以使用 REST 或 gRPC(或模型服务器支持的任何其他接口)进行。

动态控制的流模式

图 C-2. 动态控制的流模式

这个状态可以通过额外的 Kafka 主题进行更新,允许在不重新部署应用程序的情况下切换 URL(例如模型服务器部署移动的情况)。这个状态被数据处理器用于处理传入的数据。

可以向应用程序引入额外的流片段(具有相同的架构),以获取模型服务的见解,例如解释和漂移检测(详见“模型监控”)。

构建利用模型服务的批处理应用程序

典型的批处理应用程序通过读取包含所有样本的数据集来实现,然后处理它们,为每一个调用模型服务器。最简单的批处理应用程序实现是顺序执行,逐个处理数据元素。虽然这样的实现能够工作,但由于处理每个元素的网络开销,性能并不理想。

加速处理的一种流行方法是使用批处理。例如,TFServing 支持 两种批处理方法:服务器端批处理和客户端批处理。

TFServing 默认支持服务器端批处理。⁶ 要启用批处理,设置 --enable_batching--batching_parameters_file 标志。为了在延迟和吞吐量之间取得最佳平衡,请选择适当的批处理参数。⁷ 可以在 这个 TFServing GitHub 仓库 中找到一些关于 CPU 和 GPU 使用的参数值建议。

达到服务器端完整批处理后,推断请求在内部合并为一个大请求(张量),并在合并请求上运行一个 Tensorflow 会话。您需要使用异步客户端请求来填充服务器端批处理。在单个会话上运行一批请求是真正利用 CPU/GPU 并行性的地方。

客户端批处理只是在客户端将多个输入分组到一起以进行单个请求。

虽然批处理可以显著提高批量推断的性能,但通常不足以达到性能目标。另一种提高性能的流行方法是多线程。⁸ 此方法背后的想法是部署多个模型服务器实例,将数据处理分成多个线程,并允许每个线程为其负责的部分数据进行推断。

通过流处理实现多线程的一种方法是通过流处理实现批处理。这可以通过实现软件组件⁹读取源数据并将每个记录写入 Kafka 进行处理来实现。这种方法有效地将批处理转换为流处理,以允许通过如图 C-3 所示的架构实现更好的可伸缩性。

使用流处理进行批量服务实现

图 C-3. 使用流处理进行批量服务实现

这个部署包括三个层次:

  • 基于 Cloudflow 的流处理,为每个流元素调用模型服务。此解决方案的每个流单元可以适当地扩展,以提供所需的吞吐量。

  • 一个执行实际模型推断的模型服务器。通过更改模型服务器的数量,可以独立扩展此层次。

  • 负载均衡器,例如 Istio 或 Ambassador,为推断 REST/gRPC 请求提供负载均衡。

因为此架构中的每一层都可以独立扩展,因此这样的架构可以为流和批处理用例提供相当可伸缩的模型服务解决方案。

¹ L. Affetti 等人,“定义流处理引擎的执行语义”,大数据杂志 4 (2017),https://oreil.ly/TcI39

² 与 MapReduce 架构进行比较。

³ 有关实现细节,请参阅报告《机器学习模型服务》(Serving Machine Learning Models),以及Kai Waehner 在 GitHub 上的项目

⁴ 一些关于 TFServing 集成的实现示例可以在这个 GitHub 仓库中找到,而对于 Seldon 集成,则在这个 GitHub 仓库中找到。

⁵ 在使用嵌入模型的情况下,状态本身就是一个模型。

⁶ 详细信息请参阅此 TFServing 文档

⁷ 有关可用参数的完整定义,请参阅此 TFServing GitHub 仓库

⁸ 与MapReduce编程模型进行比较。

⁹ 在基于 Cloudflow 的实现中,Streamlet。

posted @ 2025-11-22 09:01  绝不原创的飞龙  阅读(16)  评论(0)    收藏  举报