机器学习系统设计-全-

机器学习系统设计(全)

原文:annas-archive.org/md5/448cbe9f1b564bbe7b4a7ae5906c9404

译者:飞龙

协议:CC BY-NC-SA 4.0

第一部分 准备工作

这一部分完全致力于机器学习系统设计的早期阶段。第一章介绍了我们对机器学习系统(ML)设计的愿景,并强调了其原则可以有用的领域。第二章详细描述了问题空间和解决方案空间,以及前者相对于后者的绝对优先级,并提供了解决问题的见解,包括风险、局限性和错误成本。第三章揭示了构建或购买的两难困境,将问题视为灵感的来源,并提出了我们对问题分解的观点。第四章致力于设计文档,作为任何机器学习系统的基本构建块。

第一章:1 机器学习系统设计的基本要素

本章涵盖

  • 机器学习(ML)系统设计是什么,为什么它如此难以定义,你可能在何处首次遇到它

  • 我们认为谁将最从阅读这本书中受益,我们将提供哪些信息,以及它的结构将如何安排

  • 机器学习系统设计有哪些原则是有帮助的,以及何时应用它们最合适

机器学习(ML)系统设计是一个相对较新的术语,常常让行业人士感到困惑。许多人发现很难确定这个术语背后的特定职责范围,更不用说尝试为相应的角色或职位找到一个合适的名字了。根据其职责范围的不同,这项工作可能由机器学习工程师、软件工程师,甚至数据科学家以不同的效率完成。

虽然所有这些观点都是有效的,但我们认为,要成为一名经验丰富的机器学习系统设计专家,你必须将每个背景领域的专业知识封装起来。请注意,虽然本书中我们讨论的一些内容是针对机器学习系统的,但其他内容对于那些已经构建过非机器学习软件系统的读者来说可能很熟悉(你可以在第二章、第十三章和第十六章中找到这些信息)。这是因为机器学习系统设计,尽管是一种新的现象,但仍然基于软件开发的经典基础。

但首先,我们需要发现机器学习系统设计作为一个整体究竟是什么。在本章的开头,我们将提出我们对机器学习系统设计定义的看法,并用我们自己和同事的个人经验中的例子来支持它;我们将描述这个职位的理想人格,并分享我们个人经验中的案例,说明为什么采用连贯一致的方法来设计机器学习系统将节省你大量的时间,并有助于实现短期业务胜利,这对于在早期阶段赢得同事对这种新工作方法的信任至关重要。

1.1 机器学习系统设计:你是谁?

如果你曾经尝试过在深度技术/大科技公司(第一个术语通常指初创公司或大公司内部从事或开发尖端技术的研发部门,第二个术语指世界上最大、最具主导地位的科技公司,它们通常以人才招聘的高标准和先进的工程文化而闻名)的机器学习工程师/经理职位面试,那么你可能会对机器学习系统设计感到熟悉。我们两人都有丰富的深度技术经验,所以在计划写这本书的时候,我们确信定义对每个人来说都很清晰,没有必要过多地纠结于此。

然而,在向各种人征求他们对大纲的意见后,我们发现这个术语本身在意见和解释上引起了分歧。也许这是因为该行业长期以来一直有一个明确的职位列表,这为求职者提供了一个相对清晰的理解,即他们申请的是哪些功能和责任。软件工程师、研究工程师、机器学习工程师等职位都包含了一定经典的功能集合,这些功能在教科书中被神圣化,并在职位描述中被巧妙地陈述。

因此,是否存在与“机器学习系统设计”直接相关的职位?目前,没有完全符合我们将在本书中描述的范围的职位,但如果遇到符合这一范围的人,他们的职位肯定会是数据科学家。

在我们试图理解这种联系的本质时,我们接触了在数据科学家职位上工作的人,最终意识到这个角色意味着一个非常广泛且模糊的责任列表。确实,你可以在 10 家不同的公司找到 10 个不同的数据科学家,并询问他们做什么——你最终会听到 10 件完全不同的事情:

  • 在 Excel 中创建数据透视表。

  • 设置一个 10 PB 的分布式集群。

  • 构建实时计算机视觉系统。

  • 部署众多聊天机器人。

  • 在 Tableau/Metabase/Looker/PowerBI 中可视化数据。

  • 编写 SQL 脚本。

  • 进行 A/B 测试。

  • 创建推荐系统。

  • 处理与利益相关者的沟通。

  • 回答来自高层管理的问题。

如您所见,一个简短、听起来清晰明了的标题包含了一系列相当混乱的功能,它已经成长为一个“万能手”的术语,用于任何超出数据工程师、机器学习工程师和研究工程师通常工作范围的事情。

在思考这个问题时,我们发现,在机器学习系统设计(或者更确切地说,后来得到这个名字的情况)中,情况正好相反:存在一种没有共同名称但具有明确的功能和责任的现象,需要做的是组织它们,并将它们纳入一个相互关联的功能结构中。

在接下来的章节中,我们将提供我们自己对机器学习系统设计的看法,甚至提出非传统想法和解决方案,但在深入探讨之前,我们想提出我们自己的定义:

机器学习系统设计是一个复杂的多步骤过程,涉及设计、实施和维护基于机器学习的系统,这需要结合来自各个领域和角色的技术和技能,包括机器学习、软件工程、项目管理、产品管理和领导力。

图 1.1 展示了此定义。

figure

图 1.1 成功进行机器学习系统设计所需具备的各种技能

我们之所以用斜体强调“维护”,是因为我们认为机器学习系统设计并不随着机器学习系统的发布而结束。除了提供准确的预测和确保高效的决策外,你的系统必须足够可扩展和灵活,以便能够轻松适应不断变化的企业环境或任何其他内部和外部因素。因此,在你上线后,维护和微调你的机器学习系统将确保其在长期内的效率,这在严格预算或容量限制下尤其重要。

但质疑的不仅仅是书简介或浏览目录的人对“机器学习系统设计”这个术语本身。我们收到了大量关于本书各个方面的提问;以下是我们认为最引人注目的几个问题:

  • “数据科学家、机器学习工程师和软件工程师是不同的角色;为什么你要将它们融合在一起?”

  • “我对一本关于机器学习系统的书涵盖数据收集和报告等内容感到有些困惑,因为这正是将传统机器学习与数据科学区分开来的地方。”

  • “我很惊讶在概述中没有提到 MLOps,这是描述你描述的许多组件(可重复性、测试、管道等)的通用行业术语。”

对于我们来说,这些问题成为了公众对机器学习与数据科学之间,以及机器学习工程师和数据科学家之间混淆的额外指标。我们对此有自己的看法,但首先,让我们尝试澄清我们的观点。

来自深度技术公司,我们习惯了称呼那些做机器学习的人为“机器学习工程师”,但机器学习工程师和软件工程师之间的区别正在缩小,尤其是在一些显赫人物将机器学习称为“软件 2.0”(mng.bz/yoNe)之后。同时,“数据科学家”这个职位通常与那些从事产品分析和与指标、见解等工作的人相关联。(请注意,我们在这里谈论的是我们在深度技术公司的经验,但由于这些公司雇佣了数千名高素质的专业人士,并逐渐为整个行业设定标准,我们倾向于将这种方法作为基准。)

当人们面试机器学习工程师职位时,他们通常会走过软件工程师招聘流程,并在此基础上增加额外的部分,其中机器学习系统设计是最重要的之一。这被用来提取关于候选人专业知识、成熟度和概述复杂系统以及将其分解为相互依赖的任务块的能力的信号。这并不是一件容易的事情,因为候选人只有 40-45 分钟的时间来展示由面试官随机挑选的系统设计。

现代人工智能作家和哲学家埃利泽·尤德科夫斯基写道:“学校里教授的最危险的思想习惯是,即使你真的不理解某件事,也应该鹦鹉学舌地把它复述出来” (mng.bz/r1Zy)。这在一些公司的技术面试流程中非常适用:面试官提供一个谜题,并期望应聘者鹦鹉学舌地给出特定答案。应聘者被录用并成为自己公司的面试官后,这种不良习惯得到强化,公司继续招聘那些从各个领域零散记忆的知识片段的人。这些人是否真正理解全局没有保证,这正是我们在进行面试时遇到的情况。

我们为多家公司面试和招聘机器学习工程师。有些人处于职业生涯的初期,有些人是有经验的专家,还有一些人是从软件工程师转向机器学习的。然而,那些未能通过面试的人中有一个共同点:在机器学习系统设计部分工作时,他们过于关注细节,从未触及更大的图景。

对我们来说,这些失败表明了期望不匹配;作为年轻的招聘经理,我们坚信,一个了解所有算法、工具和模式的人会自然而然地适合这个角色。但后来我们发现,有时人们无法将他们的知识碎片整合成一个整体愿景。

此外,在现实环境中构建系统与在面试中讨论它们截然不同。一个人可以学习数十个流行的机器学习系统设计问题(“你将如何为 LinkedIn 类似的网站设计一个职位推荐系统?”),当类似的问题在实际工作中出现时,可能会感到困惑。

但让我们暂时忽略面试的部分。雇佣机器学习专家是有原因的:公司需要他们来构建、维护、运营和改进系统——而不仅仅是编写一些代码或关闭 Jira 工单。企业需要可靠的机器学习系统来实现目标和解决问题。

构建机器学习系统需要广泛的技术技能。简单来说,负责人必须能够回答以下三个问题:

  1. 我们在构建什么?

  2. 系统的目的是什么?

  3. 它应该如何构建?

实际上,这需要多个角色的技能组合:一点产品经理的技能来理解主要目标并将其传达给同事和利益相关者,相当一部分机器学习研究者的技能来赋能系统,当然,还需要坚实的软件工程背景来使产品可用、可维护和可靠。机器学习系统设计专家应该能够全局思考,并在必要时深入到足够的地方。

很少有人能够以适当水平结合所有这些技能。然而,如今正在构建大量机器学习系统,而有人必须设计它们。根据我们的经验,机器学习系统通常由一位聪明的机器学习专家(因为它是机器学习)或一位经验丰富的软件工程师(因为它是系统)设计。他们完成了这项工作,但往往在他们的强项之外领域感到挣扎。

总结来说,围绕机器学习系统设计的困惑对于一方面缺乏专业知识的人才和另一方面寻找全能手型招聘经理或招聘人员来说更为典型。然而,如果我们从高级管理人员或专家的角度来看,就会出现一个更广阔的图景。他们知道你聘请这些专家来构建、维护和改进机器学习系统,他们在机器学习系统上的最终表现成为他们职业成长的终极标准。

我们认为,数据科学家和软件工程师的结合,以及他们在学术机器学习方面的经验,构成了机器学习系统设计方面的专家。最终设计机器学习系统的人可能来自各种背景——软件、实践机器学习、学术机器学习、数据研究——我们希望我们的小块理论辅助的实践经验能帮助他们填补差距,系统化他们在熟悉领域的知识,并在缺乏宝贵经验的地方更加自信。

1.1.1 为什么机器学习系统设计如此重要

当你拥有 MLOps 作为一套用于构建和维护你的机器学习系统的工具集时,你可以将机器学习系统设计视为一个蓝图,你可以在任何时刻依赖和参考它,这将为你提供可扩展性和灵活性(对构建块及其连接的正确理解有助于识别瓶颈和流畅地解决其他问题)。然而,最重要的是,它提供了一个框架,将你的整个系统焊接在一起。

有些项目足够简单,以至于不需要那么彻底的方法。以建筑为例。你可能不需要初始蓝图就能建造一个棚屋。但当你雄心勃勃地扩展到房屋或摩天大楼的水平时,你不能不使用预先安排的详细计划。机器学习系统设计是一种工程方法,它结合了数百位在数十家公司和众多项目上工作的专家的经验,用于构建机器学习系统。

1.1.2 机器学习系统设计的根源

构建复杂的软件系统一直是一个挑战,组织必须以某种方式使过程结晶化。人们使用一个通用原则来通过抽象管理复杂性:构建低级块,将复杂性封装在其中,将它们视为魔法黑盒,使用它们来构建更高级的块,依此类推。

这个流程是有效的,但它有一个弱点:有人必须决定所有这些块的结构(最高级别的组件是什么,它们内部的结构是什么,等等,直到最低层的实现)。最负责任的决策是由软件架构师做出的——经验丰富的工程师,他们与许多系统一起工作。

这种方法通常与瀑布方法和“大设计前期”范式相关联。换句话说,它假设软件项目开始缓慢,在真正系统代码的第一行被编写之前,进行深入的分析和文档记录。这种方法曾经并且继续是可靠的,但却是惰性和官僚的。在一个快速变化的世界里,项目在完成之前可能会失去其最初的意义。

这种缓慢但稳定方法的反对者通常是敏捷软件开发范式的热衷者。《敏捷软件开发宣言》([agilemanifesto.org/](https://agilemanifesto.org/))的作者提出了四个主要价值观:

  • 个人和互动胜过流程和工具

  • 工作软件胜过全面文档

  • 客户协作胜过合同谈判

  • 对变化的响应胜过遵循计划

换句话说,这些人公正地指出,许多软件系统在试图规划和记录一切时无法有效运作。当然,有时这种官僚主义是有道理的——例如,在构建控制医疗设备或飞机的软件时。但大多数软件工程师从事其他类型的应用——办公软件、娱乐、网站和移动应用。这就是软件架构师的角色与缓慢且过时的东西相关联的原因——与迅速改变世界的黑客相反,他们不需要整个架构师、经理和其他专家的软件规范批准。这种敏捷方法被硅谷黑客文化和数千家成功的初创公司普及。甚至像 Meta 这样的大公司也试图保持这种文化——他们的内部座右铭是“快速行动,打破事物。”

让我们总结一下这个小历史概述:在某个时刻,工业界面临了一系列的软件工程流程,从由软件架构师领导的严格监管的流程,到混乱的无政府主义“无视等级”黑客风格的构建方式。而且,正如常发生的那样,事情变得复杂。更多传统公司倾向于变得更加敏捷,而大多数无政府主义初创公司成熟起来,引入了流程和独立的角色。

这种混合导致了一种共识,这种共识现在主导着科技公司:我们不会将所有决策委托给专门的软件架构师等人,我们将保持常规软件工程师的责任;让他们既设计系统为这些系统编写代码。但这种自由程度并没有消除最初对决策的需求:仍然有人必须对事物的结构做出最终决定。有人必须对系统设计负责。每个工程师都可能在这里或那里参与其中,但看到整体图景是至关重要的。

实现设计系统低级组件的技能与设计适当系统的技能不同。这就是为什么深度技术公司往往有单独的面试部分来检查候选人在编写有效代码(即算法部分)和设计系统方面的技能:预计工程师将同时扮演这两个角色。这两个角色之间的划分可能不同:通常,初级工程师是设计文档的沉默读者,而高级工程师是作者或积极贡献者。

简而言之,有一个共识:一个优秀的软件工程师应该能够在不同的抽象级别上操作,从低级实现到高级架构决策。

我们到目前为止所说的关于系统设计定义的一切都适用于任何软件——我们没有提到与机器学习相关的内容。然而,并非每个人都能成功设计软件系统,就能成功设计机器学习系统——这是一个非常特定的系统子集。在设计机器学习系统时,负责人应该牢记许多与常规软件无关的方面。在这本书中,我们将关注这些方面;对更普遍的系统设计问题感兴趣的读者可以查阅其他文献。

1.2 本书结构

覆盖系统设计的书籍有几本,但关于机器学习系统设计的文献却很少。我们决定为这个领域做出贡献,弥合供需之间的差距。我们的目标是分享我们的知识和经验,帮助你将你所知道的各种事物转化为一个完整的系统。

这本书的结构是一个全面的实用指南,介绍了如何在各个领域构建复杂、功能正常的机器学习系统,无论你所在公司的规模大小。本指南包括

  • 整体格局,概述一般结构原则和构成这些系统的所有组件,以及你可能陷入的陷阱

  • 在每个步骤中可能派上用场的工具的低级清单,以及简要说明为什么它们很重要

书的结构类似于清单或手册,融入了我们自己经验中的篝火故事。它可以一次性阅读,也可以在工作过程中针对机器学习系统的特定方面随时使用。

每一章都是每个机器学习系统必须遵守的高级清单。请注意,虽然并非所有项目都必须完成,但每个项目都必须记住并考虑。

此外,每一章都回答了为什么以及何时给定项目重要的问题。它还包括对该领域(适合该项目的技术和工具)的描述。描述是系统化的(不仅仅是 100 个流行词汇的列表),尽管不一定详尽,因为我们相信有经验的读者能够将示例案例与他们的背景进行比较,并得出自己的结论。同时,我们尽量避免陷入典型的教科书或经典机器学习或深度学习课程。

我们来自截然不同的(因此,非常互补的)背景:我们两人都参与了超过 20 年的机器学习项目,涉及各种角色、公司和环境——从种子前初创公司到价值数十亿美元的国际公司。有时我们作为个人贡献者工作很长时间。其他时候,我们的工作主要意味着快速团队增长和指导有才华和有抱负的年轻工程师。我们见证了成功和失败、大规模收购和大量裁员。当然,我们也与朋友们讨论了许多机器学习项目的成功和失败。

但无论我们的背景有多么不同,有一点我们强烈认同:机器学习项目几乎从未因为参与者无法正确使用算法而失败。失败可能有多个原因:一个误导性的或完全不必要的任务、数据处理不当、一个无法扩展且没有增长潜力的解决方案——这个列表可以一直列下去。

存在一个如此流行的模式,以至于我们不得不在不同的部分重复一些故事:一个在狭窄领域的深度专家过于关注他们的专业领域——可能选择一些类似领域,但仍然没有看到大局。结果,一些重要的细微差别被忽略,导致项目失败、错过截止日期和违反预算。

尽管机器学习方面的书籍通常提供“正确”的答案,但我们的主要目标正好相反。我们想要教给你的,是如何提出正确的问题。这些问题可能是你问自己的、问你的队友、用户、利益相关者——无论是什么。作为科技行业的专业人士,我们每个人都积累了大量的宝贵信息,但并不总是能够将这些信息串联起来。这就是及时提问有助于构建我们周围所有知识结构的地方。

我们将本书分为四个主要部分,以便其结构与任何系统的生命周期保持一致——研究、创建、改进和维护。

前两部分基于机器学习系统设计的早期阶段。在整个第一部分中,我们将关注你对系统需要解决的问题的整体意识和理解,并定义在系统开发开始之前所需的步骤。这个阶段很少涉及编写代码,主要关注小型原型或概念验证。第二部分深入探讨了早期工作的技术细节。这个阶段需要大量的阅读和沟通,这对于理解问题、定义可能的解决方案的范围以及与其他项目参与者协调期望至关重要。如果我们把一个机器学习系统比作人体,那么它就是形成骨骼的过程。

第三部分专注于中间步骤。在系统生命周期的这个阶段,负责工程师的时间表通常会被颠倒。在这个阶段,研究和沟通较少,而实际操作、实施和改进系统的工作更多。在这里,我们关注的问题是如何使系统在多个维度上变得强大:坚实、准确和可靠。继续使用人体比喻,系统正在增长肌肉。

最后的部分全部关于整合和增长。对于一个没有经验的观察者来说,系统可能看起来已经准备就绪,但这种印象是复杂的。在系统成功上线之前,需要考虑多个(主要是工程方面)的方面。在软件领域,系统故障很少像土木工程那样是一场灾难,但它仍然是一个不受欢迎的场景。因此,在这个阶段,你将学习如何使你的系统可靠、可维护和面向未来。如果你不厌倦使用人体比喻,那么这就是系统获得思维和智慧的地方,因为不受控制的强大只会带来麻烦。

总体而言,开篇章节将包含更多一般性信息,尽管如此,这些信息对于界定问题和构建一个高效运行的机器学习系统的基础核心至关重要。然而,随着你深入阅读本书,内容将变得更加复杂和深入,为你提供实际案例和练习。从下一章开始,我们将介绍两个截然不同的虚构案例,我们将贯穿整本书来探讨它们。这两个案例都需要机器学习系统来解决问题,并且随着你继续探索,这两个案例也将不断演变。

在本书的每一部分,我们总是更倾向于直观性而不是全面性。构建机器学习系统有许多方面,每个方面都值得一本自己的书。然而,我们并不打算写一本关于数据收集和准备的书,另一本关于特征工程的书,还有一本关于指标的书。相反,我们描述冰山一角,并回顾景观结构,同时用指向值得注意论文的链接来支持我们的思想和观点,这样读者既可以熟悉高级示例,也可以将他们自己的特定知识添加到提供的框架中。我们也不旨在解释与特定库或引擎相关的细节。我们将在某些章节中提到值得注意的例子,但它们只是为了说明更高层次的抽象。

实际系统总是比我们在博客文章、会议演讲和当然还有访谈中看到的例子要复杂得多。对于所有这些场景,人们都在谈论高级抽象,但在现实中,魔鬼藏在细节中。这就是为什么我们认为对问题解决有一些直观理解如此重要的原因:一个成功的机器学习系统设计师不仅应该能够从烹饪手册中识别出一些食谱并重现它,而且应该能够适应公司特定的细节,这些细节有时可能会颠覆一切。

我们希望这本书对以下人群有用:

  • 准备面试机器学习工程师/经理职位的求职者

  • 与现有复杂系统一起工作的软件工程师、工程经理和机器学习实践者,他们想要理解或改进它

  • 计划设计自己的机器学习系统或已经设计了一个系统并想要确保他们没有忘记任何关键内容的人

由于这里描述的哲学,这本书并不适合初学者。我们期望我们的读者熟悉机器学习基础知识(例如,你可以理解一本本科生机器学习教科书),并且精通应用编程(例如,你在学习沙盒之外面临过一些真实的编程挑战)。否则,这本书最好在学习基本材料之后再阅读。

1.3 机器学习系统设计原则何时有用

正如我们之前所说的,应用这些原则对于构建一个具有多种故障模式的复杂系统至关重要。忽视它们会导致有很大可能性交付一个有缺陷的系统——一个现在可能工作得很好,但不足以在现实世界的动态环境中生存下来的系统。挑战可能是纯粹的技术性的(如果我们面临 10 倍多的数据怎么办?),与产品相关的(我们如何适应变化后的用户场景?),由商业驱动的(如果系统在收购后要集成到第三方软件堆栈中怎么办?),法律方面的(如果政府提出关于个人数据管理的新规定怎么办?),或者任何其他方面。近年来只证明我们无法预见每一个可能的风险。

改进系统甚至更重要。正如我们将在接下来的章节中更详细地描述的那样,从头开始构建系统是一个相对罕见的事件。行业外的人可能认为软件工程师大部分时间都在写代码,而实际上,正如我们所知,更多的时间是花在阅读代码上。对于系统也是如此:通常,更多的努力是花在改进和维护现有系统上(这需要深入了解系统内部),而不是从头开始构建。

改进和维护之间的区别有些模糊。为了清晰起见,我们在这里将改进定义为添加新功能或显著改变现有功能,而维护则是指在不断变化的环境中保持现有功能运行(新客户、新数据集、基础设施演变等)。

书中包含的一些原则主要关注机器学习系统改进。它们有助于识别系统的薄弱环节和增长点,有时甚至能发现新的应用。

最后,一些原则更倾向于面向机器学习系统维护。一个令人悲伤的事实是,很多时候系统是由那些没有参与构建它们的团队来维护的。因此,这是一把双刃剑:构建团队应该牢记一些原则,以简化后继者的生活,而维护团队应该理解这些原则,以便能够及时理解整个系统逻辑,并找到适当的解决方案,以使系统能够长期运行。

可以说,近 100%的没有良好设计文档的机器学习项目都失败了,而绝大多数经过充分规划的系统都取得了成功。虽然它不一定是复杂的多页文档,通常几页浓缩的信息就足够了,但在这个案例中,设计文档扮演了两个主要角色。它不仅在一个项目中设定了适当的优先级,还帮助解释你是否真正需要这个项目,并使你的注意力从核心想法(你可能过于专注于项目本身)转移到整体图景。请参阅第四章以获取详细信息。

在为多家企业工作后,我们可以肯定地说,一旦有了描述系统功能所有方面的结构化文档,任何活动,从入职新员工到应用核心变更,都可以实施得更快。你不必寻找那个唯一的知识大师,他把自己所有的知识都保留在自己手中(但仍然不能保证精确性),你可以在图书馆中找到特定的文档。

来自阿列克谢的篝火故事

很久以前,我为一家打车公司工作。它的一个雄心勃勃的项目是建立一个用于打车费用估算的系统。常规定价模型与老式出租车用于向乘客收费的模型完全一样:费用 = X * 时间 + Y * 距离。公司在实际乘车发生之前需要估算费用,以便通知司机和乘客。

项目一开始看起来就很清晰直接。我们所需做的只是拟合一个简单的模型,该模型使用来自地图服务的地理特征,并将其封装为微服务。这看起来如此简单,以至于我甚至没有考虑过写一个设计文档。

侧边栏图片

系统最初在阿列克谢想象中的样子:一个简单的逐步算法

实际上,存在多个陷阱(我们将在以下章节中分别介绍大部分):

  • 地理特征不足以进行精确估计,更复杂的特征需要先进的基础设施(即特征存储库,尽管在当年,这并不是一个流行的词汇或模式)。这将在第十一章中介绍。

  • 随着模型变得更加复杂,其预测的可靠性降低(一定数量的结果最终会变成异常值——要么太大,要么太小)。

  • 错误并非均匀分布,因此模型存在偏差。我们将在第九章中讨论这个话题。

  • 高管们有时想用一些促销活动或基于启发式算法的捷径来覆盖费用估算。这个话题将在第十三章中讨论。

  • 过多的时间被花在了构建一个实际上并没有解决确切问题的模型上。我们将在第二章中讨论这个话题。

  • 整个问题容易受到分布漂移的影响,因此需要智能监控。我们将在第十四章中更详细地介绍这个话题。

  • 基础设施没有准备好应对这种情况,导致高峰时段的延迟无法接受。这个话题将在第十五章中介绍。

  • 一些其他团队并不知道系统正在开发中,这导致了 API 不匹配。这个话题将在第十六章中讨论。

侧边栏图片

系统在多次迭代后的样子

最终系统并没有部署——在所有问题都得到解决之前,市场情况发生了显著变化,对初始系统的需求减弱。虽然系统最初的想法很棒(一些竞争对手使用了类似的想法),但我和我的同事们未能以适当的方式实施它:一些关键方面,无论是技术还是产品相关的,都被完全忽略了,直到项目后期才发现,这时改变的成本已经飙升。同时,如果一些方面在早期阶段就被考虑到,解决它们将变得微不足道。如果只有我、我的老板或我的队友读过这样一本书,我们本可以避免这次失败。

然而,对于每几个失败的案例,总有一个成功的案例。接下来的故事可能没有太多戏剧性,看起来可能有些无聊,但为了平衡起见,分享它是值得的。回想起来,瓦列里曾经在一家人工智能巨头 Yandex 工作,当时它收购了一家提供实时推荐服务的初创公司。在这种情况下,当发生这样的合并时,需要时间来微调现有单位和新单位之间的合作,吸纳新员工,同步业务流程等。然而,在这种情况下,他惊讶地发现,一个新业务是如何如此顺利和无缝地融入一个大型企业的。背后的原因是有一个精心构建的设计文档,使得这种过渡成为可能。

总结来说,我们坚信,在正确引导问题的指导下安排设计文档,并设定适当的目标,是机器学习系统成功的关键——或者说是尽早取消项目的理由,这也是一个积极的成果,考虑到你可以通过取消一项不受欢迎的活动来节省多少时间、精力和金钱。我们将至少用三章的篇幅来探讨这个项目阶段,因为这是你必须应对的最关键的部分。

摘要

  • 虽然这是一个相对较新的术语,但机器学习系统设计基于软件开发的传统基础,结合了相关学科现有的知识。在这本书中,我们将尝试将这个知识库重新组织成一套工作算法。

  • 虽然 MLOps 可以被视为构建和维护你的机器学习系统的一套工具,但将机器学习系统设计视为一个将整个系统焊接在一起的框架。

  • 要在机器学习系统设计中取得成功,至关重要的是在机器学习、软件工程、项目管理、产品管理和领导力等学科中拥有同等经验。

  • 在设计一个机器学习系统之前,你应该清楚你正在构建什么,系统的目的是什么,以及它应该如何构建。

  • 一个成功设计的机器学习系统的支柱是一致的途径、周密的路线图以及一系列初步行动,这些行动将组织你的工作并在长期内节省时间。

第二章:2 是否存在问题?

本章涵盖

  • 问题空间和解决方案空间:哪个先?

  • 定义问题作为最重要的步骤

  • 定义风险和限制

  • 错误的成本

要在机器学习(ML)系统设计中取得成功,你实际上需要在多个领域成为专家,包括项目管理、机器学习和深度学习、领导力、产品管理和软件工程。然而,当剥去一切外在的装饰,即使在机器学习系统设计中最为复杂和精密的解决方案,也将与其他任何产品一样拥有相同的框架和基础。

近年来获得的丰富知识和数量给你提供了前所未有的自由,可以选择你想要的精确方法来构建你的机器学习系统,但无论你选择的工具多么精致,它们都不过是实现的中介。

业务目标是什么?预算有多大?截止日期有多灵活?潜在的输出是否能够覆盖并超过总体成本?这些问题是在确定你的机器学习项目范围之前你需要问自己的关键问题。

在开始回答这些问题之前,有一个至关重要的行动将为成功的设计机器学习系统奠定基础,那就是找到并明确你的解决方案将解决的问题(或帮助解决的问题)。这似乎是一个微不足道的问题,尤其是对于熟练的工程师来说,但根据我们在该领域的经验,在初步工作中跳过这一步是极具欺骗性的危险。当我们意识到一些问题由于现有技术的状态或不良设定问题的随机不确定性而无法在适当水平上解决时,这一点就更加明显。在前一种情况下,问题可能成为未来解决方案的候选(例如,今天的文本生成水平对于一个在 2010 年代初的机器学习工程师来说似乎是完全不可实现的),而在后一种情况下,则意味着根本不应该尝试解决这个问题(例如,无法构建一个能够击败赌场轮盘算法的算法)。

在本章中,我们将讨论在开发解决方案之前了解问题的必要性;我们将强调在定义问题时可能面临的风险和限制;并且我们将简要提及错误定义问题可能带来的后果。

2.1 问题空间与解决方案空间

我想,如果你只有一把锤子,那么把所有东西都当作钉子来处理是很诱人的。——亚伯拉罕·马斯洛,美国心理学家

想象一下,一位老板带着一个令人兴奋的新想法来找工程师,这个想法将带来令人震撼的功能(我们都有过这样的经历)。为了说明,让我们使这个例子更加具体。史蒂夫是一家成长中的 SaaS 公司的机器学习工程师。史蒂夫的老板琳达刚刚从与销售副总裁杰克的一次会议回来,讨论他们团队一直在处理的问题——客户线索太多,而经理太少。杰克想知道机器学习团队能否提出一个 AI 解决方案,该方案能够根据公司潜在利润自动将客户线索从最好到最差进行排序。这将帮助销售团队首先挑选出有潜力的现金牛,并依次处理剩余的线索。从纸面上看,这个功能看起来非常出色。这似乎是一个不言而喻的选择!

史蒂夫,一个年轻但细致的专业人士,立即对这个项目提出了许多问题。交付的截止日期是什么时候?围绕现有线索构建机器学习模型的数据集有多大?评估一个线索的最大允许时间是多少?我们期望的准确度是多少?我们关于每个线索有什么信息?系统应该有多快?"有潜力的线索"究竟意味着什么?我们将解决方案集成到哪个销售系统中?经过一番问答,史蒂夫知道了以下信息:

  • 数据集目前相当小(公司是一家初创企业)。

  • 杰克希望这个工具能够与现有的客户关系管理(CRM)系统集成,这样公司就不需要花费金钱购买新软件,也不需要重新培训团队。

  • 幸运的是,处理时间没有硬性限制,这意味着不需要一个可靠的实时 API。

  • 截止日期是通常的“越早越好”。

史蒂夫回到他的办公桌前,开始规划项目。“好吧,这看起来很简单。我们可以将其视为一个排序或分类问题,制定一些特征,训练一个模型,暴露一个 API,集成,并部署——这就足够了。”然而,还有两件事让他感到烦恼:

  • 处理这类分类问题的最佳方法是什么?

  • 他应该如何将他的 Python 代码与杰克团队使用的 CRM 集成?

三小时后,他的浏览器里充满了几个简短的分类技术和 CRM API 的文档标签。他想向同事提出一个精确的项目交付时间估计,但因为他犯了一个关键的错误,这可能会在早期阶段造成很大的损失:在思考和提问时,他专注于解决方案空间,而不是问题空间。

在史蒂夫的理解中,他收到的信息已经足够多,可以提出一个合适的解决方案,但实际上,这只是冰山一角。剩余的上下文只能通过向参与项目的多个人提出许多具体问题来发现。

什么是问题空间和解决方案空间?这两个探索范式涵盖了问题的不同视角。虽然两者都很关键,但前者应该始终先于后者(图 2.1)。

figure

图 2.1 经验丰富的工程师总是首先通过指定问题来处理问题空间。

问题空间通常用“什么?”和“为什么?”的问题来定义,甚至可能是一系列这样的问题。甚至有一种流行的技术叫做“五问法”,它建议将你的“为什么?”问题层层叠加,以挖掘你正在分析的问题的根源。典型的问题通常看起来像这样:

  • 为什么我们需要构建解决方案?

  • 它解决了什么问题?

  • 问题为什么会发生?

  • 我们知道有哪些替代方案?

  • 为什么我们希望在给定的限制(指标、延迟、训练样本数量)下使其工作?

探索之后,你应当理解你该构建什么以及为什么。

“什么?”部分,转而,是关于理解客户和功能属性(图 2.2)——例如,“一个工具,它用分数标注客户潜在客户,显示交易发生的可能性;它应该在销售经理在周一的周会计划工作之前分配分数。”

figure

图 2.2 在开始你的项目之前必须提出的问题以及它们之间的关键区别

在一些公司,提出这些问题是产品经理独自完成的工作。然而,工程师排除自己参与问题空间分析并不太有效率,因为对问题的适当理解会极大地影响最终结果。

解决方案空间在某种程度上是相反的。它更少关注问题和客户需求,更多关注实施。在这里,我们讨论框架和接口,讨论事物在底层是如何工作的,并考虑技术风险。然而,在我们对问题有一个一致的理解之前,我们永远不应该开始实施。

在考虑技术实现之前,先达到一个稳固的理解,这让你可以考虑各种解决方案,其中一些可能会显著减少项目范围。也许有一个第三方 CRM 插件是为了解决这个问题而设计的。也许这个问题的机器学习部分出错的成本并不真的那么重要,尽管 Jack 的第一个答案是(利益相关者经常从他们需要接近 100%的准确性的声明开始!)也许数据显示,95%的空潜在客户可以通过简单的基于规则的启发式方法过滤掉。所有这些假设都在故事之外,但如果得到证实,它们中的每一个都是整体背景的重要组成部分。揭露这个背景将为你提供对问题的洞察。

我们之所以以史蒂夫的故事开始本章,有两个原因。首先,这是常见的,并且很可能会以某种方式引起你的共鸣。其次,它适用于任何场景,无论是构建新的系统、修改现有解决方案,还是在科技公司面试中。

第三,也是最重要的,由此类方法产生的规模和影响程度可能对不同的程度造成损害:

  • 史蒂夫将不得不重写终端系统的大部分内容。

  • 林达最终将使用部分解决方案,并补偿缺失的部分。

  • 解决方案可能完全被放弃。

所有这些案例都需要首先理解问题。

2.2 寻找问题

在这里广泛使用的意义上,设计系统的组织受到限制,只能产生这些组织的通信结构的副本。——梅尔文·E·康威

一些老派的企事业公司仍然保持着鼓励普通工程师专注于低级实现(仅编码)的文化,并将设计(包括问题理解和分解)留给架构师和系统分析师。根据我们的经验,由于日益增长的灵活性要求,这种文化正在迅速消失,取而代之的是更多横向结构,将更多的问题理解委托给个人贡献者。

这意味着工程师不需要是该领域的扎实专家(对于没有适当背景的人来说可能过于复杂)。原因很简单:在会议、代码审查和培训最新的神经网络之间,很难学习到构建证券交易所或制造质量控制细微之处的精髓。但在开始设计机器学习系统之前,拥有广泛的理解是必须的。

我们鼓励你使用倒金字塔方案写下问题陈述,在底层有高级理解,在顶部有细微之处。这是一种常见的有效自上而下的方法,将帮助你收集尽可能多的通用信息,确定对你项目最有价值的数据,然后,通过逐点引导问题,深入探讨问题的具体细节(图 2.3)。

figure

图 2.3 倒金字塔方案是我们推荐用于收集成功项目启动所需数据的方案。

在最高层次上,你可以形成对问题的直升机视角理解。这是任何组织 C 级办公室都能理解的水平,他们不太关心机器学习算法或软件架构——例如:

  • 我们的移动应用中有骗子试图攻击我们的合法用户。

  • 我们的价格模型在有些产品上展示了极低的利润率,而在其他类别上则完全不具备竞争力。

  • 客户抱怨我们的软件在带来价值之前需要大量手动调整;它应该是自动的

  • 应用程序用户参与度不足。

在开头有这样的声明,为接下来的探索步骤提供了许多机会。只需尝试质疑给定句子中的每一个词,以确保你能够向一个 10 岁的孩子解释它。谁是欺诈者?他们是如何攻击的?哪份报告提供了关于过度价格的初步洞察?什么最让我们的客户烦恼?在哪里浪费的时间最多?我们如何衡量用户参与度?推荐与这个指标有何关联?问自己或你的同事问题,直到你准备好构建金字塔的下一个更广泛、扩展初始块的块。

这个下一个的金字塔块需要更具体、经过深思熟虑的问题。一种成功的技巧是寻找上一级答案的起源。我们如何决定这种行为是欺诈的?我们的客户需要执行什么样的手动调整?用户参与度和推荐引擎性能目前是如何关联的?

一种更强大的技巧是寻找答案中的不一致性;人们倾向于根据相似性分组对象,根据差异区分对象。可能存在相似的用户;一些被认为是垃圾邮件发送者,应该被禁止,而其他人仍然是合法的,即使他们的行为可能与问题域外的人重叠。对于一个无知的观察者来说,对类似商品相同的增加边际可能是可以接受的,也可能不是,但标准是什么?这里的工程师不需要在问题陈述中找到所有的分割标准(它们不是决策树),但这是一个捕捉关键信号和产生洞察的好领域。这可以用以下声明来总结:试图理解人们想要什么很重要;试图理解他们需要什么是至关重要的。

一定要让所有感兴趣的各方参与这个过程。不仅你的老板或产品经理关心这个项目;你很可能有多个利益相关者(了解哪个利益相关者负责预算并将成为系统某个组件的审批点是至关重要的)。通常,建议与不同级别的专家聊天,以涵盖战略和战术视角。高级管理人员对一个特定倡议的目标了解很多。另一方面,目前处理设计系统缺失的个人贡献者知道可能严重影响设计的技巧和细节。

一旦你足够自信能够用简单的话解释问题,那就到了总结的时候了。我们建议写下你对问题的理解。通常,它是由几段文字组成的,但这段文字最终将成为你设计文档的基石。现在不要过分打磨它;这只是你的第一步(虽然非常重要)。

这一步的重要性可能因组织或环境而异。有时问题容易理解,但通常很难解决——这是成熟竞争市场的常见情况。光谱的另一端是颠覆现有市场的初创公司;在这里,对颠覆的初始理解很少是正确的。其中一位作者曾在一家公司工作,他在项目上的 50%的时间都花在定义目标和相关背景上。背景清晰后,项目的机器学习工程部分就顺利且直接了。

Valerii 的营火故事

回到 2019 年,我在一家科技公司工作,该公司决定扩大其电子商务部门并“线下拓展”。名为 Super Bill 的新项目基于以下想法:

  1. 客户可以使用 Super Bill 应用作为杂货店的比价工具,并且还可以上传他们的收据以获得现金返还,以及使用应用在店内购买。

  2. 品牌可以使用该应用为购买特定商品提供现金返还,推广他们的商品作为替代品或推荐品。

问题在于单个商品可能有多个名称(如收据上所示)在不同的杂货连锁店中。例如,“Mars 小”,“M. bar 小”,“Mars bar 小”等,这些都可能是同一库存单位(SKU)的不同拼写,需要映射到同一商品:Mars 巧克力棒。

初始的想法是训练一个深度结构化语义模型并执行搜索引擎匹配,其中收据上的名称将扮演查询的角色,SKU 则是文档。

我不喜欢那个解决方案,因为它对这个问题的复杂性和体积太大。它需要收集数据,标注它,训练模型,评估其质量等。所以我决定重新思考。如果需要标注数据,我只为每个收据上的每个名称在每家杂货店标注一次。因此,我不需要为已经标注的样本创建机器学习模型,这种情况可能相当常见。鉴于项目遵循 Zipf 定律,我们需要标注最流行的名称-SKU 对,这构成了所有唯一对的一小部分,但标注所有对的大一部分(根据这一经验法则,当一组测量值按降序排列时,第n个条目的值大约与n成反比)。

剩余的部分可以由有权访问整个 SKU 数据库的员工进行标注,但我们可能不想与一个众包标注平台共享这个数据库。最多,我们可以从这个数据库中提供一份候选名单,并检查其中哪一个(如果有的话)是匹配的。

那么,我们在这里能做什么呢?我们可以尝试预测/提取品牌并预测收据中的样本类别。缩小候选者名单是一个相对简单的分类任务或基于距离的任务,因为我们有有限的类别和品牌,并且可以使用非常简单的后处理技术,例如 Levenshtein 距离。一旦我们有候选者,我们可以将它们与收据样本一起发送,通过我们的众包流程进行标记。我们需要多久做一次?答案是每个 SKU-连锁店对只做一次。这使得它比最初的想法更容易、更快。毕竟,我们不是在为每天数十亿个查询构建搜索引擎,而是一个有限的匹配系统。

我们能在不到三周的时间内将这个解决方案投入生产,这令人惊讶地迅速。我们只需要理解我们想要解决什么问题,并收集上下文信息。

P.S. 后来,当我们为另一家拥有数千万 SKU 的深科技公司准备类似的系统时,我们将后处理层的最后一层替换为一个深度语义相似性模型,以产生一个更智能的系统。这是设计易于调整的解耦系统的益处。

一旦问题陈述足够明确,我们就需要考虑作为机器学习工程师,我们能用它做什么。

2.2.1 我们如何通过机器学习系统近似解决方案

缺乏经验或过于急躁的工程师通常会首先尝试将问题直接拖入一个普鲁克斯特床——即众所周知的机器学习算法家族,如监督学习或无监督学习,或者分类或回归问题。我们认为这不是一个最好的开始方式。

对于外部观察者来说,一个机器学习模型就像一个魔法先知:一台能够回答任何正确表述问题的通用机器。作为机器学习工程师,你的工作就是近似其行为——使用机器学习算法构建这个先知——但在模仿它之前,我们需要找到正确的问题,并教会用户如何提出这个问题。用不那么隐喻的话来说,这里我们将一个商业问题转化为一个软件/机器学习问题。

一些问题可能看起来非常直接:

  • 对于欺诈问题,我们希望先知能够尽快将用户标记为欺诈者——在理想的世界里,甚至在用户做任何事情之前。这听起来像是一种分类

  • 对于定价模型,我们希望了解客户愿意为他们的商品支付多少,而不会因为选择竞争对手而放弃服务(如果我们只针对短期问题)或者没有像“这家商店变得过于贪婪;我应该避免他们”这样的想法(如果我们关心品牌的长期前景)。这肯定类似于教科书中的回归示例

  • 对于推荐系统,我们会问我们可以向客户推荐什么,使他们满意于服务。这非常类似于排名问题

即使有神奇占卜者的隐喻,我们也经常不得不留下多个影响这个潜在答案的评论。我们将在书中不时关注类似的细节和评论,但这里的重点是以下内容:对于这个问题可能没有单一的简单答案,你的机器学习系统设计必须提前意识到这一点。

在我们的定价示例中,可能存在一系列目标,从现在立即最大化利润到长期发展公司。一个好的机器学习系统应该能够适应这个光谱中的特定点。在接下来的章节中,我们将讨论实现这一点的技术方面。

许多机器学习从业者,包括著名的安德鲁·吴,一位著名的 AI 专家、斯坦福大学的教授以及 Landing AI 的创始人,建议使用人类专家的启发式方法:让我们构建一个系统,使其以该领域专家相同的方式回答问题。这在许多领域都适用(医疗保健就是一个很好的例子),并设定了早期理解使用人工智能方法解决问题的门槛。不幸的是,这也伴随着一些缺点:有些问题机器的表现比人更好。这类问题通常发生在数据以事件日志(通常是人类行为)的形式表示的领域,而不是经过仔细标记的东西。在广告技术和金融行业中很容易找到这样的案例。因此,达到人类水平的表现可能是一个公平的门槛,但并不总是如此。

只有在问题明确之后,深入挖掘算法近似的方法并制定一个能够做到这一点的模型才有意义。这不必是一个单一的模型:一个由各种模型或算法组成的管道通常是一个合法的权衡。我们将在下一章中作为初步搜索的一部分讨论问题分解。

2.3 风险、局限性和可能后果

想象一下,你已经构建了一个欺诈检测系统:它通过评估用户活动,通过暂停风险账户来防止恶意事件。这是一件宝贵的事情——自其推出以来,没有欺诈者通过,客户成功团队也很高兴。但最近,营销团队推出了一项大型广告活动,而你完美的欺诈检测器根据用户的流量来源(根据你的算法,这是未知的,因此有些可疑)禁止了一部分新用户。对营销的负面影响可能比检测欺诈活动的效率要大得多。

你可能会觉得这个例子很明显,不值得注意。然而,现实是残酷的:这种情况在团队不协调的公司中经常发生,这是你在设计系统时应该牢记的风险之一。你不应该想,“我们的团队是专业的;那样的失败在这里是不可能发生的。”因此,明确地思考风险是正确的做法,因为潜在的风险有很大可能性会扩散到项目团队或单个部门之外。

能力越大,责任越大——这句流行的谚语非常适用于机器学习软件。毫无疑问,机器学习非常强大。但除了这种力量之外,它还有一个更重要且危险的特征,那就是对大多数观察者来说的透明度,尤其是在底下的模型很复杂的时候。因此,专业的系统设计者应该意识到潜在的风险和现有的限制。

软件开发经典理论建议考虑功能和非功能需求。简而言之,功能需求是关于新功能或系统的功能、其价值和用户流程,而非功能需求是关于性能、安全性、可移植性等方面。换句话说,功能需求决定了我们应该设计什么,而非功能需求塑造了我们对它如何工作的理解。因此,当我们谈论潜在的风险和限制时,我们实际上是在收集非功能需求。

任何防御策略的基石是一个风险模型。简单来说,它是对“我们要保护免受什么威胁?”这个问题的回答。可能的最糟糕的情况是什么,我们应该避免什么?像“错误的模型预测”这样的回答根本没有任何信息量。一个与所有可能的利益相关者相一致的详细理解是绝对必要的。

瓦列里讲述的篝火故事

曾经我为另一家大型科技公司构建了一个动态定价算法。这是一个能够优化收入、利润或流量的整洁系统,对后两者有约束。它可以在用户级别工作,但一旦有了用户级别的原子性,就可以聚合到任何你想要的级别。它可以快速适应用户行为的变化,并实时为用户提供价格。它在探索和利用之间有一个很好的平衡,能够考虑不确定性,并且训练和推理都很快速。我甚至有写一篇关于它的文章的愿望,并为它想出了一个名字:“用于动态定价的双贝叶斯通用上下文赌博。”这张图概述了系统的算法设计;这不是很好吗?

侧边栏图片

动态定价系统的初始设计

然而,可能的风险、现有的限制和不良后果极大地改变了最终的设计。

风险:结果证明,你不能对不同价格的同一样物品进行歧视。当然,你可以对地点进行歧视,但如果是在同一个城市和同一样物品,并且你在网上销售,价格必须相同。技术上我们仍然可以进行歧视,但这会带来客户起诉公司的风险,进而导致公司损失大量金钱和声誉。幸运的是,我们对这一点或多或少有所准备,因为我们可以在任何我们想要的层面上进行聚合。你还记得用户级别的原子性吗?

局限性:第二次打击来自后端。结果是我们只能每隔 6 小时(最多!)更改一次价格,因此我们实时更改价格的能力并不那么重要。这是最后的打击,迫使我彻底简化系统,但仍保留一些适应能力,这将在下一个故事中介绍。

很容易看出,局限性和风险塑造了最终的设计,我不介意,因为我享受创造它的过程,并且准备好改变它(见下一图)。但如果是我的杰作呢?那之后我会怎么感觉?道德很简单——你需要尽快找出任何可能的风险和局限性;否则,你可能会被迫放弃所有辛勤的工作。

侧边栏图

动态定价系统的最终设计

理解风险和局限性将影响许多未来的决策,我们将在关于数据集、指标、报告和回退的章节中稍后讨论。在我们这样做之前,我们想给出一些例子,说明考虑(或忽略)有价值的数据如何影响你的目标设定。

2.4 误差的成本

当谈到误差的成本时,我们想引用史蒂夫·麦克康奈尔在他的书《代码大全》(第 2 版,微软出版社,2004 年)中精确定义鲁棒性和正确性之间的差异,使用构建 X 射线机和视频游戏的例子:

如视频游戏和 X 射线示例所示,最合适的错误处理风格取决于错误发生的软件类型。这些示例还说明了错误处理通常更倾向于更多正确性或更多鲁棒性。开发者倾向于非正式地使用这些术语,但严格来说,这些术语在尺度上是相对的。正确性意味着永远不返回不准确的结果;不返回结果比返回不准确的结果更好。鲁棒性意味着始终尝试做一些事情,即使有时会导致不准确的结果,也能让软件继续运行。

安全关键的应用通常更倾向于正确性而非鲁棒性。不返回结果比返回错误结果更好。辐射机是这一原则的好例子。消费者应用通常更倾向于鲁棒性而非正确性。任何结果通常都比软件关闭要好。我偶尔使用的文字处理器在屏幕底部显示文本的一小部分。如果它检测到这种情况,我是否希望文字处理器关闭?

这个概念对机器学习系统来说更加适用,因为它们对开发者和最终用户来说都可能是神秘的。与现代深度神经网络中巨大的矩阵乘法序列相比,一组ifwhile语句更容易记住。

想象一下,你正在开发一个像 Snap 或 TikTok 的 AR 面具这样的娱乐应用。在最坏的情况下,添加的效果可能在一个帧中看起来很丑陋——这不是一个很大的风险,因此鲁棒性在这里是一个合适的方法。相反的情况是一个用于医疗或交通需求的机器学习解决方案。你会选择一辆在不确定附近是否有行人时只是向前移动的自动驾驶汽车吗?当然不会:这就是为什么你想要在这里选择正确性的原因。

我们将在本书的第三部分更多地讨论这种权衡及其实际方面。到目前为止,我们应该提到的是,理解错误的成本是收集预设计信息的关键点之一。这实际上是风险概念的定量发展:对于风险,我们定义了可能出错的事情和我们想要避免的事情,然后尝试分配数值属性。数值方面可能因问题而异,在这个阶段不一定需要精确,但对于塑造格局至关重要。

根据我们的经验,人们往往更多地考虑积极场景,而在现实中,负面结果需要更多的关注。逻辑很简单:通常任何系统都有一个(或几个)积极场景,而许多故障模式被认为是负面场景。当然,每种故障模式发生的概率通常远低于良好结果发生的概率,但如果测量期望值,情况并不总是如此。想象一个交易系统在 99%的交易中赚取几分钱,但以 0.1%的概率损失全部资本,或者更戏剧化的是,一个医疗诊断系统为高薪医生节省了每位患者 3 分钟的时间,但每 1000 位患者中就有一次错过了一个严重但可治愈的疾病。

然而,有些错误可能是无害的,甚至可能是积极的。回到 2018 年,阿森尼在一家制作 AR 应用的公司工作——这是一款虚拟试穿鞋子的应用。该应用允许用户在购买之前看到鞋子穿在脚上的样子。该应用的第一个版本包含了一个欠拟合的模型,负责脚部检测和跟踪。结果,鞋子不仅出现在人类的脚上,还出现在宠物的爪子上,甚至玩具上。许多早期用户觉得这很有趣,所以这种错误的成本并不显著。但随着时间的推移,当模型性能在更传统的用户场景中得到改善后,这种效果消失了。

在估计错误的成本时,你还应该记住可能会有二级后果。例如,你的反欺诈系统可能会今天禁止太多合法用户,明天他们可能会通过口碑传播关于你的应用(“永远不要使用它;他们无缘无故地禁止了我”),这可能会埋没你的增长潜力。你的推荐系统提供不相关的建议,后来你基于对这种糟糕推荐的罕见点击日志训练了一个新模型,从而陷入了一个负反馈循环。

另一个关于错误成本的经典例子是信用风险评分,这是一个几乎在任何银行都能找到的常见任务。在申请被接受或拒绝之前,借款人的申请通常由基于机器学习的系统处理,以输出风险评分。这个风险评分可以是 1/0(有一个特定的阈值)或者介于 0 和 1 之间的连续值。

显然,向可能违约的客户发放贷款的成本和向成功还款的客户不发放相同金额的贷款的成本是不一样的。系统需要多少人偿还贷款才能抵消一个可能破产的人?我们应该计算人数/信用还是贷款金额?我们期望这个比率随时间保持不变吗?回答所有这些问题并考虑这些信息将大大增加项目被认为是成功的机会。

对于设计机器学习系统的人来说,这意味着什么?识别风险景观有助于我们了解需要避免的问题类型。一些错误几乎无害,一些可能会严重影响业务,还有一些可能是致命的。对设计系统时错误成本的适当理解对于下一步至关重要,因为它塑造了可靠性和数据收集的要求,提出了更好的指标,并可能影响设计的其他方面。

摘要

  • 问题空间总是先于解决方案空间。否则,很可能会在项目的后期阶段引起反弹。

  • 在从利益相关者和涉及的员工收集背景信息时,开始收集广泛的背景信息,并在需要时深入挖掘。

  • 在从众多潜在的机器学习解决方案中进行选择时,研究它们的局限性,并考虑这些局限性可能引起的风险。

  • 总是评估一个错误的潜在成本。如果有,检查它可能引起的潜在副作用:其中一些甚至可能导致积极的结果。

第三章:3 初步研究

本章涵盖

  • 将来自各个领域的用例应用于给定问题

  • 面对和解决选择合适解决方案时的“构建或购买”困境

  • 问题分解

  • 选择正确的创新程度

在第二章中,我们发现确定问题是在开发成功的机器学习(ML)系统中关键要素。你描述问题越好、越精确,构建一个能够高效满足商业目标的产品概率就越高。

现在我们将深入探讨几个关键方面,这些方面标志着设计一个全面高效的机器学习(ML)系统的下一个重要阶段——解决方案空间。本章将向您介绍更多关于寻找解决方案的内容,这些解决方案在过去帮助解决了类似问题,以及在我们未来系统的主要目标下,在构建我们的组件和购买第三方产品之间做出的始终艰难的选择,以及分解问题的正确方法,以及选择最佳的创新程度。

3.1 哪些问题可以激发你?

如果我能看得更远,那是因为我站在巨人的肩膀上。——艾萨克·牛顿

想象一下,你为像 Uber 或 Lyft 这样的出租车服务公司工作,并且存在一个已知的欺诈模式:一个合法的司机开始为公司工作,但后来他们将他们的账户转给了一个人,这个人不能成为司机(他们甚至可能完全没有有效的驾驶执照)。你的目标是通过对他们注册时上传的文件中的司机照片进行个人重新识别,提示司机从他们的车内自拍,并验证显示在驾驶执照上的人是同一个人。同时,存在非常合理的非功能性要求:为了隐私,你更愿意避免从他们的设备上传司机的照片到你的服务器。还有一个方面是验证应该足够快,并且能够抵抗各种对抗性攻击(欺诈者可能非常狡猾!)。

让我们根据这些信息总结这个案例:

  • 该问题基于人脸识别。因此,作为一个系统设计师,你需要熟悉该领域。

  • 解决方案应该以移动优先。因此,关于移动设备上的机器学习知识至关重要。

  • 解决方案应该能够抵抗欺诈尝试(一个不诚实的司机可能会试图展示一个合法司机的照片而不是他们自己的脸)。因此,在活动检测方面的经验将很有用。

这些问题在行业中通常都能得到解决,但很少在单一解决方案中处理。以下是一些例子。大型监控系统(如机场安全所用的)进行大量的面部识别,但它们的计算能力很少受到限制,并且它们的推理不需要挤进手机。另一方面,许多消费娱乐应用在手机上运行推理,它们的开发者非常擅长在有限资源下运行模型。最后,活体检测通常应用于用于身份验证的生物识别系统(iPhone 上的 FaceID 是最常见的例子)。

经验无与伦比,所以如果你足够幸运,成功地解决了所有三个问题,那就大胆地去做吧。如果没有,我们建议你花时间查看各种机器学习领域的用例,因为开阔的视野是你的最佳朋友。你通常在职业生涯中的一年里无法与数十个生产级机器学习系统一起工作,但研究这么多用例是可行的,并且可以弥补经验的不足。

在设计系统时,回想一下类似的系统并将它们作为参考是有用的。你不必直接复制某些模式,但它们可以作为灵感。我们还建议你不要忽视失败的故事,因为它们可以成为你案例中要避免的提示。这种方法与我们在第四章将要讨论的反目标概念有些重叠。

如同在软件世界中经常发生的那样,至少有两个方面的相似性:领域方面和技术方面(如图 3.1 所示)。

figure

图 3.1 在寻找帮助你构建系统的解决方案时,这两个方面同等重要。

前者是指寻找在商业问题方面尽可能接近的系统;而后者,我们应该回想一下具有相似技术要求的系统(例如,平台、延迟、数据模型、数据量等)。

阿尔谢尼的篝火故事

我曾经在一个制造优化公司从事图像分割问题。我的工作是找到装配线图像中的特定组件。这个问题是关于我需要的精度:它是亚像素级的。换句话说,我的系统在搜索极小物体时需要提供高度详细的结果。

制造数据的图像分割不是一个常见问题;你不能只是谷歌一下,然后从互联网上抓取第一个食谱。但在其他领域,如医学图像分析和照片/视频编辑中,细粒度分割很受欢迎,通常被称为图像合成。

如果你曾经尝试在 Zoom 应用中更改背景,你一定注意到了头发周围的伪影,这正是由于相关的算法远非完美(很可能是为了计算效率而优化,而不是精细的精度)。头发分割是图像合成的一个经典例子:从背景中区分头发很复杂,需要特定的技巧,比如尽可能避免图像下采样,并使用“软标签”——具有特定权重的既被标记为前景又被标记为背景的像素。

带着这种推理,我学习了关于图像合成最先进的方法,并将它们应用于我的制造数据,这最终显著降低了我的测试误差。

我们还鼓励你思考为什么某些决策会在系统设计和解决方案中被采纳。在设计和最终应用自己的直觉时,这些练习非常有价值,尤其是在设计复杂的机器学习系统时,包括解决这类困境的能力,比如是否从头开始构建或寻找现成的产品。

3.2 建立或购买:基于开源或专有技术的技术

想象一下,你为 Slack 工作,这是一个支持音频和视频对话的团队消息应用。它有一个功能:接近实时音频对话的语音识别。

但 Slack 最初被设计为一个以文本为主的通讯应用,可能使用语音对话的用户比例相当小。文本字幕的使用频率更低,因为这个功能默认未启用,其应用也相对有限。同时,对语音识别准确度的要求很高:如果质量不符合预期,这样的功能将毫无用处,甚至可能有害。

对非核心功能的高质量需求可能会促使你的功能团队在市场上寻找现成的解决方案。我们不能忽视 Slack 的规模:在 2019 年疫情高峰之前,它有 1200 万日平均用户。目前声称的数字已下降到 1000 万,但这仍然是一个令人印象深刻的数字。这意味着使用由供应商提供的第三方技术可能会花费太多,而启动一个内部、理想定制的解决方案将是最佳方案。你会选择哪条路?

3.2.1 建立或购买

与复杂的技术系统,包括机器学习系统相关的一个大困境通常被称为“建立或购买”。当问题熟悉时,找到已经作为服务销售的供应商解决方案的机会很大。让我们捕捉一下看待这个困境的主要角度。

问题是否与业务的核心理部分相关?专注于关键竞争优势并使用第三方服务来处理像基础设施这样的商品是一种常见的做法。十五年前,大多数公司都有专门的系统管理员,他们在数据中心管理大量服务器;如今,大多数公司从云服务器提供商那里租赁虚拟机。这是一个使用第三方服务来处理关键基础设施的例子,尽管如此,这并不是赢得市场的关键。尽管对于服务器基础设施至关重要的公司(例如,高频交易、广告技术或云游戏)存在例外,但这个领域仍然需要大量的研发投资。

许多公司使用第三方服务来解决与机器翻译、语音识别、反欺诈等问题相关的机器学习问题。将驾驶员的自拍照片与他们的驾照照片进行验证是委托给供应商的常见例子。

困境的另一个方面是经济方面。假设有一个供应商提供这个问题的解决方案,并且其服务在指标方面足够好,但合理的价格标准并未得到满足。也许你的公司在低成本生活区域(相应的薪资范围)招聘人才方面很出色,因此从头开始构建系统比使用第三方解决方案更便宜。如果一个供应商为加州的 VC 支持的初创公司提供合理的定价,并不意味着同样的价格对于在东欧或亚洲起步的公司仍然是合理的。

你可以切换到开源解决方案,但选择开源选项和购买选项之间可能并不明显。你不能说开源解决方案的成本为零,因为它的维护通常与与基础设施工作相关的隐藏成本以及潜在的解决问题的成本相关。另一方面,使用购买解决方案允许将许多这些问题委托给供应商,这意味着在坚持某个选项之前,你需要对潜在的花费进行初步估计。

还有一个方面虽然通常不会公开披露,但与这个困境仍然非常相关:职业主义。并非每个决策都是出于商业利益的考虑,公司越大,这种模式就越常见。因此,一些员工可能对推动“构建而非购买”的想法感兴趣,以实现具有重大影响的项目,从而为他们的晋升或简历添加一项华丽的成就。当然,我们并不支持这种解决构建或购买困境的方式,但由于这些情况在商业中并不罕见,我们不得不提及它们。

总体而言,构建或购买困境归结为几个关键因素,这些因素构成了你工作的背景。购买现成的解决方案意味着在开发上节省时间(如果你是一家初创公司,发布截止日期紧迫且严格,这可能是一个因素),并避免招聘在生产阶段可能不可或缺但软件发布后难以找到工作的额外专家。这也意味着你将获得一个经过测试、时间考验的平台。然而,在补丁或新版本发布方面,你将受到供应商日程的限制。构建自己的解决方案确保你控制着功能集、可扩展性和发布日历,并且可以在路上修复关键错误,而不依赖于供应商。但更高的控制权在其他方面需要更高的代价:你需要内部支持,并且肯定需要一个经验丰富的开发团队。

我们建议选择“购买”,如果

  • 你选择更快的发布。

  • 你没有专门的团队来开发/维护解决方案。

  • 许多来自不同领域的公司对这个解决方案有很高的需求。

一定要寻找一个在市场上拥有良好声誉的稳定平台。我们建议选择“构建”,如果

  • 你有足够的时间投入开发。

  • 你更喜欢可扩展性和按需更新的灵活性,而不是固定的发布日历。

  • 你可以承担内部支持的费用。

  • 你需要的不是一个足够好的解决方案,而是一个尖端解决方案。

  • 你有一大堆需要平滑集成的遗留系统。

  • 你正在处理高度敏感的数据,无法依赖外包的信息安全,或者简单地受到关于与第三方共享数据的相关法律规定的限制。

此外,还有一个极其重要的预算因素,你不能忽视,但同时,它不能归因于任何先前的列表。这是因为预算可以影响你的决定,无论是向哪个方向。如果你开发自己的解决方案可能导致过度支出,你希望选择购买选项。另一方面,如果你预算范围内的任何现成解决方案都不合适,构建选项是你的选择。无论你的情况如何,预算都是一个关键因素,始终需要考虑。

让我们回到 Slack 的开篇示例。解决这一困境的一种方法是从供应商开始,确保客户认可其功能,突出主要使用场景,并根据收集到的信息启动内部解决方案。

提醒:我们并不知道这个功能是如何实际实现的。这只是我们可能会采取的方法。

构建与购买决策的比例往往会随着时间的推移而变化。例如,至少在 2010 年代,至少有 9 个自然语言问题需要一个非常定制的解决方案,但在 2020 年代,这些可以通过简单的大型语言模型(LLM)API 调用解决,这使得从头开始构建这样的模型吸引力大减。

3.2.2 基于开源或专有技术

在考虑的较低层面可能会出现另一个困境,那就是开源技术与企业级专有付费技术之间的选择。在某个时候,你需要决定使用什么数据库进行存储或什么推理服务器更合适。为了回答这个问题,你需要对非功能性要求(如所需正常运行时间、延迟、负载容忍度等)有广泛的知识。对于一个初步的近似,逻辑如下:当你确信不需要紧急求助专家时,安全的选择是使用开源解决方案。相反的情况是构建一个高负载、关键任务系统;在这种情况下,站在巨人的肩膀上,比如特定的供应商,通常是有意义的。也存在混合的情况——为开源解决方案购买企业级支持,有时这可能是一条合适的中间道路。

值得注意的是,这里列出的原则并非特定于 ML——事实上,当我们设计“常规”无 ML 软件时,几乎相同的推理也是适用的。

3.3 问题分解

在软件工程师的工具箱中,最有用的工具之一是“分而治之”的方法,这种方法在机器学习(ML)中非常适用,无论是在低级算法实现还是高级系统设计层面。当你面对一个看似在现有规模下无法解决的复杂问题时,这是你可以应用的第一件事。

问题分解的一个典型例子是搜索引擎设计。用户可以查询任何随机的词组,包括之前从未查询过的词组(大约 15%的 Google 搜索查询是新的),并在几百毫秒内获得相关结果。

在高层次上,搜索引擎实际上只做一件事,那就是快速从数据库中提供相关结果。让我们关注两个属性:相关性和速度。快速获取一个稍微相关的结果是否更容易?我们认为是这样:只需丢弃复杂的排名算法,用简单的“文档包含查询的一些单词”启发式方法来代替。使用这样的谓词扫描整个数据库是完全可行的。从一小部分文档(数千份,而不是数十亿份)中找到相关结果是否更容易?当然,在小规模上,我们可以应用复杂的 ML 算法和大型模型,尽管推理速度较慢。

我们打赌你已经猜到了我们想要引导的方向——是时候将那些步骤结合起来,构建一个两阶段系统了。第一阶段是快速候选过滤,第二阶段是在确定的候选者中进行更复杂的排名。这种方法在许多搜索引擎中已经使用了数十年。

这个例子可以进一步发展:除了候选过滤的一次迭代之外,还可能有多个迭代。因此,基于查询语言和用户位置,可以在候选过滤之前过滤掉其他语言的文档,从而减少下游需要处理的文档数量,如图 3.2 所示。

figure

图 3.2 问题分解的过程

在计算机视觉领域,类似的多步骤流水线非常流行:首先应用深度学习模型,后处理负责最终答案。另一个应用领域与文本和其他半结构化数据相关:一步提取结构化数据,然后这些结构化数据在下游使用更受限制的模型进行处理。

我们知道分解的六个原因:

  • 计算复杂度—分解被应用于减少所需计算量(就像之前搜索引擎的例子中那样)。

  • 算法不完美—在上一步骤中产生的错误会通过后续步骤进行调整。那些在机器学习理论方面较强的读者可能会回忆起与提升算法家族的一些相似之处。

  • 利用算法的优势同时避免其弱点—例如,我们需要在图像上计数物体。一种方法是对回归问题训练一个卷积神经网络,但经典的卷积神经网络在设计上并不完美地满足这些需求(例如,池化层往往会丢失这类信息;参见图 3.3)。另一种方法是使用一个可以检测图像中物体的模型,并在其上使用经典的计算机视觉算法来计数前一步骤中的轮廓。由于归纳偏置,纯检测模型比端到端回归模型具有更好的泛化能力,并且后处理步骤是确定性和准确的。

figure

图 3.3 在池化层之后,合并了两个激活区域,从而失去了对物体计数问题至关重要的信息。
  • 数据融合需求—机器学习解决方案不能直接从其他来源获取数据,因此运行一个模型,根据结果获取额外数据,并处理融合后的数据是一种流行的模式。最近对大型语言模型(LLM)应用的兴趣就是一个很好的例子:许多特定领域的 LLM 解决方案遵循检索增强生成模式,本质上就是从向量数据库中检索相关数据,并将这些输入作为提示的一部分提供给 LLM。

  • 处理边缘情况—机器学习解决方案可能会失败,分解有助于早期解决问题。例如,一个简单的模型(或只是一系列条件检查)可以对输入进行评分,并在输入可能无效时引发错误。

  • 将不同的模型或逻辑应用于不同的数据子集——**有可能模型对广泛的用户群体效果良好,但很难推广到整个用户基础。这导致了一个简单的想法,即根据简单的启发式方法(例如,为不同的地理区域提供不同的模型)将用户路由到不同的模型或系统路径。我们将在第十四章中分享更多关于这个问题的细节。

我们知道这个列表可能不完整,但这是我们职业生涯中遇到的最明显的六个原因。

有时,流程并不是从一开始就按照那种顺序设计的,添加步骤的想法可能是在进一步的改进中出现。但这可能不是最好的模式:逐个堆叠组件,试图覆盖最近揭示的前一步的问题,会导致一个非稳健的设计,这种设计容易出错且难以维护,因为它没有遵循单一的想法。另一方面,在初始设计和甚至第一次实现中留下占位符是完全可以接受的(“稍后会有基于模型的选择获取,但现在我们使用随机样本作为概念验证”)。

阿尔谢尼的篝火故事

我在一家增强现实公司工作,该公司正在构建虚拟试穿解决方案。其中一款产品是鞋子试穿,一个检测视频流中脚部的应用程序,并渲染所选的鞋类。它需要结合多个算法构建,包括一个遮挡算法,该算法负责确定鞋子的哪一部分应该可见并渲染,以及哪一部分被帧中的物体遮挡。

在初始发布之前,这个解决方案的部分带来了许多麻烦;团队没有好的想法来以适当、可靠的方式实现它。在某个时候,公司的 CTO 接过了领导权,并提出了一种自己的算法,该算法解决了大多数情况下的问题。这个算法有许多缺点;它不够快,不太通用,其他团队成员难以理解,等等。但有一个很大的优点超过了所有这些——该算法在大多数情况下都有效!

侧边栏图片

基于机器学习的解决方案示例,仅在相机能够捕捉到真实鞋子的区域渲染鞋子

CTO 的算法是早期设计和实施的一部分,并成为早期产品发布的有价值部分。后来,团队开发了一种完全不同的方法,主要解决了旧方法的缺点,由于设计得当,它不需要进行重大更改。整个流程中只有一步被替换为一种更先进的方法,这提高了后续版本的整体体验。

机器学习系统的设计原则正受到该领域最近趋势的影响。在过去,构建包含许多小型、顺序组件的流水线是常见的做法。然而,随着深度学习模型的兴起,趋势转向了端到端单一模型的方法。这种方法有可能捕捉到数据中更复杂的关系,因为它们不受人工设计假设和限制的约束,需要较少的领域知识,并减少了步骤之间的错误积累。

语音处理是端到端方法如何改变设计的一个很好的例子。在端到端之前,文本到语音(TTS)模型通常包括两个主要组件:一个处理文本输入并将其转换为语音原子,如音素、重音和语调模式,另一个使用预定义的规则或统计模型将语言信息映射到声波,以合成人类语音。

另一方面,端到端 TTS 模型不依赖于作为中间表示的显式语言信息。相反,它们使用单个神经网络模型直接将文本输入映射到音频波形。

虽然端到端模型取得了成功,但它们本身无法包含知识,并且通常需要使用数据库来处理许多应用。

最近,LLMs 如 GPT-4 实现了令人印象深刻的零样本性能,这意味着它们可以直接回答问题,而无需任何额外的输入或训练。然而,这些 LLMs 计算成本高昂,容易产生幻觉(即,将错误信息作为真实信息呈现;参见“自然语言生成中幻觉的概述”,arxiv.org/abs/2202.03629,以获取更广泛的背景),并且它们的知识是隐含的,不能直接修改。

目前正在进行研究,寻找将大型语言模型(LLMs)的优点与使用可维护的外部信息源的能力相结合的方法。例如,Bing AI 和 ChatGPT 插件(openai.com/blog/chatgpt-plugins)以类似于人们使用搜索引擎的方式使用额外的在线资源,而 Meta AI 的 Galactica(galactica.org/)是第一个引入工作记忆令牌概念的,这允许模型生成可以由解释器执行的 Python 代码片段,以提供精确的答案。这些想法在 Toolformer(arxiv.org/abs/2302.04761v1)模型中得到了进一步的发展,这是一个专门训练以使用各种第三方 API 的模型。类似的想法也反映在快速增长的开放源代码框架 LangChain(python.langchain.com/api_reference)中。虽然这些方法尚未在生产系统中得到广泛应用,但它们有潜力改变机器学习系统的分解方式。

根据它们的复杂性和新颖程度,机器学习系统可能意味着各种创新水平。一些竞争领域需要巨大的研究投资;在其他领域,你可以使用一个非常基础的机器学习解决方案。让我们找出如何定义你需要为你的系统达到的创新水平。

3.4 选择适当的创新程度

向任何机器学习系统的利益相关者提出这样一个简单的问题:最终产品应该有多好(即多准确)?最常见的回答通常是“完美”、“100%”和“尽可能好”。但让我们试图弄清楚这些简单而含糊其辞的回答背后是什么。

答案“尽可能好”隐含地意味着“一旦我们满足其他约束条件。”最明显的约束是时间和预算。他们是否希望在 10 年内拥有一个完美的机器学习系统?很可能不是。下一个季度末发布的“可接受的良好”系统更好吗?很可能,是的。

我们将在第五章中详细阐述对“足够好”和“完美”之间差异的精确理解。但在设计过程刚刚开始的最早期阶段,确切的指标并不重要。重要的是有一个粗略的理解。

通过我们创建、维护和改进具有多种规模和目标的机器学习系统的经验,我们确定了三个不同级别的完美要求,所有系统都可以在这些级别之间分配。术语可能有所不同,但据我们看来,这些是最合适的:

  • 可行的最小机器学习系统

  • 平均人类水平的机器学习系统

  • 最佳级别的机器学习系统

一个可行的最小系统可能是一个非常简陋的解决方案,胶带是关键粘合元素。这样的系统期望是“它基本上是可行的”,观察者将能够检测到各种故障模式。这样的系统被认为是基线和原型;不期望有任何创新。

人类水平的表现设定了一个特定的标准。许多现有的成功机器学习系统甚至还没有达到人类水平的表现,但对公司来说仍然有价值。因此,我们可以认为达到这种表现水平需要相当的研究和创新。

最后,是最佳级别的类别。一些系统在没有击败大量竞争对手时几乎毫无用处——这在像交易或广告技术或全球产品如搜索引擎这样的超级竞争领域尤为常见。准确性的微小变化可能造成数百万的利润或损失,在这种情况下,机器学习系统被设计成以实现可能达到的最佳结果为目标。

我们为什么在这里讨论创新?问题空间和解决方案空间之间的桥梁强烈依赖于我们从一开始就假设的创新水平。在“最小可行系统”桶中,我们完全没有创新——我们只是使用我们知道的简单且最快的解决方案并继续前进。在光谱的另一端,我们得到的是无尽的创新,系统永远不会准备好,团队总是在寻找新的改进措施以在下一个版本中实施。

在这三个桶之间分配问题将是一个非常强大的技术,但有一个重要的因素我们不能忽视:所需创新水平不是静态的。在许多情况下——尤其是在初创公司中——事物尽可能地以最简化的形式构建,以便以后升级。这是有道理的:公司首先评估功能是否被客户(或内部用户)需要,然后根据客户反馈来改进系统。如果一个发布的功能在市场上独一无二,即使其最简化的实现也能带来巨大的价值,竞争对手会立即着手改进自己的产品。这会将初始系统从第一个桶移动到第二个桶,甚至更接近最前沿的联盟。许多初创公司面临着这种过渡的问题,而设计一个可以从原型演变为世界级宝石的系统(这是工程的艺术)的案例极为罕见。这种艺术的简化版本是设计一个可以在尽可能保留现有构建块的同时重建的系统,这是一个相当高的目标。

3.4.1 哪些解决方案可能是有用的?

了解你所需要的创新水平以及系统的某些高级结构,你可以在较低层次上寻找实现想法。当这一章节正在准备时,有五个流行的信息来源可以深入研究。

arXiv

arXiv (arxiv.org/) 是一个主要在科学、技术、工程和数学学科中分发学术论文的网站。数学和计算机科学,包括其子学科,占那里发布的超过两百万篇论文的很大一部分。

arXiv 是了解你问题学术观点的好地方。除了阅读与你关键词相关的一切之外,我们鼓励你使用引用和链接机制:一旦你找到一篇相关论文,你可能会对它提到的旧论文和新引用它的论文感兴趣。arXiv 是一个独特的生态系统——有浏览器扩展和额外的网站可以帮助你的搜索。一个好的开始是寻找概述论文(标题中通常包含“调查”):通常它们包含关于该主题的正确提炼的智慧。

仅就其本身而言,arXiv 作为知识来源可能显得有些过于原始:几乎不可能阅读所有新论文,而且其搜索机制从现代角度来看有些原始。在 arXiv 之上,有多个流行的工具简化了探索。目前,我们推荐 arxivxplorer.com/,这是一个基于论文摘要的现代搜索引擎,尽管在本书出版时,可能会有另一个花哨的工具(之前最受欢迎的附加组件是 arxiv-sanity-lite.com/)。

Papers with code

如您所料,Papers with Code (paperswithcode.com/) 是一篇学术机器学习相关论文的汇编,这些论文都附有代码形式的实现。论文按主题分组,并在可能的情况下按性能排名。

您可以找到来自学术界的最接近的问题,并查看解决该问题的前 N 篇论文,它们的指标,一些元信息(例如,这种方法是否需要额外的数据?),以及——非常重要的一点——指向公开实现的链接。对于更喜欢仓库而不是正式学术写作的人来说,这个网站是一个真正的变革者。

GitHub

一旦我们提到了代码实现,就不可避免地要提到 GitHub (github.com)。GitHub 是最受欢迎的开源软件平台,拥有任何场合的仓库。其缺点在于其规模:如果您在那里寻找不常见的东西,您实际上是在沙堆里找针。

GitHub 并不专门针对机器学习领域,但与此同时,大多数开源机器学习项目都位于那里。

Hugging Face

Hugging Face 模型中心 (huggingface.co/models) 是一个主要平台,分享了许多模型和数据集。在撰写本文时,该中心包含超过 560,000 个公开可用的机器学习模型。分类和标签工作非常精确,其中很大一部分模型提供了小型交互式基于网络的演示,以展示其功能。

Hugging Face 公司最初专注于自然语言处理(NLP),该平台一直是分享以 NLP 为导向的模型的主要平台。如果您正在解决一个包含文本处理的机器学习问题,我们建议您去那里寻找与研究相关的模型。

Kaggle

Kaggle (kaggle.com) 是最受欢迎的竞争性机器学习平台。组织使用该平台举办挑战赛,吸引世界上最优秀的机器学习实践者争夺奖金和荣耀。在比赛中,参与者分享与给定挑战相关的想法和代码片段。比赛结束时,获胜者和领导者通常会揭示他们的秘密。除了比赛,Kaggle 还作为多个数据集的托管网站,因此找到与您问题相关的公共数据集的机会很大。

Kaggle 是这份列表中最出色的作品,原因有很多。如果一个比赛组织得不好,问题可能就有些表述不清:竞争者可能不会解决实际问题,而是试图寻找捷径,比如数据泄露。此外,最终解决方案通常在现实中不适用:模型过于庞大,因为延迟限制可能被忽略。最后,代码片段通常不够整洁:参赛者追求快速迭代,而不是长期维护。

尽管有所有这些缺点,Kaggle 论坛仍然可以为你提供关于问题的详尽概述,包括学术论文和可能后来成为学术主流的黑客风格代码。值得一提的是,还有一些网站汇总了最佳的 Kaggle 解决方案,例如 farid.one/kaggle-solutions/

我们想强调的是,目前阶段还不需要基于这项研究来选择解决方案。它应该为你提供更多关于背景的细节,使你的决策过程更加可靠。

3.4.2 在解决方案空间中工作:实际案例

让我们用一个详细的例子来重申上一节提到的要点。想象一下,你加入了一家股票摄影公司。这个业务实际上是一个市场:摄影师加入平台并上传他们的照片,而寻找特定图像用于说明目的的客户(编辑、设计师、广告专业人士)购买这些照片的版权。这个市场通过销售佣金赚钱。公司非常希望在其网站上创建一个有效的搜索系统。

从一个角度来看,照片库非常庞大,拥有数百万张图片。当客户寻找照片时,他们通常对某些特定内容感兴趣,而简单的分类或其他天真分类法很难找到。因此,你被雇佣来构建一个现代搜索工具,能够根据客户的文本查询找到最相关的照片。你应该如何理解这个问题的背景?

首先出现的是“建造还是购买”的问题。让我们假设你认为像你这样的规模的公司通常会设计自己的解决方案,但情况并不总是如此。一些侦察工作可能是合适的。你可以很容易地发现许多供应商——既包括大型企业也包括年轻初创公司——提供搜索引擎作为服务。当你尝试寻找这些服务时,很多解决方案可能会变得不相关——你的公司需要一个基于文本查询的图像搜索引擎,这并不是最受欢迎的模式。尽管如此,仍有一些技术提供商提出了相关的建议,所以让我们记住它们。

首先,让我们考虑其他公司解决类似问题的情况:

  • 当然,还有其他照片库,其中一些可能已经构建了很好的搜索引擎。他们很少会公开他们的引擎是如何构建的细节(你可以挖掘一些博客文章或会议演讲),但并非没有。

  • 有像 Google 和 Bing 这样的通用搜索引擎。显然,你需要的是一个规模非常不同的系统——你需要处理数百万张图片,而它们处理的是数十亿张。在这里,你可能会说,“如果我的团队规模是 N 倍小,我该如何复制这样的巨头?”当然,在容量方面,没有人可能在与“大玩家”竞争,但你不需要这样做,因为你的主要目标将是找到满足你解决方案需求的想法,而不是多一行代码。

  • 与前一点截然相反,你可以找到一些面向消费者的项目,它们可以帮助分类个人照片收藏。它们可能还没有准备好处理数百万张图片,但可能只有几千张。好处是其中一些是开源的,所以你可以直接深入代码以获取灵感。

  • 最后,还有一些非虚拟商品市场——例如,销售衣服、家具等等。其中一些是像亚马逊这样的巨头,而另一些则是面向利基市场,甚至比你的公司还要小。他们的业务很大程度上依赖于搜索质量,但他们的商品不仅仅是图像,而且通常有更多的属性(它们可能是文本描述或卖家信息)。这些搜索引擎使用关于物品的更多信息,而不仅仅是视觉信息;在机器学习领域,我们称它们为多模态。

搜索引擎是信息检索学科中最受欢迎的应用之一。其从业者早期就采用了许多机器学习方法,但并没有局限于仅使用机器学习方法。对于那些对领域不太自信的人来说,从维基百科开始,对学科进行高层次的了解(或刷新记忆)可能是合适的。在了解更多关于信息检索的知识后,你可以通过阅读更多关于图像检索的内容来进一步深入。

当你在阅读有关构建搜索引擎的文档时,你肯定会看到一种分解模式:正如在许多搜索引擎中一样,在你的场景中,并不是每份文档都应该被排名。从一开始,用户就可以指定要求:例如,照片应提供为原始文件(而不是压缩的 JPEG 文件),宽度至少为 5,000 像素,且价格不超过 50 美元。这些条件可以迅速将搜索候选者从数百万缩小到数万,而我们根本没有触及图像和查询语义。这种优化将非常有价值,并可能成为你未来设计的基石。

另一件你可以发现的事情是,在底层,大多数搜索引擎实际上只做一件事。它们为用户查询和潜在相关项目(文档)对计算相关性分数,并根据这个分数对项目进行排名(见图 3.4):

relevancy = f(encode_text(query), encode_image(item)) 

figure

图 3.4 基于给定查询的相关性,搜索引擎如何对图像进行排名的分解

在我们场景的情况下,这引发了许多开放性问题:

  • 函数f可以是哪种算法家族?

  • 你如何编码查询(文本)和项目(图像)?

  • 你如何衡量相关性?

  • 你如何将用户反馈纳入系统中?

每个问题都很广泛,值得单独一本书(或者至少是多个章节)来探讨,因此,为了简洁起见,我们仅建议在深入研究我们之前提到的信息来源时,应牢记这些问题,从 arXiv 到 Kaggle。

下一个问题是你所寻求的创新程度。这里有几个想法:

  • 公司已经有一个非常基础的搜索技术。它已经过时,并且经常产生不相关的结果,但总比没有搜索要好。

  • 公司的业务可以从良好的搜索中受益。目前,由于搜索结果不佳,许多用户无法找到他们想要的,因此他们离开网站并转向竞争对手。适当的搜索质量阻碍了公司其他方面的努力:如果新用户可能因为找不到所需内容而流失,那么推出大规模营销活动有什么意义呢?

  • 预算非常有限。同时,在项目首次成功之后,有很好的机会会有新的研发资金流入。

第一个观点清楚地表明,最基本的可行产品在这里不适用,因为你已经有了。同时,有限的预算意味着你最初不能追求最先进的技术解决方案。因此,你需要设计一个在有限预算和进一步改进选项下的稳固系统。

在本章中,我们涵盖了您编写设计文档准备过程中最重要的要素。您现在知道如何分解问题,哪些外部和内部因素将影响您对构建或购买困境的应对方式,哪些在线资源最有帮助,以及如何决定您的解决方案应具有的创新程度。

所有这些知识不仅将成为您编写系统设计文档的基础,而且还将帮助您理解是否最初就需要一个机器学习系统。最后一点可能听起来很有趣,甚至具有争议性,因此我们将在下一章中尝试对其进行详细阐述(以及许多其他内容)。

摘要

  • 寻找市场上可以完全或部分满足您需求解决方案。如果找到了,请自问系统中哪些部分可以使用这些解决方案。

  • 定义系统的整体视图。尽量将其绘制成三到五个非技术主管都能理解的块。

  • 建设或购买困境可能会成为在构建自己的组件和购买第三方产品之间做出选择的决定性因素。为了妥善处理它,考虑各种内部和外部因素,包括可用时间和所有涉及团队的能力。

  • 检查潜在因素列表,这些因素指向分解的必要性。如果你正在解决的问题符合其中任何一个,那么它很可能也需要被分解。

  • 可能会有强烈的诱惑去交付一个尖端的产品;然而,你需要问自己你准备好为了创新而投资于系统了吗,以及这种投资是否首先会得到回报。

  • 不要犹豫,通过流行的在线聚合器浏览,以找到你可以作为解决方案参考案例的使用案例。

第四章:4 设计文档

本章涵盖

  • 设计文档周围最常见的神话

  • 定义反目标以更尖锐地关注核心目标

  • 根据现有信息起草设计文档

  • 审查设计文档

  • 设计文档的演变

一旦你定义了系统应解决的问题、利益相关者列表,以及对于产品最合适的技术和解决方案的初步理解,如第三章所述,就到了准备设计文档的时候了。

值得注意的是,在创建机器学习(ML)系统的早期阶段,并没有固定的行动顺序。一旦你确定了问题和目标(尤其是在初创公司工作,交付速度往往比遵循流程更重要的情况下),你就可以开始准备设计文档。但鉴于本书以清单的形式呈现,行动列表也以传统顺序展示。

就像一位作者的管理者曾经说过的那样,没有任何花哨的推荐算法能打败一个拿着购物清单的客户。这些人有一个目标和实现它的计划,没有什么能阻止他们。

如果你仔细想想,编写代码只是提供一组特定的指令以实现一个特定的目标。从某种意义上说,设计文档是一个元算法集,用于实现一个特定的目标,其中涉及许多子算法。尽管如此,设计文档仍然被许多人质疑,要么是官僚化的四个恶兆之一,要么是惯性使用的基础。

在本章中,我们将检查设计文档周围最常见的神话。我们将介绍并定义反目标的概念,作为引导你朝着项目目标前进的额外指南,并将开始本书的实践部分,由基于接近现实生活场景的两个设计文档代表。

4.1 围绕设计文档的常见误区

在过去的几年里,设计文档经历了许多错误的假设和误解,可能会阻止你为项目准备一份有组织、合适的论文。接下来,我们将检查最常见的误解,并解释为什么你不应该过分关注它们。

4.1.1 误区#1. 设计文档只适用于大公司,不适用于初创公司

你可能会认为,将部分工作量用于准备设计文档只对大型公司有意义。在这个反论点中有一个合理的假设:成熟的组织需要投入更多的时间和资源来编写设计文档,与拥有十几名员工的初创公司相比。但这并不意味着小型公司应该完全不准备设计文档:正如一句著名的谚语所说,“计划没有用;规划才有用。”编写设计文档的美丽之处在于揭示你在产品和技术方面的盲点,这将让你在中期内节省很多,尤其是如果你切掉了不相关的数据。对于后者,我们推荐应用我们称之为“反目标”的方法,我们将在稍后的单独章节中详细介绍。

当这本书处于早期访问阶段时,我们的早期读者有一个共同的评论:“嗯,这很好,但在初创公司中并不是这样运作的。”虽然我们同意初创公司的交付节奏是不同的,但我们仍然坚持设计阶段是必要的。确实,联合创始人和早期工程师可以在咖啡休息时间达成共识,而一个大型企业可能会在同一范围内浪费 6 个月的时间。我们也同意编写正式文档可能效率不高,但这并不是我们倡导的。一旦你确信它能让所有合作者达成一致,一个简单的笔记加上简短的描述就足够了。在黑客马拉松中寻找奖品时,忽略软件和机器学习工程的良好实践是可以的,但黑客马拉松风格在更长的距离上几乎不起作用。

4.1.2 误区 #2:设计文档仅适用于复杂项目

如果从经典意义上来看设计文档:一个涉及最终产品所有细节的庞大、劳动密集型工作,从总体范围到部署后的风险验证,这个说法中确实有一丝真理。毕竟,仅编制这样一份文档本身可能需要的时间比项目的整个生命周期还要长!

通常,这样的论点可能来自缺乏灵活性的人,或者是一个热衷于反对设计文档、急于利用任何有利于自己的论据的坚决反对者。

实践表明,即使是小型项目,一个结构良好的设计文档也能确保早期识别潜在风险,如果项目最终扩展,可以作为未来改进的参考,最重要的是,它有助于防止范围蔓延,当每个利益相关者都倾向于添加一个新功能时。

即使是简单的倡议,也能从具有相应详细程度的设计文档中受益。

4.1.3 误区 #3:每个设计文档都应该基于模板

许多公司,尤其是那些已经建立起来的企业,会保持其推荐的模板具有严格、僵化的结构,考虑到他们的业务规模,这可能是有用的。然而,我们建议避免将设计文档模板固定化。根据我们的经验,模板永远不应该是神圣的教条。这样的模板可能会试图同时实现太多目标,因此变得臃肿,并使人失去准备和研究这些文档的动力。这就是为什么我们建议保持核心模板的简约性,并根据系统特定的需求和上下文进行扩展。

初看之下,创建设计文档的过程可能看起来直接且简单。实际上,从一开始,你将遇到许多因素,如果忽视它们,将会干扰过程并使你退后几步。

记住:你的任务不是创建一个草案文档并说服每个人都相信其纯洁性和正确性。你的任务是尽可能多地找出弱点(包括激励你的利益相关者找出它们),这样最终,经过多次迭代,你将有一个文档,它允许你开始开发你的机器学习系统。

4.1.4 误区 #4:每个设计文档都应该导致一个部署的系统

如果你是一名工程师,需要建造一台机器,你需要从蓝图开始。其他工程师将审查它并提供反馈,这可能会导致蓝图的其他迭代——一次又一次,直到你的设计最终准备好付诸实施。

同样的原则适用于设计机器学习系统。机器学习系统是一个高度复杂的互联领域机器,在实施前,你的设计文档需要经过多次迭代才能进行彻底的准备。然而,更常见的情况是,一个好的设计文档最终导致根本没有任何机器学习项目。

这可能听起来很荒谬,但让我们想象你面临两个选项的选择:

  • 在模型、特征、损失函数和数据集上不懈努力 6 个月,结果只是将你的项目放在架子上(这是大多数机器学习项目最终会发现自己所在的地方)

  • 花费 2 到 4 周的时间试图描述

    • 我们为什么要进行这个项目?

    • 我们如何做到这一点?

    • 我们是否拥有所需的一切?

    • 我们能否用一个更不高效但更省力的解决方案?

    • 是否可以达到预期的结果?

发现 90%的结果可以来自两个 IF 语句可能会令人沮丧,但仍然比两个选项中的第一个要好得多。

4.2 目标和反目标

设计文档的一个目标是通过设定基石和边界来减少对问题的不确定性。在文档起草之前,所有相关人员对问题和解决方案的理解水平都较低且不一致。一种可以帮助解决此类问题的技术是使用反目标——这些逆命题可以帮助我们缩小问题和解决方案的空间。

设计文档的每一部分都可以被视为对多个问题的回答:潜在系统的目标是什么,关键成功标准是什么,我们应该关注哪些技术方面,我们如何解决给定的子问题,等等。一个新手错误就是忽略权衡,并为系统列举无休止的目标:例如,它应该做 X、Y 和 Z;具有高性能;精确;易于维护且开发成本低;并且直观易懂。显然,不可能成功地将所有良好的属性都融入一个系统中,你需要一种方法来平衡这种可能的过度性。

设置反目标使我们能够划掉我们并不真正关心的方面,并额外突出我们认为至关重要的方面。比如说,我们正在构建一个将用于内部的系统,输出工件是供执行团队和分析人员阅读的各种报告。我们可以立即假设处理时间对于这样一个系统来说不会是关键——只需确保报告能在早上准备好即可。因此,“处理时间”将首先加入反目标列表,这样我们就不会在这个参数上浪费时间。或者想象一下为精品店构建推荐引擎:如果当前商品数量只有三位数(见图 4.1),那么你肯定不需要支持数百万件商品,这意味着对于最终解决方案来说,过度的生产力是不行的。

图

图 4.1 对于销售商品数量少于 1,000 件且流量较低的商店,在构建推荐系统时不应追求可扩展性,因为如今几乎任何技术解决方案都能处理其负载。

这样的反目标有助于我们只关注重要方面,并放弃那些对实现系统主要目标没有积极影响的方面。

以下示例说明了精品店推荐引擎的目标和反目标列表可能是什么样子:

  • 目标:

    • 从查看到加入购物车步骤的转化率提高

    • 为用户提供的多样化推荐

    • 为用户提供的低延迟

  • 反目标:

    • 以处理商品数量为尺度的可扩展性

    • 以并发用户数量为尺度的可扩展性

    • 支持新的商品类别

类似的逻辑也适用于设计文档的其他部分。如果你在实施过程中形成了一个想法,后来发现它存在内在的致命缺陷,那么在文档中提及这个问题作为反例是有意义的。想象一下,你正在设计一个可扩展的系统,并考虑大量使用云基础设施,直到你了解到最大的潜在客户由于隐私原因对其硬件的使用有严格的限制。在这种情况下,一句像“云解决方案 X 可能是一个好的数据存储选项,但由于 Y 的云隐私限制,不适用于这种情况”的话,可以设定重要的限制,并可能激发对替代技术实现的思考:“如果 X 从技术角度来看是可行的,那么是否有可以安装在我们自己服务器上的开源 X 替代方案?”

目标缺失不应该被视为设计文档中的主要信息来源,但可以成为增添缺失风味的调料,逐渐成为文档结构的重要组成部分。

有疑问的目标及其对最终结果的影响

我们有两个故事来强调目标设定不明确如何影响机器学习系统的发展。

2016 年,瓦列里在一个大型银行的收款部门工作。到那时,银行的管理层已经决定将机器学习引入其日常运营,并依赖算法支持而不是依靠一套僵化的规则和直觉。瓦列里的一项首要任务是创建一个模型,选择银行必须接触的下一位用户以最大化产出——一个可以通过激励(支付承诺、费用豁免、折扣)激活的用户。现有的流程涉及大量的人工工作,转换率大约为 50%。在接下来的两个月内,测试了一个基于大约 100 个工程特征的相当基本的非线性模型,其结果令人惊讶地达到了 80%的转换率,而旧的流程仍然提供 50%的转换率。

团队很高兴也很兴奋地向他们的高级副总裁展示结果。在我们完成演示的第二秒钟,她说:“这些客户有什么特别之处?我想知道他们的动机。”在 2016 年,用 100 个特征的非线性模型来回答这样的问题并不容易,更不用说人们的行为和他们的动机是两件完全不同的事情了。例如,从一开始,高级副总裁的目标就是理解“为什么”,而企业的目标就是理解“谁”。因此,团队必须完全不同地设计和构建系统及模型,旨在回答这两个问题,即使这样做可能不如只回答一个问题那么高效。因此,一开始设定的一个糟糕(或不恰当)的目标让团队落后了 3 个月。

第二个例子涵盖了我们在第二章中讨论的定价算法。一开始,我们的目标是基于周转率最大化商品总销售额,同时保持利润率在给定水平。

在某个时刻,该模型找到了一种巧妙的方法来实现目标。产品目录中有一个音响扬声器,该模型开始以低于购买价格的价格出售。结果,24 小时内销售的扬声器数量比之前 90 天还要多。公平地说,这仍然在利润率限制之内,因为我们不介意利润率为负作为任务的一部分。

然而,你可以想象这和我们真正想要的完全不同(正确的目标应该是增加收入同时保持利润率,影响 X%的类别,其中包含 Y%的 SKU,且侵占率不超过 Z)。当然,收入确实上升了,利润率也保持在给定的限制内,但最终,每个人都只是跑去购买那一个型号。没有购买其他扬声器。

幸运的是,那是一个带有少量动态定价商品的测试发布,这表明初始目标设计得不好,我们需要开发一个更全面的目标设定方法。幸运的是,整体设计是解耦的,易于调整。

4.3 设计文档结构

在本节中,我们本可以专注于关于经典设计文档内容和结构的理论信息,但事实是,为机器学习系统准备的设计文档几乎不会依赖于传统软件开发中应用的实践。除此之外,其结构可能因公司而异,所以我们认为过分关注布局的细微差别是没有意义的。相反,我们建议更多地关注需要涵盖的项目。此外,我们的目标是展示设计文档作为机器学习系统设计中的一个实体。因此,从本节开始,在本书的其余部分,每个章节的结尾将有一个大型的实践块,代表设计文档的一部分,它包含了给定章节的主要信息。我们认为这是本书的一个关键组成部分,它将与理论与篝火故事并驾齐驱,同时提供了一个将现实生活中的解决方案应用于问题的示例。

在接下来的内容中,我们将向您介绍两个虚构案例,每个案例都有其独特的具体细节、特征、问题和背景。这两个案例将成为两个不同设计文档的基础,这些文档将随着章节的逐步展开和演变而逐渐增长和进化,增加更多的深度和复杂性。最终,我们将拥有两个完整的文档可供使用。

在本节中,我们开始概述一个可能在实际生活中编写的项目设计文档。为此,我们引入了一个虚构公司,超级零售商(Supermegaretail),这是一家零售公司,有一个需求预测项目即将推出。

在第 4.4 节中,我们给出了设计文档第一章节可能看起来的一个非常简短的例子。我们将只包括主要主题;否则,它将无法放入一本书中。

备注:设计文档正文中用斜体写的任何文本都包含我们的支持性评论,但不属于文档本身。

设计文档:超级巨零售(Supermegaretail)

问题定义

i. 来源

超级巨零售(Supermegaretail)是一家零售连锁店,通过遍布不同地区数千家商店的网络运营。该连锁店的客户购买各种商品,主要是杂货、家居必需品、个人护理、运动补充品等等。

为了销售这些商品,超级巨零售(Supermegaretail)必须在将它们运送到商店位置之前购买或生产它们。购买商品的数量是需要定义的关键数字,这里有不同的可能情况。

为了便于计算,我们假设超级巨零售(Supermegaretail)为特定商店购买了 1,000 个单位的产品 A:

  1. 超级巨零售(Supermegaretail)在下一批交付前购买了 1,000 个单位并售出了 999 个单位。这是一个理想的情况。只有 0.1%的剩余,零售商接近最优的收入和利润。

  2. 超级巨零售(Supermegaretail)在下一批交付前购买了 1,000 个单位并售出了 100 个单位。这通常是一个糟糕的情况,原因很明显。超级巨零售(Supermegaretail)希望售出与购买数量几乎相等的单位,而不缺货。差距越大,超级巨零售(Supermegaretail)的损失就越大。

  3. 超级巨零售(Supermegaretail)购买了 1,000 个单位并售出了 1,000 个单位。这应该被视为一种糟糕的情况,因为我们不知道如果人们有机会购买,他们会购买多少个单位。可能是 1,001 个、2,000 个或 10,000 个。这种缺货情况模糊了我们对世界的理解。更糟糕的是——它驱使客户从超级巨零售(Supermegaretail)转向其竞争对手,在那里他们可以购买到没有缺货的商品。

另一个约束条件是我们有很多易腐食品,它们不能长时间放在商店货架上:它们要么被售出,要么被浪费。

项目目标是缩小已交付和已售出商品之间的差距,使其尽可能窄,同时避免出现缺货情况,具体的服务水平协议将在后续规定。为此,我们计划在 ML 系统的帮助下,预测特定商店在特定时期内特定商品的需求数量。

ii. 相关性和原因

本节强调了问题的相关性,并基于探索性数据分析进行了支持。

A. 现有流程分析

超级巨零售(Supermegaretail)目前是如何订购、配送和销售商品的?

对于超级巨零售(Supermegaretail)来说,可能的情况列表可能如下:

  1. 与商品制造商达成交易的计划范围:

    • 这是一个为期 1 年的合同,在前 9 个月的前 90 天内有机会进行调整。
  2. 增加销量时的额外折扣:

    • 每增加 2,000 万美元,额外优惠 2%。
  3. 作为制造商和商店之间物流枢纽的配送中心数量:

    • 全国共有 47 个配送中心,这使得它们成为预测的实体存在和聚合点。
  4. 配送中心和商店之间的交付节奏:

    • 通常,每两天就有一辆卡车连接配送中心和商店。
  5. 店内仓库的有无:

    • 大多数商店没有仓库。然而,装卸区可以(并且确实)有效地用于储存卸载的物品 2 到 3 天。
  6. 谁在什么阶段决定什么和在哪里交付?

    • 配送中心将有一个交付计划下达。商店经理可以覆盖并调整它。
  7. 预测范围:

    • 主要预测范围是每周和每月。然而,在处理商品制造商时,需要一年的预测范围。
  8. 流程的商务负责人:

    • 物流部门

    • 采购部门

    • 运营部门(店长)

B. Supermegaretail 在预测需求和实际需求之间的差距上损失了多少?

虽然计算因库存过剩和过期商品造成的损失相对容易,但计算因缺货情况造成的损失则要困难得多。后者可以通过一系列 A/B 测试或专家意见来估算,这通常比运行这些测试更快、更便宜。

总损失可以通过将这两个因素相加来估算,提供一个理想但无法实现的解决方案的收益估计。

初始计算显示,去年损失约为 8 亿美元。

从以下设计文档的下一部分(但仅限于本章)开始,我们已经勾勒出了一些问题,以避免内容过于庞大。回答这些问题将帮助您决定进一步行动,答案将在我们通过系统的不同阶段时在后面的章节中揭晓。

C. 其他原因
  • 其他团队可以使用我们的解决方案,使开发更具吸引力和合理性吗?

  • 也许我们可以将需求预测解决方案卖给其他零售公司(显然不是直接竞争对手)。

iii. 前期工作

本节涵盖这是否是一个全新的问题,或者之前是否已经做过某些事情。通常,这是一系列你提出的问题,以避免重复工作或犯以前的错误。

  • 如果 Supermegaretail 已经意识到这个问题并实施了一些需求预测方法,会怎样?它在不同地点有各种商店,其需求预测可能已经相当高效。公司是如何做到的?

    • 滚动窗口?

    • 专家委员会?

    • 规则加额外快速交付?

    • 我们是否有一些限制需要考虑,比如最小或最大订单量?

  • 我们能否快速改进现有的解决方案,或者我们需要一个全新的解决方案?

  • 如果超级大零售的当前预测在某些类别中足够好,而在其他类别中却毫无用处,那会怎样?换句话说,我们是否可以在这里采用混合方法,至少在最初,从最不成功的类别开始,这些类别中预测和实际销售额之间的差距最大?

  • 如果我们的方法无意中破坏了某些东西,那并不危险。我们正在测试那些我们一直都有问题的类别,而没有触及那些一切都很顺利的类别。

  • 换句话说,我们需要对现有解决方案进行广泛的、全新的探索性数据分析。

iv. 其他问题和风险

  • 我们是否有必需的基础设施,或者我们需要构建它?

  • 如果我们选择复杂的东西,它可能会失控。我们需要实施哪些必要的检查和平衡措施来避免灾难?如果我们遇到问题,我们是否有回退方案?

  • 我们有多确定可以显著提高质量并减少人工负担?我们真的能解决这个问题吗?

  • 一次错误的代价是什么?缺货和过剩库存很可能有不同的错误成本。

  • 如果我们处理缺货情况,我们能否处理增加的流量?

  • 我们需要多久进行一次预测?预测的粒度是多少?

正如你所见,即使是解决问题的简要概述和使用之前收集的数据进行的研究,也可能会轻易地让我们写出 10 页的文档。这份草案将帮助我们决定是否需要进一步深入,或者最好是现在就停止,避免复杂的机器学习解决方案。

本章的下一段内容同样重要:它提供了一个如何审查设计文档的实际例子。如果你对机器学习系统设计还不太熟悉,你可能还没有达到职业生涯中拥有足够经验和信誉以参与这种工作流程的阶段。然而,审查你的第一个设计文档只是时间问题,所以最好提前做好准备,你将看到一些关于审查基础的实际建议。

4.4 审查设计文档

Audi alteram partem [让另一方也有发言权]——拉丁谚语

到目前为止,我们还没有看到由单一个人编写的、足以从一开始就实施的完整草案设计文档。然而,我们已经遇到了一些非常好的草案,在第一次迭代之后,这已经足够了。

这个事实是至关重要的,并且很容易解释。复杂的系统需要来自具有不同专业知识和背景的许多人的输入。作为设计文档的作者,你的部分工作是使所有相关方都能更容易地导航。用章节和子章节概述你的文档将帮助领域专家从一开始就知道该去哪里。否则,当大多数人看到 10 页以上的文档时,他们的自然反应可能是关闭它并忘记它。

下面是前两个关键点:设计文档必须对尽可能多的人可访问和可见,并且对所有参与者来说易于导航。

当人们开始审阅任何类型的内容时,他们就开始批评并提出替代方案。作为作者,你希望鼓励这种行为。毕竟,你在第一次迭代后就有最佳和最合适的设计的可能性有多大?

尝试为每个命题/固定点推导出解释,因为它们可能来自不同的条件:

  • 审稿人之前使用过这个工具,并认为它是处理所有事情的最佳工具。

  • 当前基础设施存在局限性。例如,我们无法提供实时支持,但可以每 60 秒执行批量作业。这会影响流程吗?

  • 我们有能够维护技术 A 的人,但没有人维护技术 B。因此,从设计中的技术 B 转移到技术 A 会更好。

  • 审稿人希望炫耀他们对技术的了解,并向更广泛的受众展示这种知识。

  • 审稿人看到了解决特定任务的其他方法,并提出了替代方案。

尝试理解每个输入背后的推理,并征求更多信息,直到你完全理解原因。根据我们的个人经验,第一轮迭代中最无帮助的输入可能听起来像是“看起来不错。”尝试找到你看起来最可疑的部分,并向审稿人询问,表达你的担忧。一个普遍的好做法是列出一系列担忧,包括你不确定的事情,以吸引审稿人的注意并促进请求。

设计文档的一个常见失败模式是过于泛泛而谈。这对设计文档来说是一个巨大的缺点,通常这是由于一个人可能没有足够的背景知识来填补所有空白。作为初始作者,你需要促进他人的输入——例如,突出一些缺乏必要信息的问题区域,并鼓励审稿人补充拼图的缺失部分。

我们讨论了如何创建设计文档以及期望审稿人提供什么,但由于本节的标题是“审阅设计文档”,让我们尝试反转我们的建议,并从审稿人的角度应用它们:

  • 看一下设计文档,并尝试通过概述进行导航。哪些章节让你感到最有信心?

  • 如果文档中没有概述,检查文档末尾是否有开放的问题/需要考虑的事项。

  • 如果不存在,请要求设计文档的所有者提供这些信息。

  • 当添加评论时,尝试回答自己你添加了什么价值,以及你希望通过它实现什么。

  • 如果你倾向于写“看起来不错”,请三思。你是真的觉得它看起来不错,还是只是想节省时间或依赖他人的意见?如果是这样,也许最好根本不发表评论。

4.4.1 设计文档审查示例

我们为第二个示例设计文档选择的情况是第三章中提到的股票照片公司。认识一下 PhotoStock Inc.,我们被雇佣来构建一个现代化的搜索工具,该工具能够在客户文本查询的基础上找到最相关的照片,同时提供出色的性能并显示最相关的库存图片。

生意实际上是一个市场:摄影师加入平台并上传他们的照片;寻找特定图像用于说明目的的客户(编辑、设计师、广告专业人士)购买这些照片的权利。市场通过销售佣金赚钱。公司非常重视在其网站上建立一个有效的搜索系统。

我们基于前几章讨论的内容,提供了一部分原始且书写不佳的设计文档,并像审查文档一样对其进行评论。这次,用斜体加粗的文字表示审查者的评论。

设计文档:PhotoStock Inc.

I. 问题

PhotoStock Inc.的 90%用户通过我们网站上的搜索栏找到图片。这使得搜索栏成为用户体验的核心组件。

目前,搜索引擎基于由 Elasticsearch 驱动的模糊搜索算法,其索引每周一晚上自动更新。我们假设它处理同义词的能力较差。此外,用户还可以应用产品团队手动创建的预设中的额外过滤器。

许多用户对搜索质量不满意,这一点通过客户访谈和基于点击流的分析得到了证明。只有一小部分搜索会转化为购买。

审查者:确切有多少用户?请添加链接到现有的报告和仪表板以提供更多背景信息。

审查者:搜索到购买的转化率是许多变量的函数,搜索结果的相关性只是其中一个因素。我建议进一步分解问题,这样我们可以更有效地估计由搜索结果不佳造成的损失收入。

审查者:请提供更多关于当前搜索解决方案的信息,因为它的工作方式和与其他系统的交互并不清楚。主要故障模式是什么?

审查者:我们如何衡量用户满意度?请添加具体标准。

II. 目标

将搜索到购买的转化率提高 100%。

审查者:为什么是 100%?有没有任何理由要达到这个确切的增长水平?

审查者:如前所述,搜索到购买的漏斗不仅由搜索质量决定。让我们缩小目标范围。

审查者:有没有任何重要的非技术要求,比如延迟?

审查者:我们目前如何衡量转化率?我们如何衡量这项努力是否提高了转化率?

审查者:您定义了反目标来突出我们不需要关注的区域吗?

III. 风险

我们可能会失去许多忠诚的现有客户,因为他们无法遵循他们当前的行为模式。

如果我们发布有缺陷的软件,我们可能会失去一个重要的收入来源。

审阅者:关于行为模式的一个有趣的观点。有没有用户如何适应搜索引擎缺陷的例子?

审阅者:凭借我们的蓝绿部署和 A/B 测试平台的基础设施,我们应该能够逐步推出新系统;我们应该利用它来减轻此类风险。

IV. 参考文献

  • [链接到 YourPowerfulSearch,一个面向市场的企业级搜索系统]

  • [链接到 Bing 搜索相关性团队的学术论文]

  • [链接到 Google Analytics 仪表板,显示与 PhotoStock 搜索相关的各种指标]

审阅者:请添加更多与内部搜索相关的工件,例如 PhotoStock BI 仪表板和 UX 研究。

审阅者:我相信 YourPowerfulSearch 不是市场上唯一的相关解决方案;我们能否获得更广泛的概述?Bing 的论文也是如此。

审阅者:我们如何估计搜索相关性对我们商业指标的影响?这可能会对可能的预算产生很大影响。

你可以在评论中看到一些模式,例如

  • 尽早提出合法的问题

  • 建议缺失的部分,无论是以问题还是陈述的形式

在设计评审阶段尽早提供反馈可以在后期阶段节省大量时间。问题应该启动并促进健康讨论,并解锁更好的解决方案,而不应该是具有侵略性或有害的。

4.5 设计文档是一个活生生的东西

这个部分最初计划作为本章开头的列表中的第 5 个神话,但我们认为这个观点很重要,足以成为一个独立的章节。

那么为什么在任何阶段编辑或批评设计文档时都没有恐惧或犹豫呢?答案是设计文档确实是一个活生生的东西。

通常,设计文档的演变看起来是这样的:

  1. 第一次迭代

  2. 同行的反馈

  3. 重写文档的 60%

  4. 同行的反馈

  5. 重写文档的 30%

  6. 同行的反馈

  7. 重写文档的 10%

  8. 开始实施系统

  9. (三个月后)来自现实世界的反馈

  10. 重写文档的 30%

这样的演变,你需要预期你能够完成设计文档的唯一时间是在你完成系统实施之后,但这也不一定保证。

一旦你的系统实施,生活就会暴露其缺陷,你必须解决这些问题;或者产品经理决定需要新的功能,系统必须扩展;或者政府发布新的立法,你必须考虑;或者有基础设施迁移或新的用例。无论是什么情况。为了执行这些更改,工程师需要理解系统并阅读设计文档。到那时,可能已经出现了一种新的模式或技术,它完美地符合系统。

如果情况不是这样,新的功能和重构需要反映在设计文档中,这把我们带到了前面提到的设计文档演变。

正因如此,设计文档永远不会结束。它是一个活生生的东西,只要它描述的服务存在。即使你离开了公司,其他人也需要从你那里接过旗帜,如果他们不想最终得到一个完全无法支持的系统。

重写相当一部分设计文档可能会让人感到沮丧,但从长远来看,这是一件你可以从中受益的事情。对于复杂的系统来说,甚至有理由实践“设计两次”的方法——承认你的第一个设计可能不是最好的,然后采用两种截然不同的方法进行设计。正如实践所表明的,这种方法可以揭示隐藏的问题和机会。让我们引用约翰·奥尔特豪特(John Outerhout)的《软件设计哲学》(Yaknyam,2018):

我注意到,对于那些真正聪明的人来说,接受“设计两次”的原则有时是困难的。在他们成长的过程中,聪明的人会发现,对于任何问题,他们的第一个快速想法就足以获得好成绩;没有必要考虑第二个或第三个可能性。这使得他们很容易养成不良的工作习惯。然而,随着这些人变老,他们会被提升到更困难的环境中。最终,每个人都会达到一个点,即你的第一个想法不再足够好;如果你想取得真正出色的成果,你必须考虑第二个可能性,或者也许是一个第三个,无论你多么聪明。大型软件系统的设计就属于这一类:没有人足够聪明,能够第一次就做对。

一个好的设计(以及一个好的设计文档)应该减少系统的各种复杂性方面,无论是理解、构建、修改还是维护。如果系统从一开始就承诺要复杂,那么通过多次迭代提前减少这种复杂性通常是一个很好的投资。

倾向于建造而非思考的人可能会对此感到烦恼:“喂,你先是建议写文档而不是写代码,现在又建议反复做这件事?”好吧,一旦你不再收到新信息,多次迭代就没什么意义了,有时在写出一些概念证明之前,你甚至无法改进设计。然而,设计两次通常是敏捷性和准备性之间的一种公平权衡。

摘要

  • 就像正确设定的目标是你的系统的基准一样,反目标代表了一些需要避免的区域。确保记住它们,并让它们被指出。

  • 一个组织良好的设计文档将帮助你理解你是否真的需要一个机器学习系统。

  • 让所有利益相关者参与审查设计文档草案。

  • 当你看到“看起来不错”的回答时,总是再次联系你的同事,以获得更清晰、更精确的反馈。在草案阶段的不明确性很可能导致后期出现变更请求。

  • 如果你正在对你的设计文档进行评审,鼓励人们批评现有的想法并提出替代方案。

  • 考虑邀请具有不同背景和经验的审阅者,以收集多样化的反馈。

  • 在评审设计文档时,提出具体问题以指出薄弱或不必要的部分。

  • 不要害怕多次迭代,因为没有任何初稿会在未经编辑的情况下达到最终阶段。

  • 记住,设计文档是一个活生生的东西,即使在你的系统发布后,也可能会受到编辑。

第二部分 初级阶段

在本部分,我们将更深入地探讨初级阶段工作的技术细节。第五章涵盖了为您的机器学习系统选择合适的指标和损失函数的好处,定义和利用代理指标,以及应用指标层次结构。第六章专注于数据集,从选择最佳数据源和处理原始数据,到定义健康数据管道的特性以及决定多少数据足以使机器学习模型达到最佳性能。第七章回顾了标准和非常规的验证方案,描述了分割更新过程,并在设计文档中概述了验证方案。在第八章中,您将了解各种基线类型,从最早的、最简单但效率极高的常量基线开始,到模型基线、特征基线和深度学习基线。

第五章:5 损失函数和指标

本章涵盖了

  • 为你的机器学习系统选择合适的指标和损失函数

  • 定义和利用代理指标

  • 应用指标层次结构

在上一章中,我们首先触及了为你的机器学习(ML)系统创建设计文档的主题。我们弄清楚了为什么设计文档需要不断编辑,以及为什么你在其中实施的每一个变化不仅不可避免,而且是必要的。

很不幸,一个机器学习系统不能直接解决问题,但它可以通过优化特定任务来尝试近似它。为了有效地做到这一点,它必须得到适当的调整、指导和监控。

为了指导机器学习系统的努力,我们使用其算法的损失函数来奖励或惩罚以减少或增加特定的错误。然而,损失函数用于训练模型,通常必须是可微分的,这意味着可用的损失函数选择范围较窄。因此,为了评估模型的表现,我们使用指标;虽然每个损失函数都可以用作指标(一个很好的例子是均方根误差 [RMSE],它经常用作指标,尽管我们不确定这是否是最好的决定),但并非每个指标都可以用作损失函数。

在本章中,我们将讨论如何选择最佳拟合的指标和损失函数,重点关注如何在设计过程中进行适当的研究并提供选择动机。

5.1 损失

损失函数,也称为目标函数代价函数,有效地定义了模型如何了解世界以及因变量和自变量之间的联系,它最关注什么,它试图避免什么,以及它认为什么是可接受的。因此,损失函数的选择可以极大地影响你模型的总体性能,即使其他所有因素——特征、目标、模型架构、数据集大小——保持不变。切换到不同的损失函数可以完全重塑整个系统。

选择合适的损失函数(即,选择模型从其错误中学习的方式)是设计机器学习系统中最关键的决定之一。回忆一个永恒的轶事,我们可以在计算酒吧访客的平均工资时对均值进行优化,直到比尔·盖茨走进来(mng.bz/M1w8)。

很不幸,并非每个函数都可以用作损失函数。一般来说,损失函数具有两个特性:

  • 它在全局上是连续的(预测的变化会导致损失的变化)。

  • 它是可微分的(其梯度可以用于基于梯度下降的优化算法)。有一个例外:在特殊情况下,无梯度优化方法适用,尽管实践者通常更喜欢避免它们,因为基于梯度的方法通常收敛得更好。

虽然这两个点对任何损失函数都相关,但选择一个最适合你特定情况并且最接近你系统最终目标的损失函数是很重要的。

这就是高级损失函数发挥作用的地方,提供了改进你的模型的有吸引力的方法。与对特征或模型本身的操作不同,它们通常不会影响运行时方面,这意味着所有代码更改都仅与训练管道相关,将更改隔离到系统的小部分总是设计的一个好特性。但更常见的是,我们见证了机器学习工程师(尤其是应届毕业生)坚持使用特定的损失函数,仅仅因为他们习惯了将其应用于类似的问题。一个臭名昭著的例子是,回归问题中默认选择均方误差(MSE)或平均绝对误差(MAE)损失函数,并且很多时候,这是许多从业者唯一的选择

同时,虽然选择一个合适的损失函数(或一组损失函数)是一个可能大大提高你的模型性能的决定,但这并不是万能的解决方案。我们曾与几位机器学习工程师(他们通常拥有令人尊敬的学术背景和博士学位)合作,他们试图仅通过一个优雅的损失函数来解决他们遇到的所有问题。这种方法与完全不关注损失函数的方法正好相反,但仍然远非理想。一个好的机器学习系统设计者会考虑许多工具,而不是过分依赖一个。总的来说,经验法则是这样的:你的系统研究越深入,你越有可能需要投入时间来寻找或设计一个非平凡的损失函数。

几年前,瓦列里与一名实习生合作,构建了一个预测加密货币交易量的模型。像往常一样,他要求实习生在开始任何工作之前准备一个设计文档,这是一个富有洞察力的练习。实习生没有考虑地跳过了损失函数章节,列出了一些他将用来评估系统性能的指标,而没有给出任何理由。

为什么这不可接受?通过一个例子,我们可以回顾一个简化的情况,其中关于回归问题的损失函数知识被缩小到两个最广泛使用的损失函数:MSE 和 MAE。

想象一下,我们有一个目标值向量 Y = [100, 100, 100, 100, 100, 100, 100, 100, 100, 1000]和一个对所有样本都相同的独立变量 X 向量。

如果我们使用 MSE 作为损失函数来训练一个模型,它将输出一个预测向量:

Y_hat = [190, 190, 190, 190, 190, 190, 190, 190, 190, 190]

如果我们使用 MAE 作为损失函数来训练一个模型,它将输出一个预测向量:

Y_hat = [100, 100, 100, 100, 100, 100, 100, 100, 100, 100]

备注:请注意,这是一个思想实验,旨在突出这个想法并使其更容易理解。如果我们需要,我们可以创建合成数据来重现整个过程——特征、目标和模型,但为了简单起见,我们将只使用前面的数字。

当我们使用 RMSE 损失函数计算模型的 MSE 和 MAE 时,会得到以下数字:MSE = 72,900,MAE = 162,残差的均值为 0,残差的中位数为-90(图 5.1)。

figure

图 5.1 优化均值后的残差

当我们使用 MAE 损失函数计算模型的 MSE 和 MAE 时,结果将是 MSE = 81,000,MAE = 90,残差的均值为 90,残差的中位数为 0(图 5.2)。

figure

图 5.2 优化中位数后的残差

没有什么好奇怪的,优化 MSE 的模型得到了更好的 MSE,因为 MSE 试图最小化均值,所以均值残差更好。另一方面,优化 MAE 的模型提供了更好的 MAE,因为 MAE 试图优化中位数,所以中位数残差更好。但这对我们意味着什么呢?哪个损失函数更好?这取决于我们的应用。

假设我们正在优化一个飞机的导航系统,任何大于 850 的错误都意味着飞机将偏离着陆场并坠毁。在这种情况下,优化 MAE 不是一个理想的选择。当然,我们可以有 9 次中有 10 次得到完美的结果,只有 1 次车辆被摧毁,但这无论如何都是不可接受的。我们必须不惜一切代价避免异常值或对它们进行惩罚,因此使用 MSE 甚至是一些更高阶的修改。

但假设我们正在优化一个加密货币交易所每天交易所需的流动性数量。流动性指的是加密货币在没有损失价值的情况下转换为现金或其他加密货币的能力,这对所有加密货币交易所都是至关重要的。高流动性意味着一个动态且稳定的市场,允许参与者以合理的价格快速交易。然而,过多的流动性意味着分配的资源没有得到利用。在这种情况下,有 9 次中有 10 次预留比所需的现金更多,这远远不是我们想要的。我们可以从不同的角度来审视这个问题:优化 MSE 的模型多分配了 810 个单位,少分配了 810 个单位,而优化 MAE 的模型有 10 次中有 9 次准确无误,并且只少分配了 900 个单位,这似乎是一个更好的决策(如果少分配不如多分配糟糕 9 倍的话),向模型传达我们需要的信息。

很容易看出,尽管我们使用了均方误差(MSE)和平均绝对误差(MAE)来训练模型,但我们应用了不同的标准来评估它们。对于飞机导航系统,我们计算实际值与预测值之差大于 850 的次数。对于流动性优化,这是我们在现场或过度分配加权求和的次数。这表明,训练模型以优化特定的损失函数并评估该模型的表现可以代表两个不同的任务,我们将在第 5.2 节“指标”中讨论这些任务。在继续之前,我们想分享一些关于确定深度学习模型损失函数的细微差别和方面的见解。

5.1.1 深度学习模型的损失技巧

在基于深度学习的系统中,尤其是在处理文本、图像或音频数据的系统中,损失函数的选择更为关键。

正确选择的损失函数可以帮助解决与模型训练相关的许多问题,尤其是对于复杂模型和/或数据领域。例如,交叉熵损失是分类问题的经典解决方案。其中一个问题与类别不平衡有关。如果一个类别被过度代表,通过熵损失优化的模型可能会遇到所谓的模式崩溃——即对于任何输入都输出一个常数(流行类别)的情况。这些问题已经通过多种方式得到解决(例如,数据欠采样/过采样、为类别设置自定义权重等),但所有这些都需要大量的手动调整,并且不可靠。研究人员通过尝试设计一个解决该问题的损失函数来解决这个问题;最引人注目的成果可能是林等人(“密集目标检测的焦点损失”,arxiv.org/abs/1708.02002),现在这种损失函数在帮助解决数据不平衡问题的工具中占据了其应有的位置。

焦点损失(见图 5.3)是一种动态缩放的交叉熵损失,其中缩放因子随着对正确类别的信心增加而衰减到零。直观上,这个缩放因子可以自动降低训练过程中简单示例的贡献,并快速将模型集中在困难示例上(更多信息可以在paperswithcode.com/method/focal-loss找到)。

figure

图 5.3 建议的焦点损失函数更关注误分类示例,同时减少对正确分类示例的相对损失(来源:林等人)。

最初,这种损失函数是为计算机视觉中特定于对象检测的问题引入的,后来,该方法扩展到许多其他领域,包括与图像无关的领域,如音频或自然语言处理。我们发现的焦点损失最远的应用是在论文“自然语言处理能否帮助区分中国的炎症性肠病?”(Tong 等人;mng.bz/aV9X)中介绍的,这证实了思想在不同领域之间的传播。

在某些情况下,合理的解决方案可能是将多个损失函数结合用于单个模型。这种方法的必要性可能出现在复杂问题中,这些问题通常是多模态的,并且通常与多个并发数据集相关联。在这里,我们不会提供太多关于使用组合损失函数的细节,因为它研究密集,但我们愿意给出一些例子:

  1. “从手机扫描中生成逼真的体积型虚拟人”(Cao 等人;dl.acm.org/doi/abs/10.1145/3528223.3530143)。作者结合了三种损失函数家族(分割、重建、感知)。生成计算机视觉模型通常需要考虑组合损失。

  2. “使用 AlphaFold 进行高度精确的蛋白质结构预测”(Jumper 等人;www.nature.com/articles/s41586-021-03819-2)。著名的 AlphaFold 2 模型能够以令人印象深刻的准确性从蛋白质的遗传序列中预测其 3D 形状。这对生物技术界来说是一个巨大的进步,它在其内部使用了多个辅助损失函数。例如,一个掩码语言模型的目标,可能是受到 BERT 类似架构中使用的损失函数的启发,是自然语言处理模型中流行的一类。

  3. “GrokNet:用于商业的统一计算机视觉模型主干和嵌入”(Bell 等人;mng.bz/Xxr6)。这是我们能够回忆起的组合损失示例中的瑰宝。作者的目标是构建一个可以解决多个问题的单一模型,因此他们使用了 7 个商品数据集和 83(80 个分类和 3 个嵌入)损失函数!

通常,多个损失函数要么用于帮助模型收敛,要么用于使用单个模型解决多个调整问题。

当损失函数有助于设置和微调准确性、效率,并在训练过程中最小化错误时,度量标准则用于评估其在一定参数集内的性能。

5.2 度量标准

我们优化的损失函数和用于评估模型性能的度量标准可能彼此非常不同。回想一下,在第四章中,Supermegaretail 的需求预测系统的最终目标是减少交付和销售物品之间的差距,使其尽可能窄,同时避免缺货情况。如果我们尝试可视化这个流程,它可能看起来像图 5.4。

我们知道合适的损失函数是必不可少的,但关于指标呢?我们能不能选择一些标准指标,评估各种模型,选择最好的,部署它,并通过 A/B 测试来估计潜在的成功?

图

图 5.4 一个适用于需求预测系统的一般性管道,完美契合超级大零售案例

很遗憾,不行。选择正确的指标集必须像选择损失函数一样仔细。更重要的是,虽然流行的损失函数集是有限的,但总有机会为特定的业务领域定制一个指标。选择错误的指标,反过来,当我们设置我们的模型训练无关值时,会导致误导性的优化,最终导致在实际场景中的性能不佳。结果,我们不得不在模型开发中回滚几步,导致时间和资源的巨大浪费。但即使为您的机器学习系统选择了正确的指标,也不能保证项目的成功。

瓦列里营火故事

以前,我为一家经常遇到不付款债务人问题的银行开发了一个机器学习系统。我们准备好的系统有两个主要目标:

  • 减少逾期付款的数量

  • 让客户更加响应

我们选择将客户从非付款者转变为付款者的转化率作为指标。

我们首先实施了一个承诺支付系统,其工作方式如下。假设史密斯先生接到银行的电话:“史密斯先生,您没有按时支付您的贷款。我们能否期待您在三天内支付所需金额?” “哦,当然,我会的,我会的,”史密斯先生说。银行的人挂断电话并勾选“承诺支付”框。但然后史密斯先生就会违背承诺,一分钱也不付。

我们开始工作时,转化率为 0.5,这意味着这种情况发生了一半。这不算太糟糕,但绝对不算出色。

考虑到人们对银行此类电话的态度以及他们尽快挂断电话的愿望,违约承诺是一个非常常见的案例。但事实是,这是一个双刃剑。一方面,客户可能不会觉得和银行交谈愉快,尤其是如果他们没有发起对话的话。但银行也对无用的沟通不感兴趣,不得不在呼叫中心和员工身上花费过多的资金。

作为解决方案,我们建立了一个系统来预测客户同意支付并履行承诺的概率。我们用短信取代了人工电话。这样我们就不用打电话给我们的客户,说服他们做出承诺。该系统还应该预测客户行为。

在验证阶段,系统显示的转化率为 0.9——几乎是人工操作的近两倍!然而,两周后,在实战条件下,转化率骤降至 0.35,而我们只剩下了一周的时间向副总裁提交报告。

显然出了些问题,我们需要找出原因。我们回顾了之前这个指标是如何工作的,它相当简单:如果客户承诺在某个月的一天偿还债务,但 3 天内没有这样做,他们就会被标记为债务人。为什么是 3 天?答案是,实际操作与在银行数据库中获得关于此操作信息之间的差距是 3 天。

假设你应在 3 月 1 日 EOD(截至当日)支付下一笔贷款。在 3 月 1 日当天工作结束后,你下班后去银行支付所需金额。到了 3 月 2 日,系统检查数据库并发现尚未支付(难怪,因为信息要到 3 月 4 日才能到达)。系统认为:“看起来我们有逾期未付款项了,”并启动了一条短信,因为根据系统收集的数据,你收到短信后支付所需金额的概率很高(90%!)。3 月 2 日稍后,你收到了银行发来的要求你支付贷款的短信。“他们肯定搞错了。我会告诉他们我已经支付了,”你想,并开始填写回复短信中的表格。问题是,这个表格不允许你输入早于当前日期的还款日期。你只能指定你将在 3 月 2 日或之后支付。但你已经在 3 月 1 日支付了。你该怎么办?你表明你在 3 月 2 日支付了,并提交了表格。三天后,系统检查了非付款者名单,打开你的个人资料,看到你承诺在 3 月 2 日支付,但在此日期后的 3 天内并未支付。

当我们重新配置系统时,转化率几乎达到了初始值,高达 0.8,但我们在过程中遇到的问题显示了达到你的指标可能会受到整体系统行为缺陷的阻碍。

表面上,选择正确指标框架的方法非常直接:选择最接近最终目标的那个。然而,正如接下来的篝火故事将展示的,这可能非常棘手。你可以尝试自己找到这个指标,或者寻求一些外部帮助。以下是我们推荐考虑的一些选项:

  • 如果你很幸运,有一个指标层级,我们将在本章后面讨论,请使用它来导航到你需要的数据指标。

  • 一些公司有一个专门负责指标的部门;如果是这样的话,请使用他们的帮助。

  • 如果这两种情况都不适用,你可能需要使用产品经理和数据科学家来开发最佳的指标。

  • 如果你打算解决的问题类似于以前解决的问题,并且解决方案被证明是稳固和高效的,那么自然地,如果需要,可以从一个项目转移到另一个项目,并对其进行某些修改。

  • 如果你有一个 A/B 测试团队,他们通常也具备足够的知识来选择或创建一个度量标准。

如果你没有这里提到的事情的奢侈,你可以做以下事情:

  • 参考设计中的目标部分并与之对齐(重要的是要刷新最终目标是什么,而不是你如何记住它们)。了解你的目标将帮助你理解哪些度量标准将帮助你实现这些目标,或者至少帮助你排除明显不合适的度量标准。

  • 尝试通过编写类似于度量标准层次结构的地图来分解最终目标。这可能需要超过一个阶段才能实现,但这种练习将帮助你将你的大目标分解成几个较小的组成部分,每个部分都有自己的度量标准。拥有许多小部分在手将有助于组装更大的整体。

  • 找到描述每个阶段成功最佳表现的度量标准。

  • 如果某些东西难以直接衡量,可以用代理度量标准来代替(参见 5.2.2 节)。代理度量标准将允许你在系统发布之前收集必要且非常重要的信息。

  • 使用这张地图,选择最能代表最关键阶段或以最佳方式总结地图的度量标准。

在下一个篝火故事中,我们将回顾经典的二元分类问题。

Valerii 的篝火故事

最近,我和我的一个朋友就欺诈模型的评估进行了交谈。欺诈模型通常试图解决二元分类任务,其中 0 表示非欺诈,1 表示欺诈。

没有度量标准是理想的,这总是取决于最终目标。然而,当我们谈论欺诈模型时,我们通常希望保持欺诈交易与合法交易的一定比例。如果我们有 10 倍多的交易,那么有 10 倍多的欺诈是可以接受的,但不是 20 倍或 30 倍。换句话说,我们希望有一个概率模型。

此外,欺诈通常属于类别不平衡问题,而且这种平衡在时间上是不稳定的。有一天比率可能是 1:100(欺诈交易激增),第二天,1:1000(普通一天),第三天,1:10,000(欺诈者休假了)。

这个模型族最流行的度量标准集是精确度和召回率,这可能不是最佳选择。

精确度的问题在于其计算同时考虑了两个类别:

侧边栏图像

假设我们有一个模型,它有 95%的概率预测欺诈是欺诈(真阳性[TP]),5%的概率预测非欺诈是欺诈(假阳性[FP])。

让我们回顾三个场景,其中 P 是正样本的数量,N 是负样本的数量:

  • P = 10,000, N = 10,000,

sidebar figure

  • P = 100,000, N = 10,000,

sidebar figure

  • P = 1000, N = 10,000,

sidebar figure

如您所见,即使其他什么都没有改变,类别平衡也会对指标产生显著影响。

现在我们来看看召回率(召回率 = TP/(TP+FN) = TP/P = 真正例率 [TPR])并检查相同的三个场景:

  • P = 10,000, N = 10,000,

sidebar figure

  • P = 100,000, N = 10,000,

sidebar figure

  • P = 1000, N = 10,000,

sidebar figure

在这种情况下,类别平衡根本不影响指标。

还有一个称为特异性的指标可以替代精确率:inline figure

sidebar figure

同样的三个例子显示了以下内容:

  • P = 10,000, N = 10,000,

sidebar figure

  • P = 100,000, N = 10,000,

sidebar figure

  • P = 1000, N = 10,000,

sidebar figure

由于类别不平衡,召回率和特异率不会改变,因为这些指标对类别平衡不敏感。

最初,我的朋友创建了一个笔记本(mng.bz/5Ov8)来证明我是错的。以下代码展示了他的思路:

import numpy as np

def gen_labels_preds(fraud, genuine, fraud_predicted, correct_fraud_predicted):
  labels = np.concatenate([np.repeat(True, fraud), np.repeat(False, genuine)])
  preds = np.concatenate([
    np.repeat(True, correct_fraud_predicted), # TP
    np.repeat(False, fraud - correct_fraud_predicted), # FP
    np.repeat(True, fraud_predicted - correct_fraud_predicted), # FN
    np.repeat(False, genuine - (fraud_predicted - correct_fraud_predicted)) # TN
    ])

  return labels, preds

def calculate_metrics(labels, preds):
  TP = (preds & labels).sum()
  FP = (preds & ~labels).sum()
  TN = (~preds & ~labels).sum()
  FN = (~preds & labels).sum()

  recall = TP / (TP + FN)
  precision = TP / (TP + FP)
  FPR = FP / (FP + TN)

  return recall, precision, FPR

他设计了两个具有以下指标的模型:

  • A 有 20 个误报,并且 80%的欺诈被捕获。

  • B 有 920 个误报,并且 80%的欺诈被捕获。

然后他在三个不同交易数量和欺诈案件数量的场景中尝试了他的两个模型。在场景 1 中,交易数量为 100,000。总体来说,有 100 起欺诈案件,因此类别平衡为 1:1,000:

fraud = 100 # high imbalance
genuine = 100000
model_A_FP = 20
model_B_FP = 920

# Model A
a_total_fraud_predicted = model_A_FP + fraud*0.8
a_correct_fraud_predicted = fraud*0.8

a_labels, a_preds = gen_labels_preds(fraud, genuine,
a_total_fraud_predicted, a_correct_fraud_predicted)
a_recall, a_precision, a_FPR = calculate_metrics(a_labels, a_preds)

# Model B

b_total_fraud_predicted = model_B_FP + fraud*0.8 
# Flags many more transactions
b_correct_fraud_predicted = fraud*0.8
b_labels, b_preds = gen_labels_preds(fraud, genuine,b_total_fraud_predicted, b_correct_fraud_predicted)
b_recall, b_precision, b_FPR = calculate_metrics(b_labels, b_preds)

print("Model A Performance Metrics:")
print('TPR:', a_recall)
print("Precision:", a_precision)
print("FPR:", a_FPR)

print("\nModel B Performance Metrics:")
print('TPR:', b_recall)
print("Precision:", b_precision)
print("FPR:", b_FPR)

Model A Performance Metrics:
TPR: 0.8
Precision: 0.8
FPR: 0.0002

Model B Performance Metrics:
TPR: 0.8
Precision: 0.08
FPR: 0.0092

在场景 2 中,他使用了与场景 1 相同的指标。交易数量为 100,000。总体来说,有 10 起欺诈案件,因此类别平衡为 1:10,000:

fraud = 10 # high imbalance
genuine = 100000

# Model A
a_total_fraud_predicted = model_A_FP + fraud*0.8
a_correct_fraud_predicted = fraud*0.8

a_labels, a_preds = gen_labels_preds(fraud, genuine, a_total_fraud_predicted, a_correct_fraud_predicted)
a_recall, a_precision, a_FPR = calculate_metrics(a_labels, a_preds)

# Model B
b_total_fraud_predicted = model_B_FP + fraud*0.8 # Flags many more transactions
b_correct_fraud_predicted = fraud*0.8

b_labels, b_preds = gen_labels_preds(fraud, genuine, b_total_fraud_predicted, b_correct_fraud_predicted)
b_recall, b_precision, b_FPR = calculate_metrics(b_labels, b_preds)
print("Model A Performance Metrics:")
print('TPR:', a_recall)
print("Precision:", a_precision)
print("FPR:", a_FPR)

print("\nModel B Performance Metrics:")
print('TPR:', b_recall)
print("Precision:", b_precision)
print("FPR:", b_FPR)

Model A Performance Metrics:
TPR: 0.8
Precision: 0.2857142857142857
FPR: 0.0002

Model B Performance Metrics:
TPR: 0.8
Precision: 0.008620689655172414
FPR: 0.0092

在场景 3 中,他又使用了相同的指标和 100,000 笔交易。总体来说,有 1,000 起欺诈案件,因此类别平衡为 1:100:

fraud = 1000 # high imbalance
genuine = 100000

# Model A
a_total_fraud_predicted = model_A_FP + fraud*0.8
a_correct_fraud_predicted = fraud*0.8

a_labels, a_preds = gen_labels_preds(fraud, genuine, a_total_fraud_predicted, a_correct_fraud_predicted)
a_recall, a_precision, a_FPR = calculate_metrics(a_labels, a_preds)

# Model B
b_total_fraud_predicted = model_B_FP + fraud*0.8 # Flags many more transactions
b_correct_fraud_predicted = fraud*0.8

b_labels, b_preds = gen_labels_preds(fraud, genuine, b_total_fraud_predicted, b_correct_fraud_predicted)
b_recall, b_precision, b_FPR = calculate_metrics(b_labels, b_preds)

print("Model A Performance Metrics:")
print('TPR:', a_recall)
print("Precision:", a_precision)
print("FPR:", a_FPR)
print("\nModel B Performance Metrics:")
print('TPR:', b_recall)
print("Precision:", b_precision)
print("FPR:", b_FPR)
Model A Performance Metrics:
TPR: 0.8
Precision: 0.975609756097561
FPR: 0.0002

Model B Performance Metrics:
TPR: 0.8
Precision: 0.46511627906976744
FPR: 0.0092

根据接收者操作特征曲线下面积(ROC AUC)和精确率-召回率 AUC(PR AUC)指标,模型 A 表现更好。模型 B 是一个糟糕的模型,但仍然得到了一个非常好的 FPR(0.0092),即使如果将其投入生产,预测结果将是垃圾(1000 个欺诈预测中有 920 个是错误的)。精确率使我们能够看到这一点。对于模型 B 来说,它只有 0.08,所以我们永远不会考虑将其接近生产环境。

这里的谬误是什么?

首先,模型 B 的 FPR 为 0.0092,是模型 A 的 FPR(0.0002)的 46 倍。没有好的或坏的 FPR。它取决于你的量级,即使是微小的差异也可能变得很大。例如,0.99 的案例比率比 0.999(1:100 与 1:1000)高 10 倍。

但即使在笔记本示例中,虽然精确率只有 10 倍糟糕,模型 B 的 FPR 却糟糕了 46 倍;这很难称得上是一个非常好的 FPR。

如您从前面的计算和笔记本中看到的那样,当类别平衡发生变化时,精确率会显示一个非常不同的数字,即使模型的性能保持不变。相比之下,TPR 和 FPR 保持不变。

我们如何结合这些信息并将其应用于选择合适的指标?

在我们服务的一家公司中,我们的目标是每天处理超过 1000 亿个事件以减少垃圾邮件和欺诈行为。我们将特异性设置为至少 0.999999(特异性 = TNR = 1 – FPR,换句话说,我们对于每 100 万个事件中有一个误报是可以接受的)并在此特异性率下最大化召回率(TPR)。这证明比使用标准的召回率-精确率对更有益,考虑到底层数据的波动性。

然而,有些情况迫使你必须即兴发挥,以找到能够从你的系统中获得所需行为模式的指标。

阿尔谢尼的篝火故事

我曾为一家制造优化公司工作,需要改进其检测系统中的缺陷,但在过程中,另一个问题出现了:指标不够敏感。运行计划场景所需的数据集太小——每个客户产品只有 10 到 20 个有缺陷的样本。而且我们无法获取更多数据,因为根本不存在更多的现有有缺陷单元。缺陷率太低,多亏了高工程品质。

除了数据集大小外,我们的客户对中间结果不感兴趣(例如,我们的模型中缺陷概率的校准程度如何)。他们的判断非常直接。为了简单起见,让我这样描述:

  • 有 10 个有缺陷的单元和 N 个常规单元。

  • 理想的情况是没有任何错误。

  • 1 个误报或 1 个漏报已经足够好。

  • 否则,系统将无法使用。

我尝试改进现有系统的多数努力都徒劳无功,直到某个时刻我决定设计一个定制的连续指标,该指标利用了内部指标并设置了合理的阈值。这个指标看起来非常离散:

  • “0”意味着“完美的系统”。

  • “1”代表“足够好”。

  • “2”代表“垃圾”。

在这个指标到位后,我能够逐步、逐步地改进系统,同时有信心我在正确的方向上前进。

经过一系列的小幅改进,累积效应将系统从“垃圾”转变为“足够好”,再从“足够好”转变为对多个客户来说的“完美”。

在你的机器学习系统成功中,一个重要因素始终是其一致性。为了实现这一点,存在一个单独的指标类别,我们将在下一节中介绍。

5.2.1 一致性指标

在应用 ML 中,当模型面对略微干扰的输入时,通常希望模型有一致的输出。这个属性在不同的子领域中被称为一致性、鲁棒性、稳定性或平滑性,可以形式化地定义为模型在特定变换下不变的要求,即模型在原始输入和干扰输入上的输出差异趋向于零。换句话说,我们可以将这个属性数学上表达为

figure

其中 f 代表模型,x 代表原始输入,而 eps 代表应用于输入的干扰。一致性指标在学术 ML 中通常不常讨论,但在实际应用中,输入的微小变化可能会对模型输出产生重大影响,因此这是一个重要的考虑因素。

干扰可能不同。例如,对于一个固态计算机视觉模型,轻微的光照变化通常不应该改变模型输出,或者情感分析模型不应该对同义词的变化敏感。我们将在第十章讨论 ML 系统测试时,更详细地讨论这类干扰和不变性。

另有一个类似的属性:当模型重新训练(例如,通过添加新数据或使用其他种子)时,我们期望它在输入保持不变的情况下产生相同或接近的输出。对于一个反欺诈系统,如果同一个用户今天被认为是欺诈者,明天是合法用户,下周又再次被认为是欺诈者,这是不可接受的:

figure

当模型输出随时间变化时,新模型的发布(对于大多数 ML 系统来说应该是一个常规程序)可能会影响下游系统或系统的最终用户,扰乱他们的常见使用场景。人们很少喜欢工具和环境中的意外变化。

这些属性可能和我们对模型期望的默认特征(如准确的预测)一样重要,因为它们塑造了期望。正如我们在前面的章节中讨论的,如果一个模型不可信,它的效用就会降低。因此,我们需要特定的指标来衡量这种行为。

幸运的是,我们把这些属性严格地表述出来,所以剩下的最大未解问题是估计前述公式中适当的噪声或干扰类型:什么是不变性,以及预期的条件如何随时间变化?

在这些估计到位后,你可以将常规度量标准附加到估计一致性上。例如,对于搜索引擎示例(Photostock Inc.),我们不希望文档在系统发布之间因某些查询而改变其排名,因此一致性度量标准可以是(查询,文档)对在一段时间内文档和查询语料库中的排名方差。显然,方差越小,对系统越好。然而,你也不能忘记处理不明确的情况——比如说,一个虚拟常数模型往往提供最低的方差,但这并不是机器学习工程师通常追求的一致性。

一致性通常是机器学习系统的一个重要属性(见图 5.5)。如果你的系统是这样,考虑添加一个度量标准,反映你的系统如何对输入数据、训练数据或训练过程调整的变化做出响应。

figure

图 5.5 新模型发布在估计用户(U)欺诈(F)的概率(P)时相当一致。

最终,你将能够基于清晰的离线和在线度量标准层次结构形成一个单一的度量系统。

5.2.2 离线和在线度量标准、代理度量标准和度量标准层次结构

设置和改进适当的度量标准是构建高效机器学习系统的重要步骤。但即使这样,也不是我们的最终目标,因为我们还需要深入一层。当我们有计划减少垃圾邮件和欺诈行为时,目标并不是在给定的特异性下达到最高的召回率。而是通过减少垃圾邮件的数量,降低欺诈行为的风险,从而改善用户体验。

在 Supermegaretail 案例中,目标是减少因缺货和过剩情况导致的损失,这可以用现金等价物来表示,但不能用绝对误差(MAE)、均方误差(MSE)、加权平均绝对百分比误差(wMAPE)、加权绝对百分比误差(WAPE)或其他任何度量标准来表示。

换句话说,我们在训练/测试/验证阶段以及最终评估模型时使用的度量标准通常并不相同(见表 5.1)。

之前讨论的集合也被称为离线度量标准,因为我们可以在不将模型部署到生产环境中时应用和计算它们。相比之下,一些度量标准,通常是我们的目标度量标准,只能在实施系统并使用其业务输出后才能计算。尽管有时离线和在线度量标准可能一致,但我们仍然必须分别评估它们。评估在线度量标准(变化/改进)的最常见方式是通过 A/B 测试。

我们使用离线指标的原因很简单:我们可以在部署系统之前使用它们。这种方法快速且可重复,而且不需要昂贵的模型部署过程。离线指标必须具备一个特点:它们必须是对在线指标的良好预测器。换句话说,离线指标的增加或减少必须与在线指标的增加/减少有强烈的关联性或成比例。离线指标充当在线指标的代理指标,可以用作在线指标的效率预测器。

表 5.1 离线和在线指标示例
离线指标 在线指标
对于垃圾邮件消息分类的给定特定性召回率 用户对垃圾邮件消息的投诉数量
1.5、25、50、75、95 和 99 的百分位数 过期商品的价值,总销售额
均值倒数排名,归一化折现累积增益 搜索引擎结果页面的点击率

但如果我们能找到与我们的在线指标有强烈关联且改进具有传递性的离线指标,我们也可以对离线指标做同样的事情。让我们用一个例子来回顾这一点。

想象一下,我们正在为一家电子商务网站构建一个推荐系统。我们的最终目标是增加总商品交易额(GMV;这是一个衡量给定期间内销售总价值的指标)。不幸的是,正如之前提到的,这并不是我们可以在将系统部署到生产环境中并运行 A/B 测试之前测量的东西。我们相信,增加购买商品的数量将增加 GMV。为了实现这一点,我们希望通过提供有更高购买可能性的优惠来提高转化率(假设这将增加购买商品的总数量)。

平均而言,3%的优惠被点击,其中 3%导致购买:3%乘以 3%意味着如果我们展示 10,000 个优惠,只有 9 个会导致购买。这有两个相互关联的负面影响:

  • 类 1 数据(购买)数量少,类别不平衡严重

  • 增加 A/B 测试的持续时间

例如,对于成功率与尝试次数比为 9/10,000 的 A/B 测试,我们需要比 90/10,000 的比率多 100 倍的数据(最小可检测效应与样本数量之间的二次依赖性;请参阅以下示例)。

为了减轻这种情况,我们可以使用一个代理指标,点击率(CTR),同时考虑到以下背景:

  • 没有点击就无法进行购买。我们可以预期 CTR(点击率)与 CR(转化率)之间存在正相关,甚至可以计算出它。

  • 点击次数比购买次数多 33.3 倍,这意味着我们将有 33.3 倍的训练数据用于系统的第 1 类,A/B 测试将快 1,111(33.3²)倍。(为了精确起见,我们可以预期方差也会发生变化,如equation image,所以根据equation image,var = 0.000899,根据equation image,var = 0.0291,这意味着总体上我们将通过equation image倍增加收敛速度。)

使用 CTR 而不是 CR 可以帮助我们更快、更敏感地迭代,无论是在离线(对于感兴趣类别的数据,估计指标和损失更容易)还是在在线(至少部分通过 A/B 测试)。

我们可以用以下关系表示:

  • CTR → CR →(购买物品的总数)→ GMV

我们可以通过构建一组指标层次结构进一步概括:

  1. 全局、公司范围内的指标是收入。

  2. 全球收入(GMV)由不同产品的收入组成,包括我们负责的产品。

  3. 我们的产品收入受以下因素影响

    • 平均购买价格

    • 购买频率

    • 用户数量(它们相互关联并相互影响,因此用虚线表示)

  4. 购买频率受 CR 影响。

  5. 转化率受 CTR 影响。

一组指标层次结构(见图 5.6)有助于找到合适的代理指标。尽管创建它超出了设计机器学习系统的范围,但在设计过程中拥有一个现成的指标体系并参考它将非常方便。使用共同的基础有助于证明选择并降低失败的风险。

figure

图 5.6 指标层次结构

当系统足够成熟,以至于某些指标可能相互矛盾时,一组指标层次结构尤为重要。我们的一位朋友曾经告诉我们一个关于构建推荐系统的简短轶事:一个内部用户参与度更高的变体(他们更喜欢新推荐而不是旧版本)在更广泛的受众中似乎利润较低。

一组指标和代理指标的概念与我们之前讨论的多组件损失相关。例如,当为 Supermegaretail 构建这个推荐引擎时,我们可以定制一个特定的损失函数,该函数将考虑多个级别的用户活动(点击、购买、购买物品的总数)并在指标之间平衡我们的利益。

来自阿列克谢的篝火故事

之前,我曾基于计算机视觉开发了一个全新的产品功能。提出的解决方案被分解为组件,每个组件和子组件都仔细标注了指标。由于该功能的创新性,指标是定制的——主要是各种可能结果之间的比率。我们与产品高管合作设计了指标层次结构。在针对一个指标进行多次实验以推动其进展后,我产生了直觉,认为它是不平衡的。为了测试这一点,我通过用具有特定参数生成的随机噪声替换模型预测来运行了一个对抗性实验。令人惊讶的是,随机模型得分完美!该指标最初是为了在召回率和精确率之间进行权衡而设计的,但如此极端的不平衡显然是不理想的,因此我们必须尽快重新设计它。

5.3 设计文档:添加损失和指标

从第四章开始,我们开始介绍两个虚构案例的设计文档:Supermegaretail 和 PhotoStock Inc. 在这里,我们继续详细阐述每个案例的机器学习解决方案的开发,并涵盖损失函数和损失的选择。我们首先介绍 Supermegaretail,然后是 PhotoStock Inc.

5.3.1 Supermegaretail 的指标和损失函数

让我们重温一下 Supermegaretail 案例。在那里,我们的目标是减少交付和销售物品之间的差距,使其尽可能小,同时避免因特定的服务等级协议(SLA)而出现缺货情况。

设计文档:Supermegaretail

II. 指标和损失

i. 指标

在我们自行选择指标之前,进行一些初步研究是有意义的。幸运的是,有许多与这个问题相关的论文,但最突出的是 Stephen Kolassa 的“在零售销售预测中评估预测计数数据分布”(mng.bz/eVl9)。

让我们回顾一下项目目标,即减少交付和销售物品之间的差距,使其尽可能小,同时避免因特定服务等级协议(SLA)而出现缺货情况。为此,我们计划使用机器学习系统来预测特定商店在特定期间内对特定物品的需求。

在这个案例中,这篇论文的摘要看起来几乎完美匹配:

计算能力的巨大提升和新数据库架构使得数据可以以越来越细的粒度存储和处理,从而产生了计数数据时间序列,其计数越来越低。这些序列不能再使用适用于连续概率分布的近似方法来处理。此外,仅仅计算点预测是不够的:我们需要预测整个(离散)预测分布,特别是对于供应链预测和库存控制,但也包括其他规划过程。

(计数数据是整数值的时间序列。对于我们所面临的供应链预测来说,这是至关重要的,因为大多数产品都是以单位销售的。)考虑到这一点,我们可以简要回顾这篇论文(以下字母列表中),并挑选出最适合我们最终目标的指标。

A. 基于绝对误差的度量

MAE 优化了中位数;加权平均绝对百分比误差(wMAPE)是 MAE 除以样本外实现值的平均值,通过将 MAE 除以随机游走预测的样本内 MAE,可以得到平均绝对缩放误差。

在对称预测分布中,优化中位数与优化均值没有太大区别。然而,适用于低量计数数据的预测分布通常远非对称,这种区别在这种情况下会产生差异,并导致有偏预测。

B. 百分比误差

如果任何未来的实现值为零,则平均绝对百分比误差(MAPE)未定义,因此它对于计数数据来说特别不合适。

对称的 MAPE 是对 MAPE 的“对称化”版本,如果点预测和实际值在所有未来时间点都不是零,则定义。然而,在任何一个实际值为零的时期,无论点预测如何,其贡献都是 2,这使得它对于计数数据来说不合适。

C. 基于平方误差的度量

最小化平方误差自然会引导到无偏的点预测。然而,由于对非常高的预测误差敏感,MSE(均方误差)不适用于间歇性需求项目。同样的论点也适用于非间歇性计数数据。

D. 相对误差

显著的变化是中位数相对绝对误差和几何平均相对绝对误差。

在预测计数数据的特定背景下,这些度量存在两个主要弱点:

  • 相对误差通常与绝对误差进行比较。因此,它们与基于 MAE(平均绝对误差)的误差一样,受到相同的批评,如前所述。

  • 在每个时期的基础上,简单的基准,如简单的随机游走,可能没有错误地预测,因此,这个时期的相对误差由于除以零而未定义。

E. 基于率的误差

Kourentzes(2014)最近提出了两种新的间歇性需求误差度量:MSR 和 MAR,旨在评估间歇性需求点预测是否在增加的时间段内正确地捕捉了平均需求。这是一个有趣的建议,但这些度量中的一个特性是它们隐含地更重视短期未来而不是中到长期未来。有人可能会说,这正是我们在预测时想要做的,但即使如此,也可以提出这样的加权应该明确——通过在平均未来时间期间使用适当的加权方案来实现。

F. 缩放误差

Petropoulos 和 Kourentzes(2015)建议使用 MSE 的缩放版本,即 sMSE,它是通过预测期内的实际值的平方平均值缩放的平方误差的平均值。sMSE 除非所有实际值都是零,否则是良好定义的,通过f的期望最小化,并且由于缩放,可以比较不同时间序列。此外(同样由于缩放),它对高预测误差的敏感性不如 MSE。具体来说,它对剧烈的低预测更为稳健,尽管它对高预测误差仍然敏感。

G. 功能和损失函数

另一种看待预测的方法是集中在预测分布的功能性点预测。可以争论说,零售商的目标是达到一定的服务水平(比如说 95%),因此他们只对预测分布的相应分位数感兴趣。这可以通过适当的损失函数或评分规则来得出。这种方法与将预测视为库存控制系统一部分的想法密切相关。从这个角度来看,分位数预测被用作标准库存控制策略的输入,预测的质量通过评估随时间变化的库存总价值并权衡缺货来评估。

尽管作者们认为这不是最佳解决方案并提出了一个替代方案,但论文的最后一段相当有前景。从商业角度来看,预测不同的分位数以维持服务水平协议(SLA)是有意义的,而且从损失函数等于指标的角度来看也是可取的。因此,对于 1.5、25、50、75、95 和 99 的分位数,分位数指标看起来是一个合适的选择。此外,如果我们需要更多地关注特定的 SKU、商品组或聚类。在这种情况下,分位数指标支持计算对象/组权重(例如,商品价格)。

i.ii. 选择指标

对于 1.5、25、50、75、95 和 99 的分位数,以及与 SKU 价格相等的权重和必要的额外惩罚(如果认为有必要)的分位数指标,都计算为点估计值,并带有 95%的置信区间(使用自助法或交叉验证)。此外,我们可以进一步转换这个指标,将其表示为给定分位数的绝对百分比误差。必须考虑 Petropoulos 和 Kourentzes 文章中关于百分比误差的所有考虑。最终,一系列实验将有助于决定最终形式。我们可能两者都会使用,因为检查绝对值(金钱/件数)和百分比误差都是有意义的。

在 A/B 测试期间感兴趣的在线指标是

  • 收入——预期会增加

  • 库存水平——预期会下降或保持不变

  • 利润率——预期会增加

figure

  • Alpha——用于基于分位数的损失系数

  • W—权重

  • I—指示函数

  • A—模型输出

  • T—标签

ii. 损失函数

当指标等于我们的损失函数时,选择后者是直接的。我们将使用 1.5、25、50、75、95 和 99 的量级损失来训练六个模型,从而产生六个不同的模型,为我们提供对预测分布相应量级的各种保证。

作为实验的第二阶段,我们还将审查 Tweedie 损失函数。Tweedie 分布是一族概率分布,包括纯连续的正态分布、伽马分布和逆高斯分布;纯离散的缩放泊松分布;以及具有零点正质量的复合泊松-伽马分布,其他方面都是连续的。这些特性使其成为我们计数数据的理想候选者。

5.3.2 PhotoStock Inc.的指标和损失函数

接下来是 PhotoStock Inc.的设计文档,其中应根据业务案例的性质和要解决的问题应用一套完全不同的损失和指标。在 PhotoStock Inc.的情况下,我们被雇佣来构建一个现代搜索工具,该工具可以根据客户的文本查询找到最相关的照片,同时提供卓越的性能并显示最相关的库存图片。

设计文档:PhotoStock Inc.

II. 指标和损失函数

i. 指标

在选择新的 PhotoStock 搜索引擎的指标时,我们应该记住系统的预期行为,包括以下内容:

  • 用户点击搜索结果中的链接,结果越靠前,点击越多。这种行为可以通过 CTR 指标反映出来,该指标评估了多少用户点击了搜索结果。

  • 用户通过搜索购买图片。这种行为可以通过 CR 指标反映出来,该指标评估了多少点击导致了购买。

  • 用户在搜索引擎结果页面(SERP)上看到各种建议。这里没有现成的解决方案,因为我们没有对多样性有一个明确定义。让我们稍后与用户体验团队讨论这个问题。作为一个基线,我们可以使用在 SERP 上表示的不同图像类别的数量作为多样性的衡量标准。在未来,我们应该研究其他公司的经验——Airbnb 的论文“在 Airbnb 学习如何进行多样性排序”(arxiv.org/abs/2210.07774)。

  • 从人类的角度来看,搜索结果看起来是合理的。这种行为可以通过人类评估指标反映出来,该指标显示有多少用户认为搜索结果是合理的。

点击率(CTR)和转化率(CR)是在线指标,这意味着它们只能在系统运行时进行测量。多样性是一个无监督的离线指标,这意味着它不需要任何额外的数据,并且可以定期免费测量。另一方面,人工评估是一个监督的离线指标,这意味着它需要额外的数据(人工评估),因此收集数据需要时间和精力。

为了引入 CTR 和 CR 的离线代理指标,我们可以使用经典的排序问题指标,如平均倒数排名(MRR)和归一化折现累积增益(NDCG)。MRR 是一个计算给定结果集的倒数排名平均值的指标,它是第一个相关结果的排名倒数平均值的一个度量。NDCG 是一个计算给定结果集的折现累积增益(DCG)平均值的指标,它是从第一个 N 个结果中取出的相关性分数的总和除以理想的 DCG。而 DCG 是按照相关性递减的顺序在前 N 个结果中的相关性分数的总和。

MRR 和 NDCG 都需要为每个查询提供一个相关结果列表来计算指标。我们可以为 MRR 和 NDCG 使用相同的列表,但我们需要通过众包来创建这个列表,以确保它能够代表用户可能看到的结果。虽然 MRR 可能适合作为 CTR 的离线指标,但它可能不是 CR 的良好代理,因为众包的相关结果列表不能代表真实的购买数据。因此,为了准确测量 CR,我们应该考虑使用真实的购买数据。然而,对于系统的第一个版本,我们可能只能通过 A/B 测试和逐步推出在线监控 CR。

总结一下,以下是我们可以如何划分指标的方法:

  • 快速离线指标:MRR、NDCG、多样性

  • 慢速离线指标:人工评估

  • 在线指标:点击率(CTR)、转化率(CR)

ii. 损失

要使用损失函数来训练搜索引擎,重要的是要考虑可用的数据和期望的结果。在这种情况下,我们希望优化的三个主要方面是点击、购买和多样性。

对于点击和购买方面,我们可以使用二元交叉熵损失作为成功度的衡量标准。然而,需要注意的是,点击和购买的数据可能存在不平衡,这意味着某一类别的示例可能比另一类多。在这种情况下,使用对类别不平衡更鲁棒的损失函数可能更有益,例如焦点损失或其他为此目的设计的损失函数。

焦点损失(Focal Loss)是在论文“Focal Loss for Dense Object Detection”(arxiv.org/abs/1708.02002v2)中提出的一种损失函数。它是分类任务中常用二元交叉熵损失函数的推广。焦点损失与二元交叉熵损失的关键区别在于,焦点损失降低了容易样本的权重,这些样本被以高置信度正确分类。这在数据不平衡的情况下很有用,因为它有助于模型关注困难样本,这些样本通常对提高模型的整体性能更为重要,因此对于 PhotoStock 搜索引擎来说似乎相关。

关于多样性方面,我们可以在损失函数中添加一个惩罚结果相似性的项。一种潜在的方法是使用结果类别分布的熵作为多样性的度量。然而,这种方法可能并不总是可行,因此多样性损失应被视为可选的。

总体而言,最终的损失函数可以写成

figure

在这里,alpha、beta 和 gamma 被表示为超参数,它们控制着三个组件的相对重要性。这些超参数可以被调整以找到三个方面的最佳平衡。

5.3.3 总结

这两个设计文档中的示例展示了选择正确的指标和损失函数有多么重要。就像构建 ML 系统中的任何其他关键元素一样,指标和损失函数应与你的项目目标相一致。如果你觉得需要更多时间来定义适当的参数,请在你的日程中安排几天时间来做这件事,这样你就不必在一个月或更长时间内退回几步。

下一章涵盖了数据收集、数据集、数据与元数据之间的区别,以及如何实现健康的数据管道。

摘要

  • 不要因为它们在之前的(项目)中有效而陷入使用经过时间考验的损失函数的诱惑。

  • 损失函数必须全局连续且可导。

  • 损失函数的选择是一个重要的步骤,但在基于深度学习的系统中,它甚至更为关键。

  • 考虑在输入的小变化可能对模型输出产生重大影响时应用一致性指标,从产品角度来看。

  • 在将项目投入生产之前,可以应用离线指标,并充当在线指标的代理指标。

  • 确保手头有指标层次结构,因为它在系统设计过程中将非常有用。

第六章:6 收集数据集

本章涵盖了

  • 数据来源

  • 将原始数据转换为数据集

  • 区分数据和元数据

  • 定义多少才算足够

  • 解决冷启动问题

  • 寻找健康数据管道的特性

在前面的章节中,我们已经涵盖了构建机器学习(ML)系统准备中的固有步骤,包括问题空间和解决方案空间,识别风险,以及找到合适的损失函数和度量标准。现在我们将讨论你的机器学习项目没有它就无法起飞的一个方面——数据集。我们将把它们与我们生活中的重要元素进行比较。就像你需要燃料来启动你的汽车或一顿营养早餐来在忙碌的工作日之前充电一样,机器学习系统需要一个数据集才能正常工作。

有一个关于房地产的古老流行谚语:最重要的是位置,位置,还是位置。同样,如果我们只能选择三个在构建机器学习系统时需要关注的事情,那么这些将是数据,数据,还是数据。计算机科学世界中的另一个经典谚语说“垃圾进,垃圾出”,我们无法怀疑其正确性。

在这里,我们将从寻找和处理数据源到正确处理你的数据集和构建数据管道的工作本质进行分解。作为本章的总结,我们将使用 Supermegaretail 和 PhotoStock Inc.的例子,将数据集视为设计文档的一部分。

6.1 数据来源

你可以使用任何来源来寻找数据集的数据。这些来源的可用性和质量将取决于你的工作环境,你公司的传统,以及许多其他因素。首先应该解决什么问题,主要取决于你的机器学习系统的目标。在这里,我们列出了最流行的数据来源或其类别,同时附上现实世界的例子:

  • 全球活动**—这是一个庞大的数据来源类别,包括任何单一实体中定期记录并持续存储的活动。例如,在股票交易业务中,全球的贸易商在股票市场上采取行动,他们的行动结果(交易和价格)后来对其他各方可用。

  • 物理过程**—这些是我们正在谈论的全球变化,发生在我们星球上。这些变化可以在各种层面上进行监测,从卫星图像到农田上的微传感器。

  • 外部数据库**—某些第三方公司通过他们专有的方法和知识收集特定领域的数据而蓬勃发展。如果你需要的数据符合你的需求和标准,你将想要与他们打交道。

  • 本地业务流程**—这里我们从全球转向本地。业务本身在运营和成长过程中可以产生大量数据。如果你从事电子商务,购买历史可以成为你的主要数据来源。

  • 由专业团队标注**——您的公司可以雇佣一支专家团队为特定问题生成标签。

  • 由最终系统用户标注**——这是一种类似的方法,其中公司可能为最终用户提供一个用户界面。在那里,他们将指定您机器学习系统的输入。

  • 人工生成数据集——这是数据由科学模拟器、渲染环境或其他合成来源创建的地方。使用生成式 AI(例如,图像生成器或大型语言模型)创建的项目也可以归入这一类别。

一些数据源是独特的,能够访问它们可能是一个重要的竞争优势。许多大型科技公司,如谷歌和 Meta,之所以成功,主要是因为它们用于广告定位的有价值用户行为数据。另一方面,其他数据集很容易获取;信息要么可以免费下载,要么可以以非限制性价格(许多数据提供商销售的数据集相对便宜)创建。但这并不意味着便宜等于低质量,因为这完全取决于你需要什么类型的数据。可能结果是这个免费来源恰好完美地符合你的机器学习系统。然而,在这个光谱上也有中间点,但这并不是指价格。在某些地区,数据访问可能受到限制,或者从法律角度来看处于“灰色地带”。成熟的公司倾向于遵守法律(我们也建议您这样做!),而拥有 YOLO 心态的年轻初创公司有时会考虑轻微的违规行为。

一些数据集在丰富或标注/标记后变得有价值。标注意味着将原始数据集与适当的标签相结合,或者换句话说,创建一个紧密相关的数据集并将其与一个新的数据集连接起来。这是一种将独特的专有数据集与公共数据源混合,从而得到更有价值的数据集的流行模式。

之前提到的广告技术公司也可以从加入数据集中受益。让我们看看一个经典的例子。一家公司运营一款免费游玩的游戏,这意味着它拥有大量玩家,但只有其中一部分玩家付费。付费客户的人数和名单是保密的,只有游戏的出版商可以访问。同时,它的合作伙伴广告网络拥有数百万个详细用户档案,这些档案来自行为数据。当这些数据集结合在一起(见图 6.1)时,它开辟了一个巨大的营销机会:公司可以将新的广告定位到与其付费玩家相似的可能客户。这种类型的数据交换提高了在线营销的效率,因此是推动现代网络发展的力量之一。

figure

图 6.1 将两个完全不同业务的数据源结合起来,最终可能对双方都有益。

当我们谈论合并数据集时,并不总是像在两个表之间进行 SQL 连接那样简单地将相似数据集连接起来。这里的一个重要概念是多模态,它是一个数据集中各种模态的交集。简单来说,模态是我们接收到的信息的一种类型;对于人类来说,通常将世界描述为多模态(我们听到声音,看到颜色,感受到运动等)。在机器学习相关的文献中,多模态数据集是指结合各种类型的数据来描述一个问题的数据集。想想看,一个正在出售的商品的图片和它的文字描述。将不同来源和模态的数据集结合起来是一种强大的技术。

谈到合并数据源,阿森尼曾经作为顾问帮助启动了一家初创公司。该公司在农业技术领域工作,帮助农民和相关公司提高运营效率,其秘密配方基于数据集。其工作方式如下。一个数据源是公开的,使用 NASA 的 Landsat 和 ESA 的 Copernicus 等几个太空倡议提供的地球卫星图像(你可以获取无数农业用地的图像)。但仅仅拥有这些图像并不能提高初创公司的效率,因为它缺乏描述这些农业用地的信息。主要问题是大多数农业公司缺乏创新,没有关于种植了哪些作物、产量结果等单一可靠的数据来源。这类数据数字化程度较低,但对于多种商业需求来说确实非常有价值:它可以用来减少使用的肥料量,估计食品商品的未来价格,等等。最终,团队实施了智能的方法来收集此类数据并将其与庞大的照片数据库合并。建立在这些联合数据集之上的机器学习系统帮助公司迅速发展。

定义数据源及其相互连接的方式是解决系统数据问题的第一个基石。但原始数据通常几乎毫无用处,直到我们使其对系统和机器学习模型可用,对其进行过滤,并以其他方式进行预处理。

6.2 数据集的加工

有经验的工程师知道,在绝大多数情况下,原始数据过于原始,无法有效地进行处理。因此,我们称之为“原始数据”,意味着这是一个混乱编译、无组织的巨大信息块。因此,最初的原始数据集很少处于足够好的状态,可以直接使用。你需要对数据集进行加工,以便以最有效的方式将其应用于你的机器学习系统。

我们已经收集了一份您可以用来正确处理数据集的技术列表,每个技术都在一个单独的小节中介绍。这个列表并不是严格有序的,而且根据您的领域,操作顺序可能会有所不同,因此没有唯一的通用答案。在某些情况下,您可能需要在标记之前过滤数据,而在某些情况下,过滤可能会在整个处理过程中多次发生。让我们简要地探讨这些技术。

6.2.1 ETL

ETL,即“提取、转换、加载”,是一个数据准备阶段,例如从外部数据源获取信息并将其结构调整为满足您的需求。作为一个简化的例子,您可以从第三方 API 获取 JSON 文件,并将它们存储为 CSV 或 Parquet 文件到本地存储。

注意:ETL 的高级目标是解决数据可用性问题。

在这个阶段,数据可用性意味着两件事:

  • 数据可以轻松有效地用于训练过程(例如,如果目标数据集是来自多个来源的多次交互的结果,了解如何通过单次点击、命令或调用使其可获取是有用的)。

  • 数据将在训练阶段和运行时阶段可用。我们目前不关心获取数据是否对推理有效(我们将在第十一章中讨论这个问题),但我们需要保证推理时可以使用相同的数据源。

注意:设计一个有效的 ETL 流程是一门艺术,因为它需要很好地理解各种数据存储和数据处理工具。我们在本书中仅触及了这个话题的皮毛,并建议您查阅其他资料以获得更深入的了解。

这里的重要问题是:“我是否应该在这个阶段关心数据存储和结构?” 这个问题的答案在于以下范围:

  • 有时数据集足够小,突然增长多个数量级的可能性极小。这意味着您可以选择几乎任何存储方式(例如,已经在您的组织中积极使用的存储方式,或者您最熟悉的存储方式)。您可能会惊讶地发现,不经验证的 ML 工程师往往倾向于过度设计,例如为具有数千行和数十列的静态表格数据集设计分布式多集群存储。

  • 有时从一开始就很明显,您的数据集将会非常大并且会迅速增长,因此在设计数据模型时您应该特别关注。我们不认为自己是这种类型的数据工程的世界级专家,如果您的情况是这样,我们建议您查阅其他书籍。我们最喜欢的关于这个主题的作品是马丁·克莱普曼的《设计数据密集型应用》。

6.2.2 过滤

没有数据源是绝对完美的。当我们说完美时,意味着数据是干净的、一致的,并且与你的问题相关。在本章开头的故事中,我们提到了提供卫星图像的 API,但公司只需要那些不太多云且与农业区域相关的图像;否则,存储无关数据将显著增加成本。这意味着选择合适的卫星照片的大量初步工作必须作为第一步完成。

数据过滤是一个非常特定领域的操作。在某些情况下,它可以完全基于一组规则或统计自动完成;在其他情况下,它需要大规模的人类关注。经验表明,最终的方法通常位于这两个极端之间。人眼和自动化启发式方法的结合是最佳选择,以下算法是一个流行的方法:检查数据的一个子集(可以是随机选择,也可以基于一些初始洞察或反馈),寻找模式,在代码中反映这些模式以扩展覆盖范围,然后检查更窄的子集。

虽然缺乏数据过滤会导致数据集中出现大量噪声,从而降低整个系统的性能,但过于激进的过滤也可能产生负面影响:在某些情况下,它可能会扭曲数据分布,导致在真实数据上的性能更差。

6.2.3 特征工程

特征工程意味着以某种方式转换数据视图,使其对机器学习算法最有价值。我们将在本书的后续章节中更详细地介绍这个主题,当讨论到中间步骤时,因为在机器学习系统设计的早期阶段,它很少被详细讨论。在这个阶段,我们倾向于关注如何获取初始数据以构建基线模型的问题,这需要当前阶段一定程度的抽象。

有时特征不是通过手动工程创建的,而是由一个更复杂的模型创建的;与“常规”特征不同,它们可以是不可读的向量。在这些情况下,使用术语表示更准确,尽管在某种抽象层面上,它们是同一件事:机器学习模型本身的输入。

例如,在一个以机器学习为核心的大组织中,可能有一个核心团队构建一个模型,该模型可以大规模地生成用户、商品或其他项目的最佳表示。他们的模型并不直接解决业务问题,但应用团队可以使用它来生成满足他们特定需求的表示。当我们谈论图像、视频、文本或音频等数据时,这是一个流行的模式。

6.2.4 标注

在许多情况下,数据集本身并不太有价值,但添加额外的标注,在机器学习世界中通常被称为标签,却是一个转折点。决定使用哪种类型的标签非常重要,因为它决定了后续许多其他选择。

让我们假设你正在构建一个医疗辅助产品,一个帮助放射科医生分析患者图像的系统。这是医学中非常复杂但仍然非常受欢迎的机器学习应用之一。一个用例可能看起来很简单:医生查看图像并判断是否存在恶性,因此你希望医生对数据集进行标注。

有许多方法可以对其进行标注(见图 6.2),包括

  • 二分类风格——图像中是否存在恶性?

  • 多类分类风格——图像中是否存在某种恶性,如果有?

  • 检测风格——如果存在,图像中包含恶性的区域是什么?

  • 分割风格——如果存在,图像中哪些像素代表恶性?

图像

图 6.2 数据标注的各种方法

这些数据标注方法需要不同的标注工具和来自标注团队的不同专业知识。它们在所需时间上也有所不同。方法会影响你可以为问题使用的模型类型,最重要的是,它限制了产品。除非你遵循第二章的步骤并收集足够的产品目标和需求信息,否则在这种情况下做出正确的决定是不可能的。

在某些情况下,使用适当的详细程度标注数据几乎是不可能的。例如,可能需要太多时间,可能没有足够的专家来覆盖足够的数据,或者工具集尚未准备好。在这些情况下,考虑使用弱监督方法是有价值的,这些方法允许使用不准确或部分标签。如果你不熟悉这个机器学习技巧分支,我们推荐阅读“弱监督学习简介”(mng.bz/75My)以获得概述;更多阅读链接可在 Awesome-Weak-Supervision (mng.bz/mRp2)找到。

高效的数据标注需要选择一个标注平台,正确分解任务,编写易于阅读和遵循的任务说明,易于使用的任务界面,质量控制技术,汇总方法的概述,以及定价。此外,在设计说明和界面、设置不同类型的模板、培训和评估执行者以及构建评估标注流程的管道时,应遵循最佳实践。

当需要手动标注数据集时,主要有两种选择:内部标注团队或第三方众包服务。简而言之,前者提供了一种更加控制和一致的标注流程,而后者可以快速扩展标注过程,可能成本更低。选择其中一种方式而放弃另一种的最简单的方法是基于标注的复杂性:一旦需要特定的知识或技能,组建一个内部的专业团队是有意义的;否则,使用外部服务是一个不错的选择。

可用的众包平台有数十个;最流行的大概是亚马逊机械师。由于我们在本书中更注重广度而非深度,我们将不会关注不同平台的功能。相反,我们关注众包标注的通用属性:

  • 标注者往往即使在最简单的任务中也容易出错,例如对常见物体的二分类。平台上的大多数标注者为多个客户工作,因此他们很少有机会记住您提供的标注说明的细微差别。因此,应将标注者视为可互换的,并且说明应尽可能简单和非歧义。

  • 一些标注者比其他标注者更细心,您可能希望激励他们更多地投入到您的任务中,而不是其他零工。有些标注者可能不够细心或具有对抗性;例如,他们可能会尝试使用生成未经验证标签的机器人,而不是依赖自己的判断。能够区分这两者至关重要。最流行的方法是在标注过程中包含测试。选择一个您对标签有信心的数据集的一部分,并用于标注任务,这样您就可以衡量每个特定标注者的准确性和其他指标。您还可以记录标注过程的某些细节,以获得更清晰的了解。如果标签生成得非常快,这可能是一个机器人或其他非法自动化工具的强烈信号。

  • 标注任务应设计成减少众包工作者引起的方差。考虑以下场景:一家公司正在开发聊天机器人,并雇佣标注者来评估响应(问题和几个可能的答案)。这里最直接的评价方法就是要求标注者用一个数字(例如,1 到 5)对每个答案进行评分。然而,这样的标签可能不一致。在这种情况下,一个基本的改进就是引入多个评价标准,以便根据事实正确性、语气等因素单独评估每个答案。这些标签将更加一致,但仍然远非完美。另一个改进是要求进行成对比较。而不是给每个答案分配一个数字,标注者会根据标准(版本 A 优于版本 B/版本 B 优于版本 A/版本 A 和版本 B 相等)进行三元比较。这些标签更容易收集,并且标注者输出之间的预期方差较低。

在 18 世纪,法国数学家和哲学家 Marquis de Condorcet 提出了一个适用于政治学的定理。De Condorcet 揭示了以下观点:如果一个群体需要基于多数投票做出二元决策,并且每个群体成员正确决策的概率为 p > 0.5,那么群体做出正确决策的概率将随着群体成员数量的增加而呈指数增长。这可能是法国大革命初期集体决策的正式原因,而现在,同样的完美逻辑也适用于机器学习系统!

让我们假设我们有三个标注者对同一个二元分类的物体进行标注。这个问题很棘手,所以每个人在 70%的情况下是正确的。因此,我们可以预期三个标注者的多数投票在 78.4%的情况下是正确的(0.7 * 0.7 * 0.7 + 0.3 * 0.7 * 0.7 + 0.7 * 0.3 * 0.7 + 0.7 * 0.7 * 0.3)。这是一个很好的提升!

这些数字应该谨慎解读。Condorcet 的陪审团定理隐含了一些在这里并不完全成立的假设:标注者并不完全独立,他们的错误来源是相关的(这可能是当数据样本嘈杂且难以阅读时的情况)。但即便如此,标注者群体的标注比单个标注者的标注更准确,并且这项技术可以用来提高标注结果。

这个想法可以被修改以降低成本。我们面临的一个算法修改如下:

  • 使用两个标注者对物体进行标注。

  • 如果他们意见一致,则接受该标签。

  • 如果不一致,则增加三个额外的投票,并使用多数投票作为标签。

通过这次修改,更复杂的样本需要更多的投票来保证一致性,而简单情况则需要的投票较少。

这里描述的启发式方法简单但强大。然而,如果你的问题中数据标注是一个关键因素,并且你需要投入一些努力,那么有许多研究致力于更好的设计。我们推荐的一些材料包括“关于众包任务分配的调查”(arxiv.org/abs/2111.08501)和众包供应商 Toloka.ai 的教程(web.archive.org/web/20240309071018/https:/toloka.ai/events/tutorial-wsdm//)。

标注者的一致性和相互同意应该被衡量。虽然机器学习从业者通常倾向于使用常规的机器学习指标来估计它,但那些有统计学背景的人可能会回忆起一个叫做评分者间可靠性的概念及其单独的指标集(例如 Cohen 的 kappa、Scott 的 pi 和 Krippendorff 的 alpha 等)。这些指标有助于保持标注团队工作的可靠性,过滤掉不诚实的标签(从而大大提高整体系统性能),并且在某些情况下,为你的模型性能提供一个坚实的上限(回想一下我们在第三章中提到的接近人类水平的表现)。

标注团队需要适当的工具,这可能会提高效率并改善创建标签的质量。如果你选择第三方工具,它很可能会有针对最常见问题的工具集;对于你自己的团队,你需要设置或创建自己的工具。选择现有解决方案或构建全新的解决方案的方面非常特定于领域,尽管一般的方法是:你的问题在机器学习公式中越受欢迎,你找到现代高质量工具集的机会就越大。以一个例子来说,有多个软件解决方案可以用于大规模图像检测的标签创建,但一旦你需要用 3D 多边形网格标注视频,你很可能需要定制化的解决方案。

大型基础模型在标注过程中的最新进展已经成为一个游戏规则的改变者:它们通常可以在几轮设置中收集初始标签,其准确性高于非专家人类标注员。用于大型语言模型标注的工具之一是 Refuel Docs (docs.refuel.ai/),它以专注于文本数据而著称;然而,在其他领域(例如,处理像 Segment Anything [segment-anything.com/]这样的通用分割模型或像 LLaVA [llava-vl.github.io/]这样的多模态解决方案)时,也可以使用基础模型。

有各种算法技巧可以帮助简化/减少构建系统所需的数据标注方面的努力,但在这本书中我们不会深入探讨这些技巧,因为它们太特定于领域了。我们还想强调的是,在数据标注过程和工具上投入的努力往往可以成为你机器学习系统成功的关键驱动因素。

6.3 数据和元数据

当我们构建机器学习系统时,数据集被使用,但在这之上还有一个信息层——让我们用“元数据”这个词来指代它。元数据是描述你的数据集某些属性的信息。以下是一些例子:

  • 时间相关属性**—这些包括事件发生的时间和它被处理及存储的时间。

  • 来源属性**—来源属性说明数据是否来自多个来源。

  • 用户属性**—如果一个用户以某种方式参与了数据集的生成,这种情况并不少见,他们的元数据就变成了相关数据样本的重要元数据。

  • 版本**—如果数据被处理,了解参与处理软件的版本可能是有用的。

元数据对于数据流至关重要,并保证了它们的连续性。让我们回到之前的医疗应用场景。想象一下,一家公司雇佣了 10 名医疗专业人员为你的系统标记数据。经过一年多的工作,他们终于标记了一个大数据集。突然,你收到一封邮件:其中一名标记者被发现是骗子,他的文凭是假的,他的执照已被吊销。这是一个不可接受的情况,因为数据的一个有形部分可能被认为不可靠,这会危及整个数据集。因此,你采取了唯一合适的解决方案,即立即停止在模型中使用他的标记。虽然这个例子有些夸张,但你的系统中随着时间的推移可能会出现许多不那么严重的问题。数据源问题、监管干预和尝试解释模型错误将使你定期参考元数据。

正如我们之前讨论的,虽然数据集的配方可能不同,但你的数据集很可能会被正确处理。揭示问题的新的方面,从而随着时间的推移调整数据集结构,并在处理数据集的方式中相应地反映出来,这是可以的。所有这些差异都应该反映在元数据中。

另一个变化来源是测试和错误修复。想象一下,你为一家共享出行公司工作,需要解决从 B 点到 A 点需要多长时间的问题。你从公司生成的历史数据开始,有一个预处理作业从日志中提取信息并将其存储在你最喜欢的数据库中,所以你最终得到一个像latitude_fromlongitude_fromdatetime_startdatetime_finishdistancedata_source这样的表格。

在某个时候,公司收购了一个竞争对手,你决定将其数据添加到你的 ETL 流程中。团队中的新工程师开始编码,新的来源被附加,新的数据点以相同的模式添加到你的数据集中,突然你的模型性能下降。发生了什么?

快进:你的公司在存储距离时使用千米,而收购的团队使用英里。处理集成的工程师知道这一点,并为新的数据源实现了英里支持,但意外地也为旧的数据源启用了它。因此,新的数据样本对于新的数据源是正确的,但对于旧的数据源则不是。如果你有一个像preprocessing_function_version这样的元数据字段,你可以轻松地找到受影响的样本。如果没有,你需要在发现代码缺陷后从头开始收集数据集。

这种缺陷并不是唯一可能需要揭示数据样本来源日期的场景。一些机器学习系统可能会影响数据来源,产生一种称为反馈循环的现象。反馈循环的一个非常常见的例子是推荐系统:假设一个市场销售许多商品,网站上有“你可能也感兴趣的商品”这样的板块。作为一个简单的基线,公司可能会放入最受欢迎的商品。但是当这个基线被机器学习系统取代时,新的商品会出现,而旧的商品领导者会失去其受欢迎程度,转而被新的推荐商品所取代。这种系统的设计不佳,可能会让一些并不那么好的商品在热门商品中占据一席之地,并长期占据主导地位。存储样本生成时的信息和当时活跃的相关系统版本至关重要(尽管还不够!)以避免反馈循环。

与元数据相关的一个重要场景是分层,这是一种数据抽样过程,它塑造了各种子组的分布。例如,假设一家公司在国家 X 开始运营,收集了大量与 X 国客户相关的数据。后来,公司进入了一个新的市场 Y,并希望为两个国家的客户提供相同水平的服务,包括机器学习模型的准确性。这需要在训练和测试数据集中以适当的平衡来表示 X 和 Y 国的客户。

分层对于验证设计和抵抗算法偏差至关重要,这两个主题将在未来的章节中讨论。

6.4 需要多少数据才算足够?

在我们讨论了数据集的重要性之后,缺乏经验的机器学习从业者可能会认为,建立一个可以流式传输大量样本的数据管道就足以构建一个良好的系统。嗯,这就是我们无法自信地说是或否的地方。

首先,并非所有数据样本都具有同等的重要性。增加数据集的大小只有当新的对象有助于模型学习与问题相关的新知识时才有用。添加与现有样本非常相似的数据样本是没有意义的;对于极其嘈杂的数据也是如此。这就是为什么有一个流行的研究方向是寻找数据集扩展的最优样本,称为主动学习。最简单的直觉是,通过添加模型错误的样本(这种信号通常通过人工在循环处理中获取)或表现出最低置信度的样本来丰富数据集。学术界已经开发了与主动学习相关的数十种方法;有关更多信息,请参阅最近的调查(例如,Xueying Zhan 等人撰写的“深度主动学习比较调查”,arxiv.org/abs/2203.13450)。

与数据集大小相关的一个更多方面是采样。虽然大多数情况下我们问自己的问题是“我们如何获取更多数据?”,但有时问题是“我应该使用数据集的哪一部分来提高效率?”这种情况通常发生在数据集不需要手动标记且由定义的过程生成时——例如,在流行的 B2C 网络服务(搜索引擎、市场)中的点击流。

当数据集不仅巨大,而且往往不平衡和/或可能包含大量重复项时,采样是有效的。这里可以适用不同的策略,其中最常见的是基于分层的方法,即根据关键特征或算法定义的簇将数据分成组,并限制每个组使用的数据量。这些数量不必相等——例如,如果时间成分与你的问题相关,你很可能希望优先考虑新鲜数据而不是旧组。

另一点是关于数据噪声。不久前,机器学习社区中有一个强烈的共识,认为几个干净样本比多个噪声样本要好。然而,最近情况发生了变化;像 GPT-3 和 CLIP 这样的大型模型的进展表明,在某个规模上,手动过滤几乎是不可能的(处理 5000 亿个文本标记或数百万张图像将花费巨额资金),但使用大量弱监督或自监督(自动使用启发式方法)的数据是有效的,因此对于某些任务,大量不完美的数据集比小量精心挑选的数据集更适合。

备注:虽然训练数据中存在一些噪声是可以接受的,但在验证/测试数据集中,这要低得多。我们将在第七章中详细讨论。

你可能期望模型的性能随着数据集大小的平方根而渐进地提高,无论使用什么指标。这种估计非常粗略,并不一定完全符合你的问题,但它可能给你一些直觉(见图 6.3)。

并非每个模型都可以通过添加新数据来改进,因为其来源的不确定性。不确定性主要来自两个核心来源:

  • 知识或信息缺乏(称为认识不确定性

  • 在给定数据本身中发现的随机不确定性(也称为随机不确定性

随机不确定性主要源于数据中存在的复杂性,如重叠的类别或加性噪声。数据不确定性的一个关键特征是,无论收集多少额外的训练数据,它都不会减少。

figure

图 6.3 展示了一个真实项目中模型指标随数据集大小变化的图表。每条线都展示了目标指标在数据集大小增长时的动态变化。

另一方面,当模型遇到训练数据覆盖薄弱或超出训练数据范围的区域时,就会发生认知不确定性(参见图 6.4)。

figure

图 6.4 随机不确定性和认知不确定性之间主要差异的示意图(来源:“基于深度学习的分子性质预测的可解释不确定性量化”由 ChuI Yang 和 YiPei Li 撰写,mng.bz/n0ZV)

一旦你收集了一些数据并形成了一个训练管道(详见第十章),它就解锁了做出关于你需要多少更多数据的知情决策以及估计新数据的经济效率的选项。当与昂贵的数据(例如,由高度熟练的专业人士标注的数据)一起工作时,这尤其合理。一个高级算法如下:

  • 将数据集分成桶,使得桶的大小接近均匀,并且不同桶之间的样本相似性最大化。这样,每个用户的样本都流向由用户 ID 确定的相同桶(这使我们回到了之前提到的分层方面)。

  • 如果适用,固定计算预算。无论数据集的大小如何,你都要为 N 个批次训练模型。

  • 在从数据集的一小部分到全部的数据子集范围内训练模型,确保使用的子集是累积的(例如,如果在训练 10%的子集时使用了样本 X,那么它必须在 20%、30%等情况下使用)。

  • 计算每个训练模型的键指标,并使用数据集大小作为水平轴,指标本身作为垂直轴绘制图表。

  • 通过一些想象力的激发,推测还需要多少数据才能使指标变化再增加 1%。

这个指标的精度较低,并且没有考虑系统性的方面(例如,第十四章中我们将讨论的概念漂移),尽管即使这个精度对于做出关于我们应该在数据标注管道中投入多少的重要决策也是至关重要的。

6.5 鸡生蛋还是蛋生鸡的问题

你在数据集方面可能遇到的最困难的问题之一是一个冷启动问题(也称为鸡生蛋还是蛋生鸡的问题):当你需要数据来构建系统,但数据直到系统启动后才可用。陷入这样一个可怕的循环是多么可怕啊!

这通常是初创公司或试图在新的市场或垂直领域推出产品的公司遇到的问题。在机器学习世界中,常见的解决方案是近似。由于我们没有与我们的问题完美匹配的数据,我们需要找到尽可能接近的数据。哪些数据会接近取决于问题,让我们看看一些例子。

在这个例子中,让我们想象一家专注于员工安全的公司,该公司生产的产品用于监控工人在各种环境中的安全规则遵守情况。新产品应检查工厂地面上是否佩戴了安全帽。当产品可用时,将会有大量来自同意分享数据的客户的数据,但在那之前,公司的客户摄像头是不可用的。

那么,我们如何解决这个问题呢?

  • 方法 1—让我们使用计算机图形世界。我们取一叠 3D 人体模型,并在工厂背景上渲染它们,有的戴帽子,有的不戴。

  • 方法 2—让我们使用公共来源。我们在公共来源(从谷歌图片到图片库)中寻找相关的图片,并抓取或购买它们。

  • 方法 2.5 (混合)—让我们进行交叉:我们从工厂中取一些人的照片,并在其中一些照片上绘制/渲染帽子。

  • 方法 3—让我们尝试表演!你的团队购买安全帽,去一个废弃的工厂,并戴着帽子进行一次拍照。

  • 方法 4—让我们偷懒。我们找到一个公共数据集,其中人们戴着非常相似的帽子,但户外,在建筑工地上——不太近,但仍然有。

  • 方法 5—让我们构建一个非常简单的基础线,没有实际机器学习在幕后,并将其建议给客户以打破循环。这样一个基础线的例子可能是使用现有的面部检测器找到人头,然后裁剪这些面部,并添加一个简单的启发式方法尝试定位帽子(例如,一个明亮的封闭块)。

  • 方法 6—如果可用,我们将集成一个大型供应商提供的解决方案。如果从法律角度来看,可以重用供应商的标签,这个选项将工作得更好。然而,代理供应商的输出可以为你提供具有自身价值的初始未标记数据。

这些例子可能并不总是合理和适用的,但它们代表了解决这个问题的几种方法:

  • 生成合成数据

  • 使用类似情况下的可用数据

  • 手动创建数据

  • 从类似问题中获取数据并尝试调整

  • 使用虚拟基线模型或第三方进行启动

我们应该指出,并非每个场景都适用于每个问题。显然,你不会用脑部扫描的图像来构建用于肺癌检测的医疗系统,而一个天真的基线作为医疗顾问是完全不合适的。但是,使用相同设备从其他医院获取扫描数据可能是一个值得考虑的好主意;虽然每个扫描仪的校准可能略有不同,但它们一起可以提供一些泛化(在 A、B 和 C 医院的数据库上训练的模型可能对 D 医院也很有用)。在尊重的公众公司中,很少会批准抓取其他网站,尽管这在小型初创公司中是一种流行的技术。通过简单的渲染管道获得的合成图像通常不是最佳选择,因为它们不够真实(不完美的光照、阴影等)。但是,通过一些秘密配方,你可以使它们变得逼真。在论文“MobilePose:具有弱形状监督的未见物体实时姿态估计”(Hou 等人,2020 年,arxiv.org/abs/2003.03522)中,研究人员在增强现实记录上渲染了物体(使用准确估计的光照),从而使图像更加逼真,因此对模型更有价值。

在任何情况下,我们都需要记住,当系统上线时,数据并不是我们所要处理的实际分布的代表样本。这意味着验证结果应该持保留态度,用更现实的样本替换这样的代理数据集是首要任务。在讨论健康数据管道的特性时,我们再详细讨论这个问题。

6.6 健康数据管道的特性

数据收集和预处理需要具备三个重要特性:

  • 可重现性

  • 一致性

  • 可用性

让我们逐一回顾这些特性。

可重现性意味着如果需要,你应该能够从头开始创建数据集。在你的存储中不应该有由一位聪明的工程师用一点黑暗魔法制作出来的“金数据文件”。但必须有一个软件解决方案(一个简单的脚本、更高级的管道或大型系统),允许你使用与之前相同的来源重新创建这样的文件。这个解决方案需要得到良好的文档和测试,以便每个项目合作者都能在需要时运行它。

可重现性的推理与 DevOps 社区中流行的基础设施即代码范式类似。是的,最初手动创建初始数据集可能看起来更容易,但从长远来看,它并不具有可扩展性,并且容易出错。下次——当准备添加新批次数据时——你可能会忘记运行预处理步骤,从而导致隐式数据不一致,这是一个难以检测的问题,会影响整个系统的性能。

一致性本身是关键。机器学习问题通常存在标签部分不明确的情况,定义一个严格的分离平面是不可能的。在这种情况下,进行标签的专家往往意见不一致。

来自我们背景的一个非常典型的例子与信用卡交易分类问题相关。客户被期望为每一笔交易提供一个标签,描述支出的目的。初始的标签分类包括“食品和饮料”、“酒吧和酒庄”和“咖啡屋”。一个修辞问题出现了:在一家酒吧订购咖啡和三明治的支付,哪个标签更合适?这个例子的一个部分解决方案可能是制定某种协议来决定如何打破平局以及如何解决模糊的边界(例如,“当商家类型和购买物品类型冲突时,首先打开的优先”)——这极大地简化了模型训练,特别是验证。

一致性不仅关乎标签。数据的各个方面都应该是一致的:数据的来源是什么,数据是如何预处理的,应用了哪些过滤等。当系统在一个小型公司内部构建时相对容易,而对于跨国公司来说则更具挑战性,一些正式的定义可能会有所帮助。一旦你感觉有可能误解数据管道中使用的核心术语,将其添加到设计文档中可能是有用的,以确保整个团队在同一个页面上。

一致性的另一个方面是在系统构建阶段数据的收集方式和系统使用期间的系统输入。这是一个常见问题,在机器学习的术语中,它导致分布不匹配,这是一个影响模型性能和公平性的问题,在系统上线之前很难检测到。

我们最近提到了反馈循环现象——模型影响数据来源,从而违反了一致性假设。我们也描述了数据在系统启动后才可用的情况,这也可能导致不匹配。这类问题仍然是应用机器学习中最具挑战性的方面之一,在设计系统时你永远不能忽视它们。

数据一致性始终是一个开放性问题,因此在机器学习系统开发的每个阶段,从最初的草稿到长期的维护,你都应该关注它。我们将在第十四章讨论监控和漂移检测时回到这个方面(甚至会有一个来自我们朋友的一个简短但相当酷的篝火故事)。

最后但同样重要的是可用性。这个属性实际上涵盖了两个想法:系统的可用性和工程师的可用性。第一个也可以称为可靠性;系统设计者应该对不可靠的数据源非常批判。这里是一个很好的负面例子——事情最终可能会出错。想象一下,一个依赖于第三方 API 丰富你的数据流的系统,一切都很顺利,直到它不再顺利——一个为 API 提供动力的公司失去了其关键站点可靠性工程师,因此无法处理基础设施。如果你的系统对这个数据源的依赖是关键的,他们的问题就变成了你的问题。

当然,这并不意味着使用第三方 API 不是一个选择。正如我们之前提到的,使用外部解决方案通常是一种好做法——不仅由像亚马逊、谷歌和微软这样的巨头提供。但数据可用性很重要,这些风险应该被认真考虑。有些服务提供商提供的服务可能不太可靠(我们无法想象一个由实验性可视化工具故障引起的高优先级问题),但数据源不是其中之一。

同样的哲学也适用于内部系统(例如,由其他团队中的同事控制的系统)。外部和内部系统有不同的控制齿轮:对于前者,使用服务级别协议来评估风险,而内部系统仅存在于成熟组织中。同时,在较小的公司中,与维护系统的团队保持一致并有效降低相关风险更容易。

值得注意的是,与数据可用性相关的问题并不严格属于软件相关。即使所有相关系统都得到适当的构建和维护,也可能存在更复杂起源的问题——例如,由信息安全和个人隐私权(例如影响使用个人数据的新法律法规或你的关键客户 CEO 的决定,即他们的数据不应再离开他们的基础设施)引起。

工程师的可用性可能被高估:你可能会听到类似这样的话,“来吧,即使这不是一个简单的操作,我们的工程师也能获取数据——他们是专业人士,如果有的话,会处理技术难题。”很可能是真的——我们假设我们的读者和他们的同事都是杰出的专业人士,但你不能忽视时间限制。想象一下,一个工程师和他的另一个团队的伙伴一起去吃午餐;他们讨论了与工作相关的事情,喝了一杯咖啡后,一个与当前感兴趣的系统相关的新假设突然出现。

如果数据容易获取,你可以通过拉取数据集、汇总一些统计数据,甚至运行基本实验,相对快速地做出信息丰富的决策。如果第一个简单的方法得到证实,它可以被优先考虑,这意味着应该分配更多资源。谁知道呢,这可能会在未来带来重大的改进。

否则,如果拉取数据和做出数据驱动的决策耗时,那么假设很可能会被忽视(“嗯,这可能很有趣,但我有很多事情要做,找到数据会花费很长时间!”)。为了避免这些情况,我们建议将一部分工程努力投入到构建工具中,使数据集对应该使用数据的人更容易获取。虽然这适用于任何工程生产力工具,但当工具提高数据可用性时,投资回报率尤其高。正如我们一开始所说的,很难高估高质量数据对机器学习系统的重要性,因此平滑与数据集的交互是长期的良好投资。

在本书的这个阶段,关于一致性和可用性的信息已经足够深入。我们将在第十章中更详细地讨论这两个管道属性。同时,现在是时候进入本章的实践部分:设计文档。

6.7 设计文档:数据集

当我们描述数据问题的步骤时,它自然地引导我们回答设计文档中需要回答的额外问题。以下是我们建议在这个阶段问自己的问题清单。

  • ETL:

    • 数据来源是什么?

    • 我们应该如何表示和存储我们系统的数据?

  • 过滤:

    • 好的和差的数据样本的标准是什么?

    • 我们可以预期哪些边缘情况?我们如何处理它们?

    • 我们是自动过滤数据还是设置手动验证的过程?

  • 特征工程:

    • 特征是如何计算的?

    • 表示是如何生成的?

  • 标签:

    • 我们需要哪些标签?

    • 标签的来源是什么?

如同经常发生的那样,回答这些问题可能会产生更多问题。给自己自由去思考这些问题。在机器学习系统设计中,投入时间回答与数据相关的问题总是有出色的投资回报。

6.7.1 超级大零售的数据集

现在,让我们回到准备设计文档。这次我们正在准备一个专门针对我们虚构公司数据集的部分。像往常一样,我们将从超级大零售开始。

设计文档:超级大零售

III. 数据集

数据集的原子对象是一组(日期、产品、商店),我们旨在预测的目标变量是销售的单位数。

i. 数据来源

我们可以利用多个数据来源来实现我们的目标。

内部来源

  • 购买历史数据(即,交易历史)是从 Supermegaretail 连锁店收集的,并保存到集中式数据库中。它将成为我们的主要真实来源:销售数量、花费的金额、应用的折扣、交易 ID 等。

  • 库存历史是我们问题的第二个重要真实来源,因为它直接决定了每个商店可以销售多少个产品单位。这个来源可以帮助估计每天开始时有多少产品可供销售,以及有多少已经过期并被撤出销售。

  • 每个产品、商店和交易的元数据

  • 计划的促销活动日历。这是一个影响未来销售的重要因素,肯定需要考虑。

外部来源:手动收集的数据

  • 价格监控。从我们的竞争对手那里收集的价格和其他产品信息。他们每天从不同竞争对手的子集中手动收集。这可能是由我们内部团队或第三方(外包)完成的。也可以采用混合方法。每个产品也应包含一个全球产品标识符(条形码),这样我们就可以轻松地将收集到的数据与我们的产品匹配。了解汇总的竞争对手价格及其动态有助于我们了解市场正在发生什么。

外部来源:购买数据

  • 气象服务购买的历史和预测天气。天气是直接影响消费者行为的一个重要因素。

  • 顾客流量估计(来自电信提供商)。

  • 全球市场指标

移动应用和网站数据(可选)

  • Supermegaretail 有一个配送服务(即使它产生的收入不到 5%)。我们将收集关于特定地点特定销售的额外数据。有时这些信息可以是一个有价值的预测因素。

  • 此外,移动和网站服务收集关于用户活动的隐式反馈,包括查看、点击或添加到购物车,这些也可以预测实体店的销售额。

ii. 数据标注

由于我们处理的是一个需求预测问题,我们不需要直接从交易历史中派生的额外数据标注。

iii. 可用元数据

我们根据每个商店级别的 SKU 进行需求预测,有三个关键要素:产品、商店和交易。

产品

  • 产品 ID 和条形码。

  • 不同级别的类别代码(1,2,3)。我们可以使用类别层次结构来对产品之间的相似性进行基于规则的测量。此外,其他分类信息,如品牌、制造商或定价组。

  • 储存期限,它决定了过度预测该产品销售的糟糕程度。

  • 产品被添加到连锁店组合矩阵中的日期。

  • 产品的尺寸和重量。

商店

  • 店铺 ID。

  • 位置(坐标),在第三方来源的支持下——我们可以用它来添加有关天气、人流、到关键点的距离以及其他相关事项的信息,如城市、地区和相关的物流中心。

  • 最接近的竞争对手的商店(包括它们的 ID 和距离)。

  • 商店的大小及其格式。它们决定了在这个商店的品种中会有哪些产品和多少独特的商品。

  • 商店开闭的日期。

交易

  • 时间戳。这使我们能够通过添加诸如假日等信息来丰富数据集。

  • 客户 ID(如果使用了忠诚度卡)。尽管数据集的最终单元是(产品,商店),但可以捆绑(客户,产品)用于单独的数据管道,通过聚合交易到一个用户-项目矩阵及其分解来计算产品嵌入。嵌入将包含购买行为模式。

  • 商店 ID产品 ID

iv. 可用历史

对于 Supermegaretail 来说,需求预测并不新鲜。关键的 ETL 流程已经到位。Supermegaretail 已经收集了超过 3 年的数据。

这个历史对于我们的预测模型学习模式、捕捉销售的季节性、估计趋势等至关重要。同样适用于产品和商店元数据。天气数据(我们从外部来源获取)在过去一段时间内一直可用,只要我们需要。

股票历史和促销活动也已收集。

竞争对手的价格监控数据已经收集了两年。

v. 数据质量问题

交易、库存和促销数据可能包含缺失或重复的值,因此在聚合之前需要额外的过滤或预处理。

我们购买的外部数据在到达我们这里之前已经过清洗并通过了某些质量控制。然而,需要实施必要的检查。

竞争对手的价格覆盖了大约 25%的 SKU,存在差距。

vi. 最终 ETL 管道

最高级方案如下:

  1. 交易数据是按日汇总的。

  2. 新汇总的分区被添加到交易汇总表的表中。

  3. (可选)我们不仅重写了最后一天,还重写了前 2 到 3 天,以修复数据中可能存在的损坏(重复、不完整数据、重复数据等)。

  4. 我们根据数据、产品 ID 或商店 ID 将其他内部/外部数据源合并。

  5. 最后,我们根据合并后的数据集计算特征。

如果需要,我们可以添加一个产品嵌入的数据管道,如第 III 节所述。

6.7.2 PhotoStock Inc.的数据集

现在让我们切换到 PhotoStock Inc.的设计文档。

设计文档:PhotoStock Inc.

III. 数据集

i. 数据集和来源

可以用于为 PhotoStock Inc. 搜索引擎收集信息的潜在数据源之一是库存库中每张照片相关的数据。这些数据可能包括与照片相关的标签、标签和描述等信息,这些信息可以为照片内容提供有价值的背景。此数据集还应包含实际照片的 URL 和我们使用的缩略图的 URL。我们建议将此数据集命名为“描述数据集”。

另一个潜在的数据源是用户提交给 PhotoStock Inc. 平台上的搜索查询。这些查询可以提供关于用户正在寻找的图片类型的洞察,并可用于帮助指导机器学习模型的发展。当与用户点击相结合时,它们提供了关于相关性和用户兴趣的强烈信号。此数据集的扩展可能包含有关这些点击相关的会话的进一步信息:用户在照片页面上花费了多长时间,以及他们之后是否购买了该照片?我们建议将此数据集命名为“点击数据集”。

此外,我们可以雇佣标签员手动为查询和图像对分配相关性评分。标签员将获得一组搜索查询和 PhotoStock Inc. 库中的一组图像选择,并要求根据图像与查询内容匹配的紧密程度为每个查询-图像对分配相关性评分。最初,查询-图像对应从过去用户查询和库存库中可用的照片池中随机选择;然而,以后可以改进,涉及一些主动学习方法,并为模型不太自信或容易出错的对提供信号。我们建议将此数据集命名为“标签数据集”。

我们可以考虑一些公共文本/图像数据集(例如,COCO—Microsoft Common Objects in Context)用于模型启动。然而,鉴于有用户交互的坚实历史,使用我们自己的数据应该没问题,而公共数据集将仅间接使用——我们可以从在这些数据集上预训练的模型开始。

描述和点击数据集是通过 PhotoStock Inc. 业务的主体流程自然生成的。因此,目前我们不需要过多关注,除了构建适当的 ETL 流程和存储。由于标签数据集需要雇佣标签团队或使用第三方服务,因此需要为其分配专门的预算。

ii. 元数据、过滤和子采样

由于照片描述由内容质量团队审核,因此预期描述数据集将足够干净,因为他们的质量已经是我们的产品核心部分。点击数据集可能会因为用户行为的不同模式和爬虫的存在而嘈杂;我们需要提出启发式方法来处理这些问题。包含购买数据的扩展版本可能不太嘈杂,但考虑到点击到购买的转换率,它也可能小得多。考虑到这些方面,我们预计点击+购买和标签数据集对于验证更有价值,而描述和没有购买的点击数据可能更嘈杂,因此更适合用于训练。

为了解决过滤过程中出现的质量问题,我们应该注意元数据。描述数据集应该标注卖家信息和变更日期;点击数据集应该标注用户信息、点击项卖家信息和额外的搜索会话信息。在这个阶段,没有必要聚合所有关于用户和卖家信息。这可能是进一步的特征工程步骤的一部分,但至少我们需要确保存储相关用户 _id卖家 _id会话 _id购买 _id,以保持将来使用这些数据的能力。

考虑到 PhotoStock Inc.的搜索和购买量,我们可能不需要为了纯粹的技术需求进行额外的子采样;可以处理完整的数据集。然而,我们可能需要运行子采样来调整类别平衡:点击的物品比购买的物品多,搜索结果页面显示的物品比点击的物品多等。

iii. ETL 和数据准备

训练数据应该定期(目前为每日)以批量方式获取。我们建议使用 Flyte 框架来编排任务,因为它已经是 PhotoStock Inc.中其他批量任务的首选框架。

这里不需要任何花哨的东西——只需从生产数据库中收集数据,并以 Parquet 文件的形式单独存储,以便于读取即可。目前我们预计不需要进行复杂的预处理。

iv. 标注

我们不清楚应该优先考虑哪个平台进行人工标注;是否应该使用内部团队(例如,我们的客户支持和审核团队)或雇佣第三方服务也是一个悬而未决的问题。这个问题需要与这些团队经理一起解决。此外,我们还应该从一些第三方服务中获得成本估算,以比较内部标注与他们的成本。

我们需要标注(查询,图像)对,并将它们分为三个类别:

  • 相关

  • 不相关

  • 无法回答

我们应该准备好将“相关”细分为“非常相关”和“有些相关”,以获得更高的粒度;对于“不相关”也是如此。至于“无法回答”,我们需要要求标注者输入他们无法标注的原因——标注的早期迭代可以为我们提供关于数据集的新见解。

摘要

  • 不要仅限于使用单一的数据源。确定你是否拥有足够内部来源,或者是否需要超出你业务生态系统的范围来扩展你的搜索范围。

  • 访问独特的数据集可以给你带来显著的竞争优势。反过来,如果处理得当,易于获取的数据集也能带来巨大的价值。

  • 在处理数据集时,不要忽视元数据,这对于数据流至关重要,并保证了其一致性。

  • 请记住,在准备数据集时没有严格的技巧顺序。操作的最后顺序将取决于你系统的业务目标、你所在的领域和其他因素。

  • 避免将数据集填充与已存在的样本相似的样本。相反,应该填充那些模型未能正确工作的样本。

  • 在运行系统之前,确保你有可以喂给系统的数据。

  • 记住,数据管道必须满足三个标准:可重复性、一致性和可用性。

第七章:7 验证方案

本章涵盖

  • 确保可靠的评估

  • 标准验证方案

  • 非平凡验证方案

  • 分割更新程序

  • 验证方案作为设计文档的一部分

构建一个稳健的评估过程对于机器学习(ML)系统至关重要,在本章中,我们将介绍构建适当的验证方案以实现系统性能自信估计的过程。我们将涉及典型的验证方案,以及如何根据给定问题的具体情况进行适当的验证选择,以及在设计野外评估过程时需要考虑哪些因素。

适当的验证程序旨在模仿我们在实际操作环境中应该拥有的知识,以及可以丢弃的知识。这与过度拟合问题或泛化问题有关,我们将在第九章中详细讨论。

它还提供了一个可靠且稳健的系统性能估计,理想情况下带有一些理论保证。例如,我们保证在 100 次中有 95 次真实值将在下限置信区间和上限置信区间之间(这种情况将在本章后面的篝火故事中介绍)。它还有助于检测和防止数据泄露、过度拟合以及离线和在线性能之间的差异。

性能估计是验证的主要目标。我们使用验证来估计模型在未见数据上的预测能力,通常首选的方案是具有最高可靠性和稳健性的方案(即低偏差/低方差)。

只要我们有一个可靠且稳健的性能评估,我们就可以用它来做各种事情,比如超参数优化、架构、算法和特征选择。在某种程度上,它与 A/B 测试相似,其中产生较低方差的模式提供更高的敏感性,这一点我们将在本章后面进行讨论。

7.1 可靠的评估

当验证任何事物时,几乎总是一个好的主意,建立一个稳定可靠的流水线,产生可重复的结果(见图 7.1)。你很可能在文献中找到的标准建议归结为以下三个经典条件:你所需要做的就是将数据分为训练集、验证集和测试集。训练集用于模型训练,验证集旨在评估训练过程中的性能,测试集用于计算最终指标。这种三集方法对于那些熟悉竞争性机器学习(例如,Kaggle 举办的活动)或学术界的人来说是众所周知的。同时,在应用机器学习(ML)中存在一些微妙但重要的区别,我们将在本章中进一步讨论。

figure

图 7.1 基本高级模型开发周期

有几点需要注意:

  • 简单的训练-验证-测试分割假设所有三个数据集都来自相同的分布,并且这种分布在未来将保持不变。这是一个必须自己验证的强假设。如果这个假设不成立,就无法保证未来的性能。

  • 验证集必须能够重复使用来估计模型性能。基于验证集高估模型性能会导致偏差和过度拟合。停下来思考一下:当我们从高层次的角度谈论超参数优化、特征选择或模型选择时,这基本上也是学习过程的一部分。通过归纳,测试集也可能被以同样的方式滥用。

正因如此,反复使用相同的验证分割进行评估和寻找最优超参数或其他任何东西会导致偏差/过度拟合和非鲁棒的结果。因此,我们不是将验证视为一开始就完成的事情,而是将其视为在系统环境发生变化(例如,有新的数据来源、新特征、模型使用可能引起的潜在反馈循环等)后需要反复进行的持续过程。

我们永远无法 100%确定世界会带来什么;这就是为什么我们必须预料到意外。

7.2 标准模式

实践表明,在选择机器学习系统的验证模式时,你不需要重新发明轮子。大多数标准模式都是经过时间考验且表现良好的解决方案,主要需要你选择一个适合你项目需求的模式。我们将在几个子节中简要介绍这些模式。

经典的验证模式在永不过时的 Python 机器学习库 scikit-learn 中得到了很好的实现,如果你对材料的了解有疑问,所有相关文档都值得一读。信息可在mng.bz/aV6B找到。

7.2.1 保留集

我们将首先将数据集分为两个或更多部分。可能是在几乎所有关于机器学习的书籍中提到的黄金经典——我们之前讨论过的训练/验证/测试分割。

采用这种方法,我们将数据分为三个集合(可能是随机的,也可能基于特定的标准或层),具有不同的比例——例如,60/20/20(见图 7.2)。百分比可能根据样本数量和指标(数据量、指标方差、敏感性、鲁棒性和可靠性要求)而变化。经验上,整个数据集越大,分配给验证和测试的部分就越小,因此训练集增长得更快。测试集(即外部验证)用于最终模型评估,绝不应用于其他任何目的。同时,我们可以主要使用验证集(即内部验证)进行模型比较或调整超参数。

figure

图 7.2 标准的按部就班的数据分割

7.2.2 交叉验证

对于计算成本高的模型,如深度学习模型,保留法验证是一个不错的选择。它易于实现,并且不会给学习循环增加太多时间。

但让我们记住,我们从所有数据中抽取一个单独的随机子样本。我们并没有重用所有可能的数据,这可能导致评估偏差或未充分利用可用数据。最糟糕的部分是什么?我们得到一个单一的数字,它不允许我们了解估计值的分布。

统计学中解决此类问题的银弹是自助法。在验证情况下,它看起来像是多次随机采样训练验证分割,每次迭代训练和评估模型。训练模型是耗时的,我们希望快速迭代以进行一般参数调整和实验。那么我们如何做呢?

我们可以使用一个类似但简化的采样过程,称为交叉验证。我们可以将数据分成 K 折(通常为五折),逐个排除它们,将模型拟合到 K-1 折的数据,并在排除的折上测量性能。因此,我们得到 K 个估计值,可以计算它们的平均值和标准差。结果,我们得到五个数字而不是一个,这更具代表性(见图 7.3)。

figure

图 7.3 K 折分割:每个样本被分配到一个折,每个折在剩余的训练轮次中提供一次验证和一次训练。

交叉验证有几种变体,包括:

  • 分层交叉验证(我们需要保持类别的平衡)。

  • 重复交叉验证(我们将数据分成 K 折 N 次,这样每个对象参与评估 N 次)。

  • 分组交叉验证(当组内对象相似时,我们可能希望避免泄露;整个组必须完全包含在训练样本或验证样本中)。

假设我们预测数百个油井的油流量。油井根据其位置进行分组:相邻的油井从同一油田提取石油,因此它们的产量相互影响。在这种情况下,分组 K 折交叉验证是一个合理的选择。在将样本分配到折时找到一个合适的分组标准是验证过程中的一个关键决策,这里的错误会极大地影响结果。

7.2.3 K 的选择

剩下的唯一问题是选择多少折。K 的选择受三个变量的影响:偏差、方差和计算时间。经验法则是使用 K=5,它在偏差和方差之间提供了良好的平衡。

K 的一个极端情况是留一法交叉验证,其中每个折包含一个数据样本;因此 K 等于数据集中样本的总数。这种方案在计算时间和方差方面是最差的,但在偏差方面是最好的。

Ron Kohavi 在 1995 年发表了一篇经典论文,题为“关于交叉验证和自助法在准确度估计和模型选择中的应用研究”(mng.bz/4pn5),其中提供了以下指导原则:

  • 增加折叠数量可以减少偏差并提高性能估计。

  • 同时,由于每个验证折叠中的样本数量减少(估计变得过于嘈杂),随着折叠数量的增加,方差也会增加。在假设偏差一致的情况下,验证方案敏感性由方差决定。

  • 对于模型比较目标,使用重复交叉验证(K = 2 或 K = 3,重复 10 到 20 次)是一个好主意。然而,对于偏差优化,重复 K 折叠并不有帮助,因为不同重复之间的估计已经共享一致的偏差。

  • 随着数据集大小的增长,所需的折叠数量自然会减少。每个折叠中的数据越多,它就越具有代表性。

  • 对于更简单的模型(在处理基线解决方案时通常是这种情况)和表现良好的数据集,你期望偏差和方差随着折叠数量的增加而减少。

重要的是要记住,验证方案的高敏感性(即低方差)只有在我们试图捕捉模型性能变化很小的情况下才有意义。

7.2.4 时间序列验证

当处理时间敏感数据时,我们不能随机采样数据。相邻日期的产品销售之间共享一些信息。同样,最近的用户行为为他们的后续行为提供了一些线索。但我们不能根据未来的数据预测过去。在时间序列中,模式分布沿数据集不是均匀的,我们必须找出其他类型的验证方案。在这种情况下,我们如何评估模型?

在时间序列数据中使用的验证方案类似于保留集和交叉验证,但通过时间戳进行非随机分割。在滚动交叉验证中选择折叠数量及其大小的建议类似于有序 K 折叠。

时间序列验证增加了需要考虑的额外自由度。一篇优秀的论文,“评估时间序列预测模型”由 Cerqueira 等人撰写(arxiv.org/pdf/1905.11744.pdf),详细阐述了以下观点:

  • 窗口大小—测试集的大小应该反映我们预测的距离以及模型在重新训练之前将保持生产状态的时间。

  • 训练大小—关于用于训练的数据量,有两种选择:我们要么使用所有可用的历史数据,要么将训练大小限制在一到两个前期(这些可以是周、月或年,具体取决于给定的季节性)并丢弃所有以前的历史数据,因为它们是不相关的。

  • 季节性—数据中存在依赖于日、周、月、季度或年循环的模式。我们应该相应地选择测试集和训练集的大小,以捕捉这些模式。例如,为了捕捉年度模式,训练数据应至少包含 2 年的历史数据。另一个例子是测试集中的每周季节性:为了最小化折叠之间的方差,每个折叠应包含相同的日子(因此我们在每个折叠中取整个周)。

  • 差距—训练数据和测试数据之间可能存在差距,这追求两个目标。首先,它使我们为接收新数据的延迟(导致特征的延迟)做好准备,其次,它使训练数据和测试数据的相关性降低,从而最小化泄露的风险。例如,在两种情况下,我们可能在训练集和测试集之间跳过 2 到 3 天。

虽然时间序列验证是最敏感的验证方法之一,但仅仅依靠简单的“训练时不要看未来数据”规则将会过于短视。遵循这个规则可以让你避免 95%的典型错误;然而,仍然有一些情况下你可能需要打破这个规则。例如,应用于金融数据(如股市时间序列)的机器学习因其对精确验证要求的高标准而闻名。同时,该领域的某些专家强调,如图 7.4 所示,简单的时序验证可能导致由数据子集有限引起的过拟合(更多细节,请参阅 Marcos Lopez de Prado 的《金融机器学习进展》第十二章“通过交叉验证进行回测”; Wiley)。违反此规则的一个类似原因可能源于你需要估计模型在异常情况下的表现。为了获得这个信号,你可以在 2017 年至 2019 年和 2021 年至 2023 年的数据上训练模型,然后在该 2020 年 COVID 时期的数据上进行测试。这种分割几乎作为默认的验证方案不起作用,但作为辅助信息仍然可能有用。

图像

图 7.4 标准基于时间的分割。测试数据集始终跟随训练数据集,因此训练样本是“过去”,测试是“未来”。

有时你需要使用不同方案的组合。在早期流量预测的例子中,我们可能会结合分组 K 折验证和时间序列验证:

import numpy as np
from sklearn.model_selection import GroupKFold

import numpy as np
from sklearn.model_selection import GroupKFold
from sklearn.exceptions import NotFittedError

def grouped_time_series_kfold(model, X, y, groups, n_folds=5, 
n_repeats=10, seed=0):
    scores = []
    np.random.seed(seed)
    unique_groups = np.unique(groups)

    for i in range(n_repeats):
        gkf = GroupKFold(n_splits=n_folds)
        shuffled_groups = np.random.permutation(unique_groups)

        for train_group_idx, test_group_idx in gkf.split(X, y,
        groups=shuffled_groups):
            train_groups = shuffled_groups[train_group_idx]
            test_groups = shuffled_groups[test_group_idx]

            # Find the earliest and latest indices for train and test groups
            train_indices = np.where(np.isin(groups, train_groups))[0]
            test_indices = np.where(np.isin(groups, test_groups))[0]
            train_end = np.min(test_indices)

            # Ensure temporal order
            train_mask = np.isin(groups, train_groups) &
            (np.arange(len(groups)) < train_end)
            test_mask = np.isin(groups, test_groups)

            model.fit(X[train_mask], y[train_mask])
            score = model.score(X[test_mask], y[test_mask])
            scores.append(score)

    return np.array(scores)
Valerii 的篝火故事

当我在一家大型在线零售商的动态定价服务部门工作时,我们旨在构建一个销售预测模型,该模型可以预测一周后的销售量,并处理预测的后处理以确定最佳价格。

最初,我们将上一周用于验证。随着新的每日数据变得可用,验证周被向前推进了 1 天。然而,观察到验证集上的性能指标每天都会出现显著的波动。这使得在周期性特征添加和调整以及预测后处理变化的背景下,很难确定模型质量的变化。

我们想了解指标波动的原因,经过彻底调查这个问题后,我们发现产品的种类每周变化了 15%,每月变化了 40%。此外,发现单个产品的销售动态高度异质(例如,今天售出 10 个单位,但在接下来的 2 天内售出 0 个单位)。因此,我们依赖于由每日更新的验证集引起的指标变化,而不是模型质量的实际变化。

为了解决这个问题,我们实施了一种“延迟移动”验证方法。我们不是每天更新验证集,而是每月更新一次,同时仍然使用一周的验证期。这确保了用于计算指标的数据保持相对新鲜(不超过 1 个月),同时在整个月份内保持验证集固定。因此,两个模型之间的比较变得更加有意义,性能指标也变得远不那么嘈杂。

7.3 非平凡模式

我们已经审查了覆盖大多数机器学习应用的常规验证模式。有时,即使你使用它们的组合(例如,基于时间的验证与组 K 折交叉验证),它们也不足以反映已见和未见数据之间的实际差异。正如你所知,不充分的验证会导致数据泄露,从而导致模型性能估计过于乐观(如果不是随机的!)。

这种情况需要你寻找非常规的过程。让我们回顾一些。

7.3.1 嵌套验证

嵌套验证是在我们希望在学习过程中运行超参数优化(或任何其他模型选择过程)时使用的一种方法。我们不能仅仅使用排除的折或保留集,这些我们将需要用于最终评估来估计给定参数集的好坏。在拟合任何参数的同时访问测试数据的分数是直接导致过拟合的方式。

相反,我们使用折叠内折叠的架构。在每个外部分割中添加一个“内部”的训练数据分割来首先调整参数。然后,我们使用选定的超参数在所有可用的训练折叠上拟合模型,并对在超参数调整期间未见过的数据进行预测。因此,我们得到两层验证,每一层都可以有其特定的属性(例如,我们可能更喜欢内部层具有较低的方差,而外部层具有较低的偏差)。我们不仅可以应用嵌套到交叉验证中,还可以应用到时间序列验证和有序保留分割(或不同性质的混合架构)中(见图 7.5 和 7.6)。

图

图 7.5 嵌套交叉验证的示例

图

图 7.6 嵌套验证与混合架构的示例:外部循环的保留分割和内部循环的 K 折

7.3.2 对抗验证

与在标准保留集使用随机子样本数据不同,你可能更倾向于选择不同的路径。有一种称为对抗验证的技术,在像 Kaggle 这样的机器学习竞赛平台上非常流行。它通过应用一个机器学习模型来更好地验证另一个机器学习模型。

对抗验证数值估计两个给定的数据集是否不同(这两个可能是有标签和无标签数据的集合)。如果确实如此,它甚至可以在样本级别上量化它,这使得构建任意数量的彼此代表的数据集成为可能,提供了一种完美的估计工具。一个额外的优点是它不需要对数据进行标记。

算法很简单:

  1. 我们将感兴趣的数据库集合并(如果存在,则截断目标变量),将锚定数据集(我们想要表示的数据集)标记为 1,其余标记为 0。

  2. 我们在这个连接的数据集上拟合一个辅助模型来解决二元分类任务(因此 0 和 1 标记)。

  3. 如果数据集彼此代表,并且来自相同的分布,我们预计接收者操作特征曲线下面积(ROC AUC)接近 0.5。如果它们是可分离的(例如,ROC AUC 大于 0.6),那么我们可以使用模型的输出作为邻近度的度量。

注意,尽管这个技巧在机器学习竞赛中已经使用了很长时间(我们找到的第一个提及是在 2016 年,fastml.com/adversarial-validation-part-one/),但它直到 2020 年才成为更正式研究的一部分,当时它出现在 Pan 等人撰写的论文“Adversarial Validation Approach to Concept Drift Problem in User Targeting Automation Systems at Uber”中(arxiv.org/abs/2004.03045)。

我们可以在许多情况下使用这种分割。当我们检查标记和无标记数据集的相似性时,有一些问题我们应该记住。它们的分布有多不同?哪些特征是这种差异的最佳预测因子?分析由对抗性验证创建的模型可能回答这些问题。我们还将在此章节的第九部分中重用这项技术。

7.3.3 量化数据集泄露利用

我们在一篇由 DeepMind 撰写的论文中找到了一个有趣的验证技术,标题为“通过检索万亿个标记改进语言模型(2021;arxiv.org/abs/2112.04426),该论文提出了一种在下一个单词预测任务上训练的生成模型。

论文的作者通过将语言模型条件化于从大型语料库中检索到的上下文(基于与先前标记的局部相似性)来增强语言模型。该系统记忆整个数据集,并执行最近邻搜索以找到与最近句子相关的历史文本块。但如果我们尝试继续的句子几乎与模型在训练集中看到的句子相同呢?这似乎有很大的可能性会遇到数据集泄露。

作者们提前讨论了这个问题,并提出了一种值得注意的评估程序。他们开发了一种特定的度量来量化泄露利用。

通用思想如下:

  1. 将数据集划分为训练集和验证集,如通常的保留验证。

  2. 将两者都分割成固定长度的块。

  3. 对于验证集中的每个块,根据块嵌入从训练集中检索 N 个最近的邻居(这里我们将省略块如何转换为嵌入空间,但你可以找到论文中的详细信息)。

  4. 计算两个块中共同出现的标记的比例(他们使用与 Jaccard 指数类似的分数);这给我们一个从 0(块完全不同)到 1(块是重复的)的分数。

  5. 如果这个分数超过某个阈值,则从训练集中过滤掉这个块。

这种方法迫使模型从类似文本中检索有用信息并进行释义,而不是复制粘贴。你可以使用这个程序与任何现代语言模型一起使用。这是一个允许最小化数据泄露并增加数据集代表性的异国情调技术的良好例子。对模型如何应用有清晰的理解将帮助你开发自己的非平凡验证方案,如果标准方法不适用。

7.4 分割更新程序

我们在测试数据上花费的时间与在训练数据上花费的时间一样多。—— 安德烈·卡帕西

无论我们使用哪种模式,我们可能都会将其应用于动态变化的数据库。定期我们会得到新的数据,这些数据可能在分布上有所不同,并包含新的模式。我们应该多久更新一次测试集以确保我们的评估始终相关?

在为新数据设计分割更新程序时,我们可能希望达到至少两个目标。首先,我们希望我们的测试集能够代表这些新模式。从这个角度来看,评估过程应该是自适应的。

第二,我们想看到评估动态:模型在时间上是如何随着架构或特征的所有更新而变化的?为此,估计必须稳健。

以下是一些最常见的选项(见图 7.7):

figure

图 7.7 更新训练/验证集的常见选项。浅色数据块用于训练,而深色数据块用于验证。
  • 固定偏移量——当处理对时间和新颖性有强烈依赖的数据时,由于目标分布的剧烈变化,我们不会对一年前或更早的数据的性能感兴趣。相反,我们只想使用最近的数据进行验证。

    例如,我们将最后两周作为验证集(从最后完成的那天开始)并每天更新这个集合并重新训练用于评估的模型。

  • 固定比例——当处理图像或文本时,我们不会定期为新数据收集标签。与第一种情况相比,我们可能对数据的时效性没有强烈的依赖,这意味着新添加的数据可能并不比旧数据更重要。通常,我们在收到额外部分标签后,会扩展可用数据集。

    如果我们只将新标记的数据包含在训练集中,由于模型可用的数据更多,我们将增加指标。如果我们只将此数据包含在验证集中,模型可能会错过一些未见过的模式。最佳解决方案是保持训练集和验证集大小之间的比例不变,以便新添加的数据将相应地分割。

  • 固定集——有时,我们不想评估所有当前可用数据的平衡子集,而是想评估我们的模型质量在一个不变的“黄金集”上,这个集被用作基准。这种方法保证了两个模型在任何指标上都是可比较的,即使它们之间有一个很长的建模周期。

    这个固定集可以在建模之前从数据集中采样,或者手动挑选以包含各种困难案例和参考响应。它不应该按设计更新,以确保一致的模型比较。如果我们将来扩展这个黄金集,我们将将其视为一个全新的基准。

记住:我们应该在整个流水线上进行验证,包括数据集;在测试集上的推理应该与生产环境相同。如果我们想准确地对模型进行横向比较,我们应该以某种方式保存之前的数据集和模型的版本。数据版本控制和模型版本控制工具(如 DVC、Git LFS 或 Deep Lake)可能会有所帮助。

一旦这里的选择没有涵盖你的特定用例,你可能需要深入研究专门针对动态(非平稳)数据流和概念漂移的文献,以获得相关理论的全面概述(例如,“非平稳数据流中标签稀缺:综述” [mng.bz/gAXE])。我们还会在第十一章中简要讨论概念漂移问题,作为设置可靠验证方案不容易的一个潜在原因。

瓦列里·的篝火故事

当我在一家大型科技公司工作时,我们会在本地机器学习平台上训练多个机器学习模型来捕捉垃圾邮件发送者、诈骗者、抓取器和其他恶意代理。然而,该平台在评估验证集上的模型性能时只产生点估计。这最终成为一个问题,因为离线估计通常与在线性能有显著差异,导致大量误封用户或错误的期望。

为了说明点估计问题,让我们以抛硬币的例子来说明。

如果我们公平地抛掷一枚硬币 100 次,我们可以计算出它落地正面的次数。这就是我们的点估计。如果我们再次这样做,我们最终会得到另一个数字。如果我们说,在 100 次中有 95 次,我们预计这个数字将在 40 到 60 的范围内,这是一个置信区间。下限置信度为 40,这意味着我们预计在 95%的情况下这个数字至少为 40。

点估计缺乏稳健性,因为它没有考虑始终存在的不确定性,这很容易用图形来展示。以下图中的图表展示了使用相同阈值、机器学习分类器和由相同分布生成的离线数据生成的验证数据,两个指标(精确度和召回率)的方差。

侧边栏图片

样本大小等于 100,000 时的精确度和召回率分布;每个点代表一个独立的数据集。

侧边栏图片

样本大小等于 200,000 时的精确度和召回率分布;每个点代表一个独立的数据集。

侧边栏图片

样本大小等于 500,000 时的精确度和召回率分布;每个点代表一个独立的数据集。

当我们比较离线点估计和在线性能时,它们几乎总是相去甚远。即使在离线评估中,即使验证数据大小为 500,000,方差也非常大。这种情况缺乏稳健性,在整个系统中造成了脆弱性。

使用测试数据块,很容易展示精确度、召回率或其他度量指标的不确定性。尽管如此,还有更好的方法来做这件事。黄金标准将是带有替换的随机抽样,换句话说,就是 bootstrap。不幸的是,bootstrap 的计算成本非常高。对于每个 bootstrap 迭代(在 10,000 到 100,000 之间),我们必须采样长度为 N 的多项式分布(样本大小达到数千或数百万),并且需要重复 N 次。

这证明了一个问题。一方面,我无法使用平台提供的现有估计解决方案,因为它需要更加可靠和稳健。另一方面,将 bootstrap 集成到每个验证步骤中也是不可能的,因为这会使单个训练循环运行时间过长。

解决方案来自数学。假设我们独立地审查每个样本并并行运行 bootstrap。在这种情况下,我们可以从多项式抽样切换到二项式(n,1/n)并独立地对每个 bootstrap 迭代中的每个观测值进行抽样。当 N >> 100 时,具有 lambda 参数=1 的泊松分布成为二项式(n,1/n)的近似——换句话说,当 N >>100 时,二项式(n,1/n) ~ 泊松(1)。(更多详细信息请见mng.bz/OmyR。)

在泊松分布(1)中不存在 N,这使得它与数据大小完全独立,并且易于并行处理。这显著提高了速度(在我的情况下,通过一些额外的技巧,速度大约提高了 100-1,000 倍)。

一旦我们有了感兴趣度量指标的分布,我们就可以选择一个置信区间。在下面的图中,我们可以看到一个 99%的下限置信区间。平均而言,在 100 次中有 99 次,召回率不会低于 0.071。

sidebar figure

每个点都是一个 bootstrap 原始数据集的召回率分布;红色线是 99%的下限置信区间。

这里还有一件事需要考虑。一些度量指标,包括精确度和召回率,取决于我们选择的阈值来计算它们。以下图展示了添加了一些轻微噪声(均值为 0,标准差为-0.0125)的样本的精确度和召回率的分布情况。

很容易看出,应用噪声与否的结果差异显著,后者召回率增加,精确度降低。从某种意义上说,这些图证明了在这种情况下,决策边界边缘很窄且不稳健。将一些噪声作为超参数添加有助于提高对决策边界稳健性的信任,从而估计分布置信区间。

sidebar figure

样本大小为 200,000 时的精确度和召回率的分布;每个点都是一个 bootstrap 原始数据集,未添加噪声。

sidebar figure

以 200,000 个样本大小的精确度和召回率分布;每个点都是一个重采样的原始数据集,添加了噪声。

在给定的精确度/特异性下估计召回率并不是什么新鲜事,但结合泊松重采样和噪声添加,它创造了新的指标:在给定精确度下的召回率的重采样下限和给定特异性下的召回率的重采样下限。这些指标提供了保证(在特定置信水平内),可靠且稳健的机器学习模型性能估计。

sidebar figure

嵌入到原生机器学习平台中的指标

7.5 设计文档:选择验证方案

另一个设计文档块的时间到了,这次我们将填写关于 Supermegaretail 和 PhotoStock Inc.首选验证方案的信息。

7.5.1 Supermegaretail 的验证方案

我们从 Supermegaretail 开始。

设计文档:Supermegaretail

IV. 验证方案

i. 需求

在确定评估过程时,我们需要注意哪些假设?

  • 新数据每日到来。

  • 数据可能延迟最多 48 小时到达。

  • 新标签(销售单位数量)随新数据一起到来。

  • 近期数据对于预测任务来说更有可能是相关的。

  • 商品组合矩阵每月变化 15%。

  • 数据中存在季节性(周/年周期)。

尽管数据自然地分为类别,但这与验证方案的选择无关。

ii. 推断

在固定一个模型(在超参数优化过程中)后,我们在过去两年数据上对其进行训练,并预测未来四周的需求。这个过程在内循环和外循环中完全重现。

重要的是要注意,训练集和验证集之间应该有 3 天的差距,以应对数据可能延迟到达的事实。随后,这将影响我们在构建模型时可以和不能计算哪些特征。

figure

iii. 内循环和外循环

我们使用两层验证。外循环用于对模型性能的最终估计,而内循环用于超参数优化。

首先,对于外循环,鉴于我们处理的是时间序列数据,滚动交叉验证是一个明显的选择。我们设置 K = 5,以训练具有最佳参数的五个模型。由于我们预测 4 周后的数据,验证窗口大小在所有分割中也包括 28 天。集合之间存在 3 天的差距,步长为 7 天。

以下是一个外循环的示例:

  • 第一次外折:

    • 测试数据的时间范围是 2022-10-10 至 2022-11-06(4 周)。

    • 训练数据的时间范围是 2020-10-07 至 2022-10-06(2 年)。

  • 第二次外折:

    • 测试数据的时间范围是 2022-10-03 至 2022-10-30。

    • 训练数据的时间范围是 2020-09-29 至 2022-09-28。

  • 第五次外折:

    • 测试数据的时间范围是从 2022-09-12 到 2022-10-09。

    • 训练数据的时间范围是从 2020-09-09 到 2022-09-08。

其次,对于内部循环,在外部验证的每个“训练集”内部,我们执行额外的滚动交叉验证,分为三折。每个内部循环的训练样本还包括 2 年的历史数据,以捕捉年度和周的季节性。我们使用内部循环来调整超参数或进行特征选择。

以下是一个内部循环的示例:

  • 外部循环的第二层折叠:

    • 第二层外部折叠的训练数据是从 2020-10-03 到 2022-10-02。
  • 第一层内部折叠:

    • 测试数据的时间范围是从 2022-09-05 到 2022-10-02(4 周)。

    • 训练数据的时间范围是从 2020-09-02 到 2022-09-01(2 年)。

  • 第二层内部折叠:

    • 测试数据的时间范围是从 2022-08-29 到 2022-09-25。

    • 训练数据的时间范围是从 2020-08-26 到 2022-08-25。

  • 第三层内部折叠:

    • 测试数据的时间范围是从 2022-08-22 到 2022-09-18。

    • 训练数据的时间范围是从 2020-08-19 到 2022-08-18。

如果模型尚不需要模型调整,我们可以跳过内部循环。

图

iv. 更新频率

我们每周更新拆分,同时添加新的数据和标签(这样每个验证集始终包含一个整周)。这将帮助我们捕捉模型性能的局部变化和趋势。

此外,我们还有一个单独的保留集作为基准(一个“黄金集”)。我们每 3 个月更新一次。这有助于我们跟踪系统在长期内的改进。

7.5.2 PhotoStock Inc.的验证方案

现在我们将添加有关 PhotoStock Inc.验证方案的信息。

设计文档:PhotoStock Inc.

IV. 验证方案

搜索查询是验证的主要对象。在为 PhotoStock Inc.搜索引擎规划验证策略时,需要注意以下四个主要问题:

  • 验证集和测试集应代表生产数据;换句话说,它们应代表真实用户的查询。

  • 验证集和测试集应多样化;换句话说,它们应涵盖尽可能广泛的主题和上下文。

  • 同一用户的查询应只出现在训练、验证或测试集中,而不是多个集中,这样我们就可以避免数据泄露。

  • 应从数据集中删除重复查询以避免数据泄露。

因此,我们建议使用以下拆分策略:

  1. 按用户分组查询;每个查询只分配给一个用户。如果另一个用户有相同的查询,则忽略。

  2. 以固定的比例(待定;我们不知道什么比例是最好的,但我们可以从 90/5/5 开始)随机将用户拆分为训练、验证和测试集。

  3. 一次性将新用户分配到其拆分,然后不再更改。

随机分割分配应解决数据中的潜在分布偏斜问题。例如,我们可能猜测搜索中存在季节性效应(周末用户是业余爱好者,而工作日用户是专业人士),并且随着时间的推移存在一些分布漂移(新主题出现;旧主题消失)。随机分割应解决这些问题,尽管还需要额外的分析来确认这一点。

为了将分割分配给用户,我们建议使用确定性桶式方法:我们根据用户的user_id hash将用户分成桶,然后将每个桶分配给一个分割。这种方法是通用的,因为它允许分割比例在未来发生变化。例如,如果我们想增加验证集的大小,我们只需将更多的桶从训练集分配到验证集即可。

以下是一个桶式方法的示例:

def assign_bucket(user_id):
    _hash = sha1(user_id.encode()).hexdigest()
    return int(_hash, 16) % n_buckets

def assign_split(user_id):
    bucket = assign_bucket(user_id)
    if bucket < n_buckets * train_ratio:
        return 'train'
    elif bucket < n_buckets * (train_ratio + val_ratio):
        return 'val'
    else:
        return 'test'

在初始项目阶段,我们计划不添加更多子集(例如,“黄金集”),尽管我们未来不能排除这种可能性。

摘要

  • 将验证模式作为衡量模型预测能力准确性的方法。

  • 尽量避免重复使用相同的验证分割进行评估和搜索最优超参数,因为这可能导致偏差/过拟合和非稳健的结果。

  • 尝试设计一个验证模式以反映模型在实际中的应用。

  • 在寻找所需的 K 折数量时,应根据以下三个变量进行选择:偏差、方差和计算时间。

  • 要做到这一点,考虑数据在已见和未见数据之间的差异(是否有组、类别、时间或其他你应该考虑的基本属性)。

  • 如果有必要,设计一个非标准模式以适应特定问题。

  • 记住,不同的模式可以很好地协同工作以实现不同的目标。

第八章:8 基准解决方案

本章涵盖

  • 什么是基准?

  • 恒定基准

  • 模型基准和特征基准

  • 各种深度学习基准

  • 基准比较

一切都应该尽可能简单,但不能过于简单。—— 阿尔伯特·爱因斯坦

当我们开始思考我们未来机器学习(ML)系统的构建块时,它的核心部分,或者说核心组件,似乎是一个使用机器学习技术构建的模型。在某种程度上,这是如此真实,以至于我们甚至可能会认为:“这就是它:这是我应该花费大部分时间、精力和创造力的主要点。”

但在现实中,这可能会变成大多数机器学习项目陷入的陷阱,它们在从未达到生产阶段的情况下陷入困境。在机器学习系统的背景下,机器学习模型并不一定是最重要的东西,以及其设计文档。尽管诱惑很大,但你应该始终记住,花费大量时间、团队努力,更重要的是,金钱来构建一个酷、现代且复杂的 AI 模型,而这个模型却从未为用户和你的公司带来任何价值。在生产中的平庸模型通常比纸上的优秀模型要好。

这本书最初的一个版本标题是《有效的机器学习系统设计》,这与任何机器学习项目的首要目标相对应,即构建一个能够工作的系统;只有当它带来利润时,我们才会开始迭代改进它,逐渐增加其复杂性(如果需要)。在本章中,我们将讨论基准解决方案,这是使我们的系统生命化的第一步。我们将讨论为什么需要基准,以及构建它们的目的。我们将从恒定基准到复杂的专用模型,以及各种特征基准进行探讨。

8.1 基准:你是谁?

基准是系统中最简单(但可行!)的模型、特征集或其他任何东西的版本。在机器学习系统世界中,它是最小可行产品(MVP),从一开始就带来价值,而尚未深入复杂性。让我们通过概述可能同样适用于两者的关键目标来进一步阐述 MVP 类比:

  • 以最低的时间、成本和努力投入,降低产品的最大风险。在产品的生命初期,市场是否需要它,产品将有哪些用例,经济是否会收敛等等,这些都还不清楚。在很大程度上,这些风险也特属于机器学习产品。从某种意义上说,基准(或 MVP)是测试产品核心假设的最简单方法。

  • 获取早期反馈。这是将快速失败原则缩小到产品规模。如果你的机器学习系统的整个想法是错误的,你可以在早期阶段看到,重新思考整个计划,用新的知识重新编写设计文档,并重新开始。

  • 尽快带来用户价值。每个公司都希望通过让客户满意来创造收入。如果我们可以通过基线尽早为顾客带来价值,然后在逐步生成可预测收入的同时更新它,为什么不这样做呢?这将使方程中的每个人都很满意。

这三个点构成了基线和 MVP 之间相似性的基础。然而,还有三个纯粹是基线特定的目标:

  • 一个检查组件是否正常工作的占位符——基线就像烟雾测试。正如 Cem Kaner、James Bach 和 Brett Pettichord 在他们的《软件测试经验教训》一书中所说,“烟雾测试”这个短语来自电子硬件测试。你插入一块新板并打开电源。如果你看到板子上冒烟,就关掉电源。你不需要做更多的测试。

    首先,你需要检查系统是否工作,其次,它是否正确工作。要“编译”整个系统,你不需要一个强大的机器学习模型。你需要的是能够以所需格式预测某些东西的东西,可选地,基于某些东西。为什么不选择最简单可能的替代方案呢?

  • 一个用于比较的东西——我们是否可以进一步思考我们对模型的投资在未来能带来多少回报?基线是一个“基线”。它是坐标平面的起点——我们在某些指标上将其与新模型进行比较。

    在工业界工作的时候,模型的性能并不是我们比较模型的唯一指标。其他方面还需要努力、可解释性、可维护性等等。我们将在第 8.5 节中讨论它们。

  • 一个回退答案——与 MVP 不同,当我们继续进行其第二版和后续版本时,我们不会完全丢弃基线。当它与复杂的模型并行存在时,这是一种良好的实践。当这个主要模型在预测时出现问题时,系统会切换到基线响应。

那么,一个精心选择的基线有什么优势呢?简单性自动带来很多优点:它稳健,不易出现意外行为和过拟合(由于自由度较少),易于构建和维护,对计算资源的需求不是太高。因此,基线易于扩展。作为额外的奖励,从非机器学习同事的角度来看,简单的模型更容易解释,并使理解底层发生的事情变得更加容易。这有助于增加对我们机器学习产品的信任,这在风险很高时可能是关键的。然而,简单性本身并不是目标,而是一个有价值的属性。

如果我们将我们的机器学习系统比作乐高模型,基线就是一个尽可能快地组装其他所有模块的机会。尽管如此,我们仍然鼓励您通过设计使您的系统尽可能模块化(即,“正交”)。这将使后续的更新更加容易,包括过渡到更复杂的模型和功能(初始设计并不决定您未来更新系统速度的快慢)。初始系统应该是简单且灵活的,而不是微不足道的或受限的,并包含基线。

尽管基线提供了许多优势,而且不需要太多,但它们的使用频率并没有像应有的那样高。不幸的真相是,复杂性更受欢迎。Eugene Yan 有一篇出色的文章,我们强烈推荐阅读。它叫做“简单是一种优势,但遗憾的是复杂性更受欢迎”(eugeneyan.com/writing/simplicity/),强调了很多人选择复杂性而不是简单的主要原因,包括:

  • 复杂性表明了努力。

  • 复杂性表明了精通。

  • 复杂性表明了创新。

  • 复杂性表明了更多功能。

这导致了复杂性偏差,我们过度赞扬并偏爱复杂的思想和系统,而不是更简单的解决方案。

当然,基线不是万能的灵丹妙药,在某些情况下,基线可能不是必需的,甚至是不相关的:

  • 准确性至关重要。在许多情况下,几个百分点的错误甚至不会被注意到。但如果我们无法承受质量的下降——例如,在某些医疗应用中,如癌症检测或处理自动驾驶汽车时——基线将是一个糟糕的救命绳索。在这种情况下,明确切换到手动控制可能是一个更好的主意。

  • 确定性很高。我们清楚地了解用户的需求(例如,基于竞争对手的经验),或者我们有实施相同系统的自身经验。在这种情况下,如果我们已经有了在实战中证明有效的计划,并且可以简单地复制粘贴系统,我们就不需要重新发明轮子,也不需要在逐步迭代上浪费时间。

  • 我们正在重建一个已经工作的系统。假设我们已经有了一个基于深度语义相似性模型(DSSM)架构的工作搜索引擎。整个流程已经实现并经过测试。因此,当它持续为用户提供价值时,就是从速度和准确性方面进行优化的时候——例如,通过切换到基于 Transformer 的模型。这不是考虑基线的正确地方,因为旧版本实际上就是一个基线。

然而,我们相信,尽管在某些情况下,早期的高复杂性可以找到合理的理由,但它不能成为默认的解决方案,因为它会激励人们使事情变得不必要地复杂;它鼓励“没有发明在这里”的心态,人们宁愿从头开始构建,即使这样做可以节省时间和精力,也会浪费时间和资源,而且往往会导致较差的结果。

正因如此,我们相信基线解决方案是首先要做的事情,在需要和可能的地方进行逐步改进。

8.2 常数基线

基线的良好隐喻是建造一座桥梁:有时你不需要一支桥梁建筑工程师团队、巨大的预算、计划或数年时间去建造它。有时你只需要一个稳定固定的日志。基线就是那个允许你以最小规模连接组件并解决给定任务的日志——在基线的情况下,一个临时、原始、易于构建的解决方案(见图 8.1)。

在详细说明之前,我们想要传达的想法很简单:首先构建一个精简、可操作的机器学习系统,然后再对其进行改进。将可能的解决方案的复杂性视为一个连续体。根据努力-准确性权衡,在这个范围内选择一个合适的初始点,然后继续前进。除非必要,否则不要在建模上花费太多时间。

figure

图 8.1 在构建复杂模型之前,先从一个原始基线开始,这可能是你未来机器学习系统的最合适的基础。

记住这个类比,让我们从最斯巴达式的解决方案开始讨论,这些解决方案看起来像一根桥梁的木材。当我们开始寻找合适的基线时,我们经常问自己,“最直接解决这个问题的机器学习模型是什么?”或者“从哪里开始选择正确的机器学习模型?”但这些问题往往被证明是错误的。我们认为正确的问题可能是,“我们是否真的需要机器学习来解决这个问题?”

有时候我们甚至不需要机器学习来解决这个问题,或者至少我们不应该自己重新发明轮子,而可以使用第三方供应商。我们已经在第三章(第 3.2 节)讨论了这种替代方案。

但假设我们决定构建自己的模型。良好的建模始于没有任何模型:通过从解决方案空间中选择最简单、最懒惰的解决方案来尝试破解一个定义的指标。这将是我们问题的第一个近似。你可以争论说,一个常数基线本身就代表了一个模型。使用常数基线,我们通过一个常数来近似所有的依赖关系和交互作用。

为了立即给出我们正在谈论的内容,这里有一些你已经知道的例子:

  • 对于回归任务,常量基线是最后可用值(例如,对于相应的用户或项目)的平均或中位数预测(在时间序列预测中,你可以取最后一天/周/月/年的值)。此外,这也可以是用户定义的某个常量,该常量最大化了指标。

  • 对于分类任务,这将是通过主要类别进行预测(例如,在反欺诈问题中,我们可以假设根本不存在欺诈)或对正类概率的常量预测。

  • 对于排序,这可以是文档的随机顺序,或者基于无关的数值属性(如文档 ID)或简单的启发式方法(如“包含在项目描述中的查询关键字数量”)的排序。

在某种程度上,常量基线就像泰勒级数的第一项或梯度提升中的第一个基估计器的均值预测器(见图 8.2)。它们都不依赖于变量 x;它们已经(虽然粗略地)与我们的问题相关——不多,也不少。

figure

图 8.2 常量基线就像泰勒级数的第一项——这是为更复杂模型奠定基础的简单近似。

8.2.1 我们为什么需要常量基线?

建立这样的基线有两个目标。

第一个目标是基准测试。对于随机预测,获取所选指标的一个基线值是有帮助的。一个简单的合理性检查是将你的模型与简单的经验法则进行比较。确实,如果你花了两周时间进行艰苦的机器学习建模,然后最终在 5 分钟内实现了最简单的基线,并且这个基线击败了你的模型,那将是一件令人难过的事情。这听起来很荒谬,但在现实生活中这种情况相当普遍。

关于这个案例,有一个来自 Valerii 的有趣故事。他非常幸运,能够与一位既是一个出色的人也是一个伟大的专家的工程师一起工作。有一次,她通过仅使用常量基线——或者,如她喜欢纠正他的那样,逐步常量——赢得了预测某些工厂时间序列的机器学习竞赛。举办机器学习竞赛通常是一个非常直接的过程。参与者有一个标记的数据集和一个未标记的数据集。他们的目标是使用标记的数据集构建一个模型,该模型将对未标记的数据集进行预测,这些预测与实际值(仅对组织者可用)最接近。现在想象一下其他参与者们的挫败感,他们已经为几个月来工程化了数十个特征,并调整了他们梯度提升模型的参数。

这个案例激发了我们寻找并从最简单的模型开始,这是我们希望鼓励每个人去做的事情。不过,不要局限于它们。这将从一开始就让你对指标和目标值有一个更恰当的理解,这样你就可以对给定数据可以做什么以及不能做什么有一个愿景。

恒定基线的第二个目标是提供一个万无一失的回退方案。如果你的真实机器学习模型在运行时无法进行预测,由于某些错误、遇到响应时间限制、没有历史数据来计算特征(即新用户和新物品的冷启动问题)——或者它简单地变得疯狂(这有时会发生)——你的机器学习服务至少应该返回一些内容。因此,在这种情况下,恒定基线就足够了。

同时,我们可以轻松想象出一些情况,其中恒定基线过于原始,毫无价值。因此,最简单的可用基线应该更复杂,表现为一组启发式规则/正则表达式或浅层模型。恒定基线通常适用于简单的回归/分类问题,尤其是在表格或小型文本数据上;然而,使用恒定基线构建聊天机器人或语音识别系统是不可能的。

8.3 模型基线和特征基线

如果我们进一步沿着复杂度尺度前进,我们的下一个停靠点是规则基础模型,尽管在大多数情况下,我们无法在恒定基线和规则基础基线之间划出一条清晰的界限,因为我们可以将后者定义为一组之上的恒定值。但还有一个众所周知且具有说明性的规则基础基线例子:仅使用正则表达式开始解决自然语言处理问题。

几年前,Arseny 在一家出租车聚合公司工作,他参与开发了一个预测最近车辆到达客户所需时间的服务。问题很明显:如果我们预测过高,客户可能会决定不去等待并寻找其他服务;如果我们预测过低,客户会等待比我们承诺的时间更长,这意味着我们让他们失望。

Arseny 的同事,当时是一名高级工程师,将其视为一个标准的回归任务,并从像“总是预测 5 分钟”或“如果区域等于‘曼哈顿’:返回 4”这样的模型开始。简而言之:这些类型的基线很难被硬核机器学习魔法打败,而且讽刺的是,后者甚至作为回退方案在一段时间内投入了生产。

“如果区域等于 X 则返回 Y”模型是规则基础基线的优秀例子。我们可以通过取某些类别或几个类别的平均值/中位数/众数来生成类似的模型——在我们的例子中,通过位置取中位数。

随着我们沿着进步的方向前进,我们的模型变得更加复杂,并且能够找到更多对象属性与标签之间的联系。

在一个机器学习问题中,典型的基线序列可能开始于以下内容:恒定基线、规则基础基线和线性模型(见图 8.3)。只有当这些基线不足以满足我们的任务时,我们才需要更复杂和专业的解决方案。

figure

图 8.3 设计模型早期阶段的典型基线序列

例如,在构建推荐系统时,我们从一个恒定的检索开始,然后尝试协同过滤(例如交替最小二乘法),因子分解机,如果需要的话,最后是深度学习(例如深度结构化语义模型)。

无论你面临什么问题,都要注意这个领域的简单方法。它们不一定比更复杂的方法表现差。

一个生动的例子可以在 Maurizio Ferrari Dacrema 等人撰写的论文“Are We Really Making Much Progress? A Worrying Analysis of Recent Neural Recommendation Approaches”中找到,这篇论文获得了 2019 年 RecSys 最佳论文奖(arxiv.org/abs/1907.06902)。该论文因其揭示的令人印象深刻的数据而备受瞩目。研究小组检查了在过去几年顶级研究会议上展示的 18 个算法。在研究了这些算法之后,只有七个算法在合理的努力下可以重现。当发现其中七个算法中的六个通常可以使用相对简单的启发式方法(例如基于最近邻或图技术的方法)来超越时,事情变得更加有趣。唯一剩下的算法明显优于基线;然而,它并不能始终超越一个调优良好的非神经网络线性排名方法。

从已经提到的 Eugene Yan 文章中列出的非详尽例子包括以下内容:

  • 在大多数情况下,基于树的模型(随机森林、梯度提升)在表格数据上优于深度神经网络,尤其是在小型/中型数据集(例如,小于 100 万)上(arxiv.org/abs/2207.08815)。

  • 在组合图问题上,贪婪算法优于图神经网络(arxiv.org/abs/2206.13211)。

  • 在多任务学习问题上,简单的平均通常不比复杂的优化器差(arxiv.org/abs/2201.04122)。

  • 嵌入的乘积在项目推荐和检索中优于神经网络协同过滤(arxiv.org/abs/2005.09683)。

到目前为止,我们一直在谈论关注模型的基线。但特征呢?特征实际上是模型的一部分,有时甚至是模型最重要的部分。在经典机器学习中,我们必须手动构建特征,为基线选择特征是基于相同的原则;我们从一小组基本特征(最可能的是那些更容易计算的)开始。有两种方法可以添加新特征:

  • 构建新特征,这是一个具有挑战性和耗时的工作,需要构建新的 ETL 管道

  • 从已存在的特征派生新特征

我们需要尝试的基线特征序列应如下所示:原始的最小特征集,各种交互和计数器,然后是嵌入,接着是更复杂的东西(见图 8.4)。

figure

图 8.4 需要尝试的基线特征序列:从最简单到更复杂

从哪里开始的一组好的特征有哪些属性?答案是和模型完全一样,我们将在后面进一步讨论。

对于通常用深度学习方法解决的问题,可以使用简单的基线,通过浅层模型构建。正如我们回忆的那样,深度学习是表示学习的一部分,这意味着我们不是手工制作特征,而是将这项工作委托给神经网络。然而,对于一些像图像或文本分类这样的问题,你可以应用朴素的方法(基于规则或基于线性模型)。例如,在 BERT-like 架构出现之前,朴素贝叶斯在自然语言处理领域是一个非常强大的基线。对于计算机视觉,一些问题可以通过使用像素颜色的直方图(甚至只是均值/中值!)作为线性模型的特征来解决。话虽如此,对于大多数场景,从简单的深度学习模型开始——无论是在少量样本设置中的基础模型还是在训练好的模型——可能是一个更好的选择,因为这些模型已经在各种任务中证明了自己的能力。

阿尔谢尼曾经为候选人设计了一个带回家的练习题,其中他们得到了一个在简单图像数据集上解决异常检测问题的脚本。该脚本包含两个基线——一个使用神经网络,另一个使用颜色直方图——并且候选人被指示改进其中的任何一个以击败某些指标。这两个基线已经在一个相似的水平上表现,并且都实现得特别糟糕,因此候选人有改进的空间。大多数候选人更喜欢工作在更复杂的深度学习解决方案上,而只有其中经验最丰富的候选人注意到,只需对基于直方图的基线进行一行代码的更改,就可以达到所需的结果。

8.4 深度学习基线的多样性

当问题不是微不足道的,并且由于数据结构(这可以适用于大多数计算机视觉或语言处理问题)而建议使用深度学习时,基线的多样性略有不同。最常见的是重用预训练模型和训练/微调最简单的模型。

如果问题不是独特的,并且有一个在类似任务上训练过的模型,那么重用预训练模型是一种常见的做法。例如,如果我们想训练一个能够识别宠物品种的模型,我们可以重用在一个 ImageNet 数据集上训练过的模型。ImageNet 是一个包含 1,000 个类别的图像数据集,其中超过 100 个是狗的品种。所以,一旦你的目标是识别猫和狗,你就可以重用 ImageNet 数据集上训练过的模型,而无需重新训练。这对于许多通用问题(如语音识别、目标检测、文本分类、情感分析等)来说是一种常见的做法。

这种方法的稍微高级版本是重用预训练模型中的特征(也称为嵌入表示)来训练一个简单的浅层模型。例如,你可以使用在 ImageNet 数据集上训练的预训练模型,并使用其最后骨干层(在最终分类层之前)的表示来训练一个简单的线性模型,该模型将图像分类到自定义的类别集合中。这种方法在数据集较小且最终任务或多或少是平凡的(例如,分类)时特别有用,因此从头开始训练大型模型不太可能奏效。这种方法也被称为迁移学习的一个特例。我们见过一些案例,其中这样的基线实际上是无敌的,而且没有花哨的模型能够超越它。

使用预训练模型的一个更具体的版本是使用能够实现零样本或少量样本性能(意味着它们需要没有或少量训练样本来提供结果)的预训练模型或第三方 API。一个这样的 API 的华丽例子是 GPT 家族,但有许多 API 可用于不同的任务——例如,所有主要云供应商都有一个长长的 AI 解决方案列表,例如计算机视觉领域的 Amazon Rekognition 或 Google Cloud Vision AI;关于它们的详细信息超出了本书的范围。

使用主要供应商的第三方 API 作为基线有一个很好的副作用:它是谈判中的筹码,例如向大型企业销售软件产品或向潜在投资者证明初创科技是可靠的。潜在客户可能不知道给定问题的好指标是什么(回忆第五章),但将你的技术与 AWS 解决方案进行比较,可以恰当地界定问题。Arseny 至少知道三家使用过这种方法的公司,吹嘘他们如何击败了 Amazon、Google 和 OpenAI 等替代方案。在所有三个案例中,公司的声明都是合法的,这是预期的,因为主要供应商旨在提供一刀切解决方案,而初创公司可以提供更专业的 ML 系统,只做一件事就做得很好。

最后,如果这些选项都不起作用,你可以尝试从头开始训练一个简单的模型。然而,建议避免使用最新的最先进模型,而使用更简单且经过时间考验的模型。最新的模型在训练过程中往往更加“反复无常”,而一些较老的“明星”模型已经被深入研究,稳定的训练方法已知。这类模型的流行例子是用于视觉的 ResNet 系列和用于自然语言处理的 BERT 系列。我们个人的经验法则是从至少 2 年前的模型开始,但这不是一条严格的规定。它取决于系统所需的创新水平,如第三章所述。

值得注意的是,在“在预训练模型之上训练浅层模型”和“从头开始训练模型”之间有多个微调的层次。选择合适的微调程度非常具体,可能需要进行一些实验。例如,当基于 BERT 训练文本分类模型时,你可以逐渐复杂化训练范围:

  • 只训练最后一层。

  • 使用低秩适应(arxiv.org/abs/2106.09685)等适配器方法训练一些块。

  • 训练一些编码器块。

  • 训练标准化层。

  • 训练嵌入层。

  • 训练完整模型。

  • 训练完整模型 + 标准化器。

这种多样性导致了一个问题,“我们如何选择一个合适的基线?”

8.5 基线比较

让我们检查各种特征和模型基线,从最简单的开始。我们可以回答核心问题,即何时停止增加复杂性以及如何确定我们系统的合适基线。我们应该同时考虑多个因素;其中一些是

  • 准确性

  • 努力程度(主要是开发时间)

  • 可解释性

  • 计算时间

最基本的是模型准确度(或其他机器学习指标)与其所需努力之间的权衡。方程中的第一个组成部分是准确度。当你从恒定基线移动到基于规则的基线,从基于规则的基线到线性模型,或者从原始特征到它们的聚合和比率时,你已经开始感受到这些小变化对指标增加的影响。它是敏感的还是不敏感的?显著超越你的恒定基线有多难?投资更多时间尝试获得更高准确度是否合理?

在某种意义上,作为一个机器学习工程师,你通过从训练循环中获得“反馈”来进行反向传播,并使用其数据和解决方案空间中的准确度分布来更新你对问题的理解。

第二个组成部分是努力。通过“努力”,我们主要指的是时间和计算资源。没有机器学习项目有无限的预算和,因此,无限的时间。我们考虑实现一个新模型(或特征)、训练它、调试它和测试它所需的时间。你也应该注意可能出现的所有伴随的复杂性和陷阱,尤其是基础设施方面的。

让我们考察一个恒定的基线。实现它几乎不需要时间,但它提供的准确性最低。因此,我们将它在时间-准确性坐标中的(0,0)点进行映射(如图 8.5 所示)。

figure

图 8.5 简单的基线容易构建,但牺牲了最终系统指标(时间序列预测的示例)。

让我们看看线性模型。它需要更多的努力,但也很可能提供更好的准确性。我们可能会找到对应于右侧和高于上一个点的点,依此类推。另一方面,重要的是要理解,随着模型改进和演变(因此复杂性增加),成本-准确性比开始下降。这种效率下降的一个显著例子是前面提到的梯度提升。根据我们的估计和经验,梯度提升需要的输入比你在早期阶段使用的所有更简单的模型加在一起还要多,而准确性却没有显著提高。

我们应该估计尝试更复杂模型需要多长时间以及它可能提供多少额外的准确性。一旦我们了解下一步需要太多的努力而几乎没有任何显著的分数提升,我们就应该停止。这个“早期停止阈值”取决于具体的领域和问题。

但如果出了问题或者模型需要一些额外的变化呢?哪个模型更容易调试或更新?

  • 在光谱的左侧,我们有具有精确形式解的线性回归。

  • 右侧是一个具有复杂训练和推理管道的深度神经网络。

你更愿意面对哪一个?

维护,我们将在第十六章中更详细地讨论,包括调试实现的功能或模型所需的额外工作量。我们可以将维护视为更复杂基线所需的额外努力的一部分。

基线的另一个基本属性是计算时间。我们的模型及其特征的计算时间如何影响响应时间?我们的基线是否符合服务水平协议?它是解决方案空间的自然极限,尤其是在处理实时系统时吗?但即使没有实时需求,计算时间也决定了我们在未来更彻底的实验中迭代的速度。

最后,我们有可解释性。当处理任何 ML 系统的第一次迭代时,这个参数很重要,特别是对于其他队友。当我们处理敏感或医疗数据时,它也成为了一个安全问题,而不仅仅是模型预测的信任问题。一般的模式很简单:基线越简单,解释其工作原理就越容易。

我们将在第十一章中详细讨论这个话题。

8.6 设计文档:基线

只要基线可以成为您的设计文档的一部分,我们将为我们的虚构公司 Supermegaretail 和 PhotoStock,Inc.填补这个空白。

8.6.1 Supermegaretail 的基线

让我们从预测系统开始。在这里,季节性在选择预测模型时将是一个巨大的因素,所以我们不能不在我们的设计文档中考虑它。

设计文档:Supermegaretail

V. 基线解决方案

i. 常量基线

作为 Supermegaretail 需求预测系统的常量基线,我们计划使用每个 SKU 每个杂货的前一天的实际值。考虑到数据有时会延迟出现,以及杂货销售存在强烈的每周季节性,我们将后退 1 整个星期,而不是后退 1 天。因此,我们对 2022 年 9 月 8 日特定商品的预测将是该商品在 2022 年 9 月 1 日的实际销售价值。

ii. 高级常量基线

第五章提到了 1.5 分位数、25 分位数、50 分位数、75 分位数、95 分位数和 99 分位数的分位数损失。我们可以使用我们的基线使用年度窗口来计算相同的值。

iii. 线性模型基线

我们将使用一组基本特征来使用具有分位数损失的线性回归;一开始,我们只能使用目标变量,但带有多个滞后和聚合,如总和/最小值/最大值/平均值/中位数或对应于过去 7/14/30/60/90/180 天或不同大小的滚动窗口的最后几个分位数。同样的魔法也可以用于其他动态数据,如销售日期之外的价格、收入、平均账单或独特客户的数量。

iv. 时间序列特定基线

图

自回归积分移动平均(ARIMA)和季节性 ARIMA(SARIMA)都是用于预测的自回归算法;后者考虑了任何季节性模式。

这两种方法都需要调整多个超参数以提供令人满意的准确性。为了避免这种情况,我们可能更喜欢一种最先进的预测程序,它可以直接使用,称为 Prophet (github.com/facebook/prophet)。Prophet 的优点是它很稳健,不需要太多的预处理:异常值、缺失值、位移和趋势都会自动处理。

v. 特征基线

什么额外信息可以让一些基线和可能的未来模型受益?

我们将包括关于产品(品牌、类别)、商店(地理特征)和上下文(基于时间的特征、季节性、星期几)的额外静态信息——所有这些都有适合所选模型的预处理和编码。

适用于基线的特征还包括计数器和交互。例如包括

  • 当前价格与平均价格之间的差异(绝对和相对)

  • 渗透率:产品销售额与类别(1 级、2 级、3 级)销售额的比率,对于不同大小的滚动窗口

  • 自上次购买以来的天数

  • 独特客户数量

8.6.2 PhotoStock Inc.的基线

现在我们转向 PhotoStock Inc.的案例,在那里我们正在构建一个高级搜索引擎,旨在提供更好、更准确的结果,并最终增加销售额。

设计文档:PhotoStock Inc.

V. 基线解决方案

我们建议对 PhotoStock Inc.搜索引擎问题中的基线模型采用三种方法。

i. 非机器学习解决方案作为基线

目前,PhotoStock Inc.为其搜索引擎使用的是一个简单的非机器学习解决方案。它是一个基于关键字的搜索引擎,使用 ElasticSearch 数据库,可以进行模糊搜索。它不需要任何训练,并且已经部署到生产环境中,因此它是一个很好的基线模型候选。

尽管如此,它有两个缺点:它不使用图像进行搜索,只使用元数据(例如,标签、描述等),并且将其嵌入到新的机器学习管道中进行比较并不太容易。然而,将其作为基线模型仍然非常有用,因为它将允许我们比较机器学习模型与非机器学习解决方案的性能。

ii. 简单的机器学习解决方案作为基线

沿用之前的例子,我们可以使用一个简单的机器学习模型作为基线。它将不会使用图像,而只使用元数据。这样的模型可以使用查询和元数据作为原始输入,使用朴素词频-逆文档频率(TF-IDF)向量器将它们转换为特征,然后使用简单的线性模型来预测相关性得分。除此之外,它易于实现和训练,其卓越的简单性有助于早期阶段的调试和理解问题。

iii. 预训练模型作为基线

最后,我们可以使用预训练模型作为基线。鉴于问题的起源,我们需要一个能够统一视觉和文本领域的解决方案,最著名的是 CLIP (openai.com/index/clip/)。CLIP 于 2021 年发布,并在各种任务中证明是有用的。还有几个 CLIP 的后续版本可用,如果需要,可以在未来的迭代中对其进行审查。

CLIP,简而言之,是一个图像编码器和文本编码器,经过训练以预测哪些图像与适当的文本描述配对。它在一个庞大的数据集上进行了训练,因此在各种任务上表现出合理的性能。CLIP 是开源的,并且根据 MIT 许可证分发,因此可以用于商业目的。

为了使它适用于我们的用例,我们可以从使用其输出作为一对查询(图像)的相关性分数开始。这种方法不使用元数据和文本描述,因此它可以与先前的某种方法结合使用,或者进一步发展以使用两个组件的分数——例如:

relevancy_score = distance(query, image) + distance(query, description)

这两个距离都可以使用 CLIP 来计算——一个使用文本和图像编码器,另一个仅使用文本编码器。

作为第一步,我们可能完全避免任何训练。至于下一步,我们可以在我们的数据集上开始微调模型或其组件。

摘要

  • 将基线视为机器学习系统设计的一个基本点,因为它们有效地解决了技术、机器学习和产品相关的问题(连接组件、设置一个用于比较的指标,以及通过一个弱模型理解产品用户体验)。

  • 尽管基线被认为像 ABC 一样简单,但识别从哪些特征和模型开始的能力,却被低估了。

  • 随着你深入到你的项目,你的常见进展将倾向于以下进展:恒定基线,然后是规则基线,然后是线性模型,然后是更复杂的东西。

  • 虽然从一开始就构建一个复杂的模型可能很有吸引力,但始终考虑从恒定的基线开始;这将节省你的资源和时间,并指出你是否以最低的成本朝着正确的方向前进。

  • 当一个问题暗示了使用深度学习时,最常见的方法是重用预训练模型,训练/微调最简单的模型,或者如果前两种方法都不奏效,从头开始训练一个简单的模型。

  • 在选择各种基线选项时,应将准确性、努力程度(主要是开发时间)、可解释性和计算时间视为关键因素,其中准确性与努力程度的权衡尤为重要。

  • 随着模型的发展和复杂度的增加,准确性的成本不可避免地会降低。这尤其适用于转向梯度提升,实践表明,它需要的输入比所有之前的模型加起来还要多,而准确性的提升却并不显著。

第三部分 中间步骤

第三部分专注于中间步骤。第九章概述了学习曲线分析,提供了对残差分析的更深入了解,并有助于发现残差中的共性。第十章致力于训练流程,涵盖了我们可以用来构建和维护训练流程的工具和平台,并介绍了训练流程的可扩展性和可配置性等主题。第十一章讨论了特征和特征工程,分析了特征,为选择适合 ML 模型的适当特征提供了提示,并列出了特征存储的优缺点。在第十二章中,我们概述了测量和报告结果的方法,并讨论了进行 A/B 测试的好处。

第九章:9 错误分析

本章涵盖

  • 学习曲线分析

  • 剩余分析

  • 在残差中寻找共性

一旦我们组装了初始的构建块,包括收集第一个数据集、选择指标、定义评估程序和训练基线,我们就准备好开始迭代调整过程。正如神经网络中的反向传播计算最快损失减少的方向并将它从层到层传递回去一样,错误分析找到了整个系统的最快改进方式。

错误分析成为指导系统迭代更新的指南针。它帮助你在训练阶段(学习曲线分析)和预测阶段之后(残差分析)理解错误动态。通过分析这些错误,你可以识别出共性、趋势和模式,这些可以指导你改进机器学习(ML)系统。在本章中,我们将考察其关键阶段和类型,并提供我们希望有助于你更好地理解这一主题的示例。

错误分析在为机器学习系统设计时常常被忽略,这看似是一个合理的理由——因为这个步骤本身并不属于构建系统的过程。然而,在错误分析上花费的时间总是值得的,因为它揭示了系统的弱点,并提出了改进系统的方法。如果我们在这本书中省略这一步骤,那将是我们的一大失误。

9.1 学习曲线分析

学习曲线分析通过绘制和分析学习曲线来评估学习过程,显示了模型训练性能与所用训练数据量之间的关系。学习曲线分析旨在回答两个关键问题:

  • 模型是否收敛?

  • 如果是这样,我们是否避免了欠拟合或过拟合问题?

如果这两个问题都得到了否定的答案,就没有必要进行剩余的分析。

在我们深入细节之前,什么是学习曲线?这个术语起源于行为心理学,在那里它被用来显示在一段时间内观察到的个人或动物的学习进度(见图 9.1)。例如,我们可能分析受试者在每次新测试迭代中犯的错误数量,或者研究老鼠在迷宫中找到路径所需的时间与试验次数的比较。

图

图 9.1 学习曲线的基本表示

在机器学习中,学习曲线本质上是一种图形表示,显示了所选指标对特定数值属性(如迭代次数、数据集大小或模型复杂性)的依赖性。让我们简要地分解这三个属性:

  • 迭代次数—这种曲线描绘了训练过程中损失或指标的变化,有助于检查模型是否收敛。在某些资料中,它被称为损失曲线或收敛曲线。迭代的一个好例子是神经网络中的训练轮数。

  • 模型复杂度—这种类型的学习曲线显示了模型性能如何随着模型复杂度的变化而变化。随着复杂度的增加,模型倾向于更好地拟合训练数据,但可能开始对未见过的数据泛化不佳。模型复杂度的参数示例包括树深度、特征数量以及神经网络中的层数。

  • 数据集大小—这条学习曲线揭示了训练数据集中样本数量如何影响模型的表现。这有助于确定模型是否能够从更多数据中受益。

这些特性揭示了三种最常见的学习曲线类型。在深入探讨每一种之前,我们应该回顾“机器学习的圣杯追求”,即过拟合和欠拟合问题,有时也被称为偏差-方差权衡(见图 9.2)。

figure

图 9.2 随着模型参数数量的增加,训练误差趋于越来越低,同时最小化偏差。同时,模型方差增加,为我们提供了一个 U 形的验证误差。

9.1.1 过拟合和欠拟合

过拟合发生在模型在训练数据上表现出色,而在未见过的数据上表现不佳的情况下。通常,这是因为它对训练数据学得太好,变得过于专业化,无法泛化,过分关注新数据中不存在的细微细节和模式。

另一方面,当模型过于简单,错过了特征与目标变量之间的一些重要关系时,就会发生欠拟合,导致在训练数据和新的数据上表现都较差。

这两种情况都与偏差-方差权衡密切相关,这是模型复杂度与输入数据量之间的平衡。模型从数据中捕捉有用信号的能力越强,偏差就越低,过拟合的风险就越高。另一方面,减少方差需要降低复杂度,这会导致模型偏差增加。

偏差是由模型捕捉数据中有用信号的能力低而引起的错误。换句话说,模型倾向于对其关于数据的简化假设。当模型有偏差时,我们称之为欠拟合

方差是由模型对训练集中微小波动的过高敏感性引起的错误。模型在新数据上的泛化能力较差,从模型参数的角度来看,这些数据与训练集中看到的数据差异很大。通常,高方差是过拟合(假设系统其他部分没有问题)的主要原因。

一个好的学习算法应该同时最小化偏差和方差。然而,偏差-方差权衡却生动地展示了减少方差往往涉及增加偏差,反之亦然。这里的追求就是在这两者之间找到合适的平衡。这时,学习曲线分析就能为我们提供指导。

请记住,模型的冗余复杂性(即高方差)并不是过拟合的唯一原因。其他可能的情况包括

  • 数据泄露(在推理过程中使用不应知道的信息)

  • 噪声或高度细粒度的特征迫使模型捕捉无关的模式

  • 异常值的存在对损失函数有重大影响

  • 模型整体外推能力较差

  • 训练集和验证集简单地属于different distributions

不论是哪种情况,学习曲线都是检测欠拟合和过拟合的有效工具。掌握了关于过拟合和欠拟合的知识,我们就准备好去分析不同类型的曲线以及它们在这个追求中给出的提示。

9.1.2 损失曲线

当机器学习工程师听到“学习曲线”这个词时,首先想到的是基于学习迭代的损失曲线(也称为收敛曲线或学习曲线)。它显示了算法随着将越来越多的学习努力投入到任务中而不断改进的情况。

曲线在垂直轴上绘制损失(或指标),在水平轴上绘制训练迭代次数(或时期)。随着模型的训练,损失应该减少,理想情况下形成一个向曲线底部的下降斜率。

与学习曲线不同,其中 X 轴是数据集大小或模型复杂性(我们很快就会讨论),迭代曲线只需要一次训练运行,这使得即使对于大型数据集,当单次训练运行需要数小时甚至数天时,其跟踪也是实用的。损失曲线有助于在整个训练过程中保持对脉搏的把握。如果你只运行了 200 个训练时期中的 10 个,你就可以了解损失值是否按预期进展,或者是否存在使进一步训练无意义的问题。

一定要跟踪损失曲线,收集所有进行的实验的损失曲线,并使它们可用于未来的分析。在构建训练流程的早期阶段就将其纳入损失曲线监控是一个非常有价值的单次努力,因为您将需要这些洞察来用于所有未来的实验,并且对整个流程的可重复性有帮助(我们将在第十章深入探讨这个主题)。

9.1.3 解释损失曲线

损失曲线的行为存在几种主要模式,与设计期望不符。让我们简要分析每种模式,并看看我们如何解释在调试系统时可能遇到的不同模式(可以得出什么结论,以及应采取哪些步骤来调试检测到的问题)。

模式 1

模式 1 表明损失曲线发散(没有收敛到期望的损失值,而是振荡;参见图 9.3)。我们如何尝试使训练过程更稳定?考虑以下因素:

  • 检查特征和目标是否以任何方式相关——或者样本和标签是否按正确顺序传递给模型。

  • 降低学习率以防止模型在参数空间中弹跳。

  • 将数据集大小减少到单个批次(或 10-100 个样本),并检查模型是否能够过度拟合它们。

  • 从一个更简单的模型开始,逐步增加复杂性。每次,检查它是否优于恒定基线或基于规则的基线。

figure

图 9.3 损失在振荡,这表明没有收敛。

模式 2

模式 2 表明损失爆炸或趋向于 NaN(不是一个数字)(参见图 9.4)。这种行为表明,当梯度爆炸(在这种情况下,梯度裁剪、降低学习率或不同的权重初始化技术等解决方案可能有所帮助)或出现某些数学问题时(例如,除以零、零或负数的对数、或数据中的 NaN——因此,这通常表明实现错误或数据预处理不足)。

figure

图 9.4 模型在出现错误之前一直在收敛。

模式 3

模式 3 表明损失在下降,但指标是矛盾的(参见图 9.5)。如果模型根据损失继续改进,但指标停滞不前,这可能表明所选指标不适合该问题或实现不当。通常,这种情况发生在分类和其他相关任务中,我们使用包括一定阈值的指标。

figure

图 9.5 损失在下降,而指标保持恒定低。
Valerii 的篝火故事

当我在一家提供消息服务公司的公司工作时,我们的一项任务是改进现有的反垃圾邮件和反欺诈系统。我们面临的主要挑战是,在给定数据集上训练的模型在离线测试期间显示出有希望的指标,但在部署后并没有达到预期的效果。

在深入调查可能的原因后,我们发现存在三个主要问题:

  • 我们没有使用适当的指标进行离线测试。例如,像在定义的召回率下的精确度这样的指标对于欺诈检测并不那么有用,因为它们是类别敏感的(精确度),而在定义的召回率下的特异性则产生了更好的结果(参见第五章)。然而,召回率本身就像薛定谔的猫一样,因为我们从未完全了解欺诈的全貌(我们错过了欺诈案例,也不知道有多少);因此,我们的召回率只能基于已知欺诈案例的子集来计算。

  • 第二个问题隐藏在性能评估中。我们基于点估计进行评估,但现实往往偏离点估计,考虑到每天有 1000 亿个事件这一规模,即使是 0.1%的偏差也会导致与预期相比有显著的错误分类事件数量(1 亿)。

  • 第三个问题在于我们通过垃圾邮件/非垃圾邮件的二分类来评估系统,使用对数损失作为损失函数。我们真正需要的是让用户满意并将垃圾邮件减少到适当的水平(总是会有垃圾邮件,但有时并不是大问题,有时则是个问题),排除像在 1 秒内从单个号码接收 10 万条消息这样的情况。

虽然前两个问题具有挑战性但尚可管理,但第三个问题极其复杂,需要适当的指标层次结构、自定义损失函数以及持续实验。

这就是机器学习的本质:我们使用特定的损失函数训练模型,我们使用不同的指标集来衡量其性能,希望得到与第一和第二次不同的结果,迟早我们会遇到一个平台期,其中第一个甚至第二个元素有所改善,但第三个元素没有反应。

我们从这个案例中学到的最重要的教训是,尽管你可以调整你的错误分析直到达到完美,但如果一开始就错误地选择了指标,这并不能让你免于灾难。

模式 4

模式 4 显示了训练曲线的收敛以及意外的损失值。虽然训练曲线的曲率看起来很有希望,但观察到的值令人困惑。为了提前识别这样的异常,建议通过在一个批次上运行简单的单元测试来执行合理性检查,以确认损失是否在预期的范围内。通常,这种问题的原因在于缩放变换(例如,图像或分割中的掩模归一化)。

模式 5

figure

图 9.6 训练损失在下降,但验证损失没有下降,反映了潜在的过拟合。

模式 5 显示,训练损失在下降,而验证损失在上升(见图 9.6)。这是由于高方差导致的过拟合的经典教科书示例。在这些情况下,你应该限制模型的容量,要么直接减少其复杂性,要么通过增加正则化来实现。

9.1.4 模型级学习曲线

在我们确保模型收敛并且训练损失达到平台期,没有剧烈的过拟合或欠拟合之后,我们可以结束学习曲线分析并继续前进。这在初始部署阶段尤其相关。

然而,如果我们面临过拟合/欠拟合问题,或者有足够的时间实验最佳模型大小,那么第二种类型的学习曲线就派上用场了(见图 9.7):

  1. 首先,我们选择一个代表可变模型复杂性的超参数。再次,它可能是梯度提升中的树深度,正则化强度,特征数量,或者深度神经网络中的层数。

  2. 我们为这个超参数定义一个网格(例如,树深度的 2,3,4,……,16;正则化项的 10(-2),10(-1),1,10,10²,10³)。

  3. 我们训练每个模型直到收敛,并捕获最终的损失/指标值。

图像

图 9.7 基于学习曲线寻找最佳模型复杂性

现在,我们将这些值映射到垂直轴上,并将相应的超参数值映射到水平轴上。这个学习曲线帮助我们轻松地看到对于给定的数据,哪个模型复杂度范围(由这个超参数决定)是最优的。

9.1.5 样本级学习曲线

最后,让我们改变数据集的大小。我们在第六章第 6.4 节中详细讨论了这项技术。简而言之,我们保持验证集不变,并在训练集中探测不同数量的样本:100,1,000,10,000 等等。就像在模型级学习曲线中一样,我们训练模型直到其收敛,并绘制训练和验证学习曲线。

如果我们外推验证指标,我们可以估计需要多少新数据才能使指标增加 1%,反之亦然。如果我们预计收集 N 个更多数据样本,我们可以预测这将带来多少指标提升。

除了这种外推之外,样本级学习曲线还起到揭示过拟合和欠拟合的作用。具体来说,通过分析训练和验证曲线,我们能得到哪些见解?

  • 如果曲线几乎收敛(在最大样本数时曲线之间存在小的或没有差距),则模型泛化良好,无需向数据集中添加更多样本,因为这不会提高模型性能。

  • 具体来说,如果训练和验证曲线几乎收敛,但损失水平在两者中都保持较高,则报告高偏差问题(欠拟合)。在这种情况下,增加数据集大小也不会有所帮助。可能有益的是使用更复杂的模型。

  • 如果曲线之间存在较大差距,这表明存在高方差问题或训练集和验证集之间的简单差异。在前一种情况下,我们应该减少模型复杂性或收集更多数据来解决这个问题。在后一种情况下,我们需要检查数据拆分过程,并确保它公平地代表模型将遇到的现实世界场景。

按样本阶段的学习曲线表明当前系统中的瓶颈是否是数据量。理解指标对数据集大小的依赖性指导我们下一步改进系统,这可能包括收集更多数据和投入精力进行特征工程以及模型超参数调整。

9.1.6 双重下降

偏差-方差权衡在经典机器学习模型和适度规模的深度神经网络中像时钟一样运行。然而,对于现代过参数化的深度神经网络,事情变得更加复杂。

存在一种称为双重下降的现象,其中测试误差首先变好,然后变差,然后再变好。令人惊讶的是,研究人员发现与所有三个学习曲线相对应的不同双重下降阶段:按时间阶段、按模型阶段和按样本阶段。

双重下降背后的机制是什么,为什么它会发生,以及它是否意味着大型深度神经网络的重过拟合不是一个问题,这仍然是一个未解之谜。双重下降背后的常见假设如下:

  • 如果模型的容量(参数数量)低于数据集大小,它试图逼近数据,导致偏差-方差权衡发生的经典阶段。我们称这种模型为欠参数化

  • 在插值阈值处,模型有足够的能力完美地拟合训练数据并达到零偏差。在这个参数空间中实际上只有一个这样的模型。强迫这个模型拟合甚至稍微有噪声的标签将破坏其全局结构。

  • 然而,在过参数化阶段,有许多这样的模型。其中一些不仅插值训练集,而且在测试集上表现良好。结果发现,随机梯度下降由于我们尚未理解的原因导致这些“好模型”。

对于更详细的信息,我们建议阅读 OpenAI 的《深度双重下降》(openai.com/index/deep-double-descent/)。

大型神经网络的现代缩放定律重新定义了我们的建模策略。双下降现象可能会让那些只熟悉经典偏差-方差权衡的人感到惊讶,在选择系统模型时(尤其是在处理大型卷积网络和变压器时)以及确定训练和调试程序时,考虑这一点至关重要。

我们在这里只提到这一点,以强调之前描述的大多数启发式方法并不是一成不变的定律。就像机器学习设计中许多事情一样,它们揭示了信号——通常是很有用的信号——但可能并不完全适合特定的问题。

9.2 残差分析

当然,提出新想法很重要,但更重要的是理解结果。——伊利亚·苏茨克维

一旦我们确保了机器学习模型已经收敛并且没有受到欠拟合或过拟合的困扰,模型调试的下一步就是进行残差分析。这涉及到研究模型做出的单个预测与其相应的真实标签之间的差异。残差分析包括计算预测值和实际值之间的差异,称为残差(见图 9.8):

residual[i] = y_pred[i] – y_true[i]

首先,残差究竟是什么?在狭义上,残差仅仅是回归中预测值和真实值之间的差异。在广义上,残差可以是模型预测和真实值之间的任何样本级差异或误差。

figure

图 9.8 基本情况:单个回归器 x。每条垂直线代表一个残差误差(e)或简单地说是残差。回归器上方的线带有负号(实际值高于预测值),而回归器下方的线带有正号。

因此,为了将残差与特定的损失函数对齐,你可能更愿意使用损失总和的单个项作为伪残差,而不是原始差异,这些差异对于均方误差是平方误差,对于均方对数误差是对数误差,对于 LogLoss 是类别标签乘以该类别的预测概率。

通常,残差仅与回归和分类任务相关联。但回归和分类任务之外的残差又如何呢?从更广泛的角度来看,我们几乎可以在任何与机器学习相关的任务中找到等效的工具。

例如,在搜索引擎的背景下,真实标签通常是从搜索查询到前 N 个最相关文档或产品的映射。为了计算这种背景下的残差,我们可以测量模型预测的排名与列表中每个项目的真实排名之间的差异(见图 9.9)。

figure

图 9.9 图像分割问题的残差示例(图片来源:arxiv.org/abs/1810.13230)

在图像分割中,我们可以计算每个图像的预测和真实掩码之间的差异,这会产生二维残差,突出显示哪些对象的部分没有被掩码覆盖或被错误地覆盖。

9.2.1 残差分析的目标

残差分析有助于识别模型所犯错误中的模式,以便我们可以检测到改进系统的明确方向。与模型的整体误差通常用一个单一的数字表示,如损失或度量不同,残差分析则相反。它检查预测值和真实标签之间的原始差异,为模型性能提供更细致的诊断。

此外,残差分析还有其他主要目的:

  • 验证模型假设。 首先,它挑战了我们关于模型的基本假设。残差是否遵循正态分布?模型的预测是否存在偏差?如果我们发现任何显著的差异,我们可能需要重新评估我们的方法或选择不同的模型。

  • 检测度量变化的原因。 整体性能可能提高或保持不变。无论如何,捕捉度量分布的显著变化是可能的。哪些数据样本在不同模型中显示出不同的残差模式?在哪个数据子集中,我们错误答案的数量最多?哪些样本对最终分数的影响最大?

  • 确保残差的公平性。 残差分析使我们能够评估模型是否公平地对待每个样本,并在不同的群体中具有相同的分布。如果我们发现任何显著的偏斜或不平等,我们可以做出相应的调整,以确保模型无偏见并平等地对待所有样本。

  • 执行最坏情况和最好情况分析。 最大的 N 个残差样本之间是否存在共性?我们应该改变什么,以便我们的模型在这些情况下表现更好?对于最小的 N 个残差样本呢?

  • 检查边界情况。 对于具有最短或最长历史记录的用户,或者根据我们解决的问题,最短/最长的音频记录、文本和会话,我们的模型表现如何?它如何处理价格最低/最高的项目、零库存或最高收入的项目?我们必须熟悉业务案例和数据性质,以评估所有可能的陷阱。

这些问题是残差分析的核心,找到这些问题的答案就关闭了离线评估的反馈循环。我们越早开始收集进行实验的硬样本(和损失曲线),就越好。在设计文档中,收集训练后具有最大残差的 10 到 20 个对象作为附属工件是一种好习惯。很难高估在设计文档的训练流程中思考这些步骤的价值。

在项目的后期阶段,我们将它转化为每个训练模型的自动报告的一部分,包括模型漂移监控和数据质量报告。假设大多数情况下指标有所增加,但在一个关键部分略有下降。根据我们的政策,我们可能要么拒绝这个版本,要么在未来的迭代中额外关注这种变化。

9.2.2 模型假设

无论我们训练哪种模型,我们对其预测、偏差和残差分布都有先验假设。假设检查帮助我们确保我们选择了正确的模型,收集了足够的数据,并构建了正确的特征。如果假设揭示出意外的模式,可能会促使我们探索替代解决方案。

再次,从设计角度来看,我们需要提前确定我们对模型预测或,具体来说,残差的真实假设是什么,并通过相应的单元测试来表达。这将防止在下次部署后出现意外的模型行为。在下一章中,我们将深入探讨测试的更全面概述及其在训练流程中的作用。让我们探索两个不同的例子,看看假设是如何应用的。

示例 1. 线性回归假设

假设我们使用简单的线性回归来解决需求预测问题。我们在这里做出了哪些关键假设?

  • 线性**—预测变量(x)和目标(y)之间的关系是线性的。

  • 严格外生性**—残差应该是零中心的。

  • 正态性**—假设残差是正态分布的。

  • 同方差性**—残差的方差对于任何 X 的值都是相同的。

  • 独立性**—残差误差项应该是独立的。

在拟合我们的模型之后,我们检查这些假设是否成立。潜在问题包括

  • X-Y 关系中的 非线性

  • 偏差 在残差中

  • 异方差性: 错误项的非常数方差

  • 存在具有 极高影响 的数据点:预测值(y)或回归变量(x)中的异常值

为了检查回归假设,我们将检查残差的分布。为此,我们以四种不同的方式绘制残差,并构建所谓的 诊断图(见图 9.10 和 9.11):

  • 残差与拟合**—用于评估线性关系的假设。一条没有明显模式的水平线表明存在线性关系,这是有利的。实线和虚线之间没有差异意味着强烈的线性依赖。

  • 正态分位数-分位数(Q-Q)图**—用于检查残差是否正态分布。我们将标准正态分布的分位数作为 x 坐标,将标准化残差(减去均值并除以标准差后的残差)的分位数作为 y 坐标。如果得到的点接近直线(图上的虚线),则残差遵循正态分布。

  • 尺度-位置**—用于评估残差中方差的一致性。一条水平线且点均匀分布是同方差性的强烈迹象。在我们的例子中并非如此,我们有一个异方差性问题(拟合值越高,方差越大)。

  • 残差与杠杆率**—用于识别影响较大的案例,即可能影响回归结果时包含或排除分析中的极端值。杠杆率指的是如果我们从数据集中移除特定观测值,回归模型中的系数会改变的幅度。有一个常用的测量影响数据点的指标,称为库克距离(Cook’s Distance)。

图

图 9.10 两种情况(情况 1:假设成立)下线性回归残差分析的四个诊断图

图

图 9.11 两种情况(情况 2:假设不成立)下线性回归残差分析的四个诊断图

有时通过直接将我们的先验知识纳入模型,强制模型更严格地遵循我们的假设是有益的。

示例 2. 注意力图

想象一下,你是银行应用程序的产品所有者,你的下一个重大更新是添加语音助手。在调查了你的“内圈”专家后,你聘请了 Stacy,一位在文本到语音(TTS)系统方面世界级的专家。经过几周的工作,Stacy 构建了一个语音合成系统的第一个版本。

在 TTS 任务领域,存在一个基本假设:文本中字符的顺序应该在对应音频段中随时间线性增长。当我们阅读文本时,自然会假设文本的位置与听到的音频紧密对齐。这与其他序列到序列任务形成对比,例如机器翻译,在这些任务中,需要一个注意力模块来解决不同句法或标记顺序的语言之间的词对齐,例如英语和中文。

为了评估这个假设的有效性,Stacy 使用了一个注意力图——一种视觉表示,描述了音频帧(x 轴)和字符(y 轴)之间的激活映射。通过在训练过程中定期观察这个图,Stacy 旨在评估它多么接近一个几乎对角线的矩阵。

为了使注意力矩阵呈现出接近对角线的模式,Stacy 采用了一种称为引导注意力的技术。每当注意力矩阵与对角线显著偏离时,它就会使用辅助损失进行惩罚。这种启发式方法不仅加速了训练过程,而且从一开始就引导模型朝着与潜在假设一致的有意义解决方案发展(见图 9.12)。

图

图 9.12 无(左)和有(右)引导注意力损失的注意力图训练演变

注意力图与对角矩阵的偏差不过是残差。适度的残差是合适的:人们说话的速度有快有慢;因此,图表不会代表一条直线。然而,大的残差揭示了模型无法很好地学习的特定声音或字符组合。

带着这些知识,Stacy 可以制定一个即将到来的数据收集策略,以解决模型的困难并提高其性能。

要了解更多关于典型 TTS 网络架构的信息,包括此情况下注意力模块构建的细节,我们建议研究论文“基于深度卷积网络和引导注意力的高效可训练语音合成系统”(arxiv.org/abs/1710.08969)。

9.2.3 残差分布

如果模型的所有假设都不成立,那么这是一个调整训练流程、收集更多数据、工程新特征或探索替代模型和损失的信号。但我们是怎样确定必要的改进步骤的呢?猜测和检查的方法可能看起来很有吸引力,但我们不建议用它来探索解决方案空间。

例如,让我们违反相同线性回归的正态性假设(图 9.13-9.15)。

figure

图 9.13 在案例 1 中,当线性假设成立时,我们观察到正态的残差分布。

figure

图 9.14 案例 2 展示了当目标变量的对数正态分布导致线性假设不成立时的非正态残差分布(分布中存在明显的偏斜)。

figure

图 9.15 在案例 3 中,由于目标变量对回归器的非单调依赖性,当线性假设不成立时,线性回归存在非正态的残差分布。

在这个例子中,我们很幸运,立刻就看到了模型的问题:

  • 案例 2—我们似乎没有考虑目标变量的分布。像收入、销售额和价格这样的实体遵循对数正态分布,而回归模型最小化的均方误差或平均绝对误差(至少不是直接)不合适。为了克服这个问题,通常对目标变量应用对数变换有帮助。

  • 案例 3—残差形成多个簇。在这种情况下,转换目标变量将没有任何帮助。目标变量对特征依赖性不是单调的。在这种情况下,可以尝试一个能够捕捉非单调依赖性的模型,或者工程新的特征,帮助线性模型将非单调依赖性降低到单调甚至线性。

9.2.4 残差的公平性

在机器学习(ML)中,公平性是数据中不平等的一个指标。个体样本是如何贡献到损失或指标的?我们通过什么成本来增加指标?新的模型是在残差中增加不平等还是减少它?基本上,“公平性”是定义残差分布偏斜的另一个术语。

并非每一次指标的变化都意味着平等。一些改进在所有样本中均匀分布,而其他改进在一个阶层中增加显著增长,同时引起其他阶层的减少。残差分析中的“公平性”概念推动我们朝着更全面的模型评估程序发展,远超估计单一值指标。

为了更好地理解什么是公平性,请考虑图 9.16。

figure

图 9.16 公平与不公平的残差分布

在这个简化的例子中,我们有两个模型。它们都通过 20%降低了平均绝对误差(MAE)。然而,我们更愿意部署第一个模型,因为它在所有 10 个样本中均匀地减少了绝对残差。相比之下,第二个模型在一半的样本上大幅提高了指标,而在另一半上减少了指标。在这种情况下,我们在残差分布中增加了不平等,因此我们称这种分布为不公平。

评估公平性的一个定量方法,而不是完全依赖可视化,是使用经济学中的基尼指数(见图 9.17)。为了计算它,残差应根据它们的绝对值进行排序,然后绝对值的累积比例应除以残差数量的累积比例。

figure

图 9.17 基尼系数的图形表示:该图显示基尼系数等于标记为 A 的面积除以标记为 A 和 B 的面积之和——即,基尼系数 = A/(A + B)。它也等于 2A 和 1 − 2B,因为 A + B = 0.5(因为坐标轴的刻度从 0 到 1)。

对于完全公平(基尼系数 = 0.0),几乎所有残差对总误差的贡献相同。对于完全不平等(基尼系数 = 1.0),单个残差在窃取覆盖。这些是你在现实生活中很少会遇到的两极,而常见的值总是在两者之间。

关注公平性的两个主要原因。首先,我们希望模型在所有用户、物品或其他实体上都有高性能,而不仅仅是其中的一部分。这也包括在系统的每次迭代中逐步减少残差的整体不平等。

其次,我们不仅希望提高整体误差数值,还希望减少每个残差。如果我们通过提高搜索引擎质量 5%来减少预测质量,那么它将使用户的一些部分的预测质量降低 20%,这会损害用户体验。通过提高平均质量所获得的收益可能会被这些用户流失率的增加所抵消。

从长远来看,我们追求所有层级的接近相等增长。每条规则都有例外,公平性对于每个项目和每个指标来说并不那么关键。我们应该注意残差分布尾部的误差成本。这应该定义我们平均和样本改进之间的权衡。

9.2.5 低估和过度预测

在回归任务中,我们通常根据符号来分割残差——正残差表示过度预测(预测值大于真实值),而负残差表示低估预测。

根据我们要解决的问题,模型的一个或另一个偏差可能更受欢迎。例如,如果我们正在构建一个需求预测系统,错失的利润不如适度的过剩库存那么令人不快。另一方面,如果我们为银行预测客户的信用度,我们最好低估它而不是高估它。

因此,错误的成本往往是非对称的,它应该告诉我们应该最关注哪些符号和大小残差。

9.2.6 弹性曲线

需求特定错误分析工具之一是弹性图。它不是一个适用于任何机器学习系统的通用工具,但由于它对于定价相关应用至关重要,值得我们关注。尽管我们之前讨论了大多数曲线形状的分析方法,但这个例子属于这里,因为它可以被视为残差分析的一个特例。

需求预测背后的核心模型假设之一是价格-需求依赖性——价格越高,需求越低,反之亦然。这几乎适用于所有类型的商品(除非是一些特殊案例,如凡勃伦商品和吉芬商品,如果你还记得微观经济学 101 的话)。

弹性图是更通用概念“部分依赖图”的一个特例,其中我们改变一些特征并分析预测结果如何变化。它用于模型可解释性(我们将在第十一章中介绍)。

有两种方式来绘制我们模型的弹性曲线:

  • 使用训练数据(已知真实价格):

    • 取特定 SKU 的销售历史

    • 预测每个数据点的销售(Y)

    • 取每个数据点(X)的历史价格

    • 绘制 X-Y(价格→预测销售)依赖关系图

  • 使用合成数据和真实数据的混合:

    • 取特定 SKU 的最后价格

    • 将此价格乘以不同的系数(-20%,-19%,-18%,…,+19%,+20%)

    • 重新计算所有基于价格的特征,并预测每行的新销售(Y)

    • 绘制 X-Y(价格→预测销售)依赖关系图

正如我们之前提到的,在理想情况下,图表显示了反向依赖性:价格越高,销量越低。然而,如果这个图表显示了相反的情况,是嘈杂的(部分或全部非单调),或者有任何其他争议性的模式,它将表明以下情况之一:

  • 对于这个 SKU,我们没有足够的价格变化来捕捉其弹性(例如,销售历史很短)。

  • 这个 SKU 的销售非常随机。例如,这个 SKU 经常受到促销活动、季节性或其他外部因素的影响)。

  • 由于某种原因(“一个难题”),模型无法捕捉到它。

“更好”的图表(在负方向上更单调)意味着我们可以更多地依赖模型对这些 SKU 的预测。如果弹性“不好”,这表明预测不可靠,应该对这些“困难”SKU 进行进一步调查,而不是部署(见图 9.18 和 9.19)。

figure

图 9.18 需求弹性的理论示例

figure

图 9.19 需求无弹性的理论示例

不仅为预测销售绘制弹性曲线,还为实际销售绘制弹性曲线是有帮助的。了解这个特定的 SKU 是否显示出独特的弹性也很重要。如果它没有,我们不应该期望预测需求会有弹性。

如果你需要更多关于价格弹性概念的信息,我们推荐阅读文章“使用需求价格弹性进行预测”(mng.bz/GN5v)。

我们的一位朋友最近讲述了他使用弹性曲线应用的故事。他一直在处理一个定价问题,他们的解决方案实际上是一个美化的弹性图。他们构建了一个梯度提升模型,使用各种特征来预测销售数量,包括基于价格的特性和对不同可能价格的估计销售。他们的第一个模型揭示了一个令人惊讶的模式:图表看起来并不平滑,而是有一个明显的“梯子”状步骤。经过更深入的分析,他们意识到这些步骤的起源与使用的特征有关;连续变量被分割成低基数(例如,对于市场上所有商品的可能的全部价格,只有 256 个桶),这限制了模型敏感性。在增加桶的数量后,模型能够捕捉到更详细的模式,弹性曲线变得平滑,从而提高了整体系统性能。

9.3 在残差中寻找共同点

现在我们已经整体考察了残差分布,是时候研究残差子组中的模式和趋势了。为此,我们从两端来解决这个问题(见图 9.20):

  • 我们根据样本的残差将样本分组,并分析每个组中的特征。

  • 我们根据样本的特征将样本分组,并分析每个组中的残差。

figure

图 9.20 寻找残差共同点的两种方法:通过残差排名或通过样本属性选择子集

按值分组残差包括最坏/最好情况分析;它还涵盖了过度预测和不足预测问题。按属性分组残差产生分组分析和边界情况分析。

9.3.1 最坏/最好情况分析

最坏/最好情况分析的目标是定义模型运行良好的典型情况以及我们应该避免基于此模型做出决策的情况。在这些极端情况下,残差有什么共同之处?

Valerii 的篝火故事

回到前几章提到的案例,一旦我们在一个大市场中部署了一个基于预测需求的动态定价系统,我们就开始遇到它无法准确预测销售的情况。我们决定专注于剩余量最大的前 200 个产品。很快,我们就意识到最明显的问题集群:

  • 第一个集群围绕着最近添加到市场组合矩阵中的新产品,这个问题被称为冷启动问题。这些商品由于缺乏历史数据而难以准确预测,构成了挑战。很明显,仅依靠我们的机器学习模型在这种情况下是不够的。相反,我们需要开发启发式方法,利用同一类别内产品的销售升温来为预测建立一个坚实的基础。

  • 另一个集群来自电子设备类别,揭示了不同的困境。这些产品在时间上的销售稀疏,使得依赖我们模型的预测变得困难。意识到这一点,我们做出了一个关键的决定,将这批商品排除在我们的试点之外,并探索提高预测质量的其他方法。我们考虑了将模型分成更大类别的想法,相信这将更有效地捕捉每个组内的特定动态。

  • 然而,最大的偏差是由营销活动——**大促销、促销代码和折扣——引起的。模型难以解释这些因素,导致明显的不足预测,表现为大的负残差。为了纠正这种偏差,我们将促销日历纳入我们的特征集。通过这样做,我们使模型能够做出相应的调整,从而提高预测的准确性。

除了确定我们的模型不足的领域外,我们还研究了接近零的残差,以确定我们模型适用性的边界。这种分析帮助我们了解在哪些情况下我们可以对模型的预测有信心,在哪些情况下质量可能令人满意但处于可接受范围内,以及在哪些情况下依赖模型的预测变得风险。

通过检查这些残差模式,我们全面了解了我们的动态定价系统的优势和局限性,使我们能够就其推广做出明智的决定,并确保其适当的使用。

9.3.2 对抗验证

如果手动最坏情况分析不会提供新的见解,一个“用于分析机器学习模型的机器学习模型”可能会有所帮助。在第七章中,我们讨论了一个称为对抗验证的概念。它源于机器学习竞赛,用于检查两个数据集的分布是否不同。通常,我们会将带有标签 0 和 1 的数据集连接起来,进行训练和测试。

对抗验证可以轻松转移到残差分析的轨道上:我们将“好”样本数据设置为 0,“坏”样本数据设置为 1(例如,在第二种情况下,取最大的 N%残差)。我们应该尝试为我们的特定集合尝试不同的阈值。

算法的其余部分类似:我们在这些标签上拟合一个简单的分类器(例如,逻辑回归)并计算接收者操作特征曲线下的面积(ROC AUC)。如果两个类别是可分离的(曲线下的面积显著大于 0.5),那么我们分析模型的权重,这为我们提供了关于哪些确切特征最能区分我们的“最坏”案例的线索。

有时在最坏情况分析中找不到容易定义的模式。这是可以的。这意味着我们已经在模型改进空间中捕获了最大的低垂之果。

9.3.3 组分析的多样性

组分析能够识别各种群体、段、类别、队列或集群的残差中的独特模式和趋势。例如,在二进制和多类分类场景中,一种有效的方法是按类别(即目标变量)拆分残差,允许对每个组中的残差进行单独分析。

许多处理表格数据的典型应用,如欺诈检测系统,依赖于根据特定特征(如地理位置或流量来源)对样本进行分组。通过分析每个段内的残差,可以揭示模型预测中存在的共同偏差。这些见解可以指导通过纳入更多相关特征或调整现有特征的权重来进一步改进模型。

当处理文本、图像或视频形式的数据时,默认情况下可能没有明显的群体或分组。在这种情况下,一种替代方法涉及手动对一组 N 个残差进行分类,并为遇到的每个问题分配标签(例如,识别过暗或模糊的图像或标记具有特定措辞或风格的文本)。这个过程允许发现模型表现不佳的具体问题集群。因此,它提供了关于应收集哪种类型的数据以改进系统的指导。

9.3.4 边缘情况分析

边缘情况分析旨在测试模型在罕见情况下。通常,我们希望有一个基准,一个固定集合的已捕获的边缘情况,以便快速检查每个新模型的行为。

这里有一些关于我们在边缘情况分析期间可以检查的内容的想法:

  • 预测模型——历史短暂或无历史的用户/物品、动作/销售数量极多的用户/物品、特征 X 的最高和最低值、动作/销售稀有的用户/物品

  • 图像分割——低质量图像、低分辨率图像、高分辨率图像、遮挡和反射、异常光照条件、一张图像中的多个对象、图像中没有对象

  • 语言模型——最短和最长的文本、笑话、冒犯性话题、简单的算术、带错别字的文本、包含 N 种不同语言的文本、大量使用表情符号

  • 语音识别——最短或最长的音频、低质量音频、无语音音频、音乐而不是音频、发音过快或过慢的样本、嘈杂的环境、无声的语音、包含多个说话者的样本(即“鸡尾酒会”)

当最佳/最坏情况分析询问我们的模型在哪些数据上表现优秀或糟糕时,边缘情况分析和队列分析则询问模型在预定义数据子集上的性能。

阿尔谢尼的篝火故事

当我在增强现实公司工作时,系统的一个重要部分是基于深度学习模型的关键点检测器。任务是识别几个关键点,这些关键点后来被用来理解物体坐标。从一开始,训练流程就使用了适当的诊断工具,所以我们早期就发现某些损失较高的样本表现出一个共同的模式——某些图像包含镜子或其他反射表面(甚至雨天的一个水坑!),模型无法区分关键点与其反射。这意味着我们需要从系统中获取额外的属性:选择“真实”的物体,记住它,并忽略属于反射物体的关键点。对这一问题的早期理解帮助我们调整解决方案,以减轻反射关键点的情况。

侧边栏图像

模型检测到真实的脚,而不是反射

9.4 设计文档:错误分析

因为我们坚信错误分析应该是机器学习系统设计的基本要素之一,所以我们将在我们的设计文档中包含这一阶段。

9.4.1 超级大零售的错误分析

我们从超级大零售开始,我们将提出一种帮助公司实现其主要目标的方法——尽可能缩小交付和销售物品之间的差距,同时避免缺货情况。

设计文档:超级大零售

VI. 错误分析

记住,我们有针对目标值 1.5 分位数、25 分位数、50 分位数、75 分位数、95 分位数和 99 分位数的六个分位数损失,以及每个分位数对应的六个模型。常量基线根据产品过去 N 天的销售情况来估计每个产品的这些分位数。这些基线已经具有一些特定的残差分布和一些有用的特定偏差。

将更复杂的模型(线性模型和梯度提升)与这些基线模型进行比较,将帮助我们了解我们在建模和特征工程方面是否朝着正确的方向前进。

i. 学习曲线分析

i. 收敛分析

当我们开始尝试梯度提升算法时,基于迭代次数的逐步学习曲线才发挥作用。在检查损失曲线时,我们应该回答的关键问题是

  • 模型是否真的收敛?

  • 模型是否优于基线指标(分位数损失,平均绝对百分比误差等)?

  • 是否存在欠拟合/过拟合等问题?

一旦我们确保模型收敛,我们可以在一个粗略的网格(500-1,000-2,000-3,000-5,000)上选择足够多的树,并固定用于未来的实验。对于更简单的基线,不需要进行收敛分析。

ii. 模型复杂性

我们将使用模型级学习曲线来决定最佳特征数量和整体模型复杂性。

假设我们固定所有超参数,除了我们使用的滞后数:我们取的越多,模型可以捕捉到的复杂模式和季节性就越多——并且更容易过拟合训练数据。应该是 N - 1,N - 2,N - 3 天?还是 N - 1,N - 2,……,N - 30 天?最佳数量可以通过“模型大小与错误大小”图来确定。

同样,我们可以优化窗口大小。例如,“7/14/21/…”这样的窗口比“30/60/90/…”更细粒度。通过使用模型级学习曲线,我们可以选择适当的粒度级别。

以同样的方式,我们在初始调整期间调整模型的其它关键超参数——例如,正则化项的大小。

iii. 数据集大小

我们是否需要使用所有可用数据来训练模型?需要多少个月的数据足够且相关?我们需要利用所有(日,店铺,商品)数据点,还是可以下采样 20%/10%/5%而不会在指标上明显下降?

救援来了:样本级学习曲线分析,它决定了验证集上的错误达到平台期所需的样本数量。

我们应该做出一个重要的设计决策,即是否将(日,店铺,商品)作为数据集的对象,或者转向更粗粒度(周,店铺,商品)。最后一个选项可以将所需的计算量减少 7 倍,同时模型性能可以保持不变,甚至可能提高。

这个设计决策不仅会影响预测服务的速度和性能,还会影响整体产品(库存管理系统),极大地重塑了其可能的用例。因此,尽管可能有优势,这个决策应该与我们的产品经理、用户(品类经理)和利益相关者达成一致。

ii. 残差分析

记住我们有一个非对称的成本函数:过剩库存远比缺货问题危害小。我们可能面临过期商品或错失利润。未满足的需求问题是一个更糟糕的情况,从长远来看,它体现在顾客的不满和增加的风险,即他们可能会转向竞争对手。

i. 残差分布

提到的需求特性应指导我们在预测模型残差分析中的整个过程:正残差(高估)比负残差(低估)更受欢迎。然而,过度高估同样不好。

因此,我们绘制了残差的分布图,以及它们的偏差(原始残差中的简单平均值)。我们期望在以下可能的场景之一中这是正确的:

  • 小的正偏差表明略微高估,这是理想的结果。如果,此外,残差分布不广(方差低),我们就会得到一个完美的场景。

  • 在负向和正向方向上均匀分布的残差是可以接受的,但不如前一种情况受欢迎。我们应该迫使模型产生更乐观的预测,以确保我们最小化错失的利润。

  • 最糟糕的情况是我们对负残差有偏差。这意味着我们的模型倾向于增加顾客的不满。这肯定是对当前模型版本部署的一个红旗。

  • 如果我们有偏差但有利于正残差,这无疑是 Supermegaretail 的好案例,因此不如第一种情况受欢迎。

这些场景适用于我们试图估计无偏需求(我们使用中位数预测)时。但如前所述,我们还有其他几个模型用于其他分位数(1.5%,25%,75%,95%,99%)。

figurefigure

对于每一个模型,我们分析其背后的基本假设——例如:

  • 对于预测 95%分位数的模型,95%的残差都是正的吗?

  • 对于预测 25%分位数的模型,75%的残差都是负的吗?

ii. 弹性

我们应该使用弹性曲线验证弹性假设。对于所有商品是否都期望表现出弹性,没有明确的理解,这需要与利益相关者确认。

如果我们面临与弹性相关的问题,我们有两种选择来提高弹性捕捉:

  • 后处理(快速、简单、临时解决方案)——我们可以应用一个额外的模型(例如,等调回归)进行预测后处理以校准预测。

  • 改进模型(慢速、困难、通用解决方案)——这需要额外的建模、特征工程、数据预处理等。没有一组预定义的操作可以肯定地解决问题。

iii. 最佳情况 vs. 最坏情况 vs. 边缘情况

每次我们推出模型的新版本时,我们会自动报告其在最佳/最差/边缘情况下的性能,并将前 N%的案例保存为训练管道的工件。以下是一份清单草案,其中我们应在报告中找到答案:

  • 当一个物品的销售历史较短时,模型的预测误差是多少?残差主要是正的还是主要是负的?

  • 对于价格高或价格低的物品怎么办?

  • 预测误差如何依赖于周末/假日/促销日?

  • 几乎没有残差的物品之间有哪些共同点?是否必须要求它们有长期的销售历史?为了获得可接受的表现,销售历史应该有多长?模型是否需要其他条件,以帮助我们区分那些我们对预测质量有把握的情况?

  • 具有最大负残差的物品之间有哪些共同点?我们 100%希望排除这些案例或整个类别从 A/B 测试组或试点中。当我们开始改进模型时,我们也应该关注这些物品。

  • 具有最大正残差的物品有哪些共同点?

9.4.2 PhotoStock Inc.的错误分析

现在我们回到 PhotoStock Inc.,它需要一个现代的搜索工具,能够根据客户的文本查询找到最相关的照片,同时提供出色的性能并显示最相关的库存图像。

设计文档:PhotoStock Inc.

VI. 错误分析

为了能够尽早诊断潜在问题,我们应该从一开始就包括错误分析工具。在本节文档中,我们希望提前规划一些我们想要关注的部分。

i. 学习曲线分析
  • 应该启用损失曲线以进行合理性检查和进一步调整关键超参数,如早期停止阈值、学习率等。

  • 由于我们的损失是复合的(包含多个组件;参见之前的指标和损失部分),我们需要能够看到每个组件的损失曲线,以便调整其权重。

  • 应该能够在数据子样本上训练模型,以便稍后绘制样本大小学习曲线,并估计新数据如何提高整体性能。

  • 与损失曲线并行,应该有度量曲线以确保它们有公平的相关性。

  • 由于数据集可能被共享,我们需要能够看到每个分片(shard)的曲线。

ii. 残差分析
  • 对于每个训练周期,我们应该报告最有趣的样本,例如整体和每个组件损失最高/最低的样本。

  • 对于每个显示的样本,应该提供元数据,因此我们不仅报告搜索查询和相关的图像,还包括类别、标签、查询地理、查询语言和其他可能出现的属性。

在训练每个候选模型(被认为是足够好,可以用于实际系统的模型)之后,我们建议以下程序:

  • 样本 100 个高损失/指标的结果。

  • 对于每一个样本,建议提出一个简短假设来说明这个样本为何突出(例如,建议的图像模糊,图像描述过度优化,查询太短等),并按这些分辨率对这些结果进行分组。在计划系统改进的新步骤时,应进行进一步分析,因为它是一个重要的信号来源。

在未来,我们可以考虑在这里也应用可解释性技术,因为,在某个时候,诸如“为什么图像 X 在语义上与图像 Y 相似”这样的问题将会出现。然而,从当前的角度来看,它可以推迟。

摘要

  • 在设计你的机器学习系统时,不要犹豫应用错误分析,因为它将帮助你揭示其弱点并提出改进的方法。

  • 学习曲线分析是定义你模型效率的重要第一步。如果模型没有收敛,并且存在过拟合和/或欠拟合问题,就没有必要进行其他分析。

  • 过拟合或欠拟合的存在是模型复杂性与输入数据量之间可能不平衡的指标。

  • 根据你在错误分析期间观察到的损失曲线类型,你需要采取一系列行动来调试检测到的问题。

  • 设计用于计算预测值和实际值之间差异的残差分析对于验证模型假设、检测指标变化的原因、确保残差的公平性、执行最坏情况和最佳情况分析以及检查边缘情况至关重要。

第十章:10 训练管道

本章节涵盖

  • 训练管道的本质

  • 您可以使用哪些工具和平台来构建和维护训练管道

  • 训练管道的可扩展性和可配置性

  • 测试管道的方法

有一个经验法则可以区分经验丰富的机器学习(ML)工程师和新手:要求他们用一句话描述一个工作系统的训练过程。新手往往关注模型,而有些经验丰富的人会包括数据处理。成熟的工程师通常描述管道——最终产生训练好的 ML 模型所需的一系列阶段。在本章中,我们将以 ML 工程师的视角来分析这些步骤,并讨论如何相互连接和编排它们。

10.1 训练管道:你是谁?

想象一家小型比萨连锁公司。它在当地市场取得了成功,但那些理解软件正在吞噬世界(这句话来自马克·安德森的文章,a16z.com/why-software-is-eating-the-world/)以及一切都在数字化的老板们知道还有更多的市场份额可以抢占。因此,在 COVID 大流行之前,它就下注于数字化,雇佣了几位工程师来构建移动应用、简单的客户关系管理计划和多个内部软件系统。换句话说,这家公司没有科技巨头的规模或胃口,但它遵循主要趋势,并知道如何现在投资软件以在将来获得显著的利润。只需提及,它的应用帮助公司在 2020 年大流行中幸存下来。

现在,随着公司跟随趋势,人工智能热潮全面展开,雇佣 Jane 这位年轻而有潜力的机器学习工程师也就不足为奇了。她的兴趣在机器学习方面是无可否认的。入职后,CTO 将她的第一个问题交给她解决:构建一个 AI 驱动的助手,帮助比萨师傅对每个订单的比萨底部的组件进行基本视觉评估,如列出和计算组件数量。

在这家比萨公司中,软件开发生命周期至今尚未包括机器学习系统。因此,工程经理 Alex 要求 Jane 准备一个模型和一小段代码片段,展示如何运行它;内部系统团队将处理其余部分。

快进几个月后:Jane 收集了一个小数据集并训练了一个模型,在最初的测试中一切看起来都很正常,所以 Alex 的团队设法将其包装成一个服务。但在部署前,产品经理带来了多个新食谱,并说模型也应该能够支持这些。这并不复杂,只需要添加一些更多数据,更改标签映射,并重新训练模型——听起来好像不会对部署时间表产生太大影响。然而,经过讨论,Jane 和 Alex 认识到即使假设新的数据集已经准备好,这也需要额外几个月的时间。这里出了什么问题?Jane 执行了所有训练模型所需的步骤——手动验证数据集,在 Jupyter Notebook 环境中应用大量数据处理和清洗步骤,多次中断训练模型,临时性地与厨师和客户满意度团队验证结果,将训练好的模型上传到公司的共享存储,并将链接发送回 Alex。

注意:她做了所有正确的事情,但采取了一种临时的方法,没有适当的努力使这些步骤在一个单一的、透明的流程中可重复。

通过这个例子,我们想表明机器学习不仅仅是训练一个模型,还包括构建一个流水线,以便以可重复的方式准备模型和其他工件。在本章中,我们将讨论流水线中的步骤,如何编排它们,以及如何使它们可重复。

10.1.1 训练流水线与推理流水线

在机器学习领域,“流水线”这个术语被用于许多不同的上下文中。通常,人们将流水线视为一系列有序的步骤和过程。每个步骤都是一个程序,它接受一些输入,执行一些操作,并产生一些输出。一个步骤的输出是下一个步骤的输入。更正式地说,我们通常可以将流水线描述为步骤的有向无环图(DAG)。

要使事情更加复杂,模型本身通常是一个另一种类型的流水线。例如,一个简单的逻辑回归分类器通常通过一个特征缩放步骤来增强,从而形成一个至少包含两个步骤的流水线。通常,还会有基本的特征工程(例如,对分类变量进行独热编码),因此即使是最简单的模型也具有流水线的特性。其他模态,如图像、文本、音频等,需要额外的预处理步骤。例如,一个典型的图像分类模型是一个包含图像读取、归一化、调整大小以及模型本身的流水线。如果我们转向自然语言处理,流水线几乎总是从文本分词等步骤开始。总的来说,围绕“流水线”这个术语本身存在很多模糊和混淆的空间。为了使事情更清晰,我们将使用以下术语。

训练管道指的是用于训练模型的管道。它是一系列步骤的 DAG(有向无环图),这些步骤接收完整的数据集和可选的元数据作为输入,并产生一个训练好的模型作为输出。它比模型本身是一个更高层次的抽象(见图 10.1)。

figure

图 10.1 表示训练管道的 DAG 方案

推理管道指的是用于在生产环境中运行模型或作为训练管道一部分的管道(例如,使用梯度下降训练神经网络需要多个推理步骤,每个步骤都是一个管道)。它是一系列步骤的 DAG,这些步骤接收原始数据作为输入,并产生预测作为输出。它比训练管道是一个更低层次的抽象(见图 10.2)。

figure

图 10.2 表示管道中训练步骤的方案

在本章中,我们将重点关注训练管道,而推理管道将在第十五章中讨论。在最高层次上,典型的训练管道包括以下步骤:

  1. 数据获取

  2. 预处理

  3. 模型训练

  4. 模型评估和测试

  5. 后处理

  6. 报告生成

  7. 艺术品打包

让我们简要地分解每个步骤。

数据获取是管道中的第一步。它负责从源下载数据,并使其对后续步骤可用。如第六章所述,我们不将自己视为数据工程专家,因此不会详细讨论数据获取和存储。

预处理通常是管道中的第二步。这是一个非常通用的术语,对于不同的任务可能有不同的含义。一般来说,预处理是一组为准备数据以供模型训练而执行的操作。虽然我们为了本书的结构将训练和推理管道分开,但在实践中这种区分可能有些模糊。例如,你可以在训练模型之前完全预处理原始数据集,或者将其作为单个模型推理的一部分。在这种情况下,我们讨论的是特定于训练的预处理。特征选择是此类预处理的一个例子:我们只在训练之前执行它,并将选定的特征冻结在后续步骤中。模型训练是训练管道的核心。这是一个接收预处理数据并产生训练模型的步骤;它通常是管道中最长且最复杂的步骤(尤其是在基于深度学习的系统中)。

模型训练完成后,它可以被评估和测试。这些不同的方面旨在回答同一个问题:模型有多好?评估是计算指标的一个步骤,而测试是一系列检查,以确保模型按预期工作。

后处理是在评估和测试之后执行的一个步骤。它是一组执行以准备模型部署的动作。在这里,我们可以将模型转换为支持的目标平台格式,如果适用,应用后训练量化或其他优化,准备供人类评估的任务,等等。值得注意的是,后处理和评估可以互换。例如,我们可以在将模型转换为目标格式之前评估模型,或者相反,我们可以在将模型转换为目标格式后使用部署格式进行评估。

工件打包是流程中的最后一步。它负责将模型和其他工件(例如,包含预处理参数的配置文件)打包成易于部署到生产环境的格式。这里的目的是简化进一步的部署并分离训练和部署流程。理想情况下,输出应该尽可能与训练流程无关。例如,模型被导出为 ONNX 这样的通用格式以供后端服务或 CoreML 以供 iOS 服务使用,所有配置文件都导出为 JSON 这样的通用格式,并且训练流程中的任何变化都应尽可能少地影响部署。否则,部署流程将与训练流程紧密耦合,并在每次训练流程更新后需要许多更改,从而成为快速模型开发和相关实验的障碍。

报告是工件的一种特殊情况。这是一个通用术语,可以与许多事物相关联,包括包含验证/测试指标的基本表格、各种类型的错误分析(参见第九章)、额外的可视化和其他辅助信息。虽然这些工件不是直接用于部署的,但它们对于考虑训练成功至关重要;没有责任心的工程师在至少简要查看适当的报告之前不会发布新训练的模型。我们看到的唯一例外是 AutoML 场景的一种变体,当根据用户请求自动训练许多新模型时。在这种情况下,手动验证并不总是可能的;因此,工程师只能审查可疑的异常值。我们将在第十三章中讨论发布周期的话题。

一些这些工件与实验跟踪和可重复性相关,对于有多个贡献者或参与方的项目来说,这些是至关重要的。当研究人员独立研究他们自己的问题时,他们可以使用简单的笔记本或文本文件来跟踪他们所有的想法和实验。然而,当一组研究人员共同研究同一个问题时,他们需要一个更结构化的方式来跟踪、比较和重现他们的实验。为所有实验建立一个集中式存储库是帮助实现这一目标的工具之一。

10.2 工具和平台

与训练管道相关的工具和实践,以及机器学习系统设计背景下的推理管道、部署管道和监控服务,通常归因于机器学习操作(MLOps)。鉴于 MLOps 是一个相对较新的领域,平台和工具还没有形成良好的标准。其中一些相对容易识别(MLflow、Kubeflow、BentoML、AWS Sagemaker、Google Vertex AI、Azure ML),一些正在获得关注,而一些还处于早期开发阶段。

在这本书中,我们不希望突出任何特定的平台或工具,因此我们不会详细讨论它们。鉴于 MLOps 领域的变革速度,我们目前对工具和平台的理解在本书出版时很可能已经过时。相反,我们将专注于所有平台和工具共有的原则和实践。在最简单的情况下,你可以使用通用的非机器学习工具实现完整的训练管道——例如,通过创建一系列与 shell 脚本连接的 Python 脚本。然而,在实践中很少是这样:通常有一些特定的机器学习工具引入了抽象并简化了管道实现。大多数 MLOps 工具都是“有偏见的”,这意味着使用它们强烈暗示了特定的代码结构方式。从长远来看,这提高了训练管道代码的一致性,并使其在未来更容易维护(参见第十六章)。通常需要从训练管道平台获得的一些典型功能包括

  • 解决依赖关系—由于管道是一系列步骤的 DAG,因此解决步骤之间的依赖关系并按正确顺序运行它们很重要。

  • 可重现性—给定一组参数和管道版本(例如,由 git 提交指定),管道应该每次都产生相同的结果。

  • 与计算资源集成(如云提供商或 Kubernetes 安装)—例如,用户应该能够在特定的计算实例上运行作业(例如,具有 X 个 CPU 核心和 N 个 GPU 的虚拟机)或在一组实例的集群上。

  • 工件存储—一旦训练管道运行完毕,其工件应该可用。实验跟踪可以被视为此功能的一个子集。

  • 缓存中间结果—只要管道中的许多步骤都是计算密集型的,缓存中间结果以节省资源和时间就很重要。

除了功能之外,还重要的是要提及一些实践者从平台中需要的非功能性需求,包括成本效益和数据隐私(尤其是在医疗保健或法律等敏感领域)。

这里需要强调的是,功能不一定需要由同一个平台提供。例如,你可以使用一个通用平台来运行流水线,并使用一个定制的工具来进行实验跟踪,因为市场解决方案无法满足定制需求。有时这只是一个成本优化的问题。阿森尼曾在一家公司工作,该公司使用两种工具的混合体,因为其中一种提供了许多有用的功能,并且整体上为开发者提供了良好的体验,而另一种则与提供最便宜 GPU 的云服务提供商集成。在这种情况下,花一些时间进行集成并在训练成本上节省大量资金是合理的。

选择合适的工具取决于问题规模和公司的基础设施。FAANG 级别的公司通常拥有自己的机器学习平台和工具,能够在适当的规模上运行,而较小的公司通常更倾向于使用一套开源工具和云服务。每个工具都有自己的采用成本,因此选择适合特定问题的工具非常重要。据我们所知,没有一种适合所有情况的解决方案,这与许多其他更为成熟的软件工程问题不同(见图 10.3)。

figure

图 10.3 使用适当的框架进行训练管道通常是良好的实践,尽管有时保持简单就足够了。

10.3 可扩展性

可扩展性可能是某些问题的训练管道的关键属性。如果我们处理的是数千个样本的数据集,那么这并不是一个重大的问题,因为即使是单台机器也可能能够处理它。然而,当涉及到大型数据集时,情况就改变了。什么构成了大型数据集?这取决于问题和数据类型(1 百万张表格记录与 1 百万个视频剪辑相比微不足道),但如果我们必须选择一个标准“对于不适合单台机器 RAM 的数据集”呢?

当前数据集的大小不应与未来预期使用的数据集大小混淆。我们可能会遇到冷启动问题(见第六章),即使有数千个样本也可能在系统开发的初始阶段具有显著优势。然而,在未来,它可能增长几个数量级,如果你想要使用所有数据,你需要能够处理它。

虽然在大型数据集上训练模型没有一劳永逸的解决方案,但有两种经典的软件工程方法可以实现扩展。这些是垂直扩展和水平扩展(见图 10.4)。

figure

图 10.4 垂直扩展与水平扩展

垂直扩展意味着升级您的硬件或用更强大的节点替换训练机器。这种方法的最大的优点在于其简单性;增加更多资源(尤其是在使用云计算资源的情况下,这通常是情况)非常容易。然而,缺点是垂直扩展的局限性。比如说,您将机器的 RAM 加倍甚至四倍,并将 GPU 升级到最新一代。如果还不够,在垂直扩展方法内您能做的事情就很少了。

水平扩展涉及在多台机器之间分配负载。水平扩展的第一级是使用多 GPU 机器;在这种情况下,机器学习工程师通常需要修改管道代码,因为大部分繁重的工作已经由训练框架完成。然而,这并不是真正的水平扩展,因为我们仍在谈论一台机器。真正的水平扩展涉及使用多台机器并将负载在他们之间分配。如今,这种扩展通常也由框架提供,但这种方法更复杂,通常在实施过程中需要更多的工程努力。微软的 DeepSpeed、Hugging Face 的 Accelerate 以及起源于 Uber 的 Horovod 就是这类框架的例子。

一种特定的机器学习扩展方法是子采样:如果您的数据集过于庞大,对其进行子采样并减少所需的计算资源可能是合理的。最直接的子采样方法适用于大多数问题,涉及去除重复的样本。然而,还有更激进的方法:基于简单距离函数(例如,字符串的 Levenshtein 距离)的近重复样本下采样,以及基于内部 ID(例如,每个用户不超过 X 个样本)或人工 ID(例如,使用简单方法对整个数据集进行聚类并保持每个聚类不超过 X 个样本)的下采样。

在机器学习管道中,扩展通常需要更改其他管道参数:例如,批大小受 GPU 内存限制,较大的批大小会导致收敛更快,而学习率计划取决于批大小和预期的收敛计划。因此,改变参数的能力很重要。

10.4 可配置性

当机器学习工程师设计训练管道的可配置性时,存在一个光谱,每边都有两种不良做法:配置不足和配置过度。

配置不足意味着管道的可配置性不足,这使得改变模型的架构、数据集、预处理步骤等变得困难。事物在这里和那里以复杂的方式硬编码,很难理解管道的工作方式或改变最简单的方面。这是机器学习发展早期的一个典型问题。当管道小而简单时,理解和改变都很容易。因此,没有软件工程背景的研究人员可能会觉得引入适当的软件抽象是不必要的,从而导致越来越多的代码被无结构地添加。这种反模式在研究人员中很常见。

配置过度同样不理想。典型的机器学习管道有许多与数据集处理、模型架构、特征工程和训练过程相关的超参数。在现实中,很难预测所有可能的使用案例和可更改的参数,并且缺乏经验的开发者可能会试图覆盖所有可能的情况,并尽可能多地引入抽象。在某个时候,这些额外的抽象层只会增加复杂性。请注意,在本节中,我们交替使用“训练管道的参数”和“模型的超参数”。仅作提醒,超参数是在训练过程中没有学习到的模型参数,而是由用户设置的。

在以下列表中过度配置的代码示例中,我们可以看到多级子配置层次结构如何使事情复杂化。

列表 10.1 多级子编码层次结构
def train():
   ...
   batch_size = 32
   ... 
   learning_rate = 3e-4
   ...
   model.train(data, batch_size, loss_fn)
   ...   

^ example of underconfigured code: things are too rigid  

class Config(BaseConfig):
    def __init__(self):
        self.data_config = DataConfig()
        self.model_config = ModelConfig()
        self.training_config = TrainingConfig()
        self.inference_config = InferenceConfig()
        self.environment_config = EnvironmentConfig()

class DataConfig(BaseConfig):
    def __init__(self):
        self.train_data_config = TrainDataConfig()
        self.validation_data_config = ValidationDataConfig()
        self.test_data_config = TestDataConfig()

config = Config(
    data_config=DataConfig(
        train_data_config=TrainDataConfig(
            ...
        ),
        validation_data_config=ValidationDataConfig(
            ...
        ),
        test_data_config=TestDataConfig(
            ...
        ),
    ),
    ...
)

为了在两种极端之间找到一个良好的平衡,你应该估计各种参数被改变的概率。例如,数据集被更新几乎是 100%确定的事情,而模型内部的激活函数被改变的可能性并不大。因此,在忽略激活函数的同时,使数据集可配置是合理的。可变参数因各种管道而异,因此找到良好平衡的唯一方法就是考虑你会在接下来的几个月内认为的低垂之果的潜在实验。我们推荐的一个有助于基于深度学习管道的有用指南是由谷歌团队提供的:github.com/google-research/tuning_playbook

在初步决定哪些超参数可调整后,确定调整策略非常重要。当计算资源有限时,手工实验更可取。当资源充足时,应用自动超参数调整方法(如简单的随机搜索或更高级的贝叶斯优化)是有意义的。超参数调整工具(例如,Hyperopt、Optuna 和 scikit-optimize)可以是机器学习平台的一部分,并可能规定配置文件应该如何看起来。

根据我们的经验,广泛的超参数调整更适合小数据集,因为在合理的时间内可以运行大量的实验。当单个实验需要数周时间时,更实际的做法是依靠机器学习工程师的直觉手动运行几个实验。值得注意的是,使用小数据集进行的实验可能有助于建立这种直觉,尽管并非每个结论都能推广到大规模的训练运行中。

找到适当的方式来配置训练管道非常重要(见图 10.5)。

figure

图 10.5 管道需要适当的配置以实现最佳性能

最典型的做法可能是分配一个单独的文件(通常是用像 YAML 或 TOML 这样的特定语言编写的),其中包含所有可更改的值。另一种流行的方法是使用像 Hydra (hydra.cc/)这样的库。我们见过的一个反模式是将配置分散在具有相同参数的多个训练管道文件中,这些文件具有不同的优先级水平(例如,批大小可以从文件 X 中读取,但如果未指定,则尝试从文件 Y 中获取)。在实验阶段可能会出错,特别是如果实验是由不太熟悉这个特定管道的缺乏经验的工程师进行的。

10.5 测试

我们在机器学习管道中经常看到的一个常见问题是缺乏测试。这并不奇怪,因为测试机器学习管道并不是一件容易的事情。在构建常规软件系统时,我们可以通过运行它并检查输出来进行测试。然而,运行训练管道可能需要几天时间,显然我们不可能在每次实施更改后都再次运行它。另一个问题,如前所述,是机器学习管道通常配置不足,这使得它们难以单独测试。最后,考虑到可能的超参数数量,在合理的时间内测试所有可能的组合几乎是不可能的。简而言之,向机器学习管道引入测试是一项具有挑战性的任务。但这是值得做的!

适当的测试有三个目的:

  • 在引入更改的同时避免回归错误

  • 通过尽早捕捉缺陷来提高迭代速度

  • 改进整体管道设计,因为它迫使工程师找到适当的可配置性平衡

我们对测试机器学习管道的建议是结合对整个管道的高级别烟雾测试和对其最重要的单个组件的低级别单元测试。

烟雾测试应该尽可能快,这样你就可以在数据集的小子集上运行它,运行少量 epoch,也许使用模型的简化版本。它应该检查管道运行无误并产生合理的输出——例如,它确保在这个玩具数据集上损失正在减少。以下列表展示了训练管道烟雾测试的简化示例。

列表 10.2 训练管道的烟雾测试可能看起来像什么
from unittest.mock import patch, Mock
import torch
from training_pipeline import train, get_config

class DummyResnet(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.model = torch.nn.Sequential(torch.nn.AdaptiveAvgPool2d(1),
                                         torch.nn.Conv2d(3, 2048, 1))

    def forward(self, x):
        return self.model(x).squeeze(-1).squeeze(-1)

def test_train_pipeline():
    config = get_config()
    config["dataset_path"] = "/path/to/fixture"
    config["num_epochs"] = 1

    mock = Mock(wraps=DummyResnet)
    with patch('training_pipeline.models.Resnet', mock):
        result = train(config)
        assert mock.call_count == 1
        assert result['train_loss'] < .5
        assert result['val_loss'] < 1

这样的烟雾测试显著提高了迭代速度,从而简化了实验和调试。然而,也存在一个缺点。像任何集成测试一样,它们需要大量的维护工作。这是因为几乎任何重大的管道更改都可能影响代码。低级别的单元测试应该覆盖管道的各个组件。拥有几个这样的单元测试或甚至没有它们并不罕见——如果你没有它们,也没有什么可耻的。然而,我们建议至少覆盖最敏感的组件。这样一个敏感组件的例子可能是最终的模型转换——想象一下,模型是用 Pytorch 训练的,后来被部署到 iOS(并使用 CoreML 运行)以及后端(并使用 ONNX 运行)。确保模型被正确转换并且转换过程没有引入任何变化非常重要,这意味着转换模型的输出应该与原始模型相同。

10.5.1 基于属性的测试

另一组测试适用于训练好的模型,其灵感来源于基于属性的测试方法。基于属性的测试是一种软件测试方法,它涉及为函数或系统生成随机输入,然后验证对于所有输入某些属性或不变量是否成立。与编写具有预定输入和预期输出的特定测试用例不同,基于属性的测试侧重于定义系统应满足的一般属性,然后自动生成测试用例以验证这些属性。

在机器学习项目的背景下,基于属性的测试可以用来确保最终训练好的模型按预期行为并满足某些属性。以下是一些在机器学习项目中可以测试的属性示例:

  • 一致性**—给定相同的输入数据,模型应该始终如一地产生相同的输出或预测,无论执行多少次:

figure

  • 单调性**—在简单的机器学习模型中,输出应该相对于某些输入特征单调递增或递减。基于属性的测试可以用来验证模型的输出遵循预期的单调行为:

figure

单调性通常在各种价格预测模型中是预期的。例如,如果其他特征固定,房屋的价格应该随着其面积的增大而增加。

  • 变换不变性**—某些机器学习模型应该在输入数据的特定变换(如缩放或旋转)下保持不变。基于属性的测试可以用来检查当输入数据以特定方式变换时,模型的输出是否保持不变:

figure

其中 g 是一个预期的转换。这可能是对图像进行旋转或缩放,将实体更改为其同义词进行自然语言处理,改变声音的音量,等等。

  • 鲁棒性**—模型应能抵抗输入数据中的小扰动。可以使用基于属性的测试来验证当输入数据被小量扰动时,模型的输出不会发生显著变化:

figure

  • 否定**—当输入数据被反转时,模型应提供相反的预测。可以使用基于属性的测试来验证当输入数据被否定时,模型的输出是预期输出的相反。最简单的例子是情感分析,其中模型通常应该预测负面情感,如果将单词“爱”替换为“恨”:

figure

我们已经在 5.2.1 节中介绍了一个非常类似的概念。区别在于,在一种情况下,我们期望结果存在一些变化(并且我们想要测量它),而在另一种情况下,我们期望严格的致性(因此我们想要断言它)。使用一些数据样本作为固定值并为他们编写基于属性的测试是确保模型按预期行为并保持其可靠性的好方法。

通常不会在设计文档中包含一个精确的测试列表;然而,我们建议提前考虑它并在文档中提及。设计文档通常用作实现的参考,因此提及测试是有用的。

注意:如果您对机器学习测试感兴趣,我们推荐阅读 Arseny 关于该主题的深入评论的幻灯片:arseny.info/reliable_ML

10.6 设计文档:训练管道

随着我们继续为我们的虚构业务制作两个独立的设计文档的工作,现在是时候介绍 Supermegaretail 和 PhotoStock Inc.的训练管道了。

10.6.1 Supermegaretail 的训练管道

让我们看看 Supermegaretail 可能的管道可能是什么样子。

设计文档:Supermegaretail

VII. 训练管道

i. 概述

Supermegaretail 的需求预测模型旨在预测特定时间段内特定商店中特定商品的需求。为了实现这一点,我们需要一个训练管道,它可以预处理数据,训练模型,并评估其性能。我们假设该管道应该是可扩展的并且易于维护,并允许对各种模型架构、特征工程技术和超参数进行实验。

ii. 工具集

管道建议的工具是

  • Python 作为主要编程语言,因其多功能性和丰富的数据处理和机器学习生态系统

  • 用于并行和分布式计算的 Spark

  • PyTorch 用于深度学习模型

  • MLflow 用于跟踪实验和管理机器学习生命周期

  • Docker 用于容器化和可重复性

  • AWS Sagemaker 或 Google Cloud AI Platform 用于基于云的训练和部署

iii. 数据预处理

数据预处理阶段应包括

  • 数据清洗—处理缺失值、删除重复项和纠正错误数据点

  • 特征工程—从现有特征创建新特征,例如汇总销售数据、提取时间特征(如星期几、月份等)以及整合外部数据(例如,假日、天气和促销活动)

  • 数据归一化—将数值特征缩放到标准范围

  • 训练-测试分割—将数据集分割成训练集和验证集,确保它们在时间上不重叠,以防止数据泄露

iv. 模型训练

模型训练阶段应适应各种模型架构和配置,包括

  • 基线模型—简单的预测方法,如移动平均、指数平滑和自回归积分移动平均

  • 机器学习模型—决策树、随机森林、梯度提升机和支持向量机

  • 深度学习模型—循环神经网络、长短期记忆网络和转换器(如有需要!)

我们还应该实现一个超参数调整机制,例如网格搜索或贝叶斯优化,以找到最佳模型配置。

v. 模型评估

应使用我们之前推导出的指标来评估模型性能,例如 1.5、25、50、75、95 和 99 分位数指标,以及作为是和权重等于 SKU 价格的权重。它计算为点估计值,具有 95%置信区间(使用自助法或交叉验证),加上标准指标,如平均绝对误差(MAE)、平均平方误差(MSE)或均方根误差(RMSE)。我们还应包括针对 Supermegaretail 业务需求的特定自定义指标,例如过剩库存和缺货情况的成本。(见验证章节。)

vi. 实验跟踪和模型管理

使用像 MLflow 这样的工具,我们应该跟踪和管理实验,包括

  • 模型参数和超参数

  • 输入数据和特征工程技术

  • 评估指标和性能

  • 模型工件,如训练模型权重和序列化模型

vii. 持续集成和部署

训练管道应集成到 Supermegaretail 现有的 CI/CD 基础设施中。这包括定期设置自动训练和评估,确保使用最新数据更新模型,并以最小的人工干预将更新的模型部署到生产中。

viii. 监控和维护

我们应该监控模型在生产中的性能,并设置对预期性能的重大偏差的警报。这将使我们能够及早发现问题,并在必要时触发重新训练或模型更新(见第十四章)。

ix. 未来工作和实验

训练流水线应该足够灵活,以适应未来的实验,例如整合额外的数据源、尝试新的模型架构以及调整损失函数以优化特定的业务目标。

10.6.2 PhotoStock Inc.的训练流水线

现在我们回到 PhotoStock Inc.,在那里我们被要求构建一个智能内部搜索引擎以提升正确结果的输出。

设计文档:PhotoStock Inc.

VII. 训练流水线

多模态排名模型是 PhotoStock Inc.搜索引擎的核心组件,我们需要一个训练流水线来训练这个模型。如前所述,我们有一个基于预训练 CLIP 模型的坚实基础。然而,我们需要在我们的数据集上对其进行微调,这是一个图像和文本描述的组合。虽然数据集最初可能不会很大,但它可能会在后期增长,因此我们需要使流水线具有一定的可扩展性。我们假设我们可以从在单个顶级 GPU 上训练它开始,但希望将来能够将其扩展到单台机器上的多个 GPU。目前我们不追求完全分布式训练。

我们建议以下工具集用于流水线:

  • PyTorch——默认的深度学习框架,因为它是全球最受欢迎的,并且拥有大量的社区支持。

  • PyTorch Lightning——一个高级框架,用于简化训练循环并使其更具可重复性。

  • Flyte——一个工作流程管理工具,因为它已经在公司用于数据工程工作,我们可以重用一些现有的代码。

  • AWS Sagemaker——一个训练平台,因为 AWS 已经在公司使用,并且很容易与 Flyte 集成。

  • Tensorboard——一个简单的训练指标可视化工具。

  • Docker——一个容器化工具,使流水线更加便携和可重复。

流水线的输出应该是两个模型:一个文本编码器和图像编码器。两者都应转换为静态图表示(ONNX)并保存到 S3。此外,我们还应该输出用于推理(提示生成、图像预处理、距离函数)的训练参数列表。最后,每次运行都应该生成一个包含训练指标的报告。所有工件应在运行后保存到 S3。

我们预计将在以下领域进行积极的实验:

  • 要微调的内容——一些组件、整个模型,或者两者的组合,并使用自定义调度器。

  • 增强技术——我们可以为图像和文本使用不同的增强技术。

  • 各种损失函数——为不同的组件分配不同的权重。

  • CLIP 模型系列的多种骨干网络——例如,基于卷积或基于 transformer 的图像编码器;对于哪一个更好,没有强烈的直觉,因此我们需要对两者都进行实验。

  • 为文本编码器生成文本提示的方法——它必须是图像描述、标签等的组合。

  • 预处理图像输入的方法——例如,调整大小、裁剪、填充参数。

摘要

  • 记住,机器学习不仅仅是训练一个模型。其支柱之一是构建一个管道,允许以可重复的方式准备模型和其他工件。

  • 虽然训练管道和推理管道之间的区别可能看起来有些模糊且难以区分,但我们建议以下定义:训练管道用于训练模型本身,而推理管道用于在生产环境中运行模型或作为训练管道的一部分。

  • 典型训练管道的生命周期包括七个连续步骤,从数据获取、预处理、训练、评估和测试模型到后处理、工件打包和报告生成。

  • 到目前为止,在处理管道时还没有确立的平台和工具的标准。然而,在通用机器学习中存在经过时间考验的解决方案,您可以根据您设计的系统类型找到合适的解决方案。

  • 在某个时候,您将面临在两种管道扩展方法之间进行选择——垂直扩展或水平扩展。前者更简单且易于实现,但受限于机器的潜在最大性能。然而,后者却为提高硬件性能提供了更大的机会。

  • 尝试找到一种方法,使您的管道在可配置性方面保持平衡。如果您陷入任一极端(欠配置或过配置),您的管道将要么过于僵化且难以改变,要么对于既定的目标来说过于复杂。

  • 不要忽视测试您的管道!它将帮助您在引入更改时避免回归错误,通过早期捕捉缺陷来提高迭代速度,并改善整体设计,因为它将迫使您找到适当的可配置性平衡。

第十一章:11 特征与特征工程

本章涵盖

  • 特征工程的迭代过程

  • 分析特征重要性

  • 为您的模型选择合适的特征

  • 特征存储的优缺点

人们常说,一个具有出色特征的平庸模型将优于一个具有糟糕特征的出色模型。根据我们的经验,这个说法再正确不过了。特征是您系统的关键输入;它们驱动您的算法,为模型提供基本模式,并为学习做出预测所需的数据提供营养。没有好的特征,模型就是盲目的、聋的、哑的。

虽然在以深度学习为核心设计的系统中,特征工程的作用并非至关重要,但任何机器学习(ML)从业者都不能忽视其作用。从某种意义上说,将一些复杂的多模态数据框架化为深度学习模型,甚至为大型语言模型制作提示,都是特征工程的一种特定方式,这就是为什么经典的特征相关技术,如特征重要性分析,仍然非常相关。

本章探讨了创建有效特征的艺术和科学。我们将介绍帮助确定系统最有价值特征的工具,我们可能面临的工程挑战,在选择合适的特征子集时应该考虑的因素和权衡,以及如何确保所选特征是可靠和健壮的。

11.1 特征工程:你是谁?

特征工程是一个迭代的过程,涉及创建和测试新的特征或转换现有特征以提高模型性能。这个过程需要领域专业知识、创造力和数据工程技能来为系统构建新的数据管道。鉴于其耗时和迭代的特点,特征工程通常会消耗掉分配给建模的大量资源。

为了确保建模过程富有成效且流程顺畅,您在设计系统时始终应确保制定一个有效的特征工程策略。这个计划将成为一个指南,引导团队在识别和工程最具影响力的特征的同时,最大限度地减少浪费努力的风险。通过优先考虑适当的迭代顺序并规划路线,我们可以避免潜在陷阱并确保我们的行动为最终目标增加价值。

机器学习中的特征工程类似于在生成模型中构建提示结构,例如大型语言模型和文本到图像生成器。特征和提示都作为增强输入,引导模型的“注意力焦点”(字面或比喻)转向最相关的数据方面。

注释:通过开发适当的特征和提示,我们向模型注入一个特定的视角、上下文或“归纳偏差”,使其倾向于特定的结果。尽管它们的性质不同,特征和提示有一个共同的目标,即用我们的领域知识来上下文化模型,并指导它朝着我们希望实现的结果发展。

当谈到强大的深度学习模型时,在音频和图像处理等特定领域,特征工程曾经是一个复杂的问题。然后深度学习革命发生了,其从业者感到非常高兴,因为他们现在可以委托给一个端到端训练的深度学习模型,而不是工程无尽的、几乎不可靠的特征。甚至有机器学习从业者从未在研究项目之外进行过特征工程!这种趋势可以解释为一种信号,表明我们可以安全地跳过这一章。然而,我们认为即使是基于深度学习的管道也可能从特征工程和相关技术中受益。一个很好的例子来自阿尔申尼的经验。

阿尔申尼的篝火故事

我曾经在一个机器学习系统中工作,该系统内部有一个深度学习模型。该系统会接收一个图像,用深度学习模型处理它,并应用无机器学习的后处理来输出一个特定的数字。然而,第一步中存在的不准确性极大地影响了最终结果,由于性能不佳,该系统被认为不适合生产。由于严重的数据限制(该系统必须在几样本设置中工作——这是一种正式的说法,即模型应该只使用每个类别的几个标记样本就能发挥作用),改进第一个组件非常困难。但最终拯救了这一天的技巧是一个简单的回归模型,它细化了最终输出。多亏了利用手工制作的特征,模型并不那么需要数据。因此,深度学习用于重负载工作,而简单的基于特征的模型用于归纳偏差的组合,足够强大,足以使系统适合生产并最终被积极使用。

11.1.1 优秀和不良特征的准则

让我们分解一些特征特性以及我们应该注意的权衡:

  • 模型的性能——特征应与业务问题一致,并捕捉数据的相关方面,以提供与目标变量的有意义的依赖关系。与领域专家合作有助于开发正确的特征集,以及生成新的特征。当涉及到特征重要性分析时,它有助于精确评估给定特征的贡献,并深入了解哪些特征使我们更接近项目的目标。

  • 历史数据的数量**—有限的历史数据可能导致缺失值,降低数据集的整体质量。通过一次性的数据工程努力回顾性地重新创建缺失数据似乎是一个不错的解决方案,但并不总是可行。另一方面,缺乏足够的历史数据可能阻止预测模型捕捉特征值中的趋势和季节性。

  • 特征数量和质量之间的权衡**—虽然拥有更多特征可以提高机器学习模型的预测能力,但过多的无关或冗余特征会导致过拟合,并最终降低性能。我们总是更喜欢模型专注于一小组强大且多样化的特征,而不是分散其关注于许多生成的和相关的特征。

  • 可解释性和可说明性**—在设计特征时,这些是必须考虑的关键因素。虽然复杂的特征可能提高模型的表现,但它们也可能降低可解释性,使得解释预测背后的推理变得困难。另一方面,简单的特征可能更易于解释,但它们只能捕捉到数据中的一部分细微差别。在可解释性和性能之间取得平衡是至关重要的,并且可能因特定系统和领域而异。

  • 特征的开发复杂性**—复杂的特征可能需要更多的时间来开发。它们可能依赖于其他特征和复杂的数据管道,或者依赖于新的数据源,这使得它们的实现和维护更具挑战性。它们需要更多的数据工程努力来创建和维护数据管道。因此,仔细考虑每个特征的代价和收益,并决定额外的复杂性是否值得投资,是非常重要的。

  • 特征成本和服务级别协议(SLA)**—除了考虑单个特征的计算复杂度外,你还必须考虑计算所有特征所需的总时间,以及支持不断增长负载所需的 RAM。这包括计算每个特征所需的时间以及它们之间的任何依赖关系。特征交互决定了特征计算的顺序以及它们并行化的可能性。例如,对于实时应用,由于 SLA 的限制,可能无法实现计算时间较长的特征。此外,考虑训练和推理管道中的数据可用性也很重要。如果在服务过程中无法获取所有必要的数据,模型最终将产生不准确或不完整的预测。

  • 设计不良特征的风险——脆弱的特征会导致整个系统的脆弱性。它们可能对数据或模型变化敏感,导致不稳定或不可预测的行为。依赖于外部数据源或 API 的特征可能受到变化或中断的影响,影响模型的可靠性。为了防止这种情况,我们应该在将特征集成到模型之前仔细测试和验证它们,并监控它们随时间的性能,以确保它们继续提供业务价值。

特征工程旨在持续进行,因为业务目标和数据分布会随时间变化。我们必须不断评估和更新我们的特征集,以确保它保持相关性和有效性,以解决业务问题。在开发特征时,跟踪对每个特征所做的更改很重要,包括它们的版本和相互依赖关系。这使系统可重复和可维护。

11.1.2 特征生成 101

以所提到的标准和限制作为我们的指南,我们准备发现生成新特征的共同方法。

获取新特征最明显的方法是向您的数据管道添加新的数据源或使用之前未包含在数据集中的列。这个数据源可以是内部的(例如,数据库中的现有表)或外部的(例如,从第三方提供商购买数据)。一方面,这些新特征是低垂的果实,对模型性能有宝贵的贡献。另一方面,它们需要大部分数据工程工作,需要花费大量时间来管理,并可能导致基础设施问题,因为更大的复杂性总是需要更多的维护努力。

如果不使用新的来源,有两种替代方案——转换现有特征或基于现有特征的两个或更多组合生成新特征。

转换数值特征包括缩放、归一化和数学函数(例如,使用对数来改善分布偏斜)。模型的类型决定了转换的适用性。例如,在对其特征应用单调变换后,梯度提升度量通常不会增加,因为算法的核心元素——决策树——对输入的单调变换是不变的。

在处理时间序列数据时,通常利用诸如滞后(将特征的值向后移动以创建新特征)、聚合(计算特定时间窗口内的平均、最大或最小值等度量)或从过去数据生成统计特征(如不同时间段的平均值、标准差或方差)等转换。

分位数桶划分(或量化)是转换的一个特例。它通过根据其值将它们分组到离散的桶中,将连续特征转换为类别特征。例如,Uber 在其 DeepETA 网络中应用了这种方法(mng.bz/zn6B;参见图 11.1)。

figure

图 11.1 DeepETA 模型流程概述:结合基础特征工程和深度学习模型的示例(来源:mng.bz/0MoN

该网络采用 transformer 架构来预测到达时间的估计值,处理一系列表格数据。这些数据包括连续、类别和地理空间特征,全部转换为离散的标记,然后转换为适合 transformers 的可学习嵌入。您可以在 Xinyu Hu 等人的论文“DeeprETA:大规模 ETA 后处理系统”中了解更多关于 DeepETA 的信息(arxiv.org/pdf/2206.02127.pdf)。

分类别特征通常需要转换,这可以通过诸如独热编码、均值目标编码、顺序编码(该方法根据某些内在顺序对类别进行排序)或哈希技巧等方法实现,后者允许处理大规模的类别数据。需要注意的是,虽然均值目标编码功能强大,但如果实施不当,很容易导致数据泄露,因为它使用目标变量的信息来创建新特征。

对于文本等顺序数据,我们可以使用诸如词袋模型、词频-逆文档频率(TF-IDF)和 BM25 等技术,将数据转换为可以由 ML 算法处理的形式。值得注意的是,这些方法会丢失关于词序的信息;这种缺点可以通过使用比单词(单语元)更长的 N-gram 部分地解决。我们还可以使用预训练的语言模型,如 BERT,在低维嵌入空间中表示输入数据,然后将其输入到最终模型中。

记住,我们可以将几乎任何顺序数据表示为标记,而不是文本。例如,在在线零售和媒体流媒体服务等行业中,我们可以将用户会话解释为访问的产品页面或观看的视频的序列。每个访问的页面都将有其可学习的表示(一个嵌入)。之后,我们可以将这些嵌入用于我们的推荐系统中,作为“下一页预测任务”的提示,以了解用户正在寻找的产品/视频。

如果我们想在表格数据集中使用产品嵌入,一个常见的选项是利用产品之间的距离。这里的特征示例包括

  • 产品 X 的前五个邻居有多接近?

  • 产品 X 的前五个最近产品的平均/最低价格是多少?

  • 产品 X 和产品 Y 的价格之间的绝对/相对差异是多少?

尽管这些复杂特征确实增加了训练和推理管道的复杂性,但它们提供的信号可能会导致模型性能的重大进步。

那么将多个特征的信号合并为一个特征呢?当我们数据集中有多个特征时,我们可以将它们结合起来创建一个对我们模型更有信息量或更有意义的特征。例如,我们可以在电子商务网站上为用户的点击次数和购买次数创建一个新特征,而不是为它们分别设置特征,例如“购买-点击率”,这可能会成为用户购买意图的更好指标。

在出租车聚合公司的案例中,我们可以在“行程数”和“总行程距离”之间创建一个新特征,例如“每程平均距离”,这可能为司机和乘客的行为提供更有价值的见解,而不是为它们分别设置单独的特征。

我们还应该考虑现有特征之间的关系。例如,某一时期的绝对产品销售额可能提供的信息很少。然而,将它们与其他同一类别的产品销售或前期销售进行比较,可能会揭示有价值的行为模式或趋势。结合多个特征中的信号可以创建新的特征,这些特征可以捕捉数据中的更复杂关系,并提高模型的性能。

将多个特征结合的技术通常被称为特征交互特征交叉。这种技术对于线性模型尤为重要,因为这些特征可能会解锁数据点的线性可分性。

11.1.3 将模型预测作为特征

如我们之前讨论的,如果一个特征依赖于另一个特征,那么对后者的任何更改或更新都可能需要对应的前者进行相应的更改或更新。这会带来维护/调试挑战,并随着时间的推移增加系统的复杂性。

可以将模型预测视为一个特征的具体情况,其中模型的输出被用作另一个模型或系统的输入。这种方法有时被称为模型堆叠。虽然将模型预测作为特征可能非常强大和有效,但它也带来了一些工程挑战和风险。

使用模型预测作为特征的最简单例子是目标编码(maxhalford.github.io/blog/target-encoding/)。在这种方法中,分类特征通过目标值的平均值(带有一定程度的正则化)进行编码,并作为模型中的特征使用。然而,存在数据泄露的风险,编码基于训练数据中的信息,这些信息在推理期间不可用。如果我们不使用如嵌套交叉验证等高级验证技术,这可能导致过拟合,并在新数据上表现不佳(参见第七章)。

另一个例子是使用第三方模型(例如,将天气预报作为需求预测模型中的特征)。虽然天气数据可以非常有信息量,但存在风险,即预测可能需要必要的历时性。在这种情况下,具有必要历时性的预测比具有更高精度的预测更可取。此外,依赖外部数据源可能会引入超出机器学习团队控制范围之外的额外依赖和风险。

最后,在深度学习系统中使用第三方或开源模型作为特征提取器也可能存在风险。虽然生成的嵌入可以吸收数据中的有用模式,但如果外部模型在没有适当版本控制的情况下更新,或者相反——没有更新,模型可能会因为数据漂移而失去其价值,这可能会导致意外的行为,并大幅降低你的机器学习系统的性能。

为了减轻这些风险和挑战,设计特征工程管道非常重要,并应实施稳健的测试和监控程序(前者在第十章和第十三章中描述;后者可在第十四章中找到)。这可能包括使用交叉验证和其他技术来防止泄漏,验证外部数据源和模型,并建立监控和随时间更新特征的流程。

11.2 特征重要性分析

一旦为模型选择了初始的基线特征集,了解哪些特征对模型预测影响最大,可以提供有关模型如何做出决策以及可以进一步改进的地方的宝贵见解。

机器学习模型通常被视为黑盒,它们无法提供关于如何得出预测的见解。这种缺乏透明度对于必须理解给定模型提供决策背后的理由的工程师、利益相关者或最终用户来说可能是个问题。

在追求模型透明度的过程中,我们采用了两个关键概念:可解释性和可解释性。这两个概念都旨在揭示机器学习模型的工作原理:

  • 可解释性 围绕着理解模型的内部机制,阐明它是如何以及为什么生成其预测的。

  • 然而,可解释性 是关于用人类可以理解的语言来阐述模型的行为,即使模型的内部机制复杂或难以理解(mng.bz/KDvj)。

特征重要性分析作为实现可解释性和可解释性的工具,因为它有助于确定对模型预测有很大贡献的特征。特征重要性分析的结果作为训练管道工件的一部分收集,可能在模型验证过程中发挥作用,为模型的新版本提供“部署或不部署”的裁决(更多细节请见第十三章)。这里的一个好例子是确定出租车聚合应用程序中行程成本的系统,如图 11.2 所示。

figure

图 11.2 一个出租车聚合应用程序的 UI 示例,说明了为什么其动态定价算法在这个区域和这个时间选择这个特定的价格

在幕后,该应用程序与所有关键特征一起工作,并在确定最终价格时分析当前的实时数据,如交通密度、天气状况等。然而,它还以方便且用户友好的形式提供建议价格背后的理由。通过这种交付方式,用户可以理解为什么他们通常定期乘坐的便宜车突然价格上涨。

此外,特征重要性分析可以增加我们对机器学习系统的信任。这在医学和金融等高风险领域尤为重要。尽管通用数据保护条例(gdpr.eu)并没有严格强制可解释性,但它确实建议在自动化决策中保持一定程度的透明度,这在许多机器学习应用中可能是有益的,甚至是必不可少的(mng.bz/9oX7)。

识别最重要的特征可以解释驱动模型预测的变量及其背后的原因。这些信息可以帮助优化这些特征以提升模型的表现,并移除无关或冗余的特征以提高效率。此外,它还可以通过例如检测过拟合或评估新添加特征的实用性等方式,引导我们进行调试。

11.2.1 方法的分类

让我们探讨特征重要性分析的方法以及它们如何应用于提高机器学习系统的透明度和性能。

在特征重要性分析的地形中导航可能会令人畏惧,但拥有一个可用方法的地图可以向我们展示正确的方向。这些方法可以根据它们的属性进行广泛分类,例如模型类型、模型可解释性级别以及所利用的特征类型。

经典机器学习与深度学习

用于特征重要性分析的方法在经典机器学习和深度学习模型之间可能存在很大差异。对于经典机器学习模型,其中特征通常是根据领域知识或统计分析手动选择的,确定特征重要性是直接的——我们可以直接检查模型权重和决策规则,或者排除/修改一个单独的特征来调查其对模型预测的贡献。

另一方面,深度学习模型,它们自动从数据中学习特征表示,在重要性分析方面提出了独特的挑战。鉴于复杂的非线性变换和高层次抽象,理解特征重要性不仅仅是查看模型参数,而是需要依赖像显著性图、激活最大化(详见 Aravindh Mahendran 等人撰写的“使用自然预图像可视化深度卷积神经网络”,arxiv.org/abs/1512.02017)或层相关传播(详见 Alexander Binder 等人撰写的“具有局部归一化层的神经网络层相关传播”,arxiv.org/abs/1604.00825)及其后续研究等高级技术来理解神经网络内部发生的事情。请注意,这个例子列表并不全面,而且它们都不是真正通用的,因为可解释深度学习问题在一般情况下尚未解决,并且仍然处于活跃的研究阶段。

模型特定与模型无关

模型特定方法使用模型的结构和参数来估计特征重要性。例如,在基于树的模型中,我们可以在训练时间计算特定特征被分割的次数或它在所有分割中提供的总增益。同样,对于线性模型,我们可以查看分配给每个特征的系数的幅度和符号。

相应地,模型无关方法将模型视为黑盒。它们通常涉及扰动输入数据并观察对模型输出的影响。模型无关方法的例子包括

  • 排列特征重要性**—通过在数据集中随机排列每个特征值并观察模型性能的下降程度来衡量每个特征的重要性

  • SHapley Additive exPlanations (SHAP) 值—通过平均所有可能的特征组合来估计每个特征对特定预测的贡献

单个预测与整个模型解释

另一个重要的区别是方法是否是为单个预测而设计,还是为了解释整个模型(见图 11.3)。专注于单个预测的方法会估计特定输入的特征重要性,深入探究模型为何做出特定决策。另一方面,解释整个模型的方法在更广泛的意义上估计特征的重要性,详细阐述模型的总体行为。

一些专注于单个预测的方法的例子包括局部可解释模型无关解释(LIME);参见 Marco Tulio Ribeiro 等人撰写的论文“Why Should I Trust You?”(arxiv.org/abs/1602.04938),该论文使用更简单、更可解释的模型(例如线性回归)来近似特定输入周围的决策边界,以及锚定解释(在 Marco Tulio Ribeiro 等人撰写的论文“Anchors: High-Precision Model-Agnostic Explanations”中了解更多信息,mng.bz/j0vr),该论文确定了一条足够“锚定”决策的规则,使其对人类可解释。

figure

图 11.3 模型解释方法的分类

通常,我们会使用多种方法的组合来减轻单个方法的局限性,并更全面地理解模型。但请记住,没有一种方法可以提供对所有特征重要性问题的明确答案,方法的选择应针对具体问题和情境进行调整。

11.2.2 准确性与可解释性权衡

高度可解释的模型可能会牺牲准确性以换取透明度,反之亦然——达到高准确性的模型往往是以牺牲可解释性为代价的(见图 11.4)。基于 transformers 的现代大型语言模型,如 GPT,提供了一个生动的例子。它们通过在广泛的自然语言处理任务中实现最先进的性能,彻底改变了机器学习领域。然而,它们也往往非常复杂,拥有数十亿个参数,这使得理解它们如何做出决策变得困难。

figure

图 11.4 我们使用的模型越复杂(因此通常越准确),其可解释性就越低。

随着新未探索的架构的出现,准确性与可解释性权衡仍然是一个具有挑战性的问题。方法的选择应针对具体问题情境进行调整,考虑因素包括可解释性的重要性、模型的复杂性以及所需的准确度水平。

11.2.3 深度学习中的特征重要性

对于处理表格数据的模型,特征重要性分析是一个可以理解的问题,有明确的解决方案;我们有易于分离的特征和已知工具来衡量每个特征如何影响模型、目标变量或最终指标。

然而,在深度学习的背景下,特别是对于图像、音频或文本等数据类型,特征重要性可能会变得不那么清晰,更具挑战性。深度学习模型本质上会自动从数据中学习层次化的表示,通常以高度抽象和非线性的方式进行。在这些情况下,“特征”可以指从图像中的一个像素到文本中的一个单词或字符,或音频信号中的特定频率,以及复杂的属性,如图像中物体的位置、文本中句子的情感或语音记录中的特定声音模式。

尽管如此,我们仍然可以深入了解模型在原始输入数据中认为重要的内容以及它关注哪些模式。让我们探索一些深度学习中特征重要性分析的技术:

  • SHAP 值**—与经典特征类似,SHAP 值估计模型输出中每个标记或像素的贡献,提供了一种模型无关的解释,说明哪些输入部分的个别部分对模型影响最大。

  • 显著性图**—显著性图是一种局部解释形式,突出显示对模型输出敏感的输入图像区域。本质上,它们计算输入的输出梯度,结果是一个热图,其中每个像素表示改变该像素会对输出产生多大影响。显著性图方法的例子包括 GT、SalNet、SALICON 和 SalClassNet。图 11.5 取自 Francesca Murabito 等人的论文“由视觉分类驱动的自上而下显著性检测”(arxiv.org/abs/1709.05307),可以提供关于显著性图如何工作的基本直觉。

figure

图 11.5 不同方法生成的输出显著性图示例(来源:arxiv.org/abs/1709.05307
  • 基于扰动的技术**—这些方法通过观察当特征被改变或移除时对模型输出的影响来确定特征的重要性。基于扰动的技术的一个好例子是遮挡,它主要应用于视觉模型,其中图像的一部分被系统地遮挡(覆盖),随后输出中的变化被跟踪。遮挡的想法最初出现在 Matthiew D. Zeller 等人的一篇论文中,“可视化和理解卷积网络”(arxiv.org/abs/1311.2901),并在像“RISE:随机输入采样用于黑盒模型解释”这样的新发展中得到积极应用,由 Vitali Petsiuk 等人提出(arxiv.org/abs/1806.07421)。这有助于可视化模型认为对预测最相关的图像部分。

  • 注意力机制在转换器中**—在转换器中,注意力机制是一种自然的形式,用于表示特征的重要性。注意力分数表示模型为预测赋予序列中每个标记的权重。这些注意力权重可以可视化并解释为模型的重点,强调模型(特别是特定的注意力头)如何“阅读”和理解输入文本(见图 11.6)。

figure

图 11.6 编码器-解码器转换器中注意力头的可视化

正如你所见,我们开始观察到与经典机器学习的相似之处。尽管深度学习模型在特征重要性分析方面提出了独特的挑战,但仍然有方法可以提供关于模型如何做出决策以及哪些模式是目标变量基本预测者的见解。

11.3 特征选择

完美最终不是在已经无法添加任何东西的时候达到,而是在已经无法再删减任何东西的时候实现。—— 安东尼·德·圣埃克苏佩里

在前面的章节中,我们学习了特征工程的艺术以及如何将原始数据转换为有意义的特征。然而,并非所有特征都同等有用;有些可能是不相关的、冗余的,或者对我们模型的处理来说过于复杂。

这就是特征选择发挥作用的地方。通过仔细选择最有信息量的特征,我们可以提高系统的性能和可解释性,同时降低其复杂性和训练时间。我们将探讨特征选择的技术、最佳实践和潜在陷阱,并学习如何为我们的特定机器学习问题选择正确的特征。

11.3.1 特征生成与特征选择比较

机器学习中的特征生成和特征选择过程可以比作园艺。与在土壤中种植各种种子的园丁一样,我们生成一系列特征,探索新的数据源,尝试不同的特征转换,并头脑风暴可能提高模型性能的新想法。

然而,正如花园中并非所有植物都能茁壮成长一样,并非所有特征都会对模型有益,在某个时候,我们不得不修剪掉枯死或不结果实的植物(在我们的情况下,丢弃不相关或冗余的特征)以维持健康生长。这种培养和修剪、增加和减少的循环是机器学习系统生活中的一个常态,因为我们不断地精炼和改进我们的特征集。

古希腊哲学家赫拉克利特曾说过:“对立产生和谐。从冲突中产生最美好的和谐。”这在机器学习(ML)中同样适用,我们通过在生成新特征和精心选择最有信息量的特征之间保持平衡,来实现最佳性能。

11.3.2 目标和可能的缺点

你可能会问,“好吧,但为什么一开始就如此关注特征选择?”这其中有某些好处:

  • 更高的准确性和更少的过拟合**—选择最有信息量的特征有助于模型专注于最重要的信号。移除无关或冗余的特征可以降低模型变得过于复杂时过拟合的风险,此时模型会拟合训练数据中的噪声而不是潜在的模式。

  • 更容易解释**—由 10 个有意义的特征做出的决策比由 100 个特征做出的决策更容易解释和理解,即使在后一种情况下模型的性能更高。

  • 更容易构建和调试**—如果在特征选择阶段,我们收集到三个数据源中现在有五是冗余的见解,我们可以通过从训练和推理管道中移除它们来节省大量时间和计算资源。一个更简单的数据管道需要更少的努力来维护和故障排除。

  • 更快的训练和推理时间**—随着我们减少特征的数量,模型的复杂性降低,导致训练时间缩短和计算成本降低。

为了方便起见,我们将特征选择的所有好处汇总到图 11.7 中。

figure

图 11.7 特征选择的原因

在实时应用中,速度的需求往往优先考虑,即使这意味着为了满足服务水平协议(SLAs)而牺牲模型的准确性。例如,在虚拟助手等语音识别系统中,用户期望他们的口语能够即时且准确地转录成文本。即使是微小的延迟也可能破坏用户体验,并使系统看起来效率低下。

如果完美的个性化导致预测速度慢了 300 毫秒,造成负面印象,那么它就变得毫无价值。因此,具有适度质量的轻量级个性化比一个积累用户所有可能输入但让他们等待的模型更合适。

案例研究

亚马逊进行了一系列的 A/B 测试,以纯数字的形式制定这种权衡:每 100 毫秒的延迟会损失 1%的销售额。对于一个年收入 5000 亿美元的企业来说,1%的下降意味着 50 亿美元——这不是它承受的损失(mng.bz/WVga)。

除了在计算时间和准确性之间进行平衡之外,特征选择也存在潜在的风险和缺点:

  • 潜在有价值信号的丢失**—移除特征可能会导致失去未来可能改进模型的重要信息。我们可能会忽略一些合理的预处理或聚合,并草率地得出特征没有有用信号的结论。

  • 不可预见的关系**—移除某些特征可能会在特征之间产生不可预见的关系,导致意外的行为和模型性能下降。在选择特征时,考虑特征之间的关系以及可能出现的潜在交互是至关重要的。

  • 偏差**—某些特征可能比其他特征更受重视,导致特征选择产生偏差预测。想象一下,如果我们只选择与目标变量高度相关的特征。在这种情况下,我们可能会将偏差引入模型,并无法捕捉到虽然与目标变量关联度不高但仍然与预测任务相关的关键信息。

  • 过拟合风险**—与超参数优化类似,特征选择也是一个学习过程。任何需要摄入目标变量的学习过程都需要一个适当的验证方案(详见第七章的详细说明)。假设我们使用相同的数据来选择特征并评估模型的性能。在这种情况下,存在高度过拟合测试数据的风险,导致对性能估计过于乐观。

除了这些问题之外,如果定期进行,特征选择会给训练流程增加一个计算密集的阶段,我们也应该考虑这一点,尤其是在使用贪婪包装器方法时。

11.3.3 特征选择方法概述

可用于特征选择的方法有很多,每种方法都有其优缺点(见图 11.8)。最常见的方法是过滤器、包装器和嵌入式方法。让我们更详细地看看这三种方法。

figure

图 11.8 特征选择方法系列

过滤器方法通过独立于模型过滤特征,使用基于单个特征(单变量方法)或与其他特征的关联(多元方法)的统计属性的单个简单排名规则来工作。这些方法易于扩展(即使是对于高维数据)并且在主要任务之前快速进行特征选择。

在单变量过滤器方法中,特征排名的顺序由内在属性决定,如特征方差、粒度、一致性、与目标的关联等。之后,我们保留前 N 个特征作为我们的子集,或者拟合模型,或者应用更高级、计算密集的特征选择方法作为第二特征选择层。

在多元方法中,我们通过比较特征来分析它们(例如,通过估计它们的秩相关或互信息)。如果一对特征代表相似的信息,其中之一可以被省略而不会影响模型的表现。例如,特征交互评分(无论其测量方式如何)可以纳入自动报告中。当评分较高时,会在训练开始之前触发对模型性能可能降低的警告。

包装方法专注于有助于提高模型结果质量的特征子集,基于所选指标进行选择。我们这样称呼它们,因为学习算法实际上是被这些方法“包装”起来的。它们还要求设计正确的验证方案,并将其嵌套在外部验证中(有关选择正确验证方案的信息,请参阅第七章)。

包装算法包括顺序算法和进化算法。顺序算法的例子包括顺序前向选择,从空集开始逐个包含特征;SBE(顺序后向消除),逐个排除特征;以及它们的混合版本——当我们允许包含被排除的特征时,以及相反的情况。在进化算法中,我们随机采样特征子集以供考虑,有效地“跳跃”通过特征空间。进化算法的一个常见例子是在二进制特征掩码空间中运行差分进化的变体,其中“1”表示包含的特征,“0”表示排除的特征。

这些方法的主要缺点是它们都具有计算密集性,并且往往趋向于收敛到局部最优解。尽管如此,它们提供了对子集如何影响目标指标的最准确评估。请谨慎使用它们,尤其是如果你的硬件规格有限。

在嵌入式方法中,我们使用一个额外的“嵌入式”模型(这个模型可能与我们的主要模型属于同一类别,也可能不属于),并根据其特征重要性做出决策。一个很好的例子是 Lasso 回归,由于 L1 正则化能够将不相关的系数转换为零,如图 11.9 所示。

figure

图 11.9 Lasso 回归通过将系数减少到零来逐个消除特征,随着 L1 正则化项的增长。

另一个广泛使用的特征选择算法是递归特征消除(RFE),它基于嵌入式模型的特征重要性,在每一步训练和移除最差的 K 个特征。

在所需计算和选择质量方面,嵌入式方法介于过滤方法和包装方法之间。

当涉及到选择起始方法时,我们更倾向于使用粗略的截止值来减少特征数量,而不是使用 Lasso 选择器或 RFE,这取决于哪一个输出的子集更有意义。计算密集型方法可能具有更好的性能,但快速方法对于初始特征剪枝来说通常已经足够好,尤其是如果你怀疑某些特征完全是垃圾。

有许多虚拟但仍然有用的方法,也充当合理的特征选择基线。例如,我们可以取一个特征,将其值打乱,将其连接到初始数据集,并训练一个新的模型。如果给定特征的重要性低于这个随机特征(通常称为影子特征)的重要性,那么它很可能与问题无关。我们可以将这个算法标记为包装方法的平凡实例。

Valerii 的篝火故事

Valeri 之前的一个与动态定价相关联的项目,使用了一个需要改进的模型。原因很简单——该模型的表现并没有达到预期的精确度,在进行了基本的错误分析后,他意识到大多数错误都是由销售数量庞大的 SKU 引起的。进一步的调查发现,尽管某些特征,特别是基于价格历史的特征,是关键的,但其他特征几乎不显著,使用 Lasso 回归过滤掉这些特征简化了模型。当特征数量减少时,由于整体特征数量(10² 是 100,而 20² 是 400)的减少,使用简单的特征交互(存活特征的多项式组合)变得更加可行。由于特定的预处理,一开始并没有帮助,但一旦改变了预处理,就产生了明显的积极效果;输入数据被归一化到(0..1),由于一个组件等于零,无论第二个组件如何,一些交互可能会变成零,所以 00 和 10 产生相同的输出,但实际上它们非常不同。因此,通过将缩放范围调整为(1..10)来解决乘以零的问题,将数字转换为 float16 以减少 RAM 消耗,应用多项式特征交互(从内存角度来看,在 1–10 范围内的数字上更容易进行),然后再将其缩放到 1–10,并在新特征上训练一个简单的岭回归,我能够将错误减少 30%。值得注意的是,之前尝试改进模型的方法主要集中在使用更复杂的模型,如梯度提升和神经网络,但投资于特征工程似乎是一条更短的路径,比更复杂的方法更有效。

Arseny 的篝火故事

Arseny 曾经在一个系统上工作,这个系统实际上是一个文本分类系统:给定交易描述(一个充满首字母缩略词的半截字符串)及其附加属性(例如,转账金额),系统需要对交易进行分类。基于 Transformer 的模型在文本处理中展示了它们的威力;然而,处理额外的属性并不那么简单。

最终解决方案基于一个类似 BERT 的 transformer 模型,以多组件提示作为输入。这个提示包含了文本输入和从交易属性中手工制作的各种特征。与这些特征(包括特征重要性分析和特征选择)一起工作,有助于在目标指标方面比典型的深度学习模型改进(如骨干预训练或复杂的损失函数)更进一步。

11.4 特征存储

现在我们已经到达了一个强大的设计模式,将章节中提到的许多技术整合到一个单一实体中——一个特征存储。它使团队能够在一个集中的中心计算、存储、汇总、测试、记录和监控其 ML 管道中的功能。

想象一下,你投入了数周的时间来构建复杂的功能特征,却在咖啡休息时间偶然与同事交谈,发现另一个团队已经实现了并测试了完全相同的功能。或者,一个同事来找你,寻求特定功能的实现。当你确认其存在时,你意识到你的代码由于过度依赖存储库中的其他代码而缺乏适当的文档和可重用性。因此,你的同事决定采取更简单的方法,重新发明轮子并开发自己的实现。这种不一致性可能导致每个工程师都独立实现和计算每个功能,导致公司浪费了无法接受的大量资源(见图 11.10)。

figure

图 11.10 缺乏特征存储可能导致不一致和过度支出。

这些场景仅展示了使用特征存储可以解决的挑战的一小部分。通过采用它,我们摆脱了每个团队独立实现和计算功能的碎片化方法。相反,我们拥抱了一个统一的系统,最大限度地提高了功能的可重用性,如图 11.11 所示。

figure

图 11.11 一个最大化以统一方式实现的功能的可重用性的特征存储

11.4.1 特征存储:优点与缺点

设计、构建和管理特征存储可能会带来一些挑战,但其好处可能会远远超过缺点。让我们来探讨一下拥有特征存储的一些优点(图 11.12 展示了从拥有特征存储中受益最大的 ML 领域):

  • 可重用性和协作**——特征存储通过允许团队在不同项目和管道之间共享和重用功能来促进可重用性。这节省了宝贵的时间和精力,并促进了团队之间的协作,因为他们可以使用彼此的工作并在现有的功能实现之上构建。

  • 简化工作流程**—而不是每次新项目都从头开始,团队可以基于可重用特征的基础进行构建,从而加速开发过程。这种简化的工作流程允许更快地进行迭代和实验,从而更快地获得见解并提高模型性能。特征存储使团队能够通过最小化重复性任务并提供结构化框架来专注于交付模型。

  • 一致性和标准化**—有了特征存储,特征工程有一个统一和标准化的方法。这确保了特征计算的连贯性,降低了在不同模型、管道或管道阶段出现不一致或差异的风险。通过遵守预定义的标准,团队能够更无缝地协作,并提高整体系统稳定性。

  • 文档和透明度**—特征存储促进了特征的适当文档记录(或自动文档记录),包括其数据来源和计算方法。它增强了透明度,使团队能够更容易地发现和评估可用的特征。它还有助于故障排除和调试,因为文档提供了对特征工程过程的宝贵见解。

  • 可扩展性和可维护性**—一个设计良好的特征存储架构允许可扩展性,能够适应大量数据和不断变化的需求。它简化了添加新特征或修改现有特征的流程,使团队能够在不造成重大中断的情况下适应变化的需求。此外,特征存储的集中化特性有助于更轻松地维护和监控特征,从而提高机器学习管道的整体可靠性。

figure

图 11.12 具有各种特征存储需求的人工智能问题景观

拥有特征存储的缺点是显而易见的:

  • 在不同团队中收集需求、设计满足所有需求的特征存储,以及实施它(或集成第三方解决方案如 Tecton、Feast、Feathr 或 Databricks Feature Store)需要花费时间。

  • 这降低了我们在处理特征时的灵活性,同时增加了机器学习团队之间相互依赖的程度。

  • 如果从头开始开发,成本会很高。我们不推荐重新发明轮子,建议使用现成的解决方案(例如,Tecton)。

  • 特征存储可能不适合您的特定项目。

让我们详细关注最后一个缺点。并非所有机器学习问题都可以通过特征存储进行优化。拥有特征存储的一个典型有益领域主要是具有多种数据源和不同粒度以及 SLA 的表格数据(结构化数据)。大多数纯深度学习问题(通常是具有一个或两个非结构化数据源,如文本或图像的问题)不太适合特征存储。然而,鉴于多模态数据使用越来越普遍,特征存储的概念已经变得更加通用。想象一个典型的在线市场;几年前,机器学习系统会基于表格数据,如销售历史和点击记录,而现在,包括商品图片、描述和用户评论已成为一种常见的模式。包含此类数据的最简单方法是通过预训练神经网络(例如,CLIP 用于图像或 SentenceTransformers 用于简短文本)提取嵌入,并将它们作为特征。这种方法闭合了循环:这些特征可以像“经典”特征一样存储在特征存储中,从而节省处理时间并确保系统的一致性。作为额外的好处,将此类特征存储在集中式存储中解锁了额外的使用模式。例如,使用向量数据库(如 Qdrant 或 Faiss)进行存储,可以快速检索类似的项目并用于下游模型。

开始的最佳方式是分析所有团队的现有提取、转换和加载管道。以下是你应该准备好提出的问题:

  • 每个团队使用哪些数据源?他们有多少?

  • 它们与功能和用法模式有何交集?

  • 他们计算哪些类型的特征,或者他们希望计算哪些?

  • 哪些团队需要实时响应(“在线功能”)?

如果我们得出结论,许多团队将受益于拥有特征存储,那么这是一个投资于设计和构建集中式特征存储的信号。

我们想再次强调,构建自己的定制特征存储是一个庞大且昂贵的项目。这是那些你应该考虑重用开源解决方案或第三方供应商产品的案例之一。一些流行的选项包括 Feast、Tecton、Databricks Feature Store 和 AWS Sagemaker Feature Store。

11.4.2 特征存储的理想属性

在本节中,我们将讨论设计特征存储中的有用模式和属性,并强调你必须解决的重要问题。并非所有这些问题都会出现在你的特定特征存储中,但我们的目标是确保书中涵盖每个问题。

读-写偏差

写入和读取是任何特征管理系统两个基本方面,因此在设计阶段,我们需要了解读写操作的负载大小(以及数据量)。我们读取的延迟是多少?我们应该多久重新计算现有特征一次?通常,在运行时计算一个特征比从存储中检索一个要快或相当。

写入通常以批量方式进行。当我们能够同时完成时,我们不喜欢计算整个数据集,尽管这是一个广泛使用的反模式。此外,更新最近几天的特征有助于我们覆盖数据仓库侧可能发生的某些临时损坏或不可用情况,这些情况可能就在我们每天的特征更新之前。值得注意的是,“通常”并不是一个常见的情况——特征可以以不同的时间表附加或更新。例如,有些是每天通过长时间作业计算的,而有些则足够轻量,可以近乎实时地流式传输。

读取特征的关键方面通常是延迟。我们必须确保为我们特征存储构建的基础设施满足我们的非功能性需求。有时我们可以在读取操作期间将预计算的特性和实时特征(那些需要最新事件的特征)结合起来,如图 11.13 所示。

figure

图 11.13 在特征存储中,在线特征和批量特征以不同的方式写入,但以相同的方式消费。

预计算

编程中的 DRY 原则代表“不要重复自己”。这个原则引导我们到达任何优化背后的基本启发式方法:如果可能避免计算某些东西,那么应该避免它。

尤其是其中最直接的模式是计算特征,这些特征应该提前计算,但不是在我们请求特征存储收集数据集时。例如,更新特征的好时机是当我们的数据库完成前一天订单的处理。

一种紧密相关的优化技术是将计算分成多个步骤:

  1. 我们预先聚合原始数据(例如,点击量、价格、收入)到项-日总和。

  2. 我们将这些总和聚合到期望的窗口中(例如,7/14/30/60 天)。

这种方法帮助我们重用昨天或 N 个月前计算的特征(而不是每天几乎进行相同的计算)并将相似特征的计算与部分相同谱系或重叠聚合窗口合并,如图 11.14 所示。

figure

图 11.14 在特征存储中使用的聚合层次结构

功能版本化

一个好的经验法则是,每个特征的更新应该被视为一个新的特征(这并不是最佳解决方案,每次微小优化后都会生成许多类似特征,并弄乱你的代码)或一个旧特征的版本。但为什么这很重要呢?

假设某个工程师实现了一个新版本的功能,而你的系统将其视为与之前相同的功能,没有任何区别。如果你很幸运,计算结果完全相同但速度更快,例如。但如果计算原理发生变化(即使只是一点点)——或者更糟糕的是,如果工程师更改了相同功能的源数据——这会导致预先计算的功能不一致。更新前后功能的值可能显著不同,你不想将旧方法和新方法的值混合在一起。一个设计良好的功能存储库将自动覆盖更新的功能,或者更好的是,在回填所有可用历史记录的同时将其写入新表。

每个数据集在其元信息中不仅应该捕获它包含哪些特征以及给定时间戳范围内的特征,还应该捕获这些特征在计算时的版本。这使我们能够轻松回滚到旧版本的数据集,并完全重现例如两个月前开发的旧模型的成果。这种模式类似于我们应用程序中库的版本冻结。

功能依赖或功能层次

并非所有功能都可以轻松地从我们的数据仓库的原始数据中计算得出。这可能会导致计算成本高昂的查询,并且再次,它没有重用先前计算的结果。这使我们得出了功能依赖或功能层次的概念,其中每个功能都依赖于其他功能和数据源。

我们之前讨论过的一种模式,即预聚合,可以被视为最终功能的父功能。我们在图 11.15 中突出显示了它们(第 1 级功能)以及它们的子功能(第 2 级功能等)。

图

图 11.15 功能依赖关系图

我们从数据源获取每个功能的方式称为“线索”,这实际上是一个有向无环图(我们在第六章中讨论过)。我们跟踪每个功能的线索,以了解在运行功能计算时的顺序,是否需要在更改其父功能之一(这触发了功能版本更新的浪潮)后更新其子功能,或者是否需要任何其他类型的功能版本更新、损坏或删除。

线索跟踪也有助于工程师和分析人员快速探索每个功能的来源,从而简化调试并提高他们对异常值或其他意外行为的起源的理解。

功能包

我们经常将相同的转换和过滤器应用于来自相同数据源的相关列(例如,折扣前的价格、折扣后的价格和促销后的价格)。这些类似的功能具有相同的键和相同的线索。

这意味着没有必要将特性作为独立的列来处理,并为每个特性编写单独的数据管道。我们自然更喜欢以这种方式实现我们的特性存储库,即它将来自相同数据源的特性计算进行整合。因此,单个计算实体将是一批特性,而不是单个特性:

<day, user_id, item_id, f1, f2, f3, f1 / f2, …>

尽管合并了它们的计算图,但我们更倾向于在 API(或 UI)上以整体相似特性的集合为单位进行操作,以便同时将它们添加到数据集中。

11.4.3 特性目录

与 UI 相关,特性存储库的最终秘密成分是一个特性目录。特性目录是一个具有 Web UI 的服务,ML 工程师、分析师甚至你的非技术同事都可以在其中搜索特性并检查它们的实现细节。

其他可以向用户展示的内容有特性重要性、值分布、类别、所有者、更新计划(每日、每小时)、关键(用户、项目或用户-项目,或项目-日,类别-日)、特性血统、消耗此特性的 ML 服务以及其他元信息。

11.5 设计文档:特性工程

正如我们在本章引言中提到的,特性是您 ML 系统预测能力的骨架,仅就此而言,它们在设计文档中占有一席之地。我们将在我们的设计文档中涵盖它们。

11.5.1 Supermegaretail 的特性

在构建我们的基线解决方案之后,我们需要确定其改进的下一步。做到这一点的主要方法之一是使用有助于模型从原始数据中提取有用模式和关系的特性。

设计文档:Supermegaretail

VIII. 特性

我们选择合适特性的关键标准(除了预测质量之外)包括

  • 预测质量**—我们得到的预测越准确,越好。

  • 可解释性和可说明性**—我们更喜欢易于描述和解释的特性(“黑盒”解决方案既不透明也不值得信赖,尤其是在项目的初期阶段)。

  • 计算时间 (以及计算复杂度)—需要大量时间来计算的特性(以及来自多个数据源和具有复杂依赖关系的特性)除非它们在预测质量上的改进值得,否则不太受欢迎。这是因为它们会减慢训练周期并减少我们可以测试的假设数量。

  • 风险(和特性稳定性)—需要外部/多个数据源、辅助模型(或简单地设计不佳的特性),以及基于数据质量低的数据源的特性会使管道更加脆弱,这应该避免。

如果一个特性对模型性能的提升具有统计学上的显著改进,但违反了其他标准之一(例如,计算需要 2 天时间),我们更倾向于不将此特性添加到管道中。

新特性的主要来源是

  • 添加更多内部和外部数据源(例如,监控竞争对手)

  • 转换和组合现有特征

以下是我们将实验的特征列表,这将指导我们在初始部署后进一步改进模型的步骤:

  • 竞争对手的价格以及它们与我们价格的差异(外部来源)

  • 特殊促销和折扣日历

  • 价格(原价、折扣价)

  • 渗透率(SKU 销售额与品类销售额之间的比率)

  • SKU 的属性(品牌、不同级别的品类)

  • 线性弹性系数

  • SKU 在过去 N 天内的销售额总和/最小值/最大值/平均值/标准差

  • SKU 在过去 N 天内的销售额的中位数/分位数

  • 预测天气(外部来源)

  • 店铺的客流量(外部来源)

  • 店铺的销售额

  • 一年前该 SKU 的销售额

  • 经济指标(外部来源)

我们将它们表述为一个假设。例如,使用促销日历可以帮助模型捕捉到营销活动中的需求瞬时增加,这将在该期间减少过剩库存。

我们将使用模型无关(SHAP、LIME、洗牌重要性)和内置方法(线性模型的系数、梯度提升中的分割数)来衡量特征重要性。主要目标是理解每个特征对模型结果的贡献。如果一个特征贡献不大,我们将删除它。

在第一阶段自动特征选择(在我们尚未确定基本特征集之前),我们使用 RFE。

此外,我们在训练模型之前和之后在训练管道中包含特征测试:

  • 测试特征范围和异常检测器(例如,0.0 <= discount < X)

  • 测试任何一对特征之间的相关性小于 X

  • 测试该特征的系数/分割数是否大于 0

  • 测试任何特征的计算时间是否小于 6 小时

为了更容易地计算和访问特征,我们可以重用集中式特征存储,该存储从数据仓库中的不同来源收集数据,经过不同的转换和聚合后,合并成一个数据集市(SKU、店铺、日)。它每天重新计算特征,使得实验新特征和跟踪它们的版本、依赖关系和其他元信息变得容易。

图像

11.5.2 PhotoStock Inc. 的特征

PhotoStock Inc. 的潜在特征集将与 Supermegaretail 的完全不同。

设计文档:PhotoStock Inc.

VIII. 特征

如前所述,在基线部分,我们旨在通过测量查询与文档(图像及其描述)之间的距离来启动一个纯内容相关性系统:

relevancy_score = query 与 image 的距离 + query 与 description 的距离

虽然这可能导致没有特征工程参与的结论,但这至少有两个原因并不完全正确:

  • 图像描述和元数据应通过某种方式转换,以便用作模型输入。因此,我们需要提出一种稳健且可扩展的方法来实现这一点。

  • 我们可能在稍后阶段想要引入表示用户和文档的额外信号源。此类文档特征的例子包括文档点击率、平均购买率和在文档页面上的时间。此类用户特征的例子可以是他们的点击历史记录的汇总、他们配置文件的明确设置或通过类似协同过滤的方法计算的特征。这是搜索引擎的典型场景。最后,我们可能想要使用与照片作者相关的特征——例如,他们的平均评分或售出的商品数量,以隐式推广我们的核心贡献者。然而,这是一项重大的工作范围,所以我们不想立即进行。尽管如此,我们想要设计一个系统,稍后可以轻松扩展。

考虑到我们预计会出现新的特征类型,我们应该设计一个系统,能够有效地使用来自多个来源的信号并输出一个统一的相关性评分——例如:

relevancy_score = distance_function(query_to_image_distance, query_to_description_distance, user_features, document_features, any_other_features_we_want_to_add_later)

在这个基线示例中,我们建议距离函数是距离的简单总和。然而,这并非唯一的选择。一个直接的选择是使用加权总和,这实际上意味着在距离之上训练一个小型线性模型。

i. 编码照片元数据

考虑到我们考虑的是多模态 CLIP 风格的编码器,我们可以利用它可以使用任何文本作为输入的事实。因此,我们可以将元数据编码为文本并输入到编码器中。我们建议收集照片的所有重要属性并将它们连接成一个字符串——例如,按照以下模板:

“描述:{description}, 标签:{tags}, 位置:{location}”

生成此类提示是一种通用方法。我们确信描述和标签是元数据的重要组成部分,但我们可能还能制作更多。例如,在这里我们建议如果坐标是照片 EXIF 数据的一部分,则使用位置。可能还有更多类似的低垂果实——例如,从日期(以便反映季节)、相机型号等中构建特征。此外,我们可能需要过滤标签——例如,通过修剪标签列表到最有信息量的标签。

如前所述,从元数据创建特征有一定的灵活性。然而,我们拥有的特征越多,复杂性就越高。即使添加一个虚拟特征不会影响模型在指标方面的性能,它也可能增加训练和推理时间,因为基于 transformer 的模型在输入长度方面是二次的。因此,我们需要小心使用我们使用的特征数量,并应用特征选择技术以保持复杂性在控制之下。鉴于特征的性质,我们不能直接使用过滤方法,并且鉴于它们的数量较少,我们建议使用精确但较慢的方法(例如,顺序贪婪特征选择)。

ii. 特征重要性

我们至少需要在以下两种场景下访问特征重要性:

  • 我们需要对模型有一个整体的理解,以便为未来的工作设定优先级。例如,如果我们发现模型高度依赖于元数据特征,我们可能希望在该元数据特征工程管道上投入更多。同时,我们可能希望所有组件都对最终得分做出贡献,以减少过拟合和创作者利用模型弱点(例如,过度优化他们上传的图片的标签)的机会。

  • 对于排名质量,可能会有投诉,我们需要了解其原因。因此,我们需要能够解释特定查询和特定文档的排名。这可能会揭示未来改进的新机会,并检测到一些系统性问题。

幸运的是,我们不需要为这里描述的解决方案添加一个新组件,例如特征存储。然而,如果我们开始使用用户行为数据作为特征来源,这种需求可能会出现。在这种情况下,我们需要预先计算并将特征存储在某个地方,而特征存储是一个自然的选择。

摘要

  • 运行特征重要性分析将有助于实现可解释性和可说明性,确定对模型预测有显著贡献的特征,并指示模型是否已准备好部署。

  • 仔细的特征选择将使你能够提高模型的预测能力,最终提出一个准确、易于解释和说明,并且具有更快的训练和服务的解决方案。

  • 即使在与图像或文本等多模态数据一起工作时,也不要忽视特征工程的想法。

  • 考虑使用特征存储,因为它使团队能够在一个集中的中心计算、存储、汇总、测试、记录和监控他们机器学习管道中的特征。

  • 如果你正在处理多个来源的表格数据,拥有一个特征存储将特别有益。另一方面,纯深度学习问题不适合使用特征存储。

第十二章:12 衡量和报告结果

本章涵盖

  • 衡量结果

  • 从 A/B 测试中受益

  • 报告收到的结果

在前面的章节中,我们讨论了构成机器学习(ML)系统骨架的构建块,从数据收集和模型选择开始,继续到指标、损失和验证分割,最后是全面的错误分析。在管道中确立了所有这些元素后,现在是时候回到系统的初始目的,并考虑如何恰当地报告所取得的结果。

报告包括根据我们的机器学习系统的最终目标评估其性能,并将结果与团队成员和利益相关者分享。在第五章中,我们介绍了两种类型的指标:在线指标和离线指标,它们生成两种类型的评估:离线测试和在线测试。虽然离线测试相对直接,但在线测试意味着在现实世界场景中运行实验。通常,最有效的方法涉及一系列 A/B 测试,这是开发一个高效、正常工作的模型的关键程序,我们将在第 12.2 节中介绍。这有助于捕捉与我们业务目标直接匹配或高度相关的指标。

在离线测试(有时称为回测)中衡量结果是一种尝试评估我们可以在在线测试阶段期望捕捉到的效果,无论是直接还是通过代理指标。然而,在线测试通常是一个更复杂的故事。例如,推荐系统严重依赖于反馈循环,因此训练数据取决于我们在上一个时间戳所做的预测。此外,我们不知道如果我们向用户 A 展示了项目 X 或 Y 而不是在时间戳 T 的历史数据中出现的项目 Z,会发生什么。

最后,当实验完成时,我们准备好报告模型对我们感兴趣的业务指标的影响。我们报告什么?我们如何展示结果?关于系统细化的下一步,我们应该得出什么结论?在本章中,我们回答在过程中可能出现的疑问。

12.1 衡量结果

在实施机器学习系统之前,我们应该完全理解我们旨在实现的目标——这就是为什么我们的设计文档应该从目标描述开始。同样,在运行实验以检查变化如何影响我们的系统(在本章中,我们主要关注 A/B 测试)之前,我们设计它并概述一个假设,该假设涵盖了我们对给定变化预期如何影响指标的看法。这就是离线测试发挥作用的地方,因为使用离线评估作为在线评估的代理是一种有效且有益的方法。其目标是迅速确定新(修改后的)解决方案是否优于现有解决方案,如果是这样,则尝试通过多大差距来量化它。

如我们之前提到的,指标在层次结构中相互关联。对于我们的兴趣指标,存在其他代理指标的代理指标(请参阅第五章)。因此,有大量的方式进行离线评估。

12.1.1 模型性能

评估的第一层通常涉及对机器学习指标的基本估计,这些指标我们在第七章中介绍过。我们假设,除了离线指标(如均方根误差、平均绝对百分比误差或归一化折现累积增益等常见指标)之外,我们还提高了业务指标(平均每用户收入或点击率[CTR])。

预测评估是检查模型质量并迭代不同版本的最快方式,但它通常与业务指标(预测质量与在线指标之间的关系往往远非理想)相去甚远。

离线评估程序是可信的,对于评估质量非常有价值。然而,离线和在线指标之间很少存在直接联系,了解离线指标 A、B 和 C 的增加如何影响在线指标 X、Y 和 Z 本身就是一个问题。为了弥合离线和在线指标之间的差距,并使离线测试更加稳健,我们可以收集在线测试和在线指标以及相关的离线指标的历史数据,以计算离线和在线结果之间的相关性。

12.1.2 转换到业务指标

在某些情况下,我们可以从模型预测过渡到现实世界的业务指标。为了说明这一点,让我们考虑预测航空公司票务销售的例子。通常,我们用于预测的基本模型性能指标是加权平均百分比误差(WAPE)和偏差:

figurefigure

假设我们有两个模型,每个模型在预测特定航班特定小时的票务销售时都有±10%的偏差。在这个例子中,我们预测在给定时间段内售出 110 张票,而实际售出的票数是 100 张。此外,让我们假设我们拥有每天的实际票价。

我们的目标是避免过度预订(由于提供过多折扣票而错过的收入)并最小化未售出的座位(损失的收入)。假设售出的票数反映了乘客的实际需求,并且我们在每天开始时根据我们的预测调整价格。为了说明起见,我们对此粗略估计是可以接受的。

实际上,在 4 天内售出的票数是[120, 90, 110, 80]。预测的票务销售情况如下

  • 模型 A—[90, 90, 90, 90]

  • 模型 B—[110, 110, 110, 110]

模型的偏差是

  • 模型 Aequation image

  • 模型 Bequation image

模型的 WAPE 值是

  • 模型 Aequation image

  • 模型 B方程式图片

这两个模型具有相同的 WAPE 和偏差,但模型 A 具有 10%的负偏差(并倾向于低估),而模型 B 具有 10%的正偏差(并倾向于高估)。

每天的票价为[$200, $220, $230, $210]。

总收入(我们选择每天已售和预测票数的最低值)为

  • 模型 A—90 * $200 + 90 * $220 + 90 * $230 + 80 * $210 = $75,300

  • 模型 B—110 * $200 + 90 * $220 + 110 * $230 + 80 * $210 = $83,900

如您所见,模型 B 在短短 4 天内(而总飞行成本保持不变)就为单次航班带来了$8,600 的收入。当乘以数百次航班和天数时,这种差异将导致显著的收入增长。

当然,我们在现实世界的背景下过于简化了模型进行预测和影响决策的情况(例如,销售多少张票,如何动态调整价格)。在这里,整个过渡过程是通过乘以价格和可选地减去飞行成本来完成的。

尽管如此,这个例子说明了我们如何从可能不完全捕捉预测实际效果的模型性能指标,过渡到如收入这样的业务指标。这些业务指标可以在运行 A/B 测试之前报告给团队。

12.1.3 模拟环境

当风险很高时,投资于更复杂、计算成本更高但更稳健的离线测试程序是合理的。为手头的问题构建在线环境的模拟器并对该模拟器运行算法可以成为这样的投资。

这种环境的良好例子是时间序列预测,由于标签、辅助信息(例如,历史成本或库存商品的周转)和结果中的消歧的可用性,这种模拟相对简单。这通常不适用于在线推荐系统,如新闻、广告或产品推荐,在这些系统中我们处理部分标记的数据;我们只知道我们向特定用户展示的内容项的印象(查看、点击和转化),而那些我们没有展示的内容则没有用户反馈,因为总有可能,如果我们产生了不同的印象顺序,一切可能都会非常不同。

让我们回顾一下在推荐系统的情况下,如何创建这样的环境。

有时,在线推荐系统是通过多臂老丨虎丨机和上下文多臂老丨虎丨机方法解决的(通常,这个推荐系统子集被称为实时竞价,但简而言之,其目标保持不变——从多种选择中提供最佳选择以最大化特定奖励)。有一个代理,即我们的算法,它与环境“互动”。例如,它消耗有关用户上下文(他们的近期历史)的信息,并选择显示哪个广告横幅以最大化点击率(CTR)、转化率(CVR)或每英里有效成本(eCPM)。这是一个典型的强化学习问题,具有强大的反馈循环影响:之前显示的广告会改变未来的用户行为,并为我们提供有关他们偏好的信息。

所说的“重放”算法是一种简单(但不是最直观)的方法来为实时推荐构建模拟,该方法源于李红等人在论文“基于上下文多臂老丨虎丨机的新闻文章推荐算法的无偏离线评估”中的描述(arxiv.org/abs/1003.5956)。在这里,我们总结了这篇论文,因为它具有实用性:

  1. 假设我们选取一个历史数据集,该数据集包含用户上下文(用户特征)、推荐广告(比如说我们一次可以推荐一个项目),以及用户反馈(可以是二进制或连续的)。此外,对于数据集中的每一行,我们知道在特定时刻可以为该用户推荐的所有可用项目(项目是上下文多臂老丨虎丨机中的臂)。

  2. 此外,我们还有一个第二数据集,我们将其称为虚拟数据集。在模拟开始时,它是空的。

  3. 我们将历史数据集分成批次(例如,每次迭代的 1 小时间隔)。

  4. 对于单个批次中的每一行,我们根据提出的用户上下文和可用项目预测我们的算法可能会推荐哪个广告。我们的算法在初始为空的虚拟数据集上训练。因此,我们开始于随机的“探索”。

  5. 对于模型生成的推荐与历史选择广告相等的行,将保存到虚拟数据集中(包括他们的反馈)。

  6. 我们根据更新的虚拟数据集重新训练模型。

  7. 我们重复步骤 4 到 6,直到达到历史数据集的末尾。

对于步骤 4 和 5 也有一个优化版本:不是像真实环境中的一个项目,而是为每一行推荐三个或五个项目,如果推荐的项目与其中之一匹配,我们就将这一行追加到虚拟数据集中。这允许模型更有效地学习,同时减少随机性。

在图 12.1 到 12.5 中,一个视觉示例被分成几个部分,展示了无偏估计器的工作,其中图 12.1 显示了事件的真实历史,而图 12.2 到 12.5 展示了基于历史数据的模拟事件。

figure

图 12.1 无偏估计器(离线验证):真实事件流

对于这里的模拟事件,我们通过实际事件的历史来筛选,只考虑那些新模型(选择的着陆页)的输出与旧模型输出相等的事件。在图 12.2 中,模拟的用户行为显示了从横幅到着陆页的五个转换,其中有两个成功的购买和每点击预测利润 6 美元。

figure

图 12.2 无偏估计器(离线验证):事件模拟,示例 1

仅展示用户查看的四种商品,图 12.3 表明了最低可能的利润(10 美元)和每点击利润(2.5 美元)。

figure

图 12.3 无偏估计器(离线验证):事件模拟,示例 2

图 12.4 提供了另一个模拟用户行为的示例,具有相当高的转化率(50%),但由于购买成本低,每点击利润值(仅为 3%)却很低。

figure

图 12.4 无偏估计器(离线验证):事件模拟,示例 3

最后,图 12.5 中的模拟环境表明了最佳可能的 CVR(60%)和每点击利润(7 美元),而整体利润仅比真实事件流低 3 美元(见图 12.1)。

figure

图 12.5 无偏估计器(离线验证):事件模拟,示例 4

我们最终得到一个虚拟数据集,这是我们的模型与模拟数据流交互产生的。基于它,我们可以计算所有列出的指标:

  • CTR = 点击数/查看数(总行数)

  • CVR = 转化数/点击数

  • eCPM = 收入/点击数 * 1000

虽然不是理想的解决方案,但通过这个程序运行不同的模型将使从产品指标的角度评估它们成为可能,并理解这些在线指标估计与预测准确性之间的关系,然后向更广泛的受众提供这些信息。

12.1.4 人工评估

在将系统部署到生产之前,有时值得提及的最终选项是评估者的验证,或人工评估。如果模拟计算成本高且慢,由于带宽有限、延迟更长、人为引起的模糊性以及更高的成本,这可能会变得更糟。然而,在适用人工介入的情况下,这通常是测试模型最精确和最可靠的方式,除了直接在真实数据上在线测试之外。

例如,在构建搜索引擎管道的新版本后,我们可以要求一组专家对某些基准查询的搜索结果的相关性进行 1 到 5 的估计,或者选择哪个输出更相关,比较新旧版本的搜索结果。第二种(比较)方法往往会产生更稳健的评估,因为它排除了评分估计的主观性。在生成大型语言模型的情况下,人工评估和混合方法(基于人类反馈的辅助“评论”模型)是最受欢迎的选项来衡量生成的质量。

虽然人工评估的吞吐量低,成本高,可能会阻碍频繁使用,但其产生高度精确和可靠的评估(与快速但有限的自动化方法相比)的能力不容小觑。

12.2 A/B 测试

在包括市场营销、广告、产品设计在内的多个领域被广泛使用,A/B 测试是因果推断的金标准,它有助于做出基于数据的决策,而不是仅仅依赖直觉。你可能会有一个合理的问题:我们一直在讨论如何衡量和报告结果,现在我们突然跳上了因果推断的船;这是为什么?从某种意义上说,衡量结果是因果推断的一部分,因为我们想确保这些结果是由我们影响的因素(在我们的情况下,是一个机器学习系统)引起的。当然,我们可以将我们的系统部署到生产环境中,并测量所有感兴趣的指标,但我们很少这样做,主要原因如下:

  • 这很危险。如果出了问题,将其部署给所有人会导致进一步的损坏。因此,我们应该先将其部署给一小部分人。

  • 即使我们对前面的观点不关心(可能不是,但仍然如此),如果在我们控制范围之外发生了变化,影响了整体性能,但我们认为是我们自己导致了这种情况,从而得出错误的结论,那会怎样呢?

  • 这两个观点结合起来,使我们需要对照组(现状)和实验组(我们的变化),这有助于我们处理两者;一方面,我们可以改变实验组的大小,控制受影响的分数,另一方面,我们可以有对照组来比较对实验组产生影响的改变。

在 A/B 实验期间,我们将实体(在大多数情况下,是用户)分为两组:

  • 使用我们系统现有版本的对照组

  • 使用新版本的测试组

这使我们能够在控制可能影响结果的外部因素的同时,隔离变化对关键指标的影响(见图 12.6)。

figure

图 12.6 A/B 测试分割阶段

12.2.1 实验设计

每个实验都从一个假设开始:

如果我们执行[行动 A],它将帮助我们实现/解决[目标/问题 P],这将反映在基于[研究/估计 R]的[指标 M]的[X%]预期提升上。

让我们分解一下假设中提到的变量:

  • 行动 是部署一个新的解决方案来与现有的一个进行比较。例如,目标可能是减少 Supermegaretail 的缺货情况数量,并缩小预测销售和实际销售之间的差距。

  • 指标 为我们提供了一种量化我们目标进展的手段。例如,健身应用中的“减重”指标可以展示用户向其设定的目标体重进度的进展。参见第五章以获取更多见解。

  • 预期提升 可以从我们以往的经验、可用的基准、经验法则,甚至美好的愿望(这种情况很常见!)中得出。通常,离线测试提供的数据用于使其更加精确。

在运行 A/B 测试之前,至少有三个超参数我们必须定义:

  • 最小可检测效应(MDE),也称为最小可检测变化(MDC),是在给定研究设计和样本量的情况下,两组之间可以可靠检测到的最小差异。MDE 必须等于或小于预期提升,以避免忽略它。

  • I 型错误(假阳性)是在效应不存在时错误地得出效应存在的结论的概率。这种错误的典型阈值是 5%(用 α = 0.05 表示)。

  • II 型错误(假阴性)是在效应存在时未能识别出效应的概率。通常使用的阈值是 20%(用 β = 0.2 表示)。

随着数据的变动,这会影响测试所需的样本数量(因此,考虑到一些季节性因素,所需的测试时间),所有这些因素加在一起导致以下方程:

figure

其中 n 是每组所需的样本量,Z**[α] 是对应于所需显著性水平的 Z 分数(对于 I 型错误率),Z**[β] 是对应于所需功效的 Z 分数(对于 II 型错误率),s**² 是指标估计的方差,MDC 是最小可检测变化。正如我们之前提到的,非常常见的样本单位是 用户,但它也可能是 会话交易店铺级别 等等。

在制定假设和我们的期望之后,我们选择一个 分割策略,这主要取决于我们部署的系统性质以及它将如何影响现有流程。

接下来,我们决定统计标准并执行模拟测试,包括 A/A(我们比较相同的系统以检查 I 型错误率)和 A/B(我们通过添加期望大小的噪声来模拟期望的效果),作为对历史数据的合理性检查。这有助于我们确认我们是否达到了预定义的 I 型和 II 型错误水平。关于这一点的全面描述超出了本书的范围,但如果你对更多细节感兴趣,一个很好的起点是 Ron Kohavi 等人所著的《可信在线受控实验:A/B 测试实用指南》(mng.bz/86Qz)。

最后,我们拥有了启动我们的 A/B 测试所需的一切。记住,实验的设计是在事先固定的,不能在过程中改变。接下来,我们简要概述 A/B 测试的基本阶段。

12.2.2 分裂策略

问题在于,我们如何分割数据?通常的答案是“按用户”。例如,我们有一个控制组用户与现有的搜索引擎互动,一个实验组使用新版本。然而,当我们不能按用户分割(例如,没有一致的用户 ID)或者按用户分割对我们服务完全无关时,事情就变得更加复杂。

为了更深入地了解,让我们考察在不同领域应用的不同假设定价系统:

  • 线下零售中,在同一个商店内向客户展示不同的价格标签在物理上是不切实际的。因此,一个更适合分割的单位是商店而不是单个客户。

  • 在线零售中,虽然技术上可以向不同的用户显示不同的价格,但这也可能导致法律问题,因为许多国家都有法律禁止在电子商务中对用户进行歧视。

  • 共享出行中,用户可以通过应用程序下订单来分离,但我们必须考虑潜在的负面用户体验。如果我们按用户分割,并且两个用户决定一起骑滑板车,一个用户可能在他们的应用程序中看到每小时 5 美元的价格,而另一个用户则是每小时 6 美元。这种差异可能导致令人困惑和负面的用户体验和反馈。

  • 当涉及到应用于贷款和信贷利率的动态定价时,提供的利率预计将非常个性化,就像抵押贷款一样。因此,即使两个用户在同一房间里,不同的“价格”也不会引起注意。在这种情况下,按用户分割相对安全。

  • 广告网络提供了一个理想的用户测试案例,因为它们提供了大量多样化的、相对低成本的独立数据点,并且每个用户的点击成本既常见也预期。这种设置通常允许更传统的基于用户的分割。

如果按单个用户分割数据不可行,我们可以使用更高级别的实体作为原子单位。对于线下零售,我们可以使用整个商店;对于电动滑板车,可以根据停车区、社区、城市甚至地区来划分行程。然而,这种策略可能会改变数据分布,造成样本大小不均或缺乏代表性,这在选择合适的统计测试时应该予以考虑。

当按非典型键分割时,追求组(桶)相似性至关重要。例如,如果你被迫按地理划分,所选区域应该是相似的,并具有可比的经济指标(它们应该相互匹配)。

最后,还有一种更高级的分割策略,称为回溯测试。这种技术将数据分为区域-时间桶,并在不同模型之间随机且持续地切换(见图 12.7)。它确保每个区域将在控制组和测试组中大约相等的时间内。有关更多详细信息,请参阅 Iavor Bojinov 等人撰写的“回溯实验的设计与分析”(arxiv.org/abs/2009.00148)。

figure

图 12.7 回溯分割模式,表示时间-区域单元格分配

12.2.3 选择指标

在设计 A/B 测试时,选择指标是一个关键步骤,它可能是测试成功或失败的决定性因素。我们更喜欢在线测试中可信赖、敏感、可解释的指标,并且反馈延迟低。

每个实验通常都有三种类型的指标:

  • 关键指标是实验旨在改进的主要指标。这些指标直接影响我们商业目标的成功,并用于确定实验的最终结果。例如,电子商务网站的关键指标可能是转化率(CVR)、平均订单价值(AOV)或总商品价值/收入。

  • 控制指标在实验期间预期保持不变,作为对其有效性的检验。例如,如果网站访问量(或页面加载时间)的控制指标显示出显著的下降(或上升),这可能表明实验设计存在问题或外部因素的变化,这些因素可能会影响结果。

  • 辅助指标提供了关于实验的额外信息,但不用于确定最终结果。这些指标有助于更深入地了解实验结果,并可用于识别潜在问题或改进的机会。例如,电子商务网站的辅助指标可能是产品查看次数或网站上的时间花费。

所有三个组在 A/B 测试实验的设计和执行中都发挥着重要作用。关键指标决定了实验的成功,控制指标确保实验的有效性,辅助指标提供额外信息以支持结果并指导未来的实验。

12.2.4 统计标准

任何 A/B 实验的第三个基本要素是统计测试,它做出最终决定,即是否捕捉到显著效果。

简而言之,统计标准用于量化在特定假设下获得特定结果的概率。例如,获得等于或大于 3 的 t 统计值的概率通常将小于 0.01(如果两组之间没有差异,并且满足 t 测试的假设)。

在 A/B 测试中最常用的统计测试是我们刚才提到的 t 测试。它有许多修改。例如,Welch 的 t 测试相对容易解释,类似于 Student 的 t 测试,但它可以在两组的方差不相等的情况下使用。Welch t 测试的统计量计算如下:

figure

对于 Welch t 测试的零假设(测试结果的默认假设)意味着两组之间的均值没有差异;在这里,两组之间的捕获差异是偶然出现的,是由噪声引起的。备择假设是捕获的差异不是由噪声引起的,差异是统计显著的。

通过统计显著性,我们指的是在假设零假设为真的情况下(我们称之为p 值),捕捉到特定统计值或更极端值的概率小于显著性水平 α(实际上,等同于第一类错误;通常,α = 0.05)。p 值,连同显著性水平,是标准化任何统计测试结果并将其映射到单一参考点的方法,而不是为每个特定的测试定义临界值。

尽管有许多不同的统计测试可用,但特定测试的选择取决于实验的背景和数据性质。尽管如此,提供选择最合适的测试的详细指南超出了本书的范围。关于选择合适的统计测试进行 A/B 测试的更详细讨论,我们再次推荐 Ron Kohavi 等人所著的《可信在线受控实验:A/B 测试实用指南》(Trustworthy Online Controlled Experiments: A Practical Guide to A/B Testing) (mng.bz/EOKd)。

t 检验被广泛使用,因为它是一种稳健的测试,可以处理各种情况,易于实现,并且提供易于解释的结果。然而,选择特定的测试应该始终考虑到你处理的数据类型、实验的性质以及每个测试所需的假设。

12.2.5 模拟实验

我们可以通过模拟来检查我们是否成功设计了一个实验。我们在不同的样本和时间段上多次复制整个流程。

一个模拟的 A/A 测试涉及随机抽取两组没有产生差异的样本,并应用选择的测试统计量。我们重复这些动作很多次(比如说 1,000 或 10,000 次),并期望 p 值遵循均匀分布(如果一切正常)。例如,在 5%的模拟中,测试将以α = 0.05 拒绝零假设。测试确实捕捉到两组差异的确切百分比是一个模拟的第一类错误率

一个模拟的 A/B 测试做的是类似的事情,但现在我们给第二组 B(我们假设 A 组和 B 组的大小与真实 A/B 测试时相同)增加一个特定大小的提升(通常是感兴趣的 MDE)。再次,我们应用我们的测试统计量,并运行它 1,000 到 10,000 次。之后,我们重新检查结果的 p 值分布,并计算我们拒绝了零假设(“p 值通过了阈值”)的次数。选择测试拒绝零假设的案例百分比是敏感度的估计(1 – β),即测试在存在差异时捕捉到差异的概率。如果我们从敏感度中减去 1,我们得到第二类错误率(当存在 A 和 B 之间的差异时忽略差异的概率)。如果计算出的 I 型和 II 型错误率符合预定义的水平,那么统计测试就被正确选择,样本大小也被正确估计(见图 12.8)。

figure

图 12.8 模拟提供了一个最终的验证,确保一切正确设置,可以运行 A/B 测试。

12.2.6 当 A/B 测试不可行时

有一些情况下,运行经典的 A/B 测试是不可行或不理想的——可能是因为法律限制(某些行业,如医疗保健和金融,有严格的法规,禁止对客户进行实验),物流限制(可能很难将客户基础随机分成两组进行测试),或其他原因。

有不同的方法来解决这个问题,但由于这是一个超出本书范围的主题,我们不会详细讨论。为了您的方便,我们在这里列出它们,以便您可以将它们用作更深入研究的关键词:

  • 因果效应

  • 差分法

  • 合成控制

  • 中断时间序列分析

  • 回归不连续设计

  • 因果推断

12.3 报告结果

监控正在进行中的实验对于确保其顺利运行并产生可靠的结果至关重要。如果在实验过程中出现问题,及时识别和解决问题以避免负面影响的扩大至关重要。有时可能排除特定用户、项目或段落的实验,并继续进行。然而,如果这不是在初始设计中考虑的,则不建议这样做。在其他情况下,我们更喜欢完全终止实验并调查导致失败的因素。

来自阿列克谢的营火故事

在一个额外灵活的初创环境工作的那些人可能会认为所有的报告都只适用于大公司,但这并不正确;即使在小型公司,也应投入足够的努力以确保适当的报告和实验管理。我通过艰难的方式学到了这个教训:我曾经在一个相对较小的公司工作,那里只有三名机器学习工程师,我的任务是提高一个模型的核心性能。经过数月的研发和实验,我设法取得了良好的成果,得到了我的经理和公司 CTO 的高度赞赏。当我准备将新模型投入生产实施时,CTO 的一条消息弹了出来:“嘿,我们明天要开董事会会议,我想在幻灯片上突出你的成就。请给我提供关于 X 的详细信息和 Y 的粒度。”

我按照要求尝试制作了一份报告,但突然,经过仔细分析,数字显示新模型的性能实际上比旧模型差!看起来很糟糕,但我绝对不打算通过报告不存在的更好结果来欺骗 CTO。鉴于这是我在这家公司的第一个大项目,这看起来非常可疑。我们三个人——CTO、我的经理和我——开始深入调查这个问题。

直到快到午夜,我才找到了根本原因:在运行额外实验时,我意外地用一些不成功的实验覆盖了最佳模型的结果,并使用这些数据为董事会会议报告。因为我是在处理这个问题的唯一一个人,我没有足够注意这一点,而且缺乏适当的报告和实验管理(只是在开发机上存储的半随机数据块)导致了这一事件。

12.3.1 控制和辅助指标

在 12.2.1 节中,我们提到了在测试期间应监控的控制和辅助指标。它们可以在关键指标出现显著下降之前提示出问题。例如,跟踪用户反馈很重要——如果你注意到用户参与度显著下降或用户流失率激增,这是一个明显的故障指标。在这种情况下,这可能表明实验组对推入的变化反应不佳。这些信息可以轻易导致实验提前终止。

此外,在进行 A/B 测试时,我们应该考虑不同用户群体之间的公平性和偏见,这可能会影响目标和辅助指标。这种分析与我们第九章中深入探讨的内容相似。关于进一步阅读,我们推荐 Guillaume Saint-Jacques 等人所著的《Fairness through Experimentation: Inequality in A/B Testing as an Approach to Responsible Design》(arxiv.org/pdf/2002.05819.pdf)。

12.3.2 提升量监控

在 A/B 实验期间,最有价值的测量指标是提升量,即 A 组和 B 组关键指标之间的相对差异。实验运行时间越长,测试的敏感性就越高,我们能够检测到的效果(无论是正面还是负面)就越小。

图 12.9 显示了表示我们无法以统计显著性检测到的效果范围的漏斗。如果提升量移动到这个漏斗之外,那么效果是显著的。请注意,如果没有具体的设计,你不能像你希望的那样频繁地查看测试,直到看到期望的结果。关于这个主题的进一步阅读,我们推荐 Oleg Ya 的《Peeking Problem—The Fatal Mistake in A/B Testing and Experimentation》(gopractice.io/data/peeking-problem/) 和 Mårten Schultzberg 等人所著的《Sequential A/B Testing, Further Reading Choosing Sequential Testing Framework—Comparisons and Discussions》(mng.bz/NBon)。

figure

图 12.9 时间累积提升动态示例,以及具体的置信区间

这个漏斗可以根据 MDC 计算得出。在这里,我们需要解决之前看到的相同方程——然而,这次是针对不同的时间段——但我们不能在没有提前计算出的时间段过去之前做出决定。你可能会问:那么使用它的意义在哪里呢?答案是,为了有一个紧急停止标准,如果结果超出了漏斗并且是负面的——这是最不期望、最不希望和最不可能的结果,我们可以终止实验。对于之前提到的任何顺序测试框架(例如,mSPRT),我们可以在结果超出漏斗或实验时间结束时立即做出决定。

12.3.3 何时结束实验

基于中间结果提前停止或延长 A/B 测试可能会很有诱惑力。然而,如果没有采用顺序测试,这可能导致由于统计功效的变化以及假阳性或假阴性的风险而得出错误的结论。所以,如果我们没有进行顺序测试,我们应该在什么时候停止实验?

通常情况下,实验应该按照预定的设计进行,除非关键指标出现灾难性的下降(显著的负面效果),如果实验继续进行,这将导致有意义的财务损失。

另一方面,如果你在关键指标中看到积极的效果,最佳实践是坚持初始计划,让实验完整运行。这让你可以确认这些积极效果随着时间的推移而持续,并且不是短期波动的结果。

对于那些边界案例,一些指标是正的,一些是负的,还有一些在 MDE 周围徘徊的情况怎么办?在实验设计阶段,决定你将如何解释这些混合结果,并在不同的关键指标之间设定优先级是至关重要的。

如果你迫不及待地想看到最终结果,考虑使用如顺序测试等方法,这允许在实验的不同阶段进行重复的显著性测试。这些方法有其自己的假设和考虑因素,所以在进行之前确保你完全理解它们。

决定是否停止或继续实验是一个复杂的任务,需要仔细考虑指标、潜在结果及其影响。在你的实验方案中记录这些决定,包括可能的早期终止标准。

12.3.4 应报告的内容

实验完成后,是时候开始我们的分析了。在这里,我们需要计算所需的指标,双重检查监控,并为每个测量提供置信区间。

并非每个实验都是成功的。根据我们的经验,有 20%的 A/B 测试显示出统计学上的显著差异是相当不错的(记住,通常如果假阳性率为 5%,则差异为 15%)。并且记住:如果测试在统计学上是显著的,并不意味着结果是积极的或巨大的。

假设我们决定 A/B 测试是成功的,效果是显著的。在这种情况下,我们有两种选择:

  • 报告点估计效果—“效果是显著的,等于 X,”其中 X 是实验组和对照组指标值计算出的差异。

  • 估计效果置信区间—如果效果的最悲观估计等于差异的下置信界限,保守估计等于点估计差异,乐观估计等于差异的上置信界限。

我们已经提供了报告的核心信息(当然,报告的指标应该对观众来说是可理解的,最好用赚取或节省的钱、更长的用户会话等来表示)。此外,深入分析指标的变化,分析它如何影响不同的用户或项目细分,并概述进一步步骤(如果我们汇总到 100%,整体指标可能会如何变化)是很好的。表 12.1 显示了报告表中可以包含的字段示例。

表 12.1 报告表中可以包含的字段示例
指标 组 A 组 B MDE 提升率 p 值 结论
CVR 75.2% 79.8% 5% 6.12% 0.0472 +4.2–6.9% (显著)
AOV $232.2 | $242.8 11% 4.57% 0.3704 无显著影响

一旦提升被报告,可能的积极场景可能包括在更大比例的数据上进行一个额外的实验,或者完全切换到新系统——或者全面推出、暂停和反向的 A/B 测试。一系列成功的 A/B 实验将为您提供坚实的基于数据的论据,这将作为在决定是否批准进一步步骤时的充分支持。

12.3.5 简报文档

编写简报文档对于透明地沟通 A/B 实验结果以及提高未来实验的质量都是很有价值的。它应该在实验期间或实验结束后立即创建,以总结关键发现,包括捕获的见解、检测到的问题和建议。与团队分享此文档确保每个人都处于同一页面上,并能够持续学习和改进系统。

如果实验成功,简报文档将包括对其他产品中类似实验的建议。如果实验失败,讨论在未来的测试中应该做些什么不同,应该避免哪些错误,以及开发新的控制指标以防止在实验早期发生类似的失败,这些都是非常重要的。

12.4 设计文档:测量和报告

由于在准备设计文档的预生产阶段无法设计报告,因此默认情况下应跳过。然而,我们已将其包括在内以供演示目的。

12.4.1 Supermegaretail 的测量和报告

作为设计文档的一部分,对 Supermegaretail 的测量和报告有其独特的特点,其中之一是,由于我们正在预测未来,我们只能在未来到来时评估系统的质量;最终,我们希望了解我们将能够产生多少利润,但我们只能事后知道这一点,而评估这一点的唯一方法是通过某些与直接相关的指标无关的指标。

设计文档:Supermegaretail

IX. 测量和报告

i. 测量结果

作为提高已部署的销售预测模型预测质量的第一步,我们计划尝试将现有的模型(每个类别一个)合并成一个。背后的理由是,我们将编码关于物品组的信息,而不会损失具体性,但将获得更多数据,从而实现更好的泛化。即使没有质量上的改进,维护一个模型也比维护多个模型容易得多。

作为离线指标,我们考虑了不同的分位数、百分比误差和偏差。但我们不仅评估整体得分,还检查了特定类别的指标和错误分析。这些离线测试产生了以下中间结果:

  • 在所有指标上,一般预测质量稳步提高,并且在切换到分割模型时,大多数验证折都得到了提高。

  • 与基线(多)模型相比,产品数量较少的类别在离线指标上有所增加。他们拥有的数据量不足以学习到有意义的模式。大型类别在切换到统一模型时,改变了很多胜利。结果在不同季节和关键地理区域都是可重复的。

以前,A/B 测试基于离线指标提供了一些对每个主要类别预期提升的估计。如果我们将产品 P 的指标 M 减少 X%,将导致缺货(缺货)的利润减少 Y%,并减少由于过剩库存情况造成的损失 Z%。根据我们的估计,试点组的总收入预期将增加 0.3%至 0.7%。

ii. A/B 测试

  • 假设—实验假设是根据离线指标的改进,我们预计收入至少会增加 0.3%。

  • 关键指标—在 A/B 实验中,我们可以使用的最佳代理指标是平均检查金额。它与收入(假设检查次数没有变化)完美相关。统计测试的原子观察将是单个检查。

  • 分割策略—我们按配送中心进行分割,并通过它们获取两组商店,因为每个中心服务于一组商店。从这些集合中,我们选择彼此代表性的子集,并将它们用作组 A 和 B。

  • 附加指标—实验的控制指标是

    • 每天检查次数—检查销售量是否有显著下降。

    • 模型的更新频率—模型是否定期积累新收集的数据?

    • 模型的离线指标—分位数指标、偏差、WAPE。

  • 辅助指标

    • 每日收入

    • 每日利润

  • 统计标准—我们将使用 Welch 的 t 检验来捕捉样本之间的差异。

  • 错误率—我们将显著性水平设定为 5%,II 型错误为 10%。

  • 实验持续时间—根据我们的计算,两周时间足够检查结果。然而,考虑到配送中心的补充节奏为一周,我们将此期限延长至整整一个月。

iii. 报告结果

应提供包含以下章节的报告:

  • 结果—以主要和辅助指标的 95%置信区间表示

  • 图形表示—特定日期特定指标的值,以及来自控制组和处理组的所有指标的值,以便于消费

  • 绝对数字**—例如,每组商店的数量、总检查次数和总收入

  • 方法**—例如,如何选择代表彼此的组,运行模拟以检查 I 型和 II 型错误等。(见附录以获取更多详细信息)

  • *推荐/进一步步骤**—基于收到的结果下一步该做什么

12.4.2 为 PhotoStock Inc.测量和报告

我们将这一阶段作为 PhotoStock Inc.案例的模板。

设计文档:PhotoStock Inc.

IX. 测量和报告

i. 测量结果

以下是我们基线列表中的离线指标:

  • NDCG@10(人工)—*我们使用人工标记的相关性分数作为搜索结果页面(SERP)中每张照片的相关性评分。

  • NDCG@30(隐式)—我们使用隐式反馈作为相关性评分:

    • 3—购买的图片

    • 2—添加到收藏夹的图片

    • 1—点击的图片

    • 0—无交互的浏览

  • MRR(人工)—被标记为相关性的第一张照片的平均位置。

  • MRR(点击)—*在搜索结果页面(SERP)中首次点击的照片的平均位置。

  • MRR(购买)—在搜索结果页面(SERP)中首次购买的照片的平均位置。

我们回顾性地将这些指标应用于当前的非机器学习搜索算法,通过收集每个查询显示的搜索结果页面(SERP)。然后,我们在不同的时间段内测量我们的在线指标(点击率 CTR 和转化率 CVR)。之后,我们计算 Spearman 等级相关系数,并选择与每个在线指标相关性最强的离线指标。所选的离线指标随后用于一个简单的回归模型,以提供一个 A/B 实验中 MDE 的初始估计。

由于这是第一次 A/B 测试,我们还没有用于更精确 MDE 估计的比较数据。然而,对于未来的测试,我们提出了一种系统性的方法,我们将收集和关联每个 A/B 测试中 A 和 B 变体之间的离线和在线指标差异。最终目标是建立一个近似的关联:对于离线指标 C 的 X%改进,我们观察到在线指标 D 的 Y%提升。

进一步细化我们的在线指标估计可能涉及构建用户行为模拟。然而,这将是一个更复杂的任务,我们相信当前的方法为我们测试提供了一个坚实的基础。

除了上述的自动化离线测试方法外,我们还可以结合评估者的反馈。我们提出了两个评估任务:

  • 给定查询 Q 和新搜索引擎建议的前 20 张照片,评估者将根据其相关性将整体搜索结果页面(SERP)从 1 到 5 进行排名。

  • 在展示查询 Q 和两个不同搜索引擎(一个是当前版本,另一个是新版本)的前 20 张照片后,评估者将确定哪个搜索结果页面(SERP)更相关。

第二个任务允许进行比较评估,这通常通过减少个人评分偏差的影响,产生更稳健的评价。

ii. A/B 实验设计

假设我们已经收到了离线指标中最低预期效果的近似估计:点击率(CTR)+5% 和转化率(CVR)+20%。我们的实验假设如下:

  • 如果我们将当前的搜索引擎切换到我们的基于机器学习的解决方案,它将提供更相关的搜索结果,这将在点击率(CTR)至少提高 5% 和转化率(CVR)至少提高 20% 的基础上反映出来。

因此,我们的关键指标是点击率(CTR)和转化率(CVR)。

控制指标在实验期间不应发生变化

  • 每个变体的浏览量—如果不相等,要么是分割策略有问题,要么我们在测试组中极大地影响了用户体验。

  • 响应延迟—这不应该增加太多,至少要符合服务等级协议(SLAs)。

  • 每秒查询数—如果这个值低,搜索引擎似乎变得不可用。

  • 每日查询数和从搜索栏开始的会话百分比—两者都反映了用户参与度;我们预计这些数字不会下降。

  • 客户支持报告的数量—如果出现峰值,可能意味着有问题。

辅助指标提供额外的信息,但不直接参与决策

  • 在 SERP 上花费的平均时间—如果增加,这可能表明搜索引擎变得更差,用户需要花费更多时间,进行更多查询,滚动更多页面来找到他们想要的东西。

  • 每个变体的点击数和购买数—两者都将报告导致转化率提高的因素(分子、分母或两者以不同的比率)。

  • 总利润和每笔购买的平均利润

  • 点击照片在搜索结果页面(SERP)中的平均位置和每次会话的平均查询数—这是一个棘手的指标,在某些假设下,其下降可能表明用户导航变得更容易,找到所需内容所需的动作和时间更少。

  • 会话结束时的购买率—这是转化率(CVR)的一个不太敏感的版本。

  • 分割策略—新的搜索引擎不会向用户体验添加额外的依赖,所以我们不会因为用户的唯一 ID 而以标准方式分割用户,没有任何东西阻止我们这样做。

  • 统计标准—考虑到我们的目标是提高点击率(CTR)和转化率(CVR)(两者都是比例指标),我们选择比例 Z 测试。

我们将使用保守的错误率来处理第一类和第二类错误:α = 0.01 和 β = 0.05:

  • 样本大小—在测试之前,让我们分析一下我们拥有的数据:

    • PhotoStock Inc. 网站每日有 100,000 次访问

    • 平均每次访问的搜索查询数:1.5

    • 每次查询后平均查看的照片数量:20

    • 平均查看照片的点击率:10%

    • 平均点击照片的购买率:1%

因此,我们每日有 100,000 × 1.5 × 20 = 3,000,000 次浏览:

  • 基准点击率(每浏览次数点击数)为 10%

  • 基准 CVR 是 10% * 1% = 0.1%(每浏览次数的购买数)

因此,我们每日有 300,000 次点击和 3,000 笔购买。

之前,我们讨论了为了避免可能的风险,我们希望逐步推出。所以对于第一个 A/B,我们将只对两组各取 10%(组 A 为 5%,组 B 为 5%)。这意味着我们每天每个组有 3,000,000 × 2.5% = 150,000 次观看。

实验应该持续多长时间?以下是基于所需 MDE 和错误率的样本大小估计公式:

figure

这里p**[A]p**[B]是旧和新的比例(在我们的情况下,是 CTR 或 CVR),而Z**[x]是给定水平的 Z 统计量的临界值。

我们有 10%和 10.5%作为旧和新的 CTR 值:

figure

这导致需要 131,095 次观看,而我们每天每个组有 150,000 次观看。这意味着我们只需要 1 天就能收集到检测到显著增加(5%或更高)所需的数据量。

关于转化率呢?旧版本为 0.1%,新版本为 0.12%。再次应用前面的公式将导致需要 978,693 次观看。因此,我们需要 7 天来收集这些数据。

总结来说,实验将持续一周以满足两个指标。一周也是好的,因为它将涵盖整个周的季节性周期,如果有的话(例如,对于 PhotoStock Inc.,工作日可能有更多专业用户,周末可能有更多业余用户)。

为了检查我们是否一切都做得正确,我们将多次运行模拟的 A/A 和 A/B 测试,从估计大小为二项分布的样本中生成,并具有所需的 CTR 和 CVR 值,以确保计算出的错误率低于期望值。

iii. 报告模板

在实验期间,我们主要监控 MDE 动态。正如预期的那样,CTR 效果在第二天就具有统计学意义。相比之下,在实验的第四天才检测到 CVR 的统计学意义。

figure

CTR 效果

figure

CVR 效果

到最后,我们收集了以下指标值:

  • 组 A 的 CTR 为 9.91%,组 B 为 10.51%(相差 6%)。

  • 组 A 的 CVR 为 0.10%,组 B 为 0.13%(相差 29%)。

我们得出初步结论:

  • 新的搜索引擎仅略微增加了每观看一次的点击次数(+6%);然而,可点击的图片结果证明更加相关(转化为购买的转换率增加了 29%)。因此,A/B 实验是成功的。

消耗效果报告

我们应用了自举程序,从点估计效应转换为分布。我们将 95%置信区间的下限和上限视为悲观和乐观的效应估计。我们使用原始的点估计作为保守估计。

figure

自举 CTR

我们报告以下 CTR 效果:

  • 悲观估计为+5.1%(新点击率是 10.4%)。

  • 保守估计为+6%(新 CTR 为 10.5%)。

  • 乐观估计为+6.8%(新 CTR 为 10.6%)。

figure

自举 CVR

我们报告以下 CVR 的效果:

  • 悲观估计为+19.2%(新 CVR 为 0.12%)。

  • 保守估计为+29%(新 CVR 为 0.13%)。

  • 乐观估计为+39.4%(新 CVR 为 0.14%)。

扩大效果

当我们将新测试的基于机器学习的搜索引擎扩展到所有流量时,让我们以收入来评估这些收益,假设平均照片利润($3.0)和每日访问量(300 万)保持不变。

基线 CVR 为 0.1%。乘以每日流量和费用,我们得到每日收入$9,000 或每年$3.3 百万:

  • 悲观估计给我们带来每日$10,800(+$1,800)或每年$3.9 百万(+$600,000)。

  • 保守估计给我们带来每日$11,700(+$2,700)或每年$4.3 百万(+$1 百万)。

  • 乐观估计给我们带来每日$12,600(+$13,600)或每年$4.6 百万(+$1.3 百万)。

这些数字完全证明了我们所有的努力和时间。

进一步的步骤将需要运行两个到三个较短的 A/B 实验,以覆盖更大比例的用户(例如,20% -> 30% -> 50%),以确保系统安全,效果可重复,实验结果可靠。

考虑以下旁注:

  • 控制指标在实验期间未检测到任何事件。

  • 平均搜索时间略有下降,这是一个好兆头。这意味着用户体验得到了改善,用户可以更快地找到他们需要的东西。

  • 以购买结束的会话数量增加。SERPs 变得更加相关。

  • 每张照片的平均利润保持不变(约$3.0)。

实验结束后,我们还回顾性地估计了在新模型中点击的照片中有多少在顶部 1,000 名之内。结果是,这些点击的 Recall@1000 为 97%。这一重要发现暗示了系统进一步改进的可能性,特别是通过添加检索阶段(候选模型)转向两阶段管道。

摘要

  • 在部署和运行在线测试阶段之前,使用离线评估来获取您机器学习系统预期效果的近似值。

  • 有各种方法可以推导出离线和在线指标之间的关系,包括基准和之前运行的 A/B 测试及其相应的离线指标。

  • 要关于新模型是否优于旧模型(或基于机器学习的解决方案是否优于非机器学习解决方案)做出数据驱动的决策,行业标准是 A/B 测试(然而,还有其他选项可以推导出因果关系)。

  • 为了顺利且安全地运行 A/B 测试,我们需要制定一个合理的假设,选择正确的关键/控制/辅助指标,估计预期的提升和实验持续时间,并在进行实际测试之前,有时进行模拟的 A/A 和 A/B 测试。

  • 在向利益相关者和团队成员报告实验结果时,我们需要传达捕捉到的效果,如果推出服务,预期会产生什么效果,不同细分市场看到的效果是什么,实验过程中发现了什么问题,以及下一步和进一步改进的建议。

第四部分 整合和增长

这是本书的最后一部分,致力于整合和增长。第十三章涵盖了整合,从 API 设计和发布周期到操作系统,以及在系统出现故障时转向回退。在第十四章中,我们讨论了监控和可靠性、软件系统健康、数据质量和完整性,以及模型质量和相关性。第十五章概述了服务和推理优化,在服务和推理阶段可能出现的挑战,首选的工具和框架,以及优化推理管道等话题。最后,第十六章回顾了所有权和维护,责任作为拥有健康机器学习系统的一个关键因素,团队效率与冗余之间的权衡,适当安排文档的基本重要性,以及复杂性的欺骗性吸引力。

第十三章:13 集成

本章涵盖

  • API 设计

  • 发布周期

  • 操作系统

  • 覆盖和回退

如我们之前所声称的,你能做的最糟糕的事情就是构建一个系统,然后只是把它放在架子上而不是让它上线。我们俩在我们的职业生涯中至少都遇到过这样的问题,这不是我们推荐的经历。

一个新手错误的想法是认为集成是一次性的事件或项目的一个阶段。这是一个反模式:你不能只是为未来的集成分配几周时间,然后在真空中开始构建系统。实际上,它是一个从项目一开始就开始,直到系统退役才结束的持续过程。更重要的是,当系统的生命周期结束时,它需要一定的解集成努力,确保没有任何直接或间接的用户会受到关闭系统的影响。适当的集成是您系统成功的关键,使您更容易获得反馈并改进。各种元素集成到您的系统中的越平滑,反馈循环就越短,您可以实施的迭代就越快。

在本章中,我们讨论如何高效地集成您的系统,重点关注技术方面。

13.1 API 设计

API 设计是集成过程中的关键部分。它可能被视为系统与其用户之间的合同,但这是你在签署之前需要彻底阅读的合同。这是因为一旦 API 设计被设置,即使它不是一成不变的,系统仍在开发中,更改 API 设计也将是代价高昂的。

如果你是一位在机器学习(ML)方面经验丰富的读者,你可能觉得可以跳过这一节,仅仅因为你已经知道如何设计 API 并且已经多次这样做。这是一个合理的说法,因为我们不会教你 REST 和 RPC 之间的区别,或者如何一般性地设计 API。此外,关于这个主题有大量的优秀书籍和文章(例如,可以在news.ycombinator.com/item?id=24383180找到一些推荐的资料集合)。相反,我们将专注于关键方面,并突出 ML 系统特有的陷阱。

如果我们要挑选出好的 API 的两个属性,我们会选择简单性可预测性。Butler Lampson 有一句经典的软件名言,甚至被称为“软件工程的基本定理”:“我们可以通过引入一个额外的间接层来解决任何问题。”

这句话的一个变体是,任何编程问题都可以通过一层抽象来解决,除了太多抽象的问题。所以 API 的简单性是找到正确抽象的艺术,这种抽象不会泄露太多底层实现细节。

简单性的关键作用在于其使 API 更易于学习和使用,而无需深入了解内部结构。一个典型的机器学习系统通常有许多处理程序和参数,总是有向外部用户暴露它们的诱惑。这导致了解决方案过于复杂,调用方法需要提供多个参数,最终很难理解它们的含义以及它们是如何相互关联的。更好的方法是在简单的界面后面隐藏复杂性,并提供一些参数数量较少的方法。用户会为此感到感激。

然而,隐藏所有参数也不是最好的主意(见图 13.1)。为系统行为提供定制方式很重要,尤其是在调试目的上。想象一下,在一个深夜的值班期间,你正在调试一个有十几个参数的系统,而你无法修改任何一个参数。这不是一个愉快的体验!在这些情况下,建议合理的默认值并提供一种覆盖它们的方式总是很有意义的。

figure

图 13.1 过度配置与不足配置
阿尔谢尼的营火故事

我曾经为一家公司工作,该公司为外部开发者提供 API。我负责构建一个全新的端点,该端点将镜像底层的分类系统。在早期阶段,该 API 似乎非常简单——只需接受一个对象作为输入,并从预定义的分类法中输出一个标签。

尽管基线准确度并不完美,我的一个同事建议返回一个标签列表而不是单个标签。这是一个合理的建议,我毫不犹豫地实施了它。然而,实践表明,即使在标签不正确的情况下(他们的使用模式无法使用第二个标签或更多),用户也只需要一个标签。坏消息是标签列表已经暴露在 API 中,如果不破坏兼容性,很难将其移除。因此,API 无端变得过于复杂,许多用户对此表示了不满:“为什么你返回一个标签列表,而它总是包含单个项目?”过于草率的 API 设计决策导致了一个次优的解决方案,后来很难修复。

可预测性是良好 API 的另一个关键属性。我们已经讨论了机器学习系统通常是非确定性的,除非它们被强制有意地(请参阅第十章)。这对于 API 来说是一个更加关键的因素,因为它们必须是确定性和可预测的。确保相同的输入总是产生相同的输出是很重要的。当然,有一些算法是非确定性的(例如,带有温度采样的文本生成),但这只是例外,证明了规则。

总是有强制实现确定性行为的可能性。一个简单的例子就是从参数中获取随机种子(如果没有指定,可以选择自己的种子)。顺便说一句,虽然许多机器学习库使用全局状态的随机种子,但在 Google 新近出现的数值计算库 JAX 中则不行。其设计表明,你必须明确传递随机状态,正是为了这个原因——强制实现完全可重复性。更多信息请见 mng.bz/lrQ2

非确定性的另一个来源可能是输入数据,包括一些隐含的数据,如当前时间(它也应该通过参数提供)。

让我们看看两个使用时间作为输入并返回具有一定随机性结果的预测函数的实现。以下列表只有一个显式参数,而其输出取决于三个参数,这意味着输出是不可重复的。

列表 13.1 一个平庸的 predict 函数设计
def predict(features):
    time = datetime.now()
    return model.predict(features, time, seed=42)

与前面的例子不同,以下列表中显示的函数调用者控制着影响输出的所有参数。

列表 13.2 一个更好的 predict 函数设计
def predict(features, time=None, seed=42):
    if time is None:
        time = datetime.now()
    return model.predict(features, time, seed)

注释:虽然我们在这些例子中使用 Python,但高级抽象的基本原理保持不变。想象一下这个函数是 HTTP API 的一部分,其中 timeseed 是新引入的查询参数。在这种情况下,将应用相同的原理。

可预测性的一个特定方面是兼容性。当谈论兼容性时,工程师通常指的是向后兼容或向前兼容。向后兼容意味着 API 的新版本与旧版本兼容(旧代码可以在不进行任何更改的情况下与新版本的 API 一起使用)。向前兼容则意味着 API 的旧版本与新版本兼容(新代码可以在不进行任何更改的情况下与旧版本的 API 一起使用)。

在机器学习系统的背景下,兼容性也与底层模型的版本有关。通常有一个常见的做法是给模型打版本号,并在初始化时提供一个请求特定版本模型的方法。

列表 13.3 将模型版本添加到 API 中
class Model:
    def __init__(self, version):
        self.version = version
        self.model = load_model(version)

    def predict(self, features, time=None, seed=42):
        if time is None:
            time = datetime.now()
        return self.model.predict(features, time, seed)

这个例子过于简化,你可能需要一个更高级的解决方案来处理复杂的系统。如果你想了解更多关于模型注册模式的信息,可以阅读相关材料(例如,neptune.ai/blog/ml-model-registry)。

版本控制很棘手。一种反模式可能是更新模型而不提升版本,这会导致系统行为的变化而没有任何通知。有很多场景中,更新模型被视为破坏性变更,必须在版本中反映出来。更重要的是,这不仅适用于模型,还适用于管道的任何方面——数据输入/输出(IO)、预处理、后处理等。一些更改甚至不是有意为之:你可以更新依赖项并得到不同的结果,从而隐式地破坏兼容性。

阿列克谢的篝火故事

更新 Python 版本可能需要多长时间?这个问题看起来很简单,但事实上,我不得不艰难地学习到它并非如此。现实的重击如此之重,以至于让我陷入了一个兔子洞。

我工作的系统需要完全兼容。哎呀,当我提升 Python 版本(同时保持其他一切静态!)导致几个输出不匹配时,我感到非常惊讶。经过几次二分查找迭代后,我意识到问题与图像读取库有关。为 Python 3.5 构建的同一版本库与 Python 3.6 版本的库行为略有不同,因此,可以用一个版本库读取的文件与另一个版本库不兼容。

但这怎么可能发生呢?看起来这个库使用了一个用 C 实现的低级 JPEG 库;同时,Python 库的不同构建版本——即使版本相同——也使用了不同的底层 C 库版本,因为它们使用了在构建机器上全局安装的版本。在每种情况下找到使用的版本并不容易,需要一些硬核软件考古(挖掘开源库 6 年前的构建日志,从中找到线索,并最终重现相同的构建)。

再次强调,差异并不显著;然而,这足以破坏兼容性,因为模型是在使用库的一个版本读取的图像上训练的,并且它们对输入的敏感性太高。用户不太可能注意到差异,但这仍然是一个破坏性变更——本不应该发生的事情,因为这是由于用户协议。

虽然阿列克谢的经验展示了在单一系统中保持兼容性的挑战,但以下来自瓦列里伊的故事突出了当版本控制问题并非出现在系统内部,而是在我们与第三方解决方案互动的节点上时,可能会变成一个更大的难题。

瓦列里伊的篝火故事

有一次,我需要为一个金融机构实施 KYC(了解你的客户)解决方案。这个解决方案的一部分需要验证用户上传的 ID 文件,并确保用户的脸不在现有用户中。换句话说,这是一项关于用户唯一性的监管要求约束。作为一个熟悉构建或购买权衡的人(请参阅第三章),我使用了一个大型流行供应商的面部识别解决方案。

系统很简单:以文档作为输入,找到一张脸,计算一个向量,确保数据库中没有类似的向量,然后让用户注册;否则,让客户支持团队手动验证情况。系统一直运行良好,直到有一天不幸的是客户支持团队因误报而工作量过大。经过调查,发现供应商的 API 版本已被修复——除非不小心将其取消设置。结果,API 的新版本返回了隐式不兼容的结果,导致了这一事件。

这种失败由于其隐含性质而非常危险。外部 API 突然更改字段名称可能会导致中断,幸运的是,这种中断很容易被发现和修复。当模型版本更改时,你最初可能不会注意到——它之前返回的是浮点向量,现在返回的是类似的向量,中断可以通过正确配置的测试/监控设置或在下游任务(在这种情况下是客户验证)退化后检测到。

没有什么比在持续集成(CI)上运行的一套适当的测试更能帮助捕捉这类问题。

13.1.1 API 实践

几年来,行业已经发展出多种与 API 协同工作的实践。我们提到了一些我们认为效率较高的实践。它们可能并不一定专门针对机器学习系统,但它们通常与它们相关(见图 13.2):

  • 至少设计 API 的两层结构。 这里,我们谈论的是外部和内部层,其中前者在逻辑上是后者的子集,但并不一定遵循相同的协议。外部 API 暴露给用户或其他组件,而内部 API 被外部 API 使用。只要内部 API 不对用户暴露,就可以在不破坏兼容性的情况下进行更改。反过来,外部 API 是内部 API 的子集,应该考虑到兼容性进行设计。它有助于分离关注点,使外部 API 在兼容性方面更简单、更容易维护,同时使内部 API 更灵活、更容易更改。

  • 尽可能将 API 的机器学习和 IO 组件分离。 当机器学习服务是无状态的并且因此是幂等的时,它最容易维护。这并不总是可能的,但这是一个值得追求的良好实践。这种方法不仅对维护有用,而且对可扩展性也有用:IO 和 ML 组件可以独立扩展,考虑到它们有不同的要求(例如,ML 组件通常受 CPU 或 GPU 限制),这是一个很好的特性。此外,它简化了系统的演变:您可以在不触及 IO 组件的情况下部署 ML 组件的新版本,并在 A/B 测试或逐步推出期间同时使用两个 ML 组件一段时间。

  • 为您的 API 构建一个客户端库以简化使用。 不论是为外部用户还是您的团队成员,拥有一个客户端库可以降低入门门槛,从而简化调试过程并加快实验速度。它也是一个实现 API 直接不包含的实践的好地方,例如推荐的重试、超时等。

  • 考虑嵌入功能开关(也称为功能标志)或您组织使用的任何其他替代方案。机器学习系统通常在风险环境中运行,并且始终有一种方法可以禁用模型的新版本或切换到回退解决方案,以防出现问题。功能开关不是 API 的一部分,但它们有效地充当了绕过系统行为的手段,而无需重新部署或更改 API/客户端行为。

figure

图 13.2 分层 API 结构更易于维护、测试和开发。

13.2 发布周期

机器学习系统的发布周期通常与常规软件类似。然而,有两个主要区别:

  • 机器学习系统更难以测试。

  • 训练一个新的模型(即使是完全自动化的流程)通常比编译代码和构建其他工件需要更多的时间。

让我们详细阐述这些观点。

由于测试复杂性,仅仅运行测试并不总是足够的,而且仍然可能发生回归。让我们暂时谈谈软件。一旦常规软件更新,通常只需要运行测试来确保一切按预期工作。但是,如果我们谈论机器学习模型,情况就完全不同了,因为许多改进都伴随着代价。当机器学习模型更新时,即使我们有代表性的测试数据集和良好的软件相关部分的测试覆盖率,我们仍然不能保证这些更改不会引发有害的结果。例如,假设最终测试集中有 100 个样本,新模型在之前被标记为错误的 3 个样本上提高了性能,但在其他样本上引入了两个新的错误。整体性能有所提高,但这种变化是否足够好以至于可以发布?

现实生活中的场景充满了类似的例子,这意味着许多发布都需要人工介入。这类似于测试用户体验的改变,其中员工应该检查这些改变是否带来了足够的利益。评估改进和退步之间的权衡通常比仅仅检查 UI 动画是否按预期工作要复杂得多。人工介入的方法可能因系统而异。在某些情况下,ML 工程师负责;在其他情况下,可能是一位产品经理或外部领域专家,或者这项工作甚至可以委托给提供大量用户和聚合反馈的众包平台。

有一些更简单的情况,某种 AutoML 允许模型(不是整个系统——只是模型)自动发布,无需额外审查。想象一下,你正在构建一个具有强大功能的先进文本编辑器:一个模仿作者风格的自动完成功能(哦,我们希望我们在写这本书的时候就有这样一个功能!)。这个软件需要为每个用户运行一个自定义模型(可能是在大型基础模型之上),收集新用户的写作并定期更新模型,而不需要人工参与,这似乎是一个好的做法;否则,它将无法扩展。这样的场景需要更高的偏执测试水平,以覆盖尽可能多的悲观路径,并在出现故障的可能性时禁用模型更新。

好吧,让我们假设测试不是问题。假设你在预处理代码中发现了错误;你确信修复它是好的解决方案,不会使事情变得更糟,并且先进的测试工具集可以帮助你确保这真的是这样。对于常规软件来说,这意味着你可以直接修复错误,运行测试,并部署新版本;通常,这不会花费太多时间。但对于 ML 系统来说,情况并非如此,因为你需要重新训练模型。我们不会使用超大规模的例子,但根据我们的经验,训练了几周的模式并不少见,所以第二天发布修复是不可能的。

在实践中,这意味着 ML 系统的发布周期通常比常规软件要长;然而,这可能会变化很大——从每天多次到一年一次,中间有多次变化。在设计系统时,请务必考虑这一点。长的发布周期意味着系统应该更加可靠,并且需要进行广泛的测试,而短的发布周期则允许系统更加敏捷,并允许进行更多的实验。

多组件系统可以为不同的组件使用不同的发布周期。想象一个包含四个组件的简单搜索引擎:一个包含要搜索的文档的索引,一个轻量级的过滤器用于初步选择,一个重型模型用于最终排名,以及一个 API 层来公开搜索结果。文档可以随时添加到索引中(无需发布),索引代码库很少被修改,过滤器层和 API 层是纯非机器学习软件,更容易测试和构建,因此它们可以更频繁地发布。反过来,排名模型每两周训练和发布一次,并附加额外的验证。这使我们能够对非机器学习组件更加敏捷,对机器学习组件更加稳定。

存在一系列与发布相关的技术,包括蓝绿部署(见图 13.3)和金丝雀部署(见图 13.4)。它们可能略有不同,但它们背后的核心思想是同时让两个或更多系统在生产环境中运行。一旦部署了新版本,新用户就会被发送到新版本,而旧版本仍然在运行。在蓝绿部署中,更改是离散的(所有用户都切换到蓝色或绿色版本),而在金丝雀部署中,发布是细粒度的,新版本仅用于一小部分现有用户。这使我们能够在生产环境中测试新版本,并在出现问题时更容易回滚。这并不特定于机器学习系统,但也可以应用于它们。机器学习系统还可以使用的一种技术是分解(例如,某些组件如模型可以与金丝雀部署一起发布;相比之下,其他组件如 API 层可以以更传统的方式发布)。

figure

图 13.3 蓝绿部署

figure

图 13.4 金丝雀部署

金丝雀部署不应与我们在第十二章中讨论的 A/B 测试混淆。虽然在技术上它们可能看起来像类似的概念(系统的多个实例都是活跃的,并且流量以适当的分割发送到它们),但它们的意图是不同的。A/B 测试用于评估系统不同版本的性能,而金丝雀部署用于在完全切换之前测试系统的最新版本。在 A/B 测试中,我们希望比较不同版本的系统性能,测试时间基于统计显著性;保留选项 A 或选项 B 都是可行的。在金丝雀部署中,我们希望确保新版本足够好,可以供所有用户使用,并且我们希望尽可能快地完全切换到它。

虽然大型企业通常倾向于有更严格的政策和更长的发布周期,但这并不总是如此:通过技术和组织方法来缩短迭代之间的时间差距是一个崇高的目标。这也是 DevOps 文化的一个重要方面,机器学习系统也不例外。如果你对这个话题感兴趣,我们建议阅读《凤凰项目》这本书。这本书不是关于机器学习系统的,甚至不是一本非常技术性的书(更像是“商业寓言”),但它是一本关于 DevOps 文化和如何在现实世界中应用它的优秀读物。

创业公司和成熟的科技巨头通常更加敏捷,发布周期也更短,但这也会有所变化。阿尔谢尼曾为一家初创公司工作,周五晚上晚些时候部署是常见的做法(有时这会导致需要工程师解决故障,而这些工程师当时已经在享受一杯美酒)。在另一家更成熟的初创公司中,发布周期非常灵活;每个工程师都可以随时部署他们的组件,但在部署前会有一个简单的护栏警告,如果时间不合适(例如,午餐后周五)。

那些有幸安排发布的人应该意识到所有依赖关系:系统如何影响其他系统,以及其他系统如何影响它。最大的故障通常发生在基础设施层面,或者是在不属于同一团队的系统或组件之间。与其他团队进行适当的沟通以避免此类问题是任何高级工程师必备的技能。不幸的是,这种技能及其重要性的理解往往需要付出一定的代价(通常是在大故障之后)。

阿尔谢尼遇到的最大故障与一个日志记录器配置有关(在构建机器学习系统时,你通常不会太在意这类配置)。一些与机器学习相关的负载发生在线程中,当试图追踪行为时,他设计了一个复杂的日志记录器来在线程间保持请求的 ID。它在某个环境中运行良好,后来被部署到另一个环境中,那里的工程师对系统的控制力要小得多。当缺陷暴露出来时,这是一个黑暗的时刻:问题只能在有 1,000 次特定类型的请求之后发生,而这种请求在之前的环境中从未发生过。理解根本原因或回滚新版本花了一些时间,因此这次事件成为了一个很好的教训,即在发布过程中引入更多的检查。

13.3 操作系统

仅构建系统并将其直接与其他需要其输出的组件集成,从产品逻辑的角度来看,这永远是不够的。任何系统都需要额外的连接来保证健康运行,这些连接既与技术相关,也与非技术相关。有些是为了使维护和运营更加顺畅(从工程和产品两个角度来看);有些是由于与系统相关的隐含的非功能性需求引起的(例如法律或隐私问题)。让我们列举一些。

13.3.1 技术相关连接

CI 通常是整个基础设施中首先设置的元素。它有助于识别和解决集成问题,同时促进更顺畅和更快的开发过程。CI 的典型任务包括运行测试(单元或集成)和构建将用于下游(例如,用于进一步部署)的工件。然而,CI 级别可能还有其他需求,例如安全测试、性能测试、成本分析(“这个发布是否需要我们启动更多的云服务器?”)、部署到测试环境、代码风格检查、报告关键指标等等。

需要存储但不是系统数据一部分的两个主要事物是日志和指标。通常,公司中日志和指标存储、聚合和监控的方法是通用的,你只需要遵循常规方式。我们将在下一章中对此话题进行更详细的阐述。

系统的性能可能会出现故障,这并不令人惊讶。因此,系统应该连接到公司使用的警报和事件管理平台,这样值班人员就会意识到潜在的故障并做出适当的反应。

但他们具体是如何反应的呢?在这里,你可能需要准备特定的食谱,描述预期的故障模式以及如何处理它们。此外,可能还有一套额外的工具来帮助灭火,例如配置管理面板、系统特定的仪表板等等,这些内容我们将在第十六章中进行介绍。再次强调,通常 ML 和非 ML 系统之间有一个公司标准,所以你很可能只需要适应现有的软件工具集,而不需要重新发明轮子。

设计一个系统需要考虑系统的整个生命周期,而不仅仅是顺利的路径,并且保持一点偏执是有帮助的。

13.3.2 非技术相关连接

除了操作的技术方面之外,还有一些非技术方面应该考虑。它们通常与客户成功或合规性相关,并且并不总是显而易见。如果用户希望所有个人数据都按照通用数据保护条例(回想第十一章,想象他们的数据可以传播得多深)被清除,我们应该怎么办?是否有法规强制模型可解释,以及在不牺牲模型性能的情况下遵循它的最佳方式是什么?如果高级管理人员或初创投资者在系统中遇到错误并感到愤怒怎么办?我们如何在难以重现的场景中调试系统的行为(例如,缺陷只能通过上述高级管理人员的用户账户重现)?在系统发布之前,所有这些问题都应该得到解答,并且这些答案至少应该在设计阶段简要反映出来。否则,后续的更改可能过于昂贵而难以实施。通常需要构建额外的组件,例如一些用户模拟机制或用于数据管理和模型可解释性的管理面板,这可能需要大量的项目时间和需要其他团队(例如法律或合规性团队了解法规或网络开发团队构建所需的仪表板)。

根据我们的经验,所有这些额外的连接和考虑通常比核心系统本身花费更多的时间,公司规模越大,所需努力就越多。鉴于当前趋势,在不久的将来不太可能有所改善,因为全球范围内正在应用更多与机器学习和隐私相关的法规。

13.4 覆盖和后备方案

您的系统可能有一个合法的失败原因。这样的原因的一个例子可能是外部依赖:您从第三方 API 中提取了一块数据,而在某个时刻,它就不再可用了。这就是您可能想要有一个后备解决方案的情况之一。

后备方案是指在主计划或解决方案失败或不可用时可以使用的备用计划或替代解决方案。我们使用它来确保即使在主机器学习模型因任何原因失败的情况下,系统仍然可以运行并做出决策。

这在用于关键任务或即使最短的中断也可能导致重大后果的行业中尤为重要。例如,对于用于预测制造环境中设备故障的模型,后备方案可能是至关重要的,确保即使在主模型遇到问题时,生产也能继续进行。

使用后备方案的另一个原因是,当主系统无法提供令人满意的答案或自信的预测,或者当主模型的输出超出可接受范围时,提供一个替代解决方案。

实现后备方案有相当多的不同方法。一个常见的方法是使用一个辅助的机器学习模型,这个模型可以在不同的数据集或使用不同的算法上训练。它可能是一个更简单的基线解决方案,如我们在第八章中回顾的,或者是一个双模型设置。在这个设置中,第一个模型仅使用稳定特征构建,而第二个模型使用更大的特征集来纠正主模型的输出。这些模型可以一起使用,根据输入数据或预定的规则选择一个模型的输出。或者,可以为“核心”模型设置输入特征漂移监控(见第十四章)以检测关键的变化。

另一个选项是使用基于规则的系统作为后备方案,当模型不可用或表现不佳时,它可以提供稳定且可预测的响应。也有可能结合使用这些方法,例如使用基于规则的系统处理简单情况,而使用机器学习模型处理更复杂的情况(然而,这本身也引入了额外的复杂性和断点)。

与基线一样,一个简单的常数也可以作为我们的后备方案。最后,有时后备解决方案是回复一个明确的错误信息。

后备解决方案应该始终有一个计划来激活后备方案,并在机器学习模型和后备系统之间切换。它可以是自动的(由监控事件触发)、手动的或混合的,具体取决于用例。

一种自定义的后备类型是覆盖。这是一种在模型发出不良预测信号时手动覆盖模型输出的方式。一个例子可能是在模型的预测超出可接受范围或模型置信度太低时,放弃模型的输出并使用一个常数代替。使用覆盖的另一个原因与发布周期有关。例如,客户抱怨模型在非常特定的场景中失败。理想情况下,我们需要确保这个场景在训练数据中得到体现,重新训练模型,运行所有检查,然后部署它。但是,正如我们之前讨论的,这可能需要一段时间。因此,我们可以使用基于规则的策略覆盖模型在这个特定场景的输出,让客户满意,并在下一个版本中正确处理它。

覆盖(override)的缺点是它们不透明且容易被遗忘。因此,有一个跟踪它们的方法以及一个如何妥善处理它们的计划是很重要的;否则,它们可能会变成技术债务。拥有许多覆盖的好处是,覆盖集合可以通过多源弱监督来改进模型——这是一种在未标记数据上使用“标记函数”进行标记的技术(这些启发式方法并不完美,但易于实现)。标记函数提供了一个噪声数据集,这成为模型训练的基础。关于这项技术的更多细节可以在亚历山大·拉特纳等人撰写的论文中找到,“数据编程:快速创建大型训练集”(arxiv.org/abs/1605.07723)和“使用多任务弱监督训练复杂模型”(arxiv.org/abs/1810.02840)。多源弱监督的概念因名为 Snorkel 的流行库而受到行业认可(www.snorkel.org/)。

阿尔谢尼的同事曾经实施了一个更优雅的解决方案,帮助解决了长期发布周期的限制。他处理了一个命名实体识别问题,有时模型无法识别对客户重要的某些实体。然而,由于训练时间过长,在发现问题后重新训练新模型不是一个选择。因此,他基于知识库实现了一个解决方案:在运行模型之前,将输入文本与知识库进行核对,如果找到了可能的实体,则将其用作模型的提示。这允许团队在不重新训练的情况下解决问题。非技术人员可以在一分钟内将样本添加到知识库中,因此可以迅速解决问题。该解决方案在博客文章中有更详细的描述(mng.bz/BgG1)。这个案例与覆盖有些相似,但它增加了模型的输入,而不是输出。

13.5 设计文档:集成

超级大零售和 PhotoStock Inc.的集成方法旨在创建用户友好、快速和高效的机制,用于库存预测或搜索结果。

13.5.1 超级大零售的集成

超级大零售的集成策略旨在提供无缝、动态且高度响应的预测系统,以帮助管理库存。

设计文档:超级大零售

X. 集成

i. 回退策略

在面对不可预见的情况时,回退(fallback)对于保持运营效率至关重要。超级大零售采用了多级回退系统:

  • 主要回退**——主要模型在最重要的特征子集上训练。如果没有在这个子集中检测到特征漂移/问题,它将被使用。

  • 二级回退**—我们的下一层回退涉及 SARIMA 或 Prophet 等时间序列模型,我们在第 4.4 节中探讨了这些模型。这些模型对外部特征的依赖性较低,如果发生漂移,可以提供更稳健的预测。

  • 三级回退**—作为最后的手段,我们会预测与上周数据类似的销售额,并对预期事件和假日进行修改。

系统会监控数据漂移和质量问题,触发警报,自动切换到适当的回退,以确保尽可能准确的预测。

ii. API 设计

  • HTTP API 处理器**—此组件将管理请求和响应,以结构化的 JSON 格式与用户进行交互。

  • 模型 API**—这将直接从模型中提取预测。

请求格式是

GET /predictions?query=<query_string>&parameters=<parameters>&version=
↪<version>
&limit=<limit>&request_id=<request_id>&sku=<sku>&entity_id=<entity_id>
&group=<group_type>

响应格式是

{
    "predictions": [
        {
            "sku": <sku_id>,
            "demand": <demand>,
            "entity": <entity_id>,
            "period": <time_period_for_demand>,
        },
        ...
    ]
}

iii. 发布周期

A. 包装器发布与模型发布

在我们的集成策略中,包装器的发布和模型的发布代表两个不同的过程。以下是对每个过程的细微差别。

对于包装器(基础设施)的发布,我们应该考虑以下因素:

  • 频率和时间表. 释放通常比模型的频率低。由于需求模式可能会在夜间发生变化,因此能够通过训练将这些模式纳入模型中非常重要。

  • 依赖项. 基础设施发布主要依赖于软件更新、第三方服务或系统要求。这些领域的任何变化都可能需要新的发布。

  • 测试. 综合集成测试是必须的,以确保所有组件协同工作。同时,确保向后兼容性也非常关键,以避免现有服务中断。

  • 推出. 这通常采用标准的软件部署策略。根据变更的性质,蓝色-绿色部署可能并不总是必要的,特别是如果变更不是面向用户的,并且不影响批量作业。

  • 监控. 重点将放在系统健康、正常运行时间、响应时间和任何错误率上。

对于模型的发布,我们应该考虑以下因素:

  • 频率和时间表**—模型发布更频繁,并与新数据的可用性、数据模式的变化或建模技术的重大改进相关联。

  • 依赖项**—这些主要依赖于新训练数据的质量和数量。数据模式的变化或新数据源的引入可能会触发模型的更新。

  • 测试**—在推出之前,模型会经过严格的离线验证。一旦验证通过,它可能会在影子模式下进行测试,其中其预测与当前模型并行运行,但不会被使用。这有助于在没有任何风险的情况下,比较和验证新模型在真实世界场景中的性能。

  • 发布**—在引入新模型时,不仅仅是部署模型文件。还需要确保任何预处理步骤、特征工程和其他管道与模型期望的一致。

  • 监控. 主要关注模型性能指标。同时,关注数据漂移也是必要的。详见第十四章。

B. 包装器和模型发布之间的相互作用

在基础设施更新会影响模型(例如,数据管道的变化)的情况下,两个发布之间的协调变得至关重要。此外,模型架构的任何重大变化可能需要更新包装器以适应这些变化。通过将它们视为独立的过程,同时确保它们协调一致,我们保持系统的稳定性,并持续提高其能力。

iv. 运营问题

反馈对于持续改进至关重要。应向内部用户提供一个包含覆盖功能的反馈机制。这不仅有助于改进预测,还让业务用户根据实时洞察获得控制和适应性。

v. 非工程考虑因素

集成策略还将考虑非工程因素——例如

  • 管理面板—对于管理和获取系统高级概述至关重要

  • 与公司级仪表板的集成—为了实现公司范围内的可见性和决策

  • 附加报告—对于深入了解和分析至关重要

  • 覆盖—考虑到不可预见或独特情况下的手动调整的必要功能

此外,公司使用的标准 CI 工具以及典型的调度器将被集成,以保持一致性和优化工作流程。

vi. 部署

由于我们的受众主要是内部客户和频繁的批量作业,因此目前没有绿色-蓝色或金丝雀部署的迫切需要。由于没有终端用户流量,这种分阶段部署的需求不存在,简化了我们的发布策略。

13.5.2 PhotoStock Inc.的集成

PhotoStock Inc.的集成策略专注于提供最相关的搜索结果,无论搜索查询的复杂性如何,同时保持快速响应。

设计文档:PhotoStock Inc.

X. 集成

i. API 设计

我们的搜索引擎需要暴露一个 HTTP API 处理器,它接受一个查询字符串+可选的附加过滤器(例如,价格、收藏、分辨率、作者等)并返回与查询匹配的图片 ID 列表,按相关性排序。除了这些以产品为中心的参数外,我们还需要传递更多技术参数,如versionlimitrequest_id

处理器仅用于内部,不会公开暴露,因此由于它在一个私有网络中运行,我们不需要担心身份验证和授权。

我们无法使服务完全无状态(我们需要拥有索引和覆盖),但所有与查询相关的元数据都应该由后端服务处理,因为它们已经存储了其他用户的元数据。

在底层,我们将使用简单的级联来缩小搜索结果。我们首先根据可选的过滤器进行过滤,然后从嵌入空间中获取查询字符串的最近邻,最后根据相关性对结果进行排序。

我们考虑使用 Qdrant (qdrant.tech/)作为一个能够大规模过滤和获取候选者的快速向量数据库;然而,公司之前尚未使用过它,因此在使用生产环境之前,我们可能需要对其进行适当的测试。或者,如果需要,我们还可以考虑使用其他向量数据库。

请求格式是

GET /search?query=<query_string>&filters=&version=

&limit=&request_id=<request_id>

响应格式是

{

"results": [

    {

        "id": <photo_id>,

        "score": <score>

    },

    ...

]

}

请求和响应都使用 JSON 格式,这是我们内部 API 中使用的默认格式。请求和响应的结构目前简单直接,但在需要时可以扩展。

底层 API 应该按照以下方式分层:

  • HTTP API 处理器仅作为底层 API 的代理,不包含任何业务逻辑;它只是解析请求,将其传递给底层 API,将响应封装成 JSON 格式,并处理错误。

  • 向量数据库 API 负责根据查询字符串的嵌入进行过滤和获取候选者。

  • 模型 API 负责从字符串中提取嵌入并评分候选者。

  • 排名 API 负责根据相关性对候选者进行排序并应用可能的覆盖。

ii. 发布周期

我们假设模型更新将相对较少,因为训练需要花费大量时间,我们也不期望数据随时间发生显著变化。我们可以预期每 1 到 2 个月发布一个新的模型和相关 API,而大多数热更新将仅与索引和覆盖有关。

索引是搜索引擎的核心,它将需要定期更新(针对数据,而不是软件)。我们可以每天添加新项目(例如,通过每晚运行的批处理作业),并在需要时准备更新索引(例如,删除被禁止的图像或根据 VIP 用户的特殊请求添加新图像)。

iii. 运营关注点

许多内部用户可以对搜索结果提供大量反馈,因此我们需要为他们提供适当的工具。一开始,我们可以在内部 PhotoStock Inc.用户的搜索结果页面上添加一个“报告不良匹配”按钮。这将向数据网关发送请求,因此我们将照片 ID、搜索引擎结果页位置和查询字符串保存到数据湖中。然后我们可以在模型重新训练阶段、错误分析和手动覆盖期间使用这些数据。未来,我们可以考虑为一些外部认证用户提供类似的功能(例如,我们信任的顶级买家)。

iv. 覆盖和回退

作为回退,我们将使用现有的基于 Elasticsearch 的搜索引擎。虽然它在相关性方面可能不如新设定的那么好,但它仍然是一个不错的搜索引擎。

关于覆盖,我们可能对某些查询有手动覆盖,这些可以存储在一个单独的数据库中。这种情况可能发生在对流行/关键查询的相关性较差时,我们无法及时用模型修复。目前,它可能是一个简单的键值存储,其中键是查询字符串的正则表达式,值是照片 ID 的列表,我们将使用这些 ID 在搜索引擎结果页面上。这种解决方案的可扩展性不高,但对于第一个版本来说足够稳固。我们可能希望在将来为管理这些覆盖提供一个简单的用户界面。

与“不良照片”相关的另一种可能的覆盖类型,我们希望从搜索结果中隐藏(例如,通过审查的裸露/暴力照片)。然而,如果我们突然意识到某个图像不再适合我们的搜索结果,我们可以简单地从索引中删除它。

摘要

  • 记住,集成不是一个一次性的事件或项目的某个阶段,而是一个从项目开始到系统退役的持续过程。

  • 在为您的系统选择 API 时,您应该寻找的两个主要品质是简洁性和可预测性。

  • 我们认为对于机器学习系统而言,有效的 API 实践包括至少设计两层 API,在可能的情况下将 API 的机器学习和 IO 组件分离,为 API 构建客户端库,以及嵌入功能开关或其替代方案。

  • 有一个回退方案或替代方案,可以在主要计划或解决方案失败或不可用时使用。

第十四章:14 监控和可靠性

本章涵盖

  • 监控作为机器学习系统设计的一部分

  • 软件系统健康

  • 数据质量和完整性

  • 模型质量和相关性

传统软件开发基于一个简单的原则:一个高质量构建的产品将具有高稳定性、效率和可预测性,这些价值不会随时间改变。相比之下,机器学习(ML)的世界更为复杂,对机器学习系统的工作并不随着其发布而结束。这有一个实际的解释;在前一种情况下,解决方案严格在预设计的算法内执行,而在后一种情况下,功能基于在有限数量的某些输入数据上训练的概率模型。

这意味着模型不可避免地会随着时间的推移而退化,以及经历意外行为的情况,这是由于它在训练时使用的数据与它在实际条件下将接收到的数据之间的差异。

这些是无法消除的风险,但你需要为它们做好准备,并能够减轻这些风险,以确保你的系统在长期内对你的业务保持有效和有价值。

在本章中,我们将探讨监控作为机器学习系统设计的一部分的本质,以及你的机器学习模型在运行过程中可能遇到的问题来源。我们还将探讨你希望如何监控系统行为变化的典型情况,以及你需要如何对这些情况做出反应。

14.1 监控的重要性

在生产中拥有一个运行的机器学习解决方案是一种极好的感觉。拥有适当的验证方案、测试覆盖范围以及工作着的持续集成和持续交付 CI/CD 则是一种更好(尽管更罕见)的感觉。不幸的是,这并不能保证你的系统在没有出现混乱的情况下保持稳定。在这里所说的混乱是指模型可能产生的任何意外、不合逻辑、完全错误且无法解释的输出,这些输出可能会严重影响系统输出——任何你不会期望从一个健康、可靠系统中看到的东西(至少在没有通知维护者并切换到回退之前)。

没有适当的监控,即使是最受训练和最准确的模型也可能随着时间的推移因所处理数据的变化而开始退化。这在重大变化时期尤为明显,例如最近的 COVID-19 大流行期间,用于预测商品可用性、信用风险等任务的模型必须面对巨大的挑战。通过实施监控系统,我们可以识别并解决数据质量问题,并确保我们的模型继续做出可靠的预测。

如果有人要求我们对一个通用的机器学习系统进行简化的高级审查,它很可能看起来像图 14.1。

figure

图 14.1 正常机器学习系统的简化结构

只有当所有阶段都固定,确切相同的样本数据通过精确相同的模型(相同的结构和相同的权重),并且该模型是非概率性的(世界上一些最受欢迎的模型,例如生成式 AI 冠军 ChatGPT 和 Midjourney,是概率性的;然而,为了完全严谨,我们可以固定一个随机生成器或种子来提高可重复性),并且具有精确相同的后处理时,我们才能期望得到确定性的(例如,可再现的)输出。实际上,这正是设计适当训练管道的主要原因之一,它有助于创建和维护可重复迭代的条件,从而为改进和实验提供支持。

然而,这种因素组合发生的概率极低,因为我们总是处理着众多可以以不同概率和影响程度改变模型输入。让我们从图 14.1 中的所有四个组成部分开始分析,了解我们可能会遇到哪些挑战。

14.1.1 输入数据

不幸的是,我们拥有的系统越复杂,数据输入可重复的可能性就越小(如果是这种情况,我们可以使用简单的缓存),这意味着我们可以假设每个输入都是唯一的。此外,数据中特定特征的分布可能会随时间变化,影响下游阶段。另一种可能性是数据管道中可能会出现故障并损坏数据,或者有人可能会以影响输出的方式处理输入,这种方式可能会产生预期的效果,有时甚至是有害的(然而,这通常与植入后门有关;参见“在机器学习模型中植入不可检测的后门”概述(arxiv.org/pdf/2204.06974.pdf)。如果系统对这些扰动不稳健,事情可能在任何时刻都会适得其反。

14.1.2 模型

与输入数据不同,模型的架构(或结构)和权重可以保持稳定。当然,这取决于在线训练的存在(例如,以实时/准实时的方式根据输入数据更新模型)和计划更新(例如,每周在最新批次的数据上重新训练)。无论更新是批量学习还是在线学习,更新后的模型与之前并不完全相同。如果模型不同,相同数据的输出可能会改变,这正是我们重新训练模型的基本原因。尽管如此,我们仍然想要确保重新训练/更新对模型产生了充分的影响,并且是必须做的事情(回想第 13.2 节)。

另一件事是我们拥有的系统可能本质上是概率性的/生成性的。图 14.2 中显示的与 ChatGPT(截至 2024 年 7 月)的对话是一个相当有说明性的例子。

figure

图 14.2 通过让 ChatGPT 感到烦恼来确保它是一个相当非确定性的模型。尽管回复相似,但它们并不完全相同。

截图显示了 ChatGPT 的四个不同(尽管相似)的回复。虽然我们期望它在特定范围内表现,但没有相同的输出对于相同的输入使得检查系统的健康状况更具挑战性。哪个回复是可以接受的,哪个是不可以接受的?截至 2023 年 1 月,我们与 ChatGPT 的经验表明,连续收到几个完全不同或相互矛盾的答案并不罕见,尽管这在后续版本中得到了部分修复。

14.1.3 模型输出

这可能是一件相对罕见的事情,但模型的输出可能会因为所谓的概念漂移而变得不相关——例如,输入数据和输出结果之间潜在依赖关系的改变,使得模型建立的依赖关系变得过时和不足。这与输入数据分布的变化不同,因为虽然进入的数据分布保持不变,但之前正确分配给它们的标签现在却不同了。例如,根据 2022 年英国最近的一次税收变化,最高的税率现在适用于年收入 125,000 英镑的人,而之前的门槛是 150,000 英镑。现在想象一下,你有一个推广税务服务协助的市场营销活动,并有一个模型被设定为挑选最可能使用该服务的用户以保持在市场营销预算内。这种法律的变化会立即触发对你模型的许多改进。

另一个例子是时间滞后,导致对世界的理解变化发生在引发这些变化的事件之后。一个很好的例子是麦道夫投资丑闻,它是在最初的恶作剧几十年后才被揭露的。多年来,来自它的信号被审查为成功的对冲基金信号,但后来发现它是一个庞氏骗局。换句话说,多年来,进入的数据被标记为善意,最终却证明是欺诈的。

14.1.4 后处理/决策制定

重要的是要意识到机器学习模型的质量与其预期带来的商业价值之间可能存在不匹配。有几个因素可能导致这种不匹配,包括模型使用环境中发生的变化,以及模型使用方式的变化。

如果模型被设计来支持特定的业务流程,那么随着时间的推移,如果流程发生变化,它可能就不再有效。例如,假设该模型被设置为根据用户的未来收入来优先处理客户支持的新到票务,而新的流程是专注于解决最快的票务。同样,如果模型没有被按照预期的方式使用,或者根本没有被使用,它可能无法实现预期的商业价值。为了减轻这些风险,持续监控机器学习模型的性能和它所提供的商业价值,并在必要时进行调整,以确保模型能够支持预期的商业目标是非常重要的。

在接下来的章节中,我们将讨论可能出现的最常见问题,主要关注前三个阶段。我们将讨论如何监控和检测这些问题,如果发生了变化应该做什么,以及如何将这些内容纳入设计文档,以便您能够在事情发生之前进行规划,而不是在事情发生后。在我们深入探讨潜在的故障和应对方法之前,让我们先讨论监控机器学习系统的主要组件。在 Elena Samuylova 的文章“在生产中监控机器学习系统。你应该跟踪哪些指标?”(mng.bz/86B2)中,她强调了机器学习系统监控的四个基本组成部分,如图 14.3 所示。

figure

图 14.3 机器学习系统监控的核心组件(来源:mng.bz/86B2)

正确监控的基础是软件后端,随后是数据质量和完整性、模型质量和相关性,以及业务关键绩效指标(KPIs)。乍一看,最后一个点可能看似无关紧要,因为它本身不是机器学习系统的一部分,但一个被破坏的系统可能对我们的 KPIs 以及最终对业务本身的影响不容小觑。让我们从金字塔的底层开始,逆向分析每一层。

14.2 软件系统健康

将软件后端视为设计的基础至关重要。这包括监控软件性能,确保其正常运行、高效执行任务,并快速响应请求。未能优先考虑软件后端的稳定性和性能可能会对机器学习系统的整体有效性产生不利影响。由于本书涵盖了机器学习系统设计的原理,而市面上有许多关于“常规”软件系统设计和其可靠性的书籍,我们不会对此进行深入探讨。然而,这一点自然需要被考虑,因为任何系统都是其最薄弱环节的强度。对于系统健康的更深入分析,请参阅 Chris Jones 等人的文章(mng.bz/EOAl)。

监控机器学习系统的健康状况是生产部署的重要方面。确保系统正常运行并跟踪其性能特征以满足服务级别目标是至关重要的。许多在传统软件系统中使用的监控实践——例如应用和基础设施监控、警报和事件管理——也可以应用于机器学习系统。这些实践可以帮助确保机器学习系统的健康和性能,并在出现任何问题时进行及时干预。通过使用为传统软件系统开发的工具和实践,可以在生产中有效地监控和管理机器学习系统的健康和性能。

根据机器学习系统的部署架构,可以监控各种指标。这些可以包括服务使用指标,例如模型调用总数、每秒请求数和错误率,以及系统性能指标,例如正常运行时间、延迟、冷启动时间、错误率和资源利用率指标,如内存和 GPU/CPU 利用率。仔细选择几个关键指标来量化服务性能的不同方面,通常被称为服务级别指标,这是非常重要的。这些指标可以帮助识别系统问题,并允许及时干预以防止故障或性能下降。

记录事件和预测及其时间戳以监控和调试您的机器学习系统非常重要。在机器学习系统的背景下,如果可行且符合预算(在某些情况下,存储每个中间模型输出可能会严重影响利润率),我们建议记录每个预测的数据,包括输入特征和模型输出。这将使您能够跟踪模型的性能并识别可能出现的任何问题。作为一般规则,除非有特定情况,例如隐私法规或用户设备上边缘模型的使用,阻止您这样做,否则您应该记录每个预测。在这些情况下,可能需要开发解决方案来收集和标记一些数据。

除了预测日志之外,还非常重要地拥有软件系统日志——提供有关您的机器学习应用程序内部发生情况的时间戳事件。访问这些日志对于调试可能出现的问题以及提高模型性能非常有帮助。有多种工具可以帮助您集中和分析这些日志,例如开源工具中的 Prometheus 和 Grafana,或云服务中的 AWS Cloudwatch 或 Datadog。

注意:您可以随时删除后来认为不必要的事件日志,但无法事后添加它们。如果没有提供新的上下文,则不要记录;换句话说,确保日志包含适当的值和 ID,而不仅仅是静态语句。

以下展示了糟糕的日志消息和良好的日志消息的例子:

❌ logger.info("Fraud probability calculated")
✅ logger.info(f"Fraud probability = {score:.3f} for user id {user_id}")

领英的模型健康保障平台(mng.bz/dZEo)可以提供一些关于这个主题的想法。谷歌的服务级别目标(mng.bz/r1eJ)可以提供确保系统健康所需的必要词汇。

日志和指标应存储在系统中,以便进行探索和警报。为此有多种软件解决方案。日志方面,ELK 堆栈(Elasticsearch、Logstash 和 Kibana)是自托管系统的良好例子,而 Prometheus 或 Victoria Metrics 用于指标。类似系统通常由一站式云提供商(例如 AWS Cloudwatch)和更专业的公司(如 Datadog)提供。正如我们在第十三章中提到的,将您的机器学习系统连接到适当的日志/指标工具集是必须的,尽管经验不足的工程师往往低估了这一步骤的重要性。

14.3 数据质量和完整性

数据质量监控对于确保用于训练和预测的机器学习模型的数据准确性和可靠性至关重要。使用有缺陷的数据将不可避免地导致模型产生错误的预测。为了保持对数据的信任,可能需要停止并使用备用方案,直到数据质量恢复,或者调查并解决出现的问题。接下来,我们将讨论可能需要备用方案的最常见情况。

14.3.1 处理问题

系统通常依赖于各种上游系统来提供输入数据。然而,这些数据源可能会引发问题,影响您的机器学习系统的性能。例如,数据可能无法接收或可能已损坏,这可能是数据管道中存在问题造成的。想象一下,一个为客户端个性化促销活动的机器学习系统可能依赖于来自内部客户数据库、点击流日志和呼叫中心日志的数据,这些数据被合并并存储在数据仓库中。如果这些数据源中的任何一个出现问题时,它可能会影响整个系统的运行。

我们最喜欢的例子之一是,当一项工作处理零行并报告成功(UI 报告中预期的绿色方块表示提取、传输和加载作业的成功执行)时,每个人都感到高兴——直到一周后我们发现上游数据源存在问题。

此外,机器学习管道的数据处理阶段也可能容易出问题。这可以包括数据源问题、数据访问问题、用于提取数据的 SQL 或其他查询中的错误、更改数据格式的基础设施更新以及用于计算特征的代码中的问题。在批量推理系统中,通过重新运行模型可以检测和纠正这些问题。然而,在高负载流模型中,如电子商务或银行中使用的模型,由于处理的数据量很大,并且基于模型输出做出实时决策,数据处理问题的后果可能更为严重。

14.3.2 数据源损坏

除了变化之外,数据还可能因为数据源的问题而丢失。这可能是由于错误、物理故障或外部 API 的问题。密切监控数据管道对于早期发现问题至关重要,因为这些问题可能导致未来重新训练数据的不可逆损失。

有时,这些中断可能只会影响数据的一个子集,使问题更难检测。此外,一个损坏的源仍然可能提供数据,但这些数据可能是错误的或误导性的。在这些情况下,跟踪异常的数字和模式以识别潜在问题至关重要。这些故障可以是渐进的,也可以是瞬间的。让我们想象两个用于工业需求的计算机视觉系统:一个用于无菌装配线,在那里拍摄并分析半组装设备的照片,另一个用于巨大的钢铁厂,它监控金属废料如何变成新的合金。第一个系统可能会突然出现灯光故障,因此数据流突然变暗。第二个系统受灰尘和温度的影响;因此,随着时间的推移,相机镜头会退化。

如果检测到数据源问题,重要的是评估损害并采取适当的行动,例如在必要时更新、替换或暂停模型。

一些数据源问题,遗憾的是,是不可避免的。想象一下为一家市场构建推荐系统,其中一些商家填写了他们商品的每个属性,而其他人则忽略了可选字段。在这种情况下,一个明智的选择是在特征工程阶段提前考虑这个问题,使模型忽略这些情况。

让我们来看一个更具体的例子:

def get_average_rating(item):
    # return average rating across all item reviews, 1 - 5 stars
    try:
        # happy case, read from DB and return the number
        ...
    except Exception:
        return -1

如果–1没有被下游替换,这种方法并不是处理可能缺失或损坏数据的最佳方式。虽然返回一些不可能的值在底层编程中是一种常见做法,但对于机器学习来说,返回其他项目的中值等类似值可能更合适。

14.3.3 级联/上游模型

在更复杂的机器学习系统中,可能会有几个相互依赖的模型,其中一个模型的输出作为另一个模型的输入。这可能会形成一个相互连接的模型循环,这可能会使系统容易受到问题的攻击(参见图 14.4)。

图

图 14.4 输入数据通过一系列模型进行处理;即使第一个模型出现微小的漂移,也可能导致下游累积误差。

阿尔谢尼在一家使用一系列模型的公司的任职;首先,命名实体识别模型提取核心实体;随后它们通过多个内部和外部数据集进行丰富,最终结果用于分类。系统的各个组件由不同的人开发和维护,有时可能导致命名实体识别模型在其自身指标上表现出改进,而下游分类器的性能却下降。幸运的是,在第一次捕捉到问题后,工程师实施了跨栈检查,因此不再可能在上游模型没有进行适当的检查的情况下部署新版本,从而提前捕捉到潜在故障。

例如,在一个内容或产品推荐引擎中,一个模型可能预测产品或商品的流行度。相比之下,另一个模型根据估计的流行度向用户推荐。如果流行度预测模型不正确,这可能导致第二个模型提供的推荐不正确。

类似的问题也可能出现在汽车路线导航系统中,其中模型预测各种路线的预期到达时间,另一个模型对选项进行排序并建议最佳路线。如果预测预期到达时间的模型存在问题,这可能导致错误的路线推荐,进而影响交通模式。

这种类型的互联系统如果其中一个模型出现问题,可能会面临问题,导致整个系统出现连锁反应的负面行动。精心设计和监控这些类型的系统对于确保它们正常运作至关重要。

14.3.4 架构变更

数据架构的更改,其中数据的格式、类型或结构发生改变,可能对机器学习系统构成重大挑战。这些更改可能导致模型失去信号,因为它可能无法将新类别与旧类别匹配或处理新特征。这在依赖于基于类别类型的复杂特征的系统中尤其成问题,因为类别的更改可能要求模型重新学习如何解释数据。

例如,在需求预测或电子商务推荐系统中,产品目录的更改可能影响模型对数据的理解。同样,业务系统的更新或新数据源或 API 的引入也可能导致问题,如果模型没有在新数据上训练过的话。(更多信息请见第六章。)

为了减轻数据模式变化的影响,设计模型时必须考虑到这种可能性,并教育业务用户了解这些类型变化可能带来的潜在后果。数据质量监控还可以帮助在问题出现时识别和解决这些问题。

14.3.5 训练-服务偏差

训练-服务偏差是指一个机器学习模型在真实世界数据上表现不佳的情况,因为它是在一个人工构建或清理的数据集上训练的,而这个数据集并不能准确代表它将要应用的数据。这可能会发生在训练数据不完整或不足以充分捕捉现实世界的多样性和复杂性时(详见第六章的详细信息)。

图

图 14.5 这种在沙盒图像集和真实照片之间的巨大差异可能导致模型性能显著下降。

训练-服务偏差的一个例子是,在一个有限的众包图像集上训练的模型,当应用于具有广泛数据格式和图像质量的真实世界图像时表现不佳。同样,在一个实验室环境中使用高质量图像训练的模型可能难以在光线条件较差的真实世界图像上表现良好(见图 14.5)。

营火故事

这个故事是由我们的一位朋友讲述的。有一家公司正在开发一个基于相机数据的计算机视觉增强现实应用程序;公司的办公室位于波罗的海边的俄罗斯城市圣彼得堡。圣彼得堡以其多云天气而闻名——平均每年阳光小时数约为 1,600 小时。工程师们在那里收集了大部分数据并测试了他们的产品。

公司的主要投资者位于地中海国家塞浦路斯,那里每年有 3,400 小时的阳光。因此,当他们在测试产品的预发布版本时,他们感到非常害怕——机器学习组件工作得如此糟糕!经过短暂的调查,工程师们意识到问题是由数据分布不匹配造成的:他们使用的户外场景是在俄罗斯冬天的多云和雨天环境中拍摄的,而投资者在塞浦路斯的夏日烈日下测试了产品。

为了解决训练-服务偏差,可能需要继续开发模型,通过收集和标记新的数据集或根据不成功的试验运行中的数据调整现有模型。有时试验运行可能会产生足够的数据来训练新的模型或调整现有模型。Will Douglas Heaven 在《Technology Review》上发表了一篇关于谷歌医疗人工智能的精彩文章(mng.bz/V2ay)。请注意,这与我们将在下一节中回顾的数据漂移场景有些相似。

14.3.6 如何监控和反应

预见危险,一半避免,因此监控的第一步和最重要的步骤是事先知道可能出错的情况。除此之外,我们还试图提供一些可操作的建议。

数据质量监控在机器学习中是特定的,因为它涉及确保我们用于训练和用机器学习模型进行预测的数据满足特定期望。然而,对于其他分析用例也是必要的;在这里,可以重用现有的方法和工具。

传统的数据监控通常在宏观层面进行,例如监控仓库中所有数据资产和流程,但机器学习需要更细粒度的监控,专注于特定的模型输入。在某些情况下,你可以依赖现有的上游数据质量监控。然而,可能还需要进行额外的检查,以控制特征转换步骤、实时模型输入或外部数据源。

可以监控各种指标以确保用于机器学习模型的数据质量。以下是一些常见的指标和检查类型:

  • 检查缺失数据意味着在特定特征和模型输入中整体缺失数据份额中寻找丢失的数据。通常,一些缺失值是可以接受的,但确保缺失数据的水平保持在可接受的范围内很重要,无论是对于整个数据集还是对于单个特征。你还应该检查缺失数据的各种表达方式,如“N/A”、“NaN”或“undefined”,因为简单的缺失值检查可能无法捕捉到所有情况。你可以使用像图表这样的视觉辅助工具来用你的眼睛识别缺失数据,并设定一个阈值,当缺失值过多时暂停模型或使用回退方案。根据模型特征重要性或 SHAP 值(见第十一章了解更多细节)识别数据集中的关键驱动因素也有帮助,并确保这些数据不是缺失的。这将允许你为关键特征和辅助特征设置不同的监控策略。

  • 重复数据是前一个问题相反的问题,也可能很危险。重复通常发生在整个数据集的一个子集上,这改变了数据分布,影响了下游模型。

  • 数据模式验证验证输入模式是否符合预期,以检测错误输入并跟踪新列或类别出现等问题。

  • 对个体特征类型的约束确保了特定的特征类型,例如确保一个特征是数值型的。这种方法可以捕捉到输入错误,例如一个特征以错误格式到达。

  • 重要的是检查模型调用的次数以确保模型正常运行。如果预计模型将定期使用,这尤其有用,因为它可以帮助您识别任何突然的变化或偏离典型使用模式。此外,检查模型响应的次数可以帮助您检测模型是否遇到问题或服务本身是否存在问题。这有助于防止服务中断并确保系统继续平稳运行。最后但同样重要的是,这项检查非常容易实施。

  • 通过对单个特征范围的约束,可以制定关于“正常”特征值的期望,例如合理性检查(例如,“年龄小于 100”)或特定领域的检查(“正常传感器操作条件在 10 到 12 之间”)。约束的违反可能是数据质量问题的症状。它可能从常识性检查开始,但对于更复杂的领域,则需要深入了解问题。

  • 特征统计跟踪特定特征的均值、最小-最大范围、标准差、特征之间的相关性、百分位数分布或特定的统计测试。这有助于揭示不太明显的失败,例如,在预期范围内某个特征异常行为。对于分类特征,可以使用直方图/类分布进行手动检查;然而,它们对于自动数据质量监控来说并不容易。

  • 通过异常效应,可以使用异常和离群值检测方法来检测“不寻常”的数据点并捕获损坏的输入。这将允许您专注于检测单个离群值或跟踪它们的总体率。

对于非结构化数据,如图像、文本或音频,可以应用类似的原则。我们无法应用像“客户年龄应在 12 到 100 之间”这样的原始检查,但我们可以引入将在测试中使用的数据之上的简单特征。这些特征不必直接用于模型(因此我们仍然直接将深度学习模型应用于图像),但仅用于数据质量监控。对于图像,可以是亮度或颜色温度;对于文本,可以是长度(字符数);对于音频,可以是波频分布。

在大多数情况下,对于管道中的每个步骤分别验证输入和输出可能是有用的,这样可以更容易地确定问题的来源。如果您的管道复杂且涉及多个步骤,例如合并来自不同来源的数据或应用多个转换,这尤其有帮助。通过在管道的不同阶段运行检查,您可以定位任何问题的来源并更快地进行调试。另一方面,如果您只验证最终计算的输出,并注意到某些特征不正确,那么确定问题的来源可能更困难,您可能需要回溯管道中的每个步骤。

监控指标的选择将取决于各种因素,如模型的部署架构(例如,批量、实时服务或流式工作流程)、数据的特定性和现实世界过程、用例的重要性以及期望的反应级别。例如,如果失败的成本很高,可能需要更详细的数据质量检查,并在采取预测行动之前添加在线数据质量验证的通过/失败结果。在其他情况下,更积极的策略可能就足够了,例如跟踪特定特征的平均值或缺失数据在仪表板上的份额,以监控随时间的变化。

机器学习中数据质量监控的一个挑战是监控过程的执行。确保输入到机器学习管道中的数据质量至关重要,但设置监控系统可能很耗时。这尤其适用于你需要在机器学习团队之外将专家领域知识编码化,或者如果你需要设置许多检查,例如监控原始输入数据和后处理特征值。

另一个挑战是在数据质量监控过程中管理大量触点。设计监控框架以检测关键问题而不被淹没是很重要的。如果你的监控系统每天发送数十个假警报,你最终会忽略其信号,因此找到适当的敏感性平衡至关重要。

最后,追踪数据质量问题根源可能很困难,尤其是当你有一个包含许多步骤和转换的复杂管道时。数据质量监控与数据溯源和追踪紧密相连,设置这一过程可能需要额外的工作。

我们强烈推荐阅读一篇来自谷歌的论文,名为“机器学习中的数据验证”(research.google/pubs/pub47967/)。

以下是对该论文的高度浓缩总结,其中列出了我们推荐采取以有效监控系统的行动:

  • 识别对机器学习用例至关重要的关键数据质量维度,例如完整性、准确性、及时性和一致性。

  • 为每个维度设置数据质量约束,并定义每个约束的可接受范围。例如,你可能设置一个约束,即数据必须是完整的,缺失值不超过 5%。

  • 实施一个数据验证管道,定期运行以检查数据是否符合定义的约束。该管道应生成一个报告,指出每个维度的数据质量状态。

  • 当数据质量超出可接受范围时,设置可靠的警报和通知。这将允许你及时采取行动修复任何问题。可靠性是假阳性与假阴性的平衡,这取决于具体情境。

  • 持续监控并随着时间的推移提高数据质量。这可能涉及根据你对数据和机器学习用例的了解更新约束或添加新的约束。

对于详细审查,请参阅之前提到的谷歌论文。

我们建议建立一个数据治理框架,以确保数据验证系统得到适当维护并与业务目标保持一致。这可能包括建立数据质量管理角色的职责,以及建立数据质量改进和问题解决流程。

14.4 模型质量和相关性

即使软件系统运行正常且数据质量高,这也不能保证机器学习模型会按预期执行。可能出现的一个问题是模型退化,这发生在模型开始表现不佳时——要么突然,要么逐渐。

模型退化,也称为模型漂移或陈旧,指的是模型性能随时间下降的现象。这可能由多种原因引起,例如数据的变化或模型训练所基于的现实世界关系的变化。模型退化的速度可能差异很大;一些模型可能几年都不需要更新,而其他模型可能需要每天在新鲜数据上进行重新训练。监控模型退化的方法之一是定期跟踪关键性能指标,并将它们与历史基线进行比较。如果指标开始显著下降,这可能表明模型退化(见图 14.6)。

图

图 14.6 定期重新训练可以帮助解决模型漂移问题。

模型漂移主要有两种类型(见图 14.7):

  • 数据漂移发生在模型应用于其之前未曾遇到过的输入时,例如来自新人口统计数据的数据。这意味着原始数据集不足以让模型进行泛化。

  • 当数据中的关系发生变化时,例如用户行为演变时,会发生概念漂移。持续监控模型漂移并采取适当的行动很重要。一种解决方案是重新训练模型以保持其准确性和效率。

图

图 14.7 在概念漂移中,相同的输入可能导致新的预期输出;在数据漂移中,当模型未适应时,传入的数据发生变化。

模型漂移的可能原因包括

  • 全球环境的转变,例如流行病/战争/危机/立法变化的开始。

  • 故意的商业变化,例如在新的地点或针对新的用户群体推出应用程序。

  • 当恶意行为者试图适应模型的行为时,会发生对抗性适应。

  • 模型反馈循环,其中模型本身影响现实。例如,推荐系统使用当前项目流行度作为特征并频繁推荐它,因此该项目变得更加流行,从而实现自我强化。

  • 模型退化有时会在模型的设计与其实际使用之间存在不匹配时发生。例如,如果一个潜在客户评分模型被设计用来预测转化概率,但用户开始通过提供不同的输入组合来使用它进行场景分析,以了解不同因素对模型决策的影响,这可能导致模型性能不佳。这些情况可能需要一种更适合预期用例的分析工具。阿森尼在他的职业生涯中也犯了一个类似的错误:在一家叫车公司,他的团队构建了一个模型来估计出租车司机到达目的地所需的时间。模型本身是好的,但他们试图将其应用于一个略有不同的场景(估计整体行程时间),结果模型表现不佳。最初的问题是关于短途旅行(免费司机接乘客),但后者也可能涉及长途旅行。

有些人区分输出漂移,它指的是机器学习模型产生的预测、推荐或其他输出的变化。这种变化可以通过使用统计测试或描述性统计将“新”输出数据与“旧”输出数据进行比较来检测。例如,如果一个很少推荐购物者购买太阳镜的模型现在在每一个推荐块中都推动它们,这可能表明输出漂移。

我们将输出漂移视为监控和检测模型漂移的指标之一,因为它可以表明模型性能的变化或输入和输出数据之间关系的变化,这属于概念漂移或数据漂移。通过识别和解决输出漂移,你可以帮助确保模型继续产生可靠和准确的结果。

模型漂移可能导致模型误差增加或预测错误,在严重的情况下,模型可能一夜之间变得不适用。持续监控模型漂移并采取适当的行动以保持模型的准确性和有效性是非常重要的(参见第八章以了解更多背景信息)。

14.4.1 数据漂移

数据漂移,也称为特征漂移协变量偏移,指的是当机器学习模型的输入数据发生变化,以至于模型对新数据不再相关的情况。这可能会发生在数据中变量的分布与模型训练时的分布显著不同时。因此,模型可能在新的数据上表现不佳,即使它在与“旧”数据相似的数据上可能仍然表现良好。

数据漂移的一个例子是,当训练用于预测用户在在线市场购买可能性的机器学习模型应用于通过不同广告活动获得的新的用户群体时。如果新用户来自不同的来源,例如 Facebook,而在训练期间模型没有很多来自这个来源的示例,那么它可能在这个新用户群体上的表现不佳。同样,如果模型应用于新的地理区域或人口统计群体,或者数据中重要特征的分布随时间变化,也可能发生数据漂移。

为了解决数据漂移问题,可能需要在新的数据上重新训练模型或为新的数据段构建一个全新的模型。监控数据和模型性能可以帮助早期发现数据漂移并采取纠正措施,以防止模型性能显著下降。由于数据漂移并非数据本身固有的错误,因此它必须在模型层面上进行修复。

14.4.2 概念漂移

当模型学习到的模式不再有效,即使输入特征的分布保持不变时,就会发生概念漂移。根据变化规模的大小,概念漂移可能导致模型准确性的下降,甚至使模型完全过时。

存在几种类型的概念漂移:渐进式概念漂移突发式概念漂移周期性概念漂移渐进式增量式漂移发生在外部因素随时间变化时,导致模型性能逐渐下降(见图 14.8)。这种漂移通常是可预见的,可能由多种因素引起,包括消费者行为的变化、经济的变化,或者当数据来自物理传感器时设备的磨损。

图

图 14.8 渐进式概念漂移

为了解决渐进式概念漂移,可能需要在新的数据上重新训练模型,甚至完全重建。为了确定是否需要这样做,监控模型性能随时间的变化是至关重要的,以确保它继续做出准确和可靠的预测。模型性能下降的速度或“老化”速度可能因具体应用和数据而异。

为了估计模型老化速度的快慢,使用较旧的数据进行测试并测量模型在不同重新训练频率下的性能是有帮助的。这可以给出一个指标,表明模型应该多频繁地用新数据更新以保持其准确性。同时,考虑外部因素的影响也是至关重要的,例如市场变化或新产品的推出,这些都可能影响模型输入和输出之间的关系,导致概念漂移。

监控机器学习模型的表现,并在必要时定期重新训练它们,是保持其在生产环境中有效性的关键方面。

突然的概念漂移通常是由于突然或剧烈的外部变化引起的,这些变化可能很难忽视。这类变化可能影响各种模型,甚至那些通常被认为是“稳定”的模型。例如,COVID-19 大流行几乎一夜之间影响了移动性和购物模式,导致需求预测模型无法预测某些产品需求的激增或由于边境关闭而取消大多数航班。除了大流行或股市崩盘等事件之外,突然的概念漂移也可能由于中央银行利率的变化、生产线的技术改造或应用程序界面的重大更新而出现。这些变化可能导致模型无法适应未见过的模式,变得过时,或由于用户旅程的变化而变得不相关。

在机器学习系统中,某些事件或模式随时间重复出现是很常见的。例如,人们在假日季节或一周中的某些日子可能会表现出不同的行为。这些重复变化,也称为反复漂移,可以在系统设计中预先考虑并加以考虑(见图 14.9)。例如,我们可以构建一组模型,在特定条件下应用,或将周期性变化和特殊事件纳入系统设计,以考虑这种反复漂移并防止模型性能下降。

figure

图 14.9 概念漂移的反复出现

14.4.3 如何监控

监控模型质量有两个主要目的:

  • 为了让您对模型的可靠性有信心

  • 当出现问题时提醒您

一个有效的监控设置应该提供足够的信息,以便有效地识别和修复模型中出现的任何问题。这可能涉及三种主要场景:

  • 重新训练模型

  • 重建模型

  • 使用备份策略

与其他数据模式(如图像或语音)相比,数据漂移对于表格数据可能更为显著。如果预测和真实数据可用之间存在延迟,这可能就是监控数据漂移和预测漂移等代理指标信号的信号。对于高风险或关键模型,您可能会使用更细粒度的监控和特定指标(例如,公平性)。低风险模型可能只需要监控与模型类型相关的标准指标。

有数百种不同的指标可以用来评估机器学习模型的表现(参见第五章)。我们将涵盖这些指标可以分组的几个类别。

模型质量指标评估模型预测的实际质量。这些指标可以在获得真实数据或反馈后计算,可能包括

  • 回归模型的平均绝对误差和均方根误差

  • 分类模型的准确率、精确率和 F1 分数

  • 排名模型的 Top-k 准确率和平均平均精度

按段模型质量涉及跟踪数据中特定子群体的模型性能,例如地理位置。这有助于识别特定段落的性能差异。

预测漂移发生在模型的预测随时间显著变化时。为了检测预测漂移,可以使用统计测试、概率距离度量或模型输出描述性统计的变化。我们建议阅读 Olga Filippova 的文章以获取更多详细信息(mng.bz/x60d)。

输入数据漂移指的是模型使用的输入数据的变化。这可以通过跟踪单个特征的描述性统计变化、运行统计测试、使用距离度量比较分布或识别特征和预测之间的线性相关性变化来检测。监控输入数据漂移可以帮助确定模型何时在陌生的环境中运行。

异常值是那些模型可能无法按预期表现的不寻常的个体案例;识别这些并标记它们以供专家审查是很重要的。请注意,这与数据漂移不同,数据漂移的目标是检测整体分布的变化。可以使用统计方法和距离度量来检测异常值。

Valerii 的篝火故事

当我在一家著名的加密公司工作时,我变成了其服务的狂热用户,其中当然包括使用法定货币购买加密货币的基本机会。有一天我尝试这样做,并成功了,直到我被反欺诈系统阻止。

为什么我被阻止了?因为我做了太多的交易。那为什么会这样?因为同一团队引入的交易限制迫使我做了比平时更多的交易。这是处理异常值的一种可能方式——在这种情况下,依赖于单一特征(做了太多交易的人)——但这可能不太用户友好,并导致失去这些用户的收入。幸运的是,我们能够进一步改进这个系统,并实施多阶段异常值检测和特定流程(有时可能包括人工审查)来处理它们。

公平性也很关键。对于某些用例,你应该确保模型对不同人口群体具有相同的效率。可以使用人口统计学对等性和均衡赔率等指标来评估模型偏差。

监控过程不能没有隐蔽的挑战。这包括

  • 缺乏蓝图**——这属于仍然没有稳固的即用型解决方案的问题列表,因为适当的指标和启发式方法取决于特定的上下文和模型的目标。因此,了解模型和数据对于选择正确的监控方法很重要。

  • 无真实标签的监控**—在没有访问真实标签的情况下,评估模型预测的实际质量通常很困难。在这些情况下,一个解决方案是使用代理度量值,同时仔细考虑如何设置阈值和触发警报。

  • 在规模上计算度量值**—在大型规模上计算复杂的度量值可能会非常耗费计算资源,尤其是在与分布式系统一起工作时。找到一种快速、高效且可扩展的方式来计算度量值是至关重要的。

14.4.4 如何反应

要涵盖所有可能的情况和解决数据漂移和概念漂移的方法,需要单独的一本书,但接下来我们将讨论一些情况。

数据漂移

面对数据漂移,你有两种选择。第一种选择是监控模型的性能,寻找准确度、平均误差和欺诈率等指标的变化。如果性能下降,你可能需要重新评估模型,并确定它是否仍然适用于当前数据。第二种选择是将额外的数据或特征纳入模型,以更准确地捕捉数据中的变化模式。在某些情况下,你应该考虑使用更新后的数据从头开始重新训练模型。

如果有新的标签可用且训练管道正在运行,按下重新训练按钮是非常诱人的(你可以在 Emeli Dral 的文章中找到更多信息,mng.bz/AaYo)。但如果我们深入挖掘,在此之前我们还可以做一些事情。

检查问题是否与数据质量有关,或者是否是模型试图捕捉的模式中的真正变化。数据质量问题可能来自各种来源,例如数据输入错误、数据模式的变化或上游模型的问题。在检测到数据质量问题后,立即采取措施进行监控和处理非常重要(参见第 14.3 节)。

如果数据漂移是真实的,尝试调查变化的原因。这有助于我们了解如何以最佳方式解决问题,并保持模型的准确性和有效性。

开始这个过程的其中一种方式是绘制经历过漂移的特征的分布,因为这可以提供关于变化本质的见解。另一个有帮助的步骤是与领域专家进行咨询,他们可能对可能导致变化的现实世界因素有所了解。

在检测到概念漂移的情况下,特征与模型输出之间的关系可能会发生变化,即使个别特征分布保持相似。可视化这些关系可以进一步了解漂移的性质。例如,绘制特征与模型预测之间的相关性可以突出这些关系的变化。从绘制成对特征相关性的变化中也可能获得额外的见解。

确定观察到的漂移是否具有意义并需要响应是很重要的。这可能涉及为特定用例设置定制的漂移检测阈值,并设置为对可能的重要变化发出警报。

然而,通常需要通过试错来迭代这些阈值,因为很难提前准确预测数据随时间推移将如何漂移。当生产环境中触发漂移警报时,我们需要仔细评估其性质和程度,以及它可能对模型性能产生的影响。这可能涉及咨询领域专家或进行进一步分析,以获得对变化的更深入理解。基于这种评估,你将能够决定是否处理漂移。在某些情况下,你可能能够理解漂移的原因,并决定(暂时)接受这些变化而不是采取行动解决问题。

例如,假设在不久的将来可能会获得额外的标签或数据。在这种情况下,可能值得等到可以考虑到这些信息后再采取行动。如果这是一个误报,我们可以更改漂移警报条件或统计测试,以及丢弃通知,以避免未来收到类似的警报。

在某些情况下,即使存在漂移,模型仍然可能表现良好。例如,如果模型预测中某个特定类别变得更加普遍,但这与观察到的特征漂移和预期行为一致,可能不需要采取任何进一步行动。在这些情况下,可能可以继续使用模型而不需要重新训练或更新它。然而,鉴于这一决策的潜在后果,密切监控模型以确保其有效性至关重要。

调整在管道中使用的预处理。例如,假设你的系统使用工厂中相机捕获的图像。在某个时候,工厂经理决定升级灯泡,因此生产线现在照明充足,图像也是如此。一旦由于漂移影响了模型性能,一个解决方案可以是应用人工的“变暗”函数来模拟原始数据分布。

使用更新或新的数据重新训练模型。 这通常是最直接的方法,如果可以获得必要的数据,则可能有效。通过使用更新或新的数据重新训练模型,你应该提高其性能并适应数据中的变化模式(如果数据管道中没有出现故障)。如果你遵循第十章中的良好实践,重新训练模型应该相对简单。

而不是点击重新训练按钮,你可以考虑开发一个对漂移更具鲁棒性的机器学习模型。 这可能涉及应用更鲁棒的模型架构(例如,为在线学习设计的架构)或使用诸如领域自适应等技术来使模型更能抵御数据分布的变化。以下是一些关于解决数据漂移的选项的额外细节:

  • 重新加权样本**—这涉及到在训练数据中给予较近样本更多的权重,以优先考虑较新的模式。这可能是一种简单处理数据漂移的方法,但它可能并不总是有效,尤其是在漂移显著时。

  • 为不同的数据段创建单独的模型**—如果模型在某些数据段失败,你可以考虑为这些段创建一个专门的模型。或者,你可以使用模型集成,其中每个模型负责不同的数据段。 (根据我们的经验,一个模型几乎总是比多个模型好,因为它包含了更多数据,但上下文是王。)

  • 更改预测目标**—通过更改预测目标,你可能能够提高模型的表现。例如,从每周预测切换到每日预测可能允许模型更好地捕捉数据中的短期变化。或者,你可以更改所使用的模型类型,例如从回归模型切换到分类模型(在某种程度上,这可能是审查问题的更粗略方式)。

  • 将人类专家知识融入模型训练过程**—你可以使用专家特征或主动学习技术来指导模型训练数据的选取。最终,最有效的方法将取决于你数据的特定特征和你的应用需求。

  • 引入更多正则化**—包括可以隐式处理漂移的技术。让我们回顾一下增强技术;它们是一种流行的正则化实践,并且你可以设计它们时考虑到可能发生在域中的漂移。例如,对于图像数据,模拟不同的天气条件可能是有用的。

  • 使用更强大的预训练模型作为初始化器**—在深度学习领域,如果初始模型是在更大、更多样化的数据集上训练的,那么训练自定义模型效果更好,因为它往往具有更好的泛化能力。

处理数据分布变化的一种策略是识别和隔离表现不佳的数据段。当变化不是普遍的,只影响数据的一个特定子集时,这特别有用。

要做到这一点,你可以从分析数据的变化特征和识别任何与模型性能的潜在相关性开始。例如,如果你看到某个特征(例如,位置)的分布发生了变化,你可能尝试通过该特征过滤数据,看看它是否导致了低性能。

一旦你能够识别数据中的低性能段,你可以决定如何处理它们。一个选择是为这些段进行不同的预测路由,要么依靠启发式方法,要么手动整理输出。或者,你可以单独识别这些段,直到收集到足够的新标记数据来更新模型。

最后,在单独的工作流程中处理异常值也可以帮助限制数据漂移下的错误。异常值是与其他数据显著不同的单个数据点,单独处理它们可以帮助确保模型可以继续有效运行。

另一种解决数据漂移的选项是在模型之上应用额外的业务逻辑,通过调整模型预测或更改应用程序逻辑来实现。这种方法可能有效,但难以推广,并且如果不小心操作,可能会产生意外的后果。

这种方法的良好例子是手动纠正输出,这在预测需求中很常见。针对特定商品、类别和地区的业务规则可以用来调整模型对促销活动、营销活动和已知事件的预测。在数据漂移的情况下,可以对模型输出应用新的纠正来考虑变化。

另一个例子是为分类问题设置新的决策阈值。模型输出通常是概率,决策阈值可以根据所需的概率进行调整,以分配标签。如果检测到数据漂移,阈值可以改变以反映新的数据。

或者,你可以考虑使用混合方法,将机器学习与非机器学习方法结合起来,特别是如果你数据量较少或者需要在变量关系不断变化的动态环境中进行预测时。在某些情况下,使用非机器学习解决方案可能更稳健且更容易维护,因为它不依赖于数据模式,可以基于因果关系或专家知识。然而,它可能不如机器学习模型灵活或能够适应不断变化的情况。重要的是要仔细考虑权衡并选择适合特定问题的适当解决方案。参见第 13.4 节。

概念漂移

在概念漂移的情况下,有几种重新训练模型的方法,包括使用所有可用数据、为新数据分配更高的权重,以及在收集到足够的新数据后删除旧数据。在某些情况下,简单地重新训练模型可能不足以,并且可能需要调整模型或尝试新的特征、架构或数据源,因为要捕捉的新模式对于现有模型来说过于复杂。

重新训练模型可能意味着对较小模型进行完全重新训练,或者对较大模型进行某种微调,这取决于漂移的严重程度以及计算预算是多少。

可能需要修改模型的范围或业务流程(这可以通过缩短预测时间范围、推迟预测以有更多时间积累数据,或增加模型运行频率来实现)。采用这种方法时,与业务所有者和系统其他消费者保持有效沟通非常重要,确保每个人都为变化做好准备,并能妥善处理。

应特别注意重复出现的漂移或季节性。其可能的原因有很多,例如黑色星期五购物增加、节假日或月底发薪日的出行模式改变。考虑将这些周期性变化纳入考虑,或构建集成模型来处理它们。这有助于防止模型性能下降,因为这些可预测的变化(重复漂移)是预期的,并且可以加以考虑。

为了有效地处理机器学习系统中的季节性,重要的是训练模型以识别和响应这些周期性变化。例如,如果机器学习模型用于预测家居服销售,它应该能够识别并预测周末需求的增加。在这种情况下,模型正确地预测了周末购物增加的已知模式。这样的可预测变化不需要警报,因为它是一种常规发生的事件。

漂移检测框架

在处理漂移时,首先要考虑可能采取的行动,然后设计一个漂移检测框架来监控潜在的变化。这允许你定义将触发警报的变化程度以及如何应对。

为了有效地设计它,考虑模型和数据背后的现实世界过程可能发生的变化方式。根据模型的需求和约束,可能有多种方法来检测和应对漂移。

例如,如果可以使用及时获取的真实标签直接计算模型的质量,则可以忽略分布漂移。相反,可以专注于使用基于规则的数据验证检查来识别和解决诸如输入损坏等问题。

另一方面,如果模型被用于具有延迟真实值和可解释特征的临界应用中,你可能需要切换到更全面的漂移检测系统。这可能包括详细的数据漂移检测仪表板和一系列统计测试,以帮助识别数据分布的变化和特征之间的相关性。如果你知道模型的关键特征以及它们提供的商业价值,你可以为这些特征分配不同的权重,并专注于检测这些特征中的漂移。如果不了解,可以考虑多个弱特征的总体漂移,但在此情况下,将漂移的定义阈值提高,与少数关键特征相比,以避免假阳性。

最终,漂移检测的最佳方法将取决于模型的具体需求和限制,可能涉及不同方法和指标的结合。因此,务必牢记假阳性警报的潜在可能性以及模型失败的后果,并设计一个能够有效检测和响应漂移的系统,同时最大限度地减少中断和停机时间。

关于商业 KPI 的注意事项

由于隔离 ML 系统的影响复杂,以及测量某些指标困难,监控商业 KPI 可能具有挑战性。在这些情况下,找到可以提供对 ML 系统性能洞察的代理指标或可解释的检查是很重要的。还值得注意的是,在模型退化的情况下,监控商业 KPI 可能并不总是提供所需的环境。在这种情况下,可能需要调查其他因素,如模型、数据和软件,以确定任何问题的根本原因。

然而,正如图 14.5 所示,商业 KPI(关键绩效指标)之所以位于金字塔的顶端,是有其原因的。监控商业指标和关键绩效指标对于理解你的机器学习系统(ML system)的商业价值至关重要。跟踪诸如收入和转化率(例如,如果你的模型围绕用户获取展开)等指标,可以帮助你确定 ML 系统是否达到其目标,并对业务产生积极影响。

同样重要的是,在监控过程中涉及数据科学家和业务利益相关者,以确保 ML 系统满足业务需求。我们还建议跟踪绝对值和相对值,以更全面地了解 ML 系统的影响。

最重要的是,无论你为你的 ML 系统定义了什么技术精湛的指标,它们不过是商业 KPI 逐渐累积的结果。因此,任何系统的有效性和可持续性最终都会直接影响你雇主或你自己的业务的成功。

14.5 设计文档:监控

监控您的机器学习系统的方法可能因目标、特性和架构而异。我们提供的两个设计文档展示了它们独特的监控方法,因为我们正在设计的系统具有本质的不同和特性。

14.5.1 Supermegaretail 的监控

我们在本章的某些部分专门讨论了监控预测系统的特殊性,现在我们准备深入探讨专注于监控 Supermegaretail 模型的实际部分。

设计文档:Supermegaretail

第十一章 监控

i. 现有基础设施分析

不幸的是,需求预测是 Supermegaretail 的先驱机器学习项目之一,这意味着没有适当的机器学习监控基础设施。幸运的是,快速初步研究证明是富有成效的,我们发现了 Evidently AI——一个开源的 Python 库(github.com/evidentlyai/evidently),它有助于监控。口号“我们构建工具来评估、测试和监控机器学习模型,这样您就不必这样做”完美地符合我们的目标,直到我们决定建立自己的平台(见第 3.2 节)。根据描述,Evidently AI 覆盖模型质量、数据漂移、目标漂移和数据质量。这意味着我们仍然需要为实施这些功能建立一些基础。

ii. 记录

我们将在列式数据库管理系统(DBMS)中保留模型预测日志。我们应该记录每次预测的数据:输入的特征和模型输出以及时间戳。我们将使用开源的 ClickHouse,因为它已经在公司中用于其他类似需求。

此外,我们还将记录基本统计数据:每秒请求数、资源利用率、错误率、p90、p99、p999 延迟以及错误率,以及每小时、每天和每周的模型调用次数,以及模型在同一聚合级别上的平均、中位数、最小值和最大预测值。我们将使用 Kafka + Prometheus + Grafana 来实现这一点。我们将保留最后一个月的数据。我们还将使用这个堆栈进行实时机器学习监控和可视化(mng.bz/ZVOR)。

iii. 数据质量

除了基本的提取、转换、加载和数据质量检查之外,我们还将监控以下内容:

  • 缺失数据,作为整个数据集的百分比,以及根据特征重要性分别作为最重要的特征的百分比(见第 11.2 节)。我们将使用历史数据(清除损坏管道的实例)来计算 z 分数。对于重要特征,我们将设置三个 z 分数的警报,对于其他特征,我们将设置四个 z 分数的警报。此外,我们还将使用 Evidently AI 库中的几个测试套件预设。有一个预设用于检查数据质量,另一个用于检查数据稳定性。

  • 模式合规性。所有特征都在那里吗?它们的类型匹配吗?有新列吗?

  • 特征范围和统计信息。为了确保学习模型被提供高质量的数据,我们将手动为每个重要特征定义预期的范围,以及检查无效的统计信息(例如,销售额的负数据,最小值 >= 0)。

  • 相关性。为了检测数据中的任何异常,我们将绘制特征之间的相关矩阵,并比较两个图表之间的差异。对于高于 |0.15| 的残差,我们将设置一个基本的警报。

iv. 模型质量

我们非常幸运,能够以非常小的延迟获取真实标签。事件发生后 15 分钟,我们就能收到每日的销售信息。考虑到这一点,我们将监控 1.5、25、50、75、95 和 99 分位数,以及作为是和权重等于 SKU 价格的情况。此外,我们将监控均方根误差和平均绝对误差,以跟踪均值和中位数。在收到前三个月的数据后,我们将设置最终阈值;我们将根据历史数据和模型在验证集上的性能选择初始阈值。

此外,我们为负值和最大值设置了警报。如果新的最大值比之前看到的最大值高出 50%,则警报会触发。

我们将设置预测漂移监控。我们将将其用作第二天、一周和一个月预测的早期警报。我们将测试两种方法:人口稳定性指数 > 0.2 和 Wasserstein 距离 > 0.1。对于 Wasserstein 距离,我们将对控制数据集应用增长乘数。例如,当比较 2021 年 4 月与 2022 年 4 月时,考虑到整体增长预期为 15%,我们将 2021 年的所有数据乘以 1.15。我们将根据历史数据实验进一步调整这一比例。

v. 数据漂移

尽管我们能够以轻微的延迟获取真实数据,但我们不需要将输入漂移作为理解模型相关性的代理。然而,这仍然有助于我们在影响模型质量之前检测到即将到来的变化。在阅读了一篇题为“哪种测试是最好的?我们比较了 5 种在大数据集上检测数据漂移的方法”的文章(mng.bz/2g5g)后,我们决定选择 Wasserstein 距离来在数据漂移的情况下提醒我们。我们从平均漂移分数的阈值开始。我们稍后可以尝试应用标题为“特征偏移检测:通过条件分布测试定位哪些特征发生了偏移”的论文(mng.bz/RNQZ)。

vi. 业务指标

我们感兴趣的业务指标与我们在 2.1.1 节中描述的相同:收入(预期增加)、库存水平(预期减少或保持不变)和利润率(预期增加),我们将通过一系列 A/B 测试、随时间切换和交换控制组来监控这些指标。

14.5.2 对 PhotoStock Inc.的监控

PhotoStock Inc.的系统监控中的动作集合将与之前的示例不同,因为我们在这里处理的是“智能”股票图片搜索引擎的模型设计。

设计文档:PhotoStock Inc.

第十一章 监控

i. 软件健康

我们需要确保以下日志可用:

  • 原始查询

  • 候选文档 ID

  • 最终排名的前 20 名

此外,所有负面场景和回退都应该记录下来。

日志应通过三个 ID 进行追踪:

  • user_id—可能为空,因为并非所有用户都登录

  • session_id—由后端处理;可能包含连续的多个查询

  • request_id—与单个搜索/查询相关的日志批次

我们将使用公司内部使用的相同日志存储提供商(AWS Cloudwatch)。

我们应该报告与延迟相关的指标,因为我们假设这对用户体验很重要,并且一些组件可能有些慢。以下是一个样本列表(不一定是全面的):

  • 获取候选

  • 最终排名

  • 覆盖应用

指标应报告给 AWS Cloudwatch。

ii. 数据健康

考虑到我们数据来源,我们对图像数据(照片由我们内部审核团队以及他们的 AI 工具仔细审查)非常有信心,但对搜索查询(从用户处获得)和排名分数(通过众包获得)则信心不足。

搜索查询应过滤以减少总垃圾量(例如,一只猫在键盘上走过)。除此之外,一些愚蠢的查询是可能的,并且应该包含在训练集中,因为那是我们用户的真实输入。过于激进的过滤可能导致训练和服务的偏差。

用于模型训练的排名标签是我们的生计。它们应该尽可能正确,我们应该与标签平台供应商一起投资于它们的质量。这必须包括交叉检查和诱饵来过滤掉不准确的标签员。此外,我们可以考虑使用一些算法验证标签数据,借助基础模型——例如,如果图像描述 + 标签与查询或提示相关,则可以使用提示 GPT API;或者使用像 LLAVA (llava-vl.github.io/) 这样的自服务多模态图像 + 文本模型。在未来,我们可以考虑使用模型作为标签的主要来源,而人们作为验证者,根据双方的标签准确性来决定。

iii. 模型健康

根据我们的理解,图像搜索问题对漂移问题不太敏感。然而,用户偏好和我们所托管的照片可能会有一些变化:可能出现新的主题,新的图像类型等。此外,我们不输出排名之外的内容,因此很难想象任何灾难性的故障(在最坏的情况下,我们可以切换回之前的非 ML 搜索引擎)。

由于我们假设在之前的步骤(验证、测试)中模型质量得到保证,因此我们不需要在第一版中包含特定的模型健康状况监控。然而,当前搜索引擎已经存在一些监控器:平均点击位置、平均首次点击位置等。保持它们不变应该是第一道防线。如果未来我们意识到与漂移相关的问题对我们造成了真正的伤害,我们可以考虑使用开源解决方案,如 Deepchecks (deepchecks.com/) 进行概念验证。

摘要

  • 即使是最经过训练和最准确的模型,如果没有适当的监控,随着时间的推移也可能开始退化。这可能是由于输入数据、模型架构或模型输出的变化。

  • 正确监控的基础是软件后端,随后是数据质量和完整性、模型质量和相关性以及业务关键绩效指标。

  • 确保监控你系统的软件健康状况,因为它对于提供高效的任务执行和及时响应请求至关重要。未能优先考虑软件后端稳定性和性能可能会对机器学习系统的整体有效性产生不利影响。

  • 在传统软件系统中使用的许多监控实践——如应用程序和基础设施监控、警报和事件管理——也可以应用于机器学习系统。

  • 监控数据质量意味着数据处理阶段(从访问问题到数据格式变化)的问题,或数据源可能因错误、物理故障或外部 API 问题而导致的潜在损坏。

  • 在级联模型的情况下,你必须处理相互关联的模型循环,前一个模型的输出数据将迫使下一个模型做出无效预测,从而破坏整个系统。

  • 一些模型可以持续数年而不需要更新;而另一些模型可能需要每天在新鲜数据上进行重新训练。定期跟踪关键性能指标,并将它们与历史基线进行比较,以避免模型退化或尽早发现它。

  • 当数据中变量的分布与模型训练时的分布显著不同时,就会发生数据漂移。为了解决数据漂移问题,可能需要在新的数据上重新训练模型或为新的数据段构建一个新模型。

  • 当模型学习到的模式不再有效时,即使输入特征的分布保持不变,也会发生概念漂移。这可能导致模型准确性的下降或使模型完全过时。

  • 面对数据漂移时,你有两种选择:你可以监控模型的性能并寻找准确率、平均误差或欺诈率等指标的变化,或者将额外的数据或特征纳入模型以更准确地捕捉数据中的变化模式。

  • 当出现概念漂移时,你可能需要重新训练模型,包括使用所有可用数据,为新数据分配更高的权重,或者如果收集到的新数据足够,则丢弃过去的数据。

第十五章:15 服务和推理优化

本章涵盖

  • 在服务阶段和推理阶段可能出现的挑战

  • 将派上用场的工具和框架

  • 优化推理管道

在生产环境中运行你的机器学习(ML)模型是达到系统高效运行生命周期所需的最后一步。一些机器学习从业者对此方面表现出较低的兴趣,更愿意专注于模型开发和训练。然而,这可能是一个错误的决定,因为模型只有在部署并有效利用于生产中时才能发挥作用。在本章中,我们讨论了部署和推理机器学习模型的挑战,以及回顾了优化推理过程的不同方法。

正如我们在第十章中提到的,推理管道是一个序列(在大多数情况下)或更复杂的无环图,它以原始数据为输入,产生预测作为输出。除了 ML 模型本身外,推理管道还包括特征计算、数据预处理、输出后处理等步骤。预处理和后处理是有些通用的术语,因为它们可以根据特定系统的具体要求以不同的方式构建。让我们举几个例子。典型的计算机视觉管道通常从图像缩放和归一化开始;典型的语言处理管道,反过来,从标记化开始;而典型的推荐系统管道则从从特征存储中提取用户特征开始。

正确调整推理的作用可能因领域而异。高频交易公司或广告技术企业一直在寻找最优秀的人才以优化其极低延迟的系统。移动应用和物联网(IoT)开发者将高效的推理置于优先事项之首,追求更高效的电池消耗,从而改善用户体验。那些在其产品中使用高负载后端的人感兴趣的是在不花费大量资金的情况下保持负载。同时,有许多场景下模型预测并不是瓶颈。例如,每周一次,我们需要运行一个批量作业来预测下周的销售情况,生成报告,并将其分发给采购部门。在这些情况下,如果报告需要在周一早上通过公司电子邮件发送,那么它是在周日晚上编译需要 10 分钟还是 3 小时,并没有太大的区别,只要它被安排在周日晚上即可。

通常,本章主要关注基于深度学习的系统。这并不令人惊讶,因为重型模型在生产部署前需要更多的工程努力,而只要你的特征基础设施(在第十一章中描述)到位,提供一些轻量级解决方案,如逻辑回归或小型树集成,并不会太复杂。同时,这里描述的一些原则和技术适用于任何机器学习系统。

15.1 服务和推理:挑战

就像在系统设计中经常发生的那样,我们的第一步在于定义需求。有几个关键因素需要牢记,我们将在下面逐一讨论:

  • 延迟**—这定义了我们对系统在提供预测方面的响应速度的期望。对于需要即时响应的实时应用,延迟通常以毫秒计算(在某些极端情况下,即使 1 毫秒也太长了!),而在某些场景中,等待数小时甚至数天是完全可接受的。

  • 吞吐量**—这指的是系统在给定时间内可以处理的任务数量或数据量。在机器学习的世界里,它意味着模型每单位时间可以产生的预测总数。在需要短时间内处理大量数据的场景中,如大型数据集的批量处理,优化吞吐量通常至关重要。

  • 可扩展性**—我们需要了解系统可能面临的预测数量以及这个数量如何增加或减少。负载模式通常是季节性的,可能因行业而异。对于零售业,在假日季节或像黑色星期五或网络星期一这样的巨大折扣日,通常会看到销售额的激增。对于广告技术行业,由于互联网上的活动增加,负载可能会更高。虽然这里提到的某些峰值是可以预测的,但其他一些可能会突然出现;一个应用程序的病毒式流行或一个大客户的突然使用激增是我们无法提前准备的。系统应具备足够的可扩展性,以处理峰值负载,而不会降低延迟和吞吐量。

  • 目标平台—您的模型可以在仅 CPU 或 GPU 加速的服务器上运行;在 AWS Lambda 或 Cloudflare Workers 等无服务器环境中;在桌面、移动或物联网设备上;甚至可以在浏览器中运行。除了优势之外,每个平台都有其自身的局限性和要求,在构建系统之前,我们必须对这些有深刻的理解。如果我们正在构建移动应用,我们需要考虑模型的大小,因为它应该足够小,可以放入应用包中,同时足够高效,可以在用户的设备上运行而不耗尽电池。如果我们正在开发后端系统,在硬件选择上我们有更多的自由度,这为系统的复杂性和规模提供了大量的机会。如果我们目标平台是异构的物联网硬件,我们可能无法设计一个功能齐全的复杂模型,因此坚持最简单的架构成为主要的技术要求。

当我们必须为我们的产品使用多个平台时,事情可能会变得更加复杂。在某些情况下,我们可能会决定在用户设备上运行小批量,其余的发送到后端,或者在将模型发送到用户设备进行推理之前,在后台进行微调。这样,就遵循了两个平台的综合要求。此外,我们可能需要在单个平台上使用各种计算单元。阿列克谢曾经需要加速一个运行在低端 GPU 设备上的系统。在底层,该系统使用一个小模型来处理多个并发请求,这导致那些廉价的 GPU 无法处理负载。阿列克谢提出的解决方案引入了一个混合设备会话池:每次 GPU 过载时,下一个请求由 CPU 处理,从而在设备之间提供更平衡的负载,并满足延迟要求。

  • 成本—机器学习系统通常是公司中最资源密集型的解决方案,随着重型生成模型越来越受欢迎,成本已经成为一个至关重要的因素,就像以前从未出现过一样。随着机器学习基础设施成本成为一个不断增长的关注点,企业被迫寻找智能的推理管道决策,这可以带来巨大的财务效益。在构建系统之前,了解基础设施的成本以及它将如何根据增加的负载进行扩展是很重要的。甚至可能导致基础设施成本最终高于系统产生的收入。在其他情况下,如果推理是在用户设备上进行的,这可能不会成为一个问题。例如,对于移动应用或物联网设备,随着用户基础的不断增长,它不会以任何显著的方式影响基础设施成本(我们无法断言它根本不会影响;如果用户从您的内容分发网络下载模型,每增加 1,000 名新用户将使您额外花费几分钱)。

  • 可靠性**—当你在选择推理平台时选择了便宜的选择,可靠性可能会成为一个问题。仅仅为了在炎热的季节中突然出现故障而选择最便宜的硬件供应商,有时可能比投资更多在经过时间考验的可靠解决方案上更糟糕。想想在意外负载高峰期间可能发生的所有灾难,模型中的错误,或硬件故障,最重要的是,系统将如何(以及是否)处理这些问题。

  • 灵活性**—即使一个新发布的系统显示出稳定的表现和效率,我们也不能确定未来的需求和我们将需要实施的想法。因此,系统应该足够灵活,能够消化变化和改进。这些可能包括一个新的模型(甚至可能使用不同的框架进行训练!),额外的预处理或后处理,新功能,额外的 API 等。始终记住,系统将不断发展,并且应该易于修改,而不会影响现有的功能。

  • 安全和隐私**—这个主题的范畴远不止一段文字,但在这个章节的范围内,我们只能提到安全需求在很大程度上依赖于目标平台。例如,当你的系统完全运行在你的后端,预测从未离开组织的边界时,你几乎不需要考虑超出当前协议的安全问题。另一方面,如果你正在开发一个将在用户设备上运行的移动应用,你需要认真考虑保护模型免受逆向工程。在某些情况下,模型甚至需要保护用户本身,其中最显著的例子是 2023 年流行的针对大型语言模型(LLMs)的越狱,当时用户试图通过它们来获取敏感问题的答案。

15.2 权衡和模式

这里提到的一些因素往往是相互冲突的,甚至是相互排斥的。因此,我们无法同时优化所有这些因素,这意味着我们将在这些因素之间找到妥协。然而,在寻找各种场景中的公平平衡时,我们可以倾向于某些模式。

15.2.1 权衡

让我们从延迟吞吐量开始。两者都是非常受欢迎的优化候选者,其中之一得到改进可能会带来另一个的改进或退化。

真实的生产系统通常是在给定的延迟预算内优化最佳吞吐量。延迟预算由产品或用户体验需求决定(用户是否期望实时/近实时响应,或者他们是否可以容忍延迟?),在这个预算下,目标是通过改变模型架构或推理设计(例如,通过批处理,如我们在 15.3 节中描述的)来最大化吞吐量(或最小化预期吞吐量所需的服务器数量)。

让我们来看一些例子。想象一个简单的深度学习模型——一个像 Resnet 这样的卷积网络或者一个像 BERT 这样的 Transformer。如果你只是减少块的数量,无论你的推理设置如何,你的延迟和吞吐量数字都有很大可能得到改善,所以事情非常直接(模型的准确率可能会下降,但在当前例子中并非如此)。但想象一下有两个模型,它们具有相同数量的块和每个块相同的参数数量,但使用了两种不同的架构:模型 A 以并行方式运行它们并进行进一步聚合(类似于 ResNeXt 架构),而模型 B 则按顺序运行每个块。由于模型 B 架构的顺序性,其延迟将高于模型 A,但与此同时,你可以并行运行多个模型 B 的实例,或者以大批次大小运行它,因此模型 A 的吞吐量并不比模型 B 差。因此,并行性是影响延迟和吞吐量的内部因素之一。在实践中,这意味着参数数量或块的数量不能成为比较模型的最终因素,我们对架构在推理平台背景下的理解帮助我们识别瓶颈,如图 15.1 所示。

figure

图 15.1 宽度与深度模型的示例。虽然它们具有相同的参数数量,但深度模型在并行计算方面更为受限

另一个权衡与模型准确度(或用于系统的其他 ML 特定指标)以及相关的延迟/吞吐量/成本有关。ML 工程师常常倾向于通过升级到更大的模型来解决模型的不足。这偶尔可以提供一些好处,但最初这是一种昂贵且扩展性差的解决问题的方式。同时,试图仅通过选择最简单的模型来优化成本也不是一个好主意。计算成本与模型价值之间的关系不是线性的,在大多数情况下,都有一个甜点,模型足够好,成本相对较低,而光谱边缘的激进解决方案效率较低。如果我们回顾第 9.1 节,可以以同样的想法构建:我们可以将模型的准确率作为计算成本的函数进行绘制,并为我们特定的问题找到合适的平衡(详见图 15.2 的详细信息)。

figure

图 15.2 同一家族的不同大小模型及其在 ImageNet 数据集上的相应准确度值。图表表明,较大的模型通常具有更高的准确度,但这种效应趋于饱和。(来源:mng.bz/QVzG。)

最后,一个可能不是那么明显但仍然值得提到的权衡是基于研究灵活性生产级服务性能之间的联系。一方面,我们可以从构建一个易于从实验沙盒直接迁移到生产的模型中受益。另一方面,研究团队和生产系统之间有明确的分离,将允许研究团队在不影响生产系统的情况下实验新想法。但随着我们进一步深入,实验代码和真实系统推理之间的差异将放大,从而增加面对由环境差异引起的缺陷的机会,没有适当的监控和集成测试投资,这将很难追踪(请参阅第十三章关于部署方面和第十四章关于可观察性的内容)。

当面临两种选择时,你可能需要考虑某些工具和框架。因为你选择的路线将显著影响你系统的可持续性,所以接下来的部分将完全致力于这个非常重要的主题。

15.2.2 模式

一旦我们确定了为了优化我们的模型需要做出的权衡,就到了选择合适的模式来实施的时候了。我们再次将范围限制在一个小的列表中,并提及三种不同的模式。尽管这个列表并不完整,但你很可能会在你的系统中使用这些模式之一。

值得首先提到的第一个模式是批处理,这是一种用于提高吞吐量的技术,需要提前考虑。如果你知道系统可以以批处理模式使用,你应该相应地设计它。批处理不仅仅是一个二元属性(使用或不使用),它具有多个细微差别。想象一下一个典型的网页 API:客户端(我们无法控制)发送一些数据,后端返回一个预测。这并不是典型的离线批处理预测,但存在动态批处理的余地:在后端,我们等待一个短暂的时间(例如,20 毫秒或,比如说,32 个请求,哪个先到就先处理)并收集这个时间段内到达的所有请求,然后对它们进行批推理。这样,我们可以减少客户端的延迟,同时仍然保持系统的响应性。这种方法在 Nvidia 的 Triton 推理服务器和 Tensorflow Serving 等框架中得到实现。智能批处理的另一个例子与语言模型相关:模型的典型输入大小是动态的,对多个不同大小的输入进行批推理需要填充到最长输入的大小。然而,我们可以根据大小对输入进行分组,并分别对每个组进行批推理,从而减少填充开销(以较小的批次或更长的批次累积窗口为代价)。你可以在 Graphcore 的博客上了解更多关于这项技术的信息(mng.bz/XVEv)。

我们接下来要讨论的第二种模式是缓存,它已经获得了优化终极级别的地位(我们将在本章后面讨论推理优化)。缓存的使用源于一个显而易见的思想:永远不要两次计算相同的东西。有时这就像在更传统的软件系统中一样简单:输入数据与一个键相关联,并使用一些键值存储来保存之前计算的结果,这样我们就不必重复进行昂贵的计算。

列表 15.1 展示了最简单的内存缓存示例
class InMemoryCache:
    def __init__(self):
        self.cache = {}

    def get(self, key: str, or_else: Callable):
        v = self.cache.get(key)
        if v is None:
            v = or_else()
            self.cache[key] = v
        return v

在现实中,机器学习模型的输入分布可能有一个长尾,因为大多数请求都是唯一的。例如,根据谷歌的数据,所有搜索查询中有 15%是完全独特的(mng.bz/yoeB)。鉴于缓存的有效期通常远低于整个谷歌历史,任何合理的时间窗口内,独特(不可缓存的)查询的比例都会很高。这意味着缓存没有用吗?不一定。一个原因是,即使是很低的计算节省比例也能在节省资金方面带来巨大的好处。另一个概念是使用模糊缓存的想法:我们不必检查键的直接匹配,我们可以放宽匹配条件。例如,Arseny 看到过一个系统,其缓存键基于正则表达式,因此结果可以被多个匹配相同正则表达式的相似请求共享。更激进的缓存可以基于键的语义相似性构建(例如,github.com/zilliztech/GPTCache使用这种方法来缓存 LLM 查询)。作为关注可靠性的从业者,我们建议在使用这种缓存时三思而后行:其模糊程度很高,因此缓存容易产生错误的缓存命中。

最近在 LLM 领域出现了一种第三种模式:模型间的路由。受“专家混合”架构(mng.bz/M1qW)的启发,这被证明是优化的一大步:一些查询很困难(并且应该由昂贵的服务端高级模型处理);一些则更简单(因此可以委托给更复杂且成本更低的模型)。这种方法主要用于通用 LLM、机器翻译和其他主要针对自然语言处理任务的领域。这种实现的一个很好的例子在 Alireza Mohammadshahi 等人撰写的论文“Leeroo Orchestrator: Elevating LLMs Performance Through Model”中有详细阐述(arxiv.org/abs/2401.13979v1)。

这种模式的变体是使用两个模型(一个快速但不完美;另一个慢但更准确)。在这种组合中,第一个较小的模型(因此可以以低延迟渲染)快速响应,随后被第二个较重的模型的输出所取代。与之前的模式不同,它并不节省总的计算需求,但它优化了一种特殊的延迟,即初始响应时间。

15.3 工具和框架

推理过程高度依赖于工程。幸运的是,有许多工具和框架可供使用。遵循本书采用的方法,我们不旨在提供每个可能需要构建可靠推理流程的框架的全面概述;相反,我们专注于原则,并提及一些流行的解决方案以供说明。

15.3.1 选择框架

一种常见但并非立即显而易见的启发式方法是分离你的训练和推理框架。通常,在研究、原型设计或训练过程中,人们会使用像 Pandas、scikit-learn 和 Keras 这样的工具,因为它们在灵活性、简单性方面提供了便利。然而,由于灵活性和简单性之间不可避免的权衡,它们并不适合推理。这就是为什么在同一个框架中训练模型,然后将其转换为另一个框架进行进一步推理的做法变得流行。此外,尽可能地将训练框架与推理框架解耦是至关重要的,这样如果需要切换到具有新特性的不同训练框架,它就不会影响推理流程。这对于预期在几年内保持运行和演变的系统来说尤为重要。

从另一个角度来看,一些以研究为先导的框架,如 Torch,倾向于缩小研究和生产之间的差距。Torch 2.0 中引入的编译功能允许从用于训练和相关实验的相同代码中生成相当优化的推理流程。因此,无论您是想使用通用框架,还是为了不同的目的而结合两个甚至更多的解决方案,这两种范式都是可行的,具体取决于您选择哪种方法(有关最流行框架的信息,请参阅图 15.3)。

figure

图 15.3 一系列工具,它们专注于研究或生产服务目标

在研究灵活性和生产环境中的高性能之间取得平衡可能需要一种跨框架的格式。为此,ONNX 是一个流行的选择,它被许多训练框架支持,用于将它们的模型转换为 ONNX。另一方面,推理框架通常与 ONNX 格式一起工作,或者允许将 ONNX 模型转换为它们自己的格式,使 ONNX 成为 ML 世界中的通用语言。

这里应该提到,ONNX 不仅仅是一种表示格式,而是一个生态系统。它包括一个可用于推理的运行时以及一组用于模型转换和优化的工具。ONNX 运行时非常适合各种后端,并且几乎可以在任何平台上运行,并为每个平台提供特定的优化。这使得它成为多平台系统的绝佳选择。根据我们的经验,ONNX 运行时在灵活性(尽管不如直接服务 PyTorch 或 scikit-learn 模型灵活)和性能(尽管不如为特定组合的模型、硬件和使用模式量身定制的解决方案性能好)之间取得了流行的平衡。

对于服务器 CPU 推理,一个流行的引擎是英特尔的开源 VINO。对于 CUDA 推理,通常使用英伟达的 TensorRT。这两个引擎都针对其目标硬件进行了优化,并且也作为 ONNX 运行时后端提供,这使得它们可以像 ONNX 运行时一样使用。另外两个值得提及的是 TVM(tvm.apache.org/),这是一个能够为各种硬件目标生成代码的深度学习模型编译器,以及 AITemplate(github.com/facebookincubator/AITemplate),它甚至可以通过 ROCm 软件堆栈在不太常见的 AMD GPU 上运行模型。

对于熟悉 iOS 模型部署的人来说,CoreML 可能并不陌生,这是一个用于 iOS 推理的引擎,它使用自己的格式。Android 开发者通常选择 TensorFlow Lite,尽管由于 Android 的碎片化程度更高,Android 上可用的选项更多。这个列表远非完整,但它提供了一个关于可选项多样性的概念。

当我们说“引擎 X 在设备 Y 上运行”时,这可能并不完全准确。例如,即使整体推理旨在在 GPU 上运行,某些操作可能也会转发到 CPU,因为这样做效率更高。GPU 擅长大规模并行计算,这使得它们非常适合矩阵乘法或卷积等任务。然而,与控制流或稀疏数据相关的某些操作更适合由 CPU 处理。例如,CoreML 会动态地将执行图分割到 CPU、GPU 和 Apple Neural Engine 之间,以最大化效率。

各种推理引擎通常附带优化器,可用于提高模型性能。例如,ONNX 运行时提供了一套优化器,可以通过修剪图中的未使用部分(例如,仅在训练中用于损失计算的)或通过将多个操作融合为一个来减少操作的数量。这些优化器通常是用于准备模型以供推理的独立工具,但不是推理引擎本身的一部分。

到这本书出版时,这个部分可能会过时,因为这个领域正在快速发展。例如,当我们开始写这本书时,很少有人关注大型语言模型(LLMs),而现在它们无处不在。人们经常争论哪种推理引擎对他们来说更好——是 VLLM (github.com/vllm-project/vllm),TGI (mng.bz/aVG7),还是 GGML (github.com/ggerganov/ggml))。LLM 推理整体上是一个特定的主题——与大多数更传统的模型不同,LLMs 通常受限于大的内存占用和自回归范式(在预测了标记 T 之前,你不能预测标记 T + 1,这使得在没有额外技巧的情况下并行化几乎不可能)。如果你对 LLM 推理感兴趣,我们建议阅读博客文章 vgel.me/posts/faster-inference/ 和一些真正深刻的研究成果(例如,在润色这一章节的时候,我们被 Song 等人撰写的“PowerInfer: 使用消费级 GPU 的快速大型语言模型服务”所打动 (arxiv.org/abs/2312.12456))。

虽然 推理引擎推理框架 这两个术语经常被互换使用,但它们并不完全相同。推理引擎是一个用于推理的运行时,而推理框架是一个更通用的术语,可能包括一个引擎、一组用于模型转换和优化的工具,以及其他辅助服务各个方面的组件,例如批处理、版本控制、模型注册、日志记录等。例如,ONNX Runtime 是一个推理引擎,而 TorchServe (pytorch.org/serve/)) 是一个推理框架。

对于是否需要一个功能齐全的框架或仅仅是在推理引擎之上添加一个小的包装层,并没有一个唯一的答案。根据我们的经验,一旦你的需求达到高度确定性,并且你通常倾向于使用更先进的设备同时拥有足够的人力资源来维护它,框架就是最佳选择。另一方面,一旦你处于初创公司阶段,需要以某种方式交付系统,但又知道你的需求将在接下来的几个月内才会明确,选择更精简的方式,即使用推理引擎和一些通信层(例如,Web 框架)的简单组合来部署模型,并将更可靠的解决方案推迟到下一个版本,这是有意义的。

15.3.2 无服务器推理

无服务器推理是一种新兴的方法,与传统基于服务器的模型相比,具有独特之处。AWS Lambda 的推广使得这种无服务器范式现在在多个主要云服务提供商的替代方案中得到了体现,例如 Google Cloud Functions、Azure Functions 和 Cloudflare Workers,以及 Banana.dev 和 Replicate 等初创公司。截至本文撰写时,主要提供商主要提供有限的 GPU 能力,主要提供 CPU 推理,尽管随着初创公司继续在该领域突破边界,这种情况可能会改变。

主要云服务提供商如 AWS Sagemaker 作业的更高级产品也可以被视为无服务器。它们共享核心无服务器属性(管理基础设施、按使用付费定价、自动扩展),但追求更高层次的抽象:运行时间较长的函数和更多容器化作业。

需要注意的是,术语无服务器可能会有些误导。它并不意味着系统中没有服务器。相反,这意味着工程师无法直接控制服务器,因为它们被云服务提供商隔离开来,所以他们不需要担心服务器管理。

对于无服务器推理的态度通常是严格正面或严格负面,因为其显著的优缺点。让我们突出一些优点和缺点:

  • 无需管理基础设施或为闲置资源付费**—您只需为使用的资源付费。虽然无服务器倡导者强调这一点,但现实是可能仍需要一定程度的设施管理,尽管可能处于更高层次且复杂性降低。

  • 易于扩展,特别是对于间歇性负载**—然而,它并非万能的解决方案。云服务提供商可能会对并发请求施加限制,防止无限和快速扩展。此外,“冷启动”问题,即大型模型初始化可能需要几秒钟,对于低延迟要求的应用程序来说是一个关注点。一些无服务器提供商,如 Runpod,在这方面提供了更多控制,允许您设置最小和最大工作员数量以及自定义扩展规则。冷启动被视为无服务器计算的一个重大问题,因此提供商开发了特定的解决方案来解决它,例如 AWS 的 SnapStart(mng.bz/gAWV)和 Runpod 的 Flashboot(mng.bz/eV9Q)。当目标是无服务器推理时,冷启动时间也成为选择推理引擎时的一个因素,因为我们应旨在使用更精简的工件(如 Docker 镜像)和更低的加载时间。

  • 低负载的成本效益——然而,对于中等低但一致且可预测的工作负载,专用机器可能比无服务器解决方案更经济高效。定价和延迟的组合也可能令人困惑。例如,如果模型在热状态下处理需要 100 毫秒,而在冷状态下(即休息后)需要 5,000 毫秒(即,冷请求),并且定价基于处理时间,那么冷请求的成本将是热请求的 50 倍。优化这种场景并不总是简单直接。低负载的一个特殊情况是各种测试环境:避免与更传统架构相关的成本,只为在您的预发布环境中发生的罕见测试调用付费是很不错的。

  • 本地测试更困难——正如之前提到的,尽管无服务器推理可能更便宜,但它的整体开发基础设施复杂性往往会增加。这不仅仅是“没有互联网连接我就无法测试了。”一旦无服务器推理深入到我们的系统中,它可能会带来额外的问题(例如,需要重新部署测试工件以进行微小的更改,确保测试环境有适当的权限等)。

一些无服务器提供商,如 Replicate,提供了一系列开箱即用的预训练基础模型。这在启动新项目时尤其有利,尤其是用于原型设计或研究目的。

我们观察到了在小型宠物项目和高压生产环境中无服务器推理的成功和失败案例。毫无疑问,这是一个可行的选择,但在完全接受它之前,仔细考虑和彻底的成本效益分析是至关重要的。我们遵循的规则如下:当自动扩展是一个显著优势(例如,请求数量的高变异性)且模型本身不是过大(尽管 LLMs 有时也可以在仅 CPU 的无服务器环境中部署;mng.bz/pxnz)时,考虑使用无服务器推理。另一个好主意是在你对未来负载不确定时考虑无服务器推理:它可以在项目初期适应多种负载模式,给你足够的时间在情况变得清晰后重新设计推理部分。

15.4 优化推理管道

过早的优化是万恶之源。——唐纳德·克努特

优化推理管道是一个广泛的话题,它本身并不是机器学习系统设计的一部分;然而,它仍然是机器学习系统工程的一个关键部分,值得单独成章,至少应该列出常见的方法和工具作为概览。在本质上,优化推理管道通常归结于模型的速度和准确性与所需资源容量之间的权衡,这暗示了多种优化技术,这些技术主要取决于模型的特点和架构。

一个可能在这里出现的合理问题是,“优化应该从哪个步骤开始?”我们在面试中向许多机器学习工程师提出了类似的问题,并收到了各种答案,提到了诸如模型剪枝量化(参见“用于深度神经网络加速的剪枝和量化:综述” arxiv.org/abs/2101.09671)和蒸馏(参见“知识蒸馏:综述” arxiv.org/abs/2006.05525)等术语,并引用了论文的状态(我们只提到了几篇综述,以便您可以用它们作为起点)。这些技术在机器学习研究社区中广为人知;它们是有用的并且通常适用,但它们只关注模型优化,而没有让我们控制整个局面。最实际的回答是,“我会从性能分析开始。”

15.4.1 从性能分析开始

性能分析是一个衡量系统性能和识别瓶颈的过程。与前面提到的技术相比,性能分析是一种更通用的方法,可以应用于整个系统。就像战略在战术之前一样,性能分析是一个很好的起点,它允许我们确定最有可能的优化方向,并选择最合适的技巧。

你可能会惊讶于看似最明显的因素可能并不是模型的瓶颈。以延迟为例,有些情况下它并不是瓶颈(特别是当提供的服务模型不是最新的生成式事物,而是更传统的事物时),问题可能隐藏在其他地方(例如,数据预处理或网络交互)。此外,即使模型是管道中最慢的部分,这也并不意味着它应该是优化的目标。这可能看起来有些反直觉,但我们应该寻找最可优化的部分,而不是最慢的部分。想象一下,整个运行需要 200 毫秒,其中 120 毫秒是模型所需的。但事实是,模型已经是最优化的了;它通过一个高性能的引擎在 GPU 上运行,没有太多改进的空间。另一方面,数据预处理需要 80 毫秒,这是一段任意的 Python 代码,可以通过多种方式优化。在这种情况下,最好从数据预处理开始,而不是模型。

另一个例子来自 Arseny 的经验;他曾经被要求降低一个系统的延迟。该系统是一个相对简单的流水线,依次运行了数十个简单的模型。提高时间的第一想法是将顺序运行替换为批量推理。然而,分析证明结果相反:与数据预处理花费的时间相比,推理本身(大约 5%)微不足道,批量推理不会有所帮助。真正有帮助的是优化预处理步骤,它不能从批处理中受益,并且最终与最初设计的顺序运行相结合。最终,Arseny 通过不触及核心模型推理,将系统速度提高了 40%,而像线程管理、IO 以及序列化和反序列化函数、级联缓存等元素才是真正的低垂之果。

在这里我们应该提到,由于广泛使用 GPU、异步执行、在高性能原生代码之上使用薄的 Python 包装器等原因,对 ML 系统的分析略不同于对常规软件的分析。而且,由于整个过程可能包括更多的变量,因此在解释分析结果时应谨慎,因为很容易被问题的复杂性质所迷惑。

GPU 执行可能会特别令人困惑。典型的 CPU 负载很简单:数据被加载到内存中,CPU 执行代码:完成。可能会有与 CPU 缓存、单指令多数据指令或并发执行相关的细微差别,但在大多数情况下,它是直接的。尽管 GPU 是一个独立的设备,但它通常带有内置的内存,并且在执行之前必须将数据复制到 GPU 内存中。复制过程本身可能成为瓶颈,而且并不总是明显如何衡量它。GPU 执行的高度并行性也导致了非线性效应。最简单的例子是,如果你在单个图像上运行一个模型,它可能需要 100 毫秒,但如果你在 64 个图像上运行它,处理时间只会增加到 200 毫秒。这是因为当只处理一个项目时,GPU 并没有得到充分利用,这导致了将数据复制到 GPU 内存中的显著开销。在模型架构的较低级别也是如此:减少卷积层中的滤波器数量可能不会减少延迟,因为 GPU 没有得到充分利用,并且底层使用相同的 CUDA 内核。总的来说,为 CUDA 和其他通用 GPU 框架编程是一个独立且极其深奥的领域;我们唯一想强调的是,典型程序员对什么快什么慢的直觉对于基于 GPU 的计算可能是完全无关的。

因此,适当的性能分析方法需要手头有一系列工具,从 Python 标准库中的基本分析器 cProfile 开始,到更高级的第三方工具,如 Scalene (github.com/plasma-umass/scalene)、memray (github.com/bloomberg/memray)、py-spy (github.com/benfred/py-spy),以及特定于机器学习框架的工具(如 PyTorch Profiler),最后是低级 GPU 分析器如 nvprof (mng.bz/OmqE))。最后,当与如张量处理单元或物联网处理器等异构硬件一起工作时,您可能需要使用供应商特定的工具。

在解释了性能分析结果之后,我们可以开始优化系统,解决最明显的瓶颈。典型的方法分布在不同的层级:

  • 与模型相关的优化,如架构变化、剪枝、量化、蒸馏、特征选择等。

  • 与服务器相关的优化,如批处理、缓存、预计算等。

  • 与代码相关的优化,如更有效的低级算法,使用更有效的范式(如使用向量化算法而不是 for 循环),或者用更快的库/框架(例如,numba 用于数值计算)甚至语言(例如,用 C++或 Rust 的替代方案替换 Python 的瓶颈)重写。

  • 与硬件相关的优化,如使用更强大的硬件,垂直/水平扩展(请参阅第十三章)等。

15.4.2 最佳优化是最小优化

如果我们从操作级别退一步,从整体设计角度概述优化,如果在设计阶段根据原始要求对系统进行了彻底的处理,那么在维护阶段出现的一些问题可以避免。如果我们意识到严格的延迟要求,我们应该最初选择一个足够快的模型以适应目标平台。当然,我们可以通过量化或剪枝来减少内存占用,或者通过蒸馏来减少延迟,但最好从接近目标要求的模型开始,而不是在系统准备就绪时尝试加速它。根据我们的经验,迫切需要优化是系统生命周期初期(例如,使用重模型作为基线)的糟糕选择、意外成功案例(一家初创公司快速构建了一个原型并突然需要扩展)或计划中的技术债务(“好吧,我们构建了一些现在虽然不最优但很快的东西;如果它能生存并帮助我们找到产品市场匹配,我们将清理它”)的结果。

选择优化级别是有效推理的关键决策。一个初创公司的创始工程师,需要将他们的最小可行产品扩展到第二个客户,通常只需通过简单租用额外的云计算资源进行扩展。另一方面,一个大科技公司的平台工程师可能会从阅读“现代硬件算法”(en.algorithmica.org/hpc/)并应用大规模的低级优化中受益。

15.5 设计文档:服务和推理

设计文档中专门针对推理优化的一个部分应该涵盖维护阶段预期的操作。让我们来探讨两个相当不同的机器学习系统在推理优化方面的共性和差异。

15.5.1 Supermegaretail 的服务和推理

基于以零售为中心的机器学习系统的关键特性和要求,Supermegaretail 的解决方案将不需要实时参与,允许批量修改模型;尽管如此,它仍将涉及大量工作。

设计文档:Supermegaretail

XII. 服务和推理

服务和推理的主要考虑因素是

  • 高效的批量吞吐量,因为预测将每天、每周和每月在大量数据上运行

  • 敏感库存和销售数据的安全性

  • 可扩展的、成本效益的架构,可以扩展批量作业

  • 监控数据和预测质量

i. 服务架构

我们将使用 Docker 容器在 EC2 机器上通过 AWS Batch 来处理批量需求预测任务。AWS Batch 将允许定义资源需求,动态调整所需容器数量,以及排队处理大量工作负载。

批量作业将按计划触发,以处理来自 S3 的输入数据,运行推理,并将结果输出回 S3。如果需要,一个简单的 Flask API 将允许按需批量推理请求。

所有数据传输和处理都将发生在安全的 AWS 基础设施上,与外部访问隔离。将使用适当的凭证进行身份验证和授权。

ii. 基础设施

批量服务器将使用自动扩展组来匹配工作负载需求。可以使用 Spot 实例来降低灵活批量作业的成本。

在这个阶段不需要专门的硬件或优化,因为批量吞吐量是优先考虑的,批量性质允许充分的并行化。我们将使用 AWS Batch 和 S3 提供的水平扩展选项。

iii. 监控

需要跟踪的批量作业的关键指标包括

  • 作业成功率、持续时间和失败率

  • 每个作业处理的行数

  • 服务器利用率:CPU、内存、磁盘空间

  • 与实际需求相比的预测准确性

  • 数据验证检查和警报

这种监控将有助于确保批处理过程保持高效和可扩展,并产生高质量的预测。我们可以根据生产数据在未来评估优化需求。

15.5.2 PhotoStock Inc. 的服务和推理

搜索引擎优化包括两个主要组件:

  • 实时处理用户请求,其中用户总数容易受到季节性波动(例如,昼夜差异)的影响,可能出现剧烈的峰值。

  • 用于照片搜索的索引本身,应定期更新。类似于 Supermegaretail 案例,不需要实时方法,但将需要处理大量数据。

设计文档:PhotoStock Inc.

XII. 服务和推理

由于我们的搜索引擎基于向量相似性,有两个方面我们需要关注:为可搜索项生成向量(更新索引)和搜索用户查询(查询索引)。这两个方面有不同的要求和约束,因此我们将分别设计它们。

i. 索引更新

更新索引是一个每天发生一次的批处理过程(如第十三章所述)。除了常规更新外,我们还需要支持在核心模型更新时进行初始索引创建或重新创建。尽管这是一个相对罕见的事件,但拥有一个可以按需运行的流程是很重要的。

这两种情况具有相同的特征:

  • 轻度延迟要求

  • 严格的吞吐量要求

我们需要在合理的时间内以合理的成本处理大量项目。对于粗略估计,我们应该支持在几小时内支持每天重新索引约 10e5 个项目。如果核心模型更新,我们还需要能够在合理的时间内重新索引约 10e8 个项目。

ii. 索引查询

查询索引是一个实时过程,涉及每个用户查询。我们需要最小化查询的延迟,以便我们的吞吐量要求不是太高,因为我们的平均搜索量每天约为 150,000 次(请参阅第十二章),大约每秒 2 个查询。然而,查询数量并不均匀分布,我们需要能够处理每秒约 100 个查询的峰值负载,以及在流量峰值时能够快速扩容和缩容。

我们建议使用转换为 ONNX 的相同模型,用于批量和实时推理。这不是强制要求,但它将简化系统的设计和维护。然而,推理过程对每个批次都是不同的,鉴于不同的要求,实时推理应该分开。

iii. 框架和硬件

从软件角度来看,我们将使用 Nvidia Triton 推理服务器作为服务框架。它是一个高性能的开源推理服务软件,支持 ONNX,并具有许多简化服务过程的特性。我们将使用 Triton 推理服务器的 HTTP API 从我们的应用程序与其通信。我们将使用相同的模型进行批量和实时推理,但实时推理将使用更优化的配置版本(例如,动态批次的参数max_queue_delay_microseconds应低于 10 毫秒)。

对于批量推理,我们将使用云提供商的默认解决方案:AWS Sagemaker。它是一种托管服务,允许我们在可配置的实例上运行批量推理,如果需要,易于扩展,并且与其他我们使用的 AWS 服务集成。我们可以考虑在底层使用 spot 实例以降低批量推理的成本。批量作业本身将是在实时推理之上的简单脚本,它添加了一个 IO 层,从队列中读取数据并将结果写入 S3 和数据库。

对于实时推理,如果能有一个无服务器解决方案,在无查询时可以扩展到零,那将很理想。然而,鉴于我们高负载和低延迟的要求,使用像 AWS Lambda 这样的主要提供商可能难以实现;因此,我们将采用更传统的方案,在负载均衡器后面使用服务器池。我们将使用 AWS EC2 实例作为服务器,AWS 应用程序负载均衡器作为负载均衡器。我们可以使用 spot 实例在底层降低实时推理的成本,因为每个工作器是无状态的,如果它被 AWS 终止,我们可以轻松地用一个新的替换它。我们需要确保系统有合理数量的可用工作器保证,并在需要时启用额外的扩展。

批量和实时作业的精确硬件配置是未来实验的主题;显然,我们需要使用 GPU 实例,但确切的 GPU 类型和其他资源尚不明确。鉴于预处理相对简单,我们预计不会出现大量的 CPU 使用,但我们需要确保我们有足够的 CPU 并避免它成为瓶颈;需要彻底监控资源使用(CPU/RAM/GPU)。负载均衡器下的实例数量也是未来实验的主题。

iv. 辅助基础设施

我们将使用默认的 float32 精度开始模型服务,但稍后我们将尝试使用更低的精度(例如,float16)以降低服务成本。对模型本身进行延迟优化也可以稍后进行,尽管目前我们预计不会有特定的瓶颈,因为 CLIP 模型相对简单。

由于查询比其他操作更受欢迎,我们可以使用缓存来减轻推理服务器的负载。我们可以使用 AWS Elasticache 来实现这一点;它是一个支持 Redis 和 Memcached 的托管服务。我们可以使用一个简单的键值缓存,其生存时间(确切数字取决于数据分析)。缓存对于运行时推理很有用,但不适用于批量推理;尽管如此,如果键已更改,批量推理应负责更新缓存。

我们需要确保系统可以扩展以处理峰值负载。这部分设计应与站点可靠性工程团队和 AWS 专家进一步细化。在初始阶段,我们希望确保实时推理启用了自动扩展,并且所有相关指标(例如,资源使用情况、请求数、活动实例数)都进行了监控+配置了警报。

由于服务系统没有暴露在互联网上,并且只能从我们的应用程序访问,因此不需要额外的安全措施。我们需要确保对服务系统的访问仅限于我们的应用程序,并且仅授予所需的资源访问权限(例如,我们不需要给服务系统提供对数据库的任何访问权限)。

摘要

  • 虽然将精力限制在开发和训练系统上可能很有吸引力,但推理优化同样是一个重要的步骤,它将确保你在进入运营阶段时获得稳定的性能。

  • 影响你为系统设计推理优化过程的关键因素包括延迟、吞吐量、可扩展性、目标平台、成本、可靠性、灵活性和安全性与隐私。

  • 上述因素可能存在冲突,甚至相互排斥。因此,同时优化所有这些因素是不可能的,这不可避免地会迫使你进行权衡,以尽可能最佳地微调你的模型。

  • 记得在一个框架上训练你的模型,然后再将其转换为在另一个框架上进行进一步推理。这一步尤其重要,因为一旦你需要切换到具有新特性的不同框架,它就不会影响推理流程。

  • 你的主要目标之一将是实现研究灵活性和生产环境中高性能之间的平衡。然而,请记住,这需要跨框架的介质。

  • 最重要的是,最佳优化是最小优化。如果在维护阶段,系统已经根据初始要求得到适当设计,那么可以避免一些问题的出现。

第十六章:16 所有者和维护

本章涵盖了

  • 正确系统维护的重要性

  • 责任感是构建和维护健康机器学习系统的一个关键因素

  • “巴士因素”以及团队效率与冗余之间的权衡

  • 正确安排文档的基本重要性

  • 复杂性的欺骗性吸引力

有能力的程序员完全清楚自己头脑的局限性。因此,他以完全谦卑的态度来处理任务,并像躲避瘟疫一样避免使用巧妙的技巧。——埃德加·迪杰斯特拉

在过去的 15 章中,我们一直试图将这本书组织成一个扩展的、深入的清单,你可以在设计你的机器学习(ML)系统过程中的任何阶段随时参考。但这些看似明显的建议比你想象的更难遵循。

从零开始构建 ML 系统,尤其是运营现有解决方案,是一个如此复杂和苛刻的过程,我们肯定会犯错、跌倒、遇到一些障碍,并在为业务创造价值的过程中妥协一些原则。作为不完美的人类,我们也意识到有时我们可能允许自己忽略一些自己的建议。

事实上,有时你可能看到或最终得到一个不遵循我们在书中倡导的原则的系统,其原因可能以无数种方式变化:一些细节可能被遗漏,公司可能已经改变了优先级和约束条件,系统可能确实需要更新,因为其原始假设不再有效,等等。

通常,这甚至更加平凡,因为最初设计和构建系统的人往往会更换工作。更甚者,他们可能在完成系统建设之前离开公司;这是事物的本质。考虑到这一点,所有者和维护不是我们以后需要考虑的事情,而是整个系统的基石,需要从一开始就深入人心。然而,在这个过程中,有可能和风险是过于沉迷于自己的创造,这是应该避免的。系统不应被视为作者的一部分,你应该从你的设计和自我中解脱出来,以提供最佳可能的解决方案。

用三个要点来概括,在任何时刻都至关重要:

  • 每个参与项目的人背后的责任区域是什么?我们在责任/所有权方面是否有任何缺口?

  • 有多少人具有特定的知识?我们是否有足够的冗余?

  • 可用文档在哪里,系统的哪些组件被记录在案?负责团队之外的人能否运行该系统?

在一定程度上,设计文档有助于回答(尽管只是部分且不总是直接)这些问题。这意味着只有一个结论:我们必须关注这些方面,以避免未来出现任何令人遗憾的后果。毕竟,最重要的是构建和维护一个能满足你的需求并带来价值的产品。

在本章中,我们涵盖了使系统持久和稳健应对人为变化的必要基础知识。

16.1 责任制

在本书的前几章中,我们提到了在信息收集过程中涉及各种利益相关者的必要性,他们将为机器学习系统开发的准备阶段提供关键输入。

随着项目的增长,不断吸收新的输入,并从简单的常量基线演变为一系列紧密相连的复杂模型,它将逐渐被新的参与者加入。其中一些人可能只会在短时间内加入你,一旦他们的参与不再必要(例如,数据标注者在数据集处理阶段至关重要,但在部署时就会减少),一些人将变成永久贡献者直到系统发布,而少数人最终将成为所谓的“核心团队”(即项目中最亲近的人)。这些同事(或如果是外部供应商的代表,则指他们)将成为对最终结果和系统稳定性负责的人。而你应当定期与他们保持联系,或者至少要认识他们。

了解责任区域和分配给这些角色的个人对于任何项目或系统的成功启动和未来发展至关重要。这可能听起来很荒谬,但对于实际上负责特定组件的团队成员来说,了解这一点甚至更重要。你会惊讶地发现,有多少次 A 认为 B 对项目的某个部分负责,而 B 反过来又确信情况相反。这种情况的经典例子可以在图 16.1 中看到。

figure

图 16.1 明确与隐含方法在项目中对人的参与

明确地重复说明比隐含地希望每个人都处于同一页面上更有价值,根据我们的经验,这种方法一旦项目中有三个人参与,就更适用,更不用说更多的团队成员了。关于这个主题,我们最喜欢的一句话来自诺贝尔物理学奖获得者理查德·费曼。在他的书《当然你在开玩笑,费曼先生!》中,他写道

在普林斯顿这个项目中,我第一次遇到的有趣经历之一是遇见了许多杰出人物。我以前从未遇见过这么多杰出人物。但有一个评估委员会必须试图帮助我们,并最终决定我们将如何分离铀。这个委员会上有康普顿、托尔曼、斯密思、尤里、拉比和奥本海默这样的人。我会坐下来,因为我理解我们分离同位素的过程的理论,所以他们就会问我问题并讨论它。在这些讨论中,一个人会提出一个观点。然后,比如康普顿会解释一个不同的观点。他会说应该是这样,而且他完全正确。另一个人会说,嗯,也许,但是还有其他可能性我们必须考虑。

所以每个人都不同意,围坐在桌旁。我很惊讶和不安,康普顿没有重复并强调他的观点。最后,在结束时,作为主席的托尔曼会说:“好吧,听了所有这些论点,我想康普顿的论点是所有中最出色的,现在我们必须继续前进。”

看到一群人能够提出很多想法,每个人都想到了一个新的方面,同时记住其他人说过的话,最终决定哪个想法最好——总结所有这些——而不必说三遍,这对我来说是个很大的冲击。这些人确实非常伟大。

如果你没有一帮天才在处理一个国家优先级项目,你最好不要依赖他们的内部理解,说(或写!)三遍,与每个人分享,并确保他们不容易错过。像会议记录、跟进、更新和同步这类事情本质上都是为了这个目的而做的。

然而,我们看到了很多人滥用这些原则,在无意义的同步上花费太多,将会议的概念变成了整个行业的噩梦。那些仪式、遗物或无论你叫它们什么,都不需要花费太多时间,也不必仅仅因为它们写在了教科书中就去做,而是为了实现保持专注的具体目标。不使用它们,以及过度使用它们,都可能严重影响长期的表现和可持续性。我们坚信文档被严重低估了,其重要性被严重低估。

应对责任区域应该被记录下来,以确保团队对它们有明确和不含糊的理解。可能有各种正式程度——从创业友好的“爱丽丝完全负责让系统 X 工作”到复杂的跨页层次结构。一种相当平衡的方法是使用简单而强大的 RACI 矩阵,将涉及的人员分为四个组:

  • 负责——实际执行任务的个人(们)。常见人选:机器学习工程师

  • 责任人——*最终对任务完成负责的人,通常是经理或团队领导。建议每个组件的责任人应也有权批准或拒绝工作。常见角色:项目经理或高级工程师。

  • 咨询者——*寻求其意见的人,通常是领域专家或对任务有特定兴趣的利益相关者。常见角色:领域专家或法律代表。

  • 知情者——*需要了解进度或决策但并不直接贡献整体输入的人。常见角色:前端工程师。

无论好坏,总会有责任上的空缺;因此,描述一种明确的升级方式是有意义的——如果情况不符合矩阵或其他与责任相关的工具,可以采取什么措施?

关于责任的一个最终且可能不太受欢迎的部分是应急班次。处于应急状态意味着在班次期间,有特定的人负责响应任何关键事件。他们必须准备好在紧急情况下迅速反应,通常在 30 到 60 分钟内。轮换部分意味着这项职责通常由团队成员共享,并定期变更。例如,某个人可能一周处于应急状态,然后在下周将职责转交给下一个人(见图 16.2)。

图

图 16.2 应急班次是防止团队过劳的必要措施。

应急班次通常在公司成长过程中出现。那些在职业生涯早期或在较小的公司有经验的人可能还记得,在那个环境中不需要轮换,因为每个人都是应急状态,事件由当时可用的任何人处理。然而,在某个时候,这种方法变得难以管理,当团队成员之间的责任模糊时,会导致事件由每个人同时管理,又没有人管理。一旦你开始注意到这种行为,就是时候正式化应急班次了。后来,可能需要有一个专门的应急班次团队,将轮换结构化为 L1/L2/L3 等级等。

即使你的值班时间表正在运行,也可能有其他原因导致整个团队超负荷工作。Arseny 曾在一家处于早期发展阶段的公司工作,他面临了一些系统事件,由于他根本不知道哪些系统受到影响,这些问题根本无法解决。这是一次令人沮丧的经历,直到那些构建和维护这些系统的工程师最终编写了包含典型问题建议的稳固的食谱。一些技术债务得到了清理,值班班次从无尽的噩梦转变为常规任务——虽然不是最舒适的,但至少是可以忍受的。当 L1 事件由一个专门的团队负责时,机器学习团队值班的时间进一步减少。

显然,周六凌晨 3 点被叫醒并不是理想的情况,这可能会产生额外的动力去构建一个健壮且监控良好的系统,这有助于最小化问题,并以人们事先知道的方式制定值班“名单”和班次时间。任何看不到适当日志和可观察性价值的工程师都应该被安排足够长时间的值班,以改变他们的想法。

我们相信,负责系统设计和实施的人也应对系统的维护和支持负责。他们比任何人都更了解细节,并能预测边缘情况并提出解决问题的捷径。这并不意味着他们必须是唯一被召唤的人,但他们应该为值班轮换做好准备,并为值班团队提供必要的工具和文档。

常用的工具包括

  • 日志、指标、警报和仪表板

  • 系统和相关基础设施配置

  • 系统源代码和文档

访问生产数据有些复杂。修复问题并不总是需要访问生产数据;我们见证过隐私政策限制访问的情况。然而,通常来说,访问生产数据以调查问题和找到根本原因是有益的。否则,日志和指标应该足够详细,以便帮助解决这个问题。

总应该有一个运行手册,其中包含如何修复最常见问题的注释以及如果需要升级问题的人员名单(例如,供应商方面可能发生故障,值班工程师需要联系可以与供应商沟通的 CTO)。许多问题都有一定的节奏;它们是技术债务或使用模式的反映。例如,想象有一个大客户具有尖峰使用模式,这导致系统在负载下挣扎。对于这种情况,值班工程师可能需要一份关于如何启动系统额外实例以及如何在负载恢复正常时缩小规模的食谱。

出现的生产问题会引发两个衍生品:解决和学习。为了避免反复面对同样的错误,请确保建立一个从失败中学习的流程。这可以通过回顾和事后分析来安排。我们建议遵循比例响应原则——一套针对事件后采取的行动,其规模应与失败影响相似,并考虑到未来可能发生类似失败的机会。有些事件只需 10 分钟的讨论,并在操作手册中添加一段内容。Arseny 曾经触发了一次大规模的故障,以至于 CTO 不得不启动一个名为“可靠发布竞赛”的倡议,涉及公司中的每个工程团队,并提高了所有系统的可靠性,而不仅仅是 Arseny 成功破坏的机器学习系统。

16.2 公交车因素

公交车因素是一个衡量项目因单个团队成员的丢失而中断风险的指标。术语公交车因素来源于一个假设场景,即团队成员被公交车撞到,这会突然且意外地使他们从项目中消失。

CAP 定理指出,任何分布式数据存储只能提供以下三个特性中的两个:

  • 一致性——每个读取请求都收到最新的响应或错误。

  • 可用性——每个请求都收到有效的响应,但没有任何保证它包含最新的数据。

  • 分区容错——即使节点间的通信被中断或丢失,系统仍然继续运行。

当发生网络分区故障时,必须在取消操作以增强一致性但牺牲可用性或继续操作以保持可用性但可能损害一致性之间做出决定。

我们认为团队结构非常接近,但有不同的标准:效率和冗余。正如计算机科学所描述的,冗余意味着有额外的或重复的资源可用以支持主系统。这是一个备份或备用系统,如果主系统失败,它可以介入。备用资源是冗余的,因为如果一切正常,它们就不会被使用。

注意工程师往往倾向于用技术解决方案来解决组织问题(换句话说,把人当作软件来对待),仅仅因为他们在这方面很擅长。有时这已经足够好了,因为构建软件可能确实比与人相处和对齐以实现具有挑战性的目标要简单。然而,高级工程师不应该像看到到处都是钉子的锤子一样——他们应该为问题选择合适的工具,有时甚至不是技术工具。

这种冗余/效率问题如何影响团队结构?从我们所见到的来看,公司/团队/项目方案通常是从高效发展到冗余。然而,过于高效或过于冗余都不利。

16.2.1 为什么过于高效并不好?

如果你过于高效,并且只有少数人覆盖多个因素,你就有风险。团队中的每个人都是不可替代的,这不仅是因为他们的经验,而且不幸的是,也因为他们以全负荷工作。如果至少有一个团队成员发生任何问题,公司(或项目)就会陷入困境。唯一能够处理不降低整体效率出现的任何问题的方法是通过英雄行为,这些行为在任何方式上都是不可扩展的,因此被视为反模式(mng.bz/YVyA;参见图 16.3)。

figure

图 16.3 极端效率的缺点是对外部因素极端脆弱。

16.2.2 为什么过度冗余并不有益?

与效率相比,冗余的主要优势是额外的容量,这提供了可靠性、安全性、改进的空间以及度过危机的余地。然而,过多的冗余会导致粗心大意,减少团队成员之间的信任,并排斥顶尖人才,因为它损害了整体的工作氛围和完成有意义且影响深远工作的感觉。

这里的两个例子是明显的边缘情况,应该避免。一旦你觉得团队接近其容量极限,考虑扩展是值得的,尽管目前成本会增加,但这将是长期正确的解决方案。

同样的规则也可以反过来应用:一方面,有足够的容量避免过度劳累,另一方面,在高峰负载时间准备好应对潜在的危机。

16.2.3 何时以及如何使用公交车因素

我们需要在效率和冗余之间保持适当的平衡,控制某物的第一步是能够衡量;一个非常简单且广为人知的指标就是公交车因素。

公交车因素是通过计算在项目无法继续之前需要失去多少团队成员来计算的。例如,如果一个项目的公交车因素为 1,即使失去一个(特定的)团队成员,项目也会陷入严重困境。

显然,1 的公交车因素(bus factor)低于理想水平,而 10000 的公交车因素可能过多(尽管这可能发生在你需要许多具有相对相似范围的人的情况下,例如客户支持);最终数字取决于许多变量:项目重要性、预算限制、截止日期、预期周转率和项目所有者的焦虑/信心。但一旦你有一份负责系统/项目不同部分的负责人的名单,你就拥有了计算公交车因素所需的一切,这包括负责区域和负责人员。接下来你需要做的是找出有多少其他人足够了解这个领域。一般来说,负责该领域的人知道所有可以(在一定程度上)替代他们的人。考虑到这一点,你可以计算公交车因素,评估潜在风险,并做出必要的招聘、调动或协作活动决策,以传播知识并解决项目可能存在的脆弱性。

当然,我们不能将人们以离散的方式标记为“他们知道系统或其组件是如何工作的”与“他们不知道”。通常,系统内部存在知识的不同层次,即使不可能让许多工程师了解所有事情,也有可能部分地传播这种知识。这通常通过设计/代码/文档审查、结对编程会议和开放时间来实现,关键成员在此分享他们的专业知识。

这种推理描述了两个标准之间的平衡。然而,真正的决策可能需要考虑更多的标准(例如,不仅仅是团队规模,还包括其高级平衡和特定技术的接触)。

对于那些对如何看到成本、系统性能和人力容量之间的权衡感兴趣的人来说,我们建议熟悉 Rasmussen 模型,该模型在本文中得到了很好的解释:mng.bz/GNqO。与之前专注于团队结构的文本不同,这篇文章反映了 SRE 对系统权衡的看法:尽管人和硬件非常不同,但在寻找最佳权衡方面存在相似的模式。

16.3 文档

我们已经提到了文档的重要性。不幸的是,文档通常被高度低估,并且并不总是得到应有的关注和照顾。讽刺的是,“文档”这个词经常与“稍后”、“现在不”、“不紧急”等词语并列。而且当需要文档时,通常为时已晚,或者过于陈旧。

文档的重要性不容小觑。它作为知识转移和入职新团队成员的手段,有助于为新员工平滑学习曲线,并确保系统的长期运行。全面的文档允许负责团队之外的人有效地理解和运行系统。

瓦列里营火故事

我在不同的公司工作过:线下和线上食品零售、电子商务平台、社交网络公司以及金融科技公司。我非常自豪的一点是——无论公司规模、市场和发展成熟度如何——我所建立的系统和组织都能够经受住我的离职并继续成功运营。这其中的基石包括为新员工提供平滑的学习曲线、广泛的文档以及与其他部门的彻底整合,以及让每个人都知道谁负责特定的部分。

有一回,我收到了一位来自大型科技公司的前同事的消息,询问如何重现一个特定的系统组件(一个能够为销售商品生成合成评论的语言模型)。我的回答简短——那个项目的文档可以在特定的标签下找到,而且确实找到了!(坦白说,他甚至不需要我帮忙找到。)而且不仅找到了,你还可以使用它通过单个命令重现整个部署过程,或者如果需要的话,检查代码库和数据集。

这里有一个非常不同的例子。有一回我在一家公司工作,我们必须重新创建比特币区块链的链上分析,这最初是在 2 年前完成的,但一些来源已经改变,需要替换/更新。不幸的是,几乎没有文档,原本最多只需要两周就能完成的工作几乎消耗了 3 个月的时间(链上分析并不简单;例如,你可能会发送 0.1BTC,但它看起来像你发送了 1BTC 并收到了 0.9BTC,这与实际情况非常不同)。在这个项目完成后不久,那里的大部分团队成员都被裁员,公司员工总数的 30%也被裁掉。这是 6 个月内的第二次裁员。合计,他们几乎将公司的员工人数减少了 60%。很难说主要原因是否是缺乏文档,但我确信反复重新发明轮子(在这个案例中,重新走同样的步骤)并不是最有效率的资源利用方式。这就是为什么缓存在计算机科学中如此普遍。不幸的是,裁员发生在项目完成后,但文档还没有完成。

文档还有助于在团队成员离职(人员冗余)或新成员加入时实现更平滑的过渡。此外,文档促进了不同部门和利益相关者之间的协作,促进了系统(责任)的共享理解,并使每个工程师更加独立,从而更加主动和高效。

虽然一开始可能需要额外的时间和精力,但文档最终在长期中节省了资源。它减少了部落知识的依赖,防止了工作的重复,并最小化了由于信息不足而导致的高成本错误和延迟的风险。记录流程、程序、配置和最佳实践使团队能够更有效地工作,并为持续改进提供了基础。它应该从一开始就优先考虑,而不是被视为事后之想。通过承认文档的重要性并投入必要的资源,团队能够构建能够适应变化并随着时间的推移持续提供价值的弹性系统。

在一定程度上,我们整本书都是致力于探讨为什么以及如何编写特定类型的文档,这些文档能够捕捉到关于系统设计、架构、实现细节和操作流程的重要信息。但设计文档是非常具体的一份文档,其目标不是成为涵盖系统所有方面的全面文本,而是一个概述,有助于同步系统构建者和利益相关者的团队。话虽如此,总还有空间为其他文档留出位置,包括

  • 用户手册和指南 解释如何使用系统,并提供常见任务(如数据准备、模型训练和预测)的逐步说明

  • 操作手册 提供使用系统达成目标的方法,并提供代码片段、数据示例和达到目标结果的特定步骤

  • 低级技术规范API 文档,为开发者描述机器学习系统的底层技术细节,并指导如何使用函数和 API 在编程层面上与系统交互

  • 安装和配置指南,提供如何设置和微调机器学习环境、解释依赖关系、安装步骤以及配置软件的选项

  • 常见问题解答和故障排除指南,针对使用机器学习工具时遇到的一些常见问题,提出解决方案或替代方案,以解决您在训练、测试或部署过程中可能遇到的问题

  • 版本发布说明 详细说明新软件版本中应用的变化;解释新功能、错误修复、性能改进或已知问题;并通知用户关于新版本及其对整体工作流程的影响

  • 以往事件调查报告 分析并记录过去问题或失败的根本原因,分享经验教训和避免类似事件采取的措施,以提高系统可靠性和用户信心

从一开始就投入时间和精力来创建和维护文档,有助于团队减轻与知识差距和依赖特定个人相关的风险。在本书的编写过程中,您已经看到了两份设计文档,我们希望这些文档能为您提供关于所描述系统的良好概述。

16.4 复杂度

尽可能使一切尽可能简单,但不能更简单。——据说是爱因斯坦所说

根据热力学第二定律,一个孤立系统在自发演化过程中,其熵随时间不能减少。虽然最初“熵”是一个用于物理学的术语,但它后来被信息理论所采用,几乎具有相同的意义:熵量化了不确定性或随机性的数量。

比喻来说,软件和机器学习系统遵循相同的法则:随着时间的推移,熵只能增加。在某个时刻,它变得难以处理,这就是为什么旧系统通常被分解成一系列较小的系统(记住第 13.1 节中的软件工程基本定理),以便由一群相当聪明的人维护,而不是一代代的天才。添加一个新的抽象层次“隐藏”了熵,但并没有真正在整个堆栈中降低其水平。

有许多集体意识碎片表达类似的想法:极限编程文化中的 YAGNI(“你不需要它”)方法,20 世纪中叶起源于美国海军的 KISS(“保持简单,傻瓜”)原则,甚至还有从中世纪时代开始的奥卡姆剃刀。所有这些想法都追求同一个目标:限制复杂性。

同时,软件工程文化倾向于光谱的另一端,正如 DRY(“不要重复自己”)这样的缩写谚语所表达的那样。乍一看,这些是具有争议性的:遵循 DRY 意味着引入更多的抽象层,而这正是复杂性的一个(或者更确切地说,是唯一的一个)来源。

想象编写一个简单的代码片段,计算多个数据集上的一个众所周知的指标。一个典型的代码集可能包含大约三个函数:一个读取数据,下一个计算指标,最后一个协调执行,在循环中运行这两个函数并保存结果。

现在,让我们想象光谱的两端:一方面是过于简单的代码,另一方面是过于复杂的代码,它们执行相同的事情。前者可能是由一个对循环和函数知之甚少的人编写的;他们只是按照指示编写:读取文件 A,计算 A 的指标,保存 A 的指标,读取文件 B,等等。过于复杂的代码将包含一些元编程和其他复杂模式。虽然它们在本质上完全不同,但两者都会使代码的可读性变差。

这是一个在几乎每本教授如何编写干净代码的软件工程教科书中都能找到的简单示例。但同样的原则也适用于更大的规模。想象一下解决以下问题:你的公司想要帮助客户支持团队优先处理最紧急的案件,以便首先帮助最不满意的客户。

目前解决这个问题的典型方案是使用一个定制的提示来对消息进行分类,将其归入几个紧急程度不同的类别。在大语言模型革命之前,比例基线是创建一个简单的模型(例如在词袋模型上进行的逻辑回归)来进行分类,以便覆盖大多数紧急消息。这还需要一些工程努力(例如,构建数据管道以获取训练集,并使用客户支持团队进行标记,将模型与客户服务软件连接起来以传播这些标签并在 UI 中可视化它们等)。

一种不合理简单化的偏差是构建一个包含多个条件和正则表达式的基线。这个解决方案肯定会在一段时间内有效,但相对很快,你的同事会像支付了两次产品的客户一样不高兴,因为一个错误请求公司回滚交易,并且由于某些不清楚的原因,你的模型将票据分类为“低优先级”。

一种过度复杂的解决方案可以包括许多由同一个词“不相关”连接的组件。你可以引入多个从头开始训练的复杂自然语言理解模型,用一些研究使它们在少量样本的情况下工作,并用校准层进行润色。复杂性可能不仅仅是机器学习特有的,也可能偏向于工程:而不是拥有几个简单的 API,工程师可以使用他们知道的全部流行词汇来使系统具有与太空船固件一样容错性和与谷歌搜索引擎一样可扩展性,而忽略了它每天只需处理几十条消息的事实。

所有类型的系统——包括非软件系统——都受到复杂性的困扰。软件复杂性的额外层次在于其不断的演变。当你建造房子时,你不会在屋顶完工后一个月就在上面添加额外的房间。然而,向软件系统中添加新功能却是一种更为常见的做法。数据导向系统的复杂性增加是因为与数据漂移相关的持续问题。机器学习系统甚至更加复杂——一些模型设计上就是非线性黑盒,这并没有使事情变得更容易。

在书的最后,我们再次提到第二章和第三章,涵盖问题理解和初步研究。一些复杂性是不可避免的,但大量不必要的复杂性是由这些阶段的失误造成的:对问题理解不足、表面化研究、设定不相关的目标,以及从一开始就没有涵盖明显的风险。这些都可能导致一系列糟糕的决定和复杂性的增加。

当你需要横渡河流时,一艘可靠的划船可能就足够了。你甚至可以给它装上马达来加速你的渡河之旅,但无法将这艘船进化成能够运送数千人横渡大洋的船只(见图 16.4)。同时,那些在旅行距离不超过几英里时就着手组装邮轮的人永远无法达到目标。许多机器学习系统因为与这个比喻相关的原因而注定失败:他们的设计者要么试图将一个原始系统通过添加各种功能来满足相当具有挑战性的目标,要么在没有良好推理的情况下开始建造宇宙飞船,甚至在建设的第一阶段准备就绪之前就被解雇了。

图片

图 16.4 正如一个基本模型最终会表现不佳并无法满足项目目标一样,过度设计的解决方案会消耗企业宝贵的资源。
来自阿列克谢的篝火故事

我未能按时完成这一章的定稿,因为我忙于我的主要工作,并且因为我过去自己做出的聪明决定。三年前,我需要测试一个应该在两个环境中运行模型的部分系统:本地和基于云的环境。测试很简单:在两个空间中运行相同的内容,并断言结果相等。但我追求节省一些毫秒,因此让两个调用在单独的线程中并发运行。这很聪明,也很有效!几年过去了,代码库发生了演变,推出了新功能,基础设施发生了很大变化,其中这些基础设施变化之一改变了云中事物应有的工作方式。所以你可以预期这个测试开始失败。然而,Python 线程中的异常不是隐式的,所以在这种情况下测试失败只会导致一个几乎无关紧要的消息。更糟糕的是,这些测试也是并发执行的,这进一步隐藏了真正的问题。一个不必要的复杂性让我花费整整一天的时间来揭露这个问题。

这个故事省略了一些工程细节,比如非典型性的持续集成配置,但即使简化后,它也表明这种浪费时间的根本原因不是单一的——它更像是千刀万剐的死亡。正如流行的列表“复杂系统如何失败”(how.complexsystems.fail/)所说:“事故后归因于‘根本原因’是根本错误的。”并不是一个糟糕的决定,而是一系列次优选择的结果。组合数学对你不利:即使与系统相关的决策大多数是最佳的,一个复杂系统中的巨大组合数量会导致越来越多的意外事件。你能做的只是减少这些数量,以及每个小故障的影响。

16.5 维护与所有权:超级大零售和 PhotoStock Inc.

在整本书中,我们每章都以一个案例研究结束,展示了 Supermegaretail 和 PhotoStock Inc.的相关设计文档部分。在这个案例中,这样做意义不大,因为这需要虚构几十个假的前后名字。我们只限于对如何在两个例子中进行维护的一般性考虑。

由于 Supermegaretail 是一家拥有数千名员工的大型企业,官僚化程度高,工作流程复杂,而其关键业务部门主要是非技术性的,因此,在项目成功中扮演着重要角色的是与每个层级利益相关者的所有阶段的成功协调。因此,系统有效发布后的基础将是一个详细的 RACI 矩阵,包括参与项目的公司员工,以及交付团队、执行团队和业务团队的签字清单。

另一方面,PhotoStock Inc.是一家相当小、完全基于技术的公司,与数百万图像一起工作的定制搜索引擎的特定性意味着系统上持续有负载,并且有定期的峰值负载。搜索引擎是一个既依赖机器学习又依赖基础设施的项目。正如之前所强调的,它需要广泛的跨团队合作。鉴于有四个核心组件(API 处理器、索引、排名模型、内部工具),我们希望确保

  • 每个组件都有一个直接负责的高级工程师。

  • 这些工程师对其他组件有相当的了解。

考虑到这一点,两级值班轮换将是最佳解决方案:

  • 主要的值班工程师是第一个响应任何事件的。

  • 主要的值班工程师可以将特定问题升级到组件值班工程师。

正如我们在第四章所说的,设计文档是一个活生生的东西,维护部分是最后添加和填写的,这不仅是因为在这个阶段,你对你的机器学习系统有一个完整的愿景,而且还因为到这个时候,你已经设法与其他团队和单位的代表建立了联系,并了解哪些个性将包括在你的项目工作组中。

摘要

  • 负责系统设计和实施的人也负责系统的维护和支持。

  • 一定要了解责任区域和分配给这些角色的人员。更重要的是,明确提醒这些人他们的角色,因为这比隐含地希望每个人都处于同一页面上更有价值。

  • 为了跟随你系统的不断扩展,记得提供值班轮换以应对任何关键事件。

  • 总是寻找团队效率和冗余之间的权衡,以避免过度劳累,同时保持足够的容量来应对高峰时段。

  • 保持你系统中的每一份文档都更新并可供任何相关人员获取。虽然这可能需要前期额外的时力和精力,但文档最终会在长期中节省资源。

  • 从小处着手,改进一两件事,建立信任,然后继续进行更有效果的改变。在这样做的时候,你很可能从同事那里获得更多支持,并收集更多关于系统的隐性知识,这将帮助你做出更好的决策。

  • 尽量避免使你的解决方案过于复杂。过度的复杂性可能是个人雄心壮志的结果,也可能表明对问题理解不足、表面化研究、设定不相关目标,以及从一开始就没有覆盖明显的风险。

posted @ 2025-11-21 09:09  绝不原创的飞龙  阅读(35)  评论(0)    收藏  举报