机器学习驱动应用构建指南-全-
机器学习驱动应用构建指南(全)
原文:
zh.annas-archive.org/md5/eae813ffb6b86233a9c5d886a11af3a6译者:飞龙
前言
使用机器学习驱动应用的目标
在过去的十年中,机器学习(ML)越来越多地被用于驱动各种产品,如自动支持系统、翻译服务、推荐引擎、欺诈检测模型等等。
令人惊讶的是,目前几乎没有资源可以教授工程师和科学家如何构建这样的产品。许多书籍和课程会教如何训练 ML 模型或如何构建软件项目,但很少有结合两者来教如何构建由 ML 驱动的实用应用。
将 ML 部署为应用的一部分需要创造力、强大的工程实践和分析思维的结合。ML 产品建设的挑战性在于,它们需要的远不止简单地在数据集上训练模型。为特定特征选择正确的 ML 方法,分析模型错误和数据质量问题,并验证模型结果以保证产品质量,这些都是 ML 建设过程的核心挑战。
本书将详细介绍此过程的每一个步骤,并旨在通过分享方法、代码示例以及我和其他经验丰富的从业者的建议来帮助您完成每一个步骤。我们将覆盖设计、构建和部署 ML 驱动应用所需的实际技能。本书的目标是帮助您在 ML 过程的每个环节都取得成功。
利用 ML 构建实用应用
如果您经常阅读 ML 论文和企业工程博客,可能会被线性代数方程式和工程术语的组合所压倒。该领域的混合性质使许多工程师和科学家感到对 ML 领域感到畏惧,他们本可以贡献他们的多样专业知识。同样,企业家和产品领导者经常很难将他们的业务理念与 ML 今天(以及明天可能)所可能实现的联系起来。
本书涵盖了我在多家公司数据团队工作和帮助数百名数据科学家、软件工程师和产品经理通过我在洞察数据科学领导人工智能项目工作中积累的经验,构建应用 ML 项目的经验。
本书的目标是分享构建 ML 驱动应用的逐步实用指南。它是实用的,并专注于具体的提示和方法,帮助您原型设计、迭代和部署模型。因涵盖多个主题,我们将在每一步中仅提供所需的详细信息。在可能的情况下,我会提供资源,以帮助您深入研究所涵盖的主题。
重要的概念通过实际示例进行了说明,包括一个案例研究,该案例将在本书结束时从构想到部署模型。大多数示例都将附带插图,并且许多示例将包含代码。本书中使用的所有代码均可在书籍的配套 GitHub 仓库中找到。
由于本书侧重描述机器学习的过程,每一章都建立在前面定义的概念基础之上。因此,我建议按顺序阅读,以便了解每个后续步骤如何融入整个过程中。如果你希望探索机器学习过程的子集,你可能更适合选择更专业的书籍。如果是这种情况,我会分享几个推荐。
附加资源
-
如果你想深入了解机器学习,甚至能够从零开始编写自己的算法,我推荐阅读《从零开始的数据科学》,作者是 Joel Grus。如果你对深度学习理论感兴趣,那么《深度学习》(MIT Press),作者是 Ian Goodfellow、Yoshua Bengio 和 Aaron Courville,是一本全面的资源。
-
如果你想学习如何构建能够处理大量数据的可扩展应用程序,我建议阅读《设计数据密集型应用》(O’Reilly),作者是 Martin Kleppmann。
如果你具有编码经验和一些基本的机器学习知识,并且希望构建以机器学习驱动的产品,这本书将引导你完成从产品理念到交付原型的整个过程。如果你已经作为数据科学家或机器学习工程师工作,这本书将为你的机器学习开发工具添加新的技术。如果你不懂如何编码,但与数据科学家合作,这本书可以帮助你理解机器学习的过程,只要你愿意跳过一些深入的代码示例。
让我们从更深入地探讨实用机器学习的含义开始。
实用机器学习
对于本次介绍来说,可以将机器学习视为利用数据中的模式自动调整算法的过程。这是一个通用的定义,因此你不会感到意外,许多应用程序、工具和服务开始将机器学习集成到它们的核心功能中。
其中一些任务是面向用户的,例如搜索引擎、社交*台上的推荐、翻译服务,或者自动检测照片中熟悉面孔、遵循语音命令并试图为电子邮件中的句子提供有用建议的系统。
一些以不那么显眼的方式工作,悄悄地过滤垃圾邮件和欺诈账户,提供广告服务,预测未来的使用模式以有效地分配资源,或者尝试个性化网站体验以适应每个用户。
目前许多产品正在利用 ML,而更多产品可能也会这样做。实际的 ML 指的是识别可能受益于 ML 的实际问题,并为这些问题提供成功的解决方案。从高层次的产品目标到 ML 驱动的结果,是一个具有挑战性的任务,本书试图帮助您完成。
一些 ML 课程将通过提供数据集并要求学生在其中训练模型来教授学生 ML 方法,但在数据集上训练算法只是 ML 过程的一小部分。引人注目的 ML 驱动产品依赖于不仅仅是一个累积准确度分数,并且是一个漫长过程的结果。本书将从构思开始,一直到生产,以一个示例应用程序的每一步来说明。我们将分享从与部署这些类型系统的应用团队合作中学到的工具、最佳实践和常见问题。
本书涵盖的内容
为了涵盖构建由 ML 驱动的应用程序的主题,本书的重点是具体和实际的。特别是,本书旨在说明构建 ML 驱动应用程序的整个过程。
因此,我将首先描述解决每个步骤的方法。然后,我将使用一个实例项目作为案例研究来说明这些方法。本书还包含许多在工业界应用 ML 的实际例子,并采访了那些构建和维护生产 ML 模型的专业人士。
整个 ML 的过程
要成功地为用户提供一个 ML 产品,你需要做的不仅仅是简单地训练一个模型。你需要深思熟虑地转化你的产品需求为一个 ML 问题,收集充足的数据,在不同模型之间高效迭代,验证你的结果,并以稳健的方式部署它们。
构建模型通常只占据 ML 项目总工作量的十分之一。精通整个 ML 流程对于成功构建项目,成功完成 ML 面试并成为 ML 团队的重要贡献者至关重要。
一个技术实际的案例研究
虽然我们不会从头开始用 C 重新实现算法,但我们将通过使用提供更高级抽象的库和工具来保持实际和技术。我们将在本书中一起构建一个示例 ML 应用程序,从最初的想法到部署产品。
当适用时,我将使用代码片段来说明关键概念,并描述我们的应用程序的图像。学习 ML 的最佳方式是通过实践,因此我鼓励您逐步学习本书中的示例,并根据自己的需求调整它们以构建您自己的 ML 驱动应用程序。
真实的商业应用
在整本书中,我将包括来自在 StitchFix、Jawbone 和 FigureEight 等科技公司的数据团队工作过的机器学习领袖的对话和建议。这些讨论将涵盖在与数百万用户建立机器学习应用后积累的实用建议,并纠正一些关于如何使数据科学家和数据科学团队成功的流行误解。
先决条件
本书假设读者对编程有一定了解。我将主要使用 Python 进行技术示例,并假设读者熟悉其语法。如果你想要恢复你的 Python 知识,我推荐Python 之旅(O'Reilly 出版),作者是 Kenneth Reitz 和 Tanya Schlusser。
此外,虽然我会定义书中引用的大多数机器学习概念,但我不会涵盖所有使用的机器学习算法的内部工作原理。大多数这些算法都是标准的机器学习方法,这些方法在入门级机器学习资源中有所涵盖,比如在“额外资源”中提到的资源。
我们的案例研究:基于机器学习的写作辅助
为了具体说明这个想法,当我们阅读本书时,我们将一起构建一个机器学习应用程序。
作为一个案例研究,我选择了一个能够准确展示迭代和部署机器学习模型复杂性的应用。我还想要涵盖一个能够产生价值的产品。这就是为什么我们将实施一个基于机器学习的写作助手。
我们的目标是构建一个系统,帮助用户写得更好。特别是,我们将旨在帮助人们写出更好的问题。这可能看起来是一个非常模糊的目标,随着我们界定项目范围,我将更清晰地定义它,但这是一个很好的示例,因为它有几个关键原因。
文本数据无处不在
对于大多数你能想到的用例,文本数据都是充足可用的,也是许多实际机器学习应用的核心。无论我们是试图更好地理解产品评论,准确分类传入的支持请求,还是将我们的促销信息定制给潜在受众,我们都将使用和生成文本数据。
写作助手非常有用
从 Gmail 的文本预测功能到 Grammarly 的智能拼写检查器,基于机器学习的编辑器已经证明它们可以以多种方式为用户提供价值。这使得我们特别有兴趣探索如何从头开始构建它们。
基于机器学习的写作辅助是独立的
许多机器学习应用程序只有在与更广泛的生态系统紧密集成时才能正常运行,比如预测骑行应用的 ETA、在线零售商的搜索和推荐系统以及广告竞价模型。然而,一个文本编辑器,即使它可能受益于集成到文档编辑生态系统中,也可以单独提供价值,并通过一个简单的网站进行暴露。
在整本书中,这个项目将允许我们突出显示我们建议用于构建基于机器学习的应用程序的挑战和相关解决方案。
机器学习过程
从一个想法到部署的机器学习应用的道路是曲折而漫长的。在看到许多公司和个人构建这样的项目后,我确定了四个关键的连续阶段,每个阶段都将在本书的一个部分中讨论。
-
确定正确的机器学习方法: 机器学习领域广泛,并经常提出多种方法来解决特定的产品目标。对于给定问题的最佳方法将取决于许多因素,例如成功标准、数据可用性和任务复杂性。此阶段的目标是设定正确的成功标准,并确定适当的初始数据集和模型选择。
-
构建初始原型: 在开始模型工作之前,首先建立一个端到端的原型。此原型应旨在解决产品目标,不涉及机器学习,并将允许您确定如何最佳应用机器学习。建立原型后,您应该知道是否需要机器学习,并且应该能够开始收集数据集来训练模型。
-
迭代模型: 现在您已经有了数据集,可以训练模型并评估其缺点。这一阶段的目标是在错误分析和实施之间反复交替。增加此迭代循环发生的速度是提高机器学习开发速度的最佳方法。
-
部署和监控: 一旦模型显示出良好的性能,您应选择合适的部署选项。一旦部署,模型往往会以意想不到的方式失败。本书的最后两章将介绍减少和监控模型错误的方法。
有很多内容需要涵盖,因此让我们直接开始并从第一章开始吧!
本书中使用的约定
本书中使用以下排版约定:
Italic
指示新术语、网址、电子邮件地址、文件名和文件扩展名。
Constant width
用于程序列表,以及在段落中引用程序元素,如变量或函数名称、数据库、数据类型、环境变量、语句和关键字。
Constant width bold
显示应由用户按字面意义输入的命令或其他文本。
Constant width italic
显示应由用户提供的值或由上下文确定的值替换的文本。
提示
此元素表示提示或建议。
注
此元素表示一般注释。
警告
此元素表示警告或注意事项。
使用代码示例
本书的补充代码示例可在https://oreil.ly/ml-powered-applications下载。
如果您有技术问题或在使用代码示例时遇到问题,请发送电子邮件至bookquestions@oreilly.com。
本书旨在帮助您完成工作任务。一般情况下,如果本书提供了示例代码,您可以在自己的程序和文档中使用它。除非您要复制大部分代码,否则无需征得我们的许可。例如,编写一个使用本书多个代码片段的程序并不需要征得许可。销售或分发 O’Reilly 图书的示例则需要许可。如果您通过引用本书并引用示例代码来回答问题,则无需征得许可。将本书的大量示例代码整合到产品文档中需要征得许可。
我们感谢您的致谢,但通常不需要。致谢通常包括标题、作者、出版商和 ISBN。例如:Building Machine Learning Powered Applications by Emmanuel Ameisen (O’Reilly). Copyright 2020 Emmanuel Ameisen, 978-1-492-04511-3.”
如果您觉得使用的代码示例超出了合理使用范围或此处给出的权限,请随时联系我们,邮箱是permissions@oreilly.com。
致谢
写这本书的项目始于我在 Insight Data Science 指导 Fellows 和监督 ML 项目的工作。感谢 Jake Klamka 和 Jeremy Karnowski 分别给了我领导这个项目的机会,并鼓励我写下所学的教训。我还要感谢我在 Insight 与之合作的数百位 Fellows,让我有机会帮助他们推动 ML 项目的界限。
写一本书是一项艰巨的任务,而 O’Reilly 的工作人员帮助在每一步骤中都更加可控。特别是,我要感谢我的编辑 Melissa Potter,她在写书的旅程中不知疲倦地提供指导、建议和精神支持。感谢 Mike Loukides,他以某种方式说服我写书是一个合理的事业。
感谢技术审阅人员在书的初稿中仔细查找错误并提出改进建议。感谢 Alex Gude,Jon Krohn,Kristen McIntyre 和 Douwe Osinga 抽出宝贵时间帮助这本书变得更好。对于那些我向他们询问实际 ML 挑战的数据实践者们,感谢你们的时间和见解,希望你们会发现这本书充分涵盖了这些挑战。
最后,在这本书的写作过程中,经历了一系列繁忙的周末和深夜,我要感谢我的坚定伴侣 Mari,我那挑剔的搭档 Eliott,我的智慧和耐心的家人,以及那些没有将我报失踪的朋友们。因为有你们,这本书才成为现实。
第一部分:寻找正确的 ML 方法
大多数个人或公司都清楚地知道他们有兴趣解决的问题,例如预测哪些客户会离开在线*台或构建一个可以在用户滑雪时跟随用户的无人机。同样,大多数人可以迅速学习如何训练模型以合理的准确率分类客户或检测对象,只需给定一个数据集。
然而,更少见的是能够将问题提出,估计如何最好地解决它,制定使用 ML 来解决它的计划,并自信地执行该计划。这通常是通过经验学习的技能,在多个过于雄心勃勃的项目和未能按期完成的情况下学习。
对于一个特定的产品,存在许多潜在的 ML 解决方案。在图 I-1 中,您可以看到左侧的一个潜在写作助手工具的草图,其中包括建议和用户提供反馈的机会。图像的右侧是一个潜在 ML 方法的图表,用于提供此类建议。

图 I-1. 从产品到 ML
本节首先涵盖了这些不同的潜在方法,以及如何选择其中之一的方法。然后深入探讨了如何将模型的性能指标与产品需求调和。
为了做到这一点,我们将处理两个连续的主题:
第一章
在本章结束时,您将能够提出一个应用程序的想法,估计是否可能解决,确定是否需要 ML 来解决,并找出从哪种类型的模型开始是最合理的。
第二章
在本章中,我们将讨论如何在您应用程序的目标背景下准确评估您的模型的性能,以及如何利用这一指标定期取得进展。
第一章:从产品目标到 ML 框架
机器学习允许机器从数据中学习,并以概率方式行事,以解决优化特定目标的问题。这与传统编程相对立,传统编程中程序员会编写逐步说明如何解决问题。这使得机器学习特别适合于构建我们无法定义启发式解决方案的系统。
图 1-1 描述了编写检测猫的系统的两种方法。在左侧,一个程序由手动编写的过程组成。在右侧,机器学习方法利用带有相应动物标签的猫和狗的照片数据集,允许模型学习从图像到类别的映射。在机器学习方法中,没有规定结果应该如何实现,只有一组示例输入和输出。

图 1-1. 从定义过程到展示示例
机器学习(ML)非常强大,可以解锁全新的产品,但由于它基于模式识别,所以引入了一定程度的不确定性。重要的是要确定产品的哪些部分会从 ML 中受益,并且如何框定学习目标,以最小化用户体验差的风险。
例如,对于人类来说,根据像素值自动检测图像中的动物并写出逐步说明几乎是不可能的(并且极其耗时)。然而,通过向卷积神经网络(CNN)输入成千上万张不同动物的图像,我们可以构建一个比人类更准确地进行分类的模型。这使得用机器学习解决这种问题变得很有吸引力。
另一方面,自动计算您的税款的应用程序应依赖政府提供的指南。正如您可能已经听说的那样,税务申报错误通常是不受欢迎的。这使得使用机器学习自动生成税务申报存在一定的风险。
当您可以通过一组可管理的确定性规则解决问题时,您永远不希望使用机器学习。在这里,可管理意味着您可以自信地编写这些规则,并且不会因维护过于复杂而困扰。
因此,虽然机器学习为不同的应用领域开辟了新世界,但重要的是要考虑哪些任务可以和应该通过机器学习来解决。在构建产品时,您应该从一个具体的业务问题出发,确定是否需要机器学习,然后努力找到一种机器学习方法,使您能够尽快迭代。
我们将在本章中详细介绍这个过程,首先介绍估算哪些任务可以通过机器学习解决,哪些机器学习方法适合哪些产品目标,以及如何处理数据需求。我将通过我们在“我们的案例研究:ML 辅助写作”中提到的 ML 编辑器案例研究以及与 Monica Rogati 的一次采访来说明这些方法。
评估可能性
由于机器学习模型能够处理任务而无需人类提供逐步指导,这意味着它们能够比人类专家更好地执行一些任务(例如从放射影像中检测肿瘤或打围棋),以及一些对人类完全无法接触的任务(例如从数百万篇文章中推荐文章或更改说话者的声音以听起来像其他人)。
机器学习能够直接从数据中学习使其在广泛的应用领域中非常有用,但这也使人们更难准确地区分哪些问题可以通过机器学习来解决。在每篇发表在研究论文或公司博客上的成功结果背后,都有数百个听起来合理但却彻底失败的想法。
虽然目前没有确切的方法可以预测机器学习的成功,但有一些指导原则可以帮助您减少处理机器学习项目所涉及的风险。最重要的是,您应始终以产品目标为出发点,然后决定如何最好地解决它。在此阶段,要对任何方法持开放态度,无论是需要机器学习还是不需要。在考虑机器学习方法时,务必根据它们对产品的适用性来评估这些方法,而不仅仅是考虑方法本身是否有趣。
最佳方法是通过以下两个连续步骤来完成:(1) 将您的产品目标构建成一个机器学习范式,以及 (2) 评估该机器学习任务的可行性。根据您的评估结果,您可以重新调整您的构建,直到我们满意为止。让我们探讨一下这些步骤的实际含义。
-
在机器学习范式中构建产品目标: 当我们构建一个产品时,我们首先考虑要向用户提供什么样的服务。正如我们在介绍中提到的那样,我们将通过一个案例研究来说明本书中的概念,即一款帮助用户更好地提出问题的编辑器。这个产品的目标很明确:我们希望用户在他们写作的内容上获得可操作和有用的建议。然而,机器学习问题的构建方式完全不同。一个机器学习问题涉及从数据中学习函数。一个例子是学习将一种语言的句子输入并输出为另一种语言的句子。对于一个产品目标,通常会有许多不同的机器学习表述,其实施难度各不相同。
-
评估机器学习可行性: 所有机器学习问题并非一视同仁!随着我们对机器学习的理解逐步深入,例如正确分类猫和狗的照片的模型构建问题已经能够在几小时内解决,而创建能够进行对话的系统等问题则仍然是开放性研究问题。要高效地构建机器学习应用程序,重要的是考虑多种潜在的机器学习框架,并从我们认为最简单的开始。评估机器学习问题难度的最佳方法之一是查看它所需的数据类型以及可以利用该数据的现有模型。
要提出不同的框架建议并评估其可行性,我们应该检查机器学习问题的两个核心方面:数据和模型。
我们将从模型开始。
模型
在机器学习中有许多常用的模型,我们将在此不进行全部概述。您可以参考“附加资源”中列出的书籍,以获取更全面的概述。除了常见模型外,每周都会发布许多模型变体、新颖架构和优化策略。仅在 2019 年 5 月,就有超过 13,000 篇论文被提交到ArXiv,这是一个流行的电子研究存档,经常有关于新模型的论文被提交。
然而,分享不同类别模型的概述及其在不同问题中的应用是很有用的。为此,我在这里提出了一个简单的模型分类法,根据它们处理问题的方式。您可以将其作为选择解决特定机器学习问题方法的指南。由于模型和数据在机器学习中密切相关,您会注意到这一部分与“数据类型”存在一些重叠。
机器学习算法可以根据是否需要标签进行分类。在这里,标签指的是数据中一个理想输出的存在,模型应该为给定示例产生该输出。监督算法利用包含输入标签的数据集,并旨在学习从输入到标签的映射。另一方面,无监督算法则不需要标签。最后,弱监督算法利用并非完全符合期望输出但在某种程度上类似的标签。
许多产品目标可以通过监督和无监督算法来解决。例如,可以通过训练模型检测与*均交易不同的交易来构建欺诈检测系统,这不需要标签。此类系统也可以通过手动标记交易为欺诈或合法,并训练模型从这些标签中学习来构建。
对于大多数应用程序,监督方法更容易验证,因为我们可以访问标签来评估模型预测的质量。 这也使得训练模型变得更容易,因为我们可以访问所需的输出。 虽然创建带标签的数据集有时可能最初耗时,但这使得构建和验证模型变得更加容易。 因此,本书大部分内容将涵盖监督方法。
话虽如此,确定您的模型将接受哪种类型的输入和产生哪些输出将有助于显著缩小可能的方法。 根据这些类型,以下任何 ML 方法类别都可能是一个很好的选择:
-
分类和回归
-
知识提取
-
目录组织
-
生成模型
我将在以下部分进一步展开。 当我们探索这些不同的建模方法时,我建议考虑您可以使用或可以收集的数据类型。 数据可用性通常最终成为模型选择的限制因素。
分类和回归
一些项目的重点是有效地在两个或多个类别之间分类数据点,或者在连续尺度上为它们赋予一个值(称为回归而不是分类)。 回归和分类在技术上是不同的,但通常处理它们的方法有显著的重叠,因此我们在这里将它们放在一起。
分类和回归之间相似的原因之一是大多数分类模型输出一个模型属于某一类别的概率分数。 然后,分类方面归结为根据这些分数决定如何将对象归因于类别。 从高层次来看,因此分类模型可以被看作是对概率值的回归。
通常,我们对单个示例进行分类或评分,例如将每封电子邮件分类为有效或垃圾的垃圾邮件过滤器,将用户分类为欺诈或合法的欺诈检测系统,或将骨骼分类为骨折或健康的计算机视觉放射学模型。
在 图 1-2 中,您可以看到一个根据其情感和主题对句子进行分类的示例。

图 1-2. 将句子分类为多个类别
在回归项目中,与为每个示例分配一个类别不同,我们给它们一个值。 根据诸如房间数量和位置等属性预测房屋销售价格是回归问题的一个示例。
在某些情况下,我们可以访问一系列过去的数据点(而不是单个数据点)来预测未来的事件。这种类型的数据通常称为时间序列,从一系列数据点中进行预测被称为预测。时间序列数据可以代表患者的医疗历史或国家公园的一系列出勤测量。这些项目通常受益于可以利用这种增加的时间维度的模型和特征。
在其他情况下,我们尝试从数据集中检测异常事件。这被称为异常检测。当分类问题试图检测代表数据中小部分的事件时,因此很难准确检测到时,通常需要使用不同的方法集。这里用“从大数据集中找出一个小东西”来形容很贴切。
良好的分类和回归工作通常需要进行重要的特征选择和特征工程工作。特征选择包括识别具有最高预测价值的特征子集。特征生成是通过修改和组合数据集的现有特征来识别和生成目标的良好预测器的任务。我们将在第三部分更深入地讨论这两个主题。
*年来,深度学习表现出了自动从图像、文本和音频中生成有用特征的良好能力。未来,它可能在简化特征生成和选择方面发挥更大作用,但目前它们仍然是机器学习工作流的重要部分。
最后,我们通常可以在前述分类或评分的基础上提供有用的建议。这需要建立一个可解释的分类模型,并利用其特征生成可操作的建议。稍后会详细讨论这一点!
并非所有问题都旨在为示例归因一组类别或值。在某些情况下,我们希望在更细粒度的级别上操作,并从输入的部分提取信息,比如知道图片中的物体位置。
从非结构化数据中提取知识
结构化数据是以表格格式存储的数据。数据库表和 Excel 表格是结构化数据的好例子。非结构化数据指的是不以表格格式存储的数据集。这包括文本(来自文章、评论、维基百科等)、音乐、视频和歌曲。
在图 1-3 中,您可以看到左侧是结构化数据的示例,右侧是非结构化数据的示例。知识提取模型专注于通过机器学习从非结构化数据源中提取结构。
在文本的情况下,知识提取可用于为评论添加结构。可以训练模型来从评论中提取例如清洁度、服务质量和价格等方面。用户随后可以轻松访问提及他们感兴趣主题的评论。

Figure 1-3. 结构化和非结构化数据的示例类型
在医疗领域,知识提取模型可以被建立为接受医学论文中的原始文本作为输入,并提取诸如论文讨论的疾病、相关的诊断及其性能等信息。在 Figure 1-4 中,一个模型以句子作为输入,并提取哪些词语指代媒体类型以及哪些词语指代媒体的标题。例如,在粉丝论坛的评论中使用这样的模型,我们可以生成关于经常被讨论的电影的摘要。

Figure 1-4. 从句子中提取媒体类型和标题
对于图像,知识提取任务通常包括在图像中找到感兴趣的区域并对其进行分类。两种常见的方法如 Figure 1-5 所示:对象检测是一种粗略的方法,它涉及在感兴趣的区域周围绘制矩形(称为边界框),而分割则精确地将图像的每个像素归属到特定的类别。

Figure 1-5. 边界框和分割掩模
有时,这些提取的信息可以作为另一个模型的输入。例如,使用姿势检测模型从瑜伽视频中提取关键点,并将这些关键点馈送给第二个模型,根据标记数据对姿势进行分类。Figure 1-6 展示了这样一系列模型的示例。第一个模型从非结构化数据(照片)中提取结构化信息(关节坐标),第二个模型则接受这些坐标并将其分类为瑜伽姿势。

Figure 1-6. 瑜伽姿势检测
到目前为止,我们看到的模型主要关注于根据给定输入生成输出。在某些情况下,例如搜索引擎或推荐系统,产品的目标是展示相关的项目。这是我们将在下一个类别中讨论的内容。
目录组织
目录组织模型通常会生成一组结果供用户查看。这些结果可以基于键入搜索栏中的输入字符串、上传的图像或对家庭助手说的短语进行条件设定。在许多情况下,例如流媒体服务,这些结果也可以主动呈现给用户,作为他们可能喜欢的内容,而不需要他们发出任何请求。
Figure 1-7 展示了这样一个系统的示例,它基于用户刚刚观看的电影主动提供潜在的候选电影,而无需用户进行任何形式的搜索。

Figure 1-7. 电影推荐
因此,这些模型要么推荐与用户已经表达兴趣的物品相关的物品(例如类似的 Medium 文章或 Amazon 产品),要么提供一个有用的搜索目录的方式(允许用户通过键入文本或提交他们自己的照片来搜索物品)。
这些推荐通常基于从先前用户模式学习的经验,这种情况下称为协同推荐系统。有时,它们基于物品的特定属性,这种情况下称为基于内容的推荐系统。一些系统结合了协同和基于内容的方法。
最后,机器学习也可以用于创造性目的。模型可以学习生成审美良好的图像、音频,甚至有趣的文本。这样的模型被称为生成模型。
生成模型
生成模型专注于生成数据,可能依赖于用户输入。因为这些模型侧重于生成数据而不是分类、评分、提取信息或组织信息,它们通常具有广泛的输出。这意味着生成模型非常适合像翻译这样输出多样的任务。
另一方面,生成模型通常用于训练和输出不受限制的情况,这使得它们成为生产中更具风险的选择。因此,除非它们对实现目标必不可少,否则建议首先使用其他模型。然而,对于希望深入了解生成模型的读者,我推荐 David Foster 的书籍Generative Deep Learning。
实际的例子包括翻译,将一种语言中的句子映射到另一种语言;摘要生成;字幕生成,将视频和音轨映射到文本;以及神经风格转移(参见Gatys 等,“艺术风格的神经算法”),将图像映射到风格化的再现。
图 1-8 展示了一个生成模型的例子,通过给左侧的照片赋予与右侧小插图中的绘画类似风格的转换。

图 1-8 的风格转移示例来自Gatys 等,“艺术风格的神经算法”。
如您所见,每种类型的模型都需要不同类型的训练数据。通常情况下,模型的选择很大程度上取决于您能够获取的数据,因为数据的可用性通常决定了模型的选择。
让我们来看几种常见的数据情景及其关联的模型。
数据
监督机器学习模型利用数据中的模式来学习输入和输出之间有用的映射关系。如果数据集包含预测目标输出的特征,适当的模型应该能够从中学习。然而,大多数情况下,我们最初没有正确的数据来训练模型从头到尾解决产品使用案例。
例如,假设我们正在训练一个语音识别系统,它将监听客户的请求,理解他们的意图,并根据该意图执行操作。当我们开始这个项目时,我们可以定义一组我们希望理解的意图,比如“在电视上播放电影”。
要训练一个能够完成此任务的机器学习模型,我们需要有一个包含来自各种背景的用户的音频剪辑的数据集,这些用户用自己的术语请求系统播放电影。拥有代表性的输入集合至关重要,因为任何模型只能从我们提供给它的数据中学习。如果数据集仅包含某一人群的示例,产品将只对该人群有用。考虑到我们选择的专业领域,因此几乎不可能已经存在这样的示例数据集。
对于大多数我们想要解决的应用程序,我们需要搜索、策划和收集额外的数据。数据获取过程的范围和复杂性可能因项目的具体情况而异,提前估计面临的挑战是成功的关键。
首先,让我们定义一些在搜索数据集时可能遇到的不同情况。这个初始情况应该是决定如何继续的关键因素。
数据类型
一旦我们把问题定义为将输入映射到输出,我们可以搜索遵循此映射的数据来源。
对于欺诈检测,这些可能是欺诈和无辜用户的示例,以及他们账户的特征,我们可以用来预测他们的行为。对于翻译,这将是源语言和目标语言领域的句子对语料库。对于内容组织和搜索,这可能是过去搜索和点击的历史记录。
我们很少能找到我们正在寻找的确切映射。因此,考虑几种不同情况是有用的。把这看作是数据需求的层次结构。
数据可用性
大致有三个数据可用性级别,从最理想的情况到最具挑战性的情况。不幸的是,与大多数其他任务一样,通常可以假设最有用的数据类型最难找到。让我们详细讨论一下。
标记数据存在
这是图 1-9 中最左侧的类别。在处理监督模型时,找到一个标记数据集是每个从业者的梦想。这里的“标记”意味着许多数据点包含模型试图预测的目标值。这使得训练和评估模型质量变得更加容易,因为标签提供了真实的答案。在实践中,很难找到一个符合你需求并且在网上免费获取的标记数据集。然而,常见的情况是你可能会把你找到的数据集误认为是你所需要的数据集。
存在弱标记数据
这是图 1-9 中间的类别。一些数据集包含的标签与建模目标不完全一致,但与之相关。例如,音乐流媒体服务的回放和跳过历史是一个弱标记的数据集示例,用于预测用户是否不喜欢一首歌曲。虽然听众可能没有标记一首歌曲为不喜欢,但如果他们在播放时跳过了它,这表明他们可能不喜欢这首歌。弱标签按定义不够精确,但通常比完美标签更容易找到。
存在无标记数据
这是图 1-9 中右侧的类别。在某些情况下,虽然我们没有一个将期望输入映射到输出的标记数据集,但我们至少可以访问一个包含相关示例的数据集。以文本翻译为例,我们可能可以访问大量的文本集合,涵盖两种语言,但它们之间没有直接的映射。这意味着我们需要标记数据集,找到能够从无标记数据中学习的模型,或者两者兼而有之。
我们需要获取数据
在某些情况下,我们离无标记数据只有一步之遥,因为我们首先需要获取它。在许多情况下,我们没有我们所需的数据集,因此需要找到一种获取这些数据的方法。这通常被视为一项难以克服的任务,但现在存在许多快速收集和标记数据的方法。这将是第四章的重点。
对于我们的案例研究,一个理想的数据集将是一组用户提出的问题,以及一组更好的措辞问题。一个弱标记的数据集将是包含许多问题并带有一些弱标签,表明它们质量的数据集,例如“喜欢”或“点赞”。这将有助于模型学习什么是好问题和坏问题,但不提供相同问题的并列示例。你可以在图 1-9 中看到这两个例子。

图 1-9. 数据可用性与数据有用性
通常情况下,在机器学习中,弱标记数据集指的是包含可以帮助模型学习的信息,但不是精确的真实标签的数据集。实际上,我们可以收集到的大多数数据集都是弱标记的。
拥有一个不完美的数据集是完全可以接受的,不应该阻止你。ML 过程是迭代的,所以从一个数据集开始并获得一些初步结果是前进的最佳方式,无论数据质量如何。
数据集是迭代的
在许多情况下,由于您可能无法立即找到一个直接映射从输入到所需输出的数据集,我建议逐步迭代问题的表述方式,使得更容易找到一个足够的数据集作为起点。您探索和使用的每个数据集都将为您提供宝贵的信息,您可以利用这些信息策划下一个版本的数据集,并为您的模型生成有用的特征。
现在让我们深入案例研究,看看我们如何利用所学知识识别不同的模型和数据集,并选择最合适的。
构建 ML 编辑器
让我们看看如何通过一个产品用例的迭代来找到正确的 ML 框架。我们将通过概述从产品目标(帮助用户写出更好问题)到 ML 范式的方法来完成这一过程。
我们想要建立一个编辑器,接受用户的问题并改进它们,使其写作更好,但在这种情况下,“更好”是什么意思呢?让我们从更清晰地定义写作助手的产品目标开始。
许多人使用论坛、社交网络和网站如Stack Overflow来寻找答案。然而,人们提问的方式对于是否能得到有用的答案有着巨大的影响。这对于希望得到他们问题答案的用户和可能有相同问题的未来用户都是不利的。因此,我们的目标是构建一个助手,可以帮助用户写出更好的问题。
现在我们有了一个产品目标,需要决定使用哪种建模方法。为了做出这个决定,我们将进行前面提到的模型选择和数据验证的迭代循环。
尝试使用 ML 来做所有事情:一个端到端的框架
在这个背景下,端到端意味着使用单一模型从输入到输出,没有中间步骤。由于大多数产品目标非常具体,尝试通过端到端学习整个用例通常需要定制的尖端 ML 模型。这可能是那些有能力开发和维护这些模型的团队的正确解决方案,但通常最好先从更为成熟的模型开始。
在我们的情况下,我们可以尝试收集一个问题表述不佳及其专业编辑版本的数据集。然后,我们可以使用生成模型直接从一个文本生成另一个文本。
图 1-10 展示了这在实践中的样子。它显示了一个简单的图表,左侧是用户输入,右侧是期望的输出,中间是一个模型。

图 1-10. 端到端方法
如你所见,这种方法存在着显著的挑战:
数据
要获取这样的数据集,我们需要找到意图相同但措辞不同的问题对。这种数据集很难找到。自行构建将会很昂贵,因为我们需要专业编辑的帮助来生成这些数据。
模型
从一段文本到另一段文本的模型,如前面讨论的生成模型类别中所见,*年来取得了巨大进展。序列到序列模型(如 I. Sutskever 等人在 “Sequence to Sequence Learning with Neural Networks” 论文中描述的那样)最初是 2014 年提出用于翻译任务,并且正在缩小机器翻译与人类翻译之间的差距。然而,这些模型的成功主要集中在句子级任务上,并且它们并不经常用于处理比段落更长的文本。这是因为迄今为止,它们尚未能够从一段到另一段捕捉长期上下文。此外,由于它们通常具有大量参数,它们是训练速度最慢的模型之一。如果一个模型只需要训练一次,这并不一定是问题。但如果它需要每小时或每天重新训练,训练时间可能会成为一个重要因素。
延迟
序列到序列模型通常是自回归模型,这意味着它们需要上一个词的模型输出来开始处理下一个词。这使它们能够利用邻*词的信息,但导致它们在训练和推理时比更简单的模型要慢。这样的模型可能需要几秒钟来在推理时生成答案,而简单模型的延迟则为亚秒级。虽然可以优化这样的模型以使其运行速度足够快,但这将需要额外的工程工作。
实施的便利性
训练复杂的端到端模型是一个非常微妙且容易出错的过程,因为它们有许多移动部件。这意味着我们需要考虑模型潜在性能与其为流水线增加的复杂性之间的权衡。这种复杂性在构建流水线时会减慢我们的速度,同时也会增加维护负担。如果我们预期其他团队成员可能需要对您的模型进行迭代和改进,选择一组更简单、更为人熟知的模型可能是值得的。
这种端到端方法可能有效,但需要大量的前期数据收集和工程努力,并不能保证成功,因此探索其他替代方案是值得的,接下来我们将介绍。
最简单的方法:成为算法
正如您将在本节末尾的采访中看到的,对于数据科学家来说,成为算法通常是一个很好的想法,然后再实现它。换句话说,要想了解如何最好地自动化一个问题,首先尝试手动解决它是个不错的开始。那么,如果我们自己编辑问题以提高可读性和获取答案的几率,我们将如何进行?
一个初步的方法是完全不使用数据,而是利用先前的成果来定义什么样的问题或文本写得好。对于一般的写作技巧,我们可以向专业编辑或研究报纸的风格指南寻求帮助以了解更多信息。
此外,我们应该深入数据集以查看个别示例和趋势,并让它们指导我们的建模策略。由于我们将在第四章中更深入地介绍如何做到这一点,现在我们将跳过此步骤。
首先,我们可以查看现有的研究,以识别几个可能用于帮助人们更清晰地写作的属性。这些特征可能包括以下因素:
散文简洁性
我们经常建议新作家使用更简单的词语和句子结构。因此,我们可以建立一套适合句子和词语长度的标准,并在需要时推荐修改。
语气
我们可以测量副词、最高级和标点符号的使用情况来测量文本的极性。根据背景,更具意见的问题可能会得到较少的答案。
结构特征
最后,我们可以尝试提取重要结构属性的存在,如使用问候语或问号。
一旦我们确定并生成了有用的特征,我们可以构建一个简单的解决方案,利用它们提供建议。这里没有涉及机器学习,但这个阶段有两个关键原因:它提供了一个非常快速实施的基准,并将作为衡量模型的标准。
为了验证我们对如何识别优秀写作的直觉,我们可以收集一个“好”和“坏”文本的数据集,并看看是否能够利用这些特征区分好的和坏的。
中间地带:从我们的经验中学习
现在我们已经有了一组基准特征集,我们可以尝试从数据集中学习样式模型。为此,我们可以收集一个数据集,从中提取我们之前描述的特征,并对其进行分类器训练,以区分好的和坏的示例。
一旦我们有了可以对书面文本进行分类的模型,我们就可以检查它,以确定哪些特征具有很高的预测性,并将其用作推荐。我们将在实践中看到如何做到这一点,详情请见第七章。
图 1-11 描述了这种方法。左侧,一个模型被训练为将问题分类为好的或坏的。右侧,训练好的模型接受一个问题并对该问题的候选重构进行评分,以使其获得更高的分数。得分最高的重构建议给用户。

图 1-11. 手动与端到端之间的中间地带
让我们检查我们在“试图用 ML 做所有事情:端到端框架”中概述的挑战,并看看分类器方法是否能更轻松地解决它们:
数据集
我们可以通过从在线论坛收集问题及其质量指标(如浏览量或赞数)来获取一组好的和坏的示例数据集。与端到端方法相反,这不需要我们访问相同问题的修订版本。我们只需一个可以学习聚合特征的好和坏示例集合,这是一个更容易找到的数据集。
模型
我们需要考虑两件事情:模型的预测能力(它能有效区分好坏文章吗?)以及从中提取特征的便利性(我们能看到用于分类示例的哪些属性吗?)。在这里我们可以使用许多潜在的模型,以及从文本中提取不同特征以使其更具可解释性。
延迟
大多数文本分类器非常迅速。我们可以从一个简单的模型开始,比如随机森林,它可以在普通硬件上少于十分之一秒返回结果,并根据需要转向更复杂的架构。
实施的简易性
与文本生成相比,文本分类相对较为成熟,这意味着构建这样一个模型应该相对迅速。在线上存在许多工作的文本分类流水线示例,并且许多这样的模型已经部署到生产环境中。
如果我们从人类启发式开始,然后构建这个简单模型,我们将很快能够得到一个初始基准,并迈出解决问题的第一步。此外,初始模型将是指导接下来构建什么的良好方式(更多详情请参阅第三部分)。
关于从简单基线开始的重要性,我与 Monica Rogati 进行了交流,她分享了她在帮助数据团队交付产品过程中学到的一些经验教训。
Monica Rogati:如何选择和优先处理 ML 项目
莫妮卡·罗加蒂在计算机科学博士学位后,从 LinkedIn 开始她的职业生涯,她在整合机器学习到“你可能认识的人”算法等核心产品上工作,并建立了职位到候选人匹配的第一个版本。然后她成为 Jawbone 的数据副总裁,领导整个数据团队。莫妮卡现在是几十家公司的顾问,这些公司的员工数量从 5 到 8,000 不等。她很慷慨地同意分享一些她在设计和执行机器学习产品时经常给团队的建议。
Q:如何确定机器学习产品的范围?
A:您必须记住,您正在尝试使用最佳工具来解决问题,并且只有在有意义的情况下才使用机器学习。
假设您想预测应用程序用户将要做什么,并将其显示给他们作为建议。您应该从模型和产品的讨论开始。除其他事项外,这还包括围绕优雅处理机器学习失败设计产品。
您可以从考虑我们的模型对其预测的信心开始。然后,根据信心分数不同,我们可以以不同方式表达我们的建议。如果信心高于 90%,我们会突出显示建议;如果超过 50%,我们仍然显示,但不那么强调;如果信心低于这个分数,我们不显示任何内容。
Q:在机器学习项目中如何决定关注什么?
A:您必须找到影响瓶颈,即如果您改进它,可能会提供最大价值的流程部分。在与公司合作时,我经常发现,它们可能没有解决正确的问题,或者不处于适合此问题的增长阶段。
在模型周围经常会出现问题。找出问题的最佳方法是用简单的东西替换模型,然后调试整个流程。经常情况下,问题并不在于您模型的准确性。即使您的模型成功,您的产品也可能失败。
Q:为什么您通常建议从简单模型开始?
A:我们计划的目标应该是以某种方式减少我们模型的风险。这样做的最佳方式是从“稻草人基准”开始评估最坏情况下的性能。对于我们之前的例子,这可能仅仅是建议用户之前采取的任何行动。
如果我们这样做了,我们的预测有多少时会是正确的,如果我们错了,我们的模型对用户会有多烦人?假设我们的模型比这个基准好不了多少,我们的产品仍然有价值吗?
这对自然语言理解和生成的例子非常适用,比如聊天机器人、翻译、问答和摘要。例如,在摘要中,仅仅提取文章涵盖的顶级关键词和类别通常足以满足大多数用户的需求。
Q:一旦您有了整个流程,如何确定影响的瓶颈?
A: 你应该从设想影响瓶颈已解决的情况开始,并问问自己预计的努力是否值得。我鼓励数据科学家在开始项目之前撰写一条推文,公司撰写一篇新闻稿。这有助于他们避免仅仅因为觉得酷而开始某些工作,并根据努力的影响将结果的影响置于上下文中。
理想情况是,你可以无论结果如何都推销结果:即使你没有获得最佳结果,这仍然具有影响力吗?你学到了什么或验证了一些假设吗?一种有助于这一点的方法是建立基础设施,帮助降低部署所需的工作量。
在 LinkedIn,我们可以使用非常有用的设计元素,一个带有几行文本和超链接的小窗口,我们可以根据我们的数据进行自定义。这使得为诸如职位推荐之类的项目启动实验变得更容易,因为设计已经得到批准。由于资源投入低,影响不必如此大,这允许更快的迭代周期。障碍随后变成了非工程问题,如伦理、公*性和品牌。
Q: 你是如何决定使用哪些建模技术的?
A: 第一道防线是亲自查看数据。假设我们想建立一个模型来为 LinkedIn 用户推荐群组。一个天真的方法是推荐包含其公司名称的最流行群组。在查看了几个例子之后,我们发现公司 Oracle 的一个流行群组是“Oracle sucks!”,这将是向 Oracle 员工推荐的一个糟糕选择。
值得花费手动工作查看模型的输入和输出总是有价值的。浏览一堆示例以查看是否有异常情况。我在 IBM 的部门负责人有一个口头禅,在做任何工作之前,手动做一个小时的事情。
查看数据有助于你思考好的启发式、模型和重新构建产品的方法。如果你根据频率对数据集中的示例进行排名,甚至可以快速识别和标记 80%的用例。
例如,在 Jawbone,人们输入“短语”来记录他们的饮食内容。在我们手工标记了前 100 个短语之后,我们已经涵盖了 80%的短语,并且对我们需要处理的主要问题有了明确的想法,例如各种文本编码和语言的变化。
最后的防线是拥有多样化的工作人员来查看结果。这将允许您捕捉到模型展示出歧视行为的情况,例如将您的朋友标记为大猩猩,或者通过其智能的“去年此时”的回顾功能展示令人痛苦的过去经历。
结论
正如我们所见,构建一个由 ML 驱动的应用程序始于评估可行性并选择方法。在这些方法中,选择监督方法通常是开始的最简单方法。在这些方法中,分类、知识提取、目录组织或生成模型是实践中最常见的范式。
在选择方法时,您应该确定如何轻松或困难地访问有标签或无标签数据,或者根本没有数据。然后,您应该通过定义产品目标并选择最能帮助您实现此目标的建模方法,来比较潜在的模型和数据集。
我们为 ML 编辑器说明了这些步骤,选择从简单的启发式和基于分类的方法开始。最后,我们讲解了像 Monica Rogati 这样的领导者如何将这些实践应用到成功地推出 ML 模型给用户。
现在我们选择了一个初始方法,是时候定义成功的度量标准,并制定行动计划以保持定期进展了。这将涉及设置最低性能要求,深入了解可用的建模和数据资源,并构建一个简单的原型。
我们将在第二章中覆盖所有这些内容。
第二章:制定计划
在上一章中,我们讨论了如何估算是否需要机器学习,找到最合适使用机器学习的地方,并将产品目标转化为最合适的机器学习框架。在本章中,我们将讨论使用指标来跟踪机器学习和产品进展,并比较不同的机器学习实现。然后,我们将识别建立基准线和规划建模迭代的方法。
我不幸地看到许多机器学习项目从一开始就注定要失败,因为产品指标与模型指标之间存在不匹配。更多的项目失败不是因为建立了好的模型,而是因为这些模型对产品没有帮助。这就是为什么我想要专门讨论指标和规划的原因。
我们将讨论利用现有资源和问题的约束条件来构建可行计划的技巧,这将极大简化任何机器学习项目。
让我们更详细地定义性能指标。
测量成功
当涉及到机器学习时,我们建立的第一个模型应该是能够解决产品需求的最简单模型,因为生成和分析结果是快速推动机器学习进展的方法。在上一章中,我们提到了三种增加复杂性的潜在方法,用于机器学习编辑器。这里提醒一下:
基准线;基于领域知识设计启发式方法
我们可以从简单地定义规则开始,基于我们对写作优质内容的先前知识。我们将通过测试这些规则来看它们是否有助于区分优秀文本和糟糕文本。
简单模型;将文本分类为好或坏,并使用分类器生成推荐
然后我们可以训练一个简单的模型来区分好问题和坏问题。只要模型表现良好,我们可以检查它,看看它找到了哪些特征对好问题有很高的预测性,并将这些特征用作推荐。
复杂模型;训练一个从坏文本到好文本的端到端模型
这是最复杂的方法,无论是在模型还是数据方面,但如果我们有资源来收集训练数据,并构建和维护一个复杂的模型,我们就能直接解决产品需求。
所有这些方法都是不同的,随着我们在原型过程中的学习,它们可能会发展变化,但在进行机器学习时,您应该定义一套共同的指标来比较建模管道的成功。
注意
并非总是需要机器学习
您可能已经注意到,基准线方法根本不依赖于机器学习。正如我们在第一章中讨论的那样,有些特征根本不需要机器学习。同样重要的是要意识到,即使是那些可能受益于机器学习的特征,通常也可以简单地使用启发式方法作为它们的第一个版本。一旦启发式方法被采用,您甚至可能会意识到根本不需要机器学习。
构建启发式方法通常也是构建功能的最快方式。一旦构建并使用了功能,您将更清晰地了解用户的需求。这将帮助您评估是否需要 ML,并选择建模方法。
在大多数情况下,开始时没有 ML 是构建 ML 产品的最快方式。
为此,我们将涵盖四类对任何 ML 产品有重大影响的性能指标:业务指标,模型指标,新鲜度和速度。清晰定义这些指标将允许我们准确衡量每次迭代的性能。
业务绩效
我们已经讨论了以明确的产品或功能目标开始的重要性。一旦这个目标明确,就应该定义一个度量标准来评判其成功。这个度量标准应该与任何模型指标分开,并且只是产品成功的反映。产品指标可能非常简单,例如吸引功能用户的数量,或者更复杂,例如我们提供的推荐的点击率(CTR)。
产品指标最终是唯一重要的,因为它们代表了您的产品或功能的目标。所有其他指标应该被用作改进产品指标的工具。然而,产品指标并不需要是唯一的。尽管大多数项目倾向于专注于改善一个产品指标,但它们的影响通常以多个指标来衡量,包括安全指标,即不应低于给定点的指标。例如,一个机器学习项目可以旨在提高如点击率(CTR)之类的给定指标,同时保持其他指标稳定,例如*均用户会话长度。
对于 ML 编辑器,我们将选择一个度量推荐的有用性的指标。例如,我们可以使用用户按照建议行动的比例。为了计算这样的指标,ML 编辑器的界面应该捕捉用户是否赞同建议,例如通过在输入上方叠加并使其可点击。
我们看到每个产品都适用于许多潜在的 ML 方法。为了衡量 ML 方法的有效性,您应该跟踪模型性能。
模型性能
对于大多数在线产品,决定模型成功的最终产品指标是使用模型输出的访问者比例,相对于所有可能受益的访问者。例如,在推荐系统的情况下,通常通过测量有多少人点击推荐产品来评估性能(参见第八章关于此方法的潜在问题)。
当产品仍在建设中尚未部署时,无法测量使用度量。为了仍然衡量进展,定义一个单独的成功度量标准叫做离线度量或模型度量至关重要。一个好的离线度量应该可以在不暴露模型给用户的情况下评估,并且尽可能与产品度量和目标相关联。
不同的建模方法使用不同的模型指标,改变方法可以使达到足以实现产品目标的建模性能水*变得更容易。
例如,假设您正在尝试为在线零售网站上用户输入搜索查询时提供有用的建议。您将通过测量点击率来衡量此功能的成功,即用户点击您提供的建议的频率。
要生成建议,您可以构建一个模型,试图猜测用户将要输入的单词,并在用户输入时将预测的完整句子呈现给他们。您可以通过计算模型的单词级准确率来衡量其性能,计算它多频繁地预测正确的下一个单词组。这样的模型需要达到极高的准确率,以帮助提高产品的点击率,因为一个单词的预测错误足以使建议失效。这种方法在图 2-1 的左侧勾画出来。
另一种方法是训练一个模型,将用户输入分类到目录中的类别,并建议最有可能的三个预测类别。您将使用所有类别的准确率来衡量模型的性能,而不是每个英文单词的准确率。由于目录中的类别数量比英语词汇要小得多,这将是一个更容易优化的建模指标。此外,该模型只需要正确预测一个类别即可生成点击。这种模型更容易提高产品的点击率。您可以在图 2-1 的右侧看到这种方法在实践中的模拟效果。

图 2-1. 稍微改变产品可以使建模任务更加容易
正如您所见,通过对模型和产品之间的交互进行小的更改,可以使用更直接的建模方法并更可靠地交付结果。以下是更新应用程序以使建模任务更简单的几个其他示例:
-
更改接口,以便如果模型的置信度低于阈值,则可以省略模型的结果。例如,当构建一个自动完成用户输入的模型时,该模型可能只对部分句子表现良好。我们可以实现逻辑,只有当模型的置信度得分超过 90%时,才向用户显示建议。
-
除了模型的顶部预测外,还展示几个其他预测或启发法。 例如,大多数网站会显示模型建议的不止一项推荐。显示五个候选项而不是一个,可以增加建议对用户有用的可能性,即使模型是相同的。
-
向用户传达模型仍处于实验阶段,并给予他们提供反馈的机会。 当自动检测到用户非母语的语言并为其翻译时,网站通常会添加一个按钮,让用户知道翻译是否准确和有用。
即使建模方法适用于问题,有时生成与产品性能更相关的额外模型指标也是值得的。
我曾与一位数据科学家合作,他构建了一个模型,用于从简单网站的手绘草图生成 HTML(参见他的文章,“使用深度学习进行自动前端开发”)。该模型的优化指标使用交叉熵损失来比较每个预测的 HTML 标记与正确标记。然而,产品的目标是生成的 HTML 能够呈现与输入草图相似的网站,而不考虑标记的顺序。
交叉熵不考虑对齐:如果模型生成了一个正确的 HTML 序列,但在开头多了一个额外的标记,那么与目标相比,所有标记都会向后偏移一个。这样的输出会导致非常高的损失值,尽管实际上产生了几乎理想的结果。这意味着在试图评估模型的实用性时,我们应该超越其优化指标。在这个例子中,使用BLEU 分数提供了更好地衡量生成的 HTML 与理想输出之间相似性的方法。
最后,产品设计应考虑到合理的模型性能假设。如果产品依赖于模型的完美性才能有用,那么很可能会产生不准确甚至危险的结果。
例如,如果你正在构建一个模型,让你拍一张药片的照片并告诉患者其类型和剂量,那么模型能够有多少最低准确率仍然是有用的?如果当前方法很难达到这一准确性要求,您是否可以重新设计产品,以确保用户得到良好服务,而不会因其可能产生的预测错误而受到伤害?
在我们的情况下,我们希望构建的产品将提供写作建议。大多数机器学习模型在某些输入上表现出色,在某些输入上可能遇到困难。从产品的角度来看,如果我们无法帮助——我们需要确保我们不会使情况变得更糟——我们希望限制输出比输入更差的结果的时间。我们如何在模型指标中表达这一点?
假设我们构建了一个分类模型,试图预测一个问题是否好,这是根据它所获得的点赞数量来衡量的。分类器的精确度被定义为预测为好的问题中实际上好的问题的比例。另一方面,它的召回率是预测为好的问题占数据集中所有好问题的比例。
如果我们想要始终提供相关的建议,我们将优先考虑模型的精确度,因为高精确度的模型将一个问题分类为好(从而做出推荐)时,实际上这个问题确实很好的可能性很高。高精确度意味着当我们做出推荐时,它往往是正确的。关于为什么高精确度模型对撰写推荐更有用的更多信息,请参阅“克里斯·哈兰德:发布实验”。
我们通过查看模型在代表性验证集上的输出来衡量这些指标。我们将深入探讨这意味着什么,详见“评估您的模型:超越准确度”,但现在,请将验证集视为从训练中留出并用于估计模型在未见数据上的表现。
初始模型性能很重要,但面对用户行为变化时,模型保持有用性的能力同样重要。一个在特定数据集上训练的模型将在类似数据上表现良好,但我们如何知道是否需要更新数据集呢?
新鲜度和分布转变
监督模型从学习输入特征和预测目标之间的关联中获得其预测能力。这意味着大多数模型需要接触到与给定输入类似的训练数据才能表现良好。一个仅使用男性照片预测用户年龄的模型,在女性照片上将表现不佳。但即使模型在充足的数据集上进行了训练,许多问题的数据分布随着时间的推移会发生变化。当数据的分布转变时,模型通常需要相应变化以维持相同的性能水*。
让我们假设在注意到旧金山的雨对交通的影响后,您建立了一个模型,根据过去一周的降雨量预测交通状况。如果您在十月份使用过去三个月的数据建立模型,那么您的模型可能是在日降水量低于一英寸的数据上进行训练的。请参见图 2-2,展示了这种分布可能的示例。随着冬季的临*,*均降水量将接* 3 英寸,这高于模型在训练期间所接触到的任何数据,正如您可以在图 2-2 中看到的。如果模型没有接受更*期数据的训练,它将难以保持良好的表现。

图 2-2. 分布变化
总的来说,只要模型在训练期间接触到的数据足够相似,它可以在之前没有见过的数据上表现良好。
不同问题的新鲜度要求并不相同。古代语言的翻译服务可以预期其操作的数据保持相对稳定,而搜索引擎需要建立在用户搜索习惯变化快速的假设上进行构建。
根据您的业务问题,您应考虑保持模型“新鲜度”的难度。您需要多频繁重新训练模型?每次重新训练会带来多少成本?
对于 ML 编辑器,我们设想“良好构思的英文散文”定义变化的频率相对较低,可能是每年一次。然而,如果我们针对特定领域,新鲜度的需求就会发生变化。例如,提问数学问题的正确方式变化的速度会比提问音乐趋势问题的最佳措辞慢得多。由于我们估计模型需要每年重新训练一次,因此我们需要每年获得新鲜数据来进行训练。
我们的基线模型和简单模型都可以从非配对数据中学习,这使得数据收集过程更简单(我们只需找到最*一年的新问题)。然而,复杂模型需要配对数据,这意味着我们每年都必须找到相同句子的示例,以“好”和“坏”的方式表达。这意味着满足我们定义的新鲜度要求对于需要配对数据的模型来说将更加困难,因为获取更新的数据集需要更多时间。
对于大多数应用程序来说,受欢迎程度可以帮助减轻数据收集的需求。如果我们的提问服务一夜爆红,我们可以为用户添加一个按钮来评估输出质量。然后,我们可以收集用户过去的输入以及模型的预测和相关用户评分,并将它们用作训练集。
应用程序要受欢迎,关键在于它是否有用。通常,这需要及时响应用户请求。因此,模型能够快速提供预测的速度成为需要考虑的重要因素。
速度
理想情况下,模型应该能快速地提供预测。这使得用户更容易与之交互,并且更容易为多个并发用户提供模型服务。那么模型需要多快呢?对于某些用例,如翻译短句,用户会期望立即得到答案。对于其他用例,如医学诊断,患者愿意等待 24 小时,只要能确保得到最准确的结果。
在我们的情况下,我们将考虑两种潜在的方式来提供建议:通过一个提交框,用户写入内容后点击提交按钮并获取结果,或者每次用户输入新字母时动态更新。虽然我们可能更倾向于后者,因为这样可以使工具更加互动,但这需要模型运行得更快。
我们可以设想用户点击提交按钮后等待几秒钟以获取结果,但对于一个模型在用户编辑文本时运行,它需要在一秒内显著运行。最强大的模型需要更长时间来处理数据,因此在迭代模型时,我们将牢记这一要求。我们使用的任何模型应该能够在不到两秒的时间内处理一个示例的整个流程。
随着模型变得越来越复杂,模型推断的运行时间也会增加。即使在每个数据点相对较小的数据(例如 NLP,而不是实时视频任务)中,差异也很显著。例如,本书案例研究中使用的文本数据,LSTM 大约比随机森林慢三倍(LSTM 约为 22 毫秒,而随机森林只需 7 毫秒)。在单个数据点上,这些差异很小,但在需要同时对数万个示例进行推断时,它们可能会迅速累积。
对于复杂的应用程序,推断调用将与多个网络调用或数据库查询相关联,模型执行时间可能会比应用程序逻辑的其他部分短。在这些情况下,所述模型的速度就不再是问题的关键。
根据您的问题,还有其他类别可以考虑,例如硬件限制、开发时间和可维护性。在选择模型之前,了解您的需求非常重要,以确保以知情的方式选择所述模型。
一旦确定了要求和相关指标,就是制定计划的时候了。这需要估计前方的挑战。在接下来的部分,我将介绍如何利用先前的工作和探索数据集来决定接下来要构建什么。
估算范围和挑战
正如我们所见,机器学习的性能通常以模型指标报告。虽然这些指标很有用,但应用它们来改进我们定义的产品指标,这些产品指标代表我们试图解决的实际任务。在管道的迭代过程中,我们应该牢记产品指标,并努力改进它们。
到目前为止,我们所涵盖的工具将帮助我们确定是否值得处理某个项目,并衡量我们目前的表现如何。下一个逻辑步骤是草拟一个攻击计划,以估算项目的范围和持续时间,并预料可能遇到的障碍。
在机器学习中,成功通常需要充分理解任务的背景,获取一个好的数据集,并构建一个合适的模型。
我们将在下一节中详细介绍每个类别。
利用领域专业知识
我们可以从最简单的启动模型开始,即启发式方法:基于问题和数据的知识得出的经验法则。制定启发式方法的最佳途径是观察专家目前的做法。
制定启发式方法的第二最佳途径是查看您的数据。基于您的数据集,如果您手动进行此任务,您将如何解决?
要找出良好的启发式方法,我建议要么向该领域的专家学习,要么熟悉数据。接下来,我将稍微详细描述这两种方法。
向专家学习
对于我们可能想要自动化的许多领域,向该领域的专家学习可以节省我们数十小时的工作时间。例如,如果我们试图为工厂设备建立预测性维护系统,我们应该首先与工厂经理联系,了解我们可以合理假设哪些内容。这可能包括了解当前维护频率,通常表明机器即将需要维护的症状,以及与维护相关的法律要求。
当然,也有一些例子,可能很难找到领域专家,比如用于预测独特网站功能使用情况的专有数据。然而,在这些情况下,我们通常可以找到那些曾经处理过类似问题并从中学习经验的专业人士。
这将使我们了解可以利用的有用特征,找到应该避免的陷阱,更重要的是,防止我们为许多数据科学家不好的声誉重新发明轮子。
检查数据
正如 Monica Rogati 在“Monica Rogati: How to Choose and Prioritize ML Projects” 和 Robert Munro 在“Robert Munro: How Do You Find, Label, and Leverage Data?” 中提到的,开始建模之前查看数据非常关键。
探索性数据分析(EDA)是可视化和探索数据集的过程,通常是为了对给定的业务问题有直觉。EDA 是构建任何数据产品的关键部分。除了 EDA 之外,还需要以希望模型能够适当利用数据集的方式对示例进行标记。这样做有助于验证假设,并确认您选择了可以适当利用您的数据集的模型。
EDA 过程将帮助您了解数据的趋势,自行标记将迫使您建立一套启发式方法来解决问题。在完成了前两个步骤之后,您应该更清楚地知道哪种模型最适合您,以及我们可能需要的任何额外数据收集和标记策略。
下一个逻辑步骤是看看其他人如何解决类似的建模问题。
站在巨人的肩膀上
是否有人已经解决过类似的问题?如果是这样,开始的最佳方法是理解并复制现有的结果。寻找公共实现,其模型或数据集与您相似,或两者都有关。
理想情况下,这将涉及找到开源代码和可用数据集,但这些并不总是容易获得,特别是对于非常特定的产品。尽管如此,在机器学习项目上开始的最快方式是复制现有的结果,然后在其基础上建立。
在像机器学习这样有许多不同组成部分的领域中,站在巨人的肩膀上是至关重要的。
注意
如果您计划在您的工作中使用开源代码或数据集,请确保您有权这样做。大多数代码库和数据集都会包含定义可接受使用方式的许可证。此外,请给您最终使用的任何来源信用,最好附上对他们原始作品的引用。
在投入大量资源之前,构建一个令人信服的概念证明通常是一个好主意。例如,在使用时间和金钱标记数据之前,我们需要确信我们能够构建一个能够从这些数据中学习的模型。
那么,我们如何找到一个高效的开始方式呢?就像我们在本书中将要讨论的大多数主题一样,这包括两个主要部分:数据和代码。
开放数据
您可能并不总是能够找到符合您要求的数据集,但通常可以找到在性质上足够相似以帮助的数据集。在这种情况下,什么是类似的数据集?在这里将机器学习模型视为将输入映射到输出是有帮助的。基于这个想法,类似的数据集简单地意味着具有类似输入和输出类型的数据集(但不一定是相同领域的)。
经常,使用类似输入和输出的模型可以应用于完全不同的上下文。在图 2-3 的左侧是两个模型,它们都从图像输入中预测文本序列。一个用于描述照片,而另一个从该网站的截图生成网站的 HTML 代码。类似地,图 2-3 的右侧显示了一个模型,它从英文文本描述中预测食物类型,另一个从乐谱转录中预测音乐流派。

图 2-3. 具有相似输入和输出的不同模型
例如,假设我们试图构建一个模型来预测新闻文章的观看次数,但却难以找到新闻文章及其关联观看次数的数据集。我们可以先使用公开访问的Wikipedia 页面流量统计数据集训练一个预测模型。如果模型表现良好,可以合理地认为,给定一个新闻文章观看次数的数据集,我们的模型也能表现得相当不错。寻找类似的数据集有助于验证方法的有效性,并使得在获取数据方面的资源投入更具合理性。
在处理专有数据时,这种方法同样适用。往往情况下,用于预测任务的数据集可能并不容易获取。在某些情况下,所需的数据当前并未被收集。在这种情况下,构建一个在类似数据集上表现良好的模型通常是说服利益相关者建立新的数据收集流水线或促进现有流水线访问的最佳方式。
当涉及到公开数据时,新的数据源和集合经常出现。以下是我发现有用的一些:
-
互联网档案馆维护着包括网站数据、视频和书籍在内的一组数据集。
-
subreddit r/datasets 致力于分享数据集。
-
Kaggle 的数据集页面提供多个领域的大量选择。
-
UCI 机器学习库是一个庞大的 ML 数据集资源。
-
Google 的数据集搜索涵盖了一个大型可搜索的数据集索引。
-
Common Crawl从全网抓取和存档数据,并公开发布结果。
-
Wikipedia 也有一个不断更新的ML 研究数据集列表。
对于大多数用例来说,这些来源中的一个将为你提供足够接*你所需的数据集。
在这个相关数据集上训练一个模型将能够快速原型化和验证你的结果。在某些情况下,你甚至可以在相关数据集上训练一个模型,并将其性能部分转移到最终数据集上(更多内容请参见第 4 章)。
一旦确定了要从哪个数据集开始,就是时候把注意力转向模型了。虽然简单地从头开始构建自己的流程可能很诱人,但至少观察其他人的做法通常也是值得的。
开源代码
搜索现有代码可以实现两个高级目标。它让我们看到其他人在进行类似建模时面临的挑战,并揭示了给定数据集可能存在的问题。因此,我建议寻找既处理您产品目标的管道,又处理您选择的数据集的代码。如果找到一个例子,第一步将是自己复现其结果。
我见过许多数据科学家尝试利用他们在网上找到的机器学习代码,却发现他们无法将给定的模型训练到作者声称的类似精度水*。因为新方法并不总是伴随着良好文档和功能良好的代码,机器学习的结果往往难以复现,因此应始终进行验证。
类似于您搜索数据的方式,找到类似代码库的一个好方法是将问题抽象为其输入和输出类型,并找到处理具有类似类型问题的代码库。
例如,当试图从网站截图生成 HTML 代码时,论文作者 Tony Beltramelli 意识到他的问题归结为将图像转换为序列。他利用了现有的体系结构和最佳实践,这些实践来自一个更成熟的领域,并且还能从图像生成序列,这意味着图像字幕!这使他在一个全新的任务上获得了出色的结果,并利用了相邻应用多年的工作成果。
一旦您查看了数据和代码,您就可以继续前进了。理想情况下,这个过程已经给了您一些指导,让您开始工作并获得对问题更加细致的理解。让我们总结一下,在寻找先前工作后您可能会遇到的情况。
将两者结合起来
正如我们刚刚讨论的,利用现有的开源代码和数据集可以帮助加快实施过程。在最坏的情况下,如果没有现有模型在一个开放数据集上表现良好,那么您现在至少知道这个项目将需要大量的建模和/或数据收集工作。
如果您找到了一个解决类似任务的现有模型,并成功在其原始训练数据集上训练它,那么剩下的就是将其调整到您的领域。为此,我建议按照以下连续步骤进行:
-
找到一个类似的开源模型,最好是与其训练的数据集配对,并尝试自己复现训练结果。
-
一旦您复现了结果,请找一个与您使用情况更接*的数据集,并尝试在该数据集上训练之前的模型。
-
一旦您将数据集集成到训练代码中,就是时候使用您定义的指标来评估您的模型表现,并开始迭代。
我们将探讨每一个步骤的缺陷及其如何克服,从第二部分开始。现在,让我们回到案例研究,回顾刚刚描述的过程。
ML 编辑计划
让我们检查常见的写作建议,并搜索 ML 编辑的候选数据集和模型。
编辑的初始计划
我们应该从基于常见写作指南的启发式方法开始实施。我们将通过搜索现有的写作和编辑指南来收集这些规则,比如在“最简单的方法:成为算法”中描述的那些。
我们理想的数据集应该包括问题及其相关的质量。首先,我们应该快速找到一个更容易获取的类似数据集。根据这个数据集的表现,如果需要的话,我们将扩展和深化我们的搜索。
社交媒体帖子和在线论坛是与质量指标相关的文本的好例子。由于大多数这些指标存在以支持有用内容为目的,它们通常包括“赞”或“点赞”等质量指标。
Stack Exchange,一个问答社区网络,是一个流行的问答网站。同时,在互联网档案馆上有整个匿名化的 Stack Exchange 数据备份,这是我们之前提到的数据来源之一。这是一个很好的数据集来开始研究。
我们可以通过使用 Stack Exchange 的问题来构建一个初始模型,并尝试根据其内容预测问题的点赞分数。我们还将利用这个机会浏览数据集并标记它,试图找出模式。
我们想要构建的模型试图准确分类文本质量,然后提供写作建议。有许多开源模型用于文本分类;请查看关于这个主题的这个流行的 Python ML 库 scikit-learn 教程。
一旦我们有了一个工作的分类器,我们将讨论如何利用它来做推荐,在第七章中。
现在我们有了一个潜在的初始数据集,让我们过渡到模型,并决定从哪里开始。
始终从一个简单的模型开始
本章的一个重要收获是,建立初始模型和数据集的目的是产生信息丰富的结果,以指导进一步的建模和数据收集工作,以实现更有用的产品。
通过从一个简单的模型开始并提取 Stack Overflow 问题成功的趋势,我们可以快速测量性能并进行迭代。
试图从头开始建立一个完美模型的相反方法在实践中是行不通的。这是因为 ML 是一个迭代过程,其中取得进展的最快方式是看模型如何失败。我们将在第三部分中更详细地探讨这个迭代过程。
然而,我们应该记住每种方法的注意事项。例如,问题收到的关注度取决于远比问题质量更多的因素。帖子的上下文、发布社区、发布者的知名度、发布时间以及其他许多细节都非常重要,而这些初始模型可能会忽略。为了考虑这些因素,我们将限制数据集的范围到一部分社区。我们的第一个模型将忽略与帖子相关的所有元数据,但如果有必要的话,我们将考虑将其纳入。
因此,我们的模型使用了通常被称为弱标签的标签,这种标签只与所需输出略有关联。随着我们分析模型的表现,我们将确定这个标签是否包含足够的信息以便于实用。
我们有了一个起点,现在可以决定如何进展。在机器学习中,要实现稳定的进展通常似乎很困难,因为建模具有不可预测的方面。很难事先知道特定建模方法将成功到何种程度。因此,我想分享一些小贴士,帮助您实现稳定的进展。
要实现稳定的进展:从简开始
值得重申的是,机器学习中的许多挑战与软件中最大的挑战之一相似——抵制构建尚不需要的部分的冲动。许多机器学习项目失败是因为它们依赖于初始数据获取和模型构建计划,并且不定期评估和更新此计划。由于机器学习的随机性质,极其难以预测特定数据集或模型能走多远。
因此,从能够满足您需求的最简单模型开始,构建一个包含此模型的端到端原型,并根据产品目标评估其性能,这是至关重要的。
从简单的流水线开始
在绝大多数情况下,查看初始数据集上简单模型的表现是决定下一步应该解决哪个任务的最佳方法。然后,目标是为每个后续步骤重复这种方法,进行小幅增量改进,这样易于跟踪,而不是试图一次性构建完美模型。
为此,我们需要构建一个可以接收数据并返回结果的流水线。对于大多数机器学习问题,实际上有两个独立的流水线需要考虑。
训练
要使您的模型能够进行准确的预测,首先需要对其进行训练。
训练流水线将摄入所有您想要训练的标记数据(对于某些任务,数据集可能非常庞大,无法放在单台机器上),并将其传递给模型。然后,它在数据集上训练该模型,直到达到令人满意的性能。最常见的情况是,训练流水线用于训练多个模型,并在保留验证集上比较它们的性能。
推断
这是您生产中的管道。它将训练好的模型结果提供给用户。
在高层次上,推理管道从接受输入数据并对其进行预处理开始。预处理阶段通常包括多个步骤。最常见的步骤包括清理和验证输入数据,生成模型所需的特征,并将数据格式化为适合机器学习模型的数值表示。在更复杂的系统中,管道通常还需要获取模型需要的其他信息,例如存储在数据库中的用户特征。然后,管道将示例输入到模型中,应用任何后处理逻辑,并返回结果。
图 2-4 显示了典型推理和训练管道的流程图。理想情况下,清洁和预处理步骤对训练和推理管道应该是相同的,以确保训练好的模型在推理时接收到具有相同格式和特征的数据。

图 2-4. 训练和推理管道是互补的
不同模型的管道将考虑到不同的问题,但总体基础设施保持相对稳定。这就是为什么从构建端到端的训练和推理管道开始,快速评估莫妮卡·罗加蒂提到的瓶颈影响的价值所在“莫妮卡·罗加蒂:如何选择和优先处理机器学习项目”。
大多数管道具有类似的高级结构,但由于数据集结构的差异,这些功能本身通常没有共同点。我们来看一下编辑器的管道来加以说明。
机器学习编辑器的管道
对于编辑器,我们将使用 Python 构建端到端的训练和推理管道,Python 是机器学习中常用的语言选择。在这个第一个原型中,我们的目标是构建一个端到端的管道,而不是过于关注其完美性。
正如在任何需要时间的工作中应该做的那样,我们可以并会重新审视其中的部分以改进它们。对于训练,我们将编写一个相当标准的管道,适用于许多机器学习问题,并且有一些主要的功能,主要包括:
-
加载数据记录。
-
通过删除不完整的记录和在必要时输入缺失值来清洁数据。
-
以一种可以被模型理解的方式预处理和格式化数据。
-
删除一组数据,这些数据不会被用来训练,而是用来验证模型的结果(验证集)。
-
对给定数据子集进行模型训练,并返回训练好的模型和总结统计信息。
对于推理,我们将利用一些来自训练管道的功能,并编写一些自定义的功能。理想情况下,我们需要的功能包括:
-
加载训练好的模型并将其保存在内存中(以提供更快的结果)
-
将预处理(与训练相同)
-
收集任何相关的外部信息
-
将一个例子通过模型传递(推断函数)
-
将进行后处理,以在提供给用户之前清理结果
通常最容易将一个流水线可视化为流程图,例如 图 2-5 所示的流程图。

图 2-5. 编辑器的流水线
此外,我们将编写各种分析和探索函数,以帮助我们诊断问题,例如:
-
可视化模型表现最佳和最差的示例的函数
-
探索数据的函数
-
探索模型结果的函数
许多流水线包含验证模型输入和检查其最终输出的步骤。这些检查有助于调试,正如您将在 第 10 章 中看到的,并通过捕捉任何不良结果在显示给用户之前帮助保证应用程序的质量标准。
记住,在使用机器学习时,模型在未见数据上的输出通常是不可预测的,并且不总是令人满意。因此,重要的是要认识到模型不总是有效,并在此基础上设计系统以应对可能的错误。
结论
我们现在已经了解了如何定义核心指标,使我们能够比较完全不同的模型,并理解它们之间的权衡。我们涵盖了加快建立您的前几个流水线过程的资源和方法。然后,我们概述了构建每个流水线所需的概述,以获取一组初始结果。
现在我们已经将一个问题构建成了一个机器学习问题,有了衡量进展的方法和一个初始计划。现在是深入实施的时候了。
在 第 II 部分 中,我们将深入探讨如何构建第一个流水线,并探索和可视化初始数据集。
第二部分:构建工作管道
由于研究、训练和评估模型是一个耗时的过程,走错方向在机器学习中可能代价高昂。这就是为什么本书侧重于减少风险和确定最高优先级以进行工作。
虽然第一部分侧重于计划以最大化我们的速度和成功的机会,本章将深入实施。正如图 II-1 所示,在机器学习和大部分软件工程中,你应该尽快达到最小可行产品(MVP)。本节将仅覆盖此内容:快速建立管道并评估它的最快方式。
改进这个模型将是本书第三部分的重点。

图 II-1. 构建您的第一个管道的正确方式(经 Henrik Kniberg 许可重现)
我们将分两步建立我们的初始模型:
第三章
本章中,我们将构建应用程序的结构和脚手架。这将涉及建立一个管道来接受用户输入并返回建议,以及一个单独的管道来训练我们的模型,然后再使用它们。
第四章
本章将侧重于收集和检查初始数据集。这里的目标是快速识别数据中的模式,并预测哪些模式对我们的模型是预测性和有用的。
第三章:建立您的第一个端到端管道
在第 I 部分中,我们首先介绍了如何从产品需求到候选建模方法。然后,我们进入了规划阶段,并描述了如何找到相关资源并利用它们来制定一个建设初步计划的初始计划。最后,我们讨论了建立一个功能系统的初步原型是取得进展的最佳途径。这是我们将在本章中涵盖的内容。
这个第一次迭代设计的目的是有意缺乏亮点。它的目标是使我们能够将管道的所有部分放在一起,以便我们可以优先改进哪些部分。拥有一个完整的原型是识别 Monica Rogati 在“Monica Rogati:如何选择和优先处理 ML 项目”中描述的影响瓶颈的最简单方法。
让我们从构建最简单的管道开始,这个管道可以从输入产生预测。
最简单的脚手架
在“从简单管道开始”中,我们描述了大多数 ML 模型由两个管道组成,训练和推断。训练允许我们生成高质量的模型,而推断则是为用户提供结果。有关训练和推断之间区别的更多信息,请参见“从简单管道开始”。
对于应用程序的第一个原型,我们将专注于能够向用户提供结果。这意味着在我们描述的两个管道中的一个——第二章中的推断管道,我们将从推断管道开始。这将使我们能够快速检查用户如何与模型输出交互,从而收集有用的信息,以便更轻松地训练模型。
如果我们只专注于推断,我们将暂时忽略训练。既然我们不训练模型,我们可以写一些简单的规则。编写这些规则或启发式经常是一个很好的入门方式。这是快速构建原型的最快方式,使我们可以立即看到完整应用程序的简化版本。
尽管如果我们打算在书中的后面实施 ML 解决方案,这可能看起来是多余的,但它是一个关键的推动因素,使我们直面问题并制定一个关于如何最好解决它的初始假设集。
建立、验证和更新关于如何对数据进行建模的最佳方式的假设是迭代模型构建过程的核心部分,这甚至在我们构建第一个模型之前就开始了!
注意
这里有几个我在指导 Insight 数据科学院学员时见过的项目中使用的优秀启发式的例子。
-
代码质量估计: 当构建一个旨在预测编程者在 HackerRank(一个竞争性编程网站)上表现良好的模型时,Daniel 首先计算开放和关闭的括号、方括号和花括号的数量。
在大多数有效的工作代码中,开放和闭合括号的计数是匹配的,因此这一规则被证明是一个相当强大的基线。此外,这使他产生了使用抽象语法树捕获更多关于代码结构信息的直觉。
-
树木计数: 当试图从卫星图像中统计城市中的树木时,Mike 在查看了一些数据后,首先制定了一个规则,根据给定图像中绿色像素的比例来估算树木密度。
结果表明,这种方法适用于分散的树木,但在树木林地时失败了。同样,这有助于定义接下来的建模步骤,重点是构建一个可以处理密集树木群的流水线。
大多数 ML 项目应该从类似的启发式方法开始。关键在于记住根据专家知识和数据探索设计它,并使用它来确认初始假设并加速迭代。
一旦您有了启发式方法,就是创建一个可以收集输入、预处理它、应用规则并提供结果的流水线的时候了。这可以简单到您可以从终端调用的 Python 脚本,也可以是收集用户摄像头输入然后提供实时结果的 Web 应用程序。
这里的重点是为您的产品做与您的 ML 方法相同的事情,尽可能简化它,并构建一个简单的功能版本。这通常被称为 MVP(最小可行产品),是尽快获得有用结果的经过测试的方法。
ML 编辑器的原型
对于我们的 ML 编辑器,我们将利用常见的编辑建议来制定关于什么是好问题和坏问题的几条规则,并向用户显示这些规则的结果。
对于从命令行接收用户输入并返回建议的我们项目的最小版本,我们只需要编写四个函数,如下所示:
input_text = parse_arguments()
processed = clean_input(input_text)
tokenized_sentences = preprocess_input(processed)
suggestions = get_suggestions(tokenized_sentences)
让我们深入了解每一个!我们将保持参数解析器简单,并从用户那里获取文本字符串,不带任何选项。您可以在本书的GitHub 代码库中找到示例和所有其他代码示例的源代码。
解析和清理数据
首先,我们简单地解析从命令行接收的数据。这在 Python 中相对简单地编写。
def parse_arguments():
"""
:return: The text to be edited
"""
parser = argparse.ArgumentParser(
description="Receive text to be edited"
)
parser.add_argument(
'text',
metavar='input text',
type=str
)
args = parser.parse_args()
return args.text
每当模型根据用户输入运行时,您都应该开始验证和验证它!在我们的情况下,用户将输入数据,因此我们将确保他们的输入包含我们可以解析的字符。为了清理我们的输入,我们将删除非 ASCII 字符。这不应过多限制我们用户的创造力,并且允许我们对文本中的内容做出合理的假设。
def clean_input(text):
"""
:param text: User input text
:return: Sanitized text, without non ascii characters
"""
# To keep things simple at the start, let's only keep ASCII characters
return str(text.encode().decode('ascii', errors='ignore'))
现在,我们需要对输入进行预处理并提供建议。为了开始工作,我们将依赖我们在“最简单的方法:成为算法”中提到的一些关于文本分类的现有研究。这将涉及计算诸如“告诉”和“说”的词语频率,并计算音节、单词和句子的摘要统计数据,以估计句子的复杂性。
要计算词级别的统计数据,我们需要能够从句子中识别单词。在自然语言处理领域,这被称为分词。
文本分词
分词并不简单,大多数你能想到的简单方法,比如根据空格或句号将输入拆分为单词,将在实际文本中失败,因为单词分隔方式多种多样。例如,考虑斯坦福大学的NLP 课程提供的这个例子:
“奥尼尔先生认为男孩们关于智利首都的故事并不有趣。”
大多数简单的方法在处理这句话时会失败,因为其中包含具有不同含义的句点和撇号。我们不会自己构建分词器,而是会利用nltk,这是一个流行的开源库,可以通过两个简单步骤来完成,如下所示:
def preprocess_input(text):
"""
:param text: Sanitized text
:return: Text ready to be fed to analysis, by having sentences and
words tokenized
"""
sentences = nltk.sent_tokenize(text)
tokens = [nltk.word_tokenize(sentence) for sentence in sentences]
return tokens
一旦我们的输出被预处理,我们可以用它来生成特征,以帮助评估问题的质量。
生成特征
最后一步是编写一些规则,可以用来向用户提供建议。对于这个简单的原型,我们将首先计算几个常见动词和连接词的频率,然后统计副词的使用情况,并计算Flesch 可读性分数。然后,我们将向用户返回这些指标的报告:
def get_suggestions(sentence_list):
"""
Returns a string containing our suggestions
:param sentence_list: a list of sentences, each being a list of words
:return: suggestions to improve the input
"""
told_said_usage = sum(
(count_word_usage(tokens, ["told", "said"]) for tokens in sentence_list)
)
but_and_usage = sum(
(count_word_usage(tokens, ["but", "and"]) for tokens in sentence_list)
)
wh_adverbs_usage = sum(
(
count_word_usage(
tokens,
[
"when",
"where",
"why",
"whence",
"whereby",
"wherein",
"whereupon",
],
)
for tokens in sentence_list
)
)
result_str = ""
adverb_usage = "Adverb usage: %s told/said, %s but/and, %s wh adverbs" % (
told_said_usage,
but_and_usage,
wh_adverbs_usage,
)
result_str += adverb_usage
average_word_length = compute_total_average_word_length(sentence_list)
unique_words_fraction = compute_total_unique_words_fraction(sentence_list)
word_stats = "Average word length %.2f, fraction of unique words %.2f" % (
average_word_length,
unique_words_fraction,
)
# Using HTML break to later display on a webapp
result_str += "<br/>"
result_str += word_stats
number_of_syllables = count_total_syllables(sentence_list)
number_of_words = count_total_words(sentence_list)
number_of_sentences = len(sentence_list)
syllable_counts = "%d syllables, %d words, %d sentences" % (
number_of_syllables,
number_of_words,
number_of_sentences,
)
result_str += "<br/>"
result_str += syllable_counts
flesch_score = compute_flesch_reading_ease(
number_of_syllables, number_of_words, number_of_sentences
)
flesch = "%d syllables, %.2f flesch score: %s" % (
number_of_syllables,
flesch_score,
get_reading_level_from_flesch(flesch_score),
)
result_str += "<br/>"
result_str += flesch
return result_str
现在,我们可以从命令行调用我们的应用程序并实时查看其结果。虽然现在还不是非常有用,但我们有了一个可以测试和迭代的起点,接下来我们将做这些工作。
测试您的工作流程
现在我们已经构建了这个原型,我们可以测试我们对问题框架的假设以及我们提出的解决方案的实用性。在本节中,我们将评估我们初始规则的客观质量,并检查我们是否以有用的方式呈现输出。
正如 Monica Rogati 之前分享的,“即使你的模型成功了,你的产品也可能一无是处。”如果我们选择的方法在测量问题质量方面表现出色,但我们的产品没有为用户提供改善写作的建议,那么尽管我们的方法质量很高,产品也将毫无用处。在审视我们的完整流程时,让我们评估当前用户体验的实用性以及我们手工制作模型的结果。
用户体验
首先,让我们独立于我们模型的质量来检查我们的产品使用体验有多满意。换句话说,如果我们想象最终将获得一个表现足够好的模型,这是向用户展示结果最有用的方式吗?
例如,如果我们正在进行树木普查,我们可能希望将我们的结果呈现为对整个城市长期分析的摘要。我们可能希望包括报告树木数量,以及按社区细分的统计数据,以及对黄金标准测试集误差的度量。
换句话说,我们希望确保我们呈现的结果是有用的(或者如果我们改进我们的模型会变得更有用)。当然,反过来,我们也希望我们的模型表现良好。这是我们将评估的下一个方面。
建模结果
我们提到了在“衡量成功”中集中精力选择正确的度量标准的价值。早期拥有一个可工作的原型将使我们能够识别和迭代我们选择的度量标准,以确保它们代表产品成功。
例如,如果我们正在构建一个帮助用户搜索附*租车的系统,我们可能会使用像折扣累积增益(DCG)这样的度量标准。DCG 通过输出一个评分来衡量排名质量,在最相关的项目早于其他项目时得分最高(详见DCG 的维基百科文章了解更多关于排名指标的信息)。在最初构建我们的工具时,我们可能假设我们希望至少有一个有用的建议出现在前五个结果中。因此,我们使用 DCG@5 来评估我们的模型。然而,当用户试用这个工具时,我们可能注意到用户只考虑前三个显示的结果。在这种情况下,我们应该将我们的成功度量从 DCG@5 更改为 DCG@3。
考虑用户体验和模型性能的目标在于确保我们处理最具影响力的方面。如果用户体验差,改进模型是没有帮助的。事实上,你可能会意识到最好采用完全不同的模型!让我们看两个例子。
寻找影响瓶颈
同时查看建模结果和产品当前展示的目标是确定下一个解决挑战的方向。大多数情况下,这意味着在我们向用户展示结果的方式上进行迭代(这可能意味着改变我们训练模型的方式)或者通过识别关键失败点来改进模型性能。
虽然我们将在第三部分更深入地进行错误分析,但我们应该确定失败模式和适当的解决方式。重要的是确定要解决的最具影响力的任务是在建模还是产品领域,因为它们各自需要不同的修复方法。让我们看一个每个方面的例子:
在产品方面
假设你建立了一个模型,该模型查看研究论文的图像,并预测它们是否会被顶级会议接受(参见贾斌黄的论文"Deep Paper Gestalt,",该论文解决了这个问题)。然而,你注意到仅返回用户一个拒绝的概率并不是最令人满意的输出。在这种情况下,改进你的模型将没有帮助。专注于从模型中提取建议,以便我们可以帮助用户改进他们的论文并增加被接受的机会,这才是有意义的。
在模型方面
你建立了一个信用评分模型,并注意到在其他因素相同的情况下,它会给某个特定种族群体分配更高的违约风险。这很可能是由于你使用的训练数据中存在偏见,因此你应该收集更具代表性的数据,并建立新的清理和增强流水线来尝试解决这个问题。在这种情况下,无论你如何呈现结果,模型都需要修正。类似这样的例子很常见,这也是为什么你应该总是深入探究聚合指标以及分析模型对数据不同切片影响的原因。这就是我们在第五章中将要做的事情。
为了进一步说明这一点,让我们为我们的 ML 编辑器进行这个练习。
ML 编辑器原型评估
让我们看看我们的初始流水线在用户体验和模型性能方面的表现如何。让我们首先输入一些内容到我们的应用程序中。我们将开始测试一个简单问题,一个复杂问题和一个完整段落。
由于我们使用的是阅读易度分数,我们希望我们的工作流能为简单的句子返回高分,为复杂句子返回低分,并提出改进段落的建议。让我们实际运行几个示例通过我们的原型。
简单问题:
$ python ml_editor.py "Is this workflow any good?"
Adverb usage: 0 told/said, 0 but/and, 0 wh adverbs
Average word length 3.67, fraction of unique words 1.00
6 syllables, 5 words, 1 sentences
6 syllables, 100.26 flesch score: Very easy to read
复杂问题:
$ python ml_editor.py "Here is a needlessly obscure question, that"\
"does not provide clearly which information it would"\
"like to acquire, does it?"
Adverb usage: 0 told/said, 0 but/and, 0 wh adverbs
Average word length 4.86, fraction of unique words 0.90
30 syllables, 18 words, 1 sentences
30 syllables, 47.58 flesch score: Difficult to read
之前你已经认识到的整个段落:
$ python ml_editor.py "Ideally, we would like our workflow to return a positive"\
" score for the simple sentence, a negative score for the convoluted one, and "\
"suggestions for improving our paragraph. Is that the case already?"
Adverb usage: 0 told/said, 1 but/and, 0 wh adverbs
Average word length 4.03, fraction of unique words 0.76
52 syllables, 33 words, 2 sentences
52 syllables, 56.79 flesch score: Fairly difficult to read
让我们使用我们刚刚定义的两个方面来检查这些结果。
模型
目前还不清楚我们的结果是否与我们认为的优质写作相符。复杂的句子和整个段落接受到了类似的可读性评分。现在,我将首先承认,我的散文有时可能很难阅读,但与之前测试的复杂句子相比,之前的段落更易理解。
我们从文本中提取的属性未必与“好写作”最相关。这通常是因为我们没有清晰地定义成功:在给定两个问题的情况下,我们如何说一个比另一个更好?在下一章中构建数据集时,我们将更清晰地定义这一点。
正如预期的那样,我们还有一些建模工作要做,但我们是否正在以有用的方式呈现结果?
用户体验
根据之前展示的结果,立即出现了两个问题。我们返回的信息既过于冗余又不相关。我们产品的目标是向用户提供可操作的建议。特征和可读性分数是质量指标,但不会帮助用户决定如何改进他们的提交。我们可能希望将我们的建议归纳为一个单一的分数,以及行动建议来改进它。
例如,我们可以建议一般性的更改,比如使用更少的副词,或者在更细粒度的级别上建议单词和句子级别的更改。理想情况下,我们可以通过突出或下划线显示需要用户注意的输入部分来呈现结果。我已经添加了一个如何在图 3-1 中看起来的模拟。

图 3-1. 更具操作性的写作建议
即使我们不能直接在输入字符串中突出显示建议,我们的产品仍然可以从提供类似于图 3-1 右侧的更具操作性建议中受益,这些建议比分数列表更具操作性。
结论
我们已经建立了一个初始推理原型,并用它来评估我们的启发式和产品工作流的质量。这使我们能够缩小我们的绩效标准范围,并在向用户展示结果的方式上进行迭代。
对于 ML 编辑器,我们已经学到,通过提供具有操作性的建议来改善用户体验,并通过查看数据来更清晰地定义好问题的特征生成和建模方法,可以改进我们的建模方法。
在前三章中,我们利用产品目标来定义初始方法,探索现有资源以制定我们的方法计划,并建立了一个初始原型来验证我们的计划和假设。
现在,是时候深入研究 ML 项目中经常被忽视的部分了——探索我们的数据集。在第四章中,我们将看到如何收集一个初始数据集,评估其质量,并逐步标记其子集,以帮助指导我们的特征生成和建模决策。
第四章:获取初始数据集
一旦您制定了解决产品需求并构建了初始原型以验证您提议的工作流程和模型是否合理的计划,就是深入研究您的数据集的时候了。我们将利用我们发现的内容来指导我们的建模决策。通常情况下,深入了解您的数据会带来最大的性能改进。
在本章中,我们将首先看看如何高效评估数据集的质量。然后,我们将涵盖如何对数据进行向量化以及如何使用这种向量化表示更高效地标记和检查数据集。最后,我们将探讨这种检查如何指导特征生成策略。
让我们从发现数据集并评估其质量开始。
迭代数据集
构建机器学习产品的最快方法是快速构建、评估和迭代模型。数据集本身是模型成功的核心组成部分。这就是为什么数据收集、准备和标记应该被视为一个迭代过程,就像建模一样。从您可以立即获取的简单数据集开始,并根据您所学到的知识进行改进。
这种对数据的迭代方法一开始可能会令人困惑。在机器学习研究中,性能通常是报告在社区中作为基准使用的标准数据集上的。因此,这些数据集是不可变的。在传统软件工程中,我们为程序编写确定性规则,因此我们将数据视为接收、处理和存储的内容。
机器学习工程结合了工程和机器学习,以便构建产品。因此,我们的数据集只是允许我们构建产品的另一种工具。在机器学习工程中,选择初始数据集、定期更新和增强它通常是大部分的工作。这种工作流程在研究和行业之间的差异在图 4-1 中有所体现。

图 4-1。研究中的数据集是固定的,但在工业中是产品的一部分
将数据视为您可以(并且应该)迭代、更改和改进的产品的一部分,对于行业新人来说通常是一种重大范式转变。然而,一旦你习惯了,数据将成为您开发新模型的最佳灵感来源,并且当事情出错时您寻找答案的第一处去处。
进行数据科学
我见过整理数据集的过程比我能数的次数更多次成为构建机器学习产品的主要障碍。这部分原因是因为相对缺乏有关该主题的教育(大多数在线课程提供数据集并侧重于模型),这导致许多从业者害怕这部分工作。
很容易认为处理数据是在玩乐模型之前要解决的烦心事,但模型只是从现有数据中提取趋势和模式的一种方法。确保我们使用的数据展现出足够预测模型利用的模式(并检查它是否包含明显偏差)因此是数据科学家工作的基本部分(事实上,您可能已经注意到角色的名称并不是模型科学家)。
本章将专注于这一过程,从收集初始数据集到检查和验证其在机器学习中的适用性。让我们从有效地探索数据集开始,以评估其质量。
探索你的第一个数据集
那么我们如何开始探索初始数据集呢?当然,第一步是收集数据集。这是我看到从业者最常陷入困境的地方,因为他们寻找完美的数据集。记住,我们的目标是从中获得初步结果的简单数据集。与机器学习中的其他事物一样,从简单开始,逐步扩展。
高效率,从小处开始
对于大多数机器学习问题,更多的数据可能会导致更好的模型,但这并不意味着你应该从最大可能的数据集开始。在开始项目时,一个小数据集可以让你轻松检查和理解数据及其更好地建模方式。你应该瞄准一个易于处理的初始数据集。只有一旦你制定了策略,扩展到更大的规模才有意义。
如果你在一个存储有数百万兆字节数据的集群中工作,你可以开始提取一个在本地机器内存中适合的均匀抽样子集。例如,如果您想开始一个副业项目,试图识别驶过你家前面的汽车品牌,请从街上几十辆汽车的图像开始。
一旦你看到你的初始模型表现及其困难之处,你将能够以经过明智决策的方式迭代你的数据集!
你可以在诸如Kaggle或Reddit等*台上找到许多现有的数据集,或者自行收集一些示例,可以通过网页抓取,利用大型开放数据集(如Common Crawl site)或生成数据!欲了解更多信息,请参阅“开放数据”。
收集和分析数据不仅是必要的,尤其是在项目早期阶段,它还能加速你的进展。查看你的数据集并了解其特征是提出良好建模和特征生成管道的最简单方法。
大多数从业者高估了工作模型的影响,低估了处理数据的价值,因此我建议始终努力纠正这种趋势,偏向于查看数据。
在检查数据时,探索性地识别趋势是一个很好的方法,但你不应该止步于此。如果您的目标是构建机器学习产品,您应该问自己如何以自动化的方式利用这些趋势。这些趋势如何帮助您驱动一个自动化的产品?
洞察力与产品
一旦您获得了数据集,现在是时候深入探讨其内容了。当我们这样做时,让我们记住数据探索的目的分为分析目的和产品构建目的两种。虽然两者都旨在从数据趋势中提取和理解,但前者关注从趋势中创造洞察力(例如,学习到大多数网站的欺诈登录发生在星期四,并且来自西雅图地区),而后者则是利用趋势来构建功能(使用登录尝试的时间和其 IP 地址来构建防止欺诈账户登录的服务)。
尽管差异可能看似微妙,但在产品构建的情况下,这导致了额外的复杂性层级。我们需要确信我们所看到的模式将适用于将来收到的数据,并量化我们在训练数据和预期收到的生产数据之间的差异。
对于欺诈预测来说,注意欺诈登录的季节性是第一步。然后,我们应该利用观察到的季节性趋势来估计我们需要多频繁地基于最*收集到的数据来训练我们的模型。在本章的后续部分,随着我们更深入地探索数据,我们将深入探讨更多示例。
在注意到预测趋势之前,我们应该首先检查质量。如果我们选择的数据集不符合质量标准,我们应该在进行建模之前对其进行改进。
数据质量评估表
在本节中,我们将涵盖首次使用新数据集时需要检查的一些方面。每个数据集都带有其自己的偏见和奇怪之处,需要不同的工具来理解,因此编写一份涵盖您可能想要在数据集中查找的任何内容的全面规则已超出本书的范围。然而,在初次接触数据集时,有几个类别是值得注意的。让我们从格式开始。
数据格式
数据集是否已经格式化,使您拥有清晰的输入和输出,或者是否需要额外的预处理和标记?
例如,当构建一个试图预测用户是否会点击广告的模型时,常见的数据集将包含一个给定时间段内所有点击的历史记录。您需要转换此数据集,使其包含向用户展示广告的多个实例以及用户是否点击了广告。您还希望包括您认为您的模型可以利用的用户或广告的任何特征。
如果你得到了一个已经处理或聚合过的数据集,你应该验证你理解数据处理的方式。比如,如果你得到的列包含了*均转化率,你能计算出这个转化率并验证它与提供的数值是否一致吗?
在某些情况下,你将无法访问到重现和验证预处理步骤所需的信息。在这些情况下,查看数据质量将帮助你确定哪些特征是可信的,哪些是最好忽略的。
数据的质量
在开始建模之前,检查数据集的质量至关重要。如果你知道一个关键特征的一半数值是缺失的,你就不会花几个小时来调试一个模型,试图弄清楚为什么模型表现不佳。
数据可以有许多质量低下的方式。它可能缺失,可能不精确,甚至可能损坏。准确评估其质量不仅能让你估计合理的性能水*,还能更容易地选择潜在的特征和模型使用。
如果你正在使用用户活动日志来预测在线产品的使用情况,你能估计有多少事件没有被记录下来吗?对于你已经记录的事件,有多少只包含用户信息的一个子集?
如果你正在处理自然语言文本,你会如何评价文本的质量?例如,是否有很多难以理解的字符?拼写错误或不一致的情况很多吗?
如果你处理的是图像,它们的清晰度足够高,你能自己执行任务吗?如果你在图像中难以检测一个物体,你觉得你的模型会遇到困难吗?
总的来说,你的数据中有多大比例是嘈杂或不正确的?有多少输入对你来说是难以解释或理解的?如果数据有标签,你是否倾向于同意它们,还是经常质疑它们的准确性?
例如,我曾经参与过一些从卫星图像中提取信息的项目。在最好的情况下,这些项目可以访问到带有相应注释的图像数据集,标记出感兴趣的对象,如田地或飞机。然而,在某些情况下,这些注释可能是不准确的,甚至缺失的。这些错误对任何建模方法都有显著影响,因此及早了解这些问题非常重要。我们可以通过自己为初始数据集标注标签或找到一个可以使用的弱标签来处理缺失的标签,但前提是我们要在时间上有所察觉。
在验证数据的格式和质量之后,还有一个额外的步骤可以帮助及早发现问题:检查数据的数量和特征分布。
数据的数量和分布
让我们评估一下我们是否有足够的数据,以及特征值是否在合理范围内。
我们有多少数据?如果我们有大量数据集,我们应选择一个子集来开始分析。另一方面,如果我们的数据集太小或某些类别代表性不足,那么我们训练的模型可能会像我们的数据一样存在偏差。避免这种偏差的最佳方法是通过数据收集和增强增加数据的多样性。您衡量数据质量的方式取决于您的数据集,但 表 4-1 涵盖了一些问题,供您开始使用。
表 4-1. 数据质量评分表
| 质量 | 格式 | 数量和分布 | |
|---|---|---|---|
| 有任何相关字段是空的吗? | 您的数据需要多少预处理步骤? | 您有多少示例? | |
| 存在测量错误的可能性吗? | 您能在生产中以相同的方式预处理吗? | 每个类别有多少示例?有任何缺失的吗? |
作为一个实际例子,当构建一个模型自动将客户支持电子邮件分类到不同的专业领域时,我与之合作的数据科学家 Alex Wahl 收到了九个不同类别的示例,每个类别只有一个示例。这样的数据集对于模型学习来说太小了,所以他将大部分精力集中在了一个 数据生成策略 上。他使用常见表述的模板为每个九个类别生成了成千上万个示例,模型可以从中学习。通过这种策略,他成功将流水线的准确性提升到比仅仅从九个示例中尝试构建足够复杂的模型要高得多的水*。
让我们将这个探索过程应用到我们为 ML 编辑器选择的数据集上,并估计其质量!
ML 编辑器数据检查
对于我们的 ML 编辑器,我们最初决定使用匿名化的 Stack Exchange 数据转储 作为数据集。Stack Exchange 是一个以哲学或游戏等主题为中心的问答网站网络。数据转储包含许多存档,每个存档对应 Stack Exchange 网络中的一个网站。
对于我们的初始数据集,我们选择了一个看起来足够广泛的网站,以便从中构建有用的启发式。乍一看,写作社区 看起来很合适。
每个网站存档都以 XML 文件的形式提供。我们需要建立一个流水线来摄取这些文件,并将其转换为我们可以从中提取特征的文本。以下示例展示了 datascience.stackexchange.com 的 Posts.xml 文件:
<?xml version="1.0" encoding="utf-8"?>
<posts>
<row Id="5" PostTypeId="1" CreationDate="2014-05-13T23:58:30.457"
Score="9" ViewCount="516" Body="<p> "Hello World" example? "
OwnerUserId="5" LastActivityDate="2014-05-14T00:36:31.077"
Title="How can I do simple machine learning without hard-coding behavior?"
Tags="<machine-learning>" AnswerCount="1" CommentCount="1" />
<row Id="7" PostTypeId="1" AcceptedAnswerId="10" ... />
为了能够利用这些数据,我们需要能够加载 XML 文件,解码文本中的 HTML 标记,并以更易于分析的格式表示问题和关联数据,例如 pandas DataFrame。以下函数就是这样做的。提醒一下,本书中此函数的代码和所有其他代码都可以在本书的 GitHub 存储库中找到。
import xml.etree.ElementTree as ElT
def parse_xml_to_csv(path, save_path=None):
"""
Open .xml posts dump and convert the text to a csv, tokenizing it in the
process
:param path: path to the xml document containing posts
:return: a dataframe of processed text
"""
# Use python's standard library to parse XML file
doc = ElT.parse(path)
root = doc.getroot()
# Each row is a question
all_rows = [row.attrib for row in root.findall("row")]
# Using tdqm to display progress since preprocessing takes time
for item in tqdm(all_rows):
# Decode text from HTML
soup = BeautifulSoup(item["Body"], features="html.parser")
item["body_text"] = soup.get_text()
# Create dataframe from our list of dictionaries
df = pd.DataFrame.from_dict(all_rows)
if save_path:
df.to_csv(save_path)
return df
即使对于只包含 30,000 个问题的相对较小的数据集,此过程也需要超过一分钟的时间,因此我们将处理后的文件序列化回磁盘,只需处理一次。为此,我们可以简单地使用 pandas 的to_csv函数,如代码片段的最后一行所示。
这通常是训练模型所需的任何预处理的推荐实践。在模型优化过程之前运行的预处理代码可能会显著减慢实验。尽可能在预处理数据并将其序列化到磁盘。
一旦数据格式确定,我们可以检查前面描述的各个方面。我们接下来详细介绍的整个探索过程可以在本书的 GitHub 存储库的数据集探索笔记本中找到。
首先,我们使用df.info()来显示关于我们的 DataFrame 的摘要信息,以及任何空值。这是它的返回结果:
>>>> df.info()
AcceptedAnswerId 4124 non-null float64
AnswerCount 33650 non-null int64
Body 33650 non-null object
ClosedDate 969 non-null object
CommentCount 33650 non-null int64
CommunityOwnedDate 186 non-null object
CreationDate 33650 non-null object
FavoriteCount 3307 non-null float64
Id 33650 non-null int64
LastActivityDate 33650 non-null object
LastEditDate 10521 non-null object
LastEditorDisplayName 606 non-null object
LastEditorUserId 9975 non-null float64
OwnerDisplayName 1971 non-null object
OwnerUserId 32117 non-null float64
ParentId 25679 non-null float64
PostTypeId 33650 non-null int64
Score 33650 non-null int64
Tags 7971 non-null object
Title 7971 non-null object
ViewCount 7971 non-null float64
body_text 33650 non-null object
full_text 33650 non-null object
text_len 33650 non-null int64
is_question 33650 non-null bool
我们可以看到,我们有超过 31,000 个帖子,其中约 4,000 个帖子有一个被接受的答案。此外,我们可以注意到Body的一些值,代表帖子的内容,是空的,这似乎有些可疑。我们期望所有帖子都包含文本。快速查看具有空Body的行,很快发现它们属于数据集提供的文档中没有参考的一种帖子类型,因此我们将其移除。
让我们快速了解一下格式,看看我们是否理解它。每个帖子的PostTypeId值为 1 表示问题,2 表示答案。我们希望查看哪种类型的问题获得了高分,因为我们希望将问题的分数作为真正标签的弱标签,即问题的质量。
首先,让我们将问题与相关答案进行匹配。以下代码选择所有具有已接受答案的问题,并将它们与所述答案的文本连接起来。然后我们可以查看前几行,并验证答案是否与问题匹配。这还允许我们快速查看文本并评估其质量。
questions_with_accepted_answers = df[
df["is_question"] & ~(df["AcceptedAnswerId"].isna())
]
q_and_a = questions_with_accepted_answers.join(
df[["Text"]], on="AcceptedAnswerId", how="left", rsuffix="_answer"
)
pd.options.display.max_colwidth = 500
q_and_a[["Text", "Text_answer"]][:5]
在表 4-2 中,我们可以看到问题和答案似乎是匹配的,并且文本看起来大部分是正确的。我们现在相信我们可以将问题与其关联的答案匹配起来了。
表 4-2. 带有其关联答案的问题
| Id | body_text | body_text_answer |
|---|---|---|
| 1 | 我一直想要开始写作(完全是业余的),但每当我想要开始时,我立刻被各种问题和疑虑所困扰。\n 有没有一些关于如何开始成为作家的资源?\n 我想要一些有关提示和简单练习的东西,以便让我开始写作。\n | 当我考虑我是如何学会写作的时候,我觉得阅读对我来说是最重要的指导。这可能听起来很傻,但通过阅读写得好的报纸文章(事实、观点、科学文章,尤其是影视和音乐评论),我学会了别人是如何做这份工作的,什么是有效的,什么是无效的。在我自己的写作中,我尝试模仿我喜欢的其他人的风格。此外,通过阅读,我学到了新的知识,为我提供了更广泛的背景,这在重新… |
| 2 | 对于每种视角,哪种故事更适合?它们固有的优势或劣势是什么?\n 例如,第一人称写作时,你总是跟随一个角色,而在第三人称写作中,你可以在不同的故事线之间“跳跃”。\n | 使用第一人称讲述故事时,你意图让读者更加投入到主角身上。因为读者看到主角看到的东西,感受到主角的感受,读者会对这个角色产生情感投入。第三人称则没有这种紧密联系;读者可以产生情感投入,但不会像第一人称那样强烈。\n 与此相反,当你使用第一人称时,你不能有多个视角的主要角色,没有例… |
| 3 | 我完成了我的小说,我和所有我谈过的人都说我需要一个代理。我该如何找到一个代理?\n | 尝试找到一份代理人名单,他们在你的体裁中写作,查看他们的网站!\n 了解他们是否接受新客户。如果不接受,那么就找另一位代理。但如果接受,尝试向他们发送你小说的几章,简要说明,以及一封简短的求职信,请求他们代表你。\n 在求职信中提及你的先前出版成就。如果通过邮件发送,请提供给他们回复的方式,无论是电子邮件还是贴上邮票的地址信封。\n 代理… |
作为最后一次健全性检查,让我们看看有多少问题没有答案,有多少问题至少有一个答案,以及有多少问题有被接受的答案。
has_accepted_answer = df[df["is_question"] & ~(df["AcceptedAnswerId"].isna())]
no_accepted_answers = df[
df["is_question"]
& (df["AcceptedAnswerId"].isna())
& (df["AnswerCount"] != 0)
]
no_answers = df[
df["is_question"]
& (df["AcceptedAnswerId"].isna())
& (df["AnswerCount"] == 0)
]
print(
"%s questions with no answers, %s with answers, %s with an accepted answer"
% (len(no_answers), len(no_accepted_answers), len(has_accepted_answer))
)
3584 questions with no answers, 5933 with answers, 4964 with an accepted answer.
我们已经在已答复问题和部分答复问题以及未答复问题之间有一个相对均匀的分布。这看起来是合理的,因此我们可以足够自信地继续我们的探索。
我们理解我们数据的格式,并且有足够的数据可以开始。如果您正在进行一个项目,您当前的数据集要么太小,要么包含大多数难以解释的特征,您应该收集更多数据或尝试完全不同的数据集。
我们的数据集质量足以继续。现在是时候深入探讨它,以便为我们的建模策略提供信息。
标签以查找数据趋势
在我们的数据集中识别趋势不仅仅是关于质量的问题。这部分工作是要让我们模型的思路和结构预测的能力更强。我们将通过将数据分成不同的聚类(我将在 “聚类” 中解释)并尝试提取每个聚类中的共性来实现这一点。
以下是实际操作的逐步列表。我们将从生成数据集的摘要统计开始,然后看看如何通过利用向量化技术快速探索它。借助向量化和聚类的帮助,我们将高效地探索我们的数据集。
摘要统计
当您开始查看数据集时,通常建议查看每个特征的一些摘要统计数据。这不仅帮助您对数据集中的特征有一个总体感觉,还有助于识别任何简单分离类别的方法。
早期识别数据类别之间分布的差异对于机器学习非常有帮助,因为它要么使我们的建模任务更加容易,要么防止我们高估仅仅依赖一个特别信息丰富特征的模型的性能。
例如,如果您试图预测推文是否表达了积极或消极的观点,您可以从每个推文的*均字数开始计数。然后,您可以绘制该特征的直方图以了解其分布情况。
直方图将允许您注意到所有积极推文是否比负面推文更短。这可能导致您添加字长作为预测器以使任务更容易,或者相反,收集额外数据以确保您的模型可以学习推文的内容而不仅仅是它们的长度。
让我们为我们的 ML 编辑绘制一些摘要统计数据,以说明这一点。
ML 编辑的摘要统计
对于我们的示例,我们可以绘制我们数据集中问题长度的直方图,突出显示高分和低分问题之间的不同趋势。以下是使用 pandas 如何实现这一点的方法:
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
"""
df contains questions and their answer counts from writers.stackexchange.com
We draw two histograms:
one for questions with scores under the median score
one for questions with scores over
For both, we remove outliers to make our visualization simpler
"""
high_score = df["Score"] > df["Score"].median()
# We filter out really long questions
normal_length = df["text_len"] < 2000
ax = df[df["is_question"] & high_score & normal_length]["text_len"].hist(
bins=60,
density=True,
histtype="step",
color="orange",
linewidth=3,
grid=False,
figsize=(16, 10),
)
df[df["is_question"] & ~high_score & normal_length]["text_len"].hist(
bins=60,
density=True,
histtype="step",
color="purple",
linewidth=3,
grid=False,
)
handles = [
Rectangle((0, 0), 1, 1, color=c, ec="k") for c in ["orange", "purple"]
]
labels = ["High score", "Low score"]
plt.legend(handles, labels)
ax.set_xlabel("Sentence length (characters)")
ax.set_ylabel("Percentage of sentences")
我们可以在 图 4-2 中看到,分布大部分相似,高分问题倾向于稍长(这种趋势在约 800 字符处尤为明显)。这表明问题长度可能是一个模型预测问题分数的有用特征。
我们可以以类似的方式绘制其他变量,以识别更多潜在特征。一旦我们确定了一些特征,让我们仔细看一下我们的数据集,以便能够识别更细粒度的趋势。

图 4-2. 高分和低分问题文本长度的直方图
高效探索和标记
如果只看描述性统计数据,如*均值和直方图,您只能走得那么远。要对数据有直觉,您应花一些时间查看单个数据点。但是,随机查看数据集中的点效率低下。在本节中,我将介绍如何在可视化单个数据点时提高效率。
在这里使用聚类是一种有用的方法。聚类是将一组对象分组的任务,以便同一组(称为聚类)中的对象比其他组(聚类)中的对象更相似(某种意义上)。我们将使用聚类来探索我们的数据,以及稍后的模型预测(参见“降维”)。
许多聚类算法通过测量数据点之间的距离并将距离接*的点分配到同一聚类中来分组数据点。图 4-3 展示了聚类算法将数据集分成三个不同聚类的示例。聚类是一种无监督方法,通常没有一种单一正确的聚类方法。在本书中,我们将使用聚类作为指导探索的一种方法。
因为聚类依赖于计算数据点之间的距离,我们选择如何数值化表示数据点对生成的聚类有很大影响。我们将在下一节“矢量化”深入探讨这一点。

图 4-3. 从数据集生成三个聚类
绝大多数数据集可以根据它们的特征、标签或二者的组合分为不同的聚类。逐个检查每个聚类以及聚类之间的相似性和差异是识别数据集结构的好方法。
这里有多个需要注意的事项:
-
您在数据集中识别出多少个聚类?
-
您认为这些聚类各不相同吗?在哪些方面不同?
-
有些聚类比其他聚类密集得多吗?如果是这样,您的模型可能会在较稀疏的区域表现不佳。增加特征和数据可以帮助缓解这个问题。
-
所有聚类是否表示看似“难以”建模的数据?如果某些聚类似乎表示更复杂的数据点,请注意它们,以便在评估我们的模型性能时重新审视。
正如我们所提到的,聚类算法处理向量,因此我们不能简单地将一组句子传递给聚类算法。为了准备我们的数据进行聚类,我们首先需要将其矢量化。
矢量化
矢量化数据集是从原始数据到表示它的向量的过程。图 4-4 展示了文本和表格数据的矢量化表示示例。

图 4-4. 矢量化表示的示例
有许多方法可以对数据进行向量化,因此我们将专注于适用于一些最常见数据类型(如表格数据、文本和图像)的几种简单方法。
表格数据
对于既包含分类特征又包含连续特征的表格数据,一种可能的向量表示方法就是简单地将每个特征的向量表示连接起来。
连续特征应该被归一化到一个通用的尺度,以便具有更大尺度的特征不会导致较小的特征被模型完全忽略。有各种方法可以对数据进行归一化,但从使每个特征的均值为零且方差为一的转换开始通常是一个很好的第一步。这通常被称为标准化分数。
例如,颜色等分类特征可以转换为独热编码:一个与特征的不同值数量一样长的列表,只包含零和一个单一的一,其索引表示当前值(例如,在包含四种不同颜色的数据集中,我们可以将红色编码为[1, 0, 0, 0],将蓝色编码为[0, 0, 1, 0])。也许你会好奇,为什么我们不简单地为每个可能的值分配一个数字,比如将红色设为 1,将蓝色设为 3。这是因为这样的编码方案会暗示值之间的排序关系(蓝色大于红色),而这通常对于分类变量是错误的。
独热编码的一个特性是任意两个给定特征值之间的距离始终为一。这通常为模型提供了一个良好的表示,但在一些情况下,如星期几,某些值可能比其他值更相似(星期六和星期天都属于周末,因此理想情况下,它们的向量应该比星期三和星期天更接*,例如)。神经网络已经开始证明它们在学习这类表示方面是有用的(参见 C. Guo 和 F. Berkhahn 的论文"Entity Embeddings of Categorical Variables")。已经证明,使用这些表示方法的模型性能比其他编码方案要好。
最后,诸如日期之类的更复杂特征应该转换为几个捕捉其显著特征的数值特征。
让我们通过一个关于表格数据向量化的实际示例来说明。你可以在这本书的 GitHub 仓库中找到示例的代码。
假设我们不看问题的内容,而是想根据其标签、评论数和创建日期来预测问题的评分。在表 4-3 中,你可以看到一个关于writers.stackexchange.com数据集的示例。
表 4-3. 未经任何处理的表格输入
| Id | Tags | CommentCount | CreationDate | Score |
|---|---|---|---|---|
| 1 | 7 | 2010-11-18T20:40:32.857 | 32 | |
| 2 | 0 | 2010-11-18T20:42:31.513 | 20 | |
| 3 | 1 | 2010-11-18T20:43:28.903 | 34 | |
| 5 | 0 | 2010-11-18T20:43:59.693 | 28 | |
| 7 | 1 | 2010-11-18T20:45:44.067 | 21 |
每个问题都有多个标签,还有日期和评论数量。让我们预处理每个问题。首先,我们对数值字段进行归一化:
def get_norm(df, col):
return (df[col] - df[col].mean()) / df[col].std()
tabular_df["NormComment"]= get_norm(tabular_df, "CommentCount")
tabular_df["NormScore"]= get_norm(tabular_df, "Score")
然后,我们从日期中提取相关信息。例如,我们可以选择发布的年份、月份、日期和时间。这些都是我们的模型可以使用的数值。
# Convert our date to a pandas datetime
tabular_df["date"] = pd.to_datetime(tabular_df["CreationDate"])
# Extract meaningful features from the datetime object
tabular_df["year"] = tabular_df["date"].dt.year
tabular_df["month"] = tabular_df["date"].dt.month
tabular_df["day"] = tabular_df["date"].dt.day
tabular_df["hour"] = tabular_df["date"].dt.hour
我们的标签是分类特征,每个问题可能会有任意数量的标签。正如我们之前看到的,表示分类输入的最简单方法是进行独热编码,将每个标签转换为其自己的列,每个问题只有在该标签与此问题相关联时,该标签特征才为 1。
因为我们的数据集中有超过三百个标签,所以我们选择只为使用频率超过五百次的五个最流行标签创建列。我们可以添加每一个标签,但因为大多数标签只出现一次,这并没有帮助我们识别模式。
# Select our tags, represented as strings, and transform them into arrays of tags
tags = tabular_df["Tags"]
clean_tags = tags.str.split("><").apply(
lambda x: [a.strip("<").strip(">") for a in x])
# Use pandas' get_dummies to get dummy values
# select only tags that appear over 500 times
tag_columns = pd.get_dummies(clean_tags.apply(pd.Series).stack()).sum(level=0)
all_tags = tag_columns.astype(bool).sum(axis=0).sort_values(ascending=False)
top_tags = all_tags[all_tags > 500]
top_tag_columns = tag_columns[top_tags.index]
# Add our tags back into our initial DataFrame
final = pd.concat([tabular_df, top_tag_columns], axis=1)
# Keeping only the vectorized features
col_to_keep = ["year", "month", "day", "hour", "NormComment",
"NormScore"] + list(top_tags.index)
final_features = final[col_to_keep]
在表格 4-4 中,您可以看到我们的数据现在完全向量化,每行都只包含数值。我们可以将这些数据输入到聚类算法或监督学习模型中。
表格 4-4. 向量化的表格输入
| Id | Year | Month | Day | Hour | Norm-Comment | Norm-Score | Creative writing | Fiction | Style | Char-acters | Tech-nique | Novel | Pub-lishing |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 2010 | 11 | 18 | 20 | 0.165706 | 0.140501 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 2 | 2010 | 11 | 18 | 20 | -0.103524 | 0.077674 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
| 3 | 2010 | 11 | 18 | 20 | -0.065063 | 0.150972 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
| 5 | 2010 | 11 | 18 | 20 | -0.103524 | 0.119558 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 7 | 2010 | 11 | 18 | 20 | -0.065063 | 0.082909 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
不同类型的数据需要不同的向量化方法。特别是文本数据通常需要更有创意的方法。
文本数据
文本向量化的最简单方法是使用计数向量,这是词汇版本的一种独热编码。首先构建一个词汇表,包含数据集中唯一词汇的列表。将词汇表中的每个单词与一个索引关联(从 0 到词汇表大小)。然后,可以用一个与词汇表长度相同的列表来表示每个句子或段落。每个索引处的数字表示给定句子中相关单词的出现次数。
这种方法忽略了句子中单词的顺序,因此被称为词袋。图 4-5 显示了两个句子及其词袋表示。这两个句子都被转换为包含关于单词在句子中出现次数信息的向量,但不包括单词出现在句子中的顺序。

图 4-5. 从句子中获取词袋向量
使用词袋表示或其标准化版本 TF-IDF(全称词频-逆文档频率)在 scikit-learn 中很简单,您可以在这里看到:
# Create an instance of a tfidf vectorizer,
# We could use CountVectorizer for a non normalized version
vectorizer = TfidfVectorizer()
# Fit our vectorizer to questions in our dataset
# Returns an array of vectorized text
bag_of_words = vectorizer.fit_transform(df[df["is_question"]]["Text"])
多种新型文本向量化方法已经开发出来,从 2013 年的Word2Vec(参见论文,“Efficient Estimation of Word Representations in Vector Space” by Mikolov et al.)开始,到更*期的方法如fastText(参见论文,“Bag of Tricks for Efficient Text Classification” by Joulin et al.)。这些向量化技术产生的词向量试图学习一种能够更好地捕捉概念间相似性的表示,优于 TF-IDF 编码。它们通过学习哪些单词倾向于在大量文本(如维基百科)中出现在类似的上下文中来实现这一点。这种方法基于分布假设,声称具有相似分布的语言单元具有相似的含义。
具体而言,这是通过为每个单词学习一个向量,并训练一个模型来预测句子中缺失的单词,使用周围单词的词向量。考虑的相邻单词数称为窗口大小。在图 4-6,您可以看到窗口大小为二时此任务的描绘。左侧,目标词前后两个单词的词向量被输入到一个简单的模型中。然后优化这个简单模型和单词向量的值,使得输出匹配缺失单词的词向量。

图 4-6. 学习词向量,来自Mikolov 等人的 Word2Vec 论文“Efficient Estimation of Word Representations in Vector Space”
存在许多开源预训练的词向量模型。使用在大语料库上预训练的模型生成的向量(通常是维基百科或新闻存档)可以更好地利用我们模型的语义含义。
例如,Joulin 等人的fastText论文中提到的词向量可以在线获取独立工具。对于更定制化的方法,spaCy是一个自然语言处理工具包,提供预训练模型以及构建自己模型的简便方法。
这里是使用 spaCy 加载预训练词向量并使用它们获取语义有意义的句子向量的示例。在幕后,spaCy 检索我们数据集中每个单词的预训练值(如果它不是其预训练任务的一部分,则忽略它),并*均所有问题中的向量以获取问题的表示。
import spacy
# We load a large model, and disable pipeline unnecessary parts for our task
# This speeds up the vectorization process significantly
# See https://spacy.io/models/en#en_core_web_lg for details about the model
nlp = spacy.load('en_core_web_lg', disable=["parser", "tagger", "ner",
"textcat"])
# We then simply get the vector for each of our questions
# By default, the vector returned is the average of all vectors in the sentence
# See https://spacy.io/usage/vectors-similarity for more
spacy_emb = df[df["is_question"]]["Text"].apply(lambda x: nlp(x).vector)
要查看我们数据集中使用 TF-IDF 模型与预训练词嵌入的比较,请参阅书的 GitHub 存储库中的文本向量化笔记本。
自 2018 年以来,使用大型语言模型在更大的数据集上进行单词向量化已经开始产生最准确的结果(参见 J. Howard 和 S. Ruder 的论文“Universal Language Model Fine-Tuning for Text Classification”以及 J. Devlin 等人的论文“BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding”)。然而,这些大型模型的缺点是比简单的词嵌入更慢更复杂。
最后,让我们来看看另一种常用数据类型,即图像的向量化。
图像数据
图像数据已经向量化,因为图像只是一个多维数组的集合,通常在 ML 社区中称为tensors。例如,大多数标准的三通道 RGB 图像简单地存储为像素高度乘以宽度乘以三(用于红、绿和蓝通道)的数字列表。在 Figure 4-7 中,您可以看到我们如何将图像表示为一组数字的张量,表示每个主色的强度。
虽然我们可以直接使用这种表示形式,但我们希望我们的张量能够更多地捕捉图像的语义含义。为了做到这一点,我们可以采用类似于文本的方法,并利用大型预训练神经网络。
训练过大规模分类数据集(如VGG(参见 A. Simonyan 和 A. Zimmerman 的论文,“Very Deep Convolutional Networks for Large-Scale Image Recognition”)或Inception(参见 C. Szegedy 等人的论文,“Going Deeper with Convolutions”)在ImageNet 数据集上学习后,为了进行良好的分类,这些模型最终学习到了非常表达丰富的表示形式。这些模型大多遵循相似的高级结构。输入是一张图像,通过许多连续的计算层,每一层生成图像的不同表示。
最后,倒数第二层被传递给一个函数,为每个类生成分类概率。因此,这个倒数第二层包含了图像的表示,足以对包含的对象进行分类,这使得它成为其他任务的有用表示。

图 4-7. 表示 3 作为从 0 到 1 的值的矩阵(仅显示红色通道)
提取此表示层在生成图像有意义的向量方面表现出色。除了加载预训练模型之外,无需任何自定义工作。在 图 4-8 中,每个矩形代表其中一个预训练模型的不同层。最有用的表示被突出显示。通常位于分类层之前,因为这是需要最好地总结图像以使分类器表现良好的表示。

图 4-8. 使用预训练模型来向量化图像
使用像 Keras 这样的现代库可以使这项任务变得更加容易。以下是从文件夹加载图像并使用 Keras 中可用的预训练网络将其转换为语义上有意义的向量的函数:
import numpy as np
from keras.preprocessing import image
from keras.models import Model
from keras.applications.vgg16 import VGG16
from keras.applications.vgg16 import preprocess_input
def generate_features(image_paths):
"""
Takes in an array of image paths
Returns pretrained features for each image
:param image_paths: array of image paths
:return: array of last-layer activations,
and mapping from array_index to file_path
"""
images = np.zeros(shape=(len(image_paths), 224, 224, 3))
# loading a pretrained model
pretrained_vgg16 = VGG16(weights='imagenet', include_top=True)
# Using only the penultimate layer, to leverage learned features
model = Model(inputs=pretrained_vgg16.input,
outputs=pretrained_vgg16.get_layer('fc2').output)
# We load all our dataset in memory (works for small datasets)
for i, f in enumerate(image_paths):
img = image.load_img(f, target_size=(224, 224))
x_raw = image.img_to_array(img)
x_expand = np.expand_dims(x_raw, axis=0)
images[i, :, :, :] = x_expand
# Once we've loaded all our images, we pass them to our model
inputs = preprocess_input(images)
images_features = model.predict(inputs)
return images_features
一旦您拥有向量化表示,您可以对其进行聚类或将数据传递给模型,但您也可以使用它更高效地检查数据集。通过将具有相似表示的数据点分组在一起,您可以更快地查看数据集中的趋势。接下来我们将看到如何做到这一点。
降维
拥有向量表示对算法至关重要,但我们还可以利用这些表示直接可视化数据!这可能看起来很具挑战性,因为我们描述的向量通常在两个以上的维度中,这使得它们在图表上显示起来具有挑战性。我们如何显示一个 14 维向量呢?
深度学习领域的图灵奖得主 Geoffrey Hinton 在他的演讲中承认了这个问题,并给出了以下建议:“要处理 14 维空间中的超*面,请在 3D 空间中进行可视化并大声说 fourteen。每个人都这样做。”(参见 G. Hinton 等人的演讲第 16 页,“An Overview of the Main Types of Neural Network Architecture” 这里)。如果这对您来说似乎很困难,您将对降维技术感到兴奋,这是在尽可能保持其结构的同时将向量表示为更少维度的技术。
降维技术如 t-SNE(参见 L. van der Maaten 和 G. Hinton 的论文,PCA,“Visualizing Data Using t-SNE”)和 UMAP(参见 L. McInnes 等人的论文,“UMAP: Uniform Manifold Approximation and Projection for Dimension Reduction”)允许您将高维数据(如表示句子、图像或其他特征的向量)投影到二维*面上。
这些投影对于注意数据中的模式非常有用,然后您可以进一步调查它们。然而,它们只是真实数据的*似表示,因此您应该通过使用其他方法来验证您从这样的图中得出的任何假设。例如,如果您看到一类点的集群似乎都有一个共同的特征,请检查您的模型是否实际上正在利用该特征。
要开始,使用降维技术绘制你的数据,并根据你要检查的属性为每个点着色。对于分类任务,首先根据标签为每个点着色。对于无监督任务,你可以根据你正在查看的特征的值来着色点,例如。这可以让你看到是否有任何区域看起来会让你的模型容易分离,或者更棘手。
这里是如何在我们在“向量化”中生成的嵌入中轻松使用 UMAP 来做到这一点:
import umap
# Fit UMAP to our data, and return the transformed data
umap_emb = umap.UMAP().fit_transform(embeddings)
fig = plt.figure(figsize=(16, 10))
color_map = {
True: '#ff7f0e',
False:'#1f77b4'
}
plt.scatter(umap_emb[:, 0], umap_emb[:, 1],
c=[color_map[x] for x in sent_labels],
s=40, alpha=0.4)
作为提醒,我们决定首先使用来自 Stack Exchange 作者社区的数据。此数据集的结果显示在图 4-9 上。乍一看,我们可以看到一些应该探索的区域,比如左上角的未解答问题的密集区域。如果我们能确定它们共有哪些特征,我们可能会发现一个有用的分类特征。
数据向量化并绘制后,系统地开始识别相似数据点的群组通常是个好主意。我们可以简单地通过查看 UMAP 图来做到这一点,但我们也可以利用聚类。

图 4-9. UMAP plot colored by whether a given question was successfully answered
聚类
我们前面提到过聚类作为从数据中提取结构的方法。无论您是用于检查数据集还是分析模型性能,正如我们将在第五章中所做的那样,聚类都是您工具库中的核心工具。我使用聚类类似于降维,作为发现问题和有趣数据点的另一种方式。
在实践中,一种简单的聚类数据的方法是从尝试几种简单的算法开始,例如k-means,并调整它们的超参数,如聚类数,直到达到令人满意的性能。
聚类的表现很难量化。在实践中,使用数据可视化和诸如肘部法或轮廓图之类的方法对我们的用例已经足够了,这并不是为了完美地分离我们的数据,而是为了确定我们的模型可能存在问题的区域。
下面是一个示例代码片段,用于对我们的数据集进行聚类,并使用我们早期描述的降维技术 UMAP 来可视化我们的聚类。
from sklearn.cluster import KMeans
import matplotlib.cm as cm
# Choose number of clusters and colormap
n_clusters=3
cmap = plt.get_cmap("Set2")
# Fit clustering algorithm to our vectorized features
clus = KMeans(n_clusters=n_clusters, random_state=10)
clusters = clus.fit_predict(vectorized_features)
# Plot the dimentionality reduced features on a 2D plane
plt.scatter(umap_features[:, 0], umap_features[:, 1],
c=[cmap(x/n_clusters) for x in clusters], s=40, alpha=.4)
plt.title('UMAP projection of questions, colored by clusters', fontsize=14)
如您在 图 4-10 中所见,我们在 2D 表示中本能地聚类的方式并不总是与我们的算法在向量化数据上找到的聚类匹配。这可能是由于我们降维算法中的人为因素或复杂的数据拓扑。事实上,有时将点的分配簇作为特征添加到模型中可以提高模型的性能。
一旦您有了簇,就检查每个簇并尝试识别数据的每个趋势。为此,您应该选择每个簇的几个点,并像模型一样操作,因此用您认为正确答案的标签来标记这些点。在接下来的章节中,我将描述如何进行这项标注工作。

图 4-10. 可视化我们的问题,按簇着色
成为算法
一旦您查看了聚合指标和簇信息,我鼓励您遵循“Monica Rogati: 如何选择和优先处理 ML 项目”中的建议,并尝试通过标记每个簇中的几个数据点来执行您的模型工作,以期望模型产生的结果。
如果您从未尝试过执行您的算法的工作,那么很难判断其结果的质量。另一方面,如果您花一些时间自己标记数据,您通常会注意到一些趋势,这将使您的建模任务变得更加容易。
您可能会在我们关于启发式的前一节中认出这个建议,这应该不会让您感到意外。选择建模方法涉及对我们的数据几乎与构建启发式方法一样多的假设,因此对这些假设进行数据驱动的选择是有意义的。
即使您的数据集包含标签,也应进行标注。这样可以验证您的标签是否捕捉到了正确的信息,并且它们是正确的。在我们的案例研究中,我们使用问题的得分作为其质量的衡量标准,这是一个较弱的标签。自己标注一些示例将帮助我们验证这个标签是否合适。
一旦您标记了一些示例,请随时更新您的向量化策略,通过添加您发现的任何特征来使数据表示尽可能信息丰富,并继续标记。这是一个迭代的过程,正如 图 4-11 所示。

图 4-11. 数据标注过程
为了加快标注速度,请确保利用先前分析的结果,在您已识别的每个簇中标记几个数据点,并针对特征分布中的每个常见值进行标记。
一种方法是利用可视化库对数据进行交互式探索。Bokeh 提供制作交互式图表的功能。快速标记数据的一种方法是通过浏览我们的向量化示例的图表,为每个簇标记几个示例。
图 4-12 显示了一个代表性的个体示例,来自大部分未答复问题的一个聚类。此聚类中的问题往往相当模糊且难以客观回答,因此没有收到答案。这些问题被准确标记为差劲的问题。要查看此图的源代码以及其在 ML 编辑器中的使用示例,请转到本书的 GitHub 存储库中的“探索数据生成特征”笔记本。

图 4-12. 使用 Bokeh 检查和标记数据
在标注数据时,您可以选择将标签与数据本身一起存储(例如,作为 DataFrame 中的额外列),或者使用文件或标识符到标签的映射来单独存储。这纯粹是个人偏好的问题。
当您标记示例时,请注意您使用哪种过程来做出决策。这将有助于识别趋势并生成有助于模型的特征。
数据趋势
在标记数据一段时间后,通常会发现一些趋势。有些可能很有信息量(简短的推文更容易分类为积极或消极),并指导您为模型生成有用的特征。其他可能是无关的相关性,是因为数据收集的方式不同造成的。
可能我们收集的所有法语推文恰好都是负面的,这可能会导致模型自动将法语推文分类为负面。我让您决定在更广泛、更具代表性的样本上这可能有多不准确。
如果您注意到任何此类情况,请不要绝望!在开始构建模型之前,识别这些趋势至关重要,因为它们会人为地提高训练数据的准确性,并可能导致您的模型在生产中表现不佳。
处理这种有偏见的示例的最佳方法是收集额外的数据,使您的训练集更具代表性。您也可以尝试从训练数据中消除这些特征以避免模型的偏见,但在实践中可能不够有效,因为模型经常通过与其他特征的相关性来利用偏见(参见第八章)。
一旦您确定了一些趋势,就是利用它们的时候了。最常见的方法是通过创建表征该趋势的特征或者使用能够轻松利用它的模型之一。
让数据指导特征和模型
我们希望利用我们在数据中发现的趋势来指导我们的数据处理、特征生成和建模策略。首先,让我们看看如何生成能帮助我们捕捉这些趋势的特征。
利用模式构建特征
机器学习是利用统计学习算法来利用数据中的模式,但有些模式对于模型而言比其他模式更容易捕捉。想象一个简单的例子,用数值本身除以 2 作为特征来预测数值的情况。模型只需学会乘以 2 就能完美预测目标值。另一方面,从历史数据预测股市则是一个需要利用更复杂模式的问题。
这就是为什么机器学习的许多实际收益来自于生成额外的特征,这些特征将帮助我们的模型识别有用的模式。模型识别模式的容易程度取决于我们表示数据的方式以及我们拥有的数据量。数据越多且数据越少噪声,通常需要进行的特征工程工作就越少。
通常情况下,从生成特征开始是有价值的;首先是因为我们通常从一个小数据集开始,其次是因为它有助于编码我们对数据的信念并调试我们的模型。
季节性是一个常见的趋势,可以通过特定的特征生成来受益。假设在线零售商注意到他们的大多数销售都发生在月底的最后两个周末。在构建预测未来销售的模型时,他们希望确保它能够捕捉到这种模式。
如您所见,根据日期的表示方式,该任务可能对他们的模型来说非常困难。大多数模型只能接受数值输入(参见“向量化”以了解将文本和图像转换为数值输入的方法),因此让我们来探讨几种表示日期的方法。
原始日期时间
表示时间最简单的方式是使用Unix 时间,它表示“自 1970 年 1 月 1 日星期四 00:00:00 以来经过的秒数”。
虽然这种表示方式很简单,但我们的模型需要学习一些相当复杂的模式来识别月底的最后两个周末。例如,2018 年的最后一个周末(从 12 月 29 日的 00:00:00 到 12 月 30 日的 23:59:59)在 Unix 时间中表示为从 1546041600 到 1546214399 的范围(您可以验证这一点,如果您计算这两个数字之间的差异,该间隔表示为以秒计算的 23 小时 59 分钟 59 秒)。
这个时间范围并没有什么特别容易与其他月份的周末联系起来的地方,所以当使用 Unix 时间作为输入时,模型将很难将相关的周末与其他周末分开。通过生成特征,我们可以使模型的任务变得更加简单。
提取星期几和月份中的日期
使我们对日期的表示更清晰的一种方法是将星期几和月份中的日期分别提取为两个单独的属性。
例如,我们将表示 2018 年 12 月 30 日 23:59:59 的方式与前面相同,再加上代表星期几(例如星期日为 0)和日期(30)的两个额外值。
这种表示方法将使我们的模型更容易学习与周末相关的数值(星期日和星期六的 0 和 6)以及月末较晚日期对应的更高活动度。
还值得注意的是,表示通常会向我们的模型引入偏见。例如,通过将星期几编码为数字,星期五的编码(等于五)将比星期一的编码(等于一)大五倍。这种数值尺度是我们表示的产物,不代表我们希望模型学习的内容。
特征交叉
虽然前面的表示方法让我们的模型任务更容易,但它们仍然需要学习星期几和日期之间复杂的关系:高流量不会发生在月初的周末或月底的工作日。
一些模型,如深度神经网络,利用特征的非线性组合,因此可以掌握这些关系,但它们通常需要大量数据。解决这个问题的常见方法是通过让任务更容易,并引入 特征交叉。
特征交叉是通过简单地将两个或更多特征相乘(交叉)而生成的特征。这种引入特征的非线性组合使得我们的模型可以更容易地基于多个特征值的组合进行区分。
在 表 4-5 中,您可以看到我们描述的每种表示方法对几个示例数据点的效果。
表 4-5. 以更清晰的方式表示您的数据将使您的算法更容易执行良好
| 人类代表 | 原始数据(Unix 日期时间) | 周几 | 日期 | 交叉(周几 / 日期) |
|---|---|---|---|---|
| 2018 年 12 月 29 日星期六,00:00:00 | 1,546,041,600 | 7 | 29 | 174 |
| 2018 年 12 月 29 日星期六,01:00:00 | 1,546,045,200 | 7 | 29 | 174 |
| … | … | … | … | … |
| 2018 年 12 月 30 日星期日,23:59:59 | 1,546,214,399 | 1 | 30 | 210 |
在 图 4-13 中,您可以看到这些特征值随时间变化的方式,以及哪些特征使得模型更容易将特定数据点与其他数据点分离开来。

图 4-13. 使用特征交叉和提取特征最容易区分月末的最后周末
还有一种表示我们的数据的方法,可以让我们的模型更轻松地学习月末的最后两个周末的预测价值。
为您的模型提供答案
这可能看起来像作弊,但如果您确实知道某种特征值组合特别具有预测性,您可以创建一个新的二进制特征,只有当这些特征采用相关组合值时才会取非零值。在我们的情况下,这意味着添加一个名为“is_last_two_weekends”的特征,例如,在月末的最后两个周末期间该特征将设置为 1。
如果最*两个周末像我们预期的那样具有预测性,模型将简单地利用这一特征,并且将更加准确。在构建 ML 产品时,永远不要犹豫让任务对模型来说更容易。最好让模型在简单的任务上运行,而不是在复杂的任务上挣扎。
特征生成是一个广泛的领域,存在适用于大多数数据类型的方法。讨论为不同类型的数据生成有用特征的每个特征是本书范围之外的内容。如果您想看到更多实际例子和方法,我建议您查看机器学习特征工程(O’Reilly),作者是 Alice Zheng 和 Amanda Casari。
总的来说,生成有用特征的最佳方法是使用我们描述的方法查看您的数据,并问自己以使模型学习其模式的方式表示它的最简单方法是什么。在接下来的部分中,我将描述使用这一过程为 ML 编辑器生成的几个特征示例。
ML 编辑器特性
对于我们的 ML 编辑器,使用之前描述的技术来检查我们的数据集(请参阅在本书的 GitHub 存储库中的探索数据以生成特征笔记本中的详细信息),我们生成了以下特征:
-
诸如can和should之类的动作动词预示着问题会得到回答,因此我们添加了一个检查每个问题中是否存在这些动作动词的二进制值。
-
问号也是一个很好的预测因子,因此我们生成了一个
has_question特征。 -
对于英语语言正确使用的问题,往往得不到答案,因此我们添加了一个
is_language_question特征。 -
问题的文本长度是另一个因素,非常短的问题往往得不到答复。这导致添加了一个归一化的问题长度特征。
-
在我们的数据集中,问题的标题也包含关键信息,并且在标注时查看标题使任务变得更加容易。这导致在所有较早的特征计算中包括标题文本。
一旦我们有了一组初始特征,我们就可以开始构建模型。构建这个第一个模型是下一章第五章的主题。
在进入模型之前,我想更深入地探讨如何收集和更新数据集的主题。为此,我与罗伯特·门罗进行了交流,他是这个领域的专家。我希望你喜欢我们讨论的摘要,并期待你对我们下一个部分,构建我们的第一个模型,感到兴奋!
罗伯特·门罗:你如何找到、标记和利用数据?
罗伯特·门罗成立了几家人工智能公司,在人工智能领域建立了一些顶尖团队。他曾是 Figure Eight 的首席技术官,在该公司最大的增长期间领导数据标注业务。在此之前,罗伯特负责了 AWS 的首个本地自然语言处理和机器翻译服务的产品。在我们的对话中,罗伯特分享了他在构建机器学习数据集时学到的一些经验。
Q: 如何开始一个机器学习项目?
A: 最好的方法是从业务问题开始,因为它会为你的工作提供边界。在你的机器学习编辑器案例研究中,例如,你是在提交后编辑别人写的文本,还是在别人写作时实时提出建议?第一种方式可以让你使用较慢的模型批量处理请求,而第二种方式则需要更快的处理。
在模型方面,第二种方法会使序列到序列模型失效,因为它们会变得过慢。此外,今天的序列到序列模型在句子级推荐之外并不起作用,需要大量*行文本来训练。更快的解决方案是利用分类器,并使用它提取的重要特征作为建议。你想要从这个初始模型中得到的是一个简单的实现和你可以信任的结果,例如从词袋特征的朴素贝叶斯开始。
最后,你需要花一些时间查看一些数据并自己标记它。这将让你对问题的难度有直观感觉,并找出可能合适的解决方案。
Q: 你需要多少数据才能开始?
A: 在收集数据时,你要确保你拥有一个代表性和多样化的数据集。首先查看你拥有的数据,并看看是否有任何未被代表的类型,以便你可以收集更多数据。对数据集进行聚类并寻找异常值可以帮助加快这个过程。
对于数据标记,通常在分类的常见情况下,我们发现,对于你更稀少的类别,标记约 1000 个示例在实践中效果良好。至少你会得到足够的信号告诉你是否继续使用当前的建模方法。大约在 10000 个示例时,你可以开始信任你正在构建的模型的置信度。
随着你获取更多数据,你的模型准确性会慢慢提高,为你提供一个随数据增加而改进性能的曲线。在任何时候,你只关心曲线的最后部分,这应该给你一个当前价值的估计,更多的数据将会给你更大的改进。在绝大多数情况下,从标记更多数据中获得的改进将比迭代模型更为显著。
Q: 你使用什么过程来收集和标记数据?
A: 你可以查看你当前的最佳模型,看看它遇到了什么问题。不确定性采样是一种常见方法:识别你的模型最不确定的例子(即最接*其决策边界的例子),并找到类似的例子添加到训练集中。
你还可以训练一个“错误模型”来找出当前模型难以处理的更多数据。使用你的模型犯错作为标签(将每个数据点标记为“预测正确”或“预测错误”)。一旦你在这些例子上训练了一个“错误模型”,你可以将其用于你的未标记数据,并标记它预测你的模型会失败的例子。
或者,你可以训练一个“标注模型”来找到下一个最佳标注的例子。假设你有一百万个例子,你只标注了一千个。你可以创建一个包含一千个随机抽样标记图像和一千个未标记图像的训练集,并训练一个二元分类器来预测哪些图像你已经标记过。然后,你可以使用这个标注模型来识别与你已经标记过的内容最不同的数据点,并进行标注。
Q: 你如何验证你的模型学到了有用的东西?
A: 一个常见的陷阱是将标注工作集中在相关数据集的一小部分上。也许你的模型在篮球相关的文章上表现不佳。如果你继续标注更多的篮球文章,你的模型可能会在篮球方面表现出色,但其他方面表现不佳。这就是为什么在使用数据收集策略的同时,你应该始终从测试集中随机抽样以验证你的模型。
最后,最好的方法是跟踪你部署模型性能何时出现漂移。你可以追踪模型的不确定性,或者最理想的是将其转化为业务指标:你的使用指标是否逐渐下降?这可能是由其他因素引起的,但是这是一个很好的触发点,可以调查并可能更新你的训练集。
结论
在本章中,我们介绍了有效和高效地检查数据集的重要提示。
我们首先看数据的质量以及如何决定它是否足够满足我们的需求。接下来,我们介绍了熟悉你所拥有的数据类型的最佳方式:从汇总统计开始,然后转向相似点的聚类,以识别广泛的趋势。
我们随后讨论了为什么花费大量时间标记数据以识别趋势是有价值的,这些趋势我们可以利用来构建有价值的特征。最后,我们从罗伯特·蒙罗(Robert Munro)帮助多个团队构建先进机器学习数据集的经验中学到了一些东西。
现在,我们已经检查了一个数据集并生成了我们希望能够预测的特征,我们准备构建我们的第一个模型,这将在第五章中完成。
第三部分:对模型进行迭代
第 I 部分涵盖了设置机器学习项目并跟踪其进展的最佳实践。在第 II 部分中,我们了解了尽可能快速地构建端到端管道以及探索初步数据集的价值。
由于机器学习(ML)具有实验性特质,它是一个高度迭代的过程。你应该计划对模型和数据进行反复迭代,遵循如图 III-1 所示的实验循环。

图 III-1。机器学习循环
第 III 部分将描述一次迭代循环的过程。在进行机器学习项目时,你应该计划进行多个这样的迭代,才有望达到令人满意的性能。以下是本部分各章的概述:
第 5 章
在本章中,我们将训练第一个模型并对其进行基准测试。然后,深入分析其性能并识别可以改进的方面。
第 6 章
本章介绍了快速构建和调试模型的技术,并避免耗时的错误。
第 7 章
在本章中,我们将使用机器学习编辑器作为案例研究,展示如何利用一个训练好的分类器向用户提供建议,并构建一个功能完整的建议模型。
第五章:训练和评估你的模型
在之前的章节中,我们已经介绍了如何识别要解决的正确问题,制定解决方案的计划,构建一个简单的流水线,探索数据集,并生成一组初始特征。这些步骤已经为我们收集到足够的信息,以开始训练一个适当的模型。这里的适当模型意味着适合当前任务并有良好表现机会的模型。
在本章中,我们将首先简要讨论选择模型时的一些注意事项。然后,我们将描述最佳实践来分离你的数据,这将有助于在真实条件下评估你的模型。最后,我们将探讨分析建模结果和诊断错误的方法。
最简单合适的模型
现在我们准备训练模型了,我们需要决定从哪个模型开始。或许尝试每个可能的模型、对它们进行基准测试,然后根据一些指标在一个保留的测试集上选择表现最好的那个模型,这看起来很诱人。
总的来说,这并不是最佳方法。它不仅计算密集(有许多模型集和每个模型的许多参数,所以实际上你只能测试一个次优子集),而且将模型视为预测性黑匣子,并完全忽略了机器学习模型在学习方式中对数据的隐含假设。
不同的模型对数据有不同的假设,因此适合不同的任务。此外,由于机器学习是一个迭代的领域,你会想要选择可以快速构建和评估的模型。
让我们首先定义如何识别简单模型。然后,我们将涵盖一些数据模式的示例和适合利用它们的模型。
简单模型
一个简单的模型应该快速实现,易于理解和可部署:快速实现是因为你的第一个模型可能不是最后一个,易于理解是因为这将让你更容易调试它,可部署是因为这是一个基本要求,适用于基于机器学习的应用程序。让我们首先探索我所说的快速实现是什么意思。
快速实现
选择一个对你来说实现简单的模型。通常,这意味着选择一个大家对其有很多教程并且愿意帮助你的广为人知的模型(特别是如果你能用我们的 ML 编辑器提出形式良好的问题的话!)。对于一个新的基于机器学习的应用程序来说,在处理数据和部署可靠结果方面已经有足够的挑战,所以你最初应尽量避免所有模型问题。
如果可能的话,首先使用来自流行库如 Keras 或 scikit-learn 的模型,然后在深入研究那些没有文档且在过去九个月内没有更新的实验性 GitHub 存储库之前保留观望。
一旦您的模型实施完成,您将希望检查并理解它如何利用您的数据集。为此,您需要一个可以理解的模型。
可理解的
模型的可解释性和解释性描述了模型暴露出导致其进行预测的原因的能力(例如给定的预测器组合)。解释性对于多种原因非常有用,例如验证我们的模型没有以不希望的方式偏见,或向用户解释他们可以做什么来改善预测结果。它还使迭代和调试变得更加容易。
如果您可以提取模型依赖于做出决策的特征,您将更清楚地了解哪些特征可以添加、调整或删除,或者哪个模型可以做出更好的选择。
不幸的是,即使对于简单模型,模型可解释性通常也很复杂,对于较大的模型有时甚至无法解决。在“评估特征重要性”中,我们将看到解决这一挑战的方法,帮助您识别模型改进的要点。除其他事项外,我们将使用黑盒解释器,试图提供模型预测的解释,而不考虑其内部运作方式。
简单模型如逻辑回归或决策树往往更容易解释,因为它们提供了某种程度的特征重要性,这也是它们通常是首选的模型之一的原因。
可部署的
作为提醒,您模型的最终目标是为将要使用它的人们提供有价值的服务。这意味着当您考虑要训练哪种模型时,您应始终考虑是否能够部署它。
我们将在第四部分介绍部署,但您应该已经在考虑以下问题:
-
训练模型为用户做出预测需要多长时间?在考虑预测延迟时,您应该包括不仅模型输出结果所需的时间,还包括用户提交预测请求和接收结果之间的延迟。这包括任何预处理步骤,如特征生成,任何网络调用,以及在模型输出和呈现给用户的数据之间发生的任何后处理步骤。
-
如果考虑到我们预期的并发用户数量,这个推理管道是否足够快速?
-
训练模型需要多长时间,以及我们需要多频繁地进行训练?如果训练需要 12 小时,而您需要每 4 小时重新训练模型以保持新鲜,不仅计算成本会很昂贵,而且您的模型将永远处于过时状态。
我们可以通过使用诸如图 5-1 的表格来比较简单模型的情况。随着机器学习领域的发展和新工具的建立,今天可能复杂或难以解释的模型未来可能变得更容易使用,因此这个表格需要定期更新。基于这个原因,我建议您根据您特定的问题领域建立自己的版本。

图 5-1. 基于简易性评分模型
即使在简单、可解释和可部署的模型中,仍然有许多潜在的候选项。为了选择一个模型,您还应考虑在第四章中识别出的模式。
从模式到模型
我们已经识别出的模式和我们生成的特征应该指导我们选择模型。让我们来看几个数据中的模式和适当的模型示例,以利用它们。
我们希望忽略特征尺度
许多模型会更加重视较大的特征而忽略较小的特征。在某些情况下这是可以接受的,但在其他情况下可能不合适。对于使用梯度下降等优化程序的模型(如神经网络),特征尺度的差异有时会导致训练过程的不稳定性。
如果您希望同时使用年龄(从一到一百岁)和收入(假设我们的数据高达九位数)作为两个预测变量,您需要确保您的模型能够利用最具预测性的特征,而不受其尺度影响。
通过预处理特征以使其标准化到零均值和单位方差,可以确保这一点。如果所有特征都被归一化到相同的范围,模型将最初视每个特征为*等重要。
另一种解决方案是使用不受特征尺度差异影响的模型。最常见的实际例子是决策树、随机森林和梯度提升决策树。XGBoost 是一种常用的梯度提升树实现,在生产环境中因其稳健性和速度而广泛使用。
我们的预测变量是预测因子的线性组合
有时候,我们有充分理由相信,只使用特征的线性组合就可以做出良好的预测。在这些情况下,我们应该使用像线性回归这样的线性模型来处理连续性问题,或者像逻辑回归或朴素贝叶斯分类器这样的模型来解决分类问题。
这些模型简单高效,并且通常允许直接解释其权重,有助于我们识别重要特征。如果我们认为特征与预测变量之间的关系更复杂,可以使用非线性模型,如多层神经网络或生成特征交叉(见“让数据驱动特征和模型”的开始)。
我们的数据具有时间性
如果我们处理的是数据点的时间序列,其中给定时间点的值依赖于先前的值,我们将希望利用能够显式编码此信息的模型。此类模型的示例包括统计模型,如自回归积分移动*均(ARIMA)或递归神经网络(RNN)。
每个数据点都是模式的组合
当处理图像域中的问题时,例如卷积神经网络(CNN)通过其学习*移不变滤波器的能力已被证明是有用的。这意味着它们能够提取图像中的局部模式,而不考虑它们的位置。一旦 CNN 学会了如何检测眼睛,它就可以在图像的任何位置检测到它,而不仅仅是在训练集中出现的位置。
卷积滤波器已被证明在其他包含局部模式的领域中非常有用,例如语音识别或文本分类,CNN 已成功用于句子分类。例如,可以参考 Yoon Kim 在论文中的实现,“用于句子分类的卷积神经网络”。
在考虑使用正确的模型时,还有许多其他要考虑的因素。对于大多数经典机器学习问题,我建议使用这个便捷的流程图,这是 scikit-learn 团队提供的有用参考。它为许多常见用例提供了模型建议。
ML 编辑器模型
对于 ML 编辑器,我们希望我们的第一个模型快速且相对容易调试。此外,我们的数据由个别示例组成,无需考虑时间方面的因素(例如一系列问题)。因此,我们将从一个流行且有韧性的基准开始,即随机森林分类器。
一旦您确定了一个看起来合理的模型,就该是训练它的时候了。作为一般指导方针,您不应该在您在第四章中收集的整个数据集上训练模型。您需要从训练集中保留一些数据。让我们来看看为什么以及如何做到这一点。
分割您的数据集
我们模型的主要目标是为我们的用户提交的数据提供有效的预测。这意味着我们的模型最终将必须在它以前从未见过的数据上表现良好。
当您在数据集上训练模型时,仅在相同数据集上测量其性能只能告诉您它在已经看过的数据上进行预测的能力有多好。如果您仅在数据的子集上训练模型,然后可以使用模型未经训练的数据来估计其在未见数据上的表现。
在图 5-2 中,您可以看到一个基于数据集的属性(问题的作者)将数据集分割成三个单独的集合(训练集、验证集和测试集)的示例。在本章中,我们将介绍每个集合的含义以及如何考虑它们。

图 5-2. 在保留作者数据的同时将问题正确分配到每个拆分的示例
要考虑的第一个保留集是验证集。
验证集
为了估计我们的模型在未见数据上的表现,我们有意地从训练集中保留部分数据集,然后使用这个保留的数据集的性能作为我们模型在生产中表现的代理。保留的集合允许我们验证我们的模型能够推广到未见数据,因此通常称为验证集。
您可以选择数据的不同部分作为验证集来评估您的模型,并在剩余数据上训练它。执行此过程的多轮有助于控制由于特定验证集选择而引起的任何方差,这称为交叉验证。
当您更改数据预处理策略、使用的模型类型或其超参数时,您的模型在验证集上的表现将会改变(并且理想情况下会改善)。使用验证集允许您像使用训练集调整模型参数一样调整超参数。
在多次迭代使用验证集进行模型调整后,您的建模流程可能会专门针对在验证数据上表现良好。这违背了验证集的目的,即应该是未见数据的代理。因此,您应该保留一个额外的测试集。
测试集
因为我们将在模型上进行多次迭代循环,并在每个周期中测量其在验证集上的表现,所以我们可能会偏向于使模型在验证集上表现良好。这有助于我们的模型推广超出训练集,但也存在仅仅学习在特定验证集上表现良好的模型的风险。理想情况下,我们希望拥有一个在新数据上表现良好的模型,因此不包含在验证集中。
因此,通常我们会保留第三个称为测试集的数据集,这将作为我们在满意迭代后对未见数据性能的最终基准。虽然使用测试集是最佳实践,但从业者有时会将验证集作为测试集使用。这增加了使模型偏向验证集的风险,但在仅运行少量实验时可能是合适的。
避免使用测试集上的表现来指导建模决策非常重要,因为这个数据集应该代表我们在生产中将面对的未见数据。调整建模方法以在测试集上表现良好会增加高估模型性能的风险。
要让模型在生产中表现良好,你训练的数据应该与将与产品互动的用户生成的数据相似。理想情况下,你从用户那里接收到的任何数据类型都应该在你的数据集中有所体现。如果不是这样的话,那么请记住,你的测试集表现只能反映出部分用户的表现。
对于 ML 编辑器来说,这意味着不符合writers.stackoverflow.com的人群可能无法得到我们的推荐服务。如果我们想要解决这个问题,我们应该扩展数据集,包含更多符合这些用户的问题。我们可以从其他 Stack Exchange 网站收录问题,以涵盖更广泛的主题,或者选择完全不同的问答网站。
对于一个旁边的项目来说,对数据集进行这样的修正可能具有挑战性。然而,在构建消费级产品时,有必要在用户接触到之前帮助模型的弱点被早期捕捉。我们将在第八章中涵盖的许多失败模式可以通过更具代表性的数据集来避免。
相对比例
总体而言,你应该最大化模型可以学习的数据量,同时保留足够大的验证和测试集来提供准确的性能指标。实践者通常使用数据的 70%进行训练,20%进行验证,10%进行测试,但这完全取决于数据的量。对于非常大的数据集,你可以负担得起使用更大比例的数据进行训练,同时仍有足够的数据来验证模型。对于较小的数据集,你可能需要使用较小比例的数据进行训练,以便拥有足够大的验证集来提供准确的性能测量。
现在你知道为什么要分割数据,以及应该考虑哪些分割方法,但是你应该如何决定哪个数据点放入每个分割中呢?你使用的分割方法对建模性能有重大影响,并且应该依赖于数据集的特定特征。
数据泄露
你用来分离数据的方法是验证的一个关键部分。你应该努力使你的验证/测试集与你预期的未见数据尽可能接*。
大多数情况下,训练集、验证集和测试集是通过随机抽样数据点来分离的。在某些情况下,这可能会导致数据泄露。数据泄露发生在(由于我们的训练过程)模型在训练期间接收到了在实际使用时不会访问到的信息。
数据泄露必须尽量避免,因为它会导致我们模型性能的夸大看法。在数据集中训练的模型存在数据泄露时,可以利用信息进行预测,而当它遇到不同的数据时,这些信息是不可得的。这使得模型的任务在保留数据上看起来人为更容易,但这仅仅是由于泄露的信息。模型在保留数据上的性能表现很高,但在生产中会差得多。
在图 5-3 中,我画出了几种常见的情况,随机将数据分割为集合会导致数据泄露。数据泄露有许多潜在原因,接下来我们将探讨两种常见的情况。
为了开始我们的探索,让我们先看看图 5-3 顶部的例子,即时间数据泄露。接着,我们将转向样本污染,这是包含在图 5-3 底部两个例子中的一类。

图 5-3. 随机分割数据通常会导致数据泄露
时间数据泄露
在时间序列预测中,模型需要从过去的数据点学习,以预测尚未发生的事件。如果我们对预测数据集进行随机拆分,我们将引入数据泄露:一个在随机点集上训练并在剩余点上评估的模型将可以访问在其试图预测的事件之后发生的训练数据。
该模型在验证集和测试集上的表现会人为地非常好,但在生产环境中会失败,因为它所学到的都是利用未来信息,而在真实世界中这些信息是不可用的。
一旦你意识到了它,时间数据泄露通常很容易被发现。其他类型的数据泄露可能会使模型在训练过程中获得不应该有的信息,并通过“污染”其训练数据来人为地提高其性能。它们通常更难检测到。
样本污染
数据泄露的一个常见来源在于随机性发生的层次。当建立一个预测学生文章评分的模型时,一位我曾经协助的数据科学家发现他的模型在一个保留测试集上表现接*完美。
在这样一个艰巨的任务上,一个表现如此优秀的模型应该进行仔细检查,因为这通常表明管道中存在错误或数据泄露。有人说,机器学习中墨菲定律的等效物是,当你对模型在测试数据上的表现感到愉快惊讶时,你越有可能在流程中有错误。
在这个例子中,因为大多数学生写了多篇文章,随机分割数据导致了同一学生的文章同时出现在训练集和测试集中。这使得模型能够捕捉到识别学生的特征,并利用这些信息进行准确预测(这个数据集中的学生倾向于在所有文章中有类似的成绩)。
如果我们要部署这个文章评分预测器以便将来使用,它将无法为它之前没有见过的学生预测有用的分数,并且只会为那些它已经训练过的学生预测历史分数。这将毫无用处。
在这个例子中解决数据泄露的方法是,在学生而不是文章级别进行新的分割。这意味着每个学生只出现在训练集或验证集中的其中一个。由于任务变得更加困难,这导致了模型准确性的降低。然而,由于训练任务现在更接*于生产环境,这个新模型变得更有价值。
在常见任务中,样本污染可能以微妙的方式发生。让我们以公寓租赁预订网站为例。该网站整合了点击预测模型,根据用户查询和项目,预测用户是否会点击项目。该模型用于决定向用户展示哪些房源。
要训练这样的模型,该网站可以使用用户特征数据集,例如他们的先前预订次数,与展示给他们的公寓以及他们是否点击了它们。这些数据通常存储在可以查询以生成这些配对的生产数据库中。如果该网站的工程师仅仅是查询数据库来构建这样的数据集,他们可能会面临数据泄露的情况。你能看出原因吗?
在图 5-4 中,我勾画了一个针对特定用户的预测的插图,说明了可能出错的情况。在顶部,你可以看到模型在生产中可能使用的特征。在这里,一个没有先前预订的新用户被呈现给一个特定的公寓。在底部,你可以看到几天后工程师从数据库中提取数据时特征的状态。

图 5-4。数据泄露可能出现在微妙的原因,比如由于缺乏数据版本控制。
注意 previous_bookings 的差异,这是由于用户在最初看到列表后发生的活动造成的。通过使用数据库的快照,用户未来的行为信息泄漏到了训练集中。我们现在知道用户最终将预订五套公寓!这种泄漏可能导致模型在不正确的训练数据上进行正确预测。模型在生成的数据集上的准确率会很高,因为它利用了生产环境中无法访问的数据。当模型部署后,其性能将低于预期。
如果你从这个轶事中学到了什么,那就是要始终调查模型的结果,特别是如果它显示出令人惊讶的强大性能。
ML Editor 数据分割
我们用来训练我们的 ML Editor 的数据集包含在 Stack Overflow 上提出的问题及其答案。乍一看,随机分割似乎足够,并且在 scikit-learn 中实现起来非常简单。例如,我们可以编写如下所示的函数:
from sklearn.model_selection import train_test_split
def get_random_train_test_split(posts, test_size=0.3, random_state=40):
"""
Get train/test split from DataFrame
Assumes the DataFrame has one row per question example
:param posts: all posts, with their labels
:param test_size: the proportion to allocate to test
:param random_state: a random seed
"""
return train_test_split(
posts, test_size=test_size, random_state=random_state
)
这种方法存在潜在的泄漏风险;你能识别出它吗?
如果我们回顾一下我们的使用案例,我们知道我们希望我们的模型能够处理以前没有见过的问题,只看其内容。然而,在问答网站上,许多其他因素可能影响问题是否成功得到回答。其中一个因素是作者的身份。
如果我们随机分割数据,某个作者可能同时出现在我们的训练集和验证集中。如果某些知名作者具有独特的风格,我们的模型可能会在验证集上因数据泄漏而过度拟合到这种风格,并达到人为高的性能。为了避免这种情况,最好确保每个作者只出现在训练集或验证集中。这与之前描述的学生分级示例中描述的泄漏类型相同。
使用 scikit-learn 的 GroupShuffleSplit 类,并将表示作者唯一 ID 的特征传递给其分割方法,我们可以保证给定作者仅出现在其中一个分割中。
from sklearn.model_selection import GroupShuffleSplit
def get_split_by_author(
posts, author_id_column="OwnerUserId", test_size=0.3, random_state=40
):
"""
Get train/test split
Guarantee every author only appears in one of the splits
:param posts: all posts, with their labels
:param author_id_column: name of the column containing the author_id
:param test_size: the proportion to allocate to test
:param random_state: a random seed
"""
splitter = GroupShuffleSplit(
n_splits=1, test_size=test_size, random_state=random_state
)
splits = splitter.split(posts, groups=posts[author_id_column])
return next(splits)
要查看两种分割方法之间的比较,请参考这本书的 GitHub 存储库中的分割数据笔记本。
一旦数据集被分割,模型就可以拟合到训练集中。我们已经在 “从简单的流水线开始” 中覆盖了训练流水线的必要部分。在本书的 GitHub 存储库中的简单模型笔记本的训练中,我展示了一个端到端的训练流水线示例。我们将分析这个流水线的结果。
我们已经覆盖了我们在分割数据时要牢记的主要风险,但是一旦我们的数据集被分割并且我们在训练分割上训练了一个模型,我们应该做些什么呢?在下一节中,我们将讨论评估训练模型的不同实用方法以及如何最好地利用它们。
评估性能
现在我们已经分割了我们的数据,我们可以训练我们的模型并评估其表现。大多数模型被训练来最小化成本函数,该函数代表模型预测与真实标签的偏离程度。成本函数的值越小,模型对数据的拟合越好。你要最小化的具体函数取决于你的模型和问题,但总体来说,查看它在训练集和验证集上的值通常是个好主意。
这通常有助于估计我们模型的偏差-方差权衡,即衡量我们的模型在多大程度上从数据中学习到了有价值且可推广的信息,而不是记住训练集的细节。
注意
我假设你对标准分类指标很熟悉,但万一你不熟悉,这里是一个简短的提醒。对于分类问题,准确率表示模型正确预测的示例比例。换句话说,它是真实结果的比例,即真正例和真负例的比例。在存在严重不*衡的情况下,高准确率可能掩盖了模型的糟糕表现。如果 99%的情况都是正例,一个总是预测正类的模型将具有 99%的准确率,但可能并不是很有用。精确率、召回率和 f1 分数解决了这个问题。精确率是预测为正类的示例中真正例的比例。召回率是真正例在所有标签为正的元素中的比例。f1 分数是精确率和召回率的调和*均。
在这本书的 GitHub 仓库中,我们训练了一个简单模型笔记本的训练过程,使用了 TF-IDF 向量和我们在“ML 编辑器特性”中确定的特征,训练了第一个随机森林版本。
这里是我们训练集和验证集的准确率、精确率、召回率和 f1 分数。
Training accuracy = 0.585, precision = 0.582, recall = 0.585, f1 = 0.581
Validation accuracy = 0.614, precision = 0.615, recall = 0.614, f1 = 0.612
通过快速查看这些指标,我们可以注意到两件事情:
-
由于我们有一个由两个类组成的*衡数据集,对每个示例随机选择一个类别会给我们大约 50%的准确率。我们模型的准确率达到了 61%,比随机基线要好。
-
我们在验证集上的准确率高于训练集。看来我们的模型在未见过的数据上表现良好。
让我们深入了解模型的表现更多细节。
偏差方差权衡
训练集上的性能差表明存在高偏差,也称为欠拟合,这意味着模型未能捕捉到有用的信息:它甚至不能在它已经知道标签的数据点上表现良好。
训练集表现强劲但验证集表现疲软是高方差的症状,也称为过拟合,意味着模型已经找到了学习输入/输出映射的方法,但它所学习的内容并不能泛化到未见数据。
欠拟合和过拟合是偏差-方差权衡的两个极端情况,描述了模型在复杂度增加时错误类型的变化。随着模型复杂度的增加,方差增加而偏差减少,模型由欠拟合逐渐转变为过拟合。你可以在图 5-5 中看到这一点。

图 5-5. 随着复杂度增加,偏差减少但方差也增加
在我们的情况下,由于验证性能优于训练性能,我们可以看出模型没有过度拟合训练数据。我们可能可以增加模型的复杂度或特征以改善性能。应对偏差-方差权衡需要找到在减少偏差(提高模型在训练集上的性能)和减少方差(提高模型在验证集上的性能,通常作为副产品恶化训练性能)之间的最佳点。
性能指标有助于生成模型性能的整体视角。这对猜测模型的表现很有帮助,但并不能提供对模型在具体方面成功或失败的深刻理解。要改进我们的模型,我们需要深入挖掘。
超越汇总指标
性能度量帮助确定模型是否从数据集中正确学习,或者是否需要改进。下一步是进一步检查结果,以了解模型是失败还是成功的方式。这一点至关重要,原因有两个:
性能验证
性能指标可能非常具有误导性。在处理严重不*衡数据(例如预测少于 1% 患者中出现的罕见疾病)的分类问题时,任何总是预测患者健康的模型将达到 99% 的准确率,尽管它根本没有预测能力。大多数问题适用的性能指标(f1 分数 对于前述问题更合适),但关键是要记住它们是汇总指标,描绘了情况的不完整图景。要信任模型的性能,需要在更细粒度的水*上检查结果。
迭代
模型构建是一个迭代过程,开始迭代循环的最佳方式是确定需要改进的内容及其改进方法。性能指标不能帮助确定模型在哪些方面存在问题,以及管道的哪一部分需要改进。我经常看到数据科学家仅仅通过尝试许多其他模型或超参数,或者随意构建额外特征来试图提高模型性能。这种方法就像闭着眼睛往墙上扔飞镖一样。快速构建成功模型的关键是识别和解决模型失败的具体原因。
在考虑这两个动机的基础上,我们将介绍几种深入了解模型性能的方法。
评估您的模型:超越准确率
有很多方法可以检查模型的表现,我们不会涵盖每种潜在的评估方法。我们将专注于一些通常有助于挖掘潜在问题的方法。
当涉及调查模型性能时,把自己想象成侦探,每种涵盖的方法都是揭示线索的不同方式。我们将首先介绍多种技术,通过对比模型预测与数据来发现有趣的模式。
对比数据和预测
深入评估模型的第一步是找到比聚合指标更细粒度的方式,以对比数据和预测。我们希望分析不同数据子集上的聚合性能指标,例如分类中的准确率、精确率或召回率。让我们看看如何针对常见的机器学习挑战来实现这一点。
您可以在该书的 GitHub 代码库的“将数据与预测进行比较”笔记本中找到所有代码示例。
对于分类问题,我通常建议首先查看混淆矩阵,显示在图 5-6 中,其行表示每个真实类别,列表示我们模型的预测。一个预测完美的模型将拥有一个只有对角线上非零的混淆矩阵。然而在现实中,这种情况很少见。让我们看看为什么混淆矩阵通常非常有用。
混淆矩阵
混淆矩阵能够让我们一眼看出模型在某些类别上是否特别成功,而在其他类别上是否有困难。这对于包含许多不同类别或类别不*衡的数据集尤其有用。
我经常看到那些准确率惊人的模型展示一个完全空的列的混淆矩阵,这意味着有一个模型从不预测的类别。这通常发生在稀有类别上,并且有时是无害的。然而,如果稀有类别代表一个重要的结果,比如借款人违约,混淆矩阵将帮助我们注意到问题。我们可以通过在模型损失函数中更加重视稀有类别来纠正这一问题,例如。
图 5-6 的顶行显示,我们训练的初始模型在预测低质量问题时表现良好。底行显示,模型在检测所有高质量问题方面存在困难。确实,在所有得分高的问题中,我们的模型只有一半时间正确预测其类别。然而,观察右列,我们可以看到当模型预测问题是高质量时,其预测往往是准确的。
混淆矩阵在处理多类问题时尤为有用。例如,我曾与一名工程师合作,他试图对语音中的单词进行分类,并绘制了最新模型的混淆矩阵。他立即注意到两个对称的非对角线数值异常高。这两个类别(每个代表一个单词)混淆了模型,并导致了大部分错误的原因。进一步检查后发现,混淆模型的单词是when和where。为这两个示例收集额外数据足以帮助模型更好地区分这些听起来相似的单词。
混淆矩阵允许我们将模型的预测与每个类别的真实类别进行比较。在调试模型时,我们可能希望深入查看模型输出的概率,而不仅仅是它们的预测结果。

图 5-6. 我们问题分类任务的初始基线混淆矩阵
ROC 曲线
对于二元分类问题,接收者操作特征(ROC)曲线也可以提供非常有用的信息。ROC 曲线将真正率(TPR)作为误报率(FPR)的函数进行绘制。
在分类中使用的绝大多数模型返回一个给定示例属于某一类的概率分数。这意味着在推断时,如果模型给出的概率高于某个阈值,我们可以选择将示例归属于某一类。这通常称为决策阈值。
大多数分类器默认使用 50%的概率作为决策阈值,但这是可以根据我们的用例进行更改的。通过定期从 0 到 1 变化阈值,并在每个点测量 TPR 和 FPR,我们得到一个 ROC 曲线。
一旦我们有了模型的预测概率和相关的真实标签,使用 scikit-learn 简单地获取 FPR 和 TPR,然后生成 ROC 曲线。
from sklearn.metrics import roc_curve
fpr, tpr, thresholds = roc_curve(true_y, predicted_proba_y)
了解 ROC 曲线的两个重要细节,例如在图 5-7 中绘制的那条。首先,沿着从左下到右上的对角线表示随机猜测。这意味着为了超越随机基线,分类器/阈值对应应该在这条线的上方。此外,完美模型将由左上角的绿色虚线表示。

图 5-7. 初始模型的 ROC 曲线
由于这两个细节,分类模型通常使用曲线下面积(AUC)来表示性能。AUC 越大,我们的分类器就越接*“完美”模型。随机模型的 AUC 为 0.5,而完美模型的 AUC 为 1。然而,在考虑实际应用时,我们应选择一个特定的阈值,以获得对我们用例最有用的 TPR/FPR 比率。
因此,我建议在 ROC 曲线上添加垂直或水*线,以表示我们的产品需求。在构建一个系统,如果认为紧急情况足够紧急,则将客户请求路由到员工的情况下,您能够承受的 FPR 完全由您支持人员的能力和您拥有的用户数量决定。这意味着任何 FPR 高于该限制的模型都不应被考虑。
在 ROC 曲线上绘制一个阈值使您可以设定一个比仅仅获取最大 AUC 分数更具体的目标。确保您的努力能够达到您的目标!
我们的 ML 编辑器模型将问题分类为好的或坏的。在这种情况下,TPR 表示我们的模型正确判断为好的高质量问题的比例。FPR 是我们的模型声称是好问题的坏问题的比例。如果我们不能帮助我们的用户,那么我们至少希望不要伤害他们。这意味着我们不应使用任何可能频繁推荐坏问题的模型。因此,我们应该设置一个 FPR 的阈值,例如 10%,并使用我们能找到的在该阈值以下的最佳模型。在图 5-8 中,您可以看到这一要求在我们的 ROC 曲线上的表示;这显著减少了适用于模型的可接受决策阈值的空间。
ROC 曲线为我们提供了一个更细致的视角,显示了模型在我们使其预测更保守或更激进时的性能变化。观察模型的预测概率分布与真实类分布的比较是另一种方式,用于检查其是否校准良好。

图 5-8. 添加表示我们产品需求的 ROC 线条
校准曲线
校准图是二元分类任务中另一个信息丰富的图表,因为它可以帮助我们了解我们的模型输出的概率是否很好地代表了其置信度。校准图显示了我们分类器的置信度作为真正例子的分数的函数。
例如,我们的分类器对所有数据点都给出了高于 80%的正分类概率,其中有多少数据点实际上是正的?一个完美模型的校准曲线将是一个从左下到右上的对角线。
在图 5-9 中,我们可以看到在顶部,我们的模型在 0.2 到 0.7 之间的校准很好,但在该范围之外的概率上并不好。在下面查看预测概率直方图,我们可以看出,我们的模型很少预测超出该范围的概率,这可能导致之前显示的极端结果。该模型对其预测很少有信心。

图 5-9. 校准曲线:对角线代表一个完美的模型(顶部);预测值的直方图(底部)
对于许多问题,例如在广告投放中预测点击率,当概率接* 0 或 1 时,数据将导致我们的模型相当倾斜,而校准曲线将帮助我们一目了然地看到这一点。
为了诊断模型的性能,可视化单个预测结果非常有价值。让我们讨论一些方法来使这个可视化过程更加高效。
错误的降维
我们在“向量化”和“降维”章节描述了数据探索中的向量化和降维技术。让我们看看这些技术如何用于使错误分析更加高效。
当我们首次介绍如何使用降维方法来可视化数据时,我们通过其类别对数据集中的每个点进行了颜色标记,以观察标签的拓扑结构。在分析模型错误时,我们可以使用不同的颜色方案来识别错误。
要识别错误趋势,可以根据模型预测的准确性对每个数据点进行颜色标记。这样可以帮助您找出模型在哪些类型的相似数据点上表现不佳。一旦确定了模型表现不佳的区域,可以对其中的几个数据点进行可视化。可视化难例是生成能帮助模型更好拟合这些例子中所表示特征的一种很好的方式。
为了帮助发现难例中的趋势,还可以使用来自“聚类”的聚类方法。在对数据进行聚类后,评估模型在每个簇上的性能,并识别模型表现最差的簇。检查这些簇中的数据点可以帮助您生成更多的特征。
降维技术是浮现挑战性示例的一种方式。为此,我们还可以直接使用模型的置信度分数。
Top-k 方法
找到密集错误区域有助于识别模型的失败模式。在上文中,我们使用降维技术帮助我们找到这些区域,但我们也可以直接使用模型本身。通过利用预测概率,我们可以识别模型最具挑战性或最不确定的数据点。让我们称这种方法为 top-k 方法。
Top-k 方法很直接。首先,选择一些易于可视化的示例,我们将其称为 k。对于单人进行的个人项目,建议从十到十五个示例开始。对于您先前找到的每个类别或聚类,请进行可视化:
-
最优表现的 k 个示例
-
最差表现的 k 个示例
-
最不确定的 k 个示例
通过可视化这些示例,您可以识别模型认为易、难或令人困惑的示例。让我们更详细地探讨每个类别。
最优表现的 k 个示例
首先,显示模型预测正确且最有信心的 k 个示例。在可视化这些示例时,目标是识别它们之间的特征值共性,以解释模型的性能。这将帮助您识别模型成功利用的特征。
在可视化成功示例以识别模型利用的特征后,再绘制不成功的示例以识别模型未能捕捉到的特征。
最差表现的 k 个示例
显示模型预测错误且最有信心的 k 个示例。首先使用训练数据中的 k 个示例,然后再使用验证数据。
就像可视化错误聚类一样,通过可视化模型在训练集中表现最差的 k 个示例,可以帮助识别模型失败的数据点的趋势。显示这些数据点以帮助您识别能够使模型更轻松的其他特征。
当探索 ML 编辑器的初始模型错误时,例如,我发现一些发布的问题由于没有包含实际问题而得分低。初始模型无法预测这些“非问题”问题,因此我添加了一个功能来计算文本主体中的问号数。添加此功能使得模型能够准确预测这些问题。
可视化验证数据中最差的 k 个示例有助于识别与训练数据显著不同的示例。如果您在验证集中确实识别出太难的示例,请参考“分割数据集”中的提示更新您的数据分割策略。
最后,模型并非始终自信地正确或错误;它们也可能输出不确定的预测。接下来我将介绍这些情况。
最不确定的 k 个示例
可视化最不确定的 k 个示例包括显示模型在其预测中最不自信的示例。对于本书主要关注的分类模型,不确定的示例是模型对每个类输出尽可能接*相等概率的示例。
如果模型经过良好的校准(参见“校准曲线”以了解校准的解释),它将为人工标注员也会不确定的示例输出均匀的概率。例如,对于猫与狗分类器,包含狗和猫的图片会属于这一类别。
在训练集中的不确定示例通常是冲突标签的症状。确实,如果训练集包含两个重复或相似的示例,每个示例被标记为不同的类,则当模型使用此示例时,为了最小化损失,它将为每个类输出相等的概率。因此,冲突标签会导致不确定的预测,您可以使用 top-k 方法来尝试找到这些示例。
绘制验证集中最不确定的 top-k 示例可以帮助找出训练数据中的差距。对于模型不确定但对于人工标注员明确的验证示例,通常表明模型在其训练集中未接触到这种数据类型。绘制验证集中最不确定的 top-k 示例可以帮助识别应该存在于训练集中的数据类型。
Top-k 评估可以以直接的方式实现。在下一节中,我将分享一个工作示例。
Top-k 实现技巧
以下是与 pandas DataFrame 一起工作的简单的 top-k 实现。该函数以包含预测概率和标签的 DataFrame 作为输入,并返回每个 top-k 以上的结果。它可以在这本书的 GitHub 仓库中找到。
def get_top_k(df, proba_col, true_label_col, k=5, decision_threshold=0.5):
"""
For binary classification problems
Returns k most correct and incorrect example for each class
Also returns k most unsure examples
:param df: DataFrame containing predictions, and true labels
:param proba_col: column name of predicted probabilities
:param true_label_col: column name of true labels
:param k: number of examples to show for each category
:param decision_threshold: classifier decision boundary to classify as
positive
:return: correct_pos, correct_neg, incorrect_pos, incorrect_neg, unsure
"""
# Get correct and incorrect predictions
correct = df[
(df[proba_col] > decision_threshold) == df[true_label_col]
].copy()
incorrect = df[
(df[proba_col] > decision_threshold) != df[true_label_col]
].copy()
top_correct_positive = correct[correct[true_label_col]].nlargest(
k, proba_col
)
top_correct_negative = correct[~correct[true_label_col]].nsmallest(
k, proba_col
)
top_incorrect_positive = incorrect[incorrect[true_label_col]].nsmallest(
k, proba_col
)
top_incorrect_negative = incorrect[~incorrect[true_label_col]].nlargest(
k, proba_col
)
# Get closest examples to decision threshold
most_uncertain = df.iloc[
(df[proba_col] - decision_threshold).abs().argsort()[:k]
]
return (
top_correct_positive,
top_correct_negative,
top_incorrect_positive,
top_incorrect_negative,
most_uncertain,
)
让我们通过 ML Editor 来说明 top-k 方法。
ML Editor 的 top-k 方法
我们将应用 top-k 方法于我们训练的第一个分类器。关于 top-k 方法的使用示例的笔记本可以在这本书的 GitHub 仓库中找到。
图 5-10 显示了我们的第一个 ML Editor 模型每个类别最正确的前两个示例。两个类别之间最大的特征差异是text_len,表示文本的长度。分类器已学会好问题往往较长,而差问题较短。它严重依赖文本长度来区分类别。

图 5-10. 最正确的 top-k
图 5-11 证实了这一假设。我们的分类器预测未回答的问题最有可能被回答的是最长的问题,反之亦然。这一观察结果也证实了我们在“评估特征重要性”中发现的情况,即text_len是最重要的特征。

图 5-11. 最不正确的 Top-k
我们已经确定分类器利用text_len轻松识别已回答和未回答的问题,但这个特征并不足够,会导致错误分类。我们应该添加更多特征来改进我们的模型。通过可视化超过两个示例可以帮助识别更多候选特征。
在训练和验证数据上使用 top-k 方法有助于确定我们模型和数据集的限制。我们已经讨论了它如何帮助确定模型是否具备表示数据的能力,数据集是否足够*衡以及是否包含足够的代表性示例。
我们主要讨论了分类模型的评估方法,因为这些模型适用于许多具体问题。让我们简要地看看在不进行分类时如何检查性能。
其他模型
许多模型可以使用分类框架进行评估。例如,在目标检测中,模型的目标是在图像中输出围绕感兴趣对象的边界框,准确率是常见的指标。由于每个图像可以有多个表示对象和预测的边界框,计算准确度需要额外的步骤。首先,计算预测和标签之间的重叠(通常使用Jaccard 指数)允许将每个预测标记为正确或不正确。然后,可以计算准确度并在本章节中使用所有先前的方法。
类似地,当构建旨在推荐内容的模型时,迭代的最佳方式通常是在各种类别上测试模型并报告其性能。评估过程类似于一个分类问题,其中每个类别代表一个类。
对于可能证明棘手的问题类型,例如生成模型,您仍然可以使用之前对数据的探索来将数据集分成多个类别,并为每个类别生成性能指标。
当我与一位数据科学家合作构建句子简化模型时,检查模型在句子长度条件下的表现显示,较长的句子对模型来说更加困难。这需要检查和手动标记,但却指明了明确的下一步行动,即通过增加包含长句子的训练数据来改善性能,这帮助显著提高了表现。
我们已经涵盖了许多检验模型性能的方法,通过将其预测与标签进行对比,但我们也可以检查模型本身。如果一个模型表现不佳,尝试解释其预测可能是值得的。
评估特征重要性
分析模型性能的另一种方法是检查模型用于进行预测的数据特征。这样做被称为特征重要性分析。评估特征重要性有助于消除或迭代当前对模型没有帮助的特征。特征重要性还可以帮助识别那些可疑预测的特征,这通常是数据泄漏的迹象。我们将首先为可以轻松生成特征重要性的模型生成特征重要性,然后涵盖那些直接提取这些特征可能不容易的情况。
直接从分类器获取
要验证模型是否正常工作,可视化模型正在使用或忽略的特征。对于简单模型如回归或决策树,通过查看模型的学习参数,可以直观地提取特征的重要性。
对于我们在 ML 编辑案例研究中使用的第一个模型,即随机森林,我们可以简单地使用 scikit-learn 的 API 来获取所有特征重要性的排名列表。特征重要性代码及其使用方法可以在此书的 GitHub 仓库中的特征重要性笔记本找到。
def get_feature_importance(clf, feature_names):
importances = clf.feature_importances_
indices_sorted_by_importance = np.argsort(importances)[::-1]
return list(
zip(
feature_names[indices_sorted_by_importance],
importances[indices_sorted_by_importance],
)
)
如果我们在训练好的模型上使用上述函数,并进行一些简单的列表处理,我们可以得到一个简单的十大最具信息量特征列表:
Top 10 importances:
text_len: 0.0091
are: 0.006
what: 0.0051
writing: 0.0048
can: 0.0043
ve: 0.0041
on: 0.0039
not: 0.0039
story: 0.0039
as: 0.0038
在这里需要注意几点:
-
文本长度是最具信息量的特征。
-
我们生成的其他特征根本不会出现,其重要性比其他特征低一个数量级以上。该模型无法利用它们有效地区分类别。
-
其他特征代表的是非常常见的词汇,或者与写作主题相关的名词。
因为我们的模型和特征都很简单,这些结果实际上可以给我们提供构建新特征的思路。例如,我们可以添加一个计数常见和罕见词汇使用情况的特征,以查看它们是否能预测答案得到高分。
如果特征或模型变得复杂,生成特征重要性需要使用模型可解释性工具。
黑盒解释器
当特征变得复杂时,特征的重要性可能变得更难解释。一些复杂模型如神经网络甚至可能无法公开其学习到的特征重要性。在这种情况下,利用黑盒解释器可能是有用的,它们试图独立解释模型的预测,而不关注其内部运作。
通常,这些解释器识别给定数据点上模型的预测特征,而不是全局性的。它们通过改变给定示例的每个特征值,并观察模型预测如何随之变化来实现这一点。LIME 和 SHAP 是两种流行的黑盒解释器。
要了解如何使用这些的端到端示例,请参阅书籍的 GitHub 仓库中的黑盒解释器笔记本。
图 5-12 展示了由 LIME 提供的解释,说明了决定将这个示例问题分类为高质量的最重要单词。LIME 通过反复从输入问题中删除单词并观察哪些单词使我们的模型更倾向于一个类别而生成这些解释。

图 5-12. 解释一个特定的示例
我们可以看到,模型正确预测该问题将获得高分。然而,模型并不自信,仅输出 52%的概率。图 5-12 右侧显示了预测中最具影响力的单词。这些词似乎不应特别与问题高质量相关联,因此让我们查看更多示例,看看模型是否利用了更有用的模式。
要快速了解趋势,我们可以在更大的问题样本上使用 LIME。对每个问题运行 LIME 并汇总结果可以让我们了解我们的模型在整体上认为哪个词具有预测性来做出其决策。
在 图 5-13 中,我们绘制了数据集中 500 个问题的最重要预测。我们可以看到我们的模型倾向于利用常见单词的趋势在这个更大的样本中也很明显。看起来模型在推广到利用频繁单词之外时遇到了困难。表示稀有单词的词袋特征通常有零值。为了改进这一点,我们可以收集更大的数据集,让我们的模型接触更多样化的词汇,或者创建少量稀疏的特征。

图 5-13. 解释多个示例
你经常会对你的模型最终使用的预测器感到惊讶。如果某些特征对模型的预测比你预期的更具预测性,请尝试找到训练数据中包含这些特征的示例并检查它们。利用这个机会再次检查你如何分割你的数据集,并注意数据泄漏的情况。
例如,当构建一个模型自动将电子邮件根据其内容分类到不同主题时,有一次我在指导的机器学习工程师发现,最佳预测器是电子邮件顶部的一个三字母代码。结果发现这是数据集的内部代码,几乎完美地映射到了标签。模型完全忽略了电子邮件的内容,而是记忆了一个预先存在的标签。这是数据泄漏的一个明显例子,只有通过查看特征重要性才能发现。
结论
我们从覆盖到如何根据我们到目前为止学到的所有知识来决定初始模型的标准开始了本章。然后,我们讨论了将数据分割成多组的重要性,以及避免数据泄漏的方法。
训练了一个初始模型后,我们深入研究了如何评估其性能,通过不同的方式比较和对比其预测与数据。最后,我们检查了模型本身,显示了特征重要性,并使用黑盒解释器来直观地理解它用于预测的特征。
到现在为止,你应该对如何改进你的建模有了一些直觉。这将引导我们到第六章的话题,深入探讨我们在此处提到的问题处理方法,通过调试和故障排除机器学习管道。
第六章:调试你的机器学习问题
在上一章中,我们训练并评估了我们的第一个模型。
将管道提升到令人满意的性能水*是困难的,并且需要多次迭代。本章的目标是引导你通过一个这样的迭代周期。在本章中,我将介绍调试建模管道的工具以及编写测试的方法,以确保一旦我们开始更改它们,它们仍然可以正常工作。
软件最佳实践鼓励从业者定期测试、验证和检查他们的代码,特别是对于诸如安全性或输入解析等敏感步骤。这对于机器学习同样适用,因为模型中的错误比传统软件更难检测。
我们将介绍一些技巧,帮助你确保你的管道是稳健的,并且你可以在不导致整个系统失败的情况下进行尝试,但首先让我们深入了解软件最佳实践!
软件最佳实践
对于大多数机器学习项目而言,你将重复构建模型、分析其缺陷并多次解决它们的过程。你还可能多次更改基础架构的每个部分,因此找到提高迭代速度的方法至关重要。
在机器学习项目中,就像在任何其他软件项目中一样,你应该遵循经过时间考验的软件最佳实践。其中大部分可以直接应用于机器学习项目而无需修改,比如只构建你所需的部分,通常被称为保持简单愚蠢(KISS)原则。
机器学习项目是迭代性质的,并且经历了许多不同的数据清理和特征生成算法以及模型选择的迭代过程。即使遵循了这些最佳实践,调试和测试仍然是减慢迭代速度的两个主要因素。加快调试和编写测试的速度对任何项目都会产生显著影响,但对于机器学习项目来说尤为重要,因为模型的随机性质往往会把一个简单的错误变成数天的调查。
存在许多资源可以帮助你学习如何调试通用程序,比如芝加哥大学的简明调试指南。如果像大多数机器学习从业者一样,你的首选语言是 Python,我建议查阅 Python 文档中有关标准库调试器pdb的内容。
然而,与大多数软件不同的是,机器学习代码通常可能会表现得看似正确,但实际产生完全荒谬的结果。这意味着虽然这些工具和技巧可以直接应用于大多数机器学习代码,但它们并不足以诊断常见问题。我在图 6-1 中举例说明了这一点:在大多数软件应用中,强大的测试覆盖率可以让我们对应用程序的功能性有很高的信心,而机器学习管道可以通过许多测试,但仍然产生完全不正确的结果。一个机器学习程序不仅仅要运行——它应该产生准确的预测输出。

图 6-1. 一个机器学习管道可以执行无误,但仍可能是错误的
因为机器学习在调试时面临额外的一系列挑战,让我们讨论一些有助于解决这些问题的具体方法。
机器学习特定的最佳实践
对于机器学习而言,仅仅让程序端到端执行是不足以确信其正确性的。一个整个的管道可以运行无误地生成一个完全无用的模型。
假设你的程序加载数据并传递给模型。你的模型接受这些输入,并根据学习算法优化模型的参数。最后,你训练好的模型从另一组数据中产生输出。你的程序运行时没有显示任何 bug。问题在于,仅仅让程序运行,并不能保证你的模型预测是正确的。
大多数模型仅接受给定形状的数值输入(比如代表图像的矩阵),并输出不同形状的数据(例如输入图像中关键点的坐标列表)。这意味着,如果数据处理步骤在将数据传递给模型之前损坏了数据,大多数模型仍然可以运行,只要数据仍然是数值型的,并且是模型可以接受的形状。
如果你的建模管道表现不佳,你如何知道是模型质量不佳,还是在过程的早期阶段存在 bug?
在机器学习中解决这些问题的最佳方法是采用逐步的方法。首先验证数据流,然后是学习能力,最后是泛化和推断。图 6-2 展示了本章将涵盖的流程概述。

图 6-2. 调试管道的顺序
这一章将带领你逐步完成这三个步骤,并深入解释每一个步骤。当面对棘手的 bug 时,有时会很诱人地跳过计划中的某些步骤,但我发现,绝大多数情况下,遵循这种有原则的方法是识别和纠正错误的最快途径。
让我们从验证数据流开始。最简单的方法是采用非常小的数据子集,并验证它是否可以顺利通过整个管道。
调试布线:可视化和测试
这第一步非常简单,但一旦采纳,会极大地简化生活:从让你的管道在数据集的一个小子集上工作开始。这对应于图 6-2 中的布线步骤。一旦确保你的管道适用于一些示例,你就可以编写测试,以确保在进行更改时,你的管道仍然能够正常工作。
从一个示例开始
此初步步骤的目标是验证你能够接受数据,将其转换为正确的格式,传递给模型,并使模型输出正确的结果。在这个阶段,你不是在评判你的模型是否能够学到东西,而是在评估管道是否能够顺利传递数据。
具体来说,这意味着:
-
从你的数据集中选择几个示例
-
让你的模型为这些示例输出预测结果
-
使你的模型更新其参数,为这些示例输出正确的预测
前两项集中在验证我们的模型能够接受输入数据并产生看起来合理的输出。从建模的角度来看,这初步输出很可能是错误的,但它将允许我们检查数据是否完全流动。
最后一项的目标是确保我们的模型能够从给定输入到相关输出的映射中学习。拟合几个数据点不会产生有用的模型,而且很可能会导致过拟合。这个过程只是让我们验证模型是否能够更新其参数以适应一组输入和输出。
这是第一步在实践中的应用方式:如果你正在训练一个模型来预测 Kickstarter 项目是否成功,你可能计划在过去几年的所有项目上进行训练。按照这个提示,你应该从检查你的模型是否能够输出两个项目的预测开始。然后,使用这些项目的标签(它们是否成功)来优化模型的参数,直到它预测出正确的结果。
如果我们选择了合适的模型,它应该有能力从我们的数据集中学习。如果我们的模型能够从整个数据集中学习,它应该有能力记忆一个数据点。从几个示例中学习是模型从整个数据集中学习的必要条件。验证它比整个学习过程要容易得多,因此从一个示例开始允许我们迅速缩小任何潜在的未来问题。
在这个初步阶段可能出现的绝大多数错误与数据不匹配有关:你加载和预处理的数据以一种模型无法接受的格式输入。例如,由于大多数模型只接受数值,当给定值为空且具有空值时,它们可能会失败。
一些数据不匹配的情况可能更加难以察觉,并导致悄无声息的失败。如果一个管道输入的值不在正确的范围或形状内,它仍然可以运行,但会生成性能较差的模型。需要标准化数据的模型通常仍会在非标准化数据上进行训练:它们只是不能以有用的方式适应它。类似地,向模型提供错误形状的矩阵可能会导致它错误地解释输入并产生错误的输出。
捕捉这类错误更加困难,因为它们将在评估模型性能时稍后在流程中显现。主动检测它们的最佳方法是在构建管道时将数据可视化,并构建测试来编码假设。接下来我们将看看如何做到这一点。
可视化步骤
正如我们在前几章中看到的那样,虽然指标是建模工作的重要组成部分,但定期检查和调查我们的数据同样重要。从几个例子开始观察,会更容易注意到变化或不一致。
这个过程的目标是定期检查变化。如果你将数据管道比作装配线,你会希望在每次重要变更后检查产品。这意味着在每一行检查数据点的价值可能太频繁,而仅查看输入和输出值显然不够信息丰富。
在图 6-3 中,我展示了一些可以用来检查数据管道的示例检查点。在这个例子中,我们在多个步骤检查数据,从原始数据到模型输出。

图 6-3. 潜在的检查点
接下来,我们将讨论几个通常值得检查的关键步骤。我们将从数据加载开始,然后进行清理、特征生成、格式化和模型输出。
数据加载
无论您是从磁盘加载数据还是通过 API 调用,都需要验证数据格式是否正确。这个过程类似于进行 EDA 时的过程,但是这里是在您构建的管道的上下文中进行,以验证没有错误导致数据损坏。
它是否包含您预期的所有字段?这些字段中是否有任何空值或常量值?任何值是否在看起来不正确的范围内,例如年龄变量有时为负数?如果您处理的是文本、语音或图像,示例是否符合您对其外观、声音或读取方式的预期?
我们大多数处理步骤依赖于我们对输入数据结构的假设,因此验证这一方面至关重要。
因为这里的目标是识别我们对数据的期望与实际情况之间的不一致,您可能希望可视化超过一两个数据点。可视化代表性样本将确保我们不仅观察到“幸运”的示例,并错误地假设所有数据点质量相同。
图 6-4 显示了来自本书的 GitHub 存储库中数据集探索笔记本案例研究的示例。在这里,我们归档的数百篇帖子中有一些未记录的帖子类型,因此需要进行筛选。在图中,您可以看到带有 PostTypeId 为 5 的行,这在数据集文档中未被引用,因此我们将其从训练数据中移除。

图 6-4. 数据的可视化
一旦验证数据符合数据集文档中的预期,就可以开始为建模目的处理数据了。这始于数据清洗。
清洗和特征选择
大多数管道的下一步是移除任何不必要的信息。这可能包括模型不会在生产中使用的字段或值,以及可能包含有关标签信息的字段,而我们的模型在生产中将无法访问(参见“拆分数据集”)。
每个移除的特征都可能成为模型的一个潜在预测器。决定保留哪些特征,移除哪些特征的任务被称为特征选择,是迭代模型的一个不可或缺的部分。
你应该验证没有遗漏关键信息,所有不需要的值都已移除,并且没有额外信息留在数据集中,这些信息可能通过泄露信息来人为地提升我们模型的性能(参见“数据泄露”)。
数据清洗完成后,你需要为模型生成一些特征来使用。
特征生成
当生成新的特征时,例如在描述 Kickstarter 活动中添加对产品名称引用频率的特征时,检查其值非常重要。你需要检查特征值是否填充,并且这些值看起来是否合理。这是一个具有挑战性的任务,因为它不仅需要识别所有特征,还需要为每个特征估计合理的值。
此时,你不需要再深入分析它了,因为这一步是专注于验证关于数据流经模型的假设,而不是数据或模型的实用性。
特征生成完成后,你应该确保它们能够以模型能理解的格式传递给模型。
数据格式化
正如我们在之前的章节中讨论过的,在将数据点传递给模型之前,你需要将它们转换为模型能理解的格式。这可以包括规范化输入值,通过数值化表示将文本向量化,或将黑白视频格式化为 3D 张量(参见“向量化”)。
如果你在解决一个监督问题,除了输入之外,你还会使用一个标签,比如分类中的类名,或者图像分割中的分割图。这些也需要转换为模型可理解的格式。
根据我在多个图像分割问题上的经验,例如,标签与模型预测之间的数据不匹配是错误的最常见原因之一。分割模型使用分割掩模作为标签。这些掩模与输入图像大小相同,但不同于像素值,它们包含每个像素的类标签。不幸的是,不同的库使用不同的约定来表示这些掩模,因此标签经常以错误的格式结束,阻止模型学习。
我在 图 6-5 中说明了这个常见的陷阱。假设一个模型期望分割掩模的像素值为 255,表示某个类,其他情况下为 0。如果用户错误地假设掩模内的像素应该是 1 而不是 255,则他们可能以“提供的”格式传递他们的标记掩模。这将导致模型将掩模视为几乎为空,从而输出不准确的预测结果。

图 6-5. 标签格式不佳将阻止模型学习
同样,分类标签通常表示为一个以真实类索引处为单个 1,其余为零的列表。简单的误差可能导致标签被移位,模型学习始终预测移位后的标签。如果您没有花时间查看数据,则很难排除此类错误。
因为机器学习模型通常可以适应大多数数字输出,而不管它们是否具有准确的结构或内容,这一阶段是许多棘手错误发生的地方,也是找出这些错误的有用方法。
这是我们案例研究的格式化函数的示例。我生成我们问题文本的向量化表示。然后,我将附加特征附加到这个表示中。由于该函数包含多个转换和向量操作,可视化此函数的返回值将帮助我验证它确实按我们打算的方式格式化数据。
def get_feature_vector_and_label(df, feature_names):
"""
Generate input and output vectors using the vectors feature and
the given feature names
:param df: input DataFrame
:param feature_names: names of feature columns (other than vectors)
:return: feature array and label array
"""
vec_features = vstack(df["vectors"])
num_features = df[feature_names].astype(float)
features = hstack([vec_features, num_features])
labels = df["Score"] > df["Score"].median()
return features, labels
features = [
"action_verb_full",
"question_mark_full",
"text_len",
"language_question",
]
X_train, y_train = get_feature_vector_and_label(train_df, features)
尤其是在处理文本数据时,通常需要多个步骤才能正确格式化数据以供模型使用。从文本字符串到标记化列表,再到包括潜在附加特征的向量化表示,这是一个容易出错的过程。甚至在每个步骤检查对象的形状也有助于捕捉许多简单的错误。
一旦数据格式正确,您可以将其传递给模型。最后一步是可视化和验证模型的输出。
模型输出
首先,观察输出有助于我们确定模型的预测是否是正确类型或形状(如果我们预测房价和市场上市时间,我们的模型是否输出一个包含两个数字的数组?)。
此外,当将模型拟合到仅有几个数据点时,我们应该看到其输出开始匹配真实标签。如果模型不适合数据点,则可能表明数据格式不正确或已损坏。
如果模型的输出在训练过程中根本不改变,这可能意味着我们的模型实际上没有利用输入数据。在这种情况下,我建议参考“站在巨人的肩膀上”来验证模型是否被正确使用。
一旦我们为几个示例完整地通过了整个管道,现在是时候编写一些测试来自动化部分可视化工作了。
系统化我们的视觉验证
完成早期描述的可视化工作有助于捕捉大量错误,并且对每一个新的管道来说都是一个良好的时间投资。验证数据如何流经模型的假设有助于节省大量时间,现在可以用于专注于训练和泛化。
然而,管道经常发生变化。当您迭代更新不同方面以改进您的模型并修改一些处理逻辑时,如何确保一切仍按预期工作?每次进行任何更改时,通过整个管道和每个步骤的示例进行可视化将很快变得令人疲倦。
这就是我们之前讨论过的软件工程最佳实践发挥作用的地方。现在是时候隔离管道的每一部分,并将我们的观察编码为测试,随着管道的变化而运行,以验证它。
分开你的关注点
就像常规软件一样,ML 从模块化组织中受益良多。为了使当前和未来的调试更容易,将每个函数分开,这样你可以在查看更广泛的流程之前逐个检查它们的工作情况。
一旦管道被拆分成单独的函数,您就可以为它们编写测试。
测试您的 ML 代码
测试模型的行为很难。然而,ML 管道中的大多数代码并不涉及训练管道或模型本身。如果回顾我们在“从一个简单的管道开始”中的管道示例,大多数函数表现出确定性的行为,可以进行测试。
根据我的经验,帮助工程师和数据科学家调试其模型时,我学到了大多数错误都来自数据获取、处理或输入模型的方式。因此,测试数据处理逻辑对于构建成功的 ML 产品至关重要。
关于 ML 系统潜在测试的更多信息,我建议阅读 E. Breck 等人的论文“ML 测试分数:ML 生产准备和技术债务减少的标尺”,其中包含更多的例子和从谷歌部署这类系统中学到的经验。
在接下来的部分中,我们将描述为三个关键领域编写的有用测试。在图 6-6 中,您可以看到每个领域以及我们将接下来描述的一些测试示例。

图 6-6. 测试的三个关键领域
管道从摄入数据开始,因此我们首先要测试这部分。
测试数据摄入
数据通常存储在磁盘或数据库中。在将数据从存储传输到我们的管道时,我们应确保验证数据的完整性和正确性。我们可以开始编写测试,验证我们加载的数据点是否具备我们将需要的每个特征。
下面是验证我们解析器返回正确类型(数据帧)、所有重要列已定义以及特征不全为空的三个测试。你可以在本书的 GitHub 存储库的测试文件夹中找到本章涵盖的测试(以及额外的测试)。
def test_parser_returns_dataframe():
"""
Tests that our parser runs and returns a DataFrame
"""
df = get_fixture_df()
assert isinstance(df, pd.DataFrame)
def test_feature_columns_exist():
"""
Validate that all required columns are present
"""
df = get_fixture_df()
for col in REQUIRED_COLUMNS:
assert col in df.columns
def test_features_not_all_null():
"""
Validate that no features are missing every value
"""
df = get_fixture_df()
for col in REQUIRED_COLUMNS:
assert not df[col].isnull().all()
我们还可以测试每个特征的类型,并验证它不是空的。最后,我们可以通过测试它们的*均、最小和最大值来编码我们对这些值分布和范围的假设。最*,像Great Expectations这样的库已经出现,直接测试特征的分布。
在这里,您可以看到如何编写一个简单的*均测试:
ACCEPTABLE_TEXT_LENGTH_MEANS = pd.Interval(left=20, right=2000)
def test_text_mean():
"""
Validate that text mean matches with exploration expectations
"""
df = get_fixture_df()
df["text_len"] = df["body_text"].str.len()
text_col_mean = df["text_len"].mean()
assert text_col_mean in ACCEPTABLE_TEXT_LENGTH_MEANS
这些测试使我们能够验证不管存储端或数据源 API 进行了哪些更改,我们都可以知道我们的模型可以访问与最初训练时相同类型的数据。一旦我们对摄入的数据的一致性感到自信,请看管道的下一步——数据处理。
测试数据处理
在测试数据达到管道开始时符合我们的期望后,我们应测试我们的清理和特征生成步骤是否如预期那样操作。我们可以从我们拥有的预处理函数开始编写测试,验证它确实执行我们意图的操作。此外,我们可以编写类似于数据摄入的测试,并专注于确保我们关于输入模型数据状态的假设是有效的。
这意味着测试我们处理管道后的数据点的存在性、类型和特征。以下是测试生成特征存在性、类型以及最小、最大和*均值的示例:
def test_feature_presence(df_with_features):
for feat in REQUIRED_FEATURES:
assert feat in df_with_features.columns
def test_feature_type(df_with_features):
assert df_with_features["is_question"].dtype == bool
assert df_with_features["action_verb_full"].dtype == bool
assert df_with_features["language_question"].dtype == bool
assert df_with_features["question_mark_full"].dtype == bool
assert df_with_features["norm_text_len"].dtype == float
assert df_with_features["vectors"].dtype == list
def test_normalized_text_length(df_with_features):
normalized_mean = df_with_features["norm_text_len"].mean()
normalized_max = df_with_features["norm_text_len"].max()
normalized_min = df_with_features["norm_text_len"].min()
assert normalized_mean in pd.Interval(left=-1, right=1)
assert normalized_max in pd.Interval(left=-1, right=1)
assert normalized_min in pd.Interval(left=-1, right=1)
这些测试可以让我们注意到对我们的管道产生影响的任何变化,这些变化会影响到我们模型的输入,而无需编写任何额外的测试。只有在添加新功能或更改模型输入时,我们才需要编写新的测试。
现在我们可以对我们摄入的数据以及我们应用的转换感到自信,因此是时候测试管道的下一部分——模型了。
测试模型输出
类似于前两类,我们将编写测试来验证模型输出的值是否具有正确的尺寸和范围。我们还将测试特定输入的预测。这有助于及早检测新模型预测质量的退化,并保证我们使用的任何模型始终在这些示例输入上产生预期的输出。当一个新模型显示出更好的综合性能时,很难注意到它在特定类型的输入上的性能是否恶化。编写这些测试有助于更轻松地检测这些问题。
在下面的例子中,我首先测试我们模型预测的形状,以及它们的值。第三个测试旨在通过保证模型将特定的语句不佳的输入问题分类为低质量,从而防止回归。
def test_model_prediction_dimensions(
df_with_features, trained_v1_vectorizer, trained_v1_model
):
df_with_features["vectors"] = get_vectorized_series(
df_with_features["full_text"].copy(), trained_v1_vectorizer
)
features, labels = get_feature_vector_and_label(
df_with_features, FEATURE_NAMES
)
probas = trained_v1_model.predict_proba(features)
# the model makes one prediction per input example
assert probas.shape[0] == features.shape[0]
# the model predicts probabilities for two classes
assert probas.shape[1] == 2
def test_model_proba_values(
df_with_features, trained_v1_vectorizer, trained_v1_model
):
df_with_features["vectors"] = get_vectorized_series(
df_with_features["full_text"].copy(), trained_v1_vectorizer
)
features, labels = get_feature_vector_and_label(
df_with_features, FEATURE_NAMES
)
probas = trained_v1_model.predict_proba(features)
# the model's probabilities are between 0 and 1
assert (probas >= 0).all() and (probas <= 1).all()
def test_model_predicts_no_on_bad_question():
input_text = "This isn't even a question. We should score it poorly"
is_question_good = get_model_predictions_for_input_texts([input_text])
# The model classifies the question as poor
assert not is_question_good[0]
我们首先通过视觉检查数据来验证其在整个管道中保持有用和可用。然后,我们编写测试来保证这些假设在我们的处理策略发展过程中仍然正确。现在是时候解决图 6-2 的第二部分,调试训练过程。
调试训练:使您的模型学习
一旦您测试了您的管道并验证了它对一个示例的工作方式,您就会知道一些事情。您的管道接收数据并成功转换它。然后,它将这些数据传递给一个正确格式的模型。最后,模型可以取几个数据点并从中学习,输出正确的结果。
现在是时候看看你的模型是否可以处理更多的数据点,并从你的训练集中学习。这一节的重点是能够在许多示例上训练你的模型,并且适应所有的训练数据。
为此,您现在可以将整个训练集传递给您的模型,并测量其性能。或者,如果您有大量数据,您可以逐渐增加您向模型提供的数据量,同时注意综合性能。
逐步增加您的训练数据集大小的一个优点是,您将能够测量额外数据对您模型性能的影响。从几百个示例开始,然后转向几千个,在传递整个数据集之前(如果您的数据集小于一千个示例,可以直接跳过)。
在每一步,将你的模型拟合到数据上,并评估其在同一数据上的表现。如果你的模型有能力从你使用的数据中学习,它在训练数据上的表现应该保持相对稳定。
为了将模型的表现置于背景之中,我建议通过自行标记一些示例来生成一个关于任务的可接受错误水*的估计,并将您的预测与真实标签进行比较。大多数任务也伴随着一个不可减少的错误,代表了在任务复杂性下的最佳表现。参见 图 6-7 来说明通常的训练表现与这些指标的比较。
一个模型在整个数据集上的表现应该比仅使用一个示例时更差,因为记忆整个训练集比单个示例更难,但应该仍然保持在之前定义的界限内。
如果你能够提供整个训练集并且你的模型的表现达到了你在查看产品目标时定义的要求,那么可以放心地进入下一节!如果没有,我在下一节中概述了一些模型在训练集上可能遇到困难的常见原因。

图 6-7. 随数据集大小变化的训练准确度
任务难度
如果一个模型的表现显著低于预期,可能是任务太难了。要评估任务的难度,考虑以下几点:
-
你拥有的数据的数量和多样性
-
你生成的特征有多预测性
-
你的模型的复杂性
让我们稍微详细看看每一个。
数据的质量、数量和多样性
你的问题越多样化和复杂,你的模型就需要更多的数据来学习它。为了让你的模型学习模式,你应该尽量拥有每种数据类型的许多示例。例如,如果你要将猫的图片分类为一百种可能的品种之一,你就需要比仅仅试图区分猫和狗要多得多的图片。事实上,你需要的数据量通常会随着类别数量的增加呈指数增长,因为更多的类别意味着更多的误分类机会。
此外,你拥有的数据越少,标签中的任何错误或缺失值对其影响就越大。这就是为什么值得花时间检查和验证数据集的特征和标签。
最后,大多数数据集包含异常值,即与其他数据点明显不同且对模型非常难以处理的数据点。从训练集中删除异常值通常可以通过简化任务来改善模型的性能,但这并不总是正确的方法:如果你认为你的模型可能在生产中遇到类似的数据点,你应该保留异常值并专注于改进你的数据和模型,使模型能够成功地适应它们。
数据集越复杂,找到使模型学习变得更容易的数据表示方式就越有帮助。让我们看看这意味着什么。
数据表示
仅使用您给出的表示形式就能轻松检测到您关心的模式吗?如果模型在训练数据上表现不佳,您应该添加使数据更具表现力的特征,从而帮助模型更好地学习。
这可能包括我们之前决定忽略但可能具有预测能力的新特征。在我们的 ML 编辑器示例中,模型的第一次迭代只考虑了问题正文中的文本。在进一步探索数据集后,我注意到问题标题通常能够很好地表明一个问题是否好。将该特征重新加入数据集中使得模型表现更好。
新特征通常可以通过迭代现有特征或以创造性的方式组合它们来生成。我们在“让数据指导特征和模型”中看到了一个例子,当我们研究如何结合星期几和月份的特征来生成与特定业务案例相关的特征时。
在某些情况下,问题出在您的模型上。接下来我们来看看这些情况。
模型容量
提高数据质量和改进特征通常能够带来最大的好处。当模型导致性能不佳时,往往意味着它不适合当前任务。正如我们在“从模式到模型”中看到的那样,特定的数据集和问题需要特定的模型。一个不适合特定任务的模型将难以在其上表现良好,即使它能够对少数样本过拟合。
如果一个模型在似乎具有许多预测特征的数据集上表现不佳,请首先询问自己是否使用了正确类型的模型。如果可能的话,使用给定任务的简化版本更容易检查模型。例如,如果随机森林模型根本不起作用,可以尝试在同一任务上使用决策树,并可视化其分割,以检查它们是否使用了您认为会具有预测能力的特征。
另一方面,您使用的模型可能过于简单。从最简单的模型开始是快速迭代的好方法,但某些任务可能完全超出了某些模型的能力范围。为了解决这些问题,您可能需要向模型添加复杂性。要验证模型确实适合任务,我建议查看我们在“站在巨人的肩膀上”中描述的先前工作。找到类似任务的示例,并检查用于解决它们的模型。使用这些模型之一应该是一个很好的起点。
如果模型看似适合当前任务,但表现**,可能是由于训练过程的问题。
优化问题
通过验证模型能否拟合少量示例,我们可以确信数据可以来回流动。然而,我们不知道我们的训练过程是否能够充分拟合整个数据集的模型。我们模型用于更新权重的方法可能不适合当前数据集。这类问题经常发生在更复杂的模型(如神经网络)中,其中超参数的选择对训练性能有重大影响。
处理使用梯度下降技术(如神经网络)拟合的模型时,使用诸如TensorBoard之类的可视化工具可以帮助发现训练中的问题。在优化过程中绘制损失曲线时,您应该看到其最初急剧下降,然后逐渐*缓。在图 6-8 中,您可以看到 TensorBoard 仪表板的示例,显示了训练过程中的损失函数(在本例中为交叉熵)。
这样的曲线显示损失下降非常缓慢,表明模型可能学习得太慢。在这种情况下,您可以增加学习率并绘制相同的曲线,以查看损失是否下降得更快。另一方面,如果损失曲线看起来非常不稳定,这可能是由于学习率过大造成的。

图 6-8。来自 TensorBoard 文档的 TensorBoard 仪表板截图
除了损失之外,可视化权重值和激活还可以帮助您确定网络是否学习得不好。在图 6-9 中,您可以看到权重分布随训练进展而发生变化的例子。如果您看到分布在几个周期内保持稳定,这可能表明您应该增加学习率。如果它们变化太大,则应该降低学习率。

图 6-9。随着训练进展而变化的权重直方图
成功将模型拟合到训练数据是机器学习项目中的重要里程碑,但并非最后一步。构建机器学习产品的最终目标是构建一个在以前从未见过的示例上表现良好的模型。为此,我们需要一个能够很好地泛化到未见示例的模型,因此接下来我将介绍泛化。
调试泛化能力:使您的模型更有用
泛化是图 6-2 的第三部分,也是最后一部分,重点是使 ML 模型在未曾见过的数据上表现良好。在“分割数据集”中,我们看到了创建单独的训练、验证和测试拆分的重要性,以评估模型对未见示例的泛化能力。在“评估您的模型:超越准确性”中,我们涵盖了分析模型性能并识别潜在额外特征以帮助改进的方法。在这里,我们将介绍在多次迭代后模型仍无法在验证集上表现的建议。
数据泄露
我们在“数据泄露”一节中更详细地讨论了数据泄露问题,但在泛化的背景下,我想在这里提一下。一个模型在验证集上的表现通常会比在训练集上差。这是可以预期的,因为模型在之前未接触过的数据上进行预测要比在其训练时使用的数据上更困难。
注意
在训练尚未完成时查看训练损失和验证损失时,验证性能可能会比训练性能更好。这是因为随着模型训练的进行,训练损失会随着时期累积,而验证损失是在时期完成后计算的,使用的是模型的最新版本。
如果验证性能优于训练性能,有时可能是由于数据泄露。如果训练数据中的示例包含验证数据中的其他信息,模型将能够利用这些信息,并在验证集上表现良好。如果您对验证性能感到惊讶,请检查模型使用的特征,看看它们是否显示出数据泄露。修复此类泄露问题将导致更低的验证性能,但模型将更好。
数据泄露可能会导致我们误认为模型具有泛化能力,而实际上并非如此。在其他情况下,通过观察模型在留置验证集上的表现,我们可以清楚地看到模型只在训练集上表现良好。在这种情况下,模型可能存在过拟合问题。
过拟合
在“偏差方差权衡”中,我们看到当模型难以拟合训练数据时,我们称其为欠拟合。我们也看到欠拟合的反义词是过拟合,这意味着我们的模型对训练数据拟合得过于好。
“过于好地拟合数据”是什么意思?这意味着,与学习与优秀或糟糕写作相关的可泛化趋势不同,例如,模型可能会捕捉到训练集中个别示例中存在但在其他数据中不存在的特定模式。这些模式有助于其在训练集上获得高分,但对于分类其他示例则无用。
图 6-10 展示了一个玩具数据集的过拟合和欠拟合的实际例子。过拟合模型完美拟合训练数据,但无法准确*似潜在趋势,因此在预测未见数据点时表现不佳。欠拟合模型则完全未能捕捉数据的趋势。标记为合理拟合的模型在训练数据上表现比过拟合模型差,但在未见数据上表现更好。

图 6-10. 过拟合与欠拟合
当模型在训练集上的表现远远优于测试集时,通常意味着它过拟合了。它已经学习了训练数据的具体细节,但不能在未见数据上表现良好。
由于过拟合是由模型过度学习训练数据引起的,我们可以通过降低模型从数据集中学习的能力来防止过拟合。有几种方法可以做到这一点,我们将在这里介绍。
正则化
正则化会对模型表示信息的能力施加惩罚。正则化旨在限制模型集中于许多无关模式的能力,并鼓励其选择更少但更具预测性的特征。
正则化模型的一种常见方法是对其权重的绝对值施加惩罚。例如,对于线性和逻辑回归等模型,L1 和 L2 正则化会在损失函数中增加一个额外项,惩罚大权重。在 L1 的情况下,该项是权重的绝对值之和。对于 L2,它是权重*方值的和。
不同的正则化方法有不同的效果。L1 正则化可以通过将无信息的特征设为零来帮助选择信息量大的特征(更多内容请参阅“套索回归”维基百科页面)。当一些特征相关时,L1 正则化也很有用,因为它鼓励模型仅利用其中的一个特征。
正则化方法也可以是特定于模型的。神经网络通常使用 dropout 作为正则化方法。在训练过程中,dropout 会随机忽略网络中一定比例的神经元。这可以防止单个神经元过于影响网络,从而避免网络记忆训练数据的各个方面。
对于基于树的模型,如随机森林,减少树的最大深度可以降低每棵树对数据的过拟合能力,从而有助于正则化森林。增加森林中使用的树的数量也能实现正则化。
另一种防止模型过拟合训练数据的方法是使数据本身更难过拟合。我们可以通过称为数据增强的过程来实现这一点。
数据增强
数据增强是通过轻微改变现有数据点来创建新的训练数据的过程。其目标是人为产生与现有数据点不同的数据点,以使模型接触到更多种类的输入。增强策略取决于数据类型。在图 6-11 中,您可以看到图像的几种潜在增强方法。

图 6-11. 图像数据增强的几个示例
数据增强使得训练集变得不那么同质化,从而更加复杂。这使得拟合训练数据更加困难,但在训练期间使模型接触到更广泛的输入。数据增强通常会导致训练集上的性能降低,但在未见数据(如验证集和生产中的示例)上的性能更高。如果我们能够利用增强使训练集更加接*实际场景中的示例,这种策略尤其有效。
我曾经帮助一位工程师使用卫星图像来检测飓风后的被淹没道路。这个项目很具挑战性,因为他只能访问非受灾城市的标记数据。为了帮助改善他的模型在飓风图像上的表现,这些图像通常更暗、质量更低,他们建立了增强流水线,使训练图像看起来更暗、更模糊。这降低了训练性能,因为道路现在更难检测到。另一方面,它提高了模型在验证集上的表现,因为增强过程使模型接触到更接*验证集中所遇到的图像。数据增强帮助使训练集更具代表性,从而使模型更加健壮。
如果在之前描述的方法使用后,模型在验证集上表现仍然不佳,你应该对数据集本身进行迭代。
数据集重新设计
在某些情况下,困难的训练/验证分割可能导致模型欠拟合并在验证集上表现困难。如果模型只接触到训练集中的简单示例,并且只接触到验证集中的挑战性示例,它将无法从困难的数据点中学习。同样,某些类别的示例在训练集中可能表现不足,从而阻止模型学习这些示例。如果模型训练以最小化聚合指标,则有可能主要适应大多数类别,而忽略少数类别。
虽然增强策略可以帮助,但重新设计训练集以使其更具代表性通常是最佳路径。在这样做时,我们应该小心控制数据泄露,并尽可能使难度在分割时*衡。如果新的数据分割将所有简单示例分配到验证集,模型在验证集上的性能将人为地提高,但这不会转化为生产结果。为了减轻数据分割可能存在的不均质问题,我们可以使用k 折交叉验证,在其中进行 k 个连续不同的分割,并在每个分割上评估模型的性能。
一旦我们*衡了训练和验证集,确保它们的复杂性相似,我们的模型性能应该会提高。如果性能仍然不令人满意,我们可能只是在处理一个非常困难的问题。
考虑手头的任务
模型可能难以泛化,因为任务过于复杂。例如,我们使用的输入可能无法预测目标。为了确保你正在处理的任务对当前机器学习的状态来说具有适当的难度,我建议再次参考“站在巨人的肩膀上”,在那里我描述了如何探索和评估当前技术水*。
此外,拥有数据集并不意味着任务可以解决。考虑一个不可能的任务,即准确预测随机输入的随机输出。你可以通过记忆来构建一个在训练集上表现良好的模型,但这种模型将无法准确预测其他随机输入的随机输出。
如果你的模型没有泛化能力,你的任务可能太难了。你的训练样本中可能没有足够的信息来学习有意义的特征,这些特征将为未来的数据点提供信息。如果是这种情况,那么你面临的问题并不适合使用机器学习,我建议你重新阅读第一章,找到更合适的框架。
结论
在本章中,我们介绍了三个连续的步骤,你应该遵循这些步骤来使模型工作。首先,通过检查数据和编写测试来调试你的流水线。然后,在训练测试上让模型表现良好,以验证它有学习能力。最后,验证它是否能泛化,并在未见数据上产生有用的输出。
这个过程将帮助你调试模型,更快地构建它们,并使它们更加稳健。一旦你建立、训练和调试好你的第一个模型,下一步就是评估其性能,然后要么进行迭代,要么部署它。
在第七章,我们将讨论如何使用训练过的分类器为用户提供可操作的建议。然后,我们将比较 ML 编辑器的候选模型,并决定应该使用哪个来支持这些建议。
第七章:使用分类器撰写建议
在 ML 中取得进展的最佳方式是通过反复遵循第 III 部分介绍中所示的迭代循环(参见图 7-1)。首先建立建模假设,迭代建模管道,并进行详细的错误分析以指导下一个假设。

图 7-1. ML 循环
前几章描述了此循环中的多个步骤。在第五章中,我们讨论了如何训练和评分模型。在第六章中,我们分享了如何更快地构建模型和解决 ML 相关错误的建议。本章通过首先展示如何使用训练有素的分类器为用户提供建议,然后选择用于 ML Editor 的模型,并最终结合两者来构建工作中的 ML Editor,从而结束了循环的一个迭代。
在“ML Editor 规划”中,我们概述了我们的 ML Editor 计划,其中包括训练一个能将问题分类为高分和低分类别的模型,并使用这个训练有素的模型来引导用户撰写更好问题的方法。让我们看看如何使用这样的模型为用户提供写作建议。
从模型中提取推荐
ML Editor 的目标是提供写作建议。将问题分类为好与坏是朝这个方向迈出的第一步,因为它可以向用户展示问题的当前质量。我们希望进一步,通过提供可操作的建议帮助用户改善问题的表达方式。
本节介绍了提供此类建议的方法。我们将从依赖聚合特征度量且不需要在推断时使用模型的简单方法开始。然后,我们将看到如何同时使用模型得分及其对扰动的敏感性来生成更个性化的建议。您可以在本书的 GitHub 站点上的生成推荐笔记本中找到本章展示的每种方法的示例,应用于 ML Editor。
没有模型我们能实现什么?
通过多次 ML 循环迭代来训练表现良好的模型。每次迭代都有助于通过研究先前的技术成果、迭代潜在数据集和检查模型结果来创建更好的特征集。为了向用户提供建议,您可以利用这些特征迭代工作。这种方法不一定需要在用户提交的每个问题上运行模型,而是专注于提供一般性建议。
您可以直接使用特征或将训练有素的模型纳入以帮助选择相关特征。
使用特征统计
一旦确定了预测性特征,可以直接向用户传达这些特征而无需使用模型。如果一个特征的均值在每个类别中有显著不同,您可以直接分享这些信息,以帮助用户朝着目标类别的方向调整其示例。
在 ML 编辑器早期识别的一个特征是问号的存在。检查数据显示,得分高的问题倾向于问题标点少。为了利用这些信息生成推荐,我们可以编写一个规则,警告用户如果其问题中问号的比例远高于高评分问题。
可以通过几行代码使用 pandas 可视化每个标签的*均特征值。
class_feature_values = feats_labels.groupby("label").mean()
class_feature_values = class_feature_values.round(3)
class_feature_values.transpose()
运行前述代码将生成如表 7-1 所示的结果。从这些结果中,我们可以看到,我们生成的许多特征在高分和低分问题中具有显著不同的值,这里标记为真和假。
表 7-1. 类别间特征值差异
| 标签 | 假 | 真 |
|---|---|---|
| num_questions | 0.432 | 0.409 |
| num_periods | 0.814 | 0.754 |
| num_commas | 0.673 | 0.728 |
| num_exclam | 0.019 | 0.015 |
| num_quotes | 0.216 | 0.199 |
| num_colon | 0.094 | 0.081 |
| num_stops | 10.537 | 10.610 |
| num_semicolon | 0.013 | 0.014 |
| num_words | 21.638 | 21.480 |
| num_chars | 822.104 | 967.032 |
使用特征统计是提供稳健推荐的简单方法。在很多方面,它与我们在“最简单的方法:成为算法”中首次构建的启发式方法类似。
在比较类别间特征值时,很难确定哪些特征对于问题分类有最大贡献。为了更好地估计这一点,我们可以使用特征重要性。
提取全局特征重要性
我们首先展示了在“评估特征重要性”中模型评估背景下生成特征重要性的示例。特征重要性还可以用于优先考虑基于特征的推荐。在向用户展示推荐时,应优先考虑对训练过的分类器最具预测性的特征。
接下来,我展示了一个问题分类模型的特征重要性分析结果,该模型使用了共 30 个特征。每个顶部特征的重要性远高于底部特征。引导用户首先基于这些顶部特征行动将帮助他们根据模型更快地改进问题。
Top 5 importances:
num_chars: 0.053
num_questions: 0.051
num_periods: 0.051
ADV: 0.049
ADJ: 0.049
Bottom 5 importances:
X: 0.011
num_semicolon: 0.0076
num_exclam: 0.0072
CONJ: 0
SCONJ: 0
结合特征统计和特征重要性可以使推荐更具可操作性和聚焦性。第一种方法为每个特征提供目标值,而后者则优先显示最重要特征的较小子集。这些方法还能够快速提供推荐,因为它们不需要在推断时运行模型,只需根据最重要特征的特征统计检查输入即可。
正如我们在“评估特征重要性” 中看到的,对于复杂模型来说,提取特征重要性可能更为困难。如果您正在使用不公开特征重要性的模型,可以利用大量示例上的黑盒解释器来尝试推断它们的值。
特征重要性和特征统计也带来另一个缺点,即它们并不总是提供准确的推荐。由于推荐基于整个数据集上聚合的统计数据,因此它们不一定适用于每个单独的示例。特征统计仅提供一般性推荐,例如“含有更多副词的问题往往得到更高评分”。然而,存在一些含有低于*均比例副词的问题得到高分的例子。这些推荐对这些问题并不适用。
在接下来的两个部分中,我们将讨论在个别示例级别提供更精细推荐的方法。
使用模型得分
第五章 描述了分类器如何为每个示例输出得分。然后根据该得分是否超过某个阈值,来为示例分配类别。如果模型的得分校准良好(详见“校准曲线” 了解更多关于校准的信息),那么它可以被用作估计输入示例属于给定类别的概率。
若要显示一个 scikit-learn 模型的得分而不是类别,请使用 predict_proba 函数,并选择要显示得分的类别。
# probabilities is an array containing one probability per class
probabilities = clf.predict_proba(features)
# Positive probas contains only the score of the positive class
positive_probs = clf[:,1]
如果它校准良好,向用户展示得分可以让他们在跟随修改建议改进其问题时跟踪得分提升。得分等快速反馈机制帮助用户更加信任模型提供的推荐。
在校准得分之上,训练好的模型还可以用来提供改进特定示例的推荐。
提取局部特征重要性
可以通过在训练模型的基础上使用黑盒解释器为单个示例生成推荐。在“评估特征重要性”中,我们看到黑盒解释器如何通过反复应用轻微扰动到输入特征并观察模型预测得分变化来估计特定示例的特征值重要性。这使得这样的解释器成为提供推荐的好工具。
我们使用 LIME 包来演示这一点,为一个示例生成解释。在以下代码示例中,我们首先实例化一个表格解释器,然后选择我们测试数据中要解释的一个示例。我们在这本书的 GitHub 仓库的生成推荐笔记本中展示这些解释,并以数组格式显示它们。
from lime.lime_tabular import LimeTabularExplainer
explainer = LimeTabularExplainer(
train_df[features].values,
feature_names=features,
class_names=["low", "high"],
discretize_continuous=True,
)
idx = 8
exp = explainer.explain_instance(
test_df[features].iloc[idx, :],
clf.predict_proba,
num_features=10,
labels=(1,),
)
print(exp_array)
exp.show_in_notebook(show_table=True, show_all=False)
exp_array = exp.as_list()
运行前面的代码会生成 图 7-2 中显示的图以及下面代码中显示的特征重要性数组。模型的预测概率显示在图的左侧。图的中间,特征值按其对预测贡献的排名。

图 7-2. 作为推荐的解释
这些值与下面更易读的控制台输出中的值相同。此输出中的每一行代表一个特征值及其对模型得分的影响。例如,特征 num_diff_words 的值低于 88.00 会将模型得分降低约 0.038。根据这个模型,增加输入问题的长度超过这个数字将提高其质量。
[('num_diff_words <= 88.00', -0.038175093133182826),
('num_questions > 0.57', 0.022220445063244717),
('num_periods <= 0.50', 0.018064270196074716),
('ADJ <= 0.01', -0.01753028452563776),
('408.00 < num_chars <= 655.00', -0.01573650444507041),
('num_commas <= 0.39', -0.015551364531963608),
('0.00 < PROPN <= 0.00', 0.011826217792851488),
('INTJ <= 0.00', 0.011302327527387477),
('CONJ <= 0.00', 0.0),
('SCONJ <= 0.00', 0.0)]
更多用法示例,请参阅这本书的 GitHub 仓库中的生成推荐笔记本。
黑盒解释器可以为单个模型生成准确的推荐,但它们确实有一个缺点。这些解释器通过扰动输入特征并在每个扰动的输入上运行模型来生成估计,因此使用它们生成推荐比讨论的其他方法要慢。例如,LIME 用于评估特征重要性的默认扰动次数是 500 次。这使得这种方法比那些只需要运行模型一次的方法慢两个数量级,甚至比根本不需要运行模型的方法还要慢。在我的笔记本电脑上,运行一个示例问题的 LIME 大约需要 2 秒多一点。这样的延迟可能会阻止我们在用户输入时为其提供推荐,并要求他们手动提交问题。
就像许多 ML 模型一样,我们在这里看到的推荐方法在准确性和延迟之间存在权衡。对产品的正确推荐取决于其需求。
我们所介绍的每一种建议方法都依赖于在模型迭代过程中生成的特性,并且其中一些利用了训练过的模型。在接下来的部分中,我们将比较 ML 编辑器的不同模型选项,并决定哪一个最适合提供建议。
比较模型
“衡量成功”覆盖了判断产品成功的重要指标。“评估表现”描述了评估模型的方法。这些方法也可以用于比较模型和特性的连续迭代,以识别表现最佳的那些。
在本节中,我们将选择一组关键指标,并使用它们来评估 ML 编辑器的三个连续迭代在模型性能和建议的有用性方面。
ML 编辑器的目标是使用上述技术提供建议。为了支持这些建议,模型应满足以下要求。它应该校准良好,以使其预测的概率代表问题质量的有意义估计。正如我们在“衡量成功”中所讨论的,它应该具有高精度,以确保其所做的推荐是准确的。它所使用的特性应该对用户可理解,因为它们将作为建议的基础。最后,它应该足够快,以允许我们使用黑盒解释器提供建议。
让我们描述一下 ML 编辑器的几种连续建模方法,并比较它们的表现。这些性能比较的代码可以在这本书的 GitHub 存储库中的比较模型笔记本中找到。
版本 1:成绩单
在第三章中,我们构建了一个完全基于启发式的编辑器的第一个版本。这个第一个版本使用了旨在编码可读性并以结构化格式向用户显示结果的硬编码规则。通过构建这个流水线,我们能够修改我们的方法,并将机器学习的努力集中在提供更清晰建议而不是一组测量上。
由于这个初始原型是为了发展我们所处理问题的直觉而建立的,我们不会在这里将其与其他模型进行比较。
版本 2:更强大,更不明确
在建立基于启发式的版本和探索 Stack Overflow 数据集之后,我们选择了一个初始建模方法。我们训练的简单模型可以在这本书的 GitHub 存储库中的简单模型笔记本中找到。
该模型使用了通过使用“矢量化”中描述的方法对文本进行向量化生成的特征的组合,以及在数据探索过程中出现的手动创建的特性。当首次探索数据集时,我注意到了一些模式:
-
更长的问题得到了更高的分数。
-
特别涉及英语使用的问题得分较低。
-
至少包含一个问号的问题得分较高。
我创建了一些特征来编码这些假设,通过计算文本长度、包含诸如punctuate和abbreviate等词的情况以及问号的频率。
除了这些特征外,我还使用了 TF-IDF 对输入问题进行了向量化。使用简单的向量化方案使我能够将模型的特征重要性与个别单词联系起来,这可以允许使用前述方法进行单词级别的推荐。
这种方法首次展示了可接受的总体性能,精度为0.62。但其校准程度仍有待提高,如您在第 7-3 图中所见。

图 7-3. V2 模型校准
检查了这个模型的特征重要性后,我意识到唯一有预测能力的手工创建特征是问题长度。其他生成的特征没有预测能力。再次探索数据集后发现,还有几个特征似乎具有预测能力:
-
适度使用标点符号似乎预测得分较高。
-
看起来更具情感色彩的问题得分较低。
-
描述性更强、使用更多形容词的问题似乎得分较高。
为了编码这些新的假设,我生成了一组新的特征。我为每个可能的标点元素创建了计数。然后我创建了计数,对于每个词性类别,如动词或形容词,测量了问题中属于该类别的词的数量。最后,我添加了一个特征来编码问题的情感倾向。关于这些特征的更多细节,请参阅此书的 GitHub 存储库中的第二个模型笔记本。
这个更新版本的模型在总体上表现略有改善,精度为0.63。但其校准并未超越前一模型。展示这个模型的特征重要性揭示,该模型仅依赖于手工制作的特征,显示这些特征具有一定的预测能力。
使模型依赖于这些易于理解的特征,比使用向量化的单词级特征更容易向用户解释推荐。例如,对于这个模型来说,最重要的单词级特征是are和what。我们可以猜测为什么这些词可能与问题质量相关,但向用户推荐他们在问题中减少或增加任意单词的发生频率并不会产生清晰的建议。
为了解决向量化表示的局限性,并认识到手工制作的特征具有预测能力,我尝试构建一个更简单的模型,不使用任何向量化特征。
版本 3:可理解的推荐
第三个模型仅包含前面描述的特征(标点符号和词性的计数、问题情感以及问题长度)。因此,该模型仅使用 30 个特征,而不是使用向量化表示时的 7000 多个特征。详细信息请参阅该书的 GitHub 存储库中的第三个模型笔记本。删除向量化特征并保留手动特征使得 ML 编辑器只能利用对用户可解释的特征。然而,这可能会导致模型的表现较差。
在总体性能方面,该模型的表现比以前的模型差,精度为0.597。然而,它的校准比以前的模型好得多。在图 7-4 中,您可以看到模型 3 在大多数概率上都有良好的校准,甚至是其他模型难以处理的大于 0.7 的概率。直方图显示这是由于该模型相比其他模型更经常预测这样的概率。
由于它生成的分数范围增加并且分数校准得到了改进,当涉及到显示分数以指导用户时,这个模型是最佳选择。当需要明确推荐时,由于它仅依赖于可解释特征,这个模型也是最佳选择。最后,因为它依赖的特征比其他模型少,所以运行速度也是最快的。

图 7-4. 校准比较
模型 3 是 ML 编辑器的最佳选择,因此是我们应该部署的模型的初始版本。在下一节中,我们将简要介绍如何使用此模型结合推荐技术向用户提供编辑建议。
生成编辑建议
ML 编辑器可以从我们描述的四种方法中受益以生成建议。实际上,所有这些方法都展示在生成建议笔记本中,该书的 GitHub 存储库中展示了这些方法。因为我们使用的模型速度快,我们将在这里演示最详尽的方法,使用黑匣子解释器。
让我们首先看一下完整的推荐函数,该函数接受一个问题并基于训练模型提供编辑建议。函数如下所示:
def get_recommendation_and_prediction_from_text(input_text, num_feats=10):
global clf, explainer
feats = get_features_from_input_text(input_text)
pos_score = clf.predict_proba([feats])[0][1]
exp = explainer.explain_instance(
feats, clf.predict_proba, num_features=num_feats, labels=(1,)
)
parsed_exps = parse_explanations(exp.as_list())
recs = get_recommendation_string_from_parsed_exps(parsed_exps)
return recs, pos_score
对一个示例输入调用此函数并美观地打印其结果会生成诸如以下的建议。然后,我们可以将这些建议显示给用户,让他们可以对其问题进行迭代。
>> recos, score = get_recommendation_and_prediction_from_text(example_question)
>> print("%s score" % score)
0.4 score
>> print(*recos, sep="\n")
Increase question length
Increase vocabulary diversity
Increase frequency of question marks
No need to increase frequency of periods
Decrease question length
Decrease frequency of determiners
Increase frequency of commas
No need to decrease frequency of adverbs
Increase frequency of coordinating conjunctions
Increase frequency of subordinating conjunctions
让我们逐步解析这个函数。从其签名开始,该函数接受一个表示问题的输入字符串作为参数,以及一个可选参数,确定要为其推荐的最重要特征数量。它返回推荐结果,以及表示当前问题质量的分数。
在问题主体中,第一行提到了两个全局定义的变量,训练好的模型和一个像我们在“提取本地特征重要性”中定义的 LIME 解释器的实例。接下来的两行生成输入文本的特征,并将这些特征传递给分类器进行预测。然后,通过使用 LIME 生成解释来定义exp。
最后两个函数调用将这些解释转换为易于理解的建议。让我们通过查看这些函数的定义来看看,从parse_explanations开始。
def parse_explanations(exp_list):
global FEATURE_DISPLAY_NAMES
parsed_exps = []
for feat_bound, impact in exp_list:
conditions = feat_bound.split(" ")
# We ignore doubly bounded conditions , e.g. 1 <= a < 3 because
# they are harder to formulate as a recommendation
if len(conditions) == 3:
feat_name, order, threshold = conditions
simple_order = simplify_order_sign(order)
recommended_mod = get_recommended_modification(simple_order, impact)
parsed_exps.append(
{
"feature": feat_name,
"feature_display_name": FEATURE_DISPLAY_NAMES[feat_name],
"order": simple_order,
"threshold": threshold,
"impact": impact,
"recommendation": recommended_mod,
}
)
return parsed_exps
这个函数很长,但它完成了一个相对简单的目标。它接受 LIME 返回的特征重要性数组,并生成一个更结构化的字典,可以用于建议。这里是这个转换的一个例子:
# exps is in the format of LIME explanations
>> exps = [('num_chars <= 408.00', -0.03908691525058592),
('DET > 0.03', -0.014685507408497802)]
>> parse_explanations(exps)
[{'feature': 'num_chars',
'feature_display_name': 'question length',
'order': '<',
'threshold': '408.00',
'impact': -0.03908691525058592,
'recommendation': 'Increase'},
{'feature': 'DET',
'feature_display_name': 'frequency of determiners',
'order': '>',
'threshold': '0.03',
'impact': -0.014685507408497802,
'recommendation': 'Decrease'}]
请注意,函数调用将 LIME 显示的阈值转换为建议,即是否增加或减少特征值。这是通过这里显示的get_recommended_modification函数完成的:
def get_recommended_modification(simple_order, impact):
bigger_than_threshold = simple_order == ">"
has_positive_impact = impact > 0
if bigger_than_threshold and has_positive_impact:
return "No need to decrease"
if not bigger_than_threshold and not has_positive_impact:
return "Increase"
if bigger_than_threshold and not has_positive_impact:
return "Decrease"
if not bigger_than_threshold and has_positive_impact:
return "No need to increase"
一旦解释被解析为建议,剩下的就是以适当的格式显示它们。这通过在get_recommendation_and_prediction_from_text中的最后一个函数调用完成,这里显示如下:
def get_recommendation_string_from_parsed_exps(exp_list):
recommendations = []
for feature_exp in exp_list:
recommendation = "%s %s" % (
feature_exp["recommendation"],
feature_exp["feature_display_name"],
)
recommendations.append(recommendation)
return recommendations
如果你想尝试这个编辑器并对其进行迭代,可以参考本书 GitHub 仓库中的生成建议笔记本。在笔记本的结尾,我包含了一个例子,使用模型的建议多次重述问题并提高其分数。我在这里重现这个例子,以演示如何利用这些建议来指导用户编辑问题。
// First attempt at a question
>> get_recommendation_and_prediction_from_text(
"""
I want to learn how models are made
"""
)
0.39 score
Increase question length
Increase vocabulary diversity
Increase frequency of question marks
No need to increase frequency of periods
No need to decrease frequency of stop words
// Following the first three recommendations
>> get_recommendation_and_prediction_from_text(
"""
I'd like to learn about building machine learning products.
Are there any good product focused resources?
Would you be able to recommend educational books?
"""
)
0.48 score
Increase question length
Increase vocabulary diversity
Increase frequency of adverbs
No need to decrease frequency of question marks
Increase frequency of commas
// Following the recommendations once more
>> get_recommendation_and_prediction_from_text(
"""
I'd like to learn more about ML, specifically how to build ML products.
When I attempt to build such products, I always face the same challenge:
how do you go beyond a model?
What are the best practices to use a model in a concrete application?
Are there any good product focused resources?
Would you be able to recommend educational books?
"""
)
0.53 score
现在,我们有一个可以接受问题并向用户提供可操作建议的流水线。这个流水线当然不完美,但我们现在拥有一个可工作的端到端 ML 驱动的编辑器。如果你想尝试改进它,我鼓励你与当前版本互动,并识别需要解决的故障模式。有趣的是,虽然模型总是可以迭代,但我认为为这个编辑器改进最有前途的方面是生成对用户更加清晰的新特征。
结论
在本章中,我们介绍了不同的方法来从训练好的分类模型中生成建议。考虑到这些方法,我们比较了 ML 编辑器的不同建模方法,并选择了一个能够优化我们产品目标——帮助用户提出更好问题的方法。然后,我们建立了一个 ML 编辑器的端到端流水线,并用它来提供建议。
我们最终确定的模型仍有很大改进空间,并且可以从更多的迭代周期中获益。如果你想要实践我们在 第三部分 中概述的概念,我鼓励你自行完成这些周期。总体而言,第三部分 中的每一章都代表了机器学习迭代循环的一个方面。要在机器学习项目中取得进展,请反复执行本节中概述的步骤,直到你估计一个模型已经准备好部署为止。
在 第四部分,我们将涵盖部署模型所伴随的风险、如何减轻这些风险,以及监测和应对模型性能变化的方法。
第四部分: 部署和监控
一旦我们构建了一个模型并验证过它,我们希望用户能够访问它。有许多不同的方法可以展示机器学习模型。最简单的情况涉及构建一个小型 API,但为了确保您的模型能够为所有用户有效运行,您需要更多。
查看图 IV-1 以了解接下来几章中我们将涵盖的系统的插图,通常与生产中的模型伴随使用。

图 IV-1. 典型的生产建模管道
生产环境的机器学习管道需要能够检测数据和模型失败,并优雅地处理它们。理想情况下,您还应该预测任何可能的失败,并有部署更新模型的策略。如果这些对您来说听起来具有挑战性,不用担心!这正是我们将在第四部分中覆盖的内容。
第八章
在部署之前,我们应始终进行最后一轮验证。目标是彻底检查模型可能存在的滥用和负面使用,并尽力预见并建立相应的保障措施。
第九章
我们将讨论不同的方法和*台来部署模型,以及如何选择其中一种。
第十章
在本章中,我们将学习如何构建一个能够支持模型的健壮生产环境。这包括检测和解决模型失败、优化模型性能以及系统化重新训练。
第十一章
在这最后一章中,我们将处理监控的关键步骤。特别是,我们将讨论为什么需要监控模型、最佳的监控方法,以及如何将监控设置与部署策略结合起来。
第八章:部署模型时的考虑因素
前几章涵盖了模型训练和泛化性能。 这些是部署模型所必需的步骤,但不足以保证 ML 驱动产品的成功。
部署模型需要更深入地研究可能影响用户的故障模式。 在构建从数据中学习的产品时,以下是您应该回答的几个问题:
-
您使用的数据是如何收集的?
-
您的模型通过从这个数据集学习得出什么假设?
-
此数据集是否足够代表性以生成有用的模型?
-
您的工作结果如何可能被滥用?
-
您的模型的预期使用和范围是什么?
数据伦理学旨在回答其中一些问题,使用的方法不断发展。 如果您想更深入地了解,O'Reilly 有一份由 Mike Loukides 等人编写的关于这一主题的综合报告,Ethics and Data Science。
在本章中,我们将讨论围绕数据收集和使用的一些关注点以及确保模型对每个人保持良好运作的挑战。 我们将通过一个实用的访谈结束本节,介绍将模型预测翻译为用户反馈的技巧。
让我们从数据开始,首先涵盖所有权问题,然后再讨论偏见。
数据关注
在本节中,我们将首先概述存储、使用和生成数据时应牢记的几个提示。 我们将从数据所有权和存储数据所带来的责任开始。 然后,我们将讨论数据集中常见偏见的来源及在构建模型时考虑这些偏见的方法。 最后,我们将讨论此类偏见的负面后果示例及其为何重要的原因。
数据所有权
数据所有权指的是与数据收集和使用相关的要求。 以下是几个关于数据所有权需要考虑的重要方面:
-
数据收集:您是否有法律授权收集和使用您希望在模型上训练的数据集?
-
数据使用和许可:您是否向用户清楚解释了为什么需要他们的数据以及您希望如何使用它,并且他们是否同意?
-
数据存储:您如何存储数据,谁可以访问它,何时将其删除?
从用户那里收集数据可以帮助个性化和定制产品体验。 这也意味着道德和法律责任。 尽管保护用户提供的数据始终存在道德义务,但新的法规越来越多地使其成为法律义务。 例如,在欧洲,GDPR 法规现在设置了严格的数据收集和处理指南。
对于存储大量数据的组织来说,数据泄露代表了重大的责任风险。这些泄露不仅侵蚀了用户对组织的信任,还经常导致法律诉讼。因此,限制收集的数据量可以减少法律风险。
对于我们的机器学习编辑器,我们将首先使用公开可用的数据集,这些数据集是在用户同意的情况下收集并在线存储的。如果我们想要记录额外的数据,例如记录我们的服务如何使用以便改进,我们必须明确定义数据收集政策并与用户分享。
除了数据收集和存储外,考虑使用收集的数据可能导致性能不佳也很重要。在某些情况下,数据集适合使用,但在其他情况下则不适合。让我们来探讨一下为什么。
数据偏见
数据集是特定数据收集决策的结果。这些决策导致数据集呈现了对世界的偏见观点。机器学习模型从数据集中学习,因此会重现这些偏见。
例如,假设一个模型是基于历史数据进行训练的,以预测领导能力,即预测一个人成为 CEO 的可能性,其中包括他们的性别信息。根据皮尤研究中心编制的《女性领导数据》信息表所述,历史上大多数财富 500 强公司的 CEO 都是男性。使用这些数据来训练模型将导致模型学习到男性是领导力宝贵的预测因素。在所选数据集中,男性和 CEO 之间存在相关性,这是由于社会原因,导致女性甚至不被考虑这样的角色。通过盲目地在这些数据上训练模型并用于预测,我们只会加强过去的偏见。
认为数据是真实的可能很诱人。实际上,大多数数据集是*似测量的集合,忽略了更大背景。我们应该假设任何数据集都存在偏见,并估计这种偏见将如何影响我们的模型。然后,我们可以采取措施改进数据集,使其更具代表性,并调整模型以限制其传播现有偏见的能力。
以下是数据集中常见错误和偏见的几个例子:
-
测量误差或数据损坏: 每个数据点由于生成方法的不确定性而带有不确定性。大多数模型忽略这种不确定性,因此可能传播系统测量误差。
-
代表性: 大多数数据集呈现了人口的不典型视图。许多早期的面部识别数据集主要包含白人男性的图像。这导致模型在这个人群中表现良好,但在其他人群中失败。
-
获取: 有些数据集比其他数据集更难获取。例如,英文文本比其他语言更容易在线获取。这种获取的便利性导致大多数最先进的语言模型仅基于英文数据进行训练。因此,英语使用者将比非英语使用者能够访问到更好的机器学习服务。这种差异通常是自我强化的,因为英语产品的用户量更大有助于使这些模型相比其他语言的模型更加优秀。
测试集用于评估模型的性能。因此,您应特别注意确保您的测试集尽可能准确和代表性。
测试集
在每个机器学习问题中都会出现表示问题。在《分割数据集》中,我们讨论了将数据分成不同集合以评估模型性能的价值。在执行此操作时,您应努力构建一个包含、代表和现实的测试集。这是因为测试集充当了生产环境性能的代理。
为了实现这一点,在设计测试集时,请考虑每个可能与您的模型互动的用户。为了提高每个用户都能有同样积极体验的机会,尽量在测试集中包含每种类型用户的代表性示例。
设计您的测试集以编码产品目标。在构建诊断模型时,您将希望确保它对所有性别的表现都足够好。为了评估是否达到了这一目标,您需要在测试集中包含所有性别的代表性数据。收集多样化的观点可以帮助实现这一努力。如果可能的话,在部署模型之前,让多样化的用户有机会检查、与之互动并分享反馈。
当涉及到偏见时,我想最后指出一点。模型通常是基于历史数据进行训练的,这些数据反映了过去的世界状态。因此,偏见往往最容易影响到那些已经处于弱势的群体。致力于消除偏见是一个可以帮助使系统对最需要帮助的人群更公*的努力。
制度性偏见
制度性偏见指的是导致某些人群受到不公*歧视的体制和结构性政策。由于这种歧视,这些人群在历史数据集中往往被过度或不足地代表。例如,如果社会因素导致某些人群在犯罪逮捕数据库中历史上被过度代表,那么从这些数据训练的机器学习模型将会编码这种偏见,并将其延续到现代预测中。
这可能会带来灾难性的后果,并导致某些人群的边缘化。具体示例,请参见 J. Angwin 等人的《机器偏见》ProPublica 报告,关于犯罪预测中的机器学习偏见。
移除或限制数据集中的偏差是具有挑战性的。当试图防止模型对某些特征(如种族或性别)存在偏见时,有些人尝试从模型用于预测的特征列表中删除相关属性。
实际上,仅仅删除一个特征并不能防止模型对其存在偏见,因为大多数数据集中还有许多与之强相关的其他特征。例如,在美国,邮政编码和收入与种族高度相关。如果只移除一个特征,模型可能仍然存在偏见,尽管这种偏见可能更难以检测。
相反,你应该明确你试图强制执行的公*约束。例如,你可以遵循 M. B. Zafar 等人在论文中提出的方法,"公*约束:公*分类的机制",其中模型的公*性是使用 p%规则来衡量的。p%规则被定义为“具有某一敏感属性值的受试者接收积极结果的百分比与不具备该值的受试者接收相同结果的百分比之比不得低于 p:100”。使用这样的规则允许我们量化偏差,并更好地加以解决,但需要跟踪我们希望模型不偏见的特征。
除了评估数据集中的风险、偏见和错误之外,ML 还需要评估模型本身。
建模关注点
我们如何最小化模型引入不良偏差的风险?
模型可以对用户产生负面影响的方式有多种。首先,我们将解决失控的反馈循环问题,然后探讨模型在小部分人群中悄悄失败的风险。接着,我们将讨论适当地为用户提供 ML 预测的重要性,并通过讨论恶意行为者滥用模型的风险来结束本节。
反馈循环
在大多数由 ML 驱动的系统中,用户跟随模型的推荐会使未来的模型更有可能做出相同的推荐。如果不加以控制,这种现象会导致模型进入自我强化的反馈循环。
例如,如果我们训练一个模型来向用户推荐视频,并且我们的第一个版本的模型比推荐猫的视频要稍微多一点,那么用户*均来看会观看更多的猫视频而不是狗视频。如果我们使用历史推荐和点击数据集训练第二个版本的模型,我们将把第一个模型的偏见纳入我们的数据集,第二个模型将更偏向于大量推荐猫的视频。
由于内容推荐模型通常每天更新多次,不久后我们的模型最新版本将只推荐猫视频。你可以在图 8-1 中看到一个例子。由于猫视频的初始受欢迎程度,该模型逐渐学会更多推荐猫视频,直到最终仅推荐猫视频。

图 8-1. 反馈循环示例
尽管填充互联网充斥着猫视频可能看起来不像是一场悲剧,但你可以想象这些机制如何快速强化负面偏见,向毫无戒备的用户推荐不当或危险的内容。事实上,试图最大化用户点击概率的模型将学会推荐点击诱饵内容,即非常诱人点击但对用户没有任何价值的内容。
反馈循环还倾向于引入偏见,以偏爱少数非常活跃的用户。如果视频*台使用每个视频的点击次数来训练其推荐算法,它有风险将其推荐过度拟合到占点击数量绝大多数的最活跃用户。*台的其他用户将会看到相同的视频,而不考虑他们的个人偏好。
为了限制反馈循环的负面影响,选择一个不易造成这种循环的指标。点击仅仅衡量用户是否打开了视频,而不是他们是否喜欢它。将点击作为优化目标会导致推荐更吸引眼球的内容,而不考虑其相关性。用观看时间替换目标指标,这与用户满意度更相关,有助于减轻这种反馈循环。
即使如此,优化任何形式参与度的推荐算法始终存在退化为反馈循环的风险,因为它们的唯一目标是最大化一个实际上无限制的指标。例如,即使算法优化观看时间以鼓励更吸引人的内容,最大化这一指标的世界状态是每个用户整天观看视频。使用这样的参与度指标可能有助于增加使用率,但这引发了一个问题:这是否总是一个值得优化的目标。
除了存在创造反馈循环的风险外,模型在生产环境中的表现也可能比离线验证指标预期的要差。
包容性模型表现
在“评估您的模型:超越准确性”中,我们涵盖了多种评估指标,试图评估数据集不同子集上的性能。这种类型的分析有助于确保模型对不同类型的用户表现同样出色。
这在训练现有模型的新版本并决定是否部署它们时尤为重要。如果您仅比较总体性能,您可能会忽视数据的某个段落性能显著下降。
忽视性能下降的问题导致了灾难性的产品失败。2015 年,一个自动化照片标记系统将非裔美国用户的照片分类为大猩猩(请参阅这篇2015 年 BBC 文章)。这是一个令人震惊的失败,是因为没有在代表性输入集上验证模型的结果。
当更新现有模型时可能会出现这种问题。比如说,你正在更新一个面部识别模型。之前的模型精度为 90%,而新模型的精度为 92%。在部署这个新模型之前,你应该在几个不同的用户子集上评估其性能。你可能会发现,虽然总体性能略有提高,但新模型在 40 岁以上女性的照片上表现非常糟糕,因此你应该避免部署它。相反,你应该修改训练数据,增加更多代表性的例子,并重新训练一个可以在每个类别中表现良好的模型。
忽略这些基准可能导致模型无法为其预期受众的大部分工作。大多数模型永远不会对每个可能的输入都有效,但验证它们对所有预期输入的有效性非常重要。
考虑到上下文
用户并不总是意识到某些信息来自 ML 模型的预测。在可能的情况下,您应该与用户分享预测背后的背景信息,以便他们能够决定如何利用它。为此,您可以开始描述模型的训练方式。
目前尚无行业标准的“模型免责声明”格式,但该领域的活跃研究显示出了一些有前景的格式,例如模型卡片(参见 M. Mitchell 等人的文章“模型报告的模型卡片”)。在提议的方法中,模型附带有关其训练方式、测试数据以及预期用途等元数据。
在我们的案例研究中,ML 编辑器根据特定的问题数据集提供反馈。如果我们将其作为产品部署,我们将包含一个关于模型预期表现良好输入类型的免责声明。这样的免责声明可以简单地表述为“本产品试图推荐更好的问题表达方式。它是在写作 Stack Exchange 的问题上进行训练的,因此可能反映了该社区的特定偏好。”
保持善意用户的信息通知是重要的。现在,让我们来看看可能由不友好用户带来的潜在挑战。
对手
一些机器学习项目需要考虑模型被对手击败的风险。欺诈者可能试图愚弄一个旨在检测可疑信用卡交易的模型。或者,对手可能希望探测一个训练好的模型,以获取有关底层训练数据的信息,这些信息他们本不应该访问,例如敏感用户信息。
打败一个模型
许多机器学习模型被部署来保护账户和交易免受欺诈者的侵害。反过来,欺诈者试图击败这些模型,以使它们相信他们是合法用户。
如果您试图防止对在线*台的欺诈登录,例如,您可能希望考虑包括用户原籍国家在内的一组特征(许多大规模攻击使用同一地区的多个服务器)。如果您在这些特征上训练一个模型,您将面临引入针对非欺诈用户的偏见的风险,这些用户居住在欺诈者所在的国家。此外,仅依赖这样一个特征将使恶意行为者轻而易举地通过伪造他们的位置来愚弄您的系统。
为了防范对手,定期更新模型至关重要。随着攻击者了解现有的防御模式并调整他们的行为以击败它们,更新您的模型,使其能够快速将这种新行为分类为欺诈行为。这需要监控系统以便检测活动模式的变化。我们将在第十一章中详细讨论这一点。在许多情况下,防御攻击者需要生成新的特征以更好地检测他们的行为。请随时参考“让数据指导特征和模型”以便对特征生成进行复习。
攻击模型的最常见方式是欺骗它们产生错误的预测,但还存在其他类型的攻击。一些攻击旨在利用训练好的模型来了解它所训练的数据。
利用一个模型
超出简单欺骗模型的范畴,攻击者可以利用它来获取私人信息。模型反映了它所训练的数据,因此可以利用其预测来推断原始数据集中的模式。为了说明这个想法,考虑一个在包含两个示例的数据集上训练的分类模型的例子。每个示例属于不同的类,并且两个示例仅在单个特征值上有所不同。如果您让攻击者访问一个在此数据集上训练的模型,并允许他们观察其对任意输入的预测,他们最终可以推断出这个特征是数据集中唯一的预测性特征。类似地,攻击者可以推断训练数据中特征的分布。这些分布通常涉及敏感或私人信息。
在欺诈登录检测的例子中,假设邮政编码是登录时的必填字段之一。攻击者可以尝试使用许多不同的账户登录,测试不同的邮政编码,以确定哪些值能够成功登录。这样做可以帮助他们估计训练集中邮政编码的分布,从而推断出该网站客户的地理分布。
限制这类攻击效率的最简单方法是限制特定用户可以发出的请求次数,从而限制其探索特征值的能力。这并非万能药,因为复杂的攻击者可能能够创建多个账户来规避此类限制。
本节描述的对手不是你唯一需要关注的恶意用户。如果你选择与更广泛的社区分享你的工作,你还应该问自己它是否可能被用于危险的应用程序。
滥用和双重用途
双重用途指的是为一种目的开发的技术,但可以用于其他目的。由于机器学习在类似类型的数据集上表现出色(见图 2-3),ML 模型经常引起双重用途的关注。
如果你开发了一个允许人们改变声音以模仿朋友的模型,是否可能被滥用来未经同意地冒充他人?如果你决定开发它,如何包含适当的指导和资源,确保用户理解模型的正确使用方式?
同样,任何能准确分类人脸的模型都可能存在监控的双重用途。虽然这样的模型最初可能是为了智能门铃而建立的,但它们随后可能被用于自动跟踪城市范围内的个体,通过摄像头网络实现这一功能。模型是根据特定数据集构建的,但在重新训练时可能带来风险,尤其是在使用类似数据集时。
目前尚无明确的最佳实践以考虑双重用途。如果你认为你的工作可能会被用于不道德的用途,我建议你考虑增加难以复制的条件,或者与社区进行深入讨论。最*,OpenAI 决定不发布其最强大的语言模型,因为担心其可能会使在线传播虚假信息变得更容易(见 OpenAI 的公告文章,“更好的语言模型及其影响”)。虽然这是一个相对新颖的决定,但我认为类似的担忧未来可能会更频繁地被提出。
总结本章节,在接下来的部分,我将分享与克里斯·哈兰德的讨论,他目前是 Textio 的工程总监,拥有丰富的模型部署经验,并在呈现结果时提供足够的背景信息,使其有用。
克里斯·哈兰德:航运实验
克里斯拥有物理学博士学位,并在多个 ML 任务中工作,包括计算机视觉,从收据中提取结构化信息以用于报销软件。他在微软的搜索团队工作时意识到 ML 工程的价值。后来,克里斯加入了 Textio,一家专门构建增强写作产品的公司,帮助用户撰写更具吸引力的职位描述。我与克里斯坐下来讨论了他在推出基于 ML 的产品和如何验证结果的经验。
Q: Textio 使用 ML 直接指导用户。这与其他 ML 任务有何不同?
A: 当你只专注于预测,比如何时买黄金或者在 Twitter 上关注谁,你可以容忍一定程度的变化。但当你进行写作指导时,情况就不同了,因为你的建议背后潜藏着大量的含义。
如果你告诉我写 200 个字,你的模型应该是一致的,并允许用户遵循其建议。一旦用户写了 150 个字,模型就不能改变主意并建议减少字数了。
指导还需要清晰度:像“减少 50%的停用词”这样的指令是令人困惑的,但像“缩短这三个句子的长度”可能以更具体的方式帮助用户。一个挑战是在使用更易于理解的特征时保持性能。
本质上,ML 写作助手通过我们的模型,引导用户从一个初始点到更好的点在我们的特征空间中移动。有时,这可能涉及经过更糟糕的点,这可能是一种令用户沮丧的体验。产品在建设时需要考虑到这些限制。
Q: 如何进行这种引导?
A: 对于指导来说,精确度比召回率更加有趣。如果你考虑给某人建议,召回率就是在所有潜在相关的领域以及一些无关的领域(其中有很多)中给出建议的能力,而精确度则是在几个有前景的领域中给出建议,忽略潜在的其他领域。
在给出建议时,错误的成本非常高,所以精确度是最有用的。用户还会从你的模型先前给出的建议中学习,并在未来的输入中自动应用这些建议,这使得这些建议的精确度变得更加重要。
另外,因为我们展示了不同的因素,我们要衡量用户是否真正利用了它们。如果没有,我们就要了解为什么。一个实际的例子是我们的“主动到被动比率”特征,它被低估了。我们意识到这是因为建议不够具体可行,所以我们通过突出显示推荐更改的具体单词来改进它。
Q: 你如何找到新的指导用户或新特性的方法?
A: 自上而下和自下而上的方法都是有价值的。
自顶向下的假设调查是基于领域知识驱动的,基本上是通过先前经验的特征匹配来进行的。例如,这可能来自产品或销售团队。自顶向下的假设可能是“我们认为招聘邮件中神秘方面有助于提高参与度。” 自顶向下的挑战通常在于找到一种实用的方法来提取该特征。只有这样,我们才能验证该特征是否具有预测性。
自下而上的目标是审视分类管道以理解其发现预测性的方法。如果我们有文本的一般表示形式,如词向量、标记和词性标注,然后将其提供给多模型集合以分类好或坏的文本,哪些特征最能预测我们的分类?领域专家通常是最适合从模型预测中识别这些模式的人。难点在于找到一种使这些特征人类可理解的方法。
Q: 如何确定一个模型是否足够好?
A: 您不应低估相关语言的小文本数据集能为您带来多大进展。事实证明,在许多用例中,仅使用域内的一千份文档就足够了。有能力对这小部分数据集进行标记是值得的。然后,您可以开始在样本外数据上测试您的模型。
您应该简化运行实验的流程。您对于改变产品的大多数想法最终都会产生零效果,这应该使您对于新功能稍微少担心一些。
最后,建立一个糟糕的模型是可以接受的,并且这是您应该开始的。修复糟糕的模型将使您的产品更加稳健,并帮助其更快地发展。
Q: 一旦模型投入使用,如何确定其表现如何?
A: 在生产环境中,明确向用户展示您的模型预测,并允许他们覆盖它。记录特征值、预测值和覆盖值,以便稍后监控和分析它们。如果您的模型生成分数,找到比较此分数与用户推荐使用情况的方法可能是一个额外的信号。例如,如果您在预测邮件是否会被打开,获取用户的真实数据将非常有价值,因此您可以改进您的模型。
最终的成功指标是客户的成功,这是最为延迟的,并且受到许多其他因素的影响。
结论
我们首先讨论了使用和存储数据时的关注点。然后,我们深入探讨了数据集中偏见的原因以及识别和减少它们的技巧。接下来,我们看了模型在实际应用中面临的挑战,以及如何减少将其暴露给用户所带来的风险。最后,我们研究了如何设计系统,使其能够对错误具有弹性。
这些是复杂的问题,机器学习领域仍然有很多工作要做,以解决所有可能的滥用形式。第一步是让所有从业者意识到这些问题,并在他们自己的项目中注意这些问题。
现在我们准备部署模型。首先,我们将在第九章中探讨不同部署选项之间的权衡。然后,我们将介绍一些减少部署模型风险的方法,在第十章中。
第九章:选择您的部署选项
前面的章节介绍了从产品创意到 ML 实现的过程,以及迭代此应用程序直至准备部署的方法。
本章涵盖了不同的部署选项以及它们之间的权衡。不同的部署方法适合不同的需求集合。在选择时,您需要考虑多个因素,如延迟、硬件和网络要求,以及隐私、成本和复杂性问题。
部署模型的目标是允许用户与之交互。我们将介绍实现此目标的常见方法,以及在部署模型时如何在不同方法之间进行选择的技巧。
我们将从部署模型和启动 Web 服务器提供预测的最简单方法开始。
服务器端部署
服务器端部署包括设置一个能够接受客户端请求、通过推断管道运行请求并返回结果的网络服务器。这种解决方案适用于 Web 开发范式,因为它将模型视为应用程序中的另一个端点。用户向此端点发送请求,并期望得到结果。
服务器端模型有两种常见的工作负载,流式处理和批处理。流式处理工作流在接收请求时立即处理它们。批处理工作流则较少频繁运行,一次处理大量请求。让我们首先看看流式处理工作流。
流式应用程序或 API
流式处理方法将模型视为用户可以发送请求的端点。在这种情况下,用户可以是应用程序的最终用户,也可以是依赖于模型预测的内部服务。例如,预测网站流量的模型可以被内部服务使用,负责根据预测的用户数量调整服务器数量。
在流式应用程序中,请求的代码路径经过我们在“从简单流水线开始”中介绍的一系列步骤。作为提醒,这些步骤包括:
-
验证请求。验证传递的参数值,并可选择检查用户是否具有运行此模型的正确权限。
-
收集额外数据。查询其他数据源,获取可能需要的任何额外数据,例如与用户相关的信息。
-
预处理数据。
-
运行模型。
-
后处理结果。验证结果是否在可接受范围内。增加上下文,使用户能够理解,例如解释模型的置信度。
-
返回结果。
您可以在图 9-1 中看到这些步骤的示例。

图 9-1。流式 API 工作流程
端点方法实现快速,但需要基础设施按当前用户数量线性扩展,因为每个用户会导致单独的推断调用。如果流量增加超出服务器处理请求的能力,则会开始延迟或甚至失败。因此,根据流量模式调整这样的流水线需要能够轻松启动和关闭新服务器,这将需要一定程度的自动化。
然而,对于像 ML 编辑器这样的简单演示,通常一次只需几个用户,流式方法通常是一个不错的选择。为了部署 ML 编辑器,我们使用轻量级 Python Web 应用程序,例如Flask,它使得通过几行代码轻松设置一个 API 来为模型提供服务。
你可以在书的GitHub 存储库中找到原型的部署代码,但我将在这里进行高层次的概述。Flask 应用程序由两部分组成,一个 API 接收请求并使用 Flask 将其发送到模型进行处理,另一个是用 HTML 构建的简单网站,用户可以在其中输入其文本并显示结果。定义这样的 API 不需要太多代码。在这里,你可以看到两个处理大部分工作的函数,用于为 ML 编辑器的 v3 版本提供服务:
from flask import Flask, render_template, request
@app.route("/v3", methods=["POST", "GET"])
def v3():
return handle_text_request(request, "v3.html")
def handle_text_request(request, template_name):
if request.method == "POST":
question = request.form.get("question")
suggestions = get_recommendations_from_input(question)
payload = {"input": question, "suggestions": suggestions}
return render_template("results.html", ml_result=payload)
else:
return render_template(template_name)
v3 函数定义了一个路由,允许它确定当用户访问/v3页面时要显示的 HTML。它使用handle_text_request函数来决定显示什么内容。当用户首次访问页面时,请求类型为GET,因此该函数显示 HTML 模板。此 HTML 页面的截图显示在图 9-2 中。如果用户点击“获取推荐”按钮,则请求类型为POST,因此handle_text_request获取问题数据,将其传递给模型,并返回模型输出。

图 9-2. 使用模型的简单网页
当存在严格的延迟约束时,需要流式应用程序。如果模型需要的信息只在预测时可用,并且需要立即进行模型预测,则需要一种流式方法。例如,在打车应用中预测特定行程的价格时,需要用户位置信息和司机当前的可用性信息来进行预测,这些信息只在请求时可用。这样的模型还需要立即输出预测结果,因为必须显示给用户,以便他们决定是否使用该服务。
在其他一些情况下,计算预测所需的信息可以提前获得。在这些情况下,一次处理大量请求可能比随时处理更容易。这称为批处理预测,我们将在下面进行介绍。
批量预测
批处理方法将推断管道视为可以一次运行多个示例的作业。批处理作业在许多示例上运行模型并存储预测,以便在需要时使用。当您在模型的预测需要之前就能获得需要的特征时,批处理作业是适当的。
例如,假设您想要建立一个模型,为您团队的每个销售人员提供最有价值的潜在客户列表。这是一个常见的机器学习问题,称为潜在客户评分。为了训练这样的模型,您可以使用历史电子邮件对话和市场趋势等特征。在销售人员决定联系哪个潜在客户时,这些特征是可用的,这也是预测所需的时间点。这意味着您可以在夜间批处理作业中计算潜在客户列表,并在早晨准备好显示结果,这时它们将被需要。
类似地,一个利用机器学习来在早晨优先和排名最重要的消息通知以阅读的应用程序并不需要很强的延迟要求。这种应用程序的适当工作流程是在早晨批处理处理所有未读邮件,并保存优先列表以便用户需要时使用。
与流式处理相比,批处理方法需要像流式处理一样多次推断运行,但可能更加资源高效。因为预测是在预定的时间进行的,并且在批处理开始时已知预测的数量,因此更容易分配和并行化资源。此外,批处理方法在推断时间上可能更快,因为结果已经预先计算并且只需检索。这提供了类似缓存的收益。
图 9-3 展示了这个工作流程的两个方面。在批处理时,我们计算所有数据点的预测并存储我们产生的结果。在推断时,我们检索预先计算的结果。

图 9-3. 批处理工作流程示例
还可以使用混合方法。在尽可能多的情况下预先计算,在推断时要么检索预先计算的结果,要么在现场计算如果结果不可用或已过时。这种方法可以尽快产生结果,因为可以提前计算的任何内容都会被计算。但同时也需要维护批处理管道和流式处理管道,这显著增加了系统的复杂性。
我们已经讨论了两种在服务器上部署应用程序的常见方式,即流式处理和批处理。这两种方法都需要托管服务器来为客户运行推断,如果产品变得流行起来,这很快会变得昂贵。此外,这些服务器代表了您应用程序的中心故障点。如果预测需求突然增加,您的服务器可能无法容纳所有请求。
或者,您可以直接在客户端设备上处理客户的请求。在用户设备上运行模型可以降低推断成本,并允许您保持服务水*的恒定,无论应用程序的流行程度如何,因为客户端提供了必要的计算资源。这称为客户端部署。
客户端部署
在客户端部署模型的目标是在客户端上运行所有计算,消除服务器运行模型的需求。计算机、*板电脑、现代智能手机以及一些连接设备如智能音箱或门铃具有足够的计算能力来自行运行模型。
本节仅涵盖在设备上部署的经过训练的模型,而不是在设备上训练模型。模型仍然以相同的方式进行训练,然后发送到设备进行推断。模型可以通过包含在应用程序中的方式或从 Web 浏览器加载来到达设备。请参见 图 9-4 以了解在应用程序中打包模型的示例工作流程。

图 9-4. 模型在设备上运行推断(我们仍然可以在服务器上进行训练)
便携设备的计算能力比强大的服务器更有限,因此这种方法限制了可使用模型的复杂性,但在设备上运行模型可以提供多种优势。
首先,这减少了需要为每个用户运行推断的基础设施的需求。此外,将模型运行在设备上可以减少设备和服务器之间需要传输的数据量。这可以降低网络延迟,甚至允许应用程序在无网络访问的情况下运行。
最后,如果推断所需的数据包含敏感信息,则在设备上运行模型可以消除将此数据传输到远程服务器的需求。不将敏感数据存储在服务器上可降低未经授权的第三方访问这些数据的风险(参见 “数据问题” 为什么这可能是一个严重的风险)。
图 9-5 比较了为用户获取预测的服务器端模型和客户端模型的工作流程。在顶部,您可以看到服务器端工作流程的最长延迟通常是将数据传输到服务器所需的时间。在底部,您可以看到,虽然客户端模型几乎没有延迟,但由于硬件限制,它们处理示例的速度通常比服务器慢。

图 9-5. 在服务器上运行,或本地运行
就像服务器端部署一样,有多种方法可以在客户端部署应用程序。在接下来的章节中,我们将涵盖两种方法,即本地部署模型和通过浏览器运行模型。这些方法适用于拥有应用商店和 Web 浏览器访问权限的智能手机和*板电脑,但不适用于其他连接设备,比如微控制器,在此我们将不进行介绍。
在设备上
笔记本电脑和手机中的处理器通常不会针对运行机器学习模型进行优化,因此会较慢地执行推断流水线。为了使客户端模型能够快速运行且不消耗过多电力,它应尽可能小。
减小模型尺寸可以通过使用更简单的模型、减少模型参数数量或计算精度来实现。例如,在神经网络中,通常会对权重进行修剪(删除接*零值的权重)和量化(降低权重精度)。您可能还希望减少模型使用的特征数量,以进一步提高效率。*年来,诸如Tensorflow Lite之类的库开始提供有用的工具,用于减小模型尺寸并帮助使其更容易在移动设备上部署。
由于这些要求,大多数模型在移植到设备上时性能会稍微下降。那些不能容忍模型性能下降的产品,比如依赖于无法在智能手机等设备上运行的前沿模型的产品,应部署在服务器上。通常来说,如果在设备上运行推断所需的时间大于将数据传输到服务器进行处理所需的时间,您应考虑在云端运行您的模型。
对于其他应用,比如在智能手机上提供帮助快速输入的预测键盘,具有无需访问互联网的本地模型的价值超过了精度损失。类似地,一款通过拍照帮助远足者识别植物的智能手机应用应支持离线工作,以便在远足中使用。这样的应用程序需要在设备上部署模型,即使这意味着牺牲预测精度。
翻译应用程序是另一个依赖于本地运行的机器学习驱动产品的例子。这样的应用程序可能会在用户无法访问网络的国外地区使用。因此,拥有可以本地运行的翻译模型成为一种需求,即使它不像只能在服务器上运行的更复杂模型那样精确。
除了网络问题外,将模型运行在云端会增加隐私风险。将用户数据发送到云端并临时存储会增加攻击者访问数据的可能性。考虑到一个看似无害的应用,比如在照片上叠加滤镜。许多用户可能不希望他们的照片被传输到服务器进行处理并永久存储。在越来越注重隐私的世界中,向用户保证他们的照片永远不会离开设备是一个重要的差异化点。正如我们在“数据问题”中看到的,避免将敏感数据置于风险之中的最佳方法是确保它永远不离开设备或存储在您的服务器上。
另一方面,量化、修剪和简化模型是一个耗时的过程。只有在延迟、基础设施和隐私好处值得投入工程工作的情况下,设备端部署才是值得的。对于 ML 编辑器,我们将限制在基于 web 的流媒体 API。
最后,专门优化模型以在特定类型设备上运行可能是耗时的,因为优化过程可能因设备而异。有更多选项可以在本地运行模型,包括利用设备之间的共同点来减少所需的工程工作。在这一领域中一个令人兴奋的地方是浏览器中的机器学习。
浏览器端
大多数智能设备都可以访问浏览器。这些浏览器通常已经经过优化,支持快速的图形计算。这导致了对使用浏览器让客户端执行机器学习任务的库的兴趣日益增加。
这些框架中最流行的是Tensorflow.js,它使得在浏览器中使用 JavaScript 进行大多数可微分模型的训练和推断成为可能,甚至可以处理用不同语言如 Python 训练的模型。
这使用户能够通过浏览器与模型进行交互,而无需安装任何额外的应用程序。此外,由于模型在浏览器中使用 JavaScript 运行,计算是在用户的设备上完成的。您的基础设施只需提供包含模型权重的网页。最后,Tensorflow.js 支持 WebGL,这使它能够利用客户端设备上的 GPU,从而加快计算速度。
使用 JavaScript 框架可以更轻松地在客户端部署模型,而无需像以前的方法那样进行太多的设备特定工作。但是,这种方法的缺点是增加了带宽成本,因为每次客户端打开页面时都需要下载模型,而不是在安装应用程序时仅下载一次。
只要您使用的模型几兆字节或更小,并且可以快速下载,使用 JavaScript 在客户端上运行它们可以是降低服务器成本的有效方法。如果服务器成本对于 ML 编辑器成为问题,我会建议首先探索使用 Tensorflow.js 等框架部署模型的方法。
到目前为止,我们考虑的客户端纯粹是为了部署已经训练过的模型,但我们也可以决定在客户端训练模型。在接下来的部分,我们将探讨这种做法何时会有用。
联邦学习:混合方法
我们主要涵盖了已经训练的模型的不同部署方式(理想情况下是按照前几章的指南),现在我们正在选择如何部署。我们已经看过了让所有用户面前都有一个独特模型的不同解决方案,但如果我们希望每个用户都有不同的模型怎么办?
图 9-6 展示了顶部系统中所有用户都有一个共同训练模型的区别,以及底部每个用户都有略微不同版本模型的情况。

图 9-6. 一个大模型或许多个个体模型
对于许多应用程序,如内容推荐、提供写作建议或医疗保健,模型最重要的信息来源是其对用户的数据。我们可以通过为模型生成用户特定的特征,或者决定每个用户都应该有自己的模型来利用这一事实。这些模型可以共享相同的架构,但每个用户的模型将具有反映其个体数据的不同参数值。
这个想法是联邦学习的核心,这是一个深度学习领域,*年来引起越来越多的关注,例如OpenMined项目。在联邦学习中,每个客户端都有自己的模型。每个模型从其用户数据中学习,并将聚合(可能匿名化)的更新发送到服务器。服务器利用所有更新来改进其模型,并将这个新模型提炼回个体客户端。
每个用户接收到根据其需求个性化的模型,同时仍然从其他用户的汇总信息中受益。联邦学习提升了用户的隐私,因为他们的数据从不传输到服务器,服务器仅接收聚合的模型更新。这与通过收集每个用户数据并将其全部存储在服务器上来训练模型的传统方式形成鲜明对比。
联邦学习是机器学习的一个令人兴奋的方向,但它增加了额外的复杂性。确保每个个体模型表现良好,并且传输回服务器的数据得到适当的匿名化,比训练单一模型更复杂。
联邦学习已经被那些有资源部署它的团队在实际应用中使用。例如,正如 A. Hard 等人在本文中所述,“联邦学习用于移动键盘预测”,Google 的 GBoard 使用联邦学习为智能手机用户提供下一个单词预测。由于用户之间写作风格的多样性,构建一个适合所有用户且性能良好的唯一模型是具有挑战性的。在用户级别训练模型使 GBoard 能够了解用户特定的模式,并提供更好的预测。
我们已经讨论了在服务器、设备甚至两者上部署模型的多种方法。您应根据应用程序的要求考虑每种方法及其权衡。与本书其他章节一样,我建议您从简单的方法开始,并仅在验证必要性后才转向更复杂的方法。
结论
有多种方法可以为基于机器学习的应用程序提供服务。您可以设置流式 API,使模型能够处理到达的示例。您可以使用批处理工作流,定期一次性处理多个数据点。或者,您可以选择在客户端部署模型,方法是将它们打包到应用程序中或通过 Web 浏览器提供服务。这样做可以降低推断成本和基础设施需求,但会使部署过程更加复杂。
正确的方法取决于应用程序的需求,例如延迟要求、硬件、网络和隐私问题以及推断成本。对于像 ML Editor 这样的简单原型,建议从端点或简单的批处理工作流开始,并从中迭代。
部署模型不仅仅是让用户接触到它。在第十章中,我们将介绍围绕模型构建保护措施以减少错误的方法、工程工具以提高部署过程的效率,以及验证模型表现是否符合预期的方法。
第十章:为模型构建保障
在设计数据库或分布式系统时,软件工程师关注的是容错能力,即系统在某些组件失败时仍能继续工作的能力。在软件中,问题不在于系统的某个部分是否会失败,而在于何时会失败。同样的原则也适用于机器学习。无论模型有多好,在某些示例上它都会失败,因此您应该设计一个能够优雅处理这类失败的系统。
在本章中,我们将介绍不同的方法来帮助预防或减轻故障。首先,我们将看看如何验证我们接收和生成的数据的质量,并利用这些验证来决定如何向用户显示结果。然后,我们将探讨如何使建模管道更加健壮,以便有效服务于许多用户。接下来,我们将看看利用用户反馈来评估模型表现的选项。最后,我们将以与克里斯·穆迪关于部署最佳实践的访谈结束本章。
围绕故障设计
让我们来看看机器学习管道可能失败的最可能方式。敏锐的读者会注意到,这些故障案例与我们在“调试布线:可视化和测试”中看到的调试技巧有些相似。事实上,在生产中向用户公开模型会带来一系列与调试模型类似的挑战。
错误和 bug 可以在任何地方出现,但有三个特别重要的区域需要验证:管道的输入、模型的置信度以及它生成的输出。让我们依次来解决每个问题。
输入和输出检查
任何给定的模型都是在展示特定特征的特定数据集上训练的。训练数据具有特定数量的特征,每个特征都是特定类型的。此外,每个特征都遵循给定的分布,模型学习了这些分布以便准确执行。
正如我们在“新鲜度和分布偏移”中看到的,如果生产数据与模型训练时的数据不同,模型可能会难以表现出色。为了帮助解决这个问题,您应该检查管道的输入。
检查输入
一些模型可能在面对数据分布的微小差异时仍然表现良好。然而,如果一个模型接收到与其训练数据非常不同的数据,或者某些特征缺失或类型不符合预期,它将很难表现出色。
正如我们之前所见,即使给定不正确的输入(只要这些输入的形状和类型正确),ML 模型也能够运行。模型将产生输出,但这些输出可能是完全不正确的。考虑以下示例,在图 10-1 中有所说明。一个流水线通过首先对句子进行向量化并在向量化表示上应用分类模型,将句子分类为两个主题。如果流水线收到一串随机字符,它仍然会将其转换为向量,然后模型将进行预测。这个预测是荒谬的,但仅凭查看模型的结果是无法知道的。

图 10-1. 模型将仍然为随机输入输出预测
为了防止模型在错误输出上运行,我们需要在将其传递给模型之前检测这些输入是否错误。
这些检查覆盖了类似于“测试您的 ML 代码”中的测试的领域。按重要性顺序,它们将会:
-
验证所有必要特征是否存在
-
检查每种特征类型
-
验证特征值
在隔离验证特征值时可能会很困难,因为特征分布可能很复杂。执行此类验证的简单方法是定义特征可能取值的合理范围,并验证其是否落在该范围内。
如果任何输入检查失败,则不应运行模型。您应该根据用例决定应采取的措施。如果缺失的数据代表核心信息的一部分,您应返回一个指明错误来源的错误。如果您估计仍然可以提供结果,则可以将模型调用替换为启发式算法。这是在任何 ML 项目中首先建立启发式算法的另一个原因;它为您提供了一个备用选项!
在图 10-2 中,您可以看到这种逻辑的示例,其中所采取的路径取决于输入检查的结果。

图 10-2. 输入检查的分支逻辑示例
以下是来自 ML 编辑器的一些控制流逻辑示例,用于检查缺失特征和特征类型。根据输入的质量,它要么引发错误,要么运行启发式算法。我在这里复制了示例,但您也可以在本书的 GitHub 存储库中找到 ML 编辑器代码的其余部分。
def validate_and_handle_request(question_data):
missing = find_absent_features(question_data)
if len(missing) > 0:
raise ValueError("Missing feature(s) %s" % missing)
wrong_types = check_feature_types(question_data)
if len(wrong_types) > 0:
# If data is wrong but we have the length of the question, run heuristic
if "text_len" in question_data.keys():
if isinstance(question_data["text_len"], float):
return run_heuristic(question_data["text_len"])
raise ValueError("Incorrect type(s) %s" % wrong_types)
return run_model(question_data)
验证模型输入允许您缩小故障模式并识别数据输入问题。接下来,您应该验证模型的输出。
模型输出
一旦模型进行预测,您应该确定是否应将其显示给用户。如果预测结果不在模型可接受答案范围内,您应考虑不显示它。
例如,如果您正在从照片预测用户的年龄,输出值应在 0 到略高于 100 岁之间(如果您在公元 3000 年阅读本书,请随时调整范围)。如果模型输出超出此范围的值,则不应显示它。
在这种情况下,可接受的结果不仅由可能的结果定义。它还取决于您对对用户有用的结果种类的估计。
对于我们的机器学习编辑器,我们希望只提供可操作的建议。如果模型预测用户所写的一切都应完全删除,那将是一个相当无用(并且侮辱性的)建议。以下是一个示例片段,验证模型输出并在必要时返回到一种启发式方法:
def validate_and_correct_output(question_data, model_output):
# Verify type and range and raise errors accordingly
try:
# Raises value error if model output is incorrect
verify_output_type_and_range(model_output)
except ValueError:
# We run a heuristic, but could run a different model here
run_heuristic(question_data["text_len"])
# If we did not raise an error, we return our model result
return model_output
当模型失败时,您可以像之前看到的那样返回到一个启发式方法,或者返回到您之前可能构建的一个简单模型。尝试早期类型的模型通常是值得的,因为不同的模型可能存在不相关的错误。
我在图 10-3 上展示了一个玩具示例。在左侧,您可以看到一个性能更好的模型,具有更复杂的决策边界。在右侧,您可以看到一个更糟糕、更简单的模型。更糟糕的模型会犯更多错误,但其错误与复杂模型不同,因为其决策边界形状不同。因此,简单模型会正确处理一些复杂模型处理错误的案例。这就是在主模型失败时使用简单模型作为备用的直觉合理性。
如果您确实将简单模型用作备用,您还应以相同方式验证其输出,并在未通过检查时退回到一种启发式方法或显示错误。
验证模型输出是否在合理范围内是一个良好的开始,但这还不够。在接下来的部分中,我们将讨论我们可以围绕模型构建的额外保护措施。

图 10-3. 简单模型通常会犯不同的错误
模型失败的备用方案
我们已经建立了防范措施来检测和纠正错误的输入和输出。然而,在某些情况下,我们的模型的输入可能是正确的,而模型的输出可能是合理的,但完全是错误的。
要回到从照片预测用户年龄的示例,保证模型预测的年龄是一个合理的人类年龄是一个良好的开始,但理想情况下,我们希望为这个特定用户预测正确的年龄。
没有模型会百分之百正确,轻微的错误通常是可以接受的,但尽可能地,您应该努力检测模型何时错误。这样做可以让您可能标记某个案例过于困难,并鼓励用户提供更简单的输入(例如,形式良好的照片)。
检测错误有两种主要方法。最简单的方法是跟踪模型的置信度来估计输出是否准确。第二种方法是构建一个额外的模型,任务是检测主模型可能失败的示例。
对于第一种方法,分类模型可以输出一个概率,可以用作模型对其输出的置信度的估计。如果这些概率被很好地校准(见“校准曲线”),它们可以用来检测模型不确定的实例,并决定不向用户显示结果。
有时候,即使模型为一个例子分配了很高的概率,也会出现错误。这就是第二种方法的出现的地方:使用一个模型来过滤最难的输入。
过滤模型
除了不总是可信之外,使用模型的置信度得分还有另一个强大的缺点。要获得这个分数,需要运行整个推理管道,无论其预测是否会被使用。当使用需要在 GPU 上运行的更复杂的模型时,这种方式尤其浪费。理想情况下,我们希望在不运行模型的情况下估计模型在一个示例上的表现。
这就是过滤模型的理念。由于你知道某些输入对于模型来说很难处理,你应该提前检测到它们,而不需要在它们上运行模型。过滤模型是输入测试的机器学习版本。它是一个二元分类器,被训练来预测一个模型在给定示例上的表现是否良好。这种模型的核心假设是,有些数据点对主模型来说很难。如果这些困难的示例有足够的共同点,过滤模型可以学会将它们与更容易的输入区分开来。
这里有一些你可能希望过滤模型捕捉到的输入类型:
-
与主模型表现良好的输入质量上有所不同的输入
-
主模型在训练过程中有困难的输入
-
有意欺骗主模型的对抗性输入
在图 10-4 中,你可以看到更新的示例,展示了图 10-2 中的逻辑,现在包括一个过滤模型。如你所见,只有在输入检查通过时才运行过滤模型,因为你只需过滤掉可能进入“运行模型”框的输入。
要训练一个过滤模型,你只需收集一个包含两类示例的数据集;主模型成功的类别和失败的类别。这可以使用我们的训练数据完成,不需要额外的数据收集!

图 10-4. 在我们的输入检查中添加一个过滤步骤(加粗)
在 图 10-5 中,我展示了如何通过利用训练好的模型及其在数据集上的结果来做到这一点,正如左侧图表所示。随机抽取一些模型预测正确的数据点和一些模型预测失败的数据点。然后,您可以训练一个过滤模型来预测原始模型预测失败的数据点。

图 10-5. 为过滤模型获取训练数据
一旦你有了训练好的分类器,训练过滤模型就相对简单。给定一个测试集和一个训练好的分类器,以下函数将完成此任务。
def get_filtering_model(classifier, features, labels):
"""
Get prediction error for a binary classification dataset
:param classifier: trained classifier
:param features: input features
:param labels: true labels
"""
predictions = classifier.predict(features)
# Create labels where errors are 1, and correct guesses are 0
is_error = [pred != truth for pred, truth in zip(predictions, labels)]
filtering_model = RandomForestClassifier()
filtering_model.fit(features, is_error)
return filtering_model
Google 在其智能回复功能中使用了这种方法,该功能为即将到达的电子邮件建议几个简短的回复(参见 A. Kanan 等人的文章 “Smart Reply: Automated Response Suggestion for Email”)。他们使用所谓的触发模型,负责决定是否运行建议回复的主模型。在他们的情况下,仅约 11% 的电子邮件适合该模型。通过使用过滤模型,他们将基础设施需求降低了一个数量级。
过滤模型通常需要满足两个标准。它应该快速,因为其主要目的是减少计算负担,并且应该擅长消除困难案例。
一个试图识别困难案例的过滤模型不需要能够捕获所有这些案例;它只需检测足够多的案例以证明在每次推理中运行它的额外成本是合理的。通常情况下,你的过滤模型越快,它需要的效果就越小。原因如下:
假设你仅使用一个模型的*均推理时间是。
使用过滤模型的*均推理时间将是 ,其中 f 是您的过滤模型执行时间,b 是它过滤掉的*均例子比例(b 代表 block)。
要通过使用过滤模型来减少您的*均推理时间,您因此需要 ,这等同于 。
这意味着你的模型过滤掉的案例比其推理速度与你更大模型速度之间的比例要高。
例如,如果您的过滤模型比常规模型快 20 倍( ),它需要阻止超过 5%的情况( ),才能在生产中发挥作用。
当然,您还需要确保您的过滤模型的精度很高,这意味着它所阻止的大多数输入实际上对于您的主模型来说确实太难了。
一个方法是定期让一些例子通过,这些例子被您的过滤模型阻止了,然后检查主模型在这些例子上的表现。我们将在“选择监控内容”中更深入地讨论这一点。
由于过滤模型与推理模型不同,并且专门训练来预测困难情况,因此它可以比依赖主模型的概率输出更准确地检测这些情况。因此,使用过滤模型既有助于减少不良结果的可能性,又有助于改善资源使用。
由于这些原因,将过滤模型添加到现有的输入和输出检查中,可以显著增加生产管道的稳健性。在接下来的部分中,我们将讨论更多方法,通过讨论如何将机器学习应用程序扩展到更多用户以及如何组织复杂的训练流程,来使管道更加稳健。
为性能进行工程设计
在将模型部署到生产环境时保持性能是一个重大挑战,特别是产品变得越来越受欢迎,新版本的模型定期部署。我们将从讨论允许模型处理大量推理请求的方法开始这一部分。然后,我们将涵盖使定期部署更新的模型版本更容易的功能。最后,我们将讨论通过使训练流程更可复制来减少模型性能变化的方法。
扩展到多个用户
许多软件工作负载是横向可扩展的,这意味着在请求数量增加时,启动额外的服务器是一种有效的策略,以保持响应时间合理。在这个方面,机器学习也不例外,因为我们可以简单地启动新服务器来运行我们的模型,并处理额外的容量。
注意
如果使用深度学习模型,您可能需要一个 GPU 在可接受的时间内提供结果。如果是这种情况,并且您预计将有足够的请求需要超过一个启用 GPU 的机器,您应该在两个不同的服务器上运行应用逻辑和模型推理。
因为 GPU 实例在大多数云服务提供商中的价格往往比普通实例高出一个数量级,因此采用一个较便宜的实例扩展应用程序,而 GPU 实例仅处理推理,将显著降低计算成本。在使用此策略时,应注意引入一些通信开销,并确保这对您的使用案例影响不大。
除了增加资源分配外,机器学习还有助于处理额外流量的高效方式,例如缓存。
机器学习的缓存
缓存是存储函数调用结果的实践,以便以后使用相同参数调用此函数时可以通过简单检索存储的结果来更快地运行。缓存是加速工程管道的常见实践,对机器学习非常有用。
推理结果的缓存
最*最少使用(LRU)缓存是一种简单的缓存方法,它包括跟踪模型的最*输入及其对应的输出。在对任何新输入运行模型之前,查找缓存中的输入。如果找到相应条目,则直接从缓存中提供结果。图 10-6 显示了这种工作流程的示例。第一行表示首次遇到输入时的缓存步骤。第二行描述了再次看到相同输入时的检索步骤。

图 10-6. 图像字幕模型的缓存
这种缓存策略适用于用户提供相同类型输入的应用程序。如果每个输入都是唯一的,则不适用。例如,一个应用程序接收动物爪印的照片以预测它们属于哪种动物,它很少会收到两张完全相同的照片,所以 LRU 缓存对其无济于事。
使用缓存时,应仅缓存无副作用的函数。例如,如果run_model函数还将结果存储到数据库中,则使用 LRU 缓存将导致不保存重复函数调用,这可能不是预期的行为。
在 Python 中,functools模块提供了一个 LRU 缓存的默认实现,您可以通过简单的装饰器来使用,如下所示:
from functools import lru_cache
@lru_cache(maxsize=128)
def run_model(question_data):
# Insert any slow model inference below
pass
当检索特征、处理它们和运行推理速度慢于访问缓存时,缓存最为有用。根据缓存方法的不同(例如内存中还是磁盘中)以及所使用模型的复杂性,缓存的有用程度会有所不同。
按索引缓存
尽管所描述的缓存方法在接收唯一输入时不适用,我们可以缓存管道的其他可预先计算部分。如果模型不仅依赖用户输入,则这样做最为简单。
假设我们正在构建一个系统,允许用户搜索与他们提供的文本查询或图像相关的内容。如果我们预计查询会显著变化,缓存用户查询可能不会显著提升性能。然而,作为一个搜索系统的构建者,我们可以访问我们目录中潜在项目的列表,这些项目我们事先知道,无论我们是在线零售商还是文档索引*台。
这意味着我们可以预先计算仅依赖于我们目录中项目的建模方面。如果我们选择一种允许我们提前进行此计算的建模方法,我们可以显著加快推理速度。
因此,构建搜索系统时的一种常见方法是首先将所有索引文档转换为有意义的向量(有关向量化方法,请参阅“向量化”)。一旦创建了嵌入,它们可以存储在数据库中。这在图 10-7 的顶行有所说明。当用户提交搜索查询时,在推理时对其进行嵌入,并在数据库中执行查找以找到最相似的嵌入并返回对应这些嵌入的产品。您可以在图 10-7 的底行中看到这一过程的图示。
这种方法显著加快了推理速度,因为大部分计算工作都已提前完成。在诸如 Twitter(参见Twitter 博客上的这篇文章)和 Airbnb(参见 M. Haldar 等人的文章,“将深度学习应用于 Airbnb 搜索”)等公司的大规模生产流水线中已成功使用了嵌入技术。

图 10-7. 带有缓存嵌入的搜索查询
缓存可以提高性能,但会增加复杂性。缓存的大小成为一个额外的超参数,根据您的应用工作负载进行调整。此外,任何时候模型或基础数据更新,都需要清除缓存,以防止提供过时的结果。更普遍地说,将正在生产中运行的模型更新到新版本通常需要小心处理。在接下来的部分,我们将介绍一些领域,可以帮助使这些更新更容易。
模型和数据生命周期管理
保持缓存和模型的更新可能具有挑战性。许多模型需要定期重新训练以维持其性能水*。虽然我们将在第十一章中介绍何时重新训练您的模型,但我想简要讨论一下如何将更新的模型部署给用户。
通常,训练好的模型被存储为一个包含其类型、架构以及学习到的参数信息的二进制文件。大多数生产应用程序在启动时将训练好的模型加载到内存中,并调用其提供结果。用新版本替换模型的一个简单方法是替换应用程序加载的二进制文件。如图 10-8(#replacing_model_binary)所示,这只会影响到流水线中的粗体框。
然而,在实践中,这个过程通常更加复杂。理想情况下,一个机器学习应用程序能够产生可重现的结果,能够适应模型更新,并且足够灵活,以处理重大的建模和数据处理变化。确保这一点需要进行一些额外的步骤,接下来我们将详细介绍。

图 10-8. 部署更新版本的相同模型似乎是一个简单的改变
可重现性
要追踪和重现错误,您需要知道生产环境中运行的是哪个模型。为此,需要保留训练过的模型及其训练数据集的存档。每个模型/数据集对应一个唯一标识符。每次在生产中使用模型时,应记录此标识符。
在图 10-9 中,我已将这些要求添加到加载和保存框中,以展示这对机器学习流水线的复杂性产生的影响。

图 10-9. 在保存和加载时添加关键元数据
除了能够提供不同版本的现有模型外,生产流水线还应该努力在没有显著停机时间的情况下更新模型。
弹性
使应用程序能够在更新后加载新模型需要建立一个过程,能够加载更新后的模型,理想情况下不会影响到用户的服务。这可能包括启动一个新的服务器来提供更新后的模型,并逐渐将流量转移到新模型上,但对于较大的系统来说,这很快就会变得更加复杂。如果新模型表现不佳,我们希望能够回滚到以前的版本。正确执行这两项任务是具有挑战性的,并且传统上被归类为 DevOps 领域。虽然我们不会深入探讨这个领域,但我们会在第 11 章中介绍监控。
生产环境的变更可能比更新模型更加复杂。它们可以包括对数据处理的大幅更改,这些更改也应该可以部署。
流水线的灵活性
我们之前看到,改进模型的最佳方法通常是通过迭代数据处理和特征生成。这意味着新版本的模型通常需要额外的预处理步骤或不同的特征。
这种变化不仅体现在模型二进制文件中,通常还与您的应用程序的新版本相关联。因此,在模型进行预测时,还应记录应用程序版本,以使此预测可重现。
这样做增加了我们管道的另一个复杂层次,在图 10-10 中以增加的预处理和后处理框进行了描述。这些现在也需要是可重现和可修改的。
部署和更新模型具有挑战性。在构建服务基础设施时,最重要的是能够复现模型在生产中运行的结果。这意味着将每个推理调用与运行的模型、模型训练的数据集以及为该模型提供服务的数据管道版本相关联起来。

图 10-10. 添加模型和应用程序版本
数据处理和 DAGs
要像前面描述的那样生成可重现的结果,训练管道也应该是可重现和确定性的。对于给定的数据集组合、预处理步骤和模型,训练管道应该在每次训练运行时生成相同的训练模型。
构建模型需要许多连续的转换步骤,因此管道经常会在不同位置中断。这使得确保每个部分都成功运行并且按正确顺序运行成为可能。
简化这一挑战的一种方法是将从原始数据到训练模型的过程表示为一个有向无环图(DAG),其中每个节点表示一个处理步骤,每个步骤表示两个节点之间的依赖关系。这个想法是数据流编程的核心,这也是流行的 ML 库 TensorFlow 所基于的编程范式。
DAG 可以是可视化预处理的一种自然方式。在图 10-11 中,每个箭头代表一个依赖于另一个任务的任务。该表示允许我们保持每个任务简单,使用图结构来表达复杂性。

图 10-11. 我们应用程序的 DAG 示例
一旦我们有了一个 DAG(有向无环图),我们就能够保证对每个生成的模型采取相同的操作序列。有多种方法可以为 ML 定义 DAG,包括像Apache Airflow或 Spotify 的Luigi这样的活跃开源项目。这两个软件包允许您定义 DAG,并提供一组仪表板,以便您监视 DAG 的进展和任何相关日志。
当首次构建 ML 管道时,使用 DAG 可能会显得不必要复杂,但一旦模型成为生产系统的核心组成部分,可重现性要求使 DAG 变得非常吸引人。一旦模型定期进行重新训练和部署,任何能帮助系统化、调试和版本化管道的工具都将成为重要的时间节省者。
结束本章,我将介绍另一种直接的方式来确保模型的性能良好——询问用户。
请求反馈
本章介绍了能够确保我们及时向每个用户提供准确结果的系统。为了保证结果的质量,我们讨论了检测模型预测是否不准确的策略。为什么不问问用户呢?
当显示模型预测时,你可以通过请求明确反馈或测量隐式信号来从用户那里获取反馈。这可以简单到一个对话框询问“这个预测有用吗?”或者更为微妙的方式。
例如,预算应用 Mint 自动为账户上的每笔交易分类(包括旅行、食品等)。正如在图 10-12 中所示,每个类别在用户界面中显示为用户可以编辑和纠正的字段。这样的系统允许收集宝贵的反馈,以持续改进模型,比起满意度调查等方式,更少侵入性。

Figure 10-12. 让用户直接修正错误
用户无法为模型每次预测提供反馈,因此收集隐式反馈是评估 ML 性能的重要方式。收集此类反馈包括查看用户执行的操作,以推断模型是否提供了有用的结果。
隐式信号非常有用但更难解释。你不能指望找到一个总是与模型质量相关的隐式信号,只能找到在总体上相关的信号。例如,在推荐系统中,如果用户点击了一个推荐的物品,你可以合理地假设该推荐是有效的。这在所有情况下都不成立(有时人们会误点击!),但只要大多数情况下是真的,这就是一个合理的隐式信号。
通过收集这些信息,如图 10-13 所示,你可以估计用户发现结果有用的频率。收集此类隐式信号非常有用,但也伴随着收集和存储数据的风险,并且可能引入我们在第八章中讨论过的负反馈循环。

Figure 10-13. 用户行为作为反馈的来源
在你的产品中构建隐式反馈机制可以是收集额外数据的一种有价值的方式。许多动作可以被视为隐式和显式反馈的混合。
假设我们在我们的 ML 编辑器的推荐中添加了一个“在 Stack Overflow 上提问”的按钮。通过分析导致用户点击此按钮的预测,我们可以衡量推荐中可以作为问题发布的比例。通过添加此按钮,我们并不直接询问用户建议是否良好,而是允许他们采取行动,从而为我们提供了“弱标签”(请参见“数据类型”中对弱标记数据的提醒)来评估问题质量。
除了作为训练数据的良好来源外,隐式和显式用户反馈也可以是注意到 ML 产品性能下降的第一种方式。虽然理想情况下应在显示给用户之前捕获错误,但监控此类反馈有助于更快地检测和修复错误。我们将在第十一章中更详细地讨论这一点。
部署和更新模型的策略因团队规模和他们在机器学习方面的经验而异。本章中的一些解决方案对于像 ML 编辑器这样的原型而言过于复杂。另一方面,一些投入大量资源到机器学习的团队已经建立了复杂的系统,使他们能够简化部署过程,并保证给用户高水*的质量。接下来,我将分享一个关于克里斯·穆迪的采访,他领导着 Stitch Fix 的 AI Instruments 团队,并将带我们了解他们在部署机器学习模型时的理念。
克里斯·穆迪:赋予数据科学家们部署模型的权力
克里斯·穆迪毕业于加州理工学院和加州大学圣克鲁兹分校,拥有物理学背景,现在领导着 Stitch Fix 的 AI Instruments 团队。他对自然语言处理有浓厚兴趣,并涉足深度学习、变分方法和高斯过程。他为深度学习库 Chainer 做出了贡献,为 scikit-learn 贡献了超快的 Barnes–Hut 版本的 t-SNE,还编写了(为数不多的!)稀疏张量分解库 Python。他还建立了自己的 NLP 模型,lda2vec。
Q:在 Stitch Fix,数据科学家们在模型生命周期的哪个部分工作?
A: 在 Stitch Fix,数据科学家拥有整个建模管道。这个管道很广泛,包括构思、原型设计、设计和调试、ETL 以及使用 scikit-learn、pytorch 和 R 等语言和框架进行模型训练。此外,数据科学家负责建立度量系统和为模型设置“健全性检查”。最后,数据科学家运行 A/B 测试,监控错误和日志,并根据观察到的情况重新部署更新的模型版本。为了能够做到这一点,他们利用*台和工程团队的工作成果。
Q: *台团队为简化数据科学工作做了什么?
A: *台团队的工程师的目标是为建模找到合适的抽象。这意味着他们需要了解数据科学家的工作方式。工程师们不为在特定项目上工作的数据科学家构建单独的数据管道。他们构建的是能让数据科学家自己完成这些工作的解决方案。更普遍地说,他们构建工具来赋予数据科学家全流程所有权。这使工程师们能够花更多时间改进*台,而不是构建临时解决方案。
Q: 一旦部署了模型,你如何评估其性能?
A: Stitch Fix 的一大优势在于使人类和算法共同工作。例如,Stitch Fix 投入了大量时间思考向他们的设计师呈现信息的正确方式。从根本上说,如果你在一端暴露你的模型有一个 API,并在另一端有像设计师或商品购买者这样的用户,你应该如何设计他们之间的交互?
乍一看,你可能会想要构建一个前端,简单地向用户展示算法的结果。不幸的是,这可能会让用户感觉他们对算法和整个系统没有控制权,在算法表现不佳时会感到沮丧。相反,你应该将这种交互视为一个反馈循环,允许用户进行纠正和调整结果。这样做可以让用户训练算法,并对整个过程产生更大的影响,因为他们可以提供反馈。此外,这还可以帮助你收集标记数据以评估模型的性能。
要做到这一点,数据科学家应该问自己如何向用户展示模型,以便既能简化他们的工作,又能赋予他们使模型变得更好的能力。这意味着由于数据科学家最了解什么样的反馈对他们的模型最有用,因此他们对从端到端拥有整个过程至关重要。他们可以捕捉到任何错误,因为他们可以看到整个反馈循环。
Q: 你如何监控和调试模型?
- A: 当你的工程团队构建了优秀的工具时,监控和调试变得更加容易。Stitch Fix 已经构建了一个内部工具,它接收建模管道并创建 Docker 容器,验证参数和返回类型,将推断管道作为 API 公开,部署到我们的基础设施上,并在其之上构建仪表板。这些工具允许数据科学家在部署期间或之后直接修复任何错误。因为数据科学家现在负责解决模型故障,我们还发现这种设置促使简单且稳健的模型更少出现故障。整个流程的所有权促使个人优化影响和可靠性,而不是模型复杂性。
Q: 你是如何部署新模型版本的?
- A: 此外,数据科学家们通过使用自定义构建的 A/B 测试服务来运行实验,该服务允许他们定义精细的参数。然后他们分析测试结果,如果团队认为结果是确凿的,他们会自行部署新版本。
在部署方面,我们使用类似金丝雀开发的系统,从一个实例开始部署新版本,然后逐步更新实例并监控性能。数据科学家们可以访问仪表板,显示每个版本下的实例数量和随着部署进展的连续性能指标。
结论
在本章中,我们已经讨论了通过积极检测模型潜在故障并找到缓解方法来使我们的响应更具弹性的方式。这包括确定性验证策略和使用过滤模型。我们还讨论了保持生产模型更新的一些挑战。然后,我们探讨了如何评估模型性能的几种方法。最后,我们看了一个频繁部署 ML 的大规模公司的实际例子,以及他们为此构建的流程。
在 第十一章 中,我们将介绍额外的方法来监控模型的性能,并利用各种指标来诊断 ML 驱动应用的健康状况。
第十一章:监控和更新模型
一旦模型部署,其性能应该像任何其他软件系统一样受到监控。就像他们在 “测试你的 ML 代码” 中所做的那样,常规软件最佳实践同样适用。并且就像在 “测试你的 ML 代码” 中一样,处理机器学习模型时还有其他需要考虑的事项。
在本章中,我们将描述监控机器学习模型时需要牢记的关键方面。更具体地,我们将回答三个问题:
-
为什么我们应该监控我们的模型?
-
我们如何监控我们的模型?
-
我们的监控应该驱动什么行动?
开始我们先讨论监控模型如何帮助决定何时部署新版本或发现生产中的问题。
监控能够挽救生命
监控的目标是跟踪系统的健康状况。对于模型来说,这意味着监控它们的性能和预测质量。
如果用户习惯的改变突然导致模型产生次优的结果,一个良好的监控系统将允许您尽快注意并作出反应。让我们讨论一些监控可以帮助我们捕捉的关键问题。
用于指导刷新率的监控
我们在 “新鲜度和分布变化” 中看到,大多数模型需要定期更新以保持给定性能水*。监控可以用来检测模型何时不再新鲜并需要重新训练。
例如,假设我们利用用户的隐式反馈(例如他们是否点击推荐内容)来估计模型的准确性。如果我们持续监控模型的准确性,我们可以在准确性低于定义的阈值时立即训练一个新模型。 图 11-1 展示了这一过程的时间轴,重新训练事件发生在准确性低于阈值时。

图 11-1. 触发重新部署的监控
在重新部署更新模型之前,我们需要验证新模型是否更好。我们稍后将介绍如何做到这一点,在本节中的 “ML 的 CI/CD”。首先,让我们解决其他需要监控的方面,比如潜在的滥用问题。
监控以检测滥用
在某些情况下,例如构建滥用预防或欺诈检测系统时,一部分用户正在积极地尝试击败模型。在这些情况下,监控成为检测攻击和估算其成功率的关键方式。
监控系统可以使用异常检测来检测攻击。例如,当追踪银行在线门户的每次登录尝试时,如果登录尝试数量突然增加十倍,监控系统可能会发出警报,这可能是攻击的迹象。
根据越过阈值值发出警报的监控,就像您在图 11-2 中看到的那样,或包括更加微妙的指标,例如登录尝试增加的速率。根据攻击的复杂性,构建一个模型来检测这些异常可能比简单的阈值更有价值。

图 11-2. 监控仪表板上明显的异常。您可以构建一个额外的 ML 模型来自动检测它。
除了监控新鲜度和检测异常之外,我们应该监控哪些其他指标?
选择监控内容
软件应用通常监控指标,如处理请求的*均时间、未能处理的请求比例以及可用资源的数量。这些对于任何生产服务的跟踪都是有用的,并允许在太多用户受到影响之前采取积极的补救措施。
接下来,我们将覆盖更多的指标以便检测模型性能开始下降的情况。
性能指标
如果数据分布开始变化,模型可能会变得陈旧。您可以在图 11-3 中看到这一点。

图 11-3. 特征分布漂移示例
在处理分布变化时,数据的输入和输出分布都可能发生变化。例如,考虑一个试图猜测用户将来会观看哪部电影的模型。给定相同的用户历史作为输入,基于可用电影目录的新条目,模型的预测应该会改变。
-
跟踪输入分布的变化(也称为特征漂移)比跟踪输出分布更容易,因为访问满足用户期望的理想输出值可能具有挑战性。
-
监控输入分布可以简单地监控诸如关键特征的均值和方差等汇总统计数据,并在这些统计数据偏离训练数据中的值超过给定阈值时发出警报。
-
监控分布变化可能更具挑战性。一个首要方法是监控模型输出的分布。类似于输入,输出分布的显著变化可能表明模型性能已经降低。然而,用户希望看到的结果分布可能更难估计。
估计基础事实可能困难的原因之一是,模型的行动通常会阻止我们观察它。为了理解可能的情况,请考虑信用卡欺诈检测模型的示例图 11-4。模型将接收到的数据分布在左侧。随着模型对数据进行预测,应用代码根据这些预测采取行动,阻止任何预测为欺诈的交易。
一旦交易被阻止,我们就无法观察如果我们让其通过会发生什么。这意味着我们无法知道被阻止的交易是否真的是欺诈的。我们只能观察和标记我们放行的交易。因为基于模型预测行动,我们只能观察到一个偏斜的非阻止交易分布,显示在右侧。

图 11-4。基于模型预测采取行动可能会偏倚观察到的数据分布。
由于只能访问偏斜样本的真实分布,这使得正确评估模型的性能变得不可能。这是反事实评估的焦点,其目标是评估如果我们没有对模型采取行动会发生什么。为了在实践中执行这样的评估,您可以在一小部分示例上暂停运行模型(参见李力宏等人的文章,“点击指标的反事实估计和优化”(https://arxiv.org/abs/1403.1891))。不对随机示例采取行动将使我们能够观察到一个无偏的欺诈交易分布。通过比较模型预测与随机数据的真实结果,我们可以开始估计模型的精度和召回率。
这种方法提供了一种评估模型的方式,但代价是让一部分欺诈交易通过。在许多情况下,这种权衡是有利的,因为它允许模型的基准测试和比较。在某些情况下,比如在医疗领域,随机预测输出是不可接受的,就不应该采用这种方法。
在“ML 的 CI/CD”中,我们将涵盖其他比较模型并决定部署哪些模型的策略,但首先让我们了解要跟踪的其他关键类型的指标。
业务指标
正如我们在整本书中所看到的,与产品和业务目标相关的最重要的指标。它们是我们评判模型性能的标尺。如果所有其他指标都是良好的,而其余的生产系统也表现良好,但用户不点击搜索结果或使用推荐,那么产品在定义上是失败的。
因此,应密切监控产品指标。对于诸如搜索或推荐系统之类的系统,此监控可以跟踪点击率(CTR),即看到模型推荐后实际点击它的比率。
一些应用程序可能会从对产品进行修改中受益,以更轻松地追踪产品成功,类似于我们在 “请求反馈” 中看到的反馈示例。我们讨论了添加共享按钮,但我们可以在更细粒度的水*上跟踪反馈。如果我们能够让用户点击推荐内容以实施它们,我们可以跟踪每个建议的使用情况,并使用这些数据训练模型的新版本。图 11-5 显示了左侧的整体方法和右侧的细粒度方法的对比图。

图 11-5. 提议词级建议为我们提供了更多收集用户反馈的机会
由于我不希望 ML 编辑器原型频繁使用,以致于描述的方法无法提供足够大的数据集,我们将在此处放弃构建它。如果我们打算维护一个产品,收集这样的数据将使我们能够精确地获取用户对哪些建议最有用的反馈。
现在我们已经讨论了监控模型的原因和方法,接下来让我们探讨如何处理监控中检测到的任何问题。
CI/CD 用于机器学习
CI/CD 指的是持续集成(CI)和持续交付(CD)。粗略来说,CI 是让多个开发者定期将他们的代码合并到一个中心代码库的过程,而 CD 则专注于提高发布新软件版本速度的方法。采用 CI/CD 实践使个人和组织能够快速迭代和改进应用程序,不论是发布新功能还是修复现有的 bug。
因此,CI/CD 用于机器学习旨在使部署新模型或更新现有模型变得更加容易。快速发布更新很容易,但挑战在于保证其质量。
在涉及机器学习时,我们看到仅仅拥有一个测试套件并不能保证新模型优于之前的模型。训练一个新模型并测试其在留存数据上的表现是一个良好的第一步,但最终,正如我们之前看到的,没有什么能够取代实时性能来评判模型的质量。
在将模型部署给用户之前,团队通常会在其论文中所指的“机器学习模型管理挑战”中提到的 影子模式 中部署它们。这指的是将新模型与现有模型并行部署的过程。在运行推理时,会计算和存储两个模型的预测结果,但应用程序仅使用现有模型的预测结果。
通过记录新预测值,并在可能时将其与旧版本和地面实况进行比较,工程师可以估计新模型在生产环境中的性能,而不改变用户体验。这种方法还允许测试运行用于运行比现有模型更复杂的新模型所需的基础设施。影子模式唯一无法提供的是观察用户对新模型的响应的能力。唯一的方法是实际部署它。
一旦模型经过测试,就有可能部署该模型。部署新模型伴随着向用户展示性能下降的风险。减轻这种风险需要一些注意,并且是实验领域的焦点。
图 11-6 显示了我们在此处介绍的三种方法的可视化,从最安全的在测试集上评估模型到最具信息量且最危险的在生产环境中部署模型。请注意,虽然影子模式确实需要工程投入以能够在每个推断步骤中运行两个模型,但它允许评估模型几乎与使用测试集一样安全,并提供几乎与在生产中运行相同数量的信息。

图 11-6. 评估模型的方式,从最安全和最不准确到最危险和最准确
由于在生产中部署模型可能是一个风险的过程,工程团队已经开发了逐步部署更改的方法,从仅向一小部分用户展示新结果开始。我们将在接下来进行介绍。
A/B 测试和实验
在机器学习中,实验的目标是在尽可能减少试验次优模型的成本的同时,最大化使用最佳模型的机会。有许多实验方法,其中最流行的是 A/B 测试。
A/B 测试的原则很简单:向用户样本展示新模型,其余展示另一个。这通常通过将较大的“控制”组提供当前模型,并将较小的“处理”组提供我们想要测试的新版本来完成。一旦我们运行了足够长时间的实验,我们比较两组的结果,并选择更好的模型。
在 图 11-7 中,您可以看到如何从总体人群中随机抽样用户以分配到测试集。在推断时,用于给定用户的模型由其分配的组确定。
A/B 测试背后的理念很简单,但实验设计的问题,例如选择控制组和处理组,决定足够的时间量,以及评估哪个模型表现更好,都是具有挑战性的问题。

图 11-7. A/B 测试示例
此外,A/B 测试需要构建额外的基础设施,以支持能够向不同用户提供不同模型的能力。让我们更详细地讨论这些挑战。
选择组和持续时间
决定哪些用户应该服务哪些模型有一些要求。两组用户应尽可能相似,以便可以将任何观察到的结果差异归因于我们的模型,而不是归因于队列中的差异。如果 A 组的所有用户都是核心用户,而 B 组只包含偶发用户,则实验的结果将不具有决定性。
此外,治疗组 B 应足够大,以便得出具有统计学意义的结论,但尽可能小,以限制潜在较差模型的曝光。测试的持续时间存在类似的权衡:太短,我们面临信息不足的风险,太长,我们面临失去用户的风险。
这两个约束已经足够具有挑战性,但请考虑一下拥有数百名数据科学家并行运行数十个 A/B 测试的大型公司的情况。多个 A/B 测试可能同时测试管道的同一方面,这使得准确确定单个测试效果更加困难。当公司达到这种规模时,这导致它们构建实验*台以处理复杂性。请参阅 Jonathan Parks 的文章中描述的 Airbnb 的 ERF,“Scaling Airbnb’s Experimentation Platform”;A. Deb 等人的文章中描述的 Uber 的 XP,“Under the Hood of Uber’s Experimentation Platform”;或 Intuit 开源的 Wasabi 的 GitHub 存储库,Wasabi。
估计更好的变体
大多数 A/B 测试选择他们想在组之间比较的指标,例如 CTR。不幸的是,估计哪个版本表现更好比选择具有最高 CTR 的组更复杂。
由于我们预计任何指标结果都会有自然波动,因此我们首先需要确定结果是否具有统计学意义。由于我们正在估计两个群体之间的差异,因此最常用的测试是双样本假设检验。
为了得出结论性实验,需要在足够量的数据上运行。确切的数量取决于我们正在测量的变量值和我们试图检测的变化的规模。有关实际示例,请参见 Evan Miller 的样本大小计算器。
重要的是在运行实验之前决定每个组的大小和实验的长度。如果您在进行 A/B 测试时不断测试显著性,并且一旦看到显著结果就宣布测试成功,那么您将犯下重复显著性测试错误。这种错误是通过机会主义地寻找显著性来严重高估实验的显著性(Evan Miller 在这里有一个很好的解释 here)。
注意
虽然大多数实验专注于比较单一指标的价值,但也重要考虑其他影响。如果*均点击率增加,但停止使用产品的用户数量翻倍,我们可能不应认为该模型更好。
同样,A/B 测试的结果应考虑不同用户段的结果。如果*均点击率增加,但某一段的点击率暴跌,也许不应该部署新模型。
实施实验需要能力将用户分配到一个组中,跟踪每个用户的分配,并根据此呈现不同结果。这需要建立额外的基础设施,接下来我们将详细介绍。
构建基础设施
实验还伴随着基础设施需求。运行 A/B 测试的最简单方法是将每个用户关联的组与其他用户相关信息一起存储,例如在数据库中。
应用程序随后可以依赖于分支逻辑,根据给定字段的值决定运行哪个模型。这种简单的方法在用户已登录的系统中运行良好,但如果模型对未登录用户可访问,则变得更加困难。
这是因为实验通常假设每个组是独立的,并且只暴露给一个变体。当向未登录用户提供模型时,很难保证某个用户在每个会话中始终服务于相同的变体。如果大多数用户接触多个变体,这可能会使实验结果无效。
其他信息以识别用户,如浏览器 cookie 和 IP 地址,可用于识别用户。然而,这些方法再次需要建立新的基础设施,这对于小型资源受限的团队可能很困难。
其他方法
A/B 测试是一种流行的实验方法,但也存在其他方法试图解决一些 A/B 测试的限制。
多臂老丨虎丨机是一种更灵活的方法,可以持续测试变体并超过两种替代方案。 它们根据每个选项的表现动态更新要提供的模型。 我在图 11-8 中说明了多臂老丨虎丨机的工作原理。 老丨虎丨机不断地记录每个替代方案的表现情况,基于它们路由的每个请求的成功。 大多数请求简单地路由到当前最佳替代方案,如左侧所示。 小部分请求路由到随机替代方案,如右侧所示。 这允许老丨虎丨机更新它们对哪个模型最好的估计,并检测目前未提供服务的模型是否开始表现更好。

图 11-8. 实践中的多臂老丨虎丨机
上下文多臂老丨虎丨机将这一过程推向更深层次,通过学习每个特定用户更好的模型选项。 如需更多信息,我建议查看 Stitch Fix 团队的这篇概述。
注意
尽管本节覆盖了使用实验验证模型的方法,但公司越来越多地使用实验方法来验证他们应用程序所做的任何重大更改。 这使他们能够持续评估用户发现有用的功能以及新功能的表现。
由于实验是如此艰难且容易出错的过程,多家初创公司已开始提供“优化服务”,允许客户将其应用程序集成到托管的实验*台中,以决定哪些变体表现最佳。 对于没有专门实验团队的组织来说,这些解决方案可能是测试新模型版本的最简单方法。
结论
总体而言,部署和监控模型仍然是一个相对新的实践。 这是验证模型是否产生价值的关键方法,但通常需要在基础设施工作和仔细的产品设计方面做出重大努力。
随着该领域开始成熟,诸如Optimizely之类的实验*台已经出现,以简化部分工作。 理想情况下,这应该赋予 ML 应用程序的构建者持续改进它们的能力,造福所有人。
回顾本书描述的所有系统,只有少部分旨在训练模型。 大多数与构建 ML 产品相关的工作涉及数据和工程工作。 尽管事实如此,我指导过的大多数数据科学家发现更容易找到涵盖建模技术的资源,因此感到没有准备好处理该领域之外的工作。 本书是我帮助弥合这一差距的尝试。
构建机器学习应用程序需要广泛的技能,涵盖统计学、软件工程和产品管理等多个领域。该过程的每个部分都足够复杂,需要多本书来详细阐述。本书的目标是为您提供一整套工具,帮助您构建这样的应用程序,并通过“附加资源”中提出的建议来决定更深入探索哪些主题,例如。
基于此,我希望本书能为您提供工具,使您更有信心地应对构建机器学习驱动产品所涉及的大部分工作。我们涵盖了机器学习产品生命周期的每个环节,从将产品目标转化为机器学习方法开始,然后找到和筛选数据,迭代模型,最后验证其性能并部署它们。
无论您是从头到尾阅读了本书,还是深入研究了与您工作最相关的特定章节,现在您应该具备了开始构建自己的机器学习应用程序所需的知识。如果本书帮助您构建了什么,或者对书中内容有任何问题或意见,请通过电子邮件联系我:mlpoweredapplications@gmail.com。期待收到您的来信,看到您的机器学习作品。


浙公网安备 33010602011771号