Spark-机器学习快速启动指南-全-
Spark 机器学习快速启动指南(全)
原文:
annas-archive.org/md5/38444d9e78402dd0977c5a58c40c8efd译者:飞龙
前言
世界上每一个人和每一个组织都在管理数据,无论他们是否意识到这一点。数据被用来描述我们周围的世界,可以用于几乎任何目的,从分析消费者习惯以推荐最新的产品和服务,到抗击疾病、气候变化和严重的有组织犯罪。最终,我们管理数据是为了从中获得价值,无论是个人价值还是商业价值,而且世界上许多组织传统上都在投资工具和技术,以帮助他们更快、更有效地处理数据,以便提供可操作的见解。
但我们现在生活在一个高度互联的世界,由大量数据的创建和消费驱动,数据不再是仅限于电子表格的行和列,而是它自身的一种有机和不断发展的资产。随着这一认识的到来,组织在进入以智能驱动的第四次工业革命时面临着重大挑战——我们如何管理每秒产生的各种格式的数据量(不仅想到电子表格和数据库,还包括社交媒体帖子、图片、视频、音乐、在线论坛和文章、计算机日志文件等等)?一旦我们知道如何管理所有这些数据,我们如何知道向它提出什么问题,以便从中获得真正的个人或商业价值?
这本书的焦点是通过从第一原理开始以动手的方式帮助我们回答这些问题。我们介绍了最新的尖端技术(包括 Apache Spark 的大数据生态系统),这些技术可以用来管理和处理大数据。然后我们探讨了高级算法类别(机器学习、深度学习、自然语言处理和认知计算),这些算法可以应用于大数据生态系统,帮助我们揭示之前隐藏的关系,以便理解数据在告诉我们什么,从而最终解决现实世界的问题。
这本书的读者对象
这本书的目标读者是商业分析师、数据分析师、数据科学家、数据工程师和软件工程师,他们可能目前每天的工作涉及使用电子表格或关系型数据库分析数据,可能还会使用 VBA、结构化查询语言(SQL)或甚至 Python 来计算统计聚合(如平均值)以及生成图表、图表、交叉表和其他报告媒介。
随着各种格式和频率的数据爆炸式增长,你可能现在面临的挑战不仅是管理所有这些数据,还要理解它所传达的信息。你很可能已经听说过大数据、人工智能和机器学习这些术语,但现在你可能希望了解如何开始利用这些新技术和框架,不仅是在理论上,而且在实践中,以解决你的商业挑战。如果这听起来很熟悉,那么这本书就是为你准备的!
为了最大限度地利用这本书
尽管本书旨在从第一原理解释一切,但具备数学符号和基本编程技能(例如用于数据转换的 SQL、Base SAS、R 或 Python)将是有益的(尽管不是严格必需的)。对于 SQL 和 Python 的初学者,一个好的学习网站是www.w3schools.com。
为了安装、配置和提供包含详细说明的先决软件服务的自包含本地开发环境,需要具备 Linux shell 命令的基本知识。第二章,设置本地开发环境,描述了配置 CentOS 7 的各种选项。对于 Linux 命令行的初学者,一个好的学习网站是linuxcommand.org。
7-Zip/PeaZip for Linux
下载示例代码文件
您可以从www.packt.com的账户下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packt.com/support并注册,以便将文件直接通过电子邮件发送给您。
假设您有权访问配置了 CentOS Linux 7(或 Red Hat Linux)操作系统的物理或虚拟机。如果您没有,第二章,设置本地开发环境,描述了配置 CentOS 7 虚拟机(VM)的各种选项,包括通过云计算平台(如Amazon Web Services(AWS)、Microsoft Azure、Google Cloud Platform(GCP))、虚拟专用服务器托管公司或免费虚拟化软件(如 Oracle VirtualBox 和 VMWare Workstation Player,这些软件可以安装在您的本地物理设备上,如台式机或笔记本电脑)。
-
在www.packt.com登录或注册。
-
选择“支持”选项卡。
-
点击“代码下载与勘误”。
-
在搜索框中输入书籍名称,并遵循屏幕上的说明。
文件下载完成后,请确保您使用最新版本的软件解压缩或提取文件夹,例如:
-
假设您有权访问配置了 CentOS Linux 7(或 Red Hat Linux)操作系统的物理或虚拟机。如果您没有,第二章,设置本地开发环境,描述了配置 CentOS 7 虚拟机(VM)的各种选项,包括通过云计算平台(如Amazon Web Services(AWS)、Microsoft Azure、Google Cloud Platform(GCP))、虚拟专用服务器托管公司或免费虚拟化软件(如 Oracle VirtualBox 和 VMWare Workstation Player,这些软件可以安装在您的本地物理设备上,如台式机或笔记本电脑)。
-
WinRAR/7-Zip for Windows
-
Zipeg/iZip/UnRarX for Mac
本书代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Machine-Learning-with-Apache-Spark-Quick-Start-Guide。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还有其他来自我们丰富的图书和视频目录的代码包可供选择,这些代码包可在github.com/PacktPublishing/找到。去看看吧!
使用的约定
本书使用了多种文本约定。
CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“将下载的WebStorm-10*.dmg磁盘映像文件作为系统中的另一个磁盘挂载。”
代码块设置如下:
import findspark
findspark.init()
from pyspark import SparkContext, SparkConf
import random
任何命令行输入或输出都应按以下方式编写:
> source /etc/profile.d/java.sh
> echo $PATH
> echo $JAVA_HOME
粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“从管理面板中选择系统信息。”
警告或重要注意事项看起来像这样。
小贴士和技巧看起来像这样。
联系我们
我们欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并给我们发送邮件至customercare@packtpub.com。
勘误表:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告这一点,我们将不胜感激。请访问www.packt.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
盗版:如果您在互联网上发现我们作品的任何非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过copyright@packt.com与我们联系,并提供材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
评论
请留下评论。一旦您阅读并使用了这本书,为何不在购买它的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,Packt 公司可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!
如需了解 Packt 的更多信息,请访问packt.com。
第一章:大数据生态系统
现代技术已经彻底改变了我们对数据的理解。以前,数据传统上被认为是限制在电子表格或关系数据库中的文本和数字,而如今,它是一种有机且不断发展的资产,由任何拥有智能手机、电视或银行账户的人大规模创建和消费。在本章中,我们将探讨新的生态系统,其中包括前沿的工具、技术和框架,使我们能够存储、处理和分析大量数据,以便提供可操作的见解并解决现实世界的问题。到本章结束时,你将获得以下前沿技术类别的深入了解:
-
分布式系统
-
NoSQL 数据库
-
人工智能和机器学习框架
-
云计算平台
-
大数据平台和参考架构
数据简史
如果你在 1970 年代到 2000 年代初的主流 IT 行业工作,那么你组织的数据库可能存储在基于文本的定界文件、电子表格或结构良好的关系数据库中。在后一种情况下,数据被建模并持久化在预定义的、可能相关的表中,这些表代表组织中数据模型中找到的各种实体,例如,根据员工或部门。这些表包含跨越多个列的数据行,代表构成该实体的各种属性;例如,在员工的情况下,典型的属性包括名字、姓氏和出生日期。
垂直扩展
随着贵组织的数据资产和需要访问这些数据的使用者数量的增长,高性能的远程服务器将被利用,通过企业网络提供访问权限。这些远程服务器通常要么作为远程文件系统用于文件共享,要么托管关系数据库管理系统(RDBMSes)以存储和管理关系数据库。随着数据需求的增长,这些远程服务器将需要垂直扩展,这意味着将安装额外的 CPU、内存和/或硬盘空间。通常,这些关系数据库将存储从数百到可能数千万条记录。
主/从架构
作为提供弹性和负载均衡读取请求的手段,可能已经采用了主/从架构,其中数据通过近实时复制自动从主数据库服务器复制到物理上不同的从数据库服务器。这项技术要求主服务器负责所有写请求,而读取请求可以卸载并在从服务器之间进行负载均衡,其中每个从服务器都持有主数据的完整副本。这样,如果主服务器因某种原因失败,业务关键性的读取请求仍然可以由从服务器处理,同时主服务器正在恢复在线。然而,这项技术确实有几个主要的缺点:
-
可扩展性:由于主服务器仅负责处理写请求,这限制了系统的可扩展性,因为它可能很快成为瓶颈。
-
一致性和数据丢失:由于复制几乎是实时的,不能保证在主服务器离线且事务可能丢失的那个时间点,从服务器会有最新的数据。根据业务应用的不同,没有最新数据或丢失数据可能都是不可接受的。
分片
为了提高吞吐量和整体性能,并且随着单机在以成本效益的方式垂直扩展其容量时达到极限,可能已经采用了分片。这是一种水平扩展的方法,其中额外的服务器被配置,数据在集群中每台机器上的独立数据库实例之间物理分割,如图1.1所示。
这种方法允许组织线性扩展以适应增加的数据量,同时重用现有的数据库技术和通用硬件,从而优化小型到中型数据库的成本和性能。
然而,关键的是,这些独立的数据库是独立的实例,彼此之间没有了解。因此,需要某种类型的代理,基于分区策略,跟踪每个写请求数据写入的位置,之后从相同的位置检索数据以处理读取请求。分片随后引入了进一步挑战,如处理跨越多个独立数据库实例和多个服务器的数据查询、转换和连接(不进行数据规范化),从而保持引用完整性和数据的重新分区:

图 1.1:简单的分片分区策略
数据处理和分析
最后,为了转换、处理和分析存储在这些基于文本的文件、电子表格或关系型数据库中的数据,通常需要一个分析师、数据工程师或软件工程师编写一些代码。
例如,此代码可以是电子表格的公式或Visual Basic for Applications(VBA),或者关系数据库的结构化查询语言(SQL),并用于以下目的:
-
加载数据,包括批量加载和数据迁移
-
转换数据,包括数据清理、连接、合并、丰富和验证
-
标准的统计汇总,包括计算平均值、计数、总和和交叉表
-
报告,包括图表、图表、表格和仪表板
为了执行更复杂的统计计算,例如生成预测模型,高级分析师可以利用更高级的编程语言,包括 Python、R、SAS,甚至 Java。
然而,关键的是,这种数据转换、处理和分析要么是在数据持久化的服务器上直接执行(例如,在关系数据库服务器上直接执行 SQL 语句,与其他常规的读写请求竞争),要么是通过程序性查询通过网络移动数据(例如,通过 ODBC 或 JDBC 连接),或者通过平面文件(例如,CSV 或 XML 文件)到另一个远程分析处理服务器。当然,假设远程处理服务器在其单台机器上具有足够的 CPU、内存和/或磁盘空间来执行相关任务,代码可以随后在该数据上执行。换句话说,数据以某种方式被移动到代码中。
数据变得庞大
快进到今天——电子表格仍然很常见,包含结构良好数据的关联数据库,无论是否跨分片,仍然非常相关且极其有用。实际上,根据用例、数据量、结构和所需处理的计算复杂性,通过关系数据库管理系统(RDBMS)存储和管理数据,并在远程数据库服务器上直接使用 SQL 处理这些数据,可能仍然更快、更高效。当然,对于非常小的数据集和简单的统计汇总,电子表格仍然很棒。然而,自 1970 年代以来,变化的是更强大、更经济的技术的可用性,以及互联网的引入!
互联网已经改变了我们对数据的本质理解。以前,数据被认为是局限于电子表格或关系型数据库中的文本和数字,而现在它已经成为一种有机且不断发展的资产,由任何拥有智能手机、电视或银行账户的人大规模创建和消费。全球每秒钟都在以几乎任何你能想到的格式创建数据,从社交媒体帖子、图片、视频、音频和音乐到博客文章、在线论坛、文章、计算机日志文件和金融交易。所有这些结构化、半结构化和非结构化数据,无论是批量还是实时创建的,都无法再通过组织良好的基于文本的定界文件、电子表格或关系型数据库来存储和管理,也无法每次执行一些分析代码时都将其物理移动到远程处理服务器——需要一种新的技术。
大数据生态系统
如果你今天在几乎任何主流行业中工作,你可能会听到以下一些术语和短语:
-
大数据
-
分布式、可扩展和弹性
-
本地部署与云
-
SQL 与 NoSQL
-
人工智能、机器学习和深度学习
但所有这些术语和短语实际上意味着什么,它们是如何相互关联的,你从哪里开始?本节的目标是以清晰简洁的方式回答所有这些问题。
水平扩展
首先,让我们回到我们之前描述的一些以数据为中心的问题。鉴于今天数据的大规模创建和消费的爆炸性增长,显然我们不能再继续向单台机器添加 CPU、内存和/或硬盘(换句话说,垂直扩展)。如果我们这样做,很快就会达到一个点,迁移到更强大的硬件将导致收益递减,同时产生显著的成本。此外,可扩展性将受到我们所能获得的最大机器的物理限制,从而限制组织的增长潜力。
水平扩展,以分片为例,是我们通过添加或移除硬件和/或软件来增加或减少可用计算资源的过程。通常,这会涉及向节点集群中添加(或移除)服务器或节点。然而,关键的是,集群始终作为一个单一的逻辑单元运行,这意味着无论是否向其添加资源或从中移除资源,它都将继续运行并处理请求。水平扩展和垂直扩展之间的区别在图 1.2中得到了说明:

图 1.2:垂直扩展与水平扩展
分布式系统
水平扩展允许组织在数据和处理需求超过一定点时变得更加高效。但是,仅仅向集群中添加更多机器本身并不会带来太多价值。我们现在需要的是能够利用水平可伸缩性并且能够在多台机器上无缝工作的系统,无论集群包含一台机器还是 10,000 台机器。
分布式系统正是如此——它们在机器集群中无缝工作,并自动处理从该集群中添加(或移除)资源的情况。分布式系统可以分为以下类型:
-
分布式文件系统
-
分布式数据库
-
分布式处理
-
分布式消息
-
分布式流
-
分布式账本
分布式数据存储
让我们回到单机 RDBMS 面临的问题。我们已经看到,分片可以作为扩展关系数据库水平以优化成本的一种方法,适用于从小型到中型数据库的数据增长。然而,分片的问题在于每个节点以独立的方式运行,对集群中的其他节点一无所知,这意味着需要一个自定义代理来分区数据并在分片之间处理读写请求。
相反,分布式数据存储作为单个逻辑单元,直接在节点集群中运行,无需额外配置即可使用。
注意,数据存储只是一个通用术语,用来描述任何用于持久化数据的存储库。分布式数据存储通过在多个节点上存储数据来扩展这一概念,并且通常采用复制机制。
客户端应用程序将分布式数据存储视为一个单一实体,这意味着无论集群中的哪个节点实际处理客户端请求,都会返回相同的结果。下一节中讨论的分布式文件系统,如Apache Hadoop 分布式文件系统(HDFS),属于分布式数据存储类别,用于以原始格式存储文件。当数据需要以某种方式建模时,可以使用分布式数据库。根据分布式数据库的类型,它可以在分布式文件系统之上部署,也可以不部署。
分布式文件系统
想象一下你桌面、笔记本电脑、智能手机或其他个人设备中的硬盘。文件被写入并存储在本地硬盘上,并在需要时检索。你的本地操作系统通过维护本地文件系统来管理对本地硬盘的读写请求——这是操作系统跟踪磁盘组织方式和文件位置的一种方式。
随着您个人数据足迹的增长,您在本地硬盘上占用的空间越来越多,直到达到其容量。在此时刻,您可能寻求购买一个更大容量的硬盘来替换设备内部的硬盘,或者您可能寻求购买一个额外的硬盘来补充现有的硬盘。在后一种情况下,您个人管理哪些个人文件存储在哪个硬盘上,或者可能使用其中一个来存档很少使用的文件,以释放主硬盘的空间。希望您也维护个人文件的备份,以防最坏的情况发生,您的设备或主硬盘出现故障!
分布式文件系统(DFS)扩展了本地文件系统的概念,同时提供了一系列有用的好处。在我们的大数据生态系统中,分布式文件系统将数据物理分割到集群中的节点和磁盘上。就像一般分布式数据存储一样,分布式文件系统提供了一层抽象,并管理跨集群的读写请求,这意味着物理分割对请求客户端应用程序来说是不可见的,它们将分布式文件系统视为一个逻辑实体,就像传统的本地文件系统一样。
此外,分布式文件系统提供了开箱即用的有用好处,包括以下内容:
-
数据复制,其中数据可以被配置为在集群中自动复制,以实现容错,以防一个或多个节点或磁盘出现故障
-
数据完整性检查
-
能够持久化巨大的文件,通常大小为千兆字节(GB)到太字节(TB),这在传统的本地文件系统中通常是不可能的
HDFS(Hadoop 分布式文件系统)是我们大数据生态系统中的一个知名分布式文件系统示例。在 HDFS 中,采用主/从架构,包括一个负责管理分布式文件系统的单个 NameNode,以及多个 DataNode,这些 DataNode 通常位于集群中的每个节点上,并管理连接到该节点的物理磁盘以及数据物理存储的位置。就像传统的文件系统一样,HDFS 支持标准文件系统操作,例如打开和关闭文件和目录。当客户端应用程序请求将文件写入 HDFS 时,该文件被分割成一个或多个块,然后由 NameNode 映射到 DataNode,在那里它们被物理存储。当客户端应用程序请求从 HDFS 读取文件时,DataNode 满足这一请求。
HDFS 的一个核心优势是它通过其分布式架构以及数据复制提供固有的容错性。由于通常 HDFS 集群中会有多个节点(可能成千上万),它对硬件故障具有弹性,因为操作可以自动卸载到集群的健康部分,同时非功能硬件正在恢复或更换。此外,当文件被分割成块并由 NameNode 映射到 DataNode 时,这些块可以配置为自动在 DataNode 之间复制,考虑到 HDFS 集群的拓扑结构。
因此,如果确实发生了故障,例如,DataNode 上的磁盘故障,数据仍然可供客户端应用程序使用。HDFS 集群的高级架构在图 1.3中说明:

图 1.3:Apache Hadoop 分布式文件系统高级架构
要了解更多关于 Apache Hadoop 框架的信息,请访问 hadoop.apache.org/。
分布式数据库
分布式文件系统,就像传统文件系统一样,用于存储文件。在分布式文件系统(如 HDFS)的情况下,这些文件可以非常大。然而,最终它们用于存储文件。当数据需要建模时,我们需要的不仅仅是文件系统;我们需要数据库。
分布式数据库,就像单机数据库一样,允许我们建模我们的数据。然而,与单机数据库不同的是,数据和数据模型本身跨越并保存在充当单个逻辑数据库的集群中的所有节点。这意味着我们不仅可以利用分布式系统提供的增加的性能、吞吐量、容错性、弹性和成本效益,而且我们可以有效地建模我们的数据,然后高效地查询这些数据,无论其大小如何或处理要求有多复杂。根据分布式数据库的类型,它可以在分布式文件系统(如部署在 HDFS 之上的 Apache HBase)之上部署,也可以不部署。
在我们的大数据生态系统中,通常使用分布式文件系统,如 HDFS,来托管数据湖。数据湖是一个集中式数据存储库,其中数据以原始的原始格式持久化,例如文件和对象 BLOB。这使得组织能够将它们分散的原始数据资产,包括结构化和非结构化数据,整合到一个没有预定义模式的中央存储库中,同时提供在成本效益的方式下随时间扩展的能力。
此后,为了从大量无模式数据中实际交付业务价值和可操作见解,数据处理管道被设计出来,将原始数据转换为符合某种数据模型的有意义数据,然后持久化到由分布式数据库托管的服务或分析数据存储中。这些分布式数据库根据数据模型和业务应用类型进行了优化,以有效地查询它们内部存储的大量数据,以服务于面向用户的商业智能(BI)、数据发现、高级分析和以洞察力驱动的应用程序和 API。
分布式数据库的例子包括以下内容:
-
Apache HBase:
hbase.apache.org/ -
Apache Cassandra:
cassandra.apache.org/ -
Apache CouchDB:
couchdb.apache.org/ -
Apache Ignite:
ignite.apache.org/ -
Greenplum Database:
greenplum.org/ -
MongoDB:
www.mongodb.com/
Apache Cassandra 是一个采用无主架构的分布式数据库示例,该架构没有单点故障,支持处理大量数据的高吞吐量。在 Cassandra 中,没有数据的主副本。相反,数据根据分区键和其他固有的 Cassandra 模型和存储数据的方式自动分区,并根据可配置的复制因子在其他节点上复制。由于不存在主/从的概念,因此采用 Gossip 协议,以便 Cassandra 集群中的节点可以动态地了解其他节点的状态和健康情况。
为了处理来自客户端应用程序的读写请求,Cassandra 将自动从集群中可用的节点中选举一个协调节点,这个过程对客户端来说是不可见的。为了处理写请求,协调节点将根据 Cassandra 所采用的底层分布式数据模型的分区特性,联系所有应持久化写请求和副本的适用节点。为了处理读请求,协调节点将联系一个或多个副本节点,这些节点已知数据已被写入,同样基于 Cassandra 的分区特性。因此,Cassandra 所采用的底层架构可以可视化为一个环,如图图 1.4所示。请注意,尽管 Cassandra 集群的拓扑可以可视化为一个环,但这并不意味着一个节点的故障会导致整个集群的故障。如果某个节点因任何原因变得不可用,Cassandra 将简单地继续向其他应持久化请求数据的适用节点写入,同时维护一个有关失败节点的操作队列。当非功能节点重新上线时,Cassandra 将自动更新它:

图 1.4:Cassandra 拓扑图,展示了具有 3 个复制因子的写请求
NoSQL 数据库
关系数据库管理系统(如 Microsoft SQL Server、PostgreSQL 和 MySQL)允许我们在表示数据模型中实体的表格中,以结构化的方式建模我们的数据。这些表格是预定义的,其模式由各种数据类型的列组成,这些列代表所讨论实体的属性。
例如,图 1.5 描述了一个非常简单的关系模式,这种模式可以被一个电子商务网站所利用:

图 1.5:一个电子商务网站的简单关系数据库模型
另一方面,NoSQL 仅仅指的是一类数据库,其中数据不是以传统的关系方式建模。因此,如果数据不是以关系方式建模,那么在 NoSQL 数据库中是如何建模的呢?答案是,根据具体的使用案例和业务应用,存在各种类型的 NoSQL 数据库。这些不同类型在以下小节中进行了总结。
人们通常有一个常见的但错误的假设,即 NoSQL 是分布式数据库的同义词。实际上,有一个不断增长的 RDBMS 供应商列表,他们的产品被设计成可扩展和分布式,以适应大量结构化数据。这种错误假设产生的原因是因为在现实世界的实现中,NoSQL 数据库通常用于以分布式方式持久化大量结构化、半结构化和非结构化数据,因此它们与分布式数据库同义。然而,与关系型数据库一样,NoSQL 数据库也被设计成即使在单台机器上也能工作。区分关系型(或 SQL)数据库和 NoSQL 数据库的是数据建模的方式。
文档数据库
文档数据库,如Apache CouchDB和MongoDB,采用文档数据模型来存储半结构化和非结构化数据。在这个模型中,文档用于封装与对象相关的所有信息,通常以JavaScript 对象表示法(JSON)格式,这意味着单个文档是自我描述的。由于它们是自我描述的,不同的文档可能有不同的模式。例如,以下 JSON 文件中描述电影项目的文档将具有与描述图书项目的文档不同的模式:
[
{
"title" : "The Imitation Game",
"year": 2014
"metadata" : {
"directors" : [ "Morten Tyldum"],
"release_date" : "2014-11-14T00:00:00Z",
"rating" : 8.0,
"genres" : ["Biography", "Drama", "Thriller"],
"actors" : ["Benedict Cumberbatch", "Keira Knightley"]
}
}
]
由于文档是对象的自我包含表示,因此它们对于涉及单个对象频繁更新的数据模型特别有用,从而避免了需要更新整个数据库模式的需求,这在关系型数据库中是必需的。因此,文档数据库通常非常适合涉及商品目录的使用案例,例如电子商务网站和内容管理系统,如博客平台。
列式数据库
关系型数据库传统上连续存储每行数据,这意味着每行数据都会存储在磁盘上的连续块中。这种类型的数据库被称为行式数据库。对于涉及典型统计聚合操作,如计算特定属性的均值,行式数据库的效果是在处理过程中会读取该行中的每个属性,无论它们是否与查询相关。一般来说,行式数据库最适合事务型工作负载,也称为在线事务处理(OLTP),在这种工作负载中,单个行经常被写入,并且重点在于快速处理大量相对简单的查询,例如短插入和更新。使用案例包括零售和金融交易,其中数据库模式往往高度规范化。
另一方面,如Apache Cassandra和Apache HBase这样的列式数据库是列导向的,这意味着每个列都在磁盘上的顺序块中持久化。列式数据库的效果是,可以作为一个组一起访问单个属性,而不是逐行单独访问,从而减少了分析查询的磁盘 I/O,因为从磁盘加载的数据量减少了。例如,考虑以下表格:
| 产品 ID | 名称 | 类别 | 单价 |
|---|---|---|---|
| 1001 | USB drive 64 GB | 存储 | 25.00 |
| 1002 | SATA HDD 1 TB | 存储 | 50.00 |
| 1003 | SSD 256 GB | 存储 | 60.00 |
在行式数据库中,数据按如下方式持久化到磁盘:
(1001, USB drive 64 GB, storage, 25.00), (1002, SATA HDD 1 TB, storage, 50.00), (1003, SSD 256 GB, storage, 60.00)
然而,在列式数据库中,数据按如下方式持久化到磁盘:
(1001, 1002, 1003), (USB drive 64 GB, SATA HDD 1 TB, SSD 256 GB), (storage, storage, storage), (25.00, 50.00, 60.00)
通常,列式数据库最适合分析工作负载,也称为在线分析处理(OLAP),其中重点在于处理少量复杂的分析查询,通常涉及聚合。用例示例包括数据挖掘和统计分析,其中数据库模式往往是反规范的或遵循星型或雪花模式设计。
键值数据库
键值数据库,如Redis、Oracle Berkley DB和Voldemort,采用简单的键值数据模型来存储数据,作为唯一键映射到值对象的集合。以下表格展示了将 Web 应用的会话 ID 映射到会话数据的过程:
| 键(会话 ID) | 值(会话数据) |
|---|---|
ab2e66d47a04798 |
{userId: "user1", ip: "75.100.144.28", date: "2018-09-28"} |
62f6nhd47a04dshj |
{userId: "user2", ip: "77.189.90.26", date: "2018-09-29"} |
83hbnndtw3e6388 |
{userId: "user3", ip: "73.43.181.201", date: "2018-09-30"} |
键值数据结构在许多编程语言中都有,通常被称为字典或哈希表。键值数据库通过它们在集群中分区和水平扩展的能力扩展了这些数据结构,从而有效地提供了巨大的分布式字典。键值数据库特别适用于作为提高需要处理每秒数百万个请求的系统性能和吞吐量的手段。用例示例包括流行的电子商务网站、存储 Web 应用的会话数据以及促进缓存层。
图数据库
图数据库,如 Neo4j 和 OrientDB,将数据建模为由一个或多个边(也称为关系或链接)连接在一起的顶点(也称为节点)集合。在现实世界的图实现中,顶点通常用于表示现实世界的实体,如个人、组织、车辆和地址。然后使用边来表示顶点之间的关系。
顶点和边都可以有任意数量的键值对,称为 属性,与它们相关联。例如,与单个顶点相关的属性可能包括姓名和出生日期。与连接两个个人顶点的边相关的属性可能包括个人关系的性质和长度。顶点、边和属性的集合共同形成一个称为 属性图 的数据结构。图 1.6 展示了一个简单的属性图,它表示一个小型社交网络。

图 1.6:一个简单的属性图
图数据库在各种场景中被使用,在这些场景中,重点在于分析对象之间的关系,而不仅仅是对象的数据属性本身。常见的用例包括社交网络分析、欺诈检测、打击严重有组织犯罪、客户推荐系统、复杂推理、模式识别、区块链分析、网络安全和网络入侵检测。
Apache TinkerPop 是一个图计算框架的例子,它提供了一个抽象层,位于图数据模型和用于存储和处理图的底层机制之间。例如,Apache TinkerPop 可以与底层的 Apache Cassandra 或 Apache HBase 数据库结合使用,以存储包含数十亿个顶点和边的巨大分布式图,这些顶点和边在集群中分区。Apache TinkerPop 框架的一个组件,称为 Gremlin 的图遍历语言,可以用来遍历和分析分布式图,使用 Gremlin 语言的各种变体,包括 Gremlin Java、Gremlin Python、Gremlin Scala 和 Gremlin JavaScript。要了解更多关于 Apache TinkerPop 框架的信息,请访问 tinkerpop.apache.org/。
CAP 定理
如前所述,分布式数据存储使我们能够存储大量数据,同时始终以单个逻辑单元的水平扩展能力提供。许多分布式数据存储固有的以下特性:
-
一致性 指的是每个客户端对数据的视图都相同。在实践中,这意味着对集群中任何节点的读取请求都应该返回最近成功写入请求的结果。即时一致性指的是最近成功写入请求应立即对任何客户端可用。
-
可用性指的是系统对客户端提出的每个请求做出响应的保证,无论该请求是否成功。在实践中,这意味着每个客户端请求都会收到响应,无论单个节点是否非功能性。
-
分区容错指的是在节点间网络通信失败时提供的弹性保证。换句话说,如果在特定节点和另一组节点之间存在网络故障,称为网络分区,系统将继续运行。在实践中,这意味着系统应该有能力在集群的功能部分之间复制数据,以应对间歇性网络故障并确保数据不会丢失。然后,系统应该在分区解决后优雅地恢复。
CAP 定理简单地说,分布式系统不能同时立即一致、可用和分区容错。分布式系统可以同时只提供三个中的任意两个。这如图 图 1.7 所示:

图 1.7:CAP 定理
CA 分布式系统提供立即一致性和高可用性,但不能容忍节点间网络故障,这意味着数据可能会丢失。CP 分布式系统提供立即一致性和对网络故障的弹性,没有数据丢失。然而,在节点间网络故障发生时,它们可能不会响应。AP 分布式系统提供高可用性和对网络故障的弹性,没有数据丢失。然而,读请求可能不会返回最新数据。
分布式系统,例如 Apache Cassandra,允许配置所需的一致性级别。例如,假设我们为 Apache Cassandra 集群配置了 3 个副本因子。在 Apache Cassandra 中,一致性配置为 ONE 表示只要 一个 数据副本被持久化,写请求就被认为是成功的,无需等待其他两个副本被写入。在这种情况下,系统被认为是最终一致的,因为其他两个副本最终会被持久化。后续的即时读请求可能返回最新数据,如果它是由更新的副本处理的,或者它可能返回过时数据,如果它是由尚未更新的其他两个副本之一处理的(但最终会更新)。在这种情况下,Cassandra 是一个 AP 分布式系统,表现出最终一致性和对除了一个副本之外的所有副本失败的容忍性。它还提供了在此上下文中性能最快的系统。
在 Apache Cassandra 中,ALL的一致性配置意味着只有当所有副本都成功持久化后,写请求才被认为是成功的。随后的立即读取请求将始终返回最新的数据。在这种情况下,Cassandra 是一个表现出即时一致性的 CA 分布式系统,但没有对失败的容忍度。它还提供了在此背景下性能最慢的系统。
最后,在 Apache Cassandra 中,QUORUM的一致性配置意味着只有当严格多数的副本成功持久化后,写请求才被认为是成功的。随后的立即读取请求也使用 QUORUM 一致性,将等待从两个副本(在副本因子为 3 的情况下)接收数据,并通过比较时间戳,总是返回最新的数据。在这种情况下,Cassandra 也是一个表现出即时一致性的 CA 分布式系统,但具有对少数副本失败的容忍度。它还提供了在此背景下性能中等的系统。
然而,在现实世界中,数据丢失对于大多数业务关键系统来说不是可接受的,需要在性能、一致性和可用性之间进行权衡。因此,选择往往归结为 CP 或 AP 分布式系统,胜者由业务需求驱动。
分布式搜索引擎
基于 Apache Lucene 的分布式搜索引擎,如Elasticsearch,将数据转换为高度优化的数据结构,以实现快速和高效的搜索和分析。在 Apache Lucene 中,数据被索引到包含一个或多个字段(代表各种数据类型的数据属性)的文档中。文档的集合形成一个索引,当处理查询时,搜索的就是这个索引,返回满足查询的相关文档。一个合适的类比是在教科书试图找到与特定主题相关的页面时。读者不必逐页搜索,而是可以使用书后的索引更快地找到相关页面。要了解更多关于 Apache Lucene 的信息,请访问lucene.apache.org/。
Elasticsearch 通过提供在分布式集群上分区和水平扩展搜索索引和分析查询的能力,扩展了 Apache Lucene,并配备了 RESTful 搜索引擎和 HTTP 网络界面,以实现高性能的搜索和分析。要了解更多关于 Elasticsearch 的信息,请访问www.elastic.co/products/elasticsearch。
分布式处理
我们已经看到,如 HDFS 和 Apache Cassandra 这样的分布式数据存储如何使我们能够在水平可扩展的集群上存储和建模大量结构化、半结构化和非结构化数据,提供容错、弹性、高可用性和一致性。但是,为了提供可操作的见解并实现有意义的商业价值,我们现在需要能够处理和分析所有这些数据。
让我们回到本章开头描述的传统数据处理场景。通常,分析师、数据工程师或软件工程师(例如,在 SQL、Python、R 或 SAS 中)编写的数据转换和分析编程代码会依赖于将输入数据物理移动到远程处理服务器或机器,该机器上驻留着要执行的代码。这通常以程序查询的形式嵌入在代码本身中,例如,通过 ODBC 或 JDBC 连接的 SQL 语句,或者通过将 CSV 和 XML 等平面文件移动到本地文件系统。尽管这种方法对于小型到中型数据集来说效果不错,但它受限于单个远程处理服务器可用的计算资源,存在物理限制。此外,引入平面文件如 CSV 或 XML 文件,引入了一个额外的、通常不必要的中间数据存储,需要管理并增加磁盘 I/O。
然而,这种方法的主要问题是每次执行作业时都需要将数据移动到代码。当处理增加的数据量和频率时,如我们与大数据相关联的数据量和频率,这种方法很快就会变得不切实际。
因此,我们需要另一种数据处理和编程范式——一种将代码移动到数据并能在分布式集群中工作的范式。换句话说,我们需要分布式处理!
分布式处理背后的基本思想是将计算处理任务分割成更小的任务。这些较小的任务随后在集群中分散,并处理数据的具体分区。通常,计算任务会与数据本身位于同一节点上,以提高性能并减少网络 I/O。然后以某种方式汇总每个较小任务的结果,在返回最终结果之前。
MapReduce
MapReduce 是一种分布式数据处理范例,能够在节点集群上并行处理大数据。MapReduce 作业将大型数据集分割成独立的块,并包括两个阶段——第一个阶段是 Map 函数,它为输入中的每个范围创建一个映射任务,输出一个分区的键值对组。映射任务的输出随后作为减少任务的输入,其任务是合并和压缩相关的分区,以解决分析问题。在开始映射阶段之前,数据通常根据与正在进行的分析相关的条件进行排序或过滤。同样,减少函数的输出可能需要最终化函数来进一步分析数据。
让我们考虑一个简单的例子,以使这个相当抽象的定义变得生动。我们将考虑的例子是单词计数。假设我们有一个包含数百万行文本的文本文件,我们希望计算整个文本文件中每个唯一单词的出现次数。图 1.8 展示了如何使用 MapReduce 范式进行这种分析:

图 1.8:单词计数 MapReduce 程序
在这个例子中,原始文本文件包含数百万行文本,被分割成单独的行。将映射任务应用于这些单独行的范围,将它们分割成单独的单词标记,在这种情况下,使用空格分词器,然后输出一组键值对,其中键是单词。
执行一个 洗牌过程,将映射任务输出的分区键值对转移到减少任务。根据键对键值对进行排序,也有助于确定何时应该开始新的减少任务。为了在洗牌期间减少从映射任务到减少任务传输的数据量,可以指定一个可选的合并器,该合并器实现一个本地聚合函数。在这个例子中,指定了一个合并器,它对每个映射输出本地地计算每个键或单词的出现次数的总和。
减少任务随后接收那些分区的键值对,并减少具有相同键的值,输出新的(但未排序的)键值对,键是唯一的。在这个例子中,减少任务简单地计算该键的出现次数。在这种情况下,MapReduce 作业的最终输出只是整个原始文本文件中每个单词出现次数的计数。
在这个例子中,我们使用了一个简单的文本文件,该文件已被换行符分割,然后基于空格分词器映射到键值对。但同样的范式可以轻松扩展到分布式数据存储,其中大量数据已经跨集群分区,从而允许我们在巨大规模上进行数据处理。
Apache Spark
Apache Spark 是通用分布式处理引擎的一个知名例子,能够处理PB(PB)级别的数据。因为它是一个通用引擎,所以适用于各种大规模用例,包括使用其Spark SQL库进行提取-转换-加载(ETL)管道的工程和执行、交互式分析、使用其Spark Streaming库的流处理、使用其GraphX库的基于图的处理以及使用其MLlib库的机器学习。我们将在后面的章节中使用 Apache Spark 的机器学习库。然而,现在,了解 Apache Spark 内部工作原理的概述是很重要的。
Apache Spark 软件服务在Java 虚拟机(JVM)中运行,但这并不意味着 Spark 应用程序必须用 Java 编写。实际上,Spark 向包括 Java、Scala、Python 和 R 在内的各种语言变体公开其 API 和编程模型,任何一种都可以用来编写 Spark 应用程序。在逻辑架构方面,Spark 采用如图 1.9所示的主/从架构:

图 1.9:Apache Spark 逻辑架构
每个用 Apache Spark 编写的应用程序都包含一个驱动程序程序。驱动程序程序负责将 Spark 应用程序分割成任务,这些任务随后被分配到分布式集群中的工作节点上,并由驱动程序安排执行。驱动程序程序还实例化一个SparkContext,它告诉应用程序如何连接到 Spark 集群及其底层服务。
工作节点,也称为奴隶节点,是计算处理物理发生的地点。通常,Spark 工作节点与存储底层数据相同的节点上协同运行,以提高性能。工作节点会启动称为执行器的进程,这些执行器负责执行计算任务并存储任何本地缓存的本地数据。执行器通过与驱动程序通信来接收预定函数,如 map 和 reduce 函数,然后执行。集群管理器负责在集群中调度和分配资源,因此必须能够与每个工作节点以及驱动程序通信。驱动程序程序从集群管理器请求执行器(因为集群管理器了解可用的资源),以便它可以安排任务。
Apache Spark 随带自己的简单集群管理器,当使用时,被称为 Spark Standalone 模式。部署到独立集群的 Spark 应用程序默认情况下将利用集群中的所有节点,并以 先入先出(FIFO)的方式调度。Apache Spark 还支持其他集群管理器,包括 Apache Mesos 和 Apache Hadoop YARN,这两者都不在本书的范围之内。
RDDs, DataFrames, and datasets
那么 Spark 在其计算处理过程中是如何存储和分区数据的呢?嗯,默认情况下,Spark 将数据存储在内存中,这有助于使其成为一个快速的处理引擎。实际上,从 Spark 2.0 开始,有三组 API 用于存储数据——弹性分布式数据集(RDDs)、DataFrames 和 Datasets。
RDDs
RDDs 是 Spark 集群中工作节点上分区的一个不可变和分布式记录集合。由于在非功能节点或损坏分区的情况下,RDD 分区可以被重新计算,因为 RDD 本身存储了复制每个分区所需的所有依赖信息,所以它们提供了容错性。由于每个分区都是不可变的,它们也提供了一致性。RDDs 在 Spark 中被广泛使用,尤其是在处理不需要在数据处理时强加模式的数据,例如处理非结构化数据时。可以通过提供两种广泛操作类别的基础级 API 在 RDDs 上执行操作:
-
转换:这些是返回另一个 RDD 的操作。窄转换,例如 map 操作,是可以对数据的不规则分区执行而无需依赖于其他分区的操作。宽转换,例如排序、连接和分组,是需要将数据重新分区到集群的操作。某些转换,如排序和分组,需要将数据重新分配到分区,这个过程称为 洗牌。由于需要重新分配数据,需要洗牌的宽转换是昂贵的操作,在可能的情况下应尽量减少在 Spark 应用程序中的使用。
-
动作:这些是返回值给驱动程序的计算操作,而不是另一个 RDD。RDD 被称为是惰性评估的,这意味着转换只有在调用动作时才会进行计算。
DataFrames
与 RDD 类似,它们是 Spark 集群中工作节点上分区的不变和分布式记录集合。然而,与 RDD 不同,数据在概念上被组织成命名列,类似于关系数据库中的表和其他编程语言(如 Python 和 R)中找到的表格数据结构。由于 DataFrame 允许对分布式数据施加模式,它们更容易暴露给更熟悉的编程语言,如 SQL,这使得它们成为流行且可能更容易处理和操作的数据结构,同时比 RDD 更高效。
然而,DataFrame 的主要缺点是,与 Spark SQL 字符串查询类似,分析错误仅在运行时捕获,而不是在编译时。例如,想象一个名为 df 的 DataFrame,具有名为 firstname、lastname 和 gender 的命名列。现在想象我们编写了以下语句:
df.filter( df.age > 30 )
该语句试图根据一个缺失且未知的列 age 过滤 DataFrame。使用 DataFrame API,这个错误不会在编译时捕获,而是在运行时捕获,如果涉及多个转换和聚合的 Spark 应用程序,这可能会非常昂贵且耗时。
Datasets
Datasets 通过提供类型安全来扩展 DataFrame。这意味着在前面提到的缺少列的示例中,Dataset API 将在编译时抛出错误。实际上,DataFrame 实际上是 Dataset[Row] 的别名,其中 Row 是一个无类型的对象,你可能在用 Java 编写的 Spark 应用程序中看到它。然而,由于 R 和 Python 没有编译时的类型安全,这意味着 Datasets 目前对这些语言不可用。
使用 DataFrame 和 Dataset API 而不是 RDD 的优势众多,包括更好的性能和更高效的内存使用。DataFrame 和 Dataset 提供的高级 API 还使得执行标准操作,如过滤、分组以及计算统计聚合(如总和和平均值)变得更加容易。然而,RDD 仍然有其用途,因为它提供了更高级别的控制,包括低级转换和操作。它们还在编译时提供分析错误,非常适合处理非结构化数据。
任务、阶段和任务
既然我们知道了 Spark 在计算处理过程中如何存储数据,让我们回到其逻辑架构,以了解 Spark 应用程序是如何逻辑上分解成更小的单元以进行分布式处理的。
任务
当在 Spark 应用程序中调用操作时,Spark 将使用依赖图来确定该操作所依赖的数据集,然后制定执行计划。执行计划本质上是一系列数据集,从最远的数据集开始,一直到最后需要计算以返回给驱动程序值的最终数据集,这些数据集都需要被计算。这个过程被称为 Spark 作业,每个作业对应一个操作。
阶段
如果 Spark 作业以及因此导致该作业启动的操作涉及数据洗牌(即数据的重新分配),那么该作业将被分解为阶段。当需要在工作节点之间进行网络通信时,开始一个新的阶段。因此,单个阶段被定义为由单个执行器处理的任务集合,这些任务对其他执行器没有依赖。
任务
任务是 Spark 中最小的执行单元,单个任务在一个执行器上执行;换句话说,单个任务不能跨越多个执行器。构成一个阶段的所有任务共享相同的执行代码,但作用于数据的不同分区。一个执行器可以处理的任务数量受该执行器关联的核心数限制。因此,整个 Spark 集群可以并行执行的任务总数可以通过将每个执行器的核心数乘以执行器的数量来计算。这个值提供了对 Spark 集群提供的并行程度的一个可量化的度量。
在第二章,“设置本地开发环境”,我们将讨论如何安装、配置和管理用于开发目的的单节点独立 Spark 集群,以及讨论 Spark 暴露的一些基本配置选项。然后,在第三章“人工智能与机器学习”及以后章节,我们将利用 Spark 的机器学习库MLlib,以便将 Spark 用作分布式高级分析引擎。要了解更多关于 Apache Spark 的信息,请访问spark.apache.org/。
分布式消息传递
在我们通过分布式系统的旅程中继续前进,下一个我们将讨论的类别是分布式消息系统。通常,现实世界的 IT 系统实际上是一系列不同的应用程序的集合,这些应用程序可能使用不同的底层技术和框架编写,并且相互集成。为了在独立的应用程序之间发送消息,开发者可能会将消费逻辑编码到每个单独的应用程序中。然而,这是一个坏主意——如果上游应用程序发送的消息类型发生变化会发生什么?在这种情况下,消费逻辑必须重写,相关应用程序更新,整个系统重新测试。
消息系统,如Apache ActiveMQ和RabbitMQ,通过提供一个名为消息代理的中介来解决此问题。图 1.10展示了消息代理在高层次上的工作方式:

图 1.10:消息代理高层次概述
在高层次上,生产者是生成和发送系统功能所需消息的应用程序。消息代理接收这些消息并将它们存储在队列数据结构或缓冲区中。消费者应用程序,即设计用于处理消息的应用程序,订阅消息代理。然后消息代理将这些消息传递给消费者应用程序,它们消费并处理这些消息。请注意,单个应用程序可以是生产者、消费者或两者兼具。
分布式消息系统,这是Apache Kafka的一个用例,通过能够进行分区和水平扩展来扩展传统消息系统,同时提供高吞吐量、高性能、容错性和复制,就像许多其他分布式系统一样。这意味着消息永远不会丢失,同时提供负载均衡请求和提供排序保证的能力。我们将在下一节更详细地讨论 Apache Kafka,但在此处是作为实时数据分布式流平台的一部分。
分布式流处理
想象一下处理存储在传统电子表格或基于文本的分隔文件(如 CSV 文件)中的数据。当使用这些类型的数据存储时,你通常会执行的处理类型被称为批量处理。在批量处理中,数据被整理成某种形式的组,在这种情况下,是我们电子表格或 CSV 文件中的行集合,并在未来的某个时间和日期作为一个组一起处理。通常,这些电子表格或 CSV 文件将在某个时间点用更新的数据刷新,此时将执行相同的或类似的处理,可能由某种类型的计划或计时器管理。传统上,数据处理系统会考虑到批量处理,包括传统的数据仓库。
然而,今天仅批处理是不够的。随着互联网、社交媒体和更强大的技术的出现,以及尽快(理想情况下立即)消费大量数据的需求,实时数据处理和分析不再是许多企业的奢侈品,而是一种必需品。实时数据处理至关重要的用例包括处理金融交易和实时定价、实时欺诈检测和打击严重有组织犯罪、物流、旅行、机器人技术和人工智能。
微批处理通过在更小的间隔(通常是秒或毫秒)和/或在更小的数据批次上执行来扩展标准批处理。然而,与批处理一样,数据仍然是一批一批处理的。
流处理与微批处理和批处理的不同之处在于,数据处理是在单个数据单元到达时执行的。例如,Apache Kafka 这样的分布式流式平台提供了在系统和应用程序之间安全且安全地移动实时数据的能力。此后,分布式流式引擎,如 Apache Spark 的流库Spark Streaming和Apache Storm,使我们能够处理和分析实时数据。在第八章,“使用 Apache Spark 进行实时机器学习”,我们将更详细地讨论 Spark Streaming,其中我们将通过结合 Apache Kafka、Spark Streaming 和 Apache Spark 的机器学习库MLlib来开发一个实时情感分析模型。
同时,让我们快速了解一下 Apache Kafka 在底层是如何工作的。花点时间思考一下,为了设计一个实时流平台,你需要考虑哪些方面:
-
容错性:平台不得丢失实时数据流,并且在系统部分故障的情况下,必须有一种方式来存储这些数据。
-
顺序性:平台必须提供一种方法来保证数据流可以按照接收的顺序进行处理,这对于业务应用来说尤为重要,因为顺序至关重要。
-
可靠性:平台必须提供一种可靠且高效地在各种不同应用程序和系统之间移动数据流的方法。
Apache Kafka 通过其分布式流式逻辑架构提供所有这些保证,如图图 1.11所示:

图 1.11:Apache Kafka 逻辑架构
在 Apache Kafka 中,主题指的是属于特定类别的记录流。Kafka 的 Producer API 允许生产者应用程序将记录流发布到一个或多个 Kafka 主题,而其 Consumer API 允许消费者应用程序订阅一个或多个主题,并随后接收和处理属于这些主题的记录流。Kafka 中的主题被认为是多订阅者,这意味着单个主题可以有零个、一个或多个消费者订阅它。在物理上,Kafka 主题存储为一系列有序且不可变的记录,这些记录只能追加,并在 Kafka 集群中分区和复制,从而为大型系统提供可伸缩性和容错性。Kafka 保证生产者消息以发送的顺序追加到主题分区,生产者应用程序负责识别将记录分配给哪个分区,并且消费者应用程序可以按它们持久化的顺序访问记录。
Kafka 因其逻辑架构和在其系统与应用程序之间移动实时数据流时提供的保证,已经成为实时数据的代名词。但 Kafka 也可以通过其 Streams API 作为流处理引擎使用,而不仅仅是作为消息系统。通过其 Streams API,Kafka 允许我们从输入主题消费连续的数据流,以某种方式处理这些数据,然后将其作为连续的数据流输出到输出主题。换句话说,Kafka 允许我们将输入数据流转换为输出数据流,从而有助于构建与其他流处理引擎(如 Apache Spark 和 Apache Storm)竞争的实时数据处理管道。
在第八章《使用 Apache Spark 的实时机器学习》中,我们将使用 Apache Kafka 可靠地将实时数据流从其源系统移动到 Apache Spark。然后,Apache Spark 将作为我们首选的流处理引擎,结合其机器学习库。与此同时,为了了解更多关于 Apache Kafka 的信息,请访问kafka.apache.org/。
分布式账本
为了完成我们对分布式系统的探索之旅,让我们谈谈一种可能成为未来大量激动人心的尖端技术基础的特定类型的分布式系统。分布式账本是分布式数据库中的一个特殊类别,最近由于区块链的普及以及随后的加密货币(如比特币)而闻名。
传统上,当你使用信用卡或借记卡购买时,发行银行充当中心化权威。作为交易的一部分,会向银行发出请求,以确认你是否拥有完成交易所需的足够资金。如果你有,银行会记录新的交易并从你的余额中扣除相应金额,让你完成购买。银行会记录这些以及你账户上的所有交易。如果你希望查看你的历史交易和当前的总余额,你可以通过在线账户记录或纸质报表来访问,所有这些都由受信任的中心来源——你的银行管理。
与此相反,分布式账本没有单一的受信任中心权威。相反,记录是独立创建并存储在形成分布式网络的单独节点上,换句话说,是一个分布式数据库,但数据永远不会由中心权威或主节点创建或传递。分布式网络中的每个节点都会处理每一笔交易。如果你使用基于区块链技术的加密货币,如比特币进行购买,这是一种分布式账本的形式,节点会对更新进行投票。一旦达到多数共识,分布式账本就会更新,并且账本的最新版本会分别保存在每个节点上。
如所述,区块链是分布式账本的一种形式。除了共享分布式账本的基本特征外,在区块链中,数据被分组到使用密码学加密的块中。一旦区块链中的记录被持久化,就无法更改或删除,但只能附加,这使得区块链特别适合需要维护安全历史视图的使用场景,例如金融交易和包括比特币在内的加密货币。
人工智能和机器学习
我们已经讨论了如何使用分布式系统来存储、建模和处理大量结构化、半结构化和非结构化数据,同时提供水平扩展性、容错性、弹性、高可用性、一致性和高吞吐量。然而,今天其他研究领域已经变得普遍,似乎与大数据的兴起有关——人工智能和机器学习。
但为什么这些研究领域,其背后的数学理论在某些情况下已经存在了几十年甚至几个世纪,会与大数据的兴起同时变得突出?这个问题的答案在于理解这种新技术提供的好处。
分布式系统使我们能够整合、聚合、转换、处理和分析大量以前分散的数据。整合这些分散数据集的过程使我们能够推断出以前不可能的见解和揭示隐藏的关系。此外,集群计算,如分布式系统提供的,暴露了更强大、数量更多的硬件和软件作为一个单一的逻辑单元协同工作,可以分配来解决人工智能和机器学习固有的复杂计算任务。今天,通过结合这些特性,我们可以高效地运行高级分析算法,最终提供前所未有的可操作见解,其水平和广度在许多主流行业中从未见过。
Apache Spark 的机器学习库 MLlib 和 TensorFlow 是一些库的例子,这些库被开发出来,使我们能够快速高效地将机器学习算法作为分析处理流程的一部分进行设计和执行。
在 第三章,《人工智能与机器学习》,我们将讨论一些常见人工智能和机器学习算法背后的高级概念,以及 Apache Spark 机器学习库 MLlib 背后的逻辑架构。此后,在第四章 监督学习使用 Apache Spark 到第八章 使用 Apache Spark 的实时机器学习 中,我们将使用 MLlib 和实际案例开发高级分析模型,同时探索其背后的数学理论。
要了解更多关于 MLlib 和 TensorFlow 的信息,请分别访问 spark.apache.org/mllib/ 和 www.tensorflow.org/。
云计算平台
传统上,许多大型组织投资于昂贵的数据中心来容纳其业务关键的计算系统。这些数据中心与其企业网络集成,使用户能够访问这些中心存储的数据以及增加的处理能力。大型组织维护自己的数据中心的主要优势之一是安全性——数据和处理能力都保留在本地,在其控制和管理之下,在主要封闭的网络中。
然而,随着大数据和更易于访问的人工智能和机器学习驱动的分析的出现,存储、建模、处理和分析需要可扩展硬件、软件,甚至可能包含数百或数千个物理或虚拟节点的分布式集群的大量数据的需求,很快使得维护自己的 24/7 数据中心变得越来越不经济。
云计算平台,如亚马逊网络服务(AWS)、微软 Azure和谷歌云平台(GCP),提供了一种将组织的一些或全部数据存储、管理和处理需求卸载到由技术公司管理且可通过互联网访问的远程平台的方法。如今,这些云计算平台提供的不仅仅是远程存储组织不断增长的数据资产的地方。它们提供可扩展的存储、计算处理、管理、数据治理和安全软件服务,以及访问人工智能和机器学习库和框架。这些云计算平台通常还提供按需付费(PAYG)的定价模式,这意味着您只需为实际使用的存储和处理能力付费,这也可以根据需求轻松扩展或缩减。
对于那些厌倦了将敏感数据存储在可通过互联网访问的远程平台上的组织,他们往往会构建和设计混合系统,其中敏感数据保留在本地,但计算处理则卸载到云端,例如。
一个设计良好且工程化的系统应在其基础设施和最终用户(包括数据分析师和数据科学家)之间提供一层抽象层,即数据存储和处理基础设施是本地还是基于云的,对这些用户来说应该是不可见的。此外,我们之前讨论的许多分布式技术和处理引擎通常是用 Java 或 C++编写的,但它们向其他语言变体(如 Python、Scala 和 R)暴露了它们的 API 或编程模型。这使得它们对广泛的最终用户可访问,可以在任何可以运行 JVM 或编译 C++代码的机器上部署。实际上,云计算平台提供的许多云服务都是围绕开源技术构建的商业服务包装器,确保了可用性和支持。因此,一旦系统管理员和最终用户熟悉了特定类别的技术,迁移到云计算平台实际上变成了一项优化性能的配置任务,而不是学习全新的存储、建模和处理数据的方法。这对许多组织来说很重要,因为它们的显著成本是员工的培训——如果尽可能多地重用底层技术和框架,那么这比迁移到全新的存储和处理范式更可取。
数据洞察平台
我们已经讨论了今天可用的各种系统、技术和框架,允许我们存储、聚合、管理、转换、处理和分析大量结构化、半结构化和非结构化数据,无论是批量还是实时,以便提供可操作的见解并实现真正的商业价值。我们将通过讨论所有这些系统和技术如何完全集成在一起,以提供一个综合的、高性能的、安全的和可靠的数据洞察平台,该平台可被组织的各个部分访问。
参考逻辑架构
我们可以将数据洞察平台表示为一系列逻辑层,其中每一层提供独特的功能能力。当我们结合这些层时,我们形成了一个数据洞察平台的参考逻辑架构,如图1.12所示:

图 1.12:数据洞察平台参考逻辑架构
在此数据洞察平台参考架构中,逻辑层在以下子节中进行了更详细的描述。
数据源层
数据源层代表各种不同的数据存储、数据集和其他源数据系统,它们将为数据洞察平台提供输入数据。这些不同的源数据提供者可能包含结构化数据(例如,定界文本文件、CSV 文件和关系型数据库)、半结构化数据(例如,JSON 和 XML 文件)或非结构化数据(例如,图像、视频和音频)。
摄取层
摄取层负责消费源数据,并将其移动到持久化数据存储或直接到下游数据处理引擎,无论其格式和频率如何。摄取层应能够支持批数据以及基于流的实时事件数据。用于实现摄取层的开源技术示例包括以下内容:
-
Apache Sqoop
-
Apache Kafka
-
Apache Flume
持久化数据存储层
持久化数据存储层负责消费和持久化由摄取层提供的原始源数据。在持久化之前,原始源数据几乎不进行任何转换,以确保原始数据保持其原始格式。数据湖是一种持久化数据存储类别,通常以这种方式存储原始数据。用于实现持久化数据存储层的示例技术包括以下内容:
-
传统基于网络存储,例如存储区域网络(SAN)和网络附加存储(NAS)
-
开源技术,例如 HDFS
-
基于云的技术,例如AWS S3和Azure BLOBs
数据处理层
数据处理层负责对从持久化数据存储或直接从摄取层收集的原始数据进行转换、丰富和验证。数据处理层根据下游业务和分析需求对数据进行建模,并为其在服务数据存储层中的持久化或由数据智能应用进行处理做准备。同样,数据处理层必须能够处理批量数据和基于流的实时事件数据。以下是一些用于实现数据处理层的开源技术的示例:
-
Apache Hive
-
Apache Spark,包括 Spark Streaming(DStreams)和 Structured Streaming
-
Apache Kafka
-
Apache Storm
服务数据存储层
服务分析数据存储层负责将数据处理层生成的转换、丰富和验证后的数据持久化到数据存储中,这些数据存储维护数据模型结构,以便为下游数据智能和数据洞察应用提供服务。由于数据已经以与所需处理类型高度优化的数据结构持久化,因此这最小化或消除了进一步数据转换的需要。服务分析数据存储层提供的数据存储类型取决于业务和分析需求,可能包括以下任何一种或多种(附带开源实现示例):
-
关系型数据库,例如 PostgreSQL 和 MySQL
-
文档数据库,例如 Apache CouchDB 和 MongoDB
-
列式数据库,例如 Apache Cassandra 和 Apache HBase
-
键值数据库,例如 Redis 和 Voldemort
-
图数据库和框架,例如 Apache TinkerPop
-
搜索引擎,例如 Apache Lucene 和 Elasticsearch
数据智能层
数据智能层负责在转换后的批量数据和基于流的实时事件数据上执行高级分析管道,包括预测性和规范性分析。高级分析管道可能包括人工智能服务,如图像和语音分析、认知计算和复杂推理,以及机器学习模型和自然语言处理。以下是一些用于实现数据智能层的开源高级分析库和框架的示例:
-
Apache Spark MLlib(通过 Python、R、Java 和 Scala 访问 API)
-
TensorFlow (通过 Python、Java、C++、Go 和 JavaScript 访问 API,C#、R、Haskell、Ruby 和 Scala 提供第三方绑定)
统一访问层
统一访问层负责提供对服务分析数据存储层和由数据智能层公开的第三方 API 的访问。统一访问层应提供通用、可扩展和安全的访问,以满足任何需要它的下游应用程序和系统,通常涉及 API 架构和/或数据联邦和虚拟化模式的实现。用于实现统一访问层的开源技术示例包括以下:
-
Spring 框架
-
Apache Drill
数据洞察和报告层
数据洞察和报告层负责向最终用户(包括数据分析师和数据科学家)公开数据洞察平台。数据发现、自助式商业智能、搜索、可视化、数据洞察和交互式分析应用程序都可以在此层提供,并且可以通过统一访问层访问服务分析数据存储层中的转换数据以及数据智能层中的第三方 API,所有这些都可以通过统一访问层实现。数据洞察和报告层的基本目的是提供从数据洞察平台可用的所有结构化、半结构化和非结构化数据中得出的可操作洞察和业务价值。用于实现数据洞察和报告层且对最终用户也开放的开源技术示例包括以下:
-
Apache Zeppelin
-
Jupyter Notebook
-
Elastic Kibana
-
BIRT 报告
-
基于自定义 JavaScript 的用于搜索和可视化的 Web 应用程序
商业技术的示例包括以下用于创建仪表板和报告的商业智能应用程序:
-
Tableau
-
Microsoft Power BI
-
SAP Lumira
-
QlikView
平台治理、管理和行政
最后,由于数据洞察平台旨在被组织的所有领域访问,并且将存储导致可操作洞察生成的敏感数据,因此它所包含的系统必须得到适当的治理、管理和行政。因此,为了提供安全的企业和生产级平台,需要以下额外的逻辑层:
-
治理和安全:此层包括身份和访问管理(IDAM)和数据治理工具。用于实现此层的开源技术示例包括以下:
-
Apache Knox(Hadoop 认证网关)
-
Apache Metron(安全分析框架)
-
Apache Ranger(监控数据安全)
-
OpenLDAP(轻量级目录访问协议实现)
-
-
管理、行政和编排:此层包括 DevOps 流程(如版本控制、自动化构建和部署)、集群监控和管理软件以及调度和流程管理工具。用于实现此层的开源技术示例包括以下:
-
Jenkins(自动化服务器)
-
Git(版本控制)
-
Apache Ambari(管理、监控和管理)
-
Apache Oozie(工作流调度)
-
-
网络和访问中间件:这一层处理网络连接和通信,包括网络安全、监控和入侵检测软件。
-
硬件和软件:这一层包含数据洞察平台部署的物理存储、计算和网络基础设施。物理组件可能位于本地、基于云,或为两者的混合组合。
开源实现
图 1.13 展示了仅使用开源技术和框架实现的参考数据洞察平台的示例:

图 1.13:数据洞察平台开源实现的示例
摘要
在本章中,我们探索了一种新型分布式和可扩展的技术,这些技术使我们能够可靠且安全地存储、管理、建模、转换、处理和分析大量结构化、半结构化和非结构化数据,无论是批量还是实时,以便使用高级分析得出实际可操作的见解。
在下一章中,我们将指导您如何使用这些技术的一部分,包括 Apache Spark、Apache Kafka、Jupyter Notebook、Python、Java 和 Scala,来安装、配置、部署和管理单节点分析开发环境!
第二章:设置本地开发环境
在本章中,我们将通过配置一个自包含的单节点集群来安装、配置和部署一个本地分析开发环境,这将使我们能够执行以下操作:
-
在 Python 中原型设计和开发机器学习模型和管道
-
通过 Spark Python API (PySpark) 展示 Apache Spark 的机器学习库
MLlib的功能和用法 -
使用小型样本数据集在单节点集群上开发和测试机器学习模型,然后扩展到多节点集群,处理更大的数据集,而无需或只需进行少量代码更改
我们的单节点集群将托管以下技术:
-
操作系统:CentOS Linux 7
-
通用编程语言:
-
Java SE 开发工具包 (JDK) 8 (8u181)
-
Scala 2.11.x (2.11.12)
www.scala-lang.org/download/all.html -
通过 Anaconda 5.x (5.2) Python 发行版使用 Python 3.x (3.6)
-
-
通用分布式处理引擎:Apache Spark 2.3.x (2.3.2)
-
分布式流平台:Apache Kafka 2 (2.0.0)
CentOS Linux 7 虚拟机
首先,我们将假设您有权访问一个配置了 CentOS 7 操作系统的物理或虚拟机。CentOS 7 是一个由 Red Hat Enterprise Linux (RHEL)派生的免费 Linux 发行版。它通常与它的授权上游父级 RHEL 一起使用,作为基于 Linux 服务器的首选操作系统,因为它稳定,并得到一个庞大活跃社区的支持,拥有详细的文档。我们将使用所有这些命令来安装之前列出的各种技术,这些命令将是需要在单个 CentOS 7(或 RHEL)机器上执行的 Linux shell 命令,无论是物理机还是虚拟机。如果您没有访问 CentOS 7 机器,那么有许多选项可以配置 CentOS 7 虚拟机:
-
云计算平台,如 Amazon Web Services (AWS)、Microsoft Azure 和 Google Cloud Platform (GCP),都允许您使用 按量付费 (PAYG)定价模式来启动虚拟机。通常,主要的云计算平台还为新手用户提供免费层,包含一定量的免费容量,以便试用他们的服务。要了解更多关于这些主要云计算平台的信息,请访问以下网站:
-
AWS:
aws.amazon.com/ -
Microsoft Azure:
azure.microsoft.com -
GCP:
cloud.google.com/
-
-
虚拟专用服务器(VPS)托管公司,如Linode和Digital Ocean,也提供配置低成本 CentOS 虚拟机的能力。这些 VPS 提供商通常采用一个更简单的定价模型,仅包括各种规格的虚拟机。要了解更多关于这些主要 VPS 提供商的信息,请访问以下网站:
-
Linode:
www.linode.com/ -
Digital Ocean:
www.digitalocean.com/
-
-
一个常见且免费的选项,尤其是在用于原型设计和测试的本地开发环境中,是配置由您个人物理桌面或笔记本电脑托管的虚拟机。如Oracle VirtualBox(开源)和VMWare Workstation Player(个人使用免费)这样的虚拟化软件允许您在自己的个人物理设备上设置和运行虚拟机。要了解更多关于这些虚拟化软件服务的信息,请访问以下网站:
-
Oracle VirtualBox:
www.virtualbox.org/ -
VMWare Workstation Player:
www.vmware.com/
-
在本章剩余部分,我们将假设您已经配置了一台 64 位的 CentOS 7 机器,并且您可以直接通过桌面访问它,或者通过 HTTP 和 SSH 协议通过网络访问它。尽管您的虚拟机规格可能不同,但我们仍建议以下最低虚拟硬件要求,以便有效地运行本书剩余部分中的示例:
-
操作系统:CentOS 7(最小安装)
-
虚拟 CPU:4
-
内存:8 GB
-
存储:20 GB
在我们的案例中,我们的虚拟机具有以下网络属性,并将在此之后被如此引用。这些属性对于您的虚拟机可能会有所不同:
-
静态 IP 地址:
192.168.56.10 -
子网掩码:
255.255.255.0 -
完全限定域名(FQDN):
packt.dev.keisan.io
注意,您的虚拟机的安全性以及它所承载的后续软件服务(包括大数据技术)超出了本书的范围。如果您想了解更多关于如何加固基础操作系统和常见软件服务以抵御外部攻击的信息,我们建议访问www.cisecurity.org/cis-benchmarks/以及各个软件服务网站本身,例如spark.apache.org/docs/latest/security.html以了解 Apache Spark 的安全性。
Java SE 开发工具包 8
Java 是一种通用编程语言,常用于 面向对象编程(OOP)。我们在 第一章,“大数据生态系统”中讨论的许多分布式技术最初是用 Java 编写的。因此,我们需要在我们的本地开发环境中安装 Java 开发工具包(JDK),以便在 Java 虚拟机(JVM)中运行这些软件服务。Apache Spark 2.3.x 需要 Java 8+ 才能运行。要安装 Oracle Java SE 开发工具包 8,请以 Linux root 用户或具有提升权限的其他用户身份执行以下 shell 命令:
> rpm -ivh jdk-8u181-linux-x64.rpm
> vi /etc/profile.d/java.sh
$ export PATH=/usr/java/default/bin:$PATH
$ export JAVA_HOME=/usr/java/default
> source /etc/profile.d/java.sh
> echo $PATH
> echo $JAVA_HOME
这些命令将安装 JDK 8,并在之后将 Java 二进制文件的路径添加到全局 PATH 变量中,使得任何本地 Linux 用户都可以运行基于 Java 的程序。为了检查 Java 8 是否已成功安装,以下命令应返回已安装的 Java 版本,如下所示:
> java –version
$ java version "1.8.0_181"
$ Java(TM) SE Runtime Environment (build 1.8.0_181-b13)
$ Java HotSpot(TM) 64-Bit Server VM (build 25.181-b13, mixed mode)
Scala 2.11
Scala 是一种通用编程语言,用于面向对象编程和函数式编程。Apache Spark 实际上是用 Scala 编程语言编写的。然而,如 第一章 所述,“大数据生态系统”,Spark 应用程序可以用多种语言编写,包括 Java、Scala、Python 和 R。尽管 Scala 与 Python 的优缺点超出了本书的范围,但 Scala 在数据分析的上下文中通常比 Python 快,并且与 Spark 的集成更为紧密。然而,Python 目前提供了更全面的先进第三方数据科学工具和框架库,并且可以说是更容易学习和使用。本书提供的代码示例是用 Python 3 编写的。然而,本节描述了如果您想开发基于 Scala 的应用程序,所需的安装步骤。
具体到 Scala,Apache Spark 2.3.2 需要 Scala 2.11.x 才能运行基于 Scala 的 Spark 应用程序。为了安装 Scala 2.11.12,请以 Linux 根用户或具有提升权限的其他用户身份执行以下 shell 命令:
> rpm -ivh scala-2.11.12.rpm
这些命令将安装 Scala 2.11.12 并将其二进制文件放置在全局可访问的位置,使得任何本地 Linux 用户都可以运行 Scala 应用程序,无论是否基于 Spark。为了检查 Scala 2.11.12 是否已成功安装,以下命令应返回已安装的 Scala 版本:
> scala –version
$ Scala code runner version 2.11.12
您还可以通过以下方式访问 Scala shell 并执行交互式 Scala 命令:
> scala
>> 1+2
$ res0: Int = 3
>> :q
Anaconda 5 与 Python 3
Anaconda 是 Python 通用编程语言的发行版。它不仅包含 Python 解释器,而且还捆绑了大量的常用 Python 数据科学包,以及一个名为conda的 Python 包管理系统,这使得快速轻松地提供基于 Python 的数据科学平台变得可能。实际上,我们将在后面的章节中利用一些预捆绑的 Python 包。
Anaconda 5.2 捆绑了 Python 3.6。Apache Spark 2.3.x 支持 Python 的两个分支,即 Python 2 和 Python 3。具体来说,它支持 Python 2.7+和 Python 3.4+。如前所述,本书提供的代码示例是用 Python 3 编写的。
为了安装 Anaconda 5.2,请执行以下 shell 命令。您可以选择是否以 Linux root 用户执行这些 shell 命令。如果您不这样做,Anaconda 将为运行这些命令的本地 Linux 用户安装,并且不需要管理员权限:
> bash Anaconda3-5.2.0-Linux-x86_64.sh
> Do you accept the license terms? [yes|no]
>>> yes
> Do you wish the installer to prepend the Anaconda3 install location to PATH in your .bashrc ? [yes|no]
>>> yes
The last command will add the location of the Anaconda, and hence Python, binaries to your local PATH variable, allowing your local Linux user to run both Python-based programs (overriding any existing Python interpreters already installed on the operating system) and conda commands. Note that you may need to open a new Linux shell in order for the local PATH updates to take effect.
为了检查 Anaconda 5.2 及其 Python 3.6 是否已成功安装,以下命令应返回已安装的 conda 版本:
> conda --version
$ conda 4.5.4
您还可以访问 Python shell 并执行交互式 Python 命令,如下所示:
> python
$ Python 3.6.5 | Anaconda, Inc.
>>> import sys
>>> sys.path
>>> quit()
基本 conda 命令
在本小节中,我们将提供一些基本的 conda 命令供您参考。这些命令假设您的虚拟机可以访问互联网或本地 Python 仓库。
为了升级 conda 版本或整个 Anaconda,您可以执行以下命令:
> conda update conda
> conda update anaconda
要安装或更新单个 Python 包,您可以执行以下命令:
> conda install <name of Python package>
> conda update <name of Python package>
最后,为了列出您 Anaconda 发行版中当前安装的 Python 包及其版本,您可以执行以下命令:
> conda list
要了解更多关于 conda 包管理系统的信息,请访问conda.io/docs/index.html。
额外的 Python 包
以下 Python 包,这些包尚未包含在默认的 Anaconda 发行版中,是我们本地开发环境所必需的。请执行以下 shell 命令来安装这些先决条件 Python 包:
> conda install -c conda-forge findspark
> conda install -c conda-forge pykafka
> conda install -c conda-forge tweepy
> conda install -c conda-forge tensorflow
> conda install -c conda-forge keras
Jupyter Notebook
Jupyter Notebook 是一个开源的、基于 Web 的应用程序,专为交互式分析设计,它包含在 Anaconda 发行版中。由于它专为交互式分析设计,因此非常适合即席查询、实时模拟、原型设计和在开发生产就绪的数据科学模型之前可视化数据和寻找任何趋势和模式。Apache Zeppelin是另一个用于类似目的的开源、基于 Web 的笔记本示例。Jupyter Notebook 和 Apache Zeppelin 这样的笔记本通常支持多个内核,这意味着您可以使用包括 Python 和 Scala 在内的各种通用编程语言。
笔记本的一个核心优势是它们可以持久化您的输入代码以及任何由您的代码生成的输出数据结构和可视化,包括图表、图表和表格。然而,它们并不是完整的集成开发环境(IDE)。这意味着,通常情况下,它们不应用于开发用于生产级数据工程或分析管道的代码。这是因为它们在版本控制系统(如 Git)中难以管理(但并非不可能),因为它们持久化了输入代码和中间输出结构。因此,它们也难以构建代码工件并使用典型的 DevOps 管道自动部署。因此,笔记本非常适合交互式分析、即兴查询和原型设计。
为本书提供的代码文件实际上大多数是 Jupyter Notebook 文件(.ipynb),使用 Python 3 内核,以便读者可以立即看到我们模型的输出。如果您将来希望编写最终将部署到生产级系统的数据科学代码,我们强烈建议您在以下适当的 IDE 中编写代码:
-
Eclipse:
www.eclipse.org/ide/ -
IntelliJ IDEA:
www.jetbrains.com/idea/ -
PyCharm:
www.jetbrains.com/pycharm/ -
微软 Visual Studio Code(VS Code):
code.visualstudio.com/
如前所述,Jupyter Notebook 已经包含在 Anaconda 发行版中。然而,为了访问它,建议执行以下配置步骤。请以您的本地 Linux 用户身份执行以下 shell 命令,以生成一个基于用户的 Jupyter Notebook 配置文件,然后根据您的个人偏好进行编辑:
> jupyter notebook --generate-config
$ Writing default config to: /home/packt/.jupyter/jupyter_notebook_config.py
> vi /home/packt/.jupyter/jupyter_notebook_config.py
Line 174: c.NotebookApp.ip = '192.168.56.10'
Line 214: c.NotebookApp.notebook_dir = '/data/workspaces/packt/jupyter/notebooks/'
Line 240: c.NotebookApp.port = 8888
这些命令将配置一个基于用户的 Jupyter Notebook 实例,使其在指定的 IP 地址(在我们的例子中为 192.168.56.10)上监听,并使用指定的端口(在我们的例子中为 8888),以及从一个预定义的基本目录中工作,以持久化 Jupyter Notebook 代码文件(在我们的例子中为 /data/workspaces/packt/jupyter/notebooks)。请注意,您应根据您的具体环境修改这些属性。
启动 Jupyter Notebook
如果您可以通过桌面访问您的 CentOS 虚拟机,以最简单的方式实例化一个新的基于用户的 Jupyter Notebook 实例,请以您的本地 Linux 用户身份执行以下 shell 命令:
> jupyter notebook
然而,如果您只有 SSH 或命令行访问而没有 GUI,那么您应该使用以下命令代替:
> jupyter notebook --no-browser
后一个命令将阻止 Jupyter 自动打开本地浏览器会话。在任何情况下,生成的日志都将显示可以用来访问您的 Jupyter Notebook 实例的完整 URL(默认情况下包括安全令牌)。URL 应该类似于以下内容:
http://192.168.56.10:8888/?token=6ebb5f6a321b478162802a97b8e463a1a053df12fcf9d99c
请将此 URL 复制并粘贴到支持 Jupyter Notebook 的互联网浏览器中(Google Chrome、Mozilla Firefox 或 Apple Safari)。如果成功,应返回类似于 图 2.1 中所示截图的屏幕:

图 2.1:Jupyter Notebook 网络会话
Jupyter Notebook 故障排除
由于 Jupyter Notebook 是一个基于 Web 的应用程序,它可以通过指定的端口号通过 HTTP 协议访问。如果您通过远程互联网浏览器访问生成的 URL 并且无法连接,那么请检查您虚拟机上的防火墙设置(在 CentOS 和 RHEL 的情况下,还包括 SELinux),以确保从您的位置可以访问指定的端口号。例如,以下由 Linux root 用户或具有提升权限的其他用户执行的 shell 命令,将通过其公共区域在 CentOS 7 防火墙中打开端口 8888:
> firewall-cmd --get-active-zones
> firewall-cmd --zone=public --add-port=8888/tcp --permanent
> firewall-cmd --reload
> firewall-cmd --list-all
请联系您的系统管理员或参考您的云平台文档以获取更多有关网络信息和故障排除的信息。
要了解更多关于 Jupyter Notebook、其配置和常见故障排除的信息,请访问 jupyter-notebook.readthedocs.io/en/stable/index.html。
Apache Spark 2.3
如 第一章 所述,大数据生态系统,Apache Spark 是一个通用目的的分布式处理引擎,能够在数 PB 的数据规模上执行数据转换、高级分析、机器学习和图分析。Apache Spark 可以以独立模式(意味着我们利用其内置的集群管理器)或与其他第三方集群管理器(包括 Apache YARN 和 Apache Mesos)集成的方式部署。
在我们单节点开发集群的情况下,我们将以独立模式部署 Apache Spark,其中我们的单节点将托管 Apache Spark Standalone Master 服务器和一个单独的工作节点实例。由于 Spark 软件服务旨在在 JVM 中运行,将独立主进程和工作进程同时放置在单个节点上是完全可接受的,尽管在实际的 Apache Spark 实施中,集群可以更大,配置了多个工作节点。我们的单节点 Apache Spark 集群仍然允许我们原型设计和开发可以利用多核单机提供的并行性的 Spark 应用程序和机器学习模型,并且之后可以轻松地部署到更大的集群和数据集。
还请注意,我们将直接从 Apache Spark 官方网站提供的预构建二进制文件安装 Apache Spark 2.3.2,网址为 spark.apache.org/downloads.html。如今,像 Spark、Kafka 和 Hadoop 组件这样的分布式技术通常通过像 Hortonworks Data Platform (HDP)、Cloudera 和 MapR 提供的统一大数据平台一起安装。使用这些统一平台的好处包括部署经过完全测试并保证完全相互集成的单个组件版本,以及基于网络的安装、监控、管理和支持。
Spark 二进制文件
请以本地 Linux 用户身份执行以下 Shell 命令以提取 Apache Spark 二进制文件。在我们的案例中,我们将把 Spark 二进制文件安装到 /opt:
> tar -xzf spark-2.3.2-bin-hadoop2.7.tgz -C /opt
结果的 Spark 父目录将具有以下结构:
-
bin:本地 Spark 服务的 Shell 脚本,例如spark-submit -
sbin:Shell 脚本,包括启动和停止 Spark 服务 -
conf:Spark 配置文件 -
jars:Spark 库依赖项 -
python:Spark 的 Python API,称为 PySpark -
R:Spark 的 R API,称为 SparkR
本地工作目录
Spark 集群中的每个节点(在我们的案例中,仅为单个节点)都会生成日志文件以及本地工作文件,例如在洗牌和序列化 RDD 数据时。以下命令将在定义的本地目录中创建用于存储这些本地工作输出的目录,您可以根据您的偏好编辑路径,这些路径将在后续配置文件中使用:
> mkdir -p /data/spark/local/data
> mkdir -p /data/spark/local/logs
> mkdir -p /data/spark/local/pid
> mkdir -p /data/spark/local/worker
Spark 配置
配置可以通过以下方式应用于 Spark:
-
Spark 属性 控制应用程序级别的设置,包括执行行为、内存管理、动态分配、调度和安全,这些可以在以下优先级顺序中定义:
-
通过在驱动程序中定义的名为
SparkConf的 Spark 配置程序性对象 -
通过传递给
spark-submit或spark-shell的命令行参数 -
通过在
conf/spark-defaults.conf中设置的默认选项
-
-
环境变量 控制每台机器的设置,例如本地工作节点的本地 IP 地址,这些可以在
conf/spark-env.sh中定义。
Spark 属性
在我们的案例中,我们将通过 conf/spark-defaults.conf 设置一些基本的默认 Spark 属性,这样我们就可以专注于未来章节中的数据科学内容。这可以通过执行以下 Shell 命令实现(根据您的环境编辑值):
> cp conf/spark-defaults.conf.template conf/spark-defaults.conf
> vi conf/spark-defaults.conf
$ spark.master spark://192.168.56.10:7077
$ spark.driver.cores 1
$ spark.driver.maxResultSize 0
$ spark.driver.memory 2g
$ spark.executor.memory 2g
$ spark.executor.cores 2
$ spark.serializer org.apache.spark.serializer.KryoSerializer
$ spark.rdd.compress true
$ spark.kryoserializer.buffer.max 128m
环境变量
我们还将通过 conf/spark-env.sh 设置基本的环境变量,如下所示(根据您的环境编辑值):
> cp conf/spark-env.sh.template conf/spark-env.sh
> vi conf/spark-env.sh
$ export SPARK_LOCAL_IP=192.168.56.10
$ export SPARK_LOCAL_DIRS=/data/spark/local/data
$ export SPARK_MASTER_HOST=192.168.56.10
$ export SPARK_WORKER_DIR=/data/spark/local/worker
$ export SPARK_CONF_DIR=/opt/spark-2.3.2-bin-hadoop2.7/conf
$ export SPARK_LOG_DIR=/data/spark/local/logs
$ export SPARK_PID_DIR=/data/spark/local/pid
要了解有关各种 Spark 配置选项的更多信息,包括 Spark 属性和环境变量的详尽列表,请访问 spark.apache.org/docs/latest/configuration.html。
独立主服务器
我们现在可以启动 Spark 独立主服务器,如下所示:
> sbin/start-master.sh
为了检查是否成功,您可以检查写入 SPARK_LOG_DIR 的 Spark 日志。Spark 应用程序可以通过其 REST URL 提交到独立主服务器 spark://<Master IP 地址>:7077(默认端口 7077)或 spark://<Master IP 地址>:6066(默认端口 6066)。
Spark Master 服务器还提供了一个开箱即用的主网页 用户界面(UI),在其中可以监控正在运行的 Spark 应用程序和工作节点,并诊断性能。默认情况下,此主网页 UI 通过 HTTP 在端口 8080 上可访问,换句话说,http://<Master IP 地址>:8080,其界面如图 2.2 所示:

图 2.2:Spark 独立主服务器网页界面
再次,如果您无法通过远程互联网浏览器访问此 URL,您可能需要在防火墙和/或 SELinux 设置中打开端口 8080(默认)。
Spark 工作节点
我们现在可以启动我们的 Spark Worker 节点,如下所示:
> sbin/start-slave.sh spark://192.168.56.10:7077
再次,为了检查是否成功,您可以检查写入 SPARK_LOG_DIR 的 Spark 日志。您还可以访问 Spark Master 网页界面以确认工作节点已成功注册,如图 2.3 所示:

图 2.3:Spark 工作节点成功注册
注意,Spark 工作节点默认通过 HTTP 在端口 8081 上暴露一个 Worker UI,换句话说,http://<Worker IP 地址>:8081。
PySpark 和 Jupyter Notebook
现在让我们将 Jupyter Notebook 与 PySpark 集成,这样我们就可以用 Python 编写我们的第一个 Spark 应用程序了!在我们的本地开发环境中,将 Jupyter Notebook 与 PySpark 集成的最简单方法是为包含 Spark 二进制文件的目录设置一个全局 SPARK_HOME 环境变量。之后,我们可以使用之前安装的 findspark Python 包,该包将在运行时将 SPARK_HOME 的位置和 PySpark API 添加到 sys.path。请注意,findspark 不应用于生产级代码开发——相反,Spark 应用程序应作为通过 spark-submit 提交的代码工件进行部署。
请以 Linux 根用户或具有提升权限的其他用户身份执行以下 shell 命令,以便定义一个名为 SPARK_HOME 的全局环境变量(或者,也可以将其添加到您的本地 Linux 用户的 .bashrc 文件中,这不需要管理员权限):
> cd /etc/profile.d
> vi spark.sh
$ export SPARK_HOME=/opt/spark-2.3.2-bin-hadoop2.7
> source spark.sh
为了使 SPARK_HOME 环境变量被 findspark 成功识别和注册,你需要重新启动任何正在运行的 Jupyter Notebook 实例及其底层的终端会话。
现在我们已经准备好用 Python 编写我们的第一个 Spark 应用程序了!实例化一个 Jupyter Notebook 实例,通过你的网络浏览器访问它,并创建一个新的 Python 3 笔记本,包含以下代码(为了将来方便引用,你可能需要将以下代码拆分到单独的笔记本单元中):
# (1) Import required Python dependencies
import findspark
findspark.init()
from pyspark import SparkContext, SparkConf
import random
# (2) Instantiate the Spark Context
conf = SparkConf()
.setMaster("spark://192.168.56.10:7077")
.setAppName("Calculate Pi")
sc = SparkContext(conf=conf)
# (3) Calculate the value of Pi i.e. 3.14...
def inside(p):
x, y = random.random(), random.random()
return x*x + y*y < 1
num_samples = 100
count = sc.parallelize(range(0, num_samples)).filter(inside).count()
pi = 4 * count / num_samples
# (4) Print the value of Pi
print(pi)
# (5) Stop the Spark Context
sc.stop()
这个 PySpark 应用程序在高层上工作如下:
-
导入所需的 Python 依赖项,包括
findspark和pyspark -
通过实例化一个
SparkConf对象来创建一个 Spark 上下文,该对象提供高级别的应用程序级设置,从而告诉 Spark 应用程序如何连接到 Spark 集群 -
计算数学值π
-
打印 Pi 的值,并在 Jupyter Notebook 中以单元格输出的形式显示它
-
停止 Spark 上下文,这将终止 Spark 应用程序
如果你在执行sc.stop()之前访问 Spark Master 的 Web UI,Spark 应用程序将列在“运行中的应用程序”下,此时你可以查看其底层的 worker 和 executor 日志文件。如果你在执行sc.stop()之后访问 Spark Master 的 Web UI,Spark 应用程序将列在“已完成的应用程序”下。
注意,这个笔记本可以从本书配套的 GitHub 仓库中下载,并命名为chp02-test-jupyter-notebook-with-pyspark.ipynb。
Apache Kafka 2.0
为了完成我们的本地开发环境,我们将安装 Apache Kafka。如第一章,“大数据生态系统”中所述,Apache Kafka 是一个分布式流平台。我们将在第八章,“使用 Apache Spark 进行实时机器学习”中使用 Apache Kafka,通过结合 Spark Streaming 和MLlib来开发一个实时分析模型。
再次,为了我们的单节点开发集群,Apache Kafka 将部署在与 Apache Spark 软件服务相同的单节点上。我们还将安装为 Scala 2.11 构建的 Apache Kafka 2.0.0 版本。
Kafka 二进制文件
在下载 Kafka 发布版后,我们首先需要做的是在我们的单节点集群上提取和安装预编译的二进制文件。在我们的案例中,我们将把 Kafka 二进制文件安装到/opt。请以本地 Linux 用户身份执行以下 shell 命令来提取 Apache Kafka 二进制文件:
> tar -xzf kafka_2.11-2.0.0.tgz -C /opt
> cd /opt/kafka_2.11-2.0.0
本地工作目录
与 Apache Spark 进程一样,Apache Kafka 进程也需要它们自己的本地工作目录来持久化本地数据和日志文件。以下命令将创建定义好的本地目录,用于存储这些本地工作输出,您可以根据自己的偏好编辑这些路径:
> mkdir -p /data/zookeeper/local/data
> mkdir -p /data/kafka/local/logs
Kafka 配置
我们还将设置一些基本配置,如下(根据您的环境编辑值):
> vi config/zookeeper.properties
$ dataDir=/data/zookeeper/local/data
> vi config/server.properties
$ listeners=PLAINTEXT://192.168.56.10:9092
$ log.dirs=/data/kafka/local/logs
$ zookeeper.connect=192.168.56.10:2181
启动 Kafka 服务器
现在,我们已准备好按照以下方式启动 Apache Kafka:
> bin/zookeeper-server-start.sh -daemon config/zookeeper.properties
> bin/kafka-server-start.sh -daemon config/server.properties
测试 Kafka
最后,我们可以通过创建一个测试主题来测试我们的 Kafka 安装,如下所示:
> bin/kafka-topics.sh --create --zookeeper 192.168.56.10:2181 --replication-factor 1 --partitions 1 --topic our-first-topic
$ Created topic "our-first-topic".
> bin/kafka-topics.sh --list --zookeeper 192.168.56.10:2181
$ our-first-topic
一旦我们创建了测试主题,让我们启动一个命令行生产者应用程序,并按照以下方式向该主题发送一些测试消息:
> bin/kafka-console-producer.sh --broker-list 192.168.56.10:9092 --topic our-first-topic
> This is my 1st test message
> This is my 2nd test message
> This is my 3rd test message
最后,让我们在另一个终端会话中启动一个命令行消费者应用程序,以消费这些测试消息并将它们打印到控制台,如下所示:
> bin/kafka-console-consumer.sh --bootstrap-server 192.168.56.10:9092 --topic our-first-topic --from-beginning
$ This is my 1st test message
$ This is my 2nd test message
$ This is my 3rd test message
事实上,如果您在运行生产者应用程序的终端中继续输入新消息,您将立即看到它们被消费者应用程序消费,并在其终端中打印出来!
摘要
在本章中,我们已经安装、配置和部署了一个由单个节点 Apache Spark 2.3.2 和 Apache Kafka 2.0.0 集群组成的本地分析开发环境,这将使我们能够通过 Jupyter Notebook 使用 Python 3.6 交互式开发 Spark 应用程序。
在下一章中,我们将讨论一些常见人工智能和机器学习算法背后的高级概念,以及介绍 Apache Spark 的机器学习库MLlib!
第三章:人工智能与机器学习
在本章中,我们将定义我们所说的人工智能、机器学习和认知计算。我们将研究机器学习领域内常见的算法类别及其更广泛的应用,包括以下内容:
-
监督学习
-
无监督学习
-
强化学习
-
深度学习
-
自然语言处理
-
认知计算
-
Apache Spark 的机器学习库
MLlib以及如何将其用于在机器学习管道中实现这些算法
人工智能
人工智能是一个广泛的术语,用于描述表现出智能行为的机器的理论和应用。人工智能包括许多应用研究领域,包括机器学习和随后的深度学习,如图3.1所示:

图 3.1:人工智能概述
机器学习
机器学习是人工智能更广泛主题下的一个应用研究领域,它通过检测数据中的模式、趋势和关系来学习,以便进行预测,并最终提供可操作的见解以帮助决策。机器学习模型可以分为三种主要类型:监督学习、无监督学习和r强化学习*。
监督学习
在监督学习中,目标是学习一个函数,该函数能够将输入x映射到输出y,给定一个标记过的输入-输出对集D,其中D被称为训练集,N是训练集中的输入-输出对的数量:

在监督学习模型的简单应用中,每个训练输入x[i]是一个表示模型特征(如价格、年龄和温度)的数值向量。在复杂应用中,x[i]可能代表更复杂的对象,如时间序列、图像和文本。
当输出y[i](也称为响应变量)在本质上属于分类性质时,这个问题被称为分类问题,其中y[i]属于一个由K个元素或可能的分类组成的有限集:

当输出y[i]是一个实数时,这个问题被称为回归问题。
这在实践中意味着什么呢?嗯,训练集D本质上是一个已经将输入特征映射到输出的数据集。换句话说,我们已经知道训练数据集的答案——它是标记过的。例如,如果问题是根据在线广告的花费来预测电子商务网站的月销售额(即回归问题),训练数据集就已经将广告成本(输入特征)映射到已知的月销售额(输出),如图3.2所示:

图 3.2:线性回归训练数据集
监督学习算法将使用这个标记的训练数据集来计算一个数学函数,该函数是给定输入特征的最佳输出预测器。然后可以将该函数应用于测试数据集以量化其准确性,之后应用于它以前从未见过的数据集以进行预测!
回归问题是我们希望预测一个数值结果的地方。回归算法的例子包括线性回归和回归树,实际应用案例包括价格、重量和温度预测。分类问题是我们希望预测一个分类结果的地方。分类算法的例子包括逻辑回归、多项式逻辑回归和分类树,实际应用案例包括图像分类和电子邮件垃圾邮件分类。我们将在第四章《使用 Apache Spark 的监督学习》中更详细地研究这些算法,在那里我们还将开发可以应用于实际应用案例并能够量化其准确性的监督学习模型。
无监督学习
在无监督学习中,目标是揭示隐藏的关系、趋势和模式,给定只有输入数据x[i]而没有输出y[i]。在这种情况下,我们有以下内容:

在实践中,这意味着在没有已知和正确答案的情况下,重点在于揭示数据集中的有趣模式和趋势。随后,由于问题定义不够明确,我们也没有被告知数据中包含哪些类型的模式,无监督学习通常被称为知识发现。聚类是一个无监督学习算法的例子,其目标是将数据点分割成组,其中特定组中的所有数据点都共享相似的特征或属性,如图3.3所示。聚类的实际应用案例包括文档分类和为营销目的聚类客户:

图 3.3:聚类无监督学习模型
我们将在第五章《使用 Apache Spark 的无监督学习》中更详细地研究无监督学习算法,包括实际应用的动手开发。
强化学习
在强化学习中,使用奖励(或惩罚)系统来影响行为,基于代理与其更广泛环境之间的交互。代理将从环境中接收状态信息,并根据该状态执行一个动作。由于该动作,环境将过渡到一个新的状态,然后将其提供给代理,通常伴随着奖励(或惩罚)。因此,代理的目标是最大化它收到的累积奖励。例如,考虑一个孩子从不良行为中学习良好行为,并因其良好行为而从父母那里得到奖励的情况。在机器的例子中,考虑基于计算机的棋类游戏玩家的例子。通过将深度学习与强化学习相结合,计算机可以学习如何以不断提高的性能水平玩棋类游戏,以至于随着时间的推移,它们几乎不可战胜!
强化学习超出了本书的范围。然而,要了解更多关于应用于游戏中的深度强化学习,请访问deepmind.com/blog/deep-reinforcement-learning/。
深度学习
在深度学习,作为机器学习更广泛领域的一个子领域,目标仍然是学习一个函数,但通过采用模仿人类大脑中发现的神经网络架构的架构,以便使用概念或表示的层次结构从经验中学习。这使得我们能够开发更复杂和强大的函数,以更好地预测结果。
许多机器学习模型采用双层架构,其中某种函数将输入映射到输出。然而,在人类大脑中,存在多层处理,换句话说,是一个神经网络。通过模仿自然神经网络,人工神经网络(ANN)能够学习复杂的非线性表示,对输入特征没有限制,非常适合广泛的令人兴奋的应用场景,包括语音、图像和模式识别、自然语言处理(NLP)、欺诈检测、预测和价格预测。
自然神经元
深度学习算法模仿了人类大脑中发现的神经网络架构。如果我们研究人类大脑中的一个单个自然神经元,我们会发现三个主要的研究领域,如图图 3.4所示:

图 3.4:一个自然神经元
树突接收来自其他神经元的化学信号和电脉冲,这些信号在细胞体中被收集和汇总。位于细胞体内的细胞核是神经元的控制中心,负责调节细胞功能,产生构建新树突所需的蛋白质,以及制造用作信号的神经递质化学物质。信号可以分为抑制性或兴奋性。如果是抑制性的,这意味着它们不会被传递到其他神经元。如果是兴奋性的,这意味着它们将通过轴突传递到其他神经元。轴突负责在神经元之间传递信号,在某些情况下,距离可以长达几米或短至几微米。因此,神经元作为一个单一的逻辑单元,最终负责传递信息,而平均人类大脑可能包含大约 1000 亿个神经元。
人工神经元
自然神经元的核心理念可以概括为信号处理系统的组成部分。在这个通用信号处理系统中,树突接收到的信号可以被认为是输入。细胞核可以被认为是中央处理单元,它收集和汇总输入,并根据净输入幅度和激活函数,通过轴突传递输出。这个基于自然神经元建模的通用信号处理系统被称为人工神经元,并在图 3.5中展示:

图 3.5:人工神经元
权重
在人工神经元中,权重可以放大或衰减信号,并用于模拟自然界中发现的与其他神经元建立的连接。通过改变权重向量,我们可以根据输入值与权重的汇总(称为加权或净输入z)来影响神经元是否会激活:

激活函数
一旦计算了加权输入加上偏差,就使用激活函数(用希腊字母phi(Φ)表示)来确定神经元的输出以及它是否被激活。为了做出这个决定,激活函数通常是一个介于两个值之间的非线性函数,从而为人工神经网络(ANNs)添加了非线性。由于大多数现实世界数据在复杂用例中往往是非线性的,我们需要人工神经网络具有学习这些非线性概念或表示的能力。这通过非线性激活函数得以实现。激活函数的例子包括 Heaviside 阶跃函数、Sigmoid 函数和对数正切函数。
Heaviside 阶跃函数
Heaviside 阶跃函数是一种基本的不连续函数,它将值与一个简单的阈值进行比较,用于输入数据线性可分的分类。如果加权总和加上偏差超过某个阈值,则神经元被激活,该阈值用下面的方程中的希腊字母theta(θ)表示。如果没有超过,则神经元不被激活。以下阶跃函数是一个介于1和-1之间的 Heaviside 阶跃函数的例子:

这个 Heaviside 阶跃函数如图3.6所示:

图 3.6:Heaviside 阶跃激活函数
Sigmoid 函数
Sigmoid 函数是一种非线性数学函数,表现出 sigmoid 曲线,如图3.7所示,通常指的是 sigmoid 或逻辑函数:

在这种情况下,sigmoid 激活函数介于 0 和 1 之间,对所有实数输入值都有平滑的定义,这使得它比基本的 Heaviside 阶跃函数是一个更好的激活函数选择。这是因为,与 Heaviside 阶跃函数不同,非线性激活函数可以区分那些非线性可分的数据,例如图像和视频数据。请注意,使用 sigmoid 函数作为激活函数时,人工神经元实际上对应于逻辑回归模型:

图 3.7:Sigmoid 激活函数
双曲正切函数
最后,双曲正切函数,如图3.8所示,是双曲正弦函数和余弦函数的比值:

在这个例子中,基于双曲正切函数的激活函数介于1和-1之间,类似于 sigmoid 函数,对所有实数输入值都有平滑的定义:

图 3.8:双曲正切函数
人工神经网络
人工神经网络(ANN)是一组相互连接的人工神经元,其中人工神经元被聚合成相互连接的神经****层,可以分为三种类型:
-
输入层接收来自外部世界的输入信号,并将这些输入信号传递到下一层。
-
隐藏层(如果有),对这些信号进行计算并将它们传递到输出层。因此,隐藏层(的)的输出作为最终输出的输入。
-
输出层计算最终输出,然后以某种方式影响外部世界。
人工神经元通过具有相关权重的边连接到相邻的神经网络层。一般来说,增加更多的隐藏神经网络层可以提高人工神经网络(ANN)学习更复杂概念或表示的能力。它们被称为隐藏层,因为它们不直接与外界交互。请注意,所有 ANN 都有一个输入层和一个输出层,以及零个或多个隐藏层。仅在一个方向上传播信号的人工神经网络,换句话说,信号由输入层接收并转发到下一层进行处理的,被称为前馈网络。信号可能被传播回已经处理过该信号的人工神经元或神经层的 ANN 被称为反馈网络。图 3.9展示了前馈 ANN 的逻辑架构,其中每个圆圈代表一个人工神经元,有时被称为节点或单元,箭头代表人工神经元之间相邻神经网络层的边或连接:

图 3.9:前馈人工神经网络
根据其架构,ANN 可以分为两类。单层或单层ANN 的特点是所有组成人工神经元都聚集在同一层,没有隐藏层。一个单层感知器是单层 ANN 的一个例子,它只包含输入节点和输出节点之间的一层链接。多层ANN 的特点是人工神经元分布在多个相互连接的层中。多层感知器是多层 ANN 的一个例子,它包含一个或多个隐藏层。
ANN 通过优化其权重以产生期望的结果来学习,并且通过改变权重,ANN 可以为相同的输入产生不同的结果。优化权重的目标是通过找到最佳权重组合来最小化损失函数——一个计算不准确预测代价的函数,从而最佳预测结果。回想一下,权重代表与其他神经元建立的连接;因此,通过改变权重,ANN 实际上是通过改变神经元之间的连接来模拟自然神经网络的。在讨论感知器时,以下子节中将提供学习最优权重系数的各种过程。
单层感知器
图 3.10展示了单层感知器的架构。在这个单层感知器中,推导出一个最优的权重系数集,当乘以输入特征时,决定是否激活神经元。初始权重被随机设置,如果加权输入产生的预测输出与期望输出匹配(例如,在监督学习分类的上下文中),则不对权重进行更改。如果预测输出与期望输出不匹配,则更新权重以减少误差。
这使得单层感知器最适合作为分类器,但仅当类别是线性可分时:

图 3.10:单层感知器
多层感知器
多层感知器与单层感知器不同,这是由于引入了一个或多个隐藏层,使它们能够学习非线性函数。图 3.11展示了包含一个隐藏层的多层感知器架构:

图 3.11:多层感知器
反向传播是一种监督学习过程,通过这个过程多层感知器和其它人工神经网络可以学习,也就是说,推导出一个最优的权重系数集。反向传播的第一步实际上是正向传播,在这个过程中,所有权重最初被随机设置,然后计算网络的输出(类似于单层感知器,但这次涉及一个或多个隐藏层)。如果预测输出与期望输出不匹配,输出节点的总误差将通过整个网络反向传播,以尝试重新调整网络中的所有权重,从而在输出层减少误差。
多层神经网络,如多层感知器,通常计算量更大,因为优化权重的过程涉及更多的权重和计算。因此,训练神经网络,这通常也涉及大量数据点以学习大量最优权重系数,需要 CPU 和内存资源,这在以前可能并不容易获得或成本效益高。然而,随着分布式系统的出现,如第一章《大数据生态系统》中描述的,以及成本效益高、高性能和具有弹性的分布式集群的可用性,这些集群支持由通用硬件托管的数据处理,人工神经网络和深度学习的研究已经爆炸式增长,它们在令人兴奋的现实世界人工智能用例中的应用也是如此,包括以下内容:
-
医疗保健和疾病防治,包括预测诊断、药物发现和基因本体
-
语音识别,包括语言翻译
-
图像识别,包括视觉搜索
-
理论物理学和天体物理学,包括卫星图像分类和引力波探测
在本节中,我们讨论了两种特定的 ANN 类型,单层感知器和多层感知器,我们将在第七章《使用 Apache Spark 的深度学习》中更详细地研究,包括实际应用的动手开发。其他类别的神经网络包括卷积****神经网络(也在第七章《使用 Apache Spark 的深度学习》中描述),循环神经网络、Kohonen 自组织神经网络和模块化****神经网络,这些超出了本书的范围。要了解更多关于 ANN 和令人兴奋的深度学习领域,请访问deeplearning.net/。
NLP
自然语言处理(NLP)是指包括机器学习、语言学、信息工程和数据管理在内的一组计算机科学学科,用于分析和理解自然语言,包括语音和文本。NLP 可以应用于广泛的现实世界用例,包括以下内容:
-
命名实体识别(NER):自动从文本中识别和解析实体,包括人名、物理地址和电子邮件地址
-
关系抽取:自动识别解析实体之间关系的类型
-
机器翻译和转录:自动将一种自然语言翻译成另一种语言,例如,从英语翻译成中文
-
搜索:自动在大量结构化、半结构化和非结构化文档和对象中进行搜索,以满足自然语言查询
-
语音识别:自动从人类语音中提取意义
-
情感分析:自动识别人类对某个主题或实体的情感
-
问答系统:自动回答自然、完整的疑问
在自然语言处理(NLP)中,常用的一种技术是开发一个数据工程管道,该管道对文本进行预处理,以便为机器学习模型生成特征。常见的预处理技术包括分词(将文本分割成更小、更简单的单元,称为标记,标记通常是单个单词或术语)、词干提取和词形还原(将标记还原到基本形式),以及移除停用词(如I、this和at)。生成的术语集被转换成特征,然后输入到机器学习模型中。将术语集转换为特征的一个非常基础的算法称为词袋模型,它简单地计算每个唯一术语的出现次数,从而将文本转换为数值特征向量。
自然语言处理(NLP)很重要,因为它提供了一种在人工智能系统/机器和人类之间实现真正无缝交互的手段,例如通过对话界面。我们将在第六章,使用 Apache Spark 的自然语言处理中更详细地研究 NLP,包括实际应用的动手开发。
认知计算
与 NLP 类似,认知计算实际上是指一系列计算机科学学科,包括机器学习、深度学习、NLP、统计学、商业智能、数据工程和信息检索,这些学科共同用于开发模拟人类思维过程的系统。认知系统的实际应用包括能够理解自然人类语言的聊天机器人和虚拟助手(如亚马逊 Alexa、谷歌助手和微软 Cortana),它们提供包括问答、个性化推荐和信息检索系统在内的上下文对话界面。
Apache Spark 中的机器学习管道
为了结束本章,我们将探讨如何使用 Apache Spark 实现我们之前讨论的算法,通过查看其机器学习库MLlib的工作原理。MLlib提供了一套工具,旨在使机器学习变得易于访问、可扩展且易于部署。
注意,截至 Spark 2.0,基于 RDD 的MLlib API 处于维护模式。本书中的示例将使用基于 DataFrame 的 API,这是MLlib的当前主要 API。有关更多信息,请访问spark.apache.org/docs/latest/ml-guide.html。
在高层次上,机器学习模型的典型实现可以被视为一系列按顺序排列的算法,如下所示:
-
特征提取、转换和选择
-
基于这些特征向量和标签训练预测模型
-
使用训练好的预测模型进行预测
-
评估模型性能和准确性
MLlib公开了两个核心抽象,这些抽象促进了高级管道,并允许在 Apache Spark 中开发机器学习模型:
-
转换器:正式来说,转换器将一个 DataFrame(见第一章,大数据生态系统)转换为另一个 DataFrame。新的 DataFrame 通常包含一个或多个附加到其上的新列。在机器学习模型的上下文中,输入 DataFrame 可能包含一个包含相关特征向量的列。然后,转换器将这个输入 DataFrame 作为输入,并为每个特征向量预测一个标签。转换器将输出一个新的 DataFrame,其中包含一个包含预测标签的新列。
-
估计器:从形式上讲,估计器抽象了一个学习算法。在实践中,估计器是一种学习算法,例如逻辑回归算法。在这种情况下,估计器在
MLlib中被称为 LogisticRegression。估计器将接受一个输入 DataFrame 并对其调用fit()方法。fit()方法的输出,也就是估计器的输出,将是一个 训练好的模型。在这个例子中,LogisticRegression 估计器将生成一个训练好的 LogisticRegressionModel 模型对象。实际上,模型对象本身就是一个 转换器,因为训练好的模型现在可以接受包含新特征向量的新 DataFrame 并对其做出预测。
返回到我们对管道的定义,这现在可以扩展。实际上,管道是一个有序的阶段序列,其中每个阶段要么是一个转换器,要么是一个估计器。
图 3.12 展示了一个用于训练模型的管道。在 图 3.12 中,NLP 转换 被应用于将原始训练文本标记化为一组单词或术语。标记化器被称为特征 转换器。然后应用一个名为 HashingTF 的算法来将术语集转换为固定长度的特征向量(HashingTF 最终使用哈希函数计算术语频率)。HashingTF 也是一个 转换器。然后通过 LogisticRegression.fit() 将 LogisticRegression 估计器应用于这些特征向量,以生成一个训练好的 LogisticRegressionModel,它本身也是一种 转换器:

图 3.12:MLlib 训练管道
图 3.13 展示了一个用于测试模型的管道。在这个图中,类似于训练管道,使用标记化器特征 转换器 从原始测试文本中提取术语,然后应用 HashingTF 转换器 将术语集转换为固定长度的特征向量。然而,由于我们已经在 图 3.12 中的训练管道中生成了一个训练好的模型,因此特征向量被作为输入传递到这个训练好的模型 转换器 中,以便进行预测并输出一个包含这些测试数据预测的新 DataFrame:

图 3.13:MLlib 测试管道
除了提供常见的机器学习算法和方法来提取、转换和选择模型特征以及其他管道抽象之外,MLlib 还公开了将训练好的模型和管道保存到底层文件系统的方法,这样在需要时可以稍后加载。MLlib 还提供了涵盖统计、线性代数和数据工程操作的实用方法。要了解更多关于 MLlib 的信息,请访问 spark.apache.org/docs/latest/ml-guide.html。
摘要
在本章中,我们定义了人工智能、机器学习和认知计算的含义。我们以高层次探讨了常见的机器学习算法,包括深度学习和人工神经网络,同时还简要介绍了 Apache Spark 的机器学习库MLlib以及如何将其用于在机器学习管道中实现这些算法。
在下一章中,我们将开始使用PySpark和MLlib开发、部署和测试应用于实际用例的监督式机器学习模型。
第四章:使用 Apache Spark 进行监督学习
在本章中,我们将使用 Python、Apache Spark 及其机器学习库MLlib开发、测试和评估应用于各种现实世界用例的监督机器学习模型。具体来说,我们将训练、测试和解释以下类型的监督机器学习模型:
-
单变量线性回归
-
多元线性回归
-
逻辑回归
-
分类和回归树
-
随机森林
线性回归
我们将要研究的第一个监督学习模型是线性回归。形式上,线性回归使用一组一个或多个独立变量来建模依赖变量之间的关系。得到的模型可以用来预测依赖变量的数值。但这在实践中意味着什么呢?好吧,让我们看看我们的第一个实际应用案例来理解这一点。
案例研究 – 预测自行车共享需求
在过去十年左右的时间里,随着人们寻求在繁忙的城市中出行的同时减少碳足迹并帮助减少道路拥堵,自行车共享计划在全球范围内变得越来越受欢迎。如果你对自行车共享系统不熟悉,它们非常简单;人们可以从城市中的特定地点租用自行车,并在完成旅程后将其归还到同一地点或另一个地点。在本例中,我们将探讨是否可以根据特定一天的天气预测自行车共享系统的日需求量!
我们将要使用的数据集是从位于archive.ics.uci.edu/ml/index.php的加州大学(UCI)机器学习仓库中派生出来的。我们将使用的特定自行车共享数据集,可以从本书附带的 GitHub 仓库以及archive.ics.uci.edu/ml/datasets/Bike+Sharing+Dataset获取,已被 Fanaee-T, Hadi, 和 Gama, Joao 在《Event labeling combining ensemble detectors and background knowledge》一文中引用,该文发表于 2013 年的《Progress in Artificial Intelligence》,Springer Berlin Heidelberg 出版社,第 1-15 页。
如果你打开bike-sharing-data/day.csv文件,无论是从本书附带的 GitHub 仓库还是从 UCI 的机器学习仓库,你将找到使用以下模式对 731 天内的自行车共享数据进行每日汇总的数据:
| 列名 | 数据类型 | 描述 |
|---|---|---|
instant |
整数 |
唯一记录标识符(主键) |
dteday |
日期 |
日期 |
season |
整数 |
季节(1 – 春季,2 – 夏季,3 – 秋季,4 – 冬季) |
yr |
整数 |
年份 |
mnth |
整数 |
月份 |
holiday |
整数 |
该日是否为假日 |
weekday |
整数 |
周几 |
workingday |
整数 |
1 – 既不是周末也不是假日,0 – 否则 |
weathersit |
Integer |
1 – 清晰,2 – 薄雾,3 – 小雪,4 – 大雨 |
temp |
Double |
标准化温度(摄氏度) |
atemp |
Double |
标准化的感觉温度(摄氏度) |
hum |
Double |
标准化湿度 |
windspeed |
Double |
标准化风速 |
casual |
Integer |
当日非注册用户数量 |
registered |
Integer |
当日注册用户数量 |
cnt |
Integer |
当日总自行车租赁者数量 |
使用这个数据集,我们能否根据特定一天的天气模式预测该天的总自行车租赁者数量(cnt)?在这种情况下,cnt 是我们希望根据我们选择的 自变量 集合预测的 因变量。
单变量线性回归
单变量(或单变量)线性回归是指我们只使用一个自变量 x 来学习一个将 x 映射到因变量 y 的 线性 函数的线性回归模型:

在前面的方程中,我们有以下内容:
-
y^i 代表第 i 次观察的 因变量(cnt)
-
x^i 代表第 i 次观察的单个 自变量
-
ε^(i) 代表第 i 次观察的 误差 项
-
β[0] 是截距系数
-
β[1] 是单个自变量的回归系数
由于单变量线性回归模型在一般形式上是一个线性函数,我们可以在散点图上轻松地绘制它,其中 x 轴代表单个自变量,y 轴代表我们试图预测的因变量。图 4.1展示了当我们绘制标准化的感觉温度(自变量)与每日总自行车租赁者(因变量)的散点图时生成的散点图:

图 4.1:标准化温度与每日总自行车租赁者的关系
通过分析 图 4.1,你会看到标准化的感觉温度(atemp)和每日总自行车租赁者(cnt)之间似乎存在一种普遍的正线性趋势。然而,你也会看到我们的蓝色趋势线,这是我们的单变量线性回归函数的视觉表示,并不完美,换句话说,并不是所有的数据点都完全位于这条线上。在现实世界中,几乎不可能有一个完美的模型;换句话说,所有预测模型都会犯一些错误。因此,目标是尽量减少模型犯的错误数量,以便我们对它们提供的预测有信心。
残差
我们模型所犯的错误(或错误)被称为误差项或残差,在我们的单变量线性回归方程中用 ε^(i) 表示。因此,我们的目标是选择独立变量的回归系数(在我们的情况下 β[1])以最小化这些残差。为了计算第 i 个残差,我们可以简单地从实际值中减去预测值,如图 4.1所示。为了量化回归线的质量,以及我们的回归模型,我们可以使用一个称为均方误差总和(SSE)的指标,它简单地是所有平方残差的和,如下所示:

较小的 SSE 表示更好的拟合。然而,SSE 作为衡量我们回归模型质量的指标有其局限性。SSE 与数据点的数量 N 成比例,这意味着如果我们加倍数据点的数量,SSE 可能会加倍,这可能会让你认为模型是两倍糟糕,但这并不是事实!因此,我们需要其他方法来衡量我们模型的质量。
均方根误差
均方根误差(RMSE)是 SSE 的平方根除以数据点的总数 N,如下所示:

RMSE(均方根误差)通常被用作量化线性回归模型质量的手段,因为它的单位与因变量相同,并且通过 N 进行归一化。
R-squared
另一个提供线性回归模型错误度量的指标称为 R^(2)(R-squared)指标。R² 指标表示因变量中由独立变量(或多个变量)解释的方差的比例。计算 R² 的方程如下:

在这个方程中,SST 指的是总平方和,它只是从整体均值(如图 4.1中的红色水平线所示,常被用作基准模型)的 SSE。R² 值为 0 表示线性回归模型没有比基准模型提供任何改进(换句话说,SSE = SST)。R² 值为 1 表示完美的预测线性回归模型(换句话说,SSE = 0)。因此,目标是使 R² 值尽可能接近 1。
Apache Spark 中的单变量线性回归
返回到我们的案例研究,让我们使用 Apache Spark 的机器学习库 MLlib 来开发一个单变量线性回归模型,以预测使用我们的共享单车数据集的每日总租车量:
以下子部分描述了对应于本用例的 Jupyter Notebook 中相关的每个单元格,该笔记本的标题为 chp04-01-univariate-linear-regression.ipynb,并且可以在随本书附带的 GitHub 仓库中找到。
- 首先,我们导入所需的 Python 依赖项,包括
pandas(Python 数据分析库)、matplotlib(Python 绘图库)和pyspark(Apache Spark Python API)。通过使用%matplotlib魔法函数,我们生成的任何图表将自动在 Jupyter Notebook 单元格输出中渲染:
%matplotlib inline
import matplotlib.pyplot as plt
import pandas as pd
import findspark
findspark.init()
from pyspark import SparkContext, SparkConf
from pyspark.sql import SQLContext
from pyspark.ml.feature import VectorAssembler
from pyspark.ml.regression import LinearRegression
from pyspark.ml.evaluation import RegressionEvaluator
- 在实例化 Spark 上下文之前,通常将任何相关数据集的样本加载到
pandas中是一个好主意,这样我们可以在开发预测模型之前识别出任何趋势或模式。在这里,我们使用pandas库将整个 CSV 加载到名为bike_sharing_raw_df的pandasDataFrame 中(因为数据集本身非常小):
bike_sharing_raw_df = pd.read_csv('<Path to CSV file>',
delimiter = '<delimiter character>')
bike_sharing_raw_df.head()
- 在 3.1 到 3.4 单元格中,我们使用
matplotlib库将各种独立变量(temp、atemp、hum和windspeed)与因变量(cnt)绘制在一起:
bike_sharing_raw_df.plot.scatter(x = '<Independent Variable>',
y = '<Dependent Variable>')
如图 4.2所示,标准化温度(temp和atemp)与每日总自行车租赁者(cnt)之间存在一般的正线性关系。然而,当使用湿度和风速作为独立变量时,没有这样的明显趋势。因此,我们将继续开发一个使用标准化感觉温度(atemp)作为单一独立变量的单变量线性回归模型,其中总每日自行车租赁者(cnt)作为因变量:

图 4.2:共享单车散点图
- 为了开发 Spark 应用程序,我们首先需要实例化一个 Spark 上下文(如第一章,《大数据生态系统》)以连接到我们的本地 Apache Spark 集群。我们还实例化一个 Spark
SQLContext以对数据进行结构化处理:
conf = SparkConf().setMaster("spark://192.168.56.10:7077")
.setAppName("Univariate Linear Regression - Bike Sharing")
sc = SparkContext(conf=conf)
sqlContext = SQLContext(sc)
- 现在,我们可以将我们的 CSV 数据集加载到名为
bike_sharing_df的 Spark DataFrame 中(参见第一章,《大数据生态系统》)。我们使用先前定义的SQLContext,并告诉 Spark 使用第一行作为标题行,并推断数据类型:
bike_sharing_df = sqlContext.read
.format('com.databricks.spark.csv')
.options(header = 'true', inferschema = 'true')
.load('Path to CSV file')
bike_sharing_df.head(10)
bike_sharing_df.printSchema()
- 在开发预测模型之前,生成数据集的标准统计指标也是一个好主意,以便获得额外的见解。在这里,我们生成 DataFrame 的行数,以及计算每列的平均值、标准差、最小值和最大值。我们使用以下 Spark DataFrame 的
describe()方法实现这一点:
bike_sharing_df.describe().toPandas().transpose()
- 现在我们演示如何使用 Spark DataFrame 作为输入来绘制数据集。在这种情况下,我们在绘图之前简单地将 Spark DataFrame 转换为
pandasDataFrame(注意,对于非常大的数据集,建议使用数据集的代表样本进行绘图):
bike_sharing_df.toPandas().plot.scatter(x='atemp', y='cnt')
- 现在我们已经完成了我们的探索性分析,我们可以开始开发我们的单变量线性回归模型!首先,我们需要将我们的独立变量(
atemp)转换为一个数值特征向量(见第三章,人工智能与机器学习)。我们可以通过使用 MLlib 的VectorAssembler来实现这一点,它将一个或多个特征列,将它们转换为特征向量,并将这些特征向量存储在一个输出列中,在这个例子中,它被称为features:
univariate_feature_column = 'atemp'
univariate_label_column = 'cnt'
vector_assembler = VectorAssembler(
inputCols = [univariate_feature_column],
outputCol = 'features')
然后,我们将VectorAssembler 转换器(见第三章,人工智能与机器学习)应用于原始数据集,并识别包含我们的标签的列(在这种情况下,我们的因变量cnt)。输出是一个新的 Spark DataFrame,称为bike_sharing_features_df,它包含我们的独立数值特征向量(atemp)映射到一个已知的标签(cnt):
bike_sharing_features_df = vector_assembler
.transform(bike_sharing_df)
.select(['features', univariate_label_column])
bike_sharing_features_df.head(10)
- 根据监督学习模型的一般情况,我们需要一个训练数据集来训练我们的模型以学习映射函数,以及一个测试数据集来评估我们模型的性能。我们可以使用
randomSplit()方法和一个种子随机分割我们的原始标记特征向量 DataFrame,种子用于初始化随机生成器,可以是任何你喜欢的数字。请注意,如果你使用不同的种子,你将在训练和测试数据集之间得到不同的随机分割,这意味着你可能会得到最终线性回归模型的不同系数:
train_df, test_df = bike_sharing_features_df
.randomSplit([0.75, 0.25], seed=12345)
train_df.count(), test_df.count()
在我们的情况下,75%的原始行将形成我们的训练 DataFrame,称为train_df,剩下的 25%将形成我们的测试 DataFrame,称为test_df,同时使用seed为12345。
- 我们现在已准备好训练我们的单变量线性回归模型!我们通过使用
MLlib的LinearRegression估计器(见第三章,人工智能与机器学习)并将包含我们独立数值特征向量的列名(在我们的例子中,称为features)和包含我们标签的列名(在我们的例子中,称为cnt)传递给它来实现这一点。然后我们应用fit()方法来训练我们的模型并输出一个线性回归转换器,在我们的例子中,它被称为linear_regression_model:
linear_regression = LinearRegression(featuresCol = 'features',
labelCol = univariate_label_column)
linear_regression_model = linear_regression.fit(train_df)
-
在我们在测试 DataFrame 上评估我们的训练好的单变量线性回归模型之前,让我们为它生成一些摘要统计信息。转换器模型公开了一系列统计信息,包括模型系数(在我们的情况下,即β[1]),截距系数β[0],错误度量 RMSE 和 R²,以及每个数据点的残差集。在我们的情况下,我们有以下内容:
-
β[0] = 829.62
-
β[1] = 7733.75
-
RMSE = 1490.12
-
R² = 0.42
-
print("Model Coefficients: " +
str(linear_regression_model.coefficients))
print("Intercept: " + str(linear_regression_model.intercept))
training_summary = linear_regression_model.summary
print("RMSE: %f" % training_summary.rootMeanSquaredError)
print("R-SQUARED: %f" % training_summary.r2)
print("TRAINING DATASET DESCRIPTIVE SUMMARY: ")
train_df.describe().show()
print("TRAINING DATASET RESIDUALS: ")
training_summary.residuals.show()
因此,我们的训练一元线性回归模型已经学习到了以下函数,以便能够使用单个自变量x(标准化感觉温度)来预测我们的因变量y(总日自行车租赁量):
y = 829.62 + 7733.75x
- 现在,我们将我们的训练模型应用于测试 DataFrame,以评估其在测试数据上的性能。在这里,我们使用
transform()方法将我们的训练线性回归模型转换器应用于测试 DataFrame,以便进行预测。例如,我们的模型预测在标准化感觉温度为 0.11793 的情况下,总日自行车租赁量为 1742。实际的总日自行车租赁量为 1416(误差为 326):
test_linear_regression_predictions_df =
linear_regression_model.transform(test_df)
test_linear_regression_predictions_df
.select("prediction", univariate_label_column, "features")
.show(10)
- 我们现在计算相同的 RMSE 和 R²误差指标,但基于模型在测试DataFrame 上的性能。在我们的案例中,这些是 1534.51(RMSE)和 0.34(R²),分别使用
MLlib的RegressionEvaluator计算得出。因此,在我们的案例中,我们的训练模型在测试数据集上的表现实际上更差:
linear_regression_evaluator_rmse = RegressionEvaluator(
predictionCol = "prediction",
labelCol = univariate_label_column, metricName = "rmse")
linear_regression_evaluator_r2 = RegressionEvaluator(
predictionCol = "prediction",
labelCol = univariate_label_column, metricName = "r2")
print("RMSE on Test Data = %g" % linear_regression_evaluator_rmse
.evaluate(test_linear_regression_predictions_df))
print("R-SQUARED on Test Data = %g" %
linear_regression_evaluator_r2
.evaluate(test_linear_regression_predictions_df))
- 注意,我们可以使用线性回归模型的
evaluate()方法生成相同的指标,如下面的代码块所示:
test_summary = linear_regression_model.evaluate(test_df)
print("RMSE on Test Data = %g" % test_summary.rootMeanSquaredError)
print("R-SQUARED on Test Data = %g" % test_summary.r2)
- 最后,我们通过停止 Spark 上下文来终止我们的 Spark 应用程序:
sc.stop()
多元线性回归
我们的多元线性回归模型在训练集和测试集上的表现实际上相对较差,分别在训练集上具有 0.42 的 R²值,在测试集上具有 0.34 的 R²值。我们是否有办法利用原始数据集中可用的其他自变量来提高我们模型的预测质量?
多元(或多个)线性回归通过允许我们利用一个以上的自变量来扩展一元线性回归,在这种情况下,K个自变量,如下所示:

如前所述,我们有因变量yi*(对于第*i*个观察值),截距系数*β[0]*,以及残差ε(i)。但现在我们还有k个自变量,每个自变量都有自己的回归系数,β[k]。目标,如前所述,是推导出系数,以最小化模型产生的误差量。但问题现在是如何选择用于训练多元线性回归模型的独立变量子集。增加更多的自变量通常会增加模型的一般复杂性,从而增加底层处理平台的数据存储和处理需求。此外,过于复杂的模型往往会引起过拟合,即模型在用于训练模型的训练数据集上的性能(换句话说,更高的R²*指标)优于它之前未见过的数据。
相关性
相关性是一个衡量两个变量之间线性关系的指标,并帮助我们决定在模型中包含哪些自变量:
-
+1 表示完美的正线性关系
-
0 表示没有线性关系
-
-1 表示完美的负线性关系
当两个变量的相关性的绝对值接近 1 时,这两个变量被认为是“高度相关”的。
Apache Spark 中的多元线性回归
返回到我们的案例研究,现在让我们使用我们的共享单车数据集和独立变量子集来开发一个多元线性回归模型,以预测总日租车数量:
以下子节描述了对应于本用例的 Jupyter Notebook 中每个相关的单元格,标题为chp04-02-multivariate-linear-regression.ipynb,并可在本书附带的 GitHub 仓库中找到。请注意,为了简洁起见,我们将跳过那些执行与之前相同功能的单元格。
- 首先,让我们演示如何使用 Spark 计算我们的因变量
cnt与 DataFrame 中的每个独立变量之间的相关值。我们通过遍历我们的原始 Spark DataFrame 中的每一列,并使用stat.corr()方法来实现这一点,如下所示:
independent_variables = ['season', 'yr', 'mnth', 'holiday',
'weekday', 'workingday', 'weathersit', 'temp', 'atemp',
'hum', 'windspeed']
dependent_variable = ['cnt']
bike_sharing_df = bike_sharing_df.select( independent_variables +
dependent_variable )
for i in bike_sharing_df.columns:
print( "Correlation to CNT for ",
i, bike_sharing_df.stat.corr('cnt', i))
结果的相关矩阵显示,独立变量——season、yr、mnth、temp和atemp与我们的因变量cnt表现出显著的积极相关性。因此,我们将继续使用这个独立变量子集来训练多元线性回归模型。
- 如前所述,我们可以使用
VectorAssembler来生成我们独立变量集合的数值特征向量表示,以及cnt标签。语法与之前相同,但这次我们向VectorAssembler传递多个列,代表包含我们的独立变量的列:
multivariate_feature_columns = ['season', 'yr', 'mnth',
'temp', 'atemp']
multivariate_label_column = 'cnt'
vector_assembler = VectorAssembler(inputCols =
multivariate_feature_columns, outputCol = 'features')
bike_sharing_features_df = vector_assembler
.transform(bike_sharing_df)
.select(['features', multivariate_label_column])
- 我们现在可以使用 DataFrame API 通过
randomSplit方法生成各自的训练和测试数据集:
train_df, test_df = bike_sharing_features_df
.randomSplit([0.75, 0.25], seed=12345)
train_df.count(), test_df.count()
- 我们现在可以使用与我们的单变量线性回归模型中使用的相同的
LinearRegression估计器来训练我们的多元线性回归模型:
linear_regression = LinearRegression(featuresCol = 'features',
labelCol = multivariate_label_column)
linear_regression_model = linear_regression.fit(train_df)
-
在将我们的原始 DataFrame 分别拆分为训练和测试 DataFrame 后,并将相同的LinearRegression估计器应用于训练 DataFrame 后,我们现在有一个训练好的多元线性回归模型,以下是其总结训练统计信息(如本 Jupyter Notebook 的第 8 个单元格所示):
-
β[0] = -389.94, β[1] = 526.05, β[2] = 2058.85, β[3] = -51.90, β[4] = 2408.66, β[5] = 3502.94
-
RMSE = 1008.50
-
R² = 0.73
-
因此,我们的训练好的多元线性回归模型已经学习到了以下函数,以便能够使用一组独立变量x[k](季节、年份、月份、标准化温度和标准化感觉温度)来预测我们的因变量y(总日租车数量):
y = -389.94 + 526.05x[1] + 2058.85x[2] - 51.90x[3] + 2408.66x[4] + 3502.94x[5]
此外,我们的训练多元线性回归模型在测试数据集上的表现甚至更好,测试 RMSE 为 964.60,测试 R² 为 0.74。
为了完成我们对多元线性回归模型的讨论,请注意,随着更多独立变量的添加,我们的训练 R² 指标将始终增加或保持不变。然而,更好的训练 R² 指标并不总是意味着更好的测试 R² 指标——事实上,测试 R² 指标甚至可以是负数,这意味着它在测试数据集上的表现比基线模型(训练 R² 指标永远不会是这样)更差。因此,目标是能够开发一个在训练和测试数据集上都表现良好的模型。
逻辑回归
我们已经看到线性回归模型如何允许我们预测数值结果。然而,逻辑回归模型允许我们通过预测结果为真的概率来预测分类结果。
与线性回归一样,在逻辑回归模型中,我们也有一个因变量 y 和一组自变量 x[1]、x[2]、…、x[k]。然而,在逻辑回归中,我们想要学习一个函数,该函数提供在给定这组自变量的情况下 y = 1(换句话说,结果变量为真)的概率,如下所示:

这个函数被称为逻辑响应函数,它提供一个介于 0 和 1 之间的数字,表示结果相关变量为真的概率,如图 4.3 所示:

图 4.3:逻辑响应函数
正系数值 β[k] 增加了 y = 1 的概率,而负系数值减少了 y = 1 的概率。因此,在开发逻辑回归模型时,我们的目标是选择那些在 y = 1 时预测高概率,但在 y = 0 时预测低概率的系数。
阈值值
我们现在知道,逻辑回归模型为我们提供了结果变量为真的概率,也就是说,y = 1。然而,在实际应用中,我们需要做出决策,而不仅仅是提供概率。通常,我们会做出二元预测,例如是/否、好/坏、行/停。一个阈值值(t)允许我们根据概率做出以下决策:
-
如果 P(y=1) >= t,则我们预测 y = 1
-
如果 P(y=1) < t,则我们预测 y = 0
现在的挑战是如何选择一个合适的 t 值。实际上,在这个上下文中,“合适”是什么意思呢?
在现实世界的用例中,某些类型的错误比其他类型的错误更好。想象一下,如果你是一位医生,正在使用逻辑回归对大量患者进行特定疾病的检测。在这种情况下,结果 y=1 将是携带该疾病的患者(因此 y=0 将是不携带该疾病的患者),因此我们的模型将为特定个人提供 P(y=1)。在这个例子中,最好尽可能多地检测出可能携带疾病的患者,即使这意味着将一些患者错误地分类为携带疾病,但后来发现他们并不携带。在这种情况下,我们选择较小的阈值值。如果我们选择较大的阈值值,那么我们将检测出几乎肯定患有疾病的患者,但我们会错误地将大量患者分类为不携带疾病,而实际上他们确实携带,这将是一个更糟糕的情况!
因此,总的来说,当使用逻辑回归模型时,我们可以犯两种类型的错误:
-
我们预测 y=1(疾病),但实际结果是 y=0(健康)
-
我们预测 y=0(健康),但实际结果是 y=1(疾病)
混淆矩阵
一个混淆(或分类)矩阵可以帮助我们通过比较预测结果与实际结果来决定使用哪个阈值值,如下所示:
| 预测 y=0(健康) | 预测 y=1(疾病) | |
|---|---|---|
| 实际 y=0(健康) | 真阴性(TN) | 假阳性(FP) |
| 实际 y=1(疾病) | 假阴性(FN) | 真阳性(TP) |
通过生成混淆矩阵,它允许我们使用以下一系列指标,根据给定的阈值值量化我们模型的准确性:
-
N = 观察数
-
总准确率 = (TN + TP) / N
-
总错误率 = (FP + FN) / N
-
敏感性(真阳性率)= TP / (TP + FN)
-
特异性(真阴性率)= TN / (TN + FP)
-
假阳性错误率 = FP / (TN + FP)
-
假阴性错误率 = FN / (TP + FN)
阈值值较高的逻辑回归模型将具有较低的敏感性和较高的特异性。阈值值较低的模型将具有较高的敏感性和较低的特异性。因此,阈值值的选择取决于对特定用例中哪种错误“更好”。在例如政治倾向为保守/非保守等没有真正偏好的用例中,你应该选择 0.5 的阈值值,这将预测最可能的结果。
接收器操作特征曲线
为了以更直观的方式进一步帮助我们选择阈值值,我们可以生成一个接收器操作特征(ROC)曲线。ROC 曲线绘制了每个阈值值(0 到 1 之间)的假阳性错误率(FPR)与真阳性率(TPR,或敏感性)的关系,如图 4.4 所示:

图 4.4:ROC 曲线
如图 4.4所示,使用阈值为 0 意味着你会捕捉到所有 y=1(疾病)的病例,但你也会错误地将所有 y=0(健康)的病例标记为 y=1(疾病),这会让许多健康人感到恐慌!然而,使用阈值为 1 意味着你将不会捕捉到任何 y=1(疾病)的病例,导致许多人未得到治疗,但你将正确地将所有 y=0(健康)的病例标记。因此,绘制 ROC 曲线的好处在于,它可以帮助你看到每个阈值值的权衡,并最终帮助你决定在特定用例中应使用哪个阈值值。
ROC 曲线下的面积
作为量化逻辑回归模型预测质量的一种手段,我们可以计算ROC 曲线下的面积(AUC),如图4.5所示。AUC 衡量模型预测正确的时间比例,AUC 值为 1(最大值),意味着完美模型,换句话说,我们的模型 100%的时间都能正确预测,而 AUC 值为 0.5(最小值),意味着我们的模型 50%的时间能正确预测,类似于仅仅猜测:

图4.5:ROC 曲线下的面积
案例研究 – 预测乳腺癌
现在我们将逻辑回归应用于一个非常重要的实际应用案例;预测可能患有乳腺癌的患者。大约每 8 位女性中就有 1 位在其一生中被诊断出患有乳腺癌(该疾病也影响男性),导致全球每年有数十万女性过早死亡。事实上,预计到 2018 年底,全球将仅报告超过 200 万例新发乳腺癌。已知有多种因素会增加乳腺癌的风险,包括年龄、体重、家族史和以前的诊断。
使用定量预测因子的数据集,以及表示乳腺癌存在与否的二进制因变量,我们将训练一个逻辑回归模型来预测给定患者是否健康(y=1)或具有乳腺癌的生物标志物(y=0)。
我们将使用的数据集再次来源于加州大学(UCI)的机器学习仓库。具体来说,可以从本书附带的 GitHub 仓库以及archive.ics.uci.edu/ml/datasets/Breast+Cancer+Coimbra获取的乳腺癌数据集,已被[Patricio, 2018] Patricio, M., Pereira, J., Crisóstomo, J., Matafome, P., Gomes, M., Seiça, R., and Caramelo, F. (2018). Using Resistin, glucose, age, and BMI to predict the presence of breast cancer. BMC Cancer, 18(1)引用。
如果你打开breast-cancer-data/dataR2.csv在任何文本编辑器中,无论是从随本书附带的 GitHub 存储库还是从 UCI 的机器学习存储库,你将找到使用以下模式的乳腺癌数据:
| 列名 | 数据类型 | 描述 |
|---|---|---|
Age |
Integer |
患者年龄 |
BMI |
Double |
体质指数(kg/m²) |
Glucose |
Double |
血糖水平(mg/dL) |
Insulin |
Double |
胰岛素水平(µU/mL) |
HOMA |
Double |
代谢稳态模型评估(HOMA)- 用于评估β细胞功能和胰岛素敏感性 |
Leptin |
Double |
用于调节能量消耗的激素(ng/mL) |
Adiponectin |
Double |
用于调节血糖水平的蛋白质激素(µg/mL) |
Resistin |
Double |
导致胰岛素抵抗的激素(ng/mL) |
MCP.1 |
Double |
有助于从伤害和感染中恢复的蛋白质(pg/dL) |
分类 |
Integer |
1 = 作为对照组的健康患者,2 = 乳腺癌患者 |
使用这个数据集,我们能否开发一个逻辑回归模型,该模型计算给定患者健康的概率(换句话说,y=1),然后应用阈值值进行预测决策?
以下子部分描述了对应于本用例的 Jupyter Notebook 中相关的每个单元格,该笔记本的标题为chp04-03-logistic-regression.ipynb,并且可以在随本书附带的 GitHub 存储库中找到。请注意,为了简洁起见,我们将跳过那些执行与之前相同功能的单元格。
- 在加载我们的乳腺癌 CSV 文件后,我们首先确定将作为标签的列,即
分类。由于该列中的值要么是 1(健康)要么是 2(乳腺癌患者),我们将对此列应用StringIndexer以识别和索引所有可能的类别。结果是,标签 1 对应健康患者,标签 0 对应乳腺癌患者:
indexer = StringIndexer(inputCol = "Classification",
outputCol = "label").fit(breast_cancer_df)
breast_cancer_df = indexer.transform(breast_cancer_df)
- 在我们的案例中,我们将使用所有原始定量列【
年龄、BMI、血糖、胰岛素、HOMA、Leptin、Adiponectin、Resistin和MCP.1】作为独立变量,以生成模型的数值特征向量。同样,我们可以使用MLlib的VectorAssembler来实现这一点:
feature_columns = ['Age', 'BMI', 'Glucose', 'Insulin', 'HOMA',
'Leptin', 'Adiponectin', 'Resistin', 'MCP_1']
label_column = 'label'
vector_assembler = VectorAssembler(inputCols = feature_columns,
outputCol = 'features')
breast_cancer_features_df = vector_assembler
.transform(breast_cancer_df)
.select(['features', label_column])
- 在生成训练和测试 DataFrame 后,我们应用
MLlib的LogisticRegression估计器来训练一个LogisticRegression模型转换器:
logistic_regression = LogisticRegression(featuresCol = 'features',
labelCol = label_column)
logistic_regression_model = logistic_regression.fit(train_df)
- 然后,我们使用训练好的逻辑回归模型对测试 DataFrame 进行预测,使用我们的逻辑回归模型转换器的
transform()方法。这导致一个新的 DataFrame,其中附加了rawPrediction、prediction和probability列。y=1 的概率,换句话说,P(y=1),包含在probability列中,而使用默认阈值值 t=0.5 的整体预测决策包含在prediction列中:
test_logistic_regression_predictions_df = logistic_regression_model
.transform(test_df)
test_logistic_regression_predictions_df.select("probability",
"rawPrediction", "prediction", label_column, "features").show()
- 为了量化我们训练好的逻辑回归模型的质量,我们可以绘制 ROC 曲线并计算 AUC 指标。ROC 曲线使用
matplotlib库生成,给定假阳性率(FPR)和真阳性率(TPR),这是通过在测试 DataFrame 上评估我们的训练好的逻辑回归模型来暴露的。然后,我们可以使用MLlib的BinaryClassificationEvaluator来计算 AUC 指标如下:
test_summary = logistic_regression_model.evaluate(test_df)
roc = test_summary.roc.toPandas()
plt.plot(roc['FPR'],roc['TPR'])
plt.ylabel('False Positive Rate')
plt.xlabel('True Positive Rate')
plt.title('ROC Curve')
plt.show()
evaluator_roc_area = BinaryClassificationEvaluator(
rawPredictionCol = "rawPrediction", labelCol = label_column,
metricName = "areaUnderROC")
print("Area Under ROC Curve on Test Data = %g" %
evaluator_roc_area.evaluate(
test_logistic_regression_predictions_df))
Area Under ROC Curve on Test Data = 0.859375
使用matplotlib库生成的结果 ROC 曲线如图 4.6 所示:

图 4.6:使用matplotlib渲染的 ROC 曲线
- 基于测试数据集预测生成混淆矩阵的一种方法是根据预测结果等于和不等于实际结果的情况过滤测试预测的 DataFrame,然后计算过滤后的记录数:
N = test_logistic_regression_predictions_df.count()
true_positives = test_logistic_regression_predictions_df
.filter( col("prediction") == 1.0 )
.filter( col("label") == 1.0 ).count()
true_negatives = test_logistic_regression_predictions_df
.filter( col("prediction") == 0.0 )
.filter( col("label") == 0.0 ).count()
false_positives = test_logistic_regression_predictions_df
.filter( col("prediction") == 1.0 )
.filter( col("label") == 0.0 ).count()
false_negatives = test_logistic_regression_predictions_df
.filter( col("prediction") == 0.0 )
.filter( col("label") == 1.0 ).count()
- 或者,我们可以使用 MLlib 的 RDD API(自 Spark 2.0 起处于维护模式)通过将测试预测的 DataFrame 转换为 RDD 自动生成混淆矩阵(见第一章,大数据生态系统),然后传递给
MulticlassMetrics评估抽象:
predictions_and_label = test_logistic_regression_predictions_df
.select("prediction", "label").rdd
metrics = MulticlassMetrics(predictions_and_label)
print(metrics.confusionMatrix())
我们逻辑回归模型的混淆矩阵,使用默认阈值值为 0.5,如下所示:
| 预测 y=0(乳腺癌) | 预测 y=1(健康) | |
|---|---|---|
| 实际 y=0****(乳腺癌) | 10 | 6 |
| 实际 y=1****(健康) | 4 | 8 |
我们可以这样解释这个混淆矩阵。在总共 28 个观察值中,我们的模型表现出以下特性:
-
正确标记了 10 例实际为乳腺癌的乳腺癌病例
-
正确标记了 8 例实际为健康患者的健康患者
-
错误地将 6 名患者标记为健康,而他们实际上患有乳腺癌
-
错误地将 4 名患者标记为患有乳腺癌,而他们实际上是健康的
-
总体准确率 = 64%
-
总体错误率 = 36%
-
灵敏度 = 67%
-
特异性 = 63%
为了改进我们的逻辑回归模型,我们当然必须包括更多的观察结果。此外,我们模型的 AUC 指标为 0.86,相当高。然而,请注意,AUC 是一个考虑所有可能阈值值的准确度度量,而先前的混淆矩阵只考虑了一个阈值值(在这种情况下为 0.5)。作为一个扩展练习,为一系列阈值值生成混淆矩阵,以查看这如何影响我们的最终分类!
分类和回归树
我们已经看到线性回归模型如何让我们预测数值结果,以及逻辑回归模型如何让我们预测分类结果。然而,这两个模型都假设变量之间存在线性关系。分类和回归树(CART)通过生成决策树来克服这个问题,与之前看到的监督学习模型相比,这些决策树也更容易解释。然后可以遍历这些决策树来做出最终决策,结果可以是数值(回归树)或分类(分类树)。一个简单的分类树,由抵押贷款提供者使用,如图4.7所示:

图 4.7:抵押贷款提供者使用的简单分类树
在遍历决策树时,从顶部开始。之后,向左遍历表示是或正面响应,向右遍历表示否或负面响应。一旦到达分支的末端,叶子节点描述了最终结果。
案例研究 - 预测政治派别
对于我们的下一个用例,我们将使用美国众议院的国会投票记录来构建一个分类树,以预测某个国会议员或女议员是共和党人还是民主党人。
我们将使用的特定国会投票数据集可以从本书附带的 GitHub 仓库和 UCI 机器学习仓库archive.ics.uci.edu/ml/datasets/congressional+voting+records获取。它已被 Dua, D.和 Karra Taniskidou, E.(2017)引用。UCI 机器学习仓库[http://archive.ics.uci.edu/ml]。加州大学欧文分校,信息与计算机科学学院。
如果你使用任何选择的文本编辑器打开 congressional-voting-data/house-votes-84.data,无论是来自本书附带的 GitHub 仓库还是来自 UCI 的机器学习仓库,你将找到 435 项国会投票记录,其中 267 项属于民主党,168 项属于共和党。第一列包含标签字符串,换句话说,是民主党或共和党,后续列表明当时该议员或女议员在特定关键问题上的投票情况(y = 支持,n = 反对,? = 既不支持也不反对),例如反卫星武器测试禁令和对合成燃料公司的资金削减。现在,让我们开发一个分类树,以便根据议员的投票记录预测其政治派别:
以下子部分描述了对应于本用例的 Jupyter Notebook 中每个相关的单元格,该笔记本命名为 chp04-04-classification-regression-trees.ipynb,并且可以在本书附带的 GitHub 仓库中找到。请注意,为了简洁起见,我们将跳过那些执行与之前相同功能的单元格。
- 由于我们的原始数据文件没有标题行,在将其加载到 Spark DataFrame 之前,我们需要明确定义其模式,如下所示:
schema = StructType([
StructField("party", StringType()),
StructField("handicapped_infants", StringType()),
StructField("water_project_cost_sharing", StringType()),
...
])
- 由于我们所有的列,包括标签和所有自变量,都是基于字符串的数据类型,我们需要对它们应用一个 StringIndexer(就像我们在开发我们的逻辑回归模型时做的那样),以便在生成数值特征向量之前识别和索引每个列的所有可能类别。然而,由于我们需要对多个列进行索引,构建一个 pipeline(管道)会更有效率。管道是一系列应用于 Spark DataFrame 的数据和/或机器学习转换阶段。在我们的案例中,管道中的每个阶段将是对不同列的索引,如下所示:
categorical_columns = ['handicapped_infants',
'water_project_cost_sharing', ...]
pipeline_stages = []
for categorial_column in categorical_columns:
string_indexer = StringIndexer(inputCol = categorial_column,
outputCol = categorial_column + 'Index')
encoder = OneHotEncoderEstimator(
inputCols = [string_indexer.getOutputCol()],
outputCols = [categorial_column + "classVec"])
pipeline_stages += [string_indexer, encoder]
label_string_idx = StringIndexer(inputCol = 'party',
outputCol = 'label')
pipeline_stages += [label_string_idx]
vector_assembler_inputs = [c + "classVec" for c
in categorical_columns]
vector_assembler = VectorAssembler(
inputCols = vector_assembler_inputs,
outputCol = "features")
pipeline_stages += [vector_assembler]
- 接下来,我们通过传递之前单元格中生成的阶段列表来实例化我们的管道。然后,我们使用
fit()方法在原始 Spark DataFrame 上执行我们的管道,然后再像之前一样使用VectorAssembler生成我们的数值特征向量:
pipeline = Pipeline(stages = pipeline_stages)
pipeline_model = pipeline.fit(congressional_voting_df)
label_column = 'label'
congressional_voting_features_df = pipeline_model
.transform(congressional_voting_df)
.select(['features', label_column, 'party'])
pd.DataFrame(congressional_voting_features_df.take(5), columns=congressional_voting_features_df.columns).transpose()
- 我们现在准备好训练我们的分类树了!为了实现这一点,我们可以使用 MLlib 的
DecisionTreeClassifier估计器在训练数据集上训练一个决策树,如下所示:
decision_tree = DecisionTreeClassifier(featuresCol = 'features',
labelCol = label_column)
decision_tree_model = decision_tree.fit(train_df)
- 在训练我们的分类树之后,我们将评估它在测试 DataFrame 上的性能。与逻辑回归一样,我们可以使用 AUC 指标作为模型正确预测比例的衡量标准。在我们的案例中,我们的模型具有 0.91 的 AUC 指标,这是一个非常高的数值:
evaluator_roc_area = BinaryClassificationEvaluator(
rawPredictionCol = "rawPrediction", labelCol = label_column,
metricName = "areaUnderROC")
print("Area Under ROC Curve on Test Data = %g" % evaluator_roc_area.evaluate(test_decision_tree_predictions_df))
- 理想情况下,我们希望可视化我们的分类树。不幸的是,目前还没有直接的方法可以在不使用第三方工具(如
github.com/julioasotodv/spark-tree-plotting)的情况下渲染 Spark 决策树。然而,我们可以通过在训练好的分类树模型上调用toDebugString方法来渲染基于文本的决策树,如下所示:
print(str(decision_tree_model.toDebugString))
当 AUC 值为 0.91 时,我们可以说我们的分类树模型在测试数据上表现非常好,并且非常擅长根据投票记录预测国会议员的政党归属。事实上,它在所有阈值值下正确分类的比率高达 91%!
注意,CART 模型也生成概率,就像逻辑回归模型一样。因此,我们使用一个阈值值(默认为 0.5)将这些概率转换为决策,或者在我们的例子中,转换为分类。然而,在训练 CART 模型时,有一个额外的复杂性层次——我们如何控制决策树中的分裂数量?一种方法是设置每个子集或桶中放入的训练数据点的下限。在MLlib中,这个值可以通过minInstancesPerNode参数进行调整,该参数在训练我们的DecisionTreeClassifier时可用。这个值越小,生成的分裂就越多。
然而,如果它太小,那么就会发生过拟合。相反,如果它太大,那么我们的 CART 模型将过于简单,准确率低。我们将在介绍随机森林时讨论如何选择合适的值。请注意,MLlib还公开了其他可配置的参数,包括maxDepth(树的最大深度)和maxBins,但请注意,树在分裂和深度方面的规模越大,计算和遍历的成本就越高。要了解更多关于DecisionTreeClassifier可调参数的信息,请访问spark.apache.org/docs/latest/ml-classification-regression.html。
随机森林
提高 CART 模型准确性的一个方法是构建多个决策树,而不仅仅是单个决策树。在随机森林中,我们正是这样做的——生成大量 CART 树,然后森林中的每一棵树都对结果进行投票,以多数结果作为最终预测。
要生成随机森林,采用了一种称为自助法的过程,即随机有放回地选择构成森林的每棵树的训练数据。因此,每棵单独的树将使用不同的独立变量子集或桶进行训练,因此训练数据也不同。
K 折交叉验证
现在让我们回到为单个决策树选择合适的下界桶大小的挑战。当训练随机森林时,这个挑战尤其相关,因为随着森林中树的数量增加,计算复杂度也会增加。为了选择合适的最小桶大小,我们可以采用一种称为 K 折交叉验证的过程,其步骤如下:
-
将给定的训练数据集分成 K 个子集或“折”,大小相等。
-
然后使用(K - 1)个折来训练模型,剩余的折,称为验证集,用于测试模型并对考虑中的每个下界桶大小值进行预测。
-
然后对所有可能的训练和测试折组合重复此过程,从而生成多个经过测试的模型,这些模型针对考虑中的每个下界桶大小值在每个折上进行了测试。
-
对于考虑中的每个下界桶大小值,以及每个折,计算模型在该组合对上的准确率。
-
最后,对于每个折,将模型的计算准确率与每个下界桶大小值对比,如图 4.8 所示:

图 4.8:典型的 K 折交叉验证输出
如图 4.8所示,选择较小的下界桶大小值会导致模型过度拟合训练数据,从而降低准确率。相反,选择较大的下界桶大小值也会导致准确率降低,因为模型过于简单。因此,在我们的情况下,我们会选择大约 4 或 5 的下界桶大小值,因为模型的平均准确率似乎在该区域达到最大(如图 4.8中的虚线圆所示)。
返回到我们的 Jupyter Notebook,chp04-04-classification-regression-trees.ipynb,现在让我们使用相同的国会投票数据集来训练一个随机森林模型,看看它是否比我们之前开发的单个分类树有更好的性能:
- 要构建随机森林,我们可以使用
MLlib的RandomForestClassifier估计器在我们的训练数据集上训练随机森林,通过minInstancesPerNode参数指定每个子节点在分裂后必须具有的最小实例数,如下所示:
random_forest = RandomForestClassifier(featuresCol = 'features',
labelCol = label_column, minInstancesPerNode = 5)
random_forest_model = random_forest.fit(train_df)
- 我们现在可以通过使用相同的
BinaryClassificationEvaluator计算 AUC 指标来评估我们在测试数据集上训练的随机森林模型的性能,如下所示:
test_random_forest_predictions_df = random_forest_model
.transform(test_df)
evaluator_rf_roc_area = BinaryClassificationEvaluator(
rawPredictionCol = "rawPrediction", labelCol = label_column,
metricName = "areaUnderROC")
print("Area Under ROC Curve on Test Data = %g" % evaluator_rf_roc_area.evaluate(test_random_forest_predictions_df))
我们训练的随机森林模型具有 0.97 的 AUC 值,这意味着它在根据历史投票记录预测政治归属方面比我们之前开发的单个分类树更准确!
摘要
在本章中,我们已经在 Apache Spark 中使用了各种真实世界的用例,从预测乳腺癌到根据历史投票记录预测政治派别,开发了、测试了和评估了各种监督机器学习模型。
在下一章中,我们将开发、测试和评估无监督机器学习模型!
第五章:使用 Apache Spark 进行无监督学习
在本章中,我们将训练和评估应用于各种实际用例的无监督机器学习模型,再次使用 Python、Apache Spark 及其机器学习库MLlib。具体来说,我们将开发并解释以下类型无监督机器学习模型和技术:
-
层次聚类
-
K-means 聚类
-
主成分分析
聚类
如第三章“人工智能与机器学习”中所述,在无监督学习中,目标是仅根据输入数据,即x[i],没有输出y[i],揭示隐藏的关系、趋势和模式。换句话说,我们的输入数据集将具有以下形式:

聚类是已知无监督学习算法类别的一个例子,其目标是将数据点分割成组,其中特定组中的所有数据点共享相似的特征或属性。然而,由于聚类的性质,建议在大型数据集上训练聚类模型以避免过拟合。最常用的两种聚类算法是层次聚类和k-means 聚类,它们通过构建簇的过程彼此区分。我们将在本章中研究这两种算法。
欧几里得距离
根据定义,为了将数据点聚类成组,我们需要了解两个给定数据点之间的距离。距离的一个常见度量是欧几里得距离,它简单地表示在k-维空间中两个给定点之间的直线距离,其中k是独立变量或特征的个数。形式上,两个点p和q之间的欧几里得距离,给定k个独立变量或维度,定义为以下:

其他常见的距离度量包括曼哈顿距离,它是绝对值的和而不是平方(
)和最大坐标距离,其中只考虑那些偏离最大的数据点。在本章的剩余部分,我们将测量欧几里得距离。现在,我们已经了解了距离,我们可以定义两个簇之间的以下度量,如图图 5.1所示:
-
簇之间的最小距离是彼此最近的两个点之间的距离。
-
簇之间的最大距离是彼此距离最远的两个点之间的距离。
-
簇之间的质心距离是每个簇质心之间的距离,其中质心定义为给定簇中所有数据点的平均值:

图 5.1:簇距离度量
层次聚类
在层次聚类中,每个数据点最初都位于其自己定义的簇中——例如,如果您的数据集中有 10 个数据点,那么最初将有 10 个簇。然后,根据欧几里得重心距离定义的最近的两个簇将被合并。然后,对所有不同的簇重复此过程,直到最终所有数据点都属于同一个簇。
可以使用树状图来可视化此过程,如图图 5.2所示:

图 5.2:层次聚类树状图
树状图帮助我们决定何时停止层次聚类过程。它是通过在x轴上绘制原始数据点以及在y轴上绘制簇之间的距离来生成的。随着新的父簇通过合并最近的簇而创建,在这些子簇之间绘制一条水平线。最终,当所有数据点都属于同一个簇时,树状图结束。树状图的目标是告诉我们何时停止层次聚类过程。我们可以通过在树状图上画一条虚线,放置在最大化虚线与下一水平线(向上或向下)之间垂直距离的位置来推断这一点。然后,停止层次聚类过程的最终簇数就是虚线与垂直线相交的数量。在图 5.2中,我们将得到包含数据点{5, 2, 7}和{8, 4, 10, 6, 1, 3, 9}的两个簇。然而,请确保最终簇数在您的用例上下文中是有意义的。
K-均值聚类
在 k-均值聚类中,遵循不同的过程将数据点分割成簇。首先,必须根据用例上下文预先定义最终簇数k。一旦定义,每个数据点将被随机分配到这些k个簇中的一个,之后采用以下过程:
-
计算每个簇的重心
-
然后将数据点重新分配到与它们最近的簇中
-
然后重新计算所有簇的重心
-
然后将数据点再次重新分配
此过程重复进行,直到无法再重新分配数据点——也就是说,直到没有进一步的改进空间,并且所有数据点都属于一个与它们最近的簇。因此,由于簇的重心定义为给定簇中所有数据点的平均平均值,k-均值聚类实际上将数据点划分为k个簇,每个数据点分配到与其平均平均值最接近的簇中。
注意,在两种聚类过程(层次聚类和 k-means)中,都需要计算距离度量。然而,距离度量根据涉及的独立变量的类型和单位而有所不同——例如,身高和体重。因此,在训练聚类模型之前,首先对数据进行归一化(有时称为特征缩放)是很重要的,以确保其正常工作。要了解更多关于归一化的信息,请访问en.wikipedia.org/wiki/Feature_scaling。
案例研究——检测脑肿瘤
让我们将 k-means 聚类应用于一个非常重要的实际应用案例:从磁共振成像(MRI)扫描中检测脑肿瘤。MRI 扫描在全球范围内用于生成人体详细图像,可用于广泛的医疗应用,从检测癌细胞到测量血流。在本案例研究中,我们将使用健康人脑的灰度 MRI 扫描作为 k-means 聚类模型的输入。然后,我们将应用训练好的 k-means 聚类模型到另一人脑的 MRI 扫描中,以查看我们是否可以检测到可疑的生长物和肿瘤。
注意,在本案例研究中我们将使用的图像相对简单,因为任何存在的可疑生长物都可通过肉眼看到。本案例研究的基本目的是展示如何使用 Python 来操作图像,以及如何通过其 k-means 估计器原生地使用MLlib训练 k-means 聚类模型。
图像特征向量
我们面临的第一挑战是将图像转换为数值特征向量,以便训练我们的 k-means 聚类模型。在我们的案例中,我们将使用灰度 MRI 扫描。一般来说,灰度图像可以被视为像素强度值(介于 0(黑色)和 1(白色)之间)的矩阵,如图图 5.3所示:

图 5.3:灰度图像映射到像素强度值矩阵
结果矩阵的维度等于原始图像的像素高度(m)和宽度(n)。因此,进入我们 k-means 聚类模型的将是关于一个独立变量——像素强度值的(m x n)个观察值。这可以随后表示为一个包含(m x n)个数值元素的单一向量——即(0.0,0.0,0.0,0.2,0.3,0.4,0.3,0.4,0.5……)。
图像分割
现在我们已经从我们的灰度 MRI 图像中导出了特征向量,当我们在健康人脑的 MRI 扫描上训练我们的 k-means 聚类模型时,它将把每个像素强度值分配给k个聚类中的一个。在现实世界的背景下,这些k个聚类代表了大脑中的不同物质,如灰质、白质、脂肪组织和脑脊液,我们的模型将根据颜色将它们分割,这个过程称为图像分割。一旦我们在健康人脑上训练了我们的 k-means 聚类模型并识别了k个不同的聚类,我们就可以将这些定义好的聚类应用于其他患者的 MRI 脑扫描,以尝试识别可疑生长物的存在和体积。
K-means 成本函数
使用 k-means 聚类算法时面临的挑战之一是如何提前选择一个合适的k值,尤其是如果它不明显地来自所讨论用例的更广泛背景。帮助我们的一种方法是,在x轴上绘制一系列可能的k值,与y轴上 k-means 成本函数的输出相对应。k-means 成本函数计算每个点到其对应聚类质心的平方距离的总和。目标是选择一个合适的k值,以最小化成本函数,但不要太大,以免增加生成聚类时的计算复杂性,而成本降低的回报却很小。当我们在下一小节开发用于图像分割的 Spark 应用程序时,我们将展示如何生成此图,从而选择一个合适的k值。
Apache Spark 中的 K-means 聚类
我们将用于我们的 k-means 聚类模型的 MRI 脑部扫描已从癌症影像档案库(TCIA)下载,这是一个匿名化和托管大量癌症医学图像档案以供公共下载的服务,您可以在www.cancerimagingarchive.net/找到。
我们健康人脑的 MRI 扫描可以在伴随本书的 GitHub 仓库中找到,称为mri-images-data/mri-healthy-brain.png。测试人脑的 MRI 扫描称为mri-images-data/mri-test-brain.png。在以下 Spark 应用程序中,当我们在健康人脑的 MRI 扫描上训练 k-means 聚类模型并将其应用于图像分割时,我们将使用这两个。让我们开始:
以下小节将描述对应于本用例的 Jupyter 笔记本中相关的每个单元格,该笔记本称为chp05-01-kmeans-clustering.ipynb。它可以在伴随本书的 GitHub 仓库中找到。
- 让我们打开健康人脑的灰度 MRI 扫描并查看它!我们可以使用 Python 的
scikit-learn机器学习库来实现这一点:
mri_healthy_brain_image = io.imread(
'chapter05/data/mri-images-data/mri-healthy-brain.png')
mri_healthy_brain_image_plot = plt.imshow(
mri_healthy_brain_image, cmap='gray')
渲染的图像如图 5.4 所示:

图 5.4:使用 scikit-learn 和 matplotlib 渲染的原始 MRI 扫描
- 我们现在需要将这个图像转换为一个介于 0 和 1 之间的十进制点像素强度值的矩阵。方便的是,这个函数由
scikit-learn提供的img_as_float方法直接提供,如下面的代码所示。结果矩阵的维度是 256 x 256,这意味着原始图像是 256 x 256 像素:
mri_healthy_brain_matrix = img_as_float(mri_healthy_brain_image)
- 接下来,我们需要将这个矩阵展平成一个 256 x 256 元素的单一向量,其中每个元素代表一个像素强度值。这可以被视为一个维度为 1 x (256 x 256) = 1 x 65536 的另一个矩阵。我们可以使用
numpyPython 库来实现这一点。首先,我们将原始的 256 x 256 矩阵转换为二维numpy数组。然后,我们使用numpy的ravel()方法将这个二维数组展平成一维数组。最后,我们使用np.matrix命令将这个一维数组表示为一个维度为 1 x 65536 的特殊数组或矩阵,如下所示:
mri_healthy_brain_2d_array = np.array(mri_healthy_brain_matrix)
.astype(float)
mri_healthy_brain_1d_array = mri_healthy_brain_2d_array.ravel()
mri_healthy_brain_vector = np.matrix(mri_healthy_brain_1d_array)
- 现在我们已经得到了单个向量,表示为 1 x 65536 维度的矩阵,我们需要将其转换为 Spark 数据框。为了实现这一点,我们首先使用 numpy 的
reshape()方法转置矩阵,使其变为 65536 x 1。然后,我们使用 Spark 的 SQLContext 公开的createDataFrame()方法创建一个包含 65536 个观测值/行和 1 列的 Spark 数据框,代表 65536 个像素强度值,如下面的代码所示:
mri_healthy_brain_vector_transposed = mri_healthy_brain_vector
.reshape(mri_healthy_brain_vector.shape[1],
mri_healthy_brain_vector.shape[0])
mri_healthy_brain_df = sqlContext.createDataFrame(
pd.DataFrame(mri_healthy_brain_vector_transposed,
columns = ['pixel_intensity']))
- 我们现在可以使用
VectorAssembler生成MLlib特征向量,这是一个我们之前见过的方法。VectorAssembler的feature_columns将简单地是我们 Spark 数据框中唯一的像素强度列。通过transform()方法将VectorAssembler应用于我们的 Spark 数据框的输出将是一个新的 Spark 数据框,称为mri_healthy_brain_features_df,包含我们的 65536 个MLlib特征向量,如下所示:
feature_columns = ['pixel_intensity']
vector_assembler = VectorAssembler(inputCols = feature_columns,
outputCol = 'features')
mri_healthy_brain_features_df = vector_assembler
.transform(mri_healthy_brain_df).select('features')
- 我们现在可以计算并绘制 k-means 成本函数的输出,以确定此用例的最佳k值。我们通过在 Spark 数据框中使用
MLlib的KMeans()估计器,遍历range(2, 20)中的k值来实现这一点。然后,我们可以使用matplotlibPython 库来绘制这个图表,如下面的代码所示:
cost = np.zeros(20)
for k in range(2, 20):
kmeans = KMeans().setK(k).setSeed(1).setFeaturesCol("features")
model = kmeans.fit(mri_healthy_brain_features_df
.sample(False, 0.1, seed=12345))
cost[k] = model.computeCost(mri_healthy_brain_features_df)
fig, ax = plt.subplots(1, 1, figsize =(8, 6))
ax.plot(range(2, 20),cost[2:20])
ax.set_title('Optimal Number of Clusters K based on the
K-Means Cost Function for a range of K')
ax.set_xlabel('Number of Clusters K')
ax.set_ylabel('K-Means Cost')
根据结果图,如图图 5.5所示,k的值为 5 或 6 似乎是最理想的。在这些值下,k-means 成本最小化,之后获得的回报很少,如下面的图表所示:

图 5.5:K-means 成本函数
- 我们现在准备训练我们的 k-means 聚类模型!再次,我们将使用
MLlib的KMeans()估计器,但这次我们将使用定义的k值(在我们的例子中是 5,因为我们已在第 6 步中决定)。然后,我们将通过fit()方法将其应用于包含我们的特征向量的 Spark 数据框,并研究我们 5 个结果簇的质心值,如下所示:
k = 5
kmeans = KMeans().setK(k).setSeed(12345).setFeaturesCol("features")
kmeans_model = kmeans.fit(mri_healthy_brain_features_df)
kmeans_centers = kmeans_model.clusterCenters()
print("Healthy MRI Scan - K-Means Cluster Centers: \n")
for center in kmeans_centers:
print(center)
- 接下来,我们将我们的训练好的 k-means 模型应用于包含我们的特征向量的 Spark 数据框,以便将每个 65536 个像素强度值分配到五个簇中的一个。结果将是一个新的 Spark 数据框,其中包含我们的特征向量映射到预测,在这种情况下,预测是一个介于 0 到 4 之间的值,代表五个簇中的一个。然后,我们将这个新的数据框转换为 256 x 256 矩阵,以便我们可以可视化分割图像,如下所示:
mri_healthy_brain_clusters_df = kmeans_model
.transform(mri_healthy_brain_features_df)
.select('features', 'prediction')
mri_healthy_brain_clusters_matrix = mri_healthy_brain_clusters_df
.select("prediction").toPandas().values
.reshape(mri_healthy_brain_matrix.shape[0],
mri_healthy_brain_matrix.shape[1])
plt.imshow(mri_healthy_brain_clusters_matrix)
使用matplotlib渲染的分割图像结果如图 5.6 所示:

图 5.6:分割 MRI 扫描
- 现在我们已经定义了五个簇,我们可以将我们的训练好的 k-means 模型应用于一张新图像以进行分割,也是基于相同的五个簇。首先,我们使用
scikit-learn库加载属于测试患者的新灰度 MRI 脑扫描,就像我们之前使用以下代码做的那样:
mri_test_brain_image = io.imread(
'chapter05/data/mri-images-data/mri-test-brain.png')
- 一旦我们加载了新的 MRI 脑扫描图像,我们需要遵循相同的过程将其转换为包含代表新测试图像像素强度值的特征向量的 Spark 数据框。然后,我们将训练好的 k-means 模型通过
transform()方法应用于这个测试 Spark 数据框,以便将它的像素分配到五个簇中的一个。最后,我们将包含测试图像预测的 Spark 数据框转换为矩阵,以便我们可以可视化分割后的测试图像,如下所示:
mri_test_brain_df = sqlContext
.createDataFrame(pd.DataFrame(mri_test_brain_vector_transposed,
columns = ['pixel_intensity']))
mri_test_brain_features_df = vector_assembler
.transform(mri_test_brain_df)
.select('features')
mri_test_brain_clusters_df = kmeans_model
.transform(mri_test_brain_features_df)
.select('features', 'prediction')
mri_test_brain_clusters_matrix = mri_test_brain_clusters_df
.select("prediction").toPandas().values.reshape(
mri_test_brain_matrix.shape[0], mri_test_brain_matrix.shape[1])
plt.imshow(mri_test_brain_clusters_matrix)
使用matplotlib再次渲染的属于测试患者的分割图像如图 5.7 所示:

图 5.7:测试患者的分割 MRI 扫描
如果我们将两个分割图像并排比较(如图 5.8 所示),我们将看到,由于我们的 k-means 聚类模型,已经渲染了五种不同的颜色,代表五个不同的簇。反过来,这五个不同的簇代表大脑中的不同物质,通过颜色进行分区。我们还将看到,在测试 MRI 脑扫描中,其中一种颜色相对于健康 MRI 脑扫描占据了一个显著更大的区域,这表明可能是一个需要进一步分析的肿瘤,如图中所示:

图 5.8:分割 MRI 扫描的比较
主成分分析
在许多现实世界的用例中,可用于训练模型的特征数量可能非常大。一个常见的例子是经济数据,使用其构成成分的股票价格数据、就业数据、银行数据、工业数据和住房数据一起预测国内生产总值(GDP)。这类数据被称为具有高维性。虽然它们提供了可用于建模的许多特征,但高维数据集增加了机器学习算法的计算复杂性,更重要的是,还可能导致过拟合。过拟合是维度诅咒的结果之一,它正式描述了在高度空间(意味着数据可能包含许多属性,通常是数百甚至数千个维度/特征)中分析数据的问题,但在低维空间中,这种分析不再成立。
非正式地说,它描述了以模型性能为代价增加额外维度的价值。主成分分析(PCA)是一种无监督技术,用于预处理和降低高维数据集的维度,同时保留原始数据集固有的原始结构和关系,以便机器学习模型仍然可以从它们中学习并用于做出准确的预测。
案例研究 – 电影推荐系统
为了更好地理解主成分分析(PCA),让我们研究一个电影推荐用例。我们的目标是构建一个系统,该系统能够根据历史用户社区电影评分(请注意,用户观看历史数据也可以用于此类系统,但这超出了本例的范围)为用户提供个性化的电影推荐。
我们将用于案例研究的用户社区电影评分历史数据已从明尼苏达大学的 GroupLens 研究实验室下载,该实验室收集电影评分并将其公开发布在grouplens.org/datasets/movielens/。为了本案例研究的目的,我们将单个电影和评分数据集转换为一个单一的交叉表,其中 300 行代表 300 个不同的用户,而 3000 列代表 3000 部不同的电影。这个转换后的、管道分隔的数据集可以在本书附带的 GitHub 仓库中找到,并称为movie-ratings-data/user-movie-ratings.csv。
我们将要研究的用户社区电影评分历史数据样本如下:
| 电影 #1Toy Story | 电影 #2Monsters Inc. | 电影 #3Saw | 电影 #4Ring | 电影 #5Hitch | |
|---|---|---|---|---|---|
| 用户 #1 | 4 | 5 | 1 | NULL | 4 |
| 用户 #2 | 5 | NULL | 1 | 1 | NULL |
| 用户 #3 | 5 | 4 | 3 | NULL | 3 |
| 用户 #4 | 5 | 4 | 1 | 1 | NULL |
| 用户 #5 | 5 | 5 | NULL | NULL | 3 |
在这种情况下,每部电影是一个不同的特征(或维度),每个不同的用户是一个不同的实例(或观察)。因此,这个样本表代表了一个包含 5 个特征的数据库。然而,我们的实际数据集包含 3,000 部电影的不同,因此有 3,000 个特征/维度。此外,在现实生活中的表示中,并非所有用户都会对所有电影进行评分,因此将会有大量的缺失值。这样的数据集,以及用来表示它的矩阵,被称为稀疏的。这些问题会给机器学习算法带来问题,无论是在计算复杂性还是在过拟合的可能性方面。
为了解决这个问题,仔细观察之前的样本表。似乎评分很高的用户(对电影#1 评分很高的是《玩具总动员》)通常也对电影#2(《怪物公司》)给出了很高的评分。例如,我们可以说,用户#1 是所有电脑动画儿童电影爱好者的代表,因此我们可以向用户#2 推荐用户#1 历史上评分很高的其他电影(这种使用其他用户数据的推荐系统称为协同过滤)。从高层次来看,这就是 PCA 所做的——它在高维数据集中识别典型表示,称为主成分,以便在保留其潜在结构和在低维中仍然具有代表性的同时,减少原始数据集的维度!然后,这些减少的数据集可以被输入到机器学习模型中进行预测,就像正常一样,而不必担心减少原始数据集的原始大小所带来的任何不利影响。因此,我们可以将 PCA 的正式定义现在扩展,以便我们可以将 PCA 定义为识别一个低维线性子空间,其中原始数据集的最大方差得到保持。
回到我们历史用户社区电影评分数据集,我们不是完全消除电影#2,而是试图创建一个新特征,该特征以某种方式结合了电影#1 和电影#2。扩展这个概念,我们可以创建新的特征,其中每个新特征都是基于所有旧特征,然后根据这些新特征在预测用户电影评分方面的帮助程度对这些新特征进行排序。一旦排序,我们可以删除最不重要的那些,从而实现降维。那么 PCA 是如何实现这一点的呢?它是通过以下步骤实现的:
-
首先,我们对原始高维数据集进行标准化。
-
接下来,我们取标准化的数据并计算一个协方差矩阵,该矩阵提供了一种衡量所有特征之间相互关系的方法。
-
在计算协方差矩阵之后,我们然后找到其特征向量和相应的特征值。特征向量代表主成分,提供了一种理解数据方向的方法。相应的特征值代表在该方向上数据中有多少方差。
-
然后将特征向量根据其对应的特征值降序排列,之后选择前k个特征向量,代表数据中找到的最重要表示。
-
然后使用这些k个特征向量构建一个新的矩阵,从而将原始的n维数据集减少到减少的k维。
协方差矩阵
在数学中,方差是指数据集分散程度的度量,它是每个数据点,x[i],与均值x-bar的平方距离之和除以数据点的总数,N。这可以用以下公式表示:

协方差是指两个或多个随机变量(在我们的情况下,是独立变量)之间相关性强弱的度量,它是通过i维度的变量x和y计算的,如下所示:

如果协方差是正的,这表明独立变量之间是正相关。如果协方差是负的,这表明独立变量之间是负相关。最后,协方差为零意味着独立变量之间没有相关性。您可能会注意到,我们在第四章,使用 Apache Spark 进行监督学习中讨论多元线性回归时描述了相关性。当时,我们计算了因变量与其所有独立变量之间的单向协方差映射。现在我们正在计算所有变量之间的协方差。
协方差矩阵是一个对称的方阵,其中一般元素(i, j)是独立变量i和j之间的协方差,cov(i, j)(这与j和i之间的对称协方差相同)。请注意,协方差矩阵中的对角线实际上代表的是那些元素之间的方差,根据定义。
协方差矩阵如下表所示:
| x | y | z | |
|---|---|---|---|
| x | var(x) | cov(x, y) | cov(x, z) |
| y | cov(y, x) | var(y) | cov(y, z) |
| z | cov(z, x) | cov(z, y) | var(z) |
单位矩阵
单位矩阵是一个主对角线上的所有元素都是 1,其余元素都是 0 的方阵。单位矩阵在我们需要找到矩阵的所有特征向量时非常重要。例如,一个 3x3 的单位矩阵如下所示:

特征向量和特征值
在线性代数中,特征向量是一组特殊的向量,当对其进行线性变换时,其方向保持不变,仅通过一个标量因子改变。在降维的背景下,特征向量代表主成分,并提供了一种理解数据方向的方法。
考虑一个维度为(m x n)的矩阵A。我们可以将A乘以一个向量x(根据定义,其维度为n x 1),这将产生一个新的向量b(维度为m x 1),如下所示:

换句话说,
。
然而,在某些情况下,得到的向量,b,实际上是原始向量,x的缩放版本。我们称这个标量因子为λ,在这种情况下,上述公式可以重写如下:

我们说λ是矩阵A的特征值,x是与λ相关的特征向量。在降维的上下文中,特征值表示数据在该方向上的方差有多大。
为了找到一个矩阵的所有特征向量,我们需要为每个特征值解以下方程,其中I是与矩阵A相同维度的单位矩阵:

解决这个方程的过程超出了本书的范围。然而,要了解更多关于特征向量和特征值的信息,请访问en.wikipedia.org/wiki/Eigenvalues_and_eigenvectors。
一旦找到了协方差矩阵的所有特征向量,这些向量将根据它们对应的特征值按降序排序。由于特征值表示数据在该方向上的方差量,排序列表中的第一个特征向量代表了从原始数据集中捕获原始变量最大方差的第一个主成分,依此类推。例如,如图图 5.9所示,如果我们绘制一个具有两个维度或特征的数据集,第一个特征向量(将按重要性顺序成为第一个主成分)将代表两个特征之间最大变化的方向。
第二个特征向量(按重要性顺序的第二主成分)将代表两个特征之间第二大的变化方向:

图 5.9:两个维度上的主成分
为了帮助选择主成分的数量,k,从特征向量排序列表的顶部选择,我们可以在x轴上绘制主成分的数量,与y轴上的累积解释方差进行对比,如图图 5.10所示,其中解释方差是那个主成分的方差与总方差(即所有特征值的和)的比率:

图 5.10:累积解释方差
以图 5.10为例,我们会选择大约前 300 个主成分,因为这些描述了数据中的最大变化,总共有 3,000 个。最后,我们通过将原始数据集投影到由选定的特征向量表示的k-维空间中,从而构建一个新的矩阵,从而将原始数据集的维度从 3,000 维降低到 300 维。这个预处理和降维后的数据集可以用来训练机器学习模型,就像平常一样。
Apache Spark 中的 PCA
现在,让我们回到我们的转换后的管道分隔的用户社区电影评分数据集,movie-ratings-data/user-movie-ratings.csv,它包含 300 个用户对 3,000 部电影的评价。我们将在 Apache Spark 中开发一个应用程序,旨在使用 PCA(主成分分析)来降低该数据集的维度,同时保留其结构。为此,我们将执行以下步骤:
下面的子节描述了对应于这个用例的 Jupyter 笔记本中相关的每个单元格,该笔记本称为chp05-02-principal-component-analysis.ipynb。这个笔记本可以在伴随这本书的 GitHub 仓库中找到。
- 首先,让我们使用以下代码将转换后的管道分隔的用户社区电影评分数据集加载到 Spark 数据框中。生成的 Spark 数据框将包含 300 行(代表 300 个不同的用户)和 3,001 列(代表 3,000 部电影加上用户 ID 列):
user_movie_ratings_df = sqlContext.read
.format('com.databricks.spark.csv').options(header = 'true',
inferschema = 'true', delimiter = '|')
.load('<Path to CSV File>')
print((user_movie_ratings_df.count(),
len(user_movie_ratings_df.columns)))
- 我们现在可以使用
MLlib的VectorAssembler生成包含 3,000 个元素(代表 3,000 个特征)的MLlib特征向量,就像我们之前看到的那样。我们可以使用以下代码实现这一点:
feature_columns = user_movie_ratings_df.columns
feature_columns.remove('userId')
vector_assembler = VectorAssembler(inputCols = feature_columns,
outputCol = 'features')
user_movie_ratings_features_df = vector_assembler
.transform(user_movie_ratings_df)
.select(['userId', 'features'])
- 在我们能够使用 PCA 降低数据集的维度之前,我们首先需要标准化我们之前描述的特征。这可以通过使用
MLlib的StandardScaler估计器并拟合包含我们的特征向量的 Spark 数据框来实现,如下所示:
standardizer = StandardScaler(withMean=True, withStd=True,
inputCol='features', outputCol='std_features')
standardizer_model = standardizer
.fit(user_movie_ratings_features_df)
user_movie_ratings_standardized_features_df =
standardizer_model.transform(user_movie_ratings_features_df)
- 接下来,我们将我们的缩放特征转换为
MLlibRowMatrix实例。RowMatrix是一个没有索引的分布式矩阵,其中每一行都是一个向量。我们通过将我们的缩放特征数据帧转换为 RDD,并将 RDD 的每一行映射到相应的缩放特征向量来实现这一点。然后,我们将这个 RDD 传递给MLlib的RowMatrix()(如下面的代码所示),从而得到一个 300 x 3,000 维度的标准化特征向量矩阵:
scaled_features_rows_rdd =
user_movie_ratings_standardized_features_df
.select("std_features").rdd
scaled_features_matrix = RowMatrix(scaled_features_rows_rdd
.map(lambda x: x[0].tolist()))
- 现在我们已经将标准化数据以矩阵形式表示,我们可以通过调用
MLlib的RowMatrix公开的computePrincipalComponents()方法轻松地计算前k个主成分。我们可以如下计算前 300 个主成分:
number_principal_components = 300
principal_components = scaled_features_matrix
.computePrincipalComponents(number_principal_components)
- 既然我们已经确定了前 300 个主成分,我们就可以将标准化后的用户社区电影评分数据从 3000 维投影到仅 300 维的线性子空间,同时保留原始数据集的最大方差。这是通过使用矩阵乘法,并将包含标准化数据的矩阵与包含前 300 个主成分的矩阵相乘来实现的,如下所示:
projected_matrix = scaled_features_matrix
.multiply(principal_components)
print((projected_matrix.numRows(), projected_matrix.numCols()))
结果矩阵现在具有 300 x 300 的维度,证实了从原始的 3000 维到仅 300 维的降维!现在我们可以像平常一样使用这个投影矩阵及其 PCA 特征向量作为后续机器学习模型的输入。
- 或者,我们可以直接在包含我们的标准化特征向量的数据框上使用
MLlib的PCA()估计器,以生成一个新的数据框,其中包含一个新的列,包含 PCA 特征向量,如下所示:
pca = PCA(k=number_principal_components, inputCol="std_features",
outputCol="pca_features")
pca_model = pca.fit(user_movie_ratings_standardized_features_df)
user_movie_ratings_pca_df = pca_model
.transform(user_movie_ratings_standardized_features_df)
再次,这个新的数据框及其 PCA 特征向量可以像平常一样用于训练后续的机器学习模型。
- 最后,我们可以通过访问其
explainedVariance属性来从我们的 PCA 模型中提取每个主成分的解释方差,如下所示:
pca_model.explainedVariance
结果向量(300 个元素)显示,在我们的例子中,主成分有序列表中的第一个特征向量(因此是第一个主成分)解释了 8.2%的方差,第二个解释了 4%,依此类推。
在这个案例研究中,我们展示了如何使用 PCA 将用户社区电影评分数据集的维度从 3000 维降低到仅 300 维,同时保留其结构。然后,可以像平常一样使用这个降低维度的数据集来训练机器学习模型,例如用于协同过滤的层次聚类模型。
摘要
在本章中,我们使用 Apache Spark 和多种现实世界的用例训练和评估了各种无监督机器学习模型和技术,包括使用图像分割将人类大脑中发现的多种物质进行分区,以及通过降低高维用户社区电影评分数据集的维度来帮助开发电影推荐系统。
在下一章中,我们将开发、测试和评估一些在自然语言处理(NLP)中常用的算法,试图训练机器自动分析和理解人类文本和语音!
第六章:使用 Apache Spark 进行自然语言处理
在本章中,我们将研究和实现常用的自然语言处理(NLP)算法,这些算法可以帮助我们开发能够自动分析和理解人类文本和语音的机器。具体来说,我们将研究和实现以下几类与 NLP 相关的计算机科学算法:
-
特征转换器,包括以下内容:
-
分词
-
词干提取
-
词形还原
-
规范化
-
-
特征提取器,包括以下内容:
-
词袋模型
-
词频-逆文档频率
-
特征转换器
自然语言处理背后的基本概念是将人类文本和语音视为数据——就像我们在本书中迄今为止遇到的结构化和非结构化数值和分类数据源一样——同时保留其上下文。然而,自然语言是出了名的难以理解,即使是对于人类来说也是如此,更不用说机器了!自然语言不仅包括数百种不同的口语语言,具有不同的书写系统,而且还提出了其他挑战,如不同的语调、屈折、俚语、缩写、隐喻和讽刺。特别是书写系统和通信平台为我们提供了可能包含拼写错误、非传统语法和结构松散的句子的文本。
因此,我们的第一个挑战是将自然语言转换为机器可以使用的、同时保留其潜在上下文的数据。此外,当应用于机器学习时,我们还需要将自然语言转换为特征向量,以便训练机器学习模型。好吧,有两种广泛的计算机科学算法帮助我们应对这些挑战——特征提取器,它帮助我们从自然语言数据中提取相关特征,以及特征转换器,它帮助我们缩放、转换和/或修改这些特征,以便为后续建模做准备。在本小节中,我们将讨论特征转换器以及它们如何帮助我们将自然语言数据转换为更容易处理的结构。首先,让我们介绍一些 NLP 中的常见定义。
文档
在 NLP 中,文档代表文本的逻辑容器。容器本身可以是任何在您的用例上下文中有意义的东西。例如,一个文档可以指一篇单独的文章、记录、社交媒体帖子或推文。
语料库
一旦你定义了你的文档代表什么,语料库就被定义为一系列逻辑上的文档集合。使用之前的例子,语料库可以代表一系列文章(例如,一本杂志或博客)或一系列推文(例如,带有特定标签的推文)。
预处理管道
在自然语言处理中涉及的基本任务之一是尝试尽可能标准化来自不同来源的文档,以进行预处理。预处理不仅帮助我们标准化文本,通常还能减少原始文本的大小,从而降低后续过程和模型计算复杂度。以下小节描述了可能构成典型有序预处理管道的常见预处理技术。
分词
分词是指将文本分割成单个标记或术语的技术。正式来说,一个标记被定义为代表原始文本子集的字符序列。非正式来说,标记通常是组成原始文本的不同单词,并且这些单词是通过使用空白和其他标点符号进行分割的。例如,句子“使用 Apache Spark 的机器学习”可能产生一个以数组或列表形式持久化的标记集合,表示为["Machine", "Learning", "with", "Apache", "Spark"]。
停用词
停用词是在给定语言中常用的单词,用于结构化句子语法,但它们在确定其潜在意义或情感方面不一定有帮助。例如,在英语中,常见的停用词包括and、I、there、this和with。因此,一个常见的预处理技术是通过基于特定语言的停用词查找来过滤这些单词,从而从标记集合中移除它们。使用我们之前的例子,我们的过滤标记列表将是["Machine", "Learning", "Apache", "Spark"]。
词干提取
词干提取是指将单词还原到共同基础或词干的技术。例如,单词“connection”、“connections”、“connective”、“connected”和“connecting”都可以还原到它们的共同词干“connect”。词干提取不是一个完美的过程,词干提取算法可能会出错。然而,为了减少数据集的大小以训练机器学习模型,它是一种有价值的技巧。使用我们之前的例子,我们的过滤词干列表将是["Machin", "Learn", "Apach", "Spark"]。
词形还原
虽然词干提取可以快速将单词还原到基本形式,但它并没有考虑到上下文,因此不能区分在句子或上下文中位置不同而具有不同意义的单词。词形还原并不是简单地基于共同词干来还原单词,而是旨在仅移除屈折词尾,以便返回一个称为词元的单词的词典形式。例如,单词am、is、being和was可以被还原为词元be,而词干提取器则无法推断出这种上下文意义。
虽然词形还原可以在更大程度上保留上下文和意义,但它是以额外的计算复杂度和处理时间为代价的。因此,使用我们之前的例子,我们的过滤词元列表可能看起来像 ["Machine", "Learning", "Apache", "Spark"]。
正规化
最后,正规化指的是一系列常用的技术,用于标准化文本。典型的正规化技术包括将所有文本转换为小写,删除选定的字符、标点符号和其他字符序列(通常使用正则表达式),以及通过应用特定于语言的常用缩写和俚语词典来扩展缩写。
图 6.1 展示了一个典型的有序预处理管道,该管道用于标准化原始书面文本:

图 6.1:典型的预处理管道
特征提取器
我们已经看到特征转换器如何通过预处理管道将我们的文档进行转换、修改和标准化,从而将原始文本转换为一系列标记。特征提取器将这些标记提取出来,并生成特征向量,这些向量可以用于训练机器学习模型。在 NLP 中使用的典型特征提取器的两个常见例子是词袋模型和词频-逆文档频率(TF-IDF)算法。
词袋模型
词袋模型方法简单地计算每个独特单词在原始或标记文本中出现的次数。例如,给定文本 "Machine Learning with Apache Spark, Apache Spark's MLlib and Apache Kafka",词袋模型将为我们提供一个以下数值特征向量:
| Machine | Learning | with | Apache | Spark | MLlib | Kafka |
|---|---|---|---|---|---|---|
| 1 | 1 | 1 | 3 | 2 | 1 | 1 |
注意,每个独特的单词都是一个特征或维度,而词袋模型方法是一种简单的技术,通常用作基准模型,以比较更高级特征提取器的性能。
词频-逆文档频率
TF-IDF 旨在通过提供每个词在整个语料库中出现的频率的重要性指标来改进词袋模型方法。
让我们用 TF(t, d) 来表示词频,即一个词 t 在文档 d 中出现的次数。我们还可以用 DF(t, D) 来表示文档频率,即包含该词 t 的文档数量,在我们的语料库 D 中。然后我们可以定义逆文档频率 IDF(t, D) 如下:

IDF 为我们提供了一个衡量一个词重要性的度量,考虑到该词在整个语料库中出现的频率,其中|D|是我们语料库中文档的总数,D。在语料库中不那么常见的词具有更高的 IDF 度量。然而,请注意,由于使用了对数,如果一个词出现在所有文档中,其 IDF 变为 0——即log(1)。因此,IDF 提供了一个度量标准,更重视描述文档中重要但罕见的词。
最后,为了计算 TF–IDF 度量,我们只需将词频乘以逆文档频率,如下所示:

这意味着 TF–IDF 度量与一个词在文档中出现的次数成比例增加,同时抵消了该词在整个语料库中的频率。这一点很重要,因为仅凭词频可能突出显示像“a”、“I”和“the”这样的词,这些词在特定文档中非常常见,但并不能帮助我们确定文本的潜在含义或情感。通过使用 TF–IDF,我们可以减少这些类型词语对我们分析的影响。
案例研究 – 情感分析
现在我们将这些特征转换器和特征提取器应用于一个非常现代的真实世界用例——情感分析。在情感分析中,目标是分类潜在的文本情感——例如,作者对文本主题是积极、中立还是消极。对许多组织来说,情感分析是一项重要的技术,用于更好地了解他们的客户和目标市场。例如,零售商可以使用情感分析来衡量公众对特定产品的反应,或政治家可以评估公众对政策或新闻条目的情绪。在我们的案例研究中,我们将研究关于航空公司的推文,以预测客户是否对他们表示正面或负面的评论。我们的分析然后可以被航空公司用来通过关注那些被分类为负面情感的推文来改善他们的客户服务。
我们用于案例研究的推文语料库已从Figure Eight下载,这是一家为商业提供高质量真实世界机器学习训练数据集的公司。Figure Eight 还提供了一个“数据人人共享”平台,包含可供公众下载的开放数据集,网址为www.figure-eight.com/data-for-everyone/。
如果你从本书附带的 GitHub 仓库或 Figure Eight 的 Data for Everyone 平台上的任何文本编辑器打开 twitter-data/airline-tweets-labelled-corpus.csv,你将找到一组 14,872 条关于主要航空公司的推文,这些推文是在 2015 年 2 月从 Twitter 上抓取的。这些推文也已经为我们预先标记,包括正面、负面或中性的情感分类。该数据集中的相关列在以下表中描述:
| 列名 | 数据类型 | 描述 |
|---|---|---|
unit_id |
Long |
唯一标识符(主键) |
airline_sentiment |
String |
情感分类——正面、中性或负面 |
airline |
String |
航空公司名称 |
text |
String |
推文的文本内容 |
我们的目标将是使用这个推文语料库来训练一个机器学习模型,以预测关于特定航空公司的未来推文对该航空公司的情感是正面还是负面。
NLP 流程
在我们查看案例研究的 Python 代码之前,让我们可视化我们将构建的端到端 NLP 流程。本案例研究的 NLP 流程如图 6.2 所示:

图 6.2:端到端 NLP 流程
Apache Spark 中的 NLP
截至 Spark 2.3.2 版本,标记化(tokenization)和停用词去除(stop-word removal)功能转换器(以及其他众多功能),以及 TF–IDF 特征提取器在 MLlib 中原生支持。尽管在 Spark 2.3.2 中可以通过对 Spark 数据框上的转换(通过用户定义函数(UDFs)和应用于 RDDs 的映射函数)间接实现词干提取(stemming)、词形还原(lemmatization)和标准化,但我们将使用一个名为 spark-nlp 的第三方 Spark 库来执行这些特征转换。这个第三方库已被设计用来通过提供一个易于使用的 API 来扩展 MLlib 中已有的功能,以便在 Spark 数据框上进行大规模的分布式 NLP 标注。要了解更多关于 spark-nlp 的信息,请访问 nlp.johnsnowlabs.com/。最后,我们将使用 MLlib 中已经原生支持的估计器和转换器——正如我们在前面的章节中所见——来训练我们的最终机器学习分类模型。
注意,通过使用 MLlib 内置的特征转换器和提取器,然后使用第三方 spark-nlp 库提供的特征转换器,最后应用本地的 MLlib 估算器,我们将在我们的流程中需要显式定义和开发数据转换阶段,以符合两个不同库期望的底层数据结构。虽然这由于其低效性不推荐用于生产级流程,但本节的一个目的就是演示如何使用这两个库进行 NLP。读者将能够根据所讨论用例的要求选择合适的库。
根据您的环境设置,有几种方法可以用来安装 spark-nlp,具体描述见nlp.johnsnowlabs.com/quickstart.html。然而,根据我们在第二章中配置的本地开发环境* 设置本地开发环境*,我们将使用 pip 来安装 spark-nlp,这是 Anaconda 分发版中捆绑的另一个常用 Python 包管理器(截至写作时,spark-nlp 通过 conda 仓库不可用,因此我们将使用 pip)。要为我们的 Python 环境安装 spark-nlp,只需执行以下命令,这将安装 spark-nlp 的 1.7.0 版本(这是截至写作时的最新版本,并且与 Spark 2.x 兼容):
> pip install spark-nlp==1.7.0
然后,我们需要告诉 Spark 它可以在哪里找到 spark-nlp 库。我们可以通过在 {SPARK_HOME}/conf/spark-defaults.conf 中定义一个额外的参数,或者在实例化 Spark 上下文时在我们的代码中设置 spark.jars 配置来实现,如下所示:
conf = SparkConf().set("spark.jars", '/opt/anaconda3/lib/python3.6/sitepackages/sparknlp/lib/sparknlp.jar')
.setAppName("Natural Language Processing - Sentiment Analysis")
sc = SparkContext(conf=conf)
请参阅第二章,设置本地开发环境,以获取有关定义 Apache Spark 配置的更多详细信息。请注意,在多节点 Spark 集群中,所有第三方 Python 包要么需要在所有 Spark 节点上安装,要么您的 Spark 应用程序本身需要打包成一个包含所有第三方依赖项的自包含文件。然后,这个自包含文件将被分发到 Spark 集群的所有节点上。
我们现在已准备好在 Apache Spark 中开发我们的 NLP 流程,以便对航空推文语料库进行情感分析!让我们按以下步骤进行:
以下小节描述了对应于本用例的 Jupyter 笔记本中相关的每个单元格,该笔记本称为 chp06-01-natural-language-processing.ipynb。它可以在本书附带的 GitHub 仓库中找到。
- 除了导入标准的 PySpark 依赖项之外,我们还需要导入相关的
spark-nlp依赖项,包括其Tokenizer、Stemmer和Normalizer类,如下所示:
import findspark
findspark.init()
from pyspark import SparkContext, SparkConf
from pyspark.sql import SQLContext
from pyspark.sql.functions import *
from pyspark.sql.types import StructType, StructField
from pyspark.sql.types import LongType, DoubleType, IntegerType, StringType, BooleanType
from pyspark.ml.feature import VectorAssembler
from pyspark.ml.feature import StringIndexer
from pyspark.ml.feature import Tokenizer
from pyspark.ml.feature import StopWordsRemover
from pyspark.ml.feature import HashingTF, IDF
from pyspark.ml import Pipeline, PipelineModel
from pyspark.ml.classification import DecisionTreeClassifier
from pyspark.ml.evaluation import BinaryClassificationEvaluator
from pyspark.mllib.evaluation import MulticlassMetrics
from sparknlp.base import *
from sparknlp.annotator import Tokenizer as NLPTokenizer
from sparknlp.annotator import Stemmer, Normalizer
- 接下来,我们像往常一样实例化一个
SparkContext。请注意,然而,在这种情况下,我们明确告诉 Spark 使用spark-jars配置参数在哪里找到spark-nlp库。然后,我们可以调用我们的SparkContext实例上的getConf()方法来查看当前的 Spark 配置,如下所示:
conf = SparkConf().set("spark.jars", '/opt/anaconda3/lib/python3.6/site-packages/sparknlp/lib/sparknlp.jar')
.setAppName("Natural Language Processing - Sentiment Analysis")
sc = SparkContext(conf=conf)
sqlContext = SQLContext(sc)
sc.getConf().getAll()
- 在将我们的航空公司推文语料库从
twitter-data/airline-tweets-labelled-corpus.csv加载到名为airline_tweets_df的 Spark 数据框之后,我们生成一个新的标签列。现有的数据集已经包含一个名为airline_sentiment的标签列,该列基于手动预分类,可以是"positive"、"neutral"或"negative"。尽管积极的信息自然是始终受欢迎的,但在现实中,最有用的信息通常是负面的。通过自动识别和研究负面信息,组织可以更有效地关注如何根据负面反馈改进他们的产品和服务。因此,我们将创建一个名为negative_sentiment_label的新标签列,如果基础情感被分类为"negative"则为"true",否则为"false",如下面的代码所示:
airline_tweets_with_labels_df = airline_tweets_df
.withColumn("negative_sentiment_label",
when(col("airline_sentiment") == "negative", lit("true"))
.otherwise(lit("false")))
.select("unit_id", "text", "negative_sentiment_label")
- 我们现在准备构建并应用我们的预处理管道到我们的原始推文语料库!在这里,我们展示了如何利用 Spark 的
MLlib自带的特征转换器,即其Tokenizer和StopWordsRemover转换器。首先,我们使用Tokenizer转换器对每条推文的原始文本内容进行分词,从而得到一个包含解析后的标记列表的新列。然后,我们将包含标记的该列传递给StopWordsRemover转换器,该转换器从列表中移除英语语言(默认)的停用词,从而得到一个包含过滤后标记列表的新列。在下一单元格中,我们将展示如何利用spark-nlp第三方库中可用的特征转换器。然而,spark-nlp需要一个string类型的列作为其初始输入,而不是标记列表。因此,最终的语句将过滤后的标记列表重新连接成一个空格分隔的string列,如下所示:
filtered_df = airline_tweets_with_labels_df
.filter("text is not null")
tokenizer = Tokenizer(inputCol="text", outputCol="tokens_1")
tokenized_df = tokenizer.transform(filtered_df)
remover = StopWordsRemover(inputCol="tokens_1",
outputCol="filtered_tokens")
preprocessed_part_1_df = remover.transform(tokenized_df)
preprocessed_part_1_df = preprocessed_part_1_df
.withColumn("concatenated_filtered_tokens",
concat_ws(" ", col("filtered_tokens")))
- 我们现在可以展示如何利用
spark-nlp第三方库中可用的功能转换器和标注器,即其DocumentAssember转换器和Tokenizer、Stemmer、Normalizer标注器。首先,我们从字符串列创建标注文档,这些文档作为spark-nlp管道的初始输入。然后,我们应用spark-nlp的Tokenizer和Stemmer标注器,将我们的过滤令牌列表转换为词根列表。最后,我们应用其Normalizer标注器,该标注器默认将词根转换为小写。所有这些阶段都在一个管道中定义,正如我们在第四章中看到的,使用 Apache Spark 进行监督学习,这是一个有序的机器学习和数据转换步骤列表,在 Spark 数据帧上执行。
我们在我们的数据集上执行我们的管道,得到一个新的数据帧preprocessed_df,我们只保留后续分析和建模所需的相关列,即unit_id(唯一记录标识符)、text(推文的原始原始文本内容)、negative_sentiment_label(我们新的标签)和normalised_stems(作为预处理管道结果的一个过滤、词根化和归一化的spark-nlp数组),如下面的代码所示:
document_assembler = DocumentAssembler()
.setInputCol("concatenated_filtered_tokens")
tokenizer = NLPTokenizer()
.setInputCols(["document"]).setOutputCol("tokens_2")
stemmer = Stemmer().setInputCols(["tokens_2"])
.setOutputCol("stems")
normalizer = Normalizer()
.setInputCols(["stems"]).setOutputCol("normalised_stems")
pipeline = Pipeline(stages=[document_assembler, tokenizer, stemmer,
normalizer])
pipeline_model = pipeline.fit(preprocessed_part_1_df)
preprocessed_df = pipeline_model.transform(preprocessed_part_1_df)
preprocessed_df.select("unit_id", "text",
"negative_sentiment_label", "normalised_stems")
- 在我们能够使用
MLlib的本地特征提取器从我们的词根令牌数组创建特征向量之前,还有一个最后的预处理步骤。包含我们的词根令牌的列,即normalised_stems,将这些令牌持久化在专门的spark-nlp数组结构中。我们需要将这个spark-nlp数组转换回标准的令牌列表,以便我们可以应用MLlib的本地 TF-IDF 算法。我们通过首先分解spark-nlp数组结构来实现这一点,这会产生一个新数据帧观察结果,对应于数组中的每个元素。然后,我们在unit_id上对 Spark 数据帧进行分组,这是每个唯一推文的键,在将词根使用空格分隔符聚合到一个新的字符串列tokens之前。最后,我们应用split函数到这个列上,将聚合的字符串转换为字符串列表或令牌,如下面的代码所示:
exploded_df = preprocessed_df
.withColumn("stems", explode("normalised_stems"))
.withColumn("stems", col("stems").getItem("result"))
.select("unit_id", "negative_sentiment_label", "text", "stems")
aggregated_df = exploded_df.groupBy("unit_id")
.agg(concat_ws(" ", collect_list(col("stems"))),
first("text"), first("negative_sentiment_label"))
.toDF("unit_id", "tokens", "text", "negative_sentiment_label")
.withColumn("tokens", split(col("tokens"), " ")
.cast("array<string>"))
- 我们现在准备好从我们的过滤、词干提取和归一化的标记列表中生成特征向量了!正如所讨论的,我们将使用 TF–IDF 特征提取器来生成特征向量,而不是基本的词袋方法。TF–IDF 特征提取器是
MLlib的本地功能,分为两部分。首先,我们通过将我们的标记列表传递到MLlib的HashingTF转换器中来生成词频(TF)特征向量。然后,我们将MLlib的逆文档频率(IDF)估计器拟合到包含词频特征向量的数据框中,如下面的代码所示。结果是包含在名为features的列中的新的 Spark 数据框,其中包含我们的 TF–IDF 特征向量:
hashingTF = HashingTF(inputCol="tokens", outputCol="raw_features",
numFeatures=280)
features_df = hashingTF.transform(aggregated_df)
idf = IDF(inputCol="raw_features", outputCol="features")
idf_model = idf.fit(features_df)
scaled_features_df = idf_model.transform(features_df)
- 正如我们在第四章,使用 Apache Spark 进行监督学习中所见,由于我们的标签列本质上是分类的,我们需要将其应用到
MLlib的StringIndexer中,以便识别和索引所有可能的分类。结果是包含索引标签列的新 Spark 数据框,名为"label",如果negative_sentiment_label为true,则其值为 0.0,如果negative_sentiment_label为false,则其值为 1.0,如下面的代码所示:
indexer = StringIndexer(inputCol = "negative_sentiment_label",
outputCol = "label").fit(scaled_features_df)
scaled_features_indexed_label_df = indexer.transform(scaled_features_df)
- 我们现在准备好创建训练和测试数据框,以便训练和评估后续的机器学习模型。我们像往常一样使用
randomSplit方法(如下面的代码所示)来实现这一点,但在这个案例中,90%的所有观察结果将进入我们的训练数据框,剩下的 10%将进入我们的测试数据框:
train_df, test_df = scaled_features_indexed_label_df
.randomSplit([0.9, 0.1], seed=12345)
- 在这个例子中,我们将训练一个监督决策树分类器(参见第四章,使用 Apache Spark 进行监督学习),以便帮助我们判断给定的推文是正面情绪还是负面情绪。正如第四章,使用 Apache Spark 进行监督学习中所述,我们将
MLlib的DecisionTreeClassifier估计器拟合到我们的训练数据框中,以训练我们的分类树,如下面的代码所示:
decision_tree = DecisionTreeClassifier(featuresCol = 'features',
labelCol = 'label')
decision_tree_model = decision_tree.fit(train_df)
- 现在我们已经训练好了一个分类树,我们可以将其应用到我们的测试数据框中,以便对测试推文进行分类。正如我们在第四章,使用 Apache Spark 进行监督学习中所做的那样,我们使用
transform()方法(如下面的代码所示)将我们的训练好的分类树应用到测试数据框中,然后研究其预测的分类:
test_decision_tree_predictions_df = decision_tree_model
.transform(test_df)
print("TEST DATASET PREDICTIONS AGAINST ACTUAL LABEL: ")
test_decision_tree_predictions_df.select("prediction", "label",
"text").show(10, False)
例如,我们的决策树分类器预测了以下来自我们的测试数据框的推文是负面情绪:
-
"我需要你...成为一个更好的航空公司。^LOL"
-
"如果不能保证家长会和孩子一起坐,就不要承诺卖票"
-
"解决了,我很烦等你们。我想退款,并想和某人谈谈这件事。"
-
"我本想回复你的网站,直到我看到那个真的很长的形式。在商业中,新座位很糟糕"
人类也可能将这些推文归类为负面情感!但更重要的是,航空公司可以使用这个模型以及它识别的推文来关注改进的领域。根据这个推文样本,这些领域可能包括网站可用性、票务营销以及处理退款所需的时间。
- 最后,为了量化我们训练的分类树的准确性,让我们使用以下代码在测试数据上计算其混淆矩阵:
predictions_and_label = test_decision_tree_predictions_df
.select("prediction", "label").rdd
metrics = MulticlassMetrics(predictions_and_label)
print("N = %g" % test_decision_tree_predictions_df.count())
print(metrics.confusionMatrix())
得到的混淆矩阵如下所示:
| **预测 y = 0 (负面) | 预测 y = 1 (非负) | |
|---|---|---|
| 实际 y = 0****(负面) | 725 | 209 |
| 实际 y = 1****(非负) | 244 | 325 |
我们可以这样解释这个混淆矩阵——在总共 1,503 条测试推文中,我们的模型表现出以下特性:
-
正确地将 725 条实际上是情感负面的推文分类为负面情感
-
正确地将 325 条实际上是情感非负的推文分类为非负情感
-
错误地将 209 条实际上是负情感的推文分类为非负情感
-
错误地将 244 条实际上是情感非负的推文分类为负面情感
-
总体准确率 = 70%
-
总体错误率 = 30%
-
灵敏度 = 57%
-
特异性 = 78%
因此,基于默认的阈值值 0.5(在这个案例研究中是合适的,因为我们没有对哪种错误更好有偏好),我们的决策树分类器有 70%的整体准确率,这相当不错!
- 为了完整性,让我们训练一个决策树分类器,但使用从词袋算法中导出的特征向量。请注意,当我们应用
HashingTF转换器到我们的预处理语料库以计算词频(TF)特征向量时,我们已经计算了这些特征向量。因此,我们只需重复我们的机器学习流程,但仅基于HashingTF转换器的输出,如下所示:
# Create Training and Test DataFrames based on the Bag of Words Feature Vectors
bow_indexer = StringIndexer(inputCol = "negative_sentiment_label",
outputCol = "label").fit(features_df)
bow_features_indexed_label_df = bow_indexer.transform(features_df)
.withColumnRenamed("raw_features", "features")
bow_train_df, bow_test_df = bow_features_indexed_label_df
.randomSplit([0.9, 0.1], seed=12345)
# Train a Decision Tree Classifier using the Bag of Words Feature Vectors
bow_decision_tree = DecisionTreeClassifier(featuresCol =
'features', labelCol = 'label')
bow_decision_tree_model = bow_decision_tree.fit(bow_train_df)
# Apply the Bag of Words Decision Tree Classifier to the Test DataFrame and generate the Confusion Matrix
bow_test_decision_tree_predictions_df = bow_decision_tree_model
.transform(bow_test_df)
bow_predictions_and_label = bow_test_decision_tree_predictions_df
.select("prediction", "label").rdd
bow_metrics = MulticlassMetrics(bow_predictions_and_label)
print("N = %g" % bow_test_decision_tree_predictions_df.count())
print(bow_metrics.confusionMatrix())
注意,得到的混淆矩阵与我们使用IDF估计器在缩放特征向量上训练的决策树分类器时得到的混淆矩阵完全相同。这是因为我们的推文语料库相对较小,有 14,872 个文档,因此基于语料库中词频的缩放词频(TF)特征向量将对这个特定模型的预测质量产生微乎其微的影响。
MLlib提供的一个非常有用的功能是将训练好的机器学习模型保存到磁盘上以供以后使用。我们可以通过将训练好的决策树分类器保存到单节点开发环境的本地磁盘上,来利用这个功能。在多节点集群中,训练好的模型也可以简单地通过使用相关的文件系统前缀(例如hdfs://<HDFS NameNode URL>/<HDFS Path>)保存到分布式文件系统,如 Apache Hadoop 分布式文件系统(参见第一章,大数据生态系统),如下面的代码所示:
bow_decision_tree_model.save('<Target filesystem path to save MLlib Model>')
我们用于对航空公司推文进行情感分析的训练好的决策树分类器也已推送到本书配套的 GitHub 仓库中,可以在 chapter06/models/airline-sentiment-analysis-decision-tree-classifier 中找到。
摘要
在本章中,我们研究了、实现了并评估了在自然语言处理中常用的算法。我们使用特征转换器对文档语料库进行了预处理,并使用特征提取器从处理后的语料库中生成了特征向量。我们还将这些常见的 NLP 算法应用于机器学习。我们训练并测试了一个情感分析模型,该模型用于预测推文的潜在情感,以便组织可以改进其产品和服务的提供。在第八章,使用 Apache Spark 的实时机器学习中,我们将扩展我们的情感分析模型,使其能够使用 Spark Streaming 和 Apache Kafka 在实时环境中运行。
在下一章中,我们将亲身体验探索令人兴奋且前沿的深度学习世界!
第七章:使用 Apache Spark 进行深度学习
在本章中,我们将亲身体验深度学习这个激动人心且前沿的世界!我们将结合使用第三方深度学习库和 Apache Spark 的MLlib来执行精确的光学字符识别(OCR),并通过以下类型的人工神经网络和机器学习算法自动识别和分类图像:
-
多层感知器
-
卷积神经网络
-
迁移学习
人工神经网络
如我们在第三章《人工智能与机器学习》中所研究的,人工神经网络(ANN)是一组连接的人工神经元,它们被聚合为三种类型的链接神经网络层——输入层、零个或多个隐藏层和输出层。单层ANN 仅由输入节点和输出节点之间的一个层链接组成,而多层ANN 的特点是人工神经元分布在多个链接层中。
信号仅沿一个方向传播的人工神经网络——也就是说,信号被输入层接收并转发到下一层进行处理——被称为前馈网络。信号可能被传播回已经处理过该信号的输入神经元或神经层的 ANN 被称为反馈网络。
反向传播是一种监督学习过程,通过这个过程多层 ANN 可以学习——也就是说,推导出一个最优的权重系数集。首先,所有权重都最初设置为随机,然后计算网络的输出。如果预测输出与期望输出不匹配,则输出节点的总误差会通过整个网络反向传播,以尝试重新调整网络中的所有权重,以便在输出层减少误差。换句话说,反向传播通过迭代权重调整过程来最小化实际输出和期望输出之间的差异。
多层感知器
单层感知器(SLP)是一种基本的人工神经网络(ANN),它仅由两层节点组成——一个包含输入节点的输入层和一个包含输出节点的输出层。然而,多层感知器(MLP)在输入层和输出层之间引入了一个或多个隐藏层,这使得它们能够学习非线性函数,如图 7.1 所示:

图 7.1:多层感知器神经网络架构
MLP 分类器
Apache Spark 的机器学习库MLlib提供了一个现成的多层感知器分类器(MLPC),可以应用于需要从k个可能的类别中进行预测的分类问题。
输入层
在MLlib的 MLPC 中,输入层的节点代表输入数据。让我们将这个输入数据表示为一个具有m个特征的向量,X,如下所示:

隐藏层
然后将输入数据传递到隐藏层。为了简化,让我们假设我们只有一个隐藏层h¹,并且在这个隐藏层中,我们有n个神经元,如下所示:

对于这些隐藏神经元中的每一个,激活函数的净输入z是输入数据集向量X乘以一个权重集向量Wn(对应于分配给隐藏层中*n*个神经元的权重集),其中每个权重集向量*W*n 包含m个权重(对应于我们输入数据集向量X中的m个特征),如下所示:

在线性代数中,将一个向量乘以另一个向量的乘积称为点积,它输出一个由z表示的标量(即一个数字),如下所示:

偏差,如第三章《人工智能与机器学习》中所示,并在图 3.5中展示,是一个独立的常数,类似于回归模型中的截距项,并且可以添加到前馈神经网络的非输出层。它被称为独立,因为偏差节点没有连接到前面的层。通过引入一个常数,我们可以允许激活函数的输出向左或向右移动该常数,从而增加人工神经网络学习模式的有效性,提供基于数据移动决策边界的功能。
注意,在一个包含n个隐藏神经元的单隐藏层中,将计算n个点积运算,如图图 7.2所示:

图 7.2:隐藏层净输入和输出
在MLlib的 MLPC 中,隐藏神经元使用sigmoid激活函数,如下公式所示:

正如我们在第三章《人工智能与机器学习》中看到的,Sigmoid(或逻辑)函数在 0 和 1 之间有界,并且对所有实数输入值都有平滑的定义。通过使用 sigmoid 激活函数,隐藏层中的节点实际上对应于一个逻辑回归模型。如果我们研究 sigmoid 曲线,如图图 7.3所示,我们可以声明,如果净输入z是一个大的正数,那么 sigmoid 函数的输出,以及我们隐藏神经元的激活函数,将接近 1。相反,如果净输入 z 是一个具有大绝对值的负数,那么 sigmoid 函数的输出将接近 0:

图 7.3:sigmoid 函数
在所有情况下,每个隐藏神经元都会接收净输入,即z,它是输入数据X和权重集W^n的点积,再加上一个偏置,并将其应用于 sigmoid 函数,最终输出一个介于 0 和 1 之间的数字。在所有隐藏神经元计算了它们的激活函数的结果之后,我们就会从隐藏层h¹中得到n个隐藏输出,如下所示:

输出层
隐藏层的输出随后被用作输入,以计算输出层的最终输出。在我们的例子中,我们只有一个隐藏层,即h¹,其输出
。这些输出随后成为输出层的n个输入。
输出层神经元的激活函数的净输入是隐藏层计算出的这些n个输入,乘以一个权重集向量,即Wh*,其中每个权重集向量*Wh包含n个权重(对应于n个隐藏层输入)。为了简化,让我们假设我们输出层只有一个输出神经元。因此,这个神经元的权重集向量如下所示:

再次强调,由于我们是在乘以向量,我们使用点积计算,这将计算以下表示我们的净输入z的标量:

在MLlib的 MLPC 中,输出神经元使用 softmax 函数作为激活函数,它通过预测k个类别而不是标准的二分类来扩展逻辑回归。此函数具有以下形式:

因此,输出层的节点数对应于你希望预测的可能类别数。例如,如果你的用例有五个可能的类别,那么你将训练一个输出层有五个节点的 MLP。因此,激活函数的最终输出是相关输出神经元所做的预测,如图7.4所示:

图 7.4:输出层净输入和输出
注意,图 7.4 说明了 MLP 的初始正向传播,其中输入数据传播到隐藏层,隐藏层的输出传播到输出层,在那里计算最终输出。MLlib 的 MLPC 随后使用反向传播来训练神经网络并学习模型,通过迭代权重调整过程最小化实际输出和期望输出之间的差异。MLPC 通过寻求最小化损失函数来实现这一点。损失函数计算了关于分类问题的不准确预测所付出的代价的度量。MLPC 使用的特定损失函数是逻辑损失函数,其中具有高置信度预测的惩罚较小。要了解更多关于损失函数的信息,请访问en.wikipedia.org/wiki/Loss_functions_for_classification。
案例研究 1 – OCR
证明 MLP 强大功能的一个很好的实际案例是 OCR。在 OCR 中,挑战是识别人类书写,将每个手写符号分类为字母。在英文字母的情况下,有 26 个字母。因此,当应用于英语时,OCR 实际上是一个具有k=26 个可能类别的分类问题!
我们将要使用的这个数据集是从加州大学(加州大学)的机器学习仓库中提取的,该仓库位于archive.ics.uci.edu/ml/index.php。我们将使用的特定字母识别数据集,可以从本书附带的 GitHub 仓库以及archive.ics.uci.edu/ml/datasets/letter+recognition获取,由 Odesta 公司的 David J. Slate 创建;地址为 1890 Maple Ave;Suite 115;Evanston, IL 60201,并在 P. W. Frey 和 D. J. Slate 合著的论文《使用荷兰风格自适应分类器的字母识别》(来自《机器学习》第 6 卷第 2 期,1991 年 3 月)中使用。
图 7.5 展示了该数据集的视觉示例。我们将训练一个 MLP 分类器来识别和分类每个符号,例如图 7.5中所示,将其识别为英文字母:

图 7.5:字母识别数据集
输入数据
在我们进一步探讨我们特定数据集的架构之前,让我们首先了解 MLP 如何真正帮助我们解决这个问题。首先,正如我们在第五章中看到的,“使用 Apache Spark 进行无监督学习”,在研究图像分割时,图像可以被分解为像素强度值(用于灰度图像)或像素 RGB 值(用于彩色图像)的矩阵。然后可以生成一个包含(m x n)数值元素的单一向量,对应于图像的像素高度(m)和宽度(n)。
训练架构
现在,想象一下,我们想要使用我们的整个字母识别数据集来训练一个 MLP,如图图 7.6所示:

图 7.6:用于字母识别的多层感知器
在我们的 MLP 中,输入层有p(= m x n)个神经元,它们代表图像中的p个像素强度值。一个单一的隐藏层有n个神经元,输出层有 26 个神经元,代表英语字母表中的 26 个可能的类别或字母。在训练这个神经网络时,由于我们最初不知道应该分配给每一层的权重,我们随机初始化权重并执行第一次前向传播。然后我们迭代地使用反向传播来训练神经网络,从而得到一组经过优化的权重,使得输出层做出的预测/分类尽可能准确。
隐藏层中的模式检测
隐藏层中神经元的任务是学习在输入数据中检测模式。在我们的例子中,隐藏层中的神经元将检测构成更广泛符号的某些子结构。这如图图 7.7所示,我们假设隐藏层中的前三个神经元分别学会了识别正斜杠、反斜杠和水平线类型的模式:

图 7.7:隐藏层中的神经元检测模式和子结构
输出层的分类
在我们的神经网络中,输出层中的第一个神经元被训练来决定给定的符号是否是大写英文字母A。假设隐藏层中的前三个神经元被激活,我们预计输出层中的第一个神经元将被激活,而剩下的 25 个神经元不会被激活。这样,我们的 MLP 就会将这个符号分类为字母A!
注意,我们的训练架构仅使用单个隐藏层,这只能学习非常简单的模式。通过添加更多隐藏层,人工神经网络可以学习更复杂的模式,但这会以计算复杂性、资源和训练运行时间的增加为代价。然而,随着分布式存储和处理技术的出现,正如在第一章“大数据生态系统”中讨论的,其中大量数据可以存储在内存中,并且可以在分布式方式下对数据进行大量计算,今天我们能够训练具有大量隐藏层和隐藏神经元的极其复杂的神经网络。这种复杂的神经网络目前正在应用于广泛的领域,包括人脸识别、语音识别、实时威胁检测、基于图像的搜索、欺诈检测和医疗保健的进步。
Apache Spark 中的 MLP
让我们回到我们的数据集,并在 Apache Spark 中训练一个 MLP 来识别和分类英文字母。如果你在任何文本编辑器中打开 ocr-data/letter-recognition.data,无论是来自本书配套的 GitHub 仓库还是来自 UCI 的机器学习仓库,你将找到 20,000 行数据,这些数据由以下模式描述:
| 列名 | 数据类型 | 描述 |
|---|---|---|
lettr |
字符串 |
英文字母(26 个值之一,从 A 到 Z) |
x-box |
整数 |
矩形水平位置 |
y-box |
整数 |
矩形垂直位置 |
width |
整数 |
矩形宽度 |
high |
整数 |
矩形高度 |
onpix |
整数 |
颜色像素总数 |
x-bar |
整数 |
矩形内颜色像素的 x 均值 |
y-bar |
整数 |
矩形内颜色像素的 y 均值 |
x2bar |
整数 |
x 的方差平均值 |
y2bar |
整数 |
y 的方差平均值 |
xybar |
整数 |
x 和 y 的相关平均值 |
x2ybr |
整数 |
x 和 y 的平均值 |
xy2br |
整数 |
x 和 y 的平方平均值 |
x-ege |
整数 |
从左到右的平均边缘计数 |
xegvy |
整数 |
x-ege 与 y 的相关性 |
y-ege |
整数 |
从下到上的平均边缘计数 |
yegvx |
整数 |
y-ege 与 x 的相关性 |
此数据集描述了 16 个数值属性,这些属性基于扫描字符图像的像素分布的统计特征,如 图 7.5 中所示。这些属性已经标准化并线性缩放到 0 到 15 的整数范围内。对于每一行,一个名为 lettr 的标签列表示它所代表的英文字母,其中没有特征向量映射到多个类别——也就是说,每个特征向量只映射到英文字母表中的一个字母。
你会注意到我们没有使用原始图像本身的像素数据,而是使用从像素分布中得到的统计特征。然而,当我们从第五章,“使用 Apache Spark 进行无监督学习”中学习到的知识时,我们特别关注将图像转换为数值特征向量时,我们将在下一刻看到的相同步骤可以遵循来使用原始图像本身训练 MLP 分类器。
让我们现在使用这个数据集来训练一个 MLP 分类器以识别符号并将它们分类为英文字母表中的字母:
以下小节描述了对应于本用例的 Jupyter 笔记本中每个相关的单元格,该笔记本称为chp07-01-multilayer-perceptron-classifier.ipynb。这个笔记本可以在本书附带的 GitHub 仓库中找到。
- 首先,我们像往常一样导入必要的 PySpark 库,包括
MLlib的MultilayerPerceptronClassifier分类器和MulticlassClassificationEvaluator评估器,如下面的代码所示:
import findspark
findspark.init()
from pyspark import SparkContext, SparkConf
from pyspark.sql import SQLContext
from pyspark.ml.feature import VectorAssembler
from pyspark.ml.classification import MultilayerPerceptronClassifier
from pyspark.ml.evaluation import MulticlassClassificationEvaluator
- 在实例化 Spark 上下文之后,我们现在准备好将我们的数据集导入 Spark 数据框中。请注意,在我们的情况下,我们已经将数据集预处理为 CSV 格式,其中我们将
lettr列从string数据类型转换为表示英文字母表中 26 个字符之一的numeric数据类型。这个预处理好的 CSV 文件可以在本书附带的 GitHub 仓库中找到。一旦我们将这个 CSV 文件导入 Spark 数据框中,我们就使用VectorAssembler生成特征向量,包含 16 个特征列,就像通常那样。因此,生成的 Spark 数据框,称为vectorised_df,包含两个列——表示英文字母表中 26 个字符之一的数值label列,以及包含我们的特征向量的features列:
letter_recognition_df = sqlContext.read
.format('com.databricks.spark.csv')
.options(header = 'true', inferschema = 'true')
.load('letter-recognition.csv')
feature_columns = ['x-box','y-box','width','high','onpix','x-bar',
'y-bar','x2bar','y2bar','xybar','x2ybr','xy2br','x-ege','xegvy',
'y-ege','yegvx']
vector_assembler = VectorAssembler(inputCols = feature_columns,
outputCol = 'features')
vectorised_df = vector_assembler.transform(letter_recognition_df)
.withColumnRenamed('lettr', 'label').select('label', 'features')
- 接下来,我们使用以下代码以 75%到 25%的比例将我们的数据集分为训练集和测试集:
train_df, test_df = vectorised_df
.randomSplit([0.75, 0.25], seed=12345)
-
现在,我们已经准备好训练我们的 MLP 分类器。首先,我们必须定义我们神经网络各自层的尺寸。我们通过定义一个包含以下元素的 Python 列表来完成此操作:
-
第一个元素定义了输入层的尺寸。在我们的情况下,我们的数据集中有 16 个特征,因此我们将此元素设置为
16。 -
下一个元素定义了中间隐藏层的尺寸。我们将定义两个隐藏层,分别大小为
8和4。 -
最后一个元素定义了输出层的尺寸。在我们的情况下,我们有 26 个可能的类别,代表英文字母表中的 26 个字母,因此我们将此元素设置为
26:
-
layers = [16, 8, 4, 26]
- 现在我们已经定义了我们的神经网络架构,我们可以使用
MLlib的MultilayerPerceptronClassifier分类器来训练一个 MLP,并将其拟合到训练数据集上,如下面的代码所示。记住,MLlib的MultilayerPerceptronClassifier分类器为隐藏神经元使用 sigmoid 激活函数,为输出神经元使用 softmax 激活函数:
multilayer_perceptron_classifier = MultilayerPerceptronClassifier(
maxIter = 100, layers = layers, blockSize = 128, seed = 1234)
multilayer_perceptron_classifier_model =
multilayer_perceptron_classifier.fit(train_df)
- 我们现在可以将我们的训练好的 MLP 分类器应用到测试数据集上,以预测 16 个与像素相关的数值特征代表英语字母表中的 26 个字母中的哪一个,如下所示:
test_predictions_df = multilayer_perceptron_classifier_model
.transform(test_df)
print("TEST DATASET PREDICTIONS AGAINST ACTUAL LABEL: ")
test_predictions_df.select("label", "features", "probability",
"prediction").show()
TEST DATASET PREDICTIONS AGAINST ACTUAL LABEL:
+-----+--------------------+--------------------+----------+
|label| features| probability|prediction|
+-----+--------------------+--------------------+----------+
| 0|[1.0,0.0,2.0,0.0,...|[0.62605849526384...| 0.0|
| 0|[1.0,0.0,2.0,0.0,...|[0.62875656935176...| 0.0|
| 0|[1.0,0.0,2.0,0.0,...|[0.62875656935176...| 0.0|
+-----+--------------------+--------------------+----------+
- 接下来,我们使用以下代码计算我们训练好的 MLP 分类器在测试数据集上的准确性。在我们的案例中,它的表现非常糟糕,准确率仅为 34%。我们可以得出结论,在我们的数据集中,具有大小分别为 8 和 4 的两个隐藏层的 MLP 在识别和分类扫描图像中的字母方面表现非常糟糕:
prediction_and_labels = test_predictions_df
.select("prediction", "label")
accuracy_evaluator = MulticlassClassificationEvaluator(
metricName = "accuracy")
precision_evaluator = MulticlassClassificationEvaluator(
metricName = "weightedPrecision")
recall_evaluator = MulticlassClassificationEvaluator(
metricName = "weightedRecall")
print("Accuracy on Test Dataset = %g" % accuracy_evaluator
.evaluate(prediction_and_labels))
print("Precision on Test Dataset = %g" % precision_evaluator
.evaluate(prediction_and_labels))
print("Recall on Test Dataset = %g" % recall_evaluator
.evaluate(prediction_and_labels))
Accuracy on Test Dataset = 0.339641
Precision on Test Dataset = 0.313333
Recall on Test Dataset = 0.339641
- 我们如何提高我们神经网络分类器的准确性?为了回答这个问题,我们必须重新审视我们对隐藏层功能的定义。记住,隐藏层中神经元的任务是学习在输入数据中检测模式。因此,在我们的神经网络架构中定义更多的隐藏神经元应该会增加我们的神经网络检测更多模式并具有更高分辨率的能力。为了测试这个假设,我们将我们两个隐藏层中的神经元数量分别增加到 16 和 12,如下面的代码所示。然后,我们重新训练我们的 MLP 分类器并将其重新应用到测试数据集上。这导致了一个性能远更好的模型,准确率达到 72%:
new_layers = [16, 16, 12, 26]
new_multilayer_perceptron_classifier =
MultilayerPerceptronClassifier(maxIter = 400,
layers = new_layers, blockSize = 128, seed = 1234)
new_multilayer_perceptron_classifier_model =
new_multilayer_perceptron_classifier.fit(train_df)
new_test_predictions_df =
new_multilayer_perceptron_classifier_model.transform(test_df)
print("New Accuracy on Test Dataset = %g" % accuracy_evaluator
.evaluate(new_test_predictions_df
.select("prediction", "label")))
卷积神经网络
我们已经看到,MLPs 可以通过一个或多个中间隐藏层对单个输入向量进行转换来识别和分类小图像,如 OCR 中的字母和数字。然而,MLP 的一个局限性是它们在处理较大图像时的扩展能力,这不仅要考虑单个像素强度或 RGB 值,还要考虑图像本身的高度、宽度和深度。
卷积神经网络(CNNs)假设输入数据具有网格状拓扑结构,因此它们主要用于识别和分类图像中的对象,因为图像可以被表示为像素的网格。
端到端神经网络架构
卷积神经网络的端到端架构如图7.8所示:

图 7.8:卷积神经网络架构
在以下小节中,我们将描述构成卷积神经网络(CNN)的每一层和变换。
输入层
由于卷积神经网络(CNN)主要用于图像分类,因此输入 CNN 的数据是具有维度h(像素高度)、w(像素宽度)和d(深度)的图像矩阵。在 RGB 图像的情况下,深度将是三个相应的颜色通道,即红色、绿色和蓝色(RGB)。这如图 7.9 所示:

图 7.9:图像矩阵维度
卷积层
CNN 中接下来发生的转换是在卷积层中处理的。卷积层的目的是在图像中检测特征,这是通过使用滤波器(也称为核)实现的。想象一下拿一个放大镜观察一个图像,从图像的左上角开始。当我们从左到右和从上到下移动放大镜时,我们检测到放大镜移动过的每个位置的不同特征。在高层上,这就是卷积层的工作,其中放大镜代表滤波器或核,滤波器每次移动的步长大小,通常是像素级,被称为步长大小。卷积层的输出称为特征图。
让我们通过一个例子来更好地理解卷积层中进行的处理过程。想象一下,我们有一个 3 像素(高度)乘以 3 像素(宽度)的图像。为了简化,我们将在例子中忽略代表图像深度的第三维度,但请注意,现实世界的卷积对于 RGB 图像是在三个维度上计算的。接下来,想象一下我们的滤波器是一个 2 像素(高度)乘以 2 像素(宽度)的矩阵,并且我们的步长大小是 1 像素。
这些相应的矩阵在图 7.10中展示:

图 7.10:图像矩阵和滤波器矩阵
首先,我们将我们的滤波器矩阵放置在图像矩阵的左上角,并在该位置进行两个矩阵的矩阵乘法。然后,我们将滤波器矩阵向右移动我们的步长大小——1 个像素,并在该位置进行矩阵乘法。我们继续这个过程,直到滤波器矩阵穿越整个图像矩阵。结果的特征图矩阵在图 7.11中展示:

图 7.11:特征图
注意,特征图的维度比卷积层的输入矩阵小。为了确保输出维度与输入维度匹配,通过一个称为填充的过程添加了一个零值像素层。此外,滤波器必须具有与输入图像相同的通道数——因此,在 RGB 图像的情况下,滤波器也必须具有三个通道。
那么,卷积是如何帮助神经网络学习的呢?为了回答这个问题,我们必须回顾一下过滤器概念。过滤器本身是训练用来检测图像中特定模式的权重矩阵,不同的过滤器可以用来检测不同的模式,如边缘和其他特征。例如,如果我们使用一个预先训练用来检测简单边缘的过滤器,当我们把这个过滤器移动到图像上时,如果存在边缘,卷积计算将输出一个高值实数(作为矩阵乘法和求和的结果),如果不存在边缘,则输出一个低值实数。
当过滤器完成对整个图像的遍历后,输出是一个特征图矩阵,它表示该过滤器在图像所有部分的卷积。通过在每一层的不同卷积中使用不同的过滤器,我们得到不同的特征图,这些特征图构成了卷积层的输出。
矩形线性单元
与其他神经网络一样,激活函数定义了节点的输出,并用于使我们的神经网络能够学习非线性函数。请注意,我们的输入数据(构成图像的 RGB 像素)本身是非线性的,因此我们需要一个非线性激活函数。矩形线性单元(ReLU)在 CNN 中常用,其定义如下:

换句话说,ReLU 函数对其输入数据中的每个负值返回 0,对其输入数据中的每个正值返回其本身值。这如图 7.12 所示:

图 7.12:ReLU 函数
ReLU 函数可以绘制如图 7.13 所示:

图 7.13:ReLU 函数图
池化层
CNN 中接下来发生的变换在池化层中处理。池化层的目标是在保持原始输入数据的空间方差的同时,减少卷积层输出的特征图维度(但不是深度)。换句话说,通过减小数据的大小,可以减少计算复杂性、内存需求和训练时间,同时克服过拟合,以便在测试数据中检测到训练期间检测到的模式,即使它们的形状有所变化。给定一个特定的窗口大小,有各种池化算法可用,包括以下几种:
-
最大池化:取每个窗口中的最大值
-
平均池化:取每个窗口的平均值
-
求和池化:取每个窗口中值的总和
图 7.14显示了使用 2x2 窗口大小对 4x4 特征图执行最大池化的效果:

图 7.14:使用 2x2 窗口对 4x4 特征图进行最大池化
全连接层
在经过一系列卷积和池化层将 3-D 输入数据转换后,一个全连接层将最后一个卷积和池化层输出的特征图展平成一个长的 1-D 特征向量,然后将其用作一个常规 ANN 的输入数据,在这个 ANN 中,每一层的所有神经元都与前一层的所有神经元相连。
输出层
在这个人工神经网络(ANN)中,输出神经元使用诸如 softmax 函数(如 MLP 分类器中所示)这样的激活函数来分类输出,从而识别和分类输入图像数据中的对象!
案例研究 2 - 图像识别
在这个案例研究中,我们将使用一个预训练的 CNN 来识别和分类它以前从未遇到过的图像中的对象。
通过 TensorFlow 使用 InceptionV3
我们将使用的预训练 CNN 被称为 Inception-v3。这个深度 CNN 是在 ImageNet 图像数据库(一个包含大量标记图像的计算机视觉算法学术基准,覆盖了广泛的名词)上训练的,可以将整个图像分类为日常生活中发现的 1,000 个类别,例如“披萨”、“塑料袋”、“红葡萄酒”、“桌子”、“橙子”和“篮球”,仅举几例。
Inception-v3 深度 CNN 是由 TensorFlow (TM),一个最初在 Google 的 AI 组织内部开发的开源机器学习框架和软件库,用于高性能数值计算,开发和训练的。
要了解更多关于 TensorFlow、Inception-v3 和 ImageNet 的信息,请访问以下链接:
-
ImageNet:
www.image-net.org/ -
TensorFlow:
www.tensorflow.org/ -
Inception-v3:
www.tensorflow.org/tutorials/images/image_recognition
Apache Spark 的深度学习管道
在这个案例研究中,我们将通过一个名为 sparkdl 的第三方 Spark 包来访问 Inception-v3 TensorFlow 深度 CNN。这个 Spark 包是由 Apache Spark 的原始创建者成立的公司 Databricks 开发的,并为 Apache Spark 中的可扩展深度学习提供了高级 API。
要了解更多关于 Databricks 和 sparkdl 的信息,请访问以下链接:
-
Databricks:
databricks.com/
图像库
我们将用于测试预训练的 Inception-v3 深度卷积神经网络(CNN)的图像已从 Open Images v4 数据集中选取,这是一个包含超过 900 万张图片的集合,这些图片是在 Creative Common Attribution 许可下发布的,并且可以在 storage.googleapis.com/openimages/web/index.html 找到。
在本书配套的 GitHub 仓库中,您可以找到 30 张鸟类图像(image-recognition-data/birds)和 30 张飞机图像(image-recognition-data/planes)。图 7.15 显示了您可能在这些测试数据集中找到的一些图像示例:

图 7.15:Open Images v4 数据集的示例图像
在本案例研究中,我们的目标将是将预训练的 Inception-v3 深度 CNN 应用到这些测试图像上,并量化训练好的分类器模型在区分单个测试数据集中鸟类和飞机图像时的准确率。
PySpark 图像识别应用程序
注意,为了本案例研究的目的,我们不会使用 Jupyter notebook 进行开发,而是使用具有 .py 文件扩展名的标准 Python 代码文件。本案例研究提供了一个关于如何开发和执行生产级管道的初步了解;而不是在我们的代码中显式实例化 SparkContext,我们将通过 Linux 命令行将我们的代码及其所有依赖项提交给 spark-submit(包括任何第三方 Spark 包,如 sparkdl)。
现在,让我们看看如何通过 PySpark 使用 Inception-v3 深度 CNN 来对测试图像进行分类。在我们的基于 Python 的图像识别应用程序中,我们执行以下步骤(编号与 Python 代码文件中的编号注释相对应):
以下名为 chp07-02-convolutional-neural-network-transfer-learning.py 的 Python 代码文件,可以在本书配套的 GitHub 仓库中找到。
- 首先,使用以下代码,我们导入所需的 Python 依赖项,包括来自第三方
sparkdl包的相关模块和MLlib内置的LogisticRegression分类器:
from sparkdl import DeepImageFeaturizer
from pyspark.sql.functions import *
from pyspark.sql import SparkSession
from pyspark.ml.image import ImageSchema
from pyspark.ml import Pipeline
from pyspark.ml.classification import LogisticRegression
from pyspark.ml.evaluation import MulticlassClassificationEvaluator
- 与我们的 Jupyter notebook 案例研究不同,我们没有必要实例化一个
SparkContext,因为当我们通过命令行执行 PySpark 应用程序时,这会为我们完成。在本案例研究中,我们将创建一个SparkSession,如下所示,它作为 Spark 执行环境(即使它已经在运行)的入口点,它包含 SQLContext。因此,我们可以使用SparkSession来执行与之前所见相同的类似 SQL 的操作,同时仍然使用 Spark Dataset/DataFrame API:
spark = SparkSession.builder.appName("Convolutional Neural Networks - Transfer Learning - Image Recognition").getOrCreate()
- 截至 2.3 版本,Spark 通过其
MLlibAPI 提供了对图像数据源的原生支持。在此步骤中,我们通过在MLlib的ImageSchema类上调用readImages方法,将我们的鸟类和飞机测试图像从本地文件系统加载到名为birds_df和planes_df的 Spark 数据帧中。然后,我们用0文字标签所有鸟类图像,用1文字标签所有飞机图像,如下所示:
path_to_img_directory = 'chapter07/data/image-recognition-data'
birds_df = ImageSchema.readImages(path_to_img_directory + "/birds")
.withColumn("label", lit(0))
planes_df = ImageSchema.readImages(path_to_img_directory +
"/planes").withColumn("label", lit(1))
- 现在我们已经将测试图像加载到分别以它们的标签区分的单独 Spark 数据框中,我们相应地将它们合并为单个训练和测试数据框。我们通过使用 Spark 数据框 API 的
unionAll方法来实现这一点,该方法简单地将一个数据框附加到另一个数据框上,如下面的代码所示:
planes_train_df, planes_test_df = planes_df
.randomSplit([0.75, 0.25], seed=12345)
birds_train_df, birds_test_df = birds_df
.randomSplit([0.75, 0.25], seed=12345)
train_df = planes_train_df.unionAll(birds_train_df)
test_df = planes_test_df.unionAll(birds_test_df)
- 与之前的案例研究一样,我们需要从我们的输入数据生成特征向量。然而,我们不会从头开始训练一个深度 CNN——即使有分布式技术,这也可能需要好几天——我们将利用预训练的 Inception-v3 深度 CNN。为此,我们将使用称为迁移学习的过程。在这个过程中,解决一个机器学习问题获得的知识被应用于不同但相关的问题。为了在我们的案例研究中使用迁移学习,我们采用第三方
sparkdlSpark 包的DeepImageFeaturizer模块。DeepImageFeaturizer不仅将我们的图像转换为数值特征,还通过剥离预训练神经网络的最后一层来执行快速迁移学习,然后使用所有先前层的输出作为标准分类算法的特征。在我们的案例中,DeepImageFeaturizer将剥离预训练的 Inception-v3 深度 CNN 的最后一层,如下所示:
featurizer = DeepImageFeaturizer(inputCol = "image",
outputCol = "features", modelName = "InceptionV3")
- 现在我们已经通过迁移学习从预训练的 Inception-v3 深度 CNN 的所有先前层中提取了特征,我们将它们输入到分类算法中。在我们的案例中,我们将使用
MLlib的LogisticRegression分类器,如下所示:
logistic_regression = LogisticRegression(maxIter = 20,
regParam = 0.05, elasticNetParam = 0.3, labelCol = "label")
- 要执行迁移学习和逻辑回归模型训练,我们构建一个标准的
pipeline并将该管道拟合到我们的训练数据框中,如下所示:
pipeline = Pipeline(stages = [featurizer, logistic_regression])
model = pipeline.fit(train_df)
- 现在我们已经训练了一个分类模型,使用由 Inception-v3 深度 CNN 推导出的特征,我们将我们的训练逻辑回归模型应用于测试数据框以进行正常预测,如下面的代码所示:
test_predictions_df = model.transform(test_df)
test_predictions_df.select("image.origin", "prediction")
.show(truncate=False)
- 最后,我们使用
MLlib的MulticlassClassificationEvaluator在测试数据框上量化我们模型的准确性,如下所示:
accuracy_evaluator = MulticlassClassificationEvaluator(
metricName = "accuracy")
print("Accuracy on Test Dataset = %g" % accuracy_evaluator
.evaluate(test_predictions_df.select("label", "prediction")))
Spark 提交
我们现在可以运行我们的图像识别应用程序了!由于它是一个 Spark 应用程序,我们可以在 Linux 命令行通过spark-submit来执行它。为此,导航到我们安装 Apache Spark 的目录(见第二章,设置本地开发环境)。然后,我们可以通过传递以下命令行参数来执行spark-submit程序:
-
--master: Spark Master 的 URL。 -
--packages: Spark 应用程序运行所需的第三方库和依赖项。在我们的案例中,我们的图像识别应用程序依赖于sparkdl第三方库的可用性。 -
--py-files:由于我们的图像识别应用程序是一个 PySpark 应用程序,我们将传递应用程序依赖的任何 Python 代码文件的文件系统路径。在我们的情况下,由于我们的图像识别应用程序包含在一个单独的代码文件中,因此没有其他依赖项需要传递给spark-submit。 -
最后一个参数是包含我们的 Spark 驱动程序的 Python 代码文件的路径,即
chp07-02-convolutional-neural-network-transfer-learning.py。
因此,执行的最后命令如下:
> cd {SPARK_HOME}
> bin/spark-submit --master spark://192.168.56.10:7077 --packages databricks:spark-deep-learning:1.2.0-spark2.3-s_2.11 chapter07/chp07-02-convolutional-neural-network-transfer-learning.py
图像识别结果
假设图像识别应用程序运行成功,你应该会在控制台看到以下结果输出:
| Origin | Prediction |
|---|---|
planes/plane-005.jpg |
1.0 |
planes/plane-008.jpg |
1.0 |
planes/plane-009.jpg |
1.0 |
planes/plane-016.jpg |
1.0 |
planes/plane-017.jpg |
0.0 |
planes/plane-018.jpg |
1.0 |
birds/bird-005.jpg |
0.0 |
birds/bird-008.jpg |
0.0 |
birds/bird-009.jpg |
0.0 |
birds/bird-016.jpg |
0.0 |
birds/bird-017.jpg |
0.0 |
birds/bird-018.jpg |
0.0 |
Origin列指的是图像的绝对文件系统路径,Prediction列中的值如果是我们的模型预测图像中的物体是飞机,则为1.0;如果是鸟,则为0.0。当在测试数据集上运行时,我们的模型具有惊人的 92%的准确率。我们的模型唯一的错误是在plane-017.jpg上,如图7.16所示,它被错误地分类为鸟,而实际上它是一架飞机:

图 7.16:plane-017.jpg 的错误分类
如果我们查看图 7.16中的plane-017.jpg,我们可以快速理解模型为什么会犯这个错误。尽管它是一架人造飞机,但它被物理建模成鸟的样子,以提高效率和空气动力学性能。
在这个案例研究中,我们使用预训练的 CNN 对图像进行特征提取。然后,我们将得到的特征传递给标准的逻辑回归算法,以预测给定图像是鸟还是飞机。
案例研究 3 – 图像预测
在案例研究 2(图像识别)中,我们在训练最终的逻辑回归分类器之前,仍然明确地为我们的测试图像进行了标注。在这个案例研究中,我们将简单地发送随机图像到预训练的 Inception-v3 深度 CNN,而不对其进行标注,并让 CNN 本身对图像中包含的物体进行分类。同样,我们将利用第三方sparkdl Spark 包来访问预训练的 Inception-v3 CNN。
我们将使用的随机图像再次从Open Images v4 数据集下载,可以在本书附带的 GitHub 仓库中的image-recognition-data/assorted找到。图 7.17显示了测试数据集中可能找到的一些典型图像:

图 7.17:随机图像组合
PySpark 图像预测应用程序
在我们的基于 Python 的图像预测应用程序中,我们按照以下步骤进行(编号与 Python 代码文件中的注释编号相对应):
以下名为chp07-03-convolutional-neural-network-image-predictor.py的 Python 代码文件,可以在本书附带的 GitHub 存储库中找到。
- 首先,我们像往常一样导入所需的 Python 依赖项,包括来自第三方
sparkdlSpark 包的DeepImagePredictor类,如下所示:
from sparkdl import DeepImagePredictor
from pyspark.sql import SparkSession
from pyspark.ml.image import ImageSchema
- 接下来,我们创建一个
SparkSession,它作为 Spark 执行环境的入口点,如下所示:
spark = SparkSession.builder.appName("Convolutional Neural Networks - Deep Image Predictor").getOrCreate()
- 然后,我们使用我们在上一个案例研究中首次遇到的
ImageSchema类的readImages方法将我们的随机图像组合加载到 Spark 数据框中,如下所示:
assorted_images_df = ImageSchema.readImages(
"chapter07/data/image-recognition-data/assorted")
- 最后,我们将包含我们的随机图像组合的 Spark 数据框传递给
sparkdl的DeepImagePredictor,它将应用指定的预训练神经网络来对图像中的对象进行分类。在我们的案例中,我们将使用预训练的 Inception-v3 深度 CNN。我们还告诉DeepImagePredictor按置信度降序返回每个图像的前 10 个(topK=10)预测分类,如下所示:
deep_image_predictor = DeepImagePredictor(inputCol = "image",
outputCol = "predicted_label", modelName = "InceptionV3",
decodePredictions = True, topK = 10)
predictions_df = deep_image_predictor.transform(assorted_images_df)
predictions_df.select("image.origin", "predicted_label")
.show(truncate = False)
要运行此 PySpark 图像预测应用程序,我们再次通过命令行调用spark-submit,如下所示:
> cd {SPARK_HOME}
> bin/spark-submit --master spark://192.168.56.10:7077 --packages databricks:spark-deep-learning:1.2.0-spark2.3-s_2.11 chapter07/chp07-03-convolutional-neural-network-image-predictor.py
图像预测结果
假设图像预测应用程序运行成功,您应该在控制台看到以下结果输出:
| 原始 | 首次预测标签 |
|---|---|
assorted/snowman.jpg |
泰迪熊 |
assorted/bicycle.jpg |
山地自行车 |
assorted/house.jpg |
图书馆 |
assorted/bus.jpg |
有轨电车 |
assorted/banana.jpg |
香蕉 |
assorted/pizza.jpg |
披萨 |
assorted/toilet.jpg |
马桶座圈 |
assorted/knife.jpg |
大刀 |
assorted/apple.jpg |
红富士(苹果) |
assorted/pen.jpg |
圆珠笔 |
assorted/lion.jpg |
狮子 |
assorted/saxophone.jpg |
萨克斯风 |
assorted/zebra.jpg |
斑马 |
assorted/fork.jpg |
勺子 |
assorted/car.jpg |
敞篷车 |
如您所见,预训练的 Inception-v3 深度 CNN 具有惊人的识别和分类图像中对象的能力。尽管本案例研究中提供的图像相对简单,但 Inception-v3 CNN 在 ImageNet 图像数据库上的前五错误率——即模型未能将其正确答案预测为其前五个猜测之一的情况——仅为 3.46%。请记住,Inception-v3 CNN 试图将整个图像分类到 1,000 个类别中,因此仅 3.46%的前五错误率确实令人印象深刻,并且清楚地展示了卷积神经网络以及一般的人工神经网络在检测和学习模式时的学习能力和力量!
摘要
在本章中,我们亲身体验了激动人心且前沿的深度学习世界。我们开发了能够以惊人的准确率识别和分类图像中的应用程序,并展示了人工神经网络在检测和学习输入数据中的模式方面的真正令人印象深刻的学习能力。
在下一章中,我们将扩展我们的机器学习模型部署,使其超越批量处理,以便从数据中学习并在实时中进行预测!
第八章:使用 Apache Spark 进行实时机器学习
在本章中,我们将扩展我们的机器学习模型部署,使其超越批量处理,以便从数据中学习、做出预测和实时识别趋势!我们将开发并部署一个由以下高级技术组成的实时流处理和机器学习应用程序:
-
Apache Kafka 生产者应用程序
-
Apache Kafka 消费者应用程序
-
Apache Spark 的 Structured Streaming 引擎
-
Apache Spark 的机器学习库,
MLlib
分布式流平台
到目前为止,在这本书中,我们一直在执行批量处理——也就是说,我们被提供了有界原始数据文件,并将这些数据作为一个组进行处理。正如我们在第一章,“大数据生态系统”中看到的,流处理与批量处理的不同之处在于数据是按需或单个数据单元(或流)到达时处理的。我们还在第一章,“大数据生态系统”中看到,Apache Kafka作为一个分布式*流平台,通过以下组件的逻辑流架构,以容错和可靠的方式在系统和应用程序之间移动实时数据:
-
生产者:生成并发送消息的应用程序
-
消费者:订阅并消费消息的应用程序
-
主题:属于特定类别并存储为有序且不可变记录序列的记录流,这些记录在分布式集群中分区和复制
-
流处理器:以特定方式处理消息的应用程序,例如数据转换和机器学习模型
该逻辑流架构的简化示意图如图8.1所示:

图 8.1:Apache Kafka 逻辑流架构
分布式流处理引擎
Apache Kafka 使我们能够在系统和应用程序之间可靠地移动实时数据。但是,我们仍然需要一个某种处理引擎来处理和转换这些实时数据,以便根据特定用例从中提取价值。幸运的是,有多个流处理引擎可供我们使用,包括但不限于以下:
-
Apache Spark:
spark.apache.org/ -
Apache Storm:
storm.apache.org/ -
Apache Flink:
flink.apache.org/ -
Apache Samza:
samza.apache.org/ -
Apache Kafka(通过其 Streams API):
kafka.apache.org/documentation/
尽管对可用的流处理引擎进行详细比较超出了本书的范围,但鼓励读者探索前面的链接并研究可用的不同架构。为了本章的目的,我们将使用 Apache Spark 的 Structured Streaming 引擎作为我们选择的流处理引擎。
使用 Apache Spark 进行流式传输
在撰写本文时,Spark 中提供了两个流处理 API:
-
Spark Streaming (DStreams):
spark.apache.org/docs/latest/streaming-programming-guide.html -
Structured Streaming:
spark.apache.org/docs/latest/structured-streaming-programming-guide.html
Spark Streaming (DStreams)
Spark Streaming (DStreams) 扩展了核心 Spark API,通过将实时数据流划分为 输入批次,然后由 Spark 的核心 API 处理,从而生成最终的 处理批次 流,如图 8.2 所示。一系列 RDD 构成了所谓的 离散流(或 DStream),它代表了数据的连续流:

图 8.2:Spark Streaming (DStreams)
Structured Streaming
另一方面,Structured Streaming 是一个基于 Spark SQL 引擎构建的较新的且高度优化的流处理引擎,其中可以使用 Spark 的 Dataset/DataFrame API 存储和处理流数据(参见第一章,大数据生态系统)。截至 Spark 2.3,Structured Streaming 提供了使用微批处理和连续处理两种方式处理数据流的能力,微批处理的延迟低至 100 毫秒,连续处理的延迟低至 1 毫秒(从而提供真正的实时处理)。Structured Streaming 通过将数据流建模为一个不断追加的无界表来工作。当对这个无界表执行转换或其他类型的查询时,将生成一个结果表,该表代表了那个时刻的数据。
在可配置的触发间隔之后,数据流中的新数据被建模为追加到这个无界表的新行,随后结果表被更新,如图 8.3 所示:

图 8.3:Spark Structured Streaming 逻辑模型
由于流数据通过 Dataset/DataFrame API 暴露,因此可以轻松地在实时数据流上执行 SQL-like 操作(包括聚合和连接)和 RDD 操作(包括 map 和过滤)。此外,结构化流提供了针对迟到数据的处理、流查询的管理和监控以及从故障中恢复的能力。因此,结构化流是一种极其灵活、高效且可靠的流数据处理方式,具有极低的延迟,是我们将在本章剩余部分使用的流处理引擎。
通常建议开发者使用这个较新且高度优化的引擎而不是 Spark Streaming(DStreams)。然而,由于这是一个较新的 API,截至 Spark 2.3.2,可能某些功能尚未提供,这意味着在开发新 API 的同时,DStreams RDD-based 方法仍会偶尔使用。
流处理管道
在本节中,我们将开发一个端到端流处理管道,它能够从生成连续数据的数据源系统中流式传输数据,然后能够将这些流发布到 Apache Kafka 分布式集群。我们的流处理管道将使用 Apache Spark 从 Apache Kafka 中消费数据,使用其结构化流引擎,并将训练好的机器学习模型应用于这些流,以使用MLlib实时提取洞察。我们将开发的端到端流处理管道如图 8.4 所示:

图 8.4:我们的端到端流处理管道
案例研究 – 实时情感分析
在本章的案例研究中,我们将扩展我们在第六章,“使用 Apache Spark 进行自然语言处理”中开发的情感分析模型,使其能够在实时环境中运行。在第六章“使用 Apache Spark 进行自然语言处理”中,我们训练了一个决策树分类器,根据关于航空公司的历史推文训练数据集来预测和分类推文的潜在情感。在本章中,我们将应用这个训练好的决策树分类器来处理实时推文,以便预测它们的情感并识别负面推文,以便航空公司能够尽快采取行动。
因此,我们的端到端流处理管道可以扩展,如图 8.5 所示:

图 8.5:我们的端到端流处理管道,用于实时情感分析
我们用于实时情感分析的流处理管道的核心阶段如下:
-
Kafka 生产者: 我们将开发一个 Python 应用程序,使用我们在第二章,“设置本地开发环境”中安装的
pykafka(一个 Python 的 Apache Kafka 客户端)和tweepy(一个用于访问 Twitter API 的 Python 库)库,以捕获实时发布的关于航空公司的推文,并将这些推文发布到名为twitter的 Apache Kafka 主题。 -
Kafka 消费者: 然后,我们将开发一个 Spark 应用程序,使用其 Structured Streaming API,订阅并从
twitter主题消费推文到 Spark 数据框。 -
流处理器和
MLlib: 然后,我们将使用我们在第六章,“使用 Apache Spark 进行自然语言处理”中研究和开发的相同管道中的特征转换器和特征提取器,对存储在此 Spark 数据框中的推文的原始文本内容进行预处理,这些特征转换器和特征提取器包括分词、去除停用词、词干提取和归一化——在应用 HashingTF 转换器生成实时特征向量之前。 -
训练好的决策树分类器: 接下来,我们将加载我们在第六章,“使用 Apache Spark 进行自然语言处理”中训练的决策树分类器,并将其持久化到我们的单个开发节点的本地文件系统。一旦加载,我们将应用这个训练好的决策树分类器到包含我们从实时推文中提取的预处理特征向量的 Spark 数据框,以预测和分类其潜在的情感。
-
输出目标: 最后,我们将把对实时推文应用的情感分析模型的结果输出到目标目的地,称为输出目标。在我们的案例中,输出目标将是控制台目标,这是 Structured Streaming API 原生提供的内置输出目标之一。通过使用此目标,每次触发时输出都会打印到控制台/标准输出(stdout)。从此控制台,我们将能够读取原始推文的原始文本内容和来自我们模型的预测情感分类,即负面或非负面。要了解更多关于可用的各种输出目标,请访问
spark.apache.org/docs/latest/structured-streaming-programming-guide.html#output-sinks。
以下小节将描述我们将遵循的技术步骤来开发、部署和运行我们的端到端流处理管道,以进行实时情感分析。
注意,对于本案例研究的目的,我们不会使用 Jupyter 笔记本进行开发。这是因为需要为单独的组件编写单独的代码文件,如前所述。因此,本案例研究提供了另一个了解如何开发和执行生产级管道的视角。我们不会在笔记本中显式实例化SparkContext,而是将通过 Linux 命令行将我们的 Python 代码文件及其所有依赖项提交给spark-submit。
启动 Zookeeper 和 Kafka 服务器
第一步是确保我们的单节点 Kafka 集群正在运行。如第二章中所述,“设置本地开发环境”,请执行以下命令以启动 Apache Kafka:
> cd {KAFKA_HOME}
> bin/zookeeper-server-start.sh -daemon config/zookeeper.properties
> bin/kafka-server-start.sh -daemon config/server.properties
Kafka 主题
接下来,我们需要创建一个 Kafka 主题,我们的 Python Kafka 生产者应用程序(我们将在稍后开发)将发布有关航空公司的实时推文。在我们的案例中,我们将该主题称为twitter。如第二章中所示,“设置本地开发环境”,可以通过以下方式实现:
> bin/kafka-topics.sh --create --zookeeper 192.168.56.10:2181 --replication-factor 1 --partitions 1 --topic twitter
Twitter 开发者账户
为了让我们的 Python Kafka 生产者应用程序实时捕获推文,我们需要访问 Twitter API。截至 2018 年 7 月,除了普通 Twitter 账户外,还必须创建并批准一个 Twitter 开发者账户,才能访问其 API。为了申请开发者账户,请访问apps.twitter.com/,点击“申请开发者账户”按钮,并填写所需详细信息。
Twitter 应用程序和 Twitter API
一旦您创建了 Twitter 开发者账户,为了使用 Twitter API,必须创建一个 Twitter 应用程序。Twitter 应用程序根据您打算创建的应用程序的具体目的,提供认证和授权访问 Twitter API。为了创建用于我们实时情感分析模型的 Twitter 应用程序,请按照以下说明(截至撰写时有效)进行操作:
-
点击“创建应用程序”按钮。
-
提供以下必填的应用程序详细信息:
- App Name (max 32 characters) e.g. "Airline Sentiment Analysis"
- Application Description (max 200 characters) e.g. "This App will collect tweets about airlines and apply our previously trained decision tree classifier to predict and classify the underlying sentiment of those tweets in real-time"
- Website URL (for attribution purposes only - if you do not have a personal website, then use the URL to your Twitter page, such as https://twitter.com/PacktPub)
- Tell us how this app will be used (min 100 characters) e.g. "Internal training and development purposes only, including the deployment of machine learning models in real-time. It will not be visible to customers or 3rd parties."
-
点击“创建”按钮创建您的 Twitter 应用程序。
-
一旦您的 Twitter 应用程序创建完成,导航到“密钥和令牌”选项卡。
-
分别记下您的消费者 API 密钥和消费者 API 密钥字符串。
-
然后点击“访问令牌 & 访问令牌密钥”下的“创建”按钮,为您的 Twitter 应用程序生成访问令牌。将访问级别设置为只读,因为此 Twitter 应用程序将只读取推文,不会生成任何自己的内容。
-
记下生成的 访问令牌和访问令牌密钥字符串。
消费者 API 密钥和访问令牌将被用于授权我们的基于 Python 的 Kafka 生产者应用程序以只读方式访问通过 Twitter API 获取的实时推文流,因此您需要将它们记录下来。
应用程序配置
现在我们已经准备好开始开发我们的端到端流处理管道!首先,让我们创建一个 Python 配置文件,该文件将存储与我们的管道和本地开发节点相关的所有环境和应用程序级别的选项,如下所示:
以下 Python 配置文件,称为config.py,可以在伴随本书的 GitHub 存储库中找到。
#!/usr/bin/python
""" config.py: Environmental and Application Settings """
""" ENVIRONMENT SETTINGS """
# Apache Kafka
bootstrap_servers = '192.168.56.10:9092'
data_encoding = 'utf-8'
""" TWITTER APP SETTINGS """
consumer_api_key = 'Enter your Twitter App Consumer API Key here'
consumer_api_secret = 'Enter your Twitter App Consumer API Secret Key here'
access_token = 'Enter your Twitter App Access Token here'
access_token_secret = 'Enter your Twitter App Access Token Secret here'
""" SENTIMENT ANALYSIS MODEL SETTINGS """
# Name of an existing Kafka Topic to publish tweets to
twitter_kafka_topic_name = 'twitter'
# Keywords, Twitter Handle or Hashtag used to filter the Twitter Stream
twitter_stream_filter = '@British_Airways'
# Filesystem Path to the Trained Decision Tree Classifier
trained_classification_model_path = '..chapter06/models/airline-sentiment-analysis-decision-tree-classifier'
此 Python 配置文件定义了以下相关选项:
-
bootstrap_servers:Kafka 代理的主机名/IP 地址和端口号配对的逗号分隔列表。在我们的案例中,这是默认情况下位于端口9092的单节点开发环境的主机名/IP 地址。 -
consumer_api_key:在此处输入与您的 Twitter 应用程序关联的消费者 API 密钥。 -
consumer_api_secret:在此处输入与您的 Twitter 应用程序关联的消费者 API 密钥。 -
access_token:在此处输入与您的 Twitter 应用程序关联的访问令牌。 -
access_token_secret:在此处输入与您的 Twitter 应用程序关联的访问令牌密钥。 -
twitter_kafka_topic_name:我们的 Kafka 生产者将发布的 Kafka 主题名称,以及我们的结构化流 Spark 应用程序将从中消费推文的主题。 -
twitter_stream_filter:一个关键字、Twitter 用户名或标签,用于过滤从 Twitter API 捕获的实时推文流。在我们的案例中,我们正在过滤针对@British_Airways的实时推文。 -
trained_classification_model_path:我们保存我们的训练决策树分类器(在第六章中介绍,使用 Apache Spark 进行自然语言处理)的绝对路径。
Kafka Twitter 生产者应用程序
现在我们已经准备好开发我们的基于 Python 的 Kafka 生产者应用程序,该程序将捕获有关航空公司实时推文的推文,并将这些推文发布到我们之前创建的 Apache Kafka twitter 主题。在开发我们的 Kafka 生产者时,我们将使用以下两个 Python 库:
-
tweepy:这个库允许我们使用 Python 和之前生成的消费者 API 密钥和访问令牌以编程方式访问 Twitter API。 -
pykafka:这个库允许我们实例化一个基于 Python 的 Apache Kafka 客户端,通过它可以与我们的单节点 Kafka 集群进行通信和交易。
以下 Python 代码文件,称为kafka_twitter_producer.py,可以在伴随本书的 GitHub 存储库中找到。
关于我们的基于 Python 的 Kafka 生产者应用程序,我们执行以下步骤(编号与 Python 代码文件中的编号注释相对应):
- 首先,我们分别从
tweepy和pykafka库中导入所需的模块,如下面的代码所示。我们还导入了我们之前创建的config.py文件中的配置:
import config
import tweepy
from tweepy import OAuthHandler
from tweepy import Stream
from tweepy.streaming import StreamListener
import pykafka
- 接下来,我们使用
config.py中定义的消费者 API 密钥和访问令牌实例化一个tweepyTwitter API 包装器,如下所示,以提供我们认证和授权的程序访问 Twitter API:
auth = OAuthHandler(config.consumer_api_key,
config.consumer_api_secret)
auth.set_access_token(config.access_token,
config.access_token_secret)
api = tweepy.API(auth)
- 然后,我们在 Python 中定义了一个名为
KafkaTwitterProducer的类,一旦实例化,它就为我们提供了一个pykafka客户端到我们的单节点 Apache Kafka 集群,如下面的代码所示。当这个类被实例化时,它最初执行__init__函数中定义的代码,使用引导服务器创建一个pykafka客户端,这些服务器的位置可以在config.py中找到。然后它创建了一个与config.py中定义的twitter_kafka_topic_nameKafka 主题关联的生产者。当我们的pykafka生产者捕获数据时,会调用on_data函数,该函数将数据物理发布到 Kafka 主题。
如果我们的 pykafka 生产者遇到错误,则调用 on_error 函数,在我们的情况下,它只是将错误打印到控制台并继续处理下一个消息:
class KafkaTwitterProducer(StreamListener):
def __init__(self):
self.client = pykafka.KafkaClient(config.bootstrap_servers)
self.producer = self.client.topics[bytes(
config.twitter_kafka_topic_name,
config.data_encoding)].get_producer()
def on_data(self, data):
self.producer.produce(bytes(data, config.data_encoding))
return True
def on_error(self, status):
print(status)
return True
- 接下来,我们使用
tweepy库的Stream模块实例化一个 Twitter 流。为此,我们只需将我们的 Twitter 应用程序认证详情和KafkaTwitterProducer类的实例传递给Stream模块:
print("Instantiating a Twitter Stream and publishing to the '%s'
Kafka Topic..." % config.twitter_kafka_topic_name)
twitter_stream = Stream(auth, KafkaTwitterProducer())
- 现在我们已经实例化了一个 Twitter 流,最后一步是根据
config.py中的twitter_stream_filter选项过滤流,以传递感兴趣的推文,如下面的代码所示:
print("Filtering the Twitter Stream based on the query '%s'..." %
config.twitter_stream_filter)
twitter_stream.filter(track=[config.twitter_stream_filter])
我们现在可以运行我们的 Kafka 生产者应用程序了!由于它是一个 Python 应用程序,运行它的最简单方法就是使用 Linux 命令行,导航到包含 kafka_twitter_producer.py 的目录,并按以下方式执行:
> python kafka_twitter_producer.py
$ Instantiating a Twitter Stream and publishing to the 'twitter'
Kafka Topic...
$ Filtering the Twitter Stream based on the query
'@British_Airways'...
为了验证它实际上正在捕获并将实时推文发布到 Kafka,如第二章所述,设置本地开发环境,你可以启动一个命令行消费者应用程序来从 Twitter 主题中消费消息并将它们打印到控制台,如下所示:
> cd {KAFKA_HOME}
> bin/kafka-console-consumer.sh --bootstrap-server 192.168.56.10:9092 --topic twitter
希望你能看到实时打印到控制台上的推文。在我们的例子中,这些推文都是指向 "@British_Airways" 的。
推文本身是通过 Twitter API 以 JSON 格式捕获的,不仅包含推文的原始文本内容,还包含相关的元数据,如推文 ID、推文者的用户名、时间戳等。有关 JSON 模式的完整描述,请访问 developer.twitter.com/en/docs/tweets/data-dictionary/overview/tweet-object.html。
预处理和特征向量化流程
如前所述,为了能够将我们训练好的决策树分类器应用于这些实时推文,我们首先需要像在第六章《使用 Apache Spark 的自然语言处理》中处理我们的训练和测试数据集那样对它们进行预处理和向量化。然而,我们不会在 Kafka 消费者应用程序本身中重复预处理和向量化流程的逻辑,而是将在一个独立的 Python 模块和 Python 函数中定义我们的流程逻辑。这样,每次我们需要像在第六章《使用 Apache Spark 的自然语言处理》中那样预处理文本时,我们只需调用相关的 Python 函数,从而避免在不同 Python 代码文件中重复相同的代码。
以下名为model_pipelines.py的 Python 代码文件,可以在本书配套的 GitHub 仓库中找到。
在以下 Python 模块中,我们定义了两个函数。第一个函数应用了我们在第六章《使用 Apache Spark 的自然语言处理》中学习的MLlib和spark-nlp特征转换器的相同流程,以预处理推文的原始文本内容。第二个函数随后对预处理后的 Spark 数据框应用HashingTF转换器,以根据词频生成特征向量,正如我们在第六章《使用 Apache Spark 的自然语言处理》中所学习的那样。结果是包含原始推文文本的 Spark 数据框,该文本位于名为text的列中,以及位于名为features的列中的词频特征向量:
#!/usr/bin/python
""" model_pipelines.py: Pre-Processing and Feature Vectorization Spark Pipeline function definitions """
from pyspark.sql.functions import *
from pyspark.ml.feature import Tokenizer
from pyspark.ml.feature import StopWordsRemover
from pyspark.ml.feature import HashingTF
from pyspark.ml import Pipeline, PipelineModel
from sparknlp.base import *
from sparknlp.annotator import Tokenizer as NLPTokenizer
from sparknlp.annotator import Stemmer, Normalizer
def preprocessing_pipeline(raw_corpus_df):
# Native MLlib Feature Transformers
filtered_df = raw_corpus_df.filter("text is not null")
tokenizer = Tokenizer(inputCol = "text", outputCol = "tokens_1")
tokenized_df = tokenizer.transform(filtered_df)
remover = StopWordsRemover(inputCol = "tokens_1",
outputCol = "filtered_tokens")
preprocessed_part_1_df = remover.transform(tokenized_df)
preprocessed_part_1_df = preprocessed_part_1_df
.withColumn("concatenated_filtered_tokens", concat_ws(" ",
col("filtered_tokens")))
# spark-nlp Feature Transformers
document_assembler = DocumentAssembler()
.setInputCol("concatenated_filtered_tokens")
tokenizer = NLPTokenizer()
.setInputCols(["document"]).setOutputCol("tokens_2")
stemmer =
Stemmer().setInputCols(["tokens_2"]).setOutputCol("stems")
normalizer = Normalizer().setInputCols(["stems"])
.setOutputCol("normalised_stems")
preprocessing_pipeline = Pipeline(stages = [document_assembler,
tokenizer, stemmer, normalizer])
preprocessing_pipeline_model = preprocessing_pipeline
.fit(preprocessed_part_1_df)
preprocessed_df = preprocessing_pipeline_model
.transform(preprocessed_part_1_df)
preprocessed_df.select("id", "text", "normalised_stems")
# Explode and Aggregate
exploded_df = preprocessed_df
.withColumn("stems", explode("normalised_stems"))
.withColumn("stems", col("stems").getItem("result"))
.select("id", "text", "stems")
aggregated_df = exploded_df.groupBy("id")
.agg(concat_ws(" ", collect_list(col("stems"))), first("text"))
.toDF("id", "tokens", "text")
.withColumn("tokens", split(col("tokens"), " ")
.cast("array<string>"))
# Return the final processed DataFrame
return aggregated_df
def vectorizer_pipeline(preprocessed_df):
hashingTF = HashingTF(inputCol = "tokens", outputCol = "features",
numFeatures = 280)
features_df = hashingTF.transform(preprocessed_df)
# Return the final vectorized DataFrame
return features_df
Kafka Twitter 消费者应用程序
我们最终准备好使用 Spark Structured Streaming 引擎开发我们的 Kafka 消费者应用程序,以便将我们的训练好的决策树分类器应用于实时推文流,以提供实时情感分析!
以下名为kafka_twitter_consumer.py的 Python 代码文件,可以在本书配套的 GitHub 仓库中找到。
关于我们的基于 Spark Structured-Streaming 的 Kafka 消费者应用程序,我们执行以下步骤(编号与 Python 代码文件中的注释编号相对应):
- 首先,我们从
config.py文件中导入配置。我们还导入了我们之前创建的包含预处理和向量化流程逻辑的 Python 函数,如下所示:
import config
import model_pipelines
- 与我们的 Jupyter 笔记本案例研究不同,没有必要显式实例化一个
SparkContext,因为这将在我们通过命令行中的spark-submit执行 Kafka 消费者应用程序时为我们完成。在本案例研究中,我们创建了一个SparkSession,如下面的代码所示,它作为 Spark 执行环境的入口点——即使它已经在运行——并且它包含了SQLContext。因此,我们可以使用SparkSession来执行与之前看到的相同类型的 SQL 操作,同时仍然使用 Spark Dataset/DataFrame API:
spark = SparkSession.builder.appName("Stream Processing - Real-Time Sentiment Analysis").getOrCreate()
- 在这一步,我们将我们在第六章自然语言处理使用 Apache Spark 中训练的决策树分类器(使用了 HashingTF 特征提取器)从本地文件系统加载到一个
DecisionTreeClassificationModel对象中,以便我们可以在以后应用它,如下面的代码所示。注意,训练好的决策树分类器的绝对路径已在config.py中定义:
decision_tree_model = DecisionTreeClassificationModel.load(
config.trained_classification_model_path)
- 我们几乎准备好开始从我们的单节点 Kafka 集群中消费消息了。然而,在这样做之前,我们必须注意 Spark 还不支持自动推断和解析 JSON 键值到 Spark 数据框列。因此,我们必须明确定义 JSON 架构,或者我们希望保留的 JSON 架构的子集,如下所示:
schema = StructType([
StructField("created_at", StringType()),
StructField("id", StringType()),
StructField("id_str", StringType()),
StructField("text", StringType()),
StructField("retweet_count", StringType()),
StructField("favorite_count", StringType()),
StructField("favorited", StringType()),
StructField("retweeted", StringType()),
StructField("lang", StringType()),
StructField("location", StringType())
])
- 现在我们已经定义了我们的 JSON 架构,我们准备开始消费消息。为此,我们在我们的
SparkSession实例上调用readStream方法来消费流数据。我们指定流数据的来源将是一个 Kafka 集群,使用format方法,之后我们定义 Kafka 启动服务器和我们要订阅的 Kafka 主题的名称,这两个都在config.py中定义过。最后,我们调用load方法将twitter主题中消费的最新消息流式传输到一个无界的 Spark 数据框tweets_df,如下面的代码所示:
tweets_df = spark.readStream.format("kafka")
.option("kafka.bootstrap.servers", config.bootstrap_servers)
.option("subscribe", config.twitter_kafka_topic_name).load()
- 存储在 Kafka 主题中的记录以二进制格式持久化。为了处理代表我们的推文的 JSON,这些推文存储在名为
value的 Kafka 记录字段下,我们必须首先将value的内容CAST为字符串。然后我们将定义的架构应用于这个 JSON 字符串并提取感兴趣的字段,如下面的代码所示。在我们的例子中,我们只对存储在名为id的 JSON 键中的推文 ID 和存储在名为text的 JSON 键中的原始文本内容感兴趣。因此,生成的 Spark 数据框将有两个字符串列,id和text,包含这些感兴趣的相应字段:
tweets_df = tweets_df.selectExpr(
"CAST(key AS STRING)", "CAST(value AS STRING) as json")
.withColumn("tweet", from_json(col("json"), schema=schema))
.selectExpr("tweet.id_str as id", "tweet.text as text")
- 现在我们已经从我们的 Kafka 主题中消费了原始推文并将它们解析为 Spark 数据框,我们可以像在第六章中做的那样应用我们的预处理管道,使用 Apache Spark 进行自然语言处理。然而,我们不是将第六章中使用 Apache Spark 进行自然语言处理的相同代码复制到我们的 Kafka 消费者应用程序中,而是简单地调用我们在
model_pipelines.py中定义的相关函数,即preprocessing_pipeline(),如下面的代码所示。这个预处理管道将原始文本分词,去除停用词,应用词干提取算法,并规范化生成的标记:
preprocessed_df = model_pipelines.preprocessing_pipeline(tweets_df)
- 接下来,我们生成这些标记的特征向量,就像在第六章中使用 Apache Spark 进行自然语言处理所做的那样。我们调用
model_pipelines.py中的vectorizer_pipeline()函数来根据词频生成特征向量,如下面的代码所示。生成的 Spark 数据框,称为features_df,包含三个相关列,即id(原始推文 ID)、text(原始推文文本)和features(词频特征向量):
features_df = model_pipelines.vectorizer_pipeline(preprocessed_df)
- 现在我们已经从我们的推文流中生成了特征向量,我们可以将我们的训练好的决策树分类器应用到这个流中,以便预测和分类其潜在的 sentiment。我们像平常一样这样做,通过在
features_df数据框上调用transform方法,结果生成一个新的 Spark 数据框,称为predictions_df,包含与之前相同的id和text列,以及一个新的名为prediction的列,其中包含我们的预测分类,如下面的代码所示。正如在第六章中使用 Apache Spark 进行自然语言处理所描述的,预测值为1表示非负 sentiment,预测值为0表示负 sentiment:
predictions_df = decision_tree_model.transform(features_df)
.select("id", "text", "prediction")
- 最后,我们将我们的预测结果数据框写入输出接收器。在我们的例子中,我们将输出接收器定义为简单地是用于执行 Kafka 消费者 PySpark 应用程序的控制台——即执行
spark-submit的控制台。我们通过在相关的 Spark 数据框上调用writeStream方法并指定console作为选择的format来实现这一点。我们通过调用start方法启动输出流,并调用awaitTermination方法,这告诉 Spark 无限期地继续处理我们的流处理管道,直到它被明确中断并停止,如下所示:
query = predictions_df.writeStream
.outputMode("complete")
.format("console")
.option("truncate", "false")
.start()
query.awaitTermination()
注意,outputMode方法定义了写入输出接收器的内容,可以取以下选项之一:
-
complete:将整个(更新后的)结果表写入输出接收器 -
append:仅写入自上次触发以来添加到结果表的新行到输出接收器 -
update: 只有自上次触发以来在结果表中更新的行会被写入输出接收器
我们现在可以运行我们的 Kafka 消费者应用程序了!由于它是一个 Spark 应用程序,我们可以在 Linux 命令行上通过 spark-submit 来执行它。为此,导航到我们安装 Apache Spark 的目录(见第二章,设置本地开发环境)。然后我们可以通过传递以下命令行参数来执行 spark-submit 程序:
-
--master: Spark 主机 URL。 -
--packages: 给定 Spark 应用程序运行所需的第三方库和依赖项。在我们的例子中,我们的 Kafka 消费者应用程序依赖于两个第三方库的可用性,即spark-sql-kafka(Spark Kafka 集成)和spark-nlp(自然语言处理算法,如第六章所述,使用 Apache Spark 进行自然语言处理)。 -
--py-files: 由于我们的 Kafka 消费者是一个 PySpark 应用程序,我们可以使用这个参数来传递一个以逗号分隔的文件系统路径列表,这些路径包含我们的应用程序所依赖的任何 Python 代码文件。在我们的例子中,我们的 Kafka 消费者应用程序分别依赖于config.py和model_pipelines.py。 -
最后一个参数是我们 Spark Structured Streaming 驱动程序的 Python 代码文件的路径,在我们的例子中,是
kafka_twitter_consumer.py
因此,最终的命令如下所示:
> cd {SPARK_HOME}
> bin/spark-submit --master spark://192.168.56.10:7077 --packages org.apache.spark:spark-sql-kafka-0-10_2.11:2.3.2,JohnSnowLabs:spark-nlp:1.7.0 --py-files chapter08/config.py,chapter08/model_pipelines.py chapter08/kafka_twitter_consumer.py
假设基于 Python 的 Kafka 生产者应用程序也在运行,结果表应该定期写入控制台,并包含来自全球 Twitter API 消费并由真实 Twitter 用户编写的航班推文流背后的实时预测和分类!以下表格显示了在过滤使用 "@British_Airways" 时处理的真实世界分类推文选择:
| 推文原始内容 | 预测情感 |
|---|---|
| @British_Airways @HeathrowAirport 我习惯了在设得兰的寒冷,但这是完全不同的一种寒冷!! | 非负面 |
| @British_Airways 刚刚使用该应用程序为我们的航班 bari 到 lgw 办理登机手续,但应用程序显示没有行李托运 | 负面 |
| 她在夜晚看起来更美 | A380 在伦敦希思罗机场起飞 @HeathrowAviYT @HeathrowAirport @British_Airways | 非负面 |
| The @British_Airways #B747 landing into @HeathrowAirport | 非负面 |
| @British_Airways 正在尝试为明天的航班在线登机,但收到一条消息“很抱歉,我们无法为此次航班提供在线登机服务”。有什么想法吗?? | 负面 |
摘要
在本章中,我们开发了 Apache Kafka 的生产者和消费者应用程序,并利用 Spark 的 Structured Streaming 引擎处理从 Kafka 主题中消费的流数据。在我们的实际案例研究中,我们设计、开发和部署了一个端到端流处理管道,该管道能够消费全球各地发布的真实推文,并使用机器学习对这些推文背后的情感进行分类,所有这些都是在实时完成的。
在这本书中,我们经历了一次理论与实践相结合的旅程,探索了支撑当今行业数据智能革命的一些最重要和最激动人心的技术和框架。我们首先描述了一种新型分布式和可扩展的技术,这些技术使我们能够存储、处理和分析大量结构化、半结构化和非结构化数据。以这些技术为基础,我们建立了人工智能的背景及其与机器学习和深度学习的关系。然后,我们继续探讨了机器学习的关键概念及其应用,包括监督学习、无监督学习、自然语言处理和深度学习。我们通过大量相关且令人兴奋的用例来阐述这些关键概念,这些用例都是使用我们选择的 Apache Spark 大数据处理引擎实现的。最后,鉴于及时决策对许多现代企业和组织至关重要,我们将机器学习模型的部署从批量处理扩展到了实时流应用!


浙公网安备 33010602011771号