Python-数据中心的机器学习-全-
Python 数据中心的机器学习(全)
原文:
annas-archive.org/md5/db8f2beb902c7755f2a881df92b038cc译者:飞龙
前言
如果你正在阅读这本书,你已经迈出了探索之旅的第一步,旨在构建和实施更稳健、准确、公平、偏见更小且更容易解释的机器学习模型。
我们知道这是一个很大的声明。然而,我们基于在数据为中心的机器学习开发方法中看到的大量且相对未被开发的潜力,我们感到很自在地做出这个声明。
为什么我们认为以数据为中心的机器学习是开创性的?
改善数据质量将导致更多预测模型似乎很明显。然而,迄今为止的机器学习研究主要集中在不断改进各种算法和工具来构建和调整模型。
因此,我们手头上有大量的机器学习算法、工具和技术,可以在输入数据的质量和数量合适的情况下,以低成本为我们提供优秀的模型。
模型架构在大多数情况下都是一个已解决的问题。数据科学家及其所在的组织通常缺乏的是提高数据质量的最佳实践框架、工具和技术。
以数据为中心的机器学习建立在以模型为中心的方法基础上,通过利用更好的输入数据带来的巨大机会。
对数据收集和工程给予更大的重视要求我们优化收集高质量数据的过程,并发明新的技术来工程数据集,以更少的数量提供更多信号。
本书将介绍的大多数技术和例子都基于前沿研究和将现代实践应用于收集、工程和合成生成优秀数据集的应用。
以数据为中心的机器学习还需要数据科学家、领域专家和数据标记者之间更强的合作。正如你将在本书中了解到的那样,数据中心性通常始于人类以服务于运营和数据科学需求的方式收集和标记数据。
在许多组织中,专门为机器学习目的收集数据并不常见。一种更系统的方法来收集和标记数据用于数据科学,不仅会导致更好的数据,而且会将领域专家和数据科学家的思维和创造力结合起来。这种不同领域专家之间的积极反馈循环为思想的繁荣创造了新的机会,远远超出了个人机器学习项目的范围。
为什么我们声称以数据为中心的模型在几乎所有方面都将优于以模型为中心的对应模型?
想想你经常使用的任何高质量消费品。这可能是一台电脑,你驾驶的汽车,你坐的椅子,或者需要一定设计水平和技术水平的其他东西。
高质量的标准是什么?
设计和功能与之有很大关系,但除非产品由优质材料制成,否则它不会按预期工作,或者可能会完全损坏。只有当某物按预期工作并且持续工作时,它才算是高质量的。
对于机器学习模型也是如此。通过系统地提高数据质量——我们的建筑材料——我们能够构建出更具预测性、鲁棒性和可解释性的模型。
我们编写这本书是为了向您,我们的读者,提供实施数据中心化机器学习并参与人工智能革命下一阶段所需的最重要背景知识、工具、技术和应用示例。
在本书的技术章节中,我们将向您展示如何使用 Python 将数据中心化机器学习的原则应用于实际数据集,我们将探讨的技术和实际应用示例将为您提供一套工具箱,以系统化和编程方式收集、清理、增强和标记数据,以及识别和消除不希望的偏差。
在本书的结尾,您将对数据中心化机器学习的构建块和最佳实践方法有深刻的认识。
不要只听我们的一面之词。让我们深入探讨数据中心化机器学习。
本书面向的对象
这本书是为想要了解数据中心化是什么、其相对于模型中心化方法的优点以及如何将最佳实践的数据中心化方法应用于其工作的数据科学专业人士和机器学习爱好者而编写的。
这本书也是为其他数据专业人士和高级管理人员编写的,他们想要探索提高数据质量的工具和技术,以及如何在他们的组织中创造“小数据”机器学习/人工智能的机会。
本书涵盖的内容
第一章,探索数据中心化机器学习,对数据中心化机器学习进行了全面的定义,并与它的对立面——模型中心化——进行了对比。我们使用实际例子来比较经验性能,并说明这两种方法之间的关键差异。
第二章,从模型中心化到数据中心化——机器学习的演变,带您穿越人工智能和机器学习的演变历程,强调在模型调整之外提高数据质量的未开发潜力。我们还揭穿了“大数据”神话,展示了转向“优质数据”如何使机器学习解决方案民主化。准备好从数据在机器学习中的力量中获得新的视角。
第三章,数据中心化机器学习原理,为您深入数据中心化机器学习的旅程奠定了基础,概述了数据中心化机器学习的四个关键原则。这些原则提供了至关重要的背景知识——即为什么——在我们深入探讨与每个原则相关的具体方法和途径——即是什么——在随后的章节中。
第四章, 数据标注是一个协作过程,探讨了在机器学习开发中主题领域专业知识、训练有素的标注员和明确指令的关键作用。在本章中,你将了解数据标注的人本性,并获得提高其质量以减少偏差、增加一致性和构建更丰富数据集的策略。
第五章, 数据清洗技术,探讨了数据质量的六个关键方面,并展示了各种数据清洗技术,这是通过纠正错误来提高数据质量的重要过程。我们阐述为什么质疑和系统地提高数据质量对于可靠的机器学习系统至关重要,同时教授你必要的数据清洗技能。
第六章, 机器学习中程序化标注技术,关注于提高数据质量和信号强度的程序化标注技术。我们探讨了程序化标注的优缺点,并提供了如何执行和验证这些技术的实际示例。
第七章, 在数据为中心的机器学习中使用合成数据,介绍了合成数据作为一种高效且成本效益高的方法,以克服传统数据收集和标注的局限性。在本章中,你将了解什么是合成数据,它是如何用于改进模型的,生成它的技术,以及其风险和挑战。
第八章, 识别和消除偏差的技术,关注我们在收集数据、将数据和模型应用于问题以及许多数据集中固有的人类偏差中的偏差问题。我们将通过数据为中心的技术来识别和以道德方式纠正偏差。
第九章, 处理机器学习中的边缘案例和罕见事件,解释了在机器学习中检测罕见事件的过程。我们探讨了各种方法和技巧,讨论了评估指标的重要性,并说明了识别罕见事件产生的广泛影响。
第十章, 开启数据为中心的机器学习之旅,揭示了在模型开发和部署过程中可能遇到的技术和非技术挑战。本章展示了数据为中心的方法如何帮助你克服这些挑战,为你在组织内扩大机器学习应用和增长机会开辟了广阔的前景。
为了充分利用这本书
要从本书中获取最大价值,对机器学习概念有先前的了解、对统计方法的基础知识以及熟悉 Python 编程将非常有帮助。本书专为熟悉机器学习过程并希望深入了解以数据为中心的机器学习和人工智能世界的人士编写。
| 本书涵盖的软件/硬件 | 操作系统要求 | 
|---|---|
| Python 3 | Windows、macOS 或 Linux | 
如果您正在使用这本书的数字版,我们建议您亲自输入代码或从书的 GitHub 仓库(下一节中有一个链接)获取代码。这样做将帮助您避免与代码的复制和粘贴相关的任何潜在错误。
下载示例代码文件
您可以从 GitHub 下载本书的示例代码文件:github.com/PacktPublishing/Data-Centric-Machine-Learning-with-Python。如果代码有更新,它将在 GitHub 仓库中更新。
我们还有其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!
使用的约定
本书使用了许多文本约定。
文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“我们将调用loan_dataset.csv文件,并将其保存在同一目录中,然后我们将从这个目录运行此示例。”
代码块设置如下:
import pandas as pd
import os
FILENAME = "./loan_dataset.csv"
DATA_URL = "http://archive.ics.uci.edu/ml/machine-learning-databases/00350/default%20of%20credit%20card%20clients.xls"
粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“机器学习中的偏差可以采取多种形式,因此我们将这些偏差分为两种主要类型,易于识别的偏差和难以识别的偏差。”
小贴士或重要注意事项
看起来像这样。
联系我们
欢迎读者反馈。
一般反馈:如果您对本书的任何方面有疑问,请通过电子邮件发送给我们至 customercare@packtpub.com,并在邮件主题中提及书名。
勘误表:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将非常感激您能向我们报告。请访问www.packtpub.com/support/errata并填写表格。
盗版:如果您在互联网上以任何形式遇到我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过 mailto:copyright@packt.com 与我们联系,并提供材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
分享您的想法
一旦您阅读了使用 Python 进行以数据为中心的机器学习,我们非常乐意听到您的想法!请点击此处直接进入此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。
下载此书的免费 PDF 副本
感谢您购买此书!
您喜欢随时随地阅读,但无法携带您的印刷书籍到处走?
您的电子书购买是否与您选择的设备不兼容?
别担心,现在每本 Packt 书籍都免费提供该书的 DRM 免费 PDF 版本,无需额外费用。
在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠远不止于此,您还可以获得独家折扣、时事通讯和每日免费内容的每日电子邮件。
按照以下简单步骤获取优惠:
- 扫描下面的二维码或访问以下链接!img/B19297_QR_Free_PDF.jpg
 
packt.link/free-ebook/9781804618127
- 
提交您的购买证明
 - 
就这些!我们将直接将您的免费 PDF 和其他优惠发送到您的电子邮件
 
第一部分:什么是以数据为中心的机器学习以及为什么我们需要它
在这部分,我们深入探讨以数据为中心的机器学习,将其与以模型为中心的方法进行对比。我们使用真实生活中的例子来说明它们的差异,并探讨人工智能和机器学习向以数据为中心的视角的演变。我们还消除了“大数据”的神话,强调了质量胜于数量的重要性,以及民主化机器学习解决方案的潜力。准备好以全新的视角来了解数据在机器学习中的变革力量。
本部分包含以下章节:
- 
第一章**,探索以数据为中心的机器学习
 - 
第二章**,从以模型为中心到以数据为中心——机器学习的演变
 
第一章:探索数据为中心的机器学习
本章提供了对数据为中心的机器学习(ML)的基础理解。我们还将对比数据中心性与模型中心性,并比较两种方法的性能,使用实际例子来说明关键点。通过这些实际例子,你将深刻理解数据中心性的潜力。
在本章中,我们将涵盖以下主要主题:
- 
理解数据为中心的机器学习
 - 
以数据为中心与以模型为中心的机器学习
 - 
质量数据在机器学习中的重要性
 
理解数据为中心的机器学习
以数据为中心的机器学习是系统地构建机器学习和人工智能(AI)系统所使用数据的学科。
数据为中心的 AI 和 ML 运动基于这样的哲学:在构建高度信息模型时,数据质量比数据量更重要。换句话说,使用一个小但高质量的数据集比使用一个大但嘈杂的数据集能取得更多成果。对于大多数机器学习用例,基于非常大的数据集(比如数百万个观察值)构建模型是不切实际的,因为数据量根本不存在。换句话说,基于可用数据集太小而忽略机器学习作为解决某些问题的工具的潜在用途是很常见的。
但如果我们能够使用机器学习来解决基于更小数据集的问题,甚至小于 100 个观察值呢?这是数据中心运动试图通过系统性的数据收集和工程来解决的一个挑战。
对于大多数机器学习用例,你需要的算法已经存在。你输入数据(x)和依赖变量标签(y)的质量才是决定因素。传统上处理数据集中噪声的响应是尽可能获取更多数据以平均异常值。以数据为中心试图提高数据中的信号,这样就不需要更多的数据。
需要注意的是,数据中心性也为更大的数据解决方案标记了下一个前沿。无论你的数据集有多大或多小,它都是你机器学习解决方案的基础成分。让我们更深入地看看数据为中心的机器学习的不同方面。
数据中心性的起源
推动向更以数据为中心的机器学习开发方法迈进的是著名的数据科学先驱,安德鲁·吴博士。
吴博士是大型开放在线课程平台 Coursera 的联合创始人,同时也是斯坦福大学计算机科学系的兼职教授。他还是教育公司 DeepLearning.AI 和制造 AI 驱动的视觉检测平台 Landing AI 的创始人兼首席执行官。他之前在百度担任首席科学家,并曾是谷歌大脑团队的创始负责人。他在 Coursera 上关于各种机器学习主题的课程已被全球数百万学生完成。
尼古拉斯·吴博士和他的 Landing AI 团队构建了复杂的机器学习解决方案,例如用于检查制造质量的计算机视觉系统。通过这项工作,他们观察到以下特征是大多数机器学习机会的典型特征 3:
- 
大多数潜在的机器学习用例依赖于小于 10,000 个观察值的数据集。通常很难或不可能添加更多数据以减少噪声的影响,因此提高数据质量对于这些用例至关重要。
 - 
即使在非常大的数据集中,数据子集也会表现出小型数据集的行为。例如,谷歌的搜索引擎每天生成数十亿次的搜索,但其中 95%的搜索是基于每月出现次数少于 10 次的关键词组合(在美国)。15%的每日关键词组合之前从未被搜索过 4。
 - 
当数据集较小时,通常比收集更多数据更快、更容易识别和去除数据中的噪声。例如,如果一个包含 500 个观察值的数据集有 10%的误标记观察值,通常比收集一组新的观察值更容易提高现有数据的数据质量。
 - 
机器学习解决方案通常建立在预训练模型和包的基础上,需要的调整或修改很少。通过提高数据质量来提高模型性能,通常比更改模型参数或添加更多数据产生更好的结果。
 
尼古拉斯·吴博士发布了一篇关于 Landing AI 成果的比较,阐述了我们刚才讨论的最后一点。
如图 1**.1所示,Landing AI 为他们的客户提供了三个缺陷检测解决方案。在所有三个案例中,团队创建了一个基线模型,然后分别尝试使用以模型为中心和以数据为中心的方法来改进这个模型:

图 1.1 – 应用以数据为中心的机器学习 – Landing AI 的结果(来源:与 Andrew 关于 MLOps 的对话:从以模型为中心到以数据为中心的 AI)
在所有三个例子中,Landing AI 团队通过采用以数据为中心的方法而不是以模型为中心的方法,能够实现最佳结果。在三个例子中的一个中,以模型为中心的技术在基线模型性能上实现了微小的 0.04%的提升,而在另外两个例子中,没有实现任何改进。
相反,提高数据质量始终会导致基线模型得到改善,在三个案例中的两个案例中,改善相当显著。Landing AI 团队花费大约两周时间迭代改进训练数据集,以实现这些结果。
尼古拉斯·吴博士的建议很明确:无论你的数据集大小如何,如果你想构建相关且具有影响力的机器学习模型,你必须投入大量精力系统地设计你的输入数据。
从逻辑上讲,更好的数据导致更好的模型是有道理的,Landing AI 的结果也为这一点提供了实证证据。现在,让我们来看看为什么数据中心性是机器学习发展的未来。
机器学习系统的组件
机器学习系统由三个主要部分组成:
以数据为中心的方法认为,系统性的数据工程是下一个机器学习突破的关键,原因有以下两点:
- 
首先,一个模型的训练数据通常具有最大的改进潜力,因为它是任何模型的基础成分。
 - 
其次,机器学习系统的代码和基础设施组件比我们持续捕获高质量数据的方法和过程更先进。
 
在过去的几十年里,我们在机器学习算法、数据科学工具、计算和存储能力方面经历了巨大的演变,我们的数据科学解决方案的实施方法也通过诸如 机器学习 操作(MLOps)等实践而成熟。
开源工具如 Python 和 R 使得几乎任何有电脑的人都能相对便宜且容易地学习如何生成、调整和验证机器学习模型。这些工具的流行得益于大量可免费从公共库中安装的预构建包。这些包允许用户仅用几行代码就使用常见的机器学习算法。
在工具谱的另一端,低代码和无代码的 自动化机器学习(AutoML)工具允许那些具有有限或没有编码经验的非专家通过几次鼠标点击来使用机器学习技术。
云计算的发展为我们提供了弹性计算和存储能力,当需求增加时可以相对容易地进行扩展或缩减(注意可变成本!)。
换句话说,我们已经解决了围绕机器学习模型的技术约束中的许多问题。现在最大的机会在于提高输入数据的可用性、准确性、一致性、完整性、有效性和唯一性。
让我们更深入地看看原因。
数据是基础成分
想象一下,一个厨师想要创建一家世界知名的米其林星级餐厅。这位厨师花费了很长时间学习如何将风味和质地结合成令人愉悦的食谱,让顾客满意。经过多年的实践和磨练技艺,他们准备开设自己的餐厅。他们知道如何让餐厅成功。
在餐厅的前端,他们必须有一个布置得体的餐厅,舒适的家具,以让客人享受彼此的陪伴。为了服务客人,他们需要优秀的服务员,他们会关注顾客的每一个需求,确保订单被接收,酒杯被填满,餐桌保持干净整洁。
但不仅如此。一家成功的餐厅还必须拥有一个配备齐全的商业厨房,能够快速且一致地制作出许多菜肴,无论同时处理多少订单。当然,还有食物。厨师创造了一份充满精心制作的食谱的菜单,将为他们的客人提供独特而令人愉悦的风味体验。他们一切准备就绪,即将开设即将获奖的餐厅。
然而,在首演之夜,出现了一个问题。一些蔬菜在储藏室里长出了霉斑,它们必须被扔掉。一些香草和香料已经缺货,难以轻易获得。最后,菜单上最受欢迎的菜肴含有红卷心菜,但供应商只送来了绿卷心菜。因此,这些菜肴并不是令人愉悦的风味感觉,而是平淡无奇。厨师已经建立了一个完美的运营和一份精彩的菜单,但过于忽略了最重要的、最难控制的元素:食材。
食材是在餐厅外生产的,由几个不同的供应商运送。如果供应链的某个或某些部分未能交付,那么最终输出将受到影响,无论厨师多么有才华。
这家餐厅的故事说明了为什么采用更系统的方法来构建高质量数据集是构建更好模型的关键。
就像超级明星厨师需要最好的食材来制作出卓越的菜肴一样,数据科学家往往因为输入数据不够好或不够容易获取而无法构建高度有影响力的模型。我们不是有腐烂的蔬菜,而是有误标记的观察结果。我们不是缺货的食材,而是缺失的值。我们不是错误的卷心菜种类,而是具有有限预测能力的通用或高级标签。我们不是食品供应商的网络,而是大量数据源和技术平台,这些平台很少专门为机器学习而构建。
这种数据收集不成熟的部分原因与机器学习作为计算机科学领域其他学科相对能力的成熟度有关。对于只有表面了解机器学习的人来说,他们通常会将机器学习系统视为与传统软件应用相同的方式。
然而,与传统的软件不同,机器学习系统产生的是可变输出,这些输出取决于一组不断变化的数据输入。在机器学习中,数据是代码的一部分。这很重要,因为数据是影响最终模型输出的最大潜在因素。输入特征和观察的广度、深度和准确性是构建有影响力和可靠模型的基础。如果数据集不能代表你试图预测的现实世界人口或场景,那么该模型可能没有用。
同时,数据集将决定模型的大部分潜在偏差;也就是说,模型更有可能产生结果,错误地偏向某一群体而不是另一群体。简而言之,输入数据是 ML 模型中变化最大的来源,我们希望利用这种变化来发挥优势,而不是让它成为风险或障碍。
随着我们从数据转向算法,再到系统基础设施,我们希望 ML 系统越来越标准化和统一。采用以数据为中心的方法,我们希望在数据中保持大量的正确类型的可变性(而非噪声!)同时保持我们的 ML 算法和整体运营基础设施的稳健和稳定。这样,我们可以通过提高数据质量来迭代提高模型精度,同时保持其他一切稳定。
图 1.2 提供了与 ML 系统三个组成部分(数据、代码和基础设施)相关的各个方面的概述:

图 1.2 – ML 系统的组成部分
在以数据为中心的方法下,高质量的数据是稳健 ML 系统的基石。提高 ML 模型的最大机会通常在于输入数据,而不是代码。
虽然在模型参数的更改上关注数据质量很有道理,但数据科学家往往更关注后者,因为这更容易在短期内实施。在采用传统的以模型为中心的方法后,通常可以在非常短的时间内测试多个模型和超参数,但增加建模数据集中的信号并减少噪声似乎是一项复杂且耗时的练习。
在一定程度上,这是因为系统性地改进数据收集通常涉及上游流程的改变以及组织中各种利益相关者的参与。这通常是数据科学家无法单独完成的,需要整个组织认识到数据科学的价值和潜力,投入适当的时间和资源以改善数据收集。不幸的是,大多数组织在基于不良数据构建和实施次优模型上浪费的资源比收集更好数据的资源要多。
在接下来的章节中,我们将了解到,精心设计的数据中心方法可以克服这一挑战,并且通常在组织内部解锁许多新的 ML 机会。这是因为数据中心 ML 要求所有参与数据管道的人都更全面地思考组织数据的结构和目的。
为了进一步理解和欣赏以数据为中心的方法在模型开发中的潜力,让我们将数据中心性与更占主导地位的模式中心方法进行比较。
数据中心与模式中心 ML
到目前为止,我们已经确定数据中心化是关于系统地构建机器学习模型所使用的数据。传统的、更普遍的以模型为中心的机器学习方法认为,优化模型本身是提高性能的关键。
如 图 1**.3 所示,以模型为中心的方法的核心目标是改进模型背后的代码。在以数据为中心的方法下,目标是找到在改进数据质量方面的更大提升空间:

图 1.3 – 通过以模型为中心和以数据为中心的工作流程构建机器学习解决方案
机器学习模型开发传统上主要关注通过优化代码来提高模型性能。在以数据为中心的方法下,重点转向通过迭代改进数据质量来实现更大的性能提升。需要注意的是,以数据为中心的方法建立在支撑以模型为中心的机器学习原则和技术之上,而不是取代它们。两种方法都将模型和数据视为机器学习解决方案的关键组成部分。如果其中任何一个配置不当、存在错误、有偏见或应用不当,解决方案就会失败。
在以数据为中心的方法下,模型配置是一个重要步骤,而在短期内,通过优化代码来寻求模型性能的增量提升无疑是更快的。然而,正如我们讨论的那样,如果你没有合适的原料,改变配方所能带来的提升是有限的。换句话说,两种方法之间的区别在于我们在迭代改进模型性能时,将重点放在哪里和投入多少努力。
如 图 1**.4 所示,以模型为中心的方法将数据视为固定的输入,并专注于模型选择、参数调整、特征工程以及添加更多数据作为提高模型性能的主要方式。以数据为中心的方法认为模型相对静态,并主要关注通过提高数据质量来改善性能。
采用以模型为中心的方法,我们试图收集尽可能多的数据,以消除数据中的任何异常值并减少偏差——数据集越大越好。然后,我们设计我们的模型(们)尽可能具有预测性,同时避免过拟合。
这与以数据为中心的方法形成对比,后者在模型选择和调整的基础上,在数据收集和标注方面做得更好。通过异常值检测、程序化标注、更系统的特征工程和合成数据创建(这些技术将在后续章节中深入解释),数据质量得到进一步改善:

图 1.4 – 比较以模型为中心和以数据为中心的机器学习方法
机器学习模型改进来自两个方面:改进代码和改进数据。虽然数据收集和工程过程听起来像是数据工程师的工作,但它们实际上应该是数据科学家工具箱的关键部分。
让我们来看看在数据中心化方法下,数据科学家、数据工程师和其他利益相关者需要做什么。
数据中心化是一项团队运动
虽然关注数据质量而不是模型参数的变化很有意义,但数据科学家往往更关注后者,因为这在短期内更容易实施。在传统的以模型为中心的方法之后,通常可以在非常短的时间内测试多个模型和超参数,但增加建模数据集中的信号并减少噪声似乎是一项复杂且耗时的工作,难以由小团队轻松处理。数据中心化的机器学习需要整个组织投入更多的努力,而以模型为中心的方法在很大程度上依赖于数据科学家的技能和工具来提高模型性能。
数据中心化是一项团队运动。数据中心化要求数据科学家和参与机器学习开发的其他人员掌握一套新的数据质量特定技能。这些新数据中心化技能和技术中最重要的,就是我们将在本书中向您传授的。
数据捕获和标注过程必须以数据科学为出发点,并由至少对机器学习开发有基础理解的专业人士执行。数据工程过程和 ETL 层必须结构化,以识别数据质量问题,并允许迭代改进机器学习输入数据。所有这些都需要数据科学家、数据收集者、领域专家、数据工程师、商业领袖以及将数据转化为洞察力的人员之间的持续协作。
为了说明这一点,图 1**.5比较了两种方法的数据到模型过程。根据您组织的规模和目的,可能涉及多种角色来交付机器学习解决方案,例如数据架构师、机器学习工程师、数据标注员、分析师、模型验证员、决策者、项目经理和产品所有者。
然而,在我们的简化图图 1**.5中,涉及三种类型的角色——数据科学家、数据工程师和领域专家:

图 1.5 – 数据中心化与模型中心化的角色和责任
数据管道顶部的利益相关者必须积极参与过程,以便组织在机器学习目的的数据收集和工程方面做得好。简而言之,数据中心化需要大量的团队合作。
在传统的以模型为中心的方法下,数据创建通常从数据收集过程开始,这可能包括自动化、手动或两者的混合。例如,客户在网页上输入详细信息,放射科医生进行 CT 扫描,或呼叫中心操作员接听录音电话。在这个阶段,数据已经为了其主要运营目的而被捕获,但通过数据工程师的工作,这些信息也可以被转换成分析数据集。典型的过程需要数据工程师从数据库、数据湖、数据仓库或等效系统中提取、转换和标准化数据。
一旦数据科学家掌握了数据,它通常会经过几个步骤以确保准确性、一致性、有效性和完整性得到保持。换句话说,数据应该准备好使用;然而,任何数据科学家都知道这很少是情况。
数据科学中的一个常见经验法则是,构建一个新的机器学习模型所需的时间的 80%用于寻找、清理和准备用于建模的数据,而只有 20%用于分析和模型构建。传统上,这被视为一个问题,因为数据科学家被支付工资来处理数据以构建模型和执行分析,而不是花大部分时间准备数据。
采用以数据为中心的方法,数据准备成为模型构建过程中的最重要部分。我们不是问“我们如何最小化数据准备所花费的时间?”,而是问“我们如何系统地优化数据收集和准备?”问题不在于数据科学家在学习和增强他们的数据集上花费了大量的时间。问题在于机器学习开发与其他上游数据活动之间的连接不足,这允许数据科学家、工程师和领域专家更快、更准确地共同创造结果。
从本质上讲,数据中心性是关于建立系统化执行这些工作的流程、工具和技术。领域专家积极参与机器学习开发的关键部分,包括识别异常值、验证数据标签和模型预测,以及开发应在数据中捕获的新特征和属性。
在以数据为中心的方法下,数据工程师和数据科学家也承担了额外的责任。数据工程师的责任必须从构建和维护数据管道扩展到更直接地参与开发和维护特定机器学习解决方案的高质量特征和标签。反过来,这要求数据工程师和数据科学家理解彼此的角色,并朝着共同的目标进行合作。
在下一节中,我们将通过应用实例来说明,以数据为中心的方法可以对机器学习机会产生的影响。
质量数据在机器学习中的重要性
到目前为止,我们已经定义了数据中心的机器学习是什么,以及它与传统的模型中心方法相比如何。在本节中,我们将探讨在实践中良好的数据是什么样的。
从数据中心的视角来看,良好的数据如下 5:
- 
一致捕捉:独立变量(x)和依赖变量(y)被明确标记
 - 
充满信号且无噪声:输入数据覆盖了尽可能少的观察范围内的重要观察和事件
 - 
针对商业问题设计:数据是专门设计和收集的,用于解决使用机器学习(ML)的商业问题,而不是用现有数据解决任何问题
 - 
及时且相关:独立和依赖变量提供了对当前趋势的准确表示(无数据或概念漂移)
 
初看起来,这种系统性的数据收集似乎既昂贵又耗时。然而,根据我们的经验,高度审慎的数据收集通常是获得所需结果的机器学习的基础要求。
为了理解数据中心的必要性和潜力,让我们看看一些应用实例,了解数据质量和特征系统工程的系统性如何产生重大差异。
使用自然语言处理识别高价值法律案件
我们第一个关于数据质量关键重要性的例子来自乔纳斯和曼莫汉在一家大型澳大利亚法律服务公司构建的机器学习解决方案。
相比于银行、保险、公用事业和电信等类似服务行业,机器学习在法律服务中是一个新兴学科。这是由于法律服务中数据的性质和复杂性,以及在使用机器学习时的风险和伦理问题。
尽管法律服务行业数据极其丰富,但数据通常是通过人工收集的,以文本格式存储,并且高度依赖于具体法律案件的情况。这种文本数据可能以各种格式出现,例如医疗专业人士的信件、法律合同、交易对手通讯、律师与客户之间的电子邮件、案件笔记和音频录音。
此外,法律服务行业是一个高风险环境,其中任何一方犯的错误或遗漏都可能完全赢得或输掉案件。因此,法律专业人士往往花费大量时间和精力审查详细文件,并跟踪法律过程中的关键日期和步骤。魔鬼藏在细节中!
法律服务公司是一家无胜无费原告律师事务所,代表那些在身体或财务上受到伤害或被不公正对待的人。公司代表个人或团体对抗更强大的交易对手,如保险公司、疏忽的医疗或医生以及行为不端的公司。只有在客户获胜的情况下,客户才支付费用——否则,公司承担损失。
到 2022 年,业务部门发现了一个机会,可以利用数据科学来寻找罕见但高价值的案例,然后可以通过专业律师快速处理。越早识别这些高价值案例,效果越好。因此,目标是让它们在潜在客户的第一次面试中就被识别出来。
初始项目设计遵循了传统的以模型为中心的方法。数据科学团队收集了来自潜在客户访谈的两年案例笔记,并为后来证明是高价值案例的案例创建了标志(因变量,y)。团队还使用了主题建模来创建要包含在最终输入数据集中的新特征。主题建模是一种无监督机器学习技术,用于检测可以分组为主题的跨各种文档或文本片段的模式。然后,这些主题被直接输入到初始模型中,并作为解释模型预测的工具。
初始模型证明具有一定的预测能力,但团队面临了一些只能通过以数据为中心的方法才能解决的问题:
- 
每年只开放不到一千个高价值案例,因此即使在过采样之后,这也是一个小数据问题。
 - 
主要预测因素来自案例笔记,这些笔记是半结构化或非结构化的格式,通常是自由文本。尽管案例笔记遵循某些标准,但每个记录者都使用了他们独特的词汇、缩写和格式,这使得创建标准化的建模数据集变得困难。
 - 
由于输入数据主要是自由文本格式,一些非常重要的信息对模型来说过于模糊,无法捕捉。例如,法律案件是否涉及多个人受伤这一点很重要,因为这可能会完全改变案件策略。有时,每个受伤方都会被明确指出,有时则只是被称作他们。
 - 
由于某些细节要么是法律专业人士假设的知识,要么是阅读整个文档的人显而易见的内容,因此这些细节被省略在案例笔记中。不幸的是,这对学习算法并没有帮助。
 
团队决定采取以数据为中心的方法,并组建了一个跨职能项目团队,包括一名技术高超的律师、一名数据科学家、一名数据工程师、一名运营经理和一名呼叫中心专家。团队中的每个人都精通整个流程的一部分,他们共同为客户体验、法律、数据和运营流程提供了广泛的深度和广度。
与通过特征工程提高模型精度不同,团队通过设计一套高度预测案件是否具有高价值的客户问题,完全改变了数据捕获方式。新问题的标准如下:
- 
它必须提供关于案件是否具有高价值的非常具体的细节
 - 
格式必须易于人类和算法理解
 - 
必须让潜在客户容易回答新问题,并且呼叫中心操作员能够捕捉到信息
 - 
必须容易创建一个围绕捕获数据的分级流程,以便呼叫中心操作员可以立即采取正确的行动
 
之前提到的标准突出了在开发机器学习解决方案时涉及广泛领域专家的重要性。跨职能团队中的每个人都具有特定的知识,这些知识有助于整体解决方案的细节。
团队确定了一些关键问题,这些问题将高度预测一个案例是否为高价值案例。这些问题需要非常具体,以至于只能用是、否或数量来回答。例如,与其在自由文本字段中寻找单词他们,呼叫中心操作员可以简单地问有多少人参与了事件?并仅记录一个数字答案:

图 1.6 – 数据中心改进前后的假设案例笔记
回答了这些问题后,每个潜在案例都可以根据其成为高价值案例的高、中、低概率进行分组。然后,团队建立了一个简单的流程,允许呼叫中心操作员将高概率案例直接引导到由专业律师处理的高速通道流程。其他案例将继续使用机器学习模型进行监控,以检测可能将它们推入高价值领域的新的事实。
最终解决方案之所以成功,是因为它帮助更快、更准确地识别高价值病例,但采取以数据为中心的方法的好处远不止于此。对改进数据收集的关注不仅为机器学习创造了更好的数据,还促进了来自整个业务不同部门之间的一种不同类型的合作,最终导致了更明确的过程定义和对客户旅程关键时刻优化的更强关注。
预测紧急呼叫中的心脏骤停
另一个例子来自在丹麦哥本哈根的紧急医疗调度中心(EMDC)进行的实验研究 6。
由医学研究员斯蒂格·布洛姆伯格领导的一个团队致力于研究是否可以使用机器学习解决方案通过监听 EMDC 的电话来识别院外心脏骤停。
该团队使用 2014 年生成的紧急呼叫录音训练和测试了一个机器学习模型,主要目标是协助医疗调度员在早期检测心脏骤停呼叫。
研究发现,根据模型灵敏度测量的结果,机器学习解决方案在识别心脏骤停病例方面更快、更准确。然而,研究人员也发现,在采用以模型为中心的方法时存在以下局限性:
- 
由于救护车医护人员和调度员之间没有结构化的反馈能力,系统中缺乏学习。例如,通过向呼叫者提出定制和更结构化的问题,如“他看起来苍白吗?”或“他能 动吗?”,很可能会提高人类和机器对心脏骤停的预测。
 - 
非母语说话者的语言障碍影响了模型性能。机器学习解决方案在丹麦语说话者中表现最佳,在识别外国口音的电话中的心脏骤停方面不如可能说多种语言的人类调度员。
 - 
尽管该解决方案的敏感性(检测真实阳性)高于人类调度员,但不到五分之一的通知是真实阳性。这导致调度员中出现了高程度的警报疲劳,他们最终承担着根据机器学习建议行动或不行动的风险。
 
这个案例研究是机器学习用例的一个典型例子,它需要以数据为中心的方法来实现最佳结果,同时适当管理风险和伦理问题。
首先,由于底层问题的性质和复杂性,一个用于分类心脏骤停电话的机器学习解决方案将始终基于少量数据。在这种情况下,仅仅增加数据并不一定能提高模型性能。
在哥本哈根大区,每年大约有 1,000 起心脏骤停报告,而该地区的人口约为 180 万,即使是几年的通话录音也无法构成一个大数据集。一旦考虑到数据中的许多子集,如外语说话者和非母语口音的人,数据变得更加碎片化。
与生产错误预测(尤其是假阴性)相关的风险和伦理问题,尤其是在生死攸关的情况下,意味着数据标签必须经过仔细的整理,以确保任何偏差都降低到可接受的最低限度。这需要通过审查数据质量和增强模型功能来进行的迭代过程。
基于简短的电话对话对心脏骤停案例进行分类是一项复杂的任务。它需要专业知识,以及调度员和医护人员 alike 的培训和经验。为机器学习目的构建高质量的自然语言数据集主要关于减少对所寻找信号的解读的模糊性。这反过来又要求组织在设计中涉及专业知识专家,以确定建模过程中什么是重要的。您将在第四章,“数据标注是一个协作过程”,了解如何做到这一点。
在提问和回答的方式上具体明确,可以为人类代理(在这种情况下,是调度员)以及 ML 模型提供清晰性。这个例子突出了以数据为中心不仅关乎为 ML 模型收集更好的数据。这是一个黄金机会,可以更谨慎地定义和改进组织内部人们的工作和协作方式。
你刚刚阅读的两个案例研究突出了精心收集和整理数据集以保持高准确度、有效性和上下文相关性的重要性。在某些情况下,数据质量可能关乎生死!
正如你将在第二章**从模型中心到数据中心 – ML 的演变中了解到的那样,只要我们能管理好与数据质量相关的风险,ML 在法律服务和医疗保健等高风险领域有很大的潜力成为一款出色的工具。
现在我们已经讨论了数据为中心的 ML 的不同方面,让我们总结一下本章我们学到了什么。
摘要
在本章中,我们讨论了以数据为中心的机器学习(ML)的基本原理及其起源。我们还学习了以数据为中心与以模型为中心的区别,包括在典型组织中使用 ML 的关键利益相关者的角色和责任。到这一点,你应该对以数据为中心的 ML 及其与传统模型中心方法相比的额外潜力有一个扎实的理解。希望这能鼓励你在下一个项目中使用以数据为中心的 ML。
在下一章中,我们将探讨为什么 ML 开发至今一直以模型为中心,并进一步探讨为什么数据中心性是 AI 演变下一阶段的关键。
参考文献
- 
datacentricai.org/,观看日期:2022 年 7 月 10 日 - 
www.andrewng.org/和www.coursera.org/instructor/andrewng,观看日期:2022 年 7 月 6 日 - 
www.youtube.com/watch?v=06-AZXmwHjo,观看日期:2022 年 8 月 2 日 - 
ahrefs.com/blog/long-tail-keywords/,观看日期:2022 年 8 月 2 日 - 
来自 与 Andrew 关于 MLOps 的对话 – 从模型中心到数据中心 AI:
www.youtube.com/watch?v=06-AZXmwHjo,观看日期:2022 年 8 月 2 日 - 
Zicari 等人:在医疗保健中评估可信 AI:作为识别紧急呼叫中心心脏骤停的支持工具的机器学习的最佳实践。Frontiers in Human Dynamics (2021)
 
第二章:从模型为中心到数据为中心——机器学习的演变
到现在为止,你可能已经在想:如果数据中心性对于人工智能和机器学习的进一步发展至关重要,那么为什么模型中心性是主导方法?
这是一个非常相关的问题,我们将在本章中回答。为了了解转向数据中心方法需要什么,我们必须了解导致模型中心性成为主导方法的力量,以及如何克服它们。
我们将首先探讨为什么人工智能和机器学习的演变主要遵循以模型为中心的方法,然后深入探讨通过数据中心性可以解锁的巨大机会。
在本章中,我们将挑战机器学习需要大数据集以及更多数据总是更好的观念。当我们从“更大数据”转向“更好数据”时,会出现一系列小数据机器学习用例的长尾。
到本章结束时,你将清楚地了解机器学习至今的发展历程,并知道如何在此基础上构建并使用机器学习取得更好的成果。
在本章中,我们将涵盖以下主要主题:
- 
探讨为什么机器学习的发展最终变成了以模型为中心
 - 
小数据机器学习的机会
 - 
为什么我们比以往任何时候都需要以数据为中心的机器学习
 
探讨为什么机器学习的发展最终变成了以模型为中心
为了真正理解为什么以数据为中心的方法是释放机器学习(ML)全部潜力的关键,我们需要上一堂简短的历史课。
数据科学和机器学习领域自从最早尝试让电子计算机表现出“智能”行为以来,已经取得了显著的进步。今天大多数智能手机所执行的“智能”任务,在 21 世纪初几乎无法想象。此外,我们每天产生的数据量比从人类文明开始到 21 世纪所创造的数据量还要多——而且我们以每年约 23%的增长率在这样做。
尽管在技术和数据量方面取得了这些令人难以置信的进步,但数据科学的一些元素非常古老。统计学和数据分析已经使用了几个世纪,而今天机器学习模型的数学组成部分大多是在数字计算机出现之前开发的。
就我们的目的而言,机器学习和人工智能的历史始于第二次世界大战期间首次电子计算机的引入。
1940 年代至 1970 年代——早期阶段
历史学家和前美国陆军军官 Adrian R. Lewis 在他的书《美国的战争文化》中写道:“战争创造了技术巨大进步的条件……如果没有战争,人们不会在几小时内穿越海洋,在太空中旅行,或者用微波炉爆米花 2。”
这确实是在第二次世界大战期间,以及随后的几十年里。计算机科学、密码学和硬件技术取得了巨大的飞跃,因为世界各地的战斗国家在全球各个战线上相互竞争以获得主导地位。
在 20 世纪 40 年代和 50 年代,编译器、半导体晶体管、集成电路和计算机芯片等创新使得数字电子计算机能够执行更复杂的过程(在此之前,计算机主要是指那些被雇佣来执行复杂计算的计算天赋异禀的人类的工作职位 3)。这反过来又导致了今天机器学习模型的一些早期创新。
在 1943 年,美国科学家沃尔特·皮茨和沃伦·麦克库洛赫创造了世界上第一个神经网络计算模型。这为人工智能的其他创新奠定了基础,包括 1952 年亚瑟·塞缪尔的自改进国际象棋程序和由美国海军和 IBM 资助的 1958 年的感知器,这是一个用于图像分类的神经网络。
在 1950 年,英国数学家和计算机科学家艾伦·图灵提出了用于评估计算机执行与人类相当智能操作的图灵测试。这个测试常被用作衡量计算机智能的基准,并对人工智能的一般哲学产生了深远的影响。
机器学习研究在 20 世纪 60 年代继续扩展,其中最近邻算法的发展是最显著的进步之一。斯坦福研究人员托马斯·科弗和彼得·哈特的工作为 k-最近邻算法作为一种强大的统计分类方法的出现奠定了基础 4。
在 1965 年,Fairchild 半导体和 Intel 的联合创始人戈登·摩尔提出了一个观点,即计算机的处理能力和硬盘存储容量每两年会翻一番,这也被称为摩尔定律5。尽管摩尔定律被证明是相当准确的,但要达到一个可以以合理速度和成本处理大量数据的地步,还需要许多十年。
为了更清楚地说明问题,IBM 在 1970 年的主要产品是 System/370 Model 145,它有 500 KB 的 RAM 和 233 MB 的硬盘空间 6。这台计算机占据了整整一个房间,成本为 705,775 美元到 1,783,000 美元 7,约合今天的 5 到 13 百万美元。在撰写本文时,最新的 iPhone 14 的 RAM 是 System/370 Model 145 的 12,000 倍,硬盘空间高达 2,200 倍,具体取决于 iPhone 的配置 8。

图 2.1 – IBM System/370 Model 145. 这张图片中的所有东西都是计算机操作的一部分(除了墙上的时钟)。来源:Jean Weber/INRA, DIST
20 世纪 70 年代的大部分时间被广泛认为是“人工智能寒冬”的时期——在这个时期,人工智能领域几乎没有突破性的研究或发展。商业界对人工智能的短期潜力看得很低,主要是因为计算机处理能力和数据存储容量尚未充分发展,且成本高昂。
1980 年代到 1990 年代——个人电脑和互联网的兴起
1982 年,IBM 推出了第一台个人电脑(IBM PC),这引发了工作场所和人们家庭中计算机技术的革命。它还导致了苹果、微软、惠普、英特尔和其他许多硬件和软件企业如流星般崛起,这些企业乘上了技术创新的浪潮。
处理和信息的数字化能力的提高也加剧了企业界对使用存储数据进行分析目的的兴趣。关系型数据库成为主流,以牺牲网络和层次数据库模型为代价。
SQL 查询语言在 20 世纪 70 年代开发;在整个 80 年代,它成为主要的数据库语言,并在 1986 年获得了 ISO 和 ANSI 认证。
数字信息的爆炸性增长需要新的技术来从统计角度理解数据。斯坦福大学的研究人员在 1984 年开发了第一个生成分类和回归树的软件,而像 WordNet 这样的词汇数据库创新为文本分析和自然语言处理奠定了早期基础。
个人电脑继续在 20 世纪 90 年代取代打字机和大型机,这促使万维网在 1991 年形成。网站、博客、互联网论坛、电子邮件、即时消息和 VoIP 电话又引发了数据量、种类和速度的另一次爆炸性增长。
因此,新的方法逐渐发展起来,用于组织更复杂和不同类型的数据。例如,AdaBoost 和梯度提升机等梯度提升算法在 90 年代末由斯坦福研究人员开发,为搜索引擎对各种信息进行排序铺平了道路。
互联网的兴起也为那些能够组织其上信息的人创造了巨大的商业机会。在此期间,亚马逊、阿里巴巴、雅虎和谷歌等公司成立,争夺电子商务和网页搜索的主导地位。这些公司看到了计算机科学、人工智能和机器学习的巨大潜力,并大量投资于开发算法来管理他们庞大的信息库。
2000 年代——科技巨头的崛起
机器学习研究在 2000 年代全速前进,无论是在大学还是在企业的研发部门(R&D)。计算机处理能力终于达到了大多数公司和研究人员可以进行大规模数据处理的水平。
当互联网搜索引擎提供商忙于开发算法来排序和分类在线发布的不断增长的信息时,大学研究人员正在创造新的工具和技术,这些工具和技术将推动机器学习的演变。
2003 年,R 基金会成立,旨在开发和支持开源机器学习工具和编程语言 R。作为统计计算和图形的免费开源编程语言,R 显著降低了研究人员在工作中使用统计编程的门槛,以及数据爱好者练习和学习机器学习技术的门槛。
随机森林算法于 2001 年推出,并在 2006 年由加州大学伯克利分校的统计学家和机器学习先驱 Leo Breiman 以及犹他州立大学的 Adele Cutler 获得专利。
斯坦福大学教授李飞飞在 2008 年推出了 ImageNet 项目,作为一个免费和开放的图像数据库,用于训练对象识别模型。该数据库的创建是为了提供一个高质量、标准化的数据集,用于训练和基准测试对象分类模型。截至撰写本文时,ImageNet 包含超过 1400 万张标记的图像,按照 WordNet 层次结构组织。
这个时期也见证了基于网络的商业模式如雨后春笋般崛起,成为创造互联网霸权的一种方式。LinkedIn、Facebook、Twitter 和 YouTube 等社交媒体平台在这个时期推出,并利用机器学习算法组织用户创建的信息和内容,成为了跨国的科技巨头。
随着数据量的激增,对廉价且灵活的数据存储的需求也随之增加。AWS、Dropbox 和 Google Drive 等云计算和存储服务应运而生,而大学与谷歌和 IBM 合作建立了服务器农场,用于数据密集型研究。日益增多的是,处理能力的可用性现在基于用户的成本效益而非技术限制。
2010 年至今 – 大数据推动人工智能创新
基于网络的商业模式继续为互联网和机器学习的发展指明方向。搜索引擎、社交媒体平台以及软件和硬件提供商在围绕人工智能的 R&D 活动上投入了大量资金。例如,谷歌大脑研究团队成立于 2011 年,旨在提供关于大数据的前沿人工智能研究。
新的基于网络的公司正在颠覆出租车、酒店、旅游服务、支付、餐饮和食品服务、媒体、音乐、银行、消费零售和教育等行业,利用数字平台、机器学习和大量消费者数据作为其强大的竞争优势。
传统研究机构与大型科技公司紧密合作,在音频和图像识别、自然语言理解、异常检测、合成数据生成等领域,深度学习技术取得了重大突破。
到 2017 年,在年度 ImageNet 挑战赛中,四分之三的参赛团队达到了 95%以上的准确率,证明了图像识别算法现在已经非常先进。
在人工智能的黄金十年中,也开发了用于生成新数据的有力算法。2014 年,谷歌大脑团队的一名研究人员伊恩·古德费洛发明了生成对抗网络(GAN),这是一种通过配对两个模型相互竞争来工作的神经网络 15。另一种生成模型框架,生成预训练转换器(GPT),在 2018 年由 OpenAI 研究实验室推出。
在生成模型运行的情况下,现在可以产生类似人类的输出,如文本片段、图像、艺术品、音乐和深度伪造——对某人声音和举止的音频和视频模仿。
随着“大数据”、“机器学习”和“人工智能”成为日常用语的一部分,对分析师、数据科学家、数据工程师和其他数据专业人士的需求大幅增加。2011 年,数据科学家的职位空缺年增长率为 1500%16。对数据和机器学习潜力的巨大热情导致分析先驱汤姆·达文波特和 DJ 帕蒂尔将数据科学称为 21 世纪的最具魅力的工作17。
全世界成千上万的数据爱好者都在寻找学习最新机器学习和数据挖掘技术的地方。Kaggle 和 Coursera 等平台允许数百万用户通过公开在线课程学习,参加机器学习竞赛,访问高质量数据集,并分享知识。
在工具方面,R、Python 或 SQL 上运行的免费可下载软件程序和包的激增,使得以低成本访问高级数据科学技术变得相对容易:

图 2.2 – 从 1940 年到现在的机器学习历史
在 2010 年至 2020 年人工智能的黄金十年中,随着信息技术、数据和人工智能的进步,机器学习模型架构已经显著成熟。此时,大多数创造更好模型的机会在于提高数据质量。
以模型为中心是逻辑上的进化结果
数据科学历史的最后八十年遵循了一条逻辑的进化路径,导致以模型为中心成为机器学习的主要方法。
机器学习背后的思想和数学概念在技术成熟到足以与之匹配之前就已经被构想出来了。在 20 世纪 90 年代之前,计算机的运算能力不足以让大学研究人员显著地发展机器学习领域。这些技术限制还意味着,在这一时期,私营企业为了商业利益进行的有限研究。
在 20 世纪 90 年代初互联网时代到来之际,硬件和软件解决方案开始变得足够先进,足以消除这些古老的限制。互联网还引发了一场信息革命,极大地增加了可用数据的数量和种类。突然之间,机器学习不仅在经济上可行,而且成为亚马逊、雅虎和谷歌等科技公司背后的驱动力。随着比以往任何时候都更多的数字信息可用,我们需要推进我们解释和建模各种数据的方式。换句话说,机器学习研究首先需要以模型为中心的关注点。
在 2000 年代,一种新型的商业模式开始主导我们的生活。基于网络的数字业务,如社交媒体平台、搜索引擎、软件创造者和在线市场,为用户提供了创建和互动内容与产品的平台。通过将机器学习应用于大量用户生成数据,这些业务在过程中观察并优化了每一次互动。
这些“以 AI 为先”的大型科技公司受数据质量或数量的限制较小。它们的限制主要在于快速且经济的计算和存储能力,以及机器学习技术的复杂性。通过内部研究、与大学的合作以及战略投资于有希望的 AI 技术,大型科技公司在过去二十年里能够推动机器学习发展的议程。这些公司最需要的是以模型为中心的方法。
自 1995 年中叶以来,由于以模型为中心的研究,我们现在拥有了能够组织全世界信息的算法,识别人群中的个体,在开放交通中驾驶车辆,识别和生成声音、语音和图像,以及更多。由于这一创新时期,我们根据输入数据构建准确模型的能力非常先进。
随着数据成为更普遍的资产,突然出现了对更多数据科学家和其他数据专业人士的强烈需求。如今,通过在线学习平台、大学课程和机器学习竞赛,学习机会并不缺乏,但它们通常有一个共同点:初始输入数据集是预定义的。
在固定数据集上教授机器学习是有意义的。没有可复制的输出,很难验证学习者是否掌握了特定的技术,或者将不同的模型相互基准测试。然而,自然的结果是,学习以通过模型为中心的任务为中心,例如模型选择、超参数调整、特征工程和对现有数据集的其他增强。
经验丰富的数据科学家必须掌握以模型为中心的技能,但它们只是以数据为中心范式的基础。这是因为机器学习的进步分为四个部分:
- 
提高计算机能力
 - 
改进算法
 - 
提高数据
 - 
提高测量
 
到目前为止,我们在第 1 点和第 2 点上取得了巨大的进步,到了它们在很大程度上已成为大多数机器学习用例的解决方案。现在,大部分机会在于我们改进数据和质量的方法。当我们改进我们的数据时,我们可以构建更好的模型,但我们还解锁了那些通常因为只有几千行(或更少)数据而无法触及的机器学习用例的长尾。
解锁小数据机器学习的机会
被作者艾米·韦伯 18 称为“九大巨头”的科技企业群体是利用大数据和人工智能建立全球主导地位的消费互联网公司的典型例子。亚马逊、苹果、阿里巴巴、百度、Meta、谷歌、IBM、微软和腾讯在数字时代占据主导地位,因为他们利用了大量的用户数据来驱动他们的 AI 系统。
作为基于网络的“AI 优先”企业,他们以前所未有的规模积累了客户,因为用户乐于共同创造并分享他们的数据,只要这对他们有净收益。对于九大巨头来说,获取足够的建模数据很少是问题,投资于最先进的机器学习能力是一个良性循环,这有助于增强市场主导地位。
对于大多数其他组织和机器学习用例来说,这种规模是无法实现的。正如我们在第一章中探讨的,探索数据中心的机器学习,机器学习机会的长尾并不提供在大量训练数据上构建模型的选择,因为以下挑战:
- 
缺乏训练数据观察:长尾中的数据集较小——通常只有几千行或更少。此外,大多数组织在非数字化的物理世界中捕获数据,这使得捕捉和微调某些数据点变得更加困难。
 - 
脏数据:与基于网络的“AI 优先”企业不同,大多数组织通过大量不同的来源生成数据,如内部(但外部开发)IT 系统、第三方平台以及员工或客户的手动收集。这创造了一个复杂的数据来源拼凑,伴随着各种数据质量挑战。
 - 
高风险领域的偏差和不公平风险:在医疗保健、法律服务、教育、公共安全和犯罪预防等高风险领域,数据质量差可能导致对个人或弱势群体产生灾难性的影响。例如,根据医学图像预测一个人是否患有癌症是一项高风险活动——根据你的 YouTube 观看历史推荐下一个视频则不是。
 - 
模型复杂性和缺乏规模经济:尽管在长尾中可以找到大量价值,但单个机器学习项目通常需要大量定制来处理不同的场景。定制成本高昂,因为它会创建许多模型、数据集和流程的积累,这些必须在模型实施前后维护。
 - 
在数据和模型开发中需要领域专业知识:小数据集、高风险和更复杂场景的结合使得在数据收集、标注和验证、模型开发和测试过程中,没有领域专家的参与很难构建机器学习模型。
 
重要的是要注意,许多公司有机会通过小数据机器学习解锁重大价值。例如,只有少数组织会有价值 5000 万美元或以上的单个机器学习项目,但许多组织会有 50 个潜在的价值 100 万美元的机器学习机会。在实践中,这意味着如果我们想让小型项目变得可行和具有财务可行性,我们必须从我们的原材料中获得最大价值。
安德鲁·吴博士,Landing AI 的 CEO 和创始人,将这些挑战总结如下 19:
“在消费者软件互联网中,我们可以训练几个机器学习模型来服务十亿用户。在制造业,可能有 1 万家制造商正在构建 1 万家定制的 AI 模型。”
“在许多行业中,由于大型数据集根本不存在,我认为重点必须从大数据转向优质数据。拥有 50 个精心设计的示例就足以向神经网络解释你希望它学习的内容。”
图 2.3 展示了小数据机器学习的挑战和机遇。虽然大数据/高价值机器学习用例的低垂之果已被“以 AI 为先”的企业摘取,但小数据/中等价值的长尾尚未得到充分利用。实际上,大多数机器学习用例存在于小数据集和低规模经济的长尾中。当数据集较小时,需要高度重视数据质量,以使机器学习变得有用:

图 2.3 – 机器学习机会的长尾
在下一节中,我们将探讨处理更小、更复杂的数据集的挑战,以及如何克服这些挑战。
为什么我们比以往任何时候都需要以数据为中心的人工智能
自世纪初以来,人工智能的领先组织,如“大九”,在机器学习方面取得了惊人的成果,但人工智能在长尾中的应用情况如何?
2020 年由麻省理工学院斯隆管理评论和波士顿咨询集团发布的一项调查得出结论,大多数公司都难以将他们对人工智能的愿景变为现实。在对来自 112 个国家的 29 个行业的 3000 多名商业领袖的调查中,70%的受访者理解人工智能如何创造商业价值,57%的人已经试点或生产化了人工智能解决方案。然而,只有十分之一的人能够通过人工智能产生显著的财务收益 20。
调查作者发现,那些通过人工智能实现显著财务收益的公司,他们的成功建立在两个支柱之上:
- 
他们拥有正确的数据、技术和人才坚实的基础。
 - 
他们定义了人类和人工智能共同工作和学习的一些有效方式。换句话说,他们创造了一个人类与人工智能之间的迭代反馈循环,从数据收集和整理到解决方案部署。
 
为什么这两个支柱对机器学习和人工智能的成功至关重要?因为机器学习模型只是机器学习系统的一小部分。
在 2015 年,谷歌研究人员 Sculley 等人 21 发表了一篇开创性的论文,名为《机器学习系统中的隐藏技术债务》,在其中他们描述了“只有一小部分现实世界的机器学习系统由机器学习代码组成……所需的环境基础设施庞大且复杂”。
在传统的信息技术术语中,技术债务指的是在软件开发生命周期中走捷径所造成的长期成本。它包括硬编码的逻辑、缺失的文档、与其他平台的集成不足、代码效率低下,以及其他任何阻碍系统性能提升和未来改进的因素。技术债务可以通过消除这些问题来“偿还”。
机器学习系统不同之处在于它们可以在代码中承载技术债务,但它们还增加了技术债务可能存在于系统数据组件中的复杂性。输入数据是系统的基础成分,数据是可变的。由于机器学习模型是由数据和代码中许多特征加权影响驱动的,一个变量的变化可能会改变模型其余部分的逻辑结构。这也被称为 CACE 原则:改变任何东西都会改变一切。
如图 2.44 所示,一个商业化的机器学习系统远不止模型代码。在一个典型的机器学习项目中,估计只有 5-10%的系统是模型代码 22。其余的 90-95%的解决方案与数据和基础设施相关:

图 2.4 – 机器学习系统远不止代码。来源:改编自 Sculley 等人,2015
如 Sculley 等人所描述,机器学习解决方案中的数据收集和整理活动通常比直接模型开发活动资源密集得多。鉴于这一点,数据工程应该是数据科学家的最佳伙伴。然而,数据质量的重要性与大多数机器学习解决方案在实际开发中的发展之间存在脱节。
数据质量级联效应
在 2021 年,谷歌研究人员 Sambasivan 等人对来自美国、印度、东非和西非的 53 名机器学习从业者的实践进行了研究。研究参与者来自高风险领域,如医疗保健、农业、金融、公共安全、环境对话和教育。
研究的目的是确定和描述数据质量对机器学习系统的影响,并展示他们所说的数据级联的实证证据——由数据质量问题引起的累积负面效应。
数据级联是由传统的以模型为中心的机器学习实践引起的,这些实践低估了数据质量,通常会导致对模型性能的不可见和延迟影响——换句话说,这是机器学习特有的技术债务。根据研究人员的说法,数据级联非常普遍,研究中 92%的机器学习从业者在一个特定项目中经历过一个或多个数据级联。
数据级联的原因可以分为以下小节中解释的四个类别。
数据工作的感知价值低和缺乏奖励系统。
在机器学习机会的长尾中,数据不可用通常有两个潜在原因:
- 
首先,正在建模的事件是定制化和罕见的,因此对于特定用例可以收集的数据量有一个物理限制。
 - 
其次,数据收集和整理活动被认为是相对昂贵和困难的,尤其是在涉及手动收集时。
 
事实上,大多数与数据相关的工作并不是由数据科学家完成的。直接负责创建、收集和整理数据的角色通常将这些任务作为他们工作中的次要职责。由于优先级竞争、时间限制、收集系统的技术限制或简单地缺乏如何进行良好数据收集的理解,收集高质量数据的责任经常与其他职责相冲突。
以医院护士为例,他们负责与患者护理相关的各种任务,其中一些是数据收集。如果可以通过机器学习进行聚合和推广,高质量的健康数据有可能为全球的患者和医疗保健提供者创造巨大的利益。然而,对于个别护士来说,完成记录患者状况和医疗干预措施所需的最少工作有更大的激励,这样就可以有更多的时间用于初级患者护理。这种场景的典型结果是数据收集在细节深度和标签一致性方面表现不佳。
机器学习实践者在下游也面临着类似的挑战。Sambasivan 等人描述了商业和项目目标,如成本、收入、上市时间和竞争压力如何导致数据科学家匆忙进行模型开发,从而为数据质量和伦理问题留下不足的空间。正如一位实践者所说,每个人都想做模型工作,而不是 数据工作。
缺乏跨职能协作
当涉及到高风险或定制化的机器学习项目时,领域专家通常在数据收集的上游阶段是关键参与者,同时也是模型输出的最终消费者。
表面上看,领域专家应该非常愿意积极参与机器学习项目,因为他们能够获得有用模型的益处。然而,情况往往相反。
为了机器学习目的而收集额外信息的要求通常意味着数据收集者和编纂者必须更加努力地完成工作。对于数据素养有限的前线工作者来说,可能很难理解数据收集的重要性,而且不幸的是,这种行为的级联效应往往在项目生命周期后期显现出来——通常是在部署之后。
数据科学家在数据收集中也应该扮演关键角色,因为他们将在模型开发过程中做出许多关于如何解释和操作数据集的决定。因此,机器学习实践者对特定领域的技术和社会背景的好奇心和愿意理解是任何项目成功的关键部分。它是使机器学习解决方案相关和准确的无形粘合剂。
不幸的是,数据科学家通常缺乏特定领域的专业知识,并依赖领域专家来验证他们对数据集的解释。如果机器学习实践者不不断质疑他们的假设,过度依赖他们的技术专长,并且理所当然地认为输入数据的准确性,他们就会错过他们试图建模的上下文的细微之处。当这种情况发生时,机器学习项目将遭受数据级联的影响。
跨职能协作不足会导致成本高昂的项目挑战,例如额外的数据收集、结果误解释以及对机器学习作为特定问题相关解决方案的信任缺失。
机器学习实践者的教育和知识差距
即使是最技术娴熟的机器学习实践者,如果他们缺乏机器学习管道的端到端知识,也可能无法为现实场景构建有用的模型。不幸的是,大多数数据科学家的学习路径都缺乏对数据工程实践的适当关注。
研究生课程和在线培训课程建立在干净的数据集之上,但现实生活中充满了脏数据。数据科学家并没有接受过从头开始构建机器学习解决方案的训练,包括数据收集设计、数据管理和数据治理流程、培训数据收集者、清理脏数据和构建领域知识。
因此,数据工程和 MLOps 实践被那些直接负责将原始数据转化为有用洞察的人理解不足且评价不高。
缺乏对数据质量的测量和问责
传统的 ML 实践依赖于统计准确性测试,如精确度和召回率,作为模型和数据质量的代理。这些措施并不提供关于数据集质量直接信息的任何直接信息,这些信息与表示特定事件和相关的情境背景相关。在过程早期缺乏标准化的方法来识别和纠正数据质量问题,使得数据改进工作变得反应性,而不是有计划地与项目目标保持一致。
广泛使用的管理短语“衡量什么,管理什么”在数据质量环境中也是正确的。如果没有适当的流程来识别数据质量问题,就难以激励和分配个人对良好数据收集的责任。
在高风险领域对数据质量进行问责的重要性得到了这样一个事实的支持:模型准确性通常必须非常高,基于小数据集。例如,在低风险且数据丰富的行业,如在线零售或数字广告,由于数据收集的自动化和持续性,一个表现不佳的模型可以相对快速地修改。
在长尾部署的 ML 模型通常更难以验证,因为事件发生的频率要低得多。同时,高风险领域通常要求更高的模型准确性阈值。在线广告商可能可以接受 75%的准确性分数,但用于癌症诊断的模型通常必须具有低于 1%的错误率才能具有可行性。
避免数据级联和技术债务
数据级联的普遍性突显了一个更大的潜在问题:ML 开发中的主导惯例是从大数据公司的实践中借鉴的。这些实践是在数据丰富且可消耗的环境中开发的,每个用户都有一个账户 24。结合“快速行动,打破事物”25 的文化,将数据工作视为不受欢迎的苦差事,这种做法在大多数高风险领域都会失败。
数据质量不佳的级联效应难以透明化和以标准化的方式追踪,尽管它们频繁发生且持续存在。幸运的是,数据级联也是可以修复的。Sambasivan 等人将“数据卓越”定义为解决方案:一种文化转变,将数据管理视为核心业务学科,并为 ML 管道中的相关人员建立正确的流程和激励机制。
作为数据专业人士,我们决定 ML 是否应该继续成为少数人的工具,或者是否是时候允许具有较小财务价值或更高风险的项目变得可行。为此,我们必须努力追求数据卓越。
现在,让我们总结本章的关键要点。
摘要
在本章中,我们回顾了机器学习的历史,以帮助我们清楚地理解为什么以模型为中心的机器学习是当今占主导地位的方法。我们还学习了以模型为中心的方法如何限制我们从机器学习机会的长河中释放潜在价值。
到目前为止,你应该已经深刻理解为什么数据中心化对于机器学习学科实现其全部潜力是必要的,同时也认识到这将需要巨大的努力来实现这一转变。要成为一名有效的数据中心化机器学习实践者,必须打破旧习惯并形成新习惯。
现在,是时候开始探索实现这一转变的工具和技术了。在下一章中,我们将讨论数据中心化机器学习的原则以及与每个原则相关的技术和方法。
参考文献
- 
www.idc.com/getdoc.jsp?containerId=prUS47560321,于 2022 年 9 月 23 日查阅 - 
Lewis, A. R., 2006, 《美国的战争文化》,Routledge,纽约,美国
 - 
www.nasa.gov/feature/when-the-computer-wore-a-skirt-langley-s-computers-1935-1970,于 2022 年 9 月 23 日查阅 - 
www.historyofdatascience.com/k-nearest-neighbors-algorithm-classification-and-regression-star/,于 2022 年 9 月 23 日查阅 - 
large.stanford.edu/courses/2012/ph250/lee1/docs/Excepts_A_Conversation_with_Gordon_Moore.pdf,于 2022 年 9 月 23 日查阅 - 
www.businessinsider.com/ibm-1970-mainframe-specs-are-ridiculous-today-2014-5,于 2022 年 9 月 22 日查阅 - 
www.ibm.com/ibm/history/exhibits/mainframe/mainframe_PP3145.html,于 2022 年 9 月 22 日查阅 - 
www.apple.com/au/iphone-14/specs/,于 2022 年 9 月 23 日查阅 - 
www.quickbase.com/articles/timeline-of-database-history,于 2022 年 9 月 24 日查阅 - 
www.dataversity.net/brief-history-database-management/,于 2022 年 9 月 24 日查阅 - 
www.r-project.org/about.html,于 2022 年 9 月 24 日查阅 - 
www.historyofdatascience.com/leo-breiman-statistics-at-the-service-of-others/,于 2022 年 9 月 24 日查阅 - 
www.image-net.org/about.php, 查阅于 2022 年 9 月 24 日 - 
www.dataversity.net/brief-history-cloud-computing/, 查阅于 2022 年 9 月 25 日 - 
thenextweb.com/news/2010-2019-the-rise-of-deep-learning, 查阅于 2022 年 9 月 25 日 - 
www.dataversity.net/brief-history-data-science/, 查阅于 2022 年 9 月 25 日 - 
hbr.org/2012/10/data-scientist-the-sexiest-job-of-the-21st-century, 查阅于 2022 年 9 月 25 日 - 
Webb, A., 2019, The Big Nine: How Tech Titans and Their Thinking Machines Could Warp Humanity, Hachette Book Group, 纽约,美国
 - 
spectrum.ieee.org/andrew-ng-data-centric-ai, 查阅于 2022 年 9 月 25 日 - 
Ransbotham, S., Khodabandeh, S., Kiron, D., Candelon, F., Chu, M., and LaFountain, B., Expanding AI’s Impact With Organizational Learning, MIT Sloan Management Review and Boston Consulting Group, 2020 年 10 月
 - 
papers.nips.cc/paper/2015/file/86df7dcfd896fcaf2674f757a2463eba-Paper.pdf, Sculley et al., 2015, 查阅于 2022 年 7 月 23 日, - 
Yang, K., 2022, Landing AI – Moving Beyond the Software Industry,
community.ai-infrastructure.org/public/videos/landing-ai-ai-moving-beyond-the-software-industry-2022-09-30 - 
Sambasivan, N., Kapania, S., Highfill, H., Akrong, D., Paritosh, P., Aroyo, L., 2021, Everyone wants to do the model work, not the data work: Data Cascades in High-Stakes AI
 - 
hbr.org/2019/01/the-era-of-move-fast-and-break-things-is-over, 查阅于 2022 年 10 月 8 日 
第二部分:数据驱动机器学习的构建模块
在本部分中,我们通过四个关键原则为数据驱动机器学习奠定基础,这些原则支撑了这种方法,在探索具体技术之前,为您提供必要的背景知识。然后我们探讨了以人为中心和非技术性的数据质量方法,考察了专家知识、训练有素的标注员和清晰的指示如何增强您的机器学习输出。
本部分包含以下章节:
- 
第三章**, 数据驱动机器学习的原则
 - 
第四章**, 数据标注是一个协作过程
 
第三章:数据中心化机器学习的原则
在本章中,你将学习以数据为中心的机器学习的关键原则。我们将在本章中介绍数据中心化的基础原则,以提供一个高级结构和框架,供你在本书的其余部分进行工作并参考。这些原则将在我们深入探讨下一章中与每个原则相关的具体技术和方法之前——即“为什么”——为你提供重要的背景信息。
阅读原则时,请记住,以数据为中心的机器学习是模型中心方法的扩展——而不是替代品。本质上,模型中心化和数据中心化技术协同工作,以从你的努力中获得最大价值。
到本章结束时,你将对每个原则及其如何共同形成一个以数据为中心的框架有很好的理解。
在本章中,我们将涵盖以下主题:
- 
原则 1 – 数据应该是机器学习发展的中心
 - 
原则 2 – 有效利用标注者和主题专家(SMEs)
 - 
原则 3 – 使用机器学习来改进你的数据
 - 
原则 4 – 遵循道德、负责任和良好治理的机器学习实践
 
有时候,你所需要的只是正确的数据
几年前,我(乔纳斯)领导着一个数据科学家团队,他们面临着一个有趣但具有挑战性的问题。我们工作的金融服务业务吸引了大量新的在线访客,他们希望通过公司的网站向我们开设新账户。然而,由于未知原因,相当数量的潜在客户无法完成开户流程,这就是为什么公司转向数据科学家寻求帮助的原因。
这个未激活账户和失去客户的问题是多方面的,但我们决心在草堆中找到每一根针。开户流程相当直接,旨在使某人能够在 10 分钟内(无需支持)轻松开设新账户。对于客户来说,步骤如下:
- 
输入个人详细信息。
 - 
验证身份。
 - 
验证联系信息。
 - 
接受条款和条件并开设账户。
 
这个过程在大多数情况下都有效,但对于相当一部分申请人来说,在步骤 2和步骤 3中出现了问题。如果某人的身份无法在线验证(步骤 2),那么个人必须亲自验证,这对许多人来说是一个明显的减分项,并导致了显著的用户流失。
在步骤 3中出现的这些问题不太明显。大约 10%的用户会在这一点上放弃他们的旅程,尽管大部分艰苦的工作已经完成。为什么有人会经历整个过程,然后最终决定不继续进行?
我们收集了我们能得到的所有相关数据点,但遗憾的是,我们没有一个非常深入的数据集来工作,因为账户开设过程非常简单,这些是新客户。我们对数据集进行了分析,并使用了各种监督和无监督的机器学习技术来找出与账户未开设相关的任何行为,但我们的分析中没有突出的事物。
我们决定深入挖掘。由于这些客户分享了他们的联系信息,我们可以将他们的电话号码与我们的通话记录相匹配,并获取与匹配电话号码的录音对话。我们提取了数百个通话录音并开始收听。
很快,一个清晰的模式出现了:“我点击了验证联系信息按钮,但从未收到验证码,”一位录音中的呼叫者说。“我已经等了 10 分钟,但验证码还没有到来,”另一位说。用户无法通过验证,因为他们没有收到作为短信的最终验证码——即使呼叫中心的工作人员重新发送了。但并非所有新用户都遇到这种情况,那么这个特定群体出了什么问题?
随着我们继续收听通话录音,另一个微弱的信号出现了:“我不应该回来,”一位用户说。“自从我上次来这里,你们的系统并没有变得更好,”另一位说。
我们查看了一些关闭的客户账户,果然,这些人以前是我们的客户。问题很简单,企业系统将这些用户视为现有客户,因此没有发送所需的短信,无论用户或工作人员提示多少次。这个问题每周大约发生 200 次,这意味着企业每年失去了 10,000 名新客户。为什么没有人早点发现这个问题?
只有 200 次中的少数几次会生成呼叫,而且由于整个星期都有数百名呼叫中心工作人员值班,这似乎是一个罕见的故障,只偶尔发生。没有人能看出问题,因为它太不频繁,我们的模型也无法标记。毕竟,初始数据集噪音太多,信号不足。
我们之所以能够找到这个问题的根源,并从混乱中找到我们的“针”,是因为我们遵循了本章讨论的数据中心化的四个原则。让我们更详细地探讨这些原则中的每一个。
原则 1——数据应该是机器学习发展的中心
正如我们在第二章**从以模型为中心到以数据为中心——机器学习的演变中讨论的那样,占主导地位的模式中心方法在几个方面存在不足:计算和存储已经商品化,算法已经实际上自动化并且高度依赖数据,模型是可访问的但不太灵活,深度学习和 AutoML 工具无处不在。但数据呢?嗯,那还是未知数。
而不是依赖于强大的计算和存储环境以及需要大量数据的复杂算法来给我们带来模型准确性的增量提升,一个更好的方法是由数据驱动——具体来说,是由手头问题可用且相关的数据驱动。
数据对每个公司、问题和情况都是独特的,数据驱动的范式通过在模型之前将焦点和开发努力放在数据上,来认可这一点。数据不再是可以在一开始收集后就被遗忘的静态资产;现在它是一种独特的商品,需要充分利用其全部潜力以做出更好的预测。我们将论证,在许多情况下,公司的专有数据是其唯一的真正独特的竞争优势——只要得到充分利用。
通过关注数据,数据驱动的范式帮助公司区别于其竞争对手。大多数公司都能访问相同的算法和大量的计算和存储,但他们使用的数据以及从这些数据中获得的见解可以给他们带来决定性的竞争优势。
在我们的观察中,关注数据质量除了获得更好的数据外,还为组织带来了实质性的好处。关注数据质量意味着超越简单的数据精炼,因为高质量数据是业务运营的关键组成部分。
为了提高数据质量,通常需要数字化和自动化流程,并创建对流程遵守的强大问责制。强大的数据治理流程将分配数据质量的拥有权、监护权和问责制,这反过来又依赖于遵循、衡量和管理的数据收集标准和流程。
数据质量通常是底层过程质量和对这些过程遵守情况的症状。如果一个组织擅长收集高质量数据,那么它也更有可能拥有良好的通用流程。随着公司改进数据收集,它们推动更好的问责制、准确性、可靠性和整体一致性。因此,关注数据质量可以产生深远的影响,而不仅仅是改善数据完整性和可靠性。它是运营卓越的关键驱动因素。
如果我们回顾本章开头讨论的缺失验证码的例子,对现有数据集进行任何模型选择、参数调整或特征工程都不会揭示问题的根本原因。
这个问题只能通过收集手头问题的正确数据来发现和解决。在这种情况下,缺失的数据点如下:
- 
验证码没有被接收
 - 
这仅适用于之前客户返回并开设新账户的情况
 
验证码问题的发现导致业务运营方式发生了两项关键变化。首先,IT 部门修复了负责触发发送验证码的代码,在其他条件相同的情况下,这导致了新账户开设数量的显著增加。其次,客服中心团队建立了一个中央流程来记录客户的技术问题,无论大小,这样我们就能发现任何新系统问题的“冰山一角”。换句话说,现在公认的文化是收集高质量数据是改进运营流程的核心。
这对一线员工在两个方面产生了心态转变。首先,他们对数据作为一项强大的资产有了新的认识,这项资产可以汇总和分析,以了解他们工作的整体情况。其次,一线团队现在感到更有权力:如果我做好我的部分工作,捕捉和指出重要问题,就有机会修复它们。
我们的数据科学家也对数据收集和整理作为他们角色关键部分有了不同的认识。通过站在客户和一线员工的角度看问题,他们看到了他们能够产生的影响,这使团队解决问题的方法发生了深刻的变化。而不是接受数据(质量)作为既定事实,数据工程现在渗透到模型开发和部署过程的每个步骤。
数据中心的清单
为了忠实于数据中心的机器学习第一原则,在处理机器学习项目时拥有一个数据聚焦的任务清单非常有价值。以下是我们的清单,分布在模型开发生命周期中的五个步骤中。
第 1 步 – 确定业务问题,界定项目范围,并定义数据需求
任何机器学习项目的第一部分始终应该是清楚地定义你试图解决的问题;这应该与关键利益相关者,如最终用户和行业专家合作完成。当你对你要解决的问题有一个清晰的定义,以及问题解决后的成功样子时,识别数据差距会容易得多。
一个强大的建模数据集包含内容和上下文。内容是你正在测量的特定对象、事件或状态,上下文描述了对象、事件或状态发生的情境。在我们之前的缺失验证码示例中,内容是(缺失的)验证码,上下文是对于以前, 回归客户。
定义项目范围的一个关键要素是概述你试图模拟的过程。我们通过将相关最终用户和行业专家与数据科学家一起放在一个房间里,直到他们能够绘制出业务问题背后的过程或情况,来做到这一点。这使所有参与者能够深入了解问题的内容和背景,同时确定构建模型所需的重要数据点。
在这一步骤中,以下是一些需要考虑的问题清单:
- 
我们是否清楚地定义了我们试图解决的问题?
 - 
这是不是我们首先应该解决的问题?
 - 
通过解决这个问题的我们期望实现什么结果?
 - 
我们是否已经与专家一起确定了问题的关键部分?
 - 
根据专家的意见,过程中的关键步骤或时刻是什么,我们的数据是否适当地捕捉了这些?
 - 
我们的数据点是否包含内容和上下文?
 - 
从我们的解决方案中可能会产生哪些偏差,我们需要在以后留意这些偏差?
 - 
这些偏差会导致任何群体或部分受到不公平对待吗?我们如何在验证阶段(步骤 4)识别这些偏差?
 - 
在我们的数据集中使用所有特征是否合法和道德?
 - 
输出将进行内部或外部审计吗?
 
步骤 2 – 准备和标记数据
对于许多数据科学家来说,数据准备是一项令人畏惧的任务。我们确实认为数据准备既可能是重复的,也可能是耗时的,但作为数据为中心的机器学习的倡导者,我们鼓励您将其视为工作中最重要的部分。
数据准备涉及收集、清理、结构化、增强和增强您的输入数据,以增加数据集中的信号并减少噪声。这些任务可能既具有技术挑战性,又具有回报性——特别是当您开始看到 AUC 分数上升时。到目前为止,您已经意识到,如果您投入正确的努力,这个过程可能会给您带来非常强大的建模结果。
以下清单对于指导您完成数据准备过程非常有用。我们将在本书的其余部分详细教授您如何执行这些任务。
这里是清单问题:
- 
我们是否对数据质量进行了技术验证?(见第五章**,数据清理技术。)
 - 
我们能否通过清理数据来增强数据集的强度?(见第五章**,数据清理技术。)
 - 
我们需要收集额外数据或使用人工标记员提高现有数据集的质量吗?(见第四章**,数据标记是一个协作过程*。)
 - 
我们需要定义特定的标记规则并培训专家吗?(见第四章**,数据标记是一个协作过程*。)
 - 
我们能否通过程序化标记提高数据质量或填补缺失值?(见第六章**,机器学习中程序化标记的技术。)
 - 
我们是否应该使用合成数据来增强或提高数据中某些类别的质量?(见第七章**,在数据为中心的机器学习中使用合成数据。)
 - 
我们是否需要保护数据集中个人的隐私?(见第七章**,在数据为中心的机器学习中使用合成数据。)
 - 
我们的数据集是否包含有偏见的类别,我们需要调整这些类别吗?(参见第八章**,识别和 消除偏见 *的技术。)
 - 
我们的数据集是否包含足够数量的正确类型的罕见事件?我们需要添加更多或删除异常值吗?(参见第九章**,在 机器学习 *中处理边缘情况和罕见事件。)
 - 
我们能否从现有数据集中工程化新的特征?
 
第 3 步 – 训练模型
模型训练阶段是数据中心化和模型中心化机器学习原则结合在一起以产生协同效应的地方。再次强调,重要的是不要丢弃你基于模型中心方法已经知道的所有关于如何构建和增强机器模型的知识。数据中心化只是给你工具箱中额外的一套工具,并允许你放大模型的影响。
特征选择是这一协同过程的重要组成部分,因为它筛选出对模型无用(在最坏的情况下,可能是有问题的)特征。一般来说,希望有更少的属性对模型做出贡献是可取的,因为这减少了不必要的噪声,并使模型更容易解释。
考虑特征选择作为模型选择过程的一部分是很重要的,因为模型及其输入数据是手牵手产生预测的。它们本质上是相互联系的。实际上,这意味着你应该与模型一起选择特征,而不是使用静态的预选特征数据集来选择模型。
这里是清单问题:
- 
你怀疑你的数据是否脏(例如,错误的标签、缺失值、无意义的模式或不相关的输出)?
 - 
我们能否通过提高数据集的质量来提高模型的准确性?本书中概述的长期数据中心化技术列表旨在帮助你完成这项任务!
 - 
我们工程化的特征是否表明数据中存在需要我们收集额外数据(特征或观察)的关系?
 - 
我们(工程化)的特征是否表明数据中存在任何我们应该与行业专家(SMEs)验证的关系?与其假设我们的新特征是正确的,不如将任何有影响力的相关性与行业专家交叉检查,以确保它们在解决方案的上下文中相关性和有效性。
 - 
我们能否在不损失预测能力的情况下减少模型中的特征数量?通过应用降维技术或特征选择方法,我们可能能够减少模型中的特征数量,而不会显著降低其预测准确性。
 
第 4 步 – 评估性能、公平性和偏见
以数据为中心的机器学习方法在模型评估期间对检测偏见和公平性问题给予了高度重视。为了验证机器学习模型的准确性,您仍然应该从传统的验证任务开始,例如将数据分为训练集和测试集,执行交叉验证,并生成混淆矩阵。以下清单假设您将已经执行这些任务,使用标准性能评估,使用如准确率、精确率、召回率、F1 分数等指标。
偏见检测是揭示机器学习模型中潜在的不公平性和歧视的重要工具。这可以通过为每个子组单独创建混淆矩阵来实现,以比较各组之间的假阳性率和假阴性率,并评估各组之间的群体平衡(平等代表)和机会平等(平等的真阳性率)或等概率(平等的假阳性率)。与性别或种族相关的子组间的差异是偏见或不公平的常见来源。
这里是清单问题:
- 
我们能否通过提高数据质量或收集新特征来提升模型的表现?
 - 
我们能否检测到对特定群体或部分的任何偏见或不公平?
 - 
敏感属性与模型做出的预测之间是否存在任何大的相关性?
 - 
模型在处理未见过的数据时,在公平性和偏见方面的表现如何?
 
我们将在第八章**,识别和消除偏见的技术*中介绍识别和消除偏见的技术。
第 5 步 – 部署和监控
机器学习模型的有效监控也依赖于以数据为中心的原则。在监控模型性能时,包括数据质量、数据覆盖范围、数据相关性和标注一致性指标是很重要的。
数据质量指的是数据的准确性、完整性和一致性,而数据覆盖范围指的是拥有足够的数据点来首先做出自信的预测。数据相关性确保用于训练模型的数据适合该任务。最后,数据标注一致性确保用于训练模型的数据点具有正确的标签。
几种技术和工具可以帮助数据科学家有效地监控机器学习模型。例如,数据漂移检测有助于检测数据特征的变化,如均值、方差和分布。同样,异常值检测有助于识别与常见分布显著不同的数据点。此外,偏见检测技术有助于识别和纠正机器学习模型中的偏见实例。
除了依赖报告和指标来监控 ML 模型之外,理解监控是一个持续的过程,需要数据科学团队以外的利益相关者的参与至关重要。利益相关者可能包括领域专家、业务所有者和最终用户。这些利益相关者应协作评估模型的表现,解释结果,并确定需要解决的问题。
这里是清单问题:
- 
模型中使用的数据源是否自动化、一致且可靠?
 - 
我们是否设计了一个监控和报告计划,以捕捉失败、偏差和漂移?
 - 
我们的监控是否量化了数据质量、数据覆盖范围、数据相关性和标签一致性?
 - 
我们是否为最终用户提供了对模型性能进行持续反馈的机制?
 
我们的设计清单问题旨在让您在模型开发过程的每一步都思考数据质量及其影响。换句话说,它们是对更多以模型为中心的开发任务的补充,而不是替代。
数据中心化需要从“我将用这些数据构建最好的模型”的心态转变为“我们如何构建最佳数据集来解决这个特定问题?”为了做到这一点,我们需要整个组织参与协调一致的努力。这使我们来到了数据中心化机器学习的第二个原则。
原则 2 – 有效利用标注者和领域专家
无论您在阅读此内容时 AI 炒作周期处于哪个阶段,AI 和 ML 开发不太可能已经发展到不再需要人类输入和标注的阶段。
近年来,我们在 AI 技术的复杂性方面经历了大幅增长,尤其是在生成 AI 领域。尽管如此,一个事实仍然存在,即即使是功能最强大、最具革命性的 AI 技术,如 ChatGPT,也依赖于由人类标注者组成的小型军队来完善和提升其能力。
这些个人会审查和标注数据样本,然后这些样本会被反馈到模型中,以提升其对自然语言和上下文的理解。人类标注者采用的一些关键方法和技巧包括以下内容:
- 
领域专业知识:具有主题领域专业知识的标注者可以提供有价值的见解和标注,帮助模型更好地理解特定主题和领域。
 - 
主动学习:这种方法涉及优先处理模型认为模糊或具有挑战性的数据样本,使标注者能够专注于他们输入可以产生最大影响的领域。
 - 
视角多样性:通过涉及来自不同背景和具有不同经验水平的标注者,模型可以接触到更广泛的语言细微差别、文化背景和观点,从而提高其整体性能。
 - 
质量控制:定期审计和评估标注器的输出可以帮助确保标注质量的一致性和遵循指南,这对于有效的模型训练至关重要。
 
简而言之,人工标注员对于机器学习至关重要,我们模型的质量取决于我们训练、组织和与这些标注员合作的能力。从广义上讲,在机器学习开发过程中利用行业专家的方式有三种:
- 
作为数据点的直接标注员
 - 
作为输出质量验证者和不良输出(如有害内容)的检测器
 - 
作为知识专家,他们可以帮助我们制定标注规则
 
有效利用行业专家需要从仅为标注员创建标注规则(尽管这仍然很重要)的心态转变到利用行业专家和数据科学家结合的强项,通过明确的标注规则覆盖问题空间。
我们的经验表明,采用这种方法不仅提供了强大的标注功能,还有助于我们追踪模糊的例子并提高模型性能,但同样重要的是,它允许数据科学家和行业专家进行合作。随着数据科学家了解主题内容,行业专家学习数据科学的工作方式,这会产生一个飞轮效应,导致新想法、洞察和知识的产生。
让我们更详细地探讨三种人工标注方法。
直接由人工标注员进行标注
人工标注数据的主要优势是其准确性。人类能够以计算机无法做到的方式识别模式和主观性。这意味着分配给数据的标签可能比自动化过程生成的标签更准确。此外,人类可以提供自动化过程中可能丢失的数据背景。
人工标注数据也比自动化流程提供了更大的灵活性。标注员可以根据具体需求或要求定制他们的标注过程,使他们能够调整标注以适应项目目标。这使得机器能够更准确、更快速地解释数据。
最后,与其它标注方法相比,人工标注数据可能更具成本效益。这一点在数据集规模从小到中等时尤为正确。
小型数据集可能包含几百到几千个观察值,通常可以由一个小型标注团队管理。中型数据集可能有数万个观察值。虽然手动标注仍然是可能的,但随着复杂性和所需时间的增加,它开始变得在经济上不太可行。
面对更大的数据集时,由于数据量巨大和潜在的复杂性,手动标注可能会变得重复且容易出错。在这个规模上,数据中的复杂性也可能增加,需要更细腻的理解,这可能对标注员保持一致性构成挑战。
对于更大或更复杂的数据集,我们建议走程序化标注的道路,我们将在下一部分进行讨论。有趣的是,混合方法也可以有效,其中大型数据集的一个子集被手动标注,作为程序化标注算法的训练数据。这样,你可以利用人工标注的准确性和机器学习的可扩展性,确保即使是大型数据集也能获得高质量的标签。
回想一下我们在本章开头概述的丢失验证码的故事。一旦我们确定了与尚未发现的议题相关的电话,我们选择手动监听数百个电话,而不是使用机器学习技术来捕捉主题。为什么?
因为我们想确保我们理解了这些交互的内容和上下文,而人类更有可能做好这项工作。同时,我们只听了数百个电话,而不是数百万个,所以人工标注是找到噪声中的信号和定位我们“大海捞针”问题的最经济有效的方式。
虽然人工标注是数据驱动方法的重要组成部分,但在使用人工标注员时,有一些陷阱和错误需要避免。在第四章《数据标注是一个协作过程》中,我们将教你如何最大限度地发挥专家和人工标注员的作用,同时管理潜在的负面影响。
使用人工标注员验证输出质量
如前所述,即使是像 ChatGPT 这样非常复杂的 AI 解决方案,也严重依赖人工标注员来引导算法达到最佳结果。ChatGPT 是基于监督学习和一种称为人类反馈强化学习(RLHF)的技术构建的。
强化学习是机器学习的一个领域,其中代理通过与环境的互动来学习做出决策。代理的目标是选择能够最大化累积奖励的行动。然而,为复杂任务定义合适的奖励函数可能具有挑战性。
这就是人类反馈发挥作用的地方。在 RLHF 中,AI 代理从人类提供的奖励和惩罚中学习,而不是从预定义的奖励函数中学习。这种方法结合了机器学习算法的力量与人类专家的直觉、经验和知识。
该过程包括以下步骤:
- 
代理与环境互动并采取行动。
 - 
人类观察者评估代理的行为,并以奖励或惩罚的形式提供反馈。
 - 
代理使用这些反馈来更新其学习并随着时间的推移改进其决策。
 
通过这些交互,AI 代理学会通过结合人类指导来执行复杂任务。
使用 RLHF(强化学习与人类反馈)的好处有几个。首先,人类反馈使得 AI 代理能够从人类所拥有的丰富知识和经验中学习。这种方法使得代理能够学习复杂的行为,这些行为可能用传统的算法难以实现。同时,通过调整人类专家提供的反馈,学习过程可以针对特定的需求或目标进行定制。
在循环中有人类存在的不利之处在于,人类可能会犯错误或提供不一致的反馈,这可能会影响代理的学习。此外,使用人类反馈训练 AI 代理可能是一个缓慢的过程,因为它需要人类专家的持续输入。换句话说,它往往劳动密集且可能成本高昂。因此,重要的是要提前估计此类项目所需的人类和财务资源,以确保其可行性。
重要的是要注意,行业专家几乎可以成为任何机器学习练习的极其宝贵的贡献者。例如,我们经常使用行业专家来帮助我们审查模型的输出,因为这使我们能够发现问题空间中的新上下文,这些上下文应该成为训练数据中的特征。
在缺失验证码的例子中,我们通过首先通过采访呼叫中心工作人员(一种行业专家)和听取通话录音(我们自己成为行业专家)来深入了解具体的失败点,从而发现了问题的根源。一旦我们缩小了可能的问题范围,我们就与 IT 部门的同事(另一种行业专家)一起深入核心系统的内部运作,以验证故障。
这种方法与强化学习不同,但它强调了在整个开发过程中涉及行业专家的价值,即使这需要人工输入。
使用程序化标注来规范标注规则
使用人工标注的传统方法有时是过程中的瓶颈,可能会阻止我们以既高效又经济的方式创建高质量的训练集。这通常是在处理大量数据集时出现的问题。随着机器学习模型变得更加复杂,数据集变得更大,时间和成本效益高的训练变得越来越重要。
进入程序化标注。从本质上讲,程序化标注是一个基于预定义的规则或算法自动为数据点分配标签的过程。程序化标注相对于人工标注的主要优势是,它可以比人工标注更快、更准确地完成——一旦建立了稳健的标注功能。这使得它非常适合大型数据集,在这些数据集中,人工标注可能耗时过长或成本过高。
程序化标注的过程始于定义需要分配给每个数据点的标签。这可以通过 SMEs 手动完成,或者通过自动化方法,如自然语言处理(NLP)算法或基于规则的系统来完成。一旦定义了标签,就可以使用监督学习或无监督学习算法将它们应用于数据点。
程序化标注的主要好处如下:
- 
可扩展性:程序化标注可以比人工标注更高效地处理大量数据,从而实现更快的模型训练和迭代。
 - 
一致性:自动化的标注方法确保在整个数据集中一致地应用规则和标准,减少变异性以及可能由人为主观性引起的潜在错误。
 - 
成本效益:通过自动化标注过程,组织可以节省在培训、管理和补偿人工标注员所需的时间和资源。
 - 
速度:程序化标注可以比人工标注更快地处理和标注数据,从而加速整个机器学习流程。
 - 
减少人为错误:自动化最小化了在人工标注过程中可能引入的人为错误和不一致性风险。
 - 
可重复性:自动化的标注过程易于复制,确保结果可以在不同的数据集和项目中重复和验证。
 - 
适应性:程序化标注算法可以根据需要微调和更新,以适应不断变化的需求、新的数据源或不断发展的项目目标。
 - 
24/7 可用性:与人工标注员不同,程序化标注可以持续运行,无需休息或停机时间,从而确保机器学习项目进展不间断。
 
我们将在第六章“机器学习中的程序化标注技术”中向您展示如何使用特定的程序化标注技术。
程序化标注技术通常足以提高数据质量。然而,在某些情况下,特征之间的关系过于复杂,基于规则的算法无法完成这项工作。这使我们来到了数据为中心的机器学习的第三原则。
原则 3 – 使用机器学习改进你的数据
正如我们可以使用程序化或算法方法来标注我们的数据一样,我们也可以使用机器学习来识别可能错误或不明确的数据点。通过利用可解释性、错误分析和半监督方法的发展,我们可以创建新的标签并找到改进或丢弃的数据点。
这里有一些使用机器学习生成更好输入数据的实际步骤:
- 
丢弃噪声示例:有时,更多的数据并不总是更好的。噪声数据可能导致预测不准确。通过移除噪声示例,我们可以提高输入数据的质量。例如,如果你正在分析客户评论,其中一些评论充满了随机字符或不相关信息,这些可以被认为是“噪声”并予以移除。
 - 
使用技术来关注数据子集以提高质量:并非所有数据都具有相同的价值。我们可以关注数据子集来提高输入数据的质量。例如,如果你正在分析销售数据,你可能只会关注你最有利可图的地区的子集数据,以获得最大的回报,其他条件相同。
 - 
通过利用专家输入的 ML 泛化来扩展可用的标签数据:ML 可以通过使用专家输入来实现类似精度和更大覆盖范围来扩展可用的标签数据。例如,鸟类物种的专家可以提供关于有限图像集的输入,ML 可以使用这些输入来准确地为更大的图像集打标签。
 - 
使用半监督方法:包括弱学习和主动学习在内的半监督方法可以用来识别需要专家审查的数据点。例如,你可能会使用主动学习来识别需要由人工进行情感分析的客户电子邮件。
 - 
使用可解释性:可解释性在识别数据中的模式和确保模型有意义方面至关重要。复杂的模型需要特定的或模型无关的可解释性方法,包括局部和全局方法以及 SHAP 值。例如,使用 SHAP 值可以帮助你理解为什么你的模型在贷款审批过程中预测了某个特定的结果,确保决策过程透明且可解释。
 - 
使用错误分析:错误分析可以帮助识别模型在数据中犯错的模式,从而帮助我们提高输入数据的质量。例如,如果你的模型在图像识别中将猫错误地识别为其他东西,错误分析可以帮助你找出它在哪里以及为什么犯这些错误。
 
执行这些步骤所需的技术将在本书随后的章节中概述。
通过在生产中应用这些步骤,我们可以识别标签函数或模型中的性能漂移。此外,我们可以识别需要人工审查的数据点,从而提高输入数据的质量并改善预测准确性。
使用 ML 来提高输入数据质量是传统 ML 方法的一个根本性转变。它需要从使用 ML 模型进行最佳预测的心态转变为使用 ML 来识别那些不利于模型性能的数据点。毕竟,以数据为中心的 ML 的目标是增加输入数据中的信号并减少噪声。
采用数据中心的做法也为我们提供了一个独特的机会,以符合伦理、负责任和良好治理的机器学习实践的方式收集和精炼数据。这种关注点的转变使我们能够设计我们的数据策略,不仅围绕性能提升,还围绕公平、透明和问责制原则。
在我们继续前进的过程中,我们将探讨这种方法如何帮助我们将伦理嵌入到我们的数据收集和精炼过程的内核。这样,我们可以确保提高数据质量与保持我们机器学习应用的完整性和可靠性相辅相成。
原则 4 - 遵循道德、负责任和良好治理的机器学习实践
随着数据中心的兴起,我们能够应对更多高风险挑战,道德和负责任的机器学习实践变得越来越重要。这要求你在设计算法时考虑透明度、公平性和问责制等因素,以确保它们不会歧视某些群体或个人。此外,负责实施这些系统的人必须了解它们的工作原理,并理解它们的局限性,以便他们可以就其使用做出明智的决定。
很不幸,道德和负责任的机器学习实践通常不如应有的那样发达。在 2021 年,IBM 商业价值研究所和牛津经济研究所进行了一项研究 1,其中 75%的高管将 AI 伦理视为重要;然而,不到 20%的高管强烈同意他们的组织实践与声明的原则和价值观相一致。
作为数据中心的机器学习从业者,我们需要考虑的是,“数据质量”这个术语比单个数据点的客观准确性要广泛得多。高质量的数据还应使我们能够在整个机器学习开发过程及其之外识别和监控潜在的伦理问题。
人工智能伦理和责任不仅仅是打勾作业,它可能是一个潜在的差异化来源。关注 AI 伦理的组织更有可能获得客户的信任,而忽视它的组织可能会遭受客户的反感和声誉损害 2。
英国 2020 年学校评级丑闻的故事突显了在高风险环境中使用机器学习时忽视伦理考虑可能发生的事情。在 COVID-19 大流行期间,由于封锁,英国各地的学生无法参加考试。相反,使用了一个算法来评定学生的考试成绩,导致大量学生获得的分数低于他们应得的分数。这引起了学生、教师和学术界的强烈不满,因为它被视为不公平和不公正。
英国监管机构 Ofqual 使用的算法旨在标准化不同学校的成绩,以便进行比较。它考虑了诸如先前成就和学校表现等因素。然而,它没有考虑个别学生的表现或教师评估,这导致许多学生获得的分数低于他们应有的分数。
相反,该模型偏袒来自私立机构和富裕地区的学生,这对公立、公立资助学校的高绩效个体产生了重大影响。因此,许多学生因考试成绩降低而失去了大学录取资格。这给那些努力学习考试却因未能准确反映其能力的算法而失望的学生带来了极大的痛苦。最终,算法给出的分数被取消,并由更公平但更手动评分的方法所取代。
为了避免未来发生类似英国评分灾难等类似事件,AI 系统必须从一开始就考虑到道德因素进行设计。总的来说,这一事件突显了与 AI 系统相关的某些伦理问题,并说明了为什么在我们设计和实施这些系统时考虑这些问题非常重要。它还提醒我们,如果我们希望这些系统能够成为我们社会中决策的有效工具,我们必须确保这些系统是透明的、公平的和可问责的。
我们将在第四章**,数据标注是一个协作过程中讨论处理标注模糊性的具体方法,并在第八章**,识别和消除偏差的技术中向您展示一系列识别和消除偏差的技术。
摘要
在本章中,我们概述了以数据为中心的机器学习的四个原则。通过遵循这些原则,您将能够创建基于高质量数据的机器学习模型,这些数据已经通过人类、标签函数和机器学习技术进行了增强、交叉检查和验证。
这使我们能够从数据中获得更多信号,从而提高我们在小型或大型数据集上构建强大模型的能力。最后,我们可以在整个开发生命周期中捕捉到伦理考虑,这最终确保我们使用我们的力量行善。
在下一章中,我们将探讨您可以如何结构化、优化和治理使用人工标注员进行您的机器学习项目的流程。
参考文献
- 
www.ibm.com/thought-leadership/institute-business-value/en-us/report/ai-ethics-in-action,2023 年 6 月 1 日访问 - 
解码 AI 在商业成果中的信任与伦理, 访问于 2023 年 6 月 1 日
 
第四章:数据标注是一个协作过程
随着人工智能领域的发展,ChatGPT、大型语言模型 Meta AI(LLaMA)、Bard、Midjourney 等公开可用的工具为使用结构化和非结构化数据所能实现的可能性设定了新的基准。
这些模型显然依赖于先进的算法和大量数据,但许多人并不知道人类标注仍然是他们持续改进和发展的关键组成部分。例如,ChatGPT 的模型基础设施依赖于个人审查和标注数据样本,然后反馈给模型以改善其对自然语言和上下文的理解。
在本章中,我们将探讨如何充分利用涉及人类标注员的数据收集和标注任务。我们将涵盖以下一般主题:
- 
我们为什么需要人类标注员
 - 
理解人类标注任务中出现的常见挑战
 - 
设计一个框架以实现高质量的标签
 - 
优化人类标注员激励措施、避免偏见以及处理标注模糊性的最佳实践方法
 
首先,让我们了解为什么人类输入是数据驱动机器学习(ML)的基石。
理解多样化人类标注的好处
在人类标注过程中纳入多样化的个人和观点提供了几个优势。人类在数据标注方面带来的精确度和准确性是机器难以匹敌的。尽管自动化系统可能难以处理模糊性或复杂性,但人类标注员可以利用他们的理解和推理能力做出明智的决策。
数据会随时间变化,新的场景可能会出现,而这些场景在原始训练数据中并不存在。人类标注员可以适应这些变化,提供反映新现实的更新标注。这确保了随着数据的发展,机器学习模型保持相关性和有效性。
人类标注员相对于程序化标注的一些关键优势包括以下内容:
- 
领域专业知识:具有专业知识领域的标注员可以提供有价值的见解和标注,帮助模型更好地理解特定主题和领域。
 - 
主动学习:这种方法涉及优先处理模型认为模糊或具有挑战性的数据样本,使标注员能够专注于他们输入可以产生最大影响的领域。
 - 
观点多样性:多样化的标注员群体可以帮助减轻训练数据中的潜在偏见,从而实现更公平、更具包容性的 AI 模型。通过涉及来自不同背景和具有不同经验的人,模型可以接触到更广泛的语言细微差别、文化背景和观点,从而提高其整体性能。
 - 
增强的上下文理解:通过借鉴来自不同背景的标注员的经验和知识,模型可以更深入地理解语言细微差别、习语和文化参考。接触广泛的观点和输入可以使模型更具弹性和适应性,使其能够有效地处理更广泛的任务和场景。
 - 
质量控制(QC):定期审计和评估标签器输出可以帮助确保标注质量的一致性和遵守指南,这对于有效的模型训练至关重要。
 - 
遵守伦理规范:基于伦理考虑,可能存在不应成为模型输入的数据或场景。在这些情况下,人类标注员在帮助模型满足伦理标准方面发挥着至关重要的作用。例如,ChatGPT 背后的公司 OpenAI 使用人类标注员进行审查、标注和过滤掉有害的“不适宜工作环境”数据。
 
虽然人类标注是数据为中心的模型开发的关键部分,但人类也会给任何机器学习项目带来新的行为、偏见和风险,这些必须得到管理。在我们介绍管理这些挑战的框架之前,我们将讨论这些典型的挑战。
理解由人类标注员产生的一般挑战
在我们深入探讨标注准确性和一致性的最佳实践之前,我们将定义我们必须通过我们的标注框架解决的常见挑战。标注不准确性和歧义通常是由以下七个原因之一或多个原因引起的:
- 
不明确的指示:数据标注任务的不明确或不充分的指示会导致标注不一致。如果标注员没有收到明确的指南,他们可能会做出假设或猜测,导致不一致或不准确的标注。
 - 
人类偏见:当数据偏向于特定结果或结果时,偏见可能会引入歧义,导致不准确的理解。一个常见的解决方案是为同一数据分配多个标注员进行标注,选择最频繁出现的标签作为正确的标签。然而,这种聚合或投票方法有时可能会加剧偏见而不是纠正它。例如,如果大多数标注员存在特定的偏见,他们的共识可能反映了这种偏见而不是真实的数据。
 - 
人类错误:标注员是人,人都会犯错误。即使是最受培训、最投入和最专注的标注员,也可能因为一个打字错误或鼠标点击错误而应用错误的标签,导致数据集中出现随机噪声。这些错误确实会发生,但不太可能以系统化的方式发生。尽管如此,我们需要有一种方法来识别和纠正这些错误,以确保它们不会引入不必要的随机噪声。
 - 
客观任务与主观任务:每个任务都位于一个光谱上,从完全客观(只有一个正确答案)到高度主观(有多个可能的正确解释)。任务越主观,它倾向于在数据标注中引入的歧义就越多。正如你将在本章学到的那样,即使看似相对简单的任务也可能包含隐藏的主观性层次。
 - 
难度:本质复杂或难以理解的任务可能导致数据标注中的歧义。如果一个任务太难,标注者可能难以正确理解或完成它,从而导致不一致或不准确的标注。
 - 
歧义:某些任务或数据集本身具有歧义性,这意味着存在多个有效解释的空间。这种歧义可能导致数据标注的不一致性,因为不同的标注者可能会以不同的方式解释相同的数据。
 - 
静态标注与可变标注:在静态标注中,每个数据点被分配一个单一且不变的标签。相比之下,可变标注允许标签根据上下文或附加信息而变化。可变标注可能会引入歧义,因为同一数据点在不同的上下文中可能被标注为不同的标签。这种标注不一致性也可能随着标注者对任务越来越熟悉而出现,这导致他们改变对标签定义的认识。
 
现在我们将介绍我们实现准确和一致标签的框架。该框架专门设计用于识别或防止由这七个常见的标注挑战产生的问题。
设计高质量标签的框架
由人类进行的标注和审查可能劳动密集且容易受到人为错误和不一致性的影响。因此,目标是构建既准确又一致的语料库,要求标签达到准确度标准,同时确保不同标注者的结果处于同一范围内。
这些目标乍一看可能很明显,但现实中,让人类标注者达成一致意见可能非常棘手。除此之外,我们还需要验证共识意见没有受到某种形式的偏见。
我们实现高质量人工标注的框架由六个维度组成。在深入解释如何实现它们之前,我们将简要总结这些维度:
- 
清晰的指示:为确保标签质量,标注任务的指示必须明确且无歧义。标注者应清楚地了解对他们期望的内容,包括关于任务、标注标准以及正确标注数据的示例的详细信息。
 - 
动机一致:标注者的动机应与获得高质量标签的目标一致。这可能包括奖励准确性、提供反馈以及创造鼓励细致工作的环境。当标注者感到他们的工作是宝贵的并且得到认可时,他们更有可能产生高质量的标签。
 - 
主题专家(SMEs):利用对主题领域有专长的标注者可以显著提高标签的质量。这些人拥有对数据背后背景的深入知识和理解,使他们能够识别其他人可能错过的细微差别和细微之处。
 - 
迭代协作:通过迭代协作的过程可以实现高质量的标签。应鼓励标注者进行沟通和协作,根据集体反馈和讨论重新审视和改进他们的标签。
 - 
思维多样性:一个多元化的标注者群体会为任务带来不同的观点和解释,这可能导致更全面和稳健的标签。思维多样性有助于发现盲点并减少标注过程中的偏见。
 - 
处理歧义:由于歧义是任何数据标注任务固有的,因此对标注者进行如何处理模糊情况培训对于实现高质量标签至关重要。这可能包括寻求额外信息、咨询同行或主管或遵循预定义的模糊情况规则等策略。
 
让我们详细探讨这六个维度,以了解如何建立卓越的标注流程。这一切都始于确保我们对标注者的说明清晰明了。
设计清晰的说明
虽然看起来很明显,标注任务应该附带清晰的说明,但正如你将在本章中了解到的那样,这并不一定是一项容易的任务。分配不仅应该对创建它们的人清晰,而且更重要的是,对将执行它们的人也要清晰。
这个挑战有三个组成部分:首先,说明应包含执行任务所需的具体细节,无论谁执行这些任务。其次,指令设计应包括在分配过程中早期和持续地发现指令问题的方法。第三,我们必须确保我们的标注者具备足够的资格和动力来完成这项任务。
McInnis 等人(2016)1 研究了如果我们不管理一个或多个这些组成部分可能会出现的问题。研究人员检查了请求者和标注者之间指令不明确和动机不一致对亚马逊机械 Turk(AMT)的影响。
他们发现,经验丰富的标注者会使用各种工具和技术来评估作业的质量以及发布作业的请求者的可靠性,在承担新项目之前这样做。他们这样做是为了选择明确定义的作业,可以无障碍地完成,同时避免那些支付很少或影响他们声誉的任务。
基本上,好的标注者会绕过不明确的作业,以避免迭代任务或不公平拒绝已完成的工作(标注者的工作被拒绝会导致未支付工作费用)。作者发现,标注者在决定是否接受作业时,通常会在任务说明中寻找以下风险因素:
- 
任务或界面设计中的缺陷
 - 
评价标准不明确
 - 
对拒绝的不响应、任意的解决方案
 - 
请求者信息不足
 - 
缺乏经验和不熟悉的请求者
 - 
返利差的任务
 - 
优先考虑效率而非质量
 
作为数据专业人员,我们的工作是提供减轻这些七个因素的作业说明,无论我们是在使用众包标注者还是在内部 SMEs。然而,有时你只有在实际使用中才会知道你的说明是否清晰。
因此,问题是:我们如何最好地在请求者和标注者之间对任务的理解进行对齐,同时确保我们有合适的人在工作?
Liu 等人(2016)2 为此目的开发了一种最佳实践方法,称为 Gated Instruction。这项技术用于培训标注者,在请求者和标注者之间对任务的理解进行对齐,并识别表现不佳的工人。
Gated Instruction 基于这样一个观点:当人们得到关于他们表现的反馈时,他们学得更好。这是通过提供一个交互式教学环境来实现的,用户可以在其中接收关于他们标注的反馈并相应地调整他们的方法。目标是创建一个系统,可以以最小的用户努力提供准确的标注。
Gated Instruction Crowdsourcing Protocol 是一个简单且可推广的三阶段流程,旨在确保数据标注的质量。它包括一个交互式教程、筛选问题和持续筛选的问题批次。作者如下描述了该协议:
第一阶段 – 交互式教程
这一阶段涉及一个全面的教程,解释手头的任务:
- 
工人被赋予了每个关系的明确定义和标记标准。
 - 
工人通过标注说明每个关系的句子来练习。
 - 
每个练习句子之后都提供即时反馈,以指导工人。
 
第二阶段 – 筛选问题
这一阶段旨在评估工人对任务的理解:
- 
工人被要求标注一组五个代表性的黄金标准问题。
 - 
对每个问题提供反馈,以帮助工人了解他们的错误。
 - 
在这些问题的多数失败者将被排除在剩余流程之外。
 
第三阶段 – 问题批次(持续筛选)
这一阶段专注于在任务执行过程中保持高质量的工作:
- 
金标准问题被包含在任务中,但没有提供反馈。
 - 
每个包含 20 个问题的批次中包含 5 个金标准问题,其频率以指数级下降。
 - 
在最后 10 个金标准问题中得分低于 80%的工人将被淘汰。
 
一般原则
这些原则确保了过程的完整性和效率:
- 
只有那些 AMT 声誉超过一定阈值的工人才能被接受。
 - 
在整个任务中提供了关系的定义链接,以便快速参考。
 - 
工人在继续之前必须纠正反馈中指出的任何错误。
 - 
在每个批次之后,都会提供到目前为止的收入和金标准问题的表现反馈。
 - 
工人在完成所有 10 个批次后都会得到奖金提醒,这鼓励他们持续保持高质量的工作。
 
Gated Instruction 比传统的指导和筛选方法提供更准确的结果,因为用户能够收到对其标注的反馈并根据此调整方法。使用这种方法,作者在同一数据集上提高了精确度从 0.50 到 0.77,召回率从 0.70 到 0.78。这是与传统指导方法相比的结果。
这两个研究使用了众包标注员来进行他们的研究,但当你的标注员不是众包工人,而是与你有更紧密工作关系的专家知识(SMEs)时,相同的框架仍然高度相关。
例如,如果你从内部同事的池中挑选标注员,你可能基于某人的特定专业知识、经验年限和对贡献项目的兴趣来做出选择。正如你将在下一节中了解到的那样,标注员完成任务的动力可以极大地影响数据的收集,以及最终你的模型在该数据上的表现。
当我们讨论如何激励标注员以及如何使用专家知识(SMEs)进行更复杂的标注任务时,让我们基于这些原则来展开。
对齐动机和使用专家知识(SMEs)
尽管技术在自动化数据收集方面取得了巨大进步,但人类数据收集者在各个领域仍然占据着至关重要的地位。他们带来了机器无法复制的理解力、同理心和判断力。例如,人类数据收集者和标注员可以解释语言、语境和情感中的细微差别。同样,他们可以与受访者互动,建立联系,并鼓励更开放和诚实的回答。
人类数据收集者面临的一个重大挑战是保持他们的动力和参与度与数据的次要用途保持一致。让我们讨论四个通常导致这一问题的因素。
#1 – 缺乏目的
如果数据收集者不理解他们工作的意义,他们可能会感到与工作脱节且缺乏动力。数据收集者可能会问自己,“如果没有人会使用这些信息,我为什么要收集这些信息?”
解决方案:传达整体情况,并展示每个数据点如何成为机器学习解决方案的有价值构建块。
我们开始任何数据收集和标注练习时,都假设工作人员是聪明的,能够理解收集丰富、无偏见信息的重要性。他们明白,任何数据质量上的妥协都可能对其未来使用产生负面影响。我们的角色随后变为阐明良好数据收集的重要性,并解释其预期用途。这种阐述将显著减轻与数据质量相关的问题。
几年前,Manmohan 和 Jonas 进行了一系列需要一线员工通过客户访谈收集大量信息的机器学习项目。这些访谈已经进行了多年,所以我们已经收集了大量的数据,但这些信息是以对话格式以文本形式捕获的,因此难以进行统计分析。
我们希望使这些数据更容易用于机器学习目的,因此我们召集所有一线员工进行了一次关于数据质量重要性的演示和研讨会。在向这些数据收集者展示我们如何使用他们收集的数据来构建特定的机器学习解决方案后,他们惊讶地了解到他们的工作可以产生多么大的影响。
如一位团队成员所说,“如果我知道我收集到的信息可以帮助成千上万的客户,我会投入更多的精力去关注细节。”
在研讨会上,我们通过创建一系列模板化问题,让数据收集者承担起责任,这些问题将提高收集数据的准确性和信号。我们的一线同事为改善他们为公司及其客户收集数据的方式而感到自豪。他们对数据质量和“他们的数据”产生的结果有了转变后的所有权感。
我们也很高兴,因为数据质量的提升最终使我们的模型在某些情况下准确率提高了高达 40%。
#2 – 数据任务的繁琐性质
数据收集和标注可能是重复和单调的,导致无聊和缺乏参与感。这创造了一种内在的激励,即以最小的努力完成任务。
解决方案:我们采用以下四种策略来减少数据收集和标注的枯燥部分:完善我们的问题、简化流程、消除不必要的数据,并在可能的情况下进行自动化。
有时候,通过教授数据收集者提出更好的问题,你可以产生巨大的影响。更具体和有针对性的问题通常会得到更有意义和吸引人的回答。这提高了数据质量,同时使数据收集过程更有趣且不那么重复。
简化流程可能意味着优化工作流程,使用更用户友好的软件,或为参与数据收集的人员提供清晰的指示和培训。通过使过程更加直接,我们可以减少个人的认知负荷,使任务不那么令人疲惫。
如果你像大多数数据专业人士一样,丢弃或限制数据收集的想法可能会引起一定程度的焦虑。然而,我们必须认识到,数据收集并不意味着囤积你遇到的每一份数据,尤其是如果它妨碍了保持良好的用户体验。实际上,收集不必要或不相关的数据可能会增加任务的单调性,并产生阻碍数据分析的杂乱。因此,识别并仅关注真正与你的研究问题或商业目标相关的数据至关重要。
我们最后的简化策略,即自动化,通常是解决数据收集繁琐问题的绝佳解决方案,但我们更倾向于在用尽其他三种策略之后再引入自动化。没有必要自动化那些应该以不同方式完成或根本不应该做的事情。
例如,抓取工具可以从网站或文档中自动提取大量数据,基于规则的逻辑或机器学习算法可以对数据进行标记和组织。你将在本书的编码章节中学习一些这些自动化技术。
不久前,我们与一家大型企业合作,构建了关于呼叫者购买投资产品倾向性的实时预测模型。该企业希望他们的呼叫中心工作人员在通话期间就能获得这一预测结果,以便他们能为呼叫者匹配到合适的投资专家。
这些筛选通话通常需要 15 到 20 分钟才能完成,呼叫中心工作人员会艰难地逐页浏览与呼叫者个人详情、人口统计、投资经验和风险偏好相关的问题。
虽然收集了大量的数据,但这些数据并不是构建预测所需的那种信息。我们确定了一些补充数据点,这些数据点对于可靠地确定某人的产品需求至关重要。然而,将这些数据点作为额外问题添加到数据收集过程中是不切实际的,因为筛选通话已经很长且很繁琐。为了获取所需的信息,我们必须为呼叫者、呼叫中心工作人员和数据科学家创造一个双赢的局面。
当我们与呼叫中心团队一起讨论这个挑战时,我们发现大约 20%的现有问题可以通过增强来使对话更加简洁,而另外 15%的问题可以完全删除,因为它们捕捉到了无关或重复的信息。最后,10%收集到的数据可以根据呼叫者的其他问题的答案预先填写或推导出来。
在实施这些变化后,即使我们在流程中增加了五个新问题,平均通话处理时间也下降了大约 30%。通话时间的缩短使得每天可以处理更多的通话,从而增加了转化为付费客户的机会——这对所有相关人员都是双赢。
#3 – 缺乏激励
没有适当的奖励或认可,数据收集者可能缺乏最佳表现的动机。例如,如果一个人被收集到的数据点的数量而不是单个响应的质量所激励,可能会导致任务完成得更加肤浅。
解决方案:当我们专注于以下四个不同领域的激励数据收集者时,我们看到了最佳的结果:
- 
展示什么是好的样子:首先,设定清晰的高质量工作标准至关重要。大多数人都会为自己的工作感到自豪,并在理解他们的努力的价值和影响时努力表现良好。通过定义数据收集或标注任务的宗旨和重要性,并展示优秀工作的例子,你为团队提供了一个具体的目标。这种清晰度有助于确保收集到的数据是稳健的、相关的,并且富含有价值的见解,同时最大限度地减少噪音。
 - 
创造互利共赢:其次,培养数据收集者和数据使用者之间的共享成功感至关重要。当数据收集者了解他们的努力如何对大局做出贡献——可能是推动关键业务决策或推动创新项目——他们更有可能感到对工作有所投入。这种所有权和贡献感可以显著提高数据收集的质量。
 - 
提供非货币奖励:关注的第三个领域是非货币奖励。认可和感激是强大的激励因素。游戏化可以在这方面成为一个非常有效的工具,将数据收集过程转变为引人入胜的竞赛。实施如公开显示的排行榜、徽章或积分等特性,可以培养成就感,并在团队成员之间鼓励健康的竞争。长期存在的“月度员工”概念不仅奖励了卓越的表现,也为其他人树立了一个值得追求的标准。
 - 
提供经济奖励:最后,经济奖励可以是一个强有力的激励因素。将薪酬与工作质量挂钩可以促使个人达到或超过设定的标准。然而,这种方法需要一个明确的框架来概述绩效期望和绩效的客观衡量标准。同时,你还需要有能力影响某人的薪酬,如果你的注释员和数据收集员是内部员工,这可能很困难。虽然并非总是可行,但在可用时,经济激励可以成为一个强大的动机因素。如果它们不是一种选择,那么在其他三个动机领域加倍努力仍然可以产生令人印象深刻的结果。
 
总之,激励数据收集员和注释员做好工作是多方面的。它需要清晰沟通、互利、认可和适当的补偿的混合。
#4 – 工作环境
压力大或缺乏支持的工作环境也可能影响动机水平。如果一个数据收集员或注释员有许多相互竞争的优先事项,他们可能会对数据收集采取“差不多就可以了”的态度。
解决方案:为注释员和数据收集员创造一个适合专注于手头主要任务的合适工作环境基本上要求你移除任何不必要的任务或阻碍某人完成工作的摩擦。
我们通常拒绝参与数据科学项目,除非项目所需的所有人员都有动力参与,并在整个项目期间保持可用。这通常需要我们与小专家、数据收集员及其经理协商,在日历上预留专门的时间。
为数据收集员提供反馈机制也很重要。这样,他们可以参与到过程中,并指出不工作或可以做得更好的地方,例如可以以不同方式表述的问题,改变问题的顺序或任务,混淆的标签要求等。
为注释员和数据收集员提供必要的工具和资源,以便他们迅速有效地完成工作。例如,数字化数据收集,尽可能自动化数据录入,条件格式化下拉选项等。
项目参与者之间的持续和及时反馈也是至关重要的。在项目后期对工人进行负面反馈可能会令人沮丧,并产生反效果。相反,在整个过程中进行合作并提供反馈,以确保从一开始就保持一致。假设过程中可能会有一些小问题,并准备好积极引导团队通过这些问题。相反,如果你假设每个人都已经按照预期理解了每项指令,那么你就是在为自己设置失败。
有些人不认为这种工作属于数据科学领域。在我们看来,这正是许多数据专业人士的不足之处。重要的是要认识到人类行为和偏见可能是数据科学项目的严重限制因素,你希望尽可能减少这种影响。如果你不面对这些非技术障碍,你最终会得到更差的数据。
这将我们引向框架的下一个维度:迭代协作。
迭代协作
迭代协作应该是人类标注任务的核心策略。基本上,它包括创建一个持续的反馈和微调数据标注过程的流程。以下是实施数据标注协作方法的三个指导原则,这些原则得到了数据标注平台 SUPA 最佳实践方法的验证。
从小规模开始,尽早解决任何问题
使用较小的数据集启动数据标注过程是一种实用方法。与其从数千个观察值开始,不如从 50 个观察值的校准批次开始。这个可管理的数据集允许你审查标签,发现潜在问题,改进说明,并向标注员提供反馈。
重复此过程,直到你确信你已经遍历了整个数据集的代表性样本,已经识别出大多数边缘情况,并且你的标注员能够一致地完成任务。
相同观察中的标注不一致表明需要修订规则。如果标注员对指南有不同的理解,那么了解原因是否是不清晰的标注规则、个别标注员对理解和经验的差异,或者完全是其他原因,是值得的。换句话说,你的标注说明永远不会是一劳永逸的。它们应该始终处于监督之下,并随着项目需求的发展而演变。
可视化好的样子
标注规则有时可能含糊不清,导致主观解释和不一致。例如,在图像标注练习中,一条规则如“只有当大部分内容可见时才进行标注”可能被不同的标注员以不同的方式解释。因此,重要的是要直观地展示什么是好的。
可视化在数据标注过程中非常有价值,因为它们为标注员提供了明确的指导,说明标注对象应该如何呈现。我们建议展示如何正确执行标注任务的示例,同时也展示错误的示例。通过提供良好和不良标注规范的视觉说明,标注员能够更深入地理解任务,从而提高他们的生产力和精确度。
对边缘情况要非常具体
边界情况是指偏离常规的情况,由于主观意见可能导致标签不一致。例如,玩具车应该被标记为汽车吗?双人自行车是一辆还是两辆自行车,或者它属于一个单独的分类?
为了有效地管理边界情况,你需要有一个机制让标注者标记这些项目以供进一步考虑。如果你没有建立反馈机制,标注者很可能会现场自行做出判断以完成任务。
Pradhan 等人(2022)4 提出了一种所谓的 FIND-RESOLVE-LABEL 工作流程,用于众包标注,旨在解决这三个迭代步骤。FIND-RESOLVE-LABEL 工作流程是一种指导性标注过程,旨在揭示特定标注任务及其相关说明中的模糊性。
例如,一个标注任务可能是识别包含女性的图像。表面上,这个任务看起来相当简单,但现实中,标注者很快就会面临模糊性。例如,什么时候某人算作女性,什么时候算作女孩?这甚至重要吗?一个女性的雕像或《蒙娜丽莎》画作——这算数吗?如果女性只部分可见呢?
实际上,为特定的标注任务提供高质量的说明可能非常困难,因为可能有很多维度需要考虑。
FIND-RESOLVE-LABEL 工作流程旨在在标注练习的开始阶段发现并消除这些模糊性。它由三个关键组件组成,这些组件协同工作以简化数据标注过程:
- 
发现:在这个初始阶段,标注者会收到标注说明,并要求根据这些说明识别模糊的示例。对于每个识别出的示例,标注者还被要求提供一个概念标签,解释为什么选择了特定的标签。这允许收集标注决策背后的推理和概念思考,然后可以将其反馈到改进的标注说明中。
 - 
解决:一旦确定了数据点,下一步就是解决数据中的任何模糊性或冲突。这可能需要领域专业知识来做出明智的决定,如何解决不一致或缺失信息。
 - 
标注:最后,在解决任何问题后,数据点被适当地标注,确保高质量的标注,这些标注可以用于训练机器学习模型。
 
普拉德汉和他的团队发现,在标注过程中关注最不明确的数据点并澄清它们,可以大大提高数据质量。他们注意到,在某些模糊的场景中,许多标注者同意的答案与请求者认为正确的答案不同。这意味着即使使用智能答案聚合方法,也存在为这些任务获得错误标签的风险。
有趣的是,该研究还发现,工人可以正确地标注与主要概念密切相关的研究结果。这表明我们可能不需要在任务中解释每一个可能的歧义,因为精心挑选的一组例子可以帮助团队正确地标注其他不明确的例子。
考虑到这些发现,让我们以数据为中心的方式探讨如何处理标注员之间的歧义。
处理歧义和反映多样性
在人类标注员的帮助下,我们可以生成信息极其丰富的数据集,但有时这需要我们以创新的方式解决歧义。同时,歧义可能很难被发现。一个人认为明显的事情,另一个人可能觉得完全令人困惑。
公司和研究人员使用内部员工、志愿者或众包平台(如 AMT)以可承受的价格获取人类标注员。这些标签员来自不同的背景,携带不同的偏见,所有这些都可能影响标注的质量——尤其是在涉及判断因素时。
随着人工智能和机器学习被用于从可以根据上下文和解释者不同而具有不同解释的数据集中分类和生成新内容,这一挑战只会日益加剧。这一点在 Sap 等人(2019)的研究论文《仇恨言论检测中的种族偏见风险》中得到了体现,该论文调查了标注员对方言差异的敏感性不足如何导致自动化仇恨言论检测中的种族偏见。
论文指出,即使是专门为检测仇恨言论而设计的数据集,也包含对特定群体或少数族裔语言的固有偏见。这是因为底层参数是基于标注员的偏好创建的,而这些标注员可能没有意识到不同民族或语言之间细微差别的微妙之处。
例如,一些民族或社会群体可能使用一些口语,这些口语对其他群体的人来说可能显得粗鲁或冒犯。例如,研究人员发现,标签员倾向于将非裔美国英语中的短语标记为比使用通用美国英语的短语更具毒性。
这些固有的偏见难以避免,因为标注员很少是完全的领域专家,而是遵循一般指令的人。同时,标签员不太可能成为普通公众的代表样本。例如,AMT 参与者的大多数历史上是未婚且无子女的年轻人。绝大多数 Turkers 来自仅两个国家,即美国和印度,而来自全球南部的不到 2%。
这个问题不仅仅与仇恨言论的主题有关。因此,在确定任何需要主观解释的标签时,某些方言、生活方式、文化背景和世界观可能被过度代表,而其他则可能被代表性不足。由此导致的数据收集不佳可能导致标签与它们旨在代表的现实世界场景之间的差距增大。
数据标注中存在的失衡在一些全球最广泛使用的公共训练数据集中表现得非常明显。研究发现,其中两个最常用的数据库,ImageNet 和 Open Images,倾向于美国和欧洲,这一点从这些数据集创建的模型在来自全球南部的图像上的性能较差可以明显看出。
例如,当新郎的图片来自埃塞俄比亚或巴基斯坦时,与来自美国的相似图片相比,其准确性评级较低。这种特定的差异是由于“婚礼”和“香料”等物体根据其文化背景被解释的方式不同,公开可用的识别系统在来自美国或欧洲以外的国家时,往往难以正确分类它们。
理解处理标注中模糊性的方法
标签往往是绝对的,但观点不是。同时,人类在更抽象的标签应该是什么的问题上往往会意见不一。作为以数据为中心的从业者,我们应该预见标注者之间的模糊性和分歧,并制定管理它的计划。
值得注意的是,我们实际上希望出现模糊性,这样我们才能以正确的方式处理它。模糊场景可能源于标注指令不明确,但它们也可能揭示出必须包含在我们数据集中的新标签。因此,我们应该努力设计我们的标注团队,以最大化我们能够揭示出任何存在的分歧的可能性。
做这件事的一个好方法就是涉及一个更加多元化的标注者群体,他们更能适应语言和观点等元素之间的差异,但在这样做的同时,我们也希望提升少数群体的意见。斯坦福大学的研究人员 Gordon 等人(2022)8 提出了一种旨在实现此目的的方法,称为 陪审团学习。
在 Gordon 等人(2021)9 的先前研究中,发现当在评论毒性标注任务中考虑非多数群体的标签时,分类器的性能从 0.95 ROC AUC 下降到 0.73 ROC AUC。这意味着当应用于非多数群体的人的评论时,分类器并不那么有效。换句话说,不可能让每个人都达成一致,因此我们应该考虑我们听取的是谁——不一定只是简单的多数。
陪审团学习与更直接的汇总或多数投票方法形成对比。陪审团学习不是基于多数规则或概率来建立标签,而是积极使用不同的意见来挑选出潜在的偏见并压制少数意见。它被提出作为一种将不同意见的声音整合到机器学习系统中的方法,以防止它们过度依赖单一意见或观点。以下是它是如何工作的。
陪审团学习是一种监督机器学习(SML)方法,通过陪审团的隐喻明确解决分歧。这种方法允许实践者指定他们的分类器反映了哪些声音,以及以何种比例。陪审团学习的目标是定义哪些人或群体决定了系统的预测以及以何种比例,从而允许开发者分析——并可能减轻——模型中可能存在的任何潜在偏见。
为了有效地使用陪审团学习,实践者必须首先确定他们希望将其包括在模型中的陪审员。这可以通过选择代表手头问题不同视角的个人来完成。
例如,假设任务是标记电影是否好。实践者可以从不同的群体中选择陪审员(标记者),例如年龄、性别、种族、地点、政治倾向或社会经济地位。一旦选定了陪审员,他们就必须通过回答关于电影是否好以及为什么好的问题,向模型的预测提供输入。
一旦所有陪审员都向模型的预测提供了他们的意见,实践者就可以使用这些数据为每个个案创建一个汇总预测。通过考虑每个个案的多个视角,实践者可以确保他们的模型比仅依赖单一视角进行预测时更加准确和较少偏见。
最后,实践者还可以通过比较由不同背景或视角的个人组成的陪审团所做的汇总预测,使用陪审团学习来分析他们模型中存在的任何潜在偏见。这种比较有效地为我们提供了一个预测范围,而不是二元标签。分析可以帮助识别任何可能存在偏见的领域,并允许实践者相应地调整他们的模型,以减少任何潜在的偏见并提高整体准确性。这个过程在以下图中得到了说明:

图 4.1 – 来自 Gordon 等人(2022 年)的陪审团学习过程概述
到现在为止,你可能已经注意到陪审团学习并不简单。与许多以数据为中心的方法一样,陪审团学习需要广泛的规划和协调,以确保所有参与者都清楚地了解过程的期望和指南。这可能是一个耗时且资源密集的过程。
陪审团学习也需要一定规模的标注者池,以确保意见和观点的必要多样性。这可能会是一个挑战,尤其是对于资源有限的项目来说。
实施陪审团学习带来的另一个挑战是需要关于标注者的元信息。为了确保结果既准确又可靠,陪审团学习需要具有不同技能和背景的标注者。收集这些信息并开发符合要求标准的标注者池可能是一项困难的任务,这又需要提前规划。
尽管存在这些挑战,陪审团学习为从业者提供了一种创新且有效的方法,将不同意见的声音纳入机器学习模型,同时帮助他们识别和减轻模型中可能存在的任何潜在偏见。通过在做出预测和分析模型中存在的潜在偏见时考虑多个观点,从业者可以确保他们的机器学习模型在处理容易受到主观性影响的重要任务时既准确又无偏见。
为了结束这一章,让我们探讨如何从统计上衡量标注者之间的歧义或不一致。
衡量标注一致性
到目前为止,我们已经讨论了一系列创建一致性和高质量标注的工具和技术。虽然这些元素为良好的数据集奠定了基础,但我们还希望能够衡量我们的标注者是否表现出一贯性。
为了衡量标注者的一致性,我们建议使用两种标注一致性的度量,分别称为观察者内部和观察者之间的可变性。这些是临床研究中的标准术语,指的是同一观察者(内部)或不同观察者(外部)在不同测量或评估中的同意程度。为了简化解释,可以将“观察者”视为与“标注者”、“注释者”、“评分者”、“数据收集者”以及我们在本章中使用的任何其他类似术语可以互换。
虽然观察者内部和观察者之间的可变性都与测量一致性相关,但它们解决的是不同的方面。观察者内部的可变性指的是单个观察者在一段时间内的稳定性,而观察者之间的可变性指的是不同观察者之间的稳定性。诸如培训、经验和协议标准化等因素可以显著影响这两者。
跟踪观察者差异至关重要,因为它直接影响您输入数据集的质量和可靠性,从而影响您的模型。如果同一对象被不同的观察者(观察者间差异)或同一观察者在不同时间(观察者内差异)解释不同,可能会导致标签不一致,从而影响机器学习输出的整体质量。
几个因素导致了观察者之间的差异,包括测量技术缺乏标准化、观察者疲劳以及主观解释。例如,某人的判断可能受到其经验水平、个人偏见或甚至观察时的心理状态的影响。
重要的是要注意,两个或更多观察者之间的标签差异并不一定意味着其中一个观察者是正确的,而其他人是错误的。标签上的分歧可能仅仅意味着被标记的对象或情况是模糊的或短暂的,因此难以给出明确的标签。
例如,两位医生可能会根据医学影像或症状描述对疾病的相对进展意见不一致。这可能是因为诊断不确定,而不是一位医生比另一位医生更正确。
测量差异的常见技术是类内相关系数(ICC)。ICC 是一种统计工具,用于评估一个或多个观察者对同一实体进行标注的一致性或一致性。与常用的皮尔逊相关系数不同,后者测量变量之间的线性关系,ICC 评估同一组数据中评分的可靠性。当我们想知道同一组别中的单位之间相似性有多强时,它特别有用。
一个接近 1 的高 ICC 值表明同一组别中的值之间具有高度相似性。相反,一个低的 ICC 值表明评分之间的一致性较低。
ICC 有不同的形式,每种形式适用于特定的情况。例如,某些形式更适合当我们从每个受试者那里得到单一测量时,而其他形式更适合于几个测量的平均值。形式的选择取决于您研究的性质和您拥有的数据类型。
以下截图显示了 Shrout 和 Fleiss(1979)10 提出的六种常见定义:

图 4.2 – 六种常见的 ICC
为了让我们对 ICC 分数的使用有更直观的理解,让我们通过使用Pingouin Python 包的实例来实际操作。Pingouin 是一个开源包,具有大量有用的统计功能。它主要利用 pandas 和 NumPy,所以请确保您已经安装了这些。
对于我们的示例场景,假设我们有四位品酒评委通过给八种不同的葡萄酒评分 0 到 9 来评价其质量。我们想知道这些评委是否对葡萄酒的评分是一致的。评委的评分显示在以下屏幕截图:

图 4.3 – 来自四位不同评委的品酒评分
- 
一般而言,可能发生三种类型的变异性:
 - 
由于评估对象之间的差异引起的变异性(假设两种不同的同一种葡萄酒样本有略微不同的口感)
 - 
由观察者的评估引起的变异性,例如,评委 B 和 C 对葡萄酒 #3 评分的差异
 - 
标签使用的变化;例如,每个人都认为葡萄酒 #1 是最差的,但已经使用了三种不同的评分来对其进行评价
 
ICC 计算将考虑所有这些因素,因为它基于 方差分析(ANOVA)分析。在我们计算 ICC 之前,我们必须首先确定我们追求的是哪种类型的衡量标准。我们可以使用以下屏幕截图作为选择我们情况中正确 ICC 形式的指南:

图 4.4 – ICC 模型选择指南
在我们的场景中,以下适用:
- 
每位评委都只对每款葡萄酒进行过一次评分,因此我们可以确定所有受试者都由同一组观察者进行了评估
 - 
在这种情况下,我们将假设四位评委是从更大的潜在评委池中随机选择的
 - 
我们对个别观察者的可靠性感兴趣,而不是平均可靠性
 
因此,我们正在寻找使用 ICC2 计算来确定我们的 可靠性分数。
我们随后使用以下脚本在我们的葡萄酒评分上运行 ICC 函数。Pingouin ICC 操作符 intraclass_corr 将计算并展示所有六种常见的 ICC 衡量指标,但我们只对 ICC2 感兴趣:
import pingouin as pg
data = pg.read_dataset('icc')
icc = pg.intraclass_corr(data=data, targets='Wine', raters='Judge',
                         ratings='Scores').round(3)
icc.set_index("Type")
这会产生以下输出。我们的 ICC 分数为 0.728,这意味着我们的评委在评分上达成中等程度的共识:

图 4.5 – 输出表
重要的是要理解,没有严格的阈值来界定“可接受”的 ICC 分数。虽然没有固定的基准,但一些一般性指南可以帮助解释 ICC 分数。这些范围不是绝对的,应该根据你具体的研究或分析来解释:
- 
ICC 小于 0.5:这个范围通常被认为是表明可靠性差。例如,如果你有一组评分的 ICC 为 0.3,这表明评分者之间达成一致的程度较低。
 - 
ICC 在 0.5 到 0.75 之间:这个范围内的分数通常被认为是表现出中等可靠性。
 - 
ICC 在 0.75 到 0.9 之间:这些分数表明良好的可靠性。例如,如果你达到 ICC 为 0.8,这表明你的评分者之间达成高度的一致。
 - 
ICC 大于 0.90:这个范围表示出色的可靠性。例如,ICC 为 0.95 表示评分者之间几乎完全一致。
 
在解释 ICC 分数时,考虑几个可能影响其可靠性的因素也同样重要。这些因素包括以下内容:
- 
样本大小:与许多统计指标一样,ICC 对样本大小也很敏感。较大的样本大小往往能提供更可靠的 ICC 估计。
 - 
数据结果范围:ICC 分数也可能受到被评估结果的可能范围以及确定注释的难度的影响。例如,如果我们的葡萄酒评委只能将葡萄酒标注为“好”或“坏”(1,0),而不是一个范围(0–9),那么这可能会改变最终的 ICC 分数。
 - 
受试者变异性:ICC 也受到受试者之间变异性的影响。即使评分者在评分上保持一致,高变异性也可能导致 ICC 值降低。
 
在实践中,解释 ICC 分数需要理解你的研究背景、数据性质以及所使用的特定 ICC 形式。在解释和传达你的结果时,始终考虑这些因素。
记住——ICC 分数只是拼图中的一块。它们应该与其他统计指标和见解结合使用,以提供对数据的全面理解。让我们总结一下本章到目前为止所涵盖的内容。
摘要
在本章中,我们考察了人类在确保数据质量方面发挥的关键作用,尤其是在数据标注的初始阶段。我们认识到,虽然人类标注者是不可或缺的,但他们也带来了一些挑战,包括偏见和不一致性。
为了解决这些问题,我们探索了各种策略来有效地训练标注者以开发高质量的数据库。关键要点是,经过良好训练且拥有明确指示的标注者可以显著提高数据的整体质量。
改进任务说明成为了一个反复出现的主题,强调了其在促进标注过程中的重要性。迭代协作也被强调为一种基本实践,通过反馈和改进促进持续改进。
到本章结束时,你应该已经全面理解了为什么在以数据为中心的模型构建中,人类参与至关重要,人类标注者所面临的挑战,以及克服这些挑战的实际方法。更重要的是,你将学会如何使用特定的框架来实现高质量的标注,为成功的 ML 项目打下坚实的基础。
在下一章中,我们将在这些技能的基础上进一步深入探讨数据清洗和增强的技术方面,然后再探讨第六章中程序化标注技术,即机器学习中的程序化标注技术。现在是深入代码的时候了!
参考文献
- 
McInnis B., Cosley D., Nam C., Leshed G., 围绕拒绝、不信任、风险和亚马逊机械师图灵平台工人体验进行设计,信息科学法学院,康奈尔大学。
dl.acm.org/doi/epdf/10.1145/2858036.2858539 - 
Liu A., Soderland S., Bragg J., Lin C. H., Ling X., Weld D. S., 有效的大规模标注用于关系抽取,图灵中心,华盛顿大学计算机科学与工程学院。
aclanthology.org/N16-1104.pdf - 
www.supa.so/post/iteration-a-key-data-labeling-process-often-overlooked,2023 年 7 月 30 日查阅。 - 
Pradhan V. K., Schaekerman M., Lease M., 2022,寻找歧义:为众包工人澄清标注指南的三阶段工作流程设计,前沿人工智能,2022 年 5 月 18 日,机器学习与人工智能卷 5。
doi.org/10.3389/frai.2022.828187 - 
Sap, M., Card, D., Gabriel, S., Choi, Y., Smith, N. A., 仇恨言论检测中的种族偏见风险,保罗·G·艾伦计算机科学与工程学院,华盛顿大学,西雅图,美国,机器学习系,卡内基梅隆大学,匹兹堡,美国,艾伦人工智能研究所,西雅图,美国
 - 
venturebeat.com/business/the-ai-industry-is-built-on-geographic-and-social-inequality-research-shows/,2023 年 4 月 22 日查阅。 - 
venturebeat.com/ai/mit-researchers-find-systematic-shortcomings-in-imagenet-data-set/,2023 年 4 月 22 日查阅。 - 
Gordon M. L., Lam M. S., Park J. S., Patel K., Hancock J., Hashimoto T., Bernstein M. S., 2022. 陪审团学习:将不同意见融入机器学习模型. 在 CHI 计算机系统人因工程会议 (CHI ‘22),2022 年 4 月 29 日至 5 月 5 日,新奥尔良,LA,美国. ACM,纽约,NY,美国
 - 
Gordon M. L., Zhou K., Patel K., Hashimoto T., Bernstein M.S., 2021,不一致解卷积:将机器学习性能指标与现实对齐,在 CHI 计算机系统人因工程会议 (CHI ‘21),2021 年 5 月 8 日至 13 日,横滨,日本. ACM,纽约,NY,美国,14 页。
doi.org/10.1145/3411764.3445423 - 
Shrout, P. E. & Fleiss, J. L. (1979), Intraclass correlations: uses in assessing rater reliability, Psychological Bulletin, 86(2), 420.
psycnet.apa.org/doi/10.1037/0033-2909.86.2.420,2023 年 7 月 30 日查阅。 
第三部分:提高数据质量的技术方法
在本部分,我们探讨了提高机器学习中数据质量和管理的技术方法。我们涵盖了从数据清洗、程序化标注、合成数据使用,到解决偏差和处理罕见事件等主题。每一章都为你提供了在机器学习中高效工作所需的基本技能和知识,强调了高质量数据在构建稳健的机器学习系统中的重要性。
本部分包含以下章节:
- 
第五章**,数据清洗的技术
 - 
第六章**,机器学习中程序化标注的技术
 - 
第七章**,在数据为中心的机器学习中使用合成数据
 - 
第八章**,识别和消除偏差的技术
 - 
第九章**,处理机器学习中的边缘情况和罕见事件
 
第五章:数据清洗技术
在本章中,我们将介绍数据质量的六个关键维度及其提高数据质量的相关技术,这些技术通常被称为机器学习中的数据清洗技术。简单来说,数据清洗是通过实施技术来修复数据中的错误或删除错误数据,以提高数据质量的过程。正如第一章和第二章所涵盖的,减少数据中的错误是提高模型质量的一种非常高效和有效的方法,比使用以模型为中心的技术,如添加更多数据或实施复杂算法,更为有效。
在高层次上,数据清洗技术包括修复或删除不正确、不完整、无效、有偏见、不一致、过时或损坏的数据。由于数据是从多个来源捕获的,由于不同的注释者根据他们的判断或由于系统设计不良,将这些来源结合起来通常会导致数据被错误标记、不一致、重复或不完整。正如前几章所发现的,错误的数据会使算法和结果不可靠。因此,为了在机器学习系统中实现可靠性,重要的是帮助数据科学家和数据工程师质疑数据,并使用数据清洗技术系统地提高数据质量。
本章将涵盖以下主题:
- 
数据质量的六个关键维度
 - 
测量数据质量
 - 
提高数据质量所需的六个维度的数据清洗技术
 
数据质量的六个关键维度
我们可以使用六个关键维度来检查数据的整体健康状况。确保数据整体健康可以确保我们能够构建可靠的系统并做出更好的决策。例如,如果 20%的调查数据是重复的,并且大多数重复数据是由男性候选人填写的,我们可以想象,如果数据重复未被检测到,决策者的行动将有利于男性候选人。因此,了解数据的整体健康状况对于做出可靠且无偏见的决策非常重要。为了衡量数据质量或查看数据的整体健康状况,我们可以将数据质量分解为以下维度:
- 
一致性:这指的是对于给定的列或特征,同一数据是否在行之间保持一致。一个例子可能是男性性别标签是否一致。标签可以取“1”、“男性”、“M”或“male”等值,但如果数据有多个值来表示男性,那么算法将单独处理每个标签,这可能导致随机性和错误。因此,确保一致性的目标是确保标签在整个数据集中定义一致。
 - 
唯一性:这指的是每条记录是否可以唯一识别。如果系统中有重复值,模型将因为那些记录而变得有偏见。例如,如果一个地区有高贷款批准率,由于系统故障,该地区的记录被重复,算法将偏向于批准更多该地区的贷款。
 - 
完整性:这指的是给定列或特征的数据在行之间是否完整,以及数据是否由于系统错误或未捕获而缺失,尤其是在该信息将被用于机器学习系统时。
 - 
如果一个或几个标注者认为某些郊区既不是城市也不是乡村,并违反了数据规则输入了
semi_urban,那么semi_urban可能无效。这可能会在数据中引入噪声,因此确保数据符合业务规则非常重要。 - 
准确性:这指的是数据最初是否被正确输入,并且可以从内部或外部来源进行验证。在医疗保健环境中,一个例子可能是如果入院日期和时间输入了不同的时区,那么分析和洞察将是不准确的。如果入院时间是护理质量的预测因子,那么时区的不匹配可能会导致错误的结论。
 - 
新鲜度:这指的是可用的数据是否是最近的并且是最新的,以满足数据需求。某些应用程序需要数据是实时的——也就是说,每秒更新一次——而其他应用程序需要数据每月或每几个月可用一次。昨天的事实可能因为法规、天气条件、趋势、竞争、业务变化等因素的变化而改变。
 
接下来,我们将安装各种 Python 包,然后深入到数据修复和质量测量的工作。在每个部分,我们将深入探讨不同的数据清洗技术以及如何提高数据质量。
安装所需的包
对于本章,我们需要以下 Python 包或库:
- 
pandas版本 1.5.3 - 
numpy版本 1.22.4 - 
scikit-learn版本 1.2.1 - 
jupyter版本 1.0.0 - 
alibi版本 0.9.0 - 
alibi-detect版本 0.10.4 - 
seaborn版本 0.12.2 - 
matplotlib版本 3.6.3 - 
missingno版本 0.5.1 - 
feature-engine版本 1.5.2 
接下来,我们将简要介绍数据集并开始探索数据。
介绍数据集
首先,让我们介绍我们的问题陈述。对于贷款提供者来说,确保获得贷款的人能够还款并且不会违约是很重要的。然而,同样重要的是,人们不应因为基于低质量数据训练的模型而被拒绝贷款。这就是以数据为中心的方法帮助使世界变得更美好的地方——它为数据科学家和数据工程师提供了一个质疑数据质量的框架。
对于本章,我们将使用 Analytics Vidhya 的贷款预测数据集。您可以从 datahack.analyticsvidhya.com/contest/practice-problem-loan-prediction-iii/#ProblemStatement 下载数据集。有两个文件——一个用于训练,一个用于测试。测试文件不包含任何标签。对于本章,我们将利用训练文件,该文件已下载并保存为 train_loan_prediction.csv。
首先,我们将查看数据集并检查前五行。为此,我们必须导入以下必要的包:
import pandas as pd
import numpy as np
import missingno as msno
import matplotlib.pyplot as plt
接下来,我们将读取数据:
df = pd.read_csv('train_loan_prediction.csv')
df.head().T
我们将使用 pandas 的 read_csv 方法读取数据。然后,我们将使用 .head() 方法可视化数据集的前五行。如果我们有一个大的特征集,我们可以在 .head() 方法的末尾应用 .T 方法。这将表示列作为行,行作为列,其中列名不会超过 5 个,因为我们想可视化前五行。
我们得到以下输出:

图 5.1 – 我们 df 数据集的前五行
如我们所见,列名之间存在一些不一致。所有列都遵循驼峰命名法,除了 Loan_ID,而长列名由 _ 分隔,除了 LoanAmount、CoapplicantIncome 和 ApplicantIncome。这表明列名存在不一致的命名约定。在数据中,我们还可以看到一些列具有驼峰命名法的数据,但 Loan_ID 的所有值都是大写。在 Education 列中,Not Graduate 值由空格分隔。在机器学习中,确保数据的一致性非常重要;否则,模型可能会产生不一致的结果。例如,如果 Gender 列有两个不同的男性客户值——Male 和 male,会发生什么?如果我们不处理这个问题,那么我们的机器学习模型将把 male 数据点视为与 Male 分离的,模型将不会得到准确的信号。
接下来,我们将从数据中提取列名列表,将它们全部转换为小写,并确保单词之间由一个独特的字符 _ 分隔。我们还将遍历分类列的数据值,在将所有特殊字符替换为 _ 并使我们的数据一致之前,将它们全部转换为小写。
确保数据的一致性
为了确保数据的一致性,我们必须检查 DataFrame 中列的名称:
column_names = [cols for cols in df]
print(column_names)
['Loan_ID', 'Gender', 'Married', 'Dependents', 'Education', 'Self_Employed', 'ApplicantIncome', 'CoapplicantIncome', 'LoanAmount', 'Loan_Amount_Term', 'Credit_History', 'Property_Area', 'Loan_Status']
接下来,我们必须获取所有不包含下划线的列名:
num_underscore_present_columns = [cols for cols in column_names if '_' not in cols]
num_underscore_present_columns
['Gender',
 'Married',
 'Dependents',
 'Education',
 'ApplicantIncome',
 'CoapplicantIncome',
 'LoanAmount']
由于一些列的名称中有两个大写字母,我们必须在第二个大写字母之前添加下划线。接下来,我们创建一个布尔映射,针对每个列字母的索引,将大写字母的位置映射为True,这样我们就可以定位第二个大写字母的位置,并在其前面加上下划线:
cols_mappings = {}
for cols in num_underscore_present_columns:
    uppercase_in_cols = [val.isupper() for val in cols]
    num_uppercase_letters = sum(uppercase_in_cols)
    cols_mappings[cols] = {
        "is_uppercase_letter": uppercase_in_cols,
        "num_uppercase_letters": num_uppercase_letters,
        "needs_underscore": (num_uppercase_letters > 1)
    }
然后,我们遍历映射并打印需要下划线的列名,以及打印第二个大写字母的位置:
for key in cols_mappings.keys():
    if cols_mappings[key]['needs_underscore']:
        print()
        print(f'{key} need the underscore at location ', cols_mappings[key]['is_uppercase_letter'].index(True, 1))
ApplicantIncome need the underscore at location  9
CoapplicantIncome need the underscore at location  11
LoanAmount need the underscore at location  4
使用这些信息,我们为ApplicantIncome列构建了一些逻辑:
'ApplicantIncome'[:9] + '_' + 'ApplicantIncome'[9:]
'Applicant_Income'
接下来,我们将前面的步骤结合起来,遍历需要下划线的列,构建映射。然后,我们打印需要下划线的列名。最后,我们创建旧列名和新列名的映射:
cols_mappings = {}
for cols in num_underscore_present_columns:
    uppercase_in_cols = [val.isupper() for val in cols]
    num_uppercase_letters = sum(uppercase_in_cols)
    if num_uppercase_letters > 1:
        underscore_index = uppercase_in_cols.index(True, 1)
        updated_column_name = cols[:underscore_index] + "_" + cols[underscore_index:]
    else:
        updated_column_name = cols
    cols_mappings[cols] = {
        "is_uppercase_letter": uppercase_in_cols,
        "num_uppercase_letters": num_uppercase_letters,
        "needs_underscore": (num_uppercase_letters > 1),
        "updated_column_name": updated_column_name
    }
    if cols_mappings[cols]['needs_underscore']:
        print(f"{cols} will be renamed to {cols_mappings[cols]['updated_column_name']}")
column_mappings = {key: cols_mappings[key]["updated_column_name"] for key in cols_mappings.keys()}
column_mappings
ApplicantIncome will be renamed to Applicant_Income
CoapplicantIncome will be renamed to Coapplicant_Income
LoanAmount will be renamed to Loan_Amount
{'Gender': 'Gender',
 'Married': 'Married',
 'Dependents': 'Dependents',
 'Education': 'Education',
 'ApplicantIncome': 'Applicant_Income',
 'CoapplicantIncome': 'Coapplicant_Income',
 'LoanAmount': 'Loan_Amount'}
最后,我们将列映射应用于更新列名并打印新的列名:
df = df.rename(columns=column_mappings)
column_names = [cols for cols in df]
print(column_names)
['Loan_ID', 'Gender', 'Married', 'Dependents', 'Education', 'Self_Employed', 'Applicant_Income', 'Coapplicant_Income', 'Loan_Amount', 'Loan_Amount_Term', 'Credit_History', 'Property_Area', 'Loan_Status']
尽管前面的代码可以通过手动选择列来简单地更新,但通过这种方式编程化,我们可以确保数据遵循编程规则。为了提高一致性,我们将所有列名转换为小写。首先,我们创建一些简单的单行逻辑来将列名转换为小写:
print([cols.lower() for cols in df])
['loan_id', 'gender', 'married', 'dependents', 'education', 'self_employed', 'applicant_income', 'coapplicant_income', 'loan_amount', 'loan_amount_term', 'credit_history', 'property_area', 'loan_status']
然后,我们通过传递前面的逻辑来更新列名:
df.columns = [cols.lower() for cols in df]
print(df.columns)
Index(['loan_id', 'gender', 'married', 'dependents', 'education',
       'self_employed', 'applicant_income', 'coapplicant_income',
       'loan_amount', 'loan_amount_term', 'credit_history', 'property_area',
       'loan_status'],
      dtype='object')
通过这样,我们已经确保了列名的一致性。但更重要的是,我们必须确保分类值的一致性,以便我们可以使机器学习系统免受不一致数据的影响。首先,我们提取id和target列,然后识别分类列。这些列包含非数值数据:
id_col = 'loan_id'
target = 'loan_status'
cat_cols = [cols for cols in df if df[cols].dtype == 'object' and cols not in [id_col, target]]
cat_cols
['gender',
 'married',
 'dependents',
 'education',
 'self_employed',
 'property_area']
我们遍历每个列,检查唯一值以确保值是独特的且没有拼写错误。我们还检查相同的值是否以不同的方式表示,例如以不同的形式或缩写:
for cols in cat_cols:
    print(cols)
    print(df[cols].unique())
    print()
gender
['Male' 'Female' nan]
married
['No' 'Yes' nan]
dependents
['0' '1' '2' '3+' nan]
education
['Graduate' 'Not Graduate']
self_employed
['No' 'Yes' nan]
property_area
['Urban' 'Rural' 'Semiurban']
观察数据,似乎值是独特的,没有缩写或拼写错误。但机器学习系统可以被设计成面向未来。例如,如果我们将所有值都转换为小写,那么在未来,如果相同的值以不同的形式出现,在进入系统之前,它将被转换为小写。我们还可以看到,一些字符串,如Not Graduate,占用空间。就像我们确保列名的一致性一样,我们必须将所有空白替换为下划线。首先,我们创建一个新的 DataFrame 名为df_consistent;然后,我们将所有分类值转换为小写,并将所有空格替换为下划线:
df_consistent = df.copy()
for col in cat_cols:
    df_consistent[col] = df_consistent[col].apply(lambda val: val.lower() if isinstance(val, str) else val)
    df_consistent[col] = df_consistent[col].apply(lambda val: val.replace(' ','_') if isinstance(val, str) else val)
for cols in cat_cols:
    print(cols)
    print(df_consistent[cols].unique())
    print()
gender
['male' 'female' nan]
married
['no' 'yes' nan]
dependents
['0' '1' '2' '3+' nan]
education
['graduate' 'not_graduate']
self_employed
['no' 'yes' nan]
property_area
['urban' 'rural' 'semiurban']
通过这样,我们确保了数据的一致性,并且所有值都被转换为小写。
如我们所见,dependents列包含数值信息。然而,由于存在3+这样的值,列值被编码为字符串。我们必须移除特殊字符,然后将此值编码回数值,因为该列是序数的:
df_consistent.dependents = df_consistent.dependents.apply(lambda val: float(val.replace('+','')) if isinstance(val, str) else float(val))
接下来,我们查看已婚和自雇列,因为这些是二进制列,必须编码为1和0。性别列有两个值,也可以进行二进制编码——例如,我们可以将男性编码为1,将女性编码为0。教育列也有两个值,我们可以将毕业生编码为1,将非毕业生编码为0:
for cols in ['married', 'self_employed']:
    df_consistent[cols] = df_consistent[cols].map({"yes": 1, "no": 0})
df_consistent.education = df_consistent.education.map({
    'graduate': 1,
    'not_graduate': 0
})
df_consistent.gender = df_consistent.gender.map({
    'male': 1,
    'female': 0
})
for cols in cat_cols:
    print(cols)
    print(df_consistent[cols].unique())
    print()
gender
[ 1.  0\. nan]
married
[ 0.  1\. nan]
dependents
[ 0.  1.  2.  3\. nan]
education
[1 0]
self_employed
[ 0.  1\. nan]
property_area
['urban' 'rural' 'semiurban']
现在数据已经一致并且正确编码,我们必须创建一个预处理数据的函数,以便我们可以一致地处理任何未来的分类标签变化。然后,我们将该函数应用于 DataFrame,并打印值以确保函数被正确应用:
def make_data_consistent(df, cat_cols) -> pd.DataFrame:
    """Function to make data consistent and meaningful"""
    df = df.copy()
    for col in cat_cols:
        df[col] = df[col].apply(lambda val: val.lower() if isinstance(val, str) else val)
        df[col] = df[col].apply(lambda val: val.replace(' ','_') if isinstance(val, str) else val)
    df['dependents'] = df['dependents'].apply(lambda val: float(val.replace('+','')) if isinstance(val, str) else float(val))
    for cols in ['married', 'self_employed']:
        df[cols] = df[cols].map({"yes": 1, "no": 0})
    df['education'] = df['education'].map({
        'graduate': 1,
        'not_graduate': 0
    })
    df['gender'] = df['gender'].map({
        'male': 1,
        'female': 0
    })
    return df
df_consistent = df.copy()
df_consistent = make_data_consistent(df=df_consistent, cat_cols=cat_cols)
for cols in cat_cols:
    print(cols)
    print(df_consistent[cols].unique())
    print()
gender
[ 1.  0\. nan]
married
[ 0.  1\. nan]
dependents
[ 0.  1.  2.  3\. nan]
education
[1 0]
self_employed
[ 0.  1\. nan]
property_area
['urban' 'rural' 'semiurban']
现在我们已经确保了数据的一致性,因此如果分类值在未来用空格代替_或以不同的案例输入,我们可以使用我们在这里创建的功能来清理数据并使其一致,在它进入我们的模型之前:
接下来,我们将探索数据唯一性,以确保没有提供重复记录以在数据中造成偏差。
检查数据是否唯一
现在我们已经确保了数据的一致性,我们必须在它进入机器学习系统之前确保它的唯一性。
在本节中,我们将调查数据并检查loan_id列中的值是否唯一,以及某些列的组合是否可以确保数据的唯一性。
在 pandas 中,我们可以利用.nunique()方法来检查列的唯一记录数,并将其与行数进行比较。首先,我们将检查loan_id是否唯一,以及是否没有输入重复的申请:
df.loan_id.nunique(), df.shape[0]
(614, 614)
通过这种方式,我们已经确保了贷款 ID 的唯一性。然而,我们可以更进一步,确保不会将错误数据添加到另一个贷款申请中。我们相信贷款申请不太可能需要超过一种收入和贷款金额的组合。我们必须检查我们是否可以使用列值的组合来确保这些列之间的唯一性:
df[['applicant_income', 'coapplicant_income', 'loan_amount']].value_counts().reset_index(name='count')
     applicant_income  coapplicant_income  loan_amount  count
0                4333              2451.0        110.0      2
1                 150              1800.0        135.0      1
2                4887                 0.0        133.0      1
3                4758                 0.0        158.0      1
4                4817               923.0        120.0      1
..                ...                 ...          ...    ...
586              3166              2985.0        132.0      1
587              3167                 0.0         74.0      1
588              3167              2283.0        154.0      1
589              3167              4000.0        180.0      1
590             81000                 0.0        360.0      1
[591 rows x 4 columns]
如我们所见,在第一行中,有两个具有相同收入变量和贷款金额的申请。让我们通过使用第一行的值来过滤数据集,以找到这些被认为是重复的记录:
df[(df.applicant_income == 4333) & (df.coapplicant_income == 2451) & (df.loan_amount == 110)]
      loan_id  gender married dependents education self_employed  \
328  LP002086  Female     Yes          0  Graduate            No
469  LP002505    Male     Yes          0  Graduate            No
     applicant_income  coapplicant_income  loan_amount  loan_amount_term  \
328           4333              2451.0        110.0             360.0
469          4333              2451.0        110.0             360.0
     credit_history property_area loan_status
328             1.0         Urban           N
469             1.0         Urban           N
观察这个子集,很明显数据包含重复项或有两个不同的申请——一个是丈夫做的,另一个是妻子做的。这些数据除了表明一个男性候选人提交了一个申请,另一个女性候选人提交了一个申请之外,没有提供更多信息。我们可以删除其中一个数据点,但男性和女性申请的比例不平衡。此外,如果第二个申请是真实的,那么我们应该保留这个数据点:
df.gender.value_counts(normalize=True)
Male      0.813644
Female    0.186356
Name: gender, dtype: float64
基于此,我们已经理解了什么使数据点独特——那就是性别、申请人收入、共同申请人收入和贷款金额的组合。作为数据科学家和数据工程师,我们的目标是确保一旦定义了唯一性规则,进入机器学习系统的数据都符合这些唯一性检查。
在下一节中,我们将讨论数据完整性或数据不完整的问题,以及如何处理不完整的数据。
确保数据完整且无缺失
现在我们已经实现了数据的一致性和唯一性,是时候识别和解决其他质量问题了。其中一个问题是数据中的缺失信息或不完整数据。缺失数据是真实数据集中常见的问题。随着数据集大小的增加,数据点在数据中缺失的可能性也会增加。缺失记录可以以多种方式发生,其中包括:
- 
源数据集的合并:例如,当我们尝试根据出生日期或邮编匹配记录以丰富数据时,如果这些信息在一个数据集中缺失或不准确,那么这种情况将产生 NA 值。
 - 
随机事件:这在调查中很常见,其中受访者可能不知道所需信息是否为必填项,或者他们可能不知道答案。
 - 
测量失败:例如,一些特征,如血压,在传统方式(即使用血压计)测量时,已知具有很大的随机误差成分。如果两个人几乎同时测量一个受试者的血压,或者一个人在短时间内两次快速测量一个受试者的血压,测量的值可以很容易地相差 10 毫米汞柱(https://dept.stat.lsa.umich.edu/~kshedden/introds/topics/measurement/)。如果一个人意识到这些误差,他们可能会决定省略这些信息;对于一些患者,这些数据将产生 NA 值。在金融领域,一个重要的测量比率是确定个人或公司的信用价值,即债务收入比。在某些情况下,收入未申报,在这种情况下,将债务除以 0 或缺失数据会导致比率缺失信息。
 - 
数据收集过程中的设计不当:例如,在健康调查中,人们经常被问及他们的 BMI,并不是每个人都了解自己的 BMI 或理解测量方法。如果我们要求提供身高和体重会简单得多,因为人们更有可能知道这些信息。当有人被问及他们的体重测量时,有些人可能会省略或谎报这些信息。如果在收集数据时无法理解或测量 BMI,数据将产生 NA 值。
 
当训练数据集包含缺失值时,机器学习模型可能会产生不准确的预测或由于信息不完整而无法正确训练。在本节中,我们将讨论以下处理缺失数据的技术:
- 
删除数据
 - 
缺失值编码
 - 
填充方法
 
一种处理缺失数据的方法是通过删除缺失的记录。这也被称为完整案例分析(CCA)方法。如果少于 5%的行包含缺失值,这通常是可行的,但删除更多记录可能会降低模型的效力,因为样本量会变小。由于这种技术假设数据是完整随机缺失的,因此可能存在系统偏差,但违反了其他假设,例如当数据是随机缺失(MAR)或非随机缺失(MNAR)时。因此,盲目删除数据可能会使模型更加有偏差。例如,如果一个少数群体在过去没有申报收入或没有持有信用,他们可能没有信用评分。如果我们不了解缺失的原因而盲目删除这些数据,算法可能会更有利于向有信用信息的多数群体提供贷款,而少数群体将失去机会,尽管其中一些成员有稳定的收入和信用。
让我们使用 CCA 技术来探索数据集,删除所有缺失信息的行,并找出丢失了多少数据量:
remaining_rows = df_consistent.dropna(axis=0).shape[0]
total_records = df_consistent.shape[0]
perc_dropped = ((total_records - remaining_rows)/total_records)*100
print("By dropping all missing data, only {:,} records will be left out of {:,}, a reduction by {:,.3f}%".format(remaining_rows, total_records, perc_dropped))
By dropping all missing data, only 480 records will be left out of 614, a reduction by 21.824%
由于 21%几乎占数据集的四分之一,这不是一个可行的方法。因此,在本节中,我们将探讨如何识别缺失数据,揭示数据缺失的模式或原因,并发现处理缺失数据的技术,以便数据集可以用于机器学习。
首先,我们将提取分类特征、二元特征和数值特征。为此,我们必须分离标识符和目标标签:
id_col = 'loan_id'
target = 'loan_status'
feature_cols = [cols for cols in df_consistent if cols not in [id_col, target]]
binary_cols = [cols for cols in feature_cols if df_consistent[cols].nunique() == 2]
cat_cols = [cols for cols in feature_cols if (df_consistent[cols].dtype == 'object' or df_consistent[cols].nunique() <= 15)]
num_cols = [cols for cols in feature_cols if cols not in cat_cols]
cat_cols
['gender',
 'married',
 'dependents',
 'education',
 'self_employed',
 'loan_amount_term',
 'credit_history',
 'property_area']
binary_cols
['gender', 'married', 'education', 'self_employed', 'credit_history']
num_cols
['applicant_income', 'coapplicant_income', 'loan_amount']
要检查数据集中是否存在缺失数据,pandas 提供了一个方便的方法叫做.info()。此方法显示总记录中完整行数有多少。该方法还显示每列的数据类型:
df_consistent.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 614 entries, 0 to 613
Data columns (total 13 columns):
 #   Column              Non-Null Count  Dtype
---  ------              --------------  -----
 0   loan_id             614 non-null    object
 1   gender              601 non-null    float64
 2   married             611 non-null    float64
 3   dependents          599 non-null    float64
 4   education           614 non-null    int64
 5   self_employed       582 non-null    float64
 6   applicant_income    614 non-null    int64
 7   coapplicant_income  614 non-null    float64
 8   loan_amount         592 non-null    float64
 9   loan_amount_term    600 non-null    float64
 10  credit_history      564 non-null    float64
 11  property_area       614 non-null    object
 12  loan_status         614 non-null    object
dtypes: float64(8), int64(2), object(3)
memory usage: 62.5+ KB
pandas 库还有一个方便的方法叫做.isnull(),用来检查某一列的哪些行缺失数据,哪些行是完整的。通过将.sum()与.isnull()结合使用,我们可以得到每个列的缺失记录总数:
df_consistent.isnull().sum()
loan_id                0
gender                13
married                3
dependents            15
education              0
self_employed         32
applicant_income       0
coapplicant_income     0
loan_amount           22
loan_amount_term      14
credit_history        50
property_area          0
loan_status            0
dtype: int64
如我们所见,credit_history、self_employed和loan_amount列有最多的缺失数据。原始值有时难以理解,知道每个列缺失数据的百分比更有用。在下一步中,我们将创建一个函数,该函数将接受 DataFrame 并打印出每个列的缺失数据百分比。然后,我们将按缺失度降序排序数据:
def missing_data_percentage(df: pd.DataFrame):
    """Function to print percentage of missing values"""
    df = df.copy()
    missing_data = df.isnull().sum()
    total_records = df.shape[0]
    perc_missing = round((missing_data/total_records)*100, 3)
    missing_df = pd.DataFrame(data={'columm_name':perc_missing.index, 'perc_missing':perc_missing.values})
    return missing_df
missing_data_percentage(df_consistent[feature_cols]).sort_values(by='perc_missing', ascending=False)
           columm_name  perc_missing
9       credit_history         8.143
4        self_employed         5.212
7          loan_amount         3.583
2           dependents         2.443
8     loan_amount_term         2.280
0               gender         2.117
1              married         0.489
3            education         0.000
5     applicant_income         0.000
6   coapplicant_income         0.000
10       property_area         0.000
现在,我们可以提取缺失数据的量级。然而,在我们深入处理缺失数据之前,了解缺失数据的模式非常重要。通过理解这些关系,我们将能够采取适当的步骤。这是因为填补缺失数据可能会改变数据的分布,这可能会进一步影响变量交互。
我们将利用missingno库和其他可视化工具来了解数据缺失的位置,在没有主题专家的情况下,我们将对缺失数据的原因做出一些假设。
为了查看值缺失和数据中存在差距的位置,我们将利用矩阵图。当数据集具有深度或数据包含与时间相关的信息时,矩阵图非常有用。数据的存在用灰色表示,而缺失数据用白色显示:
msno.matrix(df_consistent[feature_cols], figsize=(35, 15))
<AxesSubplot: >
这里是输出:

图 5.2 – 矩阵图
仔细观察,我们可以看到credit_history列有很多缺失点,缺失的发生在整个数据中分布,而不是在某个特定的时间点。
正如我们之前提到的,了解数据缺失的原因可以帮助我们选择正确的技术来处理缺失数据。从高层次上讲,我们可以将这些缺失数据的机制称为,并将它们分为三类:
- 
完全随机缺失(MCAR)
 - 
非随机缺失(MNAR)
 - 
随机缺失(MAR)
 
数据在缺失数据对所有观测值的可能性相同,并且缺失数据与数据集中任何其他特征之间没有关系时,被认为是完全随机缺失(MCAR)。例如,一封邮件问卷可能在邮寄过程中丢失,或者如果一个人匆忙,他们可能忘记回答一个问题。在这种情况下,数据缺失与问题的类型、年龄组或性别(与其他变量的关系)无关,我们可以将这些特征或数据点归类为 MCAR。删除这些数据点或将这些实例的值更改为 0 不会对预测造成偏差。
另一方面,当数据点的缺失可能性取决于其他现有数据点时,数据被认为是随机缺失(MAR)。例如,如果男性平均有 5%的时间不披露他们的体重,而女性有 15%的时间不披露他们的体重,我们可以假设数据缺失是由性别偏见引起的。这将导致女性比男性有更高比例的数据缺失。对于这种机制,我们可以使用统计技术或机器学习来预测缺失值,利用数据集中的其他特征。
第三个机制,MNAR,通常容易与 MCAR 混淆,但略有不同。在这种情况下,可以明确地假设为什么数据不是随机缺失的。例如,如果我们试图了解导致抑郁(结果)的因素,那么抑郁的人可能更不愿意回答问题或更不可能被联系。由于缺失与结果相关,这些缺失记录可以标记为“缺失”,对于数值特征,我们可以使用机器学习结合其他特征来估计缺失数据,并通过创建另一个变量来标记数据缺失的点。
现在我们已经了解了不同类型的缺失数据,我们将利用 missingno 中的 heatmap 函数,它将创建一个关联热图。可视化显示了数据集列之间的空值相关系数。它显示了某个特征的存在或缺失如何强烈地影响其他特征。
空值相关系数范围从 -1 到 1:
- 
-1 表示如果一个列(属性)存在,另一个几乎肯定不存在
 - 
0 表示列(属性)之间没有依赖关系
 - 
1 表示如果一个列(属性)存在,另一个也肯定存在
 
与标准的关联热图不同,以下可视化是关于缺失数据特征之间的关系,因为其中许多特征几乎没有缺失数据。那些始终完整或始终为空的列没有有意义的关联,并且被从可视化中移除。
这个热图有助于识别属性对之间的数据完整性相关系数,但它对更广泛关系的解释能力有限:
msno.heatmap(df_consistent[feature_cols], labels=True)
这导致了以下热图:

图 5.3 – 热图
从这个图中,我们可以解释几个变量之间的缺失关系。dependents 和 married 之间存在 0.4 的相关性,这是有道理的,因为大多数情况下,人们先结婚再成为有依赖的人。
接下来,我们将提取包含缺失数据的列,并使用这些列进行下一个可视化。dendrogram 方法使用层次聚类,将属性分组在一起,其中缺失与另一个变量的缺失相关联,或完整性与其他变量的完整性相关联:
missing_cols = [cols for cols in feature_cols if df_consistent[cols].isnull().sum() > 0]
msno.dendrogram(df_consistent[missing_cols])
输出如下:

图 5.4 – 系谱图
我们根据自上而下的方法来解释系谱图 – 即,我们关注任何两个列在空值问题上的连接高度。高度越大,关系越小,反之亦然。例如,credit_history 中的数据缺失与任何其他变量的缺失或完整性没有关系。
这样,我们已经了解了缺失数据的模式,以及缺失数据列之间是否存在关系。接下来,我们将探索缺失数据与结果之间的关系。在我们决定删除缺失数据或进行插补之前,我们还应该看看变量的缺失是否与结果相关联——也就是说,数据可能存在 MNAR 的机会吗?
首先,我们将可视化缺失分类数据中的这种关系:
cat_missing = [cols for cols in cat_cols if df_consistent[cols].isnull().sum() > 0]
def cat_missing_association_with_outcome(data, missing_data_column, outcome):
    """Function to plot missing association of categorical varibles with outcome"""
    df = data.copy()
    df[f"{missing_data_column}_is_missing"] = df[missing_data_column].isnull().astype(int)
    df.groupby([outcome]).agg({f"{missing_data_column}_is_missing": 'mean'}).plot.bar()
for cols in cat_missing:
    cat_missing_association_with_outcome(df_consistent, cols, target)
这将创建一些图表,展示分类特征与目标变量之间的关系:






图 5.5 - 显示分类特征与目标变量关联的输出图表
在高层次上,我们可以假设对于married、dependents、loan_amount_term、gender和credit_history等变量,数据的缺失与贷款批准状态相关联。因此,我们可以认为这些变量的数据是 MNAR(Missing Not At Random,非随机缺失)。对于这三个变量,我们可以用“missing”这个词来编码缺失数据,因为这个信号将有助于预测结果。credit_history的缺失或完整性略与self_employed状态相关,如热图所示,这表明数据可能随机缺失。同样,married状态的缺失与dependents和loan_amount的缺失相关。
对于所有数据缺失的二进制变量,我们可以假设数据不是 MCAR(Missing Completely At Random,完全随机缺失),而是假设数据是 MNAR,因为缺失信息与结果之间存在某种关系,或者 MAR(Missing At Random,随机缺失),因为缺失与其他变量的存在或不存在相关,如树状图所示。
编码缺失值的一种方法是将这些值编码为最频繁的值,或者去除缺失值,或者创建一个额外的列,用 1 或 0 表示缺失。然而,对于 MAR 场景,这并不是最佳技术。如前所述,数据中心方法的目标是提高数据质量和减少偏差。因此,我们不应使用频率插补方法或仅删除记录,而应考虑要求注释者提供缺失数据的信息,或者进行系统修复以恢复缺失信息。如果这不可能,我们应该考虑使用机器学习技术或概率技术来确定可能的值,而不是简单的众数、均值和中位数插补方法。然而,当缺失超过一定阈值时,即使是高级技术也不可靠,最好是删除该特征。对于剩余的变量,我们将使用机器学习技术来确定缺失值,因为我们无法获得注释者的帮助来提供完整信息。
既然我们已经确定了分类值缺失与结果之间的关联,接下来,我们将研究缺失数值数据与结果之间的关系:
num_missing = [cols for cols in num_cols if df_consistent[cols].isnull().sum() > 0]
def num_missing_association_with_outcome(data, missing_data_column, outcome):
    """Function to plot missing association of categorical varibles with outcome"""
    df = data.copy()
    df[f"{missing_data_column}_is_missing"] = df[missing_data_column].isnull().astype(int)
    df.groupby([outcome]).agg({f"{missing_data_column}_is_missing": 'mean'}).plot.bar()
for cols in num_missing:
    num_missing_association_with_outcome(df, cols, target)
这将显示以下图表:

图 5.6 – 贷款金额缺失与目标的相关性
对于loan_amount,可以假设数据是 MNAR 以及 MAR,因为married和dependents变量中的数据缺失或完成与loan_amount的数据缺失和完整性略有关联,如热图中所示。因此,我们选择使用机器学习来插补缺失值,并创建一个额外的列来指示缺失,这将为我们模型提供更好的信号。
接下来,我们将深入研究各种数据插补方法,并进行比较,同时讨论每种方法的不足。我们还将讨论机器学习在以数据为中心的机器学习中插补缺失数据的影响。
采用以模型为中心的方法,插补数值变量的标准规则是,当 5%的数据缺失时,使用均值、中位数或众数进行插补。这种方法假设数据是随机缺失的。如果这个假设不成立,这些简单的插补方法可能会掩盖数据中的分布和关系。
首先,我们将探索未插补和用中位数插补的loan_amount的分布。当我们用中位数插补 6%的值时,分布会发生变化:
df_consistent.loan_amount.plot.kde(color='orange', label='loan_amount', legend=True)
df_consistent.loan_amount.fillna(value=df.loan_amount.median()).plot.kde(color='b', label='loan_amount_imputed', alpha=0.5, figsize=(9,7), legend=True)
以下图表是输出显示:

图 5.7 – 使用中位数的简单密度图插补
接下来,我们比较插补前后贷款金额的标准差:
round(df_consistent.loan_amount.std(),2), round(df_consistent.loan_amount.fillna(value=df_consistent.loan_amount.median()).std(),2)
(85.59, 84.11)
上一段代码展示了简单的插补方法如何掩盖数据的分布。为了抵消这些影响并保持分布,我们将使用随机样本插补方法。
首先,我们提取所有loan_amount缺失的行。然后,我们计算与loan_amount相关的变量,并使用这些值来设置种子。这是因为,如果我们对所有值使用相同的种子,那么将生成相同的随机数,该方法的行为将与任意值插补类似,这将与均值和中位数等简单插补方法一样无效。
随机样本分布的缺点是协方差将受到影响,我们需要一种同时保持协方差的方法。
首先,我们检查哪个特征与loan_amount高度相关:
df_consistent[num_cols].corr()
                    applicant_income  coapplicant_income  loan_amount
applicant_income            1.000000           -0.116605     0.570909
coapplicant_income         -0.116605            1.000000     0.188619
loan_amount                 0.570909            0.188619     1.000000
在这里,我们可以看到loan_amount与applicant_income高度相关,因此在这个例子中,我们使用这个变量来设置种子。首先,我们提取loan_amount缺失的索引。然后,我们使用缺失位置的applicant_income值,并使用这个值来设置种子。接下来,我们使用这个种子从loan_amount生成一个随机值来插补缺失的行。我们使用这种方法来插补loan_amount的所有缺失数据:
observation = df_consistent[df_consistent.loan_amount.isnull()]
imputed_values = []
for idx in observation.index:
    seed = int(observation.loc[idx,['applicant_income']])
    imputed_value = df_consistent['loan_amount'].dropna().sample(1, random_state=seed)
    imputed_values.append(imputed_value)
df_consistent.loc[df_consistent['loan_amount'].isnull(),'loan_amount_random_imputed']=imputed_values
df_consistent.loc[df['loan_amount'].isnull()==False,'loan_amount_random_imputed']=df_consistent[df_consistent['loan_amount'].isnull()==False]['loan_amount'].values
接下来,我们比较loan_amount的分布与随机样本插补的loan_amount和中位数插补的loan_amount:
df_consistent.loan_amount.plot.kde(color='orange', label='loan_amount', legend=True, linewidth=2)
df_consistent.loan_amount_random_imputed.plot.kde(color='g', label='loan_amount_random_imputed', legend=True, linewidth=2)
df_consistent.loan_amount.fillna(value=df_consistent.loan_amount.median()).plot.kde(color='b', label='loan_amount_median_imputed', linewidth=1, alpha=0.5, figsize=(9,7), legend=True)
<AxesSubplot: ylabel='Density'>
这将输出以下图表:

图 5.8 – 显示随机和中位数插补的密度图
现在,我们比较预先插补的贷款的标准差与随机样本插补方法和中位数插补方法:
round(df_consistent.loan_amount.std(),2), round(df_consistent.loan_amount_random_imputed.std(),2), round(df_consistent.loan_amount.fillna(value=df_consistent.loan_amount.median()).std(),2)
(85.59, 85.57, 84.11)
随机样本插补方法在分布和标准差上与预先插补的loan_amount方法比中位数插补的loan_amount方法更接近。接下来,我们检查随机样本插补方法是否与其他方法相比保留了与其他变量的相关性:
df_consistent['loan_amount_median_imputed'] = df_consistent['loan_amount'].fillna(value=df_consistent['loan_amount'].median())
df_consistent[['loan_amount', 'loan_amount_median_imputed','loan_amount_random_imputed', 'applicant_income']].corr()
得到的 DataFrame 如下:

图 5.9 – 相关性 DataFrame
从这一点来看,很明显,随机插补方法可以保留分布,但可能会掩盖与其他变量的相互关系。我们需要一种可以保留分布并保持与其他变量相互关系的方法。我们将使用机器学习来帮助我们实现这一点。在我们转向机器学习之前,我们首先将讨论简单插补对分类/二进制变量的影响。我们使用最频繁的值插补credit_history二进制列,并比较插补前后的分布:
df_consistent.credit_history.value_counts(normalize=True)
1.0    0.842199
0.0    0.157801
Name: credit_history, dtype: float64
df_consistent.credit_history.fillna(value=df_consistent.credit_history.mode()[0]).value_counts(normalize=True)
1.0    0.855049
0.0    0.144951
Name: credit_history, dtype: float64
通过用最频繁的值插补credit_history,我们使数据偏向于credit_history状态。正如我们之前发现的,credit_history的缺失与任何其他变量都不相关,但它可能与结果相关。
前面的例子表明,如果我们使用简单的插补方法,那么我们可能会对数据进行偏差,分布也会相应改变,而如果我们使用随机方法,分布将得以保留,但数据关系可能会改变,数据方差可能会增加。因此,当数据是 MAR(完全随机应答)或 MNAR(非随机应答)时,为了在数据偏差和数据方差之间取得平衡,我们可以使用机器学习模型。
为了利用机器学习进行数值插补,我们将利用scikit-learn库中可用的最近邻插补方法KNNImputer。这个插补器的一个问题是,我们只能传递一个 DataFrame 给它,而不能传递列的列表。因此,我们将使用SklearnTransformerWrapper模块,它是feature-engine库的一部分,来传递列的列表。由于 KNN 是一个基于距离的算法,为了确保模型收敛并且一个变量不会压倒另一个变量,我们必须在使用此算法之前对数据进行缩放。
另一种用于插补数据的技术被称为链式方程多重插补(MICE)。MICE 通过使用均值、中位数或众数来插补所有数据。然后,对于将要插补的变量,初始插补的值被转换回缺失值。接着,使用其他变量作为预测变量,利用机器学习模型预测缺失值。之后,以类似的方式插补下一个变量,其中初始插补的值被转换回缺失值,并使用包括最近插补的变量在内的其他变量作为预测变量来插补缺失值。一旦所有带有缺失值的变量都被建模,并且使用预测值插补了值,第一次插补轮次就完成了。这个过程重复n次(理想情况下为 10 次),从第二轮开始,使用第一轮的预测来预测最初缺失的记录。
使用多轮的原因是,最初我们使用其他也包含 NA 值的变量来模拟缺失数据,而初始的插补策略使用的是次优方法,如均值、中位数或众数,这些方法可能会对预测产生偏差。随着我们继续进行多轮回归,预测将趋于稳定并减少偏差。
MICE 的一个问题是,我们必须选择用于任务的机器学习模型。我们将使用随机森林算法实现 MICE,在 R 语言中这被称为[missForest]。
在我们实现的 MICE 中,我们将称之为missForest,因为它将复制 R 语言中实现的方式(R 语言中的 MissForest)。为了对抗选择算法的影响,我们鼓励实践者利用自动化机器学习,对于每一次插补和迭代,都会选择一个新的算法。这种方法的一个缺点是,当用于大数据集时,它计算量大且耗时。
首先,我们导入必要的包:
from sklearn.impute import KNNImputer
from feature_engine.wrappers import SklearnTransformerWrapper
from sklearn.preprocessing import StandardScaler
接下来,我们通过过滤掉可能包含超过 15 个类别的任何列,同时过滤id列和结果列,以及使用插补方法过滤新创建的变量来提取数值列:
num_cols = [cols for cols in df_consistent if df_consistent[cols].nunique() > 15 and cols not in [id_col, target] and not cols.endswith('imputed')]
接下来,我们创建包含数值变量的 DataFrame 并可视化它:
df_num = df_consistent[num_cols].copy()
df_num.head()
   applicant_income  coapplicant_income  loan_amount
0              5849                 0.0          NaN
1              4583              1508.0        128.0
2              3000                 0.0         66.0
3              2583              2358.0        120.0
4              6000                 0.0        141.0
接下来,我们构建一个函数,该函数接受缩放器(标准缩放器或任何其他缩放器)和 DataFrame,并返回缩放后的数据和经过处理的缩放器。在应用 KNN 估计器之前,我们必须缩放数据集,因为基于距离的方法需要数据处于相同的尺度。一旦我们缩放了数据,我们就应用 KNN 估计器来估计数据,然后使用函数返回的经过处理的缩放器来反缩放数据。完成这些后,我们可以比较机器学习估计的数据与中位数和随机估计方法:
def scale_data(df, scaler, columns):
    """Function to scale the data"""
    df_scaled = df.copy()
    if columns:
        df_scaled[columns] = scaler.fit_transform(df_scaled[columns])
    else:
        columns = [cols for cols in df_scaled]
        df_scaled[columns] = scaler.fit_transform(df_scaled[columns])
    return df_scaled, scaler
接下来,我们定义缩放器并调用scale_data函数:
scaler = StandardScaler()
df_scaled, scaler = scale_data(df_num, scaler=scaler, columns=num_cols)
然后,我们使用 10 个邻居的参数应用 KNN 估计器来估计数据。我们利用weights='distance'参数,以便在预测结果时,更重视靠近邻居的投票,而不是远离邻居的投票。
首先,我们初始化估计器:
knn_imputer = SklearnTransformerWrapper(
    transformer = KNNImputer(n_neighbors=10, weights='distance'),
    variables = num_cols
)
然后,我们应用估计:
df_imputed = knn_imputer.fit_transform(df_scaled)
接下来,我们通过调用缩放器对象的inverse_transform方法来反缩放数据,并用未缩放值覆盖df_imputed DataFrame:
df_imputed = pd.DataFrame(columns=num_cols, data=scaler.inverse_transform(df_imputed))
df_imputed.head()
   applicant_income  coapplicant_income  loan_amount
0            5849.0                 0.0   149.666345
1            4583.0              1508.0   128.000000
2            3000.0                 0.0    66.000000
3            2583.0              2358.0   120.000000
4            6000.0                 0.0   141.000000
接下来,我们比较预先估计的loan_amount的分布,并将其与机器学习估计的方法进行比较。然后,我们检查机器学习估计方法与申请者收入的关联性,并将其与其他估计方法进行比较:
df_imputed['loan_amount'].plot.kde(color='orange', label='loan_amount_knn_imputed',linewidth=2, legend=True)
df_consistent['loan_amount'].plot.kde(color='b', label='loan_amount', legend=True, linewidth=2, figsize=(9,7), alpha=0.5)
结果图如下:

图 5.10 – 贷款金额 KNN 估计
接下来,我们比较预先估计的贷款金额的标准差与所有估计方法:
round(df_consistent.loan_amount.std(),2), round(df_consistent.loan_amount_random_imputed.std(),2), round(df_consistent.loan_amount_median_imputed.std(),2), round(df_imputed.loan_amount.std(),2)
(85.59, 85.57, 84.11, 85.59)
然后,我们将检查当使用机器学习来估计loan_amount时,相关性是否保持不变:
df_consistent['loan_amount_knn_imputed'] = df_imputed.loan_amount
df_consistent[['loan_amount', 'loan_amount_median_imputed','loan_amount_random_imputed', 'loan_amount_knn_imputed', 'applicant_income']].corr()

图 5.11 – 贷款金额估计后的相关性
机器学习估计的方法几乎与原始数据具有相同的分布。然而,与预先估计的loan_amount相比,与applicant_income的相关性略高。我们现在已经看到了如何使用现成的技术来估计缺失数据。这种方法的一个优点是易于实现。然而,缺点是我们不能选择另一个算法。
因此,在下一步中,我们更进一步,使用随机森林构建一个 MICE 实现。首先,我们使用独热编码将分类数据转换为数值数据。然后,我们使用RandomForestClassifier的 MICE 实现来估计缺失的分类数据。
一旦分类数据被估计,我们使用分类和数值数据,通过利用 MICE 与RandomForestRegressor来估计数值缺失值。
为了构建 MICE 实现,我们使用 scikit-learn 中的IterativeImputer,它可以帮助进行 10 轮 MICE。为了利用IterativeImputer,我们必须从 scikit-learn 的实验包中导入enable_iterative_imputer,如文档所述:scikit-learn.org/stable/modules/generated/sklearn.impute.IterativeImputer.html。
首先,我们导入必要的包:
from sklearn.ensemble import ExtraTreesRegressor, ExtraTreesClassifier
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
from feature_engine.encoding import OneHotEncoder
接下来,我们提取字符串编码的分类列,以便我们可以对这些列进行独热编码:
ohe_cols = [cols for cols in cat_cols if df_consistent[cols].dtype == 'object']
ohe_cols
['property_area']
然后,我们对分类列进行独热编码:
df_ohe_encoded = df_consistent.copy()
ohe = OneHotEncoder(variables=ohe_cols)
df_ohe_encoded = ohe.fit_transform(df_ohe_encoded)
之后,我们可视化独热编码数据的前五个结果:
df_ohe_encoded[[cols for cols in df_ohe_encoded if 'property_area' in cols]].head()
   property_area_urban  property_area_rural  property_area_semiurban
0                    1                    0                        0
1                    0                    1                        0
2                    1                    0                        0
3                    1                    0                        0
4                    1                    0                        0
接下来,我们提取二进制编码的分类变量,包括已经独热编码的数据:
cat_cols = [cols for cols in df_ohe_encoded if df_ohe_encoded[cols].nunique() <= 15 and cols not in [id_col, target]]
cat_cols
['gender',
 'married',
 'dependents',
 'education',
 'self_employed',
 'loan_amount_term',
 'credit_history',
 'property_area_urban',
 'property_area_rural',
 'property_area_semiurban']
然后,我们使用随机森林构建 MICE 实现以填充分类数据:
miss_forest_classifier = IterativeImputer(
    estimator=ExtraTreesClassifier(n_estimators=100,
                                   random_state=1,
                                   bootstrap=True,
                                   n_jobs=-1),
    max_iter=10,
    random_state=1,
    add_indicator=True,
    initial_strategy='median')
df_cat_imputed = miss_forest_classifier.fit_transform(df_ohe_encoded[cat_cols])
接下来,我们将填充的数值通过将 NumPy 数组转换为名为df_cat_imputed的 DataFrame 来提取特征:
df_cat_imputed = pd.DataFrame(
    columns=miss_forest_classifier.get_feature_names_out(),
    data=df_cat_imputed,
    index=df_ohe_encoded.index)
让我们确保分类器没有创建任何新的意外值。为此,我们遍历所有列并打印每列的唯一值:
for cols in cat_cols:
    print(cols)
    print(df_cat_imputed[cols].unique())
    print()
gender
[1\. 0.]
married
[0\. 1.]
dependents
[0\. 1\. 2\. 3.]
education
[1\. 0.]
self_employed
[0\. 1.]
loan_amount_term
[360\. 120\. 240\. 180.  60\. 300\. 480.  36.  84.  12.]
credit_history
[1\. 0.]
property_area_urban
[1\. 0.]
property_area_rural
[0\. 1.]
property_area_semiurban
[0\. 1.]
现在,我们将分类填充数据与数值数据合并。然后,我们使用所有数据来填充数值数据:
num_cols = [cols for cols in df_consistent if cols not in df_cat_imputed and cols not in [id_col, target] + ohe_cols
            and not cols.endswith("imputed")]
df_combined = pd.concat([df_consistent[num_cols], df_cat_imputed], axis=1)
feature_cols = [cols for cols in df_combined]
feature_cols
['applicant_income',
 'coapplicant_income',
 'loan_amount',
 'gender',
 'married',
 'dependents',
 'education',
 'self_employed',
 'loan_amount_term',
 'credit_history',
 'property_area_urban',
 'property_area_rural',
 'property_area_semiurban',
 'missingindicator_gender',
 'missingindicator_married',
 'missingindicator_dependents',
 'missingindicator_self_employed',
 'missingindicator_loan_amount_term',
 'missingindicator_credit_history']
接下来,我们使用随机森林实现 MICE 填充以填充数值数据:
miss_forest_regressor = IterativeImputer(
    estimator=ExtraTreesRegressor(n_estimators=100,
                                  random_state=1,
                                  bootstrap=True,
                                  n_jobs=-1),
    max_iter=10,
    random_state=1,
    add_indicator=True,
    initial_strategy='median')
df_imputed = miss_forest_regressor.fit_transform(df_combined[feature_cols])
现在,我们将填充的数值通过将 NumPy 数组转换为 DataFrame 来提取特征:
df_imputed
df_imputed = pd.DataFrame(data=df_imputed,
                           columns=miss_forest_regressor.get_feature_names_out(),
                           index=df_combined.index)
然后,我们检查是否所有列都已填充并且没有缺失值:
df_imputed.isnull().sum()
applicant_income                     0
coapplicant_income                   0
loan_amount                          0
gender                               0
married                              0
dependents                           0
education                            0
self_employed                        0
loan_amount_term                     0
credit_history                       0
property_area_urban                  0
property_area_rural                  0
property_area_semiurban              0
missingindicator_gender              0
missingindicator_married             0
missingindicator_dependents          0
missingindicator_self_employed       0
missingindicator_loan_amount_term    0
missingindicator_credit_history      0
missingindicator_loan_amount         0
dtype: int64
接下来,我们比较预填充的loan_amount的分布,并将其与 MICE 填充方法进行比较。然后,我们检查 MICE 填充方法与申请者收入的关联性,并将其与其他填充方法进行比较:
df_imputed['loan_amount'].plot.kde(color='orange', label='loan_amount_miss_forest_imputed',linewidth=2, legend=True)
df_consistent['loan_amount'].plot.kde(color='b', label='loan_amount', legend=True, linewidth=2, figsize=(9,7), alpha=0.5)
<AxesSubplot: ylabel='Density'>
输出结果如下:

图 5.12 – loan_amount_miss_forest_imputed
接下来,我们比较预填充贷款金额的标准差与所有填充方法,包括 MICE 填充方法:
round(df_consistent.loan_amount.std(),2), round(df_consistent.loan_amount_random_imputed.std(),2), round(df_consistent.loan_amount_median_imputed.std(),2), round(df_imputed.loan_amount.std(),2)
(85.59, 85.57, 84.11, 85.41)
然后,我们检查当使用 MICE 填充方法填充loan_amount时,与其他方法相比,相关性是否保持不变:
df_consistent['loan_amount_miss_forest_imputed'] = df_imputed.loan_amount
df_consistent[['loan_amount', 'loan_amount_median_imputed','loan_amount_random_imputed', 'loan_amount_miss_forest_imputed', 'applicant_income']].corr()
输出的 DataFrame 如下:

图 5.13 – 使用 MICE 填充方法填充 loan_amount 后的相关性
标准差略低于随机填充,但高于中位数填充方法。正如我们所见,与随机填充方法或中位数填充方法相比,与applicant_income的相关性没有改善。因此,为了测试 MICE 与随机森林是否是此用例的更好实现,我们可以比较当使用 MICE 时和当使用中位数填充时机器学习模型的评估指标。
但在我们这样做之前,我们希望机器学习从业者使用 MICE 插补框架探索自动化机器学习(AutoML)。机器学习可能是一个繁琐的过程,它包括试错,这就是为什么 AutoML 框架在减少人工时间方面越来越受欢迎。这些框架自动化特征工程、交叉验证、模型选择和模型调优。MICE 当前实现的一个问题是,我们必须选择用于任务的机器学习模型。如果我们想尝试多个算法,看看哪个提供了最佳的插补任务预测,并且在尝试过程中确保预测具有可推广性,并且模型没有过拟合或欠拟合,会怎样呢?我们可以想象其复杂性。为了解决这个问题,我们将结合 AutoML 和 MICE。
这种方法的优点是,在每次迭代中,AutoML 将选择一个新的模型,从而让机器学习从业者从繁琐的任务中解放出来。然而,这种方法的缺点是,当数据量增加时,需要更多的资源,这可能不可行。另外,一些开源的 AutoML 框架的另一个缺点是,在某些操作系统上,完整功能可能会出现错误。例如,在 Mac 电脑上,TPOT 和 AutoSklearn 框架在并行处理时都会出现错误。因此,我们将让您探索使用 MICE 的 AutoML 的个性化版本。
接下来,我们将实现一个包含 MICE 实现和随机森林的 scikit-learn 流水线。然后,我们使用交叉验证训练决策树模型,并使用准确率和 ROC 评估模型。完成这些后,我们创建另一个流水线,它将使用简单的插补方法,并比较评估结果。最后,我们探索进一步改进数据以提高模型性能的技术。
我们将把这些步骤转换成 scikit-learn 流水线,因为通过使用流水线,我们可以定义步骤的顺序,并将这些步骤保存为 pickle 对象。通过利用这种做法,我们保持机器学习系统的最佳实践,并可以确保可靠性及可重复性,而无需在推理环境中重复编写代码。
首先,让我们删除df_consistent DataFrame 中所有以_imputed结尾的新创建的列:
df_consistent.drop([cols for cols in df_consistent if cols.endswith('imputed')], axis=1, inplace=True)
接下来,我们将导入所有必要的包和模块,以帮助将数据分为训练集和测试集,评估模型的性能,并创建机器学习流水线:
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.metrics import roc_auc_score, accuracy_score, confusion_matrix, ConfusionMatrixDisplay
from typing import List
from sklearn.tree import DecisionTreeClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
现在,我们提取模型所需的特征,并将数据分为训练集和测试集,其中 10%的数据保留用于测试:
feature_cols = [cols for cols in df_consistent if cols not in [target, id_col]]
X_train, X_test, y_train, y_test = train_test_split(df_consistent[feature_cols],
                                                    df_consistent[target].map({'Y':1, 'N':0}),
                                                    test_size=0.1,
                                                    random_state=1,
                                                    stratify=df_consistent[target].map({'Y':1, 'N':0}))
feature_cols
['gender',
 'married',
 'dependents',
 'education',
 'self_employed',
 'applicant_income',
 'coapplicant_income',
 'loan_amount',
 'loan_amount_term',
 'credit_history',
 'property_area']
接下来,我们将提取分类数据和数值数据到单独的列表中,以便我们可以使用这些数据为每种类型的数据设置流水线:
cat_cols = [cols for cols in X_train if X_train[cols].nunique() <= 15]
num_cols = [cols for cols in X_train if cols not in cat_cols]
现在,我们创建一个函数,该函数将返回用于分类数据的管道。首先,该管道将ohe_cols变量中的列列表进行独热编码,其中包括property_area。然后,该管道使用随机森林的 MICE 实现来填充缺失数据。该函数将返回转换器,以便当我们传递分类数据时,转换器将独热编码数据并填充缺失数据。转换器将首先在训练数据上运行,以便了解数据并保存所有元数据,以便用新数据运行相同的步骤。然后,转换器可以用来转换测试数据:
def miss_forest_categorical_transformer():
    """Function to define categorical pipeline"""
    cat_transformer = Pipeline(
        steps=[
            ("one_hot_encoding",
             OneHotEncoder(variables=ohe_cols)
            ),
            ("miss_forest_classifier",
             IterativeImputer(
                 estimator=ExtraTreesClassifier(
                     n_estimators=100,
                     random_state=1,
                     bootstrap=True,
                     n_jobs=-1),
                max_iter=10,
                random_state=1,
                initial_strategy='median',
                add_indicator=True)
            )
        ]
    )
    return cat_transformer
接下来,我们创建一个函数,该函数返回用于用 MICE 实现填充数值缺失数据的管道转换器。与分类转换器类似,数值转换器将针对训练数据进行训练,然后应用于测试数据以填充训练和测试数据中的缺失值:
def miss_forest_numerical_transformer():
    """Function to define numerical pipeline"""
    num_transformer = Pipeline(
        steps=[
            ("miss_forest",
             IterativeImputer(
                estimator=ExtraTreesRegressor(n_estimators=100,
                                              random_state=1,
                                              bootstrap=True,
                                              n_jobs=-1),
                max_iter=10,
                random_state=1,
                initial_strategy='median',
                add_indicator=True)
            )
        ]
    )
    return num_transformer
然后,我们初始化分类和数值转换器,然后转换训练和测试数据。在转换数值数据之前,将转换后的分类数据与数值数据合并。这个输出的结果是填充后的训练和测试数据框:
cat_transformer = miss_forest_categorical_transformer()
num_transformer = miss_forest_numerical_transformer()
X_train_cat_imputed = cat_transformer.fit_transform(X_train[cat_cols])
X_test_cat_imputed = cat_transformer.transform(X_test[cat_cols])
X_train_cat_imputed_df = pd.DataFrame(data=X_train_cat_imputed,
                                      columns=cat_transformer.get_feature_names_out(),
                                      index=X_train.index)
X_test_cat_imputed_df = pd.DataFrame(data=X_test_cat_imputed,
                                     columns=cat_transformer.get_feature_names_out(),
                                     index=X_test.index)
X_train_cat_imputed_df = pd.concat([X_train_cat_imputed_df, X_train[num_cols]], axis=1)
X_test_cat_imputed_df = pd.concat([X_test_cat_imputed_df, X_test[num_cols]], axis=1)
X_train_imputed = num_transformer.fit_transform(X_train_cat_imputed_df)
X_test_imputed = num_transformer.transform(X_test_cat_imputed_df)
X_train_transformed = pd.DataFrame(data=X_train_imputed,
                                   columns=num_transformer.get_feature_names_out(),
                                   index=X_train.index)
X_test_transformed = pd.DataFrame(data=X_test_imputed,
                                  columns=num_transformer.get_feature_names_out(),
                                  index=X_test.index)
在将完整数据集传递给机器学习模型之前,我们检查训练和测试标签是否具有相似的贷款批准率:
y_train.mean(), y_test.mean()
(0.6865942028985508, 0.6935483870967742)
由于类别略微不平衡,我们可以使用class_weight='balanced'选项,因为这个选项使用y的值在训练算法时自动调整与输入数据中类别频率成反比的权重。问题的目标是识别出优于可能获得贷款的人类。由于大多数类别是在收到贷款的人身上训练的,因此模型将偏向于给某人发放贷款。通过使用class_weight='balanced',算法将更多地强调类别标签 0,因为它是一个少数类别。
我们定义了决策树分类器的网格搜索以执行交叉验证,以确保模型具有泛化能力:
d_param_grid = {
    'max_features': [None, 'sqrt', 'log2'],
    'max_depth' : [4,5,6,7,8,10,20],
    'min_samples_leaf' : [1,3,5,8,10,12,15],
    'min_samples_split': [2,6,10,16,20,24,30],
    'criterion' : ['gini', 'entropy'],
    'random_state' : [1],
    'class_weight' : ['balanced']
}
d_clf = DecisionTreeClassifier()
接下来,我们创建一个自定义函数,该函数将接受训练数据、测试数据、分类器和网格搜索参数。该函数执行 10K 交叉验证以找到最佳超参数,并在最佳参数上训练模型。然后,该函数返回模型、预测、训练和测试准确率以及 ROC-AUC 分数:
def train_custom_classifier(X_train, y_train, X_test, y_test, clf, params):
    """Function to train the decision tree classifier and return some metrics"""
    d_clf_cv = GridSearchCV(estimator=d_clf, param_grid=d_param_grid, cv=10, scoring='roc_auc')
    d_clf_cv.fit(X_train_transformed, y_train)
    print("Decision tree optimised")
    d_best_params = d_clf_cv.best_params_
    print(f"Getting the best params which are {d_best_params}")
    model = DecisionTreeClassifier(**d_best_params)
    model.fit(X_train_transformed, y_train)
    training_predictions_prob = model.predict_proba(X_train_transformed)
    testing_predictions_prob = model.predict_proba(X_test_transformed)
    training_predictions = model.predict(X_train_transformed)
    testing_predictions = model.predict(X_test_transformed)
    training_roc_auc = roc_auc_score(y_train, training_predictions_prob[:,1])
    testing_roc_auc = roc_auc_score(y_test, testing_predictions_prob[:,1])
    training_acc = accuracy_score(y_train, training_predictions)
    testing_acc = accuracy_score(y_test, testing_predictions)
    print(f"Training roc is {training_roc_auc}, and testing roc is {testing_roc_auc} \n \
            training accuracy is {training_acc}, testing_acc as {testing_acc}")
    return model, testing_predictions, training_roc_auc, testing_roc_auc, training_acc, testing_acc
接下来,我们运行自定义分类器并计算模型性能:
model, test_predictions, train_roc, test_roc, train_acc, test_acc  = train_custom_classifier(
    X_train=X_train_transformed,
    y_train=y_train,
    X_test=X_test_transformed,
    y_test=y_test,
    clf=d_clf,
    params=d_param_grid
)
Decision tree optimised
Getting the best params which are {'class_weight': 'balanced', 'criterion': 'entropy', 'max_depth': 8, 'max_features': None, 'min_samples_leaf': 1, 'min_samples_split': 30, 'random_state': 1}
Training roc is 0.8763326063416048, and testing roc is 0.7858017135862914
             training accuracy is 0.8152173913043478, testing_acc as 0.7903225806451613
测试准确率略低于 80%。让我们通过观察混淆矩阵来查看模型在哪些方面表现不佳:
cm = confusion_matrix(y_test, test_predictions, labels=model.classes_, normalize='true')
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=model.classes_)
disp.plot()
这将输出以下混淆矩阵:

图 5.14 – 混淆矩阵
我们现在已应用了带有 MICE 插补的机器学习管道来创建机器学习模型。为了证明 MICE 插补技术比简单插补技术更好,我们将使用简单插补方法重新创建机器学习管道并评估模型性能。
一旦我们创建了管道步骤,我们将在将其传递给决策树分类器和自定义分类器函数以衡量模型性能之前,对训练数据和测试数据进行转换:
cat_transformer = Pipeline(
    steps=[
        ("one_hot_encoding",
         OneHotEncoder(variables=ohe_cols)
        )
    ]
)
impute_transformer = Pipeline(
    steps=[
        ("simple_imputer",
         SimpleImputer(strategy='median',
                       add_indicator=True)
        )
    ]
)
X_train_ohe = cat_transformer.fit_transform(X_train)
X_test_ohe = cat_transformer.transform(X_test)
X_train_imputed = impute_transformer.fit_transform(X_train_ohe)
X_test_imputed = impute_transformer.transform(X_test_ohe)
X_train_transformed = pd.DataFrame(data=X_train_imputed,
                                   columns=impute_transformer.get_feature_names_out(),
                                   index=X_train.index)
X_test_transformed = pd.DataFrame(data=X_test_imputed,
                                  columns=impute_transformer.get_feature_names_out(),
                                  index=X_test.index)
接下来,我们运行自定义分类器并提取模型性能:
model, test_predictions, train_roc, test_roc, train_acc, test_acc = train_custom_classifier(
    X_train=X_train_transformed,
    y_train=y_train,
    X_test=X_test_transformed,
    y_test=y_test,
    clf=d_clf,
    params=d_param_grid
)
测试准确率下降到低于 67%,下降了 12%,ROC-AUC 下降了 6%。接下来,我们回顾混淆矩阵:
cm = confusion_matrix(y_test, test_predictions, labels=model.classes_, normalize='true')
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=model.classes_)
disp.plot()
这是输出:

图 5.15 – 混淆矩阵
真阳性类的准确率从 88% 下降到 67%,而阴性类的准确率从 58% 上升到 63%。通过使用基本的插补技术,我们可以得出结论,该模型更有可能存在偏差,模型性能可能不够准确。
在以数据为中心的机器学习中,目标是改进数据并调整它,而不是改进算法和调整模型。但如何确定一个数据集是否包含标签错误的数据、缺失特征或其他数据相关的问题?我们将在第六章中介绍如何识别数据标签错误并应用技术来改进错误标记的数据,机器学习中的程序化标签技术。
为了找出是否需要更多特征或更多数据,我们利用一种称为错误分析的技术。在机器学习中,错误分析用于通过关注模型表现良好和表现不佳的数据区域来识别和诊断错误的预测。尽管模型的总体性能可能为 79%,但这种性能可能不会在整个数据区域中均匀分布,这些高低起伏可能是由某些区域存在的输入和在其他区域缺失的输入造成的。
为了识别数据问题,我们将使用 10% 的数据进行模型训练,并在每次迭代中增加 10%。然后,我们绘制训练 ROC 曲线和测试 ROC 曲线,以测试数据规模增加的情况。如果图表似乎收敛并表明数据规模增加,这将导致测试 ROC 的提高,此时我们将生成合成数据以增加数据规模。这项技术将在第七章中介绍,以数据为中心的机器学习中的合成数据使用。
如果图表似乎没有收敛,并表明数据增加,它将对提高测试 ROC 产生最小的影响。在这种情况下,我们可以观察到模型表现不佳的数据点,并可能利用特征工程生成新的列。尽管特征工程可能是一种迭代方法,但就本章的范围而言,我们只涵盖添加一个或两个特征。
要运行错误分析,首先,我们创建从 0.1 到 1.0 的数据截止点,其中 0.1 表示 10% 的训练数据,1.0 表示 100% 的训练数据:
data_cutoff_points = np.linspace(start=0.1, stop=1, num=10)
data_cutoff_points
array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1\. ])
接下来,我们创建一个名为 scores 的空列表,并对每个数据截止点进行数据预处理、模型训练和评估。如果截止点小于 1.0,我们将子集训练数据;否则,我们将所有数据传递用于训练。在每个迭代结束时,我们将截止点、训练和测试评估指标保存到 scores 中,通过将指标追加到 scores 列表:
scores = []
for cutoff in data_cutoff_points:
    if cutoff < 1.0:
        X_train_subset, X_train_rem, y_train_subset, y_train_rem = train_test_split(X_train,
                     y_train,
                          random_state=1,
                             train_size=cutoff,
                        stratify=y_train)
    else:
        X_train_subset = X_train.copy()
        y_train_subset = y_train.copy()
    print(f"Model will be trained on {X_train_subset.shape[0]} rows out of {X_train.shape[0]}")
    cat_transformer = miss_forest_categorical_transformer()
    num_transformer = miss_forest_numerical_transformer()
    X_train_cat_imputed = cat_transformer.fit_transform(X_train_subset[cat_cols])
    X_test_cat_imputed = cat_transformer.transform(X_test[cat_cols])
    X_train_cat_imputed_df = pd.DataFrame(data=X_train_cat_imputed,
                                          columns=cat_transformer.get_feature_names_out(),
                                          index=X_train_subset.index)
    X_test_cat_imputed_df = pd.DataFrame(data=X_test_cat_imputed,
                                         columns=cat_transformer.get_feature_names_out(),
                                         index=X_test.index)
    X_train_cat_imputed_df = pd.concat([X_train_cat_imputed_df, X_train_subset[num_cols]], axis=1)
    X_test_cat_imputed_df = pd.concat([X_test_cat_imputed_df, X_test[num_cols]], axis=1)
    X_train_imputed = num_transformer.fit_transform(X_train_cat_imputed_df)
    X_test_imputed = num_transformer.transform(X_test_cat_imputed_df)
    X_train_transformed = pd.DataFrame(data=X_train_imputed,
                                       columns=num_transformer.get_feature_names_out(),
                                       index=X_train_subset.index)
    X_test_transformed = pd.DataFrame(data=X_test_imputed,
                                      columns=num_transformer.get_feature_names_out(),
                                      index=X_test.index)
    model, test_predictions, train_roc, test_roc, train_acc, test_acc = train_custom_classifier(
        X_train=X_train_transformed,
        y_train=y_train_subset,
        X_test=X_test_transformed,
        y_test=y_test,
        clf=d_clf,
        params=d_param_grid)
    scores.append((cutoff, train_roc, test_roc, train_acc, test_acc))
Model will be trained on 55 rows out of 552
Training roc is 0.9094427244582044, and testing roc is 0.5917992656058751
             training accuracy is 0.7454545454545455, testing_acc as 0.5806451612903226
Model will be trained on 110 rows out of 552
Training roc is 0.901702786377709, and testing roc is 0.7552019583843328
             training accuracy is 0.7272727272727273, testing_acc as 0.6290322580645161
Model will be trained on 165 rows out of 552
Training roc is 0.8986555479918311, and testing roc is 0.7099143206854346
             training accuracy is 0.7696969696969697, testing_acc as 0.5967741935483871
Model will be trained on 220 rows out of 552
Training roc is 0.8207601497264613, and testing roc is 0.8084455324357405
             training accuracy is 0.8318181818181818, testing_acc as 0.8064516129032258
Model will be trained on 276 rows out of 552
Training roc is 0.8728942407103326, and testing roc is 0.7906976744186047
             training accuracy is 0.822463768115942, testing_acc as 0.7419354838709677
Model will be trained on 331 rows out of 552
Training roc is 0.9344501863774991, and testing roc is 0.7753977968176254
             training accuracy is 0.8368580060422961, testing_acc as 0.7419354838709677
Model will be trained on 386 rows out of 552
Training roc is 0.8977545610478715, and testing roc is 0.7184822521419829
             training accuracy is 0.7849740932642487, testing_acc as 0.6612903225806451
Model will be trained on 441 rows out of 552
Training roc is 0.8954656335198737, and testing roc is 0.7429620563035496
             training accuracy is 0.81859410430839, testing_acc as 0.7258064516129032
Model will be trained on 496 rows out of 552
Training roc is 0.9102355500898685, and testing roc is 0.7441860465116278
             training accuracy is 0.8266129032258065, testing_acc as 0.7258064516129032
Model will be trained on 552 rows out of 552
Training roc is 0.8763326063416048, and testing roc is 0.7858017135862914
             training accuracy is 0.8152173913043478, testing_acc as 0.7903225806451613
接下来,我们从 scores 列表创建一个 DataFrame,并传递相关的列名:
df = pd.DataFrame(data=scores, columns=['data_size', 'training_roc', 'testing_roc', "training_acc", "testing_acc"])
然后,我们绘制训练和测试 ROC 与每个截止点的对比图:
plt.plot(df.data_size, df.training_roc, label='training_roc')
plt.plot(df.data_size, df.testing_roc, label='testing_roc')
plt.xlabel("Data Size")
plt.ylabel("ROC")
plt.title("Error Analysis")
plt.legend()
这将输出以下图表:

图 5.15 – 错误分析训练和测试 ROC
接下来,绘制训练和测试准确率与每个截止点的对比图:
plt.plot(df.data_size, df.training_acc, label='training_acc')
plt.plot(df.data_size, df.testing_acc, label='testing_acc')
plt.xlabel("Data Size")
plt.ylabel("Accuracy")
plt.title("Error Analysis")
plt.legend()
这将输出以下图表:

图 5.17 – 错误分析训练和测试准确率
测试 ROC 和测试准确率似乎显示出与训练 ROC 和训练准确率收敛的迹象,这表明如果提供更多的数据点,模型性能可能会得到提升。这就是为什么我们将在下一章生成模拟数据(模仿真实数据的数据)并使用添加的数据重新训练模型以获得更好的模型性能。
正如我们在前面的章节中学到的,数据为中心的机器学习的原则之一是让人类参与其中。让我们想象我们与领域专家进行了交谈,他们提到,一个人能否获得贷款的关键决定因素之一是收入与债务比率——即总收入除以贷款金额。这决定了一个人是否能够偿还贷款。收入与贷款比率较低的申请更有可能被拒绝。在数据集中,有两个收入变量——申请人收入和共同申请人收入。此外,贷款金额以千位表示——即数据中的贷款金额 66 代表 66,000。为了创建这个比率,我们将贷款金额乘以 1,000,然后结合申请人和共同申请人的收入。完成这些后,我们将合并的收入除以贷款金额以获得收入与贷款比率。领域专家还提到,等额本息还款(EMIs)也可以决定候选人的还款能力。EMI 越低,贷款被接受的可能性越大,而 EMI 越高,贷款被拒绝的可能性越大。为了在没有利率的情况下计算这个值,我们可以使用贷款期限和贷款金额来得到每月的近似 EMI 金额。
对于收入与贷款比率,我们将创建一个自定义转换器,将贷款金额乘以 1,000,以便我们可以在管道中使用它。
这个转换器是一个 Python 类,我们可以用它来覆盖管道所需的 fit 和 transform 函数。这个类将继承自BaseEstimator和TransformerMixin类,这两个类都可以在sklearn.base模块中找到。这个类将用于实现 fit 和 transform 方法。这些方法应该包含X和y参数,transform 方法应该返回一个 pandas DataFrame 以确保与 scikit-learn 管道的兼容性。
为了创建完整的收入列,我们利用feature_engine库,因为它已经与 scikit-learn 管道兼容,并且有应用于其他变量的数学运算方法。首先,我们求和收入变量。这个转换的输出将除以loan_amount变量以创建收入与贷款比率。
为了创建 EMI,我们利用feature_engine库,将loan_amount除以loan_amount_term。一旦我们创建了这些特征,我们就移除了两个收入变量,因为我们已经创建了这两个变量的组合。对于这一步,我们使用feature_engine库中的DropFeatures类。所有这些特征工程步骤将组合在一个新的名为feature-transformer的管道中,并在数据插补后应用。
我们相信,通过添加这些额外特征,决策树算法的模型性能将得到提高。让我们在特征工程后运行算法并评估结果。
首先,我们创建用于特征工程步骤的自定义变量,它将接受一个变量列表:
income_variables = ['applicant_income', 'coapplicant_income']
loan_variable = ['loan_amount']
loan_term_variable = ['loan_amount_term']
接下来,我们从 feature_engine 导入相关包以执行特征工程步骤,并导入 BaseEstimator 和 TransformerMixin 类:
from feature_engine.creation.math_features import MathFeatures
from feature_engine.creation.relative_features import RelativeFeatures
from sklearn.base import BaseEstimator, TransformerMixin
from feature_engine.selection import DropFeatures
然后,我们创建一个自定义转换器,它将接受变量名和一个将被每个变量乘以的值。默认情况下,每个变量将被乘以 1:
class MultiplyColumns(BaseEstimator, TransformerMixin):
    """Custom pipeline class to multiply columns passed in a DataFrame with a value"""
    def __init__(self, multiply_by=1, variables=None):
        self.multiply_by = multiply_by
        self.variables = variables
    def fit(self, X, y=None):
        return self
    def transform(self, X, y=None):
        if self.variables:
            X[self.variables] = X[self.variables] * self.multiply_by
        return X
接下来,我们调用之前创建的 missForest 类别和数值转换器。完成此操作后,我们创建一个特征转换器管道,利用之前创建的自定义转换器将 loan_amount 乘以 1,000。新的管道然后将收入变量添加到一个收入变量中,即收入与贷款比率,以及 EMI 特征。最后,管道删除了两个收入变量,因为将创建新的收入变量。通过使用转换器管道,训练和测试数据将被转换,并创建新特征。这一步骤的输出将完全转换为训练和测试数据,以便可以传递给自定义分类器:
cat_transformer = miss_forest_categorical_transformer()
num_transformer = miss_forest_numerical_transformer()
feature_transformer = Pipeline(
    steps=[
        ("multiply_by_thousand",
         MultiplyColumns(
             multiply_by=1000,
             variables=loan_variable
         )
        ),
        ("add_columns",
         MathFeatures(
             variables=income_variables,
             func='sum'
         )
        ),
        ("income_to_loan_ratio",
         RelativeFeatures(variables=[f"sum_{income_variables[0]}_{income_variables[1]}"],
                          reference=loan_variable,
                          func=["div"]
                         )
        ),
        ("emi",
         RelativeFeatures(variables=loan_variable,
                          reference=loan_term_variable,
                          func=["div"])
        ),
        ("drop_features",
         DropFeatures(features_to_drop=income_variables
          ))
    ]
)
接下来,我们创建用于插补的类别转换器:
X_train_cat_imputed = cat_transformer.fit_transform(X_train[cat_cols])
X_test_cat_imputed = cat_transformer.transform(X_test[cat_cols])
X_train_cat_imputed_df = pd.DataFrame(data=X_train_cat_imputed,
                                      columns=cat_transformer.get_feature_names_out(),
                                      index=X_train.index)
X_test_cat_imputed_df = pd.DataFrame(data=X_test_cat_imputed,
                                     columns=cat_transformer.get_feature_names_out(),
                                     index=X_test.index)
X_train_cat_imputed_df = pd.concat([X_train_cat_imputed_df, X_train[num_cols]], axis=1)
X_test_cat_imputed_df = pd.concat([X_test_cat_imputed_df, X_test[num_cols]], axis=1)
然后,我们添加数值插补并完成插补步骤:
X_train_imputed = num_transformer.fit_transform(X_train_cat_imputed_df)
X_test_imputed = num_transformer.transform(X_test_cat_imputed_df)
X_train_imputed_df = pd.DataFrame(data=X_train_imputed,
                                   columns=num_transformer.get_feature_names_out(),
                                   index=X_train.index)
X_test_imputed_df = pd.DataFrame(data=X_test_imputed,
                                  columns=num_transformer.get_feature_names_out(),
                                  index=X_test.index)
接下来,我们使用之前创建的特征插补管道对缺失数据进行转换:
X_train_transformed = feature_transformer.fit_transform(X_train_imputed_df)
X_test_transformed = feature_transformer.transform(X_test_imputed_df)
在这一点上,我们调用自定义分类器函数,通过添加特征工程步骤来评估模型性能:
model, test_predictions, train_roc, test_roc, train_acc, test_acc = train_custom_classifier(
    X_train=X_train_transformed,
    y_train=y_train,
    X_test=X_test_transformed,
    y_test=y_test,
    clf=d_clf,
    params=d_param_grid)
Training roc is 0.8465996614150411, and testing roc is 0.8188494492044063
             training accuracy is 0.8206521739130435, testing_acc as 0.8225806451612904
接下来,我们调用混淆矩阵:
cm = confusion_matrix(y_test, test_predictions, labels=model.classes_, normalize='true')
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=model.classes_)
disp.plot()
生成的混淆矩阵如下:

图 5.18 – 使用自定义特征工程时的混淆矩阵
我们的测试准确率从 79% 提高到 82%,ROC 从 78.5% 提高到 81.8%。前面的混淆矩阵显示,正类准确率从 88% 提高到 91%,而负类准确率从 58% 提高到 63%。
通过这种方式,我们已经证明了通过使用以数据为中心的方法,我们可以遍历数据而不是遍历多个算法,并设法提高模型性能。我们将在下一章探讨如何添加一些合成数据,并进一步提高模型性能。
确保数据有效
到目前为止,我们已经确保我们的数据是一致的、唯一的和完整的。但我们是否知道我们拥有的数据是有效的?数据标签是否符合规则?例如,如果数据集中的属性区域不符合规则,semi_urban是无效的怎么办?如果一位或几位标注者认为一些郊区既不是城市也不是乡村,并且违反了规则,输入了semi_urban怎么办?为了衡量有效性,我们可能需要查看业务规则并检查符合这些业务规则的数据百分比。让我们假设semi_urban是一个无效值。在 Python 中,我们可以检查无效标签的百分比,然后联系标注者纠正数据。我们也可以通过使用用于生成标签的数据来实现这一点。如果我们有suburb_name到property_area的数据映射,并且suburb_name在数据集中可用,那么我们可以利用这个映射来捕获无效值,以及通过编程方式编码标签。在系统中构建业务规则,以便自动编码即将到来的数据,这被称为编程标签。我们将在接下来的章节中深入探讨编程标签,我们将探讨在数据捕获时使数据一致和有效的技术,这样当数据到来时,它已经是纯净的,并且一些数据清理过程将是多余的。
首先,我们创建一个包含 10 行数据的假数据集,并编写一个业务规则。它将包含一个id列,其值为 1 到 10,一个population列,包含 1,000 到 100,000 之间的 10 个随机值,以及一个property_area列,其中四个值设置为urban,五个值设置为semi_urban,一个值设置为rural:
np.random.seed(1)
data = {
    "id": np.linspace(start=1, stop=10, num=10, dtype=int),
    "population" : np.random.randint(low=1000, high=100000, size=10),
    "property_area": ["urban"]*4 + ["semi_urban"]*5 + ["rural"]*1
}
df = pd.DataFrame(data=data)
接下来,我们打印前五行:
df.head()
   id  population property_area
0   1       99539         urban
1   2       78708         urban
2   3        6192         urban
3   4       99047         urban
4   5       51057    semi_urban
假设业务规则说,当人口超过 20,000 时,郊区或property_area被归类为城市;否则,被归类为乡村。在这种情况下,验证规则应该检查property_area是否只包含urban或rural值。
在 Python 中检查这一点的简单方法是在value_counts()方法旁边使用normalize=True参数。这个输出的结果将显示 50%的数据是无效的:
df.property_area.value_counts(normalize=True)
semi_urban    0.5
urban         0.4
rural         0.1
Name: property_area, dtype: float64
接下来,我们可以对每一行运行检查,并在值位于预期值列表中时标记,以及当值不在预期值集中时标记:
df.property_area.isin(['rural', 'urban']) == False
0    False
1    False
2    False
3    False
4     True
5     True
6     True
7     True
8     True
9    False
Name: property_area, dtype: bool
现在,我们将违反数据验证规则的数据行求和,并将无效行数除以总行数,以提供一个指标——即无效标签的百分比:
sum(df.property_area.isin(['rural', 'urban']) == False) / df.shape[0]
0.5
无效数据必须通知数据源提供者,并且必须进行清理;否则,这些数据可能会悄悄进入,机器学习模型将学习这些无效标签。当使用有效数据进行训练时,模型的学习能力显著提高,因为它提供了更强的标签信号,与无效数据相比,无效数据由于对有效标签点的接触减少而削弱了这些信号。
确保数据准确
即使数据是有效的,它可能也不准确。数据准确性衡量的是与真实世界数据或可验证来源匹配的数据百分比。考虑到前面的property_area示例,为了衡量数据准确性,我们可能需要查找一个可靠的已发布数据集,并检查该地区的人口和地区类型。假设人口与可验证的数据源匹配,但地区类型来源不可用。使用定义农村地区和城市地区的规则,我们可以衡量数据准确性。
使用这个业务规则,我们将创建一个新的标签true_property_area,当人口在 20,000 人或以下时,其值为rural;否则,其值为urban:
df['true_property_area'] = df.population.apply(lambda value: 'rural' if value <= 20000 else 'urban')
接下来,我们将打印数据集的行以查看property_area和true_property_area之间是否存在任何不匹配:
df[['true_property_area', 'property_area', 'population']]
  true_property_area property_area  population
0              urban         urban       99539
1              urban         urban       78708
2              rural         urban        6192
3              urban         urban       99047
4              urban    semi_urban       51057
5              urban    semi_urban       74349
6              urban    semi_urban       22440
7              urban    semi_urban       99448
8              urban    semi_urban       21609
9              urban         rural       50100
然后,我们将匹配property_area值与真实值的行求和,然后除以总行数以计算数据准确性:
sum(df.property_area == df.true_property_area) / df.shape[0]
0.3
我们可以不创建一个函数来计算准确性,而是利用 scikit-learn 中的accuracy_score:
accuracy_score(y_pred=df.property_area, y_true=df.true_property_area)
0.3
如我们所见,两种方法都返回了相同的分数。如果错误的数据进入系统,模型可能会对半城市和农村地区学习不准确,并在推理时产生不理想的结果。
确保数据新鲜
数据新鲜度是衡量数据质量的一个重要方面,它对机器学习应用的质量和鲁棒性有影响。让我们想象一下,我们有一个在 2019 年和 2020 年客户行为上训练的机器学习应用,并用于预测到 2021 年 4 月的酒店房间预订。也许一月份和二月份的数字相当准确,但当三月份和四月份到来时,准确性下降。这可能是由于 COVID-19,这是数据未看到的情况,其影响没有被捕捉到。在机器学习中,这被称为数据漂移。这种情况正在发生;三月份和四月份的数据分布与 2019 年和 2020 年的数据分布有很大不同。通过确保数据新鲜且更新,我们可以更频繁地训练模型,或者在检测到数据漂移时立即训练。
为了测量数据漂移,我们将使用alibi Python 包。然而,还有更多广泛的 Python 包可以帮助完成这项工作。我们推荐 Evidently AI (www.evidentlyai.com/),这是一个数据与机器学习模型监控工具包,或者 WhyLogs (whylabs.ai/whylogs),这是 WhyLabs 的一个开源倡议,用于监控模型降级和数据漂移。
让我们假设,当模型在超过 5 天的旧数据上训练时,模型准确性开始下降,而当数据超过 10 天时,模型表现不佳并开始给企业带来成本。我们希望能够在这种情况发生时发出警报并捕获它。为了演示这个场景,我们将创建一个包含日期列的样本数据集,并定义错误和警告阈值——也就是说,如果数据是 5 天前的,我们打印警告;如果数据超过 10 天,我们阻止应用程序。在实践中,建议使用最新的可用数据进行模型训练。遵循以数据为中心的方法,我们必须鼓励从业者与数据提供者定义阈值和服务级别协议(SLAs),以便他们有机制来请求最新的数据,当 SLA 被违反时进行处罚,当 SLA 得到满足时进行奖励(鼓励保持高质量数据的重要性)。
现在,我们将生成 100 个样本数据点,并演示如何使用日期变量来识别数据是否过时。
我们使用alibi包来检测loan_prediction数据集中的漂移。我们将通过比较漂移前后的准确率来展示不检测和采取数据漂移措施的危害。
首先,我们导入datetime和warning包:
from datetime import datetime, timedelta
import warnings
接下来,我们生成一个基准日期——比如说我们运行代码的日期——然后从基准日期开始,通过每天减去一天来生成 100 个过去的日期:
numdays = 100
base = datetime.today()
date_list = [base - timedelta(days=day) for day in range(numdays)] # Subracting values from 1 to 100 from todays date
然后,我们按生成日期的顺序打印前 10 个日期,最近的日期是第一个日期:
[date.date().strftime('%Y-%m-%d') for date in date_list[0:10]]
['2023-02-04',
 '2023-02-03',
 '2023-02-02',
 '2023-02-01',
 '2023-01-31',
 '2023-01-30',
 '2023-01-29',
 '2023-01-28',
 '2023-01-27',
 '2023-01-26']
接下来,我们创建一个包含 100 行的 DataFrame,通过创建四个列来实现。它将包含一个id列,其值为 1 到 100,一个date_loaded列,包含我们之前创建的 100 个日期,一个population列,包含 100 个介于 1,000 到 100,000 之间的随机值,以及一个property_area列,其中 40 个值设置为urban,50 个值设置为semi_urban,10 个值设置为rural:
np.random.seed(1)
data = {
    "id": np.linspace(start=1, stop=100, num=100, dtype=int),
    "population" : np.random.randint(low=1000, high=100000, size=100),
    "property_area": ["urban"]*40 + ["semi_urban"]*50 + ["rural"]*10,
    "date_loaded": date_list
}
df = pd.DataFrame(data=data)
现在,我们可视化前五个数据点:
df.head()
   id  population property_area                date_loaded
0   1       99539         urban 2023-02-04 11:18:46.771142
1   2       78708         urban 2023-02-03 11:18:46.771142
2   3        6192         urban 2023-02-02 11:18:46.771142
3   4       99047         urban 2023-02-01 11:18:46.771142
4   5       51057         urban 2023-01-31 11:18:46.771142
接下来,我们编写一行代码来演示从今天日期减去任何日期并提取两个日期之间天数的方法:
(datetime.now() - df.date_loaded.max()).days
0
然后,我们创建一个函数,该函数将接受一个 DataFrame 及其日期列,默认情况下,如果数据超过 5 天,将发出警告;如果数据超过 10 天,将阻止应用程序:
def check_data_recency_days(df: pd.DataFrame, loaded_at_column: str, warning_at: int=5, error_at: int=10):
    """Function to detect data freshness"""
    df = df.copy()
    days_since_data_refreshed = (datetime.now() - df[loaded_at_column].max()).days
    if days_since_data_refreshed < warning_at:
        print(f"Data is fresh and is {days_since_data_refreshed} days old")
    elif error_at > days_since_data_refreshed >= warning_at:
        warnings.warn(f"Warning: Data is not fresh, and is {days_since_data_refreshed} days old")
    else:
        raise ValueError(f"Date provided is too old and stale, please contact source provider: {days_since_data_refreshed} days old")
接下来,我们使用之前创建的样本 DataFrame 运行该函数。该函数将声明数据是新鲜的,只有 0 天:
check_data_recency_days(df, "date_loaded")
Data is fresh and is 0 days old
为了展示函数在数据过时时的警告或错误输出能力,我们通过删除 6 天和 12 天内的数据来对数据进行子集划分。我们创建了两个 DataFrame——一个删除了 6 天内的数据,另一个删除了 12 天内的数据。然后,我们在这些 DataFrame 上运行check_data_recency_day函数。我们看到,当我们用 6 天前的数据运行函数时,函数将发出警告,但当我们用 12 天前的数据运行函数时,函数将发出一个Value错误。
让我们创建两个 DataFrame:
df_filter_6_days = df[df.date_loaded <= (datetime.today() -  timedelta(days=6))]
df_filter_12_days = df[df.date_loaded <= (datetime.today() -  timedelta(days=12))]
接下来,我们对 6 天前的数据进行函数运行:
check_data_recency_days(df_filter_6_days, "date_loaded")
/var/folders/6f/p7312_7n4nq5hp35rfymms1h0000gn/T/ipykernel_5374/1750573000.py:11: UserWarning: Warning: Data is not fresh, and is 6 days old
  warnings.warn(f"Warning: Data is not fresh, and is {days_since_data_refreshed} days old")
您也可以对 12 天前的数据进行函数运行;它将生成类似的输出。
有了这些,我们已经展示了如何测量数据的新鲜度,捕捉警告,并在数据极度过时时阻止应用程序。接下来,我们将展示数据新鲜度对实际数据集的影响。
在现实生活中,我们不会期望一家公司不改变其产品,或者消费者行为不会随着市场上新产品的出现而改变。公司必须不断研究消费者行为的变化;否则,他们的业绩会下降。机器学习系统面临着市场力量变化、数据变化和数据分布变化相同的问题。如果新数据与训练数据大相径庭,这将对模型性能产生影响。
这在机器学习中被称为漂移,如果未检测到且未得到处理,它会导致模型退化。
让我们探索如何检测漂移。
首先,我们从alibi-detect包中导入TabularDrift:
import alibi
from alibi_detect.cd import TabularDrift
接下来,我们展示TabularDrift参考数据,这是机器学习系统训练的数据——在我们的案例中,是在我们将数据传递给决策树分类器之前转换的贷款预测数据。我们还为 p 值测试传递了一个值为0.05的值。如果测试数据分布违反了此值,该包将通知我们测试数据已从训练数据中漂移:
cd = TabularDrift(x_ref=X_train_transformed.to_numpy(), p_val=.05 )
现在,我们运行predict方法来检查测试数据是否发生了漂移。alibi包使用Kolmogorov-Smirnov测试来确定两个分布是否不同。如果 p 值超过 0.05,则拒绝零假设,可以推断出test数据分布与train数据分布不同。这一步骤的输出将是No:
preds = cd.predict(X_test_transformed.to_numpy())
labels = ['No', 'Yes']
print('Drift: {}'.format(labels[preds['data']['is_drift']]))
Drift: No
现在,让我们假设房价开始飙升,而收入并没有以相同的速度增长。为了模拟这种情况,我们将贷款金额增加到原始测试集的 1.5 倍,但将总收入增加到测试集的 1.2 倍。然后,我们更新依赖于loan_amount和income变量的新特征值:
 X_test_transformed['loan_amount'] = X_test_transformed['loan_amount']*1.5
X_test_transformed['sum_applicant_income_coapplicant_income'] = X_test_transformed['sum_applicant_income_coapplicant_income']*1.2
X_test_transformed.sum_applicant_income_coapplicant_income_div_loan_amount = X_test_transformed.sum_applicant_income_coapplicant_income/X_test_transformed.loan_amount
X_test_transformed.loan_amount_div_loan_amount_term = X_test_transformed.loan_amount/X_test_transformed.loan_amount_term
接下来,我们再次运行 TabularDrift 的predict方法来检查是否检测到漂移。这一步骤的输出是Yes:
preds = cd.predict(X_test_transformed.to_numpy())
labels = ['No', 'Yes']
print('Drift: {}'.format(labels[preds['data']['is_drift']]))
Drift: Yes
然后,我们对由漂移引起的测试数据进行重新预测,并检查准确率和 ROC 是否受到影响:
testing_predictions_prob = model.predict_proba(X_test_transformed)
testing_predictions = model.predict(X_test_transformed)
testing_roc_auc = roc_auc_score(y_test, testing_predictions_prob[:,1])
testing_acc = accuracy_score(y_test, testing_predictions)
print(f"Testing roc is {testing_roc_auc} and testing_acc as {testing_acc}")
Testing roc is 0.747858017135863 and testing_acc as 0.6935483870967742
如我们所见,模型在训练过程中所看到的分布与真实数据不同,其影响是模型性能显著下降。ROC 值从 0.82 降至 0.74,准确率从 82%降至 70%。因此,确保数据新鲜非常重要,一旦检测到数据漂移,就需要用新数据重新训练模型,以确保模型性能不会下降。
摘要
在本章中,我们深入了解了数据质量的六个关键维度以及为什么提高数据质量对于提高模型性能至关重要。我们进一步探讨了通过迭代数据来提高模型性能的数据中心方法,而不是迭代各种算法(模型中心方法),通过提高数据的整体健康状况来实现。
接下来,我们学习了如何确保数据的一致性、唯一性、准确性、有效性、新鲜性和完整性。我们深入探讨了各种填充缺失值的技巧以及何时应用哪种方法。我们得出结论,使用机器学习填充缺失值可能比使用简单的填充方法更好,尤其是在数据是 MAR 或 MNAR 的情况下。我们还展示了如何进行错误分析,以及如何利用这些结果通过执行特征工程(涉及构建新特征)或通过创建合成数据来增加数据量,从而进一步提高模型性能,这些内容将在下一章中介绍。
我们还讨论了为什么确保数据新鲜且未从原始训练集中漂移很重要,并得出结论,漂移数据可能会损害模型性能。
现在我们已经理解了确保数据质量在数据质量的六个关键维度中的重要性,在下一章中,我们将深入探讨使用合成数据来进一步提高模型性能,特别是在边缘情况中。我们还将深入探讨数据增强技术,这是一种用于为图像创建合成数据的技术,以便算法可以从更多更好的数据中学习,尤其是在这些新示例可以以各种形式出现时。
第六章:机器学习中的程序化标记技术
在机器学习中,数据的准确标记对于训练有效的模型至关重要。数据标记涉及将有意义的类别或类分配给数据实例,虽然传统上是一个由人类驱动的流程,但存在各种程序化方法来标记数据集。本章深入探讨了机器学习中以下程序化数据标记方法:
- 
模式匹配
 - 
数据库(DB)查找
 - 
布尔标志
 - 
弱监督
 - 
半弱监督
 - 
切片函数
 - 
活动学习
 - 
迁移学习
 - 
半监督学习
 
技术要求
要执行本章提供的程序化标记技术示例,请确保您在 Python 环境中安装了以下技术先决条件:
Python 版本
本章中的示例需要 Python 版本 3.7 或更高。您可以通过运行以下命令来检查您的 Python 版本:
import sys
print(sys.version)
我们建议使用 Jupyter Notebook 集成开发环境(IDE)以获得交互式和有组织的编码体验。如果您尚未安装它,可以使用以下命令进行安装:
pip install jupyter
使用以下命令启动 Jupyter Notebook:
jupyter notebook
库要求
确保在您的环境中安装以下 Python 包。您可以使用以下命令进行安装:
pip install snorkel
pip install scikit-learn
pip install Pillow
pip install tensorflow
pip install pandas
pip install numpy
此外,对于 TensorFlow 和 Keras 组件,您可能需要 GPU 支持以获得最佳性能。如果您有兼容的 GPU,请参阅 TensorFlow 文档中的 GPU 安装说明。
模式匹配
在机器学习中,最重要的任务之一是根据某些标准或模式对数据进行标记或分类。然而,手动标记数据可能既耗时又昂贵,尤其是在处理大量数据时。通过利用预定义的模式,这种标记方法能够自动将有意义的类别或类分配给数据实例。
模式匹配涉及在数据中识别特定的模式或序列,这些模式或序列可以用作分配标签的指示器。这些模式可以使用正则表达式、基于规则的系统或其他模式识别算法来定义。目标是捕获数据中的相关信息和特征,以便与预定义的模式匹配,以准确推断标签。
模式匹配可以应用于机器学习中的各种领域和场景。以下是一些常见应用:
- 
文本分类:在自然语言处理中,模式匹配可以用来根据特定的关键词、短语或句法模式对文本数据进行标记。这使任务如情感分析、垃圾邮件检测和主题分类成为可能。
 - 
图像识别:模式匹配可以通过识别与特定类别相对应的独特视觉模式或特征来帮助标记图像。这项技术在对象识别、人脸检测和图像分割等任务中非常有价值。
 - 
时间序列分析:在处理时间依赖性数据时,模式匹配可以用于标记事件序列或随时间发生的模式。这在金融分析、异常检测和预测股市趋势方面特别有用。
 - 
欺诈检测:模式匹配可以通过将可疑模式或异常与已知的欺诈模式进行匹配,在识别欺诈活动中发挥关键作用。这项技术有助于信用卡欺诈检测、网络入侵检测和网络安全。
 
模式匹配作为机器学习中的标签技术提供了以下优势:
- 
自动化和效率:通过自动化标签过程,模式匹配减少了对手动标签的依赖,节省了时间和精力。它允许以更高的效率进行大规模数据集的标签。
 - 
灵活性和适应性:模式可以很容易地修改或扩展以适应新的数据或不断变化的需求。这提供了适应不断变化的标签标准的灵活性,并确保了可扩展性。
 - 
可解释性:模式匹配提供了一种透明且可解释的标签方法,因为规则和模式可以被检查和理解。这有助于标签过程的透明性和可解释性。
 - 
与其他技术的结合:模式匹配可以与其他标签技术结合使用,例如弱监督或迁移学习,以提高机器学习模型的总体标签准确性和鲁棒性。
 
虽然模式匹配是一种有价值的标签技术,但它也带来了一些挑战和考虑因素:
- 
噪声和歧义:不完美匹配预定义模式的数据实例可能在标签过程中引入噪声或歧义。处理此类情况需要仔细设计和考虑模式定义。
 - 
可扩展性:随着数据集的增大,模式匹配的可扩展性变得至关重要。必须采用高效算法和技术来处理不断增加的计算需求。
 - 
过拟合:如果模式过于特定并且无法很好地推广到未见过的数据实例,则可能发生过拟合。可以使用正则化技术和交叉验证来减轻这种风险。
 
在本章的这一节中,我们将探讨如何使用 Python 创建模式匹配标签函数,并将它们应用于credit-g数据集。credit-g数据集,也称为德国信用数据集,是一组用于金融和机器学习领域风险分析的数据点。它用于根据一组属性将人们分类为良好或不良信用风险。
数据集包含 20 个变量,包括数值和分类数据。这些变量提供了有关每个个人的信息,例如他们的支票账户状态、信用历史、贷款目的、信用额度、储蓄账户/债券、就业、可支配收入的百分比分期付款率、个人状况和性别,以及其他属性。
数据集中的每一项代表一个申请贷款的个人。目标变量表示该个人是否被分类为“良好”或“不良”信用风险。这使得数据集特别适用于监督机器学习任务,尤其是二元分类问题。
credit-g 数据集在学术界和工业界广泛用于开发和测试用于信用风险评估的机器学习模型。它可在多个平台上获得,如 DataHub、Kaggle、OpenML 和 UCI 机器学习仓库。
注意
请注意,变量的具体细节可能因数据集的来源而略有不同。
我们可以从将 credit-g 数据集加载到 Python 开始。该数据集包含有关贷款申请人的信息,包括他们的人口统计信息、财务信息和贷款批准状态。我们可以使用 pandas 库来加载数据集并探索其结构:
from sklearn.datasets import fetch_openml
import pandas as pd
# Fetch the credit-g dataset
credit_g = fetch_openml(name='credit-g')
# Convert to DataFrame
df = pd.DataFrame(credit_g.data, columns=credit_g.feature_names)
target = pd.Series(credit_g.target)
# If you want to add the target variable into your DataFrame
df['target'] = target
# Show top rows of the credit-g dataset
df.head().T
这里是数据集的前五行:

图 6.1 – 信用-g 数据集的特征(第一列)和前五行
现在我们已经加载了数据集,我们可以创建模式匹配的标签函数。在这个例子中,我们将创建两个标签函数,根据申请人的收入和信用历史来分配标签。income_labeling_function 将标签 1 分配给收入超过 5,000 的贷款申请人,将标签 0 分配给所有其他人。credit_history_labeling_function 将标签 1 分配给信用历史为 1 的贷款申请人,将标签 0 分配给所有其他人。
给定 credit-g 数据集中的特征,我们可以根据 credit_amount 和 age 创建两个标签函数。credit_amount_labeling_function 将标签 1 分配给信用额度超过 5,000 的贷款申请人,将标签 0 分配给所有其他人。age_labeling_function 将标签 1 分配给年龄超过 30 的贷款申请人,将标签 0 分配给所有其他人:
def credit_amount_labeling_function(df):
    if df["credit_amount"] > 5000:
        return 1
    else:
        return 0
def age_labeling_function(df):
    if df["age"] > 30:
        return 1
    else:
        return 0
在创建标签函数之后,我们可以将它们应用到 credit-g 数据集上。我们可以使用 pandas 中的 apply 函数将标签函数应用到数据集的每一行。apply 函数将标签函数应用到数据集的每一行,并将标签分配到数据集的新列中:
df["credit_amount_label"] = df.apply(credit_amount_labeling_function, axis=1)
df["age_label"] = df.apply(age_labeling_function, axis=1)
df.head().T
这里是使用这些函数的输出 DataFrame。DataFrame 现在有两个额外的列,包含新创建的标签:

图 6.2 – 更新后的 credit-g 数据集,新增两个特征:credit_amount_label 和 age_label
在探讨了模式匹配函数之后,我们现在将关注数据库查找技术的简单性和有效性。在下一节中,我们将利用结构化数据库来提高标记准确性,使我们的方法更加稳健。
数据库查找
数据库查找(DB 查找)标签技术通过利用数据库中存储的信息,为数据实例分配标签提供了一种强大的手段。通过查询相关数据库并检索标记信息,这种方法可以实现自动化和准确的标记。该技术涉及根据与数据实例相关的特定属性或键值对从数据库中搜索和检索标签。它基于数据库包含可用于数据标记目的的有价值标记信息的假设。通过针对数据库执行查询,可以检索相关标签并将其分配给相应的数据实例。
数据库查找技术在机器学习的各个领域和场景中都有应用。以下是一些常见应用:
- 
实体识别:在自然语言处理任务中,如命名实体识别或实体分类,可以使用数据库查找根据存储在数据库中的属性检索实体的标签。这有助于在文本数据中准确识别和分类实体。
 - 
产品分类:电子商务平台通常维护包含产品信息(包括类别和属性)的数据库。可以使用数据库查找根据其特征检索产品标签,从而实现产品的自动化分类和组织。
 - 
地理空间分析:包含地理信息的数据库,如地图或地理标记数据,可以通过数据库查找来查询,根据空间属性分配标签。这项技术有助于实现基于位置推荐、地理空间聚类和边界识别等任务。
 - 
医疗诊断:医疗数据库存储有关疾病、症状和患者记录的大量信息。可以使用数据库查找来检索与患者症状相关的相关标签,从而帮助自动化医疗诊断和决策支持系统。
 
现在,让我们谈谈布尔标志标记。这是一种简单而强大的方法,通过使用清晰和逻辑的条件来帮助我们改进和自动化标记。
布尔标志
(true/false 或 1/0) 与特定的特征或属性相关联,有助于识别所需的标签。通过检查这些标志的存在与否,数据实例可以自动标记。
布尔标志标记技术在机器学习的各个领域都有应用。以下是一些常见应用:
- 
数据过滤:布尔标志可用于根据特定标准过滤和标记数据实例。例如,在情感分析中,可以将积极情感标志分配给包含积极语言或关键词的文本实例,而将消极情感标志分配给包含消极语言的实例。
 - 
事件检测:布尔标志可以帮助标记实例以检测特定事件或条件。例如,在网络安全领域,可以设置一个标志来指示具有可疑网络活动的实例,从而识别潜在的安全威胁。
 - 
异常检测:布尔标志可用于将实例标记为正常或异常。通过定义捕获典型模式或行为的标志,偏离这些模式的实例可以标记为异常,从而促进异常检测任务。
 - 
质量控制:布尔标志可以帮助标记实例进行质量控制。例如,在制造业中,可以根据预定义的质量标准设置标志来标记实例为不合格或合格。
 
布尔标志标记技术在机器学习应用中提供了几个优势:
- 
简洁性与效率:布尔标志提供了一种简单且高效的标记机制。标记过程涉及检查标志的存在或不存在,这可以通过简单的条件语句或逻辑运算来实现。
 - 
灵活性与定制:布尔标志允许定制和适应不同的标记场景。标志可以根据特定标准或要求定义,提供根据所需特征分配标签的灵活性。
 - 
可解释性:布尔标志标记技术提供了可解释性,因为标志的存在或不存在直接对应于分配的标签。这种透明度有助于更好地理解和验证标记过程。
 - 
可扩展性:布尔标志可以轻松扩展以处理大量数据集。由于标记决策基于二进制指示器,计算开销保持较低,使其适合处理大量数据。
 
虽然布尔标志标记技术提供了简洁性和效率,但应考虑以下某些挑战和注意事项:
- 
特征工程:设计有效的布尔标志需要仔细的特征工程。标志应该是信息丰富且与所需标签相关的,这需要深入理解问题域和数据特征。
 - 
数据不平衡:在数据不平衡的情况下,即一个标签支配其他标签,布尔标志技术可能会面临挑战。可能需要适当的处理技术,如过采样或欠采样,来解决不平衡问题。
 - 
泛化:布尔标志可能无法捕捉到潜在数据分布的全部复杂性,可能导致过拟合或泛化能力有限。考虑互补技术,如特征提取或更高级的机器学习算法,以增强性能和泛化能力是很重要的。
 - 
标志解释:虽然布尔标志提供了可解释性,但仔细解释标志的含义与分配的标签之间的关系是至关重要的。在某些情况下,标志可能捕捉到相关性而不是因果关系,需要进一步调查以获得更准确的解释。
 
你可能已经注意到了布尔标志和独热编码(在第第五章**,数据清洗技术中介绍)之间的相似之处。因此,了解何时使用这些技术是很重要的。
在选择布尔标志和独热编码之间,具体的用例是一个关键因素。如果你正在处理一个可以自然地分为两个类别或状态(例如是/否、真/假)的分类变量,使用布尔标志可能是最佳选择。它更简单,更节省内存,并且可以使模型更容易解释。
例如,如果你正在预测一封电子邮件是否为垃圾邮件,一个布尔标志如contains_link(如果电子邮件包含链接则为1,否则为0)可能是一个非常有效的特征。这种简单性可以导致更可解释的模型,因为每个特征直接对应一个条件或状态。
另一方面,独热编码更适合于具有多个类别且不存在自然二分法的分类变量。例如,如果你正在处理一个如color这样的特征,其值有red、blue、green等,独热编码将是一个更好的选择。这是因为分配给每个类别的数字不应该暗示类别之间存在数学关系,除非确实存在。例如,将红色编码为1,蓝色编码为2并不意味着蓝色是红色的两倍。
为了避免暗示这种未预期的关系,为每种可能的颜色创建一个单独的特征是首选的。这种方法可以捕捉更多关于颜色特征的信息,并且不对不同颜色施加任意顺序或重要性。
此外,所使用的机器学习模型的类型也会影响选择。一些模型,如决策树和随机森林,可以很好地处理分类变量,因此可能不需要一热编码(这会增加数据集的维度)。然而,其他模型,如线性回归、逻辑回归和支持向量机,需要数值输入,因此需要对分类变量进行某种形式的编码。
最后,值得注意的是,这些并不是处理分类数据的唯一方法。还有其他技术,如有序编码、目标编码和二进制计数,每种技术都有其自身的优缺点。关键是理解你数据的本质和特定用例的要求,以选择最合适的方法。
让我们探讨如何使用 Python 中的布尔标志在credit-g数据集中。想象一下,我们想要创建一个函数,该函数根据基本规则或启发式方法应用布尔标志来标记数据点。例如,我们可以编写一个函数,评估信用申请人的信用额度是否高于特定阈值,然后根据这种评估为数据点分配布尔标志。
以下函数将检查信用额度是否低于或高于中值信用额度:
def lf_credit_amount_above_median(df):
    credit_amount_median = df['credit_amount'].median()
    return df['credit_amount'] >= credit_amount_median
现在我们已经定义了我们的函数,我们可以将其应用于我们的df DataFrame,用布尔标志标记数据点:
df['LF_CreditAmountAboveMedian'] = lf_credit_amount_above_median(df)
df.head().T
图 6.3 是应用这些函数后的输出 DataFrame。注意,我们现在创建了一个新列,提供了关于申请人信用额度的额外信息,这可以用作机器学习模型中的特征。

图 6.3 – 添加了新布尔标志 LF_CreditAmountAboveMedian 的 credit-g 数据集
在下一节中,让我们探讨弱监督——这是一种复杂的标记技术,它巧妙地整合了来自各种来源的信息,在现实世界数据的复杂性中导航,以增强精确性和适应性。
弱监督
弱监督是机器学习中的一种标记技术,它利用不完美或噪声的监督源为数据实例分配标签。与依赖于手动标注数据的传统标记方法不同,弱监督允许更可扩展和自动化的标记方法。它指的是使用启发式方法、规则或概率方法为数据实例生成近似标签。
弱监督不是依赖于单一的权威监督源,而是利用多个可能引入噪声或不一致性的来源。目标是生成“弱”指示真实潜在标签的标签,从而在获取完全标注数据具有挑战性或成本高昂的情况下进行模型训练。
例如,考虑一个任务,我们想要构建一个机器学习模型来识别一封电子邮件是否为垃圾邮件。理想情况下,我们会有一大批被准确标记为“垃圾邮件”或“非垃圾邮件”的电子邮件数据集。然而,获取这样的数据集可能具有挑战性,耗时且成本高昂。
使用弱监督,我们可以使用替代的、不那么完美的方法来标记我们的数据。例如,我们可以根据垃圾邮件中的常见模式创建一些规则或启发式方法。以下是一些此类规则的例子:
- 
如果电子邮件包含诸如“彩票”、“赢”或“奖品”等词语,它可能是垃圾邮件
 - 
如果电子邮件来自未知发件人且包含许多链接,它可能是垃圾邮件
 - 
如果电子邮件包含诸如“紧急行动要求”之类的短语,它可能是垃圾邮件
 
使用这些规则,我们可以自动标记我们的电子邮件数据集。这些标签不会完美——会有误报(非垃圾邮件被错误地标记为垃圾邮件)和漏报(垃圾邮件被错误地标记为非垃圾邮件)。但它们为我们训练机器学习模型提供了一个起点。
模型可以从这些“弱”标签中学习,并且,在有足够大和多样化的数据集的情况下,应该仍然能够很好地泛化到新的、未见过的电子邮件。这使得弱监督成为一种可扩展且高效的标注方法,特别适用于难以获得完美标签的情况。
弱监督可以来自各种来源,包括以下:
- 
基于规则的系统:领域专家或基于启发式的方法可以定义基于特定模式、特征或条件的规则或指南来标记数据。这些规则可能来自知识库、现有模型或专家意见。
 - 
众包:通过众包平台利用人类标注者的力量,可以通过汇总多个个体的标注来获得弱监督。这种方法引入了噪声,但可能具有成本效益和可扩展性。
 - 
远程监督:远程监督涉及使用可能与目标任务不完全一致但可以作为代理的现有标记数据。一个例子是使用具有辅助标签的现有数据来训练一个相关但不同的任务的模型。
 - 
数据增强:通过数据增强技术,如数据合成、转换或扰动,可以获得弱监督。通过根据现有标记数据生成新的标记实例,弱监督可以扩展。
 
弱监督在机器学习应用中提供了几个优势:
- 
可扩展性:弱监督通过利用自动化或半自动化技术,允许进行大规模数据标记。它减少了手动标注所需的劳动强度,使得可以利用更大的数据集。
 - 
成本效益:通过利用弱监督来源,与全监督方法相比,获取标记数据的成本可以显著降低。这在手动标记昂贵或不切实际的情况下尤其有益。
 - 
灵活性和适应性:弱监督技术可以轻松地适应和修改,以纳入新的监督来源或更新现有规则。这种灵活性允许迭代改进和细化标记过程。
 - 
处理噪声标签:弱监督技术可以通过聚合多个弱信号来处理噪声或不一致的标签。这种对噪声的鲁棒性减少了单个标记错误对整体训练过程的影响。
 
然而,也有一些挑战和注意事项需要注意:
- 
噪声与标签质量:由于监督来源的不完美性质,弱监督标签可能包含噪声或错误。仔细评估和验证是必要的,以确保标签质量并最小化在模型训练过程中噪声标签的传播。
 - 
精确度与召回率的权衡:弱监督技术通常优先考虑可扩展性和覆盖范围,而不是精确度。在获取可靠的弱标签数据时,平衡召回率(覆盖范围)和精确度(准确性)之间的权衡至关重要。
 - 
标记置信度与模型训练:处理与弱监督标签相关的不确定性至关重要。可以采用标签校准、数据增强或主动学习等技术来减轻在模型训练期间标签不确定性的影响。
 - 
泛化与模型性能:由于标签中的固有噪声,弱监督模型可能难以泛化到未见或具有挑战性的实例。可以采用正则化、集成方法或迁移学习等策略来提高模型性能。
 
在本节中,我们将探讨如何使用 Python 中的标记函数在第五章**数据清洗技术中介绍的贷款预测数据集上训练机器学习模型。首先,我们需要通过导入必要的库和加载数据集来准备数据,并且需要通过处理缺失值和编码分类变量来预处理数据:
import pandas as pd
import numpy as np
df = pd.read_csv('train_loan_prediction.csv')
df['LoanAmount'].fillna(df['LoanAmount'].mean(), inplace=True)
df['Credit_History'].fillna(df['Credit_History'].mode()[0], inplace=True)
df['Self_Employed'].fillna('No',inplace=True)
df['Gender'].fillna(df['Gender'].mode()[0], inplace=True)
df['Married'].fillna(df['Married'].mode()[0], inplace=True)
df['Dependents'].fillna(df['Dependents'].mode()[0], inplace=True)
df['Loan_Amount_Term'].fillna(df['Loan_Amount_Term'].mode()[0], inplace=True)
df['Credit_History'].fillna(df['Credit_History'].mode()[0], inplace=True)
我们将使用 scikit-learn 的preprocessing类中的LabelEncoder函数来编码分类列:
from sklearn.preprocessing import LabelEncoder
cat_features = ['Gender', 'Married','Dependents', 'Education', 'Self_Employed', 'Property_Area']
for feature in cat_features:
    encoder = LabelEncoder()
    df[feature] = encoder.fit_transform(df[feature])
现在,我们可以定义我们的标签函数。在这个例子中,我们将基于一些简单的启发式方法定义三个标签函数。这些标签函数接受数据集的一行作为输入,并返回一个标签。如果该行可能属于正类,则标签为 1;如果可能属于负类,则标签为 0;如果不确定,则标签为 -1。这类函数在弱监督方法中常用,其中你有一大量未标记数据,并希望为它们生成噪声标签:
from snorkel.labeling import labeling_function
@labeling_function()
def lf1(df):
    if df['Education'] == 0:
        return 0
    elif df['Self_Employed'] == 0:
        return 1
    else:
        return -1
@labeling_function()
def lf2(df):
    if df['Credit_History'] == 1:
        if df['LoanAmount'] <= 120:
            return 1
        else:
            return 0
    else:
        return -1
@labeling_function()
def lf3(df):
    if df['Married'] == 1:
        if df['Dependents'] == 0:
            return 1
        elif df['Dependents'] == 1:
            return 0
        else:
            return -1
    else:
        return -1
我们可以使用 Snorkel 库将标签函数应用于数据集。在这里,我们创建了一个包含三个标签函数的列表,并使用 PandasLFApplier 将它们应用于数据集。输出是一个 L_train 矩阵,其中每一行对应一个数据点,每一列对应一个标签函数:
LFs = [lf1, lf2, lf3]
from snorkel.labeling import PandasLFApplier
applier = PandasLFApplier(lfs=LFs)
L_train = applier.apply(df)
你将看到以下输出:

图 6.4 – 显示训练进度的进度条
为了提高标签函数的输出,我们需要将它们结合起来,为每个数据点获得更准确的标签。我们可以使用 Snorkel 库中的 LabelModel 类来完成此操作。LabelModel 类是一个概率模型,用于结合多个标签函数的输出,为每个数据点生成更准确和可靠的标签。它在解决可能由单个标签函数引起的噪声和不准确性方面发挥着关键作用。我们创建一个 LabelModel 对象作为 label_model,并将其拟合到标签函数的输出。基数参数指定了类的数量,在本例中为 2。我们还指定了训练的轮数和一个随机种子以确保可重复性:
from snorkel.labeling.model import LabelModel
from snorkel.labeling import PandasLFApplier, LFAnalysis
from sklearn.metrics import accuracy_score
label_model = LabelModel(cardinality=2, verbose=True)
label_model.fit(L_train=L_train, n_epochs=500, log_freq=100, seed=123)
执行前面的代码片段,使用 Snorkel 的标签模型后,将显示标签的增量应用进度条:

图 6.5 – 显示标签过程增量进度的 Snorkel 进度条
现在,我们可以使用 LabelModel 类为训练数据生成标签并评估其性能:
probs_train = label_model.predict_proba(L_train)
mapping = {'Y': 1, 'N': 0}
Y_train = df['Loan_Status'].map(mapping).values
score_train = label_model.score(L_train, Y_train)
df['label'] = probs_train.argmax(axis=1)
df['label'] = df['label'].map({1: 'Y', 0: 'N'})
print(f"Accuracy: {accuracy_score(df['Loan_Status'].values, df['label'].values)}")
现在,让我们学习如何使用半弱监督进行标签化。这是一种智能技术,它结合了弱监督和一些手动标签,使我们的标签更加准确。
半弱监督
半弱监督是一种在机器学习中使用的技巧,通过结合一小部分标记数据和大量弱标记数据来提高模型的准确性。在这种方法中,标记数据用于指导学习过程,而弱标记数据提供额外的信息以提高模型的准确性。
半弱监督在标记数据有限或难以获取时特别有用,并且可以应用于广泛的机器学习任务,如文本分类、图像识别和目标检测。
在贷款预测数据集中,我们有一组代表贷款申请的数据点,每个数据点都包含一系列特征,如收入、信用历史和贷款金额,以及一个表示贷款是否批准的标签。然而,这些标记数据可能是不完整或不准确的,这可能导致模型性能不佳。
为了解决这个问题,我们可以使用半弱监督为贷款预测数据集生成额外的标签。一种方法是通过启发式或规则自动生成标签的弱监督技术。例如,我们可以使用正则表达式来识别贷款申请文本数据中的模式,这些模式表明高风险贷款。我们还可以使用外部数据源,如信用报告或社交媒体数据,来生成额外的弱标记数据。
一旦我们有一组弱标记数据,我们就可以使用它来训练一个模型,同时结合少量标记数据。标记数据用于指导学习过程,而弱标记数据提供额外的信息以提高模型的准确性。通过使用半弱监督,我们可以有效地利用所有可用数据来提高模型性能。
下面是一个使用 Snorkel 和 Python 实现贷款预测数据集半弱监督的示例。我们首先从这些库中导入必要的库和函数。然后使用 pandas 加载数据集:
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from snorkel.labeling import labeling_function
from snorkel.labeling import PandasLFApplier
from snorkel.labeling import LFAnalysis
from snorkel.labeling.model import LabelModel
df = pd.read_csv('train_loan_prediction.csv')
让我们定义一个函数来预处理数据集。我们将使用本章前面讨论过的类似预处理方法:
def preprocess_data(df):
    df['Gender'] = df['Gender'].fillna('Unknown')
    df['Married'] = df['Married'].fillna('Unknown')
    df['Dependents'] = df['Dependents'].fillna('0')
    df['Self_Employed'] = df['Self_Employed'].fillna('Unknown')
    df['LoanAmount'] = df['LoanAmount'].fillna(df['LoanAmount'].mean())
    df['Loan_Amount_Term'] = df['Loan_Amount_Term'].fillna(df['Loan_Amount_Term'].mean())
    df['Credit_History'] = df['Credit_History'].fillna(-1)
    df['LoanAmount_bin'] = pd.cut(df['LoanAmount'], bins=[0, 100, 200, 700], labels=['Low', 'Average', 'High'])
    df['TotalIncome'] = df['ApplicantIncome'] + df['CoapplicantIncome']
    df['TotalIncome_bin'] = pd.cut(df['TotalIncome'], bins=[0, 2500, 4000, 6000, 81000], labels=['Low', 'Average', 'High', 'Very high'])
    df = df.drop(['Loan_ID', 'ApplicantIncome', 'CoapplicantIncome', 'LoanAmount', 'TotalIncome'], axis=1)
    return df
现在我们为ApplicantIncome和LoanAmount创建了三个标记函数。lf1(x)函数接收一个数据实例x作为输入,并根据ApplicantIncome特征值执行标记操作。如果ApplicantIncome小于 5,000,则函数返回标签0。否则,如果ApplicantIncome大于或等于 5,000,则函数返回标签1。本质上,这个函数将标签0分配给申请收入较低的数据实例,将标签1分配给申请收入较高的数据实例。
lf2(x)函数同样接收一个数据实例x作为输入,并根据LoanAmount特征值分配标签。如果LoanAmount大于 200,则函数返回标签0。相反,如果LoanAmount小于或等于 200,则函数返回标签1。这个函数将贷款金额较大的数据实例分类为标签0,将贷款金额较小的数据实例分类为标签1。
利用lf3(x)函数,我们计算贷款金额与申请人收入的比率。这个比率是确定贷款可行性的关键指标。根据这个计算出的比率,我们将数据点分类到不同的标签。如果贷款与收入比率低于或等于 0.3,我们分配标签1,表示批准贷款申请。在比率超过 0.3 但不超过 0.5 的情况下,我们指定数据点的标签为0,表示对贷款批准的不确定性。相反,如果比率超过 0.5,我们分配标签-1,表示拒绝贷款申请。这种方法使我们能够将可负担性方面纳入我们的标签过程,增强我们贷款批准预测的弱监督方法的粒度:
@labeling_function()
def lf1(x):
    return 0 if x['ApplicantIncome'] < 5000 else 1
@labeling_function()
def lf2(x):
    return 0 if x['LoanAmount'] > 200 else 1
@labeling_function()
def lf3(x):
    # Calculate the ratio of loan amount to applicant's income
    loan_to_income_ratio = x['LoanAmount'] / x['ApplicantIncome']
    # Return label based on the loan-to-income ratio
    if loan_to_income_ratio <= 0.3:
        return 1  # Approve loan
    elif loan_to_income_ratio > 0.3 and loan_to_income_ratio <= 0.5:
        return 0  # Label as uncertain
    else:
        return -1  # Deny loan
我们然后将预处理技术应用于输入数据(df)。使用preprocess_data函数执行必要的预处理步骤。处理后的数据存储在变量X中。此外,目标变量Loan_Status从分类值(N和Y)转换为数值(0和1),并存储在变量y中。这一步骤确保数据已准备好进行训练和评估:
X = preprocess_data(df.drop('Loan_Status', axis=1))
y = df['Loan_Status'].replace({'N': 0, 'Y': 1})
下一步是将预处理数据分为训练集和测试集。使用 scikit-learn 库中的train_test_split函数来完成此操作。数据被分为特征X_train和X_test以及相应的标签y_train和y_test。这种分离允许在训练集上训练模型并在测试集上评估其性能:
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
现在我们使用PandasLFApplier类将两个标签函数lf1和lf2应用于训练集(X_train)。生成的弱标签数据存储在L_train_weak中。LF 分析每个实例的特征并根据预定义的规则或条件分配标签:
lfs = [lf1, lf2, lf3]
applier = PandasLFApplier(lfs)
L_train_weak = applier.apply(X_train)
使用LabelModel类实例化标签模型。它配置为2的基数(表示二元分类)并设置为以详细模式运行以更新进度。然后使用fit方法在训练数据(L_train_weak)上训练标签模型:
label_model = LabelModel(cardinality=2, verbose=True)
label_model.fit(L_train_weak)
一旦标签模型训练完成,它将在测试集(X_test)上评估其性能。再次使用 applier 对象将标签函数应用于测试集,结果生成L_test,其中包含弱标签实例。然后使用标签模型的 score 方法计算标签预测的准确度与真实标签(y_test)相比:
L_test = applier.apply(X_test)
accuracy = label_model.score(L_test, y_test)["accuracy"]
print(f'Test accuracy: {accuracy:.3f}')
在接下来的部分,我们将探讨用于标签的切片函数——这是一种高级技术,允许我们精细分割我们的数据。这些函数提供了一种定制的方法,使我们能够将特定的标签策略应用于数据集的不同子集:
切片函数
切片函数是作用于数据实例并基于特定条件产生二元标签的函数。与为整个数据集提供标签的传统标记函数不同,切片函数旨在关注数据的具体子集。这些子集或切片可以根据数据的各种特征、模式或特征来定义。切片函数提供了一种细粒度的标记方法,使数据实例的标记更加有针对性和精确。
切片函数在弱监督方法中起着至关重要的作用,其中利用多个标记源来分配近似标签。切片函数通过捕获可能难以使用其他方法准确标记的数据的特定模式或子集,补充了其他标记技术,如基于规则的系统或众包。通过应用切片函数到数据中,从业者可以利用领域知识或特定的数据特征来改进标记过程。
为了全面理解切片函数的概念,让我们以一个包含来自电子商务网站一系列产品评论的数据集为例。我们的目标是将这些评论标记为正面或负面。
为了简单起见,让我们考虑两个切片函数:
- 
切片函数 1(SF1):此函数针对包含单词“退款”的评论。如果评论包含“退款”一词,则将其标记为负面,否则不标记。这个切片函数背后的直觉是,要求退款的客户可能对其购买不满意,因此具有负面情绪。
 - 
切片函数 2(SF2):此函数针对购买电子产品的客户评论。如果评论包含“出色”、“优秀”或“喜爱”等词语,则将其标记为正面;如果包含“损坏”、“有缺陷”或“无用”等词语,则将其标记为负面。如果评论不符合任何这些条件,则将其保留为未标记状态。
 
你会注意到这些切片函数作用于数据的具体子集,使我们能够将领域知识纳入标记过程。因此,设计有效的切片函数需要结合领域知识、特征工程和实验。以下是设计和实现切片函数的一些关键考虑因素:
- 
识别相关切片:确定与标记任务相关的特定数据子集或切片。这涉及到理解问题领域、分析数据以及识别独特的模式或特征。
 - 
定义切片条件:指定捕获所需数据子集的条件或规则。这些条件可以基于特征阈值、模式匹配、统计属性或任何其他相关标准。
 - 
评估和迭代:通过将分配的标签与地面真实标签或现有的标签源进行比较来评估切片函数的性能。对切片函数的设计进行迭代,细化条件和规则,以提高分配标签的质量。
 
切片函数在标签过程中提供了几个好处:
- 
细粒度标签化:切片函数允许对数据的特定子集进行有针对性的标签化,提供更详细和粒度化的标签,以捕捉独特的模式或特征。
 - 
领域知识整合:切片函数允许将领域专业知识和特定领域知识整合到标签过程中。这允许做出更明智和情境感知的标签决策。
 - 
与其他技术的互补性:切片函数通过捕捉可能难以使用传统方法进行标签化的数据切片来补充其他标签技术。它们提供了额外的弱监督来源,从而增强了整体标签过程。
 - 
可扩展性和效率:切片函数可以自动化并程序化应用,允许对大型数据集进行可扩展和高效的标签化。这减少了对手动标注的依赖,并允许在大规模上对数据进行标签化。
 
让我们了解如何使用贷款预测数据集在 Python 中实现切片函数。我们首先导入所需的库并将数据集加载到 pandas DataFrame 中。我们将使用之前章节中讨论的相同预处理步骤:
from snorkel.labeling import labeling_function
from snorkel.labeling import PandasLFApplier
from snorkel.labeling.model import LabelModel
from snorkel.labeling import LFAnalysis
df = pd.read_csv('train_loan_prediction.csv')
X = preprocess_data(df.drop('Loan_Status', axis=1))
y = df['Loan_Status'].replace({'N': 0, 'Y': 1})
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
要创建切片函数,我们利用 Snorkel 提供的@labeling_function装饰器。这些函数封装了基于特定条件或规则的标签逻辑。例如,我们可以根据ApplicantIncome、LoanAmount或Self_Employed特征定义切片函数:
@labeling_function()
def slice_high_income(x):
    return 1 if x['ApplicantIncome'] > 8000 else 0
@labeling_function()
def slice_low_income_high_loan(x):
    return 1 if x['ApplicantIncome'] < 4000 and x['LoanAmount'] > 150 else 0
@labeling_function()
def slice_self_employed(x):
    return 1 if x['Self_Employed'] == 'Yes' else 0
要将切片函数应用于训练数据,我们使用 Snorkel 提供的PandasLFApplier类。这个类接受切片函数作为输入,并将它们应用于训练数据集,生成弱标签。生成的弱标签将用于稍后训练标签模型:
lfs = [slice_high_income, slice_low_income_high_loan, slice_self_employed]
applier = PandasLFApplier(lfs)
L_train = applier.apply(df=X_train)
一旦我们从切片函数中获得弱标签,我们就可以使用 Snorkel 的LabelModel类来训练标签模型。标签模型学习弱标签与真实标签之间的相关性,并估计每个数据实例的后验概率。在这个步骤中,我们创建一个LabelModel对象,指定标签的基数(例如,二元分类),并将其拟合到弱标签的训练数据:
label_model = LabelModel(cardinality=2, verbose=True)
label_model.fit(L_train, n_epochs=500, seed=42)
在训练标签模型后,我们希望评估其在测试数据上的性能。我们使用PandasLFApplier将切片函数应用于测试数据集,获取弱标签。然后,我们计算标签模型预测的准确率,与测试集的真实标签进行比较:
L_test = applier.apply(df=X_test)
accuracy = label_model.score(L=L_test, Y=y_test)
print(f'Test accuracy: {accuracy["accuracy"]:.3f}')
Snorkel 提供了 LFAnalysis 模块,该模块允许我们分析标记函数的性能和特征。我们可以计算每个标记函数的覆盖率、冲突和准确度等指标,以深入了解其有效性和潜在问题:
LFAnalysis(L=L_train, lfs=lfs).lf_summary()
这将生成以下汇总表:

图 6.6 – 显示每个标记函数(LF)统计信息的汇总表
在下一节中,我们将探讨用于标记的主动学习——一种涉及选择正确数据进行标记的巧妙策略,通过每次迭代使我们的模型更智能。
主动学习
在本节中,我们将探讨主动学习的概念及其在数据标记中的应用。主动学习是一种强大的技术,它允许我们通过积极选择最具信息量的样本进行标注来更有效地标记数据。通过战略性地选择哪些样本进行标注,我们可以在数据集较小的情况下实现更高的准确度,其他条件保持不变。在接下来的几页中,我们将讨论各种主动学习策略,并使用 Python 代码示例进行实现。
主动学习是一种半监督学习方法,它涉及根据数据点的信息量迭代选择数据子集进行手动标注。关键思想是积极查询最不确定或最具信息量的实例的标签,以改进学习过程。通过选择和标注样本的这种迭代过程,可以显著减少实现所需性能水平所需标记数据的数量。
让我们从主动学习的一个简单例子开始,帮助你了解基本概念,然后再详细讨论具体的主动学习策略。
假设我们正在构建一个机器学习模型,用于将电子邮件分类为垃圾邮件和非垃圾邮件。我们有一个大量未标记的电子邮件数据集,但手动标记所有这些邮件将非常耗时。
这就是主动学习发挥作用的地方:
- 
初始训练:我们首先随机选择一小部分电子邮件,并将它们手动标记为垃圾邮件或非垃圾邮件。然后,我们在这个小型标记数据集上训练我们的模型。
 - 
不确定性采样:训练完成后,我们使用模型对剩余的未标记电子邮件进行预测。然而,我们不会标记所有电子邮件,而是选择模型对其预测最不确定的电子邮件。例如,如果我们的模型输出一个接近 0.5 的概率(即它不确定电子邮件是否为垃圾邮件),这些电子邮件被认为是“信息丰富”或“不确定”的。
 - 
标签查询:然后,我们手动标记这些不确定的电子邮件,并将它们添加到我们的训练集中。
 - 
迭代学习:步骤 2和步骤 3在多次迭代中重复——用新标记的数据重新训练模型,使用它来预测剩余未标记数据的标签,并选择下一个最不确定的实例进行标记。
 
这样,主动学习使我们能够有策略地选择最有信息量的示例进行标记,从而有可能在更少的标记实例的情况下提高模型的表现。
根据选择信息样本的不同标准,可以采用几种主动学习策略。让我们讨论一些常用的策略及其 Python 实现。
不确定性采样
不确定性采样基于这样的假设:模型不确定的实例更有信息量,对标记有益。想法是选择接近决策边界或具有冲突预测的实例。通过积极获取这些具有挑战性的实例的标签,模型可以细化其对数据的理解并提高其性能。
在主动学习中,不确定性采样有几种常见的方法:
- 
最低信心度:此方法选择模型对其预测最没有信心的实例。它关注预测类别概率最接近 0.5 的实例,这表明存在不确定性。例如,在我们的电子邮件示例中,如果模型预测某个电子邮件是垃圾邮件的概率为 0.52,不是垃圾邮件的概率为 0.48,这表明模型对其预测不确定。这封电子邮件将是最低信心度方法下标记的理想候选。
 - 
边缘采样:边缘采样旨在找到模型的前两个预测类别概率接近的实例。它选择最高和第二高概率之间差异最小的实例,因为这些实例可能接近决策边界。假设我们有一个对动物图像进行分类的模型。如果它预测一个图像的概率为猫 0.4,狗 0.38,鸟 0.22,最高和第二高概率之间的小差异(0.02)表明存在不确定性。这个图像将在边缘采样中被选中进行标记。
 - 
熵:基于熵的采样考虑预测类别概率的熵。它选择具有高熵的实例,这表明模型预测的不确定性很高。使用相同的动物分类模型,如果一个图像每个类别(猫、狗、鸟)的概率都为 0.33,这表明存在高度不确定性。这个图像将通过熵方法被选中进行标记。
 
让我们在 Python 中实现不确定性采样。为此示例,我们回到本章开头介绍的credit-g数据集。让我们看看这个数据集中的特征,以刷新你的记忆:

图 6.7 – credit-g 数据集的特征
在假设数据集已经加载到 df DataFrame 的情况下,我们首先通过标准化数值特征和独热编码分类特征来预处理数据集:
import pandas as pd
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
# Define preprocessor
preprocessor = ColumnTransformer(
    transformers=[
        ('num', StandardScaler(), ['duration', 'credit_amount', 'installment_commitment',
                                   'residence_since', 'age', 'existing_credits', 'num_dependents']),
        ('cat', OneHotEncoder(), ['checking_status', 'credit_history', 'purpose',
                                  'savings_status', 'employment', 'personal_status', 'other_parties',
                                  'property_magnitude', 'other_payment_plans', 'housing', 'job',
                                  'own_telephone', 'foreign_worker'])])
# Define a mapping from current labels to desired labels
mapping = {'good': 1, 'bad': 0}
# Apply the mapping to the target variable
df['target'] = df['target'].map(mapping)
# Fit and transform the features
features = preprocessor.fit_transform(df.drop('target', axis=1))
# Convert the features to a dataframe
features_df = pd.DataFrame(features)
# Add the target back to the dataframe
df_preprocessed = pd.concat([features_df, df['target'].reset_index(drop=True)], axis=1)
我们手头有了新的 df_preprocessed DataFrame,我们可以执行不确定性采样。我们首先导入必要的库和模块,包括 pandas、NumPy 和 scikit-learn,用于数据处理和机器学习操作:
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
我们将 df_preprocessed 数据集分为一个小型标记数据集和剩余的未标记数据。在这个例子中,我们随机选择 10%的小部分作为标记数据,其余的作为未标记数据:
labeled_data, unlabeled_data = train_test_split(df_preprocessed, test_size=0.9, random_state=42)
我们定义了不确定性采样函数——least_confidence、margin_sampling 和 entropy_sampling,如前所述。
下面是每个这些函数的解释:
- 
least_confidence:此函数接受一个概率的二维数组,其中每一行代表一个实例,每一列代表一个类别。对于每个实例,它计算置信度为 1 –max_probability,其中max_probability是所有类别中最大的预测概率。然后按置信度的升序对实例进行排序。其思路是,具有较低置信度(即更高不确定性)的实例更有信息量,应该首先进行标记:def least_confidence(probabilities): confidence = 1 - np.max(probabilities, axis=1) return np.argsort(confidence) - 
margin_sampling:此函数也接受一个概率的二维数组。对于每个实例,它计算边缘作为最高和第二高预测概率之间的差异。然后按边缘的升序对实例进行排序。其思路是,具有较小边缘(即接近前两个类别的概率)的实例更有信息量,应该首先进行标记:def margin_sampling(probabilities): sorted_probs = np.sort(probabilities, axis=1) margin = sorted_probs[:, -1] - sorted_probs[:, -2] return np.argsort(margin) - 
entropy_sampling:此函数计算每个实例预测概率的熵。熵是衡量不确定性或无序的指标,值越高表示不确定性越大。然后按熵的升序对实例进行排序。其思路是,具有更高熵(即类别概率中的更大不确定性)的实例更有信息量,应该首先进行标记:def entropy_sampling(probabilities): entropy = -np.sum(probabilities * np.log2(probabilities), axis=1) return np.argsort(entropy) 
我们进入主动学习循环,在这个循环中,我们迭代地训练模型,使用不确定性采样选择标记实例,为这些实例获取标签,并更新标记数据集。
首先,使用名为 accuracies 的列表来跟踪模型在每个迭代中对标记数据的准确率。
然后在指定的迭代次数上实现主动学习循环。在每个迭代中,使用标记数据训练一个逻辑回归模型,并计算其准确率并存储。然后模型对未标记数据进行预测,并将它最不自信的实例(由 least_confidence 函数确定)添加到标记数据集中。这些实例从未标记数据集中移除:
# Initialize a list to store the accuracy at each iteration
accuracies = []
# Implement the active learning loop.
num_iterations = 5
batch_size = 20
for _ in range(num_iterations):
    model = LogisticRegression()
    model.fit(X_labeled, y_labeled)
    # Calculate and store the accuracy on the labeled data at this iteration
    accuracies.append(accuracy_score(y_labeled, model.predict(X_labeled)))
    probabilities = model.predict_proba(X_unlabeled)
    indices = least_confidence(probabilities)[:batch_size]
    X_newly_labeled = X_unlabeled[indices]
    y_newly_labeled = y_unlabeled[indices]
    X_labeled = np.concatenate([X_labeled, X_newly_labeled])
    y_labeled = np.concatenate([y_labeled, y_newly_labeled])
    X_unlabeled = np.delete(X_unlabeled, indices, axis=0)
    y_unlabeled = np.delete(y_unlabeled, indices)
最终输出是一个包含在主动学习每次迭代中标记的额外实例的更新标记数据集。该过程旨在通过迭代选择和标记未标记数据中最具信息量的实例来提高模型性能。在前面的代码中,主动学习的成功和有效性取决于least_confidence自定义函数和数据集的特征。假设least_confidence函数返回对应于最不自信预测的索引。
注意,此代码使用least_confidence函数进行主动学习。要使用margin_sampling或entropy_sampling而不是least_confidence执行相同的过程,可以将least_confidence(probabilities)[:batch_size]替换为margin_sampling(probabilities)[:batch_size]或entropy_sampling(probabilities)[:batch_size]。
让我们比较三个主动学习函数在credit-g相同样本上的性能。我们使用 matplotlib 来生成三个主动学习函数准确率的视觉表示。要复制输出,将以下代码应用于每个函数的输出:
import matplotlib.pyplot as plt
from matplotlib.ticker import MaxNLocator
ax = plt.figure().gca()
ax.xaxis.set_major_locator(MaxNLocator(integer=True))
plt.plot(range(1, num_iterations + 1), accuracies)
plt.xlabel('Iteration')
plt.ylabel('Accuracy')
plt.title('Model accuracy over iterations (least_confidence)')
plt.show()
最不自信方法在五次迭代后达到了 0.878 的模型准确率,最佳性能出现在第一次迭代后:

图 6.8 – 在信用-g 数据集上预测目标变量时,最不自信主动学习函数在五次迭代中的准确率
边缘采样在经过两次和三次迭代后达到了略高的准确率,为 0.9:

图 6.9 – 在信用-g 数据集上预测目标变量时,边缘采样主动学习函数在五次迭代中的准确率
最后,熵采样和最不自信采样得到了相同的结果。这是为什么?

图 6.10 – 在信用-g 数据集上预测目标变量时,熵采样主动学习函数在五次迭代中的准确率
这两种方法在某些场景下可能会得到相同的结果,尤其是在处理二元分类问题时。原因如下。
最不自信方法考虑预测概率最高的类别。如果模型对某个特定类别非常自信(即,概率接近 1),那么这个实例被选为标记的可能性就较小。
熵采样考虑预测概率的熵或“无序”。对于二元分类问题,当概率都相等时(即模型完全不确定要预测哪个类别),熵达到最大。这可能与低置信度预测相一致。
因此,在二元分类的背景下,这两种方法通常会选择相同的实例进行标签。然而,这并不总是如此,特别是对于多类问题。
另一方面,边缘采样关注最高和第二高预测概率之间的差异。这些概率之间的微小差异可能导致与其他方法相比选择不同的实例。
在下一节中,我们将探讨委员会查询(QBC)用于标签——一种将一组模型汇集起来以帮助决定哪些数据点对标签最重要的方法。
委员会查询(QBC)
QBC 基于这样一个观点:模型委员会不一致或表现出高度不确定性的实例最有信息量,应该优先进行标签。QBC 不是依赖于单个模型的预测,而是利用委员会的多样性和集体决策来做出明智的标签决策。
QBC 过程通常包括以下步骤:
- 
委员会创建:创建一个由多个模型组成的初始委员会,这些模型在可用的标记数据上进行了训练。委员会中的模型在架构、初始化或训练方法方面可以多样化。
 - 
实例选择:将模型委员会应用于未标记的实例并获取预测。选择在委员会成员中引起最多不一致或不确定性的实例进行标签。
 - 
委员会更新:对选定的实例进行标签并添加到标记数据集中。使用扩展的标记数据集重新训练或更新模型委员会。
 - 
重复:通过返回到步骤 2来迭代过程,直到达到期望的性能水平或标签资源耗尽。
 
QBC 在主动学习标签方面提供了几个优势:
- 
模型多样性:QBC 利用一个模型委员会,允许有不同观点并捕捉数据分布的不同方面。这种多样性有助于识别具有挑战性或模糊的实例,从而提高标签决策。
 - 
模型置信度估计:通过观察委员会成员之间的不一致或不确定性,QBC 提供了模型对其预测置信度的估计。导致不一致或不确定性的实例可以被认为是更有信息和价值的标签。
 - 
标签效率:QBC 旨在优先考虑对委员会决策影响最大的实例。这种方法可以通过关注提供最多相关信息以改进模型性能的实例来节省标签工作。
 
让我们使用 Python 中的 credit-g 数据集来实现这种方法。首先,我们定义创建委员会、获取委员会预测和测量委员会成员之间分歧或不确定性的函数。
create_committee(num_models) 函数创建一个逻辑回归模型的委员会。委员会中的模型数量由 num_models 指定。
get_committee_predictions(committee, data) 函数从委员会中的每个模型获取提供数据的预测。它返回一个预测概率数组。
measure_disagreement(predictions) 函数测量委员会预测之间的分歧。它计算预测的方差并返回平均分歧:
def create_committee(num_models):
    committee = []
    for _ in range(num_models):
        model = LogisticRegression()
        # Customize and train each model as needed
        committee.append(model)
    return committee
def get_committee_predictions(committee, data):
    predictions = []
    for model in committee:
        preds = model.predict_proba(data)
        predictions.append(preds)
    return np.array(predictions)
def measure_disagreement(predictions):
    disagreement = np.var(predictions, axis=0)
    return np.mean(disagreement, axis=1)
我们进入主动学习循环,其中迭代训练委员会,测量分歧或不确定性,选择标记实例,为这些实例获取标签,并更新标记数据集:
labeled_dataset = labeled_data.copy()
num_iterations = 5
batch_size = 20
committee = create_committee(num_models=3)
for _ in range(num_iterations):
    for model in committee:
        X_train = labeled_dataset.drop('target', axis=1)
        y_train = labeled_dataset['target']
        model.fit(X_train, y_train)
    X_unlabeled = unlabeled_data.drop('target', axis=1)
    committee_predictions = get_committee_predictions(committee, X_unlabeled)
    disagreement = measure_disagreement(committee_predictions)
    indices = np.argsort(disagreement)[-batch_size:]
    labeled_instances = unlabeled_data.iloc[indices]
    labels = labeled_instances['Loan_Status']
    labeled_dataset = pd.concat([labeled_dataset, labeled_instances])
    unlabeled_data = unlabeled_data.drop(labeled_instances.index)
这里是对代码的解释。labeled_dataset = labeled_data.copy() 创建了初始标记数据集的副本。
num_iterations 循环表示半监督学习的轮数。在每一轮中,发生以下步骤:
- 
委员会中的每个模型都是在当前标记的数据集上训练的。
 - 
委员会对未标记的数据进行预测。
 - 
计算委员会预测之间的分歧。
 - 
确定分歧最大的实例的索引。这个批次的规模由
batch_size指定。 - 
这些实例被添加到标记数据集中。
 - 
最后,这些实例将从未标记的数据中移除。
 
这种方法背后的思想是,模型分歧最大的实例是模型最不确定的实例。通过将这些实例添加到标记数据集中,模型可以在下一轮训练中从它们中学到更多。这个过程会持续进行指定次数的迭代。
现在让我们讨论在标记过程中的多样性采样——一种专注于选择多样化的数据点集,以确保标记数据集全面且具有代表性的智能技术。
多样性采样
多样性采样基于这样一个原则:选择覆盖数据集中不同模式或区域的实例可以提供对潜在数据分布的更全面的理解。通过积极寻求多样化的实例进行标记,多样性采样旨在提高模型泛化能力和鲁棒性。
多样性采样过程通常涉及以下步骤:
- 
初始模型训练:使用一个小型标记数据集训练一个初始机器学习模型。此模型将用于指导选择多样化的实例进行标记。
 - 
实例选择:将训练好的模型应用于未标记的实例并获取预测。计算多样性度量来衡量每个实例与已标记实例之间的相似度或覆盖率。选择具有最高多样性度量的实例进行标记。
 - 
标记和模型更新:标记所选实例并将它们添加到标记数据集中。使用扩展的标记数据集重新训练或更新机器学习模型。
 - 
重复:通过返回到步骤 2来迭代这个过程,直到达到所需的性能水平或标记资源耗尽。
 
在主动学习标记中,多样性采样提供了几个优势:
- 
全面数据覆盖:通过选择具有多样性的实例,多样性采样确保标记数据集覆盖数据中的广泛模式或区域。这种方法有助于模型更好地泛化到未见过的实例,并提高其处理不同场景的能力。
 - 
探索数据分布:多样性采样通过积极从特征空间的不同部分寻找实例来鼓励探索底层数据分布。这种探索可以揭示关于数据的重要见解,并提高模型对复杂关系的理解。
 - 
缓解偏差和过拟合:多样性采样可以帮助缓解由于仅选择易于或相似的实例进行标记而可能出现的偏差和过拟合。通过多样化标记数据集,多样性采样降低了模型过度自信的风险,并增强了其鲁棒性。
 
让我们在预处理后的credit-g数据集上探索这种方法,使用 Python 中的sklearn库中的成对距离。sklearn库中的pairwise_distances函数计算数据集中每对实例之间的距离。在多样性采样的上下文中,此函数用于找到彼此差异最大的实例。
这里是这个过程:
- 
计算未标记数据集中所有实例对之间的成对距离。
 - 
识别彼此之间距离最大的实例。这些是根据距离度量指标确定的最具多样性的实例。
 - 
选择这些具有多样性的实例进行标记并将它们添加到标记数据集中。
 
理念是通过积极寻找具有最高多样性的实例(在所选距离度量下最远的实例),可以覆盖底层数据分布中的更广泛模式。这有助于提高模型对新数据的泛化能力,并增强其鲁棒性。
首先,我们导入pairwise_distances函数:
from sklearn.metrics.pairwise import pairwise_distances
我们定义了用于计算多样性和选择具有最高多样性的实例进行标记的函数。我们将使用成对欧几里得距离作为多样性度量指标:
def calculate_diversity(data):
    distance_matrix = pairwise_distances(data, metric='euclidean')
    diversity = np.sum(distance_matrix, axis=1)
    return diversity
def select_diverse_instances(data, num_instances):
    diversity = calculate_diversity(data)
    indices = np.argsort(diversity)[-num_instances:]
    return data.iloc[indices]
我们进入活跃学习循环,其中我们迭代地计算多样性,选择用于标注的多样性实例,为这些实例获取标签,并更新标注数据集:
labeled_dataset = labeled_data.copy()
num_iterations = 5
batch_size = 20
for _ in range(num_iterations):
    X_unlabeled = unlabeled_data.drop('target', axis=1)
    diversity = calculate_diversity(X_unlabeled)
    labeled_instances = select_diverse_instances(unlabeled_data, batch_size)
    labels = labeled_instances['target']
    labeled_dataset = pd.concat([labeled_dataset, labeled_instances])
    unlabeled_data = unlabeled_data.drop(labeled_instances.index)
在下一节中,我们将探讨迁移学习在标注中的应用——这是一种利用一个任务获得的知识来提高另一个不同但相关任务性能的高级方法。
迁移学习
迁移学习涉及使用从源任务或领域获得的知识来辅助学习。而不是从头开始,迁移学习利用现有的信息,如标注数据或预训练模型,来启动学习过程并提高目标任务的表现。迁移学习在机器学习的标注过程中提供了几个优势:
- 
减少标注工作量:通过利用现有的标注数据,迁移学习减少了为目标任务手动标注大量数据的需求。它允许重用相关任务、领域或数据集的知识,从而在获取新标签时节省时间和精力。
 - 
提高模型性能:迁移学习允许目标模型从源模型学习到的知识中受益。源模型可能是在大型标注数据集或不同但相关的任务上训练的,提供了有价值的见解和模式,可以增强目标模型的表现。
 - 
适应有限标注数据:当目标任务有限标注数据时,迁移学习特别有用。通过利用源任务或领域的标注数据,迁移学习可以帮助更好地泛化目标模型,并减轻在小型标注数据集上过拟合的风险。
 
迁移学习可以以多种方式应用于机器学习的标注:
- 
特征提取:利用预训练模型作为特征提取器。从预训练模型中提取高级特征,并将它们作为输入提供给在目标标注数据集上训练的新模型。
 - 
微调预训练模型:使用在大型标注数据集上训练的预训练模型,如 VGG、ResNet 或 BERT 等流行的深度学习架构中的模型。在针对目标任务的较小标注数据集上对这些预训练模型进行微调。
 
让我们更详细地讨论这些内容。
特征提取
特征提取涉及使用预训练模型学习到的表示作为新模型的输入特征。这种方法特别适用于预训练模型已经在大型通用数据集(如 ImageNet)上训练的情况。以下是一个使用 VGG16 模型进行图像标注的迁移学习示例。
注意
本例中使用的数据可以从github.com/odegeasslbc/FastGAN-pytorch获取。
我们首先导入必要的库:
import PIL
import PIL.Image
from tensorflow.keras.applications import VGG16
from tensorflow.keras.preprocessing import image
from tensorflow.keras.applications.vgg16 import preprocess_input, decode_predictions
import numpy as np
from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Flatten
我们将使用金毛猎犬的图片进行标注。我们可以使用PIL库查看这张图片:
PIL.Image.open('path_to_image_2.jpg')
这将显示以下图像:

图 6.11 – 金毛寻回犬:人类的最佳伙伴以及我们的样本图像
让我们加载 TensorFlow 库中的预训练 VGG16 模型,并预测这个样本图像的标签:
model = VGG16(weights='imagenet')
img_path = 'path_to_image_2.jpg'
img = image.load_img(img_path, target_size=(224, 224))
x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
x = preprocess_input(x)
features = model.predict(x)
decoded_predictions = decode_predictions(features, top=5)[0]
for _, label, confidence in decoded_predictions:
    print(label, confidence)
这将生成以下输出:

图 6.12 – VGG16 模型的预测结果及置信度;模型以高置信度将图像标记为 golden_retriever
模型已正确预测图像为golden_retriever,置信度为0.92119014。我们现在理解了如何在一个新的数据集上使用预训练模型。
微调预训练模型
在迁移学习中,微调指的是调整或更新预训练模型的参数,以更好地适应特定任务或感兴趣的数据集。在使用迁移学习时,预训练模型最初是在一个大规模数据集上训练的,通常来自不同但相关的任务或领域。微调使我们能够利用预训练模型学到的知识,并针对特定任务或数据集进行定制。
微调过程通常包括以下步骤:
- 
预训练模型初始化:加载已经从源任务或数据集中学习到有用表示的预训练模型。模型的参数最初被冻结,这意味着在初始训练期间它们不会被更新。
 - 
模型的修改:根据具体任务或数据集,预训练模型的最后几层或特定部分可能需要修改或替换。模型的架构可以根据所需的输出或适应目标任务的特性进行调整。
 - 
解冻和训练:修改模型后,之前冻结的参数被解冻,允许它们在训练过程中更新。然后,模型在目标任务特定的数据集上训练,通常称为微调数据集。模型的权重通过反向传播和基于梯度的优化算法更新,以最小化任务特定的损失函数。
 - 
使用较低的学习率进行训练:在微调过程中,通常使用比预训练模型初始训练时更小的学习率。这个较小的学习率有助于在一定程度上保留之前学到的表示,同时允许模型适应目标任务或数据集。
 
微调的过程在利用预训练模型捕获的知识和针对目标任务的特定细节进行定制之间找到了平衡。通过微调,模型可以学习特定于任务的模式并优化其在新任务或数据集上的性能。所需的微调量可能因源任务和目标任务或领域之间的相似性而异。在某些情况下,只需要几次训练迭代就足够了,而在其他情况下,可能需要更广泛的训练。
微调是迁移学习中的一个关键步骤,因为它使得从源任务或数据集向目标任务的知识迁移成为可能,从而提高了目标任务的表现并加快了收敛速度。以下是一个使用 VGG16 模型进行图像标注的迁移学习示例。我们首先导入必要的库:
from tensorflow.keras.applications import VGG16
from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Flatten
我们有两种图像类别——狗和猫,因此我们将类别数量变量设置为2。我们还将加载一个不带顶层预训练的 VGG16 模型。我们这里的图像大小是 256 x 256 x 3:
num_classes = 2
base_model = VGG16(weights='imagenet', include_top=False, input_shape=(256, 256, 3))
我们现在冻结预训练层并创建一个新的模型用于微调。然后我们使用'adam'作为优化器来编译模型:
for layer in base_model.layers:
    layer.trainable = False
model = Sequential()
model.add(base_model)
model.add(Flatten())
model.add(Dense(256, activation='relu'))
model.add(Dense(num_classes, activation='softmax'))
model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
为了准备训练,我们正在配置训练集和验证集的数据生成器。这一关键步骤涉及使用ImageDataGenerator将像素值缩放到 0 到 1 之间,这样做可以确保图像数据的一致和高效处理,增强模型在训练期间学习模式和特征的能力:
train_data_dir = /path_to_training_data'
validation_data_dir = '/path_to_validation_data'
batch_size = 32
train_datagen = ImageDataGenerator(rescale=1.0/255.0)
validation_datagen = ImageDataGenerator(rescale=1.0/255.0)
train_generator = train_datagen.flow_from_directory(
    train_data_dir,
    target_size=(256, 256),
    batch_size=batch_size,
    class_mode='categorical')
validation_generator = validation_datagen.flow_from_directory(
    validation_data_dir,
    target_size=(256, 256),
    batch_size=batch_size,
    class_mode='categorical')
我们现在可以微调模型并保存它:
epochs = 10
model.fit(
    train_generator,
    steps_per_epoch=train_generator.samples // batch_size,
    epochs=epochs,
    validation_data=validation_generator,
    validation_steps=validation_generator.samples // batch_size)
model.save('fine_tuned_model.h5')
使用这个保存的模型,我们可以将其部署到各种应用中,例如对新数据进行预测、将其集成到更大的系统中,或进一步微调类似任务。保存的模型文件封装了训练过程中学习到的模式和特征,为未来的使用和分析提供了宝贵的资源。
在下一节中,我们将深入探讨标签中的半监督学习概念——这是一种既复杂又易于接近的技术,它结合了标记和无标记数据的优点。
半监督学习
传统的监督学习依赖于一个完全标记的数据集,这可能会消耗大量时间和成本。另一方面,半监督学习允许我们利用标记和无标记数据来训练模型并做出预测。这种方法提供了一种更有效的方式来标记数据并提高模型性能。
半监督学习在标记数据稀缺或昂贵时特别有用。它允许我们利用大量现成的无标记数据,这在现实世界场景中通常很丰富。通过利用无标记数据,半监督学习提供了几个好处:
- 
成本效益:半监督学习减少了对外昂贵的手动标记工作的依赖。通过使用成本较低的未标记数据,我们可以显著减少获取标记数据相关的费用。
 - 
利用大型未标记数据集:未标记数据通常很丰富且易于获取。半监督学习使我们能够利用这一巨大资源,使我们能够在比全监督学习更大的数据集上训练模型。这可能导致更好的模型泛化能力和性能。
 - 
改进模型性能:通过在训练过程中结合未标记数据,半监督学习可以提高模型性能。未标记数据提供了额外的信息,并帮助模型更准确地捕捉潜在的数据分布。这可能导致更好的泛化能力和在未见数据上的更高准确性。
 
在半监督学习中,有不同方法可以利用未标记数据以不同的方式。以下是一些常见方法:
- 
自训练:自训练涉及首先在有限的标记数据上训练一个模型。然后,使用该模型对未标记数据进行预测,并将自信的预测视为未标记实例的伪标签。这些伪标签实例随后与标记数据结合,以迭代方式重新训练模型。
 - 
协同训练:协同训练涉及在数据的不同子集或视图中训练多个模型。每个模型从标记数据中学习,然后为未标记数据预测标签。模型对未标记数据预测的一致性或差异性被用来选择最自信的实例,这些实例随后被标记并添加到训练集中进行进一步的迭代。
 - 
生成模型:在半监督学习中,可以使用生成模型,如变分自编码器(VAEs)或生成对抗网络(GANs)。这些模型学习潜在的数据分布并生成合理的实例。通过将生成的实例纳入训练过程,模型可以捕捉更多样化的表示并提高其泛化性能。
 
让我们看看在 Python 中实现这种标记方法的一个简单示例。首先,我们导入必要的库:
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
from sklearn.semi_supervised import LabelPropagation
我们利用之前示例中预处理过的credit-g数据集,并将其分为标记和未标记的子集。本例假设您正在使用我们在不确定性 采样部分创建的df_preprocessed DataFrame:
X = df_preprocessed.drop('target', axis=1)
y = df_preprocessed['target']
# Split the dataset into labeled and unlabeled
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
labeled_percentage = 0.1  # Percentage of labeled data
X_train_labeled, X_train_unlabeled, y_train_labeled, _ = train_test_split(
    X_train, y_train, test_size=1 - labeled_percentage, random_state=42)
然后我们使用标记数据训练一个监督机器学习模型。在本例中,我们将使用逻辑回归作为监督模型:
supervised_model = LogisticRegression()
supervised_model.fit(X_train_labeled, y_train_labeled)
我们接着应用训练好的监督模型来预测未标记数据的标签。预测的标签被视为未标记实例的伪标签:
# Predict labels for the unlabeled data
pseudo_labels = supervised_model.predict(X_train_unlabeled)
现在将标记数据(X_labeled)与伪标记数据(X_unlabeled)连接起来,创建结合的特征数据集(X_combined)。将相应的标签(y_labeled和pseudo_labels)连接起来,创建结合的标签数据集(y_combined):
# Concatenate the labeled data with the pseudo-labeled data
X_combined = np.concatenate((X_labeled, X_unlabeled))
y_combined = np.concatenate((y_labeled, pseudo_labels))
接下来,使用结合的特征数据集(X_combined)和标签数据集(y_combined)训练一个半监督机器学习模型。在这个例子中,我们将使用LabelPropagation作为半监督模型:
semi_supervised_model = LabelPropagation()
semi_supervised_model.fit(X_combined, y_combined)
使用训练好的半监督模型对测试集进行预测并计算准确率:
y_pred = semi_supervised_model.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print(f'Test accuracy: {accuracy:.3f}')
打印语句输出了最终的准确率分数,在这个例子中,准确率为 0.635。
在使用LabelPropagation训练我们的semi_supervised_model之后,得到的模型有效地从标记和无标记数据中学习。测试集(y_pred)上的预测展示了模型泛化和推断先前未见实例标签的能力。这个输出作为半监督学习技术的一个宝贵示例,这些技术利用了标记和无标记数据,可以在现实世界场景中做出稳健和准确的预测。
摘要
在本章中,我们探讨了机器学习中各种程序化标记技术。标记数据对于训练有效的模型至关重要,而手动标记可能既耗时又昂贵。程序化标记提供了自动化的方法来为数据的实例分配有意义的类别或类别。我们讨论了一系列技术,包括模式匹配、数据库查找、布尔标志、弱监督、半弱监督、切片函数、主动学习、迁移学习和半监督学习。
每种技术都根据数据的性质和特定的标记要求提供独特的优势和考虑。通过利用这些技术,从业者可以简化标记过程,减少人工工作量,并使用大量标记或弱标记的数据训练有效的模型。理解和利用程序化标记技术对于构建稳健和可扩展的机器学习系统至关重要。
在下一章中,我们将探讨合成数据在以数据为中心的机器学习中的作用。
第七章:在以数据为中心的机器学习中使用合成数据
在前面的章节中,我们讨论了通过更好的收集和标注来提高机器学习目的数据质量的各种方法。
尽管人类标注者、数据所有权和技术数据质量改进实践对于数据中心化至关重要,但个人或通过经验观察所能执行的数据标注和数据创建类型是有局限性的。
合成数据有潜力填补这些空白,并以其他方法成本和时间的一小部分产生全面训练数据。
本章介绍了合成数据生成。我们将涵盖以下主要主题:
- 
合成数据是什么以及为什么它对数据中心化很重要
 - 
合成数据如何被用来生成更好的模型
 - 
常用的生成合成数据的技术
 - 
使用合成数据的风险和挑战
 
让我们先定义一下什么是合成数据。
理解合成数据
合成数据是人工创建的数据,如果做得正确,它包含了生产数据的所有特征。
它被称为合成数据的原因是它没有物理存在——也就是说,它不是来自我们为了收集数据而创建的真实生活观察或实验。
机器学习的一个基本原理是你需要大量的数据,从几千到几十亿个观测值不等。所需的数据量取决于你的模型。
正如我们已经多次概述的那样,当所需的数据量难以获得时,一种方法是提高你数据中的信号,使其能够在较小的数据集上产生准确和相关的输出。
另一个选择是创建合成数据来填补空白。合成数据的一个主要优点是其可扩展性。真实训练数据的收集是线性的,一次收集一个示例,这既耗时又昂贵。
相比之下,合成数据可以在相对较短的时间内以非常大的数量生成,并且通常成本较低。例如,如果从标注服务中获得,一个训练图像可能需要花费 5 美元,但如果人工生成,可能只需 0.05 美元。
合成数据被誉为解决更强大的机器学习和 AI 解决方案开发中许多挑战的答案。从解决隐私问题到以低成本生成建模和训练数据中罕见但重要的观察结果,合成数据可以填补现实世界数据不足的空白。
根据 Gartner 的预测 2,到 2024 年,用于 AI 和数据分析项目中 60%的数据将是合成的,而不是通过现实世界的观察收集的。
传统上,用于分析目的的数据使用是由我们拥有的数据和其局限性所驱动的。我们可能会想象一个完美的数据解决方案,但通常,数据集的深度、广度、可靠性和隐私限制会限制我们在现实中能做的事情。在很大程度上,这就是合成数据旨在解决的问题。
创建合成数据有不同的方法,在某种程度上,数据的创建技术是最不复杂的部分。验证一个合成数据集是否是潜在真实世界场景的相关反映,以及防御不希望的偏差可能既耗时又具有挑战性。
如果你选择为你的下一个项目使用合成数据,最重要的第一个问题总是,“你打算用这些数据做什么?”这个问题的答案决定了你的数据需求,反过来,这将突出你的数据空白。
让我们更深入地了解使用合成数据的典型原因,并探讨一些常见的使用场景。
合成数据的使用场景
使用合成数据的原因通常可以分为以下四个类别:
- 
可用性:合成数据创建用于弥补某个领域数据不足的问题。可能的情况是,与现实生活中的分布相比,数据集中存在不平衡的类别,因此为了使这些类别平衡,我们创建合成数据来补偿。
 - 
成本:收集某些类型的数据可能非常昂贵且耗时,在这种情况下,生成合成数据以减少项目在时间和成本上的投入可能是有用的。
 - 
风险管理:在某些情况下,合成数据也可以用来降低人类或财务损失的风险。一个例子是飞行模拟器,它被用来在各种情况下训练新飞行员和经验丰富的飞行员。在模拟环境中训练飞行员使我们能够安全且有意识地引入在自然环境中难以创建且风险不可接受的事件。
 - 
安全和法律合规性:你可能已经拥有了所需的数据,但出于机器学习目的使用它可能是不安全或不合法的。例如,某些法规,如欧洲的通用数据保护条例(GDPR),禁止在没有底层个人明确同意的情况下使用某些类型的数据。或者,在你的组织中获取批准可能过于缓慢和繁琐。
 
下面是一些合成数据常见和潜在使用场景的例子:
- 
计算机视觉和图像及视频处理
 - 
自然语言处理
 - 
隐私保护
 - 
纠正偏差(在第八章中讨论,识别和移除偏差的技术)
 - 
提高数据质量或填补数据空白(更便宜)
 - 
增加罕见事件建模数据量(在第九章中讨论,处理机器学习中的边缘情况和罕见事件)
 - 
模拟
 
我们将在本章中探讨一些这些主题,以说明合成数据如何作为您模型开发策略的一部分被使用。
为了设定场景,让我们看看合成数据在正确设置下可以有多强大,这得益于世界上领先的计算机游戏软件开发公司 Unity Technologies。作为背景,Unity 平台被用于创建 2021 年排名前 1000 的移动游戏的 72%以及移动、PC 和游戏机上的所有电脑游戏的 50%。
Unity 技术的用户通过仅通过将合成数据添加到现实世界数据中,就将物体识别率从 70%提高到几乎 100%。合成数据为训练数据增加了更多的多样性和更多的场景,使得物体可以从多个角度被识别。
Unity 人工智能和机器学习副总裁 David Lange 表示如下:
“我们使用 Unity 引擎来重新创建包含物体的三维世界。然后,我们可以生成看起来非常像在现实世界中的合成图像,**标签完美。”
“现实世界的数据实际上只是对情况的快照。你可以通过添加合成数据来增强现实世界,包括特殊用例、特殊情况和特殊事件。你可以在你的数据集中添加合成数据来提高你数据的多样性。”
“我们可以创建不可能的情况,因为这不会在毫秒内给我们带来任何成本,而不是试图在现实中安排它们。你可以轻松创建所有这些场景的便利性正在推动合成数据的使用。”
David Lange 与 Gartner 的观点一致,认为合成数据将成为未来机器学习的主要原材料:
“我相信大部分的训练数据将是合成数据。你必须有一个现实世界作为基准,但合成数据消除了隐私问题,因为没有涉及真实的人。你可以消除偏见。你可以进行数据分析,并确保你的数据以非常均匀的方式代表现实世界,比现实世界做得更好。”
注意 David Lange 提到的合成数据的益处:
- 
物体可以被完美地标记,从而避免在前面章节中讨论的标签模糊性
 - 
数据集的多样性可以大幅增加,以覆盖可能的场景的微小变化,以及罕见事件和边缘情况
 - 
数据集可以快速且便宜地扩展
 - 
由于数据更干净且去个性化,可以减少偏见和隐私问题
 
让我们更深入地探讨合成数据的各种用途,以了解与之相关的可能性、好处、风险和限制。
用于计算机视觉、图像和视频处理的合成数据
在撰写本文时,合成数据在计算机视觉问题中最普遍的使用。这是因为我们通常可以以有限的风险创建这类数据,而异常值(通常是罕见但影响重大的事件)在图像数据中尤其难以获取。
计算机视觉(以及大多数其他机器学习问题)的一个共同挑战是,现实世界的数据通常包含大量描述最可能场景的观察,而罕见事件几乎没有或没有例子。
同时,收集现实世界数据可能很困难、昂贵,甚至直接危险。例如,自动驾驶汽车模型不能通过将真实汽车置于危险情况来训练以避免车祸。相反,这些车祸和其他罕见但重要的事件必须进行模拟。
图像分类算法的一个常见问题是识别在略微不熟悉的位置或环境中的熟悉物体。由于机器学习算法不是通过逻辑或抽象进行推理,即使模型在训练和测试数据集上都表现良好,也往往无法推广到分布外的观察。无论这些观察是作为对模型性能的对抗性测试引入,还是自然发生,都是如此。
图 7.1提供了一个这种现象的简化示例。一个旋转 45 度的正方形可能被一些人——包括人类和算法——解释为钻石,而不仅仅是与侧置的正方形具有相同尺寸的倾斜正方形:

图 7.1 – 这两个正方形要么相同,要么不同,这取决于我们用来解释它们的规则
这种偏差的影响往往是重大的。在分析 ImageNet 数据集上的图像的深度神经网络性能时,Alcorn 等人(2019)5 描述了常见的图像分类器Google Inception-v3、AlexNet和ResNet-50如何能轻易被图像中物体位置的一点点变化所欺骗。
在图 7.2中,列(d)中的图像是从互联网上收集的真实照片,而列(a)到(c)是同一物体在不寻常位置中的分布外图像。图像中的主要物体被翻转和旋转,但背景保持不变:

图 7.2 – 当熟悉物体处于不常见位置时,深度神经网络很容易被欺骗。列(d)代表真实图像,而列(a)到(c)是合成的
作者随后使用Inception-v3对这些图像进行分类,每个图像下的标签和置信度得分被描绘出来。在这些例子中,当物体在现实场景中的常见位置时,算法能够以高精度和置信度进行分类。然而,当物体被翻转、旋转或非常接近移动时,算法会以高置信度错误地分类图像。
该算法不仅错误地分类了物体,而且还自信地将它们错误地标记为它们不是的物体。一辆滚动的公交车和一个沙袋就像白垩和奶酪一样,而一辆翻倒的滑板车与降落伞相差甚远。
能够在陌生位置识别熟悉物体对于观察和分类移动物体尤为重要。在自动驾驶汽车的例子中,算法的错误解读会将新颖且意外的事件引入交通环境,尽管这些车辆在统计数据上比人类驾驶员更安全 6。
以下图片是从台湾的交通摄像头中捕捉到的,展示了这个问题的一个例子。一辆卡车在繁忙的高速公路上翻车,一辆自动驾驶的特斯拉轿车没有将卡车识别为路上的障碍物,并以高速撞向了卡车:

图 7.3 – 一辆自动驾驶汽车高速撞上一辆翻车的图片。可以使用合成数据训练算法来处理这种新颖的情况
在这种极不可能但非常危险的场景中,汽车的算法没有装备正确评估前方物体阻挡道路的统计概率。
这是一种合成数据证明非常有价值的情况。正如我们刚刚学到的,即使是最好的计算机视觉算法对常见物体位置的变化也具有高度敏感性。因此,必须在训练数据中引入各种位置和光照条件,以覆盖所有可能的组合,特别是高度不可能的组合。
当我们使用机器学习来分析图像中的情况时,我们正在提取图像中的凹凸曲线,也称为深度神经网络中的特征。
从合成数据的角度创建这些曲线,你实际上是在图像中重新创建那些形态。这通常包括翻转、旋转、缩放、裁剪、调整光线变化以及调整图像大小以在相同场景中创建轻微的变化。
在特斯拉事故的例子中,我们可能会创建卡车正常行驶、侧翻、仰翻、逆向行驶、在黑暗中、部分被其他物体覆盖等图像。这些场景在现实生活中的图像中很难获得,但它们在出现这种情况时非常重要。
使用生成对抗网络(GANs)生成合成数据
GANs 是生成具有类似真实世界数据特性的合成图像数据的常用工具。GANs 由计算机科学家伊恩·古德费洛(Ian Goodfellow)于 2014 年发明,并自此以来导致了能够创建各种内容的生成模型爆炸式增长,包括文本、艺术、视频和图像。
GANs 是一种无监督学习形式,其中生成器模型与判别器模型相对抗(因此有“对抗”这一方面)。这两个模型都是神经网络,它们相互竞争,将练习变成一个“伪监督”学习问题。
生成器识别原始数据集中的模式,然后使用这些模式生成可能存在于输入数据中的合成输出。生成的示例成为判别器模型的负训练样本。
判别器的任务是分类新生成数据为虚假或真实。这种零和竞赛持续进行,直到判别器将虚假观察结果作为真实的接近 50%的时间。
从数学上讲,GAN 的训练过程可以被视为最小化一个损失函数,该函数衡量生成示例与真实示例之间的差异。这个损失函数通常是两个术语的组合:一个衡量生成模型产生与真实示例相似的示例的能力,另一个衡量判别模型区分真实和生成示例的能力。
通过以这种方式训练 GAN 的两个部分,生成模型可以学习训练数据集中真实示例的模式和特征,然后使用这些信息生成与真实示例相似的新示例。这使得 GANs 可以用于广泛的用途,包括图像生成、文本生成等。
图 7**.4提供了一个概念图,说明了 GANs 如何通过大量的小型竞赛迭代,最终到达一个可以生成非常逼真输出的模型:

图 7.4 – GAN 工作原理的概念图
生成器最初以一些非常基本的输出形式开始,但随着它通过许多示例进行迭代,它欺骗判别器的技巧会变得更好。当判别器难以识别真实与虚假时,训练完成。
GANs 的渐进式增长
如你所想,生成器最初创建的几个示例对判别器来说相对容易识别。生成器模型有很多要学习的东西,并且使 GAN 遵循我们希望其采取的学习路径可能具有挑战性。
GANs 本质上是具有不稳定性的模型,尤其是在生成复杂结构,如图像时。为了欺骗判别器,生成器必须捕捉到图像的细节和较大结构,这在高分辨率图像上可能很困难。如果生成器做不到这一点,它将陷入一个无法欺骗判别器的无人地带。
另一个挑战是,大图像需要大量的计算机内存。因此,批大小(每次训练迭代中用于更新模型权重的图像数量)必须经常减少,以确保图像可以装入内存。这再次使得训练过程变得不稳定。
解决这些问题的方法是通过逐步增加模型输入和输出的细节和复杂性。渐进增长最早由 NVIDIA 研究人员 Karras 等人于 2017 年提出,是一种训练 GANs 的技术,允许模型在多次迭代中逐渐增加生成图像的分辨率。
在渐进增长过程中,模型使用逐步方法进行训练。首先,生成器和判别器模型使用低分辨率图像进行训练,并通过改变参数以优化损失函数来提高图像质量。然后,在基于初始训练阶段收集的理解的基础上进行微调,同时提高生成图像的分辨率,直到达到所需的分辨率。换句话说,模型是分步骤学习的,而不是一次性学习。
Karras 等人提出使用一批 4x4 像素的低分辨率图像来训练生成器和判别器。然后,使用一个新的采样层逐渐增加图像的复杂度到 8x8,采用最近邻插值。
逐渐引入新的网络层以在分辨率层之间创建最小的干扰。这种方法允许新组件平滑且无缝地集成到现有基础设施中。
通过向现有的输入或输出层添加更高分辨率的输入来实现新的一层层的逐渐引入。新输出的相对影响通过一个权重α来控制,其中原始输出的权重为 1 - α。随着α的增加,旧层逐渐淡出,而新层接管。
此过程一直持续到达到所需的图像分辨率。图 7.5,来自 Kerras 等人,突出了从 4x4 到 1,024x1,024 像素图像的渐进增长过程:

图 7.5 – 来自 Kerras 等人(2017)的 GAN 渐进增长可视化
这意味着模型可以先了解图像的整体情况,然后专注于更小的细节。这种方法通常比一次性学习所有内容要产生更好的结果。通过利用这种方法,GAN 可以掌握低分辨率数据集的基本架构和特征,从而以更高的精度创建出更高质量的图像。
使用 StyleGANs 实现更高的精度
英伟达的研究团队在其渐进式 GAN 架构的基础上,于 2018 年 12 月推出了第一个 StyleGAN。从那时起,已经发布了 StyleGAN-2 和 StyleGAN-3 架构。这些增量升级解决了原始 StyleGAN 输出中的一些系统性问题,但在结构上基本相似。
StyleGAN 的主要创新是能够控制生成模型创建的输出风格。新的架构允许生成模型自动将图像中的更广泛特征与随机/随机特征分开。广泛特征的例子包括一个人的姿态和身份;头发和雀斑被认为是随机的。
在 StyleGANs 引入之前,图像生成器的内部工作原理部分是神秘的,因此难以控制。没有有效的比较不同模型产生的不同图像的方法,以及对特征起源的有限理解,原始 GAN 生成器是黑盒。
让我们看看图 7.6 中渐进式 GAN(ProGAN)和 StyleGAN 架构的比较,以了解为什么 StyleGAN 在生成高度逼真的合成图像方面如此成功:

图 7.6 – Kerras 等人(2018)中 ProGAN(a)和 StyleGAN(b)的比较
虽然 ProGAN 使用渐进式训练方法逐层提升生成图像的分辨率,但 StyleGAN 通过使用映射网络和综合网络,使用户能够对生成图像有更多的控制。
StyleGAN 的映射网络是一种神经网络,它将低维向量 z 映射到中间潜在空间 w。这被称为解耦表示学习。通过解耦这两个空间中的特征,用户可以更容易地控制生成图像的不同方面,例如其生理结构、发型或服装。简单来说,它允许对图像的高级特征进行分离控制,而不是具体的像素值。
综合网络是一种深度卷积神经网络,它通过接收风格向量 w 作为输入并返回输出图像来工作。在合成逼真图像时,特征从特征向量中提取并逐层应用于图像层,从最低层开始,每次只提升一层到更高的分辨率。
综合网络与一个学习到的自适应实例归一化(AdaIN)模块交互,该模块通过调整图像特征的比例和偏差来重新缩放图像特征,以增加图像输出的多样性。该模块接受特征向量和风格向量作为输入,通过减去特征图的均值并除以标准差来调整图像特征的缩放和偏差。因此,StyleGAN 可以通过关注特定的特征,如发型或眼睛颜色,来产生高度详细的图像。
理解 GAN 的挑战
尽管 GAN 是机器学习工具箱中的一个美妙补充,但它们并非没有挑战。
GAN 基于零和博弈理论。本质上,如果一个玩家获胜,那么另一个玩家就会失败。这种情况也被称为最小-最大:你的对手寻求最大化其输出,而你寻求最小化它。GAN 背后的理论表明,生成器和判别器模型之间的游戏将继续进行,直到达到纳什均衡。
纳什均衡是经济学、政治学和进化生物学中的一个重要解决方案概念,可以在各种现实场景中看到。纳什均衡是一种情况,其中没有任何玩家有激励去做与他们目前所做的事情不同的任何事情。这是因为他们已经考虑了其他人正在做的事情,并且他们认为他们当前的策略是最佳可能的选择。
在这种情况下,所有玩家都被说是在均衡状态,因为他们没有激励去改变他们的行为,因为任何由一个玩家做出的改变都可能使该特定玩家的结果变得更糟。因此,在这种情况下不做出任何突然的改变符合每个个体的最佳利益。
例如,考虑一个竞争公司试图为其产品或服务设定价格的场景。如果每家公司都将价格设定得太高,它可能会失去竞争对手的客户。然而,如果每家公司都将价格设定得太低,它将无法覆盖其成本并获得利润。因此,这种情况下的纳什均衡是每家公司都将价格设定在一个足够低的水准,以阻止客户从竞争对手那里购买,同时又不会低到无法获得利润。
尽管这个理论可以工作,但在实践中往往很难实现。没有保证成本函数会收敛并找到一个纳什均衡。在这种情况下,游戏会无限期地继续。
此外,当一个代理在力量和效力方面优于另一个代理时,其对应方的学习信号就变得毫无价值;因此,双方都没有获得任何知识。最常见的情况是,判别器在挑选生成器的错误方面变得如此出色,以至于生成器从未学会如何在游戏中前进。
GANs 的一个主要挑战被称为模式崩溃。像任何其他统计模型一样,GANs 倾向于找到通过底层数据的最简单路径,这可能导致模态观察结果的过度表示。
当生成器产生一个特别可信的输出时,模式崩溃是另一个常见问题,这会导致它只产生那个输出。一旦发生这种情况,判别器更有可能陷入局部最小值,无法找到它认为有效的更好输出。
因此,生成器被驱动着调整其输出以符合这个静态判别器使用的标准,而不是试图创建真实或动态的输出。因此,生成器往往会对由其单个判别器确定的特定结果进行“过度优化”。
图 7.7 中的模式崩溃示例如图所示,来自 Metz 等人(2017)的研究。在这个例子中,研究人员使用了手写数字的 MNIST 数据集来训练两个不同的 GAN。MNIST 数据集包含 10 种不同的模式,代表数字 0-9。
数字的前四个象限已经成功生成(使用展开的 GAN 训练方法),看起来像真实的手写数字,并代表所有可能的数字。另一方面,底部的四个象限(使用原始 GAN 架构生成)在早期过程中就出现了模式崩溃,只产生了数字 6 的表示:

图 7.7 – 来自 Metz 等人(2017)的 MNIST 数据集上训练的两个不同的 GAN 架构
自从 2014 年原始 GAN 架构的开发以来,已经引入了几个新的 GAN 变体来处理模式崩溃。因此,这现在是一个较少见的问题,但始终需要注意。
在 MNIST 示例中,模式崩溃相对容易发现,但重要的是要注意,GANs 可能会以更微妙的方式潜在地保留和加剧现有的偏见。
在 2021 年的一项研究中(Jain 等人,2021 年),亚利桑那州立大学和伦斯勒理工学院的研究人员评估了 GAN 在生成合成面部数据方面的性能。研究人员想测试 GAN 是否会加剧(自然)有偏见的输入数据集的模态面部特征,从而增加合成输出中最常见的特征。
比较了两种模型架构:深度卷积 GAN(DCGAN)和 ProGAN。实验涉及来自美国大学的 17,245 张工程教授的图像数据集,其中 80%被归类为男性,76%被归类为白人。实验使用了人类分类器(Turkers)来对原始数据集和 GAN 生成的面部进行分类。
图 7.8显示了两个测试的 GAN 与原始数据集相比的结果。DCGAN 模型导致了严重偏差的数据生成,合成图像中男性和白人比例过高。虽然 ProGAN 模型的偏差较小,但它仍然不是原始数据集中特征的合理表示。
DCGAN 和 ProGAN 都对具有女性特征的图像进行了惩罚。DCGAN 生成了最偏差的输出,将女性面孔的比例从 20%降低到 6.67%。
与原始数据集中的 24%非白色面孔相比,DCGAN 和 ProGAN 都显著降低了这一比率——DCGAN 为 1.33%,ProGAN 为 11.33%:

图 7.8 – 通过 GANs 生成的合成面部图像与原始数据集中性别和肤色分布的比较。来源:Jain 等人,2021 年
换句话说,GANs 可以生成高度逼真的合成数据集,但这些数据集可能仍然是原始数据集中潜在特征的偏代表性。讽刺的是,这种部分模式崩溃可能产生更逼真的图像,因为 GAN 已经专门化以在特定维度上表现良好。
根据以数据为中心的机器学习原则,识别合成数据集所有维度的模态偏差对于确保其对您的目的有用且符合道德规范至关重要。通常,这是一个试错过程,以经验验证并消除 GAN 输出中的偏差。
最后,GANs 的一个显著弱点是生成高质量输出需要大量的计算资源,没有这些资源,生成的图像可能看起来模糊或不真实。此外,如果没有经验的人选择适当的图像生成方向,使用 GANs 可能不会产生预期的结果。
通过实用例子探索图像增强
尽管合成图像数据生成涉及复杂性,但我们希望以一个实用的例子结束本节,激发您在工作中应用这些技术的灵感。
在本节中,我们将介绍数据增强,这是一种为图像数据生成合成数据的方法。我们将使用在 ImageNet 数据上训练的预训练 Xception 模型,并将其微调以适应服装示例。我们将通过应用迁移学习来微调服装示例,然后生成合成数据以增强其性能。通过迁移学习,我们可以冻结网络的预训练层,并通过更新最终输出仅训练新层。这有助于模型快速适应新数据集,同时减少训练时间。
通过在预训练的 Xception 模型之上应用迁移学习和图像增强技术,我们可以生成可以提升模型在新数据上性能的合成数据。这种方法在包括图像分类、目标检测和分割在内的各种应用中得到了广泛应用。
首先,我们需要使用 TensorFlow 库加载预训练的 Xception 模型。我们可以通过简单地从 TensorFlow 模块导入 Xception 模型,并将include_top参数设置为False来排除模型的顶层来实现这一点。
接下来,我们必须在预训练的 Xception 模型之上添加一个自定义分类器。我们可以通过添加几个密集层和一个最终输出层来实现,输出层的类别数等于我们想要分类的服装类别数量。
为了进一步提高模型性能,我们将使用 TensorFlow 的现成功能应用图像增强。这可以包括旋转、缩放、翻转和其他技术,以在训练数据中创建变化,并使模型更加鲁棒。
最后,我们将使用增强的训练数据训练模型,并在验证集上评估其性能。我们可以微调模型的超参数和增强技术以实现更高的准确率。
注意
此示例已改编自《机器学习 Bookcamp》第七章,该书可在livebook.manning.com/book/machine-learning-bookcamp/chapter-7/找到,并且该书作者通过其公司 DataTalks Club(https://github.com/alexeygrigorev/mlbookcamp-code)提供的相关课程中也有提供。
首先,我们将导入所有必要的库:
import tensorflow as tf
from tensorflow.keras.applications.xception import Xception, preprocess_input, decode_predictions
from tensorflow.keras.preprocessing.image import load_img, ImageDataGenerator
import numpy as np
from tensorflow import keras
import matplotlib.pyplot as plt
2024-01-29 17:34:32.614910: I tensorflow/core/util/util.cc:169] oneDNN custom operations are on. You may see slightly different numerical results due to floating-point round-off errors from different computation orders. To turn them off, set the environment variable `TF_ENABLE_ONEDNN_OPTS=0`.
接下来,我们将通过克隆数据集从其各自的 GitHub 仓库下载数据:
!git clone git@github.com:alexeygrigorev/clothing-dataset-small.git
接下来,我们将加载从服装数据集仓库下载的裤子图像。为了加载图像,我们将使用load_img函数:
path = './clothing-dataset-small/train/pants/12bfe0f0-accc-4539-ab51-53f63534938e.jpg'
load_img(path)
这将显示以下输出:

图 7.9 – load_img 函数的输出结果,一对裤子的图像
接下来,我们将图像表示为 NumPy 数组,因为这是模型期望的图像格式。我们还需要确保所有图像都以相同的尺寸传递给模型,因此我们将选择标准尺寸(299, 299, 3),这意味着从上到下 299 像素,从左到右 299 像素,以及三个颜色通道,分别是红色、绿色和蓝色。每个像素将用每个颜色通道的 0-255 值表示三次。
现在,我们必须以(299,299)的目标大小加载前面的图像:
img = load_img(path=path, target_size=(299,299))
img_input = np.array(img)
img_input.shape
(299, 299, 3)
让我们看看我们的新尺寸图像:
img
输出如下:

图 7.10 – 我们的一对裤子图像,已调整大小为 299x299 像素
现在我们已经确保了维度,我们将加载预训练的 Xception 模型并指定输入格式,这与之前指定图像的维度相同。一旦我们加载了模型,我们将对图像进行评分并检查预测:
model = Xception(weights='imagenet', input_shape=(299,299,3))
模型期望图像列表以特定格式;因此,我们将传递之前的图像作为列表,并将其转换为 NumPy 数组。然后,我们将使用preprocess_input对图像进行预处理,并对其进行评分。预测将存储在pred中:
img_preprocessed = preprocess_input(np.array([img_input]))
pred = model.predict(img_preprocessed)
pred.shape
1/1 [==============================] - 3s 3s/step
(1, 1000)
preprocess_input函数是必需的,因为它需要预处理模型所需的图像数据。由于 Xception 模型在训练时,输入值从 0-255 转换并缩放到-1 和 1 之间。这一点很重要,因为颜色尺度的分布可能会影响预测。想象一下,红色颜色尺度在 0-100 之间,而蓝色颜色尺度在 200-300 之间;这可能导致模型不稳定。因此,缩放很重要。如果没有正确的预处理,预测将没有意义。
接下来,我们将使用一个便捷函数来解码这些预测。由于 Xception 模型被训练来预测 1,000 个标签,因此模型将提供 1,000 个不同类别中前五个标签的概率。我们不相信目标标签包含服装示例,所以在接下来的几个步骤之后,我们将转向迁移学习。为了解码预测,我们将使用decode_predictions函数。decode_predictions是一个便捷函数,它以易于理解的方式提供预测:
decode_predictions(pred)
[[('n03594734', 'jean', 0.651147),
  ('n04371430', 'swimming_trunks', 0.22369406),
  ('n03710637', 'maillot', 0.004711655),
  ('n04525038', 'velvet', 0.0038891942),
  ('n03595614', 'jersey', 0.003085624)]]
很明显,裤子的图像与jean标签最接近,然后是swimming_trunks标签,这是根据 Xception 模型中使用的标签。然而,在训练数据中,没有jean或swimming_trunks标签。接下来,我们将提取训练数据并确保在传递给训练之前对其进行预处理。
为了做到这一点,我们将使用迁移学习并利用整个训练数据。我们将使用ImageDataGenerator来帮助处理模型所需输入数据,然后创建训练和验证数据集。训练数据将包括 3,041 张图像,而验证数据将包括 341 张图像。对于预处理,我们将使用 150x150 像素而不是 299x299 像素,以减少训练时间:
train_gen = ImageDataGenerator(preprocessing_function=preprocess_input)
接下来,我们将使用flow_from_directory属性来处理整个训练和验证数据集。我们将使用seed=42来确保数据被传递到网络中,它以相同的随机化方式传递,并且每个训练层的预测结果是可重复的。对于验证数据,我们将使用shuffle=False以确保在每一步训练中没有随机化,但数据是顺序传递的:
train_ds = train_gen.flow_from_directory(directory='./clothing-dataset-small/train/', target_size=(150,150), batch_size=32, seed=42)
validation_ds = train_gen.flow_from_directory(directory="./clothing-dataset-small/validation/", target_size=(150,150), batch_size=32, shuffle=False)
Found 3081 images belonging to 10 classes.
Found 341 images belonging to 10 classes.
现在,我们可以查看我们数据集的目标类别:
train_ds.class_indices
{'dress': 0,
 'hat': 1,
 'longsleeve': 2,
 'outwear': 3,
 'pants': 4,
 'shirt': 5,
 'shoes': 6,
 'shorts': 7,
 'skirt': 8,
 't-shirt': 9}
接下来,我们将应用迁移学习。为此,我们必须首先提取基础层——换句话说,我们必须提取 Xception 模型的卷积层并确保它被冻结。这将确保我们能够访问超过 100 万张图像的图像数据的特征图。为此,我们将使用include_top=False参数,这将忽略密集层并返回卷积神经网络的最底层。接下来,我们将构建一个定制的密集层,其中突出显示之前提到的 10 个类别标签,并为我们用例训练密集层:
base_cnn_model = Xception(weights='imagenet', include_top=False, input_shape=(150,150,3))
base_cnn_model.trainable = False
接下来,我们将构建密集层的架构,并将其与基础层结合。首先,我们将定义输入标准,以便基础模型可以提供适合相同标准的向量。我们将使用(150,150,3),这样模型可以更快地训练,因为更多的像素可能会减慢训练过程。接下来,我们将从基础层转换向量到二维数组。为此,我们将使用一种称为池化的方法,这有助于减少特征图的空间大小,但仍然保留关于基础层的关键信息。之后,我们将创建一个密集层,其中指定输出数量,在本例中为 10,并应用 softmax 激活来返回概率。
在这一点上,我们可以定义损失函数,使用学习率为 0.005 的 Adam 优化器,并选择精度作为指标:
inputs = keras.Input(shape=(150,150,3))
base = base_cnn_model(inputs)
vectors = keras.layers.GlobalAveragePooling2D()(base)
inner = keras.layers.Dense(100, activation='relu')(vectors)
drop = keras.layers.Dropout(rate=0.2)(inner)
outputs = keras.layers.Dense(10, activation='softmax')(drop)
model = keras.Model(inputs, outputs)
learning_rate = 0.005
optimizer = keras.optimizers.Adam(learning_rate=learning_rate)
loss = keras.losses.CategoricalCrossentropy()
接下来,我们将使用优化器和损失函数来编译模型,以实现最佳精度:
model.compile(optimizer=optimizer, loss=loss, metrics=["accuracy"])
现在,我们将使用服装数据集来训练网络:
model.fit(train_ds, validation_data=validation_ds, epochs=10)
Epoch 1/10
97/97 [==============================] - 18s 156ms/step - loss: 1.1373 - accuracy: 0.6228 - val_loss: 0.7507 - val_accuracy: 0.7830
...
Epoch 10/10
97/97 [==============================] - 12s 118ms/step - loss: 0.1951 - accuracy: 0.9289 - val_loss: 0.7641 - val_accuracy: 0.7918
观察这些结果,很明显模型已经过拟合,因为模型在训练数据上达到了 92.7%的精度,而在验证数据上只有 81.52%的精度,在 10 个 epoch 结束时几乎相差 10-11%。
我们可以尝试使用不同的学习率来训练密集层,以获得一个更通用的模型。学习率控制我们希望模型从训练数据中学习多快,并调整其权重以适应数据。
低学习率就像以慢速观看视频,以确保覆盖大多数细节,而高学习率则像以较快的速度观看视频,可能会错过一些细节。优化的学习率对于成功的训练至关重要,但通常需要调整以在收敛速度和精度之间找到平衡。
另一种减少过拟合的方法是通过调整 dropout 率,这是一种正则化技术,其中随机省略了一定比例的数据。这两种调整都需要实验,并且更侧重于模型本身的方法。
采用以数据为中心的方法,我们希望测试更多的数据示例或更好的数据示例。我们需要那些模型不会试图记住特定像素的示例,这样如果它在第 130 个像素位置看到 120 个红色,它就开始相信这张图片是裤子。因此,为了确保模型具有良好的泛化能力,我们可以利用数据增强。
关于以数据为中心的人工智能,数据增强可以被视为一种通过将各种变换应用于现有数据来人为增加训练数据集大小和多样性的技术。这些变换可以包括旋转、缩放、裁剪、翻转、添加噪声等等,具体取决于增强的数据类型。
存在一些常见的增强技术,例如创建图像的不同角度,移动图像,放大和缩小图像,翻转图像,等等。然而,在应用增强技术之前,我们必须首先考虑数据将如何生成。例如,如果用户不会生成颠倒的图片,那么为了创建翻转的图片而增强图像可能只会增加噪声,而不是为模型提供良好的信号。对于以下示例,我们将应用缩放增强,其中图像将被稍微放大和缩小,一些偏移使得图像中的衣物会移向边缘,并应用垂直翻转,这样如果某些图像是使用镜子拍摄的,我们就可以为每个场景捕获额外的数据。我们将通过更新图像生成器函数,添加这些额外的参数,然后在最佳参数上训练模型来实现这一点。
我们将应用旋转范围为 10 度,剪切范围为 10 度,宽度和高度偏移范围为 0.2,缩放范围为 0.1,以及垂直翻转。为了实现这一点,我们将调整ImageDataGenerator函数,这样将在幕后创建更多训练数据的示例:
train_gen = ImageDataGenerator(preprocessing_function=preprocess_input,
                               rotation_range=10,
                               shear_range=10,
                               width_shift_range=0.2,
                               height_shift_range=0.2,
                               zoom_range=0.1,
                               vertical_flip=True)
train_ds = train_gen.flow_from_directory(directory='./clothing-dataset-small/train/', target_size=(150,150), batch_size=32, seed=42)
val_gen = ImageDataGenerator(preprocessing_function=preprocess_input)
validation_ds = val_gen.flow_from_directory(directory="./clothing-dataset-small/validation/", target_size=(150,150), batch_size=32, shuffle=False)
Found 3081 images belonging to 10 classes.
Found 341 images belonging to 10 classes.
我们还将创建一个函数,该函数将接受学习率作为输入,并返回模型及其参数:
def make_fashion_classification_model(learning_rate: float=0.1):
    """Function to create a dense custom model"""
    #### Base model ####
    inputs = keras.Input(shape=(150,150,3))
    base = base_cnn_model(inputs)
    vectors = keras.layers.GlobalAveragePooling2D()(base)
    #### Dense model ####
    outputs = keras.layers.Dense(10, activation='softmax')(vectors)
    model = keras.Model(inputs, outputs)
    #### Optimizing the model ####
    optimizer = keras.optimizers.Adam(learning_rate=learning_rate)
    loss = keras.losses.CategoricalCrossentropy()
    model.compile(optimizer=optimizer, loss=loss, metrics=["accuracy"])
    return model
接下来,我们将使用这个函数,并将0.005作为学习率传递,并添加 50 个 epoch,因为我们已经为增强生成了更多的数据:
model = make_fashion_classification_model(learning_rate=0.005)
model_run = model.fit(train_ds, validation_data=validation_ds, epochs=50)
Epoch 1/50
97/97 [==============================] - 28s 266ms/step - loss: 1.5281 - accuracy: 0.4920 - val_loss: 0.9664 - val_accuracy: 0.6686
...
Epoch 32/50
97/97 [==============================] - 25s 253ms/step - loss: 0.7445 - accuracy: 0.7491 - val_loss: 0.6161 - val_accuracy: 0.8123
模型的训练准确率有所降低,但模型的泛化能力更强,并没有过拟合。此外,请注意,最佳验证准确率是在第 32 个 epoch 时达到的。然而,很难确定模型将在哪个 epoch 达到最佳准确率。
为了实现这一点,我们可以进一步利用模型检查点功能,以确保只有在一个给定 epoch 达到至少 78%的验证准确率的模型将被创建和保存,并且只有当之前的最佳准确率被超越时,才会创建新的模型。然后我们可以使用这些保存的模型来评分测试数据。
在下一步中,我们将添加一个 0.2 的 dropout 率和一个 50 的内层。我们将更新训练网络的函数并添加检查点功能。一旦创建检查点,我们将将其作为回调添加到fit函数中。
我们鼓励您在使用检查点以确保最佳准确率的同时,利用学习率、dropout 率和内部层进行超参数化。
首先,我们将定义一个函数,该函数接受三个输入 – 学习率、内部层和 dropout 率:
def make_fashion_classification_model(learning_rate: float=0.001, inner_layer: int=50, drop_rate: float=0.2):
    """Function to create a dense custom model with learning rate, inner layer and dropout rate"""
    #### Base model ####
    inputs = keras.Input(shape=(150,150,3))
    base = base_cnn_model(inputs)
    vectors = keras.layers.GlobalAveragePooling2D()(base)
    #### Dense model layers ####
    inner = keras.layers.Dense(inner_layer, activation='relu')(vectors)
    drop = keras.layers.Dropout(rate=drop_rate)(inner)
    outputs = keras.layers.Dense(10, activation='softmax')(drop)
    model = keras.Model(inputs, outputs)
    #### Optimizing the model ####
    optimizer = keras.optimizers.Adam(learning_rate=learning_rate)
    loss = keras.losses.CategoricalCrossentropy()
    model.compile(optimizer=optimizer, loss=loss, metrics=["accuracy"])
    return model
接下来,我们将定义检查点。这是我们将保存所有模型的地方,这些模型在各个 epoch 中验证准确率达到了至少 78%。然后,我们将此检查点添加到拟合函数的回调中:
checkpoint_model = keras.callbacks.ModelCheckpoint(
    filepath="xception_v1_{epoch:02d}_{val_accuracy:.4f}.h5",
    monitor="val_accuracy",
    save_best_only=True,
    initial_value_threshold=0.8,
    mode="max")
model = make_fashion_classification_model()
model.fit(train_ds, validation_data=validation_ds, epochs=50, callbacks=[checkpoint_model])
Epoch 1/50
97/97 [==============================] - 28s 266ms/step - loss: 1.4822 - accuracy: 0.5138 - val_loss: 0.8920 - val_accuracy: 0.7155
...
Epoch 37/50
97/97 [==============================] - 24s 250ms/step - loss: 0.5789 - accuracy: 0.7916 - val_loss: 0.5809 - val_accuracy: 0.8123
现在最佳模型已经训练并保存,我们将导入保存的模型,对所有数据进行评分,并计算测试准确率:
model = keras.models.load_model('xception_v1_37_0.8123.h5')
test_gen = ImageDataGenerator(preprocessing_function=preprocess_input)
test_ds = test_gen.flow_from_directory(directory="./clothing-dataset-small/test/", target_size=(150,150), batch_size=32, shuffle=False, seed=42)
accuracy = model.evaluate(test_ds)[1]
print(f"accuracy on train data was 79.16, where as validation accuracy was 81.23, but test accuracy is {accuracy*100:.2f}")
Found 372 images belonging to 10 classes.
12/12 [==============================] - 2s 94ms/step - loss: 0.6317 - accuracy: 0.7715
accuracy on train data was 79.16, whereas validation accuracy was 81.23, but test accuracy is 77.15
我们用来预测测试数据的模型在训练中的准确率为 79.16%,在验证中的准确率为 81.23%。我们实现的测试准确率仅为 77.15%,这可以通过迭代改进 – 但超出了本例的范围 – 通过利用数据增强参数和模型中心参数的超参数调整,这在现实生活中是鼓励的。然而,由于计算和时间限制,这超出了本书的范围。
接下来,我们将提取目标标签并构建一个函数,以提供预测概率分数以及相关类别。首先,我们将加载图像并对其进行预处理,然后对其进行评分:
labels = [i for i in train_ds.class_indices.keys()]
def preprocess_image(path: str, target_size: tuple):
    """Function to preprocess image"""
    img = load_img(path=path, target_size=target_size)
    img_input = np.array([np.array(img)])
    preprocessed_image = preprocess_input(img_input)
    return preprocessed_image
def decode_predictions(pred):
    """Function to decode prediction"""
    result = {c: format(float(p), '.8f') for c, p in zip(labels, pred)}
    final_prediction = sorted(result.items(), key=lambda x:x[1],  reverse=True)[0]
    return final_prediction
接下来,我们将提取一条随机裤子图像并通过模型进行预测:
path = './clothing-dataset-small/train/pants/188eaa2d-1a69-49b1-a9fb-7b3789ac93b4.jpg'
load_img(path)
这将产生以下输出:

图 7.11 – 从服装数据集中随机选择的裤子对
接下来,我们将在评分之前加载和预处理图像。最后,我们将解码预测结果以提取最终的预测。为此,我们将利用我们之前创建的函数:
preprocessed_image = preprocess_image(path=path, target_size=(150,150))
preds = model.predict(preprocessed_image)
results = decode_predictions(preds[0])
results
1/1 [==============================] - 0s 26ms/step
('pants', '0.99980551')
根据模型,该图像有 99%的概率被分类为裤子,这相当准确。
在本节中,我们通过数据增强的数据中心技术,展示了如何泛化模型。我们相信,通过迭代数据增强参数,我们可以进一步提高模型的质量。一旦参数已经调整,我们建议实践者结合以模型为中心的技术,如正则化、学习率、内部层和 dropout 率,以进一步调整和改进模型。
我们现在将转向探索另一个依赖于非结构化数据的话题:用于文本和自然语言处理的合成数据。
自然语言处理
合成文本数据通常用于增加与现实生活中观察到的类似语义意义的单词和句子的深度和广度。
用于创建自然语言处理合成数据的最常见的增强技术包括用同义词替换单词、随机打乱句子中单词的位置,以及在句子中插入或删除单词。
例如,句子“我喜欢喝茶”可以转换为“我非常享受喝茶”,而不会失去陈述的上下文意义。这是一个同义词替换的例子,其中“喜欢”被替换为“非常享受”,而“喝茶”被替换为“消费。”
回译是另一种 NLP 技术,它涉及将一个句子的语言翻译成另一种语言,然后再翻译回原始语言。通常,这将生成具有相似语义意义的略微不同的句子结构,这使得它成为对抗过拟合并增加训练数据量的绝佳方式。
我们将通过一个简单的例子来说明如何使用Hugging Face Transformers进行回译——具体来说,是MarianMT语言模型套件。MarianMT 模型最初由 Jörg Tiedemann 创建,使用 Marian C++库进行快速训练和翻译,但现在通过 Hugging Face 的 Python 库套件提供。
在撰写本文时,该资源提供了 1,440 个 transformer 编码器-解码器模型,每个模型有六个层。这些模型支持基于芬兰赫尔辛基大学语言技术研究组开发的Helsinki-NLP框架的各种语言对。
在这个例子中,我们想将以下三个句子从英语翻译成西班牙语,然后使用相同的技巧将西班牙语句子回译成英语:
- 
那个男人怀疑地看着门
 - 
彼得认为他看起来非常酷
 - 
大多数人相当和善
 
目标是生成具有相同语义意义但措辞略有不同的句子,这样合成数据将为我们最终模型提供更多学习数据。
首先,我们将安装所需的 Python 库:
pip install transformers sentencepiece
pip install mosestokenizer sacremoses
然后,我们将从transformers库中导入MarianMTModel和MarianTokenizer包,并将我们的输入文本字符串定义为src_text:
from transformers import MarianMTModel, MarianTokenizer
src_text = ['The man glanced suspiciously at the door', 'Peter thought he looked very cool', 'Most individuals are rather nice']
现在,我们将使用Helsinki-NLP/opus-mt-en-es语言模型定义我们的translator模型,该模型将英语翻译成西班牙语。MarianTokenizer和MarianMTModel函数用于定义和执行我们的标记器和翻译模型。
最终输出存储为trans_out,然后用作我们回译模型的输入:
translator = 'Helsinki-NLP/opus-mt-en-es'
tokenizer = MarianTokenizer.from_pretrained(translator)
model = MarianMTModel.from_pretrained(translator)
translated = model.generate(**tokenizer(src_text, return_tensors="pt", padding=True))
trans_out = [tokenizer.decode(t, skip_special_tokens=True) for t in translated]
在这个基本示例中,我们简单地反向重复相同的建模练习,以产生原始输入句子的略微修改版本。我们使用‘Helsinki-NLP/opus-mt-es-en’将它们回译成英语:
back_translator = 'Helsinki-NLP/opus-mt-es-en'
tokenizer = MarianTokenizer.from_pretrained(back_translator)
model_es = MarianMTModel.from_pretrained(back_translator)
back_translated = model_es.generate(**tokenizer(trans_out, return_tensors="pt", padding=True))
[tokenizer.decode(t, skip_special_tokens=True) for t in back_translated]
下表显示了原始输入句子与模型的输出。生成的句子措辞略有不同,但总体上与原始句子具有相同的语义意义。为了将这些句子用于监督模型的训练数据集,它们必须继承其原始“父”句子的标签:
| 输入句子 | 回译 | 
|---|---|
| 男人怀疑地看着门 | 男人怀疑地看着门 | 
| 彼得认为他看起来很酷 | 彼得认为他看起来很棒 | 
| 大多数人相当和善 | 大多数人相当和善 | 
表 7.1 – 使用 Hugging Face Transformers 进行回译的示例
隐私保护
合成数据对于保护个人隐私和身份也非常有用。使用合成数据保护隐私的主要目的是在保持原始数据集的统计特性(接近)完整的同时,使数据集中的个人无法被识别。
由于合成数据允许在不透露私人或敏感信息的情况下共享信息,因此它是隐私保护的一个优秀选择。为了实现这一点,我们必须创建与原始数据相似但不包含任何可识别个人信息的数据。
使用合成数据允许组织在研究或其他目的下共享数据,而不损害个人的隐私。使用合成数据保护隐私的好处有很多——例如,你可以降低数据泄露的风险,因为数据中不包含任何个人或敏感信息。
随着全球数据隐私法规的日益增多,使用消费者数据进行分析时保护个人隐私已成为强制性的。合成数据可用于遵守隐私法规,例如欧盟的 GDPR 或美国的 HIPAA 隐私规则,这些规则为保护个人数据和防止未经同意共享设定了标准。
通常,合成数据是保护隐私的有用工具,因为它允许组织在不透露敏感或个人信息的情况下共享数据。它在管理机器学习中数据质量与个人隐私之间的权衡方面特别有用,使其成为以数据为中心的工具箱的组成部分。
例如,考虑一家银行,它希望使用敏感的客户数据进行分析活动,如客户流失建模、欺诈检测和信用评估。通常,使用客户数据进行这些活动会带来许多合规风险和必须管理的强制要求,以避免隐私泄露和监管机构的重罚。
通过拥有预先生成的合成数据集,业务各个部分的数据科学家可以快速且安全地构建模型,这些模型将产生与基于真实世界数据构建的模型相似的结果。通过使用适当构建的合成数据,组织避免了每次构建和投产新模型时都必须经历繁琐的合规和治理流程。
然而,这并不意味着使用隐私保护数据没有风险。例如,生成模型可能会过度拟合原始数据,并产生与原始数据过于接近的合成实例。
此外,尽管合成数据可能看起来是匿名的,但可能存在一些情况,复杂的黑客攻击可以揭示个人的身份。隐私保护合成数据的目标是限制这种情况发生的风险。
让我们探讨一些常见的隐私披露场景,以了解这些风险以及我们可能如何限制它们。
隐私披露的类型
为了进一步理解和欣赏合成数据在隐私保护方面的有用性,让我们看看可能发生的三种不同类型的隐私披露。这不是潜在披露事件的详尽列表,但它确实有助于理解使用合成数据进行此目的的潜力和局限性。
直接身份披露是最明显的隐私披露类型。这是指外部对手,如黑客,通过将个人的身份与私人信息的记录相匹配来获取信息。这种例子可以是将一个人的身份与医疗记录相匹配。
推断性身份披露是一种数据隐私泄露形式,其中某些个人信息可以从公开可用的数据中推导出来,而不需要明确揭示个人的身份。这种隐私泄露发生在攻击者通过分析数据集中的模式和相关性,使用统计分析推断有关个人的特征时。例如,攻击者可能能够通过分析特定特征与相应性别之间的模式来确定个人的性别。
按设计,完全合成数据使得直接身份披露几乎不可能。然而,攻击者可能通过分析合成数据集来推断有关特定人群的信息,尽管他们无法识别数据集中的个人。
例如,假设原始数据集包含敏感的医疗信息。该数据的合成版本保留了与原始数据相同的统计特性。使用基本的统计方法或更高级的机器学习模型,对手可以识别具有相似特征的人群,并推断他们患某种疾病的风险。然后,对手可以利用这些知识推断具有相同特征的个人风险,而无需进行任何直接的身份披露。
另一个推断性披露的例子是,攻击者可以根据某些行为,如购物习惯或信用卡使用模式,推断某人的财务状况或收入水平。此外,攻击者可能通过分析医疗保险索赔和其他相关记录来确定某人的医疗史。这可能导致严重后果,如歧视或利用敏感信息。因此,组织必须采取适当的措施来保护其数据免受推断性披露,以确保其不会被用于恶意目的。
成员推理攻击与推断性披露类似,但并不完全相同。它们不是基于具有相似特征的人群来推断个人的个人信息,而是旨在推断原始数据集中是否存在某个个体被用于创建合成数据集。这带来了巨大的隐私风险,例如,可能揭示某人患有某种疾病,而他们从未公开过他们的医疗信息。由于原始数据集的统计属性已被保留,因此通过合成数据预防这些攻击是困难的。
换句话说,合成数据是直接身份披露的有力武器,但并不完全消除身份披露的风险。让我们来看看为什么合成数据仍然优于传统的身份掩码技术。
我们为什么需要合成数据来保护隐私
传统数据去标识化技术依赖于两种主要方法:
- 
匿名化:这是去标识化的最简单形式,涉及删除包含直接标识符(客户 ID、姓名、地址)和准标识符(邮政编码、出生日期)以及其他敏感信息的列,这些信息可能被哈希处理、加密或屏蔽。随后,使用诸如 k-匿名性、l-多样性和 t-接近性等指标来验证给定数据集中隐私保护的水平。
 - 
差分隐私:差分隐私算法使用高斯和拉普拉斯等统计分布,向数据集中的标识特征添加随机生成的噪声。因此,个人的隐私将得到保护,因为标识信息被隐藏在噪声之后。
 
尽管这些技术降低了个人直接被识别的风险,但它们并不一定足以完全消除这种风险。
Rocher 等人于 2019 年进行的一项研究表明,基于马萨诸塞州人口样本的大小,99.98%的美国人可以通过不超过 15 个人口统计属性被重新识别。作者得出结论:“高度样本化的匿名化数据集不太可能满足 GDPR 设定的现代匿名化标准,并严重挑战了去标识化 发布并忘记模型的技术和法律充分性。”
另一项研究,由 Sweeney 在 2000 年进行,发现美国 87%的人口报告了基于 ZIP 代码、性别和出生日期的特征,这些特征可能使他们仅凭这些信息就能被识别。美国 53%的人口仅凭位置、性别和出生日期即可识别,其中“位置”是指个人居住的城市、镇或市镇。18%的人口可以根据他们的县、性别和出生日期的组合来识别。
换句话说,仅根据少数准标识符就有可能识别出独特的个体。通过使用合成的数据,我们可以从数据集中移除这些个体组合,同时保留数据的整体统计特性。
让我们来探讨一下这是如何实现的。
生成用于隐私保护的合成数据
当我们创建用于隐私保护的合成数据时,我们有三个目标:
- 
通过在合成数据集中反映其统计特性来保持原始数据的效用。
 - 
确保数据结构与原始数据相同。这意味着我们可以在合成数据上使用与原始数据相同的代码和工具,而无需进行任何更改。
 - 
使用隐私保护合成数据时,不应能够识别出原始数据集中哪些现实世界个体参与了其中。
 
值得注意的是,创建合成数据的方法有很多种。部分合成数据只是用合成数据替换了部分数据,而完全合成信息则是从头开始创建,没有任何原始数据。
根据采用的方法,完全合成信息可以在不牺牲太多可用性和便利性的情况下,提供更强的保障以防止个人身份泄露。
通过合成数据宝库(SDV)项目创建的工具开始练习合成数据生成是一个很好的方法。该项目最初由麻省理工学院的 Data to AI 实验室在 2016 年成立,是一个综合性的 Python 库生态系统,允许用户学习单表、多表和时间序列数据集,这些数据集随后可以用作生成合成数据的基础,这些合成数据能够复制原始数据的格式和统计特性。
多亏了这个项目,在训练机器学习模型时,可以轻松地补充、增强,甚至在某些情况下用合成数据替换真实数据。此外,它还使得机器学习模型或其他数据依赖型软件系统可以在不承担分享实际数据带来的风险的情况下进行测试。
SDV 套件由几个基于概率图模型和深度学习的技术组成。它们用于生成分层生成模型和递归采样算法,这些算法能够生成各种数据结构的合成版本。
我们将使用 SDV 套件中的两种不同技术 – GaussianCopula 和 CopulaGAN – 来说明如何为隐私保护目的生成合成数据。然后,我们将简要介绍如何使用指标和图表来衡量 质量 和 得分。
GaussianCopula
Copula 是一种用于测量随机变量之间依赖性的工具。GaussianCopula 是多个(即多元)正态分布数据片段的集合。作为一个整体,copula 允许我们通过展示集合中一个元素的变化如何影响其他元素来描述这些独立正态分布之间的关系 – 即它们的 边缘分布。这一点很重要,因为这项练习的目的是增强任何独特的变量组合,同时保留数据集的整体统计特性。
GaussianCopula Python 程序示例
现在,我们将编写一个示例程序来展示它是如何工作的。它将执行以下操作:
- 
加载一个样本数据集,然后计算
GaussianCopula模型。 - 
使用
GaussianCopula模型生成一些样本数据。 - 
可视化输出及其统计特性,以了解我们的模型表现如何。
 
首先,我们必须安装必要的 Python 包 – sdv 和 pandas:
pip install sdv
pip install pandas
对于这项练习,我们将使用公开可用的 Adult 数据集,也称为 Census Income 数据集。要开始,请从 archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.data 下载它。
我们将使用标准的 pandas 函数从前面的 URL 创建一个 DataFrame,df:
names = ['age', 'workclass', 'fnlwgt', 'education', 'education-num',
                 'marital-status', 'occupation', 'relationship', 'race', 'sex',
                 'capital-gain', 'capital-loss', 'hours-per-week',
                 'native-country', 'income']
url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.data'
df = pd.read_csv(url, header=None, names=names, na_values=['?', ' ?'])
这将输出以下 DataFrame:

图 7.12 – Adult 数据集的前五行 – 我们用于生成合成数据的输入数据集
我们的 DataFrame 创建完成后,我们将导入 SingleTableMetadata 类,这是一个提供管理单个数据表元数据的方法的类,例如列的名称和类型、列之间的关系等。SDV 的建模套件需要这个元数据对象作为输入。
然后,我们将使用 detect_from_dataframe() 方法分析 pandas df DataFrame,并自动检测和设置表的元数据。
最后,我们将从 SDV 加载适当的 API 和对象,并实例化 GaussianCopula 模型。然后,我们将使用 fit() 方法生成模型:
from sdv.metadata import SingleTableMetadata
metadata = SingleTableMetadata()
metadata.detect_from_dataframe(df)
from sdv.single_table import GaussianCopulaSynthesizer
gc_model = GaussianCopulaSynthesizer(metadata)
gc_model.fit(df)
通常,我们会从输入数据集中抽取一个比完整输入数据集小的样本来生成模型,但在这个案例中,我们将使用整个输入数据,因为它并不太大。
现在,让我们生成合成数据集:
gc_synthetic = gc_model.sample(num_rows=df.shape[0] )
一旦我们生成了合成数据,我们可以通过将其与真实数据的属性进行比较来评估其质量。这可以通过使用几个质量指标来完成。
让我们继续进行。
计算质量分数
为了衡量合成数据的质量,我们可以使用SDV包中的各种分数指标。分数的定义和解释取决于我们查看的指标。
让我们看看一些相关的指标,用于衡量原始和合成数据之间的统计相似性,以及推断攻击成功的风险。分数介于 0 到 1 之间。0 或 1 的解释取决于你使用的指标:
- 
BoundaryAdherence:这描述了合成数据是否位于真实数据某一列的最大和最小值范围内。1 表示是,0 表示否。 - 
StatisticSimilarity:这比较了真实和合成数据中某一列的平均值、中位数和标准差。 - 
CategoricalCAP:这是使用推断攻击泄露私人信息的风险——也就是说,黑客知道一些真实数据并能将其与合成数据匹配起来。分数为 1 表示风险很高。 - 
Data Likelihood:这计算数据与原始数据中的观察结果匹配的可能性。这与Detection指标类似,该指标询问机器学习模型能否区分原始数据集和伪造的数据集。 - 
KSComplement:这使用Kolmogorov-Smirnov(K-S)测试显示真实和合成数据的列形状是否相同。K-S 测试衡量两个数据集的累积分布函数(CDF)之间的最大距离。然而,它使用其补数(1 - KS 统计量)。 - 
MissingValueSimilarity:这衡量真实和合成数据集中缺失数据的比例。 
显示所有这些指标的代码几乎相同。只需调用适当的包,然后运行compute方法:
from sdmetrics.single_column import CategoryCoverage
CategoryCoverage.compute(
    real_data=df['workclass'],
    synthetic_data=synthetic['workclass']
)
这里是MissingValueSimilarity的一个示例:
from sdmetrics.single_column import MissingValueSimilarity
MissingValueSimilarity.compute(
    real_data=df['marital-status'],
    synthetic_data=synthetic['marital-status']
)
输出分数等于 1.0,这意味着模型已成功匹配合成数据集中缺失值的比例。
量化并可视化数据质量
我们还希望量化并可视化我们的合成数据与原始数据集相比的质量。为此,我们将使用 SDV 库中的诊断和质量报告。
诊断报告应始终产生 100%的分数,这告诉我们主键是唯一的且非空的,合成数据中的连续值遵循原始数据中的最小/最大范围,离散值与真实和合成数据中的相同类别对齐,并且列名相同:
from sdv.evaluation.single_table import run_diagnostic
diagnostic = run_diagnostic(
    real_data=df,
    synthetic_data=gc_synthetic,
    metadata=metadata
)
这里是我们的输出:
Overall Score: 100.0%
Properties:
- Data Validity: 100.0%
- Data Structure: 100.0%
SDV 质量报告评估你的合成数据捕捉我们原始真实数据的数学特性的程度。它是通过一系列衡量两个数据集之间保真度的各个方面来做到这一点的。数据保真度指的是数据集在表示其源特征方面的准确性。
报告提供了结果的概述,以及每个指标的详细可视化说明,以便您可以快速了解合成数据的优缺点。通过了解您的合成数据如何捕捉真实数据的数学属性,您可以在需要时采取措施来改进它。
SDMetrics 质量报告是一个非常有价值的工具,可以帮助您确保您的合成数据尽可能准确和可靠。以下是我们可以如何使用它:
from sdv.evaluation.single_table import evaluate_quality
quality_report = evaluate_quality(
    real_data=df,
    synthetic_data=gc_synthetic,
    metadata=metadata
)
上述代码生成一个包含各种指标和可视化的质量报告,显示了原始数据和合成数据之间的整体相似性:
Overall Score: 84.6%
Properties:
- Column Shapes: 87.57%
- Column Pair Trends: 81.63%
这里有一些重要的指标需要了解:
- 
列形状:一列的形状告诉我们数据是如何分布的。得分越高,表示实际数据和合成数据越相似。为每一列计算一个单独的列形状得分,但最终得分是所有列的平均值。 - 
列对趋势:两个列之间的相关性表明它们的趋势如何相互比较;得分越高,这些趋势越相似。为数据中的每一对列生成一个得分,而最终得分是所有列的平均值。这是一个重要的得分,告诉我们我们的合成数据是否捕捉到了原始数据集中变量之间的关系。 
我们还可以使用get_visualization命令来可视化这些指标维度:
fig = quality_report.get_visualization(property_name='Column Pair Trends')
fig.show()
这将生成以下图表:

图 7.13 – 比较原始和合成数据集列对趋势的相关矩阵
如您所见,大多数列对具有很高的相似度得分,但“资本收益”列却相距甚远。我们可以使用以下代码来并排可视化实际和合成的“资本收益”列分布:
from sdv.evaluation.single_table import get_column_plot
fig = get_column_plot(
    real_data=df,
    synthetic_data=gc_synthetic,
    metadata=metadata,
    column_name='capital-gain'
)
fig.show()
输出如下生成:

图 7.14 – 实际和合成资本收益列分布的比较。高斯 Copula 模型在匹配分布方面做得并不好
在这种情况下,我们会测试各种列分布函数,以找到更适合这个特定列的匹配。在这个例子中,我们使用了GaussianCopula函数来创建合成数据集。然而,SDV 库包含其他几个可能对您的原始数据集特征有用的分布。让我们探索如何更改默认分布。
列分布函数的变化
GaussianCopula函数确定哪个统计分布最能描述每个 Copula,但它并不总是正确。幸运的是,我们可以覆盖预选择并选择我们首选的分布。
我们有以下选择:
- 
高斯(正态)分布:如果你的数据是连续的并且围绕均值对称分布,请使用此分布。它通常用于自然发生的数据,例如人口的高度或体重。
 - 
Gamma 分布:用于仅正数的偏斜数据。它通常用于等待时间或服务时间等。
 - 
Beta 分布:用于介于 0 和 1 之间的变量,例如比例或概率。
 - 
学生 t 分布:这与高斯分布类似,但尾部更重。当样本量小或标准差未知时,通常使用此分布。
 - 
高斯核密度估计(KDE):用于非参数数据 – 即当你不知道或不想假设特定分布时。KDE 使用数据本身来估计其分布。
 - 
截断高斯分布:当你的数据遵循高斯分布但被限制在特定范围内时,请使用此分布。
 
首先,这是如何显示它计算出的分布的方法:
gc_model.get_learned_distributions()
此语句生成一个详细列出了数据集中所有列的列表。而不是在这里显示输出,模型默认为所有列使用 Beta 分布。
要更改给定列的分布函数,只需再次创建一个模型,但这次明确地将特定分布应用于该列。在这种情况下,我们将对capital-gain列应用伽马分布:
gc_model2 = GaussianCopulaSynthesizer(
    metadata,
    numerical_distributions={
        'capital-gain': 'gamma',
    })
gc_model2.fit(df)
结果输出是一个新的合成数据集,其capital-gain列的分布与真实数据非常接近:

图 7.15 – 对资本收益列的新比较。在此列上使用伽马分布提高了合成数据和原始数据之间的相似性
SDV 库中另一个有用的包是CopulaGAN。此算法是GaussianCopula和CTGAN算法的结合。让我们比较CopulaGAN和GaussianCopula在Adult数据集上的性能。
CopulaGAN 代码示例
CopulaGAN是GaussianCopula的一种变体,可以使用简化的 GAN 模型产生更好的结果。我们将在本节中比较这两个模型,但首先,这是使用相同的输入数据集和元数据对象生成CopulaGAN的代码:
from sdv.single_table import CopulaGANSynthesizer
cg_model = CopulaGANSynthesizer(metadata)
cg_model.fit(df)
从 CopulaGAN 测量数据质量
现在,让我们看看关于CopulaGAN模型的数据质量,重复使用我们在GaussianCopula中使用的某些技术:
quality_report = evaluate_quality(
    real_data=df,
    synthetic_data=cg_synthetic,
    metadata=metadata
)
Overall Score: 87.39%
Properties:
- Column Shapes: 91.75%
- Column Pair Trends: 83.04%
图 7.16提供了原始和合成数据集列对趋势的视觉表示:

图 7.16 – 使用 CopulaGAN 对 Adult 数据集的列对趋势
理解 GaussianCopula 和 CopulaGAN 之间的区别
CopulaGAN 是一种混合 AI 模型,它结合了高斯 Copula 的人性化易用性和 GANs 的鲁棒准确性。
GAN 是一种深度学习算法。如果您与神经网络合作过,您知道它们是一种黑盒,这意味着网络中节点的系数是函数而不是数字。与多项式或线性模型相比,它们很难解释或理解。
GaussianCopula 更容易解释。它通过尝试不同的已知统计分布(正态分布、威布尔分布等),这对于已知或容易观察到的分布非常有用。然后,对于每一列,它选择最接近的一个。
SDV 项目背后的团队开发了 CopulaGAN 以获得两者的最佳效果:一个更准确的模型,同时仍然可解释。
下表比较了前例中我们两个模型的结果。CopulaGAN 由于能够更好地匹配列形状,因此实现了更高的整体质量评分:
| 高斯 Copula | CopulaGAN | |
|---|---|---|
| 整体质量评分 | 84.6% | 87.39% | 
| 列形状 | 87.57% | 91.75% | 
| 列对趋势 | 81.63% | 83.04% | 
表 7.2 – GaussianCopula 和 CopulaGAN 算法在 Adult 数据集上的数据质量比较
当然,质量是一个高度复杂的话题,我们的例子在这方面并不全面。您需要查看所有列和所有不同类型的评分,以验证合成数据的准确性。换句话说,如果没有对数据集中的变量进行更深入的审查,就不能断定CopulaGAN在所有情况下都更准确。这在处理高风险数据集和用例时尤为重要。
另一个需要考虑的额外指标是运行速度。据我们所知,当我们编写这个例子时,CopulaGAN 需要 1 小时才能完成,而 GaussianCopula 需要 15 秒才能完成。
验证我们新数据集的隐私性
现在我们已经为我们用例构建了一个合成数据集,我们需要确保我们已经阻止了从原始数据集中重新识别个人的能力。
要了解个人可以被重新识别的可能性,我们需要一个准确衡量原始和合成记录之间差异或“距离”的度量。两者之间的距离越远,它们被识别为同一实体的可能性就越小。如果我们讨论的是表格形式的个人信息,我们需要一种方法来衡量定性和定量属性之间的距离。
为了准确测量包含定性和定量信息的数据集中两行之间的接近程度,我们可以利用一个称为 Gower 距离的相似系数。
Gower 距离是一种独特的距离度量,与距离度量不同。它在计算具有数值和分类值的两个实体之间的差异方面脱颖而出。这很重要,因为许多常见的聚类算法,如 K-means 聚类,只有在所有变量都是数值时才工作。
Gower 距离返回一个介于 0(表示相同的观测值)和 1(表示它们处于最大距离)之间的相似系数。
假设我们在原始(o)和合成(s)数据集中有一组p个特征:
- 
对于有序数,一个特征到另一个特征的距离只是它们差异的绝对值除以该变量的范围。我们除以范围以归一化数据,这样大数就不会比小数得到更大的权重。
 - 
将分类变量转换为数字,以便我们可以进行数学运算。公式很简单 – 如果这些值相同,它们的距离是 0;否则,它是 1。
 
Gower 距离是距离的总和除以特征的数量 – 项的平均值。由于我们把这些差异除以特征的数量,所以这相当于说 Gower 距离是平均距离。
然后,我们为接近度下定义,并称之为最近记录的距离。对于s中的每个元素,o中最接近的行是具有最小 Gower 距离的行。距离为 0 表示两行数据相同,而距离为 1 表示在所使用的观测值中,两行尽可能不同。
让我们练习使用Gower Python 包应用 Gower 距离。
Gower 距离 Python 示例
在我们的 Gower 距离实践示例中,我们将使用Adult数据集,并将CopulaGAN生成的合成输出与原始数据进行比较。我们建议使用Adult数据集的小子集(例如,1,000 行)进行练习,因为 Gower 矩阵的计算在较大的数据集上可能需要很长时间。
首先,我们将根据现有的df DataFrame 中的前 1,000 行创建我们的模型 DataFrame,该 DataFrame 包含完整的Adult数据集。然后,我们将在这个数据集上拟合一个GaussianCopula模型,并生成一个新的合成数据集,称为synthetic:
new_df = df.head(1000)
model = GaussianCopulaSynthesizer(metadata)
model.fit(new_df)
synthetic = model.sample(num_rows=df.shape[0] )
接下来,我们将安装Gower包并计算两个数据集之间的 Gower 距离矩阵:
pip install gower
import gower
gowerMatrix=gower.gower_matrix(new_df, synthetic)
print(gowerMatrix)
这将生成类似以下的结果,其中计算了数据集中每行之间的距离:

图 7.17 – 结果的 gowerMatrix
现在,我们将使用gower_topn()函数来找到最接近的n(在这种情况下,10)行。
为了确保合成数据集通过我们的测试,我们必须确保其没有任何值等于 0;否则,这表明合成数据中的某些行与原始数据中的行相似。一般来说,我们希望最高值与 0 的距离足够远,这样可以降低重新识别的风险:
gower.gower_topn(df.iloc[:,], synthetic.iloc[:,], n = 10)
结果是前 10 个最近行及其 Gower 距离的索引。在这种情况下,我们数据集中两行之间的最小距离是 0.02205,这意味着我们的合成数据集在单个行级别上与原始数据集差异不足:
{'index': array([26320, 29200, 18735, 24149, 18316, 22925,  4836, 15360, 42, 3523]),'values': array([0.02205753, 0.02578343, 0.03649067, 0.0374441 , 0.03785798, 0.04503146, 0.06345809, 0.08126822, 0.08237292, 0.08368524], dtype=float32)}
在这种情况下,我们需要做更多的工作来减少集合之间的相似性。以下是一些你可以用来实现这一目标的技巧:
- 
在 SDV 目录中测试
GaussianCopula或其他合成器,例如 CTGAN 或CopulaGAN。 - 
添加噪声:你可以在合成数据中添加随机噪声。这将使合成数据与原始数据相比更加“独特”。
 - 
执行特征转换:将某种类型的转换(例如,对数、平方根、指数等)应用于合成数据集中的特征。
 - 
执行数据增强:生成新的合成数据点,这些数据点不是真实数据的直接副本。你可以通过使用合成少数过采样技术(SMOTE)或自适应合成采样(ADASYN)等技术来实现,这两者我们将在本章后面讨论。
 
记住,虽然你的目标是减少相似性,但你同样希望合成数据是有用且能代表真实数据的。如果你使合成数据过于不同,它可能无法达到预期的目的。
作为旁注,Gower 距离也可以用来找到彼此非常相似的数据行,这在创建类似受众、聚类或识别风险群体等任务中很有用。
例如,假设你刚刚对一组客户进行了一次非常成功的电子邮件营销活动,并且你希望将活动扩展到看起来像原始目标组中的客户。
要这样做,只需计算原始目标组中的客户与整个客户群其他客户的 Gower 距离,并根据最低的 Gower 距离选择一个目标组。
使用合成数据提高模型性能
在第五章**数据清洗技巧和第六章**机器学习中程序化标注技巧中,我们讨论了通过提高数据质量来提高模型性能。然而,有时提高数据质量可能不足以解决问题,尤其是在数据集较小的情况下。在这种情况下,我们可以利用生成合成数据来提高模型性能。
正如我们在本章前面所讨论的,合成数据可以帮助生成更多的训练示例,并通过提供更多不同变异和数据分布的示例来泛化模型的性能。这两种用途都可以使模型更稳健,并减少模型对训练数据的过拟合风险。
在不平衡数据集中,模型会偏向多数类,因为一个类别的示例比另一个类别的多。这是贷款预测数据集的问题,其中 30% 的数据属于少数类。
在本节中,我们将介绍为少数类生成合成数据,以便模型可以进一步泛化,并且模型性能指标可以提升。我们将继续使用决策树模型,并使用合成数据生成来进一步提高数据的信号强度。我们将使用 Python 的 imblearn 库来生成合成数据。
让我们导入库和两种过采样方法,SMOTE 和 ADASYN,以过采样少数类。我们还将利用 Counter 方法来统计在合成数据生成前后的数据样本:
import imblearn
from imblearn.over_sampling import SMOTE, ADASYN
from collections import Counter
print(imblearn.__version__)
0.10.1
SMOTE 和 ADASYN 算法都用于生成合成数据。然而,ADASYN 更稳健,因为它考虑了点的密度来生成合成数据。SMOTE 可能会在少数类周围生成合成数据,但它这样做是均匀的,不考虑数据点的稀有程度。
SMOTE 通过随机选择少数类样本对并围绕现有样本插值新样本来创建合成样本。这种技术进一步扩展到空间中,以增加少数类样本的数量。然而,由于样本是随机选择的,因此没有对稀有样本点进行加权。
另一方面,ADASYN 通过计算少数类样本的密度分布来考虑特征空间中的稀有数据点。它在特征空间密度低的区域生成合成样本,以确保在数据集最需要的地方生成合成样本以平衡数据集。ADASYN 使用 k 近邻算法来估计少数类样本的密度分布。对于每个少数类样本,ADASYN 根据属于少数类的 k 近邻点的数量来计算密度。k 的值是一个用户定义的参数,通常设置为较小的值,如 5 到 10。密度是 k 个最近点平均距离。平均距离越高,密度越低,反之亦然。
我们遍历算法参数的不同阈值,例如最近邻的数量和数据过采样的百分比。这有助于我们找到最佳参数,以生成最佳样本数量,从而使测试 ROC 和测试准确率得到最大提升。然后,我们将这些结果组合到一个 DataFrame 中,并选择最佳参数。这样做是为了衡量使用合成数据生成技术的模型性能。
对于我们的示例,我们只会使用 ADASYN,但我们鼓励你尝试不同的技术,包括 SMOTE,来解决当前的问题:
results = []
over_sampling = [0.65,0.7, 0.75, 0.8, 'auto']
n_neighbours = [1,3,5,7,9,10]
for os in over_sampling:
    for k in n_neighbours:
        oversample = ADASYN(random_state=1, sampling_strategy=os, n_neighbors=k)
        counter = Counter(y_train)
        print(f"data size before applying smote technique is {counter}")
        X_train_synthetic, y_train_synthetic = oversample.fit_resample(X_train_transformed, y_train)
        counter = Counter(y_train_synthetic)
        print(f"data size after applying smote technique is {counter}")
        model, test_predictions, train_roc, test_roc, train_acc, test_acc = train_custom_classifier(
        X_train=X_train_synthetic,
        y_train=y_train_synthetic,
        X_test=X_test_transformed,
        y_test=y_test,
        clf=d_clf,
        params=d_param_grid)
        results.append((os, k, train_roc, test_roc, train_acc, test_acc))
synthetic_df = pd.DataFrame(columns=['os_strategy', "n_neighbours", "train_roc", "test_roc", "train_acc", "test_acc"], data=results)
接下来,我们必须生成一个包含我们生成的模型参数组合和模型性能指标的 DataFrame,并按测试准确率对其进行排序。DataFrame 表明,对于少数派到多数派的类别,采用 75%的过采样策略,以及 7 个最近邻值,将提供最佳准确率和 ROC 分数:

图 7.18 – 输出 DataFrame
现在,我们必须应用我们最高效的过采样策略的参数,并重新训练决策树模型:
counter = Counter(y_train)
print(f"data size before applying smote technique {tech_name} is {counter}")
# transform the dataset
oversample = ADASYN(random_state=1, n_neighbors=7, sampling_strategy=0.75)
X_train_synthetic, y_train_synthetic = oversample.fit_resample(X_train_transformed, y_train)
counter = Counter(y_train_synthetic)
print(f"data size after applying smote technique {tech_name} is {counter}")
model, test_predictions, train_roc, test_roc, train_acc, test_acc = train_custom_classifier(
X_train=X_train_synthetic,
y_train=y_train_synthetic,
X_test=X_test_transformed,
y_test=y_test,
clf=d_clf,
params=d_param_grid)
data size before applying smote technique adasyn is Counter({1: 379, 0: 173})
data size after applying smote technique adasyn is Counter({1: 379, 0: 321})
Decision tree optimised
Getting the best params which are {'class_weight': 'balanced', 'criterion': 'entropy', 'max_depth': 6, 'max_features': 'sqrt', 'min_samples_leaf': 3, 'min_samples_split': 30, 'random_state': 1}
Training roc is 0.8816528164788466, and testing roc is 0.8629130966952264
             training accuracy is 0.8371428571428572, testing_acc as 0.8387096774193549
通过合成数据生成,ADASYN 将少数派类别的样本数量从 173 增加到 321,将测试准确率提升到 83.8%。这几乎提高了 2%的准确率。ROC 分数也提升到 86.2%,进一步增加了 4.4%。
这些结果表明,合成数据生成可以在模型性能上提供显著的提升,即使是对于小数据集也是如此。然而,重要的是要注意,这并不总是如此,特别是如果错误分析表明添加新数据不会提高模型性能的话。在这种情况下,你可能会在转向合成数据生成之前,收集更多数据或特征,甚至进行特征工程。
你应该在什么情况下使用合成数据?
到目前为止,我们已经确定合成数据可以用于多个目的,但你是如何决定是否在你的项目中使用合成数据的呢?
对于寻求通过创新或非常规方法在竞争对手中取得优势的企业,合成数据在实验和现实之间提供了一个可访问的中间地带。对于想要从其庞大的人口数据存储中学习的政府机构,合成数据允许分析高度敏感的数据集,而不会损害个人隐私。
实验和探索你数据(合成或真实)的边界可能非常有价值,但引入合成数据的收益应该始终与使用相同数据进行有害预测的成本和风险进行评估。
核心问题是,“实验的可接受成本是什么?”,尤其是如果它包括人类附带损害或声誉或财务损失的话。
在我们看来,当获取真实世界数据可能困难、昂贵或不道德时,应该使用合成数据。合成数据最常见和实用的用例是保护个人隐私,以及创建在传统测试环境中非常困难或不可能进行的模拟。对于这些用例,使用合成数据的益处更有可能超过风险,但这并不是保证,所以请确保适当管理风险。
需要缓解的主要风险是偏见的持续和加剧。机器学习模型天生容易过拟合和找到数据中的“最容易”路径,因此合成数据集应该经过严格测试,以确保它们适合用途。
合成数据也可以加速测试和训练机器学习模型的过程,从而在开发和部署周期中为公司节省时间和金钱。此外,合成数据是创建在传统测试环境中不可能进行的模拟的有用工具。
请记住,使用合成数据通常是构建模型或提高预测准确性的许多途径之一。它只应在了解并适当管理潜在风险和对受影响者的潜在影响时使用。另一方面,如果您可以缓解这种风险——在某些情况下,甚至完全避免“现实世界”的风险——那么它将是您工具箱中的一个奇妙工具。
摘要
在本章中,我们提供了合成数据及其常见用途的入门介绍。合成数据是数据为中心工具包的关键部分,因为它为我们提供了另一个途径来获得更好的输入数据,尤其是在收集新数据不可行时。
到现在为止,您应该已经清楚地理解了合成数据的基本原理及其潜在应用。合成数据通常用于计算机视觉、自然语言处理和隐私保护应用。然而,合成数据的潜力远远超出了这三个领域。
已经有整本书籍专门讨论合成数据这一主题,我们建议如果您想成为合成数据生成的真正专家,应该深入研究这一领域。
在下一章中,我们将探讨另一种无需收集新数据即可改进数据的有力技术:程序化标注。
参考文献
- 
datagen.tech/guides/synthetic-data/synthetic-data,于 2022 年 11 月 12 日查看 - 
unity.com/our-company,于 2022 年 11 月 15 日查看 - 
https://venturebeat.com/ai/unitys-danny-lange-explains-why-synthetic-data-is-better-than-the-real-thing-at-transform-2021-2/, 查阅于 2022 年 11 月 15 日
 - 
Alcorn, M A 等人 2019, 摆出奇怪姿势:神经网络容易被熟悉物体的奇怪姿势欺骗,查阅于 2022 年 11 月 13 日:
arxiv.org/pdf/1811.11553.pdf - 
www.tesla.com/VehicleSafetyReport, 查阅于 2022 年 11 月 13 日 - 
Karras T, Aila T, Laine S, Lethtinen J, 2017, 渐进式增长 GANs 以提高质量、稳定性和 多样性:
arxiv.org/abs/1710.10196 - 
Karras T, Aila T, Laine S 2018, 生成对抗网络的风格化生成器架构:
arxiv.org/pdf/1812.04948.pdf - 
Metz L, Poole B, Pfau D, Sohl-Dickstein J 2017, 展开生成对抗网络,ICLR 2017:
arxiv.org/pdf/1611.02163.pdf - 
Jain N, Olmo A, Sengupta S, Manikonda L, Kambhampati S, 2021, Imperfect ImaGANation:GANs 加剧面部数据偏差的影响,ICLR 2021 工作坊——合成数据生成——质量、隐私、偏差:
arxiv.org/pdf/2001.09528.pdf - 
Rocher L, Hendrickx J M, de Montjoye Y A 2019, 使用生成模型估计不完整数据集中重新识别的成功率:
www.nature.com/articles/s41467-019-10933-3 - 
L. Sweeney, 简单的统计数据通常能唯一识别个人,卡内基梅隆大学,数据隐私工作论文 3. 匹兹堡 2000:
dataprivacylab.org/projects/identifiability/paper1.pdf - 
mobile.twitter.com/sdv_dev/status/1519747462088507393, 查阅于 2023 年 1 月 25 日 
第八章:识别和消除偏差的技术
在以数据为中心的机器学习领域,追求无偏差和公平的模型至关重要。偏差算法的后果可能从性能不佳到道德上有疑问的决定。重要的是要认识到,偏差可以在机器学习管道的两个关键阶段显现:数据和模型。虽然以模型为中心的方法近年来受到了广泛关注,但本章将重点介绍通常被忽视的同样重要的以数据为中心的策略。
在本章中,我们将探讨机器学习中偏差的复杂性,强调数据中心性是偏差缓解的基本方面。我们将探讨来自金融、人力资源和医疗保健等领域的真实世界案例,其中未能解决偏差已经或可能产生深远的影响。
在本章中,我们将涵盖以下主题:
- 
偏差难题
 - 
偏差类型
 - 
数据中心性必要性
 - 
案例研究
 
偏差难题
机器学习中的偏差并非一个新问题。它深深植根于我们收集的数据和我们设计的算法中。偏差可能源于历史差异、社会偏见,甚至在数据收集和标注过程中人类所做的决策。忽视偏差,或者仅仅通过以模型为中心的技术来处理偏差,可能会导致不良后果。
考虑以下场景,这些场景说明了偏差的多面性:
- 
金融中的偏差:在金融领域,机器学习模型在信用评分、欺诈检测和投资建议中发挥着关键作用。然而,如果历史贷款实践偏向某些人口群体而忽视其他群体,这些偏差可能会渗透到用于训练模型的数据库中。结果,边缘化社区可能会面临不公平的贷款实践,加剧社会经济不平等。
 - 
人力资源中的偏差:人工智能在人力资源中的应用在招聘、员工绩效评估甚至薪资谈判方面势头强劲。如果职位发布或历史招聘数据偏向特定性别、种族或背景,AI 系统可能会无意中延续歧视,导致工作场所缺乏多样性和包容性。
 - 
医疗保健中的偏差:在医疗保健领域,诊断算法被用于疾病检测和治疗建议。如果训练数据主要代表某些人口群体,来自代表性不足群体的个人可能会接受不理想的护理或面临延迟诊断。其影响可能是改变一生的,强调了公平医疗保健 AI 的必要性。
 
现在我们已经涵盖了可能产生偏差的领域,在下一节中,我们将探讨机器学习中普遍存在的偏差类型。
偏差类型
在机器学习中,通常有五种需要关注的偏差类别。尽管提供的列表并不全面,但这些类别代表了最普遍的偏差类型,每种类型都可以进一步细分。
容易识别的偏差
一些类型的偏差可以通过主动监控和分析轻松识别。以下是一些例子。
报告偏差
这种偏差发生在数据生产者、数据标注者或数据收集者遗漏了重要元素时,导致数据不能代表现实世界。例如,一家医疗保健企业可能对病人对健康计划的看法感兴趣;然而,数据标注者可能会决定专注于负面和正面情绪,而中性的情绪可能被不足代表。在这样数据上训练的模型擅长识别正面和负面情绪,但可能无法准确预测中性情绪。这种偏差可以通过主动监控来识别,其中对实时数据的预测与对训练数据的预测存在偏差。为了减少报告偏差,在机器学习系统设计初期明确所需的数据点非常重要。同样重要的是确保用于训练的数据代表真实世界数据。
自动化偏差
这种偏差发生在依赖于自动化的数据收集方式,并假设数据捕获不会出错的情况下。随着人工智能的日益完善,对人类的依赖显著减少,因此通常假设如果实施自动化系统,那么它将神奇地解决所有问题。使用主动监控可以帮助识别这种类型的偏差,其中模型在真实数据上的准确性非常低。另一种识别方法是通过让人类标注标签并衡量人类性能与算法性能。如*第六章**,机器学习系统中程序化标注技术的失败可能导致数据丢失或不准确。人工智能的好坏取决于其训练数据。数据中心化的一个关键原则是让人类参与其中;因此,在构建自动化系统时,我们应该确保生成数据代表真实世界场景,并且数据多样化,而不是过度或不足代表。
选择偏差
这种偏差发生在用于训练模型的所选数据不能代表真实世界数据时。这种偏差可以有多种形式:
- 
覆盖偏差:这种偏差可能发生在数据不是以代表性的方式收集时。这可能发生在企业和从业者专注于结果,而忽略了那些对结果没有贡献的数据点。在医疗保健领域,保险公司可能想要预测医院的入院人数;然而,关于那些在保险公司频繁更换并使用竞争性保险产品的人,或者那些没有申请福利而进入医院的人的数据可能并不容易获得,因此,这些人群在训练数据中可能没有得到很好的代表。
 - 
参与偏差:这种偏差可能由于参与者选择退出数据收集过程,导致某一群体在另一群体中过度代表。例如,一个模型被训练用来根据调查数据预测流失率,其中 80%已经转移到新竞争对手的人不太可能回应调查,他们的数据在样本中高度代表性不足。
 - 
抽样偏差:这种偏差可能发生在数据收集者没有在数据收集过程中使用适当的随机化方法时。例如,一个模型被训练用来根据调查数据预测健康分数;调查者没有随机地针对人口,而是选择了 80%高度关注健康并且更有可能回应的人,与那些不太可能回应的其他受访者相比。在健康产业中,那些更关注健康的人可能比那些不太关注健康的人有更好的健康分数,这可能导致模型偏向于健康人群。
 
选择偏差难以识别;然而,如果数据中频繁出现漂移,并且频繁重新训练以确保模型质量不下降,那么就是调查和检查所捕获的数据是否代表真实世界数据的好时机。回归模型中的两种分析方法可以帮助识别这种偏差。一种是进行双变量分析,其中敏感变量可以表示在x轴上,目标变量可以放在y轴上。如果两个变量之间存在强烈的关联,那么在训练时间和评分后时间评估关联指标差异是很重要的。如果差异显著,那么用于训练的数据很可能不代表真实生活。第二种技术是通过比较数据未完全代表和完全代表时的可能结果来进行多元分析。这可以通过将子群体分为训练时包含的数据点和排除的数据点来完成。我们可以通过创建一个独立变量组来运行多回归模型,将组 1 标记为包含的数据,组 2 标记为未包含的数据。然后我们将这个新变量作为特征添加到模型训练中,并比较组 1 和组 2 之间是否存在显著的差异。如果存在差异,那么数据收集存在偏差。
在分类示例中,我们可以通过查看敏感子群体中的假阳性率和/或假阴性率来观察这些值是否差异很大。如果差异很大,数据很可能偏向于一个或几个子群体。另一个可以用来检查偏差是否持续存在的指标是人口统计学上的平等性,它是对从一个子群体到另一个子群体选择可能性的概率比较。如果高选择子群体与低选择子群体之间的概率比率低于 0.8,那么数据很可能存在偏差,并且代表性样本不足。建议检查多个指标以了解数据及算法中的偏差。
为了处理这类偏差,建议在收集数据时采用分层抽样等技巧,以确保数据集中不同群体按比例代表。现在我们已经介绍了易于识别的偏差类型,在下一节中,我们将讨论一些难以识别的偏差类型。
难以识别的偏差
一些类型的偏见可能具有挑战性,因为它们是个人可能没有意识到的偏见。这些偏见通常在潜意识层面运作,并可能影响感知、态度和行为。为了捕捉这些偏见,组织和个人需要流程和培训来确保这些偏见不会存在于工作场所。一旦确定数据收集过程或数据标注过程中存在偏见,就可以定义敏感标签来衡量和检查模型是否没有偏见,或者模型中是否存在可接受的偏见水平。以下将描述一些这些偏见。
组别归因偏见
这种类型的偏见发生在对整个数据进行归因时基于某些数据点。这通常发生在数据创建者对数据中存在的属性类型有先入为主的偏见时。这种偏见可以采取两种形式:
- 
群体偏见: 这是一种先入为主的偏见,其中相关数据点与数据创建者产生共鸣,因此这些数据点获得有利的结果——例如,如果一位数据工程经理在设计简历筛选算法时,他们认为完成 Udacity 纳米学位的人符合该职位要求。
 - 
群体同质性偏见: 这是一种先入为主的偏见,其中数据点与数据创建者不产生共鸣,因此这些数据点获得不利的结果——例如,如果一位数据工程经理在设计简历筛选算法时,他们认为没有完成 Udacity 纳米学位的人不符合该职位要求。
 
让我们继续讨论另一种难以识别的偏见。
隐性偏见
这种类型的偏见发生在数据创建者根据自己的心理模型和个人经验对数据进行假设时。例如,在航空食品服务评论数据上训练的情感分析模型可能会将“ okay”这个词与中性情感联系起来。然而,世界上的一些地区使用“ okay”这个词来表示积极情感。
机器学习中的偏见可以采取多种形式;因此,我们将这些偏见分为两大类,易于识别的偏见和难以识别的偏见。从业者通常采用以模型为中心的方法来处理这些偏见,其中修改算法或使用偏见友好型算法已被视为可接受的做法。在下一节中,我们将从以模型为中心的方法转向另一种观点:以数据为中心的方法。
数据中心化命令
解决机器学习中的偏见需要一种全面的方法,其中以数据为中心的策略补充以模型为中心的技术。数据中心化涉及采取主动措施来编辑、清理和增强数据集本身,从而最大限度地减少模型可能继承的偏见。通过采用数据中心化实践,组织可以培养公平性、责任感和道德人工智能。
在本章的剩余部分,我们将探讨一系列以数据为中心的策略,这些策略赋予机器学习从业者减少偏差的能力。这些包括数据重采样、增强、净化、特征选择等。现实世界的例子将说明这些策略在金融、人力资源和医疗保健领域的实际影响。
如果数据被公平且准确地捕获或创建,那么算法很可能大部分都是无偏的。然而,本章中我们将涵盖的技术是在数据创建之后,机器学习从业者必须与提供的数据一起工作。
在以下子节中,我们将讨论一些数据为中心的策略,以在不改变算法的情况下减少机器学习中的偏差。这些可以被称为数据去偏技术。
样本方法
下采样和过采样等样本方法解决类别不平衡问题。下采样减少多数类实例,而过采样增强少数类示例。将两者结合起来可以减轻过拟合和信息损失,有效地平衡类别表示。这些方法可以与异常值处理和 Shapley 值结合使用,以进一步采样数据,其中难以分类或难以估计的数据点可以被移除或引入,以增强公平性指标。这些技术将在下一部分进行介绍。
下采样
在下采样中,我们移除随机或策略性的多数代表数据点的子集,以平衡类别分布——从难以分类或随机删除的多数类中删除数据点是常用的技术。我们还可以使用异常值移除进行回归任务。
过采样
在过采样中,我们添加随机或策略性的少数代表数据点的子集,为算法提供更多示例。我们可以为少数类重复或生成合成数据点,以平衡类别分布。我们可以使用SMOTE(合成少数过采样技术)和随机过采样等技术进行分类任务。或者,我们可以利用异常值或边缘案例的添加/删除进行回归任务。
下采样和过采样的组合
这些包括如SMOTEENN或SMOTETomek等技术,其中SMOTE用于过采样少数类。编辑最近邻(ENN)或 Tomek 链接等技术用于移除难以分类或难以使用最近邻达成一致意见的示例,因为这些点接近边界,没有明显的分离。
对数据进行过采样和下采样的异常检测
这包括使用异常检测技术来识别边缘情况的数据点,然后这些点可以被多次重新引入或移除,以便模型能够获得更好的信号或变得更加通用。
使用 Shapley 值进行过采样和下采样数据
这涵盖了使用 Shapley 值进行数据过采样或欠采样。Shapley 值通过评估每个特征对模型预测的贡献来量化特征的重要性。高 Shapley 值突出了有影响力的特征。移除具有高 Shapley 值但预测错误的实例可能会通过减少异常值来提高模型精度。对具有高 Shapley 值和正确预测的实例进行过采样可以加强模型对关键模式的理解,从而可能提高性能。
其他以数据为中心的技术
除了采样方法之外,还有其他以数据为中心的技术可以用来减少偏差,其中一些在之前的章节中已经介绍过,还有一些我们将在案例研究中使用。以下描述了三种主要方法。
数据清洗
这包括移除缺失数据,因为包含缺失数据可能导致不公平的结果。这些技术已在第五章,数据清洗技术中介绍,其中缺失数据被归类为“非随机缺失”。
特征选择
这包括选择特定的特征或消除会减少偏差的特征。这可能意味着识别出一个与敏感变量和结果标签高度相关的变量,并移除这样的间接变量或移除敏感变量。
特征工程
特征工程提供了减轻模型偏差的有效工具。例如,重新编码敏感属性、创建交互项或引入代理变量等技术,使模型能够在没有直接访问敏感信息的情况下学习。特征选择和降维方法裁剪了无关或冗余的特征,促进了更公平和更健壮的模型。此外,生成合成特征或利用特定领域的知识有助于提高对数据有更好理解的模型,从而有助于更公平的决策,同时提高整体模型性能并减少偏差。在示例中,我们将创建一个合成变量“兴趣”,以展示模型相对于另一个子群体是如何偏向的。
现在我们已经介绍了以数据为中心的方法,在下一节中,我们将描述问题陈述,并举例说明我们如何在现实生活中识别和减少偏差。
案例研究
当前面临的挑战集中在揭示和解决台湾信用卡违约数据集中可能存在的潜在偏差。该数据集来自加州大学欧文分校机器学习库(archive.ics.uci.edu/dataset/350/default+of+credit+card+clients),包含过去六个月内 30,000 名信用卡客户的详细信息,包括性别、婚姻状况和教育等人口统计因素。关键问题是这些人口统计特征是否将这些特征训练的决策树分类器引入偏差,特别是关注与性别相关的偏差。本例的总体目标是不仅识别偏差,而且通过应用数据为中心的技术来减轻任何偏差的结果。通过使用公平性指标重新评估算法的性能,本例旨在揭示金融决策中偏差的现实影响,特别是这些偏差如何基于性别和其他人口统计因素影响个人,可能导致信用评估和金融机会的不平等对待。解决和纠正此类偏差对于促进金融系统的公平性和平等至关重要。
我们将使用两个关键指标来检查算法的公平性:
- 
均衡机会差异: 该指标比较敏感变量(如性别、种族或年龄)的假阴性率和假阳性率,然后取假阴性率和假阳性率之间的最大差异。例如,在测试集中,男性和女性的假阳性率分别为 0.3 和 0.2(差异为 0.1),而男性和女性的假阴性率分别为 0.15 和 0.12(差异为 0.03)。由于假阳性率的差异更大,均衡机会将为 0.1。
 - 
人口统计平等比率: 该指标衡量模型做出的预测是否独立于敏感变量,如种族、性别或年龄。鉴于这是一个比率,它衡量的是低选择率与高选择率之间的比率。比率为 1 表示实现了人口统计平等,而低于 0.8 通常意味着算法对某一群体的高度偏见,超过其他群体。
 
以下是对数据集中特征的描述:
- 
LIMIT_BAL: 给定信用的金额(新台币),包括个人消费者信用及其家庭(补充)信用。 - 
Sex: 性别(1 = 男;2 = 女)。 - 
Education X3: 教育(1 = 研究生;2 = 大学;3 = 高中;4 = 其他) - 
Marriage X4: 婚姻状况(1 = 已婚;2 = 未婚;3 = 其他) - 
Age X5: 人的年龄(以年为单位) - 
PAY_0- PAY_5; X6 - X11: 过去付款的历史记录,包括从 2005 年 4 月到 9 月的过去每月付款记录,其中 PAY_0X6 = 9 月的还款状态,PAY_2; X7 = 2005 年 8 月的还款状态;... PAY_6; X11 = 2005 年 4 月的还款状态。还款状态的测量尺度为 -1 = 按时支付金额;1 = 延迟 1 个月付款;2 = 延迟 2 个月付款;...;8 = 延迟 8 个月付款;9 = 延迟 9 个月,依此类推。
 - 
BILL_AMT1 . BILL_AMT6; X12-X17: 账单金额(以新台币计)。BILL_AMT1;X12 表示 2005 年 9 月的信用卡账单金额,而 BILL_AMT6;X17 表示 2005 年 4 月的信用卡账单金额。
 - 
PAY_AMT1-PAY_AMT6; X18-X23: 根据上个月账单金额支付的金额。PAY_AMT1;X18 表示 2005 年 9 月支付的金额,而 PAY_AMT6;X23 表示 2005 年 4 月支付的金额。
 - 
default payment next month: 一个人是否在下个月的付款中违约(是 = 1,否 = 0),在 2005 年 
要导入数据集,您需要安装 pandas。我们还将使用 os 库来导航路径并存储数据集。此库是 Python 的原生库。我们将调用 loan_dataset.csv 文件,并将其保存在运行此示例的同一目录中:
import pandas as pd
import os
FILENAME = "./loan_dataset.csv"
DATA_URL = "http://archive.ics.uci.edu/ml/machine-learning-databases/00350/default%20of%20credit%20card%20clients.xls"
文件加载所需时间取决于网络速度,从几秒到一分钟不等,因此当我们第一次运行此示例时,文件将存储在本地。然而,在后续运行中,借助 os 库,我们将检查文件是否存在,否则将其下载。我们将重命名两个变量:PAY_0 到 PAY_1,并将“下个月默认付款”重命名为 default。我们不认为 ID 列对机器学习有用,因此我们将删除它:
if not os.path.exists(FILENAME):
    data = (
        pd.read_excel(io=DATA_URL, header=1)
        .drop(columns=["ID"])
        .rename(
            columns={"PAY_0": "PAY_1", "default payment next month": "default"}
        )
    )
    data.to_csv(FILENAME, sep=",", encoding="utf-8", index=False)
现在,我们将从本地目录将文件加载到名为 dataset 的 DataFrame 中。该文件包含 30,000 行和 24 列,包括目标变量:
dataset = pd.read_csv(FILENAME, sep=",", encoding="utf-8")
dataset.shape
(30000, 24)
接下来,我们运行 dataset.info() 方法来检查是否存在任何缺失值或编码错误的列:

图 8.1 – dataset.info() 方法的输出
我们没有缺失数据;然而,有三个分类列(SEX、EDUCATION 和 MARRIAGE)的数据类型为整数,我们可能需要将它们转换为字符串。由于 SEX 中的值可能是序数,因此我们将首先将它们重新映射到 1 和 0:
cat_colums = ['EDUCATION', 'MARRIAGE']
for col in cat_colums:
    dataset[col] = dataset[col].astype("category")
dataset['SEX'] = dataset['SEX'].map({1: 1, 2:0})
如果我们再次运行 dataset.info(),我们会看到三个列的数据类型现在是 category;我们现在可以一维编码它们。我们排除 SEX 进行一维编码,因为在这个数据集中,一个人要么是男性要么是女性,并且该信息可以包含在一列中。我们还将提取 SEX 并将其存储在另一个变量 A 中,并分离目标变量和独立特征。接下来,我们为 SEX 特征中的值创建一个映射,用于分析和可视化,以帮助解释结果,因此 1 将映射到 male 值,而 0 将映射到 female 值。我们将此映射存储在 A_str 变量中:
Y, A = dataset.loc[:, "default"], dataset.loc[:, "SEX"]
X = pd.get_dummies(dataset.drop(columns=["default","SEX"]))
X["SEX"] = A.copy()
A_str = A.map({1: "male", 0: "female"})
接下来,让我们加载所有必需的库。
加载库
要运行示例,你需要以下额外的库:
- 
sklearn(scikit-learn)用于数据预处理和拟合模型 - 
numpy用于计算一些指标和一些数据处理 - 
imblearn用于过采样和欠采样 - 
fairlearn用于计算偏差和公平性分数 - 
shap用于可视化模型的解释 
我们在开始时加载所有库:
from sklearn.model_selection import train_test_split, cross_validate
from sklearn.preprocessing import StandardScaler
from sklearn.tree import DecisionTreeClassifier
from imblearn.over_sampling import SMOTE, ADASYN
from imblearn.combine import SMOTEENN, SMOTETomek
from sklearn.ensemble import IsolationForest
from imblearn.pipeline import make_pipeline
from imblearn.under_sampling import AllKNN, InstanceHardnessThreshold, RepeatedEditedNearestNeighbours, TomekLinks, EditedNearestNeighbours
from sklearn.metrics import balanced_accuracy_score, roc_auc_score, confusion_matrix, ConfusionMatrixDisplay
from sklearn.pipeline import Pipeline
from fairlearn.metrics import MetricFrame, equalized_odds_difference, demographic_parity_ratio
import numpy as np
import shap
接下来,我们使用 train_test_split 将数据集分为 train 和 test,并将 20% 的数据分配给测试。我们还把 A_str 分为 A_train 和 A_test,这样我们就可以在测试数据上计算公平性分数:
X_train, X_test, y_train, y_test, A_train, A_test = train_test_split(X,
                 Y,
                 A_str,
                 test_size=0.2,
                 stratify=Y,
                 random_state=42)
接下来,我们创建决策树分类器管道并用敏感特征训练算法:
d_tree_params = {
    "min_samples_leaf": 10,
    "random_state": 42
}
estimator = Pipeline(steps=[
    ("classifier", DecisionTreeClassifier(**d_tree_params))
])
estimator.fit(X_train, y_train)
接下来,我们计算 ROC 分数并提取预测值。我们还可视化混淆矩阵:
y_pred_proba = estimator.predict_proba(X_test)[:, 1]
y_pred = estimator.predict(X_test)
print(f"Roc score is : {roc_auc_score(y_test, y_pred_proba)}")
cm = ConfusionMatrixDisplay(confusion_matrix(y_test, y_pred), display_labels=estimator.classes_)
cm.plot()
Roc score is : 0.6875636482794665
代码生成了以下混淆矩阵:

图 8.2 – 输出混淆矩阵
为了检查算法是否公平,我们首先计算假阳性率和假阴性率,然后比较测试数据集中男性和女性群体之间的差异,以查看两个群体之间是否存在很大差异。
在下面的代码块中,我们创建了两个函数来计算假阳性率和假阴性率。我们进一步创建了一个公平性度量字典,其中我们使用了假阳性率和假阴性率,以及来自 scikit-learn 的平衡准确度指标。然后我们创建了一个公平性度量列表,并将其存储在一个变量中以方便访问:
def false_positive_rate(y_true, y_pred):
    """Compute the standard error for the false positive rate estimate."""
    tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel()
    return fp/(fp+tn)
def false_negative_rate(y_true, y_pred):
    """Compute the standard error for the false positive rate estimate."""
    tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel()
    return fn/(tp+fn)
fairness_metrics = {
    "balanced_accuracy": balanced_accuracy_score,
    "false_positive_rate": false_positive_rate,
    "false_negative_rate": false_negative_rate,
}
metrics_to_report = list(fairness_metrics.keys())
我们还创建了一个函数来报告男性和女性群体在公平性度量上的差异。我们首先使用 fairlearn 的便利函数 MetricFrame 创建一个名为 metricframe 的 DataFrame。它接受真实标签、预测和敏感特征值,以及一个报告的度量字典。然后我们利用 .by_group 属性报告每个群体的公平性度量。在函数内部,我们还报告了来自 fairlearn 库的 equalised_odds_difference 和 demographic_parity_ratio,以了解模型的总体公平性:
def calculate_fairness_metrics(y_test, y_pred, A_test, metrics=fairness_metrics):
    """Function to calculate fairness metrics"""
    metricframe = MetricFrame(
        metrics=fairness_metrics,
        y_true=y_test,
        y_pred=y_pred,
        sensitive_features=A_test,
    )
    print(metricframe.by_group[metrics_to_report])
    print("\n *diff*")
    print(metricframe.difference()[metrics_to_report])
    print("\n *final_metrics*")
    print(metricframe.overall[metrics_to_report])
    equalized_odds = equalized_odds_difference(
        y_test, y_pred, sensitive_features=A_test
    )
    print("\n *equalized_odds*")
    print(equalized_odds)
    dpr= demographic_parity_ratio(y_test, y_pred, sensitive_features=A_test)
    print("\n *demographic_parity_ratio*")
    print(dpr)
我们现在运行函数并计算公平性分数。很明显,由于在各个群体中假阳性率和假阴性率相似,模型在男性和女性群体中相当相似。由于假阳性率与假阴性率的差异大于假阴性率,均衡机会差异与两组假阳性率之间的差异相同。我们还可以看到,人口比例率高于 0.8,这意味着两个群体都有相当大的可能性获得积极的结果:
calculate_fairness_metrics_unmitigated = calculate_fairness_metrics(y_test, y_pred, A_test)
这将显示以下输出:

图 8.3 – 公平性分数
为了说明数据集中的偏差,我们可能需要生成一个与真实世界场景相关的合成变量,在该场景中,根据历史数据,一个群体受到更不公平的对待。首先,我们比较训练数据集中男性和女性的违约率。然后我们添加合成噪声:
for val in dataset.SEX.unique():
    print(f"{('male' if val == 1 else 'female')} default rate is: ")
    print(dataset[dataset.SEX == val]['default'].mean())
    print()
female default rate is:
0.20776280918727916
male default rate is: 0.2416722745625841
由于男性的违约率高于女性,我们可以复制一个偏差场景,其中违约率较低的申请者将获得较低的利率,而违约率较高的申请者将受到银行施加的较高利率。让我们假设银行经理认为男性更有可能违约,并且银行决定不对场景进行泛化,而是对男性收取更高的利率。
为了模拟这个场景,我们将引入一个新的特征,Interest_rate,遵循高斯分布。当某人没有违约时,平均值将是 0,但如果有违约,将是 1 的两倍。我们还为男性设置标准差为 2,为女性设置标准差为 1。
为了生成合成的高斯分布,我们使用numpy.random.normal方法,种子为42以确保可重复性:
np.random.seed(42)
X.loc[:, 'Interest_rate'] = np.random.normal(loc=2*Y, scale=A.map({1:2, 0:1}))
print("Maximum interest rate for men who defaulted vs women who defaulted")
print(X[(X.SEX == 1) & (Y == 1)]["Interest_rate"].max(), X[(X.SEX == 0) & (Y == 1)]["Interest_rate"].max())
print()
print("Maximum interest rate for men who did not default vs women that did not default")
print(X[(X.SEX == 1) & (Y == 0)]["Interest_rate"].max(), X[(X.SEX == 0) & (Y == 0)]["Interest_rate"].max())
Maximum interest rate for men who defaulted vs women who defaulted
9.852475412872653 6.479084251025757
Maximum interest rate for men who did not default vs women that did not default
6.857820956016427 3.852731490654721
现在我们已经添加了噪声,我们使用利率变量重新训练算法并重新计算公平性指标。我们首先分割数据,然后重新训练并重新计算公平性指标。我们像之前一样将数据重新分割成train和test,然后重新训练算法。一旦重新训练,我们计算影响。
在以下代码中,我们可以看到,通过添加合成的利率变量,我们提高了 ROC 指标:
y_pred_proba = estimator.predict_proba(X_test)[:, 1]
y_pred = estimator.predict(X_test)
roc_auc_score(y_test, y_pred_proba)
0.8465698909107798
从以下输出中可以清楚地看出,我们现在有一个基于均衡机会的更具偏差的算法。在男性中,假阴性率相当高,这意味着不太可能偿还银行的男性更有可能获得贷款,如果这个模型被投入生产,可能会导致不公平的结果:
calculate_fairness_metrics(y_test, y_pred, A_test)
这将打印以下信息:

图 8.4 – 公平性分数
为了减少偏差,我们将在特征选择中应用第一种以数据为中心的偏差消除技术,通过从算法中移除敏感变量来实现。这可以通过重新训练不带SEX变量的算法来完成。
由于数据集因利率差异较大而偏向于某一性别,在现实世界中,建议数据工程师和数据科学家与领域专家和数据生产者合作,以减少数据集中的这种偏差。例如,与其使用SEX来确定利率,不如使用其他特征,如支付历史、信用历史和收入。在训练步骤中,我们可以删除SEX变量:
estimator.fit(X_train.drop(['SEX'], axis=1), y_train)
Pipeline(steps=[('classifier',
                 DecisionTreeClassifier(min_samples_leaf=10, random_state=42))])
从以下输出中,我们可以看到,通过移除SEX变量,ROC 分数从 0.846 下降到 0.839:
estimator.fit(X_train.drop(['SEX'], axis=1), y_train)
y_pred_proba = estimator.predict_proba(X_test.drop(['SEX'], axis=1))[:, 1]
y_pred = estimator.predict(X_test.drop(['SEX'], axis=1))
roc_auc_score(y_test, y_pred_proba)
0.8392395442658211
观察以下公平性指标,很明显,当结果基于数据集的队列存在偏差时,从训练中移除变量可以消除算法的偏差。在male中的假阴性率有所下降,而在female中有所上升;然而,与使用SEX变量相比,算法更加公平。均衡机会从 0.18 下降到 0.07,但人口比例比有所降低,这意味着一个群体获得贷款的机会比另一个群体更多:
calculate_fairness_metrics_mitigated_v1 = calculate_fairness_metrics(y_test, y_pred, A_test)
输出如下:

图 8.5 – 公平性指标
接下来,我们将向您展示如何应用欠采样技术以确保结果变量平衡。
AllKNN 欠采样方法
我们将从imblearn包中的 AllKNN 算法开始,然后尝试即时硬度算法。由于这些算法在底层使用 KNN,这是一种基于距离的度量,我们需要确保使用 scikit-learn 的StandardScaler方法对特征进行缩放。我们将首先缩放变量,然后运行采样算法,然后训练决策树。我们将使用 5 k 交叉验证运行算法,并确保函数返回训练好的模型。交叉验证将在roc_auc和平衡准确度上进行评分。
我们将首先尝试一个来自imblearn的欠采样技术,AllKNN。这个算法并不旨在平衡多数和少数类别;然而,它从多数类别中移除难以分类的实例。它是通过迭代实现的,首先在完整数据集上训练模型。然后在多数类别的预测步骤中,如果邻居之间关于预测结果存在任何不一致,该数据点将从多数类别中移除。在第一次迭代中,训练一个 1-KNN 模型并移除一些样本,然后在下一次迭代中,训练一个 2-KNN 模型,在接下来的迭代中,训练一个 3-KNN 模型。通常,算法(默认情况下)将在 3-KNN 迭代结束时停止;然而,实践者可以选择更多的迭代,算法将不会停止,直到多数和少数类别的样本数量相同或达到最大迭代次数——哪个先到为止。
首先,我们来定义缩放器和采样方法:
scaler = StandardScaler()
sampler_method = AllKNN(n_jobs=-1)
接下来,我们创建一个管道对象,并将缩放器、采样器和估计器传递到管道中:
sampler_pipeline = make_pipeline(
    scaler,
    sampler_method,
    estimator)
然后我们传递训练数据并运行交叉验证。我们将交叉验证方法设置为通过设置 return_estimator=True 返回估计器(管道),这样我们就可以用它来对测试数据进行预测:
cv_results = cross_validate(sampler_pipeline,
                            X_train.drop(['SEX'], axis=1),
                            y_train, scoring=['roc_auc','balanced_accuracy'],
                            return_estimator=True)
接下来,我们打印交叉验证步骤返回的预测结果中每个步骤的 ROC 和平衡准确率的平均值和标准差,在每个步骤中,训练使用了四个折,并在第五个折上进行预测:
print(f"Validation roc auc : {cv_results['test_roc_auc'].mean():.3f} +/- {cv_results['test_roc_auc'].std():.3f}")
print(f"Validation balanced acc : {cv_results['test_balanced_accuracy'].mean():.3f} +/- {cv_results['test_balanced_accuracy'].std():.3f}")
Validation roc auc : 0.853 +/- 0.006
Validation balanced acc : 0.802 +/- 0.005
我们可以看到,通过使用下采样技术移除困难示例,test 数据上的 roc_auc 从上一步的 0.839 上升到 0.85:
model = sampler_pipeline.fit( X_train.drop(['SEX'], axis=1), y_train)
y_pred_proba = model.predict_proba(X_test.drop(['SEX'],axis=1))[:, 1]
y_pred = model.predict(X_test.drop(['SEX'],axis=1))
roc_auc_score(y_test, y_pred_proba)
0.8537904984477683
接下来,我们计算公平性指标。尽管男性和女性的假阴性率都有所下降,但假阳性率有所上升,与上一步相比,均衡概率差异也有所增加。这可能是由于难以分类的男性样本已被移除:
calculate_fairness_metrics_mitigated_v2 = calculate_fairness_metrics(y_test, y_pred, A_test)

图 8.6 – 公平性指标
我们现在将通过引入困难案例来探索对公平性指标的影响。
实例硬度下采样方法
如其名所示,实例硬度方法专注于难以分类的样本,这些样本通常位于边界或与其他类别重叠。通常,这取决于所使用的算法(因为一些算法在某些困难情况下比其他算法更好)以及类别之间的重叠程度。对于这样的样本,学习算法通常会显示对困难案例的低概率预测,这意味着概率越低,实例硬度越高。在底层,该方法具有根据类别不平衡保留正确数量样本的能力。
在第一步中,我们将定义算法,并将算法传递到实例硬度步骤。然后我们将定义实例硬度下采样方法,采用三折交叉验证。
接下来,我们创建决策树估计器。最后,我们将管道中的步骤与缩放数据集、下采样数据和最终训练模型相结合。当管道定义后,我们运行与之前管道类似的交叉验证:
d_tree_params = {
    "min_samples_leaf": 10,
    "random_state": 42
}
d_tree = DecisionTreeClassifier(**d_tree_params)
sampler_method = InstanceHardnessThreshold(
    estimator=d_tree,
    sampling_strategy='auto',
    random_state=42,
    n_jobs=-1,
    cv=3)
estimator = Pipeline(steps=[
    ("classifier", DecisionTreeClassifier(**d_tree_params))
])
sampler_pipeline = make_pipeline(
    scaler,
    sampler_method,
    estimator)
cv_results = cross_validate(sampler_pipeline, X_train.drop(['SEX'], axis=1), y_train, scoring=['roc_auc','balanced_accuracy'], return_estimator=True)
AllKNN 和 InstanceHardness 返回了类似的交叉验证结果:
print(f"Validation roc auc : {cv_results['test_roc_auc'].mean():.3f} +/- {cv_results['test_roc_auc'].std():.3f}")
print(f"Validation balanced acc : {cv_results['test_balanced_accuracy'].mean():.3f} +/- {cv_results['test_balanced_accuracy'].std():.3f}")
Validation roc auc : 0.853 +/- 0.005
Validation balanced acc : 0.807 +/- 0.007
当使用实例硬度方法时,test 数据上的 ROC 从 0.85 略微上升到 0.854:
model = sampler_pipeline.fit( X_train.drop(['SEX'], axis=1), y_train)
y_pred_proba = model.predict_proba(X_test.drop(['SEX'],axis=1))[:, 1]
y_pred = model.predict(X_test.drop(['SEX'],axis=1))
roc_auc_score(y_test, y_pred_proba)
0.8549627959428299
公平性指标与之前的欠采样技术相当相似,可能由于类似的原因,通过去除困难案例,模型无法处理预测困难的男性案例。然而,在两种欠采样方法中,与特征选择步骤相比,均衡机会增加了,而且人口统计学平等比率仍然低于 0.8,这意味着在预测违约时,一个性别子类更有可能被选中而不是另一个:
calculate_fairness_metrics_mitigated_v3 = calculate_fairness_metrics(y_test, y_pred, A_test)

图 8.7 - 公平性指标
接下来,让我们看看过采样方法。
过采样方法
提高模型性能和公平性指标的一种方法是引入额外的示例。接下来的两种过采样技术,SMOTE和ADASYN,在第七章中介绍,在以数据为中心的机器学习中使用合成数据,因此我们不会详细介绍算法背后的细节。我们将使用这些技术,通过添加额外的示例来提高公平性指标,希望模型能够通过额外的数据点更好地学习。
对于每种方法,我们首先将数据集进行缩放,添加额外的少数类示例,然后训练模型。我们将打印交叉验证分数和测试ROC 分数,以及公平性指标。
SMOTE
由于我们在第七章中使用了此算法,在以数据为中心的机器学习中使用合成数据,我们将直接进入代码:
estimator = Pipeline(steps=[
    ("classifier", DecisionTreeClassifier(**d_tree_params))
])
sampler_method = SMOTE(random_state=42)
sampler_pipeline = make_pipeline(
    scaler,
    sampler_method,
    estimator)
cv_results = cross_validate(sampler_pipeline, X_train.drop(['SEX'], axis=1), y_train, scoring=['roc_auc','balanced_accuracy'], return_estimator=True)
print(f"Validation roc auc : {cv_results['test_roc_auc'].mean():.3f} +/- {cv_results['test_roc_auc'].std():.3f}")
print(f"Validation balanced acc : {cv_results['test_balanced_accuracy'].mean():.3f} +/- {cv_results['test_balanced_accuracy'].std():.3f}")
model = sampler_pipeline.fit( X_train.drop(['SEX'], axis=1), y_train)
y_pred_proba = model.predict_proba(X_test.drop(['SEX'],axis=1))[:, 1]
y_pred = model.predict(X_test.drop(['SEX'],axis=1))
roc_auc_score(y_test, y_pred_proba)
Validation roc auc : 0.829 +/- 0.009
Validation balanced acc : 0.758 +/- 0.012
0.8393191272926885
与之前介绍的欠采样方法相比,验证指标和测试ROC 分数显示出较差的结果。在下一步中,我们将探索公平性指标:
calculate_fairness_metrics_mitigated_v4 = calculate_fairness_metrics(y_test, y_pred, A_test)

图 8.8 - 公平性指标
与欠采样方法相比,公平性指标更好——也就是说,男性和女性之间的假阳性率和假阴性率之间的差异减小了,基于人口统计学平等比率,模型更有可能同时选择两种性别的贷款违约申请人。在下一节中,我们将使用ADASYN算法,并将其与SMOTE和其他欠采样方法进行比较。
ADASYN
与SMOTE方法类似,我们在第七章中介绍了ADASYN,在以数据为中心的机器学习中使用合成数据,因此我们将直接进入代码,其中我们将对少数类进行过采样:
sampler_method = ADASYN(random_state=42)
sampler_pipeline = make_pipeline(
    scaler,
    sampler_method,
    estimator)
cv_results = cross_validate(sampler_pipeline, X_train.drop(['SEX'], axis=1), y_train, scoring=['roc_auc','balanced_accuracy'], return_estimator=True)
print(f"Validation roc auc : {cv_results['test_roc_auc'].mean():.3f} +/- {cv_results['test_roc_auc'].std():.3f}")
print(f"Validation balanced acc : {cv_results['test_balanced_accuracy'].mean():.3f} +/- {cv_results['test_balanced_accuracy'].std():.3f}")
model = sampler_pipeline.fit( X_train.drop(['SEX'], axis=1), y_train)
y_pred_proba = model.predict_proba(X_test.drop(['SEX'],axis=1))[:, 1]
y_pred = model.predict(X_test.drop(['SEX'],axis=1))
roc_auc_score(y_test, y_pred_proba)
Validation roc auc : 0.823 +/- 0.004
Validation balanced acc : 0.757 +/- 0.006
0.816654655300673
验证指标和测试ROC 分数略低于SMOTE结果和欠采样方法。现在,让我们回顾公平性指标:
calculate_fairness_metrics_mitigated_v5 = calculate_fairness_metrics(y_test, y_pred, A_test)

图 8.9 - 公平性指标
ADASYN的均衡机会略高,而与SMOTE相比,人口比例略好,两种过采样技术都保证了比欠采样方法更高的公平性,但 ROC 性能略差。
我们已经看到,尽管平衡了类别,但模型公平性受到了损害,模型在大多数男性例子上犯的错误更多。因此,在下一节中,我们将随机引入一些额外的男性例子,其中模型错误地将阳性案例分类为阴性。
随机过采样和错误分类的例子
我们将首先使用ADASYN平衡数据集,并避免欠采样技术,因为我们希望保留难以分类的困难案例。然后我们训练模型,并识别模型认为应该是阳性但错误地将其分类为阴性的男性案例。然后我们随机选择这些案例的 10%,将它们重新添加到训练数据集中,并使用相同的算法重新训练模型。
最后,我们回顾测试数据上的模型指标和公平性指标。
我们使用过采样并在随机位置重新引入被错误分类的数据点。让我们使用ADASYN过采样方法运行管道:
sampler_method = ADASYN(random_state=42)
sampler_pipeline = make_pipeline(
    scaler,
    sampler_method,
    estimator)
cv_results = cross_validate(sampler_pipeline, X_train.drop(['SEX'], axis=1), y_train, scoring=['roc_auc','balanced_accuracy'], return_estimator=True)
model = sampler_pipeline.fit( X_train.drop(['SEX'], axis=1), y_train)
接下来,我们识别训练数据集中模型在男性人口上犯错误的例子——即模型预测错误的阴性例子。我们首先对与男性相关的数据进行子集划分,然后在此数据上运行预测:
X_train_males = X_train[X_train.SEX == 1].copy()
X_train_males["predictions"] = model.predict(X_train_males.drop(['SEX'], axis=1))
X_train_males['y_true'] = y_train.filter(X_train_males.index)
然后,我们选取true标签为1但模型预测为0的数据子集:
X_train_male_false_negatives = X_train_males[(X_train_males.y_true == 1) & (X_train_males.predictions == 0)]
我们随机选择 10%的值并将它们添加到X_train数据集中。我们利用.sample方法,并且这个随机选择是带替换进行的:
X_train_sample = X_train_male_false_negatives[X_train.columns].sample(frac=0.1, replace=True, random_state=42, axis=0)
y_train_sample = X_train_male_false_negatives['y_true'].sample(frac=0.1, replace=True, random_state=42, axis=0)
然后,我们将这 10%添加到X_train和y_train中,并创建一个新的数据集:
X_train_with_male_samples = pd.concat([X_train, X_train_sample], axis=0, ignore_index=True)
y_train_with_male_samples = pd.concat([y_train, y_train_sample], axis=0, ignore_index=True)
然后,我们在新的数据集上训练算法,并打印出验证指标和测试ROC 分数:
cv_results = cross_validate(sampler_pipeline, X_train_with_male_samples.drop(['SEX'], axis=1), y_train_with_male_samples, scoring=['roc_auc','balanced_accuracy'], return_estimator=True)
print(f"Validation roc auc : {cv_results['test_roc_auc'].mean():.3f} +/- {cv_results['test_roc_auc'].std():.3f}")
print(f"Validation balanced acc : {cv_results['test_balanced_accuracy'].mean():.3f} +/- {cv_results['test_balanced_accuracy'].std():.3f}")
model = sampler_pipeline.fit(X_train_with_male_samples.drop(['SEX'], axis=1), y_train_with_male_samples)
y_pred_proba = model.predict_proba(X_test.drop(['SEX'],axis=1))[:, 1]
y_pred = model.predict(X_test.drop(['SEX'],axis=1))
roc_auc_score(y_test, y_pred_proba)
Validation roc auc : 0.824 +/- 0.005
Validation balanced acc : 0.754 +/- 0.005
0.8201623558253082
与过采样部分相比,验证指标和测试ROC 分数相当相似。接下来,我们回顾公平性指标,以检查它们是否有所改善:
calculate_fairness_metrics_mitigated_v6 = calculate_fairness_metrics(y_test, y_pred, A_test)

图 8.10 – 公平性指标
通过添加一些错误的阴性男性例子,我们可以看到均衡机会略微提高到了 0.098,人口比例也有所改善,增加到 0.85。我们相信,如果我们添加错误的阳性例子和错误的阴性例子,并将这些与欠采样和过采样技术结合起来,我们可以取得更好的结果。
为了演示这一点,我们将遍历四种欠采样技术(AllKNN、RepeatedEditedNearestNeighbours、InstanceHardnessThreshold 和 Tomek),两种过采样技术(SMOTE 和 ADASYN),以及两种过采样和欠采样的组合技术(SMOTEENN 和 SMOTETomek)。这些算法的工作原理超出了本例的范围。相反,目标是演示这些数据技术如何导致更好的选择和具有略微较差性能但更高公平性的泛化模型。
我们现在将开发一种机制,首先训练算法,然后添加假阳性和假阴性示例。一旦添加了示例,我们将通过采样数据集并使用先前算法运行管道。我们将记录公平性结果和 ROC 分数,以找到在我们算法中最好地促进公平性和性能平衡的技术。
我们首先创建一个字典,包含上述每种采样技术的配置,这样我们就可以遍历它。我们可以将这个采样技术称为 AutoML:
methods = {
    "all_knn": AllKNN(n_jobs=-1),
    "renn": RepeatedEditedNearestNeighbours(n_jobs=-1),
    "iht": InstanceHardnessThreshold(
        estimator=DecisionTreeClassifier(**d_tree_params),
        random_state=42,
        n_jobs=-1,
        cv=3),
    "tomek": TomekLinks(n_jobs=-1),
    "adasyn" : ADASYN(random_state=42),
    "smote" : SMOTE(random_state=42),
    "smoteenn": SMOTEENN(random_state=42,
                         smote=SMOTE(random_state=42),
                         enn=EditedNearestNeighbours(n_jobs=-1)
                        ),
    "smotetomek": SMOTETomek(random_state=42,
                             smote=SMOTE(random_state=42),
                             tomek=TomekLinks(n_jobs=-1)
                            )
          }
接下来,我们创建了两个函数,这些函数接受训练数据集、模型、列及其子集值,以帮助创建随机样本。以下函数将采样假阳性:
def sample_false_positives(X_train, y_train, estimator, perc=0.1, subset_col="SEX", subset_col_value=1, with_replace=True):
    """Function to sample false positives"""
    X_train = X_train.copy()
    y_train = y_train.copy()
    X_train_subset = X_train[X_train[subset_col] == subset_col_value].copy()
    y_train_subset = y_train.filter(X_train_subset.index).copy()
    X_train_subset["predictions"] = estimator.predict(X_train_subset.drop([subset_col], axis=1))
    X_train_subset['y_true'] = y_train_subset.values
    X_train_subset_false_positives = X_train_subset[(X_train_subset.y_true == 0) & (X_train_subset.predictions == 1)]
    X_train_sample = X_train_subset_false_positives[X_train.columns].sample(frac=perc, replace=with_replace, random_state=42, axis=0)
    y_train_sample = X_train_subset_false_positives['y_true'].sample(frac=perc, replace=with_replace, random_state=42, axis=0)
    X_train_sample = pd.concat([X_train, X_train_sample], axis=0, ignore_index=True)
    y_train_sample = pd.concat([y_train, y_train_sample], axis=0, ignore_index=True)
    return X_train_sample, y_train_sample
此函数将采样假阴性。默认情况下,两种方法都会添加 10% 的随机示例,并替换它们:
def sample_false_negatives(X_train, y_train, estimator, perc=0.1, subset_col="SEX", subset_col_value=1, with_replace=True):
    """Function to sample false positives"""
    X_train = X_train.copy()
    y_train = y_train.copy()
    X_train_subset = X_train[X_train[subset_col] == subset_col_value].copy()
    y_train_subset = y_train.filter(X_train_subset.index).copy()
    X_train_subset["predictions"] = estimator.predict(X_train_subset.drop([subset_col], axis=1))
    X_train_subset['y_true'] = y_train_subset.values
    X_train_subset_false_negatives = X_train_subset[(X_train_subset.y_true == 1) & (X_train_subset.predictions == 0)]
    X_train_sample = X_train_subset_false_negatives[X_train.columns].sample(frac=perc, replace=with_replace, random_state=42, axis=0)
    y_train_sample = X_train_subset_false_negatives['y_true'].sample(frac=perc, replace=with_replace, random_state=42, axis=0)
    X_train_sample = pd.concat([X_train, X_train_sample], axis=0, ignore_index=True)
    y_train_sample = pd.concat([y_train, y_train_sample], axis=0, ignore_index=True)
    return X_train_sample, y_train_sample
接下来,我们创建一个函数,该函数在数据改进后计算测试指标。该函数接受测试数据和估计器,并返回模型指标和公平性指标:
def calculate_metrics(estimator, X_test, y_test, A_test):
    """Function to calculate metrics"""
    y_pred_proba = estimator.predict_proba(X_test)[:, 1]
    y_pred = model.predict(X_test)
    roc_auc = roc_auc_score(y_test, y_pred_proba)
    balanced_accuracy = balanced_accuracy_score(y_test, y_pred)
    equalized_odds = equalized_odds_difference(
        y_test, y_pred, sensitive_features=A_test
    )
    dpr = demographic_parity_ratio(y_test, y_pred, sensitive_features=A_test)
    return roc_auc, balanced_accuracy, equalized_odds, dpr
接下来,我们创建一个管道,该管道将采样数据集,然后创建随机的假阳性 male 和假阴性 male 示例。然后我们将这些示例逐个合并到训练数据中,并重新训练相同的算法。然后我们计算指标并将它们存储在一个名为 results 的列表中,其中包含列。每次迭代都会添加带有模型性能和公平性指标的假阴性和假阳性示例。然后我们使用这个列表来比较算法之间的结果。
注意
创建管道的代码相当长。请参阅 GitHub 以获取完整代码:github.com/PacktPublishing/Data-Centric-Machine-Learning-with-Python/tree/main/Chapter%208%20-%20Techniques%20for%20identifying%20and%20removing%20bias
接下来,我们创建一个名为 df 的 DataFrame,并将所有的 test 指标添加进去,以便我们可以比较哪种方法获得了最佳模型性能和公平性指标:
df = pd.DataFrame(data=results,
                  columns=["method", "sample",
                           "test_roc_auc", "test_balanced_accuracy",
                           "equalized_odds",
                           "demographic_parity_ratio",
                           "validation_roc_auc",
                           "validation_balanced_accuracy"]
                 )
让我们根据均衡机会对 DataFrame 进行排序:
df.sort_values(by="equalized_odds")
我们可以看到,当数据集使用 Tomek Links 进行采样时,从边界移除了困难案例,并与额外的错误阳性男性训练样本结合,这导致了最佳均衡概率为 0.075;然而,没有达到 0.8 的人口比例。当使用 SMOTETomek 技术与错误阴性男性示例结合时,模型实现了 0.088 的均衡概率比,这是所有采样方法中最好的,模型也实现了高的人口比例比。

图 8.11 – 结果输出数据集
使用异常值进行过采样
在前面的步骤中,我们了解到通过将错误分类的示例添加到训练数据集中,我们能够提高模型公平性。在下一步中,我们不会随机选择样本,而是将利用一个识别异常值的算法,然后我们将这些异常值添加到训练数据集中作为过采样机制。
首先,我们创建一个管道来过采样少数类:
X_train_scaled = pd.DataFrame()
scaler = StandardScaler()
sampler = SMOTETomek(random_state=42,
                     smote=SMOTE(random_state=42),
                     tomek=TomekLinks(n_jobs=-1)
                    )
接下来,我们提取过采样数据。没有理由为什么不能选择欠采样或无采样。一旦提取过采样数据,我们将其缩放回原始特征空间:
columns = X_train.drop(['SEX'], axis=1).columns
X_train_scaled[columns] = scaler.fit_transform(X_train.drop(['SEX'], axis=1))
X_train_resample, y_train_resample = sampler.fit_resample(X_train_scaled, y_train)
X_train_resample[columns] = scaler.inverse_transform(X_train_resample)
接下来,我们训练隔离森林以识别 10%的异常值。为此,我们将污染率设置为0.1。然后我们在重采样数据上拟合模型,并在此数据上运行预测。我们将结果存储在一个名为IF_anomaly的列中,并将其添加到重采样数据集中。然后我们提取这些异常值,作为隔离森林标签,其值为-1:
anomaly_model = IsolationForest(contamination=float(.1), random_state=42, n_jobs=-1)
anomaly_model.fit(X_train_resample)
X_train_resample['IF_anomaly'] = anomaly_model.predict(X_train_resample)
X_train_resample['default'] = y_train_resample
X_train_additional_samples = X_train_resample[X_train_resample.IF_anomaly == -1]
X_train_additional_samples.drop(['IF_anomaly'], axis=1, inplace=True)
接下来,我们将这些额外的数据点添加到原始数据集中,并训练决策树模型。一旦模型拟合完成,我们就在测试数据上计算 ROC 分数。我们可以看到这是 0.82:
X_train_clean = X_train_resample[X_train_resample.IF_anomaly != -1]
y_train_clean = X_train_clean.default
estimator.fit(X_train_clean.drop(['IF_anomaly', 'default'], axis=1), y_train_clean)
y_pred_proba = estimator.predict_proba(X_test.drop(['SEX'], axis=1))[:, 1]
y_pred = estimator.predict(X_test.drop(['SEX'], axis=1))
roc_auc_score(y_test, y_pred_proba)
0.8248481592937735
接下来,我们计算公平性指标。根据以下结果,我们可以说,在上一节中训练的模型产生了更好的公平性和人口比例比分数:

图 8.12 – 公平性指标
现在我们已经利用了各种欠采样和过采样的数据示例,包括重新引入随机错误分类的示例和异常值,在下一节中,我们将利用一种高级技术,我们将更慎重地选择添加哪些示例以及移除哪些示例,以进一步减少算法中的偏差。
使用 Shapley 值来检测偏差、过采样和欠采样数据
在本节中,我们将利用 Shapley 值来识别模型难以做出正确预测的示例。我们将使用影响分数来添加、消除或使用两者的组合来提高公平性指标。
SHAP(代表Shapley Additive exPlanations)是一种基于博弈论原理的无模型偏见机器学习方法。它通过分配一个分数来帮助研究特征及其交互对最终结果的重要性,类似于在游戏中计算每个玩家在特定时间点的贡献,就像在计算分数输出时那样。
Shapley 值可以帮助提供全局重要性(特征对所有预测的整体影响),也可以提供局部重要性(每个特征对单个结果的影响)。它还可以帮助理解影响的方向——也就是说,一个特征是否有积极影响或消极影响。
因此,在机器学习中,Shapley 值有很多应用场景,例如偏差检测、局部和全局模型调试、模型审计和模型可解释性。
在本节中,我们使用 Shapley 值来理解模型和特征对结果的影响。我们利用这些特征的影响,并确定模型最有可能犯错误的地方。然后我们应用两种技术:一种是从数据中删除这些行,另一种是对包含这些行的数据进行过采样。
首先,我们导入 SHAP 库,然后在过采样数据集上训练决策树模型。在步骤结束时,我们有一个模型和过采样的X和y样本。我们将SEX变量包含在训练数据中,以查看 Shapley 值是否可以帮助我们检测偏差。首先,我们需要将数据重新拆分为train和test集,就像前几节所做的那样:
model = DecisionTreeClassifier(**d_tree_params)
model.fit(X_train, y_train)
DecisionTreeClassifier(min_samples_leaf=10, random_state=42)
接下来,我们定义 SHAP 树解释器,通过提供决策树模型,然后使用.shap_values方法提取train集的 Shapley 值:
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_train)
让我们提取 Shapley 值的第一个行,针对类别 0。数组包含每个特征值对最终输出的贡献。正值表示相应的特征对预测类别 0 有积极影响,而负值则对预测类别 0 产生负面影响:
shap_values[0][0]
这将打印出以下数组:

图 8.13 – 结果输出数组
接下来,我们为类别标签 0 生成一个摘要图:
shap.summary_plot(shap_values[0], X_train)
这将生成以下图表:

图 8.14 – SHAP 值
红点代表特征的高值,而蓝点代表相应特征的低值。x轴表示 Shapley 值,正值表示数据点在预测类别 0 时具有积极影响,而负值表示对应特征的数据点对类别 0 的预测产生负面影响。如果我们看图 8.13,高利率和男性客户对类别 0 的预测产生负面影响是非常明显的。Shapley 值确实表明模型对男性客户存在偏见。
接下来,我们为类别标签 1 生成一个摘要图:
shap.summary_plot(shap_values[1], X_train)
这将生成以下摘要图:

图 8.15 – SHAP 摘要图
与类别 0 的摘要图相比,高利率和男性客户对贷款违约有积极影响——也就是说,如果你是男性并且之前有更高的利率,你很可能会违约。
我们之前了解到,通过从模型训练中移除 SEX 特征,模型变得更加公平,并且使用摘要图清楚地指示 Shapley 值。现在,我们通过训练不带 SEX 特征的新模型来提取 Shapley 值。然后我们对训练数据进行评分,首先识别所有对应于假阴性和假阳性的行。然后我们计算模型出错时每行的 Shapley 值总和,并保留影响最低的行。我们运行两个实验:首先,我们对训练数据集进行下采样并计算公平性指标,其次,我们对训练数据集进行上采样以向模型提供更好的信号并重新计算公平性指标。
首先,让我们在不包含 SEX 特征的情况下训练模型:
model = DecisionTreeClassifier(**d_tree_params)
X_train_samples = X_train.drop(['SEX'], axis=1).copy()
y_train_samples = y_train.copy()
model.fit(X_train_samples, y_train_samples)
接下来,我们提取 Shapley 值:
explainer = shap.Explainer(model)
shap_values = explainer.shap_values(X_train_samples)
我们对训练数据进行评分,计算预测值,并将这些值存储在 Y_pred 中:
Y_pred = model.predict(X_train_samples)
然后我们将检查索引 0 处类别 0 和类别 1 的 Shapley 值总和,并打印相应的预测值和 true 值:
print(f"Shapley value for first value in the dataset for class 0 : {sum(shap_values[0][0])}")
print(f"Shapley value for first value in the dataset for class 1 : {sum(shap_values[1][0])}")
print(f"Prediction of first value is {Y_pred[0]}")
print(f"Actual prediction is {y_train_samples[0]}")
Shapley value for first value in the dataset for class 0 : -0.07290931372549389
Shapley value for first value in the dataset for class 1 : 0.07290931372548978
Prediction of first value is 0
Actual prediction is 1
模型预测了 0。接下来,我们提取模型出错时的 Shapley 值。为此,我们使用带有 zip 功能的列表推导。数组的第一个值将是数据点的索引位置,这样我们就可以知道哪个 Shapley 值与哪一行相关。接下来的值按照预测顺序排列,包括 true 值、类别 0 的 Shapley 值行总和以及类别 1 的 Shapley 值总和。一旦我们提取了这些值,我们就创建一个 DataFrame 并将值存储在 df 中,然后通过 DataFrame 进行采样以查看五个值。我们使用随机种子以确保可重复性:
data = [(index, pred, actual, sum(s0), sum(s1)) for
        index, (pred, actual, s0, s1) in
        enumerate(zip(Y_pred, y_train_samples, shap_values[0], shap_values[1]))
        if pred != actual]
df = pd.DataFrame(data=data, columns=["index", "predictions","actuals", "shap_class_0", "shap_class_1"])
df.sample(5, random_state=42)
这将生成以下输出:

图 8.16 – 显示造成错误的 Shapley 值的 DataFrame
对于索引 7915,Shapley 值很接近,这意味着每个类别的特征对模型预测的贡献更接近 0,而对于索引 4255,Shapley 值与 0 相差较远,特征在预测每个类别时具有区分性。
由于我们可以提取每个类别的特征 SHAP 影响值,我们想知道 Shapley 值影响最高的行,以便我们可以从训练中消除这样的数据点;影响低且接近边界的地方,我们可以对数据进行过采样。
观察索引4255的力图,对于预期的类0,由于f(x)相当低,模型可能会预测1,而模型错误地预测了1,而预期类1的力图显示了f(x)值为0.7。这样的数据点可以从数据集中删除:
shap.force_plot(explainer.expected_value[0], shap_values[0][4255,:], X_train_samples.iloc[4255, :], matplotlib=True)
这将生成以下图表:

图 8.17 – 类 0 的力图
让我们看看类1的力图:
shap.force_plot(explainer.expected_value[1], shap_values[1][4255,:], X_train_samples.iloc[4255, :], matplotlib=True)
这将显示以下输出:

图 8.18 – 类 1 的力图
现在,我们计算行索引4255的 Shapley 影响,因为它是一个假阳性预测。行索引在 DataFrame 的422位置。我们取 Shapley 影响的绝对值,并在 Shapley 影响最高且预测错误的地方,删除这些值以提高模型性能:
index = 422
shap_impact = abs(df['shap_class_0'][index])
print(shap_impact)
0.4787916666666662
接下来,我们创建一个计算 Shapley 影响的函数。我们感兴趣的是那些单个特征具有至少0.2 Shapley 影响的行。首先,我们获取数组中每个特征的绝对影响,然后提取最大值。如果最大值大于0.2,我们继续处理该行。接下来,我们检查预测值与实际值不匹配的地方,并从这样的行中提取 SHAP 影响:
def get_shapley_impact(shap_value, threshold=0.2):
    """Calculate Shapley impact"""
    shap_value_impacts = np.abs(shap_value)
    if np.max(shap_value_impacts) >= threshold:
        return np.abs(np.sum(shap_value))
我们创建一个保留数据集,其中X_train将被进一步分为训练集和验证集。我们利用 80%进行训练,20%进行验证:
X_train_sample, X_val, y_train_sample, y_val, A_train_sample, A_val = train_test_split(X_train,
                   y_train,
                   A_train,
                   test_size=0.2,
                   stratify=y_train,
                   random_state=42)
然后,我们使用SMOTETomek重新采样数据集,这是公平性和性能方面最好的采样方法,通过将困难示例重新添加到数据集中来实现。一旦数据集被重新采样,我们就按照前面的步骤训练标准决策树,并在保留的验证数据集上计算 ROC 分数:
model = DecisionTreeClassifier(**d_tree_params)
scaler = StandardScaler()
sampler = SMOTETomek(random_state=42,
                     smote=SMOTE(random_state=42),
                     tomek=TomekLinks(n_jobs=-1)
                    )
columns = X_train_sample.columns
X_train_scaled = pd.DataFrame()
X_train_scaled[columns] = scaler.fit_transform(X_train_sample[columns])
X_train_resampled, y_train_resampled = sampler_method.fit_resample(X_train_scaled, y_train_sample)
X_train_resampled[columns] = scaler.inverse_transform(X_train_resampled)
A_train_resampled = X_train_resampled['SEX'].copy()
model.fit(X_train_resampled.drop(['SEX'], axis=1), y_train_resampled)
Y_pred = model.predict(X_train_resampled.drop(['SEX'], axis=1))
y_val_pred = model.predict_proba(X_val.drop(['SEX'], axis=1))[: ,1]
val_roc = roc_auc_score(y_val, y_val_pred)
print(f"Validation roc auc : {val_roc}")
Validation roc auc : 0.8275769341994823
我们在测试数据集上计算公平性和性能指标。均衡机会很高,但人口统计平等等比率在可接受范围内:
y_pred_proba = model.predict_proba(X_test.drop(['SEX'], axis=1))[:, 1]
y_pred = model.predict(X_test.drop(['SEX'], axis=1))
print(f"Roc: {roc_auc_score(y_test, y_pred_proba)}")
Roc: 0.8258396009334518
接下来,我们计算公平性指标:
calculate_fairness_metrics(y_test, y_pred, A_test)
这将打印出以下指标:

图 8.19 – 公平性指标
我们使用 SHAP 解释器提取 Shapley 值。我们确保已删除SEX特征:
explainer = shap.Explainer(model)
columns = X_train_resampled.drop(['SEX'], axis=1).columns
shap_values = explainer.shap_values(X_train_resampled[columns])
公平性指标表明,男性和女性子类之间的假阴性率差距更大,因此我们专注于使用 Shapley 值减少男性的假阴性案例。
在下一步中,我们提取模型预测为类0但真实值为1的 Shapley 值。因此,我们对类1的 Shapley 值感兴趣,因为在模型出错的地方,类1的 SHAP 影响高可能是一个模型无法正确预测的数据点:
shapley_impact_false_negative = [(i, get_shapley_impact(s), y, p, a)
                                for s, i, y, p, a
                                in zip(shap_values[1], X_train_resampled.index, y_train_resampled, Y_pred, A_train_resampled)
                                 if y == 1 and p == 0 and a == 1]
我们还对那些 Shapley 值和模型预测都与实际值一致的数据点感兴趣。因此,我们关注那些模型正确预测男性数据点的类别为0的数据点。一旦我们提取了这些数据点,我们就关注类别0的高影响力 Shapley 值,以便我们可以对这些数据进行过采样,从而使模型可以对这些数据点获得更好的信号:
shapley_impact_true_negative = [(i, get_shapley_impact(s), y, p, a)
                                for s, i, y, p, a
                                in zip(shap_values[0], X_train_resampled.index, y_train_resampled, Y_pred, A_train_resampled)
                                 if y == 0 and p == 0 and a == 1]
接下来,我们排序假阴性 Shapley 值,以便我们可以提取高影响力的数据点:
shapley_impact_false_negative_sorted = sorted([i for i in shapley_impact_false_negative if i[1] is not None], key=lambda x: x[1], reverse=True)
与前面类似,我们感兴趣的是具有高影响力的真阴性 Shapley 值,因此我们根据高影响力的 Shapley 值对列表进行排序:
shapley_impact_true_negative_sorted =  sorted([i for i in shapley_impact_true_negative if i[1] is not None], key=lambda x: x[1], reverse=True)
现在我们已经提取并排序了假阴性和真阴性男性数据点的 Shapley 值,我们从假阴性列表中挑选前 100 个数据点进行消除,并从真阴性列表中挑选前 100 个数据点重新添加到训练数据中。真阴性列表中的前 100 个数据点将被随机打乱,并且只随机添加其中的 50 个数据点,采用替换策略。我们鼓励从业者尝试另一个打乱比例。一旦确定了用于消除和重新引入到最终训练集中的数据点,我们就更新训练数据。这些数据被命名为X_train_final和y_train_final:
data_points_to_eliminate = [i[0] for i in shapley_impact_false_negative_sorted[0:100]]
data_points_to_add = [i[0] for i in shapley_impact_true_negative_sorted[0:100]]
X_train_added = X_train_resampled[columns].iloc[data_points_to_add].sample(frac=0.5, replace=True, random_state=42, axis=0)
y_train_added = y_train_resampled.iloc[data_points_to_add].sample(frac=0.5, replace=True, random_state=42, axis=0)
X_train_reduced = X_train_resampled[columns].drop(data_points_to_eliminate)
y_train_reduced = y_train_resampled.drop(data_points_to_eliminate)
X_train_final = pd.concat([X_train_reduced, X_train_added], axis=0, ignore_index=True)
y_train_final = pd.concat([y_train_reduced, y_train_added], axis=0, ignore_index=True)
接下来,我们训练更新的训练数据,并在测试数据上计算公平性指标和性能指标。很明显,假阴性率之间的差距已经减少,均衡的几率比已提高到 0.082,ROC 分数从先前的 0.825 略微提高到 0.826:
estimator = DecisionTreeClassifier(**d_tree_params)
model = estimator.fit(X_train_final, y_train_final)
y_pred_proba = model.predict_proba(X_test.drop(['SEX'], axis=1))[:, 1]
y_pred = model.predict(X_test.drop(['SEX'], axis=1))
print(f"Roc: {roc_auc_score(y_test, y_pred_proba)}")
Roc: 0.8262453372973797
我们重新计算公平性指标:
calculate_fairness_metrics(y_test, y_pred, A_test)
输出结果如下:

图 8.20 – 公平性指标
既然我们已经确定通过使用 Shapley 值可以识别难以分类且易于分类的数据点,我们可以构建一个自动机制来迭代数据点,从而可以达到比之前更好的公平性分数。
接下来,我们创建一系列百分比以进行迭代,这样我们就可以利用并排序消除和重新引入的前数据点的百分比。我们将使用 NumPy 的linspace方法创建一个百分比值的列表以进行迭代。我们从0.05到0.5(5%到 50%)中选择 10 个值。我们称这个列表为perc_points_to_eliminate:
perc_points_to_eliminate = np.linspace(0.05,0.5,10)
perc_points_to_eliminate
array([0.05, 0.1 , 0.15, 0.2 , 0.25, 0.3 , 0.35, 0.4 , 0.45, 0.5 ])
我们遍历这些百分比,并重复先前的步骤,其中我们消除了一些值并重新引入了一些值。然而,这次,我们使用百分比来移除顶部百分比的数据点或引入顶部百分比的数据点。
我们还创建了一个空的数据列表,因此,对于每次迭代,我们捕获消除或重新引入的数据点的百分比、测试数据中的假阴性率和假阳性率、均衡的几率比和人口比例比。
一旦我们遍历了所有值 - 10*10 次迭代,我们将它们存储在一个 DataFrame 中,以查看需要移除多少数据点以及需要添加多少数据点才能达到最佳的公平性指标:
fn_examples = len(shapley_impact_false_negative)
tn_examples = len(shapley_impact_true_negative)
model = DecisionTreeClassifier(**d_tree_params)
data = []
for fnp in perc_points_to_eliminate:
    data_points_to_eliminate = [idx[0] for idx in shapley_impact_false_negative_sorted[0:(round(fn_examples*fnp))]]
    for tnp in perc_points_to_eliminate:
        data_points_to_add = [idx[0] for idx in shapley_impact_true_negative_sorted[0:(round(tn_examples*tnp))]]
        X_train_added = X_train_resampled[columns].iloc[data_points_to_add].sample(frac=0.5, replace=True, random_state=42, axis=0)
        y_train_added = y_train_resampled.iloc[data_points_to_add].sample(frac=0.5, replace=True, random_state=42, axis=0)
        X_train_reduced = X_train_resampled[columns].drop(data_points_to_eliminate)
        y_train_reduced = y_train_resampled.drop(data_points_to_eliminate)
        X_train_final = pd.concat([X_train_reduced, X_train_added], axis=0, ignore_index=True)
        y_train_final = pd.concat([y_train_reduced, y_train_added], axis=0, ignore_index=True)
        model.fit(X_train_final, y_train_final)
        y_pred = model.predict(X_test.drop(['SEX'], axis=1))
        fpr = false_positive_rate(y_test, y_pred)
        fnr = false_negative_rate(y_test, y_pred)
        equalized_odds_mitigated = equalized_odds_difference(
            y_test, y_pred, sensitive_features=A_test
        )
        demographic_parity_ratio_mitigated = demographic_parity_ratio(y_test, y_pred, sensitive_features=A_test)
        data.append((fnp,
                     tnp,
                     fpr,
                     fnr,
                     equalized_odds_mitigated,
                     demographic_parity_ratio_mitigated
                    ))
接下来,我们创建一个名为 df_shapley 的 DataFrame,用于存储每个迭代的元数据,并按均衡机会比进行排序:
columns = ["perc_false_negative_removed",
          "perc_true_negative_added",
          "false_positive_rate",
          "false_negative_rate",
          "equalized_odds_mitigated",
          "demographic_parity_ratio_mitigated"]
df_shapley = pd.DataFrame(data=data, columns=columns)
df_shapley.sort_values(by="equalized_odds_mitigated")
这将输出以下 DataFrame:

图 8.21 – 排序后的 df_shapley DataFrame
显然,当移除顶部 25% 的误报数据点并重新引入顶部 30% 的真实负数据点时,模型可以达到均衡机会比 0.074,以及最佳人口统计平等等级比得分 0.85。
最后,我们提取顶部百分比并训练最终模型:
top_values = df_shapley.sort_values(by="equalized_odds_mitigated").values[0]
perc_false_negative_removed = top_values[0]
perc_true_negative_added = top_values[1]
columns = X_train_resampled.drop(['SEX'], axis=1).columns
data_points_to_eliminate = [i[0] for i in shapley_impact_false_negative_sorted[0:(round(fn_examples*perc_false_negative_removed))]]
data_points_to_add = [i[0] for i in shapley_impact_true_negative_sorted[0:(round(tn_examples*perc_true_negative_added))]]
X_train_added = X_train_resampled[columns].iloc[data_points_to_add].sample(frac=0.5, replace=True, random_state=42, axis=0)
y_train_added = y_train_resampled.iloc[data_points_to_add].sample(frac=0.5, replace=True, random_state=42, axis=0)
X_train_reduced = X_train_resampled[columns].drop(data_points_to_eliminate)
y_train_reduced = y_train_resampled.drop(data_points_to_eliminate)
X_train_final = pd.concat([X_train_reduced, X_train_added], axis=0, ignore_index=True)
y_train_final = pd.concat([y_train_reduced, y_train_added], axis=0, ignore_index=True)
我们训练模型并计算公平性和模型指标。我们可以看到,对于 female 的误报率有所增加,但 male 和 female 之间的差距已经减小,而 male 的误报率有所降低。ROC 得分达到 0.82,但根据两个公平性指标,模型更加公平:
estimator = DecisionTreeClassifier(**d_tree_params)
model = estimator.fit(X_train_final, y_train_final)
y_pred_proba = model.predict_proba(X_test.drop(['SEX'], axis=1))[:, 1]
y_pred = model.predict(X_test.drop(['SEX'], axis=1))
print(f"Roc: {roc_auc_score(y_test, y_pred_proba)}")
Roc: 0.820911097453972
最后,我们计算公平性指标。
calculate_fairness_metrics(y_test, y_pred, A_test)
这将打印出以下内容:

图 8.22 – 公平性指标
现在我们已经探索了通过提高数据质量来减少偏差的不同数据中心技术,我们鼓励您尝试之前的技巧,并尝试这些技巧的组合。一旦您用尽了这些数据中心方法,我们鼓励您使用一些模型中心方法,例如利用公平性感知的算法,尝试集成方法、AutoML 或遍历您自己的算法列表。
摘要
本章广泛探讨了机器学习中普遍存在的偏差挑战。它从解释机器学习模型中固有的各种偏差形式开始,并检查了它们对各个行业的影响。重点是识别、监控和减轻偏差,强调收集具有最小选择和抽样偏差的数据的重要性。
该主题倡导在解决偏差问题时,以数据为中心的强制措施优于以模型为中心的强制措施。探讨了过采样、欠采样、特征选择增强和异常检测等技术,用于偏差校正。Shapley 值在偏差识别中起着至关重要的作用,强调移除具有不匹配高 Shapley 值的示例,并通过替换重新引入数据点以改善比率。根据敏感变量(如 SEX)对误分类示例进行分层,以实现有针对性的偏差校正。
本章通过强调对敏感变量进行数据集精炼和平衡的重要性作为基础步骤。它建议在数据集本身得到改进后,转向以模型为中心的方法,例如集成和公平性算法。这些后续的以模型为中心的策略旨在提高性能和公平性指标,为更通用和公平的人工智能模型奠定基础。
这种全面的方法旨在创建一个平衡的数据集,作为应用以模型为中心的技术的前奏,以促进人工智能系统中的性能和公平性。
第九章:在机器学习中处理边缘案例和罕见事件
在机器学习领域,正确识别和处理边缘情况至关重要。边缘情况指的是与数据集大多数数据显著不同的实例,它们可能会对机器学习模型的性能和可靠性产生重大影响。由于类别不平衡问题,罕见事件可能对机器学习模型具有挑战性,因为它们可能没有足够的数据来有效地学习模式。类别不平衡发生在某一类别(罕见事件)相对于其他类别(们)显著代表性不足时。传统的机器学习算法在这种场景下往往表现不佳,因为它们可能偏向于多数类别,导致在识别罕见事件时准确性降低。
在本章中,我们将探讨使用 Python 代码示例检测机器学习和数据中边缘案例的各种技术和方法。我们将探讨统计技术,包括使用可视化和其他措施如 Z 分数来分析数据分布并识别潜在的异常值。我们还将关注隔离森林和半监督方法,如自编码器,以揭示数据集中的异常和不规则模式。我们将学习如何通过过采样、欠采样和生成合成数据等技术来解决类别不平衡并提高模型性能。
在本章的后半部分,我们将了解调整学习过程以考虑不平衡类别的必要性,特别是在罕见事件具有重大后果的场景中。我们将探讨选择适当的评估指标的重要性,强调那些考虑类别不平衡的指标,以确保对模型性能的公平和准确评估。最后,我们将了解集成方法,如 bagging、boosting 和 stacking 如何帮助我们增强模型的鲁棒性,特别是在罕见事件发挥关键作用的场景中。
以下关键主题将涵盖:
- 
在机器学习中检测罕见事件和边缘案例的重要性
 - 
统计方法
 - 
异常检测
 - 
数据增强和重采样技术
 - 
成本敏感学习
 - 
选择评估指标
 - 
集成技术
 
在机器学习中检测罕见事件和边缘案例的重要性
在机器学习中,检测罕见事件和边缘情况至关重要,原因有以下几点:
- 
关键场景中的决策:罕见事件通常代表需要立即关注或特殊处理的临界场景或异常情况。例如,在医疗诊断中,罕见疾病或极端病例可能需要紧急干预。准确检测这些事件可以导致更好的决策并防止不良后果。
 - 
不平衡数据集:许多现实世界的数据集存在类别不平衡的问题,其中一个类别(通常是罕见事件)与其他类别相比显著代表性不足。这可能导致在少数类别上表现不佳的偏见模型。检测罕见事件有助于确定需要特殊处理的需求,例如使用重采样技术或采用适当的评估指标以确保公平评估。
 - 
欺诈检测:在欺诈检测应用中,罕见事件通常对应于欺诈交易或活动。检测这些罕见案例对于防止财务损失和确保金融系统的安全至关重要。
 - 
质量控制与异常检测:在制造和工业过程中,检测罕见事件可以帮助识别有缺陷或异常的产品或流程。这使及时干预成为可能,从而提高产品质量并保持运营效率。
 - 
预测性维护:在预测性维护中,检测边缘案例可以表明潜在的设备故障或异常行为,从而允许主动维护以减少停机时间并提高生产力。
 - 
模型泛化:通过准确识别和处理罕见事件,机器学习模型可以更好地泛化到未见数据,并有效地处理现实世界场景。
 - 
客户行为分析:在营销和客户分析中,检测罕见事件可以揭示有趣的异常模式或行为,例如识别高价值客户或检测潜在的流失者。
 - 
安全和入侵检测:在网络安全领域,罕见事件可能表明安全漏洞或网络攻击。实时检测和应对这些事件对于确保数字系统的安全和完整性至关重要。
 - 
环境监测:在环境应用中,罕见事件可能表明不寻常的生态条件或自然灾害。检测此类事件有助于灾害准备和环境监测工作。
 
让我们讨论不同的统计方法来分析数据分布并识别潜在的异常值。
统计方法
统计方法为我们识别数据中的异常值和异常情况提供了宝贵的工具,有助于数据预处理和决策制定。在本节中,我们将讨论如何使用诸如 Z 分数、四分位数范围(IQR)、箱线图和散点图等方法来揭示数据中的异常情况。
Z 分数
Z 分数,也称为标准分数,是一种统计量,表示数据点与数据均值之间的标准差数。Z 分数用于标准化数据,并允许在不同数据集之间进行比较,即使它们有不同的单位或尺度。它们在检测异常值和识别数据集中的极端值方面特别有用。计算数据集中数据点 x 的 Z 分数的公式如下:
Z = (x − μ) / σ
在这里,以下规则适用:
- 
Z 是数据点 x 的 Z 分数
 - 
x 是数据点的值
 - 
μ 是数据集的均值
 - 
σ 是数据集的标准差
 
Z 分数广泛用于检测数据集中的异常值。具有超出一定阈值(例如,Z > 3 或 Z < -3)的 Z 分数的数据点被认为是异常值。这些异常值可能代表数据中的极端值或测量误差。通过将数据转换为 Z 分数,均值变为 0,标准差变为 1,从而得到一个标准化的分布。
Z 分数用于正态性检验,以评估数据集是否遵循正态(高斯)分布。如果数据集遵循正态分布,则大约 68% 的数据点的 Z 分数应在 -1 和 1 之间,约 95% 在 -2 和 2 之间,几乎所有数据点都在 -3 和 3 之间。在假设检验中,Z 分数用于计算 p-值并对总体参数进行推断。
例如,当总体标准差已知时,Z 测试通常用于样本均值比较。Z 分数在异常检测中很有用,当我们想要识别与规范显著偏离的数据点时。高 Z 分数可能表明数据中的异常行为或罕见事件。
让我们使用 Loan Prediction 数据集在 Python 中探索这个概念。让我们首先加载数据集:
import pandas as pd
import numpy as np
df = pd.read_csv('train_loan_prediction.csv')
df.head().T
在这里,我们看到样本数据集:

图 9.1 – df DataFrame
现在我们已经加载了数据集,我们可以在一些数值特征上计算 Z 分数:
numerical_features = ['ApplicantIncome', 'CoapplicantIncome', 'LoanAmount']
z_scores = df[numerical_features].apply(lambda x: (x - np.mean(x)) / np.std(x))
我们利用 Z 分数有效地检测数据集中的异常值:
threshold = 3
outliers = (z_scores > threshold) | (z_scores < -threshold)
outliers['is_outlier'] = outliers.any(axis=1)
outlier_rows = df[outliers['is_outlier']]
outlier_rows
通过设置 Z 分数阈值为 3,我们可以通过检查是否有任何行的值超出 Z 分数边界(大于 3 或小于 -3)来识别异常值。这种方法使我们能够确定与均值显著偏离的数据点,并使我们能够采取适当的措施来处理这些异常值,确保数据的完整性和后续分析和模型的准确性。
使用这种方法得到的输出 DataFrame 包含异常值如下:

图 9.2 – 结果的 outlier_rows DataFrame
总结来说,Z 分数使得准确识别异常值变得更加容易。它们作为一个有用的工具,帮助我们更好地理解数据模式。在下一节关于四分位数的讨论中,我们将进一步探讨检测和解决异常值的替代方法。
四分位距(IQR)
IQR 是一种用于描述数据集分布或分散程度的统计量。它在识别和处理异常值以及理解数据的中心趋势方面特别有用。IQR 定义为数据集的第一四分位数(Q1)和第三四分位数(Q3)之间的范围。四分位数是将数据集分为四个相等部分,每个部分包含 25%的数据的点。
可以使用以下公式计算 IQR:
IQR = Q3 − Q1
在这里,以下规则适用:
- 
Q1 是第一四分位数(25 百分位数),表示低于此值的 25%的数据。
 - 
Q3 是第三四分位数(75 百分位数),表示低于此值的 75%的数据。
 
IQR 通常用于识别数据集中的异常值。低于 Q1 - 1.5 * IQR 或高于 Q3 + 1.5 * IQR 的数据点被认为是异常值,可能需要进一步调查。IQR 提供了关于数据分布的有价值信息。它有助于理解数据集中间 50%的分布,可以用来评估分布的对称性。在比较数据集时,IQR 可以用来评估两个或更多数据集之间数据分散的差异。
下面是一个简单的例子,说明如何使用 Python 和 NumPy 在贷款预测数据集上计算数据集的 IQR。首先进行异常值检测,我们计算数值特征的 IQR。通过定义一个阈值,通常是 Tukey 方法中 IQR 的 1.5 倍,该方法涉及计算 IQR 为第三四分位数(Q3)和第一四分位数(Q1)之间的差值,我们可以识别异常值。Tukey 方法是一种稳健的统计技术,有助于检测与整体分布显著偏离的数据点,为识别数据集中的潜在异常提供了一个可靠的度量。一旦所有数值特征都被标记为异常值,我们就可以显示具有异常值的行,这有助于进一步分析和潜在的数据处理。使用 IQR 和 Tukey 方法一起可以促进有效的异常值检测,并有助于确保数据集的完整性:
Q1 = df[numerical_features].quantile(0.25)
Q3 = df[numerical_features].quantile(0.75)
IQR = Q3 - Q1
threshold = 1.5
outliers = (df[numerical_features] < (Q1 - threshold * IQR)) | (df[numerical_features] > (Q3 + threshold * IQR))
outliers['is_outlier'] = outliers.any(axis=1)
outlier_rows = df[outliers['is_outlier']]
outlier_rows
下面是使用此方法得到的异常值输出 DataFrame:

图 9.3 – 结果的 outlier_rows DataFrame
总之,探索四分位数间距(IQR)为识别异常值提供了一种有价值的技术,尤其是在捕捉数据集的中心趋势方面特别有效。虽然 IQR 在关注中间范围至关重要的情况下具有优势,但认识到它在捕捉整个数据分布方面的局限性是至关重要的。在接下来的关于箱线图的章节中,我们将深入研究一种补充 IQR 的图形表示,提供一种在多种情境下更好地理解数据分散和异常值的视觉工具。
箱线图
箱线图,也称为箱形图,是数据集分布的图形表示。它们提供了一种快速且信息丰富的可视化方式,可以直观地查看数据的分布和偏斜,识别潜在的异常值,并比较多个数据集。箱线图在处理连续数值数据时特别有用,可以用来深入了解数据的中心趋势和变异性。
这里是箱线图的组成部分:
- 
箱线图(IQR):箱线图表示 IQR,即数据的第一四分位数(Q1)和第三四分位数(Q3)之间的范围。它覆盖了数据集的中间 50%,并提供了数据分布的视觉表示。
 - 
中位数(Q2):中位数,由箱内的水平线表示,指示数据集的中心值。它将数据分为两个相等的部分,其中 50%的数据点低于中位数,50%的数据点高于中位数。
 - 
须:须从箱的边缘延伸到位于“须长度”内的最远数据点。须的长度通常由一个因子(例如,IQR 的 1.5 倍)确定,并用于识别潜在的异常值。
 - 
异常值:位于须外的数据点被认为是异常值,通常单独作为单独的点或圆圈绘制。它们是显著偏离中心分布的数据点,可能需要进一步调查。
 
以下是一些箱线图的优点:
- 
可视化数据分布:箱线图提供了一种直观的方式来查看数据的分布和偏斜,以及识别任何潜在的数据簇或缺口。
 - 
比较数据集:箱线图在并排比较多个数据集时很有用,允许轻松比较中心趋势和变异性。
 - 
异常值检测:箱线图通过突出显示位于须外的数据点来促进异常值检测,有助于识别异常或极端值。
 - 
处理偏斜数据:箱线图对极端值的影响具有鲁棒性,并且比传统的均值和标准差更有效地处理偏斜数据分布。
 
让我们使用 Python 中的matplotlib库来实现这种方法。在这里,我们将为ApplicantIncome和LoanAmount生成两个箱线图:
import matplotlib.pyplot as plt
%matplotlib inline
plt.figure(figsize=(10, 6))
plt.subplot(2, 1, 1)
df.boxplot(column='ApplicantIncome')
plt.title('Box Plot - ApplicantIncome')
plt.subplot(2, 1, 2)
df.boxplot(column='LoanAmount')
plt.title('Box Plot - LoanAmount')
plt.tight_layout()
plt.show()
这里是绘图输出。我们看到在这些列中存在一些可能的异常值:

图 9.4 – ApplicantIncome(顶部)和 LoanAmount(底部)的箱线图
总结箱线图的使用,我们发现它们是一种强大的视觉辅助工具,与四分位数范围(IQR)一起描绘了数据的中心趋势和数据分散。虽然箱线图在提供整体视图方面表现出色,但重要的是要认识到它们可能无法捕捉到复杂数据集的所有细微差别。在下一节中,我们将探讨一种多功能的图形工具,它提供了一个更广阔的视角,有助于识别数据中变量之间的关系和模式。
散点图
散点图是一种流行且多功能的数据可视化技术,用于探索两个连续数值变量之间的关系。它们提供了一个清晰的视觉表示,说明一个变量(自变量)如何影响或影响另一个变量(因变量)。散点图在识别数据中的模式、相关性、簇和异常值方面特别有用,是数据分析中探索性数据分析(EDA)的一个基本工具。
现在我们来探讨散点图的构建及其关键特征。
散点图构建
要创建散点图,需要将两个数值变量的值绘制在笛卡尔坐标系上的点。每个点代表一个数据观测值,其中x坐标对应自变量的值,y坐标对应因变量的值。多个数据点共同形成一个散点图,可以提供关于两个变量之间关系的见解。
散点图的关键特征
以下是一些散点图的关键特征:
- 
相关性:散点图帮助我们评估两个变量之间的相关性或关系。如果图上的点似乎形成了一个清晰的趋势或模式(例如,线性或非线性趋势),则表明变量之间存在显著的相关性。如果点分布随机,可能没有或相关性较弱。
 - 
聚类分析:散点图可以揭示数据点的簇,表明数据中可能存在的子组或模式。
 - 
异常值检测:散点图通过识别远离主要数据点群的数据点来促进异常值检测。
 - 
数据分布:数据点在x轴和y轴上的分布提供了关于变量变异性的见解。
 - 
可视化回归线:在某些情况下,可以将回归线拟合到散点图上,以模拟变量之间的关系并做出预测。
 
让我们在 Python 的ApplicantIncome和LoanAmount列上实现这一功能:
plt.figure(figsize=(8, 6))
plt.scatter(df['ApplicantIncome'], df['LoanAmount'])
plt.xlabel('ApplicantIncome')
plt.ylabel('LoanAmount')
plt.title('Scatter Plot - ApplicantIncome vs. LoanAmount')
plt.show()
下面是展示散点图结果的输出:

图 9.5 – 展示申请收入和贷款金额的散点图
正如你所见,一些点明显偏离了大多数人群,这表明可能存在潜在的异常值。这些异常数据点与整体模式相区别,需要进一步调查以了解其重要性和对两个变量之间关系的影响。识别和处理异常值对于确保准确的数据分析和模型性能至关重要。通过可视化散点图,我们可以获得关于数据分布和关联的有价值见解,为有效的决策和数据探索铺平道路。
异常检测
异常检测是检测罕见事件的一种特定方法,其重点是识别与规范或正常行为显著偏离的实例。异常可能由罕见事件、错误或数据集中不典型的异常模式引起。当罕见事件的数据有限或没有标记数据时,这种技术特别有用。常见的异常检测算法包括以下:
- 
无监督方法:例如隔离森林和单类 SVM等技术可以在不要求罕见事件的标记示例的情况下用于识别数据中的异常。
 - 
半监督方法:这些方法在训练期间结合正常和异常数据,但只有有限数量的标记异常。自编码器和变分自编码器是半监督异常检测算法的例子。
 - 
监督方法:如果可用少量标记的异常,可以使用监督学习算法,如随机森林、支持向量机(SVM)和神经网络进行异常检测。
 
让我们通过 Python 代码示例详细了解这些方法。
使用隔离森林的无监督方法
隔离森林是一种用于无监督学习场景中异常检测的高效且有效的算法。它通过构建隔离树(随机决策树)来隔离数据中的异常或罕见事件,这些树将异常与大多数正常数据点分开。它是由 Fei Tony Liu、Kai Ming Ting 和 Zhi-Hua Zhou 在 2008 年发表的论文《Isolation Forest》中引入的。以下是隔离森林算法的一些关键概念和特性:
- 
随机划分:隔离森林使用随机划分策略来创建隔离树。在构建树的每个步骤中,选择一个随机特征,并在所选特征的值范围内选择一个随机分割值来创建一个节点。这种随机划分导致异常的路径变短,使得它们更容易从正常数据点中隔离出来。
 - 
路径长度:Isolation Forest 背后的关键思想是异常被隔离到包含较少数据点的较小分区中,而正常数据点在较大的分区中分布得更均匀。数据点到树中异常的平均路径长度被用作其“隔离”的度量。
 - 
异常分数:基于平均路径长度,每个数据点被分配一个异常分数。异常分数表示数据点被隔离或从其余数据中分离的难易程度。较短的平均路径长度对应于较高的异常分数,表明数据点更有可能是异常。
 - 
auto,它根据数据集的大小估计污染率。 
这里列出了 Isolation Forest 的一些优点:
- 
Isolation Forest 计算效率高且可扩展,使其适用于大型数据集。它不需要大量的树来实现良好的性能,从而减少了计算开销。
 - 
该算法对维度/特征的数量相对不敏感,这在处理高维数据集时尤其有利。Isolation Forest 是一种无监督学习算法,使其适用于标签异常数据稀缺或不可用的情况。
 
然而,Isolation Forest 也有一些局限性:
- 
Isolation Forest 可能在具有多个异常簇的数据集上表现不佳,或者当异常值接近大多数正常数据点时。
 - 
与大多数无监督算法一样,Isolation Forest 可能会产生假阳性(将正常数据点错误地分类为异常)和假阴性(将异常错误地分类为正常数据点)。
 
让我们用以下步骤在 Python 中实现这种方法:
- 
将
pandas库导入为pd以处理表格格式的数据,并从sklearn.ensemble模块导入IsolationForest以使用 Isolation Forest 算法进行异常检测:Import pandas as pd from sklearn.ensemble import IsolationForest - 
定义了
numerical_features列表,包含用于异常检测的数值列的名称。这些列是'ApplicantIncome'、'CoapplicantIncome'和'LoanAmount':numerical_features = ['ApplicantIncome', 'CoapplicantIncome', 'LoanAmount'] - 
X_anomalyDataFrame 是通过从原始的dfDataFrame 中提取numerical_features中指定的列创建的。这个新的 DataFrame 将被用于异常检测:X_anomaly = df[numerical_features] - 
X_anomalyDataFrame,使用每列的平均值来调用fillna()方法。这确保了任何缺失值都被其各自列的平均值所替换:X_anomaly.fillna(X_anomaly.mean(), inplace=True) - 
IsolationForest(contamination='auto', random_state=42)。将contamination参数设置为'auto',这意味着它将自动检测数据集中异常值的百分比。将random_state参数设置为42以确保可重复性:Isolation_forest = IsolationForest(contamination='auto', random_state=42) - 
使用
fit_predict()方法对X_anomaly数据进行拟合。此方法同时将模型拟合到数据并预测每个数据点是否为异常。预测存储在anomaly_predictions数组中。 - 
anomaly_predictions数组包含每个数据点的预测标签:-1表示异常(异常值)和1表示内点(非异常值)。这些预测被添加为新的'IsAnomaly'列到原始的dfDataFrame 中。 - 
dfDataFrame 中IsAnomaly等于-1,表示存在异常值。结果 DataFrame 包含所有具有异常的行,然后可以根据需要进行进一步分析或处理:anomaly_predictions = Isolation_forest.fit_predict(X_anomaly) df['IsAnomaly'] = anomaly_predictions anomalies = df[df['IsAnomaly'] == -1] anomalies.head() 
现在我们可以查看模型预测为异常的行所组成的 DataFrame:

图 9.6 – 结果异常 DataFrame
总结来说,隔离森林(Isolation Forest)作为一种强大且高效的异常检测工具,在异常数据稀少且与正常实例明显不同的情况下尤为突出。它通过创建随机树来隔离异常数据的能力,使其在从欺诈检测到网络安全等众多应用中成为一种宝贵的资源。然而,承认算法的局限性也是至关重要的。当异常数据没有很好地分离或数据集高度维度化时,隔离森林可能会面临挑战。在下一节中,我们将探讨自动编码器(autoencoders)——一种用于异常检测的半监督方法。
使用自动编码器的半监督方法
使用自动编码器进行异常检测是一种无监督学习方法,它利用神经网络(NNs)来检测数据中的异常。自动编码器是一种神经网络架构,旨在从压缩表示中重建输入数据。在异常检测中,我们利用自动编码器在重建异常实例方面存在困难这一事实,这使得它们在识别异常模式或异常值方面非常有用。
自动编码器由两个主要组件组成:编码器和解码器。编码器将输入数据压缩成称为“潜在空间”的更低维度的表示,而解码器则试图从这个表示中重建原始输入。编码器和解码器通常是对称的,网络被训练以最小化重构误差。
在异常检测中,我们使用没有异常的正常数据来训练自动编码器。由于自动编码器学会重建正常数据,因此它将难以重建异常数据,导致异常实例的重构误差更高。这一特性使我们能够将重构误差用作异常分数。
在训练过程中,我们比较原始输入(例如,数值特征)与重建输出。两者之间的差异是重建误差。低重建误差表明输入接近正常数据分布,而高重建误差则表明输入可能是异常。
在训练自动编码器后,我们需要设置一个阈值来区分正常和异常实例,基于重建误差。有几种方法可以设置阈值,例如基于百分位数或使用验证数据。阈值将取决于所需的假阳性与假阴性之间的权衡,这可以根据应用程序的要求进行调整。
自动编码器灵活且能够捕捉数据中的复杂模式,这使得它们适用于具有非线性关系的多维数据。它们可以处理全局和局部异常,这意味着它们可以检测与大多数数据点不同的异常以及数据特定区域内的异常。自动编码器能够进行无监督学习,这在标记异常数据有限或不可用的情况下具有优势。与其他无监督方法一样,自动编码器可能会产生假阳性(将正常数据错误地分类为异常)和假阴性(将异常数据错误地分类为正常数据)。它们可能难以检测与正常数据非常相似的异常,因为重建误差可能没有显著差异。
让我们通过使用 TensorFlow 库和 Python 中的 Loan Prediction 数据集来查看这种方法的一个示例实现:
- 
使用
pd.read_csv()读取train_loan_prediction.csv文件:import pandas as pd import numpy as np from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from tensorflow.keras.layers import Input, Dense from tensorflow.keras.models import Model df = pd.read_csv('train_loan_prediction.csv') - 
包含用于异常检测的数值列名称的
numerical_features列表。这些列是'ApplicantIncome'、'CoapplicantIncome'和'LoanAmount':numerical_features = ['ApplicantIncome', 'CoapplicantIncome', 'LoanAmount'] - 
通过从原始
dfDataFrame 中提取numerical_features中指定的列创建X_anomalyDataFrame。这个新的 DataFrame 将用于异常检测。 - 
使用
fillna()方法将X_anomalyDataFrame 中的列替换为其各自列的平均值:X_anomaly = df[numerical_features] X_anomaly.fillna(X_anomaly.mean(), inplace=True) - 
使用
StandardScaler()对X_anomaly进行标准化。标准化将特征缩放到具有零均值和单位方差,这对于训练机器学习模型很重要:original_indices = X_anomaly.index scaler = StandardScaler() X_anomaly_scaled = scaler.fit_transform(X_anomaly) - 
使用
train_test_split()函数对 (X_train) 和测试 (X_test) 集进行划分。数据原始索引也存储在original_indices中:X_train, X_test, _, _ = train_test_split(X_anomaly_scaled, original_indices, test_size=0.2, random_state=42) X_test_df = pd.DataFrame(X_test, columns=['ApplicantIncome', 'CoapplicantIncome', 'LoanAmount']) - 
Dense层。模型使用fit()方法进行训练,均方误差(MSE)作为损失函数:input_dim = X_anomaly.shape[1] encoding_dim = 2 input_layer = Input(shape=(input_dim,)) encoder_layer = Dense(encoding_dim, activation='relu')(input_layer) decoder_layer = Dense(input_dim, activation='sigmoid')(encoder_layer) autoencoder = Model(inputs=input_layer, outputs=decoder_layer) autoencoder.compile(optimizer='adam', loss='mean_squared_error') autoencoder.fit(X_anomaly_scaled, X_anomaly_scaled, epochs=50, batch_size=16) - 
X_test,并计算重建误差为原始数据和重建数据之间的均方差异:X_test_reconstructed = autoencoder.predict(X_test) reconstruction_error_test = np.mean(np.square(X_test - X_test_reconstructed), axis=1) - 
X_test:threshold = np.percentile(reconstruction_error_test, 95) - 
如果
anomaly_predictions中的值为1,否则将其分配为0:anomaly_predictions = (reconstruction_error_test > threshold).astype(int) - 
使用
anomaly_dfDataFrame 创建包含异常预测和对应索引的X_test_df:anomaly_df = pd.DataFrame({'IsAnomaly': anomaly_predictions}, index=X_test_df.index) - 
使用
merge()方法将'IsAnomaly'列添加到dfDataFrame 中。 - 
df中的'IsAnomaly'列存在。如果存在,它将显示'IsAnomaly'等于1的行,表示存在异常。如果不存在,它将打印"No anomalies detected.":df = df.merge(anomaly_df, how='left', left_index=True, right_index=True) if 'IsAnomaly' in df.columns: # Display the rows with anomalies anomalies = df[df['IsAnomaly'] == 1] anomalies else: print("No anomalies detected.")结果的 DataFrame 如下所示:
 

图 9.7 – 结果的 IsAnomaly DataFrame
总结来说,自动编码器证明是异常检测中的一种多才多艺且强大的工具,能够捕捉到传统方法可能无法捕捉到的细微模式。它们在复杂数据结构中发现细微异常的能力使它们在包括图像分析、网络安全和工业质量控制在内的多个领域变得非常有价值。
然而,自动编码器的有效性取决于各种因素。架构的复杂性和超参数的选择会影响性能,需要仔细调整以获得最佳结果。在下一节中,我们将了解 SVM 如何用于异常检测。
使用 SVM 的监督方法
SVM 是一类常用的监督学习算法,常用于分类任务。当应用于异常检测时,SVM 通过找到一个具有最大边缘的超平面来有效地将正常实例与异常实例分开。以下是 SVM 在底层的工作原理:
- 
超平面定义:在二维空间中,超平面是一个平坦的二维子空间。SVM 的目标是找到一个超平面,将数据集最好地分为两类——正常和异常。这个超平面定位以最大化边缘,即超平面与每个类最近的点的距离。
 - 
决策边界:超平面充当决策边界,将一类实例与另一类实例分开。在二元分类场景中,超平面一侧的实例被分类为属于一个类别,而另一侧的实例被分类为属于另一个类别。
 - 
核技巧:SVM 通过使用核函数来处理数据中的复杂关系。在许多实际场景中,特征之间的关系可能不是线性的。SVM 通过使用核函数来解决这个问题。这个函数将输入数据转换到更高维的空间,使得找到能够有效分离类别的超平面变得更容易。常用的核函数包括线性核(用于线性可分数据)、多项式核、径向基函数(RBF)或高斯核,以及 sigmoid 核。核函数的选择取决于数据的性质。
 - 
最优超平面:SVM 旨在找到最大化边界的超平面,这是超平面与每个类最近数据点之间的距离。边界越大,模型可能越稳健和可泛化。支持向量是位于决策边界最近的数据点。它们在定义最优超平面和边界中起着至关重要的作用。SVM 在训练过程中专注于这些支持向量。
 
让我们通过我们的贷款预测数据集实现一个用于异常检测的 SVM 的 Python 示例:
- 
使用
pd.read_csv()读取train_loan_prediction.csv文件:import pandas as pd from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.svm import OneClassSVM from sklearn.metrics import classification_report, accuracy_score df = pd.read_csv('train_loan_prediction.csv') - 
数据预处理:我们将进行一些基本的数据预处理任务,包括处理缺失值。对于这个异常检测示例,我们通过排除分类变量简化了分析。在更复杂的分析中,如果你认为这些变量与你的特定异常检测任务相关,你可能选择编码并包含这些变量:
df = df.drop(['Loan_ID', 'Gender', 'Married', 'Dependents', 'Education', 'Self_Employed', 'Property_Area'], axis=1) df['Loan_Status'] = df['Loan_Status'].map({'Y': 0, 'N': 1}) df.fillna(df.mean(), inplace=True) - 
为 SVM 模型创建训练-测试分割:
X = df.drop('Loan_Status', axis=1) y = df['Loan_Status'] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) - 
使用 scikit-learn 的
StandardScaler标准化特征,以确保所有特征具有相同的尺度:scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) - 
训练 One-Class SVM 模型进行异常检测。根据你数据集中预期的异常比例调整 nu 参数。nu 参数代表边界错误分数的上限和支撑向量分数的下限。它实际上控制了算法应考虑的异常或异常的比例。选择合适的 nu 值至关重要,并且它取决于你数据集的特征和预期的异常比例。以下是一些帮助你选择 nu 参数的指南:
- 
理解异常的性质:评估你的数据集的领域知识和特征。了解预期的异常比例。如果异常很少,nu 的较小值可能更合适。
 - 
尝试一系列的值:首先尝试一系列的 nu 值,例如 0.01、0.05、0.1、0.2 等。你可以根据你对数据的理解调整这个范围。
 - 
考虑数据集大小:你的数据集大小也会影响 nu 的选择。对于较大的数据集,较小的值可能更合适,而对于较小的数据集,相对较大的值可能更合适。
 - 
平衡误报和漏报:根据应用的不同,你可能需要优先考虑最小化误报或漏报。相应地调整 nu 以达到所需的平衡。
 
 - 
 - 
我们将实施一个针对一系列 nu 值的实验。我们将指定我们想要实验的 nu 值列表。这些值代表 One-Class SVM 模型中边界错误分数的上限和支撑向量的下限。然后我们创建一个空列表来存储每个 nu 值的平均决策函数值:
nu_values = [0.01, 0.05, 0.1, 0.2, 0.3] mean_decision_function_values = [] - 
对于列表中的每个 nu 值,使用该 nu 值训练一个单类 SVM 模型。检索测试集的决策函数值并计算平均决策函数值。将平均决策函数值追加到列表中:
for nu in nu_values: svm_model = OneClassSVM(nu=nu, kernel='rbf', gamma=0.1) svm_model.fit(X_train_scaled) decision_function_values= svm_model.decision_function(X_test_scaled) mean_decision_function = np.mean(decision_function_values) mean_decision_function_values.append(mean_decision_function) - 
确定与最高平均决策函数值对应的 nu 值的索引。然后,检索最佳 nu 值:
best_nu_index = np.argmax(mean_decision_function_values) best_nu = nu_values[best_nu_index] - 
使用最佳 nu 值创建一个最终的 One-Class SVM 模型,并在缩放后的训练数据上对其进行训练:
final_model = OneClassSVM(nu=best_nu, kernel='rbf', gamma=0.1) final_model.fit(X_train_scaled) - 
我们现在使用此模型在
X_test_scaled测试数据集上预测异常。这一行通过将-1 映射到 1(表示异常)和任何其他值(通常是 1)映射到 0(表示正常实例)来创建预测的二进制表示(y_pred)。这是由于单类 SVM 模型通常将-1 分配给异常,将 1 分配给正常实例。我们将将其存储在一个新的 DataFrame 中,作为df_with_anomalies:y_pred = final_model.predict(X_test_scaled) y_pred_binary = [1 if pred == -1 else 0 for pred in y_pred] test_set_df = pd.DataFrame(data=X_test_scaled, columns=X.columns, index=X_test.index) test_set_df['Anomaly_Label'] = y_pred_binary df_with_anomalies = pd.concat([df, test_set_df['Anomaly_Label']], axis=1, join='outer') df_with_anomalies['Anomaly_Label'].fillna(0, inplace=True) - 
打印混淆矩阵和准确率分数:
print("Classification Report:\n", classification_report(y_test, y_pred_binary))print("Accuracy Score:", accuracy_score(y_test, y_pred_binary))这将打印以下报告:
 

图 9.8 – 输出分类报告
- 
打印被预测为异常的 DataFrame 行:
df_with_anomalies[df_with_anomalies['Anomaly_Label'] == 1]这将输出以下 DataFrame:
 

图 9.9 – df_with_anomalies DataFrame
在本节中,我们介绍了使用 Python 实现 SVM 异常检测的过程。使用 SVM 进行异常检测可以适应具有明显异常的各种数据集,使其成为识别异常值的有价值工具。在下一节中,我们将探讨数据增强和重采样技术,以识别边缘案例和罕见事件。
数据增强和重采样技术
类别不平衡是具有罕见事件的集合中常见的问题。类别不平衡可能会对模型的性能产生不利影响,因为模型倾向于偏向多数类。为了解决这个问题,我们将探讨两种重采样技术:
- 
过采样:通过生成合成样本来增加少数类中的实例数量
 - 
欠采样:通过减少多数类中的实例数量来平衡类别分布
 
让我们更详细地讨论这些重采样技术。
使用 SMOTE 进行过采样
合成少数类过采样技术(SMOTE)是一种广泛使用的重采样方法,用于解决机器学习数据集中的类别不平衡问题,尤其是在处理罕见事件或少数类时。SMOTE 通过在现有少数类样本之间进行插值来帮助生成少数类的合成样本。该技术旨在通过创建额外的合成实例来平衡类别分布,从而减轻类别不平衡的影响。在类别不平衡的数据集中,少数类包含的实例数量明显少于多数类。这可能导致模型训练偏差,模型倾向于偏向多数类,在少数类上的表现不佳。
这里是 SMOTE 算法的关键步骤:
- 
识别少数类实例:SMOTE 的第一步是识别属于少数类的实例。
 - 
选择最近邻:对于每个少数类实例,SMOTE 选择其k个最近邻(通常通过k 近邻算法选择)。这些邻居用于创建合成样本。
 - 
创建合成样本:对于每个少数类实例,SMOTE 在特征空间中实例与其k个最近邻连接的直线上生成合成样本。通过添加实例与其邻居之间的特征差异的随机分数(通常在 0 和 1 之间),创建合成样本。这个过程有效地为合成样本引入了变异性。
 - 
与原始数据结合:合成的样本与原始的少数类实例相结合,从而得到一个具有更平衡类分布的重采样数据集。
 
SMOTE 有助于解决类别不平衡问题,而不需要丢弃任何数据,因为它生成合成样本而不是从多数类中删除实例。它增加了模型可用的信息量,可能提高模型泛化到少数类的能力。SMOTE 易于实现,并在 Python 中流行的库(如 imbalanced-learn)中可用。
虽然 SMOTE 在许多情况下都有效,但它可能并不总是对高度不平衡的数据集或具有复杂决策边界的数据集表现最佳。生成过多的合成样本可能导致训练数据上的过拟合,因此选择合适的最近邻数量(k)值至关重要。SMOTE 可能会引入一些噪声,并且如果少数类在特征空间中过于稀疏或分散,可能不会那么有效。SMOTE 可以与其他技术结合使用,例如对多数类进行下采样或使用不同的重采样比率,以实现更好的性能。评估模型在适当的指标(例如,精确度、召回率或 F1 分数)上的性能对于评估 SMOTE 和其他技术对模型检测罕见事件能力的影响至关重要。
让我们在 Python 中实现这种方法:
- 
使用
pd.read_csv()读取train_loan_prediction.csv文件:import pandas as pd from imblearn.over_sampling import SMOTE from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report from sklearn.ensemble import RandomForestClassifier df = pd.read_csv('train_loan_prediction.csv') - 
数据集中的
'Loan_Status'列包含'Y'和'N'分类值,分别代表贷款批准('Y')和拒绝('N')。为了将这个分类目标变量转换为数值格式,使用map()函数将'Y'映射到1,将'N'映射到0:df['Loan_Status'] = df['Loan_Status'].map({'Y': 1, 'N': 0}) - 
fillna()方法。这确保了数据集中任何缺失值都被其相应列的平均值所替换:df.fillna(df.mean(), inplace=True) - 
X特征集。使用select_dtypes()方法仅包括具有float和int数据类型的列,同时排除非数值列,如'Loan_Status'和'Loan_ID'。y目标变量设置为'Loan_Status':numerical_columns = df.select_dtypes(include=[float, int]).columns X = df[numerical_columns].drop('Loan_Status', axis=1) y = df['Loan_Status'] - 
使用 scikit-learn 的
train_test_split()函数对训练集(X_train,y_train)和测试集(X_test,y_test)进行分割。训练集包含 80%的数据,而测试集包含 20%的数据。random_state参数设置为42以确保可重复性:X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) - 
SMOTE(random_state=42)。然后使用fit_resample()方法对训练数据进行 SMOTE 处理。此方法通过生成合成样本对少数类(贷款拒绝)进行过采样,创建一个平衡的数据集。重采样后的数据存储在X_train_resampled和y_train_resampled中:smote = SMOTE(random_state=42) X_train_resampled, y_train_resampled = smote.fit_resample(X_train, y_train) - 
RandomForestClassifier(random_state=42)。分类器使用fit()方法在重采样数据(X_train_resampled,y_train_resampled)上训练。训练好的分类器使用predict()方法对测试数据(X_test)进行预测。预测结果存储在y_pred中:clf = RandomForestClassifier(random_state=42) clf.fit(X_train_resampled, y_train_resampled) y_pred = clf.predict(X_test) - 
使用 scikit-learn 的
classification_report()函数生成分类报告。分类报告根据测试集的预测(y_pred)和真实标签(y_test)为每个类别(贷款批准和拒绝)提供精确度、召回率、F1 分数和支持。分类报告最初以字典格式返回。代码使用pd.DataFrame()将此字典转换为clf_reportDataFrame,使其更容易处理数据。使用.T属性将clf_reportDataFrame 转置,使类别(0和1)成为行,评估指标(精确度、召回率、F1 分数和支持)成为列。这种转置提供了更方便和可读的格式,以便进行进一步的分析或展示:clf_report = pd.DataFrame(classification_report(y_test, y_pred, output_dict=True)) clf_report = clf_report.T clf_report让我们打印分类报告以了解模型的性能:
 

图 9.10 – 由前面的代码生成的分类报告
报告显示,模型在识别类别 0 的实例方面表现中等,精确度为 73.33%。然而,召回率相对较低,为 51.16%,表明模型可能遗漏了一些类别 0 的实际实例。模型在识别类别 1 的实例方面表现出色,精确度高达 77.42%,召回率非常高,为 90.00%。加权平均指标考虑了类别不平衡,为两个类别提供了平衡的评估。模型的总体准确度为 76.42%,表示正确预测实例的百分比。
在下一节中,让我们来探讨欠采样——另一种解决机器学习中类别不平衡问题的方法。
使用 RandomUnderSampler 进行欠采样
使用 RandomUnderSampler 处理类别不平衡是解决不平衡数据集挑战的有效方法,在这种情况下,一个类别显著超过其他类别。在这种情况下,传统的机器学习算法可能难以从数据中学习,并且倾向于偏向多数类别,导致在少数类别或罕见事件上的性能不佳。
RandomUnderSampler 是一种重采样技术,旨在通过随机从多数类别删除实例来平衡类别分布,直到类别比例更加平衡。通过减少多数类别的实例数量,RandomUnderSampler 确保少数类别以更成比例的方式表示,使模型更容易检测和学习与罕见事件相关的模式。
下面是关于使用 RandomUnderSampler 处理类别不平衡的一些关键点:
- 
RandomUnderSampler是一种数据级别的重采样方法。数据级别的重采样技术涉及操纵训练数据以平衡类别分布。在RandomUnderSampler中,从多数类别随机选择并删除实例,从而得到一个具有平衡类别分布的较小数据集。 - 
RandomUnderSampler直接从多数类别中删除实例,而不改变少数类别的实例。这种方法有助于保留少数类别的信息,使模型更容易专注于学习与罕见事件相关的模式。 - 
RandomUnderSampler会从多数类别中丢失信息。通过随机删除实例,可能会丢弃一些具有信息量的实例,这可能导致模型在多数类别上的泛化能力降低。 - 
RandomUnderSampler计算效率高,因为它只涉及从多数类别随机删除实例。这使得它比其他一些重采样方法更快。 - 
RandomUnderSampler在某些情况下可能有效,但它并不总是最佳选择,特别是如果多数类别包含重要的模式和信息。在选择适当的重采样技术时,仔细考虑问题和数据集特征至关重要。 - 
RandomUnderSampler可以与其他技术结合使用。例如,可以先应用RandomUnderSampler,然后使用RandomOverSampler(过采样)来进一步平衡类别分布。这种方法有助于实现两个类别的更平衡的表示。 - 
评估和模型选择:在处理类别不平衡时,评估模型在相关指标上的性能至关重要,例如精确度、召回率、F1 分数和 ROC 曲线下面积(AUC-ROC)。这些指标提供了对模型处理罕见事件和边缘情况的全面评估。
 
让我们使用 Python 实现这种方法:
使用 pd.read_csv() 读取 train_loan_prediction.csv 文件:
import pandas as pd
from imblearn.under_sampling import RandomUnderSampler
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from sklearn.ensemble import RandomForestClassifier
df = pd.read_csv('train_loan_prediction.csv')
数据集中的'Loan_Status'列包含'Y'和'N'分类值,分别代表贷款批准('Y')和拒绝('N')。为了将这个分类目标变量转换为数值格式,使用map()函数将'Y'映射到 1,将'N'映射到 0:
df['Loan_Status'] = df['Loan_Status'].map({'Y': 1, 'N': 0})
以下代码使用fillna()方法对数据集中所有具有缺失值的列应用均值填充。这确保了数据集中的任何缺失值都被其各自列的均值所替换:
df.fillna(df.mean(), inplace=True)
代码仅从数据集中选择数值列来构建X特征集。使用select_dtypes()方法仅包括数据类型为float和int的列,同时排除非数值列,例如'Loan_Status'和'Loan_ID'。y目标变量设置为'Loan_Status':
numerical_columns = df.select_dtypes(include=[float, int]).columns
X = df[numerical_columns].drop('Loan_Status', axis=1)
y = df['Loan_Status']
使用 scikit-learn 中的train_test_split()函数将数据分为训练集(X_train, y_train)和测试集(X_test, y_test)。训练集包含 80%的数据,而测试集包含 20%的数据。random_state参数设置为42以确保可重复性:
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
然后我们实例化RandomUnderSampler,并使用fit_resample()方法将其应用于训练数据。此方法对多数类(贷款批准)进行下采样以创建平衡数据集。重采样后的数据存储在X_train_resampled和y_train_resampled中:
rus = RandomUnderSampler(random_state=42)
X_train_resampled, y_train_resampled = rus.fit_resample(X_train, y_train)
然后使用fit()方法在重采样数据(X_train_resampled, y_train_resampled)上训练随机森林分类器。训练好的分类器用于使用predict()方法对测试数据(X_test)进行预测。预测结果存储在y_pred中:
clf = RandomForestClassifier(random_state=42)
clf.fit(X_train_resampled, y_train_resampled)
y_pred = clf.predict(X_test)
clf_report = pd.DataFrame(classification_report(y_test, y_pred, output_dict=True))
clf_report = clf_report.T
clf_report
让我们检查分类报告以评估模型性能:

图 9.11 – 模型性能的分类报告
模型对两类都表现出不错的性能,与类别0相比,类别1具有更高的精确度、召回率和 F1 分数。加权平均考虑了类别分布的不平衡,提供了对整体性能的更具有代表性的度量。准确率 0.7805%表明,模型正确预测了测试集中大约 78%的实例的类别。
在下一节中,让我们了解成本敏感学习,并探讨其在罕见事件具有重大后果的场景中的关键作用。
成本敏感学习
成本敏感学习是一种机器学习方法,在模型训练过程中考虑了不同类别误分类的成本。在传统的机器学习中,重点是最大化整体准确率,但在许多实际场景中,某些类别的误分类可能比其他类别的误分类有更严重的后果。
例如,在医疗诊断应用中,将严重疾病误诊为不存在(假阴性)可能比将轻微状况误诊为存在(假阳性)有更严重的后果。在欺诈检测中,错误地将合法交易标记为欺诈(假阳性)可能会给客户带来不便,而未能检测到实际欺诈交易(假阴性)可能导致重大经济损失。
成本敏感学习通过为不同类别分配不同的误分类成本来解决这些成本不平衡问题。通过将这些成本纳入训练过程,模型被鼓励优先考虑最小化整体误分类成本,而不仅仅是优化准确性。
实施成本敏感学习有几种方法:
- 
修改损失函数:在模型训练期间使用的损失函数可以被修改,以包含特定类别的误分类成本。目标是最小化预期成本,这是误分类成本和模型预测的组合。
 - 
类别权重:另一种方法是给少数类或误分类成本较高的类分配更高的权重。这种技术可以应用于各种分类器,如决策树、随机森林和 SVMs,以强调从少数类中学习。
 - 
采样技术:除了分配权重外,还可以使用重采样技术,如对少数类进行过采样或对多数类进行欠采样,以平衡类别分布并提高模型从罕见事件中学习的能力。
 - 
阈值调整:通过调整分类阈值,我们可以控制精确度和召回率之间的权衡,从而做出对少数类更敏感的预测。
 - 
集成方法:成本敏感提升等集成方法结合多个模型,专注于难以分类的实例,并给误分类样本分配更高的权重。
 
成本敏感学习在类别不平衡严重且误分类后果至关重要的场景中尤为重要。通过考虑与不同类别相关的成本,模型可以做出更明智的决策,并在检测罕见事件和处理边缘情况方面提高整体性能。
需要注意的是,成本敏感学习需要对成本矩阵进行仔细考虑,因为错误指定的成本可能导致意外结果。在考虑现实世界成本的相关指标上对模型进行适当的验证和评估,对于确保成本敏感学习算法的有效性和可靠性至关重要。
现在我们将使用 Python 中的 Loan Prediction 数据集演示成本敏感学习:
- 
使用
pandas加载所需的库和数据集:import pandas as pd from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report df = pd.read_csv('train_loan_prediction.csv') - 
现在我们需要进行数据预处理来处理缺失值并将目标变量转换为数值数据类型:
df['Loan_Status'] = df['Loan_Status'].map({'Y': 1, 'N': 0}) df.fillna(df.mean(), inplace=True) - 
对于这个例子,我们将仅使用数值列。然后我们将数据集分为
train和test:numerical_columns = df.select_dtypes(include=[float, int]).columns X = df[numerical_columns].drop('Loan_Status', axis=1) y = df['Loan_Status'] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) - 
我们首先根据训练数据中类频率的倒数计算类权重。一个类的频率越高,其权重越低,反之亦然。这样,模型会赋予少数类(罕见事件)更高的重要性,并对其正确预测更加敏感:
class_weights = dict(1 / y_train.value_counts(normalize=True)) - 
接下来,我们将使用计算出的类权重设置
class_weight参数来训练随机森林分类器。这种修改允许分类器在训练过程中考虑类权重,从而有效地实现成本敏感学习:clf = RandomForestClassifier(random_state=42, class_weight=class_weights) clf.fit(X_train, y_train) - 
在训练好模型后,我们会在测试数据上做出预测,并使用分类报告来评估分类器的性能,该报告为每个类别提供了精确度、召回率、F1 分数和支持:
y_pred = clf.predict(X_test) clf_report = pd.DataFrame(classification_report(y_test, y_pred, output_dict=True)) clf_report = clf_report.T clf_report让我们查看分类报告并评估随机森林分类器的性能:
 

图 9.12 – 随机森林分类器性能的分类报告
在成本敏感学习中,你通常会定义一个成本矩阵,该矩阵量化了每个类别的误分类成本,并使用它来指导模型的训练。分类报告的结果可以帮助你识别需要调整以使模型与你的应用中特定的成本考虑因素相一致的区域。在假阳性成本和假阴性成本不相等的情况下,成本矩阵特别有用。如果假阳性的成本更高,考虑提高决策阈值。如果假阴性的成本更高,考虑降低阈值。
在下一节中,我们将了解用于检测边缘情况和罕见事件的评估指标。
选择评估指标
在机器学习中处理边缘情况和罕见事件时,选择正确的评估指标对于准确评估模型的性能至关重要。传统的评估指标,如准确率,在感兴趣的类别(罕见事件)远多于多数类的不平衡数据集中可能不足以提供足够的信息。在不平衡数据集中,如果罕见事件是少数类,传统的评估指标如准确率可能会误导。例如,如果一个数据集有 99%的多数类和 1%的罕见事件,一个将所有实例预测为多数类的模型仍然会达到 99%的准确率,这看起来非常高。然而,这样的模型在检测罕见事件方面将无效。为了解决这个问题,我们需要关注模型在正确识别罕见事件方面的性能的评估指标,即使这会导致准确率的下降。
这里有一些更适合检测边缘案例和罕见事件的评估指标:
- 
精确率:精确率衡量模型做出的正预测的准确性。它是真实正例(正确预测的罕见事件)与真实正例和假正例(错误地将多数类作为罕见事件预测)之和的比率。高精确率表明模型在做出正预测时非常谨慎,并且假正例率很低。
 - 
召回率(灵敏度):召回率衡量模型预测出的所有实际正例中真实正例的比例。它是真实正例与真实正例和假负例(错误地将多数类作为罕见事件预测)之和的比率。高召回率表明模型能够捕获大量罕见事件实例。
 - 
F1 分数:F1 分数是精确率和召回率的调和平均数。它提供了两种指标之间的平衡,并且在精确率和召回率之间存在不平衡时特别有用。F1 分数惩罚那些以牺牲另一个指标为代价优先考虑精确率或召回率的模型。
 - 
接收器操作特征(ROC-AUC)下的面积:ROC-AUC 是一种用于评估二元分类模型的性能指标。它测量 ROC 曲线下的面积,ROC 曲线绘制了随着分类阈值变化时的真实正例率(召回率)与假正例率。更高的 ROC-AUC 表明模型性能更好,尤其是在检测罕见事件方面。
 
在下一节中,让我们深入了解集成技术,并了解它们在机器学习模型中的关键作用,尤其是在处理包含边缘案例和罕见事件的 数据时。
集成技术
集成技术是用于提高机器学习模型性能的强大方法,尤其是在不平衡数据集、罕见事件和边缘案例的场景中。这些技术通过结合多个基础模型来创建一个更稳健和准确的最终预测。让我们讨论一些流行的集成技术。
Bagging
自助聚合(bagging)是一种集成技术,它从训练数据中创建多个自助样本(带有替换的随机子集),并在每个样本上训练一个单独的基础模型。最终的预测是通过平均或投票所有基础模型的预测得到的。Bagging 在处理高方差和复杂模型时特别有用,因为它减少了过拟合并增强了模型的泛化能力。以下是与 Bagging 相关的关键概念:
- 
自助采样:Bagging 过程从通过称为自助采样的过程创建多个训练数据的随机子集开始。自助采样涉及从原始数据集中随机选择数据点,并进行替换。因此,某些数据点可能在子集中出现多次,而其他数据点可能被排除在外。
 - 
基础模型训练:对于每个自助样本,一个基础模型(学习器)独立地在该特定训练数据子集上训练。基础模型可以是任何机器学习算法,例如决策树、随机森林或 SVMs。
 - 
聚合预测:一旦所有基础模型都训练完成,它们就被用来对新、未见过的数据进行预测。对于分类任务,最终预测通常由多数投票决定,即选择在基础模型中获得最多投票的类别。在回归任务中,最终预测是通过平均所有基础模型的预测得到的。
 
这里是 Bagging 的一些好处:
- 
方差减少:Bagging 通过结合在不同数据子集上训练的多个模型的预测来帮助减少模型的方差。这导致了一个更稳定和健壮的模型。
 - 
过拟合预防:通过在每个数据子集上训练每个基础模型,Bagging 可以防止个别模型对训练集中的噪声过拟合。
 - 
模型泛化:Bagging 通过减少偏差和方差来提高模型的泛化能力,从而在未见过的数据上获得更好的性能。
 - 
并行性:由于基础模型是独立训练的,因此 Bagging 适合并行处理,使其在计算上更高效。
 
随机森林是 Bagging 技术的流行例子。在随机森林中,基础模型是决策树,多个决策树的预测结果被组合起来以做出最终预测。
下面是一个使用 Python 在 Loan Prediction 数据集上实现 Bagging 的随机森林的例子:
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report
df = pd.read_csv('train_loan_prediction.csv')
df['Loan_Status'] = df['Loan_Status'].map({'Y': 1, 'N': 0})
df.fillna(df.mean(), inplace=True)
numerical_columns = df.select_dtypes(include=[float, int]).columns
X = df[numerical_columns].drop('Loan_Status', axis=1)
y = df['Loan_Status']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
clf = RandomForestClassifier(random_state=42)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
clf_report = pd.DataFrame(classification_report(y_test, y_pred, output_dict=True))
clf_report = clf_report.T
clf_report
总之,Bagging 技术为处理边缘情况提供了一种稳健且有效的策略。通过聚合多个基础模型的预测,Bagging 不仅增强了整体模型稳定性,还加强了其准确识别和解决边缘情况的能力,从而有助于构建一个更具弹性和可靠性的预测框架。
在下一节中,我们将探讨提升技术,这是另一种处理边缘情况和罕见事件的方法。
提升技术
提升是一种集成技术,它按顺序构建基础模型,每个后续模型都专注于前一个模型错误分类的实例。它为错误分类的实例分配更高的权重,从而更多地关注罕见事件。流行的提升算法包括自适应提升(AdaBoost)、梯度提升和 XGBoost。提升的目标是通过迭代组合弱学习器来创建一个强学习器。
下面是如何提升工作的例子:
- 
基础模型训练:提升技术首先在全部训练数据集上训练一个基础模型(也称为弱学习器)。弱学习器通常是具有有限预测能力的简单模型,例如决策桩(具有单个分割的决策树)。
 - 
加权训练: 在第一个模型训练完成后,被模型错误分类的数据点被分配更高的权重。这意味着后续的模型将更加关注这些错误分类的数据点,试图纠正其预测。
 - 
迭代训练: 提升算法遵循迭代方法。对于每一次迭代(或提升轮次),都会在更新后的训练数据上训练一个新的弱学习器,并调整其权重。然后,将这些弱学习器组合起来创建一个强学习器,与单个弱学习器相比,强学习器的预测性能得到提升。
 - 
加权投票: 在最终预测过程中,弱学习器的预测通过加权投票相结合,其中准确性更高的模型对最终预测有更大的影响。这允许提升算法专注于难以分类的实例,并提高模型对罕见事件的敏感性。
 
提升算法的优点如下:
- 
提高准确性: 通过关注数据集中最具挑战性的实例并在多次迭代中细化预测,提升算法提高了模型的准确性。
 - 
鲁棒性: 通过迭代调整权重并从之前的错误中学习,提升算法减少了模型对数据中的噪声和异常值的敏感性。
 - 
模型自适应: 提升算法很好地适应不同类型的数据,并能处理特征与目标变量之间的复杂关系
 - 
集成多样性: 提升算法创建了一个多样化的弱学习器集成,这导致更好的泛化能力和减少过拟合。
 
让我们通过 AdaBoost 的一个示例来了解提升算法。
AdaBoost 是一种流行的提升算法,在实践中被广泛使用。在 AdaBoost 中,基模型通常是决策树桩,并且在每次迭代后调整模型权重,以强调被错误分类的实例。
下面是一个使用 AdaBoost 实现提升算法的 Python 示例:
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import AdaBoostClassifier
from sklearn.metrics import classification_report
df = pd.read_csv('train_loan_prediction.csv')
df['Loan_Status'] = df['Loan_Status'].map({'Y': 1, 'N': 0})
df.fillna(df.mean(), inplace=True)
numerical_columns = df.select_dtypes(include=[float, int]).columns
X = df[numerical_columns].drop('Loan_Status', axis=1)
y = df['Loan_Status']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
clf = AdaBoostClassifier(random_state=42)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
clf_report = pd.DataFrame(classification_report(y_test, y_pred, output_dict=True))
clf_report = clf_report.T
clf_report
总结来说,提升技术的应用成为处理边缘情况的一种稳健策略。通过其迭代方法,提升算法使模型能够专注于具有挑战性的实例,从而增强其识别和准确预测罕见事件的能力。
我们现在将探讨如何使用堆叠方法来检测和处理机器学习中的边缘情况和罕见事件。
堆叠
堆叠是一种高级集成学习技术,通过在基模型的输出上训练一个元模型来结合多个基模型的预测。堆叠旨在利用不同基模型的优势,创建一个更准确和鲁棒的最终预测。它是一种“学习如何学习”的形式,其中元模型学习如何最佳地结合基模型的预测。基模型充当“学习器”,它们的预测成为元模型的输入特征,从而进行最终预测。堆叠通常可以通过捕捉来自不同基模型的互补模式来提高性能。
下面是模型方法:
- 
基础模型训练:堆叠过程首先是在训练数据集上训练多个不同的基础模型。这些基础模型可以是不同类型的机器学习算法,甚至是具有不同超参数的相同算法。
 - 
基础模型预测:一旦基础模型训练完成,它们就被用来在相同的训练数据(样本内预测)或一个独立的验证数据集(样本外预测)上进行预测。
 - 
元模型训练:然后,将基础模型的预测结合起来创建一个新的数据集,该数据集作为元模型的输入。每个基础模型的预测成为该数据集中的一个新特征。元模型在这个新数据集上以及真实的目标标签上进行训练。
 - 
最终预测:在最终预测阶段,基础模型对新未见过的数据进行预测。然后,这些预测被用作元模型的输入特征,从而做出最终预测。
 
堆叠有以下优点:
- 
提高预测性能:堆叠利用了不同基础模型的互补优势,与使用单个模型相比,可能带来更好的整体预测性能
 - 
降低偏差和方差:通过结合多个模型,堆叠可以降低模型的偏差和方差,从而提高泛化能力
 - 
灵活性:堆叠允许使用不同的基础模型,使其适用于各种类型的数据和问题
 - 
集成多样性:堆叠通过使用各种基础模型创建了一个多样化的集成,这有助于防止过拟合
 
下面是一个使用 Python 中的 scikit-learn 实现堆叠的示例,使用的是贷款预测数据集:
- 
我们首先导入所需的库并加载数据集:
import pandas as pd from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report df = pd.read_csv('train_loan_prediction.csv') - 
我们现在需要执行数据预处理来处理缺失值并将目标变量转换为数值数据类型:
df['Loan_Status'] = df['Loan_Status'].map({'Y': 1, 'N': 0}) df.fillna(df.mean(), inplace=True) - 
为了简单起见,我们将在这个示例中仅使用数值列。然后,我们将数据集分为
train和test:numerical_columns = df.select_dtypes(include=[float, int]).columns X = df[numerical_columns].drop('Loan_Status', axis=1) y = df['Loan_Status'] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) - 
RandomForestClassifier和GradientBoostingClassifier分别使用RandomForestClassifier(random_state=42)和GradientBoostingClassifier(random_state=42)实例化:base_model_1 = RandomForestClassifier(random_state=42) base_model_2 = GradientBoostingClassifier(random_state=42) - 
这些基础模型使用
fit()方法在训练数据(X_train,y_train)上训练,如以下所示。训练好的基础模型被用来使用predict()方法在测试数据(X_test)上进行预测。来自两个基础模型的预测被存储在pred_base_model_1和pred_base_model_2中:base_model_1.fit(X_train, y_train) base_model_2.fit(X_train, y_train) pred_base_model_1 = base_model_1.predict(X_test) pred_base_model_2 = base_model_2.predict(X_test) - 
stacking_X_train数据集是通过结合基础模型的预测(pred_base_model_1和pred_base_model_2)创建的。这个新的数据集将被用作元模型的输入特征:stacking_X_train = pd.DataFrame({ 'BaseModel1': pred_base_model_1, 'BaseModel2': pred_base_model_2 }) - 
LogisticRegression()。元模型使用fit()方法在新的数据集 (stacking_X_train) 和测试集的真实标签 (y_test) 上进行训练。元模型学习如何结合基础模型的预测并做出最终预测:meta_model = LogisticRegression() meta_model.fit(stacking_X_train, y_test) - 
new_unseen_data) 是通过使用sample()方法随机选择测试数据 (X_test) 的 20% 创建的。基础模型使用predict()方法对新、未见过的数据 (new_unseen_data) 进行预测。对于新数据的预测结果存储在new_pred_base_model_1和new_pred_base_model_2中:new_unseen_data = X_test.sample(frac=0.2, random_state=42) new_pred_base_model_1 = base_model_1.predict(new_unseen_data) new_pred_base_model_2 = base_model_2.predict(new_unseen_data) - 
stacking_new_unseen_data数据集是通过结合基础模型 (new_pred_base_model_1和new_pred_base_model_2) 对新、未见过的数据的预测创建的。这个新的数据集将被用作元模型的输入特征,以做出最终预测:stacking_new_unseen_data = pd.DataFrame({ 'BaseModel1': new_pred_base_model_1, 'BaseModel2': new_pred_base_model_2 }) - 
stacking_new_unseen_data) 使用predict()方法进行预测。final_prediction变量根据元模型的决策持有预测的类别(0 或 1):final_prediction = meta_model.predict(stacking_new_unseen_data) final_prediction 
总结来说,这段代码演示了堆叠的概念,其中基础模型(随机森林和梯度提升)在原始数据上训练,它们的预测被用作元模型(逻辑回归)的输入特征,最终预测是通过在新的、未见过的数据上使用元模型来完成的。堆叠允许模型协同工作,并且与单独使用基础模型相比,有可能提高预测性能。
摘要
在本章中,我们探讨了机器学习中检测罕见事件和边缘案例的关键方面。由于罕见事件的罕见性,它们在各个领域都有重要的意义,并需要特别的关注。我们深入研究了多种技术和方法,使我们能够有效地识别和处理这些不常见的情况。
统计方法,如 Z 分数和 IQR,为我们提供了强大的工具,可以确定数据中的异常值和异常。这些方法有助于建立识别罕见事件的具有意义的阈值,使我们能够区分重要的数据点与噪声。
我们还探讨了基于机器学习的异常检测技术,如隔离森林和自动编码器。这些方法利用无监督学习来识别与大多数数据不同的模式和偏差,使它们非常适合在复杂数据集中检测罕见事件。
此外,我们还讨论了重采样方法(如 SMOTE 和 RandomUnderSampler)在处理类别不平衡中的重要性。这些技术使我们能够创建平衡的数据集,从而提高模型在识别罕见事件时的性能,同时保持数据完整性。
此外,我们发现集成技术,包括堆叠、袋装和提升,在增强我们模型检测边缘案例能力方面的潜力。通过集成多个模型的综合力量,增强了泛化能力和模型鲁棒性。
在存在罕见事件的情况下,选择适当的评估指标至关重要,以确保公平评估和准确评估模型性能。如精确度、召回率、F1 分数和 AUC-ROC 等指标提供了对模型性能的全面洞察,并指导决策制定。
检测罕见事件和边缘案例在医疗诊断、欺诈检测、预测性维护和环境监测等众多领域具有深远的影响。通过采用有效的技术来识别和处理这些不常见的事件,我们提高了机器学习应用的可靠性和效率。
在我们结束本章之前,让我们认识到这项技能在现实场景中的重要性。检测罕见事件使我们能够做出明智的决策,防范潜在风险,并充分利用机器学习在众多领域的积极影响。
在下一章中,我们将探讨数据驱动方法在机器学习中面临的一些挑战。
第四部分:开始使用数据中心化机器学习
到现在为止,你可能已经意识到转向以数据为中心的机器学习方法不仅需要调整你自己的工作方式,还需要影响周围的人——这是一项远非简单的任务。在本部分中,我们探讨了在模型开发和部署过程中可能遇到的技术和非技术障碍,并揭示了采用数据为中心的方法如何帮助克服这些障碍。
本部分包含以下章节:
- 第十章**,开启你的数据中心化机器学习之旅
 
第十章:启动你在数据为中心的机器学习之旅
以数据为中心的机器学习(ML)方法是为了应对以模型为中心的范式局限性而创建的。尽管以数据为中心的视角为机器学习在新的和现有领域中的应用开辟了令人难以置信的机会,但这并不意味着它容易实施。
以模型为中心的方法的吸引力在于其相对简单性。它在该领域的统治地位并不一定是因为它更优越,而是因为它更直接。它主要关注改进模型、调整算法和增强计算能力。然而,这种方法往往忽视了机器学习的一个基本方面——为这些模型提供的数据的质量和相关性。
与此相反,以数据为中心的方法优先考虑提高数据质量而不是完善模型。它认识到,即使是最复杂的模型,如果建立在质量低劣或无关的数据的脆弱基础上,也可能失败。
你将面临的挑战是,采用以数据为中心的机器学习方法可能需要大量的努力。
在本章的结尾,我们将集中讨论如何有效地实施你在本书中学到的知识。我们将涵盖以下主题:
- 
如何通过以数据为中心的方法解决六个常见的机器学习挑战
 - 
在你的组织中倡导数据质量的重要性
 - 
聚集人员以帮助你实施以数据为中心的方法
 - 
对人工智能伦理和公平性负责
 
让我们从探讨以数据为中心的方法如何提供工具来消除机器学习开发中的常见瓶颈开始。
解决六个常见的机器学习挑战
我们编写这本书是为了提供克服通常成为机器学习模型开发瓶颈的六个常见挑战所需的工具和技术。这六个挑战如下:
- 
典型的开发方法是模型为中心的:在传统的机器学习中,重点是模型——选择正确的算法、调整超参数和优化性能指标。这种以模型为中心的方法通常涉及无数次的调整模型,以挤出更多的准确性。然而,虽然模型无疑很重要,但这种方法有时会导致忽视其他同样重要的方面,例如为这些模型提供的数据的质量和相关性。
 - 
任何机器学习模型的可能性都受限于数据质量和数量:无论机器学习模型多么复杂,其性能最终都是由其训练所使用的数据的质量和数量决定的。低质量的数据可能导致预测不准确,而数据不足可能导致过拟合,即模型过度学习训练数据,在未见过的数据上表现不佳。
 - 
我们并不总能“获取更多数据”:虽然拥有更多数据可以帮助提高模型性能,但获取额外数据并不总是可行的。这可能是因为成本高昂、耗时,或者由于隐私问题或某些事件的罕见性而变得不可能。
 - 
大多数数据并非为机器学习目的收集和整理:数据通常出于各种原因被收集,如报告、商业智能(BI)或记录保存,但并非专门为机器学习。这可能导致数据对我们特定的机器学习任务来说是不相关的、不完整的或噪声的。
 - 
中小企业和标注员不了解数据收集的最终目标:在许多情况下,收集或标注数据的人并不完全清楚机器学习项目的最终目标。这种缺乏理解可能导致数据标注或收集的不一致性,因为不同的人可能会对指令有不同的解释。
 - 
中小企业和标注员存在偏见:每个人都有偏见,中小企业和标注员也不例外。这些偏见可能会无意中影响他们收集或标注数据的方式,导致数据集偏差。
 
为了解决这六个挑战或瓶颈,我们向你介绍了数据为中心的机器学习的四个原则(在第三章**,数据为中心的机器学习原则)和一个由一系列技术和非技术工具和方法组成的数据为中心的机器学习工具包。
图 10.1 展示了这些原则、工具和方法如何为你提供克服这些挑战并取得更多成果的工具:

图 10.1 – 机器学习模型开发中六个常见挑战或瓶颈的概述;本书为你提供了四个原则和一系列以数据为中心的工具和技术来克服这些挑战
然而,知识只有当它被应用时才有用。随着我们接近对以数据为中心的机器学习的探索,是时候你反思一下,为了应用你在本书中学到的知识,你将不得不做哪些不同的改变。
不可避免地,你将不得不改变一些自己的方法和习惯,并重塑你与同事之间的某些互动。正如我们多次提到的,数据科学是一项团队运动,现在是时候你成为以数据为中心的机器学习团队的队长了。
为了成功地扮演这个角色,有三个关键领域你需要关注:
- 
成为数据质量的倡导者
 - 
聚集人们来帮助你
 - 
对人工智能伦理和公平性负责
 
让我们详细地逐一探讨这些领域。
成为数据质量的倡导者
任何成功的数据中心化机器学习倡议的基础是高质量的数据。作为这个领域的专家,你有责任倡导并维护严格的数据质量标准。这包括确保数据收集、清洗和标注过程是稳健和一致的。然而,有时可能会感觉整个宇宙都在与你作对,不断地向你提供糟糕的数据。这可能确实是真的!
热力学第二定律表明,能量倾向于扩散,如果得不到积极的管理,系统自然会向无序或“熵”的增加方向发展。
这种现象几乎在你能想到的任何有序系统中都会发生,包括商业。从本质上讲,一家企业是由一群人、技术和流程系统地组织起来,以实现一系列共同目标。
但随着时间的推移,人们会进出企业,技术堆栈在规模和复杂性上会增长,流程可能会变得冗余或被人们的个人兴趣所取代。
让我们将这个概念应用到这样一个商业场景中,即随着时间的推移,一个组织的科技堆栈在增长。随着技术堆栈的扩展,添加更多的软件、平台和基础设施,就像向系统中添加更多的能量一样。如果没有适当的管理和组织,这可能会导致复杂性和“无序”的增加。系统可能无法高效互动,可能存在冗余,或者关键流程可能会被忽视——这就是我们场景中的“熵”。
我们在与每个组织的互动中都看到了这一场景的发生。尽管任何组织都是从零开始的,但典型的中等到大型企业将在任何给定时间运行数百甚至数千个技术应用程序,生成一个日益复杂的数据网络。
2022 年对北美至少有 1000 名员工的 200 多名 IT 和数据专业人士的调查证实了这一点 1。调查发现,这些组织平均管理着 400 个数据源,20%的受访者从 1000 个或更多的数据源中提取数据来喂养他们的分析系统。
换句话说,数据无序并不是随机事件,而是默认结果。除非投入大量努力来保持秩序,否则混乱的数据将会出现。作为数据专业人士,你的任务是识别并逆转这种数据熵,在组织的其他部分的帮助下。如果你不这样做,随着时间的推移,你的工作将变得更难,而不是更容易。
要管理和逆转数据熵,你首先需要对其进行测量。市场上有一些用于此目的的复杂工具,但如果你的组织无法并愿意投资这些工具,那么就从简单的数据质量措施开始,倡导数据质量。如果你的组织没有有效的数据质量测量协议,你可以成为激发行动的人。
提倡数据质量意味着向他人展示如何做到这一点。在这本书中,我们已经为你提供了采取以数据为中心的方法进行机器学习的工具。现在,轮到你通过展示这种方法的可能性来激励你的同事了。慷慨地分享你在本书中学到的知识。
提倡数据质量还意味着创造一个重视并优先考虑数据准确性的环境。鼓励你的团队永远不要对数据的状态做出假设。通过这样做,你将为可靠、准确和有效的机器学习模型奠定基础。
为了做到这一点,你需要将来自整个业务部门的人聚集在一起。
聚集人群
数据科学在本质上是一门协作学科。它需要来自不同背景和技能集的个人的投入和专业知识。作为任何机器学习项目中心的专家,你的一个主要角色是促进这种协作。
这里是挑战:大多数数据科学家都好奇、技术精湛、自主性强、高度智能的系统思考者。我们喜欢解决复杂问题,并且喜欢独自解决这些问题。我们喜欢独自工作。我们沉迷于细节。我们习惯于人们向我们寻求帮助,而不是反过来。我们是问题解决者,而不是问题制造者。
对于具有这些行为特征的人来说,聚集人群和管理从群体动态中产生的不可避免的人际复杂性可能非常困难。这可能需要大量的精力,并推动我们超越我们的舒适区。在我们这本书中涵盖的所有内容中,学习如何聚集人群可能是最困难的部分。
同时,我们的利益相关者通常对机器学习没有深入的技术理解。他们可能甚至不感兴趣了解这一切是如何运作的——他们只想看到业务成果。他们想看到结果,而不是数据质量问题。这是一个难以激发的群体。
要营造一个开放沟通、思想共享和建设性反馈成为常态的环境并不容易。要打破壁垒并鼓励跨职能协作也不容易。但如果你想要最大化构建有影响力的机器学习解决方案的机会,这是必须做到的。
你通过将其与业务问题联系起来,而不是通过描述技术复杂性,来激发非技术利益相关者对数据质量的兴趣。这要求你了解业务,比要求业务利益相关者了解数据更为重要。
因此,我们鼓励你走出舒适区,尽你所能将数据科学变成一项团队运动,即使这真的很困难,感觉不舒服。当你成功时,这将是非常有回报的。记住——最具创新性的解决方案通常来自汇聚不同视角和方法的多元化团队。
最后,采用以数据为中心的方法不仅限于优化机器学习性能。它还涉及使用数据时必须遵守的伦理和公平的重要责任。
对人工智能的伦理和公平性负责
随着机器学习和人工智能的持续增长,这些技术在伦理和公平性方面的重要性也在增加。作为该领域的专家,你有责任确保你构建模型所依据的数据尽可能公平和无偏见。
机器学习是一种创造行为。它是一个塑造我们周围世界的流程,影响着我们与产品、服务甚至彼此的互动方式。机器学习模型具有塑造行为、塑造感知和建立规范的力量。
这种力量可以用于善行,创造提升生活质量和解决紧迫社会问题的产品。但也可以被滥用,导致有害的结果。作为机器学习解决方案的设计者,我们对自己设计及其对世界的影响负有责任至关重要。仅仅创建功能准确或技术先进的东西是不够的。
这意味着要主动识别潜在的偏见来源并采取措施减轻它们。这也意味着要透明地说明模型的构建和使用方式,并对它们的成果负责。通过承担这一责任,你将有助于建立对机器学习系统的信任,并确保它们以公平和有益于所有人的方式使用。
让数据成为每个人的业务——我们的经验
几年前,我(乔纳斯)接管了一个由数据工程师、BI 开发者、数据分析师和数据科学家组成的高技能团队的领导工作。
尽管这是一个非常胜任的团队,对他们的业务有坚实的理解,但他们很难发挥全部潜力。我迅速确定了团队的首要挑战:来自组织各处的利益相关者不断向团队提出基本的数据请求,这使得每个人都非常忙碌,但影响并不大。
简而言之,利益相关者习惯于请求原始数据或最少加工的数据,以便他们可以找到自己的见解,而团队则被培养成不情愿地接受这种操作模式。
数据和分析的零散使用表明,该组织没有认识到数据质量的重要性。他们还错过了通过更有组织地使用高级分析来大幅提升业务绩效的机会。
我们希望扭转这种局面,以便能够建立一个更结构化、更高效的用数据和分析方法。我们的目标是强调数据质量的重要性及其推动业务绩效提升的潜力。我们旨在改变从自助式原始数据到通过精心策划、高级分析提供有价值的见解的方法,从而将操作模式从被动转变为主动。
我们有三个重点领域:
- 
我们希望让人们意识到数据质量的重要性。我们成功定义是看到关键业务利益相关者转变为自荐的数据质量倡导者。
 - 
我们希望提供能够创造持久业务价值的高级数据产品。我们成功定义是看到我们的团队和业务利益相关者共同定义、构建和使用数据驱动的业务工具和产品。
 - 
我们希望数据成为公司和其客户的资产。我们成功定义是团队提供的数据驱动解决方案为客户和员工创造了持久的价值。
 
我们首先举办了全公司教育会议,强调数据质量的重要性以及使用机器学习改善业务绩效的潜力。人们点头表示同意,并称赞团队使复杂信息对普通观众易于理解。但我们没有看到我们希望的行为改变。
随着利益相关者受到启发,开始使用数据来指导他们的决策,基本请求的数量急剧增加。团队比以往任何时候都忙,试图从这些设计不佳的请求中找到最佳方案。
我们迅速决定改变我们的方法。首先,我们引入了一个要求,在承诺任何需要超过半天时间完成的新工作之前,必须先建立业务案例。
其次,我们与高级利益相关者进行了一系列研讨会,以确定和排名公司最大的痛点,这些痛点可以通过提供更好的高级分析来解决。
从这些研讨会中产生了三个主要项目,涉及组织各方的利益相关者:为面向客户的员工提供新的投资组合管理系统、为后台员工提供任务管理系统,以及一个流程自动化项目。所有三个项目都依赖于基于规则的机器学习算法,并需要提高对数据质量的关注。
项目依赖于业务利益相关者负责数据质量,并创建确保数据质量始终符合我们标准的流程。我们的团队与业务利益相关者紧密合作,确保业务问题和数据采集之间始终存在清晰的联系。
让我们非常兴奋的是,在很短的时间内,公司的会议议程中就包括了数据质量问题和发展计划。我们的高级利益相关者已经承担起倡导数据质量的职责,并对其团队负责。
这三个项目都取得了显著的成功,在同时增强跨职能协作的同时产生了稳健的数据产品。这些项目推动了数据驱动决策的转变,并在公司范围内强化了数据作为战略资产的认识。
这种重大转变之所以成为可能,仅仅是因为我的团队中的数据专业人士勇敢地走出他们的舒适区,使数据成为每个人的业务。
摘要
在本书中,你已经获得了宝贵的技能,现在你拥有了以数据为中心的方法达到机器学习下一个前沿的知识。然而,这些知识只有在你应用它们时才有用,而这需要努力。
这需要你使用本书中概述的工具和技术。它还要求你走出舒适区,在数据质量、跨职能协作和数据伦理方面发挥主导作用。你不能独自改变世界,所以把这本书交给同事,并让他们加入数据为中心的行列。
在我们结束之际,请记住,开始这段以数据为中心的机器学习之旅不是一个终点,而是一个持续的过程。数据和人工智能的领域始终在不断发展,我们自己也必须不断进步。让我们继续学习、创新,共同塑造数据科学和机器学习的未来。你的数据为中心的旅程才刚刚开始。
参考文献

                    
                
                
            
        
浙公网安备 33010602011771号