Python-机器学习工程第二版-一-

80 阅读1小时+

Python 机器学习工程第二版(一)

原文:annas-archive.org/md5/12b0185c4bf68c0fcb37173533d7088b

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

“软件正在吞噬世界,但 AI 将吞噬软件。”

—— 英伟达首席执行官黄仁勋

机器学习ML),作为更广泛的人工智能AI)领域的一部分,正合理地被认为是组织从其数据中提取价值的最强大工具之一。随着机器学习算法能力的逐年增长,越来越明显的是,以可扩展、容错和自动化的方式实施这些算法需要创建新的学科。这些学科,机器学习工程MLE)和机器学习运维MLOps),正是本书的重点。

本书涵盖了广泛的主题,旨在帮助您了解可以应用于构建您的机器学习解决方案的工具、技术和流程,并着重介绍关键概念,以便您可以在未来的工作中在此基础上进行构建。目标是发展基础知识和广泛的理解,这些知识和理解能够经受时间的考验,而不仅仅是提供一系列对最新工具的介绍,尽管我们确实涵盖了大量的最新工具!

所有代码示例均以 Python 编写,这是当时世界上最流行的编程语言(撰写本文时)和数据应用的通用语言。Python 是一种高级面向对象的编程语言,拥有丰富的数据科学和机器学习工具生态系统。例如,scikit-learnpandas等包已经成为全球数据科学团队的标准化词汇。本书的核心原则是,仅了解如何使用这些包是不够的。在这本书中,我们将使用这些工具以及许多其他工具,但重点是如何将它们封装到生产级管道中,并使用适当的云和开源工具进行部署。

我们将涵盖从如何组织您的机器学习团队,到软件开发方法和最佳实践,再到通过自动化模型构建,以及如何将您的机器学习管道部署到各种不同的目标,然后到如何扩展您的工作负载以进行大规模批量运行。我们还将讨论,在第二版新增的全新章节中,将机器学习工程和 MLOps 应用于深度学习和生成式 AI 的激动人心的世界,包括如何开始使用大型语言模型LLMs)构建解决方案以及新的LLM 运维LLMOps)领域。

《使用 Python 进行机器学习工程》的第二版在几乎每一章都比第一版深入得多,包括更新的示例和更多对核心概念的分析。涵盖的工具种类也更加广泛,对开源工具和开发的关注也更多。虽然仍然强调核心概念,但我希望这种更广阔的视角意味着第二版将成为那些希望获得机器学习工程实用知识的人的优秀资源。

虽然对使用开源工具的重视程度更高,但许多示例也将利用来自亚马逊网络服务(AWS)的服务和解决方案。我相信,然而,伴随的解释和讨论将意味着您可以将这里学到的所有内容应用到任何云提供商,甚至是在本地环境中。

使用 Python 进行机器学习工程第二版将帮助您应对将 ML 投入生产的挑战,并让您有信心开始在项目中应用 MLOps。我希望您会喜欢它!

本书面向的对象

这本书是为那些希望使用机器学习组件构建稳健软件解决方案的机器学习工程师、数据科学家和软件开发人员而写的。它也适用于管理或希望了解这些系统生产生命周期的人。本书假设读者具备中级 Python 知识,并对机器学习的基本概念有所了解。一些 AWS 的基本知识和 bash 或 zsh 等 Unix 工具的使用也将有所帮助。

本书涵盖的内容

第一章机器学习工程简介,解释了机器学习工程和机器学习操作的核心概念。详细讨论了 ML 团队中的角色,并概述了 ML 工程和 MLOps 的挑战。

第二章机器学习开发过程,探讨了如何组织和成功执行一个 ML 工程项目。这包括对敏捷、Scrum 和 CRISP-DM 等开发方法的讨论,然后分享作者开发的项目方法,该方法在整本书中都有所提及。本章还介绍了持续集成/持续部署CI/CD)和开发者工具。

第三章从模型到模型工厂,展示了如何标准化、系统化和自动化训练和部署机器学习模型的过程。这是通过作者提出的模型工厂概念来实现的,这是一种可重复创建和验证模型的方法。本章还讨论了理解机器学习模型的关键理论概念,并涵盖了不同类型的漂移检测和模型重新训练触发标准。

第四章打包,讨论了在 Python 中进行编码的最佳实践,以及这与构建自己的包、库和组件以在多个项目中重用的关系。本章在介绍更高级的概念之前,首先涵盖了基本的 Python 编程概念,然后讨论了包和环境管理、测试、日志记录和错误处理以及安全性。

第五章部署模式和工具,教你一些标准的设计 ML 系统并将其投入生产的方法。本章首先关注架构、系统设计和部署模式,然后转向使用更高级的工具来部署微服务,包括容器化和 AWS Lambda。随后详细介绍了流行的 ZenML 和 Kubeflow 管道和部署平台,并提供了示例。

第六章扩展规模,主要关于在考虑大数据集的情况下进行开发。为此,详细讨论了 Apache Spark 和 Ray 框架,并提供了工作示例。本章的重点是扩展需要大量计算能力的批处理工作负载。

第七章深度学习、生成式 AI 和 LLMOps,涵盖了为生产用例训练和部署深度学习模型的最新概念和技术。本章包括讨论生成模型新趋势的内容,特别关注大型语言模型LLMs)以及 ML 工程师将这些模型投入生产的挑战。这引出了定义 LLM 操作(LLMOps)的核心要素。

第八章构建示例 ML 微服务,介绍了使用 FastAPI、Docker 和 Kubernetes 构建机器学习微服务的过程,该微服务提供预测解决方案。这汇集了本书中开发的大部分先前概念。

第九章构建 ETL 机器学习用例,构建了一个示例批处理 ML 系统,该系统利用标准 ML 算法,并通过使用 LLMs 来增强这些算法。这展示了 LLMs 和 LLMOps 的具体应用,以及 Airflow DAGs 的更高级讨论。

为了最大限度地利用本书

  • 在本书中,假设读者对 Python 开发有一些了解。为了完整性,涵盖了众多入门级概念,但一般来说,如果你已经编写过至少一些 Python 程序,将更容易理解示例。本书还假设读者对机器学习的主要概念有所了解,例如什么是模型,什么是训练和推理,以及对这些类似概念的理解。其中一些在文本中进行了回顾,但如果你之前已经熟悉了构建机器学习模型的主要思想,即使是基础水平,那么阅读本书将会更加顺畅。

  • 在技术方面,为了充分利用本书中的示例,您需要访问一台计算机或服务器,您有权安装和运行 Python 以及其他软件包和应用程序。对于许多示例,假设您有 UNIX 类型终端的访问权限,例如 bash 或 zsh。本书中的示例是在运行 Ubuntu LTS 的 Linux 机器和运行 macOS 的 M2 Macbook Pro 上编写和测试的。如果您使用的是不同的设置,例如 Windows,示例可能需要一些调整才能在您的系统上运行。请注意,使用 M2 Macbook Pro 意味着一些示例会显示一些额外的信息,以便在 Apple Silicon 设备上运行示例。如果您的系统不需要这种额外设置,这些部分可以舒适地跳过。

  • 许多基于云的示例利用亚马逊网络服务AWS),因此需要一个带有计费设置的 AWS 账户。大多数示例将使用 AWS 提供的免费层服务,但这并不总是可能的。建议谨慎行事,以避免产生大额账单。如果有疑问,建议您查阅 AWS 文档以获取更多信息。作为一个具体的例子,在第五章部署模式和工具中,我们使用了 AWS 的Apache Spark 托管工作流MWAA)服务。MWAA 没有免费层选项,因此一旦启动示例,您将开始为环境和任何实例付费。在继续之前,请确保您愿意这样做,并且我建议在完成时拆除您的 MWAA 实例。

  • CondaPip被用于本书中的包和环境管理,但在许多情况下也使用了 Poetry。为了方便在本书 GitHub 仓库(github.com/PacktPublishing/Machine-Learning-Engineering-with-Python-Second-Edition)中每个章节的复现开发环境,每个章节都有一个对应的文件夹,在该文件夹中包含requirements.txt和 Conda 的environment.yml文件,以及有用的README文件。复制环境和任何其他要求的命令在本书每个章节的开头给出。

  • 如果您使用的是本书的数字版,我仍然建议您亲自输入代码或从本书的 GitHub 仓库(github.com/PacktPublishing/Machine-Learning-Engineering-with-Python-Second-Edition)获取代码。这样做将帮助您避免与代码复制粘贴相关的任何潜在错误。

下载示例代码文件

如上所述,本书的代码包托管在 GitHub 上,网址为 github.com/PacktPublishing/Machine-Learning-Engineering-with-Python-Second-Edition。我们还有其他来自我们丰富图书和视频目录的代码包,可在 github.com/PacktPublishing/ 找到。查看它们吧!

下载彩色图像

我们还提供了一份包含本书中使用的截图/图表的彩色图像的 PDF 文件。您可以从这里下载:packt.link/LMqir

使用的约定

本书使用了多种文本约定。

CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。例如:“首先,我们必须从alibi-detect包中导入TabularDrift检测器,以及用于加载数据和分割数据的相关包。”

代码块设置如下:

from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split
import alibi
from alibi_detect.cd import TabularDrift 

任何命令行输入或输出都按以下方式编写,并在文本的主体中作为命令行命令表示:

pip install tensorflow-macos 

粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中会像这样显示。例如:“选择部署按钮。这将提供一个下拉菜单,您可以在其中选择创建服务。”

对附加资源或背景信息的引用会像这样出现。

有用的提示和重要的注意事项会像这样出现。

联系我们

我们始终欢迎读者的反馈。

一般反馈:请发送电子邮件至 feedback@packtpub.com,并在邮件主题中提及书籍标题。如果您对本书的任何方面有疑问,请通过电子邮件联系 questions@packtpub.com

勘误:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们非常感谢您向我们报告。请访问 www.packtpub.com/submit-errata,点击 提交勘误,并填写表格。

盗版:如果您在互联网上以任何形式遇到我们作品的非法副本,我们非常感谢您提供位置地址或网站名称。请通过电子邮件联系 copyright@packtpub.com 并附上材料的链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com

分享您的想法

一旦您阅读了 Python 机器学习工程 - 第二版,我们非常乐意听到您的想法!请点击此处直接进入此书的亚马逊评论页面并分享您的反馈。

您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。

下载此书的免费 PDF 副本

感谢您购买此书!

您喜欢在旅途中阅读,但无法携带您的印刷书籍到任何地方吗?您的电子书购买是否与您选择的设备不兼容?

别担心,现在,每购买一本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。

在任何地方、任何地点、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。

优惠远不止于此,您将获得独家折扣、新闻通讯以及每天收件箱中的优质免费内容。

按照以下简单步骤获取优惠:

  1. 扫描下面的二维码或访问以下链接

    packt.link/free-ebook/9781837631964

  2. 提交您的购买证明

  3. 就这样!我们将直接将您的免费 PDF 和其他优惠发送到您的电子邮件。

第一章:机器学习工程导论

欢迎来到《Python 机器学习工程》的第二版,这是一本旨在向您介绍制作机器学习ML)系统生产就绪的激动人心的世界的书籍。

自本书第一版发布以来,ML 的世界已经发生了显著的变化。现在有更多更强大的建模技术可用,更复杂的技术堆栈,以及一大堆新的框架和范式来保持更新。为了帮助从噪音中提取信号,本书的第二版比第一版更深入地涵盖了更广泛的主题,同时仍然关注您构建 ML 工程专业知识所需的关键工具和技术。本版将涵盖相同的核心主题,例如如何管理您的 ML 项目,如何创建您自己的高质量 Python ML 包,以及如何构建和部署可重用的训练和监控管道,同时增加对更现代工具的讨论。它还将更深入地展示和分析不同的部署架构,并讨论更多使用 AWS 和云无关工具扩展应用程序的方法。所有这些都将使用各种最受欢迎和最新的开源包和框架来完成,从经典如Scikit-LearnApache SparkKubeflowRayZenML。令人兴奋的是,本版还设有全新的章节,完全致力于Transformer大型语言模型LLM)如 ChatGPT 和 GPT-4,包括使用 Hugging Face 和 OpenAI API 进行微调和构建使用这些非凡新模型的管道的示例。与第一版一样,重点是为您提供进入 ML 工程各个组成部分的坚实基础。目标是到本书结束时,您将能够自信地使用这些最新的工具和概念在 Python 中构建、扩展和部署生产级的 ML 系统。

即使您不运行技术示例,或者尝试在其他编程语言或使用不同工具中应用主要观点,您也能从这本书中获得很多。正如之前提到的,目标是创建一个坚实的概念基础,您可以在此基础上构建。在介绍关键原则时,目标是让您在阅读完这本书后,在应对自己选择的工具集的 ML 工程挑战时更有信心。

在本章的第一部分,您将了解与机器学习工程相关的不同类型的数据角色以及它们为什么很重要,如何利用这些知识来构建和参与适当的团队,在现实世界中构建工作 ML 产品时需要记住的一些关键点,如何开始隔离适合工程 ML 解决方案的问题,以及如何为各种典型的商业问题创建您自己的高级 ML 系统设计。

我们将在以下章节中涵盖这些主题:

  • 定义数据学科的分类

  • 组建您的团队

  • 真实世界中的机器学习工程

  • 机器学习解决方案看起来是什么样子?

  • 高级机器学习系统设计

现在我们已经解释了在本章中我们要追求的内容,让我们开始吧!

技术要求

在整本书中,所有代码示例都将假设使用 Python 3.10.8,除非另有说明。本版中的示例已在配备 M2 苹果硅芯片的 2022 Macbook Pro 上运行,并安装了 Rosetta 2 以允许与基于 Intel 的应用程序和包向后兼容。大多数示例也已在运行 Ubuntu 22.04 LTS 的 Linux 机器上测试过。每章所需的 Python 包存储在书籍 Git 仓库中相应章节文件夹的.yml文件中的conda环境中。我们将在本书的后面部分详细讨论包和环境管理。但在此期间,假设您有一个 GitHub 账户并且已经配置了环境以从 GitHub 远程仓库中拉取和推送,要开始,您可以从命令行克隆本书的仓库:

git clone https://github.com/PacktPublishing/Machine-Learning-Engineering-with-Python-Second-Edition.git 

假设您已经安装了 Anaconda 或 Miniconda,然后您可以导航到本书 Git 仓库中的Chapter01文件夹并运行:

conda env create –f mlewp-chapter01.yml 

这将设置您可以使用它来运行本章中给出的示例的环境。对于每个章节,可以遵循类似的程序,但每个部分也会指出针对这些示例的特定安装要求。

现在我们已经完成了一些设置,我们将开始探索机器学习工程的世界以及它如何适应现代数据生态系统。让我们开始探索机器学习工程的世界吧!

注意:在运行本节中给出的conda命令之前,您可能需要手动安装特定的库。一些版本的 Facebook Prophet 库需要可以构建在运行苹果硅的 Macbook 上的 PyStan 版本。如果您遇到此问题,那么您应该尝试手动安装httpstan包。首先,访问github.com/stan-dev/httpstan/tags并选择要安装的包版本。下载该版本的tar.gz.zip文件并解压。然后您可以导航到解压后的文件夹并运行以下命令:

make
python3 -m pip install poetry
python3 -m poetry build
python3 -m pip install dist/*.whl 

在后续示例中调用model.fit()时,您可能会遇到以下错误:

dyld[29330]: Library not loaded: '@rpath/libtbb.dylib' 

如果是这样,您将需要运行以下命令,用 Conda 环境中 Prophet 安装的正确路径替换:

cd /opt/homebrew/Caskroom/miniforge/base/envs/mlewp-chapter01/lib/python3.10/site-packages/prophet/stan_model/
install_name_tool -add_rpath @executable_path/cmdstan-2.26.1/stan/lib/stan_math/lib/tbb prophet_model.bin 

哦,在苹果硅上做机器学习的乐趣!

定义数据学科的分类

近年来数据的爆炸式增长及其潜在应用导致了众多职位和职责的激增。曾经关于数据科学家与统计学家之间区别的争论现在变得极其复杂。然而,我认为这并不一定那么复杂。从数据中获得价值所需进行的活动在各个业务领域都是相当一致的,因此,合理地预期执行这些步骤所需的技能和角色也将相对一致。在本章中,我们将探讨一些我认为在任何数据项目中始终需要的核心数据学科。正如你可以猜到的,鉴于本书的名称,我将特别热衷于探讨机器学习工程的概念以及它是如何融入其中的。

现在让我们来看看在现代数据环境中使用数据的一些相关角色。

数据科学家

在《哈佛商业评论》宣布成为数据科学家是21 世纪最性感的工作hbr.org/2012/10/data-scientist-the-sexiest-job-of-the-21st-century)之后,这个职位成为最受欢迎、但也最被炒作的职位之一。其受欢迎程度仍然很高,但将高级分析和机器学习投入生产所面临的挑战意味着数据驱动型组织内部工程角色的转变越来越多。传统的数据科学家角色可以根据业务领域、组织或仅仅是个人偏好涵盖整个范围的任务、技能和责任。然而,无论这个角色如何定义,一些关键的关注领域始终应该是数据科学家工作档案的一部分:

  • 分析:数据科学家应该能够在进行数据分析之前,整理、处理、操作和整合数据集。分析是一个广泛的概念,但很明显,最终的结果是你对数据集的了解,这是你在开始之前所没有的,无论这个数据集是基本还是复杂。

  • 建模:让每个人都兴奋(可能包括亲爱的读者)的是,在数据中建模现象的想法。数据科学家通常必须能够将统计、数学和机器学习技术应用于数据,以解释其中包含的过程或关系,并执行某种预测。

  • 与客户或用户合作:数据科学家的角色通常包含更多面向商业的元素,以便前两个点的结果可以支持组织内的决策。这可能通过在 PowerPoint 演示文稿或 Jupyter 笔记本中展示分析结果,或者甚至通过发送包含关键结果摘要的电子邮件来完成。这涉及到沟通和商业洞察力,这在经典的技术角色中是超越的。

机器学习工程师

在创建机器学习概念验证和构建健壮软件之间的差距,我经常在演讲中提到的“鸿沟”,导致了现在我认为技术领域最重要的角色之一的出现。机器学习工程师满足了将数据科学建模和探索的世界转化为软件产品和系统工程世界的迫切需求。由于这并非易事,机器学习工程师的需求日益增加,现在已成为数据驱动型软件价值链的关键部分。如果你不能将事物投入生产,你就不会产生价值,如果你不产生价值,那么我们都知道那不是一件好事!

通过考虑一个经典的语音助手,你可以很好地阐述这种角色的需求。在这种情况下,数据科学家通常会专注于将业务需求转化为一个可工作的语音到文本模型,可能是一个非常复杂的神经网络,并证明它可以在原则上执行所需的语音转录任务。然后,机器学习工程就是如何将这个语音到文本模型构建成一个可以在生产中使用的软件、服务或工具。在这里,这可能意味着构建一些软件来训练、重新训练、部署和跟踪模型的性能,随着转录数据的积累或用户偏好的理解。这也可能涉及到了解如何与其他系统接口并提供模型结果的适当格式。例如,模型的结果可能需要打包成一个 JSON 对象,并通过 REST API 调用发送到在线超市,以完成订单。

数据科学家和机器学习工程师有很多重叠的技能集和能力,但有不同的关注领域和优势(稍后详述),因此他们通常会是同一个项目团队的一部分,并且可能会有任一职位,但根据他们在项目中的表现,可以清楚地知道他们扮演的角色。

与数据科学家类似,我们可以定义机器学习工程师的关键关注领域:

  • 翻译:将各种格式的模型和研究代码转化为更流畅、更健壮的代码片段。

    这可以通过面向对象编程、函数式编程、混合编程或其他方式来完成,但基本上它有助于将数据科学家的概念验证工作转化为在生产环境中更值得信赖的东西。

  • 架构:任何软件组件的部署都不会在真空中进行,总会涉及大量的集成部分。这一点在机器学习解决方案中同样适用。机器学习工程师必须理解适当的工具和流程是如何相互关联的,以便使用数据科学家构建的模型能够完成其工作,并在规模上实现。

  • 生产化:机器学习工程师专注于交付解决方案,因此应该彻底理解客户的需求,以及能够理解这对项目开发意味着什么。机器学习工程师的最终目标不仅仅是提供一个好的模型(尽管这也是其中的一部分),也不是提供一个基本上能工作的东西。他们的工作是确保在现实世界的环境中,数据科学方面的辛勤工作能够产生最大的潜在价值。

机器学习运维工程师

机器学习工程将是本书的重点,但现在正在出现一个重要的角色,其目的是使机器学习工程师能够以更高的质量、更快的速度和更大的规模开展工作。这些是机器学习运维工程师MLOps)。这个角色主要是关于构建工具和能力,以使机器学习工程师和数据科学家能够完成任务。这个角色更侧重于构建其他角色使用的工具、平台和自动化,因此它们之间有很好的联系。这并不是说 MLOps 工程师不会在特定的项目或构建中使用;只是他们的主要增值不是来自这里,而是来自在特定项目或构建中启用的能力。如果我们回顾一下在机器学习工程师部分描述的语音到文本解决方案的例子,我们就能感受到这一点。当机器学习工程师会担心构建一个在生产环境中无缝工作的解决方案时,MLOps 工程师会努力构建机器学习工程师使用的平台或工具集。机器学习工程师会构建管道,但 MLOps 工程师可能会构建管道模板;机器学习工程师可能会使用持续集成/持续部署CI/CD)实践(关于这一点稍后会有更多介绍),但 MLOps 工程师将启用这种能力并定义最佳实践,以便顺利使用 CI/CD。最后,当机器学习工程师思考“我如何使用适当的工具和技术稳健地解决这个具体问题?”时,MLOps 工程师会问“我如何确保机器学习工程师和数据科学家能够一般性地解决他们需要解决的问题,以及我如何不断更新和改进这个设置?”

正如我们对数据科学家和机器学习工程师所做的那样,让我们定义 MLOps 工程师的一些关键关注领域:

  • 自动化:通过使用 CI/CD 和基础设施即代码IAC)等技术提高数据科学和机器学习工程工作流程的自动化水平。预包装软件可以部署,以通过这些能力以及更多功能(如自动化脚本或标准化模板)实现更平滑的解决方案部署。

  • 平台工程:致力于将一系列有用的服务整合在一起,以构建不同数据驱动团队使用的机器学习平台。这可以包括开发跨编排工具、计算和更多数据驱动服务的集成,直到它们成为机器学习工程师和数据科学家可以使用的整体。

  • 启用关键 MLOps 功能:MLOps 包括一系列实践和技术,使团队中的其他工程师能够生产化机器学习模型。模型管理和模型监控等能力应由 MLOps 工程师以可跨多个项目规模使用的方式启用。

应注意,本书中涵盖的一些主题可以由 MLOps 工程师执行,并且自然存在一些重叠。这不应让我们过于担忧,因为 MLOps 基于相当通用的实践和能力集,可以被多个角色所包含(参见图 1.1)。

数据工程师

数据工程师是那些负责以高保真度、适当的延迟,以及尽可能减少其他团队成员努力的情况下,将前述章节中所有基于 A 到 B 的商品获取到的人。没有数据,你无法创建任何类型的软件产品,更不用说机器学习产品了。

数据工程师的关键关注领域如下:

  • 质量:如果数据混乱、字段缺失或 ID 出错,从 A 到 B 的数据传输就毫无意义。数据工程师关心避免这种情况,并使用各种技术和工具,通常是为了确保离开源系统的数据是你数据存储层中到达的数据。

  • 稳定性:与质量方面的前一点类似,如果数据从 A 到 B,但只有在非雨天且是每周三的第二天才到达,那么这有什么意义呢?

    数据工程师投入大量时间和精力,并运用他们丰富的技能来确保数据管道健壮、可靠,并能够在承诺的时间内交付。

  • 访问:最后,从 A 到 B 获取数据的目标是使其被应用程序、分析和机器学习模型使用,因此 B 的性质很重要。数据工程师将手头有多种技术来展示数据,并且应该与数据消费者(包括我们的数据科学家和机器学习工程师等)合作,在这些解决方案中定义和创建适当的数据模型:

图 1.1 – 显示数据科学、机器学习工程和数据工程之间关系的图表

图 1.1:一个显示数据科学、机器学习工程和数据工程之间关系的图表。

如前所述,这本书主要关注机器学习工程师的工作以及你可以学习一些对这个角色有用的技能,但重要的是要记住,你不会在真空中工作。始终牢记其他角色的特征(以及在你项目团队中可能存在的许多其他未涵盖的角色),这样你才能最有效地共同工作。毕竟,数据科学是一项团队运动!

现在你已经了解了现代数据团队中的关键角色以及他们如何覆盖构建成功机器学习产品所需的活动范围,让我们看看你如何将它们组合起来以高效有效地工作。

作为有效团队的一员工作

在现代软件组织中,有许多不同的方法来组织团队并使他们有效地一起工作。我们将在第二章“机器学习开发过程”中介绍一些相关的项目管理方法,但在此期间,本节将讨论一些如果你参与组建团队或只是作为团队的一部分工作,你应该考虑的重要观点,这将帮助你成为一个有效的团队成员或领导者。

首先,始终牢记没有人能做所有的事情。你可以在那里找到一些非常有才华的人,但永远不要认为一个人能做你需要的一切,达到你所要求的水平。这不仅不切实际,而且是不良的做法,会负面影响你产品的质量。即使你在资源严重受限的情况下,关键也是让你的团队成员保持激光般的专注以取得成功。

其次,混合是最好的选择。我们都知道多样性对于组织和团队的一般好处,当然,这也应该适用于你的机器学习团队。在一个项目中,你需要数学、代码、工程、项目管理、沟通以及各种其他技能来取得成功。所以,根据前面的观点,确保你在团队中至少在一定程度上涵盖这些技能。

第三,以动态的方式将你的团队结构与项目联系起来。如果你参与的项目主要是关于将数据放在正确的位置,而实际的机器学习模型非常简单,那么将你的团队特征集中在工程和数据建模方面。如果项目需要详细了解模型,并且它相当复杂,那么重新定位你的团队以确保这一点得到覆盖。这既合理又能够释放那些本可以未充分利用的团队成员去从事其他项目。

例如,假设你被分配了一个任务,即构建一个系统,用于在数据进入你那光鲜亮丽的新数据湖时对客户数据进行分类,并且已经决定通过流式应用程序在数据摄入点进行这一操作。分类已经为另一个项目构建好了。很明显,这个解决方案将大量涉及数据工程师和机器学习工程师的技能,但不会太多涉及数据科学家,因为这部分工作已经在另一个项目中完成了。

在下一节中,我们将探讨在将你的团队部署到现实世界的商业问题时需要考虑的一些重要点。

现实世界的机器学习工程

我们中的大多数人在机器学习、分析和相关学科工作,这些工作是在具有各种不同结构和动机的组织中进行的。这些可能是盈利性公司、非盈利组织、慈善机构,或者是政府或大学等公共部门组织。在几乎所有这些情况下,我们都不是在真空中工作,也没有无限的预算或资源。因此,考虑在现实世界中做这类工作的一些重要方面是很重要的。

首先,你工作的最终目标是创造价值。这可以通过各种方式计算和定义,但本质上你的工作必须以某种方式改善公司或其客户,从而证明所投入的投资是合理的。这就是为什么大多数公司不会高兴地看到你花一年的时间去玩新工具,然后什么具体成果都没有,或者整天只阅读最新的论文。是的,这些事情是任何技术工作的一部分,它们肯定可以非常有趣,但你必须战略性地考虑如何分配你的时间,并且始终意识到你的价值主张。

其次,要成为现实世界中的成功机器学习工程师,你不仅需要理解技术,还必须理解业务。你必须了解公司日常是如何运作的,你必须了解公司的不同部分是如何相互配合的,你必须了解公司的人和他们的角色。最重要的是,你必须理解客户,包括业务和你的工作。如果你不知道你为谁构建的人的动机、痛苦和需求,那么你怎么能期望构建正确的东西呢?

最后,这可能有些争议,但你在现实世界中成为一名成功的机器学习工程师最重要的技能是这本书不会教给你的,那就是有效沟通的能力。你将不得不与团队一起工作,与经理、更广泛的社区和商业界,当然还有上述提到的客户一起工作。如果你能这样做,并且你了解技术和技巧(其中许多在本书中讨论过),那么还有什么能阻止你呢?

但在现实世界中,你可以用 ML 解决哪些类型的问题呢?好吧,让我们从一个可能具有争议性的声明开始:很多时候,ML 并不是答案。鉴于这本书的标题,这可能会显得有些奇怪,但了解何时不应用 ML 与了解何时应用 ML 同样重要。这将节省你大量的昂贵开发和资源时间。

当你想更快、更精确地完成半常规任务,或者在其他解决方案无法达到的更大规模上完成任务时,ML 是理想的。

在下表中给出了几个典型的例子,并讨论了机器学习(ML)是否是解决该问题的合适工具:

需求ML 是否合适?详情
能源定价信号的异常检测。合适你可能需要在大量可能随时间信号变化的点上执行此操作。
在 ERP 系统中提高数据质量。不合适这听起来更像是一个流程问题。你可以尝试应用 ML,但通常最好是使数据录入过程更加自动化或使流程更加稳健。
预测仓库物品消耗。合适ML 将能够比人类更准确地完成这项工作,因此这是一个很好的应用领域。
为商业审查总结数据。可能这可能需要大规模执行,但这不是一个 ML 问题——简单的数据查询就可以完成。

表 1.1:ML 的潜在应用案例。

如此简单的例子表(希望)开始清楚地表明,ML 确实是答案的情况通常是那些可以很好地被构建为数学或统计问题的情况。毕竟,这就是 ML 的本质——一系列基于数学的算法,可以根据数据迭代一些内部参数。在现代世界中,随着深度学习或强化学习等领域的发展,我们之前认为很难为标准 ML 算法适当表述的问题现在可以解决。

在现实世界中需要警惕的另一个趋势(与让我们用 ML 做一切的趋势相伴随)是人们对 ML 会抢走他们的工作以及不应信任 ML 的担忧。这是可以理解的:普华永道(PwC)2018 年的一份报告建议,到 2030 年代,30%的英国工作将受到自动化的影响(机器人真的会偷走我们的工作吗?)。在与同事和客户合作时,你必须努力阐明你正在构建的是为了补充和增强他们的能力,而不是取代他们。

让我们通过回顾一个重要观点来结束本节:你为一家公司工作的事实,当然意味着游戏的目标是创造与投资相称的价值。换句话说,你需要展示良好的投资回报率ROI)。这对你实际上意味着几件事:

  • 你必须了解不同的设计需要不同水平的投资。如果你可以通过在一个月内用 GPU 全天候训练一百万张图片来解决你的问题,或者你知道你可以在几小时内使用一些基本的聚类和一些标准硬件上的少量统计来解决相同的问题,你应该选择哪一个?

  • 你必须清楚你将产生的价值。这意味着你需要与专家合作,并尝试将你的算法结果转化为实际美元价值。这比听起来要困难得多,所以你需要花时间去正确完成它。而且,永远不要过度承诺。你应该总是承诺少一些,交付多一些

采用并不保证。即使是在公司内部为同事构建产品,你也必须明白,你的解决方案每次在使用后都会被测试。如果你构建的是质量低劣的解决方案,那么人们就不会使用它们,你所做的一切的价值主张也将开始消失。

现在你已经了解了使用机器学习解决商业问题时的一些重要要点,让我们来探讨这些解决方案可能是什么样子。

机器学习解决方案是什么样的?

当你想到机器学习工程时,你可能会默认想象在语音助手和视觉识别应用程序上工作(我在前面的页面上也陷入了这种陷阱——你注意到了吗?)。然而,机器学习的力量在于,只要有数据和合适的问题,它就能帮助并成为解决方案的关键部分。

一些例子可能有助于使这一点更清晰。当你输入一条短信,你的手机建议下一个单词时,它很可能是在使用底下的自然语言模型。当你滚动任何社交媒体信息流或观看流媒体服务时,推荐算法正在加倍工作。如果你开车旅行,一个应用程序预测你何时可能到达目的地,那么将会有某种回归在工作。你的贷款申请通常会导致你的特征和申请细节通过一个分类器。这些应用不是新闻中大声宣扬的(也许除了它们出问题时),但它们都是精心设计的机器学习工程的例子。

在这本书中,我们将要处理的例子将更像是这些——在产品和业务中每天都会遇到的典型机器学习场景。这些是如果你能自信地构建它们,将使你成为任何组织的宝贵资产。

我们应该从考虑任何机器学习解决方案应包含的广泛元素开始,如下面的图所示:

图片 B19525_01_02.png

图 1.2:任何机器学习解决方案的一般组件或层及其负责的内容。

您的存储层构成了数据工程过程的终点和机器学习过程的起点。它包括您的训练数据、运行模型的结果、您的工件和重要的元数据。我们还可以将这一层视为包括您存储的代码。

计算层是发生“魔法”的地方,也是本书大部分关注的焦点。这是训练、测试、预测和转换(主要是)发生的地方。本书的宗旨是使这一层尽可能工程化,并与其他层进行接口。

你可以将这一层分解为以下工作流程中所示的部分:

图 1.4 – 计算层的关键元素

图 1.3:计算层的关键元素。

重要提示

详细内容将在本书的后续部分讨论,但这一点强调了这样一个事实:在基本层面上,任何机器学习解决方案的计算过程实际上只是关于接收一些数据并输出一些数据。

应用层是您与其他系统共享机器学习解决方案结果的地方。这可能包括从应用程序数据库插入到 API 端点、消息队列或可视化工具等。这是您的客户最终使用结果的地方,因此您必须设计系统以提供干净、易于理解的输出,我们将在稍后讨论这一点。

简而言之,就是这样。我们将在稍后详细讨论所有这些层和点,但现在,只需记住这些广泛的概念,你就会开始理解所有详细的技术部件是如何结合在一起的。

为什么选择 Python?

在深入探讨更详细的主题之前,讨论为什么选择 Python 作为本书的编程语言是很重要的。以下所有涉及高级主题的内容,如架构和系统设计,都可以应用于使用任何或多种语言的解决方案,但 Python 在这里被单独提出,有以下几个原因。

Python 通常被称为数据的“通用语言”。它是一种非编译的、非强类型的、多范式的编程语言,具有清晰简单的语法。其工具生态系统也非常广泛,尤其是在分析和机器学习领域。

如 scikit-learn、numpy、scipy 以及许多其他软件包构成了全球大量技术和科学发展的基础。几乎每个用于数据世界的重大新软件库都有一个 Python API。根据写作时的TIOBE 指数www.tiobe.com/tiobe-index/),Python 是世界上最受欢迎的编程语言(2023 年 8 月)。

因此,能够使用 Python 构建你的系统意味着你将能够利用这个生态系统中的所有优秀的机器学习和数据科学工具,同时确保你构建的应用程序可以与其他软件良好地协同工作。

高级机器学习系统设计

当你深入到构建解决方案的细节时,工具、技术和方法的选择如此之多,以至于很容易感到不知所措。然而,正如前几节所暗示的,很多这种复杂性可以通过一些信封背面的架构和设计来抽象化,从而理解更大的图景。一旦你知道你将尝试解决的问题,这总是一个有用的练习,而且我建议你在做出任何关于实施的详细选择之前就做这件事。

为了让你了解这在实践中是如何工作的,以下是一些经过详细分析的例子,其中一支团队必须为一些典型的商业问题创建一个高级机器学习系统设计。这些问题与我之前遇到的问题相似,也可能会与你自己在工作中遇到的问题相似。

示例 1:批量异常检测服务

你为一家技术娴熟的出租车公司工作,该公司拥有数千辆汽车。该组织希望开始使行程时间更加一致,并了解更长的旅程,以便改善客户体验,从而提高客户保留率和回头客。你的机器学习团队被雇佣来创建一个异常检测服务,以寻找具有不寻常的行程时间或行程长度行为的行程。你们开始工作,数据科学家发现,如果你使用行程距离和时间特征对行程集进行聚类,你可以清楚地识别出值得运营团队调查的异常值。数据科学家在获得批准将此开发成一项服务之前,向 CTO 和其他利益相关者展示了研究结果,该服务将在公司内部分析工具的主要表中提供一个新字段,作为异常标志。

在这个例子中,我们将模拟一些数据,以展示出租车公司的数据科学家如何进行。在本书的存储库中,该存储库可以在github.com/PacktPublishing/Machine-Learning-Engineering-with-Python-Second-Edition找到,如果你导航到Chapter01文件夹,你会看到一个名为clustering_example.py的脚本。如果你已经通过mlewp-chapter01.yml环境文件激活了提供的conda环境,那么你可以使用以下命令运行此脚本:

python3 clustering_example.py 

运行成功后,你应该会看到创建了三个文件:taxi-rides.csvtaxi-labels.jsontaxi-rides.pngtaxi-rides.png中的图像应该类似于图 1.4 中所示。

我们将逐步说明这个脚本是如何构建的:

  1. 首先,让我们定义一个函数,该函数将根据numpy中给出的随机分布模拟一些行程距离,并返回一个包含结果的numpy数组。重复行的原因是为了在数据中创建一些基本行为和异常,并且你可以清楚地与我们在下一步将为每辆出租车集合生成的速度进行比较:

    import numpy as np
    from numpy.random import MT19937
    from numpy.random import RandomState, SeedSequence
    rs = RandomState(MT19937(SeedSequence(123456789)))
    
    # Define simulate ride data function
    def simulate_ride_distances():
        ride_dists = np.concatenate(
            (
                10 * np.random.random(size=370),
                30 * np.random.random(size=10), # long distances
                10 * np.random.random(size=10), # same distance
                10 * np.random.random(size=10) # same distance
            )
        )
        return ride_dists 
    
  2. 我们现在可以对速度做完全相同的事情,并且再次将出租车分成370101010的集合,这样我们就可以创建一些具有“典型”行为的数据和一些异常集合,同时允许与distances函数的值进行清晰的匹配:

    def simulate_ride_speeds():
        ride_speeds = np.concatenate(
            (
                np.random.normal(loc=30, scale=5, size=370),
                np.random.normal(loc=30, scale=5, size=10),
                np.random.normal(loc=50, scale=10, size=10),
                np.random.normal(loc=15, scale=4, size=10) 
            )
        )
        return ride_speeds 
    
  3. 我们现在可以在一个函数内部使用这两个辅助函数,该函数将调用它们并将它们组合起来,以创建包含行程 ID、速度、距离和时间的模拟数据集。结果以pandas DataFrame 的形式返回,用于建模:

    def simulate_ride_data():
        # Simulate some ride data …
        ride_dists = simulate_ride_distances()
        ride_speeds = simulate_ride_speeds()
        ride_times = ride_dists/ride_speeds
        # Assemble into Data Frame
        df = pd.DataFrame(
            {
              'ride_dist': ride_dists,
              'ride_time': ride_times,
              'ride_speed': ride_speeds
            }
        )
        ride_ids = datetime.datetime.now().strftime("%Y%m%d") +\
                   df.index.astype(str)
        df['ride_id'] = ride_ids
        return df 
    
  4. 现在,我们来到了数据科学家在其项目中产生的核心内容,这是一个简单的函数,它封装了一些sklearn代码,以返回一个包含聚类运行元数据和结果的字典。

    我们在这里包括相关的导入,以便于使用:

    from sklearn.preprocessing import StandardScaler
    from sklearn.cluster import DBSCAN
    from sklearn import metrics 
    
    def cluster_and_label(data, create_and_show_plot=True):
        data = StandardScaler().fit_transform(data)
        db = DBSCAN(eps=0.3, min_samples=10).fit(data)
        # Find labels from the clustering
        core_samples_mask = np.zeros_like(db.labels_,dtype=bool)
        core_samples_mask[db.core_sample_indices_] = True
        labels = db.labels_
        # Number of clusters in labels, ignoring noise if present.
        n_clusters_ = len(set(labels)) -
                             (1 if -1 in labels else 0)
        n_noise_ = list(labels).count(-1)
        run_metadata = {
            'nClusters': n_clusters_,
            'nNoise': n_noise_,
            'silhouetteCoefficient':
             metrics.silhouette_score(data, labels),
            'labels': labels,
        }
        if create_and_show_plot:
            plot_cluster_results(data, labels, core_samples_mask,
                                 n_clusters_)
        else:
            pass
        return run_metadata 
    

    注意,步骤 4中的函数利用了以下所示的绘图实用函数:

    import matplotlib.pyplot as plt
    
    def plot_cluster_results(data, labels, core_samples_mask,
                             n_clusters_):
        fig = plt.figure(figsize=(10, 10))
        # Black removed and is used for noise instead.
        unique_labels = set(labels)
        colors = [plt.cm.cool(each) for each in np.linspace(0, 1,
                  len(unique_labels))]
        for k, col in zip(unique_labels, colors):
            if k == -1:
                # Black used for noise.
                col = [0, 0, 0, 1]
            class_member_mask = (labels == k)
            xy = data[class_member_mask & core_samples_mask]
            plt.plot(xy[:, 0], xy[:, 1], 'o',
                     markerfacecolor=tuple(col),
                     markeredgecolor='k', markersize=14)
            xy = data[class_member_mask & ~core_samples_mask]
            plt.plot(xy[:, 0], xy[:, 1], '^',
                     markerfacecolor=tuple(col),
                     markeredgecolor='k', markersize=14)
        plt.xlabel('Standard Scaled Ride Dist.')
        plt.ylabel('Standard Scaled Ride Time')
        plt.title('Estimated number of clusters: %d' % n_clusters_)
        plt.savefig('taxi-rides.png') 
    

    最后,所有这些内容都在程序的入口点汇总,如下所示:

    import logging
    logging.basicConfig()
    logging.getLogger().setLevel(logging.INFO)
    
    if __name__ == "__main__":
        import os
        # If data present, read it in
        file_path = 'taxi-rides.csv'
        if os.path.exists(file_path):
            df = pd.read_csv(file_path)
        else:
            logging.info('Simulating ride data')
            df = simulate_ride_data()
            df.to_csv(file_path, index=False)
        X = df[['ride_dist', 'ride_time']]
    
        logging.info('Clustering and labelling')
        results = cluster_and_label(X, create_and_show_plot=True)
        df['label'] = results['labels']
    
        logging.info('Outputting to json ...')
        df.to_json('taxi-labels.json', orient='records') 
    

此脚本运行后,会创建一个数据集,显示每个模拟的出租车行程及其聚类标签在taxi-labels.json中,以及模拟数据集在taxi-rides.csv中,以及显示聚类结果的taxi-rides.png,如图 1.4 所示。

图 1.5 – 对一些出租车行程数据进行聚类分析的结果示例集

图 1.4:对一些出租车行程数据进行聚类分析的结果示例集。

现在你已经有一个基本模型可以工作,你必须开始考虑如何将其纳入一个工程化的解决方案——你该如何做?

嗯,由于这里的解决方案将支持另一个团队进行更长时间的调查,因此不需要一个非常低延迟的解决方案。利益相关者同意,聚类的见解可以在每天结束时提供。与团队的数据科学部分合作,ML 工程师(由你领导)了解到,如果每天运行聚类,这将提供足够的数据来生成适当的聚类,但更频繁的运行可能会由于数据量较小而导致结果较差。因此,达成一致意见,采用每日批量处理流程。

下一个问题是如何安排这个运行?嗯,你需要一个编排层,这是一个工具或工具集,它将使你能够安排和管理预定义的工作。像 Apache Airflow 这样的工具可以做到这一点。

接下来你该做什么呢?嗯,你知道运行频率是每天一次,但数据量仍然非常高,因此利用分布式计算模式是有意义的。两个选项立刻浮现在脑海中,并且团队中存在这些技能,Apache Spark 和 Ray。为了尽可能减少对底层基础设施的耦合并最小化对代码重构的需求,你决定使用 Ray。你知道数据的最终消费者是一个 SQL 数据库中的表,因此你需要与数据库团队合作设计一个适当的结果交接方案。由于安全和可靠性方面的考虑,直接写入生产数据库不是一个好主意。因此,你同意使用云中的另一个数据库作为数据的中间暂存区域,主数据库可以在其每日构建中查询这些数据。

在这里可能看起来我们并没有进行任何技术性的工作,但实际上,你已经为你的项目完成了高级的系统设计。这本书的剩余部分将告诉你如何填补以下图表中的空白!

图 1.6 – 示例 1 工作流程

图 1.5:示例 1 工作流程。

现在让我们继续下一个例子!

示例 2:预测 API

在这个例子中,你为一家大型零售连锁企业的物流部门工作。为了最大化货物流通,公司希望帮助区域物流规划师在特别繁忙的时期提前做好准备,避免产品售罄。在与业务中的利益相关者和领域专家讨论后,一致认为规划师能够通过一个托管在网页上的仪表板动态请求和探索特定仓库项目的预测,这是最优的。这允许规划师在下单前了解可能的需求轮廓。

数据科学家再次表现出色,发现任何单个商店层面的数据都具有非常可预测的行为。他们决定使用 Facebook Prophet 库进行建模,以帮助加快训练多个不同模型的过程。在下面的示例中,我们将展示他们如何做到这一点,但我们不会花费时间优化模型以创建最佳的预测性能,因为这只是为了说明目的。

这个示例将使用 Kaggle API 来检索一系列不同零售店销售的示例数据集。在书库下的Chapter01/forecasting目录中有一个名为forecasting_example.py的脚本。如果您已经正确配置了 Python 环境,您可以在命令行使用以下命令运行此示例:

python3 forecasting_example.py 

脚本首先下载数据集,对其进行转换,然后使用它来训练一个 Prophet 预测模型,在测试集上进行预测并保存绘图。如前所述,这只是为了说明目的,因此不会创建验证集或执行比 Prophet 库提供的默认参数更复杂的超参数调整。

为了帮助您了解这个示例是如何组合起来的,我们现在将分解脚本的不同组件。为了简洁起见,这里排除了纯粹用于绘图或记录的功能:

  1. 如果我们查看脚本的主要部分,我们可以看到第一步都是关于读取数据集,如果它已经在正确的目录中,或者下载并读取它:

    import pandas as pd
    
    if __name__ == "__main__":
        import os
        file_path = train.csv
        if os.path.exists(file_path):
            df = pd.read_csv(file_path)
        else:
            download_kaggle_dataset()
            df = pd.read_csv(file_path) 
    
  2. 执行下载的函数使用了 Kaggle API,如下所示;您可以通过参考 Kaggle API 文档来确保正确设置(这需要一个 Kaggle 账户):

    import kaggle
    
    def download_kaggle_dataset( kaggle_dataset: str ="pratyushakar/
                                 rossmann-store-sales" ) -> None:
        api = kaggle.api
        kaggle.api.dataset_download_files(kaggle_dataset, path="./",
                                          unzip=True, quiet=False) 
    
  3. 接下来,脚本调用了一个名为prep_store_data的函数来转换数据集。这个函数使用两个默认值调用,一个用于商店 ID,另一个指定我们只想看到商店开门时的数据。该函数的定义如下:

    def prep_store_data(df: pd.DataFrame, 
                        store_id: int = 4, 
                        store_open: int = 1) -> pd.DataFrame:
        df['Date'] = pd.to_datetime(df['Date'])
        df.rename(columns= {'Date':'ds','Sales':'y'}, inplace=True)
        df_store = df[
            (df['Store'] == store_id) & 
            (df['Open'] == store_open)
            ].reset_index(drop=True)
        return df_store.sort_values('ds', ascending=True) 
    
  4. 预测模型 Prophet 随后在数据的第一个 80%上训练,并对剩余的 20%数据进行预测。为了指导模型的优化,向模型提供了季节性参数:

    seasonality = {
        'yearly': True,
        'weekly': True,
        'daily': False
    }
    predicted, df_train, df_test, train_index = train_predict(
        df = df,
        train_fraction = 0.8,
        seasonality=seasonality
    ) 
    

    train_predict方法的定义如下,您可以看到它封装了一些进一步的数据准备和 Prophet 包的主要调用:

    def train_predict(df: pd.DataFrame, train_fraction: float, 
                      seasonality: dict) -> tuple[
                          pd.DataFrame,pd.DataFrame,pd.DataFrame, int]:
        train_index = int(train_fraction*df.shape[0])
        df_train = df.copy().iloc[0:train_index]
        df_test = df.copy().iloc[train_index:]
        model=Prophet(
            yearly_seasonality=seasonality['yearly'],
            weekly_seasonality=seasonality['weekly'],
            daily_seasonality=seasonality['daily'],
            interval_width = 0.95
        )
        model.fit(df_train)
        predicted = model.predict(df_test)
        return predicted, df_train, df_test, train_index 
    
  5. 然后,最后,调用了一个实用绘图函数,当运行时将创建如图 1.6所示的输出。这显示了测试数据集预测的放大视图。由于上述讨论的简洁性,这里没有给出该函数的详细信息:

    plot_forecast(df_train, df_test, predicted) 
    

图 1.6:预测商店销售。

这里有一个问题,那就是为每个商店实施上述预测模型,如果连锁店收集到足够的数据,很快就会导致数百甚至数千个模型。另一个问题是,公司使用的资源规划系统尚未覆盖所有商店,因此一些规划者希望检索与他们自己的商店相似的其他商店的预测。大家一致认为,如果用户喜欢探索他们认为与自己的数据相似的地区概况,那么他们仍然可以做出最佳决策。

考虑到这一点和客户对动态、即兴请求的要求,你很快就会排除完整的批量处理。这不会涵盖核心系统之外的地区的用例,也不会允许通过网站动态检索最新的预测,这将允许你部署在未来不同时间范围内进行预测的模型。这也意味着你可以节省计算资源,因为你不需要每天管理数千个预测的存储和更新,你的资源可以专注于模型训练。

因此,你决定,实际上,一个可以按需返回用户所需预测的端点的托管在网站上的 API 是最有意义的。为了提供高效的响应,你必须考虑典型用户会话中会发生什么。通过与仪表板的潜在用户进行工作坊,你很快就会意识到,尽管请求是动态的,但大多数规划者将专注于任何一次会话中特定感兴趣的项目。他们也不会查看很多地区。然后你决定,有一个缓存策略是有意义的,其中你将某些你认为可能常见的请求取出来,并在应用程序中缓存以供重用。

这意味着在用户做出第一次选择后,结果可以更快地返回,从而提供更好的用户体验。这导致了一个粗略的系统草图,如图 1.7 所示:

图 1.8 – 示例 2 工作流程

图 1.7:示例 2 工作流程。

接下来,让我们看看最后的例子。

示例 3:分类流程

在这个最后的例子中,你为一家基于网络的公司工作,该公司希望根据用户的用法模式对用户进行分类,作为不同类型广告的目标,以便更有效地定位营销支出。例如,如果用户使用网站的频率较低,我们可能想通过更激进的折扣来吸引他们。业务的关键要求之一是最终结果成为其他应用程序使用的数据存储中的数据的一部分。

根据这些要求,您的团队确定运行分类模型的管道是满足所有条件的简单解决方案。数据工程师将精力集中在构建数据摄取和数据存储基础设施上,而机器学习工程师则致力于封装数据科学团队在历史数据上训练的分类模型。数据科学家确定的基础算法在sklearn中实现,我们将在下面通过将其应用于与这个用例类似的市场数据集来处理它:

这个假设的例子与许多经典数据集相吻合,包括来自 UCI ML 存储库的银行营销数据集:archive.ics.uci.edu/ml/datasets/Bank+Marketing#。与之前的例子一样,有一个可以从命令行运行的脚本,这次在Chapter01/classifying文件夹中,名为classify_example.py

python3 classify_example.py 

运行此脚本将读取下载的银行数据,重新平衡训练数据集,然后在随机网格搜索中对随机森林分类器执行超参数优化运行。与之前类似,我们将展示这些部分是如何工作的,以展示数据科学团队可能如何处理这个问题:

  1. 脚本的主要部分包含所有相关步骤,这些步骤被巧妙地封装成我们将在接下来的几个步骤中剖析的方法:

    if __name__ == "__main__":
        X_train, X_test, y_train, y_test = ingest_and_prep_data()
        X_balanced, y_balanced = rebalance_classes(X_train, y_train)
        rf_random = get_randomised_rf_cv(
                      random_grid=get_hyperparam_grid()
                      )
        rf_random.fit(X_balanced, y_balanced) 
    
  2. 下面的ingest_and_prep_data函数,它假定bank.csv数据存储在当前文件夹中名为bank_data的目录中。它将数据读入一个pandas DataFrame,然后在数据上执行训练集-测试集分割,并对训练特征进行独热编码,最后返回所有训练和测试特征及目标。与其他示例一样,本书将解释这些概念和工具,尤其是在第三章从模型到模型工厂中:

    def ingest_and_prep_data(
            bank_dataset: str = 'bank_data/bank.csv'
            ) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame,
                       pd.DataFrame]:
        df = pd.read_csv('bank_data/bank.csv', delimiter=';',
                          decimal=',')
    
        feature_cols = ['job', 'marital', 'education', 'contact',
                        'housing', 'loan', 'default', 'day']
        X = df[feature_cols].copy()
        y = df['y'].apply(lambda x: 1 if x == 'yes' else 0).copy()
        X_train, X_test, y_train, y_test = train_test_split(X, y, test_
                                              size=0.2, random_state=42)
        enc = OneHotEncoder(handle_unknown='ignore')
        X_train = enc.fit_transform(X_train)
        return X_train, X_test, y_train, y_test 
    
  3. 由于数据不平衡,我们需要使用过采样技术重新平衡训练数据。在这个例子中,我们将使用来自imblearn包的合成少数过采样技术SMOTE):

    def rebalance_classes(X: pd.DataFrame, y: pd.DataFrame
                          ) -> tuple[pd.DataFrame, pd.DataFrame]:
        sm = SMOTE()
        X_balanced, y_balanced = sm.fit_resample(X, y)
        return X_balanced, y_balanced 
    
  4. 现在我们将进入脚本的主体 ML 组件。我们将执行超参数搜索(关于这一点,将在第三章从模型到模型工厂中详细介绍),因此我们必须定义一个搜索网格:

    def get_hyperparam_grid() -> dict:
        n_estimators = [int(x) for x in np.linspace(start=200,
                        stop=2000, num=10)]
        max_features = ['auto', 'sqrt']
        max_depth = [int(x) for x in np.linspace(10, 110, num=11)]
        max_depth.append(None)
        min_samples_split = [2, 5, 10]
        min_samples_leaf = [1, 2, 4]
        bootstrap = [True, False]  # Create the random grid
        random_grid = {
            'n_estimators': n_estimators,
            'max_features': max_features,
            'max_depth': max_depth,
            'min_samples_split': min_samples_split,
            'min_samples_leaf': min_samples_leaf,
            'bootstrap': bootstrap
        }
        return random_grid 
    
  5. 最后,这个超参数网格将被用于定义一个RandomisedSearchCV对象,它允许我们在超参数值上优化估计量(在这里,是一个RandomForestClassifier):

    def get_randomised_rf_cv(random_grid: dict) -> sklearn.model_
                                   selection._search.RandomizedSearchCV:
        rf = RandomForestClassifier()
        rf_random = RandomizedSearchCV(
            estimator=rf,
            param_distributions=random_grid,
            n_iter=100,
            cv=3,
            verbose=2,
            random_state=42,
            n_jobs=-1,
            scoring='f1'
        )
        return rf_random 
    

上面的例子突出了创建典型分类模型的基本组件,但作为工程师,我们必须问自己,“接下来是什么?”很明显,我们必须实际运行已经生成的模型进行预测,因此我们需要将其持久化并稍后读取。这与本章讨论的其他用例类似。在这里更具挑战性的是,工程师可能实际上会考虑不在批量或请求-响应场景中运行,而是在流式场景中运行。这意味着我们必须考虑新的技术,如Apache Kafka,它允许你发布和订阅“主题”,在这些“主题”中可以共享称为“事件”的数据包。不仅如此,我们还需要就如何使用机器学习模型以这种方式与数据交互做出决策,提出关于适当模型托管机制的疑问。还有关于你希望多频繁地重新训练你的算法以确保分类器不过时的微妙之处。在考虑延迟或监控模型在这种非常不同的环境中的性能问题之前,这些都是需要考虑的。正如你所看到的,这意味着机器学习工程师在这里的工作相当复杂。图 1.8将这些复杂性归纳为一个非常高级的图表,它将帮助你开始考虑如果你是这个项目的工程师,你需要构建的系统交互类型。

在这本书中,我们不会过多地详细讨论流式处理,但我们将详细讨论所有其他关键组件,这些组件将帮助你将这个示例构建成一个真正的解决方案。有关流式机器学习应用的更多详细信息,请参阅 Joose Korstanje 所著的《Python 流式数据处理机器学习》,Packt 出版社,2022 年。

图 1.9 – 示例 3 工作流程

图 1.8:示例 3 工作流程。

我们现在已经探讨了三种高级机器学习系统设计,并讨论了我们工作流程选择背后的理由。我们还详细探讨了数据科学家在建模过程中通常会产生的代码类型,但这些代码将作为未来机器学习工程工作的输入。因此,本节应该让我们对在典型项目中我们的工程工作从哪里开始以及我们旨在解决哪些类型的问题有了认识。就这样,你已经开始了成为机器学习工程师的道路!

摘要

在本章中,我们介绍了机器学习工程的概念以及它如何适应基于数据的现代团队构建有价值的解决方案。讨论了机器学习工程的重点如何与数据科学和数据工程的优点相辅相成,以及这些学科的重叠之处。还提出了一些关于如何利用这些信息为你的项目组建适当资源的团队的评论。

随后讨论了在现代现实世界中构建机器学习产品的挑战,并提供了帮助你克服这些挑战的指导。特别是,强调了合理评估价值和有效与利益相关者沟通的观念。

本章随后通过讨论典型机器学习解决方案的外观以及它们应该如何设计(在高级别)来应对一些常见用例,为后续章节即将介绍的技术内容做了简要介绍。

在我们深入本书的其余部分之前,这些主题很重要,因为它们将帮助你理解为什么机器学习工程是一门如此关键的学科,以及它是如何与以数据为中心的团队和组织的复杂生态系统联系起来的。这也有助于让你了解机器学习工程所涵盖的复杂挑战,同时为你提供一些概念工具,以开始对这些挑战进行推理。我的希望是,这不仅激励你参与本版其余部分的材料,而且还能让你走上探索和自学之路,这对于成为一名成功的机器学习工程师是必需的。

下一章将重点介绍如何设置和实施你的开发流程来构建你想要的机器学习解决方案,并提供一些见解,说明这与标准软件开发流程有何不同。然后,将讨论一些你可以使用的工具,以开始管理你的项目任务和工件,而不会造成重大头痛。这将为你准备在后续章节中构建你的机器学习解决方案关键元素的技术细节。

加入我们的 Discord 社区

加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

packt.link/mle

二维码

第二章:机器学习开发过程

在本章中,我们将定义如何将任何成功的机器学习ML)软件工程项目的工作进行划分。基本上,我们将回答您如何实际上组织一个成功的 ML 项目的问题。我们不仅将讨论流程和工作流程,还将为流程的每个阶段设置所需的工具,并通过真实的 ML 代码示例突出一些重要的最佳实践。

在这一版中,我们将更详细地介绍一个重要的数据科学和机器学习项目管理方法:跨行业标准数据挖掘流程CRISP-DM)。这包括讨论这种方法与传统敏捷和瀑布方法的比较,并提供一些将此方法应用于您的机器学习项目的技巧和窍门。还有更多详细的示例,帮助您使用 GitHub Actions 开始持续集成/持续部署CI/CD),包括如何运行专注于机器学习的流程,如自动模型验证。关于在交互式开发环境IDE)中启动的建议也已经变得更加工具无关,以便使用任何合适的 IDE。与之前一样,本章将重点介绍我提出的“四步”方法,该方法涵盖了您的机器学习项目的发现、玩耍、开发、部署工作流程。这个项目工作流程将与在数据科学领域非常流行的 CRISP-DM 方法进行比较。我们还将讨论适当的发展工具及其配置和集成,以确保项目成功。我们还将涵盖版本控制策略及其基本实施,以及为您的机器学习项目设置 CI/CD。然后,我们将介绍一些潜在的执行环境,作为您的机器学习解决方案的目标目的地。到本章结束时,您将为您的 Python 机器学习工程项目成功做好准备。这是我们将在后续章节中构建一切的基础。

如往常一样,我们将通过总结本章的主要观点并强调在阅读本书其余部分时这些观点的意义来结束本章。

最后,值得注意的是,尽管我们在这里将讨论框架定为 ML 挑战,但本章中您将学到的许多内容也可以应用于其他 Python 软件工程项目。我的希望是,在详细构建这些基础概念的投资将能够让您在所有工作中反复利用。

我们将在以下章节和子章节中探讨所有这些内容:

  • 设置我们的工具

  • 从概念到解决方案的四个步骤:

    • 发现

    • 玩耍

    • 开发

    • 部署

有许多令人兴奋的内容需要消化和很多知识需要学习——让我们开始吧!

技术要求

如同 第一章机器学习工程简介 中所述,如果您想运行这里提供的示例,您可以使用本书 GitHub 仓库 Chapter02 文件夹中提供的环境 YAML 文件创建一个 Conda 环境:

conda env create –f mlewp-chapter02.yml 

此外,本章中的许多示例将需要使用以下软件和包。这些也将为你在本书其他部分的示例提供良好的基础:

  • Anaconda

  • PyCharm Community Edition,VS Code 或其他兼容 Python 的 IDE

  • Git

您还需要以下内容:

  • 一个 Atlassian Jira 账户。我们将在本章后面进一步讨论这个问题,但您可以在www.atlassian.com/software/jira/free免费注册一个账户。

  • 一个 AWS 账户。这将在本章中讨论,但您可以在aws.amazon.com/注册一个账户。注册 AWS 需要添加付款详情,但本书中我们将只使用免费层解决方案。

本章中的技术步骤都在运行 Ubuntu 22.04 LTS 的 Linux 机器上进行了测试,该机器有一个具有管理员权限的用户配置文件,以及一个按照 第一章机器学习工程简介 中描述的设置运行的 Macbook Pro M2。如果您在运行不同系统上的步骤时,如果步骤没有按预期工作,您可能需要查阅该特定工具的文档。即使如此,大多数步骤对于大多数系统来说都将相同或非常相似。您还可以在本书的 GitHub 仓库github.com/PacktPublishing/Machine-Learning-Engineering-with-Python-Second-Edition/tree/main/Chapter02中查看本章的所有代码。该仓库还将包含进一步的资源,以帮助您将代码示例运行起来。

设置我们的工具

为了准备本章以及本书其余部分的工作,设置一些工具将会很有帮助。从高层次来看,我们需要以下工具:

  • 用于编码的地方

  • 用于跟踪我们的代码更改的内容

  • 用于帮助我们管理任务的内容

  • 用于配置基础设施和部署我们的解决方案的地方

让我们逐一看看如何处理这些问题:

  • 编写代码的地方:首先,尽管数据科学家选择编码的武器当然是 Jupyter Notebook,但一旦您开始向 ML 工程转型,拥有一个 IDE 将变得非常重要。IDE 基本上是一个包含一系列内置工具和功能的应用程序,可以帮助您开发出最好的软件。PyCharm是 Python 开发者的一个优秀例子,它提供了许多对 ML 工程师有用的插件、附加组件和集成。您可以从 JetBrains 下载社区版,网址为www.jetbrains.com/pycharm/。另一个流行的开发工具是轻量但强大的源代码编辑器 VS Code。一旦您成功安装了 PyCharm,您可以从欢迎使用 PyCharm窗口创建一个新项目或打开一个现有项目,如图图 2.1所示:图 2.1 – 打开或创建您的 PyCharm 项目

    图 2.1:打开或创建您的 PyCharm 项目。

  • 跟踪代码变更的内容:接下来在列表中是代码版本控制系统。在这本书中,我们将使用GitHub,但基于相同底层开源Git技术的解决方案有很多,所有这些解决方案都是免费提供的。后面的章节将讨论如何将这些工具作为您开发工作流程的一部分,但首先,如果您还没有设置版本控制系统,您可以导航到github.com并创建一个免费账户。按照网站上的说明创建您的第一个仓库,您将看到一个类似于图 2.2的屏幕。为了使您的生活更轻松,您应该选择添加 README 文件添加.gitignore(然后选择Python)。README 文件为您提供了一个初始的 Markdown 文件,以便您开始使用,并描述您的项目。.gitignore文件告诉您的 Git 分布忽略某些类型的文件,这些文件通常对版本控制不重要。您可以选择将仓库设置为公开或私有,以及您希望使用的许可证。这本书的仓库使用MIT 许可证图 2.2 – 设置您的 GitHub 仓库

    图 2.2:设置您的 GitHub 仓库。

    一旦您设置了 IDE 和版本控制系统,您需要通过使用 PyCharm 提供的 Git 插件使它们相互通信。这就像导航到VCS | 启用版本控制集成并选择Git一样简单。您可以通过导航到文件 | 设置 | 版本 控制来编辑版本控制设置;请参阅图 2.3

    图 2.3 – 使用 PyCharm 配置版本控制

    图 2.3:使用 PyCharm 配置版本控制。

  • 一些帮助我们管理任务的东西:您现在可以编写 Python 代码并跟踪您的代码更改,但您准备好与其他团队成员一起管理或参与一个复杂的项目了吗?为此,拥有一个可以跟踪任务、问题、错误、用户故事和其他文档和工作项的解决方案通常很有用。如果这个解决方案与其他您将使用的工具有良好的集成点,那就更好了。在这本书中,我们将使用Jira作为这个示例。如果您导航到www.atlassian.com/software/jira,您可以创建一个免费的云 Jira 账户,然后在该解决方案中遵循交互式教程来设置您的第一个看板并创建一些任务。图 2.4显示了本书项目(称为Python 机器学习工程MEIP)的任务板:

    图 2.4:本书在 Jira 中的任务板。

  • 一个用于部署基础设施和部署我们的解决方案的地方:您刚刚安装和设置的一切都是工具,这些工具将真正帮助您将工作流程和软件开发实践提升到下一个层次。最后一部分是拥有部署最终解决方案所需的工具、技术和基础设施。为应用程序管理计算基础设施(过去和现在通常仍然是)提供专门的团队,但随着公共云的出现,这种能力对于从事软件各个角色的员工来说已经实现了真正的民主化。特别是,现代机器学习工程非常依赖于云计算技术的成功实施,通常是通过主要的公共云提供商,如亚马逊网络服务AWS)、微软 Azure谷歌云平台GCP)。本书将利用 AWS 生态系统中的工具,但您在这里找到的所有工具和技术在其他云中都有等效项。

云带来的能力民主化的一面是,拥有其解决方案部署权的团队必须掌握新的技能和理解。我坚信“你建它,你拥有它,你运行它”的原则,但这意味着作为一个机器学习工程师,您将不得不熟悉大量潜在的新工具和原则,以及拥有您部署的解决方案的性能。权力越大,责任越大,诸如此类。在第五章部署模式和工具中,我们将详细探讨这个话题。

让我们来谈谈如何设置它。

设置 AWS 账户

如前所述,您不必使用 AWS,但我们将在这本书的整个过程中使用它。一旦在这里设置好,您就可以用它来做我们将会做的所有事情:

  1. 让我们使这更加具体。每个阶段的主要焦点和输出可以总结如下,如图 2.1 表所示:

  2. 一旦您创建了账户,您就可以导航到 AWS 管理控制台,在那里您可以查看所有可用的服务(见图 2.5):

图 2.5 – AWS 管理控制台

图 2.5:AWS 管理控制台。

在我们的 AWS 账户准备就绪后,让我们看看涵盖整个过程的四个步骤。

| 阶段 | 输出 |

所有机器学习项目在某种程度上都是独特的:组织、数据、人员、使用的工具和技术在任何两个项目中都不会完全相同。这是好事,因为它标志着进步,以及使这个领域如此有趣的自然多样性。

话虽如此,无论细节如何,从广义上讲,所有成功的机器学习项目实际上有很多共同之处。它们需要将业务问题转化为技术问题,进行大量的研究和理解,概念验证,分析,迭代,工作整合,最终产品的构建,以及将其部署到适当的环境。这就是机器学习工程的精髓!

进一步发展这一点,您可以将这些活动开始归类为粗略的类别或阶段,每个阶段的成果都是后续阶段必要的输入。这如图 2.6 所示:

图 2.6 – 任何机器学习项目在机器学习开发过程中所经历的各个阶段

图 2.6:任何机器学习项目在机器学习开发过程中所经历的各个阶段。

每个工作类别都有其独特的风味,但综合起来,它们构成了任何良好机器学习项目的骨架。接下来的几节将详细阐述每个类别的细节,并开始展示如何使用它们来构建您的机器学习工程解决方案。正如我们稍后将要讨论的,您也不必像这样分四步完成整个项目;您实际上可以为特定功能或整体项目的一部分逐个完成这些步骤。这将在选择软件开发方法部分中介绍。

要设置 AWS 账户,请导航到aws.amazon.com并选择创建账户。您需要添加一些付款详情,但本书中提到的所有内容都可以通过 AWS 的免费层进行探索,在那里您不会因消费低于一定阈值而产生费用。

四步从概念到解决方案
阶段输出
阶段详细理解数据。工作原理的概念验证。对解决问题所采用的模型/算法/逻辑达成一致。在现实资源场景中实现解决方案的证据。实现良好投资回报率的证据。
开发开发一个可以在适当和可用的基础设施上托管的工作解决方案。详尽的测试结果和性能指标(针对算法和软件)。达成一致的再培训和模型部署策略。单元测试、集成测试和回归测试。解决方案打包和管道。
部署一个工作且经过测试的部署流程。配备适当的安全性和性能特性的基础设施。模式再培训和管理工作流程。端到端的工作解决方案!

表 2.1:机器学习开发过程不同阶段的输出。

重要提示

你可能会认为机器学习工程师只需要真正考虑后两个阶段,开发部署,而早期阶段由数据科学家或甚至业务分析师负责。我们确实会在整本书中主要关注这些阶段,并且这种劳动分工可以非常有效。然而,如果你打算构建一个机器学习解决方案,理解所有之前的动机和开发步骤至关重要——你不了解你想要去哪里,难道会建造一种新的火箭吗?

与 CRISP-DM 的比较

我们将在本章的其余部分概述的项目步骤的高级分类与一个重要的方法论 CRISP-DM 有许多相似之处,也有一些不同。这个方法论于 1999 年发布,自那时起,它已成为理解如何构建任何数据项目的一种方式。在 CRISP-DM 中,有六个不同的活动阶段,涵盖了与上一节中描述的四个步骤类似的内容:

  1. 业务理解:这全部关于了解业务问题和领域。在四步模型中,这成为发现阶段的一部分。

  2. 数据理解:将业务领域的知识扩展到包括数据的状态、其位置以及它与问题的相关性。这也包括在发现阶段。

  3. 数据准备:开始获取数据并将其转换为下游使用。这通常需要迭代。在玩耍阶段进行捕捉。

  4. 建模:对准备好的数据进行处理,并在其上开发分析;这现在可能包括不同复杂程度的机器学习。这是一个在四步方法论中的玩耍开发阶段都会发生的活动。

  5. 评估:这一阶段关注的是确认解决方案是否满足业务需求,并对之前的工作进行全面的审查。这有助于确认是否有什么被忽略或可以改进的地方。这非常是开发部署阶段的一部分;在我们本章将描述的方法论中,这些任务在整个项目中都得到了很好的整合。

  6. 部署:在 CRISP-DM 中,这最初是专注于部署简单的分析解决方案,如仪表板或计划中的 ETL 管道,这些管道将运行已决定的分析模型。

    在模型机器学习工程的世界里,这一阶段可以代表这本书中提到的任何内容!CRISP-DM 建议在规划和审查部署方面有子阶段。

如您从列表中看到的,CRISP-DM 中的许多步骤涵盖了与我提出的四个步骤中概述的类似主题。CRISP-DM 在数据科学社区中非常受欢迎,因此其优点肯定得到了全世界大量数据专业人士的认可。鉴于这一点,您可能会想,“为什么还要开发其他的东西呢?”让我说服您为什么这是一个好主意。

CRISP-DM 方法论只是将任何数据项目的重要活动分组以提供结构的一种方式。您可能从上面我给出的阶段简要描述中看到,如果您进行进一步的研究,CRISP-DM 在用于现代机器学习工程项目的使用中可能存在一些潜在的缺点:

  • CRISP-DM 中概述的过程相对僵化且相当线性。这可以为提供结构带来好处,但可能会阻碍项目中的快速进展。

  • 该方法非常重视文档。大多数步骤都详细说明了编写某种类型的报告、审查或总结。在项目中编写和维护良好的文档至关重要,但过度编写文档也可能存在风险。

  • CRISP-DM 是在“大数据”和大规模机器学习出现之前的世界中编写的。对我来说,不清楚其细节是否仍然适用于这样一个不同的世界,在那里经典的提取-转换-加载模式只是众多模式之一。

  • CRISP-DM 无疑源自数据世界,并在最后阶段试图向可部署解决方案的概念迈进。这是值得赞扬的,但在我看来,这还不够。机器学习工程是一个不同的学科,因为它与经典软件工程的距离远比接近。这本书将反复论证这一点。因此,拥有一个将部署和开发的概念与软件和现代机器学习技术完全一致的方法非常重要。

四步法试图缓解这些挑战,并以不断参考软件工程和机器学习技能和技术的方式进行。这并不意味着你永远不应该在你的项目中使用 CRISP-DM;它可能正是完美的选择!就像这本书中介绍的大多数概念一样,重要的是要拥有许多工具在你的工具箱中,以便你可以选择最适合当前工作的那个。

因此,现在让我们详细地过一遍这四个步骤。

发现

在开始构建任何解决方案之前,了解你试图解决的问题至关重要。这项活动在商业分析中通常被称为发现,如果你的机器学习项目要取得成功,这是至关重要的。

在发现阶段需要做的关键事情如下:

  • 与客户沟通!然后再与他们沟通:如果你要设计和构建正确的系统,你必须详细了解最终用户的需求。

  • 记录一切:你将根据你满足要求的好坏来评判,所以请确保你的讨论中的所有关键点都得到了团队成员和客户或其适当代表的记录和批准。

  • 定义重要的指标:在项目开始时,很容易被冲昏头脑,感觉自己可以用即将构建的神奇新工具解决任何问题。尽可能强烈地抵制这种倾向,因为它很容易在以后造成严重的头痛。相反,将你的对话引导到定义一个或非常少的指标,这些指标定义了成功将是什么样子。

  • 开始找出数据在哪里!:如果你能开始确定你需要访问哪些系统来获取所需的数据,这将节省你以后的时间,并有助于你在项目脱轨之前发现任何重大问题。

使用用户故事

一旦你与客户(几次)交谈过,你就可以开始定义一些用户故事。用户故事是对用户或客户想要看到的内容以及该功能或工作单元的验收标准的简洁且格式一致的表述。例如,我们可能想根据第一章机器学习工程简介中的出租车行程示例定义一个用户故事:“作为我们内部网络服务的用户,我希望看到异常的出租车行程,并能够进一步调查。”

让我们开始吧!

  1. 要在 Jira 中添加此内容,请选择创建按钮。

  2. 然后,选择故事

  3. 然后,根据需要填写细节。

你现在已经将一个用户故事添加到你的工作管理工具中!这让你可以做诸如创建新任务并将它们链接到这个用户故事或更新其状态等事情:

图 2.8 – Jira 中的一个示例用户故事

图 2.7:Jira 中的一个示例用户故事。

你使用的数据源尤其重要,需要理解。正如你所知,“垃圾进,垃圾出”,或者更糟,“没有数据,就没有进展”!你必须回答的数据的特定问题主要集中在访问技术质量相关性上。

对于访问和技术,你试图预先了解数据工程师开始他们的工作流程需要做多少工作,以及这会耽误整个项目多少时间。因此,正确地完成这一点至关重要。

一个很好的例子是,如果你很快发现你需要的绝大部分数据都存在于没有真正现代 API 和没有非财务团队成员访问请求机制的遗留内部财务系统中。如果其主要后端是本地部署的,你需要将锁定在云端的财务数据迁移过来,但这会让你的业务感到紧张,那么你知道在编写第一行代码之前还有很多工作要做。如果数据已经存在于你的团队可以访问的企业数据湖中,那么你显然处于更好的位置。如果价值主张足够强大,任何挑战都是可以克服的,但尽早找出这些情况将为你节省时间、精力和金钱。

在启动之前,相关性可能更难找到,但你可以开始形成一些想法。例如,如果你想执行我们在第一章机器学习工程导论中讨论的库存预测,你是否需要拉取客户账户信息?如果你想创建高端非高端客户的分类器,作为营销目标,这也如第一章机器学习工程导论中提到的,你是否需要社交媒体数据?关于相关性的问题通常不会像这些例子那样明确,但一个重要的事情要记住的是,如果你真的错过了什么重要的东西,你总是可以回过头来。你试图尽早捕捉到最重要的设计决策,所以常识和大量的利益相关者和领域专家参与将大有裨益。

在项目前进之前,你可以尝试通过向当前的数据用户或消费者或参与其输入过程的人员提出一些问题来预测数据质量。但要获得更定量的理解,你通常只需要让你的数据科学家以动手的方式与数据一起工作。

在下一节中,我们将探讨如何在最具研究密集性的阶段,即Play阶段,开发概念验证机器学习解决方案。

Play

在项目的play阶段,你的目标是确定即使在概念验证级别解决任务是否可行。为此,你可能会在创建满足你需求的机器学习模型之前,采用我们在上一章中提到的常规数据科学技术,如探索性数据分析和解释性建模。

在这个流程的这一部分,你不必过分关注实现的细节,而是要探索可能性领域,并深入理解数据和问题,这超出了初步发现工作。由于这里的目的是不创建生产就绪的代码或构建可重用的工具,因此你不必担心你编写的代码是否质量最高,或者是否使用了复杂的模式。例如,看到以下示例(实际上是从本书的 repo 中摘取的)的代码并不罕见:

图 2.9 – 在游戏阶段将创建的一些示例原型代码

图 2.8:在游戏阶段将创建的一些示例原型代码。

只需快速浏览这些截图,就能告诉你一些事情:

  • 代码位于 Jupyter 笔记本中,由用户在网页浏览器中交互式运行。

  • 代码偶尔会调用方法来简单地检查或探索数据元素(例如,df.head()df.dtypes)。

  • 对于绘图(而且它并不直观!)有专门的代码。

  • 有一个名为tmp的变量,描述性不强。

所有这些在这个更探索性的阶段都是绝对可以接受的,但本书的一个目标就是帮助你理解将此类代码转化为适合你生产机器学习管道所需的要素。下一节将开始引导我们走上这条道路。

开发

正如我们之前已经提到几次,本书的一个目标就是让你思考这样一个事实:你正在构建的软件产品恰好包含了机器学习。这意味着对于我们这些来自更数学和算法背景的人来说,学习曲线可能会很陡峭。这可能会让人感到害怕,但不要绝望!好消息是,我们可以重用几十年来软件工程社区锤炼的许多最佳实践和技术。太阳之下无新事。

本节探讨了在机器学习工程项目的开发阶段可以采用的一些方法、流程和考虑因素。

选择软件开发方法

我们作为机器学习工程师,可以而且应该毫无顾忌地复制全球项目中使用的软件开发方法。这些方法中的一类,通常被称为瀑布模型,涵盖了适合构建复杂事物的项目工作流程(想想建筑或汽车)。在瀑布模型中,有明确且顺序性的工作阶段,每个阶段都有在进入下一阶段之前所需的明确输出。例如,典型的瀑布项目可能包含涵盖需求收集、分析、设计、开发、测试和部署等阶段(听起来熟悉吗?)。关键在于,在瀑布风格的项目中,当你处于需求收集阶段时,你应该专注于收集需求,当处于测试阶段时,你应该专注于测试,依此类推。在介绍另一套方法之后,我们将在接下来的几段中讨论这种方法在机器学习中的优缺点。

另一套方法,称为敏捷,是在 2001 年敏捷宣言agilemanifesto.org/)发布之后出现的。敏捷开发的核心理念是灵活性、迭代、增量更新、快速失败和适应变化的需求。如果你来自研究或科学背景,这种基于结果和新发现灵活性和适应性的概念可能听起来很熟悉。

如果你具有这种科学或学术背景,可能不太熟悉的是,你仍然可以在一个以交付结果为中心的相对严格的框架内接受这些概念。敏捷软件开发方法的核心是寻找实验和交付之间的平衡。这通常通过引入仪式(如ScrumSprint回顾)和角色(如Scrum Master产品负责人)来实现。

此外,在敏捷开发中,有两种非常流行的变体:ScrumKanban。Scrum 项目围绕称为Sprint的短期工作单元展开,其理念是在这个短暂的时间内从构思到部署对产品的添加。在 Kanban 中,主要理念是实现从有序的待办事项到进行中工作,再到完成工作的稳定流程

所有这些方法(以及更多)都有其优点和缺点。你不必对其中任何一种方法产生依赖;你可以在它们之间随意切换。例如,在一个机器学习项目中,进行一些部署后的工作可能是有意义的,这些工作专注于维护现有的服务(有时被称为常规业务活动),例如进一步改进模型或软件优化在看板框架中。在 Sprint 中明确结果的主要交付可能是有意义的。但你可以随意切换,看看哪种最适合你的用例、你的团队和你的组织。

但将这类工作流程应用于机器学习项目有什么不同?在这个机器学习的世界中,我们需要考虑哪些以前没有考虑过的问题?好吧,一些关键点如下:

  • 你不知道你不知道的:在你看到数据之前,你无法知道你是否能够解决问题。传统的软件工程不像机器学习工程那样严重依赖于将通过系统的数据。原则上我们可以知道如何解决问题,但如果适当的数据数量不足或质量差,那么在实践中我们无法解决问题。

  • 你的系统是活生生的:如果你构建了一个经典的网站,拥有其后端数据库、闪亮的用户界面、惊人的负载均衡和其他功能,那么实际上,如果资源存在,它可以永远运行。网站及其运行方式在时间上不会发生根本性的变化。点击仍然会被转换成操作,页面导航仍然以相同的方式进行。现在,考虑在其中加入一些基于典型用户档案的机器学习生成的广告内容。什么是典型用户档案,它会随时间变化吗?随着流量和用户的增加,我们以前从未见过的行为是否变成了新常态?你的系统一直在学习,这导致了模型漂移分布偏移的问题,以及更复杂的更新和回滚场景。

  • 没有什么是确定的:在构建使用基于规则的逻辑的系统时,你知道每次会得到什么。如果 X,那么 Y的意思就是如此,始终如此。对于机器学习模型,当你提问时,往往很难知道答案是什么,这正是这些算法之所以强大的原因。

但这也意味着你可能会有不可预测的行为,无论是由于之前讨论的原因,还是因为算法学习到了人类观察者不明显的数据信息,或者,因为机器学习算法可以基于概率和统计概念,结果会附带一些不确定性或模糊性。一个经典的例子是当你应用逻辑回归并收到数据点属于某一类别的概率时。这是一个概率值,你不能确定地说它是这种情况;只是有多大的可能性!这在你的机器学习系统的输出将被用户或其他系统用于做出决策时尤其重要。

考虑到这些问题,在下一节中,我们将尝试了解哪些开发方法论可以帮助我们在构建机器学习解决方案时。在表 2.2中,我们可以看到这些敏捷方法论在不同阶段和类型的机器学习(ML)工程项目中的优缺点:

方法论优点缺点
敏捷预期具有灵活性,更快的开发到部署周期。如果管理不善,容易发生范围漂移。看板或冲刺可能不适合某些项目。
水晶球模型清晰的部署路径,明确任务的阶段和所有权。缺乏灵活性,更高的管理开销。

表 2.2:敏捷与水晶球模型在机器学习(ML)开发中的应用。

让我们继续下一节!

包管理(conda 和 pip)

如果我告诉你编写一个不使用任何库或包,仅使用纯 Python 进行数据科学或机器学习(ML)的程序,你可能觉得在合理的时间内完成这项任务非常困难,而且极其无聊!这是好事。在 Python 中开发软件的一个真正强大的特性是,你可以相对容易地利用一个广泛的工具和功能生态系统。另一方面,这也意味着管理代码库的依赖可能会变得非常复杂且难以复制。这就是包和环境管理器如pipconda发挥作用的地方。

pip是 Python 的标准包管理器,也是 Python 包权威机构推荐使用的。

它从PyPI,即Python 包索引中检索和安装 Python 包。pip使用非常简单,通常在教程和书籍中建议作为安装包的方式。

conda是 Anaconda 和 Miniconda Python 发行版附带的一个包和环境管理器。conda的一个关键优势是,尽管它来自 Python 生态系统,并且在那里具有出色的功能,但它实际上是一个更通用的包管理器。因此,如果你的项目需要 Python 之外的依赖(例如 NumPy 和 SciPy 库是很好的例子),尽管pip可以安装这些依赖,但它无法跟踪所有非 Python 依赖,也无法管理它们的版本。使用conda,这个问题就解决了。

您也可以在 conda 环境中使用 pip,因此您可以同时获得两者的最佳之处,或者使用您项目所需的任何工具。我通常使用的典型工作流程是使用 conda 管理我创建的环境,然后使用它来安装任何可能需要非 Python 依赖项的包,这些依赖项可能没有在 pip 中得到很好的处理,然后我可以在创建的 conda 环境中使用 pip 大部分时间。鉴于这一点,在本书中,您可能会看到 pipconda 安装命令交替使用。这是完全可以接受的。

要开始使用 Conda,如果您还没有,您可以从 Anaconda 网站下载 个人 分发安装程序(www.anaconda.com/products/individual)。Anaconda 已经预装了一些 Python 包,但如果您想从一个完全空的环境开始,您可以从同一网站下载 Miniconda(它们具有完全相同的功能;只是您从不同的基础开始)。

Anaconda 文档对于您熟悉适当的命令非常有帮助,但这里简要介绍一下其中的一些关键命令。

首先,如果我们想创建一个名为 mlengconda 环境并安装 Python 3.8 版本,我们只需在我们的终端中执行以下命令:

conda env --name mleng python=3.10 

然后,我们可以通过运行以下命令来激活 conda 环境:

source activate mleng 

这意味着任何新的 condapip 命令都将在此环境中安装包,而不是系统范围内。

我们经常希望与他人共享我们环境的详细信息,以便他们可以在同一项目中工作,因此将所有包配置导出到 .yml 文件中可能很有用:

conda export env > environment.yml 

本书 GitHub 仓库中包含一个名为 mleng-environment.yml 的文件,您可以使用此文件创建 mleng 环境的实例。以下命令使用此文件创建具有此配置的环境:

conda env create --file environment.yml 

从环境文件创建 conda 环境的模式是设置书中每一章示例运行环境的一个好方法。因此,每一章的 技术要求 部分将指向书中仓库中包含的正确环境 YAML 文件名称。

这些命令与您经典的 condapip install 命令结合使用,将为您的项目设置得相当好!

conda install <package-name> 

或者

pip install <package-name> 

我认为拥有多种执行某事的方法总是好的,而且通常这是良好的工程实践。因此,既然我们已经介绍了经典的 Python 环境、condapip 包管理器,我们将介绍另一个包管理器。这是一个我喜欢其易用性和多功能性的工具。我认为它为 condapip 提供了很好的功能扩展,并且可以很好地补充它们。这个工具叫做 Poetry,我们现在就转向它。

Poetry

Poetry 是另一种近年来变得非常流行的包管理器。它允许您以类似于我们在 Conda 部分讨论的环境 YAML 文件的方式,将项目的依赖项和包信息管理到一个单独的配置文件中。Poetry 的优势在于其远超其他工具的复杂依赖项管理能力,并确保“确定性”构建,这意味着您不必担心包在后台更新而破坏您的解决方案。它是通过使用“锁定文件”作为核心功能以及深入的依赖项检查来实现的。这意味着在 Poetry 中,可重复性通常更容易实现。重要的是要指出,Poetry 专注于特定的 Python 包管理,而 Conda 也可以安装和管理其他包,例如 C++ 库。可以这样理解 Poetry:它就像是 pip Python 安装包的升级版,但同时也具备一些环境管理功能。接下来的步骤将解释如何设置和使用 Poetry 以进行非常基本的用例。

我们将在本书的一些后续示例中继续这一内容。首先,按照以下步骤操作:

  1. 首先,像往常一样,我们将安装 Poetry:

    pip install poetry 
    
  2. 安装 Poetry 后,您可以使用 poetry new 命令创建一个新的项目,后面跟上是您项目的名称:

    poetry new mleng-with-python 
    
  3. 这将创建一个名为 mleng-with-python 的新目录,其中包含 Python 项目的必要文件和目录。要管理您项目的依赖项,您可以将它们添加到项目根目录下的 pyproject.toml 文件中。此文件包含您项目的所有配置信息,包括其依赖项和包元数据。

    例如,如果您正在构建一个机器学习项目并想使用 scikit-learn 库,您会在 pyproject.toml 文件中添加以下内容:

    [tool.poetry.dependencies]
    scikit-learn = "*" 
    
  4. 然后,您可以通过运行以下命令安装您项目的依赖项。这将安装 scikit-learn 库以及您在 pyproject.toml 文件中指定的任何其他依赖项:

    poetry install 
    
  5. 要在您的项目中使用依赖项,您只需像这样在 Python 代码中导入它即可:

    from sklearn import datasets
    from sklearn.model_selection import train_test_split
    from sklearn.linear_model import LogisticRegression 
    

如您所见,开始使用 Poetry 非常简单。我们将在本书中多次使用 Poetry,以便为您提供与我们将开发的 Conda 知识相补充的示例。第四章打包,将详细讨论这一点,并展示如何充分利用 Poetry。

代码版本控制

如果你将要为真实系统编写代码,你几乎肯定将会作为团队的一部分来做。如果你能够有一个清晰的变更、编辑和更新审计记录,那么你会更容易看到解决方案是如何发展的。最后,你将想要干净且安全地将你正在构建的稳定版本与可以部署的版本以及更短暂的开发版本分开。幸运的是,所有这些都可以由源代码版本控制系统来处理,其中最受欢迎的是Git

我们不会在这里深入探讨 Git 底层是如何工作的(关于这个话题有整本书的讨论!)但我们将会关注理解使用 Git 的关键实践要素:

  1. 你在章节的早期已经有一个 GitHub 账户了,所以首先要做的是创建一个以 Python 为语言的仓库,并初始化README.md.gitignore文件。接下来要做的是通过在 Bash、Git Bash 或其他终端中运行以下命令来获取这个仓库的本地副本:

    git clone <repo-name> 
    
  2. 现在你已经完成了这个步骤,进入README.md文件并进行一些编辑(任何内容都可以)。然后,运行以下命令来告诉 Git 监控这个文件,并使用简短的消息保存你的更改:

    git add README.md
    git commit -m "I've made a nice change …" 
    

    这现在意味着你的本地 Git 实例已经存储了你所做的更改,并准备好与远程仓库共享这些更改。

  3. 你可以通过以下步骤将这些更改合并到main分支:

    git push origin main 
    

    如果你现在回到 GitHub 网站,你会看到你的远程仓库中发生了更改,你添加的评论也伴随着这个更改。

  4. 你的团队成员可以通过运行以下命令来获取更新的更改:

    git pull origin main 
    

这些步骤是 Git 的绝对基础,你可以在网上学到更多。不过,我们现在要做的是以与机器学习工程相关的方式设置我们的仓库和工作流程。

Git 策略

在一个项目中,使用版本控制系统的策略通常可以成为区分数据科学和机器学习工程方面的重要因素。在探索性和基本建模阶段(发现玩耍)定义严格的 Git 策略可能有些过度,但如果你想要为部署构建某些内容(而且你正在阅读这本书,所以这很可能是你的目标),那么这基本上是非常重要的。

很好,但我们所说的 Git 策略是什么意思呢?

好吧,让我们假设我们试图在没有共享版本组织和代码组织方向的情况下开发我们的解决方案。

机器学习工程师A想要开始将一些数据科学代码构建到 Spark ML 管道中(关于这一点稍后会有更多介绍),因此从main分支创建了一个名为pipeline1spark的分支:

git checkout -b pipeline1spark 

他们然后开始在这个分支上工作,并在一个名为pipeline.py的新文件中编写了一些优秀的代码:

# Configure an ML pipeline, which consists of three stages: tokenizer, hashingTF, and lr.
tokenizer = Tokenizer(inputCol="text", outputCol="words")
hashingTF = HashingTF(inputCol=tokenizer.getOutputCol(),
                      outputCol="features")
lr = LogisticRegression(maxIter=10, regParam=0.001)
pipeline = Pipeline(stages=[tokenizer, hashingTF, lr]) 

太好了,他们已经将一些之前的 sklearn 代码翻译成了 Spark,这被认为更适合用例。然后他们继续在这个分支上工作,因为它包含了他们所有的添加,他们认为最好在一个地方完成所有工作。当他们想要将分支推送到远程仓库时,他们运行以下命令:

git push origin pipeline1spark 

机器学习工程师 B 加入进来,他们想使用机器学习工程师 A 的管道代码,并在其周围构建一些额外的步骤。他们知道工程师 A 的代码有一个包含这项工作的分支,所以他们足够了解 Git,可以创建一个包含 A 代码的另一个分支,B 称之为 pipeline

git pull origin pipeline1spark
git checkout pipeline1spark
git checkout -b pipeline 

他们接着添加了一些代码来从变量中读取模型的参数:

lr = LogisticRegression(maxIter=model_config["maxIter"], 
                        regParam=model_config["regParam"]) 

很酷,工程师 B 做了一个更新,开始抽象掉一些参数。然后他们把新的分支推送到远程仓库:

git push origin pipeline 

最后,机器学习工程师 C 加入到团队中,并想开始编写代码。打开 Git 并查看分支,他们看到有三个:

main
pipeline1spark
pipeline 

那么,哪个应该被视为最新的?如果他们想进行新的编辑,他们应该从哪里分支?这并不清楚,但更危险的是,如果他们被要求将部署代码推送到执行环境,他们可能会认为 main 包含了所有相关更改。在一个已经进行了很长时间且非常繁忙的项目中,他们甚至可能会从 main 分支中分支出来,并复制一些 BC 的工作!在一个小项目中,你会浪费时间进行这种无谓的追逐;在一个大项目中有许多不同的工作线,你几乎不可能保持良好的工作流程:

# Branch pipeline1spark - Commit 1 (Engineer A)
lr = LogisticRegression(maxIter=10, regParam=0.001)
pipeline = Pipeline(stages=[tokenizer, hashingTF, lr])
# Branch pipeline - Commit 2 (Engineer B)
lr = LogisticRegression(maxIter=model_config["maxIter"], 
                        regParam=model_config["regParam"])
pipeline = Pipeline(stages=[tokenizer, hashingTF, lr]) 

如果这两个提交同时推送到 main 分支,那么我们就会得到所谓的合并冲突,在这种情况下,工程师将不得不选择保留哪段代码,是当前的还是新的示例。如果工程师 A 首先将其更改推送到 main,这看起来可能如下所示:

<<<<<<< HEAD
lr = LogisticRegression(maxIter=10, regParam=0.001)
pipeline = Pipeline(stages=[tokenizer, hashingTF, lr])
=======
lr = LogisticRegression(maxIter=model_config["maxIter"], 
                        regParam=model_config["regParam"])
pipeline = Pipeline(stages=[tokenizer, hashingTF, lr])
>>>>>>> pipeline 

代码中的分隔符表明存在合并冲突,并且取决于开发者选择保留哪两个代码版本。

重要提示

尽管在这个简单的情况下,我们可能可以信任工程师选择更好的代码,但允许这种情况频繁发生对你的项目来说是一个巨大的风险。这不仅会浪费大量宝贵的发展时间,还可能意味着你最终得到的代码质量更差!

避免混淆和额外工作的方法是制定一个非常清晰的策略,用于实施版本控制系统,例如我们现在将要探讨的策略。

Gitflow 工作流程

之前示例的最大问题是,我们假设的所有工程师实际上都在不同地方工作着同一块代码。为了阻止这种情况发生,你必须创建一个团队都可以遵循的过程——换句话说,一个版本控制策略或工作流程。

这些策略中最受欢迎之一是Gitflow 工作流程。它基于拥有专门用于功能的分支的基本想法,并将其扩展到包含发布和热修复的概念,这对于具有持续部署元素的项目尤其相关。

主要思想是我们有几种类型的分支,每种分支都有明确和具体的存在原因:

  • 主分支包含您的官方发布版本,应只包含代码的稳定版本。

  • 开发分支是大多数仓库中从其分支和合并到的主要点;它包含代码库的持续开发,并在main之前作为预发布区域。

  • 功能分支不应直接合并到main分支;所有内容都应该从dev分支开始,然后合并回dev

  • 发布分支dev创建,在合并到maindev并删除之前启动构建或发布过程。

  • 热修复分支用于从已部署或生产软件中移除错误。您可以在合并到maindev之前从main分支创建此分支。

所有这些都可以用图 2.9 进行图解总结,该图显示了不同分支在 Gitflow 工作流程中如何贡献于代码库的演变:

图 2.11 – Gitflow 工作流程

图 2.9:Gitflow 工作流程。

此图来自lucamezzalira.com/2014/03/10/git-flow-vs-github-flow/。更多详情可以在www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow找到。

如果您的机器学习项目可以遵循这种策略(如果您想进行适应性调整,则不需要对此过于严格),您可能会看到生产率、代码质量和甚至文档的显著提高:

图 2.12 – GitHub 上拉取请求的示例代码更改图 2.10:GitHub 上拉取请求的示例代码更改。

我们尚未讨论的一个重要方面是代码审查的概念。这些审查通过所谓的拉取请求触发,您通过拉取请求表明您合并到另一个分支的意图,并允许其他团队成员在执行之前审查您的代码。这是将代码审查引入工作流程的自然方式。您可以在想要合并更改并更新到 dev 或 main 分支时进行此操作。建议的更改然后可以呈现给整个团队,他们可以在合并完成之前通过进一步的提交进行辩论和迭代。

这强制进行代码审查以提高质量,同时创建审计跟踪和更新保障。图 2.10展示了在 GitHub 拉取请求期间如何使代码更改可见以供辩论的示例。

现在我们已经讨论了一些将版本控制应用于您的代码的最佳实践,让我们探索如何在机器学习项目中实现模型的版本控制。

模型版本控制

在任何机器学习工程项目中,您不仅要清晰地跟踪代码更改,还要跟踪模型的变化。您希望跟踪的不仅仅是建模方法的变化,还包括当新的或不同的数据输入到您选择的算法中时的性能变化。跟踪这些变化并提供机器模型版本控制的最佳工具之一是MLflow,这是一个由 Linux 基金会监管的开源平台,由Databricks提供。

要安装 MLflow,请在您选择的 Python 环境中运行以下命令:

pip install mlflow 

MLflow 的主要目标是提供一个平台,通过该平台您可以记录模型实验、工件和性能指标。它是通过 Python mlflow库提供的非常简单的 API 实现的,通过一系列中央开发社区插件与选定的存储解决方案接口。它还提供了通过图形用户界面GUI)查询、分析和导入/导出数据的功能,其外观类似于图 2.11

图 2.13 – MLflow 跟踪服务器 UI 和一些预测运行

图 2.11:MLflow 跟踪服务器 UI 和一些预测运行。

图书馆的使用极其简单。以下示例中,我们将从第一章机器学习工程导论中的销售预测示例开始,并添加一些基本的 MLflow 功能以跟踪性能指标和保存训练好的 Prophet 模型:

  1. 首先,我们进行相关的导入,包括 MLflow 的pyfunc模块,它作为可以编写为 Python 函数的模型的保存和加载的通用接口。这有助于与 MLflow 原生不支持(如fbprophet库)的库和工具一起工作:

    import pandas as pd
    from fbprophet import Prophet
    from fbprophet.diagnostics import cross_validation
    from fbprophet.diagnostics import performance_metrics
    import mlflow
    import mlflow.pyfunc 
    
  2. 为了与fbprophet的预测模型实现更无缝的集成,我们定义了一个小的包装类,它继承自mlflow.pyfunc.PythonModel对象:

    class FbProphetWrapper(mlflow.pyfunc.PythonModel):
        def __init__(self, model):
            self.model = model
            super().__init__()
        def load_context(self, context):
            from fbprophet import Prophet
            return
        def predict(self, context, model_input):
            future = self.model.make_future_dataframe(
                periods=model_input["periods"][0])
            return self.model.predict(future) 
    

    现在我们将训练和预测的功能封装到一个名为train_predict()的单个辅助函数中,以便简化多次运行。我们不会在这里定义这个函数的所有细节,但让我们浏览一下其中包含的 MLflow 功能的主要部分。

  3. 首先,我们需要让 MLflow 知道我们现在开始一个希望跟踪的训练运行:

    with mlflow.start_run():
        # Experiment code and mlflow logging goes in here 
    
  4. 在这个循环内部,我们定义并训练模型,使用代码中其他地方定义的参数:

    # create Prophet model
    model = Prophet(
        yearly_seasonality=seasonality_params['yearly'],
        weekly_seasonality=seasonality_params['weekly'],
        daily_seasonality=seasonality_params['daily']
    )
    # train and predict
    model.fit(df_train) 
    
  5. 然后,我们进行交叉验证来计算我们想要记录的一些指标:

    # Evaluate Metrics
    df_cv = cross_validation(model, initial="730 days", 
                             period="180 days", horizon="365 days")
    df_p = performance_metrics(df_cv) 
    
  6. 我们可以将这些指标记录下来,例如,这里的均方根误差RMSE),到我们的 MLflow 服务器:

    # Log parameter, metrics, and model to MLflow
    mlflow.log_metric("rmse", df_p.loc[0, "rmse"]) 
    
  7. 最后,我们可以使用我们的模型包装类来记录模型并打印一些关于运行的信息:

    mlflow.pyfunc.log_model("model", python_model=FbProphetWrapper(model))
    print(
        "Logged model with URI: runs:/{run_id}/model".format(
            run_id=mlflow.active_run().info.run_id
        )
    ) 
    
  8. 只需添加几行额外的代码,我们就已经开始对我们的模型进行版本控制并跟踪不同运行状态的统计数据了!

将你构建的机器学习模型保存到 MLflow(以及一般情况)有许多不同的方法,这在跟踪模型版本时尤其重要。以下是一些主要选项:

  • picklepickle 是一个用于对象序列化的 Python 库,通常用于导出在 scikit-learn 或更广泛的 scipy 生态系统中的管道。尽管它非常容易使用且通常非常快,但在将你的模型导出为 pickle 文件时必须小心,因为以下原因:

    • 版本控制:当你 pickle 一个对象时,你必须使用与其他程序中相同的 pickle 版本来反序列化它,出于稳定性的原因。这增加了管理你的项目的复杂性。

    • 安全性pickle 的文档明确指出它是不安全的,并且很容易构造恶意的 pickle,这些 pickle 在反序列化时会执行危险的代码。这是一个非常重要的考虑因素,尤其是在你向生产环境迈进时。

    通常,只要你知道你使用的 pickle 文件的历史来源并且来源是可信的,它们就可以安全使用,并且是一种简单快速地分享你的模型的方法!

  • joblibjoblib 是一个功能强大但轻量级的 Python 通用管道库。它围绕缓存、并行化和压缩等许多非常有用的功能,使其成为保存和读取你的机器学习管道的非常通用的工具。它对于存储大型 NumPy 数组也非常快,因此对于数据存储很有用。我们将在后面的章节中更多地使用 joblib。重要的是要注意,joblibpickle 一样存在相同的安全问题,因此了解你的 joblib 文件的历史来源至关重要。

  • JSON:如果 picklejoblib 不适用,你可以将你的模型及其参数序列化为 JSON 格式。这很好,因为 JSON 是一种标准化的文本序列化格式,在许多解决方案和平台上广泛使用。但是,使用 JSON 序列化你的模型有一个缺点,那就是你通常必须手动定义包含你想要存储的相关参数的 JSON 结构。因此,这可能会产生大量的额外工作。Python 中的一些机器学习库都有自己的导出为 JSON 的功能,例如深度学习包 Keras,但它们都可以产生相当不同的格式。

  • MLeap:MLeap 是基于Java 虚拟机JVM)的序列化格式和执行引擎。它与 Scala、PySpark 和 Scikit-Learn 有集成,但您通常会在示例和教程中看到它用于保存 Spark 管道,特别是对于使用 Spark ML 构建的模型。这种关注意味着它不是最灵活的格式,但如果您在Spark 生态系统中工作,它非常有用。

  • ONNX开放神经网络交换ONNX)格式旨在实现完全跨平台,并允许主要机器学习框架和生态系统之间交换模型。ONNX 的主要缺点是(正如您可以从其名称中猜到的)它主要针对基于神经网络的模型,除了其scikit-learn API 之外。如果您正在构建神经网络,这仍然是一个极好的选择。

第三章从模型到模型工厂中,我们将使用这些格式中的一些将我们的模型导出到 MLflow,但它们都与 MLflow 兼容,因此您应该在使用它们作为您机器学习工程工作流程的一部分时感到舒适。

本章的最后一节将介绍一些重要的概念,用于规划您希望如何部署您的解决方案,为书中稍后更详细的讨论做铺垫。

部署

机器学习开发过程的最后阶段才是真正重要的:您如何将您构建的令人惊叹的解决方案带入现实世界并解决您最初的问题?答案有多个部分,其中一些将在本书稍后更详细地探讨,但将在本节中概述。如果我们想要成功部署我们的解决方案,首先,我们需要了解我们的部署选项:有什么基础设施可供选择,并且适合这项任务?然后,我们需要将解决方案从我们的开发环境转移到这个生产基础设施上,以便在适当的编排和控制下,它能够执行我们需要的任务,并在需要的地方展示结果。这就是DevOpsMLOps概念发挥作用的地方。

让我们详细阐述这两个核心概念,为后续章节奠定基础,并探讨如何开始部署我们的工作。

了解您的部署选项

第五章部署模式和工具中,我们将详细介绍您需要将您的机器学习工程项目从开发阶段过渡到部署阶段,但为了提前预告并为您提供即将到来的内容的预览,让我们探索我们可用的不同部署选项:

  • 本地部署:我们拥有的第一个选择是完全忽略公有云,并在我们拥有的基础设施上内部部署我们的解决方案。这个选项对于许多拥有大量遗留软件和强烈数据位置和数据处理监管约束的大型机构来说尤其受欢迎和必要。本地部署的基本步骤与云上部署相同,但通常需要来自其他具有特定专业知识的团队的大量参与。例如,如果你在云上,你通常不需要花很多时间配置网络或实现负载均衡器,而本地解决方案将需要这些。

    本地部署的大优势是安全性和安心感,即你的数据不会穿越公司的防火墙。缺点是它需要更大的前期硬件投资,而且你必须付出大量努力才能有效地配置和管理该硬件。在这本书中,我们不会详细讨论本地部署,但我们将在软件开发、打包、环境管理、培训和预测系统等方面使用的所有概念仍然适用。

  • 基础设施即服务IaaS):如果你打算使用云,你可用于部署的最低抽象级别之一是 IaaS 解决方案。这些通常基于虚拟化的概念,即可以根据用户的意愿启动具有各种规格的服务器。这些解决方案通常将维护和操作的需求抽象化,作为服务的一部分。最重要的是,它们允许你的基础设施在需要时具有极端的可扩展性。下周需要运行 100 多台服务器?没问题,只需扩展你的 IaaS 请求,它就会出现。尽管 IaaS 解决方案比完全管理的本地基础设施迈出了很大一步,但仍有几件事情你需要考虑和配置。云计算中的平衡始终在于你希望事情有多容易,以及你希望有多少控制权。与某些其他解决方案相比,IaaS 最大化了控制权,但最小化了(相对的)易用性。在AWS中,简单存储服务S3)和弹性计算云EC2)是 IaaS 提供的良好例子。

  • 平台即服务PaaS):PaaS 解决方案在抽象层面上是下一个级别,通常提供许多功能,而无需了解底层具体发生什么。这意味着你可以专注于平台准备支持的开发任务,而无需担心任何底层基础设施。一个很好的例子是AWS Lambda函数,这是一种无服务器函数,几乎可以无限制地扩展。

你需要做的只是将你想要在函数内部执行的主要代码块输入进去。另一个很好的例子是Databricks,它提供了在Spark 集群基础设施之上的非常直观的用户界面,几乎无缝地提供、配置和扩展这些集群。

了解这些不同选项及其功能可以帮助你设计你的机器学习解决方案,并确保你将团队的技术努力集中在最需要和最有价值的地方。例如,如果你的机器学习工程师正在配置路由器,那么你肯定在某些地方犯了错误。

但一旦你选择了要使用的组件并配置了基础设施,你该如何将这些组件集成在一起并管理你的部署和更新周期呢?这正是我们现在要探讨的。

理解 DevOps 和 MLOps

现代软件开发中的一个非常有力的观点是,你的团队应该能够根据需要持续更新代码库,同时测试、集成、构建、打包和部署解决方案应该尽可能地自动化。这意味着这些流程可以几乎持续不断地进行,而不需要分配大块预先计划的时间来更新周期。这就是CI/CD背后的主要思想。CI/CD 是DevOps及其以机器学习为重点的表亲MLOps的核心部分,它们都旨在将软件开发和部署后的运营结合起来。本书中我们将开发的一些概念和解决方案将构建得自然地适合 MLOps 框架。

CI 部分主要关注稳定地将持续变更集成到代码库中,同时确保功能保持稳定。CD 部分则是将解决方案的稳定版本推送到适当的基础设施。

图 2.12展示了这一过程的高级视图:

图 2.14 – CI/CD 流程的高级视图

图 2.12:CI/CD 流程的高级视图。

为了使 CI/CD 成为现实,你需要整合帮助自动化你传统上在开发和部署过程中手动执行的任务的工具。例如,如果你可以在代码合并时自动运行测试,或者将你的代码工件/模型推送到适当的环境,那么你就已经走上了 CI/CD 的道路。

我们可以进一步分解,并考虑解决方案的 DevOps 或 MLOps 生命周期中落入的不同类型任务。开发任务通常涵盖从电脑上的空白屏幕到可工作的软件的所有活动。这意味着在 DevOps 或 MLOps 项目中,你大部分时间都花在开发上。这包括从编写代码到正确格式化并测试它的一切。

表 2.3 将这些典型任务分开,并提供了一些关于它们如何相互依赖以及您可以在 Python 栈中使用的典型工具的详细信息。

生命周期阶段活动详细信息工具
开发测试单元测试:针对测试代码最小功能部分的测试。pytest 或 unittest
集成测试:确保代码内部和其他解决方案之间的接口正常工作。Selenium
接受测试:业务导向的测试。Behave
UI 测试:确保任何前端按预期运行。
代码风格检查报告小的风格错误和错误。flake8 或 bandit
格式化自动执行格式良好的代码。black 或 sort
构建将解决方案整合的最后阶段。Docker, twine 或 pip

表 2.3:任何 DevOps 或 MLOps 项目中执行的开发活动的详细信息。

接下来,我们可以考虑 MLOps 中的机器学习活动,本书将非常关注这些内容。这包括一个经典的 Python 软件工程师通常不需要担心,但对于我们这样的机器学习工程师来说至关重要的所有任务。这包括开发自动训练机器学习模型的能力,运行模型应生成的预测或推理,并在代码管道中将它们整合在一起。它还包括模型版本的管理和部署,这极大地补充了使用像 Git 这样的工具对应用程序代码进行版本控制的想法。最后,机器学习工程师还必须考虑他们必须为解决方案的操作模式构建特定的监控能力,这在传统的 DevOps 工作流程中并未涵盖。对于机器学习解决方案,您可能需要考虑监控诸如精确度、召回率、f1 分数、人口稳定性、熵和数据漂移等因素,以了解您的解决方案中的模型组件是否在可容忍的范围内运行。这与经典的软件工程非常不同,因为它需要了解机器学习模型的工作原理,它们可能出错的方式,以及对数据质量重要性的真正认识。这就是为什么机器学习工程是一个如此令人兴奋的地方!请参阅 表 2.4 了解这些类型活动的更多详细信息。

生命周期阶段活动详细信息工具
机器学习训练训练模型。任何机器学习包。
预测运行预测或推理步骤。任何机器学习包。
构建创建模型嵌入的管道和应用逻辑。sklearn 管道、Spark ML 管道、ZenML。
部署标记和发布您模型和管道的适当版本。MLflow 或 Comet.ml.
监控跟踪解决方案性能并在必要时发出警报。Seldon, Neptune.ai, Evidently.ai 或 Arthur.ai.

表 2.4:MLOps 项目中执行的以机器学习为中心活动的详细信息。

最后,在 DevOps 或 MLOps 中,有 Operations 部分,这指的是运维。这全部关于解决方案的实际运行方式,如果出现问题,它将如何通知你,以及它是否能够成功恢复。自然地,运维将涵盖与解决方案的最终打包、构建和发布相关的活动。它还必须涵盖另一种类型的监控,这与 ML 模型的性能监控不同。这种监控更多地关注基础设施利用率、稳定性和可扩展性,关注解决方案的延迟,以及更广泛解决方案的一般运行。在 DevOps 和 MLOps 生命周期中,这部分在工具方面相当成熟,因此有很多选项可供选择。以下是在 表 2.5 中提供的一些启动信息。

生命周期阶段活动详情工具
Ops发布将你构建的软件存储在某个中央位置以供重用。Twine、pip、GitHub 或 BitBucket。
部署将你构建的软件推送到适当的目标位置和环境。Docker、GitHub Actions、Jenkins、TravisCI 或 CircleCI。
监控跟踪底层基础设施和一般软件性能的性能和利用率,在必要时发出警报。DataDog、Dynatrace 或 Prometheus。

表 2.5:在 DevOps 或 MLOps 项目中使解决方案可操作所执行的活动详情。

现在我们已经阐明了 MLOps 生命周期中所需的核心概念,在下一节中,我们将讨论如何实施 CI/CD 实践,以便我们可以在我们的 ML 工程项目中将其变为现实。我们还将扩展这一内容,涵盖对您的 ML 模型和管道性能的自动测试,以及对您的 ML 模型的自动重新训练。

使用 GitHub Actions 构建我们的第一个 CI/CD 示例

我们将在本书中使用 GitHub Actions 作为我们的 CI/CD 工具,但还有其他一些工具可以完成同样的工作。GitHub Actions 对任何拥有 GitHub 账户的人来说都是可用的,它有一套非常有用的文档,docs.github.com/en/actions,并且非常容易开始使用,正如我们现在将展示的那样。

当使用 GitHub Actions 时,你必须创建一个 .yml 文件,告诉 GitHub 何时执行所需操作,当然,执行什么操作。这个 .yml 文件应该放在你的仓库根目录下的 .github/workflows 文件夹中。如果它还不存在,你必须创建它。我们将在一个名为 feature/actions 的新分支中这样做。通过运行以下命令创建这个分支:

git checkout –b feature/actions 

然后,创建一个名为github-actions-basic.yml.yml文件。在以下步骤中,我们将构建这个.yml文件示例,用于 Python 项目,其中我们将自动安装依赖项,运行一个代码检查器(用于检查错误、语法错误和其他问题)的解决方案,然后运行一些单元测试。此示例来自 GitHub Starter Workflows 存储库(github.com/actions/starter-workflows/blob/main/ci/python-package-conda.yml)。打开github-actions-basic.yml文件,然后执行以下操作:

  1. 首先,您定义 GitHub Actions 工作流程的名称以及什么 Git 事件将触发它:

    name: Python package
    on: [push] 
    
  2. 您然后列出您想要作为工作流程一部分执行的工作,以及它们的配置。例如,这里有一个名为build的工作,我们希望在最新的 Ubuntu 发行版上运行它,并尝试使用几个不同的 Python 版本进行构建:

    jobs:
      build:
        runs-on: ubuntu-latest
        strategy:
          matrix:
            python-version: [3.9, 3.10] 
    
  3. 您然后定义作为工作一部分执行的步骤。每个步骤由一个连字符分隔,并作为单独的命令执行。重要的是要注意,uses关键字获取标准的 GitHub Actions;例如,在第一步中,工作流程使用checkout动作的v2版本,第二步设置在工作流程中要运行的 Python 版本:

    steps:
    - uses: actions/checkout@v3
    - name: Set up Python ${{ matrix.python-version }}
    uses: actions/setup-python@v4
    with:
      python-version: ${{ matrix.python-version }} 
    
  4. 下一步使用piprequirements.txt文件(但当然您也可以使用conda)安装解决方案的相关依赖项:

    - name: Install dependencies
    run: |
      python -m pip install --upgrade pip
      pip install flake8 pytest
      if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
    - name: Lint with flake8 
    
  5. 然后,我们运行一些代码检查:

    - name: Lint with flake8
    run: |
      # stop the build if there are Python syntax errors or undefined 
      names
      flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
      # exit-zero treats all errors as warnings. The GitHub editor is 
      127 chars wide
      flake8 . --count --exit-zero --max-complexity=10 --max-line-
      length=127 --statistics 
    
  6. 最后,我们使用我们最喜欢的 Python 测试库运行我们的测试。对于这一步,我们不想运行整个仓库,因为它相当复杂,所以在这个例子中,我们使用working-directory关键字只在该目录中运行pytest

    由于它包含在test_basic.py中的简单测试函数,这将自动通过:

    - name: Test with pytest
    run: pytest
    working-directory: Chapter02 
    

我们现在已经构建了 GitHub Actions 工作流程;下一步是展示其运行。这由 GitHub 自动处理,您只需将更改后的.yml文件推送到远程仓库即可。因此,添加编辑后的.yml文件,提交它,然后推送它:

git add .github/workflows/github-actions-basic.yml
git commit –m "Basic CI run with dummy test"
git push origin feature/actions 

在终端中运行这些命令后,您可以导航到 GitHub UI,然后在顶部菜单栏中点击操作。您将看到所有操作运行的视图,如图 2.13 所示。

图 2.13:从 GitHub UI 查看的 GitHub Actions 运行。

如果您点击运行,您将看到操作运行中所有工作的详细信息,如图 2.14 所示。

图 2.14:GitHub Actions 运行细节,从 GitHub UI 查看。

最后,您可以进入每个工作,查看执行的步骤,如图 2.15 所示。点击这些步骤也会显示每个步骤的输出。这对于分析运行中的任何失败非常有用。

图 2.15:GitHub Actions 运行步骤,如图所示在 GitHub UI 上。

我们到目前为止所展示的是一个 CI 的例子。为了将其扩展到覆盖 CD,我们需要包括将生成的解决方案推送到目标主机目的地的步骤。例如,构建一个 Python 包并将其发布到 pip,或者创建一个流水线并将其推送到另一个系统以便被拾取并运行。这个后者的例子将在 第五章部署模式和工具 中进行介绍。简而言之,这就是你开始构建你的 CI/CD 流水线的方式。正如之前提到的,在本书的后面,我们将构建针对我们机器学习解决方案的特定工作流程。

现在我们将探讨如何将 CI/CD 概念提升到机器学习工程的下一个层次,并为我们的模型性能构建一些测试,这些测试可以作为持续过程的一部分被触发。

持续模型性能测试

作为机器学习工程师,我们不仅关心我们编写的代码的核心功能行为;我们还要关心我们构建的模型。这很容易被忘记,因为传统的软件项目不需要考虑这个组件。

我现在要向您展示的过程将展示如何从一些基础参考数据开始,逐步构建不同类型的测试,以增强你对模型在部署时能够按预期运行的信心。

我们已经介绍了如何使用 Pytest 和 GitHub Actions 进行自动测试,好消息是我们可以将这个概念扩展到包括一些模型性能指标的测试。为此,你需要准备以下几件事情:

  1. 在操作或测试中,你需要检索用于执行模型验证的参考数据。这可以通过从远程数据存储(如对象存储或数据库)中拉取来完成,只要提供适当的凭证。我建议将这些存储在 Github 的秘密中。在这里,我们将使用 sklearn 库生成的一个数据集作为简单的例子。

  2. 你还需要从某个位置检索你想要测试的模型或模型。这可能是一个完整的模型注册表或其他存储机制。与 第 1 点 中提到的访问和秘密管理相同的要点同样适用。在这里,我们将从 Hugging Face Hub(关于 Hugging Face 的更多内容请见 第三章)中拉取一个模型,但这同样可能是一个 MLflow Tracking 实例或其他工具。

  3. 你需要定义你想要运行的测试,并且对你能够实现预期结果有信心。你不想编写过于敏感的测试,以免因无关原因触发失败的构建,同时你也想尝试定义一些有助于捕捉你想要标记的失败类型的测试。

对于 第 1 点,这里我们从 sklearn 库中抓取一些数据,并通过 pytest fixture 使其可用于测试:

@pytest.fixture
def test_dataset() -> Union[np.array, np.array]:
    # Load the dataset
    X, y = load_wine(return_X_y=True)
    # create an array of True for 2 and False otherwise
    y = y == 2
    # Train and test split
    X_train, X_test, y_train, y_test = train_test_split(X, y, 
                                                        random_state=42)
    return X_test, y_test 

对于第 2 点,我将使用Hugging Face Hub包来检索存储的模型。如上所述的要点中提到,您需要根据您访问的模型存储机制进行适配。在这种情况下,仓库是公开的,因此无需存储任何机密信息;如果您确实需要这样做,请使用 GitHub Secrets 存储。

@pytest.fixture
def model() -> sklearn.ensemble._forest.RandomForestClassifier:
    REPO_ID = "electricweegie/mlewp-sklearn-wine"
    FILENAME = "rfc.joblib"
    model = joblib.load(hf_hub_download(REPO_ID, FILENAME))
    return model 

现在,我们只需要编写测试。让我们从一个确认模型预测产生正确对象类型的简单测试开始:

def test_model_inference_types(model, test_dataset):
    assert isinstance(model.predict(test_dataset[0]), np.ndarray)
    assert isinstance(test_dataset[0], np.ndarray)
    assert isinstance(test_dataset[1], np.ndarray) 

然后,我们可以编写一个测试来断言测试数据集上模型性能满足某些特定条件:

def test_model_performance(model, test_dataset):
    metrics = classification_report(y_true=test_dataset[1], 
                                    y_pred=model.predict(test_dataset[0]),
                                    output_dict=True)
    assert metrics['False']['f1-score'] > 0.95
    assert metrics['False']['precision'] > 0.9
    assert metrics['True']['f1-score'] > 0.8
    assert metrics['True']['precision'] > 0.8 

之前的测试可以被视为一种类似数据驱动的单元测试,并确保如果您在模型中做了更改(例如,您可能在管道中更改了一些特征工程步骤或更改了一个超参数),您不会违反期望的性能标准。一旦这些测试成功添加到仓库中,在下次推送时,GitHub 动作将被触发,您将看到模型性能测试运行成功。

这意味着我们正在将连续模型验证作为 CI/CD 过程的一部分进行执行!

图片

图 2.16:使用 GitHub Actions 作为 CI/CD 过程的一部分成功执行模型验证测试。

更复杂的测试可以在此基础上构建,并且您可以调整环境和包以适应您的需求。

持续模型训练

在机器学习工程中,“持续”概念的一个重要扩展是执行持续训练。上一节展示了如何通过推送代码来触发一些测试目的的 ML 过程;现在,我们将讨论如何扩展这一过程,以便在您想要根据代码更改触发模型重新训练的情况下。在本书的后面部分,我们将学习到很多关于基于各种不同触发器(如数据或模型漂移)对 ML 模型进行训练和重新训练的知识,例如在第三章“从模型到模型工厂”中,以及如何在第五章“部署模式和工具”中一般性地部署 ML 模型。鉴于这一点,我们在此不会详细讨论部署到不同目标的具体细节,而是向您展示如何将连续训练步骤构建到您的 CI/CD 管道中。

实际上,这比你可能想象的要简单。如您现在可能已经注意到的,CI/CD 实际上就是关于自动化一系列步骤,这些步骤在开发过程中的特定事件发生时被触发。这些步骤中的每一个都可以非常简单或更复杂,但本质上,我们只是在触发事件激活时按照指定顺序执行的其他程序。

在这个案例中,由于我们关注的是持续训练,我们应该问自己,在代码开发过程中,我们希望在何时重新训练?记住,我们正在忽略最明显的重新训练案例,即按计划或模型性能或数据质量漂移时进行重新训练,这些内容将在后面的章节中涉及。如果我们现在只考虑代码的变化,那么自然的答案是只有在代码有实质性变化时才进行训练。

例如,如果每次我们将代码提交到版本控制时都会触发一个触发器,这很可能会导致大量的计算周期被用于微小的收益,因为机器学习模型在每个情况下可能不会有很大的不同。我们可以改为仅当拉取请求合并到主分支时才触发重新训练。在一个项目中,这是一个标志着新软件功能或功能已被添加并已纳入解决方案核心的事件。

作为提醒,当在 GitHub Actions 中构建 CI/CD 时,你会在 Git 仓库的.github文件夹中创建或编辑YAML文件。如果我们想在拉取请求上触发训练过程,那么我们可以添加类似以下内容:

name: Continous Training Example
on: [pull_request] 

然后我们需要定义将适当的训练脚本推送到目标系统并运行它的步骤。首先,这很可能会需要获取一些访问令牌。假设这是针对 AWS,并且你已经将相应的 AWS 凭证作为 GitHub Secrets 加载;更多详细信息,请参阅第五章部署模式和工具。然后我们就能在deploy-trainer工作的第一步中检索到这些令牌:

jobs:
  deploy-trainer 
    runs-on: [ubuntu-latest]
    steps:
    - name: Checkout       uses: actions/checkout@v3
    - name: Configure AWS Credentials
      uses: aws-actions/configure-aws-credentials@v2
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: us-east-2
        role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
        role-external-id: ${{ secrets.AWS_ROLE_EXTERNAL_ID }}
        role-duration-seconds: 1200
        role-session-name: TrainingSession 

你可能还想将你的仓库文件复制到目标S3目的地;也许它们包含主训练脚本运行所需的模块。然后你可以做类似这样的事情:

 - name: Copy files to target destination
    run: aws s3 sync . s3://<S3-BUCKET-NAME> 

最后,你可能需要运行某种使用这些文件进行训练的过程。有如此多的方法可以做到这一点,我在这个例子中省略了具体细节。关于部署机器学习过程的各种方法将在第五章部署模式和工具中介绍:

 - name: Run training job
       run: |
        # Your bespoke run commands go in here using the tools of your choice! 

有了这些,你就拥有了运行持续机器学习模型训练所需的所有关键部件,以补充其他关于持续模型性能测试的部分。这就是如何将 DevOps 的 CI/CD 概念带入 MLOps 的世界!

摘要

本章主要讲述了为未来工作打下坚实基础的内容。我们讨论了所有机器学习工程项目中常见的开发步骤,我们称之为“发现、探索、开发、部署”,并将这种思维方式与传统方法如 CRISP-DM 进行了对比。特别是,我们概述了每个步骤的目标及其期望的输出。

接着,我们进行了关于工具的高级讨论,并介绍了主要设置步骤。我们设置了开发代码的工具,跟踪代码的更改,管理我们的机器学习工程项目,最后部署我们的解决方案。

在本章的其余部分,我们详细介绍了我们之前概述的四个步骤的细节,特别关注了开发部署阶段。我们的讨论涵盖了从瀑布式和敏捷开发方法论的优缺点到环境管理,再到软件开发最佳实践的各个方面。我们探讨了如何打包您的机器学习解决方案,以及可供您使用的部署基础设施,并概述了设置您的 DevOps 和 MLOps 工作流程的基本知识。我们通过详细讨论如何将测试应用于我们的机器学习代码来结束本章,包括如何将此测试自动化作为 CI/CD 管道的一部分。然后,我们将这些概念扩展到持续模型性能测试和持续模型训练。

在下一章中,我们将关注如何使用我们在此处讨论的许多技术来构建执行模型自动训练和再训练的软件。

加入我们的 Discord 社区

加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

packt.link/mle

二维码

第三章:从模型到模型工厂

本章全部关于机器学习工程中最重要的概念之一:您如何将训练和微调模型的困难任务自动化、可重复和可扩展到生产系统?

我们将在理论和实践层面回顾训练不同机器学习模型的主要思想,然后提供重新训练的动机,即机器学习模型不会永远表现良好的观点。这个概念也被称为漂移。在此之后,我们将介绍特征工程背后的主要概念,这是任何机器学习任务的关键部分。接下来,我们将深入探讨机器学习是如何工作的,以及它本质上是一系列优化问题。我们将探讨在着手解决这些优化问题时,您可以使用各种工具在各个抽象级别上这样做。特别是,我们将讨论您如何提供您想要训练的模型的直接定义,我称之为手动操作,或者您如何执行超参数调整或自动化机器学习AutoML)。我们将在查看使用不同库和工具的示例之后,探讨如何将它们实现以供后续在训练工作流程中使用。然后,我们将基于我们在第二章(机器学习开发过程)中进行的初步工作,通过向您展示如何与不同的 MLflow API 接口来管理模型并在 MLflow 模型注册表中更新它们的状态,来构建 MLflow。

我们将本章的结尾放在讨论允许您将所有机器学习模型训练步骤链接成单个单元(称为管道)的实用工具上,这些单元可以帮助作为我们之前讨论的所有步骤的更紧凑表示。总结部分将回顾关键信息,并概述我们在这里所做的工作将在第四章(打包)、第五章(部署模式和工具)中进一步构建。

本质上,本章将告诉您在您的解决方案中需要将哪些内容组合在一起,而后续章节将告诉您如何将这些内容稳健地组合在一起。我们将在以下章节中介绍这一点:

  • 定义模型工厂

  • 学习关于学习

  • 为机器学习构建特征

  • 设计您的训练系统

  • 需要重新训练

  • 持久化您的模型

  • 使用管道构建模型工厂

技术要求

如前几章所述,本章所需的软件包包含在Chapter03文件夹中的.yml文件中,因此要为本章创建 conda 环境,只需运行以下命令:

conda env create –f mlewp-chapter03.yml 

这将安装包括 MLflow、AutoKeras、Hyperopt Optuna、auto-sklearn、Alibi Detect 和 Evidently 在内的软件包。

注意,如果你在配备苹果硅的 Macbook 上运行这些示例,直接使用pipconda安装 TensorFlow 和auto-sklearn可能不会成功。相反,你需要安装以下包来与 TensorFlow 一起工作:

pip install tensorflow-macos 

然后

pip install tensorflow-metal 

要安装auto-sklearn,你需要运行

brew install swig 

或者使用你使用的任何 Mac 包管理器安装swig,然后你可以运行

pip install auto-sklearn 

定义模型工厂

如果我们想要开发从临时、手动和不一致的执行转向可以自动化、稳健和可扩展的机器学习系统的解决方案,那么我们必须解决如何创建和培育表演明星:模型本身的问题。

在本节中,我们将讨论必须组合在一起的关键组件,以实现这一愿景,并提供一些示例,说明这些组件在代码中可能的样子。这些示例不是实现这些概念的唯一方式,但它们将使我们能够开始构建我们的机器学习解决方案,以实现我们想要在现实世界中部署所需的复杂程度。

我们在这里讨论的主要组件如下:

  • 训练系统:一个用于以自动化方式在我们拥有的数据上稳健地训练我们模型的系统。这包括我们为在数据上训练我们的机器学习模型而开发的全部代码。

  • 模型存储库:一个用于持久化成功训练的模型的地方,以及一个与将运行预测的组件共享生产就绪模型的地方。

  • 漂移检测器:一个用于检测模型性能变化的系统,以触发训练运行。

这些组件及其与部署的预测系统的交互,构成了模型工厂的概念。以下图示展示了这一点:

图 3.1 – 模型工厂的组件

图 3.1:模型工厂的组件。

在本章的剩余部分,我们将详细探讨我们之前提到的三个组件。预测系统将是后续章节的重点,特别是第五章部署模式和工具

首先,让我们探讨训练机器学习模型意味着什么,以及我们如何构建系统来完成这项工作。

学习如何学习

在本质上,机器学习算法都包含一个关键特性:某种形式的优化。这些算法能够“学习”(意味着它们在接触到更多观察时,会迭代地改善它们在适当指标上的性能),这使得它们如此强大和令人兴奋。当我们说“训练”时,我们指的就是这个过程。

在本节中,我们将介绍支撑训练的关键概念,我们可以在代码中选择的选项,以及这些选项对我们训练系统潜在性能和能力的影响。

定义目标

我们刚刚提到训练是一个优化过程,但我们到底在优化什么?让我们考虑监督学习。在训练过程中,我们提供我们希望预测给定特征的标签或值,以便算法可以学习特征与目标之间的关系。为了在训练过程中优化算法的内部参数,它需要知道其当前参数集会有多大的“错误”。优化就是通过更新参数,使这种“错误”的度量越来越小。这正是损失函数概念所捕捉的。

损失函数有多种形式,如果你需要,你甚至可以使用很多包来定义自己的损失函数,但有一些标准损失函数是值得了解的。其中一些名称在此处提到。

对于回归问题,你可以使用以下方法:

  • 均方误差/L2 损失

  • 均方误差/L1 损失

对于二元分类问题,你可以使用以下方法:

  • 对数损失/逻辑损失/交叉熵损失

  • 拉链损失

对于多类分类问题,你可以使用以下方法:

  • 多类熵损失

  • Kullback-Leibler 散度损失

在无监督学习中,损失函数的概念仍然适用,但现在目标是输入数据的正确分布。在定义你的损失函数之后,你需要对其进行优化。这就是我们将在下一节中探讨的内容。

剪切损失

到目前为止,我们知道训练完全是关于优化的,我们也知道要优化什么,但我们还没有介绍如何优化。

通常,有很多选项可以选择。在本节中,我们将探讨一些主要的方法。

以下是一些恒定学习率的方法:

  • 梯度下降:此算法通过计算我们的损失函数相对于参数的导数,然后使用这个导数来构建一个更新,使我们在减少损失的方向上移动。

  • 批量梯度下降:我们用来在参数空间中移动的梯度是通过取所有找到的梯度的平均值得到的。它是通过查看我们的训练集中的每个数据点,并检查数据集不是太大,损失函数相对平滑且凸来做到这一点的。这几乎可以达到全局最小值。

  • 随机梯度下降:在每次迭代中,使用一个随机选择的数据点来计算梯度。这有助于更快地达到损失函数的全局最小值,但它在每次优化步骤后对损失值的突然波动更敏感。

  • 小批量梯度下降:这是批量和随机两种情况的混合。在这种情况下,对于参数的每次更新,都会使用多个大于 1 但小于整个数据集的点来更新梯度。这意味着批量大小的现在是一个需要调整的参数。批量大时,我们更接近批梯度下降,这提供了更好的梯度估计,但速度较慢。批量小时,我们更接近随机梯度下降,这速度更快,但不够稳健。小批量允许我们决定在这两者之间想要处于哪个位置。可以根据各种标准选择批大小。这些可能涉及一系列的内存考虑。并行处理的大批量批次将消耗更多内存,同时为小批量提供更好的泛化性能。有关更多详细信息,请参阅 Ian Goodfellow、Yoshua Bengio 和 Aaron Courville 所著的《深度学习》一书的第八章,网址为www.deeplearningbook.org/

然后,还有自适应学习率方法。以下是一些最常见的:

  • AdaGrad:学习率参数根据优化过程中的学习更新属性动态更新。

  • AdaDelta:这是AdaGrad的一个扩展,它不使用所有之前的梯度更新。相反,它使用一个滚动窗口来跟踪更新。

  • RMSprop:它通过维护所有梯度步骤平方的移动平均值来工作。然后,它将最新的梯度除以这个值的平方根。

  • 亚当:这是一个旨在结合AdaGradRMSprop优点的算法。

对于我们这些机器学习工程师来说,所有这些优化方法的限制和能力都很重要,因为我们希望确保我们的训练系统使用正确的工具来完成工作,并且对当前问题是最优的。仅仅意识到有多个内部优化选项也会帮助你集中精力并提高性能。

图 3.5 – 训练作为损失函数优化的简单表示

图 3.2:训练作为损失函数优化的简单表示。

现在,让我们讨论如何通过特征工程的过程准备模型工厂完成其工作所需的原始材料,即数据。

准备数据

数据可以以各种类型和质量出现。它可以来自关系型数据库的表格数据,也可以是从爬取的网站中获取的非结构化文本,或者是 REST API 的格式化响应,还可以是图像、音频文件,或者任何你能想到的其他形式。

如果你想要在这组数据上运行机器学习算法,你必须做的第一件事是让它对这些算法来说是可读的。这个过程被称为特征工程,接下来的几节将讨论这一点,以为你提供主要原则的基础。关于特征工程有许多优秀的资源可以深入探讨,所以我们在这里只会触及一些主要概念。更多信息,你可以查阅索莱达·加利亚(Soledad Galli)所著的《特征工程食谱》(Feature Engineering Cookbook),Packt 出版社,2022 年版。

为机器学习构建特征

在我们将任何数据输入到机器学习模型之前,它必须被转换成我们模型能够理解的状态。我们还需要确保我们只对那些我们认为有助于提高模型性能的数据进行转换,因为这很容易导致特征数量激增,并成为维度诅咒的受害者。这指的是一系列相关观察,在高维问题中,数据在特征空间中变得越来越稀疏,因此要实现统计显著性可能需要指数级更多的数据。在本节中,我们不会涵盖特征工程的理论基础。相反,我们将关注作为机器学习工程师,我们如何帮助自动化生产中的某些步骤。为此,我们将快速回顾主要类型的特征准备和特征工程步骤,以便我们可以在本章后面的部分添加必要的组件。

构建类别特征

分类别特征是指形成一组非数值的、不同的对象集合,例如星期几或发色。它们可以在你的数据中以多种方式分布。

为了让机器学习算法能够消化类别特征,我们需要将特征转换成某种数值形式,同时确保数值表示不会产生偏差或不适当地影响我们的值。一个例子是,如果我们有一个包含超市中不同产品销售的特征:

data = [['Bleach'], ['Cereal'], ['Toilet Roll']] 

在这里,我们可以使用sklearnOrdinalEncoder将每个类别映射到一个正整数:

from sklearn import preprocessing
ordinal_enc = preprocessing.OrdinalEncoder()
ordinal_enc.fit(data)
# Print returns [[0.]
#    [1.]
#    [2.]]
print(ordinal_enc.transform(data)) 

这就是所谓的序数编码。我们已经将这些特征映射到数字上,所以这里有一个大勾,但这种表示合适吗?好吧,如果你稍微思考一下,其实并不合适。这些数字似乎暗示谷物对漂白剂就像卫生纸对谷物一样,而卫生纸和漂白剂的平均值是谷物。这些陈述没有意义(我也不想在早餐时吃漂白剂和卫生纸),所以这表明我们应该尝试不同的方法。然而,在需要保持分类特征中顺序概念的情况下,这种表示是合适的。一个很好的例子是,如果我们有一个调查,参与者被要求对陈述“早餐是一天中最重要的一餐”发表意见。如果参与者被告知从列表中选择一个选项,如“强烈不同意”,“不同意”,“既不同意也不反对”,“同意”,“强烈同意”,然后我们将这些数据序数编码以映射到数字列表12345,那么我们可以更直观地回答诸如“平均反应是更同意还是不同意?”和“对这个陈述的意见有多普遍?”等问题。序数编码在这里会有帮助,但正如我们之前提到的,在这种情况下并不一定正确。

我们可以做的事情是考虑这个特性中的项目列表,然后提供一个二进制数来表示原始列表中的值是否存在。所以,在这里,我们将决定使用sklearnOneHotEncoder

onehot_enc = preprocessing.OneHotEncoder()
onehot_enc.fit(data)
# Print returns [[1\. 0\. 0.]
#    [0\. 1\. 0.]
#    [0\. 0\. 1.]]
print(onehot_enc.transform(data).toarray()) 

这种表示被称为独热编码。这种方法编码有几个优点,包括以下内容:

  • 没有强制排序的值。

  • 所有特征向量都有单位范数(关于这一点稍后讨论)。

  • 每个独特的特征都与其他特征正交,所以表示中没有隐含的奇怪平均值或距离陈述。

这种方法的一个缺点是,如果你的分类列表包含大量实例,那么你的特征向量的大小将很容易膨胀,我们不得不在算法级别上存储和处理极其稀疏的向量和矩阵。这很容易导致几个实现中的问题,也是可怕的维度诅咒的另一种表现。

在下一节中,将讨论数值特征。

工程数值特征

准备数值特征稍微容易一些,因为我们已经有了数字,但仍有几个步骤需要我们完成以准备许多算法。对于大多数机器学习算法,特征必须在相似的尺度上;例如,它们必须在-1 和 1 或 0 和 1 之间具有幅度。这有一个相对明显的原因,即某些算法会自动将高达百万美元的房价特征和房屋面积的另一个特征赋予更大的权重。这也意味着我们失去了关于特定值在其分布中位置的有用概念。例如,一些算法会从将特征缩放到中值美元价值和中值面积价值都表示为 0.5 而不是 500,000 和 350 中受益。或者,我们可能希望所有分布都具有相同的含义,如果它们是正态分布的,这将允许我们的算法专注于分布的形状而不是它们的位置。

那么,我们该怎么办呢?嗯,就像往常一样,我们不是从零开始,我们可以应用一些标准技术。这里列出了其中一些非常常见的,但它们的数量太多,无法全部包括:

  • 标准化:这是一种数值特征的转换,假设在缩放方差为 1 和平均值为 0 之前,值的分布是正态的或高斯分布。如果你的数据确实是正态的或高斯分布,那么这是一个很好的技术。标准化的数学公式非常简单,所以我在这里提供了它,其中 z 代表变换后的值,x 是原始值,而 分别是平均值和标准差:

图片

  • 最小-最大归一化:在这种情况下,我们希望缩放数值特征,使它们始终在 0 和 1 之间,无论它们遵循的分布类型如何。

这在直观上很容易做到,因为你只需要从任何给定值中减去分布的最小值,然后除以数据的范围(最大值减去最小值)。你可以将这一步视为确保所有值都大于或等于 0。第二步是确保它们的最大尺寸为 1。这可以用一个简单的公式来表示,其中变换后的数字 是原始数字,而 代表该特征的整个分布:

图片

  • 特征向量归一化:在这里,您需要将数据集中的每个样本缩放,使它们的范数等于 1。如果您使用的是距离或特征之间的余弦相似度是重要组成部分的算法,这可能会非常重要,例如在聚类中。它也常与TF-IDF 统计等其他特征工程方法结合使用,在文本分类中。在这种情况下,假设您的整个特征是数值的,您只需计算特征向量的适当范数,然后将每个分量除以该值。例如,如果我们使用特征向量的欧几里得或 L2 范数,,那么我们将通过以下公式转换每个分量,

为了突出这些简单步骤对模型性能的改进,我们将从sklearn葡萄酒数据集的一个简单示例中进行分析。在这里,我们将对未标准化的数据进行 Ridge 分类器的训练,然后对已标准化的数据进行训练。完成这些后,我们将比较结果:

  1. 首先,我们必须导入相关库并设置我们的训练和测试数据:

    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import StandardScaler
    from sklearn.linear_model import RidgeClassifier
    from sklearn import metrics
    from sklearn.datasets import load_wine
    from sklearn.pipeline import make_pipeline
    X, y = load_wine(return_X_y=True) 
    
  2. 然后,我们必须进行典型的 70/30 训练/测试分割:

    X_train, X_test, y_train, y_test =\
    train_test_split(X, y, test_size=0.30, random_state=42) 
    
  3. 接下来,我们必须在不进行特征标准化的情况下训练一个模型,并在测试集上进行预测:

    no_scale_clf = make_pipeline(RidgeClassifier(tol=1e-2,
                                                 solver="sag"))
    no_scale_clf.fit(X_train, y_train)
    y_pred_no_scale = no_scale_clf.predict(X_test) 
    
  4. 最后,我们必须做同样的事情,但添加一个标准化步骤:

    std_scale_clf = make_pipeline(StandardScaler(), RidgeClassifier(tol=1e-2, solver="sag"))
    std_scale_clf.fit(X_train, y_train)
    y_pred_std_scale = std_scale_clf.predict(X_test) 
    
  5. 现在,如果我们打印一些性能指标,我们会看到没有缩放的情况下,预测的准确率为0.76,而其他指标,如precisionrecallf1-score的加权平均值分别为0.830.760.68

    print('\nAccuracy [no scaling]')
    print('{:.2%}\n'.format(metrics.accuracy_score(y_test, y_pred_no_
           scale)))
    print('\nClassification Report [no scaling]')
    print(metrics.classification_report(y_test, y_pred_no_scale)) 
    
  6. 这会产生以下输出:

    Accuracy [no scaling]75.93%
    Classification Report [no scaling]
                  precision    recall  f1-score   support
               0       0.90      1.00      0.95        19
               1       0.66      1.00      0.79        21
               2       1.00      0.07      0.13        14
        accuracy                           0.76        54
       macro avg       0.85      0.69      0.63        54
    weighted avg       0.83      0.76      0.68        54 
    
  7. 在数据标准化的情况下,所有指标都非常好,准确率以及precisionrecallf1-score的加权平均值都达到了0.98

    print('\nAccuracy [scaling]')
    print('{:.2%}\n'.format(metrics.accuracy_score(y_test, y_pred_std_scale)))
    print('\nClassification Report [scaling]')
    print(metrics.classification_report(y_test, y_pred_std_scale)) 
    
  8. 这会产生以下输出:

    Accuracy [scaling]
    98.15%
    Classification Report [scaling]
                  precision    recall  f1-score   support
               0       0.95      1.00      0.97        19
               1       1.00      0.95      0.98        21
               2       1.00      1.00      1.00        14
        accuracy                           0.98        54
       macro avg       0.98      0.98      0.98        54
    weighted avg       0.98      0.98      0.98        54 
    

在这里,我们只需在机器学习训练过程中添加一个简单的步骤,就能看到性能的显著提升。

现在,让我们看看训练是如何设计和在其核心工作的。这将帮助我们为我们的算法和训练方法做出明智的选择。

设计您的训练系统

从最高层次来看,机器学习模型经历一个生命周期,有两个阶段:训练阶段和输出阶段。在训练阶段,模型被喂给数据以从数据集中学习。在预测阶段,模型(包括其优化的参数)按顺序被喂给新数据,并返回所需的输出。

这两个阶段在计算和处理需求上非常不同。在训练阶段,我们必须让模型接触到尽可能多的数据以获得最佳性能,同时确保将数据集的子集保留用于测试和验证。模型训练本质上是一个优化问题,需要几个增量步骤才能得到解决方案。

因此,这需要大量的计算资源,在数据相对较大(或计算资源相对较低)的情况下,可能需要很长时间。即使您有一个小数据集和大量的计算资源,训练仍然不是一个低延迟的过程。此外,它通常是以批量方式运行的,并且数据集的小幅增加对模型性能的影响不大(也有例外)。另一方面,预测是一个更直接的过程,可以将其视为在代码中运行任何计算或函数:输入进入,进行计算,然后输出结果。这(通常)不需要大量的计算资源,并且具有低延迟。

综合来看,这意味着首先,从逻辑和代码的角度来看,将这两个步骤(训练和预测)分开是有意义的。其次,这意味着我们必须考虑这两个阶段的不同执行需求,并将这些需求纳入我们的解决方案设计中。最后,我们需要对训练方案做出选择,包括是否批量安排训练、使用增量学习,或者根据模型性能标准触发训练。这些都是您训练系统中的关键部分。

训练系统设计选项

在我们创建训练系统的任何详细设计之前,一些一般性问题总是适用的:

  • 是否有适合该问题的基础设施可用?

  • 数据在哪里,我们将如何将其输入到算法中?

  • 我是如何测试模型性能的?

在基础设施方面,这可能会非常依赖于您用于训练的模型和数据。如果您打算在具有三个特征的数据上训练线性回归,并且您的数据集只包含 10,000 个表格记录,那么您可能无需过多考虑就能在笔记本电脑级别的硬件上运行。这不是很多数据,并且您的模型没有很多自由参数。如果您要在更大的数据集上训练,例如包含 1 亿个表格记录的数据集,那么您可能可以从 Spark 集群之类的并行化中受益。然而,如果您要在 1,000 张图像上训练一个 100 层的深度卷积神经网络,那么您可能需要使用 GPU。有很多选择,但关键是选择适合这项工作的正确工具。

关于如何将数据输入到算法中的问题,这可能并不简单。我们是否将对远程托管数据库运行 SQL 查询?如果是这样,我们将如何连接到它?运行查询的机器是否有足够的 RAM 来存储数据?

如果不是这样,我们需要考虑使用一种可以逐步学习的算法吗?对于经典的算法性能测试,我们需要使用机器学习领域的知名技巧,并在我们的数据上执行训练/测试/验证拆分。我们还需要决定我们可能想要采用的交叉验证策略。然后,我们需要选择我们偏好的模型性能指标并适当地计算它。然而,作为机器学习工程师,我们也会对其他性能指标感兴趣,例如训练时间、内存的有效使用、延迟,以及(我敢说)成本。我们还需要了解我们如何衡量并优化这些指标。

只要我们在进行过程中牢记这些事情,我们就会处于有利的位置。现在,让我们转向设计。

正如我们在本节引言中提到的,我们需要考虑两个基本方面:训练和输出过程。我们可以以两种方式将这些结合起来作为我们的解决方案。我们将在下一节中讨论这一点。

训练-运行

选项 1是在同一过程中执行训练和预测,训练可以在批量或增量模式下进行。这在下图中以示意图的形式展示。这种模式被称为训练-运行

图 3.2 – 训练-运行过程

图 3.3:训练-运行过程。

这种模式是两种模式中较简单的一种,但也是对现实世界问题最不理想的一种,因为它并不体现我们之前提到的关注点分离原则。这并不意味着它是一个无效的模式,它确实有易于实现的优点。在这里,我们在做出预测之前运行整个训练过程,中间没有真正的中断。根据我们之前的讨论,如果我们必须以非常低延迟的方式提供预测,我们可以自动排除这种方法;例如,通过事件驱动或流式解决方案(稍后会有更多介绍)。

虽然这种方法可能完全有效(我在实践中见过几次),但这可能是在以下情况下:你应用的算法实际上非常轻量级,你需要继续使用非常最新的数据,或者你运行的大批量过程相对不频繁。

虽然这是一个简单的方法,并不适用于所有情况,但它确实具有明显的优势:

  • 由于你训练的频率与预测的频率相同,你正在尽一切可能防止现代性能退化,这意味着你正在对抗漂移(参见本章后面的部分)。

  • 你显著降低了你解决方案的复杂性。虽然你紧密耦合了两个组件,这通常应该避免,但训练和预测阶段可能非常简单,以至于如果你只是将它们放在一起,你会节省大量的开发时间。这是一个非同小可的观点,因为开发时间是有成本的

现在,让我们看看另一种情况。

训练-持久

选项 2是训练以批处理方式运行,而预测以认为合适的任何模式运行,预测解决方案从存储中读取已训练的模型。我们将这种设计模式称为train-persist。这将在以下图中展示:

图 3.3 – train-persist 过程

图 3.4:train-persist 过程。

如果我们要训练我们的模型并持久化模型,以便它可以在以后由预测过程拾取,那么我们需要确保以下几点:

  • 我们有哪些模型存储选项?

  • 是否有明确的机制来访问我们的模型存储(写入和读取)?

  • 我们应该多久训练一次,多久预测一次?

在我们的情况下,我们将通过使用在第二章“机器学习开发过程”中介绍但将在后续部分重新讨论的 MLflow 来解决前两个问题。还有许多其他解决方案可用。关键点是,无论你使用什么作为模型存储和训练与预测过程之间的交接点,都应该以稳健和可访问的方式使用。

第三个点更复杂。你可以在一开始就决定你想按计划进行训练,并坚持下去。或者你可以更复杂,开发出在训练发生之前必须满足的触发标准。再次强调,这是你需要与你的团队一起做出的选择。在本章的后面部分,我们将讨论安排你的训练运行的机制。

在下一节中,我们将探讨如果你想要根据你的模型性能随时间可能退化的情况来触发你的训练运行,你需要做什么。

需要重新训练

你不会期望在完成教育后,就再也不读论文或书籍,也不再与任何人交谈,这意味着你将无法对世界正在发生的事情做出明智的决策。因此,你不应该期望一个机器学习模型一旦训练就永远表现良好。

这个想法直观易懂,但它代表了机器学习模型中一个被称为漂移的正式问题。漂移是一个涵盖你模型性能随时间下降的多种原因的术语。它可以分为两大类:

  • 概念漂移:当你的数据特征与试图预测的结果之间的基本关系发生变化时,就会发生这种情况。有时,这也被称为协变量漂移。一个例子是在训练时,你只有一部分数据似乎显示出特征和结果之间的线性关系。如果结果是,在部署后收集了大量更多数据后,这种关系是非线性的,那么就发生了概念漂移。对此的缓解措施是使用更能代表正确关系的正确数据重新训练。

  • 数据漂移:这种情况发生在你用作特征的变量的统计属性发生变化时。例如,你可能在你的某个模型中使用年龄作为特征,但在训练时间,你只有 16 至 24 岁年龄段的数据。

如果模型被部署,并且你的系统开始摄入更广泛年龄层的数据,那么你就遇到了数据漂移。

事实上,漂移是作为机器学习工程师生活的一部分,因此我们将花费大量时间来了解如何检测和减轻它。但为什么它会发生?正如你所预期的那样,漂移有多种原因需要考虑。让我们考虑一些例子。比如说,你用于采样训练数据的机制在某些方面不合适;也许你为特定的地理区域或人口统计进行了子采样,但你希望模型在更普遍的情况下应用。

在我们操作的问题域中可能存在季节性影响,正如在销售预测或天气预测中可以预期的那样。异常情况可能由“黑天鹅”或罕见事件引起,如地缘政治事件甚至新冠疫情大流行。数据收集过程可能在某个时候引入错误,例如,如果上游系统存在错误或过程本身没有遵循或已改变。最后一个例子在需要手动输入数据的流程中可能特别普遍。如果销售人员要被信任正确标记客户资源管理CRM)系统中销售的当前状态,那么训练或经验较少的销售人员可能不会准确或及时地标记数据。尽管在软件开发领域的许多方面取得了进步,但这种数据收集过程仍然非常普遍,因此你必须在自己的机器学习系统开发中防范这一点。可以通过尝试强制执行数据收集的更多自动化或为输入数据的人提供指南(例如下拉菜单)来略微减轻这种情况,但几乎可以肯定,大量的数据仍然以这种方式收集,并且在未来一段时间内也将如此。

现在应该很清楚,漂移是您系统需要考虑的重要方面,但实际上处理它是一个多步骤的过程。我们首先需要检测漂移。在部署的模型中检测漂移是 MLOps 的关键部分,作为机器学习工程师,您应该将其放在首位。然后我们需要诊断漂移的来源;这通常涉及负责监控的人员进行某种形式的离线调查。我们将提到的工具和技术将帮助您定义工作流程,从而开始自动化这个过程,以便在检测到问题时处理任何可重复的任务。最后,我们需要实施一些措施来补救漂移的影响:这通常涉及使用更新或修正的数据集重新训练模型,但可能需要重新开发或重写模型的关键组件。一般来说,如果您能够构建您的训练系统,以便根据对模型中漂移的了解有意识地触发重新训练,那么您将节省大量的计算资源,因为只有在需要时才进行训练。

下一节将讨论我们可以检测模型中漂移的一些方法。这将帮助我们开始构建解决方案中的智能重新训练策略。

检测数据漂移

到目前为止,我们已经定义了漂移,并且我们知道如果我们想构建复杂的训练系统,检测它将非常重要。下一个合乎逻辑的问题是,我们该如何做呢?

我们在上一节中给出的漂移定义非常定性;随着我们探索有助于我们检测漂移的计算和概念,我们可以开始使这些陈述更加量化。

在本节中,我们将大量依赖 Seldon 的alibi-detect Python 包,在撰写本文时,该包在Anaconda.org上不可用,但在 PyPI 上有。要获取此包,请使用以下命令:

pip install alibi
pip install alibi-detect 

使用alibi-detect包非常简单。在下面的示例中,我们将使用来自sklearnwine数据集,该数据集将在本章的其他地方使用。在这个第一个例子中,我们将数据分割为 50/50,并将其中一个集合称为参考集,另一个称为测试集。然后我们将使用 Kolmogorov-Smirnov 测试来证明这两个数据集之间没有出现数据漂移,正如预期的那样,然后人为地添加一些漂移以显示它已被成功检测:

  1. 首先,我们必须从alibi-detect包中导入TabularDrift检测器,以及用于加载数据和分割数据的相关包:

    from sklearn.datasets import load_wine
    from sklearn.model_selection import train_test_split
    import alibi
    from alibi_detect.cd import TabularDrift 
    
  2. 接下来,我们必须获取并分割数据:

    wine_data = load_wine()
    feature_names = wine_data.feature_names
    X, y = wine_data.data, wine_data.target
    X_ref, X_test, y_ref, y_test = train_test_split(X, y, test_size=0.50,
                                                   random_state=42) 
    
  3. 接下来,我们必须使用参考数据和提供的p-value来初始化我们的漂移检测器,以便在统计显著性测试中使用。如果您希望使您的漂移检测器在数据分布中出现较小差异时触发,您必须选择一个更大的p_val

    cd = TabularDrift(X_ref=X_ref, p_val=.05 ) 
    
  4. 现在我们可以检查测试数据集相对于参考数据集是否存在漂移:

    preds = cd.predict(X_test)
    labels = ['No', 'Yes']
    print('Drift: {}'.format(labels[preds['data']['is_drift']])) 
    
  5. 这返回了 'Drift: No'

  6. 因此,正如预期的那样,我们没有检测到漂移(有关更多信息,请参阅以下 重要提示)。

  7. 尽管在这种情况下没有发生漂移,我们可以轻松地模拟一个场景,其中用于测量化学性质的化学装置经历了校准错误,所有值都被记录为比真实值高 10%。在这种情况下,如果我们再次在相同的参考数据集上运行漂移检测,我们将得到以下输出:

    X_test_cal_error = 1.1*X_test
    preds = cd.predict(X_test_cal_error)
    labels = ['No', 'Yes']
    print('Drift: {}'.format(labels[preds['data']['is_drift']])) 
    
  8. 这返回了 'Drift: Yes',表明漂移已被成功检测到。

重要提示

这个例子非常人为化,但有助于说明这一点。在一个标准的像这样的数据集中,随机抽取的 50%的数据和剩下的 50%的数据之间不会有数据漂移。这就是为什么我们必须人为地 移动 一些点来表明检测器确实起作用。在现实世界场景中,数据漂移可能由于测量所用的传感器更新;到消费者行为的改变;一直到数据库软件或模式的改变而自然发生。因此,要保持警惕,因为许多漂移情况不会像这个例子中那样容易被发现!

这个例子展示了如何用几行简单的 Python 代码检测数据集中的变化,这意味着如果我们不重新训练以考虑数据的新的属性,我们的机器学习模型可能会开始性能下降。我们还可以使用类似的技术来跟踪我们的模型性能指标,例如准确度或均方误差,是否也在漂移。在这种情况下,我们必须确保我们定期在新测试或验证数据集上计算性能。

第一个漂移检测例子非常简单,展示了如何检测一次性数据漂移的基本情况,特别是特征漂移。现在我们将展示检测 标签漂移 的例子,这基本上是相同的,但现在我们只是使用标签作为参考和比较数据集。我们将忽略前几个步骤,因为它们是相同的,并从我们有参考和测试数据集可用的点开始。

  1. 就像在特征漂移的例子中一样,我们可以配置表格漂移检测器,但现在我们将使用初始标签作为我们的基线数据集:

    cd = TabularDrift(X_ref=y_ref, p_val=.05 ) 
    
  2. 我们现在可以检查测试标签相对于参考数据集的漂移:

    preds = cd.predict(y_test)
    labels = ['No', 'Yes']
    print('Drift: {}'.format(labels[preds['data']['is_drift']])) 
    
  3. 这返回了 'Drift: No'

  4. 因此,正如预期的那样,我们没有检测到漂移。请注意,这种方法也可以用作一个好的合理性检查,以确保训练和测试数据标签遵循相似的分布,并且我们的测试数据抽样具有代表性。

  5. 就像上一个例子一样,我们可以模拟数据中的一些漂移,然后检查这确实被检测到了:

    y_test_cal_error = 1.1*y_test
    preds = cd.predict(y_test_cal_error)
    labels = ['No', 'Yes']
    print('Drift: {}'.format(labels[preds['data']['is_drift']])) 
    

我们现在将转向一个更加复杂的场景,即检测概念漂移。

检测概念漂移

概念漂移在本节中进行了描述,并强调这种类型的漂移实际上完全是关于我们模型中变量之间关系的变化。这意味着按照定义,这种类型的案例更有可能很复杂,并且可能很难诊断。

你可以捕捉到概念漂移的最常见方式是通过监控你的模型随时间的变化性能。例如,如果我们再次处理wine分类问题,我们可以查看告诉我们模型分类性能的指标,随着时间的推移绘制这些指标,然后围绕我们可能在这些值中看到的趋势和异常构建逻辑。

我们已经使用过的alibi_detect包包含了一些用于在线漂移检测的有用方法,这些方法可以用来在概念漂移发生时及其影响模型性能时找到它。在这里,“在线”指的是漂移检测发生在单个数据点的层面上,因此即使在生产中数据完全按顺序到来时,这也可能发生。其中一些方法假设 PyTorch 或 TensorFlow 作为后端可用,因为这些方法使用未训练的自动编码器UAEs)作为开箱即用的预处理方法。

作为例子,让我们通过创建和使用这些在线检测器之一,即在线最大均值差异方法,来走一遍。以下示例假设除了参考数据集X_ref外,我们还定义了预期的运行时间ert和窗口大小window_size变量。预期的运行时间是一个变量,表示检测器在引发假阳性检测之前应该运行的平均数据点数。这里的想法是,你希望预期的运行时间更大,但随着它的增大,检测器对实际漂移的敏感性会降低,因此必须找到平衡点。window_size是用于计算适当的漂移测试统计量的滑动数据窗口的大小。较小的window_size意味着你正在调整检测器以在短时间内找到数据或性能的急剧变化,而较长的窗口大小则意味着你正在调整以在更长的时间内寻找更微妙的变化。

  1. 首先,我们导入该方法:

    from alibi_detect.cd import MMDDriftOnline 
    
  2. 我们随后使用前一段中讨论的一些变量设置初始化漂移检测器。我们还包含了我们想要应用的自举模拟次数,以便该方法计算检测漂移的一些阈值。

    根据你为使用的深度学习库设置的硬件配置和数据的大小,这可能会花费一些时间。

    ert = 50
    window_size = 10
    cd = MMDDriftOnline(X_ref, ert, window_size, backend='pytorch',
                        n_bootstraps=2500) 
    
  3. 然后,我们可以通过从Wine数据集取测试数据,并一次输入一个特征向量,来模拟生产环境中的漂移检测。如果给定数据的特征向量由x给出,我们就可以调用漂移检测器的predict方法,并从返回的元数据中检索'is_drift'值,如下所示:

    cd.predict(x)['data'] ['is_drift'] 
    
  4. 对测试数据的所有行执行步骤 2,并在检测到漂移的地方绘制一个垂直的橙色条,得到的图表如图 3.5 所示。

    图 3.5:用于运行一些模拟漂移检测的测试集特征,来自我们使用的Wine数据集。

在这个例子中,我们可以从模拟数据的图表中看到,数据精度随时间发生了变化。如果我们想自动化检测这种行为,那么我们不仅需要简单地绘制这些数据,还需要开始以系统化的方式分析它,并将其纳入我们正在生产的模型监控过程中。

注意:Wine数据集的测试数据仅用于漂移示例。在生产中,这种漂移检测将在从未见过的新数据上运行,但原理是相同的。

现在你已经知道漂移正在发生,我们将继续讨论你如何开始决定在你的漂移检测器上设置哪些限制,然后介绍一些帮助你诊断漂移类型和来源的过程和技术。

设置限制

我们在本节中描述的许多关于漂移的技术与统计学和机器学习中的标准技术非常一致。你可以几乎“直接使用”这些技术来诊断一系列不同类型的问题,但我们还没有讨论如何将这些技术整合成一个连贯的漂移检测机制。在着手做这件事之前,考虑设置数据和模型可接受行为边界是非常重要的,这样你知道何时你的系统应该发出警报或采取某些行动。我们将称之为“设置漂移检测系统的限制”。

那么,你从哪里开始呢?这时事情变得稍微不那么技术性,而且肯定更多地围绕在商业环境中操作,但让我们先概述一些关键点。首先,了解哪些内容重要需要发出警报是很重要的。对你可以想到的所有指标中的偏差发出警报听起来可能是个好主意,但它可能仅仅创建了一个非常嘈杂的系统,难以找到真正值得关注的问题。因此,我们必须谨慎选择我们想要跟踪和监控的内容。接下来,我们需要了解检测问题的及时性要求。这与软件中的服务级别协议SLAs)概念密切相关,它记录了系统所要求的和预期的性能。如果你的业务正在对用于危险条件下的设备运行实时异常检测和预测性维护模型,那么发出警报和采取行动的及时性要求可能相当高。然而,如果你的机器学习系统每周只进行一次财务预测,那么及时性限制可能就没有那么严格。最后,你需要设定限制。这意味着你需要仔细思考你正在跟踪的指标,并思考“什么构成了这里的坏?”或者“我们想被通知什么?”可能的情况是,作为项目发现阶段的一部分,你知道业务对回归模型感到满意,只要它提供合适的置信区间,其预测的准确性可以有很大的变化。

在另一种场景中,你正在构建的分类模型可能必须具有只在相对较窄的范围内波动的召回率;否则,它将危及下游流程的有效性。

漂移的诊断

虽然我们在另一个部分讨论了模型漂移可能存在各种原因,但归根结底,我们必须记住,机器学习模型只对特征进行操作以创建预测。这意味着,如果我们想诊断漂移的源头,我们不需要再往其他地方看,只需关注我们的特征即可。

那么,我们应该从哪里开始呢?首先,我们应该考虑的是,任何特征都有可能发生漂移,但并非所有特征在模型方面都同等重要。这意味着在优先考虑哪些特征需要补救措施之前,我们需要了解特征的重要性。

特征重要性可以通过模型相关或模型无关的方式计算。模型相关的方法特指基于树的模型,如决策树或随机森林。在这些情况下,特征重要性通常可以从模型中提取出来进行检查,具体取决于用于开发模型的包。例如,如果我们使用 Scikit-Learn 训练一个随机森林分类器,我们可以使用以下语法提取其特征重要性。在这个例子中,我们检索随机森林模型的默认特征重要性,这些重要性是通过平均不纯度减少MDI)计算的,也称为“Gini 重要性”,并将它们放入一个有序的 pandas 序列以供后续分析:

import pandas as pd
feature_names = rf[:-1].get_feature_names_out()
mdi_importances = pd.Series(rf[-1].feature_importances_,
                            index=feature_names).sort_values(ascending=True) 

尽管这非常简单,但由于几个原因,有时它可能会给出错误的结果。这里的特征重要性是通过一个不纯度度量计算的,这类度量可能会对高基数(例如数值)特征表现出偏差,并且仅基于训练集数据计算,这意味着它们不考虑模型对未见过的测试数据的泛化能力。在使用此类重要性度量时,这一点始终应牢记在心。

另一个标准的特征重要性度量,它是模型无关的,并缓解了 MDI 或 Gini 重要性的一些问题,是排列重要性。

这是通过选择我们感兴趣的特定特征,对其进行洗牌(即,通过某种重新组织方法移动特征矩阵中的值,向上、向下或通过其他方法),然后重新计算模型精度或误差来实现的。精度或误差的变化然后可以用作衡量该特征重要性的指标,因为重要性特征越少,模型性能在洗牌后变化应该越小。以下是一个使用 Scikit-Learn 的此方法的示例,再次使用我们在上一个示例中使用的相同模型:

from sklearn.inspection import permutation_importance
result = permutation_importance(
    rf, X_test, y_test, n_repeats=10, random_state=42, n_jobs=2
    )
sorted_importances_idx = result.importances_mean.argsort()
importances = pd.DataFrame(
    result.importances[sorted_importances_idx].T,
    columns=X.columns[sorted_importances_idx]) 

最后,还有一种非常流行的确定特征重要性的方法是计算特征SHAPSHapley Additive exPlanation)值。这种方法借鉴了博弈论的思想,考虑了特征如何组合来影响预测。SHAP 值是通过在包含或排除考虑特征的所有特征排列上训练模型来计算的,然后计算该特征的预测值的边际贡献。这与排列重要性不同,因为我们不再只是排列特征值;我们现在实际上正在运行一系列不同的潜在特征集,包括或排除该特征。

您可以通过安装shap包来开始在您的模型上计算 SHAP 值:

pip install shap 

然后我们可以执行以下语法,使用前面示例中的相同随机森林模型来定义一个SHAP 解释器对象并计算测试数据集中特征的 SHAP 值。我们假设这里的X_test是一个以特征名称为列名的 pandas DataFrame:

explainer = shap.Explainer(rf, predict, X_test)
shap_values = explainer(X_test) 

注意,由于运行所有排列,计算 SHAP 值可能需要一些时间。shap_values本身不是特征重要性,但包含了为所有不同的特征组合实验中每个特征计算的 SHAP 值。为了确定特征重要性,你应该取每个特征的shap_values绝对值的平均值。如果你使用以下命令,这会为你完成并绘制结果:

shap.plots.bar(shap_values) 

我们现在已经介绍了三种不同的方法来计算模型的特征重要性,其中两种完全与模型无关。特征重要性对于帮助你快速找到漂移的根源非常有帮助。如果你看到你模型的性能正在漂移或超过你设定的阈值,你可以使用特征重要性来集中你的诊断努力在最重要的特征上,并忽略不那么关键的特征的漂移。

现在我们已经介绍了一种有用的方法来帮助深入挖掘漂移,我们将讨论如何在发现似乎引起最大麻烦的特征或特征后如何着手解决它。

治疗漂移

我们可以采取几种方法来对抗漂移,以保持我们系统的性能:

  • 移除特征并重新训练:如果某些特征正在漂移或表现出某种退化,我们可以尝试移除它们并重新训练模型。这可能会变得耗时,因为我们的数据科学家可能需要重新运行一些分析和测试,以确保这种方法从建模的角度仍然有意义。我们还得考虑我们移除的特征的重要性。

  • 使用更多数据重新训练:如果我们看到概念漂移,我们可能只是注意到模型相对于数据的分布以及这些分布之间的关系已经过时。可能重新训练模型并包含更多最近的数据可以提高性能。还有选择在最近数据的一些选定部分上重新训练模型的选择。如果你能够诊断出数据中的某些重大事件或转变,例如 Covid-19 封锁的引入,这种方法可能特别有用。然而,这种方法可能难以自动化,因此有时也可以选择引入时间窗口方法,即训练一些预先选定数量的数据,直到现在的时间。

  • 回滚模型:我们可以用之前的版本或甚至是一个基线模型来替换当前模型。如果你的基线模型更简单但性能方面也更可预测,例如应用了一些简单的业务逻辑,那么这可以是一个非常不错的做法。能够回滚到模型的先前版本需要你有在模型注册库周围建立一套良好的自动化流程。这非常类似于通用软件工程中的回滚,是构建健壮系统的一个关键组件。

  • 重写或调试解决方案:可能存在这样的情况,我们处理的数据漂移非常严重,以至于现有的模型无法应对上述任何一种方法。重写模型的想法可能看起来有些激进,但这可能比你想象的更常见。例如,最初你可能部署了一个经过良好调优的 LightGBM 模型,该模型每天对一组五个特征进行二元分类。运行解决方案数月后,可能在你多次检测到模型性能漂移后,你决定最好进行一次调查,看看是否有更好的方法。在这种情况下,这尤其有帮助,因为现在你对将在生产中看到的数据有了更多的了解。你可能会发现,实际上,随机森林分类器在相同的生产数据场景上的平均性能并不如 LightGBM 模型,但它更稳定,表现更一致,并且更少触发漂移警报。你可能会决定,实际上,将这个不同的模型部署到同一个系统中对业务更有利,因为它将减少处理漂移警报的操作开销,并且这将是一个业务可以更加信任的模型。重要的是要注意,如果你需要编写一个新的管道或模型,在团队进行这项工作期间回滚到先前的模型通常是很重要的。

  • 修复数据源:有时,最具挑战性的问题实际上与底层模型无关,而更多与数据收集方式的变化以及如何将数据传递到你的系统下游有关。在许多业务场景中,由于新流程的引入、系统的更新,甚至由于负责输入某些源数据的个人人员的变动,数据的收集、数据的转换或数据的特征可能会发生变化。作者自己的一个很好的例子是,当涉及到客户资源管理(CRM)系统时,销售团队输入的数据质量可能取决于许多因素,因此合理地预期数据质量、一致性和及时性可能会出现缓慢或突然的变化。

在这种情况下,正确的答案可能实际上不是一个工程问题,而是一个流程问题,与适当的团队和利益相关者合作,确保数据质量得到维护,并遵循标准流程。这将有利于客户和业务,但仍可能难以推销。

现在,我们可以开始构建解决方案,这些解决方案将自动触发我们的 ML 模型重新训练,如图图 3.6所示:

图 3.4 – 漂移检测和训练系统过程的示例

图 3.6:漂移检测和训练系统过程的示例。

其他监控工具

本章中的示例主要使用了 alibi-detect 包,但我们现在正处于开源MLOps工具的黄金时代。有几种不同的包和解决方案可供选择,你可以开始使用它们来构建监控解决方案,而无需花费一分钱。

在本节中,我们将快速介绍这些工具,并展示它们语法的一些基本要点,以便如果你想要开发监控管道,那么你可以立即开始,并知道在哪里最好使用这些不同的工具。

首先,我们将介绍Evidently AI(www.evidentlyai.com/),这是一个非常易于使用的 Python 包,它允许用户不仅监控他们的模型,还可以通过几行语法创建可定制的仪表板。以下是文档中入门指南的改编。

  1. 首先,安装 Evidently:

    pip install evidently 
    
  2. 导入Report功能。Report是一个对象,它收集多个指标的计算结果,以便进行可视化或以 JSON 对象的形式输出。我们将在稍后展示这种后者的行为:

    from evidently.report import Report 
    
  3. 接下来,导入一个称为度量预设的东西,在这种情况下是针对数据漂移的。我们可以将其视为一个模板化的报告对象,我们可以在以后对其进行自定义:

    from evidently.metric_preset import DataDriftPreset 
    
  4. 接下来,假设你已经有了数据,然后你可以运行数据漂移报告。假设你手头有之前示例中的Wine数据集。如果我们使用scikit-learntrain_test_split()方法将葡萄酒数据分成 50/50,我们将有两个数据集,我们再次使用它们来模拟参考数据集X_ref和当前数据集X_curr

    data_drift_report = Report(metrics=[
    DataDriftPreset(), 
        ])
    report.run(
        reference_data=X_ref,
       current_data=X_ref
    ) 
    
  5. Evidently 随后提供了一些非常棒的功能,用于在报告中可视化结果。你可以使用几种不同的方法导出或查看这些结果。你可以将报告导出为 JSON 或 HTML 对象,以便消费或审查下游或其他应用程序。图 3.7图 3.8显示了使用以下命令创建这些输出时的结果片段:

    data_drift_report.save_json('data_drift_report.json')
    data_drift_report.save_html('data_drift_report.xhtml') 
    

    图片 B19525_03_07

    图 3.7:Evidently 报告的 50/50 分割葡萄酒特征集的 JSON 输出。

图片 B19525_03_08

图 3.8:Evidently 生成的 50/50 分割葡萄酒特征集的漂移报告的 HTML 版本。

渲染的 HTML 报告的一个优点是你可以动态地深入到一些有用的信息中。例如,图 3.9 显示,如果你点击进入任何特征,你会得到一个随时间变化的数据漂移图,而图 3.10 显示你也可以以同样的方式得到特征的分布图。

图片

图 3.9:当你钻入 Evidently 报告中的葡萄酒特征时自动生成数据漂移图。

图片

图 3.10:当你钻入 Evidently 报告中的葡萄酒特征集时自动生成的直方图,显示了特征的分布。

这只是触及了你可以用 Evidently 做到的事情的皮毛。有很多功能可以用来生成你自己的模型测试套件,监控功能,以及像我们看到的这样优雅地可视化所有内容。

现在我们已经探讨了模型和数据漂移的概念以及如何检测它们,我们可以继续讨论如何将我们在本章中讨论的许多概念自动化。

接下来的几节将深入探讨训练过程的不同方面,特别是如何使用各种工具自动化这个过程。

自动化训练

训练过程是模型工厂的一个组成部分,也是机器学习工程和传统软件工程之间主要区别之一。接下来的几节将详细讨论我们如何开始使用一些优秀的开源工具来简化、优化,在某些情况下,完全自动化这个过程的一些元素。

自动化层次结构

机器学习现在是软件开发的一个常见部分,也是商业和学术活动的一个主要部分,其中一个主要原因是工具的多样性。所有包含复杂算法有效和优化实现的包和库都允许人们在这些基础上构建,而不是每次遇到问题都要重新实现基础知识。

这是软件开发中抽象理念的一个强大表达,其中较低级别的单元可以在较高级别的实现中被利用和参与。

这个想法甚至可以进一步扩展到整个训练过程本身。在实施的最底层(但在底层算法的意义上仍然是一个非常高的层次),我们可以提供关于我们希望训练过程如何进行的详细信息。我们可以在代码中手动定义用于训练运行的精确超参数集(参见下一节关于优化超参数)。我称之为手动操作。然后我们可以再提高一个抽象层次,为我们的超参数提供范围和界限,供设计用于高效采样和测试我们模型性能的工具使用;例如,自动超参数调整。最后,在过去几年中,有一个更高层次的抽象引发了大量的媒体关注,即我们优化运行哪个算法。这被称为自动机器学习AutoML

围绕 AutoML 可能会有很多炒作,有些人宣称最终所有机器学习开发职位都将实现自动化。在我看来,这并不现实,因为选择你的模型和超参数只是巨大复杂工程挑战的一个方面(因此这是一本书而不是传单!)。然而,AutoML 是一个非常强大的工具,当你开始下一个机器学习项目时,应该将其添加到你的能力工具箱中。

我们可以将所有这些内容简洁地总结为自动化层次结构;基本上,作为机器学习工程师的你,在训练过程中希望有多少控制权?我曾经听到有人用汽车齿轮控制来描述这一点(感谢:Databricks at Spark AI 2019)。手动操作相当于驾驶手动挡汽车,完全控制齿轮:需要考虑的事情更多,但如果你知道自己在做什么,它可以非常高效。再高一个层次,你有自动挡汽车:需要担心的事情更少,这样你可以更多地专注于到达目的地、交通和其他挑战。这对很多人来说是一个不错的选择,但仍需要你具备足够的知识、技能和理解。最后,我们有自动驾驶汽车:放松,放松,甚至不用担心如何到达目的地。你可以专注于到达那里后你要做什么。

这种自动化层次结构在以下图中展示:

图 3.6 – 机器学习模型优化自动化的层次结构,其中 AutoML 是最自动化的可能性

图 3.11:机器学习模型优化自动化的层次结构,其中 AutoML 是最自动化的可能性。

总结来说,这就是不同层次的训练抽象是如何相互关联的。

在接下来的几节中,我们将讨论如何开始构建超参数优化和 AutoML 的实现。我们不会涵盖“手动操作”,因为这很容易理解。

优化超参数

当你将某种数学函数拟合到数据上时,一些值在拟合或训练过程中被调整:这些被称为参数。对于机器学习,我们有一个更高级别的抽象,我们必须定义告诉我们所采用的算法如何更新参数的值。这些值被称为超参数,它们的选择是训练机器学习算法的重要暗黑艺术之一。

以下表格列出了一些用于常见机器学习算法的超参数,以展示它们可能采取的不同形式。这些列表并不全面,但旨在强调超参数优化并非一项简单的练习:

算法超参数这控制什么
决策树和随机森林
  • 树的深度。

  • 最小/最大叶子节点。

|

  • 你的树有多少层。

  • 每个级别可以发生的分支数量。

|

支持向量机
  • C

  • Gamma

|

  • 误分类的惩罚。

  • 训练点对径向基函数RBF)核的影响半径。

|

神经网络(众多架构)
  • 学习率。

  • 隐藏层数量。

  • 激活函数。

  • 许多更多。

|

  • 更新步长大小。

  • 你的网络有多深。

  • 你神经元的触发条件。

|

逻辑回归
  • 求解器

  • 正则化类型。

  • 正则化预因子。

|

  • 如何最小化损失。

  • 如何防止过拟合/使问题表现良好。

  • 正则化类型的强度。

|

表 3.1:一些超参数及其对某些监督算法的控制。

更多的示例可以在以下表格中看到:

算法超参数这控制什么
K 最近邻
  • K

  • 距离度量。

|

  • 聚类的数量。

  • 如何定义点之间的距离。

|

DBSCAN
  • Epsilon

  • 最小样本数。

  • 距离度量。

|

  • 考虑邻居的最大距离。

  • 需要多少邻居才能被认为是核心。

  • 如何定义点之间的距离。

|

表 3.2:一些超参数及其对某些无监督算法的控制。

所有这些超参数都有它们自己可以取的特定值集。这个超参数值的范围对于你想要应用于你的机器学习解决方案的不同潜在算法意味着有无数种定义一个工作模型(意味着一个不会破坏你使用的实现)的方法,但你是如何找到最优模型的呢?

这就是超参数搜索的用武之地。其概念是,对于有限数量的超参数值组合,我们希望找到一组能给出最佳模型性能的值。这是另一个类似于最初训练的优化问题!

在接下来的章节中,我们将讨论两个非常流行的超参数优化库,并展示如何在几行 Python 代码中实现它们。

重要提示

理解这些超参数库中使用的算法非常重要,因为你可能希望从每个库中使用几个不同的实现来比较不同的方法和评估性能。如果你没有查看它们在底层是如何工作的,你可能会轻易地进行不公平的比较——或者更糟糕的是,你可能会在不知情的情况下比较几乎相同的东西!如果你对这些解决方案的工作原理有一些深入了解,你也将能够更好地判断它们何时有益,何时过度。目标是掌握一些这些算法和方法,因为这将帮助你设计更全面的训练系统,其中算法调整方法相互补充。

Hyperopt

Hyperopt是一个开源的 Python 包,自称是用于复杂搜索空间的串行和并行优化,这些搜索空间可能包括实值、离散和条件维度。更多信息请查看以下链接:github.com/Hyperopt/Hyperopt。在撰写本文时,版本 0.2.5 包含了三个算法,用于在用户提供的搜索空间中执行优化:

  • 随机搜索:该算法本质上是在你提供的参数值范围内选择随机数并尝试它们。然后根据你选择的性能目标函数评估哪些数字组合提供了最佳性能。

  • 帕累托树估计器TPE):这是一种贝叶斯优化方法,它对目标函数阈值以下和以上的超参数分布进行建模(大致为的评分者),然后旨在从的超参数分布中抽取更多值。

  • 自适应 TPE:这是 TPE 的一个修改版本,它允许对搜索进行一些优化,以及创建一个机器学习模型来帮助指导优化过程。

Hyperopt 的存储库和文档包含了一些很好的详细示例。我们在这里不会详细介绍这些示例。相反,我们将学习如何使用它来构建一个简单的分类模型,例如我们在第一章机器学习工程导论中定义的模型。让我们开始吧:

  1. 在 Hyperopt 中,我们必须定义我们想要优化的超参数。例如,对于一个典型的逻辑回归问题,我们可以定义超参数空间,包括我们是否希望每次都重用从先前模型运行中学习到的参数(warm_start),我们是否希望模型在决策函数中包含偏差(fit_intercept),用于决定何时停止优化的容差设置(tol),正则化参数(C),我们想要尝试的solver,以及任何训练运行中的最大迭代次数max_iter

    from Hyperopt import hp
    
    space = {
        'warm_start' : hp.choice('warm_start', [True, False]),
        'fit_intercept' : hp.choice('fit_intercept', [True, False]),
        'tol' : hp.uniform('tol', 0.00001, 0.0001),
        'C' : hp.uniform('C', 0.05, 2.5),
        'solver' : hp.choice('solver', ['newton-cg', 'lbfgs',
                                        'liblinear']),
        'max_iter' : hp.choice('max_iter', range(10,500))
        } 
    
  2. 然后,我们必须定义一个要优化的目标函数。在我们的分类算法的情况下,我们可以简单地定义我们想要最小化的loss函数为 1 减去f1-score。请注意,如果您使用fmin功能,Hyperopt 允许您的目标函数通过您的返回语句提供运行统计信息和元数据。如果您这样做,唯一的要求是您必须返回一个标记为loss的值和一个有效的状态值从Hyperopt.STATUS_STRING列表中(默认为ok,如果计算中存在您想要标记为失败的问题则为fail):

    def objective(params, n_folds, X, y):
        # Perform n_fold cross validation with hyperparameters
        clf = LogisticRegression(**params, random_state=42)
        scores = cross_val_score(clf, X, y, cv=n_folds, scoring=
                                 'f1_macro')
        # Extract the best score
        max_score = max(scores)
        # Loss must be minimized
        loss = 1 - max_score
        # Dictionary with information for evaluation
        return {'loss': loss, 'params': params, 'status': STATUS_OK} 
    
  3. 现在,我们必须使用 fmin 方法与 TPE 算法进行优化:

    # Trials object to track progress
    trials = Trials()
    # Optimize
    best = fmin(
        fn=partial(objective, n_folds=n_folds, X=X_train, y=y_train),
        space=space,
        algo=tpe.suggest,
        max_evals=16,
        trials=trials
        ) 
    
  4. best 的内容是一个包含您在定义的搜索空间中所有最佳超参数的字典。因此,在这种情况下,我们有以下内容:

    {'C': 0.26895003542493234,
    'fit_intercept': 1,
    'max_iter': 452,
    'solver': 2,
    'tol': 1.863336145787027e-05,
    'warm_start': 1} 
    

然后,您可以使用这些超参数来定义您的模型,以便在数据上进行训练。

Optuna

Optuna 是一个基于一些核心设计原则(如其 define-by-run API 和模块化架构)的软件包。在这里,“define-by-run”指的是,当使用 Optuna 时,用户不需要定义要测试的完整参数集,这是 define-and-run。相反,他们可以提供一些初始值,并要求 Optuna 建议要运行的实验集。这为用户节省了时间,并减少了代码的复杂度(对我来说是两个大优点!)。

Optuna 包含四种基本搜索算法:网格搜索随机搜索TPE协方差矩阵自适应进化策略CMA-ES)算法。我们之前已经介绍了前三种,但 CMA-ES 是混合中的一项重要补充。正如其名称所暗示的,它基于进化算法,并从多元高斯分布中抽取超参数样本。然后,它使用给定目标函数评估分数的排名来动态更新高斯分布的参数(协方差矩阵是其中之一)以帮助快速且稳健地在搜索空间中找到最优解。

然而,使 Optuna 的优化过程与 Hyperopt 不同的关键因素在于其应用了剪枝自动早期停止。在优化过程中,如果 Optuna 检测到一组超参数的试验不会导致更好的整体训练算法,它将终止该试验。该软件包的开发者建议,通过减少不必要的计算,这可以在超参数优化过程中带来整体效率的提升。

这里,我们正在查看之前查看过的相同示例,但现在我们使用 Optuna 而不是 Hyperopt:

  1. 首先,当使用 Optuna 时,我们可以使用一个称为 Study 的对象来工作,它为我们提供了一个方便的方法将搜索空间折叠到我们的 objective 函数中:

    def objective(trial, n_folds, X, y):
        """Objective function for tuning logistic regression hyperparameters"""
        params = {
            'warm_start': 
            trial.suggest_categorical('warm_start', [True, False]),
            'fit_intercept': 
            trial.suggest_categorical('fit_intercept', [True, False]),
            'tol': trial.suggest_uniform('tol', 0.00001, 0.0001),
            'C': trial.suggest_uniform('C', 0.05, 2.5),
            'solver': trial.suggest_categorical('solver', ['newton-cg',
                                                'lbfgs', 'liblinear']),
            'max_iter': trial.suggest_categorical('max_iter', 
                                                   range(10, 500))
        }
        # Perform n_fold cross validation with hyperparameters
        clf = LogisticRegression(**params, random_state=42)
        scores = cross_val_score(clf, X, y, cv=n_folds, 
                                 scoring='f1_macro')
        # Extract the best score
        max_score = max(scores)
        # Loss must be minimized
        loss = 1 - max_score
        # Dictionary with information for evaluation
        return loss 
    
  2. 现在,我们必须以与 Hyperopt 示例中相同的方式设置数据:

    n_folds = 5
    X, y = datasets.make_classification(n_samples=100000, n_features=20,n_informative=2, n_redundant=2)
    train_samples = 100  # Samples used for training the models
    X_train = X[:train_samples]
    X_test = X[train_samples:]
    y_train = y[:train_samples]
    y_test = y[train_samples:] 
    
  3. 现在,我们可以定义我们之前提到的Study对象,并告诉它我们希望如何优化objective函数返回的值,包括在study中运行多少次试验的指导。在这里,我们将再次使用 TPE 采样算法:

    from optuna.samplers import TPESampler
    study = optuna.create_study(direction='minimize', sampler=TPESampler())
    study.optimize(partial(objective, n_folds=n_folds, X=X_train, y=y_
                           train), n_trials=16) 
    
  4. 现在,我们可以通过study.best_trial.params变量访问最佳参数,它为我们提供了以下最佳情况下的值:

    {'warm_start': False,
    'fit_intercept': False,
    'tol': 9.866562116436095e-05,
    'C': 0.08907657649508408,
    'solver': 'newton-cg',
    'max_iter': 108} 
    

如你所见,Optuna 也非常简单易用且功能强大。现在,让我们来看看自动化层次结构的最后一级:AutoML。

重要注意事项

你会注意到这些值与 Hyperopt 返回的值不同。这是因为我们每种情况下只运行了 16 次试验,所以我们并没有有效地对空间进行子采样。如果你连续几次运行 Hyperopt 或 Optuna 样本,你可能会得到相当不同的结果,原因相同。这里给出的例子只是为了展示语法,但如果你有兴趣,你可以将迭代次数设置得非常高(或者创建更小的空间进行采样),两种方法的结果应该大致收敛。

AutoML

我们层次结构的最后一级是我们作为工程师对训练过程直接控制最少的一级,但也是我们可能以极少的努力获得良好答案的地方!

为了搜索你问题的许多超参数和算法,所需的开发时间可能很大,即使你编写了看起来合理的搜索参数和循环。

因此,在过去的几年里,已经部署了多种语言的多种AutoML库和工具。围绕这些技术的炒作意味着它们获得了大量的关注,这导致一些数据科学家质疑他们的工作何时会被自动化。正如我们在本章前面提到的,在我看来,宣布数据科学的死亡是极其过早的,并且从组织和业务绩效的角度来看也是危险的。这些工具被赋予了如此伪神话的地位,以至于许多公司可能会相信,仅仅使用它们几次就能解决他们所有的数据科学和机器学习问题。

他们是错的,但也是对的。

这些工具和技术确实非常强大,并且可以帮助改善某些事情,但它们并不是一个神奇的即插即用的万能药。让我们来探讨这些工具,并开始思考如何将它们融入我们的机器学习工程工作流程和解决方案中。

auto-sklearn

我们最喜欢的库之一,古老的 Scikit-Learn,注定会成为构建流行 AutoML 库的第一个目标之一。auto-sklearn 的一个非常强大的特性是,它的 API 被设计得非常灵活,使得优化模型和超参数的主要对象可以无缝地替换到你的代码中。

如同往常,一个例子将更清楚地展示这一点。在下面的例子中,我们将假设Wine数据集(本章的宠儿)已经被检索并按照其他示例(如检测漂移部分中的示例)分割成训练样本和测试样本:

  1. 首先,由于这是一个分类问题,我们需要从auto-sklearn中获取的主要东西是autosklearn.classification对象:

    import numpy as np
    import sklearn.datasets
    import sklearn.metrics
    import autosklearn.classification 
    
  2. 我们必须首先定义我们的auto-sklearn对象。这提供了几个参数,帮助我们定义模型和超参数调整过程将如何进行。在这个例子中,我们将为整体优化提供一个秒数上限,并为任何单个对 ML 模型的调用提供一个秒数上限:

    automl = autosklearn.classification.AutoSklearnClassifier(
        time_left_for_this_task=60,
        per_run_time_limit=30
        ) 
    
  3. 然后,就像我们拟合一个正常的sklearn分类器一样,我们可以拟合auto-sklearn对象。正如我们之前提到的,auto-sklearn API 已经被设计得看起来很熟悉:

    automl.fit(X_train, y_train, dataset_name='wine') 
    
  4. 现在我们已经拟合了对象,我们可以开始分析对象在优化运行期间所取得的成果。

  5. 首先,我们可以看到尝试了哪些模型,哪些被保留在对象中作为最终集成的一部分:

    print(automl.show_models()) 
    
  6. 我们可以获取运行的主要统计数据:

    print(automl.sprint_statistics()) 
    
  7. 然后,我们可以预测一些文本特征,正如预期的那样:

    predictions = automl.predict(X_test) 
    
  8. 最后,我们可以使用我们最喜欢的指标计算器来检查我们的表现——在这种情况下,是sklearn metrics模块:

    sklearn.metrics.accuracy_score(y_test, predictions) 
    
  9. 如您所见,开始使用这个强大的库非常简单,尤其是如果您已经熟悉sklearn

接下来,让我们讨论如何将这个概念扩展到神经网络,由于它们的潜在模型架构不同,神经网络有一个额外的复杂层。

AutoKeras

AutoML 在神经网络领域取得了巨大成功的一个特定领域是因为,对于神经网络来说,“什么是最优模型?”这个问题非常复杂。对于我们的典型分类器,我们通常可以想到一个相对较短、有限的算法列表来尝试。对于神经网络,我们没有这样一个有限的列表。相反,我们有一个本质上无限的神经网络架构集合;例如,将神经元组织成层以及它们之间的连接。寻找最优神经网络架构是一个问题,其中强大的优化可以使作为 ML 工程师或数据科学家的您的生活变得容易得多。

在这个例子中,我们将探索一个基于非常流行的神经网络 API 库(名为 Keras)构建的 AutoML 解决方案。难以置信,这个包的名字是——您猜对了——AutoKeras!

对于这个例子,我们再次假设Wine数据集已经被加载,这样我们就可以专注于实现细节。让我们开始吧:

  1. 首先,我们必须导入autokeras库:

    import autokeras as ak 
    
  2. 现在,是时候享受乐趣了,对于autokeras来说,这一点尤其简单!由于我们的数据是有结构的(表格形式,具有定义的架构),我们可以使用StructuredDataClassifier对象,它封装了自动神经网络架构和超参数搜索的底层机制:

    clf = ak.StructuredDataClassifier(max_trials=5) 
    
  3. 然后,我们只需拟合这个分类器对象,注意到它与sklearn API 的相似性。记住,我们假设训练数据和测试数据存在于pandas DataFrames中,就像本章其他示例中那样:

    clf.fit(x=X_train, y=y_train) 
    
  4. AutoKeras 中的训练对象包含一个方便的评估方法。让我们使用这个方法来看看我们的解决方案有多准确:

    accuracy=clf.evaluate(x=X_train, y=y_train) 
    
  5. 有了这些,我们已经成功地在几行 Python 代码中执行了神经网络架构和超参数搜索。一如既往,阅读解决方案文档以获取有关您可以提供给不同方法的参数的更多信息。

现在我们已经介绍了如何创建性能良好的模型,在下一节中,我们将学习如何持久化这些模型,以便它们可以在其他程序中使用。

持久化你的模型

在上一章中,我们介绍了使用 MLflow 的一些模型版本控制的基本知识。特别是,我们讨论了如何使用 MLflow 跟踪 API 记录您的 ML 实验的指标。现在,我们将在此基础上构建,并考虑我们的训练系统应该与模型控制系统的一般触点。

首先,让我们回顾一下我们希望通过训练系统要完成的事情。我们希望尽可能自动化数据科学家在寻找第一个工作模型时所做的许多工作,这样我们就可以持续更新并创建新的模型版本,这些版本在未来仍然可以解决问题。我们还希望有一个简单的机制,允许将训练过程的结果与将在生产中执行预测的解决方案部分共享。我们可以将我们的模型版本控制系统视为连接我们在第二章“机器学习开发过程”中讨论的 ML 开发过程不同阶段的桥梁。特别是,我们可以看到跟踪实验结果的能力使我们能够在“Play”阶段保持结果,并在“Develop”阶段在此基础上进行构建。我们还可以在“Develop”阶段相同的地点跟踪更多的实验、测试运行和超参数优化结果。然后,我们可以开始标记性能良好的模型为部署的良好候选者,从而弥合“Develop”和“Deploy”开发阶段之间的差距。

如果我们现在专注于 MLflow(尽管还有许多其他解决方案可以满足模型版本控制系统所需的需求),那么 MLflow 的跟踪和模型注册功能很好地填补了这些桥梁角色。这在下图中以示意图的形式表示:

图 3.9 – MLflow 跟踪和模型注册表功能如何帮助我们通过 ML 开发过程的不同阶段

图 3.12:MLflow 跟踪和模型注册表功能如何帮助我们通过 ML 开发过程的不同阶段。

第二章机器学习开发过程中,我们只探讨了 MLflow 跟踪 API 的基本功能,用于存储实验模型运行元数据。现在,我们将简要介绍如何以非常有序的方式存储生产就绪模型,以便您可以开始执行模型部署。这是模型可以通过准备阶段进行推进的过程,如果您愿意,您可以在生产中交换模型。这是任何提供模型并作为部署解决方案一部分运行的训练系统的极其重要的部分,这正是本书的主题!

如前所述,我们在 MLflow 中需要的功能称为模型注册表,它使您能够管理模型在整个开发周期中的部署。在这里,我们将通过示例了解如何将记录的模型推送到注册表,如何更新注册表中的信息,例如模型版本号,然后如何将模型推进不同的生命周期阶段。我们将通过学习如何在其他程序中从注册表中检索给定的模型来结束本节,如果我们想要在分开的训练和预测服务之间共享模型,这是一个关键点。

在我们深入研究与模型注册表交互的 Python 代码之前,我们有一个重要的设置要执行。注册表仅在数据库用于存储模型元数据和参数时才有效。这与仅使用文件后端存储的基本跟踪 API 不同。这意味着在将模型推送到模型注册表之前,我们必须启动一个具有数据库后端的 MLflow 服务器。您可以通过在终端中执行以下命令使用本地运行的SQLite数据库来完成此操作。

您必须在阅读本节其余部分的代码片段之前运行此命令(此命令存储在本书的 GitHub 仓库中的简短 Bash 脚本中,位于github.com/PacktPublishing/Machine-Learning-Engineering-with-Python/blob/main/Chapter03/mlflow-advanced/start-mlflow-server.sh):

mlflow server \
    --backend-store-uri sqlite:///mlflow.db \
    --default-artifact-root ./artifacts \
    --host 0.0.0.0 

现在,后端数据库已启动并运行,我们可以将其作为模型工作流程的一部分使用。让我们开始吧:

  1. 让我们从记录本章早期训练的某个模型的指标和参数开始:

    with mlflow.start_run(run_name="YOUR_RUN_NAME") as run:
        params = {'tol': 1e-2, 'solver': 'sag'}
        std_scale_clf = make_pipeline(StandardScaler(),
                                      RidgeClassifier(**params))
        std_scale_clf.fit(X_train, y_train)
        y_pred_std_scale = std_scale_clf.predict(X_test)
        mlflow.log_metrics({
                 "accuracy":
                  metrics.accuracy_score(y_test, y_pred_std_scale),
                 "precision":
                  metrics.precision_score(y_test, y_pred_std_scale, 
                                          average="macro"),
                 "f1": 
                  metrics.f1_score(y_test, y_pred_std_scale,
                                   average="macro"),
                 "recall":
                  metrics.recall_score(y_test, y_pred_std_scale, 
                                       average="macro"),
        })
    mlflow.log_params(params) 
    
  2. 在相同的代码块中,我们现在可以将模型记录到模型注册表中,并为模型提供一个名称以便稍后引用:

     mlflow.sklearn.log_model(
                sk_model=std_scale_clf,
                artifact_path="sklearn-model",
                registered_model_name="sk-learn-std-scale-clf"
            ) 
    
  3. 现在,让我们假设我们正在运行一个预测服务,并且我们想要检索模型并使用它进行预测。在这里,我们必须编写以下代码:

    model_name = "sk-learn-std-scale-clf"
    model_version = 1
    model = mlflow.pyfunc.load_model(
        model_uri=f"models:/{model_name}/{model_version}"
        )
    model.predict(X_test) 
    
  4. 默认情况下,在模型注册表中新注册的模型被分配 'Staging' 阶段值。因此,如果我们想根据阶段而不是模型版本来检索模型,我们可以执行以下代码:

    stage = 'Staging'
    model = mlflow.pyfunc.load_model(
        model_uri=f"models:/{model_name}/{stage}"
        ) 
    
  5. 基于本章的所有讨论,我们的训练系统必须能够生成一个我们愿意部署到生产的模型。以下代码片段将模型提升到不同的阶段,称为 "Production"

    client = MlflowClient()
    client.transition_model_version_stage(
        name="sk-learn-std-scale-clf",
        version=1,
        stage="Production"
        ) 
    
  6. 这些是与模型注册表交互的最重要方式,我们已经涵盖了如何在训练(和预测)系统中注册、更新、提升和检索您的模型的基础知识。

现在,我们将学习如何将我们的主要训练步骤链接成单个单元,称为 管道。我们将介绍一些在单个脚本中执行此操作的标准方法,这将使我们能够构建我们的第一个训练管道。在 第五章,部署模式和工具 中,我们将介绍构建更通用的软件管道的工具,这些工具适用于您的 ML 解决方案(其中您的训练管道可能是一个组件)。

使用管道构建模型工厂

软件管道的概念足够直观。如果你在代码中将一系列步骤链接在一起,使得下一个步骤消耗或使用前一个步骤或步骤的输出,那么你就有一个管道。

在本节中,当我们提到管道时,我们将特别处理包含适合 ML 的处理或计算的步骤。例如,以下图表显示了这一概念可能如何应用于第一章,ML 工程简介中提到的营销分类器的一些步骤:

图 3.10 – 任何训练管道的主要阶段以及这与第一章,ML 工程简介中特定案例的映射

图 3.13:任何训练管道的主要阶段以及这与第一章,ML 工程简介中特定案例的映射。

让我们讨论一些构建代码中 ML 管道的标准工具。

Scikit-learn 管道

我们的老朋友 Scikit-Learn 随带了一些不错的管道功能。API 非常易于使用,正如您所期望的 Scikit-Learn 一样,但有一些概念我们在继续之前应该理解:

  • 管道对象:这是将汇集我们所需所有步骤的对象,特别是sklearn要求实例化的管道对象由转换器和估计器的序列组成,所有中间对象都具有.fit().transform()方法,最后一步是一个至少具有.fit()方法的估计器。我们将在下两点中解释这些术语。这种条件的原因是,pipeline对象将继承序列中最后一个项目的方法,因此我们必须确保最后一个对象中存在.fit()

  • 估计器:估计器类是scikit-learn中的基本对象,任何可以在数据上拟合并预测数据的包中的内容,因此.fit().predict()方法是估计器类的子类。

  • 转换器:在Scikit-Learn中,转换器是任何具有.transform().fit_transform()方法的估计器,正如你可以猜到的,它们主要专注于将数据集从一种形式转换为另一种形式,而不是执行预测。

使用pipeline对象确实有助于简化你的代码,因为你不必编写多个不同的拟合、转换和预测步骤作为它们自己的函数调用,并管理数据流,你只需将它们全部组合在一个对象中,该对象为你管理这些操作并使用相同的简单 API。

Scikit-Learn 不断添加新的转换器和功能,这意味着可以构建越来越有用的管道。例如,在撰写本文时,Scikit-Learn 版本大于 0.20 也包含ColumnTransformer对象,它允许你构建对特定列执行不同操作的管道。这正是我们之前讨论的逻辑回归营销模型示例所希望做的,我们希望标准化数值并one-hot编码分类变量。让我们开始吧:

  1. 要创建此管道,你需要导入ColumnTransformerPipeline对象:

    from sklearn.compose import ColumnTransformer
    from sklearn.pipeline import Pipeline 
    
  2. 为了展示如何在管道组成的转换器内部链式调用步骤,我们将在稍后添加一些插补。为此,我们需要导入SimpleImputer对象:

    from sklearn.impute import SimpleImputer 
    
  3. 现在,我们必须定义包含插补和缩放两个步骤的数值转换器子管道,我们还必须定义将应用到此数值列的名称,以便我们可以在以后使用它们:

    numeric_features = ['age', 'balance']
    numeric_transformer = Pipeline(steps=[
        ('imputer', SimpleImputer(strategy='median')),
        ('scaler', StandardScaler())]) 
    
  4. 接下来,我们必须对分类变量执行类似的步骤,但在这里,我们只需要定义一个one-hot编码器的转换步骤:

    categorical_features = ['job', 'marital', 'education', 'contact',
                            'housing', 'loan', 'default','day']
    categorical_transformer = OneHotEncoder(handle_unknown='ignore') 
    
  5. 我们必须使用ColumnTransformer对象将这些预处理步骤汇集到一个单一的对象中,称为preprocessor,这将把我们的transformers应用到 DataFrame 的适当列上:

    preprocessor = ColumnTransformer(
        transformers=[
            ('num', numeric_transformer, numeric_features),
            ('cat', categorical_transformer, categorical_features)]) 
    
  6. 最后,我们想在前面步骤的末尾添加 ML 模型步骤并最终完成管道。我们将称之为clf_pipeline

    clf_pipeline = Pipeline(steps=[('preprocessor', preprocessor),
                       ('classifier', LogisticRegression())]) 
    
  7. 这是我们的第一个 ML 训练管道。scikit-learn API 的美丽之处在于,clf_pipeline对象现在可以像库中的标准算法一样被调用。所以,这意味着我们可以编写以下内容:

    clf_pipeline.fit(X_train, y_train) 
    

这将依次运行管道中所有步骤的fit方法。

之前的例子相对简单,但如果你需要更复杂的管道,有几种方法可以使这种管道更复杂。其中最简单且最可扩展的是 Scikit-Learn 创建自定义转换器对象的能力,这些对象继承自基类。你可以通过从BaseEstimatorTransformerMixIn类继承并定义自己的转换逻辑来实现这一点。作为一个简单的例子,让我们构建一个转换器,它接受指定的列并添加一个浮点数。这只是一个简单的示意图,向你展示如何实现;我无法想象在大多数情况下,将单个浮点数添加到你的列中会有多大帮助!

from sklearn.base import BaseEstimator, TransformerMixin

class AddToColumsTransformer(BaseEstimator, TransformerMixin):
    def __init__(self, addition = 0.0, columns=None):
        self.addition = addition
        self.columns = columns

    def fit(self, X, y=None):
        return self

    def transform(self, X, y=None):
        transform_columns = list(X.columns)
        if self.columns:
            transform_columns = self.columns
        X[transform_columns] = X[transform_columns] + self.addition
        return X 

你可以将这个转换器添加到你的pipeline中:

pipeline = Pipeline(
    steps=[
        ("add_float", AddToColumnsTransformer(0.5, columns=["col1","col2", 
                                                            "col3"]))
    ]
) 

这个添加数字的例子实际上并不是使用基于类的转换器定义的最佳用例,因为这个操作是无状态的。由于没有训练或对输入值进行复杂操作的需求,需要类保留和更新其状态,所以我们实际上只是封装了一个函数。添加自定义步骤的第二种方式利用了这一点,并使用FunctionTransformer类来封装你提供的任何函数:

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import FunctionTransformer

def add_number(X, columns=None, number=None):
    if columns == None and number == None:
        return X
    X[columns] = X[columns] + number
pipeline = Pipeline(
    steps=[
        (
            "add_float",
            FunctionTransformer(
                add_number, kw_args={"columns": ["col1", "col2", "col3"],
                                     "number": 0.5}),
        )
    ]
) 

通过构建这些示例,你可以开始创建可以执行任何你想要的特征工程任务的复杂管道。

为了结束本节,我们可以清楚地看到,将执行特征工程和训练模型的步骤抽象成一个单一对象的能力非常强大,这意味着你可以在各个地方重用这个对象,并使用它构建更复杂的流程,而无需不断重写实现的细节。抽象是一件好事!

现在我们将转向另一种编写管道的方法,使用 Spark ML。

Spark ML 管道

在本书中,我们一直在使用的一个工具集,在我们讨论扩展我们的解决方案时将特别重要:Apache Spark 及其 ML 生态系统。我们将看到,使用 Spark ML 构建类似的管道需要略微不同的语法,但关键概念与 Scikit-Learn 的情况非常相似。

关于 PySpark 管道有一些重要的事项需要提及。首先,与 Spark 所使用的 Scala 语言中的良好编程实践一致,对象被视为不可变的,因此转换不会在原地发生。相反,会创建新的对象。这意味着任何转换的输出都将在你的原始 DataFrame(或者确实是在新的 DataFrame 中)中需要创建新的列。

其次,Spark ML 估计器(即 ML 算法)都需要将特征组装成一个单列中的类似元组的对象。这与 Scikit-Learn 形成对比,在 Scikit-Learn 中,你可以将所有特征保留在其数据对象的列中。这意味着你需要习惯使用组装器,这些是用于将不同的特征列拉在一起的实用工具,尤其是在你处理必须以不同方式转换才能被算法使用的混合分类和数值特征时。

第三,Spark 有许多使用延迟评估的函数,这意味着它们只有在被特定操作触发时才会执行。这意味着你可以构建整个 ML 管道,而不必转换任何数据。延迟评估的原因是 Spark 中的计算步骤存储在一个有向无环图DAG)中,这样在执行计算步骤之前,执行计划可以被优化,这使得 Spark 非常高效。

最后——这是一个小点——使用骆驼命名法而不是常见的蛇形命名法来编写 PySpark 变量是常见的,后者通常用于 Python 变量(例如,variableNamevariable_name)。这样做是为了使代码与继承自 Spark 底层Scala代码的此约定的 PySpark 函数保持一致。

Spark ML 管道 API 以类似于 Scikit-Learn 管道 API 的方式利用 Transformer 和 Estimator 的概念,但也有一些重要的区别。第一个区别是 Spark ML 中的 Transformer 实现.transform()方法,但不实现.fit_transform()方法。其次,Spark ML 中的 Transformer 和 Estimator 对象是无状态的,因此一旦训练完成,它们就不会改变,并且只包含模型元数据。它们不存储有关原始输入数据的任何信息。一个相似之处在于,在 Spark ML 中,管道也被视为 Estimator。

我们现在将构建一个基本示例,以展示如何使用 Spark ML API 构建训练管道。

让我们看看:

  1. 首先,我们必须使用以下语法对前一个示例中的分类特征进行独热编码:

    from pyspark.ml import Pipeline, PipelineModel
    categoricalColumns = ["job", "marital", "education", "contact",
                          "housing", "loan", "default", "day"]
    
    for categoricalCol in categoricalColumns:
        stringIndexer = StringIndexer(inputCol=categoricalCol,
                                      outputCol=categoricalCol +
                                      "Index").setHandleInvalid("keep")
        encoder = OneHotEncoder(
                  inputCols=[stringIndexer.getOutputCol()],
                  outputCols=[categoricalCol + "classVec"]
                  )
        stages += [stringIndexer, encoder] 
    
  2. 对于数值列,我们必须进行插补:

    numericalColumns = ["age", "balance"]
    numericalColumnsImputed = [x + "_imputed" for x in numericalColumns]
    imputer = Imputer(inputCols=numericalColumns, outputCols=numericalColumnsImputed)
    stages += [imputer] 
    
  3. 然后,我们必须执行标准化。在这里,我们需要在应用StandardScaler时稍微聪明一点,因为它一次只能应用于一列。因此,在将我们的数值特征填充到单个特征向量中之后,我们需要为每个数值特征创建一个缩放器:

    from pyspark.ml.feature import StandardScaler
    
    numericalAssembler = VectorAssembler(
        inputCols=numericalColumnsImputed, 
        outputCol='numerical_cols_imputed')
    stages += [numericalAssembler]
    scaler = StandardScaler(inputCol='numerical_cols_imputed',
                            outputCol="numerical_cols_imputed_scaled")
    stages += [scaler] 
    
  4. 然后,我们必须将数值和分类转换的特征组合成一个特征列:

    assemblerInputs = [c + "classVec" for c in categoricalColumns] +\ ["numerical_cols_imputed_scaled"]
    assembler = VectorAssembler(inputCols=assemblerInputs,
                                outputCol="features")
    stages += [assembler] 
    
  5. 最后,我们可以定义我们的模型步骤,将其添加到pipeline中,然后进行训练和转换:

    lr = LogisticRegression(labelCol="label", featuresCol="features",
                            maxIter=10)
    stages += [lr]
    (trainingData, testData) = data.randomSplit([0.7, 0.3], seed=100)
    clfPipeline = Pipeline().setStages(stages).fit(trainingData)
    clfPipeline.transform(testData) 
    

你可以将模型管道持久化,就像持久化任何Spark对象一样,例如,通过使用:

clfPipeline.save(path) 

其中path是你目标位置的路劲。然后,你可以通过使用以下方式将这个管道读入内存:

from pyspark.ml import Pipeline
clfPipeline = Pipeline().load(path) 

以下是我们在 PySpark 中使用Spark ML构建训练管道的方法。这个示例展示了足够的内容,让你开始使用 API 并构建你自己的、更复杂的管道。

现在,我们将以本章所涵盖内容的简要总结来结束本章。

摘要

在本章中,我们学习了如何构建我们想要在生产中运行的 ML 模型的训练和部署解决方案的重要主题。我们将这个解决方案的组件分解为处理模型训练、模型持久化、模型服务和触发模型重新训练的各个部分。我将之称为“模型工厂”。

我们深入探讨了某些重要概念的技术细节,深入研究了训练 ML 模型真正意味着什么,我们将之定义为学习 ML 模型是如何学习的。然后,我们花了一些时间讨论特征工程的关键概念,即在这个过程中你如何将数据转换成 ML 模型可以理解的形式。随后是关于如何考虑你的训练系统可以运行的不同模式的章节,我将之称为“训练-持久”和“训练-运行”。

我们随后讨论了如何使用各种技术在你的模型及其消耗的数据上执行漂移检测。这包括了一些使用 Alibi Detect 和 Evidently 包执行漂移检测的示例,以及如何计算特征重要性的讨论。

然后,我们介绍了训练过程可以在不同抽象级别自动化的概念,并在解释如何使用 MLflow 模型注册表程序化地管理你的模型阶段之后,最后部分涵盖了如何在 Scikit-Learn 和 Spark ML 包中定义训练管道。

在下一章中,我们将找出如何以 Pythonic 的方式打包一些这些概念,以便它们可以在其他项目中无缝部署和重用。

加入我们的社区 Discord

加入我们的社区 Discord 空间,与作者和其他读者进行讨论:

packt.link/mle

二维码