每个程序员都应该知道的-40-个算法-全-
每个程序员都应该知道的 40 个算法(全)
原文:
zh.annas-archive.org/md5/8ddea683d78e7bd756401ec665273969译者:飞龙
前言
算法在计算机科学和实践中一直扮演着重要角色。本书侧重于利用这些算法来解决现实世界的问题。要充分利用这些算法,对它们的逻辑和数学有更深入的理解是必不可少的。您将从算法介绍开始,探索各种算法设计技术。接着,您将了解线性规划、页面排名和图表,甚至使用机器学习算法,理解它们背后的数学和逻辑。本书还包含案例研究,如天气预测、推文聚类和电影推荐引擎,将向您展示如何最优地应用这些算法。完成本书后,您将自信地使用算法解决现实世界的计算问题。
本书适合对象
本书适合严肃的程序员!无论您是经验丰富的程序员,希望更深入地了解算法背后的数学,还是对编程或数据科学知识有限,想了解如何利用经过实战检验的算法来改进设计和编写代码的方式,您都会发现本书很有用。必须具备 Python 编程经验,尽管了解数据科学有所帮助,但并非必需。
本书涵盖内容
第一章《算法概述》总结了算法的基本原理。它从需要理解不同算法工作原理的基本概念开始。它总结了人们如何开始使用算法来数学地表达某些类别的问题。它还提到了不同算法的局限性。接下来的部分解释了指定算法逻辑的各种方法。由于本书使用 Python 编写算法,因此接下来解释了如何设置环境以运行示例。然后,讨论了衡量算法性能并与其他算法进行比较的各种方法。最后,本章讨论了验证算法特定实现的各种方法。
第二章《算法中使用的数据结构》着重于算法对必要的内存数据结构的需求,这些数据结构可以保存临时数据。算法可以是数据密集型、计算密集型或两者兼而有之。但对于所有不同类型的算法,选择正确的数据结构对于它们的最佳实现至关重要。许多算法具有递归和迭代逻辑,并且需要基本上是迭代性质的专门数据结构。由于本书使用 Python,本章重点介绍了可以用于实现本书讨论的算法的 Python 数据结构。
第三章《排序和搜索算法》介绍了用于排序和搜索的核心算法。这些算法以后可以成为更复杂算法的基础。本章首先介绍了不同类型的排序算法。它还比较了各种方法的性能。然后,介绍了各种搜索算法。它们进行了比较,并量化了它们的性能和复杂性。最后,本章介绍了这些算法的实际应用。
第四章《设计算法》介绍了各种算法的核心设计概念。它还解释了不同类型的算法,并讨论了它们的优缺点。在设计复杂算法时,理解这些概念是很重要的。该章首先讨论了不同类型的算法设计。然后,它提出了著名的旅行推销员问题的解决方案。接着讨论了线性规划及其局限性。最后,它提出了一个实际例子,展示了线性规划如何用于容量规划。
第五章《图算法》专注于计算机科学中常见的图问题的算法。有许多计算问题最好以图的术语表示。本章介绍了表示图和搜索图的方法。搜索图意味着系统地沿着图的边缘访问图的顶点。图搜索算法可以发现关于图结构的许多信息。许多算法首先通过搜索它们的输入图来获得这些结构信息。几种其他图算法详细介绍了基本的图搜索。搜索图的技术是图算法领域的核心。第一部分讨论了图的两种最常见的计算表示形式:邻接表和邻接矩阵。接下来介绍了一种简单的图搜索算法,称为广度优先搜索,并展示了如何创建广度优先树。接着介绍了深度优先搜索,并提供了一些关于深度优先搜索访问顶点顺序的标准结果。
第六章《无监督机器学习算法》介绍了无监督机器学习算法。这些算法被分类为无监督,因为模型或算法试图从给定数据中学习内在结构、模式和关系,而无需任何监督。首先讨论了聚类方法。这些是机器学习方法,试图在数据样本中找到相似性和关系的模式,然后将这些样本聚类成各种群组,使得每个数据样本的群组或簇都具有一定的相似性,基于内在属性或特征。接下来讨论了降维算法,当我们最终拥有大量特征时使用。接着介绍了一些处理异常检测的算法。最后,本章介绍了关联规则挖掘,这是一种数据挖掘方法,用于检查和分析大型交易数据集,以识别感兴趣的模式和规则。这些模式代表跨交易的各种项目之间的有趣关系和关联。
第七章《传统监督学习算法》描述了传统的监督机器学习算法,涉及一组机器学习问题,其中存在带有输入属性和相应输出标签或类别的标记数据集。然后利用这些输入和相应的输出来学习一个泛化系统,可以用来预测以前未见过的数据点的结果。首先,在机器学习的背景下介绍了分类的概念。然后介绍了最简单的机器学习算法之一,线性回归。接着介绍了最重要的算法之一,决策树。讨论了决策树算法的局限性和优势,然后介绍了两个重要的算法,SVM 和 XGBoost。
第八章《神经网络算法》首先介绍了典型神经网络的主要概念和组件,这种网络正成为最重要的机器学习技术。然后,它介绍了各种类型的神经网络,并解释了用于实现这些神经网络的各种激活函数。接着详细讨论了反向传播算法,这是最广泛使用的收敛神经网络问题的算法。接下来解释了迁移学习技术,可以大大简化和部分自动化模型的训练。最后,介绍了如何使用深度学习来检测多媒体数据中的对象作为实际例子。
第九章《自然语言处理算法》介绍了自然语言处理(NLP)的算法。本章以渐进的方式从理论到实践。首先介绍了基本原理,然后是基础数学知识。然后讨论了设计和实施几个重要的文本数据用例的最广泛使用的神经网络之一。还讨论了 NLP 的局限性。最后,介绍了一个案例研究,其中训练模型以根据写作风格检测论文的作者。
第十章《推荐引擎》专注于推荐引擎,这是一种对用户偏好相关信息进行建模,并利用这些信息提供有根据的推荐的方法。推荐引擎的基础始终是用户和产品之间记录的互动。本章首先介绍了推荐引擎背后的基本思想。然后讨论了各种类型的推荐引擎。最后,讨论了推荐引擎如何用于向不同用户推荐物品和产品。
第十一章《数据算法》关注与数据中心算法相关的问题。该章从简要概述与数据相关的问题开始。然后介绍了对数据进行分类的标准。接下来提供了如何将算法应用于流数据应用程序的描述,然后介绍了密码学的主题。最后,介绍了从 Twitter 数据中提取模式的实际例子。
第十二章《密码学》介绍了与密码学相关的算法。该章从背景开始。然后讨论了对称加密算法。解释了 MD5 和 SHA 哈希算法,并介绍了实施对称算法的局限性和弱点。接下来讨论了非对称加密算法以及它们如何用于创建数字证书。最后,讨论了一个总结所有这些技术的实际例子。
第十三章《大规模算法》解释了如何处理无法适应单个节点内存并涉及需要多个 CPU 进行处理的数据的大规模算法。本章首先讨论了最适合并行运行的算法类型。然后讨论了与并行化算法相关的问题。还介绍了 CUDA 架构,并讨论了如何使用单个 GPU 或一组 GPU 来加速算法以及需要对算法进行哪些更改才能有效利用 GPU 的性能。最后,本章讨论了集群计算,并讨论了 Apache Spark 如何创建弹性分布式数据集(RDD)以创建标准算法的极快并行实现。
第十四章,实际考虑,从解释性的重要主题开始,这在现在已经解释了自动决策背后的逻辑变得越来越重要。然后,本章介绍了使用算法的伦理和在实施它们时可能产生偏见的可能性。接下来,详细讨论了处理 NP-hard 问题的技术。最后,总结了实施算法的方法以及与此相关的现实挑战。
充分利用本书
| 章节编号 | 所需软件(带版本) | 免费/专有 | 硬件规格 | 所需操作系统 |
|---|---|---|---|---|
| 1-14 | Python 版本 3.7.2 或更高 | 免费 | 最低 4GB RAM,推荐 8GB+ | Windows/Linux/Mac |
如果您使用本书的数字版本,我们建议您自己输入代码或通过 GitHub 存储库(链接在下一节中提供)访问代码。这样做将有助于避免与复制和粘贴代码相关的任何潜在错误。
下载示例代码文件
您可以从您的账户在www.packt.com下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,文件将直接发送到您的邮箱。
您可以按照以下步骤下载代码文件:
-
在www.packt.com登录或注册。
-
选择“支持”选项卡。
-
点击“代码下载”。
-
在搜索框中输入书名,然后按照屏幕上的说明进行操作。
文件下载后,请确保使用最新版本进行解压缩或提取文件夹:
-
Windows 的 WinRAR/7-Zip
-
Mac 的 Zipeg/iZip/UnRarX
-
Linux 的 7-Zip/PeaZip
该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/40-Algorithms-Every-Programmer-Should-Know。如果代码有更新,将在现有的 GitHub 存储库上进行更新。
我们还有来自我们丰富书籍和视频目录的其他代码包,可在github.com/PacktPublishing/上找到。去看看吧!
下载彩色图片
我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图片。您可以在这里下载:static.packt-cdn.com/downloads/9781789801217_ColorImages.pdf。
使用的约定
本书中使用了许多文本约定。
CodeInText:表示文本中的代码单词,数据库表名,文件夹名,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄。这里有一个例子:“让我们看看如何通过使用push来向堆栈添加一个新元素,或者通过使用pop来从堆栈中移除一个元素。”
代码块设置如下:
define swap(x, y)
buffer = x
x = y
y = buffer
当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体显示:
define swap(x, y)
buffer = x
x = y
y = buffer
任何命令行输入或输出都以以下方式编写:
pip install a_package
粗体:表示一个新术语,一个重要单词,或者屏幕上看到的单词。例如,菜单或对话框中的单词会以这种方式出现在文本中。这里有一个例子:“简化算法的一种方法是在准确性上做出妥协,从而产生一种称为*似算法的算法。”
警告或重要提示会以这种方式出现。提示和技巧会以这种方式出现。
第一部分:基本原理和核心算法
本节介绍了算法的核心方面。我们将探讨算法是什么以及如何设计它,还将了解算法中使用的数据结构。本节还深入介绍了排序和搜索算法,以及解决图形问题的算法。本节包括以下章节:
-
第一章,算法概述
-
第二章,算法中使用的数据结构
-
第三章,排序和搜索算法
-
第四章,设计算法
-
第五章,图算法
第一章:算法概述
本书涵盖了理解、分类、选择和实施重要算法所需的信息。除了解释它们的逻辑之外,本书还讨论了适用于不同类算法的数据结构、开发环境和生产环境。我们专注于越来越重要的现代机器学习算法。除了逻辑之外,还提供了算法实际解决日常问题的实际示例。
本章介绍了算法基础知识。它从需要理解不同算法工作原理的基本概念开始。本节总结了人们如何开始使用算法来数学表达某一类问题,并提到了不同算法的局限性。接下来的部分解释了指定算法逻辑的各种方法。由于本书使用 Python 编写算法,因此解释了如何设置环境来运行示例。然后,讨论了算法性能可以如何量化并与其他算法进行比较的各种方法。最后,本章讨论了验证算法特定实现的各种方法。
总之,本章涵盖了以下主要内容:
-
什么是算法?
-
指定算法的逻辑
-
介绍 Python 包
-
算法设计技术
-
性能分析
-
验证算法
什么是算法?
简单来说,算法是一组用于解决问题的计算规则。它旨在根据精确定义的指令为任何有效输入产生结果。如果您在英语词典(如美国传统)中查找算法一词,它定义了以下概念:
“算法是一组明确的指令,给定一些初始条件,可以按照规定的顺序执行,以达到某个目标,并具有可识别的一组结束条件。”
设计算法是创建数学配方的努力,以最有效的方式解决现实世界的问题。这个配方可以作为开发更可重用和通用的数学解决方案的基础,可以应用于更广泛的类似问题集。
算法的阶段
以下图表说明了开发、部署和最终使用算法的不同阶段:

正如我们所看到的,这个过程始于理解问题陈述中的需求,详细说明了需要做什么。一旦问题清晰陈述,就会引导我们进入开发阶段。
开发阶段包括两个阶段:
-
设计阶段:在设计阶段,算法的架构、逻辑和实现细节被构想和记录下来。在设计算法时,我们同时考虑准确性和性能。在寻找给定问题的解决方案时,在许多情况下,我们会得到多个备选算法。算法的设计阶段是一个迭代过程,涉及比较不同的候选算法。一些算法可能提供简单快速的解决方案,但可能会牺牲准确性。其他算法可能非常准确,但由于复杂性需要花费相当长的时间来运行。其中一些复杂算法可能比其他算法更有效。在做出选择之前,应仔细研究候选算法的所有固有权衡。特别是对于复杂问题,设计高效的算法非常重要。正确设计的算法将导致有效的解决方案,能够同时提供令人满意的性能和合理的准确性。
-
编码阶段:在编码阶段,设计的算法被转换为计算机程序。实际程序实现设计阶段建议的所有逻辑和架构是很重要的。
算法的设计和编码阶段是迭代的。设计满足功能和非功能需求的设计可能需要大量的时间和精力。功能需求是指定给定输入数据的正确输出是什么的要求。算法的非功能需求主要是关于给定数据大小的性能。算法的验证和性能分析将在本章后面讨论。验证算法是验证算法是否满足其功能需求。算法的性能分析是验证它是否满足其主要的非功能需求:性能。
一旦设计并在您选择的编程语言中实现,算法的代码就可以部署了。部署算法涉及设计实际的生产环境,代码将在其中运行。生产环境需要根据算法的数据和处理需求进行设计。例如,对于可并行化的算法,需要具有适当数量的计算节点的集群,以便有效地执行算法。对于数据密集型算法,可能需要设计数据进入管道和缓存和存储数据的策略。生产环境的设计将在第十三章“大规模算法”和第十四章“实际考虑”中更详细地讨论。一旦设计并实施了生产环境,算法就可以部署,它将接受输入数据,处理它,并根据要求生成输出。
指定算法的逻辑
在设计算法时,重要的是找到不同的方式来指定其细节。需要能够捕捉其逻辑和架构。通常,就像建造房屋一样,在实际实施算法之前,指定算法的结构是很重要的。对于更复杂的分布式算法,预先规划它们的逻辑在运行时如何分布在集群中对于迭代式高效设计过程是重要的。通过伪代码和执行计划,这两个需求都得到满足,并将在下一节中讨论。
理解伪代码
指定算法逻辑的最简单方法是以半结构化方式编写算法的高级描述,称为伪代码。在用伪代码编写逻辑之前,首先描述其主要流程并用简单的英语写出主要步骤是有帮助的。然后,将这个英语描述转换成伪代码,这是一种以结构化方式编写这个英语描述的方法,它紧密地代表了算法的逻辑和流程。良好编写的算法伪代码应该以合理的细节描述算法的高级步骤,即使详细的代码与主要流程和结构无关。下图显示了步骤的流程:
伪代码的一个实际例子
请注意,一旦编写了伪代码(如我们将在下一节中看到的),我们就可以使用我们选择的编程语言编写算法代码了。
图 1.3 显示了一个名为SRPMP的资源分配算法的伪代码。在集群计算中,有许多情况需要在一组可用资源上运行并行任务,统称为资源池。这个算法将任务分配给一个资源,并创建一个称为Ω的映射集。请注意,所呈现的伪代码捕捉了算法的逻辑和流程,这在下一节中进一步解释:
1: BEGIN Mapping_Phase
2: Ω = { }
3: k = 1
4: FOREACH Ti∈T
5: ωi = RA(Δk,Ti)
6: add {ωi,Ti} to Ω
7: state_changeTi [STATE 0: Idle/Unmapped] → [STATE 1: Idle/Mapped]
8: k=k+1
9: IF (k>q)
10: k=1
11: ENDIF
12: END FOREACH
13: END Mapping_Phase
让我们逐行解析这个算法:
-
我们通过执行算法开始映射。
Ω映射集是空的。 -
第一个分区被选为
T[1]任务的资源池(参见前面代码的第 3 行)。电视收视率(TRPS)迭代地调用类风湿性关节炎(RA)算法,对于每个T[i]任务,选择一个分区作为资源池。 -
RA 算法返回为
T[i]任务选择的资源集,由ω[i]表示(参见前面代码的第 5 行)。 -
T[i]和ω[i]被添加到映射集中(参见前面代码的第 6 行)。 -
T[i]的状态从STATE 0:Idle/Mapping更改为STATE 1:Idle/Mapped(参见前面代码的第 7 行)。 -
请注意,对于第一次迭代,
k=1,并且选择了第一个分区。对于每个后续迭代,k的值增加,直到k>q。 -
如果
k变得大于q,它会再次重置为1(参见前面代码的第 9 和第 10 行)。 -
这个过程重复进行,直到确定并存储了所有任务与它们将使用的资源集之间的映射,并存储在一个称为
Ω的映射集中。 -
一旦每个任务在映射阶段映射到一组资源中,它就会被执行。
使用片段
随着 Python 等简单但功能强大的编程语言的流行,另一种替代方法变得流行,即直接用编程语言表示算法的逻辑,以一种简化的方式。与伪代码一样,这个选定的代码捕捉了所提出的算法的重要逻辑和结构,避免了详细的代码。这个选定的代码有时被称为片段。在本书中,尽可能使用片段代替伪代码,因为它们节省了一个额外的步骤。例如,让我们看一个简单的片段,它是一个关于可以用来交换两个变量的 Python 函数的片段:
define swap(x, y)
buffer = x
x = y
y = buffer
请注意,片段并不能总是替代伪代码。在伪代码中,有时我们将许多行代码抽象为一行伪代码,表达算法的逻辑,而不会被不必要的编码细节分散注意力。
创建执行计划
伪代码和片段并不总是足以指定与更复杂的分布式算法相关的所有逻辑。例如,分布式算法通常需要在运行时分成不同的编码阶段,这些阶段具有优先顺序。将较大的问题划分为最佳数量的阶段,并具有正确的优先约束条件的正确策略对于有效执行算法至关重要。
我们需要找到一种方法来表示这种策略,以完全表示算法的逻辑和结构。执行计划是详细说明算法将如何被细分为一堆任务的一种方式。一个任务可以是映射器或减速器,可以被分组在称为阶段的块中。下图显示了在执行算法之前由 Apache Spark 运行时生成的执行计划。它详细说明了作业为执行我们的算法创建的运行时任务将被分成:

请注意,前面的图表有五个任务,它们被分成了两个不同的阶段:阶段 11 和阶段 12。
引入 Python 包
一旦设计好算法,就需要根据设计在编程语言中实现。对于本书,我选择了编程语言 Python。我选择它是因为 Python 是一种灵活的开源编程语言。Python 也是越来越重要的云计算基础设施的首选语言,如亚马逊网络服务(AWS)、微软 Azure 和谷歌云*台(GCP)。
官方 Python 主页可在www.python.org/找到,其中还有安装说明和有用的初学者指南。
如果您以前没有使用过 Python,浏览这本初学者指南是一个好主意。对 Python 的基本理解将有助于您更好地理解本书中提出的概念。
对于本书,我希望您使用最新版本的 Python 3。在撰写本文时,最新版本是 3.7.3,这是我们将用来运行本书中的练习的版本。
Python 软件包
Python 是一种通用语言。它设计成具有最基本的功能。根据您打算使用 Python 的用例,需要安装额外的软件包。安装额外软件包的最简单方法是通过 pip 安装程序。这个pip命令可以用来安装额外的软件包:
pip install a_package
已安装的软件包需要定期更新以获得最新功能。这可以通过使用upgrade标志来实现:
pip install a_package --upgrade
用于科学计算的另一个 Python 发行版是 Anaconda,可以从continuum.io/downloads下载。
除了使用pip命令安装新软件包外,对于 Anaconda 发行版,我们还可以使用以下命令来安装新软件包:
conda install a_package
要更新现有的软件包,Anaconda 发行版提供了以下命令选项:
conda update a_package
有各种各样的 Python 软件包可用。一些与算法相关的重要软件包在以下部分中进行了描述。
SciPy 生态系统
科学 Python(SciPy)——发音为sigh pie——是为科学界创建的一组 Python 软件包。它包含许多函数,包括各种随机数生成器、线性代数例程和优化器。SciPy 是一个综合性的软件包,随着时间的推移,人们开发了许多扩展来根据自己的需求定制和扩展软件包。
以下是该生态系统的主要软件包:
-
NumPy:对于算法来说,创建多维数据结构(如数组和矩阵)的能力非常重要。NumPy 提供了一组重要的用于统计和数据分析的数组和矩阵数据类型。有关 NumPy 的详细信息可以在
www.numpy.org/找到。 -
scikit-learn:这个机器学习扩展是 SciPy 最受欢迎的扩展之一。Scikit-learn 提供了一系列重要的机器学习算法,包括分类、回归、聚类和模型验证。您可以在
scikit-learn.org/找到有关 scikit-learn 的更多详细信息。 -
pandas:pandas 是一个开源软件库。它包含了广泛用于输入、输出和处理表格数据的表格复杂数据结构。pandas 库包含许多有用的函数,还提供了高度优化的性能。有关 pandas 的更多详细信息可以在
pandas.pydata.org/找到。 -
Matplotlib:Matplotlib 提供了创建强大可视化的工具。数据可以呈现为折线图、散点图、条形图、直方图、饼图等。更多信息可以在
matplotlib.org/找到。 -
Seaborn:Seaborn 可以被认为类似于 R 中流行的 ggplot2 库。它基于 Matplotlib,并提供了一个高级接口来绘制出色的统计图形。更多细节可以在
seaborn.pydata.org/找到。 -
iPython:iPython 是一个增强的交互式控制台,旨在促进编写,测试和调试 Python 代码。
-
运行 Python 程序:交互式编程模式对于学习和实验代码非常有用。 Python 程序可以保存在带有
.py扩展名的文本文件中,并且可以从控制台运行该文件。
通过 Jupyter Notebook 实现 Python
通过 Jupyter Notebook 运行 Python 程序的另一种方式。 Jupyter Notebook 提供了基于浏览器的用户界面来开发代码。 Jupyter Notebook 用于在本书中展示代码示例。 用文本和图形注释和描述代码的能力使其成为呈现和解释算法的完美工具,也是学习的好工具。
要启动笔记本,您需要启动Juypter-notebook进程,然后打开您喜欢的浏览器并导航到http://localhost:8888:

请注意,Jupyter Notebook 由称为单元格的不同块组成。
算法设计技术
算法是对现实世界问题的数学解决方案。在设计算法时,我们在设计和微调算法时牢记以下三个设计关注点:
-
关注 1:这个算法是否产生了我们预期的结果?
-
关注 2:这是获得这些结果的最佳方式吗?
-
关注 3:算法在更大的数据集上的表现如何?
在设计解决方案之前更好地了解问题本身的复杂性是很重要的。例如,如果我们根据需求和复杂性对问题进行表征,这有助于我们设计适当的解决方案。通常,根据问题的特征,算法可以分为以下类型:
-
数据密集型算法:数据密集型算法旨在处理大量数据。预计它们具有相对简单的处理要求。应用于大型文件的压缩算法是数据密集型算法的一个很好的例子。对于这样的算法,数据的大小预计会远大于处理引擎(单个节点或集群)的内存,并且可能需要开发迭代处理设计以根据要求高效处理数据。
-
计算密集型算法:计算密集型算法具有相当大的处理需求,但不涉及大量数据。一个简单的例子是查找非常大的质数的算法。找到将算法分成不同阶段的策略,以便至少有一些阶段是并行化的,是最大化算法性能的关键。
-
数据和计算密集型算法:有些算法处理大量数据,并且具有相当大的计算需求。用于对实时视频流执行情感分析的算法是处理任务中数据和处理要求都很大的很好的例子。这些算法是最资源密集的算法,需要仔细设计算法并智能分配可用资源。
为了更深入地研究问题的复杂性和需求,有助于我们研究其数据并计算更深入的维度,这将在下一节中进行。
数据维度
为了对问题的数据维度进行分类,我们看看其体积,速度和多样性(3V),其定义如下:
-
体积:体积是算法将处理的数据的预期大小。
-
速度:速度是在使用算法时预期的新数据生成速率。它可以为零。
-
多样性:多样性量化了设计的算法预计要处理多少不同类型的数据。
下图更详细地显示了数据的 3Vs。这个图的中心显示了最简单的数据,体积小,多样性和速度低。当我们远离中心时,数据的复杂性增加。它可以在三个维度中的一个或多个维度上增加。例如,在速度维度上,我们有批处理作为最简单的,然后是周期性处理,然后是准实时处理。最后,我们有实时处理,在数据速度的背景下处理起来最复杂。例如,由一组监控摄像头收集的实时视频流将具有高体积、高速度和高多样性,并且可能需要适当的设计来有效地存储和处理数据。另一方面,一个在 Excel 中创建的简单.csv文件将具有低体积、低速度和低多样性:

例如,如果输入数据是一个简单的csv文件,那么数据的体积、速度和多样性将很低。另一方面,如果输入数据是安全视频摄像头的实时视频流,那么数据的体积、速度和多样性将会很高,这个问题在设计算法时应该牢记在心。
计算维度
计算维度是关于问题处理和计算需求的。算法的处理需求将决定最有效的设计是什么样的。例如,深度学习算法通常需要大量的处理能力。这意味着对于深度学习算法,尽可能拥有多节点并行架构是很重要的。
一个实际的例子
假设我们想对一个视频进行情感分析。情感分析是指我们试图标记视频中不同部分的人类情感,如悲伤、快乐、恐惧、喜悦、挫折和狂喜。这是一个计算密集型的工作,需要大量的计算能力。正如你将在下图中看到的,为了设计计算维度,我们将处理分为五个任务,包括两个阶段。所有的数据转换和准备都是在三个 mapper 中实现的。为此,我们将视频分成三个不同的分区,称为拆分。在 mapper 执行完毕后,处理后的视频输入到两个聚合器,称为reducer。为了进行所需的情感分析,reducer 根据情感对视频进行分组。最后,结果在输出中合并。
请注意,mapper 的数量直接影响算法的运行并行性。最佳的 mapper 和 reducer 数量取决于数据的特性、需要使用的算法类型以及可用资源的数量。
性能分析
分析算法的性能是设计的重要部分。估计算法性能的一种方法是分析其复杂性。
复杂性理论是研究复杂算法的学科。为了有用,任何算法都应该具有三个关键特征:
-
应该是正确的。如果算法不能给出正确的答案,那么它对你来说没有太大的好处。
-
一个好的算法应该是可以理解的。即使是世界上最好的算法,如果对你来说太复杂而无法在计算机上实现,那也没有什么好处。
-
一个好的算法应该是高效的。即使一个算法产生了正确的结果,如果它需要花费一千年或者需要十亿太字节的内存,也不会对你有太大帮助。
算法复杂度的两种可能的分析类型:
-
空间复杂度分析:估计执行算法所需的运行时内存需求。
-
时间复杂度分析:估计算法运行所需的时间。
空间复杂度分析
空间复杂度分析估计算法处理输入数据所需的内存量。在处理输入数据时,算法需要将瞬态临时数据结构存储在内存中。算法的设计方式会影响这些数据结构的数量、类型和大小。在分布式计算时代,需要处理越来越多的数据,空间复杂度分析变得越来越重要。这些数据结构的大小、类型和数量将决定底层硬件的内存需求。在分布式计算中使用的现代内存数据结构,如弹性分布式数据集(RDD),需要具有高效的资源分配机制,以便在算法的不同执行阶段了解内存需求。
空间复杂度分析是算法高效设计的必要条件。如果在设计特定算法时没有进行适当的空间复杂度分析,那么对于瞬态临时数据结构的内存可用性不足可能会触发不必要的磁盘溢出,这可能会显著影响算法的性能和效率。
在本章中,我们将更深入地研究时间复杂度。空间复杂度将在第十三章《大规模算法》中更详细地讨论,那里我们将处理具有复杂运行时内存需求的大规模分布式算法。
时间复杂度分析
时间复杂度分析估计算法基于其结构完成其分配工作所需的时间。与空间复杂度相比,时间复杂度不依赖于算法将在其上运行的任何硬件。时间复杂度分析仅仅取决于算法本身的结构。时间复杂度分析的总体目标是尝试回答这些重要问题——这个算法是否可扩展?这个算法将如何处理更大的数据集?
为了回答这些问题,我们需要确定算法在数据规模增大时对性能的影响,并确保算法不仅准确而且能够良好扩展。在当今“大数据”世界中,算法的性能对于更大的数据集变得越来越重要。
在许多情况下,我们可能有多种方法来设计算法。在这种情况下进行时间复杂度分析的目标将是:
“对于一个特定的问题和多个算法,哪一个在时间效率上最有效?”
计算算法时间复杂度的基本方法有两种:
-
后实现的分析方法:在这种方法中,实现不同的候选算法并比较它们的性能。
-
预实现的理论方法:在运行算法之前,通过数学*似来估计每个算法的性能。
理论方法的优势在于它仅仅取决于算法本身的结构。它不依赖于实际用于运行算法的硬件、运行时选择的软件栈的选择,或者用于实现算法的编程语言。
性能估计
典型算法的性能将取决于输入的数据类型。例如,如果数据已根据我们试图解决的问题的上下文进行了排序,算法可能会执行得非常快。如果排序后的输入用于基准测试这个特定的算法,那么它将给出一个不真实的良好性能数字,这不会真实反映其在大多数情况下的真实性能。为了处理算法对输入数据的依赖性,我们在进行性能分析时需要考虑不同类型的情况。
最佳情况
在最佳情况下,输入的数据组织方式使得算法能够发挥最佳性能。最佳情况分析给出了算法性能的上限。
最坏情况
估计算法性能的第二种方法是尝试找到在给定一组条件下完成工作所需的最长时间。算法的最坏情况分析非常有用,因为我们保证无论条件如何,算法的性能始终优于我们分析出来的数字。最坏情况分析在处理具有更大数据集的复杂问题时特别有用。最坏情况分析给出了算法性能的下限。
*均情况
这从将各种可能的输入分成各种组开始。然后,从每个组的一个代表性输入进行性能分析。最后,计算每个组的性能的*均值。
*均情况分析并不总是准确的,因为它需要考虑算法的所有不同组合和可能性,这并不总是容易做到。
选择算法
你怎么知道哪一个是更好的解决方案?你怎么知道哪个算法运行得更快?时间复杂度和大 O 符号(本章后面讨论)是回答这些问题的非常好的工具。
要看它在哪里有用,让我们举一个简单的例子,目标是对一组数字进行排序。有几种可用的算法可以完成这项工作。问题是如何选择正确的算法。
首先,可以观察到的一点是,如果列表中的数字不太多,那么我们选择哪种算法来对数字列表进行排序就无关紧要。因此,如果列表中只有 10 个数字(n=10),那么我们选择哪种算法都无关紧要,因为即使是设计非常糟糕的算法,也可能不会花费超过几微秒的时间。但是一旦列表的大小变成 100 万,现在选择正确的算法将会有所不同。一个非常糟糕的算法甚至可能需要几个小时才能运行,而一个设计良好的算法可能在几秒内完成对列表的排序。因此,对于更大的输入数据集,投入时间和精力进行性能分析,并选择正确设计的算法来高效地完成所需的工作是非常有意义的。
大 O 符号
大 O 符号用于量化各种算法的性能,随着输入规模的增长。大 O 符号是进行最坏情况分析的最流行方法之一。本节讨论了不同类型的大 O 符号。
常数时间(O(1))复杂度
如果一个算法在运行时所需的时间与输入数据的大小无关,那么它被称为以常数时间运行。它用 O(1)表示。让我们以访问数组的第 n 个元素为例。无论数组的大小如何,获取结果都需要恒定的时间。例如,以下函数将返回数组的第一个元素,并具有 O(1)的复杂度:
def getFirst(myList):
return myList[0]
输出如下:

-
通过使用
push添加新元素到栈或使用pop从栈中移除元素。无论栈的大小如何,添加或移除元素都需要相同的时间。 -
访问哈希表的元素(如第二章中讨论的,算法中使用的数据结构)。
-
桶排序(如第二章中讨论的,算法中使用的数据结构)。
线性时间(O(n))复杂度
如果执行时间与输入大小成正比,则称算法具有线性时间复杂度,表示为 O(n)。一个简单的例子是在单维数据结构中添加元素:
def getSum(myList):
sum = 0
for item in myList:
sum = sum + item
return sum
请注意算法的主循环。主循环中的迭代次数随着n的增加而线性增加,产生了下图中的 O(n)复杂度:

数组操作的其他一些例子如下:
-
搜索一个元素
-
在数组的所有元素中找到最小值
二次时间(O(n²))复杂度
如果算法的执行时间与输入大小的*方成正比,则称算法具有二次时间复杂度;例如,一个简单的函数对二维数组求和如下:
def getSum(myList):
sum = 0
for row in myList:
for item in row:
sum += item
return sum
请注意主循环内嵌在另一个主循环中。这个嵌套循环使得前面的代码具有 O(n²)的复杂度:

另一个例子是冒泡排序算法(如第二章中讨论的,算法中使用的数据结构)。
对数时间(O(logn))复杂度
如果算法的执行时间与输入大小的对数成正比,则称算法具有对数时间复杂度。每次迭代,输入大小都会以一个常数倍数因子减少。对数的一个例子是二分搜索。二分搜索算法用于在一维数据结构中查找特定元素,例如 Python 列表。数据结构中的元素需要按降序排序。二分搜索算法在名为searchBinary的函数中实现,如下所示:
def searchBinary(myList,item):
first = 0
last = len(myList)-1
foundFlag = False
while( first<=last and not foundFlag):
mid = (first + last)//2
if myList[mid] == item :
foundFlag = True
else:
if item < myList[mid]:
last = mid - 1
else:
first = mid + 1
return foundFlag
主循环利用列表有序的事实。它每次迭代将列表分成一半,直到得到结果:

在定义函数之后,测试了在第 11 和 12 行搜索特定元素。二分搜索算法在第三章中进一步讨论,排序和搜索算法。
请注意,在所提出的四种大 O 符号类型中,O(n²)的性能最差,O(logn)的性能最佳。事实上,O(logn)的性能可以被视为任何算法性能的黄金标准(尽管并非总是实现)。另一方面,O(n²)并不像 O(n³)那么糟糕,但是仍然,属于这一类的算法不能用于大数据,因为时间复杂度对它们能够实际处理的数据量施加了限制。
减少算法复杂度的一种方法是在准确性上做出妥协,产生一种称为*似算法的算法类型。
算法性能评估的整个过程是迭代的,如下图所示:

验证算法
验证算法确认它实际上为我们尝试解决的问题提供了数学解决方案。验证过程应该检查尽可能多的可能值和输入值类型的结果。
精确、*似和随机算法
验证算法还取决于算法的类型,因为测试技术是不同的。让我们首先区分确定性和随机算法。
对于确定性算法,特定输入总是生成完全相同的输出。但对于某些类别的算法,随机数序列也被视为输入,这使得每次运行算法时输出都不同。详见第六章中详细介绍的 k 均值聚类算法,无监督机器学习算法,就是这种算法的一个例子:

根据用于简化逻辑以使其运行更快的假设或*似,算法也可以分为以下两种类型:
-
一种精确算法:精确算法预计能够在不引入任何假设或*似的情况下产生精确解决方案。
-
一种*似算法:当问题复杂度对于给定资源来说太大而难以处理时,我们通过做一些假设来简化问题。基于这些简化或假设的算法称为*似算法,它并不能给出精确解决方案。
让我们看一个例子来理解精确和*似算法之间的区别——著名的旅行推销员问题,它是在 1930 年提出的。一个旅行推销员向你挑战,要求你找到一名特定推销员访问每个城市(从城市列表中)并返回原点的最短路线。首次尝试提供解决方案将包括生成所有城市的排列并选择最便宜的城市组合。这种方法提供解决方案的复杂度是 O(n!),其中n是城市的数量。显然,随着城市数量的增加,时间复杂度开始变得难以管理。
如果城市数量超过 30 个,减少复杂性的一种方法是引入一些*似和假设。
对于*似算法,在收集要求时设定准确性期望是很重要的。验证*似算法是为了验证结果的误差是否在可接受范围内。
可解释性
当算法用于关键情况时,有必要能够在需要时解释每个结果背后的原因。这是为了确保基于算法结果的决策不会引入偏见。
能够准确识别直接或间接用于做出特定决策的特征的能力被称为算法的“可解释性”。当算法用于关键用例时,需要对偏见和成见进行评估。算法的伦理分析已成为对可能影响与人们生活相关的决策的算法进行验证的标准部分。
对于处理深度学习的算法,解释性很难实现。例如,如果算法用于拒绝某人的抵押贷款申请,具有透明度和解释原因的能力就很重要。
算法的可解释性是一个活跃的研究领域。最*开发的一种有效技术是局部可解释模型无关解释(LIME),该技术是在 2016 年的第 22 届计算机协会(ACM)知识发现和数据挖掘专业兴趣小组(SIGKDD)国际会议上提出的。LIME 基于一个概念,即对每个实例的输入进行小的改变,然后努力绘制该实例的局部决策边界。它可以量化每个变量对该实例的影响。
摘要
这一章是关于学习算法的基础知识。首先,我们学习了开发算法的不同阶段。我们讨论了指定算法逻辑的不同方式,这对于设计算法是必要的。然后,我们看了如何设计算法。我们学会了分析算法性能的两种不同方式。最后,我们研究了验证算法的不同方面。
经过这一章的学习,我们应该能够理解算法的伪代码。我们应该了解开发和部署算法的不同阶段。我们还学会了如何使用大 O 符号来评估算法的性能。
下一章是关于算法中使用的数据结构。我们将首先看一下 Python 中可用的数据结构。然后我们将看看如何使用这些数据结构来创建更复杂的数据结构,比如栈、队列和树,这些都是开发复杂算法所需的。
第二章:算法中使用的数据结构
算法需要必要的内存数据结构来在执行时保存临时数据。选择合适的数据结构对于它们的高效实现至关重要。某些类别的算法是递归或迭代的逻辑,并且需要专门为它们设计的数据结构。例如,如果使用嵌套数据结构,递归算法可能更容易实现,并表现出更好的性能。在本章中,数据结构是在算法的上下文中讨论的。由于本书中使用 Python,本章重点介绍 Python 数据结构,但本章中提出的概念也可以用于其他语言,如 Java 和 C++。
在本章结束时,您应该能够理解 Python 如何处理复杂的数据结构,以及应该为某种类型的数据使用哪种数据结构。
因此,以下是本章讨论的主要要点:
-
在 Python 中探索数据结构
-
探索抽象数据类型
-
栈和队列
-
树
在 Python 中探索数据结构
在任何语言中,数据结构都用于存储和操作复杂数据。在 Python 中,数据结构是存储容器,用于以高效的方式管理、组织和搜索数据。它们用于存储一组称为集合的数据元素,这些元素需要一起存储和处理。在 Python 中,有五种不同的数据结构可以用来存储集合:
-
列表:有序的可变元素序列
-
元组:有序的不可变元素序列
-
集合:无序的元素集合
-
字典:无序的键-值对集合
-
数据框:用于存储二维数据的二维结构
让我们在接下来的小节中更详细地了解它们。
列表
在 Python 中,列表是用于存储可变序列元素的主要数据结构。存储在列表中的数据元素的序列不一定是相同类型的。
要创建一个列表,数据元素需要用[ ]括起来,并用逗号分隔。例如,以下代码创建了四个不同类型的数据元素:
>>> aList = ["John", 33,"Toronto", True]
>>> print(aList)
*['John', 33, 'Toronto', True]Ex*
在 Python 中,列表是创建一维可写数据结构的方便方式,特别是在算法的不同内部阶段需要时。
使用列表
数据结构中的实用函数使它们非常有用,因为它们可以用来管理列表中的数据。
让我们看看如何使用它们:
- 列表索引:由于列表中元素的位置是确定的,索引可以用于获取特定位置的元素。以下代码演示了这个概念:
>>> bin_colors=['Red','Green','Blue','Yellow']
>>> bin_colors[1]
*'Green'*
此代码创建的四个元素列表如下截图所示:

请注意,索引从 0 开始,因此第二个元素Green通过索引1检索,即bin_color[1]。
- 列表切片:通过指定索引范围来检索列表的子集称为切片。以下代码可用于创建列表的切片:
>>> bin_colors=['Red','Green','Blue','Yellow']
>>> bin_colors[0:2] *['Red', 'Green']*
请注意,列表是 Python 中最受欢迎的单维数据结构之一。
在切片列表时,范围表示为:第一个数字(包括)和第二个数字(不包括)。例如,bin_colors[0:2]将包括bin_color[0]和bin_color[1],但不包括bin_color[2]。在使用列表时,应该记住这一点,因为 Python 语言的一些用户抱怨这不是很直观。
让我们来看下面的代码片段:
>>> bin_colors=['Red','Green','Blue','Yellow'] >>> bin_colors[2:]
*['Blue', 'Yellow']*
>>> bin_colors[:2]
*['Red', 'Green']*
如果未指定起始索引,则表示列表的开头,如果未指定结束索引,则表示列表的结尾。前面的代码实际上演示了这个概念。
- 负索引:在 Python 中,我们还有负索引,它们从列表的末尾开始计数。这在以下代码中得到了证明:
>>> bin_colors=['Red','Green','Blue','Yellow'] >>> bin_colors[:-1]
*['Red', 'Green', 'Blue']*
>>> bin_colors[:-2]
*['Red', 'Green']*
>>> bin_colors[-2:-1]
*['Blue']*
请注意,当我们想要使用最后一个元素作为参考点而不是第一个元素时,负索引特别有用。
- 嵌套:列表的一个元素可以是简单数据类型或复杂数据类型。这允许在列表中进行嵌套。对于迭代和递归算法,这提供了重要的功能。
让我们来看下面的代码,这是一个列表中嵌套列表的例子(嵌套):
>>> a = [1,2,[100,200,300],6]
>>> max(a[2])
*300*
>>> a[2][1]
*200*
- 迭代:Python 允许使用
for循环来迭代列表中的每个元素。这在下面的例子中进行了演示:
>>> bin_colors=['Red','Green','Blue','Yellow']
>>> for aColor in bin_colors:
print(aColor + " Square") Red Square
*Green Square
Blue Square
Yellow Square*
请注意,前面的代码会遍历列表并打印每个元素。
Lambda 函数
有一堆可以用在列表上的 lambda 函数。它们在算法的上下文中特别重要,并且提供了即时创建函数的能力。有时,在文献中,它们也被称为匿名函数。本节演示了它们的用法:
- 数据过滤:要过滤数据,首先我们定义一个谓词,它是一个输入单个参数并返回布尔值的函数。下面的代码演示了它的用法:
>>> list(filter(lambda x: x > 100, [-5, 200, 300, -10, 10, 1000]))
*[200, 300, 1000]*
请注意,在这段代码中,我们使用lambda函数来过滤列表,它指定了过滤的条件。过滤函数被设计用来根据定义的条件从序列中过滤元素。Python 中的过滤函数通常与lambda一起使用。除了列表,它还可以用来从元组或集合中过滤元素。对于前面的代码,定义的条件是x > 100。代码将遍历列表的所有元素,并过滤掉不符合这个条件的元素。
- 数据转换:可以使用
map()函数来使用 lambda 函数进行数据转换。一个例子如下:
>>> list(map(lambda x: x ** 2, [11, 22, 33, 44,55]))
*[121, 484, 1089, 1936, 3025]*
使用map函数与lambda函数提供了非常强大的功能。当与map函数一起使用时,lambda函数可以用来指定一个转换器,它转换给定序列的每个元素。在前面的代码中,转换器是乘以二。因此,我们使用map函数来将列表中的每个元素乘以二。
- 数据聚合:对于数据聚合,可以使用
reduce()函数,它会递归地对列表的每个元素运行一对值的函数:
from functools import reduce
def doSum(x1,x2):
return x1+x2
x = reduce(doSum, [100, 122, 33, 4, 5, 6])
请注意,reduce函数需要一个数据聚合函数来进行定义。在前面的代码中,数据聚合函数是functools。它定义了如何聚合给定列表的项目。聚合将从前两个元素开始,并且结果将替换前两个元素。这个缩减的过程会重复,直到达到末尾,得到一个聚合的数字。doSum函数中的x1和x2代表每次迭代中的两个数字,而doSum代表它们的聚合标准。
前面的代码块会得到一个单一的值(为270)。
range 函数
range函数可以用来轻松生成一个大量的数字列表。它用于自动填充列表中的数字序列。
range函数使用简单。我们可以通过指定列表中要包含的元素数量来使用它。默认情况下,它从零开始,每次增加一个:
>>> x = range(6)
>>> x
[0,1,2,3,4,5]
我们还可以指定结束数字和步长:
>>> oddNum = range(3,29,2)
>>> oddNum
*[3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27]*
前面的 range 函数将给我们从3到29的奇数。
列表的时间复杂度
列表各种函数的时间复杂度可以使用大 O 符号总结如下:
| 不同的方法 | 时间复杂度 |
|---|---|
| 插入一个元素 | O(1) |
| 删除一个元素 | O(n)(在最坏的情况下可能需要遍历整个列表) |
| 切片列表 | O(n) |
| 元素检索 | O(n) |
| 复制 | O(n) |
请注意,添加单个元素所需的时间与列表的大小无关。表中提到的其他操作取决于列表的大小。随着列表的大小变大,对性能的影响变得更加显著。
元组
可以用于存储集合的第二种数据结构是元组。与列表相反,元组是不可变(只读)的数据结构。元组由( )括起来的几个元素组成。
与列表一样,元组中的元素可以是不同类型的。它们还允许为其元素使用复杂数据类型。因此,可以在元组中创建一个元组,从而提供了创建嵌套数据结构的方法。在迭代和递归算法中,创建嵌套数据结构的能力尤其有用。
以下代码演示了如何创建元组:
>>> bin_colors=('Red','Green','Blue','Yellow')
>>> bin_colors[1]
*'Green'*
>>> bin_colors[2:]
*('Blue', 'Yellow')*
>>> bin_colors[:-1]
*('Red', 'Green', 'Blue')*
# Nested Tuple Data structure
>>> a = (1,2,(100,200,300),6)
>>> max(a[2])
*300*
>>> a[2][1]
*200*
在可能的情况下,应优先选择不可变数据结构(如元组)而不是可变数据结构(如列表),因为性能更好。特别是在处理大数据时,不可变数据结构比可变数据结构要快得多。例如,更改列表中的数据元素的能力是有代价的,我们应该仔细分析是否真的需要这样做,这样我们可以将代码实现为只读元组,这将更快。
请注意,在上述代码中,a[2]指的是第三个元素,即一个元组(100,200,300)。a[2][1]指的是这个元组中的第二个元素,即200。
元组的时间复杂度
使用大 O 表示法,可以总结元组各种函数的时间复杂度如下:
| 函数 | 时间 复杂度 |
|---|---|
Append |
O(1) |
请注意,Append是一个向已有元组的末尾添加元素的函数。其复杂度为 O(1)。
字典
以键值对的形式保存数据在分布式算法中尤为重要。在 Python 中,这些键值对的集合被存储为一种称为字典的数据结构。要创建字典,应选择一个最适合在整个数据处理过程中标识数据的属性作为键。值可以是任何类型的元素,例如数字或字符串。Python 还总是使用列表等复杂数据类型作为值。可以通过使用字典作为值的数据类型来创建嵌套字典。
要创建一个简单的字典,将键值对放在{ }中。例如,以下代码创建了一个由三个键值对组成的简单字典:
>>> bin_colors ={
"manual_color": "Yellow",
"approved_color": "Green",
"refused_color": "Red"
}
>>> print(bin_colors) *{'manual_color': 'Yellow', 'approved_color': 'Green', 'refused_color': 'Red'}*
前面代码创建的三个键值对也在以下截图中说明:

现在,让我们看看如何检索和更新与键相关联的值:
- 要检索与键关联的值,可以使用
get函数,也可以使用键作为索引:
>>> bin_colors.get('approved_color')
*'Green'* >>> bin_colors['approved_color']
*'Green'*
- 要更新与键关联的值,请使用以下代码:
>>> bin_colors['approved_color']="Purple"
>>> print(bin_colors) *{'manual_color': 'Yellow', 'approved_color': 'Purple', 'refused_color': 'Red'}*
请注意,前面的代码显示了如何更新与字典中特定键相关联的值。
字典的时间复杂度
以下表格给出了使用大 O 表示法的字典的时间复杂度:
| 字典 | 时间 复杂度 |
|---|---|
| 获取值或键 | O(1) |
| 设置值或键 | O(1) |
| 复制字典 | O(n) |
从字典的复杂度分析中重要的一点是,获取或设置键值的时间与字典的大小完全独立。这意味着在大小为三的字典中添加键值对所需的时间与在大小为一百万的字典中添加键值对所需的时间相同。
集合
集合被定义为可以是不同类型的元素的集合。元素被包含在{ }中。例如,看一下以下代码块:
>>> green = {'grass', 'leaves'}
>>> print(green)
{'grass', 'leaves'}
集合的定义特征是它只存储每个元素的不同值。如果我们尝试添加另一个冗余元素,它将忽略该元素,如下所示:
>>> green = {'grass', 'leaves','leaves'}
>>> print(green)
*{'grass', 'leaves'}*
为了演示可以在集合上执行的操作类型,让我们定义两个集合:
-
一个名为 yellow 的集合,其中包含黄色的东西
-
另一个名为 red 的集合,其中包含红色的东西
请注意,这两个集合之间有一些共同之处。这两个集合及其关系可以用以下维恩图表示:

如果我们想要在 Python 中实现这两个集合,代码将如下所示:
>>> yellow = *{'dandelions', 'fire hydrant', 'leaves'}
>>> red =* *{'fire hydrant', 'blood', 'rose', 'leaves'}*
现在,让我们考虑以下代码,演示了使用 Python 进行集合操作:
>>> yellow|red
*{'dandelions', 'fire hydrant', 'blood', 'rose', 'leaves'}*
>>> yellow&red
*{'fire hydrant'}*
如前面的代码片段所示,Python 中的集合可以进行联合和交集等操作。正如我们所知,联合操作将合并两个集合的所有元素,而交集操作将给出两个集合之间的共同元素集合。请注意以下内容:
-
yellow|red用于获取前面两个定义的集合的并集。 -
yellow&red用于获取黄色和红色之间的重叠部分。
集合的时间复杂度分析
以下是集合的时间复杂度分析:
| 集合 | 复杂度 |
|---|---|
| 添加一个元素 | O(1) |
| 删除一个元素 | O(1) |
| 复制 | O(n) |
从集合的复杂度分析中重要的一点是,添加一个元素所需的时间完全独立于特定集合的大小。
数据框
数据框是一种用于存储 Python 的pandas包中可用的表格数据的数据结构。它是算法中最重要的数据结构之一,用于处理传统的结构化数据。让我们考虑以下表格:
| id | name | age | decision |
|---|---|---|---|
| 1 | 费尔斯 | 32 | 真 |
| 2 | 艾琳娜 | 23 | 假 |
| 3 | 史蒂文 | 40 | 真 |
现在,让我们使用数据框来表示这一点。
可以使用以下代码创建一个简单的数据框:
>>> import pandas as pd
>>> df = pd.DataFrame([
... ['1', 'Fares', 32, True],
... ['2', 'Elena', 23, False],
... ['3', 'Steven', 40, True]])
>>> df.columns = ['id', 'name', 'age', 'decision']
>>> df
*id name age decision
0 1 Fares 32 True
1 2 Elena 23 False
2 3 Steven 40 True*
请注意,在上述代码中,df.column是一个指定列名称的列表。
数据框也用于其他流行的语言和框架中来实现表格数据结构。例如 R 和 Apache Spark 框架。
数据框的术语
让我们来看一些在数据框的上下文中使用的术语:
-
轴:在 pandas 文档中,数据框的单个列或行称为一个轴。
-
轴:如果有多个轴,则将它们作为一个组称为轴。
-
标签:数据框允许使用所谓的标签对列和行进行命名。
创建数据框的子集
从根本上讲,有两种主要方法可以创建数据框的子集(假设子集的名称为myDF):
-
列选择
-
行选择
让我们依次看一下。
列选择
在机器学习算法中,选择正确的特征集是一项重要的任务。在我们可能拥有的所有特征中,不一定所有特征在算法的特定阶段都是必需的。在 Python 中,通过列选择来实现特征选择,这在本节中有所解释。
可以按名称检索列,如下所示:
>>> df[['name','age']]
*name age
0 Fares 32
1 Elena 23
2 Steven 40*
数据框中列的位置是确定的。可以按其位置检索列,如下所示:
>>> df.iloc[:,3]
*0 True*
*1 False*
*2 True*
请注意,在此代码中,我们正在检索数据框的前三行。
行选择
数据框中的每一行对应于问题空间中的一个数据点。如果我们想要创建问题空间中的数据元素的子集,我们需要执行行选择。可以使用以下两种方法之一来创建这个子集:
-
通过指定它们的位置
-
通过指定过滤器
可以按照其位置检索数据框的子集行,如下所示:
>>> df.iloc[1:3,:]
*id name age decision*
*1 2 Elena 23 False*
*2 3 Steven 40 True*
请注意,上述代码将返回数据框的前两行和所有列。
通过指定过滤器创建子集,我们需要使用一个或多个列来定义选择条件。例如,可以通过以下方法选择数据元素的子集:
>>> df[df.age>30]
*id name age decision
0 1 Fares 32 True
2 3 Steven 40 True
**>>> df[(df.age<35)&(df.decision==True)]*** id name age decision
0 1 Fares 32 True
请注意,此代码创建了满足过滤器中规定条件的行的子集。
矩阵
矩阵是一个具有固定列数和行数的二维数据结构。矩阵的每个元素可以通过其列和行来引用。
在 Python 中,可以使用numpy数组创建矩阵,如下代码所示:
>>> myMatrix = np.array([[11, 12, 13], [21, 22, 23], [31, 32, 33]])
>>> print(myMatrix)
*[[11 12 13]*
*[21 22 23]*
*[31 32 33]]*
>>> print(type(myMatrix))
*<class 'numpy.ndarray'>*
请注意,上述代码将创建一个具有三行三列的矩阵。
矩阵操作
矩阵数据操作有许多可用的操作。例如,让我们尝试转置上述矩阵。我们将使用transpose()函数,它将列转换为行,行转换为列:
>>> myMatrix.transpose()
*array([[11, 21, 31],*
*[12, 22, 32],*
*[13, 23, 33]])*
请注意,矩阵操作在多媒体数据处理中经常使用。
现在我们已经了解了 Python 中的数据结构,让我们在下一节中转向抽象数据类型。
探索抽象数据类型
抽象,一般来说,是一个用于以其共同核心功能来定义复杂系统的概念。使用这个概念来创建通用数据结构,产生了抽象数据类型(ADT)。通过隐藏实现级别的细节并为用户提供一个通用的、与实现无关的数据结构,ADT 的使用创建了产生更简单和更清晰代码的算法。ADT 可以在任何编程语言中实现,如 C++、Java 和 Scala。在本节中,我们将使用 Python 来实现 ADT。让我们首先从向量开始。
向量
向量是一种用于存储数据的单维结构。它们是 Python 中最受欢迎的数据结构之一。在 Python 中有两种创建向量的方式如下:
- 使用 Python 列表:创建向量的最简单方法是使用 Python 列表,如下所示:
>>> myVector = [22,33,44,55]
>>> print(myVector)
*[22 33 44 55]*
>>> print(type(myVector))
*<class 'list'>*
请注意,此代码将创建一个包含四个元素的列表。
- 使用
numpy数组:创建向量的另一种流行方法是使用 NumPy 数组,如下所示:
>>> myVector = np.array([22,33,44,55])
>>> print(myVector)
*[22 33 44 55]*
>>> print(type(myVector))
*<class 'numpy.ndarray'>*
请注意,我们在此代码中使用np.array创建了myVector。
在 Python 中,我们可以使用下划线表示整数以分隔部分。这样可以使它们更易读,减少出错的可能性。在处理大数字时尤其有用。因此,十亿可以表示为 a=1
堆栈
堆栈是一种线性数据结构,用于存储一维列表。它可以以后进先出(LIFO)或先进后出(FILO)的方式存储项目。堆栈的定义特征是元素的添加和移除方式。新元素被添加到一端,只能从该端移除一个元素。
以下是与堆栈相关的操作:
-
isEmpty: 如果堆栈为空则返回 true
-
push: 添加一个新元素
-
pop: 返回最*添加的元素并将其删除
以下图表显示了如何使用 push 和 pop 操作向堆栈添加和删除数据:

上图的顶部显示了使用 push 操作向堆栈添加项目。在步骤1.1、1.2和1.3中,push 操作被用于三次向堆栈添加三个元素。上图的底部用于从堆栈中检索存储的值。在步骤2.2和2.3中,pop 操作被用于以 LIFO 格式从堆栈中检索两个元素。
让我们在 Python 中创建一个名为Stack的类,我们将在其中定义与堆栈类相关的所有操作。该类的代码如下:
class Stack:
def __init__(self):
self.items = []
def isEmpty(self):
return self.items == []
def push(self, item):
self.items.append(item)
def pop(self):
return self.items.pop()
def peek(self):
return self.items[len(self.items)-1]
def size(self):
return len(self.items)
要向堆栈推送四个元素,可以使用以下代码:

注意,上述代码创建了一个包含四个数据元素的堆栈。
堆栈的时间复杂度
让我们来看看堆栈的时间复杂度(使用大 O 表示法):
| 操作 | 时间复杂度 |
|---|---|
push |
O(1) |
pop |
O(1) |
size |
O(1) |
peek |
O(1) |
需要注意的一点是,前面表中提到的四种操作的性能都不取决于栈的大小。
实际例子
栈在许多用例中用作数据结构。例如,当用户想要在 Web 浏览器中浏览历史记录时,这是一种 LIFO 数据访问模式,可以使用栈来存储历史记录。另一个例子是当用户想要在文字处理软件中执行“撤销”操作时。
队列
和栈一样,队列将n个元素存储在单维结构中。元素以FIFO格式添加和移除。队列的一端称为 后端,另一端称为 前端。当元素从前端移除时,该操作称为 出队。当元素在后端添加时,该操作称为 入队。
在下图中,顶部部分显示了入队操作。步骤 1.1,1.2 和 1.3 将三个元素添加到队列中,结果队列显示在 1.4 中。请注意,黄色 是 后端,红色 是 前端。
下图的底部部分显示了一个出队操作。步骤 2.2,2.3 和 2.4 依次从队列的前端一个接一个地移除元素:

前面图中显示的队列可以通过以下代码实现:
class Queue(object):
def __init__(self):
self.items = []
def isEmpty(self):
return self.items == []
def enqueue(self, item):
self.items.insert(0,item)
def dequeue(self):
return self.items.pop()
def size(self):
return len(self.items)
让我们根据以下截图,按照前面的图示进行入队和出队操作:

请注意,前面的代码首先创建一个队列,然后将四个项目入队。
使用栈和队列的基本思想
让我们用一个类比来了解使用栈和队列的基本思想。假设我们有一张桌子,我们把我们从邮政服务收到的信放在上面,例如,加拿大邮件。我们堆积起来,直到有时间逐一打开和查看信件。有两种可能的做法:
-
我们把信放在一个栈里,每当我们收到一封新信时,我们把它放在栈的顶部。当我们想读一封信时,我们从顶部开始。这就是我们所说的 栈。请注意,最新的信件将位于顶部,并且将首先被处理。从列表顶部取出一封信称为 弹出 操作。每当有新信到达时,把它放在顶部称为 推入 操作。如果我们最终有一个相当大的栈,并且有大量信件不断到达,有可能我们永远没有机会到达等待我们的非常重要的信件。
-
我们把信放在一堆里,但我们想先处理最老的信:每次我们想看一个或多个信时,我们要确保先处理最老的那个。这就是我们所说的 队列。把一封信放到一堆里叫做 入队 操作。从一堆里取出一封信叫做 出队 操作。
树
在算法的上下文中,树是最有用的数据结构之一,因为它具有分层数据存储能力。在设计算法时,我们使用树来表示我们需要存储或处理的数据元素之间的分层关系。
让我们更深入地了解这个有趣且非常重要的数据结构。
每棵树都有一个有限的节点集,因此它有一个称为 根 的起始数据元素和一组由链接连接在一起的称为 分支 的节点。
术语
让我们来看一些与树数据结构相关的术语:
| 根节点 | 没有父节点的节点称为 根 节点。例如,在下图中,根节点是 A。在算法中,通常根节点保存树结构中最重要的值。 |
|---|---|
| 节点的级别 | 从根节点到节点的距离就是节点的级别。例如,在下图中,节点D,E和F的级别为 2。 |
| 兄弟节点 | 树中的两个节点如果在同一级别,则称为兄弟节点。例如,如果查看下图,节点B和C是兄弟节点。 |
| 子节点和父节点 | 如果节点C和节点F直接连接,并且节点C的级别低于节点F,那么节点F是节点C的子节点。反之,节点C是节点F的父节点。下图中的节点C和F展示了这种父子关系。 |
| 节点的度 | 节点的度是它拥有的子节点数量。例如,在下图中,节点B的度为 2。 |
| 树的度 | 树的度等于树的组成节点中可以找到的最大度。例如,下图中呈现的树的度为 2。 |
| 子树 | 树的子树是树的一部分,选择的节点是子树的根节点,所有子节点是树的节点。例如,在下图中呈现的树的节点E的子树包括节点E作为根节点和节点G和H作为两个子节点。 |
| 叶节点 | 树中没有子节点的节点称为叶节点。例如,在下图中,D,G,H和F是四个叶节点。 |
| 内部节点 | 既不是根节点也不是叶节点的任何节点都是内部节点。内部节点至少有一个父节点和至少一个子节点。 |
请注意,树是我们将在第六章中学习的网络或图的一种,无监督机器学习算法。对于图和网络分析,我们使用术语链接或边而不是分支。大多数其他术语保持不变。
树的类型
树有不同的类型,如下所述:
- 二叉树: 如果一棵树的度为 2,则称该树为二叉树。例如,下图中呈现的树是一棵二叉树,因为它的度为 2:

请注意,前图显示了一个有四个级别和八个节点的树。
- 满树: 所有节点的度都相同的树称为满树,其度将等于树的度。下图展示了前面讨论过的树的类型:

请注意,左侧的二叉树不是一棵满树,因为节点C的度为 1,而所有其他节点的度为 2。中间的树和左侧的树都是满树。
-
完美树: 完美树是一种特殊类型的满树,其中所有叶节点都在同一级别。例如,右侧的二叉树如前图所示是一棵完美的满树,因为所有叶节点都在同一级别,即级别 2。
-
有序树: 如果节点的子节点根据特定标准有序排列,那么树被称为有序树。例如,树可以按照从左到右的升序顺序进行排序,同一级别的节点在从左到右遍历时值会增加。
实际例子
树是一种主要的数据结构之一,在开发决策树中被使用,将在第七章 传统监督学习算法中讨论。由于其分层结构,它在与网络分析相关的算法中也很受欢迎,将在第六章 无监督机器学习算法中详细讨论。树还被用于各种搜索和排序算法,其中需要实现分而治之的策略。
摘要
在本章中,我们讨论了可以用来实现各种类型算法的数据结构。通过阅读本章,我期望你能够选择合适的数据结构来存储和处理算法的数据。你还应该能够理解我们选择对算法性能的影响。
下一章是关于排序和搜索算法,我们将在算法的实现中使用本章介绍的一些数据结构。
第三章:排序和搜索算法
在本章中,我们将看一下用于排序和搜索的算法。这是一类重要的算法,可以单独使用,也可以成为更复杂算法的基础(在本书的后面章节中介绍)。本章首先介绍了不同类型的排序算法。它比较了各种设计排序算法的方法的性能。然后,详细介绍了一些搜索算法。最后,探讨了本章介绍的排序和搜索算法的一个实际例子。
在本章结束时,您将能够理解用于排序和搜索的各种算法,并能够理解它们的优势和劣势。由于搜索和排序算法是大多数更复杂算法的基础,详细了解它们将有助于您理解现代复杂算法。
以下是本章讨论的主要概念:
-
介绍排序算法
-
介绍搜索算法
-
一个实际的例子
让我们首先看一些排序算法。
介绍排序算法
在大数据时代,能够高效地对复杂数据结构中的项目进行排序和搜索是非常重要的,因为许多现代算法都需要这样做。正确的排序和搜索数据的策略将取决于数据的大小和类型,正如本章所讨论的。虽然最终结果是完全相同的,但对于实际问题的高效解决方案,需要正确的排序和搜索算法。
本章介绍了以下排序算法:
-
冒泡排序
-
归并排序
-
插入排序
-
希尔排序
-
选择排序
在 Python 中交换变量
在实现排序和搜索算法时,我们需要交换两个变量的值。在 Python 中,有一种简单的交换两个变量的方式,如下所示:
var1 = 1
var2 = 2
var1,var2 = var2,var1
>>> print (var1,var2)
>>> 2 1
让我们看看它是如何工作的:

在本章中,这种简单的交换值的方式在排序和搜索算法中被广泛使用。
让我们从下一节开始看冒泡排序算法。
冒泡排序
冒泡排序是用于排序的最简单和最慢的算法。它被设计成这样,列表中的最高值会在算法循环迭代时冒泡到顶部。正如之前讨论的,它的最坏情况性能是 O(N²),因此应该用于较小的数据集。
理解冒泡排序背后的逻辑
冒泡排序基于各种迭代,称为通行。对于大小为N的列表,冒泡排序将有N-1 个通行。让我们专注于第一次迭代:第一遍。
第一遍的目标是将最高的值推到列表的顶部。随着第一遍的进行,我们会看到列表中最高的值冒泡到顶部。
冒泡排序比较相邻的邻居值。如果较高位置的值比较低索引处的值要大,我们交换这些值。这种迭代会一直持续,直到我们到达列表的末尾。如下图所示:

现在让我们看看如何使用 Python 实现冒泡排序:
#Pass 1 of Bubble Sort
lastElementIndex = len(list)-1
print(0,list)
for idx in range(lastElementIndex):
if list[idx]>list[idx+1]: list[idx],list[idx+1]=list[idx+1],list[idx]
print(idx+1,list)
如果我们在 Python 中实现冒泡排序的第一遍,它将如下所示:

第一遍完成后,最高的值在列表的顶部。算法接下来进行第二遍。第二遍的目标是将第二高的值移动到列表中第二高的位置。为了做到这一点,算法将再次比较相邻的邻居值,如果它们不按顺序,则交换它们。第二遍将排除顶部元素,它已经被第一遍放在正确的位置上,不需要再次触摸。
完成第二次通行证后,算法将继续执行第三次通行证,直到列表的所有数据点按升序排列。算法将需要N-1 次通行证来完全对大小为N的列表进行排序。Python 中冒泡排序的完整实现如下:

现在让我们来看一下BubbleSort算法的性能。
冒泡排序的性能分析
很容易看出,冒泡排序涉及两个级别的循环:
-
外部循环:这也被称为通行证。例如,通行证一是外部循环的第一次迭代。
-
内部循环:这是当列表中剩余的未排序元素被排序时,直到最大值被冒泡到右侧。第一次通行证将有N-1 次比较,第二次通行证将有N-2 次比较,每次后续通行证将减少一次比较。
由于两个级别的循环,最坏情况下的运行时复杂度将是 O(n²)。
插入排序
插入排序的基本思想是,在每次迭代中,我们从数据结构中移除一个数据点,然后将其插入到正确的位置。这就是为什么我们称之为插入排序算法。在第一次迭代中,我们选择两个数据点并对它们进行排序。然后,我们扩展我们的选择并选择第三个数据点,并根据其值找到其正确的位置。算法进行到所有数据点都移动到它们的正确位置。这个过程如下图所示:

插入排序算法可以在 Python 中编写如下:
def InsertionSort(list):
for i in range(1, len(list)):
j = i-1
element_next = list[i]
while (list[j] > element_next) and (j >= 0):
list[j+1] = list[j]
j=j-1
list[j+1] = element_next
return list
请注意,在主循环中,我们遍历整个列表。在每次迭代中,两个相邻的元素分别是list[j](当前元素)和list[i](下一个元素)。
在list[j] > element_next和j >= 0中,我们将当前元素与下一个元素进行比较。
让我们使用这段代码来对数组进行排序:

让我们来看一下插入排序算法的性能。
从算法描述中很明显,如果数据结构已经排序,插入排序将执行得非常快。实际上,如果数据结构已排序,那么插入排序将具有线性运行时间;即 O(n)。最坏情况是每个内部循环都必须移动列表中的所有元素。如果内部循环由i定义,插入排序算法的最坏情况性能如下所示:


总通行证数量如下图所示:

一般来说,插入可以用于小型数据结构。对于较大的数据结构,由于二次*均性能,不建议使用插入排序。
归并排序
到目前为止,我们已经介绍了两种排序算法:冒泡排序和插入排序。如果数据部分排序,它们的性能都会更好。本章介绍的第三种算法是归并排序算法,它是由约翰·冯·诺伊曼于 1940 年开发的。该算法的特点是其性能不依赖于输入数据是否排序。与 MapReduce 和其他大数据算法一样,它基于分而治之的策略。在第一阶段,称为拆分,算法继续递归地将数据分成两部分,直到数据的大小小于定义的阈值。在第二阶段,称为合并,算法继续合并和处理,直到我们得到最终结果。该算法的逻辑如下图所示:

让我们首先看一下归并排序算法的伪代码:
mergeSort(list, start, end)
if(start < end)
midPoint = (end - start) / 2 + start
mergeSort(list, start, midPoint)
mergeSort(list, midPoint + 1, start)
merge(list, start, midPoint, end)
正如我们所看到的算法有以下三个步骤:
-
它将输入列表分成两个相等的部分
-
它使用递归分割,直到每个列表的长度为 1
-
然后,它将排序好的部分合并成一个排序好的列表并返回它
实现MergeSort的代码如下所示:

当运行上述 Python 代码时,它会生成以下输出:

请注意,代码的结果是一个排序好的列表。
谢尔排序
冒泡排序算法比较相邻的元素,如果它们的顺序不正确,则交换它们。如果我们有一个部分排序的列表,冒泡排序应该会有合理的性能,因为它会在循环中不再发生元素交换时立即退出。
但对于完全无序的大小为N的列表,你可以说冒泡排序将不得不完全迭代N-1 次才能完全排序它。
唐纳德·谢尔提出了谢尔排序(以他的名字命名),质疑了选择相邻元素进行比较和交换的重要性。
现在,让我们理解这个概念。
在第一次通过中,我们不是选择相邻的元素,而是使用固定间隔的元素,最终对由一对数据点组成的子列表进行排序。如下图所示。在第二次通过中,它对包含四个数据点的子列表进行排序(见下图)。在后续的通过中,每个子列表中的数据点数量不断增加,子列表的数量不断减少,直到我们达到只有一个包含所有数据点的子列表的情况。此时,我们可以假设列表已排序:

在 Python 中,实现谢尔排序算法的代码如下所示:
def ShellSort(list):
distance = len(list) // 2
while distance > 0:
for i in range(distance, len(list)):
temp = input_list[i]
j = i
# Sort the sub list for this distance
while j >= distance and list[j - distance] > temp:
list[j] = list[j - distance]
j = j-distance
list[j] = temp
# Reduce the distance for the next element
distance = distance//2
return list
上述代码可以用于对列表进行排序,如下所示:

请注意,调用ShellSort函数已导致对输入数组进行排序。
谢尔排序的性能分析
谢尔排序不适用于大数据。它用于中等大小的数据集。粗略地说,它在具有多达 6000 个元素的列表上具有相当不错的性能。如果数据部分处于正确的顺序,性能会更好。在最佳情况下,如果列表已经排序,它只需要通过N个元素进行一次验证,产生O(N)的最佳性能。
选择排序
正如我们在本章前面看到的,冒泡排序是最简单的排序算法之一。选择排序是对冒泡排序的改进,我们试图最小化算法所需的总交换次数。它旨在使每次通过只进行一次交换,而不是冒泡排序算法的N-1 次通过。我们不是像冒泡排序中那样逐步将最大值向顶部冒泡(导致N-1 次交换),而是在每次通过中寻找最大值并将其向顶部移动。因此,在第一次通过后,最大值将位于顶部。第二次通过后,第二大的值将位于顶部值旁边。随着算法的进行,后续的值将根据它们的值移动到它们的正确位置。最后一个值将在第(N-1)次通过后移动。因此,选择排序需要N-1 次通过来排序N个项目:

Python 中选择排序的实现如下所示:
def SelectionSort(list):
for fill_slot in range(len(list) - 1, 0, -1):
max_index = 0
for location in range(1, fill_slot + 1):
if list[location] > list[max_index]:
max_index = location
list[fill_slot],list[max_index] = list[max_index],list[fill_slot]
当选择排序算法被执行时,它将产生以下输出:

请注意,最终输出是排序好的列表。
选择排序算法的性能
选择排序的最坏情况性能是O(N**²)。注意,它的最坏性能类似于冒泡排序,不应该用于对更大的数据集进行排序。尽管如此,选择排序比冒泡排序更好设计,并且由于交换次数的减少,其*均性能比冒泡排序更好。
选择排序算法
选择正确的排序算法取决于当前输入数据的大小和状态。对于已排序的小输入列表,使用高级算法会在代码中引入不必要的复杂性,而性能改善微乎其微。例如,对于较小的数据集,我们不需要使用归并排序。冒泡排序会更容易理解和实现。如果数据部分排序,我们可以利用插入排序。对于较大的数据集,归并排序算法是最好的选择。
搜索算法简介
在复杂数据结构中高效地搜索数据是最重要的功能之一。最简单的方法是在每个数据点中搜索所需的数据,但随着数据规模的增大,我们需要更复杂的为搜索数据设计的算法。
本节介绍了以下搜索算法:
-
线性搜索
-
二分搜索
-
插值搜索
让我们更详细地看看它们每一个。
线性搜索
搜索数据的最简单策略之一是简单地循环遍历每个元素寻找目标。每个数据点都被搜索匹配,当找到匹配时,结果被返回并且算法退出循环。否则,算法会继续搜索直到达到数据的末尾。线性搜索的明显缺点是由于固有的穷举搜索,它非常慢。优点是数据不需要像本章介绍的其他算法那样需要排序。
让我们来看一下线性搜索的代码:
def LinearSearch(list, item):
index = 0
found = False
# Match the value with each data element
while index < len(list) and found is False:
if list[index] == item:
found = True
else:
index = index + 1
return found
现在让我们来看一下前面代码的输出:

注意,运行LinearSearch函数会在成功找到数据时返回True值。
线性搜索的性能
正如讨论的那样,线性搜索是一种执行穷举搜索的简单算法。它的最坏情况行为是O(N)。
二分搜索
二分搜索算法的先决条件是排序数据。该算法迭代地将列表分成两部分,并跟踪最低和最高索引,直到找到所寻找的值:
def BinarySearch(list, item):
first = 0
last = len(list)-1
found = False
while first<=last and not found:
midpoint = (first + last)//2
if list[midpoint] == item:
found = True
else:
if item < list[midpoint]:
last = midpoint-1
else:
first = midpoint+1
return found
输出如下:

注意,调用BinarySearch函数将在输入列表中找到值时返回True。
二分搜索的性能
二分搜索之所以被这样命名,是因为在每次迭代中,算法将数据分成两部分。如果数据有N个项目,迭代最多需要 O(logN)步。这意味着算法具有O(logN)的运行时间。
插值搜索
二分搜索基于将重点放在数据的中间部分的逻辑。插值搜索更加复杂。它使用目标值来估计已排序数组中元素的位置。让我们通过一个例子来理解它。假设我们想在英语词典中搜索一个单词,比如river。我们将利用这些信息进行插值,并开始搜索以r开头的单词。更一般化的插值搜索可以编程如下:
def IntPolsearch(list,x ):
idx0 = 0
idxn = (len(list) - 1)
found = False
while idx0 <= idxn and x >= list[idx0] and x <= list[idxn]:
# Find the mid point
mid = idx0 +int(((float(idxn - idx0)/( list[idxn] - list[idx0])) * ( x - list[idx0])))
# Compare the value at mid point with search value
if list[mid] == x:
found = True
return found
if list[mid] < x:
idx0 = mid + 1
return found
输出如下:

注意,在使用IntPolsearch之前,数组首先需要使用排序算法进行排序。
插值搜索的性能
如果数据分布不均匀,插值搜索算法的性能将很差。该算法的最坏情况性能为O(N),如果数据相对均匀,最佳性能为 O(log(log N))。
实际应用
在给定数据存储库中高效准确地搜索数据对许多现实生活应用至关重要。根据您选择的搜索算法,您可能需要首先对数据进行排序。选择正确的排序和搜索算法将取决于数据的类型和大小,以及您试图解决的问题的性质。
让我们尝试使用本章介绍的算法来解决某个国家移民局新申请人与历史记录匹配的问题。当有人申请签证进入该国时,系统会尝试将申请人与现有的历史记录进行匹配。如果至少找到一个匹配项,那么系统会进一步计算个人过去被批准或拒绝的次数。另一方面,如果没有找到匹配项,系统会将申请人分类为新申请人,并为其发放新的标识符。在历史数据中搜索、定位和识别个人的能力对系统至关重要。这些信息很重要,因为如果某人过去曾申请过并且已知申请被拒绝,那么这可能会对该个人当前的申请产生负面影响。同样,如果某人的申请过去已知被批准,那么这个批准可能会增加该个人当前申请获批准的机会。通常,历史数据库将有数百万行数据,我们需要一个精心设计的解决方案来将新申请人与历史数据库进行匹配。
假设数据库中的历史表如下所示:
| 个人 ID | 申请 ID | 名字 | 姓氏 | 出生日期 | 决定 | 决定日期 |
|---|---|---|---|---|---|---|
| 45583 | 677862 | 约翰 | 多 | 2000-09-19 | 已批准 | 2018-08-07 |
| 54543 | 877653 | Xman | Xsir | 1970-03-10 | 被拒绝 | 2018-06-07 |
| 34332 | 344565 | 阿格罗 | 瓦卡 | 1973-02-15 | 被拒绝 | 2018-05-05 |
| 45583 | 677864 | 约翰 | 多 | 2000-09-19 | 已批准 | 2018-03-02 |
| 22331 | 344553 | 卡尔 | 索茨 | 1975-01-02 | 已批准 | 2018-04-15 |
在这个表中,第一列“个人 ID”与历史数据库中的每个唯一申请人相关联。如果历史数据库中有 3000 万个唯一申请人,那么将有 3000 万个唯一的个人 ID。每个个人 ID 标识历史数据库系统中的一个申请人。
第二列是“申请 ID”。每个申请 ID 标识系统中的一个唯一申请。一个人过去可能申请过多次。这意味着在历史数据库中,我们将有比个人 ID 更多的唯一申请 ID。如上表所示,约翰·多只有一个个人 ID,但有两个申请 ID。
上表仅显示了历史数据集的一部分样本。假设我们的历史数据集中有接* 100 万行数据,其中包含过去 10 年申请人的记录。新申请人以每分钟约 2 人的*均速度持续到达。对于每个申请人,我们需要执行以下操作:
-
为申请人发放新的申请 ID。
-
查看历史数据库中是否有与申请人匹配的记录。
-
如果找到匹配项,则使用历史数据库中找到的个人 ID。我们还需要确定在历史数据库中申请已被批准或拒绝的次数。
-
如果没有找到匹配项,那么我们需要为该个人发放新的个人 ID。
假设一个新的人员带着以下的证件到达:
-
“名字”: “约翰”
-
姓氏:
多 -
出生日期:2000-09-19
现在,我们如何设计一个能够执行高效和具有成本效益的搜索的应用程序呢?
搜索数据库中新申请的一个策略可以设计如下:
-
按
出生日期对历史数据库进行排序。 -
每次有新人到来时,都要为申请人发放新的申请 ID。
-
获取所有与该出生日期匹配的记录。这将是主要搜索。
-
在出现匹配项的记录中,使用名字和姓氏进行次要搜索。
-
如果找到匹配项,请使用
个人 ID来引用申请人。计算批准和拒绝的次数。 -
如果找不到匹配项,请为申请人发放新的个人 ID。
让我们尝试选择正确的算法来对历史数据库进行排序。我们可以安全地排除冒泡排序,因为数据量很大。希尔排序将表现更好,但仅当我们有部分排序的列表时。因此,归并排序可能是对历史数据库进行排序的最佳选择。
当有新人到来时,我们需要在历史数据库中定位并搜索该人。由于数据已经排序,可以使用插值搜索或二分搜索。因为申请人可能根据出生日期均匀分布,所以可以安全地使用二分搜索。
最初,我们基于出生日期进行搜索,这将返回一组共享相同出生日期的申请人。现在,我们需要在共享相同出生日期的小子集中找到所需的人。由于我们已成功将数据减少到一个小子集,任何搜索算法,包括冒泡排序,都可以用于搜索申请人。请注意,我们在这里稍微简化了次要搜索问题。如果找到多个匹配项,我们还需要通过汇总搜索结果来计算批准和拒绝的总数。
在现实场景中,每个个体都需要在次要搜索中使用一些模糊搜索算法进行识别,因为名字可能拼写略有不同。搜索可能需要使用某种距离算法来实现模糊搜索,其中相似度高于定义的阈值的数据点被视为相同。
总结
在本章中,我们介绍了一组排序和搜索算法。我们还讨论了不同排序和搜索算法的优缺点。我们量化了这些算法的性能,并学会了何时使用每个算法。
在下一章中,我们将学习动态算法。我们还将研究设计算法的实际示例以及页面排名算法的细节。最后,我们将学习线性规划算法。
第四章:设计算法
本章介绍了各种算法的核心设计概念。它讨论了设计算法的各种技术的优缺点。通过理解这些概念,您将学会如何设计高效的算法。
本章首先讨论了在设计算法时可用的不同选择。然后,它讨论了表征我们试图解决的特定问题的重要性。接下来,它以著名的“旅行推销员问题”(TSP)为案例,并应用我们将要介绍的不同设计技术。然后,它介绍了线性规划并讨论了其应用。最后,它介绍了线性规划如何用于解决现实世界问题。
通过本章结束时,您应该能够理解设计高效算法的基本概念。
本章讨论了以下概念:
-
设计算法的各种方法
-
了解选择算法正确设计所涉及的权衡
-
制定现实世界问题的最佳实践
-
解决现实世界的优化问题
让我们首先看一下设计算法的基本概念。
介绍设计算法的基本概念
根据美国传统词典的定义,算法如下:
“一组有限的明确指令,给定一些初始条件,可以按照规定的顺序执行,以实现特定目标,并具有可识别的一组结束条件。”
设计算法是为了以最有效的方式提出这个“一组有限的明确指令”来“实现特定目标”。对于一个复杂的现实世界问题,设计算法是一项繁琐的任务。为了提出一个良好的设计,我们首先需要充分了解我们试图解决的问题。我们首先需要弄清楚需要做什么(即了解需求)然后再看如何做(即设计算法)。了解问题包括解决问题的功能和非功能性需求。让我们看看这些是什么:
-
功能性需求正式指定了我们要解决的问题的输入和输出接口以及与之相关的功能。功能性需求帮助我们理解数据处理、数据操作和需要实施的计算,以生成结果。
-
非功能性需求设置了算法性能和安全方面的期望。
请注意,设计算法是在给定一组情况下以最佳方式解决功能和非功能性需求,并考虑到运行设计算法的可用资源。
为了提出一个能够满足功能和非功能需求的良好响应,我们的设计应该尊重以下三个关注点,如第一章《算法概述》中所讨论的:
-
关注点 1:设计的算法是否能产生我们期望的结果?
-
关注点 2:这是否是获得这些结果的最佳方式?
-
关注点 3:算法在更大数据集上的表现如何?
在本节中,让我们逐一看看这些关注点。
关注点 1 - 设计的算法是否能产生我们期望的结果?
算法是对现实世界问题的数学解决方案。为了有用,它应该产生准确的结果。如何验证算法的正确性不应该是事后想到的事情;相反,它应该融入到算法的设计中。在制定如何验证算法之前,我们需要考虑以下两个方面:
-
定义真相:为了验证算法,我们需要一些已知的给定输入的正确结果。在我们试图解决的问题的上下文中,这些已知的正确结果被称为真相。真相很重要,因为在我们迭代地努力将算法演变为更好的解决方案时,它被用作参考。
-
选择度量标准:我们还需要考虑如何量化与定义真相的偏差。选择正确的度量标准将帮助我们准确量化算法的质量。
例如,对于机器学习算法,我们可以使用现有的标记数据作为真相。我们可以选择一个或多个度量标准,如准确度、召回率或精确度,来量化与真相的偏差。需要注意的是,在某些用例中,正确的输出不是一个单一的值。相反,正确的输出被定义为给定一组输入的范围。在设计和开发算法时,目标是迭代改进算法,直到它在需求中指定的范围内。
关注点 2 - 这是获得这些结果的最佳方式吗?
第二个关注点是找到以下问题的答案:
这是最佳解决方案吗?我们能验证不存在比我们的解决方案更好的解决方案吗?
乍一看,这个问题看起来很简单。然而,对于某类算法,研究人员已经花费了数十年的时间,试图验证算法生成的特定解决方案是否也是最佳解决方案,以及是否存在其他解决方案可以给出更好的结果。因此,首先了解问题、其需求和可用于运行算法的资源是很重要的。我们需要承认以下声明:
我们应该努力寻找这个问题的最佳解决方案吗?找到并验证最佳解决方案是如此耗时和复杂,以至于基于启发式的可行解决方案是我们最好的选择。
因此,理解问题及其复杂性是重要的,有助于我们估计资源需求。
在我们深入研究之前,首先让我们定义这里的一些术语:
-
多项式算法:如果一个算法的时间复杂度为O(n**^k ),我们称之为多项式算法,其中k是一个常数。
-
证书:在迭代结束时产生的候选解决方案被称为证书。当我们迭代解决特定问题时,我们通常会生成一系列证书。如果解决方案朝着收敛前进,每个生成的证书都会比前一个更好。在某个时刻,当我们的证书满足要求时,我们将选择该证书作为最终解决方案。
在第一章《算法概述》中,我们介绍了大 O 符号,它可以用来分析算法的时间复杂度。在分析时间复杂度的上下文中,我们关注以下不同的时间间隔:
-
算法产生提议解决方案(证书)所需的时间,称为证书(t[r])
-
验证提议解决方案(证书)所需的时间,t[s]
表征问题的复杂性
多年来,研究界根据问题的复杂性将问题分为各种类别。在尝试设计解决方案之前,首先尝试对问题进行表征是有意义的。一般来说,问题有三种类型:
-
类型 1:我们可以保证存在一个多项式算法来解决这些问题
-
类型 2:我们可以证明它们不能通过多项式算法解决的问题
-
类型 3:我们无法找到多项式算法来解决这些问题,但也无法证明这些问题不存在多项式解决方案
让我们来看看各种问题类别:
-
非确定性多项式(NP):要成为 NP 问题,问题必须满足以下条件:
-
可以保证存在一个多项式算法,可用于验证候选解决方案(证书)是否最优。
-
多项式(P):这些问题可以被视为 NP 的子集。除了满足 NP 问题的条件外,P 问题还需要满足另一个条件:
-
可以保证至少存在一个多项式算法可用于解决它们。
P和NP问题之间的关系如下图所示:
如果一个问题是 NP,那么它也是 P 吗?这是计算机科学中仍未解决的最大问题之一。由 Clay 数学研究所选定的千禧年大奖问题宣布为解决此问题提供 100 万美元奖金,因为它将对人工智能、密码学和理论计算机科学等领域产生重大影响:

让我们继续列举各种问题类别:
-
NP 完全:NP 完全类别包含所有 NP 问题中最难的问题。NP 完全问题满足以下两个条件:
-
没有已知的多项式算法来生成证书。
-
已知有多项式算法可以验证所提出的证书是否最优。
-
NP 难:NP 难类别包含至少与 NP 类别中的任何问题一样困难的问题,但它们本身不需要属于 NP 类别。
现在,让我们尝试绘制一个图表来说明这些不同的问题类别:

请注意,研究界尚未证明 P = NP。尽管尚未证明,但极有可能 P ≠ NP。在这种情况下,NP 完全问题不存在多项式解。请注意,前面的图表是基于这一假设的。
问题 3 - 算法在更大的数据集上的表现如何?
算法以一定方式处理数据以产生结果。一般来说,随着数据规模的增加,处理数据和计算所需结果的时间也会越来越长。术语“大数据”有时用于粗略识别预计对基础设施和算法工作具有挑战性的数据集,因为它们的规模、多样性和速度。设计良好的算法应该是可扩展的,这意味着它应该以一种能够有效运行的方式设计,利用可用资源并在合理的时间内生成正确的结果。在处理大数据时,算法的设计变得更加重要。为了量化算法的可扩展性,我们需要牢记以下两个方面:
-
随着输入数据增加,资源需求的增加:估算这样的需求称为空间复杂性分析。
-
随着输入数据增加,运行所需时间的增加:估算这一点称为时间复杂性分析。
请注意,我们生活在一个被数据爆炸所定义的时代。术语“大数据”已经成为主流,因为它捕捉到了现代算法通常需要处理的数据的规模和复杂性。
在开发和测试阶段,许多算法只使用少量数据样本。在设计算法时,重要的是要考虑算法的可扩展性方面。特别重要的是要仔细分析(即测试或预测)算法在数据集增大时的性能影响。
理解算法策略
一个精心设计的算法试图通过尽可能将问题分解为更小的子问题,以最有效地优化可用资源的使用。设计算法有不同的算法策略。算法策略涉及算法列表中包含缺失算法的三个方面。
本节中我们将介绍以下三种策略:
-
分而治之策略
-
动态规划策略
-
贪婪算法策略
理解分而治之策略
其中一种策略是找到一种方法将一个较大的问题分解为可以独立解决的较小问题。这些子问题产生的子解决方案然后组合在一起生成问题的整体解决方案。这就是分而治之策略。
从数学上讲,如果我们正在为一个需要处理数据集d的n个输入的问题(P)设计解决方案,我们将问题分解为k个子问题,P[1]到P[k]。每个子问题将处理数据集d的一个分区。通常,我们将有P**[1]到P**[k]处理d[1]到d[k]。
让我们看一个实际的例子。
实际例子 - 将分而治之应用于 Apache Spark
Apache Spark 是一个用于解决复杂分布式问题的开源框架。它实现了分而治之的策略来解决问题。为了处理问题,它将问题分解为各种子问题,并独立处理它们。我们将通过使用一个简单的例子来演示这一点,从一个列表中计算单词的数量。
假设我们有以下单词列表:
wordsList = [python, java, ottawa, news, java, ottawa]
我们想要计算列表中每个单词的频率。为此,我们将应用分而治之策略以高效地解决这个问题。
分而治之的实现如下图所示:

前面的图显示了问题被分解为以下阶段:
-
分割:输入数据被分成可以独立处理的分区。这就是分割。在前面的图中我们有三个分割。
-
映射:任何可以在分割上独立运行的操作都称为映射。在前面的图中,映射操作将分区中的每个单词转换为键值对。对应于三个分割,有三个并行运行的映射器。
-
洗牌:洗牌是将相似的键放在一起的过程。一旦相似的键放在一起,聚合函数就可以对它们的值进行运算。请注意,洗牌是一个性能密集型的操作,因为需要将最初分布在网络中的相似键放在一起。
-
减少:对相似键的值运行聚合函数称为减少。在前面的图中,我们需要计算单词的数量。
让我们看看如何编写代码来实现这一点。为了演示分而治之的策略,我们需要一个分布式计算框架。我们将在 Apache Spark 上运行 Python:
- 首先,为了使用 Apache Spark,我们将创建一个 Apache Spark 的运行时上下文:
import findspark
findspark.init()
from pyspark.sql import SparkSession
spark = SparkSession.builder.master("local[*]").getOrCreate()
sc = spark.sparkContext
- 现在,让我们创建一个包含一些单词的样本列表。我们将把这个列表转换成 Spark 的本地分布式数据结构,称为弹性分布式数据集(RDD):
wordsList = ['python', 'java', 'ottawa', 'ottawa', 'java','news']
wordsRDD = sc.parallelize(wordsList, 4)
# Print out the type of wordsRDD
print (wordsRDD.collect())
- 现在,让我们使用
map函数将单词转换为键值对:

- 让我们使用
reduce函数来聚合并得到最终结果:

这显示了我们如何使用分而治之策略来计算单词的数量。
现代云计算基础设施,如 Microsoft Azure、Amazon Web Services 和 Google Cloud,通过直接或间接地实现分而治之策略来实现可扩展性。
理解动态规划策略
动态规划是理查德·贝尔曼在 1950 年代提出的一种优化某些类别算法的策略。它基于一种智能缓存机制,试图重复使用繁重的计算。这种智能缓存机制称为记忆化。
当我们试图解决的问题可以分解为子问题时,动态规划可以带来良好的性能优势。这些子问题部分涉及在这些子问题中重复的计算。其思想是执行该计算一次(这是耗时的步骤),然后在其他子问题中重复使用它。这是通过记忆化实现的,特别适用于解决可能多次评估相同输入的递归问题。
理解贪婪算法
在深入研究本节内容之前,让我们先定义两个术语:
-
算法开销:每当我们尝试找到某个问题的最优解时,都需要一些时间。随着我们试图优化的问题变得越来越复杂,找到最优解所需的时间也会增加。我们用Ω[i]表示算法开销。
-
与最优解的差异:对于给定的优化问题,存在一个最优解。通常,我们使用我们选择的算法迭代优化解决方案。对于给定的问题,总是存在一个完美的解决方案,称为最优解。正如讨论的那样,根据我们试图解决的问题的分类,最优解可能是未知的,或者计算和验证它可能需要不合理的时间。假设最优解已知,则在第i次迭代中,当前解决方案与最优解的差异称为与最优解的差异,用Δ[i]表示。
对于复杂问题,我们有两种可能的策略:
-
策略 1:花费更多时间找到最接*最优解的解决方案,使Δ[i]尽可能小。
-
策略 2:最小化算法开销Ω[i]。使用快速且简单的方法,只使用可行的解决方案。
贪婪算法是基于策略 2 的,我们不会努力寻找全局最优解,而是选择最小化算法开销。
使用贪婪算法是一种快速简单的策略,用于找到多阶段问题的全局最优值。它基于选择局部最优值,而不努力验证局部最优值是否也是全局最优值。一般来说,除非我们很幸运,贪婪算法不会得到可以被认为是全局最优的值。然而,找到全局最优值是一项耗时的任务。因此,与分而治之和动态规划算法相比,贪婪算法更快。
一般来说,贪婪算法定义如下:
-
假设我们有一个数据集D。在这个数据集中,选择一个元素k。
-
假设候选解或证书是S。考虑将k包含在解决方案S中。如果可以包含,那么解决方案就是Union(S, e)。
-
重复这个过程,直到S填满或D用尽。
实际应用-解决 TSP
让我们首先看一下 TSP 的问题陈述,这是一个在上世纪 30 年代被提出的众所周知的问题。TSP 是一个 NP 难问题。首先,我们可以随机生成一个满足访问所有城市条件的旅行路线,而不考虑最优解。然后,我们可以努力改进每次迭代的解决方案。迭代中生成的每个旅行路线称为候选解(也称为证书)。证明证书是最优的需要指数级增加的时间。相反,使用基于不同启发式的解决方案,这些解决方案生成的旅行路线接*于最优但并非最优。
旅行商需要访问给定的城市列表才能完成他们的工作:
| 输入 | 一个包含n个城市(表示为V)和每对城市之间距离的列表,d ij (1 ≤ i, j ≤ n) |
|---|---|
| 输出 | 访问每个城市一次并返回到初始城市的最短旅行路线 |
注意以下内容:
-
列表中的城市之间的距离是已知的,
-
给定列表中的每个城市都需要被访问一次。
我们能为销售员生成旅行计划吗?什么是可以最小化旅行商所走总距离的最优解?
以下是五个加拿大城市之间的距离,我们可以用于 TSP:
| Ottawa | Montreal | Kingston | Toronto | Sudbury | |
|---|---|---|---|---|---|
| Ottawa | - | 199 | 196 | 450 | 484 |
| Montreal | 199 | - | 287 | 542 | 680 |
| Kingston | 196 | 287 | - | 263 | 634 |
| Toronto | 450 | 542 | 263 | - | 400 |
| Sudbury | 484 | 680 | 634 | 400 | - |
请注意,目标是获得一个从初始城市出发并返回到初始城市的旅行路线。例如,一个典型的旅行路线可以是 Ottawa–Sudbury–Montreal–Kingston–Toronto–Ottawa,成本为484 + 680 + 287 + 263 + 450 = 2,164。这是销售员需要旅行的最短距离吗?什么是可以最小化旅行商所走总距离的最优解?我将留给你去思考和计算。
使用蛮力策略
解决 TSP 的第一个解决方案是使用蛮力策略找到销售员访问每个城市一次并返回到初始城市的最短路径。因此,蛮力策略的工作方式如下:
-
评估所有可能的旅行路线。
-
选择一个我们可以得到最短距离的方案。
问题在于对于n个城市,存在(n-1)!种可能的旅行路线。这意味着五个城市将产生4! = 24种旅行路线,我们将选择对应最短距离的那个。很明显,这种方法只适用于我们没有太多城市的情况。随着城市数量的增加,蛮力策略由于生成的排列数量庞大而变得不稳定。
让我们看看如何在 Python 中实现蛮力策略。
首先,注意到一个旅行路线{1,2,3}表示从城市 1 到城市 2 和城市 3 的旅行路线。旅行路线中的总距离是旅行路线中覆盖的总距离。我们将假设城市之间的距离是它们之间的最短距离(即欧几里得距离)。
让我们首先定义三个实用函数:
-
distance_points:计算两点之间的绝对距离 -
distance_tour:计算销售员在给定旅行中需要覆盖的总距离 -
generate_cities:随机生成一个位于宽度为500,高度为300的矩形内的n个城市的集合
让我们看一下以下代码:
import random
from itertools import permutations
alltours = permutations
def distance_tour(aTour):
return sum(distance_points(aTour[i - 1], aTour[i])
for i in range(len(aTour)))
aCity = complex
def distance_points(first, second): return abs(first - second)
def generate_cities (number_of_cities):
seed=111;width=500;height=300
random.seed((number_of_cities, seed))
return frozenset(aCity(random.randint(1, width), random.randint(1, height))
for c in range(number_of_cities))
在上面的代码中,我们从itertools包的permutations函数实现了alltours。我们还用复数表示了距离。这意味着:
-
计算两个城市a和b之间的距离就是简单的
distance (a,b), -
我们可以通过调用
generate_cities(n)来创建n个城市。
现在让我们定义一个名为brute_force的函数,它生成所有可能的城市旅游路线。一旦生成了所有可能的路线,它将选择最短距离的路线:
def brute_force(cities):
"Generate all possible tours of the cities and choose the shortest
tour."
return shortest_tour(alltours(cities))
def shortest_tour(tours): return min(tours, key=distance_tour)
现在让我们定义一些实用函数,可以帮助我们绘制城市。我们将定义以下函数:
-
visualize_tour:绘制特定旅游路线中的所有城市和链接。它还会突出显示旅游路线的起始城市。 -
visualize_segment:由visualize_tour使用,用于绘制路段中的城市和链接。
看看以下代码:
%matplotlib inline
import matplotlib.pyplot as plt
def visualize_tour(tour, style='bo-'):
if len(tour) > 1000: plt.figure(figsize=(15, 10))
start = tour[0:1]
visualize_segment(tour + start, style)
visualize_segment(start, 'rD')
def visualize_segment (segment, style='bo-'):
plt.plot([X(c) for c in segment], [Y(c) for c in segment], style, clip_on=False)
plt.axis('scaled')
plt.axis('off')
def X(city): "X axis"; return city.real
def Y(city): "Y axis"; return city.imag
让我们实现一个名为tsp()的函数,它可以执行以下操作:
-
根据算法和请求的城市数量生成旅游路线
-
计算算法运行所花费的时间
-
生成一个图
一旦定义了tsp(),我们就可以使用它来创建一条旅游路线:

请注意,我们已经用它来为 10 个城市生成旅游路线。当n=10 时,它将生成(10-1)! = 362,880个可能的排列。如果n增加,排列的数量会急剧增加,而暴力方法无法使用。
使用贪婪算法
如果我们使用贪婪算法来解决 TSP 问题,那么在每一步,我们可以选择一个看起来合理的城市,而不是找到一个可以得到最佳整体路径的城市。因此,每当我们需要选择一个城市时,我们只需选择最*的城市,而不必验证这个选择是否会得到全局最优路径。
贪婪算法的方法很简单:
-
从任何城市开始。
-
在每一步中,通过移动到尚未访问过的最*邻居的城市来构建旅游路线。
-
重复步骤 2。
让我们定义一个名为greedy_algorithm的函数,可以实现这个逻辑:
def greedy_algorithm(cities, start=None):
C = start or first(cities)
tour = [C]
unvisited = set(cities - {C})
while unvisited:
C = nearest_neighbor(C, unvisited)
tour.append(C)
unvisited.remove(C)
return tour
def first(collection): return next(iter(collection))
def nearest_neighbor(A, cities):
return min(cities, key=lambda C: distance_points(C, A))
现在,让我们使用greedy_algorithm为 2,000 个城市创建一条旅游路线:

请注意,生成 2,000 个城市的旅游路线只花了 0.514 秒。如果我们使用了暴力方法,它将生成(2000-1)!个排列,几乎是无穷大。
请注意,贪婪算法是基于启发式的,没有证据表明解决方案将是最优的。
现在,让我们来看看 PageRank 算法的设计。
介绍 PageRank 算法
作为一个实际的例子,让我们来看看 PageRank 算法,最初被谷歌用来对用户查询的搜索结果进行排名。它生成一个数字,量化了搜索结果在用户执行的查询上下文中的重要性。这是由斯坦福大学的两位博士生拉里·佩奇和谢尔盖·布林在 20 世纪 90 年代末设计的,他们后来创办了谷歌。
PageRank 算法是以拉里·佩奇的名字命名的,他在斯坦福大学与谢尔盖·布林一起创建了它。
让我们首先正式定义 PageRank 最初设计的问题。
问题定义
每当用户在网络上的搜索引擎上输入查询时,通常会得到大量的结果。为了使结果对最终用户有用,重要的是使用某些标准对网页进行排名。显示的结果使用这个排名来总结用户的结果,并且依赖于底层算法定义的标准。
实现 PageRank 算法
PageRank 算法最重要的部分是找到计算每个页面重要性的最佳方法。为了计算一个从0到1的数字,可以量化特定页面的重要性,该算法结合了以下两个组件的信息:
-
用户输入的查询特定信息:这个组件估计了用户输入的查询的上下文中,网页内容的相关性。页面的内容直接取决于页面的作者。
-
与用户输入的查询无关的信息:这个组件试图量化每个网页在其链接、浏览和邻域的重要性。这个组件很难计算,因为网页是异质的,而且很难制定可以应用于整个网络的标准。
为了在 Python 中实现 PageRank 算法,首先让我们导入必要的库:
import numpy as np import networkx as nx import matplotlib.pyplot as plt %matplotlib inline
为了演示的目的,让我们假设我们只分析网络中的五个网页。让我们称这组页面为myPages,它们一起在一个名为myWeb的网络中:
myWeb = nx.DiGraph() myPages = range(1,5)
现在,让我们随机连接它们以模拟实际网络:
connections = [(1,3),(2,1),(2,3),(3,1),(3,2),(3,4),(4,5),(5,1),(5,4)] myWeb.add_nodes_from(myPages) myWeb.add_edges_from(connections)
现在,让我们绘制这个图:
pos=nx.shell_layout(myWeb) nx.draw(myWeb, pos, arrows=True, with_labels=True) plt.show()
它创建了我们网络的可视表示,如下所示:

在 PageRank 算法中,网页的模式包含在一个称为转换矩阵的矩阵中。有一些算法不断更新转换矩阵,以捕捉不断变化的网络状态。转换矩阵的大小是n x n,其中n是节点的数量。矩阵中的数字是访问者由于出站链接而下一个转到该链接的概率。
在我们的情况下,上面的图显示了我们拥有的静态网络。让我们定义一个函数,用于创建转换矩阵:

请注意,这个函数将返回G,它代表我们图的转换矩阵。
让我们为我们的图生成转换矩阵:

请注意,我们图的转换矩阵是5 x 5。每一列对应图中的每个节点。例如,第 2 列是关于第二个节点的。访问者从节点 2 导航到节点 1 或节点 3 的概率为 0.5。请注意,转换矩阵的对角线是0,因为在我们的图中,节点没有到自身的出站链接。在实际网络中,这可能是可能的。
请注意,转换矩阵是一个稀疏矩阵。随着节点数量的增加,大多数值将为0。
理解线性规划
线性规划背后的基本算法是由乔治·丹齐格在 1940 年代初在加州大学伯克利分校开发的。丹齐格在为美国空军工作时,利用这个概念进行了物流供应和容量规划的实验。二战结束后,丹齐格开始为五角大楼工作,并将他的算法成熟为一种他称之为线性规划的技术。它被用于军事作战规划。
今天,它被用来解决与根据某些约束最小化或最大化变量相关的重要现实问题。这些问题的一些例子如下:
-
根据资源最小化修理汽车的时间
-
在分布式计算环境中分配可用的分布式资源以最小化响应时间
-
根据公司内资源的最佳分配来最大化公司的利润
制定线性规划问题
使用线性规划的条件如下:
-
我们应该能够通过一组方程来阐明问题。
-
方程中使用的变量必须是线性的。
定义目标函数
请注意,前面三个例子的目标都是关于最小化或最大化一个变量。这个目标在数学上被公式化为其他变量的线性函数,并被称为目标函数。线性规划问题的目标是在保持指定约束条件的情况下最小化或最大化目标函数。
指定约束条件
在尝试最小化或最大化某些东西时,现实世界中存在一些需要遵守的约束。例如,当试图最小化修理汽车所需的时间时,我们还需要考虑到可用的技工数量是有限的。通过线性方程指定每个约束是制定线性规划问题的重要部分。
线性规划的实际应用-容量规划
让我们看一个实际应用案例,线性规划可以用来解决一个现实世界的问题。假设我们想要最大化一家制造两种不同类型机器人的尖端工厂的利润:
-
高级模型(A):这提供了完整的功能。制造每个高级模型的单位都会带来 4200 美元的利润。
-
基本模型(B):这只提供基本功能。制造每个基本模型的单位都会带来 2800 美元的利润。
制造机器人需要三种不同类型的人。制造每种类型机器人所需的确切天数如下:
| 机器人类型 | 技术员 | AI 专家 | 工程师 |
|---|---|---|---|
| 机器人 A:高级模型 | 3 天 | 4 天 | 4 天 |
| 机器人 B:基本模型 | 2 天 | 3 天 | 3 天 |
工厂以 30 天为周期运行。一个 AI 专家在一个周期内可用 30 天。两名工程师在 30 天内休假 8 天。因此,工程师在一个周期内只有 22 天可用。一个技术员在 30 天周期内可用 20 天。
以下表格显示了工厂中我们拥有的人数:
| 技术员 | AI 专家 | 工程师 | |
|---|---|---|---|
| 人数 | 1 | 1 | 2 |
| 周期内的总天数 | 1 x 20 = 20 天 | 1 x 30 = 30 天 | 2 x 22 = 44 天 |
这可以建模如下:
-
最大利润= 4200A + 2800B
-
这取决于以下内容:
-
A ≥ 0:生产的高级机器人数量可以是
0或更多。 -
B ≥ 0:生产的基本机器人数量可以是
0或更多。 -
3A + 2B ≤ 20:这是技术员可用性的约束。
-
4A+3B ≤ 30:这是 AI 专家可用性的约束。
-
4A+ 3B ≤ 44:这是工程师可用性的约束。
首先,我们导入名为pulp的 Python 包,用于实现线性规划:
import pulp
然后,我们在这个包中调用LpProblem函数来实例化问题类。我们将实例命名为利润最大化问题:
# Instantiate our problem class
model = pulp.LpProblem("Profit maximising problem", pulp.LpMaximize)
然后,我们定义两个线性变量,A和B。变量A表示生产的高级机器人数量,变量B表示生产的基本机器人数量:
A = pulp.LpVariable('A', lowBound=0, cat='Integer')
B = pulp.LpVariable('B', lowBound=0, cat='Integer')
我们将目标函数和约束定义如下:
# Objective function
model += 5000 * A + 2500 * B, "Profit"
# Constraints
model += 3 * A + 2 * B <= 20
model += 4 * A + 3 * B <= 30
model += 4 * A + 3 * B <= 44
我们使用solve函数生成解决方案:
# Solve our problem
model.solve()
pulp.LpStatus[model.status]
然后,我们打印A和B的值以及目标函数的值:
线性规划在制造业中被广泛使用,以找到应该使用的产品的最佳数量,以优化可用资源的使用。
现在我们来到了本章的结尾!让我们总结一下我们学到了什么。
摘要
在本章中,我们看了各种设计算法的方法。我们看了选择正确的算法设计所涉及的权衡。我们看了制定现实世界问题的最佳实践。我们还看了如何解决现实世界的优化问题。从本章中学到的经验可以用来实现设计良好的算法。
在下一章中,我们将专注于基于图的算法。我们将首先研究表示图的不同方法。然后,我们将研究建立在各种数据点周围进行特定调查的技术。最后,我们将研究从图中搜索信息的最佳方法。
第五章:图算法
有一类计算问题最好以图的术语来表示。这类问题可以使用一类称为图算法的算法来解决。例如,图算法可以用于在数据的图形表示中高效搜索值。为了高效工作,这些算法首先需要发现图的结构。它们还需要找到正确的策略来跟随图的边以读取顶点中存储的数据。由于图算法需要搜索值才能工作,因此高效的搜索策略是设计高效图算法的核心。使用图算法是在复杂的相互关联的数据结构中搜索信息的最有效方式之一。在当今的大数据、社交媒体和分布式数据时代,这些技术变得越来越重要和有用。
在本章中,我们将首先介绍图算法背后的基本概念。然后,我们将介绍网络分析理论的基础知识。接下来,我们将看看可以用来遍历图的各种技术。最后,我们将看一个案例研究,展示图算法如何用于欺诈检测。
在本章中,我们将介绍以下概念:
-
表示图的不同方式
-
引入网络理论分析
-
理解图的遍历
-
案例研究:欺诈分析
-
在我们的问题空间中建立邻域的技术
在本章结束时,您将对图是什么以及如何使用它们来表示相互关联的数据结构并从直接或间接关系的实体中挖掘信息有很好的理解,以及如何使用它们来解决一些复杂的现实世界问题。
图的表示
图是一种以顶点和边的形式表示数据的结构。图表示为aGraph = (𝓥, 𝓔),其中𝓥表示一组顶点,𝓔表示一组边。注意aGraph有|𝓥|个顶点和|𝓔|条边。
一个顶点,𝓋 ∈ 𝓥,表示现实世界的对象,如人、计算机或活动。一条边,𝓋 ∈ 𝓔,连接网络中的两个顶点:
e(𝓋[1], 𝓋[2]) | e ∈ 𝓔 & 𝓋[i] ∈ 𝓥
前面的方程表明,在图中,所有边属于一个集合𝓔,所有顶点属于一个集合𝓥。
一条边连接两个顶点,因此代表它们之间的关系。例如,它可以代表以下关系:
-
人与人之间的友谊
-
一个人在 LinkedIn 上连接了一个朋友
-
一个集群中两个节点的物理连接
-
一个人参加研究会议
在本章中,我们将使用networkx Python 包来表示图。让我们尝试使用 Python 中的networtx包创建一个简单的图。首先,让我们尝试创建一个空图aGraph,没有顶点或节点:
import networkx as nx
G = nx.Graph()
让我们添加一个单个顶点:
G.add_node("Mike")
我们还可以使用列表添加一堆顶点:
G.add_nodes_from(["Amine", "Wassim", "Nick"])
我们还可以在现有的顶点之间添加一条边,如下所示:
G.add_edge("Mike", "Amine")
现在让我们打印边和顶点:

请注意,如果我们添加一条边,这也会导致添加相关的顶点,如果它们尚不存在,如下所示:
G.add_edge("Amine","Imran")
如果我们打印节点列表,我们将看到以下输出:

请注意,对已经存在的顶点进行添加的请求会被静默忽略。请求的忽略或接受取决于我们创建的图的类型。
图的类型
图可以分为四种类型,即以下四种:
-
无向图
-
有向图
-
无向多重图
-
有向多重图
现在让我们逐个详细查看每一个。
无向图
在大多数情况下,图的组成节点之间表示的关系可以被认为是无方向的。这种关系不对关系施加任何顺序。这样的边被称为无向边,结果图被称为无向图。以下是一个无向图的示例:

一些无向关系的例子如下:
-
迈克和阿敏(迈克和阿敏互相认识)。
-
节点 A 和节点 B 相连(这是一种点对点的连接)。
有向图
图中节点之间的关系具有某种方向感的图被称为有向图。以下是一个有向图的示例:

一些有向关系的例子如下:
-
迈克和他的房子(迈克住在一所房子里,但他的房子不住在迈克里)。
-
约翰管理保罗(约翰是保罗的经理)。
无向多重图
有时,节点之间有多种关系。在这种情况下,可以有多条边连接相同的两个节点。这种图称为多重图,在同一节点上允许多条*行边。我们必须明确指出一个特定的图是否是多重图。*行边可以表示节点之间的不同类型的关系。
以下图显示了一个多重图:

多向关系的一个例子是迈克和约翰既是同学又是同事。
有向多重图
如果多重图中的节点之间存在方向关系,则称为有向多重图:

有向多重图的一个例子是迈克在办公室向约翰汇报,并且约翰教迈克 Python 编程语言。
特殊类型的边
边将图的各个顶点连接在一起,并表示它们之间的关系。除了简单的边,它们还可以是以下特殊类型:
-
自边:有时,特定的顶点可以与自己有关系。例如,约翰把钱从他的商业账户转到他的个人账户。这种特殊关系可以用自导向边来表示。
-
超边:有时,多个顶点由同一条边连接。连接多个顶点以表示这种关系的边被称为超边。例如,假设迈克、约翰和莎拉三人一起参与一个特定项目。
具有一个或多个超边的图被称为超图。
这里显示了自边和超边图的图示:

请注意,一个特定的图可以有多种特殊类型的边节点。这意味着一个特定的图可以同时具有自边和超边。
自我中心网络
特定顶点m的直接邻域可能包含足够重要的信息,以进行对节点的决定性分析。自我中心,或者称为 egonet,就是基于这个想法的。特定顶点m的 egonet 包括所有直接连接到m的顶点以及节点m本身。节点m被称为自我,与之连接的一跳邻居被称为替代者。
特定节点 3 的自我网络在以下图中显示:

请注意,egonet 代表一度邻域。这个概念可以扩展到 n 度邻域,包括所有 n 跳离感兴趣的顶点的顶点。
社交网络分析
社交网络分析(SNA)是图论的重要应用之一。如果满足以下条件,网络图分析被认为是社交网络分析:
-
图的顶点代表人。
-
它们之间的边代表着它们之间的社会关系,如友谊、共同爱好、血缘关系、性关系、厌恶等等。
-
我们通过图分析试图回答的商业问题具有很强的社会方面。
人类行为在 SNA 中得到反映,并且在进行 SNA 时应始终牢记。通过在图中绘制人际关系,SNA 可以深入了解人际互动,这有助于我们理解他们的行为。
通过在每个个体周围创建邻域,并根据其社会关系分析个体的行为,您可以产生有趣的,有时令人惊讶的见解。基于个体的个人工作职能对个体进行分析的替代方法只能提供有限的见解。
因此,SNA 可以用于以下方面:
-
理解用户在社交媒体*台上的行为,如 Facebook、Twitter 或 LinkedIn
-
理解欺诈
-
理解社会的犯罪行为
LinkedIn 在 SNA 相关的新技术的研究和开发方面做出了很大贡献。事实上,LinkedIn 可以被认为是该领域许多算法的先驱。
因此,由于社交网络的固有分布和相互连接的架构,SNA 是图论最强大的用例之一。另一种抽象图的方法是将其视为网络,并应用设计用于网络的算法。这整个领域被称为网络分析理论,我们将在下面讨论。
介绍网络分析理论
我们知道,互连的数据可以表示为网络。在网络分析理论中,我们研究了开发用于探索和分析表示为网络的数据的方法的细节。让我们在本节中看一些网络分析理论的重要方面。
首先,注意网络中的一个顶点充当基本单元。网络是一个由顶点相互连接而成的网络,其中每个连接代表着调查对象之间的关系。在解决问题的背景下,量化网络中顶点的有用性和重要性是很重要的。有各种技术可以帮助我们量化重要性。
让我们看一些网络分析理论中使用的重要概念。
理解最短路径
路径是起始节点和结束节点之间的节点序列,路径上没有节点出现两次。路径代表了所选起始和结束顶点之间的路线。它将是一组连接起始顶点和结束顶点的顶点p。在p中没有重复的顶点。
路径的长度是通过计算组成边来计算的。在所有选项中,具有最小长度的路径称为最短路径。最短路径的计算在图论算法中被广泛使用,但并不总是直接计算。有不同的算法可以用来找到起始节点和结束节点之间的最短路径。其中一个最流行的算法是Dijkstra 算法,它在 20 世纪 50 年代末出版。它可以计算图中的最短路径。它可以被全球定位系统(GPS)设备用来计算源和目的地之间的最小距离。Dijkstra 算法也用于网络路由算法。
谷歌和苹果之间存在一场争夺,他们要设计出最佳的谷歌地图和苹果地图的最短距离算法。他们面临的挑战是使算法足够快,可以在几秒内计算出最短路径。
在本章后面,我们将讨论广度优先搜索(BFS)算法,它可以修改为迪杰斯特拉算法。BFS 假设在给定图中遍历每条路径的成本相同。对于迪杰斯特拉算法,遍历图的成本可能不同,需要将其纳入修改 BFS 为迪杰斯特拉算法。
正如所示,迪杰斯特拉算法是一种计算最短路径的单源算法。如果我们想解决所有最短路径对,那么可以使用弗洛伊德-沃舍尔算法。
创建邻域
为了图算法的关键节点周围创建邻域的策略至关重要。创建邻域的方法基于选择与感兴趣的顶点直接关联的方法。创建邻域的一种方法是选择一个k阶策略,该策略选择与感兴趣的顶点相距k跳的顶点。
让我们看看创建邻域的各种标准。
三角形
在图论中,找到彼此连接良好的顶点对于分析很重要。一种技术是尝试识别三角形,在网络中,三角形是由三个直接相连的节点组成的子图。
让我们看看欺诈检测的用例,我们在本章末尾也将其用作案例研究。如果节点m的 egonet 包括三个顶点,包括顶点m,那么这个 egonet 就是一个三角形。顶点m将是 ego,而两个连接的顶点将是 alter,比如顶点A和顶点B。如果两个 alter 都是已知的欺诈案例,我们可以安全地宣布顶点m也是欺诈的。如果其中一个 alter 涉及欺诈,我们无法得出结论性证据,但我们需要进一步调查欺诈证据。
密度
让我们首先定义一个完全连接的网络。我们称每个顶点直接连接到每个其他顶点的图为完全连接的网络。
如果我们有一个完全连接的网络N,那么网络中的边数可以表示如下:

现在,这就是密度发挥作用的地方。密度测量观察到的边的数量与最大边数的比值,如果Edges****[Observed]是我们想要观察的边的数量。它可以表述如下:

请注意,对于三角形,网络的密度为1,这代表了最高可能的连接网络。
理解中心性度量
有不同的度量方法来理解图或子图中特定顶点的中心性。例如,它们可以量化社交网络中一个人的重要性,或者城市中建筑物的重要性。
以下中心性度量在图分析中被广泛使用:
-
度
-
介数
-
紧密度
-
特征向量
让我们详细讨论它们。
度
顶点连接的边的数量称为其度。它可以指示特定顶点的连接情况以及其在网络中快速传播消息的能力。
让我们考虑aGraph = (𝓥, 𝓔),其中𝓥表示顶点集合,𝓔表示边集合。回想一下,aGraph有|𝓥|个顶点和|𝓔|条边。如果我们将节点的度除以(|𝓥|-1),则称为度中心性:

现在,让我们看一个具体的例子。考虑以下图:

现在,在上述图中,顶点 C 的度为 4。其度中心性可以计算如下:

介数
介数是图中的中心性度量。在社交媒体的背景下,它将量化一个人在子群中参与通信的概率。对于计算机网络,介数将量化在顶点故障的情况下对图节点之间通信的负面影响。
要计算aGraph中顶点a的介数,按照以下步骤进行:
-
计算
aGraph中每对顶点之间的最短路径。让我们用
来表示这一点。 -
从
,计算通过顶点a的最短路径数量。让我们用
来表示这一点。 -
使用
来计算介数。
公*和亲*
让我们拿一个图g。图g中顶点a的公*性被定义为顶点a到其他顶点的距离之和。请注意,特定顶点的中心性量化了它与所有其他顶点的总距离。
公*性的对立面是亲*度。
特征向量中心性
特征向量中心性给出了图中所有顶点的分数,衡量它们在网络中的重要性。该分数将是特定节点与整个网络中其他重要节点的连接性的指标。当谷歌创建了PageRank 算法时,该算法为互联网上的每个网页分配一个分数(以表达其重要性),这个想法就是源自特征向量中心性度量。
使用 Python 计算中心性度量
让我们创建一个网络,然后尝试计算其中心性度量。以下代码块说明了这一点:
import networkx as nx
import matplotlib.pyplot as plt
vertices = range(1,10)
edges = [(7,2), (2,3), (7,4), (4,5), (7,3), (7,5), (1,6),(1,7),(2,8),(2,9)]
G = nx.Graph()
G.add_nodes_from(vertices)
G.add_edges_from(edges)
nx.draw(G, with_labels=True,node_color='y',node_size=800)
这段代码生成的图如下所示:

到目前为止,我们已经研究了不同的中心性度量。让我们为前面的例子计算这些度量:

请注意,中心性的度量应该给出图或子图中特定顶点的中心性度量。从图中看,标记为 7 的顶点似乎具有最中心的位置。顶点 7 在中心性的四个度量中具有最高值,因此反映了它在这个上下文中的重要性。
现在让我们看看如何从图中检索信息。图是复杂的数据结构,存储了大量的信息,既在顶点中又在边中。让我们看一些可以用来有效地遍历图以从中收集信息以回答查询的策略。
理解图遍历
要利用图,需要从中挖掘信息。图遍历被定义为用于确保以有序方式访问每个顶点和边的策略。努力确保每个顶点和边都被访问一次,不多不少。广义上讲,可以有两种不同的遍历图的方式来搜索其中的数据。按广度进行称为广度优先搜索(BFS),按深度进行称为深度优先搜索(DFS)。让我们依次看一下它们。
广度优先搜索
当我们处理aGraph时,如果存在层次或邻域级别的概念,BFS 效果最好。例如,当 LinkedIn 中一个人的联系被表示为图时,有一级联系,然后有二级联系,这直接对应于层次。
BFS 算法从根顶点开始,探索邻居顶点,然后移动到下一个邻居级别并重复这个过程。
让我们看一个 BFS 算法。为此,让我们首先考虑以下无向图:

让我们从计算每个顶点的直接邻居开始,并将其存储在一个称为邻接表的列表中。在 Python 中,我们可以使用字典数据结构来存储它:
graph={ 'Amin' : {'Wasim', 'Nick', 'Mike'},
'Wasim' : {'Imran', 'Amin'},
'Imran' : {'Wasim','Faras'},
'Faras' : {'Imran'},
'Mike' : {'Amin'},
'Nick' : {'Amin'}}
为了在 Python 中实现它,我们按照以下步骤进行。
我们将首先解释初始化,然后是主循环。
初始化
我们将使用两种数据结构:
-
visited:包含所有已经被访问的顶点。最初,它将是空的。 -
queue:包含我们希望在下一次迭代中访问的所有顶点。
主循环
接下来,我们将实现主循环。它将一直循环,直到队列中没有一个元素。对于队列中的每个节点,如果它已经被访问过,那么它就会访问它的邻居。
我们可以在 Python 中实现这个主循环,如下所示:
- 首先,我们从队列中弹出第一个节点,并将其选择为此次迭代的当前节点。
node = queue.pop(0)
- 然后,我们检查节点是否不在已访问列表中。如果不在,我们将其添加到已访问节点的列表中,并使用邻居来表示其直接连接的节点
visited.append(node)
neighbours = graph[node]
- 现在我们将节点的邻居添加到队列中:
for neighbour in neighbours:
queue.append(neighbour)
-
主循环完成后,将返回包含所有遍历节点的
visited数据结构。 -
完整的代码,包括初始化和主循环,如下所示:

让我们来看看使用 BFS 定义的图的详尽搜索遍历模式。为了访问所有节点,遍历模式如下图所示。可以观察到,在执行过程中,它始终保持两种数据结构:
-
已访问:包含所有已经被访问的节点
-
队列:包含尚未被访问的节点
算法的工作原理如下:
-
它从第一个节点开始,也就是第一级上唯一的节点 Amin。
-
然后,它移动到第二级,并依次访问所有三个节点 Wasim、Nick 和 Mike。
-
然后,它移动到第三级和第四级,每个级别只有一个节点,Imran 和 Faras。
一旦所有节点都被访问,它们将被添加到已访问的数据结构中,迭代就会停止:

现在,让我们尝试使用 BFS 从这个图中找到特定的人。让我们指定我们正在搜索的数据,并观察结果:

现在让我们来看看深度优先搜索算法。
深度优先搜索
DFS 是 BFS 的替代方法,用于从图中搜索数据。将 DFS 与 BFS 区分开的因素是,在从根顶点开始后,算法会逐个沿着每条唯一的路径尽可能深入。对于每条路径,一旦成功到达最终深度,它就会标记所有与该路径相关的顶点为已访问。完成路径后,算法会回溯。如果它能找到另一条从根节点开始但尚未被访问的路径,算法会重复之前的过程。算法会在新分支中不断移动,直到所有分支都被访问。
请注意,图可能具有循环方法。如前所述,我们使用布尔标志来跟踪已处理的顶点,以避免在循环中迭代。
为了实现 DFS,我们将使用一个栈数据结构,这在第二章中已经详细讨论过,算法中使用的数据结构。请记住,栈是基于后进先出(LIFO)原则的。这与队列相反,队列用于 BFS,它基于先进先出(FIFO)原则。
以下代码用于 DFS:
def dfs(graph, start, visited=None):
if visited is None:
visited = set()
visited.add(start)
print(start)
for next in graph[start] - visited:
dfs(graph, next, visited)
return visited
让我们再次使用以下代码来测试先前定义的dfs函数:
graph={ 'Amin' : {'Wasim', 'Nick', 'Mike'},
'Wasim' : {'Imran', 'Amin'},
'Imran' : {'Wasim','Faras'},
'Faras' : {'Imran'},
'Mike' :{'Amin'},
'Nick' :{'Amin'}}
如果我们运行这个算法,输出将如下所示:

让我们使用 DFS 方法来查看这个图的详尽遍历模式:
-
迭代从顶部节点 Amin 开始。
-
然后,它移动到第二级,Wasim。从那里,它向下一级移动,直到达到末端,即 Imran 和 Fares 节点。
-
完成第一个完整分支后,它回溯然后到达第二级访问 Nick 和 Mike。
遍历模式如下图所示:
请注意,DFS 也可以用于树。
现在,让我们看一个案例研究,解释了本章迄今为止讨论的概念如何用于解决现实世界的问题。
案例研究 - 欺诈分析
让我们看看如何使用 SNA 来检测欺诈。人类是社会动物,人的行为据说受到周围的人的影响。同质性一词被创造出来代表他们的社交网络对一个人的影响。扩展这个概念,同质网络是一群人,他们由于某些共同因素而可能与彼此关联;例如,具有相同的起源或爱好,是同一个团伙或同一个大学的一部分,或其他因素的组合。
如果我们想在同质网络中分析欺诈,我们可以利用调查对象与网络中其他人之间的关系,这些人已经仔细计算了他们参与欺诈的风险。有时因为某人的公司而标记一个人也被称为因陋就寡。
为了理解这个过程,让我们首先看一个简单的案例。为此,让我们使用一个具有九个顶点和八条边的网络。在这个网络中,有四个顶点是已知的欺诈案例,并被分类为fraud (F)。剩下的五个人中有五个没有欺诈相关历史,被分类为non-fraud (NF)。
我们将编写以下步骤的代码来生成这个图表:
- 让我们导入我们需要的包:
import networkx as nx
import matplotlib.pyplot as plt
- 定义
vertices和edges的数据结构:
vertices = range(1,10)
edges= [(7,2), (2,3), (7,4), (4,5), (7,3), (7,5), (1,6),(1,7),(2,8),(2,9)]
- 让我们首先实例化图表:
G = nx.Graph()
- 现在,让我们绘制图表:
G.add_nodes_from(vertices)
G.add_edges_from(edges)
pos=nx.spring_layout(G)
- 让我们定义 NF 节点:
nx.draw_networkx_nodes( G,pos,
nodelist=[1,4,3,8,9],
with_labels=True,
node_color='g',
node_size=1300)
- 现在,让我们创建已知涉及欺诈的节点:
nx.draw_networkx_nodes(G,pos,
nodelist=[2,5,6,7],
with_labels=True,
node_color='r',
node_size=1300)
- 让我们为节点创建标签:
nx.draw_networkx_edges(G,pos,edges,width=3,alpha=0.5,edge_color='b') labels={} labels[1]=r'1 NF' labels[2]=r'2 F' labels[3]=r'3 NF' labels[4]=r'4 NF' labels[5]=r'5 F' labels[6]=r'6 F' labels[7]=r'7 F' labels[8]=r'8 NF' labels[9]=r'9 NF'
nx.draw_networkx_labels(G,pos,labels,font_size=16)
一旦前面的代码运行,它将显示出一个这样的图:

请注意,我们已经进行了详细的分析,将每个节点分类为图或非图。假设我们在网络中添加另一个名为q的顶点,如下图所示。我们对这个人没有先前的信息,也不知道这个人是否涉及欺诈。我们希望根据他们与社交网络中现有成员的联系来将这个人分类为NF或F:

我们已经设计了两种方法来对代表节点q的新人进行分类,分为F或NF:
-
使用一种不使用中心性指标和有关欺诈类型的附加信息的简单方法
-
使用了一个名为“瞭望塔”的方法,这是一种先进的技术,利用了现有节点的中心性指标,以及有关欺诈类型的其他信息
我们将详细讨论每种方法。
进行简单的欺诈分析
欺诈分析的简单技术是基于这样一个假设:在一个网络中,一个人的行为受到他们所连接的人的影响。在一个网络中,如果两个顶点与彼此相关联,那么它们更有可能具有相似的行为。
基于这一假设,我们设计了一种简单的技术。如果我们想找到某个节点a属于F的概率,概率表示为P(F/q),计算如下:

让我们将这应用到前面的图中,其中Neighborhood[n]代表顶点n的邻域,w(n, nj)代表n和n**j之间连接的权重。此外,degree[q]是节点q的度。然后,概率计算如下:

根据这个分析,这个人涉及欺诈的可能性为 67%。我们需要设定一个阈值。如果阈值为 30%,那么这个人就高于阈值,我们可以安全地标记他们为 F。
请注意,这个过程需要针对网络中的每个新节点重复进行。
现在,让我们看一种进行欺诈分析的高级方法。
介绍了瞭望塔欺诈分析方法
之前的简单欺诈分析技术有以下两个限制:
-
它不评估社交网络中每个顶点的重要性。与涉及欺诈的中心的联系可能与与一个远离的孤立个人的关系有不同的含义。
-
当在现有网络中将某人标记为已知的欺诈案例时,我们不考虑犯罪的严重程度。
瞭望塔欺诈分析方法解决了这两个限制。首先,让我们看一些概念。
评分负面结果
如果一个人已知涉及欺诈,我们说与这个人相关联的是一个负面结果。并非每个负面结果的严重程度或严肃程度都相同。一个已知冒充另一个人的人将会有一个更严重类型的负面结果与他们相关联,而不仅仅是试图以创新的方式使用过期的 20 美元礼品卡使其有效的人。
从 1 到 10 的评分中,我们对各种负面结果进行如下评分:
| 负面结果 | 负面结果分数 |
|---|---|
| 冒充 | 10 |
| 涉及信用卡盗窃 | 8 |
| 假支票提交 | 7 |
| 犯罪记录 | 6 |
| 无记录 | 0 |
请注意,这些分数将基于我们对欺诈案例及其在历史数据中的影响的分析。
怀疑程度
怀疑程度(DOS)量化了我们对一个人可能涉及欺诈的程度。DOS 值为 0 意味着这是一个低风险的人,DOS 值为 9 意味着这是一个高风险的人。
对历史数据的分析显示,专业的欺诈者在他们的社交网络中拥有重要的地位。为了纳入这一点,首先我们计算网络中每个顶点的四个中心度指标。然后我们取这些顶点的*均值。这反映了该特定人在网络中的重要性。
如果与一个顶点相关联的人涉及欺诈,我们将使用前面表格中显示的预先确定的值对这个人进行评分,以反映犯罪的严重程度。
最后,我们将中心度指标的*均值和负面结果分数相乘,得到 DOS 的值。我们通过将其除以网络中 DOS 的最大值来标准化 DOS。
现在,让我们计算前一个网络中每个九个节点的 DOS:
| 节点 1 | 节点 2 | 节点 3 | 节点 4 | 节点 5 | 节点 6 | 节点 7 | 节点 8 | 节点 9 | |
|---|---|---|---|---|---|---|---|---|---|
| 中心度度 | 0.25 | 0.5 | 0.25 | 0.25 | 0.25 | 0.13 | 0.63 | 0.13 | 0.13 |
| 中介中心度 | 0.25 | 0.47 | 0 | 0 | 0 | 0 | 0.71 | 0 | 0 |
| 接*中心度 | 0.5 | 0.61 | 0.53 | 0.47 | 0.47 | 0.34 | 0.72 | 0.4 | 0.4 |
| 特征向量 | 0.24 | 0.45 | 0.36 | 0.32 | 0.32 | 0.08 | 0.59 | 0.16 | 0.16 |
| 中心度指标的*均值 | 0.31 | 0.51 | 0.29 | 0.26 | 0.26 | 0.14 | 0.66 | 0.17 | 0.17 |
| 负面结果分数 | 0 | 6 | 0 | 0 | 7 | 8 | 10 | 0 | 0 |
| DOS | 0 | 3 | 0 | 0 | 1.82 | 1.1 | 6.625 | 0 | 0 |
| 标准化 DOS | 0 | 0.47 | 0 | 0 | 0.27 | 0.17 | 1 | 0 | 0 |
下图显示了每个节点及其标准化 DOS:

为了计算已添加的新节点的 DOS,我们将使用以下公式:

使用相关数值,我们将按如下计算 DOS:

这将指示与系统中添加的新节点相关的欺诈风险。这意味着在 0 到 1 的范围内,这个人的 DOS 值为 0.42。我们可以为 DOS 创建不同的风险区间,如下所示:
| DOS 的值 | 风险分类 |
|---|---|
| DOS = 0 | 无风险 |
| 0<DOS<=0.10 | 低风险 |
| 0.10<DOS<=0.3 | 中等风险 |
| DOS>0.3 | 高风险 |
根据这些标准,可以看出新个体是高风险人员,应该被标记。
通常,在进行这种分析时不涉及时间维度。但现在,有一些先进的技术可以在图的增长随时间推移时进行分析。这使研究人员能够观察网络演化时顶点之间的关系。尽管图的时间序列分析会使问题的复杂性增加许多倍,但它可能会提供对欺诈证据的额外见解,否则是不可能的。
总结
在本章中,我们了解了基于图的算法。经过本章的学习,我希望我们能够使用不同的技术来表示、搜索和处理以图形表示的数据。我们还开发了能够计算两个顶点之间的最短距离并在问题空间中构建邻域的技能。这些知识应该帮助我们使用图论来解决诸如欺诈检测之类的问题。
在下一章中,我们将专注于不同的无监督机器学习算法。本章讨论的许多用例技术与无监督学习算法相辅相成,这将在下一章中详细讨论。在数据集中找到欺诈证据就是这样的用例示例。
第二部分:机器学习算法
本节详细解释了不同类型的机器学习算法,如无监督机器学习算法和传统监督学习算法,并介绍了自然语言处理算法。本节以介绍推荐引擎结束。包括在本节中的章节有:
-
第六章,无监督机器学习算法
-
第七章,传统监督学习算法
-
第八章,神经网络算法
-
第九章,自然语言处理算法
-
第十章,推荐引擎
第六章:无监督机器学习算法
本章是关于无监督机器学习算法的。本章以介绍无监督学习技术开始。然后,我们将学习两种聚类算法:k 均值聚类和层次聚类算法。接下来的部分将介绍一种降维算法,当我们有大量输入变量时可能会很有效。接下来的部分展示了无监督学习如何用于异常检测。最后,我们将看看最强大的无监督学习技术之一,关联规则挖掘。本节还解释了从关联规则挖掘中发现的模式如何代表跨交易中各种数据元素之间的有趣关系,这可以帮助我们进行基于数据的决策。
在本章结束时,读者应该能够理解无监督学习如何用于解决一些现实世界的问题。读者将了解目前用于无监督学习的基本算法和方法论。
在本章中,我们将涵盖以下主题:
-
无监督学习
-
聚类算法
-
降维
-
异常检测算法
-
关联规则挖掘
介绍无监督学习
无监督学习的最简单定义是,它是通过发现和利用数据的固有模式来为非结构化数据提供某种结构的过程。如果数据不是由某种随机过程产生的,它在其多维问题空间中的数据元素之间将具有一些模式。无监督学习算法通过发现这些模式并利用它们来为数据集提供一些结构。这个概念在下图中显示:

请注意,无监督学习通过发现现有模式的新特征来添加结构。
数据挖掘生命周期中的无监督学习
理解无监督学习的作用,首先要看数据挖掘过程的整体生命周期。有不同的方法论将数据挖掘过程的生命周期划分为不同的独立阶段,称为阶段。目前,有两种流行的表示数据挖掘生命周期的方式:
-
CRISP-DM(跨行业标准数据挖掘过程)生命周期
-
SEMMA(样本、探索、修改、建模、访问)数据挖掘过程
CRISP-DM 是由一些数据挖掘者联合开发的,他们来自包括克莱斯勒和SPSS(社会科学统计软件包)在内的各种公司。SEMMA 是由SAS(统计分析系统)提出的。让我们看看这两种数据挖掘生命周期的表示之一,CRISP-DM,并尝试理解无监督学习在数据挖掘生命周期中的位置。请注意,SEMMA 在其生命周期内有一些类似的阶段。
如果我们看 CRISP-DM 生命周期,可以看到它包括六个不同的阶段,如下图所示:

让我们逐个了解每个阶段:
- 阶段 1:业务理解:这是收集需求的阶段,涉及从业务角度深入全面地理解问题。根据机器学习(ML)的要求定义问题的范围,并适当地重新表述它是这个阶段的重要部分。例如,对于二元分类问题,有时将需求用可以证明或拒绝的假设来表述是有帮助的。本阶段还涉及记录将在下游阶段 4 中训练的机器学习模型的期望。例如,对于分类问题,我们需要记录可以部署到生产中的模型的最低可接受准确性。
CRISP-DM 生命周期的第一阶段是业务理解,重点是需要做什么,而不是如何做。
-
第二阶段:数据理解:这是关于理解可用于数据挖掘的数据。在这个阶段,我们将找出是否有适合解决问题的正确数据集。在确定数据集之后,我们需要了解数据的质量和结构。我们需要找出可以从数据中提取的模式,这些模式可能会引导我们获得重要的见解。我们还将尝试找到可以根据第一阶段收集的要求用作标签(或目标变量)的正确特征。无监督学习算法可以在实现第二阶段目标方面发挥强大作用。无监督算法可以用于以下目的:
-
在数据集中发现模式
-
通过分析发现的模式来理解数据集的结构
-
识别或推导目标变量
-
第三阶段:数据准备:这是为我们将在第四阶段训练的 ML 模型准备数据的阶段。可用的标记数据被分成两个不相等的部分。较大的部分称为训练数据,用于在第四阶段训练模型。较小的部分称为测试数据,在第五阶段用于模型评估。在这个阶段,无监督机器学习算法可以用作准备数据的工具,例如,它们可以用于将非结构化数据转换为结构化数据,提供可以帮助训练模型的额外维度。
-
第四阶段:建模:这是我们使用监督学习来制定已发现模式的阶段。我们需要根据所选的监督学习算法的要求成功准备数据。这也是确定将用作标签的特定特征的阶段。在第三阶段,我们将数据分为测试集和训练集。在这个阶段,我们形成数学公式来表示我们感兴趣的模式中的关系。这是通过使用第三阶段创建的训练数据来训练模型完成的。如前所述,最终的数学公式将取决于我们选择的算法。
-
第五阶段:评估:这个阶段是关于使用第三阶段的测试数据测试新训练的模型。如果评估符合第一阶段设定的期望,那么我们需要再次迭代所有前面的阶段,从第一阶段开始。这在前面的图像中有所说明。
-
第六阶段:部署:如果评估符合或超过第五阶段描述的期望,那么训练好的模型将被部署到生产环境中,并开始为我们在第一阶段定义的问题提供解决方案。
CRISP-DM 生命周期的第二阶段(数据理解)和第三阶段(数据准备)都是关于理解数据并为训练模型做准备。这些阶段涉及数据处理。一些组织为这个数据工程阶段雇佣专家。
很明显,提出问题的解决方案的过程完全是数据驱动的。结合监督和无监督机器学习用于制定可行的解决方案。本章重点介绍解决方案的无监督学习部分。
数据工程包括第二阶段和第三阶段,是机器学习中最耗时的部分。它可能占据典型 ML 项目时间和资源的 70%。无监督学习算法在数据工程中可以发挥重要作用。
以下各节提供了有关无监督算法的更多细节。
无监督学习的当前研究趋势
多年来,对机器学习算法的研究更多地集中在监督学习技术上。由于监督学习技术可以直接用于推断,因此它们在时间、成本和准确性方面的优势相对容易衡量。无监督机器学习算法的潜力最*才被认识到。由于无监督学习不受指导,因此它不太依赖假设,并且可能在任何维度上收敛解决方案。尽管更难控制无监督学习算法的范围和处理要求,但它们有更多潜力发现隐藏的模式。研究人员还在努力将无监督机器学习技术与监督学习技术相结合,以设计新的强大算法。
实际例子
目前,无监督学习用于更好地理解数据并为其提供更多结构,例如,它用于市场细分、欺诈检测和市场篮分析(稍后在本章中讨论)。让我们看几个例子。
语音分类
无监督学习可以用于对语音文件中的个别声音进行分类。它利用了每个人的声音具有独特的特征这一事实,从而创建可能可分离的音频模式。这些模式可以用于语音识别,例如,谷歌在其 Google Home 设备中使用这种技术来训练它们区分不同人的声音。一旦训练完成,Google Home 可以个性化地为每个用户提供响应。
例如,假设我们有一段录制的三个人互相交谈半个小时的对话。使用无监督学习算法,我们可以识别数据集中不同人的声音。请注意,通过无监督学习,我们为给定的非结构化数据集添加了结构。这种结构为我们的问题空间提供了额外有用的维度,可以用于获取见解并为我们选择的机器学习算法准备数据。以下图表显示了无监督学习用于语音识别的情况:

请注意,在这种情况下,无监督学习建议我们添加一个具有三个不同级别的新特征。
文档分类
无监督机器学习算法也可以应用于非结构化文本数据的存储库,例如,如果我们有一组 PDF 文档的数据集,那么无监督学习可以用于以下目的:
-
发现数据集中的各种主题
-
将每个 PDF 文档与发现的主题之一关联起来
无监督学习用于文档分类的情况如下图所示。这是另一个例子,我们在非结构化数据中添加了更多的结构:
图 6.4:使用无监督学习进行文档分类
请注意,在这种情况下,无监督学习建议我们添加一个具有五个不同级别的新特征。
理解聚类算法
在无监督学习中使用的最简单和最强大的技术之一是基于通过聚类算法将相似模式分组在一起。它用于理解与我们试图解决的问题相关的数据的特定方面。聚类算法寻找数据项中的自然分组。由于该组不是基于任何目标或假设,因此被归类为无监督学习技术。
各种聚类算法创建的分组是基于在问题空间中找到各种数据点之间的相似性。确定数据点之间的相似性的最佳方法将因问题而异,并且将取决于我们正在处理的问题的性质。让我们看看可以用来计算各种数据点之间相似性的各种方法。
量化相似性
聚类算法创建的分组的可靠性是基于这样一个假设:我们能够准确量化问题空间中各种数据点之间的相似性或接*程度。这是通过使用各种距离度量来实现的。以下是用于量化相似性的三种最流行的方法:
-
欧几里得距离度量
-
曼哈顿距离度量
-
余弦距离度量
让我们更详细地看看这些距离度量。
欧几里得距离
不同点之间的距离可以量化两个数据点之间的相似性,并且广泛用于无监督机器学习技术,如聚类。欧几里得距离是最常见和简单的距离度量。它通过测量多维空间中两个数据点之间的最短距离来计算。例如,让我们考虑二维空间中的两点A(1,1)和B(4,4),如下图所示:

要计算A和B之间的距离——即d(A,B),我们可以使用以下毕达哥拉斯公式:

请注意,此计算是针对二维问题空间的。对于n维问题空间,我们可以计算两点A和B之间的距离如下:

曼哈顿距离
在许多情况下,使用欧几里得距离度量来测量两点之间的最短距离将无法真正代表两点之间的相似性或接*程度——例如,如果两个数据点代表地图上的位置,则使用地面交通工具(如汽车或出租车)从点 A 到点 B 的实际距离将大于欧几里得距离计算出的距离。对于这类情况,我们使用曼哈顿距离,它标记了两点之间的最长路线,并更好地反映了在繁忙城市中可以前往的源点和目的地点之间的接*程度。曼哈顿和欧几里得距离度量之间的比较如下图所示:

曼哈顿距离始终大于或等于相应的欧几里得距离。
余弦距离
欧几里得和曼哈顿距离度量在高维空间中表现不佳。在高维问题空间中,余弦距离更准确地反映了多维问题空间中两个数据点之间的接*程度。余弦距离度量是通过测量由两个连接到参考点的点所创建的余弦角来计算的。如果数据点接*,则角度将很窄,而不管它们具有的维度如何。另一方面,如果它们相距很远,那么角度将很大:
文本数据几乎可以被视为高维空间。由于余弦距离度量在高维空间中表现非常好,因此在处理文本数据时是一个不错的选择。
请注意,在前面的图中,A(2,5)和B(4.4)之间的角的余弦是余弦距离。这些点之间的参考点是原点——即X(0,0)。但实际上,问题空间中的任何点都可以充当参考数据点,并且不一定是原点。
K 均值聚类算法
k-means 聚类算法的名称来自于它试图创建k个聚类,通过计算均值来找到数据点之间的接*程度。它使用了一个相对简单的聚类方法,但由于其可扩展性和速度而仍然受欢迎。从算法上讲,k-means 聚类使用了一个迭代逻辑,将聚类的中心移动到它们所属的分组的最具代表性的数据点。
重要的是要注意,k-means 算法缺乏聚类所需的非常基本的功能之一。这个缺失的功能是,对于给定的数据集,k-means 算法无法确定最合适的聚类数。最合适的聚类数k取决于特定数据集中自然分组的数量。这种省略背后的哲学是尽可能简化算法,最大限度地提高其性能。这种精益简洁的设计使 k-means 适用于更大的数据集。假设将使用外部机制来计算k。确定k的最佳方法将取决于我们试图解决的问题。在某些情况下,k直接由聚类问题的上下文指定,例如,如果我们想将一类数据科学学生分成两个聚类,一个由具有数据科学技能的学生组成,另一个由具有编程技能的学生组成,那么k将为 2。在其他一些问题中,k的值可能不明显。在这种情况下,将不得不使用迭代的试错程序或基于启发式的算法来估计给定数据集的最合适的聚类数。
k-means 聚类的逻辑
本节描述了 k-means 聚类算法的逻辑。让我们逐一看一下。
初始化
为了对它们进行分组,k-means 算法使用距离度量来找到数据点之间的相似性或接*程度。在使用 k-means 算法之前,需要选择最合适的距离度量。默认情况下,将使用欧氏距离度量。此外,如果数据集中有异常值,则需要制定机制来确定要识别和删除数据集的异常值的标准。
k-means 算法的步骤
k-means 聚类算法涉及的步骤如下:
| 步骤 1 | 我们选择聚类的数量k。 |
|---|---|
| 步骤 2 | 在数据点中,我们随机选择k个点作为聚类中心。 |
| 步骤 3 | 基于所选的距离度量,我们迭代地计算问题空间中每个点到k个聚类中心的距离。根据数据集的大小,这可能是一个耗时的步骤,例如,如果聚类中有 10,000 个点,k=3,这意味着需要计算 30,000 个距离。 |
| 步骤 4 | 我们将问题空间中的每个数据点分配给最*的聚类中心。 |
| 步骤 5 | 现在我们问题空间中的每个数据点都有一个分配的聚类中心。但我们还没有完成,因为初始聚类中心的选择是基于随机选择的。我们需要验证当前随机选择的聚类中心实际上是每个聚类的重心。我们通过计算每个k聚类的组成数据点的*均值来重新计算聚类中心。这一步解释了为什么这个算法被称为 k-means。 |
| 步骤 6 | 如果在步骤 5 中聚类中心发生了变化,这意味着我们需要重新计算每个数据点的聚类分配。为此,我们将回到步骤 3 重复这个计算密集的步骤。如果聚类中心没有发生变化,或者我们的预定停止条件(例如,最大迭代次数)已经满足,那么我们就完成了。 |
下图显示了在二维问题空间中运行 k-means 算法的结果:
(a)聚类前的数据点;(b)运行 k 均值聚类算法后的结果集群
请注意,在运行 k 均值后创建的两个结果集群在这种情况下有很好的区分度。
停止条件
对于 k 均值算法,默认的停止条件是在第 5 步中不再移动集群中心。但是与许多其他算法一样,k 均值算法可能需要很长时间才能收敛,特别是在处理高维问题空间中的大型数据集时。我们可以明确定义停止条件,而不是等待算法收敛,如下所示:
-
通过指定最大执行时间:
-
停止条件:如果 t>t[max],其中t是当前执行时间,t[max]是我们为算法设置的最大执行时间。
-
通过指定最大迭代次数:
-
停止条件:如果 m>m[max],其中m是当前迭代次数,m[max]是我们为算法设置的最大迭代次数。
编写 k 均值算法
让我们看看如何在 Python 中编写 k 均值算法:
- 首先,让我们导入编写 k 均值算法所需的软件包。请注意,我们正在导入
sklearn软件包进行 k 均值聚类:
from sklearn import cluster import pandas as pd
import numpy as np
- 要使用 k 均值聚类,让我们在二维问题空间中创建 20 个数据点,这些数据点将用于 k 均值聚类:
dataset = pd.DataFrame({
'x': [11, 21, 28, 17, 29, 33, 24, 45, 45, 52, 51, 52, 55, 53, 55, 61, 62, 70, 72, 10],
'y': [39, 36, 30, 52, 53, 46, 55, 59, 63, 70, 66, 63, 58, 23, 14, 8, 18, 7, 24, 10]
})
- 让我们有两个集群(k=2),然后通过调用
fit函数创建集群:
myKmeans = cluster.KMeans(n_clusters=2)
myKmeans.fit(dataset)
- 让我们创建一个名为
centroid的变量,它是一个包含形成的集群中心位置的数组。在我们的情况下,k=2,数组的大小将为 2。让我们还创建另一个名为label的变量,表示每个数据点分配给两个集群中的一个。由于有 20 个数据点,这个数组的大小将为 20:
centroids = myKmeans.cluster_centers_
labels = myKmeans.labels_
- 现在让我们打印这两个数组,
centroids和labels:

请注意,第一个数组显示了每个数据点与集群的分配,第二个数组显示了两个集群中心。
- 让我们使用
matplotlib绘制并查看这些集群:

请注意,图中的较大点是由 k 均值算法确定的中心点。
k 均值聚类的局限性
k 均值算法旨在成为一种简单快速的算法。由于其设计上的故意简单性,它具有以下限制:
-
k 均值聚类的最大限制是初始集群数量必须预先确定。
-
集群中心的初始分配是随机的。这意味着每次运行算法时,可能会得到略有不同的集群。
-
每个数据点只分配给一个集群。
-
k 均值聚类对异常值敏感。
层次聚类
k 均值聚类使用自上而下的方法,因为我们从最重要的数据点开始算法,即集群中心。还有一种聚类的替代方法,即不是从顶部开始,而是从底部开始算法。在这种情况下,底部是问题空间中的每个单独数据点。解决方案是在向上移向集群中心的过程中不断将相似的数据点分组在一起。这种替代的自下而上方法由层次聚类算法使用,并在本节中讨论。
层次聚类的步骤
层次聚类涉及以下步骤:
-
我们在问题空间中为每个数据点创建一个单独的集群。如果我们的问题空间包含 100 个数据点,那么它将从 100 个集群开始。
-
我们只将彼此最接*的点分组。
-
我们检查停止条件;如果停止条件尚未满足,则重复步骤 2。
生成的集群结构称为树状图。
在树状图中,垂直线的高度决定了物品的接*程度,如下图所示:

请注意,停止条件显示为上图中的虚线。
编写一个分层聚类算法
让我们学习如何在 Python 中编写一个分层算法:
- 我们将首先从
sklearn.cluster库中导入AgglomerativeClustering,以及pandas和numpy包:
from sklearn.cluster import AgglomerativeClustering import pandas as pd
import numpy as np
- 然后我们将在二维问题空间中创建 20 个数据点:
dataset = pd.DataFrame({
'x': [11, 21, 28, 17, 29, 33, 24, 45, 45, 52, 51, 52, 55, 53, 55, 61, 62, 70, 72, 10],
'y': [39, 36, 30, 52, 53, 46, 55, 59, 63, 70, 66, 63, 58, 23, 14, 8, 18, 7, 24, 10]
})
- 然后我们通过指定超参数来创建分层集群。我们使用
fit_predict函数来实际处理算法:
cluster = AgglomerativeClustering(n_clusters=2, affinity='euclidean', linkage='ward')
cluster.fit_predict(dataset)
- 现在让我们看一下每个数据点与创建的两个簇的关联:

您可以看到分层和 k 均值算法的集群分配非常相似。
评估聚类
良好质量的聚类的目标是属于不同簇的数据点应该是可区分的。这意味着以下内容:
-
属于同一簇的数据点应尽可能相似。
-
属于不同簇的数据点应尽可能不同。
人类直觉可以用来通过可视化集群结果来评估集群结果,但也有数学方法可以量化集群的质量。轮廓分析是一种比较 k 均值算法创建的集群中的紧密度和分离度的技术。轮廓绘制了一个图,显示了特定集群中每个点与相邻集群中其他点的接*程度。它将与每个集群关联的数字范围为[-0, 1]。以下表显示了此范围中的数字表示什么:
| 范围 | 意义 | 描述 |
|---|---|---|
| 0.71–1.0 | 优秀 | 这意味着 k 均值聚类导致的组在相当程度上是可区分的。 |
| 0.51–0.70 | 合理 | 这意味着 k 均值聚类导致的组在某种程度上是可区分的。 |
| 0.26–0.50 | 弱 | 这意味着 k 均值聚类导致了分组,但不应依赖分组的质量。 |
| <0.25 | 未找到任何聚类 | 使用选择的参数和使用的数据,无法使用 k 均值聚类创建分组。 |
请注意,问题空间中的每个簇将获得一个单独的分数。
聚类的应用
聚类用于我们需要在数据集中发现潜在模式的地方。
在政府使用案例中,聚类可用于以下目的:
-
犯罪热点分析
-
人口社会分析
在市场研究中,聚类可用于以下目的:
-
市场细分
-
定向广告
-
客户分类
主成分分析(PCA)也用于通常探索数据并从实时数据中去除噪音,例如股票市场交易。
降维
我们数据中的每个特征对应于问题空间中的一个维度。将特征的数量最小化以使问题空间更简单称为降维。可以通过以下两种方式之一来完成:
-
特征选择:选择在我们试图解决的问题的上下文中重要的一组特征
-
特征聚合:使用以下算法之一组合两个或多个特征以减少维度:
-
PCA:线性无监督 ML 算法
-
线性判别分析(LDA):线性监督 ML 算法
-
核主成分分析:一种非线性算法
让我们更深入地了解一种流行的降维算法,即 PCA。
主成分分析
PCA 是一种无监督的机器学习技术,可以使用线性变换来降低维度。在下图中,我们可以看到两个主成分PC1和PC2,它们显示了数据点的分布形状。PC1 和 PC2 可以用适当的系数来总结数据点:

让我们考虑以下代码:
from sklearn.decomposition import PCA
iris = pd.read_csv('iris.csv')
X = iris.drop('Species', axis=1)
pca = PCA(n_components=4)
pca.fit(X)
现在让我们打印我们的 PCA 模型的系数:

请注意,原始的 DataFrame 有四个特征,Sepal.Length、Sepal.Width、Petal.Length和Petal.Width。前面的 DataFrame 指定了四个主成分 PC1、PC2、PC3 和 PC4 的系数,例如,第一行指定了可以用来替换原始四个变量的 PC1 的系数。
根据这些系数,我们可以计算我们输入 DataFrame X 的 PCA 组件:
pca_df=(pd.DataFrame(pca.components_,columns=X.columns))
# Let us calculate PC1 using coefficients that are generated
X['PC1'] = X['Sepal.Length']* pca_df['Sepal.Length'][0] + X['Sepal.Width']* pca_df['Sepal.Width'][0]+ X['Petal.Length']* pca_df['Petal.Length'][0]+X['Petal.Width']* pca_df['Petal.Width'][0]
# Let us calculate PC2
X['PC2'] = X['Sepal.Length']* pca_df['Sepal.Length'][1] + X['Sepal.Width']* pca_df['Sepal.Width'][1]+ X['Petal.Length']* pca_df['Petal.Length'][1]+X['Petal.Width']* pca_df['Petal.Width'][1]
#Let us calculate PC3
X['PC3'] = X['Sepal.Length']* pca_df['Sepal.Length'][2] + X['Sepal.Width']* pca_df['Sepal.Width'][2]+ X['Petal.Length']* pca_df['Petal.Length'][2]+X['Petal.Width']* pca_df['Petal.Width'][2]
# Let us calculate PC4
X['PC4'] = X['Sepal.Length']* pca_df['Sepal.Length'][3] + X['Sepal.Width']* pca_df['Sepal.Width'][3]+ X['Petal.Length']* pca_df['Petal.Length'][3]+X['Petal.Width']* pca_df['Petal.Width'][3]
现在让我们在计算 PCA 组件后打印 X:

现在让我们打印方差比率,并尝试理解使用 PCA 的影响:

方差比率表示如下:
-
如果我们选择用 PC1 替换原始的四个特征,那么我们将能够捕获大约 92.3%的原始变量的方差。我们通过不捕获原始四个特征 100%的方差来引入一些*似。
-
如果我们选择用 PC1 和 PC2 替换原始的四个特征,那么我们将捕获额外的 5.3%的原始变量的方差。
-
如果我们选择用 PC1、PC2 和 PC3 替换原始的四个特征,那么我们现在将捕获原始变量进一步的 0.017%的方差。
-
如果我们选择用四个主成分替换原始的四个特征,那么我们将捕获原始变量的 100%的方差(92.4 + 0.053 + 0.017 + 0.005),但用四个主成分替换四个原始特征是没有意义的,因为我们没有减少维度,也没有取得任何成果。
PCA 的局限性
PCA 的局限性如下:
-
PCA 只能用于连续变量,对于类别变量无关。
-
在聚合时,PCA *似了组件变量;它以准确性为代价简化了维度的问题。在使用 PCA 之前,应该仔细研究这种权衡。
关联规则挖掘
特定数据集中的模式是需要被发现、理解和挖掘的宝藏。有一组重要的算法试图专注于给定数据集中的模式分析。在这类算法中,较受欢迎的算法之一称为关联规则挖掘算法,它为我们提供了以下功能:
-
衡量模式频率的能力
-
建立模式之间因果关系的能力。
-
通过将它们的准确性与随机猜测进行比较,量化模式的有用性
使用示例
当我们试图调查数据集中不同变量之间的因果关系时,使用关联规则挖掘。以下是它可以帮助回答的示例问题:
-
哪些湿度、云层覆盖和温度值可能导致明天下雨?
-
什么类型的保险索赔可能表明欺诈?
-
哪些药物的组合可能会导致患者并发症?
市场篮分析
在本书中,推荐引擎在第八章“神经网络算法”中进行了讨论。篮子分析是学习推荐的一种简单方法。在篮子分析中,我们的数据只包含有关哪些物品一起购买的信息。它没有任何关于用户或用户是否喜欢个别物品的信息。请注意,获取这些数据要比获取评级数据容易得多。
例如,当我们在沃尔玛购物时,就会产生这种数据,而不需要任何特殊技术来获取数据。这些数据在一段时间内收集起来,被称为交易数据。当将关联规则分析应用于便利店、超市和快餐连锁店中使用的购物车的交易数据集时,就称为市场篮子分析。它衡量了一组物品一起购买的条件概率,有助于回答以下问题:
-
货架上物品的最佳摆放位置是什么?
-
物品在营销目录中应该如何出现?
-
基于用户的购买模式,应该推荐什么?
由于市场篮子分析可以估计物品之间的关系,因此它经常用于大众市场零售,如超市、便利店、药店和快餐连锁店。市场篮子分析的优势在于其结果几乎是不言自明的,这意味着它们很容易被业务用户理解。
让我们来看一个典型的超市。商店中所有可用的唯一物品可以用一个集合
={item[1],item[2],...,item[m]}来表示。因此,如果那家超市销售 500 种不同的物品,那么
将是一个大小为 500 的集合。
人们会从这家商店购买物品。每当有人购买物品并在柜台付款时,它就会被添加到一个特定交易中的物品集合中,称为项目集。在一段时间内,交易被分组在一个由
表示的集合中,其中
={t[1],t[2],...,t[n]}。
让我们来看一下只包含四个交易的简单交易数据。这些交易总结在下表中:
| t1 | 球门,护腕 |
|---|---|
| t2 | 球棒,球门,护腕,头盔 |
| t3 | 头盔,球 |
| t4 | 球棒、护腕、头盔 |
让我们更详细地看一下这个例子:
={球棒,球门,护腕,头盔,球},它代表了商店中所有可用的唯一物品。
让我们考虑来自
的一个交易 t3。请注意,t3 中购买的物品可以用 itemset[t3]={头盔,球}表示,这表明顾客购买了两件物品。由于这个 itemset 中有两件物品,因此 itemset[t5]的大小被称为两。
关联规则
关联规则通过数学方式描述了各种交易中涉及的物品之间的关系。它通过研究形式为X⇒Y的两个项目集之间的关系来实现这一点,其中X⊂
,Y⊂
。此外,X和Y是不重叠的项目集;这意味着
。
关联规则可以用以下形式描述:
{头盔,球}⇒{自行车}
在这里,{头盔,球}是X,{球}是Y。
规则类型
运行关联分析算法通常会从交易数据集中生成大量规则。其中大部分是无用的。为了挑选出可以提供有用信息的规则,我们可以将它们分类为以下三种类型之一:
-
琐碎
-
莫名其妙
-
可操作
让我们更详细地看看每种类型。
琐碎的规则
在生成的大量规则中,许多派生的规则将是无用的,因为它们总结了关于业务的常识。它们被称为琐碎规则。即使琐碎规则的置信度很高,它们仍然是无用的,不能用于任何数据驱动的决策。我们可以安全地忽略所有琐碎规则。
以下是琐碎规则的例子:
-
任何从高楼跳下的人都有可能死亡。
-
更努力工作会导致考试成绩更好。
-
随着温度下降,取暖器的销量会增加
-
在高速公路上超速驾驶会增加事故的可能性。
不可解释规则
在运行关联规则算法后生成的规则中,那些没有明显解释的规则是最难使用的。请注意,规则只有在能帮助我们发现和理解预期最终会导致某种行动的新模式时才有用。如果不是这种情况,我们无法解释事件X导致事件Y的原因,那么它就是一个不可解释的规则,因为它只是一个最终探索两个无关和独立事件之间毫无意义关系的数学公式。
以下是不可解释规则的例子:
-
穿红衬衫的人在考试中得分更高。
-
绿色自行车更容易被盗。
-
购买泡菜的人最终也会购买尿布。
可操作规则
可操作规则是我们正在寻找的黄金规则。它们被业务理解并引发见解。当呈现给熟悉业务领域的观众时,它们可以帮助我们发现事件可能的原因,例如,可操作规则可能根据当前的购买模式建议产品在商店中的最佳摆放位置。它们还可能建议将哪些商品放在一起,以最大化它们一起销售的机会。
以下是可操作规则及其相应的行动的例子:
- 规则 1:向用户的社交媒体账户展示广告会增加销售的可能性。
可操作项目:建议产品的替代广告方式
- 规则 2:创建更多的价格点会增加销售的可能性。
可操作项目:一个商品可能在促销中进行广告,而另一个商品的价格可能会上涨。
排名规则
关联规则有三种衡量方式:
-
物品的支持(频率)
-
置信度
-
提升
让我们更详细地看看它们。
支持
支持度量是一个数字,用来量化我们在数据集中寻找的模式有多频繁。首先计算我们感兴趣的模式出现的次数,然后将其除以所有交易的总数来计算。
让我们看看特定itemset[a]的以下公式:
numItemset[a] =包含 itemset[a]的交易数
num[total] =交易总数
仅通过支持,我们就可以了解到模式发生的罕见程度。低支持意味着我们在寻找一种罕见事件。
例如,如果itemset[a] = {头盔,球}在六次交易中出现了两次,那么支持(itemset[a])= 2/6 = 0.33。
置信度
置信度是一个数字,通过计算条件概率来量化我们可以将左侧(X)与右侧(Y)关联的强度。它计算了事件X发生的情况下,事件Y会发生的概率。
从数学上讲,考虑规则X ⇒ Y。
这条规则的置信度表示为 confidence(X ⇒ Y),并按以下方式测量:

让我们举个例子。考虑以下规则:
{头盔,球} ⇒ {球门}
这条规则的置信度由以下公式计算:

这意味着如果有人的篮子里有{头盔,球},那么他们还有球门的概率是 0.5 或 50%。
提升
估计规则质量的另一种方法是通过计算提升。提升返回一个数字,量化了规则在预测结果方面相对于仅假设等式右侧的结果的改进程度。如果X和Y项集是独立的,那么提升的计算如下:

关联分析算法
在本节中,我们将探讨以下两种可用于关联分析的算法:
-
Apriori 算法:由 Agrawal, R.和 Srikant 于 1994 年提出。
-
FP-growth 算法:由 Han 等人于 2001 年提出的改进建议。
让我们看看这些算法各自的情况。
Apriori 算法
Apriori 算法是一种迭代和多阶段的算法,用于生成关联规则。它基于生成和测试的方法。
在执行 apriori 算法之前,我们需要定义两个变量:support[threshold]和 Confidence[threshold]。
该算法包括以下两个阶段:
-
候选生成阶段:它生成包含所有高于 support[threshold]的项集的候选项集。
-
过滤阶段:它过滤掉所有低于预期 confidence[threshold]的规则。
过滤后,得到的规则就是答案。
Apriori 算法的局限性
Apriori 算法中的主要瓶颈是第 1 阶段候选规则的生成,例如,
= {item [1] , item [2] , . . . , item [m] } 可以产生 2^m 个可能的项集。由于其多阶段设计,它首先生成这些项集,然后努力找到频繁项集。这个限制是一个巨大的性能瓶颈,使得 apriori 算法不适用于更大的项。
FP-growth 算法
频繁模式增长(FP-growth)算法是对 apriori 算法的改进。它首先展示频繁交易 FP 树,这是一个有序树。它包括两个步骤:
-
填充 FP 树
-
挖掘频繁模式
让我们一步一步地看这些步骤。
填充 FP 树
让我们考虑下表中显示的交易数据。让我们首先将其表示为稀疏矩阵:
| ID | 球棒 | 球门 | 防护板 | 头盔 | 球 |
|---|---|---|---|---|---|
| 1 | 0 | 1 | 1 | 0 | 0 |
| 2 | 1 | 1 | 1 | 1 | 0 |
| 3 | 0 | 0 | 0 | 1 | 1 |
| 4 | 1 | 0 | 1 | 1 | 0 |
让我们计算每个项的频率,并按频率降序排序:
| 项 | 频率 |
|---|---|
| 防护板 | 3 |
| 头盔 | 3 |
| 球棒 | 2 |
| 球门 | 2 |
| 球 | 1 |
现在让我们根据频率重新排列基于交易的数据:
| ID | 原始项 | 重新排序的项 |
|---|---|---|
| t1 | 防护板,球门 | 防护板,球门 |
| t2 | 球棒,球门,防护板,头盔 | 头盔,防护板,球门,球棒 |
| t3 | 头盔,球 | 头盔,球 |
| t4 | 球棒,防护板,头盔 | 头盔,防护板,球棒 |
要构建 FP 树,让我们从 FP 树的第一个分支开始。FP 树以Null作为根开始。为了构建树,我们可以用一个节点表示每个项,如下图所示(这里显示了 t[1]的树表示)。请注意,每个节点的标签都是项的名称,冒号后面附加了其频率。还要注意pads项的频率为 1:

使用相同的模式,让我们绘制所有四个交易,得到完整的 FP 树。FP 树有四个叶节点,每个节点代表与四个交易相关的项集。请注意,我们需要计算每个项的频率,并在多次使用时增加它-例如,将 t[2]添加到 FP 树时,头盔 的频率增加到了两次。类似地,当添加 t[4]时,它再次增加到了三次。结果树如下图所示:

请注意,前面图中生成的 FP 树是有序树。
挖掘频繁模式
FP-growth 树的第二阶段涉及从 FP 树中挖掘频繁模式。通过创建一个有序树,意图是创建一个高效的数据结构,可以轻松导航以搜索频繁模式。
我们从叶节点(即末端节点)开始向上移动-例如,让我们从叶节点项之一 球棒 开始。然后我们需要计算 球棒 的条件模式基。通过指定从叶节点项到顶部的所有路径来计算条件模式基。球棒 的条件模式基如下:
| 球门: 1 | 护腕: 1 | 头盔: 1 |
|---|---|---|
| 护腕: 1 | 头盔: 1 |
球棒 的 频繁模式 如下:
{球门, 护腕, 头盔} : 球棒
{护腕,头盔} : 球棒
使用 FP-growth 的代码
让我们看看如何使用 Python 中的 FP-growth 算法生成关联规则。为此,我们将使用 pyfpgrowth 软件包。首先,如果我们以前从未使用过 pyfpgrowth,让我们首先安装它:
!pip install pyfpgrowth
然后,让我们导入实现此算法所需的软件包:
import pandas as pd
import numpy as np
import pyfpgrowth as fp
现在我们将创建以 transactionSet 形式的输入数据:
dict1 = {
'id':[0,1,2,3],
'items':[["wickets","pads"],
["bat","wickets","pads","helmet"],
["helmet","pad"],
["bat","pads","helmet"]]
}
transactionSet = pd.DataFrame(dict1)
一旦生成了输入数据,我们将生成基于我们传递给 find_frequent_patterns() 的参数的模式。请注意,传递给此函数的第二个参数是最小支持度,在本例中为 1:
patterns = fp.find_frequent_patterns(transactionSet['items'],1)
模式已生成。现在让我们打印模式。模式列出了项的组合及其支持:

现在让我们生成规则:

每个规则都有左侧和右侧,由冒号(:)分隔。它还为我们提供了输入数据集中每个规则的支持。
实际应用-将相似的推文进行聚类
无监督机器学习算法也可以实时应用于将相似的推文进行聚类。它们将执行以下操作:
-
步骤 1- 主题建模: 从给定的一组推文中发现各种主题
-
步骤 2- 聚类: 将每个推文与发现的主题之一关联起来
这种无监督学习的应用如下图所示:
请注意,此示例需要实时处理输入数据。
让我们逐一看看这些步骤。
主题建模
主题建模是发现一组文档中的概念的过程,这些概念可以用来区分它们。在推文的背景下,这是关于找出一组推文可以被分成哪些最合适的主题。潜在狄利克雷分配是一种用于主题建模的流行算法。因为每条推文都是一个短的 144 个字符的文档,通常涉及一个非常特定的主题,我们可以为主题建模目的编写一个更简单的算法。该算法描述如下:
-
对推文进行标记化处理。
-
预处理数据。删除停用词、数字、符号并进行词干处理
-
为推文创建一个术语-文档矩阵(TDM)。选择在唯一推文中出现最频繁的前 200 个词。
-
选择直接或间接代表概念或主题的前 10 个单词。例如时尚、纽约、编程、事故。这 10 个单词现在是我们成功发现的主题,并将成为 tweets 的聚类中心。
让我们继续下一步,即聚类
聚类
一旦我们发现了主题,我们将选择它们作为聚类的中心。然后我们可以运行 k-means 聚类算法,将每个 tweet 分配到其中一个聚类中心。
因此,这是一个实际的例子,说明一组 tweets 如何被聚类成发现的主题。
异常检测算法
异常 的词典定义是与众不同、异常、奇特或不容易分类的东西。它是偏离常规规则的。在数据科学的背景下,异常是偏离预期模式很多的数据点。寻找这样的数据点的技术被称为异常检测技术。
现在让我们看看异常检测算法的一些应用:
-
信用卡欺诈
-
在 磁共振成像(MRI) 扫描中发现恶性肿瘤
-
集群中的故障预防
-
考试中的冒名顶替
-
高速公路上的事故
在接下来的章节中,我们将看到各种异常检测技术。
使用聚类
诸如 k-means 的聚类算法可以用来将相似的数据点分组在一起。可以定义一个阈值,任何超出该阈值的点都可以被分类为异常。这种方法的问题在于,由于异常数据点的存在,k-means 聚类创建的分组本身可能会存在偏差,并可能影响方法的实用性和准确性。
使用基于密度的异常检测
基于密度的方法试图找到密集的邻域。k-最*邻(KNN)算法可以用于此目的。远离发现的密集邻域的异常被标记为异常。
使用支持向量机
支持向量机(SVM)算法可以用来学习数据点的边界。任何超出这些发现的边界的点都被识别为异常。
总结
在本章中,我们看了各种无监督的机器学习技术。我们看了尝试减少我们试图解决的问题的维度的情况,以及不同的方法。我们还研究了无监督机器学习技术在哪些情况下非常有帮助,包括市场篮分析和异常检测。
在下一章中,我们将看看各种监督学习技术。我们将从线性回归开始,然后我们将看看更复杂的监督机器学习技术,如基于决策树的算法、SVM 和 XGBoast。我们还将研究朴素贝叶斯算法,它最适合于非结构化的文本数据。
第七章:传统监督学习算法
在本章中,我们将重点介绍监督式机器学习算法,这是现代算法中最重要的类型之一。监督式机器学习算法的显著特征是使用带标签的数据来训练模型。在本书中,监督式机器学习算法分为两章。在本章中,我们将介绍所有传统的监督式机器学习算法,不包括神经网络。下一章将全面介绍使用神经网络实现监督式机器学习算法。事实上,在这一领域有如此多的持续发展,神经网络是一个值得在本书中单独章节讨论的综合性主题。
因此,这一章是关于监督式机器学习算法的两个部分中的第一部分。首先,我们将介绍监督式机器学习的基本概念。接下来,我们将介绍两种监督式机器模型——分类器和回归器。为了展示分类器的能力,我们将首先提出一个真实世界的问题作为挑战。然后,我们将介绍六种不同的分类算法,用于解决这个问题。然后,我们将专注于回归算法,首先提出一个类似的问题,以便为回归器解决问题。接下来,我们将介绍三种回归算法,并使用它们来解决问题。最后,我们将比较结果,以帮助我们总结本章介绍的概念。
本章的总体目标是让您了解不同类型的监督式机器学习技术,并了解对于某些类别的问题,最佳的监督式机器学习技术是什么。
本章讨论了以下概念:
-
理解监督式机器学习
-
理解分类算法
-
评估分类器性能的方法
-
理解回归算法
-
评估回归算法性能的方法
让我们从理解监督式机器学习背后的基本概念开始。
理解监督式机器学习
机器学习专注于使用数据驱动的方法来创建可以帮助我们做出决策的自主系统,无论是否有人类监督。为了创建这些自主系统,机器学习使用一组算法和方法来发现和制定数据中可重复的模式。在机器学习中最流行和强大的方法之一是监督式机器学习方法。在监督式机器学习中,算法被给定一组输入,称为特征,以及它们对应的输出,称为目标 变量。使用给定的数据集,监督式机器学习算法用于训练一个捕捉特征和目标变量之间复杂关系的模型,该关系由数学公式表示。这个训练好的模型是用于预测的基本工具。
通过训练模型,通过生成未知特征集的目标变量来进行预测。
在监督学习中从现有数据中学习的能力类似于人脑从经验中学习的能力。监督学习中的这种学习能力使用了人脑的一个属性,是将决策能力和智能引入机器的基本途径。
让我们考虑一个例子,我们想要使用监督式机器学习技术训练一个模型,可以将一组电子邮件分类为合法邮件(称为合法)和不需要的邮件(称为垃圾邮件)。首先,为了开始,我们需要过去的例子,这样机器才能学习应该将什么样的电子邮件内容分类为垃圾邮件。这种基于内容的文本数据学习任务是一个复杂的过程,可以通过监督式机器学习算法之一来实现。在这个例子中,可以用来训练模型的一些监督式机器学习算法包括决策树和朴素贝叶斯分类器,我们将在本章后面讨论。
制定监督式机器学习
在深入研究监督式机器学习算法的细节之前,让我们定义一些基本的监督式机器学习术语:
| 术语 | 解释 |
|---|---|
| 目标变量 | 目标变量是我们希望模型预测的变量。在监督式机器学习模型中只能有一个目标变量。 |
| 标签 | 如果我们想要预测的目标变量是一个类别变量,那么它被称为标签。 |
| 特征 | 用于预测标签的一组输入变量称为特征。 |
| 特征工程 | 将特征转换为所选监督式机器学习算法准备的过程称为特征工程。 |
| 特征向量 | 在将输入提供给监督式机器学习算法之前,所有特征都被组合在一个称为特征向量的数据结构中。 |
| 历史数据 | 用于制定目标变量和特征之间关系的过去数据称为历史数据。历史数据带有示例。 |
| 训练/测试数据 | 历史数据与示例被分成两部分——一个更大的数据集称为训练数据,一个较小的数据集称为测试数据。 |
| 模型 | 目标变量和特征之间关系的最佳捕捉模式的数学表达。 |
| 训练 | 使用训练数据创建模型。 |
| 测试 | 使用测试数据评估训练模型的质量。 |
| 预测 | 使用模型预测目标变量。 |
经过训练的监督式机器学习模型能够通过估计特征来预测目标变量。
让我们介绍一下本章中将使用的符号,讨论机器学习技术:
| 变量 | 含义 |
|---|---|
| y | 实际标签 |
| ý | 预测标签 |
| d | 总示例数量 |
| b | 训练示例的数量 |
| c | 测试示例的数量 |
现在,让我们看看一些这些术语如何在实际中被制定。
正如我们讨论的,特征向量被定义为一个包含所有特征的数据结构。
如果特征的数量是n,训练示例的数量是b,那么X_train表示训练特征向量。每个示例都是特征向量中的一行。
对于训练数据集,特征向量由X_train表示。如果训练数据集中有b个示例,那么X_train将有b行。如果训练数据集中有n个变量,那么它将有n列。因此,训练数据集将具有n x b的维度,如下图所示:

现在,让我们假设有b个训练示例和c个测试示例。一个特定的训练示例由(X, y)表示。
我们使用上标来指示训练集中的每个训练示例。
因此,我们的标记数据集由 D = {X((1)),y((1))), (X((2)),y((2))), ..... , (X((d)),y((d)))}表示。
我们将其分为两部分——D[train]和 D[test]。
因此,我们的训练集可以用 D[train] = {X((1)),y((1))), (X((2)),y((2))), ..... , (X((b)),y((b)))}来表示。
训练模型的目标是对于训练集中的任何第 i 个示例,目标值的预测值应尽可能接*示例中的实际值。换句话说,
。
因此,我们的测试集可以用 D[test] = {X((1)),y((1))), (X((2)),y((2))), ..... , (X((c)),y((c)))}来表示。
目标变量的值由向量Y表示:
Y = {y^((1)), y^((2)), ....., y^((m))}
理解启用条件
监督式机器学习是基于算法使用示例来训练模型的能力。监督式机器学习算法需要满足一定的启用条件才能执行。这些启用条件如下:
-
足够的示例:监督式机器学习算法需要足够的示例来训练模型。
-
历史数据中的模式:用于训练模型的示例需要具有其中的模式。我们感兴趣事件的发生可能性应取决于模式、趋势和事件的组合。如果没有这些,我们处理的是无法用于训练模型的随机数据。
-
有效的假设:当我们使用示例训练监督式机器学习模型时,我们期望适用于示例的假设在未来也是有效的。让我们看一个实际的例子。如果我们想要为政府训练一个可以预测学生是否会获得签证的机器学习模型,那么理解是在模型用于预测时,法律和政策不会发生变化。如果在训练模型后实施了新的政策或法律,可能需要重新训练模型以纳入这些新信息。
区分分类器和回归器
在机器学习模型中,目标变量可以是类别变量或连续变量。目标变量的类型决定了我们拥有的监督式机器学习模型的类型。基本上,我们有两种类型的监督式机器学习模型:
-
分类器:如果目标变量是类别变量,则机器学习模型称为分类器。分类器可用于回答以下类型的业务问题:
-
这种异常组织生长是否是恶性肿瘤?
-
根据当前的天气条件,明天会下雨吗?
-
基于特定申请人的资料,他们的抵押贷款申请是否应该被批准?
-
回归器:如果目标变量是连续变量,我们训练一个回归器。回归器可用于回答以下类型的业务问题:
-
根据当前的天气条件,明天会下多少雨?
-
具有给定特征的特定房屋的价格将是多少?
让我们更详细地看看分类器和回归器。
理解分类算法
在监督式机器学习中,如果目标变量是类别变量,则模型被归类为分类器:
-
目标变量称为标签。
-
历史数据称为标记数据。
-
需要预测标签的生产数据称为未标记数据。
使用训练模型准确标记未标记数据的能力是分类算法的真正力量。分类器预测未标记数据的标签以回答特定的业务问题。
在我们介绍分类算法的细节之前,让我们首先提出一个业务问题,作为分类器的挑战。然后我们将使用六种不同的算法来回答相同的挑战,这将帮助我们比较它们的方法、途径和性能。
提出分类器挑战
我们将首先提出一个常见的问题,我们将使用它作为测试六种不同分类算法的挑战。这个常见的问题在本章中被称为分类器挑战。使用所有六种分类器来解决同一个问题将帮助我们以两种方式:
-
所有输入变量都需要被处理和组装成一个复杂的数据结构,称为特征向量。使用相同的特征向量可以帮助我们避免为所有六个算法重复数据准备。
-
我们可以通过使用相同的特征向量作为输入来比较各种算法的性能。
分类器挑战是关于预测一个人购买的可能性。在零售行业,可以帮助最大化销售的一件事是更好地了解客户的行为。这可以通过分析历史数据中发现的模式来实现。让我们先阐述问题。
问题陈述
根据历史数据,我们能否训练一个二元分类器,可以预测特定用户最终是否会购买产品?
首先,让我们探索可用于解决这个问题的历史标记数据集:
x € ℜ^b, y € {0,1}
对于特定示例,当y = 1 时,我们称之为正类,当y = 0 时,我们称之为负类。
尽管正类和负类的级别可以任意选择,但定义正类为感兴趣的事件是一个好的做法。如果我们试图为银行标记欺诈交易,那么正类(即y = 1)应该是欺诈交易,而不是相反。
现在,让我们来看一下以下内容:
-
实际标签,用y表示
-
预测的标签,用y`表示
请注意,对于我们的分类器挑战,示例中找到的标签的实际值由y表示。如果在我们的示例中,有人购买了一个物品,我们说y = 1。预测值由y`表示。输入特征向量x的维度为 4。我们想确定用户在给定特定输入时购买的概率是多少。
因此,我们希望确定在给定特征向量x的特定值时y = 1 的概率。从数学上讲,我们可以表示如下:

现在,让我们看看如何处理和组装特征向量x中的不同输入变量。在下一节中,将更详细地讨论使用处理管道组装x的不同部分的方法。
使用数据处理管道进行特征工程
为了选择一个特定的机器学习算法的数据准备被称为特征工程,它是机器学习生命周期的一个关键部分。特征工程在不同的阶段或阶段进行。用于处理数据的多阶段处理代码被统称为数据管道。在可能的情况下使用标准处理步骤制作数据管道,使其可重用并减少训练模型所需的工作量。通过使用更多经过测试的软件模块,代码的质量也得到了提高。
让我们为分类器挑战设计一个可重用的处理管道。如前所述,我们将准备数据一次,然后将其用于所有分类器。
导入数据
这个问题的历史数据存储在一个名为dataset的文件中,格式为.csv。我们将使用 pandas 的pd.read_csv函数将数据导入为数据框:
dataset = pd.read_csv('Social_Network_Ads.csv')
特征选择
选择与我们想要解决的问题相关的特征的过程称为特征选择。这是特征工程的一个重要部分。
一旦文件被导入,我们删除User ID列,该列用于识别一个人,并且在训练模型时应该被排除:
dataset = dataset.drop(columns=['User ID'])
现在让我们预览数据集:
dataset.head(5)
数据集如下:

现在,让我们看看如何进一步处理输入数据集。
独热编码
许多机器学习算法要求所有特征都是连续变量。这意味着如果一些特征是类别变量,我们需要找到一种策略将它们转换为连续变量。独热编码是执行这种转换的最有效方式之一。对于这个特定的问题,我们唯一的类别变量是Gender。让我们使用独热编码将其转换为连续变量:
enc = sklearn.preprocessing.OneHotEncoder()
enc.fit(dataset.iloc[:,[0]])
onehotlabels = enc.transform(dataset.iloc[:,[0]]).toarray()
genders = pd.DataFrame({'Female': onehotlabels[:, 0], 'Male': onehotlabels[:, 1]})
result = pd.concat([genders,dataset.iloc[:,1:]], axis=1, sort=False)
result.head(5)
一旦转换完成,让我们再次查看数据集:

请注意,为了将变量从类别变量转换为连续变量,独热编码已将Gender转换为两个单独的列——Male和Female。
指定特征和标签
让我们指定特征和标签。我们将使用y来代表标签,X代表特征集:
y=result['Purchased']
X=result.drop(columns=['Purchased'])
X代表特征向量,包含我们需要用来训练模型的所有输入变量。
将数据集分为测试和训练部分
现在,让我们使用sklearn.model_selection import train_test_split将训练数据集分为 25%的测试部分和 75%的训练部分:
#from sklearn.cross_validation import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.25, random_state = 0)
这已经创建了以下四个数据结构:
-
X_train:包含训练数据特征的数据结构 -
X_test:包含训练测试特征的数据结构 -
y_train:包含训练数据集中标签值的向量 -
y_test:包含测试数据集中标签值的向量
缩放特征
对于许多机器学习算法,将变量从0到1进行缩放是一个好的做法。这也被称为特征归一化。让我们应用缩放转换来实现这一点:
from sklearn.preprocessing import StandardScaler
sc = StandardScaler()
X_train = sc.fit_transform(X_train)
X_test = sc.transform(X_test)
在我们缩放数据之后,它准备好作为输入用于我们将在后续部分中介绍的不同分类器。
评估分类器
模型训练完成后,我们需要评估其性能。为此,我们将使用以下过程:
-
我们将标签数据集分为两部分——训练部分和测试部分。我们将使用测试部分来评估训练好的模型。
-
我们将使用测试部分的特征来为每一行生成标签。这是我们的预测标签集。
-
我们将比较预测标签集与实际标签以评估模型。
除非我们试图解决的问题非常琐碎,否则在评估模型时会有一些错误分类。我们如何解释这些错误分类以确定模型的质量取决于我们选择使用的性能指标。
一旦我们有了实际标签集和预测标签集,就可以使用一系列性能指标来评估模型。用于量化模型的最佳指标将取决于我们想要解决的业务问题的要求,以及训练数据集的特征。
混淆矩阵
混淆矩阵用于总结对分类器的评估结果。二元分类器的混淆矩阵如下所示:
如果我们正在训练的分类器的标签有两个级别,则称为二元分类器。监督机器学习的第一个关键用例,特别是二元分类器,是在第一次世界大战期间用于区分飞机和飞行鸟。
分类可以分为以下四类:
-
真正例(TP):正确分类的正分类
-
真负例(TN):正确分类的负分类
-
假正例(FP):实际上是负分类的正分类
-
假阴性(FN):实际上是积极的负面分类
让我们看看如何使用这四个类别来创建各种性能指标。
性能指标
性能指标用于量化训练模型的性能。基于此,让我们定义以下四个指标:
| 指标 | 公式 |
|---|---|
| 准确率 | ![]() |
| 召回率 | ![]() |
| 精度 | ![]() |
| F1 分数 | ![]() |
准确率是所有预测中正确分类的比例。在计算准确率时,我们不区分 TP 和 TN。通过准确率评估模型是直接的,但在某些情况下,它不起作用。
让我们看看我们需要更多的东西来量化模型的性能的情况。其中之一是当我们使用模型来预测罕见事件时,比如以下的例子:
-
一个用于预测银行交易数据库中欺诈交易的模型
-
一个用于预测飞机发动机零部件机械故障可能性的模型
在这两个例子中,我们试图预测罕见事件。在这种情况下,比准确率更重要的是召回率和精度。让我们逐个来看:
-
召回率:这计算了命中率。在前面的例子中,它是模型成功标记的欺诈文件占所有欺诈文件的比例。如果在我们的测试数据集中有 100 万笔交易,其中有 100 笔被确认为欺诈交易,模型能够识别出 78 笔。在这种情况下,召回率值将是 78/100。
-
精度:精度衡量了模型标记的交易中实际上是坏的交易有多少。我们不是专注于模型未能标记的坏交易,而是想确定模型标记的坏交易有多精确。
请注意,F1 分数将召回率和精度结合在一起。如果一个模型的精度和召回率都是完美的,那么它的 F1 分数将是完美的。高 F1 分数意味着我们训练了一个高质量的模型,具有高召回率和精度。
理解过拟合
如果一个机器学习模型在开发环境中表现出色,但在生产环境中明显下降,我们说这个模型是过拟合的。这意味着训练模型过于密切地遵循训练数据集。这表明模型创建的规则中有太多细节。模型方差和偏差之间的权衡最能捕捉到这个概念。让我们逐个来看这些概念。
偏差
任何机器学习模型都是基于某些假设进行训练的。一般来说,这些假设是对一些真实世界现象的简化*似。这些假设简化了特征和特征特性之间的实际关系,并使模型更容易训练。更多的假设意味着更多的偏差。因此,在训练模型时,更简化的假设=高偏差,更符合实际现象的现实假设=低偏差。
在线性回归中,忽略了特征的非线性,并将它们*似为线性变量。因此,线性回归模型天生容易表现出高偏差。
方差
方差量化了模型在使用不同数据集训练时对目标变量的估计准确性。它量化了我们的模型的数学公式是否是底层模式的良好概括。
基于特定情景和情况的特定过拟合规则=高方差,而基于广泛情景和情况的泛化规则=低方差。
我们在机器学习中的目标是训练表现出低偏差和低方差的模型。实现这一目标并不总是容易的,通常会让数据科学家夜不能寐。
偏差-方差权衡
在训练特定的机器学习模型时,很难确定训练模型所包含的规则的正确泛化级别。为了找到正确的泛化级别而进行的挣扎被称为偏差-方差权衡。
请注意,更简化的假设=更泛化=低方差=高方差。
偏差和方差之间的权衡是由算法的选择、数据的特征和各种超参数决定的。根据您尝试解决的具体问题的要求,重要的是在偏差和方差之间取得正确的折衷。
指定分类器的阶段
一旦标记的数据准备好,分类器的开发包括训练、评估和部署。在以下图表中,CRISP-DM(数据挖掘的跨行业标准流程)生命周期展示了实施分类器的这三个阶段(CRISP-DM 生命周期在第五章*,图形算法中有更详细的解释)

在实施分类器的前两个阶段——测试和训练阶段,我们使用标记的数据。标记的数据被分成两个分区——一个更大的分区称为训练数据,一个更小的分区称为测试数据。使用随机抽样技术将输入的标记数据分成训练和测试分区,以确保两个分区都包含一致的模式。请注意,如前图所示,首先是训练阶段,使用训练数据来训练模型。训练阶段结束后,使用测试数据评估训练模型。不同的性能指标用于量化训练模型的性能。评估模型后,我们有模型部署阶段,其中训练好的模型被部署并用于推理,通过标记未标记的数据解决现实世界的问题。
现在,让我们看一些分类算法。
我们将在接下来的部分中看到以下分类算法:
-
决策树算法
-
XGBoost 算法
-
随机森林算法
-
逻辑回归算法
-
支持向量机(SVM)算法
-
朴素贝叶斯算法
让我们从决策树算法开始。
决策树分类算法
决策树基于递归分区方法(分而治之),生成一组规则,可用于预测标签。它从根节点开始,分成多个分支。内部节点表示对某个属性的测试,测试的结果由分支到下一级表示。决策树以包含决策的叶节点结束。当分区不再改善结果时,过程停止。
理解决策树分类算法
决策树分类的显著特点是生成可解释的层次规则,用于在运行时预测标签。该算法具有递归性质。创建这些规则层次涉及以下步骤:
-
找到最重要的特征:在所有特征中,算法确定了最能区分训练数据集中数据点的特征。计算基于信息增益或基尼不纯度等指标。
-
分叉:使用最重要的特征,算法创建一个标准,用于将训练数据集分成两个分支:
-
通过满足标准的数据点
-
未通过标准的数据点
-
检查叶节点:如果任何结果分支大多包含一个类的标签,则该分支被确定为最终分支,形成一个叶节点。
-
检查停止条件并重复:如果未满足提供的停止条件,则算法将返回到步骤 1进行下一次迭代。否则,模型被标记为已训练,并且结果决策树的每个最低级节点都被标记为叶节点。停止条件可以简单地定义为迭代次数,或者可以使用默认的停止条件,即一旦每个叶节点达到一定的同质性水*,算法就会停止。
决策树算法可以用以下图解释:

在上图中,根节点包含一堆圆圈和十字。该算法创建了一个标准,试图将圆圈与十字分开。在每个级别,决策树创建数据的分区,预期从第 1 级开始越来越同质。完美的分类器只包含只包含圆圈或十字的叶节点。由于训练数据集固有的随机性,训练完美的分类器通常很困难。
使用决策树分类算法进行分类器挑战
现在,让我们使用决策树分类算法来解决我们之前定义的常见问题,预测客户最终是否购买产品:
- 首先,让我们实例化决策树分类算法,并使用我们为分类器准备的训练部分数据来训练模型:
classifier = sklearn.tree.DecisionTreeClassifier(criterion = 'entropy', random_state = 100, max_depth=2)
classifier.fit(X_train, y_train)
- 现在,让我们使用我们训练好的模型来预测我们标记数据的测试部分的标签。让我们生成一个可以总结我们训练好的模型性能的混淆矩阵:
import sklearn.metrics as metrics
y_pred = classifier.predict(X_test)
cm = metrics.confusion_matrix(y_test, y_pred)
cm
这给出了以下输出:

- 现在,让我们通过使用决策树分类算法来计算所创建分类器的
准确率、召回率和精确度值:
accuracy= metrics.accuracy_score(y_test,y_pred)
recall = metrics.recall_score(y_test,y_pred)
precision = metrics.precision_score(y_test,y_pred)
print(accuracy,recall,precision)
- 运行上述代码将产生以下输出:

性能指标帮助我们比较不同的训练建模技术。
决策树分类器的优势和劣势
在本节中,让我们看看使用决策树分类算法的优势和劣势。
优势
以下是决策树分类器的优势:
-
使用决策树算法创建的模型的规则可被人类解释。这样的模型被称为白盒模型。白盒模型是在需要追踪决策的细节和原因时的必要条件。这种透明性在我们想要防止偏见和保护脆弱社区的应用中至关重要。例如,在政府和保险行业的关键用例中,通常需要白盒模型。
-
决策树分类器旨在从离散问题空间中提取信息。这意味着大多数特征都是类别变量,因此使用决策树来训练模型是一个不错的选择。
劣势
以下是决策树分类器的弱点:
-
如果决策树分类器生成的树太深,规则会捕捉太多细节,导致过拟合的模型。在使用决策树算法时,我们需要意识到决策树容易过拟合,因此我们需要及时修剪树以防止这种情况。
-
决策树分类器的一个弱点是它们无法捕捉规则中的非线性关系。
用例
在本节中,让我们看看决策树算法用于哪些用例。
分类记录
决策树分类器可用于对数据点进行分类,例如以下示例:
-
抵押贷款申请:训练一个二元分类器,以确定申请人是否可能违约。
-
客户细分:将客户分类为高价值、中价值和低价值客户,以便为每个类别定制营销策略。
-
医学诊断:训练一个分类器,可以对良性或恶性生长进行分类。
-
治疗效果分析:训练一个分类器,可以标记对特定治疗产生积极反应的患者。
特征选择
决策树分类算法选择一小部分特征来创建规则。当特征数量很大时,可以使用该特征选择来选择另一个机器学习算法的特征。
理解集成方法
集成是一种机器学习方法,通过使用不同的参数创建多个略有不同的模型,然后将它们组合成一个聚合模型。为了创建有效的集成,我们需要找到我们的聚合标准,以生成最终模型。让我们看看一些集成算法。
使用 XGBoost 算法实现梯度提升
XGBoost 于 2014 年创建,基于梯度提升原理。它已成为最受欢迎的集成分类算法之一。它生成一堆相互关联的树,并使用梯度下降来最小化残差误差。这使其非常适合分布式基础设施,如 Apache Spark,或云计算,如 Google Cloud 或亚马逊网络服务(AWS)。
现在让我们看看如何使用 XGBoost 算法实现梯度提升:
- 首先,我们将实例化 XGBClassfier 分类器,并使用数据的训练部分来训练模型:

- 然后,我们将基于新训练的模型生成预测:
y_pred = classifier.predict(X_test)
cm = metrics.confusion_matrix(y_test, y_pred)
cm
产生以下输出:

- 最后,我们将量化模型的性能:
accuracy= metrics.accuracy_score(y_test,y_pred)
recall = metrics.recall_score(y_test,y_pred)
precision = metrics.precision_score(y_test,y_pred)
print(accuracy,recall,precision)
这给我们以下输出:

接下来,让我们看看随机森林算法。
使用随机森林算法
随机森林是一种集成方法,通过组合多个决策树来减少偏差和方差。
训练随机森林算法
在训练中,该算法从训练数据中获取N个样本,并创建我们整体数据的m个子集。这些子集是通过随机选择输入数据的一些行和列来创建的。该算法构建m个独立的决策树。这些分类树由C[1]到C[m]表示。
使用随机森林进行预测
模型训练完成后,可以用于标记新数据。每个个体树生成一个标签。最终预测由这些个体预测的投票决定,如下所示:

请注意,在上图中,训练了m棵树,表示为C[1]到C[m]。即树 = {C[1],..,C[m]}
每棵树生成一个由一组表示的预测:
个体预测 = P= {P[1],..., P[m]}
最终预测由P[f]表示。它由个体预测的大多数决定。mode函数可用于找到多数决定(mode是最常重复且处于多数的数字)。个体预测和最终预测如下所示:
P[f] = mode (P)
区分随机森林算法和集成提升
随机森林算法生成的每棵树都是完全独立的。它不知道集成中其他树的任何细节。这使它与其他技术有所不同,如集成增强。
使用随机森林算法进行分类器挑战
让我们实例化随机森林算法,并使用它来训练我们的模型使用训练数据。
这里有两个关键的超参数:
-
n_estimators -
max_depth
n_estimators超参数控制构建多少个独立的决策树,max_depth超参数控制每个独立决策树可以有多深。
换句话说,决策树可以不断分裂,直到它有一个节点代表训练集中的每个给定示例。通过设置max_depth,我们限制了它可以进行多少级别的分裂。这控制了模型的复杂性,并确定了它与训练数据的拟合程度。如果我们参考以下输出,n_estimators控制了随机森林模型的宽度,max_depth控制了模型的深度:

一旦随机森林模型训练好了,让我们用它进行预测:
y_pred = classifier.predict(X_test)
cm = metrics.confusion_matrix(y_test, y_pred)
cm
它的输出是:

现在,让我们量化我们的模型有多好:
accuracy= metrics.accuracy_score(y_test,y_pred)
recall = metrics.recall_score(y_test,y_pred)
precision = metrics.precision_score(y_test,y_pred)
print(accuracy,recall,precision)
我们将观察以下输出:

接下来,让我们来看看逻辑回归。
逻辑回归
逻辑回归是一种用于二元分类的分类算法。它使用逻辑函数来制定输入特征和目标变量之间的交互。它是用于建模二元因变量的最简单的分类技术之一。
假设
逻辑回归假设以下内容:
-
训练数据集没有缺失值。
-
标签是一个二进制类别变量。
-
标签是有序的,换句话说,是一个具有有序值的分类变量。
-
所有特征或输入变量彼此独立。
建立关系
对于逻辑回归,预测值计算如下:

假设
。
所以现在:

上述关系可以用图形表示如下:

注意,如果z很大,σ (z)将等于1。如果z非常小或非常负,σ (z)将等于0。因此,逻辑回归的目标是找到W和j的正确值。
逻辑回归是根据用于制定它的函数命名的,称为逻辑或Sigmoid 函数。
损失和成本函数
loss函数定义了我们想要量化训练数据中特定示例的错误的方式。cost函数定义了我们想要最小化整个训练数据集中的错误的方式。因此,loss函数用于训练数据集中的一个示例,cost函数用于量化实际值和预测值的整体偏差。它取决于w和h的选择。
逻辑回归中使用的loss函数如下:
Loss (ý^((i)), y^((i))) = - (y^((i))log ý((i))+(1-y((i)) ) log (1-ý^((i)))
注意当*y^((i)) = 1, Loss(ý^((i)), y^((i))**) = - logý((i))*.最小化损失将导致ý((i))的值很大。作为 Sigmoid 函数,最大值将是1。
如果y^((i)) = 0, Loss (ý^((i)), y^((i))) = - log (1-ý((i))**)*。最小化损失将导致*ý((i))尽可能小,即0。
逻辑回归的成本函数如下:

何时使用逻辑回归
逻辑回归在二元分类器方面表现出色。当数据量很大但数据质量不佳时,逻辑回归效果不佳。它可以捕捉不太复杂的关系。虽然它通常不会产生最佳性能,但它确实为起步设定了一个很好的基准。
使用逻辑回归算法进行分类器挑战
在本节中,我们将看到如何使用逻辑回归算法进行分类器挑战:
- 首先,让我们实例化一个逻辑回归模型,并使用训练数据对其进行训练:
from sklearn.linear_model import LogisticRegression
classifier = LogisticRegression(random_state = 0)
classifier.fit(X_train, y_train)
- 让我们预测
test数据的值并创建一个混淆矩阵:
y_pred = classifier.predict(X_test)
cm = metrics.confusion_matrix(y_test, y_pred)
cm
运行上述代码后,我们得到以下输出:

- 现在,让我们看看性能指标:
accuracy= metrics.accuracy_score(y_test,y_pred)
recall = metrics.recall_score(y_test,y_pred)
precision = metrics.precision_score(y_test,y_pred)
print(accuracy,recall,precision)
- 运行上述代码后,我们得到以下输出:

接下来,让我们看看SVM。
SVM 算法
现在,让我们看看 SVM。SVM 是一种找到最大化两个类之间间隔的最优超*面的分类器。在 SVM 中,我们的优化目标是最大化间隔。间隔被定义为分隔超*面(决策边界)与最靠*该超*面的训练样本之间的距离,称为支持向量。因此,让我们从一个只有两个维度X1和X2的非常基本的例子开始。我们希望有一条线将圆圈与十字分开。如下图所示:

我们画了两条线,都完美地将十字与圆圈分开。然而,必须有一个最佳线或决策边界,使我们有最佳机会正确分类大多数额外的例子。一个合理的选择可能是一条均匀分布在这两个类之间的线,为每个类提供一点缓冲,如下所示:

现在,让我们看看如何使用 SVM 来训练我们挑战的分类器。
使用 SVM 算法进行分类器挑战
- 首先,让我们实例化 SVM 分类器,然后使用标记数据的训练部分对其进行训练。
kernel超参数确定应用于输入数据的转换类型,以使其线性可分。
from sklearn.svm import SVC
classifier = SVC(kernel = 'linear', random_state = 0)
classifier.fit(X_train, y_train)
- 训练完成后,让我们生成一些预测并查看混淆矩阵:
y_pred = classifier.predict(X_test)
cm = metrics.confusion_matrix(y_test, y_pred)
cm
- 观察以下输出:

- 现在,让我们来看看各种性能指标:
accuracy= metrics.accuracy_score(y_test,y_pred)
recall = metrics.recall_score(y_test,y_pred)
precision = metrics.precision_score(y_test,y_pred)
print(accuracy,recall,precision)
运行上述代码后,我们得到以下值作为输出:

理解朴素贝叶斯算法
基于概率论,朴素贝叶斯是最简单的分类算法之一。如果使用正确,它可以得出准确的预测。朴素贝叶斯算法之所以被如此命名有两个原因:
-
它基于一个天真的假设,即特征和输入变量之间是独立的。
-
它基于贝叶斯定理。
该算法试图基于先前属性/实例的概率对实例进行分类,假设属性完全独立。
有三种类型的事件:
-
独立事件不会影响另一个事件发生的概率(例如,收到一封电子邮件提供免费参加科技活动的机会和公司进行重新组织)。
-
依赖事件会影响另一个事件发生的概率;也就是说,它们在某种程度上是相关的(例如,你准时参加会议的概率可能会受到航空公司员工罢工或航班不准时的影响)。
-
互斥事件不能同时发生(例如,单次掷骰子得到三和六的概率为 0——这两个结果是互斥的)。
贝叶斯定理
贝叶斯定理用于计算两个独立事件A和B之间的条件概率。事件A和B发生的概率由 P(A)和 P(B)表示。条件概率由 P(B|A)表示,这是事件A发生的条件概率,假设事件B已经发生:

计算概率
朴素贝叶斯基于概率基本原理。单个事件发生的概率(观察概率)是通过将事件发生的次数除以可能导致该事件发生的总进程次数来计算的。例如,呼叫中心每天接到 100 多个支持电话,一个月内有 50 次。您想知道基于以前的响应时间,呼叫在 3 分钟内得到响应的概率。如果呼叫中心在 27 次匹配这个时间记录,那么 100 次呼叫在 3 分钟内得到响应的观察概率如下:
- P(3 分钟内 100 个支持电话)=(27/50)= 0.54(54%)*
根据过去的 50 次记录,100 次呼叫大约有一半的时间可以在 3 分钟内得到响应。
AND 事件的乘法规则
要计算两个或更多事件同时发生的概率,请考虑事件是独立还是相关的。如果它们是独立的,则使用简单的乘法规则:
- P(结果 1 和结果 2)= P(结果 1)* P(结果 2)*
例如,要计算收到免费参加技术活动的电子邮件的概率和工作场所发生重新组织的概率,将使用这个简单的乘法规则。这两个事件是独立的,因为其中一个发生并不影响另一个发生的机会
如果收到技术活动的电子邮件的概率为 31%,并且员工重新组织的概率为 82%,则同时发生的概率如下计算:
P(电子邮件和重新组织)= P(电子邮件)* P(重新组织)=(0.31)*(0.82)= 0.2542(25%)
一般乘法规则
如果两个或更多事件是相关的,则使用一般乘法规则。这个公式实际上在独立和相关事件的情况下都是有效的:
- P(结果 1 和结果 2)= P(结果 1)* P(结果 2 | 结果 1)*
请注意,P(结果 2 | 结果 1)指的是结果 1已经发生的情况下结果 2发生的条件概率。该公式包含了事件之间的依赖关系。如果事件是独立的,那么条件概率是无关紧要的,因为一个结果不会影响另一个发生的机会,P(结果 2 | 结果 1)就是P(结果 2)。请注意,在这种情况下,该公式变成了简单的乘法规则。
OR 事件的加法规则
在计算两个事件中的一个或另一个发生的概率(互斥)时,使用以下简单的加法规则:
- P(结果 1 或结果 2)= P(结果 1)+ P(结果 2)*
例如,掷出 6 或 3 的概率是多少?要回答这个问题,首先注意到两个结果不能同时发生。掷出 6 的概率是(1/6),掷出 3 的概率也是如此:
- P(6 或 3)=(1/6)+(1/6)= 0.33(33%)*
如果事件不是互斥的并且可以同时发生,请使用以下一般加法公式,这在互斥和非互斥的情况下都是有效的:
- P(结果 1 或结果 2)= P(结果 1)+ P(结果 2)P(结果 1 和结果 2)*
使用朴素贝叶斯算法进行分类器挑战
现在,让我们使用朴素贝叶斯算法来解决分类器挑战:
- 首先,我们导入
GaussianNB()函数并用它来训练模型:
from sklearn.naive_bayes import GaussianNB
classifier = GaussianNB()
classifier.fit(X_train, y_train)
- 现在,让我们使用训练好的模型来预测结果。我们将用它来预测我们的测试分区
X_test的标签:
Predicting the Test set results
y_pred = classifier.predict(X_test)
cm = metrics.confusion_matrix(y_test, y_pred)
cm
- 现在,让我们打印混淆矩阵:

- 现在,让我们打印性能矩阵来量化我们训练模型的质量:
accuracy= metrics.accuracy_score(y_test,y_pred)
recall = metrics.recall_score(y_test,y_pred)
precision = metrics.precision_score(y_test,y_pred)
print(accuracy,recall,precision)
这将产生以下输出:

对于分类算法,获胜者是...
让我们看一下我们提出的各种算法的性能指标。这在下表中总结如下:
| 算法 | 准确度 | 召回率 | 精确度 |
|---|---|---|---|
| 决策树 | 0.94 | 0.93 | 0.88 |
| XGBoost | 0.93 | 0.90 | 0.87 |
| 随机森林 | 0.93 | 0.90 | 0.87 |
| 逻辑回归 | 0.91 | 0.81 | 0.89 |
| 支持向量机 | 0.89 | 0.71 | 0.92 |
| 朴素贝叶斯 | 0.92 | 0.81 | 0.92 |
从前面的表中可以看出,决策树分类器在准确性和召回率方面表现最佳。如果我们寻求精确度,那么支持向量机和朴素贝叶斯之间存在*局,因此任何一个都适用于我们。
了解回归算法
监督机器学习模型使用回归算法之一,如果目标变量是连续变量。在这种情况下,机器学习模型被称为回归器。
在本节中,我们将介绍各种可用于训练监督机器学习回归模型的算法,或者简单地说,回归器。在我们深入了解算法的细节之前,让我们首先为这些算法创建一个挑战,以测试它们的性能、能力和有效性。
呈现回归器挑战
与分类算法使用的方法类似,我们将首先提出一个问题,作为所有回归算法的挑战来解决。我们将把这个共同的问题称为回归器挑战。然后,我们将使用三种不同的回归算法来解决这个挑战。使用一个共同的挑战来测试不同的回归算法有两个好处:
-
我们可以准备一次数据,然后在所有三个回归算法上使用准备好的数据。
-
我们可以以有意义的方式比较三种回归算法的性能,因为我们将使用它们来解决同一个问题。
让我们看一下挑战的问题陈述。
回归器挑战的问题陈述
预测不同车辆的里程数在当今是很重要的。高效的车辆对环境有益,也具有成本效益。里程数可以根据发动机功率和车辆特性来估算。让我们为回归器创建一个挑战,训练一个能够根据车辆特性预测车辆的每加仑英里数(MPG)的模型。
让我们看看我们将用来训练回归器的历史数据集。
探索历史数据集
以下是我们拥有的历史数据集数据的特征:
| 名称 | 类型 | 描述 |
|---|---|---|
名称 |
类别 | 标识特定车辆 |
CYLINDERS |
连续 | 气缸数量(4 至 8 之间) |
DISPLACEMENT |
连续 | 发动机排量(立方英寸) |
HORSEPOWER |
连续 | 发动机马力 |
ACCELERATION |
连续 | 从 0 到 60 英里/小时的加速时间(秒) |
这个问题的目标变量是一个连续变量,MPG,它指定了每辆车的英里数。
让我们首先为这个问题设计数据处理管道。
使用数据处理管道进行特征工程
让我们看看如何设计一个可重复使用的处理管道来解决回归器挑战。如前所述,我们将一次准备数据,然后在所有回归算法中使用它。让我们按照以下步骤进行:
- 我们首先导入数据集,如下所示:
dataset = pd.read_csv('auto.csv')
- 现在让我们预览数据集:
dataset.head(5)
数据集将如下所示:

- 现在,让我们继续进行特征选择。让我们删除
NAME列,因为它只是一个用于汽车的标识符。用于识别数据集中行的列对于训练模型是不相关的。让我们删除这一列:
dataset=dataset.drop(columns=['NAME'])
- 让我们转换所有的输入变量并填充所有的空值:
dataset=dataset.drop(columns=['NAME'])
dataset= dataset.apply(pd.to_numeric, errors='coerce')
dataset.fillna(0, inplace=True)
填充提高了数据的质量,并准备好用于训练模型。现在,让我们看最后一步:
- 让我们将数据分成测试和训练分区:
from sklearn.model_selection import train_test_split
#from sklearn.cross_validation import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.25, random_state = 0)
这创建了以下四个数据结构:
-
X_train:包含训练数据的特征的数据结构 -
X_test:包含训练测试的特征的数据结构 -
y_train:包含训练数据集中标签的值的向量 -
y_test:包含测试数据集中标签的值的向量
现在,让我们使用准备好的数据在三个不同的回归器上,以便比较它们的性能。
线性回归
在所有监督学习技术中,线性回归算法是最容易理解的。我们首先看一下简单线性回归,然后将概念扩展到多元线性回归。
简单线性回归
在其最简单的形式中,线性回归阐述了单个连续自变量和单个连续自变量之间的关系。回归用于显示因变量(显示在 y 轴上)的变化程度可以归因于解释变量(显示在 x 轴上)的变化程度。它可以表示如下:

这个公式可以解释如下:
-
y 是因变量。
-
X 是自变量。
-
是斜率,表示每增加一个 X,线就上升多少。 -
α 是截距,表示 X = 0 时 y 的值。
单个连续因变量和单个连续自变量之间关系的一些例子如下:
-
一个人的体重和他们的卡路里摄入量
-
特定社区房屋价格和面积
-
空气中的湿度和下雨的可能性
对于线性回归,输入(自变量)和目标(因变量)变量都必须是数值型的。最佳关系是通过最小化每个点到通过所有点的线的垂直距离的*方和来找到的。假设预测变量和目标变量之间是线性关系。例如,投入研发的资金越多,销售额就越高。
让我们看一个具体的例子。让我们尝试阐明特定产品的营销支出和销售之间的关系。它们被发现直接相关。营销支出和销售在二维图上绘制,并显示为蓝色的钻石。这种关系最好通过绘制一条直线来*似,如下图所示:

一旦画出线性线,我们就可以看到营销支出和销售之间的数学关系。
评估回归器
我们画的线性线是因变量和自变量之间关系的*似值。即使最佳线也会与实际值有一些偏差,如下所示:

评估线性回归模型性能的一种典型方法是使用均方根误差(RMSE)。这通过数学计算训练模型产生的误差的标准偏差。对于训练数据集中的某个示例,loss 函数计算如下:
损失(ý^((i)), y^((i))) = 1/2(ý^((i)-) y^((i)))²
这导致以下cost函数,最小化训练集中所有示例的损失:

让我们尝试解释 RMSE。如果我们的示例模型的 RMSE 为$50,这意味着大约 68.2%的预测值将在真实值(即α)的$50 范围内。这也意味着 95%的预测值将在实际值的$100(即 2α)范围内。最后,99.7%的预测值将在实际值的$150 范围内。
多元回归
事实上,大多数现实世界的分析都有多个自变量。多元回归是简单线性回归的扩展。关键区别在于额外的预测变量有额外的 beta 系数。在训练模型时,目标是找到最小化线性方程误差的 beta 系数。让我们尝试数学上阐述因变量和一组自变量(特征)之间的关系。
与简单线性方程类似,因变量y被量化为截距项的总和,加上β系数乘以每个i特征的x值:
y = α + β [1] x [1] + β [2] x 2 +...+ β [i] x [i] + ε
误差用ε表示,表明预测并不完美。
β系数允许每个特征对y的值有单独的估计影响,因为y每增加一个单位的x[i],y的变化量为β [i]。此外,截距(α)表示当独立变量都为 0 时y的期望值。
请注意,前述方程中的所有变量都可以用一堆向量表示。目标和预测变量现在是带有行的向量,而回归系数β和误差ε也是向量。
使用线性回归算法进行回归挑战
现在,让我们使用数据集的训练部分来训练模型:
- 让我们从导入线性回归包开始:
from sklearn.linear_model import LinearRegression
- 然后,让我们实例化线性回归模型,并使用训练数据集对其进行训练:
regressor = LinearRegression()
regressor.fit(X_train, y_train)
- 现在,让我们使用数据集的测试部分来预测结果:
y_pred = regressor.predict(X_test)
from sklearn.metrics import mean_squared_error
from math import sqrt
sqrt(mean_squared_error(y_test, y_pred))
- 运行上述代码生成的输出将生成以下内容:

如前一节所讨论的,RMSE 是误差的标准差。它表明 68.2%的预测值将在目标变量值的4.36范围内。
何时使用线性回归?
线性回归用于解决许多现实世界的问题,包括以下内容:
-
销售预测
-
预测最佳产品价格
-
量化事件和响应之间的因果关系,例如临床药物试验、工程安全测试或市场研究
-
识别可用于预测未来行为的模式,给定已知条件,例如预测保险索赔、自然灾害损失、选举结果和犯罪率
线性回归的弱点
线性回归的弱点如下:
-
它只适用于数值特征。
-
分类数据需要进行预处理。
-
它无法很好地处理缺失数据。
-
它对数据做出假设。
回归树算法
回归树算法类似于分类树算法,只是目标变量是连续变量,而不是类别变量。
使用回归树算法进行回归挑战
在本节中,我们将看到如何使用回归树算法进行回归挑战:
- 首先,我们使用回归树算法训练模型:

- 一旦回归树模型训练完成,我们就可以使用训练好的模型来预测值:
y_pred = regressor.predict(X_test)
- 然后,我们计算 RMSE 来量化模型的性能:
from sklearn.metrics import mean_squared_error
from math import sqrt
sqrt(mean_squared_error(y_test, y_pred))
我们得到以下输出:

梯度提升回归算法
现在,让我们来看看梯度提升回归算法。它使用一组决策树来更好地表达数据中的潜在模式。
使用梯度提升回归算法来解决回归问题
在这一部分,我们将看到如何使用梯度提升回归算法来解决回归问题:
- 首先,我们使用梯度提升回归算法来训练模型:

- 一旦梯度回归算法模型被训练,我们就可以用它来预测数值:
y_pred = regressor.predict(X_test)
- 最后,我们计算 RMSE 来量化模型的性能:
from sklearn.metrics import mean_squared_error
from math import sqrt
sqrt(mean_squared_error(y_test, y_pred))
- 运行这个将给我们输出值,如下所示:

对于回归算法,获胜者是...
让我们来看看我们在相同数据和完全相同用例上使用的三种回归算法的表现:
| 算法 | RMSE |
|---|---|
| 线性回归 | 4.36214129677179 |
| 回归树 | 5.2771702288377 |
| 梯度提升回归 | 4.034836373089085 |
从所有回归算法的表现来看,很明显梯度提升回归的表现最好,因为它具有最低的 RMSE。其次是线性回归。对于这个问题,回归树算法的表现最差。
实际例子 - 如何预测天气
让我们看看如何使用本章中开发的概念来预测天气。假设我们想根据一年内针对特定城市收集的数据来预测明天是否会下雨。
用于训练该模型的数据在名为weather.csv的 CSV 文件中:
- 让我们将数据导入为一个 pandas 数据框:
import numpy as np
import pandas as pd
df = pd.read_csv("weather.csv")
- 让我们来看看数据框的列:

- 接下来,让我们来看一下
weather.csv数据的前 13 列的标题:

- 现在,让我们来看一下
weather.csv数据的最后 10 列:

- 让我们用
x来代表输入特征。我们将在特征列表中删除Date字段,因为在预测的情境下它没有用处。我们还将删除RainTomorrow标签:
x = df.drop(['Date','RainTomorrow'],axis=1)
- 让我们用
y来代表标签:
y = df['RainTomorrow']
- 现在,让我们将数据分成
train_test_split:
from sklearn.model_selection import train_test_split
train_x , train_y ,test_x , test_y = train_test_split(x,y , test_size = 0.2,random_state = 2)
- 由于标签是一个二元变量,我们正在训练一个分类器。因此,在这里逻辑回归将是一个不错的选择。首先,让我们实例化逻辑回归模型:
model = LogisticRegression()
- 现在,我们可以使用
train_x和test_x来训练模型:
model.fit(train_x , test_x)
- 一旦模型被训练,让我们用它进行预测:
predict = model.predict(train_y)
- 现在,让我们找出我们训练模型的准确性:

现在,这个二元分类器可以用来预测明天是否会下雨。
摘要
在本章中,我们首先了解了监督式机器学习的基础知识。然后,我们更详细地了解了各种分类算法。接下来,我们研究了评估分类器性能的不同方法,并研究了各种回归算法。我们还研究了用于评估我们研究的算法性能的不同方法。
在下一章中,我们将研究神经网络和深度学习算法。我们将研究训练神经网络所使用的方法,还将研究用于评估和部署神经网络的各种工具和框架。
第八章:神经网络算法
各种因素的结合使得人工神经网络(ANNs)成为当今最重要的机器学习技术之一。这些因素包括解决日益复杂的问题的需求,数据的爆炸以及诸如可用的廉价集群等技术的出现,这些技术提供了设计非常复杂算法所需的计算能力。
事实上,这是一个迅速发展的研究领域,负责实现机器人技术、自然语言处理和自动驾驶汽车等领先技术领域所宣称的大部分重大进展。
观察 ANN 的结构,其基本单元是神经元。ANN 的真正优势在于其能够通过将它们组织成分层架构来利用多个神经元的力量。ANN 通过在不同层中将神经元链接在一起来创建分层架构。信号通过这些层传递,并在每个层中以不同的方式进行处理,直到生成最终所需的输出。正如我们将在本章中看到的,ANN 使用的隐藏层充当抽象层,实现了深度学习,这在实现强大的应用程序中被广泛使用,如亚马逊的 Alexa,谷歌的图像搜索和谷歌相册。
本章首先介绍了典型神经网络的主要概念和组件。然后,它介绍了各种类型的神经网络,并解释了这些神经网络中使用的不同类型的激活函数。接下来,详细讨论了反向传播算法,这是训练神经网络最广泛使用的算法。然后,解释了迁移学习技术,可以用来大大简化和部分自动化模型的训练。最后,通过一个真实世界的应用程序案例,探讨了如何使用深度学习来标记欺诈文件。
本章讨论的主要概念如下:
-
理解人工神经网络
-
ANN 的演变
-
训练神经网络
-
工具和框架
-
迁移学习
-
案例研究:使用深度学习进行欺诈检测
让我们从人工神经网络的基础知识开始。
理解人工神经网络
受人脑神经元工作的启发,神经网络的概念是由 Frank Rosenblatt 在 1957 年提出的。要充分理解其架构,有助于简要了解人脑中神经元的分层结构。(参考以下图表,了解人脑中的神经元是如何连接在一起的。)
在人脑中,树突充当检测信号的传感器。然后,信号传递给轴突,轴突是神经细胞的一种长而细的突出部分。轴突的功能是将这个信号传递给肌肉、腺体和其他神经元。如下图所示,信号通过称为突触的相互连接的组织传递,然后传递给其他神经元。请注意,通过这种有机管道,信号一直传递,直到它到达目标肌肉或腺体,引起所需的动作。信号通常需要七到八毫秒才能通过神经元链传递并到达目标位置:

受到这种自然的信号处理的建筑杰作的启发,Frank Rosenblatt 设计了一种意味着数字信息可以在层中处理以解决复杂数学问题的技术。他最初设计的神经网络非常简单,看起来类似于线性回归模型。这种简单的神经网络没有任何隐藏层,被命名为感知器。以下图表对其进行了说明:

让我们尝试开发这个感知器的数学表示。在前面的图表中,输入信号显示在左侧。这是输入的加权和,因为每个输入(x[1],x[2]..x[n])都会乘以相应的权重(w[1],w[2]… w[n]),然后求和:

请注意,这是一个二元分类器,因为这个感知器的最终输出取决于聚合器的输出是真还是假(在图表中显示为∑)。如果聚合器能够从至少一个输入中检测到有效信号,它将产生一个真实的信号。
现在让我们来看看神经网络是如何随着时间的推移而发展的。
人工神经网络的演变
在前面的部分中,我们研究了一个没有任何层的简单神经网络,称为感知器。发现感知器有严重的局限性,并且在 1969 年,马文·明斯基和西摩·帕帕特进行了研究,得出结论感知器无法学习任何复杂的逻辑。
事实上,他们表明即使学习像异或这样简单的逻辑函数也是一种挑战。这导致了对机器学习和神经网络的兴趣下降,开始了一个现在被称为AI 寒冬的时代。世界各地的研究人员不会认真对待人工智能,认为它无法解决任何复杂的问题。
所谓的 AI 寒冬的主要原因之一是当时可用的硬件能力的限制。要么是必要的计算能力不可用,要么是价格昂贵。到了 20 世纪 90 年代末,分布式计算的进步提供了易于获得和负担得起的基础设施,这导致了 AI 寒冬的融化。这种融化重新激发了对人工智能的研究。最终导致了将当前时代变成一个可以称为AI 春天的时代,人们对人工智能和神经网络特别感兴趣。
对于更复杂的问题,研究人员开发了一个称为多层感知器的多层神经网络。多层神经网络有几个不同的层,如下图所示。这些层包括:
-
输入层
-
隐藏层
-
输出层:
深度神经网络是一个具有一个或多个隐藏层的神经网络。深度学习是训练人工神经网络的过程。
一个重要的事情要注意的是神经元是这个网络的基本单元,每个层的神经元都连接到下一层的所有神经元。对于复杂的网络,这些互连的数量激增,我们将探索不牺牲太多质量的不同减少这些互连的方法。
首先,让我们尝试阐明我们要解决的问题。
输入是一个特征向量x,维度为n。
我们希望神经网络能够预测值。预测值由ý表示。
从数学上讲,我们想要确定在给定特定输入的情况下,交易是欺诈的概率。换句话说,给定特定的x值,y=1 的概率是多少?从数学上讲,我们可以表示如下:

注意,x是一个n[x]维向量,其中n[x]是输入变量的数量。
这个神经网络有四层。输入和输出之间的层是隐藏层。第一个隐藏层中的神经元数量用
表示。各个节点之间的连接由称为权重的参数相乘。训练神经网络就是找到权重的正确值。
让我们看看如何训练神经网络。
训练神经网络
使用给定数据集构建神经网络的过程称为训练神经网络。让我们来看一下典型神经网络的解剖结构。当我们谈论训练神经网络时,我们谈论计算权重的最佳值。训练是通过使用一组示例(训练数据的形式)进行迭代完成的。训练数据中的示例具有不同输入值组合的输出的预期值。神经网络的训练过程与传统模型的训练方式不同(这些在第七章中讨论过,传统监督学习算法)。
理解神经网络的解剖结构
让我们看看神经网络包括什么:
-
层: 层是神经网络的核心构建模块。每一层都是一个数据处理模块,充当过滤器。它接受一个或多个输入,以某种方式处理它,然后产生一个或多个输出。每当数据通过一层时,它都会经过一个处理阶段,并显示与我们试图回答的业务问题相关的模式。
-
损失函数: 损失函数提供了在学习过程的各个迭代中使用的反馈信号。损失函数提供了单个示例的偏差。
-
成本函数: 成本函数是完整示例集上的损失函数。
-
优化器: 优化器确定损失函数提供的反馈信号将如何被解释。
-
输入数据: 输入数据是用于训练神经网络的数据。它指定目标变量。
-
权重: 权重是通过训练网络计算的。权重大致对应于每个输入的重要性。例如,如果特定输入比其他输入更重要,在训练后,它将被赋予更大的权重值,充当乘数。即使对于该重要输入的弱信号也将从大的权重值(充当乘数)中获得力量。因此,权重最终会根据它们的重要性转换每个输入。
-
激活函数: 值将由不同的权重相乘然后聚合。它们将如何被聚合以及它们的值将如何被解释将由所选择的激活函数的类型确定。
现在让我们来看看神经网络训练的一个非常重要的方面。
在训练神经网络时,我们逐个示例地进行。对于每个示例,我们使用正在训练的模型生成输出。我们计算期望输出和预测输出之间的差异。对于每个单独的示例,这种差异称为损失。在整个训练数据集中,损失被称为成本。随着我们不断训练模型,我们的目标是找到导致最小损失值的权重值。在整个训练过程中,我们不断调整权重的值,直到找到导致最小可能总成本的权重值集。一旦达到最小成本,我们标记模型已经训练完成。
定义梯度下降
训练神经网络模型的目的是找到权重的正确值。我们开始训练神经网络时,权重使用随机或默认值。然后,我们迭代使用优化算法,例如梯度下降,以改变权重,使我们的预测改善。
梯度下降算法的起点是需要通过算法迭代优化的随机权重值。在随后的每次迭代中,算法通过以最小化成本的方式改变权重值来进行。
以下图解释了梯度下降算法的逻辑:

在前面的图中,输入是特征向量X。目标变量的实际值是Y,目标变量的预测值是Y'。我们确定实际值与预测值的偏差。我们更新权重并重复这些步骤,直到成本最小化。
在算法的每次迭代中如何改变权重将取决于以下两个因素:
-
方向: 要走的方向以获得损失函数的最小值
-
学习率: 我们选择的方向中变化应该有多大
下面的图表显示了一个简单的迭代过程:

该图显示了通过改变权重,梯度下降试图找到最小成本。学习率和选择的方向将决定要探索的图表上的下一个点。
选择正确的学习率很重要。如果学习率太小,问题可能需要很长时间才能收敛。如果学习率太高,问题将无法收敛。在前面的图中,代表当前解决方案的点将在图表的两条相反线之间不断振荡。
现在,让我们看看如何最小化梯度。只考虑两个变量,x和y。x和y的梯度计算如下:

为了最小化梯度,可以使用以下方法:
while(gradient!=0):
if (gradient < 0); move right
if (gradient > 0); move left
该算法也可以用于找到神经网络的最优或接*最优的权重值。
请注意,梯度下降的计算是在整个网络中向后进行的。我们首先计算最后一层的梯度,然后是倒数第二层,然后是倒数第二层之前的层,直到达到第一层。这被称为反向传播,由 Hinton、Williams 和 Rumelhart 于 1985 年引入。
接下来,让我们看看激活函数。
激活函数
激活函数规定了特定神经元的输入如何被处理以生成输出。
如下图所示,神经网络中的每个神经元都有一个激活函数,确定输入将如何被处理:

在前面的图中,我们可以看到激活函数生成的结果传递到输出。激活函数设置了如何解释输入值以生成输出的标准。
对于完全相同的输入值,不同的激活函数将产生不同的输出。在使用神经网络解决问题时,了解如何选择正确的激活函数是很重要的。
现在让我们逐个查看这些激活函数。
阈值函数
最简单的激活函数是阈值函数。阈值函数的输出是二进制的:0 或 1。如果任何输入大于 1,它将生成 1 作为输出。这可以在下图中解释:

请注意,一旦检测到输入加权和中有任何生命迹象,输出(y)就变为 1。这使得阈值激活函数非常敏感。它很容易被输入中的最轻微的信号或噪音错误触发。
Sigmoid
Sigmoid 函数可以被认为是阈值函数的改进。在这里,我们可以控制激活函数的灵敏度。

Sigmoid 函数y定义如下:

可以在 Python 中实现如下:
def sigmoidFunction(z):
return 1/ (1+np.exp(-z))
请注意,通过降低激活函数的灵敏度,我们可以减少输入中的故障的影响。请注意,Sigmoid 激活函数的输出仍然是二进制的,即 0 或 1。
修正线性单元(ReLU)
本章介绍的前两个激活函数的输出是二进制的。这意味着它们将取一组输入变量并将它们转换为二进制输出。ReLU 是一个激活函数,它将一组输入变量作为输入,并将它们转换为单一的连续输出。在神经网络中,ReLU 是最流行的激活函数,通常用于隐藏层,我们不希望将连续变量转换为类别变量。
下面的图表总结了 ReLU 激活函数:

请注意,当x≤ 0时,y = 0。这意味着输入中的任何信号为零或小于零都被转换为零输出:
for 
for 
一旦x大于零,它就是x。
ReLU 函数是神经网络中最常用的激活函数之一。它可以在 Python 中实现如下:
def ReLU(x):
if x<0:
return 0
else:
return x
现在让我们来看看 Leaky ReLU,它是基于 ReLU 的。
Leaky ReLU
在 ReLU 中,x的负值导致y的值为零。这意味着在这个过程中丢失了一些信息,这使得训练周期变得更长,特别是在训练开始时。Leaky ReLU 激活函数解决了这个问题。对于 Leaky ReLu,以下内容适用:
; for 
for
下面的图表显示了这一点:

这里,ß是一个小于一的参数。
它可以在 Python 中实现如下:
def leakyReLU(x,beta=0.01):
if x<0:
return (beta*x)
else:
return x
有三种指定ß值的方法:
-
我们可以指定一个默认值为ß。
-
我们可以在我们的神经网络中将ß设为一个参数,并让神经网络决定该值(这称为参数化 ReLU)。
-
我们可以将ß设为一个随机值(这称为随机化 ReLU)。
双曲正切(tanh)
tanh 函数类似于 Sigmoid 函数,但它也能给出负信号。下图说明了这一点:

y函数如下:

可以通过以下 Python 代码实现:
def tanh(x):
numerator = 1-np.exp(-2*x)
denominator = 1+np.exp(-2*x)
return numerator/denominator
现在让我们来看看 softmax 函数。
Softmax
有时,我们需要激活函数的输出不止两个级别。Softmax 是一个可以为输出提供不止两个级别的激活函数。它最适用于多类分类问题。假设我们有n个类。我们有输入值。输入值将类映射如下:
x = {x((1)),x((2)),....x^((n))}
Softmax 是基于概率理论的。Softmax 的第e类的输出概率计算如下:
对于二元分类器,最终层的激活函数将是 Sigmoid,对于多类分类器,它将是 Softmax。
工具和框架
在本节中,我们将详细了解用于实现神经网络的框架和工具。
随着时间的推移,许多不同的框架已经被开发出来来实现神经网络。不同的框架有各自的优势和劣势。在本节中,我们将重点关注具有 TensorFlow 后端的 Keras。
Keras
Keras 是最受欢迎和易于使用的神经网络库之一,用 Python 编写。它考虑到易用性,并提供了实现深度学习的最快方式。Keras 只提供高级模块,并被认为是在模型级别上。
Keras 的后端引擎
Keras 需要一个低级别的深度学习库来执行张量级别的操作。这个低级别的深度学习库称为后端引擎。Keras 的可能后端引擎包括以下内容:
-
TensorFlow (www.tensorflow.org):这是同类框架中最受欢迎的框架,由谷歌开源。
-
Theona (deeplearning.net/software/theano):这是在蒙特利尔大学 MILA 实验室开发的。
-
Microsoft Cognitive Toolkit(CNTK):这是由微软开发的。
这种模块化深度学习技术堆栈的格式如下图所示:

这种模块化的深度学习架构的优势在于,Keras 的后端可以在不重写任何代码的情况下进行更改。例如,如果我们发现对于特定任务,TensorFlow 比 Theona 更好,我们可以简单地将后端更改为 TensorFlow,而无需重写任何代码。
深度学习堆栈的低级层
我们刚刚提到的三种后端引擎都可以在 CPU 和 GPU 上运行,使用堆栈的低级层。对于 CPU,使用了一个名为Eigen的张量操作低级库。对于 GPU,TensorFlow 使用了 NVIDIA 的CUDA 深度神经网络(cuDNN)库。
定义超参数
如第六章中所讨论的无监督机器学习算法,超参数是在学习过程开始之前选择其值的参数。我们从常识值开始,然后尝试稍后优化它们。对于神经网络,重要的超参数包括:
-
激活函数
-
学习率
-
隐藏层的数量
-
每个隐藏层中的神经元数量
让我们看看如何使用 Keras 定义模型。
定义 Keras 模型
定义完整的 Keras 模型涉及三个步骤:
- 定义层。
我们可以用两种可能的方式使用 Keras 构建模型:
- Sequential API:这允许我们为一系列层构建模型。它用于相对简单的模型,并且通常是构建模型的常规选择:

请注意,在这里,我们创建了三层 - 前两层具有 ReLU 激活函数,第三层具有 softmax 作为激活函数。
- Functional API:这允许我们为层的非循环图形构建模型。使用 Functional API 可以创建更复杂的模型。

请注意,我们可以使用顺序和功能 API 来定义相同的神经网络。从性能的角度来看,使用哪种方法来定义模型并没有任何区别。
- 定义学习过程。
在这一步中,我们定义了三件事:
-
优化器
-
损失函数
-
将量化模型质量的指标:

请注意,我们使用model.compile函数来定义优化器、损失函数和指标。
- 训练模型。
一旦定义了架构,就是训练模型的时候了:

请注意,batch_size和epochs等参数是可配置的参数,使它们成为超参数。
选择顺序或功能模型
顺序模型将 ANN 创建为简单的层堆叠。顺序模型易于理解和实现,但其简单的架构也有一个主要限制。每一层只连接到一个输入和输出张量。这意味着如果我们的模型在任何隐藏层的输入或输出处有多个输入或多个输出,那么我们不能使用顺序模型。在这种情况下,我们将不得不使用功能模型。
理解 TensorFlow
TensorFlow 是处理神经网络的最流行的库之一。在前面的部分中,我们看到了如何将其用作 Keras 的后端引擎。它是一个开源的高性能库,实际上可以用于任何数值计算。如果我们看一下堆栈,我们可以看到我们可以用高级语言(如 Python 或 C++)编写 TensorFlow 代码,然后由 TensorFlow 分布式执行引擎解释。这使得它对开发人员非常有用和受欢迎。
TensorFlow 的工作方式是创建一个有向图(DG)来表示您的计算。连接节点的是边,数学运算的输入和输出。它们也代表数据的数组。
介绍 TensorFlow 的基本概念
让我们简要了解一下 TensorFlow 的概念,比如标量、向量和矩阵。我们知道,在传统数学中,简单的数字,比如三或五,被称为标量。此外,在物理学中,向量是具有大小和方向的东西。在 TensorFlow 中,我们使用向量来表示一维数组。延伸这个概念,二维数组被称为矩阵。对于三维数组,我们使用术语3D 张量。我们使用术语等级来捕捉数据结构的维度。因此,标量是一个等级 0的数据结构,向量是一个等级 1的数据结构,矩阵是一个等级 2的数据结构。这些多维结构被称为张量,并在下图中显示:

在前面的图表中,我们可以看到等级定义了张量的维度。
现在让我们看看另一个参数,shape。shape是一个整数元组,指定每个维度中数组的长度。下图解释了shape的概念:

使用shape和等级,我们可以指定张量的详细信息。
理解张量数学
现在让我们看看使用张量进行不同的数学计算:
- 让我们定义两个标量,并尝试使用 TensorFlow 进行加法和乘法:

- 我们可以将它们相加和相乘,并显示结果:

- 我们还可以通过将两个张量相加来创建一个新的标量张量:

- 我们还可以执行复杂的张量函数:

理解神经网络的类型
神经网络可以有多种构建方式。如果每一层中的每个神经元都连接到另一层中的每个神经元,那么我们称之为密集或全连接神经网络。让我们看看一些其他形式的神经网络。
卷积神经网络
卷积神经网络(CNNs)通常用于分析多媒体数据。为了更多地了解 CNN 如何用于分析基于图像的数据,我们需要掌握以下过程:
-
卷积
-
池化
让我们逐一探索它们。
卷积
卷积的过程通过使用另一个较小的图像(也称为过滤器或核)来处理特定图像中感兴趣的模式。例如,如果我们想要在图像中找到物体的边缘,我们可以使用特定的过滤器对图像进行卷积来得到它们。边缘检测可以帮助我们进行物体检测、物体分类和其他应用。因此,卷积的过程是关于在图像中找到特征和特点。
寻找模式的方法是基于寻找可以在不同数据上重复使用的模式。可重复使用的模式称为过滤器或核。
池化
为了进行机器学习的多媒体数据处理的重要部分是对其进行下采样。这提供了两个好处:
-
它减少了问题的整体维度,大大减少了训练模型所需的时间。
-
通过聚合,我们可以提取多媒体数据中不必要的细节,使其更通用并更具代表性。
下采样的执行如下:

请注意,我们已经用一个像素替换了每个四个像素的块,选择了四个像素中的最高值作为该像素的值。这意味着我们已经按四分之一的比例进行了下采样。由于我们选择了每个块中的最大值,这个过程被称为最大池化。我们也可以选择*均值;在那种情况下,它将是*均池化。
循环神经网络
循环神经网络(RNNs)是一种特殊类型的神经网络,它们基于循环架构。这就是为什么它们被称为循环。需要注意的重要事情是 RNNs 具有记忆。这意味着它们有能力存储最*迭代的信息。它们被用于分析句子结构以预测句子中的下一个单词等领域。
生成对抗网络
生成对抗网络(GANs)是一种生成合成数据的神经网络类型。它们是由 Ian Goodfellow 及其同事于 2014 年创建的。它们可以用来生成从未存在过的人的照片。更重要的是,它们用于生成合成数据以增加训练数据集。
在接下来的部分中,我们将看到什么是迁移学习。
迁移学习
多年来,许多组织、研究团体和开源社区内的个人已经完善了一些使用大量数据进行训练的复杂模型,以供通用用途。在某些情况下,他们已经投入了多年的努力来优化这些模型。一些这些开源模型可以用于以下应用:
-
视频中的物体检测
-
图像中的物体检测
-
音频的转录
-
文本的情感分析
每当我们开始训练一个新的机器学习模型时,我们要问自己的问题是:我们是否可以简单地定制一个经过充分验证的预训练模型,而不是从头开始?换句话说,我们是否可以将现有模型的学习迁移到我们的自定义模型,以便回答我们的业务问题?如果我们能做到这一点,它将提供三个好处:
-
我们的模型训练工作将得到一个快速启动。
-
通过使用经过充分测试和建立的模型,我们的模型整体质量可能会得到提高。
-
如果我们没有足够的数据来解决我们正在处理的问题,使用通过迁移学习的预训练模型可能会有所帮助。
让我们看两个实际例子,这将是有用的:
-
在训练机器人时,我们可以首先使用模拟游戏来训练神经网络模型。在那个模拟中,我们可以创建所有那些在现实世界中很难找到的罕见事件。一旦训练完成,我们可以使用迁移学习来训练模型适用于真实世界。
-
假设我们想要训练一个模型,可以从视频源中分类苹果和 Windows 笔记本电脑。已经有成熟的开源目标检测模型可以准确分类视频源中的各种物体。我们可以使用这些模型作为起点,识别笔记本电脑。一旦我们识别出物体是笔记本电脑,我们可以进一步训练模型区分苹果和 Windows 笔记本电脑。
在下一节中,我们将应用本章涵盖的概念来构建一个欺诈文档分类神经网络。
案例研究-使用深度学习进行欺诈检测
使用机器学习(ML)技术识别欺诈文档是一个活跃且具有挑战性的研究领域。研究人员正在调查神经网络的模式识别能力在多大程度上可以用于这个目的。可以使用原始像素而不是手动属性提取器,用于几种深度学习架构结构。
方法论
本节介绍的技术使用了一种称为Siamese 神经网络的神经网络架构,它具有两个共享相同架构和参数的分支。使用 Siamese 神经网络来标记欺诈文档如下图所示:

当需要验证特定文档的真实性时,我们首先基于其布局和类型对文档进行分类,然后将其与预期的模板和模式进行比较。如果偏离超过一定阈值,它被标记为伪造文档;否则,它被视为真实文档。对于关键用例,我们可以添加一个手动流程,用于边界情况,算法无法确定地将文档分类为真实或伪造。
为了比较文档与其预期模板,我们在 Siamese 架构中使用两个相同的 CNN。CNN 具有学习最佳的*移不变局部特征检测器和可以构建对输入图像的几何失真具有鲁棒性的表示的优势。这非常适合我们的问题,因为我们的目标是通过单个网络传递真实和测试文档,然后比较它们的相似性。为了实现这个目标,我们实施以下步骤。
假设我们想要测试一个文档。对于每类文档,我们执行以下步骤:
-
获取真实文档的存储图像。我们称之为真实文档。测试文档应该看起来像真实文档。
-
真实文档通过神经网络层,创建一个特征向量,这是真实文档模式的数学表示。我们称之为特征向量 1,如前图所示。
-
需要测试的文档称为测试文档。我们通过一个类似于用于创建真实文档特征向量的网络来传递这个文档。测试文档的特征向量称为特征向量 2。
-
我们使用特征向量 1 和特征向量 2 之间的欧氏距离来计算真实文档和测试文档之间的相似度分数。这个相似度分数被称为相似度测量(MOS)。MOS 是 0 到 1 之间的数字。较高的数字代表文档之间的距离较小,文档相似的可能性较大。
-
如果神经网络计算的相似度分数低于预定义的阈值,我们将文档标记为欺诈。
让我们看看如何使用 Python 实现 Siamese 神经网络:
- 首先,让我们导入所需的 Python 包:
import random
import numpy as np
import tensorflow as tf
- 接下来,我们将定义将用于处理 Siamese 网络各个分支的神经网络:
def createTemplate():
return tf.keras.models.Sequential([
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(128, activation='relu'),
tf.keras.layers.Dropout(0.15),
tf.keras.layers.Dense(128, activation='relu'),
tf.keras.layers.Dropout(0.15),
tf.keras.layers.Dense(64, activation='relu'),
])
请注意,为了减少过拟合,我们还指定了0.15的丢失率。
- 为了实现 Siamese 网络,我们将使用 MNIST 图像。MNIST 图像非常适合测试我们的方法的有效性。我们的方法包括以每个样本包含两个图像和一个二进制相似度标志的方式准备数据。这个标志是它们来自相同类别的指示器。现在让我们实现名为
prepareData的函数,它可以为我们准备数据:
def prepareData(inputs: np.ndarray, labels: np.ndarray):
classesNumbers = 10
digitalIdx = [np.where(labels == i)[0] for i in range(classesNumbers)]
pairs = list()
labels = list()
n = min([len(digitalIdx[d]) for d in range(classesNumbers)]) - 1
for d in range(classesNumbers):
for i in range(n):
z1, z2 = digitalIdx[d][i], digitalIdx[d][i + 1]
pairs += [[inputs[z1], inputs[z2]]]
inc = random.randrange(1, classesNumbers)
dn = (d + inc) % classesNumbers
z1, z2 = digitalIdx[d][i], digitalIdx[dn][i]
pairs += [[inputs[z1], inputs[z2]]]
labels += [1, 0]
return np.array(pairs), np.array(labels, dtype=np.float32)
注意,prepareData()将导致所有数字的样本数量相等。
- 我们现在将准备训练和测试数据集:
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()
x_train = x_train.astype(np.float32)
x_test = x_test.astype(np.float32)
x_train /= 255
x_test /= 255
input_shape = x_train.shape[1:]
train_pairs, tr_labels = prepareData(x_train, y_train)
test_pairs, test_labels = prepareData(x_test, y_test)
- 现在,让我们创建 Siamese 系统的两个部分:
input_a = tf.keras.layers.Input(shape=input_shape)
enconder1 = base_network(input_a)
input_b = tf.keras.layers.Input(shape=input_shape)
enconder2 = base_network(input_b)
- 现在,我们将实现相似度度量(MOS),它将量化我们想要比较的两个文档之间的距离:
distance = tf.keras.layers.Lambda(
lambda embeddings: tf.keras.backend.abs(embeddings[0] - embeddings[1])) ([enconder1, enconder2])
measureOfSimilarity = tf.keras.layers.Dense(1, activation='sigmoid') (distance)
现在,让我们训练模型。我们将使用 10 个 epochs 来训练这个模型:

请注意,我们使用 10 个 epochs 达到了 97.49%的准确率。增加 epochs 的数量将进一步提高准确度水*。
总结
在本章中,我们首先看了神经网络的细节。我们首先看了神经网络多年来的发展。我们研究了不同类型的神经网络。然后,我们看了神经网络的各种构建模块。我们深入研究了用于训练神经网络的梯度下降算法。我们讨论了各种激活函数,并研究了激活函数在神经网络中的应用。我们还看了迁移学习的概念。最后,我们看了一个实际例子,说明了神经网络如何用于训练可以部署到标记伪造或欺诈文件的机器学习模型。
展望未来,在下一章中,我们将探讨如何将这样的算法用于自然语言处理。我们还将介绍网络嵌入的概念,并将研究循环网络在自然语言处理中的应用。最后,我们还将研究如何实现情感分析。
第九章:自然语言处理算法
本章介绍了自然语言处理(NLP)的算法。本章从理论到实践逐步进行。它将首先介绍 NLP 的基础知识,然后介绍基本算法。然后,它将研究最流行的神经网络之一,该网络被广泛用于设计和实施文本数据的重要用例的解决方案。最后,我们将研究 NLP 的局限性,最后学习如何使用 NLP 来训练一个可以预测电影评论极性的机器学习模型。
本章将包括以下部分:
-
介绍 NLP
-
基于词袋(BoW)的 NLP
-
词嵌入介绍
-
使用递归神经网络进行 NLP
-
使用 NLP 进行情感分析
-
案例研究:电影评论情感分析
通过本章结束时,您将了解用于 NLP 的基本技术。您应该能够理解 NLP 如何用于解决一些有趣的现实世界问题。
让我们从基本概念开始。
介绍 NLP
NLP 用于研究形式化和规范化计算机与人类(自然)语言之间的交互。NLP 是一个综合性的学科,涉及使用计算机语言学算法和人机交互技术和方法来处理复杂的非结构化数据。NLP 可以用于各种情况,包括以下情况:
-
主题识别:发现文本存储库中的主题,并根据发现的主题对存储库中的文档进行分类
-
情感分析:根据文本中包含的积极或消极情感对文本进行分类
-
机器翻译:将文本从一种口头人类语言翻译成另一种口头人类语言
-
文本转语音:将口头语言转换为文本
-
主观解释:智能地解释问题并利用可用信息回答问题
-
实体识别:从文本中识别实体(如人、地点或物品)
-
假新闻检测:根据内容标记假新闻
让我们首先看一些在讨论 NLP 时使用的术语。
理解 NLP 术语
NLP 是一个综合性的学科。在围绕某一领域的文献中,我们会观察到,有时会使用不同的术语来指定相同的事物。我们将从一些与 NLP 相关的基本术语开始。让我们从规范化开始,这是一种基本的 NLP 处理,通常在输入数据上执行。
规范化
规范化是对输入文本数据进行的处理,以提高其在训练机器学习模型的情况下的质量。规范化通常包括以下处理步骤:
-
将所有文本转换为大写或小写
-
去除标点符号
-
去除数字
请注意,尽管通常需要前面的处理步骤,但实际的处理步骤取决于我们想要解决的问题。它们会因用例而异,例如,如果文本中的数字代表了在我们尝试解决的问题的情境中可能具有一些价值的东西,那么我们在规范化阶段可能就不需要从文本中去除数字。
语料库
我们用来解决问题的输入文档组称为语料库。语料库充当 NLP 问题的输入数据。
标记化
当我们使用 NLP 时,第一项工作是将文本分成一个标记列表。这个过程称为标记化。由于目标的不同,生成的标记的粒度也会有所不同,例如,每个标记可以包括以下内容:
-
一个词
-
一组单词的组合
-
一个句子
-
一个段落
命名实体识别
在 NLP 中,有许多用例需要从非结构化数据中识别特定的单词和数字,这些单词和数字属于预定义的类别,如电话号码、邮政编码、姓名、地点或国家。这用于为非结构化数据提供结构。这个过程称为命名实体识别(NER)。
停用词
在单词级别的标记化之后,我们得到了文本中使用的单词列表。其中一些单词是常见单词,预计几乎会出现在每个文档中。这些单词不会为它们出现在的文档提供任何额外的见解。这些单词被称为停用词。它们通常在数据处理阶段被移除。一些停用词的例子是was、we和the。
情感分析
情感分析,或者称为意见挖掘,是从文本中提取正面或负面情感的过程。
词干提取和词形还原
在文本数据中,大多数单词可能以稍微不同的形式存在。将每个单词减少到其原始形式或词干所属的词族中称为词干提取。它用于根据它们的相似含义对单词进行分组,以减少需要分析的单词总数。基本上,词干提取减少了问题的整体条件性。
例如,{use, used, using, uses} => use。
英语词干提取的最常见算法是波特算法。
词干提取是一个粗糙的过程,可能会导致词尾被截断。这可能导致拼写错误的单词。对于许多用例来说,每个单词只是我们问题空间中的一个级别的标识符,拼写错误的单词并不重要。如果需要正确拼写的单词,那么应该使用词形还原而不是词干提取。
算法缺乏常识。对于人类大脑来说,将类似的单词视为相同是很简单的。对于算法,我们必须引导它并提供分组标准。
从根本上讲,有三种不同的 NLP 实现方法。这三种技术在复杂性方面有所不同,如下所示:
-
基于词袋模型(BoW-based)的 NLP
-
传统的 NLP 分类器
-
使用深度学习进行自然语言处理
NLTK
自然语言工具包(NLTK)是 Python 中处理 NLP 任务最广泛使用的包。NLTK 是用于 NLP 的最古老和最流行的 Python 库之一。NLTK 非常好,因为它基本上为构建任何 NLP 流程提供了一个起点,它为您提供了基本工具,然后您可以将它们链接在一起以实现您的目标,而不是从头开始构建所有这些工具。许多工具都打包到了 NLTK 中,在下一节中,我们将下载该包并探索其中的一些工具。
让我们来看看基于词袋模型的 NLP。
基于词袋模型的 NLP
将输入文本表示为一组标记的过程称为基于词袋模型的处理。使用词袋模型的缺点是我们丢弃了大部分语法和标记化,这有时会导致丢失单词的上下文。在词袋模型的方法中,我们首先量化要分析的每个文档中每个单词的重要性。
从根本上讲,有三种不同的方法来量化每个文档中单词的重要性:
-
二进制:如果单词出现在文本中,则特征的值为 1,否则为 0。
-
计数:特征将以单词在文本中出现的次数作为其值,否则为 0。
-
词项频率/逆文档频率:特征的值将是单个文档中单词的独特程度与整个文档语料库中单词的独特程度的比率。显然,对于常见单词,如 the、in 等(称为停用词),词项频率-逆文档频率(TF-IDF)得分将很低。对于更独特的单词,例如领域特定术语,得分将更高。
请注意,通过使用词袋模型,我们丢失了信息——即文本中单词的顺序。这通常有效,但可能会导致准确性降低。
让我们看一个具体的例子。我们将训练一个模型,可以将餐厅的评论分类为负面或正面。输入文件是一个结构化文件,其中评论将被分类为正面或负面。
为此,让我们首先处理输入数据。
处理步骤在下图中定义:

让我们通过以下步骤实现这个处理流程:
- 首先,让我们导入我们需要的包:
import numpy as np
import pandas as pd
- 然后我们从
CSV文件中导入数据集:

- 接下来,我们清理数据:
# Cleaning the texts
import re
import nltk
nltk.download('stopwords')
from nltk.corpus import stopwords
from nltk.stem.porter import PorterStemmer
corpus = []
for i in range(0, 1000):
review = re.sub('[^a-zA-Z]', ' ', dataset['Review'][i])
review = review.lower()
review = review.split()
ps = PorterStemmer()
review = [ps.stem(word) for word in review if not word in set(stopwords.words('english'))]
review = ' '.join(review)
corpus.append(review)
- 现在让我们定义特征(用
y表示)和标签(用X表示):
from sklearn.feature_extraction.text import CountVectorizer
cv = CountVectorizer(max_features = 1500)
X = cv.fit_transform(corpus).toarray()
y = dataset.iloc[:, 1].values
- 让我们将数据分成测试数据和训练数据:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.20, random_state = 0)
- 对于训练模型,我们使用朴素贝叶斯算法:
from sklearn.naive_bayes import GaussianNB
classifier = GaussianNB()
classifier.fit(X_train, y_train)
- 让我们预测测试集的结果:
y_pred = classifier.predict(X_test)
- 混淆矩阵如下所示:

通过观察混淆矩阵,我们可以估计误分类情况。
词嵌入简介
在前面的部分中,我们学习了如何使用词袋模型作为输入文本数据的抽象来执行 NLP。NLP 的一个主要进展是我们能够以密集向量的形式创建单词的有意义的数值表示能力。这种技术称为词嵌入。Yoshua Bengio 首次在他的论文《神经概率语言模型》中引入了这个术语。NLP 问题中的每个词都可以被视为一个分类对象。将每个词映射到表示为向量的数字列表称为词嵌入。换句话说,用于将单词转换为实数的方法称为词嵌入。嵌入的一个区别特征是它使用密集向量,而不是使用传统方法使用稀疏矩阵向量。
使用词袋模型进行 NLP 存在两个基本问题:
-
语义上下文的丢失:当我们对数据进行标记化时,它的上下文就丢失了。一个词可能根据它在句子中的使用位置有不同的含义;当解释复杂的人类表达时,比如幽默或讽刺,这变得更加重要。
-
稀疏输入:当我们进行标记化时,每个单词都成为一个特征。正如我们在前面的例子中看到的,每个单词都是一个特征。这导致了稀疏的数据结构。
一个词的邻域
如何向算法呈现文本数据(特别是单词或词元)的关键见解来自语言学。在词嵌入中,我们关注每个词的邻域,并用它来确定其含义和重要性。一个词的邻域是围绕特定词的一组词。一个词的上下文是由它的邻域决定的。
请注意,在词袋模型中,一个词失去了它的上下文,因为它的上下文来自它所在的邻域。
词嵌入的特性
良好的词嵌入具有以下四个特性:
-
它们是密集的:实际上,嵌入本质上是因子模型。因此,嵌入向量的每个组件代表一个(潜在)特征的数量。通常我们不知道该特征代表什么;但是,我们将有非常少的(如果有的话)零值,这将导致稀疏输入。
-
它们是低维的:嵌入具有预定义的维度(作为超参数选择)。我们之前看到,在 BoW 表示中,我们需要为每个单词输入|V|,因此输入的总大小为|V| * n,其中n是我们用作输入的单词数。使用单词嵌入,我们的输入大小将是d * n,其中d通常在 50 到 300 之间。考虑到大型文本语料库通常远大于 300 个单词,这意味着我们在输入大小上有很大的节省,我们看到这可能导致更小的数据实例总数的更高准确性。
-
它们嵌入领域语义:这个属性可能是最令人惊讶的,但也是最有用的。当正确训练时,嵌入会学习关于其领域的含义。
-
易于泛化:最后,网络嵌入能够捕捉到一般的抽象模式——例如,我们可以对(嵌入的)猫、鹿、狗等进行训练,模型将理解我们指的是动物。请注意,模型从未接受过对羊的训练,但模型仍然会正确分类它。通过使用嵌入,我们可以期望得到正确的答案。
现在让我们探讨一下,我们如何使用 RNN 进行自然语言处理。
使用 RNN 进行 NLP
RNN 是一个具有反馈的传统前馈网络。对 RNN 的一种简单思考方式是,它是一个带有状态的神经网络。RNN 可用于任何类型的数据,用于生成和预测各种数据序列。训练 RNN 模型是关于构建这些数据序列。RNN 可用于文本数据,因为句子只是单词序列。当我们将 RNN 用于 NLP 时,我们可以用它来进行以下操作:
-
在输入时预测下一个单词
-
生成新的文本,遵循文本中已经使用的风格:

还记得导致它们正确预测的单词组合吗?RNN 的学习过程是基于语料库中的文本。它们通过减少预测的下一个单词和实际的下一个单词之间的错误来进行训练。
使用 NLP 进行情感分析
本节介绍的方法是基于对分类高速流推文的使用情况。手头的任务是提取关于所选主题的推文中嵌入的情绪。情感分类实时量化每条推文中的极性,然后聚合所有推文的总情感,以捕捉关于所选主题的整体情感。为了应对 Twitter 流数据的内容和行为带来的挑战,并有效地执行实时分析,我们使用 NLP 使用训练过的分类器。然后将训练过的分类器插入 Twitter 流中,以确定每条推文的极性(积极、消极或中性),然后聚合并确定关于某一主题的所有推文的整体极性。让我们一步一步地看看这是如何完成的。
首先,我们必须训练分类器。为了训练分类器,我们需要一个已经准备好的数据集,其中包含有历史的 Twitter 数据,并且遵循实时数据的模式和趋势。因此,我们使用了来自网站www.sentiment140.com的数据集,该数据集带有一个人工标记的语料库(基于该分析的大量文本集合),其中包含超过 160 万条推文。该数据集中的推文已经被标记为三种极性之一:零表示负面,两表示中性,四表示正面。除了推文文本之外,语料库还提供了推文 ID、日期、标志和推文用户。现在让我们看看在训练分类器之前对实时推文执行的每个操作:
-
首先将推文分割成称为标记的单词(标记化)。
-
标记化的输出创建了一个 BoW,其中包含文本中的单个单词。
-
这些推文进一步通过去除数字、标点和停用词(停用词去除)进行过滤。停用词是非常常见的词,如is、am、are和the。由于它们没有额外的信息,这些词被移除。
-
此外,非字母字符,如#**@和数字,使用模式匹配进行删除,因为它们在情感分析的情况下没有相关性。正则表达式用于仅匹配字母字符,其余字符将被忽略。这有助于减少 Twitter 流的混乱。
-
先前阶段的结果被用于词干处理阶段。在这个阶段,派生词被减少到它们的词根-例如,像fish这样的词与fishing和fishes具有相同的词根。为此,我们使用标准 NLP 库,它提供各种算法,如 Porter 词干处理。
-
一旦数据被处理,它被转换成一个称为术语文档矩阵(TDM)的结构。TDM 表示过滤后语料库中每个词的术语和频率。
-
从 TDM 中,推文到达训练过的分类器(因为它经过训练,可以处理推文),它计算每个词的情感极性重要性(SPI),这是一个从-5 到+5 的数字。正负号指定了该特定词所代表的情绪类型,其大小表示情感的强度。这意味着推文可以被分类为正面或负面(参考下图)。一旦我们计算了个别推文的极性,我们将它们的总体 SPI 相加,以找到来源的聚合情感-例如,总体极性大于一表示我们观察时间内推文的聚合情感是积极的。
为了获取实时原始推文,我们使用 Scala 库Twitter4J,这是一个提供实时 Twitter 流 API 包的 Java 库。该 API 要求用户在 Twitter 上注册开发者帐户并填写一些认证参数。该 API 允许您获取随机推文或使用选择的关键词过滤推文。我们使用过滤器来检索与我们选择的关键词相关的推文。
总体架构如下图所示:

情感分析有各种应用。它可以用来分类客户的反馈。政府可以利用社交媒体极性分析来找到他们政策的有效性。它还可以量化各种广告活动的成功。
在接下来的部分,我们将学习如何实际应用情感分析来预测电影评论的情感。
案例研究:电影评论情感分析
让我们使用 NLP 进行电影评论情感分析。为此,我们将使用一些开放的电影评论数据,可在www.cs.cornell.edu/people/pabo/movie-review-data/上找到:
- 首先,我们将导入包含电影评论的数据集:
import numpy as np
import pandas as pd
- 现在,让我们加载电影数据并打印前几行以观察其结构。
df=pd.read_csv("moviereviews.tsv",sep='\t')
df.head()

请注意数据集有2000条电影评论。其中一半是负面的,一半是正面的。
- 现在,让我们开始准备数据集以训练模型。首先,让我们删除数据中的任何缺失值
df.dropna(inplace=True)
- 现在我们需要移除空格。空格不是空的,但需要被移除。为此,我们需要遍历输入
DataFrame中的每一行。我们将使用.itertuples()来访问每个字段:
blanks=[]
for i,lb,rv in df.itertuples():
if rv.isspace():
blanks.append(i)
df.drop(blanks,inplace=True)
请注意,我们已经使用i,lb和rv来索引、标签和评论列。
让我们将数据分割成测试和训练数据集:
- 第一步是指定特征和标签,然后将数据分割成训练集和测试集:
from sklearn.model_selection import train_test_split
X = df['review']
y = df['label']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
现在我们有测试和训练数据集。
- 现在让我们将数据集分成训练集和测试集:
from sklearn.pipeline import Pipeline
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import MultinomialNB
# Naïve Bayes:
text_clf_nb = Pipeline([('tfidf', TfidfVectorizer()),
('clf', MultinomialNB()),
])
请注意,我们正在使用tfidf来量化集合中数据点的重要性。
接下来,让我们使用朴素贝叶斯算法来训练模型,然后测试训练好的模型。
让我们按照以下步骤来训练模型:
- 现在让我们使用我们创建的测试和训练数据集来训练模型:
text_clf_nb.fit(X_train, y_train)
- 让我们运行预测并分析结果:
# Form a prediction set
predictions = text_clf_nb.predict(X_test)
让我们通过打印混淆矩阵来查看模型的性能。我们还将查看精确度、召回率、F1 分数和准确度。

这些性能指标为我们提供了预测质量的度量。准确率为 0.78,现在我们已经成功训练了一个可以预测特定电影评论类型的模型。
摘要
在本章中,我们讨论了与自然语言处理相关的算法。首先,我们研究了与自然语言处理相关的术语。接下来,我们研究了实施自然语言处理策略的 BoW 方法。然后,我们研究了词嵌入的概念以及在自然语言处理中使用神经网络。最后,我们看了一个实际的例子,我们在这一章中使用了开发的概念来根据电影评论的文本来预测情感。通过学习本章内容,用户应该能够将自然语言处理用于文本分类和情感分析。
在下一章中,我们将研究推荐引擎。我们将研究不同类型的推荐引擎以及它们如何用于解决一些现实世界的问题。
第十章:推荐引擎
推荐引擎是利用用户偏好和产品详情提供明智建议的一种方式。推荐引擎的目标是了解一组项目之间的相似性模式和/或制定用户和项目之间的互动。
本章首先介绍了推荐引擎的基础知识。然后,讨论了各种类型的推荐引擎。接下来,本章讨论了推荐引擎如何用于向不同用户建议项目和产品以及推荐引擎的各种限制。最后,我们将学习如何使用推荐引擎解决现实问题。
本章讨论了以下概念:
-
推荐系统的介绍
-
推荐引擎的类型
-
了解推荐系统的限制
-
实际应用领域
-
一个实际的例子——创建一个推荐引擎向订阅者推荐电影
在本章结束时,您应该能够理解如何使用推荐引擎根据一些偏好标准建议各种项目。
让我们从推荐引擎的背景概念开始。
推荐系统的介绍
推荐系统代表研究人员最初开发的方法,用于预测用户最有可能感兴趣的项目。推荐系统在给出关于项目的个性化建议方面的能力使其可能是在线购买世界中最重要的技术。
在电子商务应用中,推荐引擎使用复杂的算法来改善购物体验,允许服务提供商根据用户的偏好定制产品。
2009 年,Netflix 向任何能够通过超过 10%改进其现有推荐引擎(Cinematch)的算法提供 100 万美元的奖金。奖金被 BellKor 的 Pragmatic Chaos 团队赢得。
推荐引擎的类型
通常有三种不同类型的推荐引擎:
-
基于内容的推荐引擎
-
协同过滤引擎
-
混合推荐引擎
基于内容的推荐引擎
基于内容的推荐引擎的基本思想是建议与用户先前表现出兴趣的项目相似的项目。基于内容的推荐引擎的有效性取决于我们量化项目与其他项目的相似性的能力。
让我们看看下面的图表。如果用户 1已阅读文档 1,那么我们可以向用户推荐与文档 1相似的文档 2:

现在的问题是如何确定哪些项目彼此相似。让我们看看找到不同项目之间相似性的几种方法。
查找非结构化文档之间的相似性
确定不同文档之间相似性的一种方法是首先处理输入文档。处理非结构化文档后得到的数据结构称为术语文档矩阵(TDM),如下图所示:

TDM 具有所有术语的词汇表作为行,所有文档作为列。它可以用于根据所选的距离度量确定哪些文档与其他文档相似。例如,Google 新闻根据与用户已经表现出兴趣的新闻相似性向用户推荐新闻。
一旦我们有了 TDM,有两种方法可以量化文档之间的相似性:
-
使用频率计数:这意味着我们假设一个词的重要性与每个词的频率成正比。这是计算重要性的最简单方法。
-
使用 TFIDF(词频-逆文档频率的缩写):这是一个计算每个词在我们试图解决的问题的上下文中重要性的数字。它是两个术语的乘积:
-
词频(TF):这是一个词或术语在文档中出现的次数。词频直接与一个词的重要性相关联。
-
逆文档频率(IDF):首先,文档频率(DF)是包含我们搜索的术语的文档数量。作为 DF 的相反,IDF 给出了一个词所代表的独特性的度量,并将其与其重要性相关联。
-
由于 TF 和 IDF 都量化了我们试图解决的问题的上下文中一个词的重要性,它们的组合 TF-IDF 是每个词的重要性的一个很好的度量,是使用简单频率计数的更复杂的替代方法。
使用共现矩阵
这种方法基于这样的假设:如果两个物品大多数情况下一起购买,那么它们很可能是相似的,或者至少属于通常一起购买的物品类别。
例如,如果人们大多数情况下一起使用剃须膏和剃刀,那么如果有人买了剃刀,建议他也买剃须膏是有道理的。
让我们分析这四个用户的历史购买模式:
| 剃刀 | 苹果 | 剃须膏 | 自行车 | 鹰嘴豆泥 | |
|---|---|---|---|---|---|
| 迈克 | 1 | 1 | 1 | 0 | 1 |
| 泰勒 | 1 | 0 | 1 | 1 | 1 |
| 埃琳娜 | 0 | 0 | 0 | 1 | 0 |
| 阿明 | 1 | 0 | 1 | 0 | 0 |
这将创建以下共现矩阵:
| 剃刀 | 苹果 | 剃须膏 | 自行车 | 鹰嘴豆泥 | |
|---|---|---|---|---|---|
| 剃刀 | - | 1 | 3 | 1 | 1 |
| 苹果 | 1 | - | 1 | 0 | 1 |
| 剃须膏 | 3 | 1 | - | 1 | 2 |
| 自行车 | 1 | 0 | 1 | - | 1 |
| 鹰嘴豆泥 | 1 | 1 | 2 | 1 | - |
前述共现矩阵总结了一起购买两件物品的可能性。让我们看看如何使用它。
协同过滤推荐引擎
协同过滤的推荐是基于用户的历史购买模式的分析。基本假设是,如果两个用户对大多数相同的物品表现出兴趣,我们可以将两个用户都归类为相似。换句话说,我们可以假设以下内容:
-
如果两个用户的购买历史重叠超过阈值,我们可以将它们归类为相似用户。
-
查看相似用户的历史,购买历史中不重叠的物品将成为协同过滤推荐的基础。
例如,让我们看一个具体的例子。我们有两个用户,迈克和埃琳娜,如下图所示:

请注意以下内容:
-
迈克和埃琳娜都对文档 1和文档 2表现出了完全相同的兴趣。
-
根据他们相似的历史模式,我们可以将他们两个都归类为相似用户。
-
如果埃琳娜现在阅读文档 3,那么我们也可以建议迈克阅读文档 3。
请注意,根据用户历史记录向用户推荐物品的策略并不总是有效的。
假设埃琳娜和迈克对文档 1都表现出了兴趣,这是关于摄影的(因为他们对摄影有共同的爱好)。此外,他们两个都对文档 2表现出了兴趣,这是关于云计算的,同样是因为他们对这个主题有兴趣。根据协同过滤,我们将他们归类为相似用户。现在埃琳娜开始阅读文档 3,这是一本关于女性时尚的杂志。如果我们遵循协同过滤算法,我们会建议迈克也阅读,而他可能对此并不感兴趣。
回到 2012 年,美国超市 Target 正在尝试使用协同过滤为购买者推荐产品。该算法根据他们的档案将一个父亲归类为他的十几岁的女儿。结果,Target 最终向父亲发送了一个关于尿布、婴儿奶粉和婴儿床的折扣券。他并不知道他女儿怀孕了。
请注意,协同过滤算法不依赖于任何其他信息,是一种独立的算法,基于用户的变化行为和协同推荐。
混合推荐引擎
到目前为止,我们已经讨论了基于内容和基于协同过滤的推荐引擎。这两种类型的推荐引擎可以结合起来创建混合推荐引擎。为此,我们按照以下步骤进行:
-
生成物品的相似矩阵。
-
生成用户的偏好矩阵。
-
生成推荐。
让我们逐步了解这些步骤。
生成物品的相似矩阵
在混合推荐中,我们首先通过使用基于内容的推荐创建物品的相似矩阵。这可以通过使用共现矩阵或使用任何距离度量来量化物品之间的相似性来实现。
假设我们目前有五种物品。使用基于内容的推荐,我们生成一个捕捉物品之间相似性的矩阵,看起来像这样:

让我们看看如何将这个相似矩阵与偏好矩阵结合起来生成推荐。
生成用户的参考向量。
基于系统中每个用户的历史,我们将产生一个捕捉这些用户兴趣的偏好向量。
假设我们想为名为KentStreetOnline的在线商店生成推荐,该商店销售 100 种独特的物品。KentStreetOnline 很受欢迎,拥有 100 万活跃订阅者。重要的是要注意,我们只需要生成一个 100x100 维度的相似矩阵。我们还需要为每个用户生成一个偏好向量;这意味着我们需要为 100 万用户中的每一个生成 100 万个偏好向量。
性能向量的每个条目表示对项目的偏好。第一行的值表示项目 1的偏好权重为4。例如,第二行的值表示对项目 2没有偏好。
以下是图形显示:

现在,让我们看看如何基于相似矩阵 S 和用户偏好矩阵 U 生成推荐。
生成推荐
为了进行推荐,我们可以将这些矩阵相乘。用户更有可能对经常与他们给出高评分的物品共现的物品感兴趣:
Matrix[S] x Matrix[U] = Matrix[R]
这个计算在以下图表中显示:

为每个用户生成一个单独的结果矩阵。推荐矩阵Matrix[R]中的数字量化了用户对每个物品的预测兴趣。例如,在结果矩阵中,第四个物品的数字最高,为 58。因此,这个物品对这个特定用户来说是高度推荐的。
现在,让我们来看看不同推荐系统的局限性。
了解推荐系统的局限性
推荐引擎使用预测算法向一群用户建议推荐。这是一种强大的技术,但我们应该意识到它的局限性。让我们来看看推荐系统的各种局限性。
冷启动问题
显然,为了使协同过滤起作用,我们需要有关用户偏好的历史数据。对于新用户,我们可能没有任何数据,因此我们的用户相似性算法将基于可能不准确的假设。对于基于内容的推荐,我们可能不会立即获得有关新物品的详细信息。需要有关物品和用户的数据来生成高质量推荐的要求被称为冷启动问题。
元数据要求
基于内容的方法需要明确的物品描述来衡量相似性。这种明确的详细描述可能不可用,影响预测的质量。
数据稀疏问题
在大量物品中,用户只会对少数物品进行评分,导致非常稀疏的用户/物品评分矩阵。
亚马逊有大约十亿用户和十亿物品。据说亚马逊的推荐引擎是世界上数据最稀疏的推荐引擎。
由于社交影响而产生的偏见
社交影响在推荐系统中可以起到重要作用。社交关系可以被视为影响用户偏好的因素。朋友倾向于购买类似的物品并给出类似的评分。
有限的数据
有限数量的评论使得推荐系统难以准确衡量用户之间的相似性。
实际应用领域
让我们看看推荐系统在实际世界中的应用:
-
Netflix 上的电影有三分之二是推荐的。
-
亚马逊的三分之一的销售额来自推荐。
-
在 Google 新闻上,推荐引擎产生的点击率增加了 38%。
-
尝试预测用户对物品的偏好是基于其他物品的过去评分。
-
他们可以根据学生的需求和偏好为大学生推荐课程。
-
他们可以在在线求职门户网站上将简历与工作匹配。
现在,让我们尝试使用推荐引擎来解决一个现实世界的问题。
实际示例 - 创建推荐引擎
让我们构建一个可以向一群用户推荐电影的推荐引擎。我们将使用明尼苏达大学 GroupLens 研究小组收集的数据。
按照以下步骤:
- 首先,我们将导入相关的包:
import pandas as pd
import numpy as np
- 现在,让我们导入
user_id和item_id数据集:
df_reviews = pd.read_csv('reviews.csv')
df_movie_titles = pd.read_csv('movies.csv',index_col=False)
- 我们通过电影 ID 合并了这两个 DataFrame:
df = pd.merge(df_users, df_movie_titles, on='movieId')
在运行上述代码后,df DataFrame 的标题如下:

列的详细信息如下:
-
- userid:每个用户的唯一 ID
-
电影 id:每部电影的唯一 ID
-
rating:每部电影的评分从 1 到 5
-
timestamp:电影被评分的时间戳
-
title:电影的标题
-
genres:电影的流派
- 为了了解输入数据的摘要趋势,让我们使用
groupby按title和rating列计算每部电影的*均评分和评分次数:
。
- 现在让我们为推荐引擎准备数据。为此,我们将把数据集转换为一个具有以下特征的矩阵:
-
- 电影标题将成为列。
-
User_id将成为索引。 -
评分将是值。
我们将使用 DataFrame 的pivot_table函数来完成它:
movie_matrix = df.pivot_table(index='userId', columns='title', values='rating')
请注意,上述代码将生成一个非常稀疏的矩阵。
- 现在,让我们使用我们创建的推荐矩阵来推荐电影。为此,让我们考虑一个特定的用户,他观看了电影Avatar (2009)。首先,我们将找到所有对Avatar (2009)表现出兴趣的用户:
Avatar_user_rating = movie_matrix['Avatar (2009)']
Avatar_user_rating = Avatar_user_rating.dropna()
Avatar_user_rating.head()
- 现在,让我们尝试推荐与Avatar (2009)相关的电影。为此,我们将计算
Avatar_user_ratingDataFrame 与movie_matrix的相关性,如下所示:
similar_to_Avatar=movie_matrix.corrwith(Avatar_user_rating)
corr_Avatar = pd.DataFrame(similar_to_Avatar, columns=['correlation'])
corr_Avatar.dropna(inplace=True)
corr_Avatar = corr_Avatar.join(df_ratings['number_of_ratings'])
corr_Avatar.head()
这会产生以下输出:

这意味着我们可以将这些电影作为用户的推荐。
总结
在本章中,我们学习了推荐引擎。我们研究了根据我们试图解决的问题选择合适的推荐引擎。我们还研究了如何准备数据以创建相似性矩阵,以供推荐引擎使用。我们还学习了推荐引擎如何用于解决实际问题,比如根据用户过去的模式推荐电影。
在下一章中,我们将专注于用于理解和处理数据的算法。
第三部分:高级主题
正如名称所示,本节将涉及算法的更高级概念。密码学和大规模算法是本节的重点亮点。本节的最后一章,也是本书的最后一章,探讨了在实施这些算法时应该牢记的实际考虑因素。本节包括的章节有:
-
第十一章,数据算法
-
第十二章,密码学
-
第十三章,大规模算法
-
第十四章,实际考虑因素
第十一章:数据算法
本章讨论了数据中心算法的三个方面:存储、流式处理和压缩。本章首先简要概述了数据中心算法,然后讨论了可以用于数据存储的各种策略。接下来,描述了如何将算法应用于流式数据,然后讨论了压缩数据的不同方法。最后,我们将学习如何使用本章中开发的概念来使用最先进的传感器网络监测高速公路上行驶车辆的速度。
通过本章的学习,您应该能够理解设计各种数据中心算法所涉及的概念和权衡。
本章讨论了以下概念:
-
数据分类
-
数据存储算法
-
如何使用算法来压缩数据
-
如何使用算法来流式处理数据
让我们首先介绍基本概念。
数据算法简介
无论我们是否意识到,我们生活在一个大数据时代。只需了解一下不断产生的数据量,看一下谷歌在 2019 年发布的一些数字就可以了。众所周知,Google Photos 是谷歌创建的存储照片的多媒体库。2019 年,*均每天有 12 亿张照片和视频上传到 Google Photos。此外,每天*均有 400 小时的视频(相当于 1 PB 的数据)上传到 YouTube。我们可以肯定地说,正在产生的数据量简直是爆炸式增长。
当前对数据驱动算法的兴趣是因为数据包含有价值的信息和模式。如果以正确的方式使用,数据可以成为政策制定、营销、治理和趋势分析的基础。
由于处理数据的算法变得越来越重要,有明显的原因。设计能够处理数据的算法是一个活跃的研究领域。毫无疑问,探索最佳利用数据以提供一些可量化的好处是全世界各种组织、企业和政府的关注焦点。但原始形式的数据很少有用。要从原始数据中挖掘信息,需要对其进行处理、准备和分析。
为此,我们首先需要将其存储在某个地方。高效存储数据的方法变得越来越重要。请注意,由于单节点系统的物理存储限制,大数据只能存储在由高速通信链路连接的多个节点组成的分布式存储中。因此,对于学习数据算法来说,首先看不同的数据存储算法是有意义的。
首先,让我们将数据分类为各种类别。
数据分类
让我们来看看在设计数据算法的背景下如何对数据进行分类。正如在第二章中讨论的那样,算法中使用的数据结构,量化数据的容量、多样性和速度可以用来对其进行分类。这种分类可以成为设计数据算法的基础,用于其存储和处理。
让我们在数据算法的背景下逐个查看这些特征:
-
容量 量化了需要在算法中存储和处理的数据量。随着容量的增加,任务变得数据密集,并需要足够的资源来存储、缓存和处理数据。大数据是一个模糊定义的术语,用来描述无法由单个节点处理的大量数据。
-
速度定义了新数据生成的速率。通常,高速数据被称为“热数据”或“热流”,低速数据被称为“冷流”或简单地称为“冷数据”。在许多应用中,数据将是热流和冷流的混合,首先需要准备并合并到一个表中,然后才能与算法一起使用。
-
多样性指的是需要将不同类型的结构化和非结构化数据合并到一个表中,然后才能被算法使用。
下一节将帮助我们理解涉及的权衡,并在设计存储算法时提出各种设计选择。
数据存储算法介绍
可靠和高效的数据存储库是分布式系统的核心。如果这个数据存储库是为分析而创建的,那么它也被称为数据湖。数据存储库将来自不同领域的数据汇集到一个地方。让我们首先了解分布式存储库中与数据存储相关的不同问题。
了解数据存储策略
在数字计算的最初几年,设计数据存储库的常规方式是使用单节点架构。随着数据集的不断增大,数据的分布式存储现在已经成为主流。在分布式环境中存储数据的正确策略取决于数据的类型、预期的使用模式以及其非功能性需求。为了进一步分析分布式数据存储的需求,让我们从一致性可用性分区容忍(CAP)定理开始,这为我们提供了制定分布系统数据存储策略的基础。
介绍 CAP 定理
1998 年,Eric Brewer 提出了一个定理,后来被称为 CAP 定理。它突出了设计分布式存储系统涉及的各种权衡。
为了理解 CAP 定理,首先让我们定义分布式存储系统的以下三个特性:一致性、可用性和分区容忍。CAP 实际上是由这三个特性组成的首字母缩写:
-
一致性(简称 C):分布式存储由各种节点组成。这些节点中的任何一个都可以用于读取、写入或更新数据存储库中的记录。一致性保证在某个时间t[1],无论我们使用哪个节点来读取数据,我们都会得到相同的结果。每个读操作要么返回跨分布式存储库一致的最新数据,要么给出错误消息。
-
可用性(简称 A):可用性保证分布式存储系统中的任何节点都能立即处理请求,无论是否具有一致性。
-
分区容忍(简称 P):在分布式系统中,多个节点通过通信网络连接。分区容忍保证在一小部分节点(一个或多个)之间的通信失败的情况下,系统仍然可以正常运行。请注意,为了保证分区容忍,数据需要在足够数量的节点上复制。
使用这三种特性,CAP 定理仔细总结了分布系统的架构和设计中涉及的权衡。具体来说,CAP 定理规定,在存储系统中,我们只能拥有以下两种特性中的两种:一致性或 C,可用性或 A,以及分区容忍性或 P。
这在以下图表中显示:

CAP 定理也意味着我们可以有三种类型的分布式存储系统:
-
CA 系统(实现一致性-可用性)
-
AP 系统(实现可用性-分区容忍)
-
CP 系统(实现一致性-分区容忍)
让我们依次来看看它们。
CA 系统
传统的单节点系统是 CA 系统。这是因为如果我们没有分布式系统,那么我们就不需要担心分区容忍性。在这种情况下,我们可以拥有既有一致性又有可用性的系统,即 CA 系统。
传统的单节点数据库,如 Oracle 或 MySQL,都是 CA 系统的例子。
AP 系统
AP 系统是为可用性调整的分布式存储系统。设计为高度响应的系统,它们可以牺牲一致性,以适应高速数据。这意味着这些是设计为立即处理用户请求的分布式存储系统。典型的用户请求是读取或写入快速变化的数据。典型的 AP 系统用于实时监控系统,如传感器网络。
高速分布式数据库,如 Cassandra,是 AP 系统的良好例子。
让我们看看 AP 系统可以在哪些地方使用。如果加拿大交通部想要通过在渥太华一条高速公路上安装的传感器网络监控交通情况,建议使用 AP 系统来实现分布式数据存储。
CP 系统
CP 系统具有一致性和分区容忍性。这意味着这些是调整为在读取过程可以获取值之前保证一致性的分布式存储系统。
CP 系统的典型用例是当我们想要以 JSON 格式存储文档文件时。像 MongoDB 这样的文档数据存储系统是为分布式环境中的一致性而调整的 CP 系统。
分布式数据存储越来越成为现代 IT 基础设施中最重要的部分。分布式数据存储应该根据数据的特性和我们想要解决的问题的要求进行精心设计。将数据存储分类为 CA、AP 和 CP 系统有助于我们理解在设计数据存储系统时涉及的各种权衡。
现在,让我们来看看流数据算法。
呈现流数据算法
数据可以被分类为有界或无界。有界数据是静态数据,通常通过批处理过程处理。流式处理基本上是对无界数据进行数据处理。让我们看一个例子。假设我们正在分析银行的欺诈交易。如果我们想要查找 7 天前的欺诈交易,我们必须查看静态数据;这是一个批处理的例子。
另一方面,如果我们想要实时检测欺诈,那就是流式处理的一个例子。因此,流数据算法是处理数据流的算法。其基本思想是将输入数据流分成批次,然后由处理节点处理。流算法需要具有容错能力,并且应该能够处理数据的传入速度。随着对实时趋势分析的需求增加,对流处理的需求也在这些天增加。请注意,为了使流处理工作,数据必须快速处理,而在设计算法时,这一点必须始终牢记在心。
流应用
流数据及其有意义的利用有许多应用。一些应用如下:
-
欺诈检测
-
系统监控
-
智能订单路由
-
实时仪表板
-
高速公路上的交通传感器
-
信用卡交易
-
用户在多用户在线游戏中移动
现在,让我们看看如何使用 Python 实现流处理。
呈现数据压缩算法
数据压缩算法参与了减小数据大小的过程。
在这一章中,我们将深入研究一种名为无损压缩算法的特定数据压缩算法。
无损压缩算法
这些算法能够以一种可以在解压缩时不丢失信息的方式压缩数据。当重要的是在解压缩后检索到确切的原始文件时,它们被使用。无损压缩算法的典型用途如下:
-
压缩文件
-
压缩和打包源代码和可执行文件
-
将大量小文件转换为少量大文件
了解无损压缩的基本技术
数据压缩是基于这样一个原则,即大多数数据使用的位数比其熵所指示的最佳位数多。回想一下,熵是一个用来指定数据所携带信息的术语。这意味着可以有更优化的位表示相同信息。探索和制定更有效的位表示成为设计压缩算法的基础。无损数据压缩利用这种冗余来压缩数据而不丢失任何信息。在 80 年代后期,Ziv 和 Lempel 提出了基于字典的数据压缩技术,可以用于实现无损数据压缩。由于其速度和良好的压缩率,这些技术一炮而红。这些技术被用于创建流行的基于 Unix 的compress工具。此外,普遍存在的gif图像格式使用了这些压缩技术,因为它们可以用较少的位数表示相同的信息,节省了空间和通信带宽。这些技术后来成为开发zip实用程序及其变体的基础。调制解调器中使用的压缩标准 V.44 也是基于它。
现在让我们逐一查看即将到来的部分中的技术。
Huffman 编码
Huffman 编码是压缩数据的最古老方法之一,它基于创建 Huffman 树,用于对数据进行编码和解码。Huffman 编码可以通过利用某些数据(例如字母表的某些字符)在数据流中更频繁地出现这一事实,以更紧凑的形式表示数据内容。通过使用不同长度的编码(对于最常见的字符较短,对于最不常见的字符较长),数据占用的空间更少。
现在,让我们学习一些与 Huffman 编码相关的术语:
-
编码:在数据的上下文中,编码表示将数据从一种形式表示为另一种形式的方法。我们希望结果形式简洁。
-
码字:编码形式中的特定字符称为码字。
-
固定长度编码:每个编码字符,即码字,使用相同数量的位。
-
可变长度编码:码字可以使用不同数量的位。
-
代码评估:这是每个码字的预期位数。
-
前缀自由码:这意味着没有码字是任何其他码字的前缀。
-
解码:这意味着可变长度编码必须不受任何前缀的限制。
要理解最后两个术语,您首先需要查看此表:
| 字符 | 频率 | 固定长度编码 | 可变长度编码 |
|---|---|---|---|
| L | .45 | 000 | 0 |
| M | .13 | 001 | 101 |
| N | .12 | 010 | 100 |
| X | .16 | 011 | 111 |
| Y | .09 | 100 | 1101 |
| Z | .05 | 101 | 1100 |
现在,我们可以推断以下内容:
-
固定长度编码:该表的固定长度编码为 3。
-
可变长度编码:该表的可变长度编码为45(1) + .13(3) + .12(3) + .16(3) + .09(4) + .05(4) = 2.24。
以下图表显示了从上面的例子创建的 Huffman 树:

请注意,Huffman 编码是将数据转换为 Huffman 树以实现压缩。解码或解压缩将数据恢复到原始格式。
一个实际的例子- Twitter 实时情感分析
据说 Twitter 每秒有* 7000 条关于各种话题的推文。让我们尝试构建一个情感分析器,可以实时捕捉来自不同新闻来源的新闻的情绪。我们将从导入所需的包开始:
- 导入所需的包:
import tweepy,json,time
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
analyzer = SentimentIntensityAnalyzer()
请注意,我们使用以下两个包:
- VADER情感分析,代表Valence Aware Dictionary and Sentiment Reasoner。这是一种流行的基于规则的情感分析工具,专为社交媒体开发。如果您以前从未使用过它,那么您首先需要运行以下命令:
pip install vaderSentiment
Tweepy,这是一个用于访问 Twitter 的基于 Python 的 API。同样,如果您以前从未使用过它,您需要首先运行这个:
pip install Tweepy
- 下一步有点棘手。您需要向 Twitter 发出请求,创建一个开发者帐户,以获取对推文的实时流的访问权限。一旦您获得 API 密钥,您可以用以下变量表示它们:
twitter_access_token = <your_twitter_access_token>
twitter_access_token_secret = <your_twitter_access_token_secret>
twitter_consumer_key = <your_consumer_key>
twitter_consumer_secret = <your_twitter_consumer_secret>
- 然后,让我们配置
TweepyAPI 的身份验证。为此,我们需要提供先前创建的变量:
auth = tweepy.OAuthHandler(twitter_consumer_key, twitter_consumer_secret)
auth.set_access_token(twitter_access_token, twitter_access_token_secret)
api = tweepy.API(auth, parser=tweepy.parsers.JSONParser())
- 现在是有趣的部分。我们将选择我们想要监控情感分析的新闻来源的 Twitter 句柄。在这个例子中,我们选择了以下新闻来源:
news_sources = ("@BBC", "@ctvnews", "@CNN","@FoxNews", "@dawn_com")
- 现在,让我们创建主循环。这个循环将从一个名为
array_sentiments的空数组开始,用于保存情感。然后,我们将循环遍历所有五个新闻来源,并收集每个 100 条推文。然后,对于每条推文,我们将计算其极性:

- 现在,让我们创建一个图表,显示来自这些个别新闻来源的新闻的极性:

请注意,每个新闻来源都用不同的颜色表示。
- 现在,让我们看一下摘要统计信息:

上述数字总结了情感的趋势。例如,BBC 的情感被发现是最积极的,而加拿大新闻频道 CTVnews 似乎带有最消极的情绪。
摘要
在本章中,我们研究了以数据为中心的算法的设计。我们关注了数据为中心算法的三个方面:存储、压缩和流式处理。
我们研究了数据特征如何决定数据存储设计。我们研究了两种不同类型的数据压缩算法。然后,我们研究了一个实际示例,说明了如何使用数据流算法来计算文本数据流中的单词计数。
在下一章中,我们将研究加密算法。我们将学习如何利用这些算法的力量来保护交换和存储的消息。
第十二章:密码学
本章向您介绍了与密码学相关的算法。我们将首先介绍背景,然后讨论对称加密算法。然后我们将解释消息摘要 5(MD5)算法和安全哈希算法(SHA),并介绍实施对称算法的局限性和弱点。接下来,我们将讨论非对称加密算法以及它们用于创建数字证书的方式。最后,我们将提供一个总结所有这些技术的实际示例。
通过本章的学习,您将对与密码学相关的各种问题有一个基本的了解。
本章讨论以下主题:
-
密码学简介
-
了解密码学技术的类型
-
示例-部署机器学习模型时的安全问题
让我们先从基本概念开始。
密码学简介
保护秘密的技术已经存在了几个世纪。最早的保护和隐藏数据免受敌人侵害的尝试可以追溯到埃及的古代铭文,那里使用了只有少数信任的人知道的特殊字母表。这种早期的安全形式被称为模糊,今天仍然以不同的形式使用。为了使这种方法有效,保护字母表的含义至关重要。随后,在一战和二战中,寻找保护重要消息的绝对可靠方法变得非常重要。20 世纪末,随着电子和计算机的引入,开发了复杂的算法来保护数据,从而产生了一个全新的领域,称为密码学。本章讨论了密码学的算法方面。这些算法的目的是允许两个进程或用户之间进行安全数据交换。密码算法找到了使用数学函数来确保所述安全目标的策略。
了解最弱链接的重要性
有时,在构建数字基础设施的安全性时,我们过分强调个体实体的安全性,而忽视了端到端的安全性。这可能导致我们忽视系统中的一些漏洞和弱点,这些漏洞和弱点后来可能被黑客利用来获取敏感数据。要记住的重要一点是,整个数字基础设施的强度取决于它的最弱链接。对于黑客来说,这个最弱链接可以提供对数字基础设施中敏感数据的后门访问。在某个程度上,加固前门而不关闭所有后门并没有太多好处。
随着数字基础设施的算法和技术变得越来越复杂,攻击者也在不断升级他们的技术。要记住的一点是,攻击者入侵数字基础设施最简单的方法之一就是利用这些漏洞来获取敏感信息。
2014 年,对加拿大联邦研究机构——国家研究委员会(NRC)的网络进行了一次网络攻击,据估计造成了数亿美元的损失。攻击者能够窃取数十年的研究数据和知识产权材料。他们利用了网页服务器上使用的 Apache 软件中的一个漏洞来获取对敏感数据的访问权限。
在本章中,我们将重点介绍各种加密算法的漏洞。
让我们首先来看看使用的基本术语。
基本术语
让我们先来看看与密码学相关的基本术语:
-
密码:执行特定加密功能的算法。
-
明文:可以是文本文件、视频、位图或数字化语音的原始数据。在本章中,我们将表示明文为P。
-
密文:应用加密后得到的混乱文本。在本章中,我们将其表示为C。
-
密码套件:一组或套件的加密软件组件。当两个独立节点想要使用加密交换消息时,它们首先需要就密码套件达成一致。这对于确保它们使用完全相同的加密函数实现非常重要。
-
加密:将明文P转换为密文C的过程称为加密。在数学上,它表示为encrypt(P) = C。
-
解密:将密文转换回明文的过程。在数学上,它表示为decrypt(C) = P。
-
密码分析:用于分析加密算法强度的方法。分析人员试图在没有秘密访问的情况下恢复明文。
-
个人可识别信息(PII):PII 是指可以单独使用或与其他相关数据一起用于追踪个人身份的信息。一些例子包括受保护的信息,如社会安全号码、出生日期或母亲的婚前姓氏。
了解安全需求
首先了解系统的确切安全需求是很重要的。了解这一点将帮助我们使用正确的加密技术,并发现系统中的潜在漏洞。为了做到这一点,我们首先需要更好地了解系统的需求。为了了解安全需求,我们执行以下三个步骤:
-
识别实体。
-
确立安全目标。
-
了解数据的敏感性。
让我们逐一看看这些步骤。
识别实体
识别实体的一种方法是首先回答以下四个问题,这将帮助我们了解系统在安全环境中的需求:
-
哪些应用程序需要受到保护?
-
我们要保护应用程序免受谁的攻击?
-
我们应该在哪里保护它们?
-
我们为什么要保护它们?
一旦我们更好地了解这些要求,我们就可以确立我们数字系统的安全目标。
确立安全目标
通常使用加密算法来满足一个或多个安全目标:
-
认证:简单来说,认证是我们如何证明用户是其所声称的人的方式。通过认证的过程,我们确保用户的身份得到验证。认证的过程始于用户提供其身份。接着是提供只有用户知道的信息,因此只能由他们产生。
-
机密性:需要受到保护的数据称为敏感数据。机密性是将敏感数据限制为仅授权用户的概念。为了在传输或存储过程中保护敏感数据的机密性,您需要使数据变得不可读,除了授权用户之外。这是通过使用加密算法来实现的,我们将在本章后面讨论。
-
完整性:完整性是指建立数据在传输或存储过程中没有被任何方式改变的过程。例如,TCP/IP(传输控制协议/互联网协议)使用校验和或循环冗余校验(CRC)算法来验证数据的完整性。
-
不可否认性:不可否认性是指信息发送方收到数据已被接收的确认,接收方收到发送方身份的确认的概念。这提供了无可辩驳的证据,证明了消息的发送或接收,这可以在以后用来证明数据的接收和通信中的故障点。
了解数据的敏感性
了解数据的机密性很重要。我们还需要考虑数据泄露的后果有多严重。数据的分类有助于我们选择正确的加密算法。根据信息的敏感性,有多种分类数据的方式。让我们看看数据分类的典型方式:
-
公共数据或未分类数据:任何对公众可用的数据。例如,在公司网站或政府信息门户上找到的信息。
-
内部数据或机密数据:虽然不适合公开,但将这些数据暴露给公众可能不会造成破坏性后果。例如,如果员工的投诉经理的电子邮件被曝光,这可能会让公司尴尬,但可能不会造成破坏性后果。
-
敏感数据或机密数据:不应该公开的数据,如果暴露给公众,对个人或组织会造成破坏性后果。例如,泄露未来 iPhone 的细节可能会损害苹果的业务目标,并给三星等竞争对手带来优势。
-
高度敏感数据:也称为绝密数据。这是如果泄露将对组织造成极大损害的信息。这可能包括客户社会安全号码、信用卡号码或其他非常敏感的信息。绝密数据通过多层安全保护,并需要特别许可才能访问。
一般来说,更复杂的安全设计比简单的算法要慢得多。在安全性和系统性能之间取得正确的*衡非常重要。
了解密码的基本设计
设计密码是为了想出一种算法,可以混淆敏感数据,使恶意进程或未经授权的用户无法访问它。尽管随着时间的推移,密码变得越来越复杂,但密码基于的基本原理保持不变。
让我们从一些相对简单的密码开始,这将帮助我们理解加密算法设计中使用的基本原理。
呈现替换密码
替换密码在各种形式上已经使用了数百年。顾名思义,替换密码基于一个简单的概念——以预定的有序方式用其他字符替换明文中的字符。
让我们看看其中涉及的确切步骤:
-
首先,我们将每个字符映射到一个替代字符。
-
然后,通过替换映射,将明文编码并转换为密文,用密文中的另一个字符替换明文中的每个字符。
-
解码时,使用替换映射将明文还原。
让我们看一些例子:
- 凯撒密码:
在凯撒密码中,替换映射是通过用右边的第三个字符替换每个字符来创建的。这个映射在下图中描述:

让我们看看如何使用 Python 实现凯撒密码:
import string
rotation = 3
P = 'CALM'; C=''
for letter in P:
C = C+ (chr(ord(letter) + rotation))
我们可以看到我们对明文CALM应用了凯撒密码。
让我们用凯撒密码加密后打印密文:
据说凯撒密码曾被朱利叶斯·凯撒用来与他的顾问交流。
- 旋转 13(ROT13):
ROT13 是另一种基于替换的加密。在 ROT13 中,替换映射是通过用右边的第 13 个字符替换每个字符来创建的。以下图表说明了这一点:

这意味着如果ROT13()是实现 ROT13 的函数,那么以下内容适用:
import codecs
P = 'CALM'
C=''
C=codecs.encode(P, 'rot_13')
现在,让我们打印C的编码值:

- 替换密码的密码分析:
替换密码很容易实现和理解。不幸的是,它们也很容易破解。替换密码的简单密码分析表明,如果我们使用英语字母表,那么我们需要确定的是破解密码的旋转量。我们可以逐个尝试英语字母表的每个字母,直到我们能够解密文本。这意味着需要大约 25 次尝试才能重构明文。
现在,让我们看另一种简单密码—置换密码。
理解置换密码
在置换密码中,明文的字符被置换。让我们看一下其中涉及的步骤:
-
创建矩阵并选择置换矩阵的大小。它应该足够大,以适应明文字符串。
-
通过横向写入字符串的所有字符来填充矩阵。
-
在矩阵中垂直读取字符串的所有字符。
让我们看一个例子。
让我们以Ottawa Rocks明文(P)为例。
首先,让我们对P进行编码。为此,我们将使用一个 3 x 4 的矩阵,横向写入明文:
| O | t | t | a |
|---|---|---|---|
| w | a | R | o |
| c | k | s |
read过程将垂直读取字符串,这将生成密码文本—OwctaktRsao。
德国人在第一次世界大战中使用了一种名为 ADFGVX 的密码,它同时使用了置换和替换密码。多年后,它被 George Painvin 破解。
因此,这些是一些密码类型。现在,让我们看一些当前使用的密码技术。
理解密码技术的类型
不同类型的密码技术使用不同类型的算法,并在不同的情况下使用。
广义上,密码技术可以分为以下三种类型:
-
散列
-
对称
-
非对称
让我们逐个来看。
使用密码哈希函数
密码哈希函数是一种数学算法,可以用来创建消息的唯一指纹。它从明文中创建一个称为哈希的固定大小的输出。
从数学上看,这看起来是这样的:
C[1] = hashFunction(P[1])
这是解释如下的:
-
P[1] 是表示输入数据的明文。
-
C[1] 是由密码哈希函数生成的固定长度哈希。
这在下图中显示。可变长度数据通过单向哈希函数转换为固定长度哈希:

哈希函数具有以下五个特征:
-
它是确定性的。相同的明文生成相同的哈希。
-
唯一的输入字符串应该生成唯一的输出哈希值。
-
无论输入消息如何,它都具有固定长度。
-
明文中的微小变化会生成新的哈希。
-
它是一个单向函数,这意味着无法从密码文本C[1]生成明文P[1]。
如果我们遇到每个唯一消息没有唯一哈希的情况,我们称之为碰撞。也就是说,如果我们有两个文本P[1]和P[2],在碰撞的情况下,意味着hashFunction(P[1]) = hashFunction(P[2])。
无论使用的哈希算法如何,碰撞都是罕见的。否则,哈希将毫无用处。然而,对于一些应用,不能容忍碰撞。在这些情况下,我们需要使用一个更复杂但生成碰撞哈希值的可能性更小的哈希算法。
实现密码哈希函数
密码哈希函数可以通过使用各种算法来实现。让我们深入了解其中的两种。
理解 MD5 容忍
MD5 是由 Poul-Henning Kamp 于 1994 年开发的,用来替代 MD4。它生成 128 位哈希。MD5 是一个相对简单的算法,容易发生碰撞。在不能容忍碰撞的应用中,不应使用 MD5。
让我们看一个例子。为了在 Python 中生成 MD5 哈希,我们将使用passlib库,这是一个最流行的开源库之一,实现了 30 多种密码哈希算法。如果它还没有安装在您的设备上,请在 Jupyter 笔记本中使用以下代码安装它:
!pip install passlib
在 Python 中,我们可以按照以下方式生成 MD5 哈希:

请注意,MD5 生成 128 位的哈希。
如前所述,我们可以将生成的哈希用作原始文本的指纹,原始文本是myPassword。让我们看看如何在 Python 中实现这一点:

请注意,对myPassword字符串生成的哈希与原始哈希匹配,生成了一个True值。但是,一旦明文更改为myPassword2,它就返回了False。
现在,让我们来看另一个哈希算法——安全哈希算法(SHA)。
理解 SHA
SHA 是由国家标准与技术研究所(NIST)开发的。让我们看看如何使用 Python 来创建 SHA 算法的哈希:
from passlib.hash import sha512_crypt
sha512_crypt.using(salt = "qIo0foX5",rounds=5000).hash("myPassword")
请注意使用一个名为salt的参数。加盐是在哈希之前添加随机字符的过程。
运行这段代码将给我们带来以下结果:

请注意,当我们使用 SHA 算法时,生成的哈希是 512 字节。
加密哈希函数的应用
哈希函数用于在复制文件后检查文件的完整性。为了实现这一点,当文件从源复制到目的地(例如,从 Web 服务器下载时),相应的哈希也会被复制。这个原始哈希,h[original],充当了原始文件的指纹。复制文件后,我们再次从复制的文件版本生成哈希,即h[copied]。如果h[original] = h[copied]—也就是说,生成的哈希与原始哈希匹配—这验证了文件没有改变,并且在下载过程中没有丢失任何数据。我们可以使用任何加密哈希函数,比如 MD5 或 SHA,来为此目的生成哈希。
现在,让我们来看对称加密。
使用对称加密
在密码学中,密钥是一组数字,用于使用我们选择的算法对明文进行编码。在对称加密中,我们使用相同的密钥进行加密和解密。如果用于对称加密的密钥是K,那么对称加密的等式如下:
EK = C
这里,P是明文,C是密文。
对于解密,我们使用相同的密钥K将其转换回P:
DK = P
这个过程在下面的图表中显示:

现在,让我们看看如何在 Python 中使用对称加密。
编写对称加密
在本节中,我们将使用 Python 的cryptography包来演示对称加密。它是一个全面的包,实现了许多加密算法,比如对称密码和不同的消息摘要。第一次使用时,我们需要使用pip命令来安装它:
!pip install cryptography
安装完成后,我们现在可以使用该包来实现对称加密,如下所示:
- 首先,让我们导入我们需要的包:
import cryptography as crypt
from cryptography.fernet import Fernet
- 让我们生成密钥:

- 现在,让我们打开密钥:
file = open('mykey.key', 'wb')
file.write(key)
file.close()
- 使用密钥,现在让我们尝试加密消息:
file = open('mykey.key', 'rb')
key = file.read()
file.close()
- 现在,让我们使用相同的密钥解密消息:
from cryptography.fernet import Fernet
message = "Ottawa is really cold".encode()
f = Fernet(key)
encrypted = f.encrypt(message)
- 让我们解密消息并将其赋给一个名为
decrypt的变量:
decrypted = f.decrypt(encrypted)
- 现在让我们打印
decrypt变量,以验证我们是否能够得到相同的消息:

让我们看一些对称加密的优势。
对称加密的优势
尽管对称加密的性能取决于所使用的确切算法,但一般来说,它比非对称加密快得多。
对称加密的问题
当两个用户或进程计划使用对称加密进行通信时,它们需要使用安全通道交换密钥。这引发了以下两个问题:
-
密钥保护:如何保护对称加密密钥。
-
密钥分发:如何将对称加密密钥从源共享到目的地。
现在,让我们看一下非对称加密。
非对称加密
在 20 世纪 70 年代,非对称加密被设计出来以解决我们在前一节中讨论的对称加密的一些弱点。
非对称加密的第一步是生成两个看起来完全不同但在算法上相关的不同密钥。其中一个被选择为私钥,K[pr],另一个被选择为公钥,K[pu]。在数学上,我们可以表示如下:
EKpr = C
这里,P是明文,C是密文。
我们可以按以下方式解密:
DKpu = P
公钥应该被自由分发,私钥由密钥对的所有者保密。
基本原则是,如果使用其中一个密钥进行加密,解密的唯一方法是使用另一个密钥。例如,如果我们使用公钥加密数据,我们将需要使用另一个密钥来解密它,即私钥。现在,让我们看一下非对称加密的一个基本协议——安全套接字层(SSL)/传输层安全性(TLS)握手,它负责使用非对称加密在两个节点之间建立连接。
SSL/TLS 握手算法
SSL 最初是为 HTTP 添加安全性而开发的。随着时间的推移,SSL 被更高效、更安全的协议 TLS 所取代。TLS 握手是 HTTP 创建安全通信会话的基础。TLS 握手发生在两个参与实体——客户端和服务器之间。此过程如下图所示:

TLS 握手在参与节点之间建立了安全连接。以下是涉及此过程的步骤:
- 客户端向服务器发送一个“客户端 hello”消息。消息还包含以下内容:
-
所使用的 TLS 版本
-
客户端支持的密码套件列表
-
一个压缩算法
-
一个由
byte_client标识的随机字节字符串
- 服务器向客户端发送一个“服务器 hello”消息。消息还包含以下内容:
-
服务器从客户端提供的列表中选择的密码套件
-
一个会话 ID
-
一个由
byte_server标识的随机字节字符串 -
包含服务器公钥的服务器数字证书,由
cert_server标识 -
如果服务器需要客户端身份验证的数字证书或客户端证书请求,客户端服务器请求还包括以下内容:
-
可接受的 CA 的可区分名称
-
支持的证书类型
-
客户端验证
cert_server。 -
客户端生成一个随机的字节字符串,由
byte_client2标识,并使用服务器通过cert_server提供的公钥进行加密。 -
客户端生成一个随机的字节字符串,并用自己的私钥进行加密。
-
服务器验证客户端证书。
-
客户端向服务器发送一个使用秘密密钥加密的“完成”消息。
-
为了从服务器端确认这一点,服务器向客户端发送一个使用秘密密钥加密的“完成”消息。
-
服务器和客户端现在建立了一个安全通道。他们现在可以交换使用共享秘密密钥对称加密的消息。整个方法如下所示:

现在,让我们讨论如何使用非对称加密来创建公钥基础设施(PKI),PKI 是为了满足组织的一个或多个安全目标而创建的。
公钥基础设施
非对称加密用于实现 PKI。PKI 是管理组织加密密钥的最流行和可靠的方式之一。所有参与者都信任一个名为 CA 的中央信任机构。CA 验证个人和组织的身份,然后为他们颁发数字证书(数字证书包含个人或组织的公钥副本和其身份),验证与该个人或组织相关联的公钥实际上属于该个人或组织。
它的工作方式是 CA 要求用户证明其身份,对个人和组织遵循不同的标准。这可能涉及简单地验证域名的所有权,也可能涉及更严格的过程,包括身份的物理证明,这取决于用户试图获得的数字证书的类型。如果 CA 确信用户确实是他们声称的人,用户随后通过安全通道向 CA 提供他们的公共加密密钥。CA 使用这些信息创建包含用户身份和他们的公钥信息的数字证书。该证书由 CA 数字签名。用户随后可以向任何想要验证其身份的人展示其证书,而无需通过安全通道发送它,因为证书本身不包含任何敏感信息。接收证书的人不必直接验证用户的身份。该人只需验证证书是否有效,验证 CA 的数字签名,以验证证书中包含的公钥实际上属于证书上命名的个人或组织。
组织的 CA 的私钥是 PKI 信任链中最薄弱的环节。例如,如果冒名顶替者获取了微软的私钥,他们可以通过冒充 Windows 更新在全球数百万台计算机上安装恶意软件。
示例-部署机器学习模型时的安全问题
在第六章中,无监督机器学习算法,我们看了CRISP-DM(跨行业标准数据挖掘过程)生命周期,该生命周期指定了训练和部署机器学习模型的不同阶段。一旦模型被训练和评估,最后阶段是部署。如果这是一个关键的机器学习模型,那么我们希望确保它的所有安全目标都得到满足。
让我们分析部署这样一个模型时面临的常见挑战,以及如何使用本章讨论的概念来解决这些挑战。我们将讨论保护我们训练好的模型免受以下三个挑战的策略:
-
中间人(MITM)攻击
-
冒充
-
数据篡改
让我们逐个来看。
中间人攻击
我们希望保护我们的模型免受的可能攻击之一是中间人攻击。中间人攻击发生在入侵者试图窃听假定为私人通信的情况下,部署训练好的机器学习模型。
让我们尝试使用一个示例场景来顺序理解中间人攻击。
假设鲍勃和爱丽丝想要使用 PKI 交换消息:
-
鲍勃使用{Pr[Bob],Pu[Bob]},爱丽丝使用{Pr[Alice],Pu[Alice]}。鲍勃创建了消息M[Bob],爱丽丝创建了消息M[Alice]。他们希望以安全的方式彼此交换这些消息。
-
最初,他们需要交换他们的公钥以建立彼此之间的安全连接。 这意味着鲍勃在发送消息给艾丽斯之前使用Pu[Alice]加密M[Bob]。
-
假设我们有一个窃听者X,他正在使用{Pr[X],Pu[X]}。 攻击者能够拦截鲍勃和艾丽斯之间的公钥交换,并用自己的公共证书替换它们。
-
鲍勃将M[Bob]发送给艾丽斯,使用Pu[X]而不是Pu[Alice]进行加密,错误地认为这是艾丽斯的公共证书。 窃听者X拦截了通信。 它拦截了M[Bob]消息并使用Pr[Bob]解密。
这种中间人攻击显示在以下图表中:

现在,让我们看看如何防止中间人攻击。
如何防止中间人攻击
让我们探讨如何通过引入 CA 来防止中间人攻击到组织中。 假设这个 CA 的名字是 myTrustCA。 数字证书中嵌入了它的公钥,名为Pu[myTrustCA]。 myTrustCA 负责为组织中的所有人,包括艾丽斯和鲍勃签署证书。 这意味着鲍勃和艾丽斯的证书都由 myTrustCA 签署。 在签署他们的证书时,myTrustCA 验证他们确实是他们声称的人。
现在,有了这个新的安排,让我们重新审视鲍勃和艾丽斯之间的顺序交互:
-
鲍勃正在使用{Pr[Bob],Pu[Bob]},艾丽斯正在使用{Pr[Alice],Pu[Alice]}。 他们的公钥都嵌入到他们的数字证书中,由 myTrustCA 签名。 鲍勃创建了一条消息M[Bob],艾丽斯创建了一条消息M[Alice]。 他们希望以安全的方式互相交换这些消息。
-
他们交换他们的数字证书,其中包含他们的公钥。 只有在证书中嵌入的公钥由他们信任的 CA 签署时,他们才会接受这些公钥。 他们需要交换他们的公钥以建立彼此之间的安全连接。 这意味着鲍勃将使用Pu**[Alice]来加密M**[Bob],然后将消息发送给艾丽斯。
-
假设我们有一个窃听者X,他正在使用{Pr[X],Pu[X]}。 攻击者能够拦截鲍勃和艾丽斯之间的公钥交换,并用自己的公共证书Pu[X]替换它们。
-
鲍勃拒绝X的尝试,因为坏人的数字证书没有被鲍勃信任的 CA 签名。 安全握手被中止,尝试的攻击被记录下来,并且引发了安全异常。
在部署训练好的机器学习模型时,不是艾丽斯,而是一个部署服务器。 鲍勃只有在建立安全通道后才能部署模型,使用先前提到的步骤。
让我们看看如何在 Python 中实现这一点。
首先让我们导入所需的包。
from xmlrpc.client import SafeTransport, ServerProxy
import ssl
现在让我们创建一个可以验证证书的类。
class CertVerify(SafeTransport):
def __init__(self, cafile, certfile=None, keyfile=None):
SafeTransport.__init__(self)
self._ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
self._ssl_context.load_verify_locations(cafile)
if cert:
self._ssl_context.load_cert_chain(certfile, keyfile)
self._ssl_context.verify_mode = ssl.CERT_REQUIRED
def make_connection(self, host):
s = super().make_connection((host, {'context': self._ssl_context}))
return s
# Create the client proxy
s = ServerProxy('https://cloudanum.com:15000', transport=VerifyCertSafeTransport('server_cert.pem'), allow_none=True)
让我们看看我们部署的模型可能面临的其他漏洞。
避免伪装
攻击者X假装成授权用户鲍勃,并获得对敏感数据的访问权限,这在这种情况下是训练模型。 我们需要保护模型免受任何未经授权的更改。
保护我们训练模型免受伪装的一种方法是使用授权用户的私钥对模型进行加密。 一旦加密,任何人都可以通过解密授权用户的公钥来读取和利用模型,这在他们的数字证书中找到。 没有人可以对模型进行任何未经授权的更改。
数据和模型加密
一旦模型部署,提供给模型作为输入的实时未标记数据也可能被篡改。训练好的模型用于推断并为这些数据提供标签。为了防止数据被篡改,我们需要保护静态数据和通信中的数据。为了保护静态数据,可以使用对称加密进行编码。可以建立基于 SSL/TLS 的安全通道来传输数据,以提供安全的隧道。这个安全隧道可以用来传输对称密钥,并且数据可以在提供给训练好的模型之前在服务器上解密。
这是保护数据免受篡改的更有效和可靠的方法之一。
在将模型部署到服务器之前,也可以使用对称加密对模型进行加密。这将防止在部署之前未经授权访问模型。
让我们看看如何使用以下步骤在源处使用对称加密加密训练好的模型,然后在目的地解密它,然后再使用它:
- 让我们首先使用鸢尾花数据集训练一个简单的模型:
import cryptography as crypt
from sklearn.linear_model
import LogisticRegression
from cryptography.fernet
import Fernet from sklearn.model_selection
import train_test_split
from sklearn.datasets import load_iris
iris = load_iris()
X = iris.data
y = iris.target
X_train, X_test, y_train, y_test = train_test_split(X, y)
model = LogisticRegression()
model.fit(X_train, y_train)
- 现在,让我们定义将存储模型的文件的名称:
filename_source = 'myModel_source.sav'
filename_destination = "myModel_destination.sav"
filename_sec = "myModel_sec.sav"
请注意,filename_source 是将在源处存储训练好的未加密模型的文件。filename_destination 是将在目的地存储训练好的未加密模型的文件,filename_sec 是加密的训练好的模型。
- 我们将使用
pickle将训练好的模型存储在文件中:
from pickle import dump dump(model, open(filename_source, 'wb'))
- 让我们定义一个名为
write_key()的函数,它将生成一个对称密钥并将其存储在名为key.key的文件中:
def write_key():
key = Fernet.generate_key()
with open("key.key", "wb") as key_file:
key_file.write(key)
- 现在,让我们定义一个名为
load_key()的函数,它可以从key.key文件中读取存储的密钥:
def load_key():
return open("key.key", "rb").read()
- 接下来,让我们定义一个
encrypt()函数,它可以加密和训练模型,并将其存储在名为filename_sec的文件中:
def encrypt(filename, key):
f = Fernet(key)
with open(filename_source,"rb") as file:
file_data = file.read()
encrypted_data = f.encrypt(file_data)
with open(filename_sec,"wb") as file:
file.write(encrypted_data)
- 我们将使用这些函数生成对称密钥并将其存储在文件中。然后,我们将读取此密钥并使用它将我们的训练好的模型存储在名为
filename_sec的文件中:
write_key()
encrypt(filename_source,load_key())
现在模型已经加密。它将被传输到目的地,在那里将用于预测。
- 首先,我们将定义一个名为
decrypt()的函数,我们可以使用它来使用存储在key.key文件中的密钥将模型从filename_sec解密到filename_destination:
def decrypt(filename, key):
f = Fernet(key)
with open(filename_sec, "rb") as file:
encrypted_data = file.read()
decrypted_data = f.decrypt(encrypted_data)
with open(filename_destination, "wb") as file: file.write(decrypted_data)
- 现在让我们使用这个函数来解密模型并将其存储在名为
filename_destination的文件中:
decrypt(filename_sec,load_key())
- 现在让我们使用这个未加密的文件来加载模型并用于预测:

请注意,我们已经使用对称加密对模型进行了编码。如果需要,可以使用相同的技术来加密数据。
摘要
在本章中,我们学习了加密算法。我们首先确定了问题的安全目标。然后讨论了各种加密技术,还研究了 PKI 基础设施的细节。最后,我们研究了不同的方法来保护训练好的机器学习模型免受常见攻击。现在,您应该能够理解用于保护现代 IT 基础设施的安全算法的基本原理。
在下一章中,我们将研究设计大规模算法。我们将研究设计和选择大型算法涉及的挑战和权衡。我们还将研究使用 GPU 和集群来解决复杂问题。
第十三章:大规模算法
大规模算法旨在解决庞大的复杂问题。大规模算法的特征是由于其数据规模和处理要求的缘故,需要多个执行引擎。本章首先讨论了什么类型的算法最适合并行运行。然后,讨论了与并行化算法相关的问题。接下来,介绍了计算统一设备架构(CUDA)架构,并讨论了如何使用单个图形处理单元(GPU)或一组 GPU 来加速算法。还讨论了需要对算法进行哪些更改才能有效利用 GPU 的性能。最后,本章讨论了集群计算,并讨论了 Apache Spark 如何创建弹性分布式数据集(RDDs)以创建标准算法的极快并行实现。
在本章结束时,您将能够理解与设计大规模算法相关的基本策略。
本章涵盖了以下主题:
-
大规模算法介绍
-
并行算法的设计
-
利用 GPU 的算法
-
利用集群计算理解算法
-
如何利用 GPU 运行大规模算法
-
如何利用集群的能力运行大规模算法
让我们从介绍开始。
大规模算法介绍
人类喜欢受到挑战。几个世纪以来,各种人类创新使我们能够以不同的方式解决真正复杂的问题。从预测蝗虫袭击的下一个目标区域到计算最大的质数,为我们周围的复杂问题提供答案的方法不断发展。随着计算机的出现,我们发现了一种强大的解决复杂算法的新方法。
定义良好的大规模算法
良好设计的大规模算法具有以下两个特征:
-
它旨在使用现有资源池最佳地处理大量数据和处理需求。
-
它是可扩展的。随着问题变得更加复杂,它可以通过提供更多资源来处理复杂性。
实现大规模算法的一种最实用的方法是使用分而治之的策略,即将较大的问题分解为可以独立解决的较小问题。
术语
让我们来看看一些用于量化大规模算法质量的术语。
延迟
延迟是执行单个计算所需的端到端时间。如果Compute[1]表示从t[1]开始到t[2]结束的单个计算,则我们可以说以下内容:
延迟 = t[2]-t[1]
吞吐量
在并行计算的背景下,吞吐量是可以同时执行的单个计算的数量。例如,如果在t[1]时,我们可以同时执行四个计算,C[1],C[2],C[3]和C[4],那么吞吐量为四。
网络双分带宽
网络中两个相等部分之间的带宽称为网络双分带宽。对于分布式计算要有效工作,这是最重要的参数。如果我们没有足够的网络双分带宽,分布式计算中多个执行引擎的可用性带来的好处将被慢速通信链路所掩盖。
弹性
基础设施对突然增加的处理需求做出反应并通过提供更多资源来满足需求的能力称为弹性。
三大云计算巨头,谷歌、亚马逊和微软可以提供高度弹性的基础设施。由于它们共享资源池的巨大规模,很少有公司有潜力与这三家公司的基础设施弹性相匹敌。
如果基础设施是弹性的,它可以为问题创建可扩展的解决方案。
并行算法的设计
重要的是要注意,并行算法并不是万能的。即使设计最好的并行架构也可能无法达到我们期望的性能。广泛使用的一个定律来设计并行算法是安达尔定律。
安达尔定律
Gene Amdahl 是 20 世纪 60 年代研究并行处理的第一批人之一。他提出了安达尔定律,这个定律至今仍然适用,并可以成为理解设计并行计算解决方案时涉及的各种权衡的基础。安达尔定律可以解释如下:
它基于这样一个概念,即在任何计算过程中,并非所有过程都可以并行执行。将会有一个无法并行化的顺序部分。
让我们看一个具体的例子。假设我们想要读取存储在计算机上的大量文件,并使用这些文件中的数据训练机器学习模型。
整个过程称为 P。很明显,P 可以分为以下两个子过程:
-
P1:扫描目录中的文件,创建与输入文件匹配的文件名列表,并传递它。
-
P2:读取文件,创建数据处理管道,处理文件并训练模型。
进行顺序过程分析
运行P的时间由T[seq]**(P)表示。运行P1和P2的时间由Tseq和Tseq表示。很明显,当在单个节点上运行时,我们可以观察到两件事:
-
P2在P1完成之前无法开始运行。这由P1 --> P2表示
-
Tseq = Tseq + Tseq
假设 P 在单个节点上运行需要 10 分钟。在这 10 分钟中,P1 需要 2 分钟运行,P2 需要 8 分钟在单个节点上运行。如下图所示:

现在要注意的重要事情是P1的性质是顺序的。我们不能通过并行化来加快它。另一方面,P2可以很容易地分成可以并行运行的并行子任务。因此,我们可以通过并行运行它来加快运行速度。
使用云计算的主要好处是拥有大量资源池,其中许多资源可以并行使用。使用这些资源解决问题的计划称为执行计划。安达尔定律被广泛用于识别给定问题和资源池的瓶颈。
进行并行执行分析
如果我们想要使用多个节点加速P,它只会影响P2,乘以一个大于 1 的因子s>1:

过程 P 的加速可以很容易地计算如下:

进程的可并行部分与其总体的比例由b表示,并计算如下:

例如,在前面的情景中,b = 8/10 = 0.8。
简化这些方程将给我们安达尔定律:

在这里,我们有以下内容:
-
P是整个过程。
-
b是P的可并行部分的比例。
-
s是在P的可并行部分实现的加速。
假设我们计划在三个并行节点上运行过程 P:
-
P1是顺序部分,不能通过使用并行节点来减少。它将保持在 2 秒。
-
P2现在需要 3 秒而不是 9 秒。
因此,P的总运行时间减少到 5 秒,如下图所示:

在前面的例子中,我们可以计算以下内容:
-
n[p] = 处理器的数量 = 3
-
b = 并行部分 = 9/11 = 81.81%
-
s = 速度提升 = 3
现在,让我们看一个典型的图表,解释阿姆达尔定律:

在前面的图表中,我们绘制了不同b值的s和n[p]之间的图表。
理解任务粒度
当我们并行化算法时,一个更大的任务被分成多个并行任务。确定任务应该被分成的最佳并行任务数量并不总是直截了当的。如果并行任务太少,我们将无法从并行计算中获得太多好处。如果任务太多,那么将会产生太多的开销。这也是一个被称为任务粒度的挑战。
负载*衡
在并行计算中,调度程序负责选择执行任务的资源。在没有实现最佳负载*衡的情况下,资源无法充分利用。
局部性问题
在并行处理中,应该避免数据的移动。在可能的情况下,应该在数据所在的节点上本地处理数据,否则会降低并行化的质量。
在 Python 中启用并发处理
在 Python 中启用并行处理的最简单方法是克隆一个当前进程,这将启动一个名为子进程的新并发进程。
Python 程序员,虽然不是生物学家,但已经创造了他们自己的克隆过程。就像克隆的羊一样,克隆副本是原始过程的精确副本。
制定多资源处理策略
最初,大规模算法是在称为超级计算机的巨大机器上运行的。这些超级计算机共享相同的内存空间。资源都是本地的——物理上放置在同一台机器上。这意味着各种处理器之间的通信非常快,它们能够通过共同的内存空间共享相同的变量。随着系统的发展和运行大规模算法的需求增长,超级计算机演变成了分布式共享内存(DSM),其中每个处理节点都拥有一部分物理内存。最终,发展出了松散耦合的集群,依赖处理节点之间的消息传递。对于大规模算法,我们需要找到多个并行运行的执行引擎来解决复杂的问题:

有三种策略可以拥有多个执行引擎:
-
向内寻找:利用计算机上已有的资源。使用 GPU 的数百个核心来运行大规模算法。
-
向外寻找:使用分布式计算来寻找更多的计算资源,这些资源可以共同用于解决手头的大规模问题。
-
混合策略:使用分布式计算,并在每个节点上使用 GPU 或 GPU 阵列来加速算法的运行。
介绍 CUDA
GPU 最初是为图形处理而设计的。它们被设计来满足处理典型计算机的多媒体数据的优化需求。为此,它们开发了一些特性,使它们与 CPU 有所不同。例如,它们有成千上万的核心,而 CPU 核心数量有限。它们的时钟速度比 CPU 慢得多。GPU 有自己的 DRAM。例如,Nvidia 的 RTX 2080 有 8GB 的 RAM。请注意,GPU 是专门的处理设备,没有通用处理单元的特性,包括中断或寻址设备的手段,例如键盘和鼠标。以下是 GPU 的架构:

GPU 成为主流后不久,数据科学家开始探索 GPU 在高效执行并行操作方面的潜力。由于典型的 GPU 具有数千个 ALU,它有潜力产生数千个并发进程。这使得 GPU 成为优化数据并行计算的架构。因此,能够执行并行计算的算法最适合于 GPU。例如,在视频中进行对象搜索,GPU 的速度至少比 CPU 快 20 倍。图算法在第五章 图算法中讨论过,已知在 GPU 上比在 CPU 上运行得快得多。
为了实现数据科学家充分利用 GPU 进行算法的梦想,Nvidia 在 2007 年创建了一个名为 CUDA 的开源框架,全称为 Compute Unified Device Architecture。CUDA 将 CPU 和 GPU 的工作抽象为主机和设备。主机,即 CPU,负责调用设备,即 GPU。CUDA 架构有各种抽象层,可以表示为以下形式:

请注意,CUDA 在 Nvidia 的 GPU 上运行。它需要在操作系统内核中得到支持。最*,Windows 现在也得到了全面支持。然后,我们有 CUDA Driver API,它充当编程语言 API 和 CUDA 驱动程序之间的桥梁。在顶层,我们支持 C、C+和 Python。
在 CUDA 上设计并行算法
让我们更深入地了解 GPU 如何加速某些处理操作。我们知道,CPU 设计用于顺序执行数据,这导致某些类别的应用程序运行时间显著增加。让我们以处理尺寸为 1,920 x 1,200 的图像为例。可以计算出有 2,204,000 个像素需要处理。顺序处理意味着在传统 CPU 上处理它们需要很长时间。像 Nvidia 的 Tesla 这样的现代 GPU 能够产生惊人数量的 2,204,000 个并行线程来处理像素。对于大多数多媒体应用程序,像素可以独立地进行处理,并且会实现显著加速。如果我们将每个像素映射为一个线程,它们都可以在 O(1)常数时间内进行处理。
但图像处理并不是唯一可以利用数据并行性加速处理的应用。数据并行性可以用于为机器学习库准备数据。事实上,GPU 可以大大减少可并行化算法的执行时间,包括以下内容:
-
为比特币挖矿
-
大规模模拟
-
DNA 分析
-
视频和照片分析
GPU 不适用于单程序,多数据(SPMD)。例如,如果我们想要计算一块数据的哈希值,这是一个无法并行运行的单个程序。在这种情况下,GPU 的性能会较慢。
我们想要在 GPU 上运行的代码使用特殊的 CUDA 关键字标记为内核。这些内核用于标记我们打算在 GPU 上并行处理的函数。基于这些内核,GPU 编译器分离出需要在 GPU 和 CPU 上运行的代码。
在 Python 中使用 GPU 进行数据处理
GPU 在多维数据结构的数据处理中非常出色。这些数据结构本质上是可并行化的。让我们看看如何在 Python 中使用 GPU 进行多维数据处理:
- 首先,让我们导入所需的 Python 包:
import numpy as np
import cupy as cp
import time
-
我们将使用 NumPy 中的多维数组,这是一个传统的使用 CPU 的 Python 包。
-
然后,我们使用 CuPy 数组创建一个多维数组,它使用 GPU。然后,我们将比较时间:
### Running at CPU using Numpy
start_time = time.time()
myvar_cpu = np.ones((800,800,800))
end_time = time.time()
print(end_time - start_time)
### Running at GPU using CuPy
start_time = time.time()
myvar_gpu = cp.ones((800,800,800))
cp.cuda.Stream.null.synchronize()
end_time = time.time()
print(end_time - start_time)
如果我们运行这段代码,它将生成以下输出:

请注意,使用 NumPy 创建此数组大约需要 1.13 秒,而使用 CuPy 只需要大约 0.012 秒,这使得在 GPU 中初始化此数组的速度快了 92 倍。
集群计算
集群计算是实现大规模算法并行处理的一种方式。在集群计算中,我们有多个通过高速网络连接的节点。大规模算法被提交为作业。每个作业被分成各种任务,并且每个任务在单独的节点上运行。
Apache Spark 是实现集群计算的最流行方式之一。在 Apache Spark 中,数据被转换为分布式容错数据集,称为Resilient Distributed Datasets(RDDs)。RDDs 是 Apache Spark 的核心抽象。它们是不可变的元素集合,可以并行操作。它们被分割成分区,并分布在节点之间,如下所示:

通过这种并行数据结构,我们可以并行运行算法。
在 Apache Spark 中实现数据处理
让我们看看如何在 Apache Spark 中创建 RDD 并在整个集群上运行分布式处理:
- 为此,首先,我们需要创建一个新的 Spark 会话,如下所示:
from pyspark.sql import SparkSession
spark = SparkSession.builder.appName('cloudanum').getOrCreate()
- 一旦我们创建了一个 Spark 会话,我们就可以使用 CSV 文件作为 RDD 的来源。然后,我们将运行以下函数-它将创建一个被抽象为名为
df的 DataFrame 的 RDD。在 Spark 2.0 中添加了将 RDD 抽象为 DataFrame 的功能,这使得处理数据变得更加容易:
df = spark.read.csv('taxi2.csv',inferSchema=True,header=True)
让我们来看看 DataFrame 的列:

- 接下来,我们可以从 DataFrame 创建一个临时表,如下所示:
df.createOrReplaceTempView("main")
- 一旦临时表创建完成,我们就可以运行 SQL 语句来处理数据:

需要注意的重要一点是,尽管它看起来像一个常规的 DataFrame,但它只是一个高级数据结构。在幕后,它是将数据分布到整个集群的 RDD。同样,当我们运行 SQL 函数时,在幕后,它们被转换为并行转换器和减少器,并充分利用集群的能力来处理代码。
混合策略
越来越多的人开始使用云计算来运行大规模算法。这为我们提供了结合向外看和向内看策略的机会。这可以通过在多个虚拟机中配置一个或多个 GPU 来实现,如下面的屏幕截图所示:

充分利用混合架构是一项非常重要的任务。首先将数据分成多个分区。在每个节点上并行化需要较少数据的计算密集型任务在 GPU 上进行。
总结
在本章中,我们研究了并行算法的设计以及大规模算法的设计问题。我们研究了使用并行计算和 GPU 来实现大规模算法。我们还研究了如何使用 Spark 集群来实现大规模算法。
在本章中,我们了解了与大规模算法相关的问题。我们研究了与并行化算法相关的问题以及在此过程中可能产生的潜在瓶颈。
在下一章中,我们将探讨实现算法的一些实际方面。
第十四章:实际考虑
本书中介绍的一堆算法可以用于解决现实世界问题。本章是关于本书中介绍的算法的一些实际考虑。
本章的组织如下。我们将从介绍开始。然后,我们将介绍算法可解释性的重要主题,即算法内部机制能否以可理解的方式解释的程度。接下来,我们将介绍使用算法的道德和在实施时可能产生偏见的可能性。然后讨论处理 NP 难问题的技术。最后,我们将探讨在选择算法之前应考虑的因素。
在本章结束时,您将了解在使用算法时需要牢记的实际考虑。
在本章中,我们将涵盖以下主题:
-
介绍实际考虑
-
算法的可解释性
-
理解伦理和算法
-
减少模型中的偏见
-
解决 NP 难问题
-
何时使用算法
让我们从介绍开始,
介绍实际考虑
除了设计、开发和测试算法外,在许多情况下,考虑开始依赖机器解决现实世界问题的某些实际方面也很重要,因为这会使解决方案更有用。对于某些算法,我们可能需要考虑可靠地整合预计会在部署算法后继续变化的新重要信息的方法。整合这些新信息会以任何方式改变我们经过良好测试的算法的质量吗?如果是,我们的设计如何处理?然后,对于一些使用全局模式的算法,我们可能需要关注捕捉全球地缘政治局势变化的实时参数。此外,在某些用例中,我们可能需要考虑在使用时强制执行的监管政策,以使解决方案有用。
当我们使用算法解决现实世界问题时,我们在某种程度上依赖机器进行问题解决。即使是最复杂的算法也是基于简化和假设的,并且无法处理意外情况。我们甚至还远远没有完全将关键决策交给我们设计的算法。
例如,谷歌设计的推荐引擎算法最*面临欧盟的监管限制,原因是隐私问题。这些算法可能是其领域中最先进的。但如果被禁止,这些算法实际上可能会变得无用,因为它们无法用于解决它们本应解决的问题。
事实上,不幸的是,算法的实际考虑仍然是在初始设计阶段通常不考虑的事后想法。对于许多用例来说,一旦算法部署并且提供解决方案的短期激动感过去后,使用算法的实际方面和影响将随着时间的推移被发现,并将定义项目的成功或失败。
让我们看一个实际例子,其中不注意实际考虑导致了一家世界顶尖 IT 公司设计的备受关注的项目失败。
一个 AI Twitter 机器人的悲伤故事
让我们来看看 Tay 的经典例子,它是微软于 2016 年创建的第一个 AI Twitter 机器人。由 AI 算法操作,Tay 应该从环境中学习并不断改进自己。不幸的是,在网络空间生活了几天后,Tay 开始从不断发出的种族主义和粗鲁的推文中学习。它很快开始发表冒犯性的推文。尽管它表现出了智能,并迅速学会根据实时事件创建定制推文,但同时,它严重冒犯了人们。微软将其下线并尝试重新调整,但没有成功。微软最终不得不终止该项目。这是一个雄心勃勃的项目的悲伤结局。
请注意,尽管微软内置的智能令人印象深刻,但该公司忽视了部署自学习 Twitter 机器人的实际影响。NLP 和机器学习算法可能是最好的,但由于明显的缺陷,这实际上是一个无用的项目。如今,Tay 已成为忽视允许算法在飞行中学习的实际影响而导致失败的典型案例。Tay 的失败所带来的教训肯定影响了后来几年的 AI 项目。数据科学家也开始更加关注算法的透明度。这将引出下一个主题,探讨使算法透明的需求和方法。
算法的可解释性
黑匣子算法是指其逻辑由于复杂性或逻辑以混乱的方式表示而无法被人类解释的算法。另一方面,白匣子算法是指其逻辑对人类可见和可理解的算法。换句话说,可解释性帮助人类大脑理解算法为何给出特定结果。可解释性的程度是特定算法对人类大脑可理解的程度。许多类别的算法,特别是与机器学习相关的算法,被归类为黑匣子。如果算法用于关键决策,了解算法产生结果的原因可能很重要。将黑匣子算法转换为白匣子算法还可以更好地了解模型的内部工作。可解释的算法将指导医生哪些特征实际上被用来将患者分类为患病或非患病。如果医生对结果有任何疑问,他们可以回头检查这些特定特征的准确性。
机器学习算法和可解释性
算法的可解释性对于机器学习算法非常重要。在许多机器学习应用中,用户被要求相信模型能帮助他们做出决策。在这种情况下,可解释性在需要时提供透明度。
让我们深入研究一个具体的例子。假设我们想使用机器学习来预测波士顿地区房屋价格,基于它们的特征。还假设当地的城市法规只允许我们使用机器学习算法,只要我们能在需要时提供任何预测的详细信息来进行辩解。这些信息是为了审计目的,以确保房地产市场的某些部分不会被人为操纵。使我们的训练模型可解释将提供这些额外信息。
让我们看看实现我们训练模型可解释性的不同选项。
提供可解释性策略
对于机器学习,基本上有两种策略可以为算法提供可解释性:
-
全球可解释性策略:这是为了提供模型整体的制定细节。
-
局部可解释性策略: 这是为我们训练模型所做的一个或多个个体预测提供理由。
对于全局可解释性,我们有诸如概念激活向量测试(TCAV)之类的技术,用于为图像分类模型提供可解释性。TCAV 依赖于计算方向导数来量化用户定义的概念与图片分类之间的关系程度。例如,它将量化将一个人分类为男性的预测对图片中面部毛发的敏感程度。还有其他全局可解释性策略,如部分依赖图和计算排列重要性,可以帮助解释我们训练模型中的公式。全局和局部可解释性策略都可以是特定于模型的或模型不可知的。特定于模型的策略适用于某些类型的模型,而模型不可知的策略可以应用于各种模型。
以下图表总结了机器学习可解释性的不同策略:

现在,让我们看看如何使用这些策略之一来实施可解释性。
实施可解释性
局部可解释模型不可知解释(LIME)是一种模型不可知的方法,可以解释训练模型所做的个体预测。作为模型不可知,它可以解释大多数类型的训练机器学习模型的预测。
LIME 通过对每个实例的输入进行微小更改来解释决策。它可以收集该实例的局部决策边界的影响。它迭代循环以提供每个变量的详细信息。通过查看输出,我们可以看到哪个变量对该实例的影响最大。
让我们看看如何使用 LIME 使我们的房价模型的个体预测变得可解释:
- 如果您以前从未使用过 LIME,您需要使用
pip安装该软件包:
!pip install lime
- 然后,让我们导入我们需要的 Python 软件包:
import sklearn as sk
import numpy as np
from lime.lime_tabular import LimeTabularExplainer as ex
- 我们将训练一个能够预测特定城市房价的模型。为此,我们将首先导入存储在
housing.pkl文件中的数据集。然后,我们将探索它具有的功能:

基于这些功能,我们需要预测房屋的价格。
- 现在,让我们训练模型。我们将使用随机森林回归器来训练模型。首先,我们将数据分为测试和训练分区,然后使用它来训练模型:
from sklearn.ensemble import RandomForestRegressor
X_train, X_test, y_train, y_test = sklearn.model_selection.train_test_split(
housing.data, housing.target)
regressor = RandomForestRegressor()
regressor.fit(X_train, y_train)
- 接下来,让我们识别类别列:
cat_col = [i for i, col in enumerate(housing.data.T)
if np.unique(col).size < 10]
- 现在,让我们使用所需的配置参数实例化 LIME 解释器。请注意,我们正在指定我们的标签是
'price',表示波士顿房屋的价格:
myexplainer = ex(X_train,
feature_names=housing.feature_names,
class_names=['price'],
categorical_features=cat_col,
mode='regression')
- 让我们尝试查看预测的详细信息。首先让我们从 matplotlib 中导入绘图器。
exp = myexplainer.explain_instance(X_test[25], regressor.predict,
num_features=10)
exp.as_pyplot_figure()
from matplotlib import pyplot as plt
plt.tight_layout()
- 由于 LIME 解释器适用于个体预测,我们需要选择要分析的预测。我们已要求解释器解释索引为
1和35的预测的理由:

让我们尝试分析 LIME 的上述解释,它告诉我们以下内容:
-
个体预测中使用的功能列表:它们在前面的截图中显示在y轴上。
-
决策中功能的相对重要性: 条形的长度越长,重要性越大。数字的值在x轴上。
-
每个输入功能对标签的正面或负面影响: 红色条表示负面影响,绿色条表示特定功能的正面影响。
理解伦理和算法
通过算法制定模式可能会直接或间接导致不道德的决策。在设计算法时,很难预见潜在的道德影响的全部范围,特别是对于大规模算法,其中可能涉及多个用户。这使得分析人类主观性的影响变得更加困难。
越来越多的公司将算法的道德分析作为其设计的一部分。但事实是,问题可能直到我们发现一个有问题的用例才会变得明显。
学习算法存在的问题
能够根据不断变化的数据模式进行自我调整的算法被称为学习算法。它们处于实时学习模式,但这种实时学习能力可能具有道德影响。这可能导致它们的学习结果在道德上存在问题。由于它们被创建为处于持续进化阶段,几乎不可能对它们进行持续的道德分析。
随着算法复杂性的增加,要完全理解它们对社会中个人和群体的长期影响变得越来越困难。
理解道德考虑
算法解决方案是没有感情的数学公式。负责开发算法的人有责任确保它们符合我们试图解决的问题周围的道德敏感性。这些算法的道德考虑取决于算法的类型。
例如,让我们看看以下算法及其道德考虑。一些需要仔细考虑道德问题的强大算法的例子如下:
-
分类算法在社会上的使用决定了个人和群体的塑造和管理方式。
-
在推荐引擎中使用算法时,可以将简历与求职者(个人和群体)进行匹配。
-
数据挖掘算法用于从用户那里挖掘信息,并提供给决策者和政府。
-
政府开始使用机器学习算法来决定是否向申请人发放签证。
因此,算法的道德考虑将取决于使用情况以及它们直接或间接影响的实体。在开始使用算法进行关键决策之前,需要从道德角度进行仔细分析。在接下来的部分中,我们将看到在进行算法的仔细分析时应该牢记的因素。
不确定的证据
用于训练机器学习算法的数据可能没有确凿的证据。例如,在临床试验中,由于有限的可用证据,一种药物的有效性可能无法得到证实。同样,可能存在有限的不确定证据表明某个城市的某个邮政编码更有可能涉及欺诈。我们在基于通过这些有限数据找到的数学模式做出决策时应该谨慎。
基于不确定的证据做出的决定很可能导致不合理的行为。
可追溯性
机器学习算法在训练阶段和测试阶段之间的脱节意味着如果算法造成了一些伤害,很难追踪和调试。此外,当发现算法中存在问题时,很难确定受到影响的人。
误导的证据
算法是数据驱动的公式。垃圾进,垃圾出(GIGO)原则意味着算法的结果只能像其基础数据一样可靠。如果数据中存在偏见,它们也会反映在算法中。
不公*的结果
算法的使用可能会对已处于不利地位的脆弱社区和群体造成伤害。
此外,已经证明使用算法分配研究资金在多个场合上对男性人口存在偏见。用于授予移民的算法有时会无意中对脆弱人口群体存在偏见。
尽管使用高质量的数据和复杂的数学公式,如果结果是不公*的,那么整个努力可能会带来更多的伤害而不是好处。
减少模型中的偏见
在当前世界中,基于性别、种族和性取向已知的、有充分记录的一般偏见。这意味着我们收集的数据预计会展现出这些偏见,除非我们处理的是一个在收集数据之前已经努力消除这些偏见的环境。
算法中的所有偏见,直接或间接地都是由人类偏见造成的。人类偏见可以体现在算法使用的数据中,也可以体现在算法本身的制定中。对于遵循CRISP-DM(跨行业标准流程)生命周期的典型机器学习项目,该生命周期在第五章中有解释,图算法,偏见看起来像这样:

减少偏见最棘手的部分是首先识别和定位无意识的偏见。
解决 NP-hard 问题
NP-hard 问题在第四章中得到了广泛讨论,设计算法。一些 NP-hard 问题很重要,我们需要设计算法来解决它们。
如果由于其复杂性或可用资源的限制而发现解决 NP-hard 问题似乎是不可能的,我们可以采取以下其中一种方法:
-
简化问题
-
定制一个已知解决方案以解决类似问题
-
使用概率方法
让我们逐一看看它们。
简化问题
我们可以基于某些假设简化问题。解决的问题仍然给出的解决方案并不完美,但仍然具有洞察力和有用。为了使其起作用,所选择的假设应尽可能不受限制。
示例
在回归问题中,特征和标签之间的关系很少是完全线性的。但在我们通常的操作范围内可能是线性的。将关系*似为线性大大简化了算法,并且被广泛使用。但这也引入了一些影响算法准确性的*似。*似和准确性之间的权衡应该被仔细研究,并且应选择适合利益相关者的正确*衡。
定制一个已知解决方案以解决类似问题
如果已知类似问题的解决方案,那么可以将该解决方案用作起点。它可以定制以解决我们正在寻找的问题。机器学习中的迁移学习(TL)就是基于这一原则。其思想是使用已经预先训练的模型的推理作为训练算法的起点。
示例
假设我们想要训练一个二元分类器,它可以基于实时视频流使用计算机视觉在企业培训期间区分苹果和 Windows 笔记本电脑。从视频流中,模型开发的第一阶段将是检测不同的物体并确定哪些物体是笔记本电脑。一旦完成,我们可以进入第二阶段,制定可以区分苹果和 Windows 笔记本电脑的规则。
现在,已经有经过良好训练、经过充分测试的开源模型,可以处理这个模型训练的第一阶段。为什么不以它们作为起点,并将推理用于第二阶段,即区分 Windows 和苹果笔记本电脑?这将使我们有一个快速起步,解决方案在第一阶段已经经过充分测试,因此错误更少。
使用概率方法
我们使用概率方法来获得一个相当不错的解决方案,但并非最佳解决方案。当我们在第七章中使用决策树算法来解决给定问题时,解决方案是基于概率方法的。我们没有证明这是一个最佳解决方案,但它是一个相当不错的解决方案,可以为我们在需求定义中规定的约束条件下提供一个有用的答案。
例子
许多机器学习算法从一个随机解决方案开始,然后迭代地改进解决方案。最终的解决方案可能是有效的,但我们无法证明它是最好的。这种方法用于解决复杂问题,以在合理的时间范围内解决它们。这就是为什么对于许多机器学习算法来说,获得可重复的结果的唯一方法是使用相同的种子来使用相同的随机数序列。
何时使用算法
算法就像从业者工具箱中的工具。首先,我们需要了解在给定情况下哪种工具是最好的。有时,我们需要问自己,我们是否有解决问题的解决方案,以及部署解决方案的正确时间是什么。我们需要确定算法的使用是否能够提供一个实际有用的解决方案,而不是其他替代方案。我们需要分析使用算法的效果,从三个方面来看:
-
成本:能否证明与实施算法相关的成本?
-
时间:我们的解决方案是否比更简单的替代方案使整个过程更有效?
-
准确性:我们的解决方案是否比更简单的替代方案产生更准确的结果?
为了选择正确的算法,我们需要找到以下问题的答案:
-
我们是否可以通过做出假设来简化问题?
-
我们将如何评估我们的算法?关键指标是什么?
-
它将如何部署和使用?
-
它需要解释吗?
-
我们是否理解了三个重要的非功能性要求-安全性、性能和可用性?
-
是否有预期的截止日期?
一个实际的例子-黑天鹅事件
算法输入数据,处理并制定它,并解决问题。如果收集的数据是关于一个极具影响力且非常罕见的事件,我们如何使用由该事件生成的数据以及可能导致大爆炸的事件?让我们在本节中探讨这个方面。
纳西姆·塔勒布在他的 2001 年的书《被随机愚弄》中用黑天鹅事件的比喻来代表这些极其罕见的事件。
在黑天鹅首次在野外被发现之前,几个世纪以来,它们被用来代表不可能发生的事情。在它们被发现后,这个术语仍然很受欢迎,但它所代表的含义发生了变化。现在它代表着一些如此罕见以至于无法预测的事情。
塔勒布提供了将事件分类为黑天鹅事件的四个标准。
将事件分类为黑天鹅事件的四个标准
决定罕见事件是否应该被分类为黑天鹅事件有点棘手。一般来说,为了被归类为黑天鹅,它应该符合以下四个标准。
-
首先,一旦事件发生,对观察者来说,它必须是一个令人震惊的惊喜,例如在广岛投下原子弹。
-
事件应该是一场轰动一时的事件-一场颠覆性的重大事件,比如西班牙流感的爆发。
-
一旦事件发生并尘埃落定,作为观察者群体的数据科学家应该意识到实际上这并不是那么令人惊讶。观察者们从未注意到一些重要的线索。如果他们有能力和主动性,黑天鹅事件本来是可以预测的。例如,西班牙流感爆发之前有一些被忽视的线索。此外,曼哈顿计划在原子弹实际投放广岛之前已经运行了多年。观察者群体只是无法将这些线索联系起来。
-
当事件发生时,虽然黑天鹅事件的观察者们感到终身的惊讶,但也许有些人对他们来说根本不是什么惊讶。例如,多年来致力于开发原子弹的科学家们,使用原子能从未是一个惊讶,而是一个预期的事件。
将算法应用于黑天鹅事件
黑天鹅事件与算法相关的主要方面有:
-
有许多复杂的预测算法可用。但如果我们希望使用标准的预测技术来预测黑天鹅事件作为预防措施,那是行不通的。使用这种预测算法只会提供虚假的安全感。
-
一旦黑天鹅事件发生,通常不可能准确预测其对包括经济、公众和政府问题在内的更广泛社会领域的影响。首先,作为一种罕见事件,我们没有正确的数据来供给算法,也没有掌握我们可能从未探索和理解的更广泛社会领域之间的相关性和相互作用。
-
需要注意的一点是,黑天鹅事件并不是随机事件。我们只是没有能力关注最终导致这些事件发生的复杂事件。这是算法可以发挥重要作用的领域。我们应该确保在未来有一种策略来预测和检测这些小事件,这些事件随着时间的推移组合在一起产生了黑天鹅事件。
2020 年初的 COVID-19 爆发是我们这个时代最好的黑天鹅事件的例子。
前面的例子显示了首先考虑和理解我们试图解决的问题的细节,然后提出我们可以通过实施基于算法的解决方案来为解决方案做出贡献的重要性。没有全面的分析,如前所述,使用算法可能只能解决复杂问题的一部分,达不到预期。
总结
在本章中,我们了解了在设计算法时应考虑的实际方面。我们探讨了算法可解释性的概念以及我们可以在不同层面提供它的各种方式。我们还研究了算法中潜在的道德问题。最后,我们描述了在选择算法时要考虑的因素。
算法是我们今天所见证的新自动化世界中的引擎。了解、实验和理解使用算法的影响是很重要的。了解它们的优势和局限性以及使用算法的道德影响将在使这个世界成为一个更好的居住地方方面产生深远影响。这本书正是为了在这个不断变化和发展的世界中实现这一重要目标而做出的努力。



来表示这一点。
,计算通过顶点a的最短路径数量。让我们用
来表示这一点。
来计算介数。



是斜率,表示每增加一个 X,线就上升多少。
浙公网安备 33010602011771号