机器学*基础设施最佳实践-全-

机器学*基础设施最佳实践(全)

原文:annas-archive.org/md5/f3dcd9d910b2341a99f14dab450d4e9b

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

*年来,机器学*获得了极大的普及。GPT-3 和 4 等大型语言模型的引入加速了这一领域的发展。这些大型语言模型变得如此强大,以至于在本地计算机上训练它们几乎是不可能的。然而,这根本不是必要的。这些语言模型提供了创建新工具的能力,而无需训练它们,因为它们可以通过上下文窗口和提示来引导。

在这本书中,我的目标是展示如何训练、评估和测试机器学*模型——无论是在小型原型还是在完整软件产品中。这本书的主要目标是弥合机器学*在软件工程中的理论知识与实践应用之间的差距。它旨在使你具备理解、有效实施并在你的职业追求中创新 AI 和机器学*技术的技能。

将机器学*融入软件工程的旅程既令人兴奋又充满挑战。当我们深入研究机器学*基础设施的复杂性时,这本书充当了一本全面的指南,帮助软件工程师穿越复杂性和最佳实践,这些对于软件工程师至关重要。它旨在弥合机器学*的理论方面与在现实场景实施中面临的实际挑战之间的差距。

我们首先探讨机器学*的根本概念,为那些新进入该领域的人提供坚实的基础。随着我们的进展,焦点转向基础设施——任何成功机器学*项目的支柱。从数据收集和处理到模型训练和部署,每一步都至关重要,需要仔细考虑和规划。

书中很大一部分内容致力于最佳实践。这些实践不仅仅是理论上的指导方针,而是源自我的研究团队在这个领域工作中发现的真实经验和案例研究。这些最佳实践为处理常见陷阱和确保机器学*系统的可扩展性、可靠性和效率提供了无价的见解。

此外,我们深入探讨了数据和机器学*算法的伦理问题。我们研究了机器学*伦理背后的理论,更深入地探讨了数据和模型许可,最后,探讨了可以量化机器学*中数据和模型偏见的实际框架。

这本书不仅仅是一本技术指南;它是一次穿越软件工程中机器学*演变景观的旅程。无论你是渴望学*的初学者,还是寻求提升技能的资深专业人士,这本书旨在成为一项宝贵的资源,为机器学*这个充满活力且不断变化的领域提供清晰的方向。

本书面向的对象

本书精心制作,旨在为寻求在他们的领域中应用人工智能和机器学*实际应用的软件工程师、计算机科学家和程序员提供帮助。内容旨在传授关于与机器学*模型一起工作的基础知识,通过程序员的视角和系统架构师的视角来审视。

本书假设读者对编程原则有所了解,但并不要求在数学或统计学方面有专业知识。这种做法确保了软件开发领域的更广泛的专业人士和爱好者能够接触本书。对于那些没有 Python 先前经验的读者,本书需要掌握语言的基本理解。然而,材料结构旨在帮助快速全面地掌握 Python 的基本知识。相反,对于那些精通 Python 但尚未在专业编程方面有所经验的人,本书作为过渡到以 AI 和 ML 应用为重点的软件工程领域的宝贵资源。

本书涵盖的内容

第一章机器学*与传统软件的比较,探讨了这两种类型的软件系统最适用的情况。我们了解了程序员用来创建这两种类型软件的软件开发过程,以及我们学*了经典的四种机器学*软件类型 – 基于规则的、监督的、无监督的和强化学*。最后,我们还了解了数据在传统和机器学*软件中的不同作用。

第二章机器学*系统要素,回顾了专业机器学*系统的每个要素。我们首先了解哪些要素是重要的以及为什么重要。然后,我们探讨如何创建这些要素以及如何通过将它们组合成一个单一的机器学*系统(所谓的机器学*管道)来工作。

第三章软件系统中的数据 – 文本、图像、代码和特征,介绍了三种数据类型 – 图像、文本和格式化文本(程序源代码)。我们探讨了每种类型的数据如何在机器学*中使用,它们应该如何标注以及用于什么目的。引入这三种类型的数据为我们提供了探索不同标注这些数据来源方式的可能性。

第四章数据获取、数据质量和噪声,深入探讨了与数据质量相关的话题。我们介绍了一个评估数据质量的理论模型,并提供了实施它的方法和工具。我们还探讨了机器学*中的噪声概念以及如何通过不同的标记化方法来减少它。

第五章量化并改进数据属性,深入探讨了数据的属性以及如何改进它们。与上一章相比,我们专注于特征向量而不是原始数据。特征向量已经是数据的一种转换;因此,我们可以改变诸如噪声之类的属性,甚至改变数据被感知的方式。我们专注于文本的处理,这是当今许多机器学*算法的一个重要部分。我们首先理解如何使用简单的算法,如词袋模型,将数据转换为特征向量,以便我们可以在特征向量上工作。

第六章机器学*系统中数据处理,深入探讨了数据和算法相互交织的方式。我们经常用通用术语谈论数据,但在这章中,我们解释了机器学*系统中需要哪种类型的数据。我们解释了所有类型的数据都是以数值形式使用的——要么作为特征向量,要么作为更复杂的特征矩阵。然后,我们将解释将非结构化数据(例如文本)转换为结构化数据的必要性。本章将为深入探讨每种类型的数据奠定基础,这是下一章几章的内容。

第七章数值和图像数据的特征工程,专注于数值和图像数据的特征工程过程。我们首先回顾了典型的方法,例如主成分分析PCA),我们之前曾用它进行可视化。然后,我们转向更高级的方法,如t-Student 分布随机网络嵌入t-SNE)和独立成分分析ICA)。最终,我们使用自动编码器作为数值和图像数据的降维技术。

第八章自然语言数据的特征工程,探讨了使变换器(GPT)技术如此强大的第一步——从自然语言数据中提取特征。自然语言是软件工程中的一种特殊数据源。随着 GitHub Copilot 和 ChatGPT 的引入,变得明显的是,用于软件工程任务的机器学*和人工智能工具不再是科幻。

第九章机器学*系统类型 – 基于特征和原始数据(深度学*),探讨了不同类型的机器学*系统。我们从经典的机器学*模型,如随机森林开始,然后转向卷积和 GPT 模型,这些被称为深度学*模型。它们的名称来源于它们使用原始数据作为输入,并且模型的第一层包括特征提取层。它们也被设计为随着输入数据通过这些模型而逐步学*更抽象的特征。本章展示了这些模型类型,并从经典机器学*进展到生成式 AI 模型。

第十章经典机器学*系统和神经网络的训练与评估,对训练和评估过程进行了更深入的探讨。我们首先介绍不同算法背后的基本理论,然后展示它们是如何被训练的。我们从经典的机器学*模型开始,以决策树为例,然后逐渐转向深度学*,探索密集神经网络和一些更高级的网络类型。

第十一章高级机器学*算法的训练与评估 – GPT 和自编码器,探讨了基于 GPT 和双向编码器表示转换器(BERT)的生成式 AI 模型的工作原理。这些模型被设计为根据它们训练的模式生成新的数据。我们还探讨了自编码器的概念,其中我们训练一个自编码器根据之前训练的数据生成新的图像。

第十二章设计机器学*管道及其测试,描述了 MLOps 的主要目标是弥合数据科学和运维团队之间的差距,促进协作,并确保机器学*项目能够有效地和可靠地在规模上部署。MLOps 有助于自动化和优化整个机器学*生命周期,从模型开发到部署和维护,从而提高生产中机器学*系统的效率和效果。在本章中,我们学*如何在实践中设计和操作机器学*系统。本章展示了如何将管道转化为一个软件系统,重点关注测试 ML 管道及其在 Hugging Face 的部署。

第十三章设计和实现大规模、健壮的机器学*软件,解释了如何将机器学*模型与用 Gradio 编写的图形用户界面和数据库中的存储集成。我们使用两个机器学*管道的例子 – 来自我们之前章节的预测缺陷的模型示例和一个生成式 AI 模型,用于根据自然语言提示创建图片。

第十四章数据获取与管理中的伦理问题,首先探讨了几个不道德的系统示例,这些系统表现出偏见,例如惩罚某些少数群体的信用评级系统。我们还解释了使用开源数据和揭露受试者身份的问题。然而,本章的核心是对数据管理和软件系统伦理框架的解释和讨论,包括 IEEE 和 ACM 的行为准则。

第十五章机器学*系统中的伦理问题,重点关注机器学*系统中的偏见。我们首先探讨了偏见来源,并简要讨论了这些来源。然后,我们探讨了如何发现偏见,如何最小化它们,最后,如何向我们的系统用户传达潜在的偏见。

第十六章在生态系统中集成机器学*系统,解释了如何将机器学*系统打包成 Web 服务,使我们能够以非常灵活的方式将它们集成到工作流程中。我们不必编译或使用动态链接库,而可以部署通过 HTTP 协议使用 JSON 协议进行通信的机器学*组件。实际上,我们已经在使用 OpenAI 托管的 GPT-3 模型时看到了如何使用该协议。在本章中,我们探讨了创建自己的带有预训练机器学*模型的 Docker 容器,部署它,并将其与其他组件集成的可能性。

第十七章总结与下一步行动,回顾了所有最佳实践,并按章节进行了总结。此外,我们还探讨了机器学*和人工智能的未来可能给软件工程带来的影响。

为了充分利用本书

在这本书中,我们使用 Python 和 PyTorch,因此您需要在您的系统上安装这两个软件。我在 Windows 和 Linux 上使用过它们,但它们也可以在 Google Colab 或 GitHub Codespaces(两者都已测试过)这样的云环境中使用**。

本书涵盖的软件/硬件 操作系统要求
Python 3.11 Windows,Ubuntu,Debian Linux 或 Windows Subsystem for Linux (WSL)
PyTorch 2.1 Windows, Ubuntu 或 Debian Linux

如果您正在使用这本书的数字版,我们建议您亲自输入代码或从书的 GitHub 仓库(下一节中有一个链接)获取代码。这样做将帮助您避免与代码的复制和粘贴相关的任何潜在错误

下载示例代码文件

您可以从 GitHub 下载本书的示例代码文件:github.com/PacktPublishing/Machine-Learning-Infrastructure-and-Best-Practices-for-Software-Engineers。如果代码有更新,它将在 GitHub 仓库中更新。

我们还有来自我们丰富的书籍和视频目录的其他代码包可供选择,请访问github.com/PacktPublishing/。查看它们!

使用的约定

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

文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“模型本身是在 model = LinearRegression()这一行上面创建的。”

代码块设置如下:

def fibRec(n):
  if n < 2:
      return n
  else:
      return fibRec(n-1) + fibRec(n-2)

任何命令行输入或输出都按以下方式编写:

>python app.py

最佳实践

看起来像这样。

联系我们

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

总体反馈:如果您对本书的任何方面有任何疑问,请通过电子邮件发送至 customercare@packtpub.com,并在邮件主题中提及书名。

勘误表:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在本书中发现错误,我们将不胜感激,如果您能向我们报告,请访问www.packtpub.com/support/errata并填写表格。

盗版:如果您在互联网上以任何形式发现我们作品的非法副本,我们将不胜感激,如果您能提供位置地址或网站名称,请通过 copyright@packt.com 与我们联系,并提供材料的链接。

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

分享您的想法

一旦您阅读了《软件工程师的机器学*基础设施和最佳实践》,我们非常乐意听到您的想法!请点击此处直接进入本书的亚马逊评论页面并分享您的反馈。

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

下载本书的免费 PDF 副本

感谢您购买本书!

您喜欢随时随地阅读,但又无法携带您的印刷书籍到处走吗?

您的电子书购买是否与您选择的设备不兼容?

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

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

优惠远不止于此,您还可以获得独家折扣、时事通讯以及每天收件箱中的精彩免费内容。

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

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

下载本书的免费 PDF 副本

packt.link/free-ebook/978-1-83763-406-4

  1. 提交您的购买证明

  2. 就这样!我们将直接将您的免费 PDF 和其他福利发送到您的邮箱

第一部分:软件工程中的机器学*景观

传统上,机器学*(ML)被认为是软件工程中的一个细分领域。没有大型软件系统在生产中使用统计学*。这种情况在 2010 年代发生了变化,当时推荐系统开始利用大量数据 – 例如,推荐电影、书籍或音乐。随着转换技术的兴起,这种情况发生了变化。众所周知的产品,如 ChatGPT,普及了这些技术,并表明它们不再是细分产品,而是已经进入了主流软件产品和服务的领域。软件工程需要跟上步伐,我们需要知道如何基于这些现代机器学*模型创建软件。本书的第一部分,我们将探讨机器学*如何改变软件开发,以及我们需要如何适应这些变化。

本部分包含以下章节:

  • 第一章, 机器学*与传统软件的比较

  • 第二章, 机器学*系统的要素

  • 第三章, 软件系统中的数据 – 文本、图像、代码和特性

  • 第四章, 数据采集、数据质量和噪声

  • 第五章, 量化并改进数据特性

第一章:与传统软件相比,机器学*

机器学*软件是一种特殊的软件,它能在数据中找到模式,从中学*,甚至在新数据上重新创建这些模式。因此,开发机器学*软件的重点是找到合适的数据,将其与适当的算法匹配,并评估其性能。相反,传统软件的开发是以算法为前提的。基于软件需求,程序员开发解决特定任务的算法,然后对其进行测试。数据是次要的,尽管并非完全不重要。这两种类型的软件可以在同一软件系统中共存,但程序员必须确保它们之间的兼容性。

在本章中,我们将探讨这两种类型软件系统最合适的领域。我们将了解程序员用来创建这两种类型软件的软件开发过程。我们还将了解四种经典的机器学*软件类型——基于规则的学*、监督学*、无监督学*和强化学*。最后,我们将了解数据在传统和机器学*软件中的不同作用——在传统软件中作为预编程算法的输入,在机器学*软件中作为训练模型的输入。

本章介绍的最佳实践提供了何时选择每种类型软件以及如何评估这些类型软件优缺点的实用指导。通过探讨几个现代实例,我们将了解如何以机器学*算法为中心创建整个软件系统。

本章将涵盖以下主要内容:

  • 机器学*不是传统软件

  • 概率和软件——它们搭配得怎么样?

  • 测试和验证——相同但不同

机器学*不是传统软件

尽管机器学*和人工智能自 20 世纪 50 年代由艾伦·图灵引入以来就已经存在,但它们只有在第一个 MYCIN 系统出现以及我们对机器学*系统的理解随时间变化后才开始流行。直到 2010 年代,我们才开始以今天(2023 年)的方式感知、设计和开发机器学*。在我看来,有两个关键时刻塑造了我们今天所看到的机器学*格局。

第一个关键时刻是在 2000 年代末和 2010 年代初对大数据的关注。随着智能手机的引入,公司开始收集和处理越来越多的数据,这些数据大多关于我们在网上的行为。其中一家完美掌握这一技术的公司是谷歌,它收集了我们搜索、在线行为以及使用谷歌操作系统 Android 的数据。随着收集到的数据量(及其速度/速度)的增加,其价值和对真实性的需求——即五个“V”——也随之增加。这五个“V”——量、速度、价值、真实性和多样性——需要一种新的数据处理方法。传统的基于关系型数据库(SQL)的方法在处理高速数据流时变得过于缓慢,这导致了 map-reduce 算法、分布式数据库和内存数据库的出现。传统的基于关系模式的处理方法对于数据的多样性来说过于约束,这导致了非 SQL 数据库的出现,这些数据库存储文档。

第二个关键时刻是现代机器学*算法——深度学*的兴起。深度学*算法被设计来处理非结构化数据,如文本、图像或音乐(与表格和矩阵形式的结构化数据相比)。传统的机器学*算法,如回归、决策树或随机森林,需要表格形式的数据。每一行是一个数据点,每一列是它的一个特征——一个属性。传统的模型被设计来处理相对较小的数据集。另一方面,深度学*算法可以处理大型数据集,并利用大型神经网络和其复杂的架构找到数据中的更复杂模式。

机器学*有时被称为统计学*,因为它基于统计方法。统计方法计算数据的属性(如平均值、标准偏差和系数),从而在数据中找到模式。机器学*的核心特征是它使用数据来寻找模式,从中学*,然后在新的数据上重复这些模式。我们称这种学*模式的方式为训练,将这些模式重复为推理,或者用机器学*的语言来说,就是预测。使用机器学*软件的主要好处来自于我们不需要设计算法——我们专注于要解决的问题以及我们用来解决问题的数据。“图 1.1”展示了这样一个机器学*软件流程图的实现示例。

首先,我们从库中导入一个通用的机器学*模型。这个通用模型具有它特有的所有元素,但它并未训练用于解决任何任务。这类模型的例子是一个决策树模型,它被设计用来学*数据中的依赖关系,以决策(或数据拆分)的形式呈现,之后它将使用这些信息来处理新数据。为了使这个模型变得有些用处,我们需要对其进行训练。为此,我们需要数据,我们称之为训练数据。

第二步,我们在新的数据上评估训练好的模型,我们称之为测试数据。评估过程使用训练好的模型,并应用它来检查其推理是否正确。更精确地说,它检查推理正确的程度。训练数据与测试数据具有相同的格式,但这两个数据集的内容是不同的。不应该有任何数据点同时存在于两者中。

在第三步,我们将模型作为软件系统的一部分使用。我们开发其他非机器学*组件,并将它们连接到训练好的模型。整个软件系统通常包括数据采集组件、实时验证组件、数据清洗组件、用户界面和业务逻辑组件。所有这些组件,包括机器学*模型,都为最终用户提供特定的功能。一旦软件系统开发完成,就需要对其进行测试,这时输入数据就派上用场。输入数据是最终用户输入到系统中的数据,例如通过填写表格。输入数据被设计成既包含输入又包含预期的输出——以测试软件系统是否正确工作。

最后一步是部署整个系统。部署可能非常不同,但大多数现代机器学*系统被组织成两部分——用于非机器学*组件的机载/边缘算法和用户界面,以及用于机器学*推理的离岸/云算法。尽管有可能在目标设备上部署系统的所有部分(包括机器学*和非机器学*组件),但复杂的机器学*模型需要大量的计算能力以实现良好的性能和无缝的用户体验。原则很简单——更多的数据/更复杂的数据意味着更复杂的模型,这意味着需要更多的计算能力:

图 1.1 – 机器学*软件开发典型流程

图 1.1 – 机器学*软件开发典型流程

图 1**.1所示,机器学*软件的一个关键元素是模型,这是经过特定数据训练的通用机器学*模型之一,例如神经网络。这类模型用于进行预测和推理。在大多数系统中,这种组件——模型——通常是用 Python 进行原型设计和开发的。

模型针对不同的数据集进行训练,因此,机器学*软件的核心特征是其对那个数据集的依赖性。这样的模型的一个例子是视觉系统,我们训练一个机器学*算法,如卷积神经网络CNN),以对猫和狗的图像进行分类。

由于模型是在特定数据集上训练的,因此在进行推理时,它们在类似数据集上的表现最佳。例如,如果我们训练一个模型在 160 x 160 像素的灰度图像中识别猫和狗,该模型可以识别这样的图像中的猫和狗。然而,如果需要在该模型中识别彩色图像中的猫和狗而不是灰度图像,该模型的性能将非常差(如果有的话!)——分类的准确性将很低(接* 0)。

另一方面,当我们开发和设计传统软件系统时,我们并不那么依赖数据,如图1.2所示。这张图概述了传统、非机器学*软件的开发过程。尽管它被描绘为流程,但通常是一个迭代过程,其中步骤 1步骤 3是循环进行的,每次循环都以向产品中添加新功能结束。

第一步是开发软件系统。这包括开发所有组件——用户界面、业务逻辑(处理)、数据处理和通信。除非软件工程师为了测试目的创建数据,否则这一步不涉及太多数据。

第二步是系统测试,我们使用输入数据来验证软件系统。本质上,这一步几乎与测试机器学*软件相同。输入数据与预期的结果数据相结合,这使得软件测试人员能够评估软件是否正确工作。

第三步是部署软件。部署可以通过多种方式进行。然而,如果我们考虑与机器学*软件在功能上相似的传统的软件,它通常更简单。它通常不需要在云上进行部署,就像机器学*模型一样:

图 1.2 - 传统软件开发典型流程

图 1.2 - 传统软件开发典型流程

传统软件与基于机器学*的软件之间的主要区别在于,我们需要设计、开发和测试传统软件的所有元素。在基于机器学*的软件中,我们使用一个包含所有必要元素的空模型,并使用数据对其进行训练。我们不需要从头开始开发机器学*模型的各个组件。

传统软件的主要部分之一是算法,这是软件工程师从头开始根据需求或用户故事开发的。算法通常被编写为一系列按编程语言实现的步骤。自然地,所有算法都使用数据来操作它,但它们与机器学*系统不同。它们基于软件工程师的设计——如果 x,则 y或类似的东西。

我们通常将这些传统算法视为确定性的、可解释的和可追溯的。这意味着软件工程师的设计决策在算法中得到了记录,并且算法可以在之后进行分析。它们是确定性的,因为它们是基于规则编写的;没有从数据中训练或从数据中识别模式。它们是可解释的,因为它们是由程序员设计的,程序中的每一行都有预定义的含义。最后,它们是可追溯的,因为我们能够调试这些程序的每一步。

然而,也存在一个缺点——软件工程师需要彻底考虑所有边界情况,并且非常了解问题。软件工程师使用的这些数据只是为了支持他们分析算法,而不是用于训练算法。

一个可以使用机器学*算法和传统算法实现的系统示例是读取护照信息的系统。而不是使用机器学*进行图像识别,软件使用护照中的特定标记(通常是<<<字符序列)来标记行首或表示姓氏的字符序列的开始。这些标记可以使用基于规则的光学字符识别OCR)算法快速识别,无需深度学*或 CNN。

因此,我想介绍第一条最佳实践。

最佳实践 #1

当你的问题集中在数据而不是算法时,使用机器学*算法。

在选择合适的技术时,我们需要了解它是否基于经典方法,其中算法的设计是重点,或者我们是否需要专注于处理数据和在其中寻找模式。通常,以下指导方针是有益的。

如果问题需要处理大量原始格式的数据,则使用机器学*方法。这类系统的例子包括对话机器人、图像识别工具、文本处理工具,甚至是预测系统。

然而,如果问题需要可追溯性和控制,则使用传统方法。这类系统的例子包括汽车中的控制软件(如防抱死制动系统、发动机控制等)和嵌入式系统。

如果问题需要根据现有数据生成新数据,即所谓的数据处理过程,请使用机器学*方法。这类系统的例子包括图像处理程序(DALL-E)、文本生成程序、深度伪造程序和源代码生成程序(GitHub Copilot)。

如果问题需要随着时间的推移进行适应和优化,请使用机器学*软件。这类系统的例子包括电网优化软件、计算机游戏中的非玩家角色行为组件、播放列表推荐系统,甚至现代汽车的 GPS 导航系统。

然而,如果问题需要稳定性和可追溯性,请使用传统方法。这类系统的例子包括医学中的诊断和推荐系统、汽车、飞机和火车中的安全关键系统,以及基础设施控制和监控系统。

监督学*、无监督学*和强化学*——这只是开始

现在是提到机器学*领域非常庞大的时候了,它被组织成三个主要领域——监督学*、无监督学*和强化学*。每个领域都有数百种不同的算法。例如,监督学*领域有超过 1,000 种算法,所有这些都可以通过元启发式算法如 AutoML 自动选择:

  • 监督学*:这是一组基于标注数据进行训练的算法。这些算法中使用的数据需要有一个目标标签。标签用于告诉算法寻找哪种模式。例如,这样的标签可以是监督学*模型需要识别的每张图像的。历史上,监督学*算法是最古老的,因为它们直接来自如线性回归和多项式回归等统计方法。现代算法更先进,包括深度学*神经网络等方法,可以识别 3D 图像中的对象并相应地进行分割。该领域最先进的算法是深度学*和多模态模型,它们可以同时处理文本和图像。

    监督学*算法的一个子集是自监督模型,它们通常基于转换器架构。这些模型不需要数据中的标签,但它们使用数据本身作为标签。这些算法中最突出的例子是自然语言的翻译模型和图像或文本的生成模型。这些算法通过在原始文本中遮蔽单词并预测它们来训练。对于生成模型,这些算法通过遮蔽其输出的部分来预测它进行训练。

  • 无监督学*:这是一组应用于在数据中寻找模式而不需要任何标签的模型。这些模型未经训练,但它们使用输入数据的统计特性来寻找模式。这类算法的例子包括聚类算法和语义映射算法。这些算法的输入数据没有标签,应用这些算法的目标是根据相似性在数据集中找到结构;然后,这些结构可以用来为这些数据添加标签。当我们得到购买产品、阅读书籍、听音乐或看电影的建议时,我们每天都会遇到这些算法。

  • 强化学*:这是一组应用于数据以解决特定任务的模型。对于这些模型,我们除了需要提供数据外,还需要提供目标。这被称为奖励函数,它是一个定义我们何时达到目标的表达式。模型基于这个适应函数进行训练。这类模型的例子包括玩围棋、象棋或星际争霸的算法。这些算法也被用来解决困难的编程问题(AlphaCode)或优化能耗。

因此,让我介绍第二个最佳实践。

最佳实践 #2

在你开始开发机器学*系统之前,要进行尽职调查,并确定使用正确的算法组。

由于这些模型组各自具有不同的特性,解决不同的问题,并需要不同的数据,选择错误算法可能会造成高昂的代价。监督模型在解决与预测和分类相关的问题上非常出色。在这个领域中最强大的模型可以在选定的领域与人类竞争——例如,GitHub Copilot 可以创建可以以人类编写的程序。无监督模型在需要将实体分组并做出推荐时非常强大。最后,当我们需要连续优化并且每次数据或环境变化时都需要重新训练模型时,强化学*模型是最好的选择。

尽管所有这些模型都基于统计学*,但它们都是更大系统的一部分,以便使它们变得有用。因此,我们需要了解机器学*的这种概率和统计性质如何与传统、数字软件产品相结合。

传统机器学*和机器学*软件的例子

为了说明传统软件和机器学*软件之间的区别,让我们使用这两种范式实现相同的程序。我们将使用传统方法实现一个计算斐波那契序列的程序,这在计算机科学课程中我们已经见过无数次。然后,我们将使用机器学*模型——确切地说是一个模型——来实现相同的程序,即逻辑回归。

这里展示了传统的实现方式。它基于一个递归函数和一个测试它的循环:

# a recursive function to calculate the fibonacci number
# this is a standard solution that is used in almost all
# of computer science examples
def fibRec(n):
  if n < 2:
      return n
  else:
      return fibRec(n-1) + fibRec(n-2)
# a short loop that uses the above function
for i in range(23):
  print(fibRec(i))

实现非常简单,基于算法——在我们的案例中,是fibRec函数。它很简单,但有其局限性。第一个是其递归实现,这会消耗资源。尽管它可以写成迭代形式,但它仍然存在第二个问题——它专注于计算而不是数据。

现在,让我们看看机器学*实现的步骤。我将通过将其分为两部分来解释——数据准备和模型训练/推理:

#predicting fibonacci with linear regression
import pandas as pd
import numpy as np
from sklearn.linear_model import LinearRegression
# training data for the algorithm
# the first two columns are the numbers and the third column is the result
dfTrain = pd.DataFrame([[1, 1, 2],
                        [2, 1, 3],
                        [3, 2, 5],
                        [5, 3, 8],
                        [8, 5, 13]
])
# now, let's make some predictions
# we start the sequence as a list with the first two numbers
lstSequence = [0,1]
# we add the names of the columns to make it look better
dfTrain.columns = ['first number','second number','result']

在机器学*软件的情况下,我们准备数据来训练算法。在我们的案例中,这是dfTrain DataFrame。它是一个表格,包含机器学*算法需要找到模式的数字。

请注意,我们准备了两个数据集——dfTrain,其中包含训练算法所需的数字,以及lstSequence,这是我们稍后将找到的斐波那契数列。

现在,让我们开始训练算法:

# algorithm to train
# here, we use linear regression
model = LinearRegression()
# now, the actual process of training the model
model.fit(dfTrain[['first number', 'second number']],
                               dfTrain['result'])
# printing the score of the model, i.e. how good the model is when trained
print(model.score(dfTrain[['first number', 'second number']], dfTrain['result']))

整个代码片段的魔力在于加粗的代码——model.fit方法的调用。此方法基于我们为其准备的数据训练逻辑回归模型。模型本身是在上一行的model = LinearRegression()行中创建的。

现在,我们可以使用以下代码片段进行推理或创建新的斐波那契数:

# and loop through the newly predicted numbers
for k in range(23):
  # the line below is where the magic happens
  # it takes two numbers from the list
  # formats them to an array
  # and makes the prediction
  # since the model returns a float,
  # we need to convert it to it
  intFibonacci = int(model.predict(np.array([[lstSequence[k],lstSequence[k+1]]])))
  # add this new number to the list for the next iteration
  lstSequence.append(intFibonacci)
  # and print it
  print(intFibonacci)

此代码片段包含与上一个类似的行——model.predict()。此行使用先前创建的模型进行推理。由于斐波那契数列是递归的,我们需要在lstSequence.append()行中将新创建的数字添加到列表中,然后才能进行新的推理。

现在非常重要的一点是要强调这两种解决相同问题的方法之间的区别。传统的实现暴露了用于创建数字的算法。在那里我们看不到斐波那契数列,但我们可以看到它是如何计算的。机器学*实现暴露了用于创建数字的数据。我们看到第一个序列作为训练数据,但我们从未看到模型是如何创建该序列的。我们不知道该模型是否总是正确的——我们需要将其与真实序列进行测试——仅仅因为我们不知道算法是如何工作的。这引出了下一个部分,也就是关于概率的部分。

概率与软件——它们如何相得益彰

使机器学*软件与传统软件不同的基本特征是,机器学*模型的核心是统计学。这种统计学*意味着机器学*模型的输出是一个概率,因此它不如传统软件系统那样清晰。

概率,即模型的结果,意味着我们收到的答案是某事的概率。例如,如果我们对一张图片进行分类以检查它是否包含狗或猫,这个分类的结果就是一个概率——例如,有 93% 的概率该图片包含狗,有 7% 的概率包含猫。这如图 图 1**.3 所示:

图 1.3 – 机器学*软件的概率性质

图 1.3 – 机器学*软件的概率性质

要在其他软件部分或其他系统中使用这些概率结果,机器学*软件通常使用阈值(例如,如果 x<0.5)来只提供单一结果。这些阈值指定了哪些概率是可以接受的,以便将结果考虑为属于特定类别。以我们的图像分类为例,这个概率将是 50%——如果识别图像中狗的概率大于 50%,则模型表示该图像包含狗(不包含概率)。

将这些概率结果转换为数字,就像我们在前面的例子中所做的那样,通常是正确的,但并不总是如此。特别是在边缘情况下,例如当概率接*阈值的下限时,分类可能导致错误,从而引起软件故障。这样的故障通常是可以忽略的,但并不总是如此。在安全关键系统中,不应有任何错误,因为它们可能导致不必要的危害,并可能产生灾难性的后果。

在机器学*软件的概率性质成为问题,但我们仍然需要机器学*以利用其其他好处的情况下,我们可以构建机制来减轻误判、误分类和次优化的后果。这些机制可以保护机器学*模型,防止它们提出错误的建议。例如,当我们使用机器学*图像分类在汽车的安全系统中时,我们在模型周围构建了一个所谓的 安全笼。这个安全笼是一个非机器学*组件,它使用规则来检查在特定环境中某个建议、分类或预测是否合理。例如,它可以防止汽车在高速公路上突然停车,因为这是由前摄像头摄像头的误分类引起的。

因此,让我们看看另一个最佳实践,它鼓励在安全关键系统中使用机器学*软件。

最佳实践 #3

如果你的软件是安全关键的,确保你可以设计机制来防止由机器学*软件的概率性质引起的安全隐患。

尽管这个最佳实践是针对关键安全系统制定的,但它比这更普遍。即使是任务关键或业务关键系统,我们也可以构建机制来门控机器学*模型,并防止整个软件系统的错误行为。如何构建这样一个笼子的一个例子在图 1**.4中显示,其中门控器组件提供了一个额外的信号,表明模型的预测不可信/不可用:

图 1.4 – 机器学*模型的门控

图 1.4 – 机器学*模型的门控

在此图中,附加组件被放置在处理管道的最后,以确保结果始终是二元的(对于这种情况)。在其他情况下,这样的门控器可以与机器学*模型并行放置,并可以作为并行处理流程,检查数据质量而不是分类模型。

这样的门控器模型被相当频繁地使用,例如在感知系统检测对象时 – 模型检测单个图像中的对象,而门控器检查在一系列连续图像中是否一致地识别出相同的对象。它们可以形成冗余的处理通道和管道。它们可以形成可行性检查组件,或者可以将超出范围的输出结果更正为适当的值。最后,它们还可以将机器学*组件从管道中断开,并将这些管道适应软件的其他组件,通常是做出决策的算法 – 从而形成自适应或自修复的软件系统。

机器学*软件的这种概率性质意味着部署前的活动与传统软件不同。特别是,测试机器学*和传统软件的过程是不同的。

测试和评估 – 相同但不同

每个机器学*模型都需要经过验证,这意味着模型需要能够为模型之前未见过的一个数据集提供正确的推理。目标是评估模型是否在数据中学*了模式,数据本身,或者两者都不是。分类问题中正确性的典型度量是准确率(正确推断的实例数与所有分类实例数的商),曲线下面积/接收器操作特性AUROC),以及真正例率TPR)和假正例率FPR)。

对于预测问题,模型的质量通过误预测来衡量,例如均方误差MSE)。这些度量量化了预测中的错误 – 值越小,模型越好。图 1**.5显示了最常见形式监督学*的评估过程:

图 1.5 – 监督学*模型评估过程

图 1.5 – 监督学*模型评估过程

在这个过程中,模型在每次训练迭代中都会接受不同的数据,之后它被用来对相同的测试数据进行推断(分类或回归)。测试数据在训练之前就被留出,并且仅在验证时作为模型的输入,永远不会在训练期间使用。

最后,一些模型是强化学*模型,其质量通过模型根据预定义函数(奖励函数)优化输出的能力来评估。这些措施允许算法优化其操作并找到最优解——例如,在遗传算法、自动驾驶汽车或能源网格操作中。这些模型的挑战在于没有单一的指标可以衡量性能——它取决于场景、函数以及模型接收到的训练量。一个著名的此类训练的例子是来自 1983 年电影《战争游戏》中的算法,其中主要超级计算机玩数百万个井字棋游戏,以理解没有获胜策略——游戏没有赢家。

图 1.6以图形方式展示了训练强化系统的过程:

图 1.6 – 强化学*训练过程

图 1.6 – 强化学*训练过程

我们可能会产生这样的印象,当开发机器学*软件时,我们只需要训练、测试和验证机器学*模型。这远远不是真的。这些模型是更大系统的一部分,这意味着它们需要与其他组件集成;这些组件在图 1.5图 1.6中描述的验证过程中并未得到验证。

每个软件系统在发布之前都需要经过严格的测试。测试的目标是尽可能多地发现和消除缺陷,以便软件用户能够体验到最佳的质量。通常,测试软件的过程是一个包含多个阶段的流程。测试过程遵循软件开发过程,并与之一致。最初,软件工程师(或测试人员)使用单元测试来验证其组件的正确性。

图 1.7展示了这三种测试类型是如何相互关联的。在单元测试中,重点是算法。通常,这意味着软件工程师必须测试单个函数和模块。集成测试关注模块之间的连接以及它们如何共同执行任务。最后,系统测试和验收测试关注整个软件产品。测试人员模仿真实用户来检查软件是否满足用户的需求:

图 1.7 – 三种软件测试类型 – 单元测试(左)、集成测试(中)和系统及验收测试(右)

图 1.7 – 三种软件测试类型 – 单元测试(左)、集成测试(中)和系统及验收测试(右)

软件测试过程与模型验证过程非常不同。尽管我们可以将单元测试误认为是模型验证,但这并不完全正确。模型验证过程的输出是其中一个指标(例如,准确率),而单元测试的输出是true/false——软件是否产生了预期的输出。对于软件公司来说,没有已知的缺陷(相当于错误的测试结果)是可以接受的。

在传统的软件测试中,软件工程师准备一系列测试用例来检查他们的软件是否按照规格工作。在机器学*软件中,测试过程基于留出一部分数据集(测试集)并检查训练好的模型(在训练集上)在该数据上的表现如何。

因此,这是我关于测试机器学*系统的第四个最佳实践。

最佳实践 #4

将机器学*软件测试作为机器学*模型开发典型训练-验证-评估流程的补充。

测试整个系统非常重要,因为整个软件系统包含处理机器学*组件概率性质的机制。其中一种机制是安全笼机制,我们可以监控机器学*组件的行为,防止它们向系统其他部分提供低质量信号(在边缘情况、接*决策边界、推理过程中)。

当我们测试软件时,我们也会了解机器学*组件的限制以及我们处理边缘情况的能力。这种知识对于在需要为软件指定操作环境时部署系统非常重要。我们需要了解与软件需求和规格相关的限制——我们软件的使用案例。更重要的是,我们需要了解软件使用的伦理和可靠性方面的含义。

我们将在第十五章第十六章中讨论伦理问题,但重要的是要理解我们需要从一开始就考虑伦理问题。如果我们不这样做,我们的系统可能会犯出潜在的有害错误,例如大型人工智能招聘系统、人脸识别系统或自动驾驶汽车所犯的错误。这些有害错误涉及货币成本,但更重要的是,它们涉及对产品的信任损失,甚至错失机会。

摘要

机器学*与传统软件通常被视为两种替代品。然而,它们更像是兄弟姐妹——一个不能没有另一个。机器学*模型在解决约束性问题方面非常出色,但它们需要传统软件进行数据收集、准备和展示。

机器学*模型的概率性质需要额外的元素才能使它们在完整的软件产品环境中变得有用。因此,我们需要接受这种性质并利用它来发挥优势。即使对于安全关键系统,如果我们知道如何设计安全机制来防止危险后果,我们也可以(并且应该)使用机器学*。

在本章中,我们探讨了机器学*软件与传统软件之间的差异,同时关注如何设计能够包含这两部分的软件。我们还展示了机器学*软件不仅仅是训练、测试和评估模型,我们还展示了严格的测试对于部署可靠的软件是有意义且必要的。

现在,是时候进入下一章了,我们将揭开机器学*软件的黑箱,探讨我们需要开发一个完整的机器学*软件产品所需的内容——从数据采集开始,到用户交互结束。

参考文献

  • Shortliffe, E.H. 等,基于计算机的临床治疗咨询:MYCIN 系统的解释和规则获取能力。计算机与生物医学研究,1975 年,第 8 卷第 4 期: p. 303-320。

  • James, G. 等,统计学*引论。第 112 卷。Springer,2013 年。

  • Saleh, H.,机器学*基础:使用 Python 和 scikit-learn 开始使用机器学*的最新发展。Packt 出版公司,2018 年。

  • Raschka, S. 和 V. Mirjalili, Python 机器学*:使用 Python、scikit-learn 和 TensorFlow 2 的机器学*和深度学*,Packt 出版公司,2019 年。

  • Sommerville, I.,软件工程。第 10 版。软件工程书籍。第 10 版,软件工程系列,2015 年。

  • Houpis, C.H.,G.B. Lamont 和 B. Lamont,数字控制系统:理论、硬件、软件。McGraw-Hill,纽约,1985 年。

  • Sawhney, R.,人工智能能否使软件开发更高效?LSE 商业评论,2021 年。

  • He, X.,K. Zhao 和 X. Chu,AutoML:最新技术的调查。知识库系统。2021 年,第 212 卷: p. 106622。

  • *Reed, S. 等,通用代理。arXiv 预印本 arXiv:2205.06175,2022 年。

  • Floridi, L. 和 M. Chiriatti,GPT-3:其本质、范围、局限性和后果。心智与机器,2020 年,第 30 卷第 4 期: p. 681-694。

  • Creswell, A. 等,生成对抗网络:概述。IEEE 信号处理杂志,2018 年,第 35 卷第 1 期: p. 53-65。

  • Celebi, M.E. 和 K. Aydin,无监督学*算法。Springer,2016 年。

  • Chen, J.X., 计算机发展的演变:AlphaGo。科学工程计算,2016 年。18(4): p. 4-7.

  • Ko, J.-S., J.-H. Huh, and J.-C. Kim, 改进应用于工业 4.0 数据中心冷却系统风扇的能源效率和控制性能。电子学,2019 年。8(5): p. 582.

  • Dastin, J., 亚马逊废弃了显示对女性有偏见的秘密 AI 招聘工具。在《数据与分析伦理》中。2018 年,Auerbach 出版社。 p. 296-299.

  • Castelvecchi, D., 面部识别技术是否偏见过大以至于不能放任?自然,2020 年。587(7834): p. 347-350.

  • Siddiqui, F., R. Lerman, 和 J.B. Merrill,自去年以来报告的特斯拉 Autopilot 涉及 273 起事故。在《华盛顿邮报》中。2022 年。

第二章:机器学*系统的要素

数据和算法对于机器学*系统至关重要,但它们远远不够。算法是生产级机器学*系统中最小的一部分。机器学*系统还需要数据、基础设施、监控和存储才能高效运行。对于大规模机器学*系统,我们需要确保我们可以包含一个良好的用户界面或包装模型在微服务中。

在现代软件系统中,结合所有必要的元素需要不同的专业能力——包括机器学*/数据科学工程专业知识、数据库工程、软件工程,以及最终的用户交互设计。在这些专业系统中,提供可靠的结果,为用户提供价值,比包含大量不必要的功能更为重要。同时,也很重要将机器学*的所有元素(数据、算法、存储、配置和基础设施)协同起来,而不是单独优化每一个元素——所有这些都是为了提供一个最优化系统,用于满足一个或多个最终用户的使用案例。

在本章中,我们将回顾专业机器学*系统的每个要素。我们将首先了解哪些要素是重要的以及为什么重要。然后,我们将探讨如何创建这些要素以及如何将它们组合成一个单一的机器学*系统——所谓的机器学*管道。

在本章中,我们将涵盖以下主要主题:

  • 机器学*不仅仅是算法和数据

  • 数据和算法

  • 配置和监控

  • 基础设施和资源管理

  • 机器学*管道

生产级机器学*系统的要素

现代机器学*算法非常强大,因为它们使用大量数据,并包含大量可训练的参数。目前可用的最大模型是来自 OpenAI 的生成预训练转换器 3GPT-3)(拥有 1750 亿个参数)和来自 NVIDIA 的 Megatron-Turing(3560 亿个参数)。这些模型可以创建文本(小说)和进行对话,还可以编写程序代码、创建用户界面或编写需求。

现在,这样的大型模型无法在台式电脑、笔记本电脑甚至专用服务器上使用。它们需要先进的计算基础设施,能够承受长期训练和评估这样的大型模型。这样的基础设施还需要提供自动为这些模型提供数据、监控训练过程,以及最终为用户提供访问模型进行推理的可能性。提供这种基础设施的现代方式之一是机器学*即服务MLaaS)。MLaaS 为数据分析师或软件集成商提供了一个简单的方式来使用机器学*,因为它将基础设施的管理、监控和配置委托给专业公司。

图 2**.1 展示了基于现代机器学*软件系统的元素。从那时起,谷歌已经使用这些来描述生产机器学*系统。尽管这种设置存在变化,但原则仍然适用:

图 2.1 – 生产机器学*系统的元素

图 2.1 – 生产机器学*系统的元素

在这里,机器学*模型(ML 代码)是这些元素中最小的(谷歌,根据创意共享 4.0 属性许可,developers.google.com/machine-learning/crash-course/production-ml-systems)。从实际源代码的角度来看,在 Python 中,模型创建、训练和验证只需要三行代码(至少对于某些模型):

model = RandomForestRegressor(n_estimators=10, max_depth=2)
model.fit(X_train, Y_train)
Y_pred = model.predict(X_test)

第一行从模板创建模型——在这种情况下,它是一个包含 10 棵树的随机森林模型,每棵树最多有两个分割。随机森林是一种集成学*方法,在训练过程中构建多个决策树,并输出给定输入的各个树的类(分类)的模态。通过聚合多个树的结果,它减少了过拟合,并比单个决策树提供了更高的准确性。第二行基于训练数据(X_train,其中只包含预测者/输入特征,以及 Y_train,其中包含预测的类/输出特征)训练模型。最后,最后一行对测试数据(X_test)进行预测,以便在后续步骤中将预测结果与先知(预期值)进行比较。尽管这一行 model.predict(X_test) 不是生产系统的一部分,但我们仍然需要进行推断,因此我们的软件中始终存在类似的行。

因此,我们可以介绍下一个最佳实践。

最佳实践 #5

在设计机器学*软件时,应优先考虑数据和要解决的问题,而不是算法。

在这个例子中,我们从软件工程师的角度看到,机器学*代码相当小。在应用算法之前,我们需要正确准备数据,因为算法(model.fit(X_train, Y_train))需要数据以特定格式存在——第一个参数是用于推断的数据(所谓的特征向量或输入数据样本),而第二个参数是目标值(所谓的决策类、参考值或目标值,具体取决于算法)。

数据和算法

现在,如果使用算法不是机器学*代码的主要部分,那么其他部分必须是——也就是说,数据处理。如图 图 2**.1 所示,在机器学*软件中管理数据包括三个领域:

  1. 数据收集。

  2. 特征提取。

  3. 数据验证。

尽管我们将在整本书中回顾这些领域,但让我们来看看它们包含的内容。图 2**.2 展示了这些领域的处理流程:

图 2.2 – 数据收集和准备流程

图 2.2 – 数据收集和准备流程

注意,为算法准备数据的过程可能变得相当复杂。首先,我们需要从其来源中提取数据,这通常是一个数据库。它可以是测量、图像、文本或其他任何原始数据的数据库。一旦我们导出/提取了所需的数据,我们必须以原始数据格式存储它。这可以是表格的形式,如图中所示,也可以是一组原始文件,如图像。

数据收集

数据收集是将数据从其原始格式转换为机器学*算法可以接受的输入格式的过程。根据数据和算法的不同,这个过程可以采取不同的形式,如图 图 2**.3 所示:

图 2.3 – 不同形式的数据收集 – 示例

图 2.3 – 不同形式的数据收集 – 示例

来自图像和测量(如时间序列)的数据通常被收集来进行分类和预测。这两类问题需要可用的事实数据,这在之前的代码示例中我们将其视为 Y_train。这些目标标签要么从原始数据中自动提取,要么通过标记过程手动添加。手动过程耗时较长,因此更倾向于自动化。

用于非监督学*和强化学*模型的数据通常被提取为无标签的表格数据。这些数据用于决策过程或优化过程,以找到给定问题的最佳解决方案。没有优化,我们的结果可能无法代表新数据。前面的图显示了此类问题的两个示例——工业 4.0 智能工厂的优化和自动驾驶汽车。在智能工厂中,强化学*模型用于优化生产过程或控制所谓的暗工厂,这些工厂完全不需要人工干预(暗工厂这个名字来源于这些工厂不需要灯光;机器人不需要灯光工作)。

用于现代自监督模型的常用数据通常来自文本或语音等来源。这些模型不需要数据的表格形式,但需要结构。例如,为了训练文本转换器模型,我们需要按句子(或段落)对文本进行分词,以便模型学*单词的上下文。

因此,接下来是我的下一个最佳实践。

最佳实践 #6

一旦你探索了你想要解决的问题,并了解了数据的可用性,决定你是否想使用监督学*、自监督学*、无监督学*或强化学*算法。

我们需要为不同的算法使用不同的数据是自然的。然而,我们还没有讨论如何决定算法。仅当我们想要静态地预测或分类数据时,选择监督学*才有意义——也就是说,我们训练模型,然后使用它进行推断。当我们训练并做出推断时,模型不会改变。我们没有进行调整,并且算法的重新训练是定期进行的——我把这称为“训练一次,预测多次”原则。

当我们在没有目标类的情况下使用/训练和应用算法时,我们可以选择无监督方法。其中一些算法也用于根据数据的属性对数据进行分组,例如,进行聚类。我把这称为“训练一次,预测一次”原则。

对于自监督模型,情况要有趣得多。在那里,我们可以使用一种称为“预训练”的方法。预训练意味着我们可以在没有任何特定上下文的大数据语料库上训练一个模型——例如,我们在维基百科的英文文本大型语料库上训练语言模型。然后,当我们想要将模型用于特定任务时,例如编写新文本,我们会在该任务上对其进行更多训练。我把这称为“训练多次,预测一次”原则,因为我们必须为每个任务进行预训练和训练模型。

最后,强化学*需要每次使用模型时都发生变化的数据。例如,当我们使用强化学*算法优化一个过程时,它每次使用时都会更新模型——我们可以说是从错误中学*。我把这称为“训练多次,预测多次”原则。

通常,原始数据不适合用于机器学*,因为它可能包含空数据点、噪声或损坏的文件。因此,我们需要清理这些错误数据点,例如通过删除空数据点(使用 Python 命令如dataFrame.dropna(…))或使用数据插补技术。

现在,删除数据点和它们的插补之间存在根本性的区别。数据插补过程是我们根据相似数据点添加数据缺失属性。这就像在数字序列中填补空白——1, 2, 3, …, 5,其中我们填补数字 4。尽管填补数据增加了可用的数据点数量(从而使得模型更好),但它可以加强数据的某些属性,这可能导致模型学*。当数据量较小时,插补也相关;在大数据集中,删除缺失数据点更好(更资源高效且公平)。有了这个,我们就来到了我的下一个最佳实践。

最佳实践 #7

只有当你知道你希望加强哪些数据属性时,才使用数据插补,并且只为小数据集这样做。

最后,一旦我们有了可以工作的干净数据,我们就可以提取特征。在那里,我们可以使用针对我们手头问题的特定算法。例如,当我们处理文本数据时,我们可以使用简单的词袋模型来计算单词的频率,尽管我们也可以使用 word2vec 算法来嵌入单词共现的频率(我们将在下一章讨论的算法)。一旦我们提取了特征,我们就可以开始验证数据。这些特征可以强调我们之前没有看到的数据的某些属性。

以下是一个这样的例子:噪声——当我们有特征格式的数据时,我们可以检查数据中是否存在属性类别噪声。类别噪声是与标签错误相关的一种现象——一个或多个数据点被错误地标记。类别噪声可以是矛盾示例或错误标记的数据点。这是一个危险的现象,因为它可能导致在训练和使用机器学*模型时性能低下。

属性噪声是指一个(或多个)属性被错误值所损坏。例如,包括错误值、缺失属性(特征)值和不相关值。

一旦数据经过验证,就可以用于算法。所以,让我们深入探讨数据处理管道的每个步骤。

现在,由于不同的算法以不同的方式使用数据,数据具有不同的形式。让我们探索每种算法应该如何构建数据结构。

特征提取

将原始数据转换为算法可用的格式的过程称为特征提取。这是一个应用特征提取算法来寻找数据中感兴趣属性的过程。提取特征算法根据问题和数据类型的不同而有所不同。

当我们处理文本数据时,我们可以使用几个算法,但让我来展示最简单的一个——词袋模型的使用。该算法简单地计算句子中单词的出现次数——它要么计算一个预定义的单词集,要么使用统计方法来找到最频繁的单词。让我们考虑以下句子:

Mike is a tall boy.

当我们无约束地使用词袋算法时,它提供了以下表格:

句子 ID Mike Is A tall Boy
0 1 1 1 1 1

图 2.4 – 使用词袋模型提取的特征

该表包含句子中的所有单词作为特征。对于单个句子来说,它并不很有用,但如果我们添加另一个(句子 1),事情就会变得更加明显。所以,让我们添加以下句子:

Mary is a smart girl.

这将导致以下特征表:

句子 ID Mike Is A Tall boy smart girl
0 1 1 1 1 1 0 0
1 0 1 1 0 0 1 1

图 2.5 – 从两个句子中提取的特征

现在,我们已经准备好将标签列添加到数据中。假设我们想要将每个句子标记为正面或负面。然后表格将增加一列 – label – 其中 1 表示句子是正面的,否则为 0

句子 ID Mike Is A Tall boy smart girl Label
0 1 1 1 1 1 0 0 1
1 0 1 1 0 0 1 1 1

图 2.6 – 添加到数据中的标签

现在,这些特征使我们能够看到两个句子之间的差异,然后我们可以使用这些差异来训练和测试机器学*算法。

然而,这种方法有两个重要的局限性。第一个局限性是,将所有句子中的所有单词作为列/特征是不切实际(如果不是不可能)的。对于任何非平凡文本,这会导致大型稀疏矩阵 – 浪费大量空间。第二个局限性是,我们通常会丢失重要的信息 – 例如,句子“Is Mike a boy?”会产生与第一个句子相同的特征向量。特征向量是描述机器学*中模式识别中某个对象的 n- 维数值特征向量。尽管这些句子并不相同,但它们变得无法区分,这可能导致如果它们被标记为不同类别时出现类别噪声。

如果我们使用统计方法来选择最频繁的单词作为特征,那么添加这种噪声的问题就会变得更加明显。在这里,我们可能会丢失一些重要的特征,这些特征以有用的方式区分数据点。因此,这种词袋方法在这里仅用于说明。在本书的后面部分,我们将更深入地探讨所谓的转换器,它们使用更先进的技术来提取特征。

数据验证

特征向量是机器学*算法的核心。它们最显著且直接地被监督机器学*算法使用。然而,数据验证的相同概念也适用于其他类型验证中使用的数据。

每种形式的数据验证都是一系列检查,确保数据包含所需的属性。以下是一个此类检查集的示例 图 2**.7

图 2.7 – 数据质量检查示例

图 2.7 – 数据质量检查示例

数据的完整性是描述我们的数据覆盖总分布多少的一个属性。这可以通过对象分布来衡量 – 例如,在我们的图像数据集中有多少种/型号/颜色的汽车 – 或者它可以通过属性来衡量 – 例如,我们的数据中包含的语言中有多少个单词。

准确性是描述我们的数据与经验(现实)世界相关性的一个属性。例如,我们可能想要检查我们数据集中的所有图像是否都与一个对象相关联,或者图像中的所有对象是否都被标注了。

一致性描述了数据内部结构的好坏以及相同的数据点是否以相同的方式进行标注。例如,在二元分类中,我们希望所有数据点都被标注为“0”和“1”或“True”和“False”,而不是两者都有。

完整性是检查数据是否可以与其他数据集成的属性。集成可以通过公共键或其他方式完成。例如,我们可以检查我们的图像是否包含允许我们知道图像拍摄地点的元数据。

最后,及时性是描述数据新鲜程度的属性。它检查数据是否包含最新的所需信息。例如,当我们设计推荐系统时,我们希望推荐新项目和旧项目,因此及时性很重要。

因此,这里是下一个最佳实践。

最佳实践 #8

选择对您的系统最相关的数据验证属性。

由于每个检查都需要在工作流程中增加额外的步骤,可能会减慢数据处理的速度,因此我们应该选择影响我们业务和架构的数据质量检查。如果我们开发一个希望提供最新推荐的系统,那么及时性是我们的首要任务,我们应该专注于这一点,而不是完整性。

虽然有很多用于数据验证和评估数据质量的框架,但我通常使用来自 AIMQ 框架的数据质量属性子集。AIMQ 框架已被设计为根据几个质量属性量化数据质量,类似于软件工程中的质量框架,如 ISO 25000 系列。我发现以下数据属性对于验证来说是最重要的:

  • 数据无噪声

  • 数据是新鲜的

  • 数据适合用途

第一个属性是最重要的,因为噪声数据会导致机器学*算法性能低下。对于之前介绍的类别噪声,重要的是检查数据标签是否没有矛盾,并检查标签是否分配正确。矛盾标签可以自动找到,但错误标注的数据点需要人工评估。对于属性噪声,我们可以使用统计方法来识别具有低变异性(甚至完全恒定的)或对模型学*不贡献的完全随机的属性。让我们考虑一个句子的例子:

Mike is not a tall boy.

如果我们使用与之前句子相同的特征提取技术,我们的特征矩阵看起来就像图 2.8 中所示的那样。对于句子 2,我们使用与句子 0 和 1 相同的特征。由于最后一个句子仅在一词(not)上有所不同,这可能导致类别噪声。最后一列有一个标签,not。这可能会发生在我们在一个数据集上训练模型并将其应用于另一个数据集时。这意味着第一个句子和最后一个句子具有相同的特征向量,但标签不同:

句子 ID Mike Is A tall boy smart girl 标签
0 1 1 1 1 1 0 0 1
1 0 1 1 0 0 1 1 1
2 1 1 1 1 1 0 0 0

图 2.8 – 噪声数据集

对于同一特征向量存在两种不同的标注是有问题的,因为机器学*算法无法学*这些噪声数据点的模式。因此,我们需要验证数据是否无噪声。

数据需要具备的另一属性是其时效性——也就是说,数据必须是新鲜的。我们必须使用当前数据,而不是旧数据。在自动驾驶领域,这一点尤为重要,我们需要确保模型与最新条件(例如,最新的交通标志)保持更新。

最后,验证最重要的部分是评估数据是否适合使用。请注意,这种评估不能自动完成,因为它需要专家进行。

配置和监控

机器学*软件旨在进行专业化的设计、部署和维护。现代公司称这个过程为MLOps,这意味着同一个团队需要负责机器学*系统的开发和运营。这种扩展责任背后的逻辑是,团队最了解系统,因此可以以最佳方式对其进行配置、监控和维护。团队知道在开发系统时必须做出的设计决策,对数据的假设,以及在部署后需要监控的潜在风险。

配置

配置是开发团队做出的设计决策之一。团队配置机器学*模型的参数、执行环境和监控基础设施。让我们探讨第一个;后两个将在接下来的几节中讨论。

为了说明这个挑战,让我们看看一个用于在特定手术期间分类事件的随机森林分类器。该分类器,至少在其 Python 实现中,有 16 个超参数(scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestClassifier.html)。这些超参数中的每一个都有几个值,这意味着找到最佳的超参数集可能是一项繁琐的任务。

然而,在实践中,我们不需要探索所有超参数的值,也不需要探索所有组合。我们只需探索与我们任务和,通过扩展,数据集最相关的那些。我通常只探索两个超参数,因为它们是最重要的:树木的数量和树的深度。第一个决定了森林有多宽,而第二个决定了它有多深。指定这些参数的代码可能看起来像这样:

rnd_clf = RandomForestClassifier(n_estimators=2,
                                    max_leaf_nodes=10,
                                    n_jobs=-1)

n_estimators超参数是树木的数量,而max_depth超参数是每棵树的深度。这些参数的值取决于数据集——我们有多少个特征以及有多少个数据点。如果我们有太多的树木和叶子,与特征和数据点的数量相比,分类器就会过拟合,无法从数据中泛化。这意味着分类器学会了识别每个实例,而不是识别数据中的模式——我们称这种情况为过拟合

如果树木或叶子太少,那么泛化模式就会过于宽泛,因此我们在分类中观察到错误——至少比最佳分类器更多的错误。我们称这种情况为欠拟合,因为模型没有正确学*到模式。

因此,我们可以编写一段代码,根据预定义的值集搜索这两个参数的最佳组合。手动寻找最佳参数的代码可能看起来像这样:

numEstimators = [2, 4, 8, 16, 32, 64, 128, 256, 512]
numLeaves = [2, 4, 8, 16, 32, 64, 128]
for nEst in numEstimators:
  for nLeaves in numLeaves:
    rnd_clf = RandomForestClassifier(n_estimators=nEst,
                                    max_leaf_nodes=nLeaves,
                                    n_jobs=-1)
    rnd_clf.fit(X_train, y_train)
    y_pred_rf = rnd_clf.predict(X_test)
    accuracy_rf = accuracy_score(y_test, y_pred_rf)
    print(f'Trees: {nEst}, Leaves: {nLeaves}, Acc: {accuracy_rf:.2f}')

突出显示的两行橙色代码展示了探索这些参数的两个循环——内循环循环使用这些参数训练分类器并打印输出。

让我们将这个算法应用于经历过手术的患者的生理数据。当我们把输出绘制在图表上,如图*图 2**.9 所示,我们可以观察到准确率是如何演变的。如果我们把树木的数量设置为 2,分类器的最佳性能是在 8 个叶子时达到的,但即使如此,它也无法完美地分类事件。对于四棵树,分类器在 128 个叶子时达到最佳性能,准确率为 1.0。从下面的图表中,我们可以看到增加更多的树木并没有显著提高结果:

图 2.9 – 每个估计器和叶子数目的准确率。x 轴的标签显示了树的数量(下划线之前)和叶子的数量(下划线之后)

图 2.9 – 每个估计器和叶子数目的准确率。x 轴的标签显示了树的数量(下划线之前)和叶子的数量(下划线之后)

对于这个例子,搜索最佳结果所需的时间相对较短——在标准笔记本电脑上最多需要 1-2 分钟。然而,如果我们想找到所有 16 个参数的最佳组合,我们将花费相当多的时间来做这件事。

有一种更自动化的方法来找到机器学*分类器的最佳参数——不同类型的搜索算法。其中最受欢迎的一种是 GridSearch 算法 (scikit-learn.org/stable/modules/generated/sklearn.model_selection.GridSearchCV.html),它的工作方式与我们的手动脚本类似,只不过它可以进行交叉验证,有多个分割,以及许多其他统计技巧来改进搜索。使用 GridSearch 算法搜索最佳解决方案可能看起来像这样:

# Create the parameter grid based on the results of random search
param_grid = {
    'max_depth': [2, 4, 8, 16, 32, 64, 128],
    'n_estimators': [2, 4, 8, 16, 32, 64, 128, 256, 512]
}
# Create a base model
rf = RandomForestClassifier()
# Instantiate the grid search model
grid_search = GridSearchCV(estimator = rf,
                           param_grid = param_grid,
                           cv = 3,
                           n_jobs = -1)
# Fit the grid search to the data
grid_search.fit(X_train, y_train)
# get the best parameters
best_grid = grid_search.best_estimator_
# print the best parameters
print(grid_search.best_params_)

之前的代码找到了最佳解决方案并将其保存为 GridSearch 模型的 best_estimator_ 参数。在这个数据集和模型的情况下,算法找到了最佳随机森林,它有 128 棵树(n_estimators)和 4 个层级(max_depth)。结果与手动找到的结果略有不同,但这并不意味着其中一种方法更优越。

然而,树的数量过多可能会导致过拟合,因此我宁愿选择有 4 棵树和 128 个叶子的模型,而不是有 128 棵树和 4 个层级的模型。或者,也许我还会使用一个介于两者之间的模型——也就是说,一个具有相同准确率但不太容易在深度或宽度上过拟合的模型。

这导致我的下一个最佳实践。

最佳实践 #9

在手动探索参数搜索空间之后,使用 GridSearch 和其他算法。

尽管自动参数搜索算法非常有用,但它们隐藏了数据的一些特性,并且不允许我们自行探索数据和参数。根据我的经验,理解数据、模型、其参数及其配置对于成功部署机器学*软件至关重要。我只有在尝试手动找到一些最优解之后才会使用 GridSearch(或其他优化算法),因为我希望理解数据。

监控

一旦机器学*系统配置完成,它就会被投入生产,通常作为更大软件系统的一部分。机器学*所做的推断是产品特性和背后商业模式的基础。因此,机器学*组件应尽可能减少错误。不幸的是,客户对失败和错误的关注程度高于正确运行的产品。

然而,机器学*系统的性能会随时间退化,但这并不是因为编程或设计质量低劣——这是概率计算的本质。因此,所有机器学*系统都需要监控和维护。

需要监控的一个方面被称为概念漂移。概念漂移是数据中的一个现象,这意味着数据中实体的分布因自然原因随时间变化。图 2.10展示了机器学*分类器(蓝色和红色线条)对黄色和橙色卡车图像的概念漂移:

图 2.10 – 概念漂移的示意图。物体(左侧)的原始分布随时间(右侧)变化,因此分类器必须重新训练(蓝色与红色线条)

图 2.10 – 概念漂移的示意图。物体(左侧)的原始分布随时间(右侧)变化,因此分类器必须重新训练(蓝色与红色线条)

左侧显示了用于训练模型的原始数据分布。模型在概念上表示为蓝色虚线。模型能够识别两种图像类别之间的差异。然而,随着时间的推移,数据可能会发生变化。新的图像可能会出现在数据集中,分布也会发生变化。原始模型在推理中开始出错,因此需要调整。重新训练的模型——即实心红色线条——捕捉了数据的新分布。

我们将数据集中的这种变化称为概念漂移。它在复杂的数据集和监督学*模型中更为常见,但概念漂移对非监督模型和强化学*模型的影响同样成问题。

图 2.11展示了应用于相同分布(直接在训练后)和经过某些操作后的数据的同一随机森林模型的表现。概念漂移在准确率从 1.0 降至 0.44 的降低中可见。该模型与图 2.9中的示例使用相同的数据进行训练,但应用于来自另一位患者的数据:

图 2.11 – 概念漂移前后性能降低的示例

图 2.11 – 概念漂移前后性能降低的示例

因此,我想介绍我的下一个最佳实践。

最佳实践#10

在您的机器学*系统中始终包含监控机制。

即使是使用卡方统计测试来比较分布相似性的简单机制,包括监控概念漂移的机制,也能产生很大的影响。它使我们能够识别系统中的问题,解决问题,并防止它们扩散到软件的其他部分,甚至影响到软件的最终用户。

专业机器学*工程师在生产中设置了概念漂移的监控机制,这表明人工智能软件的退化。

基础设施和资源管理

机器学*软件所需的基础设施和资源被组织成两个区域——数据服务基础设施(例如,数据库)和计算基础设施(例如,GPU 计算平台)。还有服务基础设施,用于向最终用户提供服务。服务基础设施可以是桌面应用程序、嵌入式软件(例如自动驾驶汽车中的软件)、工具的插件(如 GitHub Co-pilot 的情况)或网站(如 ChatGPT)。然而,在这本书中,我们将重点关注数据服务基础设施和计算基础设施。

这两个区域都可以本地或远程部署。本地部署意味着我们使用公司自己的基础设施,而远程基础设施意味着我们使用云服务或其他供应商的服务。

从概念上讲,我们可以将这些区域视为相互依赖的,如图图 2.12所示:

图 2.12 – 服务、计算和数据服务基础设施之间的相互依赖关系

图 2.12 – 服务、计算和数据服务基础设施之间的相互依赖关系

数据服务基础设施提供用于计算基础设施的数据。它包括数据库和其他数据源(例如,原始文件)。计算基础设施包括用于训练和测试机器学*模型的计算资源。最后,用户服务基础设施使用模型进行推理,并向最终用户提供服务和功能。

数据服务基础设施

数据服务基础设施是机器学*软件的基本组成部分之一,因为没有数据就没有机器学*。对数据有需求的人工智能应用对基础设施在性能、可靠性和可追溯性方面提出了新的要求。最后一个要求非常重要,因为机器学*训练数据决定了训练好的机器学*模型如何进行推理。在终端用户功能出现缺陷的情况下,软件工程师需要仔细审查用于构建失败机器学*系统的算法、模型和数据。

与传统软件相比,数据服务基础设施通常由三个不同的部分组成,如图图 2.13所示:

图 2.13 – 数据服务基础设施

图 2.13 – 数据服务基础设施

数据存储在持久存储中,例如硬盘上的数据库。它可以是本地存储或云服务器上的存储。最重要的是,这些数据是安全的,并且可以用于进一步处理。持久存储的数据需要提取出来,以便在机器学*中使用。第一步是找到所需的数据快照——例如,通过选择用于训练的数据。快照需要准备并格式化为表格形式,以便机器学*算法可以使用这些数据进行推理。

今天,机器学*系统使用了几种不同类型的数据库。首先,有标准的数据库,数据以表格形式存储。这些是众所周知且在传统和机器学*软件中广泛使用的数据库。

新类型的数据库是非 SQL 数据库,如 Elasticsearch (www.elastic.co),它被设计用于存储文档,而不是表格。这些文档可以灵活索引,以便根据这些文档存储和检索数据。在机器学*软件的情况下,这些文档可以是图像、整个文本文档,甚至是声音数据。这对于以收集时的相同格式存储数据非常重要,这样我们就可以在需要时追踪数据。

不论数据库中数据的格式如何,它都是从数据库中检索出来并转换为表格形式的;我们将在第三章中讨论这一点。这种表格形式是数据处理所需的基础设施。

有了这些,我们就来到了我的下一个最佳实践。

最佳实践#11

为您的数据选择正确的数据库——从数据的角度而不是系统的角度来考虑。

虽然选择适合我们数据的正确数据库听起来很明显,但对于机器学*系统来说,选择最适合当前数据的数据库,而不是系统本身,这一点非常重要。例如,当我们使用自然语言处理模型时,我们应该将数据存储在我们可以轻松检索并按组织形式存储的文档中。

计算基础设施

计算基础设施可能会随时间变化。在机器学*系统开发的早期阶段,软件开发者通常会使用预配置的实验环境。这些可以是他们电脑上的 Jupyter Notebooks,也可以是预配置的服务,如 Google Colab 或 Microsoft Azure Notebooks。这种基础设施支持机器学*的快速原型设计,易于数据提供,无需设置高级功能。它们还允许我们轻松地根据需要调整计算资源,而无需添加或移除额外硬件。

这种方法的替代方案是使用我们自己的基础设施,在那里我们为您设置自己的服务器和运行时环境。这需要更多的努力,但它使我们能够完全控制计算资源,以及完全控制数据处理。对数据处理拥有完全控制权有时可能是选择基础设施最重要的因素。

因此,我的下一个最佳实践。

最佳实践 #12

如果可能,请使用云基础设施,因为它可以节省资源并减少对专业技术的需求。

专业 AI 工程师使用自有的基础设施进行原型设计和训练,并使用基于云的基础设施进行生产,因为它的扩展性更好,可以随着用户数量的增加而扩展。相反,即使用我们自己的基础设施,也只有在我们需要保留对数据或基础设施的完全控制时才是正确的。对于使用敏感客户数据、军事应用、安全应用和其他数据极其敏感的应用程序,可能需要完全控制。

幸运的是,我们有几个大型演员提供计算基础设施,以及一个庞大的小型演员生态系统。其中最大的三个是亚马逊网络服务(Amazon Web Services)、谷歌云(Google Cloud)和微软 Azure(Microsoft Azure),它们可以为小型和大型企业提供各种服务。亚马逊网络服务(aws.amazon.com)专注于提供数据存储和处理基础设施。它通常用于必须快速处理大量数据的应用程序。该基础设施由专业人员维护,并可用于实现基于该平台构建的产品的高*完美可靠性。为了高效使用它,你通常必须与执行机器学*应用程序代码的容器和虚拟机一起工作。

谷歌云(cloud.google.com)专注于为数据密集型应用程序和计算密集型解决方案提供平台。多亏了谷歌的处理器(张量处理单元TPUs)),该平台为训练和使用机器学*解决方案提供了一个非常高效的环境。谷歌云还提供免费的学*机器学*解决方案,形式为 Google Colab,它是 Python 平台 Jupyter Notebook(jupyter.org)的扩展。

微软 Azure(azure.microsoft.com)专注于提供虚拟机形式的机器学*系统训练和部署平台。它还提供用于图像识别、分类和自然语言处理的现成可部署模型,甚至提供用于训练通用机器学*模型的平台。它是所有可用平台中最灵活的,也是最具可扩展性的。

除了这些平台,你还可以使用几个专门的平台,例如 Facebook 的机器学*平台,该平台专注于推荐系统。然而,由于专门的平台通常比较狭窄,如果我们想将我们的软件从一个平台迁移到另一个平台,我们需要记住可能存在的问题。

因此,我的下一个最佳实践。

最佳实践#13

早期确定你希望使用的生产环境,并将你的流程与该环境对齐。

我们需要决定是想使用亚马逊的、谷歌的,还是微软的云环境,或者我们是否想使用自己的基础设施以降低软件开发成本。虽然可以在这些环境之间移动我们的软件,但这并不简单,并且需要(最好)进行大量的测试和预部署验证,这通常伴随着显著的成本。

所有这些如何结合在一起——机器学*管道

在本章中,我们探讨了机器学*系统的主要特征,并将它们与传统软件系统进行了比较。让我们通过总结我们通常如何设计和描述机器学*系统——通过使用管道来完成这一比较。管道是一系列数据处理步骤,包括机器学*模型。典型的步骤集(也称为阶段)如图2.14所示:

图 2.14 – 机器学*管道中的典型步骤序列

图 2.14 – 机器学*管道中的典型步骤序列

这种类型的管道,尽管是线性绘制的,但通常是在循环中处理的,例如,监控概念漂移可以触发重新训练、重新测试和重新部署。

机器学*管道,就像图2.14中展示的那样,通常被描绘为整个系统的一部分组件集。然而,使用管道类比来展示它有助于我们理解机器学*系统是按步骤处理数据的。

在下一章中,我们将探讨管道的第一部分——数据处理。我们将从探索不同类型的数据以及这些类型的数据在现代软件系统中的收集、处理和使用开始。

参考文献

  • Shortliffe, E.H.,等人,基于计算机的临床治疗咨询:MYCIN 系统的解释和规则获取能力。计算机与生物医学研究,1975 年。8(4): p. 303-320。

  • Vaswani, A.,等人,注意力就是一切。神经信息处理系统进展,2017 年。30。

  • Dale, R.,GPT-3:它有什么用?自然语言工程,2021 年。27(1): p. 113-118。

  • Smith, S.,等人,使用 deepspeed 和 megatron 训练 megatron-turing nlg 530b,一个大规模生成语言模型。arXiv 预印本 arXiv:2201.11990,2022 年。

  • Lee, Y.W.,等人,AIMQ:信息质量评估的方法。信息与管理,2002 年。40(2): p. 133-146。

  • Zenisek, J., F. Holzinger, and M. Affenzeller, 基于机器学*的概念漂移检测用于预测性维护。计算机与工业工程,2019. 137: p. 106031.

  • Amershi, S., 等人. 软件工程在机器学*中的应用:一个案例研究。在 2019 IEEE/ACM 第 41 届国际软件工程会议:软件工程实践(ICSE-SEIP)。 2019. IEEE.

第三章:软件系统中的数据——文本、图像、代码及其标注

机器学*ML)系统是数据需求大的应用,它们喜欢数据在训练和推理前已经准备妥当。尽管这听起来可能很显然,但比选择一个处理数据的算法更重要的是仔细审查数据的属性。然而,数据可以以许多不同的格式出现,并来自不同的来源。我们可以考虑数据以原始格式——例如,文本文档或图像文件。我们还可以考虑数据以特定于当前任务的手动格式——例如,分词文本(其中单词被分成标记)或带有边界框的图像(其中对象被识别并包含在矩形内)。

当考虑最终用户系统时,我们可以用数据做什么以及我们如何处理数据变得至关重要。然而,识别数据中的重要元素并将其转换为对机器学*算法有用的格式取决于我们的任务和使用的算法。因此,在本章中,我们将同时处理数据和算法来处理它。

在本章中,我们将介绍三种数据类型——图像、文本和格式化文本(程序源代码)。我们将探讨每种类型的数据如何在机器学*中使用,它们应该如何标注,以及用于什么目的。

介绍这三种类型的数据为我们提供了探索这些数据源不同标注方式的可能性。因此,在本章中,我们将重点关注以下内容:

  • 原始数据和特征——它们之间有什么区别?

  • 每种数据都有其用途——标注和任务

  • 不同类型的数据可以一起使用的地方——多模态数据模型展望

原始数据和特征——它们之间有什么区别?

机器学*(ML)系统对数据的需求很大。它们依赖于数据进行训练和推理。然而,并非所有数据都同等重要。在深度学*(DL)时代之前,数据需要经过处理才能用于机器学*。在深度学*之前,算法在可用于训练的数据量方面受到限制。存储和内存限制也有限,因此,机器学*工程师必须为深度学*准备更多的数据。例如,机器学*工程师需要花费更多精力来找到一个既小又具有代表性的数据样本用于训练。深度学*引入后,机器学*模型可以在更大的数据集中找到复杂的模式。因此,机器学*工程师的工作现在集中在寻找足够大且具有代表性的数据集。

经典机器学*系统——即非深度学*系统——需要表格形式的数据来进行推理,因此为这类系统设计正确的特征提取机制非常重要。

另一方面,深度学*系统需要最少的数据处理,并且可以从数据(几乎)原始格式中学*模式。由于深度学*系统需要为不同任务获取一些关于数据的不同信息,因此需要最少的数据处理;它们还可以自行从原始数据中提取信息。例如,它们可以捕捉文本的上下文,而无需手动处理。图 3.1展示了基于可以执行的任务的不同类型数据之间的这些差异。在这种情况下,数据以图像的形式存在:

图 3.1 – 学*系统的类型及其对图像所需的数据

图 3.1 – 学*系统的类型及其对图像所需的数据

原始图像通常用于进一步处理,但它们也可以用于图像分类等任务。图像分类的任务与算法的输入是原始图像,输出是图像类别相关。当我们谈论包含“猫”、“狗”或“汽车”等内容的图像时,我们经常看到这类任务。

这个任务有许多实际应用。一个应用是在保险领域。几家保险公司已经改变了他们的商业模式并将业务数字化。在 2010 年代中期之前,保险公司需要首先到车间进行一次访问以对汽车损坏进行初步评估。今天,这种初步的损坏评估是通过图像分类算法自动完成的。我们用智能手机拍摄损坏的部分并发送给保险公司的软件,在那里使用训练好的机器学*算法进行评估。在罕见且困难的情况下,图像需要由人工操作员仔细检查。这种工作流程节省了金钱和时间,并为处理索赔提供了更好的体验。

另一个应用是医学图像分类,其中放射学图像被自动分类以提供初步诊断,从而减轻医学专家(在这种情况下,是放射科医生)的负担。

遮罩图像通过使用过滤器来强调感兴趣的部分进行处理。最常见的是黑白过滤器或灰度过滤器。它们强调图像中明暗部分之间的差异,以便更容易地识别形状,然后对这些形状进行分类并追踪它们(在视频流的情况下)。这类应用通常用于感知系统——例如,在汽车中。

使用掩码图像的感知系统的一个实际应用是识别水平道路标记,如车道标记。车辆的摄像头拍摄汽车前方的道路图像,然后其软件对图像进行掩码处理,并将其发送到机器学*算法进行检测和分类。OpenCV是用于此类任务的一个库。其他实际应用包括人脸识别或光学字符识别OCR)。

语义地图图像包括描述图像中可见内容的叠加,覆盖包含特定信息(如天空、汽车、人或建筑物)的部分图像。语义地图可以覆盖包含汽车、汽车所在的路面、周围环境和天空的图像部分。语义地图提供了关于图像的丰富信息,这些信息用于高级视觉感知算法,反过来,这些算法为决策算法提供信息。在自动驾驶汽车系统中,视觉感知尤其重要。

语义地图的一个应用领域是车辆主动安全系统。前摄像头捕捉到的图像通过卷积神经网络CNNs)进行处理,添加语义地图,然后用于决策算法。这些决策算法要么向驾驶员提供反馈,要么自主采取行动。我们可以看到,当一辆车对另一辆车行驶过*或在其路径上检测到障碍物时,通常会有反应。

语义地图的其他应用包括医学图像分析,其中机器学*算法为医学专家提供有关图像内容的输入。一个例子是使用深度****CNNsDCNNs)进行脑肿瘤分割。

最后,边界框图像包含有关图像中物体边界的信息。对于感兴趣的每个形状,如汽车、行人或肿瘤,都有一个围绕该图像部分的边界框,并标注该形状的类别。这类图像用于检测物体并将该信息提供给其他算法。

我们使用这类图像的一个应用是机器人协调系统中的物体识别。机器人的摄像头捕捉到一个图像,CNN 识别出物体,然后机器人的决策软件追踪该物体以避免碰撞。追踪每个物体用于改变自主机器人的行为,以降低碰撞和损坏的风险,以及优化机器人和其环境操作。

因此,本章的第一条最佳实践。

最佳实践 #14

设计整个软件系统应基于您需要解决的问题,而不仅仅是机器学*模型。

由于我们使用的每种算法都需要对图像进行不同的处理并提供不同类型的信息,因此我们需要了解如何围绕它创建整个系统。在前一章中,我们讨论了管道,它只包括机器学*数据管道,但一个软件系统需要更多。对于关键功能,我们需要设计安全笼和信号来降低机器学*模型错误分类/检测的风险。因此,我们需要了解我们想要做什么——信息是否仅用于做出简单决策(例如,汽车上的损坏保险杠与未损坏的情况)或者分类是否是复杂行为决策的一部分(例如,机器人应该向右转以避开障碍物,还是应该减速以让另一个机器人通过?)。

图像是我们用于机器学*的一种数据类型;另一种是文本。*年来,随着循环神经网络RNNs)和转换器的引入,文本的使用变得流行。这些神经网络架构是深度学*网络,能够捕捉到词语的上下文(以及由此扩展的基本语义)。这些模型在标记(因此,词语)之间发现统计联系,因此可以识别出经典机器学*模型无法识别的相似性。机器翻译是这些模型最初的一个流行应用,但现在,应用范围比这更广——例如,在理解编程语言方面。图 3.2展示了可以与不同类型的模型一起使用的文本数据类型:

图 3.2 – 学*系统的类型及其对文本数据的需求

图 3.2 – 学*系统的类型及其对文本数据的需求

原始文本数据目前用于训练word2vec模型,该模型将文本标记转换为数字向量——嵌入——这些向量是该标记与词汇表中其他标记的距离。我们在第二章中看到了一个例子,其中我们计算了句子中的单词数量。通过使用这项技术,word2vec模型捕捉到标记的上下文,也称为它们的相似性。这种相似性可以扩展到整个句子或段落,这取决于模型的大小和深度。

原始文本的另一个应用,尽管是以结构化格式,是在情感分析SA)中。我们使用文本数据的表格格式来分析文本的情感是积极、消极还是中性。该任务的扩展是理解文本的意图——它是否是解释、查询还是描述。

掩码文本数据指的是在一系列标记中掩码一个或多个标记,并训练模型来预测该标记。这是一个自监督训练的例子,因为模型是在未标注的数据上训练的,但通过以不同的方式(例如,随机、基于相似性、人工标注)掩码标记,模型可以理解哪些标记可以在特定上下文中使用。模型越大——即 transformer——需要的数据就越多,并且需要更复杂的训练过程。

最后,标注文本指的是当我们用特定的类别标记文本片段,就像图像一样。这种标注的一个例子是情感。然后,模型捕捉数据中的模式,因此可以重复这些模式。这个领域的一个任务示例是情感识别,其中模型被训练来识别文本的语气是积极还是消极。

文本数据的一个特殊情况是编程语言源代码。在过去的几年里,使用 ML 模型进行编程语言任务变得越来越流行,因为它提供了提高软件开发速度和质量的可能。图 3.3展示了编程语言数据类型和典型任务:

图 3.3 – 编程语言数据类型和典型任务

图 3.3 – 编程语言数据类型和典型任务

原始源代码数据用于与编程语言理解相关的任务——例如,使用 TransCoder 模型在不同编程语言之间的翻译。这项任务类似于自然语言之间的翻译,尽管它增加了额外的步骤来使程序编译并通过测试用例。

掩码编程语言代码通常用于训练旨在修复缺陷的模型——模型是在一组纠正缺陷的程序上训练的,然后应用于有缺陷的程序。掩码程序用于训练能够识别问题并提供修复方案的模型。在撰写本书时,这些任务相当实验性,但结果非常令人鼓舞。

标注源代码用于各种任务。这些任务包括缺陷预测、代码审查以及识别设计模式或公司特定的设计规则。与静态代码分析工具等其他技术相比,ML 模型在这些任务上提供了更好的结果——例如,与静态代码分析工具相比。

源代码用于训练用于高级软件工程任务的模型,例如创建程序。GitHub Copilot 就是这样一种工具,它在研究和商业应用中都取得了巨大的成功。

现在,上述三种类型的数据仅展示了 ML 应用的一小部分。对于那些想要利用 ML 模型设计软件系统的人来说,天空才是极限。然而,在设计系统之前,我们需要更详细地了解我们如何与数据打交道。

图像

原始图像数据通常存储在包含在其他文件中的注释的文件中。原始图像数据呈现与所讨论系统相关的方面。用于训练主动安全算法的数据示例在图 3.4中展示:

图 3.4 – 车辆的前置摄像头图像

图 3.4 – 车辆的前置摄像头图像

在这个例子中,使用带有汽车的图像来训练 CNN 以识别是否安全驾驶(例如,前方道路是否无障碍)。当在图像级别上标注数据时——即没有蒙版和边界框——机器学*模型可以分类整个图像或识别对象。在识别对象时,模型会将边界框信息添加到图像中。

为了训练一个用于包含许多显著大小对象的图像(例如 1920 x 1080 像素的高清分辨率)的 CNN,我们需要大量的数据集和计算资源。这有几个原因。

首先,颜色需要大量数据才能正确识别。尽管我们人类将红色视为几乎均匀,但实际上该颜色的像素强度变化很大,这意味着我们需要创建一个 CNN,使其能够理解不同深度的红色有时对于识别制动车辆很重要。

其次,图像的大尺寸包含了不相关的细节。图 3.5展示了 CNN 的设计方式。这是一个 LeNet 风格的 CNN:

图 3.5 – CNN 的概念设计

图 3.5 – CNN 的概念设计

图 3.5显示 NN 以 192 x 108 像素大小的图像(比高清图像小 10 倍)作为输入。然后它使用MaxPool层(例如)来减少元素数量,然后使用卷积来识别形状。最后,它使用两个密集层将图像分类为 64 个不同类别的向量。图像的大小决定了网络的复杂性。图像越大,所需的卷积就越多,第一层也越大。更大的网络需要更多的时间来训练(差异可能以天计)并且需要更多的数据(差异可能以成千上万张图像计,取决于类别的数量和图像的质量)。

因此,对于许多应用,我们使用灰度图像并将它们显著缩小。图 3.6显示了之前相同的图像,但以灰度显示,缩小到 192 x 108 像素。图像的大小已经显著减小,因此对第一卷积层的要求也减少了:

图 3.6 – 黑白转换图像(故意展示的低质量有损转换)

图 3.6 – 黑白转换图像(故意展示的低质量有损转换)

然而,图像中的物体仍然非常清晰可见,可以用于进一步分析。因此,这里是下一个最佳实践。

最佳实践#15

缩小图像的大小并尽可能使用较少的颜色,以减少系统的计算复杂度。

在设计系统之前,我们需要了解我们有哪些类型的图像以及我们如何使用它们。然后,我们可以执行这些类型的转换,以便我们设计的系统能够处理其设计的目标任务。然而,需要注意的是,缩小图像也可能导致信息丢失,这可能会影响机器学*模型的准确性。在决定如何预处理图像以用于机器学*任务时,重要的是要仔细权衡计算复杂度和信息丢失之间的权衡。

在机器学*中,缩小图像并将其转换为灰度是一个常见的做法。实际上,存在几个广为人知且广泛使用的基准数据集,它们使用了这种技术。其中一个就是手写数字的MNIST数据集。该数据集可以作为最受欢迎的机器学*库(如 TensorFlow 和 Keras)的一部分下载。只需使用以下代码即可获取图像:

# import the Keras library that contains the MNIST dataset
from keras.datasets import mnist
# load the dataset directly from the Keras website
# and use the standard train/test splits
(X_train, Y_train), (X_test, Y_test) = mnist.load_data()
# import a Matplot library to plot the images
from matplotlib import pyplot
# plot first few images
for i in range(9):
# define subplot to be 330 pixels wide
pyplot.subplot(330 + 1 + i)
# plot raw pixel data
   pyplot.imshow(X_train[i],
                   cmap=pyplot.get_cmap('gray'))
# show the figure
pyplot.show()

该代码说明了如何下载数据集,该数据集已经分为测试训练数据,并带有注释。它还展示了如何可视化数据集,这导致了图 3.7中看到的图像:

图 3.7 – MNIST 数据集中前几个图像的可视化;有意将图像转换为位图以展示其实际大小

图 3.7 – MNIST 数据集中前几个图像的可视化;有意将图像转换为位图以展示其实际大小

MNIST 数据集中图像的大小是 28 x 28 像素,这对于训练和测试新的机器学*模型来说已经足够完美。尽管该数据集在机器学*中广为人知并被使用,但它相对较小且均匀——只有灰度数字。因此,对于更高级的任务,我们应该寻找更多样化的数据集。

手写数字的图像自然是有用的,但我们通常希望使用更复杂的图像,因此,标准库中包含的图像不仅仅只有 10 个类别(数字的数量)。其中一个这样的数据集是 Fashion-MNIST 数据集。

我们可以使用以下代码下载它:

from keras.datasets import fashion_mnist
(X_train, Y_train), (X_test, Y_test)=fashion_mnist.load_data()

该代码可以用于生成图 3.7中所示的可视化,它产生了图 3.8中看到的图像集合。图像大小和类别数量相同,但复杂度更大:

图 3.8 – Fashion-MNIST 数据集;有意将图像转换为位图以展示其实际大小

图 3.8 – Fashion-MNIST 数据集;有意将图像转换为位图以展示其实际大小

最后,我们还可以使用包含彩色图像的库,例如 CIFAR-10 数据集。可以使用以下代码访问该数据集:

from keras.datasets import cifar10
# load dataset
(X_train, Y_train), (X_test, Y_test)== cifar10.load_data()

该数据集包含 10 个不同类别的图像,大小相似(32 x 32 像素),但带有颜色,如图 图 3.9 所示。

图 3.9 – CIFAR-10 数据集;图像有意进行光栅化以展示其实际大小

图 3.9 – CIFAR-10 数据集;图像有意进行光栅化以展示其实际大小

这并不是这些基准数据集的终点。一些数据集包含更多类别、更大的图像,或者两者都有。因此,在系统必须执行的任务之前和期间查看这些数据集是很重要的。

在大多数情况下,灰度图像对于分类任务来说已经足够好了。它们能够快速让我们在数据中找到方向,而且它们足够小,以至于分类质量良好。

基准数据集的通常大小约为 50,000–100,000 张图像。这表明,即使是如此小的类别数量和如此小的图像,数量也是相当大的。想象一下标注那 100,000 张图像。对于更复杂的图像,数据集的大小可以显著更大。例如,在汽车软件中使用的 BDD100K 数据集包含超过 100,000 张图像。

因此,这是我的下一个最佳实践。

最佳实践 #16

使用参考数据集(如 MNIST 或 STL)来基准测试系统是否工作。

为了理解整个系统是否工作,这样的基准数据集非常有用。它们为我们提供了一个预配置的训练/测试分割,并且有许多算法可以用来理解我们算法的质量。

我们还应该考虑下一个最佳实践。

最佳实践 #17

在可能的情况下,使用已经为特定任务预训练的模型(例如,用于图像分类或语义分割的神经网络模型)。

正如我们应该努力重复使用图像进行基准测试一样,我们也应该努力重复使用预训练的模型。这节省了之前的设计资源,并减少了花费太多时间寻找神经网络模型的最佳架构或最佳参数集(即使我们使用 GradientSearch 算法)的风险。

文本

在对文本进行的分析类型中,SA 是其中之一——对文本片段(例如句子)是否为正面或负面的分类。

图 3.10 展示了可用于 SA 的数据示例。这些数据是公开可用的,并且是从亚马逊产品评论中创建的。这类分析的数据通常以表格形式结构化,其中包含诸如 ProductId(为了简洁,我已截断 Id 列)或 UserId 这样的实体,以及用于参考的 Score 和用于分类的 Text

这种数据结构为我们提供了快速总结文本和可视化的可能性。可视化可以通过多种方式进行——例如,通过绘制得分的直方图。然而,最有趣的视觉化是那些由文本中使用的单词/标记的统计信息提供的:

Id ProductId UserId Score 摘要 文本
1 B001KFG0 UHU8GW 5 良好的狗粮质量 我购买了几种 Vitality 罐装狗粮产品,并发现它们的质量都很好。产品看起来更像炖菜而不是加工肉类,而且气味更好。我的拉布拉多很挑食,她比大多数狗更喜欢这个产品。
2 B008GRG4 ZCVE5NK 1 不如广告所述 产品到达时标明为巨型盐味花生...实际上花生是小型未加盐的。不确定这是否是一个错误,还是供应商有意将产品标为“巨型”。
3 B000OCH0 WJIXXAIN 4 “满意”就足够了 这是一种存在了几百年的糖果。它是一种轻盈、蓬松的柑橘果冻,里面有坚果——在这种情况下是榛子。然后切成小块,然后大量裹上糖粉。这是一口小小的天堂。不太嚼劲,非常美味。我强烈推荐这种美味的小吃。如果你熟悉 C.S.路易斯的《狮子、女巫和魔衣橱》的故事——这就是诱惑埃德蒙向女巫出卖他的兄弟姐妹的糖果。
4 B000A0QIQ C6FGVXV 2 咳嗽药水 如果你正在寻找 Robitussin 的秘密成分,我相信我已经找到了。我除了订购的根啤酒提取物(很好)外,还做了一些樱桃汽水。味道非常药性。
5 B0062ZZ7K LF8GW1T 5 优秀的太妃糖 价格合理的优秀太妃糖。有各种各样的美味太妃糖。送货非常快。如果你是太妃糖爱好者,这是一个划算的交易。

图 3.10 – 产品评论数据示例,为 SA 结构化;仅显示前五行

一种可视化数据的方法是使用词云可视化技术。以下是一个用于可视化此类数据的简单脚本的示例:

# Create stopwords list:
stopwords = set(STOPWORDS)
stopwords.update(["br", "href"])
# create text for the wordcloud
textt = " ".join(review for review in dfRaw.Text)
# generation of wordcloud
wordcloud = WordCloud(stopwords=stopwords,
                  max_words=100,
                  background_color="white").generate(textt)
# showing the image
# and saving it to the png file
plt.figure(figsize = [12,9])
plt.imshow(wordcloud, interpolation='bilinear')
plt.axis("off")
plt.show()

运行此脚本的输出结果如图3.11所示。一个词云显示了单词使用频率的趋势——使用频率较高的单词比使用频率较低的单词大:

图 3.11 – 文本列的词云可视化

图 3.11 – 文本列的词云可视化

因此,我的下一个最佳实践如下。

最佳实践 #18

将你的原始数据可视化,以了解数据中的模式。

数据的可视化对于理解潜在模式非常重要。这一点我无法强调得更多。我既使用 Python 的 Matplotlib 和 Seaborn,也使用 TIBCO Spotfire 等可视化分析工具来绘制图表并理解我的数据。没有这种可视化,以及没有对模式的这种理解,我们必然会得出错误的结论,甚至设计出需要完全重新设计才能移除缺陷的系统。

更高级文本处理输出的可视化

文本的可视化帮助我们理解文本包含的内容,但它并不捕捉其含义。在这本书中,我们将使用高级文本处理算法——特征提取器。因此,我们需要了解如何创建这些算法输出的可视化。

与特征提取一起工作的一种方法是用词嵌入——一种将单词或句子转换为数字向量的方法。word2vec是能够做到这一点的模型之一,但还有更强大的模型。OpenAI 的 GPT-3 模型是公开可用的最大模型之一。获取段落嵌入相当直接。首先,我们连接到 OpenAI API,然后查询它以获取嵌入。以下是查询 OpenAI API 的代码(加粗):

# first we combine the title and the content of the review
df['combined'] = "Title: " + df.Summary.str.strip() + "; Content: " + df.Text.str.strip()
# we define a function to get embeddings, to make the code more straightforward
def get_embedding(text, engine="text-similarity-davinci-001"):
   text = text.replace("\n", " ")
   return openai.Embedding.create(input = [text], engine=engine)['data'][0]['embedding']
# and then we get embeddings for the first 5 rows
df['babbage_similarity'] = df.head(5).combined.apply(lambda x: get_embedding(x, engine='text-similarity-babbage-001'))

通过运行这段代码,我们获得了5个向量(每个行一个)的2048个数字,我们称之为嵌入。整个向量太大,无法包含在页面上,但前几个元素看起来像这样:[-0.005302980076521635, 0.018141526728868484, -0.018141526728868484, 0.004692177753895521, …]

这些数字对我们人类来说意义不大,但对语言模型来说却有着意义。意义在于它们之间的距离——相似度高的单词/标记/句子比不相似的单词/标记/句子更接*。为了理解这些相似性,我们使用降低维度的转换——其中之一是t 分布随机邻域嵌入t-SNE)。图 3.12展示了我们获得的五个嵌入的这种可视化:

图 3.12 – 五个评论嵌入向量的 t-SNE 可视化

图 3.12 – 五个评论嵌入向量的 t-SNE 可视化

每个点代表一条评论,每个十字代表簇的中心。簇由原始数据集中的Score列指定。截图显示,每条评论中的文本都不同(点不重叠),并且簇是分开的——十字在截图的不同部分。

因此,我接下来的最佳实践就是关于这个。

最佳实践#19

当数据被转换为特征以监控是否仍可观察到相同模式时,请可视化您的数据。

就像之前的最佳实践一样,我们需要可视化数据以检查我们在原始数据中观察到的模式是否仍然可观察。这一步很重要,因为我们需要知道机器学*模型确实可以学*这些模式。由于这些模型本质上是统计的,它们总是捕捉到模式,但当模式不存在时,它们捕捉到的可能是无用的东西 – 即使它看起来像是一个模式。

结构化文本 – 程序的源代码

程序的源代码是文本数据的一个特殊情况。它具有相同类型的模态 – 文本 – 但它包含程序语法/句法结构形式的附加信息。由于每种编程语言都基于语法,因此程序的结构化都有特定的规则。例如,在 C 语言中,应该有一个名为 main 的特定函数,它是程序的入口点。

这些特定的规则使得文本以特定的方式结构化。它们可能使人类理解文本变得更加困难,但这种结构确实非常有帮助。使用这种结构的一个模型是 code2vec (code2vec.org/)。code2vec 模型与 word2vec 类似,但它以输入它所分析程序的 抽象语法树 (AST) – 例如,以下程序:

void main()
{
    Console.println("Hello World");
}

这可以通过 图 3**.13 中的 AST 来表示:

图 3.13 – 简单“Hello World”程序的 AST

图 3.13 – 简单“Hello World”程序的 AST

示例程序被可视化为一组指令及其上下文以及它们在程序中扮演的角色。例如,voidmain 是方法声明的一部分,与块语句(BlockStmt)一起,构成了方法的主体。

code2vec 是一个使用编程语言信息(在这种情况下,是语法)作为模型输入的模型的例子。模型可以执行的任务包括查找单词之间的相似性(如 word2vec 模型)、查找组合以及识别类比。例如,模型可以识别 intmain 这两个单词的所有组合,并提供以下答案(带有概率):realMain(71%),isInt(71%),和 setIntField(69%)。通过扩展,这些任务可以用于程序修复,其中模型可以识别错误并修复它们。

然而,使用 AST 或类似信息也有缺点。主要缺点是分析程序必须编译。这意味着我们无法在想要分析不完整程序的环境中使用这些类型的模型 – 例如,在 持续集成 (CI) 或现代代码审查的环境中。当我们只分析代码的一小部分时,模型无法解析它,获取其 AST,并使用它。因此,这是我的下一个最佳实践。

最佳实践 #20

只将必要的信息作为输入提供给机器学*模型。过多的信息可能需要额外的处理,并使训练难以收敛(完成)。

在设计处理流程时,确保提供给模型的信息是必要的,因为每一条信息都对整个软件系统提出新的要求。例如,在抽象语法树(AST)的例子中,当它是必要的,它就是强大的信息,但如果不可用,它可能会成为数据分析流程工作的巨大障碍。

每种数据都有其目的 – 标注和任务

原始格式的数据很重要,但只是机器学*软件开发和运营的第一步。最重要的是数据标注,这也是成本最高的部分。为了训练机器学*模型并使用它进行推理,我们需要定义一个任务。定义任务既是概念性的也是操作性的。概念性定义是定义我们希望软件做什么,而操作性定义是我们希望如何实现这一目标。操作性定义归结为对我们在数据中看到的内容以及我们希望机器学*模型识别/复制的定义。

标注是我们指导机器学*算法的机制。我们使用的每一条数据都需要某种标签来表示其内容。在数据的原始格式中,这种标注可以是数据点包含的内容的标签。例如,这样的标签可以是图像包含数字 1(来自 MNIST 数据集)或汽车(来自 CIFAR-10 数据集)。然而,这些简单的标注在专用任务中很重要。对于更高级的任务,标注需要更丰富。

这类标注的一种类型与我们在数据中指定部分数据为有趣的部分相关。在图像的情况下,这是通过在感兴趣的对象周围绘制边界框来完成的。图 3.14 展示了这样的图像:

图 3.14 – 带有边界框的图像

图 3.14 – 带有边界框的图像

图像包含围绕我们希望模型识别的元素的框。在这种情况下,我们希望识别车辆(绿色框)、其他道路使用者(橙色框)和重要的背景对象(灰色框)。这类标注用于使机器学*模型学*形状,并在新对象中识别这些形状。在这个例子中,边界框识别了对于汽车主动安全系统重要的元素,但这不是唯一的用途。

这种边界框的其他应用包括医学图像分析,其任务是识别需要进一步分析的组织。这些也可以是面部识别和物体检测的系统。

虽然这个任务和边界框可以被视为标注原始数据的一个特殊情况,但它略有不同。每个框都可以被视为一个带有标签的独立图像,但挑战在于每个框的大小都不同。因此,使用这种不同形状的图像将需要预处理(例如,重新缩放)。它也仅适用于训练,因为在推理中,我们需要在分类之前识别对象——这正是我们需要神经网络为我们完成的任务。

我使用这类数据的最佳实践将在下面列出。

最佳实践 #21

当任务需要检测和跟踪对象时,在数据中使用边界框。

由于边界框使我们能够识别对象,因此这些数据的自然用途是在跟踪系统中。一个示例应用是使用摄像头监控停车位的系统。它检测停车位并跟踪是否有车辆停在该位置。

对象检测任务的扩展是感知任务,其中我们的机器学*软件需要根据数据的上下文或情况做出决策。

对于图像数据,这种上下文可以通过语义地图来描述。图 3.15 展示了这样一个地图:

图 3.15 – 带有语义地图的图像;建筑是语义地图的一个标签

图 3.15 – 带有语义地图的图像;建筑是语义地图的一个标签

截图显示了覆盖特定类型对象的多种颜色叠加。橙色叠加显示车辆,紫色叠加显示易受伤害的道路使用者,在本图中即为行人。最后,粉色表示建筑,红色覆盖背景/天空。

语义地图比边界框提供了更多的灵活性(因为某些对象比其他对象更有趣),并允许机器学*系统获取图像的上下文。通过识别图像中存在哪些类型的元素,机器学*模型可以为我们设计的软件系统的决策算法提供有关图像拍摄位置的信息。

因此,这是我的下一个最佳实践。

最佳实践 #22

当你需要获取图像的上下文或需要特定区域的详细信息时,使用语义地图。

语义地图需要大量计算才能有效使用;因此,我们应该很少使用它们。当我们有与上下文相关的任务时,例如感知算法或图像修改——例如,改变图像中天空的颜色——我们应该使用这些地图。关于信息的准确性,一般来说,语义地图需要大量计算,因此是选择性使用的。一个进行此类语义映射的工具示例是 Segments.ai。

语义地图在需要理解图像的上下文或特定区域的细节时非常有用。例如,在自动驾驶中,语义地图可以用来识别道路上的物体及其相互关系,从而使车辆能够就其移动做出明智的决定。然而,语义地图的具体应用案例可能因应用而异。

为意图识别标注文本

我们之前提到的 SA 只是文本数据标注的一种类型。它有助于评估文本是正面还是负面。然而,我们不是用情感标注文本,而是可以用——例如——意图来标注文本,并训练一个机器学*模型从其他文本段落中识别意图。图3.16中的表格提供了这样的标注,基于之前的相同评论数据:

Id Score Summary Text Intent
1 5 高质量狗粮 我购买了几个 Vitality 罐装狗粮产品,并发现它们的质量都很好。产品看起来更像炖菜而不是加工肉类,而且味道更好。我的拉布拉多很挑食,她比大多数狗更喜欢这个产品。 广告
2 1 不如广告所说 产品到达时标有“巨型盐味花生”……实际上花生是小型无盐的。不确定这是否是一个错误,还是卖家有意将产品标为“巨型”。 批评
3 4 “快乐”一词已尽其意 这是一种存在了几百年的糖果。它是一种轻盈、蓬松的柑橘果冻,里面有坚果——在这种情况下是榛子。然后它被切成小块,并大量裹上糖粉。这是一口小小的天堂。不太嚼劲,非常美味。我强烈推荐这种美味的点心。如果你熟悉 C.S.路易斯的《狮子、女巫和魔衣橱》的故事——这就是诱惑爱德蒙背叛他的兄弟姐妹给女巫的点心。 描述
4 2 咳嗽
 药品 如果你正在寻找 Robitussin 的秘诀成分,我相信我已经找到了。我除了订购的根啤酒提取物(味道不错)外,还制作了一些樱桃汽水。味道非常像药。 批评
5 5 优秀的太妃糖 价格合理的优秀太妃糖。有各种各样的美味太妃糖。送货非常快。如果你是太妃糖爱好者,这是一个划算的交易。 广告

图 3.16 – 用于意图识别的文本数据标注

表格的最后一列显示了文本的注释——意图。现在,我们可以使用意图作为标签来训练一个机器学*模型以识别新文本中的意图。通常,这项任务需要两步方法,如图3.17所示:

图 3.17 – 基于文本训练模型的两个步骤方法

图 3.17 – 基于文本训练模型的两个步骤方法

标注的文本被组织成两部分。第一部分是文本本身(例如,我们示例表中的Text列),第二部分是标注(例如,我们示例中的Intent列)。文本使用如 word2vec 模型或 transformer 等模型进行处理,将文本编码为向量或矩阵。标注使用如 one-hot 编码等技术编码为向量,以便它们可以作为分类算法的决策类别。分类算法接受编码的标注和向量化的文本。然后,它被训练以找到向量化文本(X)与标注(Y)的最佳匹配。

这里是我的最佳实践,如何执行这项操作。

最佳实践#23

使用预训练的嵌入模型,如 GPT-3 或现有的 BERT 模型来向量化您的文本。

根据我的经验,如果我们使用预定义的语言模型来向量化文本,那么处理文本通常会更简单。Hugging Face网站(www.huggingface.com)是这些模型的优秀来源。由于 LLMs 需要大量的资源来训练,现有的模型通常对于大多数任务来说已经足够好了。由于我们将在管道的下一步开发分类器模型,我们可以集中精力使该模型更好,并与我们的任务保持一致。

文本数据的另一种标注类型是关于词性POS)的上下文。它可以被视为在图像数据中使用的语义图。每个词都被标注,无论它是名词、动词还是形容词,无论它属于句子的哪个部分。这种标注的一个示例可以通过使用艾伦研究所的 AllenNLP 语义角色标注SRL)工具(demo.allennlp.org/semantic-role-labeling)进行视觉展示。图 3.18展示了简单句子的此类标注截图,而图 3.19展示了更复杂句子的标注:

图 3.18 – 使用 AllenNLP 工具集进行 SRL

图 3.18 – 使用 AllenNLP 工具集进行 SRL

在这个句子中,每个词的作用都被强调,我们可以看到有三个具有不同关联的动词——最后一个动词是主要的,因为它将句子的其他部分联系起来:

图 3.19 – 更复杂句子的 SRL

图 3.19 – 更复杂句子的 SRL

复杂句子具有更大的语义角色框架,因为它包含句子的两个不同部分。我们使用这种角色标注来提取文本段落的意义。这在设计基于所谓基于事实的模型的软件系统时尤其有用,这些模型会检查信息是否与事实相符。这些模型解析文本数据,找到正确的锚点(例如,问题是什么),并在它们的数据库中找到相关的答案。这些与非基于事实的模型相对,这些模型基于哪个词最适合完成句子来创建答案——例如,ChatGPT。

因此,我的最佳实践如下。

最佳实践#24

在设计需要提供基于事实的决策的软件时,使用角色标签。

基于事实的决策通常更难提供,因为模型需要理解句子的上下文,捕捉其意义,并提供相关的答案。然而,这并不总是必需的,甚至可能不是所希望的。非基于事实的模型对于可以由专家稍后修正的建议通常已经足够好。ChatGPT 这样的软件工具就是一个例子,它提供的答案有时是不正确的,需要人工干预。然而,它们是一个非常好的起点。

在可以使用不同类型的数据一起使用的地方——对多模态数据模型的展望

本章介绍了三种类型的数据——图像、文本和结构化文本。这三种类型的数据是数值形式的数据的例子,如数字矩阵,或时间序列的形式。然而,无论形式如何,与数据和机器学*系统一起工作是非常相似的。我们需要从源系统中提取数据,然后将其转换为我们可以注释的格式,然后将其用作机器学*模型的输入。

当我们考虑不同类型的数据时,我们可以开始思考是否可以在同一个系统中使用两种类型的数据。有几种方法可以实现这一点。第一种是在不同的管道中使用不同的机器学*系统,但我们连接了这些管道。GitHub Copilot 就是这样一种系统。它使用一个管道来处理自然语言,以找到类似的程序并将它们转换成适合当前正在开发的程序上下文。

另一个例子是生成图像文本描述的系统。它接受一个图像作为输入,识别其中的对象,然后基于这些对象生成文本。文本的生成是通过一个与图像分类完全不同的机器学*模型完成的。

然而,有一些新模型在同一个神经网络中使用两种不同的模态——图像和文本——例如,Gato 模型。通过使用来自两个来源的输入,并在中间使用一个非常窄(就神经元数量而言)的网络,该模型被训练以泛化由两种不同模态描述的概念。这样,该模型被训练以理解,如果不在完全相同的位置,猫的图像和单词“猫”应该被放置在非常接*的同一嵌入空间中。尽管仍然处于实验阶段,但这些类型的网络旨在模仿人类对概念的理解。

在下一章中,我们将更深入地探讨数据理解,通过深入研究特征工程的过程。

参考文献

  • Tao, J. et al., An object detection system based on YOLO in traffic scene. In 2017 6th International Conference on Computer Science and Network Technology (ICCSNT). 2017. IEEE.

  • Artan, C.T. and T. Kaya, Car Damage Analysis for Insurance Market Using Convolutional Neural Networks. In International Conference on Intelligent and Fuzzy Systems. 2019. Springer.

  • Nakaura, T. et al., A primer for understanding radiology articles about machine learning and deep learning. Diagnostic and Interventional Imaging, 2020. 101(12): p. 765-770.

  • Bradski, G., The OpenCV Library. Dr. Dobb’s Journal: Software Tools for the Professional Programmer, 2000. 25(11): p. 120-123.

  • Memon, J. et al., Handwritten optical character recognition (OCR): A comprehensive systematic literature review (SLR). IEEE Access, 2020. 8: p. 142642-142668.

  • Mosin, V. et al., Comparing autoencoder-based approaches for anomaly detection in highway driving scenario images. SN Applied Sciences, 2022. 4(12): p. 1-25.

  • Zeineldin, R.A. et al., DeepSeg: deep neural network framework for automatic brain tumor segmentation using magnetic resonance FLAIR images. International journal of computer assisted radiology and surgery, 2020. 15(6): p. 909-920.

  • Reid, R. et al., Cooperative multi-robot navigation, exploration, mapping and object detection with ROS. In 2013 IEEE Intelligent Vehicles Symposium (IV). 2013. IEEE.

  • Mikolov, T. et al., Recurrent neural network based language model. In Interspeech. 2010. Makuhari.

  • Vaswani, A. et al., Attention is all you need. Advances in neural information processing systems, 2017. 30.

  • Ma, L. and Y. Zhang, Using Word2Vec to process big text data. In 2015 IEEE International Conference on Big Data (Big Data). 2015. IEEE.

  • Ouyang, X. et al., Sentiment analysis using convolutional neural network. In 2015 IEEE International Conference on Computer and Information Technology; ubiquitous computing and communications; dependable, autonomic and secure computing; pervasive intelligence and computing. 2015. IEEE.

  • Roziere, B. et al., Unsupervised translation of programming languages. Advances in Neural Information Processing Systems, 2020. 33: p. 20601-20611.

  • Yasunaga, M. and P. Liang, Break-it-fix-it: Unsupervised learning for program repair. In International Conference on Machine Learning. 2021. PMLR.

  • Halali, S. et al., Improving defect localization by classifying the affected asset using machine learning. In International Conference on Software Quality. 2019. Springer.

  • Ochodek, M. et al., Recognizing lines of code violating company-specific coding guidelines using machine learning. In Accelerating Digital Transformation. 2019, Springer. p. 211-251.

  • Nguyen, N. and S. Nadi, An empirical evaluation of GitHub copilot’s code suggestions. In Proceedings of the 19th International Conference on Mining Software Repositories. 2022.

  • Zhang, C.W. et al., Pedestrian detection based on improved LeNet-5 convolutional neural network. Journal of Algorithms & Computational Technology, 2019. 13: p. 1748302619873601.

  • LeCun, Y. et al., Gradient-based learning applied to document recognition. Proceedings of the IEEE, 1998. 86(11): p. 2278-2324.

  • Xiao, H., K. Rasul, and R. Vollgraf, Fashion-MNIST: a novel image dataset for benchmarking machine learning algorithms. arXiv preprint arXiv:1708.07747, 2017.

  • Recht, B. et al., Do CIFAR-10 classifiers generalize to CIFAR-10? arXiv preprint arXiv:1806.00451, 2018.

  • Robert, T., N. Thome, and M. Cord, HybridNet: Classification and reconstruction cooperation for semi-supervised learning. In Proceedings of the European Conference on Computer Vision (**ECCV). 2018.

  • Yu, F. et al., Bdd100k: A diverse driving video database with scalable annotation tooling. arXiv preprint arXiv:1805.04687, 2018. 2(5): p. 6.

  • McAuley, J.J. and J. Leskovec, From amateurs to connoisseurs: modeling the evolution of user expertise through online reviews. In Proceedings of the 22nd International Conference on World Wide Web. 2013.

  • Van der Maaten, L. and G. Hinton, Visualizing data using t-SNE. Journal of Machine Learning Research, 2008. 9(11).

  • Sengupta, S. et al., Automatic dense visual semantic mapping from street-level imagery. In 2012 IEEE/RSJ International Conference on Intelligent Robots and Systems. 2012. IEEE.

  • Palmer, M., D. Gildea, and N. Xue, Semantic role labeling. Synthesis Lectures on Human Language Technologies, 2010. 3(1): p. 1-103.

  • Reed, S. et al., A generalist agent. arXiv preprint arXiv:2205.06175, 2022.

第四章:数据采集、数据质量和噪声

机器学*系统的数据可以直接来自人类和软件系统——通常称为源系统。数据的来源对其外观、质量以及如何处理它都有影响。

来自人类的数据通常比来自软件系统的数据更嘈杂。我们人类以小的不一致性而闻名,我们也可以不一致地理解事物。例如,两个人报告的同一缺陷可能有非常不同的描述;对于需求、设计和源代码也是如此。

来自软件系统的数据通常更一致,包含的噪声更少,或者数据中的噪声比人类生成数据的噪声更规律。这些数据由源系统生成。因此,控制和监控自动生成数据的品质是不同的——例如,软件系统不会在数据中“撒谎”,因此检查自动生成数据的可信度是没有意义的。

在本章中,我们将涵盖以下主题:

  • 数据的不同来源以及我们可以如何利用它们

  • 如何评估用于机器学*的数据的品质

  • 如何识别、测量和减少数据中的噪声

数据来源以及我们可以如何利用它们

机器学*软件在当今所有领域都变得越来越重要。从电信网络、自动驾驶汽车、计算机游戏、智能导航系统、面部识别到网站、新闻制作、电影制作和实验音乐创作,都可以使用机器学*来完成。一些应用在例如使用机器学*进行搜索字符串(BERT 模型)方面非常成功。一些应用则不太成功,例如在招聘过程中使用机器学*。通常,这取决于在这些应用中使用到的程序员、数据科学家或模型。然而,在大多数情况下,机器学*应用的成功往往取决于用于训练和使用的训练数据。它取决于数据的品质以及从中提取的特征。例如,亚马逊的机器学*推荐系统被停用,因为它对女性存在偏见。由于它是基于历史招聘数据训练的,而这些数据主要包含男性候选人,因此系统倾向于在未来的招聘中推荐男性候选人。

用于机器学*系统的数据可以来自各种来源。然而,我们可以将这些来源分为两大类——人工/人类和自动化软件/硬件。这两类具有不同的特征,决定了如何组织这些来源以及如何从这些来源的数据中提取特征。图 4.1展示了这些数据类型并提供了每种类型数据的示例。

手动生成数据是指来自人类输入或起源于人类的数据。这类数据通常比软件生成数据信息量更丰富,但变异性也更大。这种变异性可能来自我们人类的自然变异性。例如,在表格中提出的问题,两个人可能会有不同的理解和回答。这类数据通常比系统性错误更容易受到随机错误的影响。

自动生成数据起源于硬件或软件系统,通常由传感器或测量脚本收集来自其他系统、产品、流程或组织的数据。这类数据通常更一致、可重复,但也更容易受到系统性错误的影响:

图 4.1 – 数据来源及其分类。此图中的绿色部分是本书的范围

图 4.1 – 数据来源及其分类。此图中的绿色部分是本书的范围

人类起源的数据示例,常用于机器学*的是关于软件缺陷的数据。人类测试员通常会检查问题并使用表格报告。这个表格包含有关测试阶段、受影响的组件、对客户的影响等内容,但表格的一部分通常是问题描述的自然语言描述,这是由人类测试员对发生情况的一种解释。

另一种由人类生成数据是源代码。作为程序员,我们使用给定语法的编程语言编写软件源代码,并使用编程指南来保持一致的风格,以便我们的工作成果——源代码——可以被软件自动解释或编译。我们编写的代码中存在一些结构,但远非一致。即使是相同的算法,当由两位不同的程序员实现时,也会在变量命名、类型或解决问题的方法上有所不同。一个很好的例子是 Rosetta 代码网站,它提供了不同编程语言中的相同解决方案,有时甚至在同一编程语言中。

使用表格进行的需求规范或数据输入具有相同的属性和特征。

然而,有一种数据来源特别有趣——医疗数据。这是来自患者记录和图表的数据,由医疗专家作为医疗程序的一部分输入。这些数据可以是电子的,但它反映了专家对症状的理解以及他们对医疗测试和诊断的解释。

另一方面,我们还有由软件或硬件以某种方式生成的数据。自动生成的数据更一致,尽管不是没有问题,并且更重复。此类数据的例子是电信网络中生成以从一电信节点传输信息到另一电信节点。与其它类型的数据相比,无线电信号非常稳定,可能会受到外部因素(如降水)或障碍物(如建筑起重机)的干扰。数据是可重复的,所有变异性都源于外部因素。

另一个例子是来自车辆的数据,这些数据记录了它们周围的信息并存储以供进一步处理。这些数据可以包含车辆组件之间的信号以及与其他车辆或基础设施的通信。

医疗数据,例如脑电图EEG——即脑电波)或心电图ECG——即心率),是从源系统中收集的,我们可以将其视为测量仪器。因此,从技术上讲,这些数据是由计算机系统生成的,但它们来自人类患者。这种来自患者的起源意味着数据具有与其他从人类收集的数据相同的自然变异性。由于每个患者都有所不同,测量系统可以以略微不同的方式连接到每个患者,因此每个患者生成的数据与其他患者略有不同。例如,心电图心跳数据包含基本、一致的信息——每分钟的跳动次数(以及其他参数)。然而,原始数据在心电图信号的幅度(取决于测量电极的位置)或曲线尖峰之间的差异(R 和 T 尖峰)上有所不同。

因此,本章我的第一个最佳实践与我们所使用的软件数据的来源有关。

最佳实践 #25

识别你软件中使用的数据的来源,并据此创建你的数据处理流程。

由于所有类型的数据在清洁、格式化和特征提取方面都需要不同的处理方式,我们应该确保我们知道数据是如何产生的,以及我们可以预期(并处理)哪些问题。因此,首先,我们需要确定我们需要什么类型的数据,它来自哪里,以及它可能携带哪些问题。

从软件工程工具中提取数据——Gerrit 和 Jira

为了说明如何进行数据提取,让我们从一款流行的代码审查软件工具——Gerrit 中提取数据。这个工具用于审查和讨论个人程序员在代码集成到产品主代码库之前开发的代码片段。

以下程序代码展示了如何通过 JSON API 访问 Gerrit 的数据库——也就是说,通过 JSON API ——以及如何提取特定项目的所有变更列表。此程序使用 Python pygerrit2 包 (pypi.org/project/pygerrit2/)。此模块帮助我们使用 JSON API,因为它提供 Python 函数而不是仅仅 JSON 字符串:

# importing libraries
from pygerrit2 import GerritRestAPI
# A bit of config - repo
gerrit_url = "https://gerrit.onap.org/r"
# since we use a public OSS repository
auth = None
# this line gets sets the parameters for the HTML API
rest = GerritRestAPI(url=gerrit_url, auth = auth)
# the main query where we ask the endpoint to provide us the list and details of all changes
# each change is essentially a review that has been submitted to the repository
changes = rest.get("/changes/?q=status:merged&o=ALL_FILES&o=ALL_REVISIONS&o=DETAILED_LABELS&start=0",
headers={'Content-Type': 'application/json'})

此代码片段中的关键行是 rest.get("/changes/?q=status:merged&o=ALL_FILES&o=ALL_REVISIONS&o=DETAILED_LABELS&start=0", headers={'Content-Type': 'application/json'})。这一行指定了要检索变更的端点以及参数。它表示我们想要访问所有文件和所有修订,以及所有变更的详细信息。在这些详细信息中,我们可以找到有关所有修订(特定的补丁/提交)的信息,然后我们可以解析这些修订中的每一个。重要的是要知道 JSON API 在每个查询中返回的最大变更数是 500,因此最后一个参数——start=0——可以用来访问从 500 开始的变更。此程序的输出是一个非常长的变更列表,以 JSON 格式,因此我不会在本书中详细展示。相反,我鼓励您执行此脚本,并根据自己的节奏浏览此文件。脚本可以在本书的 GitHub 仓库中找到,网址为 github.com/miroslawstaron/machine_learning_best_practices,在 第四章 下。脚本的名称是 gerrit_exporter.ipynb

现在,仅提取变更列表对于分析来说并不很有用,因为它只提供了自动收集的信息——例如,哪些修订存在,以及谁创建了这些修订。它不包含有关哪些文件和行被注释的信息,或者注释是什么——换句话说,特别有用的信息。因此,我们需要与 Gerrit 进行更多交互,如 图 4**.2 所示。

图 4**.2 中展示的程序流程说明了代码注释数据库(如 Gerrit)中关系的复杂性。因此,访问数据库并导出这些数据的程序对于本书来说有点太长了。它可以在与之前相同的仓库中找到,名称为 gerrit_exporter_loop.ipynb

图 4.2 – 与 Gerrit 交互以提取注释文件、注释行和注释内容

图 4.2 – 与 Gerrit 交互以提取注释文件、注释行和注释内容

这类数据可以用来训练机器学*模型以审查代码,甚至可以识别哪些代码行需要被审查。

在使用 Gerrit 的时候,我发现以下最佳实践非常有用。

最佳实践 # 26

提取您所需的所有数据并将其存储在本地,以减少使用该工具进行工作的软件工程师的干扰。

虽然可以逐个提取更改,但最好一次性提取整个更改集并保留其本地副本。这样,我们减轻了其他人日常工作中使用的服务器压力。我们必须记住,数据提取是这些源系统的次要任务,而它们的主要任务是支持软件工程师的工作。

另一个为支持软件工程任务的软件系统提供数据的好来源是 JIRA,一个问题和任务管理系统。JIRA 用于记录史诗、用户故事、软件缺陷和任务,并已成为此类活动最受欢迎的工具之一。

因此,我们可以从 JIRA 作为源系统提取大量有关流程的有用信息。然后,我们可以使用这些信息来开发机器学*模型,评估和改进任务和需求(以用户故事的形式),并设计帮助我们识别重叠的用户故事或将它们分组为更连贯的史诗的工具。这种软件可以用来提高这些任务的质量或提供更好的估计。

以下代码片段说明了如何连接到 JIRA 实例,然后如何提取特定项目的所有问题:

# import the atlassian module to be able to connect to JIRA
from atlassian import Jira
jira_instance = Jira(
    #Url of the JIRA server
    url="https://miroslawstaron.atlassian.net/",
    #  user name
    username='email@domain.com',
    # token
    password='your_token',
    cloud=True
)
# get all the tasks/ issues for the project
jql_request = 'project = MLBPB'
issues = jira_instance.jql(jql_request)

在这个片段中,为了说明目的,我正在使用自己的 JIRA 数据库和自己的项目(MLBPB)。此代码需要导入atlassian-python-api模块。此模块提供了一个 API,用于使用 Python 连接到并交互 JIRA 数据库,类似于 Gerrit 的 API。因此,适用于 Gerrit 的最佳实践也适用于 JIRA。

从产品数据库中提取数据——GitHub 和 Git

JIRA 和 Gerrit 在一定程度上是主要产品开发工具的补充工具。然而,每个软件开发组织都使用源代码仓库来存储主要资产——公司软件产品的源代码。如今,使用最频繁的工具是 Git 版本控制和它的*亲 GitHub。源代码仓库可以成为机器学*系统非常有用的数据来源——我们可以提取产品的源代码并对其进行分析。

如果我们负责任地使用 GitHub,它对机器学*来说是一个很好的数据来源。请记住,由社区提供的开源源代码不是为了盈利。我们需要遵守许可证,并承认开源社区作者、贡献者和维护者所做的贡献。无论许可证如何,我们总是能够分析我们自己的代码或我们公司的代码。

一旦我们可以访问我们产品或我们想要分析的产品源代码,以下代码片段帮助我们连接到 GitHub 服务器并访问存储库:

# First create a Github instance:
# using an access token
g = Github(token, per_page=100)
# get the repo for this book
repo = g.get_repo("miroslawstaron/machine_learning_best_practices")
# get all commits
commits = repo.get_commits()

为了支持对代码的安全访问,GitHub 在连接到它时使用访问令牌而不是密码。我们还可以使用 SSL 和 CLI 接口,但为了简单起见,我们将使用带有令牌的 HTTPS 协议。g = Github(token, per_page=100)这一行使用令牌来实例化 PyGitHub 库的主类。令牌是唯一的,需要为每个仓库或每个用户单独生成。

下一行代码通过repo = g.get_repo("miroslawstaron/machine_learning_best_practices")建立与仓库的连接,在这个例子中,它连接到与这本书相关的仓库。最后,代码片段中的最后一行获取仓库中的提交数量。一旦获取,我们可以打印它并开始分析,如下面的代码片段所示:

# print the number of commits
print(f'Number of commits in this repo: {commits.totalCount}')
# print the last commit
print(f'The last commit message: {commits[0].commit.message}')

以下代码片段的最后一行打印出最新提交的提交信息。值得注意的是,最新提交总是位于提交列表的第一位。一旦我们知道提交,我们也可以访问包含在该提交中的文件列表。以下代码片段展示了这一点:

# print the names of all files in the commit
# 0 means that we are looking at the latest commit
print(commits[0].file)

打印提交中文件的列表是好的,但并不十分有用。更有用的事情是访问这些文件并分析它们。以下代码片段展示了如何从最*的两个提交中访问两个文件。首先,我们访问文件,然后下载它们的内容并将它们存储在两个不同的变量中——linesOnelinesTwo

# get one of the files from the commit
fileOne = commits[0].files[0]
# get the file from the second commit
fileTwo = commits[1].files[0]
# to get the content of the file, we need to get the sha of the commit
# otherwise we only get the content from the last commit
fl = repo.get_contents(fileOne.filename, ref=commits[0].sha)
fr = repo.get_contents(fileTwo.filename, ref=commits[1].sha)
# read the file content, but decoded into strings
# otherwise we would get the content in bytes
linesOne = fl.decoded_content
linesTwo = fr.decoded_content

最后,我们可以分析这两个文件,执行最重要的任务之一——获取两个文件之间的差异。我们可以使用difflib Python 库来完成这项任务,如下所示:

# calculate the diff using difflib
# for which we use a library difflib
import difflib
# print diff lines by iterating the list of lines
# returned by the difflib library
for line in difflib.unified_diff(str(linesOne),
                                 str(linesTwo),
                                 fromfile=fileOne.filename,
                                 tofile=fileTwo.filename):
  print(line)

上述代码片段允许我们识别文件之间的差异,并以类似于 GitHub 展示差异的方式打印它们。

我接下来的最佳实践与公共仓库的使用有关。

最佳实践 # 27

当从公共仓库访问数据时,请检查许可证并确保你承认创建了分析代码的社区的贡献。

如我之前提到的,开源程序是为了让每个人使用,包括分析和从中学*。然而,这个源代码背后的社区已经投入了无数小时来创建和完善它。因此,我们应该负责任地使用这些仓库。如果我们使用仓库来创建自己的产品,包括机器学*软件产品,我们需要承认社区的贡献,如果我们使用的是 copyleft 许可证下的软件,我们需要将我们的工作回馈给社区。

数据质量

在设计和开发机器学*系统时,我们从相对较低的水平考虑数据质量。我们寻找缺失值、异常值或类似情况。它们很重要,因为它们可能在训练机器学*模型时引起问题。尽管如此,从软件工程的角度来看,它们几乎足够了。

在构建可靠的软件系统时,我们需要了解我们使用的不仅仅是数据是否包含(或不包含)缺失值。我们需要知道我们是否可以信任数据(是否可信),数据是否具有代表性,或者是否是最新的。因此,我们需要为我们的数据建立一个质量模型。

在软件工程中,数据有几种质量模型,我经常使用并推荐的模型是 AIMQ 模型——一种评估信息质量的方法。

AIMQ 模型的质量维度如下(摘自 Lee, Y.W.等,AIMQ:信息质量评估方法。信息与管理,2002 年,40(2):p. 133-146):

  • 可访问性:信息易于检索,并且可以轻松访问我们的系统

  • 适当数量:信息对于我们的需求和我们的应用来说是足够的

  • 可信度:信息是可信的,可以信赖

  • 完整性:信息包括我们系统所需的所有必要值

  • 简洁表示:信息以紧凑和适当的方式格式化,适用于我们的应用

  • 一致表示:信息以一致的方式呈现,包括其随时间的变化表示

  • 易用性:信息易于操作以满足我们的需求

  • 无错误:信息对于我们所创建的应用来说是正确的、准确的和可靠的

  • 可解释性:很容易理解信息的含义

  • 客观性:信息是客观收集的,并基于事实

  • 相关性:信息对我们系统是有用的、相关的、适当的,并且适用于我们的系统

  • 声誉:信息在质量上享有良好声誉,且来源于可靠来源

  • 安全性:信息受到未经授权的访问保护,并且受到充分的限制

  • 时效性:信息对于我们的工作来说是足够最新的

  • 可理解性:信息易于理解和理解

其中一些维度对于所有类型的应用都是通用的。例如,“无错误”维度对所有系统和所有机器学*模型都相关。同时,“相关性”必须在我们所设计和设计的应用和软件的上下文中进行评估。接*原始数据的维度比与应用相关的维度更容易自动评估。对于与应用相关的维度,我们通常需要进行专家评估或进行手动分析或调查。

以可信度为例。为了评估我们的源数据是否可信以及是否可用于此应用,我们需要了解数据来自哪里,谁/什么创造了这些数据,以及基于哪些前提。这不能自动化,因为它需要人类、专家的判断。

因此,我们可以将这些维度组织在不同的抽象层次上 – 原始数据或源系统、用于训练和推理的数据、机器学*模型和算法,以及整个软件产品。显示这些维度中哪些更接*原始数据,哪些更接*算法,哪些更接*产品可能是有用的。图 4.3 展示了这种组织:

图 4.3 – 根据逻辑相关性组织的数据质量属性

图 4.3 – 根据逻辑相关性组织的数据质量属性

图 4.3 表明我们在检查信息质量方面存在一个抽象层次,这是完全正确的。最低的抽象层次,或者说第一次检查,旨在量化基本的质量维度。这些检查也不必非常复杂。整个质量测量和监控系统可以非常简单,但非常强大。图 4.4 展示了这样一个系统的概念设计。该系统由三个部分组成 – 机器学*管道、日志文件和信息质量测量与监控:

图 4.4 – 信息质量测量系统

图 4.4 – 信息质量测量系统

首先,机器学*管道包含探测器,或测量仪器,用于收集与信息质量相关的问题信息。例如,这些仪器可以收集有关访问数据是否存在问题的信息,这可以表明可访问性质量维度存在问题。

以下代码片段展示了如何在实践中实现这一点。它配置了一个基本的日志文件,用于收集机器学*管道的错误信息:

import logging
# create a logging file
# including the format of the log messages
logging.basicConfig(filename='./information_quality_gerrit.log',
                    filemode='w',
                    format='%(asctime)s;%(name)s;%(levelname)s;%(message)s',
                    level=logging.DEBUG)
# specifying the name of the logger,
# which will tell us that the message comes from this program
# and not from any other modules or components imported
logger = logging.getLogger('Gerrit data export pipeline')
# the first log message to indicate the start of the execution
# it is important to add this, since the same log-file can be re-used
# the re-use can be done by other components to provide one single point of logging
logger.info('Configuration started')

这段代码创建日志文件,为它们提供整个机器学*管道使用的唯一名称,并指定错误消息的格式。然后,在机器学*管道本身中,日志文件通过消息进行传播。以下代码片段展示了之前展示的数据导出工具是如何被配置为传播这些信息的:

# A bit of config – repo
gerrit_url = "https://gerrit.onap.org/r"
fileName = "./gerrit_reviews.csv"
# since we use a public oss repository, we don't need to authenticate
auth = None
# this line gets sets the parameters for the HTML API
rest = GerritRestAPI(url=gerrit_url, auth = auth)
logger.info('REST API set-up complete')
# a set of parameters for the JSON API to get changes in batches of 500
start = 0                       # which batch we start from – usually 0
logger.info('Connecting to Gerrit server and accessing changes')
try:
    # the main query where we ask the endpoing to provide us the list and details of all changes
    # each change is essentially a review that has been submitted to the repository
    changes = rest.get("/changes/?q=status:merged&o=ALL_FILES&o=ALL_REVISIONS&o=DETAILED_LABELS&start={}".format(start),
                       headers={'Content-Type': 'application/json'})
except Exception as e:
    logger.error('ENTITY ACCESS – Error retrieving changes: {}'.format)
logger.info(…) statement as well as error messages with the logger.error(…) statement.
			The content of this log file can be quite substantial, so we need to filter the messages based on their importance. That’s why we distinguish between errors and information.
			The following is a fragment of such a log file. The first line contains the information message (boldface `INFO`) to show that the machine learning pipeline has been started:

2023-01-15 17:11:45,618;Gerrit 数据导出管道;INFO;配置开始

2023-01-15 17:11:45,951;Gerrit 数据导出管道;INFO;配置结束

2023-01-15 17:11:46,052;Gerrit 数据导出管道;INFO;将新鲜数据下载到./gerrit_reviews.csv

2023-01-15 17:11:46,055;pygerrit2;DEBUG;解析 netrc 错误:netrc 文件缺失或未在 netrc 中找到凭据

2023-01-15 17:11:46,057;Gerrit 数据导出管道;INFO;获取从 0 到 500 的更改数据

2023-01-15 17:11:46,060;urllib3.connectionpool;DEBUG;开始新的 HTTPS 连接(1):gerrit.onap.org:443


			We filter these messages in the last part of our measurement system – the information quality measurement and monitoring system. This last part reads through the log files and collects the error messages, categorizes them, and then visualizes them:

try:

logFile = open("./information_quality_gerrit.log", "r")

for logMessage in logFile:

再次分割日志信息 - 这与我们的方法有关

在测量系统中结构化日志消息

logItem = logMessage.split(';')

logLevel = logItem[2]

logSource = logItem[1]

logTime = logItem[0]

logProblem = logItem[3]

这部分是关于提取相关信息

如果这确实是一个问题:

if (logLevel == 'ERROR'):

如果这是库的问题

if ('LIBRARIES' in logProblem):

iq_problems_configuration_libraries += 1

if ('ENTITY_ACCESS' in logProblem):

iq_problems_entity_access += 1

except Exception as e:

iq_general_error = 1


			The bold-faced lines categorize the error messages found – in other words, they quantify the quality dimensions. This quantification is important as we need to understand how many problems of each kind were found in the machine learning pipeline.
			The next step is to visualize the information quality, and for that, we need a quality analysis model. Then, we can use this quality model to visualize the quality dimensions:

def getIndicatorColor(ind_value):

if ind_value > 0:

return 'red'

else:

return 'green'

列 = ('信息质量检查', '值')

行 = ['实体访问检查', '库']

cell_text = [[f'实体访问: {iq_problems_entity_access}'],

[f'库: {iq_problems_configuration_libraries}']]

颜色 = [[getIndicatorColor(iq_problems_entity_access)],

[getIndicatorColor(iq_problems_configuration_libraries)]]


			The visualization can be done in several ways, but in the majority of cases, it is enough to visualize it in a tabular form, which is easy to overview and comprehend. The most important for this visualization is that it communicates whether there are (or not) any information quality problems:

fig, ax = plt.subplots()

ax.axis('tight')

ax.axis('off')

the_table = ax.table(cellText=cell_text,

cellColours=colors,

colLabels=columns,

loc='left')

plt.show()


			The result of this code fragment is the visual representation shown in *Figure 4**.5*. This example can be found in this book’s GitHub repository:
			![Figure 4.5 – Results from the quality checks, visualized in a tabular form](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ml-infra-bst-prac/img/B19548_04_5.jpg)

			Figure 4.5 – Results from the quality checks, visualized in a tabular form
			This rudimentary way of checking the information’s quality illustrates my next best practice.
			Best practice # 28
			Use simple logging to trace any problems in your machine learning pipeline to monitor information quality.
			It’s generally a good practice to design and develop robust software systems. Logging is one of the mechanisms that’s used to detect potential problems. Logging is also a very good software engineering practice for systems that are not interactive, such as machine learning-based ones. Therefore, extracting the information from logs can help us understand the quality of the information that is used in a machine learning-based system.
			Noise
			Data quality in machine learning systems has one additional and crucial attribute – noise. Noise can be defined as data points that contribute negatively to the ability of machine learning systems to identify patterns in the data. These data points can be outliers that make the datasets skew toward one or several classes in classification problems. The outliers can also cause prediction systems to over- or under-predict because they emphasize patterns that do not exist in the data.
			Another type of noise is contradictory entries, where two (or more) identical data points are labeled with different labels. We can illustrate this with the example of product reviews on Amazon, which we saw in *Chapter 3*. Let’s import them into a new Python script with `dfData = pd.read_csv('./book_chapter_4_embedded_1k_reviews.csv')`. In this case, this dataset contains a summary of the reviews and the score. We focus on these two columns and we define noise as different scores for the same summary review. For example, if one person provides a score of 5 for the review with the tag “Awesome!” and another person provides a score of 4 for another review with the tag “Awesome!,” the same data point becomes noisy as it is annotated with two different labels – two different scores.
			So, first, we must check whether there are any duplicate entries:

现在,让我们检查是否有任何重复条目

获取所有数据点的数量

allDataPoints = len(dfData.Summary)

获取唯一数据点的数量

uniqueDataPoints = len(dfData.Summary.unique())

检查唯一数据点和所有数据点的数量是否相同

if allDataPoints != uniqueDataPoints:

print(f'有 {allDataPoints - uniqueDataPoints} 个重复条目,这可能会产生噪声')


			This code checks whether the number of data points is the same as the number of unique data points; if not, then we risk having noisy entries. We can check whether there are duplicate entries by using the following code:

然后,我们找到重复数据点的索引

首先,我们分组数据点

dfGrouped = dfData.groupby(by=dfData.Summary).count()

然后,我们找到那些不唯一的索引

lstDuplicated = dfGrouped[dfGrouped.Time > 1].index.to_list()


			Now, we can remove all duplicate entries using the following command, though a simple solution would be to remove them (`dfClean = dfData[~dfData.Summary.isin(lstDuplicated)]`). A better solution is to check whether they are noisy entries or just duplicates. We can do this using the following code fragment:

对于这些数据点中的每一个,我们检查这些数据点

被分类到不同的标签,并仅移除具有不同标签的标签

for onePoint in lstDuplicated:

找到这个数据点的所有实例

dfPoint = dfData[dfData.Summary == onePoint]

现在检查这些数据点是否有不同的分数

numLabels = len(dfPoint.Score.unique())

如果标签数量超过 1,那么

这意味着数据集中有噪声

我们应该移除这个点

if numLabels > 1:

dfData.drop(dfData[dfData.Summary == onePoint].index, inplace=True)

让我们也打印出我们移除的数据点

print(f'点: {onePoint}, 标签数量: {len(dfPoint.Score.unique())}')


			After running this fragment of code, the dataset does not contain any contradictory entries and therefore no class noise. Although it is possible to adopt a different strategy – for example, instead of removing noisy data points, we could change them to one of the classes – such an approach changes the pattern in the data, and therefore is not fully representative of the data. We simply do not know which of the classes is more correct than the others, especially if there are duplicate data points with two different labels.
			Best practice # 29
			The best strategy to reduce the impact of noise on machine learning classifiers is to remove the noisy data points.
			Although we can correct noisy data points by changing their label or reducing the impact of these attributes on the predictions, the best strategy is to remove these data points. Removing is better as it does not change the patterns in the data. Imagine that we relabel noisy entries – this creates a pattern in the data that does not exist, which causes the algorithms to mispredict future data points.
			Removing noise from the data is the only one way to handle noise. Another method is to increase the number of features so that we can distinguish between data points. We can analyze data and identify whether there is a risk of noise, and then we can check whether  it is possible to add one more feature to the dataset to distinguish between entries labeled differently. However, this is outside the scope of this chapter.
			Summary
			Data for machine learning systems is crucial – without data, there can be no machine learning systems. In most machine learning literature, the process of training models usually starts with the data in tabular form. In software engineering, however, this is an intermediate step. The data is collected from source systems and needs to be processed.
			In this chapter, we learned how to access data from modern software engineering systems such as Gerrit, GitHub, JIRA, and Git. The code included in this chapter illustrates how to collect data that can be used for further steps in the machine learning pipeline – feature extraction. We’ll focus on this in the next chapter.
			Collecting data is not the only preprocessing step that is required to design and develop a reliable software system. Quantifying and monitoring information (and data) quality is equally important. We need to check that the data is fresh (timely) and that there are no problems in preprocessing that data.
			One of the aspects that is specific to machine learning systems is the presence of noise in the data. In this chapter, we learned how to treat class noise in the data and how to reduce the impact of the noise on the final machine learning algorithm.
			In the next chapter, we dive deeper into concepts related to data - clearning it from noise and quantifying its properties.
			References

				*   *Vaswani, A. et al., Attention is all you need. Advances in neural information processing systems,* *2017\. 30.*
				*   *Dastin, J., Amazon scraps secret AI recruiting tool that showed bias against women. In Ethics of Data and Analytics. 2018, Auerbach Publications.* *p. 296-299.*
				*   *Staron, M., D. Durisic, and R. Rana,* *Improving measurement certainty by using calibration to find systematic measurement error—a case of lines-of-code measure. In Software Engineering: Challenges and Solutions. 2017, Springer.* *p. 119-132.*
				*   *Staron, M. and W. Meding, Software Development Measurement Programs. Springer. https://doi. org/10.1007/978-3-319-91836-5, 2018\. 10:* *p. 3281333.*
				*   *Fenton, N. and J. Bieman, Software metrics: a rigorous and practical approach. 2014:* *CRC press.*
				*   *Li, N., M. Shepperd, and Y. Guo, A systematic review of unsupervised learning techniques for software defect prediction. Information and Software Technology, 2020\. 122:* *p. 106287.*
				*   *Staron, M. et al. Robust Machine Learning in Critical Care—Software Engineering and Medical Perspectives. In* *2021 IEEE/ACM 1st Workshop on AI Engineering-Software Engineering for AI (WAIN).* *2021\. IEEE.*
				*   *Zhang, J. et al., CoditT5: Pretraining for Source Code and Natural Language Editing. arXiv preprint* *arXiv:2208.05446, 2022.*
				*   *Staron, M. et al. Using machine learning to identify code fragments for manual review. In 2020 46th Euromicro Conference on Software Engineering and Advanced Applications (SEAA).* *2020\. IEEE.*
				*   *Ochodek, M., S. Kopczyńska, and M. Staron, Deep learning model for end-to-end approximation of COSMIC functional size based on use-case names. Information and Software Technology, 2020\. 123:* *p. 106310.*
				*   *Cichy, C. and S. Rass, An overview of data quality frameworks. IEEE Access, 2019\. 7:* *p. 24634-24648.*
				*   *Lee, Y.W. et al., AIMQ: a methodology for information quality assessment. Information & management, 2002\. 40(2):* *p. 133-146.*
				*   *Staron, M. and W. Meding. Ensuring reliability of information provided by measurement systems. In International Workshop on Software Measurement.* *2009\. Springer.*
				*   *Pandazo, K. et al. Presenting software metrics indicators: a case study. In Proceedings of the 20th international conference on Software Product and Process Measurement (**MENSURA). 2010.*
				*   *Staron, M. et al. Improving Quality of Code Review Datasets–Token-Based Feature Extraction Method. In International Conference on Software Quality.* *2021\. Springer.*

第五章:量化并改进数据属性

在机器学*系统中获取数据是一个漫长的过程。到目前为止,我们主要关注从源系统收集数据和从数据中清除噪声。然而,噪声并不是我们可能在数据中遇到的所有问题的唯一来源。缺失值或随机属性是可能导致机器学*系统出现问题的数据属性示例。即使输入数据的长度如果超出预期值,也可能成为问题。

在本章中,我们将更深入地探讨数据的属性以及如何改进它们。与上一章相比,我们将专注于特征向量而不是原始数据。特征向量已经是数据的一种转换,因此我们可以改变诸如噪声等属性,甚至改变数据被感知的方式。

我们将专注于文本的处理,这是许多机器学*算法中一个重要的部分。我们将从了解如何使用简单的算法,如词袋模型,将数据转换为特征向量开始。我们还将学*处理数据问题的技术。

本章将涵盖以下主要内容:

  • 为机器学*系统量化数据属性

  • 培育噪声——在干净数据集中的特征工程

  • 处理噪声数据——机器学*算法和噪声消除

  • 消除属性噪声——数据集精炼指南

特征工程——基础

特征工程是将原始数据转换为可用于机器学*算法的数字向量的过程。这个过程是有结构的,需要我们首先选择需要使用的特征提取机制——这取决于任务的类型——然后配置所选的特征提取机制。当所选算法配置完成后,我们可以使用它将原始输入数据转换为特征矩阵——我们称这个过程为特征提取。有时,在特征提取之前(或之后)需要处理数据,例如通过合并字段或去除噪声。这个过程称为数据整理。

特征提取机制的种类繁多,我们无法涵盖所有内容。我们也不需要这样做。然而,我们需要理解的是,特征提取机制的选择如何影响数据的属性。我们将在下一章深入探讨特征工程的过程,但在这章中,我们将介绍一个用于文本数据的基本算法。我们需要介绍它,以便了解它如何影响数据的属性以及如何应对特征提取过程中可能出现的最常见问题,包括处理需要清理的“脏”数据。

为了理解这个过程,让我们从使用称为“词袋”算法的特征提取的第一个文本示例开始。词袋是一种将文本转换为表示该文本中哪些单词的数字向量的方法。单词形成结果数据框中的特征集——或列。在下面的代码中,我们可以看到特征提取是如何工作的。我们使用了sklearn标准库来创建词袋特征向量。

在下面的代码片段中,我们取了两行 C 代码——printf("Hello world!");return 1——然后将这些代码转换成特征矩阵:

# create the feature extractor, i.e., BOW vectorizer
# please note the argument - max_features
# this argument says that we only want three features
# this will illustrate that we can get problems - e.g. noise
# when using too few features
vectorizer = CountVectorizer(max_features = 3)
# simple input data - two sentences
sentence1 = 'printf("Hello world!");'
sentence2 = 'return 1'
# creating the feature vectors for the input data
X = vectorizer.fit_transform([sentence1, sentence2])
# creating the data frame based on the vectorized data
df_bow_sklearn = pd.DataFrame(X.toarray(),
                              columns=vectorizer.get_feature_names(),
                              index=[sentence1, sentence2])
# take a peek at the featurized data
df_bow_sklearn.head()

粗体的行是创建CodeVectorizer类实例的语句,该类将给定的文本转换为特征向量。这包括提取已识别的特征。这一行有一个参数——max_features = 3。此参数告诉算法我们只想获取三个特征。在这个算法中,特征是输入文本中使用的单词。当我们向算法输入文本时,它提取标记(单词),然后对于每一行,它计算是否包含这些单词。这是在语句X = vectorizer.fit_transform([sentence1, sentence2])中完成的。当特征被提取后,结果数据集看起来如下:

Hello printf return
printf(“Hello world!”); 1 1 0
return 1 0 0 1

图 5.1 – 提取的特征创建此数据集

表格的第一行包含索引——输入算法的行——然后是10以表示该行包含词汇表中的单词。由于我们只要求三个特征,因此表格有三列——Helloprintfreturn。如果我们更改CountVectorizer()的参数,我们将获得这两行中的完整标记列表,即helloprintfreturnworld

对于这两行简单的 C 代码,我们得到了四个特征,这说明了这种特征提取可以快速增加数据的大小。这使我们转向我的下一个最佳实践。

最佳实践#30

平衡特征数量与数据点数量。特征数量并不总是越多越好。

在创建特征向量时,重要的是提取有意义的特征,这些特征可以有效地区分数据点。然而,我们应该记住,拥有更多特征将需要更多内存,并且可能会使训练过程变慢。它也容易受到缺失数据点的问题。

清洁数据

当涉及到机器学*时,数据集的一个最棘手的问题就是存在空数据点或数据点的特征值为空。让我们通过前一个章节中提取的特征的例子来说明这一点。在下面的表格中,我引入了一个空数据点——中间列的NaN值。这意味着该值不存在。

Hello printf return
printf(“Hello world!”); 1 NaN 0
return 1 0 0 1

图 5.2 – 表格中包含 NaN 值的提取特征

如果我们将这些数据作为机器学*算法的输入,我们会得到一个错误消息,指出数据包含空值,并且模型无法训练。这是对这个问题的非常准确的描述——如果存在缺失值,那么模型不知道如何处理它,因此无法进行训练。

应对数据集中的空值有两种策略——移除数据点或插补值。

让我们从第一个策略开始——移除空数据点。以下脚本读取我们用于进一步计算的数据,即我们的代码审查数据:

# read the file with gerrit code reviews
dfReviews = pd.read_csv('./gerrit_reviews.csv', sep=';')
# just checking that we have the right columns
# and the right data
dfReviews.head()

上述代码片段读取文件并显示其前 10 行,以便我们检查数据内容。

一旦我们将数据存储在内存中,我们可以检查包含实际代码行(命名为LOC)的列中包含 null 值的行数有多少。然后,我们还可以删除不包含任何数据的行/数据点。数据点的删除由以下行处理——dfReviews = dfReviews.dropna()。此语句删除了空行,并将结果保留在数据框本身中(inplace=True参数):

import numpy as np
# before we use the feature extractor, let's check if the data contains NANs
print(f'The data contains {dfReviews.LOC.isnull().sum()} empty rows')
# remove the empty rows
dfReviews = dfReviews.dropna()
# checking again, to make sure that it does not contain them
print(f'The data contains {dfReviews.LOC.isnull().sum()} empty rows')

在执行这些命令后,我们的数据集已准备好创建特征向量。我们可以使用CountVectorizer从数据集中提取特征,如下面的代码片段所示:

# now, let's convert the code (LOC) column to the vector of features
# using BOW from the example above
vectorizer = CountVectorizer(min_df=2,
                             max_df=10)
dfFeatures = vectorizer.fit_transform(dfReviews.LOC)
# creating the data frame based on the vectorized data
df_bow_sklearn = pd.DataFrame(dfFeatures.toarray(),
                              columns=vectorizer.get_feature_names(),index=dfReviews.LOC)
# take a peek at the featurized data
df_bow_sklearn.head()

此代码片段创建了一个包含两个参数的词袋模型(CountVectorizer)——标记的最小频率和最大频率。这意味着算法计算每个标记在数据集中出现的频率统计,然后选择满足条件的标记。在我们的情况下,算法选择至少出现两次(min_df=2)且最多 20 次(max_df=20)的标记。

此代码片段的结果是一个包含 661 个特征的大型数据框,每个特征对应于我们数据集中每行代码。我们可以通过在执行上述代码片段后编写len(df_bow_sklearn.columns)来检查这一点。

为了检查如何处理数据插补,让我们打开一个不同的数据集并检查每列有多少缺失数据点。让我们读取名为gerrit_reviews_nan.csv的数据集,并使用以下代码片段列出该数据集中的缺失值:

# read data with NaNs to a dataframe
dfNaNs = pd.read_csv('./gerrit_reviews_nan.csv', sep='$')
# before we use the feature extractor, let's check if the data contains NANs
print(f'The data contains {dfNaNs.isnull().sum()} NaN values')

由于这个代码片段,我们得到了一个包含列中缺失值数量的列表——列表的尾部如下:

yangresourcesnametocontentmap     213
yangtextschemasourceset           205
yangtextschemasourcesetbuilder    208
yangtextschemasourcesetcache      207
yangutils                         185

有许多缺失值,因此,我们需要采用不同于删除它们的策略。如果我们删除所有这些值,我们将得到恰好 0 个数据点——这意味着每个(或更多)数据列中都有一个 NaN 值。所以,我们需要采用另一种策略——填充。

首先,我们需要为填充器准备数据,它只对特征有效。因此,我们需要从数据集中删除索引:

# in order to use the imputer, we need to remove the index from the data
# we remove the index by first re-setting it (so that it becomes a regular column)
# and then by removing this column.
dfNaNs_features = dfNaNs.reset_index()
dfNaNs_features.drop(['LOC', 'index'], axis=1, inplace=True)
dfNaNs_features.head()

然后,我们可以创建填充器。在这个例子中,我使用了一种基于在现有数据上训练分类器,然后使用它来填充原始数据集中数据的现代方法。训练填充器的代码片段如下所示:

# let's use iterative imputed to impute data to the dataframe
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
# create the instance of the imputer
imp = IterativeImputer(max_iter=3,
                       random_state=42,
                       verbose = 2)
# train the imputer on the features in the dataset
imp.fit(dfNaNs_features)

代码片段的最后一行是填充器的实际训练。在这之后,我们可以开始对数据集进行填充,如下面的代码片段所示:

# now, we fill in the NaNs in the original dataset
npNoNaNs = imp.transform(dfNaNs_features)
dfNoNaNs = pd.DataFrame(npNoNaNs)

在这个片段之后,我们得到了一个包含填充值的数据集。现在,我们需要记住这些值只是估计值,而不是真实值。这个特定的数据集很好地说明了这一点。当我们执行dfNoNaNs.head()命令时,我们可以看到一些填充值是负数。由于我们的数据集是CountVectorizer的结果,负值不太可能。因此,我们可以使用另一种类型的填充器——KNNImputer。这个填充器使用最*邻算法找到最相似的数据点,并根据相似数据点的值填充缺失数据。这样,我们得到一组具有相同属性(例如,没有负值)的填充值,与数据集的其余部分相同。然而,填充值的模式是不同的。

因此,这是我的下一个最佳实践。

最佳实践#30

在数据点之间相似性预期是局部的情况下使用 KNNImputer。

当数据中有明显的局部结构时,KNNImputer表现良好,尤其是在相邻数据点在缺失值的特征上相似时。它可能对最*邻数(k)的选择敏感。

IterativeImputer在数据集中特征之间存在复杂关系和依赖时往往表现良好。它可能更适合那些缺失值不容易由局部模式解释的数据集。

然而,检查填充方法是否为当前数据集提供逻辑结果,以降低偏差风险。

数据管理中的噪声

缺失数据和矛盾的注释只是数据问题的一种类型。在许多情况下,由特征提取算法生成的大型数据集可能包含过多的信息。特征可能是多余的,不会对算法的最终结果做出贡献。许多机器学*模型可以处理特征中的噪声,称为属性噪声,但特征过多可能会在训练时间、存储甚至数据收集本身方面造成成本。

因此,我们也应该注意属性噪声,识别它,然后将其移除。

属性噪声

在大型数据集中减少属性噪声有几种方法。其中一种方法是一个名为成对属性噪声检测算法PANDA)的算法。PANDA 成对比较特征并识别出哪些特征给数据集带来了噪声。这是一个非常有效的算法,但不幸的是计算量非常大。如果我们的数据集有几百个特征(这是我们真正需要使用这个算法的时候),我们需要大量的计算能力来识别这些对分析贡献甚微的特征。

幸运的是,有一些机器学*算法提供了类似的功能,同时计算开销很小。其中之一是随机森林算法,它允许你检索特征重要性值的集合。这些值是一种识别哪些特征在这个森林中的任何决策树中都没有被使用的方法。

让我们看看如何使用该算法提取和可视化特征的重要性。在这个例子中,我们将使用前几章从 Gerrit 工具中提取的数据:

# importing the libraries to vectorize text
# and to manipulate dataframes
from sklearn.feature_extraction.text import CountVectorizer
import pandas as pd
# create the feature extractor, i.e., BOW vectorizer
# please note the argument - max_features
# this argument says that we only want three features
# this will illustrate that we can get problems - e.g. noise
# when using too few features
vectorizer = CountVectorizer()
# read the file with gerrit code reviews
dfReviews = pd.read_csv('./gerrit_reviews.csv', sep=';')

在这个数据集中,我们有两个列是从中提取特征的。第一列是LOC列,我们使用CountVectorizer来提取特征——就像在之前的例子中一样。这些特征将成为训练算法的X值。第二列是感兴趣的列是message列。message列用于提供decision类。为了转换消息文本,我们使用情感分析模型来识别消息是正面还是负面。

首先,让我们使用CountVectorizer提取 BOW 特征:

# now, let's convert the code (LOC) column to the vector of features
# using BOW from the example above
vectorizer = CountVectorizer(min_df=2,
                             max_df=10)
dfFeatures = vectorizer.fit_transform(dfReviews.LOC)
# creating the data frame based on the vectorized data
df_bow_sklearn = pd.DataFrame(dfFeatures.toarray(),
                              columns=vectorizer.get_feature_names(),index=dfReviews.LOC)

要将信息转换为情感,我们可以使用 Hugging Face Hub 上公开可用的模型。我们需要使用以下命令安装相关库:! pip install -q transformers。一旦我们有了这些库,我们就可以开始特征提取:

# using a classifier from the Hugging Face hub is quite straightforward
# we import the package and create the sentiment analysis pipeline
from transformers import pipeline
# when we create the pipeline, and do not provide the model
# then the huggingface hub will choose one for us
# and download it
sentiment_pipeline = pipeline("sentiment-analysis")
# now we are ready to get the sentiment from our reviews.
# let's supply it to the sentiment analysis pipeline
lstSentiments = sentiment_pipeline(list(dfReviewComments))
# transform the list to a dataframe
dfSentiments = pd.DataFrame(lstSentiments)
# and then we change the textual value of the sentiment to
# a numeric one – which we will use for the random forest
dfSentiment = dfSentiments.label.map({'NEGATIVE': 0, 'POSITIVE': 1})

上述代码片段使用了预训练的情感分析模型和一个来自标准管道的模型——sentiment-analysis。结果是包含正面或负面情感的 dataframe。

现在,我们有了X值——从代码行中提取的特征——以及预测的Y值——来自评论消息的情感。我们可以使用这些信息创建一个数据框,将其用作随机森林算法的输入,训练算法,并确定哪些特征对结果贡献最大:

# now, we train the RandomForest classifier to get the most important features
# Note! This training does not use any data split, as we only want to find
# which features are important.
X = df_bow_sklearn.drop(['sentiment'], axis=1)
Y = df_bow_sklearn['sentiment']
# import the classifier – Random Forest
from sklearn.ensemble import RandomForestClassifier
# create the classifier
clf = RandomForestClassifier(max_depth=10, random_state=42)
# train the classifier
# please note that we do not check how good the classifier is
# only train it to find the features that are important.
Clf.fit(X,Y)

当随机森林模型训练完成后,我们可以提取重要特征列表:

# now, let's check which of the features are the most important ones
# first we create a dataframe from this list
# then we sort it descending
# and then filter the ones that are not important
dfImportantFeatures = pd.DataFrame(clf.feature_importances_, index=X.columns, columns=['importance'])
# sorting values according to their importance
dfImportantFeatures.sort_values(by=['importance'],
                                ascending=False,
                                inplace=True)
# choosing only the ones that are important, skipping
# the features which have importance of 0
dfOnlyImportant = dfImportantFeatures[dfImportantFeatures['importance'] != 0]
# print the results
print(f'All features: {dfImportantFeatures.shape[0]}, but only {dfOnlyImportant.shape[0]} are used in predictions. ')

以下代码片段选择了重要性大于0的特征,并将它们列出。我们发现 662 个特征中有 363 个被用于预测。这意味着剩下的 270 个特征只是属性噪声。

我们还可以使用seaborn库可视化这些特征,如下面的代码片段所示:

# we use matplotlib and seaborn to make the plot
import matplotlib.pyplot as plt
import seaborn as sns
# Define size of bar plot
# We make the x axis quite much larger than the y-axis since
# there is a lot of features to visualize
plt.figure(figsize=(40,10))
# plot seaborn bar chart
# we just use the blue color
sns.barplot(y=dfOnlyImportant['importance'],
            x=dfOnlyImportant.index,
            color='steelblue')
# we make the x-labels rotated so that we can fit
# all the features
plt.xticks(rotation=90)
# add chart labels
plt.title('Importance of features, in descending order')
plt.xlabel('Feature importance')
plt.ylabel('Feature names')

以下代码片段为数据集生成了以下图表:

图 5.3 – 具有许多特征的特性重要性图表

图 5.3 – 具有许多特征的特性重要性图表

由于特征太多,图表变得非常杂乱且难以阅读,所以我们只能可视化前 20 个特征,以了解哪些是最重要的。

图 5.4 – 数据集中最重要的前 20 个特征

图 5.4 – 数据集中最重要的前 20 个特征

上述代码示例表明,我们可以将特征数量减少 41%,这几乎是特征数量的一半。算法只需几秒钟就能找到最重要的特征,这使得它成为减少数据集中属性噪声的完美候选。

最佳实践#31

使用随机森林分类器来消除不必要的特征,因为它提供了非常好的性能。

虽然我们并没有真正得到关于被移除的特征包含多少噪声的信息,但得到它们对预测算法没有价值的信息就足够了。因此,我建议在机器学*管道中使用这种特征减少技术,以减少我们管道的计算和存储需求。

数据分割

在设计基于机器学*的软件的过程中,另一个重要的属性是理解数据的分布,并且随后确保用于训练和测试的数据具有相似的分布。

用于训练和验证的数据分布很重要,因为机器学*模型识别模式并重新创建它们。这意味着如果训练数据中的数据分布与测试集中的数据分布不同,我们的模型就会错误地分类数据点。错误分类(或错误预测)是由于模型在训练数据中学*到的模式与测试数据不同所导致的。

让我们了解分割算法在理论上的工作原理以及在实际中的应用。图 5.5展示了在理论和概念层面上分割是如何工作的:

图 5.5 – 将数据分割为训练集和测试集

图 5.5 – 将数据分割为训练集和测试集

图标代表审查评论(和讨论)。每个图标代表一个自己的讨论线程,每种类型的图标反映不同的团队。分割数据集背后的想法是,这两个集合非常相似,但并不完全相同。因此,训练集和测试集中元素的分发需要尽可能相似。然而,这并不总是可能的,如图5**.5所示 – 训练集中有一种类型的四个图标中的三个,而测试集中只有一个。在设计机器学*软件时,我们需要考虑这个方面,尽管它只与机器学*模型相关。我们的数据处理管道应该包含检查,提供理解数据是否正确分布的能力,如果不正确,我们需要纠正它。如果我们不纠正它,我们的系统开始做出错误的预测。在基于机器学*的系统中,数据分布随时间的变化,这是自然的,被称为概念漂移。

让我们通过计算我们的 Gerrit 审查数据集中数据的分布来实际应用这个方法。首先,我们读取数据,然后使用sklearntrain_test_split方法创建一个随机分割:

# then we read the dataset
dfData = pd.read_csv('./bow_sentiment.csv', sep='$')
# now, let's split the data into train and test
# using the random split
from sklearn.model_selection import train_test_split
X = dfData.drop(['LOC', 'sentiment'], axis=1)
y = dfData.sentiment
# now we are ready to split the data
# test_size parameter says that we want 1/3rd of the data in the test set
# random state allows us to replicate the same split over and over again
X_train, X_test, y_train, y_test =
                train_test_split(X, y,
                                 test_size=0.33,
                                 random_state=42)

在这个代码片段中,我们将预测值(y)与预测值(X)特征分开。然后我们使用train_test_split方法将数据集分割成两个部分 – 训练集中的三分之二数据和测试集中的一分之一数据。这个 2:1 的比例是最常见的,但根据应用和数据集的不同,我们也可能遇到 4:1 的比例。

现在我们有了两组数据,我们应该探索它们的分布是否相似。本质上,我们应该对每个特征和预测变量(y)都这样做,但在我们的数据集中,我们有 662 个特征,这意味着我们可能需要进行如此多的比较。所以,为了举例,我们只可视化其中一个 – 在我们之前的例子中被认为是最重要的一个 – dataresponse

# import plotting libraries
import matplotlib.pyplot as plt
import seaborn as sns
# we make the figure a bit larger
# and the font a bit more visible
plt.figure(figsize=(10,7))
sns.set(font_scale=1.5)
# here we visualize the histogram using seaborn
# we take only one of the variables, please see the list of columns
# above, or use print(X_train.columns) to get the list
# I chose the one that was the most important one
# for the prediction algorithm
sns.histplot(data=X_train['dataresponse'],
             binwidth=0.2)

我们也将对测试集进行同样的操作:

plt.figure(figsize=(10,7))
sns.set(font_scale=1.5)
sns.histplot(data=X_test['dataresponse'],
             binwidth=0.2)

这两个片段产生了两个直方图,显示了该变量的分布。它们在图 5**.6中展示:

图 5.6 – 训练集和测试集中数据响应特征的分布

图 5.6 – 训练集和测试集中数据响应特征的分布

列车集的分布位于左侧,测试集的分布位于右侧。乍一看,分布显示只有一个值 - 0 值。因此,我们需要更深入地手动探索数据。我们可以通过计算每个值(0 和 1)的实体数量来检查分布:

# we can even check the count of each of these values
X_train_one_feature = X_train.groupby(by='dataresponse').count()
X_train_one_feature
# we can even check the count of each of these values
X_test_one_feature = X_test.groupby(by='dataresponse').count()
X_test_one_feature

从前面的计算中,我们发现训练集中有 624 个 0 值和 5 个 1 值,测试集中有 309 个 0 值和 1 个 1 值。这些比例并不完全相同,但考虑到规模——0 值显著多于 1 值——这不会对机器学*模型产生任何影响。

我们数据集中的特征应该具有相同的分布,Y值——预测变量也是如此。我们可以使用相同的技巧来可视化Y值之间的类别分布。下面的代码片段正是这样做的:

# we make the figure a bit larger
# and the font a bit more visible
plt.figure(figsize=(10,7))
sns.set(font_scale=1.5)
sns.histplot(data=y_train, binwidth=0.5)
sns.histplot(data=y_test,  binwidth=0.5)

此代码片段生成两个图表,显示了两个类别的差异。它们在图 5.7中展示:

图 5.7 – 训练和测试数据中类(0 和 1)的分布

图 5.7 – 训练和测试数据中类(0 和 1)的分布

预测的Y变量 0 值是负面情绪值,而 1 值是正面情绪值。尽管两个图表中 y 轴的刻度不同,但分布非常相似——在负面(0)情绪和正面(1)情绪的数量上大约是 2:1。

类别不平衡——0 的数量远大于 1 的数量,但分布相同。类别不平衡的事实意味着在此数据上训练的模型略微偏向负面情绪而不是正面情绪。然而,这反映了我们的经验观察:在代码审查中,审查员更可能评论需要改进的代码,而不是写得很好的代码。

最佳实践#32

尽可能保留数据的原始分布,因为它反映了经验观察。

尽管我们可以使用欠采样、过采样或类似的技术来平衡类别,但我们应尽可能保持原始分布。改变分布使模型在预测/分类方面“更公平”,但它改变了观察到的现象的本质。

机器学*模型如何处理噪声

从数据集中减少噪声是一个耗时的工作,而且也是一个难以自动化的任务。我们需要了解数据中是否存在噪声,数据中存在什么类型的噪声,以及如何去除它。幸运的是,大多数机器学*算法在处理噪声方面相当出色。

例如,我们迄今为止使用得相当多的算法——随机森林——对数据集中的噪声相当鲁棒。随机森林是一个集成模型,这意味着它由几个独立的决策树组成,这些决策树内部“投票”选择最佳结果。因此,这个过程可以过滤掉噪声,并趋向于数据中包含的模式。

深度学*算法也有类似的特性——通过利用大量的小神经元,这些网络对大数据集中的噪声具有鲁棒性。它们可以强制数据中的模式。

最佳实践 #33

在大型软件系统中,如果可能的话,依赖机器学*模型来处理数据中的噪声。

这听起来可能像是在提出一条简单的出路,但事实并非如此。数据的手动清理至关重要,但它也很慢且成本高昂。因此,在大型系统操作期间,最好选择一个对数据噪声鲁棒且同时使用更干净数据的模型。由于手动噪声处理过程需要时间和精力,依赖它们将为我们的产品运营带来不必要的成本。

因此,使用为我们做这件事的算法并因此创建可靠且维护成本最低的产品会更好。与其进行昂贵的噪声清理过程,不如重新训练算法,让它为你做这项工作。

在下一章中,我们将探讨数据可视化技术。这些技术帮助我们理解数据中的依赖关系,以及它是否揭示了可以被机器学*模型学*到的特征。

参考文献

  • *斯科特,S. 和 S. 马特温. 文本分类的特征工程. 在 ICML. 1999.

  • 库尔卡尼,A. 等,将文本转换为特征. 自然语言处理食谱:使用 Python 的机器学*和深度学*解锁文本数据,2021: p. 63-106.

  • 范·胡尔斯,J.D.,T.M. 科什戈法塔和黄,成对属性噪声检测算法. 知识与信息系统,2007. 11: p. 171-190.

  • 李,X. 等,利用 BERT 进行端到端基于方面的情感分析. arXiv 预印本 arXiv:1910.00883, 2019.

  • 徐,Y. 和 R. 古德雷克,关于分割训练集和验证集:比较交叉验证、自助法和系统抽样在估计监督学*泛化性能方面的研究. 分析与测试杂志,2018. 2(3): p. 249-262.

  • 莫辛,V. 等. 比较测试深度学*算法的输入优先级技术. 在 2022 年 48 届欧姆尼微软件工程和高级应用会议(SEAA). 2022. IEEE.

  • 刘,X.-Y.,吴,J. 和 周志华,探索性欠采样用于类别不平衡学*. IEEE 系统,人,和网络,第 B 部分(网络学),2008. 39(2): p. 539-550.

  • 阿特拉,A. 等,不同机器学*算法对噪声的敏感性. 计算机科学学院杂志,2011. 26(5): p. 96-103.

第二部分:数据获取与管理

机器学*软件比其他类型的软件更依赖于数据。为了利用统计学*,我们需要收集、处理和准备数据以用于机器学*模型的发展。数据需要代表软件解决的问题以及它提供的服务,不仅在开发期间,而且在运营期间都需要。在本部分书中,我们专注于数据——我们如何获取它以及我们如何使它对机器学*模型的训练、测试和部署有用。

本部分包含以下章节:

  • 第六章, 机器学*系统中的数据处理

  • 第七章, 数值和图像数据的特征工程

  • 第八章, 自然语言数据的特征工程

第六章:在机器学*系统中处理数据

我们在第三章中讨论了数据,其中我们介绍了在机器学*系统中使用的数据类型。在本章中,我们将更深入地探讨数据和算法相互交织的方式。我们将以通用术语讨论数据,但在本章中,我们将解释机器学*系统中需要哪种类型的数据。我将解释所有类型的数据都是以数值形式使用的——要么作为特征向量,要么作为更复杂的特征矩阵。然后,我将解释将非结构化数据(例如,文本)转换为结构化数据的必要性。本章将为深入探讨每种类型的数据奠定基础,这是下一章的内容。

在本章中,我们将做以下工作:

  • 讨论测量过程(获取数值数据)以及在该过程中使用的测量仪器

  • 使用 Matplotlib 和 Seaborn 库可视化数值数据

  • 使用主成分分析PCA)降低维度

  • 使用 Hugging Face 的 Dataset 模块下载和处理图像和文本数据

数值数据

数值数据通常以数字表的形式出现,类似于数据库表。这种形式中最常见的数据之一是指标数据——例如,自 1980 年代以来一直使用的标准面向对象指标。

数值数据通常是测量过程的成果。测量过程是一个使用测量仪器将实体的经验属性量化为数字的过程。这个过程必须保证重要的经验属性在数学领域中得以保留——也就是说,在数字中。图 6.1展示了这一过程的例子:

图 6.1 – 使用缺陷进行质量测量的测量过程示例

图 6.1 – 使用缺陷进行质量测量的测量过程示例

这个过程的重要部分包括三个要素。首先是测量仪器,它需要将经验属性真实地映射到数字上。然后是测量标准,例如 ISO 计量学词汇VIM),被称为测量的真值。最后,我们有测量过程的成果——将测量仪器应用于特定的测量实体——这会产生一个数字,即测量属性的量化。然而,一个单一的数字并不能表征整个软件产品或其任何部分,无论它对测量实体的真实性如何。因此,在实践中,我们使用多个测量仪器来创建对测量实体的整体视图。

这就是数值数据发挥作用的地方。每个表征测量实体的测量值都存储在数据库或表中——每个实体成为一行,每个度量成为一列。我们拥有的列越多,测量的实体特征就越好。然而,同时,我们收集的度量越多,它们相互关联、相关(正相关性或负相关性)以及重叠的风险就越高。因此,我们需要对数据进行一些处理,以便对其有一个大致的了解。所以,首先,我们必须可视化数据。

本章的这一部分所使用的数据来自 Alhustain 和 Sultan 的一篇论文(预测面向对象度量的相对阈值。"2021 IEEE/ACM 国际技术债务会议(TechDebt)。IEEE,2021)并可在 Zenodo 上获得(zenodo.org/records/4625975),这是软件工程研究中最常用的开放数据存储库之一。数据包含典型面向对象度量度量的值:

  • 对象之间的耦合CB):从测量实体(类)到其他类的引用数量

  • 直接类耦合DCC):从此类到其他类的连接数量(例如,关联)

  • 导出耦合ExportCoupling):从类的出去连接数量

  • 导入耦合ImportCoupling):到类的进入连接数量

  • 方法数量NOM):类中的方法数量

  • 按类加权的度量方法WMC):类中方法的数量,按其大小加权

  • 缺陷计数defect):为此类发现的缺陷数量

数据集描述了来自 Apache 基金会的几个软件项目——例如,Ant 工具。对于每个产品,测量的实体是项目中的类。

那么,让我们从下一个最佳实践开始,这将引导我们进入可视化阶段。

最佳实践 #34

在处理数值数据时,首先可视化它,从数据的概览视图开始。

当我处理数值数据时,我通常从可视化开始。我从数据的概述开始,然后逐步深入到细节。

总结数据

使用表格、交叉表以及图表可以总结数据。我通常开始工作的图表之一就是相关图——它是一个显示数据集中每个变量/度量之间相关性的图表。

因此,让我们将数据读入笔记本并开始可视化:

# read the file with data using openpyxl
import pandas as pd
# we read the data from the excel file,
# which is the defect data from the ant 1.3 system
dfDataAnt13 = pd.read_excel('./chapter_6_dataset_numerical.xlsx',
                            sheet_name='ant_1_3',
                            index_col=0)

一旦数据集存储在内存中,我们可以使用 Python 的 Seaborn 库通过相关图来可视化它。以下代码就是这样做:

# now, let's visualize the data using correlograms
# for that, we use the seaborn library
import seaborn as sns
import matplotlib.pyplot as plt
# in seaborn, the correlogram is called
# pairplot
sns.pairplot(dfDataAnt13)

此代码片段的结果是以下相关图:

图 6.2 – 来自 Alhusain 论文数据集的相关图

图 6.2 – 来自 Alhusain 论文数据集的相关图

这里有趣的部分是展示在对角线单元格中的每个度量值的分布。在我们的数据中,这种分布对于某些变量来说很难解释,因此我们可以以不同的方式可视化它。当我们用sns.pairplot(dfDataAnt13, diag_kind="kde")替换代码片段的最后一行时,我们得到一个新的可视化,可以更好地查看分布。这显示在图 6.3

图 6.3 – 具有更好分布可视化的自相关图

图 6.3 – 具有更好分布可视化的自相关图

这些自相关图为我们提供了快速了解哪些变量可以相互关联的导向。这些相关性是我们可以在以后的工作中使用的。

我们还可以通过使用热图来可视化数字来查看数据。热图是一种表格可视化,其中颜色的强度表示每个变量值的强度。我们可以使用以下代码创建热图:

# heatmap
p1 = sns.heatmap(dfDataAnt13, cmap="Reds")

结果图展示在图 6.4

图 6.4 – 度量值的总结热图

图 6.4 – 度量值的总结热图

在深入进行相关性分析之前,我经常先进行一些成对比较的深入分析。我也建议我的学生这样做,因为处理成对数据可以让我们理解变量之间的联系。

因此,这是我的下一个最佳实践。

最佳实践#35

在总体层面上可视化数据时,关注值之间关系的强度和连接。

在总体层面上进行可视化可以为我们提供许多不同的视角,但我们应该寻找变量之间的联系。自相关图和热图为我们提供了这种数据可视化和理解。

深入了解相关性

一套好的图表用于工作的是散点图。然而,我经常使用被称为 KDE 图(核密度估计图)的图表,也称为密度图。它们提供了变量更好的概述。以下代码片段以这种方式可视化数据:

# now, let's make some density plots
# set seaborn style
sns.set_style("white")
# Basic 2D density plot
sns.kdeplot(x=dfDataAnt13.CBO, y=dfDataAnt13.DCC)
plt.show()

此代码片段的结果是图 6.5中展示的图表:

图 6.5 – 两个度量值(DCC 和 CBO)的密度图

图 6.5 – 两个度量值(DCC 和 CBO)的密度图

此图表表明,两个度量值 – CBO 和 DCC – 相互之间有很强的依赖性(或者它们量化了相似/相同的可度量概念)。

如果我们想在仪表板上使用此图,可以使用以下代码片段使其更美观:

# Custom the color, add shade and bandwidth
sns.kdeplot(x=dfDataAnt13.WMC,
            y=dfDataAnt13.ImportCoupling,
            cmap="Reds",
            shade=True,
            bw_adjust=.5)
plt.show()

此代码片段的结果是以下图表:

图 6.6 – 带有颜色图的密度图

图 6.6 – 带有颜色图的密度图

前面的图表显示了每个区域的相关性和点数——颜色越深,该区域的数据点就越多。对于 DCC 和 CBO 测量的相同图表显示在图 6.7中:

图 6.7 – DCC 和 CBO 测量的密度图,带有颜色图

图 6.7 – DCC 和 CBO 测量的密度图,带有颜色图

最后,我们可以使用气泡图来可视化相关性和每个组的数据点数。以下代码创建了气泡图:

# now a bubble diagram
# use the scatterplot function to build the bubble map
sns.scatterplot(data=dfDataAnt13,
                x="NOM",
                y="DCC",
                size="Defect",
                legend=False,
                sizes=(20, 2000))
# show the graph
plt.show()

这段代码产生了图 6.8中展示的图表:

图 6.8 – 散点图 – 一种称为气泡图的变体

图 6.8 – 散点图 – 一种称为气泡图的变体

这个图让我们可以看到散点图中每个区域的点数,这有助于我们直观地追踪相关性。

总结单个测量值

散点图和密度图适合追踪变量之间的依赖关系。然而,我们经常需要总结单个测量值。为此,我们可以使用箱线图。以下代码为我们示例中的数据创建了一个箱线图:

# boxplot
sns.boxplot( x=dfDataAnt13.Defect, y=dfDataAnt13.CBO )

结果是图 6.9中展示的箱线图:

图 6.9 – 描述有缺陷和无缺陷类别的 CBO 测量的箱线图

图 6.9 – 描述有缺陷和无缺陷类别的 CBO 测量的箱线图

总结提供了快速视觉指示,表明有缺陷的类别通常比无缺陷的类别与其他类别更紧密地连接。这并不令人惊讶,因为通常类别是相互连接的,而那些不连接的类别通常非常简单,因此不太可能出错。

箱线图的一种变体是提琴图,如果我们把最后一个代码片段的最后一行改为sns.violinplot( x='Defect', y='CBO', data=dfDataAnt13),就会得到这样的提琴图:图 6.10展示了这样的提琴图:

图 6.10 – 一种箱线图的变体,称为提琴图

图 6.10 – 一种箱线图的变体,称为提琴图

可视化是理解我们拥有的数值数据的好方法。我们可以更进一步,通过使用降维等方法开始与之工作。

因此,这里是下一个最佳实践。

最佳实践#36

深入分析单个分析应该由当前的机器学*任务指导。

虽然我们没有明确讨论我们的数值数据的任务,但它始终存在。在涉及缺陷相关的数据的情况下,最常见的任务是预测每个模块或类别的缺陷数量。这意味着像提琴图这样的图表非常有用,它为我们提供了对是否存在某种差异的视觉理解——这种差异可以被机器学*模型捕捉。

减少测量数量 – 主成分分析(PCA)

本章关于数值数据的最终分析是关于减少变量数量。它来自统计学领域,并且已被用于减少实验中的变量数量:PCA(Wold,1987 #104)。简而言之,PCA 是一种找到最佳拟合预定义数量向量的技术,以适应手头的数据。它不会删除任何变量;相反,它会以这种方式重新计算它们,使得新变量集(称为主成分)之间的相关性最小化。

让我们使用以下代码片段将此应用于我们的数据集:

# before we use PCA, we need to remove the variable "defect"
# as this is the variable which we predict
dfAnt13NoDefects = dfDataAnt13.drop(['Defect'], axis=1)
# PCA for the data at hand
from sklearn.decomposition import PCA
# we instantiate the PCA class with two parameters
# the first one is the number of principal components
# and the second is the random state
pcaComp = PCA(n_components=2,
              random_state=42)
# then we find the best fit for the principal components
# and fit them to the data
vis_dims = pcaComp.fit_transform(dfAnt13NoDefects)

现在,我们可以可视化数据:

# and of course, we could visualize it
import matplotlib.pyplot as plt
import matplotlib
import numpy as np
colors = ["red", "darkgreen"]
x = [x for x,y in vis_dims]
y = [y for x,y in vis_dims]
# please note that we use the dataset with defects to
# assign colors to the data points in the diagram
color_indices = dfDataAnt13.Defect
colormap = matplotlib.colors.ListedColormap(colors)
plt.scatter(x, y, c=color_indices, cmap=colormap, alpha=0.3)
for score in [0,1]:
    color = colors[score]
plt.rcParams['figure.figsize'] = (20,20)

这个代码片段产生了图 6.11中展示的图表:

图 6.11 – 缩减缺陷数据集维度的 PCA 结果。红色数据点是具有缺陷的类别,绿色数据点是无缺陷的类别

图 6.11 – 缩减缺陷数据集维度的 PCA 结果。红色数据点是具有缺陷的类别,绿色数据点是无缺陷的类别

PCA 变换的典型特征是其线性。我们可以看到这个图表包含了一些这样的痕迹 - 它看起来像一个三角形,有一个水平维度沿着x 轴,一个垂直维度沿着y 轴,以及左侧的 0 点。

对于这个数据集,图表显示红色标记的数据点聚集在左侧,而绿色标记的点稍微向右分散。这意味着有缺陷和无缺陷的类别之间存在一些差异。然而,这种差异并不明显。这表明机器学*模型找不到模式 - 至少,找不到一个稳健的模式。

其他类型的数据 - 图片

第三章中,我们研究了图像数据,主要从存在什么类型的图像数据的视角。现在,我们将采取更实际的方法,介绍一种比仅使用文件更好的图像处理方式。

让我们看看图像数据在流行的存储库 Hugging Face 中是如何存储的。这个库有一个专门用于处理数据集的模块 - 便于称为Dataset。它可以通过pip install -q datasets命令安装。因此,让我们加载一个数据集,并使用以下代码片段可视化其中一张图片:

# importing the images library
from datasets import load_dataset, Image
# loading a dataset "food101", or more concretely it's split for training
dataset = load_dataset("food101", split="train")

现在,变量数据集包含了所有图片。嗯,不是全部 - 只包含数据集设计者指定的训练集部分(见代码片段的最后一行)。我们可以使用以下代码来可视化其中一张图片:

# visualizing the first image
dataset[0]["image"]

由于图像的版权未知,我们不会在这本书中可视化它们。然而,上一行将显示数据集中的第一张图像。我们还可以通过简单地输入 dataset 来查看该数据集中还有什么其他内容。我们将看到以下输出:

Dataset({ features: ['image', 'label'], num_rows: 75750 })

这意味着数据集包含两列——图像及其标签。它包含 75,750 个。让我们使用以下代码来看看这个数据集中标签的分布情况:

# we can also plot the histogram
# to check the distribution of labels in the dataset
import seaborn as sns
import matplotlib.pyplot as plt
plt.rcParams['figure.figsize'] = (20,10)
sns.histplot(data=dataset['label'], x=dataset['label'])

这为我们提供了一个漂亮的直方图,如图 图 6.12 所示:

图 6.12 – 标签分布的直方图。每列是带有适当标签的图像数量——0 到 100

图 6.12 – 标签分布的直方图。每列是带有适当标签的图像数量——0 到 100

此图表显示了图像类别,其中包含的图像数量大于其他类别——包含超过 2,000 个图像的类别。然而,如果不理解数据集,很难检查这些标签的含义。我们可以通过手动可视化图像来做到这一点。所以,这是我的下一个最佳实践。

最佳实践 #37

在可视化图像的元数据时,确保可视化图像本身。

我们必须记住通过绘制图像来可视化图像数据。我们需要确保我们知道标签的含义以及我们如何使用它们。

文本数据

对于文本数据,我们将使用相同的 Hugging Face hub 获取两种类型的数据——非结构化文本,正如我们在 第三章 中所做的那样,以及结构化数据——编程语言代码:

# import Hugging Face Dataset
from datasets import load_dataset
# load the dataset with text classification labels
dataset = load_dataset('imdb')

上述代码片段从 互联网电影数据库IMDb)加载电影评论数据集。我们可以通过使用与图像类似的接口来获取数据的示例:

# show the first example
dataset['train'][0]

我们可以使用类似的图表来可视化它:

# plot the distribution of the labels
sns.histplot(dataset['train']['label'], bins=2)

上述代码片段创建以下图表,显示正面和负面评论完全平衡:

图 6.13 – IMDb 电影数据库评论中的平衡类别

图 6.13 – IMDb 电影数据库评论中的平衡类别

我们可以在下一步对文本数据进行各种处理。然而,这种处理与特征提取相关,所以我们将在接下来的几章中讨论它。

在我们这样做之前,让我们先看看更接*软件工程领域的数据集——编程语言代码。我们在 第三章 中使用了类似的数据,所以让我们关注一下我们如何从 Hugging Face 获取更大的编程语言代码语料库。我们可以使用以下代码来获取数据并检查第一个程序:

# now, let us import the code to the text summarization dataset
dsCode = load_dataset('code_x_glue_ct_code_to_text', 'java', split='test')
# and see the first example of the code
dsCode[0]

此代码片段显示第一个程序,它已经分词并准备好进行进一步分析。所以,让我们看看这个数据集中标记的频率。我们可以使用以下代码来做这件事:

import pandas as pd
import matplotlib.pyplot as plt
# create a list of tokens
lstCodeLines = dsCode['code_tokens']
# flatten the list of lists to one list
lstCodeLines = [item for sublist in lstCodeLines for item in sublist]
#print the first elements of the list
print(lstCodeLines[:10])
dfCode = pd.DataFrame(lstCodeLines, columns=['token'])
# group the tokens and count the number of occurences
# which will help us to visualize the frequency of tokens in the next step
dfCodeCounts = dfCode.groupby('token').size().reset_index(name='counts')
# sort the counts by descending order
dfCodeCounts = dfCodeCounts.sort_values(by='counts', ascending=False)
fig, ax = plt.subplots(figsize=(12, 6))
# plot the frequency of tokens as a barplot
# for the simplicity, we only take the first 20 tokens
sns.barplot(x='token',
            y='counts',
            data=dfCodeCounts[:20],
            palette=sns.color_palette("BuGn_r", n_colors=20),
            ax=ax)
# rotate the x-axis labels to make sure that
# we see the full token names, i.e. lines of code
ax.set_xticklabels(ax.get_xticklabels(),
                   rotation=45,
                   horizontalalignment='right')

以下代码提取标记,计算它们,并创建前 20 个标记频率的图表。结果在图 6.14中展示:

图 6.14 – 代码数据集中前 20 个最常见标记的频率

图 6.14 – 代码数据集中前 20 个最常见标记的频率

有趣的是,我们可以观察到括号、逗号、分号和大括号是数据集中最常用的标记。这并不令人惊讶,因为这些字符在 Java 中具有特殊含义。

在前 20 个标记列表中的其他标记,不出所料,是 Java 中的关键字或具有特殊含义(如==)。

因此,我在本章的最后一条最佳实践是关于理解文本数据。

最佳实践 #38

文本数据的汇总统计有助于我们对数据进行合理性检查。

尽管文本数据在本质上相当无结构,但我们仍然可以可视化数据的一些属性。例如,标记频率分析可以揭示我们对数据的经验理解是否合理,以及我们是否可以信任它。

向特征工程迈进

在本章中,我们探讨了可视化数据的方法。我们学*了如何创建图表并识别数据中的依赖关系。我们还学*了如何使用降维技术将多维数据绘制在二维图表上。

在接下来的几章中,我们将深入研究不同类型数据的特征工程。有时,将特征工程与数据提取混淆是很常见的。在实践中,区分这两者并不那么困难。

提取的数据是通过应用某种测量仪器收集的数据。原始文本或图像是这类数据的良好例子。提取的数据接*数据来源的领域——或者它是如何被测量的。

特征基于我们想要执行的分析来描述数据——它们更接*我们想要对数据进行什么操作。它们更接*我们想要实现的目标以及我们想要进行的机器学*分析形式。

参考文献

  • 国际标准化组织,国际计量学基本和通用术语词汇(VIM)。在国际组织。2004 年第 09-14 页。

  • Alhusain, S. 预测面向对象度量指标的相对阈值。在 2021 IEEE/ACM 国际技术债务会议(TechDebt)。 2021 年 IEEE 出版。

  • Feldt, R. 等。支持软件决策会议:用于可视化测试和代码度量的热图。在 2013 年第 39 届 Euromicro 软件工程和高级应用会议。 2013 年 IEEE 出版。

  • Staron, M. 等。在三家公司的案例研究中测量和可视化代码稳定性。在 2013 年第 23 届国际软件度量研讨会和第 8 届国际软件过程和产品度量会议联合会议。 2013 年 IEEE 出版。

  • 文,S.,C. 尼尔森,和 M. 斯塔隆。评估引擎控制软件的发布准备情况。载于《第 1 届国际软件质量及其 依赖性研讨会论文集》。2018 年。

第七章:数值和图像数据的特征工程

在大多数情况下,当我们设计大规模机器学*系统时,我们得到的数据类型需要比仅仅可视化更多的处理。这种可视化仅用于机器学*系统的设计和开发。在部署期间,我们可以监控数据,正如我们在前几章中讨论的那样,但我们需要确保我们使用优化的数据来进行推理。

因此,在本章中,我们将专注于特征工程——找到描述我们的数据更接*问题域而不是数据本身的正确特征。特征工程是一个从原始数据中提取和转换变量的过程,以便我们可以使用它们进行预测、分类和其他机器学*任务。特征工程的目标是分析和准备数据,以便用于不同的机器学*任务,如预测或分类。

在本章中,我们将专注于数值和图像数据的特征工程过程。我们将从介绍典型的方法开始,例如我们之前用于可视化的主成分分析PCA)。然后,我们将介绍更高级的方法,例如t-学生分布随机网络嵌入t-SNE)和独立成分分析ICA)。最终,我们将使用自编码器作为数值和图像数据的降维技术。

在本章中,我们将涵盖以下主题:

  • 特征工程过程的基本原理

  • PCA 和类似方法

  • 用于数值和图像数据的自编码器

特征工程

特征工程是将原始数据转换为可用于机器学*算法的数值的过程。例如,我们可以将有关软件缺陷的原始数据(例如,它们的描述、它们所属模块的特征等)转换成我们可以用于机器学*的数值表。正如我们在上一章中看到的,原始数值是我们对作为数据来源的实体进行量化的结果。它们是应用测量仪器到数据的结果。因此,根据定义,它们更接*问题域而不是解决方案域。

另一方面,特征量化了原始数据,并且只包含对当前机器学*任务重要的信息。我们使用这些特征来确保我们在训练期间找到数据中的模式,然后可以在部署期间使用这些模式。如果我们从测量理论的角度来看这个过程,这个过程改变了数据的抽象级别。如果我们从统计学的角度来看这个过程,这是一个去除噪声和降低数据维度的过程。

在本章中,我们将重点关注使用诸如自动编码器等高级方法来降低数据维度和去噪图像数据的过程。

图 7**.1 展示了特征提取在典型机器学*流程中的位置。这个流程在第二章中介绍过:

图 7.1 – 典型机器学*流程中的特征工程

图 7.1 – 典型机器学*流程中的特征工程

此图显示特征尽可能接*干净和验证过的数据,因此我们需要依赖前几章中的技术来可视化数据并减少噪声。特征工程之后的下一个活动是建模数据,如图 7**.2 所示。此图展示了整个流程的某种简化视图。这也在第二章中介绍过:

图 7.2 – 典型机器学*流程。来自第二章的某种简化视图

图 7.2 – 典型机器学*流程。来自第二章的某种简化视图

我们之前已经讨论过建模,所以让我们更深入地探讨特征提取过程。由于数值数据和图像数据从这个角度来看有些相似,所以我们将在本章一起讨论它们。文本数据是不同的,因此我们将在下一章中专门讨论它。

然而,本章我的第一个最佳实践与特征提取和模型之间的联系相关。

最佳实践 #39

如果数据复杂但任务简单,例如创建一个分类模型,请使用特征工程技术。

如果数据复杂且任务复杂,尝试使用复杂但功能强大的模型,例如本书后面介绍的变压器模型。这类任务的例子可以是当模型完成了一个程序员开始编写的程序的一部分时进行代码补全。简化复杂数据以适应更简单的模型,可以使我们增加训练模型的可解释性,因为我们作为人工智能工程师,在数据整理过程中更加参与。

数值数据的特征工程

我们将通过使用之前用于可视化数据的技术来介绍数值数据的特征工程 – 主成分分析(PCA)。

PCA

PCA 用于将一组变量转换成相互独立的部分。第一个部分应该解释数据的变异性或与大多数变量相关。图 7**.3 说明了这种转换:

图 7.3 – 从二维到二维的 PCA 变换的图形说明

图 7.3 – 从二维到二维的 PCA 变换的图形说明

这个图包含两个轴——蓝色的轴是原始坐标轴,橙色的轴是想象中的轴,为主成分提供坐标。转换不会改变xy轴的值,而是找到这样的转换,使得轴与数据点对齐。在这里,我们可以看到转换后的Y轴比原始的Y轴更好地与数据点对齐。

现在,让我们执行一些代码,这些代码可以读取数据并执行这种 PCA 转换。在这个例子中,数据有六个维度——也就是说,六个变量:

# read the file with data using openpyxl
import pandas as pd
# we read the data from the excel file,
# which is the defect data from the ant 1.3 system
dfDataAnt13 = pd.read_excel('./chapter_6_dataset_numerical.
              xlsx',sheet_name='ant_1_3', index_col=0)
dfDataAnt13

上述代码片段读取数据并显示它有六个维度。现在,让我们创建 PCA 转换。首先,我们必须从我们的数据集中移除依赖变量Defect

# let's remove the defect column, as this is the one that
# we could potentially predict
dfDataAnt13Pred = dfDataAnt13.drop(['Defect'], axis = 1)

然后,我们必须导入 PCA 转换并执行它。我们希望从五个变量(减去Defect变量后的六个变量)转换到三个维度。维度的数量完全是任意的,但因为我们之前章节中使用了两个维度,所以这次让我们使用更多:

# now, let's import PCA and find a few components
from sklearn.decomposition import PCA
# previously, we used 2 components, now, let's go with
# three
pca = PCA(n_components=3)
# now, the transformation to the new components
dfDataAnt13PCA = pca.fit_transform(dfDataAnt13Pred)
# and printing the resulting array
# or at least the three first elements
dfDataAnt13PCA[:3]

结果的 DataFrame——dfDataAnt13PCA——包含了转换后变量的值。它们尽可能独立于彼此(线性独立)。

我想强调一下我们如何处理这类数据转换的一般方案,因为这是一种相对标准的做事方式。

首先,我们实例化转换模块并提供参数。在大多数情况下,参数很多,但有一个参数n_components,它描述了我们希望有多少个组件。

第二,我们使用fit_transform()函数来训练分类器并将其转换成这些组件。我们使用这两个操作一起,仅仅是因为这些转换是针对特定数据的。没有必要在一个数据上训练转换,然后应用到另一个数据上。

我们可以用 PCA 做的,而其他类型的转换做不到的是,检查每个组件解释了多少变异性——也就是说,组件与数据对齐得有多好。我们可以用以下代码来做这件事:

# and let's visualize that using the seaborn library
import seaborn as sns
sns.set(rc={"figure.figsize":(8, 8)})
sns.set_style("white")
sns.set_palette('rocket')
sns.barplot(x=['PC 1', 'PC 2', 'PC 3'], y=pca.explained_variance_ratio_)

这段代码片段产生了图 7**.4所示的图表:

图 7.4 – 主成分解释的变异性

图 7.4 – 主成分解释的变异性

这个图显示第一个组件是最重要的——也就是说,它解释了最大的变异性。这种变异性可以看作是数据包含的信息量。在这个数据集的例子中,第一个组件解释了大约 80%的变异性,第二个组件几乎解释了 20%。这意味着我们的数据集有一个主导维度,以及数据在第二个维度上的分散。第三个维度几乎不存在。

这就是我的下一个最佳实践所在。

最佳实践 #40

如果数据在某种程度上是线性可分的,并且处于相似的比例,请使用 PCA。

如果数据是线性的,或者多线性的,PCA 对于训练模型有很大的帮助。然而,如果数据不是线性的,请使用更复杂的模型,例如 t-SNE。

t-SNE

作为一种转换,PCA 在数据在某种程度上线性可分时工作得很好。在实践中,这意味着坐标系可以定位得使大部分数据位于其轴之一上。然而,并非所有数据都如此。一个这样的数据例子是可以被可视化为圆的数据 – 它在两个轴上均匀分布。

为了降低非线性数据的维度,我们可以使用另一种技术 – t-SNE。这种降维技术基于提取一个神经网络的激活值,该神经网络被训练以拟合输入数据。

以下代码片段创建了对数据进行 t-SNE 转换。它遵循了之前描述的 PCA 的相同架构,并且也将维度降低到三个:

# for t-SNE, we use the same data as we used previously
# i.e., the predictor dfDataAnt13Pred
from sklearn.manifold import TSNE
# we create the t-sne transformation with three components
# just like we did with the PCA
tsne = TSNE(n_components = 3)
# we fit and transform the data
dfDataAnt13TSNE = tsne.fit_transform(dfDataAnt13Pred)
# and print the three first rows
dfDataAnt13TSNE[:3]

生成的 DataFrame – dfDataAnt13TSNE – 包含了转换后的数据。不幸的是,t-SNE 转换不允许我们获取解释变异性的值,因为这种概念对于这种转换来说并不存在。然而,我们可以可视化它。以下图展示了三个成分的 3D 投影:

图 7.5 – t-SNE 成分的可视化。绿色点代表无缺陷成分,红色点代表有缺陷的成分

图 7.5 – t-SNE 成分的可视化。绿色点代表无缺陷成分,红色点代表有缺陷的成分

这是我在本章中的下一个最佳实践。

最佳实践 #41

如果你对数据的属性不了解,并且数据集很大(超过 1,000 个数据点),请使用 t-SNE。

t-SNE 是一个非常好且稳健的转换。它特别适用于大型数据集 – 即那些包含数百个数据点的数据集。然而,一个挑战是,t-SNE 提供的成分没有解释。我们还应该知道,t-SNE 的最佳结果需要仔细调整超参数。

ICA

我们还可以使用另一种类型的转换 – ICA。这种转换以这种方式工作,即它找到最不相关的数据点并将它们分离。它在历史上被用于医疗领域,以从高频 脑电图EEG)信号中去除干扰和伪影。这种干扰的一个例子是 50 - Hz 的电力信号。

然而,它可以用于任何类型的数据。以下代码片段说明了如何使用 ICA 对我们在之前转换中使用过的相同数据集进行处理:

# we import the package
from sklearn.decomposition import FastICA
# instantiate the ICA
ica = FastICA(n_components=3)
# transform the data
dfDataAnt13ICA = ica.fit_transform(dfDataAnt13Pred)
# and check the first three rows
dfDataAnt13ICA[:3]

ICA 需要产生比原始数据更少的组件,尽管在前面的代码片段中我们只使用了三个。以下图示展示了这些组件的可视化:

图 7.6 – 使用 ICA 转换的数据集的可视化

图 7.6 – 使用 ICA 转换的数据集的可视化

图 7.6 中,绿色组件是没有缺陷的,而红色组件含有缺陷。

局部线性嵌入

一种介于 t-SNE 和 PCA(或 ICA)之间的技术被称为局部线性嵌入LLE)。这种技术假设相邻节点在某种虚拟平面上彼此靠*。算法以这种方式训练一个神经网络,即它保留了相邻节点之间的距离。

以下代码片段说明了如何使用 LLE 技术:

from sklearn.manifold import LocallyLinearEmbedding
# instantiate the classifier
lle = LocallyLinearEmbedding(n_components=3)
# transform the data
dfDataAnt13LLE = lle.fit_transform(dfDataAnt13Pred)
# print the three first rows
dfDataAnt13LLE[:3]

这个片段的结果与之前算法的 DataFrame 相似。以下是可视化:

图 7.7 – LLE 组件的可视化

图 7.7 – LLE 组件的可视化

我们迄今为止讨论的所有技术都是灵活的,允许我们指明在转换后的数据中需要多少个组件。然而,有时问题在于我们不知道需要多少个组件。

线性判别分析

线性判别分析LDA)是一种技术,其结果与我们的数据集中拥有的组件数量相同。这意味着我们数据集中的列数与 LDA 提供的组件数相同。这反过来又意味着我们需要定义一个变量作为算法的依赖变量。

LDA 算法以这种方式在低维空间中对数据集进行投影,使得它能够将数据分离到依赖变量的类别中。因此,我们需要一个。以下代码片段说明了在数据集上使用 LDA 的方法:

from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
# create the classifier
# please note that we can only use one component, because
# we have only one predicted variable
lda = LinearDiscriminantAnalysis(n_components=1)
# fit to the data
# please note that this transformation requires the predicted
# variable too
dfDataAnt13LDA = lda.fit(dfDataAnt13Pred, dfDataAnt13.Defect).transform(dfDataAnt13Pred)
# print the transformed data
dfDataAnt13LDA[:3]

结果 DataFrame 只包含一个组件,因为我们数据集中只有一个依赖变量。

自动编码器

*年来,一种新的特征提取技术越来越受欢迎——自动编码器。自动编码器是一种特殊的神经网络,旨在将一种类型的数据转换成另一种类型的数据。通常,它们被用来以略微修改的形式重建输入数据。例如,它们可以用来去除图像中的噪声或将图像转换为使用不同画笔风格的图像。

自动编码器非常通用,可以用于其他类型的数据,我们将在本章的剩余部分学*这些内容(例如,用于图像数据)。图 7.8展示了自动编码器的概念模型。它由两部分组成——编码器和解码器。编码器的作用是将输入数据——在这个例子中是一个图像——转换成抽象表示。这种抽象表示存储在特定的层(或几层),称为瓶颈。瓶颈的作用是存储允许解码器重建数据的输入数据的属性。解码器的作用是从瓶颈层获取数据的抽象表示,并尽可能好地重建输入数据:

图 7.8 – 自动编码器的概念可视化。这里,输入数据是图像的形式

图 7.8 – 自动编码器的概念可视化。这里,输入数据是图像的形式

由于自动编码器被训练以尽可能好地重建数据,瓶颈值通常被认为是对输入数据的良好内部表示。这种表示如此之好,以至于它允许我们区分不同的输入数据点。

瓶颈值也非常灵活。与之前介绍的技术不同,我们没有限制可以提取多少特征。如果我们需要,我们甚至可以提取比我们数据集中列数更多的特征,尽管这样做没有太多意义。

因此,让我们构建一个用于从设计用来学*缺陷数据表示的自动编码器中提取特征的管道:

下面的代码片段展示了如何读取数据集并从中移除有缺陷的列:

# read the file with data using openpyxl
import pandas as pd
# we read the data from the excel file,
# which is the defect data from the ant 1.3 system
dfDataAnt13 = pd.read_excel('./chapter_6_dataset_numerical.
              xlsx',sheet_name='ant_1_3',index_col=0)
# let's remove the defect column, as this is the one that we could
# potentially predict
X = dfDataAnt13.drop(['Defect'], axis = 1)
y = dfDataAnt13.Defect

除了移除列之外,我们还需要对数据进行缩放,以便自动编码器有很好的机会识别所有列中的小模式:

# split into train test sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=1)
# scale data
t = MinMaxScaler()
t.fit(X_train)
X_train = t.transform(X_train)
X_test = t.transform(X_test)

现在,我们可以创建我们的自动编码器的编码器部分,它将在下面的代码片段中展示:

# number of input columns
n_inputs = X.shape[1]
# the first layer - the visible one
visible = Input(shape=(n_inputs,))
# encoder level 1
e = Dense(n_inputs*2)(visible)
e = BatchNormalization()(e)
e = LeakyReLU()(e)
# encoder level 2
e = Dense(n_inputs)(e)
e = BatchNormalization()(e)
e = LeakyReLU()(e)

之前的代码创建了自动编码器的两个级别,因为我们的数据相当简单。现在,有趣的部分是瓶颈,可以通过运行以下代码来创建:

n_bottleneck = 3
bottleneck = Dense(n_bottleneck)(e)

在我们的案例中,瓶颈非常窄——只有三个神经元——因为数据集相对较小,且并不复杂。在下一部分,当我们使用自动编码器处理图像时,我们将看到瓶颈可以更宽。一般的思想是,更宽的瓶颈允许我们捕捉数据中的更复杂依赖关系。例如,对于彩色图像,我们需要更多的神经元,因为我们需要捕捉颜色,而对于灰度图像,我们需要更窄的瓶颈。

最后,我们可以使用以下代码创建自动编码器的解码器部分:

# define decoder, level 1
d = Dense(n_inputs)(bottleneck)
d = BatchNormalization()(d)
d = LeakyReLU()(d)
# decoder level 2
d = Dense(n_inputs*2)(d)
d = BatchNormalization()(d)
d = LeakyReLU()(d)
# output layer
output = Dense(n_inputs, activation='linear')(d)

构造过程的最后一部分是将这三个部分放在一起——编码器、瓶颈和解码器。我们可以使用以下代码来完成这项工作:

# we place both of these into one model
# define autoencoder model
model = Model(inputs=visible, outputs=output)
# compile autoencoder model
model.compile(optimizer='adam', loss='mse')

到目前为止,我们已经构建了我们的自动编码器。我们已经定义了其层和瓶颈。现在,自动编码器必须被训练以理解如何表示我们的数据。我们可以使用以下代码来完成:

# we train the autoencoder model
history = model.fit(X_train, X_train,
                    epochs=100,
                    batch_size=16,
                    verbose=2,
                    validation_data=(X_test,X_test))

请注意,我们使用相同的数据作为输入和验证,因为我们需要训练编码器尽可能准确地重新创建相同的数据,考虑到瓶颈层的大小。在训练编码器模型后,我们可以使用它来从模型中提取瓶颈层的值。我们可以通过定义一个子模型并使用它作为输入数据来完成:

submodel = Model(model.inputs, model.get_layer("dense_8").output)
# this is the actual feature extraction -
# where we make prediction for the train dataset
# please note that the autoencoder requires a two dimensional array
# so we need to take one datapoint and make it into a two dimensional array
# with only one row
results = submodel.predict(np.array([X_train[0]]))
results[0]

执行此代码的结果是一个包含三个值的向量——自动编码器的瓶颈值。

我在本章中的下一个最佳实践与自动编码器的使用相关。

最佳实践 #42

当数据集非常大时,使用自动编码器对数值数据进行处理,因为自动编码器复杂且需要大量数据进行训练。

由于特征的质量是自动编码器训练效果的一个函数,我们需要确保训练数据集足够大。因此,自动编码器通常用于图像数据。

图像数据的特征工程

对于图像数据,最突出的特征提取方法之一是使用卷积神经网络CNNs)并从这些网络中提取嵌入。*年来,引入了这种类型神经网络的一种新类型——自动编码器。虽然我们可以使用自动编码器处理各种数据,但它们特别适合图像。因此,让我们为 MNIST 数据集构建一个自动编码器,并从中提取瓶颈层的值。

首先,我们需要使用以下代码片段下载 MNIST 数据集:

# first, let's read the image data from the Keras library
from tensorflow.keras.datasets import mnist
# and load it with the pre-defined train/test splits
(X_train, y_train), (X_test, y_test) = mnist.load_data()
X_train = X_train/255.0
X_test = X_test/255.0

现在,我们可以使用以下代码构建编码器部分。请注意,编码器部分有一个额外的层。该层的目的是将二维图像转换为一维输入数组——即展平图像:

# image size is 28 pixels
n_inputs = 28
# the first layer - the visible one
visible = Input(shape=(n_inputs,n_inputs,))
# encoder level 1
e = Flatten(input_shape = (28, 28))(visible)
e = LeakyReLU()(e)
e = Dense(n_inputs*2)(e)
e = BatchNormalization()(e)
e = LeakyReLU()(e)
# encoder level 2
e = Dense(n_inputs)(e)
e = BatchNormalization()(e)
e = LeakyReLU()(e)

现在,我们可以构建我们的瓶颈层。在这种情况下,瓶颈层可以更宽,因为图像比我们之前在自动编码器中使用的模块数值数组更复杂(而且数量更多):

n_bottleneck = 32
bottleneck = Dense(n_bottleneck)(e)

解码器部分与之前的例子非常相似,但有一个额外的层,该层可以从其扁平表示中重新创建图像:

# and now, we define the decoder part
# define decoder, level 1
d = Dense(n_inputs)(bottleneck)
d = BatchNormalization()(d)
d = LeakyReLU()(d)
# decoder level 2
d = Dense(n_inputs*2)(d)
d = BatchNormalization()(d)
d = LeakyReLU()(d)
# output layer
d = Dense(n_inputs*n_inputs, activation='linear')(d)
output = Reshape((28,28))(d)

现在,我们可以编译和训练自动编码器:

# we place both of these into one model
# define autoencoder model
model = Model(inputs=visible, outputs=output)
# compile autoencoder model
model.compile(optimizer='adam', loss='mse')
# we train the autoencoder model
history = model.fit(X_train, X_train,
                    epochs=100,
                    batch_size=16,
                    verbose=2,
                    validation_data=(X_test,X_test))

最后,我们可以从模型中提取瓶颈层的值:

submodel = Model(model.inputs, bottleneck)
# this is the actual feature extraction -
# where we make prediction for the train dataset
# please note that the autoencoder requires a two dimensional array
# so we need to take one datapoint and make it into a two dimensional array
# with only one row
results = submodel.predict(np.array([X_train[0]]))
results[0]

现在,结果值的数组要大得多——它有 32 个值,与我们瓶颈层中的神经元数量相同。

瓶颈层中的神经元数量基本上是任意的。以下是选择神经元数量的最佳实践。

最佳实践 #43

在瓶颈层开始时使用较少的神经元数量——通常是列数的三分之一。如果自动编码器没有学*,可以逐渐增加数量。

我选择 1/3 的列数并没有具体的原因,只是基于经验。你可以从相反的方向开始——将瓶颈层做得和输入层一样宽——然后逐渐减小。然而,拥有与列数相同数量的特征并不是我们最初使用特征提取的原因。

摘要

在本章中,我们的重点是特征提取技术。我们探讨了如何使用降维技术和自动编码器来减少特征数量,以便使机器学*模型更加有效。

然而,数值和图像数据只是数据类型中的两个例子。在下一章中,我们将继续介绍特征工程方法,但对于文本数据,这在当代软件工程中更为常见。

参考文献

  • Zheng, A. 和 A. Casari,机器学*特征工程:数据科学家原理与技术。2018 年:O’Reilly 媒体公司

  • Heaton, J. 对预测建模中特征工程的经验分析。在 2016 年东南会议. 2016 年,IEEE。

  • Staron, M. 和 W. Meding,软件开发度量计划。Springer。https://doi.org/10.1007/978-3-319-91836-5. 第 10 卷. 2018 年,3281333.

  • Abran, A.,软件度量与软件计量学。2010 年:John Wiley & Sons。

  • Meng, Q.,等人。关系自动编码器用于特征提取。在 2017 年国际神经网络联合会议(IJCNN)。 2017 年,IEEE。

  • Masci, J.,等人。用于层次特征提取的堆叠卷积自动编码器。在人工神经网络与机器学*,ICANN 2011:第 21 届国际人工神经网络会议,芬兰埃斯波,2011 年 6 月 14-17 日,第 21 卷. 2011 年,Springer。

  • Rumelhart, D.E.,G.E. Hinton,和 R.J. Williams,通过反向传播错误学*表示。自然,1986 年,323(6088): p. 533-536.

  • Mosin, V.,等人,比较基于自动编码器的高速公路驾驶场景图像异常检测方法。SN 应用科学,2022 年,4(12): p. 334.

第八章:自然语言数据的特征工程

在上一章中,我们探讨了如何从数值数据和图像中提取特征,并探讨了用于此目的的几个算法。在本章中,我们将继续探讨用于从自然语言数据中提取特征的算法。

自然语言是软件工程中的一种特殊数据源。随着 GitHub Copilot 和 ChatGPT 的引入,变得明显的是,用于软件工程任务的机器学*和人工智能工具不再是科幻。因此,在本章中,我们将探讨使这些技术变得如此强大的第一步——从自然语言数据中提取特征。

在本章中,我们将涵盖以下主题:

  • 标记化器及其在特征提取中的作用

  • 词袋作为处理自然语言数据的一种简单技术

  • 作为更高级方法,词嵌入可以捕捉上下文

软件工程中的自然语言数据和 GitHub Copilot 的兴起

编程一直是科学、工程和创造力的结合。创建新的程序和能够指导计算机执行某些操作一直是被认为值得付费的事情——这就是所有程序员谋生的手段。人们尝试过自动化编程和支持较小的任务——例如,为程序员提供如何使用特定函数或库方法的建议。

然而,优秀的程序员可以编写出持久且易于他人阅读的程序。他们还可以编写出长期无需维护的可靠程序。最好的程序员是那些能够解决非常困难任务并遵循软件工程原则和最佳实践的程序员。

在 2020 年,发生了某件事情——GitHub Copilot 登上了舞台,并展示了基于大型语言模型LLMs)的自动化工具不仅能提供简单的函数调用建议,还能提供更多。它已经证明,这些语言模型能够提供整个解决方案和算法的建议,甚至能够解决编程竞赛。这为程序员开辟了全新的可能性——最优秀的程序员变得极其高效,并得到了允许他们专注于编程任务复杂部分的工具。简单的任务现在由 GitHub Copilot 和其他工具解决。

这些工具之所以如此出色,是因为它们基于 LLMs,能够找到并量化程序的上下文。就像一位伟大的棋手可以提前预见几步棋一样,这些工具可以提前预见程序员可能需要什么,并提供有用的建议。

有一些简单的技巧使得这些工具如此有效,其中之一就是特征工程。自然语言任务(包括编程)的特征工程是一个将文本片段转换为数字向量(或矩阵)的过程。这些向量可以是简单的——例如,量化标记——也可以是非常复杂的——例如,找到与其他任务相关联的原子文本片段。我们将在本章中探讨这些技术。我们将从稍微重复一下第三章和第五章中看到的词袋技术(见第三章第五章)开始。我们不需要重复整个代码,但我们确实需要提供一个小的总结来理解这些方法的局限性。然而,这是我选择是否需要分词器或嵌入的最佳实践。

最佳实践 #44

对于 LLM(如 BERT)使用分词器,对于简单任务使用词嵌入。

对于简单的任务,例如文本的基本分词用于情感分析或快速理解文本中的依赖关系,我通常使用词嵌入。然而,当与 LLM(如 BERT、RoBERTa 或 AlBERT)一起工作时,我通常使用不同的分词器,因为这些模型在自身寻找依赖关系方面非常出色。然而,在设计分类器时,我使用词嵌入,因为它们提供了一种快速创建与“经典”机器学*算法兼容的特征向量的方法。

选择分词器需要根据任务来决定。我们将在本章中更详细地探讨这个问题,但这个主题本身可能需要一整本书来阐述。例如,对于需要关于词性(或者在很多情况下,程序抽象语法树的某一部分)的信息的任务,我们需要使用专门设计来捕获这些信息的分词器——例如,从编程语言解析器中获取。这些分词器为模型提供了更多信息,但它们对数据的要求也更高——基于抽象语法树的分词器要求程序具有良好的格式。

分词器是什么以及它做什么

特征工程文本数据的第一个步骤是决定文本的分词。文本分词是一个提取能够捕捉文本意义而不包含太多额外细节的词的部分的过程。

有不同的方法来提取标记,我们将在本章中探讨这些方法,但为了说明提取标记的问题,让我们看看一个可以采取不同形式的单词——print。这个单词本身可以是一个标记,但它可以有不同的形式,如printingprintedprinterprintsimprinted等等。如果我们使用简单的分词器,这些单词中的每一个都将是一个标记——这意味着有很多标记。然而,所有这些标记都捕捉到与打印相关的某种意义,所以可能我们不需要这么多。

这就是分词器发挥作用的地方。在这里,我们可以决定如何处理这些不同的词形。我们可以只取主要部分——print——然后所有其他形式都会被计为那样,所以imprintedprinting都会被计为print。这减少了标记的数量,但我们也减少了特征向量的表达性——一些信息丢失了,因为我们没有相同数量的标记可以使用。我们可以预先设计一组标记——也就是说,使用printimprint来区分不同的上下文。我们也可以使用双词(两个词一起)作为标记(例如,is_goingisgoing——第一个需要两个词以特定顺序出现,而第二个允许它们出现在两个不同的序列中),或者我们可以添加关于单词是否为句子中主语对象的信息。

词袋和简单分词器

第三章第五章中,我们看到了词袋特征提取技术的应用。这种技术对文本进行计数,统计标记的数量,在第三章第五章中是单词。它简单且计算效率高,但有几个问题。

当实例化词袋分词器时,我们可以使用几个参数,这些参数会强烈影响结果,就像我们在前几章的代码片段中所做的那样:

# create the feature extractor, i.e., BOW vectorizer
# please note the argument - max_features
# this argument says that we only want three features
# this will illustrate that we can get problems - e.g. noise
# when using too few features
vectorizer = CountVectorizer(max_features = 3)

max_features参数是一个截止值,它减少了特征的数量,但它也可能在两个(或更多)不同句子具有相同特征向量时引入噪声(我们在第二章中看到了这样一个句子的例子)。由于我们已经讨论了噪声及其相关问题,我们可能会倾向于使用其他参数——max_dfmin_df。这两个参数决定了单词在文档中应该出现多少次才能被认为是标记。过于罕见的标记(min_df)可能导致稀疏矩阵——特征矩阵中有许多 0——但它们可以在数据点之间提供很好的区分。也许这些罕见的单词正是我们所寻找的。另一个参数(max_df)导致更密集的特征矩阵,但它们可能无法完全区分数据点。这意味着选择这些参数并不简单——我们需要实验,并使用机器学*模型训练(和验证)来找到正确的向量。

除此之外,还有一种方法——我们可以执行递归搜索以找到这样一个特征向量,它能够区分所有数据点而不会添加太多噪声。我的团队已经尝试过这样的算法,这些算法在模型训练和验证方面表现出色,但计算成本非常高。这种算法在图 8.1中展示:

图 8.1 – 一种在文本文件中找到一组能够区分所有数据点的特征算法。流程已被简化以说明主要点

图 8.1 – 一个用于在文本文件中找到一组能够区分所有数据点的特征的算法。流程已被简化以说明主要观点

该算法通过添加新标记来工作,如果数据点具有与之前任何数据点相同的特征向量。它首先从第一行取第一个标记,然后是第二行。如果标记可以区分这两行,则继续到第三行。一旦算法发现两行具有不同的特征向量,它就会找出是否存在可以区分这些行的标记,并将其添加到特征集。它继续添加,直到没有新标记可以添加或所有行都已分析。

此算法保证找到最佳区分分析数据集的标记集。然而,它有一个很大的缺点——它很慢(因为它必须从找到/需要新标记的第一行开始)。生成的特征矩阵也不是最优的——它包含很多 0,因为大多数标记只能在一行中找到。反过来,特征矩阵可能比实际原始数据集大得多。

这就是我的下一个最佳实践发挥作用的地方。

最佳实践 #45

当你的任务需要预定义的单词集时,请使用词袋模型分词器并结合字典。

在分析编程语言代码时,我经常使用词袋模型分词器。我使用编程语言中预定义的关键词集来增强分词器,然后使用标准的CountVectorizer。这使我能够控制我感兴趣的部分词汇量——关键词——并允许分词器适应文本。

WordPiece 分词器

从文本文档中分词和提取特征的一个更好的方法是使用 WordPiece 分词器。这种分词器以这样的方式工作,即它找到它可以区分的最常见的文本片段,以及最常见的那些。这种类型的分词器需要训练——也就是说,我们需要提供一组代表性文本以获得正确的词汇(标记)。

让我们来看一个例子,我们使用一个简单的程序,一个开源项目中的模块,来训练这样的分词器,然后将这个分词器应用于著名的“Hello World”C 语言程序。让我们首先创建分词器:

from tokenizers import BertWordPieceTokenizer
# initialize the actual tokenizer
tokenizer = BertWordPieceTokenizer(
    clean_text=True,
    handle_chinese_chars=False,
    strip_accents=False,
    lowercase=True
)

在这个例子中,我们使用 Hugging Face 库中的 WordPiece 分词器,特别是为与 BERT 等 LLM 一起工作而准备的分词器。我们可以使用几个参数,但让我们只使用显示我们只对小写字母感兴趣;我们不希望处理中文字符,并希望从头开始。

现在,我们需要找到一个可以用来训练分词器的文本片段。在这个例子中,我将使用开源项目中的一个文件 – AzureOS NetX。它是一个用 C 语言编写的组件,用于处理互联网 HTTP 协议的部分。我们创建一个新的变量 – path – 并将文件的路径添加到那里。一旦我们准备好了文本,我们就可以训练分词器:

# and train the tokenizer based on the text
tokenizer.train(files=paths,
                vocab_size=30_000,
                min_frequency=1,
                limit_alphabet=1000,
                wordpieces_prefix='##',
                special_tokens=['[PAD', '[UNK]', '[CLS]', '[SEP]', '[MASK]'])

我们已经将分词器设置成与之前示例中的CountVectorizer相似的一组参数。这个前代码片段找到了最常见的单词片段并将它们用作标记。

我们可以通过tokenizer.get_vocab()语句获取标记列表,这将产生一个长的标记字典。以下是前几个标记:

'##ll': 183,
'disable': 326,
'al': 263,
'##cket': 90,
'##s': 65,
'computed': 484

第一个标记是单词的一部分,这通过它开头有两个井号的事实来表示。这个标记在词汇表中映射到数字183。这种映射很重要,因为数字在后续的机器学*模型中会被使用。

另一个有趣的观察是,一些标记,如'disable',不是单词的一部分,而是整个单词。这意味着这个标记在任何地方都没有作为单词的一部分出现,并且它不包含词汇表中其他单词的任何部分。

一旦我们训练了 WordPiece 分词器,我们可以检查分词器如何从一个简单的 C 程序中提取特征:

strCProgram = '''
int main(int argc, void **argc)
{
  printf("%s", "Hello World\n");
  return 0;
}
'''
# now, let's see how the tokenizer works
# we invoke it based on the program above
tokenizedText = tokenizer.encode(strCProgram)
tokenizedText.tokens

前面的代码片段对程序进行了分词。结果是以下标记列表(只显示了 50 个标记中的前 10 个):

'in', '##t', 'ma', '##in', '(', 'in', '##t', 'a', '##r', '##g'

第一行,以int标记开始,已经被以下方式分词。第一个单词 – int – 被分割成两个标记:"in""##t"。这是因为这两个部分被用于训练程序中。我们还可以看到第二个标记 – main – 被分割成两个标记:"ma""##in"。这些标记的 ID 如下:

110, 57, 272, 104, 10, 110, 57, 30, 61, 63

这意味着这个数字列表是我们简单 C 程序的特征向量。

WordPiece 分词非常有效,但它很大程度上依赖于训练数据。如果我们使用与分词文本非常不同的训练数据,标记集将不会很有帮助。因此,我的下一个最佳实践是关于训练这个分词器。

最佳实践 #46

将 WordPiece 分词器作为首选。

我通常将这个分词器作为首选。它相对灵活但相当快速。它允许我们捕获一个词汇表,大多数时候都能完成任务,并且不需要很多设置。对于具有直接语言和明确定义词汇的简单任务,传统的词级分词或其他子词分词方法,如字节对编码BPE)可能就足够了。WordPiece 分词可能会由于引入子词标记而增加输入数据的大小。这可能会影响内存和计算需求。

BPE

文本标记化的一个更高级的方法是 BPE 算法。这个算法基于与 20 世纪 90 年代由 Gage 创建的压缩算法相同的原理。该算法通过压缩数据中未使用的字节来压缩一系列字节。BPE 标记化程序做的是类似的事情,只不过它用未在文本中使用的新的字节替换了一系列标记。这样,该算法可以创建比CountVectorizer和 WordPiece 标记化程序更大的词汇表。BPE 因其处理大型词汇表的能力和通过 fastBPE 库的高效实现而非常受欢迎。

让我们探讨如何将这个标记化程序应用于相同的数据,并检查与前两种方法的差异。以下代码片段展示了如何从 Hugging Face 库中实例化这个标记化程序:

# in this example we use the tokenizers
# from the HuggingFace library
from tokenizers import Tokenizer
from tokenizers.models import BPE
# we instantiate the tokenizer
tokenizer = Tokenizer(BPE(unk_token="[UNK]"))

这个标记化程序需要训练,因为它需要找到最优的标记对集合。因此,我们需要实例化一个训练类并对其进行训练。以下代码片段正是这样做的:

from tokenizers.trainers import BpeTrainer
# here we instantiate the trainer
# which is a specific class that will manage
# the training process of the tokenizer
trainer = BpeTrainer(special_tokens=["[UNK]", "[CLS]",
                     "[SEP]", "[PAD]", "[MASK]"])
from tokenizers.pre_tokenizers import Whitespace
tokenizer.pre_tokenizer = Whitespace()
# now, we need to prepare a dataset
# in our case, let's just read a dataset that is a code of a program
# in this example, I use the file from an open-source component - Azure NetX
# the actual part is not that important, as long as we have a set of
# tokens that we want to analyze
paths = ['/content/drive/MyDrive/ds/cs_dos/nx_icmp_checksum_compute.c']
# finally, we are ready to train the tokenizer
tokenizer.train(paths, trainer)

这个训练过程中的重要部分是使用一个特殊的预标记化程序。预标记化程序是我们最初将单词分割成标记的方式。在我们的案例中,我们使用标准的空白字符,但我们可以使用更高级的方法。例如,我们可以使用分号,因此可以将整行代码作为标记。

执行上述代码片段后,我们的标记化程序已经训练完毕,可以使用了。我们可以通过编写tokenizer.get_vocab()来检查标记。以下是一些标记(前 10 个标记):

'only': 565, 'he': 87, 'RTOS': 416, 'DE': 266, 'CH': 154, 'a': 54, 'ps': 534, 'will': 372, 'NX_SHIFT_BY': 311, 'O': 42,

这组标记与之前案例中的标记集非常不同。它包含了一些单词,如“will”,和一些子词,如“ol.”。这是因为 BPE 标记化程序发现了一些重复的标记,并用专门的字节替换了它们。

最佳实践 #47

在处理大型语言模型和大量文本语料库时使用 BPE。

当我分析大量文本时,例如大型代码库,我会首选使用 BPE。这项任务对 BPE 来说非常快速,并且能够捕捉复杂的依赖关系。它也在 BERT 或 GPT 等模型中被大量使用。

现在,在我们的案例中,我们用来训练 BPE 标记化程序的源代码很小,所以很多单词没有重复出现,优化并没有太多意义。因此,WordPiece 标记化程序可以完成同样(如果不是更好)的工作。然而,对于更大的文本语料库,这个标记化程序比 WordPiece 或词袋模型更有效率和高效。它也是下一个标记化程序——SentencePiece 的基础。

SentencePiece 标记化程序

句子分割(SentencePiece)比 BPE 更通用,还有一个原因:它允许我们将空白视为常规标记。这使我们能够找到更复杂的依赖关系,因此可以训练出理解不仅仅是单词片段的模型。因此得名——句子分割。这个分词器最初是为了使像日语这样的语言(例如,与英语不同,日语不使用空白)的标记化成为可能。可以通过运行pip install -q sentencepiece命令来安装这个分词器。

在以下代码示例中,我们实例化和训练了 SentencePiece 分词器:

import sentencepiece as spm
# this statement trains the tokenizer
spm.SentencePieceTrainer.train('--input="/content/drive/MyDrive/ds/cs_dos/nx_icmp_checksum_compute.c" --model_prefix=m --vocab_size=200')
# makes segmenter instance and
# loads the model file (m.model)
sp = spm.SentencePieceProcessor()
sp.load('m.model')

我们在与其他分词器相同的文件上对其进行了训练。文本是一个编程文件,因此我们可以预期分词器能比正常文本更好地理解编程语言的内容。值得注意的是词汇表的大小,它是 200,而之前的例子中是 30,000。这是因为这个分词器试图找到尽可能多的标记。由于我们的输入程序非常短——一个包含几个函数的文件——分词器不能创建超过大约 300 个标记。

以下片段使用这个分词器对“Hello World”程序进行编码,并打印以下输出:

strCProgram = '''
int main(int argc, void **argc)
{
  printf("%s", "Hello World\n");
  return 0;
}
'''
print(sp.encode_as_pieces(strCProgram))

前十个标记的表示方式如下:

'▁in', 't', '▁', 'm', 'a', 'in', '(', 'in', 't', '▁a'

在这个分词器中引入的新元素是下划线字符(_)。它在文本中表示空白。这是独特的,它使我们能够更有效地在编程语言理解中使用这个分词器,因为它允许我们捕获诸如嵌套之类的编程结构——也就是说,使用制表符而不是空格,或者在同一行中编写多个语句。这一切都是因为这个分词器将空白视为重要的事物。

最佳实践 #48

当没有明显的单词边界时,请使用 SentencePiece 分词器。

当分析编程语言代码并关注编程风格时,我会使用 SentencePiece——例如,当我们关注诸如驼峰式变量命名等问题时。对于这个任务,理解程序员如何使用空格、格式化和其他编译器透明的元素非常重要。因此,这个分词器非常适合这样的任务。

词嵌入

分词器是从文本中提取特征的一种方法。它们功能强大,可以训练以创建复杂的标记并捕获单词的统计依赖关系。然而,它们受限于它们是完全无监督的,并且不捕获任何单词之间的意义或关系。这意味着分词器非常适合为神经网络模型,如 BERT,提供输入,但有时我们希望有与特定任务更对齐的特征。

这就是词嵌入发挥作用的地方。以下代码展示了如何实例化从gensim库导入的词嵌入模型。首先,我们需要准备数据集:

from gensim.models import word2vec
# now, we need to prepare a dataset
# in our case, let's just read a dataset that is a code of a program
# in this example, I use the file from an open source component - Azure NetX
# the actual part is not that important, as long as we have a set of
# tokens that we want to analyze
path = '/content/drive/MyDrive/ds/cs_dos/nx_icmp_checksum_compute.c'
# read all lines into an array
with open(path, 'r') as r:
  lines = r.readlines()
# and see how many lines we got
print(f'The file (and thus our corpus) contains {len(lines)} lines')

与之前的标记化器相比,前面的代码片段以不同的方式准备文件。它创建了一个行列表,每行是一个由空格分隔的标记列表。现在,我们已经准备好创建word2vec模型并在这些数据上训练它:

# we need to pass splitted sentences to the model
tokenized_sentences = [sentence.split() for sentence in lines]
model = word2vec.Word2Vec(tokenized_sentences,
                          vector_size=10,
                          window=1,
                          min_count=0,
                          workers=4)

结果是,该模型是在我们提供的语料库上训练的——实现 HTTP 协议一部分的 C 程序。我们可以通过编写model.wv.key_to_index来查看已提取的前 10 个标记:

'*/': 0, '/*': 1, 'the': 2, '=': 3, 'checksum': 4, '->': 5, 'packet': 6, 'if': 7, 'of': 8, '/**********************************************************************/': 9,

总共,word2vec提取了 259 个标记。

与我们之前使用的标记化器不同,这个词嵌入模型将词(标记)的值嵌入到一个潜在空间中,这使得我们可以更智能地利用这些词的词汇属性。例如,我们可以使用model.wv.most_similar(positive=['add'])来检查词的相似性:

('NX_LOWER_16_MASK;', 0.8372778296470642),
('Mask', 0.8019374012947083),
('DESCRIPTION', 0.7171915173530579),

我们也可以假设这些词是向量,它们的相似性被这个向量捕捉。因此,我们可以写一些类似的东西,比如 model.wv.most_similar(positive= ['file', 'function'], negative=['found']) 并获得如下结果:

('again', 0.24998697638511658),
('word', 0.21356187760829926),
('05-19-2020', 0.21174617111682892),
('*current_packet;', 0.2079058289527893),

如果我们用数学来表示这个表达式,那么结果将是:result = file + function – found。这个相似词列表是距离这个计算结果捕获的向量最*的词列表。

当我们想要捕捉词和表达式的相似性时,词嵌入非常强大。然而,该模型的原始实现存在某些限制——例如,它不允许我们使用原始词汇表之外的词。请求与未知标记(例如,model.wv.most_similar(positive=['return']))相似的词会导致错误。

FastText

幸运的是,有一个word2vec模型的扩展可以*似未知标记——FastText。我们可以用与使用word2vec非常相似的方式使用它:

from gensim.models import FastText
# create the instance of the model
model = FastText(vector_size=4,
                 window=3,
                 min_count=1)
# build a vocabulary
model.build_vocab(corpus_iterable=tokenized_sentences)
# and train the model
model.train(corpus_iterable=tokenized_sentences,
            total_examples=len(tokenized_sentences),
            epochs=10)

在前面的代码片段中,模型是在与word2vec相同的 数据集上训练的。model = FastText(vector_size=4, window=3, min_count=1) 创建了一个具有三个超参数的 FastText 模型实例:

  • vector_size:结果特征向量中的元素数量

  • window:用于捕捉上下文词的窗口大小

  • min_count:要包含在词汇表中的单词的最小频率

model.build_vocab(corpus_iterable=tokenized_sentences)通过遍历tokenized_sentences可迭代对象(该对象应包含一个列表的列表,其中每个内部列表代表一个句子被分解成单个单词)并将每个单词添加到词汇表中,如果它满足min_count阈值。model.train(corpus_iterable=tokenized_sentences, total_examples=len(tokenized_sentences), epochs=10)使用tokenized_sentences可迭代对象训练 FastText 模型,总共 10 个 epoch。在每个 epoch 中,模型再次遍历语料库,并根据每个目标词周围的上下文单词更新其内部权重。total_examples参数告诉模型语料库中有多少个总示例(即句子),这用于计算学*率。

输入是相同的。然而,如果我们调用未知标记的相似度,例如model.wv.most_similar(positive=['return']),我们会得到以下结果:

('void', 0.5913326740264893),
('int', 0.43626993894577026),
('{', 0.2602742612361908),

这三个相似词的集合表明模型可以*似未知标记。

我接下来的最佳实践是关于 FastText 的使用。

最佳实践 #49

使用词嵌入,如 FastText,作为文本分类任务的有价值特征表示,但考虑将其纳入更全面的模型以实现最佳性能。

除非我们需要使用 LLM,这种特征提取是简单词袋技术以及强大的 LLM 的绝佳替代方案。它捕捉到一些含义的部分,并允许我们基于文本数据设计分类器。它还可以处理未知标记,这使得它非常灵活。

从特征提取到模型

本章中提出的特征提取方法并非我们唯一能使用的。至少还有更多(更不用说其他方法了)。然而,它们的工作原理相似。不幸的是,没有一劳永逸的解决方案,所有模型都有其优势和劣势。对于同一任务,但不同的数据集,简单的模型可能比复杂的模型更好。

现在我们已经看到了如何从文本、图像和数值数据中提取特征,现在是时候开始训练模型了。这就是我们在下一章将要做的。

参考文献

  • Al-Sabbagh, K.W., et al. Selective regression testing based on big data: comparing feature extraction techniques. in 2020 IEEE International Conference on Software Testing, Verification and Validation Workshops (ICSTW). 2020. IEEE.

  • Staron, M., et al. Improving Quality of Code Review Datasets–Token-Based Feature Extraction Method. in Software Quality: Future Perspectives on Software Engineering Quality: 13th International Conference, SWQD 2021, Vienna, Austria, January 19–21, 2021, Proceedings 13. 2021. Springer.

  • Sennrich, R., B. Haddow, and A. Birch, Neural machine translation of rare words with subword units. arXiv preprint arXiv:1508.07909, 2015.

  • Gage, P., A new algorithm for data compression. C Users Journal, 1994. 12(2): p. 23-38.

  • Kudo, T. 和 J. Richardson, SentencePiece:一种简单且语言无关的子词分词和去分词器,用于神经文本处理。arXiv 预印本 arXiv:1808.06226, 2018.

第三部分:机器学*系统设计与开发

尽管机器学*和其*亲人工智能广为人知,但它们涉及广泛的算法和模型。首先,有基于统计学*的经典机器学*模型,通常需要数据以表格形式准备。它们在数据中识别模式,并能复制这些模式。然而,也有基于深度学*的现代模型,能够捕捉到更精细的数据模式,这些数据结构相对较少。这些模型的典范是转换器模型(GPT)和自编码器(扩散器)。在本部分书中,我们更深入地探讨这些模型。我们关注这些模型如何被训练和集成到机器学*管道中。我们还探讨如何将这些模型应用于软件工程实践。

本部分包含以下章节:

  • 第九章, 机器学*系统类型 – 基于特征和原始数据(深度学*)

  • 第十章, 经典机器学*系统和神经网络的训练与评估

  • 第十一章, 高级机器学*算法的训练与评估 – GPT-3 和自编码器

  • 第十二章, 设计机器学*管道及其测试

  • 第十三章, 大规模、鲁棒机器学*软件的设计与实现

第九章:机器学*系统类型——基于特征和基于原始数据(深度学*)

在前几章中,我们学*了数据、噪声、特征和可视化。现在,是时候转向机器学*模型了。没有一种单一的模型,但有很多种——从经典的模型,如随机森林,到用于视觉系统的深度学*模型,再到生成式 AI 模型,如 GPT。

卷积和 GPT 模型被称为深度学*模型。它们的名称来源于它们使用原始数据作为输入,并且模型的第一层包括特征提取层。它们还设计为随着输入数据通过这些模型而逐步学*更抽象的特征。

本章演示了这些模型类型中的每一种,并从经典机器学*到生成式 AI 模型逐步推进。

本章将涵盖以下主题:

  • 为什么我们需要不同类型的模型?

  • 经典的机器学*模型和系统,例如随机森林、决策树和逻辑回归

  • 用于视觉系统的深度学*模型、卷积神经网络模型和你只需看一次YOLO)模型

  • 通用预训练转换器GPT)模型

为什么我们需要不同类型的模型?

到目前为止,我们在数据处理上投入了大量的努力,同时专注于诸如噪声减少和标注等任务。然而,我们还没有深入研究用于处理这些处理数据的模型。虽然我们简要提到了基于数据标注的不同类型模型,包括监督学*、无监督学*和强化学*,但我们还没有彻底探讨用户在利用这些模型时的视角。

在使用机器学*模型处理数据时,考虑用户的视角非常重要。用户的需求、偏好和具体要求在选择和使用适当的模型中起着至关重要的作用。

从用户的角度来看,评估诸如模型可解释性、集成简便性、计算效率和可扩展性等因素变得至关重要。根据应用和用例,用户可能会优先考虑模型的不同方面,例如准确性、速度或处理大规模数据集的能力。

此外,用户的领域专业知识和对底层算法的熟悉程度会影响模型的选择和评估。一些用户可能更喜欢简单、更透明的模型,这些模型提供可解释性和可理解性,而其他人可能愿意为了使用更复杂的模型(如深度学*网络)来提高预测性能而牺牲可解释性。

考虑用户的视角使模型选择和部署的方法更加全面。这涉及到积极地将用户纳入决策过程,收集反馈,并持续改进模型以满足他们的特定需求。

通过将用户的视角纳入讨论中,我们可以确保我们选择的模型不仅满足技术要求,而且与用户的期望和目标相一致,从而最终提高整个系统的有效性和可用性。

因此,在未来的工作中,我们将探讨不同类型的用户如何与各种机器学*模型互动并从中受益,同时考虑他们的具体需求、偏好和领域专业知识。我们将从经典的机器学*模型开始,这些模型在历史上是最先出现的。

经典机器学*模型

经典机器学*模型需要以表格和矩阵的形式预处理数据。例如,随机森林、线性回归和支持向量机等经典机器学*模型需要一个清晰的预测器和类别集合来发现模式。因此,我们需要为手头的任务手动设计预处理管道。

从用户的视角来看,这些系统是以非常经典的方式设计的——有一个用户界面、数据处理引擎(我们的经典机器学*模型)和输出。这如图 图 9**.1 所示:

图 9.1 – 机器学*系统的要素

图 9.1 – 机器学*系统的要素

图 9**.1 展示了有三个要素——输入提示、模型和输出。对于大多数这样的系统,输入提示是为模型提供的一组属性。用户填写某种形式的表格,系统提供答案。它可以是一个预测土地价格的表格,或者是一个贷款、求职、寻找最佳汽车的系统,等等。

这样的系统的源代码可能看起来像这样:

import pandas as pd
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
# Load the stock price data into a pandas DataFrame
data = pd.read_csv('land_property_data.csv')
# Select the features (e.g., historical prices, volume, etc.) and the target variable (price)
X = data[['Feature1', 'Feature2', ...]]  # Relevant features here
y = data['price']
# read the model from the serialized storage here
# Make predictions on the test data
y_pred = model.predict(X)
# Evaluate the model using mean squared error (MSE)
print(f'The predicted value of the property is: {y_pred}')

这段代码需要模型已经训练好,并且只使用它来进行预测。使用模型的主要行是加粗的行。代码片段的其余部分用于处理输入,最后一行用于输出通信。

在现代生态系统中,机器学*模型的力量来自于无需大量更改代码就能改变模型的能力。大多数经典机器学*模型使用这种拟合/预测接口,正是这种接口使得这一点成为可能。那么,我们可以使用哪些机器学*模型呢?它们的种类实在太多,无法一一列举。然而,这些模型中的一些群体具有某些特性:

  • 回归模型将用于预测类别值的机器学*模型分组。它们既可以用于分类(将模块分类为易出故障或不),也可以用于预测任务(预测模块中的缺陷数量)。这些模型基于找到最佳曲线来拟合给定数据。

  • 基于树的模型将基于在数据集中寻找差异的模型分组,就像我们编写了一系列的 if-then 语句。这些 if-then 语句的逻辑条件基于数据的统计特性。这些模型适用于分类和预测模型。

  • 聚类算法将基于在数据中寻找相似性和将相似实体分组的模型分组。它们通常是未监督的,并且需要一些实验来找到正确的参数集(例如,簇的数量)。

  • 神经网络将所有可用于经典机器学*任务的神经网络分组。这些算法需要我们设计和训练神经网络模型。

我们可以根据这些模型的特点来选择它们,并通过测试找到最佳模型。然而,如果我们包括超参数训练,这个过程将非常耗时且费力。因此,我强烈推荐使用 AutoML 方法。AutoML 是一组算法,它们利用机器学*模型的 fit/predict 接口自动寻找最佳模型。通过探索众多模型,它们可以找到最适合数据集的模型。我们用星号表示这一点。有时,人类理解数据和其特性的能力会超过大多数自动机器学*过程(见博客文章)。

因此,这是本章的第一个最佳实践。

最佳实践#50

当您在训练经典机器学*模型时,请将 AutoML 作为首选。

使用 AutoML 非常简单,以下是从 auto-sklearn 文档中的代码片段可以说明这一点:

import autosklearn.classification
cls = autosklearn.classification.AutoSklearnClassifier()
cls.fit(X_train, y_train)
predictions = cls.predict(X_test)

前面的片段说明了使用 auto-sklearn 工具包寻找最佳模型是多么容易。请注意,这个工具包仅设计用于基于 Linux 的系统。要在 Microsoft Windows 操作系统上使用它,我建议使用Windows Subsystem for Linux 2.0WSL 2)。该界面以这种方式隐藏最佳模型,以至于用户甚至不需要看到哪个模型最适合当前的数据。

import autosklearn.classification导入专门用于分类任务的 auto-sklearn 模块。cls = autosklearn.classification.AutoSklearnClassifier()初始化AutoSklearnClassifier类的一个实例,它代表autosklearn中的 AutoML 分类器。它创建一个对象,该对象将用于自动搜索最佳分类器和其超参数。cls.fit(X_train, y_train)AutoSklearnClassifier拟合到训练数据。它自动探索不同的分类器和它们的超参数配置,以根据提供的X_train(特征)和y_train(目标标签)找到最佳模型。它在提供的训练数据集上训练 AutoML 模型。

predictions = cls.predict(X_test) 使用拟合的 AutoSklearnClassifierX_test 数据集进行预测。它将上一步找到的最佳模型应用于测试数据,并将预测的标签分配给 predictions 变量。

让我们在第六章中使用的相同数据集上应用 auto-sklearn:

# read the file with data using openpyxl
import pandas as pd
# we read the data from the excel file,
# which is the defect data from the ant 1.3 system
dfDataCamel12 = pd.read_excel('./chapter_6_dataset_numerical.xlsx',
                            sheet_name='camel_1_2',
                            index_col=0)
# prepare the dataset
import sklearn.model_selection
X = dfDataCamel12.drop(['Defect'], axis=1)
y = dfDataCamel12.Defect
X_train, X_test, y_train, y_test = \
        sklearn.model_selection.train_test_split(X, y, random_state=42, train_size=0.9)

我们将使用之前使用的相同代码:

import autosklearn.classification
cls = autosklearn.classification.AutoSklearnClassifier()
cls.fit(X_train, y_train)
predictions = cls.predict(X_test)

一旦我们训练了模型,我们可以检查它——例如,通过使用 print(cls.sprint_statistics()) 命令让 auto-sklearn 提供有关最佳模型的信息。结果如下:

auto-sklearn results:
Dataset name: 4b131006-f653-11ed-814a-00155de31e8a
Metric: accuracy
Best validation score: 0.790909
Number of target algorithm runs: 1273
Number of successful target algorithm runs: 1214
Number of crashed target algorithm runs: 59
Number of target algorithms that exceeded the time limit: 0
Number of target algorithms that exceeded the memory limit: 0

这条信息显示工具包已测试了 1273 个算法,其中有 59 个崩溃。这意味着它们与我们提供的数据集不兼容。

我们也可以使用 print(cls.show_models()) 命令让工具包为我们提供最佳模型。此命令提供了一系列用于集成学*的模型及其在最终得分中的权重。最后,我们可以使用 print(f\"Accuracy score {sklearn.metrics.accuracy_score(y_test, predictions):.2f}\") 来获取测试数据的准确度分数。对于这个数据集,测试数据的准确度分数为 0.59,这并不多。然而,这是通过使用最佳集成获得的模型。如果我们要求模型提供训练数据的准确度分数,我们将得到 0.79,这要高得多,但这是因为模型非常优化。

在本书的后面部分,我们将探讨这些算法,并学*它们在软件工程及其它任务中的行为。

卷积神经网络和图像处理

经典的机器学*模型相当强大,但在输入方面有限。我们需要预处理它,使其成为一组特征向量。它们在学*能力上也有限——它们是一次性学*者。我们只能训练它们一次,并且不能添加更多训练。如果需要更多训练,我们需要从头开始训练这些模型。

经典的机器学*模型在处理复杂结构,如图像的能力上也被认为相当有限。正如我们之前所学的,图像至少有两个不同的维度,并且可以包含三个信息通道——红色、绿色和蓝色。在更复杂的应用中,图像可以包含来自激光雷达或地理空间数据的数据,这些数据可以提供关于图像的元信息。

因此,为了处理图像,需要更复杂的模型。其中之一是 YOLO 模型。由于其准确性和速度之间取得了很好的平衡,YOLO 模型被认为在目标检测领域处于最前沿。

让我们看看如何利用 Hugging Face 中的预训练 YOLO v5 模型。在这里,我想提供我的下一个最佳实践。

最佳实践 #51

从 Hugging Face 或 TensorFlow Hub 使用预训练模型开始。

使用预训练模型有几个优点:

  • 首先,它允许我们将网络作为我们管道的基准。在继续前进并开始训练它之前,我们可以对其进行实验并了解其局限性。

  • 其次,它为我们提供了为现有、经过实际使用验证的模型添加更多训练的可能性,这些模型也被其他人使用过。

  • 最后,它为我们提供了与社区分享我们的模型的可能性,以支持人工智能的道德和负责任的发展。

以下代码片段安装 YoLo 模型并实例化它:

# install YoLo v5 network
!pip install -q -U yolov5
# then we set up the network
import yolov5
# load model
model = yolov5.load('fcakyon/yolov5s-v7.0')
# set model parameters
model.conf = 0.25  # NMS confidence threshold
model.iou = 0.45  # NMS IoU threshold
model.agnostic = False  # NMS class-agnostic
model.multi_label = False  # NMS multiple labels per box
model.max_det = 1000  # maximum number of detections per image

前几行使用 load 函数从指定的源加载 YOLOv5 模型——即 fcakyon/yolov5s-v7.0 ——并将加载的模型分配给变量 model,该变量可用于执行目标检测。model.conf 参数设置了 非极大值抑制NMS)的置信度阈值,该阈值用于过滤掉低于此置信度水平的检测。在这种情况下,它被设置为 0.25,这意味着只有置信度分数高于 0.25 的检测将被考虑。

model.iou 参数设置了 model.agnostic 参数确定 NMS 是否为类别无关。如果设置为 False,NMS 将在抑制过程中考虑类别标签,这意味着如果两个边界框具有相同的坐标但不同的标签,它们将不会被考虑为重复。在这里,它被设置为 Falsemodel.multi_label 参数控制 NMS 是否允许每个边界框有多个标签。如果设置为 False,每个框将被分配一个具有最高置信度分数的单个标签。在这里,它被设置为 False

最后,model.max_det 参数设置了每张图像允许的最大检测数量。在这种情况下,它被设置为 1000,这意味着只有前 1,000 个检测(按置信度分数排序)将被保留。

现在,我们可以执行推理——即使用网络检测对象——但首先,我们必须加载图像:

# and now we prepare the image
from PIL import Image
from torchvision import transforms
# Load and preprocess the image
image = Image.open('./test_image.jpg')

此代码片段使用 PIL 的 Image 模块的 open 函数加载位于 ./test_image.jpg 的图像文件。它创建了一个表示图像的 Image 类实例。

一旦加载了图像,你可以在将其馈送到 YOLOv5 模型进行目标检测之前对其进行各种转换以进行预处理。这可能涉及调整大小、归一化或其他预处理步骤,具体取决于模型的要求:

# perform inference
results = model(image)
# inference with larger input size
results = model(image, size=640)
# inference with test time augmentation
results = model(image, augment=True)
# parse results
predictions = results.pred[0]
boxes = predictions[:, :4] # x1, y1, x2, y2
scores = predictions[:, 4]
categories = predictions[:, 5]
# show detection bounding boxes on image
results.show()

前面的代码片段在第一行执行目标检测,然后绘制图像,以及检测到的对象的边界框。在我们的例子中,这是前面代码片段的结果:

图 9.2 – 图像中检测到的对象

图 9.2 – 图像中检测到的对象

请注意,该模型将汽车识别为卡车,可能是因为汽车后部存在额外的行李。图像来源是 Pixabay。图示表明,目标检测未能正确识别对象。然而,这并不是问题。我们可以使用这个预训练模型并进一步训练它。然而,这将是下一章的主题,所以我们在这里不会涉及。

最佳实践#52

与预训练网络合作,识别它们的局限性,然后在您的数据集上训练网络。

我强烈建议从预训练模型开始使用,然后在您的数据集上训练网络。深度学*模型能够继续训练的能力是我们设计基于机器学*的系统时可以利用的伟大特性。这样,我们就能兼得两者之优——我们的系统可以检测通用对象,同时在检测系统指定的对象方面表现得更好。这种方法通常用于设计汽车系统。

让我们看看其他类型的深度学*系统,这些系统使用预训练并增加了一层复杂性——提示工程。

BERT 和 GPT 模型

BERT 和 GPT 模型使用原始数据作为输入,它们的主要输出是一个预测的单词。这个单词可以在句中预测,也可以在句尾预测。这意味着围绕这些模型设计的产物需要以不同于其他模型的方式处理数据。

图 9**.3 提供了这种处理方法的概述,重点关注开始时的提示工程和结束时的输出处理。此图显示了位于中心的基于 BERT 或 GPT 架构的机器学*模型。这是一个重要方面,但它只提供了整个系统(或工具)的非常小的一部分。

工具的工作流程从左侧的输入处理开始。对于用户来说,这是一个提示,要求模型执行某些操作,例如:“用 C 语言编写一个反转字符串的函数”。工具将这个提示转换为模型的有用输入——它可以找到与模型输入相似的 C 程序,并在该程序末尾添加<mask>标记,使模型预测输出。

然后,模型进行预测——一次一个标记。工具需要循环输出,直到生成有用的内容。工具需要定义停止标准,并将输出处理成对用户有用的形式:

图 9.3 – 现代深度学*工具设计概述,其中机器学*模型只是工具的一个小(但重要)部分

图 9.3 – 现代深度学*工具设计概述,其中机器学*模型只是工具的一个小(但重要)部分

如果我们将这个提示发送到 ChatGPT,我们会得到以下三部分的响应。首先,我们得到函数的实际代码,该函数用 C 语言反转字符串:

#include <stdio.h>
#include <string.h>
void reverseString(char* str) {
    int length = strlen(str);
    int i, j;
    for (i = 0, j = length - 1; i < j; i++, j--) {
        char temp = str[i];
        str[i] = str[j];
        str[j] = temp;
    }
}
int main() {
    char str[] = "Hello, world!";
    printf("Original string: %s\n", str);
    reverseString(str);
    printf("Reversed string: %s\n", str);
    return 0;
}

这个函数没有生成任何注释,所以 ChatGPT 使用摘要管道以以下方式描述模型:

This code defines a function called reverseString that takes a character array (char*) as input and reverses the string in-place. It uses two pointers, i and j, starting from the beginning and end of the string respectively, and swaps the characters until they meet in the middle.
In the main function, a sample string "Hello, world!" is provided, and the reverseString function is called to reverse it. The original and reversed strings are then printed for verification.

这个说明应该替换代码的文档,这需要添加以符合良好的编程实践。

最后,这个工具为我们提供了一个测试这个函数的代码示例:

Original string: Hello, world!
Reversed string: !dlrow ,olleH

响应的最后部分也是作为提示的一部分生成的。这是因为测试代码通常在数据集中以类似的情况存在。现在,如果我们的提示不仅仅是字符串反转,也许是在不同的编程语言中,比如 C#,我们可能会得到不同的响应结构。例如,如果我们的提示是Write a function that creates 3000 random numbers in C# and prints them to the console,那么响应将只包含该函数的 C#代码,而不是测试代码:

using System;
class Program {
    static void Main(string[] args) {
        const int numNumbers = 3000;
        const int minValue = 0;
        const int maxValue = 100;
        Random random = new Random();
        for (int i = 0; i < numNumbers; i++) {
            int num = random.Next(minValue, maxValue + 1);
            Console.WriteLine(num);
        }
    }
}

也生成了说明,但没有测试这个函数的代码。相反,这个函数被封装为class Program,并且没有main()函数来测试它。

在软件系统中使用语言模型

使用 ChatGPT 等产品很棒,但它们也局限于它们被设计的目的。现在,我们可以使用 Hugging Face 界面从头开始使用这样的模型。在下面的代码示例中,我们可以看到如何使用专门用于特定任务(如识别设计模式)的模型来完成文本——即编写 Singleton 设计模式的签名。这说明了语言模型(包括 GPT-3/4)在底层是如何与文本工作的。

在下面的代码片段中,我们正在从 Hugging Face 库导入模型并实例化它。该模型已经在一系列专门的单一程序上进行了预训练,并通过添加来自 Linux 内核源代码的随机代码作为 C++中单一类代码来合成构建:

# import the model via the huggingface library
from transformers import AutoTokenizer, AutoModelForMaskedLM
# load the tokenizer and the model for the pretrained SingBERTa
tokenizer = AutoTokenizer.from_pretrained('mstaron/SingBERTa')
# load the model
model = AutoModelForMaskedLM.from_pretrained("mstaron/SingBERTa")
# import the feature extraction pipeline
from transformers import pipeline

这段代码从 Hugging Face 的 Transformers 库中导入必要的模块。然后,它加载了预训练的 SingBERTa 的标记器和模型。标记器负责将文本转换为数值标记,而模型是一个专门为掩码语言建模MLM)任务设计的预训练语言模型。它从预训练的 SingBERTa 中加载模型。之后,它从 Transformers 库中导入特征提取管道。特征提取管道使我们能够轻松地从模型中提取上下文化的嵌入。

总体而言,这段代码为我们设置了使用 SingBERTa 模型进行各种自然语言处理任务(如文本分词、MLM 和特征提取)所必需的组件。下面的代码片段正是这样做的——它创建了填充空白的管道。这意味着模型已经准备好预测句子中的下一个单词:

fill_mask = pipeline(
    "fill-mask",
    model="./SingletonBERT",
    tokenizer="./SingletonBERT"
)

我们可以通过使用fill_mask("static Singleton:: <mask>")命令来使用这个管道,这将产生以下输出:

[{'score': 0.9703333973884583, 'token': 74, 'token_str': 'f', 'sequence': 'static Singleton::f'},
{'score': 0.025934329256415367, 'token': 313, 'token_str': ' );', 'sequence': 'static Singleton:: );'},
{'score': 0.0003994493163190782, 'token': 279, 'token_str': '();', 'sequence': 'static Singleton::();'},
{'score': 0.00021698368072975427, 'token': 395, 'token_str': ' instance', 'sequence': 'static Singleton:: instance'},
{'score': 0.00016094298916868865, 'token': 407, 'token_str': ' getInstance', 'sequence': 'static Singleton:: getInstance'}]

前面的输出显示,最佳预测是f标记。这是正确的,因为训练示例使用了f作为添加到 Singleton 类中的函数的名称(例如Singleton::f1())。

如果我们想要扩展这些预测,就像 ChatGPT 的代码生成功能一样,我们需要循环前面的代码,一次生成一个标记,从而填充程序。无法保证程序能够编译,因此后处理基本上只能选择这些结构(从提供的标记列表中),这将导致一段可编译的代码。我们甚至可以添加测试此代码的功能,从而使我们的产品越来越智能,而无需创建更大的模型。

因此,这是本章的最后一个最佳实践。

最佳实践 #53

不要寻找更复杂的模型,而是创建一个更智能的管道。

与一个好的管道一起工作可以使一个好的模型变成一个优秀的软件产品。通过提供正确的提示(用于预测的文本开头),我们可以创建一个对我们产品所满足的使用案例有用的输出。

摘要

在本章中,我们窥见了机器学*模型从内部的样子,至少是从程序员的角度来看。这说明了我们在构建基于机器学*的软件时存在的重大差异。

在经典模型中,我们需要创建大量的预处理管道,以确保模型获得正确的输入。这意味着我们需要确保数据具有正确的属性,并且处于正确的格式;我们需要与输出一起工作,将预测转化为更有用的东西。

在深度学*模型中,数据以更流畅的方式进行预处理。模型可以准备图像和文本。因此,软件工程师的任务是专注于产品和其使用案例,而不是监控概念漂移、数据准备和后处理。

在下一章中,我们将继续探讨训练机器学*模型的示例——既包括经典的,也包括最重要的深度学*模型。

参考文献

  • Staron, M. 和 W. Meding. 在大型软件项目中短期缺陷流入预测的初步评估。在《国际软件工程经验评估会议》(EASE)中。2007 年。

  • Prykhodko, S. 基于归一化变换的回归分析开发软件缺陷预测模型。在《现代应用软件测试问题》(PTTAS-2016)中,研究与实践研讨会摘要,波尔塔瓦,乌克兰。2016 年。

  • Ochodek, M. 等, 第八章 使用机器学*识别违反公司特定编码指南的代码行。在《加速数字化转型:软件中心 10 年》中。2022 年,Springer。* 第 211-251 页。*

  • Ibrahim, D.R.,R. Ghnemat,和 A. Hudaib。使用特征选择和随机森林算法进行软件缺陷预测。在 2017 年国际计算科学新趋势会议(ICTCS)上。2017. IEEE.

  • Ochodek, M.,M. Staron,和 W. Meding, 第九章 SimSAX:基于符号*似方法和软件缺陷流入的项目相似度度量。在加速数字化转型:软件中心 10 年。2022,Springer。 p. 253-283.

  • Phan, V.A.,使用自动编码器和 K-Means 学*拉伸-收缩潜在表示以进行软件缺陷预测。IEEE Access,2022. 10: p. 117827-117835.

  • Staron, M.,等人,机器学*支持持续集成中的代码审查。软件工程人工智能方法,2021: p. 141-167.

  • Li, J.,等人。通过卷积神经网络进行软件缺陷预测。在 2017 年 IEEE 国际软件质量、可靠性和安全性会议(QRS)上。2017. IEEE.

  • Feurer, M.,等人,高效且鲁棒的自动机器学*。神经网络信息处理系统进展, 2015. 28.

  • Feurer, M.,等人,Auto-sklearn 2.0:通过元学*实现免手动的自动机器学*。机器学*研究杂志,2022. 23(1): p. 11936-11996.

  • Redmon, J.,等人。你只看一次:统一、实时目标检测。在 IEEE 计算机视觉和模式识别会议论文集中。2016.

  • Staron, M., 《汽车软件架构》. 2021: Springer.

  • Gamma, E., 等人,设计模式:可重用面向对象软件的元素。1995: Pearson Deutschland GmbH.

第十章:训练和评估经典机器学*系统和神经网络

现代机器学*框架被设计成对程序员友好。Python 编程环境(以及 R)的流行表明,设计、开发和测试机器学*模型可以专注于机器学*任务,而不是编程任务。机器学*模型的开发者可以专注于开发整个系统,而不是算法内部的编程。然而,这也带来了一些负面影响——对模型内部结构和它们是如何被训练、评估和验证的缺乏理解。

在本章中,我将更深入地探讨训练和评估的过程。我们将从不同算法背后的基本理论开始,然后学*它们是如何被训练的。我们将从经典的机器学*模型开始,以决策树为例。然后,我们将逐步转向深度学*,在那里我们将探索密集神经网络和更高级的网络类型。

本章最重要的部分是理解训练/评估算法与测试/验证整个机器学*软件系统之间的区别。我将通过描述机器学*算法作为生产机器学*系统的一部分(或整个机器学*系统的样子)来解释这一点。

在本章中,我们将涵盖以下主要主题:

  • 训练和测试过程

  • 训练经典机器学*模型

  • 训练深度学*模型

  • 误导性结果——数据泄露问题

训练和测试过程

机器学*通过使计算机能够从数据中学*并做出预测或决策,而无需明确编程,从而彻底改变了我们解决复杂问题的方式。机器学*的一个关键方面是训练模型,这涉及到教算法识别数据中的模式和关系。训练机器学*模型的两种基本方法是 model.fit()model.predict()

model.fit() 函数是训练机器学*模型的核心。它是模型从标记数据集中学*以做出准确预测的过程。在训练过程中,模型调整其内部参数以最小化其预测与训练数据中真实标签之间的差异。这种迭代优化过程,通常被称为“学*”,允许模型推广其知识并在未见过的数据上表现良好。

除了训练数据和标签之外,model.fit()函数还接受各种超参数作为参数。这些超参数包括周期数(即模型将遍历整个数据集的次数)、批量大小(在更新参数之前处理的样本数量)和学*率(确定参数更新的步长)。正确调整这些超参数对于确保有效的训练和防止诸如过拟合或欠拟合等问题至关重要。

一旦训练过程完成,训练好的模型就可以用于对新数据做出预测。这就是model.predict()方法发挥作用的地方。给定一个训练好的模型和一组输入数据,model.predict()函数将应用学*到的权重和偏差来生成预测或类别概率。预测输出可以用于各种目的,如分类、回归或异常检测,具体取决于手头问题的性质。

我们在之前的章节中看到了这个界面的例子。现在,是时候了解这个界面底下的内容以及训练过程是如何进行的了。在上一章中,我们将这个过程视为一个黑盒,即程序跳过model.fit()行之后,这个过程就完成了。这是这个过程的基本原理,但不仅如此。这个过程是迭代的,并且取决于正在训练的算法/模型。由于每个模型都有不同的参数,拟合函数可以接受更多的参数。我们甚至可以在实例化模型时添加额外的参数,甚至在训练过程之前。图 10.1 将这个过程呈现为一个灰色框:

图 10.1 – 训练机器学*模型的灰色框

图 10.1 – 训练机器学*模型的灰色框

在我们开始训练过程之前,我们将数据分为训练集和测试集(我们之前已经讨论过)。同时,我们选择我们使用的机器学*模型的参数。这些参数可以是任何东西,从随机森林中的树的数量到神经网络中的迭代次数和批量大小。

训练过程是迭代的,其中模型在数据上训练,内部评估,然后重新训练以找到更适合数据的拟合。在本章中,我们将探讨这种内部训练是如何工作的。

最后,一旦模型经过训练,它就准备好进行测试过程。在测试过程中,我们使用预定义的性能指标来检查模型学*到的模式对于新数据能否得到良好的重现。

训练经典机器学*模型

我们将首先训练一个模型,让我们能够查看其内部。我们将使用 CART 决策树分类器,我们可以可视化训练的实际决策树。我们将使用与上一章相同的数值数据。首先,让我们读取数据并创建训练/测试分割:

# read the file with data using openpyxl
import pandas as pd
# we read the data from the excel file,
# which is the defect data from the ant 1.3 system
dfDataAnt13 = pd.read_excel('./chapter_6_dataset_numerical.xlsx',
                            sheet_name='ant_1_3',
                            index_col=0)
# prepare the dataset
import sklearn.model_selection
X = dfDataAnt13.drop(['Defect'], axis=1)
y = dfDataAnt13.Defect
X_train, X_test, y_train, y_test = \
        sklearn.model_selection.train_test_split(X, y, random_state=42, train_size=0.9)

上述代码使用 pandas 库中的pd.read_excel()函数读取名为'chapter_6_dataset_numerical.xlsx'的 Excel 文件。文件被读取到一个名为dfDataAnt13的 DataFrame 中。sheet_name参数指定了要读取的 Excel 文件中的工作表,而index_col参数将第一列设置为 DataFrame 的索引。

代码为训练机器学*模型准备数据集。通过使用drop()方法从dfDataAnt13 DataFrame 中删除'Defect'列,将独立变量(特征)分配给X变量。通过从dfDataAnt13 DataFrame 中选择'Defect'列,将因变量(目标)分配给y变量。

使用sklearn.model_selection.train_test_split()函数将数据集分为训练集和测试集。Xy变量被分为X_trainX_testy_trainy_test变量。train_size参数设置为0.9,表示 90%的数据将用于训练,剩余的 10%将用于测试。random_state参数设置为42以确保分割的可重复性。

一旦数据准备就绪,我们可以导入决策树库并训练模型:

# now that we have the data prepared
# we import the decision tree classifier and train it
from sklearn.tree import DecisionTreeClassifier
# first we create an empty classifier
decisionTreeModel = DecisionTreeClassifier()
# then we train the classifier
decisionTreeModel.fit(X_train, y_train)
# and we test it for the test set
y_pred_cart = decisionTreeModel.predict(X_test)

上述代码片段从sklearn.tree模块导入DecisionTreeClassifier类。创建了一个空的决策树分类器对象,并将其分配给decisionTreeModel变量。该对象将在之前片段中准备好的数据集上进行训练。在decisionTreeModel对象上调用fit()方法来训练分类器。fit()方法接受训练数据(X_train)和相应的目标值(y_train)作为输入。分类器将学*训练数据中的模式和关系以进行预测。

训练好的决策树分类器用于预测测试数据集(X_test)的目标值。在decisionTreeModel对象上调用predict()方法,并将X_test作为输入。预测的目标值存储在y_pred_cart变量中。预测的模型需要评估,因此让我们评估模型的准确率、精确率和召回率:

# now, let's evaluate the code
from sklearn.metrics import accuracy_score
from sklearn.metrics import recall_score
from sklearn.metrics import precision_score
print(f'Accuracy: {accuracy_score(y_test, y_pred_cart):.2f}')
print(f'Precision: {precision_score(y_test, y_pred_cart, average="weighted"):.2f}, Recall: {recall_score(y_test, y_pred_cart, average="weighted"):.2f}')

这段代码片段生成了以下输出:

Accuracy: 0.83
Precision: 0.94, Recall: 0.83

指标显示模型表现不错。它正确地将测试集中的 83%的数据分类。它对真实正例(更高的精确率)比对真实负例(较低的召回率)更敏感。这意味着它在预测中可能会错过一些缺陷易发模块。然而,决策树模型让我们能够查看模型内部,并探索它从数据中学到的模式。以下代码片段就是这样做的:

from sklearn.tree import export_text
tree_rules = export_text(decisionTreeModel, feature_names=list(X_train.columns))
print(tree_rules)

前面的代码片段以文本形式导出决策树,我们将其打印出来。export_text() 函数接受两个参数——第一个是要可视化的决策树,下一个是特征列表。在我们的情况下,特征列表是数据集的列列表。

在这种情况下,整个决策树相当复杂,但第一个决策路径看起来是这样的:

|--- WMC <= 36.00
|   |--- ExportCoupling <= 1.50
|   |   |--- NOM <= 2.50
|   |   |   |--- NOM <= 1.50
|   |   |   |   |--- class: 0
|   |   |   |--- NOM >  1.50
|   |   |   |   |--- WMC <= 5.50
|   |   |   |   |   |--- class: 0
|   |   |   |   |--- WMC >  5.50
|   |   |   |   |   |--- CBO <= 4.50
|   |   |   |   |   |   |--- class: 1
|   |   |   |   |   |--- CBO >  4.50
|   |   |   |   |   |   |--- class: 0
|   |   |--- NOM >  2.50
|   |   |   |--- class: 0

这个决策路径看起来非常类似于一个大的 if-then 语句,如果我们知道数据中的模式,我们就可以自己编写它。这个模式并不简单,这意味着数据相当复杂。它可能是非线性的,需要复杂的模型来捕捉依赖关系。它也可能需要大量的努力来找到模型性能和其泛化数据能力之间的正确平衡。

因此,这是我处理这类模型的最佳实践。

最佳实践 #54

如果你想要理解你的数值数据,请使用提供可解释性的模型。

在前面的章节中,我提倡使用 AutoML 模型,因为它们稳健且能为我们节省大量寻找正确模块的麻烦。然而,如果我们想更好地理解我们的数据并了解模式,我们可以从决策树等模型开始。它们对数据的洞察为我们提供了关于我们可以从数据中获得什么的良好概述。

作为反例,让我们看看来自同一数据集的另一个模块的数据。让我们读取它并执行分割:

# read the file with data using openpyxl
import pandas as pd
# we read the data from the excel file,
# which is the defect data from the ant 1.3 system
dfDataCamel12 = pd.read_excel('./chapter_6_dataset_numerical.xlsx',
                            sheet_name='camel_1_2',
                            index_col=0)
# prepare the dataset
import sklearn.model_selection
X = dfDataCamel12.drop(['Defect'], axis=1)
y = dfDataCamel12.Defect
X_train, X_test, y_train, y_test = \
        sklearn.model_selection.train_test_split(X, y, random_state=42, train_size=0.9)

现在,让我们为这些数据训练一个新的模型:

# now that we have the data prepared
# we import the decision tree classifier and train it
from sklearn.tree import DecisionTreeClassifier
# first we create an empty classifier
decisionTreeModelCamel = DecisionTreeClassifier()
# then we train the classifier
decisionTreeModelCamel.fit(X_train, y_train)
# and we test it for the test set
y_pred_cart_camel = decisionTreeModel.predict(X_test)

到目前为止,一切顺利——没有错误,没有问题。让我们检查一下模型的表现:

# now, let's evaluate the code
from sklearn.metrics import accuracy_score
from sklearn.metrics import recall_score
from sklearn.metrics import precision_score
print(f'Accuracy: {accuracy_score(y_test, y_pred_cart_camel):.2f}')
print(f'Precision: {precision_score(y_test, y_pred_cart_camel, average="weighted"):.2f}, Recall: {recall_score(y_test, y_pred_cart_camel, average="weighted"):.2f}')

然而,性能并不像之前那么高:

Accuracy: 0.65
Precision: 0.71, Recall: 0.65

现在,让我们打印出树:

from sklearn.tree import export_text
tree_rules = export_text(decisionTreeModel, feature_names=list(X_train.columns))
print(tree_rules)

如我们所见,结果也相当复杂:

|--- WMC >  36.00
|   |--- DCC <= 3.50
|   |   |--- WMC <= 64.50
|   |   |   |--- NOM <= 17.50
|   |   |   |   |--- ImportCoupling <= 7.00
|   |   |   |   |   |--- NOM <= 6.50
|   |   |   |   |   |   |--- class: 0
|   |   |   |   |   |--- NOM >  6.50
|   |   |   |   |   |   |--- CBO <= 4.50
|   |   |   |   |   |   |   |--- class: 0
|   |   |   |   |   |   |--- CBO >  4.50
|   |   |   |   |   |   |   |--- ExportCoupling <= 13.00
|   |   |   |   |   |   |   |   |--- NOM <= 16.50
|   |   |   |   |   |   |   |   |   |--- class: 1
|   |   |   |   |   |   |   |   |--- NOM >  16.50
|   |   |   |   |   |   |   |   |   |--- class: 0
|   |   |   |   |   |   |   |--- ExportCoupling >  13.00
|   |   |   |   |   |   |   |   |--- class: 0
|   |   |   |   |--- ImportCoupling >  7.00
|   |   |   |   |   |--- class: 0
|   |   |   |--- NOM >  17.50
|   |   |   |   |--- class: 1
|   |   |--- WMC >  64.50
|   |   |   |--- class: 0

如果我们看看这个树中的第一个决策和上一个决策,它基于 WMC 特征。WMC 代表 加权方法每类,是 20 世纪 90 年代由 Chidamber 和 Kamerer 提出的经典软件度量之一。该度量捕捉了类的复杂性和大小(以某种方式),因此大类的缺陷倾向性更强——简单地说,如果源代码更多,犯错误的机会就更大。在这个模型的情况下,这要复杂一些,因为模型认识到 WMC 超过 36 的类比其他类更容易出错,除了超过 64.5 的类,这些类不太容易出错。后者也是一个已知现象,即大类的测试也更为困难,因此可能包含未发现的缺陷。

这里是我的下一个最佳实践,关于模型的可解释性。

最佳实践 #55

最好的模型是那些能够捕捉数据中经验现象的模型。

尽管机器学*模型可以捕捉任何类型的依赖关系,但最佳模型是那些能够捕捉逻辑和经验观察的模型。在先前的例子中,模型可以捕捉与类的大小及其易出错性相关的软件工程经验观察。拥有能够捕捉经验关系的模型可以带来更好的产品和可解释的人工智能。

理解训练过程

从软件工程师的角度来看,训练过程相当简单——我们拟合模型,验证它,并使用它。我们检查模型在性能指标方面的好坏。如果模型足够好,并且我们可以解释它,那么我们就围绕它开发整个产品,或者将其用于更大的软件产品中。

当模型没有学*到任何有用的东西时,我们需要了解为什么会出现这种情况,以及是否可能存在另一个可以做到的模型。我们可以使用我们在第六章中学到的可视化技术来探索数据,并使用第四章中的技术清除噪声。

现在,让我们探索决策树模型如何从数据中学*的流程。DecisionTree分类器通过递归地根据训练数据集中特征的值对特征空间进行分区来从提供的数据中学*。它构建一个二叉树,其中每个内部节点代表一个特征和一个基于阈值值的决策规则,每个叶节点代表一个预测的类别或结果。

训练过程分为以下步骤:

  1. 选择最佳特征:分类器评估不同的特征,并确定最佳分离数据为不同类别的特征。这通常是通过不纯度度量或信息增益来完成的,例如基尼不纯度或熵。

  2. 分割数据集:一旦选定了最佳特征,分类器将数据集根据该特征的值分割成两个或更多子集。每个子集代表决策树中的不同分支或路径。

  3. 递归重复过程:上述步骤对决策树的每个子集或分支重复进行,将它们视为单独的数据集。这个过程会继续进行,直到满足停止条件,例如达到最大深度、节点上的最小样本数或其他预定义标准。

  4. 分配类别标签:在决策树的叶节点处,分类器根据该区域样本的多数类别分配类别标签。这意味着在做出预测时,分类器将叶节点中最频繁的类别分配给落入该区域的未见样本。

在学*过程中,DecisionTree分类器旨在找到最佳分割,以最大化类别的分离并最小化每个结果子集中的不纯度。通过根据提供的训练数据递归地根据特征空间进行分区,分类器学*决策规则,使其能够对未见数据做出预测。

需要注意的是,决策树容易过拟合,这意味着它们可以过度记住训练数据,并且对新数据泛化能力不强。例如剪枝、限制最大深度或使用随机森林等集成方法可以帮助减轻过拟合并提高决策树模型的表现。

我们在这本书中已经使用过随机森林分类器,所以这里不会深入细节。尽管随机森林在泛化数据方面表现更好,但与决策树相比,它们是不透明的。我们无法探索模型学到了什么——我们只能探索哪些特征对判决贡献最大。

随机森林和不透明模型

让我们基于与反例中相同的数据训练随机森林分类器,并检查模型是否表现更好,以及模型是否使用与原始反例中DecisionTree分类器相似的特征。

让我们使用以下代码片段实例化、训练和验证模型:

from sklearn.ensemble import RandomForestClassifier
randomForestModel = RandomForestClassifier()
randomForestModel.fit(X_train, y_train)
y_pred_rf = randomForestModel.predict(X_test)

在评估模型后,我们获得了以下性能指标:

Accuracy: 0.62
Precision: 0.63, Recall: 0.62

诚然,这些指标与决策树中的指标不同,但整体性能并没有太大的差异。0.03 的准确度差异是可以忽略不计的。首先,我们可以提取重要特征,重复使用在第五章中介绍过的相同技术:

# now, let's check which of the features are the most important ones
# first we create a dataframe from this list
# then we sort it descending
# and then filter the ones that are not imporatnt
dfImportantFeatures = pd.DataFrame(randomForestModel.feature_importances_, index=X.columns, columns=['importance'])
# sorting values according to their importance
dfImportantFeatures.sort_values(by=['importance'],
                                ascending=False,
                                inplace=True)
# choosing only the ones that are important, skipping
# the features which have importance of 0
dfOnlyImportant = dfImportantFeatures[dfImportantFeatures['importance'] != 0]
# print the results
print(f'All features: {dfImportantFeatures.shape[0]}, but only {dfOnlyImportant.shape[0]} are used in predictions. ')

我们可以通过执行以下代码来可视化决策中使用的特征集:

# we use matplotlib and seaborn to make the plot
import matplotlib.pyplot as plt
import seaborn as sns
# Define size of bar plot
# We make the x axis quite much larger than the y-axis since
# there is a lot of features to visualize
plt.figure(figsize=(40,10))
# plot Searborn bar chart
# we just use the blue color
sns.barplot(y=dfOnlyImportant['importance'],
            x=dfOnlyImportant.index,
            color='steelblue')
# we make the x-labels rotated so that we can fit
# all the features
plt.xticks(rotation=90)
sns.set(font_scale=6)
# add chart labels
plt.title('Importance of features, in descending order')
plt.xlabel('Feature importance')
plt.ylabel('Feature names')

此代码帮助我们理解图 10.2中显示的重要性图表。在这里,WMC(加权方法计数)是最重要的特征。这意味着森林中有许多树使用此指标来做出决策。然而,由于森林是一个集成分类器——它使用投票来做出决策——这意味着在做出最终调用/预测时总是使用多棵树:

图 10.2 – 随机森林分类器的特征重要性图表。

图 10.2 – 随机森林分类器的特征重要性图表。

请注意,该模型比这些特征的线性组合更复杂。此图表展示了不是最佳实践,而是一种最佳经验。因此,我将将其用作最佳实践来展示其重要性。

最佳实践 #56

简单但可解释的模型通常可以很好地捕捉数据。

在我使用不同类型数据的实验过程中,我所学到的经验是,如果有模式,一个简单的模型就能捕捉到它。如果没有模式,或者数据有很多不符合规则的情况,那么即使是最复杂的模型在寻找模式时也会遇到问题。因此,如果你不能解释你的结果,不要将它们用于你的产品中,因为这些结果可能会使产品变得相当无用。

然而,在这个隧道尽头有一线光明。一些模型可以捕捉非常复杂的模式,但它们是透明的——神经网络。

训练深度学*模型

训练密集神经网络涉及多个步骤。首先,我们准备数据。这通常涉及特征缩放、处理缺失值、编码分类变量以及将数据分为训练集和验证集。

然后,我们定义密集神经网络的架构。这包括指定层数、每层的神经元数量、要使用的激活函数以及任何正则化技术,如 dropout 或批量归一化。

一旦定义了模型,我们就需要初始化它。我们根据定义的架构创建神经网络模型的一个实例。这涉及到创建神经网络类的一个实例或使用深度学*库中可用的预定义模型架构。我们还需要定义一个损失函数,该函数量化模型预测输出与实际目标值之间的误差。损失函数的选择取决于问题的性质,例如分类(交叉熵)或回归(均方误差)。

除了损失函数之外,我们还需要一个优化器。优化器算法将在训练过程中更新神经网络的权重。常见的优化器包括随机梯度下降(SGD)、Adam 和 RMSprop。

然后,我们可以训练模型。在这里,我们遍历训练数据多次(整个数据集的遍历)。在每个 epoch(遍历整个数据集)中,执行以下步骤:

  1. 前向传播:我们将一批输入数据输入到模型中,并计算预测输出。

  2. 计算损失:我们使用定义的损失函数将预测输出与实际目标值进行比较,以计算损失。

  3. 反向传播:我们通过反向传播将损失反向传播到网络中,以计算权重相对于损失的梯度。

  4. 更新权重:我们使用优化器根据计算出的梯度来更新神经网络的权重,调整网络参数以最小化损失。

我们对训练数据中的每个批次重复这些步骤,直到所有批次都已被处理。

最后,我们需要执行验证过程,就像在之前的模型中一样。在这里,我们计算一个验证指标(例如准确度或均方误差)来评估模型对未见数据的泛化能力。这有助于我们监控模型的进展并检测过拟合。

一旦模型经过训练和验证,我们就可以在未用于训练或验证的单独测试数据集上评估其性能。在这里,我们计算相关的评估指标来评估模型的准确度、精确度、召回率或其他所需的指标。

因此,让我们为我们的数据集做这件事。首先,我们必须使用以下代码定义模型的架构:

import torch
import torch.nn as nn
import torch.optim as optim
# Define the neural network architecture
class NeuralNetwork(nn.Module):
    def __init__(self, input_size, hidden_size, num_classes):
        super(NeuralNetwork, self).__init__()
        self.fc1 = nn.Linear(input_size, hidden_size)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(hidden_size, num_classes)
    def forward(self, x):
        out = self.fc1(x)
        out = self.relu(out)
        out = self.fc2(out)
        return out

在这里,我们定义了一个名为 NeuralNetwork 的类,它是 nn.Module 的子类。这个类代表我们的神经网络模型。它有两个全连接层(fc1fc2),层间使用 ReLU 激活函数。网络看起来就像 图 10.3 中所示的那样:

图 10.3 – 用于预测缺陷的神经网络

图 10.3 – 用于预测缺陷的神经网络。

这个可视化是使用alexlenail.me/NN-SVG/index.html创建的。隐藏层中的神经元数量是 64,但在这个图中,只显示了 16 个,以便使其更易于阅读。网络从 6 个输入神经元开始,然后是隐藏层(中间)的 64 个神经元,最后是两个用于决策类别的神经元。

现在,我们可以定义训练网络的超参数并实例化它:

# Define the hyperparameters
input_size = X_train.shape[1]  # Number of input features
hidden_size = 64              # Number of neurons in the hidden layer
num_classes = 2               # Number of output classes
# Create an instance of the neural network
model = NeuralNetwork(input_size, hidden_size, num_classes)
# Define the loss function and optimizer
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# Convert the data to PyTorch tensors
X_train_tensor = torch.Tensor(X_train.values)
y_train_tensor = torch.LongTensor(y_train.values)
X_test_tensor = torch.Tensor(X_test.values)
# Training the neural network
num_epochs = 10000
batch_size = 32

在这里,我们创建了一个名为 modelNeuralNetwork 类实例,具有指定的输入大小、隐藏大小和输出类数量,正如我们在第一个代码片段中定义的那样。我们定义了损失函数(交叉熵损失)和优化器(Adam 优化器)来训练模型。然后,使用 torch.Tensor()torch.LongTensor() 将数据转换为 PyTorch 张量。最后,我们表示我们希望在 10,000 个 epoch(迭代)中训练模型,每个迭代包含 32 个元素(数据点):

for epoch in range(num_epochs):
    for I in range(0, len(X_train_tensor), batch_size):
        batch_X = X_train_tensor[i:i+batch_size]
        batch_y = y_train_tensor[i:i+batch_size]
        # Forward pass
        outputs = model(batch_X)
        loss = criterion(outputs, batch_y)
        # Backward and optimize
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    # Print the loss at the end of each epoch
    if (epoch % 100 == 0):
      print(""Epoch {epoch+1}/{num_epochs}, Loss: {loss.item():.3f"")

现在,我们可以获取测试数据的预测并获取性能指标:

with torch.no_grad():
    model.eval()  # Set the model to evaluation mode
    X_test_tensor = torch.Tensor(X_test.values)
    outputs = model(X_test_tensor)
    _, predicted = torch.max(outputs.data, 1)
    y_pred_nn = predicted.numpy()
# now, let's evaluate the code
from sklearn.metrics import accuracy_score
from sklearn.metrics import recall_score
from sklearn.metrics import precision_score
print(f'Accuracy: {accuracy_score(y_test, y_pred_nn):.2f}')
print(f'Precision: {precision_score(y_test, y_pred_nn, average="weighted"):.2f}, Recall: {recall_score(y_test, y_pred_nn, average="weighted"):.2f}')

性能指标如下:

Accuracy: 0.73
Precision: 0.79, Recall: 0.73

因此,这比之前的模型要好一些,但并不出色。模式根本不存在。我们可以通过增加隐藏层的数量来使网络更大,但这并不会使预测变得更好。

误导性结果 – 数据泄露

在训练过程中,我们使用一组数据,在测试集中使用另一组数据。最佳的训练过程是当这两个数据集是分开的时候。如果它们不是分开的,我们就会遇到一个称为数据泄露问题的情况。这个问题是指我们在训练集和测试集中有相同的数据点。让我们用一个例子来说明这一点。

首先,我们需要创建一个新的分割,其中两个集合中都有一些数据点。我们可以通过使用 split 函数并将 20%的数据点设置为测试集来实现这一点。这意味着至少有 10%的数据点在两个集合中:

X_trainL, X_testL, y_trainL, y_testL = \
        sklearn.model_selection.train_test_split(X, y, random_state=42, train_size=0.8)

现在,我们可以使用相同的代码对这组数据进行预测,然后计算性能指标:

# now, let's evaluate the model on this new data
with torch.no_grad():
    model.eval()  # Set the model to evaluation mode
    X_test_tensor = torch.Tensor(X_testL.values)
    outputs = model(X_test_tensor)
    _, predicted = torch.max(outputs.data, 1)
    y_pred_nn = predicted.numpy()
print(f'Accuracy: {accuracy_score(y_testL, y_pred_nn):.2f}')
print(f'Precision: {precision_score(y_testL, y_pred_nn, average="weighted"):.2f}, Recall: {recall_score(y_testL, y_pred_nn, average="weighted"):.2f}')

结果如下:

Accuracy: 0.85
Precision: 0.86, Recall: 0.85

结果比之前更好。然而,它们之所以更好,仅仅是因为 10%的数据点被用于训练和测试集。这意味着模型的性能比指标所暗示的要差得多。因此,我们得出了我的下一个最佳实践。

最佳实践 #56

总是要确保训练集和测试集中的数据点是分开的。

尽管我们在这里故意犯了这个错误,但在实践中很容易犯这个错误。请注意 split 函数中的random_state=42参数。显式设置它确保了分割的可重复性。然而,如果我们不这样做,我们每次进行分割时都可能会得到不同的分割,从而可能导致数据泄露问题。

当我们处理图像或文本时,数据泄露问题甚至更难发现。仅仅因为一个图像来自两个不同的文件,并不能保证它是不同的。例如,在高速公路上连续拍摄的图像将不同,但不会太不同,如果它们最终出现在测试集和训练集中,我们就会得到数据泄露问题的一个全新维度。

摘要

在本章中,我们讨论了与机器学*和神经网络相关的各种主题。我们解释了如何使用 pandas 库从 Excel 文件中读取数据,并为训练机器学*模型准备数据集。我们探讨了决策树分类器的使用,并展示了如何使用 scikit-learn 训练决策树模型。我们还展示了如何使用训练好的模型进行预测。

然后,我们讨论了如何从决策树分类器切换到随机森林分类器,后者是决策树的集成。我们解释了必要的代码修改,并提供了示例。接下来,我们将重点转向在 PyTorch 中使用密集神经网络。我们描述了创建神经网络架构、训练模型以及使用训练好的模型进行预测的过程。

最后,我们解释了训练密集神经网络所涉及的步骤,包括数据准备、模型架构、初始化模型、定义损失函数和优化器、训练循环、验证、超参数调整和评估。

总体来说,我们涵盖了与机器学*算法相关的一系列主题,包括决策树、随机森林和密集神经网络,以及它们各自的训练过程。

在下一章中,我们将探讨如何训练更先进的机器学*模型——例如自编码器。

参考文献

  • Chidamber, S.R. 和 C.F. Kemerer, 对面向对象设计的度量集。IEEE 软件工程 Transactions, 1994. 20(6): p. 476–493.

第十一章:高级机器学*算法的训练和评估——GPT 和自动编码器

经典的机器学*ML)和神经网络NNs)非常适合处理经典问题——预测、分类和识别。正如我们在上一章所学,训练它们需要适量的数据,并且我们针对特定任务进行训练。然而,在 2010 年代末和 2020 年代初,机器学*(ML)和人工智能AI)的突破是关于完全不同类型的模型——深度学*DL)、生成预训练转换器GPTs)和生成 AIGenAI)。

生成 AI 模型提供了两个优势——它们可以生成新数据,并且可以为我们提供数据的内部表示,该表示捕捉了数据的上下文,并在一定程度上捕捉了其语义。在前几章中,我们看到了如何使用现有模型进行推理和生成简单的文本片段。

在本章中,我们探讨基于 GPT 和双向编码器表示转换器(BERT)模型的生成 AI 模型是如何工作的。这些模型旨在根据它们训练的模式生成新数据。我们还探讨了自动编码器(AEs)的概念,其中我们训练一个 AE 根据先前训练的数据生成新图像。

在本章中,我们将涵盖以下主要内容:

  • 从经典机器学*模型到生成 AI

  • 生成 AI 模型背后的理论——自动编码器(AEs)和转换器

  • Robustly Optimized BERT ApproachRoBERTa)模型的训练和评估

  • 自动编码器(AE)的训练和评估

  • 开发安全笼子以防止模型破坏整个系统

从经典机器学*到生成 AI

经典人工智能,也称为符号人工智能或基于规则的 AI,是该领域最早的思想流派之一。它基于明确编码知识和使用逻辑规则来操纵符号并推导出智能行为的理念。经典人工智能系统旨在遵循预定义的规则和算法,使它们能够以精确和确定性解决定义明确的问题。我们深入探讨经典人工智能的潜在原则,探索其对基于规则的系统、专家系统和逻辑推理的依赖。

相比之下,生成 AI 代表了人工智能发展的范式转变,利用机器学*(ML)和神经网络(NNs)的力量来创建能够生成新内容、识别模式和做出明智决策的智能系统。生成 AI 不是依赖于显式规则和手工知识,而是利用数据驱动的方法从大量信息中学*,并推断模式和关系。我们探讨生成 AI 的核心概念,包括深度学*(DL)、神经网络(NNs)和概率模型,以揭示其创造原创内容并促进创造性问题解决的能力。

生成式人工智能(GenAI)模型的一个例子是 GPT-3 模型。GPT-3 是由 OpenAI 开发的最先进的语言模型。它基于转换器架构。GPT-3 使用一种称为无监督学*(UL)的技术进行训练,这使得它能够生成连贯且上下文相关的文本。

先进模型(AE 和转换器)背后的理论

经典机器学*(ML)模型的一个大局限是访问标注数据。大型神经网络包含数百万(如果不是数十亿)个参数,这意味着它们需要同样数量的标记数据点来正确训练。数据标注,也称为注释,是 ML 中最昂贵的活动,因此标注过程成为了 ML 模型的实际限制。在 2010 年代初,解决这个问题的方法是使用众包。

众包,这是一种集体数据收集的过程(以及其他),意味着我们使用我们服务的用户来标注数据。CAPTCHA 是最突出的例子之一。当我们需要识别图像以登录服务时,会使用 CAPTCHA。当我们引入新图像时,每次用户需要识别这些图像时,我们可以在相对较短的时间内标注大量数据。

然而,这个过程本身存在一个固有的问题。好吧,有几个问题,但最突出的问题是这个过程主要与图像或类似类型的数据一起工作。它也是一个相对有限的过程——我们只能要求用户识别图像,但不能添加语义图,也不能在图像上绘制边界框。我们不能要求用户评估图像或任何其他,稍微复杂一些的任务。

这里引入了更高级的方法——生成式人工智能(GenAI)和如生成对抗网络(GANs)之类的网络。这些网络被设计用来生成数据并学*哪些数据类似于原始数据。这些网络非常强大,并被用于如图像生成等应用;例如,在所谓的“深度伪造”中。

AEs

这样的模型的主要组成部分是自动编码器(AE),它被设计用来学*输入数据的压缩表示(编码),然后从这个压缩表示中重建原始数据(解码)。

自动编码器(AE)的架构(图 11.1.1)由两个主要组件组成:编码器和解码器。编码器接收输入数据并将其映射到一个低维的潜在空间表示,通常被称为编码/嵌入或潜在表示。解码器接收这个编码表示并将其重建为原始输入数据:

图 11.1 – 自动编码器的高级架构

图 11.1 – 自动编码器的高级架构

自动编码器(AE)的目标是最小化重建误差,即输入数据与解码器输出之间的差异。通过这样做,AE 学*在潜在表示中捕获输入数据的最重要特征,从而有效地压缩信息。最有趣的部分是潜在空间或编码。这一部分允许模型在只有几个数字的小向量中学*复杂数据点(例如,图像)的表示。AE 学*的潜在表示可以被视为输入数据的压缩表示或低维嵌入。这种压缩表示可用于各种目的,例如数据可视化、降维、异常检测,或作为其他下游任务的起点。

编码器部分计算潜在向量,解码器部分可以将它扩展成图像。自动编码器有多种类型;最有趣的一种是变分自动编码器VAE),它编码的是可以生成新数据的函数的参数,而不是数据的表示本身。这样,它可以根据分布创建新数据。实际上,它甚至可以通过组合不同的函数来创建完全新的数据类型。

转换器

自然语言处理NLP)任务中,我们通常使用一种略有不同的生成人工智能类型——转换器。转换器彻底改变了机器翻译领域,但已被应用于许多其他任务,包括语言理解和文本生成。

在其核心,转换器采用了一种自注意力机制,允许模型在处理序列中的不同单词或标记时,权衡它们的重要性。这种注意力机制使得模型能够比传统的循环神经网络RNNs)或卷积神经网络CNNs)更有效地捕捉单词之间的长距离依赖关系和上下文关系。

转换器由编码器-解码器结构组成。编码器处理输入序列,如句子,解码器生成输出序列,通常基于输入和目标序列。转换器有两个独特的元素:

  • 多头自注意力(MHSA):一种允许模型同时关注输入序列中不同位置的机制,捕捉不同类型的依赖关系。这是对 RNN 架构的扩展,它能够连接同一层中的神经元,从而捕捉时间依赖关系。

  • 位置编码:为了将位置信息纳入模型,添加了位置编码向量到输入嵌入中。这些位置编码基于标记及其相互之间的相对位置。这种机制允许我们捕捉特定标记的上下文,因此捕捉文本的基本上下文语义。

图 11.2展示了转换器的高级架构:

图 11.2 – Transformer 的高级架构

图 11.2 – Transformer 的高级架构

在这个架构中,自注意力是模型在处理序列中的单词或标记时,权衡不同单词或标记重要性的关键机制。自注意力机制独立应用于输入序列中的每个单词,并有助于捕捉单词之间的上下文关系和依赖。术语指的是并行操作的独立注意力机制。在 Transformer 模型中可以使用多个自注意力头来捕捉不同类型的关系(尽管我们不知道这些关系是什么)。

每个自注意力头通过计算查询表示和键表示之间的注意力分数来操作。这些注意力分数表示序列中每个单词相对于其他单词的重要性或相关性。通过将查询和键表示之间的点积,然后应用 softmax 函数来归一化分数,获得注意力分数。

然后使用注意力分数来权衡值表示。将加权值相加,以获得序列中每个单词的输出表示。

Transformer 中的前馈网络有两个主要作用:特征提取和位置表示。特征提取从自注意力输出中提取高级特征,其方式与我们之前学*过的词嵌入提取非常相似。通过应用非线性变换,模型可以捕捉输入序列中的复杂模式和依赖关系。位置表示确保模型可以学*每个位置的不同变换。它允许模型学*句子的复杂表示,因此捕捉每个单词和句子的更复杂上下文。

Transformer 架构是现代模型如 GPT-3 的基础,GPT-3 是一个预训练的生成 Transformer;也就是说,它已经在大量文本上进行了预训练。然而,它基于 BERT 及其相关模型。

RoBERTa 模型的训练和评估

通常,GPT-3 的训练过程涉及将模型暴露于来自不同来源的大量文本数据,如书籍、文章、网站等。通过分析这些数据中的模式、关系和语言结构,模型学*根据周围上下文预测单词或短语出现的可能性。这种学*目标是通过称为掩码语言模型MLM)的过程实现的,其中随机掩码输入中的某些单词,模型的任务是根据上下文预测正确的单词。

在本章中,我们训练 RoBERTa 模型,这是现在经典的 BERT 模型的一个变体。我们不是使用如书籍和维基百科文章等通用来源,而是使用程序。为了使我们的训练任务更加具体,让我们训练一个能够“理解”来自网络域的代码的模型——WolfSSL,这是一个 SSL 协议的开源实现,用于许多嵌入式软件设备。

一旦训练完成,BERT 模型能够通过利用其学*到的知识和给定提示中提供的上下文来生成文本。当用户提供一个提示或部分句子时,模型会处理输入,并通过基于从训练数据中学*到的上下文概率预测最可能的下一个词来生成响应。

当涉及到 GPT-3(以及类似的)模型时,它是 BERT 模型的扩展。GPT-3 的生成过程涉及 transformer 架构内的多层注意力机制。这些注意力机制允许模型关注输入文本的相关部分,并在不同的单词和短语之间建立联系,确保生成输出的连贯性和上下文性。模型通过在每个步骤中采样或选择最可能的下一个词来生成文本,同时考虑到之前生成的单词。

因此,让我们通过准备训练数据来开始我们的训练过程。首先,我们读取数据集:

from tokenizers import ByteLevelBPETokenizer
paths = ['source_code_wolf_ssl.txt']
print(f'Found {len(paths)} files')
print(f'First file: {paths[0]}')

这为我们提供了原始训练集。在这个集合中,文本文件包含了一个文件中的所有 WolfSSL 协议的源代码。我们不必这样准备,但这样做确实使过程更容易,因为我们只需处理一个源文件。现在,我们可以训练分词器,这与我们在前几章中看到的过程非常相似:

# Initialize a tokenizer
tokenizer = ByteLevelBPETokenizer()
print('Training tokenizer...')
# Customize training
# we use a large vocabulary size, but we could also do with ca. 10_000
tokenizer.train(files=paths,
                vocab_size=52_000,
                min_frequency=2,
                special_tokens=["<s>","<pad>","</s>","<unk>","<mask>",])

第一行初始化ByteLevelBPETokenizer分词器类的实例。这个分词器基于字节对编码BPE)算法的字节级版本,这是一种流行的子词分词方法。我们已在前几章中讨论过它。

下一行打印一条消息,表明分词器训练过程开始。

调用tokenizer.train()函数来训练分词器。训练过程需要几个参数:

  • files=paths: 此参数指定包含要训练分词器的文本数据的输入文件或路径。它期望一个文件路径列表。

  • vocab_size=52_000: 此参数设置词汇表的大小;也就是说,分词器将生成的唯一标记的数量。在这种情况下,分词器将创建一个包含 52,000 个标记的词汇表。

  • min_frequency=2: 此参数指定一个标记必须在训练数据中出现的最小频率,才能包含在词汇表中。低于此阈值的标记将被视为词汇表外OOV)标记。

  • `special_tokens=["","","

一旦训练过程完成,分词器将学*词汇表,并能够使用训练的子词单元对文本进行编码和解码。现在,我们可以使用以下代码保存分词器:

import os
# we give this model a catchy name - wolfBERTa
# because it is a RoBERTa model trained on the WolfSSL source code
token_dir = './wolfBERTa'
if not os.path.exists(token_dir):
  os.makedirs(token_dir)
tokenizer.save_model('wolfBERTa')

我们还使用以下行测试此分词器:tokenizer.encode("int main(int argc, void **argv)").tokens

现在,让我们确保在下一步中分词器与我们的模型可比较。为此,我们需要确保分词器的输出永远不会超过模型可以接受的标记数:

from tokenizers.processors import BertProcessing
# let's make sure that the tokenizer does not provide more tokens than we expect
# we expect 512 tokens, because we will use the BERT model
tokenizer._tokenizer.post_processor = BertProcessing(
    ("</s>", tokenizer.token_to_id("</s>")),
    ("<s>", tokenizer.token_to_id("<s>")),
)
tokenizer.enable_truncation(max_length=512)

现在,我们可以开始准备模型。我们通过从 HuggingFace hub 导入预定义的类来完成此操作:

import the RoBERTa configuration
from transformers import RobertaConfig
# initialize the configuration
# please note that the vocab size is the same as the one in the tokenizer.
# if it is not, we could get exceptions that the model and the tokenizer are not compatible
config = RobertaConfig(
    vocab_size=52_000,
    max_position_embeddings=514,
    num_attention_heads=12,
    num_hidden_layers=6,
    type_vocab_size=1,
)

第一行,from transformers import RobertaConfig,从transformers库中导入RobertaConfig类。RobertaConfig类用于配置 RoBERTa 模型。接下来,代码初始化 RoBERTa 模型的配置。传递给RobertaConfig构造函数的参数如下:

  • vocab_size=52_000: 此参数设置 RoBERTa 模型使用的词汇表大小。它应与分词器训练期间使用的词汇表大小相匹配。在这种情况下,分词器和模型都具有 52,000 的词汇表大小,确保它们兼容。

  • max_position_embeddings=514: 此参数设置 RoBERTa 模型可以处理的最大序列长度。它定义了模型可以处理的序列中的最大标记数。较长的序列可能需要截断或分成更小的段。请注意,输入是 514,而不是分词器输出的 512。这是由于我们从起始和结束标记中留出了位置。

  • num_attention_heads=12: 此参数设置 RoBERTa 模型中多头注意力MHA)机制中的注意力头数量。注意力头允许模型同时关注输入序列的不同部分。

  • num_hidden_layers=6: 此参数设置 RoBERTa 模型中的隐藏层数量。这些层包含模型的可学*参数,并负责处理输入数据。

  • type_vocab_size=1: 此参数设置标记类型词汇表的大小。在 RoBERTa 等不使用标记类型(也称为段)嵌入的模型中,此值通常设置为 1。

配置对象 config 存储了所有这些设置,将在初始化实际的 RoBERTa 模型时使用。与分词器具有相同的配置参数确保了模型和分词器是兼容的,并且可以一起使用来正确处理文本数据。

值得注意的是,与拥有 1750 亿个参数的 GPT-3 相比,这个模型相当小,它只有(只有)8500 万个参数。然而,它可以在配备中等性能 GPU 的笔记本电脑上训练(任何具有 6GB VRAM 的 NVIDIA GPU 都可以)。尽管如此,该模型仍然比 2017 年的原始 BERT 模型大得多,后者只有六个注意力头和数百万个参数。

模型创建完成后,我们需要初始化它:

# Initializing a Model From Scratch
from transformers import RobertaForMaskedLM
# initialize the model
model = RobertaForMaskedLM(config=config)
# let's print the number of parameters in the model
print(model.num_parameters())
# let's print the model
print(model)

最后两行打印出模型中的参数数量(略超过 8500 万)以及模型本身。该模型的输出相当大,因此我们在此不展示。

现在模型已经准备好了,我们需要回到数据集并为其准备训练。最简单的方法是重用之前训练好的分词器,通过从文件夹中读取它,但需要更改分词器的类别,以便它适合模型:

from transformers import RobertaTokenizer
# initialize the tokenizer from the file
tokenizer = RobertaTokenizer.from_pretrained("./wolfBERTa", max_length=512)

完成这些操作后,我们可以读取数据集:

from datasets import load_dataset
new_dataset = load_dataset("text", data_files='./source_code_wolf_ssl.txt')

之前的代码片段读取了我们用于训练分词器的相同数据集。现在,我们将使用分词器将数据集转换成一组标记:

tokenized_dataset = new_dataset.map(lambda x: tokenizer(x["text"]), num_proc=8)

这需要一点时间,但它也让我们有机会反思这个代码利用了所谓的 map-reduce 算法,该算法在 2010 年代初大数据概念非常流行时成为了处理大型文件的黄金标准。是 map() 函数利用了该算法。

现在,我们需要通过创建所谓的掩码输入来准备数据集以进行训练。掩码输入是一组句子,其中单词被掩码标记(在我们的例子中是 <mask>)所替换。它可以看起来像 图 11.3 中的示例:

图 11.3 – MLMs 的掩码输入

图 11.3 – MLMs 的掩码输入

很容易猜测 <mask> 标记可以出现在任何位置,并且为了模型能够真正学*掩码标记的上下文,它应该在相似位置出现多次。手动操作会非常繁琐,因此 HuggingFace 库为此提供了一个专门的类 – DataCollatorForLanguageModeling。以下代码演示了如何实例化该类以及如何使用其参数:

from transformers import DataCollatorForLanguageModeling
data_collator = DataCollatorForLanguageModeling(
    tokenizer=tokenizer, mlm=True, mlm_probability=0.15
)

from transformers import DataCollatorForLanguageModeling 这一行导入了 DataCollatorForLanguageModeling 类,该类用于准备语言建模任务的数据。代码初始化了一个名为 data_collatorDataCollatorForLanguageModeling 对象。此对象接受多个参数:

  • tokenizer=tokenizer: 此参数指定用于编码和解码文本数据时要使用的标记器。它期望一个tokenizer对象的实例。在这种情况下,似乎tokenizer对象已被预先定义并分配给tokenizer变量。

  • mlm=True: 此参数指示语言建模任务是 MLM 任务。

  • mlm_probability=0.15: 此参数设置在输入文本中掩码标记的概率。每个标记在数据准备期间有 15%的概率被掩码。

data_collator对象现在已准备好用于准备语言建模任务的数据。它负责诸如标记化和掩码输入数据等任务,以确保与 RoBERTa 模型兼容。现在,我们可以实例化另一个辅助类——Trainer——它管理 MLM 模型的训练过程:

from transformers import Trainer, TrainingArguments
training_args = TrainingArguments(
    output_dir="./wolfBERTa",
    overwrite_output_dir=True,
    num_train_epochs=10,
    per_device_train_batch_size=32,
    save_steps=10_000,
    save_total_limit=2,
)
trainer = Trainer(
    model=model,
    args=training_args,
    data_collator=data_collator,
    train_dataset=tokenized_dataset['train'],
)

from transformers import Trainer, TrainingArguments这一行从transformers库中导入了Trainer类和TrainingArguments类。然后它初始化了一个TrainingArguments对象,training_args。此对象接受多个参数以配置训练过程:

  • output_dir="./wolfBERTa": 此参数指定训练模型和其他训练工件将被保存的目录。

  • overwrite_output_dir=True: 此参数确定是否在存在的情况下覆盖output_dir。如果设置为True,它将覆盖目录。

  • num_train_epochs=10: 此参数设置训练的轮数;即在训练过程中训练数据将被迭代的次数。在我们的例子中,只需要几轮就足够了,例如 10 轮。训练这些模型需要花费很多时间,这就是为什么我们选择较小的轮数。

  • per_device_train_batch_size=32: 此参数设置每个 GPU 的训练批大小。它确定在每次训练步骤中并行处理多少个训练示例。如果您 GPU 的 VRAM 不多,请减少此数值。

  • save_steps=10_000: 此参数指定在保存模型检查点之前要进行的训练步骤数。

  • save_total_limit=2: 此参数限制保存检查点的总数。如果超过限制,将删除较旧的检查点。

初始化训练参数后,代码使用以下参数初始化一个Trainer对象:

  • model=model: 此参数指定要训练的模型。在这种情况下,从我们之前的步骤中预初始化的 RoBERTa 模型被分配给模型变量。

  • args=training_args: 此参数指定训练参数,这是我们之前步骤中准备的。

  • data_collator=data_collator: 此参数指定在训练期间要使用的数据合并器。此对象已在我们的代码中预先准备。

  • train_dataset=tokenized_dataset['train']:此参数指定了训练数据集。看起来已经准备并存储在一个名为 tokenized_dataset 的字典中的标记化数据集,并且该数据集的训练部分被分配给了 train_dataset。在我们的案例中,因为我们没有定义训练-测试分割,所以它采用了整个数据集。

Trainer 对象现在已准备好使用指定的训练参数、数据收集器和训练数据集来训练 RoBERTa 模型。我们只需简单地编写 trainer.train() 即可。

一旦模型完成训练,我们可以使用以下命令保存它:trainer.save_model("./wolfBERTa")。之后,我们可以像在 第十章 中学*的那样使用该模型。

训练模型需要一段时间;在 NVIDIA 4090 这样的消费级 GPU 上,10 个周期的训练可能需要大约一天时间,但如果我们想使用更大的数据集或更多的周期,可能需要更长的时间。我不建议在没有 GPU 的计算机上执行此代码,因为它比在 GPU 上慢约 5-10 倍。因此,我的下一个最佳实践是。

最佳实践 #57

使用 NVIDIA Compute Unified Device Architecture (CUDA;加速计算)来训练如 BERT、GPT-3 和 AEs 等高级模型。

对于经典机器学*,甚至对于简单的神经网络,现代 CPU 已经足够。计算量很大,但并不极端。然而,当涉及到训练 BERT 模型、AEs 以及类似模型时,我们需要加速来处理张量(向量)以及一次性在整个向量上执行计算。CUDA 是 NVIDIA 的加速框架。它允许开发者利用 NVIDIA GPU 的强大能力来加速计算任务,包括深度学*模型的训练。它提供了一些好处:

  • GPU 并行处理,旨在同时处理许多并行计算。深度学*模型,尤其是像 RoBERTa 这样的大型模型,包含数百万甚至数十亿个参数。训练这些模型涉及到对这些参数执行大量的数学运算,如矩阵乘法和卷积。CUDA 使得这些计算可以在 GPU 的数千个核心上并行化,与传统的 CPU 相比,大大加快了训练过程。

  • 针对 PyTorch 或 TensorFlow 优化的张量操作,这些操作旨在与 CUDA 无缝工作。这些框架提供了 GPU 加速库,实现了专门为 GPU 设计的优化张量操作。张量是多维数组,用于在深度学*模型中存储和处理数据。有了 CUDA,这些张量操作可以在 GPU 上高效执行,利用其高内存带宽和并行处理能力。

  • 高内存带宽,这使数据能够以更快的速度在 GPU 内存之间传输,从而在训练期间实现更快的数据处理。深度学*模型通常需要大量数据以批量形式加载和处理。CUDA 允许这些批次在 GPU 上有效地传输和处理,从而减少训练时间。

通过利用 CUDA,深度学*框架可以有效地利用 NVIDIA GPU 的并行计算能力和优化操作,从而显著加速大规模模型如 RoBERTa 的训练过程。

AE 的训练和评估

我们在讨论图像特征工程过程时提到了 AEs,见 第七章。然而,AEs 的用途远不止图像特征提取。它们的一个主要方面是能够重新创建图像。这意味着我们可以根据图像在潜在空间中的位置创建图像。

因此,让我们为在机器学*(ML)中相当标准的 Fashion MNIST 数据集训练 AE 模型。我们在前面的章节中看到了数据集的样子。我们通过以下代码片段准备数据开始我们的训练:

# Transforms images to a PyTorch Tensor
tensor_transform = transforms.ToTensor()
# Download the Fashion MNIST Dataset
dataset = datasets.FashionMNIST(root = "./data",
                         train = True,
                         download = True,
                         transform = tensor_transform)
# DataLoader is used to load the dataset
# for training
loader = torch.utils.data.DataLoader(dataset = dataset,
                                     batch_size = 32,
                                     shuffle = True)

它从 PyTorch 库中导入必要的模块。

它使用 transforms.ToTensor() 定义了一个名为 tensor_transform 的转换。这个转换用于将数据集中的图像转换为 PyTorch 张量。

代码片段使用 datasets.FashionMNIST() 函数下载数据集。train 参数设置为 True,表示下载的数据集用于训练目的。download 参数设置为 True,以自动下载数据集,如果它尚未存在于指定的目录中。

由于我们使用具有加速计算的 PyTorch 框架,我们需要确保图像被转换成张量。transform 参数设置为 tensor_transform,这是在代码片段的第一行定义的转换器。

然后,我们创建一个 DataLoader 对象,用于批量加载数据集进行训练。dataset 参数设置为之前下载的数据集。batch_size 参数设置为 32,表示数据集的每一批次将包含 32 张图像。

shuffle 参数设置为 True,以在每个训练周期的样本顺序中打乱,确保随机化并减少训练过程中的任何潜在偏差。

一旦我们准备好了数据集,我们就可以创建我们的 AE,具体做法如下:

# Creating a PyTorch class
# 28*28 ==> 9 ==> 28*28
class AE(torch.nn.Module):
    def __init__(self):
        super().__init__()
        # Building an linear encoder with Linear
        # layer followed by Relu activation function
        # 784 ==> 9
        self.encoder = torch.nn.Sequential(
            torch.nn.Linear(28 * 28, 128),
            torch.nn.ReLU(),
            torch.nn.Linear(128, 64),
            torch.nn.ReLU(),
            torch.nn.Linear(64, 36),
            torch.nn.ReLU(),
            torch.nn.Linear(36, 18),
            torch.nn.ReLU(),
            torch.nn.Linear(18, 9)
        )
        # Building an linear decoder with Linear
        # layer followed by Relu activation function
        # The Sigmoid activation function
        # outputs the value between 0 and 1
        # 9 ==> 784
        self.decoder = torch.nn.Sequential(
            torch.nn.Linear(9, 18),
            torch.nn.ReLU(),
            torch.nn.Linear(18, 36),
            torch.nn.ReLU(),
            torch.nn.Linear(36, 64),
            torch.nn.ReLU(),
            torch.nn.Linear(64, 128),
            torch.nn.ReLU(),
            torch.nn.Linear(128, 28 * 28),
            torch.nn.Sigmoid()
        )
    def forward(self, x):
        encoded = self.encoder(x)
        decoded = self.decoder(encoded)
        return decoded

首先,我们定义一个名为 AE 的类,它继承自 torch.nn.Module 类,这是 PyTorch 中所有神经网络模块的基类。super().__init__() 行确保调用基类(torch.nn.Module)的初始化。由于 AEs 是一种具有反向传播学*的特殊神经网络类,我们可以从库中继承很多基本功能。

然后,我们定义 AE 的编码器部分。编码器由几个具有 ReLU 激活函数的线性(全连接)层组成。每个torch.nn.Linear层代表输入数据的线性变换,后面跟着一个激活函数。在这种情况下,输入大小为 28 * 28(这对应于 Fashion MNIST 数据集中图像的维度),输出大小逐渐减小,直到达到 9,这是我们潜在向量的大小。

然后,我们定义 AE 的解码器部分。解码器负责从编码表示中重建输入数据。它由几个具有 ReLU 激活函数的线性层组成,后面跟着一个具有 sigmoid 激活函数的最终线性层。解码器的输入大小为 9,这对应于编码器瓶颈中潜在向量空间的大小。输出大小为 28 * 28,这与原始输入数据的维度相匹配。

forward方法定义了 AE 的前向传递。它接受一个x输入,并通过编码器传递以获得编码表示。然后,它通过解码器将编码表示传递以重建输入数据。重建的输出作为结果返回。我们现在可以实例化我们的 AE:

# Model Initialization
model = AE()
# Validation using MSE Loss function
loss_function = torch.nn.MSELoss()
# Using an Adam Optimizer with lr = 0.1
optimizer = torch.optim.Adam(model.parameters(),
                             lr = 1e-1,
                             weight_decay = 1e-8)

在此代码中,我们首先将我们的 AE 实例化为我们自己的模型。然后,我们创建了一个由 PyTorch 提供的均方误差MSE)损失函数的实例。MSE 是回归任务中常用的损失函数。我们需要它来计算预测值和目标值之间的平均平方差异——这些目标值是我们数据集中的单个像素,提供了衡量模型性能好坏的指标。图 11.4显示了学*函数在训练 AE 过程中的作用:

图 11.4 – AE 训练过程中的损失函数(均方误差)

图 11.4 – AE 训练过程中的损失函数(均方误差)

然后,我们初始化用于在训练过程中更新模型参数的优化器。在这种情况下,代码创建了一个 Adam 优化器,这是一种用于训练神经网络(NNs)的流行优化算法。它接受三个重要参数:

  • model.parameters():这指定了将要优化的参数。在这种情况下,它包括我们之前创建的模型(自动编码器,AE)的所有参数。

  • lr=1e-1:这设置了学*率,它决定了优化器更新参数的步长大小。较高的学*率可能导致更快收敛,但可能风险超过最佳解,而较低的学*率可能收敛较慢,但可能具有更好的精度。

  • weight_decay=1e-8:此参数向优化器添加一个权重衰减正则化项。权重衰减通过向损失函数添加一个惩罚项来防止过拟合,该惩罚项会阻止权重过大。1e-8的值表示权重衰减系数。

使用此代码,我们现在有一个自动编码器(AE)的实例用于训练。现在,我们可以开始训练过程。我们训练模型 10 个 epoch,如果需要的话,可以尝试更多:

epochs = 10
outputs = []
losses = []
for epoch in range(epochs):
    for (image, _) in loader:
      # Reshaping the image to (-1, 784)
      image = image.reshape(-1, 28*28)
      # Output of Autoencoder
      reconstructed = model(image)
      # Calculating the loss function
      loss = loss_function(reconstructed, image)
      # The gradients are set to zero,
      # the gradient is computed and stored.
      # .step() performs parameter update
      optimizer.zero_grad()
      loss.backward()
      optimizer.step()
      # Storing the losses in a list for plotting
      losses.append(loss)
    outputs.append((epochs, image, reconstructed))

我们首先遍历指定的 epoch 数量进行训练。在每个 epoch 中,我们遍历加载器,它提供图像数据批次及其相应的标签。我们不使用标签,因为 AE 是一个用于重建图像的网络,而不是学*图像显示的内容——从这个意义上说,它是一个无监督模型。

对于每张图像,我们通过将原始形状为(batch_size, 28, 28)的输入图像数据展平,形成一个形状为(batch_size, 784)的 2D 张量,其中每一行代表一个展平的图像。展平图像是在我们将像素的每一行连接起来创建一个大型向量时创建的。这是必需的,因为图像是二维的,而我们的张量输入需要是一维的。

然后,我们使用reconstructed = model(image)获取重建图像。一旦我们得到重建图像,我们可以计算均方误差(MSE)损失函数,并使用该信息来管理学*的下一步(optimizer.zero_grad())。在最后一行,我们将此信息添加到每次迭代的损失列表中,以便我们可以创建学*图。我们通过以下代码片段来完成:

# Defining the Plot Style
plt.style.use('seaborn')
plt.xlabel('Iterations')
plt.ylabel('Loss')
# Convert the list to a PyTorch tensor
losses_tensor = torch.tensor(losses)
plt.plot(losses_tensor.detach().numpy()[::-1])

这导致了学*图,如图 11.5所示:

图 11.5 – 从训练我们的 AE 得到的学*率图

图 11.5 – 从训练我们的 AE 得到的学*率图

学*率图显示 AE 还不是很好,我们应该再训练一段时间。然而,我们总是可以检查重建图像的外观。我们可以使用以下代码来做这件事:

for i, item in enumerate(image):
  # Reshape the array for plotting
  item = item.reshape(-1, 28, 28)
  plt.imshow(item[0])

代码生成了如图 11.6所示的输出:

图 11.6 – 我们 AE 重建的图像

图 11.6 – 我们 AE 重建的图像

尽管学*率是 OK 的,我们仍然可以从我们的 AE 中获得非常好的结果。

最佳实践#58

除了监控损失,确保可视化生成的实际结果。

监控损失函数是理解 AE 何时稳定的好方法。然而,仅仅损失函数是不够的。我通常绘制实际输出以了解 AE 是否已经正确训练。

最后,我们可以使用此代码可视化学*过程:

yhat = model(image[0])
make_dot(yhat,
         params=dict(list(model.named_parameters())),
         show_attrs=True,
         show_saved=True)

此代码可视化整个网络的学*过程。它创建了一个大图像,我们只能显示其中的一小部分。图 11.7显示了这一部分:

图 11.7 – 训练 AE 的前三个步骤,以 AE 架构的形式可视化

图 11.7 – 训练 AE 的前三个步骤,以 AE 架构的形式可视化

我们甚至可以使用以下代码以文本形式可视化整个架构:

from torchsummary import summary
summary(model, (1, 28 * 28))

这导致了以下模型:

----------------------------------------------------------------
        Layer (type)               Output Shape         Param #
================================================================
            Linear-1               [-1, 1, 128]         100,480
              ReLU-2               [-1, 1, 128]               0
            Linear-3                [-1, 1, 64]           8,256
              ReLU-4                [-1, 1, 64]               0
            Linear-5                [-1, 1, 36]           2,340
              ReLU-6                [-1, 1, 36]               0
            Linear-7                [-1, 1, 18]             666
              ReLU-8                [-1, 1, 18]               0
            Linear-9                 [-1, 1, 9]             171
           Linear-10                [-1, 1, 18]             180
             ReLU-11                [-1, 1, 18]               0
           Linear-12                [-1, 1, 36]             684
             ReLU-13                [-1, 1, 36]               0
           Linear-14                [-1, 1, 64]           2,368
             ReLU-15                [-1, 1, 64]               0
           Linear-16               [-1, 1, 128]           8,320
             ReLU-17               [-1, 1, 128]               0
           Linear-18               [-1, 1, 784]         101,136
          Sigmoid-19               [-1, 1, 784]               0
================================================================
Total params: 224,601
Trainable params: 224,601
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.00
Forward/backward pass size (MB): 0.02
Params size (MB): 0.86
Estimated Total Size (MB): 0.88
----------------------------------------------------------------

瓶颈层用粗体表示,以说明编码和解码部分是如何相互连接的。

开发安全笼以防止模型破坏整个系统

随着像 MLM 和 AE 这样的 GenAI 系统创建新的内容,存在它们生成的内容可能会破坏整个软件系统或变得不道德的风险。

因此,软件工程师经常使用安全笼的概念来保护模型本身免受不适当的输入和输出。对于像 RoBERTa 这样的 MLM,这可能是一个简单的预处理程序,用于检查生成的内容是否存在问题。从概念上讲,这如图 11**.8 所示:

图 11.8 – MLM 的安全笼概念

图 11.8 – MLM 的安全笼概念

wolfBERTa 模型的例子中,这可能意味着我们检查生成的代码是否不包含网络安全漏洞,这些漏洞可能允许黑客接管我们的系统。这意味着由 wolfBERTa 模型生成的所有程序都应该使用 SonarQube 或 CodeSonar 等工具进行检查,以查找网络安全漏洞,因此我的下一个最佳实践。

最佳实践 #59

检查 GenAI 模型的输出,以确保它不会破坏整个系统或提供不道德的回应。

我建议创建这样的安全笼,应从系统的需求开始。第一步是了解系统将要做什么,以及这项任务涉及哪些危险和风险。安全笼的输出处理器应确保这些危险情况不会发生,并且得到妥善处理。

一旦我们了解了如何预防危险,我们就可以转向思考如何在语言模型层面上预防这些风险。例如,当我们训练模型时,我们可以选择已知是安全的且不包含安全漏洞的代码。尽管这并不能保证模型生成的代码是安全的,但它确实降低了风险。

摘要

在本章中,我们学*了如何训练高级模型,并看到它们的训练并不比训练第十章中描述的经典 ML 模型更困难。尽管我们训练的模型比第十章中的模型复杂得多,但我们可以使用相同的原则,并将此类活动扩展到训练更复杂的模型。

我们专注于以 BERT 模型(基础 GPT 模型)和 AEs 形式存在的 GenAI。训练这些模型并不困难,我们不需要巨大的计算能力来训练它们。我们的wolfBERTa模型大约有 8000 万个参数,这看起来很多,但真正优秀的模型,如 GPT-3,有数十亿个参数——GPT-3 有 1750 亿个参数,NVIDIA Turing 有超过 3500 亿个参数,而 GPT-4 是 GPT-3 的 1000 倍大。训练过程是相同的,但我们需要超级计算架构来训练这些模型。

我们还了解到,这些模型只是更大软件系统的一部分。在下一章中,我们将学*如何创建这样一个更大的系统。

参考文献

  • Kratsch, W. 等人,《Machine learning in business process monitoring: a comparison of deep learning and classical approaches used for outcome prediction. Business & Information Systems Engineering》,2021 年,第 63 卷: p. 261-276.

  • Vaswani, A. 等人,《Attention is all you need. Advances in neural information processing systems》,2017 年,第 30 卷。

  • Aggarwal, A.,M. Mittal,和 G. Battineni,《Generative adversarial network: An overview of theory and applications. International Journal of Information Management Data Insights》,2021 年,第 1 卷: p. 100004.

  • Creswell, A. 等人,《Generative adversarial networks: An overview. IEEE signal processing magazine》,2018 年,第 35 卷(第 1 期): p. 53-65.

第十二章:设计机器学*流水线(MLOps)及其测试

MLOps,即机器学*(ML)运维,是一套旨在简化机器学*模型在生产环境中部署、管理和监控的实践和技术。它借鉴了 DevOps(开发和运维)方法的概念,将其适应机器学*所面临的独特挑战。

MLOps 的主要目标是弥合数据科学团队和运维团队之间的差距,促进协作,并确保机器学*项目能够有效地和可靠地在规模上部署。MLOps 有助于自动化和优化整个机器学*生命周期,从模型开发到部署和维护,从而提高生产中机器学*系统的效率和效果。

在本章中,我们学*如何在实践中设计和操作机器学*系统。本章展示了如何将流水线转化为软件系统,重点关注在 Hugging Face 上测试机器学*流水线和它们的部署。

在本章中,我们将介绍以下主要主题:

  • 什么是机器学*流水线

  • 机器学*流水线 – 如何在实际系统中使用机器学*

  • 基于原始数据的流水线

  • 基于特征的流水线

  • 机器学*流水线的测试

  • 监控运行时的机器学*系统

什么是机器学*流水线

毫无疑问,在过去的几年里,机器学*领域取得了显著的进步,颠覆了行业并赋予了创新应用以力量。随着对更复杂和精确的模型的需求增长,开发和有效部署它们的复杂性也在增加。机器学*系统的工业应用需要对这些基于机器学*的系统进行更严格测试和验证。为了应对这些挑战,机器学*流水线的概念应运而生,成为简化整个机器学*开发过程的关键框架,从数据预处理和特征工程到模型训练和部署。本章探讨了 MLOps 在尖端深度学*DL)模型如生成预训练转换器GPT)和传统经典机器学*模型中的应用。

我们首先探讨机器学*流水线的潜在概念,强调其在组织机器学*工作流程和促进数据科学家与工程师之间协作的重要性。我们综合了前几章中提出的许多知识——数据质量评估、模型推理和监控。

接下来,我们将讨论构建 GPT 模型及其类似模型的流水线的独特特性和考虑因素,利用它们的预训练特性来处理广泛的语言任务。我们探讨了在特定领域数据上微调 GPT 模型的复杂性以及将它们纳入生产系统的挑战。

在探索了 GPT 管道之后,我们将注意力转向经典机器学*模型,检查特征工程过程及其在从原始数据中提取相关信息中的作用。我们深入研究传统机器学*算法的多样化领域,了解何时使用每种方法,以及在不同场景中的权衡。

最后,我们展示了如何测试机器学*管道,并强调模型评估和验证在评估性能和确保生产环境中的鲁棒性方面的重要性。此外,我们探讨了模型监控和维护的策略,以防止概念漂移并保证持续的性能改进。

机器学*管道

机器学*(ML)管道是一个系统化和自动化的过程,它组织了机器学*工作流程的各个阶段。它包括准备数据、训练机器学*模型、评估其性能以及将其部署到实际应用中的步骤。机器学*管道的主要目标是简化端到端的机器学*过程,使其更加高效、可重复和可扩展。

机器学*管道通常包括以下基本组件:

  • 数据收集、预处理和整理:在这个初始阶段,从各种来源收集相关数据,并准备用于模型训练。数据预处理涉及清理、转换和归一化数据,以确保数据适合机器学*算法。

  • 特征工程和选择:特征工程涉及从原始数据中选择和创建有助于模型学*模式和做出准确预测的相关特征(输入变量)。适当的特征选择对于提高模型性能和减少计算开销至关重要。

  • 模型选择和训练:在这个阶段,选择一个或多个机器学*算法,并在准备好的数据上训练模型。模型训练涉及学*数据中的潜在模式和关系,以进行预测或分类。

  • 模型评估和验证:使用准确率、精确率、召回率、F1 分数等指标来评估训练好的模型在未见数据上的性能。通常使用交叉验证技术来确保模型的一般化能力。

  • 超参数调整:许多机器学*算法都有超参数,这些是可以调整的参数,控制模型的行为。超参数调整涉及找到这些参数的最佳值,以提高模型性能。

  • 模型部署:一旦模型经过训练和验证,它就会被部署到生产环境中,在那里它可以在新的、未见过的数据上进行预测。模型部署可能涉及将模型集成到现有的应用程序或系统中。

  • 模型监控和维护:部署后,持续监控模型的性能,以检测任何性能问题或漂移。定期的维护可能包括使用新数据重新训练模型,以确保其保持准确性和时效性。

机器学*管道为管理机器学*项目的复杂性提供了一个结构化框架,使数据科学家和工程师能够更有效地协作,并确保模型可以可靠且高效地开发和部署。它促进了可重复性、可扩展性和实验的简便性,促进了高质量机器学*解决方案的开发。图 12.1展示了机器学*管道的概念模型,我们在第二章中介绍了它。

图 12.1 – 机器学*管道:概念概述

图 12.1 – 机器学*管道:概念概述

我们在之前的章节中介绍了蓝色阴影元素的要素,在这里,我们主要关注尚未涉及的部分。然而,在我们深入探讨这个管道的技术要素之前,让我们先介绍 MLOps 的概念。

MLOps 的要素

MLOps 的主要目标是弥合数据科学和运维团队之间的差距,因此 MLOps 自动化并优化了整个机器学*生命周期,从模型开发到部署和维护,从而提高了生产中机器学*系统的效率和效果。

MLOps 中的关键组件和实践包括:

  • 版本控制:应用版本控制系统(VCSs)如 Git 来管理和跟踪机器学*代码、数据集和模型版本的变更。这使协作、可重复性和模型改进的跟踪变得容易。

  • 持续集成和持续部署(CI/CD):利用 CI/CD 管道来自动化机器学*模型的测试、集成和部署。这有助于确保代码库的更改能够无缝部署到生产中,同时保持高质量标准。

  • 模型打包:为机器学*模型创建标准化的、可重复的、可共享的容器或包,使其在不同环境中一致部署变得更容易。

  • 模型监控:实施监控和日志记录解决方案,以实时跟踪模型的性能和行为。这有助于早期发现问题并确保模型的持续可靠性。

  • 可扩展性和基础设施管理:设计和管理底层基础设施以支持生产中机器学*模型的需求,确保它们能够处理增加的工作负载并高效扩展。

  • 模型治理和合规性:实施流程和工具以确保在部署和使用机器学*模型时符合法律和伦理要求、隐私法规和公司政策。

  • 协作与沟通:促进数据科学家、工程师以及其他参与 ML 部署流程的利益相关者之间的有效沟通和协作。

通过采用 MLOps 原则,组织可以在保持模型在实际应用中的可靠性和有效性的同时,加速 ML 模型的开发和部署。这也有助于降低部署失败的风险,并在数据科学和运营团队中促进协作和持续改进的文化。

ML 管道 – 如何在实际系统中使用 ML

在本地平台上训练和验证 ML 模型是使用 ML 管道的过程的开始。毕竟,如果我们不得不在客户的每一台计算机上重新训练 ML 模型,那么这将非常有限。

因此,我们通常将 ML 模型部署到模型仓库中。有几个流行的仓库,但使用最大社区的是 HuggingFace 仓库。在那个仓库中,我们可以部署模型和数据集,甚至创建模型可以用于实验的空间,而无需下载它们。让我们将训练好的模型部署到第十一章中的那个仓库。为此,我们需要在 huggingface.com 上有一个账户,然后我们就可以开始了。

将模型部署到 HuggingFace

首先,我们需要使用主页上的新建按钮创建一个新的模型,如图图 12.2所示:

图 12.2 – 创建模型的新按钮

图 12.2 – 创建模型的新按钮

然后,我们填写有关我们模型的信息,为其创建空间。图 12.3展示了这个过程的一个截图。在表单中,我们填写模型的名称、是否为私有或公共,并为它选择一个许可证。在这个例子中,我们选择了 MIT 许可证,这是一个非常宽松的许可证,允许每个人只要包含 MIT 许可证文本,就可以使用、重新使用和重新分发模型:

图 12.3 – 模型元数据卡片

图 12.3 – 模型元数据卡片

一旦模型创建完成,我们就可以开始部署模型了。空余空间看起来就像图 12.4中的那样:

图 12.4 – 空余模型空间

图 12.4 – 空余模型空间

顶部菜单包含四个选项,但前两个是最重要的 – 模型卡片文件和版本。模型卡片是对模型的简要描述。它可以包含任何类型的信息,但最常见的信息是模型的使用方法。我们遵循这个惯例,并按照图 12.5所示准备模型卡片:

图 12.5 – 我们 wolfBERTa 模型卡片的开头

图 12.5 – 我们 wolfBERTa 模型卡片的开头

最佳实践 #60

模型卡片应包含有关模型如何训练、如何使用它、它支持哪些任务以及如何引用模型的信息。

由于 HuggingFace 是一个社区,因此正确记录创建的模型并提供有关模型如何训练以及它们能做什么的信息非常重要。因此,我的最佳实践是将所有这些信息包含在模型卡片中。许多模型还包括有关如何联系作者以及模型在训练之前是否已经预训练的信息。

一旦模型卡片准备就绪,我们就可以转到Readme.txt(即模型卡片),并可以添加实际的模型文件(见图 12.6):

图 12.6 – 模型的文件和版本;我们可以在右上角使用“添加文件”按钮添加模型

图 12.6 – 模型的文件和版本;我们可以在右上角使用“添加文件”按钮添加模型

一旦我们点击wolfBERTa子文件夹。该文件夹包含以下文件:

Mode                 LastWriteTime                     Name
------        --------------------        -----------------
d----l        2023-07-01     10:25        checkpoint-340000
d----l        2023-07-01     10:25        checkpoint-350000
-a---l        2023-06-27     21:30        config.json
-a---l        2023-06-27     17:55        merges.txt
-a---l        2023-06-27     21:30        pytorch_model.bin
-a---l        2023-06-27     21:30        training_args.bin
-a---l        2023-06-27     17:55        vocab.json

前两个条目是模型检查点;即我们在训练过程中保存的模型版本。这两个文件夹对于部署来说并不重要,因此将被忽略。其余的文件应复制到 HuggingFace 上新建的模型仓库中。

模型上传后,应该看起来像图 12.7中展示的那样:

图 12.7 – 上传到 HuggingFace 仓库的模型

图 12.7 – 上传到 HuggingFace 仓库的模型

在此之后,该模型就准备好供社区使用了。我们还可以为社区创建一个推理 API,以便他们快速测试我们的模型。一旦我们回到模型卡片菜单,在托管推理 API部分(图 12.8的右侧)就会自动提供给我们:

图 12.8 – 为我们的模型自动提供的托管推理 API

图 12.8 – 为我们的模型自动提供的托管推理 API

当我们输入int HTTP_get(<mask>)时,我们要求模型为该函数提供输入参数。结果显示,最可能的标记是void,其次是int标记。这两个都是相关的,因为它们是参数中使用的类型,但它们可能不会使这个程序编译,因此我们需要开发一个循环,预测程序中的不止一个标记。可能还需要更多的训练。

现在,我们有一个完全部署的模型,可以在其他应用中使用而无需太多麻烦。

从 HuggingFace 下载模型

我们已经看到了如何从 HuggingFace 下载模型,但为了完整性,让我们看看如何为wolfBERTa模型执行此操作。本质上,我们遵循模型卡片并使用以下 Python 代码片段:

from transformers import pipeline
unmasker = pipeline('fill-mask', model='mstaron/wolfBERTa')
unmasker("Hello I'm a <mask> model.")

此代码片段下载模型并使用 unmasker 接口通过 fill-mask 管道进行推理。该管道允许您输入一个带有 <mask> 掩码标记的句子,模型将尝试预测最适合填充掩码位置的单词。此代码片段中的三行代码执行以下操作:

  • from transformers import pipeline: 这行代码从 transformers 库中导入管道函数。管道函数简化了使用预训练模型进行各种自然语言处理NLP)任务的过程。

  • unmasker = pipeline('fill-mask', model='mstaron/wolfBERTa'): 这行代码为任务创建了一个名为 unmasker 的新管道。该管道将使用预训练的 wolfBERTa 模型。

  • unmasker("Hello I'm a <mask> model."): 这行代码利用 unmasker 管道来预测最适合给定句子中掩码位置的单词。<mask> 标记表示模型应尝试填充单词的位置。

当执行此行代码时,管道将调用 wolfBERTa 模型,并根据提供的句子进行预测。该模型将在 <mask> 标记的位置预测最佳单词以完成句子。

可以以非常相似的方式使用其他模型。像 HuggingFace 这样的社区模型中心的主要优势是它提供了一种统一管理模型和管道的绝佳方式,并允许我们在软件产品中快速交换模型。

基于原始数据的管道

创建完整的管道可能是一项艰巨的任务,需要为所有模型和所有类型的数据创建定制工具。它允许我们优化模型的使用方式,但需要付出大量努力。管道背后的主要理念是将机器学*的两个领域——模型及其计算能力与任务和领域数据联系起来。幸运的是,对于像 HuggingFace 这样的主要模型中心,它们提供了一个 API,可以自动提供机器学*管道。HuggingFace 中的管道与模型相关,并由基于模型架构、输入和输出的框架提供。

与自然语言处理相关的管道

文本分类是一个管道,旨在将文本输入分类到预定义的类别或类别中。它特别适用于情感分析SA)、主题分类、垃圾邮件检测、意图识别等任务。该管道通常采用针对不同分类任务在特定数据集上微调的预训练模型。我们在本书的第一部分使用机器学*进行代码审查的情感分析时,已经看到了类似的功能。

下面的代码片段提供了一个示例:

from transformers import pipeline
# Load the text classification pipeline
classifier = pipeline("text-classification")
# Classify a sample text
result = classifier("This movie is amazing and highly recommended!")
print(result)

代码片段显示,实际上我们需要实例化管道的代码有两行(粗体显示),正如我们之前所见。

文本生成是另一个允许使用预训练语言模型(如 GPT-3)根据提供的提示或种子文本生成文本的流程。它能够为各种应用生成类似人类的文本,例如聊天机器人、创意写作、问答QA)等。

以下代码片段展示了这样的一个示例:

from transformers import pipeline
# Load the text generation pipeline
generator = pipeline("text-generation")
# Generate text based on a prompt
prompt = "In a galaxy far, far away… "
result = generator(prompt, max_length=50, num_return_sequences=3)
for output in result:
    print(output['generated_text'])

摘要是设计用于将较长的文本总结为较短、连贯的摘要的流程。它利用了在大型数据集上针对摘要任务进行训练的基于 transformer 的模型。以下代码片段展示了该流程的示例:

from transformers import pipeline
# Load the summarization pipeline
summarizer = pipeline("summarization")
# Summarize a long article
article = """
In a groundbreaking discovery, scientists have found a new species of dinosaur in South America. The dinosaur, named "Titanus maximus," is estimated to have been the largest terrestrial creature to ever walk the Earth. It belonged to the sauropod group of dinosaurs, known for their long necks and tails. The discovery sheds new light on the diversity of dinosaurs that once inhabited our planet.
"""
result = summarizer(article, max_length=100, min_length=30, do_sample=False)
print(result[0]['summary_text'])

HuggingFace 的transformers API 中还有更多流程,所以我鼓励您查看这些流程。然而,我关于流程的最佳实践是这样的:

最佳实践 #61

尝试不同的模型以找到最佳流程。

由于 API 为类似模型提供了相同的流程,因此更改模型或其版本相当简单。因此,我们可以基于具有类似(但不同)功能(但不是相同)的模型创建产品,并同时训练模型。

图像流程

图像处理流程专门设计用于与图像处理相关的任务。HuggingFace hub 包含这些流程中的几个,以下是一些最受欢迎的。

图像分类专门设计用于将图像分类到特定类别。这与可能是最广为人知的任务相同——将图像分类为“猫”、“狗”或“车”。以下代码示例(来自 HuggingFace 教程)展示了图像分类流程的使用:

from transformers import pipeline
# first, create an instance of the image classification pipeline for the selected model
classifier = pipeline(model="microsoft/beit-base-patch16-224-pt22k-ft22k")
# now, use the pipeline to classify an image
classifier("https://huggingface.co/datasets/Narsil/image_dummy/raw/main/parrots.png")

前面的代码片段表明,创建图像分类流程与创建文本分析任务的流程一样容易(如果不是更容易)。

当我们想要向图像添加所谓的语义地图时,会使用图像分割流程(见图 12.9):

图 12.9 – 图像的语义地图,与我们第三章中看到的一样

图 12.9 – 图像的语义地图,与我们第三章中看到的一样第三章

下一个示例代码片段(同样来自 HuggingFace 教程)展示了包含此类流程的示例代码:

from transformers import pipeline
segmenter = pipeline(model="facebook/detr-resnet-50-panoptic")
segments = segmenter("https://huggingface.co/datasets/Narsil/image_dummy/raw/main/parrots.png")
segments[0]["label"]

前面的代码片段创建了一个图像分割流程,使用它并将结果存储在segments列表中。列表的最后一行打印出第一个分割的标签。使用segments[0]["mask"].size语句,我们可以接收到图像地图的像素大小。

目标检测流程用于需要识别图像中预定义类别对象的任务。我们已经在第三章中看到了这个任务的示例。此类流程的代码看起来与前几个非常相似:

from transformers import pipeline
detector = pipeline(model="facebook/detr-resnet-50")
detector("https://huggingface.co/datasets/Narsil/image_dummy/raw/main/parrots.png")

执行此代码将创建一个包含图像中检测到的对象的边界框列表,以及其边界框。我在使用管道处理图像方面的最佳实践与语言任务相同。

基于特征的管道

基于特征的管道没有特定的类,因为它们处于更低级别。它们是标准 Python 机器学*实现中的 model.fit()model.predict() 语句。这些管道要求软件开发者手动准备数据,并手动处理结果;也就是说,通过实现预处理步骤,如使用独热编码将数据转换为表格,以及后处理步骤,如将数据转换为人类可读的输出。

这种管道的一个例子是预测书中前几部分中看到的缺陷;因此,它们不需要重复。

然而,重要的是,所有管道都是将机器学*领域与软件工程领域联系起来的方式。我在开发管道后的第一个活动就是对其进行测试。

机器学*管道的测试

机器学*管道的测试在多个层面上进行,从单元测试开始,然后向上发展到集成(组件)测试,最后到系统测试和验收测试。在这些测试中,有两个元素很重要——模型本身和数据(对于模型和预言机)。

虽然我们可以使用 Python 内置的单元测试框架,但我强烈推荐使用 Pytest 框架,因为它简单灵活。我们可以通过以下命令安装此框架:

>> pip install pytest

这将下载并安装所需的包。

最佳实践 #62

使用像 Pytest 这样的专业测试框架。

使用专业框架为我们提供了 MLOps 原则所需的兼容性。我们可以共享我们的模型、数据、源代码以及所有其他元素,而无需繁琐地设置和安装框架本身。对于 Python,我推荐使用 Pytest 框架,因为它广为人知,被广泛使用,并且得到一个庞大社区的支持。

这里是一个下载模型并为其测试做准备代码片段:

# import json to be able to read the embedding vector for the test
import json
# import the model via the huggingface library
from transformers import AutoTokenizer, AutoModelForMaskedLM
# load the tokenizer and the model for the pretrained SingBERTa
tokenizer = AutoTokenizer.from_pretrained('mstaron/SingBERTa')
# load the model
model = AutoModelForMaskedLM.from_pretrained("mstaron/SingBERTa")
# import the feature extraction pipeline
from transformers import pipeline
# create the pipeline, which will extract the embedding vectors
# the models are already pre-defined, so we do not need to train anything here
features = pipeline(
    "feature-extraction",
    model=model,
    tokenizer=tokenizer,
    return_tensor = False
)

这段代码用于加载和设置预训练的语言模型,特别是 SingBERTa 模型,使用 Hugging Face 的 transformers 库。它包含以下元素:

  1. transformers 库导入必要的模块:

    1. AutoTokenizer:这个类用于自动选择适合预训练模型的适当分词器。

    2. AutoModelForMaskedLM:这个类用于自动选择适合 掩码语言模型MLM)任务的适当模型。

  2. 加载预训练的 SingBERTa 模型的分词器和模型:

    1. tokenizer = AutoTokenizer.from_pretrained('mstaron/SingBERTa'):这一行从 Hugging Face 模型库中加载预训练的 SingBERTa 模型的分词器。

    2. model = AutoModelForMaskedLM.from_pretrained("mstaron/SingBERTa"):这一行加载预训练的SingBERTa模型。

  3. 导入特征提取管道:

    1. from transformers import pipeline:这一行从transformers库中导入管道类,这使得我们能够轻松地为各种 NLP 任务创建管道。
  4. 创建特征提取管道:

    1. features = pipeline("feature-extraction", model=model, tokenizer=tokenizer, return_tensor=False):这一行创建一个用于特征提取的管道。该管道使用之前加载的预训练模型和分词器从输入文本中提取嵌入向量。return_tensor=False参数确保输出将以非张量格式(可能是 NumPy 数组或 Python 列表)返回。

使用这个设置,你现在可以使用features管道从文本输入中提取嵌入向量,而无需进行任何额外的训练,使用预训练的SingBERTa模型。我们之前已经看到过这个模型的使用,所以在这里,让我们专注于它的测试。以下代码片段是一个测试用例,用于检查模型是否已正确下载并且准备好使用:

def test_features():
    # get the embeddings of the word "Test"
    lstFeatures = features("Test")
    # read the oracle from the json file
    with open('test.json', 'r') as f:
        lstEmbeddings = json.load(f)
    # assert the embeddings and the oracle are the same
    assert lstFeatures[0][0] == lstEmbeddings

这个代码片段定义了一个test_features()测试函数。该函数的目的是通过将管道从之前代码片段中创建的特征提取管道获得的单词"Test"的嵌入与存储在名为'test.json'的 JSON 文件中的预期嵌入进行比较来测试特征提取管道的正确性。该文件的内容是我们的预言,它是一个包含大量数字的大向量,我们用它来与实际模型输出进行比较:

  • lstFeatures = features("Test"):这一行使用之前定义的features管道提取单词"Test"的嵌入。features管道是使用预训练的SingBERTa模型和分词器创建的。该管道将输入"Test"通过分词器处理,然后通过模型,并返回嵌入向量作为lstFeatures

  • with open('test.json', 'r') as f::这一行使用上下文管理器(with语句)以读取模式打开'test.json'文件。

  • lstEmbeddings = json.load(f):这一行读取'test.json'文件的内容,并将其内容加载到lstEmbeddings变量中。该 JSON 文件应包含表示预期嵌入的单词"Test"的嵌入向量的列表。

  • assert lstFeatures[0][0] == lstEmbeddings:这一行执行断言以检查从管道获得的嵌入向量(lstFeatures[0][0])是否等于从 JSON 文件中获得的预期嵌入向量(预言)。通过检查两个列表中相同位置的元素是否相同来进行比较。

如果断言为true(即管道提取的嵌入向量与 JSON 文件中预期的向量相同),则测试将通过而没有任何输出。然而,如果断言为false(即嵌入不匹配),则测试框架(Pytest)将此测试用例标记为失败。

为了执行测试,我们可以在与我们的项目相同的目录中编写以下语句:

>> pytest

在我们的情况下,这导致以下输出(为了简洁起见,已省略):

=================== test session starts ===================
platform win32 -- Python 3.11.4, pytest-7.4.0, pluggy-1.2.0
rootdir: C:\machine_learning_best_practices\chapter_12
plugins: anyio-3.7.0
collected 1 item
chapter_12_download_model_test.py .                 [100%]
====================== 1 passed in 4.17s ==================

这个片段显示,框架找到了一个测试用例(收集了 1 个条目)并执行了它。它还说明该测试用例在 4.17 秒内通过。

因此,接下来是我的下一个最佳实践。

最佳实践 #63

根据您的训练数据设置测试基础设施。

由于模型本质上是概率性的,因此最好基于训练数据来测试模型。这里,我的意思不是我们在机器学*(ML)的意义上测试性能,比如准确性。我的意思是测试模型是否真正工作。通过使用与训练相同的相同数据,我们可以检查模型对之前使用的数据的推理是否正确。因此,我指的是在软件工程意义上这个词的测试。

现在,类似于之前提出的语言模型,我们可以使用类似的方法来测试一个经典 ML 模型。有时这被称为零表测试。在这个测试中,我们使用只有一个数据点的简单数据来测试模型的预测是否正确。以下是设置此类测试的方法:

# import the libraries pandas and joblib
import pandas as pd
import joblib
# load the model
model = joblib.load('./chapter_12_decision_tree_model.joblib')
# load the data that we used for training
dfDataAnt13 = pd.read_excel('./chapter_12.xlsx',
                            sheet_name='ant_1_3',
                            index_col=0)

这段代码片段使用joblib库加载一个 ML 模型。在这种情况下,它是我们训练经典 ML 模型时在第十章中使用的模型。它是一个决策树模型。

然后,程序读取我们用于训练模型的相同数据集,以确保数据的格式与训练数据集完全相同。在这种情况下,我们可以期望得到与训练数据集相同的结果。对于更复杂的模型,我们可以在模型训练后直接进行一次推理,在保存模型之前创建这样的表。

现在,我们可以在以下代码片段中定义三个测试用例:

# test that the model is not null
# which means that it actually exists
def test_model_not_null():
    assert model is not None
# test that the model predicts class 1 correctly
# here correctly means that it predicts the same way as when it was trained
def test_model_predicts_class_correctly():
    X = dfDataAnt13.drop(['Defect'], axis=1)
    assert model.predict(X)[0] == 1
# test that the model predicts class 0 correctly
# here correctly means that it predicts the same way as when it was trained
def test_model_predicts_class_0_correctly():
    X = dfDataAnt13.drop(['Defect'], axis=1)
    assert model.predict(X)[1] == 0

第一个测试函数(test_model_not_null)检查model变量,该变量预期将包含训练好的 ML 模型,是否不是null。如果模型是null(即不存在),则assert语句将引发异常,表示测试失败。

第二个测试函数(test_model_predicts_class_correctly)检查模型是否正确预测了给定数据集的类别 1。它是通过以下方式完成的:

  • 通过从dfDataAnt13 DataFrame 中删除'Defect'列来准备X输入特征,假设'Defect'是目标列(类别标签)。

  • 使用训练好的模型(model.predict(X))对X输入特征进行预测。

  • 断言第一次预测(model.predict(X)[0])应该等于 1(类别 1)。如果模型正确预测类别 1,则测试通过;否则,将引发异常,表示测试失败。

第三个测试用例(test_model_predicts_class_0_correctly)检查模型是否正确预测给定数据集的类别 0。它遵循与上一个测试类似的过程:

  • 通过从dfDataAnt13 DataFrame 中删除'Defect'列来准备X输入特征。

  • 使用训练好的模型(model.predict(X))对X输入特征进行预测。

  • 断言第二次预测(model.predict(X)[1])应该等于 0(类别 0)。如果模型正确预测类别 0,则测试通过;否则,将引发异常,表示测试失败。

这些测试验证了训练模型的完整性和正确性,并确保它在给定的数据集上按预期执行。以下是执行测试的输出:

=============== test session starts =======================
platform win32 -- Python 3.11.4, pytest-7.4.0, pluggy-1.2.0
rootdir: C:\machine_learning_best_practices\chapter_12
plugins: anyio-3.7.0
collected 4 items
chapter_12_classical_ml_test.py                      [ 75%]
chapter_12_download_model_test.py                    [100%]
================ 4 passed in 12.76s =======================

Pytest 框架找到了我们所有的测试,并显示其中三个(四个中的三个)位于chapter_12_classical_ml_test.py文件中,另一个位于chapter_12_downloaded_model_test.py文件中。

因此,我的下一个最佳实践是:

最佳实践#64

将模型视为单元并相应地为它们准备单元测试。

我建议将 ML 模型视为单元(与模块相同)并为其使用单元测试实践。这有助于减少模型概率性质的影响,并为我们提供了检查模型是否正确工作的可能性。它有助于在之后调试整个软件系统。

监控 ML 系统运行时

监控生产中的管道是 MLOps 的关键方面,以确保部署的 ML 模型的表现、可靠性和准确性。这包括几个实践。

第一个实践是记录和收集指标。这项活动包括在 ML 代码中添加日志语句,以在模型训练和推理期间捕获相关信息。要监控的关键指标包括模型准确性、数据漂移、延迟和吞吐量。流行的日志和监控框架包括 Prometheus、Grafana 以及Elasticsearch、Logstash 和 KibanaELK)。

第二个是警报,它基于预定义的关键指标阈值设置警报。这有助于在生产管道中主动识别问题或异常。当警报被触发时,适当的团队成员可以被通知进行调查并迅速解决问题。

数据漂移检测是第三项活动,包括监控输入数据的分布以识别数据漂移。数据漂移指的是数据分布随时间的变化,这可能会影响模型性能。

第四个活动是性能监控,MLOps 团队持续跟踪已部署模型的性能。他们测量推理时间、预测准确度以及其他相关指标,并监控性能下降,这可能由于数据、基础设施或依赖关系的变化而引起。

除了这四个主要活动之外,MLOps 团队还有以下职责:

  • 错误分析:使用工具分析和记录推理过程中遇到的错误,并理解错误的本质,可以帮助改进模型或识别数据或系统中的问题。

  • 模型版本控制:跟踪模型版本及其随时间的变化性能,并在最新部署出现问题时(如果需要)回滚到之前的版本。

  • 环境监控:使用诸如 CPU/内存利用率、网络流量等关键绩效指标监控模型部署的基础设施和环境,寻找性能瓶颈。

  • 安全和合规性:确保部署的模型遵守安全和合规性标准,并监控访问日志和任何可疑活动。

  • 用户反馈:收集、分析和将用户反馈融入监控和推理过程中。MLOps 从最终用户那里征求反馈,以从现实世界的角度了解模型的性能。

通过有效地监控管道,MLOps 可以迅速响应任何出现的问题,提供更好的用户体验,并维护 ML 系统的整体健康。然而,监控所有上述方面相当费力,并非所有 MLOps 团队都有资源去做。因此,我在本章的最后一条最佳实践是:

最佳实践#65

识别 ML 部署的关键方面,并相应地监控这些方面。

虽然这听起来很简单,但并不总是容易识别关键方面。我通常从优先监控基础设施和日志记录以及收集指标开始。监控基础设施很重要,因为任何问题都会迅速传播到客户那里,导致失去信誉,甚至业务损失。监控指标和日志记录为 ML 系统的运行提供了深刻的洞察,并防止了许多 ML 系统生产中的问题。

摘要

构建 ML 管道是本书专注于 ML 核心技术方面的部分。管道对于确保 ML 模型按照软件工程的最佳实践使用至关重要。

然而,ML 管道仍然不是一个完整的 ML 系统。它们只能提供数据的推理并提供输出。为了使管道有效运行,它们需要连接到系统的其他部分,如用户界面和存储。这就是下一章的内容。

参考文献

  • A. LimaL. Monteiro,和 A.P. FurtadoMLOps:实践、成熟度模型、角色、工具和挑战的系统文献综述ICEIS (1),2022: p. 308-320

  • John, M.M.Olsson, H.H.,和 Bosch, J.迈向 MLOps:一个框架和成熟度模型。在2021 年第 47 届欧姆尼微软件工程和高级应用会议(SEAA)2021IEEE

  • Staron, M. et al., 从演化的测量系统到自愈系统以提高可用性的工业经验软件:实践与经验201848(3)p. 719-739

第十三章:设计和实现大规模、健壮的机器学*软件

到目前为止,我们已经学*了如何开发机器学*模型,如何处理数据,以及如何创建和测试整个机器学*管道。剩下的是学*如何将这些元素集成到用户界面UI)中,以及如何部署它,以便它们可以在不编程的情况下使用。为此,我们将学*如何部署包含 UI 和数据存储的模型。

在本章中,我们将学*如何将机器学*模型与使用 Gradio 编写的图形用户界面以及数据库中的存储集成。我们将使用两个机器学*管道的示例——一个是从我们之前章节中预测缺陷的模型示例,以及一个从自然语言提示创建图片的生成式 AI 模型。

本章将涵盖以下主要内容:

  • 机器学*并非孤立存在——基于机器学*的部署系统元素

  • 机器学*模型的 UI

  • 数据存储

  • 部署用于数值数据的机器学*模型

  • 部署用于图像的生成式机器学*模型

  • 将代码补全模型作为 Visual Studio Code 的扩展部署

机器学*并非孤立存在

第二章介绍了机器学*系统的几个元素——存储、数据收集、监控和基础设施,仅举几个例子。我们需要所有这些来为用户部署模型,但并非所有这些对用户直接重要。我们需要记住,用户对结果感兴趣,但我们需要注意与这些系统开发相关的所有细节。这些活动通常被称为 AI 工程。

UI 很重要,因为它提供了访问我们模型的能力。根据我们软件的使用情况,界面可以不同。到目前为止,我们关注的是模型本身以及用于训练模型的数据。我们还没有关注模型的可用性以及如何将它们集成到工具中。

通过扩展,对于 UI,我们还需要讨论在机器学*中存储数据。我们可以使用逗号分隔值CSV)文件,但它们很快就会变得难以处理。它们要么太大,无法读入内存,要么过于繁琐,不适合版本控制和数据交换。

因此,在本章中,我们将专注于使机器学*系统可用。我们将学*如何开发 UI,如何将系统链接到数据库,以及如何设计一个能够完成 Python 代码的 Visual Studio Code 扩展。

机器学*模型的 UI

UI 作为连接机器学*算法的复杂性和与系统交互的最终用户之间的桥梁。它是用户可以输入数据、可视化结果、控制参数并从机器学*模型的输出中获得见解的交互式画布。一个设计良好的 UI 能够赋予用户,无论其技术专长如何,利用机器学*解决现实世界问题的潜力。

适用于机器学*应用的有效 UI 应优先考虑清晰度、可访问性和交互性。无论应用的目标用户是商业分析师、医疗保健专业人员还是研究人员,界面都应该适应用户的领域知识和目标。清晰传达模型的能力和限制至关重要,这有助于建立对技术的信任,并使用户能够根据其输出做出明智的决定。因此,我的下一个最佳实践。

最佳实践 #66

设计 ML 模型的 UI 时,应专注于用户任务。

我们可以使用不同类型的 UI,但大多数现代工具都围绕着两种类型——基于网页的界面(需要轻量级客户端)和扩展(提供现场改进)。ChatGPT 是 GPT-4 模型的基于网页界面的一个例子,而 GitHub CoPilot 是同一模型的扩展界面的一个例子。

在第一个例子中,让我们看看使用 Gradio 框架部署 ML 应用有多简单。一旦我们为我们的模型准备好了流程,我们只需要几行代码就能创建这个应用。以下是基于 Hugging Face 上存在的模型示例的代码行,用于文本分类:

import gradio as gr
from transformers import pipeline
pipe = pipeline("text-classification")
gr.Interface.from_pipeline(pipe).launch()

前两行导入必要的库——一个用于 UI(Gradio)和一个用于流程。第二行从 Hugging Face 导入默认文本分类流程,最后一行创建流程的 UI。UI 是一个带有输入和输出按钮的网站,如图 图 13.1 所示:

图 13.1 – 默认文本分类流程的 UI

图 13.1 – 默认文本分类流程的 UI

我们可以通过输入一些示例文本来测试它。通常,我们会在脚本中输入这些内容并提供某种分析,但这一切都是由 Gradio 框架为我们完成的。我们甚至不需要将流程的参数与 UI 的元素链接起来。

背后发生的事情可以通过观察控制台中的脚本输出(为了简洁而编辑)来解释:

No model was supplied, defaulted to distilbert-base-uncased-finetuned-sst-2-english and revision af0f99b
Using a pipeline without specifying a model name and revision in production is not recommended.
Downloading (…)lve/main/config.json: 100%|██████████████████| 629/629 [00:00<00:00, 64.6kB/s]
Downloading model.safetensors:
100%|████████████████| 268M/268M [00:04<00:00, 58.3MB/s]
Downloading (…)okenizer_config.json: 100%|████████████████| 48.0/48.0 [00:00<00:00, 20.7kB/s]
Downloading (…)solve/main/vocab.txt: 100%|████████████████| 232k/232k [00:00<00:00, 6.09MB/s]
Running on local URL:  http://127.0.0.1:7860

框架已下载默认模型、其分词器和词汇文件,然后在本地机器上创建了应用程序。

使用此应用的结果在 图 13.2 中展示。我们输入一些简单的文本,几乎瞬间就得到了其分类:

图 13.2 – 使用默认文本分类流程分析的数据

图 13.2 – 使用默认文本分类流程分析的数据

这种集成是首先部署模型并确保它们可以在不打开 Python 环境或类似环境的情况下使用的一种很好的方式。有了这个,我们就来到了我的下一个最佳实践。

最佳实践 #67

准备你的模型以进行网络部署。

无论你开发什么类型的模型,都尽量为它们准备网络部署。我们的模型可以被打包成 Docker 容器,作为微服务系统的一部分提供。使用 Gradio 是一个很好的例子,说明了如何实现这种网络部署。

数据存储

到目前为止,我们使用 CSV 文件和 Excel 文件来存储我们的数据。这是与 ML 一起工作的简单方法,但也是一种本地方法。然而,当我们想要扩展我们的应用程序并在我们的机器之外使用它时,通常更方便使用真正的数据库引擎。数据库在 ML 流水线中扮演着至关重要的角色,它提供了一个结构化和组织化的存储库,用于存储、管理和检索数据。随着 ML 应用越来越多地依赖于大量数据,将数据库集成到流水线中成为几个原因的必要条件。

数据库提供了一种系统化的方式来存储大量数据,使其易于访问和检索。原始数据、清洗后的数据集、特征向量和其他相关信息可以高效地存储在数据库中,使 ML 流水线的各个组件能够无缝访问。

在许多 ML 项目中,数据预处理是一个关键步骤,它涉及在将数据输入模型之前对数据进行清理、转换和聚合。数据库允许你存储中间预处理的中间数据,减少了每次训练模型时重复资源密集型预处理步骤的需求。这加快了整个流水线并保持了数据的一致性。

ML 流水线通常涉及来自不同来源的数据,例如传感器、API、文件和数据库。拥有一个集中的数据库简化了整合不同数据流的过程,确保所有相关信息都能方便地用于训练和推理。

即使维护不同数据集版本的记录对于可重复性和跟踪变化也很重要。数据库可以用来存储数据集的不同版本,如果需要,可以轻松回滚到以前的版本,并促进团队成员之间的协作。

最后,处理大规模数据的 ML 应用需要有效的数据管理来有效扩展。数据库提供了索引、分区和优化查询的机制,这些机制提高了性能,并允许流水线处理不断增长的数据量。

因此,让我们在 SQLite 中创建一个数据库,它将包含我们在之前工作中使用的相同数值数据:

# create the database
import sqlite3
conn = sqlite3.connect('ant13.db')
c = conn.cursor()

在前面的代码片段中,我们使用sqlite3引擎创建数据库并连接到它(sqlite3.connect)。一旦连接到数据库,我们需要一个游标在数据库中移动并执行我们的查询。下一步是将现有数据导入数据库。

现在,我们可以打开 Excel 文件并将数据传输到数据库中:

# read the excel file with the data
# and save the data to the database
import pandas as pd
# read the excel file
df = pd.read_excel('chapter_12.xlsx', sheet_name='ant_1_3')
# print the first 5 rows
print(df.head())
# create the engine that we use to connect to the database to
# save the data
engine = create_engine('sqlite:///ant13.db')
# save the dataframe to the database
df.to_sql('ant_1_3', engine, index=False, if_exists='replace')

上述代码从 Excel 文件中读取数据,使用 pandas 库进行处理,然后将处理后的数据保存到 SQLite 数据库中。首先,代码读取名为 'chapter_12.xlsx' 的 Excel 文件,并从 'ant_1_3' 工作表提取数据。数据被加载到 pandas DataFrame,df 中。然后,代码使用 sqlalchemy 库中的 create_engine 函数建立与 SQLite 数据库的连接。然后,它创建到名为 'ant13.db' 的数据库文件的连接。

然后,它使用内置的 to_sql 函数根据 DataFrame 创建一个数据库表。在这个例子中,该函数有以下参数:

  • 'ant_1_3' 是数据库中存储数据的表名。

  • engine 是之前创建的 SQLite 数据库的连接。

  • index=False 指定 DataFrame 的索引不应作为单独的列保存到数据库中。

  • if_exists='replace' 表示如果数据库中已存在名为 'ant_1_3' 的表,则应使用新数据替换它。if_exists 的其他选项包括 append(如果表已存在,则向表中添加数据)和 fail(如果表已存在,则引发错误)。

在此之后,我们已经在数据库中有数据,可以轻松地在多个机器学*管道之间共享数据。然而,在我们的案例中,我们将仅演示如何将此类数据提取到 DataFrame 中,以便我们可以在简单的机器学*应用程序中使用它:

# select all rows from that database
data = engine.execute('SELECT * FROM ant_1_3').fetchall()
# and now, let's create a dataframe from that data
df = pd.DataFrame(data)
# get the names of the columns from the SQL database
# and use them as the column names for the dataframe
df.columns = [x[0] for x in engine.description]
# print the head of the dataframe
df.head()

'SELECT * FROM ant_1_3' 查询从数据库中的 'ant_1_3' 表中选择所有列。fetchall() 方法检索查询返回的所有行,并将它们存储在数据变量中。数据变量将是一个元组列表,其中每个元组代表一行数据。

然后,它从数据列表创建一个 pandas DataFrame,名为 df。列表中的每个元组对应于 DataFrame 中的行,DataFrame 的列将自动编号。最后,代码检索原始数据库表中的列名。engine.description 属性包含执行 SQL 查询的结果的元数据。具体来说,它提供了关于查询返回的列的信息。然后,代码提取 engine.description 中每个元组的第一个元素,即列名,并将这些名称分配给 DataFrame 的列,df

从那里开始,数据处理的工作流程就像我们所知的那样——它使用 pandas DataFrame。

在这个例子中,整个 DataFrame 可以适应数据库,整个数据库也可以适应一个框架。然而,对于大多数机器学*数据集来说,情况并非如此。pandas 库在数据量方面存在限制,因此在训练 GPT 模型等模型时,我们需要比 DataFrame 能容纳更多的数据。为此,我们可以使用 Hugging Face 的 Dataset 库,或者我们可以使用数据库。我们只能获取有限的数据量,在它上面训练神经网络,然后在另一组数据上验证,然后获取新的一组行,再训练神经网络更多一些,依此类推。

除了在文件上创建数据库,这可能有点慢之外,SQLite 库允许我们在内存中创建数据库,这要快得多,但它们不会被序列化到我们的永久存储中——我们需要自己处理这一点。

要创建一个内存数据库,我们只需在第一个脚本中将数据库的名称更改为:memory:,如下所示:

conn = sqlite3.connect(':memory:')
c = conn.cursor()

我们可以稍后以类似的方式使用它,如下所示:

# create the enginve that we use to connect to the database to
# save the data
engine = create_engine('sqlite:///:memory:')
# save the dataframe to the database
df.to_sql('ant_1_3', engine, index=False, if_exists='replace')

最后,我们需要记住将数据库序列化到文件中;否则,一旦我们的系统关闭,它就会消失:

# serialize to disk
c.execute("vacuum main into 'saved.db'")

如果我们知道如何处理 DataFrame,那么将数据库与机器学*结合使用相当简单。然而,其附加价值却相当大。我们可以将数据序列化到文件中,将它们读入内存,进行操作,然后再将它们序列化。我们还可以将我们的应用程序扩展到多个系统之外,并在线使用这些系统。然而,为了做到这一点,我们需要一个用户界面。

有了这些,我们就来到了我的下一个最佳实践。

最佳实践 #68

尝试使用内存数据库,并经常将它们转储到磁盘上。

例如,pandas 等库在可以包含的数据量方面存在限制。数据库没有。使用内存数据库提供了两者的优点,而没有这些限制。将数据存储在内存中可以启用快速访问,而使用数据库引擎不会限制数据的大小。我们只需要记住,偶尔将数据库从内存保存(转储)到磁盘上,以防止在异常、错误、缺陷或设备故障的情况下数据丢失。

部署用于数值数据的机器学*模型

在我们创建 UI 之前,我们需要定义一个函数,该函数将负责使用我们在上一章中训练的模型进行预测。这个函数接受用户会看到的参数,然后进行预测。以下代码片段包含了这个函数:

import gradio as gr
import pandas as pd
import joblib
def predict_defects(cbo,
                    dcc,
                    exportCoupling,
                    importCoupling,
                    nom,
                    wmc):
    # we need to convert the input parameters to floats to use them in the prediction
    cbo = float(cbo)
    dcc = float(dcc)
    exportCoupling = float(exportCoupling)
    importCoupling = float(importCoupling)
    nom = float(nom)
    wmc = float(wmc)
    # now, we need to make a data frame out of the input parameters
    # this is necessary because the model expects a data frame
    # we create a dictionary with the column names as keys
    # and the input parameters as values
    # please note that the names of the features must be the same as in the model
    data = {
        'CBO': [cbo],
        'DCC': [dcc],
        'ExportCoupling': [exportCoupling],
        'ImportCoupling': [importCoupling],
        'NOM': [nom],
        'WMC': [wmc]
    }
    # we create a data frame from the dictionary
    df = pd.DataFrame(data)
    # load the model
    model = joblib.load('./chapter_12_decision_tree_model.joblib')
    # predict the number of defects
    result = model.predict(df)[0]
    # return the number of defects
    return result

这个片段首先导入了三个对 UI 和建模都非常重要的库。我们已经知道 pandas 库,但其他两个如下:

  • gradio:这个库用于创建简单的 UI,以便进行交互式机器学*模型测试。这个库使得创建 UI 并将其连接到模型变得非常容易。

  • joblib:这个库用于保存和加载 Python 对象,特别是机器学*模型。多亏了这个库,我们不需要每次用户想要打开软件(UI)时都训练模型。

predict_defects 函数是我们使用模型的地方。需要注意的是,参数的命名被 UI 自动用于命名输入框(我们稍后会看到)。它接受六个输入参数:cbodccexportCouplingimportCouplingnomwmc。这些参数是我们用来训练模型的相同软件度量。由于这些参数作为文本或数字输入,因此将它们转换为浮点数很重要,因为这是我们模型的输入值。一旦转换完成,我们需要将这些自由参数转换成一个我们可以用作模型输入的单个 DataFrame。首先,我们必须将其转换为字典,然后使用该字典创建一个 DataFrame。

一旦数据准备就绪,我们可以使用 model = joblib.load('./chapter_12_decision_tree_model.joblib') 命令加载模型。我们必须做的最后一件事是使用该模型进行预测。我们可以通过编写 result = model.predict(df)[0] 来完成。函数通过返回预测结果结束。

有几点需要注意。首先,我们需要一个单独的函数来处理整个工作流程,因为 UI 是基于这个函数的。这个函数必须具有与我们在 UI 上拥有的输入元素数量相同的参数数量。其次,需要注意的是,DataFrame 中的列名应该与训练数据(列名区分大小写)中的列名相同。

因此,实际的 UI 完全由 Gradio 库处理。以下代码片段展示了这一点:

# This is where we integrate the function above with the user interface
# for this, we need to create an input box for each of the following parameters:
# CBO, DCC, ExportCoupling,  ImportCoupling,  NOM,  WMC
demo = gr.Interface(fn=predict_defects,
                    inputs = ['number', 'number', 'number', 'number', 'number', 'number'],
                    outputs = gr.Textbox(label='Will contain defects?',
                                         value= 'N/A'))
# and here we start the actual user interface
# in a browser window
demo.launch()

此代码片段展示了之前定义的 predict_defects 函数与 UI 的集成。使用 Gradio 创建了一个简单的 UI,它从用户那里获取输入,使用提供的函数处理它,并显示结果。代码由两个语句组成:

  1. 使用 gr.Interface 函数创建接口,以下为参数:

    • fn=predict_defects: 这个参数指定了将用于处理用户输入并生成输出的函数。在这种情况下,它是之前定义的 predict_defects 函数。请注意,函数的参数没有提供,库会自动提取它们(及其名称)。

    • inputs: 这个参数指定了接口应该期望的输入类型。在这种情况下,它列出了六个输入参数,每个参数都是 'number' 类型。这些对应于 predict_defects 函数中的 cbodccexportCouplingimportCouplingnomwmc 参数。

    • outputs: 这个参数指定了接口应该向用户显示的输出格式。在这种情况下,它是一个标记为 'N/A' 的文本框。由于我们的模型是二进制的,我们只使用 1 和 0 作为输出。为了标记模型尚未使用的事实,我们以 '``N/A' 标签开始。

  2. 启动界面(demo.launch()):这一行代码在浏览器窗口中启动 UI,使用户能够与之交互。

使用 Gradio 创建的 UI 具有输入字段,用户可以提供软件指标(cbodccexportCouplingimportCouplingnomwmc)的值。一旦用户提供了这些值并提交了表单,predict_defects函数将使用提供的输入值被调用。预测结果(是否存在缺陷)将在标记为'将包含缺陷?'的文本框中显示。

我们可以通过在命令提示符中输入以下内容来启动此应用程序:

>python app.py

这启动了一个本地 Web 服务器并提供了它的地址。一旦我们打开带有应用程序的页面,我们将看到以下 UI:

图 13.3 – 使用 Gradio 创建的缺陷预测模型 UI

图 13.3 – 使用 Gradio 创建的缺陷预测模型 UI

UI 分为两列 – 右侧列显示结果,左侧列显示输入数据。目前,输入数据是默认的,因此预测值是 N/A,正如我们的设计。

我们可以填写数据并按下提交按钮以获取预测值。这如图图 13**.4所示:

图 13.4 – 包含预测结果的 UI

图 13.4 – 包含预测结果的 UI

一旦我们填写数据以进行预测,我们就可以提交它;此时,我们的结果显示具有这些特征的模块将包含缺陷。这也很有逻辑性 – 对于任何有 345 个输入的模块,我们几乎可以保证会有一些缺陷。它只是太复杂了。

这个模型和 UI 仅在我们电脑的本地可用。然而,如果我们只更改一行,我们就可以与他人共享它,甚至将其嵌入到网站上。我们可以用demo.launch(share=True)代替不带参数的demo.launch()

尽管我们以 Gradio 作为 UI 的示例,但它说明了将现有模型链接到 UI 相当容易。我们可以手动输入数据并从模型中获得预测。UI 是用 Gradio 还是任何其他框架编程变得不那么重要。难度可能不同 – 例如,我们可能需要手动编程输入文本框和模型参数之间的链接 – 但本质是相同的。

部署用于图像的生成式机器学*模型

Gradio 框架非常灵活,允许快速部署如生成式 AI 稳定扩散模型等模型 – 与 DALL-E 模型类似工作的图像生成器。此类模型的部署与我们之前覆盖的数值模型的部署非常相似。

首先,我们需要创建一个函数,该函数将根据 Hugging Face 中的一个模型生成图像。以下代码片段显示了此函数:

import gradio as gr
import pandas as pd
from diffusers import StableDiffusionPipeline
import torch
def generate_images(prompt):
    '''
    This function uses the prompt to generate an image
    using the anything 4.0 model from Hugging Face
    '''
    # importing the model from Hugging Face
    model_id = "xyn-ai/anything-v4.0"
    pipe = StableDiffusionPipeline.from_pretrained(model_id,
                                                   torch_dtype=torch.float16,
                                                   safety_checker=None)
    # send the pipeline to the GPU for faster processing
    pipe = pipe.to("cuda")
    # create the image here
    image = pipe(prompt).images[0]
    # return the number of defects
    return image

此代码片段首先导入必要的库。在这里,我们会注意到还有一个库——diffusers,它是图像生成网络的接口。函数从 Hugging Face hub 导入一个预训练模型。该模型是 "xyn-ai/anything-v4.0",它是 Anything 4.0 模型的一个变体,由一位用户克隆。使用 StableDiffusionPipeline.from_pretrained() 函数将模型作为图像生成的管道加载。torch_dtype 参数设置为 torch.float16,表示用于计算的 数据类型(为了更快的处理速度,使用较低的精度)。

该图像是通过绕过将提示作为 pipe() 函数的参数来生成的。生成的图像是通过 images[0] 属性访问的。prompt 参数通过函数的参数提供,该参数由 UI 提供。

该函数返回图像,然后由 UI 捕获并显示。

一旦我们知道了前一个示例中的代码,UI 的代码也相当简单:

demo = gr.Interface(fn=generate_images,
                    inputs = 'text',
                    outputs = 'image')
# and here we start the actual user interface
# in a browser window
demo.launch()

与前一个示例相比,这段代码只包含一个输入参数,即用于生成图像的提示。它还有一个输出,即图像本身。我们使用 'image' 类来表示它是一个图像,应该以图像的形式显示。该模型的输出在 图 13.5 中展示:

图 13.5 – 使用“汽车和太阳”提示从 Anything 4.0 模型生成的图像

图 13.5 – 使用“汽车和太阳”提示从 Anything 4.0 模型生成的图像

请注意,由于生成的汽车存在扭曲伪影,因此模型并不完美——例如,右后尾灯并没有完美生成。

将代码补全模型作为扩展部署

到目前为止,我们已经学*了如何在网络上和 Hugging Face hub 上部署模型。这些方法是好的,并为我们提供了创建模型 UI 的能力。然而,这些是独立的工具,需要手动输入,并提供了我们需要手动使用的输出——例如,粘贴到另一个工具或保存到磁盘。

在软件工程中,许多任务都是自动化的,许多现代工具提供了一整套扩展和插件。GitHub Copilot 就是 Visual Studio 2022 的一个插件,也是 Visual Studio Code 等其他工具的扩展。ChatGPT 既是独立的网络工具,也是微软 Bing 搜索引擎的插件。

因此,在本章的最后部分,我们将我们的模型打包为编程环境的扩展。在本节中,我们将学*如何创建一个扩展以完成代码,就像 GitHub CoPilot 一样。自然地,我们不会使用 CoPilot 的 CodeX 模型,而是使用 Codeparrot 的 Python 编程语言模型。我们之前已经见过这个模型,所以让我们深入了解实际的扩展。

我们需要一些工具来开发扩展。自然地,我们需要 Visual Studio Code 本身和 Python 编程环境。我们还需要 Node.js 工具包来创建扩展。我们从 nodejs.org 安装了它。一旦安装完成,我们可以使用 Node.js 的包管理器来安装 Yeoman 和用于开发扩展的框架。我们可以在命令提示符中使用以下命令来完成:

npm install -g yo generator-code

一旦安装了这些包,我们需要通过输入以下内容来为我们的扩展创建骨架代码:

yo code

这将弹出我们需要填写的菜单:

     _-----_     ╭──────────────────────────╮
    |       |    │   Welcome to the Visual  │
    |--(o)--|    │   Studio Code Extension  │
   `---------´   │        generator!        │
    ( _´U`_ )    ╰──────────────────────────╯
    /___A___\   /
     |  ~  |
   __'.___.'__
 ´   `  |° ´ Y `
? What type of extension do you want to create? (Use arrow keys)
> New Extension (TypeScript)
  New Extension (JavaScript)
  New Color Theme
  New Language Support
  New Code Snippets
  New Keymap
  New Extension Pack
  New Language Pack (Localization)
  New Web Extension (TypeScript)
  New Notebook Renderer (TypeScript)

我们需要选择第一个选项,即使用 TypeScript 的新扩展。这是开始编写扩展的最简单方法。我们可以使用语言包和语言协议开发一个非常强大的扩展,但在这个第一个扩展中,简洁胜过强大。

我们需要就我们扩展的设置做出一些决定,所以现在让我们来做这件事:

? What type of extension do you want to create? New Extension (TypeScript)
? What's the name of your extension? mscopilot
? What's the identifier of your extension? mscopilot
? What's the description of your extension? Code generation using Parrot
? Initialize a git repository? No
? Bundle the source code with webpack? No
? Which package manager to use? (Use arrow keys)
> npm
  yarn
  pnpm

我们将我们的扩展命名为 mscopilot 并不创建很多额外的代码——没有 Git 仓库和没有 webpack。在这个例子中,简洁是关键。一旦文件夹创建完成,我们需要从 Node.js 中获取一个额外的包来与 Python 交互:

npm install python-shell

点击最后一个条目后,我们会得到一个名为 mscopilot 的新文件夹;我们可以使用 code . 命令进入它。它将打开 Visual Studio Code,在那里我们可以用为新扩展编写的代码填充模板。一旦环境打开,我们需要导航到 package.json 文件并更改一些内容。在该文件中,我们需要找到 contributes 部分,并做一些修改,如下所示:

"contributes": {
    "commands": [
      {
        "command": "mscopilot.logSelectedText",
        "title": "MS Suggest code"
      }
    ],
    "keybindings": [
      {
        "command": "mscopilot.logSelectedText",
        "key": "ctrl+shift+l",
        "mac": "cmd+shift+l"
      }
    ]
  },

在前面的代码片段中,我们添加了一些信息,说明我们的扩展有一个新的功能——logSelectedText——并且它将通过 Windows 上的 Ctrl + Shift + L 键组合(以及 Mac 上类似的组合)可用。我们需要记住,命令名称包括我们扩展的名称,这样扩展管理器就知道这个命令属于我们的扩展。现在,我们需要转到 extension.ts 文件并添加我们命令的代码。以下代码片段包含代码的第一部分——扩展的设置和激活:

import * as vscode from 'vscode';
// This method is called when your extension is activated
export function activate(context: vscode.ExtensionContext) {
  // Use the console to output diagnostic information (console.log) and errors (console.error)
  // This line of code will only be executed once when your extension is activated
  console.log('Congratulations, your extension "mscopilot" is now active!');

这个函数只是记录我们的扩展已被激活。由于扩展对用户来说相当不可见(而且应该是这样),使用日志文件来存储已实例化的信息是一种良好的做法。

现在,我们添加将获取所选文本、实例化 Parrot 模型并将建议添加到编辑器的代码:

// Define a command to check which code is selected.
vscode.commands.registerCommand('mscopilot.logSelectedText', () => {
  // libraries needed to execute python scripts
  const python = require('python-shell');
  const path = require('path');
  // set up the path to the right python interpreter
  // in case we have a virtual environment
  python.PythonShell.defaultOptions = { pythonPath: 'C:/Python311/python.exe' };
  // Get the active text editor
  const editor = vscode.window.activeTextEditor;
  // Get the selected text
     const selectedText = editor.document.getText(editor.selection);
  // prompt is the same as the selected text
  let prompt:string = selectedText;
  // this is the script in Python that we execute to
  // get the code generated by the Parrot model
  //
  // please note the strange formatting,
  // which is necessary as python is sensitive to indentation
  let scriptText = `
from transformers import pipeline
pipe = pipeline("text-generation", model="codeparrot/codeparrot-small")
outputs = pipe("${prompt}", max_new_tokens=30, do_sample=False)
print(outputs[0]['generated_text'])`;
  // Let the user know what we start the code generation
  vscode.window.showInformationMessage(`Starting code generation for prompt: ${prompt}`);
  // run the script and get the message back
  python.PythonShell.runString(scriptText, null).then(messages=>{
  console.log(messages);
  // get the active editor to paste the code there
  let activeEditor = vscode.window.activeTextEditor;
  // paste the generated code snippet
  activeEditor.edit((selectedText) => {
  // when we get the response, we need to format it
  // as one string, not an array of strings
  let snippet = messages.join('\n');
  // and replace the selected text with the output
  selectedText.replace(activeEditor.selection, snippet)  });
  }).then(()=>{
   vscode.window.showInformationMessage(`Code generation finished!`);});
  });
context.subscriptions.push(disposable);
}

这段代码注册了我们的 'mscopilot.logSelectedText' 命令。我们在之前的文件——package.json——中使这个命令对扩展管理器可见。当这个命令执行时,它执行以下步骤。重要的是 TypeScript 代码和 Python 代码之间的交互。由于我们使用的是 Hugging Face 模型,最简单的方法是使用本书中迄今为止使用的相同脚本。然而,由于扩展是用 TypeScript(或 JavaScript)编写的,我们需要在 TypeScript 中嵌入 Python 代码,向其中添加一个变量,并捕获结果:

  1. 首先,导入所需的库——python-shellpath——这些库是执行 Node.js 环境中的 Python 脚本所必需的。

  2. 然后,它通过 C:/Python311/python.exe 设置 Python 解释器,这将用于运行 Python 脚本。这对于确保使用正确的 Python 环境非常重要,即使在使用虚拟环境时也是如此。如果我们没有指定它,我们需要在脚本中找到它,这在用户环境中可能有点棘手。

  3. 接着,它设置活动文本编辑器和选定的文本。我们需要这个选择,以便我们可以将提示信息发送到模型。在我们的例子中,我们将简单地发送选择的内容到模型,并获取建议的代码。

  4. 然后,它准备提示信息,这意味着它创建了一个字符串变量,我们在 Python 脚本的代码中使用这个变量。

  5. 然后,它定义了 Python 脚本,其中建立了与 ML 模型的连接。我们的 Python 脚本被定义为多行字符串(scriptText),使用模板字面量。这个脚本利用 Hugging Face Transformers 库的 pipeline 函数,使用 codeparrot-small 模型进行文本生成。Python 代码以粗体显示,我们可以看到字符串与提示信息相补充,即活动编辑器中的选定文本。

  6. 然后,它向用户显示简短的信息,因为模型需要一些时间来加载和进行推理。第一次执行可能需要一分钟(因为模型需要下载和设置)。因此,显示一条消息,说明我们正在开始推理是很重要的。使用 vscode.window.showInformationMessage 向用户显示一条消息,表明代码生成过程即将开始。

  7. 接着,它使用 python.PythonShell.runString 运行 Python 脚本(scriptText)。脚本的输出被捕获在 messages 数组中。我们暂时失去了对执行的掌控,因为我们等待 Python 脚本完成;它为我们提供了一个代码补全的建议。

  8. 接下来,它将响应中的生成代码(messages 数组)粘贴到一个单独的字符串(snippet)中。然后,该片段被粘贴到活动文本编辑器中选中文本的位置,有效地用生成的代码替换了选中的文本。由于模型响应的第一个元素是提示,我们可以简单地用片段替换选择。

  9. 最后,在代码生成过程完成后,它显示完成消息。

现在,我们有一个可以测试的扩展。我们可以通过按 F5 键来执行它。这会打开一个新的 Visual Studio 实例,在那里我们可以输入要完成的代码片段,如图 图 13.6 所示:

图 13.6 – 激活我们的扩展的一些 Visual Studio 代码的测试实例。选中的文本用作模型的提示

图 13.6 – 激活我们的扩展的一些 Visual Studio 代码的测试实例。选中的文本用作模型的提示

一旦我们按下 Ctrl + Shift + L,正如我们在 package.json 文件中定义的那样,我们的命令就会被激活。这可以通过环境右下角的消息来指示,如图 图 13.7 所示:

图 13.7 – 开始生成代码。消息框和日志信息表明我们的命令正在工作

图 13.7 – 开始生成代码。消息框和日志信息表明我们的命令正在工作

几秒钟后,我们从 Parrot 模型获得建议,然后必须将其粘贴到编辑器中,如图 图 13.8 所示:

图 13.8 – 将来自 Parrot 模型的代码建议粘贴到编辑器中

图 13.8 – 将来自 Parrot 模型的代码建议粘贴到编辑器中

在这里,我们可以看到我们的扩展相当简单,并从模型获取所有建议。因此,除了正确的代码(return "Hello World!")之外,它还包括了我们不需要的更多内容。我们可以编写更有趣的逻辑,解析代码,并清理它 – 天空才是极限。我将这项工作留给你来完成,并使其变得更好。我的工作是用来说明编写 GitHub CoPilot 类似的工具并不像看起来那么困难。

通过这个,我们来到了本章的最后一项最佳实践。

最佳实践 #69

如果你的模型/软件旨在帮助用户处理日常任务,确保你将其开发为一个插件。

虽然我们可以使用来自 Gradio 界面的 Codeparrot 模型,但这可能不会被欣赏。程序员需要将他们的代码复制到网页浏览器中,点击一个按钮,等待建议,并将其粘贴回他们的环境中。通过为 Visual Studio Code 提供一个扩展,我们可以利用软件开发者的工作流程。唯一的额外任务是选择要完成的文本并按 Ctrl + Shift + L;我相信这可以进一步简化,就像 GitHub Copilot 所做的那样。

摘要

本章结束了本书的第三部分。它也结束了我们通过最佳实践之旅中最技术性的部分。我们学*了如何开发机器学*系统以及如何部署它们。这些活动通常被称为人工智能工程,这个术语将重点放在软件系统的开发上,而不是模型本身。这个术语还表明,测试、部署和使用机器学*要远比训练、验证和测试模型复杂得多。

自然地,这里还有更多。仅仅开发和部署人工智能软件是不够的。作为软件工程师或人工智能工程师,我们需要考虑我们行为的后果。因此,在这本书的下一部分,我们将探讨偏见、伦理以及我们工作成果——人工智能软件系统的可持续使用等概念。

参考文献

  • Rana, R.,等人。在软件缺陷预测中采用机器学*的框架。在 2014 年第 9 届软件工程与应用国际会议(ICSOFT-EA)上。 2014 年。IEEE。

  • Bosch, J.,H.H. Olsson,和 I. Crnkovic。人工智能系统的工程:研究议程。智能网络物理系统的人工智能范式,2021 年: p. 1-19。

  • Giray, G.。从软件工程的角度看机器学*系统的工程:现状与挑战。系统与软件杂志,2021 年。180: p. 111031。

第四部分:数据管理和机器学*系统开发的伦理方面

由于机器学*基于训练数据中的模式,我们需要从伦理的角度理解如何与这类系统打交道。伦理是软件工程师不太熟悉的领域之一。在本书的这一部分,我们探讨在数据获取和管理中存在哪些伦理问题,以及如何处理机器学*算法中的偏差。最后,我们通过探讨如何将本书中学到的知识整合到整个网络服务生态系统中,以及如何部署它们来结束这一部分的内容。

本部分包含以下章节:

  • 第十四章, 数据获取和管理中的伦理

  • 第十五章, 机器学*系统中的伦理

  • 第十六章, 生态系统中的机器学*系统集成

  • 第十七章, 总结与下一步方向

第十四章:数据获取和管理中的伦理

机器学*ML)需要大量数据,这些数据可能来自各种来源,但并非所有来源都同样易于使用。在软件工程中,我们可以设计和开发使用来自其他系统数据的系统。我们还可以使用并非真正来自人类的数据;例如,我们可以使用关于系统缺陷或复杂性的数据。然而,为了给社会提供更多价值,我们需要使用包含有关人或其财产信息的数据;例如,当我们训练机器识别人脸或车牌时。然而,无论我们的用例如何,我们都需要遵循伦理准则,最重要的是,我们的软件不应造成任何伤害。

我们本章从探讨几个不道德的系统示例开始,这些系统显示出偏见;例如,惩罚某些少数群体的信用评级系统。我还会解释使用开源数据和揭露受试者身份的问题。然而,本章的核心是对数据管理和软件系统伦理框架的解释和讨论,包括电气和电子工程师协会IEEE)和计算机协会ACM)的行为准则。

在本章中,我们将涵盖以下主要内容:

  • 计算机科学和软件工程中的伦理

  • 数据无处不在,但我们真的能利用它吗?

  • 来自开源系统数据的伦理

  • 从人类收集的数据背后的伦理

  • 合同和法律义务

计算机科学和软件工程中的伦理

现代伦理观源于第二次世界大战后制定的《纽伦堡法典》。该法典基于几个原则,但最重要的是,每个涉及人类受试者的研究都需要获得许可。这是至关重要的,因为它防止了在实验中对人类的滥用。研究中的每个参与者也应能够在任何时候撤回他们的许可。让我们看看所有 10 个原则:

  1. 人类受试者的自愿同意绝对必要。

  2. 实验应当产生有益于社会的丰富结果,这些结果无法通过其他方法或研究手段获得,并且其本质不是随机的和不必要的。

  3. 实验应当设计得基于动物实验的结果和对研究疾病或其他问题的自然历史的了解,以便预期的结果将证明进行实验的合理性。

  4. 实验应当以避免所有不必要的身体和精神上的痛苦和伤害的方式进行。

  5. 不应进行任何实验,除非有先验理由相信将发生死亡或致残伤害,除非在这些实验中,实验医生也作为受试者。

  6. 需要承担的风险程度不应超过实验解决所涉问题的 humanitarian 重要性所确定的程度。

  7. 应做好适当的准备,并提供充足的设施以保护实验对象免受伤害、残疾或死亡等远程可能性的影响。

  8. 实验应由具有科学资格的人员进行。在整个实验过程中,进行或参与实验的人员应要求具备最高程度的技能和谨慎。

  9. 在实验过程中,如果实验对象达到他们认为继续实验似乎不可能的身体或心理状态,他们应有权自由结束实验。

  10. 在实验过程中,负责的科学家必须准备好在任何阶段终止实验,如果他们有合理的理由相信,在行使他们所需的诚信、高超技能和谨慎判断时,实验的继续可能导致实验对象受伤、残疾或死亡。

《纽伦堡法典》为现代人体实验和研究中的伦理标准奠定了基础。它在后续伦理指南和法规的发展中产生了重大影响,例如《赫尔辛基宣言》(DoH)以及各种国家和国际关于人体研究伦理的法规。这些原则强调了尊重研究参与者的权利和福祉的重要性,并确保研究以道德和负责任的方式进行。

上述原则旨在指导实验为社会创造价值,同时尊重实验参与者。第一条原则是关于同意的,这很重要,因为我们希望防止使用那些对实验不知情的人。在机器学*的背景下,这意味着我们在收集包含人类数据时必须非常小心;例如,当我们收集图像数据以训练物体识别时,或者当我们从包含个人信息的开源存储库中收集数据时。

虽然我们在软件工程中确实进行实验,但这些原则比我们想象的更为普遍。在本章中,我们探讨这些原则如何影响人工智能系统工程中的数据伦理。

除了这些原则之外,我们还关注数据偏差的来源以及如何避免它。

数据无处不在,但我们真的能利用它吗?

我们保护受试者和数据的一种方式是使用适当的数据使用许可。许可在某种程度上是一种合同,许可方授予许可方以特定方式使用数据的权限。许可既用于软件产品(算法、组件)也用于数据。以下是一些在当代软件中最常用的许可模型:

  • 专有许可证:这是一种许可人拥有数据并授予使用数据用于某些目的(通常是盈利目的)的许可模式。在这种合同中,各方通常规定数据可以如何使用、使用多长时间,以及双方的责任。

  • 许可开放许可证:这些许可证为许可人提供了几乎无限制的数据访问权限,同时限制了许可人的责任。通常,许可人不需要向许可人提供其产品或衍生作品。

  • 非许可开放许可证:这些许可证提供了几乎无限制的访问权限,同时要求某种形式的互惠。通常,这种互惠是以要求许可人提供产品或衍生作品访问权限的形式出现的。

自然,这三类许可证也有变体。因此,让我们看看一种流行的开源许可证——来自 Hugging Face 的 Unsplash 许可证:

Unsplash
All unsplash.com images have the Unsplash license copied below:
https://unsplash.com/license
License
Unsplash photos are made to be used freely. Our license reflects that.
All photos can be downloaded and used for free
Commercial and non-commercial purposes
No permission needed (though attribution is appreciated!)
What is not permitted
Photos cannot be sold without significant modification.
Compiling photos from Unsplash to replicate a similar or competing service.
Tip: How to give attribution
Even though attribution isn't required, Unsplash photographers appreciate it as it provides exposure to their work and encourages them to continue sharing.
Photo by <person name> on Unsplash
Longform
Unsplash grants you an irrevocable, nonexclusive, worldwide copyright license to download, copy, modify, distribute, perform, and use photos from Unsplash for free, including for commercial purposes, without permission from or attributing the photographer or Unsplash. This license does not include the right to compile photos from Unsplash to replicate a similar or competing service.
Other Images
All other images were either taken by the authors, or created by friends of the authors and all permissions to modify, distribute, copy, perform and use are given to the authors.

许可证来源于此数据集:huggingface.co/datasets/google/dreambooth/blob/main/dataset/references_and_licenses.txt。让我们更详细地了解一下许可证的含义。首先,这是 Unsplash 许可证允许的内容:

  • 免费使用:我们可以免费下载和使用 Unsplash 上的图像。

  • 商业和非商业用途:我们可以将图像用于与商业相关的目的(如广告、网站和产品包装)以及个人项目或非营利活动。

  • 无需许可:我们不需要从摄影师或 Unsplash 那里获得同意或许可来使用图像。这使得整个过程变得无烦恼且方便。

  • 修改和重新分发:我们可以根据您的需求以任何方式修改原始图像并分发它。然而,如果您想出售该图像,它应该与原始图像有显著的不同。

  • 不可撤销、非独占、全球范围的许可证:一旦下载了照片,您就有权无限期地使用它(不可撤销)。非独占意味着其他人也可以使用相同的图像,而全球意味着在使用上没有地理限制。

同时,许可证禁止某些活动:

  • 销售未修改的照片:我们不能以原始形式或未经对它们进行重大修改的情况下出售照片。

  • 复制 Unsplash 的服务:我们不能使用这些图像创建直接竞争或类似 Unsplash 的服务。换句话说,下载大量 Unsplash 图像然后使用这些图像开始自己的股票照片服务将违反许可证。

许可证还规定了致谢的形式——虽然在使用图片时不必提及摄影师或 Unsplash,但这是鼓励的。致谢是认可摄影师努力的一种方式。提供的示例,“照片由<人名>在 Unsplash 上拍摄”是建议的致谢格式。

它还规定了如果数据集中有其他图片(例如,由第三方添加)会发生什么:对于不是来自 Unsplash 的图片,作者要么自己拍了这些照片,要么让熟人拍摄。他们有完全的权限无限制地使用、分发、修改和表演这些图片。

我们还可以查看创意共享CC)的一个许可示例;例如,创意共享,署名,版本 4.0CC-BY-4.0)许可(creativecommons.org/licenses/by/4.0/legalcode)。简而言之,此许可证允许在以下条件下共享和重新分配数据:

  • 署名,这意味着当我们使用数据时,我们必须给予数据作者适当的信用,并必须提供参考链接并指出我们对数据所做的更改。

  • 无额外限制,这意味着我们不能对以这种方式许可的数据的使用施加任何额外的限制。如果我们使用的数据在我们的产品中使用,我们不得使用任何手段来限制他人使用这些数据。

许可证的全文有点长,不适合全部引用,但让我们分析其中的一部分,以便我们可以看到它与非常宽松的 Unsplash 许可之间的差异。首先,许可证提供了一系列定义,这些定义有助于法律纠纷。例如,第 1.i 节定义了共享的含义:

Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them.

前面的法律文本可能听起来很复杂,但当我们阅读它时,文本具体说明了数据共享的含义。例如,它说“进口”是数据共享的一种方式。这意味着将数据作为 Python 包、C#库或 GitHub 仓库的一部分提供,是数据共享的一种方式。

我们还可以查看许可证的第二部分,其中使用了第一部分中的定义。以下是一个第 2.a.1 节的例子:

Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to:
* reproduce and Share the Licensed Material, in whole or in part; and
* produce, reproduce, and Share Adapted Material.

在第一部分,我们了解到许可证允许我们免费使用材料;也就是说,我们不必为此付费。它还指定了使用地点(全球范围内)并说明我们不是唯一使用这些数据的人(非独占)。然后,许可证还说明我们可以复制材料,无论是全部还是部分,或者制作改编材料。

然而,某些权利并未转让给我们这些许可方,我们可以在第 2.b.1 节中了解到:

Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise.

本节解释了数据上的道德权利不会转让。特别是,如果我们遇到私人数据或可以与个人联系的数据,我们不会获得对其的权利。以下三个部分很重要:

  • 道德权利:道德权利是版权的一个子集,与作品的个人和声誉方面有关,而不是纯粹的经济方面。这些权利可能因司法管辖区而异,但通常包括以下内容:

    • 完整性权:这是作者对任何可能对其荣誉或声誉造成损害的扭曲、毁损或其他修改作品提出异议的权利。

    • 署名权:这是作者对其作品作为创作者获得认可的权利。

    CC 许可证规定这些道德权利不受许可。这意味着尽管某人可能能够根据 CC 许可证定义的方式使用作品,但他们没有无限制地修改作品的权利,这些修改可能会损害原始创作者的声誉或荣誉。

  • 公开、隐私和其他类似的人格权利:这些权利涉及个人的个人数据、肖像、姓名或声音。它们是保护免受不希望曝光或剥削的权利。CC 许可证也不授予用户侵犯这些权利的权利。

    例如,如果一个人的照片在 CC 许可证下,尽管我们可能能够根据该许可证允许的方式使用照片本身,但这并不意味着我们可以在未经其同意的情况下以商业或促销方式使用该人的肖像。

  • 放弃或不行使权利:然而,许可人正在放弃或同意不执行这些道德或个人权利,以必要的程度来行使许可权利。这意味着许可人不会通过行使他们的道德或个人权利来干涉我们根据 CC 许可证允许的使用作品,但仅限于一定程度。他们并没有完全放弃这些权利;他们只是在与 CC 许可证相关的环境中限制其执行。

我强烈建议读者阅读许可证并反思其构建方式,包括规范许可人责任的部分。

然而,我们继续讨论不如 CC-BY-4.0 许可证那么宽容的许可证。例如,让我们看看所谓的 copyleft 许可证之一:署名-非商业-禁止演绎 4.0 国际CC BY-NC-ND 4.0)。此许可证允许我们在以下条件下复制和重新分发数据(以下内容引用并改编自以下 CC 网页:creativecommons.org/licenses/by-nc-nd/4.0/):😃

  • 署名:我们必须给予适当的信用,提供链接到许可证,并指出是否进行了更改。我们可以在任何合理的方式下这样做,但不能以任何方式暗示许可人支持我们或我们的使用。

  • NonCommercial: 我们不得将材料用于商业目的。

  • NoDerivatives: 如果我们对材料进行混搭、转换或在此基础上构建,我们可能不得分发修改后的材料。

  • No additional restrictions: 我们不得应用法律条款或技术措施,这些条款或措施在法律上限制了他人执行许可证允许的任何行为。

许可证文本的主体结构与之前引用的 CC-BY-4.0 许可类似。让我们看看许可证不同的部分——第 2.a.1 节

Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to:
* reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and
* produce and reproduce, but not Share, Adapted Material for NonCommercial purposes only.

第一项意味着我们只能为了非商业目的分享许可材料。第二项将此扩展到改编材料。

那么,一个可以直接提出的问题是:如何选择我们创建的数据的许可证? 这就是我的下一个最佳实践发挥作用的地方。

最佳实践 #70

如果您为了非商业目的创建自己的数据,请使用限制您责任的许可衍生许可证之一。

在设计商业许可证时,请务必咨询您的法律顾问,以确保您为您的数据和产品选择最佳的许可证模式。然而,如果您在开源数据上工作,请尝试使用一个规范两个方面的许可证——是否可以使用您的数据进行商业目的,以及您的责任是什么。

在我的工作中,我尽量保持开放,这仅仅是因为我坚信开放科学,但这并不一定普遍适用。因此,我经常保护自己的数据不被商业使用——这就是非商业目的的原因。

第二个方面是责任。当我们收集数据时,我们无法考虑数据所有可能的用途,这意味着数据被误用或在使用我们的数据时犯错误的风险总是存在的。我们不希望因为我们没有做的事情而被起诉,所以在这种许可证中限制我们的责任总是个好主意。我们可以通过几种方式来限制它;其中之一是声明许可方有责任确保数据以道德的方式使用或遵守适用于适当地区的所有规则和法规。

这引出了下一个要点——开源系统数据的伦理使用。

开源系统数据的伦理背景

专有系统通常有许可证来规范数据的所有权及其用途。例如,来自公司的代码审查数据通常属于公司。员工为公司工作通常意味着他们放弃了对为公司生成数据的权利。在法律意义上这是必要的,因为员工为此得到了补偿——通常是以工资的形式。

然而,员工没有转移给公司的权利是自由使用他们的个人数据。这意味着当我们与源系统,如 Gerrit 审查系统,一起工作时,我们不应在没有涉及人员的许可下提取个人信息。如果我们执行无法对数据进行屏蔽的查询,我们必须确保个人信息(尽可能快地)被匿名化,并且不会泄露给分析。我们必须确保此类个人信息不会被公开提供。

我们可以在以下领域找到指导,那就是挖掘软件仓库的领域;例如,在 Gold 和 Krinke 最*的一篇文章中。尽管这项研究样本量较小,但作者们触及了与 GitHub 或 Gerrit 等软件仓库数据伦理使用相关的重要问题。他们提出了几个数据来源,其中最受欢迎的是:

  • 版本控制数据,如 CVS、Subversion、Git 或 Mercurial:存在与许可和个人信息在仓库中的存在相关的挑战。

  • 问题跟踪器数据,如 Bugzilla:挑战大多与这些仓库中个人数据的存在或数据与个人隐式关联的能力有关。

  • 邮件存档:邮件列表具有灵活性,可用于不同的目的;例如,问题跟踪、代码审查、问答论坛等。然而,它们可能包含敏感的个人信息或可能导致影响个人的结论。

  • 构建日志版本控制系统VCS)通常使用某种持续集成CI)系统来自动化软件构建。如果构建结果被存档,它们可以为测试和构建实践的研究提供数据。

  • Stack Overflow:Stack Overflow 提供了他们的官方数据存档,并且(数据存档的)子集已被直接用作挑战,或包含历史信息的数据集的一部分。尽管有要求用户允许使用其数据进行分析的许可法规,但并非所有行为都是道德的。

  • IDE 事件:主要挑战与每个 IDE 都是为个人设置且可以访问非常个人化的数据和用户行为有关。

考虑到 GitHub 等仓库中在线可用的源代码量,分析它们以各种目的具有诱惑力。我们可以挖掘仓库中的源代码,以了解软件是如何构建、设计和测试的。这种源代码的使用受到每个仓库提供的许可证的限制。

同时,当挖掘有关源代码的数据时,我们可以挖掘有关贡献者、他们的拉取请求评论以及他们工作的组织的数据。这种对代码库的使用由许可证和个人数据使用的伦理准则所规范。如本章开头所述,使用个人数据需要从个人那里获得同意,这意味着我们应该请求我们分析数据的人的许可。然而,在实践中,这是不可能的,有两个原因——一个是联系所有贡献者的实际能力,另一个是代码库的使用条款。大多数代码库禁止联系贡献者进行研究。

因此,我们需要应用与隐私和利益平衡相关的原则。在大多数情况下,个人的隐私比我们进行的研究的价值更重要,因此我的下一个最佳实践是。

最佳实践 #71

限制自己只研究源代码和其他工件,并且只有在主体同意的情况下才使用个人数据。

有许多伦理准则,但它们大多数得出相同的结论——在使用个人数据时需要同意。因此,我们应该谨慎使用开源数据,仅分析我们研究必需的数据。在分析来自人们的数据时,始终要获得知情同意。正如文章所建议的,我们可以遵循以下指导原则来研究代码库。

研究开源代码库有一些指导原则:

  • 当研究源代码时,我们主要需要关注许可证,并确保我们不会将源代码与人们的个人数据混合。

  • 当研究提交时,我们需要尊重做出贡献的人的身份,因为他们可能没有意识到自己正在被研究。我们必须确保我们的研究不会对这些个人造成伤害或损害。如果我们需要研究个人数据,那么我们需要向机构伦理委员会申请批准并获得明确同意。在某些许可证的情况下,许可证扩展到提交中的消息——例如,Apache License 2.0:“任何形式的电子、口头或书面通信发送给许可方或其代表,包括但不限于在电子邮件列表、源代码控制系统和问题 跟踪系统中的通信*”。

  • 当挖掘 IDE 事件时,我们应该获得同意,因为个人可能没有意识到自己正在被研究。与从公开仓库中收集数据的研究相比,IDE 事件可能包含更多个人数据,因为这些工具与用户的软件生态系统集成。

  • 在挖掘构建日志时:这里适用的原则与提交时相同,因为这些可以很容易地联系起来。

  • 在挖掘 Stack Overflow 时:尽管 Stack Overflow 许可允许我们进行某些研究,因为用户需要允许这样做(根据使用条款),但我们需要确保研究的好处与分析数据相关的风险之间有一个平衡,这些数据通常是自由文本和一些源代码。

  • 在挖掘问题跟踪器时:一些问题跟踪器,如 Debian,提供了分析数据的可能性,但需要与伦理委员会同步。

  • 在挖掘邮件列表时,我们需要尝试从伦理委员会获得许可,因为这些列表通常包含个人信息,在使用之前应进行审查。

2012 年由肯纳利和迪特里奇撰写的《Menlo 报告》为信息技术和通信技术(包括软件工程)的伦理研究提供了一系列指导方针。他们定义了四个原则(以下内容引用自报告):

  • 尊重个人:作为研究对象的参与是自愿的,并基于知情同意;将个人视为自主代理并尊重他们决定自身最佳利益的权力;尊重那些尚未成为研究目标但受到影响的人;自主能力减弱、无法自行做出决定的人有权得到保护。

  • 仁慈:不造成伤害;最大化可能的利益并最小化可能的伤害;系统地评估伤害风险和利益。

  • 正义:每个人在如何被对待的问题上都应得到平等的考虑,研究的好处应根据个人的需求、努力、社会贡献和功绩公平分配;受影响者的选择应公平,负担应在受影响者之间公平分配。

  • 尊重法律和公共利益:进行法律尽职调查;在方法和结果上保持透明;对行动负责。

报告鼓励研究人员和工程师进行利益相关者识别以及他们的观点和考虑因素的识别。其中一项考虑因素是存在恶意行为者,他们可能会滥用我们工作的成果并/或伤害我们分析的个人数据。我们必须始终保护我们分析数据的人,并确保我们不会给他们造成任何伤害。同样适用于我们与之合作的组织。

从人类收集的数据背后的伦理

在欧洲,规范我们如何使用数据的最重要的法律框架之一是通用数据保护条例GDPR)(eur-lex.europa.eu/legal-content/EN/TXT/PDF/?uri=CELEX:32016R0679)。它规范了处理个人数据的范围,并要求组织获得收集、处理和使用个人数据的许可,同时要求组织为个人提供撤销许可的能力。该条例是旨在保护个人(我们)免受那些有能力收集和处理我们数据的公司滥用的最严格国际条例。

尽管我们使用了来自 GitHub 和类似存储库的大量数据,但我们也在存储数据的存储库中。其中之一是 Zenodo,它越来越多地被用来存储数据集。它的使用条款要求我们获得适当的权限。以下是它的使用条款(about.zenodo.org/terms/)::)

1\. Zenodo is an open dissemination research data repository for the preservation and making available of research, educational and informational content. Access to Zenodo's content is open to all, for non-military purposes only.
2\. Content may be uploaded free of charge by those without ready access to an organised data centre.
3\. The uploader is exclusively responsible for the content that they upload to Zenodo and shall indemnify and hold CERN free and harmless in connection with their use of the service. The uploader shall ensure that their content is suitable for open dissemination, and that it complies with these terms and applicable laws, including, but not limited to, privacy, data protection and intellectual property rights *. In addition, where data that was originally sensitive personal data is being uploaded for open dissemination through Zenodo, the uploader shall ensure that such data is either anonymised to an appropriate degree or fully consent cleared **.
4\. Access to Zenodo, and all content, is provided on an "as-is" basis. Users of content ("Users") shall respect applicable license conditions. Download and use of content from Zenodo does not transfer any intellectual property rights in the content to the User.
5\. Users are exclusively responsible for their use of content, and shall indemnify and hold CERN free and harmless in connection with their download and/or use. Hosting and making content available through Zenodo does not represent any approval or endorsement of such content by CERN.
6\. CERN reserves the right, without notice, at its sole discretion and without liability, (i) to alter, delete or block access to content that it deems to be inappropriate or insufficiently protected, and (ii) to restrict or remove User access where it considers that use of Zenodo interferes with its operations or violates these Terms of Use or applicable laws.
7\. Unless specified otherwise, Zenodo metadata may be freely reused under the CC0 waiver.
8\. These Terms of Use are subject to change by CERN at any time and without notice, other than through posting the updated Terms of Use on the Zenodo website.
* Uploaders considering Zenodo for the storage of unanonymised or encrypted/unencrypted sensitive personal data are advised to use bespoke platforms rather than open dissemination services like Zenodo for sharing their data
** See further the user pages regarding uploading for information on anonymisation of datasets that contain sensitive personal information.

重要的是关于内容责任的部分:

  • 如果你上传内容,你对其负有完全责任。

  • 我们必须确保我们的内容适合公众查看,并遵守所有相关法律和这些条款。这包括与隐私、数据保护和知识产权IP)相关的法律和条款。

  • 如果我们正在上传敏感的个人数据,我们必须确保它要么被适当匿名化,要么我们有权分享它。

我无法强调这一点——我们在 Zenodo 提供的信息是我们的责任,因此我们应该确保我们不会通过向每个人开放信息而造成任何伤害。如果有未匿名化的数据,我们应该考虑其他类型的存储;例如,需要身份验证或访问控制的存储,因此我的下一个最佳实践。

最佳实践 #72

任何个人数据都应该存储在身份验证和访问控制之后,以防止恶意行为者访问它。

尽管我们可能有权使用个人、非匿名化数据,但我们应将此类数据存储在身份验证之后。我们需要保护这些数据背后的个人。我们还需要使用访问控制和监控,以防我们需要回溯谁访问了数据;例如,当出现错误时。

合同和法律义务

为了完成这一章,我想讨论最后一个话题。尽管有大量数据可用,但我们必须确保我们做了尽职调查,并找出哪些合同和义务适用于我们。

许可证是一种合同类型,但并非唯一。几乎所有大学都会对研究人员施加合同和义务。这可能包括需要从伦理审查委员会请求许可或需要使数据可供其他研究人员审查。

专业行为准则是一种义务的另一种类型;例如,来自 ACM 的准则(www.acm.org/code-of-ethics)。这些行为准则通常源自于《纽伦堡法典》,并要求我们确保我们的工作是为了社会的利益。

最后,当与商业组织合作时,我们可能需要签署所谓的保密协议NDA)。此类协议通常是为了确保我们未经事先许可不泄露信息。它们常常被误解为需要隐藏信息,但在大多数情况下,这意味着我们需要确保我们报告的专有信息不会损害组织。在大多数情况下,我们可能需要确保我们的报告是关于一般实践,而不是特定公司。如果我们发现与我们的工业合作伙伴存在缺陷,我们需要与他们讨论,并帮助他们改进——因为我们需要为社会的最佳利益而工作。

因此,我强烈建议您了解哪些行为准则、义务和合同适用于您。

参考文献

  • Code, N., 《纽伦堡法典》。在控制委员会法律下的纽伦堡军事法庭对战争罪犯的审判,1949 年。第 10 卷(1949 年):第 181-2 页。

  • Wohlin, C. 等人,《软件工程中的实验》。2012 年:Springer Science & Business Media。

  • Gold, N.E. 和 J. Krinke,软件仓库挖掘中的伦理。实证软件工程,2022 年。第 27 卷第 1 期:第 17 页。

  • Kenneally, E. 和 D. Dittrich,《门洛报告》:指导信息和通信技术研究的伦理原则。可在 SSRN 2445102 找到,2012 年。

第十五章:机器学*系统的伦理

伦理涉及数据获取和管理,重点是收集数据,特别关注保护个人和组织免受可能对他们造成的任何伤害。然而,数据并不是机器学*(ML)系统中偏见的唯一来源。

算法和数据处理方式也容易引入数据偏见。尽管我们尽了最大努力,数据处理的一些步骤甚至可能强调偏见,使其超出算法范围,向基于机器学*的系统其他部分扩散,例如用户界面或决策组件。

因此,在本章中,我们将关注机器学*系统中的偏见。我们将首先探讨偏见的来源,并简要讨论这些来源。然后,我们将探讨发现偏见的方法、如何最小化偏见,以及最后如何向我们的系统用户传达潜在的偏见。

在本章中,我们将涵盖以下主要主题:

  • 偏见与机器学* – 是否可能拥有一个客观的人工智能?

  • 测量和监控偏见

  • 减少偏见

  • 制定机制以防止机器学*偏见在整个系统中扩散

偏见与机器学* – 是否可能拥有一个客观的人工智能?

在机器学*和软件工程的交织领域中,数据驱动决策和预测建模的吸引力无可否认。这些曾经主要在孤岛中运作的领域,现在在众多应用中汇聚,从软件开发工具到自动化测试框架。然而,随着我们越来越依赖数据和算法,一个紧迫的问题出现了:偏见问题。在这个背景下,偏见指的是在机器学*模型的决策和预测中表现出的系统性和不公平的差异,通常源于软件工程过程中的数据。

软件工程数据中偏见的来源是多方面的。它们可能源于历史项目数据、用户反馈循环,甚至软件本身的设计和目标。例如,如果一个软件工具主要使用特定人群的反馈进行测试和改进,它可能会无意中在那些群体之外的用户中表现不佳或行为不当。同样,如果训练数据来自缺乏团队构成或编码实践多样性的项目,缺陷预测模型可能会出现偏差。

这种偏见的后果不仅限于技术上的不准确。它们可能导致软件产品使某些用户群体感到疏远或处于不利地位,从而持续和放大现有的社会不平等。例如,一个开发环境可能对某一文化背景的提议比对另一文化背景的提议更响亮,或者一个软件推荐系统可能会偏向于知名开发者的应用程序,而忽视新来者。

通常,偏差被定义为对某个人或群体的倾向或偏见。在机器学*中,偏差是指模型系统地产生有偏见的结果。机器学*中存在几种类型的偏差:

  • 偏见偏差:这是一种存在于经验世界中并进入机器学*模型和算法中的偏差——无论是故意还是无意。一个例子是种族偏见或性别偏见。

  • 测量偏差:这是一种通过我们测量工具中的系统性错误引入的偏差。例如,我们通过计算 if/for 语句来衡量软件模块的 McCabe 复杂性,而排除 while 循环。

  • 采样偏差:这是一种当我们的样本不能反映数据的真实分布时出现的偏差。可能的情况是我们从特定类别中采样过于频繁或过于稀少——这种偏差会影响推理。

  • 算法偏差:这是一种在我们使用错误的算法来完成手头任务时出现的偏差。一个错误的算法可能无法很好地泛化,因此它可能会在推理中引入偏差。

  • 确认偏差:这是一种在我们移除/选择与我们要捕捉的理论概念一致的数据点时引入的偏差。通过这样做,我们引入了证实我们理论的偏差,而不是反映经验世界。

这个列表绝不是排他的。偏差可以通过许多方式以多种方式引入,但始终是我们的责任去识别它、监控它并减少它。

幸运的是,有一些框架可以让我们识别偏差——公平机器学*、IBM AI 公平 360 和微软 Fairlearn,仅举几个例子。这些框架允许我们仔细审查我们的算法和数据集,以寻找最常见的偏差。

Donald 等人最*概述了减少软件工程中偏差的方法和工具,包括机器学*。那篇文章的重要部分是它侧重于用例,这对于理解偏差很重要;偏差不是普遍存在的,而是取决于数据集和该数据的使用案例。除了之前提出的偏差来源外,他们还认识到偏差是随着时间的推移而变化的,就像我们的社会变化和我们的数据变化一样。尽管 Donald 等人的工作具有普遍性,但它倾向于关注一种数据类型——自然语言——以及偏差可能存在的方式。他们概述了可以帮助识别诸如仇恨言论等现象的工具和技术。

在本章中,然而,我们将关注一个稍微更通用的框架,以说明如何一般性地处理偏差问题。

测量和监控偏差

让我们看看这些框架中的一个——IBM AI 公平性 360(github.com/Trusted-AI/AIF360)。这个框架的基础是能够设置可以与偏见相关联的变量,然后计算其他变量之间的差异。所以,让我们深入一个如何计算数据集偏见的例子。由于偏见通常与性别或类似属性相关联,我们需要使用包含这种属性的数据集。到目前为止,在这本书中,我们还没有使用过包含这种属性的数据集,因此我们需要找到另一个。

让我们以泰坦尼克号生存数据集来检查男性和女性乘客在生存方面的偏见。首先,我们需要安装 IBM AI 公平性 360 框架:

pip install aif360

然后,我们可以开始创建一个检查偏见的程序。我们需要导入适当的库并创建数据。在这个例子中,我们将创建薪资数据,该数据倾向于男性:

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 aif360.datasets import BinaryLabelDataset
from aif360.metrics import BinaryLabelDatasetMetric
from aif360.algorithms.preprocessing import Reweighing
t i
data = {
    'Age': [25, 45, 35, 50, 23, 30, 40, 28, 38, 48, 27, 37, 47, 26, 36, 46],
    'Income': [50000, 100000, 75000, 120000, 45000, 55000, 95000, 65000, 85000, 110000, 48000, 58000, 98000, 68000, 88000, 105000],
    'Gender': [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1],  # 1: Male, 0: Female
    'Hired': [1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1]   # 1: Hired, 0: Not Hired
}
df = pd.DataFrame(data)

这份数据包含四个不同的属性——年龄、收入、性别以及是否建议雇佣这个人。很难发现性别之间是否存在偏见,但让我们应用 IBM 公平性算法来检查这一点:

# Split data into training and testing sets
train, test = train_test_split(df, test_size=0.2, random_state=42)
# Convert dataframes into BinaryLabelDataset format
train_bld = BinaryLabelDataset(df=train, label_names=['Hired'], protected_attribute_names=['Gender'])
test_bld = BinaryLabelDataset(df=test, label_names=['Hired'], protected_attribute_names=['Gender'])
# Compute fairness metric on original training dataset
metric_train_bld = BinaryLabelDatasetMetric(train_bld, unprivileged_groups=[{'Gender': 1}], privileged_groups=[{'Gender': 0}])
print(f'Original training dataset disparity: {metric_train_bld.mean_difference():.2f}')
# Mitigate bias by reweighing the dataset
RW = Reweighing(unprivileged_groups=[{'Gender': 1}], privileged_groups=[{'Gender': 0}])
train_bld_transformed = RW.fit_transform(train_bld)
# Compute fairness metric on transformed training dataset
metric_train_bld_transformed = BinaryLabelDatasetMetric(train_bld_transformed, unprivileged_groups=[{'Gender': 1}], privileged_groups=[{'Gender': 0}])
print(f'Transformed training dataset disparity: {metric_train_bld_transformed.mean_difference():.2f}')

上述代码创建了一个数据分割并计算了公平性指标——数据集差异。算法的重要部分在于我们设置了受保护属性——性别(protected_attribute_names=['Gender'])。我们手动设置了我们认为可能存在偏见的属性,这是一个重要的观察。公平性框架不会自动设置任何属性。然后,我们设置了该属性的哪些值表示特权组和非特权组——unprivileged_groups=[{'Gender': 1}]。一旦代码执行,我们就能了解数据集中是否存在偏见:

Original training dataset disparity: 0.86
Transformed training dataset disparity: 0.50

这意味着算法可以减少差异,但并没有完全消除。差异值 0.86 表示对特权组(在这种情况下是男性)存在偏见。值 0.5 表示偏见已经减少,但仍然远未达到 0.0,这会表明没有偏见。偏见减少而没有被消除的事实可能表明数据量太少,无法完全减少偏见。

因此,让我们看看实际的包含偏见的真实数据集——泰坦尼克号数据集。该数据集包含受保护属性,如性别,并且它非常大,这样我们就有更好的机会进一步减少偏见:

from aif360.datasets import BinaryLabelDataset
from aif360.metrics import BinaryLabelDatasetMetric
from aif360.algorithms.preprocessing import Reweighing
# Load Titanic dataset
url = "https://web.stanford.edu/class/archive/cs/cs109/cs109.1166/stuff/titanic.csv"
df = pd.read_csv(url)

现在我们已经准备好了数据集,我们可以编写脚本计算差异度量,该度量量化了基于控制变量的数据差异程度:

# Preprocess the data
df['Sex'] = df['Sex'].map({'male': 1, 'female': 0})  # Convert 'Sex' to binary: 1 for male, 0 for female
df.drop(['Name'], axis=1, inplace=True)  # Drop the 'Name' column
# Split data into training and testing sets
train, test = train_test_split(df, test_size=0.2, random_state=42)
# Convert dataframes into BinaryLabelDataset format
train_bld = BinaryLabelDataset(df=train, label_names=['Survived'], protected_attribute_names=['Sex'])
test_bld = BinaryLabelDataset(df=test, label_names=['Survived'], protected_attribute_names=['Sex'])
# Compute fairness metric on the original training dataset
metric_train_bld = BinaryLabelDatasetMetric(train_bld, unprivileged_groups=[{'Sex': 0}], privileged_groups=[{'Sex': 1}])
print(f'Original training dataset disparity: {metric_train_bld.mean_difference():.2f}')
# Mitigate bias by reweighing the dataset
RW = Reweighing(unprivileged_groups=[{'Sex': 0}], privileged_groups=[{'Sex': 1}])
train_bld_transformed = RW.fit_transform(train_bld)
# Compute fairness metric on the transformed training dataset
metric_train_bld_transformed = BinaryLabelDatasetMetric(train_bld_transformed, unprivileged_groups=[{'Sex': 0}], privileged_groups=[{'Sex': 1}])
print(f'Transformed training dataset disparity: {metric_train_bld_transformed.mean_difference():.2f}')

首先,我们需要将 DataFrame df 中的'Sex'列转换为二进制格式:男性为1,女性为0。然后,我们需要从 DataFrame 中删除'Name'列,因为它可能会与索引混淆。然后,使用train_test_split函数将数据分为训练集和测试集。20%的数据(test_size=0.2)保留用于测试,其余用于训练。random_state=42确保分割的可重复性。

接下来,我们将训练和测试的 DataFrame 转换为BinaryLabelDataset格式,这是公平框架使用的特定格式。目标变量(或标签)是'Survived',受保护的属性(即我们在公平性方面关注的属性)是'Sex'。该框架将女性('Sex': 0)视为无特权群体,将男性('Sex': 1)视为特权群体。

mean_difference方法计算特权群体和无特权群体之间平均结果的差异。0 值表示完全公平,而非零值表示存在一些差异。然后,代码使用Reweighing方法来减轻训练数据集中的偏差。这种方法通过给数据集中的实例分配权重来确保公平性。转换后的数据集(train_bld_transformed)具有这些新的权重。然后,我们在转换后的数据集上计算相同的指标。这导致以下输出:

Original training dataset disparity: 0.57
Transformed training dataset disparity: 0.00

这意味着算法已经平衡了数据集,使得男性和女性的生存率相同。现在我们可以使用这个数据集来训练一个模型:

# Train a classifier (e.g., logistic regression) on the transformed dataset
scaler = StandardScaler()
X_train = scaler.fit_transform(train_bld_transformed.features)
y_train = train_bld_transformed.labels.ravel()
clf = LogisticRegression().fit(X_train, y_train)
# Test the classifier
X_test = scaler.transform(test_bld.features)
y_test = test_bld.labels.ravel()
y_pred = clf.predict(X_test)
# Evaluate the classifier's performance
from sklearn.metrics import accuracy_score, classification_report
accuracy = accuracy_score(y_test, y_pred)
print(f"Accuracy: {accuracy:.4f}")
report = classification_report(y_test, y_pred, target_names=["Not Survived", "Survived"])
print(report)

首先,我们初始化StandardScaler。这个缩放器通过去除均值并缩放到单位方差来标准化特征。然后,我们使用缩放器的fit_transform方法对训练数据集的特征(train_bld_transformed.features)进行转换和标准化。标准化的特征存储在X_train中。然后,我们使用ravel()方法从转换后的训练数据集中提取标签,得到y_train。之后,我们使用标准化的特征(X_train)和标签(y_train)来训练逻辑回归分类器(clf)。

然后,我们使用缩放器的转换方法对测试数据集的特征(test_bld.features)进行标准化,以获得X_test。我们对y_test数据也进行同样的操作。我们使用训练好的分类器(clf)对标准化的测试特征进行预测,并将结果存储在y_pred中。

最后,我们计算数据集的评估分数,并打印包含准确率、精确率和召回率的报告。

这样,我们就来到了关于偏差的最佳实践。

最佳实践 #73

如果数据集包含可能存在偏差的变量,请使用差异指标来快速了解数据。

虽然我们并不总是能够访问用于其计算的变量,例如性别或年龄,检查偏差是很重要的。如果我们没有,我们应该寻找可以与之相关的属性,并检查对这些属性的偏差。

其他偏差度量标准

我们迄今为止使用的数据集差异度量标准只是与偏差相关的一些度量标准。IBM AI Fairness 360 框架中可用的其他一些度量标准如下:

  • 真正率:在受保护属性条件下的真正率的比率。这通常用于分类。

  • 假发现率:在分类任务中,特权组和未特权组之间假发现率的差异。

  • 通用二元混淆矩阵:在分类任务中对受保护属性进行混淆矩阵的条件。

  • 特权实例与未特权实例之间的比率,可用于各种任务。

除了这些之外,还有一些度量标准,但我们在这里提到的这些度量标准说明了最重要的观点——或者两个观点。首先,我们可以看到需要有一个属性,称为受保护属性,这可以帮助我们理解偏差。没有这样的属性,框架无法进行任何计算,因此它无法为开发者提供任何有用的反馈。第二个观点是,这些度量标准是基于不同群体之间——特权组和未特权组——的不平衡,这是我们自行定义的。我们不能使用这个框架来发现隐藏的偏差。

隐藏的偏差是指没有直接由属性表示的偏差。例如,男性和女性在职业上有差异,因此职业可以是一个与性别相关但不等于性别的属性。这意味着我们不能将其视为受保护属性,但我们需要考虑它——基本上,没有纯粹男性或纯粹女性的职业,但不同的职业有不同的男性和女性的比例。

开发机制以防止机器学*偏差在整个系统中传播

不幸的是,通常无法完全从机器学*中去除偏差,因为我们往往无法访问减少偏差所需的属性。然而,我们可以减少偏差并降低偏差传播到整个系统的风险。

意识和教育是我们可以用来管理软件系统偏差的最重要措施之一。我们需要了解偏差的潜在来源及其影响。我们还需要识别与受保护属性(例如,性别)相关的偏差,并确定其他属性是否可以与之相关联(例如,职业和地址)。然后,我们需要教育我们的团队了解偏差模型伦理影响。

然后,我们需要多样化我们的数据收集。我们必须确保我们收集的数据能够代表我们要建模的群体。为了避免过度或不足代表某些群体,我们需要确保在应用之前对数据收集程序进行审查。我们还需要监控收集到的数据中的偏差并减少它们。例如,如果我们发现信用评分中存在偏差,我们可以引入数据,以防止我们的模型加强这种偏差。

在数据预处理期间,我们需要确保我们正确处理缺失数据。而不仅仅是删除数据点或用平均值填充它们,我们应该使用正确的填充方法,这种方法会考虑到特权和不特权群体之间的差异。

我们还需要积极工作于偏差检测。我们应该使用统计测试来检查数据分布是否偏向于某些群体,此时我们需要可视化分布并识别潜在的偏差。我们已经讨论了可视化技术;在这个阶段,我们可以补充说,我们需要为特权和不特权群体使用不同的符号,以便在同一个图表上可视化两个分布,例如。

除了与数据合作外,我们还需要在模型设计时考虑算法公平性。我们需要设置公平性约束,并引入可以帮助我们识别特权和不特权群体的属性。例如,如果我们知道不同的职业对性别存在一定的偏见,我们需要引入表面上的性别偏见属性,以帮助我们创建一个考虑到这一点并防止偏差传播到系统其他部分的模型。我们还可以在训练后对模型进行事后调整。例如,在预测薪水时,我们可以在预测后根据预定义的规则调整那个薪水。这有助于减少模型中固有的偏差。

我们还可以使用公平性增强干预措施,例如 IBM 的公平性工具和技术,包括去偏差、重新加权以及消除不同影响。这可以帮助我们实现可解释的模型,或者允许我们使用模型解释工具来理解决策是如何做出的。这有助于识别和纠正偏差。

最后,我们可以定期审计我们的模型以检查偏差和公平性。这包括自动检查和人工审查。这有助于我们了解是否存在无法自动捕捉的偏差,以及我们需要做出反应的偏差。

有了这些,我们来到了我的下一个最佳实践。

最佳实践 #74

通过定期的审计来补充自动化偏差管理。

我们需要接受数据中固有的偏差这一事实,因此我们需要相应地采取行动。而不是依赖算法来检测偏差,我们需要手动监控偏差并理解它。因此,我建议定期手动检查偏差。进行分类和预测,并通过将它们与无偏差的预期数据进行比较来检查它们是否增强了或减少了偏差。

摘要

作为软件工程师,我们的一项责任是确保我们开发的软件系统对社会的大局有益。我们热爱与技术开发打交道,但技术的发展需要负责任地进行。在本章中,我们探讨了机器学*中的偏差概念以及如何与之合作。我们研究了 IBM 公平性框架,该框架可以帮助我们识别偏差。我们还了解到,自动偏差检测过于有限,无法完全从数据中消除偏差。

有更多的框架可以探索,每天都有更多的研究和工具可用。这些框架更加具体,提供了一种捕捉更多特定领域偏差的方法——在医学和广告领域。因此,在本章的最后,我的建议是探索针对当前任务和领域的特定偏差框架。

参考文献

  • Donald, A. 等,客户交互数据偏差检测:关于数据集、方法和工具的调查。IEEE Access,2023 年。

  • Bellamy, R.K. 等,AI 公平性 360:一个用于检测、理解和缓解不希望算法偏差的可扩展工具包。arXiv 预印本 arXiv:1810.01943,2018 年。

  • Zhang, Y. 等。人工智能公平性简介。在 2020 年 CHI 计算机系统人类因素会议扩展摘要中。2020 年。

  • Alves, G. 等。减少机器学*模型在表格和文本数据上的无意偏差。在 2021 年 IEEE 第 8 届数据科学和高级分析会议(DSAA)中。2021 年。IEEE。

  • Raza, S.,D.J. Reji 和 C. Ding,Dbias:检测新闻文章中的偏差并确保公平性。国际数据科学和分析杂志,2022 年: 第 1-21 页。

第十六章:在生态系统中集成机器学*系统

机器学*系统因其两个原因而获得了大量人气——它们从数据中学*的能力(我们已在整本书中探讨了这一点),以及它们被打包成网络服务的能力。

将这些机器学*系统打包成网络服务,使我们能够以非常灵活的方式将它们集成到工作流程中。我们不必编译或使用动态链接库,而是可以部署通过 HTTP 协议使用 JSON 协议进行通信的机器学*组件。我们已经看到了如何使用该协议,通过使用由 OpenAI 托管的 GPT-3 模型。在本章中,我们将探讨创建一个包含预训练机器学*模型的 Docker 容器、部署它以及将其与其他组件集成的可能性。

在本章中,我们将涵盖以下主要主题:

  • 机器学*系统——软件生态系统

  • 使用 Flask 在机器学*模型上创建网络服务

  • 使用 Docker 部署机器学*模型

  • 将网络服务组合成生态系统

生态系统

在软件工程的动态领域,工具、方法和范式处于不断演化的状态。推动这一变革的最有影响力的力量之一是机器学*。虽然机器学*本身是计算能力的奇迹,但它的真正天才在于将其集成到更广泛的软件工程生态系统中。本章深入探讨了在生态系统中嵌入机器学*的细微差别。生态系统是一组协同工作的软件,但在编译时并未连接。一个著名的生态系统是 PyTorch 生态系统,其中一系列库在机器学*环境中协同工作。然而,在软件工程中的机器学*生态系统中还有更多内容。

从从每次迭代中学*的自动化测试系统到适应用户行为的推荐引擎,机器学*正在重新定义软件的设计、开发和部署方式。然而,将机器学*集成到软件工程中并非简单的即插即用操作。它要求重新思考传统的工作流程,更深入地理解数据驱动决策,并致力于持续学*和适应。

随着我们深入探讨机器学*在软件工程中的集成,讨论两个关键组成部分变得至关重要:网络服务和 Docker 容器。这些技术虽然并非仅限于机器学*应用,但在软件生态系统中无缝部署和扩展机器学*驱动解决方案中发挥着关键作用。

Web 服务,特别是在微服务架构时代,为构建软件应用程序提供了一种模块化方法。通过将特定功能封装到不同的服务中,它们提供了更大的灵活性和可扩展性。当与机器学*模型结合使用时,Web 服务可以根据从数据中得出的见解提供动态的、实时的响应。例如,一个 Web 服务可能会利用机器学*模型为用户提供个性化的内容推荐或实时检测欺诈活动。

另一方面,Docker 容器已经彻底改变了软件(包括机器学*模型)的打包和部署方式。容器将应用程序及其所有依赖项封装到一个标准化的单元中,确保在不同环境中的一致行为。对于机器学*从业者来说,这意味着设置环境、管理依赖项和确保兼容性的繁琐过程得到了极大的简化。Docker 容器确保在开发者的机器上训练的机器学*模型将在生产服务器或任何其他平台上以相同的效率和精度运行。

此外,当 Web 服务和 Docker 容器结合使用时,它们为基于机器学*的微服务铺平了道路。这种架构允许快速部署可扩展的、隔离的服务,这些服务可以独立更新而不会干扰整个系统。这在机器学*领域尤其有价值,因为模型可能需要根据新数据或改进的算法进行频繁更新。

在本章中,我们将学*如何使用这两种技术来打包模型并创建基于 Docker 容器的生态系统。阅读本章后,我们将对如何通过将机器学*(ML)作为更大系统体系的一部分来扩展我们的开发有一个良好的理解——生态系统。

使用 Flask 在机器学*模型上创建 Web 服务

在这本书中,我们主要关注了机器学*模型的训练、评估和部署。然而,我们没有讨论对它们进行灵活结构化的需求。我们使用的是单体软件。单体软件的特点是统一的、单一的代码库结构,其中所有功能,从用户界面到数据处理,都是紧密交织并作为一个统一的单元运行的。这种设计简化了初始开发和部署,因为所有内容都捆绑在一起并一起编译。任何微小的变化都需要重新构建和重新部署整个应用程序。这使得当当代软件的演变速度很快时变得有问题。

另一方面,基于 Web 服务的软件,通常与微服务架构相关联,将应用程序分解为更小、独立的、通过网络通信的服务,通常使用 HTTP 和 REST 等协议。每个服务负责特定的功能并且独立运行。这种模块化方法提供了更大的灵活性。服务可以单独扩展、更新或重新部署,而不会影响整个系统。此外,一个服务的故障并不一定会导致整个应用程序崩溃。图 16.1展示了这两种类型软件之间的差异:

图 16.1 – 单体软件与基于 Web 服务的软件

图 16.1 – 单体软件与基于 Web 服务的软件

在左侧,所有组件都捆绑在一起成为一个产品。用户通过用户界面与产品交互。只有一个用户可以与软件交互,更多的用户需要更多的软件安装。

在右侧,我们有一个去中心化的架构,其中每个组件都是一个独立的 Web 服务。这些组件的协调是通过一个瘦客户端完成的。如果更多的用户/客户端想要使用相同的服务,他们只需使用 HTTP REST 协议(API)将它们连接起来。

这里是我的第一条最佳实践。

最佳实践#75

在部署机器学*模型到生产环境时使用 Web 服务(RESTful API)。

虽然创建 Web 服务需要额外的努力,但它们是值得使用的。它们提供了很好的关注点分离和异步访问,同时也提供了负载均衡的巨大可能性。我们可以使用不同的服务器来运行相同的 Web 服务,从而平衡负载。

因此,让我们使用 Flask 创建第一个 Web 服务。

使用 Flask 创建 Web 服务

Flask 是一个框架,它允许我们通过 HTTP 协议上的 REST 接口提供对内部 API 的便捷访问。首先,我们需要安装它:

pip install flask
pip install flask-restful

一旦我们安装了接口,我们就可以编写我们的程序。在这个例子中,我们的第一个 Web 服务计算发送给它的程序代码行数和复杂度。以下代码片段展示了这一点:

from fileinput import filename
from flask import *
from radon.complexity import cc_visit
from radon.cli.harvest import CCHarvester
app = Flask(__name__)
# Dictionary to store the metrics for the file submitted
# Metrics: lines of code and McCabe complexity
metrics = {}
def calculate_metrics(file_path):
    with open(file_path, 'r') as file:
        content = file.read()
    # Count lines of code
    lines = len(content.splitlines())
    # Calculate McCabe complexity
    complexity = cc_visit(content)
    # Store the metrics in the dictionary
    metrics[file_path] = {
        'lines_of_code': lines,
        'mccabe_complexity': complexity
    }
@app.route('/')
def main():
    return render_template("index.html")
@app.route('/success', methods=['POST'])
def success():
    if request.method == 'POST':
        f = request.files['file']
        # Save the file to the server
        file_path = f.filename
        f.save(file_path)
        # Calculate metrics for the file
        calculate_metrics(file_path)
        # Return the metrics for the file
        return metrics[file_path]
@app.route('/metrics', methods=['GET'])
def get_metrics():
    if request.method == 'GET':
        return metrics
if __name__ == '__main__':
    app.run(host='0.0.0.0', debug=True)

首先,代码需要一些导入,然后在app = Flask(__name__)中初始化应用程序。然后,它创建路由——即程序将通过 REST API 进行通信的地方:

  • @app.route('/'):这是一个装饰器,它定义了根 URL("/")的路由。当用户访问根 URL 时,它渲染"index.html"模板。

  • @app.route('/success', methods=['POST']):这个装饰器定义了一个针对"/success" URL 的路由,它期望 HTTP POST 请求。此路由用于处理文件上传、计算代码行数和计算麦卡贝复杂度。

  • @app.route('/metrics', methods=['GET']): 这个装饰器定义了"/metrics" URL 的路由,它期望 HTTP GET 请求。它用于检索和显示度量。

  • def main(): 这个函数与根("/")路由相关联。当用户访问根 URL 时,它返回一个名为 "index.html" 的 HTML 模板。

  • def success(): 这个函数与"/success"路由相关联,它处理文件上传:

    • 它检查请求方法是否为 POST

    • 它将上传的文件保存到服务器

    • 它计算上传文件的代码行数

    • 它使用 radon 库计算 McCabe 复杂性

    • 它将度量(代码行数和 McCabe 复杂性)存储在度量字典中

    • 它以 JSON 响应返回上传文件的度量

  • def get_metrics(): 这个函数与/metrics路由相关联:

    • 它检查请求方法是否为 GET。

    • 它返回整个度量字典作为 JSON 响应。这用于调试目的,以查看会话期间上传的所有文件的度量。

  • if __name__ == '__main__':: 这个块确保只有当此脚本直接执行时(而不是作为模块导入时)才会运行 Web 应用程序。

  • app.run(host='0.0.0.0', debug=True): 这以调试模式启动 Flask 应用程序,允许你在开发期间看到详细的错误消息。

然后,应用程序被执行 – app.run(debug=True) 以启用调试模式启动 Flask 应用程序。这意味着对代码所做的任何更改都将自动重新加载服务器,并且任何错误都将显示在浏览器中。一旦执行,就会出现以下网页(请注意,网页的代码必须位于templates子文件夹中,并且应包含以下代码):

<html>
<head>
    <title>Machine learning best practices for software engineers: Chapter 16 - Upload a file to make predictions</title>
</head>
<body>
    <h1>Machine learning best practices for software engineers - Chapter 16</h1>
    <p>This page allows to upload a file to a web service that has been written using Flask. The web application behind this interface calculates metrics that are important for the predictions. It returns a JSON string with the metrics.  </p>
    <p>We need another web app that contains the model in order to actually obtain predictions if the file can contain defects. </p>
    <h1>Upload a file to make predictions</h1>
    <p>The file should be a .c or .cpp file</p>
    <form action = "/success" method = "post" enctype="multipart/form-data">
        <input type="file" name="file" />
        <input type = "submit" value="Upload">
    </form>
    <p>Disclaimer: the container saves the file it its local folder, so don't send any sensitive files for analysis.</p>
    <p>This is a research prototype</p>
</body>
</html>

页面包含一个简单的表单,允许我们上传文件到服务器:

图 16.2 – 我们可以发送文件以计算代码行数的网页。这是将此信息发送到网络服务的一种方式

图 16.2 – 我们可以发送文件以计算代码行数的网页。这是将此信息发送到网络服务的一种方式

上传文件后,我们得到结果:

图 16.3 – 计算代码行数的结果

图 16.3 – 计算代码行数的结果

大多数传输都是由 Flask 框架完成的,这使得开发过程非常愉快。然而,仅仅计算代码行数和复杂性并不是一个很好的机器学*模型。因此,我们需要创建另一个包含机器学*模型本身的代码的 Web 服务。

因此,我的下一个最佳实践是关于双接口。

最佳实践#76

使用网站和 API 为 Web 服务。

虽然我们总是可以设计 Web 服务,使其只接受 JSON/REST 调用,但我们应尝试提供不同的接口。之前展示的 Web 界面允许我们测试 Web 服务,甚至向其发送数据,而无需编写单独的程序。

创建包含预训练 ML 模型的 Web 服务

ML 模型 Web 服务的代码遵循相同的模板。它使用 Flask 框架来提供 Web 服务的 REST API。以下是显示此 Web 服务的代码片段:

#
# This is a flask web service to make predictions on the data
# that is sent to it. It is meant to be used with the measurement instrument
#
from flask import *
from joblib import load
import pandas as pd
app = Flask(__name__)   # create an app instance
# entry point where we send JSON with two parameters:
# LOC and MCC
# and make prediction using make_prediction method
@app.route('/predict/<loc>/<mcc>')
def predict(loc,mcc):
    return {'Defect': make_prediction(loc, mcc)}
@app.route('/')
def hello():
    return 'Welcome to the predictor! You need to send a GET request with two parameters: LOC (lines of code) and MCC (McCabe complexity))'
# the main method for making the prediction
# using the model that is stored in the joblib file
def make_prediction(loc, mcc):
    # now read the model from the joblib file
    # and predict the defects for the X_test data
    dt = load('dt.joblib')
    # input data to the model
    input = {'LOC': loc,
             'MCC': mcc}
    # convert input data into dataframe
    X_pred = pd.DataFrame(input, index=[0])
    # make prediction
    y_pred = dt.predict(X_pred)
    # return the prediction
    # as an integer
    return int(y_pred[0])
# run the application
if __name__ == '__main__':
    app.run(debug=True)

此 Web 服务的主要入口点接受两个参数:@app.route('/predict/<loc>/<mcc>')。它使用这两个参数作为实例化模型并使用它进行预测的方法的参数 – make_prediction(loc, mcc)make_prediction方法从一个joblib文件中读取模型,并使用它来预测模块是否包含缺陷。我使用joblib来存储这个模型,因为它基于 NumPy 数组。然而,如果一个模型基于 Python 对象(例如,当它是一个来自 scikit-learn 库的估计器时),那么最好使用 pickle 而不是joblib。它返回包含结果的 JSON 字符串。图 16**.4说明了我们可以如何使用网页浏览器调用这个 Web 服务 – 而不是地址栏:

图 16.4 – 使用预测端点获取此模块中预测的缺陷数量

图 16.4 – 使用预测端点获取此模块中预测的缺陷数量

地址栏将参数发送给模型,响应是一个 JSON 字符串,表示这个模块很可能包含一个缺陷。嗯,这并不令人惊讶,因为我们说这个模块有 10 行代码,复杂度为 100(不切实际,但可能)。

这两个 Web 服务已经为我们提供了一个例子,说明了 REST API 可以多么强大。现在,让我们学*如何使用 Docker 打包它,这样我们就可以更容易地部署这些 Web 服务。

使用 Docker 部署 ML 模型

要创建一个包含我们新创建的 Web 服务(或两个)的 Docker 容器,我们需要在我们的系统上安装 Docker。一旦我们安装了 Docker,我们就可以用它来编译容器。

将 Web 服务打包到 Docker 容器中的关键部分是 Dockerfile。它是一个如何组装容器以及如何启动它的配方。如果你感兴趣,我在进一步阅读部分建议了一本关于 Docker 容器的优秀书籍,这样你可以了解更多关于如何创建比这本书中更高级的组件。

在我们的例子中,我们需要两个容器。第一个将是测量仪器的容器。该容器的代码如下:

FROM alpine:latest
RUN apk update
RUN apk add py-pip
RUN apk add --no-cache python3-dev
RUN pip install --upgrade pip
WORKDIR /app
COPY . /app
RUN pip --no-cache-dir install -r requirements.txt
CMD ["python3", "main.py"]

这个 Dockerfile 正在设置基于 Alpine Linux 的环境,安装 Python 和必要的开发包,将您的应用程序代码复制到镜像中,然后在容器启动时作为默认命令运行 Python 脚本。这是为 Python 应用程序创建 Docker 镜像的常见模式。让我们更仔细地看看:

  1. FROM alpine:latest: 这一行指定了 Docker 镜像的基础镜像。在这种情况下,它使用 Alpine Linux 发行版,这是一个轻量级和极简的发行版,通常用于 Docker 容器。latest 指的是 Docker Hub 上可用的 Alpine 镜像的最新版本。

  2. RUN apk update: 这个命令更新了 Alpine Linux 包管理器(apk)的包索引,以确保它有关于可用包的最新信息。

  3. RUN apk add py-pip: 这里,它安装了 py-pip 包,这是 Python 包的包管理器。这一步是使用 pip 安装 Python 包所必需的。

  4. RUN apk add --no-cache python3-dev: 这安装了 python3-dev 包,它提供了 Python 的开发文件。这些开发文件在编译或构建具有本地代码扩展的 Python 包时通常需要。

  5. RUN pip install --upgrade pip: 这个命令将 pip 包管理器升级到最新版本。

  6. WORKDIR /app: 这为后续命令设置了 /app 作为工作目录。这个目录是应用程序代码将被复制的地方,并且它成为运行命令的默认目录。

  7. COPY . /app: 这将当前目录(Dockerfile 所在的位置)的内容复制到 Docker 镜像中的 /app 目录。这通常包括应用程序代码,包括 requirements.txt

  8. RUN pip --no-cache-dir install -r requirements.txt: 这安装了在 requirements.txt 文件中指定的 Python 依赖项。--no-cache-dir 标志用于确保在安装过程中不使用缓存,这有助于减小 Docker 镜像的大小。

  9. CMD ["python3", "main.py"]: 这指定了从该镜像启动容器时默认要运行的命令。在这种情况下,它使用 python3 运行 main.py Python 脚本。这是我们基于此 Docker 镜像运行容器时将要执行的命令。

第 8 步 中,我们需要 requirements.txt 文件。在这种情况下,文件不需要太复杂——它需要使用与网络服务脚本相同的导入:

flask
flask-restful

现在,我们已经准备好编译 Docker 容器。我们可以通过以下命令从命令行完成:

docker build -t measurementinstrument .

一旦编译过程完成,我们可以启动容器:

docker run -t -p 5000:5000 measurementinstrument

前面的命令告诉 Docker 环境我们要启动名为 measurementinstrument 的容器,并将网络服务的端口(5000)映射到本地机器上的相同端口。现在,如果我们导航到该地址,我们可以上传文件,就像在没有 Docker 容器运行网络服务时一样。

最佳实践 #77

为了版本控制和可移植性,将你的网络服务 Docker 化。

使用 Docker 是确保我们的网络服务可移植性的方法之一。一旦我们将网络服务打包到容器中,我们就可以确信它将在任何能够运行 Docker 的系统上表现相同。这使得我们的生活比使用 requirements.txt 文件来设置 Python 环境更加容易。

一旦我们有了包含测量仪器的容器,我们可以将第二个网络服务(带有预测模型)打包到另一个网络服务中。下面的 Dockerfile 执行此操作:

FROM ubuntu:latest
RUN apt update && apt install python3 python3-pip -y
WORKDIR /app
COPY . /app
RUN pip --no-cache-dir install -q -r requirements.txt
CMD ["python3", "main.py"]

这个 Dockerfile 设置了一个基于 Ubuntu 的环境,安装 Python 3 和 pip,将你的应用程序代码复制到镜像中,从 requirements.txt 安装 Python 依赖项,然后在容器启动时运行 Python 脚本作为默认命令。请注意,我们在这里使用 Ubuntu 而不是 Alpine Linux。这不是偶然的。Alpine Linux 上没有 scikit-learn 软件包,因此我们需要使用 Ubuntu(该 Python 软件包可用):

  • FROM ubuntu:latest: 这一行指定了 Docker 镜像的基础镜像。在这种情况下,它使用 Ubuntu Linux 分发的最新版本作为基础镜像。latest 指的是 Docker Hub 上可用的 Ubuntu 镜像的最新版本。

  • RUN apt update && apt install python3 python3-pip -y: 这个命令用于更新 Ubuntu 软件包管理器 (apt) 的软件包索引,然后安装 Python 3 和 Python 3 的 pip-y 标志用于在安装过程中自动回答“是”以响应任何提示。

  • WORKDIR /app: 这个命令设置后续命令的工作目录为 /app。这个目录是应用程序代码将被复制的地方,并且它成为运行命令的默认目录。

  • COPY . /app: 这个命令将当前目录(Dockerfile 所在的位置)的内容复制到 Docker 镜像中的 /app 目录。

  • RUN pip --no-cache-dir install -q -r requirements.txt: 这个命令使用 pip 安装 requirements.txt 文件中指定的 Python 依赖项。这里使用的标志如下:

    • --no-cache-dir: 这个标志确保在安装过程中不使用缓存,这有助于减少 Docker 镜像的大小。

    • -q: 这个标志以静默模式运行 pip,意味着它将产生更少的输出,这可以使 Docker 构建过程不那么冗长。

  • CMD ["python3", "main.py"]:这指定了从该镜像启动容器时运行的默认命令。在这种情况下,它使用python3运行main.py Python 脚本。这是我们基于此 Docker 镜像运行容器时将执行的命令。

在这种情况下,需求代码稍微长一点,尽管不是特别复杂:

scikit-learn
scipy
flask
flask-restful
joblib
pandas
numpy

我们使用类似的命令编译 Docker 容器:

docker build -t predictor .

我们使用类似的命令执行它:

docker run -t -p 5001:5000 predictor

现在,我们应该能够使用相同的浏览器命令来连接。请注意,我们使用不同的端口,这样新的 web 服务就不会与之前的冲突。

将 web 服务组合成生态系统

现在,让我们开发将连接这两个 web 服务的软件。为此,我们将创建一个新的文件,该文件将向第一个 web 服务发送一个文件,获取数据,然后将数据发送到第二个 web 服务进行预测:

import requests
# URL of the Flask web service for file upload
upload_url = 'http://localhost:5000/success'  # Replace with the actual URL
# URL of the Flask web service for predictions
prediction_url = 'http://localhost:5001/predict/'  # Replace with the actual URL
def upload_file_and_get_metrics(file_path):
    try:
        # Open and read the file
        with open(file_path, 'rb') as file:
            # Create a dictionary to hold the file data
            files = {'file': (file.name, file)}
            # Send a POST request with the file to the upload URL
            response = requests.post(upload_url, files=files)
            response.raise_for_status()
            # Parse the JSON response
            json_result = response.json()
            # Extract LOC and mccabe_complexity from the JSON result
            loc = json_result.get('lines_of_code')
            mccabe_complexity = json_result.get('mccabe_complexity')[0][-1]
            if loc is not None and mccabe_complexity is not None:
                print(f'LOC: [3], McCabe Complexity: {mccabe_complexity}')
                return loc, mccabe_complexity
            else:
                print('LOC or McCabe Complexity not found in JSON result.')
    except Exception as e:
        print(f'Error: {e}')
def send_metrics_for_prediction(loc, mcc):
    try:
        # Create the URL for making predictions
        predict_url = f'{prediction_url}[3]/[4]'
        # Send a GET request to the prediction web service
        response = requests.get(predict_url)
        response.raise_for_status()
        # Parse the JSON response to get the prediction
        prediction = response.json().get('Defect')
        print(f'Prediction: {prediction}')
    except Exception as e:
        print(f'Error: {e}')
if __name__ == '__main__':
    # Specify the file path you want to upload
    file_path = './main.py'  # Replace with the actual file path
    # Upload the specified file and get LOC and McCabe Complexity
    loc, mcc = upload_file_and_get_metrics(file_path)
    send_metrics_for_prediction(loc, mcc)

这段代码演示了如何将文件上传到 Flask web 服务以获取度量,然后将这些度量发送到另一个 Flask web 服务进行预测。它使用requests库来处理两个服务之间的 HTTP 请求和 JSON 响应:

  • import requests:这一行导入了requests库,该库用于向 web 服务发送 HTTP 请求。

  • upload_url:这个变量存储了用于文件上传的 Flask web 服务的 URL。

  • prediction_url:这个变量存储了用于预测的 Flask web 服务的 URL。

  • upload_file_and_get_metrics

    • 这个函数接受file_path作为输入,它应该是你想要上传并获取度量的文件路径

    • 它向upload_url发送 POST 请求以上传指定的文件

    • 在上传文件后,它解析从文件上传服务收到的 JSON 响应

    • 它从 JSON 响应中提取"lines_of_code""mccabe_complexity"字段

    • 提取的度量被打印出来,并且函数返回它们

  • send_metrics_for_prediction

    • 这个函数接受loc(代码行数)和mcc(McCabe 复杂性)值作为输入

    • 它通过将locmcc值附加到prediction_url来构造用于预测的 URL

    • 它使用构造的 URL 向预测服务发送 GET 请求

    • 在收到预测后,它解析 JSON 响应以获取"Defect"

    • 预测结果被打印到控制台

  • if __name__ == '__main__':这个块指定了你想要上传并获取度量的文件路径(file_path)。它调用upload_file_and_get_metrics函数上传文件并获取度量(代码行数和 McCabe 复杂性)。然后,它调用send_metrics_for_prediction函数将这些度量发送给预测,并打印预测结果。

这个程序表明我们可以将我们的模型打包成一个 Web 服务(带或不带容器),然后使用它,正如图 16**.1所建议的那样。这种设计整个系统的方式使我们能够使软件更具可扩展性和健壮性。根据使用场景,我们可以调整 Web 服务并在多个不同的服务器上部署,以实现可扩展性和负载均衡。

摘要

在本章中,我们学*了如何使用 Web 服务和 Docker 部署机器学*模型。尽管我们只部署了两个 Web 服务,但我们可以看出它可以成为一个机器学*生态系统。通过分离预测和测量,我们可以分离计算密集型工作负载(预测)和数据收集部分的管道。由于模型可以部署在任何服务器上,我们可以重用服务器,从而减少这些模型的能源消耗。

有了这些,我们来到了这本书的最后一章技术章节。在下一章,我们将探讨机器学*的新趋势,并展望未来,至少是猜测未来。

参考文献

  • Masse, M., REST API 设计规则手册:设计一致的 RESTful Web 服务接口。2011: “O’Reilly Media, Inc.”.

  • Raj, P.,J.S. Chelladhurai 和 V. Singh,学* Docker。2015: Packt Publishing Ltd.

  • Staron, M.等,重症监护中的鲁棒机器学*——软件工程和医学视角。在 2021 年 IEEE/ACM 首届人工智能工程-人工智能软件工程(WAIN)研讨会。2021. IEEE.

  • McCabe, T.J., 复杂度度量。IEEE 软件工程 Transactions,1976(4): p. 308-320.

第十七章:摘要及下一步行动

这是本书的最后一章。我们学到了很多——从理解传统软件和基于机器学*的软件之间的区别开始。我们学*了如何处理数据,如何与算法合作。我们还探讨了如何部署模型,以及如何与机器学*进行道德合作。在本章中,我们将总结最佳实践,并试图一窥机器学*与软件工程交叉领域的未来发展趋势。

在本章中,我们将涵盖以下主要主题:

  • 要知道我们将走向何方,我们需要知道我们曾经在哪里

  • 最佳实践

  • 当前的发展

  • 我对未来看法

要知道我们将走向何方,我们需要知道我们曾经在哪里

我的计算机之旅始于 20 世纪 90 年代初,那时我拥有 Atari 800XL。一旦我接触到这台电脑,我就被它能够执行我所指示的事情的事实所震惊。我的第一个程序当然是 BASIC 语言编写的程序:

10 PRINT Hello world!
20 GOTO 10

这既不是一个结构良好的程序,也不是一个非常有用的程序,但那是我当时能做的所有事情。这个第一个程序塑造了我整个职业生涯,因为它激发了我对软件开发的兴趣。后来,在我的职业生涯中,我意识到专业的软件工程远不止是编写源代码和编译它。程序需要结构良好、文档齐全、设计合理、测试充分(以及其他许多事情)。这个观察结果反过来又塑造了我对软件工程作为一门学科的见解,它可以将自制的软件变成一件艺术品,如果维护得当,可以在很长时间内使用。那是在 2000 年代初。大约在 2015 年,我看到了机器学*的潜力,这也激发了我的兴趣。我参与的第一个项目是由我的一个同事完成的,他向我展示了如何使用随机森林分类器来解释编程语言代码并推断出缺陷。快进到今天,我们有变压器、扩散器、自动编码器和混合网络,它们可以用数据做惊人的事情。

然而,我们需要从人工智能开发转向人工智能工程,就像我们从软件开发转向软件工程一样。这把我们带到了这本书,我在书中展示了超过 70 个人工智能工程的最佳实践。这本书带领我们从机器学*和人工智能的基本概念(第一部分),通过处理数据(第二部分)到算法和模型(第三部分)。我们还探讨了设计、使用和部署机器学*系统的伦理和法律方面(第四部分)。

最佳实践

本书的第一部分包含了许多最佳实践,这是因为这些最佳实践与软件工程、设计以及关于机器学*的关键决策有关——例如,第一个最佳实践告诉我们何时使用(以及何时不使用)机器学*。

作为这本书的这一部分是关于软件工程中的机器学*景观,我们将讨论不同类型的模型和数据,并展示它们是如何结合在一起的。

本书第一部分的最佳实践列表在表 17.1中呈现:

ID 最佳实践
1 当你的问题关注数据而不是算法时,使用机器学*算法。
2 在开始开发机器学*系统之前,进行尽职调查,并确定要使用的正确算法组。
3 如果你的软件是安全关键的,确保你可以设计机制来防止由机器学*的概率性质引起的安全隐患。
4 将机器学*软件作为机器学*模型开发典型训练-验证-评估流程的补充进行测试。
5 在设计机器学*软件时,首先关注你的数据和要解决的问题,其次才是算法。
6 只有在你知道需要加强哪些数据属性时,才使用数据插补。
7 一旦你探索了要解决的问题并理解了可用的数据,决定你是否想使用监督学*、自监督学*、无监督学*或强化学*算法。
8 选择对你系统最相关的数据验证属性。
9 在手动探索参数搜索空间之后,使用 GridSearch 和其他算法。
10 总是在你的机器学*系统中包含监控机制。
11 根据数据的视角选择正确的数据库,而不是从系统的角度考虑。
12 如果可能,使用云基础设施,因为它可以节省资源并减少对专业知识的需要。
13 早期决定你的生产环境,并将你的流程与该环境对齐。
14 根据你需要解决的问题来设计整个软件系统,而不仅仅是机器学*模型。
15 尽可能减小图像的大小,并尽可能少地使用颜色,以减少系统的计算复杂度。
16 在可能的情况下,使用参考数据集进行基准测试。
17 在可能的情况下,使用为特定任务预训练的模型。
18 可视化你的原始数据,以了解你的数据中的模式。
19 当数据被转换为特征以进行监控时,可视化你的数据,以检查是否仍然可以观察到相同的模式。
20 只使用必要的输入信息作为机器学*模型的输入。过多的信息可能需要额外的处理,并对系统提出不必要的性能要求。
21 当任务需要检测和跟踪对象时,在数据中使用边界框。
22 当你需要获取图像的上下文或需要特定区域的详细信息时,请使用语义图。
23 使用预训练的嵌入模型,如 GPT-3 或现有的 BERT 模型,来向量化你的文本。
24 在设计需要提供基于事实的决策的软件时,请使用角色标签。
25 确定您软件中使用的数据的来源,并据此创建您的数据处理管道。
26 提取您所需的所有数据,并将其存储在本地以减少使用该工具进行工作的软件工程师的干扰。
27 当从公共存储库访问数据时,请检查许可证,并确保您承认创建了分析代码的社区的贡献。
28 减少噪声对机器学*分类器影响的最佳策略是移除噪声数据点。
29 平衡特征数量与数据点数量。特征数量多并不总是更好。
30 对于分类任务的数据,使用KNNImputer;对于回归任务的数据,使用IterativeImputer
31 使用随机森林分类器来减少属性噪声,因为它提供了非常好的性能。
32 尽可能保留数据的原始分布,因为它反映了经验观察。
33 在大规模软件系统中,如果可能,依靠机器学*模型来处理数据中的噪声。

表 17.1:本书第一部分的最佳实践

本书第二部分讲述了数据获取与管理。我们专注于从数据的角度进行机器学*——如何在数据中找到方向,存在哪些类型的数据,以及如何处理它们。

因此,以下最佳实践与数据相关:

ID 最佳实践
34 当处理数值数据时,首先可视化它,从数据的汇总视图开始。
35 在可视化数据聚合层面时,关注值之间的关系和连接的强度。
36 深入分析个体分析应受当前机器学*任务的指导。
37 当可视化图像的元数据时,确保您可视化图像本身。
38 文本数据的汇总统计有助于对数据进行合理性检查。
39 如果数据复杂但任务简单,请使用特征工程技术——例如,创建一个分类模型。
40 如果数据在某种程度上是线性的并且具有相似的尺度,请使用 PCA。
41 如果您不知道数据的属性并且数据集很大(>1,000 个数据点),请使用 t-SNE。
42 当数据集非常大时(因为自动编码器复杂且需要大量数据进行训练),使用自动编码器对数值数据进行处理。
43 在瓶颈处开始使用少量神经元——通常是列数的三分之一。如果自动编码器没有学*,则逐步增加数量。
44 对于 BERT 等大型语言模型,使用标记化器;对于简单任务,使用词嵌入。
45 当您的任务需要预定义的单词集时,请使用 BoW 分词器与字典一起使用。
46 将 WordPiece 分词器作为首选。
47 当您处理大型语言模型和大型文本语料库时,请使用 BPE。
48 当空白符很重要时,请使用 sentence piece 分词器。
49 使用词嵌入(FastText)作为设计分类器或文本的默认模型。

表 17.2:本书第二部分的最佳实践

一旦我们处理了数据,我们就专注于算法,这是机器学*的另一部分。

在本书的第三部分《设计和开发机器学*系统》中,我们的重点是软件。我们首先探讨了哪些算法存在以及如何选择最佳算法。从 AutoML 开始,并与 Hugging Face 合作,为我们提供了一个良好的平台,以学*如何训练和部署机器学*系统。

这里是它的最佳实践:

ID 最佳实践
50 在训练经典机器学*模型时,将 AutoML 作为首选。
51 从 Hugging Face 或 TensorFlow Hub 开始使用预训练模型。
52 与预训练网络一起工作,以确定它们的局限性,然后在您的数据集上训练网络。
53 而不是寻找更复杂的模型,创建一个更智能的管道。
54 如果您想理解您的数值数据,请使用提供可解释性的模型,例如决策树或随机森林。
55 最佳模型是那些能够捕捉数据中经验现象的模型。
56 简单但可解释的模型通常可以很好地捕捉数据。
57 总是确保训练集和测试集中的数据点是分开的。
58 使用 NVidia CUDA(加速计算)来训练 BERT、GPT-3 和自编码器等高级模型。
59 除了监控损失之外,确保您可视化生成的实际结果。
60 检查生成式 AI 模型的输出,以确保它不会破坏整个系统或提供不道德的回应。
61 模型卡片应包含有关模型如何训练、如何使用它、它支持哪些任务以及如何引用模型的信息。
62 尝试不同的模型以找到最佳管道。
63 使用像 Pytest 这样的专业测试框架。
64 根据您的训练数据设置测试基础设施。
65 将模型视为单元,并相应地为其准备单元测试。
66 识别机器学*部署的关键方面,并相应地监控这些方面。
67 设计机器学*模型的用户界面时,请专注于用户任务。
68 准备您的模型以进行 Web 部署。
69 尝试使用内存数据库,并非常频繁地将它们转储到磁盘。

表 17.3:本书第三部分的最佳实践

在本书的最后部分,“数据管理和机器学*系统开发中的伦理问题”,我们从伦理和法律的角度探讨了与机器学*相关联的挑战。我们提供了一些解决方案,但它们不能取代人类的判断和智慧。在最后一章,我们回到了更技术性的部分,教您如何与生态系统合作,通过开辟新的途径——微服务和 Docker 来结束本书:

ID 最佳实践
70 如果您的模型/软件旨在帮助用户完成日常任务,请确保将其开发为附加组件。
71 如果您出于非商业目的创建自己的数据,请使用限制您责任的宽松衍生许可证之一。
72 仅限于研究源代码和其他工件,并且只有在主体同意的情况下才使用个人数据。
73 应当将任何个人数据存储在身份验证和访问控制之后,以防止恶意行为者访问。
74 如果数据集包含可能存在偏差的变量,请使用差异度量来快速了解数据。
75 结合定期的审计来补充自动化偏差管理。
76 在将机器学*模型部署到生产时,请使用网络服务。
77 为网络服务同时使用网站和 Python API。
78 将您的网络服务 Docker 化,以实现版本控制和可移植性。

表 17.4:本书第四部分的最佳实践

本书提出的最佳实践基于我在设计、开发、测试和部署机器学*系统方面的经验。这些最佳实践并不全面,根据我们的兴趣,我们可能会找到更多。

为了寻找这些最佳实践,我强烈建议您查看该领域持续出现的科研出版物。特别是,我建议关注人工智能的主要会议——即 NeurIPS 和 AAAI——以及软件工程的主要会议——即 ICSE、ASE 和 ESEC/FSE。

当前发展

在撰写这本书的时候,技术创新研究所(www.tii.ae/)刚刚发布了其最大的模型——Falcon 170B。这是与 GPT-3.5 模型相似的最大全开源模型。它展示了大型语言模型研究的当前方向。

尽管存在 GPT-4 这样的模型,其规模比 GPT-3.5 大 1000 倍,但我们仍然可以使用像 GPT-3.5 这样适度大的模型来开发非常好的软件。这让我们想到了一些我们作为社区需要讨论的当前话题。其中之一是这些模型的能源可持续性。Falcon-170B 需要 400 GB 的 RAM 来执行(根据 Hugging Face 的说法)。我们不知道 GPT-4 模型需要多少硬件。它消耗的电力和使用的资源必须与从该模型中获得的价值相当。

在机器学*方面,我们也接*了传统计算能力的极限,这让我们想到了量子计算(Cerezo,Verdon 等人,2022)。然而,尽管将机器学*嵌入到量子计算形式主义中的想法非常吸引人,但它并非没有挑战。机器学*系统/模型需要重新设计以适应新的范式,而新的范式需要更容易为 AI 工程师和软件工程师所接受。

当谈到算法时,越来越受到关注的是蜂群算法,这主要归功于物联网(IoT)背后的关注(Rosenberg 2016,Rajeesh Kumar,Jaya Lakshmi 等人,2023)。在 5G 和 6G 等电信技术的支持下,这些发展显然正在上升,并将继续上升。然而,当前的一个挑战是协议和技术的碎片化,这导致与网络安全和针对关键基础设施的攻击相关的问题。

另一个目前正在增长的研究领域是图神经网络(Gao,Zheng 等人,2023)。由于当前的架构仅限于线性数据或可以线性化的数据,它们无法处理某些任务。编程语言模型就是这样一个领域的例子——尽管它们能够执行许多任务,但目前它们无法考虑执行图或类似元素。因此,图神经网络被视为下一个发展方向,尽管在那里需要解决的主要挑战与图神经网络训练和推理所需的计算能力有关。

同时,越来越多的研究正在解决模型及其软性方面的发展,如伦理、可解释性和它们对整个社会的影响(Meskó和 Topol 2023)。医学、大型语言模型和社会的数字化只是正在进行的密集标准化工作的几个领域。

我对未来观点的看法

基于我对当前机器学*领域的观察,特别是关注软件工程,我可以看到一些趋势。

语言模型将更好地完成软件工程任务,如需求、测试和文档编写。这意味着软件工程师将能够专注于他们的核心工作——工程软件——而不是枯燥、重复的任务。我们将看到能够测试软件、编写文档、解释软件,甚至可能修复软件的模型。该领域的最新进展非常有希望。

混合模型将更加流行。结合符号分析和神经网络将获得动力,并能够帮助我们找到软件中的高级漏洞,以及在它们被利用之前识别它们。这将使我们的软件随着时间的推移变得更加健壮和有弹性。

大型模型和强大的计算能力也将帮助我们检测软件操作中的异常。通过分析日志,我们将能够预测老化,并将能够指导维护工作。再次强调,我们将能够专注于软件工程,而不是分析。

最后但同样重要的是,混合模型将帮助我们进行设计任务。结合图像生成和其他类型的方法将帮助我们设计软件——其架构和详细设计,甚至预测软件的操作性能。

最后的评论

我希望这本书对您来说很有趣,并且您在机器学*、人工智能和工程软件方面获得了新的知识。我希望您可以将这本书作为参考,并使用相关的代码来创建新产品。如果您喜欢这本书,请告诉我,通过 LinkedIn 与我联系,并为这本书的 GitHub 仓库做出贡献。我会监控它,并整合您可能提出的所有 pull requests。

在我们分别之前,我还有一个最后的最佳实践。

最佳实践#79

永远不要停止学*。

以一位大学教授的身份来说,机器学*领域发展迅速,几乎每周都有新的模型被引入。请确保您关注该领域的科学出版物和商业发展。这将帮助您保持知识的更新,并帮助您在职业生涯中取得进步。

参考文献

  • Cerezo, M., G. Verdon, H.-Y. Huang, L. Cincio 和 P. J. Coles (2022). 量子机器学*中的挑战与机遇。自然计算科学 2(9): 567-576.

  • Gao, C., Y. Zheng, N. Li, Y. Li, Y. Qin, J. Piao, Y. Quan, J. Chang, D. Jin 和 X. He (2023). 推荐系统中图神经网络的研究综述:挑战、方法和方向**。ACM 推荐系统交易 1(1): 1-51.

  • Meskó, B.和 E. J. Topol (2023). 在医疗保健中对大型语言模型(或生成式 AI)进行监管审查的必要性。npj 数字医学 6(1): 120.

  • 拉杰什·库马尔,N. V.,N.贾亚拉什米,B.马拉拉和 V.贾达夫 (2023). 基于战斗竞争蜂群优化的物联网安全信任感知多目标路由协议。 人工智能评论

  • 罗森伯格,L. (2016). 人工蜂群智能:人工智能中的人机交互方法。 AAAI 人工智能会议论文集

第十八章:索引

由于本电子书版本没有固定的页码,以下页码仅为参考,基于本书的印刷版。

A

抽象语法树 (AST) 58

ACM

URL 277

高级模型

AEs 199

经典机器学*,迁移到 198

转换器 200, 201

高级文本处理

输出可视化 55 - 57

AE 199

训练和评估 210 - 217

人工智能工程 242

AIMQ 模型

质量维度 77, 78

算法 23

数据,准备 24

亚马逊网络服务

URL 38

注释 59, 60, 198

曲线下面积/接收器操作特性 (AUROC) 15

属性噪声

消除 95 - 100

自动编码器 138 - 142

B

词袋分词器 149 - 151

BERT 模型 172, 173

最佳实践 306 - 311

偏差 280

偏差,机器学*

算法偏差 280

确认偏差 280

测量偏差 280

测量 281 - 285

指标 285

监控 281 - 285

有偏偏差 280

预防机制,开发 286, 287

样本偏差 280

从转换器中提取的双向编码表示 (BERT) 197

字节对编码 (BPE) 202

分词器 153 - 155

C

经典机器学*模型 164, 166

聚类算法 166

神经网络 166

回归模型 166

训练 181 - 186

基于树的模型 166

经典机器学*

迁移到 GenAI 198

code2vec 58

URL 58

代码补全模型

部署,作为扩展 255 - 261

逗号分隔值 (CSV) 文件 242

计算机科学

道德 266

计算统一设备架构 (CUDA) 209

利益 209

配置 31 - 33

持续集成和持续部署 (CI/CD) 224

持续集成 (CI) 59

合同和法律义务 277

卷积神经网络 (CNN) 6, 43, 142, 168 - 171

当前发展 311

D

黑工厂 25

数据

清洗 91 - 94

分割 100 - 104

来自人类的数据

道德使用 275, 276

数据收集 24 - 26

数据验证 28 - 30

示例 25

特征提取 27, 28

数据泄露问题 194, 195

数据管理

处理噪声数据 95

数据操作 8

数据质量 77

评估 78 - 83

数据来源 69 - 72

数据存储 244 - 248

数据结构 53

可视表示 55

哈尔辛基宣言(DoH)266

深度卷积神经网络(DCNNs)43, 44

深度伪造 199

深度学*(DL)42, 222

深度学*模型

训练 190 - 193

Docker

用于部署机器学*模型 298 - 301

Docker 容器 298

E

生态系统 289, 290

网络服务,结合 301 - 304

Elasticsearch

URL 37

Elasticsearch, Logstash, Kibana(ELK)239

心电图(ECG)71

脑电图(EEG)71

脑电图(EEG)信号 136

F

假阳性率(FPR)15

FastText 158, 159

基于特征的管道 233

特征工程 127 - 129

基础知识 89 - 91

用于图像数据 142 - 144

在典型的机器学*流程中 130, 131

特征工程,用于数值数据

自动编码器 138 - 142

独立成分分析(ICA)136

线性判别分析(LDA)138

局部线性嵌入(LLE)137, 138

主成分分析(PCA)131 - 133

t-SNE 134, 135

Flask 292

用于创建网络服务 292 - 296

G

生成对抗网络(GANs)198

生成式人工智能(GenAI)197

经典机器学*,迁移到 198

生成式机器学*模型

部署,用于图像 253, 254

生成预训练变换器-3(GPT-3)22

生成预训练转换器(GPT)197,222

Gerrit 72,75

数据,从 72,73 提取

Git 75

GitHub 75

数据,从 75 - 77 提取

谷歌云

URL 38

GPT 模型 172,173

基于事实的模型 65

H

Hugging Face

模型,部署到 225 - 229

模型,从 229 下载

URL 63

I

独立成分分析(ICA)136

图像数据 121 - 123

图像处理 168 - 171

基础设施和资源管理 35

计算基础设施 38,39

数据服务基础设施 36,37

知识产权(IP)权利 276

电影数据库(IMDb)123

物联网(IoT)311

求交并(IoU)阈值 170

J

JIRA 74,75

数据,从 74 提取

Jupyter Notebook

URL 38

L

语言模型

在软件系统中使用 174,175

大型语言模型(LLMs)45,148

线性判别分析(LDA)138

局部线性嵌入(LLE)137,138

记录 83

M

机器学*即服务(MLaaS)22

机器学*(ML)3,4,6,41,265

最佳实践 8,10 - 17

偏差 280

经典机器学*模型 164 - 168

经典机器学*系统 42

深度学*系统 42

模型类型 164

管道 39

原始数据,与特征 42 - 47

使用,与概率 13, 14

与传统软件相比 7, 8

机器学*系统

元素 21

掩码语言建模 (MLM) 201, 234

均方误差 (MSE) 15

偏差指标

误发现率 285

通用二元混淆矩阵 285

真阳性率 285

微软 Azure

URL 39

机器学*模型

数据集噪声处理 104, 105

部署,针对数值数据 248 - 252

部署,使用 Docker 298 - 301

用户界面 242 - 244

MLOps

元素 223, 224

机器学*管道 222

组件 222, 223

模型,部署到 Hugging Face 225 - 229

模型,从 Hugging Face 下载 229

测试 233 - 239

使用 224

机器学*系统

运行时监控 239

监控 34, 35

道德权 270

个性权 270

隐私权 270

公开权 270

归属权 270

完整权 270

放弃或不行使权利 271

多头注意力 (MHA) 204

多头自注意力 (MHSA) 200

多模态数据模型 66

N

自然语言 147

自然语言处理 (NLP) 200, 229

噪声 84 - 86

非公开协议 (NDA) 277

非极大值抑制 (NMS) 169

非宽松的开源许可证 267

数值数据 109 - 111

相关性 114 - 117

单一度量,摘要 117 - 119

度量,减少 119

主成分分析 (PCA) 119 - 121

摘要 111 - 114

O

OpenCV 43

开源系统

数据的道德使用 272 - 275

光学字符识别 (OCR) 8, 43

词汇表外 (OOV) 203

过拟合 31

P

成对属性噪声检测算法 (PANDA) 95

词性 (POS) 63

主成分分析 (PCA) 131 - 133

感知任务 61

宽松许可证

来自创意共享 (CC) 269

宽松的开源许可证 267

位置编码 200

预训练 25

概率

使用,与机器学*软件 13, 14

生产级机器学*系统

元素 22, 23

排他性许可证 267

pygerrit2 包 72

R

随机森林和不透明模型 187 - 189

基于原始数据的管道 229

对于图像 231 - 233

用于 NLP 相关任务 230, 231

原始图像数据 47 - 53

循环神经网络 (RNNs) 44, 200

强化学* 9

训练过程 16

奖励函数 9

RoBERTa 模型

评估 201 - 209

训练 201 - 209

优化稳健的 BERT 方法 (RoBERTa) 197

S

安全笼

开发,以防止模型破坏系统 218

自监督模型 9

语义地图 61

语义角色标注 (SRL) 工具

URL 63

句子分割标记器 155

情感分析 (SA) 45, 230

软件工程

伦理 266

软件系统

语言模型,使用 174, 175

软件测试 16 - 18

集成测试 16

系统和验收测试 17

单元测试 17

源系统 69

统计学* 4

结构化文本

程序源代码 57 - 59

监督学* 9

模型评估过程 15

T

任务 60

t-分布随机邻域嵌入 (t-SNE) 56

张量处理单元 (TPUs)

URL 38

测试过程 180

文本分析 53 - 55

文本标注

用于意图识别 62 - 65

文本数据 123 - 126

标记化 148

标记化器 149

传统软件 6

示例 10

与机器学* 7 对比

训练过程 180 - 187

数据泄露问题 194, 195

多次训练,一次预测原则 25

一次训练,多次预测原则 25

真阳性率 (TPR) 15

t-SNE 134, 135

U

过度拟合 31

未归一化模型 65

Unsplash 许可证 268

无监督学* (UL) 9, 198

V

变分自动编码器 (VAE) 199

版本控制系统 (VCSs) 224

W

网络服务

结合,进入生态系统 301 - 303

创建,包含预训练的 ML 模型 296, 297

使用 Flask 创建 ML 模型 290 - 296

Windows Subsystem for Linux 2.0 (WSL 2) 167

词嵌入 157, 158

WordPiece 分词器 151 - 153

Packt Logo

www.packtpub.com

订阅我们的在线数字图书馆,即可全面访问超过 7,000 本书籍和视频,以及行业领先的工具,帮助您规划个人发展并推进您的职业生涯。更多信息,请访问我们的网站。

第十九章:为什么订阅?

  • 使用来自 4,000 多名行业专业人士的实用电子书和视频,节省学*时间,多花时间编码

  • 通过为您量身定制的技能计划提高学*效果

  • 每月免费获得一本电子书或视频

  • 完全可搜索,便于轻松访问关键信息

  • 复制和粘贴、打印和收藏内容

您知道 Packt 为每本书都提供电子书版本,并提供 PDF 和 ePub 文件吗?您可以在www.packtpub.com升级到电子书版本,并且作为印刷书客户,您有权获得电子书副本的折扣。有关更多信息,请联系我们 customercare@packtpub.com。

www.packtpub.com,您还可以阅读一系列免费的技术文章,订阅各种免费通讯,并享受 Packt 书籍和电子书的独家折扣和优惠。

您可能还会喜欢的其他书籍

如果您喜欢这本书,您可能还会对 Packt 的其他书籍感兴趣:

实用机器学*在 Databricks[(https://packt.link/9781801812030)]

Databricks 上的 实用机器学*

Debu Sinha

ISBN: 978-1-80181-203-0

  • 从 DIY 设置顺利过渡到 Databricks

  • 掌握 AutoML 以快速设置 ML 实验

  • 自动化模型重新训练和部署

  • 利用 Databricks 功能存储进行数据准备

  • 使用 MLflow 进行有效的实验跟踪

  • 获取可扩展 ML 解决方案的实用见解

  • 了解如何在生产环境中处理模型漂移

不平衡数据的机器学*[(https://packt.link/9781801070836)]

不平衡数据的机器学*[(https://packt.link/9781801070836)]

不平衡数据机器学*

Kumar Abhishek, Dr. Mounir Abdelaziz

ISBN: 978-1-80107-083-6

  • 在机器学*模型中有效地使用不平衡数据

  • 探索类别不平衡时使用的指标

  • 了解何时以及如何应用各种采样方法,如过采样和欠采样

  • 应用基于数据、算法和混合方法来处理类别不平衡

  • 在避免常见陷阱的同时,从各种数据平衡选项中进行组合和选择

  • 了解在处理不平衡数据集的背景下,模型校准和阈值调整的概念

Packt 正在寻找像您这样的作者

如果你有兴趣成为 Packt 的作者,请访问authors.packtpub.com并今天申请。我们与成千上万的开发者和技术专业人士合作,就像你一样,帮助他们将见解分享给全球技术社区。你可以提交一般申请,申请我们正在招募作者的特定热门话题,或者提交你自己的想法。

分享你的想法

现在你已经完成了《软件工程师的机器学*基础设施和最佳实践》,我们非常想听听你的想法!如果你在亚马逊购买了这本书,请点击此处直接跳转到该书的亚马逊评论页面并分享你的反馈或在该购买网站上留下评论。

你的评论对我们和整个技术社区都很重要,并将帮助我们确保我们提供高质量的内容。

下载这本书的免费 PDF 副本

感谢你购买这本书!

你喜欢在路上阅读,但又无法携带你的印刷书籍到处走?

你的电子书购买是否与您选择的设备不兼容?

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

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

优惠远不止于此,你还可以获得独家折扣、时事通讯和每日免费内容的每日电子邮件。

按照以下简单步骤获取这些好处:

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

下载这本书的免费 PDF 副本

packt.link/free-ebook/978-1-83763-406-4

  1. 提交你的购买证明

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

posted @ 2025-09-04 14:12  绝不原创的飞龙  阅读(3)  评论(0)    收藏  举报