Spark2-数据处理和实时分析-全-

Spark2 数据处理和实时分析(全)

原文:zh.annas-archive.org/md5/16D84784AD68D8BF20A18AC23C62DD82

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Apache Spark 是一个基于内存的集群数据处理系统,提供广泛的功能,如大数据处理、分析、机器学习等。通过这个学习路径,您可以将 Apache Spark 的知识提升到一个新的水平,学习如何扩展 Spark 的功能,并在此平台上构建自己的数据流和机器学习程序。您将使用 Apache Spark 的不同模块,如使用 Spark SQL 进行交互式查询、使用 DataFrames 和数据集、使用 Spark Streaming 实现流分析,以及使用 MLlib 和各种外部工具在 Spark 上应用机器学习和深度学习技术。通过这个精心设计的学习...

本书面向的读者

如果您是一名中级 Spark 开发者,希望掌握 Apache Spark 2.x 的高级功能和用例,这个学习路径非常适合您。希望学习如何集成和使用 Apache Spark 功能并构建强大大数据管道的大数据专业人士也会发现这个学习路径很有用。要理解本学习路径中解释的概念,您必须了解 Apache Spark 和 Scala 的基础知识。

本书内容

第一章Apache Spark V2 初体验及新特性,概述了 Apache Spark,介绍了其模块内的功能,以及如何进行扩展。它涵盖了 Apache Spark 标准模块之外的生态系统中可用的处理和存储工具。还提供了性能调优的技巧。

第二章Apache Spark 流处理,讲述了使用 Apache Spark Streaming 的连续应用程序。您将学习如何增量处理数据并创建可行的见解。

第三章结构化流处理,讲述了使用 DataFrame 和 Dataset API 定义连续应用程序的新方式——结构化流处理。

第四章Apache Spark MLlib,介绍了...

充分利用本书

操作系统: 首选 Linux 发行版(包括 Debian、Ubuntu、Fedora、RHEL 和 CentOS),具体来说,推荐使用完整的 Ubuntu 14.04(LTS)64 位(或更高版本)安装,VMware player 12 或 VirtualBox。您也可以在 Windows(XP/7/8/10)或 Mac OS X(10.4.7+)上运行 Spark 作业。

硬件配置: 处理器建议使用 Core i3、Core i5(推荐)或 Core i7(以获得最佳效果)。然而,多核处理将提供更快的数据处理和可扩展性。对于独立模式,您至少需要 8-16 GB RAM(推荐),对于单个虚拟机至少需要 32 GB RAM——集群模式则需要更多。您还需要足够的存储空间来运行繁重的作业(取决于您将处理的数据集大小),并且最好至少有 50 GB 的可用磁盘存储空间(对于独立模式和 SQL 仓库)。

此外,您还需要以下内容:

  • VirtualBox 5.1.22 或更高版本

  • Hortonworks HDP Sandbox V2.6 或更高版本

  • Eclipse Neon 或更高版本

  • Eclipse Scala 插件

  • Eclipse Git 插件

  • Spark 2.0.0(或更高版本)

  • Hadoop 2.7(或更高版本)

  • Java(JDK 和 JRE)1.7+/1.8+

  • Scala 2.11.x(或更高版本)

  • Python 2.7+/3.4+

  • R 3.1+ 和 RStudio 1.0.143(或更高版本)

  • Maven Eclipse 插件(2.9 或更高版本)

  • Maven 编译器插件 for Eclipse(2.3.2 或更高版本)

  • Maven 装配插件 for Eclipse(2.4.1 或更高版本)

  • Oracle JDK SE 1.8.x

  • JetBrain IntelliJ 社区版 2016.2.X 或更高版本

  • IntelliJ 的 Scala 插件 2016.2.x

  • Jfreechart 1.0.19

  • breeze-core 0.12

  • Cloud9 1.5.0 JAR

  • Bliki-core 3.0.19

  • hadoop-streaming 2.2.0

  • Jcommon 1.0.23

  • Lucene-analyzers-common 6.0.0

  • Lucene-core-6.0.0

  • Spark-streaming-flume-assembly 2.0.0

  • Spark-streaming-kafka-assembly 2.0.0

下载示例代码文件

您可以从www.packt.com的账户下载本书的示例代码文件。如果您在其他地方购买了本书,可以访问www.packt.com/support并注册,以便将文件直接发送到您的邮箱。

您可以按照以下步骤下载代码文件:

  1. 登录或注册于www.packt.com

  2. 选择支持选项卡。

  3. 点击代码下载与勘误。

  4. 在搜索框中输入书名,并按照屏幕上的指示操作。

下载文件后,请确保使用最新版本的以下软件解压缩或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

本书代码包也托管在 GitHub 上。

使用的约定

本书中,您会发现多种文本样式用于区分不同类型的信息。以下是这些样式的示例及其含义的解释。

文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:“接下来的代码行读取链接并将其分配给BeautifulSoup函数。”

代码块设置如下:

import org.apache.spark.SparkContext
import org.apache.spark.SparkContext._
import org.apache.spark.SparkConf

任何命令行输入或输出如下所示:

$./bin/spark-submit --class com.chapter11.RandomForestDemo \
--master spark://ip-172-31-21-153.us-west-2.compute:7077 \
--executor-memory 2G \
--total-executor-cores 2 \
file:///home/KMeans-0.0.1-SNAPSHOT.jar \
file:///home/mnist.bz2

粗体:新术语和重要词汇以粗体显示。屏幕上看到的词汇,例如在菜单或对话框中,在文本中这样显示:“配置全局库。选择 Scala SDK 作为您的全局库。”

警告或重要提示以这种方式出现。

提示和技巧以这种方式出现。

第一章:初探 Apache Spark V2 的新特性

Apache Spark是一个分布式且高度可扩展的内存数据分析系统,为你提供了使用 Java、Scala、Python 以及 R 等语言开发应用程序的能力。它是当前 Apache 顶级项目中贡献/参与度最高的项目之一。Apache 系统,如 Mahout,现在将其作为处理引擎,而非 MapReduce。还可以使用 Hive 上下文让 Spark 应用程序直接处理 Apache Hive 中的数据。

最初,Apache Spark 提供了四个主要子模块——SQL、MLlib、GraphX 和 Streaming。它们将在各自的章节中进行解释,但在此之前,一个简单的概述将是有益的。...

Spark 机器学习

机器学习是 Apache Spark 的真正原因,因为归根结底,你不仅仅希望将数据从 A 地运送到 B 地(这一过程称为ETL提取、转换、加载))。你希望在你的数据上运行高级数据分析算法,并且希望这些算法能够扩展。这正是 Apache Spark 发挥作用的地方。

Apache Spark 的核心提供了大规模并行数据处理的运行时环境,不同的并行机器学习库在其上运行。这是因为流行的编程语言如 R 和 Python 有大量机器学习算法,但它们不具备可扩展性。一旦你向系统可用主内存加载更多数据,它们就会崩溃。

相比之下,Apache Spark 可以利用多个计算机节点形成集群,并且即使在单个节点上,也能透明地将数据溢出到磁盘,从而避免主内存瓶颈。Apache Spark 自带了两个有趣的机器学习库,但本工作还将涵盖第三方机器学习库。

Spark MLlib 模块,即经典 MLlib,提供了一个不断增长但尚不完整的机器学习算法列表。自从基于DataFrame的机器学习 API——SparkML推出以来,MLlib 的命运已定。它仅因向后兼容的原因而被保留。

在 SparkML 中,我们已有一个机器学习库,该库开箱即用,可利用这些改进作为底层架构。

SparkML 最终将取代 MLlib。Apache SystemML 推出了首个运行在 Apache Spark 之上的库,该库并非随 Apache Spark 发行版一同提供。SystemML 为你提供了一个具有内置成本优化器的 R 风格语法执行环境。大规模并行机器学习是一个不断变化的高频领域。很难预测这一旅程将走向何方,但这是首次,使用开源和云计算的每个人都能获得大规模的高级机器学习。

Apache Spark 上的深度学习使用H2ODeeplearning4jApache SystemML,这些都是非常有趣的第三方机器学习库的例子,它们并未随 Apache Spark 分发。

尽管 H2O 在某种程度上与 MLlib 互补,但 Deeplearning4j 仅专注于深度学习算法。两者都使用 Apache Spark 作为数据处理并行化的手段。您可能会好奇为什么我们要研究不同的机器学习库。

实际上,每个库在实现不同算法时都有其优缺点。因此,通常取决于您的数据和数据集大小,您会选择哪种实现以获得最佳性能。

然而,令人高兴的是,使用 Apache Spark 时有如此多的选择,您不会被锁定在一个单一的库中。开源意味着开放性,这只是我们如何从与单一供应商、单一产品锁定相反的方法中受益的一个例子。尽管最近 Apache Spark 将另一个库 GraphX 集成到其分发中,但我们不期望这种情况会很快发生。因此,最有可能的是,Apache Spark 作为一个中央数据处理平台和额外的第三方库将共存,就像 Apache Spark 是大数据操作系统,而第三方库是您在其上安装和运行的软件一样。

Spark Streaming

流处理是 Apache Spark 的另一个重大且流行的话题。它涉及在 Spark 中以流的形式处理数据,并涵盖了输入和输出操作、转换、持久性和检查点等主题。

Apache Spark Streaming 将涵盖处理领域,我们还将看到不同类型流处理的实际示例。这讨论了批处理和窗口流配置,并提供了一个检查点设置的实际示例。它还涵盖了包括 Kafka 和 Flume 在内的不同流处理示例。

流数据有许多用途。其他 Spark 模块功能(例如,SQL、MLlib 和 GraphX)可用于处理流。您...

Spark SQL

从 Spark 版本 1.3 开始,Apache Spark 引入了数据帧,使得 Spark 数据可以以表格形式处理,并可以使用表格函数(如selectfiltergroupBy)来处理数据。Spark SQL 模块与 Parquet 和 JSON 格式集成,允许数据以更好地表示数据的格式存储。这也提供了更多与外部系统集成的选项。

将 Apache Spark 集成到 Hadoop Hive 大数据数据库的想法也可以引入。基于 Hive 上下文的 Spark 应用程序可用于操作基于 Hive 的表数据。这使得 Hive 能够利用 Spark 的快速内存分布式处理能力,有效地让 Hive 使用 Spark 作为处理引擎。

此外,还有大量额外的连接器,可以直接从 Apache Spark 访问 Hadoop 生态系统之外的 NoSQL 数据库。

Spark 图处理

图处理是数据分析中另一个非常重要的主题。事实上,大多数问题都可以表示为图。

基本上是一个项目及其相互关系的网络。项目称为节点,关系称为。关系可以是定向的或非定向的。关系以及项目可以具有属性。因此,例如,地图也可以表示为图。每个城市是一个节点,城市之间的街道是边。城市之间的距离可以作为边上的属性分配。

Apache Spark GraphX模块使 Apache Spark 能够提供快速的大数据内存图处理。这使您能够运行图算法...

扩展生态系统

在审视大数据处理系统时,我们认为不仅要关注系统本身,还要关注它如何扩展以及如何与外部系统集成,以便提供更高级别的功能。在这本书的篇幅中,我们无法涵盖每一种选择,但通过引入一个主题,我们希望能够激发读者的兴趣,使他们能够进一步研究。

在 Apache Spark V2 中有哪些新变化?

自 Apache Spark V2 以来,许多事情都发生了变化。这并不意味着 API 已被破坏。相反,大多数 V1.6 的 Apache Spark 应用程序将在 Apache Spark V2 上运行,无论是否需要很少的更改,但在幕后,已经发生了很多变化。

尽管Java 虚拟机JVM)本身是一件杰作,但它是一个通用的字节码执行引擎。因此,存在大量的 JVM 对象管理和垃圾回收GC)开销。例如,存储一个 4 字节的字符串,在 JVM 上需要 48 字节。GC 基于对象生命周期估计进行优化,但 Apache Spark 通常比 JVM 更了解这一点。因此,Tungsten 对私有子集禁用了 JVM GC...

集群设计

正如我们已经提到的,Apache Spark 是一个分布式、内存内并行处理系统,需要一个关联的存储系统。因此,当您构建大数据集群时,您可能会使用分布式存储系统,如 Hadoop,以及用于移动数据的工具,如 Sqoop、Flume 和 Kafka。

我们希望在大数据集群中引入边缘节点的概念。这些集群中的节点将面向客户端,上面驻留着如 Hadoop NameNode 或可能是 Spark master 等客户端面向组件。大多数大数据集群可能位于防火墙后面。边缘节点将减少由防火墙引起的复杂性,因为它们将是外部可访问的唯一接触点。下图展示了一个简化的大数据集群:

它展示了五个简化的集群节点,每个 CPU 核心有一个执行器 JVM,以及位于集群外部的 Spark 驱动程序 JVM。此外,您可以看到直接连接到节点的磁盘。这被称为JBOD只是一堆磁盘)方法。非常大的文件在磁盘上分区,虚拟文件系统(如 HDFS)将这些块作为一个大虚拟文件提供。当然,这是风格化和简化的,但您可以理解这个概念。

下面的简化组件模型展示了驱动程序 JVM 位于集群外部。它与集群管理器通信,以获得在 worker 节点上调度任务的许可,因为集群管理器负责跟踪集群上运行的所有进程的资源分配。

正如我们稍后将看到的,存在多种不同的集群管理器,其中一些还能够管理其他 Hadoop 工作负载,甚至与 Spark 执行器并行运行的非 Hadoop 应用程序。请注意,执行器和驱动程序之间始终保持双向通信,因此从网络角度来看,它们也应该彼此靠近:

图源:https://spark.apache.org/docs/2.0.2/cluster-overview.html

通常,虽然防火墙为集群增加了安全性,但也增加了复杂性。系统组件之间的端口需要打开,以便它们可以相互通信。例如,Zookeeper 被许多组件用于配置。Apache Kafka,发布/订阅消息系统,使用 Zookeeper 来配置其主题、组、消费者和生产者。因此,需要打开到 Zookeeper 的客户端端口,可能跨越防火墙。

最后,需要考虑将系统分配给集群节点的方案。例如,如果 Apache Spark 使用 Flume 或 Kafka,则会使用内存通道。这些通道的大小以及数据流导致的内存使用量需要考虑。Apache Spark 不应与其他 Apache 组件竞争内存使用。根据您的数据流和内存使用情况,可能需要在不同的集群节点上部署 Spark、Hadoop、Zookeeper、Flume 和其他工具。或者,可以使用 YARN、Mesos 或 Docker 等资源管理器来解决此问题。在标准的 Hadoop 环境中,YARN 最有可能被采用。

通常,作为集群 NameNode 服务器或 Spark 主服务器的边缘节点将需要比防火墙内的集群处理节点更多的资源。当许多 Hadoop 生态系统组件部署在集群上时,它们都需要在主服务器上额外内存。您应该监控边缘节点的资源使用情况,并根据需要调整资源和/或应用程序位置。例如,YARN 正在处理这个问题。

本节简要介绍了大数据集群中的 Apache Spark、Hadoop 及其他工具。但是,大数据集群内部,Apache Spark 集群本身可能如何配置呢?例如,可以有多种类型的 Spark 集群管理器。下一节将探讨这一点,并描述每种 Apache Spark 集群管理器的类型。

集群管理

Spark 上下文,正如你在本书的许多示例中看到的,可以通过 Spark 配置对象和 Spark URL 来定义。Spark 上下文连接到 Spark 集群管理器,后者随后在集群的工作节点之间分配资源给应用程序。集群管理器在集群的工作节点上分配执行器。它将应用程序 JAR 文件复制到工作节点,并最终分配任务。

以下小节描述了目前可用的 Apache Spark 集群管理器的各种选项。

本地

通过指定一个本地 Spark 配置 URL,可以使应用程序在本地运行。通过指定 local[n],可以使 Spark 使用 n 个线程在本地运行应用程序。这是一个有用的开发和测试选项,因为你还可以测试某种并行化场景,但将所有日志文件保留在单个机器上。

Standalone

Standalone 模式使用 Apache Spark 自带的基本集群管理器。Spark 主节点的 URL 将如下所示:

Spark://<hostname>:7077

在此,<hostname> 表示运行 Spark 主节点的宿主机的名称。我们已将端口指定为 7077,这是默认值,但可配置。当前这种简单的集群管理器仅支持 FIFO先进先出)调度策略。你可以通过为每个应用程序设置资源配置选项来设法实现并发应用调度;例如,使用 spark.core.max 在应用程序之间共享核心。

Apache YARN

在更大规模上,当与 Hadoop YARN 集成时,Apache Spark 集群管理器可以是 YARN,应用程序可以运行在两种模式之一。如果将 Spark 主节点值设置为 yarn-cluster,则可以将应用程序提交到集群并随后终止。集群将负责分配资源和运行任务。然而,如果应用程序主节点以 yarn-client 方式提交,则应用程序在处理周期内保持活动状态,并向 YARN 请求资源。

Apache Mesos

Apache Mesos 是一个开源系统,用于集群间的资源共享。它允许多个框架通过管理和调度资源来共享集群。作为一个集群管理器,它利用 Linux 容器提供隔离,并允许 Hadoop、Spark、Kafka、Storm 等多种系统安全地共享集群。它高度可扩展至数千个节点。它是一个基于主/从的系统,并具有故障容忍性,使用 Zookeeper 进行配置管理。

对于单个主节点的 Mesos 集群,Spark 主 URL 将采用以下形式:

mesos://<hostname>:5050.

在此,<hostname>是 Mesos 主服务器的 hostname;端口定义为5050,这是默认的 Mesos 主端口(...)

基于云的部署

云系统有三种不同的抽象层次——基础设施即服务IaaS)、平台即服务PaaS)和软件即服务SaaS)。我们将探讨如何在所有这些层面上使用和安装 Apache Spark。

新的 IaaS 方式是 Docker 和 Kubernetes,与虚拟机相对,基本上提供了一种在几分钟内自动设置 Apache Spark 集群的方法。Kubernetes 的优势在于,由于它是开放标准且基于开源,因此可以在多个不同的云提供商之间使用。

你甚至可以使用 Kubernetes,在本地数据中心内透明且动态地移动工作负载,跨越本地、专用和公共云数据中心。相比之下,PaaS 为你减轻了安装和操作 Apache Spark 集群的负担,因为这作为一项服务提供。

关于 Docker 是 IaaS 还是 PaaS 的讨论仍在进行中,但在我们看来,它只是一种轻量级预装虚拟机形式。这一点特别有趣,因为其完全基于开源技术,使得你能够在任何其他数据中心复制该系统。

我们将介绍的开源组件之一是 Jupyter 笔记本;一种在基于云的协作环境中进行数据科学的现代方式。

性能

在进入涵盖 Apache Spark 功能区域和扩展的其余章节之前,我们将审视性能领域。需要考虑哪些问题和领域?从集群级别到实际 Scala 代码,哪些因素可能影响 Spark 应用程序性能?我们不想仅仅重复 Spark 网站上的内容,因此请查看此 URL:http://spark.apache.org/docs/<version>/tuning.html

在此,<version>对应于你正在使用的 Spark 版本;即,最新版本或类似1.6.1的特定版本。因此,浏览此页面后,我们将简要提及一些主题领域。本节中,我们将列出一些一般性要点,但不暗示...

集群结构

大数据集群的规模和结构将影响性能。如果你拥有一个基于云的集群,相比非共享硬件集群,你的 IO 和延迟将会受到影响。你将与多个客户共享底层硬件,且集群硬件可能位于远程。当然,也有例外。例如,IBM 云提供按小时租赁的专用裸金属高性能集群节点,配备 InfiniBand 网络连接。

此外,集群组件在服务器上的位置可能导致资源争用。例如,在大规模集群中仔细考虑 Hadoop NameNodes、Spark 服务器、Zookeeper、Flume 和 Kafka 服务器的布局。在高负载情况下,您可能需要将服务器隔离到单独的系统中。您还可以考虑使用 Apache Mesos 等系统,它为各个进程提供更好的资源分配和分配。

同时考虑潜在的并行性。对于大型数据集,您的 Spark 集群中的工作者数量越多,实现并行处理的机会就越大。一个经验法则是每个超线程或虚拟核心分别对应一个工作者。

Hadoop 分布式文件系统

根据您的集群需求,您可能考虑使用 HDFS 的替代方案。例如,IBM 提供了GPFS通用目的文件系统)以提高性能。

GPFS 可能是更好选择的原因在于,它源自高性能计算背景,这种文件系统具有完整的读写能力,而 HDFS 设计为一次写入、多次读取的文件系统。它在性能上优于 HDFS,因为它在核心级别运行,而 HDFS 在Java 虚拟机JVM)中运行,后者又作为操作系统进程运行。它还与 Hadoop 和 Spark 集群工具集成。IBM 使用 GPFS 配置了数百 PB 的系统。...

数据局部性

良好数据处理性能的关键是避免网络传输。这在几年前是非常正确的,但对于 CPU 需求高、I/O 需求低的任务来说,这不太相关,但对于 CPU 需求低、I/O 需求高的数据处理算法,这仍然适用。

由此我们可以得出结论,HDFS 是实现数据局部性的最佳方式之一,因为文件块分布在集群节点上,在大多数情况下,使用直接连接到服务器系统的硬盘。这意味着可以在包含个别数据块的机器上使用 CPU 并行处理这些块,以避免网络传输。

另一种实现数据局部性的方法是使用ApacheSparkSQL。根据连接器实现的不同,SparkSQL 可以利用源引擎的数据处理能力。例如,当结合使用 MongoDB 和 SparkSQL 时,SQL 语句的部分内容在数据发送到 Apache Spark 之前由 MongoDB 预处理。

内存

为了避免 Apache Spark 集群上的任务出现内存不足OOM)消息,请考虑以下调优问题:

  • 考虑您的 Spark 工作节点上可用的物理内存级别。是否可以增加?在高负载期间检查操作系统进程的内存消耗,以了解可用内存的情况。确保工作者有足够的内存。

  • 考虑数据分区。你能增加分区数量吗?一般而言,分区的数量应至少与集群中可用的 CPU 核心数相等。可使用 RDD API 中的repartition函数。

  • 你能调整用于存储和缓存 RDD 的 JVM 内存比例吗?...

编码

尝试优化你的代码,以提升 Spark 应用程序的性能。例如,在你的 ETL 周期早期基于应用程序数据进行过滤。一个例子是,当使用原始 HTML 文件时,在早期阶段去除标签并裁剪掉不需要的部分。调整并行度,尝试找出代码中资源消耗大的部分,并寻找替代方案。

ETL是分析项目中首先要做的事情之一。因此,你正在从第三方系统抓取数据,要么直接访问关系型或 NoSQL 数据库,要么通过读取各种文件格式的导出,如 CSV、TSV、JSON,甚至是来自本地或远程文件系统或 HDFS 中暂存区的更奇特的格式:在对文件进行一些检查和合理性检查后,Apache Spark 中的 ETL 过程基本上读取这些文件并从中创建 RDD 或 DataFrames/Datasets。

它们被转换以适应下游的分析应用程序,这些应用程序运行在 Apache Spark 或其他应用程序之上,然后存储回文件系统,格式可以是 JSON、CSV 或 PARQUET 文件,甚至返回到关系型或 NoSQL 数据库。

最后,对于任何与 Apache Spark 性能相关的问题,我推荐以下资源:spark.apache.org/docs/latest/tuning.html

尽管本书的部分内容将专注于 Apache Spark 在物理服务器集群上安装的示例,但我们想强调,市面上存在多种基于云的选项,它们带来了许多好处。有些云系统将 Apache Spark 作为集成组件,而有些则提供 Spark 作为服务。

错误与恢复

通常,对于你的应用程序,需要问的问题是:是否必须接收并处理所有数据?如果不是,那么在失败时,你可能只需重启应用程序并丢弃缺失或丢失的数据。如果情况并非如此,那么你需要使用将在下一节中描述的检查点机制。

同样值得注意的是,你的应用程序的错误管理应该是健壮且自给自足的。我们的意思是,如果异常不是关键性的,那么管理该异常,可能记录它,并继续处理。例如,当任务达到最大失败次数(由spark.task.maxFailures指定)时,它将终止处理。

这一属性及其他属性,可以在创建SparkContext对象时设置,或者在调用spark-shellspark-submit时作为额外的命令行参数。

总结

在结束本章之际,我们邀请你逐步学习后续章节中基于 Scala 代码的示例。Apache Spark 的发展速度令人印象深刻,值得注意的是其发布的频繁程度。因此,尽管在撰写本文时 Spark 已达到 2.2 版本,但我们确信你将使用更新的版本。

如果你遇到问题,请在www.stackoverflow.com上报并相应地标记它们;你将在几分钟内收到反馈——用户社区非常活跃。获取信息和帮助的另一种方式是订阅 Apache Spark 邮件列表:user@apachespark.org

本章结束时,你应该对本书中等待你的内容有了一个清晰的认识。我们专门...

第二章:Apache Spark 流处理

Apache 流处理模块是 Apache Spark 中的一个基于流处理的模块。它使用 Spark 集群,提供高度扩展的能力。基于 Spark,它也具有高度容错性,能够通过检查点正在处理的数据流来重新运行失败的任务。在本章的介绍部分之后,将涵盖以下主题,该部分将提供 Apache Spark 如何处理基于流的数据的实际概述:

  • 错误恢复与检查点

  • TCP 基础的流处理

  • 文件流

  • Kafka 流源

对于每个主题,我们将提供一个在 Scala 中实现的工作示例,并展示如何设置和测试基于流的架构。

概览

以下图表展示了 Apache 流处理的潜在数据源,如 Kafka、Flume 和 HDFS:

这些输入被送入 Spark 流处理模块,并作为离散流进行处理。该图还显示了其他 Spark 模块功能,如机器学习,也可以用于处理基于流的数。

完全处理后的数据可以作为输出到 HDFS、数据库或仪表板。此图基于 Spark 流处理网站上的图,但我们希望扩展它以表达 Spark 模块功能:

检查点

在批处理中,我们习惯于具备容错性。这意味着,如果某个节点崩溃,作业不会丢失其状态,丢失的任务会在其他工作节点上重新调度。中间结果被写入持久存储(当然,这种存储也必须具备容错性,如 HDFS、GPFS 或云对象存储)。现在我们希望在流处理中也实现同样的保证,因为确保我们正在处理的数据流不丢失可能至关重要。

可以设置一个基于 HDFS 的检查点目录来存储基于 Apache Spark 的流处理信息。在这个 Scala 示例中,数据将存储在 HDFS 下的/data/spark/checkpoint。以下 HDFS 文件系统ls命令显示,在开始之前,该目录不存在:

[hadoop@hc2nn stream]$ hdfs dfs -ls /data/spark/checkpoint
 ls: `/data/spark/checkpoint': No such file or directory

为了复制以下示例,我们使用 Twitter API 凭证来连接到 Twitter API 并获取推文流。以下链接解释了如何在 Twitter UI 中创建此类凭证:dev.twitter.com/oauth/overview/application-owner-access-tokens

以下 Scala 代码示例首先导入 Spark 流处理上下文和基于 Twitter 的功能。然后定义了一个名为stream1的应用程序对象:

import org.apache.spark._
import org.apache.spark.SparkContext._
import org.apache.spark.streaming._
import org.apache.spark.streaming.twitter._
import org.apache.spark.streaming.StreamingContext._

object stream1 {

接下来,定义了一个名为 createContext 的方法,该方法将用于创建 Spark 和 Streaming 上下文。它还将使用流上下文检查点方法将流检查点到基于 HDFS 的目录,该方法接受目录路径作为参数。目录路径是传递给 createContext 方法的值 (cpDir)

def createContext( cpDir : String ) : StreamingContext = {
  val appName = "Stream example 1"
  val conf    = new SparkConf()
  conf.setAppName(appName)
  val sc = new SparkContext(conf)
  val ssc    = new StreamingContext(sc, Seconds(5) )
  ssc.checkpoint( cpDir )
  ssc
}

现在,定义了主方法以及 HDFS 目录、Twitter 访问权限和参数。Spark Streaming 上下文 ssc 通过 StreamingContext 方法的 checkpoint 使用 HDFS 检查点目录检索或创建。如果目录不存在,则调用之前的方法 createContext,该方法将创建上下文和 checkpoint。显然,出于安全原因,我们在这个例子中截断了自己的 Twitter auth.keys

def main(args: Array[String]) {
  val hdfsDir = "/data/spark/checkpoint"
  val consumerKey       = "QQpxx"
  val consumerSecret    = "0HFzxx"
  val accessToken       = "323xx"
  val accessTokenSecret = "IlQxx"

  System.setProperty("twitter4j.oauth.consumerKey", consumerKey)
  System.setProperty("twitter4j.oauth.consumerSecret", consumerSecret)
  System.setProperty("twitter4j.oauth.accessToken", accessToken)
  System.setProperty("twitter4j.oauth.accessTokenSecret", accessTokenSecret)
  val ssc = StreamingContext.getOrCreate(hdfsDir,
        () => { createContext( hdfsDir ) })
  val stream = TwitterUtils.createStream(ssc,None).window( 
Seconds(60) )
  // do some processing
  ssc.start()
  ssc.awaitTermination()
} // end main

运行此代码后,由于没有实际处理,可以再次检查 HDFS checkpoint 目录。这次,很明显 checkpoint 目录已被创建,数据已被存储:

 [hadoop@hc2nn stream]$ hdfs dfs -ls /data/spark/checkpoint
 Found 1 items
 drwxr-xr-x   - hadoop supergroup          0 2015-07-02 13:41 
/data/spark/checkpoint/0fc3d94e-6f53-40fb-910d-1eef044b12e9

本例取自 Apache Spark 官网,展示了如何设置和使用检查点存储。检查点执行的频率是多少?元数据在每个流批次期间存储。实际数据存储在一个周期内,该周期是批次间隔或十秒的最大值。这可能不适合您,因此您可以使用以下方法重置该值:

 DStream.checkpoint( newRequiredInterval )

这里,newRequiredInterval 是您需要的新检查点间隔值;通常,您应该瞄准一个值,该值是您的批次间隔的五到十倍。检查点保存了流批次和元数据(关于数据的数据)。

如果应用程序失败,那么当它重新启动时,在处理开始时使用检查点数据。在失败时正在处理的数据批次与自失败以来的批处理数据一起重新处理。请记住监控用于检查点的 HDFS 磁盘空间。

在下一节中,我们将检查流源并提供每种类型的示例。

流源

在本节中,我们无法涵盖所有流类型的实际示例,但当本章太小而无法包含代码时,我们将至少提供描述。在本章中,我们将介绍 TCP 和文件流以及 Flume、Kafka 和 Twitter 流。Apache Spark 通常只支持这个有限的集合开箱即用,但这不是问题,因为第三方开发者也提供了连接到其他源的连接器。我们将从一个基于 TCP 的实际示例开始。本章检查流处理架构。

例如,在流数据交付速率超过潜在数据处理速率的情况下会发生什么?像 Kafka 这样的系统提供了可能解决这个问题的可能性...

TCP 流

有可能使用 Spark Streaming Context 的socketTextStream方法通过 TCP/IP 流式传输数据,只需指定主机名和端口号。本节中的基于 Scala 的代码示例将在端口10777接收数据,这些数据是通过netcatLinux 命令提供的。

netcat命令是一个 Linux/Unix 命令,它允许你使用 TCP 或 UDP 向本地或远程 IP 目的地发送和接收数据。这样,每个 shell 脚本都可以充当完整的网络客户端或服务器。以下是一个关于如何使用netcat的良好教程:www.binarytides.com/netcat-tutorial-for-beginners/

代码示例首先导入了 Spark、上下文以及流处理类。定义了一个名为stream2的对象类,它是带有参数的主方法。

import org.apache.spark._
import org.apache.spark.SparkContext._
import org.apache.spark.streaming._
import org.apache.spark.streaming.StreamingContext._

object stream2 {
  def main(args: Array[String]) {

检查传递给类的参数数量,以确保它是主机名和端口号。创建了一个带有定义的应用程序名称的 Spark 配置对象。然后创建了 Spark 和流处理上下文。接着,设置了10秒的流处理批次时间:

if ( args.length < 2 ) {
 System.err.println("Usage: stream2 <host> <port>")
 System.exit(1)
}

val hostname = args(0).trim
val portnum  = args(1).toInt
val appName  = "Stream example 2"
val conf     = new SparkConf()
conf.setAppName(appName)
val sc  = new SparkContext(conf)
val ssc = new StreamingContext(sc, Seconds(10) )

通过使用hostname和端口名参数调用流处理上下文的socketTextStream方法,创建了一个名为rawDstream的 DStream:

val rawDstream = ssc.socketTextStream( hostname, portnum )

通过用空格分割单词,从原始流数据中创建了一个前十单词计数。然后,创建了一个(key, value)对,即(word,1),它按键值,即单词进行缩减。现在,有一个单词列表及其关联的计数。键和值被交换,使得列表变为(计数和单词)。然后,对键(现在是计数)进行排序。最后,从 DStream 中的 RDD 中取出前 10 项并打印出来:

val wordCount = rawDstream
  .flatMap(line => line.split(" "))
  .map(word => (word,1))
  .reduceByKey(_+_)
  .map(item => item.swap)
  .transform(rdd => rdd.sortByKey(false))
  .foreachRDD( rdd =>
    { rdd.take(10).foreach(x=>println("List : " + x)) }
  )

代码以调用 Spark Streaming 的startawaitTermination方法结束,以启动流处理并等待进程终止:

    ssc.start()
      ssc.awaitTermination()
  } // end main
} // end stream2

正如我们之前所述,此应用程序的数据由 Linux Netcat (nc)命令提供。Linux cat命令转储日志文件的内容,该内容被管道传输到nclk选项强制 Netcat 监听连接,并在连接丢失时保持监听。此示例显示正在使用的端口是10777

 [root@hc2nn log]# pwd
 /var/log
 [root@hc2nn log]# cat ./anaconda.storage.log | nc -lk 10777

这里展示了基于 TCP 的流处理的输出。实际输出不如所展示的方法重要。然而,数据显示,正如预期的那样,是一份按降序计数的 10 个日志文件单词列表。请注意,顶部单词为空,因为流未被过滤以排除空单词:

 List : (17104,)
 List : (2333,=)
 List : (1656,:)
 List : (1603,;)
 List : (1557,DEBUG)
 List : (564,True)
 List : (495,False)
 List : (411,None)
 List : (356,at)
 List : (335,object)

如果你想基于 TCP/IP 从主机和端口使用 Apache Spark Streaming 进行数据流处理,这会很有趣。但是,更奇特的方法呢?如果你想从消息系统或通过基于内存的通道流式传输数据怎么办?如果你想使用当今可用的一些大数据工具,如 Flume 和 Kafka,该怎么办?接下来的部分将探讨这些选项,但首先,我们将展示如何基于文件构建流。

文件流

我们已修改上一节中的基于 Scala 的代码示例,通过调用 Spark Streaming 上下文的textFileStream方法来监控基于 HDFS 的目录。鉴于这一小改动,我们将不展示所有代码。应用程序类现在称为stream3,它接受一个参数——HDFS 目录。目录路径也可以位于另一个存储系统上(所有代码示例都将随本书提供):

val rawDstream = ssc.textFileStream( directory )

流处理与之前相同。流被分割成单词,并打印出前十个单词列表。这次唯一的区别是,数据必须在应用程序运行时放入 HDFS 目录。这是通过...实现的

Flume

Flume 是一个 Apache 开源项目及产品,旨在以大数据规模移动大量数据。它具有高度可扩展性、分布式和可靠性,基于数据源、数据接收器和数据通道运作,如下图所示,取自flume.apache.org/

Flume 使用代理处理数据流。如前图所示,一个代理具有数据源、数据处理通道和数据接收器。更清晰地描述此流程的方法是通过我们刚才看到的图。通道充当源数据的队列,接收器将数据传递到链中的下一个环节。

Flume 代理可以构成 Flume 架构;一个代理的接收器输出可以作为第二个代理的输入。Apache Spark 支持两种使用 Apache Flume 的方法。第一种是基于 Avro 的内存推送方法,而第二种方法,同样基于 Avro,是使用自定义 Spark 接收器库的拉取系统。本例中我们使用 Flume 版本 1.5:

[root@hc2nn ~]# flume-ng version
Flume 1.5.0-cdh5.3.3
Source code repository: https://git-wip-us.apache.org/repos/asf/flume.git
Revision: b88ce1fd016bc873d817343779dfff6aeea07706
Compiled by jenkins on Wed Apr  8 14:57:43 PDT 2015
From source with checksum 389d91c718e03341a2367bf4ef12428e

我们在此初步实现的基于 Flume 的 Spark 示例是基于 Flume 的推送方法,其中 Spark 充当接收器,Flume 将数据推送到 Spark。下图表示我们将在单个节点上实现的结构:

消息数据将被发送到名为hc2r1m1的主机的10777端口,使用 Linux 的netcatnc)命令。这将作为 Flume 代理(agent1)的一个源(source1),该代理将有一个名为channel1的内存通道。agent1使用的接收器将是基于 Apache Avro 的,同样在名为hc2r1m1的主机上,但这次端口号将是11777。Apache Spark Flume 应用程序stream4(我们稍后将描述)将监听此端口上的 Flume 流数据。

我们通过向10777端口执行nc命令来启动流处理。现在,当我们在该窗口中输入文本时,它将作为 Flume 源,数据将被发送到 Spark 应用程序:

[hadoop@hc2nn ~]$ nc  hc2r1m1.semtech-solutions.co.nz  10777

为了运行 Flume 代理agent1,我们创建了一个名为agent1.flume.cfg的 Flume 配置文件,该文件描述了代理的源、通道和接收器。文件内容如下。第一部分定义了agent1的源、通道和接收器名称。

agent1.sources  = source1
agent1.channels = channel1
agent1.sinks    = sink1

下一部分定义source1为基于 netcat,运行在名为hc2r1m1的主机上和10777端口:

agent1.sources.source1.channels=channel1
agent1.sources.source1.type=netcat
agent1.sources.source1.bind=hc2r1m1.semtech-solutions.co.nz
agent1.sources.source1.port=10777

agent1通道channel1被定义为具有最大事件容量1000事件的内存通道:

agent1.channels.channel1.type=memory
agent1.channels.channel1.capacity=1000

最后,agent1接收器sink1被定义为在名为hc2r1m1的主机上和11777端口的 Apache Avro 接收器:

agent1.sinks.sink1.type=avro
agent1.sinks.sink1.hostname=hc2r1m1.semtech-solutions.co.nz
agent1.sinks.sink1.port=11777 agent1.sinks.sink1.channel=channel1

我们创建了一个名为flume.bash的 Bash 脚本来运行 Flume 代理agent1。它如下所示:

[hadoop@hc2r1m1 stream]$ more flume.bash #!/bin/bash # run the bash agent flume-ng agent \
 --conf /etc/flume-ng/conf \
 --conf-file ./agent1.flume.cfg \
 -Dflume.root.logger=DEBUG,INFO,console  \
 -name agent1

该脚本调用 Flume 可执行文件flume-ng,传递agent1配置文件。调用指定了名为agent1的代理。它还指定了 Flume 配置目录为/etc/flume-ng/conf/,这是默认值。最初,我们将使用一个基于 Scala 的示例,该示例使用netcat Flume 源来展示如何将数据发送到 Apache Spark 应用程序。然后,我们将展示如何以类似方式处理基于 RSS 的数据源。因此,最初接收netcat数据的 Scala 代码看起来是这样的。应用程序类名被定义。导入 Spark 和 Flume 所需的类。最后,定义了主方法:

import org.apache.spark._
import org.apache.spark.SparkContext._
import org.apache.spark.streaming._
import org.apache.spark.streaming.StreamingContext._
import org.apache.spark.streaming.flume._

object stream4 {
  def main(args: Array[String]) {
  //The host and port name arguments for the data stream are checked and extracted:
      if ( args.length < 2 ) {
        System.err.println("Usage: stream4 <host> <port>")
        System.exit(1)
      }
      val hostname = args(0).trim
      val portnum  = args(1).toInt
      println("hostname : " + hostname)
      println("portnum  : " + portnum)

Spark 和 Streaming 上下文被创建。然后,使用流上下文主机和端口号创建基于 Flume 的数据流。为此,使用了基于 Flume 的类FlumeUtils,通过调用其createStream方法来实现:

val appName = "Stream example 4"
val conf    = new SparkConf()
conf.setAppName(appName)
val sc  = new SparkContext(conf)
val ssc = new StreamingContext(sc, Seconds(10) )
val rawDstream = FlumeUtils.createStream(ssc,hostname,portnum)

最终,会打印出流事件计数,并且在测试流时(出于调试目的)会转储流内容。之后,流上下文被启动并配置为运行,直到通过应用程序终止:

    rawDstream.count()
           .map(cnt => ">>>> Received events : " + cnt )
           .print()
    rawDstream.map(e => new String(e.event.getBody.array() ))
           .print
    ssc.start()
    ssc.awaitTermination()
  } // end main
} // end stream4

编译完成后,我们将使用spark-submit运行此应用程序。在本书的其他一些章节中,我们将使用一个名为run_stream.bash的基于 Bash 的脚本来执行任务。该脚本如下所示:

[hadoop@hc2r1m1 stream]$ more run_stream.bash #!/bin/bash SPARK_HOME=/usr/local/spark
SPARK_BIN=$SPARK_HOME/bin
SPARK_SBIN=$SPARK_HOME/sbin JAR_PATH=/home/hadoop/spark/stream/target/scala-2.10/streaming_2.10-1.0.jar
CLASS_VAL=$1
CLASS_PARAMS="${*:2}" STREAM_JAR=/usr/local/spark/lib/spark-examples-1.3.1-hadoop2.3.0.jar cd $SPARK_BIN ./spark-submit \
 --class $CLASS_VAL \
 --master spark://hc2nn.semtech-solutions.co.nz:7077  \
 --executor-memory 100M \
 --total-executor-cores 50 \
 --jars $STREAM_JAR \
 $JAR_PATH \
 $CLASS_PARAMS

因此,此脚本设置了一些基于 Spark 的变量和一个 JAR 库路径用于此作业。它将 Spark 类作为第一个参数运行。它将所有其他变量作为参数传递给 Spark 应用程序类作业。因此,应用程序的执行如下所示:

[hadoop@hc2r1m1 stream]$ ./run_stream.bash stream4 hc2r1m1 11777

这意味着 Spark 应用程序已准备好,并在端口11777上作为 Flume 接收器运行。Flume 输入已准备好,作为端口10777上的netcat任务运行。现在,Flume 代理agent1可以使用名为flume.bash的 Flume 脚本启动,以将基于netcat源的数据发送到 Apache Spark 基于 Flume 的接收器:

 [hadoop@hc2r1m1 stream]$ ./flume.bash

现在,当文本传递给netcat会话时,它应该通过 Flume 流动,并由 Spark 作为流处理。让我们试试:

[hadoop@hc2nn ~]$ nc  hc2r1m1.semtech-solutions.co.nz 10777
 I hope that Apache Spark will print this
 OK
 I hope that Apache Spark will print this
 OK
 I hope that Apache Spark will print this
 OK

已向netcat会话添加了三个简单的文本片段,并使用OK进行了确认,以便它们可以传递给 Flume。Flume 会话中的调试输出显示已收到并处理了事件(每行一个):

2015-07-06 18:13:18,699 (netcat-handler-0) [DEBUG - org.apache.flume.source.NetcatSource$NetcatSocketHandler.run(NetcatSource.java:318)] Chars read = 41
 2015-07-06 18:13:18,700 (netcat-handler-0) [DEBUG - org.apache.flume.source.NetcatSource$NetcatSocketHandler.run(NetcatSource.java:322)] Events processed = 1
 2015-07-06 18:13:18,990 (netcat-handler-0) [DEBUG - org.apache.flume.source.NetcatSource$NetcatSocketHandler.run(NetcatSource.java:318)] Chars read = 41
 2015-07-06 18:13:18,991 (netcat-handler-0) [DEBUG - org.apache.flume.source.NetcatSource$NetcatSocketHandler.run(NetcatSource.java:322)] Events processed = 1
 2015-07-06 18:13:19,270 (netcat-handler-0) [DEBUG - org.apache.flume.source.NetcatSource$NetcatSocketHandler.run(NetcatSource.java:318)] Chars read = 41
 2015-07-06 18:13:19,271 (netcat-handler-0) [DEBUG - org.apache.flume.source.NetcatSource$NetcatSocketHandler.run(NetcatSource.java:322)] Events processed = 1

最后,在 Spark stream4应用程序会话中,已收到并处理了三个事件;在这种情况下,它们已被转储到会话中,以证明数据已到达。当然,这不是您通常会做的,但我们想证明数据通过此配置传输:

-------------------------------------------
 Time: 1436163210000 ms
 -------------------------------------------
 >>> Received events : 3
 -------------------------------------------
 Time: 1436163210000 ms
 -------------------------------------------
 I hope that Apache Spark will print this
 I hope that Apache Spark will print this
 I hope that Apache Spark will print this

这很有趣,但它并不是真正值得生产的 Spark Flume 数据处理示例。因此,为了演示一种可能的实际数据处理方法,我们将更改 Flume 配置文件源详细信息,使其使用一个 Perl 脚本,该脚本可执行如下:

agent1.sources.source1.type=exec
agent1.sources.source.command=./rss.perl

先前引用的 Perl 脚本rss.perl仅作为路透社科学新闻的来源。它接收 XML 格式的消息并将其转换为 JSON 格式。它还清理了数据中的不必要噪音。首先,它导入 LWP 和XML::XPath等包以启用 XML 处理。然后,它指定基于科学的 Reuters 新闻数据源,并创建一个新的 LWP 代理来处理数据,如下所示:

#!/usr/bin/perl
use strict;
use LWP::UserAgent;
use XML::XPath;
my $urlsource="http://feeds.reuters.com/reuters/scienceNews" ;
my  $agent = LWP::UserAgent->new;
#Then an infinite while loop is opened, and an HTTP GET request is carried out against  the URL. The request is configured, and the agent makes the request via 
a call to the request method:
while()
{
  my  $req = HTTP::Request->new(GET => ($urlsource));
  $req->header('content-type' => 'application/json');
  $req->header('Accept'       => 'application/json');
  my $resp = $agent->request($req);

如果请求成功,则返回的 XML 数据定义为请求的解码内容。通过使用路径/rss/channel/item/title的 XPath 调用从 XML 中提取标题信息:

    if ( $resp->is_success )
    {
      my $xmlpage = $resp -> decoded_content;
      my $xp = XML::XPath->new( xml => $xmlpage );
      my $nodeset = $xp->find( '/rss/channel/item/title' );
      my @titles = () ;
      my $index = 0 ;

对于提取的标题数据XML字符串中的每个节点,都会提取数据。它清除了不需要的XML标签,并添加到名为 titles 的基于 Perl 的数组中:

     foreach my $node ($nodeset->get_nodelist) {
        my $xmlstring = XML::XPath::XMLParser::as_string($node) ;
        $xmlstring =~ s/<title>//g;
        $xmlstring =~ s/<\/title>//g;
        $xmlstring =~ s/"//g;
        $xmlstring =~ s/,//g;
        $titles[$index] = $xmlstring ;
        $index = $index + 1 ;
      } # foreach find node

对于请求响应 XML 中的基于描述的数据,执行相同的处理。这次使用的 XPath 值是/rss/channel/item/description/。描述数据中有许多更多的标签需要清理,因此有许多更多的 Perl 搜索和行替换作用于该数据(s///g):

    my $nodeset = $xp->find( '/rss/channel/item/description' );
    my @desc = () ;
    $index = 0 ;
    foreach my $node ($nodeset->get_nodelist) {
       my $xmlstring = XML::XPath::XMLParser::as_string($node) ;
       $xmlstring =~ s/<img.+\/img>//g;
       $xmlstring =~ s/href=".+"//g;
       $xmlstring =~ s/src="img/.+"//g;
       $xmlstring =~ s/src='.+'//g;
       $xmlstring =~ s/<br.+\/>//g;
       $xmlstring =~ s/<\/div>//g;
       $xmlstring =~ s/<\/a>//g;
       $xmlstring =~ s/<a >\n//g;
       $xmlstring =~ s/<img >//g;
       $xmlstring =~ s/<img \/>//g;
       $xmlstring =~ s/<div.+>//g;
       $xmlstring =~ s/<title>//g;
       $xmlstring =~ s/<\/title>//g;
       $xmlstring =~ s/<description>//g;
       $xmlstring =~ s/<\/description>//g;
       $xmlstring =~ s/&lt;.+>//g;
       $xmlstring =~ s/"//g;
       $xmlstring =~ s/,//g;
       $xmlstring =~ s/\r|\n//g;
       $desc[$index] = $xmlstring ;
       $index = $index + 1 ;
    } # foreach find node

最后,基于 XML 的标题和描述数据使用print命令以 RSS JSON 格式输出。然后脚本休眠 30 秒,并请求更多 RSS 新闻信息进行处理:

   my $newsitems = $index ;
   $index = 0 ;
   for ($index=0; $index < $newsitems; $index++) {
      print "{"category": "science","
              . " "title": "" .  $titles[$index] . "","
              . " "summary": "" .  $desc[$index] . """
               . "}\n";
      } # for rss items
    } # success ?
    sleep(30) ;
 } # while

我们创建了第二个基于 Scala 的流处理代码示例,名为 stream5。它类似于 stream4 示例,但现在它处理来自流中的 rss 项数据。接下来,定义 case class 以处理来自 XML RSS 信息的类别、标题和摘要。定义了一个 HTML 位置来存储从 Flume 通道传来的结果数据。

case class RSSItem(category : String, title : String, summary : String) {
  val now: Long = System.currentTimeMillis
  val hdfsdir = "hdfs://hc2nn:8020/data/spark/flume/rss/"

来自基于 Flume 事件的 RSS 流数据被转换为字符串,然后使用名为 RSSItem 的 case 类进行格式化。如果有事件数据,则使用之前的 hdfsdir 路径将其写入 HDFS 目录。

         rawDstream.map(record => {
         implicit val formats = DefaultFormats
         readRSSItem.array()))
      }).foreachRDD(rdd => {
              if (rdd.count() > 0) {
                rdd.map(item => {
                  implicit val formats = DefaultFormats
                  write(item)
                 }).saveAsTextFile(hdfsdir+"file_"+now.toString())
               }
      })

运行此代码示例,可以观察到 Perl RSS 脚本正在生成数据,因为 Flume 脚本的输出表明已接受并接收了 80 个事件。

2015-07-07 14:14:24,017 (agent-shutdown-hook) [DEBUG - org.apache.flume.source.ExecSource.stop(ExecSource.java:219)] Exec source with command:./news_rss_collector.py stopped. Metrics:SOURCE:source1{src.events.accepted=80, src.events.received=80, src.append.accepted=0, src.append-batch.accepted=0, src.open-connection.count=0, src.append-batch.received=0, src.append.received=0}
The Scala Spark application stream5 has processed 80 events in two batches:
>>>> Received events : 73
>>>> Received events : 7

事件已存储在 HDFS 下的预期目录中,正如 Hadoop 文件系统 ls 命令所示:

[hadoop@hc2r1m1 stream]$ hdfs dfs -ls /data/spark/flume/rss/
 Found 2 items
 drwxr-xr-x   - hadoop supergroup          0 2015-07-07 14:09 /data/spark/flume/rss/file_1436234439794
 drwxr-xr-x   - hadoop supergroup          0 2015-07-07 14:14 /data/spark/flume/rss/file_1436235208370

此外,使用 Hadoop 文件系统 cat 命令,可以证明 HDFS 上的文件包含基于 rss 订阅源的新闻数据,如下所示:

[hadoop@hc2r1m1 stream]$  hdfs dfs -cat /data/spark/flume/rss/file_1436235208370/part-00000 | head -1 {"category":"healthcare","title":"BRIEF-Aetna CEO says has not had specific conversations with DOJ on Humana - CNBC","summary":"* Aetna CEO Says Has Not Had Specific Conversations With Doj About Humana Acquisition - CNBC"}

此基于 Spark 流的示例使用了 Apache Flume 将数据从 rss 源传输,经过 Flume,通过 Spark 消费者到达 HDFS。这是一个很好的示例,但如果你想向一组消费者发布数据呢?在下一节中,我们将探讨 Apache Kafka——一个发布/订阅消息系统——并确定如何将其与 Spark 结合使用。

Kafka

Apache Kafka (kafka.apache.org/) 是 Apache 基金会下的一个顶级开源项目。它是一个快速且高度可扩展的大数据发布/订阅消息系统,利用消息代理进行数据管理,并通过 ZooKeeper 进行配置,以便数据可以组织成消费者组和主题。

Kafka 中的数据被分割成多个分区。在本例中,我们将展示一个基于 Spark 的无接收器 Kafka 消费者,这样我们就不需要在比较 Kafka 数据时担心配置 Spark 数据分区。为了演示基于 Kafka 的消息生产和消费,我们将使用上一节中的 Perl RSS 脚本作为数据源。传递到 Kafka 并到 Spark 的数据将是路透社 RSS 新闻...

总结

我们本可以为其他系统提供流式示例,但本章没有空间。Twitter 流式传输已在 检查点 部分通过示例进行了探讨。本章提供了通过 Spark Streaming 中的检查点进行数据恢复的实用示例。它还触及了检查点的性能限制,并表明检查点间隔应设置为 Spark 流批处理间隔的五到十倍。

检查点提供了一种基于流的恢复机制,用于在 Spark 应用程序失败时进行恢复。本章提供了一些基于 TCP、文件、Flume 和 Kafka 的 Spark 流编码的流式工作示例。这里所有的示例都是基于 Scala 并用sbt编译的。如果你更熟悉Maven,以下教程将解释如何设置基于 Maven 的 Scala 项目:www.scala-lang.org/old/node/345

第三章:结构化流处理

正如你可能已经从前几章理解的那样,Apache Spark 目前正从基于 RDD 的数据处理过渡到更结构化的处理,背后有 DataFrames 和 Datasets 支持,以便让 Catalyst 和 Tungsten 发挥作用,进行性能优化。这意味着社区目前采用双轨制。虽然非结构化 API 仍然得到支持——它们甚至还没有被标记为已弃用,而且它们是否会这样做也值得怀疑——但在 Apache Spark V 2.0 中为各种组件引入了一套新的结构化 API,这也适用于 Spark Streaming。Structured Steaming 在 Apache Spark V 2.2 中被标记为稳定。请注意,截至 Apache Spark V 2.1 时...

连续应用的概念

流应用程序往往变得复杂。流计算不是孤立运行的;它们与存储系统、批处理应用程序和机器学习库交互。因此,与批处理相对的连续应用的概念应运而生,基本上意味着批处理和实时流处理的组合,其中流处理部分是应用程序的主要驱动力,并且仅访问由批处理过程创建或处理的数据以进行进一步增强。连续应用程序永不停止,并且随着新数据的到达而持续产生数据。

真正的统一 - 相同的代码,相同的引擎

因此,一个连续的应用程序也可以基于 RDD 和 DStreams 实现,但需要使用两种不同的 API。在 Apache Spark Structured Streaming 中,API 得到了统一。这种统一是通过将结构化流视为一张无边界的关系表来实现的,其中新数据不断追加到表的底部。在批处理中使用关系 API 或 SQL 处理 DataFrames 时,会创建中间 DataFrames。由于流和批处理在 Apache SparkSQL 引擎上得到了统一,当处理结构化流时,会创建无边界的中间关系表。

重要的是要注意,可以混合(连接)静态和增量...

窗口化

开源和商业流处理引擎,如 IBM Streams、Apache Storm 或 Apache Flink,都在使用窗口的概念。

Windows 指定了粒度或后续记录的数量,这些记录在执行流上的聚合函数时会被考虑。

流处理引擎如何使用窗口化

存在五个不同的属性,分为两个维度,这就是窗口如何被定义的方式,其中每个窗口定义都需要使用每个维度的一个属性。

第一个属性是连续流中元组的后续窗口可以创建的模式:滑动和翻滚。

第二个是必须指定落入窗口的元组数量:基于计数、基于时间或基于会话。

让我们来看看它们的含义:

  • 滑动窗口:每当有新元组符合条件被纳入时,滑动窗口就会移除一个元组。

  • 翻滚窗口:每当有足够多的元组到达以创建新窗口时,翻滚窗口就会移除所有元组。

  • 基于计数的...

Apache Spark 如何优化窗口操作

Apache Spark 结构化流在窗口处理模型中展现出显著的灵活性。由于流被视为持续追加的表,且表中每行都带有时间戳,窗口操作可以在查询中直接指定,每个查询可以定义不同的窗口。此外,如果静态数据中存在时间戳,窗口操作也可以定义,从而形成一个非常灵活的流处理模型。

换言之,Apache Spark 的窗口操作本质上是对时间戳列的一种特殊分组。这使得处理迟到数据变得非常简单,因为 Apache Spark 可以将迟到数据纳入适当的窗口,并在特定数据项迟到时重新计算该窗口。此功能高度可配置。

事件时间与处理时间对比:在时间序列分析中,尤其是在流计算中,每个记录都会被分配一个特定的时戳。一种创建这种时戳的方法是记录到达流处理引擎的时间。然而,这往往并非所需。通常,我们希望为每个记录分配一个事件时间,即该记录创建时的特定时间点,例如,当物联网设备进行测量时。这有助于处理事件创建与处理之间的延迟,例如,当物联网传感器离线一段时间,或网络拥堵导致数据交付延迟时。

在使用事件时间而非处理时间为每个元组分配唯一时戳时,迟到数据的概念颇具趣味。事件时间是指特定测量发生的时间戳。Apache Spark 结构化流能够自动透明地处理在稍后时间点到达的数据子集。

迟到数据:无论记录何时到达任何流引擎,都会立即处理。在此方面,Apache Spark 流处理与其他引擎并无二致。然而,Apache Spark 具备在任何时间确定特定元组所属窗口的能力。如果由于任何原因元组迟到,所有受影响的窗口将被更新,基于这些更新窗口的所有受影响聚合操作将重新运行。这意味着,如果迟到数据到达,结果允许随时间变化,而无需程序员为此担忧。最后,自 Apache Spark V2.1 起,可以使用withWatermark方法指定系统接受迟到数据的时间量。

水印基本上是阈值,用于定义延迟到达的数据点允许有多旧,以便仍能被包含在相应的窗口中。再次考虑 HTTP 服务器日志文件在超过一分钟长度的窗口上工作。如果,由于任何原因,一个数据元组到达,它超过 4 小时旧,如果这个应用程序用于创建基于小时的时间序列预测模型来为集群提供或取消提供额外的 HTTP 服务器,那么它可能没有意义将其包含在窗口中。一个四小时前的数据点就没有意义处理,即使它可能改变决策,因为决策已经做出。

与老朋友一起提升性能

正如在 Apache SparkSQL 中用于批处理,以及作为 Apache Spark 结构化流的一部分,Catalyst Planner 也为微批创建增量执行计划。这意味着整个流模型基于批处理。这也是为什么能够实现流处理和批处理的统一 API 的原因。我们付出的代价是,Apache Spark 流处理在面对极低延迟要求(亚秒级,在几十毫秒范围内)时有时会有缺点。正如结构化流和使用 DataFrame 及 Dataset 所暗示的,我们也因 Tungsten 项目带来的性能提升而受益,该项目在之前的...

如何实现透明的容错和精确一次投递保证

Apache Spark 结构化流支持完全崩溃容错和精确一次投递保证,而无需用户处理任何特定的错误处理例程。这不是很神奇吗?那么这是如何实现的呢?

完全崩溃容错和精确一次投递保证是系统理论中的术语。完全崩溃容错意味着你可以在任何时间点拔掉整个数据中心的电源,而不会丢失任何数据或留下不一致的状态。精确一次投递保证意味着,即使拔掉同一个电源插头,也能确保每个元组——从数据源到数据汇——仅且仅一次被投递。既不是零次,也不会超过一次。当然,这些概念也必须在一个节点失败或行为异常(例如开始限流)的情况下成立。

首先,各个批次和偏移量范围(源流中的位置)之间的状态保持在内存中,但由预写日志WAL)在如 HDFS 这样的容错文件系统中支持。WAL 基本上是一个日志文件,以主动的方式反映整个流处理状态。这意味着在数据通过操作符转换之前,它首先以一种可以在崩溃后恢复的方式持久存储在 WAL 中。因此,换句话说,在处理单个迷你批次期间,工作者内存的区域以及流源的偏移位置都被持久化到磁盘。如果系统失败并需要恢复,它可以重新请求源中的数据块。当然,这只在源支持这种语义的情况下才可能。

可重放源可以从给定的偏移量重放流

端到端的一次性交付保证要求流源支持在请求位置进行某种流重放。这对于文件源和 Apache Kafka 等是正确的,例如,以及本章中示例将基于的 IBM Watson 物联网平台。

幂等接收器防止数据重复

端到端一次性交付保证的另一个关键是幂等接收器。这基本上意味着接收器知道过去哪些特定的写操作已经成功。这意味着这样的智能接收器可以在失败时重新请求数据,并在相同数据被发送多次时丢弃数据。

状态版本化确保重跑后结果一致

那么状态呢?设想一个机器学习算法在所有工作者上维护一个计数变量。如果你将完全相同的数据重放两次,你最终会多次计数这些数据。因此,查询计划器也在工作者内部维护一个版本化的键值映射,这些工作者依次将其状态持久化到 HDFS——这是设计上的容错机制。

因此,在发生故障时,如果数据需要被替换,计划器确保工作者使用正确的键值映射版本。

示例 - 连接到 MQTT 消息代理

那么,让我们从一个示例用例开始。让我们连接到一个物联网IoT)传感器数据流。由于我们到目前为止还没有涉及机器学习,我们不分析数据,我们只是展示概念。

我们使用 IBM Watson 物联网平台作为流数据源。在其核心,Watson 物联网平台由MQTT消息队列遥测传输)消息代理支持。MQTT 是 IBM 于 1999 年发明的一种轻量级遥测协议,并于 2013 年成为OASIS结构化信息标准促进组织,一个全球非营利性联盟,致力于安全、物联网、能源、内容技术、应急管理等领域的标准开发、融合和采纳)的标准——物联网数据集成的实际标准。

应用程序间的消息传递可以由消息队列支持,这是一种支持各种交付模式的异步点对点通道的中间件系统,如先进先出FIFO)、后进先出LIFO)或优先级队列(其中每条消息可以根据特定标准重新排序)。

这已经是一个非常棒的功能,但仍然以某种方式耦合了应用程序,因为一旦消息被读取,它就对其他应用程序不可用了。

这种 N 对 N 通信实现起来较为困难(但并非不可能)。在发布/订阅模型中,应用程序完全解耦。不再存在任何队列,而是引入了主题的概念。数据提供者在特定主题上发布消息,而数据消费者则订阅这些主题。这样一来,N 对 N 通信的实现就变得非常直接,因为它反映了底层的消息传递模型。这种中间件被称为消息代理,与消息队列相对。

由于云服务不断变化,且本书稍后才会介绍云,以下教程解释了如何在云中设置测试数据生成器并连接到远程 MQTT 消息代理。在本例中,我们将使用 IBM Watson IoT 平台,这是一个在云中可用的 MQTT 消息代理。或者,也可以安装开源消息代理如 MOSQUITTO,它还提供了一个公开可用的测试安装,网址如下:test.mosquitto.org/

为了复现示例,以下步骤(1)和(2)是必要的,如以下教程所述:www.ibm.com/developerworks/library/iot-cognitive-iot-app-machine-learning/index.html。请确保在执行教程时记下http_hostorgapiKeyapiToken。这些信息稍后用于通过 Apache Spark 结构化流订阅数据。

由于 IBM Watson 物联网平台采用开放的 MQTT 标准,因此无需特殊的 IBM 组件即可连接到该平台。相反,我们使用 MQTT 和 Apache Bahir 作为 MQTT 与 Apache Spark 结构化流之间的连接器。

Apache Bahir 项目的目标是为包括 Apache Spark 和 Apache Flink 在内的各种数据处理引擎提供一组源和汇连接器,因为它们缺乏这些连接器。在这种情况下,我们将使用 Apache Bahir MQTT 数据源进行 MQTT 通信。

为了使用 Apache Bahir,我们需要向本地 maven 仓库添加两个依赖项。本章下载部分提供了一个完整的pom.xml文件。让我们看一下pom.xml的依赖部分:

我们基本上是在获取 Apache Bahir 的 MQTT Apache 结构化流适配器以及一个用于低级 MQTT 处理的依赖包。在pom.xml文件所在的目录中执行简单的mvn dependency:resolve命令,会将所需的依赖项拉取到我们的本地 maven 仓库,在那里它们可以被 Apache Spark 驱动程序访问并自动传输到 Apache Spark 工作节点。

另一种解决依赖关系的方法是在启动 spark-shell(spark-submit 同样适用)时使用以下命令;必要的依赖项会自动分发给工作节点:

现在我们需要之前获取的 MQTT 凭证。让我们在这里设置值:

val mqtt_host = "pcoyha.messaging.internetofthings.ibmcloud.com"
val org = "pcoyha"
val apiKey = "a-pcoyha-oaigc1k8ub"
val apiToken = "&wuypVX2yNgVLAcLr8"
var randomSessionId = scala.util.Random.nextInt(10000)

现在我们可以开始创建一个连接到 MQTT 消息代理的流。我们告诉 Apache Spark 使用 Apache Bahir MQTT 流源:

val df = spark.readStream.format("org.apache.bahir.sql.streaming.mqtt.MQTTStreamSourceProvider")

为了从 MQTT 消息代理拉取数据,我们需要指定凭证,如usernamepasswordclientId;前面提到的教程链接解释了如何获取这些凭证:

    .option("username",apiKey)
    .option("password",apiToken)
    .option("clientId","a:"+org+":"+apiKey)

由于我们使用的是发布/订阅消息模型,我们必须提供我们正在订阅的主题——这个主题由您之前部署到云端的测试数据生成器使用:

.option("topic", "iot-2/type/WashingMachine/id/Washer01/evt/voltage/fmt/json")

一旦配置方面一切就绪,我们就必须提供端点主机和端口以创建流:

   .load("tcp://"+mqtt_host+":1883")

有趣的是,正如以下截图所示,这导致了 DataFrame 的创建:

请注意,模式固定为[String, Timestamp],并且在流创建过程中无法更改——这是 Apache Bahir 库的一个限制。然而,使用丰富的 DataFrame API,您可以解析值(例如,JSON 字符串)并创建新列。

如前所述,这是 Apache Spark 结构化流的一个强大功能,因为相同的 DataFrame(和 Dataset)API 现在可以用于处理历史和实时数据。因此,让我们通过将其写入控制台来查看此流的

val query = df.writeStream.
outputMode("append").
format("console").
start()

作为输出模式,我们选择append以强制增量显示,并避免历史流的内容被反复写入控制台。作为格式,我们指定console,因为我们只想调试流上发生的情况:

最后,start 方法启动查询处理,如这里所示:

控制连续应用程序

一旦连续应用程序(即使是简单的,不考虑历史数据)启动并运行,它就必须以某种方式进行控制,因为调用 start 方法立即开始处理,但也不会阻塞返回。如果您希望程序在此阶段阻塞,直到应用程序完成,可以使用 awaitTermination 方法,如下所示:

query.awaitTermination()

这在预编译代码并使用 spark-submit 命令时尤为重要。当使用 spark-shell 时,应用程序无论如何都不会终止。

更多关于流生命周期管理

流式传输通常用于创建连续应用程序。这意味着该过程在后台运行,与批处理不同,它没有明确的停止时间;因此,由流式源支持的 DataFrames 和 Datasets 支持各种流生命周期管理方法,如下所述:

  • start:这启动了连续应用程序。此方法不会阻塞。如果这不是您想要的,请使用 awaitTermination

  • stop:这终止了连续应用程序。

  • awaitTermination:如前所述,使用 start 方法启动流立即返回,这意味着调用不会阻塞。有时您希望等待直到流被终止,无论是由其他人调用 stop 还是由于错误。

  • exception:如果流因错误而停止,可以使用此方法读取原因。

  • sourceStatus:这是为了获取流式源的实时元信息。

  • sinkStatus:这是为了获取流式接收器的实时元信息。

Apache Spark 流式传输中的接收器很智能,因为它们支持故障恢复和端到端的一次性交付保证,如前所述。此外,Apache Spark 需要它们支持不同的输出方法。目前,以下三种输出方法 appendupdatecomplete 显著改变了底层语义。以下段落包含有关不同输出方法的更多详细信息。

不同的输出模式在接收器上:接收器可以指定以不同方式处理输出。这称为 outputMode。最简单的选择是使用增量方法,因为我们无论如何都在处理增量数据流。此模式称为 append。然而,存在一些需求,其中已经由接收器处理的数据必须更改。一个例子是特定时间窗口中缺失数据的延迟到达问题,一旦为该特定时间窗口重新计算,就可能导致结果改变。此模式称为 complete

自 Apache Spark 2.1 版本起,引入了update模式,其行为类似于complete模式,但仅更改已修改的行,从而节省处理资源并提高速度。某些模式不支持所有查询类型。由于这不断变化,最好参考spark.apache.org/docs/latest/streaming-programming-guide.html上的最新文档。

总结

那么为什么在同一个数据处理框架内会有两种不同的流处理引擎呢?我们希望在阅读本章后,您会认同经典 DStream 引擎的主要痛点已得到解决。以前,基于事件时间的处理是不可能的,只考虑了数据的到达时间。随后,延迟数据仅以错误的时戳进行处理,因为只能使用处理时间。此外,批处理和流处理需要使用两种不同的 API:RDD 和 DStreams。尽管 API 相似,但并不完全相同;因此,在两种范式之间来回切换时重写代码是必要的。最后,端到端的交付保证难以实现...

第四章:Apache Spark MLlib

MLlib 是 Apache Spark 附带的原始机器学习库,Apache Spark 是一个基于内存的集群式开源数据处理系统。该库仍然基于 RDD API。在本章中,我们将从回归、分类和神经网络处理等领域来探讨 MLlib 库提供的功能。在提供解决实际问题的示例之前,我们将先探讨每种算法的理论基础。网络上的示例代码和文档可能稀少且令人困惑。

我们将采取逐步的方式来描述以下算法的使用方法及其功能:

  • 架构

  • 使用朴素贝叶斯进行分类

  • K-Means 聚类

  • 使用人工神经网络进行图像分类

架构

请记住,尽管 Spark 因其内存中的分布式处理速度而被使用,但它并不提供存储。您可以使用主机(本地)文件系统来读写数据,但如果您的数据量足够大,可以称之为大数据,那么使用基于云的分布式存储系统(如 OpenStack Swift 对象存储)是有意义的,该系统可以在许多云环境中找到,也可以安装在私有数据中心中。

如果需要极高的 I/O 性能,HDFS 也是一个选项。更多关于 HDFS 的信息可以在这里找到:hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-hdfs/HdfsDesign.html

开发环境

本书中的代码示例将使用 Scala 语言编写。这是因为作为一种脚本语言,它产生的代码比 Java 少。它也可以在 Spark shell 中使用,以及与 Apache Spark 应用程序一起编译。我们将使用sbt 工具来编译 Scala 代码,我们已经按照以下方式将其安装到 Hortonworks HDP 2.6 Sandbox 中:

[hadoop@hc2nn ~]# sudo su -
[root@hc2nn ~]# cd /tmp
[root@hc2nn ~]#wget http://repo.scala-sbt.org/scalasbt/sbt-native-packages/org/scala-sbt/sbt/0.13.1/sbt.rpm
[root@hc2nn ~]# rpm -ivh sbt.rpm

以下 URL 提供了在包括 Windows、Linux 和 macOS 在内的其他操作系统上安装 sbt 的说明:www.scala-sbt.org/0.13/docs/Setup.html

我们使用了一个名为Hadoop的通用 Linux 账户。如前述命令所示,我们需要以 root 账户安装sbt,我们通过sudo su -l(切换用户)访问了该账户。然后,我们使用wget从名为repo.scala-sbt.org的基于网络的服务器下载了sbt.rpm文件到/tmp目录。最后,我们使用带有i(安装)、v(验证)和h(打印哈希标记)选项的rpm命令安装了rpm文件。

本章中,我们在 Linux 服务器上使用 Linux Hadoop 账户开发了 Apache Spark 的所有 Scala 代码。我们将每组代码放置在/home/hadoop/spark下的一个子目录中。例如,以下sbt结构图显示 MLlib 朴素贝叶斯代码存储在 Spark 目录下的名为nbayes的子目录中。该图还显示,Scala 代码是在nbayes目录下的src/main/scala子目录结构中开发的。名为bayes1.scalaconvert.scala的文件包含将在下一节中使用的朴素贝叶斯代码:

bayes.sbt文件是sbt工具使用的配置文件,描述了如何编译 Scala 目录内的 Scala 文件。(注意,如果你使用 Java 开发,你将使用nbayes/src/main/java这样的路径。)接下来展示bayes.sbt文件的内容。pwdcatLinux 命令提醒你文件位置,并提示你查看文件内容。

nameversionscalaVersion选项设置项目详细信息及使用的 Scala 版本。libraryDependencies选项定义 Hadoop 和 Spark 库的位置。

[hadoop@hc2nn nbayes]$ pwd
/home/hadoop/spark/nbayes
[hadoop@hc2nn nbayes]$ cat bayes.sbt 
name := "Naive Bayes"
version := "1.0"
scalaVersion := "2.11.2"
libraryDependencies += "org.apache.hadoop" % "hadoop-client" % "2.8.1"
libraryDependencies += "org.apache.spark" %% "spark-core" % "2.6.0"
libraryDependencies += "org.apache.spark" %% "spark-mllib" % "2.1.1"

可以使用以下命令从nbayes子目录编译 Scala nbayes项目代码:

[hadoop@hc2nn nbayes]$ sbt compile

sbt compile命令用于将代码编译成类。这些类随后被放置在nbayes/target/scala-2.10/classes目录下。使用此命令可将编译后的类打包成 JAR 文件:

[hadoop@hc2nn nbayes]$ sbt package

sbt package命令将在nbayes/target/scala-2.10目录下创建一个 JAR 文件。如sbt 结构图所示例中,编译打包成功后,名为naive-bayes_2.10-1.0.jar的 JAR 文件已被创建。此 JAR 文件及其包含的类可通过spark-submit命令使用。随着对 Apache Spark MLlib 模块功能的探索,这将在后面描述。

使用朴素贝叶斯进行分类

本节将提供一个 Apache Spark MLlib 朴素贝叶斯算法的实际示例。它将阐述该算法的理论基础,并提供一个逐步的 Scala 示例,展示如何使用该算法。

分类理论

要使用朴素贝叶斯算法对数据集进行分类,数据必须是线性可分的;即数据中的类别必须能通过类别边界线性分割。下图通过三条数据集和两条虚线表示的类别边界直观解释了这一点:

朴素贝叶斯假设数据集内的特征(或维度)彼此独立;即它们互不影响。以下示例考虑将电子邮件分类为垃圾邮件。如果你有 100 封电子邮件,则执行以下操作:

60% of emails are spam
80% of spam emails contain the word buy
20% of spam emails don't contain the word buy
40% of emails are not spam
10% of non spam emails contain the word buy
90% of non spam emails don't contain the word buy

让我们将此示例转换为条件概率,以便朴素贝叶斯分类器可以识别:

P(Spam) = the probability that an email is spam = 0.6
P(Not Spam) = the probability that an email is not spam = 0.4
P(Buy|Spam) = the probability that an email that is spam has the word buy = 0.8
P(Buy|Not Spam) = the probability that an email that is not spam has the word buy = 0.1

包含单词“buy”的电子邮件是垃圾邮件的概率是多少?这可以写为P(Spam|Buy)。朴素贝叶斯表示,它由以下等式描述:

因此,使用之前的百分比数据,我们得到以下结果:

P(Spam|Buy) = ( 0.8 * 0.6 ) / (( 0.8 * 0.6 ) + ( 0.1 * 0.4 ) ) = ( .48 ) / ( .48 + .04 )

= .48 / .52 = .923

这意味着包含单词“buy”的电子邮件是垃圾邮件的可能性92%更高。以上是理论部分;现在是时候尝试一个使用 Apache Spark MLlib 朴素贝叶斯算法的真实示例了。

朴素贝叶斯实践

第一步是选择一些用于分类的数据。我们选择了英国政府数据网站上的一些数据,网址为data.gov.uk/dataset/road-accidents-safety-data

数据集名为道路安全 - 数字呼吸测试数据 2013,下载一个名为DigitalBreathTestData2013.txt的压缩文本文件。该文件包含大约五十万行。数据如下所示:

Reason,Month,Year,WeekType,TimeBand,BreathAlcohol,AgeBand,GenderSuspicion of Alcohol,Jan,2013,Weekday,12am-4am,75,30-39,MaleMoving Traffic Violation,Jan,2013,Weekday,12am-4am,0,20-24,MaleRoad Traffic Collision,Jan,2013,Weekend,12pm-4pm,0,20-24,Female

为了对数据进行分类,我们对列进行了修改...

使用 K-Means 进行聚类

本例将使用与前例相同的测试数据,但我们尝试使用 MLlib 的 K-Means 算法在数据中寻找簇。

聚类理论

K-Means 算法通过迭代尝试,通过最小化簇中心向量的均值与新候选簇成员向量之间的距离,来确定测试数据中的簇。以下等式假设数据集成员范围从X1Xn;同时也假设K个簇集合,范围从S1Sk,其中K <= n

K-Means 实践

MLlib 的 K-Means 功能使用LabeledPoint结构处理数据,因此需要数值输入数据。由于正在重复使用上一节的数据,我们将不再解释数据转换。本节中数据方面的唯一变化是,处理将在 HDFS 下的/data/spark/kmeans/目录进行。此外,K-Means 示例的转换 Scala 脚本生成的记录全部以逗号分隔。

为了将工作与其他开发分开,K-Means 示例的开发和处理已在/home/hadoop/spark/kmeans目录下进行。sbt配置文件现在称为kmeans.sbt,与上一个示例相同,只是项目名称不同:

name := "K-Means"

本节代码可在软件包的chapter7\K-Means目录下找到。因此,查看存储在kmeans/src/main/scala下的kmeans1.scala代码,会发现一些类似的操作。导入语句引用了 Spark 上下文和配置。然而,这一次,K-Means 功能是从 MLlib 导入的。此外,为了这个例子,应用程序类名已更改为kmeans1

import org.apache.spark.SparkContext
import org.apache.spark.SparkContext._
import org.apache.spark.SparkConf
import org.apache.spark.mllib.linalg.Vectors
import org.apache.spark.mllib.clustering.{KMeans,KMeansModel}

object kmeans1 extends App {

与上例相同,正在采取行动定义数据文件——定义 Spark 配置并创建 Spark 上下文:

 val hdfsServer = "hdfs://localhost:8020"
 val hdfsPath   = "/data/spark/kmeans/" 
 val dataFile   = hdfsServer + hdfsPath + "DigitalBreathTestData2013-MALE2a.csv"
 val sparkMaster = "spark://localhost:7077"
 val appName = "K-Means 1"
 val conf = new SparkConf()
 conf.setMaster(sparkMaster)
 conf.setAppName(appName)
 val sparkCxt = new SparkContext(conf)

接下来,从数据文件加载 CSV 数据,并通过逗号字符分割到VectorData变量中:

 val csvData = sparkCxt.textFile(dataFile)
 val VectorData = csvData.map {
   csvLine =>
     Vectors.dense( csvLine.split(',').map(_.toDouble))
 }

KMeans对象被初始化,并设置参数以定义簇的数量和确定它们的最大迭代次数:

 val kMeans = new KMeans
 val numClusters         = 3
 val maxIterations       = 50

为初始化模式、运行次数和 Epsilon 定义了一些默认值,这些值是我们进行 K-Means 调用所需的,但在处理过程中并未改变。最后,这些参数被设置到KMeans对象上:

 val initializationMode = KMeans.K_MEANS_PARALLEL
 val numRuns            = 1
 val numEpsilon         = 1e-4 
 kMeans.setK( numClusters )
 kMeans.setMaxIterations( maxIterations )
 kMeans.setInitializationMode( initializationMode )
 kMeans.setRuns( numRuns )
 kMeans.setEpsilon( numEpsilon )

我们缓存了训练向量数据以提高性能,并使用向量数据训练了KMeans对象,创建了一个经过训练的 K-Means 模型:

 VectorData.cache
 val kMeansModel = kMeans.run( VectorData )

我们计算了 K-Means 成本和输入数据行数,并通过println语句输出了结果。成本值表示簇的紧密程度以及簇之间的分离程度:

 val kMeansCost = kMeansModel.computeCost( VectorData ) 
 println( "Input data rows : " + VectorData.count() )
 println( "K-Means Cost   : " + kMeansCost )

接下来,我们使用 K-Means 模型打印出计算出的三个簇中每个簇的中心作为向量:

 kMeansModel.clusterCenters.foreach{ println }

最后,我们使用 K-Means 模型的预测函数来创建一个簇成员资格预测列表。然后,我们按值计数这些预测,以给出每个簇中数据点的计数。这显示了哪些簇更大,以及是否真的存在三个簇:

 val clusterRddInt = kMeansModel.predict( VectorData ) 
 val clusterCount = clusterRddInt.countByValue
  clusterCount.toList.foreach{ println }
} // end object kmeans1

因此,为了运行此应用程序,必须从kmeans子目录进行编译和打包,正如 Linux 的pwd命令所示:

[hadoop@hc2nn kmeans]$ pwd
/home/hadoop/spark/kmeans
[hadoop@hc2nn kmeans]$ sbt package
Loading /usr/share/sbt/bin/sbt-launch-lib.bash
[info] Set current project to K-Means (in build file:/home/hadoop/spark/kmeans/)
[info] Compiling 2 Scala sources to /home/hadoop/spark/kmeans/target/scala-2.10/classes...
[info] Packaging /home/hadoop/spark/kmeans/target/scala-2.10/k-means_2.10-1.0.jar ...
[info] Done packaging.
[success] Total time: 20 s, completed Feb 19, 2015 5:02:07 PM

一旦打包成功,我们检查 HDFS 以确保测试数据已就绪。如前例所示,我们使用软件包中提供的convert.scala文件将数据转换为数值形式。我们将处理 HDFS 目录/data/spark/kmeans中的DigitalBreathTestData2013-MALE2a.csv数据文件,如下所示:

[hadoop@hc2nn nbayes]$ hdfs dfs -ls /data/spark/kmeans
Found 3 items
-rw-r--r--   3 hadoop supergroup   24645166 2015-02-05 21:11 /data/spark/kmeans/DigitalBreathTestData2013-MALE2.csv
-rw-r--r--   3 hadoop supergroup   5694226 2015-02-05 21:48 /data/spark/kmeans/DigitalBreathTestData2013-MALE2a.csv
drwxr-xr-x   - hadoop supergroup         0 2015-02-05 21:46 /data/spark/kmeans/result

使用spark-submit工具运行 K-Means 应用程序。此命令中唯一的更改是类名现在是kmeans1

spark-submit \
 --class kmeans1 \
 --master spark://localhost:7077 \
 --executor-memory 700M \
 --total-executor-cores 100 \
 /home/hadoop/spark/kmeans/target/scala-2.10/k-means_2.10-1.0.jar

来自 Spark 集群运行的输出显示如下:

Input data rows : 467054
K-Means Cost   : 5.40312223450789E7

先前的输出显示了输入数据量,看起来是正确的;它还显示了K-Means 成本值。该成本基于内部总和平方误差(WSSSE),基本上给出了找到的簇质心与数据点分布匹配程度的度量。匹配得越好,成本越低。以下链接datasciencelab.wordpress.com/2013/12/27/finding-the-k-in-k-means-clustering/更详细地解释了 WSSSE 以及如何找到一个好的k值。

接下来是三个向量,它们描述了具有正确维数的数据簇中心。请记住,这些簇质心向量将具有与原始向量数据相同的列数:

[0.24698249738061878,1.3015883142472253,0.005830116872250263,2.9173747788555207,1.156645130895448,3.4400290524342454] 
[0.3321793984152627,1.784137241326256,0.007615970459266097,2.5831987075928917,119.58366028156011,3.8379106085083468] 
[0.25247226760684494,1.702510963969387,0.006384899819416975,2.231404248000688,52.202897927594805,3.551509158139135]

最后,给出了 1 至 3 簇的簇成员资格,其中簇 1(索引 0)拥有最大的成员资格,有407539个成员向量:

(0,407539)
(1,12999)
(2,46516)

因此,这两个示例展示了如何使用朴素贝叶斯和 K-Means 对数据进行分类和聚类。如果我想对图像或更复杂的模式进行分类,并使用黑盒方法进行分类呢?下一节将探讨基于 Spark 的分类,使用ANNs,即人工神经网络

人工神经网络

下图左侧展示了一个简单的生物神经元。该神经元具有接收其他神经元信号的树突。细胞体控制激活,轴突将电脉冲传递到其他神经元的树突。右侧的人工神经元有一系列加权输入:一个汇总函数,将输入分组,以及一个触发机制F(Net)),该机制决定输入是否达到阈值,如果是,则神经元将触发:

神经网络对噪声图像和失真具有容忍度,因此在需要潜在的...黑盒分类方法时非常有用。

ANN 实践

为了开始 ANN 训练,需要测试数据。鉴于这种分类方法应该擅长分类扭曲或噪声图像,我们决定在这里尝试对图像进行分类:

它们是手工制作的文本文件,包含由 1 和 0 组成的形状块。当存储在 HDFS 上时,回车符会被移除,使得图像呈现为单行向量。因此,ANN 将对一系列形状图像进行分类,然后与添加了噪声的相同图像进行测试,以确定分类是否仍然有效。有六张训练图像,每张图像将被赋予一个从 0.1 到 0.6 的任意训练标签。因此,如果 ANN 呈现一个闭合的正方形,它应该返回标签 0.1。下图展示了一个带有噪声的测试图像示例。

通过在图像内添加额外的零(0)字符创建的噪声已被突出显示:

与之前一样,ANN 代码是在 Linux Hadoop 账户下的spark/ann子目录中开发的。ann.sbt文件位于ann目录中:

[hadoop@hc2nn ann]$ pwd
/home/hadoop/spark/ann

[hadoop@hc2nn ann]$ ls
ann.sbt   project src target

ann.sbt文件的内容已更改,以使用 Spark 依赖项 JAR 库文件的完整路径:

name := "A N N"
version := "1.0"
scalaVersion := "2.11.2"
libraryDependencies += "org.apache.hadoop" % "hadoop-client" % "2.8.1"
libraryDependencies += "org.apache.spark" % "spark-core" % "2.6.0"
libraryDependencies += "org.apache.spark" % "spark-mllib" % "2.1.1"
libraryDependencies += "org.apache.spark" % "akka" % "2.5.3"

如前例所示,实际要编译的 Scala 代码存放在名为src/main/scala的子目录中。我们创建了两个 Scala 程序。第一个程序使用输入数据进行训练,然后用同一输入数据测试 ANN 模型。第二个程序则用噪声数据测试已训练模型的扭曲数据分类能力:

[hadoop@hc2nn scala]$ pwd
/home/hadoop/spark/ann/src/main/scala 
[hadoop@hc2nn scala]$ ls
test_ann1.scala test_ann2.scala

我们将检查第一个 Scala 文件,然后仅展示第二个文件的额外特性,因为两个示例在训练 ANN 之前非常相似。此处展示的代码示例可在本书提供的软件包中的路径chapter2\ANN下找到。因此,要检查第一个 Scala 示例,导入语句与前例类似。正在导入 Spark 上下文、配置、向量和LabeledPoint。这次还导入了用于 RDD 处理的RDD类以及新的 ANN 类ANNClassifier。请注意,MLlib/分类例程广泛使用LabeledPoint结构作为输入数据,该结构将包含要训练的特征和标签:

import org.apache.spark.SparkContext
import org.apache.spark.SparkContext._
import org.apache.spark.SparkConf 
import org.apache.spark.mllib.classification.ANNClassifier
import org.apache.spark.mllib.regression.LabeledPoint
import org.apache.spark.mllib.linalg.Vectors
import org.apache.spark.mllib.linalg._
import org.apache.spark.rdd.RDD 

object testann1 extends App {

本例中的应用程序类名为testann1。要处理的 HDFS 文件已根据 HDFS 的serverpath和文件名定义:

 val server = "hdfs://localhost:8020"
 val path   = "/data/spark/ann/"

 val data1 = server + path + "close_square.img"
 val data2 = server + path + "close_triangle.img"
 val data3 = server + path + "lines.img"
 val data4 = server + path + "open_square.img"
 val data5 = server + path + "open_triangle.img"
 val data6 = server + path + "plus.img"

Spark 上下文已使用 Spark 实例的 URL 创建,现在端口号不同——8077。应用程序名称为ANN 1。当应用程序运行时,这将在 Spark Web UI 上显示:

 val sparkMaster = "spark://localhost:8077"
 val appName = "ANN 1"
 val conf = new SparkConf()

 conf.setMaster(sparkMaster)
 conf.setAppName(appName)

 val sparkCxt = new SparkContext(conf)

基于 HDFS 的输入训练和测试数据文件被加载。每行上的值通过空格字符分割,数值已转换为双精度数。包含此数据的变量随后存储在一个名为inputs的数组中。同时,创建了一个名为 outputs 的数组,包含从0.10.6的标签。这些值将用于对输入模式进行分类:

 val rData1 = sparkCxt.textFile(data1).map(_.split(" ").map(_.toDouble)).collect
 val rData2 = sparkCxt.textFile(data2).map(_.split(" ").map(_.toDouble)).collect
 val rData3 = sparkCxt.textFile(data3).map(_.split(" ").map(_.toDouble)).collect
 val rData4 = sparkCxt.textFile(data4).map(_.split(" ").map(_.toDouble)).collect
 val rData5 = sparkCxt.textFile(data5).map(_.split(" ").map(_.toDouble)).collect
 val rData6 = sparkCxt.textFile(data6).map(_.split(" ").map(_.toDouble)).collect 
 val inputs = Array[Array[Double]] (
     rData1(0), rData2(0), rData3(0), rData4(0), rData5(0), rData6(0) ) 
 val outputs = ArrayDouble

代表输入数据特征和标签的输入和输出数据随后被合并并转换为LabeledPoint结构。最后,数据被并行化,以便为最佳并行处理进行分区:

 val ioData = inputs.zip( outputs )
 val lpData = ioData.map{ case(features,label) =>

   LabeledPoint( label, Vectors.dense(features) )
 }
 val rddData = sparkCxt.parallelize( lpData )

变量用于定义人工神经网络(ANN)的隐藏层拓扑结构。在此例中,我们选择了两个隐藏层,每层各有 100 个神经元。同时定义了最大迭代次数、批次大小(六个模式)以及收敛容差。容差指的是训练误差达到多大时,我们可以认为训练已经成功。接着,根据这些配置参数和输入数据创建了一个 ANN 模型:

 val hiddenTopology : Array[Int] = Array( 100, 100 )
 val maxNumIterations = 1000
 val convTolerance   = 1e-4
 val batchSize       = 6
 val annModel = ANNClassifier.train(rddData,
                                    batchSize,
                                    hiddenTopology,
                                    maxNumIterations,
                                    convTolerance)

为了测试已训练的 ANN 模型,使用相同的输入训练数据作为测试数据以获取预测标签。首先,创建一个名为rPredictData的输入数据变量。然后,数据被分区,并最终使用已训练的 ANN 模型获取预测结果。为了使该模型工作,它必须输出标签,即0.10.6

 val rPredictData = inputs.map{ case(features) => 
   ( Vectors.dense(features) )
 }
 val rddPredictData = sparkCxt.parallelize( rPredictData )
 val predictions = annModel.predict( rddPredictData )

打印标签预测结果,脚本以闭合括号结束:

 predictions.toArray().foreach( value => println( "prediction > " + value ) )
} // end ann1

因此,要运行此代码示例,首先必须对其进行编译和打包。至此,您应该已经熟悉了从ann子目录执行的sbt命令:

[hadoop@hc2nn ann]$ pwd
/home/hadoop/spark/ann
[hadoop@hc2nn ann]$ sbt package

然后,在新spark/spark路径内使用新的基于 Spark 的 URL(端口8077)运行应用程序testann1,使用spark-submit命令:

/home/hadoop/spark/spark/bin/spark-submit \
 --class testann1 \
 --master spark://localhost:8077 \
 --executor-memory 700M \
 --total-executor-cores 100 \
 /home/hadoop/spark/ann/target/scala-2.10/a-n-n_2.10-1.0.jar

通过访问 Apache Spark 网页 URL http://localhost:19080/,现在可以看到应用程序正在运行。下图显示了ANN 1应用程序的运行情况以及先前完成的执行:

通过选择其中一个集群主机工作实例,可以看到实际执行集群处理的工作实例的执行程序列表:

最后,通过选择其中一个执行程序,可以看到其历史和配置,以及到日志文件和错误信息的链接。在这一级别,借助提供的日志信息,可以进行调试。可以检查这些日志文件以处理错误消息:

ANN 1应用程序提供了以下输出,以显示它已正确地对相同输入数据进行了重新分类。重新分类成功,因为每个输入模式都被赋予了与其训练时相同的标签:

prediction > 0.1
prediction > 0.2
prediction > 0.3
prediction > 0.4
prediction > 0.5
prediction > 0.6

这表明 ANN 训练和测试预测将适用于相同的数据。现在,我们将使用相同的数据进行训练,但测试时使用扭曲或含噪声的数据,我们已展示了一个示例。该示例可在软件包中的test_ann2.scala文件中找到。它与第一个示例非常相似,因此我们将仅展示更改的代码。该应用程序现在称为testann2

object testann2 extends App

在 ANN 模型使用训练数据创建后,会生成一组额外的测试数据。此测试数据包含噪声:

 val tData1 = server + path + "close_square_test.img"
 val tData2 = server + path + "close_triangle_test.img"
 val tData3 = server + path + "lines_test.img"
 val tData4 = server + path + "open_square_test.img"
 val tData5 = server + path + "open_triangle_test.img"
 val tData6 = server + path + "plus_test.img"

此数据被处理成输入数组并分区以供集群处理:

 val rtData1 = sparkCxt.textFile(tData1).map(_.split(" ").map(_.toDouble)).collect
 val rtData2 = sparkCxt.textFile(tData2).map(_.split(" ").map(_.toDouble)).collect
 val rtData3 = sparkCxt.textFile(tData3).map(_.split(" ").map(_.toDouble)).collect
 val rtData4 = sparkCxt.textFile(tData4).map(_.split(" ").map(_.toDouble)).collect
 val rtData5 = sparkCxt.textFile(tData5).map(_.split(" ").map(_.toDouble)).collect
 val rtData6 = sparkCxt.textFile(tData6).map(_.split(" ").map(_.toDouble)).collect 
 val tInputs = Array[Array[Double]] (
     rtData1(0), rtData2(0), rtData3(0), rtData4(0), rtData5(0), rtData6(0) )

 val rTestPredictData = tInputs.map{ case(features) => ( Vectors.dense(features) ) }
 val rddTestPredictData = sparkCxt.parallelize( rTestPredictData )

它随后以与第一个示例相同的方式生成标签预测。如果模型正确分类数据,则应从0.10.6打印相同的标签值:

 val testPredictions = annModel.predict( rddTestPredictData )
 testPredictions.toArray().foreach( value => println( "test prediction > " + value ) )

代码已经编译完成,因此可以使用spark-submit命令运行:

/home/hadoop/spark/spark/bin/spark-submit \
 --class testann2 \
 --master spark://localhost:8077 \
 --executor-memory 700M \
 --total-executor-cores 100 \
 /home/hadoop/spark/ann/target/scala-2.10/a-n-n_2.10-1.0.jar

本脚本的集群输出显示了使用训练好的 ANN 模型对一些噪声测试数据进行成功分类的情况。噪声数据已被正确分类。例如,如果训练模型出现混淆,它可能会对位置一的噪声close_square_test.img测试图像给出 0.15 的值,而不是像实际那样返回0.1

test prediction > 0.1
test prediction > 0.2
test prediction > 0.3
test prediction > 0.4
test prediction > 0.5
test prediction > 0.6

总结

本章试图为你概述 Apache Spark MLlib 模块中可用的一些功能。它还展示了即将在 ANNs 或人工神经网络方面可用的功能。你可能会对 ANNs 的工作效果印象深刻。由于时间和篇幅限制,本章无法涵盖 MLlib 的所有领域。此外,我们现在希望在下一章中更多地关注 SparkML 库,该库通过支持 DataFrames 以及底层 Catalyst 和 Tungsten 优化来加速机器学习。

我们学习了如何开发基于 Scala 的示例,用于朴素贝叶斯分类、K-Means 聚类和 ANNs。你了解了如何准备测试...

第五章:Apache SparkML

既然你已经学了很多关于 MLlib 的知识,为什么还需要另一个 ML API 呢?首先,在数据科学中,与多个框架和 ML 库合作是一项常见任务,因为它们各有优劣;大多数情况下,这是性能和功能之间的权衡。例如,R 在功能方面是王者——存在超过 6000 个 R 附加包。然而,R 也是数据科学执行环境中最慢的之一。另一方面,SparkML 目前功能相对有限,但却是速度最快的库之一。为什么会这样呢?这引出了 SparkML 存在的第二个原因。

RDD 与 DataFrames 和 Datasets 之间的二元性就像本书中的一条红线,并且不断影响着机器学习章节。由于 MLlib 设计为在 RDD 之上工作,SparkML 在 DataFrames 和 Datasets 之上工作,因此利用了 Catalyst 和 Tungsten 带来的所有新性能优势。

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

  • SparkML API 简介

  • 管道概念

  • 转换器和估计器

  • 一个工作示例

新 API 是什么样的?

在 Apache Spark 上进行机器学习时,我们习惯于在将数据实际输入算法之前将其转换为适当的格式和数据类型。全球的机器学习实践者发现,机器学习项目中的预处理任务通常遵循相同的模式:

  • 数据准备

  • 训练

  • 评估

  • 超参数调整

因此,新的 ApacheSparkML API 原生支持这一过程。它被称为 管道,灵感来源于 scikit-learn scikit-learn.org,一个非常流行的 Python 编程语言机器学习库。中央数据结构是 DataFrame,所有操作都在其上运行。

管道概念

ApacheSparkML 管道包含以下组件:

  • DataFrame:这是中央数据存储,所有原始数据和中间结果都存储于此。

  • 转换器:顾名思义,转换器通过在大多数情况下添加额外的(特征)列将一个 DataFrame 转换为另一个。转换器是无状态的,这意味着它们没有任何内部内存,每次使用时行为完全相同;这个概念在使用 RDD 的 map 函数时你可能已经熟悉。

  • 估计器:在大多数情况下,估计器是一种机器学习模型。与转换器不同,估计器包含内部状态表示,并且高度依赖于它已经见过的数据历史。

  • 管道:这是将前面提到的组件——DataFrame、Transformer 和 Estimator——粘合在一起的胶水。

  • 参数:机器学习算法有许多可调整的旋钮。这些被称为超参数,而机器学习算法为了拟合数据所学习的值被称为参数。通过标准化超参数的表达方式,ApacheSparkML 为任务自动化打开了大门,正如我们稍后将看到的。

变压器

让我们从简单的事情开始。机器学习数据准备中最常见的任务之一是对分类值进行字符串索引和独热编码。让我们看看这是如何完成的。

字符串索引器

假设我们有一个名为df的 DataFrame,其中包含一个名为 color 的分类标签列——红色、绿色和蓝色。我们希望将它们编码为整数或浮点值。这时org.apache.spark.ml.feature.StringIndexer就派上用场了。它会自动确定类别集的基数,并为每个类别分配一个唯一值。所以在我们的例子中,一个类别列表,如红色、红色、绿色、红色、蓝色、绿色,应该被转换为 1、1、2、1、3、2:

import org.apache.spark.ml.feature.StringIndexer
var indexer = new StringIndexer()
  .setInputCol("colors")
  .setOutputCol("colorsIndexed")

var indexed = indexer.fit(df).transform(df)

此转换的结果是一个名为 indexed 的 DataFrame,除了字符串类型的颜色列外,现在还包含一个名为colorsIndexed的 double 类型列。

独热编码器

我们仅进行了一半。尽管机器学习算法能够利用colorsIndexed列,但如果我们对其进行独热编码,它们的表现会更好。这意味着,与其拥有一个包含 1 到 3 之间标签索引的colorsIndexed列,不如我们拥有三个列——每种颜色一个——并规定每行只允许将其中一个列设置为 1,其余为 0。让我们这样做:

var encoder = new OneHotEncoder()  .setInputCol("colorIndexed")  .setOutputCol("colorVec")var encoded = encoder.transform(indexed)

直观上,我们期望在编码后的 DataFrame 中得到三个额外的列,例如,colorIndexedRedcolorIndexedGreencolorIndexedBlue...

向量汇编器

在我们开始实际的机器学习算法之前,我们需要应用最后一个转换。我们必须创建一个额外的特征列,其中包含我们希望机器学习算法考虑的所有列的信息。这是通过org.apache.spark.ml.feature.VectorAssembler如下完成的:

import org.apache.spark.ml.feature.VectorAssembler
vectorAssembler = new VectorAssembler()
        .setInputCols(Array("colorVec", "field2", "field3","field4"))
        .setOutputCol("features")

这个转换器只为结果 DataFrame 添加了一个名为features的列,该列的类型为org.apache.spark.ml.linalg.Vector。换句话说,这个由VectorAssembler创建的新列 features 包含了我们定义的所有列(在这种情况下,colorVecfield2field3field4),每行编码在一个向量对象中。这是 Apache SparkML 算法所喜欢的格式。

管道

在我们深入了解估计器之前——我们已经在StringIndexer中使用过一个——让我们首先理解管道的概念。你可能已经注意到,转换器只向 DataFrame 添加一个单一列,并且基本上省略了所有未明确指定为输入列的其他列;它们只能与org.apache.spark.ml.Pipeline一起使用,后者将单个转换器(和估计器)粘合在一起,形成一个完整的数据分析过程。因此,让我们为我们的两个Pipeline阶段执行此操作:

var transformers = indexer :: encoder :: vectorAssembler :: Nilvar pipeline = new Pipeline().setStages(transformers).fit(df)var transformed = pipeline.transform(df)

现在得到的 DataFrame 称为transformed,包含所有...

估计器

我们在StringIndexer中已经使用过估计器。我们已经说过,估计器在查看数据时会改变其状态,而转换器则不会。那么为什么StringIndexer是估计器呢?这是因为它需要记住所有先前见过的字符串,并维护字符串和标签索引之间的映射表。

在机器学习中,通常至少使用可用的训练数据的一个训练和测试子集。在管道中的估计器(如StringIndexer)在查看训练数据集时可能没有看到所有的字符串标签。因此,当你使用测试数据集评估模型时,StringIndexer现在遇到了它以前未见过的标签,你会得到一个异常。实际上,这是一个非常罕见的情况,基本上可能意味着你用来分离训练和测试数据集的样本函数不起作用;然而,有一个名为setHandleInvalid("skip")的选项,你的问题就解决了。

区分估计器和转换器的另一种简单方法是查看估计器上是否有额外的fit方法。实际上,fit 方法会根据给定数据集填充估计器的内部数据管理结构,在StringIndexer的情况下,这是标签字符串和标签索引之间的映射表。现在让我们来看另一个估计器,一个实际的机器学习算法。

RandomForestClassifier

假设我们处于二分类问题设置中,并希望使用RandomForestClassifier。所有 SparkML 算法都有一个兼容的 API,因此它们可以互换使用。所以使用哪个并不重要,但RandomForestClassifier比更简单的模型如逻辑回归有更多的(超)参数。在稍后的阶段,我们将使用(超)参数调整,这也是 Apache SparkML 内置的。因此,使用一个可以调整更多参数的算法是有意义的。将这种二分类器添加到我们的Pipeline中非常简单:

import org.apache.spark.ml.classification.RandomForestClassifiervar rf = new RandomForestClassifier()   .setLabelCol("label") .setFeaturesCol("features") ...

模型评估

如前所述,模型评估是 ApacheSparkML 内置的,你会在org.apache.spark.ml.evaluation包中找到所需的一切。让我们继续进行二分类。这意味着我们将不得不使用org.apache.spark.ml.evaluation.BinaryClassificationEvaluator

import org.apache.spark.ml.evaluation.BinaryClassificationEvaluator
val evaluator = new BinaryClassificationEvaluator()

import org.apache.spark.ml.param.ParamMap
var evaluatorParamMap = ParamMap(evaluator.metricName -> "areaUnderROC")
var aucTraining = evaluator.evaluate(result, evaluatorParamMap)

为了编码,之前初始化了一个二元分类评估器函数,并告诉它计算ROC 曲线下面积,这是评估机器学习算法预测性能的众多可能指标之一。

由于我们在名为结果的数据框中同时拥有实际标签和预测,因此计算此分数很简单,使用以下代码行完成:

var aucTraining = evaluator.evaluate(result, evaluatorParamMap)

交叉验证和超参数调整

我们将分别看一个交叉验证和超参数调整的例子。让我们来看看交叉验证

交叉验证

如前所述,我们使用了机器学习算法的默认参数,我们不知道它们是否是好的选择。此外,与其简单地将数据分为训练集和测试集,或训练集、测试集和验证集,交叉验证可能是一个更好的选择,因为它确保最终所有数据都被机器学习算法看到。

交叉验证基本上将你全部可用的训练数据分成若干个k折。这个参数k可以指定。然后,整个流水线对每一折运行一次,并为每一折训练一个机器学习模型。最后,通过分类器的投票方案或回归的平均方法将得到的各种机器学习模型合并。

下图说明了十折交叉验证

超参数调整

交叉验证通常与所谓的(超)参数调整结合使用。什么是超参数?这些是你可以在你的机器学习算法上调整的各种旋钮。例如,以下是随机森林分类器的一些参数:

  • 树的数量

  • 特征子集策略

  • 不纯度

  • 最大箱数

  • 最大树深度

设置这些参数可能会对训练出的分类器的性能产生重大影响。通常,没有明确的方案来选择它们——当然,经验有帮助——但超参数调整被视为黑魔法。我们不能只选择许多不同的参数并测试预测性能吗?当然可以。这个功能...

使用 Apache SparkML 赢得 Kaggle 竞赛

赢得 Kaggle 竞赛本身就是一门艺术,但我们只是想展示如何有效地使用 Apache SparkML 工具来做到这一点。

我们将使用博世公司提供的一个存档竞赛来进行这个操作,博世是一家德国跨国工程和电子公司,关于生产线性能数据。竞赛数据的详细信息可以在www.kaggle.com/c/bosch-production-line-performance/data找到。

数据准备

挑战数据以三个 ZIP 包的形式提供,但我们只使用其中两个。一个包含分类数据,一个包含连续数据,最后一个包含测量时间戳,我们暂时忽略它。

如果你提取数据,你会得到三个大型 CSV 文件。因此,我们首先要做的是将它们重新编码为 parquet,以便更节省空间:

def convert(filePrefix : String) = {   val basePath = "yourBasePath"   var df = spark              .read              .option("header",true)              .option("inferSchema", "true")              .csv("basePath+filePrefix+".csv")    df = df.repartition(1)    df.write.parquet(basePath+filePrefix+".parquet")}convert("train_numeric")convert("train_date")convert("train_categorical")

首先,我们定义一个函数...

特征工程

现在,是时候运行第一个转换器(实际上是估计器)了。它是StringIndexer,需要跟踪字符串和索引之间的内部映射表。因此,它不是转换器,而是估计器:

import org.apache.spark.ml.feature.{OneHotEncoder, StringIndexer}

var indexer = new StringIndexer()
  .setHandleInvalid("skip")
  .setInputCol("L0_S22_F545")
  .setOutputCol("L0_S22_F545Index")

var indexed = indexer.fit(df_notnull).transform(df_notnull)
indexed.printSchema

如图所示,已创建一个名为L0_S22_F545Index的附加列:

最后,让我们检查新创建列的一些内容,并与源列进行比较。

我们可以清楚地看到类别字符串是如何转换为浮点索引的:

现在,我们想要应用OneHotEncoder,这是一个转换器,以便为我们的机器学习模型生成更好的特征:

var encoder = new OneHotEncoder()
  .setInputCol("L0_S22_F545Index")
  .setOutputCol("L0_S22_F545Vec")

var encoded = encoder.transform(indexed)

如图所示,新创建的列L0_S22_F545Vec包含org.apache.spark.ml.linalg.SparseVector对象,这是一种稀疏向量的压缩表示:

稀疏向量表示OneHotEncoder与其他许多算法一样,返回一个org.apache.spark.ml.linalg.SparseVector类型的稀疏向量,根据定义,向量中只有一个元素可以为 1,其余必须保持为 0。这为压缩提供了大量机会,因为只需知道非零元素的位置即可。Apache Spark 使用以下格式的稀疏向量表示:(l,[p],[v]),其中l代表向量长度,p代表位置(这也可以是位置数组),v代表实际值(这可以是值数组)。因此,如果我们得到(13,[10],[1.0]),如我们之前的例子所示,实际的稀疏向量看起来是这样的:(0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0)。

现在,我们的特征工程已完成,我们想要创建一个包含机器学习器所需所有必要列的总体稀疏向量。这是通过使用VectorAssembler完成的:

import org.apache.spark.ml.feature.VectorAssembler
import org.apache.spark.ml.linalg.Vectors

var vectorAssembler = new VectorAssembler()
        .setInputCols(Array("L0_S22_F545Vec", "L0_S0_F0", "L0_S0_F2","L0_S0_F4"))
        .setOutputCol("features")

var assembled = vectorAssembler.transform(encoded)

我们基本上只需定义列名列表和目标列,其余工作将自动完成:

由于features列的视图有些压缩,让我们更详细地检查特征字段的一个实例:

我们可以清楚地看到,我们处理的是一个长度为 16 的稀疏向量,其中位置 0、13、14 和 15 是非零的,并包含以下值:1.00.03-0.034-0.197。完成!让我们用这些组件创建一个Pipeline

测试特征工程管道

让我们用我们的转换器和估计器创建一个Pipeline

import org.apache.spark.ml.Pipelineimport org.apache.spark.ml.PipelineModel//Create an array out of individual pipeline stagesvar transformers = Array(indexer,encoder,assembled)var pipeline = new Pipeline().setStages(transformers).fit(df_notnull)var transformed = pipeline.transform(df_notnull)

请注意,PipelinesetStages方法仅期望一个由transformersestimators组成的数组,这些我们之前已经创建。由于Pipeline的部分包含估计器,我们必须先对我们的DataFrame运行fit。得到的Pipeline对象在transform方法中接受一个DataFrame,并返回转换的结果:

正如预期的,...

训练机器学习模型

现在是时候向Pipeline添加另一个组件了:实际的机器学习算法——随机森林:

import org.apache.spark.ml.classification.RandomForestClassifier
var rf = new RandomForestClassifier() 
  .setLabelCol("label")
  .setFeaturesCol("features")

var model = new Pipeline().setStages(transformers :+ rf).fit(df_notnull)

var result = model.transform(df_notnull)

这段代码非常直接。首先,我们必须实例化我们的算法,并将其作为引用获取到rf中。我们可以为模型设置额外的参数,但我们将稍后在CrossValidation步骤中以自动化方式进行。然后,我们只需将阶段添加到我们的Pipeline,拟合它,并最终转换。fit方法,除了运行所有上游阶段外,还调用RandomForestClassifier上的拟合以训练它。训练好的模型现在包含在Pipeline中,transform方法实际上创建了我们的预测列:

正如我们所见,我们现在获得了一个名为 prediction 的额外列,其中包含RandomForestClassifier模型的输出。当然,我们仅使用了可用特征/列的一个非常有限的子集,并且尚未调整模型,因此我们不期望表现很好;但是,让我们看看如何使用 Apache SparkML 轻松评估我们的模型。

模型评估

没有评估,模型一文不值,因为我们不知道它的准确性如何。因此,我们现在将使用内置的BinaryClassificationEvaluator来评估预测性能,并使用一个广泛使用的度量标准,称为areaUnderROC(深入探讨这一点超出了本书的范围):

import org.apache.spark.ml.evaluation.BinaryClassificationEvaluatorval evaluator = new BinaryClassificationEvaluator()import org.apache.spark.ml.param.ParamMapvar evaluatorParamMap = ParamMap(evaluator.metricName -> "areaUnderROC")var aucTraining = evaluator.evaluate(result, evaluatorParamMap)

正如我们所见,有一个内置类名为org.apache.spark.ml.evaluation.BinaryClassificationEvaluator,还有其他一些...

交叉验证与超参数调整

如前所述,机器学习中的一个常见步骤是使用测试数据对训练数据进行交叉验证,并调整机器学习算法的旋钮。让我们使用 Apache SparkML 来自动完成这一过程!

首先,我们必须配置参数映射和CrossValidator

import org.apache.spark.ml.tuning.{CrossValidator, ParamGridBuilder}
var paramGrid = new ParamGridBuilder()
    .addGrid(rf.numTrees, 3 :: 5 :: 10 :: 30 :: 50 :: 70 :: 100 :: 150 :: Nil)
    .addGrid(rf.featureSubsetStrategy, "auto" :: "all" :: "sqrt" :: "log2" :: "onethird" :: Nil)
    .addGrid(rf.impurity, "gini" :: "entropy" :: Nil)    
    .addGrid(rf.maxBins, 2 :: 5 :: 10 :: 15 :: 20 :: 25 :: 30 :: Nil)
    .addGrid(rf.maxDepth, 3 :: 5 :: 10 :: 15 :: 20 :: 25 :: 30 :: Nil)
    .build()

var crossValidator = new CrossValidator()
      .setEstimator(new Pipeline().setStages(transformers :+ rf))
      .setEstimatorParamMaps(paramGrid)
      .setNumFolds(5)
.setEvaluator(evaluator)
var crossValidatorModel = crossValidator.fit(df_notnull)
var newPredictions = crossValidatorModel.transform(df_notnull)

org.apache.spark.ml.tuning.ParamGridBuilder用于定义CrossValidator需要在其中搜索的超参数空间,而org.apache.spark.ml.tuning.CrossValidator则接收我们的Pipeline、随机森林分类器的超参数空间以及CrossValidation的折数作为参数。现在,按照惯例,我们只需对CrossValidator调用 fit 和 transform 方法,它就会基本运行我们的Pipeline多次,并返回一个表现最佳的模型。你知道训练了多少个不同的模型吗?我们有 5 折的CrossValidation和 5 维超参数空间基数在 2 到 8 之间,所以让我们计算一下:5 * 8 * 5 * 2 * 7 * 7 = 19600 次!

使用评估器来评估经过交叉验证和调优的模型的质量

既然我们已经以全自动方式优化了Pipeline,接下来让我们看看如何获得最佳模型:

var bestPipelineModel = crossValidatorModel.bestModel.asInstanceOf[PipelineModel]    var stages = bestPipelineModel.stagesimport org.apache.spark.ml.classification.RandomForestClassificationModel    val rfStage = stages(stages.length-1).asInstanceOf[RandomForestClassificationModel]rfStage.getNumTreesrfStage.getFeatureSubsetStrategyrfStage.getImpurityrfStage.getMaxBinsrfStage.getMaxDepth

crossValidatorModel.bestModel代码基本上返回了最佳Pipeline。现在我们使用bestPipelineModel.stages来获取各个阶段,并获得经过调优的RandomForestClassificationModel ...

总结

你已经了解到,正如在许多其他领域一样,引入DataFrames促进了互补框架的发展,这些框架不再直接使用 RDDs。机器学习领域亦是如此,但还有更多内容。Pipeline实际上将 Apache Spark 中的机器学习提升到了一个新的水平,极大地提高了数据科学家的生产力。

所有中间对象之间的兼容性以及精心设计的概念简直令人惊叹。太棒了!最后,我们将讨论的概念应用于来自 Kaggle 竞赛的真实数据集,这对于你自己的 Apache SparkML 机器学习项目来说是一个非常好的起点。下一章将介绍 Apache SystemML,这是一个第三方机器学习库,用于 Apache Spark。让我们看看它为何有用以及与 SparkML 的区别。

第六章:Apache SystemML

到目前为止,我们只涵盖了 Apache Spark 标准发行版附带的组件(当然,除了 HDFS、Kafka 和 Flume)。然而,Apache Spark 也可以作为第三方组件的运行时,使其成为某种大数据应用的操作系统。在本章中,我们将介绍最初由IBM Almaden Research Lab在加利福尼亚开发的 Apache SystemML,这是一项令人惊叹的技术。Apache SystemML 经历了许多转变阶段,现在已成为 Apache 顶级项目。

在本章中,我们将探讨以下主题,以深入了解该主题:

  • 在 Apache Spark 之上使用 SystemML 开发您自己的机器学习应用

  • 学习...

为什么我们需要另一个库?

为了回答这个问题,我们需要了解 SystemML 的历史,该历史始于 2007 年,作为IBM Almaden Research Lab在加利福尼亚的一个研究项目。该项目旨在改善数据科学家的工作流程,特别是那些希望改进和增强现有机器学习算法功能的人。

因此,SystemML是一种声明性标记语言,能够透明地在 Apache Spark 上分发工作。它支持通过多线程和 CPU 上的 SIMD 指令以及 GPU 进行 Scale-up,以及通过集群进行 Scale-out,当然,两者可以同时进行。

最后,有一个基于成本的优化器,它生成考虑数据集大小统计信息的低级执行计划。换句话说,Apache SystemML之于机器学习,正如 Catalyst 和 Tungsten 之于 DataFrames。

为何基于 Apache Spark?

Apache Spark 解决了数据处理和机器学习中的许多常见问题,因此 Apache SystemML 可以利用这些功能。例如,Apache Spark 支持在通用 RDD 结构之上统一 SQL、图形、流和机器学习数据处理。

换言之,它是一个支持惰性求值和分布式内存缓存的通用DAG有向无环图)执行引擎。

Apache SystemML 的历史

Apache SystemML 已有十年历史。当然,它经历了多次重构,现已成为世界上最先进、最快的机器学习库之一。

如前图所示,针对 Apache SystemML 进行了大量研究。它比 Apache Spark 早两年,并在 2017 年成为 Apache 顶级项目,脱离孵化器状态。甚至在 SystemML 启动之初,IBM Research Almaden的研究人员就意识到,通常情况下,开箱即用的机器学习算法在大数据集上表现非常糟糕。

因此,数据分析管道在经过小规模原型调整后必须进行优化。下图说明了这一点:

这意味着数据科学家将在他选择的编程语言中设计他的应用程序,最可能是 Matlab、R 或 Python,最终,系统程序员将接手这个工作,并将其重新实现为 JVM 语言,如 Java 或 Scala,这通常会提供更好的性能,并且也能在数据并行框架如 Apache Spark 上进行线性扩展。

原型的缩放版本将在整个数据集上返回结果,数据科学家再次负责修改原型,整个循环再次开始。不仅 IBM Almaden 研究中心的员工经历过这种情况,我们的团队也见证了这一点。因此,让我们使系统程序员变得多余(或者至少只需要他来处理我们的 Apache Spark 作业),使用 Apache SystemML。

机器学习算法的成本优化器

让我们从一个例子开始,来说明 Apache SystemML 内部是如何工作的。考虑一个推荐系统。

一个例子 - 交替最小二乘法

推荐系统试图根据其他用户的历史记录预测用户可能感兴趣的潜在商品。

因此,让我们考虑一个所谓的商品-用户或产品-客户矩阵,如图所示:

这是一个所谓的稀疏矩阵,因为只有少数单元格填充了非零值,表示客户i和产品j之间的匹配。要么在单元格中放置一个,要么放置任何其他数值,例如,表示购买的产品数量或客户i对特定产品j的评分。我们称这个矩阵为r[ui],其中u代表用户,i代表商品。

熟悉线性代数的你可能知道,任何矩阵都可以通过两个较小的矩阵进行因式分解。这意味着你需要找到两个矩阵p[u]q[i],当它们相乘时,能够重构原始矩阵r[ui];我们称这个重构为r[ui]'。目标是找到p[u]q[i]以重构r[ui]',使其与r[ui]的差异不过大。这通过求和平方误差目标函数来实现。

下图说明了这一点以及矩阵的稀疏性特性:

一旦我们找到了良好的因子p[u]q[i],我们就能构建r[ui]',最终,新的非零单元格将出现,这些将成为新的预测产品推荐。如果你还没有完全理解所有细节,不用担心,因为理解本章其余部分并不需要太多这个例子。

寻找p[u]q[i]的常用算法称为交替最小二乘法ALS)——交替是因为在每次迭代中,优化目标从p[u]切换到q[i],反之亦然。对此不必过于纠结,但实际运作即是如此,而在 Apache Spark MLlib 中,这仅是一行 Scala 代码:

那么问题何在?在我们解释之前,先来看看 ALS 如何在统计编程语言如 R 中实现:

同样,若你未能理解每一行代码也不必担心,此图旨在展示在 R 中,该算法仅需 27 行代码即可表达。若我们再查看 MLlib 中的 ALS 实现,会发现它有超过 800 行代码。你可在github.com/apache/spark/tree/master/mllib/src/main/scala/org/apache/spark/mllib/recommendation找到此实现。

那么为何在 Spark 上需要超过 800 行的 Scala 代码,而在 R 中仅需 27 行呢?这是因为性能优化。MLlib 中的 ALS 实现包含了超过 50%的性能优化代码。如果我们能做到以下这些呢?

  • 去除我们算法实现中的所有性能优化

  • 将我们的 R 代码 1:1 移植到某个并行框架

  • 如有变动,只需修改我们的 R 实现

这正是 Apache SystemML 发挥作用的地方,它支持这一切。Apache SystemML 的DSL特定领域语言)是 R 语法的一个子集,因此你可以直接将之前的示例原封不动地运行在 Apache SystemML 之上,无需任何修改。此外,基于成本的性能优化器会在 Apache Spark 之上生成物理执行计划,以根据数据规模属性最小化执行时间。那么,让我们探究其工作原理。

Apache SystemML 架构

在 Apache SystemML 中,关键在于优化器。该组件将算法的高级描述在特定领域语言中转化为 Apache Spark 上高度优化的物理执行,如图所示:

语言解析

让我们稍稍揭开 Apache SystemML 优化器的神秘面纱,以便理解其中究竟发生了什么。引擎首先进行的是 DSL 的编译步骤。首先是语法检查,然后进行活跃变量分析以确定哪些中间结果仍需保留,最后进行语义检查。

生成高级操作符

一旦通过前述步骤,便生成使用所谓高级操作符HOPs)的执行计划。这些操作符构建自 DSL 的抽象语法树AST)。在此阶段,以下重要优化步骤正在进行:

  • 静态重写:DSL 提供了一套丰富的语法和语义特性,使得实现易于理解,但可能导致非最优执行。Apache SystemML 检测到这些 AST 分支,并静态地将其重写为更好的版本,保持语义等价。

  • 动态重写:动态重写与静态重写非常相似,但它们是由基于成本的统计数据驱动的,考虑了数据集的大小...

低级操作符如何被优化

让我们看看,低级操作符是如何被选择和优化的。我们将坚持使用加权除法矩阵乘法的例子——一个在 HOP 优化过程之前被选中的 HOP,而不是一系列普通的矩阵乘法。现在问题来了,例如,是否应该使用在 Apache Spark 工作节点上并行运行的 LOP 的并行版本,或者是否应该优先考虑本地执行。在这个例子中,Apache SystemML 确定所有中间结果都适合驱动节点的主内存,并选择本地操作符WDivMM,而不是并行操作符MapWDivMM。下图说明了这一过程:

性能测量

所有这些努力值得吗?让我们来看一些本地 R 脚本、MLlib 和 Apache SystemML 之间的性能比较:

在不同大小的数据集(1.2GB、12GB 和 120GB)上运行 ALS 算法,使用 R、MLlib 和 ApacheSystemML。我们可以清楚地看到,即使在最小的数据集上,R 也不是一个可行的解决方案,因为它花费了超过 24 小时,我们不确定它是否能完成。在 12GB 的数据集上,我们注意到 ApacheSystemML 比 MLlib 运行得快得多,最后,在 120GB 的数据集上,MLlib 的 ALS 实现一天内没有完成,我们...

Apache SystemML 的实际应用

让我们来看一个非常简单的例子。让我们在 Apache SystemML DSL 中创建一个脚本——一种类似 R 的语法——以便乘以两个矩阵:

import org.apache.sysml.api.MLOutput
import org.apache.spark.sql.SQLContext
import org.apache.spark.mllib.util.LinearDataGenerator
import org.apache.sysml.api.MLContext
import org.apache.sysml.runtime.instructions.spark.utils.{RDDConverterUtilsExt => RDDConverterUtils}
import org.apache.sysml.runtime.matrix.MatrixCharacteristics;

val sqlContext = new SQLContext(sc)

val simpleScript =
"""
fileX = "";
fileY = "";
fileZ = "";

X = read (fileX);
Y = read (fileY);

Z = X %*% Y

write (Z,fileZ);
"""

然后,我们生成一些测试数据:

// Generate data
val rawDataX = sqlContext.createDataFrame(LinearDataGenerator.generateLinearRDD(sc, 100, 10, 1))
val rawDataY = sqlContext.createDataFrame(LinearDataGenerator.generateLinearRDD(sc, 10, 100, 1))

// Repartition into a more parallelism-friendly number of partitions
val dataX = rawDataX.repartition(64).cache()
val dataY = rawDataY.repartition(64).cache()

为了使用 Apache SystemML,我们必须创建一个MLContext对象:

// Create SystemML context
val ml = new MLContext(sc)

现在我们需要将数据转换成 Apache SystemML 能理解的格式:

// Convert data to proper format
val mcX = new MatrixCharacteristics()
val mcY = new MatrixCharacteristics()
val X = RDDConverterUtils.vectorDataFrameToBinaryBlock(sc, dataX, mcX, false, "features")
val Y = RDDConverterUtils.vectorDataFrameToBinaryBlock(sc, dataY, mcY, false, "features")

现在,我们将数据XY传递给 Apache SystemML 运行时,并预先注册一个名为Z的变量,以便从运行时获取结果:

// Register inputs & outputs
ml.reset()  
ml.registerInput("X", X, mcX)
ml.registerInput("Y", Y, mcY)
ml.registerOutput("Z")

最后,我们实际执行了存储在simpleScript中的脚本,并使用executeScript方法从运行时获取结果:

val outputs = ml.executeScript(simpleScript)

// Get outputs
val Z = outputs.getDF(sqlContext, "Z")

现在Z包含了一个带有矩阵乘法结果的DataFrame。完成!

概要

你已了解到,在 Apache Spark 之上还有额外的机器学习框架和库的空间,并且,一个类似于我们在 Catalyst 中已使用的基于成本的优化器可以极大地加速处理。此外,将性能优化代码与算法代码分离,有助于在不考虑性能的情况下进一步改进算法方面。

另外,这些执行计划高度适应数据量的大小,并根据主内存大小和可能的加速器(如 GPU)等可用硬件配置进行调整。Apache SystemML 显著提升了机器学习应用的生命周期,尤其是在机器学习方面...

第七章:Apache Spark GraphX

在本章中,我们希望探讨 Apache Spark GraphX 模块和图处理的一般性。因此,本章将涵盖在 GraphX 之上实现图分析工作流程的主题。GraphX 编码部分,用 Scala 编写,将提供一系列图编码示例。在用 Scala 编写代码以使用 Spark GraphX 模块之前,我们认为提供关于图处理中图实际是什么的概述将是有用的。以下部分使用几个简单图作为示例,提供了一个简短的介绍。

在本章中,我们将涵盖:

  • 从原始数据创建图

  • 计数

  • 过滤

  • PageRank

  • 三角形计数

  • 连通组件

概述

图可以被视为一种数据结构,由一组顶点和连接它们的边组成。图中的顶点或节点可以是任何对象(例如人),而边则是它们之间的关系。边可以是无向的或有向的,意味着关系从一个节点操作到另一个节点。例如,节点A是节点B的父母。

在下面的图中,圆圈代表顶点或节点(AD),而粗线代表它们之间的边或关系(E1E6)。每个节点或边可能具有属性,这些值由相关的灰色方块表示(P1P7):

因此,如果一个图代表了一个物理...

使用 GraphX 进行图分析/处理

本节将探讨使用上一节中展示的家庭关系图数据样本,在 Scala 中进行 Apache Spark GraphX 编程。此数据将被访问为一组顶点和边。尽管此数据集较小,但通过这种方式构建的图可能非常庞大。例如,我们仅使用四个 Apache Spark 工作者就能够分析一家大型银行的 30 TB 金融交易数据。

原始数据

我们正在处理两个数据文件。它们包含将用于本节的顶点和边数据,这些数据构成了一个图:

graph1_edges.csvgraph1_vertex.csv

顶点文件仅包含六行,代表上一节中使用的图。每个顶点代表一个人,并具有顶点 ID 号、姓名和年龄值:

1,Mike,482,Sarah,453,John,254,Jim,535,Kate,226,Flo,52

文件包含一组有向值,形式为源顶点 ID、目标顶点 ID 和关系。因此,记录 1 在FloMike之间形成了一个姐妹关系:

6,1,Sister1,2,Husband2,1,Wife5,1,Daughter5,2,Daughter3,1,Son3,2,Son4,1,Friend1,5,Father1,3,Father2,5,Mother2,3,Mother

让我们,检查一些...

创建图

本节将解释通用 Scala 代码,直到从数据创建 GraphX 图。这将节省时间,因为相同的代码在每个示例中都被重复使用。一旦解释完毕,我们将专注于每个代码示例中的实际基于图的操作。

  1. 通用代码首先导入 Spark 上下文、GraphX 和 RDD 功能,以便在 Scala 代码中使用:
import org.apache.spark.SparkContext
import org.apache.spark.SparkContext._
import org.apache.spark.SparkConf
import org.apache.spark.graphx._
import org.apache.spark.rdd.RDD
  1. 然后定义一个应用程序,它扩展App类。应用程序名称从graph1graph5每个示例都会更改。运行应用程序时将使用此应用程序名称spark-submit
object graph1 extends App {
  1. 如前所述,有两个数据文件包含顶点信息:
val vertexFile = "graph1_vertex.csv"
val edgeFile   = "graph1_edges.csv"
  1. Spark 主 URL定义为应用程序名称,该名称将在应用程序运行时出现在 Spark 用户界面中。创建一个新的 Spark 配置对象,并将 URL 和名称分配给它:
val sparkMaster = "spark://localhost:7077"
val appName = "Graph 1"
val conf = new SparkConf()
conf.setMaster(sparkMaster)
conf.setAppName(appName)
  1. 使用刚刚定义的配置创建一个新的 Spark 上下文:
val sparkCxt = new SparkContext(conf)
  1. 然后,使用sparkCxt.textFile方法将文件中的顶点信息加载到称为顶点的 RDD 基础结构中。数据存储为长VertexId和字符串,以表示人的姓名和年龄。数据行按逗号分割,因为这是基于 CSV 的数据:
val vertices: RDD[(VertexId, (String, String))] =
     sparkCxt.textFile(vertexFile).map { line =>
       val fields = line.split(",")
       ( fields(0).toLong, ( fields(1), fields(2) ) )
}
  1. 同样,数据加载到称为边的 RDD 基础数据结构中。基于 CSV 的数据再次按逗号值分割。前两个数据值转换为长值,因为它们表示源和目标顶点 ID。最后代表边关系的值保持为字符串。请注意,RDD 结构边中的每个记录实际上现在是一个Edge记录:
val edges: RDD[Edge[String]] =
     sparkCxt.textFile(edgeFile).map { line =>
       val fields = line.split(",")
       Edge(fields(0).toLong, fields(1).toLong, fields(2))
}
  1. 如果缺少连接或顶点,则定义默认值;然后从基于 RDD 的结构顶点和边以及默认记录构建图:
val default = ("Unknown", "Missing")
val graph = Graph(vertices, edges, default)
  1. 这创建了一个基于 GraphX 的结构,称为,现在可以用于每个示例。请记住,尽管这些数据样本可能很小,但您可以使用这种方法创建非常大的图。

这些算法中的许多都是迭代应用,例如 PageRank 和三角计数。因此,程序将生成许多迭代的 Spark 作业。

示例 1 – 计数

图已加载,我们知道数据文件中的数据量。但在实际图中,顶点和边的数据内容是什么?使用以下所示的顶点和边计数函数提取此信息非常简单:

println( "vertices : " + graph.vertices.count )println( "edges   : " + graph.edges.count )

运行graph1示例,使用先前创建的.jar文件和示例名称,将提供计数信息。主 URL 用于连接到 Spark 集群,并为执行器内存和总执行器核心提供一些默认参数:

spark-submit \--class graph1 \--master spark://localhost:7077 \--executor-memory 700M \--total-executor-cores ...

示例 2 – 过滤

如果我们需要从主图中创建一个子图,并根据人物年龄或关系进行过滤,会发生什么?第二个示例 Scala 文件graph2中的示例代码展示了如何实现这一点:

val c1 = graph.vertices.filter { case (id, (name, age)) => age.toLong > 40 }.count
val c2 = graph.edges.filter { case Edge(from, to, property)
   => property == "Father" | property == "Mother" }.count
println( "Vertices count : " + c1 )
println( "Edges   count : " + c2 )

已经从主图创建了两个示例计数:第一个仅根据年龄过滤基于人的顶点,选取那些年龄大于四十岁的人。请注意,存储为字符串的年龄值已转换为长整型以进行比较。

第二个示例根据MotherFather的关系属性过滤边。创建并打印了两个计数值c1c2,作为 Spark 运行输出,如下所示:

Vertices count : 4
Edges   count : 4

示例 3 – PageRank

PageRank 算法为图中的每个顶点提供一个排名值。它假设连接到最多边的顶点是最重要的。

搜索引擎使用 PageRank 为网页搜索期间的页面显示提供排序,如下面的代码所示:

val tolerance = 0.0001val ranking = graph.pageRank(tolerance).verticesval rankByPerson = vertices.join(ranking).map {   case (id, ( (person,age) , rank )) => (rank, id, person)}

示例代码创建了一个容差值,并使用它调用图的pageRank方法。然后,顶点被排名到一个新的值排名中。为了使排名更有意义,排名值与原始值进行了连接...

示例 4 – 三角形计数

三角形计数算法提供了一个基于顶点的与该顶点相关的三角形数量的计数。例如,顶点Mike (1) 连接到Kate (5),Kate 连接到Sarah (2),Sarah 连接到Mike (1),从而形成一个三角形。这在需要生成无三角形的最小生成树图进行路线规划时可能很有用。

执行三角形计数并打印它的代码很简单,如下所示。对图的顶点执行triangleCount方法。结果保存在值tCount中并打印出来:

val tCount = graph.triangleCount().vertices
println( tCount.collect().mkString("\n") )

应用程序作业的结果显示,顶点Flo (4) 和Jim (6) 没有三角形,而Mike (1) 和Sarah (2) 如预期那样拥有最多,因为他们有最多的关系:

(4,0)
(6,0)
(2,4)
(1,4)
(3,2)
(5,2)

示例 5 – 连通组件

当从数据中创建一个大图时,它可能包含不相连的子图或彼此隔离的子图,并且可能不包含它们之间的桥接或连接边。这些算法提供了一种连接性的度量。根据你的处理需求,了解所有顶点是否连接可能很重要。

此示例的 Scala 代码调用了两个图方法,connectedComponentsstronglyConnectedComponentsstrong方法需要一个最大迭代计数,已设置为1000。这些计数作用于图的顶点:

val iterations = 1000val connected = graph.connectedComponents().verticesval connectedS = graph.stronglyConnectedComponents(iterations).vertices ...

总结

本章通过示例展示了如何使用基于 Scala 的代码调用 Apache Spark 中的 GraphX 算法。使用 Scala 是因为它比 Java 需要更少的代码来开发示例,从而节省时间。请注意,GraphX 不适用于 Python 或 R。可以使用基于 Scala 的 shell,并且代码可以编译成 Spark 应用程序。

已经介绍了最常见的图算法,你现在应该知道如何使用 GraphX 解决任何图问题。特别是,既然你已经理解了 GraphX 中的图仍然由 RDD 表示和支持,那么你已经熟悉使用它们了。本章的配置和代码示例也将随书提供下载。

第八章:火花调优

在本章中,我们将深入探讨 Apache Spark 的内部机制,并看到尽管 Spark 让我们感觉像是在使用另一个 Scala 集合,但我们不应忘记 Spark 实际上运行在一个分布式系统中。因此,需要格外小心。简而言之,本章将涵盖以下主题:

  • 监控火花作业

  • Spark 配置

  • 火花应用开发中的常见错误

  • 优化技术

监控火花作业

Spark 提供 Web UI 来监控计算节点(驱动程序或执行程序)上运行或完成的全部作业。在本节中,我们将简要讨论如何使用适当的示例通过 Spark Web UI 监控 Spark 作业。我们将看到如何监控作业的进度(包括已提交、排队和运行中的作业)。我们将简要讨论 Spark Web UI 中的所有标签页。最后,我们将讨论 Spark 中的日志记录过程以进行更好的调优。

Spark Web 界面

Web UI(也称为 Spark UI)是运行 Spark 应用程序的 Web 界面,用于在 Firefox 或 Google Chrome 等 Web 浏览器上监控作业的执行。当 SparkContext 启动时,在独立模式下,一个显示应用程序有用信息的 Web UI 会在端口 4040 上启动。根据应用程序是否仍在运行或已完成执行,Spark Web UI 有不同的访问方式。

此外,您可以在应用程序执行完毕后通过使用EventLoggingListener持久化所有事件来使用 Web UI。然而,EventLoggingListener不能单独工作,需要结合 Spark 历史服务器。结合这两个功能,...

作业

根据 SparkContext,Jobs 标签页显示 Spark 应用程序中所有 Spark 作业的状态。当您通过 Web 浏览器在http://localhost:4040(独立模式)访问 Spark UI 的 Jobs 标签页时,您应该会看到以下选项:

  • 显示提交 Spark 作业的活跃用户

  • 总运行时间:显示作业的总运行时间

  • 调度模式:大多数情况下,它是先进先出(FIFO)

  • 活跃作业:显示活跃作业的数量

  • 已完成作业:显示已完成作业的数量

  • 事件时间线:显示已完成执行的作业的时间线

内部,Jobs 标签页由JobsTab类表示,这是一个带有 jobs 前缀的自定义 SparkUI 标签页。Jobs 标签页使用JobProgressListener来访问 Spark 作业的统计信息,以在页面上显示上述信息。请看以下截图:

图 2:Spark Web UI 中的 Jobs 标签页

如果您在 Jobs 标签页中进一步展开 Active Jobs 选项,您将能够看到该特定作业的执行计划、状态、已完成阶段数和作业 ID,如 DAG 可视化所示:

图 3: Spark Web UI 中任务的 DAG 可视化(简略版)

当用户在 Spark 控制台中输入代码时(例如,Spark shell 或使用 Spark submit),Spark Core 会创建一个操作符图。这基本上是用户在特定节点上对 RDD(不可变对象)执行操作(例如,reduce、collect、count、first、take、countByKey、saveAsTextFile)或转换(例如,map、flatMap、filter、mapPartitions、sample、union、intersection、distinct)时发生的情况。

图 4: DAG 调度器将 RDD 血统转换为阶段 DAG

在转换或操作期间,使用有向无环图DAG)信息来恢复到最后一个转换和操作的节点(参见图 4图 5以获得更清晰的图像),以保持数据弹性。最后,图形被提交给 DAG 调度器。

如何从 RDD 计算 DAG,然后执行任务?

从高层次来看,当对 RDD 调用任何操作时,Spark 会创建 DAG 并将其提交给 DAG 调度器。DAG 调度器将操作符划分为任务阶段。一个阶段根据输入数据的分区包含任务。DAG 调度器将操作符流水线化。例如,可以在一个阶段中调度多个映射操作符。DAG 调度器的最终结果是一组阶段。这些阶段被传递给任务调度器。任务调度器通过集群管理器(Spark Standalone/YARN/Mesos)启动任务。任务调度器不知道阶段的依赖关系。工作节点在阶段上执行任务。

有向无环图(DAG)调度器随后跟踪哪些阶段输出的 RDD 被物化。接着,它找到一个最小调度来运行作业,并将相关操作符划分为任务阶段。根据输入数据的分区,一个阶段包含多个任务。然后,操作符与 DAG 调度器一起流水线化。实际上,一个阶段中可以调度多个映射或归约操作符(例如)。

图 5: 执行操作导致 DAG 调度器中新的 ResultStage 和 ActiveJob

DAG 调度器中的两个基本概念是作业和阶段。因此,它必须通过内部注册表和计数器来跟踪它们。从技术上讲,DAG 调度器是 SparkContext 初始化的一部分,它专门在驱动程序上工作(在任务调度器和调度器后端准备好之后立即)。DAG 调度器负责 Spark 执行中的三个主要任务。它为作业计算执行 DAG,即阶段的 DAG。它确定运行每个任务的首选节点,并处理由于洗牌输出文件丢失而导致的故障。

图 6: SparkContext 创建的 DAGScheduler 与其他服务

DAG 调度器的最终结果是一组阶段。因此,大多数统计数据和作业状态可以通过这种可视化查看,例如执行计划、状态、已完成阶段的数量以及该特定作业的作业 ID。

阶段

Spark UI 中的阶段选项卡显示了 Spark 应用程序中所有作业的所有阶段的当前状态,包括一个阶段的任务和统计数据的两个可选页面以及池详细信息。请注意,此信息仅在应用程序以公平调度模式工作时可用。你应该能够通过http://localhost:4040/stages访问阶段选项卡。请注意,当没有提交作业时,该选项卡仅显示标题。阶段选项卡显示了 Spark 应用程序中的阶段。以下阶段可以在该选项卡中看到:

  • 活跃阶段

  • 待处理阶段

  • 已完成阶段

例如,当你在本地提交一个 Spark 作业时,你应该能看到以下状态:

图 7:Spark 中所有作业的阶段...

存储

存储选项卡显示了每个 RDD、DataFrame 或 Dataset 的大小和内存使用情况。你应该能够看到 RDDs、DataFrames 或 Datasets 的存储相关信息。下图显示了存储元数据,如 RDD 名称、存储级别、缓存分区数量、缓存数据的比例以及 RDD 在主内存中的大小:

图 9:存储选项卡显示了磁盘上 RDD 所占用的空间。

请注意,如果 RDD 无法缓存在主内存中,将使用磁盘空间。本章后面将对此进行更详细的讨论。

图 10:数据分布以及磁盘上 RDD 使用的存储空间。

环境

环境选项卡展示了当前机器(即驱动程序)上设置的环境变量。更具体地说,运行时信息如 Java Home、Java 版本和 Scala 版本可以在运行时信息下查看。Spark 属性如 Spark 应用 ID、应用名称、驱动程序主机信息、驱动程序端口、执行器 ID、主 URL 和调度模式也可以看到。此外,其他与系统相关的属性和作业属性,如 AWT 工具包版本、文件编码类型(例如,UTF-8)和文件编码包信息(例如,sun.io)可以在系统属性下查看。

图...

执行器

执行器选项卡使用ExecutorsListener收集有关 Spark 应用程序执行器的信息。执行器是一种分布式代理,负责执行任务。执行器以不同方式实例化。例如,当CoarseGrainedExecutorBackend收到 Spark Standalone 和 YARN 的RegisteredExecutor消息时,执行器被实例化。第二种情况是当 Spark 作业提交给 Mesos 时,Mesos 的MesosExecutorBackend被注册。第三种情况是当你在本地运行 Spark 作业时,即创建了LocalEndpoint。执行器通常在整个 Spark 应用程序生命周期内运行,这称为执行器的静态分配,尽管你也可以选择动态分配。执行器后端专门管理计算节点或集群中的所有执行器。执行器定期向驱动程序上的HeartbeatReceiver RPC 端点报告心跳和活动任务的部分指标,并将结果发送给驱动程序。它们还通过块管理器为用户程序缓存的 RDD 提供内存存储。请参考下图以更清晰地了解这一点:

图 12:Spark 驱动程序实例化一个负责处理 HeartbeatReceiver 心跳消息的执行器。

当执行器启动时,它首先向驱动程序注册,并直接通信以执行任务,如下所示:

图 13:使用 TaskRunners 在执行器上启动任务。

你应该能够访问http://localhost:4040/executors上的执行器选项卡。

图 14:Spark Web UI 上的执行器选项卡。

如前图所示,可以查看执行器 ID、地址、状态、RDD 块、存储内存、磁盘使用、核心、活动任务、失败任务、完成任务、总任务、任务时间(GC 时间)、输入、洗牌读取、洗牌写入和关于执行器的线程转储。

SQL

Spark UI 中的 SQL 选项卡显示每个操作符的所有累加器值。你应该能够访问http://localhost:4040/SQL/上的 SQL 选项卡。它默认显示所有 SQL 查询执行及其底层信息。但是,SQL 选项卡仅在选择查询后显示 SQL 查询执行的详细信息。

本章不涉及对 SQL 的详细讨论。感兴趣的读者可参考Spark SQL 编程指南,了解如何提交 SQL 查询并查看其结果输出。

使用 Web UI 可视化 Spark 应用程序

当提交 Spark 作业执行时,会启动一个 Web 应用程序 UI,显示有关该应用程序的有用信息。事件时间线展示了应用程序事件的相对顺序和交错情况。时间线视图有三个级别:跨所有作业、单个作业和单个阶段。时间线还显示了执行器的分配和解除分配。

图 15:在 Spark Web UI 上以 DAG 形式执行的 Spark 作业

观察正在运行和已完成的 Spark 作业

要访问和观察正在运行和已完成的 Spark 作业,请在 Web 浏览器中打开http://spark_driver_host:4040。请注意,您需要将spark_driver_host替换为相应的 IP 地址或主机名。

请注意,如果同一主机上运行了多个 SparkContext,它们将绑定到从 4040 开始的连续端口,如 4041、4042 等。默认情况下,此信息仅在您的 Spark 应用程序运行期间可用。这意味着当您的 Spark 作业执行完毕后,绑定将不再有效或可访问。

现在,要访问仍在执行的活动作业,请点击“Active Jobs”链接,您将看到与这些作业相关的信息...

使用日志调试 Spark 应用程序

查看所有正在运行的 Spark 应用程序的信息取决于您使用的集群管理器。在调试 Spark 应用程序时,应遵循以下说明:

  • Spark Standalone:访问http://master:18080上的 Spark master UI。master 和每个 worker 都会显示集群和相关作业统计信息。此外,每个作业的详细日志输出也会写入每个 worker 的工作目录。我们将讨论如何使用log4j手动启用 Spark 的日志记录。

  • YARN:如果您的集群管理器是 YARN,并且假设您在 Cloudera(或其他基于 YARN 的平台)上运行 Spark 作业,则请转到 Cloudera Manager Admin Console 中的 YARN 应用程序页面。现在,要调试在 YARN 上运行的 Spark 应用程序,请查看 Node Manager 角色的日志。为此,打开日志事件查看器,然后过滤事件流以选择时间窗口、日志级别并显示 Node Manager 源。您也可以通过命令访问日志。命令格式如下:

 yarn logs -applicationId <application ID> [OPTIONS]

例如,以下是针对这些 ID 的有效命令:

 yarn logs -applicationId application_561453090098_0005 
 yarn logs -applicationId application_561453090070_0005 userid

请注意,用户 ID 可能不同。但是,仅当yarn-site.xml中的yarn.log-aggregation-enable为 true 且应用程序已完成执行时,此情况才成立。

Spark 使用 log4j 进行日志记录

Spark 使用log4j进行自身日志记录。所有后端发生的操作都会被记录到 Spark shell 控制台(该控制台已配置到基础存储)。Spark 提供了一个log4j的属性文件模板,我们可以扩展和修改该文件以在 Spark 中进行日志记录。转到SPARK_HOME/conf目录,您应该会看到log4j.properties.template文件。这可以作为我们自己日志系统的起点。

现在,让我们在运行 Spark 作业时创建自己的自定义日志系统。完成后,将文件重命名为log4j.properties并将其放在同一目录下(即项目树)。文件的示例快照如下所示:

图 17: 快照...

Spark 配置

有多种方法可以配置您的 Spark 作业。在本节中,我们将讨论这些方法。更具体地说,根据 Spark 2.x 版本,有三个位置可以配置系统:

  • Spark 属性

  • 环境变量

  • 日志记录

Spark 属性

如前所述,Spark 属性控制大多数应用程序特定参数,并可以使用SparkConf对象设置。或者,这些参数可以通过 Java 系统属性设置。SparkConf允许您配置一些常见属性,如下所示:

setAppName() // App name setMaster() // Master URL setSparkHome() // Set the location where Spark is installed on worker nodes. setExecutorEnv() // Set single or multiple environment variables to be used when launching executors. setJars() // Set JAR files to distribute to the cluster. setAll() // Set multiple parameters together.

应用程序可以配置为使用机器上可用的多个核心。例如,我们...

环境变量

环境变量可用于设置计算节点或机器设置。例如,IP 地址可以通过每个计算节点上的conf/spark-env.sh脚本设置。下表列出了需要设置的环境变量的名称和功能:

图 18: 环境变量及其含义

日志记录

最后,日志可以通过位于 Spark 应用程序树下的log4j.properties文件进行配置,如前一节所述。Spark 使用 log4j 进行日志记录。log4j 支持的几个有效日志级别如下:

日志级别 用途
OFF 这是最具体的,不允许任何日志记录
FATAL 这是最具体的,显示致命错误,数据量较少
ERROR 这仅显示一般错误
WARN 这显示了建议修复但非强制性的警告
INFO 这显示了 Spark 作业所需的信息
DEBUG 调试时,这些日志将被打印
TRACE 这提供了最不具体的错误跟踪,包含大量数据
ALL ...

常见的 Spark 应用程序开发错误

常见且经常发生的错误包括应用程序失败、由于多种因素导致的工作缓慢且卡住、聚合、操作或转换中的错误、主线程中的异常,当然还有内存溢出OOM)。

应用程序失败

大多数情况下,应用程序失败是因为一个或多个阶段最终失败。如本章前面所述,Spark 作业包含多个阶段。阶段并非独立执行:例如,处理阶段不能在相关输入读取阶段之前进行。因此,假设阶段 1 成功执行,但阶段 2 未能执行,整个应用程序最终会失败。这可以表示如下:

图 19:典型 Spark 作业中的两个阶段

为了举例说明,假设您有以下三个 RDD 操作作为阶段。同样可以如图2021等所示:

慢作业或无响应

有时,如果 SparkContext 无法连接到 Spark 独立主节点,驱动程序可能会显示以下错误:

02/05/17 12:44:45 ERROR AppClient$ClientActor: All masters are unresponsive! Giving up. 
02/05/17 12:45:31 ERROR SparkDeploySchedulerBackend: Application has been killed. Reason: All masters are unresponsive! Giving up. 
02/05/17 12:45:35 ERROR TaskSchedulerImpl: Exiting due to error from cluster scheduler: Spark cluster looks down

在其他时候,驱动程序能够连接到主节点,但主节点无法与驱动程序通信。然后,尽管驱动程序会报告无法连接到 Master 的日志目录,但仍会进行多次连接尝试。

此外,您可能会经常遇到 Spark 作业性能和进度非常缓慢的情况。这是因为您的驱动程序计算作业的速度不够快。如前所述,有时某个特定阶段可能比平常花费更长时间,因为可能涉及洗牌、映射、连接或聚合操作。即使计算机磁盘存储或主内存耗尽,您也可能会遇到这些问题。例如,如果主节点没有响应,或者在一段时间内计算节点没有响应,您可能会认为 Spark 作业已停止,并在某个阶段停滞不前:

图 24:执行器/驱动程序无响应的示例日志

可能的解决方案有多种,包括以下内容:

  1. 请确保工作者和驱动程序正确配置以连接到 Spark 主节点上列出的确切地址,并在启动 Spark shell 时明确提供 Spark 集群的主 URL:
 $ bin/spark-shell --master spark://master-ip:7077
  1. SPARK_LOCAL_IP设置为驱动程序、主节点和工作进程的集群可访问主机名。

有时,我们因硬件故障而遇到一些问题。例如,如果计算节点上的文件系统意外关闭,即发生 I/O 异常,您的 Spark 作业最终也会失败。这是显而易见的,因为您的 Spark 作业无法将结果 RDD 或数据写入本地文件系统或 HDFS。这也意味着由于阶段失败,DAG 操作无法执行。

有时,由于底层磁盘故障或其他硬件故障,会发生此 I/O 异常。这通常会提供以下日志:

图 25:文件系统关闭示例

尽管如此,您经常遇到作业计算性能缓慢,因为 Java GC 在处理 GC 时有些繁忙,或者无法快速完成 GC。例如,下图显示任务 0 完成 GC 耗时 10 小时!我在 2014 年刚接触 Spark 时遇到过这个问题。然而,控制这类问题并不在我们手中。因此,我们的建议是您应该让 JVM 空闲,并尝试重新提交作业。

图 26:GC 在中间卡顿的一个示例

第四个因素可能是由于缺乏数据序列化导致的响应缓慢或作业性能低下。这一点将在下一节讨论。第五个因素可能是代码中的内存泄漏,这将导致应用程序消耗更多内存,并保持文件或逻辑设备打开状态。因此,务必确保没有可能导致内存泄漏的选项。例如,通过调用sc.stop()spark.stop()来结束 Spark 应用程序是一个好习惯。这将确保只有一个 SparkContext 保持打开和活跃状态。否则,您可能会遇到意外的异常或问题。第六个问题是,我们经常保持过多的打开文件,这有时会在洗牌或合并阶段引发FileNotFoundException

优化技术

有多种方法可以针对更好的优化技术调整 Spark 应用程序。在本节中,我们将讨论如何通过调整主内存与更好的内存管理相结合,应用数据序列化来进一步优化我们的 Spark 应用程序。我们还可以通过在开发 Spark 应用程序时调整 Scala 代码中的数据结构来优化性能。另一方面,通过利用序列化的 RDD 存储,可以很好地维护存储。

垃圾回收及其调整是 Spark 应用程序使用 Java 或 Scala 编写时最重要的方面之一。我们将探讨如何针对优化性能进行调整。对于分布式环境和基于集群的...

数据序列化

序列化是任何分布式计算环境中性能改进和优化的重要调整。Spark 也不例外,但 Spark 作业通常数据和计算密集。因此,如果您的数据对象格式不佳,那么您首先需要将它们转换为序列化数据对象。这需要大量内存字节。最终,整个过程将大大减慢整个处理和计算速度。

因此,您经常遇到计算节点响应缓慢的问题。这意味着我们有时未能充分利用计算资源。确实,Spark 试图在便利性和性能之间保持平衡。这也意味着数据序列化应该是 Spark 调优以提高性能的第一步。

Spark 提供了两种数据序列化选项:Java 序列化和 Kryo 序列化库:

  • Java 序列化:Spark 使用 Java 的ObjectOutputStream框架来序列化对象。你通过创建任何实现java.io.Serializable的类来处理序列化。Java 序列化非常灵活,但通常速度很慢,不适合大型数据对象的序列化。

  • Kryo 序列化:你还可以使用 Kryo 库更快地序列化你的数据对象。与 Java 序列化相比,Kryo 序列化快 10 倍,且比 Java 序列化更紧凑。然而,它有一个问题,即不支持所有可序列化类型,但你需要要求你的类被注册。

你可以通过初始化你的 Spark 作业并调用conf.set(spark.serializer, org.apache.spark.serializer.KryoSerializer)来开始使用 Kryo。要向 Kryo 注册你自己的自定义类,请使用registerKryoClasses方法,如下所示:

val conf = new SparkConf()
               .setMaster(“local[*]”)
               .setAppName(“MyApp”)
conf.registerKryoClasses(Array(classOf[MyOwnClass1], classOf[MyOwnClass2]))
val sc = new SparkContext(conf)

如果你的对象很大,你可能还需要增加spark.kryoserializer.buffer配置。这个值需要足够大,以容纳你序列化的最大对象。最后,如果你没有注册你的自定义类,Kryo 仍然可以工作;但是,每个对象都需要存储完整的类名,这确实是浪费的。

例如,在监控 Spark 作业部分的日志记录子部分中,可以使用Kryo序列化优化日志记录和计算。首先,只需将MyMapper类创建为普通类(即没有任何序列化),如下所示:

class MyMapper(n: Int) { // without any serialization
  @transient lazy val log = org.apache.log4j.LogManager.getLogger("myLogger")
  def MyMapperDosomething(rdd: RDD[Int]): RDD[String] = rdd.map { i =>
    log.warn("mapping: " + i)
    (i + n).toString
  }
}

现在,让我们将这个类注册为Kyro序列化类,然后按照以下方式设置Kyro序列化:

conf.registerKryoClasses(Array(classOf[MyMapper])) // register the class with Kyro
conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer") // set Kayro serialization

这就是你所需要的。以下给出了这个示例的完整源代码。你应该能够运行并观察到相同的输出,但与前一个示例相比,这是一个优化版本:

package com.chapter14.Serilazition
import org.apache.spark._
import org.apache.spark.rdd.RDD
class MyMapper(n: Int) { // without any serilization
  @transient lazy val log = org.apache.log4j.LogManager.getLogger
                                ("myLogger")
  def MyMapperDosomething(rdd: RDD[Int]): RDD[String] = rdd.map { i =>
    log.warn("mapping: " + i)
    (i + n).toString
  }
}
//Companion object
object MyMapper {
  def apply(n: Int): MyMapper = new MyMapper(n)
}
//Main object
object KyroRegistrationDemo {
  def main(args: Array[String]) {
    val log = LogManager.getRootLogger
    log.setLevel(Level.WARN)
    val conf = new SparkConf()
      .setAppName("My App")
      .setMaster("local[*]")
    conf.registerKryoClasses(Array(classOf[MyMapper2]))
     // register the class with Kyro
    conf.set("spark.serializer", "org.apache.spark.serializer
             .KryoSerializer") // set Kayro serilazation
    val sc = new SparkContext(conf)
    log.warn("Started")
    val data = sc.parallelize(1 to 100000)
    val mapper = MyMapper(1)
    val other = mapper.MyMapperDosomething(data)
    other.collect()
    log.warn("Finished")
  }
}

输出如下:

17/04/29 15:33:43 WARN root: Started 
.
.
17/04/29 15:31:51 WARN myLogger: mapping: 1 
17/04/29 15:31:51 WARN myLogger: mapping: 49992
17/04/29 15:31:51 WARN myLogger: mapping: 49999
17/04/29 15:31:51 WARN myLogger: mapping: 50000 
.
.                                                                                
17/04/29 15:31:51 WARN root: Finished

做得好!现在让我们快速了解一下如何调整内存。在下一节中,我们将探讨一些高级策略,以确保主内存的高效使用。

内存调优

在本节中,我们将讨论一些高级策略,这些策略可以被像你这样的用户用来确保在执行 Spark 作业时进行高效的内存使用。更具体地说,我们将展示如何计算你的对象的内存使用量。我们将建议一些高级方法来通过优化你的数据结构或通过使用 Kryo 或 Java 序列化器将你的数据对象转换为序列化格式来改进它。最后,我们将探讨如何调整 Spark 的 Java 堆大小、缓存大小和 Java 垃圾收集器。

调整内存使用时有三个考虑因素:

  • 你的对象使用的内存量:你可能甚至希望你的整个数据集都能适应内存

  • 访问那些...

内存使用和管理

你的 Spark 应用程序及其底层计算节点的内存使用可分为执行和存储两类。执行内存用于合并、洗牌、连接、排序和聚合等计算过程中的使用。另一方面,存储内存用于缓存和在集群间传播内部数据。简而言之,这是由于网络间的大量 I/O 造成的。

从技术上讲,Spark 会将网络数据缓存在本地。在迭代或交互式地使用 Spark 时,缓存或持久化是 Spark 中的优化技巧。这两者有助于保存中间部分结果,以便在后续阶段重用。然后,这些中间结果(作为 RDD)可以保存在内存中(默认),或更稳定的存储介质,如磁盘,以及/或进行复制。此外,RDD 也可以通过缓存操作进行缓存。它们还可以使用持久化操作进行持久化。缓存和持久化操作之间的区别纯粹是语法上的。缓存是持久化或持久(MEMORY_ONLY)的同义词,即缓存仅以默认存储级别 MEMORY_ONLY 进行持久化。

如果你在 Spark 网页界面的存储标签下查看,你应该能看到 RDD、DataFrame 或 Dataset 对象使用的内存/存储,如图 10 所示。尽管 Spark 中有两个与内存调优相关的配置,但用户无需调整它们。原因在于配置文件中设置的默认值足以满足你的需求和负载。

spark.memory.fraction 表示统一区域大小占(JVM 堆空间 - 300 MB)的比例(默认值为 0.6)。剩余空间(40%)用于用户数据结构、Spark 内部元数据,以及防范因稀疏和异常大的记录导致的 OOM 错误。另一方面,spark.memory.storageFraction 表示 R 存储空间占统一区域的比例(默认值为 0.5)。此参数的默认值为 Java 堆空间的 50%,即 300 MB。

现在,你心中可能浮现一个问题:应该选择哪种存储级别?针对这个问题,Spark 存储级别提供了不同内存使用与 CPU 效率之间的权衡。如果你的 RDD 能舒适地适应默认存储级别(MEMORY_ONLY),就让 Spark 驱动器或主节点采用它。这是最节省内存的选项,能让 RDD 上的操作尽可能快地运行。你应该选择这个,因为它是最节省内存的选项。这也使得 RDD 上的众多操作能以最快速度完成。

如果您的 RDD 不适合主内存,即MEMORY_ONLY不起作用,您应该尝试使用MEMORY_ONLY_SER。强烈建议不要将 RDD 溢出到磁盘,除非您的UDF(即您为处理数据集定义的用户定义函数)成本过高。如果您的 UDF 在执行阶段过滤掉大量数据,这也适用。在其他情况下,重新计算分区,即重新分区,可能比从磁盘读取数据对象更快。最后,如果您需要快速故障恢复,请使用复制存储级别。

总之,Spark 2.x 支持以下 StorageLevels:(名称中的数字 _2 表示 2 个副本):

  • DISK_ONLY:这是为 RDD 进行磁盘操作

  • DISK_ONLY_2:这是为 RDD 进行磁盘操作,有 2 个副本

  • MEMORY_ONLY:这是 RDD 在内存中进行缓存操作的默认设置

  • MEMORY_ONLY_2:这是 RDD 在内存中进行缓存操作的默认设置,有 2 个副本

  • MEMORY_ONLY_SER:如果您的 RDD 不适合主内存,即MEMORY_ONLY不起作用,此选项特别有助于以序列化形式存储数据对象

  • MEMORY_ONLY_SER_2:如果您的 RDD 不适合主内存,即MEMORY_ONLY在 2 个副本下不起作用,此选项也有助于以序列化形式存储数据对象

  • MEMORY_AND_DISK:基于内存和磁盘(即组合)的 RDD 持久化

  • MEMORY_AND_DISK_2:基于内存和磁盘(即组合)的 RDD 持久化,有 2 个副本

  • MEMORY_AND_DISK_SER:如果MEMORY_AND_DISK不起作用,可以使用它

  • MEMORY_AND_DISK_SER_2:如果MEMORY_AND_DISK在 2 个副本下不起作用,可以使用此选项

  • OFF_HEAP:不允许写入 Java 堆空间

请注意,缓存是持久化(MEMORY_ONLY)的同义词。这意味着缓存仅以默认存储级别持久化,即MEMORY_ONLY。详细信息请参见jaceklaskowski.gitbooks.io/mastering-apache-spark/content/spark-rdd-StorageLevel.html

调整数据结构

减少额外内存使用的第一种方法是避免 Java 数据结构中的一些特性,这些特性会带来额外的开销。例如,基于指针的数据结构和包装对象会导致非平凡的开销。为了使用更好的数据结构调整您的源代码,我们在这里提供了一些建议,这些建议可能会有所帮助。

首先,设计您的数据结构,以便更多地使用对象和基本类型的数组。因此,这也建议更频繁地使用标准 Java 或 Scala 集合类,如SetListQueueArrayListVectorLinkedListPriorityQueueHashSetLinkedHashSetTreeSet

其次,在可能的情况下,避免使用包含大量小对象和指针的嵌套结构,以便...

序列化 RDD 存储

如前所述,尽管有其他类型的内存调整,但当你的对象太大而无法有效地放入主内存或磁盘时,减少内存使用的一个更简单、更好的方法是将其存储在序列化形式中。

这可以通过 RDD 持久化 API 中的序列化存储级别来实现,例如MEMORY_ONLY_SER。有关更多信息,请参阅上一节关于内存管理的介绍,并开始探索可用的选项。

如果你指定使用MEMORY_ONLY_SER,Spark 将把每个 RDD 分区存储为一个大的字节数组。然而,这种方法的唯一缺点是可能会减慢数据访问时间。这是合理的,也是显而易见的;公平地说,由于每个对象在重用时都需要在回弹时进行反序列化,因此无法避免这一点。

如前所述,我们强烈建议使用 Kryo 序列化而不是 Java 序列化,以使数据访问更快一些。

垃圾收集调优

尽管在你的 Java 或 Scala 程序中,只是顺序或随机地读取一次 RDD,然后对其执行大量操作,这并不是一个主要问题,但如果你在驱动程序中存储了大量与 RDD 相关的数据对象,Java 虚拟机JVM)GC 可能会成为一个问题且复杂。当 JVM 需要从旧对象中删除过时和未使用的对象,为新对象腾出空间时,必须识别它们并最终从内存中删除它们。然而,这在处理时间和存储方面是一个代价高昂的操作。你可能会想知道,GC 的成本与存储在主内存中的 Java 对象数量成正比。因此,我们强烈建议...

并行度级别

虽然你可以通过SparkContext.text文件的可选参数来控制要执行的映射任务数量,但 Spark 会根据文件大小自动为每个文件设置相同的数量。此外,对于groupByKeyreduceByKey等分布式reduce操作,Spark 使用最大父 RDD 的分区数。然而,有时我们会犯一个错误,即没有充分利用计算集群中节点的全部计算资源。因此,除非你明确设置并指定 Spark 作业的并行度级别,否则无法充分利用全部计算资源。因此,你应该将并行度级别设置为第二个参数。

关于此选项的更多信息,请参考spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.rdd.PairRDDFunctions

或者,您可以通过设置配置属性 spark.default.parallelism 来更改默认值。对于没有父 RDD 的并行化等操作,并行度取决于集群管理器,即独立、Mesos 或 YARN。对于本地模式,将并行度设置为本地机器上的核心数。对于 Mesos 或 YARN,将细粒度模式设置为 8。在其他情况下,将所有执行器节点上的核心总数或 2 中较大的一个设置为并行度,并建议在集群中每个 CPU 核心上运行 2-3 个任务。

广播

广播变量使 Spark 开发者能够在每个驱动程序上缓存一个只读副本的实例或类变量,而不是将副本与其依赖任务一起传输。然而,仅当多个阶段中的任务需要以反序列化形式使用相同数据时,显式创建广播变量才有用。

在 Spark 应用程序开发中,使用 SparkContext 的广播选项可以大幅减少每个序列化任务的大小。这也有助于降低在集群中启动 Spark 作业的成本。如果在 Spark 作业中有一个任务使用了来自驱动程序的大对象,应将其转换为广播变量。

在 Spark 中使用广播变量...

数据局部性

数据局部性意味着数据与待处理代码的接近程度。从技术上讲,数据局部性可以对本地或集群模式下执行的 Spark 作业的性能产生显著影响。因此,如果数据和待处理代码紧密相连,计算速度应该会快得多。通常,从驱动程序向执行器发送序列化代码要快得多,因为代码大小远小于数据大小。

在 Spark 应用程序开发和作业执行中,存在多个级别的局部性。从最近到最远,级别取决于您需要处理的数据的当前位置:

数据局部性 含义 特别说明
PROCESS_LOCAL 数据和代码位于同一位置 最佳局部性
NODE_LOCAL 数据和代码位于同一节点,例如存储在 HDFS 上的数据 PROCESS_LOCAL 稍慢,因为数据需要在进程和网络之间传播
NO_PREF 数据从其他地方等同访问 没有局部性偏好
RACK_LOCAL 数据位于同一机架的服务器上 适用于大规模数据处理
ANY 数据位于网络上的其他地方,不在同一机架内 除非没有其他选择,否则不推荐使用

表 2: 数据局部性与 Spark

Spark 被设计成倾向于在最佳局部性级别调度所有任务,但这并不能保证,也不总是可能的。因此,根据计算节点的实际情况,如果可用计算资源过于繁忙,Spark 会切换到较低的局部性级别。此外,如果你想获得最佳的数据局部性,你有两个选择:

  • 等待繁忙的 CPU 空闲下来,以便在同一服务器或同一节点上启动处理你的数据的任务

  • 立即开始一个新的,这需要将数据迁移过去

总结

在本章中,我们讨论了使 Spark 作业性能更优的一些高级主题。我们讨论了一些基本技术来调整你的 Spark 作业。我们讨论了如何通过访问 Spark Web UI 来监控你的作业。我们讨论了如何设置 Spark 配置参数。我们还讨论了一些 Spark 用户常犯的错误,并提供了一些建议。最后,我们讨论了一些有助于调整 Spark 应用程序的优化技术。

第九章:测试和调试 Spark

在理想世界中,我们编写的 Spark 代码完美无缺,一切总是运行得完美无瑕,对吧?开个玩笑;实际上,我们知道处理大规模数据集几乎从未那么简单,总会有一些数据点暴露出你代码中的边缘情况。

考虑到上述挑战,因此,在本章中,我们将探讨如果应用程序是分布式的,测试它会有多困难;然后,我们将探讨一些应对方法。简而言之,本章将涵盖以下主题:

  • 分布式环境下的测试

  • 测试 Spark 应用程序

  • 调试 Spark 应用程序

分布式环境下的测试

Leslie Lamport 将分布式系统定义如下:

"分布式系统是指由于某些我从未听说过的机器崩溃,导致我无法完成任何工作的系统。"

通过万维网(又称WWW),一个连接的计算机网络(又称集群)共享资源,是分布式系统的一个好例子。这些分布式环境通常很复杂,经常出现大量异质性。在这些异质环境中进行测试也是具有挑战性的。在本节中,首先,我们将观察在处理此类系统时经常出现的一些常见问题。

分布式环境

分布式系统有众多定义。让我们看一些定义,然后我们将尝试将上述类别与之关联。Coulouris 将分布式系统定义为一个系统,其中位于网络计算机上的硬件或软件组件仅通过消息传递进行通信和协调其动作。另一方面,Tanenbaum 以几种方式定义了这个术语:

  • 一组独立的计算机,对系统用户而言,它们表现为一台单一的计算机。

  • 由两个或多个独立计算机组成的系统,它们通过同步或异步消息传递协调其处理。

  • 分布式系统是一组通过网络连接的自主计算机,其软件设计旨在提供一个集成的计算设施。

现在,基于前面的定义,分布式系统可以分类如下:

  • 只有硬件和软件是分布式的:通过 LAN 连接的本地分布式系统。

  • 用户是分布式的,但存在运行后端的计算和硬件资源,例如 WWW。

  • 用户和硬件/软件都是分布式的:通过 WAN 连接的分布式计算集群。例如,在使用 Amazon AWS、Microsoft Azure、Google Cloud 或 Digital Ocean 的 droplets 时,你可以获得这类计算设施。

分布式系统中的问题

我们将在此讨论软件和硬件测试期间需要注意的一些主要问题,以确保 Spark 作业在集群计算中顺畅运行,集群计算本质上是一种分布式计算环境。

请注意,所有这些问题都是不可避免的,但我们可以至少对其进行优化。您应遵循上一章节中给出的指导和建议。根据卡马尔·希尔·米什拉阿尼尔·库马尔·特里帕蒂在《国际计算机科学与信息技术杂志》第 5 卷(4),2014 年,4922-4925 页中的《分布式软件系统的某些问题、挑战和问题》,网址为pdfs.semanticscholar.org/4c6d/c4d739bad13bcd0398e5180c1513f18275d8.pdf,其中...

分布式环境中的软件测试挑战

在敏捷软件开发中,与任务相关的一些常见挑战,在最终部署前在分布式环境中测试软件时变得更加复杂。团队成员经常需要在错误激增后并行合并软件组件。然而,根据紧急程度,合并往往发生在测试阶段之前。有时,许多利益相关者分布在不同的团队中。因此,存在巨大的误解潜力,团队往往在其中迷失。

例如,Cloud Foundry(www.cloudfoundry.org/)是一个开源的、高度分布式的 PaaS 软件系统,用于管理云中应用程序的部署和可扩展性。它承诺提供诸如可扩展性、可靠性和弹性等特性,这些特性在 Cloud Foundry 上的部署中是固有的,需要底层分布式系统实施措施以确保鲁棒性、弹性和故障转移。

软件测试的过程早已被熟知包括单元测试集成测试冒烟测试验收测试可扩展性测试性能测试服务质量测试。在 Cloud Foundry 中,分布式系统的测试过程如下图所示:

图 1: 类似 Cloud 的分布式环境中软件测试的一个示例

如前图(第一列)所示,在云这样的分布式环境中进行测试的过程始于对系统中最小的接触点运行单元测试。在所有单元测试成功执行后,运行集成测试以验证作为单个连贯软件系统(第二列)一部分的交互组件的行为,该系统在单个盒子(例如,虚拟机VM)或裸机)上运行。然而,虽然这些测试验证了系统作为单体的整体行为,但它们并不能保证系统在分布式部署中的有效性。一旦集成测试通过,下一步(第三列)就是验证系统的分布式部署并运行冒烟测试。

如你所知,软件的成功配置和单元测试的执行使我们能够验证系统行为的可接受性。这种验证是通过运行验收测试(第四列)来完成的。现在,为了克服分布式环境中上述问题和挑战,还有其他隐藏的挑战需要由研究人员和大数据工程师解决,但这些实际上超出了本书的范围。

既然我们知道在分布式环境中软件测试面临的真正挑战是什么,现在让我们开始测试我们的 Spark 代码。下一节专门介绍测试 Spark 应用程序。

测试 Spark 应用程序

尝试测试 Spark 代码的方法有很多,取决于它是 Java(你可以进行基本的 JUnit 测试来测试非 Spark 部分)还是 ScalaTest 用于你的 Scala 代码。你还可以通过在本地或小型测试集群上运行 Spark 来进行完整的集成测试。Holden Karau 提供的另一个很棒的选择是使用 Spark-testing base。你可能知道,到目前为止,Spark 还没有原生的单元测试库。尽管如此,我们可以使用以下两个库作为替代方案:

  • ScalaTest

  • Spark-testing base

然而,在开始测试用 Scala 编写的 Spark 应用程序之前,了解单元测试和测试 Scala 方法的一些背景知识是必要的。

测试 Scala 方法

在这里,我们将看到一些测试 Scala 方法的简单技巧。对于 Scala 用户来说,这是最熟悉的单元测试框架(你也可以用它来测试 Java 代码,很快也可以用于 JavaScript)。ScalaTest 支持多种不同的测试风格,每种风格都是为了支持特定类型的测试需求而设计的。详情请参阅 ScalaTest 用户指南,网址为www.scalatest.org/user_guide/selecting_a_style。尽管 ScalaTest 支持多种风格,但最快上手的方法之一是使用以下 ScalaTest 特性,并以TDD测试驱动开发)风格编写测试:

  1. FunSuite

  2. Assertions

  3. BeforeAndAfter

欢迎浏览上述 URL 以了解更多关于这些特性的信息,这将使本教程的其余部分顺利进行。

需要注意的是,TDD 是一种开发软件的编程技术,它指出您应该从测试开始开发。因此,它不影响测试的编写方式,而是影响测试的编写时机。在ScalaTest.FunSuiteAssertionsBeforeAndAfter中没有特质或测试风格来强制或鼓励 TDD,它们仅与 xUnit 测试框架更为相似。

在 ScalaTest 的任何风格特质中,有三种断言可用:

  • assert:这在您的 Scala 程序中用于通用断言。

  • assertResult:这有助于区分预期值与实际值。

  • assertThrows:这用于确保一段代码抛出预期的异常。

ScalaTest 的断言定义在特质Assertions中,该特质进一步被Suite扩展。简而言之,Suite特质是所有风格特质的超特质。根据 ScalaTest 文档(www.scalatest.org/user_guide/using_assertions),Assertions特质还提供了以下功能:

  • assume 用于条件性地取消测试

  • fail 无条件地使测试失败

  • cancel 无条件取消测试

  • succeed 使测试无条件成功

  • intercept 确保一段代码抛出预期的异常,然后对异常进行断言

  • assertDoesNotCompile 确保一段代码无法编译

  • assertCompiles 确保一段代码能够编译

  • assertTypeError 确保一段代码因类型(非解析)错误而无法编译

  • withClue 用于添加有关失败的更多信息

从上述列表中,我们将展示其中几个。在您的 Scala 程序中,您可以通过调用assert并传递一个Boolean表达式来编写断言。您可以简单地开始编写您的简单单元测试用例,使用AssertionsPredef是一个对象,其中定义了 assert 的这种行为。请注意,Predef的所有成员都会被导入到您的每个 Scala 源文件中。以下源代码将针对以下情况打印Assertion success

package com.chapter16.SparkTesting
object SimpleScalaTest {
  def main(args: Array[String]):Unit= {
    val a = 5
    val b = 5
    assert(a == b)
      println("Assertion success")       
  }
}

然而,如果您设置a = 2b = 1,例如,断言将失败,您将看到以下输出:

图 2:断言失败的示例

如果您传递一个真表达式,assert 将正常返回。然而,如果提供的表达式为假,assert 将以 AssertionError 异常突然终止。与AssertionErrorTestFailedException形式不同,ScalaTest 的 assert 提供了更多信息,它会告诉您确切在哪一行测试用例失败或对于哪个表达式。因此,ScalaTest 的 assert 提供的错误信息比 Scala 的 assert 更优。

例如,对于以下源代码,您应该会遇到TestFailedException,它会告诉您 5 不等于 4:

package com.chapter16.SparkTesting
import org.scalatest.Assertions._
object SimpleScalaTest {
  def main(args: Array[String]):Unit= {
    val a = 5
    val b = 4
    assert(a == b)
      println("Assertion success")       
  }
}

下图显示了前述 Scala 测试的输出:

图 3:TestFailedException 的一个示例

以下源代码说明了使用assertResult单元测试来测试您方法结果的用法:

package com.chapter16.SparkTesting
import org.scalatest.Assertions._
object AssertResult {
  def main(args: Array[String]):Unit= {
    val x = 10
    val y = 6
    assertResult(3) {
      x - y
    }
  }
}

上述断言将会失败,Scala 将抛出异常TestFailedException并打印出Expected 3 but got 4图 4):

图 4:TestFailedException 的另一个示例

现在,让我们看一个单元测试,展示预期的异常:

package com.chapter16.SparkTesting
import org.scalatest.Assertions._
object ExpectedException {
  def main(args: Array[String]):Unit= {
    val s = "Hello world!"
    try {
      s.charAt(0)
      fail()
    } catch {
      case _: IndexOutOfBoundsException => // Expected, so continue
    }
  }
}

如果您尝试访问超出索引范围的数组元素,上述代码将告诉您是否允许访问前述字符串Hello world!的第一个字符。如果您的 Scala 程序能够访问索引中的值,断言将会失败。这也意味着测试案例失败了。因此,由于第一个索引包含字符H,上述测试案例自然会失败,您应该会遇到以下错误信息(图 5):

图 5:TestFailedException 的第三个示例

然而,现在让我们尝试访问位于-1位置的索引,如下所示:

package com.chapter16.SparkTesting
import org.scalatest.Assertions._
object ExpectedException {
  def main(args: Array[String]):Unit= {
    val s = "Hello world!"
    try {
      s.charAt(-1)
      fail()
    } catch {
      case _: IndexOutOfBoundsException => // Expected, so continue
    }
  }
}

现在断言应为真,因此测试案例将会通过。最后,代码将正常终止。现在,让我们检查我们的代码片段是否能编译。很多时候,您可能希望确保代表潜在“用户错误”的特定代码顺序根本不编译。目的是检查库对错误的抵抗力,以防止不希望的结果和行为。ScalaTest 的Assertions特质为此目的包括了以下语法:

assertDoesNotCompile("val a: String = 1")

如果您想确保由于类型错误(而非语法错误)某段代码不编译,请使用以下方法:

assertTypeError("val a: String = 1")

语法错误仍会导致抛出TestFailedException。最后,如果您想声明某段代码确实编译通过,您可以通过以下方式使其更加明显:

assertCompiles("val a: Int = 1")

完整示例如下所示:

package com.chapter16.SparkTesting
import org.scalatest.Assertions._ 
object CompileOrNot {
  def main(args: Array[String]):Unit= {
    assertDoesNotCompile("val a: String = 1")
    println("assertDoesNotCompile True")

    assertTypeError("val a: String = 1")
    println("assertTypeError True")

    assertCompiles("val a: Int = 1")
    println("assertCompiles True")

    assertDoesNotCompile("val a: Int = 1")
    println("assertDoesNotCompile True")
  }
}

上述代码的输出显示在以下图中:

图 6:多个测试合并进行

由于篇幅限制,我们希望结束基于 Scala 的单元测试。但对于其他单元测试案例,您可以参考Scala 测试指南

单元测试

在软件工程中,通常会对源代码的各个单元进行测试,以确定它们是否适合使用。这种软件测试方法也称为单元测试。这种测试确保软件工程师或开发者编写的源代码符合设计规范并按预期工作。

另一方面,单元测试的目标是将程序的每个部分(即以模块化的方式)分开。然后尝试观察所有单独的部分是否正常工作。单元测试在任何软件系统中都有几个好处:

  • 早期发现问题: 它在开发周期的早期发现错误或规范中缺失的部分。

  • 便于变更: 它有助于重构...

测试 Spark 应用程序

我们已经看到了如何使用 Scala 内置的ScalaTest包测试 Scala 代码。然而,在本小节中,我们将看到如何测试我们用 Scala 编写的 Spark 应用程序。以下三种方法将被讨论:

  • 方法 1: 使用 JUnit 测试 Spark 应用程序

  • 方法 2: 使用ScalaTest包测试 Spark 应用程序

  • 方法 3: 使用 Spark 测试基进行 Spark 应用程序测试

方法 1 和 2 将在这里讨论,并附带一些实际代码。然而,方法 3 的详细讨论将在下一小节中提供。为了保持理解简单明了,我们将使用著名的单词计数应用程序来演示方法 1 和 2。

方法 1:使用 Scala JUnit 测试

假设你已经编写了一个 Scala 应用程序,它可以告诉你文档或文本文件中有多少单词,如下所示:

package com.chapter16.SparkTestingimport org.apache.spark._import org.apache.spark.sql.SparkSessionclass wordCounterTestDemo {  val spark = SparkSession    .builder    .master("local[*]")    .config("spark.sql.warehouse.dir", "E:/Exp/")    .appName(s"OneVsRestExample")    .getOrCreate()  def myWordCounter(fileName: String): Long = {    val input = spark.sparkContext.textFile(fileName)    val counts = input.flatMap(_.split(" ")).distinct()    val counter = counts.count()    counter  }}

前面的代码简单地解析一个文本文件,并通过简单地分割单词执行flatMap操作。然后,它执行...

方法 2:使用 FunSuite 测试 Scala 代码

现在,让我们通过仅返回文档中文本的 RDD 来重新设计前面的测试案例,如下所示:

package com.chapter16.SparkTesting
import org.apache.spark._
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.SparkSession
class wordCountRDD {
  def prepareWordCountRDD(file: String, spark: SparkSession): RDD[(String, Int)] = {
    val lines = spark.sparkContext.textFile(file)
    lines.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _)
  }
}

因此,前面类中的prepareWordCountRDD()方法返回一个字符串和整数值的 RDD。现在,如果我们想要测试prepareWordCountRDD()方法的功能,我们可以通过扩展测试类与FunSuiteBeforeAndAfterAll从 Scala 的ScalaTest包来更明确地进行。测试工作的方式如下:

  • 通过扩展FunSuiteBeforeAndAfterAll从 Scala 的ScalaTest包来扩展测试类

  • 覆盖beforeAll()方法以创建 Spark 上下文

  • 使用test()方法执行测试,并在test()方法内部使用assert()方法

  • 覆盖afterAll()方法以停止 Spark 上下文

基于前面的步骤,让我们看一个用于测试前面prepareWordCountRDD()方法的类:

package com.chapter16.SparkTesting
import org.scalatest.{ BeforeAndAfterAll, FunSuite }
import org.scalatest.Assertions._
import org.apache.spark.sql.SparkSession
import org.apache.spark.rdd.RDD
class wordCountTest2 extends FunSuite with BeforeAndAfterAll {
  var spark: SparkSession = null
  def tokenize(line: RDD[String]) = {
    line.map(x => x.split(' ')).collect()
  }
  override def beforeAll() {
    spark = SparkSession
      .builder
      .master("local[*]")
      .config("spark.sql.warehouse.dir", "E:/Exp/")
      .appName(s"OneVsRestExample")
      .getOrCreate()
  }  
  test("Test if two RDDs are equal") {
    val input = List("To be,", "or not to be:", "that is the question-", "William Shakespeare")
    val expected = Array(Array("To", "be,"), Array("or", "not", "to", "be:"), Array("that", "is", "the", "question-"), Array("William", "Shakespeare"))
    val transformed = tokenize(spark.sparkContext.parallelize(input))
    assert(transformed === expected)
  }  
  test("Test for word count RDD") {
    val fileName = "C:/Users/rezkar/Downloads/words.txt"
    val obj = new wordCountRDD
    val result = obj.prepareWordCountRDD(fileName, spark)    
    assert(result.count() === 214)
  }
  override def afterAll() {
    spark.stop()
  }
}

第一个测试表明,如果两个 RDD 以两种不同的方式实现,内容应该相同。因此,第一个测试应该通过。我们将在下面的例子中看到这一点。现在,对于第二个测试,正如我们之前所见,RDD 的单词计数为 214,但让我们暂时假设它是未知的。如果它恰好是 214,测试案例应该通过,这是其预期行为。

因此,我们期望两个测试都通过。现在,在 Eclipse 中,运行测试套件为ScalaTest-File,如下所示:

图 10: 以 ScalaTest-File 形式运行测试套件

现在您应该观察到以下输出(图 11)。输出显示了我们执行了多少测试案例,以及其中有多少通过了、失败了、被取消了、被忽略了或处于待定状态。它还显示了执行整个测试所需的时间。

图 11: 运行两个测试套件作为 ScalaTest-file 时的测试结果

太棒了!测试案例通过了。现在,让我们尝试在两个单独的测试中通过使用test()方法改变断言中的比较值:

test("Test for word count RDD") { 
  val fileName = "data/words.txt"
  val obj = new wordCountRDD
  val result = obj.prepareWordCountRDD(fileName, spark)    
  assert(result.count() === 210)
}
test("Test if two RDDs are equal") {
  val input = List("To be", "or not to be:", "that is the question-", "William Shakespeare")
  val expected = Array(Array("To", "be,"), Array("or", "not", "to", "be:"), Array("that", "is", "the", "question-"), Array("William", "Shakespeare"))
  val transformed = tokenize(spark.sparkContext.parallelize(input))
  assert(transformed === expected)
}

现在,您应该预料到测试案例会失败。现在运行之前的类作为ScalaTest-File图 12):

图 12: 运行前两个测试套件作为 ScalaTest-File 时的测试结果

做得好!我们已经学会了如何使用 Scala 的 FunSuite 进行单元测试。然而,如果您仔细评估前面的方法,您应该同意存在一些缺点。例如,您需要确保SparkContext的创建和销毁有明确的管理。作为开发者或程序员,您需要为测试一个示例方法编写更多行代码。有时,代码重复发生,因为BeforeAfter步骤必须在所有测试套件中重复。然而,这一点有争议,因为公共代码可以放在一个公共特质中。

现在的问题是我们如何能改善我们的体验?我的建议是使用 Spark 测试基底来使生活更轻松、更直接。我们将讨论如何使用 Spark 测试基底进行单元测试。

方法 3:利用 Spark 测试基底简化生活

Spark 测试基底助您轻松测试大部分 Spark 代码。那么,这种方法的优势何在?实际上,优势颇多。例如,使用此方法,代码不会冗长,却能得到非常简洁的代码。其 API 本身比 ScalaTest 或 JUnit 更为丰富。支持多种语言,如 Scala、Java 和 Python。内置 RDD 比较器。还可用于测试流应用程序。最后且最重要的是,它支持本地和集群模式测试。这对于分布式环境中的测试至关重要。

GitHub 仓库位于github.com/holdenk/spark-testing-base

开始之前...

在 Windows 上配置 Hadoop 运行时

我们已经看到如何在 Eclipse 或 IntelliJ 上测试用 Scala 编写的 Spark 应用程序,但还有一个潜在问题不容忽视。虽然 Spark 可以在 Windows 上运行,但 Spark 设计为在类 UNIX 操作系统上运行。因此,如果您在 Windows 环境中工作,则需要格外小心。

在使用 Eclipse 或 IntelliJ 为 Windows 上的数据分析、机器学习、数据科学或深度学习应用程序开发 Spark 应用程序时,您可能会遇到 I/O 异常错误,您的应用程序可能无法成功编译或可能被中断。实际上,Spark 期望 Windows 上也有 Hadoop 的运行时环境。例如,如果您第一次在 Eclipse 上运行 Spark 应用程序,比如KMeansDemo.scala,您将遇到一个 I/O 异常,如下所示:

17/02/26 13:22:00 ERROR Shell: Failed to locate the winutils binary in the hadoop binary path java.io.IOException: Could not locate executable null\bin\winutils.exe in the Hadoop binaries.

原因是默认情况下,Hadoop 是为 Linux 环境开发的,如果您在 Windows 平台上开发 Spark 应用程序,则需要一个桥梁,为 Spark 提供一个 Hadoop 运行时环境,以便正确执行。I/O 异常的详细信息可以在下图看到:

图 14:由于未能在 Hadoop 二进制路径中定位 winutils 二进制文件,导致发生了 I/O 异常

那么,如何解决这个问题呢?解决方案很简单。正如错误信息所说,我们需要一个可执行文件,即winutils.exe。现在从github.com/steveloughran/winutils/tree/master/hadoop-2.7.1/bin下载winutils.exe文件,将其粘贴到 Spark 分发目录中,并配置 Eclipse。更具体地说,假设包含 Hadoop 的 Spark 分发位于C:/Users/spark-2.1.0-bin-hadoop2.7。在 Spark 分发中,有一个名为 bin 的目录。现在,将可执行文件粘贴到那里(即path = C:/Users/spark-2.1.0-binhadoop2.7/bin/)。

解决方案的第二阶段是前往 Eclipse,然后选择主类(即本例中的KMeansDemo.scala),接着进入运行菜单。从运行菜单中,选择运行配置选项,并从那里选择环境标签,如图所示:

图 15:解决因 Hadoop 二进制路径中缺少 winutils 二进制文件而发生的 I/O 异常

如果您选择了该标签,您将有机会使用 JVM 为 Eclipse 创建一个新的环境变量。现在创建一个名为HADOOP_HOME的新环境变量,并将其值设置为C:/Users/spark-2.1.0-bin-hadoop2.7/。现在点击应用按钮并重新运行您的应用程序,您的问题应该得到解决。

需要注意的是,在使用 PySpark 在 Windows 上运行 Spark 时,也需要winutils.exe文件。

请注意,上述解决方案也适用于调试您的应用程序。有时,即使出现上述错误,您的 Spark 应用程序仍能正常运行。然而,如果数据集规模较大,很可能会出现上述错误。

调试 Spark 应用程序

在本节中,我们将了解如何调试在本地(在 Eclipse 或 IntelliJ 上)、独立模式或 YARN 或 Mesos 集群模式下运行的 Spark 应用程序。然而,在深入之前,有必要了解 Spark 应用程序中的日志记录。

Spark 使用 log4j 进行日志记录的回顾

如前所述,Spark 使用 log4j 进行自己的日志记录。如果正确配置了 Spark,所有操作都会记录到 shell 控制台。可以从以下图表中看到文件的示例快照:

图 16:log4j.properties 文件的快照

将默认的 spark-shell 日志级别设置为 WARN。运行 spark-shell 时,此类的日志级别用于覆盖根日志记录器的日志级别,以便用户可以为 shell 和常规 Spark 应用设置不同的默认值。我们还需要在启动由执行器执行并由驱动程序管理的作业时附加 JVM 参数。为此,您应该编辑conf/spark-defaults.conf。简而言之,可以添加以下选项:

spark.executor.extraJavaOptions=-Dlog4j.configuration=file:/usr/local/spark-2.1.1/conf/log4j.properties spark.driver.extraJavaOptions=-Dlog4j.configuration=file:/usr/local/spark-2.1.1/conf/log4j.properties

为了使讨论更清晰,我们需要隐藏所有由 Spark 生成的日志。然后我们可以将它们重定向到文件系统中进行记录。另一方面,我们希望自己的日志记录在 shell 和单独的文件中,以免与 Spark 的日志混淆。从这里开始,我们将指示 Spark 指向存放我们自己日志的文件,在本例中为/var/log/sparkU.log。当应用程序启动时,Spark 会拾取这个log4j.properties文件,因此我们除了将其放置在提及的位置外,无需做其他事情:

package com.chapter14.Serilazition
import org.apache.log4j.LogManager
import org.apache.log4j.Level
import org.apache.spark.sql.SparkSession
object myCustomLog {
  def main(args: Array[String]): Unit = {   
    val log = LogManager.getRootLogger    
    //Everything is printed as INFO once the log level is set to INFO untill you set the level to new level for example WARN. 
    log.setLevel(Level.INFO)
    log.info("Let's get started!")    
    // Setting logger level as WARN: after that nothing prints other than WARN
    log.setLevel(Level.WARN)    
    // Creating Spark Session
    val spark = SparkSession
      .builder
      .master("local[*]")
      .config("spark.sql.warehouse.dir", "E:/Exp/")
      .appName("Logging")
      .getOrCreate()
    // These will note be printed!
    log.info("Get prepared!")
    log.trace("Show if there is any ERROR!")
    //Started the computation and printing the logging information
    log.warn("Started")
    spark.sparkContext.parallelize(1 to 20).foreach(println)
    log.warn("Finished")
  }
}

在前面的代码中,一旦将日志级别设置为INFO,所有内容都会作为 INFO 打印,直到您将级别设置为新的级别,例如WARN。然而,在那之后,不会有任何信息、跟踪等被打印出来。此外,log4j 支持 Spark 的几个有效日志级别。成功执行前面的代码应该会产生以下输出:

17/05/13 16:39:14 INFO root: Let's get started!
17/05/13 16:39:15 WARN root: Started
4 
1 
2 
5 
3 
17/05/13 16:39:16 WARN root: Finished

您还可以在conf/log4j.properties中设置 Spark shell 的默认日志记录。Spark 提供了一个 log4j 的属性文件模板,我们可以扩展和修改该文件以在 Spark 中进行日志记录。转到SPARK_HOME/conf目录,您应该会看到log4j.properties.template文件。在重命名后,您应该使用以下conf/log4j.properties.template作为log4j.properties。在基于 IDE 的环境(如 Eclipse)中开发 Spark 应用程序时,您可以将log4j.properties文件放在项目目录下。但是,要完全禁用日志记录,只需将log4j.logger.org标志设置为OFF,如下所示:

log4j.logger.org=OFF

到目前为止,一切都很容易。然而,我们还没有注意到前述代码段中的一个问题。org.apache.log4j.Logger类的一个缺点是它不是可序列化的,这意味着我们在使用 Spark API 的某些部分进行操作时,不能在闭包内部使用它。例如,假设我们在 Spark 代码中执行以下操作:

object myCustomLogger {
  def main(args: Array[String]):Unit= {
    // Setting logger level as WARN
    val log = LogManager.getRootLogger
    log.setLevel(Level.WARN)
    // Creating Spark Context
    val conf = new SparkConf().setAppName("My App").setMaster("local[*]")
    val sc = new SparkContext(conf)
    //Started the computation and printing the logging information
    //log.warn("Started")
    val i = 0
    val data = sc.parallelize(i to 100000)
    data.map{number =>
      log.info(“My number”+ i)
      number.toString
    }
    //log.warn("Finished")
  }
}

你应该会遇到一个异常,它会说Task不可序列化,如下所示:

org.apache.spark.SparkException: Job aborted due to stage failure: Task not serializable: java.io.NotSerializableException: ...
Exception in thread "main" org.apache.spark.SparkException: Task not serializable 
Caused by: java.io.NotSerializableException: org.apache.log4j.spi.RootLogger
Serialization stack: object not serializable

首先,我们可以尝试用一种简单的方法来解决这个问题。你可以做的就是让执行实际操作的 Scala 类Serializable,使用extends Serializable。例如,代码如下所示:

class MyMapper(n: Int) extends Serializable {
  @transient lazy val log = org.apache.log4j.LogManager.getLogger("myLogger")
  def logMapper(rdd: RDD[Int]): RDD[String] =
    rdd.map { i =>
      log.warn("mapping: " + i)
      (i + n).toString
    }
  }

本节旨在进行关于日志记录的讨论。然而,我们借此机会使其更适用于通用 Spark 编程和问题。为了更有效地克服task not serializable错误,编译器将尝试发送整个对象(不仅仅是 lambda),使其可序列化,并强制 Spark 接受它。然而,这会显著增加数据混洗,尤其是对于大型对象!其他方法是将整个类设为Serializable,或者仅在传递给 map 操作的 lambda 函数中声明实例。有时,在节点之间保留不可Serializable的对象可能有效。最后,使用forEachPartition()mapPartitions()而不是仅使用map(),并创建不可Serializable的对象。总之,这些是解决问题的方法:

  • 序列化该类

  • 仅在传递给 map 的 lambda 函数中声明实例

  • 将不可序列化对象设为静态,并在每台机器上创建一次

  • 调用forEachPartition ()mapPartitions()而不是map(),并创建不可序列化对象

在前述代码中,我们使用了注解@transient lazy,它标记Logger类为非持久性的。另一方面,包含应用方法(即MyMapperObject)的对象,用于实例化MyMapper类的对象,如下所示:

//Companion object 
object MyMapper {
  def apply(n: Int): MyMapper = new MyMapper(n)
}

最后,包含main()方法的对象如下:

//Main object
object myCustomLogwithClosureSerializable {
  def main(args: Array[String]) {
    val log = LogManager.getRootLogger
    log.setLevel(Level.WARN)
    val spark = SparkSession
      .builder
      .master("local[*]")
      .config("spark.sql.warehouse.dir", "E:/Exp/")
      .appName("Testing")
      .getOrCreate()
    log.warn("Started")
    val data = spark.sparkContext.parallelize(1 to 100000)
    val mapper = MyMapper(1)
    val other = mapper.logMapper(data)
    other.collect()
    log.warn("Finished")
  }

现在,让我们看另一个例子,它提供了更好的洞察力,以继续解决我们正在讨论的问题。假设我们有以下类,用于计算两个整数的乘法:

class MultiplicaitonOfTwoNumber {
  def multiply(a: Int, b: Int): Int = {
    val product = a * b
    product
  }
}

现在,本质上,如果你尝试使用这个类来计算 lambda 闭包中的乘法,使用map(),你将会遇到我们之前描述的Task Not Serializable错误。现在我们只需简单地使用foreachPartition()和内部的 lambda,如下所示:

val myRDD = spark.sparkContext.parallelize(0 to 1000)
    myRDD.foreachPartition(s => {
      val notSerializable = new MultiplicaitonOfTwoNumber
      println(notSerializable.multiply(s.next(), s.next()))
    })

现在,如果你编译它,它应该返回期望的结果。为了方便,包含main()方法的完整代码如下:

package com.chapter16.SparkTesting
import org.apache.spark.sql.SparkSession
class MultiplicaitonOfTwoNumber {
  def multiply(a: Int, b: Int): Int = {
    val product = a * b
    product
  }
}

object MakingTaskSerilazible {
  def main(args: Array[String]): Unit = {
    val spark = SparkSession
      .builder
      .master("local[*]")
      .config("spark.sql.warehouse.dir", "E:/Exp/")
      .appName("MakingTaskSerilazible")
      .getOrCreate()
 val myRDD = spark.sparkContext.parallelize(0 to 1000)
    myRDD.foreachPartition(s => {
      val notSerializable = new MultiplicaitonOfTwoNumber
      println(notSerializable.multiply(s.next(), s.next()))
    })
  }
}

输出如下:

0
5700
1406
156
4032
7832
2550
650

调试 Spark 应用程序

在本节中,我们将讨论如何在本地 Eclipse 或 IntelliJ 上调试运行在独立模式或集群模式(在 YARN 或 Mesos 上)的 Spark 应用程序。在开始之前,您还可以阅读调试文档:hortonworks.com/hadoop-tutorial/setting-spark-development-environment-scala/

在 Eclipse 上以 Scala 调试方式调试 Spark 应用程序

要实现这一目标,只需将您的 Eclipse 配置为将 Spark 应用程序作为常规 Scala 代码进行调试。配置方法为选择运行 | 调试配置 | Scala 应用程序,如图所示:

图 17:配置 Eclipse 以将 Spark 应用程序作为常规 Scala 代码进行调试

假设我们想要调试我们的KMeansDemo.scala,并要求 Eclipse(您可以在 InteliJ IDE 中拥有类似选项)从第 56 行开始执行并在第 95 行设置断点。为此,请以调试模式运行您的 Scala 代码,您应该在 Eclipse 上观察到以下场景:

图 18:在 Eclipse 上调试 Spark 应用程序

然后,Eclipse 将在您要求它停止执行的第 95 行暂停,如下面的截图所示:

图 19:在 Eclipse 上调试 Spark 应用程序(断点)

总之,为了简化上述示例,如果在第 56 行和第 95 行之间出现任何错误,Eclipse 将显示错误实际发生的位置。否则,如果没有中断,它将遵循正常的工作流程。

调试作为本地和独立模式运行的 Spark 作业

在本地或独立模式下调试您的 Spark 应用程序时,您应该知道调试驱动程序程序和调试其中一个执行程序是不同的,因为使用这两种节点需要向spark-submit传递不同的提交参数。在本节中,我将使用端口 4000 作为地址。例如,如果您想调试驱动程序程序,您可以在您的spark-submit命令中添加以下内容:

--driver-java-options -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=4000

之后,您应将远程调试器设置为连接到您提交驱动程序程序的节点。对于上述情况,端口号 4000 是...

在 YARN 或 Mesos 集群上调试 Spark 应用程序

当您在 YARN 上运行 Spark 应用程序时,有一个选项可以通过修改yarn-env.sh来启用:

YARN_OPTS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=4000 $YARN_OPTS"

现在,远程调试将通过 Eclipse 或 IntelliJ IDE 上的 4000 端口可用。第二种方法是设置SPARK_SUBMIT_OPTS。您可以使用 Eclipse 或 IntelliJ 开发可以提交到远程多节点 YARN 集群执行的 Spark 应用程序。我所做的是在 Eclipse 或 IntelliJ 上创建一个 Maven 项目,将我的 Java 或 Scala 应用程序打包成 jar 文件,然后作为 Spark 作业提交。然而,为了将 IDE(如 Eclipse 或 IntelliJ)调试器附加到您的 Spark 应用程序,您可以使用SPARK_SUBMIT_OPTS环境变量定义所有提交参数,如下所示:

$ export SPARK_SUBMIT_OPTS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=4000

然后如下提交您的 Spark 作业(请根据您的需求和设置相应地更改值):

$ SPARK_HOME/bin/spark-submit \
--class "com.chapter13.Clustering.KMeansDemo" \
--master yarn \
--deploy-mode cluster \
--driver-memory 16g \
--executor-memory 4g \
--executor-cores 4 \
--queue the_queue \
--num-executors 1\
--executor-cores 1 \
--conf "spark.executor.extraJavaOptions=-agentlib:jdwp=transport=dt_socket,server=n,address= host_name_to_your_computer.org:4000,suspend=n" \
--driver-java-options -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=4000 \
 KMeans-0.0.1-SNAPSHOT-jar-with-dependencies.jar \
Saratoga_NY_Homes.txt

执行上述命令后,它将等待您连接调试器,如下所示:Listening for transport dt_socket at address: 4000。现在,您可以在 IntelliJ 调试器中配置 Java 远程应用程序(Scala 应用程序也可以),如下面的截图所示:

图 20:在 IntelliJ 上配置远程调试器

对于上述情况,10.200.1.101 是远程计算节点上运行 Spark 作业的基本 IP 地址。最后,您需要通过点击 IntelliJ 的运行菜单下的调试来启动调试器。然后,如果调试器连接到您的远程 Spark 应用程序,您将在 IntelliJ 的应用程序控制台中看到日志信息。现在,如果您可以设置断点,其余的调试就是正常的了。

下图展示了在 IntelliJ 中暂停带有断点的 Spark 作业时的示例视图:

图 21:在 IntelliJ 中暂停带有断点的 Spark 作业时的示例视图

尽管效果良好,但有时我发现使用SPARK_JAVA_OPTS在 Eclipse 甚至 IntelliJ 的调试过程中帮助不大。相反,在运行 Spark 作业的真实集群(YARN、Mesos 或 AWS)上,使用并导出SPARK_WORKER_OPTSSPARK_MASTER_OPTS,如下所示:

$ export SPARK_WORKER_OPTS="-Xdebug -Xrunjdwp:server=y,transport=dt_socket,address=4000,suspend=n"
$ export SPARK_MASTER_OPTS="-Xdebug -Xrunjdwp:server=y,transport=dt_socket,address=4000,suspend=n"

然后如下启动 Master 节点:

$ SPARKH_HOME/sbin/start-master.sh

现在打开一个 SSH 连接到运行 Spark 作业的远程机器,并将本地主机映射到 4000(即localhost:4000)到host_name_to_your_computer.org:5000,假设集群位于host_name_to_your_computer.org:5000并监听端口 5000。现在,您的 Eclipse 将认为您只是在调试本地 Spark 应用程序或进程。然而,要实现这一点,您需要在 Eclipse 上配置远程调试器,如下所示:

图 22:在 Eclipse 上连接远程主机以调试 Spark 应用程序

就这样!现在你可以在实时集群上调试,就像在桌面一样。前面的示例是在 Spark Master 设置为 YARN-client 的情况下运行的。然而,在 Mesos 集群上运行时也应该有效。如果你使用的是 YARN-cluster 模式,你可能需要将驱动程序设置为附加到调试器,而不是将调试器附加到驱动程序,因为你事先不一定知道驱动程序将执行的模式。

使用 SBT 调试 Spark 应用程序

上述设置主要适用于使用 Maven 项目的 Eclipse 或 IntelliJ。假设你已经完成了应用程序,并正在你喜欢的 IDE(如 IntelliJ 或 Eclipse)中工作,如下所示:

object DebugTestSBT {  def main(args: Array[String]): Unit = {    val spark = SparkSession      .builder      .master("local[*]")      .config("spark.sql.warehouse.dir", "C:/Exp/")      .appName("Logging")      .getOrCreate()          spark.sparkContext.setCheckpointDir("C:/Exp/")    println("-------------Attach debugger now!--------------")    Thread.sleep(8000)    // code goes here, with breakpoints set on the lines you want to pause  }}

现在,如果你想将这项工作部署到本地集群(独立模式),第一步是打包...

总结

在本章中,你看到了测试和调试 Spark 应用程序的难度。在分布式环境中,这些甚至可能更为关键。我们还讨论了一些高级方法来全面应对这些问题。总之,你学习了在分布式环境中的测试方法。然后你学习了测试 Spark 应用程序的更好方法。最后,我们讨论了一些调试 Spark 应用程序的高级方法。

这基本上是我们关于 Spark 高级主题的小旅程的结束。现在,我们给读者的一般建议是,如果你是数据科学、数据分析、机器学习、Scala 或 Spark 的相对新手,你应该首先尝试了解你想执行哪种类型的分析。更具体地说,例如,如果你的问题是机器学习问题,尝试猜测哪种学习算法最适合,即分类、聚类、回归、推荐或频繁模式挖掘。然后定义和制定问题,之后你应该根据我们之前讨论的 Spark 特征工程概念生成或下载适当的数据。另一方面,如果你认为你可以使用深度学习算法或 API 解决问题,你应该使用其他第三方算法并与 Spark 集成,直接工作。

我们给读者的最终建议是定期浏览 Spark 官网(位于spark.apache.org/)以获取更新,并尝试将常规的 Spark 提供的 API 与其他第三方应用程序或工具结合使用,以实现最佳的协同效果。

第十章:使用 Spark 和 Scala 进行实用机器学习

在本章中,我们将涵盖:

  • 配置 IntelliJ 以与 Spark 配合工作并运行 Spark ML 示例代码

  • 运行 Spark 中的示例 ML 代码

  • 识别实用机器学习的数据源

  • 使用 IntelliJ IDE 运行您的第一个 Apache Spark 2.0 程序

  • 如何向您的 Spark 程序添加图形

简介

随着集群计算的最新进展,以及大数据的兴起,机器学习领域已被推到了计算的前沿。长期以来,人们一直梦想有一个能够实现大规模数据科学的交互式平台,现在这个梦想已成为现实。

以下三个领域的结合使得大规模交互式数据科学得以实现并加速发展:

  • Apache Spark:一个统一的数据科学技术平台,它将快速计算引擎和容错数据结构结合成一个设计精良且集成的解决方案

  • 机器学习:人工智能的一个领域,使机器能够模仿原本专属于人脑的一些任务

  • Scala:一种基于现代 JVM 的语言,它建立在传统语言之上,但将函数式和面向对象的概念结合在一起,而不会像其他语言那样冗长

首先,我们需要设置开发环境,它将包括以下组件:

  • Spark

  • IntelliJ 社区版 IDE

  • Scala

本章中的配方将为您提供详细的安装和配置 IntelliJ IDE、Scala 插件和 Spark 的说明。开发环境设置完成后,我们将继续运行一个 Spark ML 示例代码来测试设置。

Apache Spark

Apache Spark 正成为大数据分析的事实标准平台和行业语言,并作为Hadoop范式的补充。Spark 使数据科学家能够以最有利于其工作流程的方式直接开始工作。Spark 的方法是在完全分布式的方式下处理工作负载,无需MapReduceMR)或重复将中间结果写入磁盘。

Spark 提供了一个易于使用的统一技术栈中的分布式框架,这使其成为数据科学项目的首选平台,这些项目往往需要一个最终合并到解决方案的迭代算法。由于这些算法的内部工作原理,它们会产生大量的...

机器学习

机器学习的目的是制造能够模仿人类智能并自动化一些传统上由人脑完成的任务的机器和设备。机器学习算法旨在在相对较短的时间内处理大量数据集,并近似出人类需要更长时间才能处理出的答案。

(机器学习领域可以分为多种形式,从高层次上可以分为监督学习和无监督学习。监督学习算法是一类使用训练集(即标记数据)来计算概率分布或图形模型的 ML 算法,进而使它们能够在没有进一步人工干预的情况下对新数据点进行分类。无监督学习是一种机器学习算法,用于从没有标签响应的输入数据集中提取推断。)

(Spark 开箱即提供丰富的 ML 算法集合,无需进一步编码即可部署在大型数据集上。下图展示了 Spark 的 MLlib 算法作为思维导图。Spark 的 MLlib 旨在利用并行性,同时拥有容错分布式数据结构。Spark 将此类数据结构称为 弹性分布式数据集RDD。)

Scala

Scala 是一种新兴的现代编程语言,作为传统编程语言如 JavaC++ 的替代品而崭露头角。Scala 是一种基于 JVM 的语言,不仅提供简洁的语法,避免了传统的样板代码,还将面向对象和函数式编程融合到一个极其精炼且功能强大的类型安全语言中。

Scala 采用灵活且富有表现力的方法,使其非常适合与 Spark 的 MLlib 交互。Spark 本身是用 Scala 编写的,这一事实有力地证明了 Scala 语言是一种全功能编程语言,可用于创建具有高性能需求的复杂系统代码。

Scala 基于 Java 的传统...

Software versions and libraries used in this book

The following table provides a detailed list of software versions and libraries used in this book. If you follow the installation instructions covered in this chapter, it will include most of the items listed here. Any other JAR or library files that may be required for specific recipes are covered via additional installation instructions in the respective recipes:

Core systems Version
Spark 2.0.0
Java 1.8
IntelliJ IDEA 2016.2.4
Scala-sdk 2.11.8

Miscellaneous JARs that will be required are as follows:

Miscellaneous JARs Version
bliki-core 3.0.19
breeze-viz 0.12
Cloud9 1.5.0
Hadoop-streaming 2.2.0
JCommon 1.0.23
JFreeChart 1.0.19
lucene-analyzers-common 6.0.0
Lucene-Core 6.0.0
scopt 3.3.0
spark-streaming-flume-assembly 2.0.0
spark-streaming-kafka-0-8-assembly 2.0.0

We have additionally tested all the recipes in this book on Spark 2.1.1 and found that the programs executed as expected. It is recommended for learning purposes you use the software versions and libraries listed in these tables.

为了跟上快速变化的 Spark 环境和文档,本书中提到的 Spark 文档的 API 链接指向最新的 Spark 2.x.x 版本,但食谱中的 API 参考明确针对 Spark 2.0.0。

本书提供的所有 Spark 文档链接将指向 Spark 网站上的最新文档。如果您希望查找特定版本的 Spark(例如,Spark 2.0.0)的文档,请使用以下 URL 在 Spark 网站上查找相关文档:

spark.apache.org/documentation.html

为了清晰起见,我们已尽可能简化代码,而不是展示 Scala 的高级特性。

配置 IntelliJ 以配合 Spark 运行 Spark ML 示例代码

在运行 Spark 或本书列出的任何程序提供的示例之前,我们需要进行一些配置以确保项目设置正确。

准备就绪

在配置项目结构和全局库时,我们需要特别小心。设置完成后,我们运行 Spark 团队提供的示例 ML 代码以验证安装。示例代码可在 Spark 目录下找到,或通过下载包含示例的 Spark 源代码获取。

如何操作...

以下是配置 IntelliJ 以配合 Spark MLlib 工作以及在示例目录中运行 Spark 提供的示例 ML 代码的步骤。示例目录可在您的 Spark 主目录中找到。使用 Scala 示例继续:

  1. 点击“项目结构...”选项,如以下截图所示,以配置项目设置:

  1. 验证设置:

  1. 配置全局库。选择 Scala SDK 作为您的全局库:

  2. 选择新的 Scala SDK 的 JAR 文件并允许下载...

还有更多...

在 Spark 2.0 之前,我们需要 Google 的另一个库Guava来促进 I/O 并提供定义表的一组丰富方法,然后让 Spark 在集群中广播它们。由于难以解决的依赖问题,Spark 2.0 不再使用 Guava 库。如果您使用的是 2.0 之前的 Spark 版本(在 1.5.2 版本中需要),请确保使用 Guava 库。Guava 库可从此 URL 访问:

github.com/google/guava/wiki

您可能希望使用 Guava 版本 15.0,该版本可在此处找到:

mvnrepository.com/artifact/com.google.guava/guava/15.0

如果您使用的是之前博客中的安装说明,请确保从安装集中排除 Guava 库。

另请参见

如果完成 Spark 安装还需要其他第三方库或 JAR,您可以在以下 Maven 仓库中找到它们:

repo1.maven.org/maven2/org/apache/spark/

从 Spark 运行样本 ML 代码

我们可以通过简单地下载 Spark 源树中的样本代码并将其导入 IntelliJ 以确保其运行来验证设置。

准备就绪

我们首先运行样本中的逻辑回归代码以验证安装。在下一节中,我们将编写自己的版本并检查输出,以便理解其工作原理。

如何操作...

  1. 转到源目录并选择一个 ML 样本代码文件运行。我们选择了逻辑回归示例。

如果您在目录中找不到源代码,您可以随时下载 Spark 源码,解压缩,然后相应地提取示例目录。

  1. 选择示例后,选择“编辑配置...”,如下面的截图所示:

  1. 在配置选项卡中,定义以下选项:

    • VM 选项:所示选项允许您运行独立 Spark 集群

    • 程序参数:我们需要传递给程序的内容

  1. 通过转到运行'LogisticRegressionExample'来运行逻辑回归,如下面的截图所示:

  1. 验证退出代码,并确保它与下面的截图所示相同:

识别实用机器学习的数据源

过去为机器学习项目获取数据是一个挑战。然而,现在有一系列特别适合机器学习的公共数据源。

准备就绪

除了大学和政府来源外,还有许多其他开放数据源可用于学习和编写自己的示例和项目。我们将列出数据源,并向您展示如何最好地获取和下载每章的数据。

如何操作...

以下是一些值得探索的开源数据列表,如果您想在此领域开发应用程序:

  • UCI 机器学习库:这是一个具有搜索功能的广泛库。在撰写本文时,已有超过 350 个数据集。您可以点击archive.ics.uci.edu/ml/index.html链接查看所有数据集,或使用简单搜索(Ctrl + F)查找特定数据集。

  • Kaggle 数据集:你需要创建一个账户,但你可以下载任何用于学习和参加机器学习竞赛的数据集。www.kaggle.com/competitions链接提供了探索和了解更多关于 Kaggle 以及机器学习竞赛内部运作的详细信息。...

另请参阅

机器学习数据的其它来源:

有一些专业数据集(例如,西班牙语文本分析数据集,以及基因和 IMF 数据)可能对您有所帮助:

使用 IntelliJ IDE 运行您的第一个 Apache Spark 2.0 程序

本程序的目的是让您熟悉使用刚设置的 Spark 2.0 开发环境编译和运行示例。我们将在后续章节中探讨组件和步骤。

我们将编写自己的 Spark 2.0.0 程序版本,并检查输出,以便理解其工作原理。需要强调的是,这个简短的示例仅是一个简单的 RDD 程序,使用了 Scala 的糖语法,以确保在开始处理更复杂的示例之前,您已正确设置了环境。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含了必要的 JAR 文件。

  2. 下载本书的示例代码,找到myFirstSpark20.scala文件,并将代码放置在以下目录中。

我们在 Windows 机器上的C:\spark-2.0.0-bin-hadoop2.7\目录下安装了 Spark 2.0。

  1. myFirstSpark20.scala文件放置在C:\spark-2.0.0-bin-hadoop2.7\examples\src\main\scala\spark\ml\cookbook\chapter1目录下:

Mac 用户请注意,我们在 Mac 机器上的/Users/USERNAME/spark/spark-2.0.0-bin-hadoop2.7/目录下安装了 Spark 2.0。

myFirstSpark20.scala文件放置在/Users/USERNAME/spark/spark-2.0.0-bin-hadoop2.7/examples/src/main/scala/spark/ml/cookbook/chapter1目录下。

  1. 设置程序将驻留的包位置:
package spark.ml.cookbook.chapter1 
  1. 为了使 Spark 会话能够访问集群并使用log4j.Logger减少 Spark 产生的输出量,导入必要的包:
import org.apache.spark.sql.SparkSession 
import org.apache.log4j.Logger 
import org.apache.log4j.Level 
  1. 将输出级别设置为ERROR以减少 Spark 的日志输出:
Logger.getLogger("org").setLevel(Level.ERROR) 
  1. 通过使用构建器模式指定配置来初始化 Spark 会话,从而为 Spark 集群提供一个入口点:
val spark = SparkSession 
.builder 
.master("local[*]")
 .appName("myFirstSpark20") 
.config("spark.sql.warehouse.dir", ".") 
.getOrCreate() 

myFirstSpark20对象将在本地模式下运行。前面的代码块是创建SparkSession对象的典型方式。

  1. 然后我们创建两个数组变量:
val x = Array(1.0,5.0,8.0,10.0,15.0,21.0,27.0,30.0,38.0,45.0,50.0,64.0) 
val y = Array(5.0,1.0,4.0,11.0,25.0,18.0,33.0,20.0,30.0,43.0,55.0,57.0) 
  1. 然后让 Spark 基于之前创建的数组创建两个 RDD:
val xRDD = spark.sparkContext.parallelize(x) 
val yRDD = spark.sparkContext.parallelize(y) 
  1. 接下来,我们让 Spark 对RDD进行操作;zip()函数将从之前提到的两个 RDD 创建一个新的RDD
val zipedRDD = xRDD.zip(yRDD) 
zipedRDD.collect().foreach(println) 

在运行时控制台输出(关于如何在 IntelliJ IDE 中运行程序的更多详细信息将在后续步骤中介绍),您将看到这个:

  1. 现在,我们汇总xRDDyRDD的值,并计算新的zipedRDD总和值。我们还计算了zipedRDD的项目计数:
val xSum = zipedRDD.map(_._1).sum() 
val ySum = zipedRDD.map(_._2).sum() 
val xySum= zipedRDD.map(c => c._1 * c._2).sum() 
val n= zipedRDD.count() 
  1. 我们在控制台上打印出之前计算的值:
println("RDD X Sum: " +xSum) 
println("RDD Y Sum: " +ySum) 
println("RDD X*Y Sum: "+xySum) 
println("Total count: "+n) 

这里是控制台输出:

  1. 我们通过停止 Spark 会话来关闭程序:
spark.stop() 
  1. 程序完成后,myFirstSpark20.scala在 IntelliJ 项目资源管理器中的布局将如下所示:

  1. 确保没有编译错误。您可以通过重建项目来测试这一点:

一旦重建完成,控制台上应该会出现构建完成的消息:

Information: November 18, 2016, 11:46 AM - Compilation completed successfully with 1 warning in 55s 648ms
  1. 您可以通过在项目资源管理器中右键点击myFirstSpark20对象并选择上下文菜单选项(如下一张截图所示)运行 myFirstSpark20来运行前面的程序。

您也可以从菜单栏的“运行”菜单执行相同的操作。

  1. 一旦程序成功执行,您将看到以下消息:
Process finished with exit code 0

这也显示在下面的截图中:

  1. IntelliJ 的 Mac 用户可以使用相同的上下文菜单执行此操作。

将代码放置在正确的路径上。

工作原理...

在本例中,我们编写了第一个 Scala 程序myFirstSpark20.scala,并在 IntelliJ 中展示了执行该程序的步骤。我们按照步骤中描述的路径,在 Windows 和 Mac 上都放置了代码。

myFirstSpark20代码中,我们看到了创建SparkSession对象的典型方式,以及如何使用master()函数配置它以在本地模式下运行。我们从数组对象创建了两个 RDD,并使用简单的zip()函数创建了一个新的 RDD。

我们还对创建的 RDD 进行了简单的求和计算,并在控制台中显示了结果。最后,我们通过调用spark.stop()退出并释放资源。

还有更多...

Spark 可以从spark.apache.org/downloads.html下载。

Spark 2.0 关于 RDD 的文档可以在spark.apache.org/docs/latest/programming-guide.html#rdd-operations找到。

另请参见

如何向你的 Spark 程序添加图形

在本食谱中,我们讨论了如何使用 JFreeChart 向你的 Spark 2.0.0 程序添加图形图表。

如何操作...

  1. 设置 JFreeChart 库。JFreeChart 的 JAR 文件可以从sourceforge.net/projects/jfreechart/files/网站下载。

  2. 本书中介绍的 JFreeChart 版本为 JFreeChart 1.0.19,如以下截图所示。它可以从sourceforge.net/projects/jfreechart/files/1.%20JFreeChart/1.0.19/jfreechart-1.0.19.zip/download网站下载:

  1. 下载 ZIP 文件后,将其解压。我们在 Windows 机器上的C:\下解压了 ZIP 文件,然后继续在解压的目标目录下找到lib目录。

  2. 接着,我们找到了所需的两个库(JFreeChart...

工作原理...

在本例中,我们编写了MyChart.scala,并看到了在 IntelliJ 中执行程序的步骤。我们按照步骤中描述的路径在 Windows 和 Mac 上放置了代码。

在代码中,我们看到了创建SparkSession对象的典型方法以及如何使用master()函数。我们创建了一个 RDD,其元素为 1 到 15 范围内的随机整数数组,并将其与索引进行了压缩。

然后,我们使用 JFreeChart 制作了一个包含简单xy轴的基本图表,并提供了我们从前几步中的原始 RDD 生成的数据集。

我们为图表设置了架构,并在 JFreeChart 中调用show()函数,以显示一个带有xy轴的线性图形图表的框架。

最后,我们通过调用spark.stop()退出并释放资源。

还有更多...

更多关于 JFreeChart 的信息,请访问:

另请参见

关于 JFreeChart 功能和能力的更多示例,请访问以下网站:

www.jfree.org/jfreechart/samples.html

第十一章:Spark 的机器学习三剑客 - 完美结合

本章我们将涵盖以下内容:

  • 使用 Spark 2.0 的内部数据源创建 RDD

  • 使用 Spark 2.0 的外部数据源创建 RDD

  • 使用 Spark 2.0 的 filter() API 转换 RDD

  • 使用非常有用的 flatMap() API 转换 RDD

  • 使用集合操作 API 转换 RDD

  • 使用 groupBy() 和 reduceByKey() 进行 RDD 转换/聚合

  • 使用 zip() API 转换 RDD

  • 使用配对键值 RDD 进行连接转换

  • 使用配对键值 RDD 进行归约和分组转换

  • 从 Scala 数据结构创建 DataFrame

  • 以编程方式操作 DataFrame 而不使用 SQL

  • 从外部源加载 DataFrame 并进行设置...

引言

Spark 高效处理大规模数据的三驾马车是 RDD、DataFrames 和 Dataset API。虽然每个都有其独立的价值,但新的范式转变倾向于将 Dataset 作为统一的数据 API,以满足单一接口中的所有数据处理需求。

Spark 2.0 的新 Dataset API 是一种类型安全的领域对象集合,可以通过转换(类似于 RDD 的过滤、mapflatMap() 等)并行使用函数或关系操作。为了向后兼容,Dataset 有一个名为 DataFrame 的视图,它是一个无类型的行集合。在本章中,我们展示了所有三种 API 集。前面的图总结了 Spark 数据处理关键组件的优缺点:

机器学习的高级开发者必须理解并能够无障碍地使用所有三种 API 集,无论是为了算法增强还是遗留原因。虽然我们建议每位开发者都应向高级 Dataset API 迁移,但你仍需了解 RDD,以便针对 Spark 核心系统编程。例如,投资银行和对冲基金经常阅读机器学习、数学规划、金融、统计学或人工智能领域的领先期刊,然后使用低级 API 编码研究以获得竞争优势。

RDDs - 一切的起点...

RDD API 是 Spark 开发者的重要工具,因为它在函数式编程范式中提供了对数据底层控制的偏好。RDD 的强大之处同时也使得新程序员更难以使用。虽然理解 RDD API 和手动优化技术(例如,在 groupBy() 操作之前使用 filter())可能很容易,但编写高级代码需要持续的练习和熟练度。

当数据文件、块或数据结构转换为 RDD 时,数据被分解为称为 分区(类似于 Hadoop 中的拆分)的较小单元,并分布在节点之间,以便它们可以同时并行操作。Spark 直接提供了这种功能...

数据帧——通过高级 API 统一 API 和 SQL 的自然演进

Spark 开发者社区始终致力于从伯克利的 AMPlab 时代开始为社区提供易于使用的高级 API。数据 API 的下一个演进是在 Michael Armbrust 向社区提供 SparkSQL 和 Catalyst 优化器时实现的,这使得使用简单且易于理解的 SQL 接口进行数据虚拟化成为可能。数据帧 API 是利用 SparkSQL 的自然演进,通过将数据组织成关系表那样的命名列来实现。

数据帧 API 通过 SQL 使数据整理对众多熟悉 R(data.frame)或 Python/Pandas(pandas.DataFrame)中的数据帧的数据科学家和开发者可用。

数据集——一个高级的统一数据 API

数据集是一个不可变的对象集合,这些对象被建模/映射到传统的关系模式。有四个属性使其成为未来首选的方法。我们特别发现数据集 API 具有吸引力,因为它与 RDD 相似,具有常规的转换操作符(例如,filter()map()flatMap()等)。数据集将遵循与 RDD 类似的惰性执行范式。尝试调和数据帧和数据集的最佳方式是将数据帧视为可以被认为是Dataset[Row]的别名。

  • 强类型安全:我们现在在统一的数据 API 中既有编译时(语法错误)也有运行时安全,这有助于 ML 开发者...

使用 Spark 2.0 通过内部数据源创建 RDD

在 Spark 中创建 RDD 有四种方式,从用于客户端驱动程序中简单测试和调试的parallelize()方法,到用于近实时响应的流式 RDD。在本节中,我们将提供多个示例,展示如何使用内部数据源创建 RDD。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter3
  1. 导入必要的包:
import breeze.numerics.pow 
import org.apache.spark.sql.SparkSession 
import Array._
  1. 导入用于设置log4j日志级别的包。此步骤是可选的,但我们强烈建议这样做(根据开发周期适当更改级别)。
import org.apache.log4j.Logger 
import org.apache.log4j.Level 
  1. 将日志级别设置为警告和错误以减少输出。参见上一步骤了解包要求。
Logger.getLogger("org").setLevel(Level.ERROR) ...

工作原理...

客户端驱动程序中的数据通过分区 RDD 的数量(第二个参数)作为指导进行并行化和分布。生成的 RDD 是 Spark 的魔力,它开启了这一切(参阅 Matei Zaharia 的原始白皮书)。

生成的 RDD 现在是具有容错性和血统的完全分布式数据结构,可以使用 Spark 框架并行操作。

我们从www.gutenberg.org/读取文本文件查尔斯·狄更斯的《双城记》到 Spark RDDs 中。然后我们继续分割和标记化数据,并使用 Spark 的操作符(例如,mapflatMap()等)打印出总单词数。

使用外部数据源创建 Spark 2.0 的 RDDs

在本配方中,我们为您提供了几个示例,以展示使用外部源创建 RDD。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter3 
  1. 导入必要的包:
import breeze.numerics.pow 
import org.apache.spark.sql.SparkSession 
import Array._
  1. 导入用于设置log4j日志级别的包。这一步是可选的,但我们强烈建议这样做(根据开发周期适当更改级别)。
import org.apache.log4j.Logger 
import org.apache.log4j.Level 
  1. 将日志级别设置为警告和错误,以减少输出。请参阅上一步骤了解包要求。
Logger.getLogger("org").setLevel(Level.ERROR) 
Logger.getLogger("akka").setLevel(Level.ERROR) 
  1. 设置 Spark 上下文和应用程序参数,以便 Spark 可以运行。
val spark = SparkSession 
  .builder 
  .master("local[*]") 
  .appName("myRDD") 
  .config("Spark.sql.warehouse.dir", ".") 
  .getOrCreate()
  1. 我们从古腾堡项目获取数据。这是一个获取实际文本的绝佳来源,涵盖了从莎士比亚全集到查尔斯·狄更斯的作品。

  2. 从以下来源下载文本并将其存储在本地目录中:

  3. 再次,我们使用SparkContext,通过SparkSession可用,并使用其textFile()函数读取外部数据源并在集群上并行化它。值得注意的是,所有工作都是由 Spark 在幕后为开发者完成的,只需一次调用即可加载多种格式(例如,文本、S3 和 HDFS),并使用protocol:filepath组合在集群上并行化数据。

  4. 为了演示,我们加载了这本书,它以 ASCII 文本形式存储,使用SparkContext通过SparkSessiontextFile()方法,后者在幕后工作,并在集群上创建分区 RDDs。

val book1 = spark.sparkContext.textFile("../data/sparkml2/chapter3/a.txt") 

输出将如下所示:

Number of lines = 16271
  1. 尽管我们尚未涉及 Spark 转换操作符,我们将查看一小段代码,该代码使用空格作为分隔符将文件分解成单词。在实际情况下,需要一个正则表达式来处理所有边缘情况以及所有空白变化(请参考本章中的使用 filter() API 的 Spark 中转换 RDDs配方)。

    • 我们使用 lambda 函数接收每行读取的内容,并使用空格作为分隔符将其分解成单词。

    • 我们使用 flatMap 来分解单词列表的数组(即,每行的一组单词对应于该行的不同数组/列表)。简而言之,我们想要的是每行的单词列表,而不是单词列表的列表。

val book2 = book1.flatMap(l => l.split(" ")) 
println(book1.count())

输出将如下所示:

Number of words = 143228  

它是如何工作的...

我们从www.gutenberg.org/读取查尔斯·狄更斯的《双城记》文本文件到一个 RDD 中,然后通过使用空格作为分隔符在 lambda 表达式中使用.split().flatmap()方法对 RDD 本身进行单词分词。然后,我们使用 RDD 的.count()方法输出单词总数。虽然这很简单,但您必须记住,该操作是在 Spark 的分布式并行框架中进行的,仅用了几行代码。

还有更多...

使用外部数据源创建 RDD,无论是文本文件、Hadoop HDFS、序列文件、Casandra 还是 Parquet 文件,都异常简单。再次,我们使用SparkSession(Spark 2.0 之前的SparkContext)来获取集群的句柄。一旦执行了函数(例如,textFile 协议:文件路径),数据就会被分解成更小的部分(分区),并自动流向集群,这些数据作为可以在并行操作中使用的容错分布式集合变得可用。

  1. 在处理实际场景时,必须考虑多种变体。根据我们的经验,最好的建议是在编写自己的函数或连接器之前查阅文档。Spark 要么直接支持您的数据源,要么供应商有一个可下载的连接器来实现相同功能。

  2. 我们经常遇到的另一种情况是,许多小文件(通常在HDFS目录中生成)需要并行化为 RDD 以供消费。SparkContext有一个名为wholeTextFiles()的方法,它允许您读取包含多个文件的目录,并将每个文件作为(文件名, 内容)键值对返回。我们发现这在使用 lambda 架构的多阶段机器学习场景中非常有用,其中模型参数作为批处理计算,然后每天在 Spark 中更新。

在此示例中,我们读取多个文件,然后打印第一个文件以供检查。

spark.sparkContext.wholeTextFiles()函数用于读取大量小文件,并将它们呈现为(K,V),即键值对:

val dirKVrdd = spark.sparkContext.wholeTextFiles("../data/sparkml2/chapter3/*.txt") // place a large number of small files for demo 
println ("files in the directory as RDD ", dirKVrdd) 
println("total number of files ", dirKVrdd.count()) 
println("Keys ", dirKVrdd.keys.count()) 
println("Values ", dirKVrdd.values.count()) 
dirKVrdd.collect() 
println("Values ", dirKVrdd.first()) 

运行前面的代码后,您将得到以下输出:

    files in the directory as RDD ,../data/sparkml2/chapter3/*.txt
    WholeTextFileRDD[10] at wholeTextFiles at myRDD.scala:88)
    total number of files 2
    Keys ,2
    Values ,2
    Values ,(file:/C:/spark-2.0.0-bin-hadoop2.7/data/sparkml2/chapter3/a.txt,
    The Project Gutenberg EBook of A Tale of Two Cities, 
    by Charles Dickens

参见

Spark 文档中关于textFile()wholeTextFiles()函数的说明:

spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.SparkContext

textFile()API 是与外部数据源接口的单一抽象。协议/路径的制定足以调用正确的解码器。我们将演示从 ASCII 文本文件、Amazon AWS S3 和 HDFS 读取,用户可以利用这些代码片段来构建自己的系统。

  • 路径可以表示为简单路径(例如,本地文本文件)到完整的 URI,包含所需协议(例如,s3n 用于 AWS 存储桶),直至具有服务器和端口配置的完整资源路径(例如,从 Hadoop 集群读取 HDFS 文件)。...

使用 Spark 2.0 的 filter() API 转换 RDD

在本食谱中,我们探讨了 RDD 的filter()方法,该方法用于选择基础 RDD 的子集并返回新的过滤 RDD。格式类似于map(),但 lambda 函数决定哪些成员应包含在结果 RDD 中。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动新项目。确保包含必要的 JAR 文件。

  2. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter3
  1. 导入必要的包:
import breeze.numerics.pow 
import org.apache.spark.sql.SparkSession 
import Array._
  1. 导入用于设置log4j日志级别的包。此步骤可选,但我们强烈建议执行(根据开发周期调整级别)。
import org.apache.log4j.Logger 
import org.apache.log4j.Level 
  1. 将日志级别设置为警告和错误,以减少输出。请参阅上一步骤了解包要求。
Logger.getLogger("org").setLevel(Level.ERROR) ...

工作原理...

filter() API 通过几个示例进行了演示。在第一个示例中,我们遍历了一个 RDD,并通过使用 lambda 表达式.filter(i => (i%2) == 1)输出了奇数,该表达式利用了模(取模)函数。

在第二个示例中,我们通过使用 lambda 表达式num.map(pow(_,2)).filter(_ %2 == 1)将结果映射到平方函数,使其变得更有趣。

在第三个示例中,我们遍历文本并使用 lambda 表达式.filter(_.length < 30).filter(_.length > 0)过滤掉短行(例如,长度小于 30 个字符的行),以打印短行与总行数的对比(.count())作为输出。

还有更多...

filter() API 遍历并行分布式集合(即 RDD),并应用作为 lambda 提供给filter()的选择标准,以便将元素包含或排除在结果 RDD 中。结合使用map()(转换每个元素)和filter()(选择子集),在 Spark ML 编程中形成强大组合。

稍后我们将通过DataFrame API 看到,如何使用类似Filter() API 在 R 和 Python(pandas)中使用的高级框架实现相同效果。

另请参阅

使用极其有用的 flatMap() API 转换 RDD

在本节中,我们探讨了常令初学者困惑的flatMap()方法;然而,通过深入分析,我们展示了它是一个清晰的概念,它像 map 一样将 lambda 函数应用于每个元素,然后将结果 RDD 扁平化为单一结构(不再是列表的列表,而是由所有子列表元素构成的单一列表)。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含了必要的 JAR 文件。

  2. 设置程序将驻留的包位置

package spark.ml.cookbook.chapter3 
  1. 导入必要的包
import breeze.numerics.pow 
import org.apache.spark.sql.SparkSession 
import Array._
  1. 导入设置log4j日志级别的包。此步骤可选,但我们强烈建议执行(根据开发周期调整级别)。
import org.apache.log4j.Logger 
import org.apache.log4j.Level 
  1. 将日志级别设置为警告和错误,以减少输出。请参阅上一步骤了解包需求。
Logger.getLogger("org").setLevel(Level.ERROR) 
Logger.getLogger("akka").setLevel(Level.ERROR) 
  1. 设置 Spark 上下文和应用程序参数,以便 Spark 能够运行。
val spark = SparkSession 
  .builder 
  .master("local[*]") 
  .appName("myRDD") 
  .config("Spark.sql.warehouse.dir", ".") 
  .getOrCreate() 
  1. 我们使用textFile()函数从之前下载的文本文件创建初始(即基础 RDD):www.gutenberg.org/cache/epub/98/pg98.txt
val book1 = spark.sparkContext.textFile("../data/sparkml2/chapter3/a.txt")
  1. 我们对 RDD 应用 map 函数以展示map()函数的转换。首先,我们错误地尝试仅使用map()根据正则表达式[\s\W]+]分离所有单词,以说明结果 RDD 是列表的列表,其中每个列表对应一行及其内的分词单词。此例展示了初学者在使用flatMap()时可能遇到的困惑。

  2. 以下代码行修剪每行并将其分割成单词。结果 RDD(即 wordRDD2)将是单词列表的列表,而不是整个文件的单一单词列表。

val wordRDD2 = book1.map(_.trim.split("""[\s\W]+""") ).filter(_.length > 0) 
wordRDD2.take(3)foreach(println(_)) 

运行上述代码后,您将得到以下输出。

[Ljava.lang.String;@1e60b459
[Ljava.lang.String;@717d7587
[Ljava.lang.String;@3e906375
  1. 我们使用flatMap()方法不仅进行映射,还扁平化列表的列表,最终得到由单词本身构成的 RDD。我们修剪并分割单词(即分词),然后筛选出长度大于零的单词,并将其映射为大写。
val wordRDD3 = book1.flatMap(_.trim.split("""[\s\W]+""") ).filter(_.length > 0).map(_.toUpperCase()) 
println("Total number of lines = ", book1.count()) 
println("Number of words = ", wordRDD3.count()) 

在此情况下,使用flatMap()扁平化列表后,我们能如预期般取回单词列表。

wordRDD3.take(5)foreach(println(_)) 

输出如下:

Total number of lines = 16271
Number of words = 141603
THE
PROJECT
GUTENBERG
EBOOK
OF  

它是如何工作的...

在这个简短的示例中,我们读取了一个文本文件,然后使用flatMap(_.trim.split("""[\s\W]+""") lambda 表达式对单词进行分割(即,令牌化),以获得一个包含令牌化内容的单一 RDD。此外,我们使用filter() API filter(_.length > 0)来排除空行,并在输出结果之前使用.map() API 中的 lambda 表达式.map(_.toUpperCase())映射为大写。

在某些情况下,我们不希望为基 RDD 的每个元素返回一个列表(例如,为对应于一行的单词获取一个列表)。有时我们更倾向于拥有一个单一的扁平列表,该列表对应于文档中的每个单词。简而言之,我们不想要一个列表的列表,而是想要一个包含...的单一列表。

还有更多...

glom()函数允许你将 RDD 中的每个分区建模为数组,而不是行列表。虽然在大多数情况下可以产生结果,但glom()允许你减少分区之间的数据移动。

尽管在表面上,文本中提到的第一种和第二种方法在计算 RDD 中的最小数时看起来相似,但glom()函数将通过首先对所有分区应用min(),然后发送结果数据,从而在网络上引起更少的数据移动。要看到差异的最佳方式是在 10M+ RDD 上使用此方法,并相应地观察 IO 和 CPU 使用情况。

  • 第一种方法是在不使用glom()的情况下找到最小值:
val minValue1= numRDD.reduce(_ min _) 
println("minValue1 = ", minValue1)

运行上述代码后,你将得到以下输出:

minValue1 = 1.0
  • 第二种方法是通过使用glom()来找到最小值,这会导致对一个分区进行本地应用的最小函数,然后通过 shuffle 发送结果。
val minValue2 = numRDD.glom().map(_.min).reduce(_ min _) 
println("minValue2 = ", minValue2) 

运行上述代码后,你将得到以下输出:

minValue1 = 1.0  

另请参见

使用集合操作 API 转换 RDD

在本食谱中,我们探索了 RDD 上的集合操作,如intersection()union()subtract()distinct()Cartesian()。让我们以分布式方式实现常规集合操作。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序将驻留的包位置

package spark.ml.cookbook.chapter3
  1. 导入必要的包
import breeze.numerics.pow 
import org.apache.spark.sql.SparkSession 
import Array._
  1. 导入用于设置log4j日志级别的包。此步骤是可选的,但我们强烈建议您(根据开发周期适当更改级别)。
import org.apache.log4j.Logger 
import org.apache.log4j.Level 
  1. 将日志级别设置为警告和错误,以减少输出。请参阅上一步骤了解包要求。
Logger.getLogger("org").setLevel(Level.ERROR) ...

它是如何工作的...

在本例中,我们以三组数字数组(奇数、偶数及其组合)开始,然后将它们作为参数传递给集合操作 API。我们介绍了如何使用intersection()union()subtract()distinct()cartesian() RDD 操作符。

另请参见

虽然 RDD 集合操作符易于使用,但必须注意 Spark 在后台为完成某些操作(例如,交集)而必须进行的数据洗牌。

值得注意的是,union 操作符不会从结果 RDD 集合中删除重复项。

RDD 转换/聚合与groupBy()reduceByKey()

在本食谱中,我们探讨了groupBy()reduceBy()方法,这些方法允许我们根据键对值进行分组。由于内部洗牌,这是一个昂贵的操作。我们首先更详细地演示groupby(),然后介绍reduceBy(),以展示编写这些代码时的相似性,同时强调reduceBy()操作符的优势。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter3 
  1. 导入必要的包:
import breeze.numerics.pow 
import org.apache.spark.sql.SparkSession 
import Array._
  1. 导入用于设置log4j日志级别的包。此步骤是可选的,但我们强烈建议您(根据开发周期适当更改级别):
import org.apache.log4j.Logger 
import org.apache.log4j.Level 
  1. 将日志级别设置为警告和错误,以减少输出。请参阅上一步骤了解包要求。
Logger.getLogger("org").setLevel(Level.ERROR) ...

它是如何工作的...

在本例中,我们创建了数字一到十二,并将它们放置在三个分区中。然后,我们继续使用简单的模运算将它们分解为奇数/偶数。groupBy()用于将它们聚合为两个奇数/偶数组。这是一个典型的聚合问题,对于 SQL 用户来说应该很熟悉。在本章后面,我们将使用DataFrame重新审视此操作,DataFrame也利用了 SparkSQL 引擎提供的更好的优化技术。在后面的部分,我们展示了groupBy()reduceByKey()的相似性。我们设置了一个字母数组(即,ab),然后将它们转换为 RDD。然后,我们根据键(即,唯一的字母 - 在本例中只有两个)进行聚合,并打印每个组的总数。

还有更多...

鉴于 Spark 的发展方向,它更倾向于 Dataset/DataFrame 范式而不是低级 RDD 编码,因此必须认真考虑在 RDD 上执行groupBy()的原因。虽然有些情况下确实需要此操作,但建议读者重新制定解决方案,以利用 SparkSQL 子系统和称为Catalyst的优化器。

Catalyst 优化器在构建优化查询计划时考虑了 Scala 的强大功能,如模式匹配准引用

另请参见

RDD 下的groupBy()reduceByKey()操作文档:

spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.api.java.JavaRDD

使用 zip() API 转换 RDD

在本配方中,我们探讨了zip()函数。对于我们这些在 Python 或 Scala 中工作的人来说,zip()是一个熟悉的方法,它允许你在应用内联函数之前配对项目。使用 Spark,它可以用来促进成对 RDD 之间的算术运算。从概念上讲,它以这样的方式组合两个 RDD,即一个 RDD 的每个成员与第二个 RDD 中占据相同位置的成员配对(即,它对齐两个 RDD 并从成员中制作配对)。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序将驻留的包位置

package spark.ml.cookbook.chapter3 
  1. 导入必要的包
    import org.apache.spark.sql.SparkSession 
  1. 导入用于设置log4j日志级别的包。此步骤是可选的,但我们强烈建议这样做(根据开发周期适当更改级别)。
import org.apache.log4j.Logger 
import org.apache.log4j.Level 
  1. 将日志级别设置为警告和错误,以减少输出。请参阅上一步骤了解包要求。
Logger.getLogger("org").setLevel(Level.ERROR) 
Logger.getLogger("akka").setLevel(Level.ERROR) 
  1. 设置 Spark 上下文和应用程序参数,以便 Spark 能够运行。
val spark = SparkSession 
.builder 
.master("local[*]") 
.appName("myRDD") 
.config("Spark.sql.warehouse.dir", ".") 
.getOrCreate() 
  1. 设置示例的数据结构和 RDD。在本例中,我们创建了两个从Array[]生成的 RDD,并让 Spark 决定分区数量(即,parallize()方法中的第二个参数未设置)。
val SignalNoise: Array[Double] = Array(0.2,1.2,0.1,0.4,0.3,0.3,0.1,0.3,0.3,0.9,1.8,0.2,3.5,0.5,0.3,0.3,0.2,0.4,0.5,0.9,0.1) 
val SignalStrength: Array[Double] = Array(6.2,1.2,1.2,6.4,5.5,5.3,4.7,2.4,3.2,9.4,1.8,1.2,3.5,5.5,7.7,9.3,1.1,3.1,2.1,4.1,5.1) 
val parSN=spark.sparkContext.parallelize(SignalNoise) // parallelized signal noise RDD 
val parSS=spark.sparkContext.parallelize(SignalStrength)  // parallelized signal strength 
  1. 我们对 RDD 应用zip()函数以演示转换。在示例中,我们取分区 RDD 的范围,并使用模函数将其标记为奇数/偶数。我们使用zip()函数将来自两个 RDD(SignalNoiseRDD 和 SignalStrengthRDD)的元素配对,以便我们可以应用map()函数并计算它们的比率(噪声与信号比率)。我们可以使用此技术执行几乎所有类型的算术或非算术操作,涉及两个 RDD 的单个成员。

  2. 两个 RDD 成员的配对行为类似于元组或行。通过zip()创建的配对中的单个成员可以通过其位置访问(例如,._1._2

val zipRDD= parSN.zip(parSS).map(r => r._1 / r._2).collect() 
println("zipRDD=") 
zipRDD.foreach(println) 

运行前面的代码后,您将得到以下输出:

zipRDD=
0.03225806451612903
1.0
0.08333333333333334
0.0625
0.05454545454545454  

工作原理...

在本例中,我们首先设置两个数组,分别代表信号噪声和信号强度。它们只是一系列测量数字,我们可以从物联网平台接收这些数字。然后,我们将两个独立的数组配对,使得每个成员看起来像是原始输入的一对(x, y)。接着,我们通过以下代码片段将配对分割并计算噪声与信号的比率:

val zipRDD= parSN.zip(parSS).map(r => r._1 / r._2) 

zip()方法有许多涉及分区的变体。开发者应熟悉带有分区的zip()方法的变体(例如,zipPartitions)。

另请参阅

使用配对键值 RDD 的连接转换

在本配方中,我们介绍了KeyValueRDD对 RDD 及其支持的连接操作,如join()leftOuterJoinrightOuterJoin()fullOuterJoin(),作为通过集合操作 API 提供的更传统且更昂贵的集合操作(如intersection()union()subtraction()distinct()cartesian()等)的替代方案。

我们将演示join()leftOuterJoinrightOuterJoin()fullOuterJoin(),以解释键值对 RDD 的强大功能和灵活性。

println("Full Joined RDD = ") 
val fullJoinedRDD = keyValueRDD.fullOuterJoin(keyValueCity2RDD) 
fullJoinedRDD.collect().foreach(println(_)) 

如何操作...

  1. 设置示例的数据结构和 RDD:
val keyValuePairs = List(("north",1),("south",2),("east",3),("west",4)) 
val keyValueCity1 = List(("north","Madison"),("south","Miami"),("east","NYC"),("west","SanJose")) 
val keyValueCity2 = List(("north","Madison"),("west","SanJose"))
  1. 将列表转换为 RDD:
val keyValueRDD = spark.sparkContext.parallelize(keyValuePairs) 
val keyValueCity1RDD = spark.sparkContext.parallelize(keyValueCity1) 
val keyValueCity2RDD = spark.sparkContext.parallelize(keyValueCity2) 
  1. 我们可以访问配对 RDD 中的
val keys=keyValueRDD.keys 
val values=keyValueRDD.values 
  1. 我们对配对 RDD 应用mapValues()函数来演示这一转换。在此示例中,我们使用 map 函数将值提升,为每个元素增加 100。这是一种向数据引入噪声(即抖动)的流行技术。
val kvMappedRDD = keyValueRDD.mapValues(_+100) 
kvMappedRDD.collect().foreach(println(_)) 

运行上述代码后,您将得到以下输出:

(north,101)
(south,102)
(east,103)
(west,104)

  1. 我们对 RDD 应用join()函数来演示这一转换。我们使用join()来连接两个 RDD。我们基于键(即北、南等)连接两个 RDD。
println("Joined RDD = ") 
val joinedRDD = keyValueRDD.join(keyValueCity1RDD) 
joinedRDD.collect().foreach(println(_)) 

运行上述代码后,您将得到以下输出:

(south,(2,Miami))
(north,(1,Madison))
(west,(4,SanJose))
(east,(3,NYC))
  1. 我们对 RDD 应用leftOuterJoin()函数来演示这一转换。leftOuterjoin的作用类似于关系左外连接。Spark 用None替换成员资格的缺失,而不是NULL,这在关系系统中很常见。
println("Left Joined RDD = ") 
val leftJoinedRDD = keyValueRDD.leftOuterJoin(keyValueCity2RDD) 
leftJoinedRDD.collect().foreach(println(_)) 

运行上述代码后,您将得到以下输出:

(south,(2,None))
(north,(1,Some(Madison)))
(west,(4,Some(SanJose)))
(east,(3,None))

  1. 我们将对 RDD 应用rightOuterJoin()来演示这一转换。这与关系系统中的右外连接类似。
println("Right Joined RDD = ") 
val rightJoinedRDD = keyValueRDD.rightOuterJoin(keyValueCity2RDD) 
rightJoinedRDD.collect().foreach(println(_)) 

运行上述代码后,您将得到以下输出:

(north,(Some(1),Madison))
(west,(Some(4),SanJose))  
  1. 然后,我们对 RDD 应用fullOuterJoin()函数来演示这一转换。这与关系系统中的全外连接类似。
val fullJoinedRDD = keyValueRDD.fullOuterJoin(keyValueCity2RDD) 
fullJoinedRDD.collect().foreach(println(_)) 

运行上述代码后,您将得到以下输出:

Full Joined RDD = 
(south,(Some(2),None))
(north,(Some(1),Some(Madison)))
(west,(Some(4),Some(SanJose)))
(east,(Some(3),None))

工作原理...

在本食谱中,我们声明了三个列表,代表关系表中可用的典型数据,这些数据可通过连接器导入 Casandra 或 RedShift(为简化本食谱,此处未展示)。我们使用了三个列表中的两个来表示城市名称(即数据表),并将它们与第一个列表连接,该列表代表方向(例如,定义表)。第一步是定义三个配对值的列表。然后我们将它们并行化为键值 RDD,以便我们可以在第一个 RDD(即方向)和其他两个代表城市名称的 RDD 之间执行连接操作。我们对 RDD 应用了 join 函数来演示这一转换。

我们演示了join()leftOuterJoinrightOuterJoin()...

还有更多...

RDD 下join()及其变体的文档可在spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.api.java.JavaRDD找到。

配对键值 RDD 的 reduce 和分组转换

在本食谱中,我们探讨了 reduce 和按 key 分组。reduceByKey()groupbyKey()操作在大多数情况下比reduce()groupBy()更高效且更受青睐。这些函数提供了便捷的设施,通过减少洗牌来聚合值并按 key 组合它们,这在大型数据集上是一个问题。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含了必要的 JAR 文件。

  2. 设置程序将驻留的包位置

package spark.ml.cookbook.chapter3
  1. 导入必要的包
import org.apache.spark.sql.SparkSession 
  1. 导入用于设置log4j日志级别的包。此步骤可选,但我们强烈建议执行(根据开发周期调整级别)。
import org.apache.log4j.Logger 
import org.apache.log4j.Level 
  1. 将日志级别设置为警告和错误以减少输出。请参阅前一步骤了解包要求:
Logger.getLogger("org").setLevel(Level.ERROR) 
Logger.getLogger("akka").setLevel(Level.ERROR)
  1. 设置 Spark 上下文和应用程序参数,以便 Spark 能够运行。
val spark = SparkSession 
  .builder 
  .master("local[*]") 
  .appName("myRDD") 
  .config("Spark.sql.warehouse.dir", ".") 
  .getOrCreate() 
  1. 设置示例所需的数据结构和 RDD:
val signaltypeRDD = spark.sparkContext.parallelize(List(("Buy",1000),("Sell",500),("Buy",600),("Sell",800))) 
  1. 我们应用groupByKey()以演示转换。在此示例中,我们在分布式环境中将所有买卖信号分组在一起。
val signaltypeRDD = spark.sparkContext.parallelize(List(("Buy",1000),("Sell",500),("Buy",600),("Sell",800))) 
val groupedRDD = signaltypeRDD.groupByKey() 
groupedRDD.collect().foreach(println(_)) 

运行前面的代码,您将得到以下输出:

Group By Key RDD = 
(Sell, CompactBuffer(500, 800))
(Buy, CompactBuffer(1000, 600))
  1. 我们对 RDD 对应用reduceByKey()函数以演示转换。在此示例中,该函数用于计算买卖信号的总成交量。Scala 符号(_+_)简单表示每次添加两个成员并从中产生单个结果。就像reduce()一样,我们可以应用任何函数(即简单函数的内联和更复杂情况下的命名函数)。
println("Reduce By Key RDD = ") 
val reducedRDD = signaltypeRDD.reduceByKey(_+_) 
reducedRDD.collect().foreach(println(_))   

运行前面的代码,您将得到以下输出:

Reduce By Key RDD = 
(Sell,1300)
(Buy,1600)  

它是如何工作的...

在此示例中,我们声明了一个商品买卖清单及其对应价格(即典型的商业交易)。然后,我们使用 Scala 简写符号(_+_)计算总和。最后一步,我们为每个键组(即BuySell)提供了总计。键值 RDD 是一个强大的结构,可以在减少代码量的同时提供所需的聚合功能,将配对值分组到聚合桶中。groupByKey()reduceByKey()函数模拟了相同的聚合功能,而reduceByKey()由于在组装最终结果时数据移动较少,因此更高效。

另请参阅

有关 RDD 下的groupByKey()reduceByKey()操作的文档,请访问spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.api.java.JavaRDD

从 Scala 数据结构创建 DataFrames

在本节中,我们探讨了DataFrame API,它为处理数据提供了比 RDD 更高的抽象层次。该 API 类似于 R 和 Python 数据帧工具(pandas)。

DataFrame简化了编码,并允许您使用标准 SQL 检索和操作数据。Spark 保留了关于 DataFrames 的额外信息,这有助于 API 轻松操作框架。每个DataFrame都将有一个模式(从数据推断或显式定义),允许我们像查看 SQL 表一样查看框架。SparkSQL 和 DataFrame 的秘诀在于催化优化器将在幕后工作,通过重新排列管道中的调用来优化访问。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动新项目。确保包含必要的 JAR 文件。

  2. 设置程序所在包的位置:

package spark.ml.cookbook.chapter3 
  1. 设置与 DataFrames 相关的导入以及所需的数据结构,并根据示例需要创建 RDD:
import org.apache.spark.sql._
  1. log4j设置日志级别导入所需的包。此步骤可选,但我们强烈建议执行(根据开发周期调整级别)。
import org.apache.log4j.Logger 
import org.apache.log4j.Level 
  1. 将日志级别设置为警告和错误,以减少输出。有关包要求的详细信息,请参阅前一步骤。
Logger.getLogger("org").setLevel(Level.ERROR) 
Logger.getLogger("akka").setLevel(Level.ERROR)
  1. 设置 Spark 上下文和应用程序参数,以便 Spark 可以运行。
val spark = SparkSession 
  .builder 
  .master("local[*]") 
  .appName("myDataFrame") 
  .config("Spark.sql.warehouse.dir", ".") 
  .getOrCreate() 
  1. 我们设置了两个List()对象和一个序列(即Seq())的 Scala 数据结构。然后,我们将List结构转换为 RDD,以便转换为DataFrames进行后续步骤:
val signaltypeRDD = spark.sparkContext.parallelize(List(("Buy",1000),("Sell",500),("Buy",600),("Sell",800))) 
val numList = List(1,2,3,4,5,6,7,8,9) 
val numRDD = spark.sparkContext.parallelize(numList) 
val myseq = Seq( ("Sammy","North",113,46.0),("Sumi","South",110,41.0), ("Sunny","East",111,51.0),("Safron","West",113,2.0 )) 
  1. 我们取一个列表,使用parallelize()方法将其转换为 RDD,并使用 RDD 的toDF()方法将其转换为 DataFrame。show()方法允许我们查看类似于 SQL 表的 DataFrame。
val numDF = numRDD.toDF("mylist") 
numDF.show 

运行上述代码后,您将获得以下输出:

+------+
|mylist|
+------+
|     1|
|     2|
|     3|
|     4|
|     5|
|     6|
|     7|
|     8|
|     9|
+------+
  1. 在以下代码片段中,我们取一个通用的 Scala Seq序列)数据结构,并使用createDataFrame()显式创建一个 DataFrame,同时命名列。
val df1 = spark.createDataFrame(myseq).toDF("Name","Region","dept","Hours") 
  1. 在接下来的两个步骤中,我们使用show()方法查看内容,然后使用printSchema()方法显示基于类型的推断方案。在此示例中,DataFrame 正确识别了 Seq 中的整数和双精度数作为两个数字列的有效类型。
df1.show() 
df1.printSchema() 

运行上述代码后,您将获得以下输出:

+------+------+----+-----+
|  Name|Region|dept|Hours|
+------+------+----+-----+
| Sammy| North| 113| 46.0|
|  Sumi| South| 110| 41.0|
| Sunny|  East| 111| 51.0|
|Safron|  West| 113|  2.0|
+------+------+----+-----+

root
|-- Name: string (nullable = true)
|-- Region: string (nullable = true)
|-- dept: integer (nullable = false)
|-- Hours: double (nullable = false) 

工作原理...

在本示例中,我们取两个列表和一个 Seq 数据结构,将它们转换为 DataFrame,并使用df1.show()df1.printSchema()显示表的内容和模式。

DataFrames 可以从内部和外部源创建。与 SQL 表类似,DataFrames 具有与之关联的模式,这些模式可以被推断或使用 Scala case 类或map()函数显式转换,同时摄取数据。

还有更多...

为确保完整性,我们包含了在 Spark 2.0.0 之前使用的import语句以运行代码(即,Spark 1.5.2):

import org.apache.spark._
import org.apache.spark.rdd.RDD 
import org.apache.spark.sql.SQLContext 
import org.apache.spark.mllib.linalg 
import org.apache.spark.util 
import Array._
import org.apache.spark.sql._
import org.apache.spark.sql.types 
import org.apache.spark.sql.DataFrame 
import org.apache.spark.sql.Row; 
import org.apache.spark.sql.types.{ StructType, StructField, StringType}; 

另请参阅

DataFrame 文档可在此处找到:spark.apache.org/docs/latest/sql-programming-guide.html

如果遇到隐式转换问题,请确保已包含隐式导入语句。

示例代码适用于 Spark 2.0:

import sqlContext.implicits 

以编程方式操作 DataFrames,无需 SQL

在本教程中,我们探索如何仅通过代码和方法调用(不使用 SQL)来操作数据框。数据框拥有自己的方法,允许您使用编程方式执行类似 SQL 的操作。我们展示了一些命令,如select()show()explain(),以说明数据框本身能够不使用 SQL 进行数据整理和操作。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序将驻留的包位置。

package spark.ml.cookbook.chapter3 
  1. 设置与数据框相关的导入以及所需的数据结构,并根据示例需要创建 RDD。
import org.apache.spark.sql._
  1. 导入设置log4j日志级别的包。此步骤可选,但我们强烈建议执行(根据开发周期调整级别)。
import org.apache.log4j.Logger 
import org.apache.log4j.Level 
  1. 将日志级别设置为警告和错误,以减少输出。请参阅上一步骤了解包要求。
Logger.getLogger("org").setLevel(Level.ERROR) ...

工作原理...

在本例中,我们从文本文件加载数据到 RDD,然后使用.toDF()API 将其转换为数据框结构。接着,我们使用内置方法如select()filter()show()explain()来模拟 SQL 查询,以编程方式探索数据(无需 SQL)。explain()命令显示查询计划,这对于消除瓶颈非常有用。

数据框提供了多种数据整理方法。

对于熟悉数据框 API 和 R 语言包(如cran.r-project.org的 dplyr 或旧版本)的用户,我们提供了一个具有丰富方法集的编程 API,让您可以通过 API 进行所有数据整理。

对于更熟悉 SQL 的用户,您可以简单地使用 SQL 来检索和操作数据,就像使用 Squirrel 或 Toad 查询数据库一样。

还有更多...

为确保完整性,我们包含了在 Spark 2.0.0 之前运行代码(即 Spark 1.5.2)所需的import语句。

import org.apache.spark._  import org.apache.spark.rdd.RDD import org.apache.spark.sql.SQLContext import org.apache.spark.mllib.linalg._ import org.apache.spark.util._ import Array._ import org.apache.spark.sql._ import org.apache.spark.sql.types._ import org.apache.spark.sql.DataFrame import org.apache.spark.sql.Row; import org.apache.spark.sql.types.{ StructType, StructField, StringType};

另请参阅

数据框的文档可在spark.apache.org/docs/latest/sql-programming-guide.html获取。

如果遇到隐式转换问题,请再次检查以确保您已包含隐式import语句。

Spark 2.0 的示例import语句:

import sqlContext.implicits._

从外部源加载数据框并进行设置

在本教程中,我们探讨使用 SQL 进行数据操作。Spark 提供实用且兼容 SQL 的接口,在生产环境中表现出色,我们不仅需要机器学习,还需要使用 SQL 访问现有数据源,以确保与现有 SQL 系统的兼容性和熟悉度。使用 SQL 的数据框在实际环境中实现集成是一个优雅的过程。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter3
  1. 设置与 DataFrame 相关的导入和所需的数据结构,并根据示例需要创建 RDD:
import org.apache.spark.sql._
  1. 导入设置log4j日志级别的包。此步骤是可选的,但我们强烈建议这样做(根据开发周期适当更改级别)。
import org.apache.log4j.Logger
import org.apache.log4j.Level
  1. 将日志级别设置为警告和Error以减少输出。请参阅前面的步骤了解包要求:
Logger.getLogger("org").setLevel(Level.ERROR)
Logger.getLogger("akka").setLevel(Level.ERROR)
  1. 设置 Spark 上下文和应用程序参数,以便 Spark 可以运行。
val spark = SparkSession
 .builder
 .master("local[*]")
 .appName("myDataFrame")
 .config("Spark.sql.warehouse.dir", ".")
 .getOrCreate()
  1. 我们创建对应于customer文件的 DataFrame。在此步骤中,我们首先创建一个 RDD,然后使用toDF()将 RDD 转换为 DataFrame 并命名列。
val customersRDD = spark.sparkContext.textFile("../data/sparkml2/chapter3/customers13.txt") //Customer file 

val custRDD = customersRDD.map {
   line => val cols = line.trim.split(",")
     (cols(0).toInt, cols(1), cols(2), cols(3).toInt) 
} 
val custDF = custRDD.toDF("custid","name","city","age")   

客户数据内容参考:

custDF.show()

运行前面的代码,您将得到以下输出:

  1. 我们创建对应于product文件的 DataFrame。在此步骤中,我们首先创建一个 RDD,然后使用toDF()将 RDD 转换为 DataFrame 并命名列。
val productsRDD = spark.sparkContext.textFile("../data/sparkml2/chapter3/products13.txt") //Product file
 val prodRDD = productsRDD.map {
     line => val cols = line.trim.split(",")
       (cols(0).toInt, cols(1), cols(2), cols(3).toDouble) 
}  
  1. 我们将prodRDD转换为 DataFrame:
val prodDF = prodRDD.toDF("prodid","category","dept","priceAdvertised")
  1. 使用 SQL select,我们显示表格内容。

产品数据内容:

prodDF.show()

运行前面的代码,您将得到以下输出:

  1. 我们创建对应于sales文件的 DataFrame。在此步骤中,我们首先创建一个 RDD,然后使用toDF()将 RDD 转换为 DataFrame 并命名列。
val salesRDD = spark.sparkContext.textFile("../data/sparkml2/chapter3/sales13.txt") *//Sales file* val saleRDD = salesRDD.map {
     line => val cols = line.trim.split(",")
       (cols(0).toInt, cols(1).toInt, cols(2).toDouble)
}
  1. 我们将saleRDD转换为 DataFrame:
val saleDF = saleRDD.toDF("prodid", "custid", "priceSold")  
  1. 我们使用 SQL select 来显示表格。

销售数据内容:

saleDF.show()

运行前面的代码,您将得到以下输出:

  1. 我们打印客户、产品和销售 DataFrame 的架构,以验证列定义和类型转换后的架构:
custDF.printSchema()
productDF.printSchema()
salesDF. printSchema()

运行前面的代码,您将得到以下输出:

root
 |-- custid: integer (nullable = false)
 |-- name: string (nullable = true)
 |-- city: string (nullable = true)
 |-- age: integer (nullable = false)
root
 |-- prodid: integer (nullable = false)
 |-- category: string (nullable = true)
 |-- dept: string (nullable = true)
 |-- priceAdvertised: double (nullable = false)
root
 |-- prodid: integer (nullable = false)
 |-- custid: integer (nullable = false)
 |-- priceSold: double (nullable = false)

它是如何工作的...

在此示例中,我们首先将数据加载到 RDD 中,然后使用toDF()方法将其转换为 DataFrame。DataFrame 非常擅长推断类型,但有时需要手动干预。我们在创建 RDD 后使用map()函数(应用惰性初始化范式)来处理数据,无论是通过类型转换还是调用更复杂的用户定义函数(在map()方法中引用)来进行转换或数据整理。最后,我们继续使用show()printSchema()检查三个 DataFrame 的架构。

还有更多...

为了确保完整性,我们包含了在 Spark 2.0.0 之前用于运行代码的import语句(即,Spark 1.5.2):

import org.apache.spark._
 import org.apache.spark.rdd.RDD
 import org.apache.spark.sql.SQLContext
 import org.apache.spark.mllib.linalg._
 import org.apache.spark.util._
 import Array._
 import org.apache.spark.sql._
 import org.apache.spark.sql.types._
 import org.apache.spark.sql.DataFrame
 import org.apache.spark.sql.Row;
 import org.apache.spark.sql.types.{ StructType, StructField, StringType};

另请参阅

DataFrame 的文档可在spark.apache.org/docs/latest/sql-programming-guide.html找到。

如果遇到隐式转换问题,请再次检查以确保您已包含 implicits import语句。

Spark 1.5.2 的示例import语句:

 import sqlContext.implicits._

使用标准 SQL 语言与 DataFrames - SparkSQL

在本食谱中,我们展示了如何使用 DataFrame 的 SQL 功能执行基本的 CRUD 操作,但没有任何限制您使用 Spark 提供的 SQL 接口达到所需的任何复杂程度(即 DML)。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动新项目。确保包含必要的 JAR 文件。

  2. 设置程序将驻留的包位置

package spark.ml.cookbook.chapter3
  1. 设置与 DataFrames 相关的导入以及所需的数据结构,并根据示例需要创建 RDDs
import org.apache.spark.sql._
  1. 导入用于设置log4j日志级别的包。此步骤是可选的,但我们强烈建议您根据开发周期的不同阶段适当调整级别。
import org.apache.log4j.Logger import org.apache.log4j.Level
  1. 将日志级别设置为警告和ERROR以减少输出。请参阅上一步骤了解包要求。
Logger.getLogger( ...

工作原理...

使用 SQL 的基本 DataFrame 工作流程是首先通过内部 Scala 数据结构或外部数据源填充 DataFrame,然后使用createOrReplaceTempView()调用将 DataFrame 注册为类似 SQL 的工件。

使用 DataFrames 时,您可以利用 Spark 存储的额外元数据(无论是 API 还是 SQL 方法),这可以在编码和执行期间为您带来好处。

虽然 RDD 仍然是核心 Spark 的主力,但趋势是向 DataFrame 方法发展,该方法已成功展示了其在 Python/Pandas 或 R 等语言中的能力。

还有更多...

将 DataFrame 注册为表的方式已发生变化。请参考此内容:

  • 对于 Spark 2.0.0 之前的版本:registerTempTable()

  • 对于 Spark 2.0.0 及更早版本:createOrReplaceTempView()

在 Spark 2.0.0 之前,将 DataFrame 注册为类似 SQL 表的工件:

在我们能够使用 DataFrame 通过 SQL 进行查询之前,我们必须将 DataFrame 注册为临时表,以便 SQL 语句可以引用它而无需任何 Scala/Spark 语法。这一步骤可能会让许多初学者感到困惑,因为我们并没有创建任何表(临时或永久),但调用registerTempTable()在 SQL 领域创建了一个名称,SQL 语句可以引用它而无需额外的 UDF 或无需任何特定领域的查询语言。

  • 注册...

另请参阅

数据框(DataFrame)的文档可在此处获取。

如果遇到隐式转换问题,请再次检查以确保您已包含 implicits import语句。

Spark 1.5.2 的示例import语句

 import sqlContext.implicits._

DataFrame 是一个广泛的子系统,值得用一整本书来介绍。它使 SQL 程序员能够大规模地进行复杂的数据操作。

使用 Scala 序列与数据集 API 协同工作

在本示例中,我们探讨了新的数据集以及它如何与 Scala 数据结构seq协同工作。我们经常看到 LabelPoint 数据结构与 ML 库一起使用,以及与数据集配合良好的 Scala 序列(即 seq 数据结构)之间的关系。

数据集正被定位为未来统一的 API。值得注意的是,DataFrame 仍然可用,作为Dataset[Row]的别名。我们已经通过 DataFrame 的示例广泛地介绍了 SQL 示例,因此我们将重点放在数据集的其他变体上。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序将驻留的包位置

package spark.ml.cookbook.chapter3
  1. 导入必要的包以获取 Spark 会话访问集群,并导入Log4j.Logger以减少 Spark 产生的输出量。
import org.apache.log4j.{Level, Logger}
import org.apache.spark.sql.SparkSession
  1. 定义一个 Scala case class来建模处理数据,Car类将代表电动和混合动力汽车。
case class Car(make: String, model: String, price: Double,
style: String, kind: String)
  1. 让我们创建一个 Scala 序列,并用电动和混合动力汽车填充它。
val *carData* =
*Seq*(
*Car*("Tesla", "Model S", 71000.0, "sedan","electric"),
*Car*("Audi", "A3 E-Tron", 37900.0, "luxury","hybrid"),
*Car*("BMW", "330e", 43700.0, "sedan","hybrid"),
*Car*("BMW", "i3", 43300.0, "sedan","electric"),
*Car*("BMW", "i8", 137000.0, "coupe","hybrid"),
*Car*("BMW", "X5 xdrive40e", 64000.0, "suv","hybrid"),
*Car*("Chevy", "Spark EV", 26000.0, "coupe","electric"),
*Car*("Chevy", "Volt", 34000.0, "sedan","electric"),
*Car*("Fiat", "500e", 32600.0, "coupe","electric"),
*Car*("Ford", "C-Max Energi", 32600.0, "wagon/van","hybrid"),
*Car*("Ford", "Focus Electric", 29200.0, "sedan","electric"),
*Car*("Ford", "Fusion Energi", 33900.0, "sedan","electric"),
*Car*("Hyundai", "Sonata", 35400.0, "sedan","hybrid"),
*Car*("Kia", "Soul EV", 34500.0, "sedan","electric"),
*Car*("Mercedes", "B-Class", 42400.0, "sedan","electric"),
*Car*("Mercedes", "C350", 46400.0, "sedan","hybrid"),
*Car*("Mercedes", "GLE500e", 67000.0, "suv","hybrid"),
*Car*("Mitsubishi", "i-MiEV", 23800.0, "sedan","electric"),
*Car*("Nissan", "LEAF", 29000.0, "sedan","electric"),
*Car*("Porsche", "Cayenne", 78000.0, "suv","hybrid"),
*Car*("Porsche", "Panamera S", 93000.0, "sedan","hybrid"),
*Car*("Tesla", "Model X", 80000.0, "suv","electric"),
*Car*("Tesla", "Model 3", 35000.0, "sedan","electric"),
*Car*("Volvo", "XC90 T8", 69000.0, "suv","hybrid"),
*Car*("Cadillac", "ELR", 76000.0, "coupe","hybrid")
)

  1. 将输出级别配置为ERROR以减少 Spark 的日志输出。
   Logger.getLogger("org").setLevel(Level.ERROR)
   Logger.getLogger("akka").setLevel(Level.ERROR)
  1. 创建一个 SparkSession,以访问 Spark 集群,包括底层会话对象属性和功能。
val spark = SparkSession
.builder
.master("local[*]")
.appName("mydatasetseq")
.config("Spark.sql.warehouse.dir", ".")
.getOrCreate()

  1. 导入 Spark 隐式,从而仅通过导入添加行为。
import spark.implicits._
  1. 接下来,我们将利用 Spark 会话的createDataset()方法从汽车数据序列创建一个数据集。
val cars = spark.createDataset(MyDatasetData.carData) 
// carData is put in a separate scala object MyDatasetData
  1. 让我们打印出结果,以确认我们的方法调用通过调用 show 方法将序列转换为 Spark 数据集。
infecars.show(false)
+----------+--------------+--------+---------+--------+
|make |model |price |style |kind |

  1. 打印出数据集的隐含列名。我们现在可以使用类属性名称作为列名。
cars.columns.foreach(println)
make
model
price
style
kind
  1. 让我们展示自动生成的模式,并验证推断的数据类型。
println(cars.schema)
StructType(StructField(make,StringType,true), StructField(model,StringType,true), StructField(price,DoubleType,false), StructField(style,StringType,true), StructField(kind,StringType,true))
  1. 最后,我们将根据价格对数据集进行过滤,参考Car类属性价格作为列,并展示结果。
cars.filter(cars("price") > 50000.00).show()

  1. 我们通过停止 Spark 会话来关闭程序。
spark.stop()

工作原理...

在本示例中,我们介绍了 Spark 的数据集功能,该功能首次出现在 Spark 1.6 中,并在后续版本中得到进一步完善。首先,我们借助 Spark 会话的createDataset()方法从 Scala 序列创建了一个数据集实例。接下来,我们打印出有关生成数据集的元信息,以确认创建过程如预期进行。最后,我们使用 Spark SQL 片段根据价格列过滤数据集,筛选出价格大于$50,000.00 的记录,并展示最终执行结果。

还有更多...

数据集有一个名为DataFrame的视图,它是的未类型化数据集。数据集仍然保留了 RDD 的所有转换能力,如filter()map()flatMap()等。这就是为什么如果我们使用 RDD 编程 Spark,我们会发现数据集易于使用的原因之一。

另请参阅

  • 数据集文档可在此处找到。

  • KeyValue 分组数据集文档可在此处找到。

  • 关系分组数据集文档可在此处找到。

从 RDD 创建和使用数据集,以及反向操作

在本食谱中,我们探讨了如何使用 RDD 与 Dataset 交互,以构建多阶段机器学习管道。尽管 Dataset(概念上被认为是具有强类型安全的 RDD)是未来的方向,但您仍然需要能够与其他机器学习算法或返回/操作 RDD 的代码进行交互,无论是出于遗留还是编码原因。在本食谱中,我们还探讨了如何创建和从 Dataset 转换为 RDD 以及反向操作。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter3
  1. 为 Spark 会话导入必要的包以访问集群,并使用Log4j.Logger来减少 Spark 产生的输出量。
import org.apache.log4j.{Level, Logger}import org.apache.spark.sql.SparkSession
  1. 定义一个 Scala 样例类来模拟处理数据。
case class Car(make: String, model: String, price: Double,style: String, kind: String)
  1. 让我们创建一个 Scala 序列,并用电动和混合动力汽车填充它。
val carData =Seq(Car("Tesla", "Model S", 71000.0, "sedan","electric"), ...

工作原理...

在本节中,我们将 RDD 转换为 Dataset,最终又转换回 RDD。我们从一个 Scala 序列开始,将其转换为 RDD。创建 RDD 后,调用 Spark 会话的createDataset()方法,将 RDD 作为参数传递,并接收作为结果的 Dataset。

接下来,数据集按制造商列分组,统计各种汽车制造商的存在情况。下一步涉及对特斯拉制造商的数据集进行过滤,并将结果转换回 RDD。最后,我们通过 RDD 的foreach()方法显示了最终的 RDD。

还有更多...

Spark 中的数据集源文件仅包含约 2500+行 Scala 代码。这是一段非常优秀的代码,可以在 Apache 许可证下进行专业化利用。我们列出了以下 URL,并鼓励您至少浏览该文件,了解在使用数据集时缓冲是如何发挥作用的。

数据集的源代码托管在 GitHub 上,地址为github.com/apache/spark/blob/master/sql/core/src/main/scala/org/apache/spark/sql/Dataset.scala

参见

结合使用数据集 API 和 SQL 处理 JSON

在本节中,我们探讨如何使用 JSON 与数据集。在过去的 5 年中,JSON 格式迅速成为数据互操作性的实际标准。

我们探讨数据集如何使用 JSON 并执行 API 命令,如select()。然后,我们通过创建一个视图(即createOrReplaceTempView())并执行 SQL 查询来演示如何使用 API 和 SQL 轻松查询 JSON 文件。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 我们将使用一个名为cars.json的 JSON 数据文件,该文件是为这个示例创建的:

{"make": "Telsa", "model": "Model S", "price": 71000.00, "style": "sedan", "kind": "electric"}
{"make": "Audi", "model": "A3 E-Tron", "price": 37900.00, "style": "luxury", "kind": "hybrid"}
{"make": "BMW", "model": "330e", "price": 43700.00, "style": "sedan", "kind": "hybrid"}
  1. 设置程序将驻留的包位置
package spark.ml.cookbook.chapter3
  1. 为 Spark 会话导入必要的包以访问集群,并使用Log4j.Logger来减少 Spark 产生的输出量。
import org.apache.log4j.{Level, Logger}
import org.apache.spark.sql.SparkSession
  1. 定义一个 Scalacase class来建模处理数据。
case class Car(make: String, model: String, price: Double,
style: String, kind: String)
  1. 将输出级别设置为ERROR,以减少 Spark 的日志输出。
Logger.getLogger("org").setLevel(Level.ERROR)
Logger.getLogger("akka").setLevel(Level.ERROR)
  1. 初始化一个 Spark 会话,创建访问 Spark 集群的入口点。
val spark = SparkSession
.builder
.master("local[*]")
.appName("mydatasmydatasetjsonetrdd")
.config("Spark.sql.warehouse.dir", ".")
.getOrCreate()
  1. 导入 Spark 隐式,从而仅通过导入添加行为。
import spark.implicits._
  1. 现在,我们将 JSON 数据文件加载到内存中,并指定类类型为Car
val cars = spark.read.json("../data/sparkml2/chapter3/cars.json").as[Car]
  1. 让我们打印出我们生成的Car类型数据集中的数据。
cars.show(false)

  1. 接下来,我们将显示数据集的列名,以验证汽车的 JSON 属性名称是否已正确处理。
cars.columns.foreach(println)
make
model
price
style
kind
  1. 让我们查看自动生成的模式并验证推断的数据类型。
println(cars.schema)
StructType(StructField(make,StringType,true), StructField(model,StringType,true), StructField(price,DoubleType,false), StructField(style,StringType,true), StructField(kind,StringType,true))
  1. 在这一步中,我们将选择数据集的make列,通过应用distinct方法去除重复项,并展示结果。
cars.select("make").distinct().show()

  1. 接下来,在 cars 数据集上创建一个视图,以便我们可以对数据集执行一个字面上的 Spark SQL 查询字符串。
cars.createOrReplaceTempView("cars")
  1. 最后,我们执行一个 Spark SQL 查询,筛选数据集中的电动汽车,并仅返回定义的三个列。
spark.sql("select make, model, kind from cars where kind = 'electric'").show()

  1. 我们通过停止 Spark 会话来结束程序。
spark.stop() 

工作原理...

使用 Spark 读取JavaScript 对象表示法(JSON)数据文件并将其转换为数据集非常简单。JSON 在过去几年中已成为广泛使用的数据格式,Spark 对这种格式的支持非常充分。

在第一部分中,我们展示了通过 Spark 会话内置的 JSON 解析功能将 JSON 加载到数据集的方法。您应该注意 Spark 的内置功能,它将 JSON 数据转换为 car 案例类。

在第二部分中,我们展示了如何将 Spark SQL 应用于数据集,以将所述数据整理成理想状态。我们利用数据集的 select 方法检索make列,并应用distinct方法去除...

还有更多...

要全面理解和掌握数据集 API,务必理解RowEncoder的概念。

数据集遵循惰性执行范式,意味着执行仅在 Spark 中调用操作时发生。当我们执行一个操作时,Catalyst 查询优化器生成一个逻辑计划,并为并行分布式环境中的优化执行生成物理计划。请参阅引言中的图表了解所有详细步骤。

Row的文档可在spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.sql.Dataset找到。

Encoder的文档可在spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.sql.Encoder找到。

参见

再次确保下载并探索来自 GitHub 的 Dataset 源文件,该文件约有 2500+行。探索 Spark 源代码是学习 Scala、Scala 注解以及 Spark 2.0 本身高级编程的最佳方式。

对于 Spark 2.0 之前的用户值得注意:

  • SparkSession 是单一入口...

使用 Dataset API 进行领域对象的函数式编程

在本教程中,我们探讨了如何使用 Dataset 进行函数式编程。我们利用 Dataset 和函数式编程将汽车(领域对象)按其车型进行分类。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 使用包指令提供正确的路径

package spark.ml.cookbook.chapter3
  1. 导入必要的包以获取 Spark 上下文对集群的访问权限,并使用Log4j.Logger减少 Spark 产生的输出量。
import org.apache.log4j.{Level, Logger}import org.apache.spark.sql.{Dataset, SparkSession}import spark.ml.cookbook.{Car, mydatasetdata}import scala.collection.mutableimport scala.collection.mutable.ListBufferimport org.apache.log4j.{Level, Logger}import org.apache.spark.sql.SparkSession
  1. 定义一个 Scala 案例类来包含我们处理的数据,我们的汽车类将代表电动和...

工作原理...

在此示例中,我们使用 Scala 序列数据结构来存储原始数据,即一系列汽车及其属性。通过调用createDataset(),我们创建了一个 DataSet 并填充了它。接着,我们使用'make'属性配合groupBymapGroups(),以函数式范式列出按车型分类的汽车。在 DataSet 出现之前,使用领域对象进行这种形式的函数式编程并非不可能(例如,使用 RDD 的案例类或 DataFrame 的 UDF),但 DataSet 结构使得这一过程变得简单且自然。

还有更多...

确保在所有 DataSet 编码中包含implicits声明:

import spark.implicits._

参见

Dataset 的文档可在spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.sql.Dataset访问。

第十二章:实施稳健机器学习系统的常见配方

本章我们将涵盖:

  • Spark 的基本统计 API,助你构建自己的算法

  • 现实生活中的机器学习应用的 ML 管道

  • 使用 Spark 进行数据标准化

  • 数据分割以进行训练和测试

  • 使用新的 Dataset API 进行常见操作

  • 在 Spark 2.0 中从文本文件创建和使用 RDD、DataFrame 与 Dataset

  • Spark ML 中的 LabeledPoint 数据结构

  • 在 Spark 2.0+中访问 Spark 集群

  • 在 Spark 2.0 之前访问 Spark 集群

  • 在 Spark 2.0 中通过 SparkSession 对象访问 SparkContext

  • Spark 2.0 中的新模型导出和 PMML 标记

  • 使用 Spark 2.0 进行回归模型评估

  • 二元分类模型评估...

引言

在各行各业中,无论是经营一家小企业还是开发和维护关键任务应用程序,都有一系列常见的任务需要在执行功能过程中几乎每个工作流程中包含。即使在构建强大的机器学习系统时也是如此。在 Spark 机器学习中,这些任务包括从数据分割以进行模型开发(训练、测试、验证)到标准化输入特征向量数据,再到通过 Spark API 创建 ML 管道。本章提供了一系列配方,旨在帮助读者思考实施端到端机器学习系统实际需要的内容。

本章试图展示在任何稳健的 Spark 机器学习系统实现中存在的多种常见任务。为了避免在本书中每个配方中重复提及这些常见任务,我们将这些常见任务作为本章中的简短配方提取出来,读者在阅读其他章节时可根据需要加以利用。这些配方既可以独立使用,也可以作为大型系统中的管道子任务。请注意,这些常见配方在后续章节中关于机器学习算法的更大背景下得到强调,同时为了完整性,本章也包含了它们作为独立配方。

Spark 的基本统计 API,助你构建自己的算法

在本配方中,我们涵盖了 Spark 的多变量统计摘要(即Statistics.colStats),如相关性、分层抽样、假设检验、随机数据生成、核密度估计器等,这些可以在处理极大数据集时利用 RDD 的并行性和弹性。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动新项目。确保包含必要的 JAR 文件。

  2. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter4
  1. 为 Spark 会话导入必要的包以访问集群,并使用log4j.Logger减少 Spark 产生的输出量:
import org.apache.spark.mllib.linalg.Vectors
import org.apache.spark.mllib.stat.Statistics
import org.apache.spark.sql.SparkSession
import org.apache.log4j.Logger
import org.apache.log4j.Level
  1. 将输出级别设置为ERROR以减少 Spark 的日志输出:
Logger.getLogger("org").setLevel(Level.ERROR)
Logger.getLogger("akka").setLevel(Level.ERROR)
  1. 通过构建器模式初始化 Spark 会话并指定配置,从而为 Spark 集群提供入口点:
val spark = SparkSession
.builder
.master("local[*]")
.appName("Summary Statistics")
.config("spark.sql.warehouse.dir", ".")
.getOrCreate()
  1. 让我们检索底层 SparkContext 的 Spark 会话,以便在生成 RDD 时使用:
val sc = spark.sparkContext
  1. 现在我们使用手工数据创建一个 RDD,以说明摘要统计的使用:
val rdd = sc.parallelize(
  Seq(
    Vectors.dense(0, 1, 0),
    Vectors.dense(1.0, 10.0, 100.0),
    Vectors.dense(3.0, 30.0, 300.0),
    Vectors.dense(5.0, 50.0, 500.0),
    Vectors.dense(7.0, 70.0, 700.0),
    Vectors.dense(9.0, 90.0, 900.0),
    Vectors.dense(11.0, 110.0, 1100.0)
  )
)
  1. 我们通过调用 colStats() 方法并传递 RDD 作为参数来使用 Spark 的统计对象:
val summary = Statistics.colStats(rdd)

colStats() 方法将返回一个 MultivariateStatisticalSummary,其中包含计算出的摘要统计信息:

println("mean:" + summary.mean)
println("variance:" +summary.variance)
println("none zero" + summary.numNonzeros)
println("min:" + summary.min)
println("max:" + summary.max)
println("count:" + summary.count)
mean:[5.142857142857142,51.57142857142857,514.2857142857142]
variance:[16.80952380952381,1663.952380952381,168095.2380952381]
none zero[6.0,7.0,6.0]
min:[0.0,1.0,0.0]
max:[11.0,110.0,1100.0]
count:7
  1. 我们通过停止 Spark 会话来关闭程序:
spark.stop()

工作原理...

我们创建了一个从密集向量数据生成的 RDD,然后使用统计对象在其上生成摘要统计信息。一旦调用 colStats() 方法返回,我们便获取了诸如均值、方差、最小值、最大值等摘要统计信息。

还有更多...

在大数据集上,统计 API 的高效性怎么强调都不为过。这些 API 将为您提供实现任何统计学习算法的基本元素。基于我们的研究和经验,我们鼓励您在实现自己的算法之前,先阅读源代码,确保 Spark 中没有实现相应功能。

虽然我们在此仅演示了基本的统计摘要,但 Spark 自带了以下功能:

  • 相关性:Statistics.corr(seriesX, seriesY, "相关性类型")

    • 皮尔逊(默认)

    • 斯皮尔曼

  • 分层抽样 - RDD API:

    • 有替换的 RDD

    • 无替换 - 需要额外遍历

  • 假设检验:

    • 向量 - Statistics.chiSqTest( 向量 )

    • 矩阵 - Statistics.chiSqTest( 密集矩阵 )

  • 柯尔莫哥洛夫-斯米尔诺夫KS)等同性检验 - 单侧或双侧:

    • Statistics.kolmogorovSmirnovTest(RDD, "norm", 0, 1)
  • 随机数据生成器 - normalRDD()

    • 正态 - 可以指定参数

    • 众多选项加上 map() 以生成任何分布

  • 核密度估计器 - KernelDensity().estimate( data )

统计学中拟合优度概念的快速参考可在en.wikipedia.org/wiki/Goodness_of_fit链接找到。

另请参阅

更多多元统计摘要的文档:

ML 管道,适用于实际机器学习应用

这是涵盖 Spark 2.0 中 ML 管道的两个配方中的第一个。有关 ML 管道的更高级处理,包括 API 调用和参数提取等详细信息,请参阅本书后面的章节。

在本例中,我们尝试构建一个单一管道,该管道能够对文本进行分词,使用 HashingTF(一种老技巧)映射词频,运行回归以拟合模型,并预测新词属于哪个组(例如,新闻过滤、手势分类等)。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含了必要的 JAR 文件。

  2. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter4
  1. 为 Spark 会话导入必要的包以访问集群,并使用log4j.Logger来减少 Spark 产生的输出量:
import org.apache.spark.ml.Pipelineimport org.apache.spark.ml.classification.LogisticRegressionimport org.apache.spark.ml.feature.{HashingTF, Tokenizer}import org.apache.spark.sql.SparkSessionimport org.apache.log4j.{Level, Logger}
  1. 将输出级别设置为ERROR以减少 Spark 的日志输出:
Logger.getLogger("org").setLevel(Level.ERROR)Logger.getLogger("akka" ...

工作原理...

本节中,我们探讨了使用 Spark 构建简单机器学习管道的过程。我们首先创建了一个包含两组文本文档的 DataFrame,随后设置了管道。

首先,我们创建了一个分词器来将文本文档解析为词项,随后创建了 HashingTF 来将这些词项转换为特征。接着,我们创建了一个逻辑回归对象来预测新文本文档属于哪个组。

其次,我们通过传递一个参数数组来构建管道,指定了三个执行阶段。您会注意到,每个后续阶段都使用前一阶段的输出列作为输入,并提供一个指定的结果列。

最后,我们通过在管道对象上调用fit()并定义一组测试数据进行验证来训练模型。接下来,我们使用模型转换测试集,确定测试集中的文本文档属于定义的两个组中的哪一个。

还有更多...

Spark ML 中的管道灵感来源于 Python 中的 scikit-learn,此处为完整性引用:

scikit-learn.org/stable/

机器学习管道使得在 Spark 中结合多个用于实现生产任务的算法变得容易。在实际应用中,通常不会只使用单一算法。往往需要多个协作的机器学习算法共同工作以实现复杂的用例。例如,在基于 LDA 的系统(如新闻简报)或人类情感检测中,在核心系统前后都有多个步骤需要实现为一个单一管道,以产生有意义且适用于生产的系统。请参阅以下链接了解一个实际应用案例,该案例需要...

另请参见

更多多元统计摘要的文档:

使用 Spark 进行数据归一化

在本食谱中,我们展示了在将数据导入 ML 算法之前进行归一化(缩放)。有许多 ML 算法(如支持向量机SVM))在缩放输入向量上比在原始值上工作得更好。

如何操作...

  1. 前往 UCI 机器学习库并下载此文件

  2. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  3. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter4
  1. 导入必要的包,以便 Spark 会话能够访问集群并使用log4j.Logger减少 Spark 产生的输出量:
import org.apache.spark.sql.SparkSession
import org.apache.spark.ml.linalg.{Vector, Vectors}
import org.apache.spark.ml.feature.MinMaxScaler
  1. 定义一个方法,将葡萄酒数据解析为元组:
def parseWine(str: String): (Int, Vector) = {
val columns = str.split(",")
(columns(0).toInt, Vectors.dense(columns(1).toFloat, columns(2).toFloat, columns(3).toFloat))
 }
  1. 将输出级别设置为ERROR以减少 Spark 的日志输出:
Logger.getLogger("org").setLevel(Level.ERROR)
Logger.getLogger("akka").setLevel(Level.ERROR)
  1. 使用构建器模式初始化 Spark 会话,指定配置,从而为 Spark 集群提供入口点:
val spark = SparkSession
.builder
.master("local[*]")
.appName("My Normalize")
.getOrCreate()
  1. 导入spark.implicits,从而仅通过import添加行为:
import spark.implicits._
  1. 让我们将葡萄酒数据加载到内存中,只取前四列,并将后三列转换为一个新的特征向量:
val data = Spark.read.text("../data/sparkml2/chapter4/wine.data").as[String].map(parseWine)
  1. 接下来,我们生成一个包含两列的 DataFrame:
val df = data.toDF("id", "feature")
  1. 现在,我们将打印出 DataFrame 模式并展示 DataFrame 中包含的数据:
df.printSchema()
df.show(false)

  1. 最后,我们生成缩放模型并将特征转换为介于负一和正一之间的公共范围,展示结果:
val scale = new MinMaxScaler()
      .setInputCol("feature")
      .setOutputCol("scaled")
      .setMax(1)
      .setMin(-1)
scale.fit(df).transform(df).select("scaled").show(false)

  1. 通过停止 Spark 会话来关闭程序:
spark.stop()

工作原理...

在本例中,我们探讨了特征缩放,这是大多数机器学习算法(如分类器)中的关键步骤。我们首先加载葡萄酒数据文件,提取标识符,并使用接下来的三列创建特征向量。

接着,我们创建了一个MinMaxScaler对象,配置了一个最小和最大范围,以便将我们的值进行缩放。通过在缩放器类上调用fit()方法来执行缩放模型,然后使用该模型对 DataFrame 中的值进行缩放。

最后,我们展示了结果的 DataFrame,并注意到特征向量值的范围在负 1 到正 1 之间。

还有更多...

通过检查线性代数入门中的单位向量概念,可以更好地理解归一化和缩放的根源。以下是一些关于单位向量的常见参考链接:

对于输入敏感的算法,如 SVM,建议在特征的缩放值(例如,范围从 0 到 1)上训练算法,而不是原始向量表示的绝对值。

另请参阅

MinMaxScaler的文档可在spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.ml.feature.MinMaxScaler获取。

我们想强调MinMaxScaler是一个广泛的 API,它扩展了Estimator(来自 ML 管道的概念),正确使用时可以实现编码效率和高精度结果。

分割数据用于训练和测试

在本食谱中,你将学习使用 Spark 的 API 将可用的输入数据分割成不同的数据集,这些数据集可用于训练和验证阶段。通常使用 80/20 的分割比例,但也可以根据你的偏好考虑其他数据分割方式。

如何操作...

  1. 前往 UCI 机器学习库并下载archive.ics.uci.edu/ml/machine-learning-databases/00359/NewsAggregatorDataset.zip文件。

  2. 在 IntelliJ 或你选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  3. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter4
  1. 为 Spark 会话导入必要的包以访问集群,并导入log4j.Logger以减少 Spark 产生的输出量:
import org.apache.spark.sql.SparkSessionimport org.apache.log4j.{ Level, Logger}
  1. 将输出级别设置为ERROR以减少 Spark 的日志输出:
Logger.getLogger("org").setLevel(Level.ERROR)Logger.getLogger("akka" ...

工作原理...

我们首先加载newsCorpora.csv数据文件,然后通过数据集对象上的randomSplit()方法,我们分割了数据集。

还有更多...

为了验证结果,我们必须设置一个德尔菲技术,其中测试数据对模型来说是完全未知的。详情请参见 Kaggle 竞赛,网址为www.kaggle.com/competitions

一个健壮的机器学习系统需要三种类型的数据集。

  • 训练数据集:用于拟合模型以进行采样。

  • 验证数据集:用于估计拟合模型(由训练集训练)的预测误差或增量。

  • 测试数据集:一旦选定最终模型,用于评估模型的泛化误差。

另请参见

关于randomSplit()的文档可在spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.api.java.JavaRDD@randomSplit(weights:Array%5BDouble%5D):Array%5Borg.apache.spark.api.java.JavaRDD%5BT%5D%5D找到。

randomSplit()是在 RDD 内部调用的方法。尽管 RDD 方法调用的数量可能令人不知所措,但掌握这个 Spark 概念和 API 是必须的。

API 签名如下:

def randomSplit(weights: Array[Double]): Array[JavaRDD[T]]

使用提供的权重随机分割此 RDD。

使用新 Dataset API 的常见操作

在本菜谱中,我们介绍了 Dataset API,这是 Spark 2.0 及以后版本进行数据整理的前进方式。本章中,我们涵盖了一些处理这些新 API 集合所需的常见、重复操作。此外,我们还展示了由 Spark SQL Catalyst 优化器生成的查询计划。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 我们将使用一个名为cars.json的 JSON 数据文件,该文件是为这个示例创建的:

name,city
Bears,Chicago
Packers,Green Bay
Lions,Detroit
Vikings,Minnesota
  1. 设置程序将驻留的包位置。
package spark.ml.cookbook.chapter4
  1. 为 Spark 会话导入必要的包以访问集群,并导入log4j.Logger以减少 Spark 产生的输出量。
import org.apache.spark.ml.Pipeline
import org.apache.spark.ml.classification.LogisticRegression
import org.apache.spark.ml.feature.{HashingTF, Tokenizer}
import org.apache.spark.sql.SparkSession
import org.apache.log4j.{Level, Logger}
  1. 定义一个 Scala case class来建模数据:
case class Team(name: String, city: String)
  1. 将输出级别设置为ERROR以减少 Spark 的日志输出。
Logger.getLogger("org").setLevel(Level.ERROR)
Logger.getLogger("akka").setLevel(Level.ERROR)
  1. 使用构建器模式初始化 Spark 会话并指定配置,从而为 Spark 集群提供入口点。
val spark = SparkSession
.builder
.master("local[*]")
.appName("My Dataset")
.config("spark.sql.warehouse.dir", ".")
.getOrCreate()
  1. 导入spark.implicits,从而通过仅一个导入操作添加行为。
import spark.implicits._
  1. 让我们从一个 Scala 列表创建数据集并打印结果:
val champs = spark.createDataset(List(Team("Broncos", "Denver"), Team("Patriots", "New England")))
champs.show(false)

  1. 接下来,我们将加载一个 CSV 文件到内存中,并将其转换为类型为Team的数据集。
val teams = spark.read
 .option("Header", "true")
 .csv("../data/sparkml2/chapter4/teams.csv")
 .as[Team]

 teams.show(false)

  1. 现在我们通过使用map函数遍历 teams 数据集,生成一个新的城市名称数据集。
val cities = teams.map(t => t.city)
cities.show(false)

  1. 显示检索城市名称的执行计划:
cities.explain()
== Physical Plan ==
*SerializeFromObject [staticinvoke(class org.apache.spark.unsafe.types.UTF8String, StringType, fromString, input[0, java.lang.String, true], true) AS value#26]
+- *MapElements <function1>, obj#25: java.lang.String
+- *DeserializeToObject newInstance(class Team), obj#24: Team
+- *Scan csv [name#9,city#10] Format: CSV, InputPaths: file:teams.csv, PartitionFilters: [], PushedFilters: [], ReadSchema: struct<name:string,city:string>
  1. 最后,我们将teams数据集保存到 JSON 文件中。
teams.write
.mode(SaveMode.Overwrite)
.json("../data/sparkml2/chapter4/teams.json"){"name":"Bears","city":"Chicago"}
{"name":"Packers","city":"Green Bay"}
{"name":"Lions","city":"Detroit"}
{"name":"Vikings","city":"Minnesota"}
  1. 我们通过停止 Spark 会话来关闭程序。
spark.stop()

工作原理...

首先,我们从 Scala 列表创建了一个数据集,并显示输出以验证数据集的创建是否符合预期。其次,我们将一个 逗号分隔值CSV)文件加载到内存中,将其转换为类型为 Team 的数据集。第三,我们对数据集执行了 map() 函数,以构建一个团队城市名称列表,并打印出用于生成数据集的执行计划。最后,我们将之前加载的 teams 数据集持久化为 JSON 格式的文件,以备将来使用。

还有更多...

请注意关于数据集的一些有趣点:

  • 数据集采用 惰性 评估

  • 数据集利用了 Spark SQL Catalyst 优化器

  • 数据集利用了 tungsten 的堆外内存管理

  • 未来两年内,许多系统仍将停留在 Spark 2.0 之前,因此出于实际考虑,您必须学习并掌握 RDD 和 DataFrame。

参见

Dataset 的文档可在 spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.sql.Dataset 获取。

Spark 2.0 中从文本文件创建和使用 RDD 与 DataFrame 与 Dataset

在本示例中,我们通过一段简短的代码探索了从文本文件创建 RDD、DataFrame 和 Dataset 的细微差别以及它们之间的关系:

Dataset: spark.read.textFile()
RDD: spark.sparkContext.textFile()
DataFrame: spark.read.text()

假设 spark 是会话名称

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含了必要的 JAR 文件。

  2. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter4
  1. 为 Spark 会话导入必要的包以访问集群,并导入 log4j.Logger 以减少 Spark 产生的输出量:
import org.apache.log4j.{Level, Logger}import org.apache.spark.sql.SparkSession
  1. 我们还定义了一个 case class 来承载所使用的数据:
case class Beatle(id: Long, name: String)
  1. 将输出级别设置为 ERROR 以减少 Spark 的日志输出:
Logger.getLogger("org").setLevel(Level.ERROR)
  1. 通过构建器模式初始化 Spark 会话并指定配置,从而...

工作原理...

我们使用相同文本文件的类似方法创建了 RDD、DataFrame 和 Dataset 对象,并使用 getClass 方法确认了类型:

Dataset: spark.read.textFile
RDD: spark.sparkContext.textFile
DataFrame: spark.read.text

请注意,它们非常相似,有时会令人困惑。Spark 2.0 已将 DataFrame 转变为 Dataset[Row] 的别名,使其真正成为一个数据集。我们展示了前面的方法,以便用户选择示例来创建自己的数据类型风格。

还有更多...

数据类型的文档可在 spark.apache.org/docs/latest/sql-programming-guide.html 获取。

如果您不确定手头有什么样的数据结构(有时差异并不明显),请使用 getClass 方法进行验证。

Spark 2.0 已将 DataFrame 转变为 Dataset[Row] 的别名。虽然 RDD 和 Dataram 在不久的将来仍然完全可行,但最好学习和使用数据集编写新项目。

参见

RDD 和 Dataset 的文档可在以下网站获取:

Spark ML 中的 LabeledPoint 数据结构

LabeledPoint是一种自早期以来就存在的数据结构,用于打包特征向量及其标签,以便用于无监督学习算法。我们展示了一个简短的示例,使用 LabeledPoint、Seq数据结构和 DataFrame 来运行二元分类数据的逻辑回归。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter4
  1. 导入必要的包以使 SparkContext 能够访问集群:
import org.apache.spark.ml.feature.LabeledPoint
import org.apache.spark.ml.linalg.Vectors
import org.apache.spark.ml.classification.LogisticRegression
import org.apache.spark.sql._

  1. 创建 Spark 配置和 SparkContext,以便我们能够访问集群:
val spark = SparkSession
.builder
.master("local[*]")
.appName("myLabeledPoint")
.config("spark.sql.warehouse.dir", ".")
.getOrCreate()
  1. 我们创建 LabeledPoint,使用SparseVectorDenseVector。在以下代码块中,前四个 LabeledPoint 通过DenseVector创建,最后两个 LabeledPoint 通过SparseVector创建:
val myLabeledPoints = spark.createDataFrame(Seq(
 LabeledPoint(1.0, Vectors.dense(0.0, 1.1, 0.1)),
 LabeledPoint(0.0, Vectors.dense(2.0, 1.0, -1.0)),
 LabeledPoint(0.0, Vectors.dense(2.0, 1.3, 1.0)),
 LabeledPoint(1.0, Vectors.dense(0.0, 1.2, -0.5)),

 LabeledPoint(0.0, Vectors.sparse(3, Array(0,2), Array(1.0,3.0))),
 LabeledPoint(1.0, Vectors.sparse(3, Array(1,2), Array(1.2,-0.4)))

 ))

DataFrame 对象从前述 LabeledPoint 创建。

  1. 我们验证原始数据计数和处理数据计数。

  2. 您可以对创建的 DataFrame 调用show()函数:

myLabeledPoints.show()
  1. 您将在控制台看到以下内容:

  2. 我们基于刚创建的数据结构创建了一个简单的 LogisticRegression 模型:

val lr = new LogisticRegression()

 lr.setMaxIter(5)
 .setRegParam(0.01)
 val model = lr.fit(myLabeledPoints)

 println("Model was fit using parameters: " + model.parent.extractParamMap())

在控制台中,将显示以下model参数:

Model was fit using parameters: {
 logreg_6aebbb683272-elasticNetParam: 0.0,
 logreg_6aebbb683272-featuresCol: features,
 logreg_6aebbb683272-fitIntercept: true,
 logreg_6aebbb683272-labelCol: label,
 logreg_6aebbb683272-maxIter: 5,
 logreg_6aebbb683272-predictionCol: prediction,
 logreg_6aebbb683272-probabilityCol: probability,
 logreg_6aebbb683272-rawPredictionCol: rawPrediction,
 logreg_6aebbb683272-regParam: 0.01,
 logreg_6aebbb683272-standardization: true,
 logreg_6aebbb683272-threshold: 0.5,
 logreg_6aebbb683272-tol: 1.0E-6
}
  1. 随后通过停止 Spark 会话来关闭程序:
spark.stop()

工作原理...

我们使用 LabeledPoint 数据结构来建模特征并驱动逻辑回归模型的训练。首先定义了一组 LabeledPoints,用于创建 DataFrame 以进行进一步处理。接着,我们创建了一个逻辑回归对象,并将 LabeledPoint DataFrame 作为参数传递给它,以便训练我们的模型。Spark ML API 设计得与 LabeledPoint 格式兼容良好,且几乎无需干预。

还有更多内容...

LabeledPoint 是一种常用结构,用于将数据打包为Vector + Label,适用于监督机器学习算法。LabeledPoint 的典型布局如下所示:

Seq( 
LabeledPoint (Label, Vector(data, data, data)) 
...... 
LabeledPoint (Label, Vector(data, data, data)) 
) 

请注意,不仅密集向量,稀疏向量也可以与 LabeledPoint 配合使用,这在测试和开发期间,如果驱动程序中驻留了大型且稀疏的数据集,将会在效率上产生巨大差异。

另请参阅

获取 Spark 2.0 中 Spark 集群的访问权限

在本示例中,我们演示了如何通过名为 SparkSession 的单点访问来获取 Spark 集群的访问权限。Spark 2.0 将多个上下文(如 SQLContext、HiveContext)抽象为一个入口点 SparkSession,允许您以统一的方式访问所有 Spark 子系统。

操作方法...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序所在包的位置:

package spark.ml.cookbook.chapter4
  1. 导入必要的包以通过 SparkContext 访问集群。

  2. Spark 2.x 中,SparkSession 更常用。

import org.apache.spark.sql.SparkSession
  1. 创建 Spark 配置和 SparkSession,以便我们能够访问集群:
val spark = SparkSession.builder.master("local[*]") // if use cluster master("spark://master:7077").appName("myAccesSparkCluster20").config("spark.sql.warehouse.dir", ".").getOrCreate()

上述代码利用 master() 函数来设置集群类型...

工作原理...

在本例中,我们展示了如何使用本地和远程选项连接到 Spark 集群。首先,我们创建一个 SparkSession 对象,通过 master() 函数指定集群是本地还是远程,从而授予我们访问 Spark 集群的权限。您还可以通过在启动客户端程序时传递 JVM 参数来指定主节点位置。此外,您可以配置应用程序名称和工作数据目录。接下来,调用 getOrCreate() 方法创建新的 SparkSession 或为您提供现有会话的引用。最后,我们执行一个小型示例程序以验证 SparkSession 对象创建的有效性。

还有更多...

Spark 会话拥有众多可设置和使用的参数及 API,但建议查阅 Spark 文档,因为其中一些方法/参数被标记为实验性或留空 - 对于非实验性状态(我们上次检查时至少有 15 个)。

另一个需要注意的变化是使用 spark.sql.warehouse.dir 来指定表的位置。Spark 2.0 使用 spark.sql.warehouse.dir 设置仓库位置以存储表,而不是 hive.metastore.warehouse.dirspark.sql.warehouse.dir 的默认值为 System.getProperty("user.dir")

更多详情请参阅 spark-defaults.conf

同样值得注意的是以下几点:

  • 我们最喜欢的 Spark 中一些有趣且有用的 API...

另请参阅

SparkSession API 文档可在此处获取。

在 Spark 2.0 之前访问 Spark 集群

这是一份Spark 2.0 之前的指南,但对于希望快速比较和对比集群访问方式,以便将 Spark 2.0 之前程序迁移至 Spark 2.0 新范式的开发者来说,将大有裨益。

操作方法...

  1. 在 IntelliJ 或您选择的 IDE 中新建项目。确保包含必要的 JAR 文件。

  2. 设置程序所在包的位置:

package spark.ml.cookbook.chapter4
  1. 为 SparkContext 导入必要包以访问集群:
import org.apache.spark.{SparkConf, SparkContext}
  1. 创建 Spark 配置及 SparkContext 以便访问集群:
val conf = new SparkConf()
.setAppName("MyAccessSparkClusterPre20")
.setMaster("local[4]") // if cluster setMaster("spark://MasterHostIP:7077")
.set("spark.sql.warehouse.dir", ".")

val sc = new SparkContext(conf)

上述代码利用setMaster()函数设置集群主节点位置。如您所见,我们正运行于本地模式。

若两者同时存在,代码中集群主参数设置将覆盖-D选项值。

以下是三种不同模式下连接集群的示例方法:

  1. 运行于本地模式:
setMaster("local")
  1. 运行于集群模式:
setMaster("spark://yourmasterhostIP:port")
  1. 传递主节点值:
-Dspark.master=local

  1. 我们使用上述 SparkContext 读取 CSV 文件,并将其解析为 Spark 中的数据,代码如下:
val file = sc.textFile("../data/sparkml2/chapter4/mySampleCSV.csv")
val headerAndData = file.map(line => line.split(",").map(_.trim))
val header = headerAndData.first
val data = headerAndData.filter(_(0) != header(0))
val maps = data.map(splits => header.zip(splits).toMap)
  1. 我们将示例结果打印至控制台:
val result = maps.take(4)
result.foreach(println)
  1. 控制台将显示如下内容:

  2. 随后,我们通过停止 SparkContext 来关闭程序:

sc.stop()

工作原理...

本例展示如何在 Spark 2.0 之前通过本地和远程模式连接至 Spark 集群。首先,我们创建一个SparkConf对象并配置所有必需参数。我们将指定主节点位置、应用名称及工作数据目录。接着,我们创建 SparkContext,将SparkConf作为参数传递以访问 Spark 集群。此外,启动客户端程序时,您可通过传递 JVM 参数指定主节点位置。最后,我们执行一个小型示例程序以验证 SparkContext 运行正常。

更多内容...

Spark 2.0 之前,访问 Spark 集群通过SparkContext实现。

对子系统(如 SQL)的访问需通过特定名称上下文(例如,SQLContext**)。

Spark 2.0 通过创建单一统一访问点(即SparkSession)改变了我们访问集群的方式。

相关内容

SparkContext 文档可在此查阅

Spark 2.0 中通过 SparkSession 对象访问 SparkContext

在本教程中,我们展示了如何在 Spark 2.0 中通过 SparkSession 对象获取 SparkContext。本教程将演示 RDD 到 Dataset 的创建、使用以及来回转换。这样做的重要性在于,尽管我们倾向于使用 Dataset,但我们仍需能够使用和增强主要利用 RDD 的遗留(预 Spark 2.0)代码。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序所在包的位置:

package spark.ml.cookbook.chapter4
  1. 为 Spark 会话导入必要的包以访问集群,并使用log4j.Logger减少 Spark 产生的输出量:
import org.apache.log4j.{Level, Logger}import org.apache.spark.sql.SparkSessionimport scala.util.Random
  1. 将输出级别设置为ERROR以减少 Spark 的日志输出:
Logger.getLogger("org").setLevel(Level.ERROR)
  1. 通过构建器模式初始化 Spark 会话并指定配置,从而为 Spark 集群提供入口点:
val session = SparkSession ...

工作原理...

我们使用 SparkContext 创建了 RDD;这在 Spark 1.x 中广泛使用。我们还展示了在 Spark 2.0 中使用 Session 对象创建 Dataset 的方法。为了处理生产环境中的预 Spark 2.0 代码,这种来回转换是必要的。

本教程的技术要点是,尽管 Dataset 是未来数据处理的首选方法,我们始终可以使用 API 在 RDD 和 Dataset 之间来回转换。

还有更多...

关于数据类型的更多信息,请访问spark.apache.org/docs/latest/sql-programming-guide.html

另请参阅

SparkContext 和 SparkSession 的文档可在以下网站找到:

Spark 2.0 中的新模型导出和 PMML 标记

在本教程中,我们探讨了 Spark 2.0 中的模型导出功能,以使用预测模型标记语言PMML)。这种基于 XML 的标准语言允许您在其他系统上导出和运行模型(存在一些限制)。更多信息,请参阅还有更多...部分。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序所在包的位置:

package spark.ml.cookbook.chapter4
  1. 为 SparkContext 导入必要的包以访问集群:
import org.apache.spark.mllib.linalg.Vectors
import org.apache.spark.sql.SparkSession
import org.apache.spark.mllib.clustering.KMeans
  1. 创建 Spark 的配置和 SparkContext:
val spark = SparkSession
.builder
.master("local[*]")   // if use cluster master("spark://master:7077")
.appName("myPMMLExport")
.config("spark.sql.warehouse.dir", ".")
.getOrCreate()
  1. 我们从文本文件中读取数据;数据文件包含一个用于 KMeans 模型的示例数据集:
val data = spark.sparkContext.textFile("../data/sparkml2/chapter4/my_kmeans_data_sample.txt")

val parsedData = data.map(s => Vectors.dense(s.split(' ').map(_.toDouble))).cache()
  1. 我们设置了 KMeans 模型的参数,并使用前面提到的数据集和参数来训练模型:
val numClusters = 2
val numIterations = 10
val model = KMeans.train(parsedData, numClusters, numIterations)
  1. 我们有效地从刚刚创建的数据结构中创建了一个简单的 KMeans 模型(通过将集群数量设置为 2)。
println("MyKMeans PMML Model:\n" + model.toPMML)

在控制台中,将显示以下模型:

MyKMeans PMML Model:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<PMML version="4.2" >
    <Header description="k-means clustering">
        <Application name="Apache Spark MLlib" version="2.0.0"/>
        <Timestamp>2016-11-06T13:34:57</Timestamp>
    </Header>
    <DataDictionary numberOfFields="3">
        <DataField name="field_0" optype="continuous" dataType="double"/>
        <DataField name="field_1" optype="continuous" dataType="double"/>
        <DataField name="field_2" optype="continuous" dataType="double"/>
    </DataDictionary>
    <ClusteringModel modelName="k-means" functionName="clustering" modelClass="centerBased" numberOfClusters="2">
        <MiningSchema>
            <MiningField name="field_0" usageType="active"/>
            <MiningField name="field_1" usageType="active"/>
            <MiningField name="field_2" usageType="active"/>
        </MiningSchema>
        <ComparisonMeasure kind="distance">
            <squaredEuclidean/>
        </ComparisonMeasure>
        <ClusteringField field="field_0" compareFunction="absDiff"/>
        <ClusteringField field="field_1" compareFunction="absDiff"/>
        <ClusteringField field="field_2" compareFunction="absDiff"/>
        <Cluster name="cluster_0">
            <Array n="3" type="real">9.06 9.179999999999998 9.12</Array>
        </Cluster>
        <Cluster name="cluster_1">
            <Array n="3" type="real">0.11666666666666665 0.11666666666666665 0.13333333333333333</Array>
        </Cluster>
    </ClusteringModel>
</PMML>
  1. 然后我们将 PMML 导出到数据目录中的 XML 文件:
model.toPMML("../data/sparkml2/chapter4/myKMeansSamplePMML.xml")

  1. 然后我们通过停止 Spark 会话来关闭程序:
spark.stop()

它是如何工作的...

在花费时间训练模型后,下一步将是持久化模型以供将来使用。在本教程中,我们首先训练了一个 KMeans 模型,以生成后续步骤中用于持久化的模型信息。一旦我们有了训练好的模型,我们就调用模型的toPMML()方法将其转换为 PMML 格式以便存储。该方法的调用会生成一个 XML 文档,然后该 XML 文档文本可以轻松地持久化到文件中。

还有更多...

PMML 是由数据挖掘组DMG)开发的标准。该标准通过允许您在一个系统上构建,然后部署到生产中的另一个系统,实现了跨平台的互操作性。PMML 标准已经获得了动力,并已被大多数供应商采用。其核心是基于一个 XML 文档,包含以下内容:

  • 包含一般信息的头部

  • 字典描述了第三组件(模型)使用的字段级定义。

  • 模型结构和参数

截至本文撰写时,Spark 2.0 机器学习库对 PMML 导出的支持目前仅限于:

  • 线性回归

  • 逻辑回归

  • 岭回归

  • Lasso

  • SVM

  • KMeans

您可以将模型导出到 Spark 支持的以下文件类型:

  • 本地文件系统:
Model_a.toPMML("/xyz/model-name.xml")
  • 分布式文件系统:
Model_a.toPMML(SparkContext, "/xyz/model-name")
  • 输出流——充当管道:
Model_a.toPMML(System.out)

另请参见

关于PMMLExportable API 的文档可以在spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.mllib.pmml.PMMLExportable找到。

使用 Spark 2.0 进行回归模型评估

在本教程中,我们探讨了如何评估回归模型(本例中为回归决策树)。Spark 提供了回归度量工具,该工具具有基本的统计功能,如均方误差MSE)、R 平方等,开箱即用。

本教程的目标是理解 Spark 原生提供的评估指标。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含了必要的 JAR 文件。

  2. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter4
  1. 导入必要的 SparkContext 包以访问集群:
import org.apache.spark.mllib.evaluation.RegressionMetricsimport org.apache.spark.mllib.linalg.Vectorsimport org.apache.spark.mllib.regression.LabeledPointimport org.apache.spark.mllib.tree.DecisionTreeimport org.apache.spark.sql.SparkSession
  1. 创建 Spark 的配置和 SparkContext:
val spark = SparkSession.builder.master("local[*]").appName("myRegressionMetrics").config("spark.sql.warehouse.dir", ".").getOrCreate()
  1. 我们利用了...

它是如何工作的...

在本教程中,我们探讨了生成回归度量以帮助评估我们的回归模型。我们首先加载了一个乳腺癌数据文件,然后以 70/30 的比例将其分割,创建了训练和测试数据集。接下来,我们训练了一个决策树回归模型,并利用它对测试集进行预测。最后,我们获取了这些预测结果,并生成了回归度量,这些度量为我们提供了平方误差、R 平方、平均绝对误差和解释方差。

还有更多...

我们可以使用RegressionMetrics()来生成以下统计量:

  • 均方误差(MSE)

  • 均方根误差(RMSE)

  • R 平方

  • 平均绝对误差(MAE)

  • 解释方差

关于回归验证的文档可在en.wikipedia.org/wiki/Regression_validation找到。

R 平方/决定系数可在en.wikipedia.org/wiki/Coefficient_of_determination找到。

另请参阅

使用 Spark 2.0 进行二分类模型评估

在本教程中,我们展示了在 Spark 2.0 中使用BinaryClassificationMetrics工具及其应用于评估具有二元结果(例如,逻辑回归)的模型。

这里的重点不是展示回归本身,而是演示如何使用常见的度量标准(如接收者操作特征(ROC)、ROC 曲线下的面积、阈值等)来评估它。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含了必要的 JAR 文件。

  2. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter4
  1. 为 SparkContext 导入必要的包以访问集群:
import org.apache.spark.sql.SparkSession
import org.apache.spark.mllib.classification.LogisticRegressionWithLBFGS
import org.apache.spark.mllib.evaluation.BinaryClassificationMetrics
import org.apache.spark.mllib.regression.LabeledPoint
import org.apache.spark.mllib.util.MLUtils
  1. 创建 Spark 配置和 SparkContext:
val spark = SparkSession
.builder
.master("local[*]")
.appName("myBinaryClassification")
.config("spark.sql.warehouse.dir", ".")
.getOrCreate()
  1. 我们从 UCI 下载原始数据集,并对其进行修改以适应代码需求:
// Load training data in LIBSVM format
//https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/binary.html
val data = MLUtils.loadLibSVMFile(spark.sparkContext, "../data/sparkml2/chapter4/myBinaryClassificationData.txt")

该数据集是一个修改过的数据集。原始的成人数据集有 14 个特征,其中 6 个是连续的,8 个是分类的。在这个数据集中,连续特征被离散化为分位数,每个分位数由一个二进制特征表示。我们修改了数据以适应代码的目的。数据集特征的详细信息可在archive.ics.uci.edu/ml/index.php UCI 网站上找到。

  1. 我们将数据集以 60:40 的随机比例分割为训练和测试部分,然后获取模型:
val Array(training, test) = data.randomSplit(Array(0.6, 0.4), seed = 11L)
 training.cache()

 // Run training algorithm to build the model
val model = new LogisticRegressionWithLBFGS()
 .setNumClasses(2)
 .run(training)
  1. 我们使用训练数据集创建的模型来进行预测:
val predictionAndLabels = test.map { case LabeledPoint(label, features) =>
 val prediction = model.predict(features)
 (prediction, label)
 }
  1. 我们从预测结果创建了BinaryClassificationMetrics对象,并开始对度量进行评估:
val metrics = new BinaryClassificationMetrics(predictionAndLabels)
  1. 我们在控制台输出了按阈值的精确度:
val precision = metrics.precisionByThreshold
 precision.foreach { case (t, p) =>
 println(s"Threshold: $t, Precision: $p")
 }

从控制台输出中:

Threshold: 2.9751613212299755E-210, Precision: 0.5405405405405406
Threshold: 1.0, Precision: 0.4838709677419355
Threshold: 1.5283665404870175E-268, Precision: 0.5263157894736842
Threshold: 4.889258814400478E-95, Precision: 0.5
  1. 我们在控制台输出了recallByThreshold
val recall = metrics.recallByThreshold
 recall.foreach { case (t, r) =>
 println(s"Threshold: $t, Recall: $r")
 }

从控制台输出中:

Threshold: 1.0779893231660571E-300, Recall: 0.6363636363636364
Threshold: 6.830452412352692E-181, Recall: 0.5151515151515151
Threshold: 0.0, Recall: 1.0
Threshold: 1.1547199216963482E-194, Recall: 0.5757575757575758
  1. 我们在控制台输出了fmeasureByThreshold
val f1Score = metrics.fMeasureByThreshold
 f1Score.foreach { case (t, f) =>
 println(s"Threshold: $t, F-score: $f, Beta = 1")
 }

从控制台输出中:

Threshold: 1.0, F-score: 0.46874999999999994, Beta = 1
Threshold: 4.889258814400478E-95, F-score: 0.49230769230769234, Beta = 1
Threshold: 2.2097791212639423E-117, F-score: 0.48484848484848486, Beta = 1

val beta = 0.5
val fScore = metrics.fMeasureByThreshold(beta)
f1Score.foreach { case (t, f) =>
  println(s"Threshold: $t, F-score: $f, Beta = 0.5")
}

从控制台输出中:

Threshold: 2.9751613212299755E-210, F-score: 0.5714285714285714, Beta = 0.5
Threshold: 1.0, F-score: 0.46874999999999994, Beta = 0.5
Threshold: 1.5283665404870175E-268, F-score: 0.5633802816901409, Beta = 0.5
Threshold: 4.889258814400478E-95, F-score: 0.49230769230769234, Beta = 0.5
  1. 我们在控制台输出了Precision Recall 曲线下的面积
val auPRC = metrics.areaUnderPR
println("Area under precision-recall curve = " + auPRC)

从控制台输出中:

Area under precision-recall curve = 0.5768388996048239
  1. 我们在控制台输出了 ROC 曲线下的面积:
val thresholds = precision.map(_._1)

val roc = metrics.roc

val auROC = metrics.areaUnderROC
println("Area under ROC = " + auROC)

从控制台输出中:

Area under ROC = 0.6983957219251337
  1. 然后我们通过停止 Spark 会话来关闭程序:
spark.stop()

工作原理...

在本示例中,我们研究了二分类度量的评估。首先,我们加载了libsvm格式的数据,并按 60:40 的比例分割,生成了训练集和测试集。接着,我们训练了一个逻辑回归模型,并从测试集中生成预测。

在我们得到预测结果后,我们创建了一个二分类度量对象。最后,我们获取了真阳性率、阳性预测值、接收者操作特征曲线、接收者操作特征曲线下的面积、精确召回曲线下的面积和 F 度量,以评估模型的适应性。

还有更多...

Spark 提供了以下度量以方便评估:

  • TPR - 真阳性率

  • PPV - 阳性预测值

  • F - F 度量

  • ROC - 接收者操作特征曲线

  • AUROC - 接收者操作特征曲线下的面积

  • AUORC - 精确召回曲线下的面积

以下链接提供了度量的良好入门材料:

另请参见

原始数据集信息的文档可在以下链接获得:

二分类度量的文档可在此处获得。

使用 Spark 2.0 进行多类别分类模型评估

在本示例中,我们探讨了MulticlassMetrics,它允许你评估一个将输出分类为两个以上标签(例如,红色、蓝色、绿色、紫色、未知)的模型。它突出了混淆矩阵(confusionMatrix)和模型准确性的使用。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter4
  1. 为 SparkContext 导入必要的包以访问集群:
import org.apache.spark.sql.SparkSessionimport org.apache.spark.mllib.classification.LogisticRegressionWithLBFGSimport org.apache.spark.mllib.evaluation.MulticlassMetricsimport org.apache.spark.mllib.regression.LabeledPointimport org.apache.spark.mllib.util.MLUtils
  1. 创建 Spark 的配置和 SparkContext:
val spark = SparkSession.builder.master("local[*]").appName("myMulticlass").config("spark.sql.warehouse.dir", ".").getOrCreate() ...

工作原理...

在本节中,我们探讨了为多分类模型生成评估指标。首先,我们将鸢尾花数据加载到内存中,并按 60:40 的比例分割。其次,我们使用三个分类训练了一个逻辑回归模型。第三,我们使用测试数据集进行预测,并利用MultiClassMetric生成评估测量。最后,我们评估了诸如模型准确性、加权精度、加权召回率、加权 F1 分数、加权假阳性率等指标。

还有更多...

尽管本书的范围不允许对混淆矩阵进行全面处理,但提供了一个简短的解释和一个链接作为快速参考。

混淆矩阵只是一个错误矩阵的华丽名称。它主要用于无监督学习中以可视化性能。它是一种布局,捕捉实际与预测结果,使用两维中相同的标签集:

混淆矩阵

要快速了解无监督和监督统计学习系统中的混淆矩阵,请参见en.wikipedia.org/wiki/Confusion_matrix

另请参见

原始数据集信息的文档可在以下网站获得:

多类分类度量文档可在此处获得:

使用 Spark 2.0 进行多标签分类模型评估

在本节中,我们探讨了 Spark 2.0 中的多标签分类MultilabelMetrics,这不应与前一节中涉及多类分类MulticlassMetrics的内容混淆。探索此节的关键是专注于评估指标,如汉明损失、准确性、F1 度量等,以及它们所衡量的内容。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter4
  1. 为 SparkContext 导入必要的包以访问集群:
import org.apache.spark.sql.SparkSession
import org.apache.spark.mllib.evaluation.MultilabelMetrics
import org.apache.spark.rdd.RDD
  1. 创建 Spark 的配置和 SparkContext:
val spark = SparkSession
.builder
.master("local[*]")
.appName("myMultilabel")
.config("spark.sql.warehouse.dir", ".")
.getOrCreate()
  1. 我们为评估模型创建数据集:
val data: RDD[(Array[Double], Array[Double])] = spark.sparkContext.parallelize(
Seq((Array(0.0, 1.0), Array(0.1, 2.0)),
     (Array(0.0, 2.0), Array(0.1, 1.0)),
     (Array.empty[Double], Array(0.0)),
     (Array(2.0), Array(2.0)),
     (Array(2.0, 0.0), Array(2.0, 0.0)),
     (Array(0.0, 1.0, 2.0), Array(0.0, 1.0)),
     (Array(1.0), Array(1.0, 2.0))), 2)
  1. 我们从预测结果创建MultilabelMetrics对象,并开始对指标进行评估:
val metrics = new MultilabelMetrics(data)
  1. 我们在控制台打印出总体统计摘要:
println(s"Recall = ${metrics.recall}")
println(s"Precision = ${metrics.precision}")
println(s"F1 measure = ${metrics.f1Measure}")
println(s"Accuracy = ${metrics.accuracy}")

从控制台输出中:

Recall = 0.5
Precision = 0.5238095238095238
F1 measure = 0.4952380952380952
Accuracy = 0.4523809523809524
  1. 我们在控制台打印出各个标签的值:
metrics.labels.foreach(label =>
 println(s"Class $label precision = ${metrics.precision(label)}"))
 metrics.labels.foreach(label => println(s"Class $label recall = ${metrics.recall(label)}"))
 metrics.labels.foreach(label => println(s"Class $label F1-score = ${metrics.f1Measure(label)}"))

从控制台输出中:

Class 0.0 precision = 0.5
Class 1.0 precision = 0.6666666666666666
Class 2.0 precision = 0.5
Class 0.0 recall = 0.6666666666666666
Class 1.0 recall = 0.6666666666666666
Class 2.0 recall = 0.5
Class 0.0 F1-score = 0.5714285714285715
Class 1.0 F1-score = 0.6666666666666666
Class 2.0 F1-score = 0.5
  1. 我们在控制台打印出微观统计值:
println(s"Micro recall = ${metrics.microRecall}")
println(s"Micro precision = ${metrics.microPrecision}")
println(s"Micro F1 measure = ${metrics.microF1Measure}")
From the console output:
Micro recall = 0.5
Micro precision = 0.5454545454545454
Micro F1 measure = 0.5217391304347826
  1. 我们在控制台从度量中打印出汉明损失和子集准确性:
println(s"Hamming loss = ${metrics.hammingLoss}")
println(s"Subset accuracy = ${metrics.subsetAccuracy}")
From the console output:
Hamming loss = 0.39285714285714285
Subset accuracy = 0.2857142857142857
  1. 然后我们通过停止 Spark 会话来关闭程序。
spark.stop()

工作原理...

在本节中,我们探讨了为多标签分类模型生成评估指标的过程。我们首先手动创建了一个用于模型评估的数据集。接着,我们将数据集作为参数传递给MultilabelMetrics,并生成了评估指标。最后,我们打印出了各种指标,如微观召回率、微观精确度、微观 F1 度量、汉明损失、子集准确性等。

还有更多...

注意,多标签分类和多类别分类听起来相似,但实际上是两种不同的概念。

所有多标签的MultilabelMetrics()方法试图实现的是将多个输入(x)映射到一个二进制向量(y),而不是典型分类系统中的数值。

与多标签分类相关的重要度量包括(参见前面的代码):

  • 准确性

  • 汉明损失

  • 精确度

  • 召回率

  • F1 分数

每个参数的完整解释超出了范围,但以下链接提供了对多标签度量的简要说明:

多标签分类

另请参见

多标签分类度量文档:

使用 Scala Breeze 库在 Spark 2.0 中进行图形绘制

在本节中,我们将使用 Scala Breeze 线性代数库(的一部分)中的scatter()plot()函数来绘制二维数据的散点图。一旦在 Spark 集群上计算出结果,要么可以在驱动程序中使用可操作数据进行绘图,要么可以在后端生成 JPEG 或 GIF 图像,并推送以提高效率和速度(这在基于 GPU 的分析数据库如 MapD 中很流行)

如何操作...

  1. 首先,我们需要下载必要的 ScalaNLP 库。从 Maven 仓库下载 JAR 文件,地址为repo1.maven.org/maven2/org/scalanlp/breeze-viz_2.11/0.12/breeze-viz_2.11-0.12.jar

  2. 将 JAR 文件放置在 Windows 机器上的C:\spark-2.0.0-bin-hadoop2.7\examples\jars目录中:

  3. 在 macOS 上,请将 JAR 文件放置在其正确的路径中。对于我们的设置示例,路径是/Users/USERNAME/spark/spark-2.0.0-bin-hadoop2.7/examples/jars/

  4. 以下是显示 JAR 文件的示例截图:

  5. 在 IntelliJ 或你选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  6. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter4
  1. 导入...

工作原理...

在本教程中,我们通过随机数在 Spark 中创建了一个数据集。随后,我们创建了一个 Breeze 图形并设置了基本参数。我们从创建的数据集中导出了x, y数据。

我们使用 Breeze 的scatter()plot()函数,通过 Breeze 库进行图形绘制。

还有更多...

在前一章中,我们展示了可以使用 Breeze 作为 JFreeChart 等更复杂、功能更强大的图表库的替代方案。ScalaNLP 项目倾向于利用 Scala 的特性,如隐式转换,使得编码相对容易。

Breeze 图形 JAR 文件可在central.maven.org/maven2/org/scalanlp/breeze-viz_2.11/0.12/breeze-viz_2.11-0.12.jar下载。

更多关于 Breeze 图形的信息可在github.com/scalanlp/breeze/wiki/Quickstart找到。

API 文档(请注意,API 文档可能不是最新的)可在www.scalanlp.org/api/breeze/#package找到。

注意,一旦进入根包,你需要点击 Breeze 以...

参见

关于 Breeze 的更多信息,请参阅 GitHub 上的原始资料github.com/scalanlp/breeze

注意,一旦进入根包,你需要点击 Breeze 以查看详细信息。

关于 Breeze API 文档的更多信息,请下载repo1.maven.org/maven2/org/scalanlp/breeze-viz_2.11/0.12/breeze-viz_2.11-0.12-javadoc.jar JAR。

第十三章:使用 Spark 实现可扩展的推荐引擎

在本章中,我们将涵盖:

  • 为 Spark 2.0 中的可扩展推荐引擎准备所需数据

  • 探索 Spark 2.0 中推荐系统的电影数据细节

  • 探索 Spark 2.0 中推荐系统的评分数据细节

  • 构建可扩展的推荐引擎:使用 Spark 2.0 中的协同过滤

引言

在前几章中,我们使用简短的方法和极其简化的代码来演示 Spark 机器学习库的基本构建块和概念。在本章中,我们展示了一个更成熟的应用,它使用 Spark 的 API 和功能来解决特定的机器学习库领域。本章中的方法较少;然而,我们进入了一个更 ML 应用设置。

在本章中,我们将探讨推荐系统及其使用称为交替最小二乘ALS)的潜在因子模型矩阵分解技术的实现。简而言之,当我们尝试将用户-物品评分的大矩阵分解为两个低秩、更瘦的矩阵时,我们通常会面临一个非常难以解决的非线性或非凸优化问题。我们非常擅长通过固定一个变量并部分解决另一个变量,然后来回切换(因此交替)来解决凸优化问题;我们可以使用已知的优化技术在并行中更好地解决这种分解(从而发现一组潜在因素)。

我们使用一个流行的数据集(电影数据集)来实现推荐引擎,但与其他章节不同,我们使用两个方法来探索数据,并展示如何引入图形元素,如流行的 JFreeChart 库到您的 Spark 机器学习工具包。

下图展示了本章中概念和方法的流程,以演示一个 ALS 推荐应用:

推荐引擎已经存在很长时间,并在 20 世纪 90 年代早期的电子商务系统中使用,从硬编码的产品关联到基于用户画像的内容推荐。现代系统使用协同过滤CF)来解决早期系统的不足,并解决在现代商业系统(如亚马逊、Netflix、eBay、新闻等)中竞争所需的规模和延迟(例如,最大 100 毫秒及以下)。

现代系统采用基于历史互动和记录(页面浏览、购买、评分等)的 CF。这些系统解决了两个主要问题,即规模化和稀疏性(即我们没有所有电影或歌曲的所有评分)。大多数系统采用基于交替最小二乘法与加权 Lambda 正则化的变体,这些可以在大多数主要平台上并行化(例如,Spark)。尽管如此,为商业目的实施的实用系统会采用多种增强措施来处理偏差(即并非所有电影和用户都是平等的)和时间问题(即用户的选择会变化,物品库存也会变化),这些问题在当今生态系统中普遍存在。在开发智能且领先的电子商务系统时,构建一个有竞争力的推荐器并非纯粹主义方法,而是一种实用方法,它采用多种技术,至少利用所有三种技术(协同过滤、基于内容的过滤和相似性)来构建亲和矩阵/热图。

鼓励读者查阅有关推荐系统冷启动问题的白皮书和资料。

为了设定背景,下图提供了一个构建推荐系统可用方法的高级分类。我们简要讨论了每种系统的优缺点,但重点是 Spark 中可用的矩阵分解(潜在因子模型)。

尽管单值分解SVD)和交替最小二乘法ALS)都可用,但由于 SVD 在处理缺失数据等方面的不足,我们专注于使用 MovieLens 数据的 ALS 实现。

以下部分将解释当前使用的推荐引擎技术。

内容过滤

内容过滤是推荐引擎的原始技术之一,它依赖用户档案来提供推荐。这种方法主要依赖于用户(类型、人口统计、收入、地理位置、邮政编码)和库存(产品、电影或歌曲的特性)的预先设定档案来推断属性,然后可以进行过滤和处理。主要问题在于,预先获取的知识往往不完整且成本高昂。这项技术已有十多年历史,至今仍在使用。

协同过滤

协同过滤是现代推荐系统的核心,它依赖于生态系统中的用户互动而非档案来提供推荐。

该技术依赖于用户过去的行为和产品评分,并不假设任何预先存在的知识。简而言之,用户对库存项目进行评分,并假设客户口味在一段时间内将保持相对稳定,这可以用来提供推荐。话虽如此,一个智能系统将根据任何可用上下文(例如,用户是来自中国的女性)来增强和重新排序推荐。

这类技术的主要问题是冷启动,但其不受领域限制、更高的准确性和易于扩展的优势,使其在大数据时代成为赢家。

邻域方法

这项技术主要以加权局部邻域的形式实现。其核心是一种相似性技术,严重依赖于对物品和用户的假设。尽管该技术易于理解和实施,但算法在可扩展性和准确性方面存在缺陷。

潜在因子模型技术

该技术试图通过推断一组次级潜在因素来解释用户对库存项目(例如,亚马逊上的产品)的评分,这些潜在因素是从评分中推断出来的。其优势在于,你无需事先了解这些因素(类似于 PCA 技术),而是直接从评分本身推断出来。我们采用矩阵分解技术来推导这些潜在因素,这些技术因其极高的可扩展性、预测准确性和灵活性(允许偏差和用户及库存的时间特性)而广受欢迎。

  • 奇异值分解SVD):SVD 自 Spark 早期就已可用,但我们建议不要将其作为核心技术,因为其在处理现实数据稀疏性(例如,用户通常不会对所有内容进行评分)、过拟合和排序(我们真的需要生成最底部的 1000 条推荐吗?)方面存在问题。

  • 随机梯度下降SGD):SGD 易于实现,且由于其逐个电影和逐个用户/物品向量的处理方式(选择一部电影并针对该用户微调其配置文件,而非批量处理),运行速度更快。我们可以根据需要使用 Spark 中的矩阵设施和 SGD 来实现这一点。

  • 交替最小二乘法ALS):在开始这段旅程之前,请先了解 ALS。Spark 中的 ALS 从一开始就能利用并行化。与普遍认为 Spark 使用半因子分解相反,Spark 实际上在内部实现了完整的矩阵分解。我们鼓励读者参考源代码自行验证。Spark 提供了针对显式(有评分)和隐式(需要间接推断,例如,播放曲目的时长而非评分)的 API。我们在食谱中通过引入数学和直觉来讨论偏差和时间问题,以阐明我们的观点。

为 Spark 2.0 中的可扩展推荐引擎设置所需数据

在本节中,我们探讨下载 MovieLens 公共数据集并初步探索数据。我们将使用基于 MovieLens 数据集中客户评级的显式数据。MovieLens 数据集包含来自 6,000 名用户的 4,000 部电影的 1,000,000 个评分。

你需要以下命令行工具之一来检索指定数据:curl(Mac 推荐)或wget(Windows 或 Linux 推荐)。

如何操作...

  1. 你可以通过以下任一命令开始下载数据集:
wget http://files.grouplens.org/datasets/movielens/ml-1m.zip

你也可以使用以下命令:

curl http://files.grouplens.org/datasets/movielens/ml-1m.zip -o ml-1m.zip
  1. 现在你需要解压 ZIP 文件:
unzip ml-1m.zip
creating: ml-1m/
inflating: ml-1m/movies.dat
inflating: ml-1m/ratings.dat
inflating: ml-1m/README
inflating: ml-1m/users.dat

该命令将创建一个名为ml-1m的目录,其中包含解压后的数据文件。

  1. 切换到m1-1m目录:
cd m1-1m
  1. 现在我们通过验证movies.dat中的数据格式开始数据探索的第一步:
head -5 movies.dat
1::Toy Story (1995)::Animation|Children's|Comedy
2::Jumanji (1995)::Adventure|Children's|Fantasy
3::Grumpier Old Men (1995)::Comedy|Romance
4::Waiting to Exhale (1995)::Comedy|Drama
5::Father of the Bride Part II (1995)::Comedy
  1. 现在我们来看看评分数据的格式:
head -5 ratings.dat
1::1193::5::978300760
1::661::3::978302109
1::914::3::978301968
1::3408::4::978300275
1::2355::5::978824291

工作原理...

MovieLens 数据集是原始 Netflix KDD 杯数据集的绝佳替代品。此数据集有多个版本,从小型(100 K 数据集)到大型(1 M 和 20 M 数据集)。对于那些希望调整源代码以添加自己的增强功能(例如,更改正则化技术)的用户,数据集的范围使其易于研究缩放效果并查看执行者每秒 Spark 利用率与数据从 100 K 到 20 M 的性能曲线。

下载 URL 为grouplens.org/datasets/movielens/

还有更多...

仔细查看我们下载数据的位置,因为更多数据集可在files.grouplens.org/datasets/上使用。

下图展示了数据的规模和范围。本章我们使用小数据集,以便在资源有限的小型笔记本电脑上轻松运行。

来源:MovieLens

另请参阅

请阅读解压数据后所在目录中的 README 文件。README 文件包含有关数据文件格式和数据描述的信息。

还有一个 MovieLens 基因组标签集可供参考。

  • 计算的标签-电影 1100 万

  • 从 1,100 个标签池中得出的相关性评分

  • 应用于 10,000 部电影

对于那些有兴趣探索原始 Netflix 数据集的人,请参阅academictorrents.com/details/9b13183dc4d60676b773c9e2cd6de5e5542cee9a的 URL。

在 Spark 2.0 中为推荐系统探索电影数据详情

在本教程中,我们将开始通过将数据解析到 Scala case类中并生成一个简单指标来探索电影数据文件。关键在于获取对数据的了解,以便在后续阶段,如果出现模糊的结果,我们将有一些见解来做出关于我们结果正确性的明智结论。

这是探索电影数据集的两个教程中的第一个。数据探索是统计分析和机器学习的重要第一步。

快速理解数据的最佳方法之一是生成其数据可视化,我们将使用 JFreeChart 来实现这一点。确保您对数据感到舒适并直接了解每个文件中的内容以及它试图讲述的故事非常重要。

在我们做任何其他事情之前,我们必须始终探索、理解和可视化数据。大多数 ML 和其他系统的性能和失误都可以追溯到对数据布局及其随时间变化的社会缺乏了解。如果我们在本教程的第 14 步中查看给出的图表,我们立即意识到电影在年份上的分布不均匀,而是具有高偏度的。虽然我们不会在这本书中探讨这个属性以进行优化和采样,但它强调了电影数据性质的重要观点。

如何实现它...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. JFreeChart JAR 可以从sourceforge.net/projects/jfreechart/files/网站下载。

  3. 请确保 JFreeChart 库及其依赖项(JCommon)位于本章的类路径上。

  4. 我们为 Scala 程序定义包信息:

package spark.ml.cookbook.chapter7
  1. 导入必要的包:
import java.text.DecimalFormat import org.apache.log4j.{Level, Logger} import org.apache.spark.sql.SparkSession import org.jfree.chart.{ChartFactory, ChartFrame, JFreeChart} import org.jfree.chart.axis.NumberAxis import org.jfree.chart.plot.PlotOrientation import org.jfree.data.xy.{XYSeries, ...

它是如何工作的...

当程序开始执行时,我们在驱动程序中初始化了一个 SparkContext,以启动处理数据任务。这意味着数据必须适合驱动程序的内存(用户的工作站),在这种情况下这不是服务器的要求。必须设计其他分治方法来处理极端数据集(部分检索和目的地组装)。

我们继续通过将数据文件加载并解析到具有电影数据类型的数据集中。然后,电影数据集按年份分组,产生一个按年份键入的电影地图,并附有相关电影的存储桶。

接下来,我们提取特定年份及其相关电影数量的计数,以生成我们的直方图。然后,我们收集数据,导致整个结果数据集合在驱动程序上具体化,并将其传递给 JFreeChart 以构建数据可视化。

还有更多...

由于 Spark SQL 的灵活性,你需要了解我们对它的使用。更多信息可访问spark.apache.org/docs/latest/sql-programming-guide.html#running-sql-queries-programmatically

另请参阅

更多关于使用 JFreeChart 的信息,请参考 JFreeChart API 文档www.jfree.org/jfreechart/api.html

你可以在www.tutorialspoint.com/jfreechart/链接找到关于 JFreeChart 的优质教程。

JFreeChart 本身的链接是www.jfree.org/index.html

探索 Spark 2.0 中推荐系统的评分数据细节

在本食谱中,我们从用户/评分的角度探索数据,以了解我们数据文件的性质和属性。我们将开始通过将数据解析为 Scala case class 并生成可视化来探索评分数据文件以获取洞察。评分数据稍后将用于为我们的推荐引擎生成特征。我们再次强调,任何数据科学/机器学习实践的第一步都应该是数据的可视化和探索。

再次强调,快速理解数据的最佳方式是生成其数据可视化,我们将使用 JFreeChart 散点图来实现这一点。快速查看图表...

如何做到这一点...

  1. 在 IntelliJ 或你选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 我们为 Scala 程序定义包信息:

package spark.ml.cookbook.chapter7
  1. 导入必要的包:
import java.text.DecimalFormat
 import org.apache.log4j.{Level, Logger}
 import org.apache.spark.sql.SparkSession
 import org.jfree.chart.{ChartFactory, ChartFrame, JFreeChart}
 import org.jfree.chart.axis.NumberAxis
 import org.jfree.chart.plot.PlotOrientation
 import org.jfree.data.xy.{XYSeries, XYSeriesCollection}
  1. 我们现在定义一个 Scala case class来模拟评分数据:
case class Rating(userId: Int, movieId: Int, rating: Float, timestamp: Long)
  1. 让我们定义一个在窗口中显示 JFreeChart 的函数:
def show(chart: JFreeChart) {
 val frame = new ChartFrame("plot", chart)
 frame.pack()
 frame.setVisible(true)
 }
  1. 在这一步中,我们定义了一个函数,用于将ratings.dat文件中的一行数据解析为评分case class
def parseRating(str: String): Rating = {
 val columns = str.split("::")
 assert(columns.size == 4)
 Rating(columns(0).toInt, columns(1).toInt, columns(2).toFloat, columns(3).toLong)
 }
  1. 我们准备好开始构建我们的main函数,所以让我们从ratings.dat文件的位置开始:
val ratingsFile = "../data/sparkml2/chapter7/ratings.dat"
  1. 创建 Spark 的配置,SparkSession。在本例中,我们首次展示了如何在小笔记本上设置 Spark executor 内存(例如,2GB)。如果你想使用大型数据集(144MB 的数据集),你必须增加这个分配:
val spark = SparkSession
 .*builder* .master("local[*]")
 .appName("MovieRating App")
 .config("spark.sql.warehouse.dir", ".")
 .config("spark.executor.memory", "2g")
 .getOrCreate()
  1. 日志消息的交错导致输出难以阅读;因此,将日志级别设置为ERROR
Logger.getLogger("org").setLevel(Level.ERROR)
  1. 从数据文件创建所有评分的数据集:
import spark.implicits._
 val ratings = spark.read.textFile(ratingsFile).map(*parseRating*)
  1. 现在我们将评分数据集转换为内存表视图,我们可以在其中执行 Spark SQL 查询:
ratings.createOrReplaceTempView("ratings")
  1. 我们现在生成一个按用户分组的所有用户评分的列表,以及他们的总数:
val resultDF = spark.sql("select ratings.userId, count(*) as count from ratings group by ratings.userId")
resultDF.show(25, false);

从控制台输出:

  1. 展示每个用户的评分散点图。我们选择散点图以展示与前一节不同的数据查看方式。鼓励读者探索标准化技术(例如移除均值)或波动性变化机制(例如 GARCH),以探索此数据集的自回归条件异方差特性(这超出了本书的范围)。建议读者查阅任何高级时间序列书籍,以理解时间序列的时间变化。
val scatterPlotDataset = new XYSeriesCollection()
 val xy = new XYSeries("")

 resultDF.collect().foreach({r => xy.add( r.getAsInteger, r.getAsInteger) })

 scatterPlotDataset.addSeries(xy)

 val chart = ChartFactory.*createScatterPlot*(
 "", "User", "Ratings Per User", scatterPlotDataset, PlotOrientation.*VERTICAL*, false, false, false)
 val chartPlot = chart.getXYPlot()

 val xAxis = chartPlot.getDomainAxis().asInstanceOf[NumberAxis]
 xAxis.setNumberFormatOverride(new DecimalFormat("####"))
  1. 显示图表:
*show*(chart)

  1. 我们通过停止 Spark 会话来关闭程序:
spark.stop()

其工作原理...

我们首先将数据文件加载并解析为具有评分数据类型的数据集,最终将其转换为 DataFrame。然后,使用 DataFrame 执行 Spark SQL 查询,该查询按用户及其总数对所有评分进行分组。

全面理解 API 及其概念(延迟实例化、阶段划分、流水线和缓存)对每位 Spark 开发者至关重要。

最后,我们将数据集的结果传递给 JFreeChart 散点图组件以显示我们的图表。

还有更多...

Spark DataFrame 是一个分布式数据集合,按命名列组织。所有 DataFrame 操作也会自动在集群上并行化和分布。此外,DataFrames 像 RDD 一样是惰性评估的。

参见

DataFrame 的文档可在spark.apache.org/docs/latest/sql-programming-guide.html找到。

关于 JFreeChart 的优秀教程可在www.tutorialspoint.com/jfreechart/链接中找到。

JFreeChart 可从www.jfree.org/index.html网址下载。

使用 Spark 2.0 中的协同过滤构建可扩展的推荐引擎

本节中,我们将演示一个利用协同过滤技术的推荐系统。协同过滤的核心在于分析用户之间的关系以及库存(例如电影、书籍、新闻文章或歌曲)之间的依赖性,基于一组称为潜在因素的次要因素(例如女性/男性、快乐/悲伤、活跃/被动)来识别用户与物品之间的关系。关键在于,您无需预先了解这些潜在因素。

推荐将通过 ALS 算法生成,这是一种协同过滤技术。从高层次上看,协同过滤涉及基于收集的先前已知偏好以及许多其他用户的偏好,对用户可能感兴趣的内容进行预测。我们将使用 MovieLens 数据集中的评分数据,并将其转换为推荐算法的输入特征。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 我们为 Scala 程序定义包信息:

package spark.ml.cookbook.chapter7
  1. 导入必要的包:
import org.apache.log4j.{Level, Logger} import org.apache.spark.sql.SparkSession import org.apache.spark.ml.recommendation.ALS
  1. 我们现在定义两个 Scala case 类,以模拟电影和评分数据:
case class Movie(movieId: Int, title: String, year: Int, genre: Seq[String]) case class FullRating(userId: Int, movieId: Int, rating: Float, timestamp: Long)
  1. 在此步骤中,我们定义函数,用于将ratings.dat文件中的一行数据解析为评分case class,以及用于解析...

它是如何工作的...

由于程序的复杂性,我们首先提供概念性解释,然后逐步详细说明程序内容。

下图描绘了 ALS 的概念视图及其如何将用户/电影/评分矩阵分解为低阶的瘦长矩阵和潜在因子向量:f(用户)和 f(电影)。

另一种思考方式是,这些因子可用于将电影置于n维空间中,该空间将与给定用户的推荐相匹配。始终希望将机器学习视为在维度变量空间中的搜索查询。需要记住的是,潜在因子(学习的几何空间)并非预先定义,其数量可以从 10 到 100 或 1000 不等,具体取决于所搜索或分解的内容。因此,我们的推荐可以看作是在 n 维空间内放置概率质量。下图提供了一个可能的双因子模型(二维)的极其简化的视图,以阐明这一点:

尽管 ALS 的实现可能因系统而异,但其核心是一种迭代的全因子分解方法(在 Spark 中),带有加权正则化。Spark 的文档和教程提供了对该算法实际数学和性质的洞察。它将算法描述如下:

理解这个公式/算法的最佳方式是将其视为一个迭代装置,试图通过交替输入(即,固定一个输入,然后近似/优化另一个——如此往复)来发现潜在因子,同时试图最小化与加权 lambda 正则化惩罚相关的最小二乘误差(MSE)。下一节将提供更详细的解释。

程序流程如下:

  • 示例首先从 MovieLens 数据集中加载评分和电影数据。加载的数据随后被转换为 Scala case 类以便进一步处理。接下来,将评分数据划分为训练集和测试集。训练集数据用于训练机器学习算法。训练是机器学习中用于构建模型以便提供所需结果的过程。测试数据将用于最终步骤中验证结果。

  • 虚构用户,即用户 ID 零,通过配置一个未包含在原始数据集中的单一用户,帮助通过即时创建包含随机信息的数据显示结果,并最终将其附加到训练集中。通过将包含用户 ID、电影 ID 和评分的训练集数据传递给 ALS 算法来调用它,随后从 Spark 中产生一个矩阵分解模型。为测试数据集和用户 ID 零生成预测。

  • 最终结果通过结合评分信息与电影数据展示,以便结果能被理解并在原始评分旁显示估计评分。最后一步是计算生成评分的均方根误差,该评分包含在测试数据集中。RMSE 将告诉我们训练模型的准确性。

还有更多...

尽管 ALS 本质上是一个带有正则化惩罚的简单线性代数运算,但人们常常难以掌握。ALS 的强大之处在于其能够并行化处理以及应对规模(例如 Spotify)。

ALS 用通俗语言来说涉及以下内容:

  • 使用 ALS,你基本上想要将一个大型评分矩阵 X(拥有超过 1 亿用户并非夸张)和用户产品评分分解为两个低秩矩阵 A 和 B(参见任何入门线性代数书籍)。问题在于,这通常成为一个非常难以解决的非线性优化问题。为了解决这个问题,ALS 引入了一个简单方案(交替),其中你固定一个矩阵并部分...

另请参见

Spark 2.0 ML 文档以探索 ALS API:

Spark 2.0 MLlib 文档可于spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.mllib.recommendation.ALS获取。

ALS 参数及其默认构造,以默认参数创建 ALS 实例如下:

{numBlocks: -1, rank: 10, iterations: 10, lambda: 0.
numBlocks: -1,
rank: 10,
iterations: 10,
lambda: 0.01,
implicitPrefs: false,
alpha: 1.0

处理训练中的隐式输入

有时实际观察(评分)数据不可得,此时需处理隐含反馈参数。这可能简单到记录用户在互动期间听取了哪个音轨,或是观看了多久的电影,亦或是上下文(预先索引)以及导致切换的原因(如 Netflix 电影在开头、中间或特定场景附近被放弃观看)。第三个示例中通过使用ALS.train()处理了显式反馈。

针对隐式数据,Spark ML 库提供了另一种方法ALS.trainImplicit(),该方法有四个超参数来控制算法。若你对测试此方法感兴趣(它与显式反馈非常相似...

第十四章:使用 Apache Spark 2.0 进行无监督聚类

在本章中,我们将涵盖:

  • 在 Spark 2.0 中构建 KMeans 分类系统

  • 在 Spark 2.0 中,二分 KMeans 作为新星登场

  • 在 Spark 2.0 中使用高斯混合模型和期望最大化(EM)算法进行数据分类

  • 在 Spark 2.0 中使用幂迭代聚类(PIC)对图的顶点进行分类

  • 使用潜在狄利克雷分配(LDA)将文档和文本分类为主题

  • 使用流式 KMeans 在接近实时的情况下对数据进行分类

引言

无监督机器学习是一种学习技术,我们试图直接或间接(通过潜在因素)从一组未标记的观察中得出推断。简而言之,我们试图在未对训练数据进行初始标记的情况下,从一组数据中发现隐藏的知识或结构。

尽管大多数机器学习库的实现在大数据集上应用时会崩溃(迭代、多次遍历、大量中间写入),但 Apache Spark 机器学习库通过提供为并行性和极大数据集设计的算法,并默认使用内存进行中间写入,从而取得了成功。

在最抽象的层面上,我们可以将无监督学习视为:

在 Spark 2.0 中构建 KMeans 分类系统

在本教程中,我们将使用 LIBSVM 文件加载一组特征(例如,x,y,z 坐标),然后使用KMeans()实例化一个对象。接着,我们将设置期望的簇数为三个,并使用kmeans.fit()执行算法。最后,我们将打印出我们找到的三个簇的中心。

值得注意的是,Spark并未实现 KMeans++,这与流行文献相反,而是实现了 KMeans ||(发音为 KMeans Parallel)。请参阅以下教程以及代码之后的部分,以获得对 Spark 中实现的算法的完整解释。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter8
  1. 为了获取集群访问权限并使用Log4j.Logger减少 Spark 产生的输出量,需要导入必要的 Spark 上下文包:
import org.apache.log4j.{Level, Logger}import org.apache.spark.ml.clustering.KMeansimport org.apache.spark.sql.SparkSession
  1. 将输出级别设置为ERROR以减少 Spark 的日志输出:
Logger.getLogger("org").setLevel(Level.ERROR)
  1. 创建 Spark 的 Session 对象:
val spark = SparkSession .builder.master("local[*]") .appName("myKMeansCluster") .config("spark.sql.warehouse.dir" ...

工作原理...

我们读取了一个 LIBSVM 文件,其中包含一组坐标(可以解释为三个数字的元组),然后创建了一个 KMean() 对象,但将默认簇数从 2(开箱即用)更改为 3,以便演示。我们使用 .fit() 创建模型,然后使用 model.summary.predictions.show() 显示哪个元组属于哪个簇。在最后一步中,我们打印了成本和三个簇的中心。从概念上讲,可以将其视为拥有一组 3D 坐标数据,然后使用 KMeans 算法将每个单独的坐标分配给三个簇之一。

KMeans 是一种无监督机器学习算法,其根源在于信号处理(矢量量化)和压缩(将相似的物品矢量分组以实现更高的压缩率)。一般来说,KMeans 算法试图将一系列观察值 {X[1,] X[2], .... , X[n]} 分组到一系列簇 {C[1,] C[2 .....] C[n]} 中,使用一种距离度量(局部优化),该度量以迭代方式进行优化。

目前使用的 KMeans 算法主要有三种类型。在一项简单的调查中,我们发现了 12 种 KMeans 算法的专门变体。值得注意的是,Spark 实现了一个名为 KMeans ||(KMeans 并行)的版本,而不是文献或视频中提到的 KMeans++ 或标准 KMeans。

下图简要描绘了 KMeans 算法:

来源:Spark 文档

KMeans(Lloyd 算法)

基本 KMeans 实现(Lloyd 算法)的步骤如下:

  1. 从观察结果中随机选择 K 个数据中心作为初始中心。

  2. 持续迭代直至满足收敛条件:

    • 测量一个点到每个中心的距离

    • 将每个数据点包含在与其最接近的中心对应的簇中

    • 根据距离公式(作为不相似性的代理)计算新的簇中心

    • 使用新的中心点更新算法

下图描绘了三代算法:

KMeans++(亚瑟算法)

对标准 KMeans 的下一个改进是 David Arthur 和 Sergei Vassilvitskii 于 2007 年提出的 KMeans++。亚瑟算法通过在种子过程(初始步骤)中更加挑剔来改进初始的 Lloyd 的 KMeans。

KMeans++并非随机选择初始中心(随机质心),而是随机选取第一个质心,然后逐个选取数据点并计算D(x)。接着,它随机选择另一个数据点,并使用比例概率分布D(x)2,重复最后两个步骤,直到选出所有K个数。初始播种完成后,我们最终运行 KMeans 或其变体,使用新播种的质心。KMeans++算法保证在Omega= O(log k)复杂度内找到解决方案。尽管初始播种步骤较多,但准确性提升显著。

KMeans||(发音为 KMeans Parallel)

KMeans || 经过优化,可并行运行,相较于 Lloyd 的原始算法,性能提升可达一到两个数量级。KMeans++的局限性在于它需要对数据集进行 K 次遍历,这在大规模或极端数据集上运行 KMeans 时会严重限制其性能和实用性。Spark 的 KMeans||并行实现运行更快,因为它通过采样 m 个点并在过程中进行过采样,减少了数据遍历次数(大幅减少)。

算法的核心及数学原理在下图中展示:

简而言之,KMeans ||(并行...)的亮点在于...

还有更多...

在 Spark 中还有一个流式 KMeans 实现,允许您实时对特征进行分类。

还有一个类帮助您生成 KMeans 的 RDD 数据。我们在应用程序开发过程中发现这非常有用:

def generateKMeansRDD(sc: SparkContext, numPoints: Int, k: Int, d: Int, r: Double, numPartitions: Int = 2): RDD[Array[Double]] 

此调用使用 Spark 上下文创建 RDD,同时允许您指定点数、簇数、维度和分区数。

一个相关的实用 API 是:generateKMeansRDD()。关于generateKMeansRDD的文档可以在spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.mllib.util.KMeansDataGenerator$找到,用于生成供 KMeans 使用的测试数据 RDD。

另请参阅

我们需要两个对象来编写、测量和操作 Spark 中 KMeans ||算法的参数。这两个对象的详细信息可以在以下网站找到:

Bisecting KMeans,Spark 2.0 中的新秀

在本节中,我们将下载玻璃数据集,并尝试使用 Bisecting KMeans 算法来识别和标记每种玻璃。Bisecting KMeans 是 K-Mean 算法的层次化版本,在 Spark 中通过BisectingKMeans()API 实现。虽然该算法在概念上类似于 KMeans,但在存在层次路径的情况下,它可以为某些用例提供显著的速度优势。

本节中使用的数据集是玻璃识别数据库。对玻璃类型分类的研究源于犯罪学研究。如果玻璃能被正确识别,它可能被视为证据。数据可在台湾大学(NTU)找到,已采用 LIBSVM 格式。

如何操作...

  1. 我们从以下链接下载了 LIBSVM 格式的预处理数据文件:www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/multiclass/glass.scale

该数据集包含 11 个特征和 214 行数据。

  1. 原始数据集及数据字典亦可在 UCI 网站上获取:archive.ics.uci.edu/ml/datasets/Glass+Identification

    • ID 号:1 至 214

    • RI: 折射率

    • Na: 钠(单位测量:相应氧化物中的重量百分比,属性 4-10 也是如此)

    • Mg: 镁

    • Al: 铝

    • Si: 硅

    • K: 钾

    • Ca: 钙

    • Ba: 钡

    • Fe: 铁

玻璃类型:我们将使用BisectingKMeans()来寻找我们的类别属性或簇:

  • building_windows_float_processed

  • building_windows_non-_float_processed

  • vehicle_windows_float_processed

工作原理...

在本节中,我们探讨了 Spark 2.0 中新引入的 Bisecting KMeans 模型。我们利用了玻璃数据集,并尝试使用BisectingKMeans()来指定玻璃类型,但将 k 值调整为 6,以便拥有足够的簇。按照惯例,我们使用 Spark 的 libsvm 加载机制将数据加载到数据集中。我们将数据集随机分为 80%和 20%,其中 80%用于训练模型,20%用于测试模型。

我们创建了BiSectingKmeans()对象,并使用fit(x)函数来构建模型。随后,我们使用transform(x)函数对测试数据集进行模型预测,并在控制台输出结果。我们还输出了计算簇的成本(误差平方和),并展示了簇中心。最后,我们打印了特征及其分配的簇编号,并停止操作。

层次聚类的方法包括:

  • 分割型:自上而下的方法(Apache Spark 实现)

  • 聚合型:自下而上的方法

还有更多...

关于 Bisecting KMeans 的更多信息,请访问:

我们使用聚类来探索数据,并对聚类结果的外观有所了解。二分 K 均值是层次分析与 K 均值聚类的一个有趣案例。

最佳的理解方式是将二分 K 均值视为递归层次的 K 均值。二分 K 均值算法通过类似 K 均值的相似度测量技术分割数据,但采用层次结构以提高准确性。它在...中尤为普遍...

参见

实现层次聚类有两种方法——Spark 采用递归自顶向下的方法,在其中选择一个簇,然后在算法向下移动层次时执行分割:

在 Spark 中使用高斯混合和期望最大化(EM)进行数据分类

在本食谱中,我们将探讨 Spark 对期望最大化EM)的实现GaussianMixture(),它计算给定一组特征输入的最大似然。它假设每个点可以从 K 个子分布(簇成员)中采样的高斯混合。

操作方法...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter8.
  1. 导入用于向量和矩阵操作的必要包:
 import org.apache.log4j.{Level, Logger}
 import org.apache.spark.mllib.clustering.GaussianMixture
 import org.apache.spark.mllib.linalg.Vectors
 import org.apache.spark.sql.SparkSession
  1. 创建 Spark 的会话对象:
val spark = SparkSession
 .builder.master("local[*]")
 .appName("myGaussianMixture")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()
  1. 让我们查看数据集并检查输入文件。模拟的 SOCR 膝痛质心位置数据代表了 1000 名受试者假设的膝痛位置的质心位置。数据包括质心的 X 和 Y 坐标。

此数据集可用于说明高斯混合和期望最大化。数据可在wiki.stat.ucla.edu/socr/index.php/SOCR_Data_KneePainData_041409获取

样本数据如下所示:

  • X:一个受试者和一个视图的质心位置的x坐标。

  • Y:一个受试者和一个视图的质心位置的y坐标。

X, Y

11 73

20 88

19 73

15 65

21 57

26 101

24 117

35 106

37 96

35 147

41 151

42 137

43 127

41 206

47 213

49 238

40 229

下图基于wiki.stat.ucla的 SOCR 数据集描绘了一个膝痛地图:

  1. 我们将数据文件放置在一个数据目录中(您可以将数据文件复制到您喜欢的任何位置)。

数据文件包含 8,666 条记录:

val dataFile ="../data/sparkml2/chapter8/socr_data.txt"
  1. 接着,我们将数据文件加载到 RDD 中:
val trainingData = spark.sparkContext.textFile(dataFile).map { line =>
 Vectors.dense(line.trim.split(' ').map(_.toDouble))
 }.cache()
  1. 现在,我们创建一个高斯混合模型并设置模型参数。我们将 K 值设为 4,因为数据是通过四个视角收集的:左前LF)、左后LB)、右前RF)和右后RB)。我们将收敛值设为默认值 0.01,最大迭代次数设为 100:
val myGM = new GaussianMixture()
 .setK(4 ) // default value is 2, LF, LB, RF, RB
 .setConvergenceTol(0.01) // using the default value
 .setMaxIterations(100) // max 100 iteration
  1. 我们运行模型算法:
val model = myGM.run(trainingData)
  1. 训练后,我们打印出高斯混合模型的关键值:
println("Model ConvergenceTol: "+ myGM.getConvergenceTol)
 println("Model k:"+myGM.getK)
 println("maxIteration:"+myGM.getMaxIterations)

 for (i <- 0 until model.k) {
 println("weight=%f\nmu=%s\nsigma=\n%s\n" format
 (model.weights(i), model.gaussians(i).mu, model.gaussians(i).sigma))
 }
  1. 由于我们将 K 值设为 4,因此控制台记录器将打印出四组值:

  1. 我们还根据高斯混合模型预测打印出前 50 个聚类标签:
println("Cluster labels (first <= 50):")
 val clusterLabels = model.predict(trainingData)
 clusterLabels.take(50).foreach { x =>
 *print*(" " + x)
 }
  1. 控制台中的样本输出将显示以下内容:
Cluster labels (first <= 50):
 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
  1. 然后通过停止 Spark 上下文来关闭程序:
spark.stop()

工作原理...

在前一个示例中,我们观察到 KMeans 能够发现并基于迭代方法(如欧几里得距离等)将成员分配到一个且仅一个集群。可以将 KMeans 视为高斯混合模型中 EM 模型的专用版本,其中强制执行离散(硬)成员资格。

但存在重叠情况,这在医学或信号处理中很常见,如下图所示:

在这种情况下,我们需要一个能够表达每个子分布中成员资格的概率密度函数。采用期望最大化算法的高斯混合模型

新建 GaussianMixture()

这构建了一个默认实例。控制模型行为的默认参数如下:

采用期望最大化算法的高斯混合模型是一种软聚类形式,其中可以通过对数最大似然函数推断出成员资格。在此情况下,使用具有均值和协方差的概率密度函数来定义属于 K 个集群的成员资格或似然性。其灵活性在于,成员资格未量化,这允许基于概率(索引到多个子分布)的成员资格重叠。

下图是 EM 算法的一个快照:

以下是 EM 算法的步骤:

  1. 假设有N个高斯分布。

  2. 迭代直至达到收敛:

    1. 对于每个点 Z,其条件概率为从分布 Xi 中抽取,记作P(Z | Xi)

    2. 调整参数的均值和方差,使其适合分配给子分布的点

有关更数学化的解释,包括关于最大似然的详细工作,请参阅以下链接:www.ee.iisc.ernet.in/new/people/faculty/prasantg/downloads/GMM_Tutorial_Reynolds.pdf

还有更多...

下图提供了一个快速参考点,以突出硬聚类与软聚类之间的一些差异:

另请参阅

在 Spark 2.0 中使用幂迭代聚类(PIC)对图的顶点进行分类

这是一种基于顶点相似性(由边定义)对图的顶点进行分类的方法。它使用随 Spark 一起提供的 GraphX 库来实现算法。幂迭代聚类类似于其他特征向量/特征值分解算法,但没有矩阵分解的开销。当您有一个大型稀疏矩阵(例如,以稀疏矩阵表示的图)时,它很适用。

未来,GraphFrames 将成为 GraphX 库的替代/接口(databricks.com/blog/2016/03/03/introducing-graphframes.html)。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter8
  1. 为 Spark 上下文导入必要的包以访问集群,并导入Log4j.Logger以减少 Spark 产生的输出量:
 import org.apache.log4j.{Level, Logger}
 import org.apache.spark.mllib.clustering.PowerIterationClustering
 import org.apache.spark.sql.SparkSession
  1. 将日志级别设置为 ERROR,仅以减少输出:
Logger.getLogger("org").setLevel(Level.*ERROR*)
  1. 创建 Spark 配置和 SQL 上下文,以便我们可以访问集群并能够根据需要创建和使用 DataFrame:
// setup SparkSession to use for interactions with Sparkval spark = SparkSession
 .builder.master("local[*]")
 .appName("myPowerIterationClustering")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()
  1. 我们使用 Spark 的sparkContext.parallelize()函数创建包含一系列数据集的训练数据集,并创建 Spark RDD:
val trainingData =spark.sparkContext.parallelize(*List*(
 (0L, 1L, 1.0),
 (0L, 2L, 1.0),
 (0L, 3L, 1.0),
 (1L, 2L, 1.0),
 (1L, 3L, 1.0),
 (2L, 3L, 1.0),
 (3L, 4L, 0.1),
 (4L, 5L, 1.0),
 (4L, 15L, 1.0),
 (5L, 6L, 1.0),
 (6L, 7L, 1.0),
 (7L, 8L, 1.0),
 (8L, 9L, 1.0),
 (9L, 10L, 1.0),
 (10L,11L, 1.0),
 (11L, 12L, 1.0),
 (12L, 13L, 1.0),
 (13L,14L, 1.0),
 (14L,15L, 1.0)
 ))
  1. 我们创建一个PowerIterationClustering对象并设置参数。我们将K值设置为3,最大迭代次数设置为15
val pic = new PowerIterationClustering()
 .setK(3)
 .setMaxIterations(15)
  1. 然后让模型运行:
val model = pic.run(trainingData)
  1. 我们根据模型打印出训练数据的集群分配情况:
model.assignments.foreach { a =>
 println(s"${a.id} -> ${a.cluster}")
 }
  1. 控制台输出将显示以下信息:

  1. 我们还为每个聚类在集合中打印出模型分配数据:
val clusters = model.assignments.collect().groupBy(_.cluster).mapValues(_.map(_.id))
 val assignments = clusters.toList.sortBy { case (k, v) => v.length }
 val assignmentsStr = assignments
 .map { case (k, v) =>
 s"$k -> ${v.sorted.mkString("[", ",", "]")}" }.mkString(", ")
 val sizesStr = assignments.map {
 _._2.length
 }.sorted.mkString("(", ",", ")")
 println(s"Cluster assignments: $assignmentsStr\ncluster sizes: $sizesStr")
  1. 控制台输出将显示以下信息(总共,我们在前面的参数中设置了三个聚类):
Cluster assignments: 1 -> [12,14], 2 -> [4,6,8,10], 0 -> [0,1,2,3,5,7,9,11,13,15]
 cluster sizes: (2,4,10)
  1. 然后我们通过停止 Spark 上下文来关闭程序:
spark.stop()

其工作原理...

我们创建了一个图的边和顶点列表,然后继续创建对象并设置参数:

new PowerIterationClustering().setK(3).setMaxIterations(15)

下一步是训练数据模型:

val model = pic.run(trainingData)

然后输出聚类以供检查。代码末尾附近的代码使用 Spark 转换运算符在集合中为每个聚类打印出模型分配数据。

PIC幂迭代聚类)的核心是一种避免矩阵分解的特征值类算法,它通过生成一个特征值加上一个特征向量来满足Av = λv。由于 PIC 避免了矩阵 A 的分解,因此它适用于输入矩阵 A(描述图...

还有更多...

如需对主题(幂迭代)进行更详细的数学处理,请参阅卡内基梅隆大学提供的以下白皮书:www.cs.cmu.edu/~wcohen/postscript/icml2010-pic-final.pdf

另请参阅

使用潜在狄利克雷分配(LDA)对文档和文本进行主题分类

在本食谱中,我们将探讨 Spark 2.0 中的潜在狄利克雷分配LDA)算法。本食谱中使用的 LDA 与线性判别分析完全不同。潜在狄利克雷分配和线性判别分析都称为 LDA,但它们是截然不同的技术。在本食谱中,当我们使用 LDA 时,我们指的是潜在狄利克雷分配。关于文本分析的章节也与理解 LDA 相关。

LDA 常用于自然语言处理,试图将大量文档(例如安然欺诈案中的电子邮件)分类为有限数量的主题或主题,以便于理解。LDA 也是根据个人兴趣选择文章的良好候选方法(例如,当你翻页并花时间在特定主题上时),在给定的杂志文章或页面上。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter8
  1. 导入必要的包:
import org.apache.log4j.{Level, Logger}import org.apache.spark.sql.SparkSessionimport org.apache.spark.ml.clustering.LDA
  1. 我们设置必要的 Spark 会话以访问集群:
val spark = SparkSession .builder.master("local[*]") .appName("MyLDA") .config("spark.sql.warehouse.dir", ".") .getOrCreate()
  1. 我们有一个 LDA 样本数据集,位于以下相对路径(您也可以使用绝对路径)。该样本文件随任何 Spark 发行版提供,并且...

工作原理...

LDA 假设文档是具有 Dirichlet 先验分布的不同主题的混合体。文档中的单词被认为对特定主题有亲和力,这使得 LDA 能够对整体文档(构成并分配分布)进行分类,以最佳匹配主题。

主题模型是一种生成潜在模型,用于发现文档主体中出现的抽象主题(主题)(通常对于人类来说太大而无法处理)。这些模型是总结、搜索和浏览大量未标记文档及其内容的先驱。一般来说,我们试图找到一起出现的特征(单词、子图像等)的集群。

下图描绘了 LDA 的整体方案:

为了完整性,请务必参考此处引用的白皮书:ai.stanford.edu/~ang/papers/nips01-lda.pdf

LDA 算法的步骤如下:

  1. 初始化以下参数(控制集中度和平滑度):

    1. Alpha 参数(高 alpha 值使得文档间更为相似,且包含相似的主题)

    2. Beta 参数(高 beta 值意味着每个主题最可能包含大多数单词的混合)

  2. 随机初始化主题分配。

  3. 迭代:

    1. 对于每个文档。

      1. 对于文档中的每个单词。

      2. 为每个单词重新采样主题。

        1. 相对于所有其他单词及其当前分配(对于当前迭代)。
  4. 获取结果。

  5. 模型评估

在统计学中,Dirichlet 分布 Dir(alpha)是一族由正实数向量α参数化的连续多元概率分布。关于 LDA 的更深入探讨,请参阅原始论文:

机器学习杂志上的原论文链接:www.jmlr.org/papers/volume3/blei03a/blei03a.pdf

LDA 不对主题赋予任何语义,也不关心主题的名称。它只是一个生成模型,使用细粒度项(例如,关于猫、狗、鱼、汽车的单词)的分布来分配总体主题,该主题得分最高。它不知道、不关心,也不理解被称为狗或猫的主题。

我们通常需要通过 TF-IDF 对文档进行分词和向量化,然后才能输入到 LDA 算法中。

还有更多...

下图简要描绘了 LDA:

文档分析有两种方法。我们可以简单地使用矩阵分解将大型数据集矩阵分解为较小的矩阵(主题分配)乘以向量(主题本身):

另请参阅

另请参阅,通过 Spark 的 Scala API,以下文档链接:

  • DistributedLDAModel

  • EMLDAOptimizer

  • LDAOptimizer

  • LocalLDAModel

  • OnlineLDAOptimizer

流式 KMeans 用于近实时分类数据

Spark 流式处理是一个强大的功能,它允许您在同一范式中结合近实时和批处理。流式 KMeans 接口位于 ML 聚类和 Spark 流式处理的交叉点,充分利用了 Spark 流式处理本身提供的核心功能(例如,容错、精确一次交付语义等)。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 导入流式 KMeans 所需的包:

package spark.ml.cookbook.chapter14.

  1. 导入流式 KMeans 所需的包:
import org.apache.log4j.{Level, Logger}
 import org.apache.spark.mllib.clustering.StreamingKMeans
 import org.apache.spark.mllib.linalg.Vectors
 import org.apache.spark.mllib.regression.LabeledPoint
 import org.apache.spark.sql.SparkSession
 import org.apache.spark.streaming.{Seconds, StreamingContext}
  1. 我们为流式 KMeans 程序设置了以下参数。训练目录将是发送训练数据文件的目录。KMeans 聚类模型利用训练数据运行算法和计算。testDirectory将用于预测的测试数据。batchDuration是以秒为单位的批处理运行时间。在以下情况下,程序将每 10 秒检查一次是否有新的数据文件用于重新计算。

  2. 集群设置为2,数据维度将为3

val trainingDir = "../data/sparkml2/chapter8/trainingDir" val testDir = "../data/sparkml2/chapter8/testDir" val batchDuration = 10
 val numClusters = 2
 val numDimensions = 3
  1. 使用上述设置,示例训练数据将包含以下格式的数据(以[X[1], X[2], ...X[n]]格式,其中nnumDimensions):

[0.0,0.0,0.0]

[0.1,0.1,0.1]

[0.2,0.2,0.2]

[9.0,9.0,9.0]

[9.1,9.1,9.1]

[9.2,9.2,9.2]

[0.1,0.0,0.0]

[0.2,0.1,0.1]

....

测试数据文件将包含以下格式的数据(以(y, [X1, X2, .. Xn])格式,其中nnumDimensionsy是标识符):

(7,[0.4,0.4,0.4])

(8,[0.1,0.1,0.1])

(9,[0.2,0.2,0.2])

(10,[1.1,1.0,1.0])

(11,[9.2,9.1,9.2])

(12,[9.3,9.2,9.3])

  1. 我们设置必要的 Spark 上下文以访问集群:
val spark = SparkSession
 .builder.master("local[*]")
 .appName("myStreamingKMeans")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()
  1. 定义流式上下文和微批处理窗口:
val ssc = new StreamingContext(spark.sparkContext, Seconds(batchDuration.toLong))
  1. 以下代码将通过解析上述两个目录中的数据文件创建trainingDatatestData RDDs
val trainingData = ssc.textFileStream(trainingDir).map(Vectors.parse)
 val testData = ssc.textFileStream(testDir).map(LabeledPoint.parse)
  1. 我们创建StreamingKMeans模型并设置参数:
val model = new StreamingKMeans()
 .setK(numClusters)
 .setDecayFactor(1.0)
 .setRandomCenters(numDimensions, 0.0)
  1. 程序将使用训练数据集训练模型,并使用测试数据集进行预测:
model.trainOn(trainingData)
 model.predictOnValues(testData.map(lp => (lp.label, lp.features))).print()
  1. 我们启动流式上下文,程序将每 10 秒运行一次批处理,以检查是否有新的训练数据集可用,以及是否有新的测试数据集用于预测。如果收到终止信号(退出批处理运行),程序将退出。
ssc.start()
 ssc.awaitTermination()
  1. 我们将testKStreaming1.txt数据文件复制到上述testDir设置中,并在控制台日志中看到以下打印输出:

  1. 对于 Windows 机器,我们将testKStreaming1.txt文件复制到了目录:C:\spark-2.0.0-bin-hadoop2.7\data\sparkml2\chapter8\testDir\

  2. 我们还可以通过访问http://localhost:4040/来检查 SparkUI 以获取更多信息。

作业面板将显示流式作业,如图所示:

如图所示,流式面板将显示上述流式 KMeans 矩阵,显示批处理作业每 10 秒运行一次:

您可以通过点击任何批处理,如图所示,获取有关流式批处理的更多详细信息:

工作原理...

在某些情况下,我们不能使用批处理方法来加载和捕获事件,然后对其做出反应。我们可以使用在内存或着陆数据库中捕获事件的创造性方法,然后快速将其转移到另一个系统进行处理,但大多数这些系统无法作为流式系统运行,并且通常构建成本非常高。

Spark 提供了一种近乎实时的(也称为主观实时)方式,可以接收来自 Twitter feeds、信号等的传入源,通过连接器(例如 Kafka 连接器)进行处理,并以 RDD 接口的形式呈现。

这些是构建和构造 Spark 中流式 KMeans 所需的元素:

  1. 使用流式上下文而不是...

还有更多...

流式 KMeans 是 KMeans 实现的一种特殊情况,其中数据可以近乎实时地到达,并根据需要被分类到集群(硬分类)中。关于 Voronoi 图的参考,请参见以下 URL:en.wikipedia.org/wiki/Voronoi_diagram

目前,Spark 机器学习库中除了流式 KMeans 外还有其他算法,如图所示:

另请参阅

第十五章:使用 Spark 2.0 ML 库实现文本分析

本章中,我们将涵盖以下内容:

  • 使用 Spark 进行词频统计 - 一切计数

  • 使用 Spark 通过 Word2Vec 显示相似词

  • 为实际的 Spark ML 项目下载维基百科的完整数据集

  • 使用 Spark 2.0 进行文本分析的潜在语义分析

  • Spark 2.0 中的主题建模与潜在狄利克雷分配

引言

文本分析处于机器学习、数学、语言学和自然语言处理的交叉点。文本分析,在较早的文献中称为文本挖掘,试图从未结构化和半结构化数据中提取信息并推断更高级别的概念、情感和语义细节。值得注意的是,传统的关键词搜索不足以处理需要根据实际上下文过滤掉的噪声、模糊和无关的词条和概念。

最终,我们试图做的是对于一组给定的文档(文本、推文、网页和社交媒体),确定沟通的要点以及它试图传达的概念(主题和...

使用 Spark 进行词频统计 - 一切计数

对于此示例,我们将从古腾堡项目下载一本文本格式的书籍,网址为www.gutenberg.org/cache/epub/62/pg62.txt

古腾堡项目提供超过 50,000 种格式的免费电子书供人类消费。请阅读他们的使用条款;让我们不要使用命令行工具下载任何书籍。

当你查看文件内容时,会注意到该书的标题和作者是《火星公主》项目古腾堡电子书》,作者是埃德加·赖斯·巴勒斯。

这本电子书可供任何人免费使用,几乎没有任何限制。你可以复制它、赠送它,或根据古腾堡项目许可证中包含的条款重新使用它,该许可证可在www.gutenberg.org/在线获取。

然后,我们使用下载的书籍来演示使用 Scala 和 Spark 的经典词频统计程序。这个例子起初可能看起来有些简单,但我们正在开始文本处理的特征提取过程。此外,对文档中词频计数的一般理解将大大有助于我们理解 TF-IDF 的概念。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 该示例的声明如下:

package spark.ml.cookbook.chapter12
  1. 导入 Scala、Spark 和 JFreeChart 所需的必要包:
import org.apache.log4j.{Level, Logger}import org.apache.spark.sql.SQLContextimport org.apache.spark.{SparkConf, SparkContext}import org.jfree.chart.axis.{CategoryAxis, CategoryLabelPositions}import org.jfree.chart.{ChartFactory, ChartFrame, JFreeChart}import org.jfree.chart.plot.{CategoryPlot, PlotOrientation}import org.jfree.data.category.DefaultCategoryDataset
  1. 我们将定义一个函数,在窗口中显示我们的 JFreeChart:
def show(chart: JFreeChart) ...

其工作原理...

我们首先加载下载的书籍,并通过正则表达式对其进行分词。接下来的步骤是将所有词条转换为小写,并从词条列表中排除停用词,然后过滤掉任何长度小于两个字符的词。

去除停用词和特定长度的词减少了我们需要处理的特征数量。这可能不明显,但根据各种处理标准去除特定单词会减少机器学习算法后续处理的维度数量。

最后,我们对结果的词频进行了降序排序,取前 25 个,并为其展示了一个条形图。

还有更多...

在本菜谱中,我们有了关键词搜索的基础。理解主题建模和关键词搜索之间的区别很重要。在关键词搜索中,我们试图根据出现次数将短语与给定文档关联。在这种情况下,我们将向用户指向出现次数最多的文档集。

另请参见

该算法的下一步演化,开发者可以尝试作为扩展,将是添加权重并计算加权平均值,但 Spark 提供了我们将在接下来的菜谱中探讨的设施。

使用 Spark 的 Word2Vec 展示相似词

在本菜谱中,我们将探讨 Word2Vec,这是 Spark 评估词相似性的工具。Word2Vec 算法受到普通语言学中分布假设的启发。其核心思想是,出现在相同上下文(即与目标的距离)中的词倾向于支持相同的原始概念/意义。

谷歌的一组研究人员发明了 Word2Vec 算法。请参考本菜谱中还有更多...部分提到的白皮书,该白皮书对 Word2Vec 进行了更详细的描述。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 本菜谱的package语句如下:

package spark.ml.cookbook.chapter12
  1. 导入 Scala 和 Spark 所需的必要包:
import org.apache.log4j.{Level, Logger}
import org.apache.spark.ml.feature.{RegexTokenizer, StopWordsRemover, Word2Vec}
import org.apache.spark.sql.{SQLContext, SparkSession}
import org.apache.spark.{SparkConf, SparkContext}
  1. 让我们定义书籍文件的位置:
val input = "../data/sparkml2/chapter12/pg62.txt"
  1. 使用工厂构建器模式创建具有配置的 Spark 会话:
val spark = SparkSession
         .builder
.master("local[*]")
         .appName("Word2Vec App")
         .config("spark.sql.warehouse.dir", ".")
         .getOrCreate()
import spark.implicits._
  1. 我们应该将日志级别设置为警告,否则输出将难以跟踪:
Logger.getRootLogger.setLevel(Level.WARN)
  1. 我们加载书籍并将其转换为 DataFrame:
val df = spark.read.text(input).toDF("text")
  1. 我们现在利用 Spark 的正则表达式分词器将每行转换为词袋,将每个词转换为小写,并过滤掉任何字符长度小于四的词:
val tokenizer = new RegexTokenizer()
 .setPattern("\\W+")
 .setToLowercase(true)
 .setMinTokenLength(4)
 .setInputCol("text")
 .setOutputCol("raw")
 val rawWords = tokenizer.transform(df)
  1. 我们使用 Spark 的StopWordRemover类去除停用词:
val stopWords = new StopWordsRemover()
 .setInputCol("raw")
 .setOutputCol("terms")
 .setCaseSensitive(false)
 val wordTerms = stopWords.transform(rawWords)
  1. 我们应用 Word2Vec 机器学习算法提取特征:
val word2Vec = new Word2Vec()
 .setInputCol("terms")
 .setOutputCol("result")
 .setVectorSize(3)
 .setMinCount(0)
val model = word2Vec.fit(wordTerms)
  1. 我们从书中找出martian的十个同义词:
val synonyms = model.findSynonyms("martian", 10)
  1. 展示模型找到的十个同义词的结果:
synonyms.show(false)

  1. 我们通过停止 SparkContext 来关闭程序:
spark.stop()

它是如何工作的...

在 Spark 中,Word2Vec 使用的是跳字模型而非连续词袋模型CBOW),后者更适合神经网络NN)。其核心在于尝试计算词的表示。强烈建议用户理解局部表示与分布式表示之间的差异,这与词本身的表面意义截然不同。

如果使用分布式向量表示词,自然地,相似的词在向量空间中会彼此靠近,这是一种理想的模式抽象和操作泛化技术(即,我们将问题简化为向量运算)。

对于给定的一组词{Word[1,] Word[2, .... ...],我们想要做的是

还有更多...

无论如何,你如何找到相似的词?有多少算法能解决这个问题,它们之间有何不同?Word2Vec 算法已经存在一段时间,并且有一个对应的模型称为 CBOW。请记住,Spark 提供的实现技术是跳字模型。

Word2Vec 算法的变化如下:

  • 连续词袋模型(CBOW):给定一个中心词,周围的词是什么?

  • 跳字模型:如果我们知道周围的词,能否猜出缺失的词?

有一种算法变体称为带负采样的跳字模型SGNS),它似乎优于其他变体。

共现是 CBOW 和跳字模型背后的基本概念。尽管跳字模型并不直接使用共现矩阵,但它间接地使用了它。

在本方法中,我们使用了 NLP 中的停用词技术,在运行算法前对语料库进行净化。停用词如英语中的"the"需要被移除,因为它们对结果的改进没有贡献。

另一个重要概念是词干提取,这里未涉及,但将在后续方法中展示。词干提取去除额外的语言特征,将词还原为其词根(例如,EngineeringEngineerEngineers变为Engin,即词根)。

以下 URL 的白皮书应提供对 Word2Vec 更深入的解释:

arxiv.org/pdf/1301.3781.pdf

参见

Word2Vec 方法的文档:

为实际的 Spark ML 项目下载完整的维基百科转储

在本示例中,我们将下载并探索维基百科的转储,以便我们有一个实际的示例。本示例中我们将下载的数据集是维基百科文章的转储。您将需要命令行工具curl或浏览器来检索压缩文件,目前该文件大小约为 13.6 GB。由于文件较大,我们建议使用 curl 命令行工具。

如何操作...

  1. 您可以使用以下命令开始下载数据集:
curl -L -O http://dumps.wikimedia.org/enwiki/latest/enwiki-latest-pages-articles-multistream.xml.bz2
  1. 现在您想要解压缩 ZIP 文件:
bunzip2 enwiki-latest-pages-articles-multistream.xml.bz2

这将创建一个未压缩的文件,名为enwiki-latest-pages-articles-multistream.xml,大小约为 56 GB。

  1. 让我们来看看维基百科 XML 文件:
head -n50 enwiki-latest-pages-articles-multistream.xml<mediawiki xmlns=http://www.mediawiki.org/xml/export-0.10/  xsi:schemaLocation="http://www.mediawiki.org/xml/export-0.10/ http://www.mediawiki.org/xml/export-0.10.xsd" version="0.10" ...

还有更多...

我们建议将 XML 文件分块处理,并在准备好提交最终作业之前使用抽样进行实验。这将节省大量时间和精力。

另请参阅

维基下载文档可在en.wikipedia.org/wiki/Wikipedia:Database_download找到。

使用 Spark 2.0 进行文本分析的潜在语义分析

在本示例中,我们将利用维基百科文章的数据转储来探索 LSA。LSA 意味着分析文档集合以发现这些文档中的隐藏含义或概念。

本章第一个示例中,我们介绍了 TF(即词频)技术的基本概念。在本示例中,我们使用 HashingTF 计算 TF,并使用 IDF 将模型拟合到计算出的 TF 上。LSA 的核心在于对词频文档进行奇异值分解SVD),以降低维度并提取最重要的概念。我们还需要进行其他清理步骤(例如,停用词和词干提取),以在开始分析之前清理词袋。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 本示例的包声明如下:

package spark.ml.cookbook.chapter12
  1. 为 Scala 和 Spark 导入必要的包:
import edu.umd.cloud9.collection.wikipedia.WikipediaPage import edu.umd.cloud9.collection.wikipedia.language.EnglishWikipediaPage import org.apache.hadoop.fs.Path import org.apache.hadoop.io.Text import org.apache.hadoop.mapred.{FileInputFormat, JobConf} import org.apache.log4j.{Level, Logger} import org.apache.spark.mllib.feature.{HashingTF, IDF} import org.apache.spark.mllib.linalg.distributed.RowMatrix import org.apache.spark.sql.SparkSession import org.tartarus.snowball.ext.PorterStemmer ...

它是如何工作的...

示例首先通过使用 Cloud9 Hadoop XML 流工具加载维基百科 XML 转储来处理庞大的 XML 文档。一旦我们解析出页面文本,分词阶段就会将我们的维基百科页面文本流转换为令牌。在分词阶段,我们使用了 Porter 词干提取器来帮助将单词简化为共同的基本形式。

关于词干提取的更多详细信息,请访问en.wikipedia.org/wiki/Stemming

下一步是使用 Spark 的 HashingTF 对每个页面令牌计算词频。在此阶段完成后,我们利用 Spark 的 IDF 生成逆文档频率。

最后,我们采用了 TF-IDF API,并应用奇异值分解来处理因式分解和降维。

以下截图展示了该方法的步骤和流程:

图片:

可在以下链接找到 Cloud9 Hadoop XML 工具及其他所需依赖:

还有更多...

现在应该很明显,尽管 Spark 没有直接提供 LSA 实现,但 TF-IDF 与 SVD 的结合将使我们能够构建并分解大型语料库矩阵为三个矩阵,这有助于我们通过 SVD 进行降维来解释结果。我们可以专注于最有意义的集群(类似于推荐算法)。

SVD 将词频文档(即按属性划分的文档)分解为三个不同的矩阵,这些矩阵更便于从难以处理且成本高昂的大型矩阵中提取N个概念(例如,在我们的例子中N=27)。在机器学习中,我们总是偏好高瘦矩阵(即本例中的U矩阵)...

另请参阅

关于SingularValueDecomposition()的更多详情,请参阅spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.mllib.linalg.SingularValueDecomposition

关于RowMatrix()的更多详情,请参考spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.mllib.linalg.distributed.RowMatrix

使用 Spark 2.0 进行主题建模与潜在狄利克雷分配

在本方法中,我们将展示如何利用潜在狄利克雷分配(Latent Dirichlet Allocation)从文档集合中推断主题模型。

我们在前面的章节中已经介绍了 LDA,因为它适用于聚类和主题建模,但在本章中,我们展示了一个更复杂的示例,以展示它如何应用于使用更真实和复杂数据集的文本分析。

我们还应用了诸如词干提取和停用词等 NLP 技术,以提供更真实的 LDA 问题解决方法。我们试图做的是发现一组潜在因素(即与原始因素不同),这些因素可以在减少的...中以更高效的方式解决问题并描述解决方案。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 该配方中的package声明如下:

package spark.ml.cookbook.chapter12
  1. 导入 Scala 和 Spark 所需的包:
import edu.umd.cloud9.collection.wikipedia.WikipediaPage
import edu.umd.cloud9.collection.wikipedia.language.EnglishWikipediaPage
import org.apache.hadoop.fs.Path
import org.apache.hadoop.io.Text
import org.apache.hadoop.mapred.{FileInputFormat, JobConf}
import org.apache.log4j.{Level, Logger}
import org.apache.spark.ml.clustering.LDA
import org.apache.spark.ml.feature._
import org.apache.spark.sql.SparkSession
  1. 我们定义了一个函数来解析 Wikipedia 页面并返回页面的标题和内容文本:
def parseWikiPage(rawPage: String): Option[(String, String)] = {
 val wikiPage = new EnglishWikipediaPage()
 WikipediaPage.*readPage*(wikiPage, rawPage)

 if (wikiPage.isEmpty
 || wikiPage.isDisambiguation
 || wikiPage.isRedirect
 || !wikiPage.isArticle) {
 None
 } else {
 *Some*(wikiPage.getTitle, wikiPage.getContent)
 }
 }
  1. 让我们定义 Wikipedia 数据转储的位置:
val input = "../data/sparkml2/chapter12/enwiki_dump.xml" 
  1. 我们为 Hadoop XML 流创建作业配置:
val jobConf = new JobConf()
 jobConf.set("stream.recordreader.class", "org.apache.hadoop.streaming.StreamXmlRecordReader")
 jobConf.set("stream.recordreader.begin", "<page>")
 jobConf.set("stream.recordreader.end", "</page>")
  1. 我们为 Hadoop XML 流处理设置数据路径:
FileInputFormat.addInputPath(jobConf, new Path(input))
  1. 使用工厂构建器模式创建具有配置的SparkSession
val spark = SparkSession
    .builder
.master("local[*]")
    .appName("ProcessLDA App")
    .config("spark.serializer",   "org.apache.spark.serializer.KryoSerializer")
    .config("spark.sql.warehouse.dir", ".")
    .getOrCreate()
  1. 我们应该将日志级别设置为警告,否则输出将难以跟踪:
Logger.getRootLogger.setLevel(Level.WARN)
  1. 我们开始处理巨大的 Wikipedia 数据转储成文章页面,从文件中抽取样本:
val wikiData = spark.sparkContext.hadoopRDD(
 jobConf,
 classOf[org.apache.hadoop.streaming.StreamInputFormat],
 classOf[Text],
 classOf[Text]).sample(false, .1)
  1. 接下来,我们将示例数据处理成包含标题和页面上下文文本的元组的 RDD,最终生成一个 DataFrame:
val df = wiki.map(_._1.toString)
 .flatMap(parseWikiPage)
 .toDF("title", "text")
  1. 我们现在使用 Spark 的RegexTokenizer将 DataFrame 的文本列转换为每个 Wikipedia 页面的原始单词:
val tokenizer = new RegexTokenizer()
 .setPattern("\\W+")
 .setToLowercase(true)
 .setMinTokenLength(4)
 .setInputCol("text")
 .setOutputCol("raw")
 val rawWords = tokenizer.transform(df)
  1. 下一步是过滤原始单词,通过去除令牌中的所有停用词:
val stopWords = new StopWordsRemover()
 .setInputCol("raw")
 .setOutputCol("words")
 .setCaseSensitive(false)

 val wordData = stopWords.transform(rawWords)
  1. 我们使用 Spark 的CountVectorizer类为过滤后的令牌生成词频,从而产生包含列特征的新 DataFrame:
val cvModel = new CountVectorizer()
 .setInputCol("words")
 .setOutputCol("features")
 .setMinDF(2)
 .fit(wordData)
 val cv = cvModel.transform(wordData)
 cv.cache()

"MinDF"指定必须出现在词汇表中的不同文档术语的最小数量。

  1. 我们现在调用 Spark 的 LDA 类来生成主题以及令牌到主题的分布:
val lda = new LDA()
 .setK(5)
 .setMaxIter(10)
 .setFeaturesCol("features")
 val model = lda.fit(tf)
 val transformed = model.transform(tf)

"K"指的是主题数量,"MaxIter"是执行的最大迭代次数。

  1. 我们最终描述了生成的五个顶级主题并显示:
val topics = model.describeTopics(5)
 topics.show(false)

  1. 现在显示与主题相关联的主题和术语:
val vocaList = cvModel.vocabulary
topics.collect().foreach { r => {
 println("\nTopic: " + r.get(r.fieldIndex("topic")))
 val y = r.getSeqInt).map(vocaList(_))
 .zip(r.getSeqDouble))
 y.foreach(println)

 }
}

控制台输出将如下所示:

  1. 我们通过停止 SparkContext 来关闭程序:
spark.stop()

它是如何工作的...

我们首先加载 Wikipedia 文章的转储,并使用 Hadoop XML 利用流 API 将页面文本解析为令牌。特征提取过程利用了几个类来设置最终由 LDA 类处理的流程,让令牌从 Spark 的RegexTokenizeStopwordsRemoverHashingTF流过。一旦我们有了词频,数据就被传递给 LDA 类,以便在几个主题下将文章聚类在一起。

Hadoop XML 工具和其他几个必需的依赖项可以在以下位置找到:

还有更多...

请参阅第八章《Apache Spark 2.0 无监督聚类》中的LDA 食谱,以获取有关 LDA 算法本身的更详细解释,该章节介绍了如何将文档和文本分类为主题。

《机器学习研究杂志》(JMLR) 的以下白皮书为希望进行深入分析的人提供了全面的论述。这是一篇写得很好的论文,具有统计和数学基础的人应该能够毫无困难地理解它。

欲了解更多关于 JMLR 的详情,请参考www.jmlr.org/papers/volume3/blei03a/blei03a.pdf链接;另有一替代链接为www.cs.colorado.edu/~mozer/Teaching/syllabi/ProbabilisticModels/readings/BleiNgJordan2003.pdf

另请参阅

亦可参阅 Spark 的 Scala API 文档,了解以下内容:

  • DistributedLDAModel

  • EMLDAOptimizer

  • LDAOptimizer

  • LocalLDAModel

  • OnlineLDAOptimizer

第十六章:Spark 流处理与机器学习库

在本章中,我们将介绍以下内容:

  • 结构化流处理,用于近实时机器学习

  • 使用流式 DataFrames 进行实时机器学习

  • 使用流式数据集进行实时机器学习

  • 使用 queueStream 进行流数据和调试

  • 下载并理解著名的鸢尾花数据集,用于无监督分类

  • 为实时在线分类器实现流式 KMeans

  • 下载葡萄酒质量数据,用于流式回归

  • 为实时回归实现流式线性回归

  • 下载 Pima 糖尿病数据集,用于监督分类

  • 为在线分类器实现流式逻辑回归

引言

Spark 流处理是一个不断发展的过程,旨在统一和结构化 API,以解决批处理与流处理的关切。自 Spark 1.3 起,Spark 流处理就已提供离散流DStream)。新的方向是使用无界表模型抽象底层框架,用户可以使用 SQL 或函数式编程查询表,并将输出写入到另一个输出表中,支持多种模式(完整、增量和追加输出)。Spark SQL Catalyst 优化器和 Tungsten(堆外内存管理器)现已成为 Spark 流处理的核心部分,从而实现更高效的执行。

在本章中,我们不仅涵盖了 Spark 中可用的流处理功能...

结构化流处理,用于近实时机器学习

在本节中,我们将探讨 Spark 2.0 引入的新结构化流处理范式。我们通过使用套接字和结构化流 API 进行实时流处理,并相应地投票和统计票数。

我们还通过模拟一系列随机生成的投票来探索新引入的子系统,以选出最不受欢迎的漫画书反派角色。

本节包含两个不同的程序(VoteCountStream.scalaCountStreamproducer.scala)。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序所在的包位置:

package spark.ml.cookbook.chapter13
  1. 导入必要的包,以便 Spark 上下文可以访问集群,并使用log4j.Logger减少 Spark 产生的输出量:
import org.apache.log4j.{Level, Logger}import org.apache.spark.sql.SparkSessionimport java.io.{BufferedOutputStream, PrintWriter}import java.net.Socketimport java.net.ServerSocketimport java.util.concurrent.TimeUnitimport scala.util.Randomimport org.apache.spark.sql.streaming.ProcessingTime
  1. 定义一个 Scala 类,用于将投票数据生成到客户端套接字:
class CountSreamThread(socket: ...

工作原理...

在本节中,我们创建了一个简单的数据生成服务器,用于模拟投票数据流,并对投票进行计数。下图提供了这一概念的高层次描述:

首先,我们启动了数据生成服务器。其次,我们定义了一个套接字数据源,以便连接到数据生成服务器。接着,我们构建了一个简单的 Spark 表达式,按反派(即坏超级英雄)分组并统计当前收到的所有投票。最后,我们设置了一个 10 秒的阈值触发器来执行我们的流查询,该查询将累积结果输出到控制台。

本方案涉及两个简短的程序:

  • CountStreamproducer.scala

    • 生产者 - 数据生成服务器

    • 模拟投票过程并进行广播

  • VoteCountStream.scala

    • 消费者 - 消费并聚合/制表数据

    • 接收并统计我们反派超级英雄的投票

还有更多...

本书不涉及使用 Spark 流处理和结构化流处理的编程主题,但我们认为有必要分享一些程序以引入概念,然后再深入探讨 Spark 的 ML 流处理功能。

如需对流处理进行全面介绍,请参阅 Spark 相关文档:

另请参阅

实时机器学习的流数据帧

在本例中,我们探讨了流式 DataFrame 的概念。我们创建了一个包含个人姓名和年龄的 DataFrame,该数据将通过网络流式传输。流式 DataFrame 是与 Spark ML 配合使用的一种流行技术,因为在撰写本文时,Spark 结构化 ML 尚未完全集成。

本例仅限于演示流式 DataFrame,并留给读者将其适配到自己的自定义 ML 管道中。虽然流式 DataFrame 在 Spark 2.1.0 中并非开箱即用,但它将是 Spark 后续版本的自然演进。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter13
  1. 导入必要的包:
import java.util.concurrent.TimeUnit
import org.apache.log4j.{Level, Logger}
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.streaming.ProcessingTime
  1. 创建一个SparkSession作为访问 Spark 集群的入口点:
val spark = SparkSession
.builder.master("local[*]")
.appName("DataFrame Stream")
.config("spark.sql.warehouse.dir", ".")
.getOrCreate()

  1. 日志消息的交错导致输出难以阅读,因此将日志级别设置为警告:
Logger.getLogger("org").setLevel(Level.ERROR)
Logger.getLogger("akka").setLevel(Level.ERROR)
  1. 接下来,加载人员数据文件以推断数据模式,无需手动编码结构类型:
val df = spark.read .format("json")
.option("inferSchema", "true")
.load("../data/sparkml2/chapter13/person.json")
df.printSchema()

从控制台,您将看到以下输出:

root
|-- age: long (nullable = true)
|-- name: string (nullable = true)
  1. 现在配置一个流式 DataFrame 以接收数据:
val stream = spark.readStream
.schema(df.schema)
.json("../data/sparkml2/chapter13/people/")
  1. 让我们执行一个简单的数据转换,通过筛选年龄大于60的记录:
val people = stream.select("name", "age").where("age > 60")
  1. 现在我们将转换后的流数据输出到控制台,该操作将每秒触发一次:
val query = people.writeStream
.outputMode("append")
.trigger(ProcessingTime(1, TimeUnit.SECONDS))
.format("console")
  1. 我们启动定义的流查询,并等待数据出现在流中:
query.start().awaitTermination()
  1. 最终,我们的流查询结果将显示在控制台上:

它是如何工作的...

在本例中,我们首先使用快速方法(使用 JSON 对象)发现人员对象的底层模式,如步骤 6 所述。生成的 DataFrame 将了解我们随后对流输入(通过流式传输文件模拟)施加的模式,并作为流式 DataFrame 处理,如步骤 7 所示。

将流视为 DataFrame 并使用函数式或 SQL 范式对其进行操作的能力是一个强大的概念,如步骤 8 所示。然后,我们继续使用writestream()append模式和 1 秒批处理间隔触发器输出结果。

还有更多...

DataFrames 与结构化编程的结合是一个强大的概念,有助于我们将数据层与流分离,从而使编程变得更加简单。DStream(Spark 2.0 之前)最大的缺点之一是其无法将用户与流/RDD 实现的底层细节隔离。

关于 DataFrames 的文档:

另请参阅

Spark 数据流读写器文档:

实时机器学习的流式数据集

在本示例中,我们创建了一个流式数据集,以展示在 Spark 2.0 结构化编程范式中使用数据集的方法。我们通过数据集从文件中流式传输股票价格,并应用过滤器选择当日收盘价高于$100 的股票。

本示例展示了如何使用简单的结构化流编程模型来过滤和处理传入数据。虽然它类似于 DataFrame,但在语法上存在一些差异。本示例以通用方式编写,以便用户可以根据自己的 Spark ML 编程项目进行定制。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter13
  1. 导入必要的包:
import java.util.concurrent.TimeUnitimport org.apache.log4j.{Level, Logger}import org.apache.spark.sql.SparkSessionimport org.apache.spark.sql.streaming.ProcessingTime
  1. 定义一个 Scala case class来模拟流数据:
case class StockPrice(date: String, open: Double, high: Double, low: Double, close: Double, volume: Integer, adjclose: Double)
  1. 创建SparkSession作为访问 Spark 集群的入口点:
val spark = SparkSession.builder.master("local[*]").appName("Dataset ...

工作原理...

在本示例中,我们将利用通用电气GE)自 1972 年以来的收盘价市场数据。为了简化数据,我们已为本次示例预处理了数据。我们采用了上一示例《实时机器学习的流式数据帧》中的方法,通过查看 JSON 对象来发现模式(步骤 7),并在步骤 8 中将其应用于流。

以下代码展示了如何使用模式使流看起来像一个简单的表格,以便实时从中读取数据。这是一个强大的概念,使得流编程对更多程序员来说变得易于访问。以下代码片段中的schema(s.schema)as[StockPrice]是创建具有关联模式的流式数据集所必需的:

val streamDataset = spark.readStream
            .schema(s.schema)
            .option("sep", ",")
            .option("header", "true")
            .csv("../data/sparkml2/chapter13/ge").as[StockPrice]

还有更多...

所有数据集下可用的 API 文档位于:spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.sql.Dataset

另请参阅

以下文档在探索流式数据集概念时很有帮助:

使用 queueStream 进行流数据和调试

在本食谱中,我们探讨了queueStream()的概念,这是在开发周期中尝试使流式程序工作时的宝贵工具。我们发现queueStream() API 非常有用,并认为其他开发人员可以从完全展示其用法的食谱中受益。

我们首先使用ClickGenerator.scala程序模拟用户浏览与不同网页关联的各种 URL,然后使用ClickStream.scala程序消费和汇总数据(用户行为/访问):

我们使用 Spark 的流式 API,Dstream(),这将需要使用...

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter13
  1. 导入必要的包:
import java.time.LocalDateTime
import scala.util.Random._
  1. 定义一个 Scala case class来模拟用户点击事件,包含用户标识符、IP 地址、事件时间、URL 和 HTTP 状态码:
case class ClickEvent(userId: String, ipAddress: String, time: String, url: String, statusCode: String)
  1. 定义生成的状态码:
val statusCodeData = Seq(200, 404, 500)
  1. 定义生成的 URL:
val urlData = Seq("http://www.fakefoo.com",
 "http://www.fakefoo.com/downloads",
 "http://www.fakefoo.com/search",
 "http://www.fakefoo.com/login",
 "http://www.fakefoo.com/settings",
 "http://www.fakefoo.com/news",
 "http://www.fakefoo.com/reports",
 "http://www.fakefoo.com/images",
 "http://www.fakefoo.com/css",
 "http://www.fakefoo.com/sounds",
 "http://www.fakefoo.com/admin",
 "http://www.fakefoo.com/accounts" )
  1. 定义生成的 IP 地址范围:
val ipAddressData = generateIpAddress()
def generateIpAddress(): Seq[String] = {
 for (n <- 1 to 255) yield s"127.0.0.$n" }
  1. 定义生成的时间戳范围:
val timeStampData = generateTimeStamp()

 def generateTimeStamp(): Seq[String] = {
 val now = LocalDateTime.now()
 for (n <- 1 to 1000) yield LocalDateTime.*of*(now.toLocalDate,
 now.toLocalTime.plusSeconds(n)).toString
 }
  1. 定义生成的用户标识符范围:
val userIdData = generateUserId()

 def generateUserId(): Seq[Int] = {
 for (id <- 1 to 1000) yield id
 }
  1. 定义一个函数来生成一个或多个伪随机事件:
def generateClicks(clicks: Int = 1): Seq[String] = {
 0.until(clicks).map(i => {
 val statusCode = statusCodeData(nextInt(statusCodeData.size))
 val ipAddress = ipAddressData(nextInt(ipAddressData.size))
 val timeStamp = timeStampData(nextInt(timeStampData.size))
 val url = urlData(nextInt(urlData.size))
 val userId = userIdData(nextInt(userIdData.size))

 s"$userId,$ipAddress,$timeStamp,$url,$statusCode" })
 }
  1. 定义一个函数,从字符串解析伪随机ClickEvent
def parseClicks(data: String): ClickEvent = {
val fields = data.split(",")
new ClickEvent(fields(0), fields(1), fields(2), fields(3), fields(4))
 }
  1. 创建 Spark 配置和 Spark 流式上下文,持续时间为 1 秒:
val spark = SparkSession
.builder.master("local[*]")
 .appName("Streaming App")
 .config("spark.sql.warehouse.dir", ".")
 .config("spark.executor.memory", "2g")
 .getOrCreate()
val ssc = new StreamingContext(spark.sparkContext, Seconds(1))
  1. 日志消息的交错导致输出难以阅读,因此将日志级别设置为警告:
Logger.getRootLogger.setLevel(Level.WARN)
  1. 创建一个可变队列,将我们生成的数据附加到其上:
val rddQueue = new Queue[RDD[String]]()
  1. 从流式上下文中创建一个 Spark 队列流,传递我们的数据队列的引用:
val inputStream = ssc.queueStream(rddQueue)
  1. 处理队列流接收到的任何数据,并计算用户点击每个特定链接的总次数:
val clicks = inputStream.map(data => ClickGenerator.parseClicks(data))
 val clickCounts = clicks.map(c => c.url).countByValue()
  1. 打印出12个 URL 及其总数:
clickCounts.print(12)
  1. 启动我们的流式上下文以接收微批量:
ssc.start()
  1. 循环 10 次,每次迭代生成 100 个伪随机事件,并将它们附加到我们的可变队列,以便它们在流式队列抽象中实现:
for (i <- 1 to 10) {
 rddQueue += ssc.sparkContext.parallelize(ClickGenerator.*generateClicks*(100))
 Thread.sleep(1000)
 }
  1. 我们通过停止 Spark 流式上下文来关闭程序:
ssc.stop()

工作原理...

通过本教程,我们介绍了使用许多人忽视的技术来引入 Spark Streaming,即利用 Spark 的QueueInputDStream类构建流式应用程序。QueueInputDStream类不仅有助于理解 Spark 流处理,而且在开发周期中调试也非常有用。在初始步骤中,我们设置了一些数据结构,以便稍后生成用于流处理的伪随机clickstream事件数据。

需要注意的是,在第 12 步中,我们创建的是流式上下文而非 SparkContext。流式上下文用于 Spark 流处理应用。接下来,创建队列和队列流以接收流数据。现在步骤...

另请参见

本质上,queueStream()只是一个 RDD 队列,在 Spark 流处理(2.0 版本之前)转换为 RDD 后形成:

下载并理解著名的鸢尾花数据,用于无监督分类

在本教程中,我们下载并检查了著名的鸢尾花数据集,为即将到来的流式 KMeans 教程做准备,该教程让您实时看到分类/聚类过程。

数据存储在 UCI 机器学习库中,这是一个用于算法原型设计的数据宝库。你会发现 R 语言博客作者们往往钟爱这个数据集。

如何操作...

  1. 您可以通过以下任一命令开始下载数据集:
wget https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data

您也可以使用以下命令:

curl https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data -o iris.data

您也可以使用以下命令:

https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data
  1. 现在我们通过检查iris.data中的数据格式开始数据探索的第一步:
head -5 iris.data
5.1,3.5,1.4,0.2,Iris-setosa
4.9,3.0,1.4,0.2,Iris-setosa
4.7,3.2,1.3,0.2,Iris-setosa
4.6,3.1,1.5,0.2,Iris-setosa
5.0,3.6,1.4,0.2,Iris-setosa
  1. 现在我们来看看鸢尾花数据的格式:
tail -5 iris.data
6.3,2.5,5.0,1.9,Iris-virginica
6.5,3.0,5.2,2.0,Iris-virginica
6.2,3.4,5.4,2.3,Iris-virginica
5.9,3.0,5.1,1.8,Iris-virginica

工作原理...

数据包含 150 个观测值。每个观测值由四个数值特征(以厘米为单位)和一个标签组成,该标签指示每朵鸢尾花所属的类别:

特征/属性

  • 萼片长度(厘米)

  • 萼片宽度(厘米)

  • 花瓣长度(厘米)

  • 花瓣宽度(厘米)

标签/类别

  • 山鸢尾

  • 变色鸢尾

  • 维吉尼亚鸢尾

更多内容...

下图清晰地标示了一朵鸢尾花的花瓣和萼片:

另请参见

以下链接更详细地探讨了鸢尾花数据集:

en.wikipedia.org/wiki/Iris_flower_data_set

实时在线分类器的流式 KMeans

在本教程中,我们探索了 Spark 中用于无监督学习方案的流式 KMeans。流式 KMeans 算法的目的在于根据数据点的相似性因子将其分类或分组到多个簇中。

KMeans 分类方法有两种实现,一种用于静态/离线数据,另一种版本用于持续到达的实时更新数据。

我们将对鸢尾花数据集进行流式聚类,因为新数据流入了我们的流式上下文。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter13
  1. 导入必要的包:
import org.apache.spark.mllib.linalg.Vectorsimport org.apache.spark.mllib.regression.LabeledPointimport org.apache.spark.rdd.RDDimport org.apache.spark.SparkContextimport scala.collection.mutable.Queue
  1. 我们首先定义一个函数,将鸢尾花数据加载到内存中,过滤掉空白行,为每个元素附加一个标识符,最后返回一个字符串和长整型的元组:
def readFromFile(sc: SparkContext) = { sc.textFile("../data/sparkml2/chapter13/iris.data") .filter(s ...

它是如何工作的...

在本教程中,我们首先加载鸢尾花数据集,并使用zip() API 将数据与唯一标识符配对,以生成用于 KMeans 算法的标记点数据结构。

接下来,我们创建了可变队列和QueueInputDStream,用于追加数据以模拟流式处理。一旦QueueInputDStream开始接收数据,流式 k 均值聚类就会开始动态地聚类数据并输出结果。您会注意到的一个有趣之处是,我们正在一个队列流上对训练数据集进行流式处理,而在另一个队列流上对测试数据进行流式处理。当我们向队列追加数据时,KMeans 聚类算法正在处理我们的传入数据,并动态生成簇。

还有更多...

关于StreamingKMeans()的文档:

另请参阅

通过构建器模式或streamingKMeans定义的超参数为:

setDecayFactor()
setK()
setRandomCenters(,)

下载葡萄酒质量数据以进行流式回归

在本教程中,我们从 UCI 机器学习存储库下载并检查葡萄酒质量数据集,为 Spark 的流式线性回归算法准备数据。

如何操作...

您需要以下命令行工具之一curlwget来检索指定数据:

  1. 您可以从以下三个命令中任选其一下载数据集。第一个命令如下:
wget http://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-white.csv

您也可以使用以下命令:

curl http://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-white.csv -o winequality-white.csv

此命令是执行相同操作的第三种方式:

http://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-white.csv
  1. 现在我们开始通过查看winequality-white.csv中的数据格式来进行数据探索的第一步:
head -5 winequality-white.csv

"fixed acidity";"volatile acidity";"citric acid";"residual sugar";"chlorides";"free sulfur dioxide";"total sulfur dioxide";"density";"pH";"sulphates";"alcohol";"quality"
7;0.27;0.36;20.7;0.045;45;170;1.001;3;0.45;8.8;6
6.3;0.3;0.34;1.6;0.049;14;132;0.994;3.3;0.49;9.5;6
8.1;0.28;0.4;6.9;0.05;30;97;0.9951;3.26;0.44;10.1;6
7.2;0.23;0.32;8.5;0.058;47;186;0.9956;3.19;0.4;9.9;6
  1. 现在我们来看看葡萄酒质量数据,了解其格式:
tail -5 winequality-white.csv
6.2;0.21;0.29;1.6;0.039;24;92;0.99114;3.27;0.5;11.2;6
6.6;0.32;0.36;8;0.047;57;168;0.9949;3.15;0.46;9.6;5
6.5;0.24;0.19;1.2;0.041;30;111;0.99254;2.99;0.46;9.4;6
5.5;0.29;0.3;1.1;0.022;20;110;0.98869;3.34;0.38;12.8;7
6;0.21;0.38;0.8;0.02;22;98;0.98941;3.26;0.32;11.8;6

其工作原理...

数据由 1,599 种红葡萄酒和 4,898 种白葡萄酒组成,具有 11 个特征和一个可用于训练的输出标签。

以下是特征/属性的列表:

  • 固定酸度

  • 挥发性酸度

  • 柠檬酸

  • 残余糖分

  • 氯化物

  • 游离二氧化硫

  • 总二氧化硫

  • 密度

  • pH

  • 硫酸盐

  • 酒精

以下是输出标签:

  • 质量(介于 0 到 10 之间的数值)

还有更多...

以下链接列出了流行的机器学习算法的数据集。根据需要可以选择新数据集进行实验。

其他数据集可在en.wikipedia.org/wiki/List_of_datasets_for_machine_learning_research获取。

我们选择了鸢尾花数据集,以便我们可以使用连续数值特征进行线性回归模型。

实时回归的流式线性回归

在本配方中,我们将使用 UCI 的葡萄酒质量数据集和 MLlib 的 Spark 流式线性回归算法来基于一组葡萄酒特征预测葡萄酒质量。

此配方与之前看到的传统回归配方之间的区别在于使用 Spark ML 流式传输来实时使用线性回归模型评估葡萄酒质量。

如何操作...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter13
  1. 导入必要的包:
import org.apache.log4j.{Level, Logger}
 import org.apache.spark.mllib.linalg.Vectors
 import org.apache.spark.mllib.regression.LabeledPoint
 import org.apache.spark.mllib.regression.StreamingLinearRegressionWithSGD
 import org.apache.spark.rdd.RDD
 import org.apache.spark.sql.{Row, SparkSession}
 import org.apache.spark.streaming.{Seconds, StreamingContext}
 import scala.collection.mutable.Queue
  1. 创建 Spark 的配置和流式上下文:
val spark = SparkSession
 .builder.master("local[*]")
 .appName("Regression Streaming App")
 .config("spark.sql.warehouse.dir", ".")
 .config("spark.executor.memory", "2g")
 .getOrCreate()

 import spark.implicits._

 val ssc = new StreamingContext(spark.sparkContext, *Seconds*(2))
  1. 日志消息的交错导致难以阅读的输出,因此将日志级别设置为警告:
Logger.getRootLogger.setLevel(Level.WARN)
  1. 使用 Databricks CSV API 将葡萄酒质量 CSV 加载到 DataFrame 中:
val rawDF = spark.read
 .format("com.databricks.spark.csv")
 .option("inferSchema", "true")
 .option("header", "true")
 .option("delimiter", ";")
 .load("../data/sparkml2/chapter13/winequality-white.csv")
  1. 将 DataFrame 转换为rdd,并将唯一标识符zip到其上:
val rdd = rawDF.rdd.zipWithUniqueId()
  1. 构建查找映射以稍后比较预测质量与实际质量值:
val lookupQuality = rdd.map{ case (r: Row, id: Long)=> (id, r.getInt(11))}.collect().toMap
  1. 将葡萄酒质量转换为标签点,以便与机器学习库一起使用:
val labelPoints = rdd.map{ case (r: Row, id: Long)=> LabeledPoint(id,
 Vectors.dense(r.getDouble(0), r.getDouble(1), r.getDouble(2), r.getDouble(3), r.getDouble(4),
 r.getDouble(5), r.getDouble(6), r.getDouble(7), r.getDouble(8), r.getDouble(9), r.getDouble(10))
 )}
  1. 创建一个可变队列以追加数据:
val trainQueue = new Queue[RDD[LabeledPoint]]()
val testQueue = new Queue[RDD[LabeledPoint]]()
  1. 创建 Spark 流式队列以接收流式数据:
val trainingStream = ssc.queueStream(trainQueue)
val testStream = ssc.queueStream(testQueue)
  1. 配置流式线性回归模型:
val numFeatures = 11
 val model = new StreamingLinearRegressionWithSGD()
 .setInitialWeights(Vectors.zeros(numFeatures))
 .setNumIterations(25)
 .setStepSize(0.1)
 .setMiniBatchFraction(0.25)
  1. 训练回归模型并预测最终值:
model.trainOn(trainingStream)
val result = model.predictOnValues(testStream.map(lp => (lp.label, lp.features)))
result.map{ case (id: Double, prediction: Double) => (id, prediction, lookupQuality(id.asInstanceOf[Long])) }.print()

  1. 启动 Spark 流式上下文:
ssc.start()
  1. 将标签点数据拆分为训练集和测试集:
val Array(trainData, test) = labelPoints.randomSplit(Array(.80, .20))
  1. 将数据追加到训练数据队列以进行处理:
trainQueue += trainData
 Thread.sleep(4000)
  1. 现在将测试数据分成两半并追加到队列以进行处理:
val testGroups = test.randomSplit(*Array*(.50, .50))
 testGroups.foreach(group => {
 testQueue += group
 Thread.sleep(2000)
 })
  1. 一旦数据被队列流接收,您将看到以下输出:

  1. 通过停止 Spark 流式上下文来关闭程序:
ssc.stop()

其工作原理...

我们首先通过 Databrick 的spark-csv库将葡萄酒质量数据集加载到 DataFrame 中。下一步是为我们数据集中的每一行附加一个唯一标识符,以便稍后将预测质量与实际质量匹配。原始数据被转换为带标签的点,以便它可以作为流式线性回归算法的输入。在步骤 9 和 10 中,我们创建了可变队列和 Spark 的QueueInputDStream类的实例,用作进入回归算法的通道。

然后我们创建了流式线性回归模型,该模型将预测我们的最终结果——葡萄酒质量。我们通常从原始数据中创建训练和测试数据集,并将它们附加到适当的队列以开始...

还有更多...

StreamingLinearRegressionWithSGD()的文档:spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.mllib.regression.StreamingLinearRegressionWithSGD

另请参阅

StreamingLinearRegressionWithSGD()的超参数

  • setInitialWeights(Vectors.*zeros*())

  • setNumIterations()

  • setStepSize()

  • setMiniBatchFraction()

还有一个不使用随机梯度下降SGD)版本的StreamingLinearRegression() API:

spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.mllib.regression.StreamingLinearAlgorithm

以下链接提供了线性回归的快速参考:

en.wikipedia.org/wiki/Linear_regression

下载 Pima 糖尿病数据用于监督分类

在本教程中,我们从 UCI 机器学习仓库下载并检查了 Pima 糖尿病数据集。稍后我们将使用该数据集与 Spark 的流式逻辑回归算法。

如何操作...

你需要以下命令行工具之一curlwget来检索指定数据:

  1. 你可以通过以下两个命令之一开始下载数据集。第一个命令如下:
http://archive.ics.uci.edu/ml/machine-learning-databases/pima-indians-diabetes/pima-indians-diabetes.data

这是一个你可以使用的替代方案:

wget http://archive.ics.uci.edu/ml/machine-learning-databases/pima-indians-diabetes/pima-indians-diabetes.data -o pima-indians-diabetes.data
  1. 现在我们开始通过查看pima-indians-diabetes.data中的数据格式来进行数据探索的第一步(从 Mac 或 Linux 终端):
head -5 pima-indians-diabetes.data6,148,72,35,0,33.6,0.627,50,11,85,66,29,0,26.6,0.351,31,0 ...

它是如何工作的...

该数据集有 768 个观测值。每行/记录包含 10 个特征和一个标签值,可用于监督学习模型(即逻辑回归)。标签/类别为1表示检测出糖尿病阳性,0表示检测结果为阴性。

特征/属性:

  • 怀孕次数

  • 口服葡萄糖耐量试验 2 小时血浆葡萄糖浓度

  • 舒张压(mm Hg)

  • 三头肌皮肤褶皱厚度(mm)

  • 2 小时血清胰岛素(mu U/ml)

  • 身体质量指数(体重(kg)/(身高(m)²))

  • 糖尿病遗传函数

  • 年龄(岁)

  • 类别变量(0 或 1)

    Label/Class:
               1 - tested positive
               0 - tested negative

还有更多...

我们发现普林斯顿大学提供的以下替代数据集非常有帮助:

data.princeton.edu/wws509/datasets

另请参见

您可以用来探索此配方的数据集必须以这样的方式结构化:标签(预测类别)必须是二元的(检测为糖尿病阳性/阴性)。

流式逻辑回归用于在线分类器

在本配方中,我们将使用之前配方中下载的 Pima 糖尿病数据集和 Spark 的流式逻辑回归算法与 SGD 来预测具有各种特征的 Pima 是否会检测为糖尿病阳性。它是一个在线分类器,根据流数据进行学习和预测。

如何做到这一点...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter13
  1. 导入必要的包:
import org.apache.log4j.{Level, Logger}
import org.apache.spark.mllib.classification.StreamingLogisticRegressionWithSGD
import org.apache.spark.mllib.linalg.Vectors
import org.apache.spark.mllib.regression.LabeledPoint
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.{Row, SparkSession}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import scala.collection.mutable.Queue

  1. 创建一个SparkSession对象作为集群的入口点和一个StreamingContext
val spark = SparkSession
 .builder.master("local[*]")
 .appName("Logistic Regression Streaming App")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()

 import spark.implicits._

 val ssc = new StreamingContext(spark.sparkContext, *Seconds*(2))
  1. 日志消息的交错导致难以阅读的输出,因此将日志级别设置为警告:
Logger.getLogger("org").setLevel(Level.ERROR)
  1. 将 Pima 数据文件加载到字符串类型的数据集中:
val rawDS = spark.read
.text("../data/sparkml2/chapter13/pima-indians- diabetes.data").as[String]
  1. 通过生成一个元组,将最后一个项目作为标签,其余所有内容作为序列,从我们的原始数据集构建 RDD:
val buffer = rawDS.rdd.map(value => {
val data = value.split(",")
(data.init.toSeq, data.last)
})
  1. 将预处理的数据转换为标签点,以便与机器学习库一起使用:
val lps = buffer.map{ case (feature: Seq[String], label: String) =>
val featureVector = feature.map(_.toDouble).toArray[Double]
LabeledPoint(label.toDouble, Vectors.dense(featureVector))
}

  1. 为追加数据创建可变队列:
val trainQueue = new Queue[RDD[LabeledPoint]]()
val testQueue = new Queue[RDD[LabeledPoint]]()
  1. 创建 Spark 流队列以接收流数据:
val trainingStream = ssc.queueStream(trainQueue)
val testStream = ssc.queueStream(testQueue)
  1. 配置流式逻辑回归模型:
val numFeatures = 8
val model = new StreamingLogisticRegressionWithSGD()
.setInitialWeights(Vectors.*zeros*(numFeatures))
.setNumIterations(15)
.setStepSize(0.5)
.setMiniBatchFraction(0.25)
  1. 训练回归模型并预测最终值:
model.trainOn(trainingStream)
val result = model.predictOnValues(testStream.map(lp => (lp.label,
lp.features)))
 result.map{ case (label: Double, prediction: Double) => (label, prediction) }.print()
  1. 启动 Spark 流上下文:
ssc.start()
  1. 将标签点数据拆分为训练集和测试集:
val Array(trainData, test) = lps.randomSplit(*Array*(.80, .20))
  1. 将数据追加到训练数据队列以进行处理:
trainQueue += trainData
 Thread.sleep(4000)
  1. 现在将测试数据对半拆分并追加到队列以进行处理:
val testGroups = test.randomSplit(*Array*(.50, .50))
 testGroups.foreach(group => {
 testQueue += group
 Thread.sleep(2000)
 })
  1. 一旦数据被队列流接收,您将看到以下输出:
-------------------------------------------
Time: 1488571098000 ms
-------------------------------------------
(1.0,1.0)
(1.0,1.0)
(1.0,0.0)
(0.0,1.0)
(1.0,0.0)
(1.0,1.0)
(0.0,0.0)
(1.0,1.0)
(0.0,1.0)
(0.0,1.0)
...
-------------------------------------------
Time: 1488571100000 ms
-------------------------------------------
(1.0,1.0)
(0.0,0.0)
(1.0,1.0)
(1.0,0.0)
(0.0,1.0)
(0.0,1.0)
(0.0,1.0)
(1.0,0.0)
(0.0,0.0)
(1.0,1.0)
...
  1. 通过停止 Spark 流上下文来关闭程序:
ssc.stop()

它是如何工作的...

首先,我们将 Pima 糖尿病数据集加载到数据集中,并将其解析为元组,除了最后一个元素外,我们将每个元素作为特征,最后一个元素作为标签。其次,我们将元组 RDD 转换为标记点,以便它可以作为流式逻辑回归算法的输入。第三,我们创建了可变队列和 Spark 的QueueInputDStream类的实例,用作逻辑算法的通道。

第四,我们创建了流式逻辑回归模型,该模型将预测我们的最终结果的葡萄酒质量。最后,我们通常从原始数据创建训练和测试数据集,并将其追加到适当的队列以触发模型处理流数据。最终...

还有更多...

StreamingLogisticRegressionWithSGD()的文档可在spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.mllib.classification.StreamingLogisticRegressionWithSGD查阅

另请参阅

模型的超参数:

  • setInitialWeights()

  • setNumIterations()

  • setStepSize()

  • setMiniBatchFraction()

posted @ 2024-05-21 12:53  绝不原创的飞龙  阅读(16)  评论(0编辑  收藏  举报