Spark-2-x-机器学习秘籍-全-

Spark 2.x 机器学习秘籍(全)

原文:zh.annas-archive.org/md5/3C1ECF91245FC64E4B95E8DC509841AB

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

教育不是学习事实,

但是训练思维。

  • 阿尔伯特·爱因斯坦

数据是我们时代的新硅,而机器学习与生物启发式认知系统结合,不仅能够实现,还能够加速第四次工业革命的诞生。这本书献给我们的父母,他们通过极大的艰辛和牺牲,使我们的教育成为可能,并教导我们始终要行善。

Apache Spark 2.x 机器学习食谱由四位具有不同背景的朋友共同创作,他们在多个行业和学术学科中拥有丰富的经验。团队在手头的主题上拥有丰富的经验。这本书不仅关乎友谊,也关乎支撑 Spark 和机器学习的科学。我们希望将我们的想法汇集起来,为社区撰写一本书,不仅结合了 Spark 的 ML 代码和真实数据集,还提供了相关的解释、参考和阅读,以便更深入地理解并促进进一步的研究。这本书反映了我们团队在开始使用 Apache Spark 时希望拥有的东西。

我对机器学习和人工智能的兴趣始于八十年代中期,当时我有机会阅读两篇重要的文章,恰好在 1986 年 2 月的人工智能国际期刊第 28 卷第 1 期中依次列出。对于我这一代工程师和科学家来说,这是一个漫长的旅程,幸运的是,弹性分布式计算、云计算、GPU、认知计算、优化和先进的机器学习的进步使长达数十年的梦想成真。所有这些进步对当前一代机器学习爱好者和数据科学家都变得可及。

我们生活在历史上最罕见的时期之一——多种技术和社会趋势在同一时间点融合。具有内置 ML 和深度学习网络访问权限的云计算的弹性将提供一整套新的机会,以创造和占领新的市场。Apache Spark 作为近实时弹性分布式计算和数据虚拟化的通用语言的出现,为聪明的公司提供了在不需要大量投资于专门的数据中心或硬件的情况下应用 ML 技术的机会。

Apache Spark 2.x 机器学习食谱是对 Apache Spark 机器学习 API 最全面的处理之一,选择了 Spark 的子组件,为您提供在掌握机器学习和 Apache Spark 的高端职业之前所需的基础。本书的目标是提供清晰和易懂的内容,反映了我们自己的经验(包括阅读源代码)和学习曲线,我们从 Spark 1.0 开始。

Apache Spark 2.x 机器学习食谱处于 Apache Spark、机器学习和 Scala 的交汇处,面向开发人员和数据科学家,通过实践者的视角,他们不仅需要理解代码,还需要了解给定 Spark ML 算法或 API 的细节、理论和内部工作,以在新经济中建立成功的职业。

本书采用食谱格式,将可下载的即时运行的 Apache Spark ML 代码配方与背景、可操作的理论、参考、研究和真实数据集相结合,以帮助读者理解 Spark 为机器学习库提供的广泛功能背后的什么如何为什么。本书从奠定成功所需的基础开始,然后迅速发展到涵盖 Apache Spark 中所有有意义的 ML 算法。

本书内容

第一章,《使用 Scala 实现 Spark 的实用机器学习》,涵盖了在实际开发环境中安装和配置机器学习和 Apache Spark 的内容。通过屏幕截图,它引导您下载、安装和配置 Apache Spark 和 IntelliJ IDEA,以及必要的库,这些都反映了开发者在真实世界环境中的桌面。然后,它继续识别和列出了 40 多个真实数据集的数据存储库,这些数据集可以帮助读者通过实验和进一步的代码配方。最后,我们在 Spark 上运行我们的第一个 ML 程序,然后提供如何将图形添加到您的机器学习程序的指导,这些图形在后续章节中使用。

第二章,《Spark 中机器学习的线性代数基础》,涵盖了线性代数(向量和矩阵)的使用,这是机器学习中一些最重要的工作的基础。它通过该章的配方全面介绍了 Apache Spark 中可用的 DenseVector、SparseVector 和矩阵功能。它提供了本地和分布式矩阵的配方,包括 RowMatrix、IndexedRowMatrix、CoordinateMatrix 和 BlockMatrix,以提供对这个主题的详细解释。我们包括了这一章,因为只有通过逐行阅读大部分源代码,并理解矩阵分解和向量/矩阵算术在 Spark 中更粗粒度算法下的工作方式,才能掌握 Spark 和 ML/MLlib。

第三章,《Spark 的三大数据武士用于机器学习-完美结合》,提供了 Apache Spark 中具有弹性分布式数据处理和整理功能的三大支柱的端到端处理。该章节包括了从实践者的角度详细介绍 RDDs、DataFrame 和 Dataset 功能的详细配方。通过详尽的 17 个配方、示例、参考和解释,它奠定了在机器学习科学领域建立成功职业的基础。该章节提供了功能性(代码)和非功能性(SQL 接口)的编程方法,以巩固知识基础,反映了一名成功的 Spark ML 工程师在一线公司的真实需求。

第四章,《实现强大机器学习系统的常见配方》,通过 16 个简短但直截了当的代码配方,涵盖了大多数机器学习系统中常见的任务,并将这些任务因素化,读者可以在自己的真实世界系统中使用。它涵盖了一系列技术,从数据归一化到评估模型输出,使用了 Spark 的 ML/MLlib 功能的最佳实践指标,这些可能不会立即对读者可见。这是我们在日常工作中大多数情况下使用的配方的组合,但单独列出以节省空间和其他配方的复杂性。

第五章,《Spark 2.0 中回归和分类的实用机器学习-第一部分》,是探索 Apache Spark 中分类和回归的两章中的第一章。该章从广义线性回归(GLM)开始,扩展到具有不同类型优化的 Lasso、Ridge。然后,该章继续涵盖等温回归、生存回归、多层感知器(神经网络)和一对多分类器。

第六章,“Spark 2.0 中的回归和分类实用机器学习-第二部分”,是两个回归和分类章节中的第二部分。该章节涵盖了基于 RDD 的回归系统,从线性、逻辑和岭到套索,使用 Spark 中的随机梯度下降和 L_BFGS 优化。最后三个配方涵盖了支持向量机(SVM)和朴素贝叶斯,最后详细介绍了在 Spark ML 生态系统中占据重要位置的 ML 管道的配方。

第七章,“使用 Spark 扩展的推荐引擎”,介绍了如何探索数据集并利用 Spark 的 ML 库设施构建电影推荐引擎。它使用了大型数据集和一些配方,以及图表和解释,探索了推荐系统的各种方法,然后深入研究了 Spark 中的协同过滤技术。

第八章,“Apache Spark 2.0 中的无监督聚类”,涵盖了无监督学习中使用的技术,如 KMeans、混合和期望(EM)、幂迭代聚类(PIC)和潜在狄利克雷分布(LDA),同时也涵盖了为了帮助读者理解核心概念而介绍的原因和方法。利用 Spark Streaming,该章节以实时 KMeans 聚类配方开始,通过无监督手段将输入流分类为标记类。

第九章,“优化-使用梯度下降下山”,是一章独特的章节,它带领读者了解优化在机器学习中的应用。它从闭合形式公式和二次函数优化(例如成本函数)开始,到使用梯度下降(GD)来从头解决回归问题。该章节通过使用 Scala 代码来培养读者的技能,并深入解释如何编写和理解从头开始的随机下降(GD)。该章节以 Spark 的 ML API 结束,以实现我们从头开始编码的相同概念。

第十章,“使用决策树和集成模型构建机器学习系统”,深入介绍了 Spark 的机器学习库中用于分类和回归的树和集成模型。我们使用三个真实世界的数据集,使用决策树、随机森林树和梯度提升树来探索分类和回归问题。该章节提供了这些方法的深入解释,以及逐步探索 Apache Spark 的机器学习库的即插即用代码配方。

第十一章,“大数据中的高维度诅咒”,揭开了降维的艺术和科学之谜,并全面介绍了 Spark 的 ML/MLlib 库,该库在大规模机器学习中促进了这一重要概念。该章节充分深入地介绍了理论(什么和为什么),然后继续介绍了 Spark 中供读者使用的两种基本技术(如何)。该章节涵盖了与第二章相关的奇异值分解(SVD),然后深入研究了主成分分析(PCA),并附有代码和解释。

第十二章,使用 Spark 2.0 ML 库实现文本分析,介绍了 Spark 中用于实现大规模文本分析的各种技术。它从基础知识开始,如词频(TF)和相似性技术,如 Word2Vec,然后继续分析完整的维基百科转储,用于实际的 Spark ML 项目。本章最后深入讨论并提供了在 Spark 中实现潜在语义分析(LSA)和使用潜在狄利克雷分配(LDA)进行主题建模的代码。

第十三章,Spark Streaming 和机器学习库,首先介绍了 Spark 流处理的概念和未来发展方向,然后提供了基于 RDD(DStream)和结构化流的食谱,以建立基线。本章然后继续介绍了在撰写本书时 Spark 中所有可用的 ML 流算法。本章提供了代码,并展示了如何实现流 DataFrame 和流数据集,然后继续介绍了用于调试的 queueStream,然后进入了 Streaming KMeans(无监督学习)和使用真实世界数据集的流线性模型,如线性和逻辑回归。

本书所需的内容

请使用软件清单文档中的详细信息。

要执行本书中的食谱,您需要运行 Windows 7 及以上版本或 Mac 10 的系统,并安装以下软件:

  • Apache Spark 2.x

  • Oracle JDK SE 1.8.x

  • JetBrain IntelliJ Community Edition 2016.2.X 或更高版本

  • Scala 插件适用于 IntelliJ 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

此软件的硬件要求在本书的代码包中提供的软件清单中有所提及。

本书适合谁

本书适用于 Scala 开发人员,他们对机器学习技术有相当丰富的经验和理解,但缺乏 Spark 的实际实现。假定您具有扎实的机器学习算法知识,以及一些使用 Scala 实现 ML 算法的实际经验。但是,您不需要熟悉 Spark ML 库和生态系统。

部分

在本书中,您将经常看到几个标题(准备工作、如何做、工作原理、更多内容和参见)。为了清晰地说明如何完成食谱,我们使用以下部分:

准备工作

本节告诉您在食谱中可以期待什么,并描述了为食谱设置任何软件或任何先决设置所需的步骤。

如何做…

本节包含了遵循食谱所需的步骤。

工作原理…

本节通常包括对前一节中发生的事情的详细解释。

更多内容…

本节包括有关食谱的额外信息,以使读者更加了解食谱。

参见

本节提供了有关食谱的其他有用信息的链接。

约定

在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些样式的示例及其含义的解释。文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:"Mac 用户请注意,我们在 Mac 机器上的/Users/USERNAME/spark/spark-2.0.0-bin-hadoop2.7/目录中安装了 Spark 2.0。"

代码块设置如下:

object HelloWorld extends App { 
   println("Hello World!") 
 } 

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

 mysql -u root -p

新术语重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会以这种方式出现在文本中:"配置全局库。选择 Scala SDK 作为您的全局库。"

警告或重要提示会显示为这样。

提示和技巧会显示为这样。

第一章:使用 Scala 进行实用机器学习与 Spark

在本章中,我们将涵盖:

  • 下载和安装 JDK

  • 下载和安装 IntelliJ

  • 下载和安装 Spark

  • 配置 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 提供了一个易于使用的统一技术堆栈中的分布式框架,这使得它成为数据科学项目的首选平台,这些项目往往需要一个最终朝着解决方案合并的迭代算法。由于其内部工作原理,这些算法生成大量中间结果,这些结果需要在中间步骤中从一阶段传递到下一阶段。对于大多数数据科学项目来说,需要一个具有强大本地分布式机器学习库MLlib)的交互式工具,这排除了基于磁盘的方法。

Spark 对集群计算有不同的方法。它解决问题的方式是作为技术堆栈而不是生态系统。大量集中管理的库与一个快速的计算引擎相结合,可以支持容错数据结构,这使得 Spark 成为首选的大数据分析平台,取代了 Hadoop。

Spark 采用模块化方法,如下图所示:

机器学习

机器学习的目标是生产可以模仿人类智能并自动执行一些传统上由人脑保留的任务的机器和设备。机器学习算法旨在在相对较短的时间内处理非常大的数据集,并近似得出人类需要更长时间处理的答案。

机器学习领域可以分为许多形式,从高层次上来看,可以分为监督学习和无监督学习。监督学习算法是一类使用训练集(即标记数据)来计算概率分布或图形模型的机器学习算法,从而使它们能够对新数据点进行分类,而无需进一步的人为干预。无监督学习是一种用于从不带标记响应的输入数据集中推断的机器学习算法。

Spark 提供了丰富的机器学习算法,可以在大型数据集上部署,无需进一步编码。下图描述了 Spark 的 MLlib 算法作为思维导图。Spark 的 MLlib 旨在利用并行性,同时具有容错的分布式数据结构。Spark 将这些数据结构称为弹性分布式数据集RDDs

Scala

Scala是一种现代编程语言,正在成为传统编程语言(如JavaC++)的替代品。Scala 是一种基于 JVM 的语言,不仅提供了简洁的语法,而且还将面向对象和函数式编程结合到了一个极其简洁和非常强大的类型安全语言中。

Scala 采用灵活和富有表现力的方法,使其非常适合与 Spark 的 MLlib 交互。Spark 本身是用 Scala 编写的事实证明了 Scala 语言是一种全方位的编程语言,可以用来创建具有重大性能需求的复杂系统代码。

Scala 通过解决一些 Java 的缺点,同时避免了全有或全无的方法,继承了 Java 的传统。Scala 代码编译成 Java 字节码,从而使其能够与丰富的 Java 库互换使用。能够在 Scala 和 Java 之间使用 Java 库提供了连续性和丰富的环境,使软件工程师能够构建现代和复杂的机器学习系统,而不必完全脱离 Java 传统和代码库。

Scala 完全支持功能丰富的函数式编程范式,标准支持 lambda、柯里化、类型接口、不可变性、惰性求值和一种类似 Perl 的模式匹配范式,而不带有神秘的语法。由于 Scala 支持代数友好的数据类型、匿名函数、协变、逆变和高阶函数,因此 Scala 非常适合机器学习编程。

以下是 Scala 中的 hello world 程序:

object HelloWorld extends App { 
   println("Hello World!") 
 } 

在 Scala 中编译和运行HelloWorld看起来像这样:

《Apache Spark 机器学习食谱》采用实用的方法,以开发人员为重点提供多学科视角。本书侧重于机器学习Apache SparkScala的交互和凝聚力。我们还采取额外步骤,教您如何设置和运行开发环境,使其熟悉开发人员,并提供必须在交互式 shell 中运行的代码片段,而不使用现代 IDE 提供的便利设施:

本书中使用的软件版本和库

以下表格提供了本书中使用的软件版本和库的详细列表。如果您按照本章中的安装说明进行安装,将包括此处列出的大部分项目。可能需要特定配方的任何其他 JAR 或库文件都将通过各自配方中的额外安装说明进行覆盖:

核心系统 版本
Spark 2.0.0
Java 1.8
IntelliJ IDEA 2016.2.4
Scala-sdk 2.11.8

将需要的其他 JAR 如下:

其他 JAR 版本
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

我们还在 Spark 2.1.1 上测试了本书中的所有配方,并发现程序按预期执行。建议您在学习目的上使用这些表中列出的软件版本和库。

为了跟上快速变化的 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 的高级功能。

下载和安装 JDK

第一步是下载 Scala/Spark 开发所需的 JDK 开发环境。

准备工作

当您准备好下载和安装 JDK 时,请访问以下链接:

www.oracle.com/technetwork/java/javase/downloads/index.html

如何做...

下载成功后,请按照屏幕上的说明安装 JDK。

下载和安装 IntelliJ

IntelliJ Community Edition 是用于 Java SE、Groovy、Scala 和 Kotlin 开发的轻量级 IDE。为了完成设置您的机器学习与 Spark 开发环境,需要安装 IntelliJ IDE。

准备工作

当您准备好下载和安装 IntelliJ 时,请访问以下链接:

www.jetbrains.com/idea/download/

如何做...

在撰写本文时,我们使用的是 IntelliJ 15.x 或更高版本(例如,版本 2016.2.4)来测试本书中的示例,但是请随时下载最新版本。下载安装文件后,双击下载的文件(.exe)并开始安装 IDE。如果您不想进行任何更改,请将所有安装选项保持默认设置。按照屏幕上的说明完成安装:

下载和安装 Spark

现在我们继续下载和安装 Spark。

准备工作

当您准备好下载和安装 Spark 时,请访问 Apache 网站上的此链接:

spark.apache.org/downloads.html

如何做...

转到 Apache 网站并选择所需的下载参数,如此屏幕截图所示:

确保接受默认选择(点击下一步)并继续安装。

配置 IntelliJ 以便与 Spark 一起工作并运行 Spark ML 示例代码

在能够运行 Spark 提供的示例或本书中列出的任何程序之前,我们需要运行一些配置以确保项目设置正确。

准备工作

在配置项目结构和全局库时,我们需要特别小心。设置好一切后,我们继续运行 Spark 团队提供的示例 ML 代码以验证设置。示例代码可以在 Spark 目录下找到,也可以通过下载带有示例的 Spark 源代码获得。

如何做...

以下是配置 IntelliJ 以使用 Spark MLlib 并在示例目录中运行 Spark 提供的示例 ML 代码的步骤。示例目录可以在 Spark 的主目录中找到。使用 Scala 示例继续:

  1. 单击“Project Structure...”选项,如下截图所示,以配置项目设置:

  1. 验证设置:

  1. 配置全局库。选择 Scala SDK 作为全局库:

  1. 选择新的 Scala SDK 的 JAR 文件并让下载完成:

  1. 选择项目名称:

  1. 验证设置和额外的库:

  1. 添加依赖的 JAR 文件。在左侧窗格的项目设置下选择模块,然后单击依赖项选择所需的 JAR 文件,如下截图所示:

  1. 选择 Spark 提供的 JAR 文件。选择 Spark 的默认安装目录,然后选择lib目录:

  1. 然后我们选择提供给 Spark 的示例 JAR 文件。

  1. 通过验证在左侧窗格中选择并导入所有列在External Libraries下的 JAR 文件来添加所需的 JAR 文件:

  1. Spark 2.0 使用 Scala 2.11。运行示例需要两个新的流 JAR 文件,Flume 和 Kafka,并可以从以下 URL 下载:

下一步是下载并安装 Flume 和 Kafka 的 JAR 文件。出于本书的目的,我们使用了 Maven 存储库:

  1. 下载并安装 Kafka 组件:

  1. 下载并安装 Flume 组件:

  1. 下载完成后,将下载的 JAR 文件移动到 Spark 的lib目录中。我们在安装 Spark 时使用了C驱动器:

  1. 打开您的 IDE 并验证左侧的External Libraries文件夹中的所有 JAR 文件是否存在于您的设置中,如下截图所示:

  1. 构建 Spark 中的示例项目以验证设置:

  1. 验证构建是否成功:

还有更多...

在 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

您可能想使用版本为 15.0 的 Guava,可以在这里找到:

mvnrepository.com/artifact/com.google.guava/guava/15.0

如果您正在使用以前博客中的安装说明,请确保从安装集中排除 Guava 库。

另请参阅

如果还有其他第三方库或 JAR 文件需要完成 Spark 安装,您可以在以下 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 以及机器学习竞赛内部运作的详细信息。

  • MLdata.org:一个向所有人开放的公共网站,其中包含机器学习爱好者的数据集存储库。

  • Google 趋势:您可以在www.google.com/trends/explore上找到自 2004 年以来任何给定术语的搜索量统计(作为总搜索量的比例)。

  • 中央情报局世界概况www.cia.gov/library/publications/the-world-factbook/链接提供了有关 267 个国家的历史、人口、经济、政府、基础设施和军事的信息。

另请参阅

机器学习数据的其他来源:

有一些专门的数据集(例如,西班牙文本分析和基因和 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. 程序完成后,IntelliJ 项目资源管理器中的myFirstSpark20.scala布局将如下所示:

  1. 确保没有编译错误。您可以通过重新构建项目来测试:

重建完成后,控制台上应该会有一个构建完成的消息:

Information: November 18, 2016, 11:46 AM - Compilation completed successfully with 1 warning in 55s 648ms
  1. 您可以通过右键单击项目资源管理器中的myFirstSpark20对象,并选择上下文菜单选项(如下一截图所示)Run myFirstSpark20来运行上一个程序。

您也可以使用菜单栏中的运行菜单执行相同的操作。

  1. 程序成功执行后,您将看到以下消息:
Process finished with exit code 0

这也显示在以下截图中:

  1. 使用相同的上下文菜单,Mac 用户可以执行此操作。

将代码放在正确的路径中。

工作原理...

在这个例子中,我们编写了我们的第一个 Scala 程序myFirstSpark20.scala,并展示了在 IntelliJ 中执行程序的步骤。我们将代码放在了 Windows 和 Mac 的步骤中描述的路径中。

myFirstSpark20代码中,我们看到了创建SparkSession对象的典型方式,以及如何使用master()函数将其配置为在本地模式下运行。我们从数组对象中创建了两个 RDD,并使用简单的zip()函数创建了一个新的 RDD。

我们还对创建的 RDD 进行了简单的求和计算,然后在控制台中显示了结果。最后,我们通过调用spark.stop()退出并释放资源。

还有更多...

Spark 可以从spark.apache.org/downloads.html下载。

有关与 RDD 相关的 Spark 2.0 文档可以在spark.apache.org/docs/latest/programming-guide.html#rdd-operations找到。

另请参阅

如何将图形添加到您的 Spark 程序

在这个示例中,我们讨论了如何使用 JFreeChart 将图形图表添加到您的 Spark 2.0.0 程序中。

如何做...

  1. 设置 JFreeChart 库。可以从sourceforge.net/projects/jfreechart/files/网站下载 JFreeChart JAR 文件。

  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 机器上将 ZIP 文件解压到C:\,然后继续找到解压目标目录下的lib目录。

  2. 然后找到我们需要的两个库(JFreeChart 需要 JCommon),JFreeChart-1.0.19.jarJCommon-1.0.23

  1. 现在我们将之前提到的两个 JAR 文件复制到C:\spark-2.0.0-bin-hadoop2.7\examples\jars\目录中。

  2. 如前面设置部分中提到的,此目录在 IntelliJ IDE 项目设置的类路径中:

在 macOS 中,您需要将前面提到的两个 JAR 文件放在/Users/USERNAME/spark/spark-2.0.0-bin-hadoop2.7/examples\jars\目录中。

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 下载该书的示例代码,找到MyChart.scala,并将代码放在以下目录中。

  3. 我们在 Windows 的C:\spark-2.0.0-bin-hadoop2.7\目录中安装了 Spark 2.0。将MyChart.scala放在C:\spark-2.0.0-bin-hadoop2.7\examples\src\main\scala\spark\ml\cookbook\chapter1目录中。

  4. 设置程序将驻留的包位置:

  package spark.ml.cookbook.chapter1
  1. 导入 Spark 会话所需的包,以便访问集群和log4j.Logger以减少 Spark 产生的输出量。

  2. 导入用于图形的必要 JFreeChart 包:

import java.awt.Color 
import org.apache.log4j.{Level, Logger} 
import org.apache.spark.sql.SparkSession 
import org.jfree.chart.plot.{PlotOrientation, XYPlot} 
import org.jfree.chart.{ChartFactory, ChartFrame, JFreeChart} 
import org.jfree.data.xy.{XYSeries, XYSeriesCollection} 
import scala.util.Random 
  1. 将输出级别设置为ERROR以减少 Spark 的日志输出:
Logger.getLogger("org").setLevel(Level.ERROR) 
  1. 使用构建模式指定配置初始化 Spark 会话,从而为 Spark 集群提供入口点:
val spark = SparkSession 
  .builder 
  .master("local[*]") 
  .appName("myChart") 
  .config("spark.sql.warehouse.dir", ".") 
  .getOrCreate() 
  1. myChart对象将在本地模式下运行。前面的代码块是创建SparkSession对象的典型开始。

  2. 然后我们使用随机数创建一个 RDD,并将数字与其索引进行压缩:

val data = spark.sparkContext.parallelize(Random.shuffle(1 to 15).zipWithIndex) 
  1. 我们在控制台打印出 RDD:
data.foreach(println) 

这是控制台输出:

  1. 然后我们为 JFreeChart 创建一个数据系列来显示:
val xy = new XYSeries("") 
data.collect().foreach{ case (y: Int, x: Int) => xy.add(x,y) } 
val dataset = new XYSeriesCollection(xy) 
  1. 接下来,我们从 JFreeChart 的ChartFactory创建一个图表对象,并设置基本配置:
val chart = ChartFactory.createXYLineChart( 
  "MyChart",  // chart title 
  "x",               // x axis label 
  "y",                   // y axis label 
  dataset,                   // data 
  PlotOrientation.VERTICAL, 
  false,                    // include legend 
  true,                     // tooltips 
  false                     // urls 
)
  1. 我们从图表中获取绘图对象,并准备显示图形:
val plot = chart.getXYPlot() 
  1. 首先配置绘图:
configurePlot(plot) 
  1. configurePlot函数定义如下;它为图形部分设置了一些基本的颜色方案:
def configurePlot(plot: XYPlot): Unit = { 
  plot.setBackgroundPaint(Color.WHITE) 
  plot.setDomainGridlinePaint(Color.BLACK) 
  plot.setRangeGridlinePaint(Color.BLACK) 
  plot.setOutlineVisible(false) 
} 
  1. 现在我们展示chart
show(chart) 
  1. show()函数定义如下。这是一个非常标准的基于帧的图形显示函数:
def show(chart: JFreeChart) { 
  val frame = new ChartFrame("plot", chart) 
  frame.pack() 
  frame.setVisible(true) 
}
  1. 一旦show(chart)成功执行,将弹出以下窗口:

  1. 通过停止 Spark 会话来关闭程序:
spark.stop() 

它是如何工作的...

在这个示例中,我们编写了MyChart.scala,并看到了在 IntelliJ 中执行程序的步骤。我们在 Windows 和 Mac 的步骤中描述的路径中放置了代码。

在代码中,我们看到了创建SparkSession对象的典型方式以及如何使用master()函数。我们从 1 到 15 范围内的随机整数数组创建了一个 RDD,并将其与索引进行了压缩。

然后,我们使用 JFreeChart 来组合一个基本的图表,其中包含一个简单的xy轴,并使用我们在前面步骤中从原始 RDD 生成的数据集来提供图表。

我们为图表设置了架构,并调用了 JFreeChart 中的show()函数,以显示一个带有xy轴的线性图表。

最后,我们通过调用spark.stop()退出并释放资源。

还有更多...

有关 JFreeChart 的更多信息,请访问以下网站:

另请参阅

Additional examples about the features and capabilities of JFreeChart can be found at the following website:

www.jfree.org/jfreechart/samples.html

第二章:机器学习与 Spark 的线性代数基础

在本章中,我们将涵盖以下内容:

  • 向量和矩阵的包导入和初始设置

  • 创建 DenseVector 并在 Spark 2.0 中设置

  • 创建 SparseVector 并在 Spark 2.0 中设置

  • 创建 DenseMatrix 并在 Spark 2.0 中设置

  • 使用 Spark 2.0 的稀疏本地矩阵

  • 使用 Spark 2.0 进行向量运算

  • 使用 Spark 2.0 进行矩阵运算

  • Spark 2.0 ML 库中的分布式矩阵

  • 在 Spark 2.0 中探索 RowMatrix

  • 在 Spark 2.0 中探索分布式 IndexedRowMatrix

  • 在 Spark 2.0 中探索分布式 CoordinateMatrix

  • 在 Spark 2.0 中探索分布式 BlockMatrix

介绍

线性代数是机器学习ML)和数学 编程MP)的基石。在处理 Spark 的机器库时,必须了解 Scala 提供的 Vector/Matrix 结构(默认导入)与 Spark 提供的 ML、MLlib Vector、Matrix 设施有所不同。后者由 RDD 支持,是所需的数据结构,如果要使用 Spark(即并行性)进行大规模矩阵/向量计算(例如,某些情况下用于衍生定价和风险分析的 SVD 实现替代方案,具有更高的数值精度)。Scala Vector/Matrix 库提供了丰富的线性代数操作,如点积、加法等,在 ML 管道中仍然有其自己的位置。总之,使用 Scala Breeze 和 Spark 或 Spark ML 之间的关键区别在于,Spark 设施由 RDD 支持,允许同时分布式、并发计算和容错,而无需任何额外的并发模块或额外的工作(例如,Akka + Breeze)。

几乎所有的机器学习算法都使用某种形式的分类或回归机制(不一定是线性的)来训练模型,然后通过比较训练输出和实际输出来最小化错误。例如,在 Spark 中任何推荐系统的实现都会严重依赖于矩阵分解、因子分解、近似或单值分解SVD)。另一个与处理大型数据集的维度约简有关的机器学习领域是主成分分析PCA),它严重依赖于线性代数、因子分解和矩阵操作。

当我们第一次在 Spark 1.x.x 中检查 Spark ML 和 MLlib 算法的源代码时,我们很快注意到向量和矩阵使用 RDD 作为许多重要算法的基础。

当我们重新审视 Spark 2.0 和机器学习库的源代码时,我们注意到一些有趣的变化需要在以后考虑。以下是从 Spark 1.6.2 到 Spark 2.0.0 的一些变化的示例,这些变化影响了我们在 Spark 中的一些线性代数代码:

val w3 = w1.toBreeze // spark 1.6.x code
val w4 = w2.toBreeze //spark 1.6.x code
  • 在 Spark 2.0 中,toBreeze()函数不仅已更改为asBreeze(),而且还已降级为私有函数

  • 为了解决这个问题,可以使用以下代码片段之一将前面的向量转换为常用的BreezeVector实例:

val w3 = new BreezeVector(x.toArray)//x.asBreeze, spark 2.0
val w4 = new BreezeVector(y.toArray)//y.asBreeze, spark 2.0

Scala 是一种简洁的语言,对象导向和函数式编程范式可以在其中共存而不冲突。虽然在机器学习范式中,函数式编程更受青睐,但在初始数据收集和稍后的展示阶段使用面向对象的方法也没有问题。

在大规模分布式矩阵方面,我们的经验表明,当处理大矩阵集 10⁹至 10¹³至 10²⁷等时,您必须更深入地研究分布式操作中固有的网络操作和洗牌。根据我们的经验,当您以规模运行时,本地和分布式矩阵/向量操作的组合(例如,点积,乘法等)在操作时效果最佳。

以下图片描述了可用 Spark 向量和矩阵的分类:

包导入和向量矩阵的初始设置

在我们可以在 Spark 中编程或使用向量和矩阵工件之前,我们首先需要导入正确的包,然后设置SparkSession,以便我们可以访问集群句柄。

在这个简短的配方中,我们突出了一系列可以涵盖大多数 Spark 线性代数操作的包。接下来的各个配方将包括特定程序所需的确切子集。

如何做...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter2
  1. 导入用于向量和矩阵操作的必要包:
import org.apache.spark.mllib.linalg.distributed.RowMatrix
import org.apache.spark.mllib.linalg.distributed.{IndexedRow, IndexedRowMatrix}
import org.apache.spark.mllib.linalg.distributed.{CoordinateMatrix, MatrixEntry}
import org.apache.spark.sql.{SparkSession}
import org.apache.spark.rdd._
import org.apache.spark.mllib.linalg._
import breeze.linalg.{DenseVector => BreezeVector}
import Array._
import org.apache.spark.mllib.linalg.DenseMatrix
import org.apache.spark.mllib.linalg.SparseVector
  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("myVectorMatrix")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()

还有更多...

在 Spark 2.0 之前,SparkContext 和 SQLContext 必须分别初始化。如果您计划在 Spark 的早期版本中运行代码,请参考以下代码片段。

设置应用程序参数,以便 Spark 可以运行(使用 Spark 1.5.2 或 Spark 1.6.1):

val conf = new SparkConf().setMaster("local[*]").setAppName("myVectorMatrix").setSparkHome("C:\\spark-1.5.2-bin-hadoop2.6")
 val sc = new SparkContext(conf)
 val sqlContext = new SQLContext(sc)

另请参阅

SparkSession 是 Spark 2.x.x 及以上版本中集群的新入口点。SparkSession 统一了对集群和所有数据的访问。它统一了对 SparkContext、SQLContext 或 HiveContext 的访问,同时使得使用 DataFrame 和 Dataset API 更加容易。我们将在第四章中专门的配方中重新讨论 SparkSession,实现强大的机器学习系统的常见配方

参考以下图片:

方法调用的文档可以在spark.apache.org/docs/2.0.0/api/scala/#org.apache.spark.sql.SparkSession中查看。

创建 DenseVector 并在 Spark 2.0 中设置

在这个配方中,我们将使用 Spark 2.0 机器库来探索DenseVectors

Spark 提供了两种不同类型的向量设施(密集和稀疏),用于存储和操作将用于机器学习或优化算法中的特征向量。

如何做...

  1. 在本节中,我们将研究使用 Spark 2.0 机器库的DenseVectors示例,这些示例最有可能用于实现/增强现有的机器学习程序。这些示例还有助于更好地理解 Spark ML 或 MLlib 源代码和底层实现(例如,单值分解)。

  2. 在这里,我们将从数组创建 ML 向量特征(具有独立变量),这是一个常见用例。在这种情况下,我们有三个几乎完全填充的 Scala 数组,对应于客户和产品特征集。我们将这些数组转换为 Scala 中相应的DenseVectors

val CustomerFeatures1: Array[Double] = Array(1,3,5,7,9,1,3,2,4,5,6,1,2,5,3,7,4,3,4,1)
 val CustomerFeatures2: Array[Double] = Array(2,5,5,8,6,1,3,2,4,5,2,1,2,5,3,2,1,1,1,1)
 val ProductFeatures1: Array[Double]  = Array(0,1,1,0,1,1,1,0,0,1,1,1,1,0,1,2,0,1,1,0)

设置变量以从数组创建向量。从数组转换为DenseVector

val x = Vectors.dense(CustomerFeatures1)
 val y = Vectors.dense(CustomerFeatures2)
 val z = Vectors.dense(ProductFeatures1)
  1. 下一步是创建DenseVector并通过初始化赋值。

这是最常引用的情况,通常用于处理批量输入的类构造函数:

val denseVec2 = Vectors.dense(5,3,5,8,5,3,4,2,1,6)
  1. 以下是另一个示例,展示了在初始化期间从字符串转换为双精度的即时转换。在这里,我们从一个字符串开始,并在内联中调用toDouble
val xyz = Vectors.dense("2".toDouble, "3".toDouble, "4".toDouble)
 println(xyz)

输出如下:

[2.0,3.0,4.0]

它是如何工作的...

  1. 该方法构造函数的签名为:
DenseVector (double[] values)
  1. 该方法继承自以下内容,使其具体方法对所有例程可用:
interface class java.lang.Object
interface org.apache.spark.mllib.linalg. Vector
  1. 有几个感兴趣的方法调用:

  2. 制作向量的深拷贝:

DenseVector copy()
    1. 转换为SparseVector。如果您的向量很长,并且在进行了一定数量的操作后密度减小(例如,将不贡献的成员归零),则会执行此操作:
SparseVector toSparse()
    1. 查找非零元素的数量。如果密度 ID 较低,则这很有用,因此您可以即时转换为 SparseVector:
Int numNonzeros()
    1. 将向量转换为数组。在处理需要与 RDD 或使用 Spark ML 作为子系统的专有算法进行紧密交互的分布式操作时,这通常是必要的:
Double[] toArray()

还有更多...

必须小心,不要将Breeze库提供的向量功能与 Spark ML 向量混合使用。要使用 ML 库算法,您需要使用其本机数据结构,但您始终可以从 ML 向量转换为Breeze,进行所有数学运算,然后在使用 ML 库算法(例如 ALS 或 SVD)时转换为 Spark 所需的数据结构。

我们需要向量和矩阵导入语句,这样我们才能使用 ML 库本身,否则 Scala 向量和矩阵将默认使用。当程序无法在集群上扩展时,这是导致许多混乱的根源。

以下图示了一个图解视图,这应该有助于澄清主题:

另请参阅

创建 SparseVector 并使用 Spark 进行设置

在这个示例中,我们检查了几种SparseVector的创建类型。当向量的长度增加(百万级)且密度保持较低(非零成员较少)时,稀疏表示变得越来越有利于DenseVector

如何做...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 导入向量和矩阵操作所需的必要包:

import org.apache.spark.sql.{SparkSession}
import org.apache.spark.mllib.linalg._
import breeze.linalg.{DenseVector => BreezeVector}
import Array._
import org.apache.spark.mllib.linalg.SparseVector
  1. 设置 Spark 上下文和应用程序参数,以便 Spark 可以运行。有关更多详细信息和变体,请参见本章的第一个示例:
val spark = SparkSession
 .builder
 .master("local[*]")
 .appName("myVectorMatrix")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()
  1. 在这里,我们看一下创建与其等效的 DenseVector 相对应的 ML SparseVector。调用包括三个参数:向量的大小,非零数据的索引,最后是数据本身。

在下面的示例中,我们可以比较密集与 SparseVector 的创建。如您所见,四个非零元素(5、3、8、9)对应于位置(0、2、18、19),而数字 20 表示总大小:

val denseVec1 = Vectors.dense(5,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,9)
val sparseVec1 = Vectors.sparse(20, Array(0,2,18,19), Array(5, 3, 8,9))
  1. 为了更好地理解数据结构,我们比较输出和一些重要属性,这些属性对我们有所帮助,特别是在使用向量进行动态编程时。

首先,我们看一下 DenseVector 的打印输出,以查看其表示:


println(denseVec1.size)
println(denseVec1.numActives)
println(denseVec1.numNonzeros)
println("denceVec1 presentation = ",denseVec1)

输出如下:

denseVec1.size = 20

denseVec1.numActives = 20

denseVec1.numNonzeros = 4

(denseVec1 presentation = ,[5.0,0.0,3.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,8.0,9.0])

  1. 接下来,我们看一下 SparseVector 的打印输出,以查看其内部表示:
println(sparseVec1.size)
println(sparseVec1.numActives)
println(sparseVec1.numNonzeros)
println("sparseVec1 presentation = ",sparseVec1)

如果我们比较内部表示和元素数量与活跃和非零元素的对比,您会发现 SparseVector 只存储非零元素和索引以减少存储需求。

输出如下:

denseVec1.size = 20
println(sparseVec1.numActives)= 4
sparseVec1.numNonzeros = 4
 (sparseVec1 presentation = ,(20,[0,2,18,19],[5.0,3.0,8.0,9.0]))
  1. 我们可以根据需要在稀疏向量和密集向量之间进行转换。您可能想这样做的原因是,外部数学和线性代数不符合 Spark 的内部表示。我们明确指定了变量类型以阐明观点,但在实际操作中可以消除该额外声明:
val ConvertedDenseVect : DenseVector= sparseVec1.toDense
 val ConvertedSparseVect : SparseVector= denseVec1.toSparse
println("ConvertedDenseVect =", ConvertedDenseVect)
 println("ConvertedSparseVect =", ConvertedSparseVect)

输出如下:

(ConvertedDenseVect =,[5.0,0.0,3.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,8.0,9.0])
(ConvertedSparseVect =,(20,[0,2,18,19],[5.0,3.0,8.0,9.0])) 

工作原理...

  1. 此方法构造函数的签名为:
SparseVector(int size, int[] indices, double[] values)

该方法继承自以下内容,使其具体方法对所有例程可用:

interface class java.lang.Object  

有几个与向量相关的方法调用是有趣的:

    1. 对向量进行深拷贝:
SparseVector Copy() 
    1. 转换为SparseVector。如果您的向量很长,并且密度在多次操作后减少(例如,将不贡献的成员归零),则会执行此操作:
DenseVector toDense()
    1. 查找非零元素的数量。这很有用,因此您可以在需要时将其转换为稀疏向量,如果密度 ID 较低。
Int numNonzeros()
    1. 将向量转换为数组。在处理需要与 RDD 或使用 Spark ML 作为子系统的专有算法进行 1:1 交互的分布式操作时,通常需要这样做:
Double[] toArray() 

还有更多...

  1. 必须记住,密集向量和稀疏向量是本地向量,不得与分布式设施混淆(例如,分布式矩阵,如 RowMatrix 类)。

  2. 本地机器上向量的基本数学运算将由两个库提供:

还有一个与向量直接相关的数据结构,称为 LabeledPoint,我们在第四章中介绍过,实现强大的机器学习系统的常见配方。简而言之,它是一种数据结构,用于存储 ML 数据,包括特征向量和标签(例如,回归中的自变量和因变量)。

另请参阅

创建密集矩阵并使用 Spark 2.0 进行设置

在这个配方中,我们探讨了您在 Scala 编程中可能需要的矩阵创建示例,以及在阅读许多用于机器学习的开源库的源代码时可能需要的示例。

Spark 提供了两种不同类型的本地矩阵设施(密集和稀疏),用于在本地级别存储和操作数据。简单来说,将矩阵视为向量的列是一种思考方式。

准备工作

在这里要记住的关键是,该配方涵盖了存储在一台机器上的本地矩阵。我们将在本章中介绍的另一个配方Spark2.0 ML 库中的分布式矩阵,用于存储和操作分布式矩阵。

如何做...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 导入向量和矩阵操作所需的包:

 import org.apache.spark.sql.{SparkSession}
 import org.apache.spark.mllib.linalg._
 import breeze.linalg.{DenseVector => BreezeVector}
 import Array._
 import org.apache.spark.mllib.linalg.SparseVector
  1. 设置 Spark 会话和应用程序参数,以便 Spark 可以运行:
val spark = SparkSession
 .builder
 .master("local[*]")
 .appName("myVectorMatrix")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()
  1. 在这里,我们将从 Scala 数组创建 ML 向量特征。让我们定义一个 2x2 的密集矩阵,并用数组实例化它:
val MyArray1= Array(10.0, 11.0, 20.0, 30.3)
val denseMat3 = Matrices.dense(2,2,MyArray1)   

输出如下:

DenseMat3=
10.0  20.0  
11.0  30.3 

通过初始化一步构建密集矩阵并分配值:

通过内联定义数组直接构造密集本地矩阵。这是一个 3x3 的数组,有九个成员。您可以将其视为三列三个向量(3x3):

val denseMat1 = Matrices.dense(3,3,Array(23.0, 11.0, 17.0, 34.3, 33.0, 24.5, 21.3,22.6,22.2))

输出如下:

    denseMat1=
    23.0  34.3  21.3  
    11.0  33.0  22.6  
    17.0  24.5  22.2

这是另一个示例,展示了使用向量内联实例化密集本地矩阵。这是一个常见情况,您将向量收集到矩阵(列顺序)中,然后对整个集合执行操作。最常见的情况是收集向量,然后使用分布式矩阵执行分布式并行操作。

在 Scala 中,我们使用++运算符与数组实现连接:

val v1 = Vectors.dense(5,6,2,5)
 val v2 = Vectors.dense(8,7,6,7)
 val v3 = Vectors.dense(3,6,9,1)
 val v4 = Vectors.dense(7,4,9,2)

 val Mat11 = Matrices.dense(4,4,v1.toArray ++ v2.toArray ++ v3.toArray ++ v4.toArray)
 println("Mat11=\n", Mat11)

输出如下:

    Mat11=
    5.0  8.0  3.0  7.0  
   6.0  7.0  6.0  4.0  
   2.0  6.0  9.0  9.0  
   5.0  7.0  1.0  2.0

它是如何工作的...

  1. 此方法构造函数的签名为(按列主要密集矩阵):
DenseMatrix(int numRows, int numCols, double[] values)
DenseMatrix(int numRows, int numCols, double[] values, boolean isTransposed)
  1. 该方法继承自以下内容,使其具体方法对所有例程可用:
  • 接口类 java.lang.Object

  • java.io.Serializable

  • 矩阵

  1. 有几个感兴趣的方法调用:

  2. 从向量中提供的值生成对角矩阵:

static DenseMatrix(Vector vector) 
    1. 创建一个单位矩阵。单位矩阵是对角线为 1,其他元素为 0 的矩阵:
static eye(int n) 
    1. 跟踪矩阵是否被转置:
boolean isTransposed()
    1. 创建一个包含一组随机数的矩阵-从均匀分布中抽取:
static DenseMatrix rand(int numRows, int numCols, java.util.Random rng) 
    1. 创建一个包含一组随机数的矩阵-从高斯分布中抽取:
static DenseMatrix randn(int numRows, int numCols, java.util.Random rng) 
    1. 转置矩阵:
DenseMatrix transpose() 
    1. 对向量进行深拷贝:
DenseVector Copy() 
    1. 转换为稀疏向量。如果您的向量很长,并且密度在一系列操作后减少(例如,将不贡献的成员归零),则会执行此操作:
SparseVector toSparse() 
    1. 查找非零元素的数量。这很有用,因此您可以根据需要将其转换为稀疏向量,如果密度 ID 较低:
Int numNonzeros()
    1. 获取矩阵中存储的所有值:
Double[] Values()

还有更多...

在 Spark 中处理矩阵最困难的部分是习惯于列顺序与行顺序。要记住的关键是,Spark ML 使用的底层库更适合使用列存储机制。以下是一个示例:

  1. 给定定义 2x2 矩阵的矩阵:
val denseMat3 = Matrices.dense(2,2, Array(10.0, 11.0, 20.0, 30.3))
  1. 矩阵实际上存储为:
10.0  20.0 
11.0 30.3

您从值集合的左侧移动到右侧,然后从列到列进行矩阵的放置。

  1. 如您所见,矩阵按行存储的假设与 Spark 方法不一致。从 Spark 的角度来看,以下顺序是不正确的:
 10.0  11.0 
 20.0 30.3

另请参阅

使用 Spark 2.0 的稀疏本地矩阵

在这个示例中,我们专注于稀疏矩阵的创建。在上一个示例中,我们看到了如何声明和存储本地密集矩阵。许多机器学习问题领域可以表示为矩阵中的一组特征和标签。在大规模机器学习问题中(例如,疾病在大型人口中的传播,安全欺诈,政治运动建模等),许多单元格将为 0 或 null(例如,患有某种疾病的人数与健康人口的当前数量)。

为了帮助存储和实时操作的高效性,稀疏本地矩阵专门用于以列表加索引的方式高效存储单元格,从而实现更快的加载和实时操作。

如何做到...

  1. 在 IntelliJ 或您选择的 IDE 中启动新项目。确保包含必要的 JAR 文件。

  2. 导入向量和矩阵操作所需的包:

 import org.apache.spark.mllib.linalg.distributed.RowMatrix
 import org.apache.spark.mllib.linalg.distributed.{IndexedRow, IndexedRowMatrix}
 import org.apache.spark.mllib.linalg.distributed.{CoordinateMatrix, MatrixEntry}
 import org.apache.spark.sql.{SparkSession}
 import org.apache.spark.mllib.linalg._
 import breeze.linalg.{DenseVector => BreezeVector}
 import Array._
 import org.apache.spark.mllib.linalg.DenseMatrix
 import org.apache.spark.mllib.linalg.SparseVector
  1. 设置 Spark 上下文和应用程序参数,以便 Spark 可以运行-有关更多详细信息和变体,请参见本章的第一个配方:
val spark = SparkSession
 .builder
 .master("local[*]")
 .appName("myVectorMatrix")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()
  1. 由于我们将稀疏表示存储为压缩列存储(CCS),因此稀疏矩阵的创建要复杂一些,也称为 Harwell-Boeing 稀疏矩阵格式。 请参见*它是如何工作...*以获取详细说明。

我们声明并创建一个本地的 3x2 稀疏矩阵,只有三个非零成员:

 val sparseMat1= Matrices.sparse(3,2 ,Array(0,1,3), Array(0,1,2), Array(11,22,33))

让我们检查输出,以便我们充分理解在较低级别发生的情况。 这三个值将被放置在(0,0),(1,1),(2,1):

 println("Number of Columns=",sparseMat1.numCols)
 println("Number of Rows=",sparseMat1.numRows)
 println("Number of Active elements=",sparseMat1.numActives)
 println("Number of Non Zero elements=",sparseMat1.numNonzeros)
 println("sparseMat1 representation of a sparse matrix and its value=\n",sparseMat1)

输出如下:

(Number of Columns=,2)
(Number of Rows=,3)
(Number of Active elements=,3)
(Number of Non Zero elements=,3)
sparseMat1 representation of a sparse matrix and its value= 3 x 2 CSCMatrix
(0,0) 11.0
(1,1) 22.0
(2,1) 33.0)

进一步澄清,这是稀疏矩阵的代码,该矩阵在 Spark 的文档页面上有所说明(请参阅以下标题为另请参阅的部分)。 这是一个 3x3 矩阵,有六个非零值。 请注意,声明的顺序是:矩阵大小,列指针,行索引,值作为最后一个成员:

/* from documentation page
 1.0 0.0 4.0
 0.0 3.0 5.0
 2.0 0.0 6.0
 *
 */
 //[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], rowIndices=[0, 2, 1, 0, 1, 2], colPointers=[0, 2, 3, 6]
 val sparseMat33= Matrices.sparse(3,3 ,Array(0, 2, 3, 6) ,Array(0, 2, 1, 0, 1, 2),Array(1.0, 2.0, 3.0, 4.0, 5.0, 6.0))
 println(sparseMat33)

输出如下:

3 x 3 CSCMatrix
(0,0) 1.0
(2,0) 2.0
(1,1) 3.0
(0,2) 4.0
(1,2) 5.0
(2,2) 6.0
  • 列指针= [0,2,3,6]

  • 行索引= [0,2,1,0,1,2]

  • 非零值= [1.0,2.0,3.0,4.0,5.0,6.0]

它是如何工作的...

根据我们的经验,大多数稀疏矩阵的困难来自于对压缩行存储CRS)和压缩列存储CCS)之间差异的理解不足。 我们强烈建议读者深入研究这个主题,以清楚地理解这些差异。

简而言之,Spark 使用 CCS 格式来处理转置目标矩阵:

  1. 此方法调用构造函数有两个不同的签名:

    • SparseMatrix(int numRows,int numCols,int[] colPtrs,int[] rowIndices,double[] values)
  • 稀疏矩阵(int numRows,int numCols,int[] colPtrs,int[] rowIndices,double[] values,boolean isTransposed)

在第二个选项中,我们指示矩阵已经声明为转置,因此将以不同方式处理矩阵。

  1. 该方法继承自以下内容,使它们的具体方法对所有例程可用:
  • 接口类 java.lang.Object

  • java.io.Serializable

  • 矩阵

  1. 有几个感兴趣的方法调用:
  • 从向量中提供的值生成对角矩阵:
static SparseMatrix spdiag(Vector vector)
    • 创建一个单位矩阵。 单位矩阵是一个对角线为 1,其他任何元素为 0 的矩阵:
static speye(int n)
    • 跟踪矩阵是否转置:
boolean isTransposed()
    • 创建一个具有一组随机数的矩阵-从均匀分布中抽取:
static SparseMatrix sprand(int numRows, int numCols, java.util.Random rng)
    • 创建一个具有一组随机数的矩阵-从高斯分布中抽取:
static SparseMatrix sprandn(int numRows, int numCols, java.util.Random rng)
    • 转置矩阵:
SparseMatrix transpose()
    • 对向量进行深层复制
SparseMatrix Copy()
    • 转换为稀疏向量。 如果您的向量很长,并且在一些操作后密度减小(例如,将不贡献的成员归零),则会执行此操作:
DenseMatrix toDense()
    • 查找非零元素的数量。 这很有用,因此您可以根据需要将其转换为稀疏向量(例如,如果密度 ID 较低):
Int numNonzeros()
    • 获取矩阵中存储的所有值:
Double[] Values()
    • 还有其他对应于稀疏矩阵特定操作的调用。 以下是一个示例,但我们强烈建议您熟悉手册页面(请参阅*还有更多...*部分):
      1. 获取行索引:int rowIndices()
  1. 检查是否转置:booleanisTransposed()

  2. 获取列指针:int[]colPtrs()

还有更多...

再次强调,在许多机器学习应用中,由于特征空间的大尺寸特性不是线性分布的,最终会处理稀疏性。 举例来说,我们以最简单的情况为例,有 10 个客户指示他们对产品线中四个主题的亲和力:

主题 1 主题 2 主题 3 主题 4
Cust 1 1 0 0 0
Cust 2 0 0 0 1
Cust 3 0 0 0 0
Cust 4 0 1 0 0
Cust 5 1 1 1 0
Cust 6 0 0 0 0
Cust 7 0 0 1 0
Cust 8 0 0 0 0
客户 9 1 0 1 1
客户 10 0 0 0 0

正如你所看到的,大部分元素都是 0,当我们增加客户和主题的数量到数千万(M x N)时,将它们存储为密集矩阵是不可取的。SparseVector 和矩阵有助于以高效的方式存储和操作这些稀疏结构。

另请参阅

使用 Spark 2.0 进行向量运算

在本教程中,我们使用Breeze库进行底层操作,探索了在 Spark 环境中进行向量加法的方法。向量允许我们收集特征,然后通过线性代数运算(如加法、减法、转置、点积等)对其进行操作。

操作步骤...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 导入向量和矩阵操作所需的包:

 import org.apache.spark.mllib.linalg.distributed.RowMatrix
 import org.apache.spark.mllib.linalg.distributed.{IndexedRow, IndexedRowMatrix}
 import org.apache.spark.mllib.linalg.distributed.{CoordinateMatrix, MatrixEntry}
 import org.apache.spark.sql.{SparkSession}
 import org.apache.spark.mllib.linalg._
 import breeze.linalg.{DenseVector => BreezeVector}
 import Array._
 import org.apache.spark.mllib.linalg.DenseMatrix
 import org.apache.spark.mllib.linalg.SparseVector
  1. 设置 Spark 会话和应用程序参数,以便 Spark 可以运行:
val spark = SparkSession
 .builder
 .master("local[*]")
 .appName("myVectorMatrix")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()
  1. 我们创建向量:
val w1 = Vectors.dense(1,2,3)
val w2 = Vectors.dense(4,-5,6)
  1. 我们将向量从 Spark 公共接口转换为Breeze(库)工件,以便使用丰富的向量操作符:
val w1 = Vectors.dense(1,2,3)
val w2 = Vectors.dense(4,-5,6) 
val w3 = new BreezeVector(w1.toArray)//w1.asBreeze
val w4=  new BreezeVector(w2.toArray)// w2.asBreeze
println("w3 + w4 =",w3+w4)
println("w3 - w4 =",w3+w4)
println("w3 * w4 =",w3.dot(w4)) 
  1. 让我们看一下输出并理解结果。要了解向量加法、减法和乘法的操作原理,请参阅本教程中的*How it works...*部分。

输出如下:

w3 + w4 = DenseVector(5.0, -3.0, 9.0)
w3 - w4 = DenseVector(5.0, -3.0, 9.0)
w3 * w4 =12.0 
  1. 使用 Breeze 库转换的稀疏和密集向量的向量操作包括:
val sv1 = Vectors.sparse(10, Array(0,2,9), Array(5, 3, 13))
val sv2 = Vectors.dense(1,0,1,1,0,0,1,0,0,13)
println("sv1 - Sparse Vector = ",sv1)
println("sv2 - Dense Vector = ",sv2)
println("sv1 * sv2 =", new BreezeVector(sv1.toArray).dot(new BreezeVector(sv2.toArray)))

这是一种替代方法,但它的缺点是使用了一个私有函数(请参阅 Spark 2.x.x 的实际源代码)。我们建议使用之前介绍的方法:

println("sv1  * sve2  =", sv1.asBreeze.dot(sv2.asBreeze))

我们来看一下输出:

sv1 - Sparse Vector =  (10,[0,2,9],[5.0,3.0,13.0]) 
sv2 - Dense  Vector = [1.0,0.0,1.0,1.0,0.0,0.0,1.0,0.0,0.0,13.0] 
sv1 * sv2 = 177.0

操作原理...

向量是数学工件,允许我们表达大小和方向。在机器学习中,我们将对象/用户的偏好收集到向量和矩阵中,以便利用分布式操作规模化。

向量通常是一些属性的元组,通常用于机器学习算法。这些向量通常是实数(测量值),但很多时候我们使用二进制值来表示对特定主题的偏好或偏见的存在或不存在。

向量可以被看作是行向量或列向量。列向量的表示更适合于机器学习思维。列向量表示如下:

行向量表示如下:

向量加法表示如下:

向量减法表示如下:

向量乘法或“点”积表示如下:

还有更多...

Spark ML 和 MLlib 库提供的公共接口,无论是用于稀疏向量还是密集向量,目前都缺少进行完整向量运算所需的运算符。我们必须将我们的本地向量转换为Breeze库向量,以便使用线性代数运算符。

在 Spark 2.0 之前,转换为BreezetoBreeze)的方法是可用的,但现在该方法已更改为asBreeze()并且变为私有!需要快速阅读源代码以了解新的范式。也许这种变化反映了 Spark 核心开发人员希望减少对底层Breeze库的依赖。

如果你使用的是 Spark 2.0 之前的任何版本(Spark 1.5.1 或 1.6.1),请使用以下代码片段进行转换。

Spark 2.0 之前的示例 1:

val w1 = Vectors.dense(1,2,3)
 val w2 = Vectors.dense(4,-5,6)
 val w3 = w1.toBreeze
 val w4= w2.toBreeze
 println("w3 + w4 =",w3+w4)
 println("w3 - w4 =",w3+w4)
 println("w3 * w4 =",w3.dot(w4))

Spark 2.0 之前的示例 2:

println("sv1 - Sparse Vector = ",sv1)
 println("sv2 - Dense Vector = ",sv2)
 println("sv1 * sv2 =", sv1.toBreeze.dot(sv2.toBreeze))

另请参阅

使用 Spark 2.0 执行矩阵运算

在这个示例中,我们探讨了 Spark 中的矩阵操作,如加法、转置和乘法。更复杂的操作,如逆、SVD 等,将在未来的章节中介绍。Spark ML 库的本机稀疏和密集矩阵提供了乘法运算符,因此不需要显式转换为Breeze

矩阵是分布式计算的工作马。收集的 ML 特征可以以矩阵配置的形式进行排列,并在规模上进行操作。许多 ML 方法,如ALS交替最小二乘法)和SVD奇异值分解),依赖于高效的矩阵和向量操作来实现大规模机器学习和训练。

如何做...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 导入必要的包进行向量和矩阵操作:

 import org.apache.spark.mllib.linalg.distributed.RowMatrix
 import org.apache.spark.mllib.linalg.distributed.{IndexedRow, IndexedRowMatrix}
 import org.apache.spark.mllib.linalg.distributed.{CoordinateMatrix, MatrixEntry}
 import org.apache.spark.sql.{SparkSession}
 import org.apache.spark.mllib.linalg._
 import breeze.linalg.{DenseVector => BreezeVector}
 import Array._
 import org.apache.spark.mllib.linalg.DenseMatrix
 import org.apache.spark.mllib.linalg.SparseVector
  1. 设置 Spark 会话和应用程序参数,以便 Spark 可以运行:
val spark = SparkSession
 .builder
 .master("local[*]")
 .appName("myVectorMatrix")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()
  1. 我们创建了矩阵:
val sparseMat33= Matrices.sparse(3,3 ,Array(0, 2, 3, 6) ,Array(0, 2, 1, 0, 1, 2),Array(1.0, 2.0, 3.0, 4.0, 5.0, 6.0))
val denseFeatureVector= Vectors.dense(1,2,1)
val denseVec13 = Vectors.dense(5,3,0)
  1. 将矩阵和向量相乘并打印结果。这是一个非常有用的操作,在大多数 Spark ML 案例中都是一个常见主题。我们使用SparseMatrix来演示密集、稀疏和矩阵是可互换的,只有密度(例如非零元素的百分比)和性能应该是选择的标准:
val result0 = sparseMat33.multiply(denseFeatureVector)
println("SparseMat33 =", sparseMat33)
 println("denseFeatureVector =", denseFeatureVector)
 println("SparseMat33 * DenseFeatureVector =", result0)

输出如下:

(SparseMat33 =,3 x 3 CSCMatrix
(0,0) 1.0
(2,0) 2.0
(1,1) 3.0
(0,2) 4.0
(1,2) 5.0
(2,2) 6.0)
denseFeatureVector =,[1.0,2.0,1.0]
SparseMat33 * DenseFeatureVector = [5.0,11.0,8.0]
  1. DenseMatrixDenseVector相乘。

这是为了完整性考虑,将帮助用户更轻松地跟随矩阵和向量乘法,而不用担心稀疏性:

println("denseVec2 =", denseVec13)
println("denseMat1 =", denseMat1)
val result3= denseMat1.multiply(denseVec13)
println("denseMat1 * denseVect13 =", result3) 

输出如下:

    denseVec2 =,[5.0,3.0,0.0]
    denseMat1 =  23.0  34.3  21.3  
                          11.0  33.0  22.6  
                          17.0  24.5  22.2 
    denseMat1 * denseVect13 =,[217.89,154.0,158.5]

  1. 我们演示了矩阵的转置,这是一种交换行和列的操作。如果你参与 Spark ML 或数据工程,这是一个重要的操作,几乎每天都会用到。

在这里我们演示了两个步骤:

    1. 将稀疏矩阵转置并通过输出检查新的结果矩阵:
val transposedMat1= sparseMat1.transpose
 println("transposedMat1=\n",transposedMat1)

输出如下:


Original sparseMat1 =,3 x 2 CSCMatrix
(0,0) 11.0
(1,1) 22.0
(2,1) 33.0)

(transposedMat1=,2 x 3 CSCMatrix
(0,0) 11.0
(1,1) 22.0
(1,2) 33.0)

1.0  4.0  7.0  
2.0  5.0  8.0  
3.0  6.0  9.0

    1. 演示转置的转置产生原始矩阵:
val transposedMat1= sparseMat1.transpose
println("transposedMat1=\n",transposedMat1)         println("Transposed twice", denseMat33.transpose.transpose) // we get the original back

输出如下:

Matrix transposed twice=
1.0  4.0  7.0  
2.0  5.0  8.0  
3.0  6.0  9.0

转置密集矩阵并通过输出检查新的结果矩阵:

这样更容易看到行和列索引是如何交换的:

val transposedMat2= denseMat1.transpose
 println("Original sparseMat1 =", denseMat1)
 println("transposedMat2=" ,transposedMat2)
Original sparseMat1 =
23.0  34.3  21.3  
11.0  33.0  22.6  
17.0  24.5  22.2 
transposedMat2=
23.0  11.0  17.0  
34.3  33.0  24.5  
21.3  22.6  22.2   
    1. 现在我们来看矩阵乘法以及在代码中的表现。

我们声明了两个 2x2 的密集矩阵:

// Matrix multiplication
 val dMat1: DenseMatrix= new DenseMatrix(2, 2, Array(1.0, 3.0, 2.0, 4.0))
 val dMat2: DenseMatrix = new DenseMatrix(2, 2, Array(2.0,1.0,0.0,2.0))

 println("dMat1 * dMat2 =", dMat1.multiply(dMat2)) //A x B
 println("dMat2 * dMat1 =", dMat2.multiply(dMat1)) //B x A   not the same as A xB

输出如下:

dMat1 =,1.0  2.0  
               3.0  4.0  
dMat2 =,2.0  0.0  
       1.0 2.0 
dMat1 * dMat2 =,4.0   4.0  
                              10.0  8.0
//Note: A x B is not the same as B x A
dMat2 * dMat1 = 2.0   4.0   
                               7.0  10.0

它是如何工作的...

矩阵可以被看作是向量的列。矩阵是涉及线性代数变换的分布式计算的强大工具。可以通过矩阵收集和操作各种属性或特征表示。

简而言之,矩阵是二维m x n数组,其中元素可以使用两个元素的下标ij来引用(通常是实数):

矩阵表示如下:

矩阵转置表示如下:

矩阵乘法表示如下:

向量矩阵乘法或“点”积表示如下:

Spark 2.0 ML 库中的分布式矩阵:在接下来的四个配方中,我们将介绍 Spark 中的四种分布式矩阵类型。Spark 提供了对由 RDD 支持的分布式矩阵的全面支持。Spark 支持分布式计算的事实并不意味着开发人员可以不考虑并行性来规划他们的算法。

底层的 RDD 提供了存储在矩阵中的数据的全面并行性和容错性。Spark 捆绑了 MLLIB 和 LINALG,它们共同提供了一个公共接口,并支持那些由于其大小或链式操作的复杂性而需要完整集群支持的矩阵。

Spark ML 提供了四种类型的分布式矩阵来支持并行性:RowMatrixIndexedRowMatrixCoordinateMatrixBlockMatrix

  • RowMatrix:表示与 ML 库兼容的面向行的分布式矩阵

  • IndexedRowMatrix:与RowMatrix类似,但具有一个额外的好处,即对行进行索引。这是RowMatrix的一个专门版本,其中矩阵本身是从IndexedRow(索引,向量)数据结构的 RDD 创建的。要可视化它,想象一个矩阵,其中每一行都是一对(长,RDD),并且已经为您完成了它们的配对(zip函数)。这将允许您在给定算法的计算路径中将索引与 RDD 一起携带(规模上的矩阵运算)。

  • CoordinateMatrix:用于坐标的非常有用的格式(例如,在投影空间中的xyz坐标)

  • BlockMatrix:由本地维护的矩阵块组成的分布式矩阵

我们将简要介绍这四种类型的创建,然后迅速转向一个更复杂(代码和概念)的用例,涉及RowMatrix,这是一个典型的 ML 用例,涉及大规模并行分布式矩阵操作(例如乘法)与本地矩阵。

如果您计划编写或设计大型矩阵操作,您必须深入了解 Spark 内部,例如核心 Spark 以及在每个版本的 Spark 中分期、流水线和洗牌的工作方式(每个版本中的持续改进和优化)。

在着手进行大规模矩阵和优化之旅之前,我们还建议以下几点:

Apache Spark 中矩阵计算和优化的来源可在www.kdd.org/kdd2016/papers/files/adf0163-bosagh-zadehAdoi.pdfpdfs.semanticscholar.org/a684/fc37c79a3276af12a21c1af1ebd8d47f2d6a.pdf找到。

使用 Spark 进行高效大规模分布式矩阵计算的来源可在www.computer.org/csdl/proceedings/big-data/2015/9926/00/07364023.pdfdl.acm.org/citation.cfm?id=2878336&preflayout=flat找到。

探索矩阵依赖关系以实现高效的分布式矩阵计算的来源可在net.pku.edu.cn/~cuibin/Papers/2015-SIGMOD-DMac.pdfdl.acm.org/citation.cfm?id=2723712找到。

探索 Spark 2.0 中的 RowMatrix

在这个配方中,我们探索了 Spark 提供的RowMatrix功能。RowMatrix顾名思义,是一个面向行的矩阵,但缺少可以在RowMatrix的计算生命周期中定义和传递的索引。这些行是 RDD,它们提供了分布式计算和容错性。

矩阵由本地向量的行组成,通过 RDDs 并行化和分布。简而言之,每行将是一个 RDD,但列的总数将受到本地向量最大大小的限制。在大多数情况下,这不是问题,但出于完整性考虑,我们觉得应该提一下。

如何做...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 导入向量和矩阵操作所需的包:

 import org.apache.spark.mllib.linalg.distributed.RowMatrix
 import org.apache.spark.mllib.linalg.distributed.{IndexedRow, IndexedRowMatrix}
 import org.apache.spark.mllib.linalg.distributed.{CoordinateMatrix, MatrixEntry}
 import org.apache.spark.sql.{SparkSession}
 import org.apache.spark.mllib.linalg._
 import breeze.linalg.{DenseVector => BreezeVector}
 import Array._
 import org.apache.spark.mllib.linalg.DenseMatrix
 import org.apache.spark.mllib.linalg.SparseVector
  1. 设置 Spark 上下文和应用程序参数,以便 Spark 可以运行。有关更多详细信息和变体,请参阅本章的第一个配方:
val spark = SparkSession
 .builder
 .master("local[*]")
 .appName("myVectorMatrix")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()
  1. 由于分布式计算(非顺序)的性质,警告语句的数量和时间返回的输出会有所不同。消息与实际输出的交错程度取决于执行路径,这导致输出难以阅读。在以下语句中,我们将log4j消息从警告(WARN - 默认)提升到错误(ERROR)以便更清晰。我们建议开发人员详细跟踪警告消息,以了解这些操作的并行性质并充分理解 RDD 的概念:
import Log4J logger and the level
import org.apache.log4j.Logger
 import org.apache.log4j.Level

将级别设置为错误:

Logger.getLogger("org").setLevel(Level.ERROR)
Logger.getLogger("akka").setLevel(Level.ERROR)

最初的输出如下

Logger.getLogger("org").setLevel(Level.WARN)
Logger.getLogger("akka").setLevel(Level.WARN)
  1. 我们定义了两个密集向量的序列数据结构。

Scala 密集本地向量的序列,这将是分布式RowMatrix的数据:

val dataVectors = Seq(
   Vectors.dense(0.0, 1.0, 0.0),
   Vectors.dense(3.0, 1.0, 5.0),
   Vectors.dense(0.0, 7.0, 0.0)
 )

Scala 密集本地向量的序列,这将是本地身份矩阵的数据。线性代数的快速检查表明,任何矩阵乘以一个单位矩阵将产生相同的原始矩阵(即,A x I = A)。我们喜欢使用单位矩阵来证明乘法有效,并且原始矩阵上计算的原始统计量与原始* x *单位矩阵相同:

val identityVectors = Seq(
   Vectors.dense(1.0, 0.0, 0.0),
   Vectors.dense(0.0, 1.0, 0.0),
   Vectors.dense(0.0, 0.0, 1.0)
 )
  1. 通过并行化基础密集向量到 RDDs,创建我们的第一个分布式矩阵。

从现在开始,我们的密集向量现在是由 RDD 支持的新分布式向量中的行(即,所有 RDD 操作都得到充分支持!)。

将原始序列(由向量组成)转换为 RDDs。我们将在下一章详细介绍 RDDs。在这个单一语句中,我们已经将一个本地数据结构转换为了一个分布式工件:

val distMat33 = new RowMatrix(sc.parallelize(dataVectors))

我们计算一些基本统计量,以验证RowMatrix是否正确构建。要记住的是,密集向量现在是行而不是列(这是导致许多混淆的根源):

println("distMatt33 columns - Count =", distMat33.computeColumnSummaryStatistics().count)
 println("distMatt33 columns - Mean =", distMat33.computeColumnSummaryStatistics().mean)
 println("distMatt33 columns - Variance =", distMat33.computeColumnSummaryStatistics().variance)
 println("distMatt33 columns - CoVariance =", distMat33.computeCovariance())

输出如下:

计算的统计量(均值、方差、最小值、最大值等)是针对每列而不是整个矩阵的。这就是为什么您看到均值和方差对应于每列的三个数字的原因。

    distMatt33 columns - Count =            3
    distMatt33 columns - Mean =             [ 1.0, 3.0, 1.66 ]
    (distMatt33 columns - Variance =      [ 3.0,12.0,8.33 ]
    (distMatt33 columns - CoVariance = 3.0   -3.0  5.0                
                                                            -3.0  12.0  -5.0               
                                                             5.0   -5.0  8.33  
  1. 在这一步中,我们从身份向量的数据结构中创建我们的本地矩阵。要记住的一点是,乘法需要一个本地矩阵而不是分布式矩阵。请查看调用签名以进行验证。我们使用maptoArrayflatten操作符来创建一个 Scala 扁平化数组数据结构,该数据结构可以作为创建本地矩阵的参数之一,如下一步所示:
val flatArray = identityVectors.map(x => x.toArray).flatten.toArray 
 dd.foreach(println(_))
  1. 我们将本地矩阵创建为单位矩阵,以便验证乘法A * I = A
 val dmIdentity: Matrix = Matrices.dense(3, 3, flatArray)
  1. 我们将分布式矩阵乘以本地矩阵,并创建一个新的分布式矩阵。这是一个典型的用例,您最终会将一个高瘦的本地矩阵与大规模分布式矩阵相乘,以实现规模和结果矩阵的继承降维:
val distMat44 = distMat33.multiply(dmIdentity)
 println("distMatt44 columns - Count =", distMat44.computeColumnSummaryStatistics().count)
 println("distMatt44 columns - Mean =", distMat44.computeColumnSummaryStatistics().mean)
 println("distMatt44 columns - Variance =", distMat44.computeColumnSummaryStatistics().variance)
 println("distMatt44 columns - CoVariance =", distMat44.computeCovariance())
  1. 比较第 7 步和第 8 步,我们实际上看到操作进行正确,并且我们可以通过描述性统计和协方差矩阵验证A x I = A,使用分布式和本地矩阵。

输出如下:

distMatt44 columns - Count = 3
distMatt44 columns - Mean = [ 1.0, 3.0, 1.66 ]
distMatt44 columns - Variance = [ 3.0,12.0,8.33 ]
distMatt44 columns - CoVariance = 3.0 -3.0 5.0 
 -3.0 12.0 -5.0 
 5.0 -5.0 8.33

工作原理...

  1. 此方法构造函数的签名为:
  • RowMatrix(RDD<Vector> rows)

  • RowMatrix(RDD<Vector>, long nRows, Int nCols)

  1. 该方法继承自以下内容,使它们的具体方法对所有例程可用:
  • 接口类 java.lang.Object

  • 实现以下接口:

  • 记录

  • 分布式矩阵

  1. 有一些有趣的方法调用:
  • 计算描述性统计,如均值、最小值、最大值、方差等:
      • MultivariateStatisticalSummary
  • computeColumnSummaryStatistics()

  • 从原始计算协方差矩阵:

  • Matrix computeCovariance()

  • 计算 Gramian 矩阵,也称为 Gram 矩阵

(A^TA ):

  • Matrix computeGramianMatrix()

  • 计算 PCA 组件:

  • Matrix computePrincipalComponents(int k)

k是主成分的数量

    • 计算原始矩阵的 SVD 分解:
  • SingularValueDecomposition<RowMatrix, Matrix> computeSVD(int k, boolean compute, double rCond)

k是要保留的前导奇异值的数量(0<k<=n)。

    • 相乘:
      • RowMatrix Multiply(Matrix B)
  • 行:

  • RDD<Vector> rows()

  • 计算 QR 分解:

  • QRDecomposition<RowMatrix, Matrix> tallSkinnyQR(boolean computeQ))

  • 找到非零元素的数量。如果密度较低,这很有用,因此您可以在需要时转换为 SparseVector:

  • Int numNonzeros()

  • 获取矩阵中存储的所有值:

  • Double[] Values()

  • 其他:

  • 计算列之间的相似性(在文档分析中非常有用)。有两种可用的方法,这些方法在第十二章中有介绍,使用 Spark 2.0 ML 库实现文本分析

      • 我们发现对于动态规划很有用的列数和行数

还有更多...

在使用稀疏或密集元素(向量或块矩阵)时,还有一些额外的因素需要考虑。通常情况下,通过本地矩阵进行乘法是更可取的,因为它不需要昂贵的洗牌。

在处理大矩阵时,更喜欢简单和控制,这四种分布式矩阵类型简化了设置和操作。这四种类型各有优缺点,必须根据以下三个标准进行考虑和权衡:

  • 基础数据的稀疏度或密度

  • 在使用这些功能时将进行的洗牌。

  • 处理边缘情况时的网络容量利用率

出于上述原因,尤其是为了减少分布式矩阵操作(例如两个 RowMatrix 相乘)期间所需的洗牌(即网络瓶颈),我们更喜欢使用本地矩阵进行乘法,以显着减少洗牌。虽然这乍看起来有点违反直觉,但在实践中,对于我们遇到的情况来说是可以的。原因是当我们将一个大矩阵与一个向量或高瘦矩阵相乘时,结果矩阵足够小,可以放入内存中。

另一个需要注意的地方是返回的信息(行或本地矩阵)必须足够小,以便可以将其返回给驱动程序。

对于导入,我们需要本地和分布式向量和矩阵导入,以便我们可以使用 ML 库进行工作。否则,默认情况下将使用 Scala 向量和矩阵。

另请参阅

在 Spark 2.0 中探索分布式 IndexedRowMatrix

在这个教程中,我们介绍了IndexRowMatrix,这是本章中我们介绍的第一个专门的分布式矩阵。IndexedRowMatrix的主要优势是索引可以与行(RDD)一起传递,这就是数据本身。

IndexRowMatrix的情况下,我们有一个由开发人员定义的索引,它与给定的行永久配对,对于随机访问非常有用。索引不仅有助于随机访问,而且在执行join()操作时也用于标识行本身。

如何做...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 导入向量和矩阵操作所需的包:

    import org.apache.spark.mllib.linalg.distributed.RowMatrix
      import org.apache.spark.mllib.linalg.distributed.{IndexedRow, IndexedRowMatrix}
      import org.apache.spark.mllib.linalg.distributed.{CoordinateMatrix, MatrixEntry}
      import org.apache.spark.sql.{SparkSession}
      import org.apache.spark.mllib.linalg._
      import breeze.linalg.{DenseVector => BreezeVector}
      import Array._
      import org.apache.spark.mllib.linalg.DenseMatrix
      import org.apache.spark.mllib.linalg.SparseVector
  1. 设置 Spark 上下文和应用程序参数,以便 Spark 可以运行。有关更多详细信息和变体,请参见本章的第一个教程:
val spark = SparkSession
 .builder
 .master("local[*]")
 .appName("myVectorMatrix")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()
  1. 我们从原始数据向量开始,然后继续构造一个适当的数据结构(即 RowIndex)来容纳索引和向量。

  2. 然后我们继续构造IndexedRowMatrix并显示访问。对于那些使用过 LIBSVM 的人来说,这种格式接近标签和向量的工件,但标签现在是索引(即长)。

  3. 从一系列向量作为IndexedRowMatrix的基本数据结构开始:

val dataVectors = Seq(
   Vectors.dense(0.0, 1.0, 0.0),
   Vectors.dense(3.0, 1.0, 5.0),
   Vectors.dense(0.0, 7.0, 0.0)
 )
  1. 从一系列向量作为IndexedRowMatrix的基本数据结构开始:
   val distInxMat1 
 = sc.parallelize( List( IndexedRow( 0L, dataVectors(0)), IndexedRow( 1L, dataVectors(1)), IndexedRow( 1L, dataVectors(2)))) 
println("distinct elements=", distInxMat1.distinct().count()) 

输出如下:

(distinct elements=,3)

工作原理...

索引是一个长数据结构,为IndexedRowMatrix的每一行提供了一个有意义的行索引。实现底层的动力是 RDDs,它们从一开始就在并行环境中提供了分布式弹性数据结构的所有优势。

IndexedRowMatrix的主要优势是索引可以与数据本身(RDD)一起传递。我们可以定义并携带数据(矩阵的实际行)的索引,这在join()操作需要键来选择特定数据行时非常有用。

下图显示了IndexedRowMatrix的图解视图,应有助于澄清主题:

定义可能不清晰,因为您需要重复定义索引和数据来组成原始矩阵。以下代码片段显示了内部列表中(索引,数据)的重复以供参考:

List( IndexedRow( 0L, dataVectors(0)), IndexedRow( 1L, dataVectors(1)), IndexedRow( 1L, dataVectors(2)))

其他操作与前一篇中介绍的IndexRow矩阵类似。

另请参阅

在 Spark 2.0 中探索分布式 CoordinateMatrix

在这个教程中,我们介绍了专门的分布式矩阵的第二种形式。在处理需要处理通常较大的 3D 坐标系(x,y,z)的 ML 实现时,这是非常方便的。这是一种将坐标数据结构打包成分布式矩阵的便捷方式。

如何做...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 导入向量和矩阵操作所需的包:

 import org.apache.spark.mllib.linalg.distributed.RowMatrix
 import org.apache.spark.mllib.linalg.distributed.{IndexedRow, IndexedRowMatrix}
 import org.apache.spark.mllib.linalg.distributed.{CoordinateMatrix, MatrixEntry}
 import org.apache.spark.sql.{SparkSession}
 import org.apache.spark.mllib.linalg._
 import breeze.linalg.{DenseVector => BreezeVector}
 import Array._
 import org.apache.spark.mllib.linalg.DenseMatrix
 import org.apache.spark.mllib.linalg.SparseVector
  1. 设置 Spark 上下文和应用程序参数,以便 Spark 可以运行。有关更多详细信息和变体,请参见本章的第一个教程:
val spark = SparkSession
 .builder
 .master("local[*]")
 .appName("myVectorMatrix")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()
  1. 我们从MatrixEntry的 SEQ 开始,它对应于每个坐标,并将放置在CoordinateMatrix中。请注意,这些条目不再可以是实数(毕竟它们是 x、y、z 坐标):
val CoordinateEntries = Seq(
   MatrixEntry(1, 6, 300),
   MatrixEntry(3, 1, 5),
   MatrixEntry(1, 7, 10)
 )
  1. 我们实例化调用并构造CoordinateMatrix。我们需要额外的步骤来创建 RDD,我们已经在构造函数中使用 Spark 上下文进行了展示(即sc.parallelize):
val distCordMat1 = new CoordinateMatrix( sc.parallelize(CoordinateEntries.toList)) 
  1. 我们打印第一个MatrixEntry以验证矩阵元素。我们将在下一章中讨论 RDD,但请注意,“count()”本身就是一个动作,使用“collect()”将是多余的:
 println("First Row (MatrixEntry) =",distCordMat1.entries.first())

输出如下:

    First Row (MatrixEntry) =,MatrixEntry(1,6,300.0)

它是如何工作的...

  1. CoordinateMatrix是一种专门的矩阵,其中每个条目都是一个坐标系或三个数字的元组(长,长,长对应于xyz坐标)。相关的数据结构是MatrixEntry,其中将存储坐标,然后放置在CoordinateMatrix的位置。以下代码片段演示了MaxEntry的使用,这似乎本身就是一个混乱的来源。

  2. 下图显示了CoordinateMatrix的图示视图,这应该有助于澄清主题:

包含三个坐标的代码片段是:

MatrixEntry(1, 6, 300), MatrixEntry(3, 1, 5), MatrixEntry(1, 7, 10)

MaxEntry只是一个必需的结构,用于保存坐标。除非您需要修改 Spark 提供的源代码(请参阅 GitHubCoordinateMatrix.scala)以定义一个更专业的容器(压缩),否则没有必要进一步了解它:

    • CoordinateMatrix也由 RDD 支持,这让您可以从一开始就利用并行性。
  • 您还需要导入IndexedRow,这样您就可以在实例化IndexedRowMatrix之前定义带有索引的行。

  • 这个矩阵可以转换为RowMatrixIndexedRowMatrixBlockMatrix

稀疏坐标系统还带来了高效的存储、检索和操作的附加好处(例如,所有设备与位置的安全威胁矩阵)。

另请参阅

在 Spark 2.0 中探索分布式 BlockMatrix

在这个示例中,我们探索了BlockMatrix,这是一个很好的抽象和其他矩阵块的占位符。简而言之,它是其他矩阵(矩阵块)的矩阵,可以作为单元访问。

CoordinateMatrix to a BlockMatrix and then do a quick check for its validity and access one of its properties to show that it was set up properly. BlockMatrix code takes longer to set up and it needs a real life application (not enough space) to demonstrate and show its properties in action.

如何做...

  1. 在 IntelliJ 或您选择的编辑器中启动一个新项目,并确保所有必要的 JAR 文件(Scala 和 Spark)对您的应用程序可用。

  2. 导入用于向量和矩阵操作的必要包:

import org.apache.spark.mllib.linalg.distributed.RowMatrix
 import org.apache.spark.mllib.linalg.distributed.{IndexedRow, IndexedRowMatrix}
 import org.apache.spark.mllib.linalg.distributed.{CoordinateMatrix, MatrixEntry}
 import org.apache.spark.sql.{SparkSession}
 import org.apache.spark.mllib.linalg._
 import breeze.linalg.{DenseVector => BreezeVector}
 import Array._
 import org.apache.spark.mllib.linalg.DenseMatrix
 import org.apache.spark.mllib.linalg.SparseVector
  1. 设置 Spark 上下文和应用程序参数,以便 Spark 可以运行。有关更多详细信息和变化,请参见本章的第一个示例:
val spark = SparkSession
 .builder
 .master("local[*]")
 .appName("myVectorMatrix")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()
  1. 快速创建一个CoordinateMatrix以用作转换的基础:
val distCordMat1 = new CoordinateMatrix( sc.parallelize(CoordinateEntries.toList))
  1. 我们将CoordinateMatrix转换为BlockMatrix
val distBlkMat1 =  distCordMat1.toBlockMatrix().cache() 
  1. 这是一个非常有用的矩阵类型的调用。在现实生活中,通常需要在进行计算之前检查设置:
distBlkMat1.validate() 
println("Is block empty =", distBlkMat1.blocks.isEmpty()) 

输出如下:

Is block empty =,false

它是如何工作的...

矩阵块将被定义为(int,int,Matrix)的元组。这种矩阵的独特之处在于它具有Add()Multiply()函数,这些函数可以将另一个BlockMatrix作为分布式矩阵的第二个参数。虽然一开始设置它有点令人困惑(特别是在数据到达时),但有一些辅助函数可以帮助您验证您的工作,并确保BlockMatrix被正确设置。这种类型的矩阵可以转换为本地的IndexRowMatrixCoordinateMatrixBlockMatrix最常见的用例之一是拥有CoordinateMatricesBlockMatrix

另请参阅

第三章:Spark 的三大数据武士-完美搭档

在本章中,我们将涵盖以下内容:

  • 使用内部数据源创建 Spark 2.0 的 RDDs

  • 使用外部数据源创建 Spark 2.0 的 RDDs

  • 使用 Spark 2.0 的 filter() API 转换 RDDs

  • 使用超级有用的 flatMap() API 转换 RDD

  • 使用集合操作 API 转换 RDDs

  • 使用 groupBy()和 reduceByKey()进行 RDD 转换/聚合

  • 使用 zip() API 转换 RDDs

  • 使用配对键值 RDD 进行连接转换

  • 使用配对键值 RDD 进行减少和分组转换

  • 从 Scala 数据结构创建数据框

  • 在没有 SQL 的情况下以编程方式操作数据框

  • 从外部源加载数据框和设置

  • 使用标准 SQL 语言的数据框- SparkSQL

  • 使用 Scala 序列使用数据集 API

  • 从 RDDs 创建和使用数据集,然后再次转换

  • 使用数据集 API 和 SQL 一起处理 JSON

  • 使用领域对象使用数据集 API 进行函数式编程

介绍

Spark 的三大工作马是 RDD、数据框架和数据集 API,用于高效处理规模化数据。虽然每个都有其自身的优点,但新的范式转变更青睐数据集作为统一的数据 API,以满足单一接口中的所有数据整理需求。

新的 Spark 2.0 数据集 API 是一组类型安全的领域对象集合,可以通过转换(类似于 RDD 的 filter、mapflatMap()等)并行使用功能或关系操作。为了向后兼容,数据集有一个名为DataFrame的视图,它是一组无类型的行。在本章中,我们演示了所有三个 API 集。前面的图总结了 Spark 数据整理的关键组件的优缺点:

机器学习中的高级开发人员必须理解并能够无障碍地使用所有三个 API 集,用于算法增强或遗留原因。虽然我们建议每个开发人员都应该向高级数据集 API 迁移,但您仍然需要了解 RDDs,以针对 Spark 核心系统进行编程。例如,投资银行和对冲基金经常阅读机器学习、数学规划、金融、统计或人工智能领域的领先期刊,然后使用低级 API 编写研究以获得竞争优势。

RDDs-一切的开始...

RDD API 是 Spark 开发人员的关键工具包,因为它偏向于在函数式编程范式中对数据进行低级控制。RDD 强大的地方也使得新手程序员更难使用。虽然理解 RDD API 和手动优化技术(例如,在groupBy()操作之前的filter())可能很容易,但编写高级代码需要持续的练习和流利。

当数据文件、块或数据结构转换为 RDD 时,数据被分解成称为分区的较小单元(类似于 Hadoop 中的拆分),并分布在节点之间,以便它们可以同时并行操作。Spark 提供了这种功能,可以在规模上立即使用,无需额外编码。框架会为您处理所有细节,您可以专注于编写代码,而不必担心数据。

要欣赏底层 RDD 的天才和优雅,必须阅读这个主题的原始论文,这被认为是这个主题上的最佳作品。可以在这里访问论文:

www.usenix.org/system/files/conference/nsdi12/nsdi12-final138.pdf

Spark 中有许多类型的 RDD 可以简化编程。下面的思维导图描述了 RDD 的部分分类。建议 Spark 上的程序员至少了解可用的 RDD 类型,甚至是不太知名的类型,如RandomRDDVertexRDDHadoopRDDJdbcRDDUnionRDD,以避免不必要的编码。

DataFrame - 通过高级 API 将 API 和 SQL 统一的自然演变。

Spark 开发人员社区一直致力于为社区提供易于使用的高级 API,从伯克利的 AMPlab 时代开始。数据 API 的下一个演变是当 Michael Armbrust 为社区提供了 SparkSQL 和 Catalyst 优化器,这使得使用简单且广为人知的 SQL 接口在 Spark 中实现了数据虚拟化。DataFrame API 是利用 SparkSQL 的自然演变,通过将数据组织成命名列来利用 SparkSQL。

DataFrame API 使得通过 SQL 进行数据整理对于许多熟悉 R(data.frame)或 Python/Pandas(pandas.DataFrame)的数据科学家和开发人员变得可行。

数据集 - 一个高级统一的数据 API

数据集是一个不可变的对象集合,它被建模/映射到传统的关系模式。有四个属性使其成为未来首选的方法。我们特别喜欢数据集 API,因为我们发现它与 RDD 非常相似,具有通常的转换操作符(例如filter()map()flatMap()等)。数据集将遵循类似 RDD 的延迟执行范式。试图调和 DataFrame 和 DataSet 的最佳方法是将 DataFrame 视为可以被视为Dataset[Row]的别名。

  • 强类型安全:我们现在在统一的数据 API 中既有编译时(语法错误)又有运行时安全,这有助于机器学习开发人员不仅在开发过程中,还可以在运行时防范意外。由于数据中的缺陷而在 Scala 或 Python 中使用 DataFrame 或 RDD Lambda 遇到意外运行时错误的开发人员将更好地理解和欣赏来自 Spark 社区和 Databricks(databricks.com)的这一新贡献。

  • 启用 Tungsten 内存管理:Tungsten 使 Apache Spark 更加接近裸金属(即利用sun.misc.Unsafe接口)。编码器便于将 JVM 对象映射到表格格式(见下图)。如果您使用数据集 API,Spark 将 JVM 对象映射到内部 Tungsten 堆外二进制格式,这更加高效。虽然 Tungsten 内部的细节超出了机器学习食谱的范围,但值得一提的是,基准测试显示使用堆外内存管理与 JVM 对象相比有显著改进。值得一提的是,在 Spark 中可用之前,堆外内存管理的概念在 Apache Flink 中一直是内在的。Spark 开发人员意识到了 Tungsten 项目的重要性,自 Spark 1.4、1.5 和 1.6 以来,一直到 Spark 2.0+的当前状态。再次强调,尽管在撰写本文时 DataFrame 将得到支持,并且已经详细介绍过(大多数生产系统仍然是 Spark 2.0 之前的版本),我们鼓励您开始思考数据集范式。下图显示了 RDD、DataFrame 和 DataSet 与 Tungsten 项目的演进路线之间的关系:

  • 编码器:编码器是 Spark 2.0 中的 Spark 序列化和反序列化(即 SerDe)框架。编码器无缝处理将 JVM 对象映射到表格格式的操作,您可以在底层获取并根据需要进行修改(专家级别)。

  • 与标准 Java 序列化和其他序列化方案(例如 Kryo)不同,编码器不使用运行时反射来动态发现对象内部以进行序列化。相反,编码器代码在编译时为给定对象生成并编译为字节码,这将导致更快的操作(不使用反射)来序列化和反序列化对象。运行时的反射对象内部(例如,查找字段及其格式)会带来额外的开销,在 Spark 2.0 中不存在。如果需要,仍然可以使用 Kryo、标准 java 序列化或任何其他序列化技术(边缘情况和向后兼容性)。

  • 标准数据类型和对象(由标准数据类型制成)的编码器在 Tungsten 中是开箱即用的。使用快速非正式的程序基准测试,使用 Hadoop MapReduce 开发人员广泛使用的 Kryo 序列化来回序列化对象,与编码器相比,发现了显着的 4 倍到 8 倍的改进。当我们查看源代码并深入了解后,我们意识到编码器实际上使用运行时代码生成(在字节码级别!)来打包和解包对象。为了完整起见,我们提到对象似乎也更小,但更多细节以及为什么会这样的原因超出了本书的范围。

  • Encoder[T]是 DataSet[T]的内部构件,它只是记录的模式。您可以根据需要在 Scala 中使用底层数据的元组(例如 Long、Double 和 Int)创建自定义编码器。在开始自定义编码器之前(例如,想要在 DataSet[T]中存储自定义对象),请确保查看 Spark 源目录中的[Encoders.scala](https://github.com/apache/spark/blob/v2.0.0/sql/catalyst/src/main/scala/org/apache/spark/sql/Encoders.scala#L270-L316)[SQLImplicits.scala](https://github.com/apache/spark/blob/v2.0.0/sql/core/src/main/scala/org/apache/spark/sql/SQLImplicits.scala#L77-L96)。Spark 的计划和战略方向是在未来的版本中提供一个公共 API。

  • Catalyst 优化器友好:使用 Catalyst,API 手势被转换为使用目录(用户定义的函数)的逻辑查询计划,并最终将逻辑计划转换为物理计划,这通常比原始方案提出的更有效(即使您尝试在filter()之前使用groupBy(),它也足够聪明地安排它的顺序)。为了更好地理解,请参见下图:

对于 Spark 2.0 之前的用户值得注意:

  • SparkSession现在是系统的唯一入口点。SQLContext 和 HiveContext 被 SparkSession 取代。

  • 对于 Java 用户,请确保将 DataFrame 替换为Dataset<Row>

  • 通过SparkSession使用新的目录接口执行cacheTable()dropTempView()createExternalTable()ListTable()等。

  • DataFrame 和 DataSet API:

  • unionALL()已被弃用;现在应该使用union()

  • explode()应该被functions.explode()select()flatMap()替换

  • registerTempTable已被弃用,替换为createOrReplaceTempView()

使用内部数据源在 Spark 2.0 中创建 RDD

在 Spark 中有四种创建 RDD 的方法。它们从parallelize()方法用于在客户端驱动程序代码中进行简单测试和调试到流式 RDD,用于近实时响应。在本教程中,我们提供了几个示例来演示使用内部源创建 RDD。流式情况将在第十三章中的流式 Spark 示例中进行介绍,因此我们可以以有意义的方式来解决它。

如何做到...

  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. 我们声明两个本地数据结构来保存数据,然后再使用任何分布式 RDD。需要注意的是,这里的数据将通过本地数据结构保存在驱动程序的堆空间中。我们在这里明确提到,因为程序员在使用parallelize()技术进行大数据集测试时会遇到多种问题。如果使用这种技术,请确保驱动程序本地有足够的空间来保存数据。
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) 
  1. 我们使用parallelize()函数将本地数据分发到集群中。
val parSN=spark.sparkContext.parallelize(SignalNoise) // parallelized signal noise RDD 
val parSS=spark.sparkContext.parallelize(SignalStrength)  // parallelized signal strength 
  1. 让我们看看 Spark 如何看待这两种数据结构的区别。可以通过打印两个数据结构句柄来完成:一个本地数组和一个集群并行集合(即 RDD)。

输出如下:

    Signal Noise Local Array ,[D@2ab0702e)
    RDD Version of Signal Noise on the cluster  
    ,ParallelCollectionRDD[0] at parallelize at myRDD.scala:45)
  1. Spark 尝试根据集群的配置自动设置分区数(即 Hadoop 中的分区),但有时我们需要手动设置分区数。parallelize()函数提供了第二个参数,允许您手动设置分区数。
val parSN=spark.sparkContext.parallelize(SignalNoise) // parallelized signal noise RDD set with default partition 
val parSS=spark.sparkContext.parallelize(SignalStrength)  // parallelized signal strength set with default partition 
val parSN2=spark.sparkContext.parallelize(SignalNoise,4) // parallelized signal noise set with 4 partition 
val parSS2=spark.sparkContext.parallelize(SignalStrength,8)  // parallelized signal strength set with 8 partition 
println("parSN partition length ", parSN.partitions.length ) 
println("parSS partition length ", parSS.partitions.length ) 
println("parSN2 partition length ",parSN2.partitions.length ) 
println("parSS2 partition length ",parSS2.partitions.length ) 

输出如下:

parSN partition length ,2
parSS partition length ,2
parSN2 partition length ,4
parSS2 partition length ,8

在前两行中,Spark 默认选择了两个分区,接下来两行中,我们分别将分区数设置为 4 和 8。

工作原理...

客户端驱动程序中保存的数据被并行化并分布到集群中,使用分区 RDD 的数量(第二个参数)作为指导。生成的 RDD 是 Spark 的魔力,它启动了一切(参考 Matei Zaharia 的原始白皮书)。

生成的 RDD 现在是完全分布式的数据结构,具有容错性和血统,可以使用 Spark 框架并行操作。

我们从www.gutenberg.org/读取了查尔斯·狄更斯的《双城记》文本文件到 Spark RDD 中。然后我们继续拆分和标记数据,并使用 Spark 的操作符(例如mapflatMap()等)打印总词数。

使用外部数据源使用 Spark 2.0 创建 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) 
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. 从以下来源下载文本并将其存储在本地目录中:

  1. 再次使用SparkContext,通过SparkSession可用,并使用其textFile()函数来读取外部数据源,并在集群中并行化。值得注意的是,Spark 在幕后使用一次单一调用来加载各种格式(例如文本、S3 和 HDFS),并使用protocol:filepath组合将数据并行化到集群中,为开发人员完成了所有工作。

  2. 为了演示,我们使用textFile()方法从SparkContext通过SparkSession读取存储为 ASCII 文本的书籍,然后在幕后创建跨集群的分区 RDDs。

val book1 = spark.sparkContext.textFile("../data/sparkml2/chapter3/a.txt") 

输出将如下所示:

Number of lines = 16271
  1. 尽管我们还没有涵盖 Spark 转换运算符,但我们将看一个小的代码片段,它将使用空格将文件分割成单词。在现实生活中,需要使用正则表达式来处理所有的边缘情况和所有的空格变化(参考本章中的使用 filter() API 转换 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 的.count()方法输出单词的总数。虽然这很简单,但您必须记住,这个操作是在 Spark 的分布式并行框架中进行的,只需要几行代码。

还有更多...

使用外部数据源创建 RDDs,无论是文本文件、Hadoop HDFS、序列文件、Casandra 还是 Parquet 文件,都非常简单。再次使用SparkSession(Spark 2.0 之前使用SparkContext)来获取对集群的控制。一旦执行函数(例如,textFile Protocol: file path),数据就会被分成更小的片段(分区),并自动流向集群,变成可供计算使用的容错分布式集合,可以并行操作。

  1. 在处理真实情况时,有许多变化需要考虑。根据我们自己的经验,最好的建议是在编写自己的函数或连接器之前查阅文档。Spark 要么直接支持您的数据源,要么供应商有一个可以下载的连接器来完成相同的工作。

  2. 我们经常看到的另一种情况是生成许多小文件(通常在HDFS目录中),需要将它们并行化为 RDDs 以供使用。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(例如,AWS 存储桶的 s3n)到具有服务器和端口配置的完整资源路径(例如,从 Hadoop 集群读取 HDFS 文件)。

  • textFile()方法还支持完整目录、正则表达式通配符和压缩格式。看一下这个示例代码:

val book1 = spark.sparkContext.textFile("C:/xyz/dailyBuySel/*.tif")
  • textFile()方法在末尾有一个可选参数,定义了 RDD 所需的最小分区数。

例如,我们明确指示 Spark 将文件分成 13 个分区:

val book1 = spark.sparkContext.textFile("../data/sparkml2/chapter3/a.txt", 13) 

您还可以选择指定 URI 来从其他来源(如 HDFS 和 S3)读取和创建 RDD,通过指定完整的 URI(协议:路径)。以下示例演示了这一点:

  1. 从 Amazon S3 存储桶中读取和创建文件。需要注意的是,如果 AWS 秘钥中有斜杠,URI 中的 AWS 内联凭据将会中断。请参阅此示例文件:
spark.sparkContext.hadoopConfiguration.set("fs.s3n.awsAccessKeyId", "xyz") 
spark.sparkContext.hadoopConfiguration.set("fs.s3n.awsSecretAccessKey", "....xyz...") 
S3Rdd = spark.sparkContext.textFile("s3n://myBucket01/MyFile01") 
  1. 从 HDFS 读取非常相似。在这个例子中,我们从本地 Hadoop 集群读取,但在现实世界的情况下,端口号将是不同的,并由管理员设置。
val hdfsRDD = spark.sparkContext.textFile("hdfs:///localhost:9000/xyz/top10Vectors.txt") 

使用 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) 
Logger.getLogger("akka").setLevel(Level.ERROR) 
  1. 设置 Spark 上下文和应用程序参数,以便 Spark 可以运行。
val spark = SparkSession 
  .builder 
  .master("local[*]") 
  .appName("myRDD") 
  .config("Spark.sql.warehouse.dir", ".") 
  .getOrCreate() 
  1. 添加以下行以使示例编译通过。pow()函数将允许我们将任何数字提升到任意幂(例如,平方该数字):
import breeze.numerics.pow
  1. 我们创建一些数据并parallelize()它以获得我们的基本 RDD。我们还使用textFile()从我们之前从www.gutenberg.org/cache/epub/98/pg98.txt链接下载的文本文件创建初始(例如,基本 RDD):
val num : Array[Double] = Array(1,2,3,4,5,6,7,8,9,10,11,12,13) 
  val numRDD=sc.parallelize(num) 
  val book1 = spark.sparkContext.textFile("../data/sparkml2/chapter3/a.txt")
  1. 我们应用filter()函数到 RDD 中,以演示filter()函数的转换。我们使用filter()函数从原始 RDD 中选择奇数成员。

  2. filter()函数并行迭代 RDD 的成员,并应用 mod 函数(%)并将其与 1 进行比较。简而言之,如果除以 2 后有余数,那么它必须是奇数。

  val myOdd= num.filter( i => (i%2) == 1) 

这是上一行的第二种变体,但在这里我们演示了_(下划线)的使用,它充当通配符。我们在 Scala 中使用这种表示法来缩写明显的内容:

val myOdd2= num.filter(_ %2 == 1) // 2nd variation using scala notation  
myOdd.take(3).foreach(println) 

在运行上述代码时,您将获得以下输出:

1.0
3.0
5.0
  1. 另一个例子将 map 和 filter 结合在一起。这段代码首先对每个数字进行平方,然后应用filter函数从原始 RDD 中选择奇数。
val myOdd3= num.map(pow(_,2)).filter(_ %2 == 1) 
myOdd3.take(3).foreach(println)  

输出将如下所示:

1.0
9.0
25.0
  1. 在这个例子中,我们使用filter()方法来识别少于 30 个字符的行。结果 RDD 将只包含短行。对计数和输出的快速检查验证了结果。只要格式符合函数语法,RDD 转换函数就可以链接在一起。
val shortLines = book1.filter(_.length < 30).filter(_.length > 0) 
  println("Total number of lines = ", book1.count()) 
  println("Number of Short Lines = ", shortLines.count()) 
  shortLines.take(3).foreach(println) 

运行上述代码后,您将获得以下输出:

  1. 在这个例子中,我们使用contain()方法来过滤包含任何大小写组合的单词two的句子。我们使用多个方法链接在一起来找到所需的句子。
val theLines = book1.map(_.trim.toUpperCase()).filter(_.contains("TWO")) 
println("Total number of lines = ", book1.count()) 
println("Number of lines with TWO = ", theLines.count()) 
theLines.take(3).foreach(println) 

它是如何工作的...

使用几个示例演示了filter() API。在第一个示例中,我们通过使用 lambda 表达式.filter(i => (i%2) == 1)遍历了一个 RDD 并输出了奇数,利用了模(modulus)函数。

在第二个示例中,我们通过使用 lambda 表达式num.map(pow(_,2)).filter(_ %2 == 1)将结果映射到一个平方函数,使其变得有趣一些。

在第三个示例中,我们遍历文本并使用 lambda 表达式.filter(_.length < 30).filter(_.length > 0)过滤出短行(例如,少于 30 个字符的行),以打印短行与总行数(.count())作为输出。

还有更多...

filter() API 遍历并应用于filter()中提供的选择条件的并行分布式集合(即 RDD),以便通过 lambda 包含或排除结果 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()函数从我们之前从www.gutenberg.org/cache/epub/98/pg98.txt下载的文本文件创建初始(即基本 RDD):
val book1 = spark.sparkContext.textFile("../data/sparkml2/chapter3/a.txt")
  1. 我们将 map 函数应用于 RDDs 以演示map()函数的转换。首先,我们以错误的方式进行演示:我们首先尝试根据正则表达式*[\s\W]+]*使用map()来分隔所有单词,以演示生成的 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()APIfilter(_.length > 0)来排除空行,并在.map()API 中使用 lambda 表达式.map(_.toUpperCase())将结果映射到大写形式。

有些情况下,我们不希望为基本 RDD 的每个元素都返回一个列表(例如,为与一行对应的单词获取一个列表)。我们有时更喜欢有一个单一的扁平化列表,它是平的并且对应于文档中的每个单词。简而言之,我们希望得到一个包含所有元素的单一列表,而不是一个列表的列表。

还有更多...

函数glom()是一个函数,它允许您将 RDD 中的每个分区建模为一个数组,而不是一个行列表。虽然在大多数情况下可能会产生结果,但glom()允许您减少分区之间的洗牌。

尽管在下面的文本中,方法 1 和 2 看起来很相似,用于计算 RDD 中最小数的方法,但glom()函数将通过首先将min()应用于所有分区,然后发送结果数据来减少网络上的数据洗牌。查看差异的最佳方法是在 10M+ RDD 上使用它,并相应地观察 IO 和 CPU 使用情况。

  • 第一种方法是在不使用glom()的情况下找到最小值:
val minValue1= numRDD.reduce(_ min _) 
println("minValue1 = ", minValue1)

运行上述代码后,您将获得以下输出:

minValue1 = 1.0
  • 第二种方法是使用glom()找到最小值,这将导致将最小函数局部应用于一个分区,然后通过洗牌发送结果。
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) 
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 num : Array[Double]    = Array(1,2,3,4,5,6,7,8,9,10,11,12,13) 
val odd : Array[Double]    = Array(1,3,5,7,9,11,13) 
val even : Array[Double]    = Array(2,4,6,8,10,12) 
  1. 我们将intersection()函数应用于 RDD,以演示转换:
val intersectRDD = numRDD.intersection(oddRDD) 

运行上述代码后,您将获得以下输出:

1.0
3.0
5.0
  1. 我们将union()函数应用于 RDD,以演示转换:
    val unionRDD = oddRDD.union(evenRDD) 

运行上述代码后,您将获得以下输出:

1.0
2.0
3.0
4.0

  1. 我们将subract()函数应用于 RDD,以演示转换:
val subtractRDD = numRDD.subtract(oddRDD) 

运行上述代码后,您将获得以下输出:

2.0
4.0
6.0
8.0

  1. 我们将distinct()函数应用于 RDD,以演示转换:
val namesRDD = spark.sparkContext.parallelize(List("Ed","Jain", "Laura", "Ed")) 
val ditinctRDD = namesRDD.distinct() 

运行上述代码后,您将获得以下输出:

"ED"
"Jain"
"Laura"

  1. 我们将distinct()函数应用于 RDD,以演示转换。
val cartesianRDD = oddRDD.cartesian(evenRDD) 
cartesianRDD.collect.foreach(println) 

运行上述代码后,您将获得以下输出:

(1.0,2.0)
(1.0,4.0)
(1.0,6.0)
(3.0,2.0)
(3.0,4.0)
(3.0,6.0)   

它是如何工作的...

在这个例子中,我们从三组数字数组(奇数、偶数和它们的组合)开始,然后将它们作为参数传递到集合操作 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) 
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。在这个例子中,我们使用范围工具创建了一个 RDD,并将其分成三个分区(即,显式参数集)。它只是创建了 1 到 12 的数字,并将它们放入 3 个分区。
    val rangeRDD=sc.parallelize(1 to 12,3)
  1. 我们将groupBy()函数应用于 RDD,以演示转换。在这个例子中,我们使用mod函数将分区 RDD 标记为奇数/偶数。
val groupByRDD= rangeRDD.groupBy( i => {if (i % 2 == 1) "Odd" 
  else "Even"}).collect 
groupByRDD.foreach(println) 

运行上述代码后,您将获得以下输出:

  1. 现在我们已经看到了如何编写groupBy(),我们转而演示reduceByKey()

  2. 为了看到编码上的差异,同时更有效地产生相同的输出,我们设置了一个包含两个字母(即ab)的数组,以便我们可以通过对它们进行求和来展示聚合。

val alphabets = Array("a", "b", "a", "a", "a", "b") // two type only to make it simple 
  1. 在这一步中,我们使用 Spark 上下文来生成并行化的 RDD:
val alphabetsPairsRDD = spark.sparkContext.parallelize(alphabets).map(alphabets => (alphabets, 1)) 
  1. 我们首先应用groupBy()函数,使用通常的 Scala 语法(_+_)来遍历 RDD 并在按字母类型(即被视为键)进行聚合时进行求和:
val countsUsingGroup = alphabetsPairsRDD.groupByKey() 
  .map(c => (c._1, c._2.sum)) 
  .collect() 
  1. 我们首先应用reduceByKey()函数,使用通常的 Scala 语法(_+_)来遍历 RDD 并在按字母类型(即被视为键)进行聚合时进行求和。
val countsUsingReduce = alphabetsPairsRDD 
  .reduceByKey(_ + _) 
  .collect()
  1. 我们输出了结果:
println("Output for  groupBy") 
countsUsingGroup.foreach(println(_)) 
println("Output for  reduceByKey") 
countsUsingReduce.foreach(println(_)) 

运行上述代码后,您将得到以下输出:

Output for groupBy
(b,2)
(a,4)
Output for reduceByKey
(b,2)
(a,4)  

它是如何工作的...

在这个例子中,我们创建了从一到十二的数字,并将它们放在三个分区中。然后我们使用简单的模运算将它们分成奇数/偶数。groupBy()用于将它们聚合成奇数/偶数两组。这是一个典型的聚合问题,对于 SQL 用户来说应该很熟悉。在本章的后面,我们将使用DataFrame重新讨论这个操作,它也利用了 SparkSQL 引擎提供的更好的优化技术。在后面的部分,我们演示了groupBy()reduceByKey()的相似之处。我们设置了一个字母数组(即ab),然后将它们转换为 RDD。然后我们根据键(即唯一的字母 - 在这种情况下只有两个)对它们进行聚合,并打印出每个组的总数。

还有更多...

鉴于 Spark 更倾向于 Dataset/DataFrame 范式而不是低级别的 RDD 编码,我们必须认真考虑在 RDD 上执行groupBy()的原因。虽然有合法的情况需要这个操作,但读者们被建议重新构思他们的解决方案,以利用 SparkSQL 子系统及其名为Catalyst的优化器。

Catalyst 优化器在构建优化的查询计划时考虑了 Scala 强大的特性,比如模式匹配准引用

运行时效率考虑:groupBy()函数按键对数据进行分组。这个操作会导致内部的数据重分配,可能会导致执行时间的爆炸;必须始终优先使用reduceByKey()系列的操作,而不是直接使用groupBy()方法。groupBy()方法由于数据重分配而是一个昂贵的操作。每个组由键和属于该键的项目组成。Spark 不保证与键对应的值的顺序。

有关这两个操作的解释,请参阅 Databricks 知识库博客:

databricks.gitbooks.io/databricks-Spark-knowledge-base/content/best_practices/prefer_reducebykey_over_groupbykey.html

另请参阅

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. 我们将zip()函数应用于 RDD,以演示转换。在这个例子中,我们使用 mod 函数将分区 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  

它是如何工作的...

在这个例子中,我们首先设置了两个代表信号噪声和信号强度的数组。它们只是一组我们可以从 IoT 平台接收到的测量数字。然后,我们继续配对这两个单独的数组,使每个成员看起来就像它们最初被输入为一对(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. 将列表转换为 RDDs:
val keyValueRDD = spark.sparkContext.parallelize(keyValuePairs) 
val keyValueCity1RDD = spark.sparkContext.parallelize(keyValueCity1) 
val keyValueCity2RDD = spark.sparkContext.parallelize(keyValueCity2) 
  1. 我们可以访问对 RDD 内部的keysvalues
val keys=keyValueRDD.keys 
val values=keyValueRDD.values 
  1. 我们将mapValues()函数应用于对 RDD,以演示转换。在这个例子中,我们使用 map 函数通过将每个元素加 100 来提升值。这是一种引入数据噪声(即抖动)的常用技术。
val kvMappedRDD = keyValueRDD.mapValues(_+100) 
kvMappedRDD.collect().foreach(println(_)) 

运行上述代码后,您将获得以下输出:

(north,101)
(south,102)
(east,103)
(west,104)

  1. 我们将join()函数应用于 RDD,以演示转换。我们使用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. 我们将leftOuterJoin()函数应用于 RDD,以演示转换。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. 我们将应用rightOuterJoin()到 RDD,以演示转换。这类似于关系系统中的右外连接。
println("Right Joined RDD = ") 
val rightJoinedRDD = keyValueRDD.rightOuterJoin(keyValueCity2RDD) 
rightJoinedRDD.collect().foreach(println(_)) 

运行上述代码后,您将获得以下输出:

(north,(Some(1),Madison))
(west,(Some(4),SanJose))  
  1. 然后,我们将fullOuterJoin()函数应用于 RDD,以演示转换。这类似于关系系统中的全外连接。
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()leftOuterJoinrightOuterJoin(),以及fullOuterJoin(),以展示与键值对 RDD 结合时的强大和灵活性。

还有更多...

join()及其在 RDD 下的变体的文档可在spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.api.java.JavaRDD上找到。

使用配对键值 RDD 进行减少和分组转换

在这个示例中,我们探讨了减少和按键分组。reduceByKey()groupbyKey()操作比reduce()groupBy()更有效,通常更受欢迎。这些函数提供了方便的设施,以较少的洗牌来聚合值并按键组合,这在大型数据集上是有问题的。

如何做...

  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. 我们将reduceByKey()函数应用于 RDD 的一对,以演示转换。在这个例子中,函数是为买入和卖出信号的总成交量求和。(_+_)的 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. 我们设置了 Scala 数据结构作为两个List()对象和一个序列(即Seq())。然后我们将List结构转换为 RDD,以便进行下一步的 DataFrame 转换:
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()方法允许我们查看 DataFrame,这类似于 SQL 表。
val numDF = numRDD.toDF("mylist") 
numDF.show 

在运行上述代码时,您将获得以下输出:

+------+
|mylist|
+------+
|     1|
|     2|
|     3|
|     4|
|     5|
|     6|
|     7|
|     8|
|     9|
+------+
  1. 在下面的代码片段中,我们使用createDataFrame()显式地从一个通用的 Scala SeqSequence)数据结构创建一个 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 

在没有使用 SQL 的情况下以编程方式操作 DataFrame

在这个配方中,我们探讨如何仅通过代码和方法调用来操作 DataFrame(而不使用 SQL)。DataFrame 有自己的方法,允许您使用编程方法执行类似 SQL 的操作。我们演示了一些这些命令,比如select()show()explain(),以表明 DataFrame 本身能够在不使用 SQL 的情况下处理和操作数据。

如何做...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序所在的包位置

package spark.ml.cookbook.chapter3 
  1. 设置与 DataFrame 相关的导入和所需的数据结构,并根据需要创建 RDDs
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. 我们从外部来源创建了一个 RDD,这是一个逗号分隔的文本文件:
val customersRDD = spark.sparkContext.textFile("../data/sparkml2/chapter3/customers13.txt") //Customer file
  1. 这是客户数据文件的快速查看
Customer data file    1101,susan,nyc,23 1204,adam,chicago,76
1123,joe,london,65
1109,tiffany,chicago,20

  1. 在为相应的客户数据文件创建 RDD 之后,我们继续使用map()函数从 RDD 中显式解析和转换数据类型。在这个例子中,我们要确保最后一个字段(即年龄)表示为整数。
val custRDD = customersRDD.map { 
  line => val cols = line.trim.split(",") 
    (cols(0).toInt, cols(1), cols(2), cols(3).toInt) 
} 
  1. 在第三步中,我们使用toDF()调用将 RDD 转换为 DataFrame。
    val custDF = custRDD.toDF("custid","name","city","age") 
  1. 一旦 DataFrame 准备好,我们希望快速显示内容以进行视觉验证,并打印和验证模式。
custDF.show() 
custDF.printSchema() 

运行上述代码后,您将得到以下输出:

+------+-------+-------+---+
|custid|   name|   city|age|
+------+-------+-------+---+
|  1101|  susan|    nyc| 23|
|  1204|   adam|chicago| 76|
|  1123|    joe| london| 65|
|  1109|tiffany|chicago| 20|
+------+-------+-------+---+

root
|-- custid: integer (nullable = false)
|-- name: string (nullable = true)
|-- city: string (nullable = true)
|-- age: integer (nullable = false)
  1. 在 DataFrame 准备好并经过检查后,我们继续演示通过show()select()sort()groupBy()explain()API 对 DataFrame 进行访问和操作。

  2. 我们使用filter()方法来列出年龄超过 25 岁的客户:

custDF.filter("age > 25.0").show() 

运行上述代码后,您将得到以下输出:

+------+----+-------+---+ 
|custid|name|   city|age| 
+------+----+-------+---+ 
|  1204|adam|chicago| 76| 
|  1123| joe| london| 65| 
+------+----+-------+---+ 
  1. 我们使用select()方法来显示客户的姓名。
custDF.select("name").show() 

运行上述代码后,您将得到以下输出。

+-------+ 
|   name| 
+-------+ 
|  susan| 
|   adam| 
|    joe| 
|tiffany| 
+-------+ 
  1. 我们使用select()来列出多个列:
custDF.select("name","city").show() 

运行上述代码后,您将得到以下输出:

    +-------+-------+
    |   name|   city|
    +-------+-------+
    |  susan|    nyc|
    |   adam|chicago|
    |    joe| london|
    |tiffany|chicago|
    +-------+-------+
  1. 我们使用另一种语法来显示和引用 DataFrame 中的字段:
custDF.select(custDF("name"),custDF("city"),custDF("age")).show() 

运行上述代码后,您将得到以下输出:

+-------+-------+---+
|   name|   city|age|
+-------+-------+---+
|  susan|    nyc| 23|
|   adam|chicago| 76|
|    joe| london| 65|
|tiffany|chicago| 20|
+-------+-------+---+  
  1. 使用select()和谓词,列出年龄小于 50 岁的客户的姓名和城市:
custDF.select(custDF("name"),custDF("city"),custDF("age") <50).show() 

运行上述代码后,您将得到以下输出:

  1. 我们使用sort()groupBy()来按客户所在城市对客户进行排序和分组:
custDF.sort("city").groupBy("city").count().show() 

运行上述代码后,您将得到以下输出。

  1. 我们还可以要求执行计划:这个命令在我们使用 SQL 来访问和操作 DataFrame 的即将到来的配方中将更加相关。
custDF.explain()  

运行上述代码后,您将得到以下输出:

== Physical Plan ==
TungstenProject [_1#10 AS custid#14,_2#11 AS name#15,_3#12 AS city#16,_4#13 AS age#17]
 Scan PhysicalRDD[_1#10,_2#11,_3#12,_4#13]

它是如何工作的...

在这个例子中,我们从文本文件中加载数据到 RDD,然后使用.toDF()API 将其转换为 DataFrame 结构。然后,我们使用内置方法如select()filter()show()explain()来模拟 SQL 查询,这些方法帮助我们通过 API 程序化地探索数据(无需 SQL)。explain()命令显示查询计划,这对于消除瓶颈非常有用。

DataFrame 提供了多种数据整理方法。

对于那些熟悉 DataFrame API 和来自 R 的包(cran.r-project.org)如 dplyr 或旧版本的人,我们有一个编程 API,其中包含一系列广泛的方法,让您可以通过 API 进行所有数据整理。

对于那些更喜欢使用 SQL 的人,您可以简单地使用 SQL 来检索和操作数据,就像使用 Squirrel 或 Toad 查询数据库一样。

还有更多...

为了确保完整性,我们包括了我们在 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找到。

如果您发现隐式转换有任何问题,请仔细检查是否已包含了隐式import语句。

Spark 2.0 的示例import语句:

import sqlContext.implicits._

从外部源加载 DataFrame 和设置

在这个示例中,我们使用 SQL 来进行数据操作。Spark 提供的既实用又 SQL 接口的方法在生产环境中非常有效,我们不仅需要机器学习,还需要使用 SQL 访问现有数据源,以确保与现有基于 SQL 的系统的兼容性和熟悉度。DataFrame 与 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找到。

如果您发现隐式转换存在问题,请仔细检查是否已包含了隐式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("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. 我们将使用上一个示例中创建的 DataFrames 来演示 DataFrame 的 SQL 功能。您可以参考上一步了解详情。
a. customerDF with columns: "custid","name","city","age" b. productDF with Columns: "prodid","category","dept","priceAdvertised" c. saleDF with columns: "prodid", "custid", "priceSold"

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") 
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)       } 

val prodDF = prodRDD.toDF("prodid","category","dept","priceAdvertised")

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)
   }
val saleDF = saleRDD.toDF("prodid", "custid", "priceSold")
  1. 在我们可以通过 SQL 对 DataFrame 进行查询之前,我们必须将 DataFrame 注册为临时表,以便 SQL 语句可以在不使用任何 Scala/Spark 语法的情况下引用它。这一步可能会让许多初学者感到困惑,因为我们并没有创建任何表(临时或永久),但registerTempTable()(Spark 2.0 之前)和createOrReplaceTempView()(Spark 2.0+)的调用在 SQL 中创建了一个名称,SQL 语句可以在其中引用,而无需额外的 UDF 或任何特定领域的查询语言。简而言之,Spark 在后台保留了额外的元数据(registerTempTable()调用),这有助于在执行阶段进行查询。

  2. 创建名为customersCustDf DataFrame:

custDF.createOrReplaceTempView("customers")
  1. 创建名为productprodDf DataFrame:
prodDF.createOrReplaceTempView("products")
  1. 创建名为salessaleDf DataFrame,以便 SQL 语句能够识别:
saleDF.createOrReplaceTempView("sales")
  1. 现在一切准备就绪,让我们演示 DataFrames 与标准 SQL 的强大功能。对于那些不愿意使用 SQL 的人来说,编程方式始终是一个选择。

  2. 在此示例中,我们演示了如何从 customers 表中选择列(实际上并不是 SQL 表,但您可以将其抽象为 SQL 表)。

val query1DF = spark.sql ("select custid, name from customers")
 query1DF.show()

运行上述代码后,您将获得以下输出。

  1. 从 customer 表中选择多列:
val query2DF = spark.sql("select prodid, priceAdvertised from products")
 query2DF.show()

运行上述代码后,您将获得以下输出。

  1. 我们打印了 customer、product 和 sales DataFrames 的模式,以便在列定义和类型转换后进行验证:
val query3DF = spark.sql("select sum(priceSold) as totalSold from sales")
query3DF.show()

运行上述代码后,您将获得以下输出。

  1. 在这个示例中,我们连接销售和产品表,并列出所有购买了打折超过 20%的产品的客户。这个 SQL 连接了销售和产品表,然后使用一个简单的公式来找到以深度折扣出售的产品。重申一下,DataFrame 的关键方面是我们使用标准 SQL 而没有任何特殊的语法。
val query4DF = spark.sql("select custid, priceSold, priceAdvertised from sales s, products p where (s.priceSold/p.priceAdvertised < .80) and p.prodid = s.prodid")
query4DF.show()

运行上述代码后,您将得到以下输出。

我们可以始终使用explain()方法来检查 Spark SQL 用于执行查询的物理查询计划。

query4DF.explain()

运行上述代码后,您将得到以下输出:

== Physical Plan ==
TungstenProject [custid#30,priceSold#31,priceAdvertised#25]
 Filter ((priceSold#31 / priceAdvertised#25) < 0.8)
 SortMergeJoin [prodid#29], [prodid#22]
 TungstenSort [prodid#29 ASC], false, 0
 TungstenExchange hashpartitioning(prodid#29)
 TungstenProject [_1#26 AS prodid#29,_2#27 AS custid#30,_3#28 AS priceSold#31]
 Scan PhysicalRDD[_1#26,_2#27,_3#28]
 TungstenSort [prodid#22 ASC], false, 0
 TungstenExchange hashpartitioning(prodid#22)
 TungstenProject [_4#21 AS priceAdvertised#25,_1#18 AS prodid#22]
 Scan PhysicalRDD[_1#18,_2#19,_3#20,_4#21]

它是如何工作的...

使用 SQL 处理 DataFrame 的基本工作流程是首先通过内部 Scala 数据结构或外部数据源填充 DataFrame,然后使用createOrReplaceTempView()调用将 DataFrame 注册为类似 SQL 的工件。

当您使用 DataFrame 时,您将获得 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 表类似的工件:

在我们可以通过 SQL 查询使用 DataFrame 之前,我们必须将 DataFrame 注册为临时表,以便 SQL 语句可以引用它而不需要任何 Scala/Spark 语法。这一步可能会让许多初学者感到困惑,因为我们并没有创建任何表(临时或永久),但是registerTempTable()调用在 SQL 领域创建了一个名称,SQL 语句可以引用它而不需要额外的 UDF 或任何特定于领域的查询语言。

  • CustDf DataFrame 注册为 SQL 语句识别为customers的名称:
custDF.registerTempTable("customers")
  • prodDf DataFrame 注册为 SQL 语句识别为product的名称:
custDF.registerTempTable("customers")
  • saleDf DataFrame 注册为 SQL 语句识别为sales的名称:
custDF.registerTempTable("customers")

为了确保完整性,我们包括了我们在 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找到。

如果您遇到隐式转换的问题,请仔细检查是否已经包含了隐式import语句。

Spark 1.5.2 的示例import语句

 import sqlContext.implicits._

DataFrame 是一个庞大的子系统,值得单独写一本书。它使得规模化的复杂数据操作对 SQL 程序员可用。

使用 Scala 序列处理数据集 API

在这个示例中,我们将研究新的 Dataset 以及它如何与seq Scala 数据结构一起工作。我们经常看到 ML 库中使用的 LabelPoint 数据结构与 Scala 序列(即 seq 数据结构)之间存在关系,这些数据结构与数据集很好地配合。

数据集被定位为未来的统一 API。重要的是要注意,DataFrame 仍然作为别名Dataset[Row]可用。我们已经通过 DataFrame 示例广泛涵盖了 SQL 示例,因此我们将集中精力研究数据集的其他变化。

如何做...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序所在的包位置

package spark.ml.cookbook.chapter3
  1. 导入必要的包以获取对集群的访问以及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 implicits,只需导入即可添加行为。
import spark.implicits._
  1. 接下来,我们将利用 Spark 会话的createDataset()方法从汽车数据序列创建一个数据集。
val cars = spark.createDataset(MyDatasetData.carData) 
// carData is put in a separate scala object MyDatasetData
  1. 让我们打印出结果,确认我们的方法调用将序列转换为 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 中编程。

另请参阅

从 RDD 创建和使用数据集,然后再转回去

在这个示例中,我们探讨了如何使用 RDD 并与数据集交互,以构建多阶段的机器学习流水线。尽管数据集(在概念上被认为是具有强类型安全性的 RDD)是未来的发展方向,但您仍然必须能够与其他机器学习算法或返回/操作 RDD 的代码进行交互,无论是出于传统还是编码的原因。在这个示例中,我们还探讨了如何创建和转换数据集到 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 类来建模处理数据。
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. 使用构建器模式初始化 Spark 会话,从而为 Spark 集群提供入口点。
val spark = SparkSession
.builder
.master("local[*]")
.appName("mydatasetrdd")
.config("Spark.sql.warehouse.dir", ".")
.getOrCreate()
  1. 接下来,我们从 Spark 会话中检索对 Spark 上下文的引用,因为我们稍后将需要它来生成 RDD。
val sc = spark.sparkContext
  1. 导入 Spark implicits,因此只需导入即可添加行为。
import spark.implicits._
  1. 让我们从汽车数据序列中创建一个 RDD。
val rdd = spark.makeRDD(MyDatasetData.carData)
  1. 接下来,我们将使用 Spark 的会话createDataset()方法从包含汽车数据的 RDD 创建数据集。
val cars = spark.createDataset(*rdd*)
  1. 让我们打印出数据集,以验证创建是否按预期进行,通过show方法。
cars.show(false)

运行上述代码后,您将获得以下输出。

  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. 现在,让我们按制造商对数据集进行分组,并计算数据集中制造商的数量。
cars.groupBy("make").count().show()

运行上述代码后,您将获得以下输出。

  1. 下一步将使用 Spark 的 SQL 对数据集进行过滤,按制造商过滤特斯拉的值,并将结果数据集转换回 RDD。
val carRDD = cars.where("make = 'Tesla'").rdd
Car(Tesla,Model X,80000.0,suv,electric)
Car(Tesla,Model 3,35000.0,sedan,electric)
Car(Tesla,Model S,71000.0,sedan,electric)
  1. 最后,利用foreach()方法显示 RDD 的内容。
carRDD.foreach(println)
Car(Tesla,Model X,80000.0,suv,electric)
Car(Tesla,Model 3,35000.0,sedan,electric)
Car(Tesla,Model S,71000.0,sedan,electric)
  1. 我们通过停止 Spark 会话来关闭程序。
spark.stop() 

它是如何工作的...

在本节中,我们将 RDD 转换为数据集,最终将其转换回 RDD。我们从一个 Scala 序列开始,然后将其更改为 RDD。创建 RDD 后,调用了 Spark 的会话createDataset()方法,将 RDD 作为参数传递,同时接收数据集作为结果。

接下来,数据集按制造商列进行分组,计算各种汽车制造商的存在。下一步涉及过滤制造商为特斯拉的数据集,并将结果转换回 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. 定义一个 Scala case 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 implicits,只需导入即可添加行为。
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. 接下来,在汽车数据集上创建一个视图,以便我们可以针对数据集执行一个文字 Spark SQL 查询字符串。
cars.createOrReplaceTempView("cars")
  1. 最后,我们执行一个 Spark SQL 查询,过滤数据集以获取电动汽车,并仅返回三个已定义的列。
spark.sql("select make, model, kind from cars where kind = 'electric'").show()

  1. 我们通过停止 Spark 会话来关闭程序。
spark.stop() 

它是如何工作的...

读取JavaScript 对象表示JSON)数据文件并将其转换为 Spark 数据集非常简单。在过去几年中,JSON 已经成为广泛使用的数据格式,Spark 对该格式的支持非常丰富。

在第一部分中,我们演示了通过 Spark 会话中内置的 JSON 解析功能将 JSON 加载到数据集中。您应该注意 Spark 的内置功能,它将 JSON 数据转换为汽车 case 类。

在第二部分中,我们演示了在数据集上应用 Spark SQL 来整理所述数据以达到理想状态。我们利用数据集的 select 方法检索make列,并应用distinct方法来去除重复项。接下来,我们在汽车数据集上设置了一个视图,以便我们可以对其应用 SQL 查询。最后,我们使用会话的 SQL 方法来执行针对数据集的文字 SQL 查询字符串,检索任何属于电动汽车的项目。

还有更多...

要完全理解和掌握数据集 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,大约有 2500 多行。探索 Spark 源代码是学习 Scala 高级编程、Scala 注解和 Spark 2.0 本身的最佳途径。

对于 Spark 2.0 之前的用户值得注意的是:

  • SparkSession 是系统的唯一入口点。SQLContext 和 HiveContext 已被 SparkSession 取代。

  • 对于 Java 用户,请确保用Dataset<Row>替换 DataFrame。

  • 通过 SparkSession 使用新的目录接口来执行cacheTable()dropTempView()createExternalTable()ListTable()等操作。

  • DataFrame 和 DataSet API

  • unionALL()已被弃用,现在应该使用union()

  • explode()应该被functions.explode()加上select()flatMap()替换

  • registerTempTable已被弃用,并被createOrReplaceTempView()取代

  • Dataset() API 源代码(即Dataset.scala)可以在 GitHub 上找到github.com/apache/spark/blob/master/sql/core/src/main/scala/org/apache/spark/sql/Dataset.scala

使用领域对象的 DataSet API 进行函数式编程

在这个示例中,我们探索了如何使用 DataSet 进行函数式编程。我们使用 DataSet 和函数式编程来按照汽车的型号(领域对象)进行分离。

如何做...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 使用 package 指令提供正确的路径

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.mutable
import scala.collection.mutable.ListBuffer
import org.apache.log4j.{Level, Logger}
import org.apache.spark.sql.SparkSession
  1. 定义一个 Scala case 来包含我们的数据进行处理,我们的汽车类将代表电动车和混合动力车。
case class Car(make: String, model: String, price: Double,
style: String, kind: String)
  1. 让我们创建一个包含电动车和混合动力车的Seq
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 集群和底层会话对象属性,如 SparkContext 和 SparkSQLContext。
val spark = SparkSession
.builder
.master("local[*]")
.appName("mydatasetseq")
.config("spark.sql.warehouse.dir", ".")
.getOrCreate()

  1. 导入 spark implicits,因此只需导入即可添加行为。
import spark.implicits._
  1. 现在,我们将使用 SparkSessions 的createDataset()函数从汽车数据 Seq 创建一个 DataSet。
val cars = spark.createDataset(MyDatasetData.carData)
  1. 显示数据集以了解如何在后续步骤中转换数据。
cars.show(false)

运行上述代码后,您将得到以下输出。

  1. 现在我们构建一个功能序列步骤,将原始数据集转换为按制造商分组的数据,并附上所有不同的型号。
val modelData = cars.groupByKey(_.make).mapGroups({
case (make, car) => {
val carModel = new ListBuffer[String]()
           car.map(_.model).foreach({
               c =>  carModel += c
         })
         (make, carModel)
        }
      })
  1. 让我们显示之前的函数逻辑序列的结果以进行验证。
  modelData.show(false)

运行上述代码后,您将得到以下输出。

  1. 通过停止 Spark 会话来关闭程序。
spark.stop()

它是如何工作的...

在这个例子中,我们使用 Scala 序列数据结构来保存原始数据,即一系列汽车及其属性。使用createDataset(),我们创建一个 DataSet 并填充它。然后,我们使用groupBymapGroups()来使用函数范式与 DataSet 列出汽车的型号。在 DataSet 之前,使用这种形式的函数式编程与领域对象并不是不可能的(例如,使用 RDD 的 case 类或使用 DataFrame 的 UDF),但是 DataSet 构造使这变得简单和内在化。

还有更多...

确保在所有的 DataSet 编码中包含implicits语句:

import spark.implicits._

另请参阅

数据集的文档可以在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 2.0 进行二元分类模型评估

  • 使用 Spark 2.0 进行多标签分类模型评估

  • 使用 Spark 2.0 进行多类别分类模型评估

  • 使用 Scala Breeze 库在 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, "type of correlation")

  • Pearson(默认)

  • Spearman

  • 分层抽样 - RDD API:

  • 使用替换 RDD

  • 无需替换 - 需要额外的传递

  • 假设检验:

  • 向量 - Statistics.chiSqTest( vector )

  • 矩阵 - Statistics.chiSqTest( dense matrix )

  • Kolmogorov-SmirnovKS)相等性检验 - 单侧或双侧:

  • 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.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. 将输出级别设置为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 Pipeline")
.config("spark.sql.warehouse.dir", ".")
.getOrCreate()
  1. 让我们创建一个包含几个随机文本文档的训练集 DataFrame:
val trainset = spark.createDataFrame(Seq(
 (1L, 1, "spark rocks"),
 (2L, 0, "flink is the best"),
 (3L, 1, "Spark rules"),
 (4L, 0, "mapreduce forever"),
 (5L, 0, "Kafka is great")
 )).toDF("id", "label", "words")
  1. 创建一个标记化器来解析文本文档为单独的词项:
val tokenizer = new Tokenizer()
 .setInputCol("words")
 .setOutputCol("tokens")
  1. 创建一个 HashingTF 来将词项转换为特征向量:
val hashingTF = new HashingTF()
 .setNumFeatures(1000)
 .setInputCol(tokenizer.getOutputCol)
 .setOutputCol("features")
  1. 创建一个逻辑回归类来生成一个模型,以预测一个新的文本文档属于哪个组:
val lr = new LogisticRegression()
 .setMaxIter(15)
 .setRegParam(0.01)
  1. 接下来,我们构建一个包含三个阶段的数据流水线:
val pipeline = new Pipeline()
 .setStages(Array(tokenizer, hashingTF, lr))
  1. 现在,我们训练模型,以便稍后进行预测:
val model = pipeline.fit(trainset)
  1. 让我们创建一个测试数据集来验证我们训练好的模型:
val testSet = spark.createDataFrame(Seq(
 (10L, 1, "use spark please"),
 (11L, 2, "Kafka")
 )).toDF("id", "label", "words")
  1. 最后,我们使用训练好的模型转换测试集,生成预测:
model.transform(testSet).select("probability", "prediction").show(false)

  1. 通过停止 Spark 会话来关闭程序:
spark.stop()

它是如何工作的...

在本节中,我们研究了如何使用 Spark 构建一个简单的机器学习流水线。我们首先创建了一个由两组文本文档组成的 DataFrame,然后设置了一个流水线。

首先,我们创建了一个分词器,将文本文档解析为术语,然后创建了 HashingTF 来将术语转换为特征。然后,我们创建了一个逻辑回归对象,以预测新文本文档属于哪个组。

其次,我们通过向其传递参数数组来构建管道,指定三个执行阶段。您会注意到每个后续阶段都将结果提供为指定的列,同时使用前一阶段的输出列作为输入。

最后,我们通过在管道对象上调用fit()并定义一组测试数据来训练模型以进行验证。接下来,我们使用模型转换测试集,确定测试集中的文本文档属于定义的两个组中的哪一个。

还有更多...

Spark ML 中的管道受到了 Python 中 scikit-learn 的启发,这里提供了参考:

scikit-learn.org/stable/

ML 管道使得在 Spark 中组合多个算法以实现生产任务变得容易。在现实情况下,很少会看到由单个算法组成的用例。通常,一些合作的 ML 算法一起工作以实现复杂的用例。例如,在基于 LDA 的系统(例如新闻简报)或人类情感检测中,核心系统之前和之后有许多步骤,需要作为单个管道来实现任何有意义且值得投入生产的系统。请参阅以下链接,了解需要使用管道来实现强大系统的真实用例:

www.thinkmind.org/index.php?view=article&articleid=achi_2013_15_50_20241

另见

更多多元统计摘要的文档:

使用 Spark 对数据进行归一化

在这个示例中,我们演示了在将数据导入 ML 算法之前对数据进行归一化(缩放)。有很多 ML 算法,比如支持向量机SVM),它们使用缩放后的输入向量而不是原始值效果更好。

如何做...

  1. 转到 UCI 机器学习库并下载archive.ics.uci.edu/ml/machine-learning-databases/wine/wine.data文件。

  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 模式并显示其中包含的数据:
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.SparkSession
import org.apache.log4j.{ Level, Logger}
  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("Data Splitting")
.getOrCreate()
  1. 我们首先通过 Spark 会话的csv()方法加载数据文件,以解析和加载数据到数据集中:
val data = spark.read.csv("../data/sparkml2/chapter4/newsCorpora.csv")
  1. 现在,我们计算 CSV 加载程序解析并加载到内存中的项目数。我们稍后需要这个值来调和数据拆分。
val rowCount = data.count()
println("rowCount=" + rowCount)
  1. 接下来,我们利用数据集的randomSplit方法将数据分成两个桶,每个桶分配 80%和 20%的数据:
val splitData = data.randomSplit(Array(0.8, 0.2))
  1. randomSplit方法返回一个包含两组数据的数组,第一组数据占 80%的训练集,下一组占 20%的测试集:
val trainingSet = splitData(0)
val testSet = splitData(1)
  1. 让我们为训练集和测试集生成计数:
val trainingSetCount = trainingSet.count()
val testSetCount = testSet.count()
  1. 现在我们对值进行调和,并注意到原始行数为415606,训练集和测试集的最终总和等于415606
println("trainingSetCount=" + trainingSetCount)
println("testSetCount=" + testSetCount)
println("setRowCount=" + (trainingSetCount+testSetCount))
rowCount=415606
trainingSetCount=332265
testSetCount=83341
setRowCount=415606
  1. 我们通过停止 Spark 会话来关闭程序:
spark.stop()

它是如何工作的...

我们首先加载数据文件newsCorpora.csv,然后通过附加到数据集对象的randomSplit()方法来拆分数据集。

还有更多...

为了验证结果,我们必须建立一个 Delphi 技术,其中测试数据对模型是完全未知的。有关详细信息,请参阅 Kaggle 竞赛www.kaggle.com/competitions

强大的 ML 系统需要三种类型的数据集:

  • 训练数据集:用于将模型拟合到样本

  • 验证数据集:用于估计由训练集拟合的模型的增量或预测误差

  • 测试数据集:用于在选择最终模型后评估模型的泛化误差

另请参阅

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 及更高版本中数据处理的未来方向。在第三章,Spark 的三大数据武士-机器学习的完美组合中,我们涵盖了三个详细的数据集示例,本章中我们涵盖了一些使用这些新 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就可以添加行为:
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函数对团队数据集进行横向遍历,生成一个新的城市名称数据集:
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 优化器

  • 数据集利用了钨的非堆内存管理

  • 在接下来的两年中,仍将有许多系统保持在 Spark 2.0 之前,因此出于实际原因,您仍必须学习和掌握 RDD 和 DataFrame。

另请参阅

数据集的文档可在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 会话,从而为 Spark 集群提供入口点:
val spark = SparkSession
.builder
.master("local[*]")
.appName("DatasetvsRDD")
.config("spark.sql.warehouse.dir", ".")
.getOrCreate()
  1. 在下面的代码块中,我们让 Spark 从文本文件中创建数据集对象。

文本文件包含非常简单的数据(每行包含逗号分隔的 ID 和名称):

import spark.implicits._

val ds = spark.read.textFile("../data/sparkml2/chapter4/beatles.txt").map(line => {
val tokens = line.split(",")
Beatle(tokens(0).toLong, tokens(1))
}).as[Beatle]

我们读取文件并解析文件中的数据。数据集对象由 Spark 创建。我们在控制台中确认类型,然后显示数据:

println("Dataset Type: " + ds.getClass)
ds.show()

从控制台输出:

Dataset Type: class org.apache.spark.sql.Dataset

  1. 现在我们使用与前一步非常相似的方式创建了一个包含相同数据文件的 RDD:
val rdd = spark.sparkContext.textFile("../data/sparkml2/chapter4/beatles.txt").map(line => {
val tokens = line.split(",")
Beatle(tokens(0).toLong, tokens(1))
 })

然后我们确认它是一个 RDD,并在控制台中显示数据:

println("RDD Type: " + rdd.getClass)
rdd.collect().foreach(println)

请注意,方法非常相似但不同。

从控制台输出:

RDD Type: class org.apache.spark.rdd.MapPartitionsRDD
Beatle(1,John)
Beatle(2,Paul)
Beatle(3,George)
Beatle(4,Ringo)
  1. DataFrame 是 Spark 社区常用的另一种数据结构。我们展示了使用相同的方法基于相同的数据文件创建 DataFrame 的类似方式:
val df = spark.read.text("../data/sparkml2/chapter4/beatles.txt").map(
 row => { // Dataset[Row]
val tokens = row.getString(0).split(",")
 Beatle(tokens(0).toLong, tokens(1))
 }).toDF("bid", "bname")

然后我们确认它是一个 DataFrame。

 println("DataFrame Type: " + df.getClass)
 df.show()

请注意DataFrame = Dataset[Row],因此类型是 Dataset。

从控制台输出:

DataFrame Type: class org.apache.spark.sql.Dataset

  1. 我们通过停止 Spark 会话来关闭程序:
spark.stop()

工作原理...

我们使用相同的文本文件使用类似的方法创建了一个 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 来运行数据的二元分类的逻辑回归。这里重点是 LabeledPoint,回归算法在第五章和第六章中有更深入的介绍,Spark 2.0 中的回归和分类的实用机器学习-第 I 部分Spark 2.0 中的回归和分类的实用机器学习-第 II 部分

如何做...

  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. 我们使用SparseVectorDenseVector创建 LabeledPoint。在以下代码块中,前四个 LabeledPoints 是由DenseVector创建的,最后两个 LabeledPoints 是由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()函数将集群类型设置为local。提供了一个注释,显示如何在特定端口上运行本地集群。

如果两者都存在,-D选项值将被代码中设置的集群主参数覆盖。

SparkSession对象中,我们通常使用master()函数,而在 Spark 2.0 之前,在SparkConf对象中,使用setMaster()函数。

以下是连接到不同模式的集群的三种示例方式:

  1. local模式下运行:
master("local") 
  1. 在集群模式下运行:
master("spark://yourmasterhostIP:port") 
  1. 传递主值:
 -Dspark.master=local

  1. 我们读取一个 CSV 文件,并使用以下代码将 CSV 文件解析为 Spark:
val df = spark.read
       .option("header","True")
       .csv("../data/sparkml2/chapter4/mySampleCSV.csv")
  1. 我们在控制台中显示 DataFrame:
df.show()
  1. 您将在控制台中看到以下内容:

  2. 然后通过停止 Spark 会话来关闭程序:

spark.stop()

它是如何工作的...

在这个例子中,我们展示了如何使用本地和远程选项连接到 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 2.0 文档中选择了一些我们喜欢和有趣的 API:
Def version: String

此应用程序运行的 Spark 版本:

使用 Spark 执行 SQL 查询,将结果作为DataFrame返回- 首选 Spark 2.0

SQLContext的包装版本,用于向后兼容。

Spark 的运行时配置界面。

用户可以通过它创建、删除、更改或查询底层数据库、表、函数等的接口。

  • Def newSession(): SparkSession

使用隔离的 SQL 配置和临时表启动一个新会话;注册的函数是隔离的,但共享底层的SparkContext和缓存数据。

一组用于注册用户定义函数(UDF)的方法。

我们可以通过 Spark 会话直接创建 DataFrame 和 Dataset。这是有效的,但在 Spark 2.0.0 中被标记为实验性的。

如果您要进行任何与 SQL 相关的工作,现在 SparkSession 是访问 Spark SQL 的入口点。SparkSession 是您必须创建的第一个对象,以创建 Spark SQL 应用程序。

另请参阅

SparkSession API 文档的文档可在spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.sql.SparkSession上找到。

在 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()函数来设置集群主位置。正如您所看到的,我们是在local模式下运行代码。

如果两者都存在,那么-D选项值将被代码中设置的集群主参数覆盖)。

以下是连接到不同模式下集群的三种示例方式:

  1. 在本地模式下运行:
setMaster("local")
  1. 在集群模式下运行:
setMaster("spark://yourmasterhostIP:port")
  1. 通过以下方式传递主值:
-Dspark.master=local

  1. 我们使用上述 SparkContext 来读取 CSV 文件并使用以下代码将 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 之前,通过SparkContext获取对 Spark 集群的访问。

对子系统的访问,如 SQL,是特定名称上下文(例如,SQLContext**)。

Spark 2.0 通过创建一个统一的访问点(即SparkSession)来改变我们访问集群的方式。

另请参阅

SparkContext 的文档可在spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.SparkContext上找到。

在 Spark 2.0 中通过 SparkSession 对象访问 SparkContext

在这个示例中,我们演示了如何使用 SparkSession 对象在 Spark 2.0 中获取 SparkContext。这个示例将演示从 RDD 到数据集的创建、使用和来回转换。这是重要的原因是,即使我们更喜欢数据集,我们仍然必须能够使用和增强大部分使用 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.SparkSession
import scala.util.Random
  1. 将输出级别设置为ERROR以减少 Spark 的日志输出:
Logger.getLogger("org").setLevel(Level.ERROR)
  1. 使用构建模式初始化 Spark 会话,从而为 Spark 集群提供入口点:
val session = SparkSession
.builder
.master("local[*]")
.appName("SessionContextRDD")
.config("spark.sql.warehouse.dir", ".")
.getOrCreate()
  1. 我们首先展示了如何使用sparkContext创建 RDD。以下代码示例在 Spark 1.x 中非常常见:
import session.implicits._

 // SparkContext
val context = session.sparkContext

我们获取SparkContext对象:

println("SparkContext")

val rdd1 = context.makeRDD(Random.shuffle(1 to 10).toList)
rdd1.collect().foreach(println)
println("-" * 45)

val rdd2 = context.parallelize(Random.shuffle(20 to 30).toList)
rdd2.collect().foreach(println)
println("\n End of SparkContext> " + ("-" * 45))

我们首先从makeRDD方法创建了rdd1并在控制台中显示了 RDD:

SparkContext
4
6
1
10
5
2
7
3
9
8

然后我们使用parallelize方法生成了rdd2,并在控制台中显示了 RDD 中的数据。

从控制台输出:

25
28
30
29
20
22
27
23
24
26
21
 End of SparkContext
  1. 现在我们展示了使用session对象创建数据集的方法:
val dataset1 = session.range(40, 50)
 dataset1.show()

val dataset2 = session.createDataset(Random.shuffle(60 to 70).toList)
 dataset2.show()

我们使用不同的方法生成了dataset1dataset2

从控制台输出:

对于 dataset1:

对于 dataset2:

  1. 我们展示了如何从数据集中检索基础 RDD:
// retrieve underlying RDD from Dataset
val rdd3 = dataset2.rdd
rdd3.collect().foreach(println)

从控制台输出:

61
68
62
67
70
64
69
65
60
66
63
  1. 以下代码块显示了将 RDD 转换为数据集对象的方法:
// convert rdd to Dataset
val rdd4 = context.makeRDD(Random.shuffle(80 to 90).toList)
val dataset3 = session.createDataset(rdd4)
dataset3.show()

从控制台输出:

  1. 通过停止 Spark 会话来关闭程序:
session.stop()

它是如何工作的...

我们使用 SparkContext 创建了 RDD;这在 Spark 1.x 中被广泛使用。我们还演示了在 Spark 2.0 中使用 Session 对象创建数据集的方法。在生产中,来回转换是必要的,以处理 Spark 2.0 之前的代码。

这个示例的技术信息是,虽然 DataSet 是未来数据处理的首选方法,但我们始终可以使用 API 来回转换为 RDD,反之亦然。

还有更多...

有关数据类型的更多信息,请访问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 导出的支持目前仅限于:

  • 线性回归

  • 逻辑回归

  • 岭回归

  • 套索

  • 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 提供了RegressionMetrics工具,它具有基本的统计功能,如均方误差(MSE),R-Squared 等。

这个示例的目标是了解 Spark 开箱即用提供的评估指标。最好集中在第 8 步,因为我们在第五章中更详细地介绍了回归,Spark 2.0 中的实用机器学习-回归和分类第一部分和第六章,Spark 2.0 中的实用机器学习-回归和分类第二部分以及整本书中。

如何做...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter4
  1. 导入必要的包以便 SparkContext 可以访问集群:
import org.apache.spark.mllib.evaluation.RegressionMetrics
import org.apache.spark.mllib.linalg.Vectors
import org.apache.spark.mllib.regression.LabeledPoint
import org.apache.spark.mllib.tree.DecisionTree
import org.apache.spark.sql.SparkSession
  1. 创建 Spark 的配置和 SparkContext:
val spark = SparkSession
.builder
.master("local[*]")
.appName("myRegressionMetrics")
.config("spark.sql.warehouse.dir", ".")
.getOrCreate()
  1. 我们利用威斯康星州乳腺癌数据集作为回归模型的示例数据集。

威斯康星州乳腺癌数据集是从威斯康星大学医院的 William H Wolberg 博士那里获得的。数据集是定期获得的,因为 Wolberg 博士报告了他的临床病例。

有关数据集的更多详细信息可以在第九章中找到,优化-使用梯度下降进行下坡

val rawData = spark.sparkContext.textFile("../data/sparkml2/chapter4/breast-cancer-wisconsin.data")
val data = rawData.map(_.trim)
   .filter(text => !(text.isEmpty || text.indexOf("?") > -1))
   .map { line =>
 val values = line.split(',').map(_.toDouble)
 val slicedValues = values.slice(1, values.size)
 val featureVector = Vectors.dense(slicedValues.init)
 val label = values.last / 2 -1
      LabeledPoint(label, featureVector)

   }

我们将数据加载到 Spark 中,并过滤数据中的缺失值。

  1. 我们将数据集按 70:30 的比例分割成两个数据集,一个用于训练模型,另一个用于测试模型:
val splits = data.randomSplit(Array(0.7, 0.3))
val (trainingData, testData) = (splits(0), splits(1))
  1. 我们设置参数并使用DecisionTree模型,在训练数据集之后,我们使用测试数据集进行预测:
val categoricalFeaturesInfo = Map[Int, Int]()
val impurity = "variance" val maxDepth = 5
val maxBins = 32

val model = DecisionTree.trainRegressor(trainingData, categoricalFeaturesInfo, impurity,
maxDepth, maxBins)
val predictionsAndLabels = testData.map(example =>
(model.predict(example.features), example.label)
)
  1. 我们实例化RegressionMetrics对象并开始评估:
val metrics = new RegressionMetrics(predictionsAndLabels)
  1. 我们在控制台中打印出统计值:
// Squared error
println(s"MSE = ${metrics.meanSquaredError}")
 println(s"RMSE = ${metrics.rootMeanSquaredError}")

 // R-squared
println(s"R-squared = ${metrics.r2}")

 // Mean absolute error
println(s"MAE = ${metrics.meanAbsoluteError}")

 // Explained variance
println(s"Explained variance = ${metrics.explainedVariance}")

从控制台输出:

MSE = 0.06071332254584681
RMSE = 0.2464007356844675
R-squared = 0.7444017305996473
MAE = 0.0691747572815534
Explained variance = 0.22591111058744653
  1. 然后我们通过停止 Spark 会话来关闭程序:
spark.stop()

它是如何工作的...

在这个示例中,我们探讨了生成回归度量标准来帮助我们评估回归模型。我们开始加载一个乳腺癌数据文件,然后将其按 70/30 的比例分割,以创建训练和测试数据集。接下来,我们训练了一个DecisionTree回归模型,并利用它对我们的测试集进行了预测。最后,我们拿到了预测结果,并生成了回归度量标准,这给了我们平方误差,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 曲线下面积,阈值等)来评估它。

我们建议您专注于第 8 步,因为我们在第五章中更详细地介绍了回归,使用 Spark 2.0 进行回归和分类的实际机器学习-第 I 部分和第六章中更详细地介绍了回归和分类的实际机器学习-第 II 部分*。

如何做...

  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 个特征,其中六个是连续的,八个是分类的。在这个数据集中,连续特征被离散化为分位数,并且每个分位数由一个二进制特征表示。我们修改了数据以适应代码的目的。数据集特征的详细信息可以在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. 我们在控制台中按Threashold打印出精度:
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. 我们在控制台中打印出Area Under Precision Recall Curve
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.apache.org/docs/latest/api/scala/index.html#org.apache.spark.mllib.evaluation.BinaryClassificationMetrics

使用 Spark 2.0 进行多类分类模型评估

在这个示例中,我们探讨了MulticlassMetrics,它允许您评估将输出分类到两个以上标签的模型(例如,红色、蓝色、绿色、紫色、不知道)。它突出了混淆矩阵(confusionMatrix)和模型准确性的使用。

如何做...

  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.MulticlassMetrics
import org.apache.spark.mllib.regression.LabeledPoint
import org.apache.spark.mllib.util.MLUtils
  1. 创建 Spark 的配置和 SparkContext:
val spark = SparkSession
.builder
.master("local[*]")
.appName("myMulticlass")
.config("spark.sql.warehouse.dir", ".")
.getOrCreate()
  1. 我们下载了最初来自 UCI 的数据集,并对其进行修改以适应代码的需要:
// Load training data in LIBSVM format
//https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/multiclass.html
val data = MLUtils.loadLibSVMFile(spark.sparkContext, "../data/sparkml2/chapter4/myMulticlassIrisData.txt")

数据集是一个修改后的数据集。原始的鸢尾花植物数据集有四个特征。我们修改了数据以适应代码的目的。数据集特征的详细信息可以在 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(3)
 .run(training)
  1. 我们在测试数据集上计算原始分数:
val predictionAndLabels = test.map { case LabeledPoint(label, features) =>
 val prediction = model.predict(features)
 (prediction, label)
 }
  1. 我们从预测中创建了MulticlassMetrics对象,并开始对指标进行评估:
val metrics = new MulticlassMetrics(predictionAndLabels)
  1. 我们在控制台中打印出混淆矩阵:
println("Confusion matrix:")
println(metrics.confusionMatrix)

从控制台输出:

Confusion matrix: 
18.0 0.0 0.0 
0.0 15.0 8.0 
0.0 0.0 22.0
  1. 我们在控制台中打印出总体统计信息:
val accuracy = metrics.accuracy
println("Summary Statistics")
println(s"Accuracy = $accuracy")

从控制台输出:

Summary Statistics
Accuracy = 0.873015873015873
  1. 我们在控制台中按标签值打印出精度:
val labels = metrics.labels
labels.foreach { l =>
 println(s"Precision($l) = " + metrics.precision(l))
 }

从控制台输出:

Precision(0.0) = 1.0
Precision(1.0) = 1.0
Precision(2.0) = 0.7333333333333333
  1. 我们在控制台中按标签打印出召回率:
labels.foreach { l =>
println(s"Recall($l) = " + metrics.recall(l))
 }

从控制台输出:

Recall(0.0) = 1.0
Recall(1.0) = 0.6521739130434783
Recall(2.0) = 1.0
  1. 我们在控制台中按标签打印出假阳性率:
labels.foreach { l =>
 println(s"FPR($l) = " + metrics.falsePositiveRate(l))
 }

从控制台输出:

FPR(0.0) = 0.0
FPR(1.0) = 0.0
FPR(2.0) = 0.1951219512195122
  1. 我们在控制台中按标签打印出 F-度量:
labels.foreach { l =>
 println(s"F1-Score($l) = " + metrics.fMeasure(l))
 }

从控制台输出:

F1-Score(0.0) = 1.0
F1-Score(1.0) = 0.7894736842105263
F1-Score(2.0) = 0.846153846153846
  1. 我们在控制台中打印出加权统计值:
println(s"Weighted precision: ${metrics.weightedPrecision}")
 println(s"Weighted recall: ${metrics.weightedRecall}")
 println(s"Weighted F1 score: ${metrics.weightedFMeasure}")
 println(s"Weighted false positive rate: ${metrics.weightedFalsePositiveRate}")

从控制台输出:

Weighted precision: 0.9068783068783068
Weighted recall: 0.873015873015873
Weighted F1 score: 0.8694171325750273
Weighted false positive rate: 0.06813782423538521
  1. 然后通过停止 Spark 会话来关闭程序:
spark.stop()

它是如何工作的...

在这个教程中,我们探索了为多类别模型生成评估指标。首先,我们将 Iris 数据加载到内存中,并将其按比例 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

每个参数的详细解释超出了范围,但以下链接提供了多标签指标的简短处理:

en.wikipedia.org/wiki/Multi-label_classification

另请参阅

多标签分类指标的文档:

在 Spark 2.0 中使用 Scala Breeze 库进行图形处理

在这个示例中,我们将使用 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. 在 Windows 机器上的C:\spark-2.0.0-bin-hadoop2.7\examples\jars目录中放置 JAR 文件:

  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 会话可以访问集群和log4j.Logger以减少 Spark 产生的输出量:
import org.apache.log4j.{Level, Logger}
import org.apache.spark.sql.SparkSession
import breeze.plot._

import scala.util.Random
  1. 将输出级别设置为ERROR以减少 Spark 的日志输出:
Logger.getLogger("org").setLevel(Level.ERROR)
  1. 通过使用构建器模式指定配置来初始化 Spark 会话,从而为 Spark 集群提供入口点:
val spark = SparkSession
.builder
.master("local[*]")
.appName("myBreezeChart")
.config("spark.sql.warehouse.dir", ".")
.getOrCreate()
  1. 现在我们创建图形对象,并设置图形的参数:
import spark.implicits._

val fig = Figure()
val chart = fig.subplot(0)

chart.title = "My Breeze-Viz Chart" chart.xlim(21,100)
chart.ylim(0,100000)
  1. 我们从随机数创建一个数据集,并显示数据集。

  2. 数据集将在以后使用。

val ages = spark.createDataset(Random.shuffle(21 to 100).toList.take(45)).as[Int]

 ages.show(false)

从控制台输出:

  1. 我们收集数据集,并设置xy轴。

  2. 对于照片部分,我们将数据类型转换为 double,并将值派生为y2

  3. 我们使用 Breeze 库的 scatter 方法将数据放入图表中,并使用 Breeze 的 plot 方法绘制对角线:

val x = ages.collect()
val y = Random.shuffle(20000 to 100000).toList.take(45)

val x2 = ages.collect().map(xx => xx.toDouble)
val y2 = x2.map(xx => (1000 * xx) + (xx * 2))

chart += scatter(x, y, _ => 0.5)
chart += plot(x2, y2)

chart.xlabel = "Age" chart.ylabel = "Income" fig.refresh()
  1. 我们为x轴和y轴设置标签,并刷新图形对象。

  2. 以下是生成的 Breeze 图表:

  1. 通过停止 Spark 会话来关闭程序:
spark.stop()

它是如何工作的...

在这个示例中,我们从随机数中创建了一个 Spark 数据集。然后创建了一个 Breeze 图,并设置了基本参数。我们从创建的数据集中派生了xy数据。

我们使用 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 2.0 中进行回归和分类的实用机器学习-第一部分

在本章中,我们将涵盖以下内容:

  • 将线性回归线拟合到数据的传统方法

  • Spark 2.0 中的广义线性回归

  • Spark 2.0 中具有 Lasso 和 L-BFGS 的线性回归 API

  • Spark 2.0 中具有 Lasso 和自动优化选择的线性回归 API

  • Spark 2.0 中具有岭回归和自动优化选择的线性回归 API

  • Apache Spark 2.0 中的等保回归

  • Apache Spark 2.0 中的多层感知器分类器

  • Apache Spark 2.0 中的一对多分类器(One-vs-All)

  • Apache Spark 2.0 中的生存回归-参数 AFT 模型

介绍

本章与下一章一起,涵盖了 Spark 2.0 ML 和 MLlib 库中可用的回归和分类的基本技术。Spark 2.0 通过将基于 RDD 的回归(见下一章)移动到维护模式来突显新的方向,同时强调线性回归广义回归

在高层次上,新的 API 设计更倾向于对弹性网的参数化,以产生岭回归与 Lasso 回归以及两者之间的一切,而不是命名 API(例如,LassoWithSGD)。新的 API 方法是一个更清晰的设计,并迫使您学习弹性网及其在特征工程中的作用,这在数据科学中仍然是一门艺术。我们提供充分的例子、变化和注释,以指导您应对这些技术中的复杂性。

以下图表描述了本章中回归和分类覆盖范围(第一部分):

首先,您将学习如何使用代数方程通过 Scala 代码和 RDD 从头开始实现线性回归,以便了解数学和为什么我们需要迭代优化方法来估计大规模回归系统的解决方案。其次,我们探讨广义线性模型GLM)及其各种统计分布家族和链接函数,同时强调当前实现中仅限于 4,096 个参数的限制。第三,我们解决线性回归模型LRM)以及如何使用弹性网参数化来混合和匹配 L1 和 L2 惩罚函数,以实现逻辑回归、岭回归、Lasso 等。我们还探讨了求解器(即优化器)方法以及如何设置它以使用 L-BFGS 优化、自动优化选择等。

在探索 GLM 和线性回归配方之后,我们继续提供更多外来的回归/分类方法的配方,例如等保回归、多层感知器(即神经元网络的形式)、一对多和生存回归,以展示 Spark 2.0 处理线性技术无法解决的情况的能力和完整性。随着 21 世纪初金融世界风险的增加和基因组的新进展,Spark 2.0 还将四种重要方法(等保回归、多层感知器、一对多和生存回归或参数 AFT)整合到一个易于使用的机器学习库中。规模化的参数 AFT 方法应该特别受到金融、数据科学家或精算专业人士的关注。

尽管一些方法,比如LinearRegression() API,从理论上来说自 1.3x+版本就已经可用,但重要的是要注意,Spark 2.0 将它们全部整合到一个易于使用和可维护的 API 中(即向后兼容),以 glmnet R 的方式移动基于 RDD 的回归 API 到维护模式。L-BFGS 优化器和正规方程占据主导地位,而 SGD 在 RDD-based APIs 中可用以实现向后兼容。

弹性网络是首选方法,不仅可以绝对处理 L1(Lasso 回归)和 L2(岭回归)的正则化方法,还可以提供类似拨盘的机制,使用户能够微调惩罚函数(参数收缩与选择)。虽然我们在 1.4.2 中使用了弹性网功能,但 Spark 2.0 将所有内容整合在一起,无需处理每个单独的 API 进行参数调整(根据最新数据动态选择模型时很重要)。当我们开始深入研究这些配方时,我们强烈鼓励用户探索各种参数设置setElasticNetParam()setSolver()配置,以掌握这些强大的 API。重要的是不要混淆惩罚函数setElasticNetParam(value: Double)(L1,L2,OLs,弹性网:线性混合 L1/L2),这些是正则化或模型惩罚方案与与成本函数优化技术相关的优化(正常,L-BFGS,自动等)技术。

需要注意的是,基于 RDD 的回归仍然非常重要,因为有很多当前的 ML 实现系统严重依赖于以前的 API 体系及其 SGD 优化器。请参阅下一章,了解基于 RDD 的回归的完整处理和教学笔记。

用传统的方法将线性回归线拟合到数据

在这个配方中,我们使用 RDD 和封闭形式公式从头开始编写一个简单的线性方程。我们之所以将这个作为第一个配方,是为了演示您可以始终通过 RDD 实现任何给定的统计学习算法,以实现使用 Apache Spark 的计算规模。

如何做...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序所在的包位置:

package spark.ml.cookbook.chapter5
  1. 导入必要的包,以便SparkSession获得对集群的访问,以及log4j.Logger以减少 Spark 产生的输出量:
import org.apache.spark.sql.SparkSession
import scala.math._
import org.apache.log4j.Logger
import org.apache.log4j.Level
  1. 使用构建模式初始化SparkSession,指定配置,从而使 Spark 集群的入口点可用:
val spark = SparkSession
 .builder
 .master("local[4]")
 .appName("myRegress01_20")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()
  1. 将输出级别设置为ERROR以减少 Spark 的输出:
Logger.getLogger("org").setLevel(Level.ERROR)
Logger.getLogger("akka").setLevel(Level.ERROR)
  1. 我们创建两个数组,表示因变量(即y)和自变量(即x):
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. 我们使用sc.parallelize(x)将两个数组转换为 RDD:
val xRDD = sc.parallelize(x)
val yRDD = sc.parallelize(y)
  1. 在这一步中,我们演示了 RDD 的zip()方法,它从两个 RDD 中创建因变量/自变量元组*(y,x)*。我们介绍这个函数,因为您经常需要学习如何在机器学习算法中使用成对工作:
val zipedRDD = xRDD.zip(yRDD)
  1. 为了确保我们理解zip()功能,让我们来看一下输出,但一定要包括collect()或其他形式的操作,以确保数据按顺序呈现。如果我们不使用操作方法,RDD 的输出将是随机的:

  1. 这是一个重要的步骤,演示了如何迭代、访问和计算每个成员。为了计算回归线,我们需要计算和、乘积和平均值(即sum(x)sum(y)sum (x * y))。map(_._1).sum()函数是一种机制,RDD 对被迭代,但只考虑第一个元素:
val xSum = zipedRDD.map(_._1).sum()
val ySum = zipedRDD.map(_._2).sum()
val xySum= zipedRDD.map(c => c._1 * c._2).sum()
  1. 在这一步中,我们继续计算每个 RDD 对成员的平均值以及它们的乘积。这些单独的计算(即mean(x)mean(y)mean(xy)*),以及平均平方,将用于计算回归线的斜率和截距。虽然我们可以从前面的统计数据中手动计算平均值,但我们应该确保熟悉 RDD 内在可用的方法:
val n= zipedRDD.count() 
val xMean = zipedRDD.map(_._1).mean()
val yMean = zipedRDD.map(_._2).mean()
val xyMean = zipedRDD.map(c => c._1 * c._2).mean()
  1. 这是最后一步,我们计算xy的平方的平均值:
val xSquaredMean = zipedRDD.map(_._1).map(x => x * x).mean()
val ySquaredMean = zipedRDD.map(_._2).map(y => y * y).mean()
  1. 我们打印统计信息以供参考:
println("xMean yMean xyMean", xMean, yMean, xyMean) 
xMean yMean xyMean ,26.16,25.16,989.08 
  1. 我们计算公式的分子分母
val numerator = xMean * yMean  - xyMean
val denominator = xMean * xMean - xSquaredMean
  1. 我们最终计算回归线的斜率:
val slope = numerator / denominator
println("slope %f5".format(slope))

slope 0.9153145 
  1. 现在我们计算截距并打印。如果你不想要截距(截距设置为0),那么斜率的公式需要稍作修改。你可以在其他来源(如互联网)中寻找更多细节并找到所需的方程:
val b_intercept = yMean - (slope*xMean)
println("Intercept", b_intercept) 

Intercept,1.21
  1. 使用斜率和截距,我们将回归线方程写成如下形式:
Y = 1.21 + .9153145 * X

它是如何工作的...

我们声明了两个 Scala 数组,将它们并行化为两个分开的x()y()的 RDD,然后使用 RDD API 中的zip()方法产生了一个成对的(即,压缩的)RDD。它产生了一个 RDD,其中每个成员都是一个*(x,y)*对。然后我们继续计算均值,总和等,并应用上述封闭形式的公式来找到回归线的截距和斜率。

在 Spark 2.0 中,另一种选择是直接使用 GLM API。值得一提的是,GLM 支持的封闭正态形式方案的最大参数数量限制为 4,096。

我们使用了封闭形式的公式来证明与一组数字(*Y1,X1),...,(Yn,Xn)*相关联的回归线简单地是最小化平方误差和的线。在简单的回归方程中,该线如下:

  • 回归线的斜率 

  • 回归线的偏移 

  • 回归线的方程 

回归线简单地是最小化平方误差和的最佳拟合线。对于一组点(因变量,自变量),有许多直线可以穿过这些点并捕捉到一般的线性关系,但只有其中一条线是最小化所有拟合误差的线。

例如,我们呈现了线Y = 1.21 + .9153145 * X。下图显示了这样一条线,我们使用封闭形式的公式计算了斜率和偏移。线性模型由线性方程表示,代表了我们使用封闭形式公式得到的给定数据的最佳线性模型(斜率=.915345截距=1.21):

在前面的图中绘制的数据点如下:

(Y, X)
(5.0,    1.0) 
(8.0,    4.0) 
(10.0,   11.0) 
(15.0,   25.0) 
(21.0,   18.0) 
(27.0,   33.0) 
(30.0,   20.0) 
(38.0,   30.0) 
(45.0,   43.0) 
(50.0,   55.0) 
(64.0,   57.0) 

还有更多...

值得注意的是,并非所有的回归形式都有封闭形式的公式,或者在大型数据集上变得非常低效(即,不切实际)-这就是我们使用 SGD 或 L-BFGS 等优化技术的原因。

从之前的教程中,你应该确保缓存与机器学习算法相关的任何 RDD 或数据结构,以避免由于 Spark 优化和维护血统(即,延迟实例化)的方式而导致的延迟实例化。

另请参阅

我们推荐一本来自斯坦福大学的书,可以从以下网站免费下载。无论你是新手还是高级从业者,这都是一本经典必读的书:

《统计学习的要素,数据挖掘,推断和预测,第二版》,Hastie,Tibshirani 和 Friedman(2009)。Springer-Verlag (web.stanford.edu/~hastie/ElemStatLearn/)。

Spark 2.0 中的广义线性回归

本教程涵盖了 Spark 2.0 中的广义回归模型GLM)实现。Spark 2.0 中的GeneralizedLinearRegression与 R 中的glmnet实现之间存在很大的相似性。这个 API 是一个受欢迎的补充,允许你选择和设置分布族(例如,高斯)和链接函数(例如,反对数)的 API。

如何做...

  1. 我们使用了 UCI 机器库存储中的房屋数据集。

  2. 从以下网址下载整个数据集:

数据集由 14 列组成,前 13 列是自变量(即特征),试图解释美国波士顿自住房的中位价格(即最后一列)。

我们已经选择并清理了前八列作为特征。我们使用前 200 行来训练和预测房价的中位数:

    • CRIM:按城镇人均犯罪率
  • ZN:超过 25,000 平方英尺的住宅用地比例

  • INDUS:每个城镇的非零售业务面积比例

  • CHAS:查尔斯河虚拟变量(如果地块与河流相接则为 1;否则为 0)

  • NOX:一氧化氮浓度(每千万份之一)

  • RM:每个住宅的平均房间数

  • AGE:1940 年前建成的自住单位比例

  1. 请使用housing8.csv文件,并确保将其移动到以下目录:
../data/sparkml2/chapter5/housing8.csv
  1. 在 IntelliJ 或您选择的 IDE 中开始一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序所在的包位置:

package spark.ml.cookbook.chapter5.
  1. 导入SparkSession所需的必要包,以便访问集群和log4j.Logger以减少 Spark 产生的输出量:
import org.apache.spark.ml.feature.LabeledPoint
import org.apache.spark.ml.linalg.Vectors
import org.apache.spark.ml.regression.GeneralizedLinearRegression
import org.apache.spark.sql.SparkSession
import org.apache.log4j.{Level, Logger}
  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("GLR")
.config("spark.sql.warehouse.dir", ".")
.getOrCreate()
  1. 我们需要导入数据转换例程的隐式:
import spark.implicits._
  1. 接下来,我们将住房数据加载到数据集中:
val data = spark.read.textFile( "../data/sparkml2/ /chapter5/housing8.csv" ).as[ String ]
  1. 让我们解析住房数据并将其转换为标签点:
val regressionData = data.map { line =>
val columns = line.split(',')
LabeledPoint(columns(13).toDouble , Vectors.dense(columns(0).toDouble,columns(1).toDouble, columns(2).toDouble, columns(3).toDouble,columns(4).toDouble,
columns(5).toDouble,columns(6).toDouble, columns(7).toDouble))
}
  1. 现在使用以下代码显示加载的数据:
regressionData.show(false)

输出如下所示:

  1. 接下来,我们为生成一个新模型配置了一个广义线性回归算法:
val glr = new GeneralizedLinearRegression()
.setMaxIter(1000)
.setRegParam(0.03) //the value ranges from 0.0 to 1.0\. Experimentation required to identify the right value.
.setFamily("gaussian")
.setLink( "identity" )

请随意尝试不同的参数以获得更好的拟合效果。

  1. 我们将模型拟合到住房数据:
val glrModel = glr.fit(regressionData)
  1. 然后,我们检索摘要数据以判断模型的准确性:
val summary = glrModel.summary
  1. 最后,我们打印出摘要统计信息:
val summary = glrModel.summary
summary.residuals().show()
println("Residual Degree Of Freedom: " + summary.residualDegreeOfFreedom)
println("Residual Degree Of Freedom Null: " + summary.residualDegreeOfFreedomNull)
println("AIC: " + summary.aic)
println("Dispersion: " + summary.dispersion)
println("Null Deviance: " + summary.nullDeviance)
println("Deviance: " +summary.deviance)
println("p-values: " + summary.pValues.mkString(","))
println("t-values: " + summary.tValues.mkString(","))
println("Coefficient Standard Error: " + summary.coefficientStandardErrors.mkString(","))
}

  1. 通过停止SparkSession来关闭程序:
spark.stop()

工作原理...

在这个示例中,我们展示了广义线性回归算法的运行情况。我们首先将 CSV 文件加载和解析为数据集。接下来,我们创建了一个广义线性回归算法,并通过将数据集传递给fit()方法来生成一个新模型。完成拟合操作后,我们从模型中检索摘要统计信息,并显示计算出的值以调整准确性。

在这个例子中,我们探索了使用高斯分布和身份拟合数据,但还有许多其他配置可以用来解决特定的回归拟合问题,这些将在下一节中解释。

还有更多...

Spark 2.0 中的 GLM 是一个通用的回归模型,可以支持许多配置。我们对 Spark 2.0.0 初始版本提供的众多系列印象深刻。

重要的是要注意,截至 Spark 2.0.2:

  • 目前回归的最大参数数量限制为 4,096 个。

  • 目前唯一支持的优化(即求解器)是迭代重新加权最小二乘法IRLS),这也是默认求解器。

  • 当您将求解器设置为auto时,它默认为 IRLS。

  • setRegParam()设置 L2 正则化的正则化参数。根据 Spark 2.0 文档,正则化项为0.5 * regParam * L2norm(coefficients)² - 请确保您理解其影响。

如果您不确定如何处理分布拟合,我们强烈推荐我们最喜欢的书之一,用 R 拟合统计分布手册,在建模芝加哥期货交易所小麦等农产品时为我们提供了很好的帮助,它具有反向波动率笑曲线(与股票非常不同)。

配置和可用选项如下:

一定要尝试不同的族和链接函数,以确保您对基础分布的假设是正确的。

另请参阅

GeneralizedLinearRegression()的文档可在以下链接找到:

spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.ml.regression.GeneralizedLinearRegression

GeneralizedLinearRegression中的一些重要 API 调用:

  • def **setFamily**(value: String): GeneralizedLinearRegression.this.type

  • def **setLink**(value: String): GeneralizedLinearRegression.this.type

  • def **setMaxIter**(value: Int): GeneralizedLinearRegression.this.type

  • def **setRegParam**(value: Double): GeneralizedLinearRegression.this.type

  • def **setSolver**(value: String): GeneralizedLinearRegression.this.type

  • def **setFitIntercept**(value: Boolean): GeneralizedLinearRegression.this.type

求解器目前是 IRLS;快速参考可在以下链接找到:

en.wikipedia.org/wiki/Iteratively_reweighted_least_squares

要完全理解 Spark 2.0+中 GLM 和线性回归的新方法,请务必参考并了解 R 中 CRAN glmnet 的实现:

Spark 2.0 中带有 Lasso 和 L-BFGS 的线性回归 API

在这个示例中,我们将演示如何使用 Spark 2.0 的LinearRegression() API 来展示一个统一/参数化的 API,以全面的方式处理线性回归,能够在不会出现 RDD 命名 API 的向后兼容问题的情况下进行扩展。我们展示如何使用setSolver()将优化方法设置为一阶内存高效的 L-BFGS,它可以轻松处理大量参数(即在稀疏配置中)。

在这个示例中,.setSolver()设置为lbgfs,这使得 L-BFGS(详见基于 RDD 的回归)成为选择的优化方法。.setElasticNetParam()未设置,因此默认值0仍然有效,这使得这是一个 Lasso 回归。

如何做...

  1. 我们使用 UCI 机器库存储中的住房数据集。

  2. 从以下链接下载整个数据集:

数据集由 14 列组成,前 13 列是独立变量(即特征),试图解释美国波士顿自住房的中位价格(即最后一列)。

我们选择并清理了前八列作为特征。我们使用前 200 行来训练和预测中位价格:

    • CRIM:按城镇划分的人均犯罪率
  • ZN:用于 25,000 平方英尺以上地块的住宅用地比例

  • INDUS:每个城镇的非零售业务土地比例

  • CHAS:查尔斯河虚拟变量(如果地块与河流相接,则为1;否则为0

  • NOX:一氧化氮浓度(每千万分之一)

  • RM:每个住宅的平均房间数

  • AGE:1940 年前建成的自住单位比例

  1. 请使用housing8.csv文件,并确保将其移动到以下目录:
../data/sparkml2/chapter5/housing8.csv
  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序所在的包位置:

package spark.ml.cookbook.chapter5.
  1. 导入必要的包,以便SparkSession访问集群和log4j.Logger减少 Spark 产生的输出量:
import org.apache.spark.ml.regression.LinearRegression
import org.apache.spark.ml.feature.LabeledPoint
import org.apache.spark.sql.SparkSession
import org.apache.spark.ml.linalg.Vectors
import org.apache.log4j.{Level, Logger}
  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("myRegress02")
.config("spark.sql.warehouse.dir", ".")
.getOrCreate()

  1. 我们需要导入数据转换例程的隐式:
import spark.implicits._
  1. 接下来,我们将房屋数据加载到数据集中:
val data = spark.read.text(
  "../data/sparkml2/chapter5/housing8.csv"
).as[
  String
]

  1. 让我们解析房屋数据并将其转换为标签点:
val RegressionDataSet = data.map { line =>
val columns = line.split(',')
LabeledPoint(columns(13).toDouble , Vectors.dense(columns(0).toDouble,columns(1).toDouble, columns(2).toDouble, columns(3).toDouble,columns(4).toDouble,
columns(5).toDouble,columns(6).toDouble, columns(7).toDouble
))
}
  1. 现在显示加载的数据:
RegressionDataSet.show(false)

输出如下所示:

  1. 接下来,我们配置线性回归算法以生成模型:
val numIterations = 10
val lr = new LinearRegression()
.setMaxIter(numIterations)
.setSolver("l-bfgs")
  1. 现在我们将模型拟合到房屋数据中:
val myModel = lr.fit(RegressionDataSet)
  1. 接下来,我们检索摘要数据以调和模型的准确性:
val summary = myModel.summary
  1. 最后,我们打印出摘要统计信息:
println ( "training Mean Squared Error = " + summary. meanSquaredError )
println("training Root Mean Squared Error = " + summary.rootMeanSquaredError) }
training Mean Squared Error = 13.608987362865541
training Root Mean Squared Error = 3.689036102136375
  1. 通过停止SparkSession来关闭程序:
spark.stop()

它是如何工作的...

在这个示例中,我们再次使用房屋数据来演示 Spark 2.0 的LinearRegression()API,使用 L-BFGS 优化选项。我们读取文件,解析数据,并选择回归的特定列。我们通过接受默认参数来保持示例简短,但在运行.fit()方法之前,将迭代次数(用于收敛到解决方案)和优化方法设置为lbfgs。然后,我们继续输出一些快速指标(即 MSE 和 RMSE)仅用于演示。我们展示了如何使用 RDD 自己实现/计算这些指标。使用 Spark 2.0 的本机功能/指标和基于 RDD 的回归示例,我们展示了 Spark 现在可以直接完成这些指标,这证明了我们从 Spark 1.0.1 走过了多远!

对于少量列使用牛顿优化技术(例如lbfgs)是一种过度,稍后在本书中进行演示,以便读者能够在实际环境中的大型数据集上使用这些示例(例如,从第一章中提到的典型癌症/基因组数据)。

还有更多...

弹性网(由 DB Tsai 和其他人贡献)和由 Alpine Labs 推广的技术从 Spark 1.4 和 1.5 开始引起了我们的关注,现在已成为 Spark 2.0 中的事实标准技术。

为了水平设置,弹性网是 L1 和 L2 惩罚的线性组合。它可以在概念上被建模为一个可以决定在惩罚中包含多少 L1 和多少 L2 的旋钮(收缩与选择)。

我们想强调的是,现在我们可以通过参数设置来选择回归类型,而不是命名的 API。这是与基于 RDD 的 API(即现在处于维护模式)的重要分歧,我们稍后在本章中进行演示。

以下表格提供了一个快速设置参数的备忘单,以在 Lasso、Ridge、OLS 和弹性网之间进行选择。

请参阅以下表格setElasticNetParam(value: Double)

回归类型 惩罚 参数
Lasso L1 0
Ridge L2 1
弹性网 L1 + L2 0.0 < alpha < 1.0
OLS 普通最小二乘法

通过以下简要处理来了解正则化是如何通过弹性网参数(对应于 Alpha)来控制的:

另请参阅

Spark ML 的一个重要方面是其简单而异常强大的 API 集,它允许开发人员在现有集群上轻松扩展到数十亿行,而几乎不需要额外的工作。您会惊讶于 Lasso 可以用于发现相关特征集的规模,而 L-BFGS 优化(不需要直接的海森矩阵存在)可以轻松处理大量特征。Spark 2.0 源代码中 LBFGS 的updater实现的细节超出了本书的范围。

由于其复杂性,我们将在后续章节中介绍与这些 ML 算法相关的优化。

Spark 2.0 中具有 Lasso 和'auto'优化选择的线性回归 API

在这个配方中,我们通过显式选择 LASSO 回归setElasticNetParam(0.0)来构建上一个配方LinearRegression,同时让 Spark 2.0 使用setSolver('auto')自行选择优化。*我们再次提醒,基于 RDD 的回归 API 现在处于维护模式,这是未来的首选方法。

如何做...

  1. 我们使用 UCI 机器库存储的住房数据集。

  2. 从以下网址下载整个数据集:

数据集由 14 列组成,前 13 列是独立变量(即特征),试图解释美国波士顿自有住房的中位价格(即最后一列)。

我们已经选择并清理了前八列作为特征。我们使用前 200 行来训练和预测中位数价格:

    • CRIM: 按城镇划分的人均犯罪率
  • ZN: 用于超过 25,000 平方英尺的住宅用地比例

  • INDUS: 每个城镇的非零售业务面积比例

  • CHAS: 查尔斯河虚拟变量(如果地块边界河流,则为1;否则为0

  • NOX: 一氧化氮浓度(每 1000 万份之一)

  • RM: 每个住宅的平均房间数

  • AGE: 1940 年前建造的自有住房的比例

  1. 请使用housing8.csv文件,并确保将其移动到以下目录:
 ../data/sparkml2/chapter5/housing8.csv
  1. 在 IntelliJ 或您选择的 IDE 中开始一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序所在的包位置:

package spark.ml.cookbook.chapter5.
  1. 导入必要的包,以便SparkSession访问集群和Log4j.Logger减少 Spark 产生的输出量:
import org.apache.spark.ml.regression.LinearRegression
import org.apache.spark.ml.feature.LabeledPoint
import org.apache.spark.sql.SparkSession
import org.apache.spark.ml.linalg.Vectors
import org.apache.log4j.{Level, Logger}
  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("myRegress03")
.config("spark.sql.warehouse.dir", ".")
.getOrCreate()
  1. 我们需要导入数据转换例程的隐式:
import spark.implicits._
  1. 接下来,我们将住房数据加载到数据集中:
val data = spark.read.text( "../data/sparkml2/chapter5/housing8.csv" ).as[ String ]
  1. 让我们解析住房数据并将其转换为标签点:
val RegressionDataSet = data.map { line =>
val columns = line.split(',')
LabeledPoint(columns(13).toDouble , Vectors.dense(columns(0).toDouble,columns(1).toDouble, columns(2).toDouble, columns(3).toDouble,columns(4).toDouble,
columns(5).toDouble,columns(6).toDouble, columns(7).toDouble
))
}
  1. 现在显示加载的数据:

  1. 接下来,我们配置一个线性回归算法来生成模型:
val lr = new LinearRegression()
.setMaxIter(1000)
.setElasticNetParam(0.0)
.setRegParam(0.01)
.setSolver( "auto" )
  1. 现在我们将模型拟合到住房数据中:
val myModel = lr.fit(RegressionDataSet)
  1. 接下来,我们检索摘要数据以调和模型的准确性:
val summary = myModel.summary
  1. 最后,我们打印摘要统计信息:
println ( "training Mean Squared Error = " + summary. meanSquaredError )
println("training Root Mean Squared Error = " + summary.rootMeanSquaredError) }
training Mean Squared Error = 13.609079490110766
training Root Mean Squared Error = 3.6890485887435482
  1. 我们通过停止SparkSession来关闭程序:
spark.stop()

它是如何工作的...

我们读取住房数据并加载选定的列,并使用它们来预测住房单位的价格。我们使用以下代码片段选择回归为 LASSO,并让 Spark 自行选择优化:

val lr = new LinearRegression()
.setMaxIter(1000)
.setElasticNetParam(0.0)
.setRegParam(0.01)
.setSolver( "auto" )

我们将setMaxIter()更改为1000以进行演示。默认设置为100

还有更多...

虽然 Spark 对 L-BFGS 有很好的实现,请参阅以下链接,快速了解 BFGS 及其内部工作原理,因为它与这个示例相关:

还可以查看基于 RDD 的回归示例,以了解有关 LBGFS 的更多详细信息。如果您需要了解 BFGS 技术的实现细节,请参考以下链接。

这个 C 语言实现帮助我们在代码级别开发对一阶优化的扎实理解:www.chokkan.org/software/liblbfgs/

另请参阅

Spark 2.0 中具有岭回归和“自动”优化选择的线性回归 API

在这个示例中,我们使用LinearRegression接口实现岭回归。我们使用弹性网参数来设置适当的值以进行完整的 L2 惩罚,从而相应地选择岭回归。

如何做到...

  1. 我们使用 UCI 机器库存储的住房数据集。

  2. 从以下网址下载整个数据集:

数据集由 14 列组成,前 13 列是独立变量(即特征),试图解释美国波士顿自住房的中位价格(即最后一列)。

我们选择并清理了前八列作为特征。我们使用前 200 行来训练和预测中位价格:

    • CRIM:按城镇计算的人均犯罪率
  • ZN:用于超过 25,000 平方英尺的地块的住宅用地比例

  • INDUS:每个城镇非零售业务英亩的比例

  • CHAS:查尔斯河虚拟变量(如果地区与河流相接则为 1;否则为 0)

  • NOX:一氧化氮浓度(每千万分之一)

  • RM:每个住宅的平均房间数

  • AGE:1940 年前建成的自住单位比例

  1. 请使用housing8.csv文件,并确保将其移动到以下目录:
 ../data/sparkml2/chapter5/housing8.csv
  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter5.
  1. 导入必要的包以便SparkSession访问集群和Log4j.Logger减少 Spark 产生的输出量:
import org.apache.spark.ml.feature.LabeledPoint
import org.apache.spark.ml.linalg.Vectors
import org.apache.spark.ml.regression.LinearRegression
import org.apache.spark.sql.SparkSession
import org.apache.log4j.{Level, Logger}
  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("myRegress04")
.config("spark.sql.warehouse.dir", ".")
.getOrCreate()
  1. 我们需要导入数据转换例程的隐式:
import spark.implicits._
  1. 接下来,我们将房屋数据加载到数据集中:
val data = spark.read.text( "../data/sparkml2/chapter5/housing8.csv" ).as[ String ]
  1. 让我们解析房屋数据并将其转换为标签点:
val RegressionDataSet = data.map { line =>
val columns = line.split(',')
LabeledPoint(columns(13).toDouble , Vectors.dense(columns(0).toDouble,columns(1).toDouble, columns(2).toDouble, columns(3).toDouble,columns(4).toDouble,
columns(5).toDouble,columns(6).toDouble, columns(7).toDouble
))
}
  1. 现在显示加载的数据:

  1. 接下来,我们配置线性回归算法以生成模型:
val lr = new LinearRegression()
.setMaxIter(1000)
.setElasticNetParam(1.0)
.setRegParam(0.01)
.setSolver( "auto" )
  1. 现在,我们将模型拟合到房屋数据:
val myModel = lr.fit(RegressionDataSet)
  1. 接下来,我们检索摘要数据以调和模型的准确性:
val summary = myModel.summary
  1. 最后,我们打印出摘要统计信息:
println ( "training Mean Squared Error = " + summary. meanSquaredError )
println("training Root Mean Squared Error = " + summary.rootMeanSquaredError) }
training Mean Squared Error = 13.61187856748311
training Root Mean Squared Error = 3.6894279458315906
  1. 通过停止SparkSession来关闭程序:
spark.stop()

它是如何工作的...

我们通过读取房屋数据并加载适当的列来加载数据。然后,我们继续设置将强制LinearRegression()执行岭回归的参数,同时保持优化为'auto'。以下代码显示了如何使用线性回归 API 来设置所需的回归类型为岭回归:

val lr = new LinearRegression()
.setMaxIter(1000)
.setElasticNetParam(1.0)
.setRegParam(0.01)
.setSolver( "auto" )

然后我们使用.fit()将模型拟合到数据。最后,我们使用.summary提取模型摘要并打印模型的 MSE 和 RMSE。

还有更多...

为了确保我们清楚岭回归和 Lasso 回归之间的区别,我们必须首先强调参数收缩(即,我们使用平方根函数压缩权重,但从不将其设置为零)和特征工程或参数选择之间的区别(即,我们将参数收缩到0,从而导致一些参数从模型中完全消失):

另请参阅

线性回归文档:spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.ml.regression.LinearRegression

Apache Spark 2.0 中的等渗回归

在这个示例中,我们演示了 Spark 2.0 中的IsotonicRegression()函数。当数据中期望有顺序并且我们想要将递增的有序线(即,表现为阶梯函数)拟合到一系列观察中时,使用等渗或单调回归。术语等渗回归IR)和单调回归MR)在文献中是同义的,可以互换使用。

简而言之,我们尝试使用IsotonicRegression()配方提供比朴素贝叶斯和 SVM 的一些缺点更好的拟合。虽然它们都是强大的分类器,但朴素贝叶斯缺乏 P(C | X)的良好估计,支持向量机SVM)最多只提供代理(可以使用超平面距离),在某些情况下并不是准确的估计器。

如何做...

  1. 转到网站下载文件并将文件保存到以下代码块中提到的数据路径。我们使用著名的鸢尾花数据并将一步线拟合到观察中。我们使用库中以LIBSVM格式的鸢尾花数据来演示 IR。

我们选择的文件名是iris.scale.txt www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/multiclass/iris.scale

  1. 在 IntelliJ 或您选择的 IDE 中启动新项目。确保包含必要的 JAR 文件。

  2. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter5
  1. 导入必要的包,以便SparkSession可以访问集群和Log4j.Logger以减少 Spark 产生的输出量:
import org.apache.spark.sql.SparkSession
 import org.apache.spark.ml.regression.IsotonicRegression
  1. 将输出级别设置为ERROR以减少 Spark 的日志输出:
Logger.getLogger("org").setLevel(Level.ERROR)
Logger.getLogger("akka").setLevel(Level.ERROR)
  1. 使用构建模式初始化SparkSession,从而使 Spark 集群的入口点可用:
val spark = SparkSession
 .builder
 .master("local[4]")
 .appName("myIsoTonicRegress")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()
  1. 然后我们读入数据文件,打印出数据模式,并在控制台中显示数据:
val data = spark.read.format("libsvm")
 .load("../data/sparkml2/chapter5/iris.scale.txt")
 data.printSchema()
 data.show(false)

我们得到以下控制台输出:

  1. 然后,我们将数据集分割为训练集和测试集,比例为0.7:0.3
val Array(training, test) = data.randomSplit(Array(0.7, 0.3), seed = System.currentTimeMillis())
  1. 接下来,我们创建IsotonicRegression对象并将其拟合到训练数据中:
val itr = new IsotonicRegression()

 val itrModel = itr.fit(training)
  1. 现在我们在控制台中打印出模型边界和预测:
println(s"Boundaries in increasing order: ${itrModel.boundaries}")
 println(s"Predictions associated with the boundaries: ${itrModel.predictions}")

我们得到以下控制台输出:

Boundaries in increasing order: [-1.0,-0.666667,-0.666667,-0.5,-0.5,-0.388889,-0.388889,-0.333333,-0.333333,-0.222222,-0.222222,-0.166667,-0.166667,0.111111,0.111111,0.333333,0.333333,0.5,0.555555,1.0]
Predictions associated with the boundaries: [1.0,1.0,1.1176470588235294,1.1176470588235294,1.1666666666666663,1.1666666666666663,1.3333333333333333,1.3333333333333333,1.9,1.9,2.0,2.0,2.3571428571428577,2.3571428571428577,2.5333333333333314,2.5333333333333314,2.7777777777777786,2.7777777777777786,3.0,3.0]
  1. 我们让模型转换测试数据并显示结果:
itrModel.transform(test).show()

我们得到以下控制台输出:

  1. 我们通过停止SparkSession来关闭程序:
spark.stop()

它是如何工作的...

在这个例子中,我们探索了等距回归模型的特性。我们首先以libsvm格式将数据集文件读入 Spark。然后我们分割数据(70/30)并进行下一步。接下来,我们通过调用.show()函数在控制台中显示 DataFrame。然后,我们创建了IsotonicRegression()对象,并通过调用fit(data)函数让模型自行运行。在这个示例中,我们保持简单,没有改变任何默认参数,但读者应该进行实验,并使用 JChart 包来绘制线条,看看增长和阶梯线条结果的影响。

最后,我们在控制台中显示了模型边界和预测,并使用模型转换测试数据集,并在控制台中显示包含预测字段的结果 DataFrame。所有 Spark ML 算法对超参数值都很敏感。虽然设置这些参数没有硬性规定,但在投入生产之前需要进行大量的科学方法实验。

我们在前几章中介绍了 Spark 提供的许多模型评估设施,并在整本书中讨论了评估指标,而没有重复。Spark 提供以下模型评估方法。开发人员必须根据正在评估的算法类型(例如,离散、连续、二进制、多类等)选择特定的评估指标设施。

我们将单独使用配方来覆盖评估指标,但请参阅以下链接,了解 Spark 的模型评估覆盖范围:spark.apache.org/docs/latest/mllib-evaluation-metrics.html

还有更多...

在撰写本文时,Spark 2.0 实现具有以下限制:

  • 仅支持单特征(即单变量)算法:
def setFeaturesCol(value: String): IsotonicRegression.this.type

另请参阅

有关保序回归的更多信息,请参见:

en.wikipedia.org/wiki/Isotonic_regression

保序回归线最终成为一个阶梯函数,而不是线性回归,即一条直线。以下图(来源:维基百科)提供了一个很好的参考:

Apache Spark 2.0 中的多层感知器分类器

在这个示例中,我们探索了 Spark 2.0 的多层感知器分类器MLPC),这是前馈神经网络的另一个名称。我们使用鸢尾花数据集来预测描述输入的特征向量的二元结果。要记住的关键点是,即使名称听起来有点复杂,MLP 本质上只是用于无法通过简单的线性线或超平面分离的数据的非线性分类器。

如何做...

  1. 转到LIBSVM数据:分类(多类)存储库,并从以下 URL 下载文件:www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/multiclass/iris.scale

  2. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  3. 设置程序所在的包位置:

package spark.ml.cookbook.chapter5
  1. 导入SparkSession所需的包,以访问集群,并导入Log4j.Logger以减少 Spark 产生的输出量:
import org.apache.spark.ml.classification
.MultilayerPerceptronClassifier
import org.apache.spark.ml.evaluation.
MulticlassClassificationEvaluator
import org.apache.spark.sql.SparkSession
import org.apache.log4j.{ Level, Logger}
  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("MLP")
 .getOrCreate()
  1. 首先,我们将libsvm格式的数据文件加载到内存中:
val data = spark.read.format( "libsvm" )
.load("../data/sparkml2/chapter5/iris.scale.txt")
  1. 现在显示加载的数据:

从控制台,这是输出:

data.show(false)

  1. 接下来,我们利用数据集的randomSplit方法将数据分成两个桶,每个桶分配 80%和 20%的数据:
val splitData = data.randomSplit(Array( 0.8 , 0.2 ), seed = System.currentTimeMillis())
  1. randomSplit方法返回一个包含两组数据的数组,其中训练集占 80%,测试集占 20%:
val train = splitData(0)
 val test = splitData(1)
  1. 接下来,我们配置多层感知器分类器,输入层为四个节点,隐藏层为五个节点,输出为四个节点:
val layers = ArrayInt
val mlp = new MultilayerPerceptronClassifier()
.setLayers(layers)
.setBlockSize(110)
.setSeed(System.currentTimeMillis())
.setMaxIter(145)
    • Blocksize:用于将输入数据堆叠在矩阵中以加快计算速度的块大小。这更多是一个效率参数,推荐的大小在101000之间。该参数涉及将数据推入分区以提高效率的总量。
  • MaxIter:运行模型的最大迭代次数。

  • Seed:如果未设置权重,则设置权重初始化的种子。

在 GitHub 上 Spark 源代码的以下两行显示了代码中的默认设置:

setDefault(maxIter->100, tol -> 1e-6, blockSize ->128, solver -> MultilayerPerceptronClassifier.LBFGS, stepSize ->0.03)

要更好地理解参数和种子,请查看 MLP 源代码github.com/apache/spark/blob/master/mllib/src/main/scala/org/apache/spark/ml/classification/MultilayerPerceptronClassifier.scala

  1. 我们通过调用 fit 方法生成模型:
val mlpModel = mlp.fit(train)
  1. 接下来,我们利用训练好的模型对测试数据进行转换,并显示预测结果:
val result = mlpModel.transform(test)
result.show(false)

结果将像以下内容一样显示在控制台上:

  1. 最后,我们从结果中提取预测和标签,并将它们传递给多类分类评估器以生成准确度值:
val predictions = result.select("prediction", "label")
val eval = new MulticlassClassificationEvaluator().setMetricName("accuracy")
println("Accuracy: " + eval.evaluate(predictions))
Accuracy: 0.967741935483871
  1. 通过停止SparkSession来关闭程序:
spark.stop()

它是如何工作的...

在这个示例中,我们演示了多层感知器分类器的用法。我们首先加载了经典的鸢尾花数据集,格式为libsvm。接下来,我们将数据集分成 80%的训练集数据和 20%的测试集数据。在定义阶段,我们配置了一个输入层有四个节点,一个隐藏层有五个节点,一个输出层有四个节点的多层感知器分类器。我们通过调用fit()方法生成了一个训练模型,然后利用训练模型进行预测。

最后,我们获取了预测和标签,并将它们传递给多类分类评估器,计算准确度值。

在没有太多实验和拟合的情况下,对预测与实际情况进行简单的目测似乎非常令人印象深刻,并且证明了为什么神经网络(与上世纪 90 年代的版本大不相同)重新受到青睐。它们在捕捉非线性表面方面做得很好。以下是一些非线性表面的例子(来源:Mac App Store 上的 Graphing Calculator 4)。

以下图显示了一个样本非线性情况的二维描述:

以下图显示了一个样本非线性情况的三维描述。

一般来说,神经网络首先由以下代码示例定义:

val layers = ArrayInt
val mlp = new MultilayerPerceptronClassifier()
.setLayers(layers)
.setBlockSize(110)
.setSeed(System.currentTimeMillis())
.setMaxIter(145)

这定义了网络的物理配置。在这种情况下,我们有一个4 x 5 x 4 MLP,意思是四个输入层,五个隐藏层和四个输出层。通过使用setBlockSize(110)方法将BlockSize设置为 110,但默认值为 128。重要的是要有一个良好的随机函数来初始化权重,在这种情况下是当前系统时间setSeed(System.*currentTimeMillis*()setMaxIter(145)setSolver()方法使用的最大迭代次数,默认为l-bfgs求解器。

还有更多...

多层感知器MLP)或前馈网络FFN)通常是人们在毕业于受限玻尔兹曼机RBM)和循环神经网络RRN)之前首先了解的神经网络类型,这些类型在深度学习中很常见。虽然 MLP 在技术上可以被配置/称为深度网络,但必须进行一些调查并了解为什么它被认为是通往深度学习网络的第一步(仅此而已)。

在 Spark 2.0 的实现中,Sigmoid 函数(非线性激活)用于深度可堆叠网络配置(超过三层),将输出映射到Softmax函数,以创建一个能够捕捉数据极端非线性行为的非平凡映射表面。

Spark 使用 Sigmoid 函数通过易于使用的 API 在可堆叠的配置中实现非线性映射。以下图显示了 Sigmoid 函数及其在 Mac 上的图形计算器软件上的图形。

另请参阅

理解深度信念网络(绝对最低限度)及其与简单 MLP 的对比所需的经典论文:

MultilayerPerceptronClassifier中的一些重要的 API 调用:**

BlockSize默认设置为 128 - 只有在您完全掌握 MLP 时才应开始调整此参数:

  • def **setLayers**(value: Array[Int]): MultilayerPerceptronClassifier.this.type

  • def **setFeaturesCol**(value: String): MultilayerPerceptronClassifier

  • def **setLabelCol**(value: String): MultilayerPerceptronClassifier

  • def **setSeed**(value: Long): MultilayerPerceptronClassifier.this.type

  • def **setBlockSize**(value: Int): MultilayerPerceptronClassifier.this.type

  • def **setSolver**(value: String): MultilayerPerceptronClassifier.this.type

Apache Spark 2.0 中的 One-vs-Rest 分类器(One-vs-All)

在这个示例中,我们演示了 Apache Spark 2.0 中的 One-vs-Rest。我们尝试通过OneVsRest()分类器使二元逻辑回归适用于多类/多标签分类问题。该示例是一个两步方法,首先我们配置一个LogisticRegression()对象,然后在OneVsRest()分类器中使用它来解决使用逻辑回归的多类分类问题。

如何做...

  1. 转到LIBSVM数据:分类(多类)存储库,并下载文件:www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/multiclass/iris.scale

  2. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  3. 设置程序所在的包位置:

package spark.ml.cookbook.chapter5
  1. 导入必要的包,以便SparkSession可以访问集群,Log4j.Logger可以减少 Spark 产生的输出量:
import org.apache.spark.sql.SparkSession
import org.apache.spark.ml.classification
.{LogisticRegression, OneVsRest}
import org.apache.spark.ml.evaluation
.MulticlassClassificationEvaluator
import org.apache.log4j.{ Level, Logger}
  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("One-vs-Rest")
 .getOrCreate()
  1. 我们首先将libsvm格式的数据文件加载到内存中:
 val data = spark.read.format("libsvm")
 .load("../data/sparkml2/chapter5/iris.scale.txt")
  1. 现在显示加载的数据:
data.show(false)

  1. 接下来,我们利用数据集的randomSplit方法,将数据集按 80%的训练数据和 20%的测试数据进行分割:
val Array (train, test) = data.randomSplit(Array( 0.8 , 0.2 ), seed = System.currentTimeMillis())
  1. 让我们配置一个逻辑回归算法,用作 One-vs-Rest 算法的分类器:
val lrc = new LogisticRegression()
.setMaxIter(15)
.setTol(1E-3)
.setFitIntercept(true)
  1. 接下来,我们创建一个 one versus rest 对象,将我们新创建的逻辑回归对象作为参数传递:
val ovr = new OneVsRest().setClassifier(lrc)
  1. 通过在我们的 one-vs-rest 对象上调用 fit 方法生成模型:
val ovrModel = ovr.fit(train)
  1. 现在,我们将使用训练好的模型为测试数据生成预测并显示结果:

  2. 最后,我们将预测传递给多类分类评估器,生成准确度值:

val eval = new MulticlassClassificationEvaluator()
.setMetricName("accuracy")
val accuracy = eval.evaluate(predictions)
println("Accuracy: " + eval.evaluate(predictions))
Accuracy: 0.9583333333333334
  1. 通过停止SparkSession来关闭程序:
spark.stop()

工作原理...

在这个示例中,我们演示了 One-vs-Rest 分类器的用法。我们首先加载了经典的 Iris 数据集,格式为libsvm。接下来,我们将数据集按 80%的比例分割为训练数据集和 20%的测试数据集。我们提醒用户注意,我们如何使用系统时间来进行分割的随机性如下:

data.randomSplit(Array( 0.8 , 0.2 ), seed = System.currentTimeMillis())

该算法最好可以描述为一个三步过程:

  1. 我们首先配置回归对象,而无需手头上有基本的逻辑模型,以便将其输入到我们的分类器中:
LogisticRegression()
.setMaxIter(15)
.setTol(1E-3)
.setFitIntercept(true)
  1. 在下一步中,我们将配置好的回归模型输入到我们的分类器中,并调用fit()函数来完成相应的工作:
val ovr = new OneVsRest().setClassifier(lrc)
  1. 我们生成了一个训练模型,并通过该模型转换了测试数据。最后,我们将预测传递给多类分类评估器,生成一个准确度值。

还有更多...

这种算法的典型用法是将关于一个人的不同新闻项目标记和打包到各种类别中(例如友好与敌对、温和与欢欣等)。在医疗账单中的另一个用途可能是将患者诊断分类为用于自动结算和收入循环最大化的不同医疗编码。

一对多:如下图所示,这通过二元逻辑回归解决了一个n标签分类问题:

另请参阅

Spark 2.0 关于OneVsRest()的文档可以在以下找到:

spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.ml.classification.OneVsRest

另一种可视化方法是,评估给定二元分类器是否可以将 n 类输入分解为N个逻辑回归,然后选择最能描述数据的那个。以下是使用 Python 中 Scikit Learn 库的此分类器的众多示例:

scikit-learn.org/stable/modules/generated/sklearn.multiclass.OneVsOneClassifier.html#sklearn.multiclass.OneVsOneClassifier

但我们建议您在 GitHub 上对实际的 Scala 源代码进行快速扫描(仅不到 400 行):

github.com/apache/spark/blob/v2.0.2/mllib/src/main/scala/org/apache/spark/ml/classification/OneVsRest.scala

生存回归 - 参数 AFT 模型在 Apache Spark 2.0 中

在这个教程中,我们探索了 Spark 2.0 对生存回归的实现,这不是典型的比例危险模型,而是加速失效时间AFT)模型。这是一个重要的区别,应该在运行这个教程时牢记,否则结果将毫无意义。

生存回归分析关注的是事件发生时间的模型,这在医学、保险和任何时候对主题的生存能力感兴趣的情况下都很常见。我的一位合著者碰巧是一位受过全面训练的医生(除了是计算机科学家),所以我们使用了该领域一本备受尊敬的书中的真实数据集 HMO-HIM+研究,以便获得合理的输出。

目前,我们正在使用这种技术来进行干旱建模,以预测农产品在长期时间范围内的价格影响和预测。

如何做...

  1. 前往 UCLA 网站下载文件:

stats.idre.ucla.edu/stat/r/examples/asa/hmohiv.csv

我们使用的数据集是 David W Hosmer 和 Stanley Lemeshow(1999)的书《应用生存分析:事件发生时间数据的回归建模》中的实际数据。数据来自 HMO-HIM+研究,数据包含以下字段:

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序所在的包位置:

package spark.ml.cookbook.chapter5
  1. 导入SparkSession所需的包,以便访问集群,以及Log4j.Logger以减少 Spark 产生的输出量:
import org.apache.log4j.{Level, Logger}
 import org.apache.spark.ml.linalg.Vectors
 import org.apache.spark.ml.regression.AFTSurvivalRegression
 import org.apache.spark.sql.SparkSession
  1. 将输出级别设置为ERROR,以减少 Spark 的日志输出:
Logger.getLogger("org").setLevel(Level.ERROR)
Logger.getLogger("akka").setLevel(Level.ERROR)
  1. 使用构建器模式初始化SparkSession,指定配置,从而为 Spark 集群提供入口点:
val spark = SparkSession
 .builder
 .master("local[4]")
 .appName("myAFTSurvivalRegression")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()
  1. 然后我们读取csv文件,跳过第一行(标题)。

注意:有多种方法可以将csv文件读入 Spark DataFrame:

val file = spark.sparkContext.textFile("../data/sparkml2/chapter5/hmohiv.csv")
 val headerAndData = file.map(line => line.split(",").map(_.trim))
 val header = headerAndData.first
 val rawData = headerAndData.filter(_(0) != header(0))
  1. 我们将字段从字符串转换为双精度。我们只对 ID、时间、年龄和审查字段感兴趣。然后这四个字段形成一个 DataFrame:
val df = spark.createDataFrame(rawData
 .map { line =>
 val id = line(0).toDouble
 val time =line(1).toDouble
 val age = line(2).toDouble
 val censor = line(4).toDouble
 (id, censor,Vectors.dense(time,age))
 }).toDF("label", "censor", "features")

新的features字段是由timeage字段组成的向量。

  1. 接下来,我们在控制台中显示了 DataFrame:
df.show()

从控制台,这是输出:

  1. 现在我们将创建AFTSurvivalRegression对象,并设置参数。

对于这个特定的配方,分位数概率设置为 0.3 和 0.6。这些值描述了分位数的边界,它们是概率的数值向量,其值范围在0.01.0 [0.0,1.0]之间。例如,使用(0.25, 0.5, 0.75)作为分位数概率向量是一个常见的主题。

分位数列名设置为quantiles

在下面的代码中,我们创建了AFTSurvivalRegression()对象,并设置了列名和分位数概率向量。

来自 Spark 在 GitHub 上的源代码的以下代码显示了默认值:

 @Since("1.6.0")
def getQuantileProbabilities: Array[Double] = $(quantileProbabilities)
setDefault(quantileProbabilities -> Array(0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99)) 

要了解参数化和种子,可以在 GitHub 上参考Spark Source Code for Survival Regression github.com/apache/spark/blob/master/mllib/src/main/scala/org/apache/spark/ml/regression/AFTSurvivalRegression.scala

val aft = new AFTSurvivalRegression()
 .setQuantileProbabilities(Array(0.3, 0.6))
 .setQuantilesCol("quantiles")
  1. 我们让模型运行:
val aftmodel = aft.fit(df)
  1. 我们将模型数据打印到控制台:
println(s"Coefficients: ${aftmodel.coefficients} ")
 println(s"Intercept: ${aftmodel.intercept}" )
 println(s"Scale: ${aftmodel.scale}")

控制台中将看到以下输出:

Coefficients: [6.601321816135838E-4,-0.02053601452465816]
Intercept: 4.887746420937845
Scale: 0.572288831706005
  1. 我们使用前面的模型转换了数据集,并在控制台中显示了结果:
aftmodel.transform(df).show(false)

控制台中将看到以下输出:

  1. 我们通过停止SparkSession来关闭程序:
spark.stop()

工作原理...

我们探索了加速失效时间AFT)模型的特征。我们首先使用sparkContext.textFile()将数据集文件读入 Spark。有多种方法可以读取csv格式文件。我们只选择了一个显示更详细步骤的方法。

接下来,我们过滤了头行,并将感兴趣的字段从字符串转换为双精度,然后将双精度数据集转换为具有新features字段的新 DataFrame。

然后,我们创建了AFTSurvivalRegression对象并设置了分位数参数,并通过调用fit(data)函数让模型自行运行。

最后,我们显示了模型摘要,并使用模型转换了数据集,并显示了包括预测和分位数字段的结果 DataFrame。

还有更多...

Spark 实现的生存回归(AFTSurvivalRegression

  • **模型:**加速失效时间(AFT)。

  • **参数化:**使用威布尔分布。

  • **优化:**Spark 选择 AFT 是因为它更容易并行化,并将问题视为具有 L-BFGS 作为优化方法的凸优化问题。

  • **R/SparkR 用户:**在没有拦截器的情况下拟合AFTSurvivalRegressionModel到具有常数非零列的数据集时,Spark MLlib 会对常数非零列输出零系数。这种行为与 R survival::survreg不同。(来自 Spark 2.0.2 文档)

您应该将结果视为发生感兴趣事件的时间,比如疾病的发生、赢得或失去、按揭违约时间、婚姻、离婚、毕业后找到工作等。这些模型的独特之处在于时间事件是一个持续时间,并不一定有解释变量(也就是说,它只是一段时间,以天、月或年为单位)。

您可能使用生存模型而不是简单回归(即诱人)的原因如下:

  • 需要将结果变量建模为时间事件

  • 审查-并非所有数据都是已知的或使用的(在使用来自过去几个世纪的长期商品数据时很常见)

  • 非正态分布的结果-通常是时间的情况

  • 这可能是多变量分析的情况,也可能不是。

尽管在此处概述了生存回归的两种方法,但在撰写本文时,Spark 2.0 仅支持 AFT 模型,而不支持最广为人知的比例风险模型:

  • 比例风险模型(PH):

  • 比例性假设随时间而定

  • 在考虑时间内通过协方差乘以常数

  • 示例:Cox 比例风险模型

  • *hx(y) = h0(y)g(X)

  • 加速时间故障(ATF)- Spark 2.0 实施:

  • 可以假设或违反比例性假设

  • 通过协方差乘以常数值以获得回归系数值可能是:

  • 加速

  • 减速

  • 允许回归展开的阶段:

  • 疾病的阶段

  • 生存能力的阶段

  • Yx * g(X) = Y0

Sx(y) = S0(yg(X))

其中,

Y:生存时间,

X:协变量向量,

hx(y):危险函数,

Sx(y):给定XY的生存函数,

Yx:给定XY

  • 参数建模 - 时间变量的基础分布:

  • 指数

  • Weibull - Spark 2.0 实施

  • 对数逻辑

  • 正态

  • 伽马

  • 另请参阅 - 在 R 中非常受欢迎 - 我们使用了这两个软件包:

  • Library(survival):标准生存分析

  • Library(eha):用于 AFT 建模

SurvivalRegression的文档可在以下网址找到:

HMOHIV数据集的原始格式可在以下网址找到 - 以访客身份连接:

ftp://ftp.wiley.com/public/sci_tech_med/survival

可以在以下网址找到 Proportional 与 AFT(Spark 2.0)风险模型的深入完整比较:

ecommons.usask.ca/bitstream/handle/10388/etd-03302009-140638/JiezhiQiThesis.pdf

具有图表的端到端真实世界医学研究:

www.researchgate.net/profile/Richard_Kay2/publication/254087561_On_the_Use_of_the_Accelerated_Failure_Time_Model_as_an_Alternative_to_the_Proportional_Hazards_Model_in_the_Treatment_of_Time_to_Event_Data_A_Case_Study_in_Influenza/links/548ed67e0cf225bf66a710ce.pdf

另请参阅

第六章:在 Spark 2.0 中使用回归和分类的实用机器学习 - 第二部分

在 Spark 2.0 中使用逻辑回归探索 ML 管道和数据框

  • 在 Spark 2.0 中使用 SGD 优化的线性回归

  • 在 Spark 2.0 中使用 SGD 优化的逻辑回归

  • 在这一章中,Spark 2.0 中回归和分类的后半部分,我们重点介绍了基于 RDD 的回归,这是目前许多现有 Spark ML 实现中的实践。由于现有的代码库,预期中级到高级的从业者能够使用这些技术。

  • 在 Spark 2.0 中使用 SGD 优化的 Lasso 回归

  • 在 Spark 2.0 中使用 L-BFGS 优化的逻辑回归

  • 在本章中,我们将涵盖以下配方:

  • 使用 Spark 2.0 MLlib 的朴素贝叶斯机器学习

  • archive.ics.uci.edu/ml/machine-learning-databases/housing/

介绍

使用 Spark 2.0 的支持向量机(SVM)

在本章中,您将学习如何使用各种回归(线性、逻辑、岭和套索)以及随机梯度下降SGD)和 L-BFGS 优化来实现一个小型应用程序,使用线性但强大的分类器,如支持向量机SVM)和朴素贝叶斯分类器,使用 Apache Spark API。我们将每个配方与适当的样本拟合度量相结合(例如,MSE、RMSE、ROC 和二进制和多类度量),以展示 Spark MLlib 的强大和完整性。我们介绍了基于 RDD 的线性、逻辑、岭和套索回归,然后讨论了 SVM 和朴素贝叶斯,以展示更复杂的分类器。

以下图表描述了本章中的回归和分类覆盖范围:

有报道称在实际应用中使用 SGD 进行回归存在问题,但这些问题很可能是由于 SGD 的调优不足或者未能理解大型参数系统中这种技术的利弊。

在本章和以后,我们开始朝着更完整(即插即用)的回归和分类系统迈进,这些系统可以在构建机器学习应用程序时加以利用。虽然每个配方都是一个独立的程序,但可以使用 Spark 的 ML 管道来组装一个更复杂的系统,以创建一个端到端的机器学习系统(例如,通过朴素贝叶斯对癌症簇进行分类,然后使用套索对每个部分进行参数选择)。您将在本章的最后一个配方中看到 ML 管道的一个很好的例子。虽然两个回归和分类章节为您提供了 Spark 2.0 分类中可用内容的良好示例,但我们将更复杂的方法保留到以后的章节中。

最好使用数据科学中的最新方法,但在毕业到更复杂的模型之前,掌握基础知识是很重要的,从 GLM、LRM、岭回归、套索和 SVM 开始 - 确保您了解何时使用每个模型。

在 Spark 2.0 中使用 SGD 优化的线性回归

在这个配方中,我们使用 Spark 基于 RDD 的回归 API 来演示如何使用迭代优化技术来最小化成本函数,并得出线性回归的解决方案。

我们将研究 Spark 如何使用迭代方法来收敛到回归问题的解决方案,使用一种称为梯度下降的众所周知的方法。Spark 提供了一种更实用的实现,称为 SGD,用于计算截距(在本例中设置为 0)和参数的权重。

在这个配方中,我们使用 Spark 基于 RDD 的回归 API 来演示如何使用迭代优化技术来最小化成本函数,并得出线性回归的解决方案。

  1. 我们使用了 UCI 机器库存储中的一个房屋数据集。您可以从以下网址下载整个数据集:

在 Spark 2.0 中使用 SGD 优化的岭回归

数据集包括 14 列,前 13 列是独立变量(特征),试图解释美国波士顿自住房的中位价格(最后一列)。

我们选择并清理了前八列作为特征。我们使用前 200 行来训练和预测中位价格:

  1. 在 IntelliJ 或您选择的 IDE 中开始一个新项目。确保必要的 JAR 文件已包含。

  2. 设置程序所在的包位置:

package spark.ml.cookbook.chapter6
  1. 导入必要的 Spark 会话包以访问集群和Log4j.Logger以减少 Spark 产生的输出量:
import org.apache.spark.mllib.regression.{LabeledPoint, LinearRegressionWithSGD}
import org.apache.spark.sql.SparkSession
import org.apache.spark.mllib.linalg.{Vector, Vectors}
import org.apache.log4j.Logger
import org.apache.log4j.Level
  1. 使用构建器模式初始化一个 SparkSession,指定配置,从而为 Spark 集群提供入口点:
val spark = SparkSession
 .builder
 .master("local[4]")
 .appName("myRegress02")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()
  1. 将输出级别设置为 ERROR 以减少 Spark 的输出:
 Logger.getLogger("org").setLevel(Level.ERROR)
 Logger.getLogger("akka").setLevel(Level.ERROR)
  1. 我们摄取并并行化数据集(仅前 200 行):
val data = sc.textFile("../data/sparkml2/chapter6/housing8.csv") 
  1. 我们获取并并行化 RDD(即数据变量),并使用map()函数拆分列。然后我们继续遍历列,并将它们存储在 Spark 所需的结构中(LabeledPoint)。LabeledPoint 是一个数据结构,第一列是因变量(即标签),后面是一个 DenseVector(即Vectors.Dense)。我们必须以这种格式呈现数据,以供 Spark 的LinearRegressionWithSGD()算法使用:
val RegressionDataSet = data.map { line =>
   val columns = line.split(',')

   LabeledPoint(columns(13).toDouble , Vectors.dense(columns(0).toDouble,columns(1).toDouble, columns(2).toDouble, columns(3).toDouble,columns(4).toDouble,
     columns(5).toDouble,columns(6).toDouble, columns(7).toDouble
   ))
 }
  1. 现在我们通过输出来检查回归数据,以熟悉 LabeledPoint 数据结构:
RegressionDataSet.collect().foreach(println(_)) 

(24.0,[0.00632,18.0,2.31,0.0,0.538,6.575,65.2,4.09]) 
(21.6,[0.02731,0.0,7.07,0.0,0.469,6.421,78.9,4.9671]) 
(34.7,[0.02729,0.0,7.07,0.0,0.469,7.185,61.1,4.9671]) 
(33.4,[0.03237,0.0,2.18,0.0,0.458,6.998,45.8,6.0622]) 
(36.2,[0.06905,0.0,2.18,0.0,0.458,7.147,54.2,6.0622]) 
  1. 我们设置模型参数,即迭代次数和 SGD 步长。由于这是梯度下降方法,必须尝试各种值,找到能够得到良好拟合并避免浪费资源的最佳值。我们通常使用迭代次数从 100 到 20000(极少情况下),SGD 步长从.01 到.00001 的值范围:
val numIterations = 1000
 val stepsSGD      = .001
  1. 我们调用构建模型:
   val myModel = LinearRegressionWithSGD.train(RegressionDataSet, numIterations,stepsSGD) 
  1. 在这一步中,我们使用数据集使用在前一步中构建的模型来预测值。然后将预测值和标记值放入predictedLabelValue数据结构中。澄清一下,前一步是构建模型(即决定数据的拟合),而这一步使用模型进行预测:
val predictedLabelValue = RegressionDataSet.map { lp => val predictedValue = myModel.predict(lp.features)
   (lp.label, predictedValue)
 }
  1. 在这一步中,我们检查截距(默认情况下未选择截距)和八列的权重(列 0 到 7):
println("Intercept set:",myModel.intercept)
 println("Model Weights:",myModel.weights)

输出如下:

Intercept set: 0.0
 Model Weights:,[-0.03734048699612366,0.254990126659302,0.004917402413769299,
 0.004611027094514264,0.027391067379836438,0.6401657695067162,0.1911635554630619,0.408578077994874])
  1. 为了感受预测值,我们使用takesample()函数随机选择了二十个值,但不进行替换。在这种情况下,我们仅展示了其中七个值的数值:
predictedLabelValue.takeSample(false,5).foreach(println(_)) 

输出如下:

(21.4,21.680880143786645)
 (18.4,24.04970929955823)
 (15.0,27.93421483734525)
 (41.3,23.898190127554827)
 (23.6,21.29583657363941)
 (33.3,34.58611522445151)
 (23.8,19.93920838257026)
  1. 我们使用均方根误差(其中之一)来量化拟合。拟合可以得到显著改善(更多数据、步骤 SGD、迭代次数,最重要的是特征工程的实验),但我们将其留给统计书籍来探讨。以下是 RMSD 的公式:

val MSE = predictedLabelValue.map{ case(l, p) => math.pow((l - p), 2)}.reduce(_ + _) / predictedLabelValue.count
 val RMSE = math.sqrt(MSE)println("training Mean Squared Error = " + MSE)
 println("training Root Mean Squared Error = " + RMSE)

输出如下:

training Mean Squared Error = 91.45318188628684
training Root Mean Squared Error = 9.563115699722912

工作原理...

我们使用了来自房屋数据(自变量)文件的选定列来预测房屋价格(因变量)。我们使用了基于 RDD 的回归方法,采用 SGD 优化器进行迭代求解。然后我们输出了截距和每个参数的权重。在最后一步,我们使用样本数据进行了预测并显示了输出。最后一步是输出模型的 MSE 和 RMSE 值。请注意,这仅用于演示目的,您应该使用第四章中演示的评估指标,实施强大机器学习系统的常见方法,进行模型评估和最终选择过程。

此方法构造函数的签名如下:

newLinearRegressionWithSGD()

参数的默认值:

  • stepSize= 1.0

  • numIterations= 100

  • miniBatchFraction= 1.0

miniBatchFraction是一个重要的参数,它对性能有重大影响。这在学术文献中被称为批量梯度与梯度。

还有更多...

  1. 我们还可以使用构造函数更改默认的拦截行为,创建一个新的回归模型,然后相应地使用setIntercept(true)

示例代码如下:

  val myModel = new LinearRegressionWithSGD().setIntercept(true)
  1. 如果模型的权重计算为 NaN,则必须更改模型参数(SGD 步数或迭代次数),直到收敛。例如,模型权重未正确计算(通常由于参数选择不当导致 SGD 存在收敛问题)。第一步应该是使用更精细的 SGD 步长参数:
(Model Weights:,[NaN,NaN,NaN,NaN,NaN,NaN,NaN,NaN]) 

另请参阅

我们在第九章中详细介绍了梯度下降和 SGD,优化 - 用梯度下降下山。在本章中,读者应该将 SGD 抽象为一种优化技术,用于最小化拟合一系列点的损失函数。有一些参数会影响 SGD 的行为,我们鼓励读者将这些参数改变到极端值,观察性能不佳和不收敛(即结果将显示为 NaN)。

LinearRegressionWithSGD()构造函数的文档位于以下 URL:

spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.package

Spark 2.0 中带有 SGD 优化的逻辑回归

在这个示例中,我们使用 UCI 机器库存储库中的入学数据来构建并训练一个模型,以预测基于给定一组特征(GRE、GPA 和等级)的学生入学情况,使用基于 RDD 的LogisticRegressionWithSGD() Apache Spark API 集。

这个示例演示了优化(SGD)和正则化(惩罚模型的复杂性或过拟合)。我们强调它们是两个不同的概念,常常让初学者感到困惑。在接下来的章节中,我们将更详细地演示这两个概念,因为理解它们对于成功学习机器学习是至关重要的。

如何做...

  1. 我们使用了来自 UCLA 数字研究和教育IDRE)的入学数据集。您可以从以下 URL 下载整个数据集:

数据集包括四列,第一列是因变量(标签 - 学生是否被录取),接下来的三列是解释变量,即将解释学生入学情况的特征。

我们选择并清理了前三列作为特征。我们使用前 200 行来训练和预测中位数价格:

    • 入学 - 0,1 表示学生是否被录取
  • GRE - 研究生录取考试的分数

  • GPA - 平均成绩

  • RANK - 排名

以下是前 10 行的样本数据:

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序所在的包位置:

package spark.ml.cookbook.chapter6
  1. 导入必要的包以便 Spark 会话可以访问集群,并使用Log4j.Logger来减少 Spark 产生的输出量:
import org.apache.spark.mllib.classification.LogisticRegressionWithSGD
 import org.apache.spark.mllib.linalg.Vectors
 import org.apache.spark.mllib.regression.{LabeledPoint, LassoWithSGD}
 import org.apache.spark.sql.{SQLContext, SparkSession}
 import org.apache.spark.{SparkConf, SparkContext}
 import org.apache.spark.ml.classification.{LogisticRegression, LogisticRegressionModel}
import org.apache.log4j.Logger
import org.apache.log4j.Level
  1. 初始化SparkSession,使用构建器模式指定配置,从而为 Spark 集群提供入口点:
val spark = SparkSession
 .builder
 .master("local[4]")
 .appName("myRegress05")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()
  1. 将输出级别设置为ERROR以减少 Spark 的输出:
Logger.getLogger("org").setLevel(Level.ERROR)
 Logger.getLogger("akka").setLevel(Level.ERROR)
  1. 加载数据文件并将其转换为 RDD:
val data = sc.textFile("../data/sparkml2/chapter6/admission1.csv")
  1. 通过拆分数据并将其转换为双精度,同时构建一个 LabeledPoint(Spark 所需的数据结构)数据集来摄取数据。第 1 列(位置 0)是回归中的因变量,而第 2 到第 4 列(GRE、GPA、Rank)是特征:
val RegressionDataSet = data.map { line =>
   val columns = line.split(',')

   LabeledPoint(columns(0).toDouble , Vectors.dense(columns(1).toDouble,columns(2).toDouble, columns(3).toDouble ))

 }
  1. 我们加载数据集后检查数据集,这是一直建议的,还演示了标签点内部,这是一个单一值(例如标签或因变量),后面是我们试图用来解释因变量的 DenseVector 特征:
RegressionDataSet.collect().foreach(println(_)) 

(0.0,[380.0,3.61,3.0]) 
(1.0,[660.0,3.67,3.0]) 
(1.0,[800.0,4.0,1.0]) 
(1.0,[640.0,3.19,4.0])      
   . . . . .  
. . . . . 
. . . . . 
. . . . .
  1. 我们为LogisticRegressionWithSGD()设置了模型参数。

这些参数最终控制拟合,因此需要一定程度的实验才能获得良好的拟合。我们在上一个示例中看到了前两个参数。第三个参数的值将影响权重的选择。您必须进行实验并使用模型选择技术来决定最终值。在本示例的*还有更多...*部分,我们基于两个极端值展示了特征的权重选择(即哪些权重设置为 0):

// Logistic Regression with SGD r Model parameters

val numIterations = 100
val stepsSGD = .00001
val regularizationParam = .05 // 1 is the default
  1. 使用 LabeledPoint 和前述参数创建和训练逻辑回归模型,调用LogisticRegressionWithSGD()
val myLogisticSGDModel = LogisticRegressionWithSGD.train(RegressionDataSet, numIterations,stepsSGD, regularizationParam) 
  1. 使用我们的模型和数据集预测值(类似于所有 Spark 回归方法):
val predictedLabelValue = RegressionDataSet.map { lp => val predictedValue =  myLogisticSGDModel.predict(lp.features)
   (lp.label, predictedValue)
 }
  1. 我们打印出模型的截距和权重。如果将值与线性或岭回归进行比较,您将看到选择效果。在极端值或选择具有更多共线性的数据集时,效果将更加显著。

在这个例子中,通过设置权重为 0.0,套索消除了三个参数,使用了正则化参数(例如 4.13):

println("Intercept set:",myRidgeModel.intercept) 
println("Model Weights:",myRidgeModel.weights) 

(Intercept set:,0.0) 
(Model Weights:,[-0.0012241832336285247,-7.351033538710254E-6,-8.625514722380274E-6])

从统计学的模型参数选择原则仍然适用于我们是否使用 Spark MLlib。例如,参数权重为-8.625514722380274E-6 可能太小而无法包含在模型中。我们需要查看每个参数的t-statisticp value,并决定最终的模型。

  1. 我们随机选择 20 个预测值,并直观地检查预测结果(这里只显示了前五个值):
(0.0,0.0) 
(1.0,0.0) 
(1.0,0.0) 
(0.0,0.0) 
(1.0,0.0) 
. . . . .   
. . . . .   
  1. 我们计算 RMSE 并显示结果:
val MSE = predictedLabelValue.map{ case(l, p) => math.pow((l - p), 2)}.reduce(_ + _) / predictedLabelValue.count

val RMSE = math.sqrt(MSE)

println("training Mean Squared Error = " + MSE) 
println("training Root Mean Squared Error = " + RMSE)

输出如下:

training Mean Squared Error = 0.3175 
training Root Mean Squared Error = 0.5634713834792322

它是如何工作的...

我们使用入学数据,并尝试使用逻辑回归来预测具有给定特征集(向量)的学生是否被录取(标签)。我们拟合了回归,设置了 SGD 参数(您应该进行实验),并运行了 API。然后,我们输出回归系数的截距和模型权重。使用模型,我们预测并输出一些预测值以进行视觉检查。最后一步是输出模型的 MSE 和 RMSE 值。请注意,这仅用于演示目的,您应该使用上一章中演示的评估指标进行模型评估和最终选择过程。通过查看 SME 和 RMSE,我们可能需要不同的模型、参数设置、参数或更多数据点来做得更好。

该方法构造函数的签名如下:

newLogisticRegressionWithSGD()

参数的默认值:

  • stepSize= 1.0

  • numIterations= 100

  • regParm= 0.01

  • miniBatchFraction= 1.0

还有更多...

虽然非逻辑回归试图发现将解释因素(特征)与方程左侧的数值变量相关联的线性或非线性关系,逻辑回归试图将特征集分类到一组离散类别(例如通过/不通过,好/坏或多类)。

理解逻辑回归的最佳方法是将左侧的领域视为一组离散的结果(即分类类),用于标记新的预测。使用离散标签(例如 0 或 1),我们能够预测一组特征是否属于特定类别(例如疾病的存在或不存在)。

简而言之,常规回归和逻辑回归之间的主要区别是可以在左侧使用的变量类型。 在常规回归中,预测的结果(即标签)将是一个数值,而在逻辑回归中,预测是从可能结果的离散类别中进行选择(即标签)。

出于时间和空间的考虑,我们不在每个示例中涵盖将数据集分割为训练和测试的内容,因为我们在之前的示例中已经演示了这一点。 我们也不使用任何缓存,但强调现实生活中的应用必须缓存数据,因为 Spark 中使用了惰性实例化、分阶段和优化的方式。 请参阅第四章,实施强大的机器学习系统的常见示例,以参考有关缓存和训练/测试数据拆分的示例。

如果模型的权重计算为 NaN,则必须更改模型参数(即 SGD 步骤或迭代次数),直到收敛。

另请参阅

这是构造函数的文档:

spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.mllib.classification.LogisticRegressionWithSGD

Spark 2.0 中的 SGD 优化岭回归

在这个示例中,我们使用 UCI 机器库存储库中的入学数据来构建并训练一个模型,以使用基于 RDD 的LogisticRegressionWithSGD() Apache Spark API 集来预测学生入学。 我们使用一组给定的特征(GRE,GPA 和 Rank)在入学期间用于预测模型权重,使用岭回归。 我们在另一个示例中演示了输入特征标准化,但应该注意的是,参数标准化对结果有重要影响,特别是在岭回归设置中。

Spark 的岭回归 API(LogisticRegressionWithSGD)旨在处理多重共线性(解释变量或特征相关,并且独立和随机分布的特征变量的假设有些错误)。 岭回归是关于收缩(通过 L2 正则化或二次函数进行惩罚)一些参数,从而减少它们的影响,进而降低复杂性。 需要记住的是,LogisticRegressionWithSGD()没有套索效应,其中一些参数实际上被减少到零(即被消除)。 岭回归只是收缩参数,而不是将它们设为零(收缩后仍将保留一些小效果)。

如何做...

  1. 我们使用 UCI 机器库存储库中的房屋数据集。 您可以从以下 URL 下载整个数据集:

archive.ics.uci.edu/ml/machine-learning-databases/housing/

数据集包括 14 列,前 13 列是独立变量(即特征),试图解释美国波士顿自住房的中位价格(最后一列)。

我们选择并清理了前八列作为特征。 我们使用前 200 行来训练和预测中位价格。

1 CRIM 按城镇人均犯罪率
2 ZN 用于超过 25,000 平方英尺的地块的住宅用地比例
3 INDUS 每个镇的非零售业务面积比例
4 CHAS 查尔斯河虚拟变量(如果地块与河流相接则为 1;否则为 0)
5 NOX 一氧化氮浓度(每 1000 万份之)
6 RM 每个住宅的平均房间数
7 AGE 1940 年之前建造的自住单位比例
  1. 在 IntelliJ 或您选择的 IDE 中启动新项目。 确保包含必要的 JAR 文件。

  2. 设置程序所在的包位置:

package spark.ml.cookbook.chapter6
  1. 导入必要的包以使 SparkSession 能够访问集群和Log4j.Logger以减少 Spark 产生的输出量:
import org.apache.spark.mllib.regression.{LabeledPoint, LinearRegressionWithSGD, RidgeRegressionWithSGD}
 import org.apache.spark.sql.{SQLContext, SparkSession}

 import org.apache.spark.ml.tuning.{ParamGridBuilder, TrainValidationSplit}
 import org.apache.spark.mllib.linalg.{Vector, Vectors}
import org.apache.log4j.Logger
import org.apache.log4j.Level
  1. 使用构建器模式初始化 SparkSession,从而为 Spark 集群提供入口点:
val spark = SparkSession
 .builder
 .master("local[4]")
 .appName("myRegress03")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()
  1. 为了有效地展示岭回归中模型参数的收缩(它会收缩到一个小值,但永远不会被消除),我们使用相同的房屋数据文件,并清理和使用前八列来预测最后一列的值(房屋价格中位数):
val data = sc.textFile("../data/sparkml2/chapter6/housing8.csv")
  1. 通过拆分数据并将其转换为 double,同时构建一个 LabeledPoint(Spark 所需的数据结构)数据集来摄取数据:
val RegressionDataSet = data.map { line =>
   val columns = line.split(',')

   LabeledPoint(columns(13).toDouble , Vectors.dense(columns(0).toDouble,columns(1).toDouble, columns(2).toDouble, columns(3).toDouble,columns(4).toDouble,
     columns(5).toDouble,columns(6).toDouble, columns(7).toDouble
   ))

 }
  1. 加载数据集后检查数据集,这是一直建议的,还演示了 LabeledPoint 的内部结构,它是一个单一值(标签/因变量),后跟我们试图用来解释因变量的特征的 DenseVector:
RegressionDataSet.collect().foreach(println(_)) 

(24.0,[0.00632,18.0,2.31,0.0,0.538,6.575,65.2,4.09]) 
(21.6,[0.02731,0.0,7.07,0.0,0.469,6.421,78.9,4.9671]) 
(34.7,[0.02729,0.0,7.07,0.0,0.469,7.185,61.1,4.9671]) 

. . . . .  
. . . . . 
. . . . . 
. . . . . 

(33.3,[0.04011,80.0,1.52,0.0,0.404,7.287,34.1,7.309]) 
(30.3,[0.04666,80.0,1.52,0.0,0.404,7.107,36.6,7.309]) 
(34.6,[0.03768,80.0,1.52,0.0,0.404,7.274,38.3,7.309]) 
(34.9,[0.0315,95.0,1.47,0.0,0.403,6.975,15.3,7.6534]) 

我们为RidgeRegressionWithSGD()设置了模型参数。

在本教程的*还有更多...*部分,我们基于两个极端值展示了收缩效应。

// Ridge regression Model parameters
 val numIterations = 1000
 val stepsSGD = .001
 val regularizationParam = 1.13 
  1. 使用上述参数调用RidgeRegressionWithSGD()和 LabeledPoint 创建和训练岭回归模型:
val myRidgeModel = RidgeRegressionWithSGD.train(RegressionDataSet, numIterations,stepsSGD, regularizationParam) 
  1. 使用我们的模型和数据集预测数值(类似于所有 Spark 回归方法):
val predictedLabelValue = RegressionDataSet.map { lp => val predictedValue = myRidgeModel.predict(lp.features)
   (lp.label, predictedValue)
 }
  1. 打印模型截距和权重。如果将值与线性回归进行比较,您将看到收缩效应。在选择具有更多共线性的数据集或极端值时,效果将更加明显:
println("Intercept set:",myRidgeModel.intercept)
 println("Model Weights:",myRidgeModel.weights) 

(Intercept set:,0.0) 
(Model Weights:,[-0.03570346878210774,0.2577081687536239,0.005415957423129407,0.004368409890400891, 0.026279497009143078,0.6130086051124276,0.19363086562068213,0.392655338663542])
  1. 随机选择 20 个预测数值并直观地检查预测结果(仅显示前五个值):
(23.9,15.121761357965845) 
(17.0,23.11542703857021) 
(20.5,24.075526274194395) 
(28.0,19.209708926376237) 
(13.3,23.386162089812697) 

. . . . .   
. . . . .
  1. 计算 RMSE 并显示结果:
val MSE = predictedLabelValue.map{ case(l, p) => math.pow((l - p), 2)}.reduce(_ + _) / predictedLabelValue.count
 val RMSE = math.sqrt(MSE)

 println("training Mean Squared Error = " + MSE)
 println("training Root Mean Squared Error = " + RMSE) 

输出如下:

training Mean Squared Error = 92.60723710764655
training Root Mean Squared Error = 9.623265407731752  

工作原理...

为了能够与其他回归方法进行比较并观察收缩效应,我们再次使用房屋数据并使用RidgeRegressionWithSGD.train训练模型。在拟合模型后,我们输出了刚刚训练的模型的截距和参数权重。然后我们使用*.predict()*API 预测数值。在输出 MSE 和 RMSE 之前,我们打印了预测值并直观地检查了前 20 个数字。

此方法构造函数的签名如下:

new RidgeRegressionWithSGD()

这些参数最终控制了拟合,因此需要一定程度的实验来实现良好的拟合。我们在上一个教程中看到了前两个参数。第三个参数将根据所选的值影响权重的收缩。您必须进行实验并使用模型选择技术来决定最终值。

参数的默认值:

  • stepSize = 1.0

  • numIterations = 100

  • regParm = 0.01

  • miniBatchFraction = 1.0

我们在第九章中详细介绍了优化和 L1(绝对值)与 L2(平方),“优化 - 使用梯度下降下山”,但是对于本教程的目的,读者应该了解岭回归使用 L2 进行惩罚(即收缩一些参数),而即将到来的教程“Spark 2.0 中使用 SGD 优化的套索回归”使用 L1 进行惩罚(即根据所使用的阈值消除一些参数)。我们鼓励用户将本教程的权重与线性回归和套索回归教程进行比较,以亲自看到效果。我们使用相同的房屋数据集来展示效果。

以下图表显示了带有正则化函数的岭回归:

简而言之,这是通过添加一个小的偏差因子(岭回归)来处理特征依赖的一种补救措施,它使用正则化惩罚来减少变量。岭回归会收缩解释变量,但永远不会将其设置为 0,不像套索回归会消除变量。

本示例的范围仅限于演示 Spark 中岭回归的 API 调用。岭回归的数学和深入解释是统计书籍中多章的主题。为了更好地理解,我们强烈建议读者在考虑 L1、L2、... L4 正则化以及岭回归和线性 PCA 之间的关系的同时,熟悉这个概念。

还有更多...

参数的收缩量因参数选择而异,但权重收缩需要共线性的存在。您可以通过使用真正随机(由随机生成器生成的 IID 解释变量)与高度相互依赖的特征(例如,腰围和体重)来自行证明这一点。

以下是极端正则化值的两个示例以及它们对模型权重和收缩的影响:

val regularizationParam = .00001
(Model Weights:,
[-0.0373404807799996, 0.25499013376755847, 0.0049174051853082094, 0.0046110262713086455, 0.027391063252456684, 0.6401656691002464, 0.1911635644638509, 0.4085780172461439 ]) 

val regularizationParam = 50
(Model Weights:,[-0.012912409941749588, 0.2792184353165915, 0.016208621185873275, 0.0014162706383970278, 0.011205887829385417, 0.2466274224421205, 0.2261797091664634, 0.1696120633704305])

如果模型的权重计算为 NaN,则必须更改模型参数(SGD 步骤或迭代次数),直到收敛。

这是一个由于参数选择不当而未正确计算模型权重的示例。第一步应该是使用更精细的 SGD 步骤参数:

(Model Weights:,[NaN,NaN,NaN,NaN,NaN,NaN,NaN,NaN])

另请参阅

以下是构造函数的文档:

spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.mllib.regression.RidgeRegressionWithSGD

Spark 2.0 中使用 SGD 优化的套索回归

在本示例中,我们将使用先前示例中的住房数据集,以演示 Spark 的基于 RDD 的套索回归LassoWithSGD(),它可以通过将其他权重设置为零(因此根据阈值消除一些参数)来选择一部分参数,同时减少其他参数的影响(正则化)。我们再次强调,岭回归减少了参数权重,但从不将其设置为零。

LassoWithSGD()是 Spark 的基于 RDD 的套索(最小绝对收缩和选择算子)API,这是一种回归方法,同时执行变量选择和正则化,以消除不贡献的解释变量(即特征),从而提高预测的准确性。基于普通最小二乘法OLS)的套索可以轻松扩展到其他方法,例如广义线性方法GLM)。

操作步骤...

  1. 在 IntelliJ 或您选择的 IDE 中启动新项目。确保包含必要的 JAR 文件。

  2. 设置程序所在的包位置:

package spark.ml.cookbook.chapter6
  1. 导入 SparkSession 所需的必要包,以便访问集群和Log4j.Logger以减少 Spark 产生的输出量:
import org.apache.spark.mllib.regression.{LabeledPoint, LassoWithSGD, LinearRegressionWithSGD, RidgeRegressionWithSGD}
 import org.apache.spark.sql.{SQLContext, SparkSession}
 import org.apache.spark.ml.classification.LogisticRegression
 import org.apache.spark.ml.tuning.{ParamGridBuilder, TrainValidationSplit}
 import org.apache.spark.mllib.linalg.{Vector, Vectors}
import org.apache.log4j.Logger
import org.apache.log4j.Level
  1. 使用构建器模式初始化 SparkSession,从而为 Spark 集群提供入口点:
val spark = SparkSession
 .builder
 .master("local[4]")
 .appName("myRegress04")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()
  1. 为了有效地演示岭回归模型参数的收缩(它将收缩到一个小值,但永远不会被消除),我们使用相同的住房数据文件,并清理并使用前八列来预测最后一列的值(中位数住房价格):
val data = sc.textFile("../data/sparkml2/chapter6/housing8.csv")
  1. 我们通过拆分数据并将其转换为双精度来摄取数据,同时构建一个 LabeledPoint(Spark 所需的数据结构)数据集:
val RegressionDataSet = data.map { line => 
val columns = line.split(',')

  LabeledPoint(columns(13).toDouble , Vectors.dense(columns(0).toDouble,columns(1).toDouble, columns(2).toDouble, columns(3).toDouble,columns(4).toDouble,

    columns(5).toDouble,columns(6).toDouble, columns(7).toDouble

  ))

} 
  1. 加载数据集后,我们检查数据集,这是一直建议的,并演示标签点的内部,这是一个值(即标签/因变量),后跟我们试图用来解释因变量的特征的 DenseVector:
   RegressionDataSet.collect().foreach(println(_)) 

(24.0,[0.00632,18.0,2.31,0.0,0.538,6.575,65.2,4.09]) 
   . . . . .  
. . . . . 
. . . . . 
. . . . . 
   (34.6,[0.03768,80.0,1.52,0.0,0.404,7.274,38.3,7.309]) 
(34.9,[0.0315,95.0,1.47,0.0,0.403,6.975,15.3,7.6534]) 
  1. 我们设置了lassoWithSGD()的模型参数。这些参数最终控制拟合,因此需要一定程度的实验来获得良好的拟合。我们在前面的配方中看到了前两个参数。第三个参数的值将影响权重的选择。您必须进行实验并使用模型选择技术来决定最终值。在本配方的更多内容部分,我们展示了基于两个极端值的特征的权重选择(即哪些权重设置为 0):
// Lasso regression Model parameters

val numIterations = 1000 
val stepsSGD = .001 
val regularizationParam = 1.13  
  1. 我们通过调用RidgeRegressionWithSGD()和我们的 LabeledPoint 来创建和训练岭回归模型,使用了前述参数:
val myRidgeModel = LassoWithSGD.train(RegressionDataSet, numIterations,stepsSGD, regularizationParam) 
  1. 我们使用我们的模型和数据集来预测值(与所有 Spark 回归方法类似):
val predictedLabelValue = RegressionDataSet.map { lp => val predictedValue = myRidgeModel.predict(lp.features) 
  (lp.label, predictedValue)

} 
  1. 我们打印我们的模型截距和权重。如果将值与线性或岭回归进行比较,您将看到选择效果。在极端值或选择具有更多共线性的数据集时,效果将更加显著。

在这个例子中,套索通过设置权重为 0.0 使用正则化参数(例如 4.13)消除了三个参数。

println("Intercept set:",myRidgeModel.intercept) 
println("Model Weights:",myRidgeModel.weights) 

(Intercept set:,0.0) 
(Model Weights:,[-0.0,0.2714890393052161,0.0,0.0,0.0,0.4659131582283458 ,0.2090072656520274,0.2753771238137026]) 

  1. 我们随机选择了 20 个预测值,并直观地检查了预测结果(这里只显示了前五个值):
(18.0,24.145326403899134) 
(29.1,25.00830500878278) 
(23.1,10.127919006877956) 
(18.5,21.133621139346403) 
(22.2,15.755470439755092) 
. . . . .   
. . . . .   
  1. 我们计算 RMSE 并展示结果:
val MSE = predictedLabelValue.map{ case(l, p) => math.pow((l - p), 2)}.reduce(_ + _) / predictedLabelValue.count

val RMSE = math.sqrt(MSE)

println("training Mean Squared Error = " + MSE) 
println("training Root Mean Squared Error = " + RMSE) 

输出如下:

training Mean Squared Error = 99.84312606110213
 training Root Mean Squared Error = 9.992153224460788

工作原理...

同样,我们使用了房屋数据,这样我们可以将这种方法与岭回归进行比较,并展示套索不仅像岭回归一样收缩参数,而且它会一直进行下去,并将那些没有显著贡献的参数设置为零。

此方法构造函数的签名如下:

new LassoWithSGD()

参数的默认值:

  • stepSize= 1.0

  • numIterations= 100

  • regParm= 0.01

  • miniBatchFraction= 1.0

作为提醒,岭回归减少了参数的权重,但并不会消除它们。在数据挖掘/机器学习中处理大量参数时,如果没有深度学习系统,通常会优先选择套索,以减少 ML 管道早期阶段的输入数量,至少在探索阶段。

由于套索能够根据阈值选择一部分权重(即参数),因此在高级数据挖掘和机器学习中扮演着重要角色。简而言之,套索回归根据阈值决定包括或排除哪些参数(即将权重设置为 0)。

虽然岭回归可以大大减少参数对整体结果的贡献,但它永远不会将权重减少到零。套索回归通过能够将特征的权重减少到零(因此选择了贡献最大的特征子集)而不同于岭回归。

更多内容...

参数选择(即将一些权重设置为零)随正则化参数值的变化而变化。

这是两个极端正则化值的例子,以及它们对模型权重和收缩的影响:

val regularizationParam = .30

在这种情况下,我们使用套索消除了一个参数:

   (Model Weights:,[-0.02870908693284211,0.25634834423693936,1.707233741603369E-4, 0.0,0.01866468882602282,0.6259954005818621,0.19327180817037548,0.39741266136942227]) 
val regularizationParam = 4.13

在这种情况下,我们使用套索消除了四个参数:

(Model Weights:,[-0.0,0.2714890393052161,0.0,0.0,0.0, 0.4659131582283458,0.2090072656520274,0.2753771238137026])

如果模型的权重计算为 NaN,则必须更改模型参数(即 SGD 步骤或迭代次数),直到收敛。

这是一个模型权重计算不正确的例子(即 SGD 中的收敛问题),这是由于参数选择不当。第一步应该是使用更精细的 SGD 步骤参数:

(Model Weights:,[NaN,NaN,NaN,NaN,NaN,NaN,NaN,NaN])

另请参阅

这是构造函数的文档:

spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.mllib.regression.LassoWithSGD

Spark 2.0 中使用 L-BFGS 优化的逻辑回归

在这个示例中,我们将再次使用 UCI 录取数据集,以便演示 Spark 基于 RDD 的逻辑回归解决方案,LogisticRegressionWithLBFGS(),用于某些类型的 ML 问题中存在的大量参数。

对于非常大的变量空间,我们建议使用 L-BFGS,因为可以使用更新来近似二阶导数的 Hessian 矩阵。如果您的 ML 问题涉及数百万或数十亿个参数,我们建议使用深度学习技术。

如何做...

  1. 我们使用 UCLA IDRE 的录取数据集。您可以从以下网址下载整个数据集:

数据集包括四列,第一列是因变量(即标签-学生是否被录取),接下来的三列是自变量(解释学生录取的特征)。

我们选择并清理了前八列作为特征。我们使用前 200 行来训练和预测中位数价格。

以下是前三行的一些示例数据:

  1. 在 IntelliJ 或您选择的 IDE 中开始一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序所在的包位置:

package spark.ml.cookbook.chapter6
  1. 导入 SparkSession 所需的包,以便访问集群和Log4j.Logger以减少 Spark 产生的输出量:
import org.apache.spark.mllib.linalg.Vectors
 import org.apache.spark.mllib.regression.LabeledPoint
 import org.apache.spark.mllib.classification.LogisticRegressionWithLBFGS
 import org.apache.spark.sql.{SQLContext, SparkSession}
 import org.apache.log4j.Logger
 import org.apache.log4j.Level
  1. 使用构建器模式初始化 SparkSession,从而为 Spark 集群提供入口点:
val spark = SparkSession
 .builder
.master("local[4]")
 .appName("myRegress06")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()
  1. 加载数据文件并将其转换为 RDD:
val data = sc.textFile("../data/sparkml2/chapter6/admission1.csv")
  1. 通过拆分数据并转换为双精度来摄取数据,同时构建一个 LabeledPoint(即 Spark 所需的数据结构)数据集。列 1(即位置 0)是回归中的因变量,而列 2 到 4(即 GRE、GPA、Rank)是特征:
val RegressionDataSet = data.map { line =>
   val columns = line.split(',')

   LabeledPoint(columns(0).toDouble , Vectors.dense(columns(1).toDouble,columns(2).toDouble, columns(3).toDouble ))

 }
  1. 加载后检查数据集,这是一直建议的,还演示标签点内部,这是一个单一值(即标签/因变量),后面是我们试图用来解释因变量的特征的 DenseVector:
RegressionDataSet.collect().foreach(println(_)) 

(0.0,[380.0,3.61,3.0]) 
(1.0,[660.0,3.67,3.0]) 
(1.0,[800.0,4.0,1.0]) 
(1.0,[640.0,3.19,4.0])      
   . . . . .  
. . . . . 
. . . . . 
. . . . . 

  1. 使用新运算符创建一个 LBFGS 回归对象,并将截距设置为 false,以便我们可以与logisticregressionWithSGD()示例进行公平比较:
val myLBFGSestimator = new LogisticRegressionWithLBFGS().setIntercept(false) 
  1. 使用run()方法使用数据集创建训练好的模型(即结构化为 LabeledPoint):
val model1 = myLBFGSestimator.run(RegressionDataSet)
  1. 模型已经训练好。使用predict()方法来预测并分类相应的组。在接下来的行中,只需使用一个密集向量来定义两个学生的数据(GRE、GPA 和 Rank 特征),并让它预测学生是否被录取(0 表示被拒绝,1 表示学生将被录取):
// predict a single applicant on the go
 val singlePredict1 = model1.predict(Vectors.dense(700,3.4, 1))
 println(singlePredict1)

val singlePredict2 = model1.predict(Vectors.dense(150,3.4, 1))
 println(singlePredict2) 

输出将如下所示:

1.0   
0.0   
  1. 为了展示一个稍微复杂的过程,为五名学生定义一个 SEQ 数据结构,并尝试在下一步使用map()predict()来批量预测。很明显,此时可以读取任何数据文件并转换,以便我们可以预测更大的数据块:
   val newApplicants=Seq(
   (Vectors.dense(380.0, 3.61, 3.0)),
   (Vectors.dense(660.0, 3.67, 3.0)),
   (Vectors.dense(800.0, 1.3, 1.0)),
   (Vectors.dense(640.0, 3.19, 4.0)),
   (Vectors.dense(520.0, 2.93, 1.0))
 )
  1. 现在使用map()predict()来运行 SEQ 数据结构,并使用训练好的模型批量产生预测:
 val predictedLabelValue = newApplicants.map {lp => val predictedValue =  model1.predict(lp)
   ( predictedValue)
 }
  1. 查看学生的输出和预测结果。0 或 1 的存在表示基于模型的学生被拒绝或接受:
predictedLabelValue.foreach(println(_)) 

Output: 
0.0 
0.0 
1.0 
0.0 
1.0 

工作原理...

我们使用 UCI 入学数据和LogisticRegressionWithLBFGS()来预测学生是否被录取。截距被设置为 false,使用.run().predict() API 来预测拟合模型。这里的重点是 L-BFGS 适用于大量参数,特别是在存在大量稀疏性时。无论使用了什么优化技术,我们再次强调岭回归可以减少参数权重,但永远不会将其设置为零。

此方法构造函数的签名如下:

LogisticRegressionWithLBFGS ()

Spark 中的 L-BFGS 优化,L-BFGS(),基于牛顿优化算法(在点处使用曲率和曲线的 2^(nd)导数),可以被认为是寻找可微函数上的稳定点的最大似然函数。这种算法的收敛应该特别注意(也就是说,需要最优或梯度为零)。

请注意,本示例仅用于演示目的,您应该使用第四章中演示的评估指标进行模型评估和最终选择过程。

还有更多...

LogisticRegressionWithLBFGS()对象有一个名为setNumClasses()的方法,允许它处理多项式(也就是说,超过两个组)。默认情况下,它设置为二,这是二元逻辑回归。

L-BFGS 是原始 BFGS(Broyden-Fletcher-Goldfarb-Shanno)方法的有限内存适应。L-BFGS 非常适合处理大量变量的回归模型。它是一种带有有限内存的 BFGS 近似,它试图在搜索大搜索空间时估计 Hessian 矩阵。

我们鼓励读者退后一步,将问题视为回归加上优化技术(使用 SGD 的回归与使用 L-BFGS 的回归)。在这个示例中,我们使用了逻辑回归,它本身是线性回归的一种形式,只是标签是离散的,再加上一个优化算法(也就是说,我们选择了 L-BFGS 而不是 SGD)来解决问题。

为了欣赏 L-BFGS 的细节,必须了解 Hessian 矩阵及其作用,以及在优化中使用稀疏矩阵配置时出现的大量参数(Hessian 或 Jacobian 技术)的困难。

另请参阅

这是构造函数的文档:

spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.mllib.classification.LogisticRegressionWithLBFGS

Spark 2.0 中的支持向量机(SVM)

在这个示例中,我们使用 Spark 的基于 RDD 的 SVM API SVMWithSGD与 SGD 将人口分类为两个二进制类,并使用计数和BinaryClassificationMetrics来查看模型性能。

为了节省时间和空间,我们使用了已经提供给 Spark 的样本LIBSVM格式,但提供了由台湾大学提供的额外数据文件的链接,以便读者可以自行进行实验。支持向量机SVM)作为一个概念基本上非常简单,除非你想深入了解它在 Spark 或任何其他软件包中的实现细节。

虽然 SVM 背后的数学超出了本书的范围,但鼓励读者阅读以下教程和原始 SVM 论文,以便更深入地理解。

原始文件是由VapnikChervonenkis(1974 年,1979 年-俄语)编写的,还有Vapnik的 1982 年翻译他的 1979 年著作:

www.amazon.com/Statistical-Learning-Theory-Vladimir-Vapnik/dp/0471030031

对于更现代的写作,我们建议从我们的图书馆中选择以下三本书:

  • V. Vapnik 的《统计学习理论的本质》

www.amazon.com/Statistical-Learning-Information-Science-Statistics/dp/0387987800

  • B. Scholkopf 和 A. Smola 的《使用核方法学习:支持向量机、正则化、优化和更多》

mitpress.mit.edu/books/learning-kernels

  • K. Murphy 的《机器学习:概率视角》

mitpress.mit.edu/books/machine-learning-0

如何做...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序所在的包位置:

package spark.ml.cookbook.chapter6
  1. 导入必要的 SparkSession 包以访问集群和Log4j.Logger以减少 Spark 产生的输出量:
import org.apache.spark.mllib.util.MLUtils
 import org.apache.spark.mllib.classification.{SVMModel, SVMWithSGD}
 import org.apache.spark.mllib.evaluation.{BinaryClassificationMetrics, MultilabelMetrics, binary}
 import org.apache.spark.sql.{SQLContext, SparkSession}
import org.apache.log4j.Logger
import org.apache.log4j.Level
  1. 使用构建模式初始化 SparkSession,从而为 Spark 集群提供入口点:
val spark = SparkSession
 .builder
.master("local[4]")
 .appName("mySVM07")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()
  1. Spark 提供了 MLUtils 包,使我们能够读取任何格式正确的libsvm文件。我们使用LoadLibSVMFile()来加载 Spark 附带的一个短样本文件(100 行),以便进行简单的实验。sample_libsvm_data文件可以在 Spark 主目录的.../data/mlib/目录中找到。我们只需将文件复制到我们自己的 Windows 机器上的目录中:
val dataSetSVM = MLUtils.loadLibSVMFile(sc," ../data/sparkml2/chapter6/sample_libsvm_data.txt") 
  1. 打印并检查样本文件的内容输出。输出中包含了简短版本的内容以供参考:
println("Top 10 rows of LibSVM data")
 dataSetSVM.collect().take(10).foreach(println(_)) 

Output: 
(0.0,(692,[127,128,129,130,131,154, .... ])) 
(1.0,(692,[158,159,160,161,185,186, .... ])) 

  1. 检查确保所有数据都已加载,并且文件没有重复:
println(" Total number of data vectors =", dataSetSVM.count())
 val distinctData = dataSetSVM.distinct().count()
 println("Distinct number of data vectors = ", distinctData) 

Output: 
( Total number of data vectors =,100) 
(Distinct number of data vectors = ,100) 
  1. 在这一步中,将数据分成两组(80/20),并准备相应地训练模型。allDataSVM变量将根据拆分比例随机选择两个部分。这些部分可以通过索引 0 和 1 来引用,分别指代训练和测试数据集。您还可以在randomSplit()中使用第二个参数来定义随机拆分的初始种子:
val trainingSetRatio = .20
 val populationTestSetRatio = .80

val splitRatio = Array(trainingSetRatio, populationTestSetRatio) 

 val allDataSVM = dataSetSVM.randomSplit(splitRatio)
  1. 将迭代次数设置为 100。接下来的两个参数是 SGD 步骤和正则化参数-我们在这里使用默认值,但您必须进行实验以确保算法收敛:
val numIterations = 100 

 val myModelSVM = SVMWithSGD.train(allDataSVM(0), numIterations,1,1)
  1. 在上一步中训练模型之后,现在使用map()predict()函数来预测测试数据的结果(即拆分数据的索引 1):
val predictedClassification = allDataSVM(1).map( x => (myModelSVM.predict(x.features), x.label)) 
  1. 通过输出直观地检查预测(为方便起见进行了缩短)。接下来的步骤尝试量化我们的预测效果:
   predictedClassification.collect().foreach(println(_)) 

(1.0,1.0) 
(1.0,1.0) 
(1.0,1.0) 
(1.0,1.0) 
(0.0,0.0) 
(0.0,1.0) 
(0.0,0.0) 
   ....... 
   ....... 
  1. 首先,使用快速计数/比率方法来感受准确度。由于我们没有设置种子,数字将因运行而异(但保持稳定):
 val falsePredictions = predictedClassification.filter(p => p._1 != p._2)

println(allDataSVM(0).count())
 println(allDataSVM(1).count())

println(predictedClassification.count())
 println(falsePredictions.count()) 

Output: 
13 
87 
87 
2 
  1. 现在使用更正式的方法来量化 ROC(即曲线下面积)。这是最基本的准确度标准之一。读者可以在这个主题上找到许多教程。我们使用标准和专有方法(即手工编码)的组合来量化测量。

  2. Spark 自带了一个二元分类量化测量。使用这个来收集测量:

val metrics = new BinaryClassificationMetrics(predictedClassification) 

  1. 访问areaUnderROC()方法以获取 ROC 测量:
val areaUnderROCValue = metrics.areaUnderROC() 

  println("The area under ROC curve = ", areaUnderROCValue) 

  Output: 
  (The area under ROC curve = ,0.9743589743589743) 

工作原理...

我们使用了 Spark 提供的样本数据,格式为LIBSVM,来运行 SVM 分类配方。在读取文件后,我们使用SVMWithSGD.train来训练模型,然后继续将数据预测为两组标记输出,0 和 1。我们使用BinaryClassificationMetrics指标来衡量性能。我们专注于一个流行的指标,即 ROC 曲线下面积,使用metrics.areaUnderROC()来衡量性能。

该方法构造函数的签名如下:

new SVMWithSGD()

参数的默认值:

  • stepSize= 1.0

  • numIterations= 100

  • regParm= 0.01

  • miniBatchFraction= 1.0

建议读者尝试各种参数以获得最佳设置。

SVM 之所以伟大,是因为一些点落在错误的一侧是可以接受的,但模型会惩罚模型选择最佳拟合。

Spark 中的 SVM 实现使用 SGD 优化来对特征集的标签进行分类。当我们在 Spark 中使用 SVM 时,我们需要将数据准备成一种称为libsvm的格式。用户可以使用以下链接了解格式,并从国立台湾大学获取libsvm格式的现成数据集:

www.csie.ntu.edu.tw/~cjlin/libsvm/

www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/

简而言之,libsvm格式如下:

<label> <index1>:<value1> <index2>:<value2> ...

可以使用 Python 或 Scala 程序创建管道,将文本文件转换为所需的libsvm格式。

Spark 在/data/mlib目录中有大量各种算法的示例文件。我们鼓励读者在熟悉 Spark MLlib 算法时使用这些文件:

SVMWithSGD() 

接收器操作特性ROC)是一个图形绘图,说明了二元分类器系统的诊断能力,随着其判别阈值的变化。

ROC 的教程可以在以下链接找到:

en.wikipedia.org/wiki/Receiver_operating_characteristic

还有更多...

您可以使用libsvm格式的公开可用数据源,也可以使用 Spark API 调用SVMDataGenerator(),该调用会生成 SVM 的样本数据(即,高斯分布):

object SVMDataGenerator() 

SVM 背后的想法可以总结如下:不是使用线性判别(例如,在许多线中选择一条线)和目标函数(例如,最小二乘法)来分隔和标记左侧变量,而是首先使用最大分隔边界(如下图所示),然后在最大边界之间绘制实线。另一种思考方式是如何使用两条线(下图中的虚线)来最大程度地分隔类别(即,最好和最具有歧视性的分隔器)。简而言之,我们能够分隔类别的越宽,歧视性就越好,因此,在标记类别时更准确。

执行以下步骤以了解有关 SVM 的更多信息:

  1. 选择最能够分隔两组的最宽边界。

  2. 其次,绘制一条分隔最宽边界的线。这将作为线性判别。

  3. 目标函数:最大化两条分隔线。

另请参阅

这是构造函数的文档:

spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.mllib.classification.SVMWithSGD

使用 Spark 2.0 MLlib 的朴素贝叶斯机器学习

在这个示例中,我们使用著名的鸢尾花数据集,并使用 Apache Spark API NaiveBayes()来对给定的一组观测属于三类花中的哪一类进行分类/预测。这是一个多类分类器的示例,并需要多类度量来衡量拟合度。之前的示例使用了二元分类和度量来衡量拟合度。

如何做...

  1. 对于朴素贝叶斯练习,我们使用一个名为iris.data的著名数据集,可以从 UCI 获得。该数据集最初是由 R. Fisher 在 1930 年代引入的。该集是一个多元数据集,具有被分类为三组的花属性测量。

简而言之,通过测量四列,我们试图将一种物种分类为三类鸢尾花中的一类(即,鸢尾花 Setosa,鸢尾花 Versicolor,鸢尾花 Virginica)。

我们可以从这里下载数据:

archive.ics.uci.edu/ml/datasets/Iris/

列定义如下:

    • 以厘米为单位的萼片长度
  • 以厘米为单位的萼片宽度

  • 以厘米为单位的花瓣长度

  • 以厘米为单位的花瓣宽度

  • 类:

      • -- 鸢尾花山鸢尾 ⇒ 用 0 替换
  • -- 鸢尾花变色鸢尾 ⇒ 用 1 替换

  • -- 鸢尾花维吉尼亚 ⇒ 用 2 替换

我们需要对数据执行的步骤/操作如下:

    • 下载并用数字值替换第五列(即标签或分类类别),从而生成 iris.data.prepared 数据文件。朴素贝叶斯调用需要数字标签而不是文本,这在大多数工具中都很常见。
  • 删除文件末尾的额外行。

  • 使用distinct()调用在程序内去除重复项。

  1. 在 IntelliJ 或您选择的 IDE 中开始一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序所在的包位置:

package spark.ml.cookbook.chapter6
  1. 导入 SparkSession 所需的包,以访问集群和Log4j.Logger以减少 Spark 产生的输出量:

 import org.apache.spark.mllib.linalg.{Vector, Vectors}
 import org.apache.spark.mllib.regression.LabeledPoint
 import org.apache.spark.mllib.classification.{NaiveBayes, NaiveBayesModel}
 import org.apache.spark.mllib.evaluation.{BinaryClassificationMetrics, MulticlassMetrics, MultilabelMetrics, binary}
 import org.apache.spark.sql.{SQLContext, SparkSession}

 import org.apache.log4j.Logger
 import org.apache.log4j.Level
  1. 初始化一个 SparkSession,使用构建器模式指定配置,从而为 Spark 集群提供一个入口点:
val spark = SparkSession
 .builder
 .master("local[4]")
 .appName("myNaiveBayes08")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()
  1. 加载iris.data文件并将数据文件转换为 RDDs:
val data = sc.textFile("../data/sparkml2/chapter6/iris.data.prepared.txt") 
  1. 使用map()解析数据,然后构建一个 LabeledPoint 数据结构。在这种情况下,最后一列是标签,前四列是特征。同样,我们将最后一列的文本(即鸢尾花的类别)替换为相应的数值(即 0、1、2):
val NaiveBayesDataSet = data.map { line =>
   val columns = line.split(',')

   LabeledPoint(columns(4).toDouble , Vectors.dense(columns(0).toDouble,columns(1).toDouble,columns(2).toDouble,columns(3).toDouble ))

 }
  1. 然后确保文件不包含任何冗余行。在这种情况下,有三行冗余。我们将使用不同的数据集继续:
println(" Total number of data vectors =", NaiveBayesDataSet.count())
 val distinctNaiveBayesData = NaiveBayesDataSet.distinct()
 println("Distinct number of data vectors = ", distinctNaiveBayesData.count()) 

Output: 

(Total number of data vectors =,150) 
(Distinct number of data vectors = ,147) 
  1. 我们通过检查输出来检查数据:
distinctNaiveBayesData.collect().take(10).foreach(println(_)) 

Output: 
(2.0,[6.3,2.9,5.6,1.8]) 
(2.0,[7.6,3.0,6.6,2.1]) 
(1.0,[4.9,2.4,3.3,1.0]) 
(0.0,[5.1,3.7,1.5,0.4]) 
(0.0,[5.5,3.5,1.3,0.2]) 
(0.0,[4.8,3.1,1.6,0.2]) 
(0.0,[5.0,3.6,1.4,0.2]) 
(2.0,[7.2,3.6,6.1,2.5]) 
.............. 
................ 
............. 
  1. 使用 30%和 70%的比例将数据分割为训练集和测试集。在这种情况下,13L 只是一个种子数(L 代表长数据类型),以确保在使用randomSplit()方法时结果不会因运行而改变:
val allDistinctData = distinctNaiveBayesData.randomSplit(Array(.30,.70),13L)
 val trainingDataSet = allDistinctData(0)
 val testingDataSet = allDistinctData(1)
  1. 打印每个集合的计数:
println("number of training data =",trainingDataSet.count())
 println("number of test data =",testingDataSet.count()) 

Output: 
(number of training data =,44) 
(number of test data =,103) 
  1. 使用train()和训练数据集构建模型:
         val myNaiveBayesModel = NaiveBayes.train(trainingDataSet) 
  1. 使用训练数据集加上map()predict()方法根据它们的特征对花进行分类:
val predictedClassification = testingDataSet.map( x => (myNaiveBayesModel.predict(x.features), x.label)) 
  1. 通过输出检查预测:
predictedClassification.collect().foreach(println(_)) 

(2.0,2.0) 
(1.0,1.0) 
(0.0,0.0) 
(0.0,0.0) 
(0.0,0.0) 
(2.0,2.0) 
....... 
....... 
.......
  1. 使用MulticlassMetrics()创建多类分类器的度量。提醒一下,这与之前的方法不同,之前我们使用的是BinaryClassificationMetrics()
val metrics = new MulticlassMetrics(predictedClassification) 
  1. 使用常用的混淆矩阵来评估模型:
val confusionMatrix = metrics.confusionMatrix
 println("Confusion Matrix= \n",confusionMatrix) 

Output: 
   (Confusion Matrix=  
   ,35.0  0.0   0.0    
    0.0   34.0  0.0    
    0.0   14.0  20.0  ) 
  1. 我们检查其他属性来评估模型:
val myModelStat=Seq(metrics.precision,metrics.fMeasure,metrics.recall)
 myModelStat.foreach(println(_)) 

Output: 
0.8640776699029126 
0.8640776699029126 
0.8640776699029126  

工作原理...

我们使用了 IRIS 数据集进行这个方法,但是我们提前准备了数据,然后使用NaiveBayesDataSet.distinct() API 选择了不同数量的行。然后我们使用NaiveBayes.train() API 训练模型。在最后一步,我们使用.predict()进行预测,然后通过MulticlassMetrics()评估模型性能,输出混淆矩阵、精度和 F-度量。

这里的想法是根据选择的特征集(即特征工程)对观察结果进行分类,使其对应于左侧的标签。这里的不同之处在于,我们将联合概率应用于分类的条件概率。这个概念被称为贝叶斯定理,最初由 18 世纪的托马斯·贝叶斯提出。必须满足独立性的强烈假设,以使贝叶斯分类器正常工作。

在高层次上,我们实现这种分类方法的方式是简单地将贝叶斯定理应用于我们的数据集。作为基本统计学的复习,贝叶斯定理可以写成如下形式:

该公式说明了给定 B 为真时 A 为真的概率等于给定 A 为真时 B 为真的概率乘以 A 为真的概率除以 B 为真的概率。这是一个复杂的句子,但如果我们退后一步思考,它就会有意义。

贝叶斯分类器是一个简单而强大的分类器,允许用户考虑整个概率特征空间。要欣赏其简单性,必须记住概率和频率是同一枚硬币的两面。贝叶斯分类器属于增量学习器类,遇到新样本时会更新自身。这使得模型能够在新观察到达时即时更新自身,而不仅仅在批处理模式下运行。

还有更多...

我们使用不同的指标评估了一个模型。由于这是一个多类分类器,我们必须使用MulticlassMetrics()来检查模型的准确性。

有关更多信息,请参见以下链接:

spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.mllib.evaluation.MulticlassMetrics

另请参阅

这是构造函数的文档:

spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.ml.classification.NaiveBayes

在 Spark 2.0 中使用逻辑回归探索 ML 管道和数据框

我们已经尽力以尽可能简单的方式详细呈现代码,以便您可以开始,而无需使用 Scala 的额外语法糖。

准备工作

在这个教程中,我们将 ML 管道和逻辑回归结合起来,以演示如何将各种步骤组合成单个管道,该管道在数据框上操作,使其在转换和传输过程中。我们跳过了一些步骤,比如数据拆分和模型评估,并将它们保留到后面的章节中,以使程序更短,但提供了管道、数据框、估计器和转换器的全面处理。

这个教程探讨了管道和数据框在管道中传输并进行操作的细节。

如何做...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序所在的包位置:

package spark.ml.cookbook.chapter6
  1. 导入LogisticRegression包,以构建和训练模型所需。在 Spark MLlib 中有其他形式的LogisticRegression,但现在我们只集中在基本的逻辑回归方法上:
import org.apache.spark.ml.classification.LogisticRegression
  1. 导入 SparkSession,以便我们可以通过 SparkSession 获得对集群、Spark SQL 以及 DataFrame 和 Dataset 抽象的访问:
org.apache.spark.sql.SparkSession
  1. ml.linlang导入 Vector 包。这将允许我们从 Spark 生态系统中导入和使用向量,包括密集和稀疏向量:
import org.apache.spark.ml.linalg.Vector
  1. log4j导入必要的包,这样我们就可以将输出级别设置为 ERROR,并使程序的输出更简洁:
import org.apache.log4j.Logger
 import org.apache.log4j.Level
  1. 使用导入的 SparkSession 设置各种参数,以成功初始化并获得对 Spark 集群的控制。在 Spark 2.0 中,实例化和访问 Spark 的方式已经发生了变化。有关更多详细信息,请参阅本教程中的*There's more...*部分。

  2. 设置参数如下:

val spark = SparkSession
 .builder
 .master("local[*]")
 .appName("myfirstlogistic")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()
  1. 设置您需要的 Spark 集群类型,并定义获取对 Spark 的访问所需的其他参数。

  2. 在这里,将其设置为本地集群,并让它抓取尽可能多的线程/核心。您可以使用数字而不是*来告诉 Spark 确切有多少核心/线程

master("local[*]")
  1. 您可以选择指定要分配的确切核心数,而不是使用*
master("local[2]")

这将分配两个核心。这在资源有限的较小笔记本电脑上可能会很方便。

  1. 设置应用程序名称,以便在集群上运行多个应用程序时易于跟踪:
appName("myfirstlogistic")
  1. 相对于 Spark 主目录配置工作目录:
config("spark.sql.warehouse.dir", ".")
  1. 现在我们继续构建所需的数据结构,以容纳下载的学生入学数据的前 20 行(请参阅前一个示例):
val trainingdata=Seq(
 (0.0, Vectors.dense(380.0, 3.61, 3.0)),
 (1.0, Vectors.dense(660.0, 3.67, 3.0)),
 (1.0, Vectors.dense(800.0, 1.3, 1.0)),
 (1.0, Vectors.dense(640.0, 3.19, 4.0)),
 (0.0, Vectors.dense(520.0, 2.93, 4.0)),
 (1.0, Vectors.dense(760.0, 3.00, 2.0)),
 (1.0, Vectors.dense(560.0, 2.98, 1.0)),
 (0.0, Vectors.dense(400.0, 3.08, 2.0)),
 (1.0, Vectors.dense(540.0, 3.39, 3.0)),
 (0.0, Vectors.dense(700.0, 3.92, 2.0)),
 (0.0, Vectors.dense(800.0, 4.0, 4.0)),
 (0.0, Vectors.dense(440.0, 3.22, 1.0)),
 (1.0, Vectors.dense(760.0, 4.0, 1.0)),
 (0.0, Vectors.dense(700.0, 3.08, 2.0)),
 (1.0, Vectors.dense(700.0, 4.0, 1.0)),
 (0.0, Vectors.dense(480.0, 3.44, 3.0)),
 (0.0, Vectors.dense(780.0, 3.87, 4.0)),
 (0.0, Vectors.dense(360.0, 2.56, 3.0)),
 (0.0, Vectors.dense(800.0, 3.75, 2.0)),
 (1.0, Vectors.dense(540.0, 3.81, 1.0))
 )
  1. 理解给定行的最佳方法是将其视为两部分:
  • 标签 - 0.0,表示学生未被录取。

  • 特征向量 - Vectors.dense(380.0, 3.61, 3.0),显示了学生的 GRE、GPA 和 RANK。我们将在接下来的章节中介绍密集向量的细节。

  1. SEQ 是具有特殊属性的 Scala 集合。序列可以被视为具有定义顺序的可迭代数据结构。有关 SEQ 的更多信息,请参阅以下 URL:

www.scala-lang.org/api/current/index.html#scala.collection.Seq

  1. 下一步将 SEQ 结构转换为 DataFrame。我们强烈建议您在任何新编程中使用 DataFrame 和 Dataset,而不是低级别的 RDD,以便与新的 Spark 编程范式保持一致:
val trainingDF = spark.createDataFrame(trainingdata).toDF("label", "features")

labelfeature将成为 DataFrame 中的列标题。

  1. Estimator 是一个接受 DataFrame 作为其数据的 API 抽象,并通过调用Fit函数生成实际模型。在这里,我们从 Spark MLlib 中的LogisticRegression类创建一个 Estimator,然后将最大迭代次数设置为 80;默认值为 100。我们将正则化参数设置为 0.01,并告诉模型我们也想拟合一个截距:
val lr_Estimator = new LogisticRegression().setMaxIter(80).setRegParam(0.01).setFitIntercept(true)
  1. 为了更好地了解程序的功能,请查看以下输出并检查参数:
println("LogisticRegression parameters:\n" + lr_Estimator.explainParams() + "\n")

输出如下:

Admission_lr_Model parameters:
{
logreg_34d0e7f2a3f9-elasticNetParam: 0.0,
logreg_34d0e7f2a3f9-featuresCol: features,
logreg_34d0e7f2a3f9-fitIntercept: true,
logreg_34d0e7f2a3f9-labelCol: label,
logreg_34d0e7f2a3f9-maxIter: 80,
logreg_34d0e7f2a3f9-predictionCol: prediction,
logreg_34d0e7f2a3f9-probabilityCol: probability,
logreg_34d0e7f2a3f9-rawPredictionCol: rawPrediction,
logreg_34d0e7f2a3f9-regParam: 0.01,
logreg_34d0e7f2a3f9-standardization: true,
logreg_34d0e7f2a3f9-threshold: 0.5,
logreg_34d0e7f2a3f9-tol: 1.0E-6
}
  1. 以下是如何解释和理解前一步中列出的Admission_lr_Model参数:
  • elasticNetParam: ElasticNet 混合参数,范围为[0, 1]。对于 alpha = 0,惩罚是 L2 惩罚。对于 alpha = 1,它是 L1 惩罚(默认值:0.0)。

  • featuresCol: 特征列名称(默认值:features)。

  • fitIntercept: 是否拟合截距项(默认值:true,当前值:true)。

  • labelCol: 标签列名称(默认值:label)。

  • maxIter: 最大迭代次数(>= 0)(默认值:100,当前值:80)。

  • predictionCol: 预测列名称(默认值:prediction)。

  • probabilityCol: 预测类条件概率的列名。请注意,并非所有模型都输出经过良好校准的概率估计!这些概率应被视为置信度,而不是精确概率(默认值:probability)。

  • rawPredictionCol: 原始预测,也称为置信度,列名(默认值:rawPrediction)。

  • regParam: 正则化参数(>= 0)(默认值:0.0,当前值:0.01)。

  • standardization: 在拟合模型之前是否对训练特征进行标准化(默认值:true)。

  • threshold: 二元分类预测中的阈值,范围为[0, 1](默认值:0.5)。

  • thresholds: 多类分类中的阈值,用于调整预测每个类的概率。数组的长度必须等于类的数量,其值>= 0。预测具有最大值 p/t 的类,其中 p 是该类的原始概率,t 是类的阈值(未定义)。

  • tol: 迭代算法的收敛容限(默认值:1.0E-6)。

  1. 现在调用 fit 准备好的 DataFrame 并生成我们的逻辑回归模型:
val Admission_lr_Model=lr_Estimator.fit(trainingDF)
  1. 现在探索模型摘要,以了解拟合后我们得到了什么。我们需要了解组件,以便知道要提取什么内容进行下一步操作:
println(Admission_lr_Model.summary.predictions)

以下是输出:

Admission_lr_Model Summary:
[label: double, features: vector ... 3 more fields]
  1. 现在从我们的训练 DataFrame 构建实际和最终模型。取我们创建的 Estimator,并让它通过执行 transform 函数来运行模型。现在我们将有一个新的 DataFrame,其中所有部分都被填充(例如,预测)。打印我们的 DataFrame 的模式,以了解新填充的 DataFrame 的情况:
// Build the model and predict
 val predict=Admission_lr_Model.transform(trainingDF)

这是实际的转换步骤。

  1. 打印模式以了解新填充的 DataFrame:
// print a schema as a guideline
predict.printSchema()

输出如下:

root
|-- label: double (nullable = false)
|-- features: vector (nullable = true)
|-- rawPrediction: vector (nullable = true)
|-- probability: vector (nullable = true)
|-- prediction: double (nullable = true)

前两列是我们的标签和特征向量,就像我们在 API 调用中设置的那样,当转换为 DataFrame 时。rawPredictions列被称为置信度。概率列将包含我们的概率对。最后一列,预测,将是我们模型预测的结果。这向我们展示了拟合模型的结构以及每个参数可用的信息。

  1. 我们现在继续提取回归模型的参数。为了使代码清晰简单,我们将每个参数的属性分别提取到一个集合中:
// Extract pieces that you need looking at schema and parameter
// explanation output earlier in the program
// Code made verbose for clarity
val label1=predict.select("label").collect()
val features1=predict.select("features").collect()
val probability=predict.select("probability").collect()
val prediction=predict.select("prediction").collect()
val rawPrediction=predict.select("rawPrediction").collect()
  1. 仅供信息目的,我们打印原始训练集的数量:
println("Training Set Size=", label1.size )

输出如下:

(Training Set Size=,20)
  1. 我们现在继续提取每一行的模型预测(结果、置信度和概率):
println("No. Original Feature Vector Predicted Outcome confidence probability")
 println("--- --------------------------- ---------------------- 
 ------------------------- --------------------")
 for( i <- 0 to label1.size-1) {
 print(i, " ", label1(i), features1(i), " ", prediction(i), " ", rawPrediction(i), " ", probability(i))
 println()
 }

输出如下:

No. Original Feature Vector Predicted Outcome confidence probability
--- --------------------------- ---------------------- ------------------------- --------------------
(0, ,[0.0],[[380.0,3.61,3.0]], ,[0.0], ,[[1.8601472910617978,-1.8601472910617978]], ,[[0.8653141150964327,0.13468588490356728]])
(1, ,[1.0],[[660.0,3.67,3.0]], ,[0.0], ,[[0.6331801846053525,-0.6331801846053525]], ,[[0.6532102092668394,0.34678979073316063]])
(2, ,[1.0],[[800.0,1.3,1.0]], ,[1.0], ,[[-2.6503754234982932,2.6503754234982932]], ,[[0.06596587423646814,0.9340341257635318]])
(3, ,[1.0],[[640.0,3.19,4.0]], ,[0.0], ,[[1.1347022244505625,-1.1347022244505625]], ,[[0.7567056336714486,0.2432943663285514]])
(4, ,[0.0],[[520.0,2.93,4.0]], ,[0.0], ,[[1.5317564062962097,-1.5317564062962097]], ,[[0.8222631520883197,0.17773684791168035]])
(5, ,[1.0],[[760.0,3.0,2.0]], ,[1.0], ,[[-0.8604923106990942,0.8604923106990942]], ,[[0.2972364981043905,0.7027635018956094]])
(6, ,[1.0],[[560.0,2.98,1.0]], ,[1.0], ,[[-0.6469082170084807,0.6469082170084807]], ,[[0.3436866013868022,0.6563133986131978]])
(7, ,[0.0],[[400.0,3.08,2.0]], ,[0.0], ,[[0.803419600659086,-0.803419600659086]], ,[[0.6907054912633392,0.30929450873666076]])
(8, ,[1.0],[[540.0,3.39,3.0]], ,[0.0], ,[[1.0192401951528316,-1.0192401951528316]], ,[[0.7348245722723596,0.26517542772764036]])
(9, ,[0.0],[[700.0,3.92,2.0]], ,[1.0], ,[[-0.08477122662243242,0.08477122662243242]], ,[[0.4788198754740347,0.5211801245259653]])
(10, ,[0.0],[[800.0,4.0,4.0]], ,[0.0], ,[[0.8599949503972665,-0.8599949503972665]], ,[[0.7026595993369233,0.29734040066307665]])
(11, ,[0.0],[[440.0,3.22,1.0]], ,[0.0], ,[[0.025000247291374955,-0.025000247291374955]], ,[[0.5062497363126953,0.49375026368730474]])
(12, ,[1.0],[[760.0,4.0,1.0]], ,[1.0], ,[[-0.9861694953382877,0.9861694953382877]], ,[[0.27166933762974904,0.728330662370251]])
(13, ,[0.0],[[700.0,3.08,2.0]], ,[1.0], ,[[-0.5465264211455029,0.5465264211455029]], ,[[0.3666706806887138,0.6333293193112862]])
  1. 通过查看上一步的输出,我们可以检查模型的表现以及它的预测与实际情况的对比。在接下来的章节中,我们将使用模型来预测结果。

以下是一些来自几行的示例:

    • 第 10 行:模型预测正确
  • 第 13 行:模型预测错误
  1. 在最后一步,我们停止集群并发出资源释放的信号:
spark.stop()

它是如何工作的...

我们首先定义了一个Seq数据结构来容纳一系列向量,每个向量都是一个标签和一个特征向量。然后我们将数据结构转换为 DataFrame,并通过Estimator.fit()运行它以产生适合数据的模型。我们检查了模型的参数和 DataFrame 模式,以了解产生的模型。然后我们继续组合.select().predict()来分解 DataFrame,然后循环显示预测和结果。

虽然我们不必使用流水线(Spark 中从 scikit-learn 借鉴的工作流概念,scikit-learn.org/stable/index.html)来运行回归,但我们决定向您展示 Spark ML 流水线和逻辑回归算法的强大功能。

根据我们的经验,所有生产 ML 代码都使用一种流水线形式来组合多个步骤(例如,整理数据、聚类和回归)。接下来的章节将向您展示如何在开发过程中使用这些算法而不使用流水线来减少编码。

还有更多...

由于我们刚刚看到如何在 Scala 和 Spark 中编写流水线概念的代码,让我们重新审视并以高层次定义一些概念,以便对其有一个坚实的概念理解。

管道

Spark 通过标准化 API 使机器学习流水线(MLlib)中的步骤组合变得容易,这些 API 可以组合成工作流程(即在 Spark 中称为流水线)。虽然可以在不使用这些流水线的情况下调用回归,但一个工作系统(即端到端)的现实需要我们采取多步流水线方法。

流水线的概念来自另一个流行的库scikit-learn

  • 转换器:转换器是一种可以将一个 DataFrame 转换为另一个 DataFrame 的方法。

  • 估计器:估计器在 DataFrame 上操作,以产生一个转换器。

向量

向量的基类支持密集向量和稀疏向量。根本区别在于对于稀疏数据结构的表示效率。这里选择了密集向量,因为训练数据每行都是有意义的,几乎没有稀疏性。在处理稀疏向量、矩阵等情况时,稀疏元组将同时包含索引和相应的值。

另请参阅

虽然使用 Spark 文档和 Scala 参考是可选的,也许对于本章来说还为时过早,但它们被包含在内是为了完整性:

第七章:使用 Spark 扩展的推荐引擎

在本章中,我们将涵盖:

  • 在 Spark 2.0 中设置可扩展的推荐引擎所需的数据

  • 在 Spark 2.0 中探索推荐系统的电影数据细节

  • 在 Spark 2.0 中探索推荐系统的评分数据细节

  • 在 Spark 2.0 中构建可扩展的协同过滤推荐引擎

引言

在之前的章节中,我们使用简短的配方和极其简化的代码来演示 Spark 机器库的基本构建块和概念。在本章中,我们提出了一个更为发展的应用程序,该应用程序使用 Spark 的 API 和设施来解决特定的机器学习库领域。本章的配方数量较少;然而,我们进入了更多的机器学习应用设置。

在本章中,我们探讨了推荐系统及其实现,使用了一种称为交替最小二乘法(ALS)的矩阵分解技术,该技术依赖于称为潜在因子模型的潜在因子。简而言之,当我们尝试将用户-物品评分的大矩阵因子分解为两个较低排名、较瘦的矩阵时,我们经常面临一个非线性或非凸优化问题,这是非常难以解决的。我们很擅长通过固定一个腿并部分解决另一个腿,然后来回进行(因此交替)来解决凸优化问题;我们可以使用已知的优化技术并行地更好地解决这种因子分解(因此发现一组潜在因子)。

我们使用一个流行的数据集(电影镜头数据集)来实现推荐引擎,但与其他章节不同的是,我们使用两个配方来探索数据,并展示如何将图形元素(例如 JFreeChart 流行库)引入到您的 Spark 机器学习工具包中。

以下图显示了本章中概念和配方的流程,以演示 ALS 推荐应用程序:

推荐引擎已经存在很长时间,并且在 20 世纪 90 年代的早期电子商务系统中使用,使用的技术范围从硬编码产品关联到基于内容的推荐,由个人资料驱动。现代系统使用协同过滤(CF)来解决早期系统的缺点,并解决现代商业系统(例如亚马逊、Netflix、eBay、新闻等)中必须竞争的规模和延迟(例如,最大 100 毫秒及以下)。

现代系统使用基于历史互动和记录的协同过滤(CF)(页面浏览、购买、评分等)。这些系统主要解决两个主要问题,即可扩展性和稀疏性(也就是说,我们并没有所有电影或歌曲的所有评分)。大多数系统使用交替最小二乘法与加权λ正则化的变体,可以在大多数主要平台上并行化(例如 Spark)。话虽如此,为了商业目的实施的实际系统使用许多增强功能来处理偏见(也就是说,并非所有电影和用户都是平等的)和时间问题(也就是说,用户的选择会改变,物品的库存也会改变),这些问题存在于今天的生态系统中。在智能和领先的电子商务系统上工作过后,构建一个有竞争力的推荐系统并不是一种纯粹的方法,而是一种实用的方法,它使用多种技术,最少使用协同过滤、基于内容的过滤和相似性这三种技术,以亲和矩阵/热图作为上下文。

鼓励读者查阅有关推荐系统中冷启动问题的白皮书和资料。

为了设定背景,以下图表提供了可用于构建推荐系统的方法的高级分类。我们简要介绍了每种系统的优缺点,但集中讨论了在 Spark 中可用的矩阵分解(潜在因子模型)。虽然单值分解SVD)和交替最小二乘法ALS)都可用,但由于 SVD 在处理缺失数据等方面的缺点,我们集中讨论了 MovieLens 数据中的 ALS 实现。我们将在第十一章中详细探讨 SVD,大数据中的高维度问题

下一节将解释所使用的推荐引擎技术。

内容过滤

内容过滤是推荐引擎的最初技术之一。它依赖于用户档案来进行推荐。这种方法主要依赖于用户的预先存在的档案(类型、人口统计学、收入、地理位置、邮政编码)和库存的特征(产品、电影或歌曲的特征)来推断属性,然后进行过滤和采取行动。主要问题是预先存在的知识通常是不完整的并且获取成本高昂。这种技术已有十多年历史,但仍在使用中。

协同过滤

协同过滤是现代推荐系统的主要工具,它依赖于生态系统中用户的互动而不是档案来进行推荐。

这种技术依赖于过去的用户行为和产品评分,并不假设任何预先存在的知识。简而言之,用户对库存商品进行评分,假设是客户口味随时间相对稳定,这可以被利用来提供推荐。话虽如此,一个智能系统将会根据任何可用的上下文(例如,用户是从中国登录的女性)来增强和重新排序推荐。

这类技术的主要问题是冷启动,但其无领域限制、更高的准确性和易扩展性的优势使其在大数据时代成为赢家。

邻域方法

这种技术主要作为加权本地邻域实现。在其核心,它是一种相似性技术,且在对商品和用户的假设上依赖较多。虽然这种技术易于理解和实现,但该算法在可扩展性和准确性方面存在缺陷。

潜在因子模型技术

这种技术试图通过推断从评分中推断出的次要潜在因子集来解释用户对库存商品(例如,亚马逊上的产品)的评分。其优势在于不需要提前了解这些因子(类似于 PCA 技术),而是仅仅从评分中推断出来。我们使用矩阵分解技术来推导潜在因子,这种技术因其极端的可扩展性、预测的准确性和灵活性(允许偏差和用户和库存的时间特性)而备受欢迎。

  • 奇异值分解(SVD):SVD 从早期就在 Spark 中可用,但由于其在处理现实生活中数据稀疏性(例如,用户通常不会对所有东西进行评分)、过拟合和顺序(我们真的需要生成底部的 1,000 个推荐吗?)等问题,我们建议不将其作为核心技术使用。

  • 随机梯度下降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 杯数据集的一个很好的替代品。该数据集包含多个集合,从小(100K 集)到大(1M 和 20M 集)。对于那些有兴趣调整源代码以添加自己的增强(例如,更改正则化技术)的用户来说,数据集的范围使得研究从 100K 到 20M 的数据规模效果和 Spark 利用率与执行者性能曲线变得容易。

下载的 URL 是grouplens.org/datasets/movielens/

还有更多...

仔细查看我们从哪里下载数据,因为在files.grouplens.org/datasets/上还有更多数据集可供使用。

以下图表描述了数据的大小和范围。在本章中,我们使用小数据集,因此它可以轻松运行在资源有限的小型笔记本电脑上。

来源:MovieLens

另请参阅

请阅读解压数据的目录中包含的 README 文件。README 文件包含有关数据文件格式和数据描述的信息。

还有一个 MovieLens 基因标签集,可用于参考。

  • 计算标签电影 1100 万

  • 从 1100 个标签中获取相关性分数

  • 应用于 10000 部电影

对于那些有兴趣探索原始 Netflix 数据集的用户,请参阅academictorrents.com/details/9b13183dc4d60676b773c9e2cd6de5e5542cee9a URL。

在 Spark 2.0 中探索用于推荐系统的电影数据细节

在这个示例中,我们将开始通过将数据解析为 Scala case类并生成一个简单的度量来探索电影数据文件。关键在于获得对我们的数据的理解,因此在后期阶段,如果出现模糊的结果,我们将有一些见解,以便对我们结果的正确性做出知情的结论。

这是探索电影数据集的两个示例中的第一个。数据探索是统计分析和机器学习中的重要第一步。

快速了解数据的最佳方法之一是生成其数据可视化,我们将使用 JFreeChart 来实现这一点。确保您对数据感到舒适,并首先了解每个文件中的内容以及它试图传达的故事非常重要。

在我们做任何其他事情之前,我们必须始终探索、理解和可视化数据。大多数机器学习和其他系统的性能和缺失都可以追溯到对数据布局及其随时间变化的理解不足。如果我们看一下这个配方中第 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, XYSeriesCollection}
  1. 现在我们定义一个 Scalacase class来建模电影数据:
case class MovieData(movieId: Int, title: String, year: Int, genre: Seq[String])
  1. 让我们定义一个函数,在稍后将调用它来在窗口中显示 JFreeChart。此软件包中有许多图表和绘图选项可供探索:
def show(chart: JFreeChart) {
 val frame = new ChartFrame("plot", chart)
 frame.pack()
 frame.setVisible(true)
 }
  1. 在这一步中,我们定义了一个函数,用于将movie.dat文件中的单行数据解析为我们的电影case class
def parseMovie(str: String): MovieData = {
 val columns = str.split("::")
 *assert*(columns.size == 3)

 val titleYearStriped = """\(|\)""".r.replaceAllIn(columns(1), " ")
 val titleYearData = titleYearStriped.split(" ")

 *MovieData*(columns(0).toInt,
 titleYearData.take(titleYearData.size - 1).mkString(" "),
 titleYearData.last.toInt,
 columns(2).split("|"))
 }
  1. 我们准备开始构建我们的main函数,所以让我们从定义我们的movie.dat文件的位置开始:
 val movieFile = "../data/sparkml2/chapter7/movies.dat"
  1. 创建 Spark 的会话对象并设置配置:
val spark = SparkSession
 .*builder* .master("local[*]")
 .appName("MovieData 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 movies = spark.read.textFile(movieFile).map(parseMovie)  
  1. 使用 Spark SQL 将所有电影按年份分组:
movies.createOrReplaceTempView("movies")
val moviesByYear = spark.sql("select year, count(year) as count from movies group by year order by year")
  1. 现在我们显示一个按发行年份分组的直方图图表:
val histogramDataset = new XYSeriesCollection()
 val xy = new XYSeries("")
 moviesByYear.collect().foreach({
 row => xy.add(row.getAsInt, row.getAsLong)
 })

 histogramDataset.addSeries(xy)

 val chart = ChartFactory.createHistogram(
 "", "Year", "Movies Per Year", histogramDataset, PlotOrientation.VERTICAL, false, false, false)
 val chartPlot = chart.getXYPlot()

 val xAxis = chartPlot.getDomainAxis().asInstanceOf[NumberAxis]
 xAxis.setNumberFormatOverride(new DecimalFormat("####"))

 show(chart)
  1. 查看生成的图表,以对电影数据集有一个良好的了解。读者可以探索至少另外两到四种可视化数据的方法。

  1. 通过停止 Spark 会话来关闭程序:
spark.stop()  

它是如何工作的...

当程序开始执行时,我们在驱动程序中初始化了一个 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 散点图来实现这一点。通过 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 执行器内存(例如 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 查询,按用户对其总数对所有评分进行了分组。

我们在第三章“Spark 的三大数据武士——机器学习完美结合”中探索了数据集/DataFrame,但我们鼓励用户刷新并深入了解数据集/DataFrame API。对 API 及其概念(延迟实例化、分阶段、流水线和缓存)的充分理解对每个 Spark 开发人员至关重要。

最后,我们将结果集传递给 JFreeChart 散点图组件以显示我们的图表。

还有更多...

Spark DataFrame 是一个分布式的数据集合,组织成命名列。所有 DataFrame 操作也会自动并行化和分布在集群上。此外,DataFrame 像 RDD 一样是惰性评估的。

另请参阅

可以在spark.apache.org/docs/latest/sql-programming-guide.html找到有关 DataFrame 的文档。

可以在www.tutorialspoint.com/jfreechart/找到关于 JFreeChart 的很好的教程。

JFreeChart 可以从www.jfree.org/index.html URL 下载。

在 Spark 2.0 中构建可扩展的协同过滤推荐引擎

在这个示例中,我们将演示一个利用协同过滤技术的推荐系统。在核心上,协同过滤分析用户之间的关系以及库存之间的依赖关系(例如,电影、书籍、新闻文章或歌曲),以识别基于一组称为潜在因素的次要因素的用户与项目之间的关系(例如,女性/男性,快乐/悲伤,活跃/ pass)。关键在于您不需要提前知道潜在因素。

推荐将通过 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 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的函数,以及用于将movies.dat文件中的单行数据解析为电影case class的函数:
def parseMovie(str: String): Movie = {
val columns = str.split("::")
*assert*(columns.size == 3)

val titleYearStriped = """\(|\)""".r.replaceAllIn(columns(1), " ")
val titleYearData = titleYearStriped.split(" ")

*Movie*(columns(0).toInt,
     titleYearData.take(titleYearData.size - 1).mkString(" "),
     titleYearData.last.toInt,
     columns(2).split("|"))
 }

def parseFullRating(str: String): FullRating = {
val columns = str.split("::")
*assert*(columns.size == 4)
*FullRating*(columns(0).toInt, columns(1).toInt, columns(2).toFloat, columns(3).toLong)
 }
  1. 我们准备开始构建我们的main函数,所以让我们从movie.datratings.dat文件的位置开始:
val movieFile = "../data/sparkml2/chapter7/movies.dat" val ratingsFile = "../data/sparkml2/chapter7/ratings.dat"
  1. 创建一个 SparkSession 对象及其相关配置:
val spark = SparkSession
 .builder
.master("local[*]")
 .appName("MovieLens App")
 .config("spark.sql.warehouse.dir", ".")
 .config("spark.executor.memory", "2g")
 .getOrCreate()
  1. 日志消息的交错导致输出难以阅读;因此,将日志级别设置为ERROR
Logger.getLogger("org").setLevel(Level.ERROR)
  1. 创建所有评分的数据集,并将其注册为内存中的临时视图,以便可以使用 SQL 进行查询:
val ratings = spark.read.textFile(ratingsFile).map(*parseFullRating*)

 val movies = spark.read.textFile(movieFile).map(*parseMovie*).cache()
 movies.createOrReplaceTempView("movies")
  1. 对视图执行 SQL 查询:
val rs = spark.sql("select movies.title from movies")
rs.show(25)

从控制台输出:

  1. 我们将评分数据分类为训练和测试数据集。训练数据将用于训练交替最小二乘推荐机器学习算法,而测试数据将稍后用于评估预测和测试数据之间的准确性:
val splits = ratings.randomSplit(*Array*(0.8, 0.2), 0L)
val training = splits(0).cache()
val test = splits(1).cache()

val numTraining = training.count()
val numTest = test.count()
*println*(s"Training: $numTraining, test: $numTest.")
  1. 现在创建一个虚构的用户,用户 ID 为零,生成几个评分的数据集。稍后这个用户将帮助我们更好地理解 ALS 算法计算的预测:
val testWithOurUser = spark.createDataset(Seq(
  FullRating(0, 260, 0f, 0), // Star Wars: Episode IV - A New Hope
  FullRating(0, 261, 0f, 0), // Little Women
  FullRating(0, 924, 0f, 0), // 2001: A Space Odyssey
  FullRating(0, 1200, 0f, 0), // Aliens
  FullRating(0, 1307, 0f, 0) // When Harry Met Sally...
)).as[FullRating]

val trainWithOurUser = spark.createDataset(Seq(
  FullRating(0, 76, 3f, 0), // Screamers
  FullRating(0, 165, 4f, 0), // Die Hard: With a Vengeance
  FullRating(0, 145, 2f, 0), // Bad Boys
  FullRating(0, 316, 5f, 0), // Stargate
  FullRating(0, 1371, 5f, 0), // Star Trek: The Motion Picture
  FullRating(0, 3578, 4f, 0), // Gladiator
  FullRating(0, 3528, 1f, 0) // Prince of Tides
)).as[FullRating]
  1. 使用数据集联合方法将testWithOurUser附加到原始训练集。还要在原始训练集和测试集上使用unpersist方法释放资源:
val testSet = test.union(testWithOurUser)
 test.unpersist()
val trainSet = training.union(trainWithOurUser)
 training.unpersist()
  1. 创建 ALS 对象并设置参数。

使用训练数据集获取模型。

val als = new ALS()
 .setUserCol("userId")
 .setItemCol("movieId")
 .setRank(10)
 .setMaxIter(10)
 .setRegParam(0.1)
 .setNumBlocks(10)
val model = als.fit(trainSet.toDF)
  1. 让模型在测试数据集上运行:
val predictions = model.transform(testSet.toDF())
 predictions.cache()
 predictions.show(10, false)

从控制台输出:

  1. 构建一个内存表,其中包含 Spark SQL 查询的所有预测:
val allPredictions = predictions.join(movies, movies("movieId") === predictions("movieId"), "left")

  1. 从表中检索评分和预测,并在控制台中显示前 20 行:
allPredictions.select("userId", "rating", "prediction", "title")show(false)

从控制台输出:

  1. 现在获取特定用户的电影预测:
allPredictions.select("userId", "rating", "prediction", "title").where("userId=0").show(false)

从控制台输出:

  1. 通过停止 Spark 会话来关闭程序:
spark.stop()

它是如何工作的...

由于程序的复杂性,我们提供了一个概念性的解释,然后继续解释程序的细节。

以下图示了 ALS 的概念视图以及它是如何将用户/电影/评分矩阵进行因式分解的,这是一个高阶矩阵,分解为一个较低阶的高瘦矩阵和一个潜在因素的向量:f(用户)和 f(电影)。

另一种思考方式是,这些因素可以用来将电影放置在一个n维空间中,以便与给定用户的推荐匹配。将机器学习视为在一个维度变量空间中进行搜索查询总是令人满意的。要记住的是,潜在因素(学习的几何空间)并非预先定义,可以低至 10 到 100 或 1000,具体取决于正在搜索或进行因式分解的内容。因此,我们的推荐可以被视为在 n 维空间中放置概率质量。以下图提供了一个可能的两因子模型(二维)的极其简化的视图,以证明这一点:

虽然 ALS 的实现在不同系统中可能有所不同,但在其核心是一个带有加权正则化的迭代全因子分解方法(在 Spark 中)。Spark 的文档和教程提供了对实际数学和算法性质的见解。它描述了算法如下:

理解这个公式/算法的最佳方法是将其视为一个迭代装置,它通过在输入之间交替(即固定一个输入,然后近似/优化另一个输入,然后来回进行),试图发现潜在因素,同时试图最小化与加权 lambda 的正则化惩罚相关的最小二乘误差(MSE)。更详细的解释将在下一节中提供。

程序流程如下:

  • 示例从 MovieLens 数据集中加载了评分和电影数据。加载的数据随后被转换为 Scala 案例类以进行进一步处理。下一步是将评分数据分成训练集和测试集。训练集数据用于训练机器学习算法。训练是机器学习中用于构建模型以便提供所需结果的过程。测试数据将用于验证最终结果。

  • 虚构的用户,或者用户 ID 为零,配置了一个未包含在原始数据集中的单个用户,以帮助通过在现有数据集上创建一个包含随机信息的数据集,并最终将其附加到训练集中,从而为结果提供见解。然后,通过将训练集数据传递给 ALS 算法,包括用户 ID、电影 ID 和评分,从 Spark 中产生了一个矩阵因子分解模型。对用户 ID 为零和测试数据集进行了预测生成。

  • 最终结果是通过将评分信息与电影数据相结合,以便在原始评分旁边理解和显示结果。最后一步是计算生成评分的均方根误差,其中包含在测试数据集中的现有评分。RMSE 将告诉我们训练模型的准确性。

还有更多...

尽管在本质上 ALS 是一个带有额外正则化惩罚的简单线性代数运算,但人们经常在处理 ALS 时遇到困难。ALS 之所以强大,是因为它能够并行化处理规模(例如 Spotify)。

用通俗的语言来说,ALS 涉及以下内容:

  • 使用 ALS,基本上要对评级矩阵 X(1 亿多用户根本不是问题)和用户产品评级进行因式分解为 A 和 B 两个矩阵,其秩较低(参见任何入门线性代数书)。问题在于,通常它变成了一个非常难解的非线性优化问题。为了解决 ALS,引入了一个简单的解决方案(A代表Alternating),其中你固定其中一个矩阵并部分解决另一个(另一个矩阵)使用最小二乘法进行优化(LS代表Least Square)。一旦这一步完成,然后交替进行,但这次你固定第二个矩阵并解决第一个。

  • 为了控制过拟合,我们在原方程中引入了一个正则化项。这一步通常是加权正则化,并由参数 lambda 控制惩罚或平滑的程度。

  • 简而言之,这一方法的矩阵因式分解非常适合并行操作,这是 Spark 在其核心的特长。

为了深入理解 ALS 算法,我们引用了两篇被认为是这一领域经典的原始论文:

从 ACM 数字图书馆使用dl.acm.org/citation.cfm?id=1608614链接。

从 IEEE 数字图书馆ieeexplore.ieee.org/xpl/login.jsp?tp=&arnumber=5197422&url=http%3A%2F%2Fieeexplore.ieee.org%2Fxpls%2Fabs_all.jsp%3Farnumber%3D5197422

以下图显示了从数学角度更深入地理解 ALS,这是之前引用的原始论文:

使用 RankingMetrics 类来评估模型性能。参数与用于评估回归模型(二元和多项式)的类相似:

  • Recall

  • Precision

  • fMeasure

MLlib 提供的 RankingMetrics 类可用于评估模型并量化模型的有效性。

RankingMetrics API 文档可在spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.mllib.evaluation.RankingMetrics找到。

另见

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(),有四个超参数来控制算法并解决隐式数据问题。如果您有兴趣测试这个方法(它与显式方法非常相似),您可以使用 100 万首歌曲数据集进行简单的训练和预测。您可以从labrosa.ee.columbia.edu/millionsong/ URL 下载用于实验的数据集。

协同过滤的优缺点如下:

优点 缺点

| 可扩展 | 冷启动问题

  • 库存中添加了新项目

  • 生态系统中添加了新用户

|

发现难以找到且常常难以捉摸的数据属性,而无需配置文件 需要相当数量的数据
更准确
便携

第八章:使用 Apache Spark 2.0 进行无监督聚类

在本章中,我们将涵盖:

  • 在 Spark 2.0 中构建 KMeans 分类系统

  • 在 Spark 2.0 中的新成员 Bisecting KMeans

  • 使用高斯混合和期望最大化(EM)在 Spark 2.0 中对数据进行分类

  • 使用 Spark 2.0 中的 Power Iteration Clustering(PIC)对图的顶点进行分类

  • 使用 Latent Dirichlet Allocation(LDA)对文档和文本进行主题分类

  • 流式 KMeans 用于近实时分类数据

介绍

无监督机器学习是一种学习技术,我们试图从一组未标记的观察中直接或间接(通过潜在因素)推断出推理。简而言之,我们试图在一组数据中找到隐藏的知识或结构,而不是最初标记训练数据。

虽然大多数机器学习库实现在应用于大型数据集时会出现问题(迭代,多次传递,大量中间写入),但 Apache Spark 机器库通过提供专为并行处理和极大数据集设计的机器库算法而成功,使用内存进行中间写入。

在最抽象的层面上,我们可以将无监督学习视为:

  • 聚类系统:将输入分类为硬分类(仅属于单个簇)或软分类(概率成员和重叠)。

  • 降维系统:使用原始数据的简化表示找到隐藏因素。

以下图显示了机器学习技术的景观。在之前的章节中,我们专注于监督机器学习技术。在本章中,我们专注于无监督机器学习技术,从聚类到使用 Spark 的 ML/MLIB 库 API 的潜在因素模型:

这些簇通常使用簇内相似度测量来建模,例如欧几里得或概率技术。Spark 提供了一套完整且高性能的算法,适合于规模化的并行实现。它们不仅提供 API,还提供完整的源代码,非常有助于理解瓶颈并解决它们(分叉到 GPU)以满足您的需求。

机器学习的应用是广泛的,可以想象的无限。一些最广为人知的例子和用例包括:

  • 欺诈检测(金融,执法)

  • 网络安全(入侵检测,流量分析)

  • 模式识别(营销,情报界,银行)

  • 推荐系统(零售,娱乐)

  • 亲和营销(电子商务,推荐系统,深度个性化)

  • 医学信息学(疾病检测,患者护理,资产管理)

  • 图像处理(对象/子对象检测,放射学)

关于 Spark 中 ML 与 MLIB 的使用和未来方向的警告:

虽然 MLIB 目前仍然可行,但在未来的发展中,人们逐渐转向 Spark 的 ML 库而不是 Spark 中的 MLIB。org.apache.spark.ml.clustering是一个高级机器学习包,API 更专注于 DataFrame。org.apache.spark.mllib.clustering是一个较低级别的机器学习包,API 直接在 RDD 上。虽然两个包都将受益于 Spark 的高性能和可伸缩性,但主要区别在于 DataFrame。org.apache.spark.ml将是未来的首选方法。

例如,我们鼓励开发人员查看为什么 KMeans 分类系统存在于 ML 和 MLLIB 中:org.apache.spark.ml.clusteringorg.apache.spark.mllib.clustering

在 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. 导入 Spark 上下文所需的必要包,以便访问集群和Log4j.Logger以减少 Spark 产生的输出量:
import org.apache.log4j.{Level, Logger}
import org.apache.spark.ml.clustering.KMeans
import 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", ".")
 .getOrCreate()
  1. 我们从libsvm格式的文件中创建一个训练数据集,并在控制台上显示文件:
val trainingData = spark.read.format("libsvm").load("../data/sparkml2/chapter8/my_kmeans_data.txt")

trainingData.show()

从控制台,您将看到:

以下公式通过等高线图可视化数据,描述了每个特征向量(每行)与三个唯一特征的 3D 和平面等高线图:

  1. 然后我们创建一个 KMeans 对象,并设置一些关键参数到 KMeans 模型和设置参数。

在这种情况下,我们将K值设置为3,并将feature列设置为“features”列,该列在上一步中定义。这一步是主观的,最佳值会根据特定数据集而变化。我们建议您尝试值从 2 到 50,并检查最终值的聚类中心。

我们还将最大迭代次数设置为10。大多数值都有默认设置,如下面的代码中所示的注释。

// Trains a k-means modelval kmeans = new KMeans()
.setK(3) // default value is 2.setFeaturesCol("features")
.setMaxIter(10) // default Max Iteration is 20.setPredictionCol("prediction")
.setSeed(1L)
  1. 然后训练数据集。fit()函数将运行算法并执行计算。它是基于前面步骤中创建的数据集。这些步骤在 Spark 的 ML 中是常见的,通常不会因算法而异:
val model = kmeans.fit(trainingData)

我们还在控制台上显示模型的预测:

model.summary.predictions.show()

从控制台:

  1. 然后我们使用包括computeCost(x)函数来计算成本。

  2. KMeans 成本是通过WSSSE(Within Set Sum of Squared Errors)计算的。该值将在程序的控制台中打印出来:

println("KMeans Cost:" +model.computeCost(trainingData))

控制台输出将显示以下信息:

KMeans Cost:4.137499999999979
  1. 然后根据模型的计算打印出簇的中心:
println("KMeans Cluster Centers: ")
 model.clusterCenters.foreach(println)
  1. 控制台输出将显示以下信息:
The centers for the 3 cluster (i.e. K= 3) 
KMeans Cluster Centers:  
[1.025,1.075,1.15] 
[9.075,9.05,9.025] 
[3.45,3.475,3.55] 

根据 KMeans 聚类的设置,我们将K值设置为3;该模型将根据我们拟合的训练数据集计算出三个中心。

  1. 然后通过停止 Spark 上下文来关闭程序:
spark.stop()

它是如何工作的...

我们读取了一个带有一组坐标的 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 Parallel)的版本,而不是一些文献或视频中提到的 KMeans++或标准 KMeans。

下图简要描述了 KMeans:

来源:Spark 文档

KMeans(Lloyd 算法)

基本 KMeans 实现(Lloyd 算法)的步骤是:

  1. 从观察中随机选择 K 个数据中心作为初始质心。

  2. 保持迭代直到满足收敛标准:

  • 测量从点到每个质心的距离

  • 将每个数据点包括在最接近的质心的群集中

  • 根据距离公式(代表不相似性的代理)计算新的群集质心

  • 使用新的中心点更新算法

三代人的情况如下图所示:

KMeans++(Arthur 的算法)

对标准 KMeans 的下一个改进是由 David Arthur 和 Sergei Vassilvitskii 于 2007 年提出的 KMeans++。 Arthur 的算法通过在种植过程(初始步骤)中更加选择性来改进最初的 Lloyd's 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 ||(Parallel KMeans)的亮点是粗粒度采样,它在*log(n)轮次中重复,并且最终我们剩下k * log(n)*个距离最优解有 C(常数)距离的点!这种实现对可能会扭曲 KMeans 和 KMeans++中的聚类结果的异常数据点也不太敏感。

为了更深入地理解该算法,读者可以访问 Bahman Bahmani 在theory.stanford.edu/~sergei/papers/vldb12-kmpar.pdf上的论文。

还有更多...

Spark 还有一个 KMeans 实现的流式版本,允许您即时对特征进行分类。 KMeans 的流式版本在《第十三章》Spark Streaming 和 Machine Learning Library中有更详细的介绍。

还有一个类可以帮助您为 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 是 Spark 中使用BisectingKMeans()API 实现的 K-Mean 算法的分层版本。虽然这个算法在概念上类似于 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 也是如此)

  • 镁:镁

  • 铝:铝

  • 硅:硅

  • 钾:钾

  • 钙:钙

  • Ba:钡

  • 铁:铁

玻璃类型:将使用BisectingKMeans()找到我们的类属性或簇:

  • building_windows_float_processed

  • building_windows_non-_float_processed

  • vehicle_windows_float_processed

  • vehicle_windows_non-_float_processed(此数据库中没有)

  • Containers

  • Tableware

  • Headlamps

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序所在的包位置:

package spark.ml.cookbook.chapter8
  1. 导入必要的包:
import org.apache.spark.ml.clustering.BisectingKMeans
import org.apache.spark.sql.SparkSession
import org.apache.log4j.{Level, Logger}
  1. 将输出级别设置为ERROR以减少 Spark 的日志输出:
Logger.getLogger("org").setLevel(Level.ERROR)
  1. 创建 Spark 的 Session 对象:
val spark = SparkSession
 .builder.master("local[*]")
 .appName("MyBisectingKMeans")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()
  1. 我们从 libsvm 格式的文件创建数据集,并在控制台上显示数据集:
val dataset = spark.read.format("libsvm").load("../data/sparkml2/chapter8/glass.scale")
 dataset.show(false)

从控制台,您将看到:

  1. 然后,我们将数据集随机分成 80%和 20%的两部分:
val splitData = dataset.randomSplit(Array(80.0, 20.0))
 val training = splitData(0)
 val testing = splitData(1)

 println(training.count())
 println(testing.count())

从控制台输出(总数为 214):

180
34
  1. 然后,我们创建一个BisectingKMeans对象,并为模型设置一些关键参数。

在这种情况下,我们将K值设置为6,并将Feature列设置为"features"列,这在前面的步骤中已定义。这一步是主观的,最佳值将根据特定数据集而变化。我们建议您尝试值从 2 到 50,并检查最终值的聚类中心。

  1. 我们还将最大迭代次数设置为65。大多数值都有默认设置,如下面的代码所示:
// Trains a k-means modelval bkmeans = new BisectingKMeans()
   .setK(6)
   .setMaxIter(65)
   .setSeed(1)
  1. 然后我们训练数据集。fit()函数将运行算法并进行计算。它基于前面步骤中创建的数据集。我们还打印出模型参数:
val bisectingModel = bkmeans.fit(training)
 println("Parameters:")
 println(bisectingModel.explainParams())

从控制台输出:

Parameters:
featuresCol: features column name (default: features)
k: The desired number of leaf clusters. Must be > 1\. (default: 4, current: 6)
maxIter: maximum number of iterations (>= 0) (default: 20, current: 65)
minDivisibleClusterSize: The minimum number of points (if >= 1.0) or the minimum proportion of points (if < 1.0) of a divisible cluster. (default: 1.0)
predictionCol: prediction column name (default: prediction)
seed: random seed (default: 566573821, current: 1)
  1. 然后我们使用包括 computeCost(x)函数来计算成本:
val cost = bisectingModel.computeCost(training)
 println("Sum of Squared Errors = " + cost)

控制台输出将显示以下信息:

Sum of Squared Errors = 70.38842983516193
  1. 然后,我们根据模型的计算打印出聚类中心:
println("Cluster Centers:")
val centers = bisectingModel.clusterCenters
centers.foreach(println)

控制台输出将显示以下信息:

The centers for the 6 cluster (i.e. K= 6) 
KMeans Cluster Centers: 

  1. 然后我们使用训练好的模型对测试数据集进行预测:
val predictions = bisectingModel.transform(testing)
 predictions.show(false)

从控制台输出:

  1. 然后通过停止 Spark 上下文来关闭程序:
spark.stop()

它是如何工作的...

在本节中,我们探索了 Spark 2.0 中新的 Bisecting KMeans 模型。在本节中,我们使用了玻璃数据集,并尝试使用BisectingKMeans()来分配玻璃类型,但将 k 更改为 6,以便有足够的聚类。像往常一样,我们使用 Spark 的 libsvm 加载机制将数据加载到数据集中。我们将数据集随机分为 80%和 20%,其中 80%用于训练模型,20%用于测试模型。

我们创建了BiSectingKmeans()对象,并使用fit(x)函数创建模型。然后我们使用transform(x)函数对测试数据集进行探索模型预测,并在控制台输出结果。我们还输出了计算聚类的成本(误差平方和),然后显示了聚类中心。最后,我们打印出了特征及其分配的聚类编号,并停止操作。

层次聚类的方法包括:

  • 分裂式:自上而下的方法(Apache Spark 实现)

  • 聚合式:自下而上的方法

还有更多...

有关 Bisecting KMeans 的更多信息,请访问:

我们使用聚类来探索数据,并了解聚类的结果。分裂式 KMeans 是分层分析与 KMeans 聚类的有趣案例。

理解分裂式 KMeans 的最佳方式是将其视为递归的分层 KMeans。分裂式 KMeans 算法使用类似 KMeans 的相似性测量技术来划分数据,但使用分层方案来提高准确性。它在文本挖掘中特别普遍,其中分层方法将最小化语料库中文档之间的聚类内依赖性。

Bisecting KMeans 算法首先将所有观察结果放入单个聚类中,然后使用 KMeans 方法将聚类分成 n 个分区(K=n)。然后,它继续选择最相似的聚类(最高内部聚类分数)作为父类(根聚类),同时递归地分割其他聚类,直到以分层方式得出目标聚类数。

Bisecting KMeans 是文本分析中用于智能文本/主题分类的强大工具,用于减少特征向量的维度。通过使用这种聚类技术,我们最终将相似的单词/文本/文档/证据分组到相似的组中。最终,如果您开始探索文本分析、主题传播和评分(例如,哪篇文章会成为病毒?),您一定会在旅程的早期阶段遇到这种技术。

一份白皮书描述了使用 Bisecting KMeans 进行文本聚类的方法,可以在以下链接找到:www.ijarcsse.com/docs/papers/Volume_5/2_February2015/V5I2-0229.pdf

另请参阅

有两种实现分层聚类的方法--Spark 使用递归自顶向下的方法,在该方法中选择一个簇,然后在算法向下移动时执行拆分:

使用 Gaussian Mixture 和期望最大化(EM)在 Spark 中对数据进行分类

在这个示例中,我们将探讨 Spark 对期望最大化EMGaussianMixture()的实现,它根据一组特征计算最大似然。它假设一个高斯混合模型,其中每个点可以从 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 膝痛质心位置数据表示了 1,000 个主题的假设膝痛位置的质心位置。数据包括质心的 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. 现在我们创建一个 GaussianMixture 模型,并设置模型的参数。我们将 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. 我们在训练后打印出 GaussianMixture 模型的关键值:
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. 我们还根据 GaussianMixture 模型的预测打印出前 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 模型的高斯混合模型的专门版本,在其中强制执行离散(硬)成员资格。

但有些情况会有重叠,这在医学或信号处理中经常发生,如下图所示:

在这种情况下,我们需要一个概率密度函数,可以表达在每个子分布中的成员资格。具有期望最大化EM)的高斯混合模型是 Spark 中可处理此用例的算法GaussianMixture()

这是 Spark 用于实现具有期望最大化的高斯混合的 API(对数似然的最大化)。

新的 GaussianMixture()

这构造了一个默认实例。控制模型行为的默认参数是:

具有期望最大化的高斯混合模型是一种软聚类形式,可以使用对数最大似然函数推断成员资格。在这种情况下,使用均值和协方差的概率密度函数来定义对 K 个集群的成员资格或成员资格的可能性。它是灵活的,因为成员资格没有被量化,这允许基于概率的重叠成员资格(索引到多个子分布)。

以下图是 EM 算法的快照:

以下是 EM 算法的步骤:

  1. 假设* N *个高斯分布。

  2. 迭代直到收敛:

  3. 对于每个点 Z,条件概率被绘制为从分布 Xi 中被绘制的P(Z | Xi)

  4. 调整参数的均值和方差,使它们适合分配给子分布的点

更多数学解释,请参见以下链接:www.ee.iisc.ernet.in/new/people/faculty/prasantg/downloads/GMM_Tutorial_Reynolds.pdf

还有更多...

以下图提供了一个快速参考点,以突出硬聚类与软聚类之间的一些差异:

另请参阅

在 Spark 2.0 中使用幂迭代聚类(PIC)对图的顶点进行分类

这是一种根据边缘定义的相似性对图的顶点进行分类的方法。它使用 GraphX 库,该库与 Spark 一起提供以实现该算法。幂迭代聚类类似于其他特征向量/特征值分解算法,但没有矩阵分解的开销。当您有一个大型稀疏矩阵时(例如,图表示为稀疏矩阵),它是合适的。

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. 将日志记录器级别设置为仅错误以减少输出:
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 转换操作符将模型分配数据打印到集合中。

在核心PICPower Iteration Clustering)是一种特征值类算法,它通过产生满足Av = λv的特征值加上特征向量来避免矩阵分解。因为 PIC 避免了矩阵 A 的分解,所以当输入矩阵 A(在 Spark 的 PIC 的情况下描述为图)是一个大稀疏矩阵时,它是合适的。

PIC 在图像处理中的示例(已增强以供论文使用)如下图所示:

PIC 算法的 Spark 实现是对以前常见实现(NCut)的改进,通过计算伪特征向量来定义相似性,这些相似性被定义为给定 N 个顶点的边(如亲和矩阵)。

如下图所示的输入是描述图的 RDD 的三元组。输出是每个节点的聚类分配的模型。假定算法相似性(边)是正的和对称的(未显示):

还有更多...

有关该主题(幂迭代)的更详细的数学处理,请参阅卡内基梅隆大学的以下白皮书: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.SparkSession
import 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 分发一起提供,并且可以在 Spark 的主目录下的 data 目录中找到(请参见以下)。假设输入是输入到 LDA 方法的一组特征:
val input = "../data/sparkml2/chapter8/my_lda_data.txt"

  1. 在这一步中,我们读取文件并从输入文件创建必要的数据集,并在控制台中显示前五行:
val dataset = spark.read.format("libsvm").load(input)
 dataset.show(5)

从控制台输出:

  1. 我们创建 LDA 对象并设置对象的参数:
val lda = new LDA()
 .setK(5)
 .setMaxIter(10)
 .setFeaturesCol("features")
 .setOptimizer("online")
 .setOptimizeDocConcentration(true)
  1. 然后我们使用包的高级 API 运行模型:
val ldaModel = lda.fit(dataset)

 val ll = ldaModel.logLikelihood(dataset)
 val lp = ldaModel.logPerplexity(dataset)

 println(s"\t Training data log likelihood: $ll")
 println(s"\t Training data log Perplexity: $lp")

从控制台输出:

Training data log likelihood: -762.2149142231476
 Training data log Perplexity: 2.8869048032045974
  1. 我们从 LDA 模型中获取每组特征的主题分布,并显示主题。

  2. 我们将maxTermsPerTopic值设置为3

val topics = ldaModel.describeTopics(3)
 topics.show(false) // false is Boolean value for truncation for the dataset
  1. 在控制台上,输出将显示以下信息:

  1. 我们还从 LDA 模型转换训练数据集,并显示结果:
val transformed = ldaModel.transform(dataset)
 transformed.show(false)

输出将显示以下内容:

如果前面的方法被更改为:

transformed.show(true)
  1. 结果将被显示为截断:

  1. 我们关闭 Spark 上下文以结束程序:
spark.stop()

它是如何工作的...

LDA 假设文档是具有狄利克雷先验分布的不同主题的混合物。假定文档中的单词倾向于特定主题,这使得 LDA 能够对最匹配主题的整个文档进行分类(组成和分配分布)。

主题模型是一种生成潜在模型,用于发现文档集合中出现的抽象主题(主题)(通常对于人类来说太大)。这些模型是总结,搜索和浏览一大批未标记文档及其内容的先导条件。一般来说,我们试图找到一组特征(单词,子图像等),这些特征一起出现。

以下图表描述了整体 LDA 方案:

请务必参考此处引用的白皮书以获取完整信息 ai.stanford.edu/~ang/papers/nips01-lda.pdf

LDA 算法的步骤如下:

  1. 初始化以下参数(控制浓度和平滑):

  2. Alpha 参数(高 alpha 使文档更相似,并包含相似的主题)

  3. Beta 参数(高 beta 意味着每个主题很可能包含大部分单词的混合)

  4. 随机初始化主题分配。

  5. 迭代:

  6. 对于每个文档。

  7. 对于文档中的每个单词。

  8. 为每个单词重新取样主题。

  9. 相对于所有其他单词及其当前分配(对于当前迭代)。

  10. 获得结果。

  11. 模型评估

在统计学中,Dirichlet 分布 Dir(alpha)是由一组正实数α参数化的连续多元概率分布家族。有关 LDA 的更深入处理,请参阅原始论文

机器学习杂志:www.jmlr.org/papers/volume3/blei03a/blei03a.pdf

LDA 不会为主题分配任何语义,也不在乎主题的名称。它只是一个生成模型,使用细粒度项目的分布(例如,关于猫、狗、鱼、汽车的单词)来分配得分最高的整体主题。它不知道、不关心、也不理解称为狗或猫的主题。

在输入 LDA 算法之前,我们经常需要通过 TF-IDF 对文档进行标记化和向量化。

还有更多...

以下图描述了 LDA 的要点:

文档分析有两种方法。我们可以简单地使用矩阵分解将大型数据集的矩阵分解为较小的矩阵(主题分配)乘以一个向量(主题本身):

另请参阅

另请参阅,通过 Spark 的 Scala API,以下文档链接:

  • DistributedLDAModel

  • EMLDAOptimizer

  • LDAOptimizer

  • LocalLDAModel

  • OnlineLDAOptimizer

流式 KMeans 用于实时分类数据

Spark 流式处理是一个强大的工具,它让您可以在同一范式中结合近实时和批处理。流式 KMeans 接口位于 ML 聚类和 Spark 流式处理的交集处,并充分利用了 Spark 流式处理本身提供的核心功能(例如,容错性、精确一次性传递语义等)。

如何做...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 导入流式 KMeans 所需的包:

spark.ml.cookbook.chapter8包。

  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. 我们还可以检查 SparkUI 以获取更多信息:http://localhost:4040/

作业面板将显示流式作业,如下图所示:

如下图所示,流式面板将显示前述流式 KMeans 矩阵作为显示的矩阵,在本例中每 10 秒运行一次批处理作业:

您可以通过单击任何批次来获取有关流式批处理的更多详细信息,如下图所示:

工作原理...

在某些情况下,我们无法使用批处理方法来加载和捕获事件,然后对其做出反应。我们可以使用创造性的方法在内存或着陆数据库中捕获事件,然后迅速将其调度到另一个系统进行处理,但大多数这些系统无法充当流式系统,并且通常非常昂贵。

Spark 提供了几乎实时(也称为主观实时)的功能,可以通过连接器(例如 Kafka 连接器)接收传入的来源,如 Twitter feeds、信号等,然后将其处理并呈现为 RDD 接口。

这些是在 Spark 中构建和构造流式 KMeans 所需的元素:

  1. 使用流式上下文,而不是迄今为止使用的常规 Spark 上下文:
val ssc = new StreamingContext(conf, Seconds(batchDuration.toLong))
  1. 选择连接器以连接到数据源并接收事件:
  • Twitter

  • Kafka

  • 第三方

  • ZeroMQ

  • TCP

  • ........

  1. 创建您的流式 KMeans 模型;根据需要设置参数:
model = new StreamingKMeans()
  1. 像往常一样进行训练和预测:
  • 请记住,K 不能在运行时更改
  1. 启动上下文并等待终止信号以退出:
  • ssc.start()

  • ssc.awaitTermination()

还有更多...

流式 KMeans 是 KMeans 实现的特殊情况,其中数据可以几乎实时到达并根据需要分类到一个簇(硬分类)。应用程序广泛,可以从几乎实时的异常检测(欺诈、犯罪、情报、监视和监控)到金融中细粒度微部门旋转可视化与 Voronoi 图。第十三章,Spark Streaming 和机器学习库提供了更详细的流式覆盖。

有关 Voronoi 图的参考,请参阅以下网址:en.wikipedia.org/wiki/Voronoi_diagram

目前,在 Spark 机器库中除了流式 KMeans 之外,还有其他算法,如下图所示:

另请参阅

第九章:优化-使用梯度下降下山

在本章中,我们将涵盖:

  • 通过数学优化二次成本函数并找到最小值来获得洞察

  • 从头开始编码二次成本函数优化,使用梯度下降(GD)

  • 编码梯度下降优化以从头解决线性回归

  • 在 Spark 2.0 中,正规方程作为解决线性回归的替代方法

介绍

了解优化的工作原理对于成功的机器学习职业至关重要。我们选择了梯度下降GD)方法进行端到端的深入挖掘,以演示优化技术的内部工作原理。我们将使用三个配方来开发这个概念,从头开始到完全开发的代码,以解决实际问题和真实世界数据。第四个配方探讨了使用 Spark 和正规方程(大数据问题的有限扩展)来解决回归问题的 GD 的替代方法。

让我们开始吧。机器到底是如何学习的?它真的能从错误中学习吗?当机器使用优化找到解决方案时,这意味着什么?

在高层次上,机器学习基于以下五种技术之一:

  • 基于错误的学习:在这种技术中,我们搜索领域空间,寻找最小化训练数据上总误差(预测与实际)的参数值组合(权重)。

  • 信息论学习:这种方法使用经典香农信息论中的熵和信息增益等概念。基于树的 ML 系统,在 ID3 算法中经典地根植于这一类别。集成树模型将是这一类别的巅峰成就。我们将在第十章中讨论树模型,使用决策树和集成模型构建机器学习系统

  • 概率空间学习:这个机器学习分支基于贝叶斯定理(en.wikipedia.org/wiki/Bayes'_theorem))。机器学习中最著名的方法是朴素贝叶斯(多种变体)。朴素贝叶斯以引入贝叶斯网络而告终,这允许对模型进行更多控制。

  • 相似度测量学习:这种方法通过尝试定义相似度度量,然后根据该度量来拟合观察的分组。最著名的方法是 KNN(最近邻),这是任何 ML 工具包中的标准。 Spark ML 实现了带有并行性的 K-means++,称为 K-Means||(K 均值并行)。

  • 遗传算法(GA)和进化学习:这可以被视为达尔文的理论(物种的起源)应用于优化和机器学习。 GA 背后的想法是使用递归生成算法创建一组初始候选者,然后使用反馈(适应性景观)来消除远距离的候选者,折叠相似的候选者,同时随机引入突变(数值或符号抖动)到不太可能的候选者,然后重复直到找到解决方案。

一些数据科学家和 ML 工程师更喜欢将优化视为最大化对数似然,而不是最小化成本函数-它们实际上是同一枚硬币的两面!在本章中,我们将专注于基于错误的学习,特别是梯度下降

为了提供扎实的理解,我们将深入研究梯度下降(GD),通过三个 GD 配方来了解它们如何应用于优化。然后,我们将提供 Spark 的正规方程配方作为数值优化方法的替代方法,例如梯度下降(GD)或有限内存的 Broyden-Fletcher-Goldfarb-ShannoLBFGS)算法。

Apache Spark 为所有类别提供了出色的覆盖范围。以下图表描述了一个分类法,将指导您在数值优化领域的旅程,这对于在机器学习中取得卓越成就至关重要。

机器如何使用基于错误的系统学习?

机器学习的学习方式与我们大致相同-它们从错误中学习。首先,它们首先进行初始猜测(参数的随机权重)。其次,它们使用自己的模型(例如 GLM、RRN、等温回归)进行预测(例如一个数字)。第三,它们查看答案应该是什么(训练集)。第四,它们使用各种技术(如最小二乘法、相似性等)来衡量实际与预测答案之间的差异。

一旦所有这些机制都就位,它们将在整个训练数据集上重复这个过程,同时试图提出一种参数组合,当考虑整个训练数据集时具有最小误差。有趣的是,机器学习的每个分支都使用数学或领域已知事实,以避免蛮力组合方法,这种方法在现实世界的环境中不会终止。

基于错误的机器学习优化是数学规划(MP)的一个分支,它是通过算法实现的,但精度有限(精度变化为 10^(-2)到 10^(-6))。这一类别中的大多数方法,如果不是全部,都利用简单的微积分事实,如一阶导数(斜率)(例如 GD 技术)和二阶导数(曲率)(例如 BFGS 技术),以最小化成本函数。在 BFGS 的情况下,隐形的手是更新器函数(L1 更新器)、秩(二阶秩更新器),使用无 Hessian 矩阵的 Hessian 自由技术来近似最终答案/解决方案(en.wikipedia.org/wiki/Hessian_matrix)。

以下图表描述了 Spark 中涉及优化的一些设施:

Spark 中有用于执行 SGD 和 LBFGS 优化的函数。要使用它们,您应该能够编写并提供自己的成本函数。这些函数,如runMiniBatchSGD(),不仅标记为私有,而且需要对两种算法的实现有很好的理解。

由于这是一本食谱,我们无法深入研究优化理论,因此我们建议从我们的图书馆中参考以下书籍:

通过数学来优化二次成本函数并找到最小值

在本教程中,我们将在介绍梯度下降(一阶导数)和 L-BFGS(一种无 Hessian 的拟牛顿方法)之前,探索数学优化背后的基本概念。

我们将研究一个样本二次成本/误差函数,并展示如何仅通过数学找到最小值或最大值。

我们将使用顶点公式和导数方法来找到最小值,但我们将在本章的后续教程中介绍数值优化技术,如梯度下降及其在回归中的应用。

如何操作...

  1. 假设我们有一个二次成本函数,我们找到它的最小值:

  1. 在统计机器学习算法中,成本函数充当我们在搜索空间中移动时的难度级别、能量消耗或总误差的代理。

  2. 我们要做的第一件事是绘制函数并进行直观检查。

  1. 通过直观检查,我们看到 是一个凹函数,其最小值在 (2,1) 处。

  2. 我们的下一步将是通过优化函数来找到最小值。在机器学习中,呈现成本或误差函数的一些示例可能是平方误差、欧几里得距离、MSSE,或者任何其他能够捕捉我们离最佳数值答案有多远的相似度度量。

  3. 下一步是寻找最小化误差(例如成本)的最佳参数值。例如,通过优化线性回归成本函数(平方误差的总和),我们得到其参数的最佳值。

  • 导数方法:将一阶导数设为 0 并解出

  • 顶点方法:使用封闭代数形式

  1. 首先,我们通过计算一阶导数,将其设为 0,并解出 xy 来使用导数方法求解最小值。

给定 f(x) = 2x² - 8x +9 作为我们的成本/误差函数,导数可以计算如下:

幂规则:![]

我们将导数设为 0 并解出![]

我们现在使用顶点公式方法验证最小值。要使用代数方法计算最小值,请参见以下步骤。

  1. 给定函数 ,顶点可以在以下位置找到:

  1. 让我们使用顶点代数公式来计算最小值:

2(2)2 + (-8) (2) +9

  1. 作为最后一步,我们检查步骤 4 和 5 的结果,以确保我们使用封闭代数形式得出的最小值 (2, 1) 与导数方法得出的 (2, 1) 一致。

  2. 在最后一步,我们在左侧面板中展示 f(x) 的图形,右侧面板中展示其导数,这样您可以直观地检查答案。

  1. 正如您所看到的,随意检查显示最小值顶点在左侧为 (2,1) { x=2, f(x)=1 },而右侧图表显示函数关于 X(仅参数)的导数在 X=2 处取得最小值。如前面的步骤所示,我们将函数的导数设为零并解出 X,结果为数字 2。您还可以直观地检查两个面板和方程,以确保 X=2 在两种情况下都是正确的并且有意义。

工作原理...

我们有两种技术可以用来找到二次函数的最小值,而不使用数值方法。在现实生活中的统计机器学习优化中,我们使用导数来找到凸函数的最小值。如果函数是凸的(或者优化是有界的),那么只有一个局部最小值,所以工作比在深度学习中出现的非线性/非凸问题要简单得多。

在前面的配方中使用导数方法:

  • 首先,我们通过应用导数规则(例如指数)找到了导数。

  • 其次,我们利用了这样一个事实,对于给定的简单二次函数(凸优化),当第一导数的斜率为零时,最小值出现。

  • 第三,我们简单地通过遵循和应用机械微积分规则找到了导数。

  • 第四,我们将函数的导数设置为零!并解出 x

  • 第五,我们使用 x 值并将其代入原方程以找到 y。通过步骤 1 到 5,我们最终得到了点(2,1)处的最小值。

还有更多...

大多数统计机器学习算法在定义和搜索域空间时使用成本或误差函数来得到最佳的数值近似解(例如,回归的参数)。函数达到最小值(最小化成本/误差)或最大值(最大化对数似然)的点是最佳解(最佳近似)存在的地方,误差最小。

可以在以下网址找到微分规则的快速复习:en.wikipedia.org/wiki/Differentiation_rules www.math.ucdavis.edu/~kouba/Math17BHWDIRECTORY/Derivatives.pdf

可以在以下网址找到有关最小化二次函数的更多数学写作:www.cis.upenn.edu/~cis515/cis515-11-sl12.pdf

可以在 MIT 找到有关二次函数优化和形式的科学写作:ocw.mit.edu/courses/sloan-school-of-management/15-084j-nonlinear-programming-spring-2004/lecture-notes/lec4_quad_form.pdf

另见

二次函数 ax² + bx + c 形式 二次函数的标准形式

其中a,bc是实数。

下图提供了最小值/最大值和参数的快速参考,这些参数调节了函数的凸/凹外观和感觉:

从头开始编写使用梯度下降(GD)的二次成本函数优化

在这个配方中,我们将编写一个名为梯度下降(GD)的迭代数值优化技术,以找到二次函数f(x) = 2x² - 8x +9的最小值。

这里的重点从使用数学来解决最小值(将第一导数设置为零)转移到了一种名为梯度下降(GD)的迭代数值方法,该方法从一个猜测开始,然后在每次迭代中使用成本/误差函数作为指导方针逐渐接近解决方案。

如何做...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 使用包指令设置路径:package spark.ml.cookbook.chapter9

  3. 导入必要的包。

scala.util.control.Breaks将允许我们跳出程序。我们仅在调试阶段使用它,当程序无法收敛或陷入永无止境的过程中(例如,当步长过大时)。

import scala.collection.mutable.ArrayBuffer
import scala.util.control.Breaks._
  1. 这一步定义了我们试图最小化的实际二次函数:
def quadratic_function_itself(x:Double):Double = {
// the function being differentiated
// f(x) = 2x² - 8x + 9
return 2 * math.pow(x,2) - (8*x) + 9
}
  1. 这一步定义了函数的导数。这被称为点 x 处的梯度。这是函数f(x) = 2x² - 8x + 9的一阶导数。
def derivative_of_function(x:Double):Double = {
// The derivative of f(x)
return 4 * x - 8
}
  1. 在这一步中,我们设置一个随机起始点(这里设置为 13)。这将成为我们在x轴上的初始起始点。
var currentMinimumValue = 13.0 // just pick up a random value
  1. 我们继续设置从上一个配方优化二次成本函数并仅使用数学来获得洞察力找到最小值计算出的实际最小值,以便我们可以计算我们的估计与实际的每次迭代。
val actualMinima = 2.0 // proxy for a label in training phase

这一点试图充当您在 ML 算法的训练阶段提供的标签。在现实生活中,我们会有一个带有标签的训练数据集,并让算法进行训练并相应地调整其参数。

  1. 设置记录变量并声明ArrayBuffer数据结构以存储成本(错误)加上估计的最小值,以便进行检查和绘图:
var oldMinimumValue = 0.0
var iteration = 0;
var minimumVector = ArrayBuffer[Double]()
var costVector = ArrayBuffer[Double]()
  1. 梯度下降算法的内部控制变量在这一步中设置:
val stepSize = .01
val tolerance = 0.0001

stepSize,也被称为学习率,指导程序每次移动多少,而容差帮助算法在接近最小值时停止。

  1. 我们首先设置一个循环来迭代,并在接近最小值时停止,基于期望的容差:
while (math.abs(currentMinimumValue - oldMinimumValue) > tolerance) {
iteration +=1 //= iteration + 1 for debugging when non-convergence
  1. 我们每次更新最小值并调用函数计算并返回当前更新点的导数值:
oldMinimumValue = currentMinimumValue
val gradient_value_at_point = derivative_of_function(oldMinimumValue)
  1. 我们决定移动多少,首先通过取上一步返回的导数值,然后将其乘以步长(即,我们对其进行缩放)。然后我们继续更新当前最小值并减少它的移动(导数值 x 步长):
val move_by_amount = gradient_value_at_point * stepSize
currentMinimumValue = oldMinimumValue - move_by_amount
  1. 我们通过使用一个非常简单的平方距离公式来计算我们的成本函数值(错误)。在现实生活中,实际的最小值将从训练中得出,但在这里我们使用上一个配方优化二次成本函数并仅使用数学来获得洞察力找到最小值中的值。
costVector += math.pow(actualMinima - currentMinimumValue, 2)
minimumVector += currentMinimumValue
  1. 我们生成一些中间输出结果,以便您观察每次迭代时 currentMinimum 的行为:
print("Iteration= ",iteration," currentMinimumValue= ", currentMinimumValue)
print("\n")

输出将如下所示:

(Iteration= ,1, currentMinimumValue= ,12.56)
(Iteration= ,2, currentMinimumValue= ,12.1376)
(Iteration= ,3, currentMinimumValue= ,11.732096)
(Iteration= ,4, currentMinimumValue= ,11.342812160000001)
(Iteration= ,5, currentMinimumValue= ,10.9690996736)
(Iteration= ,6, currentMinimumValue= ,10.610335686656)
(Iteration= ,7, currentMinimumValue= ,10.265922259189761)
(Iteration= ,8, currentMinimumValue= ,9.935285368822171)
..........
..........
..........
(Iteration= ,203, currentMinimumValue= ,2.0027698292180602)
(Iteration= ,204, currentMinimumValue= ,2.0026590360493377)
(Iteration= ,205, currentMinimumValue= ,2.0025526746073643)
(Iteration= ,206, currentMinimumValue= ,2.00245056762307)
(Iteration= ,207, currentMinimumValue= ,2.002352544918147)
  1. 以下声明包括一个提醒,即使优化算法如何实现,它也应始终提供退出非收敛算法的手段(即,它应防范用户输入和边缘情况):
if (iteration == 1000000) break //break if non-convergence - debugging
}
  1. 我们在每次迭代中收集的成本和最小值向量的输出,以供以后分析和绘图:
print("\n Cost Vector: "+ costVector)
print("\n Minimum Vactor" + minimumVector)

输出是:

Cost vector: ArrayBuffer(111.51360000000001, 102.77093376000002, 94.713692553216, 87.28813905704389, ........7.0704727116774655E-6, 6.516147651082496E-6, 6.005281675238673E-6, 5.534467591900128E-6)

Minimum VactorArrayBuffer(12.56, 12.1376, 11.732096, 11.342812160000001, 10.9690996736, 10.610335686656, 10.265922259189761, 9.935285368822171, ........2.0026590360493377, 2.0025526746073643, 2.00245056762307, 2.002352544918147)

  1. 我们定义并设置最终最小值和实际函数值*f(minima)*的变量。它们充当最小值的(X,Y)位置:
var minimaXvalue= currentMinimumValue
var minimaYvalue= quadratic_function_itself(currentMinimumValue)
  1. 我们打印出最终结果,与我们在配方中的计算匹配,优化二次成本函数并仅使用数学来获得洞察力找到最小值,使用迭代方法。最终输出应该是我们的最小值位于(2,1),可以通过视觉或计算通过配方优化二次成本函数并仅使用数学来获得洞察力找到最小值进行检查。
print("\n\nGD Algo: Local minimum found at X="+f"$minimaXvalue%1.2f")
print("\nGD Algo: Y=f(x)= : "+f"$minimaYvalue%1.2f")
}

输出是:

GD Algo: Local minimum found at X = : 2.00 GD Algo: Y=f(x)= : 1.00

该过程以退出码 0 完成

工作原理...

梯度下降技术利用了函数的梯度(在这种情况下是一阶导数)指向下降方向的事实。概念上,梯度下降(GD)优化成本或错误函数以搜索模型的最佳参数。下图展示了梯度下降的迭代性质:

我们通过定义步长(学习率)、容差、要进行微分的函数以及函数的一阶导数来开始配方,然后继续迭代并从初始猜测(在这种情况下为 13)接近目标最小值 0。

在每次迭代中,我们计算了点的梯度(该点的一阶导数),然后使用步长对其进行缩放,以调节每次移动的量。由于我们在下降,我们从旧点中减去了缩放的梯度,以找到下一个更接近解决方案的点(以最小化误差)。

关于梯度值是应该加还是减以到达新点存在一些混淆,我们将在下面尝试澄清。指导原则应该是斜率是负还是正。为了朝着正确的方向移动,您必须朝着第一导数(梯度)的方向移动。

以下表格和图表提供了 GD 更新步骤的指南:

< 0负梯度 > 0正梯度

以下图表描述了单个步骤(负斜率)的内部工作,我们要么从起始点减去梯度,要么加上梯度,以到达下一个点,使我们离二次函数的最小值更近一步。例如,在这个配方中,我们从 13 开始,经过 200 多次迭代(取决于学习率),最终到达(2,1)的最小值,这与本章中的配方优化二次成本函数并仅使用数学来获得洞察中找到的解决方案相匹配。

为了更好地理解这些步骤,让我们尝试从前图的左侧跟随一个步骤,对于一个简单的函数。在这种情况下,我们位于曲线的左侧(原始猜测是负数),并且我们试图在每次迭代中向下爬升并增加 X,朝着梯度(一阶导数)的方向。

以下步骤将引导您浏览下一个图表,以演示核心概念和配方中的步骤:

  1. 在给定点计算导数--梯度。

  2. 使用步骤 1 中的梯度,并按步长进行缩放--移动的量。

  3. 通过减去移动量找到新位置:

  • 负梯度情况:在下图中,我们减去负梯度(有效地加上梯度)到原始点,以便向下爬升到的最小值 0。图中所示的曲线符合这种情况。

  • 正梯度情况:如果我们在曲线的另一侧,梯度为正,那么我们从先前位置减去正梯度数(有效减小梯度)以向下爬向最小值。本配方中的代码符合这种情况,我们试图从正数 13(初始猜测)开始,并以迭代方式向 0 的最小值移动。

  1. 更新参数并移动到新点。

  2. 我们不断重复这些步骤,直到收敛到解决方案,从而最小化函数。

  1. 重要的是要注意,梯度下降(GD)及其变体使用一阶导数,这意味着它们忽略曲率,而牛顿或拟牛顿(BFGS,LBFGS)方法等二阶导数算法使用梯度和曲率,有时还使用海森矩阵(相对于每个变量的部分导数矩阵)。

GD 的替代方案将是在整个域空间中搜索最佳设置,这既不切实际,也永远不会在实际意义上终止,因为真实大数据机器学习问题的规模和范围。

还有更多...

当你刚开始使用 GD 时,掌握步长或学习率非常重要。如果步长太小,会导致计算浪费,并给人一种梯度下降不收敛到解决方案的错觉。虽然对于演示和小项目来说设置步长是微不足道的,但将其设置为错误的值可能会导致大型 ML 项目的高计算损失。另一方面,如果步长太大,我们就会陷入乒乓情况或远离收敛,通常表现为误差曲线爆炸,意味着误差随着每次迭代而增加,而不是减少。

根据我们的经验,最好查看误差与迭代图表,并使用拐点来确定正确的值。另一种方法是尝试 .01, .001,......0001,并观察每次迭代的收敛情况(步长太小或太大)。值得记住的是,步长只是一个缩放因子,因为在某一点的实际梯度可能太大而无法移动(它会跳过最小值)。

总结:

  • 如果步长太小,收敛速度就会很慢。

  • 如果步长太大,你会跳过最小值(过冲),导致计算缓慢或出现乒乓效应(卡住)。

下图显示了基于不同步长的变化,以演示前面提到的要点。

  • 场景 1:步长= .01 - 步长适中 - 只是稍微有点小,但在大约 200 次迭代中完成了任务。我们不希望看到任何少于 200 的情况,因为它必须足够通用以在现实生活中生存。

  • 场景 2:步长= .001 - 步长太小,导致收敛速度缓慢。虽然看起来并不那么糟糕(1,500+次迭代),但可能被认为太细粒度了。

  • 场景 3:步长= .05 - 步长太大了。在这种情况下,算法会陷入困境,来回徘徊而无法收敛。不能再强调了,你必须考虑在现实生活中出现这种情况时的退出策略(数据的性质和分布会发生很大变化,所以要有所准备)。

  • 场景 4:步长= .06 - 步长太大,导致不收敛和爆炸。误差曲线爆炸(以非线性方式增加),意味着误差随着每次迭代而变大,而不是变小。在实践中,我们看到这种情况(场景 4)比之前的情况更多,但两种情况都可能发生,所以你应该为两种情况做好准备。正如你所看到的,场景 3 和场景 4 之间步长的微小差异造成了梯度下降行为的不同。这也是使算法交易困难的相同问题(优化)。

值得一提的是,对于这种光滑凸优化问题,局部最小值通常与全局最小值相同。你可以将局部最小值/最大值视为给定范围内的极值。对于同一函数,全局最小值/最大值指的是函数整个范围内的全局或最绝对值。

另见

随机梯度下降:梯度下降(GD)有多种变体,其中随机梯度下降(SGD)是最受关注的。Apache Spark 支持随机梯度下降(SGD)变体,其中我们使用训练数据的子集来更新参数 - 这有点具有挑战性,因为我们需要同时更新参数。SGD 与 GD 之间有两个主要区别。第一个区别是 SGD 是一种在线学习/优化技术,而 GD 更多是一种离线学习/优化技术。SGD 与 GD 之间的第二个区别是由于不需要在更新任何参数之前检查整个数据集,因此收敛速度更快。这一区别在下图中有所体现:

我们可以在 Apache Spark 中设置批处理窗口大小,以使算法对大规模数据集更具响应性(无需一次遍历整个数据集)。SGD 会有一些与之相关的随机性,但总体上它是当今使用的“事实标准”方法。它速度更快,收敛速度更快。

在 GD 和 SGD 的情况下,您通过更新原始参数来寻找模型的最佳参数。不同之处在于,在核心 GD 中,您必须遍历所有数据点以在给定迭代中对参数进行单次更新,而在 SGD 中,您需要查看来自训练数据集的每个单个(或小批量)样本以更新参数。

对于简短的通用写作,一个好的起点是以下内容:

可以在 CMU、微软和统计软件杂志中找到更多数学处理:

编写梯度下降优化来解决线性回归问题

在这个示例中,我们将探讨如何编写梯度下降来解决线性回归问题。在上一个示例中,我们演示了如何编写 GD 来找到二次函数的最小值。

这个示例演示了一个更现实的优化问题,我们通过 Scala 在 Apache Spark 2.0+上优化(最小化)最小二乘成本函数来解决线性回归问题。我们将使用真实数据运行我们的算法,并将结果与一流的商业统计软件进行比较,以展示准确性和速度。

如何做...

  1. 我们首先从普林斯顿大学下载包含以下数据的文件:

来源:普林斯顿大学

  1. 下载源码:data.princeton.edu/wws509/datasets/#salary.

  2. 为了简化问题,我们选择yrsl来研究年级对薪水的影响。为了减少数据整理代码,我们将这两列保存在一个文件中(Year_Salary.csv),如下表所示,以研究它们的线性关系:

  1. 我们使用 IBM SPSS 软件的散点图来直观地检查数据。不能再次强调,视觉检查应该是任何数据科学项目的第一步。

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 我们使用 import 包将代码放在所需的位置:

package spark.ml.cookbook.chapter9.

前四个语句导入了 JFree 图表包的必要包,以便我们可以在同一代码库中绘制 GD 错误和收敛。第五个导入处理ArrayBuffer,我们用它来存储中间结果:

import java.awt.Color
import org.jfree.chart.plot.{XYPlot, PlotOrientation}
import org.jfree.chart.{ChartFactory, ChartFrame, JFreeChart}
import org.jfree.data.xy.{XYSeries, XYSeriesCollection}
import scala.collection.mutable.ArrayBuffer
  1. 定义数据结构以保存中间结果,因为我们最小化错误并收敛到斜率(mStep)和截距(bStep)的解决方案:
val gradientStepError = ArrayBuffer[(Int, Double)]()
val bStep = ArrayBuffer[(Int, Double)]()
val mStep = ArrayBuffer[(Int, Double)]()
  1. 定义通过 JFree 图表进行绘图的函数。第一个函数只显示图表,第二个函数设置图表属性。这是一个模板代码,您可以根据自己的喜好进行自定义:
def show(chart: JFreeChart) {
val frame = new ChartFrame("plot", chart)
frame.pack()
frame.setVisible(true)
}
def configurePlot(plot: XYPlot): Unit = {
plot.setBackgroundPaint(Color.WHITE)
plot.setDomainGridlinePaint(Color.BLACK)
plot.setRangeGridlinePaint(Color.BLACK)
plot.setOutlineVisible(false)
}
  1. 该函数基于最小二乘原理计算错误,我们最小化该错误以找到最佳拟合解决方案。该函数找到我们预测的值与训练数据中实际值(薪水)之间的差异。找到差异后,对其进行平方以计算总错误。pow()函数是一个 Scala 数学函数,用于计算平方。

来源:维基百科

Beta : Slope (m variable)
Alpha : Intercept b variable)

def compute_error_for_line_given_points(b:Double, m:Double, points: Array[Array[Double]]):Double = {
var totalError = 0.0
for( point <- points ) {
var x = point(0)
var y = point(1)
totalError += math.pow(y - (m * x + b), 2)
}
return totalError / points.length
}
  1. 下一个函数计算f(x)= b + mx的两个梯度(一阶导数),并在整个定义域(所有点)上对它们进行平均。这与第二个配方中的过程相同,只是我们需要偏导数(梯度),因为我们要最小化两个参数mb(斜率和截距),而不仅仅是一个参数。

在最后两行中,我们通过学习率(步长)将梯度进行缩放。我们这样做的原因是为了确保我们不会得到很大的步长,并超过最小值,导致出现乒乓情景或错误膨胀,正如前面的配方中所讨论的那样。

def step_gradient(b_current:Double, m_current:Double, points:Array[Array[Double]], learningRate:Double): Array[Double]= {
var b_gradient= 0.0
var m_gradient= 0.0
var N = points.length.toDouble
for (point <- points) {
var x = point(0)
var y = point(1)
b_gradient += -(2 / N) * (y - ((m_current * x) + b_current))
m_gradient += -(2 / N) * x * (y - ((m_current * x) + b_current))
}
var result = new ArrayDouble
result(0) = b_current - (learningRate * b_gradient)
result(1) = m_current - (learningRate * m_gradient)
return result
}
  1. 该函数读取并解析 CSV 文件:
def readCSV(inputFile: String) : Array[Array[Double]] = {scala.io.Source.fromFile(inputFile)
.getLines()
.map(_.split(",").map(_.trim.toDouble))
.toArray
}
  1. 以下是一个包装函数,它循环 N 次迭代,并调用step_gradient()函数来计算给定点的梯度。然后,我们继续逐步存储每一步的结果,以便以后处理(例如绘图)。

值得注意的是使用Tuple2()来保存step_gradient()函数的返回值。

在函数的最后几步中,我们调用compute_error_for_line_given_points()函数来计算给定斜率和截距组合的错误,并将其存储在gradientStepError中。

def gradient_descent_runner(points:Array[Array[Double]], starting_b:Double, starting_m:Double, learning_rate:Double, num_iterations:Int):Array[Double]= {
var b = starting_b
var m = starting_m
var result = new ArrayDouble
var error = 0.0
result(0) =b
result(1) =m
for (i <-0 to num_iterations) {
result = step_gradient(result(0), result(1), points, learning_rate)
bStep += Tuple2(i, result(0))
mStep += Tuple2(i, result(1))
error = compute_error_for_line_given_points(result(0), result(1), points)
gradientStepError += Tuple2(i, error)
}
  1. 最后一步是主程序,它设置了斜率、截距、迭代次数和学习率的初始起点。我们故意选择了较小的学习率和较大的迭代次数,以展示准确性和速度。

  2. 首先,我们从初始化 GD 的关键控制变量开始(学习率、迭代次数和起始点)。

  3. 其次,我们继续显示起始点(0,0),并调用compute_error_for_line_given_points()来显示起始错误。值得注意的是,经过 GD 运行后,错误应该更低,并在最后一步显示结果。

    1. 第三,我们为 JFree 图表设置必要的调用和结构,以显示两个图表,描述斜率、截距和错误的行为,当我们朝着优化解决方案(最小化错误的最佳斜率和截距组合)合并时。
def main(args: Array[String]): Unit = {
val input = "../data/sparkml2/chapter9/Year_Salary.csv"
val points = readCSV(input)
val learning_rate = 0.001
val initial_b = 0
val initial_m = 0
val num_iterations = 30000
println(s"Starting gradient descent at b = $initial_b, m =$initial_m, error = "+ compute_error_for_line_given_points(initial_b, initial_m, points))
println("Running...")
val result= gradient_descent_runner(points, initial_b, initial_m, learning_rate, num_iterations)
var b= result(0)
var m = result(1)
println( s"After $num_iterations iterations b = $b, m = $m, error = "+ compute_error_for_line_given_points(b, m, points))
val xy = new XYSeries("")
gradientStepError.foreach{ case (x: Int,y: Double) => xy.add(x,y) }
val dataset = new XYSeriesCollection(xy)
val chart = ChartFactory.createXYLineChart(
"Gradient Descent", // chart title
"Iteration", // x axis label
"Error", // y axis label
dataset, // data
PlotOrientation.VERTICAL,
false, // include legend
true, // tooltips
false // urls)
val plot = chart.getXYPlot()
configurePlot(plot)
show(chart)
val bxy = new XYSeries("b")
bStep.foreach{ case (x: Int,y: Double) => bxy.add(x,y) }
val mxy = new XYSeries("m")
mStep.foreach{ case (x: Int,y: Double) => mxy.add(x,y) }
val stepDataset = new XYSeriesCollection()
stepDataset.addSeries(bxy)
stepDataset.addSeries(mxy)
val stepChart = ChartFactory.createXYLineChart(
"Gradient Descent Steps", // chart title
"Iteration", // x axis label
"Steps", // y axis label
stepDataset, // data
PlotOrientation.VERTICAL,
true, // include legend
true, // tooltips
false // urls
)
val stepPlot = stepChart.getXYPlot()
configurePlot(stepPlot)
show(stepChart)
}
  1. 以下是此配方的输出。

首先,我们显示起始点为 0,0,错误为 6.006,然后允许算法运行,并在完成迭代次数后显示结果:

值得注意的是起始和结束的错误数字以及由于优化而随时间减少。

  1. 我们使用 IBM SPSS 作为控制点,以显示我们组合的 GD 算法与 SPSS 软件生成的结果(几乎 1:1)几乎完全相同!

下图显示了 IBM SPSS 的输出,用于比较结果:

  1. 在最后一步,程序并排生成了两个图表。

下图显示了斜率(m)和截距(b)是如何朝着最小化错误的最佳组合收敛的,当我们通过迭代运行时。

下图显示了斜率(m)和截距(b)是如何朝着最小化错误的最佳组合收敛的,当我们通过迭代运行时。

工作原理...

梯度下降是一种迭代的数值方法,它从一个初始猜测开始,然后通过查看一个错误函数来询问自己,我做得有多糟糕,这个错误函数是训练文件中预测数据与实际数据的平方距离。

在这个程序中,我们选择了一个简单的线性方程f(x) = b + mx作为我们的模型。为了优化并找出最佳的斜率 m 和截距 b 的组合,我们有 52 对实际数据(年龄,工资)可以代入我们的线性模型(预测工资=斜率 x 年龄+截距)。简而言之,我们想要找到最佳的斜率和截距的组合,帮助我们拟合一个最小化平方距离的线性线。平方函数给我们所有正值,并让我们只关注错误的大小。

  • ReadCSV(): 读取和解析数据文件到我们的数据集中:

(x[1], y[1]), (x[2], y[2]), (x[3], y[4]), ... (x[52], y[52])

  • Compute_error_for_line_given_points(): 这个函数实现了成本或错误函数。我们使用一个线性模型(一条直线的方程)来预测,然后测量与实际数字的平方距离。在添加错误后,我们平均并返回总错误:

y[i] = mx[i] + b:对于所有数据对(x, y)

函数内值得注意的代码:第一行代码计算了预测值(m * x + b)与实际值(y)之间的平方距离。第二行代码对其进行平均并返回:

*totalError += math.pow(y - (m * x + b), 2)**....*return totalError / points.length

下图显示了最小二乘法的基本概念。简而言之,我们取实际训练数据与我们的模型预测之间的距离,然后对它们进行平方,然后相加。我们平方的原因是为了避免使用绝对值函数abs(),这在计算上是不可取的。平方差具有更好的数学性质,提供了连续可微的性质,这在想要最小化它时是更可取的。

  • step_gradient(): 这个函数是计算梯度(一阶导数)的地方,使用我们正在迭代的当前点(x[i],y[i])。需要注意的是,与之前的方法不同,我们有两个参数,所以我们需要计算截距(b_gradient)和斜率(m_gradient)的偏导数。然后我们需要除以点的数量来求平均。

  • 使用对截距(b)的偏导数:

b_gradient += -(2 / N) * (y - ((m_current * x) + b_current))

  • 使用对斜率(m)的偏导数:

m_gradient += -(2 / N) * x * (y - ((m_current * x) + b_current))

  • 最后一步是通过学习率(步长)来缩放计算出的梯度,然后移动到斜率(m_current)和截距(b_current)的新估计位置:*result(0) = b_current - (learningRate * b_gradient)*result(1) = m_current - (learningRate * m_gradient)

  • gradient_descent_runner(): 这是执行step_gradient()compute_error_for_line_given_points()的驱动程序,执行定义的迭代次数:

r (i <-0 to num_iterations) {
step_gradient()
...
compute_error_for_line_given_points()
...
}

还有更多...

虽然这个方法能够处理现实生活中的数据并与商业软件的估计相匹配,但在实践中,您需要实现随机梯度下降。

Spark 2.0 提供了带有小批量窗口的随机梯度下降(SGD)。

Spark 提供了两种利用 SGD 的方法。第一种选择是使用独立的优化技术,您可以通过传入优化函数来使用。参见以下链接:spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.mllib.optimization.Optimizerspark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.mllib.optimization.GradientDescent

第二种选择是使用已经内置了 SGD 的专门 API 作为它们的优化技术:

  • LogisticRegressionWithSGD()

  • StreamingLogisticRegressionWithSGD()

  • LassoWithSGD()

  • LinearRegressionWithSGD()

  • RidgeRegressionWithSGD()

  • SVMWithSGD()

截至 Spark 2.0,所有基于 RDD 的回归只处于维护模式。

另请参阅

正规方程作为解决 Spark 2.0 中线性回归的替代方法

在这个示例中,我们提供了使用正规方程来解决线性回归的梯度下降(GD)和 LBFGS 的替代方法。在正规方程的情况下,您正在将回归设置为特征矩阵和标签向量(因变量),同时尝试通过使用矩阵运算(如逆、转置等)来解决它。

重点在于强调 Spark 使用正规方程来解决线性回归的便利性,而不是模型或生成系数的细节。

如何操作...

  1. 我们使用了在第五章和第六章中广泛涵盖的相同的房屋数据集,这些章节分别是Spark 2.0 中的回归和分类的实际机器学习-第 I 部分Spark 2.0 中的回归和分类的实际机器学习-第 II 部分,它们将各种属性(例如房间数量等)与房屋价格相关联。

数据可在Chapter 9数据目录下的housing8.csv中找到。

  1. 我们使用 package 指令来处理放置:
package spark.ml.cookbook.chapter9
  1. 然后导入必要的库:
import org.apache.spark.ml.feature.LabeledPoint
import org.apache.spark.ml.linalg.Vectors
import org.apache.spark.ml.regression.LinearRegression
import org.apache.spark.sql.SparkSession
import org.apache.log4j.{Level, Logger}
import spark.implicits._
  1. 将 Spark 生成的额外输出减少,将 Logger 信息级别设置为Level.ERROR
Logger.getLogger("org").setLevel(Level.ERROR)
Logger.getLogger("akka").setLevel(Level.ERROR)
  1. 使用适当的属性设置 SparkSession:
val spark = SparkSession
.builder
.master("local[*]")
.appName("myRegressNormal")
.config("spark.sql.warehouse.dir", ".")
.getOrCreate()
  1. 读取输入文件并将其解析为数据集:
val data = spark.read.text("../data/sparkml2/housing8.csv").as[String]
val RegressionDataSet = data.map { line => val columns = line.split(',')
LabeledPoint(columns(13).toDouble , Vectors.dense(columns(0).toDouble,columns(1).toDouble, columns(2).toDouble, columns(3).toDouble,columns(4).toDouble,
columns(5).toDouble,columns(6).toDouble, columns(7).toDouble
))
}
  1. 显示以下数据集内容,但限制为前三行以供检查:

  1. 我们创建一个 LinearRegression 对象,并设置迭代次数、ElasticNet 和正则化参数。最后一步是通过选择setSolver("normal")来设置正确的求解器方法:
val lr = new LinearRegression()                                      
  .setMaxIter(1000)                                   
  .setElasticNetParam(0.0)  
  .setRegParam(0.01)                                    
  .setSolver("normal")

请确保将 ElasticNet 参数设置为 0.0,以便"normal"求解器正常工作。

  1. 使用以下内容将LinearRegressionModel拟合到数据中:
val myModel = lr.fit(RegressionDataSet)
Extract the model summary:
val summary = myModel.summary

运行程序时会生成以下输出:

training Mean Squared Error = 13.609079490110766
training Root Mean Squared Error = 3.6890485887435482

读者可以输出更多信息,但模型摘要已在第五章和第六章中进行了覆盖,Spark 2.0 中的回归和分类的实际机器学习-第 I 部分Spark 2.0 中的回归和分类的实际机器学习-第 II 部分,通过其他技术。

它是如何工作的...

我们最终尝试使用封闭形式公式解决线性回归的以下方程:

Spark 通过允许您设置setSolver("normal")提供了一个完全并行的解决这个方程的方法。

还有更多...

如果未将 ElasticNet 参数设置为 0.0,则会出现错误,因为在 Spark 中通过正规方程求解时使用了 L2 正则化(截至目前)。

有关 Spark 2.0 中等稳定回归的文档可以在以下网址找到:spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.ml.regression.LinearRegression 和 spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.ml.regression.LinearRegressionModel

模型摘要可以在以下链接找到:spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.ml.regression.LinearRegressionSummary



另请参阅

还可以参考以下表格:

迭代方法(SGD,LBFGS) 闭合形式正规方程
选择学习率 无参数
迭代次数可能很大 不迭代
在大特征集上表现良好 在大特征集上速度慢且不实用
容易出错:由于参数选择不当而卡住 (xTx)(-1)的计算代价高 - 复杂度为 n³

以下是关于 LinearRegression 对象配置的快速参考,但是请查看第五章,Spark 2.0 中的回归和分类的实用机器学习-第 I 部分和第六章,Spark 2.0 中的回归和分类的实用机器学习-第 II 部分以获取更多细节。

  • L1:套索回归

  • L2:岭回归

  • L1 - L2:弹性网络,可以调整参数

以下链接是哥伦比亚大学的一篇文章,解释了正规方程与解决线性回归问题的关系:

第十章:使用决策树和集成模型构建机器学习系统

在本章中,我们将涵盖:

  • 获取和准备真实世界的医疗数据,以探索 Spark 2.0 中的决策树和集成模型

  • 在 Spark 2.0 中使用决策树构建分类系统

  • 在 Spark 2.0 中使用决策树解决回归问题

  • 在 Spark 2.0 中使用随机森林树构建分类系统

  • 在 Spark 2.0 中使用随机森林树解决回归问题

  • 在 Spark 2.0 中使用梯度提升树(GBT)构建分类系统

  • 在 Spark 2.0 中使用梯度提升树(GBT)解决回归问题

介绍

决策树是商业中最古老和广泛使用的机器学习方法之一。它们受欢迎的原因不仅在于它们处理更复杂的分区和分割的能力(它们比线性模型更灵活),还在于它们解释我们是如何得出解决方案以及“为什么”结果被预测或分类为类/标签的能力。

Apache Spark 提供了一系列基于决策树的算法,完全能够利用 Spark 中的并行性。实现范围从直接的单决策树(CART 类型算法)到集成树,例如随机森林树和梯度提升树(GBT)。它们都有变体风味,以便进行分类(例如,分类,例如,身高=矮/高)或回归(例如,连续,例如,身高=2.5 米)的便利。

以下图描述了一个思维导图,显示了决策树算法在 Spark ML 库中的覆盖范围,截至撰写时:

快速理解决策树算法的一种方法是将其视为一种智能分区算法,它试图最小化损失函数(例如,L2 或最小二乘),因为它将范围划分为最适合数据的分段空间。通过对数据进行采样并尝试组合特征,该算法变得更加复杂,从而组装出更复杂的集成模型,其中每个学习者(部分样本或特征组合)都对最终结果进行投票。

以下图描述了一个简化版本,其中一个简单的二叉树(树桩)被训练为将数据分类为属于两种不同颜色的段(例如,健康患者/患病患者)。该图描述了一个简单的算法,每次建立决策边界(因此分类)时,它只是将 x/y 特征空间分成一半,同时最小化错误的数量(例如,L2 最小二乘测量):

以下图提供了相应的树,以便我们可以可视化算法(在这种情况下,简单的分而治之)针对提出的分割空间。决策树算法受欢迎的原因是它们能够以一种易于向业务用户沟通的语言显示其分类结果,而无需太多数学:

Spark 中的决策树是一种并行算法,旨在将单个树拟合和生长到可以是分类(分类)或连续(回归)的数据集中。它是一种贪婪算法,基于树桩(二进制分割等),通过递归地划分解空间,尝试使用信息增益最大化(基于熵)来选择所有可能分割中的最佳分割。

集成模型

观察 Spark 对决策树的另一种方式是将算法视为属于两个阵营。第一个阵营,我们在介绍中看到过,关注于试图找到各种技术来为数据集找到最佳的单棵树。虽然这对许多数据集来说是可以的,但算法的贪婪性质可能会导致意想不到的后果,比如过拟合和过度深入以能够捕捉训练数据中的所有边界(即过度优化)。

为了克服过拟合问题并提高准确性和预测质量,Spark 实现了两类集成决策树模型,试图创建许多不完美的学习器,这些学习器要么看到数据的子集(有或没有替换地采样),要么看到特征的子集。虽然每棵单独的树不太准确,但树的集合组装的投票(或在连续变量的情况下的平均概率)和由此产生的平均值比任何单独的树更准确:

  • 随机森林:这种方法并行创建许多树,然后投票/平均结果以最小化单棵树算法中容易出现的过拟合问题。它们能够捕捉非线性和特征交互而无需任何缩放。它们应该至少被认真考虑为用于解剖数据并了解其构成的第一工具集之一。以下图提供了 Spark 中此实现的可视化指南:

  • 梯度提升树:这种方法是另一种集成模型,通过许多树的平均值(即使它们不太完美)来提高预测的准确性和质量。它们与随机森林的不同之处在于,它们一次构建一棵树,每棵树都试图从前一棵树的缺点中学习,通过最小化损失函数来改进。它们类似于梯度下降的概念,但它们使用最小化(类似于梯度)来选择和改进下一棵树(它们沿着创建最佳准确性的树的方向前进)。

损失函数的三个选项是:

  • 对数损失:分类的负对数似然

  • L2:回归的最小二乘

  • L1:回归的绝对误差

以下图提供了一个易于使用的可视化参考:

Spark 中决策树的主要包在 ML 中,如下所示:

org.apache.spark.mllib.tree
org.apache.spark.mllib.tree.configuration
org.apache.spark.mllib.tree.impurity
org.apache.spark.mllib.tree.model

不纯度的度量

对于所有的机器学习算法,我们都试图最小化一组成本函数,这些函数帮助我们选择最佳的移动。Spark 使用三种可能的最大化函数选择。以下图描述了这些替代方案:

在本节中,我们将讨论三种可能的替代方案:

  • 信息增益:粗略地说,这是根据熵的概念来衡量群体中不纯度的水平--参见香农信息理论,然后后来由 Quinlan 在他的 ID3 算法中建议。

熵的计算如下方程所示:

信息增益帮助我们在每个特征向量空间中选择一个属性,这个属性可以最好地帮助我们将类别彼此分开。我们使用这个属性来决定如何对节点中的属性进行排序(从而影响决策边界)。

以下图形以易于理解的方式描述了计算。在第一步中,我们希望选择一个属性,以便在根节点或父节点中最大化 IG(信息增益),然后为所选属性的每个值构建我们的子节点(它们的关联向量)。我们不断递归地重复算法,直到我们再也看不到任何收益:

  • 基尼指数: 这试图通过隔离类别来改进信息增益(IG),以便最大的类别与总体分离。基尼指数与熵有些不同,因为您尝试实现 50/50 的分割,然后应用进一步的分割来推断解决方案。它旨在反映一个变量的影响,并且不会扩展其影响到多属性状态。它使用简单的频率计数来对总体进行评估。用于更高维度和更多噪音数据的基尼指数。

在您拥有复杂的多维数据并且正在尝试从中解剖简单信号的情况下,请使用基尼不纯度。

另一方面,在您拥有更清洁和低维数据集的情况下,可以使用信息增益(或任何基于熵的系统),但您正在寻找更复杂(在准确性和质量方面)的数据集:

  • 方差:方差用于信号树算法的回归模型。简而言之,我们仍然试图最小化 L2 函数,但不同之处在于这里我们试图最小化观察值与被考虑的节点(段)的平均值之间的距离的平方。

以下图表描述了可视化的简化版本:

用于评估树模型的 Spark 模型评估工具包括以下内容:

混淆矩阵是用来描述分类模型性能的表格,其中真实值已知的测试数据集。混淆矩阵本身相对简单;它是一个 2x2 的矩阵:

预测
实际 真正例(TP) 假反例(FN)
假正例(FP) 真反例(TN)

对于我们的癌症数据集:

  • 真正例(TP): 预测为是,且他们确实患有乳腺癌

  • 真反例(TN): 预测为否,且他们没有乳腺癌

  • 假正例(FP): 我们预测为是,但他们没有乳腺癌

  • 假反例(FN): 我们预测为否,但他们确实患有乳腺癌

一个良好的分类系统应该与现实情况密切匹配,具有良好的 TP 和 TN 值,同时具有较少的 FP 和 FN 值。

总的来说,以下术语也被用作分类模型的标记:

  1. 准确性:模型的正确率:
  • (TP + TN)/总数
  1. 错误:总体上,模型错误的百分比:
  • (FP+FN)/总数

  • 也等于 1-准确性

在 Spark 机器学习库中,有一个实用程序类来处理上述常见矩阵的计算:

   org.apache.spark.mllib.evaluation.MulticlassMetrics 

我们将在以下示例代码中使用实用程序类。

同样,对于回归算法,均方误差(MSE) 或误差平方的平均值,被广泛用作模型测量的关键参数。在 Spark 机器学习库中,也有一个实用程序类,它将提供回归模型的关键指标:

   org.apache.spark.mllib.evaluation.RegressionMetrics 

Spark 矩阵评估器的文档可以在spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.mllib.evaluation.MulticlassMetricsspark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.mllib.evaluation.RegressionMetrics找到。

获取和准备真实世界的医学数据,以探索 Spark 2.0 中的决策树和集成模型

使用决策树在机器学习中的真实应用。我们使用了一个癌症数据集来预测患者病例是恶性还是良性。为了探索决策树的真正威力,我们使用了一个展现真实生活非线性和复杂误差表面的医学数据集。

如何做...

威斯康星乳腺癌数据集是从威斯康星大学医院的 William H Wolberg 博士处获得的。该数据集是定期获得的,因为 Wolberg 博士报告了他的临床病例。

该数据集可以从多个来源检索,并且可以直接从加州大学尔湾分校的网络服务器 archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/breast-cancer-wisconsin.data 获取。

数据也可以从威斯康星大学的网络服务器 ftp://ftp.cs.wisc.edu/math-prog/cpo-dataset/machine-learn/cancer/cancer1/datacum 获取。

该数据集目前包含 1989 年至 1991 年的临床病例。它有 699 个实例,其中 458 个被分类为良性肿瘤,241 个被分类为恶性病例。每个实例由九个属性描述,属性值在 1 到 10 的范围内,并带有二进制类标签。在这 699 个实例中,有 16 个实例缺少一些属性。

我们将从内存中删除这 16 个实例,并处理其余的(总共 683 个实例)进行模型计算。

样本原始数据如下所示:

1000025,5,1,1,1,2,1,3,1,1,2
1002945,5,4,4,5,7,10,3,2,1,2
1015425,3,1,1,1,2,2,3,1,1,2
1016277,6,8,8,1,3,4,3,7,1,2
1017023,4,1,1,3,2,1,3,1,1,2
1017122,8,10,10,8,7,10,9,7,1,4
...

属性信息如下:

# 属性
1 样本编号 ID 编号
2 块厚度 1 - 10
3 细胞大小的均匀性 1 - 10
4 细胞形态的均匀性 1 - 10
5 边缘粘附 1 - 10
6 单个上皮细胞大小 1 - 10
7 裸核 1 - 10
8 淡染色质 1 - 10
9 正常核仁 1 - 10
10 有丝分裂 1 - 10
11 类别 (2 表示良性,4 表示恶性)

如果以正确的列呈现,将如下所示:

ID 编号 块厚度 细胞大小的均匀性 细胞形态的均匀性 边缘粘附 单个上皮细胞大小 裸核 淡染色质 正常核仁 有丝分裂 类别
1000025 5 1 1 1 2 1 3 1 1 2
1002945 5 4 4 5 7 10 3 2 1 2
1015425 3 1 1 1 2 2 3 1 1 2
1016277 6 8 8 1 3 4 3 7 1 2
1017023 4 1 1 3 2 1 3 1 1 2
1017122 8 10 10 8 7 10 9 7 1 4
1018099 1 1 1 1 2 10 3 1 1 2
1018561 2 1 2 1 2 1 3 1 1 2
1033078 2 1 1 1 2 1 1 1 5 2
1033078 4 2 1 1 2 1 2 1 1 2
1035283 1 1 1 1 1 1 3 1 1 2
1036172 2 1 1 1 2 1 2 1 1 2
1041801 5 3 3 3 2 3 4 4 1 4
1043999 1 1 1 1 2 3 3 1 1 2
1044572 8 7 5 10 7 9 5 5 4 4
... ... ... ... ... ... ... ... ... ... ...

还有更多...

威斯康星乳腺癌数据集在机器学习社区中被广泛使用。该数据集包含有限的属性,其中大部分是离散数字。非常容易将分类算法和回归模型应用于该数据集。

已经有 20 多篇研究论文和出版物引用了这个数据集,它是公开可用的,非常容易使用。

该数据集具有多变量数据类型,其中属性为整数,属性数量仅为 10。这使得它成为本章分类和回归分析的典型数据集之一。

在 Spark 2.0 中构建决策树分类系统

在本示例中,我们将使用乳腺癌数据并使用分类来演示 Spark 中的决策树实施。我们将使用 IG 和 Gini 来展示如何使用 Spark 已经提供的设施,以避免冗余编码。此示例尝试使用二进制分类来拟合单棵树,以训练和预测数据集的标签(良性(0.0)和恶性(1.0))。

如何做到这一点

  1. 在 IntelliJ 或您选择的 IDE 中启动新项目。确保包含必要的 JAR 文件。

  2. 设置程序所在的包位置:

package spark.ml.cookbook.chapter10
  1. 导入 Spark 上下文所需的必要包,以便访问集群和Log4j.Logger以减少 Spark 产生的输出量:
import org.apache.spark.mllib.evaluation.MulticlassMetrics
import org.apache.spark.mllib.tree.DecisionTree
import org.apache.spark.mllib.linalg.Vectors
import org.apache.spark.mllib.regression.LabeledPoint
import org.apache.spark.mllib.tree.model.DecisionTreeModel
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.SparkSession
import org.apache.log4j.{Level, Logger} 
  1. 创建 Spark 的配置和 Spark 会话,以便我们可以访问集群:
Logger.getLogger("org").setLevel(Level.ERROR)

 val spark = SparkSession
 .builder.master("local[*]")
 .appName("MyDecisionTreeClassification")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()
  1. 我们读取原始原始数据文件:
val rawData = spark.sparkContext.textFile("../data/sparkml2/chapter10/breast-cancer-wisconsin.data")
  1. 我们预处理数据集:
val data = rawData.map(_.trim)
 .filter(text => !(text.isEmpty || text.startsWith("#") || text.indexOf("?") > -1))
 .map { line =>
 val values = line.split(',').map(_.toDouble)
 val slicedValues = values.slice(1, values.size)
 val featureVector = Vectors.dense(slicedValues.init)
 val label = values.last / 2 -1
 LabeledPoint(label, featureVector)
 }

首先,我们修剪行并删除任何空格。一旦行准备好进行下一步,如果行为空或包含缺失值(“?”),则删除行。在此步骤之后,内存中的数据集将删除 16 行缺失数据。

然后我们将逗号分隔的值读入 RDD。由于数据集中的第一列只包含实例的 ID 号,最好将此列从实际计算中删除。我们使用以下命令切片,将从 RDD 中删除第一列:

val slicedValues = values.slice(1, values.size)

然后我们将其余数字放入密集向量。

由于威斯康星州乳腺癌数据集的分类器要么是良性病例(最后一列值=2),要么是恶性病例(最后一列值=4),我们使用以下命令转换前面的值:

val label = values.last / 2 -1

因此,良性病例 2 转换为 0,恶性病例值 4 转换为 1,这将使后续计算更容易。然后将前一行放入Labeled Points

    Raw data: 1000025,5,1,1,1,2,1,3,1,1,2
    Processed Data: 5,1,1,1,2,1,3,1,1,0
    Labeled Points: (0.0, [5.0,1.0,1.0,1.0,2.0,1.0,3.0,1.0,1.0])
  1. 我们验证原始数据计数并处理数据计数:
println(rawData.count())
println(data.count())

然后您将在控制台上看到以下内容:

699
683
  1. 我们将整个数据集随机分成训练数据(70%)和测试数据(30%)。请注意,随机拆分将生成大约 211 个测试数据集。这大约是但并非完全是数据集的 30%:
val splits = data.randomSplit(Array(0.7, 0.3))
val (trainingData, testData) = (splits(0), splits(1))
  1. 我们定义一个度量计算函数,它利用 Spark 的MulticlassMetrics
def getMetrics(model: DecisionTreeModel, data: RDD[LabeledPoint]): MulticlassMetrics = {
 val predictionsAndLabels = data.map(example =>
 (model.predict(example.features), example.label)
 )
 new MulticlassMetrics(predictionsAndLabels)
 }

此函数将读取模型和测试数据集,并创建一个包含前面提到的混淆矩阵的度量。它将包含模型准确性,这是分类模型的指标之一。

  1. 我们定义一个评估函数,它可以接受一些可调参数用于决策树模型,并对数据集进行训练:
def evaluate(
 trainingData: RDD[LabeledPoint],
 testData: RDD[LabeledPoint],
 numClasses: Int,
 categoricalFeaturesInfo: Map[Int,Int],

 impurity: String,
 maxDepth: Int,
 maxBins:Int
 ) :Unit = {

 val model = DecisionTree.*trainClassifier*(trainingData, numClasses,
 categoricalFeaturesInfo,
 impurity, maxDepth, maxBins)
 val metrics = getMetrics(model, testData)
 println("Using Impurity :"+ impurity)
 println("Confusion Matrix :")
 println(metrics.confusionMatrix)
 println("Decision Tree Accuracy: "+metrics.*precision*)
 println("Decision Tree Error: "+ (1-metrics.*precision*))

 }

评估函数将读取几个参数,包括不纯度类型(模型的基尼或熵)并生成评估指标。

  1. 我们设置以下参数:
val numClasses = 2
 val categoricalFeaturesInfo = *Map*[Int, Int]()
 val maxDepth = 5
 val maxBins = 32

由于我们只有良性(0.0)和恶性(1.0),我们将 numClasses 设置为 2。其他参数是可调的,其中一些是算法停止标准。

  1. 首先我们评估基尼不纯度:
evaluate(trainingData, testData, numClasses, categoricalFeaturesInfo,
"gini", maxDepth, maxBins)

从控制台输出:

Using Impurity :gini
Confusion Matrix :
115.0 5.0
0 88.0
Decision Tree Accuracy: 0.9620853080568721
Decision Tree Error: 0.03791469194312791
To interpret the above Confusion metrics, Accuracy is equal to (115+ 88)/ 211 all test cases, and error is equal to 1 -accuracy
  1. 我们评估熵不纯度:
evaluate(trainingData, testData, numClasses, categoricalFeaturesInfo,
"entropy", maxDepth, maxBins)

从控制台输出:

Using Impurity:entropy
Confusion Matrix:
116.0 4.0
9.0 82.0
Decision Tree Accuracy: 0.9383886255924171
Decision Tree Error: 0.06161137440758291
To interpret the preceding confusion metrics, accuracy is equal to (116+ 82)/ 211 for all test cases, and error is equal to 1 - accuracy
  1. 然后通过停止会话来关闭程序:
spark.stop()

它是如何工作的...

数据集比通常更复杂,但除了一些额外步骤外,解析它与前几章介绍的其他示例相同。解析将数据以原始形式转换为中间格式,最终将成为 Spark ML 方案中常见的 LabelPoint 数据结构:

     Raw data: 1000025,5,1,1,1,2,1,3,1,1,2
     Processed Data: 5,1,1,1,2,1,3,1,1,0
     Labeled Points: (0.0, [5.0,1.0,1.0,1.0,2.0,1.0,3.0,1.0,1.0])

我们使用DecisionTree.trainClassifier()在训练集上训练分类器树。然后通过检查各种不纯度和混淆矩阵测量来演示如何衡量树模型的有效性。

鼓励读者查看输出并参考其他机器学习书籍,以了解混淆矩阵和不纯度测量的概念,以掌握 Spark 中决策树和变体。

还有更多...

为了更好地可视化,我们在 Spark 中包含了一个样本决策树工作流程,它将首先将数据读入 Spark。在我们的情况下,我们从文件创建 RDD。然后我们使用随机抽样函数将数据集分为训练数据和测试数据。

数据集分割后,我们使用训练数据集来训练模型,然后使用测试数据来测试模型的准确性。一个好的模型应该有一个有意义的准确度值(接近 1)。下图描述了工作流程:

基于威斯康星乳腺癌数据集生成了一棵样本树。红点代表恶性病例,蓝点代表良性病例。我们可以在下图中直观地检查这棵树:

另请参阅

在 Spark 2.0 中使用决策树解决回归问题

与之前的示例类似,我们将使用DecisionTree()类来训练和预测使用回归树模型的结果。刷新所有这些模型是CART分类和回归树)的一个变体,有两种模式。在这个示例中,我们探索了 Spark 中决策树实现的回归 API。

如何做...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序所在的包位置:

package spark.ml.cookbook.chapter10
  1. 导入 Spark 上下文所需的必要包,以便访问集群和Log4j.Logger以减少 Spark 产生的输出量:
import org.apache.spark.mllib.evaluation.RegressionMetrics
import org.apache.spark.mllib.linalg.Vectors
import org.apache.spark.mllib.regression.LabeledPoint
import org.apache.spark.mllib.tree.DecisionTree
import org.apache.spark.mllib.tree.model.DecisionTreeModel
import org.apache.spark.rdd.RDD

import org.apache.spark.sql.SparkSession
import org.apache.log4j.{Level, Logger}
  1. 创建 Spark 的配置和 Spark 会话,以便我们可以访问集群:
Logger.getLogger("org").setLevel(Level.*ERROR*)

 val spark = SparkSession
 .builder.master("local[*]")
 .appName("MyDecisionTreeRegression")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()
  1. 我们读取原始的原始数据文件:
val rawData = spark.sparkContext.textFile("../data/sparkml2/chapter10/breast-cancer-wisconsin.data")
  1. 我们预处理数据集(详细信息请参见前面的代码):
val data = rawData.map(_.trim)
 .filter(text => !(text.isEmpty || text.startsWith("#") || text.indexOf("?") > -1))
 .map { line =>
 val values = line.split(',').map(_.toDouble)
 val slicedValues = values.slice(1, values.size)
 val featureVector = Vectors.dense(slicedValues.init)
 val label = values.last / 2 -1
 LabeledPoint(label, featureVector)
 }
  1. 我们验证原始数据计数并处理数据计数:
println(rawData.count())
println(data.count())

在控制台上你会看到以下内容:

699
683
  1. 我们将整个数据集分为训练数据(70%)和测试数据(30%)集:
val splits = data.randomSplit(Array(0.7, 0.3))
val (trainingData, testData) = (splits(0), splits(1))
  1. 我们定义一个度量计算函数,该函数利用 Spark 的RegressionMetrics
def getMetrics(model: DecisionTreeModel, data: RDD[LabeledPoint]): RegressionMetrics = {
 val predictionsAndLabels = data.map(example =>
 (model.predict(example.features), example.label)
 )
 new RegressionMetrics(predictionsAndLabels)
 }
  1. 我们设置以下参数:
val categoricalFeaturesInfo = Map[Int, Int]()
val impurity = "variance" val maxDepth = 5
val maxBins = 32
  1. 我们首先评估基尼不纯度:
val model = DecisionTree.trainRegressor(trainingData, categoricalFeaturesInfo, impurity, maxDepth, maxBins)
val metrics = getMetrics(model, testData)
println("Test Mean Squared Error = " + metrics.meanSquaredError)
println("My regression tree model:\n" + model.toDebugString)

从控制台输出:

Test Mean Squared Error = 0.037363769271664016
My regression tree model:
DecisionTreeModel regressor of depth 5 with 37 nodes
If (feature 1 <= 3.0)
   If (feature 5 <= 3.0)
    If (feature 0 <= 6.0)
     If (feature 7 <= 3.0)
      Predict: 0.0
     Else (feature 7 > 3.0)
      If (feature 0 <= 4.0)
       Predict: 0.0
      Else (feature 0 > 4.0)
       Predict: 1.0
    Else (feature 0 > 6.0)
     If (feature 2 <= 2.0)
      Predict: 0.0
     Else (feature 2 > 2.0)
      If (feature 4 <= 2.0)
       Predict: 0.0
      Else (feature 4 > 2.0)
       Predict: 1.0
   Else (feature 5 > 3.0)
    If (feature 1 <= 1.0)
     If (feature 0 <= 5.0)
      Predict: 0.0
     Else (feature 0 > 5.0)
      Predict: 1.0
    Else (feature 1 > 1.0)
     If (feature 0 <= 6.0)
      If (feature 7 <= 4.0)
       Predict: 0.875
      Else (feature 7 > 4.0)
       Predict: 0.3333333333333333
     Else (feature 0 > 6.0)
      Predict: 1.0
  Else (feature 1 > 3.0)
   If (feature 1 <= 4.0)
    If (feature 4 <= 6.0)
     If (feature 5 <= 7.0)
      If (feature 0 <= 8.0)
       Predict: 0.3333333333333333
      Else (feature 0 > 8.0)
       Predict: 1.0
     Else (feature 5 > 7.0)
      Predict: 1.0
    Else (feature 4 > 6.0)
     Predict: 0.0
   Else (feature 1 > 4.0)
    If (feature 3 <= 1.0)
     If (feature 0 <= 6.0)
      If (feature 0 <= 5.0)
       Predict: 1.0
      Else (feature 0 > 5.0)
       Predict: 0.0
     Else (feature 0 > 6.0)
      Predict: 1.0
    Else (feature 3 > 1.0)
     Predict: 1.0
  1. 然后通过停止 Spark 会话来关闭程序:
spark.stop()

工作原理...

我们使用相同的数据集,但这次我们使用决策树来解决数据的回归问题。值得注意的是创建一个度量计算函数,该函数利用 Spark 的RegressionMetrics()

def getMetrics(model: DecisionTreeModel, data: RDD[LabeledPoint]): RegressionMetrics = {
 val predictionsAndLabels = data.map(example =>
 (model.predict(example.features), example.label)
 )
 new RegressionMetrics(predictionsAndLabels)
 }

然后我们继续使用DecisionTree.trainRegressor()来执行实际的回归,并获得不纯度测量(GINI)。然后我们继续输出实际的回归,这是一系列决策节点/分支和用于在给定分支上做出决策的值:

If (feature 0 <= 4.0)
       Predict: 0.0
      Else (feature 0 > 4.0)
       Predict: 1.0
    Else (feature 0 > 6.0)
     If (feature 2 <= 2.0)
      Predict: 0.0
     Else (feature 2 > 2.0)
      If (feature 4 <= 2.0)
........
........
.......

另请参阅

在 Spark 2.0 中使用随机森林树构建分类系统

在这个示例中,我们将探讨 Spark 中随机森林的实现。我们将使用随机森林技术来解决离散分类问题。由于 Spark 利用并行性(同时生长许多树),我们发现随机森林的实现非常快。我们也不需要太担心超参数,技术上我们只需设置树的数量。

如何做...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序将驻留的包位置:

package spark.ml.cookbook.chapter10
  1. 导入 Spark 上下文所需的包,以便访问集群和Log4j.Logger以减少 Spark 产生的输出量:
import org.apache.spark.mllib.evaluation.MulticlassMetrics
import org.apache.spark.mllib.linalg.Vectors
import org.apache.spark.mllib.regression.LabeledPoint
import org.apache.spark.mllib.tree.model.RandomForestModel
import org.apache.spark.rdd.RDD
import org.apache.spark.mllib.tree.RandomForest

import org.apache.spark.sql.SparkSession
import org.apache.log4j.{Level, Logger}

  1. 创建 Spark 的配置和 Spark 会话,以便我们可以访问集群:
Logger.getLogger("org").setLevel(Level.*ERROR*)

 val spark = SparkSession
 .builder.master("local[*]")
 .appName("MyRandomForestClassification")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()
  1. 我们读取原始原始数据文件:
val rawData = spark.sparkContext.textFile("../data/sparkml2/chapter10/breast-cancer-wisconsin.data")
  1. 我们对数据集进行预处理(有关详细信息,请参见前面的部分):
val data = rawData.map(_.trim)
 .filter(text => !(text.isEmpty || text.startsWith("#") || text.indexOf("?") > -1))
 .map { line =>
 val values = line.split(',').map(_.toDouble)
 val slicedValues = values.slice(1, values.size)
 val featureVector = Vectors.*dense*(slicedValues.init)
 val label = values.last / 2 -1
 LabeledPoint(label, featureVector)
 }
  1. 我们验证原始数据计数并处理数据计数:
println("Training Data count:"+trainingData.count())
println("Test Data Count:"+testData.count())

您将在控制台中看到以下内容:

Training Data count: 501 
Test Data Count: 182
  1. 我们将整个数据集随机分为训练数据(70%)和测试数据(30%):
val splits = data.randomSplit(Array(0.7, 0.3))
val (trainingData, testData) = (splits(0), splits(1))
  1. 我们定义了一个度量计算函数,它利用了 Spark 的MulticlassMetrics
def getMetrics(model: RandomForestModel, data: RDD[LabeledPoint]): MulticlassMetrics = {
 val predictionsAndLabels = data.map(example =>
 (model.predict(example.features), example.label)
 )
 new MulticlassMetrics(predictionsAndLabels)
 }

此函数将读取模型和测试数据集,并创建包含先前提到的混淆矩阵的度量。它将包含模型准确性,这是分类模型的指标之一。

  1. 我们定义了一个评估函数,该函数可以接受一些可调参数,用于随机森林模型,并对数据集进行训练:
def evaluate(
 trainingData: RDD[LabeledPoint],
 testData: RDD[LabeledPoint],
 numClasses: Int,
 categoricalFeaturesInfo: Map[Int,Int],
 numTrees: Int,
 featureSubsetStrategy: String,
 impurity: String,
 maxDepth: Int,
 maxBins:Int
 ) :Unit = {
val model = RandomForest.*trainClassifier*(trainingData, numClasses, categoricalFeaturesInfo, numTrees, featureSubsetStrategy,impurity, maxDepth, maxBins)
val metrics = *getMetrics*(model, testData)
println("Using Impurity :"+ impurity)
println("Confusion Matrix :")
println(metrics.confusionMatrix)
println("Model Accuracy: "+metrics.*precision*)
println("Model Error: "+ (1-metrics.*precision*))
 }

评估函数将读取几个参数,包括不纯度类型(模型的基尼或熵)并生成用于评估的度量。

  1. 我们设置了以下参数:
val numClasses = 2
 val categoricalFeaturesInfo = *Map*[Int, Int]()
 val numTrees = 3 *// Use more in practice.* val featureSubsetStrategy = "auto" *// Let the algorithm choose.

* val maxDepth = 4
 val maxBins = 32
  1. 我们首先评估基尼不纯度:
evaluate(trainingData, testData, numClasses,categoricalFeaturesInfo,numTrees,
featureSubsetStrategy, "gini", maxDepth, maxBins)

从控制台输出:

Using Impurity :gini
Confusion Matrix :
118.0 1.0
4.0 59.0
Model Accuracy: 0.9725274725274725
Model Error: 0.027472527472527486
To interpret the above Confusion metrics, Accuracy is equal to (118+ 59)/ 182 all test cases, and error is equal to 1 -accuracy
  1. 我们评估熵不纯度:
evaluate(trainingData, testData, numClasses, categoricalFeaturesInfo,
 "entropy", maxDepth, maxBins)

从控制台输出:

Using Impurity :entropy
Confusion Matrix :
115.0  4.0   
0.0    63.0
Model Accuracy: 0.978021978021978
Model Error: 0.02197802197802201             
To interpret the above Confusion metrics, Accuracy is equal to (115+ 63)/ 182 all test cases, and error is equal to 1 -accuracy
  1. 然后通过停止 Spark 会话来关闭程序:
spark.stop()

它是如何工作的...

数据与前一个示例中的数据相同,但我们使用随机森林和多指标 API 来解决分类问题:

  • RandomForest.trainClassifier()

  • MulticlassMetrics()

我们有很多选项可以调整随机森林树,以获得分类复杂表面的正确边缘。这里列出了一些参数:

 val numClasses = 2
 val categoricalFeaturesInfo = *Map*[Int, Int]()
 val numTrees = 3 // Use more in practice. val featureSubsetStrategy = "auto" // Let the algorithm choose. val maxDepth = 4
 val maxBins = 32

值得注意的是这个示例中的混淆矩阵。混淆矩阵是通过MulticlassMetrics() API 调用获得的。要解释前面的混淆度量,准确度等于(118+ 59)/ 182,对于所有测试案例,错误等于 1-准确度:

Confusion Matrix :
118.0 1.0
4.0 59.0
Model Accuracy: 0.9725274725274725
Model Error: 0.027472527472527486

另请参阅

在 Spark 2.0 中使用随机森林树解决回归问题

这与之前的步骤类似,但我们使用随机森林树来解决回归问题(连续)。以下参数用于指导算法应用回归而不是分类。我们再次将类的数量限制为两个:

val impurity = "variance" // USE variance for regression

如何做...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序所在的包位置:

package spark.ml.cookbook.chapter10
  1. 从 Spark 中导入必要的包:
import org.apache.spark.mllib.evaluation.RegressionMetrics
import org.apache.spark.mllib.linalg.Vectors
import org.apache.spark.mllib.regression.LabeledPoint
import org.apache.spark.mllib.tree.model.RandomForestModel
import org.apache.spark.rdd.RDD
import org.apache.spark.mllib.tree.RandomForest

import org.apache.spark.sql.SparkSession
import org.apache.log4j.{Level, Logger}

  1. 创建 Spark 的配置和 Spark 会话:
Logger.getLogger("org").setLevel(Level.*ERROR*)

val spark = SparkSession
.builder.master("local[*]")
.appName("MyRandomForestRegression")
.config("spark.sql.warehouse.dir", ".")
.getOrCreate()
  1. 我们读取原始的原始数据文件:
val rawData = spark.sparkContext.textFile("../data/sparkml2/chapter10/breast-cancer-wisconsin.data")
  1. 我们预处理数据集(详情请参阅前面的部分):
val data = rawData.map(_.trim)
 .filter(text => !(text.isEmpty || text.startsWith("#") || text.indexOf("?") > -1))
 .map { line =>
 val values = line.split(',').map(_.toDouble)
 val slicedValues = values.slice(1, values.size)
 val featureVector = Vectors.dense(slicedValues.init)
 val label = values.last / 2 -1
 LabeledPoint(label, featureVector)
 }
  1. 我们随机将整个数据集分为训练数据(70%)和测试数据(30%):
val splits = data.randomSplit(Array(0.7, 0.3))
val (trainingData, testData) = (splits(0), splits(1))
println("Training Data count:"+trainingData.count())
println("Test Data Count:"+testData.count())

您将在控制台上看到以下内容:

Training Data count:473
Test Data Count:210
  1. 我们定义一个度量计算函数,它利用 Spark 的RegressionMetrics
def getMetrics(model: RandomForestModel, data: RDD[LabeledPoint]): RegressionMetrics = {
val predictionsAndLabels = data.map(example =>
 (model.predict(example.features), example.label)
 )
new RegressionMetrics(predictionsAndLabels)
 }

  1. 我们设置以下参数:
val numClasses = 2
val categoricalFeaturesInfo = Map[Int, Int]()
val numTrees = 3 // Use more in practice.val featureSubsetStrategy = "auto" // Let the algorithm choose.val impurity = "variance"
 val maxDepth = 4
val maxBins = 32
val model = RandomForest.trainRegressor(trainingData, categoricalFeaturesInfo,
numTrees, featureSubsetStrategy, impurity, maxDepth, maxBins)
val metrics = getMetrics(model, testData)
println("Test Mean Squared Error = " + metrics.meanSquaredError)
println("My Random Forest model:\n" + model.toDebugString)

从控制台输出:

Test Mean Squared Error = 0.028681825568809653
My Random Forest model:
TreeEnsembleModel regressor with 3 trees
  Tree 0:
    If (feature 2 <= 3.0)
     If (feature 7 <= 3.0)
      If (feature 4 <= 5.0)
       If (feature 0 <= 8.0)
        Predict: 0.006825938566552901
       Else (feature 0 > 8.0)
        Predict: 1.0
      Else (feature 4 > 5.0)
       Predict: 1.0
     Else (feature 7 > 3.0)
      If (feature 6 <= 3.0)
       If (feature 0 <= 6.0)
        Predict: 0.0
       Else (feature 0 > 6.0)
        Predict: 1.0
      Else (feature 6 > 3.0)
       Predict: 1.0
    Else (feature 2 > 3.0)
     If (feature 5 <= 3.0)
      If (feature 4 <= 3.0)
       If (feature 7 <= 3.0)
        Predict: 0.1
       Else (feature 7 > 3.0)
        Predict: 1.0
      Else (feature 4 > 3.0)
       If (feature 3 <= 3.0)
        Predict: 0.8571428571428571
       Else (feature 3 > 3.0)
        Predict: 1.0
     Else (feature 5 > 3.0)
      If (feature 5 <= 5.0)
       If (feature 1 <= 4.0)
        Predict: 0.75
       Else (feature 1 > 4.0)
        Predict: 1.0
      Else (feature 5 > 5.0)
       Predict: 1.0
  Tree 1:
...
  1. 然后通过停止 Spark 会话来关闭程序:
spark.stop()

工作原理...

我们使用数据集和随机森林树来解决数据的回归问题。解析和分离的机制仍然相同,但我们使用以下两个 API 来进行树回归和评估结果:

  • RandomForest.trainRegressor()

  • RegressionMetrics()

值得注意的是定义getMetrics()函数以利用 Spark 中的RegressionMetrics()功能:

def getMetrics(model: RandomForestModel, data: RDD[LabeledPoint]): RegressionMetrics = {
val predictionsAndLabels = data.map(example =>
 (model.predict(example.features), example.label)
 )
new RegressionMetrics(predictionsAndLabels)
}

我们还将杂质值设置为“方差”,以便我们可以使用方差来测量错误:

val impurity = "variance" // use variance for regression

另请参阅

在 Spark 2.0 中构建使用梯度提升树(GBT)的分类系统

在这个步骤中,我们将探讨 Spark 中梯度提升树(GBT)分类的实现。GBT 在决定最终结果之前需要更多的超参数和多次尝试。必须记住,如果使用 GBT,完全可以种植较短的树。

如何做...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序所在的包位置:

package spark.ml.cookbook.chapter10
  1. 为 Spark 上下文导入必要的包:
import org.apache.spark.mllib.evaluation.MulticlassMetrics
import org.apache.spark.mllib.linalg.Vectors
import org.apache.spark.mllib.regression.LabeledPoint
import org.apache.spark.mllib.tree.model.GradientBoostedTreesModel
import org.apache.spark.rdd.RDD
import org.apache.spark.mllib.tree.GradientBoostedTrees
import org.apache.spark.mllib.tree.configuration.BoostingStrategy
import org.apache.spark.sql.SparkSession
import org.apache.log4j.{Level, Logger}

  1. 创建 Spark 的配置和 Spark 会话,以便我们可以访问集群:
Logger.getLogger("org").setLevel(Level.*ERROR*)

val spark = SparkSession
   .builder.master("local[*]")
   .appName("MyGradientBoostedTreesClassification")
   .config("spark.sql.warehouse.dir", ".")
   .getOrCreate()
  1. 我们读取原始的原始数据文件:
val rawData = spark.sparkContext.textFile("../data/sparkml2/chapter10/breast-cancer-wisconsin.data")
  1. 我们预处理数据集(详情请参阅前面的部分):
val data = rawData.map(_.trim)
 .filter(text => !(text.isEmpty || text.startsWith("#") || text.indexOf("?") > -1))
 .map { line =>
 val values = line.split(',').map(_.toDouble)
 val slicedValues = values.slice(1, values.size)
 val featureVector = Vectors.*dense*(slicedValues.init)
 val label = values.last / 2 -1
 LabeledPoint(label, featureVector)
 }
  1. 我们随机将整个数据集分为训练数据(70%)和测试数据(30%)。请注意,随机分割将生成大约 211 个测试数据集。这大约但并非完全是数据集的 30%:
val splits = data.randomSplit(Array(0.7, 0.3))
val (trainingData, testData) = (splits(0), splits(1))
println("Training Data count:"+trainingData.count())
println("Test Data Count:"+testData.count())

您将在控制台上看到:

Training Data count:491
Test Data Count:192
  1. 我们定义一个度量计算函数,它利用 Spark 的MulticlassMetrics
def getMetrics(model: GradientBoostedTreesModel, data: RDD[LabeledPoint]): MulticlassMetrics = {
 val predictionsAndLabels = data.map(example =>
 (model.predict(example.features), example.label)
 )
 new MulticlassMetrics(predictionsAndLabels)
 }
  1. 我们定义一个评估函数,该函数可以接受一些可调参数用于梯度提升树模型,并对数据集进行训练:
def evaluate(
 trainingData: RDD[LabeledPoint],
 testData: RDD[LabeledPoint],
 boostingStrategy : BoostingStrategy
 ) :Unit = {

 val model = GradientBoostedTrees.*train*(trainingData, boostingStrategy)

 val metrics = getMetrics(model, testData)
 println("Confusion Matrix :")
 println(metrics.confusionMatrix)
 println("Model Accuracy: "+metrics.*precision*)
 println("Model Error: "+ (1-metrics.*precision*))
 }
  1. 我们设置以下参数:
val algo = "Classification" val numIterations = 3
val numClasses = 2
val maxDepth = 5
val maxBins = 32
val categoricalFeatureInfo = *Map*[Int,Int]()
val boostingStrategy = BoostingStrategy.*defaultParams*(algo)
boostingStrategy.setNumIterations(numIterations)
boostingStrategy.treeStrategy.setNumClasses(numClasses) 
boostingStrategy.treeStrategy.setMaxDepth(maxDepth)
boostingStrategy.treeStrategy.setMaxBins(maxBins) boostingStrategy.treeStrategy.categoricalFeaturesInfo = categoricalFeatureInfo
  1. 我们使用前面的策略参数评估模型:
evaluate(trainingData, testData, boostingStrategy)

从控制台输出:

Confusion Matrix :
124.0 2.0
2.0 64.0
Model Accuracy: 0.9791666666666666
Model Error: 0.02083333333333337

To interpret the above Confusion metrics, Accuracy is equal to (124+ 64)/ 192 all test cases, and error is equal to 1 -accuracy
  1. 然后通过停止 Spark 会话来关闭程序:
spark.stop()

工作原理...

我们跳过数据摄取和解析,因为这与之前的步骤类似,但不同的是我们如何设置参数,特别是将“classification”作为参数传递给BoostingStrategy.defaultParams()

val algo = "Classification"
 val numIterations = 3
 val numClasses = 2
 val maxDepth = 5
 val maxBins = 32
 val categoricalFeatureInfo = Map[Int,Int]()

 val boostingStrategy = BoostingStrategy.*defaultParams*(algo)

我们还使用evaluate()函数通过查看不纯度和混淆矩阵来评估参数:

evaluate(trainingData, testData, boostingStrategy)
Confusion Matrix :
124.0 2.0
2.0 64.0
Model Accuracy: 0.9791666666666666
Model Error: 0.02083333333333337

还有更多...

重要的是要记住 GBT 是一个多代算法,我们一次生长一棵树,从错误中学习,然后以迭代的方式构建下一棵树。

另请参阅

在 Spark 2.0 中使用 Gradient Boosted Trees(GBT)解决回归问题

这个示例与 GBT 分类问题类似,但我们将使用回归。我们将使用BoostingStrategy.defaultParams()来指示 GBT 使用回归:

algo = "Regression" val boostingStrategy = BoostingStrategy.defaultParams(algo)

如何做...

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序所在的包位置:

package spark.ml.cookbook.chapter10

  1. 导入 Spark 上下文所需的包:
import org.apache.spark.mllib.evaluation.RegressionMetrics
import org.apache.spark.mllib.linalg.Vectors
import org.apache.spark.mllib.regression.LabeledPoint
import org.apache.spark.mllib.tree.model.GradientBoostedTreesModel
import org.apache.spark.rdd.RDD
import org.apache.spark.mllib.tree.GradientBoostedTrees
import org.apache.spark.mllib.tree.configuration.BoostingStrategy

import org.apache.spark.sql.SparkSession
import org.apache.log4j.{Level, Logger}

  1. 创建 Spark 的配置和 Spark 会话:
Logger.getLogger("org").setLevel(Level.ERROR)

val spark = SparkSession
   .builder   .master("local[*]")
   .appName("MyGradientBoostedTreesRegression")
   .config("spark.sql.warehouse.dir", ".")
   .getOrCreate()
  1. 我们在原始的原始数据文件中阅读:
val rawData = spark.sparkContext.textFile("../data/sparkml2/chapter10/breast-cancer-wisconsin.data")
  1. 我们对数据集进行预处理(有关详细信息,请参见前面的会话):
val data = rawData.map(_.trim)
 .filter(text => !(text.isEmpty || text.startsWith("#") || text.indexOf("?") > -1))
 .map { line =>
 val values = line.split(',').map(_.toDouble)
 val slicedValues = values.slice(1, values.size)
 val featureVector = Vectors.*dense*(slicedValues.init)
 val label = values.last / 2 -1
 *LabeledPoint*(label, featureVector)
 }
  1. 我们将整个数据集随机分为训练数据(70%)和测试数据(30%):
val splits = data.randomSplit(Array(0.7, 0.3))
val (trainingData, testData) = (splits(0), splits(1))
println("Training Data count:"+trainingData.count())
println("Test Data Count:"+testData.count())

您将在控制台中看到以下内容:

Training Data count:469
Test Data Count:214
  1. 我们定义一个度量计算函数,它利用 Spark 的RegressionMetrics
def getMetrics(model: GradientBoostedTreesModel, data: RDD[LabeledPoint]): RegressionMetrics = {
 val predictionsAndLabels = data.map(example =>
 (model.predict(example.features), example.label)
 )
 new RegressionMetrics(predictionsAndLabels)
 }
  1. 我们设置以下参数:
val algo = "Regression" val numIterations = 3
val maxDepth = 5
val maxBins = 32
val categoricalFeatureInfo = Map[Int,Int]()
val boostingStrategy = BoostingStrategy.defaultParams(algo)
boostingStrategy.setNumIterations(numIterations)
boostingStrategy.treeStrategy.setMaxDepth(maxDepth) 
boostingStrategy.treeStrategy.setMaxBins(maxBins) boostingStrategy.treeStrategy.categoricalFeaturesInfo = categoricalFeatureInfo
  1. 我们使用前面的策略参数评估模型:
val model = GradientBoostedTrees.train(trainingData, boostingStrategy)
val metrics = getMetrics(model, testData)

 println("Test Mean Squared Error = " + metrics.meanSquaredError)
 println("My regression GBT model:\n" + model.toDebugString)

从控制台输出:

Test Mean Squared Error = 0.05370763765769276
My regression GBT model:
TreeEnsembleModel regressor with 3 trees
Tree 0:
If (feature 1 <= 2.0)
If (feature 0 <= 6.0)
If (feature 5 <= 5.0)
If (feature 5 <= 4.0)
Predict: 0.0
Else (feature 5 > 4.0)
...
  1. 然后我们通过停止 Spark 会话来关闭程序:
spark.stop()

它是如何工作的...

我们使用与上一个示例相同的 GBT 树,但我们调整了参数,以指示 GBT API 执行回归而不是分类。值得注意的是将以下代码与上一个示例进行比较。 "回归"用于指示 GBT 对数据执行回归:

 val algo = "Regression"
 val numIterations = 3
 val maxDepth = 5
 val maxBins = 32
 val categoricalFeatureInfo = *Map*[Int,Int]()

 val boostingStrategy = BoostingStrategy.*defaultParams*(algo)

我们使用以下 API 来训练和评估模型的指标:

  • GradientBoostedTrees.train()

  • getMetrics()

以下代码片段显示了检查模型所需的典型输出:

Test Mean Squared Error = 0.05370763765769276
My regression GBT model:
Tree 0:
If (feature 1 <= 2.0)
If (feature 0 <= 6.0)
If (feature 5 <= 5.0)
If (feature 5 <= 4.0)
Predict: 0.0
Else (feature 5 > 4.0)
...

还有更多...

GBT 可以像随机森林一样捕捉非线性和变量交互,并且可以处理多类标签。

另请参阅

第十一章:大数据中的高维度诅咒

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

  • 在 Spark 中摄取和准备 CSV 文件进行处理的两种方法

  • 奇异值分解SVD)以减少 Spark 中的高维度

  • 主成分分析PCA)在 Spark 中为机器学习选择最有效的潜在因素

介绍

维度诅咒并不是一个新的术语或概念。这个术语最初是由 R.贝尔曼在解决动态规划问题(贝尔曼方程)时创造的。机器学习中的核心概念指的是,随着我们增加维度(轴或特征)的数量,训练数据(样本)的数量保持不变(或相对较低),这导致我们的预测准确性降低。这种现象也被称为休斯效应,以 G.休斯的名字命名,它讨论了随着我们在问题空间中引入越来越多的维度,搜索空间的迅速(指数级)增加所导致的问题。这有点违直觉,但如果样本数量的增长速度不如添加更多维度的速度,实际上你最终会得到一个更不准确的模型!

总的来说,大多数机器学习算法本质上是统计学的,它们试图通过在训练期间切割空间并对每个子空间中每个类别的数量进行某种计数来学习目标空间的属性。维度诅咒是由于随着维度的增加,能够帮助算法区分和学习的数据样本变得越来越少。一般来说,如果我们在一个密集的D维度中有N个样本,那么我们需要*(N)^D*个样本来保持样本密度恒定。

例如,假设你有 10 个患者数据集,这些数据集是沿着两个维度(身高、体重)进行测量的。这导致了一个二维平面上的 10 个数据点。如果我们开始引入其他维度,比如地区、卡路里摄入量、种族、收入等,会发生什么?在这种情况下,我们仍然有 10 个观察点(10 个患者),但是在一个更大的六维空间中。当新的维度被引入时,样本数据(用于训练)无法呈指数级增长,这就是所谓的维度诅咒

让我们看一个图形示例来展示搜索空间与数据样本的增长。下图描述了一组五个数据点,这些数据点在 5 x 5(25 个单元格)中被测量。当我们添加另一个维度时,预测准确性会发生什么变化?我们仍然有五个数据点在 125 个 3D 单元格中,这导致了大量稀疏子空间,这些子空间无法帮助机器学习算法更好地学习(区分),因此导致了更低的准确性:

我们的目标应该是努力朝着一个接近最佳特征或维度的数量,而不是不断添加更多特征(最大特征或维度)。毕竟,如果我们只是不断添加更多特征或维度,难道我们不应该有更好的分类错误吗?起初这似乎是个好主意,但在大多数情况下答案是“不”,除非你能指数级增加样本,而这在几乎所有情况下都是不切实际的也几乎不可能的。

让我们看一下下图,它描述了学习错误与特征总数的关系:

在前一节中,我们研究了维度诅咒背后的核心概念,但我们还没有讨论它的其他副作用或如何处理诅咒本身。正如我们之前所看到的,与普遍观念相反,问题不在于维度本身,而在于样本与搜索空间的比率的减少,随之而来的是更不准确的预测。

想象一个简单的 ML 系统,如下图所示。这里显示的 ML 系统使用 MNIST(yann.lecun.com/exdb/mnist/)类型的手写数据集,并希望对自己进行训练,以便能够预测包裹上使用的六位邮政编码是什么:

来源:MNIST

即使 MNIST 数据是 20 x 20,为了使问题更加明显,让我们假设每个数字有一个 40 x 40 像素的补丁需要存储、分析,然后用于未来的预测。如果我们假设是黑/白,那么“表观”维度是两个(40 x 40)或 21,600,这是很大的。接下来应该问的问题是:给定数据的 21,600 个表观维度,我们需要多少实际维度来完成我们的工作?如果我们看一下从 40 x 40 补丁中抽取的所有可能样本,有多少实际上是在寻找数字?一旦我们仔细看一下这个问题,我们会发现“实际”维度(即限制在一个较小的流形子空间中,这是笔画用来制作数字的空间。实际上,实际子空间要小得多,而且不是随机分布在 40 x 40 的补丁上)实际上要小得多!这里发生的情况是,实际数据(人类绘制的数字)存在于更小的维度中,很可能局限于子空间中的一小组流形(即,数据存在于某个子空间周围)。为了更好地理解这一点,从 40 x 40 的补丁中随机抽取 1,000 个样本,并直观地检查这些样本。有多少样本实际上看起来像 3、6 或 5?

当我们增加维度时,我们可能会无意中增加错误率,因为由于没有足够的样本来准确预测,或者由于测量本身引入了噪声,系统可能会引入噪声。增加更多维度的常见问题如下:

  • 更长的计算时间

  • 增加噪声

  • 需要更多样本以保持相同的学习/预测速率

  • 由于稀疏空间中缺乏可操作样本而导致数据过拟合

图片展示可以帮助我们理解“表观维度”与“实际维度”的差异,以及在这种情况下“少即是多”的原因:

我们希望减少维度的原因可以表达为:

  • 更好地可视化数据

  • 压缩数据并减少存储需求

  • 增加信噪比

  • 实现更快的运行时间

特征选择与特征提取

我们有两个选择,特征选择和特征提取,可以用来将维度减少到一个更易管理的空间。这些技术各自是一个独立的学科,有自己的方法和复杂性。尽管它们听起来相同,但它们是非常不同的,需要单独处理。

下图提供了一个思维导图,比较了特征选择和特征提取。虽然特征选择,也称为特征工程,超出了本书的范围,但我们通过详细的配方介绍了两种最常见的特征提取技术(PCA 和 SVD):

用于选择 ML 算法的一组特征或输入的两种可用技术是:

  • 特征选择:在这种技术中,我们利用我们的领域知识选择最能描述数据方差的特征子集。我们试图做的是选择最能帮助我们预测结果的最佳因变量(特征)。这种方法通常被称为“特征工程”,需要数据工程师或领域专业知识才能有效。

例如,我们可能会查看为物流分类器提出的 200 个独立变量(维度、特征),以预测芝加哥市的房屋是否会出售。在与在芝加哥市购买/销售房屋有 20 多年经验的房地产专家交谈后,我们发现最初提出的 200 个维度中只有 4 个是足够的,例如卧室数量、价格、总平方英尺面积和学校质量。虽然这很好,但通常非常昂贵、耗时,并且需要领域专家来分析和提供指导。

  • 特征提取:这是指一种更算法化的方法,使用映射函数将高维数据映射到低维空间。例如,将三维空间(例如,身高、体重、眼睛颜色)映射到一维空间(例如,潜在因素),可以捕捉数据集中几乎所有的变化。

我们在这里尝试的是提出一组潜在因素,这些因素是原始因素的组合(通常是线性的),可以以准确的方式捕捉和解释数据。例如,我们使用单词来描述文档,通常以 10⁶到 10⁹的空间结束,但是用主题(例如,浪漫、战争、和平、科学、艺术等)来描述文档会更抽象和高层次,这不是很好吗?我们真的需要查看或包含每个单词来更好地进行文本分析吗?以什么代价?

特征提取是一种从“表观维度”到“实际维度”映射的降维算法方法。

两种在 Spark 中摄取和准备 CSV 文件进行处理的方法

在这个示例中,我们探讨了读取、解析和准备 CSV 文件用于典型的 ML 程序。逗号分隔值CSV)文件通常将表格数据(数字和文本)存储在纯文本文件中。在典型的 CSV 文件中,每一行都是一个数据记录,大多数情况下,第一行也被称为标题行,其中存储了字段的标识符(更常见的是字段的列名)。每个记录由一个或多个字段组成,字段之间用逗号分隔。

如何做...

  1. 示例 CSV 数据文件来自电影评分。该文件可在files.grouplens.org/datasets/movielens/ml-latest-small.zip中获取。

  2. 文件提取后,我们将使用ratings.csv文件来加载数据到 Spark 中。CSV 文件将如下所示:

userId movieId rating timestamp
1 16 4 1217897793
1 24 1.5 1217895807
1 32 4 1217896246
1 47 4 1217896556
1 50 4 1217896523
1 110 4 1217896150
1 150 3 1217895940
1 161 4 1217897864
1 165 3 1217897135
1 204 0.5 1217895786
... ... ... ...
  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序所在的包位置:

package spark.ml.cookbook.chapter11

  1. 导入 Spark 所需的包,以便访问集群和Log4j.Logger以减少 Spark 产生的输出量:
import org.apache.log4j.{Level, Logger}
import org.apache.spark.sql.SparkSession
  1. 创建 Spark 的配置和 Spark 会话,以便我们可以访问集群:
Logger.getLogger("org").setLevel(Level.ERROR)

 val spark = SparkSession
 .builder
.master("local[*]")
 .appName("MyCSV")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()
  1. 我们将 CSV 文件读入为文本文件:
// 1\. load the csv file as text file
val dataFile = "../data/sparkml2/chapter11/ratings.csv"
val file = spark.sparkContext.textFile(dataFile)
  1. 我们处理数据集:
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)
 val result = maps.take(10)
 result.foreach(println)

这里应该提到,split函数仅用于演示目的,生产中应该使用更健壮的标记技术。

  1. 首先,我们修剪行,删除任何空格,并将 CSV 文件加载到headerAndData RDD 中,因为ratings.csv确实有标题行。

  2. 然后我们将第一行读取为标题,将其余数据读入数据 RDD 中。任何进一步的计算都可以使用数据 RDD 来执行机器学习算法。为了演示目的,我们将标题行映射到数据 RDD 并打印出前 10 行。

在应用程序控制台中,您将看到以下内容:

Map(userId -> 1, movieId -> 16, rating -> 4.0, timestamp -> 1217897793)
Map(userId -> 1, movieId -> 24, rating -> 1.5, timestamp -> 1217895807)
Map(userId -> 1, movieId -> 32, rating -> 4.0, timestamp -> 1217896246)
Map(userId -> 1, movieId -> 47, rating -> 4.0, timestamp -> 1217896556)
Map(userId -> 1, movieId -> 50, rating -> 4.0, timestamp -> 1217896523)
Map(userId -> 1, movieId -> 110, rating -> 4.0, timestamp -> 1217896150)
Map(userId -> 1, movieId -> 150, rating -> 3.0, timestamp -> 1217895940)
Map(userId -> 1, movieId -> 161, rating -> 4.0, timestamp -> 1217897864)
Map(userId -> 1, movieId -> 165, rating -> 3.0, timestamp -> 1217897135)
Map(userId -> 1, movieId -> 204, rating -> 0.5, timestamp -> 1217895786)
  1. 还有另一种选项可以使用 Spark-CSV 包将 CSV 文件加载到 Spark 中。

要使用此功能,您需要下载以下 JAR 文件并将它们放在类路径上:repo1.maven.org/maven2/com/databricks/spark-csv_2.10/1.4.0/spark-csv_2.10-1.4.0.jar

由于 Spark-CSV 包也依赖于common-csv,您需要从以下位置获取common-csv JAR 文件:commons.apache.org/proper/commons-csv/download_csv.cgi

我们获取common-csv-1.4-bin.zip并提取commons-csv-1.4.jar,然后将前两个 jar 放在类路径上。

  1. 我们使用 Databricks 的spark-csv包加载 CSV 文件,使用以下代码。成功加载 CSV 文件后,它将创建一个 DataFrame 对象:
// 2\. load the csv file using databricks package
val df = spark.read.format("com.databricks.spark.csv").option("header", "true").load(dataFile)
  1. 我们从 DataFrame 中注册一个名为ratings的临时内存视图:
df.createOrReplaceTempView("ratings")
 val resDF = spark.sql("select * from ratings")
 resDF.show(10, false)

然后我们对表使用 SQL 查询并显示 10 行。在控制台上,您将看到以下内容:

  1. 进一步的机器学习算法可以在之前创建的 DataFrame 上执行。

  2. 然后我们通过停止 Spark 会话来关闭程序:

spark.stop()

工作原理...

在旧版本的 Spark 中,我们需要使用特殊包来读取 CSV,但现在我们可以利用spark.sparkContext.textFile(dataFile)来摄取文件。开始该语句的Spark是 Spark 会话(集群句柄),可以在创建阶段通过任何您喜欢的名称来命名,如下所示:

val spark = SparkSession
 .builder
.master("local[*]")
 .appName("MyCSV")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()
spark.sparkContext.textFile(dataFile)
spark.sparkContext.textFile(dataFile)

Spark 2.0+使用spark.sql.warehouse.dir来设置存储表的仓库位置,而不是hive.metastore.warehouse.dirspark.sql.warehouse.dir的默认值是System.getProperty("user.dir")

另请参阅spark-defaults.conf以获取更多详细信息。

在以后的工作中,我们更喜欢这种方法,而不是按照本示例的第 9 步和第 10 步所解释的获取特殊包和依赖 JAR 的方法:

spark.read.format("com.databricks.spark.csv").option("header", "true").load(dataFile)

这演示了如何使用文件。

还有更多...

CSV 文件格式有很多变化。用逗号分隔字段的基本思想是清晰的,但它也可以是制表符或其他特殊字符。有时甚至标题行是可选的。

由于其可移植性和简单性,CSV 文件广泛用于存储原始数据。它可以在不同的应用程序之间进行移植。我们将介绍两种简单而典型的方法来将样本 CSV 文件加载到 Spark 中,并且可以很容易地修改以适应您的用例。

另请参阅

使用 Singular Value Decomposition(SVD)在 Spark 中降低高维度

在这个示例中,我们将探讨一种直接来自线性代数的降维方法,称为SVD奇异值分解)。这里的重点是提出一组低秩矩阵(通常是三个),它们可以近似原始矩阵,但数据量要少得多,而不是选择使用大型M乘以N矩阵。

SVD 是一种简单的线性代数技术,它将原始数据转换为特征向量/特征值低秩矩阵,可以捕捉大部分属性(原始维度)在一个更有效的低秩矩阵系统中。

以下图示了 SVD 如何用于降低维度,然后使用 S 矩阵来保留或消除从原始数据派生的更高级概念(即,具有比原始数据更少列/特征的低秩矩阵):

如何做...

  1. 我们将使用电影评分数据进行 SVD 分析。MovieLens 1M 数据集包含大约 100 万条记录,由 6000 个 MovieLens 用户对约 3900 部电影的匿名评分组成。

数据集可以在以下位置检索:files.grouplens.org/datasets/movielens/ml-1m.zip

数据集包含以下文件:

  • ratings.dat:包含用户 ID、电影 ID、评分和时间戳

  • movies.dat:包含电影 ID、标题和类型

  • users.dat:包含用户 ID、性别、年龄、职业和邮政编码

  1. 我们将使用ratings.dat进行 SVD 分析。ratings.dat的样本数据如下:
1::1193::5::978300760
1::661::3::978302109
1::914::3::978301968
1::3408::4::978300275
1::2355::5::978824291
1::1197::3::978302268
1::1287::5::978302039
1::2804::5::978300719
1::594::4::978302268
1::919::4::978301368
1::595::5::978824268
1::938::4::978301752

我们将使用以下程序将数据转换为评分矩阵,并将其适应 SVD 算法模型(在本例中,总共有 3953 列):

电影 1 电影 2 电影... 电影 3953
用户 1 1 4 - 3
用户 2 5 - 2 1
用户... - 3 - 2
用户 N 2 4 - 5
  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序所在的包位置:

package spark.ml.cookbook.chapter11

  1. 导入 Spark 会话所需的包:
import org.apache.log4j.{Level, Logger}
import org.apache.spark.mllib.linalg.distributed.RowMatrix
import org.apache.spark.mllib.linalg.Vectors
import org.apache.spark.sql.SparkSession
  1. 创建 Spark 的配置和 Spark 会话,以便我们可以访问集群:
Logger.getLogger("org").setLevel(Level.ERROR)

val spark = SparkSession
.builder
.master("local[*]")
.appName("MySVD")
.config("spark.sql.warehouse.dir", ".")
.getOrCreate()    

  1. 我们读取原始的原始数据文件:
val dataFile = "../data/sparkml2/chapter11/ratings.dat" //read data file in as a RDD, partition RDD across <partitions> cores
val data = spark.sparkContext.textFile(dataFile)
  1. 我们预处理数据集:
//parse data and create (user, item, rating) tuplesval ratingsRDD = data
   .map(line => line.split("::"))
   .map(fields => (fields(0).toInt, fields(1).toInt, fields(2).toDouble))

由于我们对评分更感兴趣,我们从数据文件中提取userIdmovieId和评分值,即fields(0)fields(1)fields(2),并基于记录创建一个评分 RDD。

  1. 然后我们找出评分数据中有多少部电影,并计算最大电影索引:
val items = ratingsRDD.map(x => x._2).distinct()
val maxIndex = items.max + 1

总共,我们根据数据集得到 3953 部电影。

  1. 我们将所有用户的电影项目评分放在一起,使用 RDD 的groupByKey函数,所以单个用户的电影评分被分组在一起:
val userItemRatings = ratingsRDD.map(x => (x._1, ( x._2, x._3))).groupByKey().cache()
 userItemRatings.take(2).foreach(println)

然后我们打印出前两条记录以查看集合。由于我们可能有一个大型数据集,我们缓存 RDD 以提高性能。

在控制台中,您将看到以下内容:

(4904,CompactBuffer((2054,4.0), (588,4.0), (589,5.0), (3000,5.0), (1,5.0), ..., (3788,5.0)))
(1084,CompactBuffer((2058,3.0), (1258,4.0), (588,2.0), (589,4.0), (1,3.0), ..., (1242,4.0)))

在上述记录中,用户 ID 为4904。对于电影 ID2054,评分为4.0,电影 ID 为588,评分为4,依此类推。

  1. 然后我们创建一个稀疏向量来存储数据:
val sparseVectorData = userItemRatings
 .map(a=>(a._1.toLong, Vectors.sparse(maxIndex,a._2.toSeq))).sortByKey()

 sparseVectorData.take(2).foreach(println)

然后我们将数据转换为更有用的格式。我们使用userID作为键(排序),并创建一个稀疏向量来存储电影评分数据。

在控制台中,您将看到以下内容:

(1,(3953,[1,48,150,260,527,531,588,...], [5.0,5.0,5.0,4.0,5.0,4.0,4.0...]))
(2,(3953,[21,95,110,163,165,235,265,...],[1.0,2.0,5.0,4.0,3.0,3.0,4.0,...]))

在上述打印输出中,对于用户1,总共有3,953部电影。对于电影 ID1,评分为5.0。稀疏向量包含一个movieID数组和一个评分值数组。

  1. 我们只需要评分矩阵进行 SVD 分析:
val rows = sparseVectorData.map{
 a=> a._2
 }

上述代码将提取稀疏向量部分并创建一个行 RDD。

  1. 然后我们基于 RDD 创建一个 RowMatrix。一旦创建了 RowMatrix 对象,我们就可以调用 Spark 的computeSVD函数来计算矩阵的 SVD:
val mat = new RowMatrix(rows)
val col = 10 //number of leading singular values
val computeU = true
val svd = mat.computeSVD(col, computeU)
  1. 上述参数也可以调整以适应我们的需求。一旦我们计算出 SVD,就可以获取模型数据。

  2. 我们打印出奇异值:

println("Singular values are " + svd.s)
println("V:" + svd.V)

您将在控制台上看到以下输出:

  1. 从 Spark Master(http://localhost:4040/jobs/)中,您应该看到如下截图所示的跟踪:

  1. 然后我们通过停止 Spark 会话来关闭程序:
spark.stop()

它是如何工作的...

工作的核心是声明一个RowMatrix(),然后调用computeSVD()方法将矩阵分解为更小的子组件,但以惊人的准确度近似原始矩阵:

valmat = new RowMatrix(rows)
val col = 10 //number of leading singular values
val computeU = true
val svd = mat.computeSVD(col, computeU)

SVD 是一个用于实数或复数矩阵的因式分解技术。在其核心,它是一种直接的线性代数,实际上是从 PCA 中导出的。这个概念在推荐系统(ALS,SVD),主题建模(LDA)和文本分析中被广泛使用,以从原始的高维矩阵中推导出概念。让我们尝试概述这个降维的方案及其数据集(MovieLens)与 SVD 分解的关系,而不深入讨论 SVD 分解中的数学细节。以下图表描述了这个降维方案及其数据集(MovieLens)与 SVD 分解的关系:

还有更多...

我们将得到基于原始数据集的更高效(低秩)的矩阵。

以下方程描述了一个m x n数组的分解,这个数组很大,很难处理。方程的右边帮助解决了分解问题,这是 SVD 技术的基础。

以下步骤逐步提供了 SVD 分解的具体示例:

  • 考虑一个 1,000 x 1,000 的矩阵,提供 1,000,000 个数据点(M=用户,N=电影)。

  • 假设有 1,000 行(观测数量)和 1,000 列(电影数量)。

  • 假设我们使用 Spark 的 SVD 方法将 A 分解为三个新矩阵。

  • 矩阵U [m x r]有 1,000 行,但现在只有 5 列(r=5r可以被看作是概念)

  • 矩阵S [r x r]保存了奇异值,它们是每个概念的强度(只对对角线感兴趣)

  • 矩阵V [n x r]具有右奇异值向量(n=电影r=概念,如浪漫,科幻等)

  • 假设在分解后,我们得到了五个概念(浪漫,科幻剧,外国,纪录片和冒险)

  • 低秩如何帮助?

  • 最初我们有 1,000,000 个兴趣点

  • 在 SVD 之后,甚至在我们开始使用奇异值(矩阵 S 的对角线)选择我们想要保留的内容之前,我们得到了总的兴趣点数= U(1,000 x 5)+ S(5 x 5)+ V(1,000 x 5)

  • 现在我们不再使用 1 百万个数据点(矩阵 A,即 1,000 x 1,000),而是有了 5,000+25+5,000,大约有 10,000 个数据点,这要少得多

  • 选择奇异值的行为允许我们决定我们想要保留多少,以及我们想要丢弃多少(你真的想向用户展示最低的 900 部电影推荐吗?这有价值吗?)

另见

主成分分析(PCA)用于在 Spark 中为机器学习选择最有效的潜在因子

在这个方案中,我们使用PCA(主成分分析)将高维数据(表面维度)映射到低维空间(实际维度)。难以置信,但 PCA 的根源早在 1901 年(参见 K. Pearson 的著作)和 1930 年代由 H. Hotelling 独立提出。

PCA 试图以最大化垂直轴上的方差的方式选择新的组件,并有效地将高维原始特征转换为一个具有派生组件的低维空间,这些组件可以以更简洁的形式解释变化(区分类别)。

PCA 背后的直觉如下图所示。现在假设我们的数据有两个维度(x,y),我们要问的问题是,大部分变化(和区分)是否可以用一个维度或更准确地说是原始特征的线性组合来解释:

如何做...

  1. 克利夫兰心脏病数据库是机器学习研究人员使用的一个已发布的数据集。该数据集包含十几个字段,对克利夫兰数据库的实验主要集中在试图简单地区分疾病的存在(值 1,2,3)和不存在(值 0)(在目标列,第 14 列)。

  2. 克利夫兰心脏病数据集可在archive.ics.uci.edu/ml/machine-learning-databases/heart-disease/processed.cleveland.data找到。

  3. 数据集包含以下属性(年龄,性别,cp,trestbps,chol,fbs,restecg,thalach,exang,oldpeak,slope,ca,thal,num),如下表的标题所示:

有关各个属性的详细解释,请参阅:archive.ics.uci.edu/ml/datasets/Heart+Disease

  1. 数据集将如下所示:
age sex cp trestbps chol fbs restecg thalach exang oldpeak slope ca thal num
63 1 1 145 233 1 2 150 0 2.3 3 0 6 0
67 1 4 160 286 0 2 108 1 1.5 2 3 3 2
67 1 4 120 229 0 2 129 1 2.6 2 2 7 1
37 1 3 130 250 0 0 187 0 3.5 3 0 3 0
41 0 2 130 204 0 2 172 0 1.4 1 0 3 0
56 1 2 120 236 0 0 178 0 0.8 1 0 3 0
62 0 4 140 268 0 2 160 0 3.6 3 2 3 3
57 0 4 120 354 0 0 163 1 0.6 1 0 3 0
63 1 4 130 254 0 2 147 0 1.4 2 1 7 2
53 1 4 140 203 1 2 155 1 3.1 3 0 7 1
57 1 4 140 192 0 0 148 0 0.4 2 0 6 0
56 0 2 140 294 0 2 153 0 1.3 2 0 3 0
56 1 3 130 256 1 2 142 1 0.6 2 1 6 2
44 1 2 120 263 0 0 173 0 0 1 0 7 0
52 1 3 172 199 1 0 162 0 0.5 1 0 7 0
57 1 3 150 168 0 0 174 0 1.6 1 0 3 0
... ... ... ... ... ... ... ... ... ... ... ... ... ...
  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序所在的包位置:

package spark.ml.cookbook.chapter11.

  1. 导入 Spark 会话所需的包:
import org.apache.log4j.{Level, Logger}
import org.apache.spark.ml.feature.PCA
import org.apache.spark.ml.linalg.Vectors
import org.apache.spark.sql.SparkSession
  1. 创建 Spark 的配置和 Spark 会话,以便我们可以访问集群:
Logger.getLogger("org").setLevel(Level.ERROR)
val spark = SparkSession
.builder
.master("local[*]")
.appName("MyPCA")
.config("spark.sql.warehouse.dir", ".")
.getOrCreate()
  1. 我们读取原始数据文件并计算原始数据:
val dataFile = "../data/sparkml2/chapter11/processed.cleveland.data"
val rawdata = spark.sparkContext.textFile(dataFile).map(_.trim)
println(rawdata.count())

在控制台中,我们得到以下内容:

303
  1. 我们对数据集进行预处理(详细信息请参见前面的代码):
val data = rawdata.filter(text => !(text.isEmpty || text.indexOf("?") > -1))
 .map { line =>
 val values = line.split(',').map(_.toDouble)

 Vectors.dense(values)
 }

 println(data.count())

data.take(2).foreach(println)

在前面的代码中,我们过滤了缺失的数据记录,并使用 Spark DenseVector 来托管数据。在过滤缺失数据后,我们在控制台中得到以下数据计数:

297

记录打印,2,将如下所示:

[63.0,1.0,1.0,145.0,233.0,1.0,2.0,150.0,0.0,2.3,3.0,0.0,6.0,0.0]
[67.0,1.0,4.0,160.0,286.0,0.0,2.0,108.0,1.0,1.5,2.0,3.0,3.0,2.0]
  1. 我们从数据 RDD 创建一个 DataFrame,并创建一个用于计算的 PCA 对象:
val df = sqlContext.createDataFrame(data.map(Tuple1.apply)).toDF("features")
val pca = new PCA()
.setInputCol("features")
.setOutputCol("pcaFeatures")
.setK(4)
.fit(df)
  1. PCA 模型的参数如前面的代码所示。我们将K值设置为4K代表在完成降维算法后我们感兴趣的前 K 个主成分的数量。

  2. 另一种选择也可以通过矩阵 API 实现:mat.computePrincipalComponents(4)。在这种情况下,4代表了在完成降维后的前 K 个主成分。

  3. 我们使用 transform 函数进行计算,并在控制台中显示结果:

val pcaDF = pca.transform(df)
val result = pcaDF.select("pcaFeatures")
result.show(false)

以下内容将显示在控制台上。

您所看到的是四个新的 PCA 组件(PC1、PC2、PC3 和 PC4),可以替代原始的 14 个特征。我们已经成功地将高维空间(14 个维度)映射到了一个低维空间(四个维度):

  1. 从 Spark Master(http://localhost:4040/jobs)中,您还可以跟踪作业,如下图所示:

  1. 然后通过停止 Spark 会话来关闭程序:
spark.stop()

工作原理...

在加载和处理数据之后,通过以下代码完成了 PCA 的核心工作:

val pca = new PCA()
 .setInputCol("features")
 .setOutputCol("pcaFeatures")
 .setK(4)
 .fit(df)

PCA()调用允许我们选择需要多少个组件(setK(4))。在这个配方的情况下,我们选择了前四个组件。

目标是从原始的高维数据中找到一个较低维度的空间(降低的 PCA 空间),同时保留结构属性(沿主成分轴的数据方差),以便最大限度地区分带标签的数据,而无需原始的高维空间要求。

下图显示了一个样本 PCA 图表。在降维后,它将看起来像下面这样--在这种情况下,我们可以很容易地看到大部分方差由前四个主成分解释。如果您快速检查图表(红线),您会看到第四个组件后方差如何迅速消失。这种膝盖图(方差与组件数量的关系)帮助我们快速选择所需的组件数量(在这种情况下,四个组件)来解释大部分方差。总之,几乎所有的方差(绿线)可以累积地归因于前四个组件,因为它几乎达到了 1.0,同时可以通过红线追踪每个单独组件的贡献量:

上面的图表是“凯撒法则”的描述,这是选择组件数量最常用的方法。要生成图表,可以使用 R 来绘制特征值与主成分的关系,或者使用 Python 编写自己的代码。

请参见密苏里大学的以下链接以在 R 中绘制图表:

web.missouri.edu/~huangf/data/mvnotes/Documents/pca_in_r_2.html

如前所述,图表与凯撒法则有关,凯撒法则指出在特定主成分中加载的更多相关变量,该因子在总结数据方面就越重要。在这种情况下,特征值可以被认为是一种衡量组件在总结数据方面的好坏的指标(在最大方差方向上)。

使用 PCA 类似于其他方法,我们试图学习数据的分布。我们仍然需要每个属性的平均值和 K(要保留的组件数量),这只是一个估计的协方差。简而言之,降维发生是因为我们忽略了具有最小方差的方向(PCA 组件)。请记住,PCA 可能很困难,但您可以控制发生的事情以及保留多少(使用膝盖图表来选择 K 或要保留的组件数量)。

有两种计算 PCA 的方法:

  • 协方差方法

  • 奇异值分解SVD

我们将在这里概述协方差矩阵方法(直接特征向量和特征值加上居中),但是请随时参考 SVD 配方(Singular Value Decomposition(SVD)在 Spark 中减少高维度)以了解 SVD 与 PCA 的内部工作原理。

用协方差矩阵方法进行 PCA 算法,简而言之,涉及以下内容:

  1. 给定一个 N 乘以 M 的矩阵:

  2. N = 训练数据的总数

  3. M 是特定的维度(或特征)

  4. M x N 的交集是一个带有样本值的调用

  5. 计算平均值:

  1. 通过从每个观察中减去平均值来对数据进行中心化(标准化):

  2. 构建协方差矩阵:

  3. 计算协方差矩阵的特征向量和特征值(这很简单,但要记住并非所有矩阵都可以分解)。

  4. 选择具有最大特征值的特征向量。

  5. 特征值越大,对组件的方差贡献越大。

还有更多...

使用 PCA 在这个案例中的净结果是,原始的 14 维搜索空间(也就是说 14 个特征)被减少到解释原始数据集中几乎所有变化的 4 个维度。

PCA 并不纯粹是一个机器学习概念,在机器学习运动之前,它在金融领域已经使用了很多年。在本质上,PCA 使用正交变换(每个组件都与其他组件垂直)将原始特征(明显的维度)映射到一组新推导的维度,以便删除大部分冗余和共线性属性。推导的(实际的潜在维度)组件是原始属性的线性组合。

虽然使用 RDD 从头开始编程 PCA 很容易,但学习它的最佳方法是尝试使用神经网络实现 PCA,并查看中间结果。您可以在 Café(在 Spark 上)中进行此操作,或者只是 Torch,以查看它是一个直线转换,尽管围绕它存在一些神秘。在本质上,无论您使用协方差矩阵还是 SVD 进行分解,PCA 都是线性代数的基本练习。

Spark 在 GitHub 上提供了 PCA 的源代码示例,分别在降维和特征提取部分。

另请参阅

关于 PCA 的使用和缺点的一些建议:

  • 有些数据集是相互排斥的,因此特征值不会下降(矩阵中每个值都是必需的)。例如,以下向量(.5,0,0), (0,.5,0,0), (0,0,.5,0), and (0,0,0,.5)......不会允许任何特征值下降。

  • PCA 是线性的,试图通过使用均值和协方差矩阵来学习高斯分布。

  • 有时,两个彼此平行的高斯分布不会允许 PCA 找到正确的方向。在这种情况下,PCA 最终会终止并找到一些方向并输出它们,但它们是否是最好的呢?

第十二章:使用 Spark 2.0 ML 库实现文本分析

在本章中,我们将涵盖以下示例:

  • 使用 Spark 进行词频统计-所有都计算

  • 使用 Word2Vec 在 Spark 中显示相似的单词

  • 下载维基百科的完整转储,用于实际的 Spark ML 项目

  • 使用潜在语义分析进行文本分析,使用 Spark 2.0

  • 在 Spark 2.0 中使用潜在狄利克雷分配进行主题建模

介绍

文本分析处于机器学习、数学、语言学和自然语言处理的交叉点。文本分析,在旧文献中称为文本挖掘,试图从非结构化和半结构化数据中提取信息并推断出更高级别的概念、情感和语义细节。重要的是要注意,传统的关键字搜索无法处理嘈杂、模糊和无关的标记和概念,需要根据实际上下文进行过滤。

最终,我们试图做的是针对一组给定的文档(文本、推文、网络和社交媒体),确定沟通的要点以及它试图传达的概念(主题和概念)。如今,将文档分解为其部分和分类是太原始了,无法被视为文本分析。我们可以做得更好。

Spark 提供了一套工具和设施,使文本分析变得更容易,但用户需要结合技术来构建一个可行的系统(例如 KKN 聚类和主题建模)。

值得一提的是,许多商业系统使用多种技术的组合来得出最终答案。虽然 Spark 拥有足够数量的技术,在规模上运行得非常好,但可以想象,任何文本分析系统都可以从图形模型(即 GraphFrame、GraphX)中受益。下图总结了 Spark 提供的文本分析工具和设施:

文本分析是一个新兴且重要的领域,因为它适用于许多领域,如安全、客户参与、情感分析、社交媒体和在线学习。使用文本分析技术,可以将传统数据存储(即结构化数据和数据库表)与非结构化数据(即客户评论、情感和社交媒体互动)结合起来,以确定更高级的理解和更全面的业务单位视图,这在以前是不可能的。在处理选择社交媒体和非结构化文本作为其主要沟通方式的千禧一代时,这尤为重要。

非结构化文本的主要挑战在于无法使用传统的数据平台工具,如 ETL 来提取并对数据进行排序。我们需要结合 NLP 技术的新数据整理、ML 和统计方法,可以提取信息和洞察力。社交媒体和客户互动,比如呼叫中心的通话记录,包含了有价值的信息,如果不加以重视就会失去竞争优势。

我们不仅需要文本分析来处理静态的大数据,还必须考虑到动态的大数据,比如推文和数据流,才能有效。

处理非结构化数据有几种方法。下图展示了当今工具包中的技术。虽然基于规则的系统可能适用于有限的文本和领域,但由于其特定的决策边界设计为在特定领域中有效,因此无法推广。新系统使用统计和 NLP 技术以实现更高的准确性和规模。

在本章中,我们涵盖了四个示例和两个实际数据集,以演示 Spark 在规模上处理非结构化文本分析的能力。

首先,我们从一个简单的配方开始,不仅模仿早期的网络搜索(关键词频率),而且还以原始代码格式提供了 TF-IDF 的见解。这个配方试图找出一个单词或短语在文档中出现的频率。尽管听起来难以置信,但实际上美国曾对这种技术发出了专利!

其次,我们使用一个众所周知的算法 Word2Vec,它试图回答这样一个问题,即*如果我给你一个单词,你能告诉我周围的单词,或者它的邻居是什么吗?*这是使用统计技术在文档中寻找同义词的好方法。

第三,我们实现了潜在语义分析LSA),这是一种主题提取方法。这种方法是在科罗拉多大学博尔德分校发明的,并且一直是社会科学的主要工具。

第四,我们实现了潜在狄利克雷分配LDA)来演示主题建模,其中抽象概念以可扩展和有意义的方式(例如,家庭,幸福,爱情,母亲,家庭宠物,孩子,购物和聚会)提取并与短语或单词相关联。

使用 Spark 进行词频统计 - 一切都计算在内

对于这个配方,我们将从 Project Gutenberg 下载一本文本格式的书籍,网址为www.gutenberg.org/cache/epub/62/pg62.txt

Project Gutenberg 提供了超过 5 万本各种格式的免费电子书供人类使用。请阅读他们的使用条款;让我们不要使用命令行工具下载任何书籍。

当您查看文件的内容时,您会注意到书的标题和作者是《火星公主》的作者是埃德加·赖斯·伯勒斯。

这本电子书可以供任何人在任何地方免费使用,几乎没有任何限制。您可以复制它,赠送它,或者根据本电子书在线附带的 Project Gutenberg 许可证条款进行重复使用,网址为www.gutenberg.org/

然后我们使用下载的书籍来演示 Scala 和 Spark 的经典单词计数程序。这个例子一开始可能看起来有些简单,但我们正在开始进行文本处理的特征提取过程。此外,对于理解 TF-IDF 的概念,对文档中单词出现次数的一般理解将有所帮助。

如何做...

  1. 在 IntelliJ 或您选择的 IDE 中开始一个新项目。确保包含必要的 JAR 文件。

  2. 该配方的package语句如下:

package spark.ml.cookbook.chapter12
  1. 导入 Scala、Spark 和 JFreeChart 所需的包:
import org.apache.log4j.{Level, Logger}
import org.apache.spark.sql.SQLContext
import 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) {
val frame = new ChartFrame("", chart)
   frame.pack()
   frame.setVisible(true)
 }
  1. 让我们定义我们书籍文件的位置:
val input = "../data/sparkml2/chapter12/pg62.txt"
  1. 使用工厂构建器模式创建一个带有配置的 Spark 会话:
val spark = SparkSession
 .builder .master("local[*]")
 .appName("ProcessWordCount")
 .config("spark.sql.warehouse.dir", ".")
 .getOrCreate()
import spark.implicits._
  1. 我们应该将日志级别设置为警告,否则输出将难以跟踪:
Logger.getRootLogger.setLevel(Level.*WARN*)
  1. 我们读取停用词文件,稍后将用作过滤器:
val stopwords = scala.io.Source.fromFile("../data/sparkml2/chapter12/stopwords.txt").getLines().toSet
  1. 停用词文件包含常用词,这些词在匹配或比较文档时没有相关价值,因此它们将被排除在术语池之外。

  2. 我们现在加载书籍进行标记化、分析、应用停用词、过滤、计数和排序:

val lineOfBook = spark.sparkContext.textFile(input)
 .flatMap(line => line.split("\\W+"))
 .map(_.toLowerCase)
 .filter( s => !stopwords.contains(s))
 .filter( s => s.length >= 2)
 .map(word => (word, 1))
 .reduceByKey(_ + _)
 .sortBy(_._2, false)
  1. 我们取出出现频率最高的 25 个单词:
val top25 = lineOfBook.take(25)
  1. 我们循环遍历结果 RDD 中的每个元素,生成一个类别数据集模型来构建我们的单词出现图表:
val dataset = new DefaultCategoryDataset()
top25.foreach( {case (term: String, count: Int) => dataset.setValue(count, "Count", term) })

显示单词计数的条形图:

val chart = ChartFactory.createBarChart("Term frequency",
 "Words", "Count", dataset, PlotOrientation.VERTICAL,
 false, true, false)

 val plot = chart.getCategoryPlot()
 val domainAxis = plot.getDomainAxis();
 domainAxis.setCategoryLabelPositions(CategoryLabelPositions.DOWN_45);
show(chart)

以下图表显示了单词计数:

  1. 我们通过停止 SparkContext 来关闭程序:
spark.stop()

它是如何工作的...

我们首先通过正则表达式加载下载的书籍并对其进行标记化。下一步是将所有标记转换为小写,并从我们的标记列表中排除停用词,然后过滤掉任何少于两个字符长的单词。

去除停用词和特定长度的单词会减少我们需要处理的特征数量。这可能并不明显,但根据各种处理标准去除特定单词会减少我们的机器学习算法后续处理的维度数量。

最后,我们按降序对结果进行了排序,取前 25 个,并为其显示了条形图。

还有更多...

在本食谱中,我们有了关键词搜索的基础。重要的是要理解主题建模和关键词搜索之间的区别。在关键词搜索中,我们试图根据出现的次数将短语与给定文档关联起来。在这种情况下,我们将指导用户查看出现次数最多的一组文档。

另请参阅

这个算法的演进的下一步,开发者可以尝试作为扩展的一部分,是添加权重并得出加权平均值,但是 Spark 提供了一个我们将在即将到来的食谱中探讨的设施。

使用 Word2Vec 在 Spark 中显示相似的单词

在本食谱中,我们将探讨 Word2Vec,这是 Spark 用于评估单词相似性的工具。Word2Vec 算法受到了一般语言学中的分布假设的启发。在本质上,它试图表达的是在相同上下文中出现的标记(即,与目标的距离)倾向于支持相同的原始概念/含义。

Word2Vec 算法是由 Google 的一个研究团队发明的。请参考本食谱中*还有更多...*部分提到的一篇白皮书,其中更详细地描述了 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. 我们从书中找到火星的十个同义词:
val synonyms = model.findSynonyms("martian", 10)
  1. 显示模型找到的十个同义词的结果:
synonyms.show(false)

  1. 我们通过停止 SparkContext 来关闭程序:
spark.stop()

它是如何工作的...

Spark 中的 Word2Vec 使用 skip-gram 而不是连续词袋CBOW),后者更适合神经网络NN)。在本质上,我们试图计算单词的表示。强烈建议用户了解局部表示与分布式表示之间的区别,这与单词本身的表面含义非常不同。

如果我们使用分布式向量表示单词,那么相似的单词自然会在向量空间中靠在一起,这是一种理想的模式抽象和操作的泛化技术(即,我们将问题简化为向量运算)。

对于一组经过清理并准备好进行处理的单词*{Word[1,] Word[2, .... ,]Word[n]}*,我们要做的是定义一个最大似然函数(例如,对数似然),然后继续最大化似然(即,典型的 ML)。对于熟悉 NN 的人来说,这是一个简单的多类 softmax 模型。

我们首先将免费书籍加载到内存中,并将其标记为术语。然后将术语转换为小写,并过滤掉任何少于四个字的单词。最后应用停用词,然后进行 Word2Vec 计算。

还有更多...

无论如何,你如何找到相似的单词?有多少算法可以解决这个问题,它们又有什么不同?Word2Vec 算法已经存在一段时间了,还有一个叫做 CBOW 的对应算法。请记住,Spark 提供了 skip-gram 方法作为实现技术。

Word2Vec 算法的变体如下:

  • Continuous Bag of Words (CBOW):给定一个中心词,周围的词是什么?

  • Skip-gram:如果我们知道周围的单词,我们能猜出缺失的单词吗?

有一种称为skip-gram 模型与负采样SGNS)的算法变体,似乎优于其他变体。

共现是 CBOW 和 skip-gram 的基本概念。尽管 skip-gram 没有直接使用共现矩阵,但它间接使用了它。

在这个食谱中,我们使用了 NLP 中的停用词技术,在运行算法之前对我们的语料库进行了清理。停用词是英语单词,比如“the”,需要被移除,因为它们对结果没有任何改进。

另一个重要的概念是词干提取,这里没有涉及,但将在以后的食谱中演示。词干提取去除额外的语言构件,并将单词减少到其根(例如,“工程”、“工程师”和“工程师”变成“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:lang="en"> 

  <siteinfo> 
    <sitename>Wikipedia</sitename> 
    <dbname>enwiki</dbname> 
    <base>https://en.wikipedia.org/wiki/Main_Page</base> 
    <generator>MediaWiki 1.27.0-wmf.22</generator> 
    <case>first-letter</case> 
    <namespaces> 
      <namespace key="-2" case="first-letter">Media</namespace> 
      <namespace key="-1" case="first-letter">Special</namespace> 
      <namespace key="0" case="first-letter" /> 
      <namespace key="1" case="first-letter">Talk</namespace> 
      <namespace key="2" case="first-letter">User</namespace> 
      <namespace key="3" case="first-letter">User talk</namespace> 
      <namespace key="4" case="first-letter">Wikipedia</namespace> 
      <namespace key="5" case="first-letter">Wikipedia talk</namespace> 
      <namespace key="6" case="first-letter">File</namespace> 
      <namespace key="7" case="first-letter">File talk</namespace> 
      <namespace key="8" case="first-letter">MediaWiki</namespace> 
      <namespace key="9" case="first-letter">MediaWiki talk</namespace> 
      <namespace key="10" case="first-letter">Template</namespace> 
      <namespace key="11" case="first-letter">Template talk</namespace> 
      <namespace key="12" case="first-letter">Help</namespace> 
      <namespace key="13" case="first-letter">Help talk</namespace> 
      <namespace key="14" case="first-letter">Category</namespace> 
      <namespace key="15" case="first-letter">Category talk</namespace> 
      <namespace key="100" case="first-letter">Portal</namespace> 
      <namespace key="101" case="first-letter">Portal talk</namespace> 
      <namespace key="108" case="first-letter">Book</namespace> 
      <namespace key="109" case="first-letter">Book talk</namespace> 
      <namespace key="118" case="first-letter">Draft</namespace> 
      <namespace key="119" case="first-letter">Draft talk</namespace> 
      <namespace key="446" case="first-letter">Education Program</namespace> 
      <namespace key="447" case="first-letter">Education Program talk</namespace> 
      <namespace key="710" case="first-letter">TimedText</namespace> 
      <namespace key="711" case="first-letter">TimedText talk</namespace> 
      <namespace key="828" case="first-letter">Module</namespace> 
      <namespace key="829" case="first-letter">Module talk</namespace> 
      <namespace key="2300" case="first-letter">Gadget</namespace> 
      <namespace key="2301" case="first-letter">Gadget talk</namespace> 
      <namespace key="2302" case="case-sensitive">Gadget definition</namespace> 
      <namespace key="2303" case="case-sensitive">Gadget definition talk</namespace> 
      <namespace key="2600" case="first-letter">Topic</namespace> 
    </namespaces> 
  </siteinfo> 
  <page> 
    <title>AccessibleComputing</title> 
    <ns>0</ns> 
    <id>10</id> 
    <redirect title="Computer accessibility" />

还有更多...

我们建议使用 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

以下两个语句导入了处理维基百科 XML 转储/对象所需的Cloud9库工具包元素。Cloud9是一个库工具包,使得开发人员更容易访问、整理和处理维基百科 XML 转储。有关更详细信息,请参阅以下代码行:

import edu.umd.cloud9.collection.wikipedia.WikipediaPage
import edu.umd.cloud9.collection.wikipedia.language.EnglishWikipediaPage

维基百科是一个免费的知识体,可以通过以下维基百科下载链接免费下载为 XML 块/对象的转储:

en.wikipedia.org/wiki/Wikipedia:Database_download

文本的复杂性和结构可以通过Cloud9工具包轻松处理,该工具包可以使用之前列出的import语句来访问和处理文本。

以下链接提供了有关Cloud9库的一些信息:

接下来,执行以下步骤:

  1. 我们定义一个函数来解析维基百科页面并返回页面的标题和内容文本:
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. 我们定义一个简短的函数来应用 Porter 词干算法到术语上:
def wordStem(stem: PorterStemmer, term: String): String = {
 stem.setCurrent(term)
 stem.stem()
 stem.getCurrent
 }
  1. 我们定义一个函数将页面的内容文本标记为术语:
def tokenizePage(rawPageText: String, stopWords: Set[String]): Seq[String] = {
 val stem = new PorterStemmer()

 rawPageText.split("\\W+")
 .map(_.toLowerCase)
 .filterNot(s => stopWords.contains(s))
 .map(s => wordStem(stem, s))
 .filter(s => s.length > 3)
 .distinct
 .toSeq
 }
  1. 让我们定义维基百科数据转储的位置:
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("ProcessLSA App")
   .config("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
   .config("spark.sql.warehouse.dir", ".")
   .getOrCreate()
  1. 我们应该将日志级别设置为警告,否则输出将难以跟踪:
Logger.getRootLogger.setLevel(Level.WARN)
  1. 我们开始处理庞大的维基百科数据转储成文章页面,取样文件:
val wikiData = spark.sparkContext.hadoopRDD(
 jobConf,
 classOf[org.apache.hadoop.streaming.StreamInputFormat],
 classOf[Text],
 classOf[Text]).sample(false, .1)
  1. 接下来,我们将样本数据处理成包含标题和页面内容文本的 RDD:
val wikiPages = wikiData.map(_._1.toString).flatMap(*parseWikiPage*)
  1. 我们现在输出我们将处理的维基百科文章的数量:
println("Wiki Page Count: " + wikiPages.count())
  1. 我们将加载停用词以过滤页面内容文本:
val stopwords = scala.io.Source.fromFile("../data/sparkml2/chapter12/stopwords.txt").getLines().toSet
  1. 我们标记化页面内容文本,将其转换为术语以进行进一步处理:
val wikiTerms = wikiPages.map{ case(title, text) => tokenizePage(text, stopwords) }
  1. 我们使用 Spark 的HashingTF类来计算我们标记化的页面内容文本的术语频率:
val hashtf = new HashingTF()
 val tf = hashtf.transform(wikiTerms)
  1. 我们获取术语频率并利用 Spark 的 IDF 类计算逆文档频率:
val idf = new IDF(minDocFreq=2)
 val idfModel = idf.fit(tf)
 val tfidf = idfModel.transform(tf)
  1. 使用逆文档频率生成一个RowMatrix并计算奇异值分解:
tfidf.cache()
 val rowMatrix = new RowMatrix(tfidf)
 val svd = rowMatrix.computeSVD(k=25, computeU = true)

 println(svd)

U:行将是文档,列将是概念。

S:元素将是每个概念的变化量。

V:行将是术语,列将是概念。

  1. 通过停止 SparkContext 来关闭程序:
spark.stop()

工作原理...

该示例首先通过使用 Cloud9 Hadoop 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矩阵)而不是其他变体。

以下是 SVD 的技术:

SVD 的主要目标是降维以获得所需的(即前N个)主题或抽象概念。我们将使用以下输入来获得以下部分中所述的输出。

作为输入,我们将采用m x nm为文档数,n为术语或属性数)的大矩阵。

这是我们应该得到的输出:

有关 SVD 的更详细示例和简短教程,请参见以下链接:

您还可以参考 RStudio 的写作,链接如下:

rstudio-pubs-static.s3.amazonaws.com/222293_1c40c75d7faa42869cc59df879547c2b.html

另请参阅

SVD 在第十一章中有详细介绍,大数据中的高维度诅咒

有关 SVD 的图示表示,请参阅第十一章中的示例使用奇异值分解(SVD)解决高维度问题大数据中的高维度问题

有关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 中使用潜在狄利克雷分配进行主题建模

在这个示例中,我们将利用潜在狄利克雷分配来演示主题模型生成,以从一系列文档中推断主题。

我们在之前的章节中已经涵盖了 LDA,因为它适用于聚类和主题建模,但在本章中,我们演示了一个更详细的示例,以展示它在文本分析中对更真实和复杂的数据集的应用。

我们还应用 NLP 技术,如词干处理和停用词,以提供更真实的 LDA 问题解决方法。我们试图发现一组潜在因素(即与原始因素不同),可以以更高效的方式在减少的计算空间中解决和描述解决方案。

当使用 LDA 和主题建模时,经常出现的第一个问题是狄利克雷是什么? 狄利克雷只是一种分布,没有别的。请参阅明尼苏达大学的以下链接了解详情:www.tc.umn.edu/~horte005/docs/Dirichletdistribution.pdf

如何做...

  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. 我们定义一个函数来解析维基百科页面,并返回页面的标题和内容文本:
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. 让我们定义维基百科数据转储的位置:
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. 我们开始处理庞大的维基百科数据转储,将其转换为文章页面并对文件进行抽样:
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 的文本列转换为原始单词,以处理每个维基百科页面:
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()

它是如何工作的...

我们首先加载了维基百科文章的转储,并使用 Hadoop XML 利用流式处理 API 将页面文本解析为标记。特征提取过程利用了几个类来设置最终由 LDA 类进行处理,让标记从 Spark 的RegexTokenizeStopwordsRemoverHashingTF中流出。一旦我们有了词频,数据就被传递给 LDA 类,以便将文章在几个主题下进行聚类。

Hadoop XML 工具和其他一些必需的依赖项可以在以下位置找到:

还有更多...

请参阅第八章中的 LDA 配方,了解更多关于 LDA 算法本身的详细解释。Apache Spark 2.0 无监督聚类

来自*机器学习研究杂志(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 Streaming 和机器学习库

在本章中,我们将涵盖以下内容:

  • 结构化流式处理用于近实时机器学习

  • 实时机器学习的流式数据框架

  • 实时机器学习的流式数据集

  • 使用 queueStream 进行流式数据和调试

  • 下载和理解著名的鸢尾花数据,用于无监督分类

  • 流式 KMeans 用于实时在线分类器

  • 下载葡萄酒质量数据进行流式回归

  • 流式线性回归用于实时回归

  • 下载皮马糖尿病数据进行监督分类

  • 流式逻辑回归用于在线分类器

介绍

Spark 流式处理是朝着统一和结构化 API 的发展之路,以解决批处理与流处理的问题。自 Spark 1.3 以来,Spark 流式处理一直可用,使用离散流(DStream)。新的方向是使用无界表模型来抽象底层框架,用户可以使用 SQL 或函数式编程查询表,并以多种模式(完整、增量和追加输出)将输出写入另一个输出表。Spark SQL Catalyst 优化器和 Tungsten(堆外内存管理器)现在是 Spark 流式处理的固有部分,这导致了更高效的执行。

在本章中,我们不仅涵盖了 Spark 机器库中提供的流式设施,还提供了四个介绍性的配方,这些配方在我们对 Spark 2.0 的更好理解之旅中非常有用。

以下图表描述了本章涵盖的内容:

Spark 2.0+通过抽象掉一些框架的内部工作原理,并将其呈现给开发人员,而不必担心端到端的一次写入语义,来构建上一代的成功。这是从基于 RDD 的 DStream 到结构化流处理范式的一次旅程,在这个范式中,您的流处理世界可以被视为具有多种输出模式的无限表。

状态管理已经从updateStateByKey(Spark 1.3 到 Spark 1.5)发展到mapWithState(Spark 1.6+),再到结构化流处理(Spark 2.0+)的第三代状态管理。

现代 ML 流式系统是一个复杂的连续应用程序,不仅需要将各种 ML 步骤组合成管道,还需要与其他子系统交互,以提供实用的端到端信息系统。

在我们完成这本书时,Databricks,这家支持 Spark 社区的公司,在 Spark Summit West 2017 上宣布了关于 Spark 流处理未来方向的声明(尚未发布):

“今天,我们很高兴提出一个新的扩展,连续处理,它还消除了执行中的微批处理。正如我们今天早上在 Spark Summit 上展示的那样,这种新的执行模式让用户在许多重要的工作负载中实现亚毫秒的端到端延迟 - 而不需要更改他们的 Spark 应用程序。”

来源:databricks.com/blog/2017/06/06/simple-super-fast-streaming-engine-apache-spark.html

以下图表描述了大多数流式系统的最小可行流式系统(为了演示而过于简化):

如前图所示,任何现实生活中的系统都必须与批处理(例如,模型参数的离线学习)进行交互,而更快的子系统则集中于对外部事件的实时响应(即在线学习)。

Spark 的结构化流处理与 ML 库的完全集成即将到来,但与此同时,我们可以创建和使用流式数据框架和流式数据集来进行补偿,这将在接下来的一些配方中看到。

新的结构化流式处理具有多个优势,例如:

  • 批处理和流处理 API 的统一(无需翻译)

  • 更简洁的表达式语言的函数式编程

  • 容错状态管理(第三代)

  • 大大简化的编程模型:

  • 触发

  • 输入

  • 查询

  • 结果

  • 输出

  • 数据流作为无界表

以下图表描述了将数据流建模为无限无界表的基本概念:

在 Spark 2.0 之前的范式中,DStream 构造推进了流作为一组离散数据结构(RDDs)的模型,当我们有延迟到达时,这是非常难处理的。固有的延迟到达问题使得难以构建具有实时回溯模型的系统(在云中非常突出),因为实际费用的不确定性。

以下图表以可视化方式描述了 DStream 模型,以便进行相应比较:

相比之下,使用新模型,开发人员需要担心的概念更少,也不需要将代码从批处理模型(通常是 ETL 类似的代码)转换为实时流模型。

目前,由于时间线和遗留问题,必须在所有 Spark 2.0 之前的代码被替换之前一段时间内了解两种模型(DStream 和结构化流)。我们发现新的结构化流模型特别简单,与 DStream 相比,并尝试在本章涵盖的四个入门配方中展示和突出显示差异。

结构化流用于近实时机器学习

在这个配方中,我们探索了 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.SparkSession
import java.io.{BufferedOutputStream, PrintWriter}
import java.net.Socket
import java.net.ServerSocket
import java.util.concurrent.TimeUnit
import scala.util.Random
import org.apache.spark.sql.streaming.ProcessingTime
  1. 定义一个 Scala 类来生成投票数据到客户端套接字:
class CountSreamThread(socket: Socket) extends Thread
  1. 定义一个包含人们投票的文字字符串值的数组:
val villians = Array("Bane", "Thanos", "Loki", "Apocalypse", "Red Skull", "The Governor", "Sinestro", "Galactus",
 "Doctor Doom", "Lex Luthor", "Joker", "Magneto", "Darth Vader")
  1. 现在我们将覆盖Threads类的run方法,随机模拟对特定恶棍的投票:
override def run(): Unit = {

 println("Connection accepted")
 val out = new PrintWriter(new BufferedOutputStream(socket.getOutputStream()))

 println("Producing Data")
 while (true) {
 out.println(villians(Random.nextInt(villians.size)))
 Thread.sleep(10)
 }

 println("Done Producing")
 }
  1. 接下来,我们定义一个 Scala 单例对象,以接受在定义的端口9999上的连接并生成投票数据:
object CountStreamProducer {

 def main(args: Array[String]): Unit = {

 val ss = new ServerSocket(9999)
 while (true) {
 println("Accepting Connection...")
 new CountSreamThread(ss.accept()).start()
 }
 }
 }
  1. 不要忘记启动数据生成服务器,这样我们的流式示例就可以处理流式投票数据。

  2. 将输出级别设置为ERROR以减少 Spark 的输出:

   Logger.getLogger("org").setLevel(Level.ERROR)
    Logger.getLogger("akka").setLevel(Level.ERROR)
  1. 创建一个SparkSession,以访问 Spark 集群和底层会话对象属性,如SparkContextSparkSQLContext
val spark = SparkSession
.builder.master("local[*]")
.appName("votecountstream")
.config("spark.sql.warehouse.dir", ".")
.getOrCreate()
  1. 导入 spark implicits,因此只需导入行为:
import spark.implicits._
  1. 通过连接到本地端口9999创建一个流 DataFrame,该端口利用 Spark 套接字源作为流数据的来源:
val stream = spark.readStream
 .format("socket")
 .option("host", "localhost")
 .option("port", 9999)
 .load()
  1. 在这一步中,我们通过恶棍名称和计数对流数据进行分组,以模拟用户实时投票:
val villainsVote = stream.groupBy("value").count()
  1. 现在我们定义一个流查询,每 10 秒触发一次,将整个结果集转储到控制台,并通过调用start()方法来调用它:
val query = villainsVote.orderBy("count").writeStream
 .outputMode("complete")
 .format("console")
 .trigger(ProcessingTime.create(10, TimeUnit.SECONDS))
 .start()

第一个输出批次显示在这里作为批次0

额外的批处理结果显示在这里:

  1. 最后,等待流查询的终止或使用SparkSession API 停止进程:
query.awaitTermination()

它是如何工作的...

在这个配方中,我们创建了一个简单的数据生成服务器来模拟投票数据的流,然后计算了投票。下图提供了这个概念的高级描述:

首先,我们通过执行数据生成服务器来开始。其次,我们定义了一个套接字数据源,允许我们连接到数据生成服务器。第三,我们构建了一个简单的 Spark 表达式,按反派(即坏超级英雄)分组,并计算当前收到的所有选票。最后,我们配置了一个 10 秒的阈值触发器来执行我们的流查询,将累积的结果转储到控制台上。

这个配方涉及两个简短的程序:

  • CountStreamproducer.scala:

  • 生产者-数据生成服务器

  • 模拟为自己投票并广播

  • VoteCountStream.scala:

  • 消费者-消费和聚合/制表数据

  • 接收并计算我们的反派超级英雄的选票

还有更多...

如何使用 Spark 流处理和结构化流处理编程的主题超出了本书的范围,但我们认为有必要在深入研究 Spark 的 ML 流处理提供之前分享一些程序来介绍这些概念。

要了解流处理的基本知识,请参阅以下关于 Spark 的文档:

另请参见

用于实时机器学习的流 DataFrame

在这个配方中,我们探讨了流 DataFrame 的概念。我们创建了一个由个人的姓名和年龄组成的 DataFrame,我们将通过电线进行流式传输。流 DataFrame 是与 Spark ML 一起使用的一种流行技术,因为在撰写本文时,我们尚未完全集成 Spark 结构化 ML。

我们将此配方限制为仅演示流 DataFrame 的范围,并留给读者将其适应其自定义 ML 管道。虽然在 Spark 2.1.0 中,流 DataFrame 并不是开箱即用的,但在后续版本的 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 秒批处理间隔触发器输出结果。

还有更多...

DataFrame 和结构化编程的结合是一个强大的概念,它帮助我们将数据层与流分离,使编程变得更加容易。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.TimeUnit
import org.apache.log4j.{Level, Logger}
import org.apache.spark.sql.SparkSession
import 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 Stream")
.config("spark.sql.warehouse.dir", ".")
.getOrCreate()
  1. 日志消息的交错导致输出难以阅读,因此将日志级别设置为警告:
Logger.getLogger("org").setLevel(Level.ERROR)
Logger.getLogger("akka").setLevel(Level.ERROR)
  1. 现在,加载通用电气 CSV 文件并推断模式:
val s = spark.read
.format("csv")
.option("header", "true")
.option("inferSchema", "true")
.load("../data/sparkml2/chapter13/GE.csv")
s.printSchema()

您将在控制台输出中看到以下内容:

root
|-- date: timestamp (nullable = true)
|-- open: double (nullable = true)
|-- high: double (nullable = true)
|-- low: double (nullable = true)
|-- close: double (nullable = true)
|-- volume: integer (nullable = true)
|-- adjclose: double (nullable = true)
  1. 接下来,我们将通用电气 CSV 文件加载到类型为StockPrice的数据集中:
val streamDataset = spark.readStream
            .schema(s.schema)
            .option("sep", ",")
            .option("header", "true")
            .csv("../data/sparkml2/chapter13/ge").as[StockPrice]
  1. 我们将过滤流,以获取任何收盘价大于 100 美元的股票:
val ge = streamDataset.filter("close > 100.00")
  1. 现在,我们将转换后的流数据输出到控制台,每秒触发一次:
val query = ge.writeStream
.outputMode("append")
.trigger(ProcessingTime(1, TimeUnit.SECONDS))
.format("console")

  1. 我们启动我们定义的流式查询,并等待数据出现在流中:
query.start().awaitTermination()
  1. 最后,我们的流式查询结果将出现在控制台中:

它是如何工作的…

在这个示例中,我们将利用追溯到 1972 年的通用电气GE)的收盘价格市场数据。为了简化数据,我们已经对此示例进行了预处理。我们使用了上一个示例中的相同方法,用于实时机器学习的流式数据框架,通过窥探 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(),这将需要使用流式上下文。我们明确指出这一点,以突出 Spark 流和 Spark 结构化流编程模型之间的差异之一。

这个示例由两个不同的程序(ClickGenerator.scalaClickStream.scala)组成。

如何做到…

  1. 在 IntelliJ 或您选择的 IDE 中启动一个新项目。确保包含必要的 JAR 文件。

  2. 设置程序所在的包位置:

package spark.ml.cookbook.chapter13
  1. 导入必要的包:
import java.time.LocalDateTime
import scala.util.Random._
  1. 定义一个 Scalacase 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 的配置和具有 1 秒持续时间的 Spark 流上下文:
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 流应用程序的。接下来,创建队列和队列流以接收流数据。现在的第 15 步和第 16 步类似于操作 RDD 的一般 Spark 应用程序。下一步是启动流上下文处理。流上下文启动后,我们将数据附加到队列,处理开始以微批处理方式进行。

这里提到了一些相关主题的文档:

另请参阅

在其核心,queueStream()只是一个队列,我们在 Spark 流(2.0 之前)转换为 RDD 后拥有的 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 个观测组成。每个观测由四个数值特征(以厘米为单位测量)和一个标签组成,该标签表示每个鸢尾花属于哪个类别:

特征/属性

  • 花萼长度(厘米)

  • 花萼宽度(厘米)

  • 花瓣长度(厘米)

  • 花瓣宽度(厘米)

标签/类别

  • Iris Setosa

  • Iris Versicolour

  • Iris Virginic

还有更多...

以下图片描述了一朵鸢尾花,标有花瓣和萼片以便清晰显示:

另请参阅

以下链接更详细地探讨了鸢尾花数据集:

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.Vectors
import org.apache.spark.mllib.regression.LabeledPoint
import org.apache.spark.rdd.RDD
import org.apache.spark.SparkContext
import scala.collection.mutable.Queue
  1. 我们首先定义一个函数,将鸢尾花数据加载到内存中,过滤掉空白行,为每个元素附加一个标识符,最后返回类型为字符串和长整型的元组:
def readFromFile(sc: SparkContext) = {
 sc.textFile("../data/sparkml2/chapter13/iris.data")
 .filter(s => !s.isEmpty)
 .zipWithIndex()
 }
  1. 创建一个解析器来获取我们元组的字符串部分并创建一个标签点:
def toLabelPoints(records: (String, Long)): LabeledPoint = {
 val (record, recordId) = records
 val fields = record.split(",")
 LabeledPoint(recordId,
 Vectors.*dense*(fields(0).toDouble, fields(1).toDouble,
 fields(2).toDouble, fields(3).toDouble))
 }
  1. 创建一个查找映射,将标识符转换回文本标签特征:
def buildLabelLookup(records: RDD[(String, Long)]) = {
 records.map {
 case (record: String, id: Long) => {
 val fields = record.split(",")
 (id, fields(4))
 }
 }.collect().toMap
 }
  1. 创建 Spark 的配置和 Spark 流式上下文,持续 1 秒:
val spark = SparkSession
 .builder.master("local[*]")
 .appName("KMean 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 irisData = IrisData.readFromFile(spark.sparkContext)
val lookup = IrisData.buildLabelLookup(irisData)
  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. 创建流式 KMeans 对象将数据聚类成三组:
val model = new StreamingKMeans().setK(3)
 .setDecayFactor(1.0)
 .setRandomCenters(4, 0.0)
  1. 设置 KMeans 模型以接受流式训练数据来构建模型:
model.trainOn(trainingStream.map(lp => lp.features))
  1. 设置 KMeans 模型以预测聚类组值:
val values = model.predictOnValues(testStream.map(lp => (lp.label, lp.features)))
 values.foreachRDD(n => n.foreach(v => {
 println(v._2, v._1, lookup(v._1.toLong))
 }))
  1. 启动流式上下文,以便在接收到数据时处理数据:
  ssc.start()
  1. 将鸢尾花数据转换为标签点:
val irisLabelPoints = irisData.map(record => IrisData.toLabelPoints(record))
  1. 现在将标签点数据分成训练数据集和测试数据集:
val Array(trainData, test) = irisLabelPoints.randomSplit(Array(.80, .20))
  1. 将训练数据追加到流式队列进行处理:
trainQueue += irisLabelPoints
 Thread.sleep(2000)
  1. 现在将测试数据分成四组,并追加到流式队列进行处理:
val testGroups = test.randomSplit(*Array*(.25, .25, .25, .25))
 testGroups.foreach(group => {
 testQueue += group
 *println*("-" * 25)
 Thread.sleep(1000)
 })
  1. 配置的流式队列打印出聚类预测组的以下结果:
-------------------------
(0,78.0,Iris-versicolor)
(2,14.0,Iris-setosa)
(1,132.0,Iris-virginica)
(0,55.0,Iris-versicolor)
(2,57.0,Iris-versicolor)
-------------------------
(2,3.0,Iris-setosa)
(2,19.0,Iris-setosa)
(2,98.0,Iris-versicolor)
(2,29.0,Iris-setosa)
(1,110.0,Iris-virginica)
(2,39.0,Iris-setosa)
(0,113.0,Iris-virginica)
(1,50.0,Iris-versicolor)
(0,63.0,Iris-versicolor)
(0,74.0,Iris-versicolor)
-------------------------
(2,16.0,Iris-setosa)
(0,106.0,Iris-virginica)
(0,69.0,Iris-versicolor)
(1,115.0,Iris-virginica)
(1,116.0,Iris-virginica)
(1,139.0,Iris-virginica)
-------------------------
(2,1.0,Iris-setosa)
(2,7.0,Iris-setosa)
(2,17.0,Iris-setosa)
(0,99.0,Iris-versicolor)
(2,38.0,Iris-setosa)
(0,59.0,Iris-versicolor)
(1,76.0,Iris-versicolor)
  1. 通过停止 SparkContext 来关闭程序:
ssc.stop()

它是如何工作的...

在这个配方中,我们首先加载鸢尾花数据集,并使用zip() API 将数据与唯一标识符配对,以生成用于 KMeans 算法的标记点数据结构。

接下来,创建可变队列和QueueInputDStream,以便追加数据以模拟流式。一旦QueueInputDStream开始接收数据,流式 k 均值聚类就开始动态聚类数据并打印结果。你会注意到的有趣的事情是,我们在一个队列流上流式训练数据,而在另一个队列流上流式测试数据。当我们向我们的队列追加数据时,KMeans 聚类算法正在处理我们的传入数据并动态生成簇。

还有更多...

*StreamingKMeans()*的文档:

另请参阅

通过构建模式或streamingKMeans定义的超参数为:

setDecayFactor()
setK()
setRandomCenters(,)

有关更多详细信息,请参阅第八章中的在 Spark 中构建 KMeans 分类系统食谱,使用 Apache Spark 2.0 进行无监督聚类

下载用于流回归的葡萄酒质量数据

在这个食谱中,我们下载并检查了 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

下载皮马糖尿病数据进行监督分类

在这个配方中,我们从 UCI 机器学习库下载并检查了皮马糖尿病数据集。我们将稍后使用该数据集与 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.data
6,148,72,35,0,33.6,0.627,50,1
1,85,66,29,0,26.6,0.351,31,0
8,183,64,0,0,23.3,0.672,32,1
1,89,66,23,94,28.1,0.167,21,0
0,137,40,35,168,43.1,2.288,33,1
  1. 现在我们来看一下皮马糖尿病数据,以了解其格式:
tail -5 pima-indians-diabetes.data
10,101,76,48,180,32.9,0.171,63,0
2,122,70,27,0,36.8,0.340,27,0
5,121,72,23,112,26.2,0.245,30,0
1,126,60,0,0,30.1,0.349,47,1
1,93,70,31,0,30.4,0.315,23,0

它是如何工作的...

我们有 768 个观测值的数据集。每行/记录由 10 个特征和一个标签值组成,可以用于监督学习模型(即逻辑回归)。标签/类别要么是1,表示糖尿病检测呈阳性,要么是0,表示检测呈阴性。

特征/属性:

  • 怀孕次数

  • 口服葡萄糖耐量试验 2 小时后的血浆葡萄糖浓度

  • 舒张压(毫米汞柱)

  • 三头肌皮褶厚度(毫米)

  • 口服葡萄糖耐量试验 2 小时后的血清胰岛素(mu U/ml)

  • 身体质量指数(体重(公斤)/(身高(米)²))

  • 糖尿病谱系功能

  • 年龄(岁)

  • 类变量(0 或 1)

    Label/Class:
               1 - tested positive
               0 - tested negative

还有更多...

我们发现普林斯顿大学提供的以下替代数据集非常有帮助:

data.princeton.edu/wws509/datasets

另请参阅

您可以用来探索此配方的数据集必须以标签(预测类)为二进制(糖尿病检测呈阳性/阴性)的方式进行结构化。

在线分类器的流式逻辑回归

在这个示例中,我们将使用在上一个示例中下载的 Pima 糖尿病数据集和 Spark 的流式逻辑回归算法进行预测,以预测具有各种特征的 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类,以用作逻辑算法的路径。

第四,我们创建了流式逻辑回归模型,它将预测我们最终结果的葡萄酒质量。最后,我们通常从原始数据创建训练和测试数据集,并将其附加到适当的队列中,以触发模型对流数据的处理。每个微批处理的最终结果显示了测试真正阳性的原始标签和预测标签为 1.0,或者真正阴性的标签为 0.0。

还有更多...

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  绝不原创的飞龙  阅读(6)  评论(0编辑  收藏  举报