Spark-大规模机器学习-全-

Spark 大规模机器学习(全)

原文:zh.annas-archive.org/md5/7A35D303E4132E910DFC5ADB5679B82A

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

机器学习的核心是关注将原始数据转化为可操作智能的算法。这一事实使得机器学习非常适合于大数据的预测分析。因此,如果没有机器学习,要跟上这些大规模信息流几乎是不可能的。相对较新且新兴的技术 Spark 为大数据工程师和数据科学家提供了一个强大的响应和统一的引擎,既更快速又易于使用。

这使得来自多个领域的学习者能够以更大规模地交互解决他们的机器学习问题。本书旨在使数据科学家、工程师和研究人员能够开发和部署规模化的机器学习应用程序,以便他们学会如何在数据密集型环境中处理大数据集群,构建强大的机器学习模型。

本书的内容是从 Spark 和 ML 基础开始以自下而上的方式编写的,探索了特征工程中的数据,构建可扩展的 ML 管道,通过调整和适应新的数据和问题类型,最终进行模型构建和部署。为了更清晰,我们以这样一种方式提供了章节大纲,以便具有最基本的机器学习和 Spark 编程知识的新读者能够跟随示例,并朝着一些真实的机器学习问题及其解决方案迈进。

本书内容包括以下内容

第一章,使用 Spark 进行数据分析简介,本章介绍了 Spark 的概述、计算范式、安装,并帮助我们开始使用 Spark。它将简要描述 Spark 的主要组件,并专注于其具有弹性分布式数据集(RDD)和数据集的新计算进展。然后,它将专注于 Spark 的机器学习库生态系统。在扩展到 Amazon EC2 之前,将演示使用 Spark 和 Maven 安装、配置和打包简单的机器学习应用程序。

第二章,机器学习最佳实践,提供了对统计机器学习(ML)技术的概念介绍,旨在带领新手从对机器学习的最基本知识到成为熟练的从业者。本章的第二部分侧重于为根据应用类型和要求选择合适的机器学习算法提供一些建议。然后,它将介绍应用大规模机器学习管道时的一些最佳实践。

第三章,通过了解数据来理解问题,详细介绍了用于处理结构化数据的数据集和弹性分布式数据集(RDD)API,旨在提供对可用数据进行基本理解的机器学习问题。最后,您将能够轻松处理基本和复杂的数据操作。将提供使用 RDD 和基于数据集的数据操作的基本抽象的一些比较,以展示在编程和性能方面的收益。此外,我们将指导您走上正确的道路,以便您能够使用 Spark 将 RDD 或数据对象持久化在内存中,从而在后期的并行操作中有效地重复使用。

《第四章》《通过特征工程提取知识》解释了了解应该用于创建预测模型的特征不仅至关重要,而且可能是一个需要深入了解问题领域的难题。可以自动选择数据中对某人正在处理的问题最有用或最相关的特征。考虑到这些问题,本章详细介绍了特征工程,解释了应用它的原因以及特征工程中的一些最佳实践。

除此之外,还将讨论应用于大规模机器学习技术的特征提取、转换和选择的理论描述和示例,使用 Spark MLlib 和 Spark ML API。

《第五章》《通过示例进行监督和无监督学习》将提供围绕如何快速而有力地将监督和无监督技术应用于可用数据解决新问题的实际知识,这些知识是基于前几章的一些广泛使用的示例。这些示例将从 Spark 的角度进行演示。

《第六章》《构建可扩展的机器学习管道》解释了机器学习的最终目标是使机器能够在不需要繁琐和耗时的人工参与和交互的情况下自动从数据中构建模型。因此,本章将指导读者通过使用 Spark MLlib 和 Spark ML 创建一些实用和广泛使用的机器学习管道和应用。将详细描述这两个 API,并且还将涵盖基线用例。然后,我们将专注于扩展 ML 应用程序,使其能够应对不断增加的数据负载。

《第七章》《调整机器学习模型》表明,调整算法或机器学习应用可以简单地被视为一个过程,通过这个过程优化影响模型的参数,以使算法表现最佳。本章旨在指导读者进行模型调整。它将涵盖用于优化 ML 算法性能的主要技术。技术将从 MLlib 和 Spark ML 的角度进行解释。我们还将展示如何通过调整多个参数(如超参数、MLlib 和 Spark ML 的网格搜索参数、假设检验、随机搜索参数调整和交叉验证)来改善 ML 模型的性能。

《第八章》《调整您的机器学习模型》涵盖了使算法适应新数据和问题类型的高级机器学习技术。它将主要关注批处理/流处理架构和使用 Spark 流处理的在线学习算法。最终目标是为静态机器学习模型带来动态性。读者还将看到机器学习算法如何逐渐从数据中学习,即每次算法看到新的训练实例时,模型都会更新。

第九章《使用流式和图形数据进行高级机器学习》解释了如何利用 Spark MLlib 和 Spark ML 等工具在流式和图形数据上应用机器学习技术,例如在主题建模中。读者将能够利用现有的 API 从流数据源(如 Twitter)构建实时和预测性应用程序。通过 Twitter 数据分析,我们将展示如何进行大规模社交情感分析。我们还将展示如何使用 Spark MLlib 开发大规模电影推荐系统,这是社交网络分析的一个隐含部分。

第十章《配置和使用外部库》指导读者如何使用外部库来扩展他们的数据分析。将给出使用第三方包或库在 Spark 核心和 ML/MLlib 上进行机器学习应用的示例。我们还将讨论如何编译和使用外部库与 Spark 的核心库进行时间序列分析。如约定的,我们还将讨论如何配置 SparkR 以改进探索性数据操作。

本书所需内容

软件要求:

第 1-8 章和第十章需要以下软件:Spark 2.0.0(或更高版本)、Hadoop 2.7(或更高版本)、Java(JDK 和 JRE)1.7+/1.8+、Scala 2.11.x(或更高版本)、Python 2.6+/3.4+、R 3.1+和已安装的 RStudio 0.99.879(或更高版本)。可以使用 Eclipse Mars 或 Luna(最新版本)。此外,还需要 Maven Eclipse 插件(2.9 或更高版本)、用于 Eclipse 的 Maven 编译器插件(2.3.2 或更高版本)和用于 Eclipse 的 Maven 汇编插件(2.4.1 或更高版本)。最重要的是,重复使用 Packt 提供的pom.xml文件,并相应地更改先前提到的版本和 API,一切都会得到解决。

对于第九章《使用流式和图形数据进行高级机器学习》,几乎所有先前提到的所需软件都是必需的,除了 Twitter 数据收集示例,该示例将在 Spark 1.6.1 中展示。因此,需要 Spark 1.6.1 或 1.6.2,以及友好的 Maven pom.xml文件。

操作系统要求:

Spark 可以在多个操作系统上运行,包括 Windows、Mac OS 和 LINUX。然而,Linux 发行版更可取(包括 Debian、Ubuntu、Fedora、RHEL、CentOS 等)。更具体地说,例如对于 Ubuntu,建议使用 14.04/15.04(LTS)64 位完整安装或 VMWare player 12 或 Virtual Box。对于 Windows,建议使用 Windows(XP/7/8/10),对于 Mac OS X(10.4.7+)也是如此。

硬件要求:

为了顺利使用 Spark,建议使用至少核心 i3 或核心 i5 处理器的计算机。然而,为了获得最佳结果,核心 i7 将实现更快的数据处理和可伸缩性,至少需要 8GB RAM(建议)用于独立模式,至少需要 32GB RAM 用于单个 VM,或者用于集群的更高内存。此外,需要足够的存储空间来运行繁重的任务(取决于您将处理的数据大小),最好至少有 50GB 的免费磁盘存储空间(用于独立和 SQL 仓库)。

本书适合对象

由于 Python 和 R 是数据科学家常用的两种流行语言,因为有大量的模块或软件包可用来帮助他们解决数据分析问题。然而,这些工具的传统用法通常有限,因为它们在单台机器上处理数据,或者使用基于主存储器的方法处理数据,数据的移动变得耗时,分析需要抽样,并且从开发到生产环境的转换需要大量的重新设计。为了解决这些问题,Spark 提供了一个强大且统一的引擎,既快速又易于使用,这使您能够以交互方式解决机器学习问题,并且规模更大。

因此,如果您是学术界人士、研究人员、数据科学工程师,甚至是处理大型和复杂数据集的大数据工程师。此外,如果您想要加速数据处理管道和机器学习应用的扩展,这本书将是您旅程中的合适伴侣。此外,Spark 提供了许多语言选择,包括 Scala、Java 和 Python。这将帮助您将机器学习应用程序置于 Spark 之上,并使用这些编程语言之一进行重塑。

您应该至少熟悉机器学习概念的基础知识。了解开源工具和框架,如 Apache Spark 和基于 Hadoop 的 MapReduce,会很有帮助,但并非必需。我们期望您具备扎实的统计学和计算数学背景。此外,了解 Scala、Python 和 Java 是明智的。然而,如果您熟悉中级编程语言,这将有助于您理解本书中的讨论和示例。

约定

在这本书中,您会发现许多不同风格的文本,用以区分不同类型的信息。以下是一些这些风格的例子,以及它们的含义解释。

文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下: "我们可以通过使用 include 指令来包含其他上下文。"

在 Windows 环境中创建 Spark 会话的代码块设置如下:

[default]
SparkSession spark = SparkSession
                  .builder()
                  .appName("JavaFPGrowthExample")
                  .master("local[*]")
                  .config("spark.sql.warehouse.dir", "E:/Exp/")                  .getOrCreate();

或者从输入数据集创建简单的 RDD 设置如下:

[default]
            String filename = “input/dataset.txt”;
            RDD<String> data = spark.sparkContext().textFile(fileName, 1);

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

$ scp -i /usr/local/key/my-key-pair.pem  /usr/local/code/FPGrowth-0.0.1-SNAPSHOT-jar-with-dependencies.jar ec2-user@ec2-52-18-252-59.eu-west-1.compute.amazonaws.com:/home/ec2-user/

新术语重要单词以粗体显示。您在屏幕上看到的单词、菜单或对话框中的单词等都会以这样的方式出现在文本中: "点击 下一步 按钮将您移动到下一个屏幕"。

注意

警告或重要说明会出现在这样的框中。

提示

提示和技巧会出现在这样。

第一章:Spark 数据分析简介

本章概述了 Apache Spark、其计算范式和安装开始。它将简要描述 Spark 的主要组件,并专注于其新的计算进展。将讨论弹性分布式数据集RDD)和数据集作为本书其余部分的基础知识。然后将专注于 Spark 机器学习库。然后演示如何安装和打包一个简单的机器学习应用程序与 Spark 和 Maven。简而言之,本章将涵盖以下主题:

  • Spark 概述

  • 具有 Spark 的新计算范式

  • Spark 生态系统

  • Spark 机器学习库

  • 安装和开始使用 Spark

  • 打包您的应用程序与依赖项

  • 运行一个简单的机器学习应用程序

Spark 概述

本节介绍了 Spark(spark.apache.org/)的基础知识,然后介绍了传统并行和分布式计算的问题,接着介绍了 Spark 的演变,以及它为大数据处理和分析带来了新的计算范式。此外,我们还介绍了一些令人兴奋的 Spark 功能,这些功能很容易吸引大数据工程师、数据科学家和研究人员,包括:

  • 数据处理和计算的简单性

  • 计算速度

  • 在大规模数据集上的可伸缩性和吞吐量

  • 对各种数据类型的复杂性

  • 使用不同的集群管理器轻松进行集群计算和部署

  • 与各种大数据存储和来源的工作能力和支持

  • 广泛使用和新兴编程语言编写的多样化 API

Spark 基础知识

在赞美 Spark 及其许多优点之前,有必要进行简要概述。Apache Spark 是一个快速、内存中的大数据处理和通用集群计算框架,具有一系列复杂的高级 API,用于高级数据分析。与基于 Hadoop 的 MapReduce 只适用于批处理作业的速度和易用性不同,Spark 可以被认为是一个适用于对静态(批处理)和实时数据应用高级分析的通用执行引擎:

  • Spark 最初是在加州大学伯克利分校的 AMPLab 基于弹性分布式数据集RDDs)开发的,它为内存集群计算设施提供了容错抽象。然而,后来 Spark 的代码库被捐赠给了 Apache 软件基金会,使其成为开源,自那时起,开源社区一直在照顾它。Spark 提供了一个接口,通过其高级 API(Java、Scala、Python 和 R 编写)在整个集群上以规模执行数据分析,具有隐式数据并行性和容错性。

在 Spark 2.0.0 中,实现了提升的库(最广泛使用的数据分析算法),包括:

  • 用于查询和处理大规模结构化数据的 Spark SQL

  • SparkR 用于统计计算,使用 R 语言进行分布式计算规模化

  • MLlib 用于机器学习(ML)应用程序,内部分为两部分;MLlib 用于基于 RDD 的机器学习应用程序开发和 Spark ML 用于开发完整的计算数据科学和机器学习工作流的高级抽象

  • 用于大规模图形数据处理的 GraphX

  • Spark Streaming 用于处理大规模实时流数据,为静态机器学习提供动态工作环境

自其首个稳定版本发布以来,Spark 已经经历了戏剧性和迅速的发展,并得到了全球范围内各种 IT 解决方案提供商、开源社区和研究人员的积极倡导。最近,它已成为大数据处理和集群计算领域最活跃、最大的开源项目之一,不仅因为其广泛的采用,还因为全球范围内 IT 人员、数据科学家和大数据工程师对其部署和调查。正如 Spark 的创始人、Databricks 的 CTO Matei Zaharia 在Big Data analytics新闻网站上所说:

这是一件有趣的事情。在商业上并没有引起太多噪音,但实际的开发者社区通过实际行动投票,人们实际上正在完成工作并与项目合作。

尽管许多科技巨头如雅虎、百度、Conviva、ClearStory、Hortonworks、Gartner 和腾讯已经在生产中使用 Spark,另一方面,IBM、DataStax、Cloudera 和 BlueData 为企业提供了商业化的 Spark 分发。这些公司已经热情地在规模庞大的集群上部署了 Spark 应用程序,共同处理了数百 PB 的数据,这是已知的最大的 Spark 集群。

Spark 的优点

您计划开发机器学习(ML)应用程序吗?如果是这样,您可能已经有一些数据在训练模型之前进行预处理,最终,您将使用训练好的模型对新数据进行预测以查看适应性。这就是您需要的全部吗?我们猜想不是,因为您还必须考虑其他参数。显然,您希望您的 ML 模型在准确性、执行时间、内存使用、吞吐量、调整和适应性方面都能完美运行。等等!还没有结束;如果您希望您的应用程序能够处理大规模的训练和新数据集呢?或者作为数据科学家,如果您可以构建您的 ML 模型,以克服这些问题,从数据整合到训练和错误再到生产的多步旅程,通过在大集群和个人计算机上运行相同的机器学习代码而不会进一步崩溃?您可以简单地依靠 Spark 并闭上眼睛。

Spark 相对于其他大数据技术(如 MapReduce 和 Storm)具有几个优势。首先,Spark 提供了一个全面统一的引擎,以满足各种数据集(文本、表格、图形数据)和数据源(批处理和实时流数据)的大数据处理需求。作为用户(数据科学工程师、学者或开发人员),您可能会从 Spark 的快速应用程序开发中受益,因为它具有简单易懂的 API,可用于批处理、交互和实时流应用程序。

使用 Spark 进行工作和编程是简单易行的。让我们来展示一个例子。雅虎是 Spark 的贡献者和早期采用者之一,他们用 120 行 Scala 代码实现了一个 ML 算法。仅仅 30 分钟的大型数据集训练,包含 1 亿条记录,Scala ML 算法就准备好投入使用了。令人惊讶的是,之前使用 C++编写相同算法需要 15000 行代码(请参考以下网址获取更多信息:www.datanami.com/2014/03/06/apache_spark_3_real-world_use_cases/)。您可以使用 Java、Scala、R 或 Python 开发您的应用程序,并使用 100 多个高级操作符(大多数在 Spark 1.6.1 发布后支持)来转换数据集,并熟悉数据框 API,以操作半结构化、结构化和流数据。除了 Map 和 Reduce 操作,它还支持 SQL 查询、流数据、机器学习和图数据处理。此外,Spark 还提供了用 Scala 和 Python 编写的交互式 shell,用于顺序执行代码(如 SQL 或 R 风格)。

Spark 之所以迅速被采用的主要原因是因为两个主要因素:速度和复杂性。Spark 为许多应用程序提供了数量级的性能,使用粗粒度、不可变和复杂的数据,称为弹性分布式数据集,这些数据集分布在集群中,并且可以存储在内存或磁盘中。RDD 提供了容错性,即一旦创建就无法更改。此外,Spark 的 RDD 具有从其血统中重新创建的属性,如果在计算过程中丢失,它可以重新创建。此外,RDD 可以通过分区自动分布到集群中,并且可以保存您的数据。您还可以通过 Spark 的缓存机制将数据保存在内存中,并且这种机制使得基于 Hadoop 的 MapReduce 集群中的大数据应用程序在内存中执行时速度提高了 100 倍,甚至在基于磁盘的操作中提高了 10 倍。

让我们来看一下关于 Spark 及其计算能力的一个令人惊讶的统计数据。最近,Spark 通过完成 2014 年 Gray Sort Benchmark 中的 100 TB 类别,取代了基于 Hadoop 的 MapReduce,这是一个关于系统能够多快地对 100 TB 数据(1 万亿条记录)进行排序的行业基准(请参考spark.apache.org/news/spark-wins-daytona-gray-sort-100tb-benchmark.htmlsortbenchmark.org/)。最终,它成为了用于对 PB 级数据进行排序的开源引擎(请参考以下网址获取更多信息databricks.com/blog/2014/11/05/spark-officially-sets-a-new-record-in-large-scale-sorting.html)。相比之下,之前由 Hadoop MapReduce 创下的世界纪录需要使用 2100 台机器,执行时间为 72 分钟,这意味着 Spark 使用了 10 倍少的机器,以三倍的速度对相同的数据进行了排序。此外,您可以无缝地结合多个库来开发大规模机器学习和数据分析管道,以在各种集群管理器(如 Hadoop YARN、Mesos 或通过访问数据存储和源(如 HDFS、Cassandra、HBase、Amazon S3 甚至 RDBMs)在云中执行作业。此外,作业可以作为独立模式在本地 PC 或集群上执行,甚至在 AWS EC2 上执行。因此,将 Spark 应用程序部署到集群上非常容易(我们将在本章后面展示如何在集群上部署 Spark 应用程序)。

Spark 的其他优点是:它是开源的,平台无关的。这两点也是它的最大优势,即可以免费使用、分发和修改,并在任何平台上开发应用程序。开源项目也更安全,因为代码对每个人都是可访问的,任何人都可以在发现错误时修复错误。因此,Spark 发展得如此迅速,以至于它已成为涉及大数据解决方案的最大开源项目,拥有来自 200 多个组织的 750 多名贡献者。

具有 Spark 的新计算范式

在本节中,我们将展示 Spark 的发展历程,以及它如何成为大数据处理和集群计算的革命。除此之外,我们还将简要描述 Spark 生态系统,以更详细地了解 Spark 的特点和功能。

传统分布式计算

传统的数据处理范式通常被称为客户端-服务器模型,人们过去常常将数据移动到代码中。数据库服务器(或简称服务器)主要负责执行数据操作,然后将结果返回给客户端-服务器(或简称客户端)程序。然而,当需要计算的任务数量增加时,各种操作和客户端设备也开始呈指数级增长。因此,服务器中的计算端点也开始逐渐复杂起来。因此,为了保持这种计算模型,我们需要增加应用程序(客户端)服务器和数据库服务器的平衡,以存储和处理增加的操作数量。因此,节点之间的数据传播和网络中来回传输的数据也急剧增加。因此,网络本身成为性能瓶颈。因此,在这种计算范式中,性能(无论是可伸缩性还是吞吐量)也无疑会下降。如下图所示:

传统分布式计算

图 1:传统分布式处理的实际应用。

在生命科学中成功完成人类基因组计划后,实时物联网数据、传感器数据、移动设备数据和网络数据正在创造数据洪流,并为大数据做出贡献,这在很大程度上推动了数据密集型计算的发展。如今,数据密集型计算正在以一种新兴的方式不断发展,这需要一个集成的基础设施或计算范式,以便将计算资源和数据带入一个共同的平台,并在其上进行分析。原因是多样的,因为大数据在复杂性(容量多样性速度)方面确实非常庞大,从操作角度来看,还有四个 ms(即移动管理合并整理)。

此外,由于本书将讨论大规模机器学习应用程序,我们还需要考虑一些额外的关键评估参数,如有效性、真实性、价值和可见性,以促进业务增长。可见性很重要,因为假设你有一个大小为 1PB 的大数据集;但是如果没有可见性,一切都是一个黑洞。我们将在接下来的章节中更详细地解释大数据价值。

在单个系统中存储和处理这些大规模和复杂的大型数据集可能是不可行的;因此,它们需要被分区并存储在多台物理机器上。大型数据集被分区或分布,但为了处理和分析这些严格复杂的数据集,数据库服务器和应用程序服务器可能需要增加,以加强大规模的处理能力。同样,多维度中出现的性能瓶颈问题最糟糕,需要一种新的、更数据密集的大数据处理和相关计算范式。

将代码移动到数据

为了克服先前提到的问题,迫切需要一种新的计算范式,这样我们就可以将代码或应用程序移动到数据中,执行数据操作、处理和相关计算(也就是说,数据存储的地方)。由于您已经了解了动机和目标,现在可以将反转的编程模型称为将代码移动到数据并在分布式系统上进行并行处理,可以在以下图表中可视化:

将代码移动到数据

图 2:新的计算(将代码移动到数据并在分布式系统上进行并行处理)。

为了理解图 2中所示的工作流程,我们可以设想一个新的编程模型,描述如下:

  • 使用您在个人计算机上启动的应用程序执行大数据处理(让我们称之为驱动程序),它在集群、网格或更开放地说是云中远程协调执行。

  • 现在你需要做的是将开发的应用程序/算法/代码段(可以使用命令行或 shell 脚本调用或撤销)转移到具有大容量存储、主存储器和处理能力的计算/工作节点。我们可以简单地想象要计算或操作的数据已经存储在这些计算节点中,作为分区或块。

  • 可以理解的是,由于网络或计算瓶颈,大容量数据不再需要传输(上传/下载)到驱动程序,而是仅在其变量中保存数据引用,基本上是一个地址(主机名/IP 地址和端口),用于在集群中定位存储在计算节点中的物理数据(当然,也可以使用其他解决方案进行大容量上传,例如可扩展的配置,将在后面的章节中讨论)。

  • 那么远程计算节点有什么?它们既有数据,也有执行数据计算和必要处理以实现输出或修改数据的代码,而不离开它们的家(更准确地说是计算节点)。

  • 最后,根据您的请求,只有结果可以通过网络传输到您的驱动程序进行验证或其他分析,因为原始数据集有许多子集。

值得注意的是,通过将代码移动到数据,计算结构已经发生了巨大变化。最有趣的是,网络传输的数据量显著减少。这里的理由是,您只会将一小部分软件代码传输到计算节点,并收到原始数据的一个小子集作为返回的结果。这是 Spark 为我们带来的大数据处理最重要的范式转变,它引入了 RDD、数据集、DataFrame 和其他有利特性,这意味着在大数据工程和集群计算历史上有着巨大的革命。然而,为了简洁起见,下一节我们将只讨论 RDD 的概念,其他计算特性将在接下来的章节中讨论。

RDD - 一种新的计算范式

为了理解新的计算范式,我们需要了解弹性分布式数据集RDDs)的概念及 Spark 如何实现数据引用的概念。因此,它已经能够轻松地将数据处理扩展。RDD 的基本特点是它帮助您几乎像处理任何其他数据对象一样处理输入数据集。换句话说,它带来了输入数据类型的多样性,这是您在基于 Hadoop 的 MapReduce 框架中极度缺失的。

RDD 以一种弹性的方式提供了容错能力,一旦创建就无法更改,Spark 引擎将尝试在操作失败时迭代操作。它是分布式的,因为一旦执行了分区操作,RDD 会自动通过分区在集群中分布。RDD 允许您更多地处理输入数据集,因为 RDD 也可以快速而稳健地转换为其他形式。同时,RDD 也可以通过操作转储并在逻辑上相关或计算上同质的应用程序之间共享。这是因为它是 Spark 通用执行引擎的一部分,可以获得大规模的并行性,因此几乎可以应用于任何类型的数据集。

然而,为了在输入数据上进行 RDD 和相关操作,Spark 引擎要求您在数据指针(即引用)和输入数据本身之间建立明显的界限。基本上,您的驱动程序不会保存数据,而只会保存数据的引用,数据实际上位于集群中的远程计算节点上。

为了使数据处理更快、更容易,Spark 支持可以在 RDD 上执行的两种操作:转换和操作(请参考图 3)。转换操作基本上是从现有数据集创建一个新数据集。另一方面,操作在成功计算远程服务器上的输入数据集后,将一个值实现为驱动程序(更确切地说是计算节点)。

由驱动程序启动的数据执行方式构建了一个有向无环图DAG)样式的图表;其中节点表示 RDD,转换操作由边表示。然而,执行本身直到执行操作之前不会在 Spark 集群中的计算节点中开始。然而,在开始操作之前,驱动程序将执行图(表示数据计算流水线或工作流的操作方式)和代码块(作为特定领域的脚本或文件)发送到集群,每个工作节点/计算节点从集群管理节点接收一个副本:

RDD-一种新的计算范式

图 3:RDD 的操作(转换和操作操作)。

在继续下一节之前,我们建议您更详细地了解操作和转换操作。虽然我们将在第三章中详细讨论这两种操作,但目前 Spark 支持两种类型的转换操作。第一种是窄转换,其中数据混合是不必要的。典型的 Spark 窄转换操作使用filter()sample()map()flatMap()mapPartitions()等方法进行。宽转换对于对输入数据集进行更广泛的更改是必不可少的,以便将数据从多个数据分区中的共同节点中带出。宽转换操作包括groupByKey()reduceByKey()union()intersection()join()等。

动作操作通过触发执行作为有向无环图DAG)样式返回 RDD 计算的最终结果到驱动程序。但实际上,材料化的结果实际上是写在存储中的,包括数据对象的中间转换结果,并返回最终结果。常见的动作包括:first()take()reduce()collect()count()saveAsTextFile()saveAsSequenceFile()等。在这一点上,我们相信您已经掌握了 RDD 的基本操作,因此我们现在可以以自然的方式定义 RDD 和相关程序。Spark 提供的典型 RDD 编程模型可以描述如下:

  • 从环境变量中,Spark 上下文(Spark shell 或 Python Pyspark 为您提供了一个 Spark 上下文,或者您可以自己创建,这将在本章后面描述)创建一个初始数据引用 RDD 对象。

  • 通过转换初始 RDD 以创建更多的 RDD 对象,遵循函数式编程风格(稍后将讨论)。

  • 将代码/算法/应用程序从驱动程序发送到集群管理器节点。然后集群管理器为每个计算节点提供一个副本。

  • 计算节点在其分区中保存 RDD 的引用(同样,驱动程序也保存数据引用)。然而,计算节点也可以由集群管理器提供输入数据集。

  • 在转换之后(通过窄转换或宽转换),生成的结果将是全新的 RDD,因为原始的 RDD 不会被改变。最后,通过动作将 RDD 对象或更多(具体数据引用)实现为将 RDD 转储到存储中。

  • 驱动程序可以请求计算节点为程序的分析或可视化结果请求一部分结果。

等等!到目前为止,我们一切顺利。我们假设您将把应用程序代码发送到集群中的计算节点。但是您仍然需要上传或发送输入数据集到集群中以分发给计算节点。即使在大量上传期间,您也需要通过网络传输数据。我们还认为应用程序代码和结果的大小是可以忽略不计的。另一个障碍是,如果您/我们希望 Spark 进行规模计算的数据处理,可能需要首先从多个分区合并数据对象。这意味着我们需要在工作节点/计算节点之间进行数据洗牌,通常通过partition()intersection()join()转换操作来完成。

坦率地说,数据传输并没有完全消除。正如我们和你理解的那样,特别是对于这些操作的大量上传/下载所贡献的开销,它们对应的结果如下:

RDD - 一种新的计算范式

图 4:RDD 的操作(缓存机制)。

好吧,我们已经受到了这些负担的影响是事实。然而,使用 Spark 的缓存机制可以显著减少或解决这些情况。想象一下,您将在相同的 RDD 对象上多次执行操作,这些对象具有很长的血统;这将导致执行时间的增加以及计算节点内部的数据移动。您可以使用 Spark 的缓存机制(图 4)来消除(或至少减少)这种冗余,该机制将 RDD 的计算结果存储在内存中。这样就可以消除每次的重复计算。因为当您在 RDD 上进行缓存时,其分区将加载到主内存中,而不是节点的磁盘(但是,如果内存空间不足,将使用磁盘)。这种技术使得 Spark 集群上的大数据应用程序在每一轮并行处理中明显优于 MapReduce。我们将在第三章中详细讨论 Spark 数据操作和其他技术,通过了解数据来理解问题

Spark 生态系统

为了提供更多增强和额外的大数据处理能力,Spark 可以配置并在现有基于 Hadoop 的集群上运行。正如已经提到的,尽管 Hadoop 提供了Hadoop 分布式文件系统HDFS)以便廉价高效地存储大规模数据;然而,MapReduce 提供的计算完全基于磁盘。MapReduce 的另一个限制是;只能使用高延迟批处理模型执行简单计算,或者更具体地说是静态数据。另一方面,Spark 的核心 API 是用 Java、Scala、Python 和 R 编写的。与 MapReduce 相比,Spark 具有更通用和强大的编程模型,还提供了几个库,这些库是 Spark 生态系统的一部分,用于大数据分析、处理和机器学习领域的冗余功能。如图 5所示,Spark 生态系统包括以下组件:

Spark 生态系统

图 5:Spark 生态系统(截至 Spark 1.6.1)。

正如我们已经提到的,可以无缝地结合这些 API 来开发大规模的机器学习和数据分析应用程序。此外,可以通过访问 HDFS、Cassandra、HBase、Amazon S3 甚至 RDBMs 等数据存储和源,在各种集群管理器上执行作业,如 Hadoop YARN、Mesos、独立或云端。

然而,Spark 还具有其他功能和 API。例如,最近思科宣布向 Spark 生态系统投资 1.5 亿美元,用于思科 Spark 混合服务(www.cisco.com/c/en/us/solutions/collaboration/cloud-collaboration/index.html)。因此,思科 Spark 开放 API 可以提高其在开发人员中的受欢迎程度(高度安全的协作和将智能手机系统连接到云端)。除此之外,Spark 最近集成了 Tachyon(ampcamp.berkeley.edu/5/exercises/tachyon.html),这是一个分布式内存存储系统,可以经济地适应内存,进一步提高 Spark 的性能。

Spark 核心引擎

Spark 本身是用 Scala 编写的,它是一种功能性的面向对象编程语言,运行在 JVM 之上。此外,如图 5 所示,Spark 的生态系统是建立在通用和核心执行引擎之上的,该引擎在不同语言中实现了一些可扩展的 API。较低级别的层或较高级别的层也使用 Spark 核心引擎作为通用执行作业执行引擎,并在其上提供所有其他功能。Spark Core 已经提到是用 Scala 编写的,并且在 Java 虚拟机上运行,高级 API(即 Spark MLlib、SparkR、Spark SQL、Dataset、DataFrame、Spark Streaming 和 GraphX)在执行时使用核心。

Spark 已经使内存计算模式得到了很大的可见度。这个概念(内存计算)使得 Spark 核心引擎能够通过通用执行模型来提高速度,从而开发多样化的应用程序。

用 Java、Scala、R 和 Python 编写的通用数据计算和机器学习算法的低级实现对大数据应用程序开发非常容易。Spark 框架是基于 Scala 构建的,因此在 Scala 中开发 ML 应用程序可以访问最新的功能,这些功能最初可能在其他 Spark 语言中不可用。然而,这并不是一个大问题,开源社区也关注全球开发者的需求。因此,如果您需要开发特定的机器学习算法,并希望将其添加到 Spark 库中,您可以向 Spark 社区做出贡献。Spark 的源代码在 GitHub 上是公开可用的。您可以提交拉取请求,开源社区将在将其添加到主分支之前审查您的更改或算法。有关更多信息,请查看 Spark Jira confluence 网站。

Python 以前是数据科学家的强大工具,Python 在 Spark 中的贡献也不例外。这意味着 Python 也有一些优秀的用于数据分析和处理的库;然而,它相对较慢。另一方面,R 具有丰富的环境,用于数据处理、数据预处理、图形分析、机器学习和统计分析,这可以帮助提高开发者的生产力。对于来自 Java 和 Hadoop 背景的开发者来说,Java 绝对是一个不错的选择。然而,Java 也有与 Python 类似的问题,因为 Java 也比 Scala 慢。

最近在 Databricks 网站上发布的一项调查显示,Spark 用户中有 58%使用 Python,71%使用 Scala,31%使用 Java,18%使用 R 来开发他们的 Spark 应用程序。然而,在本书中,我们将尽量以 Java 为主要示例,必要时会使用少量 Scala 来简化。这是因为许多读者非常熟悉基于 Java 的 MapReduce。然而,我们将在附录中提供一些在 Python 或 R 中使用相同示例的提示。

Spark SQL

Spark SQL 是用于查询和结构化数据处理的 Spark 组件。需求是显而易见的,因为许多数据科学工程师和商业智能分析师也依赖于交互式 SQL 查询来探索来自 RDBMS 的数据。以前,企业经常使用 MS SQL 服务器、Oracle 和 DB2。然而,这些工具不具备可扩展性或交互性。因此,为了使其更容易,Spark SQL 提供了一个称为 DataFrames 和数据集的编程抽象,它们作为分布式 SQL 查询引擎,支持在现有部署和数据上执行未修改的 Hadoop Hive 查询,速度提高了 100 倍。Spark SQL 与 Spark 生态系统的其他部分强大地集成在一起。

最近,Spark 提供了一个新的实验性接口,通常称为数据集(将在下一节中详细讨论),它提供了与 RDD 相同的好处,可以强大地使用lambda函数。Lambda 源自 Lambda 演算(en.wikipedia.org/wiki/Lambda_calculus),指的是计算机编程中的匿名函数。这是现代编程语言中的一个灵活概念,允许您快速编写任何函数而不给它们命名。此外,它还提供了一种写闭包的好方法。例如,在 Python 中:

def adder(x): 
    return lambda y: x + y 
add6 = adder(6) 
add4(4) 

它返回结果为10。另一方面,在 Java 中,如果一个整数是奇数还是偶数,可以类似地编写:

Subject<Integer> sub = x -> x % 2 = 0; // Tests if the parameter is even. 
boolean result = sub.test(8); 

true since 8 is divisible by 2.

请注意,在 Spark 2.0.0 中,Spark SQL 在 SQL 2003 支持的基础上大幅改进了 SQL 功能。因此,现在 Spark SQL 可以执行所有 99 个 TPC-DS 查询。更重要的是,现在原生 SQL 解析器支持 ANSI_SQL 和 Hive QL。原生 DDL 是一个可以执行的命令,它现在也支持 SQL 的子查询和规范化支持的视图。

DataFrames 和数据集的统一

在最新的 Spark 2.0.0 版本中,在 Scala 和 Java 中,DataFrame 和数据集已经统一。换句话说,DataFrame 只是行数据集的类型别名。然而,在 Python 和 R 中,由于缺乏类型安全性,DataFrame 是主要的编程接口。对于 Java,不再支持 DataFrame,而只支持基于数据集和 RDD 的计算,DataFrame 已经过时(请注意,它已经过时 - 而不是被折旧)。虽然为了向后兼容性保留了 SQLContext 和 HiveContext;然而,在 Spark 2.0.0 版本中,替代 DataFrame 和数据集 API 的新入口点是 SparkSession。

Spark Streaming

您可能希望您的应用程序能够处理和分析不仅是静态数据集,还有实时流数据。为了使您的愿望更容易实现,Spark Streaming 提供了将应用程序与流行的批处理和流数据源集成的功能。最常用的数据源包括 HDFS、Flume、Kafka 和 Twitter,它们可以通过它们的公共 API 使用。这种集成允许用户在流和历史数据上开发强大的交互式和分析应用程序。除此之外,容错特性是通过 Spark Streaming 实现的。

图计算 - GraphX

GraphX是建立在 Spark 之上的弹性分布式图计算引擎。GraphX 为希望以大规模交互方式构建、转换和推理图结构化数据的用户带来了革命。作为开发人员,您将享受到简单性,以便使用少量 Scala、Java 或 Python 代码表示大规模图(社交网络图、普通网络图或天体物理学)。GraphX 使开发人员能够充分利用数据并行和图并行系统,通过简单快速地表达图计算。GraphX 柜中增加的另一个美丽之处是,它可以用于构建实时流数据上的端到端图分析管道,其中图空间分区用于处理具有与每个顶点和边相关的属性的大规模有向多图。为了实现这一点,使用了一些基本的图操作符,如子图、joinVertices 和 aggregateMessages,以及 Pregel API 的优化变体。

机器学习和 Spark ML 管道

传统的机器学习应用程序是使用 R 或 Matlab 构建的,存在可扩展性问题。Spark 引入了两个新兴的 API,Spark MLlib 和 Spark ML。这些 API 使得机器学习成为了工程大数据的可行见解,以消除可扩展性约束。建立在 Spark 之上,MLlib 是一个可扩展的机器学习库,拥有众多高质量的算法,具有高精度性能,主要适用于 RDD。Spark 为开发人员提供了许多语言选项,包括 Java、Scala、R 和 Python,以开发完整的工作流程。另一方面,Spark ML 是一个 ALPHA 组件,它增强了一组新的机器学习算法,让数据科学家可以快速组装和配置基于 DataFrames 的实用机器学习管道。

统计计算 - SparkR

SparkR 是一个专为熟悉 R 语言并希望分析大型数据集并从 R shell 交互式运行作业的数据科学家设计的 R 包,支持所有主要的 Spark DataFrame 操作,如聚合、过滤、分组、摘要统计等。同样,用户还可以从本地 R 数据框或任何 Spark 支持的数据源(如 Hive、HDFS、Parquet 或 JSON)创建 SparkR 数据框。从技术上讲,Spark DataFrame 的概念类似于 R 的本机 DataFrame(cran.r-project.org/web/packages/dplyr/vignettes/data_frames.html),另一方面,在语法上类似于dplyr(一个 R 包,参见cran.rstudio.com/web/packages/dplyr/vignettes/introduction.html),但存储在集群设置中。

Spark 机器学习库

在本节中,我们将描述两个主要的机器学习库(Spark MLib 和 Spark ML)以及最广泛使用的实现算法。最终目标是让您对 Spark 的机器学习宝藏有所了解,因为许多人仍然认为 Spark 只是一个通用的内存大数据处理或集群计算框架。然而,情况并非如此,相反,这些信息将帮助您了解使用 Spark 机器学习 API 可以做些什么。此外,这些信息将帮助您探索并增加使用 Spark MLlib 和 Spark ML 部署实际机器学习管道的可用性。

使用 Spark 进行机器学习

在 Spark 时代之前,大数据建模者通常使用统计语言(如 R 和 SAS)构建他们的机器学习模型。然后数据工程师通常会重新在 Java 中实现相同的模型以部署在 Hadoop 上。然而,这种工作流程缺乏效率、可伸缩性、吞吐量和准确性,执行时间也较长。使用 Spark,可以构建、采用和部署相同的机器学习模型,使整个工作流程更加高效、稳健和快速,从而提供实时洞察力以提高性能。Spark 机器学习库的主要目标是使实际的机器学习应用可扩展、更快速和更容易。它包括常见和广泛使用的机器学习算法及其实用工具,包括分类、回归、聚类、协同过滤和降维。它分为两个包:Spark MLlib(spark.mllib)和 Spark ML(spark.ml)。

Spark MLlib

MLlib 是 Spark 的机器学习库。它是一个分布式的低级库,使用 Scala、Java 和 Python 针对 Spark 核心运行时编写。MLlib 主要关注学习算法及其适当的实用工具,不仅提供机器学习分析能力。主要的学习工具包括分类、回归、聚类、推荐系统和降维。此外,它还有助于优化用于开发大规模机器学习流水线的通用原语。正如前面所述,MLlib 带有一些令人兴奋的 API,使用 Java、Scala、R 和 Python 编写。Spark MLlib 的主要组件将在以下部分中描述。

数据类型

Spark 提供了支持存储在单台机器上的本地向量和矩阵数据类型,以及由一个或多个 RDD 支持的分布式矩阵。本地向量和矩阵是简单的数据模型,用作公共接口。向量和矩阵操作严重依赖于线性代数运算,建议在使用这些数据类型之前先获取一些背景知识。

基本统计

Spark 不仅提供了对 RDD 进行列摘要和基本统计的功能,还支持计算两个或多个数据系列之间的相关性,或者更复杂的相关性操作,例如在许多数据系列之间的成对相关性,这是统计学中的常见操作。然而,目前仅支持 Pearson 和 Spearman 的相关性,未来 Spark 版本将添加更多相关性。与其他统计函数不同,Spark 还支持分层抽样,并且可以在 RDD 的键值对上执行;但是,一些功能尚未添加到 Python 开发人员。

Spark 仅提供 Pearson 卡方检验用于假设检验的拟合优度和声明假设的独立性,这是统计学中的一种强大技术,用于确定结果是否在统计上显著以满足声明。Spark 还提供了一些在线实现的测试,以支持诸如 A/B 测试之类的用例,通常在实时流数据上执行显著性测试。Spark 的另一个令人兴奋的功能是生成随机双重 RDD 或向量 RDD 的工厂方法,这对于随机算法、原型、性能和假设检验非常有用。当前 Spark MLib 中的其他功能提供了从样本 RDD 计算核密度估计的计算功能,这是一种用于可视化经验概率分布的有用技术。

分类和回归

分类是一个典型的过程,它帮助新的数据对象和组件根据训练数据进行组织、区分和理解,或者以某种方式属于某种方式。在统计计算中,存在两种类型的分类,二元分类(也常常被称为二项分类)和多类分类。二元分类是将给定观察的数据对象分类为两组的任务。支持向量机SVMs)、逻辑回归、决策树、随机森林、梯度提升树和朴素贝叶斯已经实现到 Spark 的最新版本。

多类分类,另一方面,是将给定观察的数据对象分类到两组以上的任务。逻辑回归、决策树、随机森林和朴素贝叶斯被实现为多类分类。然而,更复杂的分类算法,如多级分类和多类感知器尚未被实现。回归分析也是一种估计变量或观察之间关系的统计过程。除了分类过程,回归分析还涉及多种建模和分析数据对象的技术。目前,Spark MLlib 库支持以下算法:

  • 线性最小二乘

  • 套索

  • 岭回归

  • 决策树

  • 随机森林

  • 梯度提升树

  • 等渗回归

推荐系统开发

智能和可扩展的推荐系统是一种新兴的应用,目前许多企业正在开发,以扩大他们的业务和成本,以实现对客户的推荐自动化。协同过滤方法是推荐系统中最广泛使用的算法,旨在填补用户-项目关联矩阵的缺失条目。例如,Netflix 就是一个例子,他们成功地减少了数百万美元的电影推荐。然而,目前 Spark MLlib 的实现只提供了基于模型的协同过滤技术。

基于模型的协同过滤算法的优点是用户和产品可以通过一小组潜在因素来描述,使用交替最小二乘ALS)算法来预测缺失的条目。缺点是用户评分或反馈不能被考虑在内以预测兴趣。有趣的是,开源开发人员也在努力开发一种基于内存的协同过滤技术,以纳入 Spark MLib 中,其中用户评分数据可以用于计算用户或项目之间的相似性,使得机器学习模型更加多功能。

聚类

聚类是一种无监督的机器学习问题/技术。其目的是根据某种相似性概念将实体的子集彼此分组,通常用于探索性分析和开发分层监督学习管道。Spark MLib 提供了对各种聚类模型的支持,如 K 均值、高斯矩阵、幂迭代聚类PIC)、潜在狄利克雷分配LDA)、二分 K 均值和来自实时流数据的流式 K 均值。我们将在接下来的章节中更多地讨论监督/无监督和强化学习。

降维

处理高维数据既酷又需要满足与大数据相关的复杂性。然而,高维数据的一个问题是不需要的特征或变量。由于所有测量的变量可能对建立模型并不重要,为了回答感兴趣的问题,您可能需要减少搜索空间。因此,基于某些考虑或要求,我们需要在创建任何模型之前减少原始数据的维度,而不损害原始结构。

MLib API 的当前实现支持两种降维技术:奇异值分解SVD)和主成分分析PCA),用于存储在面向行的格式中的高瘦矩阵和任何向量。SVD 技术存在一些性能问题;然而,PCA 是降维中最广泛使用的技术。这两种技术在大规模 ML 应用中非常有用,但它们需要对线性代数有很强的背景知识。

特征提取和转换

Spark 提供了不同的技术,通过词频-逆文档频率TF-IDF)、Word2Vec标准缩放器ChiSqSelector等,使特征工程易于使用。如果您正在从事或计划从事文本挖掘领域的工作,TF-IDF 将是 Spark MLlib 中一个有趣的选项。TF-IDF 提供了一种特征向量化方法,以反映术语对语料库中文档的重要性,这对开发文本分析管道非常有帮助。

此外,您可能对在文本分析的 ML 应用中使用 Word2Vec 计算机分布式词向量表示感兴趣。Word2Vec 的这一特性最终将使您在新颖模式领域的泛化和模型估计更加健壮。您还可以使用 StandardScaler 来通过基于列摘要统计的单位方差缩放或去除均值来规范提取的特征。在构建可扩展的 ML 应用程序的预处理步骤中通常在训练数据集中执行。假设您已通过这种方法提取了特征,现在您需要选择要纳入 ML 模型的特征。因此,您可能还对 Spark MLlib 的 ChiSqSelector 算法进行特征选择感兴趣。ChiSqSelector 在 ML 模型构建过程中尝试识别相关特征。显然,其目的是减少特征空间的大小以及树状方法中的搜索空间,并改善强化学习算法中的速度和统计学习行为。

频繁模式挖掘

在开始构建 ML 模型之前,分析大规模数据集通常是挖掘频繁项、最大频繁模式/项集、连续频繁模式或子序列等的第一步。Spark MLib 的当前实现提供了 FP-growth 的并行实现,用于挖掘频繁模式和关联规则。它还提供了另一个流行算法 PrefixSpan 的实现,用于挖掘序列模式。但是,您将需要根据需要定制算法来挖掘最大频繁模式。我们将在即将到来的章节中提供一个可扩展的 ML 应用程序,用于挖掘隐私并保留最大频繁模式。

Spark ML

Spark ML 是一个 ALPHA 组件,它为用户提供了一组新的机器学习 API,让用户可以快速组装和配置实用的机器学习管道,基于 DataFrames。在赞扬 Spark ML 的特性和优势之前,我们应该了解可以应用和开发到各种数据类型的 DataFrames 机器学习技术,例如向量、非结构化(即原始文本)、图像和结构化数据。为了支持各种数据类型,使应用程序开发更容易,最近,Spark ML 采用了来自 Spark SQL 的 DataFrame 和 Dataset。

数据框架或数据集可以从支持基本和结构化类型的对象的 RDD 隐式或显式创建。Spark ML 的目标是提供一组统一的高级 API,构建在数据框架和数据集之上,而不是 RDD。它帮助用户创建和调整实际的机器学习管道。Spark ML 还提供了用于开发可扩展 ML 管道的特征估计器和转换器。Spark ML 系统化了许多 ML 算法和 API,使得更容易将多个算法组合成单个管道或数据工作流,使用数据框架和数据集的概念。

特征工程中的三个基本步骤是特征提取、特征转换和选择。Spark ML 提供了几种算法的实现,使这些步骤更容易。提取提供了从原始数据中提取特征的功能,而转换提供了从提取步骤中找到的特征进行缩放、转换或修改的功能,选择则帮助从第二步的较大特征集中选择子集。Spark ML 还提供了几种分类(逻辑回归、决策树分类器、随机森林分类器等)、回归(线性回归、决策树回归、随机森林回归、生存回归和梯度提升树回归)、决策树和树集成(随机森林和梯度提升树)以及聚类(K 均值和 LDA)算法的实现,用于在数据框架之上开发 ML 管道。我们将在第三章中更多地讨论 RDD 和数据框架及其基础操作,通过了解数据来理解问题

安装和开始使用 Spark

Spark 是 Apache Hadoop 的继任者。因此,最好将 Spark 安装和工作到基于 Linux 的系统中,即使您也可以尝试在 Windows 和 Mac OS 上。还可以配置 Eclipse 环境以将 Spark 作为 Maven 项目在任何操作系统上运行,并将应用程序作为具有所有依赖项的 jar 文件捆绑。其次,您可以尝试从 Spark shell(更具体地说是 Scala shell)运行应用程序,遵循与 SQL 或 R 编程相同的方式:

第三种方法是从命令行(Windows)/终端(Linux/Mac OS)开始。首先,您需要使用 Scala 或 Java 编写您的 ML 应用程序,并准备具有所需依赖项的 jar 文件。然后,可以将 jar 文件提交到集群以计算 Spark 作业。

我们将展示如何以三种方式开发和部署 Spark ML 应用程序。但是,第一个前提是准备好您的 Spark 应用程序开发环境。您可以在许多操作系统上安装和配置 Spark,包括:

  • Windows(XP/7/8/10)

  • Mac OS X(10.4.7+)

  • Linux 发行版(包括 Debian、Ubuntu、Fedora、RHEL、CentOS 等)

注意

请查看 Spark 网站spark.apache.org/documentation.html获取与 Spark 版本和操作系统相关的文档。以下步骤向您展示如何在 Ubuntu 14.04(64 位)上安装和配置 Spark。请注意,Spark 2.0.0 运行在 Java 7+、Python 2.6+/3.4+和 R 3.1+上。对于 Scala API,Spark 2.0.0 使用 Scala 2.11。因此,您需要使用兼容的 Scala 版本(2.11.x)。

步骤 1:Java 安装

在安装 Spark 时,应将 Java 安装视为安装的强制要求之一,因为基于 Java 和 Scala 的 API 需要在系统上安装 Java 虚拟机。尝试以下命令验证 Java 版本:

$ java -version 

如果 Java 已经安装在您的系统上,您应该看到以下消息:

java version "1.7.0_80"
Java(TM) SE Runtime Environment (build 1.7.0_80-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.80-b11, mixed mode)

如果您的系统上没有安装 Java,请确保在进行下一步之前安装 Java。请注意,为了使用 lambda 表达式支持,建议在系统上安装 Java 8,最好同时安装 JDK 和 JRE。尽管对于 Spark 1.6.2 和之前的版本,Java 7 应该足够:

$ sudo apt-add-repository ppa:webupd8team/java
$ sudo apt-get update
$ sudo apt-get install oracle-java8-installer

安装后,请不要忘记设置JAVA_HOME。只需应用以下命令(我们假设 Java 安装在/usr/lib/jvm/java-8-oracle):

$ echo "export JAVA_HOME=/usr/lib/jvm/java-8-oracle" >> ~/.bashrc 
$ echo "export PATH=$PATH:$JAVA_HOME/bin" >> ~/.bashrc

您可以在主目录中的.bashrc文件中手动添加这些环境变量。如果找不到文件,可能是隐藏的,因此需要进行探索。只需转到视图选项卡并启用显示隐藏文件

步骤 2:安装 Scala

Spark 本身是用 Scala 编写的,因此您的系统上应该安装了 Scala。通过使用以下命令检查这一点非常简单:

$ scala -version

如果 Scala 已经安装在您的系统上,您应该在终端上收到以下消息:

Scala code runner version 2.11.8 -- Copyright 2002-2016, LAMP/EPFL

请注意,在编写此安装过程时,我们使用的是最新版本的 Scala,即 2.11.8。如果您的系统上没有安装 Scala,请确保安装 Scala,因此在进行下一步之前,您可以从 Scala 网站www.scala-lang.org/download/下载最新版本的 Scala。下载完成后,您应该在下载文件夹中找到 Scala tar文件:

  1. 通过从其位置提取 Scala tar文件来提取,或者在终端中键入以下命令来提取 Scala tar 文件:
 $ tar -xvzf scala-2.11.8.tgz 

  1. 现在将 Scala 分发移动到用户的透视图(例如,/usr/local/scala)通过以下命令或手动执行:
 $ cd /home/Downloads/ 
 $ mv scala-2.11.8 /usr/local/scala 

  1. 设置 Scala 主目录:
$ echo "export SCALA_HOME=/usr/local/scala/scala-2.11.8" >> 
        ~/.bashrc 
$ echo "export PATH=$PATH:$SCALA_HOME/bin" >> ~/.bashrc

  1. 安装完成后,您应该使用以下命令进行验证:
 $ scala -version

  1. 如果 Scala 已成功配置到您的系统上,您应该在终端上收到以下消息:
Scala code runner version 2.11.8 -- Copyright 2002-2016, LAMP/EPFL

步骤 3:安装 Spark

从 Apace Spark 网站spark.apache.org/downloads.html下载最新版本的 Spark。对于此安装步骤,我们使用了最新的 Spark 稳定版本 2.0.0 版本,预先构建为 Hadoop 2.7 及更高版本。下载完成后,您将在下载文件夹中找到 Spark tar文件:

  1. 从其位置提取 Scala tar文件,或者在终端中键入以下命令来提取 Scala tar文件:
 $ tar -xvzf spark-2.0.0-bin-hadoop2.7.tgz 

  1. 现在将 Scala 分发移动到用户的透视图(例如,/usr/local/spark)通过以下命令或手动执行:
 $ cd /home/Downloads/ 
 $ mv spark-2.0.0-bin-hadoop2.7 /usr/local/spark 

  1. 安装完 Spark 后,只需应用以下命令:
$ echo "export SPARK_HOME=/usr/local/spark/spark-2.0.0-bin-hadoop2.7" >>
      ~/.bashrc 
$ echo "export PATH=$PATH:$SPARK_HOME/bin" >> ~/.bashrc

步骤 4:使所有更改永久生效

使用以下命令对~/.bashrc文件进行源操作,以使更改永久生效:

$ source ~/.bashrc

如果执行$ vi ~/. bashrc命令,您将在bashrc文件中看到以下条目如下:

export JAVA_HOME=/usr/lib/jvm/java-8-oracle
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/usr/lib/jvm/java-8-oracle/bin:/usr/lib/jvm/java-8-oracle/db/bin:/usr/lib/jvm/java-8-oracle/jre/bin: /usr/lib/jvm/java-8-oracle/bin
export SCALA_HOME=/usr/local/scala/scala-2.11.8
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/usr/lib/jvm/java-8-oracle/bin:/usr/lib/jvm/java-8-oracle/db/bin:/usr/lib/jvm/java-8-oracle/jre/bin: /bin
export SPARK_HOME=/usr/local/spark/spark-2.0.0-bin-hadoop2.7
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/usr/lib/jvm/java-8-oracle/bin:/usr/lib/jvm/java-8-oracle/db/bin:/usr/lib/jvm/java-8-oracle/jre/bin: /bin

步骤 5:验证 Spark 安装

Spark 安装的验证显示在以下截图中:

安装并开始使用 Spark

图 6:Spark shell 确认了成功安装 Spark。

写以下命令以打开 Spark shell,以验证 Spark 是否已成功配置:

$ spark-shell

如果 Spark 安装成功,您应该看到以下消息(图 6)。

Spark 服务器将在本地主机的端口4040上启动,更确切地说是在http://localhost:4040/图 7)。只需转到那里,以确保它是否真的在运行:

安装并开始使用 Spark

图 7:Spark 作为本地 Web 服务器运行。

干得好!现在您已经准备好在 Spark shell 上开始编写 Scala 代码。

使用依赖项打包您的应用程序

现在我们将向您展示如何在 Eclipse 上将应用程序打包为带有所有必需依赖项的 Java 存档(JAR)文件,这是一个集成开发环境IDE)和一个用于 Java 开发的开源工具,是 Apache Maven 项目(maven.apache.org/)。Maven 是一个软件项目管理和理解工具,就像 Eclipse 一样。基于项目对象模型POM)的概念,Maven 可以管理项目的构建、报告和文档编制,从一个中央信息中。

请注意,可以使用命令提示符将用 Java 或 Scala 编写的 ML 应用程序导出为存档/可执行的 jar 文件。但是,为了简化和加快应用程序开发,我们将使用与 Eclipse 相同的 Maven 项目,以便读者可以享受相同的便利性将应用程序提交到主节点进行计算。现在让我们转到讨论如何将频繁模式挖掘应用程序导出为带有所有依赖项的 jar 文件。

步骤 1:在 Eclipse 中创建一个 Maven 项目

成功创建示例 Maven 项目后,您将在 Eclipse 中看到以下项目结构,如图 8所示:

使用依赖项打包应用程序

图 8:Eclipse 中的 Maven 项目结构。

步骤 2:应用程序开发

创建一个 Java 类,并将以下源代码复制到src/main/java目录下,用于挖掘频繁模式。在这里,输入文件名已经通过命令行参数或手动指定源代码来指定文件名字符串。目前,我们只提供了行注释,但是您将从第三章中了解详细信息,通过了解数据来了解问题以及之后的内容:

import java.util.Arrays; 
import java.util.List; 
import org.apache.spark.api.java.JavaRDD; 
import org.apache.spark.api.java.function.Function; 
import org.apache.spark.mllib.fpm.FPGrowth; 
import org.apache.spark.mllib.fpm.FPGrowthModel; 
import org.apache.spark.rdd.RDD; 
import org.apache.spark.sql.SparkSession; 

public class JavaFPGrowthExample { 
  public static void main(String[] args) { 
   //Specify the input transactional as command line argument  
   String fileName = "input/input.txt";  
   //Configure a SparkSession as spark by specifying the application name, master URL, Spark config, and Spark warehouse directory 
  SparkSession spark = SparkSession 
                  .builder() 
                  .appName("JavaFPGrowthExample") 
                  .master("local[*]") 
                  .config("spark.sql.warehouse.dir", "E:/Exp/") 
                  .getOrCreate(); 

   //Create an initial RDD by reading the input database  
   RDD<String> data = spark.sparkContext().textFile(fileName, 1); 

   //Read the transactions by tab delimiter & mapping RDD(data) 
   JavaRDD<List<String>> transactions = data.toJavaRDD().map( 
                   new Function<String, List<String>>(){ 
                   public List<String> call(String line) { 
                          String[] parts = line.split(" "); 
                          return Arrays.asList(parts); 
                                 } 
                             }); 

  //Create FPGrowth object by min. support & no. of partition     
  FPGrowth fpg = new  FPGrowth() 
                       .setMinSupport(0.2) 
                       .setNumPartitions(10); 

  //Train and run your FPGrowth model using the transactions 
  FPGrowthModel<String> model = fpg.run(transactions); 

  //Convert and then collect frequent patterns as Java RDD. After that print the frequent patterns along with their support 
    for (FPGrowth.FreqItemset<String> itemset :      
          model.freqItemsets().toJavaRDD().collect()) {   
       System.out.println(itemset.javaItems()  
                             + "==> " + itemset.freq()); 
      } 
    }   
  }  

步骤 3:Maven 配置

现在您需要配置 Maven,指定相关依赖项和配置。首先,编辑您现有的pom.xml文件,将每个 XML 源代码片段复制到<dependencies>标记内。请注意,根据 Spark 版本,您的依赖项可能会有所不同,因此请相应更改版本:

  1. Spark 核心依赖项用于 Spark 上下文和配置:
      <dependency> 
      <groupId>org.apache.spark</groupId> 
      <artifactId>spark-core_2.11</artifactId> 
      <version>2.0.0</version> 
     </dependency> 

  1. Spark MLib 依赖项用于 FPGrowth:
    <dependency> 
      <groupId>org.apache.spark</groupId> 
      <artifactId>spark-mllib_2.11</artifactId> 
      <version>2.0.0</version> 
     </dependency> 

现在您需要添加构建要求。将以下代码片段立即复制到</dependencies>标记之后。在这里,我们将<groupId>指定为 maven 插件,<artifactId>指定为 maven shade 插件,并使用<finalName>标记指定 jar 文件命名约定。确保您已经指定了源代码下载插件,设置了编译器级别,并为 Maven 设置了装配插件,如下所述:

  1. 使用 Maven 指定源代码下载插件:
       <plugin> 
        <groupId>org.apache.maven.plugins</groupId> 
        <artifactId>maven-eclipse-plugin</artifactId> 
        <version>2.9</version> 
        <configuration> 
          <downloadSources>true</downloadSources> 
          <downloadJavadocs>false</downloadJavadocs> 
        </configuration> 
      </plugin>  

  1. 为 Maven 设置编译器级别:
      <plugin> 
        <groupId>org.apache.maven.plugins</groupId> 
        <artifactId>maven-compiler-plugin</artifactId> 
        <version>2.3.2</version>         
      </plugin> 
      <plugin> 
        <groupId>org.apache.maven.plugins</groupId> 
        <artifactId>maven-shade-plugin</artifactId> 
        <configuration> 
          <shadeTestJar>true</shadeTestJar> 
        </configuration> 
      </plugin> 

  1. 设置 Maven 装配插件:
      <plugin> 
        <groupId>org.apache.maven.plugins</groupId> 
        <artifactId>maven-assembly-plugin</artifactId> 
        <version>2.4.1</version> 
        <configuration> 
          <!-- get all project dependencies --> 
          <descriptorRefs> 
            <descriptorRef>jar-with-dependencies</descriptorRef> 
          </descriptorRefs> 
          <!-- MainClass in mainfest make a executable jar --> 
          <archive> 
            <manifest>              <mainClass>com.example.SparkFPGrowth.JavaFPGrowthExample</mainClass>            </manifest> 
          </archive> 
          <property> 
            <name>oozie.launcher.mapreduce.job.user.classpath.first</name> 
            <value>true</value> 
          </property> 
          <finalName>FPGrowth-${project.version}</finalName> 
        </configuration> 
        <executions> 
          <execution> 
            <id>make-assembly</id> 
            <!-- bind to the packaging phase --> 
            <phase>package</phase> 
            <goals> 
              <goal>single</goal> 
            </goals> 
          </execution> 
        </executions> 
      </plugin> 

完整的pom.xml文件,输入数据和 Java 源文件可以从我们的 GitHub 存储库github.com/rezacsedu/PacktMLwithSpark下载。请注意,我们使用了 Eclipse Mars Eclipse IDE for Java Developers,并且版本是 Mars Release (4.5.0)。您可以选择这个版本或其他发行版,比如 Eclipse Luna。

步骤 4:Maven 构建

在本节中,我们将描述如何在 Eclipse 上创建一个 Maven 友好的项目。在您按照所有步骤后,您将能够成功运行 Maven 项目。步骤应按照以下时间顺序进行:

  1. 将您的项目作为 Maven 安装运行。

  2. 如果您的代码和 Maven 配置文件没有问题,那么 Maven 构建将成功。

  3. 构建 Maven 项目。

  4. 右键单击您的项目,运行 Maven 项目为Maven 构建...,并在Goals选项中写入clean package

  5. 检查 Maven 依赖项。

  6. 展开 Maven 依赖树,并检查是否已安装所需的 jar 文件。

  7. 检查 jar 文件是否生成了依赖项。

  8. 如我们所指定的,您应该在/target目录树下找到两个 jar 文件(参考图 9)。打包文件应该与<finalName>标签中指定的名称完全相同。现在将您的代码(jar 文件)移动到与我们的实验对齐的目录(即/user/local/code)和您的数据(即/usr/local/data/)。我们将在后期使用这个 jar 文件在 AWS EC2 集群上执行 Spark 作业。我们将在下一步讨论输入数据集。使用依赖项打包您的应用程序

图 9:在 Eclipse 上生成了所有必需依赖项的 Maven 项目的 jar。

运行一个样本机器学习应用程序

在本节中,我们将描述如何从 Spark shell 在本地机器上以独立模式运行一个样本机器学习应用程序,最后我们将向您展示如何使用 Amazon EC2(aws.amazon.com/ec2/)部署和运行应用程序。

从 Spark shell 运行 Spark 应用程序

请注意,这只是一个检查安装和运行样本代码的练习。有关机器学习应用程序开发的详细信息将从第三章开始,通过了解数据来理解问题,到第九章使用流和图数据进行高级机器学习

现在我们将进一步进行一种流行的机器学习问题,也称为频繁模式挖掘,使用频繁模式增长或 FP-growth。假设我们有一个如下表所示的交易数据库。每行表示特定客户完成的交易。我们的目标是从数据库中找到频繁模式,这是计算关联规则(en.wikipedia.org/wiki/Association_rule_learning)的先决条件,从客户购买规则中。将此数据库保存为input.txt,不包括交易 ID,在/usr/local/data目录中:

交易 ID 交易
12345678910 A B C D FA B C EB C D E FA C D EC D FD E FD EC D FC FA C D E

表 1:交易数据库。

现在让我们通过指定主机和要使用的计算核心数量来移动到 Spark shell,作为独立模式(这里有四个核心,例如):

$ spark-shell --master "local[4]" 

第 1 步:加载软件包

加载所需的 FPGrowth 软件包和其他依赖软件包:

scala>import org.apache.spark.mllib.fpm.FPGrowth
scala>import org.apache.spark.{SparkConf, SparkContext}

第 2 步:创建 Spark 上下文

要创建一个 Spark 上下文,首先需要通过提及应用程序名称和主 URL 来配置 Spark 会话。然后,您可以使用 Spark 配置实例变量来创建一个 Spark 上下文,如下所示:

val conf = new SparkConf().setAppName(s"FPGrowthExample with $params")
val sc = new SparkContext(conf)

第 3 步:读取交易

让我们在创建的 Spark 上下文(sc)上将交易作为 RDDs 读取(参见图 6):

scala> val transactions = sc.textFile(params.input).map(_.split(" ")).cache()

第 4 步:检查交易数量

这是用于检查交易数量的代码:

Scala>println(s"Number of transactions: ${transactions.count()}")
Number of transactions: 22
Scala>

第 5 步:创建 FPGrowth 模型

通过指定最小支持阈值(也请参阅en.wikipedia.org/wiki/Association_rule_learning)和分区数来创建模型:

scala>val model = new FPGrowth()
 .setMinSupport(0.2)
 .setNumPartitions(2)
 .run(transactions)

第 6 步:检查频繁模式的数量

以下代码解释了如何检查频繁模式的数量:

scala> println(s"Number of frequent itemsets:
    ${model.freqItemsets.count()}")
Number of frequent itemsets: 18
Scala>

第 7 步:打印模式和支持

打印频繁模式及其相应的支持/频率计数(参见图 10)。Spark 作业将在本地主机上运行(参见图 11):

scala> model.freqItemsets.collect().foreach { itemset => println(itemset.items.mkString("[", ",", "]") + ", " + itemset.freq)}

从 Spark shell 运行 Spark 应用程序

图 10:频繁模式。

在本地集群上运行 Spark 应用程序

一旦用户应用程序被打包为 jar 文件(用 Scala 或 Java 编写)或 Python 文件,它可以使用 Spark 分发中 bin 目录下的spark-submit脚本启动。

根据 Spark 网站提供的 API 文档spark.apache.org/docs/2.0.0-preview/submitting-applications.html,此脚本负责设置带有 Spark 及其依赖项的类路径,并且可以支持 Spark 支持的不同集群管理器和部署模型。简而言之,Spark 作业提交语法如下:

$spark-submit [options] <app-jar | python-file> [app arguments]

在这里,[options]可以是:--class <main-class>``--master <master-url>``--deploy-mode <deploy-mode>,以及许多其他选项。

更具体地说,<main-class>是主类名称,是我们应用程序的入口点。<master-url>指定了集群的主 URL(例如,spark://HOST:PORT用于连接给定的 Spark 独立集群主节点,local 用于在本地运行具有没有并行性的一个工作线程的 Spark,local [k]用于在具有 K 个工作线程的本地运行 Spark 作业,这是您计算机上的核心数,local[*]用于在具有与计算机上逻辑核心一样多的工作线程的本地运行 Spark 作业,mesos://IP:PORT用于连接到可用的 Mesos 集群,甚至您可以将作业提交到 Yarn 集群-有关更多信息,请参见spark.apache.org/docs/latest/submitting-applications.html#master-urls)。

<deploy-mode>用于在工作节点(集群)上部署我们的驱动程序,或者作为外部客户端(client)在本地部署。<app-jar>是我们刚刚构建的 jar 文件,包括所有依赖项。<python-file>是使用 Python 编写的应用程序主要源代码。[app-arguments]可以是应用程序开发人员指定的输入或输出参数:

在本地集群上运行 Spark 应用程序

图 11:在本地主机上运行的 Spark 作业

因此,对于我们的情况,作业提交语法将如下所示:

$./bin/spark-submit --class com.example.SparkFPGrowth.JavaFPGrowthExample --master local[4] FPGrowth-0.0.1-SNAPSHOT-jar-with-dependencies.jar input.txt

在这里,JavaFPGrowthExample是用 Java 编写的主类文件;local 是主 URL;FPGrowth-0.0.1-SNAPSHOT-jar-with-dependencies.jar是我们刚刚通过 maven 项目生成的应用程序jar文件;input.txt是作为文本文件的事务数据库,output 是要生成输出的目录(在我们的情况下,输出将显示在控制台上)。现在让我们提交此作业以在本地执行。

如果成功执行,您将找到以下消息,包括图 12中的输出(摘要):

在本地集群上运行 Spark 应用程序

图 12:终端上的 Spark 作业输出。

在 EC2 集群上运行 Spark 应用程序

在前一节中,我们说明了如何在本地或独立模式下提交 spark 作业。在这里,我们将描述如何在集群模式下运行 spark 应用程序。为了使我们的应用程序在 spark 集群模式下运行,我们考虑亚马逊弹性计算云EC2)服务,作为基础设施即服务IaaS)或平台即服务PaaS)。有关定价和相关信息,请参阅此网址aws.amazon.com/ec2/pricing/

步骤 1:密钥对和访问密钥配置

我们假设您已经创建了 EC2 帐户。第一个要求是创建 EC2 密钥对和 AWS 访问密钥。EC2 密钥对是您在通过 SSH 进行安全连接到 EC2 服务器或实例时需要的私钥。要创建密钥,您必须通过 AWS 控制台 docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html#having-ec2-create-your-key-pair。请参考图 13,显示了 EC2 帐户的密钥对创建页面:

在 EC2 集群上运行 Spark 应用程序

图 13:AWS 密钥对生成窗口。

一旦下载并保存在本地机器上,请将其命名为my-key-pair.pem。然后通过执行以下命令确保权限(出于安全目的,您应该将此文件存储在安全位置,比如/usr/local/key):

$ sudo chmod  400  /usr/local/key/my-key-pair.pem

现在您需要的是 AWS 访问密钥,您的帐户凭据,如果您想要使用 spark-ec2 脚本从本地机器提交您的 Spark 作业到计算节点。要生成和下载密钥,请登录到您的 AWS IAM 服务 docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html#Using_CreateAccessKey。下载完成后(即/usr/local/key),您需要在本地机器上设置两个环境变量。只需执行以下命令:

$ echo "export AWS_ACCESS_KEY_ID=<access_key_id>" >> ~/.bashrc 
$ echo " export AWS_SECRET_ACCESS_KEY=<secret_access_key_id>" >> ~/.bashrc 

步骤 2:在 EC2 上配置 Spark 集群

Spark 分发(即/usr/local/spark``/ec2)提供了一个名为spark-ec2的脚本,用于从本地机器(驱动程序)在 EC2 实例上启动 Spark 集群,帮助启动、管理和关闭 Spark 集群。

注意

请注意,在 AWS 上启动集群将花费金钱。因此,当计算完成时,停止或销毁集群始终是一个好习惯。否则,将产生额外的费用。有关 AWS 定价的更多信息,请参阅此 URL aws.amazon.com/ec2/pricing/

一旦您执行以下命令启动新实例,它将自动在集群上设置 Spark、HDFS 和其他依赖项:

$./spark-ec2 --key-pair=<name_of_the_key_pair> --identity-file=<path_of_the key_pair>  --instance-type=<AWS_instance_type > --region=<region> zone=<zone> --slaves=<number_of_slaves> --hadoop-major-version=<Hadoop_version> --spark-version=<spark_version> launch <cluster-name>

我们相信这些参数是不言自明的,或者,也可以在spark.apache.org/docs/latest/ec2-scripts.html上查看详细信息。对于我们的情况,应该是这样的:

$./spark-ec2 --key-pair=my-key-pair --identity-file=/usr/local/key/my-key-pair.pem  --instance-type=m3.2xlarge --region=eu-west-1 --zone=eu-west-1a --slaves=2 --hadoop-major-version=yarn --spark-version=1.6.0 launch ec2-spark-cluster-1

如下截图所示:

在 EC2 集群上运行 Spark 应用程序

图 14:集群主页。

成功完成后,spark 集群将在您的 EC2 帐户上实例化两个工作(从属)节点。这个任务可能需要大约半个小时,具体取决于您的互联网速度和硬件配置。因此,您可能需要喝杯咖啡休息一下。在集群设置成功完成后,您将在终端上获得 Spark 集群的 URL。

要检查集群是否真的在运行,请在浏览器上检查此 URL https://<master-hostname>:8080,其中主机名是您在终端上收到的 URL。如果一切正常,您将发现您的集群正在运行,参见图 14中的集群主页。

步骤 3:在 Spark 集群上运行和部署 Spark 作业

执行以下命令连接到 SSH 远程 Spark 集群:

$./spark-ec2 --key-pair=<name_of_the_key_pair> --identity-file=<path_of_the _key_pair> --region=<region> login <cluster-name> 

对于我们的情况,应该是这样的:

$./spark-ec2 --key-pair=my-key-pair --identity-file=/usr/local/key/my-key-pair.pem --region=eu-west-1 login ec2-spark-cluster-1 

现在通过执行以下命令将您的应用程序(我们在 Eclipse 上生成的 Maven 项目的 jar 包)复制到远程实例(在我们的情况下是ec2-52-48-119-121.eu-west-1.compute.amazonaws.com)(在新终端中):

$ scp -i /usr/local/key/my-key-pair.pem  /usr/local/code/FPGrowth-0.0.1-SNAPSHOT-jar-with-dependencies.jar ec2-user@ec2-52-18-252-59.eu-west-1.compute.amazonaws.com:/home/ec2-user/

然后,您需要通过执行以下命令将您的数据(在我们的情况下是/usr/local/data/input.txt)复制到同一远程实例:

$ scp -i /usr/local/key/my-key-pair.pem /usr/local/data/input.txt ec2-user@ec2-52-18-252-59.eu-west-1.compute.amazonaws.com:/home/ec2-user/ 

在 EC2 集群上运行 Spark 应用程序

图 15:Spark 集群中作业运行状态。

干得好!您已经快完成了!现在,最后您需要提交您的 Spark 作业,让从节点或工作节点进行计算。要这样做,只需执行以下命令:

$./bin/spark-submit --class com.example.SparkFPGrowth.JavaFPGrowthExample --master spark://ec2-52-48-119-121.eu-west-1.compute.amazonaws.com:7077 /home/ec2-user/FPGrowth-0.0.1-SNAPSHOT-jar-with-dependencies.jar /home/ec2-user/input.txt

作业计算成功完成后,您应该在端口 8080 上看到作业的状态,就像图 15一样(输出将显示在终端上)。

第四步:暂停和重新启动 Spark 集群

要停止您的集群,请从本地机器执行以下命令:

$./ec2/spark-ec2 --region=<ec2-region> stop <cluster-name>

对于我们的情况,将会是:

$./ec2/spark-ec2 --region=eu-west-1 stop ec2-spark-cluster-1

要稍后重新启动集群,请执行以下命令:

$./ec2/spark-ec2 -i <key-file> --region=<ec2-region> start <cluster-name>

对于我们的情况,将会是以下内容:

$./ec2/spark-ec2 --identity-file=/usr/local/key/my-key-pair.pem --region=eu-west-1 start ec2-spark-cluster-1

提示

要终止您的 Spark 集群:$./spark-ec2 destroy <cluster-name>

在我们的情况下,将是:$./spark-ec2 --region=eu-west-1 destroy ec2-spark-cluster-1

如果您希望您的应用程序针对大规模数据集进行扩展,最快的方法是将它们从 Amazon S3 或 Amazon EBS 设备加载到节点上的 Hadoop 分布式文件系统(HDFS)中。我们将在后面的章节中通过实际的机器学习示例讨论这种技术。

参考文献

  • 弹性分布式数据集内存集群计算的容错抽象Zaharia,Mosharaf Chowdhury,Tathagata Das,Ankur Dave,Justin Ma,Murphy McCauley,Michael J. Franklin,Scott Shenker,Ion Stoica。NSDI 2012。2012 年 4 月。

  • Spark使用工作集进行集群计算Matei Zaharia,Mosharaf Chowdhury,Michael J. Franklin,Scott Shenker,Ion Stoica,HotCloud 2010。2010 年 6 月。

  • Spark SQLSpark 中的关系数据处理Michael Armbrust,Reynold S. Xin,Cheng Lian,Yin Huai,Davies Liu,Joseph K. Bradley,Xiangrui Meng,Tomer Kaftan,Michael J. Franklin,Ali Ghodsi,Matei Zaharia,SIGMOD 2015。2015 年 6 月。

  • 离散化流规模化的容错流计算Matei Zaharia,Tathagata Das,Haoyuan Li,Timothy Hunter,Scott Shenker,Ion Stoica。SOSP 2013。2013 年 11 月。

  • 离散化流大规模集群上流处理的高效容错模型Matei Zaharia,Tathagata Das,Haoyuan Li,Scott Shenker,Ion Stoica。HotCloud 2012。2012 年 6 月。

  • GraphX统一数据并行和图并行分析Reynold S. Xin,Daniel Crankshaw,Ankur Dave,Joseph E. Gonzalez,Michael J. FranklinIon Stoica。OSDI 2014。2014 年 10 月。

  • MLlibApache Spark 中的机器学习Meng 等人。*arXiv:1505.06807v1,[cs.LG],2015 年 5 月 26 日。

  • 推荐系统协同过滤技术分析Christopher R. Aberger斯坦福出版物,2014 年

总结

这结束了我们对 Spark 的相当快速的介绍。我们已经尝试涵盖了 Spark 的一些最基本的特性,它的计算范式,并通过安装和配置开始使用 Spark。如果 Spark ML 适合机器学习流水线概念(例如特征提取、转换和选择),则建议使用它,因为它在 DataFrame 和 Dataset 方面更加灵活多变。然而,根据 Apache 的文档,他们将继续支持和贡献 Spark MLib,并与 Spark ML 的积极开发一起。

另一方面,数据科学开发人员应该熟悉使用 Spark MLlib 的特性,并期待将来有更多的特性。然而,一些算法目前尚不可用,或者有待添加到 Spark ML 中,尤其是降维。尽管如此,开发人员可以无缝地将这些在 Spark MLib 中找到的技术实现与 Spark ML 中找到的其他算法结合起来,作为混合或可互操作的机器学习应用。我们还展示了一些在集群和云服务上部署机器学习应用的基本技术,尽管您也可以尝试其他可用的部署选项。

提示

有关更多更新,请感兴趣的读者参考 Spark 网站spark.apache.org/docs/latest/mllib-guide.html获取发布日期、API 和规范。由于 Spark 的开源社区和来自全球各地的开发人员不断丰富和更新实现,因此最好保持更新。

在下一章(第二章,机器学习最佳实践),我们将讨论在使用 Spark 开发高级机器学习时的一些最佳实践,包括机器学习任务和类、一些实际的机器学习问题及其相关讨论、机器学习应用开发中的一些最佳实践、选择适合 ML 应用的正确算法等。

第二章:机器学习最佳实践

本章的目的是为那些在典型的统计培训中可能不会接触到这些方法的人提供统计机器学习(ML)技术的概念介绍。本章还旨在通过几个步骤,将新手从对机器学习的最小知识带到了解的实践者。本章的第二部分侧重于根据应用类型和要求选择合适的机器学习算法的一些建议。然后,它将引导人们在应用大规模机器学习流程时遵循一些最佳实践。简而言之,本章将讨论以下主题:

  • 什么是机器学习?

  • 机器学习任务

  • 实际机器学习问题

  • Spark 中的大规模机器学习 API

  • 实际机器学习最佳实践

  • 为您的应用选择合适的算法

什么是机器学习?

在本节中,我们将尝试从计算机科学、统计学和数据分析的角度定义机器学习这个术语。然后我们将展示分析机器学习应用的步骤。最后,我们将讨论一些典型和新兴的机器学习任务,并列举一些需要解决的实际机器学习问题。

现代文献中的机器学习

让我们看看机器学习著名教授 Tom Mitchell 是如何定义机器学习这个术语的。他是 CMU 机器学习系主任,也是卡内基梅隆大学的教授。在他的文献中(Tom M. Mitchell, The Discipline of Machine Learning, CMU-ML-06-108, July 2006www.cs.cmu.edu/~tom/pubs/MachineLearning.pdf)中定义了机器学习这个术语:

机器学习是计算机科学和统计学交叉的自然产物。我们可以说,计算机科学的定义性问题是“我们如何构建解决问题的机器,哪些问题本质上是可解的/不可解的?”统计学的定义性问题主要是“在数据加上一组建模假设的情况下,可以推断出什么,以及推断的可靠性是什么?”机器学习的定义性问题建立在这两者之上,但它是一个独特的问题。计算机科学主要关注如何手动编程计算机,而机器学习关注的是如何让计算机自己编程(从经验中加上一些初始结构)。统计学主要关注从数据中可以推断出什么结论,而机器学习还包括关于如何最有效地捕获、存储、索引、检索和合并这些数据的计算架构和算法,以及如何在更大的系统中协调多个学习子任务,以及计算可解性的问题。

我们相信 Tom 教授的这个定义是不言自明的。然而,我们将在接下来的两个小节中从计算机科学、统计学和数据分析的角度提供对机器学习的更清晰的理解。

提示

感兴趣的读者应该查阅其他资源,以获取有关机器学习及其理论视角的更多见解。在这里,我们提供了一些链接如下:机器学习en.wikipedia.org/wiki/Machine_learning

机器学习:它是什么,为什么重要 - www.sas.com/en_us/insights/analytics/machine-learning.html

机器学习的初步介绍www.youtube.com/watch?v=NOm1zA_Cats

什么是机器学习,它是如何工作的www.youtube.com/watch?v=elojMnjn4kk

使用机器学习进行数据分析入门www.youtube.com/watch?v=U4IYsLgNgoY

机器学习和计算机科学

机器学习是计算机科学的一个分支,研究可以从启发式学习中学习的算法,这通常源自于模式识别和人工智能中的计算学习理论。艾伦·图灵脑海中出现了一个有趣的问题,即机器能思考吗?实际上,有一些很好的理由相信一个足够复杂的机器有一天可以通过无限制的图灵测试;让我们推迟这个问题,直到图灵测试通过。然而,机器至少可以学习。随后,阿瑟·塞缪尔是第一个在 1959 年将术语机器学习定义为一种研究领域,使计算机能够在没有明确编程的情况下学习的人。典型的机器学习任务包括概念学习、预测建模、分类、回归、聚类、降维、推荐系统、深度学习以及从大规模数据集中找到有用模式。

最终目标是通过改进学习方式使其变得自动化,以至于不再需要人类干预,或者尽可能减少人类干预的程度。尽管机器学习有时与知识发现和数据挖掘KDDM)混淆,但后者更专注于探索性数据分析,被称为无监督学习 - 例如聚类分析、异常检测、人工神经网络ANN)等。

其他机器学习技术包括监督学习,其中学习算法分析训练数据并生成可用于映射新示例进行预测的推断函数。分类和回归分析是监督学习的两个典型示例。另一方面,强化学习受行为主义心理学(参见en.wikipedia.org/wiki/Behaviorism)的启发,通常关注软件代理如何通过最大化奖励函数在新的环境中执行动作。动态规划和智能代理是强化学习的两个示例。

典型的机器学习应用可以分为科学知识发现和更多商业应用,从机器人或人机交互HCI)到反垃圾邮件过滤和推荐系统。

统计学和数据分析中的机器学习

机器学习是研究和构建算法的学科(参见en.wikipedia.org/wiki/Algorithm),这些算法可以从启发式学习(参见en.wikipedia.org/wiki/Learning)并对数据进行有意义的预测。然而,为了进行数据驱动的预测或决策,这些算法通过从训练数据集中构建模型(参见en.wikipedia.org/wiki/Mathematical_model)来操作,比严格遵循静态程序或指令更快。机器学习也与计算统计学密切相关并经常重叠。另一方面,计算统计学是统计学的一个应用领域,专注于通过计算机化方法进行预测。此外,它与数学优化有着密切的关系,提供了方法和计算任务以及理论和应用领域。由于对数学背景知识的强烈需求,数学中不可行的任务最适合机器学习,并可以作为替代方法应用。

另一方面,在数据分析领域,机器学习是一种用于设计复杂模型和算法的方法,这些模型和算法朝着预测未来结果的方向发展。这些分析模型允许研究人员、数据科学家、工程师和分析师通过从过去的关系(启发式)和数据中的趋势中学习来产生可靠、可重复和可再现的结果,并挖掘隐藏的见解。我们再次引用 Tom 教授的著名定义,他在文献中解释了从计算机科学的角度来看学习的真正含义(Tom M. Mitchell, The Discipline of Machine Learning, CMU-ML-06-108, July 2006, www.cs.cmu.edu/~tom/pubs/MachineLearning.pdf):

如果一个计算机程序在某类任务 T 上的表现,根据性能度量 P,随着经验 E 的积累而提高,那么就可以说它在任务 T 上从经验 E 中学习。

因此,我们可以得出结论,计算机程序或机器可以:

  • 从数据和历史中学习

  • 可以通过经验进行改进

  • 交互式地增强模型,以用于预测问题的结果

此外,以下图表帮助我们理解机器学习的整个过程:

统计学和数据分析中的机器学习

图 1:一览机器学习。

典型的机器学习工作流程

典型的机器学习应用包括从输入、处理到输出的几个步骤,形成了一个科学工作流程,如图 2 所示。典型的机器学习应用涉及以下步骤:

  1. 加载样本数据。

  2. 将数据解析成算法的输入格式。

  3. 预处理数据并处理缺失值。

  4. 将数据分成两组,一组用于构建模型(训练数据集),另一组用于测试模型(测试数据集或验证数据集)。

  5. 运行算法来构建或训练您的机器学习模型。

  6. 使用训练数据进行预测并观察结果。

  7. 使用测试数据测试和评估模型,或者使用第三个数据集(验证数据集)使用交叉验证技术验证模型。

  8. 调整模型以获得更好的性能和准确性。

  9. 扩展模型,以便将来能处理大规模数据集。

  10. 在商业化中部署机器学习模型:典型的机器学习工作流程

图 2:机器学习工作流程。

通常,机器学习算法有一些方法来处理数据集中的偏斜;这种偏斜有时可能非常严重。在第 4 步中,实验数据集通常被随机分成训练集和测试集,这被称为抽样。训练数据集用于训练模型,而测试数据集用于评估最佳模型的性能。更好的做法是尽可能多地使用训练数据集,以提高泛化性能。另一方面,建议只使用测试数据集一次,以避免在计算预测误差和相关指标时出现过拟合和欠拟合问题。

提示

过拟合是一种统计特性,描述了除了正常和基础关系之外的随机误差和噪音。当超参数相对于观察值或特征的数量过多时,它通常会发生。另一方面,欠拟合是指既不能对训练数据建模,也不能对新数据进行泛化,以适应模型评估或适应性。

然而,这些步骤包括几种技术,我们将在第五章中详细讨论这些技术。第 9 步和第 10 步通常被认为是高级步骤,因此它们将在后面的章节中讨论。

机器学习任务

机器学习任务或机器学习过程通常根据学习系统可用的学习反馈的性质分为三类。监督学习、无监督学习和强化学习;这三种机器学习任务在图 3中显示,并将在本节中讨论:

机器学习任务

图 3:机器学习任务。

监督学习

监督学习应用程序基于一组示例进行预测,其目标是学习将输入映射到与现实世界一致的输出的一般规则。例如,用于垃圾邮件过滤的数据集通常包含垃圾邮件和非垃圾邮件。因此,我们可以知道训练集中哪些消息是垃圾邮件或非垃圾邮件。然而,我们可能有机会使用这些信息来训练我们的模型,以便对新的和未见过的消息进行分类。图 4 显示了监督学习的示意图。

换句话说,在这种情况下,用于训练机器学习模型的数据集带有感兴趣的值标签,并且监督学习算法会寻找这些值标签中的模式。算法找到所需的模式后,这些模式可以用于对未标记的测试数据进行预测。这是最流行和有用的机器学习任务类型,对于 Spark 也不例外,其中大多数算法都是监督学习技术:

监督学习

图 4:监督学习实例。

无监督学习

无监督学习中,数据点没有相关的标签,或者换句话说,在无监督学习的训练数据集中,正确的类别是未知的,如图 5所示。因此,类别必须从非结构化数据集中推断出来,这意味着无监督学习算法的目标是通过描述其结构来对数据进行预处理。

为了克服无监督学习中的这一障碍,通常使用聚类技术来基于某些相似性度量对未标记的样本进行分组,挖掘隐藏模式以进行特征学习。更技术上地说,我们可以编写一个生成模型,然后告诉数据找到解释数据的参数。现在,如果我们对这种阐释的可能性不满意,接下来会发生什么?答案是,我们应该告诉数据再做一次,直到我们使用一些有效的算法或技术为止。

现在你可能会产生一个新的问题,为什么我们必须在数据上贴标签?或者我们不能只欣赏当前顺序的数据,认识到每个数据都是独特的,就像雪花一样?换句话说,通过一点监督,我们的数据可以成长为任何它想成为的东西!那么为什么未标记的数据也应该被考虑进来呢?

嗯,关于这个问题还有一些更深层次的问题。例如,数据中的大部分变化来自于与我们所期望的标记方案无关的现象。一个更现实的例子是 Gmail 如何使用监督学习技术将电子邮件分类为垃圾邮件和正常邮件,其中数据可能使用其参数来解释其语义,而我们关心的只是其句法属性:

无监督学习

图 5:无监督学习。

强化学习

强化学习是一种技术,模型本身从一系列行为或行为中学习。在强化学习中,数据集的复杂性或样本复杂性对于算法成功学习目标函数非常重要。此外,为了实现最终目标,与外部环境互动时应确保最大化奖励函数,如图 6所示。为了使最大化更容易,奖励函数可以通过惩罚不良行为或奖励良好行为来利用。

为了获得最高的奖励,算法应该通过策略进行修改,也允许机器或软件代理定期学习其行为。这些行为可以一劳永逸地学习,或者随着时间的推移,机器学习模型可以不断适应:

强化学习

图 6:强化学习。

例如,强化学习在机器人技术中很常见;算法必须基于一组传感器读数选择机器人的下一个动作。它也是物联网IoT)应用的自然选择,其中计算机程序与动态环境进行交互,必须实现某个目标,而没有明确的导师。另一个例子是游戏Flappy Bird,它已经被训练成自己玩。

推荐系统

推荐系统是一种新兴应用,是信息过滤系统的子类,用于预测用户通常对项目提供的评分或偏好。最近几年推荐系统的概念变得非常普遍,并随后应用于不同的应用程序。最流行的可能是产品(例如电影、音乐、书籍、研究文章、新闻、搜索查询、社交标签等)。推荐系统通常可以分为四类:

  • 协同过滤系统,其中消费者的偏好和对其他用户的推荐是基于行为模式的相似性积累。

  • 基于内容的系统,其中使用监督机器学习来说服分类器区分用户感兴趣和不感兴趣的项目。

  • 混合推荐系统是最近的研究和混合方法(即,结合协同过滤和基于内容的过滤)。Netflix 是这样一个推荐系统的良好例子,它使用受限玻尔兹曼机RBM)和一种矩阵分解算法来处理大型电影数据库,如 IMDb。这种推荐,通过比较相似用户的观看和搜索习惯简单地推荐电影或戏剧或流媒体,被称为评分预测。

  • 基于知识的系统,其中使用有关用户和产品的知识来推理满足用户需求的内容,使用感知树、决策支持系统和基于案例的推理。

半监督学习

监督学习无监督学习之间,有一个小小的地方是半监督学习;在这种情况下,机器学习模型通常接收不完整的训练信号。更具体地说,机器学习模型接收到一组目标输出部分缺失的训练集。半监督学习更多地基于假设,并且通常使用三种假设算法作为未标记数据集的学习算法。使用以下假设:平滑性、聚类和流形假设。

换句话说,半监督学习还可以被称为弱监督自举技术,用于利用未标记示例的隐藏财富来增强从少量标记数据中学习。新兴示例包括半监督期望最小化和人类认知中的概念学习以及传递 SVM

实际机器学习问题

机器学习到底是什么意思?我们在本章的开头已经看到了一些令人信服的对这个术语的定义,以及术语“学习”的含义。然而,机器学习本身的定义取决于要解决的问题。在本节中,我们将首先强调机器学习的类别,然后列举一些现实世界中广为人知和广泛使用的机器学习问题的例子。典型的类别包括分类、聚类、规则提取和回归,这些都将被讨论。

此外,我们还将讨论基于标准机器学习问题的主要分类法的问题。这很重要,因为了解我们可能面临的问题类型可以让我们考虑我们需要的数据。另一个重要的事实是,在了解一些实际的机器学习问题之前,你可能会在开发机器学习应用程序的想法上遇到困难。换句话说,要知道问题,我们首先需要了解数据。因此,本章将讨论算法的类型及其优化问题;数据处理将在第三章中进行讨论,通过了解数据来理解问题

机器学习类别

我们上面提到的问题类别是我们在日常生活中使用和应用机器学习技术时所指的大多数问题的标准。然而,仅仅知道机器学习类别是不够的,我们还需要知道机器正在学习什么类型的问题,因为你会发现许多问题只是简单的问题解决,并没有帮助机器学习模型或代理进行学习。

当你认为一个问题是一个机器学习问题时,更准确地说,你在考虑一个需要从数据中建模的决策问题,这可以被称为一个机器学习问题。换句话说,作为数据科学家或人类专家,如果你有足够的时间通过了解可用的数据集来回答一个特定的问题,你可以或多或少地应用一个合适的机器学习问题。因此,我们可以假设使用一些机器学习算法可以解决的问题主要有两个部分 - 数据本身,可以用来指向问题的特定观察结果,以及可用解决方案的质量的定量测量。一旦你成功地将一个问题确定为机器学习问题,你可能能够思考如何轻松地制定出什么类型的问题,或者你的客户将会要求什么样的后果,或者需要满足什么样的要求。正如上面所述,更常用的机器学习类别包括:分类、聚类、回归和规则提取。我们现在将对每个类别进行简要概述。

分类和聚类

如果实验数据集已经标记,这意味着已经为其分配了一个类别。例如,在垃圾邮件检测中的垃圾邮件/非垃圾邮件,或者在信用卡欺诈识别中的欺诈/非欺诈。然而,如果基本决策的数据集是未标记的,新的标签需要手动或算法地制作。这可能很困难,可以被视为一个判断问题。相反,雕刻出几个群体之间的差异或相似之处可能在计算上更加困难。

另一方面,聚类处理的是未标记或无标记的数据。然而,它仍然可以根据相似性和数据中的自然结构的其他度量来分成组。将数字相册中的图片仅按面孔组织起来而不带有姓名可能是一个例子,人类用户必须手动为组分配名称。同样,手动标记多个图像文件可能会产生相同的计算复杂性;我们将在后面的章节中提供一些示例,说明 Spark 如何提供多个 API 来解决这些问题。

规则提取和回归

从给定的数据集中,可以通过前提和结论以if...then的方式生成命题规则,定义了机器学习代理的行为。这种规则生成技术通常被称为规则提取。你可能会想知道这样的规则是否存在,然而,它们通常不是有针对性的。这意味着用于发现数据中属性之间的统计显著或统计相关关系的方法。

规则提取的一个例子是在面向业务的事务性数据库中挖掘项目之间的关联规则。非技术上,一个实际的例子可能是发现啤酒购买和尿布购买之间的关系或关联,这说明了顾客的愿望和机会。然而,可能会出现一些预测不一定直接涉及规则或数据的情况。

现在让我们谈谈回归,其中数据带有实际值的标签。更确切地说,一些浮点值而不是数据中的标签。理解一个例子的最简单方法是时间序列数据,类似于股票或货币随时间变化的价格。在这些类型的数据中,回归任务是通过一些回归建模技术对新的和不可预测的数据进行预测。

最广泛使用的机器学习问题

你会发现在日常生活中使用机器学习相关问题的大量例子,因为它们解决了广泛使用的技术或算法中的困难部分。我们经常使用许多桌面或基于网络的应用程序,即使不知道使用了哪些基础技术,也可以解决你的问题。你会惊讶地发现,其中许多实际上使用了广泛使用的机器学习算法,使你的生活更轻松。周围有许多机器学习问题。在这里,我们将提到一些真正代表机器学习的例子问题:

  • 垃圾邮件检测或垃圾邮件过滤:给定收件箱中的一些电子邮件,任务是识别哪些电子邮件是垃圾邮件,哪些是非垃圾邮件(通常称为正常)电子邮件。现在具有挑战性的部分是开发一个可以应用的 ML 应用,以便它只能识别非垃圾邮件电子邮件留在收件箱中,并将垃圾邮件移动到相应的垃圾邮件文件夹中,或者永久从电子邮件帐户中删除它们。一个典型的例子可能是在使用 Gmail 时手动执行的操作,但如果你有一个 ML 应用程序,该应用程序将自动执行。

  • 异常检测或异常值检测:异常检测涉及识别数据集中意外或不符合预期模式的项目、事件或观察结果;换句话说,是怀疑模式的识别。最常见的例子是使用一些机器学习应用进行网络异常检测。现在具有挑战性的任务是开发一个可以成功应用于简单识别网络中传播的异常数据点的 ML 应用。

  • 信用卡欺诈检测:信用卡欺诈现在非常普遍。从网上购物中窃取信用卡相关信息,并以非法方式使用在许多国家都有发生。假设你有一个客户一个月的交易数据库。现在具有挑战性的任务是开发一个机器学习应用程序,以识别客户自己进行的交易和他人非法进行的交易。

  • 语音识别:识别声音并将其转换为相应的文本命令,然后执行一些操作,就像智能代理一样。最常用的应用包括苹果的 Siri,三星的 S-Voice,亚马逊的 Echo(消费领域)和微软的 Cortana(特别是因为 Cortana 具有用于可扩展性和集成等的 SDK)。另一个例子是使用识别的声音来锁定或解锁智能手机。

  • 数字/字符识别:假设你有一个手写的邮政编码、地址或信件,现在数字/字符识别的任务是识别和分类每个不同人写的手写字符的数字或字符。一个高效的机器学习应用可以帮助阅读和理解手写的邮政编码或字符,并按地理区域或更技术上的说法,按图像分割对信封内容进行分类。

  • 物联网:大规模传感器数据分析,用于实时流数据的预测和分类。例如,智能客厅监控,包括水位检测,室温检测,家用电器控制等。

  • 游戏分析:用于预测升级销售和针对应用内购买和修改的体育、游戏和基于控制台的游戏档案分析

  • 人脸检测:给定数百或数千张照片的数字相册,任务是识别与给定人相似的照片。在这种情况下,高效的机器学习应用可以帮助按人员组织照片。

  • 产品推荐:根据客户的购买历史和大量的产品库存,目标是识别客户可能有兴趣购买的产品。亚马逊、Facebook 和 Google Plus 等商业和科技巨头为用户提供了这一推荐功能。

  • 股票交易:根据股票市场的当前和历史价格,预测是否应该买入或卖出股票,以便利用机器学习系统获利。

以下是一些新兴的机器学习示例和当前研究的需求:

  • 隐私保护数据挖掘:从面向业务的零售数据库中挖掘最大频繁模式和关联规则,以增加未来的购买

  • 作者姓名消歧:使用手动验证从给定出版物集合的作者列表的聚类结果中的随机样本来评估消歧性能

  • 推荐系统:基于点击流数据的推荐系统,使用关联规则挖掘

  • 文本挖掘:例如,从给定的文本语料库中检查抄袭

  • 情感分析:如今很多商业和科技公司的决策都是基于他人的意见,这将是创新机器学习的好地方

  • 语音理解:给定用户的话语,目标是识别用户提出的具体请求。这个问题的模型将允许程序理解并尝试满足该请求。例如,iPhone 的 Siri 和三星的语音记录器在会议模式下都实现了这个功能

其中一些问题是人工智能、自然语言处理和计算机视觉中最困难的问题,可以使用机器学习算法来解决。同样,我们将尝试开发一些强调这些问题的机器学习应用程序,在接下来的章节中进行讨论。

Spark 中的大规模机器学习 API

在本节中,我们将描述 Spark 机器学习库(Spark MLlib 和 Spark ML)引入的两个关键概念,以及与我们在上述部分讨论的监督和无监督学习技术相一致的最常用的实现算法。

Spark 机器学习库

如前所述,在 Spark 时代之前,大数据建模者通常使用统计语言(如 R、STATA 和 SAS)构建他们的机器学习模型。然后数据工程师通常会重新用 Java 等语言实现相同的模型,以部署在 Hadoop 上。

然而,这种工作流程缺乏效率、可扩展性、吞吐量和准确性,以及延长的执行时间。

使用 Spark,可以重新构建、采用和部署相同的机器学习模型,使整个工作流程更加高效、稳健和快速,从而使您能够提供实时洞察力以提高性能。Spark 机器学习库分为两个包:Spark MLlib(spark.mllib)和 Spark ML(spark.ml)。

Spark MLlib

MLlib 是 Spark 的可扩展机器学习库,它是 Spark Core API 的扩展,提供了一系列易于使用的机器学习算法库。算法是用 Java、Scala 和 Python 实现和编写的。Spark 支持存储在单台机器上的本地向量和矩阵数据类型,以及由一个或多个 RDD 支持的分布式矩阵:

Spark MLlib
ML 任务 离散 连续
监督 分类:逻辑回归及其正则化变体线性支持向量机朴素贝叶斯决策树随机森林梯度提升树 回归:线性回归及其正则化变体线性最小二乘 Lasso 和岭回归等距回归
无监督 聚类:K 均值高斯矩阵幂迭代聚类(PIC)潜在狄利克雷分配(LDA)二分 K 均值流式 K 均值 降维、矩阵分解:主成分分析奇异值分解交替最小二乘
强化 N/A N/A
推荐系统 协同过滤:Netflix 推荐 N/A

表 1:一览 Spark MLlib。

  • 图例:连续:对连续变量进行预测,例如,预测未来几天的最高温度

  • 离散:将离散的类标签分配给特定观察结果作为预测的结果,例如,在天气预报中,可以预测晴天、雨天或雪天

Spark MLlib 的美妙之处在于众多。例如,使用 Scala、Java 和 Python 实现的算法具有高度可扩展性,并利用 Spark 处理大量数据的能力。它们设计快速,用于并行计算,基于内存的操作比 MapReduce 数据处理快 100 倍(它们还支持基于磁盘的操作,比 MapReduce 普通数据处理快 10 倍),使用 Dataset、DataFrame 或基于有向无环图(DAG)的 RDD API。

它们也是多样的,因为它们涵盖了用于回归分析、分类、聚类、推荐系统、文本分析、频繁模式挖掘的常见机器学习算法,显然也涵盖了构建可扩展机器学习应用程序所需的所有步骤。

Spark ML

Spark ML 添加了一组新的机器学习 API,让用户可以快速组装和配置实用的机器学习管道,构建在数据集之上。Spark ML 旨在提供一组统一的高级 API,构建在 DataFrame 而不是 RDD 之上,帮助用户创建和调整实用的机器学习管道。Spark ML API 标准化了机器学习算法,使学习任务更容易将多个算法组合成单个管道或数据工作流,供数据科学家使用。

Spark ML 使用 DataFrame 的概念(尽管在 Java 中已经过时,但仍然是 Python 和 R 中的主要编程接口),这是在 Spark 1.3.0 版本中从 Spark SQL 引入的机器学习数据集。数据集包含各种数据类型,例如存储文本、特征向量和数据的真实标签的列。除此之外,Spark ML 还使用转换器将一个 DataFrame 转换为另一个,反之亦然,其中估计器的概念用于在 DataFrame 上拟合以生成新的转换器。另一方面,管道 API 可以将多个转换器和估计器一起约束,以指定一个 ML 数据工作流。参数的概念是在开发 ML 应用程序期间引入的,用于指定所有转换器和估计器在一个统一的 API 下共享一个公共 API:

Spark ML
ML 任务 离散 连续
监督 分类:逻辑回归决策树分类器随机森林分类器梯度提升树分类器多层感知分类器一对多分类器 回归:线性回归决策树回归随机森林回归梯度提升树回归生存回归
无监督 聚类:K 均值潜在狄利克雷分配(LDA) 树集成:随机森林梯度提升树
强化 N/A N/A
推荐系统 N/A N/A

表 2:一览 Spark ML(图例与表 1 相同)。

如表 2 所示,Spark ML 还提供了几种分类、回归、决策树和树集成,以及用于在 DataFrame 上开发 ML 管道的聚类算法。正在积极实施的优化算法称为正交有限内存拟牛顿OWL-QN),这也是一种高级算法,是 L-BFGS 的扩展,可以有效处理 L1 正则化和弹性网(也请参阅 Spark ML 高级主题,spark.apache.org/docs/latest/ml-advanced.html)。

从业者的重要说明

然而,目前仅支持 Pearson 和 Spearman 的相关性,并且将在未来的 Spark 版本中添加更多。与其他统计函数不同,Spark 还支持分层抽样,可以在 RDD 上作为键值对执行;但是,一些功能尚未添加到 Python 开发人员。目前在 Spark 机器学习库中没有强化学习算法模块(请参阅表 1表 2)。Spark MLlib 的当前实现提供了 FP-growth 的并行实现,用于挖掘频繁模式和关联规则。但是,您将需要根据需要自定义算法来挖掘最大频繁模式。我们将在即将到来的章节中提供一个可扩展的 ML 应用程序,用于挖掘隐私保护的最大频繁模式。

另一个事实是,Spark 中协同推荐系统的当前实现不支持实时流数据的使用,然而,在后面的章节中,我们将尝试基于点击流数据使用关联规则挖掘来展示一个实际的推荐系统(参见 Mitchell, Tom M. 机器学习的学科,2006 年,www.cs.cmu.edu/。CMU. Web. 2014 年 12 月)。然而,一些算法尚未添加到 Spark ML 中,最值得注意的是降维是一个例子。

然而,开发人员可以无缝地将 Spark MLlib 中找到的这些技术的实现与 Spark ML 中找到的其他算法结合起来,作为混合或可互操作的 ML 应用程序。 Spark 的神经网络和感知是基于大脑的学习算法,涵盖了多类、双类和回归问题,这些问题在 Spark ML API 中尚未实现。

实际机器学习最佳实践

在本节中,我们将描述在开发特定兴趣的机器学习应用程序之前需要遵循的一些良好的机器学习实践,如图 7所示:

实际机器学习最佳实践

图 7:机器学习系统化流程。

可扩展和准确的 ML 应用需求,需要从问题定义到呈现结果的开发中遵循系统化的方法,可以总结为四个步骤:问题定义和制定、数据准备、寻找适合的机器学习算法,最后,在机器学习模型部署后呈现结果。嗯,这些步骤可以如图 6所示。

在开发 ML 应用程序之前的最佳实践

机器学习系统的学习可以被公式化为表示、评估和优化的总和。换句话说,根据 Pedro D 等人的说法(Pedro Domingos,关于机器学习的一些有用的东西homes.cs.washington.edu/~pedrod/papers/cacm12.pdf):

学习=表示+评估+优化

考虑到这个公式,我们将在进入 ML 应用程序开发之前为从业者提供一些建议。

良好的机器学习和数据科学价值巨大

那么,在开发有效的机器学习应用程序之前,我们需要什么?实际上,在开始开发 ML 应用程序之前,我们需要四种武器,包括:

  • 数据基元(或更坦率地说,实验数据)。

  • 管道综合工具(用于理解机器学习步骤中的数据和控制流)。

  • 有效和健壮的错误分析工具。

  • 验证或验证工具(用于验证或验证 ML 模型的预测准确性或性能)。然而,最重要的是,如果没有一些具有良好数据科学的强大理论基础,整个过程将是徒劳的。事实上,许多数据科学家和机器学习专家经常引用类似于这样的声明:如果你能将你的问题提出为一个简单的优化问题,那么你几乎已经完成了(见数据分析与 Radvanceddataanalytics.net/2015/01/31/condensed-news-7/)。

这意味着在开始机器学习之前,如果你能确定你的问题是一个机器学习问题,你将能够找到一些合适的算法来一起开发你的 ML 应用。当然,在实践中,大多数机器学习应用无法转化为简单的优化问题。因此,像你这样的数据科学家的职责是管理和维护复杂的数据集。之后,你将不得不处理其他问题,比如在工程化机器学习管道时出现的分析问题,以解决我们之前提到的那些问题。

因此,最佳实践是使用 Spark MLlib、Spark ML、GraphX 和 Spark Core API 以及最佳实践的数据科学启发式方法来共同开发您的机器学习应用程序。现在你可能会想从中获益;是的,好处是显而易见的,它们如下:

  • 内置的分布式算法

  • 内存和基于磁盘的数据计算和处理

  • 迭代工作负载的内存能力

  • 算法的准确性和性能

  • 更快的数据清理、特征工程和特征选择、训练和测试

  • 预测结果的实时可视化

  • 朝着更好的性能调整

  • 适应新数据集

  • 随着数据集的增加而扩展性

最佳实践-特征工程和算法性能

在最佳实践中,特征工程应被视为机器学习中最重要的部分之一。关键是在实验数据集中非技术性地找到特征的更好表示。与此同时,使用哪些学习算法或技术也很重要。参数调整当然也很重要,但最终的选择更多取决于您将要开发的 ML 模型的实验。

在实践中,通过“开箱即用”方法(也称为功能性或 OOTB,是指产品安装或配置后立即可用的功能)和良好的数据预处理,轻松掌握天真的性能基线是微不足道的。因此,您可能会不断地这样做,以了解基线在哪里,以及这种性能是否达到了令人满意的水平或足够满足您的要求。

一旦您训练了所有的开箱即用方法,总是建议并且是一个好主意将它们一起尝试。此外,为了解决 ML 问题,您可能经常需要知道计算上困难的问题(例如第二部分中所示)需要领域特定的知识或大量挖掘数据或两者兼而有之。因此,广泛接受的特征工程技术和领域特定知识的结合将有助于您的 ML 算法/应用/系统解决与预测相关的问题。

简而言之,如果您拥有所需的数据集和一个强大的算法,可以利用数据集学习复杂的特征,几乎可以保证您会成功。此外,有时领域专家在选择好的特征时可能会出错;因此,多个领域专家(问题领域专家)、更结构化的数据和 ML 专业知识的整合总是有帮助的。

最后但同样重要的是,有时我们建议考虑错误率而不仅仅是准确性。例如,假设一个 ML 系统的准确率为 99%,错误率为 50%,比起准确率为 90%,错误率为 25%的系统更糟糕。

注意过拟合和欠拟合

初学者数据科学家经常犯的一个常见错误是在构建 ML 模型时受到过拟合问题的影响,这可能是由于听而不是泛化。更具体地说,如果您在训练数据上评估模型而不是测试或验证数据,您可能无法确定您的模型是否过拟合。常见的症状包括:

  • 用于训练的数据的预测准确性可能过高(有时甚至达到 100%)

  • 并且与新数据相比,模型可能会稍微好一些

有时 ML 模型本身对特定调整或数据点变得欠拟合,这意味着模型变得过于简单。我们的建议(我们相信其他人也是如此)如下:

  • 将数据集分为两组以检测过拟合情况,第一组用于训练和模型选择,称为训练集;第二组是用于评估模型的测试集,取代了 ML 工作流程部分中所述的模型。

  • 或者,您还可以通过使用更简单的模型(例如,线性分类器优先于高斯核 SVM)或通过增加 ML 模型的正则化参数(如果可用)来避免过拟合。

  • 调整模型的参数值以避免过拟合和欠拟合

另一方面,Hastie 等人(Hastie Trevor,Tibshirani Robert,Friedman Jerome,《统计学习的要素:数据挖掘、推断和预测》,第二版,2009 年)建议将大规模数据集分为三组:训练集(50%)、验证集(25%)和测试集(25%)(大致)。他们还建议使用训练集构建模型,并使用验证集计算预测误差。建议使用测试集来评估最终模型的泛化误差。

如果在监督学习期间可用的标记数据量较小,则不建议拆分数据集。在这种情况下,使用交叉验证或训练拆分技术(将在第七章中讨论,调整机器学习模型,并附有几个示例)。更具体地说,将数据集分为大致相等的 10 部分,然后对这 10 部分中的每一部分进行迭代训练分类器,并使用第十部分来测试模型。

保持关注并将 Spark MLlib 与 Spark ML 结合使用

管道设计的第一步是创建构建模块(作为由节点和边组成的有向或无向图)并在这些模块之间建立联系。然而,作为数据科学家,您还应专注于扩展和优化节点(基元),以便在后期处理大规模数据集时能够扩展应用程序,使您的 ML 管道始终保持高性能。管道过程还将帮助您使您的模型适应新数据集。然而,其中一些基元可能会明确定义为特定领域和数据类型(例如文本、图像、视频、音频和时空数据)。

除了这些类型的数据之外,基元还应适用于通用领域的统计学或数学。将您的 ML 模型转换为这些基元的形式将使您的工作流程更加透明、可解释、可访问和可解释。最近的一个例子是 ML-Matrix,它是一个可以在 Spark 之上使用的分布式矩阵库:

保持关注并将 Spark MLlib 与 Spark ML 结合使用

图 8:保持关注并使 ML、MLlib 和 GraphX 互操作。

正如我们在前一节中已经提到的,作为开发人员,您可以无缝地将 Spark MLlib 中的实现技术与 Spark ML、Spark SQL、GraphX 和 Spark Streaming 中开发的算法结合起来,作为基于 RDD、DataFrame 和 Datasets 的混合或可互操作的 ML 应用程序,如图 8 所示。例如,可以使用混合模型开发基于物联网的实时应用程序。因此,建议您与您周围的最新技术保持同步,以改进您的 ML 应用程序。

使 ML 应用程序模块化并简化管道合成

在构建 ML 管道时的另一个常用做法是使 ML 系统模块化。一些监督学习问题可以使用常称为广义线性模型的非常简单的模型来解决。然而,这取决于您将要使用的数据,有些数据可能不适用于这些模型。

因此,要将一系列简单的线性二元分类器合并成一个轻量级的模块化架构。这可能是在工作流程或算法级别。优势是显而易见的,因为应用程序的模块化架构以并行和分布式的方式处理大量数据流。因此,我们建议您采用文献中提到的三种关键创新机制:加权阈值抽样、逻辑校准和智能数据分区(例如,Yu Jin;Nick Duffield;Jeffrey Erman;Patrick Haffner;Subhabrata Sen;Zhi Li Zhang,《大型网络中基于流级流量分类的模块化机器学习系统》,ACM 数据发现知识交易,V-6,Issue-1,2012 年 3 月)。目标是在实现高吞吐量的同时,实现 ML 应用/系统预测结果的高准确性。虽然原语可以作为构建块,但您仍需要其他工具来使用户能够构建 ML 管道。

随后,工作流程工具如今变得更加普遍,这些工具适用于数据工程师、数据科学家,甚至适用于业务分析师,如 Alteryx、RapidMiner、Alpine Data 和 Dataiku。在这一点上,我们谈论并强调业务分析师,因为在最后阶段,您的目标客户将是一家重视您的 ML 模型的商业公司,对吧?Spark 的最新版本配备了用于构建机器学习管道的 Spark ML API,并制定了领域特定语言(参见en.wikipedia.org/wiki/Domain-specific_language)用于管道。

思考一个创新的 ML 系统

然而,为了开发算法以利用可用数据持续学习 ML 模型,机器学习背后的观点是自动化分析模型的创建。不断发展的模型产生越来越积极的结果,并减少了对人类干预的需求。这使得 ML 模型能够自动产生可靠且可重复的预测。

更具体地说,假设您计划使用 ML 算法开发推荐系统。那么,开发该推荐系统的目标是什么?在机器学习产品开发方面有哪些创新的想法?这两个问题在您开始开发 ML 应用程序或系统之前应该考虑。持续的创新可能具有挑战性,特别是在推动新想法的同时,理解最大利益所在也可能很困难。机器学习可以通过各种途径提供创新,例如确定当前产品的弱点、预测分析或识别以前隐藏的模式。

因此,您将不得不考虑大规模计算来离线训练您的 ML 模型,随后您的推荐系统必须能够像传统的搜索引擎分析一样进行在线推荐。因此,如果您的系统:

  • 可以使用您的机器学习应用程序预测购买商品

  • 可以进行产品分析

  • 可以作为生产中的新趋势

思考并变得更加聪明,以应对大数据的复杂性

如图 9 所示,新的商业模式是可利用数据的不可避免的延伸,因此考虑大数据及其商业价值可以使业务分析师的工作、生活和思维更加智能,从而使您的目标公司为客户提供价值。除此之外,您还需要调查(更准确地说是分析)竞争对手或更好的公司。

现在的问题是,你如何收集和使用企业数据?大数据不仅仅是关于大小(容量),它还与速度、真实性、多样性和价值有关。对于这些类型的复杂性,例如,速度可以使用 Spark Streaming 来解决,因为基于流的数据也是需要实时分析的大数据。其他参数,如容量和多样性,可以使用 Spark Core 和 Spark MLlib/ML 来处理大数据处理。

好吧,你必须想方设法管理数据。如果你能够管理数据,那么从数据中获得的见解可以真正改变企业运营的方式,利用大数据的有用特征:

思考和更加智能地处理大数据复杂性

图 9:大数据最佳实践中的机器学习。

在这一点上,仅有数据是不够的(参见 Pedro Domingos,《关于机器学习的一些有用知识》,homes.cs.washington.edu/~pedrod/papers/cacm12.pdf),但是从数据中提取有意义的特征并将数据的语义放入模型更为重要。这就像 LinkedIn 等大多数科技巨头正在通过大规模机器学习框架开发的社区特征定位一样,这多多少少是一种监督学习技术。工作流程如下:

  • 获取数据,提取特征,并设置目标

  • 特征和目标连接

  • 从连接数据创建一个快照

  • 将快照分成两部分:训练集和测试集

  • 从训练集中,通过采样技术准备样本数据

  • 使用采样数据训练模型

  • 评分

  • 从先前开发的持久模型以及步骤 4 中准备的测试数据中评估模型。

  • 如果找到了最佳模型

  • 为目标受众部署模型

那么接下来呢?你的模型也应该适应大规模动态数据,比如实时流式物联网数据,而且实时反馈也很重要,这样你的 ML 系统才能从错误中学习。下一小节将讨论这一点。

将机器学习应用于动态数据

原因是显而易见的,因为机器学习为物联网项目带来了具体和动态的方面。最近,机器学习在工业公司中的受欢迎程度有所提高,他们从中获利。因此,几乎每个 IT 供应商都在急速宣布物联网平台和咨询服务。但是通过物联网数据实现财务收益并不是一件容易的工作。此外,许多企业未能清楚地确定实施物联网战略将改变哪些领域。

综合考虑这些积极和消极的问题,你的 ML 模型应该适应大规模动态数据,因为大规模数据意味着数十亿条记录、大特征空间和来自稀疏问题的低正率。然而,数据是动态的,因此 ML 模型必须足够适应;否则你将面临糟糕的体验或者迷失在黑洞中。

开发 ML 应用程序后的最佳实践

ML 模型/系统开发后的最佳实践步骤包括:可视化以理解预测值,模型验证,错误和准确性分析,模型调整,模型适应和扩展以便轻松处理大规模数据集。

如何实现实时 ML 可视化

可视化提供了一个交互界面,以保持 ML 模型本身的关注。因此,如果不可视化预测结果,进一步改善 ML 应用程序的性能将变得困难。最佳实践可能是这样的:

  • 为了可视化大规模图形相关数据,可以将一些第三方工具与 GraphX 结合起来(更多内容将在第九章中讨论,流式和图形数据的高级机器学习)

  • 对于非图形数据,Spark ML 算法可以通过集成其他工具如 Apache Kafka 来发送和接收消息的回调接口:

  • 算法决定何时发送什么消息

  • 算法不关心消息是如何传递的

  • 一个任务通道用于处理从 Spark 驱动程序到 Spark 客户端或 Spark 集群节点的消息传递服务。任务通道将使用 Spark 核心在更低的抽象级别进行通信:

  • 它不关心消息的内容或消息的接收者

  • 消息从 Spark 客户端传递到浏览器或可视化客户端:

  • 我们建议同时使用 HTML5 的服务器发送事件(SSE)和 HTTP 分块响应(PUSH)。将 Spark 与这种类型的技术结合起来将在第十章中讨论,配置和使用外部库

  • 拉取是可能的;然而,它需要一个消息队列

  • 使用 JavaScript 框架进行可视化,比如Plot.ly(请参考plot.ly/)和D3.js(请参考d3js.org/

进行一些错误分析

随着算法变得更加普遍,我们需要更好的工具来构建复杂的、稳健的和稳定的机器学习系统。像 Apache Spark 这样的流行分布式框架将这些想法应用到了更广泛的大型数据集中。因此,如果我们能够绑定分层管道的近似误差和收敛速度,那将更好。

假设我们可以计算节点的误差范围,下一步将是为这些管道提取误差范围的机制。然而,在实践中,当 ML 模型部署到生产环境时,我们可能需要工具来确认管道将正常工作,不会出现故障或中途停止,并且可以提供一些预期的错误度量。

保持你的 ML 应用程序调优

设计一个或两个在简单问题上表现良好的算法可以被认为是一个良好的开端。然而,有时你可能渴望获得最佳的准确性,甚至会牺牲宝贵的时间和可用的计算资源。这将是一个更明智的方式,它不仅可以帮助你挤出额外的性能,还可以改善你之前设计的机器学习算法的准确性结果。为了做到这一点,当你调整模型和相关算法时,你必须对结果有很高的信心。

显然,这些结果将在你指定测试和验证之后可用。这意味着你应该只使用那些减少性能测量方差的技术,以便评估那些运行更顺利的算法。

与大多数数据从业者一样,我们还建议您使用交叉验证技术(也经常称为旋转估计),并且使用相当高数量的折叠(即 K 折交叉验证,其中一个子样本用作验证数据集,用于测试模型本身,其余的 K-1 个子样本用于训练数据)。尽管折叠的确切数量,或 K,取决于你的数据集,但是 10 折交叉验证通常被使用,但是 K 的值通常是不固定的。我们将在这里提到三种策略,你需要调整你的机器学习模型:

  • 算法调优:使您的机器学习算法参数化。然后,调整这些参数的值(如果它们有多个参数)以影响整个学习过程的结果。

  • 集成:有时候天真是好的!因此,为了获得改进的结果,不断尝试将多个机器学习方法或算法的结果结合起来。

  • 极端特征工程:如果您的数据中嵌入了复杂和多维结构,ML 算法知道如何找到并利用它来做出决策。

使您的 ML 应用程序适应和扩展

如图 10 所示,自适应学习根据 Rob Munro 的说法,将基于规则的、简单的机器学习和深度学习方法融合到机器智能中。

使您的 ML 应用程序适应和扩展

图 10:机器智能的四代(图由 Rob Munro 提供)。

机器学习的第四代:自适应学习,(http://idibon.com/the-fourth-generation-of-machine-learning-adaptive-learning/#comment-175958)。

研究还表明,自适应学习在预测人们购买汽车的意图方面准确率达到 95%(请参阅 Rob Munro,《机器学习的第四代:自适应学习》,http://idibon.com/the-fourth-generation-of-machine-learning-adaptive-learning/#comment-175958)。此外,如果您的 ML 应用程序能够适应新环境和新数据,那么只要提供足够的基础设施,预计您的 ML 系统可以扩展以处理不断增加的数据负载。

为您的应用程序选择正确的算法

我应该使用什么机器学习算法?对于天真的机器学习从业者来说,这是一个非常常见的问题,但答案总是取决于。更详细地说:

  • 这取决于要测试/使用的数据的数量、质量、复杂性和性质

  • 这取决于外部环境和参数,例如您的计算系统配置或基础设施

  • 这取决于您想要用答案做什么

  • 这取决于算法的数学和统计公式如何被转化为计算机的机器指令

  • 这取决于你有多少时间

  • 图 11提供了选择解决 ML 问题的正确算法的完整工作流程。但是,请注意,某些技巧可能会根据数据和问题类型而不起作用:为您的应用程序选择正确的算法

图 11:选择正确算法的工作流程

事实上,即使是最有经验的数据科学家或数据工程师在尝试所有算法之前也无法给出哪种 ML 算法在性能上表现最佳的明确建议。大多数一致/不一致的陈述都以取决于...嗯...开始。习惯上,您可能会思考是否有机器学习算法的备忘单,如果有的话,如何使用该备忘单。我们与几位数据科学家交谈时,他们表示找到最佳算法的唯一方法是尝试所有算法;因此,没有捷径!让我们明确一下,假设您有一组数据,并且想要进行一些聚类。因此,从技术上讲,这可能是分类或回归,如果您的数据是标记/未标记的或值或训练集数据。现在,您脑海中首先出现的问题是:

  • 在选择适当的算法之前,我应该考虑哪些因素?还是我应该随机选择一个算法?

  • 我如何选择可以应用于我的数据的任何数据预处理算法或工具?

  • 我应该使用什么样的特征工程技术来提取有用的特征?

  • 哪些因素可以提高我的 ML 模型的性能?

  • 我如何为新数据类型采用我的 ML 应用程序?

  • 我可以将我的 ML 应用程序扩展到大规模数据集吗?等等。

您总是期望得到更合理的最佳答案,并解释应考虑的一切。在本节中,我们将尝试用我们的一点机器学习知识来回答这些问题。

在选择算法时的考虑

我们在这里提供的建议或建议是给初学者数据科学家和尝试选择 Spark ML API 的最佳算法的专家数据科学家。这意味着它做了一些概述和过度简化,但它会指引您朝着安全的方向,相信我们!假设您计划开发一个 ML 系统来回答以下问题基于规则:

  • IF特征 X 具有属性 ZTHEN执行 Y

肯定地,应该有这样的规则:

  • 如果 XTHEN,尝试使用属性 Z 并避免 W 是明智的

然而,什么是明智的,什么不是取决于:

  • 您的应用程序和问题的预期复杂性。

  • 数据集的大小(即有多少行/列,有多少独立案例)。

  • 您的数据集是否有标签或无标签?

  • 数据类型和测量类型,因为不同类型的数据暗示着不同的顺序或结构,对吧?

  • 显然,在实践中,您在应用不同方法时的经验是高效和智能的。

此外,如果您想对一般问题得到一般答案,我们建议初学者从《统计学习的要素》(Hastie Trevor,Tibshirani Robert,Friedman Jerome,统计学习的要素:数据挖掘、推断和预测,第二版,2009)开始。然而,我们还建议遵循以下算法属性:

  • 展示出色的准确性

  • 具有快速的训练时间

  • 以及线性的使用

准确性

从您的 ML 应用程序中获得最准确的结果并非总是不可或缺的。根据您想要将其用于的情况,有时近似也足够。如果情况是这样,您可以通过合并更好估计的方法来大幅减少处理时间。当您熟悉 Spark 机器学习 API 的工作流程时,您将享受到拥有更多近似方法的优势,因为这些近似方法将自动倾向于避免您的 ML 模型中的过度拟合问题。

训练时间

执行时间需要完成数据预处理或构建模型,并且在不同算法、继承复杂性和鲁棒性之间变化很大。训练时间通常与准确性密切相关。此外,您经常会发现,与其他算法相比,您将使用的一些算法对数据点的数量是难以捉摸的。然而,当您的时间足够充裕,特别是当数据集较大时,为了完成所有的程序,选择算法可能会变得轻松。因此,如果您特别关注时间,尝试牺牲准确性或性能,并使用满足您最低要求的简单算法。

线性

最近开发了许多利用线性的机器学习算法(也可在 Spark MLlib 和 Spark ML 中使用)。例如,线性分类算法允许通过绘制区分直线或数据集的高维等价物来分离类别。另一方面,线性回归算法假设数据趋势遵循简单的直线。对于一些机器学习问题,这种假设并不天真;然而,在其他一些情况下,准确性可能会下降。尽管存在危险,线性算法对于数据工程师或数据科学家来说是首选。此外,这些算法在整个训练过程中也倾向于算法简单且训练速度快。

在选择算法时与您的数据交谈

你可以在machinelearningmastery.com/tour-of-real-world-machine-learning-problems/或 UC Irvine 机器学习库(archive.ics.uci.edu/ml/)免费找到许多机器学习数据集。还应首先考虑以下数据属性:

  • 参数数量

  • 特征数量

  • 训练数据集的大小

参数数量

参数或数据属性是像你这样的数据科学家在设置算法时可以调整的手段。它们是影响算法性能的数字,例如误差容限或迭代次数,或者是算法行为的变体之间的选项。算法的训练时间和准确性有时对于找到合适的设置非常敏感。通常,具有大量参数的算法需要通过试错来找到最佳组合。

尽管这是跨越参数空间的好方法,但随着参数数量的增加,模型构建或训练时间呈指数增长。这既是一个困境,也是一个时间性能的权衡。积极的一面是,拥有许多参数通常表示机器学习算法的更大灵活性。其次,你的机器学习应用可以获得更好的准确性。

你的训练集有多大?

如果你的训练集较小,偏差较高且方差较低的分类器(如朴素贝叶斯)比偏差较低且方差较高的分类器(如 kNN)具有优势。因此,后者会过拟合。但是,偏差较低且方差较高的分类器在你的训练集线性或指数增长时开始占优势,因为它们具有更低的渐近误差。这是因为高偏差的分类器不足以提供准确的模型。你也可以将其视为生成模型与判别模型之间的权衡。

特征数量

对于某些类型的实验数据集,提取的特征数量可能与数据点数量本身相比非常大。这在基因组学、生物医学或文本数据中经常发生。大量特征可能会淹没一些学习算法,使训练时间变得非常长。支持向量机在这种情况下特别适用,因为它具有高准确性、关于过拟合的良好理论保证以及适当的核函数。

广泛使用的机器学习算法的特殊说明

在这一部分,我们将为最常用的机器学习算法或技术提供一些特殊说明。我们将重点介绍的技术包括逻辑回归、线性回归、推荐系统、支持向量机、决策树、随机森林、贝叶斯方法和决策森林、决策丛林以及变种。表 3 显示了一些广泛使用的算法的优缺点,包括何时选择这些算法。

算法 优点 缺点 擅长
线性回归(LR) 非常快,通常在恒定时间内运行易于理解建模不太容易过拟合和欠拟合本质上简单速度非常快,因此建模时间较短不太容易过拟合和欠拟合方差较低 通常无法进行复杂的数据建模无法概念化非线性关系,需要转换输入数据集不适合复杂建模仅适用于单一决策边界需要大样本量才能获得稳定的结果偏差较高 具有大量特征的数值数据集广泛用于生物学、行为学和社会科学,以预测变量之间可能的关系对数值和分类变量都有效用于医学和社会科学等各个领域
决策树(DT) 模型构建和预测时间较短,对噪声和缺失值具有鲁棒性,准确性高 大型和复杂树的解释困难,同一子树内可能出现重复,对角决策边界可能存在问题 针对高准确的分类、医学诊断和预后、信用风险分析
神经网络(NN) 非常强大和稳健,能够建模非常复杂的关系,可以在不知道基础数据的情况下工作 容易过拟合和欠拟合,训练和预测时间长,计算成本高,模型不可读或可重复使用 图像处理、视频处理、人工智能、机器人、深度学习
随机森林(RF) 适用于装袋树,方差低,准确性高,可以处理过拟合问题 不易直观解释,训练和预测时间长 处理可能相关的多个特征、生物医学诊断和预后、可用于分类和回归
支持向量机(SVM) 准确性高 容易过拟合和欠拟合,数值稳定性差,计算成本高,需要大量计算资源 图像分类、手写识别
K 最近邻(K-NN) 简单而强大,需要懒惰训练,可用于多类分类和回归 训练和预测时间长,需要准确的距离函数,高维数据集性能低 低维数据集、异常检测、半导体故障检测、基因表达、蛋白质相互作用
K-means 线性执行时间表现优于分层聚类,对超球状聚类效果更好 可重复但缺乏一致性,需要先验知识 如果数据集中出现的自然聚类是非球状的,则不是一个好选择,适用于大型数据集
潜在狄利克雷分配(LDA) 可应用于大规模文本数据集,可以克服 pLSA 的过拟合问题,可用于文档分类和通过主题建模进行聚类 不能应用于高维和复杂的文本数据库,需要指定主题数量,无法找到最佳级别,层次狄利克雷过程(HDP)是更好的选择 从大规模文本数据集中进行文档分类和通过主题建模进行聚类,可应用于自然语言处理和其他文本分析
朴素贝叶斯(NB) 计算速度快,实现简单,适用于高维数据,可以处理缺失值,适应性强,模型可以根据新的训练数据进行修改而无需重建 依赖独立性假设,如果假设不成立则表现不佳,准确性相对较低 当数据有大量缺失值、特征之间的依赖关系类似、垃圾邮件过滤和分类、对科技、政治或体育新闻文章进行分类、文本挖掘
奇异值分解(SVD)和主成分分析(PCA) 反映了关于数据的真实直觉,可以在高维数据中估计概率,数据大小显著减少,两者都基于强大的线性代数 对于像 Twitter 和网络分析这样的许多应用来说太昂贵,对于细粒度类别的任务来说灾难性,需要正确理解线性性,复杂度通常是立方的,计算速度较慢 SVD 用于低秩矩阵逼近、图像处理、生物信息学、信号处理、NLP,PCA 用于利率衍生品投资组合、神经科学等,两者都适用于具有高维和多变量数据的数据集

表 3:一些广泛使用算法的优缺点

逻辑回归和线性回归

逻辑回归是一种强大的工具,因为它快速且简单,已在全球范围内用于两类和多类分类。事实上,它使用S形曲线而不是直线,使其自然适合将数据分成组。它提供线性类边界,因此在使用它时,请确保线性逼近是您可以接受的。与决策树或 SVM 不同,它还具有良好的概率解释,因此您将能够轻松更新模型以适应新数据集。

因此,建议使用它,如果您希望体验概率框架的味道,或者期望将来获得更多的训练数据并将其纳入您的模型。如前所述,线性回归将一条直线、平面或超平面拟合到数据集。它是一个实用、简单且快速的工具,但对于某些问题可能过于简单。

推荐系统

我们已经讨论了大多数常用的机器学习算法和工具的准确性和性能问题。然而,除了准确性研究之外,对推荐系统的另一个关注点是寻找其他环境因素和/或参数多样性。因此,一个准确性高且列表内多样性高的推荐系统将是赢家。因此,您的产品将对目标客户非常宝贵。然而,让用户重新对物品进行评分,而不仅仅是显示新物品,可能会更有效。如果您的客户有一些需要满足的额外要求,比如隐私或安全性,您的系统必须能够处理与隐私相关的问题。

这一点特别重要,因为客户必须提供一些个人信息,因此建议不要公开这些敏感信息。

然而,使用一些强大的技术或算法(如协同过滤)来构建用户档案可能会从隐私角度带来问题。此外,该领域的研究发现,用户人口统计信息可能会影响其他用户对推荐的满意程度(另请参阅 Joeran Beel、Stefan Langer、Andreas Nürnberger、Marcel Genzmehr,《人口统计信息(年龄和性别)和其他用户特征对评估推荐系统的影响》,在 Trond Aalberg 和 Milena Dobreva 和 Christos Papatheodorou 和 Giannis Tsakonas 和 Charles Farrugia 的《第 17 届数字图书馆理论与实践国际会议论文集》,Springer,第 400-404 页,2013 年 11 月 1 日检索)。

尽管偶然性是衡量推荐有多么令人惊讶的关键指标,但最终建立信任还是需要通过推荐系统。这可以通过解释它是如何生成推荐的,以及为什么会推荐一个物品,即使用户的人口统计信息很少,来实现。

因此,如果用户根本不信任系统,他们将不会提供任何人口统计信息,也不会重新对物品进行评分。根据Cowley 等人(G.C.Cawley 和 N.L.C.Talbot,《模型选择中的过拟合和性能评估中的后续选择偏差》,《机器学习研究杂志》,第 11 卷,第 2079-2107 页,2010 年 7 月),支持向量机有几个优点:

  • 您可以通过 SVM 提供的正则化参数来解决过拟合问题

  • SVM 使用核技巧来帮助轻松构建机器学习模型

  • SVM 算法是基于凸优化问题开发、设计和定义的,因此没有局部最小值的概念

  • 这是一个对测试错误率的边界的大致估计,其中有一个重要且深入研究的理论可以发挥作用

SVM 的这些有前途的特性确实会帮助您,建议经常使用。另一方面,缺点是:

  • 理论只能真正涵盖对给定的正则化和核参数值的参数确定。因此,你只能选择核。

  • 也可能存在更糟糕的情况,核模型本身在模型选择标准期间可能非常敏感于过拟合。

决策树

决策树很酷,因为它们易于解释和解释围绕机器学习问题。与此同时,它们可以很容易地处理与特征相关的交互。最重要的是,它们通常是非参数的。因此,即使你是一个工作能力有限的普通数据科学家,你也不需要担心异常值、参数设置和调整等问题。有时,基本上,你可以依赖决策树,这样它们将减轻你处理数据线性问题的压力,或者更技术上说,你的数据是否是线性可分的,你不需要担心。相反,也有一些缺点。例如:

  • 在某些情况下,决策树可能不合适,有时它们不支持实时数据集的在线学习。在这种情况下,当出现新的示例或数据集时,你必须重新构建你的树;更技术上说,获得模型的适应性是不可能的。

  • 其次,如果你没有意识到,它们很容易过拟合。

随机森林

随机森林非常受欢迎,对于数据科学家来说是一个赢家,因为它们对于大量分类问题来说是非常好用的。它们通常在可用性方面略领先于支持向量机,并且对于大多数分类问题的操作速度更快。此外,它们在增加可用数据集时也是可扩展的。与此同时,你不需要担心调整一系列参数。相反,当处理数据时,你需要关注许多参数和调整。

决策森林、决策丛林和变体

决策森林、决策丛林和提升决策树都是基于决策树的,决策树是一个基础的机器学习概念,使用较少。决策树有许多变体;尽管如此,它们都做同样的事情,即将特征空间细分为具有相同标签的区域。为了避免过拟合问题,使用数学和统计公式构建了大量的树,这些树之间没有任何相关性。

其平均值被称为决策森林;这是一种避免过拟合问题的树,如前所述。然而,决策森林可能会使用大量内存。另一方面,决策丛林是一种通过牺牲略长的训练时间来消耗较少内存的变体。幸运的是,提升决策树通过限制分区的数量和每个区域允许的数据点数量来避免过拟合。

贝叶斯方法

当实验或样本数据集规模较大时,贝叶斯方法通常会为参数模型提供与其他经典统计方法产生的结果非常相似的结果。使用贝叶斯方法的一些潜在优势由 Elam 等人总结(W.T. Elam, B. Scruggs, F. Eggert, and J.A. Nicolosi,《获取 XRF NET 强度方法的优缺点》,版权所有©JCPDS-国际衍射数据中心 2011 ISSN 1097-0002)。例如,它提供了一种将先验信息与数据结合的自然方式。因此,作为一名数据科学家,你可以将过去关于参数的信息并入未来分析新数据集的先验分布。它还提供了在不需要算法渐近逼近的情况下,条件于数据的推断。

它为各种模型提供了一些合适的设置,比如层次模型和缺失数据问题。使用贝叶斯分析也有一些缺点。例如,它不告诉你如何选择先验世界模型,甚至没有正确选择先验的方法。因此,如果你不小心进行,你可能会产生许多伪阳性或伪阴性的结果,这往往伴随着高昂的计算成本,如果模型中的参数数量很大的话。

总结

这结束了我们对机器学习和需要遵循的最佳实践的相当快速的介绍。虽然我们试图涵盖一些最基本的要点,但合适的数据往往胜过更好的算法和更高的需求。最重要的是,从数据中设计出好的特征可能需要很长时间;然而,这将对你非常有帮助。然而,如果你有一个大规模的数据集要应用到你的机器学习算法或模型中,无论你使用哪种分类、聚类或回归算法,都可能不是关于机器学习类别及其相应的分类性能的事实。

因此,选择一个能够满足速度、内存使用、吞吐量、可扩展性或可用性等要求的合适的机器学习算法将是一个明智的决定。除了我们在上面的部分中所说的内容之外,如果你真的关心准确性,你应该毫无疑问地尝试一组不同的分类器,使用交叉验证技术找到最佳的一个,或者使用集成方法来一起选择它们。

你也可以从 Netflix Prize PLUS 中得到启发并吸取教训。我们详细讨论了 Spark 机器学习 API、ML 应用开发中的一些最佳实践、机器学习任务和类别、一些广泛使用的最佳实践等等。然而,我们并没有深入分析机器学习技术。我们打算在第四章中更详细地讨论这个问题,通过特征工程提取知识

在下一章中,我们将详细介绍 DataFrame、Dataset 和Resilient Distributed DatasetRDD)API,以处理结构化数据,旨在提供对可用数据进行机器学习问题的基本理解。因此,最终,你将能够轻松地应用从基本到复杂的数据操作。

第三章:通过了解数据来了解问题

本章将详细介绍 DataFrame、Datasets 和Resilient Distributed DatasetRDD)API,用于处理结构化数据,旨在提供对可用数据进行机器学习问题的基本理解。在本章结束时,您将能够轻松应用从基本到复杂的数据操作。将提供一些比较,使用 RDD、DataFrame 和 Dataset 进行数据操作的基本抽象,以展示在编程和性能方面的收益。此外,我们将指导您走上正确的道路,以便您能够使用 Spark 将 RDD 或数据对象持久化在内存中,从而在后期的并行操作中高效地重复使用。简而言之,本章将涵盖以下主题:

  • 分析和准备您的数据

  • Resilient Distributed Dataset(RDD)基础知识

  • 数据集基础知识

  • 来自字符串和类型类的数据集

  • Spark 和数据科学家,工作流程

  • 深入 Spark

分析和准备您的数据

在实践中,有几个因素会影响给定任务上机器学习(ML)应用的成功。因此,实验数据集的表示和质量首先被视为一流实体。拥有更好的数据总是明智的。例如,不相关和冗余的数据,具有空值或嘈杂数据的数据特征会导致不可靠的信息来源。数据集中的不良属性使得在机器学习模型训练阶段的知识发现过程更加繁琐和时间低效。

因此,数据预处理将在总体 ML 工作流程步骤中占据相当大的计算时间。正如我们在上一章中所述,除非您了解可用数据,否则很难理解问题本身。此外,了解数据将帮助您制定问题。同时,更重要的是,在尝试将 ML 算法应用于问题之前,首先您必须确定问题是否真的是一个机器学习问题,以及 ML 算法是否可以直接应用于解决问题。您需要采取的下一步是了解机器学习类别。更具体地说,您需要知道已识别的问题是否属于分类、聚类、规则重构或回归类别。

为了简单起见,我们假设您有一个机器学习问题。现在,您需要进行一些数据预处理,包括一些步骤,如数据清理、归一化、转换、特征提取和选择。数据预处理工作流程步骤的产物通常用于构建/训练 ML 模型的最终训练集。

在上一章中,我们还论证了机器学习算法是从数据和模型构建和反馈活动中学习的。关键是,您需要为您想要解决的问题为算法提供正确的数据。即使您拥有良好的数据(或者更准确地说是结构良好的数据),您也需要确保数据处于适当的规模,并且具有编程语言可以解析的良好格式,最重要的是,是否还包括最有意义的特征。

在本节中,您将学习如何准备数据,使您的机器学习算法对最佳性能变得自发。总体数据处理是一个庞大的主题;然而,我们将尝试覆盖一些基本技术,以便在第六章构建可扩展的机器学习管道中进行一些大规模的机器学习应用。

数据准备过程

如果您在数据处理和准备步骤中更加专注和纪律,您很可能会在第一时间获得更一致和更好的结果。然而,数据准备是一个包含多个步骤的繁琐过程。然而,为了让数据准备好用于机器学习算法,可以总结为三个步骤:

  • 数据选择

  • 数据预处理

  • 数据转换

数据选择

这一步将专注于选择您将在机器学习应用程序开发和部署中使用和处理的所有可用数据集的子集。在机器学习应用程序开发中,总是有一种强烈的冲动,即包含所有可用数据,因为更多的数据将提供更多的特征。换句话说,按照众所周知的格言,越多越好。然而,实际上,在所有情况下,这可能并不正确。在实际回答问题之前,您需要考虑需要哪些数据。最终目标是提供特定假设的解决方案。在一开始,您可能对数据做出一些假设。虽然这很困难,但如果您是该问题的领域专家,您可以在应用 ML 算法之前做出一些假设以至少了解一些见解。但是,要小心记录这些假设,以便在需要时在以后的阶段进行测试。我们将提出一些常见问题,以帮助您思考数据选择过程:

  • 第一个问题是,您可用的数据范围是多少?例如,范围可能是整个时间、数据库表、连接的系统文件等。因此,最好的做法是确保您清楚地了解并低级结构化您可以使用的一切,或者非正式地持有可用资源(当然包括可用的数据和计算资源)。

  • 第二个问题有点奇怪!哪些数据尚未可用,但对解决问题很重要?在这种情况下,您可能需要等待数据可用,或者您可以至少使用一些生成器或软件生成或模拟这些类型的数据。

  • 第三个问题可能是:您不需要哪些数据来解决问题?这意味着再次排除冗余数据,因此排除这些冗余或不需要的数据几乎总是比全部包含它更容易。您可能会想知道是否需要记录排除的数据以及原因?我们认为应该是的,因为您可能在以后的阶段需要一些琐碎的数据。

此外,在实践中,对于小问题或游戏,玩具竞赛数据已经为您选择好了;因此,您无需担心!

数据预处理

在选择要处理的数据后,您需要考虑如何使用数据和所需的适当利用。这个预处理步骤将解决一些步骤或技术,以便将所选数据转换为您可以在模型构建和验证步骤中使用和应用的形式。最常用的三个数据预处理步骤是格式化、清理和抽样数据:

  • 格式化:所选数据可能不够完善,因此可能不适合直接使用。您的数据很可能是原始数据格式(如文本格式或较少使用的专有格式的平面文件格式),如果您足够幸运,数据可能是在关系数据库中。如果是这种情况,最好应用一些转换步骤(即,例如将关系数据库转换为其格式,因为使用 Spark 您无法进行任何转换)。正如已经说明的,Spark 的美妙之处在于其对多种文件格式的支持。因此,我们将能够在接下来的部分中利用这一点。

  • 清洗:您将要使用的数据往往带有许多不需要的记录,有时甚至有缺失的记录。这个清洗过程涉及到删除或修复缺失的数据。可能总会有一些微不足道或不完整的数据对象,处理它们应该是首要任务。因此,这些实例可能需要从数据集中删除、忽略或删除以摆脱这个问题。此外,如果由于某些属性中存在敏感信息的存在而导致隐私或安全成为问题,那么这些属性需要被匿名化或从数据中完全删除(如果适用)。

  • 抽样:第三步将是在格式化和清理的数据集上进行抽样。由于可用数据量可能很大或记录数量很多,因此通常需要抽样。然而,我们建议尽可能使用数据。另一个原因是更多的数据可能导致整个机器学习过程的执行时间更长。如果是这种情况,这也会增加算法的运行时间,并需要更强大的计算基础设施。因此,您可以对所选数据进行较小的代表性样本,这样在考虑整个数据集之前,探索和原型化机器学习解决方案可能会更快。显然,无论您为机器学习应用开发和商业化应用的机器学习工具,数据都将影响您需要执行的预处理。

数据转换

在选择适当的数据源并对这些数据进行预处理后,最后一步是转换已处理的数据。您特定的机器学习算法和对问题领域的了解将在这一步中受到影响。三种常见的数据转换技术是属性缩放、分解和属性聚合。这一步通常也被称为特征工程,在下一章中将更详细地讨论:

  • 缩放:预处理的数据可能包含具有各种数量和单位的混合比例的属性,例如美元、千克和销售量。然而,机器学习方法要求数据属性在相同的比例内,例如对于给定特征的最小值和最大值之间的 0 到 1 之间。因此,考虑您可能需要执行适当的特征缩放来对已处理的数据进行适当的缩放。

  • 分解:数据可能具有一些代表复杂概念的特征,当您将数据集分解为基本部分时,可以使机器学习算法产生更强大的响应。例如,考虑一天由 24 小时、1,440 分钟和 86,400 秒组成,这些时间又可以进一步分解。可能一些特定的小时或一天中的小时对于需要调查和解决的问题是相关的。因此,考虑适当的特征提取和选择来执行已处理数据的适当分解。

  • 聚合:通常,分散或零散的特征可能在其自身上是微不足道的。然而,这些特征可以被聚合成一个更有意义的特征,对您试图解决的问题更有意义。例如,一些数据实例可以在在线购物网站上呈现,每次客户登录网站时都会产生数据对象。这些数据对象可以通过丢弃额外的实例来聚合成登录次数的计数。因此,考虑适当的特征聚合来正确处理数据。

Apache Spark 具有其分布式数据结构,包括 RDD、DataFrame 和 Datasets,您可以使用这些数据结构高效地进行数据预处理。这些数据结构在处理数据时具有不同的优势和性能。在接下来的章节中,我们将分别描述这些数据结构,并展示如何使用它们处理大型数据集的示例。

弹性分布式数据集基础知识

在第一章中,使用 Spark 进行数据分析简介,我们简要描述了弹性分布式数据集,包括数据转换和操作以及缓存机制。我们还指出 RDD 基本上是一个不可变的记录集合,只能通过 map、filter、group by 等操作来创建。在本章中,我们将使用 Spark 的这种本机数据结构进行数据操作和数据预处理,用于一个常被称为垃圾邮件过滤的实际机器学习应用。Spark 还提供了另外两个更高级别的 API,如 DataFrame 和 Datasets,用于数据操作。

然而,我们将展示包括 RDD 在内的所有 API,因为您可能需要这个 API 来处理更复杂的数据操作。我们已经从 Spark 编程指南中引用了一些关于 Spark 操作和操作的常用定义。

正如我们已经讨论过使用操作和转换的 RDD 操作的一些基础知识。RDD 可以通过稳定的存储(如 Hadoop 分布式文件系统(HDFS))和对现有 RDD 的转换来创建。Spark 定期记录这些转换,而不是实际数据,因此从技术上讲,原始 RDD 和数据集不会发生变化。

可以从现有数据集创建一个转换后的数据集;但是在 Spark 中不可能反过来。在数据集上完成计算后,操作将返回一个值给驱动程序。例如,根据 Spark 编程指南,map 是一种通过函数传递每个数据集元素并返回一个全新的 RDD 来表示和保存结果的转换。相反,reduce 也是一种通过函数聚合 RDD 的所有元素并返回一个全新的 RDD 作为最终结果返回给驱动程序的操作。

更具体地说,假设我们有一个包含以逗号分隔的数字序列的文本文件(即 CSV 文件)。现在在读取完毕后,您将拥有一个 RDD,因此您可能希望计算每个数字的频率。为此,您需要将 RDD 转换为键值对,其中键是数字,值将是每个数字的频率。

另一方面,您可能需要通过一些操作在驱动程序中收集结果。在接下来的几节中,我们将通过基于实际机器学习问题的一些示例,提供有关转换和操作等一些有用主题的更多细节。

读取数据集

为了从不同的数据源(如本地文件系统、HDFS、Cassandra、HBase 等)读取数据集,Spark 提供了易于使用的不同 API。它支持包括文本文件、序列文件、Hadoop 输入格式、CSV、TSV、TXT、MD、JSON 和其他数据格式在内的不同数据表示。输入 API 或方法支持在压缩文件、目录和通配符上运行。例如,表 1显示了读取格式的列表。textFile()方法从目录/my/directory中读取不同的文件格式,如.txt.gz

textFile("/my/directory"),textFile("/my/directory/.txt"),textFile("/my/directory/.gz").

表 1:读取文件格式

从文件中读取

您可能需要从本地或 HDFS 读取数据集。以下代码展示了从存储在本地机器或 HDFS 上的给定数据集创建 RDD 的不同方法。

然而,在使用 Spark 进行读取和写入之前,我们需要通过 Spark 会话创建 Spark 入口点,可以通过以下方式实例化:

static SparkSession spark = SparkSession 
      .builder() 
      .appName("JavaLDAExample") 
      .master("local[*]") 
      .config("spark.sql.warehouse.dir", "E:/Exp/") 
      .getOrCreate(); 

在这里,Spark SQL 仓库设置为E:/Exp/路径。您应该根据您所在的操作系统类型设置您的路径。好了,现在我们有了变量spark作为我们的 Spark 会话,让我们看看如何轻松地使用它来从文本文件中读取。

从文本文件中读取

它使用SparkContext()textFile()方法,并返回一个包含行集合的字符串的 RDD。在第一章中,我们解释了什么是 SparkContext。尽管如此,Spark Context 是 Spark 应用程序的入口点。假设我们有一个名为1.txt的数据集,其中包含一些推文数据作为非结构化文本。您可以从 Packt 材料中下载数据,并将其存储在project_path/input/test/目录下,定义如下:

String csvFile = "input/test/1.txt"; 
RDD<String> distFile = spark.sparkContext().textFile(csvFile, 2); 

在这里,我们已经创建了一个存储在变量distFile中的字符串的两个分区的 RDD。但是,要使用 Java,RDD 必须转换为 JavaRDD。通过调用toJavaRDD()方法来实现:

JavaRDD<String> distFile2 = distFile.toJavaRDD(); 

从目录中读取多个文本文件

它将返回 RDD 作为(文件名和内容)对。假设我们有多个文件存储在csvFiles/目录中以供读取,定义如下:

RDD<Tuple2<String, String>> distFile = spark.sparkContext().wholeTextFiles("csvFiles/*.txt", 2); 
JavaRDD<Tuple2<String, String>> distFile2 = distFile.toJavaRDD(); 

请注意,当 RDD 中的数据对象不存储在主内存或 HDD 中时,我们需要对 RDD 执行分区以增加并行性。

从现有集合中读取

创建 RDD 的第二个来源是从驱动程序程序的集合中,例如包含整数的列表。在深入讨论之前,让我们以另一种方式初始化 Spark,如下所示:

SparkConf conf = new SparkConf().setAppName("SampleAppliation").setMaster("local[*]"); 
JavaSparkContext sc = new JavaSparkContext(conf); 

这里 Java Spark 上下文可用作变量sc。这一次,我们已经创建了 Spark 上下文,因此我们将能够创建字符串的 Java RDD,而无需使用toJavaRDD()方法。

现在,您可以使用 Spark Context 的并行化方法来实现:

  • 读取整数列表:它返回一个并行化的整数 RDD:
      List<Integer> list = Arrays.asList(1,2,3,4,5); 
      JavaRDD<Integer> rdd = sc.parallelize(list); 

  • 读取成对列表:它返回一个并行化的pairRDD,其中包含成对的整数和字符串:
      List<Tuple2<Integer, String>> pairs = Arrays.asList( 
                new Tuple2<>(1, "Hello"), 
                new Tuple2<>(2, "World"), 
                new Tuple2<>(3, "How are you?")); 
      JavaPairRDD<Integer, String> pairRDD = sc.parallelizePairs(pairs); 

RDD 的预处理

为了继续讨论我们在上一节开始的数据预处理,我们将在本节中展示一个机器学习问题的示例,以及如何使用 RDD 对数据集进行预处理。

我们正在考虑的是垃圾邮件过滤器应用程序,这是一个受欢迎的监督学习问题的典型示例。问题是从传入的电子邮件中预测和识别垃圾邮件消息(请参阅表 2)。通常,为了训练模型,您必须使用历史数据(您在几天、几小时或几个月甚至一年内收到的历史电子邮件)来训练模型。预处理任务的最终输出是制作特征向量或提取包括其标签或类别的特征。通常,您可能会执行以下步骤:

  • 停用词去除:文本文件可能包含一些对于特征向量无用或多余的单词,例如andtheof,因为这些单词在所有形式的英语句子中都非常常见。另一个原因是它们在决定垃圾邮件或正常邮件状态时没有太多意义,或者它们可能包含微不足道的意义。因此,在进行下一步之前,需要从电子邮件数据集中过滤掉这些单词。

  • 词形还原:一些具有相同含义但不同结尾的单词需要被调整,以使它们在整个数据集中保持一致,如果它们都具有相同的形式,将更容易将它们转换为特征向量。例如,attachedattachmentattach都可以表示为电子邮件attachmentsSMSSpamCollection数据集可以从 UCI ML 存储库archive.ics.uci.edu/ml/machine-learning-databases/00228/下载。

请注意,在这个阶段,电子邮件正文中的所有单词通常都会被转换为小写。现在,让我们来看一下下表:

ham: 你在干什么?你好吗?ham: 好的啦...只是和你开玩笑。ham: 不要这么早说...你已经看到了然后说。ham: 我的号码在卢顿 0125698789,如果你在附近给我打电话!H*spam: 免费信息:短信:拨打 86888 号码,领取您的 3 小时通话时间奖励,现在可以在您的手机上使用!订阅 6 英镑/月,包括 3 小时 16 次停止?txtStop ham: 西瓦在宿舍哈哈。ham: 因为我刚才和达伦出去购物,我给他打电话问他想要什么礼物。然后他开始猜我和谁在一起,最后他猜到了是达伦。spam: 阳光测验!如果你能说出澳大利亚的首都,就赢得一个超级索尼 DVD 录像机!发送 MQUIZ 到 82277. B

表 2:包含正常邮件和垃圾邮件消息的训练集的测试文件

  • 去除非单词:数字和标点也必须被去除。然而,由于页面限制和简洁性,我们不会在这里展示所有可能的预处理数据的转换,但我们将尝试展示一些基本的预处理数据的转换和操作,其中包含一些标签数据,如表 2中所示。数据集或电子邮件被标记为正常邮件或垃圾邮件,后面跟着消息或电子邮件。正常邮件意味着非垃圾邮件,垃圾邮件被识别为垃圾邮件消息。

使用 RDD 进行的整体预处理可以用以下步骤描述。我们需要的第一步是使用 RDD 操作和转换准备特征向量。其余步骤如下:

  • 读取数据集:以下代码用于读取数据集,从SMSSpamCollection数据集创建一个字符串的linesRDD。请从 Packt 材料中下载这个数据集,并将其存储在您的磁盘或 HDFS 中的Project+path/input/目录中。稍后将提供有关此数据集的详细描述:
      String filePath = "input/SMSSpamCollection.txt"; 
      JavaRDD<String> linesRDD = sc.textFile(filePath); 

然而,linesRDD包含了垃圾邮件和正常邮件消息。因此,我们需要从文件中分离垃圾邮件和正常邮件。

  • 过滤垃圾邮件:为了从现有的 RDD 中过滤数据,Spark 提供了一个名为filter()的方法,它返回一个只包含所选元素的新数据集。在下面的代码中,您可以看到我们已经将new Function()作为参数传递给了filter()方法,它接受两个类型为 String 和 Boolean 的参数。基本上,Spark API 在集群上运行时大量依赖于将函数传递给驱动程序。有两种方法可以创建包含函数的函数,包括:

  • 实现函数接口,无论是创建匿名内部类还是命名内部类,并将其实例传递给 Spark

  • 使用 lambda 表达式(尽管您需要安装 Java 8 才能利用 lambda 表达式)

  • 以下代码片段说明了我们使用的匿名类概念作为包含call()方法的参数,如果行包含单词spam,则返回true

      JavaRDD<String> spamRDD = linesRDD.filter(new Function<String,   
        Boolean>() { 
        @Override 
        public Boolean call(String line) throws Exception { 
          return line.split("\t")[0].equals("spam");}}); 

  • 过滤掉正常邮件:同样,我们可以过滤掉正常邮件,如下所示:
      JavaRDD<String> hamRDD = linesRDD.filter(new Function<String,  
       Boolean>() { 
       @Override 
       public Boolean call(String line) throws Exception { 
         return line.split("\t")[0].equals("ham"); 
        } 
     }); 

  • 从行中分割单词:为了从每行中提取特征和标签,我们需要使用空格或制表符来分割它们。之后,我们可以得到不包含垃圾邮件或正常邮件单词的行。

以下代码段显示了从行中分离垃圾邮件和正常邮件特征。我们使用了map转换,它返回一个新的 RDD,通过将现有 RDD 的每一行传递给call函数来形成。这里call方法总是返回一个单个项目。您将在后面的部分中发现与flatMap的区别:

      JavaRDD<String> spam = spamRDD.map(new Function<String, String>() { 
        @Override 
        public String call(String line) throws Exception {       
         return line.split("\t")[1]; 
        } 
     }); 

输出:ham.collect()

RDD 的预处理

图 1:垃圾邮件 RDD 的快照

      JavaRDD<String> ham = hamRDD.map(new Function<String, String>() { 
        @Override 
        public String call(String line) throws Exception {     
          return line.split("\t")[1]; 
      } 
      }); 

输出:ham.collect()

RDD 的预处理

图 2:正常邮件 RDD 的快照

  • 从垃圾邮件 RDD 的行中拆分单词:在我们分别得到垃圾邮件和正常邮件 RDD 的特征行之后,我们需要拆分单词以便在将来创建特征向量。以下代码通过使用空格进行拆分并返回单词列表 RDD。调用方法返回每行的单词列表:
      JavaRDD<ArrayList<String>> spamWordList = spam.map(new  
        Function<String, ArrayList<String>>() { 
          @Override 
      public ArrayList<String> call(String line) throws Exception{ 
            ArrayList<String> words = new ArrayList<>(); 
            words.addAll(Arrays.asList(line.split(" "))); 
            return words; 
          }}); 
      JavaRDD<ArrayList<String>> hamWordList = ham.map(new Function<String,  
        ArrayList<String>>() { 
          @Override 
      public ArrayList<String> call(String line) throws Exception{   
            ArrayList<String> words = new ArrayList<>(); 
            words.addAll(Arrays.asList(line.split(" "))); 
            return words;} 
      }); 

  • 创建标签和特征对 RDD:现在我们有了垃圾邮件和正常邮件的两个 RDD。我们想要用 1.0 或 0.0 来标记它们,分别表示垃圾邮件和正常邮件的单词或特征。为了方便使用,我们可以再次创建一个新的 RDD,每行包含一个标签和特征或单词列表的元组。在下面的代码中,我们使用了Tuple2来创建一对。您也可以使用JavaPairRDD来创建标签和特征的一对:
      JavaRDD<Tuple2<Double, ArrayList<String>>> spamWordsLabelPair =  
      spamWordList.map(new Function<ArrayList<String>, Tuple2<Double,  
          ArrayList<String>>>() { 
          @Override 
            public Tuple2<Double, ArrayList<String>> call( 
            ArrayList<String> v1) throws Exception {     
            return new Tuple2<Double, ArrayList<String>>(1.0, v1); 
          }}); 
      JavaRDD<Tuple2<Double, ArrayList<String>>> hamWordsLabelPair =  
      hamWordList.map(new Function<ArrayList<String>, Tuple2<Double,  
          ArrayList<String>>>() { 
          @Override 
          public Tuple2<Double, ArrayList<String>> call( 
            ArrayList<String> v1) throws Exception {     
            return new Tuple2<Double, ArrayList<String>>(0.0, v1); 
          }}); 

      [Output: print spamWordsLabelPair2.collect() using for loop] 
      1.0: [FreeMsg:, Txt:, CALL, to, No:, 86888, &, claim, your, reward, 
      of, 3, hours, talk, time, to, use, from, your, phone, now!, 
      ubscribe6GBP/, mnth, inc, 3hrs, 16, stop?txtStop] 
      1.0: [Sunshine, Quiz!, Win, a, super, Sony, DVD, recorder,
      if, you, canname, the, capital, of, Australia?, Text, MQUIZ, 
      to, 82277., B] 

  • 两个 RDD 的并集:现在我们在表 2中有数据集的两个标签,即垃圾邮件和正常邮件的特征对 RDD。现在要创建训练数据集,我们可以将这两个 RDD 合并成一个。Spark 有一个union()方法可以做到这一点,它返回一个新的 RDD,包含数据集和参数或另一个数据集的并集:
      JavaRDD<Tuple2<Double, ArrayList<String>>> train_set =  
      spamWordsLabelPair.union(hamWordsLabelPair); 

  • 对于前面的所有操作,称为转换。这在每种情况下都会从现有的工作节点中返回一个新的数据集。如果您想要将返回结果带回驱动程序或打印结果,那将被称为动作操作。Spark 支持几种内置方法作为动作。count()方法用于计算数据集中的元素数量:
      System.out.println(train_set.count()); 
      The following output is 8

  • 打印 RDDcollect()take()也是用于打印或收集数据集作为驱动程序中的数组的动作方法,其中,take()接受一个参数,比如n,返回数据集的前 n 个元素。以下代码段打印出训练集中的前 10 个元素或元组:
      for (Tuple2<Double, ArrayList<String>> tt : train_set.collect()) { 
          System.out.println(tt._1 + ": " + tt._2.toString()); } 

输出如下:

      1.0: [FreeMsg:, Txt:, CALL, to, No:, 86888,
       &, claim, your, reward, of, 3, hours, talk, time, to, use, from,
       your, phone, now!, ubscribe6GBP/, mnth, inc, 3hrs, 16, stop?txtStop] 
      1.0: [Sunshine, Quiz!, Win, a, super, Sony, DVD, recorder, if, 
      you, canname, the, capital, of, Australia?, Text, MQUIZ, 
      to, 82277., B] 
      0.0: [What, you, doing?, how, are, you?] 
      0.0: [Ok, lar..., Joking, wif, u, oni...] 
      0.0: [dun, say, so, early, hor..., U, c, already, then, say...] 
      0.0: [MY, NO., IN, LUTON, 0125698789, RING, ME, IF, UR, AROUND!, H*] 
      0.0: [Siva, is, in, hostel, aha:-.] 
      0.0: [Cos, i, was, out, shopping, wif, darren, jus, now,
       n, i, called, 
      him, 2, ask, wat, present, he, wan, lor., Then, he, 
      started, guessing, 
      who, i, was, wif, n, he, finally, guessed, darren, lor.] 

  • 将结果保存在本地文件系统中:有时您可能需要将 RDD 保存在文件系统中。您可以使用以下代码直接保存您的 RDD:
      train_set.saveAsTextFile("output.txt"); 

从 SMSSpamCollection 数据集中获取见解

以下源代码显示了基本的正常邮件和垃圾邮件统计信息:

String path = "input/SMSSpamCollection.txt";     
RDD<String> lines = spark.sparkContext().textFile(path, 2); 
System.out.println(lines.take(10)); 

JavaRDD<Row> rowRDD = lines.toJavaRDD().map( new Function<String, Row>() { 
    public Row call(String line) throws Exception { 
      return RowFactory.create(line); 
      }}); 
System.out.println(rowRDD.collect()); 
List<StructField> fields = new ArrayList<StructField>(); 
fields.add(DataTypes.createStructField("line", DataTypes.StringType, true)); 
org.apache.spark.sql.types.StructType schema = DataTypes.createStructType(fields); 
Dataset<Row> df = spark.sqlContext().createDataFrame(rowRDD, schema); 
df.select("line").show(); 
Dataset<Row> spam = df.filter(df.col("line").like("%spam%")); 
Dataset<Row> ham = df.filter(df.col("line").like("%ham%")); 
System.out.println(spam.count()); 
System.out.println(ham.count()); 
spam.show(); 

上述代码生成了以下垃圾邮件和正常邮件计数:

747 
4831 

这意味着在 5578 封电子邮件中,有 747 封是垃圾邮件,4831 封被标记为正常邮件或非垃圾邮件。换句话说,垃圾邮件和正常邮件的比例分别为 13.40%和 86.6%。

处理键值对

本小节描述了在数据分析中经常需要的键值对,特别是在文本处理中。

mapToPair()

这个方法将返回一个(K,V)对的数据集,其中 K 是键,V 是值。例如,如果您有一个包含整数列表的 RDD,然后想要计算列表中重复条目的数量,那么第一步是将每个数字映射为 1。之后您可以对其进行 reduce 操作。该代码产生了如下所示的输出和缓存:

JavaRDD<Integer> rdd = sc.parallelize(Arrays.asList(1,2,1,3,4,5)); 
JavaPairRDD<Integer, Integer> pairs = rdd.mapToPair( 
  new PairFunction<Integer, Integer, Integer>() { 
    @Override 
    public Tuple2<Integer, Integer> call(Integer x) { 
      return new Tuple2<>(x, 1); 
    } 
}).cache(); 

[Output: pairs.collect()] 
[(1,1), (2,1), (1,1), (3,1), (4,1), (5,1)] 

更多关于转换的内容

在本节中,您可以了解更多关于转换的内容,包括一些类似方法之间的差异。主要将讨论mapflatMapgroupByKeyreduceByKeyaggregateByKeysortByKeysortBy。然而,有兴趣的读者可以参考[2]中的 Spark 编程指南了解 RDD 操作。

map 和 flatMap

flatMap类似于我们在前面的示例中展示的map,但是每个输入项或每次调用匿名类的call()方法都可以映射到零个或多个输出项。因此,call()函数返回的是一个序列,而不是像map那样返回单个项。例如,对于以下 RDD 的输入,输出应该如下所示:

JavaRDD<String> rdd = sc.parallelize(Arrays.asList("Hello World!", 
  "How are you.")); 
JavaRDD<String> words = rdd 
  .flatMap(new FlatMapFunction<String, String>() { 
    @Override 
    public Iterable<String> call(String t) throws Exception { 
      return Arrays.asList(t.split(" ")); 
    }}); 

[output: words.collect()] 
[Hello, World!, How, are, you.] 

对于前面的示例,您无法执行映射操作,因为mapcall()方法只返回一个对象,而不是一系列对象。

groupByKey,reduceByKey 和 aggregateByKey

为了在预处理数据集时执行一些操作,您可能需要根据键值进行一些汇总,例如求和和平均值。Spark 提供了一些方法来执行这些类型的操作。假设您有以下 RDD 对,并且您想根据键对值进行分组并进行一些汇总:

JavaPairRDD<Integer, Integer> rdd_from_integer = sc 
.parallelizePairs(Arrays.asList( new Tuple2<>(1, 1),  
new Tuple2<>(1, 1), new Tuple2<>(3, 2), 
new Tuple2<>(5, 1), new Tuple2<>(5, 3)), 2);  

您想要执行的汇总可以通过 Spark 的三种方法来完成,包括groupByKeyreduceByKeyaggregateByKey。但它们在性能、效率和灵活性方面有所不同,可以执行诸如计数、计算摘要统计信息、从数据集中找到唯一元素等操作。groupByKey方法返回一个(k,Iterable<v>)对的数据集,其中 k 是键,Iterable<v>是键 k 的值序列。使用此方法的先前数据集的输出如下所示,显示了每个键的集合值:

[Output: pairs.groupByKey(2).collect() ] 

groupByKey,reduceByKey 和 aggregateByKey

图 3:使用 groupBykey 的对

为了对每个唯一键的值进行求和,groupByKey在性能上是低效的,因为它不执行组合的映射。您必须进行更多的转换来明确进行这种求和。因此,它会增加网络 I/O 和洗牌大小。通过reduceByKeyaggregateByKey可以获得更好的性能,因为它们执行映射端的组合。

这些方法返回具有每个键聚合结果的数据集,例如每个键的值的总和。以下代码显示了这些方法的操作,它们返回由给定函数聚合键的值的(k,v)对的数据集。

reduceByKey需要一个函数,用于减少每个键的值,而aggregateByKey需要两个函数,其中第一个函数用于指定如何在每个分区内进行聚合,第二个函数用于指定如何在分区之间进行聚合:

  • 代码:reduceByKey()
      JavaPairRDD<Integer, Integer> counts = rdd 
        .reduceByKey(new Function2<Integer, Integer, Integer>() { 
          @Override 
          public Integer call(Integer a, Integer b) { 
            return a + b;}}); 

  • 代码:aggregateByKey()
      JavaPairRDD<Integer, Integer> counts = pairs.aggregateByKey(0, 
          new Function2<Integer, Integer, Integer>() { 
            @Override 
            public Integer call(Integer v1, Integer v2) { 
              return v1 + v2; 
            } 
            }, new Function2<Integer, Integer, Integer>() { 
            @Override 
            public Integer call(Integer v1, Integer v2) { 
                  return v1 + v2; 
                } 
              }); 

对于这两种情况,输出将如下所示:

counts.collect()的输出:

groupByKey,reduceByKey 和 aggregateByKey

图 4:使用计数的 RDD

sortByKey 和 sortBy

排序是数据预处理中的常见操作。Spark 提供了两种方法,可以将一个数据集转换为另一个排序的配对数据集,包括sortByKeysortBy。例如,我们有一个如下所示的数据集:

List<Tuple2<Integer, Integer>> pairs = new ArrayList<>(); 
pairs.add(new Tuple2<>(1, 5)); 
pairs.add(new Tuple2<>(4, 2)); 
pairs.add(new Tuple2<>(-1, 1)); 
pairs.add(new Tuple2<>(1, 1)); 

sortByKey()方法在(k,v)对上执行,并按键的升序或降序返回(k,v)对。您还可以通过提供比较器来自定义排序。以下代码显示了对前面数据集的键进行排序:

  • 代码:sortByKey()
      JavaPairRDD<Integer, Integer> rdd = sc.parallelizePairs(pairs); 
      JavaPairRDD<Integer, Integer> sortedRDD=rdd.sortByKey(Collections.
      <Integer> reverseOrder(), false); 
      [Output: sortedRDD.collect()] 

sortByKey 和 sortBy

图 5:使用 sortByKey 的对

sortBy()方法接受一个函数作为参数,您可以在其中指定排序方法,无论是按键还是按值。以下代码显示了对前面数据集的值进行排序:

  • 代码:sortBy()
      JavaRDD<Tuple2<Integer, Integer>> rdd_new = sc.parallelize(pairs); 
      JavaRDD<Tuple2<Integer, Integer>> sortedRDD=rdd.sortBy( 
      new Function<Tuple2<Integer, Integer>, Integer>() { 
      @Override 
          public Integer call(Tuple2<Integer, Integer> t) { 
              return t._2(); 
          } 
      ,} true, 2);

sortedRDD.collect()的输出:

sortByKey 和 sortBy

图 6:使用 sortBy 的对

数据集基础知识

如在第一章中讨论的,使用 Spark 进行数据分析简介,在 Spark 2.0.0 发布中,DataFrame 仍然是 Scala、Python 和 R 的主要计算抽象,但在使用 Java 时,将使用 Dataset 替换。因此,本书中将始终使用类型为 Row 的 Dataset。

Dataset 是一个分布式的数据集合,结构化为行。这是与 Spark SQL 模块交互的更方便的方式之一。

换句话说,它可以被视为类似于关系数据库(RDB)格式的等价实体。与 DataFrame 和 RDD 等其他数据抽象一样,Dataset 也可以从各种数据源创建,如结构化数据文件(TSV、CSV、JSON 和 TXT)、Hive 表、辅助存储、外部数据库或现有的 RDD 和 DataFrame。然而,在 Spark 2.0.0 发布后,基于 Java 的计算不支持 DataFrame,但如果您使用 Python、Scala 或 R 开发应用程序,仍然可以使用 DataFrame。

在接下来的几节中,您将找到使用 Dataset 的操作和操作,以及如何从不同的来源创建 Dataset。

读取数据集以创建 Dataset

如上所述,Dataset 是从 Spark 1.5.0 版本开始引入的 Spark SQL 模块的一个组件。因此,所有功能的入口都始于初始化 Spark SQLContext。基本上,Spark SQL 用于执行 SQL 查询,可以使用基本的 SQL 语法或 HiveQL 编写。

在另一种编程语言中运行 SQL 时,将返回一个 Dataset 对象。以下代码段将在 Spark 上下文中初始化SQLContext。另一方面,您可能需要初始化HiveContext以从 Hive 中读取数据集。您还可以创建一个不同的上下文,如HiveContext,它提供了SQLContext基本功能的超集:

JavaSparkContext sc = new JavaSparkContext("local","DFDemo"); 
SQLContext sqlContext = new org.apache.spark.sql.SQLContext(sc); 

从文件中读取

例如,您有一个如下所示的 JSON 文件。现在您想要使用 SQL 上下文读取此文件,基本上返回一个 DataFrame,您可以执行所有基本的 SQL 操作和其他 Spark 的 DSL 操作:

[Input File] 
{"name":"Michael"} 
{"name":"Andy", "age":30} 
{"name":"Justin", "age":19} 
[Code]    
Dataset<Row> df = sqlContext.read().json("people.json");    

[Output: df.show()] 
+----+-------+ 
| age|   name| 
+----+-------+ 
|null|Michael| 
|  30|   Andy| 
|  19| Justin| 
+----+-------+ 

从 Hive 中读取

以下代码连接到 Hive 上下文,其中创建了一个表,并将 people JSON 文件加载到 Hive 中。DataFrame 的输出将与上述相同:

The code is as follows:]hiveContext.sql("CREATE TEMPORARY TABLE people USING  
org.apache.spark.sql.json OPTIONS ( path "people.json" )"); 
Dataset<Row> results = hiveContext.sql("SELECT * FROM people "); 
results.show(); 

使用 Dataset 进行预处理

在前一节中,我们已经描述了在实际机器学习应用程序中使用 RDD 进行预处理。现在我们将使用 DataFrame(DF)API 进行相同的示例。您会发现很容易操作SMSSpamCollection数据集(请参阅www.dt.fee.unicamp.br/~tiago/smsspamcollection/)。我们将展示相同的示例,通过对垃圾邮件和正常消息进行标记化,以准备一个训练集:

  • 读取数据集:您可以使用初始化之前的 Spark 会话变量spark来读取该数据集。读取文件作为 Dataset 后,输出将是单列的表格格式。此列的默认名称是value
      Dataset<Row> df = spark.read().load("input/SMSSpamCollection.txt"); 
      df.show(); 

输出:

使用 Dataset 进行预处理

图 7:SMS 垃圾邮件数据集的快照

  • 从现有数据集创建 Row RDD:从前面的输出中,您可以看到一个包含所有行在一起的列。为了使两列,如标签和特征,我们必须将其拆分。由于 Dataset 是不可变的,您无法修改现有的列或 Dataset。因此,您必须使用现有的 Dataset 创建新的 Dataset。这里的代码将 Dataset 转换为 Row 数据集的集合。Row 是一个接口,表示来自关系运算符的输出的一行。您可以使用 Spark 的RowFactory类创建一个新的 Row:
         JavaRDD<Row> rowRDD = df.toJavaRDD(); 

  • 从现有的行 RDD 创建新的行 RDD:在拥有行 RDD 之后,您可以执行常规的 map 操作,其中包含所有包含两个值的行数据集。以下代码拆分每一行并返回一个新的行:
      JavaRDD<Row> splitedRDD = rowRDD.map(new Function<Row, Row>() { 
           @Override 
          public Row call(Row r) throws Exception { 
            String[] split = r.getString(0).split("\t"); 
            return RowFactory.create(split[0],split[1]); 
          }}); 

  • 从行 RDD 创建数据集:现在您有了包含每行两个值的行 RDD。要创建 DF,您必须定义列名或模式及其数据类型。有两种方法可以定义,包括使用反射推断模式和以编程方式指定模式。方法如下:

  • 第一种方法基本上使用 POJO 类和字段名称将成为模式

  • 第二种方法通过定义数据类型并创建 structype 来创建 StruchFields 列表。在本例中,我们使用了第二种方法来从现有行 RDD 创建 DF,如下所示:

      List<StructField> fields  = new ArrayList<>(); 
      fields.add(DataTypes.createStructField("labelString", 
      DataTypes.StringType, true)); 
      fields.add(DataTypes.createStructField("featureString",  
      DataTypes.StringType, true)); 
      org.apache.spark.sql.types.StructType schema = DataTypes 
      .createStructType(fields); 
      Dataset<Row> schemaSMSSpamCollection = sqlContext 
      .createDataFrame(splitedRDD, schema); 
      schemaSMSSpamCollection.printSchema(); 
      [Output: schemaSMSSpamCollection.printSchema()] 

使用数据集进行预处理

图 8:集合的模式

  • 添加新列:现在我们有了两列的 DF。但是我们想要添加新列,将labledSting转换为labedDouble,将featureString转换为featureTokens。您可以像以前的代码一样进行操作。在添加新字段后,创建新模式。然后在现有 DF 中进行常规 map 转换后创建新 DF。以下代码给出了具有四列的新 DF 的输出:
      fields.add(DataTypes.createStructField("labelDouble",  
      DataTypes.DoubleType, true)); 
      fields.add(DataTypes.createStructField("featureTokens",  
      DataTypes.StringType, true)); 
      org.apache.spark.sql.types.StructType schemaUpdated =  
      DataTypes.createStructType(fields); 
      Dataset Row> newColumnsaddedDF = sqlContext 
      .createDataFrame(schemaSMSSpamCollection.javaRDD().map( 
      new Function<Row, Row>() { 
          @Override 
          public Row call(Row row) throws Exception { 
            double label; 
            if(row.getString(0).equalsIgnoreCase("spam")) 
              label = 1.0; 
            else 
              label = 0.0; 
            String[] split = row.getString(1).split(" "); 
            ArrayList<String> tokens = new ArrayList<>(); 
            for(String s:split) 
              tokens.add(s.trim()); 
            return RowFactory.create(row.getString(0), 
       row.getString(1),label, tokens.toString()); 
          }}), schemaUpdated);   
      [Output: newColumnsaddedDF.show()] 

使用数据集进行预处理

图 9:添加新列后的数据集

  • 一些数据集操作:对于数据操作,DF 提供了 Java、Scala 等领域特定语言。您可以对 DF 进行选择、计数、过滤、groupBy等操作。以下代码展示了对上述 DF 的一些操作:
      newColumnsaddedDF.select(newColumnsaddedDF.col("labelDouble"),
      newColumnsaddedDF.col("featureTokens")).show(); 

使用数据集进行预处理

图 10:显示标签和特征的数据集

      newColumnsaddedDF.filter(newColumnsaddedDF.col
      ("labelDouble").gt(0.0)).show(); 

使用数据集进行预处理

图 11:数据集显示标签已转换为双值

      newColumnsaddedDF.groupBy("labelDouble").count().show(); 

使用数据集进行预处理

图 12:显示操作后的数据集统计信息

更多关于数据集操作

本节将描述如何在 DF 上使用 SQL 查询以及在数据集之间创建不同方式的数据集。主要讨论在 DataFrame 上运行 SQL 查询以及从 JavaBean 创建 DataFrame。然而,有兴趣的读者可以参考 Spark 编程指南中有关 SQL 操作的内容[3]。

在数据集上运行 SQL 查询

Spark 的SQLContext具有sql方法,使应用程序能够运行 SQL 查询。该方法返回一个 DataFrame 作为结果:

  • [FilternewColumnsAddedDF.createOrReplaceTempView(SMSSpamCollection)]:
      Dataset<Row> spam = spark.sqlContext().sql("SELECT * FROM 
      SMSSpamCollection
      WHERE labelDouble=1.0"); 
      spam.show();  

以下是上述代码的输出:

在数据集上运行 SQL 查询

图 13:使用 SQL 查询检索与图 11 相同的结果

  • 计数:
      Dataset<Row> counts = sqlContext.sql("SELECT labelDouble, COUNT(*)  
      AS count FROM SMSSpamCollection GROUP BY labelDouble"); 
      counts.show(); 

输出:

在数据集上运行 SQL 查询

图 14:显示数据集统计信息

从 Java Bean 创建数据集

您可以从 Java Bean 创建数据集;在这种情况下,您不需要以编程方式定义模式。例如,您可以在以下代码中看到名为 Bean 的普通旧 Java 对象POJO):

public class SMSSpamBean implements Serializable { 
  private String labelString; 
  private String featureString; 
public SMSSpamBean(String labelString, String featureString) { 
    super(); 
    this.labelString = labelString; 
    this.featureString = featureString; 
  } 
  public String getLabelString() { 
    return labelString; 
  } 
  public void setLabelString(String labelString) { 
    this.labelString = labelString; 
  } 
  public String getFeatureString() { 
    return featureString; 
  }  public void setFeatureString(String featureString) {    this.featureString = featureString; 
  }}  

创建 DF:

JavaRDD<SMSSpamBean> smsSpamBeanRDD =  rowRDD.map(new Function<Row, SMSSpamBean>() { 
      @Override 
    public SMSSpamBean call(Row r) throws Exception { 
        String[] split = r.getString(0).split("\t"); 
        return new SMSSpamBean(split[0],split[1]); 
      }});   
Dataset<Row> SMSSpamDF = spark.sqlContext().createDataFrame(smsSpamBeanRDD, SMSSpamBean.class); 
SMSSpamDF.show();   

以下输出如下:

从 Java Bean 创建数据集

图 15:相应的特征和标签字符串

从字符串和类型化类创建数据集

如前所述,数据集是一组对象的类型化和不可变集合。数据集基本上映射到关系模式。使用数据集抽象,Spark 引入了一个新概念,称为编码器。编码器有助于实体转换,例如 JVM 对象与相应的表格表示之间的转换。您会发现这个 API 与 RDD 的转换非常相似,比如map、mapToPair、flatMapfilter

我们将在下一节中展示使用数据集 API 的垃圾邮件过滤示例。它使用文本文件读取并返回数据集作为表格格式。然后执行类似 RDD 的映射转换,以添加额外的编码器参数(标签、令牌列)。在这里,我们使用了SMSSpamTokenizedBean类的 bean 编码器。

在本小节中,我们将展示如何从字符串和类型类SMSSpamTokenizedBean创建数据集。首先让我们创建 Spark 会话,如下所示:

static SparkSession spark = SparkSession.builder() 
      .appName("DatasetDemo") 
      .master("local[*]") 
      .config("spark.sql.warehouse.dir", "E:/Exp/") 
      .getOrCreate(); 

现在从smm过滤数据集创建一个新的 String 类型的数据集,即Dataset<String>,并显示结果如下:

Dataset<String> ds = spark.read().text("input/SMSSpamCollection.txt").as(org.apache.spark.sql.Encoders.STRING()); 
ds.show(); 

以下是前面代码的输出:

从字符串和类型类创建的数据集

图 16:显示使用数据集进行垃圾邮件过滤的快照

现在让我们通过将我们之前创建的字符串数据集映射为SMSSpamTokenizedBean类型的第二个数据集,来创建第二个数据集,如下所示:

Dataset<SMSSpamTokenizedBean> dsSMSSpam = ds.map( 
new MapFunction<String, SMSSpamTokenizedBean>() { 
          @Override 
public SMSSpamTokenizedBean call(String value) throws Exception { 
      String[] split = value.split("\t"); 
      double label; 
      if(split[0].equalsIgnoreCase("spam")) 
          label = 1.0; 
      else 
          label=0.0; 
ArrayList<String> tokens = new ArrayList<>(); 
  for(String s:split) 
    tokens.add(s.trim());           
      return new SMSSpamTokenizedBean(label, tokens.toString()); 
         } 
}, org.apache.spark.sql.Encoders.bean(SMSSpamTokenizedBean.class)); 

现在让我们打印数据集及其模式,如下所示:

dsSMSSpam.show(); 
dsSMSSpam.printSchema(); 

以下输出为:

从字符串和类型类创建的数据集

图 17:显示令牌和标签,下方是模式

现在,如果您想将此类型的数据集转换为 Row 类型,那么您可以使用toDF()方法,并且可以轻松地使用createOrReplaceTempView()方法创建新的Dataset<Row>的临时视图,如下所示:

Dataset<Row> df = dsSMSSpam.toDF(); 
df.createOrReplaceTempView("SMSSpamCollection");      

同样,您可能希望通过调用show()方法查看相同的数据集,如下所示:

df.show(); 

输出:

从字符串和类型类创建的数据集

图 18:相应的标签和令牌。标签转换为双值

现在让我们探索类型类SMSSpamTokenizedBean。该类作为用于标记文本的 Java 标记化 bean 类。更具体地说,该类接受输入,然后设置标签,然后获取标签。其次,它还设置和获取用于垃圾邮件过滤的令牌。包括 setter 和方法,以下是该类:

public class SMSSpamTokenizedBean implements Serializable { 
private Double labelDouble; 
private String tokens;     
public SMSSpamTokenizedBean(Double labelDouble, String tokens) { 
  super(); 
  this.labelDouble = labelDouble; 
  this.tokens = tokens; 
  } 
  public Double getLabelDouble() { 
    return labelDouble; 
  } 
  public void setLabelDouble(Double labelDouble) { 
    this.labelDouble = labelDouble; 
  } 
  public String getTokens() { 
    return tokens; 
  } 
  public void setTokens(String tokens) { 
    this.tokens = tokens; 
  }} 

RDD、DataFrame 和 Dataset 之间的比较

将数据集作为 Spark 的新数据结构带来一些目标。虽然 RDD API 非常灵活,但有时很难优化处理。另一方面,DataFrame API 很容易优化,但缺少 RDD 的一些好特性。因此,数据集的目标是允许用户轻松表达对象上的转换,并提供 Spark SQL 执行引擎的优势(性能和鲁棒性)。

数据集可以执行许多操作,如排序或洗牌,而无需对对象进行反序列化。为此,它需要一个显式的编码器,用于将对象序列化为二进制格式。它能够将给定对象(Bean)的模式映射到 Spark SQL 类型系统。另一方面,RDD 基于运行时反射的序列化,而改变数据集对象类型的操作也需要新类型的编码器。

Spark 和数据科学家的工作流程

正如已经说明的,数据科学家的一个常见任务是选择数据、数据预处理(格式化、清理和抽样)和数据转换(缩放、分解和聚合)原始数据,以便将其传递到机器学习模型中构建模型。随着实验数据集的增加,传统的单节点数据库将无法处理这些类型的数据集,因此,您需要切换到像 Spark 这样的大数据处理计算。幸运的是,我们有 Spark 作为可扩展的分布式计算系统,可以处理您的数据集。

Spark 和数据科学家的工作流程

图 19:数据科学家使用 Spark 的工作流程

现在让我们来到确切的点,作为一名数据科学家,首先你将不得不阅读以各种格式提供的数据集。然后阅读数据集将为你提供我们已经描述的 RDDs、DataFrames 和 Datasets 的概念。你可以将数据集缓存到主内存中;你可以从 DataFrame、SQL 或 Datasets 转换读取的数据集。最后,你将执行一个操作,将数据转储到磁盘、计算节点或集群。我们在这里描述的步骤基本上形成了一个工作流程,你将按照这个工作流程进行使用 Spark 进行基本数据处理,如图 1所示。

深入了解 Spark

在这一部分,我们将展示 Spark 的高级特性,包括使用共享变量(广播变量和累加器),并讨论它们的基本概念。然而,我们将在后面的章节中讨论数据分区。

共享变量

在编程上下文中,共享变量的概念并不新鲜。需要许多函数和方法并行使用的变量称为共享变量。Spark 有一些机制来使用或实现共享变量。在 Spark 中,传递给 Spark 操作的函数(如 map 或 reduce)在远程集群节点上执行。代码或函数在节点上作为变量的独立副本工作,结果的更新不会传播回驱动程序。然而,Spark 提供了两种常见用法模式的共享变量:广播变量和累加器。

广播变量

在 Spark API 中有许多类型和方法需要了解。然而,更多和更详细的讨论超出了本书的范围。

Broadcast<int[]> broadcastVariable=sc.broadcast(new int[] {2,3,4}); 
int[] values = broadcastVariable.value(); 
for(int i:values){ 
  System.out.println(i);} 

累加器

累加器是另一种共享变量,可以用于实现计数器(如 MapReduce 中)或求和。Spark 只支持累加器为数值类型。然而,你也可以使用现有技术为新的数据类型添加支持[1]。通过调用以下初始值为val的方法创建:

SparkContext. accumulator(val) 

以下代码显示了使用累加器将数组元素相加的用法:

Accumulator<Integer> accumulator = sc.accumulator(0); 
sc.parallelize(Arrays.asList(1, 5, 3, 4)) 
.foreach(x -> accumulator.add(x));   
System.out.println(accumulator.value()); 

Spark 编程指南:spark.apache.org/docs/latest/programming-guide.html

提示

有兴趣的读者应该参考以下网页上关于 Spark 和相关材料的内容:

在本章中,我们使用了 RDDs、Dataset 和 DataFrame API 进行了基本的数据操作。我们还学习了如何通过这些 API 进行一些复杂的数据操作。我们试图专注于数据操作,以理解一个实际的机器学习问题——垃圾邮件过滤。除此之外,我们还展示了如何从不同的来源读取数据。分析和准备你的数据,以理解垃圾邮件过滤作为一个例子。

Spark RDD 操作:spark.apache.org/docs/latest/programming-guide.html#rdd-operations

Spark SQL 操作:spark.apache.org/docs/latest/sql-programming-guide.html

总结

广播变量提供了将只读变量持久化缓存在本地机器上的功能,而不是将副本发送到计算节点或驱动程序。以一种高效的方式将大型输入数据集的副本提供给 Spark 中的每个节点。它还减少了通信成本,因为 Spark 使用了高效的广播。广播变量可以通过调用SparkContext.broadcast(v)从变量v创建。以下代码显示了这一点:

然而,我们并没有开发任何完整的机器学习应用程序,因为我们的目标只是向您展示实验数据集上的基本数据操作。我们打算在第六章构建可扩展的机器学习管道中开发完整的 ML 应用程序。

创建预测模型应该使用哪些特征不仅是一个重要的问题,而且可能是一个需要深入了解问题领域才能回答的困难问题。可以自动选择数据中对某人正在处理的问题最有用或最相关的特征。考虑到这些问题,下一章将详细介绍特征工程,解释为什么要应用它以及一些特征工程的最佳实践。一些仍不清楚的主题将在下一章中更清晰。

第四章:通过特征工程提取知识

应该使用哪些特征来创建预测模型不仅是一个重要问题,而且可能是一个需要深入了解问题领域才能回答的难题。可以自动选择数据中对某人正在处理的问题最有用或最相关的特征。考虑到这些问题,本章详细介绍了特征工程,解释了为什么要应用它以及一些特征工程的最佳实践。

除此之外,我们还将提供特征提取、转换和选择的理论描述和示例,这些示例应用于大规模机器学习技术,使用 Spark MLlib 和 Spark ML API。此外,本章还涵盖了高级特征工程的基本思想(也称为极端特征工程)。

请注意,在继续本章之前,您需要在计算机上安装 R 和 RStudio,因为将使用 R 来展示探索性数据分析的示例。

简而言之,本章将涵盖以下主题:

  • 特征工程的最新技术

  • 特征工程的最佳实践

  • 使用 Spark 进行特征工程

  • 高级特征工程

特征工程的最新技术

尽管特征工程是一个非正式的话题,但它被认为是应用机器学习中的一个重要部分。安德鲁·吴(Andrew Ng)是机器学习领域的领先科学家之一,他在他的书《通过大脑模拟的机器学习和人工智能》中定义了特征工程这个术语(另请参见:en.wikipedia.org/wiki/Andrew_Nghttps://en.wikipedia.org/wiki/Andrew_Ng)。如下所示:

提出特征是困难的,耗时的,需要专业知识。应用机器学习基本上就是特征工程。

基于前述定义,我们可以认为特征工程实际上是人类智慧,而不是人工智能。此外,我们将从其他角度解释特征工程是什么。特征工程还可以被定义为将原始数据转换为有用特征(通常称为特征向量)的过程。这些特征有助于更好地表示基本问题,最终用于预测模型;因此,预测建模可以应用于新数据类型,以获得高预测准确性。

或者,我们可以将特征工程定义为使用或重复使用某人对基本问题和可用数据的高级领域知识的软件工程过程,以创建使机器学习算法轻松工作的特征。

这就是我们如何定义特征工程的术语。如果您仔细阅读,您会发现这些定义中有四个依赖关系:

  • 问题本身

  • 你将使用的原始数据来找出有用的模式或特征

  • 机器学习问题或类别的类型

  • 您将使用的预测模型

现在基于这四个依赖关系,我们可以得出一个工作流程。首先,您必须了解您的问题本身,然后您必须了解您的数据以及它是否有序,如果没有,处理您的数据以找到某种模式或特征,以便您可以构建您的模型。

一旦您确定了特征,您需要知道您的问题属于哪些类别。换句话说,您必须能够根据特征确定它是分类、聚类还是回归问题。最后,您将使用诸如随机森林或支持向量机SVMs)等著名方法在测试集或验证集上构建模型进行预测。

在本章中,你将看到并论证特征工程是一门处理不确定和常常无结构数据的艺术。也是真实的,有许多明确定义的程序可以应用于分类、聚类、回归模型,或者像 SVM 这样的方法,这些程序既有条理又可证明;然而,数据是一个变量,经常在不同时间具有各种特征。

特征提取与特征选择

你将会知道何时以及如何通过经验学徒的实践来决定应该遵循哪些程序。特征工程涉及的主要任务是:

  • 数据探索和特征提取:这是揭示原始数据中隐藏宝藏的过程。一般来说,这个过程在消耗特征的算法中并不会有太大变化。然而,在这方面,对实际经验、业务领域和直觉的更好理解起着至关重要的作用。

  • 特征选择:这是根据你所处理的机器学习问题决定选择哪些特征的过程。你可以使用不同的技术来选择特征;然而,它可能会因算法和使用特征而有所不同。

特征工程的重要性

当最终目标是从预测模型中获得最准确和可靠的结果时,你必须投入你所拥有的最好的东西。在这种情况下,最好的投资将是三个参数:时间和耐心,数据和可用性,以及最佳算法。然而,“如何从数据中获取最有价值的宝藏用于预测建模?”是特征工程的过程和实践以新兴方式解决的问题。

事实上,大多数机器学习算法的成功取决于你如何正确和智能地利用价值并呈现你的数据。通常认为,从你的数据中挖掘出的隐藏宝藏(即特征或模式)将直接刺激预测模型的结果。

因此,更好的特征(即你从数据集中提取和选择的内容)意味着更好的结果(即你将从模型中获得的结果)。然而,在你为你的机器学习模型概括之前,请记住一件事,你需要一个很好的特征,尽管具有描述数据固有结构的属性。

总之,更好的特征意味着三个优点:灵活性、调整和更好的结果:

  • 更好的特征(更好的灵活性):如果你成功地提取和选择了更好的特征,你肯定会获得更好的结果,即使你选择了一个非最佳或错误的模型。事实上,可以根据你拥有的原始数据的良好结构来选择或挑选最佳或最合适的模型。此外,良好的特征将使你能够最终使用更简单但高效、更快速、易于理解和易于维护的模型。

  • 更好的特征(更好的调整):正如我们已经提到的,如果你没有聪明地选择你的机器学习模型,或者如果你的特征不够好,你很可能会从 ML 模型中获得更糟糕的结果。然而,即使在构建模型过程中选择了一些错误的参数,如果你有一些经过良好设计的特征,你仍然可以期望从模型中获得更好的结果。此外,你不需要过多担心或者更加努力地选择最优模型和相关参数。原因很简单,那就是好的特征,你实际上已经很好地理解了问题,并准备使用更好地代表问题本身的所有数据。

  • 更好的特征(更好的结果):即使你把大部分精力投入到更好的特征选择上,你很可能会获得更好的结果。

我们还建议读者不要过分自信地只依赖特征。前面的陈述通常是正确的;然而,有时它们会误导。我们想进一步澄清前面的陈述。实际上,如果你从一个模型中获得了最佳的预测结果,实际上是由三个因素决定的:你选择的模型,你拥有的数据,以及你准备的特征。

因此,如果你有足够的时间和计算资源,总是尝试使用标准模型,因为通常简单并不意味着更好的准确性。尽管如此,更好的特征将在这三个因素中做出最大的贡献。你应该知道的一件事是,不幸的是,即使你掌握了许多实践经验和研究其他人在最新技术领域做得很好的特征工程,一些机器学习项目最终也会失败。

特征工程和数据探索

很多时候,对训练和测试样本进行智能选择,选择更好的特征会导致更好的解决方案。尽管在前一节中我们认为特征工程有两个任务:从原始数据中提取特征和特征选择。然而,特征工程没有明确或固定的路径。

相反,特征工程中的整个步骤很大程度上受到可用原始数据的指导。如果数据结构良好,你会感到幸运。然而,现实往往是原始数据来自多种格式的多源数据。因此,在进行特征提取和特征选择之前,探索这些数据非常重要。

提示

我们建议您使用直方图和箱线图来找出数据的偏度和峰度,并使用数据辅助技术(由 Abe Gong 介绍)对数据进行自举(参见:curiosity.com/paths/abe-gong-building-for-resilience-solid-2014-keynote-oreilly/#abe-gong-building-for-resilience-solid-2014-keynote-oreilly)。

在应用特征工程之前,需要通过数据探索来回答和了解以下问题:

  • 对于所有可用字段,总数据的百分比是存在还是不存在空值或缺失值?然后尝试处理这些缺失值,并在不丢失数据语义的情况下进行解释。

  • 字段之间的相关性是多少?每个字段与预测变量的相关性是多少?它们取什么值(即,是分类还是非分类,是数值还是字母数字,等等)?

  • 然后找出数据分布是否倾斜。你可以通过查看离群值或长尾(略微向右倾斜或正向倾斜,略微向左倾斜或负向倾斜,如图 1所示)来确定偏斜程度。现在确定离群值是否有助于预测。

  • 之后,观察数据的峰度。更技术性地,检查你的峰度是否是 mesokurtic(小于但几乎等于 3),leptokurtic(大于 3),或者 platykurtic(小于 3)。请注意,任何一元正态分布的峰度被认为是 3。

  • 现在尝试调整尾部并观察(预测是否变得更好?)当你去除长尾时会发生什么?

图 1:数据分布的偏斜(x 轴=数据,y 轴=密度)。

你可以使用简单的可视化工具,如密度图来做到这一点,如下例所示。

示例 1. 假设您对健身步行感兴趣,并且在过去的四周(不包括周末)在体育场或乡村散步。您花费了以下时间(以分钟为单位完成 4 公里步行道):15, 16, 18, 17.16, 16.5, 18.6, 19.0, 20.4, 20.6, 25.15, 27.27, 25.24, 21.05, 21.65, 20.92, 22.61, 23.71, 35, 39 和 50。现在让我们使用 R 计算和解释这些值的偏度和峰度。

提示

我们将展示如何在第十章中配置和使用 SparkR,配置和使用外部库并展示如何在 SparkR 上执行相同的代码。这样做的原因是一些绘图包,如ggplot2,在当前用于 SparkR 的版本中仍未直接实现。但是,ggplot2在 GitHub 上作为名为ggplot2.SparkR的组合包可用,可以使用以下命令安装和配置:

devtools::install_github("SKKU-SKT/ggplot2.SparkR")

然而,在配置过程中需要确保许多依赖项。因此,我们应该在以后的章节中解决这个问题。目前,我们假设您具有使用 R 的基本知识,如果您已经在计算机上安装和配置了 R,则请按照以下步骤操作。然而,将在第十章中逐步演示如何使用 RStudio 安装和配置 SparkR。

现在只需复制以下代码片段并尝试执行,以确保您有 Skewness 和 Kurtosis 的正确值。

安装moments包以计算 Skewness 和 Kurtosis:

install.packages("moments")  

使用moments包:

library(moments) 

在锻炼期间所花费的时间制作一个向量:

time_taken <- c (15, 16, 18, 17.16, 16.5, 18.6, 19.0, 20.4, 20.6, 25.15, 27.27, 25.24, 21.05, 21.65, 20.92, 22.61, 23.71, 35, 39, 50) 

将时间转换为 DataFrame:

df<- data.frame(time_taken) 

现在计算skewness

skewness(df) 
[1]1.769592  

现在计算kurtosis

> kurtosis(df) 
[1]5.650427  

结果的解释:您的锻炼时间的偏度为 1.769592,这意味着您的数据向右倾斜或呈正偏态。另一方面,峰度为 5.650427,这意味着数据的分布是尖峰的。现在检查异常值或尾部,请查看以下直方图。同样,为了简单起见,我们将使用 R 来绘制解释您的锻炼时间的密度图。

安装ggplot2package以绘制直方图:

install.packages("ggplot2") 

使用moments包:

library(ggplot2)

现在使用ggplot2qplot()方法绘制直方图:

ggplot(df, aes(x = time_taken)) + stat_density(geom="line", col=  
"green", size = 1, bw = 4) + theme_bw() 

特征工程和数据探索

图 2. 锻炼时间的直方图(右偏)。

在数据(锻炼时间)的图 2中呈现的解释显示密度图向右倾斜,因此是尖峰。除了密度图,您还可以查看每个特征的箱线图。箱线图根据五数总结显示数据分布:最小值第一四分位数,中位数,第三四分位数最大值,如图 3所示,我们可以查找超出三(3)个四分位距IQR)的异常值:

特征工程和数据探索

图 3. 锻炼时间的直方图(图表由箱线图提供,www.physics.csbsju.edu/stats/box2.htmlhttp://www.physics.csbsju.edu/stats/box2.html)。

有时,对数据集进行自举也可以提供有关异常值的见解。如果数据量太大(即大数据),进行数据辅助、评估和预测也是有用的。数据辅助的想法是利用可用数据的一小部分来确定可以从数据集中得出什么见解,这也通常被称为“使用小数据来放大大数据的价值”。

这对大规模文本分析非常有用。例如,假设您有大量文本语料库,当然您可以使用其中的一小部分来测试各种情感分析模型,并选择在性能方面效果最好的模型(计算时间、内存使用、可扩展性和吞吐量)。

现在我们想要引起您对特征工程的其他方面的注意。此外,将连续变量转换为分类变量(具有一定特征组合)会产生更好的预测变量。

提示

在统计语言中,数据中的变量要么代表某些连续尺度上的测量,要么代表某些分类或离散特征。例如,运动员的体重、身高和年龄代表连续变量。另外,以时间为标准的生存或失败也被视为连续变量。另一方面,一个人的性别、职业或婚姻状况是分类或离散变量。从统计学上讲,某些变量可以以两种方式考虑。例如,电影观众对电影的评分可能被视为连续变量,也可以被视为具有 10 个类别的离散变量。时间序列数据或实时流数据通常用于连续变量直到某个时间点。

同时,考虑特征的平方或立方甚至使用非线性模型也可以提供更好的见解。此外,明智地考虑前向选择或后向选择,因为它们都需要大量计算。

最后,当特征数量变得显著大时,使用主成分分析(PCA)或奇异值分解(SVD)技术找到正确的特征组合是明智的决定。

特征提取 - 从数据中创建特征

特征提取是从您已有或将要收集的原始数据中自动构建新特征的方式。在特征提取过程中,通常通过将观察结果自动转换为可以在后续阶段建模的更小的集合来降低复杂原始数据的维度。投影方法,如 PCA 和无监督聚类方法,用于 TXT、CSV、TSV 或 RDB 格式的表格数据。然而,从另一种数据格式中提取特征非常复杂。特别是解析诸如 XML 和 SDRF 之类的许多数据格式,如果要提取的字段数量很大,这是一个繁琐的过程。

对于诸如图像数据之类的多媒体数据,最常见的技术类型包括线条或边缘检测或图像分割。然而,受限于领域和图像,视频和音频观察本身也适用于许多相同类型的数字信号处理(DSP)方法,其中通常模拟观察结果以数字格式存储。

特征提取的最大优点和关键在于,已经开发和可用的方法是自动的;因此,它们可以解决高维数据难以处理的问题。正如我们在第三章中所述,通过了解数据来理解问题,更多的数据探索和更好的特征提取最终会提高您的 ML 模型的性能(因为特征提取也涉及特征选择)。事实上,更多的数据最终将提供更多关于预测模型性能的见解。然而,数据必须是有用的,丢弃不需要的数据将浪费宝贵的时间;因此,在收集数据之前,请考虑这个陈述的意义。

特征提取过程涉及几个步骤,包括数据转换和特征转换。正如我们多次提到的,如果模型能够从原始数据中提取更好的特征,那么机器学习模型很可能会提供更好的结果。优化学习和泛化是好数据的关键特征。因此,通过一些数据处理步骤(如清洗、处理缺失值以及从文本文档到单词转换等中间转换),将数据以最佳格式组合起来的过程是通过一些数据处理步骤实现的。

帮助创建新特征作为预测变量的方法被称为特征转换,实际上是一组方法。特征转换基本上是为了降维。通常,当转换后的特征具有描述性维度时,与原始特征相比,可能会有更好的顺序。

因此,在构建机器学习模型时,可以从训练或测试样本中删除较少描述性的特征。特征转换中最常见的任务包括非负矩阵分解、主成分分析和使用缩放、分解和聚合操作的因子分析。

特征提取的例子包括图像中轮廓的提取、从文本中提取图表、从口语文本录音中提取音素等。特征提取涉及特征的转换,通常是不可逆的,因为在降维过程中最终会丢失一些信息。

特征选择 - 从数据中筛选特征

特征选择是为了为预测建模和分析准备训练数据集或验证数据集的过程。特征选择在大多数机器学习问题类型中都有实际意义,包括分类、聚类、降维、协同过滤、回归等。

因此,最终目标是从原始数据集的大量特征中选择一个子集。通常会应用降维算法,如奇异值分解SVD)和主成分分析PCA)。

特征选择技术的一个有趣的能力是,最小的特征集可以被应用来表示可用数据中的最大方差。换句话说,特征的最小子集足以有效地训练您的机器学习模型。

这个特征子集用于训练模型。特征选择技术有两种类型,即前向选择和后向选择。前向选择从最强的特征开始,不断添加更多特征。相反,后向选择从所有特征开始,删除最弱的特征。然而,这两种技术都需要大量计算。

特征选择的重要性

由于并非所有特征都同等重要;因此,您会发现一些特征比其他特征更重要,以使模型更准确。因此,这些属性可以被视为与问题无关。因此,您需要在准备训练和测试集之前删除这些特征。有时,相同的技术可能会应用于验证集。

与重要性并行的是,您总是会发现一些特征在其他特征的背景下是多余的。特征选择不仅涉及消除不相关或多余的特征,还有其他重要目的,可以增加模型的准确性,如下所述:

  • 特征选择通过消除不相关、空/缺失和冗余特征来提高模型的预测准确性。它还处理高度相关的特征。

  • 特征选择技术通过减少特征数量,使模型训练过程更加稳健和快速。

特征选择与降维

虽然通过使用特征选择技术可以在数据集中选择某些特征来减少特征数量。然后,使用子集来训练模型。然而,整个过程通常不能与术语降维互换使用。

事实上,特征选择方法用于从数据中提取子集,而不改变其基本属性。

另一方面,降维方法利用已经设计好的特征,可以通过减少变量的数量来将原始特征转换为相应的特征向量,以满足机器学习问题的特定考虑和要求。

因此,它实际上修改了基础数据,通过压缩数据从原始和嘈杂的特征中提取原始特征,但保持了原始结构,大多数情况下是不可逆的。降维方法的典型例子包括主成分分析(PCA)、典型相关分析(CCA)和奇异值分解(SVD)。

其他特征选择技术使用基于过滤器的、包装器方法和嵌入方法的特征选择,通过在监督上下文中评估每个特征与目标属性之间的相关性。这些方法应用一些统计量来为每个特征分配一个得分,也被称为过滤方法。

然后基于评分系统对特征进行排名,可以帮助消除特定特征。这些技术的例子包括信息增益、相关系数得分和卡方检验。作为特征选择过程的包装器方法的一个例子是递归特征消除算法。另一方面,最小绝对值收缩和选择算子(LASSO)、弹性网络和岭回归是特征选择的嵌入方法的典型例子,也被称为正则化方法。

Spark MLlib 的当前实现仅为RowMatrix类提供了对 SVD 和 PCA 的降维支持。另一方面,从原始数据收集到特征选择的一些典型步骤包括特征提取、特征转换和特征选择。

提示

建议感兴趣的读者阅读特征选择和降维的 API 文档:spark.apache.org/docs/latest/mllib-dimensionality-reduction.html

特征工程的最佳实践

在这一部分,我们已经找出了在可用数据上进行特征工程时的一些良好做法。机器学习的一些最佳实践在第二章中进行了描述,机器学习最佳实践。然而,这些对于整体机器学习的最新技术来说还是太笼统了。当然,这些最佳实践在特征工程中也会很有用。此外,我们将在接下来的子章节中提供更多关于特征工程的具体示例。

理解数据

尽管术语“特征工程”更加技术化,但它是一门艺术,可以帮助你理解特征的来源。现在也出现了一些重要的问题,需要在理解数据之前回答:

  • 这些特征的来源是什么?数据是实时的还是来自静态来源?

  • 这些特征是连续的、离散的还是其他的?

  • 特征的分布是什么样的?分布在很大程度上取决于正在考虑的示例子集是什么样的吗?

  • 这些特征是否包含缺失值(即 NULL)?如果是,是否可能处理这些值?是否可能在当前、未来或即将到来的数据中消除它们?

  • 是否存在重复或冗余条目?

  • 我们是否应该进行手动特征创建,这样会证明有用吗?如果是,将这些特征纳入模型训练阶段会有多难?

  • 是否有可以用作标准特征的特征?

了解前面的问题的答案很重要。因为数据来源可以帮助你更快地准备特征工程技术。你需要知道你的特征是离散的还是连续的,或者请求是否是实时响应。此外,你需要了解数据的分布以及它们的偏斜和峰度,以处理异常值。

你需要为缺失或空值做好准备,无论是将它们移除还是需要用替代值填充。此外,你需要首先移除重复的条目,这非常重要,因为重复的数据点可能会严重影响模型验证的结果,如果不适当地排除的话。最后,你需要了解你的机器学习问题本身,因为了解问题类型将帮助你相应地标记你的数据。

创新的特征提取方式

在提取和选择特征时要有创新性。在这里,我们总共提供了八条提示,这些提示将帮助你在机器学习应用开发过程中进行泛化。

提示

通过将现有数据字段汇总到更广泛的级别或类别来创建输入。

更具体地说,让我们给你一些例子。显然,你可以根据同事的职称将他们分类为战略或战术。例如,你可以将副总裁或 VP及以上职位的员工编码为战略,总监及以下职位的员工编码为战术。

将几个行业整合到更高级别的行业可能是这种分类的另一个例子。将石油和天然气公司与大宗商品公司整合在一起;黄金、白银或铂金作为贵金属公司;高科技巨头和电信行业作为技术;将营收超过 10 亿美元的公司定义为大型,而净资产 100 万美元以下的公司定义为小型

提示

将数据分成单独的类别或区间。

更具体地说,让我们举几个例子。假设你正在对年收入在 5000 万美元到 10 亿美元以上的公司进行一些分析。因此,显然,你可以将收入分成一些连续的区间,比如 5000 万美元至 2 亿美元,2.01 亿美元至 5 亿美元,5.01 亿美元至 10 亿美元,以及 10 亿美元以上。现在,如何以一种可呈现的格式表示这些特征?很简单,尝试在公司落入收入区间时将值设为 1;否则,值为 0。现在从年收入字段中创建了四个新的数据字段,对吧?

提示

想出一种创新的方法将现有数据字段组合成新的字段。

更具体地说,让我们举几个例子。在第一个提示中,我们讨论了如何通过将现有字段合并成更广泛的字段来创建新的输入。现在,假设你想创建一个布尔标志,用于识别是否有人在拥有 10 年以上经验的情况下属于 VP 或更高级别。因此,在这种情况下,你实际上是通过将一个数据字段与另一个数据字段相乘、相除、相加或相减来创建新的字段。

提示

同时考虑手头的问题,并且要有创造性。

在之前的提示中,假设你已经创建了足够的箱子和字段或输入。现在,不要太担心一开始创建太多的变量。最好就让头脑风暴自然地进行特征选择步骤。

提示

不要愚蠢。

谨慎创建不必要的字段;因为从少量数据中创建太多特征可能会导致模型过拟合,从而产生虚假结果。当面对数据相关性时,记住相关性并不总是意味着因果关系。我们对这个常见观点的逻辑是,对观测数据进行建模只能告诉我们两个变量之间的关系,但不能告诉我们原因。

《奇迹经济学》一书中的研究文章(也可参见*史蒂文·D·莱维特,斯蒂芬·J·杜布纳,《奇迹经济学:一位流氓经济学家探索一切的隐藏面》,www.barnesandnoble.com/w/freakonomics-steven-d-levitt/1100550563http://www.barnesandnoble.com/w/freakonomics-steven-d-levitt/1100550563)发现,公立学校的考试成绩数据表明,家中拥有更多书籍的孩子倾向于比家中书籍较少的孩子有更高的标准化考试成绩。

因此,在创建和构建不必要的特征之前要谨慎,这意味着不要愚蠢。

提示

不要过度设计。

在特征工程阶段,判断迭代花费几分钟还是半天的时间差异是微不足道的。因为特征工程阶段最有效的时间通常是在白板上度过的。因此,确保做得正确的最有效的方法是向你的数据提出正确的问题。如今,“大数据”这个词正在取代“特征工程”这个词。没有空间进行黑客攻击,所以不要过度设计:

特征提取的创新方式

图 4:对假阳性和假阴性的真实解释。

提示

谨防假阳性和假阴性。

另一个重要的方面是比较假阴性和假阳性。根据问题,获得其中一个更高的准确性是重要的。例如,如果你在医疗领域进行研究,试图开发一个机器学习模型来预测疾病,那么获得假阳性可能比获得假阴性结果更好。因此,我们在这方面的建议是查看混淆矩阵,这将帮助你以一种可视化的方式查看分类器的预测结果。

行表示每个观察的真实类别,而列对应于模型本身预测的类别,如图 4所示。然而,图 5将提供更多的见解。请注意,对角线元素,也称为正确决策,用粗体标记。最后一列Acc表示每个关键的准确性如下:

特征提取的创新方式

图 5:一个简单的混淆矩阵。

提示

在选择特征之前考虑精确度和召回率。

最后,还有两个重要的量需要考虑,即精确度和召回率。更技术性地说,您的分类器正确预测正值结果的频率称为召回率。相反,当您的分类器预测正值输出时,它实际上是真实的频率称为精确度。

预测这两个值确实非常困难。然而,仔细的特征选择将有助于在最后一个位置更好地获得这两个值。

您将在Matthew Shardlow撰写的研究论文中找到更多有趣和优秀的特征选择描述(也可参见 Matthew Shardlow,特征选择技术分析studentnet.cs.manchester.ac.uk/pgt/COMP61011/goodProjects/Shardlow.pdf)。现在让我们在下一节中探索 Spark 的特征工程功能。

使用 Spark 进行特征工程

基于大数据的机器学习是一个深度和广泛的领域,它需要一个新的配方,其中的成分将是特征工程和对数据模型的稳定优化。优化后的模型可以称为大模型(也可参见S. MartinezA. ChenG. I. WebbN. A. ZaidiBayesian network classifiers 的可扩展学习,已被接受发表在Journal of Machine Learning Research中),它可以从大数据中学习,并且是突破的关键,而不仅仅是大数据。

大模型还表示,您从多样化和复杂的大数据中得到的结果将具有低偏差(请参见D. Brain 和 G. I. Webb分类学习中对小数据集需要低偏差算法在 PKDDpp. 62, 73, 2002),并且可以使用多类机器学习算法进行外部核心学习(请参见外部核心学习定义,en.wikipedia.org/wiki/Out-of-core_algorithmen.wikipedia.org/wiki/Out-of-core_algorithm)并且具有最小的调整参数。

Spark 为我们引入了这个大模型,以便我们能够规模化部署我们的机器学习应用。在本节中,我们将描述 Spark 如何开发机器学习库和 Spark 核心来有效处理大规模数据集和不同数据结构的高级特征工程功能。

正如我们已经提到的,Spark 的机器学习模块包含两个 API,包括spark.mllibspark.ml。MLlib 包建立在 RDD 之上,而 ML 包建立在 DataFrame 和 Dataset 之上,为构建 ML 流水线提供了更高级别的 API。接下来的几节将向您展示 ML 的细节(MLlib 将在第五章中讨论,通过示例进行监督和无监督学习),其中包括一个实际的机器学习问题。

机器学习流水线-概述

Spark 的 ML 包提供了一组统一的高级 API,帮助创建一个实用的机器学习流水线。这个流水线的主要概念是将多个机器学习算法组合在一起,形成一个完整的工作流程。在机器学习领域,经常会运行一系列算法来处理和学习可用的数据。

例如,假设您想要开发一个文本分析机器学习应用程序。对于一系列简单的文本文档,总体过程可以分为几个阶段。自然地,处理工作流程可能包括几个阶段。在第一步中,您需要从每个文档中将文本拆分为单词。一旦您拆分了单词,您应该将这些单词转换为每个文档的数值特征向量。

最后,您可能希望使用第 2 阶段得到的特征向量学习预测模型,并且还想要为使用监督机器学习算法的每个向量进行标记。简而言之,这四个阶段可以总结如下。对于每个文档,执行以下操作:

  • 拆分文本=>单词

  • 将单词转换为数值特征向量

  • 数值特征向量=>标记

  • 使用向量和标签构建 ML 模型作为预测模型

这四个阶段可以被视为一个工作流程。Spark ML 将这些类型的工作流程表示为由一系列 PipelineStages 组成的管道,其中转换器和估计器在管道的每个阶段中以特定顺序运行。转换器实际上是一个将一个数据集转换为另一个数据集的算法。

另一方面,估计器也是一种算法,负责在数据集上进行拟合以生成一个转换器。从技术上讲,估计器实现了一个称为fit()的方法,它接受一个数据集并生成一个模型,这是一个转换器。

提示

有兴趣的读者应该参考此网址spark.apache.org/docs/latest/ml-pipeline.html了解有关管道中转换器和估计器的更多详细信息。

机器学习管道-概述

图 6:管道是一个估计器。

更具体地说,让我们举个例子,假设使用诸如逻辑回归(或线性回归)之类的机器学习算法作为估计器。现在通过调用fit()方法,训练一个逻辑回归模型(它本身也是一个模型,因此是一个转换器)。从技术上讲,转换器实现了一种方法,即transform(),它将一个数据集转换为另一个数据集。

在转换过程中,还会有一个列取决于选择和列位置。需要注意的是,Spark 开发的管道概念大多受到 Scikit-learn 项目的启发,这是一个用于数据挖掘和数据分析的简单高效的工具(也可以参见 Scikit-learn 项目,scikit-learn.org/stable/)。

如第一章中所讨论的,Spark 已将 RDD 操作实现为有向无环图DAG)风格。同样的方式也适用于管道,其中每个 DAG 管道的阶段都被指定为一个有序数组。我们之前描述的具有四个阶段的文本处理管道实际上是一个线性管道;在这种管道中,每个阶段都消耗前一阶段产生的数据。只要特征工程图的数据流以 DAG 样式形成和对齐,也可以创建非线性管道。

需要注意的是,如果管道形成一个 DAG,那么阶段需要按拓扑顺序指定。我们所讨论的管道可以在包括各种文件类型的数据集上运行,因此需要对管道一致性进行运行时和编译时检查。不幸的是,Spark 管道的当前实现不提供使用编译时类型检查。但是,Spark 提供了运行时检查,由管道和管道模型使用,使用数据集模式进行。

由于 RDD 的概念是不可变的,这意味着一旦创建了 RDD,就不可能更改 RDD 的内容,同样,流水线阶段的唯一性应该是持久的(请参考图 6图 7以获得清晰的视图)具有唯一的 ID。为简单起见,前述文本处理工作流程可以像图 5 一样进行可视化;我们展示了具有三个阶段的文本处理流水线。TokenizerHashingTF是两个独特的转换器。

另一方面,LogisticRegression 是一个估计器。在底部一行,一个圆柱表示一个数据集。在原始包含带有标签的文本文档的数据集上调用了 pipeline 的fit()方法。现在Tokenizer.transform()方法将原始文本文档分割成单词,而HashingTF.transform()方法则将单词列转换为特征向量。

请注意,在每种情况下,数据集上都添加了一列。现在调用LogisticRegression.fit()方法来生成LogisticRegressionModel

机器学习流水线-概述

图 7:Pipeline 是一个估计器。

图 7中,PipelineModel 具有与原始 Pipeline 相同数量的阶段。然而,在这种情况下,原始 Pipeline 中的所有估计器都需要转换为转换器。

当在测试数据集(即数值特征向量)上调用PipelineModeltransform()方法时,数据按特定顺序通过已安装的流水线传递。

总之,流水线和 PipelineModel 有助于确保训练和测试数据经过相同的特征处理步骤。以下部分展示了我们描述的前述流水线过程的实际示例。

流水线-使用 Spark ML 的示例

本节将展示一个名为垃圾邮件过滤的实际机器学习问题,该问题在第三章中介绍了,通过了解数据来理解问题,使用 Spark 的流水线。我们将使用从archive.ics.uci.edu/ml/datasets/SMS+Spam+Collection下载的SMSSpamCollection数据集来展示 Spark 中的特征工程。以下代码使用普通的旧 Java 对象POJO)类将样本数据集读取为数据集(更多信息请参见en.wikipedia.org/wiki/Plain_Old_Java_Object)。请注意,SMSSpamHamLabelDocument类包含标签(label: double)和短信行(text: String)。

要运行代码,只需在 Eclipse IDE 中创建一个 Maven 项目,指定提供的pom.xml文件下的 Maven 项目和包的依赖关系,并将应用程序打包为 jar 文件。或者,作为独立的 Java 应用程序在 Eclipse 上运行示例。

Spark 会话创建的代码如下:

  static SparkSession spark = SparkSession 
      .builder().appName("JavaLDAExample") 
      .master("local[*]") 
      .config("spark.sql.warehouse.dir", "E:/Exp/") 
      .getOrCreate(); 

在这里,Spark SQL 仓库设置为E:/Exp/目录,用于 Windows。根据操作系统类型设置您的路径。

smsspamdataset样本的代码如下:

public static void main(String[] args) { 
 // Prepare training documents, which are labelled. 
 Dataset<Row> smsspamdataset = spark.createDataFrame(Arrays.asList( 
      new SMSSpamHamLabelDocument(0.0, "What you doing how are you"), 
      new SMSSpamHamLabelDocument(0.0, "Ok lar Joking wif u oni"), 
      new SMSSpamHamLabelDocument(1.0, "FreeMsg Txt CALL to No 86888 & claim your reward of 3 hours talk time to use from your phone now ubscribe6GBP mnth inc 3hrs 16 stop txtStop"), 
      new SMSSpamHamLabelDocument(0.0, "dun say so early hor U c already then say"), 
      new SMSSpamHamLabelDocument(0.0, "MY NO IN LUTON 0125698789 RING ME IF UR AROUND H"), 
      new SMSSpamHamLabelDocument(1.0, "Sunshine Quiz Win a super Sony DVD recorder if you canname the capital of Australia Text MQUIZ to 82277 B") 
    ), SMSSpamHamLabelDocument.class); 

现在,通过调用show()方法来查看数据集的结构:

Smsspamdataset.show(); 

输出如下所示:

流水线-使用 Spark ML 的示例

POJO 类的代码如下:

public class SMSSpamHamLabelDocument implements Serializable { 
    private double label; 
    private String wordText; 
    public SMSSpamHamLabelDocument(double label, String wordText) { 
      this.label = label; 
      this.wordText = wordText; 
    } 
    public double getLabel() { return this.label; } 
    public void setLabel(double id) { this.label = label; } 
    public String getWordText() { return this.wordText; }    public void setWordText(String wordText) { this.wordText = wordText; } 
}  } 

现在,让我们将数据集分割为trainingData(60%)和testData(40%)以进行模型训练。

分割的代码如下:

Dataset<Row>[] splits = smsspamdataset.randomSplit(new double[] { 0.6, 0.4 }); 
Dataset<Row> trainingData = splits[0]; 
Dataset<Row> testData = splits[1]; 

数据集的目标是使用分类算法构建预测模型,我们从数据集中知道,有两种类型的消息。一种是垃圾邮件,表示为 1.0,另一种是正常邮件,表示为 0.0 标签。我们可以考虑使用 LogisticRegression 或线性回归算法来训练模型以简化训练和使用。

然而,使用回归等更复杂的分类器,如广义回归,将在第八章, 调整您的机器学习模型中进行讨论。因此,根据我们的数据集,我们的工作流程或管道将如下所示:

  • 将训练数据的文本行标记为单词

  • 使用哈希技术提取特征

  • 应用逻辑回归估计器构建模型

前面的三个步骤可以通过 Spark 的管道组件轻松完成。您可以将所有阶段定义为单个 Pipeline 类,该类将以高效的方式构建模型。以下代码显示了构建预测模型的整个管道。分词器类定义了输入和输出列(例如,wordText到单词),HashTF类定义了如何从分词器类的单词中提取特征。

LogisticRegression类配置其参数。最后,您可以看到 Pipeline 类,该类将前面的方法作为 PipelineStage 数组,并返回一个估计器。在训练集上应用fit()方法后,它将返回最终模型,该模型已准备好进行预测。您可以在应用模型进行预测后看到测试数据的输出。

管道的代码如下:

Tokenizer tokenizer = new Tokenizer() 
      .setInputCol("wordText") 
      .setOutputCol("words"); 
HashingTF hashingTF = new HashingTF() 
      .setNumFeatures(100) 
      .setInputCol(tokenizer.getOutputCol()) 
      .setOutputCol("features"); 
LogisticRegression logisticRegression = new LogisticRegression() 
      .setMaxIter(10) 
      .setRegParam(0.01); 
Pipeline pipeline = new Pipeline().setStages(new PipelineStage[] {tokenizer, hashingTF, logisticRegression}); 
    // Fit the pipeline to training documents. 
PipelineModel model = pipeline.fit(trainingData); 
Dataset<Row> predictions = model.transform(testData); 
for (Row r: predictions.select("label", "wordText", "prediction").collectAsList()) { 
  System.out.println("(" + r.get(0) + ", " + r.get(1) + ") --> prediction=" + r.get(2)); 
    } } 

输出如下:

(0.0, What you doing how are you)  
--> prediction=0.0 
(0.0, MY NO IN LUTON 0125698789 RING ME IF UR AROUND H)  
--> prediction=0.0 
(1.0, Sunshine Quiz Win a super Sony DVD recorder if you canname the capital of Australia Text MQUIZ to 82277 B)  
--> prediction=0.0 

特征转换、提取和选择

前面的部分向您展示了管道的整体流程。这个管道或工作流基本上是一些操作的集合,例如将一个数据集转换为另一个数据集,提取特征和选择特征。这些是我们在前几节中已经描述过的特征工程的基本操作符。本节将向您展示如何使用 Spark 机器学习包中的这些操作符的详细信息。Spark 提供了一些高效的特征工程 API,包括 MLlib 和 ML。

在本节中,我们将继续使用垃圾邮件过滤器示例开始 ML 包。让我们从文本文件中读取一个大型数据集作为数据集,其中包含以 ham 或 spam 单词开头的行。此数据集的示例输出如下。现在我们将使用此数据集来提取特征并使用 Spark 的 API 构建模型。

Input DF的代码如下:

Dataset<Row> df = spark.read().text("input/SMSSpamCollection.txt"); 
df.show();  

输出如下:

特征转换、提取和选择

转换 - RegexTokenizer

从前面的输出中,您可以看到我们必须将其转换为两列以识别垃圾邮件和 ham 消息。为此,我们可以使用RegexTokenizer转换器,该转换器可以从正则表达式(regex)中获取输入并将其转换为新数据集。此代码生成labelFeatured。例如,请参阅以下输出中显示的数据集:

// Feature Transformers (RegexTokenizer) 
RegexTokenizer regexTokenizer1 = new RegexTokenizer() 
        .setInputCol("value") 
        .setOutputCol("labelText") 
        .setPattern("\\t.*$");     
Dataset<Row> labelTextDataFrame = regexTokenizer1.transform(df); 
RegexTokenizer regexTokenizer2 = new RegexTokenizer() 
        .setInputCol("value").setOutputCol("text").setPattern("\\W"); 
Dataset<Row> labelFeatureDataFrame = regexTokenizer2 
        .transform(labelTextDataFrame); 
for (Row r : labelFeatureDataFrame.select("text", "labelText").collectAsList()) { 
      System.out.println( r.getAs(1) + ": " + r.getAs(0)); 
    } 

以下是labelFeature数据集的输出:

WrappedArray(ham): WrappedArray(ham, what, you, doing, how, are, you) 
WrappedArray(ham): WrappedArray(ham, ok, lar, joking, wif, u, oni) 
WrappedArray(ham): WrappedArray(ham, dun, say, so, early, hor, u, c, already, then, say) 
WrappedArray(ham): WrappedArray(ham, my, no, in, luton, 0125698789, ring, me, if, ur, around, h) 
WrappedArray(spam): WrappedArray(spam, freemsg, txt, call, to, no, 86888, claim, your, reward, of, 3, hours, talk, time, to, use, from, your, phone, now, ubscribe6gbp, mnth, inc, 3hrs, 16, stop, txtstop) 
WrappedArray(ham): WrappedArray(ham, siva, is, in, hostel, aha) 
WrappedArray(ham): WrappedArray(ham, cos, i, was, out, shopping, wif, darren, jus, now, n, i, called, him, 2, ask, wat, present, he, wan, lor, then, he, started, guessing, who, i, was, wif, n, he, finally, guessed, darren, lor) 
WrappedArray(spam): WrappedArray(spam, sunshine, quiz, win, a, super, sony, dvd, recorder, if, you, canname, the, capital, of, australia, text, mquiz, to, 82277, b) 

现在让我们通过以下方式从我们刚刚创建的labelFeatured数据集创建一个新的数据集,选择标签文本:

Dataset<Row> newDF = labelFeatureDataFrame.withColumn("labelTextTemp",        labelFeatureDataFrame.col("labelText").cast(DataTypes.StringType))        .drop(labelFeatureDataFrame.col("labelText"))        .withColumnRenamed("labelTextTemp", "labelText"); 

现在让我们通过调用show()方法进一步探索新数据集中的内容:

转换 - RegexTokenizer

转换 - 字符串索引器

前面的输出显示了 ham 和 spam 消息的分类,但我们必须将 ham 和 spam 文本转换为双精度值。StringIndexer转换器可以轻松完成此操作。它可以将标签的字符串列编码为另一列中的索引。索引按标签频率排序。StringIndexer为我们的数据集生成了两个索引,0.0 和 1.0:

// Feature Transformer (StringIndexer) 
StringIndexer indexer = new StringIndexer().setInputCol("labelText") 
        .setOutputCol("label"); 
Dataset<Row> indexed = indexer.fit(newDF).transform(newDF); 
    indexed.select(indexed.col("labelText"), indexed.col("label"), indexed.col("text")).show();  

indexed.show()函数的输出如下:

转换 - 字符串索引器

转换 - 停用词移除器

前述输出包含单词或标记,但有些单词并不像特征那样重要。因此,我们需要删除这些单词。为了使这项任务更容易,Spark 通过StopWordsRemover类提供了停用词列表,这将在第六章中更多地讨论,构建可扩展的机器学习管道

我们可以使用这些单词来过滤不需要的单词。此外,我们将从文本列中删除垃圾邮件和垃圾邮件单词。StopWordsRemover类将通过删除特征中的停用词将前述数据集转换为过滤后的数据集。下面的输出将显示没有垃圾邮件和垃圾邮件单词标记的单词:

// Feature Transformers (StopWordsRemover) 
StopWordsRemover remover = new StopWordsRemover(); 
String[] stopwords = remover.getStopWords(); 
String[] newStopworks = new String[stopwords.length+2]; 
newStopworks[0]="spam"; 
newStopworks[1]="ham"; 
for(int i=2;i<stopwords.length;i++){ 
      newStopworks[i]=stopwords[i];}   
remover.setStopWords(newStopworks).setInputCol("text").setOutputCol("filteredWords"); 
Dataset<Row> filteredDF = remover.transform(indexed); 
filteredDF.select(filteredDF.col("label"), filteredDF.col("filteredWords")).show();  

输出如下:

转换 - StopWordsRemover

TF 提取

现在我们有了包含双精度值标签和过滤后的单词或标记的数据集。下一个任务是对特征进行向量化(使其成为数值)或从单词或标记中提取特征。

TF-IDFHashingTFIDF;也称为词频-逆文档频率)是一种广泛用于提取特征的特征向量化方法,基本上计算了术语在语料库中对文档的重要性。

TF计算文档或行中术语的频率,IDF计算文档或行的频率,即包含特定术语的文档或行的数量。以下代码使用 Spark 的高效HashingTF类解释了前述数据集的词频。HashingTF是一个将术语集合转换为固定长度特征向量的转换器。还显示了特征数据的输出:

// Feature Extractors (HashingTF transformer) 
int numFeatures = 100; 
HashingTF hashingTF = new HashingTF().setInputCol("filteredWords") 
        .setOutputCol("rawFeatures").setNumFeatures(numFeatures); 
Dataset<Row> featurizedData = hashingTF.transform(filteredDF); 
    for (Row r : featurizedData.select("rawFeatures", "label").collectAsList()) { 
Vector features = r.getAs(0); ////Problematic line 
Double label = r.getDouble(1); 
System.out.println(label + "," + features); 
    }  

输出如下:

0.0,(100,[19],[1.0]) 
0.0,(100,[9,16,17,48,86,96],[1.0,1.0,1.0,1.0,1.0,1.0]) 
0.0,(100,[17,37,43,71,99],[1.0,1.0,2.0,1.0,2.0]) 
0.0,(100,[4,41,42,47,92],[1.0,1.0,1.0,1.0,1.0]) 
1.0,(100,[3,12,19,26,28,29,34,41,46,51,71,73,88,93,94,98],[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,2.0]) 
0.0,(100,[19,25,38],[1.0,1.0,1.0]) 
0.0,(100,[8,10,16,30,37,43,48,49,50,55,76,82,89,95,99],[1.0,4.0,2.0,1.0,1.0,2.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,2.0]) 
1.0,(100,[0,24,36,39,42,48,53,58,67,86,95,97,98],[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,2.0,1.0,1.0,2.0,1.0]) 

提取 - IDF

同样,我们可以将IDF应用于特征数据以计算文档频率。IDF是一个适合于前述数据集的估计器,并产生一个将转换为包含特征和标签的重新缩放数据集的IDFModel

// Feature Extractors (IDF Estimator) 
IDF idf = new IDF().setInputCol("rawFeatures").setOutputCol("features"); 
IDFModel idfModel = idf.fit(featurizedData); 
Dataset<Row> rescaledData = idfModel.transform(featurizedData); 
for (Row r : rescaledData.select("features", "label").collectAsList()) { 
Vector features = r.getAs(0); 
Double label = r.getDouble(1); 
System.out.println(label + "," + features); 
    }  

输出如下:

0.0,(100,[19],[0.8109302162163288]) 
0.0,(100,[9,16,17,48,86,96],[1.5040773967762742,1.0986122886681098,1.0986122886681098,0.8109302162163288,1.0986122886681098,1.5040773967762742]) 
0.0,(100,[17,37,43,71,99],[1.0986122886681098,1.0986122886681098,2.1972245773362196,1.0986122886681098,2.1972245773362196]) 
0.0,(100,[4,41,42,47,92],[1.5040773967762742,1.0986122886681098,1.0986122886681098,1.5040773967762742,1.5040773967762742]) 
1.0,(100,[3,12,19,26,28,29,34,41,46,51,71,73,88,93,94,98],[1.5040773967762742,1.5040773967762742,0.8109302162163288,1.5040773967762742,1.5040773967762742,1.5040773967762742,1.5040773967762742,1.0986122886681098,1.5040773967762742,1.5040773967762742,1.0986122886681098,1.5040773967762742,1.5040773967762742,1.5040773967762742,1.5040773967762742,2.1972245773362196]) 
0.0,(100,[19,25,38],[0.8109302162163288,1.5040773967762742,1.5040773967762742]) 
0.0,(100,[8,10,16,30,37,43,48,49,50,55,76,82,89,95,99],[1.5040773967762742,6.016309587105097,2.1972245773362196,1.5040773967762742,1.0986122886681098,2.1972245773362196,0.8109302162163288,1.5040773967762742,1.5040773967762742,1.5040773967762742,1.5040773967762742,1.5040773967762742,1.5040773967762742,1.0986122886681098,2.1972245773362196]) 
1.0,(100,[0,24,36,39,42,48,53,58,67,86,95,97,98],[1.5040773967762742,1.5040773967762742,1.5040773967762742,1.5040773967762742,1.0986122886681098,0.8109302162163288,1.5040773967762742,1.5040773967762742,3.0081547935525483,1.0986122886681098,1.0986122886681098,3.0081547935525483,1.0986122886681098]) 

前述输出从原始文本中提取特征。第一个条目是标签,其余是提取的特征向量。

选择 - ChiSqSelector

前述输出已准备好使用分类算法进行训练,例如LogisticRegression。但是我们可以从分类特征中使用更重要的特征。为此,Spark 提供了一些特征选择器 API,如ChiSqSelectorChiSqSelector被称为卡方特征选择

它在具有分类特征的标记数据上运行。它根据卡方检验对特征进行排序,该检验独立于类,并过滤出类标签最依赖的前几个特征。此选择器对提高模型的预测能力很有用。以下代码将从特征向量中选择前三个特征,以及给出的输出:

org.apache.spark.ml.feature.ChiSqSelector selector = new org.apache.spark.ml.feature.ChiSqSelector(); 
selector.setNumTopFeatures(3).setFeaturesCol("features") 
        .setLabelCol("label").setOutputCol("selectedFeatures"); 
Dataset<Row> result = selector.fit(rescaledData).transform(rescaledData); 
    for (Row r : result.select("selectedFeatures", "label").collectAsList()) { 
  Vector features = r.getAs(0); 
  Double label = r.getDouble(1); 
  System.out.println(label + "," + features); 
    } 

提示

我们将在第六章中更多地讨论ChiSqSelectorIDFModelIDFStopWordsRemoverRegexTokenizer类,构建可扩展的机器学习管道

输出如下:

0.0,(3,[],[]) 
0.0,(3,[],[]) 
0.0,(3,[],[]) 
0.0,(3,[],[]) 
1.0,(3,[1,2],[1.5040773967762742,2.1972245773362196]) 
0.0,(3,[],[]) 
0.0,(3,[],[]) 
1.0,(3,[0,2],[1.5040773967762742,1.0986122886681098]) 

现在,当构建具有特征向量的模型时,您可以应用LogisticRegression。Spark 提供了许多不同的特征工程 API。但是,出于简洁和页面限制的原因,我们没有使用 Spark 的其他机器学习(即 Spark MLlib)。我们将在未来的章节中逐渐讨论使用spark.mllib进行特征工程的示例。

高级特征工程

在本节中,我们将讨论一些高级特性,这些特性也涉及到特征工程过程,如手动特征构建,特征学习,特征工程的迭代过程和深度学习。

特征构建

最好的结果来自于您通过手动特征工程或特征构建。因此,手动构建是从原始数据中创建新特征的过程。基于特征重要性的特征选择可以告诉您有关特征的客观效用;然而,这些特征必须来自其他地方。事实上,有时候,您需要手动创建它们。

与特征选择相比,特征构建技术需要花费大量的精力和时间,不是在聚合或挑选特征上,而是在实际的原始数据上,以便新特征能够有助于提高模型的预测准确性。因此,它还涉及思考数据的潜在结构以及机器学习问题。

在这方面,要从复杂和高维数据集中构建新特征,您需要了解数据的整体结构。除此之外,还需要知道如何在预测建模算法中使用和应用它们。在表格、文本和多媒体数据方面将有三个方面:

  • 处理和手动创建表格数据通常意味着混合组合特征以创建新特征。您可能还需要分解或拆分一些原始特征以创建新特征。

  • 对于文本数据,通常意味着设计文档或上下文特定的指标与问题相关。例如,当您在大型原始数据上应用文本分析,比如来自 Twitter 标签的数据。

  • 对于多媒体数据,比如图像数据,通常需要花费大量时间以手动方式挑选出相关结构。

不幸的是,特征构建技术不仅是手动的,整个过程也更慢,需要人类像你和我们一样进行大量的研究。然而,从长远来看,它可能会产生重大影响。事实上,特征工程和特征选择并不是互斥的;然而,在机器学习领域,它们都很重要。

特征学习

是否可能避免手动指定如何从原始数据中构建或提取特征的过程?特征学习可以帮助您摆脱这一点。因此,特征学习是一个高级过程;或者说是从原始数据中自动识别和使用特征的过程。这也被称为表示学习,有助于您的机器学习算法识别有用的特征。

特征学习技术通常用于深度学习算法。因此,最近的深度学习技术在这一领域取得了一些成功。自动编码器和受限玻尔兹曼机就是使用特征学习概念的例子。特征学习的关键思想是使用无监督或半监督学习算法以压缩形式自动和抽象地表示特征。

语音识别、图像分类和物体识别是一些成功的例子;研究人员在这些领域取得了最新的支持结果。由于篇幅有限,本书中无法详细介绍更多细节。

不幸的是,Spark 还没有实现任何自动特征提取或构建的 API。

特征工程的迭代过程

特征工程的整个过程不是独立的,而是更多或少是迭代的。因为您实际上是在数据选择到模型评估之间反复交互,直到您完全满意或时间用尽。迭代可以想象为一个随时间迭代运行的四步工作流程。当您聚合或收集原始数据时,您可能没有进行足够的头脑风暴。然而,当您开始探索数据时,您真的深入了解问题。

之后,您将会看到大量数据,研究特征工程的最佳技术以及现有技术中提出的相关问题,您将看到自己能够偷取多少。当您进行了足够的头脑风暴后,您将开始设计所需的特征或根据问题类型或类别提取特征。您可以使用自动特征提取或手动特征构建(有时两者都有)。如果对性能不满意,您可能需要重新进行特征提取以改进。请参考图 7,以清晰地了解特征工程的迭代过程:

特征工程的迭代过程

图 8:特征工程中的迭代处理。

当您设计或提取了特征后,您需要选择特征。您可以根据特征重要性应用不同的评分或排名机制。同样,您可能需要迭代相同的过程,如设计特征以改进模型。最后,您将评估模型,以估计模型在新数据上的准确性,使您的模型具有适应性。

您还需要一个明确定义的问题,这将帮助您停止整个迭代。完成后,您可以继续尝试其他模型。一旦您在想法或准确性增量上达到平台,未来将有收益等待着您的 ML 管道。

深度学习

我们可以说,数据表示中最有趣和有前途的举措之一是深度学习。它在张量计算应用和人工智能神经网络AINN)系统中非常受欢迎。使用深度学习技术,网络学习如何在不同层次表示数据。

因此,您将具有表示您拥有的线性数据的指数能力。Spark 可以利用这一优势,用于改进深度学习。有关更一般的讨论,请参阅以下网址en.wikipedia.org/wiki/Deep_learning,要了解如何在 TensorFlow 集群上部署管道,请参见www.tensorflow.org/

Databricks 最近的研究和开发(也请参阅databricks.com/)表明,Spark 也可以用于找到 AINN 训练的最佳超参数集。优势在于,Spark 的计算速度比普通的深度学习或神经网络算法快 10 倍。

因此,您的模型训练时间将大幅减少多达 10 倍,错误率将降低 34%。此外,Spark 可以应用于大量数据上训练的 AINN 模型,因此您可以大规模部署您的 ML 模型。我们将在后面的章节中更多地讨论深度学习作为高级机器学习。

总结

特征工程、特征选择和特征构建是准备训练和测试集以构建机器学习模型时最常用的三个步骤。通常,首先应用特征工程从可用数据集中生成额外的特征。之后,应用特征选择技术来消除不相关、缺失或空值、冗余或高度相关的特征,以便获得高预测准确性。

相比之下,特征构建是一种高级技术,用于构建在原始数据集中要么不存在要么微不足道的新特征。

请注意,并不总是需要进行特征工程或特征选择。是否进行特征选择和构建取决于您拥有或收集的数据,您选择了什么样的 ML 算法,以及实验本身的目标。

在本章中,我们已经详细描述了所有三个步骤,并提供了实际的 Spark 示例。在下一章中,我们将详细描述使用两个机器学习 API(Spark MLlib 和 Spark ML)的监督学习和无监督学习的一些实际示例。

第五章:通过示例进行监督和无监督学习

在第二章中,《机器学习最佳实践》读者学习了一些基本机器学习技术的理论基础。而第三章中,《通过了解数据来理解问题》,描述了使用 Spark 的 API(如 RDD、DataFrame 和 Datasets)进行基本数据操作。另一方面,第四章描述了特征工程的理论和实践。然而,在本章中,读者将学习到快速而强大地在可用数据上应用监督和无监督技术以解决新问题所需的实际知识,这些知识是基于前几章的理解,并且将从 Spark 的角度演示这些例子。简而言之,本章将涵盖以下主题:

  • 机器学习课程

  • 监督学习

  • 无监督学习

  • 推荐系统

  • 高级学习和泛化

机器学习课程

正如在第一章中所述,《使用 Spark 进行数据分析简介》和第二章中,《机器学习最佳实践》,机器学习技术可以进一步分为三类主要算法:监督学习、无监督学习和推荐系统。分类和回归算法在监督学习应用开发中被广泛使用,而聚类则属于无监督学习的范畴。在本节中,我们将描述一些监督学习技术的示例。

接下来我们将提供一些使用 Spark 呈现的相同示例的例子。另一方面,聚类技术的示例将在“无监督学习”部分中讨论,回归技术经常模拟变量之间的过去关系,以预测它们的未来变化(上升或下降)。在这里,我们分别展示了分类和回归算法的两个现实生活中的例子。相比之下,分类技术接受一组带有已知标签的数据,并学习如何基于该信息为新记录打上标签:

  • 示例(分类):Gmail 使用一种称为分类的机器学习技术,根据电子邮件的数据来确定电子邮件是否为垃圾邮件。

  • 示例(回归):举个例子,假设你是一名在线货币交易员,你在外汇或 Fortrade 上工作。现在你心里有两个货币对要买入或卖出,比如:GBP/USD 和 USD/JPY。如果你仔细观察这两对货币,USD 是这两对货币的共同点。现在,如果你观察 USD、GBP 或 JPY 的历史价格,你可以预测未来的结果,即你应该开仓买入还是卖出。这些问题可以通过使用回归分析的监督学习技术来解决:机器学习课程

图 1:分类、聚类和协同过滤-大局观

另一方面,聚类和降维常用于无监督学习。以下是一些示例:

  • 示例(聚类):Google 新闻使用一种称为聚类的技术,根据标题和内容将新闻文章分成不同的类别。聚类算法发现数据集中出现的分组。

  • 示例(协同过滤):协同过滤算法经常用于推荐系统的开发。像亚马逊和 Netflix 这样的知名公司使用一种称为协同过滤的机器学习技术,根据用户的历史和与其他用户的相似性来确定用户会喜欢哪些产品。

  • 示例(降维):降维通常用于使高维数据集变得更加可用。例如,假设您有一张尺寸为 2048x1920 的图像,并且希望将其降维到 1080x720,而不会牺牲太多质量。在这种情况下,可以使用流行的算法,如主成分分析PCA)或奇异值分解SVD),尽管您也可以实现 SVD 来实现 PCA。这就是为什么 SVD 更广泛使用的原因。

监督学习

正如已经说明的,监督学习应用程序基于一组示例进行预测,其目标是学习将输入与与现实世界相一致的输出相映射的一般规则。例如,垃圾邮件过滤的数据集通常包含垃圾邮件和非垃圾邮件。因此,我们可以知道训练集中哪些消息是垃圾邮件或非垃圾邮件。因此,监督学习是从标记的训练数据中推断函数的机器学习技术。监督学习任务涉及以下步骤:

  • 使用训练数据集训练 ML 模型

  • 使用测试数据集测试模型性能

因此,在这种情况下,用于训练 ML 模型的数据集被标记为感兴趣的值,监督学习算法会寻找这些值标签中的模式。算法找到所需的模式后,这些模式可以用于对未标记的测试数据进行预测。

监督学习的典型用途多种多样,通常用于生物信息学、化学信息学、数据库营销、手写识别、信息检索、计算机视觉中的对象识别、光学字符识别、垃圾邮件检测、模式识别、语音识别等应用中,这些应用中主要使用分类技术。另一方面,监督学习是生物系统中向下因果关系的特例。

提示

有关监督学习技术如何从理论角度工作的更多信息可以在以下书籍中找到:Vapnik, V. N. 统计学习理论的本质(第二版),Springer Verlag,2000 年;以及 Mehryar M.,Afshin R. Ameet T.(2012)机器学习基础,麻省理工学院出版社 ISBN 9780262018258。

监督学习示例

分类是一类监督机器学习算法,将输入指定为预定义类别之一。分类的一些常见用例包括:

  • 信用卡欺诈检测

  • 电子邮件垃圾邮件检测

分类数据被标记,例如垃圾邮件/非垃圾邮件或欺诈/非欺诈。机器学习为新数据分配标签或类别。您根据预先确定的特征对某物进行分类。特征是您提出的“如果问题”。标签是这些问题的答案。例如,如果一个对象像鸭子一样走路,游泳和呱呱叫,那么标签将是鸭子。或者假设航班延误超过 1 小时,那么它将是延误;否则不是延误。

使用 Spark 进行监督学习-一个例子

我们将通过分析航班延误来演示一个示例。将使用美国交通部网站上的名为On_Time_Performance_2016_1.csv的数据集www.transtats.bts.gov/

使用 Spark 进行航班延误分析

我们使用 2016 年的航班信息。对于每次航班,我们在表 1中提供了以下信息(截至 2016 年 5 月 17 日,共 444,827 行和 110 列):

数据字段 描述 示例值
DayofMonth 月份 2
DayOfWeek 星期几 5
TailNum 飞机尾号 N505NK
FlightNum 航班号 48
AirlineID 航空公司 ID 19805
OriginAirportID 起飞机场 ID JFK
DestAirportID 目的地机场 ID LAX
Dest 目的地机场代码 1424
CRSDepTime 计划起飞时间 10:00
DepTime 实际起飞时间 10:30
DepDelayMinutes 起飞延误时间 30
CRSArrTime 计划到达时间 22:45
ArrTime 实际到达时间 23:45
ArrDelayMinutes 到达延误时间 60
CRSElapsedTime 飞行时间 825
Distance 总距离 6200

表 1:来自“准时表现 2016_1”数据集的样本数据

在这种情况下,我们将构建一棵树,根据图中显示的以下特征来预测延误或未延误的标签,这是航班数据集的一个小快照。这里ArrDelayMinutes为 113,应该被分类为延误(1.0),其他行的延误时间少于 60 分钟,因此标签应为 0.0(未延误)。从这个数据集中,我们将进行一些操作,如特征提取、转换和选择。表 2显示了我们将在此示例中考虑的与特征相关的前五行:

  • 标签:延误和未延误 - 如果延误>60 分钟,则为延误

  • 特征:{DayOfMonth, WeekOfday, CRSdeptime, CRSarrtime, Carrier, CRSelapsedtime, Origin, Dest, ArrDelayMinutes}使用 Spark 进行航班延误分析

图 2:用于航班延误预测的选定特征

加载和解析数据集

在执行特征提取之前,我们需要加载和解析数据集。这一步还包括:加载包和相关依赖项,将数据集读取为 DataFrame,创建 POJO 或 Bean 类,并根据要求添加新的标签列。

步骤 1:加载所需的包和依赖项

为了读取 csv 文件,我们使用了 Databricks 提供的 csv 读取器:

import org.apache.log4j.Level; 
import org.apache.log4j.Logger; 
import org.apache.spark.api.java.JavaRDD; 
import org.apache.spark.api.java.function.Function; 
import org.apache.spark.ml.Pipeline; 
import org.apache.spark.ml.PipelineModel; 
import org.apache.spark.ml.PipelineStage; 
import org.apache.spark.ml.classification.DecisionTreeClassificationModel; 
import org.apache.spark.ml.classification.DecisionTreeClassifier; 
import org.apache.spark.ml.evaluation.MulticlassClassificationEvaluator; 
import org.apache.spark.ml.feature.IndexToString; 
import org.apache.spark.ml.feature.LabeledPoint; 
import org.apache.spark.ml.feature.StringIndexer; 
import org.apache.spark.ml.feature.StringIndexerModel; 
import org.apache.spark.ml.feature.VectorAssembler; 
import org.apache.spark.ml.feature.VectorIndexer; 
import org.apache.spark.ml.feature.VectorIndexerModel; 
import org.apache.spark.ml.linalg.Vector; 
import org.apache.spark.rdd.RDD; 
import org.apache.spark.sql.Dataset; 
import org.apache.spark.sql.Row; 
import org.apache.spark.sql.SparkSession; 
import scala.Tuple2; 

步骤 2:创建 Spark 会话

以下是创建 Spark 会话的代码:

  static SparkSession spark = SparkSession 
      .builder() 
      .appName("JavaLDAExample") 
      .master("local[*]") 
      .config("spark.sql.warehouse.dir", "E:/Exp/") 
      .getOrCreate(); 

步骤 3:使用数据集读取和解析 csv 文件

这个数据集包含许多列,我们在这个示例中不会将其作为特征。因此,我们将从 DataFrame 中仅选择我们之前提到的特征。这个 DataFrame 的输出已经在图 2中显示过了:

String csvFile = "input/On_Time_On_Time_Performance_2016_1.csv"; 
Dataset<Row> df = spark.read().format("com.databricks.spark.csv").option("header", "true").load(csvFile);  
RDD<Tuple2<String, String>> distFile = spark.sparkContext().wholeTextFiles("input/test/*.txt", 2); 
JavaRDD<Tuple2<String, String>> distFile2 = distFile.toJavaRDD(); 
JavaRDD<Row> rowRDD = df.toJavaRDD(); 
Dataset<Row> newDF = df.select(df.col("ArrDelayMinutes"), 
df.col("DayofMonth"), df.col("DayOfWeek"), 
df.col("CRSDepTime"), df.col("CRSArrTime"), df.col("Carrier"), 
df.col("CRSElapsedTime"), df.col("Origin"), df.col("Dest")); 
newDF.show(5); 

以下是前 5 行的输出:

加载和解析数据集

步骤 4:创建 POJO 或 Bean 类

我们开发的 POJO 类名为Flight,其中将使用相应的 setter 和 getter 定义所需的特征和标签字段。

public class Flight implements Serializable { 
  double label; 
  double monthDay; 
  double weekDay; 
  double crsdeptime; 
  double crsarrtime; 
  String carrier; 
  double crselapsedtime; 
  String origin; 
  String dest; 

public Flight(double label, double monthDay, double weekDay, double crsdeptime, double crsarrtime, String carrier, 
      double crselapsedtime, String origin, String dest) { 
    super(); 
    this.label = label; 
    this.monthDay = monthDay; 
    this.weekDay = weekDay; 
    this.crsdeptime = crsdeptime; 
    this.crsarrtime = crsarrtime; 
    this.carrier = carrier; 
    this.crselapsedtime = crselapsedtime; 
    this.origin = origin; 
    this.dest = dest; 
  } 
  public double getLabel() { 
    return label; 
  } 
  public void setLabel(double label) { 
    this.label = label; 
  } 
  public double getMonthDay() { 
    return monthDay; 
  } 
  public void setMonthDay(double monthDay) { 
    this.monthDay = monthDay; 
  } 
  public double getWeekDay() { 
    return weekDay; 
  } 
  public void setWeekDay(double weekDay) { 
    this.weekDay = weekDay; 
  } 
  public double getCrsdeptime() { 
    return crsdeptime; 
  } 
  public void setCrsdeptime(double crsdeptime) { 
    this.crsdeptime = crsdeptime; 
  } 
  public double getCrsarrtime() { 
    return crsarrtime; 
  } 
  public void setCrsarrtime(double crsarrtime) { 
    this.crsarrtime = crsarrtime; 
  } 
  public String getCarrier() { 
    return carrier; 
  } 
  public void setCarrier(String carrier) { 
    this.carrier = carrier; 
  } 
  public double getCrselapsedtime() { 
    return crselapsedtime; 
  } 
  public void setCrselapsedtime(double crselapsedtime) { 
    this.crselapsedtime = crselapsedtime; 
  } 
  public String getOrigin() { 
    return origin; 
  } 
  public void setOrigin(String origin) { 
    this.origin = origin; 
  } 
  public String getDest() { 
    return dest; 
  } 
  public void setDest(String dest) { 
    this.dest = dest; 
  } 
  @Override 
  public String toString() { 
    return "Flight [label=" + label + ", monthDay=" + monthDay + ", weekDay="
       + weekDay + ", crsdeptime=" 
        + crsdeptime + ", crsarrtime=" + crsarrtime + ", carrier=" + 
      carrier + ", crselapsedtime=" 
        + crselapsedtime + ", origin=" + origin + ", dest=" +
       dest + "]"; 
  } 

我们相信前面的类是不言自明的,它用于从原始数据集中设置和获取特征值。

步骤 5:根据延误列添加新的标签列

如果延误超过 40 分钟,则标签应为 1,否则应为 0。使用 Flight bean 类创建一个新的数据集。这个数据集可以在ArrDelayMinutes列中包含空字符串。因此,在映射之前,我们从数据集中过滤掉包含空字符串的行:

JavaRDD<Flight> flightsRDD = newDF.toJavaRDD().filter(new Function<Row, Boolean>() { 
          @Override 
          public Boolean call(Row v1) throws Exception { 
            return !v1.getString(0).isEmpty(); 
          } 
        }).map(new Function<Row, Flight>() { 
          @Override 
          public Flight call(Row r) throws Exception { 
            double label; 
            double delay = Double.parseDouble(r.getString(0)); 
            if (delay > 60) 
              label = 1.0; 
else 
      label = 0.0; 
double monthday = Double.parseDouble(r.getString(1)) - 1; 
double weekday = Double.parseDouble(r.getString(2)) - 1; 
double crsdeptime = Double.parseDouble(r.getString(3)); 
double crsarrtime = Double.parseDouble(r.getString(4)); 
String carrier = r.getString(5); 
double crselapsedtime1 = Double.parseDouble(r.getString(6)); 
String origin = r.getString(7); 
String dest = r.getString(8); 
Flight flight = new Flight(label, monthday, weekday,crsdeptime, crsarrtime, carrier,crselapsedtime1, origin, dest); 
        return flight; 
    }}); 

现在从上面创建的 RDD 中创建一个新的数据集:

Dataset<Row> flightDelayData = spark.sqlContext().createDataFrame(flightsRDD,Flight.class); 
flightDelayData.printSchema(); 

现在在下面的图 3中显示数据帧flightDelayData的前 5 行:

flightDelayData.show(5); 

[输出:]

加载和解析数据集

图 3:显示新标签列的 DataFrame

特征提取

为了提取特征,我们必须制作数值,并且如果有任何文本值,那么我们必须制作一个标记向量,以应用机器学习算法。

第 1 步:转向特征提取

在这里,我们将把包含文本的列转换为双值列。在这里,我们使用StringIndexer为每个唯一的文本制作一个唯一的索引:

StringIndexer carrierIndexer = new StringIndexer().setInputCol("carrier").setOutputCol("carrierIndex"); 
Dataset<Row> carrierIndexed = carrierIndexer.fit(flightDelayData).transform(flightDelayData); 
StringIndexer originIndexer = new StringIndexer().setInputCol("origin").setOutputCol("originIndex"); 
Dataset<Row> originIndexed = originIndexer.fit(carrierIndexed).transform(carrierIndexed); 
StringIndexer destIndexer = new StringIndexer().setInputCol("dest").setOutputCol("destIndex"); 
Dataset<Row> destIndexed = destIndexer.fit(originIndexed).transform(originIndexed); 
destIndexed.show(5); 

[输出]:

特征提取

图 4:每个唯一文本的唯一索引

第 2 步:使用向量组装器制作特征向量

使用向量组装器制作特征向量,并将其转换为标记向量,以应用机器学习算法(决策树)。请注意,这里我们使用决策树只是举个例子,因为它显示了更好的分类准确性。根据算法和模型的选择和调整,您将能够进一步探索和使用其他分类器:

VectorAssembler assembler = new VectorAssembler().setInputCols( 
        new String[] { "monthDay", "weekDay", "crsdeptime", 
            "crsarrtime", "carrierIndex", "crselapsedtime", 
            "originIndex", "destIndex" }).setOutputCol( 
        "assembeledVector"); 

现在将组装器转换为行数据集,如下所示:

Dataset<Row> assembledFeatures = assembler.transform(destIndexed); 

现在将数据集转换为JavaRDD,以制作特征向量,如下所示:

JavaRDD<Row> rescaledRDD = assembledFeatures.select("label","assembeledVector").toJavaRDD(); 

按如下方式将 RDD 映射为LabeledPoint

JavaRDD<LabeledPoint> mlData = rescaledRDD.map(new Function<Row, LabeledPoint>() { 
          @Override 
          public LabeledPoint call(Row row) throws Exception { 
            double label = row.getDouble(0); 
            Vector v = row.getAs(1); 
            return new LabeledPoint(label, v); 
          } 
        }); 

现在按如下方式打印前五个值:

System.out.println(mlData.take(5));  

[输出]:

特征提取

图 5:相应的组装向量

准备训练和测试集

在这里,我们将从标记向量的数据集中准备训练数据集。最初,我们将制作一个训练集,其中 15%的记录将是非延迟记录,85%将是延迟记录。最后,训练和测试数据集将分别准备为 70%和 30%。

第 1 步:从整个数据集中制作训练和测试集

首先,根据之前创建的标签(即 1 和 0)来过滤 RDD,创建一个新的 RDD,如下所示:

JavaRDD<LabeledPoint> splitedData0 = mlData.filter(new Function<LabeledPoint, Boolean>() { 
          @Override 
          public Boolean call(LabeledPoint r) throws Exception { 
              return r.label() == 0; 
          } 
        }).randomSplit(new double[] { 0.85, 0.15 })[1]; 

    JavaRDD<LabeledPoint> splitedData1 = mlData.filter(new Function<LabeledPoint, Boolean>() { 
          @Override 
          public Boolean call(LabeledPoint r) throws Exception { 
            return r.label() == 1; 
          } 
        }); 

    JavaRDD<LabeledPoint> splitedData2 = splitedData1.union(splitedData0); 
    System.out.println(splitedData2.take(1)); 

现在使用union()方法将两个 RDD 联合起来,如下所示:

JavaRDD<LabeledPoint> splitedData2 = splitedData1.union(splitedData0); 
System.out.println(splitedData2.take(1)); 

现在将合并的 RDD 进一步转换为行数据集,如下所示(最大类别设置为 4):

Dataset<Row> data = spark.sqlContext().createDataFrame(splitedData2, LabeledPoint.class); 
data.show(100); 

现在我们需要对分类变量进行向量索引,如下所示:

VectorIndexerModel featureIndexer = new VectorIndexer() 
          .setInputCol("features") 
          .setOutputCol("indexedFeatures") 
          .setMaxCategories(4) 
          .fit(data); 

现在我们已经使用VectorIndexerModel估计器进行了特征索引。现在下一个任务是使用StringIndexerModel估计器进行字符串索引,如下所示:

StringIndexerModel labelIndexer = new StringIndexer() 
          .setInputCol("label") 
          .setOutputCol("indexedLabel") 
          .fit(data); 

最后,将行数据集分割为训练和测试集(分别为 70%和 30%,但您应根据您的需求调整值),如下所示:

Dataset<Row>[] splits = data.randomSplit(new double[]{0.7, 0.3}); 
Dataset<Row> trainingData = splits[0]; 
Dataset<Row> testData = splits[1]; 

干得好!现在我们的数据集已经准备好训练模型了,对吧?暂时,我们会天真地选择一个分类器,比如说让我们使用决策树分类器来解决我们的目的。您可以根据第六章, 构建可扩展的机器学习管道,第七章, 调整机器学习模型,和 第八章, 调整您的机器学习模型中提供的示例尝试其他多类分类器。

训练模型

图 2所示,训练和测试数据将从原始数据中收集。在特征工程过程完成后,带有标签或评级的特征向量的 RDD 将在构建预测模型之前通过分类算法进行处理(如图 6所示),最后测试数据将用于测试模型的性能:

训练模型

图 6:使用 Spark 进行监督学习

接下来,我们为决策树所需的参数值做准备。也许你会想为什么我们要谈论决策树。原因很简单,因为我们观察到使用决策树(即二叉决策树)相比朴素贝叶斯方法有更好的预测准确度。参考表 2,描述了分类特征及其重要性如下:

分类特征 映射 重要性
categoricalFeaturesInfo 0 -> 31 指定特征索引 0(代表月份的天数)有 31 个类别[值{0,...,31}]
categoricalFeaturesInfo 1 -> 7 表示一周的天数,并指定特征索引 1 有七个类别
Carrier 0 -> N N 表示从 0 到不同航空公司的数量

表 2:分类特征及其重要性

现在我们将简要描述决策树构建的方法。我们将使用 CategoricalFeaturesInfo 来指定哪些特征是分类的,以及在树构建过程中每个特征可以取多少个分类值。这是一个从特征索引到该特征的类别数的映射。

然而,该模型是通过将输入特征与与这些特征相关联的标记输出进行关联来训练的。我们使用DecisionTreeClassifier方法训练模型,最终返回一个DecisionTreeModel,如图 7所示。构建树的详细源代码将在本节后面显示。

训练模型

图 7:为航班延误分析生成的二叉决策树(部分显示)

步骤 1:训练决策树模型

要训练决策树分类器模型,我们需要有必要的标签和特征:

DecisionTreeClassifier dt = new DecisionTreeClassifier() 
      .setLabelCol("indexedLabel") 
      .setFeaturesCol("indexedFeatures"); 

步骤 2:将索引标签转换回原始标签

要创建决策树管道,我们需要除了索引标签之外的原始标签。因此,让我们按照以下方式进行:

IndexToString labelConverter = new IndexToString() 
      .setInputCol("prediction") 
      .setOutputCol("predictedLabel")         
        .setLabels(labelIndexer.labels());  

步骤 3:将索引器和树链接成一个单一管道

创建一个新的管道,其中阶段如下:labelIndexerfeatureIndexerdtlabelConverter如下:

Pipeline pipeline = new Pipeline() 
      .setStages(new PipelineStage[]{labelIndexer,  
        featureIndexer, dt, labelConverter}); 

现在使用我们在步骤 8中创建的训练集来拟合管道如下:

PipelineModel model = pipeline.fit(trainingData); 

测试模型

在接下来的步骤中,我们将测试模型:

步骤 1:对测试数据集进行预测

通过转换PipelineModel对测试集进行预测,并显示性能参数如下:

Dataset<Row> predictions = model.transform(testData); 
predictions.select("predictedLabel", "label", "features").show(5); 

步骤 2:评估模型

通过多类分类评估器评估模型,并打印准确度和测试错误如下:

MulticlassClassificationEvaluator evaluator = new MulticlassClassificationEvaluator() 
      .setLabelCol("indexedLabel") 
      .setPredictionCol("prediction") 
      .setMetricName("accuracy"); 
    double accuracy = evaluator.evaluate(predictions); 
    System.out.println("accuracy: "+accuracy); 
    System.out.println("Test Error = " + (1.0 - accuracy)); 

前面的代码段生成了分类准确度和测试错误如下:

Accuracy: 0.7540472721385786 
Test Error = 0.24595272786142142 

请注意,由于我们随机将数据集分成训练集和测试集,你可能会得到不同的结果。分类准确度为 75.40%,这并不好,我们认为。

现在轮到你使用不同的分类器和调整模型了。有关调整 ML 模型的更多详细讨论将在第七章中进行,调整机器学习模型

步骤 3:打印决策树

以下是打印决策树的代码:

DecisionTreeClassificationModel treeModel = 
      (DecisionTreeClassificationModel) (model.stages()[2]); 
System.out.println("Learned classification tree model:\n" + treeModel.toDebugString()); 

此代码段生成一个决策树,如图 7所示。

步骤 4:停止 Spark 会话

使用 Spark 的stop()方法停止 Spark 会话如下:

spark.stop();

这是一个很好的做法,你要正确启动和关闭或停止 Spark 会话,以避免应用程序中的内存泄漏。

无监督学习

在无监督学习中,数据点没有与之相关的标签;因此,我们需要通过算法给它们贴上标签。换句话说,在无监督学习中,训练数据集的正确类别是未知的。

因此,必须从非结构化数据集中推断出类别,这意味着无监督学习算法的目标是通过描述其结构以某种结构化方式预处理数据。无监督学习算法或技术的主要目标是探索大多数未标记的输入数据的未知模式。这样,它与理论和应用统计学中使用的密度估计问题密切相关。

然而,无监督学习还包括许多其他技术,以总结和解释数据的关键特征,包括用于发现这些隐藏模式的探索性数据分析,甚至对数据点或特征进行分组,并根据数据挖掘方法应用无监督学习技术进行数据预处理。

为了克服无监督学习中的这一障碍,通常使用聚类技术根据某些相似性度量对未标记的样本进行分组,挖掘隐藏的模式以进行特征学习。

提示

要深入了解理论知识,了解无监督算法的工作原理,请参考以下三本书:Bousquet, O.; von Luxburg, U.; Raetsch, G., eds. (2004). Advanced Lectures on Machine Learning. Springer-Verlag. ISBN 978-3540231226。或者 Duda, Richard O.; Hart, Peter E.; Stork, David G. (2001). Unsupervised Learning and Clustering. Pattern Classification (2nd Ed.). Wiley. ISBN 0-471-05669-3 和 Jordan, Michael I.; Bishop, Christopher M. (2004). Neural Networks. In Allen B. Tucker. Computer Science Handbook, Second Edition (Section VII: Intelligent Systems). Boca Raton, FL: Chapman & Hall/CRC Press LLC. ISBN 1-58488-360-X。

无监督学习示例

在聚类中,算法通过分析输入示例之间的相似性将对象分组到类别中,其中相似的对象或特征被聚类并用圆圈标记。

聚类的用途包括:搜索结果分组,如客户分组,异常检测用于发现可疑模式,文本分类用于在测试中发现有用的模式,社交网络分析用于找到连贯的群体,数据中心计算集群用于找到将相关计算机放在一起以提高性能的方法,天文数据分析用于星系形成,以及房地产数据分析用于基于相似特征识别社区。此外,聚类使用无监督算法,事先没有输出。

使用 K 均值算法进行聚类是通过将所有坐标初始化为质心开始的。请注意,Spark 还支持其他聚类算法,如高斯混合幂迭代聚类PIC),潜在狄利克雷分配LDA),二分 K 均值和流式 K 均值。而高斯混合主要用于期望最小化作为优化算法,另一方面,LDA 用于文档分类和聚类。PIC 用于根据边属性的成对相似性对图的顶点进行聚类。二分 K 均值比常规 K 均值更快,但通常会产生不同的聚类。因此,为了使讨论更简单,我们将在我们的目的中使用 K 均值算法。

有兴趣的读者应该参考 Spark ML 和基于 Spark MLlib 的聚类技术,分别在spark.apache.org/docs/latest/ml-clustering.htmlspark.apache.org/docs/latest/mllib-clustering.html网页上获取更多见解。在算法的每次迭代中,根据某种距离度量,通常是欧几里得距离,每个点都被分配到其最近的质心。

请注意,还有其他计算距离的方法,例如,切比雪夫距离用于仅考虑最重要的维度来测量距离。汉明距离算法用于逐位识别两个字符串的不同。马哈 alanobis 距离用于将协方差矩阵标准化,使距离度量在尺度上不变。

曼哈顿距离用于仅遵循轴对齐方向的距离。闵可夫斯基距离算法用于使欧几里德距离、曼哈顿距离和切比雪夫距离泛化。Haversine 距离用于测量球面上两点之间的大圆距离,根据它们的经度和纬度。考虑这些距离测量算法,很明显,欧几里得距离算法将是解决我们问题最合适的方法。

然后更新质心为该通行中分配给它的所有点的中心。这一过程重复,直到中心发生最小变化。K 均值算法是一个迭代算法,分为两步:

  • 簇分配步骤:该算法将遍历每个数据点,并根据它离哪个质心更近来分配该质心,进而分配它代表的簇。

  • 移动质心步骤:该算法将取每个质心并将其移动到簇中数据点的平均值

使用 Spark 进行无监督学习-一个例子

我们将使用从 URL course1.winona.edu/bdeppa/Stat%20425/Datasets.html 下载的Saratoga NY Homes来演示使用 Java 中的 Spark 作为无监督学习技术的聚类的一个例子。数据集包含以下几个特征:价格、地块大小、水边、年龄、土地价值、新建、中央空调、燃料类型、加热类型、下水道类型、居住面积、大学百分比、卧室、壁炉、浴室和房间数量。然而,在这些列中,我们只在表 3中显示了一些选择的列。请注意,原始数据集是下载的,后来转换为相应的文本文件作为制表符分隔符:

价格 地块大小 水边 年龄 土地价值 房间
132500 0.09 0 42 5000 5
181115 0.92 0 0 22300 6
109000 0.19 0 133 7300 8
155000 0.41 0 13 18700 5
86060 0.11 0 0 15000 3
120000 0.68 0 31 14000 8
153000 0.4 0 33 23300 8
170000 1.21 0 23 146000 9
90000 0.83 0 36 222000 8
122900 1.94 0 4 212000 6
325000 2.29 0 123 126000 12

表 3:来自“Saratoga NY Homes”数据集的样本数据

我们进一步仅使用前两个特征(即价格和地块大小),以简化前一章中介绍的 Spark 特征学习算法。我们的目标是基于这两个特征对位于同一区域的房屋可能的邻域进行探索性分析。首先,看一下基于数据集中的值的基本散点图:

使用 Spark 进行无监督学习-一个例子

图 8:邻域的簇

很明显,在图 8中标有圆圈的图中有四个簇。然而,确定簇的数量是一项棘手的任务。在这里,我们有视觉检查的优势,这对于超平面或多维数据上的数据是不可用的。现在我们需要使用 Spark 找到相同的结果。为简单起见,我们将使用 Spark 的 K 均值聚类 API。原始数据的使用和查找特征向量如图 9所示:

使用 Spark 进行无监督学习-一个例子

图 9:使用 Spark 进行无监督学习

邻域的 K 均值聚类

在执行特征提取之前,我们需要加载和解析 Saratoga NY Homes 数据集。这一步还包括:加载包和相关依赖项,将数据集读取为 RDD,模型训练和预测,收集本地解析的数据,并进行聚类比较。

步骤 1:导入统计和相关类

以下是导入统计和相关类的代码:

import java.io.Serializable; 
import java.util.List; 
import org.apache.spark.api.java.JavaRDD; 
import org.apache.spark.api.java.function.Function; 
import org.apache.spark.mllib.clustering.KMeans; 
import org.apache.spark.mllib.clustering.KMeansModel; 
import org.apache.spark.mllib.linalg.Vector; 
import org.apache.spark.mllib.linalg.Vectors; 
import org.apache.spark.rdd.RDD; 
import org.apache.spark.sql.SparkSession;  

步骤 2:创建 Spark 会话

以下是创建 Spark 会话的代码:

  static SparkSession spark = SparkSession 
      .builder().appName("JavaLDAExample") 
      .master("local[*]") 
      .config("spark.sql.warehouse.dir", "E:/Exp/") 
      .getOrCreate(); 

步骤 3:加载 Saratoga NY Homes.txt

从数据集中读取、解析和创建 RDD:

RDD<String> data = spark.sparkContext().textFile("input/Saratoga_ NY_Homes.txt", 2); 

步骤 4:将数据转换为密集向量的 RDD

如果您仔细遵循前面的步骤,实际上我们已经创建了普通的 RDD。因此,在将其映射为密集向量之前,该 RDD 必须转换为相应的JavaRDD

JavaRDD<Vector> parsedData = data.toJavaRDD().map(new Function<String, Vector>() { 
      @Override 
      public Vector call(String s) throws Exception { 
        String[] sarray = s.split(","); 
        double[] values = new double[sarray.length]; 
        for (int i = 0; i < sarray.length; i++) 
          values[i] = Double.parseDouble(sarray[i]); 
        return Vectors.dense(values); 
      } 
    });  

步骤 5:训练模型

通过指定四个聚类和五次迭代来训练模型。只需参考以下代码来执行:

int numClusters = 4; 
int numIterations = 10; 
int runs = 2; 
KMeansModel clusters = KMeans.train(parsedData.rdd(), numClusters, numIterations, runs , KMeans.K_MEANS_PARALLEL());  
Now estimate the cost to compute the clsuters as follows: 
double cost = clusters.computeCost(parsedData.rdd()); 
System.out.println("Cost: " + cost);  

您应该收到以下结果:

Cost: 3.60148995801542E12   

步骤 6:显示聚类中心

Vector[] centers = clusters.clusterCenters(); 
System.out.println("Cluster Centers: "); 
for (Vector center : centers)  
{ 
  System.out.println(center); 
} 

前面的代码应该产生如下的聚类中心:

[545360.4081632652,0.9008163265306122,0.1020408163265306,21.73469387755102,111630.61224489794,0.061224489795918366,0.7551020408163265,2.3061224489795915,2.1632653061224487,2.714285714285714,2860.755102040816,59.346938775510196,3.510204081632653,1.1020408163265305,2.714285714285714,10.061224489795917] 
[134073.06845637583,0.3820000000000002,0.0026845637583892616,33.72617449664429,19230.76510067114,0.012080536912751677,0.22818791946308722,2.621476510067114,2.7234899328859057,2.6630872483221477,1332.9234899328858,52.86040268456375,2.7395973154362414,0.38120805369127514,1.4946308724832214,5.806711409395973] 
[218726.0625,0.5419711538461538,0.0,25.495192307692307,32579.647435897434,0.041666666666666664,0.3830128205128205,2.3205128205128203,2.4615384615384617,2.692307692307692,1862.3076923076922,57.4599358974359,3.3894230769230766,0.7019230769230769,2.032852564102564,7.44551282051282] 
[332859.0580645161,0.6369354838709671,0.025806451612903226,19.803225806451614,63188.06451612903,0.13870967741935483,0.6096774193548387,2.2225806451612904,2.2483870967741937,2.774193548387097,2378.4290322580646,57.66774193548387,3.6225806451612903,0.8516129032258064,2.479032258064516,8.719354838709677] 

步骤 7:评估模型错误率

double WSSSE = clusters.computeCost(parsedData.rdd()); 
System.out.println("Within Set Sum of Squared Errors = " + WSSSE); 

这应该产生如下结果:

Within Set Sum of Squared Errors = 3.60148995801542E12 

步骤 8:预测第二个元素的聚类

List<Vector> houses = parsedData.collect(); 
int prediction  = clusters.predict(houses.get(18)); 
System.out.println("Prediction: "+prediction);  

输出预测:0

步骤 9:停止 Spark 会话

使用stop()方法停止 Spark 会话如下:

spark.stop(); 

步骤 10:聚类比较

现在让我们比较 k-means 与我们单独完成的聚类分配。k-means 算法从 0 开始给出聚类 ID。一旦您检查数据,您会发现我们在表 4 中给出的 A 到 D 聚类 ID 与 k-means 之间的以下映射:

聚类名称 聚类编号 聚类分配
A 3 A=>3
B 1 B=>1
C 0 C=>0
D 2 D=>2

表 4:邻域 k-means 聚类示例的聚类分配

现在,让我们从图表的不同部分挑选一些数据,并预测它属于哪个聚类。让我们看一下房屋(以 1 为例)的数据,它的占地面积为 876 平方英尺,售价为 65.5 万美元:

int prediction  = clusters.predict(houses.get(18)); 
    System.out.println("Prediction: "+prediction); 

[输出] 预测:2

这意味着具有前述属性的房屋属于聚类 2。当然,您可以通过更多数据测试预测能力。让我们进行一些邻域分析,看看这些聚类承载着什么含义。我们可以假设聚类 3 中的大多数房屋都靠近市中心。例如,聚类 2 中的房屋位于多山的地形上。

在这个例子中,我们处理了一组非常少的特征;常识和视觉检查也会导致我们得出相同的结论。然而,如果您想获得更准确的结果,当然,您应该构建更有意义的特征,不仅考虑占地面积和房价,还要考虑其他特征,如房间数量、房龄、土地价值、供暖类型等。

然而,将滨水作为一个有意义的特征是不明智的,因为在这个例子中没有房子的前面有水上花园。我们将在下一章节中对更准确地预测的更有意义的特征的准确性进行详细分析。

k-means 算法的美妙之处在于它可以对具有无限特征的数据进行聚类。当您有原始数据并想了解数据中的模式时,这是一个很好的工具。然而,在进行实验之前决定聚类的数量可能不成功,有时可能会导致过拟合问题或欠拟合问题。

提示

为了克服 K 均值的上述局限性,我们有一些更健壮的算法,如马尔可夫链蒙特卡洛(MCMC,也见en.wikipedia.org/wiki/Markov_chain_Monte_Carlo)在 Tribble, Seth D.,Markov chain Monte Carlo algorithms using completely uniformly distributed driving sequences,2007 年斯坦福大学博士论文中提出。此外,更多技术讨论可以在www.autonlab.org/tutorials/kmeans11.pdf的网址中找到。

推荐系统

推荐系统是一种原始的杀手级应用程序,是信息过滤系统的一个子类,旨在预测用户通常对项目提供的评分或偏好。推荐系统的概念近年来变得非常普遍,并随后被应用于不同的应用程序。最流行的可能是产品(例如电影、音乐、书籍、研究文章)、新闻、搜索查询、社交标签等。推荐系统可以分为四类,如第二章中所述,机器学习最佳实践。这些显示在图 10中:

  • 协同过滤系统:这是根据行为模式的相似性累积消费者的偏好和对其他用户的推荐基于内容的系统:这里使用监督机器学习来说服分类器区分用户感兴趣和不感兴趣的项目

  • 混合推荐系统:这是最近的研究和混合方法(即,结合协同过滤和基于内容的过滤)

  • 基于知识的系统:这里使用关于用户和产品的知识来理解用户的需求,使用感知树、决策支持系统和基于案例的推理:推荐系统

图 10:推荐系统的层次结构

从技术角度来看,我们可以进一步将它们分类如下:

  • 物品层次结构是最弱的,它天真地假设一个物品与另一个物品相关,例如,如果你买了打印机,你更有可能购买墨水。之前BestBuy使用过这种方法

  • 基于属性的推荐:假设你喜欢史泰龙主演的动作电影,因此你可能会喜欢兰博系列。Netflix 曾经使用过这种方法

  • 协同过滤(用户-用户相似性):假设并且举例说,那些像你一样购买了婴儿奶粉的人也购买了尿布。Target 使用这种方法

  • 协同过滤(物品-物品相似性):假设并且举例说,喜欢教父系列的人也喜欢《疤面煞星》。Netflix 目前使用这种方法

  • 社交、兴趣和基于图的方法:例如,假设喜欢迈克尔·杰克逊的朋友也会喜欢《Just Beat It》。像LinkedInFacebook这样的科技巨头使用这种方法

  • 基于模型的方法:这使用高级算法,如SVMLDASVD基于隐含特征

图 11所示,基于模型的推荐系统广泛使用高级算法,如 SVM、LDA 或 SVD,是推荐系统类中最健壮的方法:

推荐系统

图 11:从技术角度看的推荐系统

Spark 中的协同过滤

如前所述,协同过滤技术通常用于推荐系统。然而,Spark MLlib 目前仅支持基于模型的协同过滤。在这里,用户和产品由一小组潜在因素描述。这些潜在因素后来用于预测缺失的条目。根据 Spark API 参考协同过滤spark.apache.org/docs/latest/mllib-collaborative-filtering.html交替最小二乘法ALS)(也称为非线性最小二乘法,即 NLS;更多信息请参见en.wikipedia.org/wiki/Non-linear_least_squares)算法用于通过考虑以下参数来学习这些潜在因素:

  • numBlocks是使用本机 LAPACK 进行并行计算的块数

  • rank是在构建机器学习模型期间的潜在因素的数量

  • iterations是需要进行更准确预测的迭代次数

  • lambda表示 ALS 算法的正则化参数

  • implicitPrefs指定要使用的反馈(显式反馈 ALS 变体或适用于隐式反馈数据的变体)

  • alpha指定 ALS 算法中偏好观察的基线置信度

首先,ALS 是一种迭代算法,用于将评分矩阵建模为低秩用户和产品因子的乘积。之后,通过最小化观察到的评分的重建误差来使用这些因子进行学习任务。

然而,未知的评分可以通过将这些因素相乘来逐步计算。在 Spark MLlib 中使用的协同过滤技术进行的移动推荐或其他推荐的方法已被证明是一个高性能的方法,具有高预测准确性,并且可扩展到像 Netflix 这样的公司使用的商品集群上的数十亿个评分。按照这种方式,Netflix 这样的公司可以根据预测的评分向其订阅者推荐电影。最终目标是增加销售额,当然也包括客户满意度。

为了简洁和页面限制,我们将不在本章中展示使用协同过滤方法进行电影推荐。但是,将在第九章中展示使用 Spark 的逐步示例,流式数据和图数据的高级机器学习

提示

目前,建议感兴趣的读者访问 Spark 网站获取最新的 API 和相同代码,网址为:spark.apache.org/docs/latest/mllib-collaborative-filtering.html,其中提供了一个示例,展示了使用 ALS 算法进行样本电影推荐。

高级学习和泛化

在本节中,我们将讨论一些学习的高级方面,例如如何将监督学习技术泛化为半监督学习、主动学习、结构化预测和强化学习。此外,将简要讨论强化学习和半监督学习。

监督学习的泛化

标准监督学习问题可以泛化的几种方式:

  • 半监督学习:在这种泛化技术中,仅为所选特征的一部分训练数据提供所需的输出值,以构建和评估机器学习模型。另一方面,其余数据保持不变或未标记。

  • 主动学习:相比之下,在主动学习中,算法通常通过向人类用户提出查询来交互地收集新特征,而不是假设所有训练特征都已给出。因此,这里使用的查询是基于未标记数据的。有趣的是,这也是将半监督学习与主动学习相结合的一个例子。

  • 结构化预测:有时需要从复杂对象(如解析树或带标签的图)中提取或选择所需的特征,然后必须改进标准监督或无监督方法以使其适应于泛化。更准确地说,例如,当监督机器学习技术尝试预测结构化或非结构化文本时,如将自然语言处理句子翻译成句法表示时,需要处理大规模解析树的结构化预测。为了简化这个任务,通常使用结构化 SVM 或马尔可夫逻辑网络或受限条件模型,这些技术上扩展和更新了经典的监督学习算法。

  • 学习排名:当输入本身是对象的子集并且期望的输出是这些对象的排名时,必须类似于结构预测技术来扩展或改进标准方法。

提示

感兴趣的读者可以参考以下两个网址:en.wikipedia.org/wiki/Learning_to_ranken.wikipedia.org/wiki/Structured_prediction,在这里可以找到更详细的讨论。

总结

我们已经从理论和 Spark 的角度讨论了一些监督、无监督和推荐系统。然而,监督、无监督、强化或推荐系统也有许多例子。尽管如此,我们已经尽力提供一些简单的例子以求简单。

我们将在第六章构建可扩展的机器学习管道中提供更多关于这些示例的见解。还将讨论使用 Spark ML 和 Spark MLlib 管道进行更多特征合并、提取、选择、模型扩展和调整。我们还打算提供一些包括数据收集到模型构建和预测的示例。

第六章: 构建可扩展的机器学习管道

机器学习的最终目标是使机器能够自动从数据中构建模型,而无需繁琐和耗时的人类参与和交互。 因此,本章将指导读者通过使用 Spark MLlib 和 Spark ML 创建一些实用和广泛使用的机器学习管道和应用。 将详细描述这两个 API,并且还将为两者都涵盖一个基线用例。 然后,我们将专注于扩展 ML 应用程序,以便它可以应对不断增加的数据负载。 阅读本章的所有部分后,读者将能够区分这两个 API,并选择最适合其要求的 API。 简而言之,本章将涵盖以下主题:

  • Spark 机器学习管道 API

  • 使用 Spark Core 进行癌症诊断管道

  • 使用 Spark 进行癌症预后管道

  • 使用 Spark Core 进行市场篮子分析

  • Spark 中的 OCR 管道

  • 使用 Spark MLlib 和 ML 进行主题建模

  • 使用 Spark 进行信用风险分析管道

  • 扩展 ML 管道

  • 提示和性能考虑

Spark 机器学习管道 API

MLlib 的目标是使实用的机器学习(ML)可扩展且易于使用。 Spark 引入了管道 API,用于轻松创建和调整实用的 ML 管道。 如第四章中所讨论的,实用的 ML 管道涉及一系列数据收集,预处理,特征提取,特征选择,模型拟合,验证和模型评估阶段。 例如,对文档进行分类可能涉及文本分割和清理,提取特征以及使用交叉验证训练分类模型。 大多数 ML 库都不是为分布式计算而设计的,或者它们不提供管道创建和调整的本地支持。

数据集抽象

如第一章中所述,当在另一种编程语言中运行 SQL 时,结果将返回为 DataFrame。 DataFrame 是一个分布式的数据集合,组织成命名列。 另一方面,数据集是一种接口,试图提供 Spark SQL 中 RDD 的好处。

数据集可以由 JVM 对象构建,这些对象可以在 Scala 和 Java 中使用。 在 Spark 管道设计中,数据集由 Spark SQL 的数据集表示。 ML 管道涉及一系列数据集转换和模型。 每个转换都接受输入数据集并输出转换后的数据集,这成为下一阶段的输入。

因此,数据导入和导出是 ML 管道的起点和终点。 为了使这些更容易,Spark MLlib 和 Spark ML 提供了一些特定于应用程序的类型的数据集,DataFrame,RDD 和模型的导入和导出实用程序,包括:

  • 用于分类和回归的 LabeledPoint

  • 用于交叉验证和潜在狄利克雷分配(LDA)的 LabeledDocument

  • 协同过滤的评分和排名

然而,真实数据集通常包含许多类型,例如用户 ID,项目 ID,标签,时间戳和原始记录。

不幸的是,当前的 Spark 实现工具无法轻松处理包含这些类型的数据集,特别是时间序列数据集。如果您回忆起第四章中的机器学习管道-概述部分,通过特征工程提取特征,特征转换通常占据实际 ML 管道的大部分。特征转换可以被视为从现有列创建新列或删除新列。

图 1中,用于机器学习模型的文本处理,您将看到文本标记器将文档分解为一袋词。之后,TF-IDF 算法将一袋词转换为特征向量。在转换过程中,标签需要被保留以用于模型拟合阶段:

数据集抽象

图 1:用于机器学习模型的文本处理(DS 表示数据源)

如果您回忆起第四章中的图 5图 6通过特征工程提取特征,在转换步骤中,ID、文本和单词都被让步。它们在进行预测和模型检查时非常有用。但是,它们实际上对于模型拟合来说是不必要的。根据 Databricks 关于 ML 管道的博客databricks.com/blog/2015/01/07/ml-pipelines-a-new-high-level-api-for-mllib.html,如果预测数据集只包含预测标签,它并没有提供太多信息。

因此,如果您想检查预测指标,如准确性、精确度、召回率、加权真正例和加权假正例,查看预测标签以及原始输入文本和标记化单词是非常有用的。相同的建议也适用于使用 Spark ML 和 Spark MLlib 的其他机器学习应用。

因此,已经实现了在内存、磁盘或外部数据源(如 Hive 和 Avro)之间进行 RDD、数据集和数据框之间的简单转换。虽然使用用户定义的函数从现有列创建新列很容易,但数据集的表现是一种延迟操作。

相比之下,数据集仅支持一些标准数据类型。然而,为了增加可用性并使其更适合机器学习模型,Spark 还添加了对向量类型的支持,作为一种支持密集和稀疏特征向量的用户定义类型,支持mllib.linalg.DenseVectormllib.linalg.Vector

提示

可以在 Spark 分发的examples/src/main/文件夹下找到 Java、Scala 和 Python 的完整 DataFrame、Dataset 和 RDD 示例。感兴趣的读者可以参考 Spark SQL 的用户指南spark.apache.org/docs/latest/sql-programming-guide.html了解更多关于 DataFrame、Dataset 以及它们支持的操作。

管道

Spark 在 Spark ML 下提供了管道 API。如前所述,管道由一系列阶段组成,包括转换器和估计器。管道阶段有两种基本类型,称为转换器和估计器。

转换器将数据集作为输入,并产生增强的数据集作为输出,以便将输出馈送到下一步。例如,TokenizerHashingTF****是两个转换器。 Tokenizer 将具有文本的数据集转换为具有标记化单词的数据集。另一方面,HashingTF 产生术语频率。标记化和 HashingTF 的概念通常用于文本挖掘和文本分析。

相反,估计器必须是输入数据集的第一个,以产生模型。在这种情况下,模型本身将被用作转换器,将输入数据集转换为增强的输出数据集。例如,在拟合训练数据集与相应的标签和特征之后,可以使用逻辑回归或线性回归作为估计器。

之后,它产生一个逻辑或线性回归模型。这意味着开发管道是简单而容易的。好吧,你所需要做的就是声明所需的阶段,然后配置相关阶段的参数;最后,将它们链接在一个管道对象中,如图 2所示:

管道

图 2:使用逻辑回归估计器的 Spark ML 管道模型(DS 表示数据存储,虚线内的步骤仅在管道拟合期间发生)

如果您看一下图 2,拟合模型由分词器、哈希 TF 特征提取器和拟合的逻辑回归模型组成。拟合的管道模型充当了可以用于预测、模型验证、模型检查和最终模型部署的转换器。然而,为了提高预测准确性,模型本身需要进行调整。我们将在第七章调整机器学习模型中更多地讨论如何调整机器学习模型。

为了更实际地展示流水线技术,以下部分展示了如何使用 Spark ML 和 MLlib 创建癌症诊断的实际管道。

使用 Spark 的癌症诊断管道

在本节中,我们将看看如何使用 Spark ML 和 MLlib 开发癌症诊断管道。将使用真实数据集来预测乳腺癌的概率,这种癌症几乎是可以治愈的,因为这种癌症类型的罪魁祸首基因已经成功地被确定。然而,我们想要讨论一下这种癌症类型,因为在非洲和亚洲的第三世界国家,它仍然是一种致命疾病。

提示

我们建议读者对这种疾病的结果或状态保持开放的态度,因为我们将展示 Spark ML API 如何通过整合和组合来自威斯康星乳腺癌(原始)、威斯康星诊断乳腺癌(WDBC)和威斯康星预后乳腺癌(WPBC)数据集的数据来预测癌症,这些数据集来自以下网站:archive.ics.uci.edu/ml

Spark 乳腺癌诊断管道

在本小节中,我们将开发一个逐步的癌症诊断管道。步骤包括对乳腺癌的背景研究、数据集收集、数据探索、问题形式化和基于 Spark 的实现。

背景研究

根据 Salama 等人的研究(使用多分类器在三个不同数据集上进行乳腺癌诊断,国际计算机和信息技术杂志2277-0764第 01-01 期,2012 年 9 月),乳腺癌在 20 至 29 岁的女性中排名第四,仅次于甲状腺癌、黑色素瘤和淋巴瘤。

乳腺癌是由乳腺组织突变引起的,原因包括性别、肥胖、酒精、家族史、缺乏体育锻炼等。此外,根据疾病控制和预防中心TCDCP)的统计数据(www.cdc.gov/cancer/breast/statistics/),2013 年,美国共有 230,815 名妇女和 2,109 名男性被诊断出患有乳腺癌。不幸的是,40,860 名妇女和 464 名男性死于此病。

研究发现,约 5-10%的病例是由父母的一些遗传因素引起的,包括 BRCA1 和 BRCA2 基因突变等。早期诊断可以帮助拯救全球数千名乳腺癌患者。尽管罪魁祸首基因已经被确定,但化疗并不十分有效。基因沉默正在变得流行,但需要更多的研究。

正如前面提到的,机器学习中的学习任务严重依赖分类、回归和聚类技术。此外,传统的数据挖掘技术正在与这些机器学习技术一起应用,这是最基本和重要的任务。因此,通过与 Spark 集成,这些应用技术在生物医学数据分析领域得到了广泛的接受和应用。此外,正在使用多类和多级分类器和特征选择技术对生物医学数据集进行大量实验,以进行癌症诊断和预后。

数据集收集

癌症基因组图谱TCGA),癌症体细胞突变目录COSMIC),国际癌症基因组联盟ICGC)是最广泛使用的癌症和肿瘤相关数据集,用于研究目的。这些数据来源已经从麻省理工学院、哈佛大学、牛津大学等世界知名研究所进行了整理。然而,这些可用的数据集是非结构化的、复杂的和多维的。因此,我们不能直接使用它们来展示如何将大规模机器学习技术应用于它们。原因是这些数据集需要大量的预处理和清洗,这需要大量的页面。

通过练习这个应用程序,我们相信读者将能够将相同的技术应用于任何类型的生物医学数据集,用于癌症诊断。由于页面限制,我们应该使用结构化和手动策划的简单数据集,用于机器学习应用开发,当然,其中许多显示出良好的分类准确性。

例如,来自 UCI 机器学习库的威斯康星州乳腺癌数据集,可在archive.ics.uci.edu/ml上找到,这些数据是由威斯康星大学的研究人员捐赠的,并包括来自乳腺肿块细针穿刺的数字图像的测量。这些值代表数字图像中细胞核的特征,如下一小节所述。

提示

关于威斯康星州乳腺癌数据的更多信息,请参考作者的出版物:乳腺肿瘤诊断的核特征提取。IS&T/SPIE 1993 年国际电子成像研讨会:科学与技术,卷 1905,第 861-870 页,作者为 W.N. Street,W.H. Wolberg 和 O.L. Mangasarian,1993 年

数据集描述和准备

威斯康星州乳腺癌数据集WDBC)手册所示,可在archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/wdbc.names上找到,肿块厚度良性细胞往往成片状分组,而癌细胞通常成多层分组。因此,在应用机器学习技术之前,手册中提到的所有特征和字段都很重要,因为这些特征将有助于确定特定细胞是否患癌。

乳腺癌数据包括 569 个癌症活检样本,每个样本有 32 个特征。一个特征是患者的识别号码,另一个是癌症诊断,标记为良性或恶性,其余的是数值型的生物测定,是在分子实验室工作中确定的。诊断编码为 M 表示恶性或 B 表示良性。

类别分布如下:良性:357(62.74%)和恶性:212(37.25%)。训练和测试数据集将按照此处给出的数据集描述进行准备。30 个数值测量包括均值、标准误差和最坏值,即三个最大值的均值。字段 3 是均值半径,13 是半径 SE,23 是最坏半径。通过对数字化的细胞核的不同特征进行计算,为每个细胞核计算了 10 个实值特征,这些特征描述在表 1,10 个实值特征及其描述中:

编号 数值 解释
1 半径 中心到周边点的距离的平均值
2 纹理 灰度值的标准偏差
3 周长 细胞核的周长
4 面积 细胞核覆盖周长的面积
5 光滑度 半径长度的局部变化
6 紧凑性 计算如下:(周长)² / 面积 - 1.0
7 凹度 轮廓凹陷部分的严重程度
8 凹点 轮廓的凹陷部分的数量
9 对称性 表示细胞结构是否对称
10 分形维数 计算如下:海岸线近似 - 1

表 1:10 个实值特征及其描述

所有特征值都记录了四个有效数字,没有缺失或空值。因此,我们不需要进行任何数据清理。但是,从前面的描述中,很难让某人获得有关数据的任何良好知识。例如,除非您是肿瘤学家,否则您不太可能知道每个字段与良性或恶性肿块的关系。随着我们继续进行机器学习过程,这些模式将被揭示。数据集的样本快照如图 3所示:

数据集描述和准备

图 3:数据快照(部分)

问题形式化

图 4乳腺癌诊断和预后管道模型,描述了提出的乳腺癌诊断模型。该模型包括两个阶段,即训练和测试阶段:

  • 训练阶段包括四个步骤:数据收集、预处理、特征提取和特征选择

  • 测试阶段包括与训练阶段相同的四个步骤,另外还有分类步骤

在数据收集步骤中,首先进行预处理,以检查是否存在不需要的值或任何缺失值。我们已经提到没有缺失值。但是,检查是一种良好的做法,因为即使特殊字符的不需要值也可能中断整个训练过程。之后,通过特征提取和选择过程进行特征工程步骤,以确定适用于后续逻辑或线性回归分类器的正确输入向量:

问题形式化

图 4:乳腺癌诊断和预后管道模型

这有助于对与模式向量相关联的类做出决定。基于特征选择或特征提取,完成了降维技术。但是,请注意,我们不会使用任何正式的降维算法来开发这个应用程序。有关降维的更多信息,您可以参考第四章中的降维部分,通过特征工程提取知识

在分类步骤中,应用逻辑回归分类器以获得肿瘤诊断和预后的最佳结果。

使用 Spark ML 开发癌症诊断管道

如前所述,在 WDBC 数据集中找到的属性的详细信息在archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/breast-cancer-wisconsin.names包括患者 ID、诊断(M = 恶性,B = 良性)和为每个细胞核计算的 10 个实值特征,如表 110 个实值特征及其描述所述。

这些特征是从乳腺肿块的细针穿刺FNA)的数字化图像计算出来的,因为我们对数据集有足够的了解。在本小节中,我们将逐步看看如何开发乳腺癌诊断机器学习流水线,包括在图 4中描述的 10 个步骤中从数据集输入到预测的数据工作流程。

第 1 步:导入必要的包/库/API

这是导入包的代码:

import org.apache.spark.api.java.JavaRDD; 
import org.apache.spark.api.java.function.Function; 
import org.apache.spark.ml.Pipeline; 
import org.apache.spark.ml.PipelineModel; 
import org.apache.spark.ml.PipelineStage; 
import org.apache.spark.ml.classification.LogisticRegression; 
import org.apache.spark.ml.feature.LabeledPoint; 
import org.apache.spark.ml.linalg.DenseVector; 
import org.apache.spark.ml.linalg.Vector; 
import org.apache.spark.sql.Dataset; 
import org.apache.spark.sql.Row; 
import org.apache.spark.sql.SparkSession; 

第 2 步:初始化 Spark 会话

可以使用以下代码初始化 Spark 会话:

static SparkSession spark = SparkSession 
        .builder() 
        .appName("BreastCancerDetectionDiagnosis") 
       .master("local[*]") 
       .config("spark.sql.warehouse.dir", "E:/Exp/") 
       .getOrCreate();

在这里,我们将应用程序名称设置为BreastCancerDetectionDiagnosis,主 URL 设置为local。Spark 上下文是程序的入口点。请相应地设置这些参数。

第 3 步:将乳腺癌数据作为输入并准备 JavaRDD

这是准备JavaRDD的代码:

  String path = "input/wdbc.data"; 
  JavaRDD<String> lines = spark.sparkContext().textFile(path, 3).toJavaRDD();

要了解更多关于数据的信息,请参考图 3数据快照(部分)

第 4 步:为回归创建标记点 RDD

为诊断(B = 良性,M = 恶性)创建LabeledPoint RDDs:

JavaRDD<LabeledPoint> linesRDD = lines 
        .map(new Function<String, LabeledPoint>() { 
          public LabeledPoint call(String lines) { 
            String[] tokens = lines.split(","); 
            double[] features = new double[30]; 
            for (int i = 2; i < features.length; i++) { 
              features[i - 2] = Double.parseDouble(tokens[i]); 
            } 
            Vector v = new DenseVector(features); 
            if (tokens[1].equals("B")) { 
              return new LabeledPoint(1.0, v); // benign 
            } else { 
              return new LabeledPoint(0.0, v); // malignant 
            } 
          } 
        }); 

第 5 步:从 linesRDD 创建 Row 数据集并显示顶部特征

这是所示代码:

Dataset<Row> data = spark.createDataFrame(linesRDD,LabeledPoint.class); 
data.show(); 

以下图显示了顶部特征及其对应的标签:

使用 Spark ML 开发癌症诊断流水线

图 5:顶部特征及其对应的标签

第 6 步:拆分数据集以准备训练和测试集

在这里,我们将原始数据框拆分为训练集和测试集,比例分别为 60%和 40%。在这里,12345L是种子值。这个值表示每次拆分都是相同的,这样 ML 模型在每次迭代中都会产生相同的结果。我们在每一章中都遵循相同的转换来准备测试和训练集:

Dataset<Row>[] splits = data.randomSplit(new double[] { 0.6, 0.4 }, 12345L); 
Dataset<Row> trainingData = splits[0]; 
Dataset<Row> testData = splits[1]; 

要快速查看这两个集合的快照,只需写trainingData.show()testData.show()分别用于训练和测试集。

第 7 步:创建一个逻辑回归分类器

通过指定最大迭代次数和回归参数创建一个逻辑回归分类器:

LogisticRegression logisticRegression = new LogisticRegression() 
                          .setMaxIter(100) 
                             .setRegParam(0.01) 
                             .setElasticNetParam(0.4); 

提示

逻辑回归通常需要三个参数:最大迭代次数、回归参数和弹性网络正则化。请参考以下行以更清楚地了解:

      LogisticRegression lr = new 
      LogisticRegression().setMaxIter(100)
      .setRegParam(0.01).setElasticNetParam(0.4); 

提示

上述语句创建了一个逻辑回归模型lr,最大迭代次数为100,回归参数为0.01,弹性网络参数为0.4

第 8 步:创建和训练流水线模型

这是所示代码:

Pipeline pipeline = new Pipeline().setStages(new PipelineStage[] {logisticRegression}); 
PipelineModel model = pipeline.fit(trainingData); 

在这里,我们创建了一个流水线,其阶段由逻辑回归阶段定义,这也是我们刚刚创建的一个估计器。请注意,如果您处理的是文本数据集,可以尝试创建分词器和 HashingTF 阶段。

然而,在这个癌症数据集中,所有的值都是数字。因此,我们不创建这样的阶段来链接到流水线。

第 9 步:创建数据集,转换模型和预测

创建一个类型为 Row 的数据集,并根据测试数据集进行预测转换模型:

Dataset<Row> predictions=model.transform(testData); 

第 10 步:显示预测及预测精度

predictions.show(); 
long count = 0; 
for (Row r : predictions.select("features", "label", "prediction").collectAsList()) { 
    System.out.println("(" + r.get(0) + ", " + r.get(1) + r.get(2) + ", prediction=" + r.get(2)); 
      count++; 
    } 

使用 Spark ML 开发癌症诊断流水线

图 6:预测及预测精度

图 7显示了测试集的预测数据集。所示的打印方法本质上生成输出,就像下面的例子一样:

使用 Spark ML 开发癌症诊断管道

图 7:朝向预测的样本输出。第一个值是特征,第二个是标签,最后一个值是预测值

现在让我们计算精度分数。我们通过将计数器乘以 100,然后除以完成的预测数量来做到这一点,如下所示:

System.out.println("precision: " + (double) (count * 100) / predictions.count()); 
Precision - 100.0 

因此,精度为 100%,这是很棒的。但是,如果您仍然不满意或有任何困惑,下一章将演示如何调整几个参数,以提高预测准确性,因为可能有许多假阴性预测。

此外,由于随机拆分的性质和您一侧的数据集处理,结果可能会有所不同。

使用 Spark 的癌症预后管道

在上一节中,我们展示了如何开发一个癌症诊断管道,用于基于两个标签(良性和恶性)预测癌症。在本节中,我们将看看如何使用 Spark ML 和 MLlib API 开发癌症预后管道。威斯康星预后乳腺癌WPBC)数据集将用于预测乳腺癌的概率,以预测复发和非复发的肿瘤细胞。同样,数据集是从archive.ics.uci.edu/ml/datasets/Breast+Cancer+Wisconsin+(Prognostic)下载的。要了解问题的形式化,请再次参考图 1,因为在癌症预后管道开发过程中,我们将几乎遵循相同的阶段。

数据集探索

archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/wpbc.names中找到的 WPBC 数据集的属性详细信息如下:

  • ID 编号

  • 结果(R = 复发,N = 非复发)

  • 时间(如果字段 2 => R,则为复发时间,如果字段 2 => N,则为无病时间)

  • 3 到 33:为每个细胞核计算了十个实值特征:半径、纹理、周长、面积、光滑度、紧凑性、凹度、凹点、对称性和分形维度。三十四是肿瘤大小,三十五是淋巴结状态,如下所示:

  • 肿瘤大小:切除肿瘤的直径(厘米)

  • 淋巴结状态:阳性腋窝淋巴结的数量

如果您比较图 3图 9,您会发现诊断和预后具有相同的特征,但预后有两个额外的特征(如前面提到的 34 和 35)。请注意,这些是在 1988 年至 1995 年手术时观察到的,在 198 个实例中,有 151 个是非复发(N),47 个是复发(R),如图 8所示。

当然,今天的真实癌症诊断和预后数据集以结构化或非结构化的方式包含许多其他特征和字段:

数据集探索

图 8:数据快照(部分)

提示

对于更详细的讨论和有意义的见解,感兴趣的读者可以参考以下研究论文:威斯康星乳腺癌问题:使用概率和广义回归神经分类器进行诊断和 DFS 时间预后,2005 年第四季度 Ioannis A.等人在以下链接中找到:citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.65.2463&rep=rep1&type=pdf

使用 Spark ML/MLlib 的乳腺癌预后管道

在本小节中,我们将逐步介绍如何开发乳腺癌预后机器学习管道,包括从数据集输入到预测的 10 个不同步骤,这些步骤在图 1中有描述,作为数据工作流。

提示

建议读者从 Packt 材料中下载数据集和项目文件,以及 Maven 项目配置的pom.xml文件。我们已经在之前的章节中介绍了如何使代码工作,例如第一章,Spark 数据分析简介

步骤 1:导入必要的包/库/API

import org.apache.spark.api.java.JavaRDD; 
import org.apache.spark.api.java.function.Function; 
import org.apache.spark.ml.Pipeline; 
import org.apache.spark.ml.PipelineModel; 
import org.apache.spark.ml.PipelineStage; 
import org.apache.spark.ml.classification.LogisticRegression; 
import org.apache.spark.ml.feature.LabeledPoint; 
import org.apache.spark.ml.linalg.DenseVector; 
import org.apache.spark.ml.linalg.Vector; 
import org.apache.spark.sql.Dataset; 
import org.apache.spark.sql.Row; 
import org.apache.spark.sql.SparkSession; 

步骤 2:初始化必要的 Spark 环境

static SparkSession spark = SparkSession 
        .builder() 
        .appName("BreastCancerDetectionPrognosis") 
       .master("local[*]") 
       .config("spark.sql.warehouse.dir", "E:/Exp/") 
       .getOrCreate(); 

在这里,我们将应用程序名称设置为BreastCancerDetectionPrognosis,主 URL 设置为local[*]。Spark Context 是程序的入口点。请相应地设置这些参数。

步骤 3:将乳腺癌数据作为输入并准备 JavaRDD 数据

String path = "input/wpbc.data"; 
JavaRDD<String> lines = spark.sparkContext().textFile(path, 3).toJavaRDD(); 

提示

要了解更多关于数据的信息,请参考图 5及其描述以及数据集探索子部分。

步骤 4:创建带标签的点 RDD

使用以下代码段为 N=复发和 R=非复发的预后创建LabeledPoint RDD:

JavaRDD<LabeledPoint> linesRDD = lines.map(new Function<String, LabeledPoint>() { 
      public LabeledPoint call(String lines) { 
        String[] tokens = lines.split(","); 
        double[] features = new double[30]; 
        for (int i = 2; i < features.length; i++) { 
          features[i - 2] = Double.parseDouble(tokens[i]); 
        } 
        Vector v = new DenseVector(features); 
        if (tokens[1].equals("N")) { 
          return new LabeledPoint(1.0, v); // recurrent 
        } else { 
          return new LabeledPoint(0.0, v); // non-recurrent 
        } 
      } 
    });  

步骤 5:从行 RDD 创建数据集并显示顶部特征

Dataset<Row> data = spark.createDataFrame(linesRDD,LabeledPoint.class); 
data.show(); 

顶部特征及其相应标签显示在图 9中:

使用 Spark ML/MLlib 进行乳腺癌预后管道

图 9:顶部特征及其相应标签

步骤 6:将数据集拆分为训练集和测试集

在这里,我们将数据集分为测试集和训练集,比例分别为 60%和 40%。请根据您的要求进行调整:

Dataset<Row>[] splits = data.randomSplit(new double[] { 0.6, 0.4 }, 12345L); 
Dataset<Row> trainingData = splits[0];   
Dataset<Row> testData = splits[1]; 

要快速查看这两个集合的快照,只需编写trainingData.show()testData.show(),分别用于训练和测试集。

步骤 7:创建逻辑回归分类器

通过指定最大迭代次数和回归参数创建逻辑回归分类器:

LogisticRegression logisticRegression = new LogisticRegression() 
.setMaxIter(100) 
.setRegParam(0.01) 
.setElasticNetParam(0.4); 

步骤 8:创建管道并训练管道模型

Pipeline pipeline = new Pipeline().setStages(new PipelineStage[]{logisticRegression}); 
PipelineModel model=pipeline.fit(trainingData); 

在这里,类似于诊断管道,我们创建了预后管道,其阶段仅由逻辑回归定义,这又是一个估计器,当然也是一个阶段。

步骤 9:创建数据集并转换模型

创建数据集并进行转换,以基于测试数据集进行预测:

Dataset<Row> predictions=model.transform(testData); 

步骤 10:显示预测及其精度

predictions.show(); 

使用 Spark ML/MLlib 进行乳腺癌预后管道

图 10:预测精度

long count = 0; 
for (Row r : predictions.select("features", "label", "prediction").collectAsList()) { 
      System.out.println("(" + r.get(0) + ", " + r.get(1) + r.get(2) + ", prediction=" + r.get(2)); 
      count++; 
    } 

这段代码将产生类似于图 7的输出,其中包含不同的特征、标签和预测:

System.out.println("precision: " + (double) (count * 100) / predictions.count());  
Precision: 100.0  

因此,精度几乎达到 100%,这是非常棒的。然而,根据数据准备的不同,你可能会得到不同的结果。

如果您有任何困惑,下一章将演示如何调整参数,以提高预测准确性,因为可能会有许多假阴性预测。

提示

在他们的书中《Machine Learning with R, Packt Publishing, 2015》,Brett Lantz 等人认为,通过将每个肿块分类为恶性、良性、复发或非复发,可以完全消除假阴性。显然,这不是一个现实的策略。但是,这说明了预测涉及在假阳性率和假阴性率之间取得平衡的事实。

如果您仍然不满意,我们将在第七章中调整多个参数,调整机器学习模型,以便预测准确性朝着更复杂的测量预测准确性的方法增加,这些方法可以用于确定可以根据每种错误类型的成本来优化错误率的地方。

使用 Spark Core 进行市场篮分析

在本节中,我们将探讨如何开发大规模机器学习管道,以进行市场篮分析。除了使用 Spark ML 和 MLlib 之外,我们还将演示如何使用 Spark Core 来开发这样的应用程序。

背景

在一篇早期的论文《在 Hadoop 上改进的 MapReduce 框架上的高效市场篮分析技术:电子商务视角》(可在onlinepresent.org/proceedings/vol6_2012/8.pdf获取),作者们认为市场篮分析(MBA)技术对于日常业务决策非常重要,因为可以通过发现顾客频繁购买和一起购买的物品来提取顾客的购买规则。因此,可以根据这些关联规则为经常购物的顾客揭示购买规则。

您可能仍然想知道为什么我们需要市场篮分析,为什么它很重要,以及为什么它在计算上很昂贵。如果您能够识别高度特定的关联规则,例如,如果顾客喜欢芒果或橙子果酱以及牛奶或黄油,您需要有大规模的交易数据进行分析和处理。此外,一些大型连锁零售商或超市,例如 E-mart(英国)、HomePlus(韩国)、Aldi(德国)或 Dunnes Stores(爱尔兰)使用数百万甚至数十亿的交易数据库,以找到特定物品之间的关联,例如品牌、颜色、原产地甚至口味,以增加销售和利润的可能性。

在本节中,我们将探讨使用 Spark 库进行大规模市场篮分析的高效方法。阅读并实践后,您将能够展示 Spark 框架如何将现有的单节点管道提升到可在多节点数据挖掘集群上使用的管道。结果是我们提出的关联规则挖掘算法可以以相同的好处并行重复使用。

我们使用 SAMBA 作为 Spark-based Market Basket Analysis 的缩写,min_sup表示最小支持度,min_conf表示最小置信度。我们还将频繁模式和频繁项集这两个术语互换使用。

动机

传统的主存储器或基于磁盘的计算和关系型数据库管理系统无法处理不断增加的大规模交易数据。此外,正如第一章中讨论的,使用 Spark 进行数据分析简介,MapReduce 在 I/O 操作、算法复杂性、低延迟和完全基于磁盘的操作方面存在一些问题。因此,找到空交易并随后从未来方案中消除它们是这种方法的初始部分。

通过识别那些不出现在至少一个频繁 1 项集中的交易,很可能找到所有的空交易。正如前面提到的,Spark 将中间数据缓存到内存中,并提供弹性分布式数据集(RDDs)的抽象,可以通过这种方式克服这些问题,过去三年在处理分布式计算系统中的大规模数据方面取得了巨大成功。这些成功是有希望的,也是激励人的例子,可以探索将 Spark 应用于市场篮分析的研究工作。

探索数据集

请从github.com/stedy/Machine-Learning-with-R-datasets/blob/master/groceries.csv下载购物篮分析的杂货数据集。原始grocery.csv数据的前五行如图 11所示。这些行表示 10 个独立的杂货店交易。第一笔交易包括四件商品:柑橘类水果、半成品面包、人造黄油和即食汤。相比之下,第三笔交易只包括一件商品,全脂牛奶:

Exploring the dataset

图 11:杂货数据集的快照

问题陈述

我们相信我们有足够的动机和理由来分析使用事务或零售数据集的购物篮。现在,让我们讨论一些背景研究,这些研究需要应用我们基于 Spark 的购物篮分析技术。

假设您有一组不同的项目I = {i1, i2...in}n是不同项目的数量。事务数据库T = {t1, t2...tN}是一组N个事务,|N|是总事务数。集合XProblem statements称为模式或项集。我们假设输入是作为事务序列给出的,其中项目用逗号分隔,如表 1所示。

为了简单起见描述背景研究,相同的交易在表 2中用单个字符表示:

交易 1 交易 2 交易 3 交易 4... 饼干,冰淇淋,可乐,橙子,牛肉,比萨,可乐,面包法棍,苏打水,洗发水,饼干,百事可乐汉堡,奶酪,尿布,牛奶...

表 1. 顾客的样本交易

TID Itemset (Sequence of items)
10 A, B, C, F
20 C, D, E
30 A, C, E, D
40 A
50 D, E, G
60 B, D
70 B
80 A, E, C
90 A, C, D
100 B, E, D

表 2. 事务数据库

如果Problem statements,则称X发生在t中或t包含X。支持计数是项集在所有事务中出现的频率,可以描述如下:

Problem statements

换句话说,如果支持Problem statements,我们说X是频繁项集。例如,在表 2中,项集CDDECDE的出现次数分别为332,如果min_sup2,所有这些都是频繁项集。

另一方面,关联规则是形式为Problem statements或更正式地:

Problem statements

因此,我们可以说关联规则是一种模式,它陈述了当X发生时,Y以一定概率发生。方程 1 中定义的关联规则的置信度可以表示为Y中的项目在包含X的事务中出现的频率,如下所示:

Problem statements

现在我们需要引入一个称为lift的新参数,作为一个度量,它衡量了一个项目相对于其典型购买率更有可能被购买的程度,假设您知道另一个项目已被购买。这由以下方程定义:

Problem statements

简而言之,给定一个事务数据库,现在购物篮分析的问题是通过关联规则找到支持和置信度都不低于min_supmin_conf阈值的频繁项集的完整一组顾客购买规则。

使用 Spark 进行大规模购物篮分析

图 12所示,我们假设事务数据库以分布方式存储在一组 DB 服务器的集群中。DB 服务器是具有大存储和主存储器的计算节点。因此,它可以存储大型数据集,因此可以计算分配给它的任何任务。驱动 PC 也是一个计算节点,主要作为客户端并控制整个过程。

显然,它需要有大内存来处理和保存 Spark 代码,以便发送到计算节点。这些代码包括 DB 服务器 ID、最小支持度、最小置信度和挖掘算法:

使用 Spark 进行大规模市场篮分析

图 12:使用 Spark 的 SAMBA 算法的工作流程

从模式中,使用 reduce 阶段 1 生成频繁模式,满足约束条件min_sup。在计算频繁模式上应用 map 阶段,以生成最终帮助生成关联规则的子模式。从子模式中,应用 reduce 阶段 2 生成满足约束条件min_conf的关联规则。

由于 Spark 生态系统对 Spark 核心和相关 API 的支持,可以实现两个 Map 和 Reduce 阶段的结合。最终结果是完整的关联规则集,以及它们各自的支持计数和置信度。

这些商店根据商品之间的关联关系,有完整的形式来放置它们的商品,以增加对频繁和非频繁购物者的销售。由于空间限制,我们无法展示表 2中呈现的样本交易数据库的逐步示例。

然而,我们相信工作流程和伪代码足以理解整个情景。DB 服务器接收来自驱动 PC 的代码输入并开始计算。从环境变量 Spark 会话中,我们创建一些初始数据引用或 RDD 对象。然后,初始 RDD 对象被转换以在 DB 服务器中创建更多和全新的 RDD 对象。首先,它以纯文本(或其他支持的格式)读取数据集,并使用窄/宽转换(即flatMapmapToPairreduceByKey)来处理空事务。

因此,过滤连接 RDD 操作提供了一个没有空事务的数据段。然后,RDD 对象被实现以将 RDD 转储到 DB 服务器的存储中作为筛选后的数据集。Spark 的间 RDD 连接操作允许在单个数据节点内合并多个 RDD 的内容。总之,在获得筛选后的数据集之前,我们遵循这里给出的步骤:

  1. 将分布式处理模型和集群管理器(即 Mesos)的系统属性设置为 true。这个值可以保存在你的应用开发中作为标准的 Spark 代码。

  2. 设置 SparkConf、AppName、Master URL、Spark 本地 IP、Spark 驱动主机 IP、Spark 执行器内存和 Spark 驱动内存。

  3. 使用SparkConf创建JavaSparkContext

  4. 创建JavaRDD并将数据集作为纯文本读取,作为事务,并执行必要的分区。

  5. 对 RDD 执行flatMap操作以将事务拆分为项目。

  6. 执行mapToPair操作以便于查找项目的键/值对。

  7. 执行过滤操作以删除所有空事务。

当我们有了筛选后的数据库时,我们会实现一个动作间 RDD 连接操作,将数据集保存在 DB 服务器或分区上,如果单台机器的存储空间不够,或者缓存,如果内存不够。

图 12显示了使用 Spark 的 API 获取关联规则作为最终结果的完整工作流程。另一方面,图 13 显示了该算法的伪代码,即基于 Spark 的市场篮分析SAMBA)。这里实际上有两个 Map 和 Reduce 操作:

  • Map/Reduce 阶段 1:映射器从 HDFS 服务器读取交易并将交易转换为模式。另一方面,减速器找到频繁模式。

  • Map/Reduce 阶段 2:映射器将频繁模式转换为子模式。另一方面,减速器根据给定的约束条件(min_conflift)生成关联规则:使用 Spark 进行大规模购物篮分析

图 13:SAMBA 算法

之后,SAMBA 算法读取过滤数据库FTDB),并应用映射阶段 1 生成模式的所有可能组合。然后mapToPair()方法将它们作为具有相应支持的模式。

使用 Spark Core 的算法解决方案

在这里,我们将看看如何使用 Spark Core 进行购物篮分析。请注意,我们将不使用 Spark ML 或 MLlib,因为虽然 MLlib 提供了计算关联规则的技术,但它不显示如何计算其他参数,例如计算置信度,支持和提升,这些参数对于完整分析杂货数据集非常重要。因此,我们将逐步展示一个完整的示例,从数据探索到关联规则生成。

第 1 步:导入必要的包和 API

以下是导入包和 API 的代码:

import java.util.ArrayList; 
import java.util.Iterator; 
import java.util.List; 
import org.apache.spark.api.java.JavaPairRDD; 
import org.apache.spark.api.java.JavaRDD; 
import org.apache.spark.api.java.function.Function; 
import org.apache.spark.api.java.function.Function2; 
import org.apache.spark.api.java.function.PairFlatMapFunction; 
import org.apache.spark.rdd.RDD; 
import org.apache.spark.sql.SparkSession; 
import scala.Tuple2;  
import scala.Tuple4; 

第 2 步:通过指定 Spark 会话创建入口点

可以使用以下代码创建入口点:

SparkSession spark = SparkSession 
.builder() 
.appName("MarketBasketAnalysis") 
.master("local[*]") 
.config("spark.sql.warehouse.dir", "E:/Exp/") 
.getOrCreate(); 

第 3 步:为交易创建 Java RDD

可以使用以下代码创建交易的 Java RDD:

String transactionsFileName = "Input/groceries.data"; 
RDD<String> transactions = spark.sparkContext().textFile(transactionsFileName, 1); 
transactions.saveAsTextFile("output/transactions"); 

第 4 步:创建创建列表的方法

创建一个名为toList的方法,从创建的交易 RDD 中添加所有交易中的项目:

  static List<String> toList(String transaction) { 
    String[] items = transaction.trim().split(","); 
    List<String>list = new ArrayList<String>(); 
    for (String item :items) { 
      list.add(item); 
    } 
    returnlist; 
  } 

第 5 步:删除不频繁的项目和空交易

创建一个名为removeOneItemAndNullTransactions的方法,以删除不频繁的项目和空交易:

static List<String> removeOneItemAndNullTransactions(List<String>list, int i) { 
    if ((list == null) || (list.isEmpty())) { 
      returnlist; 
    } 
    if ((i< 0) || (i> (list.size() - 1))) { 
      returnlist; 
    } 
    List<String>cloned = new ArrayList<String>(list); 
    cloned.remove(i); 
    return cloned; 
  } 

第 6 步:扁平映射和创建 1 项集(映射阶段 1)

进行flatmap并创建 1 项集。最后,保存模式:

JavaPairRDD<List<String>, Integer> patterns = transactions.toJavaRDD() 
        .flatMapToPair(new PairFlatMapFunction<String, List<String>, Integer>() { 
          @Override 
  public Iterator<Tuple2<List<String>, Integer>> call(String transaction) { 
  List<String> list = toList(transaction); 
  List<List<String>> combinations = Combination.findSortedCombinations(list); 
  List<Tuple2<List<String>, Integer>> result = new ArrayList<Tuple2<List<String>, Integer>>(); 
for (List<String> combList : combinations) { 
  if (combList.size() > 0) { 
  result.add(new Tuple2<List<String>, Integer>(combList, 1)); 
              } 
            } 
    return result.iterator(); 
          } 
        }); 
    patterns.saveAsTextFile("output/1itemsets"); 

注意

请注意,模式 RDD 的最后保存是为了可选的参考目的,以便您可以查看 RDD 的内容。

以下是 1 项集的屏幕截图:

使用 Spark Core 的算法解决方案

图 14:1 项集

第 7 步:组合和减少频繁模式(减少阶段 1)

组合和减少所有频繁模式,并保存它们:

JavaPairRDD<List<String>, Integer> combined = patterns.reduceByKey(new Function2<Integer, Integer, Integer>() { 
      public Integer call(Integer i1, Integer i2) { 
        int support = 0; 
        if (i1 + i2 >= 2) { 
          support = i1 + i2; 
        } 
        // if(support >= 2) 
        return support; 
      } 
    }); 
  combined.saveAsTextFile("output/frequent_patterns"); 

以下是具有相应支持的频繁模式的快照(图 15):

使用 Spark Core 的算法解决方案

图 15:具有相应支持的频繁模式(频率)

第 8 步:生成所有候选频繁模式(映射阶段 2)

通过从频繁模式中删除 1 项集来生成所有候选频繁模式或子模式,并最终保存候选模式:

JavaPairRDD<List<String>, Tuple2<List<String>, Integer>> candidate-patterns = combined.flatMapToPair( 
new PairFlatMapFunction<Tuple2<List<String>, Integer>, List<String>, Tuple2<List<String>, Integer>>() { 
          @Override 
public Iterator<Tuple2<List<String>, Tuple2<List<String>, Integer>>> call( 
Tuple2<List<String>, Integer> pattern) { 
List<Tuple2<List<String>, Tuple2<List<String>, Integer>>> result = new ArrayList<Tuple2<List<String>, Tuple2<List<String>, Integer>>>(); 
  List<String> list = pattern._1; 
  frequency = pattern._2; 
  result.add(new Tuple2(list, new Tuple2(null, frequency))); 
            if (list.size() == 1) { 
              return result.iterator(); 
            } 

  // pattern has more than one item 
  // result.add(new Tuple2(list, new Tuple2(null,size))); 
    for (int i = 0; i < list.size(); i++) { 
    List<String> sublist = removeOneItem(list, i); 
              result.add(new Tuple2<List<String>, Tuple2<List<String>, Integer>>(sublist, 
                  new Tuple2(list, frequency))); 
            } 
            return result.iterator(); 
          } 
        }); 
candidate-patterns.saveAsTextFile("output/sub_patterns"); 

以下是子模式的快照:

使用 Spark Core 的算法解决方案

图 16:项目的子模式

第 9 步:组合所有子模式

组合所有子模式并将它们保存在磁盘上或持久保存在内存中:

JavaPairRDD<List<String>, Iterable<Tuple2<List<String>, Integer>>>rules = candidate_patterns.groupByKey(); 
rules.saveAsTextFile("Output/combined_subpatterns"); 

以下是组合形式的候选模式(子模式)的屏幕截图:

使用 Spark Core 的算法解决方案

图 17:组合形式的候选模式(子模式)

第 10 步:生成关联规则

通过指定置信度提升从子模式生成所有关联规则(减少阶段 2):

JavaRDD<List<Tuple4<List<String>, List<String>, Double, Double>>> assocRules = rules.map( 
        new Function<Tuple2<List<String>, Iterable<Tuple2<List<String>, Integer>>>, List<Tuple4<List<String>, List<String>, Double, Double>>>() { 
          @Override 
public List<Tuple4<List<String>, List<String>, Double, Double>> call( 
Tuple2<List<String>, Iterable<Tuple2<List<String>, Integer>>> in) throws Exception { 

List<Tuple4<List<String>, List<String>, Double, Double>> result = new ArrayList<Tuple4<List<String>, List<String>, Double, Double>>(); 
  List<String> fromList = in._1; 
  Iterable<Tuple2<List<String>, Integer>> to = in._2; 
  List<Tuple2<List<String>, Integer>> toList = new ArrayList<Tuple2<List<String>, Integer>>(); 
Tuple2<List<String>, Integer> fromCount = null; 
      for (Tuple2<List<String>, Integer> t2 : to) { 
        // find the "count" object 
      if (t2._1 == null) { 
                fromCount = t2; 
              } else { 
                toList.add(t2); 
              } 
            } 
            if (toList.isEmpty()) { 
              return result; 
            } 
for (Tuple2<List<String>, Integer> t2 : toList) { 
  double confidence = (double) t2._2 / (double) fromCount._2; 
double lift = confidence / (double) t2._2; 
double support = (double) fromCount._2; 
List<String> t2List = new ArrayList<String>(t2._1); 
t2List.removeAll(fromList); 
if (support >= 2.0 && fromList != null && t2List != null) { 
  result.add(new Tuple4(fromList, t2List, support, confidence)); 
System.out.println(fromList + "=>" + t2List + "," + support + "," + confidence + "," + lift); 
              } 
            } 
            return result; 
          } 
        }); 
assocRules.saveAsTextFile("output/association_rules_with_conf_lift"); 

以下是包括置信度和提升的关联规则的输出。有关支持,置信度和提升的更多详细信息,请参阅问题说明部分。

[前提=>结论],支持,置信度,提升:

使用 Spark Core 的算法解决方案

图 18:包括置信度和提升的关联规则

在 SAMBA 中调整和设置正确的参数

请注意,如果您尝试使用默认参数设置,如支持=0.1 和置信度=0.6,可能会得到空规则,或者从技术上讲,没有规则生成。您可能会想知道为什么。实际上,0.1 的默认支持意味着为了生成关联规则,一个项目必须至少出现在0.1 * 9385 = 938.5交易中,或者 938.5 次(对于我们使用的数据集,|N| = 9385)。

然而,在这方面,在他们的书中,Brett Lantz 等人认为有一种方法可以解决这个问题,同时设置支持。他们建议考虑在您认为模式有趣之前需要的最小交易数量。此外,例如,您还可以认为,如果一个项目每天购买两次(大约每月 60 次),那么考虑该交易可能是非平凡的。

从这个角度来看,可以估计如何设置支持值,以便仅找到至少匹配那么多交易的规则。因此,您可以将最小支持值设置为 0.006,因为 9,835 中的 60 等于 0.006;我们将首先尝试设置支持值。

另一方面,设置最小置信度也需要一个棘手的平衡,在这方面,我们再次想参考 Brett Lantz 等人的书,题为Machine Learning with R, Packt Publishing, 2015。如果置信度太低,显然我们可能会对相当多的不可靠规则产生怀疑的假阳性结果。

因此,最小置信度阈值的最佳值严重取决于您分析的目标。因此,如果您从保守值开始,可以随时将其降低以扩大搜索,如果您找不到可操作的情报。如果将最小置信度阈值设置为 0.25,这意味着为了包含在结果中,规则必须至少有 25%的时间是正确的。这将消除最不可靠的规则,同时为我们留出一些空间,以通过有针对性的产品促销来修改行为。

现在,让我们谈谈第三个参数,“提升”。在建议如何设置“提升”的值之前,让我们先看一个实际例子,看看它可能如何影响首次生成关联规则。这是第三次,我们参考了 Brett Lantz 等人的书,题为Machine Learning with R, Packt Publishing, 2015

例如,假设在超市里,很多人经常一起购买牛奶和面包。因此,自然地,您期望找到许多包含牛奶和面包的交易。然而,如果提升(牛奶=>面包)大于 1,则意味着这两种物品一起出现的频率比预期的要高。因此,较大的提升值是规则重要性的强烈指标,并反映了交易中物品之间的真实联系。

总之,我们需要仔细考虑这些参数的值,考虑前面的例子。然而,作为一个独立的模型,算法可能需要几个小时才能完成。因此,请花足够的时间运行应用程序。或者,减少长交易以减少时间开销。

Spark 的 OCR 流水线

图像处理和计算机视觉是两个经典但仍在不断发展的研究领域,它们经常充分利用许多类型的机器学习算法。有几种用例,其中将图像像素的模式与更高概念的关系联系起来是极其复杂且难以定义的,当然,也是计算上费时的。

从实际角度来看,人类相对容易识别物体是脸、狗,还是字母或字符。然而,在某些情况下定义这些模式是困难的。此外,与图像相关的数据集通常存在噪音。

在本节中,我们将开发一个类似于用于光学字符识别OCR)的核心的模型,用于将打印或手写文本转换为电子形式以保存在数据库中,以便处理基于纸张的文档。

当 OCR 软件首次处理文档时,它将纸张或任何对象分成一个矩阵,以便网格中的每个单元格包含一个单个字形(也称为不同的图形形状),这只是一种指代字母、符号、数字或来自纸张或对象的任何上下文信息的复杂方式。

为了演示 OCR 流水线,我们将假设文档只包含英文的字母,与 26 个字母 A 到 Z 中的一个匹配的字形。我们将使用 UCI 机器学习数据存储库(archive.ics.uci.edu/ml)中的 OCR 字母数据集。该数据集是由 W. Frey 和 D. J. Slate 等人捐赠的。我们已经发现数据集包含 20,000 个例子,使用 20 种不同的随机重塑和扭曲的黑白字体作为不同形状的字形的 26 个英文字母大写字母。

提示

有关这些数据的更多信息,请参阅 W. Frey 和 D.J. Slate(1991 年)的文章《使用荷兰式自适应分类器进行字母识别,机器学习,第 6 卷,第 161-182 页》。

图 19中显示的图像是由 Frey 和 Slate 发表的,提供了一些印刷字形的示例。以这种方式扭曲,这些字母对计算机来说很具挑战性,但对人类来说很容易识别。前 20 行的统计属性显示在图 20中:

使用 Spark 的 OCR 流水线

图 19:一些印刷字形[由 W. Frey 和 D.J. Slate(1991 年)的文章《使用荷兰式自适应分类器进行字母识别,机器学习,第 6 卷,第 161-182 页》提供]

探索和准备数据

根据 Frey 和 Slate 提供的文档,当使用 OCR 阅读器扫描字形到计算机时,它们会自动转换为像素。因此,提到的 16 个统计属性也被记录到计算机中。

请注意,字符所在的方框各个区域的黑色像素的浓度应该提供一种区分字母表中的 26 个字母的方法,使用 OCR 或机器学习算法进行训练。

提示

要跟随本示例,从 Packt Publishing 网站下载letterdata.data文件,并将其保存到您的项目目录中的一个或另一个目录中。

在从 Spark 工作目录中读取数据之前,我们确认已收到定义每个字母类的 16 个特征的数据。如预期的那样,字母有 26 个级别,如图 20所示:

探索和准备数据

图 20:显示为数据框的数据集的快照

请记住,SVM、朴素贝叶斯分类器或任何其他分类器算法以及它们的相关学习器都需要所有特征都是数字。此外,每个特征都被缩放到一个相当小的区间。

此外,SVM 在密集向量化特征上表现良好,因此在稀疏向量化特征上表现不佳。在我们的情况下,每个特征都是整数。因此,我们不需要将任何值转换为数字。另一方面,这些整数变量的一些范围似乎相当宽。

在实际情况下,可能需要对所有少数特征点对数据进行归一化。

使用 Spark ML 和 Spark MLlib 的 OCR 流水线

由于其准确性和健壮性,让我们看看 SVM 是否能胜任。正如您在图 17中所看到的,我们有一个多类 OCR 数据集(具体来说有 26 个类);因此,我们需要一个多类分类算法,例如逻辑回归模型,因为 Spark 中的线性 SVM 的当前实现不支持多类分类。

提示

有关更多详细信息,请参阅以下网址:spark.apache.org/docs/latest/mllib-linear-methods.html#linear-support-vector-machines-svms

步骤 1:导入必要的包/库/接口

以下是导入必要包的代码:

import java.util.HashMap; 
import java.util.Map; 
import org.apache.spark.api.java.JavaRDD; 
import org.apache.spark.api.java.function.Function; 
import org.apache.spark.mllib.classification.LogisticRegressionWithLBFGS; 
import org.apache.spark.mllib.evaluation.MulticlassMetrics; 
import org.apache.spark.mllib.evaluation.MultilabelMetrics; 
import org.apache.spark.mllib.linalg.DenseVector; 
import org.apache.spark.mllib.linalg.Vector; 
import org.apache.spark.mllib.regression.LabeledPoint; 
import org.apache.spark.sql.Dataset; 
import org.apache.spark.sql.Row; 
import org.apache.spark.sql.SparkSession; 
import scala.Tuple2; 

步骤 2:初始化必要的 Spark 环境

以下是初始化 Spark 环境的代码:

  static SparkSession spark = SparkSession 
        .builder() 
        .appName("OCRPrediction") 
            .master("local[*]") 
            .config("spark.sql.warehouse.dir", "E:/Exp/"). 
            getOrCreate(); 

在这里,我们将应用程序名称设置为OCRPrediction,主 URL 设置为local。Spark 会话是程序的入口点。请相应地设置这些参数。

步骤 3:读取数据文件并创建相应的数据集,并显示前 20 行

以下是读取数据文件的代码:

String input = "input/letterdata.data"; 
Dataset<Row> df = spark.read().format("com.databricks.spark.csv").option("header", "true").load(input);  
  df.show();  

对于前 20 行,请参阅图 5。正如我们所看到的,有 26 个字符呈现为需要预测的单个字符;因此,我们需要为每个字符分配一个随机双精度值,以使该值与其他特征对齐。因此,在下一步中,这就是我们要做的。

步骤 4:创建一个字典,为每个字符分配一个随机双精度值

以下代码是为每个字符分配一个随机双精度值的字典:

final Map<String, Integer>alpha = newHashMap(); 
    intcount = 0; 
    for(chari = 'A'; i<= 'Z'; i++){ 
      alpha.put(i + "", count++); 
      System.out.println(alpha); 
    } 

以下是从前面的代码段生成的映射输出:

使用 Spark ML 和 Spark MLlib 的 OCR 管道

图 21:映射分配

步骤 5:创建标记点和特征向量

为来自 16 个特征(即 16 列)的组合特征创建标记点和特征向量。还将它们保存为 Java RDD,并将其转储或缓存在磁盘或内存中,并显示样本输出:

JavaRDD<LabeledPoint> dataRDD = df.toJavaRDD().map(new Function<Row, LabeledPoint>() { 
      @Override 
      public LabeledPoint call(Row row) throws Exception { 

        String letter = row.getString(0); 
        double label = alpha.get(letter); 
        double[] features= new double [row.size()]; 
        for(int i = 1; i < row.size(); i++){ 
          features[i-1] = Double.parseDouble(row.getString(i)); 
        } 
        Vector v = new DenseVector(features);         
        return new LabeledPoint(label, v); 
      } 
    }); 

dataRDD.saveAsTextFile("Output/dataRDD"); 
System.out.println(dataRDD.collect()); 

如果您仔细观察前面的代码段,我们已经创建了一个名为 features 的数组,其中包含 16 个特征,并创建了密集向量表示,因为密集向量表示是一种更紧凑的表示,其中内容可以显示如下截图所示:

使用 Spark ML 和 Spark MLlib 的 OCR 管道

图 22:相应标签和特征的 Java RDD

步骤 6:生成训练和测试集

以下是生成测试集的代码:

JavaRDD<LabeledPoint>[] splits = dataRDD.randomSplit(new double[] {0.7, 0.3}, 12345L); 
JavaRDD<LabeledPoint> training = splits[0]; 
JavaRDD<LabeledPoint> test = splits[1];  

如果您希望查看训练或测试数据集的快照,您应该将它们转储或缓存。以下是一个示例代码:

training.saveAsTextFile("Output/training"); 
test.saveAsTextFile("Output/test"); 

提示

我们已随机生成了要训练和测试的模型的训练集和测试集。在我们的案例中,分别为 70%和 30%,长种子为 11L。根据您的数据集重新调整这些值。请注意,如果向随机数添加种子,每次运行代码时都会获得相同的结果,这些结果一直为质数,最多为 1062348。

步骤 7:训练模型

正如您所看到的,我们有一个包含 26 个类的多类数据集;因此,我们需要一个多类分类算法,例如逻辑回归模型:

Boolean useFeatureScaling= true; 
final LogisticRegressionModel model = new LogisticRegressionWithLBFGS() 
  .setNumClasses(26).setFeatureScaling(useFeatureScaling) 
  .run(training.rdd()); 

前面的代码段通过指定类别数(即26)和特征缩放为Boolean true来使用训练数据集构建模型。正如您所看到的,我们使用了训练数据集的 RDD 版本,使用training.rdd(),因为训练数据集是以正常向量格式的。

提示

Spark 支持多类逻辑回归算法,支持有限内存 Broyden-Fletcher-Goldfarb-ShannoLBFGS)算法。在数值优化中,Broyden-Fletcher-Goldfarb-ShannoBFGS)算法是用于解决无约束非线性优化问题的迭代方法。

步骤 8:计算测试数据集上的原始分数

以下是计算原始分数的代码:

JavaRDD<Tuple2<Object, Object>> predictionAndLabels = test.map( 
    new Function<LabeledPoint, Tuple2<Object, Object>>() { 
    public Tuple2<Object, Object> call(LabeledPoint p) { 
    Double prediction = model.predict(p.features()); 
    return new Tuple2<Object, Object>(prediction, p.label()); 
          } 
        } 
      );  
predictionAndLabels.saveAsTextFile("output/prd2");  

如果您仔细查看前面的代码,您会发现我们实际上是通过将它们作为 Java RDD 来计算模型中创建的预测特征,从而计算出步骤 7中的预测特征。

步骤 9:预测标签为 8.0(即 I)的结果并获取评估指标

以下代码说明了如何预测结果:

MulticlassMetrics metrics = new MulticlassMetrics(predictionAndLabels.rdd()); 
MultilabelMetrics(predictionAndLabels.rdd()); 
System.out.println(metrics.confusionMatrix()); 
double precision = metrics.precision(metrics.labels()[0]); 
double recall = metrics.recall(metrics.labels()[0]); 
double tp = 8.0; 
double TP = metrics.truePositiveRate(tp); 
double FP = metrics.falsePositiveRate(tp); 
double WTP = metrics.weightedTruePositiveRate(); 
double WFP =  metrics.weightedFalsePositiveRate(); 
System.out.println("Precision = " + precision); 
System.out.println("Recall = " + recall); 
System.out.println("True Positive Rate = " + TP); 
System.out.println("False Positive Rate = " + FP); 
System.out.println("Weighted True Positive Rate = " + WTP); 
System.out.println("Weighted False Positive Rate = " + WFP); 

使用 Spark ML 和 Spark MLlib 的 OCR 流水线

图 23:精度和召回率的性能指标

因此,精度为 75%,显然不令人满意。然而,如果您仍然不满意,下一章将讨论如何调整参数以提高预测准确性。

提示

要了解如何计算精度、召回率、真正率和真负率,请参阅维基百科页面en.wikipedia.org/wiki/Sensitivity_and_specificity,其中详细讨论了敏感性和特异性。您还可以参考Powers, David M W (2011). Evaluation: From Precision, Recall and F-Measure to ROC, Informedness, Markedness & Correlation(PDF). Journal of Machine Learning Technologies 2 (1): 37-63

使用 Spark MLlib 和 ML 进行主题建模

主题建模技术广泛用于从大量文档中挖掘文本的任务。这些主题可以用来总结和组织包括主题术语及其相对权重的文档。自 Spark 1.3 发布以来,MLlib 支持 LDA,这是文本挖掘和自然语言处理领域中最成功使用的主题建模技术之一。此外,LDA 也是第一个采用 Spark GraphX 的 MLlib 算法。

提示

要了解 LDA 背后的理论如何工作,请参考David M. Blei, Andrew Y. Ng and Michael I. Jordan, Latent Dirichlet Allocation, Journal of Machine Learning Research 3 (2003) 993-1022

图 24显示了从随机生成的推文文本中的主题分布的输出,将在第九章中进一步讨论,使用流式和图数据进行高级机器学习。此外,我们将进一步解释为什么我们在第九章中使用 LDA 而不是其他主题建模算法。

使用 Spark MLlib 和 ML 进行主题建模

图 24:主题分布及其外观

在本节中,我们将介绍使用 Spark MLlib 的 LDA 算法处理非结构化原始推文数据集的主题建模示例。

使用 Spark MLlib 进行主题建模

在这一小节中,我们使用 Spark 表示了一种半自动的主题建模技术。以下步骤展示了从数据读取到打印主题及其术语权重的主题建模,同时使用其他选项作为默认值,我们在从 GitHub URL 下载的数据集上训练 LDA,网址为github.com/minghui/Twitter-LDA/tree/master/data/Data4Model/test

步骤 1:加载所需的软件包和 API

以下是加载所需软件包的代码:

import java.io.File; 
import java.io.FileNotFoundException; 
import java.io.Serializable; 
import java.util.ArrayList; 
import java.util.List; 
import java.util.Scanner; 
import org.apache.spark.ml.clustering.LDA; 
import org.apache.spark.ml.clustering.LDAModel; 
import org.apache.spark.ml.feature.ChiSqSelector; 
import org.apache.spark.ml.feature.HashingTF; 
import org.apache.spark.ml.feature.IDF; 
import org.apache.spark.ml.feature.IDFModel; 
import org.apache.spark.ml.feature.RegexTokenizer; 
import org.apache.spark.ml.feature.StopWordsRemover; 
import org.apache.spark.ml.feature.StringIndexer; 
import org.apache.spark.sql.Dataset; 
import org.apache.spark.sql.Row; 
import org.apache.spark.sql.SparkSession; 
import org.apache.spark.sql.types.DataTypes; 

步骤 2:创建 Spark 会话

以下是创建 Spark 会话的代码:

static SparkSession spark = SparkSession 
        .builder() 
        .appName("JavaLDAExample") 
        .master("local[*]") 
        .config("spark.sql.warehouse.dir", "E:/Exp/") 
        .getOrCreate(); 

步骤 3:读取和查看数据集的内容

以下代码说明了如何读取和查看数据集的内容:

Dataset<Row> df = spark.read().text("input/test/*.txt"); 

请注意,使用字符***表示读取项目路径中 input/text 目录中的所有文本文件。如果要打印前 20 行,只需使用以下代码,您将看到以下文本:

df.show(); 

使用 Spark MLlib 进行主题建模

图 25:文本的前 20 行

从前面的屏幕截图可以清楚地看出,我们使用的文本文件只是包含列名标签的非常不规则的文本。因此,我们需要使用正则表达式分词器进行特征转换预处理,然后才能用于我们的目的。

步骤 4:使用 RegexTokenizer 进行特征转换

以下是RegexTokenizer的代码:

RegexTokenizer regexTokenizer1 = new RegexTokenizer().setInputCol("value").setOutputCol("labelText").setPattern("\\t.*$"); 

仔细观察前面的代码段,您会发现我们指定了输入列名为value,输出列名为labelText和模式。现在使用以下代码段使用刚刚标记的正则表达式分词器创建另一个数据框:

Dataset<Row> labelTextDataFrame = regexTokenizer1.transform(df); 

现在,让我们使用以下语句查看新数据框labelTextDataFrame包含什么:

labelTextDataFrame.show(); 

使用 Spark MLlib 进行主题建模

图 26:一个新列,其中的字符转换为相应的小写字符

在前面的屏幕截图(图 26)中显示,分词器创建了一个新列,大多数大写单词或字符已转换为相应的小写字符。由于主题建模关心每个输入词的词权重和频率,我们需要从标签文本中分离单词,这是通过使用以下代码段完成的:

RegexTokenizer regexTokenizer2 = new RegexTokenizer().setInputCol("value").setOutputCol("text").setPattern("\\W"); 

现在让我们创建另一个数据框,并使用以下代码查看转换的结果:

Dataset<Row> labelFeatureDataFrame = regexTokenizer2.transform(labelTextDataFrame); 
labelFeaturedDataFrame.show(); 

使用 Spark MLlib 进行主题建模

图 27:标签文本作为逗号分隔的单词

从前面的屏幕截图(图 27)中,我们可以看到添加了一个新列label,其中显示标签文本作为逗号分隔的单词。

现在,由于我们有一堆文本可用,为了使预测和主题建模更容易,我们需要对我们分割的单词进行索引。但在此之前,我们需要在新数据框中交换labelTexttext,如图 28所示。要检查是否真的发生了这种情况,只需打印新创建的数据框:

Dataset<Row> newDF = labelFeatureDataFrame 
        .withColumn("labelTextTemp",          labelFeatureDataFrame.col("labelText") 
          .cast(DataTypes.StringType))        .drop(labelFeatureDataFrame.col("labelText")).withColumnRenamed("labelTextTemp", "labelText"); 
newDF.show(); 

使用 Spark MLlib 进行主题建模

图 28:在新数据框中交换 labelText 和 text

步骤 5:通过字符串索引器进行特征转换

以下是特征转换的代码:

StringIndexer indexer = new StringIndexer().setInputCol("labelText").setOutputCol("label"); 

现在为步骤 2中创建的数据框newDF创建一个新数据框,并查看数据框的内容。请注意,我们选择了旧列labelText,并将新列简单设置为label

Dataset<Row> indexed = indexer.fit(newDF).transform(newDF); 
indexed.select(indexed.col("labelText"), indexed.col("label"), indexed.col("text")).show(); 
Indexed.show(); 

使用 Spark MLlib 进行主题建模

图 29:与 labelText 列相对应的标签

因此,如图 29所示,我们得到了一个新列label,其中包含与labelText列相对应的标签。接下来的步骤是去除停用词。

步骤 6:特征转换(去除停用词)

以下是去除停用词的特征转换的代码:

StopWordsRemover remover = new StopWordsRemover(); 
String[] stopwords = remover.getStopWords(); 
remover.setStopWords(stopwords).setInputCol("text").setOutputCol("filteredWords"); 

Spark 的StopWordsRemover类的当前实现包含以下单词作为停用词。由于我们没有任何先决条件,我们直接使用了这些单词:

使用 Spark MLlib 进行主题建模

图 30:Spark 提供的用于文本分析的一些停用词

步骤 7:通过去除停用词创建一个过滤后的数据集

以下是通过去除停用词创建过滤后数据集的代码:

Dataset<Row> filteredDF = remover.transform(indexed); 
filteredDF.show(); 
filteredDF.select(filteredDF.col("label"), filteredDF.col("filteredWords")).show(); 

现在为过滤后的单词(即不包括停用词)创建一个新数据框。让我们查看过滤后数据集的内容:

Dataset<Row> featurizedData = hashingTF.transform(filteredDF); 
featurizedData.show(); 

使用 Spark MLlib 进行主题建模

图 31:排除停用词的过滤词

第 8 步:使用 HashingTF 进行特征提取

以下是使用 HashingTF 进行特征提取的代码:

int numFeatures = 5; 
HashingTF hashingTF = new HashingTF().setInputCol("filteredWords").setOutputCol("rawFeatures").setNumFeatures(numFeatures); 

在前面的代码中,我们只对五个特征进行了 HashingTF,以简化操作。现在从旧数据框架(即filteredDF)中提取特征创建另一个数据框架,并显示相同的输出:

Dataset<Row> featurizedData = hashingTF.transform(filteredDF); 
       featurizedData.show();   

使用 Spark MLlib 进行主题建模

图 32:从旧数据框架filteredDF中提取特征创建的数据框架

提示

有关特征转换、估计器和哈希的更多信息和 API 文档细节,请参考 Spark 网站spark.apache.org/docs/latest/ml-features.html

第 9 步:使用 IDF 估计器进行特征提取

IDF idf = new IDF().setInputCol("rawFeatures").setOutputCol("features"); 
IDFModel idfModel = idf.fit(featurizedData); 

前面的代码通过拟合idfModel从原始特征中创建新特征,该模型接受第 5 步中的特征数据框架(即featurizedData)。现在让我们创建并显示使用我们刚刚创建的估计器(即idfModel)的重新缩放数据的新数据框架,该估计器消耗了用于特征化数据的旧数据框架(即featurizedData):

Dataset<Row> rescaledData = idfModel.transform(featurizedData); 
rescaledData.show(). 

使用 Spark MLlib 进行主题建模

图 33:使用估计器重新缩放的数据

第 10 步:卡方特征选择

卡方特征选择选择要用于预测分类标签的分类特征。以下代码段执行此选择:

ChiSqSelector selector = new org.apache.spark.ml.feature.ChiSqSelector(); 
selector.setNumTopFeatures(5).setFeaturesCol("features").setLabelCol("label").setOutputCol("selectedFeatures"); 

现在创建另一个选定特征的数据框架,如下所示:

Dataset<Row> result = selector.fit(rescaledData).transform(rescaledData); 
result.show(); 

使用 Spark MLlib 进行主题建模

图 34:卡方特征选择

您可以从前面的输出/屏幕截图中看到,我们的数据已准备好用于训练 LDA 模型并进行主题建模。

第 11 步:创建并训练 LDA 模型

使用训练数据集(即数据框架结果)创建并训练 LDA 模型,指定K(主题建模必须大于 1 的聚类数,其中默认值为 10)和最大迭代次数:

long value = 5;     
LDA lda = new LDA().setK(10).setMaxIter(10).setSeed(value); 
LDAModel model = lda.fit(result); 

现在我们已经训练、拟合并准备好用于我们目的的模型,让我们来看看我们的输出。但在这之前,我们需要有一个能够捕获与主题相关的指标的数据框架。使用以下代码:

System.out.println(model.vocabSize()); 
Dataset<Row> topics = model.describeTopics(5); 
org.apache.spark.ml.linalg.Matrix metric = model.topicsMatrix(); 

现在让我们看一下主题分布。看看前面的数据集:

System.out.println(metric); 
topics.show(false); 

使用 Spark MLlib 进行主题建模

图 35:相应的术语权重、主题名称和术语索引

仔细观察前面的输出,我们找到了相应的术语权重、主题名称和术语索引。前述术语及其相应的权重将在第九章中使用,使用流式和图数据进行高级机器学习,用于使用 GraphX 和 Scala 查找连接组件。

但是,我们还需要实际的术语。我们将在第九章中展示检索术语的详细技术,使用流式和图数据进行高级机器学习,这在很大程度上取决于需要开发或生成的术语词汇的概念。

可扩展性

前面的例子展示了如何使用 LDA 算法进行主题建模作为独立应用。然而,根据 Joseph B.在 Databricks 博客中的一篇文章databricks.com/blog/2015/03/25/topic-modeling-with-lda-mllib-meets-graphx.html,LDA 的并行化并不直接,已经有许多研究论文提出了不同的策略。在这方面的关键障碍是所有方法都涉及大量的通信。根据 Databricks 网站上的博客,以下是在实验过程中使用的数据集和相关训练和测试集的统计数据:

  • 训练集大小:460 万份文件

  • 词汇量:110 万个术语

  • 训练集大小:11 亿个标记(~每份文件 239 个单词)

  • 100 个主题

  • 16 个工作节点的 EC2 集群,例如 M4.large 或 M3.medium,具体取决于预算和要求

  • 时间结果:平均每次迭代 176 秒/次

使用 Spark 进行信用风险分析流程

在本节中,我们将开发一个信用风险流程,这在银行和信用合作社等金融机构中通常使用。首先,我们将讨论信用风险分析是什么以及为什么它很重要,然后使用基于 Spark ML 的流程开发基于随机森林的分类器。最后,我们将提供一些建议以提高性能。

什么是信用风险分析?为什么它很重要?

当申请人申请贷款并且银行收到申请时,基于申请人的资料,银行必须决定是否批准贷款申请。

在这方面,银行对贷款申请的决定涉及两种风险:

  • 申请人是良好的信用风险:这意味着客户或申请人更有可能偿还贷款。那么,如果贷款未获批准,银行可能会遭受业务损失。

  • 申请人是不良的信用风险:这意味着客户或申请人很可能无法偿还贷款。在这种情况下,向客户批准贷款将导致银行财务损失。

我们的常识告诉我们,第二种风险是更大的风险,因为银行更有可能无法收回借款金额。

因此,大多数银行或信用合作社评估向客户、申请人或顾客放贷所涉及的风险。在商业分析中,最小化风险往往会最大化银行自身的利润。换句话说,从财务角度来看,最大化利润和最小化损失是重要的。

通常,银行会根据申请人的不同因素和参数对贷款申请做出决定。例如,他们的贷款申请的人口统计和社会经济状况。

使用 Spark ML 开发信用风险分析流程

在本节中,我们将首先详细讨论信用风险数据集,以便获得一些见解。之后,我们将看看如何开发大规模的信用风险流程。最后,我们将提供一些性能改进建议,以提高预测准确性。

数据集探索

德国信用数据集是从 UCI 机器学习库archive.ics.uci.edu/ml/machine-learning-databases/statlog/german/下载的。尽管链接中提供了数据集的详细描述,但我们在表 3中提供了一些简要见解。数据包含 21 个变量的与信用有关的数据,以及 1000 个贷款申请人被认为是良好还是不良信用风险的分类。表 3显示了在将数据集提供在线之前考虑的每个变量的详细信息:

条目 变量 解释
1 creditability 有偿还能力
2 balance 当前余额
3 duration 申请贷款的期限
4 history 是否有不良贷款历史?
5 purpose 贷款目的
6 amount 申请金额
7 savings 每月储蓄
8 employment 就业状态
9 instPercent 利息百分比
10 sexMarried 性别和婚姻状况
11 guarantors 是否有担保人?
12 residenceDuration 目前地址居住时间
13 assets 净资产
14 age 申请人年龄
15 concCredit 并发信用
16 apartment 住宅状况
17 credits 当前信用
18 occupation 职业
19 dependents 受抚养人数
20 hasPhone 申请人是否使用电话
21 foreign 申请人是否是外国人

表 3:德国信用数据集属性

请注意,尽管表 3描述了数据集中的变量,但没有相关的标题。在表 3中,我们显示了每个变量的位置和相关重要性。

Spark ML 的信用风险流程

涉及到几个步骤,从数据加载、解析、数据准备、训练测试集准备、模型训练、模型评估和结果解释。让我们逐步进行这些步骤。

步骤 1:加载所需的 API 和库

以下是加载所需 API 和库的代码:

import org.apache.spark.api.java.JavaRDD; 
import org.apache.spark.api.java.function.Function; 
import org.apache.spark.ml.classification.RandomForestClassificationModel; 
import org.apache.spark.ml.classification.RandomForestClassifier; 
import org.apache.spark.ml.evaluation.BinaryClassificationEvaluator; 
import org.apache.spark.ml.feature.StringIndexer; 
import org.apache.spark.ml.feature.VectorAssembler; 
import org.apache.spark.mllib.evaluation.RegressionMetrics; 
import org.apache.spark.sql.Dataset; 
import org.apache.spark.sql.Row; 
import org.apache.spark.sql.SparkSession; 

步骤 2:创建 Spark 会话

以下是另一个创建 Spark 会话的代码:

  static SparkSession spark = SparkSession.builder() 
      .appName("CreditRiskAnalysis") 
      .master("local[*]") 
      .config("spark.sql.warehouse.dir", "E:/Exp/") 
      .getOrCreate();  

步骤 3:加载和解析信用风险数据集

请注意,数据集采用逗号分隔值CSV)格式。现在使用 Databricks 提供的 CSV 读取器加载和解析数据集,并准备一个 Row 数据集,如下所示:

String csvFile = "input/german_credit.data"; 
Dataset<Row> df = spark.read().format("com.databricks.spark.csv").option("header", "false").load(csvFile); 

现在,显示数据集以了解确切的结构,如下所示:

df.show(); 

Spark ML 的信用风险流程

图 36:信用风险数据集快照

步骤 4:创建 Credit 类型的 RDD

创建一个类型为Credit的 RDD,如下所示:

JavaRDD<Credit> creditRDD = df.toJavaRDD().map(new Function<Row, Credit>() { 
      @Override 
      public Credit call(Row r) throws Exception { 
        return new Credit(parseDouble(r.getString(0)), parseDouble(r.getString(1)) - 1, 
            parseDouble(r.getString(2)), parseDouble(r.getString(3)), parseDouble(r.getString(4)), 
            parseDouble(r.getString(5)), parseDouble(r.getString(6)) - 1, parseDouble(r.getString(7)) - 1, 
            parseDouble(r.getString(8)), parseDouble(r.getString(9)) - 1, parseDouble(r.getString(10)) - 1, 
            parseDouble(r.getString(11)) - 1, parseDouble(r.getString(12)) - 1, 
            parseDouble(r.getString(13)), parseDouble(r.getString(14)) - 1, 
            parseDouble(r.getString(15)) - 1, parseDouble(r.getString(16)) - 1, 
            parseDouble(r.getString(17)) - 1, parseDouble(r.getString(18)) - 1, 
            parseDouble(r.getString(19)) - 1, parseDouble(r.getString(20)) - 1); 
      } 
    }); 

前面的代码段在使用parseDouble()方法将变量作为双精度值创建了一个Credit类型的 RDD,该方法接受一个字符串并以Double格式返回相应的值。parseDouble()方法如下所示:

  public static double parseDouble(String str) { 
    return Double.parseDouble(str); 
  } 

现在我们需要了解Credit类的结构,以便结构本身有助于使用类型化类创建 RDD。

嗯,Credit类基本上是一个单例类,通过构造函数初始化数据集中的 21 个变量的所有 setter 和 getter 方法。以下是该类:

public class Credit { 
  private double creditability; 
  private double balance; 
  private double duration; 
  private double history; 
  private double purpose; 
  private double amount; 
  private double savings; 
  private double employment; 
  private double instPercent; 
  private double sexMarried; 
  private double guarantors; 
  private double residenceDuration; 
  private double assets; 
  private double age; 
  private double concCredit; 
  private double apartment; 
  private double credits; 
  private double occupation; 
  private double dependents; 
  private double hasPhone; 
  private double foreign; 

  public Credit(double creditability, double balance, double duration, 
  double history, double purpose, double amount, 
      double savings, double employment, double instPercent, 
      double sexMarried, double guarantors, 
      double residenceDuration, double assets, double age, 
      double concCredit, double apartment, double credits, 
      double occupation, double dependents, double hasPhone, double foreign) { 
    super(); 
    this.creditability = creditability; 
    this.balance = balance; 
    this.duration = duration; 
    this.history = history; 
    this.purpose = purpose; 
    this.amount = amount; 
    this.savings = savings; 
    this.employment = employment; 
    this.instPercent = instPercent; 
    this.sexMarried = sexMarried; 
    this.guarantors = guarantors; 
    this.residenceDuration = residenceDuration; 
    this.assets = assets; 
    this.age = age; 
    this.concCredit = concCredit; 
    this.apartment = apartment; 
    this.credits = credits; 
    this.occupation = occupation; 
    this.dependents = dependents; 
    this.hasPhone = hasPhone; 
    this.foreign = foreign; 
  } 

  public double getCreditability() { 
    return creditability; 
  } 

  public void setCreditability(double creditability) { 
    this.creditability = creditability; 
  } 

  public double getBalance() { 
    return balance; 
  } 

  public void setBalance(double balance) { 
    this.balance = balance; 
  } 

  public double getDuration() { 
    return duration; 
  } 

  public void setDuration(double duration) { 
    this.duration = duration; 
  } 

  public double getHistory() { 
    return history; 
  } 

  public void setHistory(double history) { 
    this.history = history; 
  } 

  public double getPurpose() { 
    return purpose; 
  } 

  public void setPurpose(double purpose) { 
    this.purpose = purpose; 
  } 

  public double getAmount() { 
    return amount; 
  } 

  public void setAmount(double amount) { 
    this.amount = amount; 
  } 

  public double getSavings() { 
    return savings; 
  } 

  public void setSavings(double savings) { 
    this.savings = savings; 
  } 

  public double getEmployment() { 
    return employment; 
  } 

  public void setEmployment(double employment) { 
    this.employment = employment; 
  } 

  public double getInstPercent() { 
    return instPercent; 
  } 

  public void setInstPercent(double instPercent) { 
    this.instPercent = instPercent; 
  } 

  public double getSexMarried() { 
    return sexMarried; 
  } 

  public void setSexMarried(double sexMarried) { 
    this.sexMarried = sexMarried; 
  } 

  public double getGuarantors() { 
    return guarantors; 
  } 

  public void setGuarantors(double guarantors) { 
    this.guarantors = guarantors; 
  } 

  public double getResidenceDuration() { 
    return residenceDuration; 
  } 

  public void setResidenceDuration(double residenceDuration) { 
    this.residenceDuration = residenceDuration; 
  } 

  public double getAssets() { 
    return assets; 
  } 

  public void setAssets(double assets) { 
    this.assets = assets; 
  } 

  public double getAge() { 
    return age; 
  } 

  public void setAge(double age) { 
    this.age = age; 
  } 

  public double getConcCredit() { 
    return concCredit; 
  } 

  public void setConcCredit(double concCredit) { 
    this.concCredit = concCredit; 
  } 

  public double getApartment() { 
    return apartment; 
  } 

  public void setApartment(double apartment) { 
    this.apartment = apartment; 
  } 

  public double getCredits() { 
    return credits; 
  } 

  public void setCredits(double credits) { 
    this.credits = credits; 
  } 

  public double getOccupation() { 
    return occupation; 
  } 

  public void setOccupation(double occupation) { 
    this.occupation = occupation; 
  } 

  public double getDependents() { 
    return dependents; 
  } 

  public void setDependents(double dependents) { 
    this.dependents = dependents; 
  } 

  public double getHasPhone() { 
    return hasPhone; 
  } 

  public void setHasPhone(double hasPhone) { 
    this.hasPhone = hasPhone; 
  } 

  public double getForeign() { 
    return foreign; 
  } 

  public void setForeign(double foreign) { 
    this.foreign = foreign; 
  } 
} 

如果您查看类的流程,首先它声明了 21 个变量,对应数据集中的 21 个特征。然后使用构造函数对它们进行初始化。其余的是简单的 setter 和 getter 方法。

步骤 5:从 Credit 类型的 RDD 创建类型为 Row 的数据集

以下代码显示如何创建类型为 Row 的数据集:

Dataset<Row> creditData = spark.sqlContext().createDataFrame(creditRDD, Credit.class); 

现在将数据集保存为临时视图,或更正式地说,保存为内存中的表以供查询,如下所示:

creditData.createOrReplaceTempView("credit"); 

现在让我们了解表的模式,如下所示:

creditData.printSchema(); 

Spark ML 的信用风险流程

图 37:数据集的模式

步骤 6:使用 VectorAssembler 创建特征向量

使用 Spark 的VectorAssembler类为 21 个变量创建一个新的特征向量,如下所示:

VectorAssembler assembler = new VectorAssembler() 
        .setInputCols(new String[] { "balance", "duration", "history", "purpose", "amount", "savings", 
            "employment", "instPercent", "sexMarried", "guarantors", "residenceDuration", "assets", "age", 
            "concCredit", "apartment", "credits", "occupation", "dependents", "hasPhone", "foreign" }) 
        .setOutputCol("features"); 

步骤 7:通过组合和转换组装器创建数据集

通过使用先前创建的creditData数据集转换组装器来创建一个数据集,并打印数据集的前 20 行,如下所示:

Dataset<Row> assembledFeatures = assembler.transform(creditData); 
assembledFeatures.show(); 

Spark ML 的信用风险流程

图 38:新创建的特色信用数据集

步骤 8:创建用于预测的标签

从前面的数据集(图 38)的信用度列创建一个标签列,如下所示:

StringIndexer creditabilityIndexer = new StringIndexer().setInputCol("creditability").setOutputCol("label"); 
Dataset<Row> creditabilityIndexed = creditabilityIndexer.fit(assembledFeatures).transform(assembledFeatures); 

现在让我们使用show()方法来探索新的数据集,如下所示:

creditabilityIndexed.show(); 

使用 Spark ML 的信用风险管道

图 39:带有新标签列的数据集

从前面的图中,我们可以了解到与数据集相关的标签只有两个,分别是 1.0 和 0.0。这表明问题是一个二元分类问题。

步骤 9:准备训练和测试集

按以下方式准备训练和测试集:

long splitSeed = 12345L; 
Dataset<Row>[] splits = creditabilityIndexed.randomSplit(new double[] { 0.7, 0.3 }, splitSeed); 
Dataset<Row> trainingData = splits[0]; 
Dataset<Row> testData = splits[1]; 

在这里,训练集和测试集的比例分别为 70%和 30%,种子值较长,以防止在每次迭代中生成随机结果。

步骤 10:训练随机森林模型

要训练随机森林模型,请使用以下代码:

RandomForestClassifier classifier = new RandomForestClassifier() 
        .setImpurity("gini") 
        .setMaxDepth(3) 
        .setNumTrees(20) 
        .setFeatureSubsetStrategy("auto") 
        .setSeed(splitSeed); 

如前所述,问题是一个二元分类问题。因此,我们将使用二元评估器对label列评估随机森林模型,如下所示:

RandomForestClassificationModel model = classifier.fit(trainingData); 
BinaryClassificationEvaluator evaluator = new BinaryClassificationEvaluator().setLabelCol("label"); 

现在我们需要在测试集上收集模型性能指标,如下所示:

Dataset<Row> predictions = model.transform(testData); 
model.toDebugString(); 

步骤 11:打印性能参数

我们将观察二元评估器的几个性能参数,例如拟合模型后的准确性,均方误差MSE),平均绝对误差MAE),均方根误差RMSE),R 平方和解释变量等。让我们按照以下方式进行:

double accuracy = evaluator.evaluate(predictions); 
System.out.println("Accuracy after pipeline fitting: " + accuracy); 
RegressionMetrics rm = new RegressionMetrics(predictions); 
System.out.println("MSE: " + rm.meanSquaredError()); 
System.out.println("MAE: " + rm.meanAbsoluteError()); 
System.out.println("RMSE Squared: " + rm.rootMeanSquaredError()); 
System.out.println("R Squared: " + rm.r2()); 
System.out.println("Explained Variance: " + rm.explainedVariance() + "\n"); 

前面的代码段生成了以下输出:

Accuracy after pipeline fitting: 0.7622000403307129 
MSE: 1.926235109206349E7 
MAE: 3338.3492063492063 
RMSE Squared: 4388.8895055655585 
R Squared: -1.372326447615067 
Explained Variance: 1.1144695981899707E7 

性能调优和建议

如果您查看第 11 步的性能指标,显然信用风险预测不尽如人意,特别是在准确性方面,只有 76.22%。这意味着对于给定的测试数据,我们的模型可以以 76.22%的精度预测是否存在信用风险。由于我们需要对这些敏感的金融领域更加小心,因此无疑需要更高的准确性。

现在,如果您想提高预测性能,您应该尝试使用除基于随机森林的分类器之外的其他模型进行模型训练。例如,逻辑回归或朴素贝叶斯分类器。

此外,您可以使用基于 SVM 的分类器或基于神经网络的多层感知器分类器。在第七章中,调整机器学习模型,我们将看看如何调整超参数以选择最佳模型。

扩展 ML 管道

数据挖掘和机器学习算法对并行和分布式计算平台提出了巨大挑战。此外,并行化机器学习算法高度依赖于任务的特定性,并且通常取决于前面提到的问题。在第一章中,使用 Spark 进行数据分析简介,我们讨论并展示了如何在集群或云计算基础设施(即 Amazon AWS/EC2)上部署相同的机器学习应用。

按照这种方法,我们可以处理具有巨大批量大小或实时处理的数据集。除此之外,扩展机器学习应用还涉及到成本、复杂性、运行时间和技术要求等其他权衡。此外,为了进行大规模机器学习的任务,需要了解可用选项的优势、权衡和约束,以做出适合任务的算法和平台选择。

为了解决这些问题,在本节中,我们将提供一些处理大型数据集以部署大规模机器学习应用的理论方面。然而,在进一步进行之前,我们需要知道一些问题的答案。例如:

  • 我们如何收集大型数据集以满足我们的需求?

  • 大数据集有多大,我们如何处理它们?

  • 有多少训练数据足以扩展大型数据集上的 ML 应用?

  • 如果我们没有足够的训练数据,有什么替代方法?

  • 应该使用什么样的机器学习算法来满足我们的需求?

  • 应选择哪种平台进行并行学习?

在这里,我们讨论了部署和扩展处理前述大数据挑战的机器学习应用的一些重要方面,包括大小、数据偏斜、成本和基础设施。

大小很重要

大数据是指量、种类、真实性、速度和价值都太大,以至于传统的内存计算机系统无法处理。通过处理大数据来扩展机器学习应用涉及任务,如分类、聚类、回归、特征选择、提升决策树和支持向量机。我们如何处理 10 亿或 1 万亿的数据实例?此外,50 亿部手机、Twitter 等社交网络以前所未有的方式产生大数据集。另一方面,众包是现实,即在一周内标记 10 万个以上的数据实例。

就稀疏性而言,大数据集不能太稀疏,而从内容角度来看要密集。从机器学习的角度来看,为了证明这一点,让我们想象一个数据标记的例子。例如,100 万个数据实例不可能属于 100 万个类别,因为拥有 100 万个类别是不切实际的,而且多个数据实例属于特定类别。因此,基于如此大规模数据集的稀疏性和大小,进行预测分析是另一个需要考虑和处理的挑战。

大小与偏斜度考虑

机器学习还取决于标记数据的可用性,其可信度取决于学习任务,如监督、无监督或半监督。您可能拥有结构化数据,但存在极端的偏斜。更具体地说,假设您有 1K 个标记和 1M 个未标记的数据点,因此标记和未标记的比例为 0.1%。

因此,您认为只有 1K 标签点足以训练监督模型吗?再举一个例子,假设您有 1M 个标记和 1B 个未标记的数据点,其中标记和未标记的比例也是 0.1%。同样,又出现了同样的问题,即,仅有 1M 个标签足以训练监督模型吗?

现在的问题是,使用现有标签作为半监督聚类、分类或回归的指导而不是指令,可以采取什么措施或方法。或者,标记更多的数据,要么手动标记,要么在众包的帮助下。例如,假设有人想对某种疾病进行聚类或分类分析。更具体地说,假设我们想对推文进行分类,看特定的推文是否指示出与埃博拉或流感相关的疾病。在这种情况下,我们应该使用半监督方法来标记推文。

然而,在这种情况下,数据集可能非常偏斜,或者标记可能存在偏见。通常,训练数据来自不同的用户,其中显式用户反馈可能经常具有误导性。

因此,从隐式反馈中学习是一个更好的主意;例如,通过点击网络搜索结果收集数据。在这些类型的大规模数据集中,训练数据的偏斜很难检测到,如在第四章中讨论的,通过特征工程提取知识。因此,在大数据集中要警惕这种偏斜。

成本和基础设施

要扩展您的机器学习应用,您将需要更好的基础设施和计算能力来处理如此庞大的数据集。最初,您可能希望利用本地集群。然而,有时,如果数据集呈指数增长,集群可能不足以扩展您的机器学习应用。

在部署 ML 管道到强大基础设施(如亚马逊 AWS 云计算,如 EC2)的章节中,您将不得不选择按使用量付费,以享受云作为平台即服务和基础设施即服务,即使您使用自己的 ML 应用作为软件即服务。

提示和性能考虑

Spark 还支持用于超参数调整的交叉验证,这将在下一章中广泛讨论。Spark 将交叉验证视为一种元算法,它使用用户指定的参数组合来拟合底层估计器,交叉评估拟合模型并输出最佳模型。

然而,对于底层估计器并没有特定的要求,它可以是一个管道,只要它能够与一个评估器配对,输出预测的标量度量,如精度和召回率。

让我们回顾 OCR 预测,我们发现精度为 75%,显然是不令人满意的。现在让我们进一步调查原因,打印出标签 8.0 或“I”的混淆矩阵。如果您查看图 40中的矩阵,您会发现正确预测的实例数量很少:

提示和性能考虑

图 40:标签 8.0 或“I”的混淆矩阵

现在让我们尝试使用随机森林模型进行预测。但在进入模型训练步骤之前,让我们对随机森林分类器所需的参数进行一些初始化,它也支持多类分类,如 LBFGS 的逻辑回归模型:

Integer numClasses = 26; 
HashMap<Integer, Integer>categoricalFeaturesInfo = new HashMap<Integer, Integer>(); 
Integer numTrees = 5; // Use more in practice. 
String featureSubsetStrategy = "auto"; // Let the algorithm choose. 
String impurity = "gini"; 
Integer maxDepth = 20; 
Integer maxBins = 40; 
Integer seed = 12345; 

现在通过指定先前的参数来训练模型,如下所示:

final RandomForestModelmodel = RandomForest.trainClassifier(training, numClasses, categoricalFeaturesInfo, numTrees, featureSubsetStrategy, impurity, maxDepth, maxBins, seed); 

现在让我们看看它的表现。我们将重用我们在第 9 步中使用的相同代码段。请参考以下截图:

提示和性能考虑

图 41:精度和召回率的性能指标

提示和性能考虑

图 42:标签 8.0 或“I”的改进混淆矩阵

如果您查看图 42,您会发现在所有打印的参数方面有显著的改善,精度已从 75.30%提高到 89.20%。这背后的原因是随机森林模型对于全局最大值计算的改进解释,以及混淆矩阵,如图 38所示;您会发现由对角箭头标记的预测实例数量有显著改善。

通过反复试验,您可以确定一组显示潜力的算法,但是您如何知道哪个是最好的呢?此外,如前所述,很难为您的数据集找到表现良好的机器学习算法。因此,如果您对 89.20%的准确性仍然不满意,我建议您调整参数值并查看精度和召回率。

总结

在本章中,我们展示了几个机器学习应用,并试图区分 Spark MLlib 和 Spark ML。我们还表明,仅使用 Spark ML 或 Spark MLlib 开发完整的机器学习应用是非常困难的。

然而,我们想要提出一个结合的方法,或者说这两个 API 之间的互操作性,对于这些目的来说是最好的。此外,我们学习了如何使用 Spark ML 库构建 ML 管道,以及如何通过考虑一些性能考虑因素来扩展基本模型。

调整算法或机器学习应用可以简单地被认为是一个过程,通过这个过程,您可以优化影响模型的参数,以使算法在其最佳状态下运行(就运行时间和内存使用而言)。

在第七章中,调整机器学习模型,我们将更多地讨论调整机器学习模型的内容。我们将尝试重用本章和第五章中的一些应用,通过调整几个参数来提高性能。

第七章:调整机器学习模型

调整算法或机器学习应用程序只是一个过程,通过这个过程,可以使算法在优化影响模型的参数时以最佳方式运行(以运行时间和内存使用方面)。本章旨在指导读者进行模型调整。它将涵盖用于优化 ML 算法性能的主要技术。技术将从 MLlib 和 Spark ML 的角度进行解释。在第五章和第六章中,我们描述了如何从数据收集到模型评估开发一些完整的机器学习应用程序和管道。在本章中,我们将尝试重用其中一些应用程序,通过调整一些参数(如超参数调整、MLlib 和 Spark ML 的网格搜索参数调整、随机搜索参数调整和交叉验证)来提高性能。还将讨论假设,这也是一个重要的统计测试。总之,本章将涵盖以下主题:

  • 有关机器学习模型调整的详细信息

  • 模型调整中的典型挑战

  • 评估机器学习模型

  • 验证和评估技术

  • 机器学习的参数调整

  • 假设检验

  • 机器学习模型选择

有关机器学习模型调整的详细信息

基本上,可以说机器学习(ML)的最终目标是制造一台可以自动从数据中构建模型的机器,而不需要繁琐和耗时的人类参与。你会发现机器学习中的一个困难是学习算法,如决策树、随机森林和聚类技术,需要在使用模型之前设置参数。或者,您需要对这些参数设置一些约束。

如何设置这些参数取决于一系列因素和规格。在这方面,您的目标通常是将这些参数设置为最佳值,以使您以最佳方式完成学习任务。因此,调整算法或 ML 技术可以简单地被认为是一个过程,通过这个过程,您可以优化影响模型性能的参数,以使算法以最佳方式运行。

在第三章,“通过了解数据来理解问题”,和第五章,“通过示例进行监督和无监督学习”,我们讨论了一些根据数据选择最佳算法的技术,并讨论了最广泛使用的算法。为了从模型中获得最佳结果,您必须首先定义“最佳”是什么。我们将以抽象和具体的方式讨论调整。

在机器学习的抽象意义上,调整涉及使用已经确定影响系统性能的变量或基于参数,通过一些适当的度量来评估。因此,改进的性能揭示了哪些参数设置更有利(即调整)或不利(即未调整)。在常识层面上,调整实质上是选择算法的最佳参数,以优化其性能,考虑到硬件的工作环境、特定工作负载等。机器学习中的调整是一个自动化的过程。

好吧,让我们通过一些例子更具体地讨论这个问题。如果你选择一个用于聚类的 ML 算法,比如 KNN 或 K-Means,作为开发者/数据科学家/数据工程师,你需要指定模型或质心中的 K 的数量。

因此,问题是“你如何做到这一点?”从技术上讲,没有绕过调整模型的必要。计算上,一个天真的方法是尝试不同的 K 值作为模型,当然观察当你改变模型中的 K 的数量时,它如何转向组内和组间误差。

第二个例子可能是使用支持向量机SVM)进行分类任务。正如你所知,SVM 分类需要一个初始学习阶段,其中训练数据用于调整分类参数。这实际上表示一个初始的参数调整阶段,你可能会尝试调整模型以获得高质量的结果。

第三个实际例子表明,没有一套完美的优化适用于所有 Apache Web 服务器的部署。系统管理员从工作中的数据中学习,适当地优化其特定环境的 Apache Web 服务器配置。现在想象一个自动化过程来完成这三件事,也就是说,一个可以自行从数据中学习的系统;这就是机器学习的定义。一个以这种基于数据的方式调整自己参数的系统将是机器学习中调整的一个实例。现在,让我们总结为什么我们评估模型的预测性能的主要原因:

  • 我们希望估计泛化误差,即我们的模型在未来(未见过的)数据上的预测性能。

  • 我们希望通过调整学习算法并从给定的假设空间中选择表现最佳的模型来提高预测性能。

  • 我们想要确定最适合手头问题的机器学习算法;因此,我们想要比较不同的算法,选择表现最佳的算法以及该算法假设空间中表现最佳的模型。

简而言之,在找到最佳参数集的过程中有四个步骤:

  • 定义参数空间:首先,我们需要决定我们想要考虑的算法的确切参数值。

  • 定义交叉验证设置:其次,我们需要决定如何为数据选择最佳的交叉验证折叠(将在本章后面讨论)。

  • 定义度量标准:第三,我们需要决定使用哪种度量标准来确定最佳参数组合。例如,准确度、均方根误差、精确度、召回率或 F 分数等等。

  • 训练、评估和比较:第四,对于每个参数值的唯一组合,进行交叉验证,并根据用户在第三步中定义的错误度量,可以选择表现最佳的模型。

模型调整有许多可用的技术和算法,比如超参数优化或模型选择、超参数调整、网格搜索参数调整、随机搜索参数调整和交叉验证CV)。不幸的是,Spark 的当前实现只开发了其中的一部分,包括交叉验证器和训练验证拆分。

因此,我们将尝试使用这两个超参数来调整不同的模型,包括随机森林、线性回归和逻辑回归。一些来自第五章和第六章的应用将被重新使用,而不再提供许多细节,以使模型调整更容易。

模型调优中的典型挑战

在以下讨论之后,您可能会认为这个过程很困难,您是对的。事实上,由于确定最佳模型参数的困难,通常在有效地尝试更简单的选项之前会使用一些更复杂的学习算法,这些选项具有更好调整的参数。正如我们已经讨论过的,机器学习涉及大量的实验。调整学习算法的内部旋钮,通常称为超参数,从模型构建到预测以及部署之前同样重要。

从技术上讲,在训练数据集上使用不同超参数设置运行学习算法将导致不同的模型,当然也会有不同的性能参数。据 Oracle 开发人员称,不建议在没有首先确立清晰目标的情况下开始调优,因为如果没有成功的定义,就不可能成功。

随后,我们通常有兴趣从训练数据集中选择表现最佳的模型;我们需要找到一种方法来估计它们各自的性能,以便将它们彼此排名。在超越纯粹的算法微调之后,我们通常不仅仅是在尝试我们认为在给定情况下是最佳解决方案的单一算法。往往我们想要将不同的算法相互比较,通常是在预测和计算性能方面。

通常有一个非常基本的问题,即使用网格搜索和随机搜索进行参数调优。通常,一些机器学习方法需要使用其中之一来调整参数。例如,根据 Wei Ch.等人的说法,(支持向量机的一般公式,第 9 届国际神经信息处理会议论文集(ICONIP,02),V-5,2002 年 11 月 18-22 日) SVM 的标准公式如下:

模型调优中的典型挑战

图 1:SVM 的标准公式

现在,假设我们需要调整模型参数 C,并且需要轻松地进行调整。从方程中可以清楚地看到,调整 C 还涉及其他参数,如xiiw;其中,正则化参数 C > 0,是 w 的范数。在 RHS 中,它是稳定器,模型调优中的典型挑战是依赖于目标函数 f(xi)的经验损失项。在标准 SVM(线性 SVM 或其他变体)中,可以通过解决凸二次优化问题进一步最小化正则化功能。一旦问题解决了,它就保证了获得最佳预测性能的唯一全局最小解。因此,整个过程多多少少是一个优化问题。

总之,图 2,模型调优过程、考虑和工作流程显示了调优过程及其考虑作为工作流程:

模型调优中的典型挑战

图 2:模型调优过程、考虑和工作流程

现在,如果您获得了原始数据集,您很可能会进行预处理并将数据集分成训练集和测试集。因此,要调整超参数 C,您需要首先将训练集分成验证训练集和验证测试集。

之后,您可以尝试使用验证训练集和验证测试集来调整参数。然后使用您得到的最佳参数在完整的训练集上重新训练模型。现在您可以在测试集上执行测试作为最后一步。

到目前为止,您的方法似乎还可以,但以下两个选项中哪一个平均更好呢?

  • 使用在验证训练集上训练的最终模型进行最终测试会更好吗?

  • 或者最好使用整个训练集,并使用网格或随机搜索的最佳参数重新训练模型吗?尽管这些参数未针对此集进行优化,但在这种情况下,我们有最终的训练数据。

您打算选择选项 1 吗,因为参数已经在此训练(即验证训练集)集上进行了优化?还是您打算选择选项 2,因为尽管这些参数未针对训练集进行优化,但在这种情况下,您有最终的训练数据?我们建议您选择选项 2,但前提是您信任选项 2 中的验证设置。原因是您已执行了交叉验证CV)以确定最通用的参数设置,否则模型选择或您尝试优化的任何其他内容。这些发现应该应用于整个训练集,并在测试集上进行一次测试。

好吧,假设您选择了选项 2;现在第二个挑战正在发展。我们如何估计机器学习模型的性能?从技术上讲,您可能会认为我们应该将训练数据提供给我们的学习算法,以学习最佳模型。然后,我们可以基于测试标签预测标签。其次,我们计算测试数据集上的错误预测数量,以计算模型的错误率。

就这样?朋友,不要那么着急!根据我们的目标,不幸的是,估计该模型的性能并不那么微不足道。也许我们应该从另一个角度来解决前面的问题:我们为什么要关心性能估计?理想情况下,模型的估计性能告诉我们它在未观察到的数据上的表现如何-在应用机器学习或开发新算法的应用中,对未来数据进行预测通常是我们想要解决的主要问题。

最后,还有其他几个挑战,取决于数据结构、问题类型、问题领域和适当的用例,当您开始大量练习时,您将遇到这些挑战。

评估机器学习模型

在这一部分,我们将讨论如何评估机器学习模型,因为您应该始终评估模型,以确定它是否准备好始终表现良好,预测新数据和未来数据的目标。显然,未来的数据可能有许多未知的目标值。因此,您需要检查性能相关的指标,如数据上的 ML 模型的准确度指标。在这方面,您需要提供一个包含从训练模型生成的分数的数据集,然后评估模型以计算一组行业标准的评估指标。

要适当评估模型,您需要提供一个已标记目标的数据样本,这些数据将用作来自训练数据源的地面真相或事实数据集。正如我们已经讨论过的那样,使用相同的训练数据集评估 ML 模型的预测准确度可能没有用。

原因是模型本身可以根据其接收的奖励记住训练数据,而不是从中概括。因此,当 ML 模型训练完成时,您可以从模型中呈现的观察中了解要预测的目标值。之后,您可以将您训练的 ML 模型返回的预测值与已知的目标值进行比较。

最后,您可能对计算总结指标感兴趣,该指标显示性能指标,以指示预测值和真实值匹配的准确度参数,如精确度、召回率、加权真正例、加权真负例、提升等。然而,在这一部分,我们将特别讨论如何首先评估回归、分类(即二元分类、多类分类)和聚类模型。

评估回归模型

假设您是一名在线货币交易员,您在外汇或 Fortrade 上工作。现在您心中有两个货币对要买入或卖出,例如 GBP/USD 和 USD/JPY。如果您仔细观察这两个货币对,您会发现 USD 在这两个货币对中都是共同的。现在,如果您观察 USD、GBP 或 JPY 的历史价格,您可以预测未来的结果,即您应该开仓买入还是卖出。

这种问题可以被视为典型的回归问题。在这里,目标变量(在这种情况下是价格)是一个随时间变化的连续数值,基于市场开盘时间。因此,为了根据给定的某种货币(例如本例中的 USD、GBP 或 JPY)的特征值做出价格预测,我们可以拟合一个简单的线性回归或逻辑回归模型。

在这种情况下,特征值可以是历史价格和一些外部因素,这些因素会使某种货币或货币对的价值发生变化。通过这种方式,训练好的模型可以预测某种货币的价格。

回归模型(即线性、逻辑或广义线性回归模型)可用于找到或计算我们训练的相同数据集的分数,现在可以使用所有货币的预测价格或这三种货币的历史价格。我们可以通过分析预测价格与实际价格相比平均偏离多少来进一步评估模型的性能。通过这种方式,人们可以猜测预测价格是上涨还是下跌,并可以从外汇或 Fortrade 等在线货币网站赚钱。

评估二元分类模型

正如我们在二元分类场景中已经讨论过的,目标变量只有两种可能的结果。例如:{0, 1},{垃圾邮件,快乐},{B,N},{false,true}和{负面,正面}等。现在假设您获得了一个包含世界各地研究人员的人口统计学、社会经济和就业变量的数据集,并且您想要预测博士奖学金金额(即薪水水平)作为一个二元变量,其值为,比如{<=1.5K\(,>=4.5K\)}。

在这个特定的例子中,负类将代表薪水或奖学金每月低于或等于 1500 美元的研究人员。因此,另一方面,正类代表薪水高于或等于 4500 美元的所有其他研究人员。

现在,从问题场景中可以清楚地看出,这也是一个回归问题。因此,您将训练模型,对数据进行评分,并评估结果,看看它与实际标签相差多少。因此,在这种类型的问题中,您将进行实验,评估两类(即二元类)逻辑回归模型的性能,这是 ML 领域中最常用的二元分类器之一。

评估多类分类模型

在第六章中,构建可扩展的机器学习管道,我们开发了几个应用程序和管道,您可能还记得我们还使用逻辑回归为 OCR 数据集开发了一个多类分类问题,并使用多类指标显示了结果。在该示例中,有 26 个类别对应 26 个字符(即从 A 到 Z)。我们必须预测某个字符的类别或标签,以及它是否真的属于正确的类别或标签。这种回归问题可以使用多类分类方法解决。

因此,在这种类型的问题中,您还将进行实验,评估多类(即两类以上)逻辑回归模型的性能。

评估聚类模型

由于聚类模型在许多不同方面与分类和回归模型有显著差异,如果您评估一个聚类模型,您将找到一组不同的统计数据和与聚类模型相关的性能指标。在聚类模型评估技术中返回的性能指标描述了每个簇分配了多少数据点,簇之间的分离程度以及数据点在每个簇内的紧密程度。

例如,如果您回忆一下,在第五章中,通过示例进行监督和无监督学习,您会发现我们讨论并使用 Spark ML 和 MLlib 解决了一个聚类问题,在使用 Spark 进行无监督学习:示例部分。在这个特定的例子中,我们展示了使用 Saratoga NY Homes 数据集的 K-Means 聚类,并基于价格和地块大小特征对位于同一地区的房屋可能的邻域进行了探索性分析。这种问题可以使用聚类模型解决和评估。然而,Spark 的当前实现尚未为模型评估提供任何发展的算法。

验证和评估技术

在机器学习应用开发中有一些广泛使用的术语可能有点棘手和令人困惑,所以让我们一起讨论并梳理一下。这些术语包括模型、目标函数、假设、混淆矩阵、模型部署、归纳算法、分类器、学习算法、交叉验证和参数:

  • 目标函数:在强化学习或预测建模中,假设我们专注于对一个对象进行建模。最终目标是学习或逼近一个特定且未知但有针对性的函数。目标函数表示为f(x) = y;其中xy都是变量,f(x)是我们想要建模的真实函数,它也表示我们试图最大化或实现目标值y

  • 假设:统计假设(不要将其与研究者提出的研究假设混淆)是一个可通过观察过程进行测试的广义函数。该过程类似于通过训练数据集中的一组随机变量对建模的真实函数进行测试。假设检验的目标是提出一个假设,以测量两个数据集之间的统计关系,例如训练集和测试集,其中,这两个数据集都必须与理想化模型(记住不是随机化或标准化模型)具有统计显著性。

  • 学习算法:如前所述,机器学习应用的最终目标是找到或逼近目标函数。在这个持续的过程中,学习算法是一组指令,它使用训练数据集对目标函数进行建模。从技术上讲,学习算法通常伴随着一个假设空间,并制定最终的假设。

  • 模型:统计模型是一个数学模型,它在生成样本和类似数据时体现了一组假设。最终,该模型通常代表了数据生成过程的一个显著理想化形式。此外,在机器学习领域,假设和模型这两个术语经常可以互换使用。

  • 归纳算法:归纳算法接受特定实例的输入,以产生超出这些输入实例的广义模型。

  • 模型部署:模型部署通常表示将已构建和开发的模型应用于实际数据,以对一个示例进行预测。

  • 交叉验证:这是一种通过将数据分成大约相等大小的 K 个互斥子集或折叠来估计机器学习模型的准确性的方法。然后,模型在迭代中进行 K 次训练和测试,每次在可用数据集上排除一个折叠,然后在该折叠上进行测试。

  • 分类器:分类器是假设或离散值函数的特殊情况,用于为特定数据点分配最分类类别标签。

  • 回归器:回归器也是假设的一种特殊情况,它将未标记的特征映射到预定义度量空间内的值。

  • 超参数:超参数是机器学习算法的调整参数,学习算法将训练数据拟合到这些参数上。

根据 Pedro D. 等人在homes.cs.washington.edu/~pedrod/papers/cacm12.pdf中的 关于机器学习的一些有用的东西,我们还需要概述一些其他事项,如下所示:

  • 表示:分类器或回归器必须用计算机可以处理的某种形式语言表示,从而创建适当的假设空间。

  • 评估:需要一个评估函数(即目标函数或评分函数)来区分好的分类器或回归器与坏的分类器或回归器,该函数由算法内部使用,用于构建或训练模型。

  • 优化:我们还需要一种在分类器或回归器中搜索的方法,以寻求最高得分。因此,优化是决定学习者效率的关键,有助于确定最佳参数。

简而言之,在机器学习算法中学习的关键公式是:

学习 = 表示 + 评估 + 优化

因此,为了验证和评估训练好的模型,您需要非常清楚地理解上述术语,以便能够概念化机器学习问题以及 Spark ML 和 MLlib API 的正确用法。

机器学习模型的参数调整

在本节中,我们将讨论调整参数和技术,例如超参数调整、随机搜索参数调整、网格搜索参数调整和交叉验证等机器学习模型的调整参数和技术。

超参数调整

超参数调整是一种根据所呈现数据的性能选择正确参数组合的技术。这是从实践中获得机器学习算法的有意义和准确结果的基本要求之一。例如,假设我们有两个要调整的超参数,用于 图 3 中呈现的管道,即使用逻辑回归估计器的 Spark ML 管道模型(虚线仅在管道拟合期间发生)。

我们可以看到我们为每个参数放入了三个候选值。因此,总共会有九种组合。然而,图中只显示了四种,即 TokenizerHashingTFTransformerLogistic RegressionLR)。现在我们要找到最终会导致最佳评估结果的模型。正如我们在第六章中已经讨论的那样,拟合模型由分词器、哈希 TF 特征提取器和拟合的逻辑回归模型组成:

超参数调整

图 3:使用逻辑回归估计器的 Spark ML 管道模型(虚线仅在管道拟合期间发生)

图 3 显示了先前提到的管道的典型工作流程。然而,虚线仅在管道拟合期间发生。

如前所述,拟合的管道模型是一个转换器。转换器可用于预测、模型验证和模型检查。此外,我们还提到 ML 算法的一个不幸的特点是,它们通常有许多需要调整以获得更好性能的超参数。

例如,这些超参数中的正则化程度与 Spark MLlib 优化的模型参数不同。因此,如果没有对数据和要使用的算法的专业知识,很难猜测或衡量最佳的超参数组合。由于复杂数据集是基于 ML 问题类型的,管道的大小和超参数的数量可能呈指数级增长(或线性增长),即使对于 ML 专家来说,超参数调整也变得繁琐,更不用说调整参数的结果可能变得不可靠。

根据spark.apache.org/docs/latest/ml-guide.html提供的 Spark API 文档,用于指定 Spark ML 估计器和转换器的是一个独特且统一的 API。ParamMap是一组具有Param作为命名参数的(参数,值)对,由 Spark 提供自包含文档。从技术上讲,有两种方式可以传递参数给算法,如下所示:

  • 设置参数。例如,如果 LR 是LogisticRegression的一个实例(即估计器),您可以调用setMaxIter()方法如下:LR.setMaxIter(5)。这本质上是将回归实例指向模型的拟合:LR.fit()。在这个特定的例子中,最多会有五次迭代。

  • 第二个选项涉及将ParamMaps传递给fit()transform()(有关详细信息,请参阅图 1)。在这种情况下,任何参数都将被先前通过 ML 应用程序特定代码或算法中的 setter 方法指定的ParamMaps覆盖。

提示

为您的数据集创建一个表现良好的算法的简短列表的一个很好的方法是使用 R 中的 Caret 包,因为 Spark 中的调整不够健壮。Caret 是由辉瑞公司的 Max Kuhn 创建和维护的 R 中的一个包。开发始于 2005 年,后来被开源并上传到 CRAN,实际上 CRAN 是分类和回归训练CARET)的缩写。最初开发是出于运行给定问题的多个不同算法的需求。有兴趣的读者可以访问topepo.github.io/caret/index.html查看该包的理论和实际考虑。

网格搜索参数调整

假设您已经选择了您的超参数,并通过调整应用了调整,现在您还需要找到特征。在这方面,对超参数和特征空间进行完整的网格搜索计算量太大。因此,您需要执行 K 折交叉验证的一次折叠,而不是进行完整的网格搜索:

  • 使用折叠的训练集在交叉验证上调整所需的超参数,使用所有可用的特征。

  • 使用这些超参数选择所需的特征

  • 对 K 中的每个折叠重复计算

  • 最终模型是在使用从 CV 的每个折叠中选择的 N 个最常见特征的所有数据上构建的

有趣的是,超参数也将在交叉验证循环中再次使用所有数据进行调整。与完整的网格搜索相比,这种方法会有很大的缺点吗?实质上,我在每个自由参数的维度上进行线性搜索(找到一个维度上的最佳值,将其保持不变,然后找到下一个维度上的最佳值),而不是每个参数设置的每个组合。

沿着单个参数搜索而不是一起优化的最重要的缺点是忽略了相互作用。很常见的情况是,例如,不止一个参数影响模型复杂性。在这种情况下,您需要查看它们的相互作用,以成功地优化超参数。根据您的数据集有多大以及您比较了多少模型,返回最大观察性能的优化策略可能会遇到麻烦(这对于网格搜索和您的策略都是如此)。

原因是在大量性能估计中搜索最大值会忽略性能估计的方差:您可能最终得到一个看起来不错的模型和训练/测试拆分组合。更糟糕的是,您可能会得到几个看起来完美的组合,然后优化就无法知道选择哪个模型,因此变得不稳定。

随机搜索参数调整

在训练中优化调整参数的默认方法是使用网格搜索。这种方法通常是有效的,但是在存在许多调整参数的情况下,可能效率低下。有一些模型可以在相对较短的时间内找到调整参数的合理值,这是有益的。然而,有一些模型在小范围搜索中的效率可能会抵消其他优化。不幸的是,Spark 目前对超参数调整的实现没有提供任何随机搜索调整的技术。

提示

相反,例如,CARET 中的许多模型利用子模型技巧,其中评估了 M 个调整参数组合;可能远少于 M 个模型拟合所需的数量。当使用简单的网格搜索时,这种方法最有效。因此,使用随机搜索可能效率低下。最后,训练包装的许多模型具有少量参数。平均参数数量为 1.7。要使用随机搜索,trainControl中还有一个选项叫做 search。该参数的可能值是gridrandom。CARET 中包含的内置模型包含生成随机调整参数组合的代码。唯一组合的总数由 train 的tuneLength选项指定。

交叉验证

交叉验证(也称为旋转估计RE))是一种评估统计分析和结果质量的模型验证技术。目标是使模型向独立测试集泛化。

交叉验证技术的一个完美用途是从机器学习模型中进行预测。从技术上讲,如果您想要估计预测模型在实践中的准确性,当您将其部署为 ML 应用程序时,它将会有所帮助。

在交叉验证过程中,模型通常使用已知类型的数据集进行训练。相反,它使用未知类型的数据集进行测试。在这方面,交叉验证有助于使用验证集在训练阶段描述数据集以测试模型。

然而,为了最小化机器学习模型中的缺陷,如过拟合和欠拟合,交叉验证技术提供了关于模型如何泛化到独立集的见解。

有两种类型的交叉验证,可以如下分类:

  • 穷尽交叉验证:包括留 p 法交叉验证和留一法交叉验证

  • 非穷尽交叉验证:包括 K 折交叉验证和重复随机子采样验证交叉验证

由于页面限制,本书不会对这些进行详细讨论。此外,使用 Spark ML 和 Spark MLlib,读者将能够在下一节中按照我们的示例执行交叉验证。

除了时间序列数据外,在大多数情况下,研究人员/数据科学家/数据工程师使用 10 折交叉验证,而不是在验证集上进行测试(其中 K = 10)。这是最广泛应用的交叉验证技术,适用于各种用例和问题类型。此外,为了减少变异性,使用不同分区进行多次交叉验证迭代;最后,对多轮验证结果进行平均。

使用交叉验证而不是传统验证有两个主要优点,如下所述:

  • 首先,如果没有足够的数据可用于在单独的训练和测试集之间进行分区,就有可能失去重要的建模或测试能力。

  • 其次,K 折交叉验证估计器的方差低于单个留出集估计器。这种低方差限制了变异性,如果可用数据量有限,则这一点非常重要。

在这些情况下,正确估计模型预测和相关性能的公平方法是使用交叉验证作为一种强大的通用技术进行模型选择和验证。

机器学习模型选择部分将展示一个更加技术性的例子。让我们举一个具体的例子来说明这一点。假设我们需要对模型调整进行手动特征和参数选择,之后,在整个数据集上进行 10 折交叉验证的模型评估。什么才是最佳策略?我们建议您选择提供乐观分数的策略如下:

  • 将数据集分为训练集(80%)和测试集(20%)或其他比例

  • 使用 K 折交叉验证来调整你的模型

  • 重复交叉验证,直到找到优化的模型,因此调整。

  • 现在使用你的模型在测试集上进行预测,以获得模型外误差的估计

假设检验

假设检验是用于确定结果是否具有统计学意义的统计工具。此外,它还可以用来证明你所得到的结果是偶然发生的还是实际结果。

在这方面,此外,根据 Oracle 开发人员在docs.oracle.com/cd/A57673_01/DOC/server/doc/A48506/method.htm上的说法,某种工作流程将提供更好的性能调整。他们建议的典型步骤如下:

  • 为调整设定明确的目标

  • 创建最小可重复测试

  • 测试假设

  • 保留所有记录

  • 避免常见错误

  • 当达到目标时停止调整

通常,首先计算测试统计量 T 的观察值 tobs。之后,在零假设下计算概率,也称为 p 值。最后,只有当 p 值小于显著性水平(所选概率)阈值时,才会拒绝零假设,支持备择假设。

要了解更多信息,请参考以下出版物:R.A. Fisher 等人,《生物农业和医学研究的统计表》,第 6 版,表 IV,Oliver & Boyd,Ltd.,爱丁堡

以下是两个经验法则(尽管这些可能因数据质量和类型而有所不同):

  • 如果 p 值为 p > 0.05,则接受你的假设。请注意,如果偏差足够小,可能是由于机会而接受水平。例如,0.6 的 p 值意味着有 60%的概率出现任何偏离预期结果的情况。然而,这在可接受的偏差范围内。

  • 如果 p 值为 p < 0.05,则拒绝你的假设,得出结论是除了机会之外还有其他因素在起作用,使得偏差完美。同样,0.01 的 p 值意味着只有 1%的机会是由于机会而产生的偏差,这意味着其他因素必须参与,并且这些因素需要解决。

然而,这两条规则可能并不适用于每个假设检验。在下一小节中,我们将展示使用 Spark MLlib 进行假设检验的示例。

使用 Spark MLlib 的 ChiSqTestResult 进行假设检验

根据 Apache 提供的 API 文档spark.apache.org/docs/latest/mllib-statistics.html#hypothesis-testing,Spark MLlib 的当前实现支持 Pearson 卡方(χ2)拟合度和独立性测试:

  • 拟合度的好坏,或者是否正在进行独立性检验,由输入数据类型决定

  • 拟合度检验需要一个向量类型的输入(主要是密集向量,尽管对于稀疏向量也适用)。另一方面,独立性检验需要一个矩阵作为输入格式。

除此之外,Spark MLlib 还支持输入类型 RDD [LabeledPoint],以通过卡方独立性检验实现特征选择,特别是对于 SVM 或基于回归的测试,其中统计类提供了运行 Pearson 卡方检验所需的方法。

此外,Spark MLlib 提供了Kolmogorov-SmirnovKS)概率分布相等的 1 样本、2 边实现的检验。Spark MLlib 提供了一些在线实现的测试,以支持 A/B 测试等用例。这些测试可以在 Spark Streaming DStream 上执行,其中每个元组的第一个元素表示对照组(false)或处理组(true),第二个元素是观察值的值。

然而,由于简洁和页面限制,这两种测试技术将不会被讨论。以下示例演示了如何通过ChiSqTestResult运行和解释假设检验。在示例中,我们将展示三个测试:对从乳腺癌诊断数据集创建的密集向量的拟合度检验,对随机创建的矩阵的独立性检验,以及对癌症数据集本身的列联表进行独立性检验。

步骤 1:加载所需的包

以下是加载所需包的代码:

import java.io.BufferedReader; 
import java.io.FileReader; 
import java.io.IOException; 
import org.apache.spark.api.java.JavaRDD; 
import org.apache.spark.api.java.function.Function; 
import org.apache.spark.mllib.linalg.DenseVector; 
import org.apache.spark.mllib.linalg.Matrices; 
import org.apache.spark.mllib.linalg.Matrix; 
import org.apache.spark.mllib.linalg.Vector; 
import org.apache.spark.mllib.regression.LabeledPoint; 
import org.apache.spark.mllib.stat.Statistics; 
import org.apache.spark.mllib.stat.test.ChiSqTestResult; 
import org.apache.spark.rdd.RDD; 
import org.apache.spark.sql.SparkSession; 
import com.example.SparkSession.UtilityForSparkSession; 

步骤 2:创建一个 Spark 会话

以下代码帮助我们创建一个 Spark 会话:

static SparkSession spark = UtilityForSparkSession.mySession(); 

UtilityForSparkSession类的实现如下:

public class UtilityForSparkSession { 
  public static SparkSession mySession() { 
  SparkSession spark = SparkSession 
               .builder() 
               .appName("JavaHypothesisTestingOnBreastCancerData ") 
               .master("local[*]") 
              .config("spark.sql.warehouse.dir", "E:/Exp/") 
              .getOrCreate(); 
    return spark; 
  } 
} 

步骤 3:执行拟合度检验

首先,我们需要从类别数据集(如威斯康星州乳腺癌诊断数据集)准备一个密集向量。由于我们已经在这个数据集上提供了许多示例,所以在本节中我们将不再讨论数据探索。以下代码行收集了我们使用myVector()方法创建的向量:

Vector v = myVector(); 

myVector()方法的实现如下:

public static Vector myVector() throws NumberFormatException, IOException {     
BufferedReader br = new BufferedReader(new FileReader(path)); 
    String line = nulNow let's compute the goodness of the fit. Note, if a second vector tol; 
    Vector v = null; 
    while ((line = br.readLine()) != null) { 
      String[] tokens = line.split(","); 
      double[] features = new double[30]; 
      for (int i = 2; i < features.length; i++) { 
        features[i-2] =     
                       Double.parseDouble(tokens[i]); 
      } 
      v = new DenseVector(features); 
    } 
    return v; 
  } 

现在让我们计算拟合度。请注意,如果没有提供第二个要测试的向量作为参数,测试将自动针对均匀分布进行:

ChiSqTestResult goodnessOfFitTestResult = Statistics.chiSqTest(v); 

现在让我们打印拟合度的结果:

System.out.println(goodnessOfFitTestResult + "\n"); 

请注意,测试的摘要包括 p 值、自由度、检验统计量、使用的方法和零假设。我们得到了以下输出:

Chi squared test summary: 
method: pearson 
degrees of freedom = 29  
statistic = 4528.611649568829  
pValue = 0.0  

对于零假设:观察结果遵循与预期相同的分布,存在非常强烈的假设。

由于 p 值低到足够不显著,因此我们不能根据数据接受假设。

步骤 4:对列联表进行独立性检验

首先让我们随机创建一个 4x3 的列联表。在这里,矩阵如下所示:

 ((1.0, 3.0, 5.0, 2.0), (4.0, 6.0, 1.0, 3.5), (6.9, 8.9, 10.5, 12.6)) 
Matrix mat = Matrices.dense(4, 3, new double[] { 1.0, 3.0, 5.0, 2.0, 4.0, 6.0, 1.0, 3.5, 6.9, 8.9, 10.5, 12.6});     

现在让我们对输入的列联表进行 Pearson 独立性检验:

ChiSqTestResult independenceTestResult = Statistics.chiSqTest(mat); 

现在让我们评估测试结果,并给出包括 p 值和自由度在内的测试摘要:

System.out.println(independenceTestResult + "\n"); 

我们得到了以下统计摘要:

Chi squared test summary: 
method: pearson 
degrees of freedom = 6  
statistic = 6.911459343085576  
pValue = 0.3291131185252161  
No presumption against null hypothesis: the occurrence of the outcomes is statistically independent.  

步骤 5:对列联表进行独立性检验

首先,让我们通过 RDDs 从癌症数据集创建一个列联表如下:

static String path = "breastcancer/input/wdbc.data"; 
RDD<String> lines = spark.sparkContext().textFile(path, 2);     
JavaRDD<LabeledPoint> linesRDD = lines.toJavaRDD().map(new Function<String, LabeledPoint>() { 
    public LabeledPoint call(String lines) { 
    String[] tokens = lines.split(","); 
    double[] features = new double[30]; 
    for (int i = 2; i < features.length; i++) { 
    features[i - 2] = Double.parseDouble(tokens[i]); 
            } 
    Vector v = new DenseVector(features); 
    if (tokens[1].equals("B")) { 
    return new LabeledPoint(1.0, v); // benign 
      } else { 
    return new LabeledPoint(0.0, v); // malignant 
        } 
      } 
    }); 

我们已经从原始(特征,标签)对构建了一个列联表,并用它进行了独立性测试。现在让我们对每个特征针对标签进行ChiSquaredTestResult测试:

ChiSqTestResult[] featureTestResults = Statistics.chiSqTest(linesRDD.rdd()); 

现在让我们使用以下代码段观察每列(即每 30 个特征点)的测试结果:

int i = 1; 
for (ChiSqTestResult result : featureTestResults) { 
System.out.println("Column " + i + ":"); 
System.out.println(result + "\n");  
i++; 
} 

Column 1: 
Chi-squared test summary: 
method: Pearson 
degrees of freedom = 455  
statistic = 513.7450859274513  
pValue = 0.02929608473276224  
Strong presumption against null hypothesis: the occurrence of the outcomes is statistically independent. 

Column 2: 
Chi-squared test summary: 
method: Pearson 
degrees of freedom = 478  
statistic = 498.41630331377735  
pValue = 0.2505929829141742  
No presumption against null hypothesis: the occurrence of the outcomes is statistically independent. 

Column 3: 
Chi-squared test summary: 
method: Pearson 
degrees of freedom = 521  
statistic = 553.3147340697276  
pValue = 0.1582572931194156  
No presumption against null hypothesis: the occurrence of the outcomes is statistically independent. 
. 
. 
Column 30: 
Chi-squared test summary: 
method: Pearson 
degrees of freedom = 0  
statistic = 0.0  
pValue = 1.0  
No presumption against null hypothesis: the occurrence of the outcomes is statistically independent.  

从这个结果,我们可以看到对于一些特征点(即列),我们有一个与其他特征点相比较大的 p 值。因此,建议读者在应用超参数调整之前选择适当的数据集并进行假设检验。在这方面没有具体的例子,因为结果可能会根据您拥有的数据集而有所不同。

使用 Spark MLlib 的 Kolmogorov-Smirnov 测试进行假设检验

自 Spark 1.1.0 发布以来,Spark 还提供了使用 Kolmogorov-Smirnov 测试进行实时流数据的假设检验的功能。在这里,获得测试统计结果(至少与实际观察到的结果一样极端)的概率。它实际上假设零假设始终为真。

提示

有关更多详细信息,感兴趣的读者应参考 Spark 分发中的 Java 类(JavaHypothesisTestingKolmogorovSmirnovTestExample.java)在以下目录下:spark-2.0.0-bin-hadoop2.7\examples\src\main\java\org\apache\spark\examples\mllib

Spark MLlib 的流显著性测试

除了 Kolmogorov-Smirnov 测试之外,Spark 还支持流显著性测试,这是假设测试的在线实现,类似于 A/B 测试?这些测试可以在 Spark 流中使用 DStream 进行(关于这个主题的更多技术讨论将在第九章中进行,使用流和图数据进行高级机器学习)。

基于 MLlib 的流显著性测试支持以下两个参数:

  • peacePeriod:这是要忽略的来自流的初始数据点的数量。这实际上用于减轻新颖性效应和您将收到的流的质量。

  • windowSize:这是进行假设检验的过去批次数量。如果将其值设置为 0,它将使用所有先前接收和处理的批次进行累积处理。

感兴趣的读者应参考 Spark API 文档spark.apache.org/docs/latest/mllib-statistics.html#hypothesis-testing

机器学习模型选择

大多数机器学习算法依赖于各种参数。当我们训练模型时,我们需要为这些参数提供值。训练模型的有效性取决于我们选择的模型参数。找到最佳参数集的过程称为模型选择。

通过交叉验证技术进行模型选择

在使用 Python 的 scikit-learn 库或 R 进行机器学习时,通常可以通过使用模型的开箱即用设置获得相当好的预测性能。然而,如果您花一些时间调整模型以适应您的特定问题和数据集,回报可能会很大。

然而,我们还需要考虑其他问题,如过度拟合、交叉验证和偏差-方差权衡。这些想法对于优化算法的超参数至关重要。

在本节中,我们将探讨超参数优化的概念,并演示调整和训练逻辑回归分类器以用于著名的垃圾邮件过滤数据集。目标是调整并应用逻辑回归到这些特征,以预测给定的电子邮件/短信是否为垃圾邮件或非垃圾邮件:

通过交叉验证技术进行模型选择

图 4:通过交叉验证进行模型选择

交叉验证和 Spark

管道通过一次调整整个管道来实现模型选择,而不是分别调整管道中的每个元素。请参阅spark.apache.org/docs/latest/ml-guide.html中的 API 文档。

当前的 Spark ML 实现支持使用CrossValidator类进行模型选择。它接受一个 Estimator、一组ParamMaps和一个 Evaluator。模型选择任务始于拆分数据集(即将其拆分为一组折叠),这些折叠随后被用作单独的训练和测试数据集。

例如,对于 K=10 个折叠,CrossValidator将生成 10 个(训练,测试)数据集对。其中每个使用数据的三分之二(2/3)进行训练,另外三分之一(1/3)进行测试。之后,CrossValidator遍历ParamMaps集合。对于每个ParamMap,它训练给定的 Estimator 并使用可用的 Evaluator 进行评估。Evaluator 可以是相关的 ML 任务,例如:

  • RegressionEvaluator 用于回归相关问题

  • BinaryClassificationEvaluator 用于二进制数据及其相关问题

  • MultiClassClassificationEvaluator 用于多类问题

在选择最佳的ParamMap时,使用默认指标。请注意,ParamMap也可以被这些评估器中的setMetric()方法覆盖。相反,当涉及到最佳模型选择时:

  • ParamMap产生最佳的评估指标

  • 这些评估指标然后在 K 个折叠上进行平均

  • 最后,选择最佳模型

一旦选择了最佳的ParamMap和模型,CrossValidator将使用它们来拟合整个数据集的 Estimator。

为了更清楚地了解CrossValidator并从参数网格中进行选择,Spark 使用ParamGridBuilder实用程序来构建参数网格。例如,假设参数网格中hashingTF.numFeatures的值为 4,LR.regParam的值为 3。还假设CrossValidator使用 10 个折叠。

一旦这些值相乘的结果为 120(即 4310=120),这意味着有大量不同的模型(即 120 个)正在被训练。因此,使用CrossValidator有时可能非常昂贵。然而,与基于启发式手工调整相比,它也是一种选择相关性能和超参数的统计上更可靠的方法。

提示

有兴趣的读者可以参考以下三本书以获得更多见解:

Evan R. Sparks 等人,《大规模机器学习的模型搜索自动化》,ACM,978-1-4503-3651-2/15/08,dx.doi.org/10.1145/2806777.2806945

Cawley, G. C. & Talbot, N. L 关于模型选择中的过度拟合和随后的选择偏差在性能评估中的影响,《机器学习研究杂志》,JMLR.org,2010 年,11,2079-2107。

N. Japkowicz 和 M. Shah,《评估学习算法:分类视角》,剑桥大学出版社,2011 年。

在下一个小节中,我们将展示如何使用 Spark ML API 对数据集进行交叉验证以进行模型选择。

使用 Spark ML 进行垃圾邮件过滤数据集的交叉验证

在这个小节中,我们将向您展示如何对电子邮件垃圾邮件数据集进行交叉验证以进行模型选择。我们将首先使用逻辑回归,然后我们将继续使用其他模型。最后,我们将推荐最适合电子邮件垃圾邮件分类的模型。

第一步:导入必要的包/库/API

以下是导入必要的包/库/API 的代码:

import java.io.Serializable; 
import java.util.Arrays; 
import java.util.logging.Level; 
import java.util.logging.Logger; 
import org.apache.spark.api.java.JavaPairRDD; 
import org.apache.spark.api.java.JavaRDD; 
import org.apache.spark.api.java.function.Function; 
import org.apache.spark.ml.Pipeline; 
import org.apache.spark.ml.PipelineStage; 
import org.apache.spark.ml.classification.LogisticRegression; 
import org.apache.spark.ml.evaluation.BinaryClassificationEvaluator; 
import org.apache.spark.ml.feature.HashingTF; 
import org.apache.spark.ml.feature.Tokenizer; 
import org.apache.spark.ml.param.ParamMap; 
import org.apache.spark.ml.tuning.CrossValidator; 
import org.apache.spark.ml.tuning.CrossValidatorModel; 
import org.apache.spark.ml.tuning.ParamGridBuilder; 
import org.apache.spark.sql.Dataset; 
import org.apache.spark.sql.Row; 
import org.apache.spark.sql.SparkSession; 
import scala.Tuple2; 

第二步:初始化必要的 Spark 环境

以下代码帮助我们初始化必要的 Spark 环境:

  static SparkSession spark = SparkSession 
        .builder() 
        .appName("CrossValidationforSpamFiltering") 
        .master("local[*]") 
        .config("spark.sql.warehouse.dir", "E:/Exp/") 
        .getOrCreate(); 

在这里,我们将应用程序名称设置为交叉验证,主 URL 设置为local[*],Spark 会话设置为程序的入口点。请相应地设置这些参数。最重要的是,将仓库目录设置为E:/Exp/,并将其替换为适当的路径。

步骤 3:从 SMS 垃圾短信数据集准备数据集

以电子邮件垃圾邮件数据作为输入,从数据中准备一个数据集,将其用作原始文本,并通过调用show()方法检查数据是否被正确读取:

Dataset<Row> df = spark.read().text("input/SMSSpamCollection.txt"); 
df.show(); 

使用 Spark ML 进行交叉验证进行垃圾邮件过滤数据集

图 5:前 20 行

要了解更多关于数据的信息,请参考Pipeline - an example with Spark ML部分,在第四章中,通过特征工程提取知识,以及数据集探索的描述。正如您在图 5中所看到的,前 20 行,只有两个标签 - 垃圾邮件或正常邮件(即,这是一个二元分类问题),与文本值(即,每行)相关联。

然而,没有数字标签或 ID。因此,我们需要准备数据集(训练集),使数据框还包含 ID 和标签以及文本(即值),以便我们可以准备测试集,并使用任何分类算法(即逻辑回归)预测相应的标签,并决定我们的模型选择是否合适。

然而,为了做到这一点,我们需要先准备训练数据集。如您所见,上面显示的数据框只有一列,并且如前所述,我们确实需要三列。如果我们从先前的数据集(即df)准备一个 RDD,那么对我们来说将更容易进行转换。

步骤 4:创建用于存储行和索引的 Java RDD 对

通过将 DataFrame(即df)转换为 Java RDD,并通过压缩索引来创建 Java RDD 对:

JavaPairRDD<Row, Long> rowRDD = df.toJavaRDD().zipWithIndex(); 

步骤 5:创建 LabeledDocument RDDs

通过基于两个标签拆分数据集并将文本标签转换为数字标签(即,如果是正常邮件则为1.0,否则为 0.0)来创建LabeledDocument Java RDDs。请注意,LabeledDocument是一个在步骤 6中讨论的用户定义类:

JavaRDD<LabeledDocument> splitedRDD = rowRDD.map(new Function<Tuple2<Row, Long>, LabeledDocument>() { 
@Override 
public LabeledDocument call(Tuple2<Row, Long> v1) throws Exception {   
  Row r = v1._1; 
  long index = v1._2; 
  String[] split = r.getString(0).split("\t"); 
  if(split[0].equals("ham")) 
    return new LabeledDocument(index,split[1], 1.0); 
  else 
    return new LabeledDocument(index,split[1], 0.0); 
      } 
    });  

步骤 6:准备训练数据集

使用createDataFrame()方法从LabeledDocument RDDs 准备训练数据集,并指定类。最后,使用show()方法查看数据框结构,如下所示:

Dataset<Row> training = spark.createDataFrame(splitedRDD, LabeledDocument.class); 
training.show(false); 

使用 Spark ML 进行交叉验证进行垃圾邮件过滤数据集

图 6:从数据集中新创建的标签和 ID;其中 ID 是行数。

图 6中,从数据集中新创建的标签和 ID;其中 ID 是行数,我们可以看到新的训练数据集有三列:ID、测试和标签。实际上,可以通过在原始文档的每一行(即每一行)添加一个新的 ID 来完成。让我们为此创建一个名为Document的类,它应该为每行文本设置一个唯一的 ID。类的结构可以是以下内容:

public class Document implements Serializable { 
  private long id; 
  private String text; 
  //Initialise the constructor that should take two parameters: id and text// 
  Set the id 
  Get the id 
  Set the text 
  Get the text   
  } 

现在让我们来看一下类构造函数的结构:

  public LabeledDocument (long id, String text) {   
      this.id = id; 
           this.text = text; 
  } 

ID 的 setter 和 getter 可以是这样的:

  public void setId(long id) { 
    this.id = id; 
  } 
  public long getId() { 
    return this.id; 
  } 

类似地,文本的 setter 和 getter 方法可能是这样的:

  public String getText() { 
    return this.text; 
  } 

  public void setText(String text) { 
    this.text = text; 
  } 

因此,如果我们总结一下,Document类可能是这样的:

import java.io.Serializable; 
public class Document implements Serializable { 
  private long id; 
  private String text; 
  public Document(long id, String text) { 
    this.id = id; 
    this.text = text; 
  } 
  public long getId() { 
    return this.id; 
  } 
  public void setId(long id) { 
    this.id = id; 
  } 
  public String getText() { 
    return this.text; 
  } 
  public void setText(String text) { 
    this.text = text; 
  } 
} 

另一方面,LabeledDocument类的结构可能如下所示,并且可以从Document类扩展(稍后讨论):

现在让我们来看一下类构造函数的结构:

  public LabeledDocument(long id, String text, double label) {   
    this.label = label; 
  } 

然而,我们还没有完成,因为我们将扩展Document类,我们需要使用super()方法从Document类继承构造函数,如下所示:

  public LabeledDocument(long id, String text, double label) { 
    super(id, text); 
    this.label = label; 
  }  

现在 setter 方法可以是这样的:

  public void setLabel(double label) { 
    this.label = label; 
  } 

当然,标签的 getter 方法可能是这样的:

  public double getLabel() { 
    return this.label; 
  } 

因此,简而言之,LabelDocument类如下:

import java.io.Serializable; 
public class LabeledDocument extends Document implements Serializable { 
  private double label; 
  public LabeledDocument(long id, String text, double label) { 
    super(id, text); 
    this.label = label; 
  } 
  public double getLabel() { 
    return this.label; 
  } 
  public void setLabel(double label) { 
    this.label = label; 
  } 
} 

第 7 步:配置 ML 管道

配置一个 ML 管道,包括三个阶段:tokenizerhashingTFlr

Tokenizer tokenizer = new Tokenizer().setInputCol("text").setOutputCol("words"); 
HashingTF hashingTF = new HashingTF().setNumFeatures(1000).setInputCol(tokenizer.getOutputCol()).setOutputCol("features"); 
LogisticRegression lr = new LogisticRegression().setMaxIter(10).setRegParam(0.01); 
Pipeline pipeline = new Pipeline().setStages(new PipelineStage[] { tokenizer, hashingTF, lr }); 

第 8 步:构建要搜索的参数网格

目前,Spark 使用ParamGridBuilder来构建要搜索的参数网格。在这方面,假设我们有hashingTF.numFeatures的三个值和lr.regParam的两个值,那么这个网格将有 3 x 2 = 6 个参数设置供CrossValidator选择:

ParamMap[] paramGrid = new ParamGridBuilder() 
.addGrid(hashingTF.numFeatures(), new int[] { 10, 100, 1000 }).addGrid(lr.regParam(), new double[] { 0.1, 0.01 }) 
 .build(); 

我们现在将管道视为一个估计器,并将其包装在CrossValidator实例中。这将允许我们共同为所有管道阶段选择参数。CrossValidator需要一个估计器,一组估计器ParamMaps和一个评估器。请注意,这里的评估器是BinaryClassificationEvaluator,其默认指标是areaUnderROC

第 9 步:创建一个 CrossValidator 实例

以下是创建CrossValidator实例的代码:

    CrossValidator cv = new CrossValidator() 
        .setEstimator(pipeline) 
        .setEvaluator(new BinaryClassificationEvaluator())                  
        .setEstimatorParamMaps(paramGrid) 
        .setNumFolds(5); // 5-fold cross validation 

第 10 步:运行交叉验证

运行交叉验证并选择最佳参数集。只需使用以下代码段:

CrossValidatorModel cvModel = cv.fit(training); 

现在您的CrossValidator模型已准备好执行预测。但在此之前,我们需要一个测试集或验证集。现在让我们准备一个样本测试集。只需使用以下代码段创建数据集:

    Dataset<Row> test = spark.createDataFrame(Arrays.asList( 
      new Document(4L, "FreeMsg CALL j k"),  
      new Document(5L, "Siva  hostel"), 
      new Document(6L, "darren now"),  
     new Document(7L, "Sunshine Quiz! Win a super Sony")),Document.class); 

现在让我们通过调用图 7中的show()方法来查看测试集的结构:

使用 Spark ML 进行交叉验证进行 SPAM 过滤数据集

图 7:测试集

第 11 步:创建一个数据集来收集预测参数

以下代码说明了如何创建数据集:

Dataset<Row> predictions = cvModel.transform(test); 

第 12 步:显示测试集中每个文本的预测参数

借助以下代码,我们可以显示预测参数:

for (Row r : predictions.select("id", "text", "probability", "prediction").collect()) 
    { 
System.out.println("(" + r.get(0) + ", " + r.get(1) + ") --> prob=" + r.get(2) + ", prediction=" + r.get(3)); 
    }  

使用 Spark ML 进行交叉验证进行 SPAM 过滤数据集

图 8:针对每个文本和 ID 的预测

因此,如果您比较图 6中显示的结果,“从数据集中新创建的标签和 ID;其中 ID 是行数”,您会发现预测准确性从更复杂的方法中增加,用于测量预测准确性的方法可以用于识别可以根据每种错误类型的成本来优化错误率的地方。

通过训练验证拆分进行模型选择

根据 Spark 在spark.apache.org/docs/latest/ml-guide.html提供的 API 文档,Spark 还为超参数调整提供了TrainValidationSplit,以及CrossValidatorTrainValidationSplit的想法是它只评估参数的每个组合,而不是像交叉验证那样迭代 k 次。因此,它在计算上更便宜,产生结果更快。然而,结果可能不像CrossValidator那样可靠。有一个例外:如果训练数据集足够大,那么它也可以产生可靠的结果。

TrainValidationSplit背后的理论是它将以下三个作为输入:

  • 一个估计器

  • estimatorParamMaps参数中提供的一组ParamMap

  • 一个评估器

因此,它通过使用trainRatio参数将模型选择分为两部分开始。另一方面,trainRatio参数用于单独的训练和测试数据集。

例如,对于trainRatio = 0.75(默认值也是 0.75),TrainValidationSplit算法生成一个训练和测试对。在这种情况下,总数据的 75%用于训练模型。因此,其余的 25%用作验证集。

CrossValidator类似,TrainValidationSplit也会遍历一组 ParamMaps,如前所述。对于参数的每种组合,它在每次迭代中训练给定的估计器。

因此,使用给定的评估器评估模型。之后,最佳模型被选为最佳选项,因为ParamMap产生了最佳的评估指标,从而简化了模型选择。TrainValidationSplit最终使用最佳的ParamMap拟合估计器,并用于整个数据集。

基于线性回归的 OCR 数据集模型选择

在本小节中,我们将展示如何为 OCR 数据执行训练验证分割调整。首先将使用逻辑回归,然后我们将继续使用其他模型。最后,我们将为 OCR 数据分类推荐最合适的参数。

步骤 1:导入必要的包/库/API:

import org.apache.spark.ml.evaluation.MulticlassClassificationEvaluator; 
import org.apache.spark.ml.param.ParamMap; 
import org.apache.spark.ml.regression.LinearRegression; 
import org.apache.spark.ml.tuning.ParamGridBuilder; 
import org.apache.spark.ml.tuning.TrainValidationSplit; 
import org.apache.spark.ml.tuning.TrainValidationSplitModel; 
import org.apache.spark.sql.Dataset; 
import org.apache.spark.sql.Row; 
import org.apache.spark.sql.SparkSession;

步骤 2:初始化必要的 Spark 环境

  SparkSession spark = SparkSession 
          .builder() 
          .appName("TrainSplitOCR") 
              .master("local[*]") 
               .config("spark.sql.warehouse.dir",  
                 "E:/Exp/") 
              .getOrCreate(); 

在这里,我们将应用名称设置为TrainValidationSplit,主 URL 设置为local[*],Spark 上下文是程序的入口点。请相应地设置这些参数。

步骤 3:将 OCR 数据准备为 libsvm 格式

如果您回忆一下第六章中的图 19构建可扩展的机器学习管道,您将记得数据如下所示图 9原始 OCR 数据集的数据框快照

基于线性回归的 OCR 数据集模型选择

图 9:原始 OCR 数据集的数据框快照

然而,TrainValidationSplitModel API 的当前实现仅适用于已经处于libsvm格式的数据集。

提示

感兴趣的读者应参考以下研究文章以获取更深入的知识:Chih-Chung Chang 和 Chih-Jen Lin,LIBSVM - 支持向量机库。ACM 智能系统和技术交易,2:27:1--27:27,2011 年。该软件可在www.csie.ntu.edu.tw/~cjlin/libsvm上获得。

因此,我们确实需要将数据集从当前的制表符分隔的 OCR 数据转换为libsvm格式。

提示

读者应该使用 Packt 软件包提供的数据集,或者可以将 CSV/CSV 文件转换为相应的libsvm格式。感兴趣的读者可以参考我们在 GitHub 上提供的公共脚本github.com/rezacsedu/CSVtoLibSVMConverterinR,该脚本可以直接将 CSV 文件转换为libsvm格式。只需正确显示输入和输出文件路径,并在 RStudio 上运行脚本。

步骤 4:准备 OCR 数据集,并准备训练和测试集

我们假设读者已经下载了数据或使用我们的 GitHub 脚本或使用自己的脚本转换了 OCR 数据。现在,将 OCR libsvm格式数据作为输入,并准备数据集作为原始文本,并通过调用show()方法检查数据是否被正确读取,如下所示:

Dataset<Row> data = spark.read().format("libsvm").load("input/Letterdata_libsvm.data"); 
data.show(false); 

基于线性回归的 OCR 数据集模型选择

图 10:前 20 行

// Prepare training and test data. 
Dataset<Row>[] splits = data.randomSplit(new double[] {0.9, 0.1}, 12345); 
Dataset<Row> training = splits[0]; 
Dataset<Row> test = splits[1]; 

要了解更多关于数据的信息,请参考章节Pipeline - An Example with Spark ML,在第四章中,通过特征工程提取知识,以及数据集探索的描述。正如您在图 2中所看到的,使用逻辑回归估计器的 Spark ML 管道模型(虚线仅在管道拟合期间发生),只有两个标签(垃圾邮件或正常邮件)与文本值(即每行)相关联。然而,没有数字标签或 ID。

因此,我们需要准备数据集(训练集),使得数据集还包含 ID 和标签以及文本(即值),以便我们可以准备一个测试集,并对任何分类算法(即逻辑回归)预测其相应的标签,并决定我们的模型选择是否合适。

但是,为了做到这一点,我们首先需要准备训练数据集。正如您所看到的,上面显示的数据框只有一列,正如之前提到的,我们确实需要三列。如果我们从上述数据集(即df)准备一个 RDD,那么我们更容易进行转换。

步骤 5:使用线性回归配置 ML 管道

LinearRegression lr = new LinearRegression(); 

步骤 6:构建要搜索的参数网格

目前,Spark 使用ParamGridBuilder来构建要搜索的参数网格。在这方面,对于hashingTF.numFeatures有三个值,对于lr.regParam有两个值,因此这个网格将有 3 x 2 = 6 个参数设置供CrossValidator选择:

ParamMap[] paramGrid = new ParamGridBuilder() 
.addGrid(hashingTF.numFeatures(), new int[] { 10, 100, 1000 }).addGrid(lr.regParam(), new double[] { 0.1, 0.01 }) 
.build(); 

我们现在将管道视为一个估计器,并将其包装在CrossValidator实例中。这将允许我们共同选择所有管道阶段的参数。正如已经讨论的,CrossValidator需要一个估计器,一组估计器ParamMaps和一个评估器。

注意

请注意,这里的评估器是BinaryClassificationEvaluator,其默认指标是areaUnderROC

步骤 7:创建 TrainValidationSplit 实例:

TrainValidationSplit trainValidationSplit = new TrainValidationSplit() 
    .setEstimator(lr) 
    .setEvaluator(new MulticlassClassificationEvaluator()) 
    .setEstimatorParamMaps(paramGrid) 
    .setTrainRatio(0.7); 

在这种情况下,估计器只是我们在步骤 4中创建的线性回归。TrainValidationSplit需要一个估计器,一组估计器ParamMaps和一个评估器。在这种情况下,70%的数据将用于训练,剩下的 30%用于验证。

步骤 8:运行 TrainValidationSplit,并选择参数

运行TrainValidationSplit并使用训练集选择最佳参数集。只需使用以下代码段:

TrainValidationSplitModel model = trainValidationSplit.fit(training); 

步骤 9:对测试集进行预测

对测试数据进行预测,其中模型是表现最佳的参数组合的模型。最后,要显示预测,请使用以下代码段:

Dataset<Row> per_param = model.transform(test); 
per_param.show(false);   

基于 OCR 数据集的线性回归模型选择

图 11:对每个特征和标签的预测

图 11中,我们展示了行预测与实际标签的对比。第一列是实际标签,第二列表示特征向量,第三列显示基于TrainValidationSplitModel创建的特征向量的原始预测。

基于逻辑回归的癌症数据集模型选择

在这个子部分中,我们将展示如何对 OCR 数据执行训练验证分割调整。我们将首先使用逻辑回归;然后我们将继续其他模型。最后,我们将为 OCR 数据分类推荐最合适的参数。

步骤 1:导入必要的包/库/API

import org.apache.spark.api.java.JavaRDD; 
import org.apache.spark.api.java.function.Function; 
import org.apache.spark.ml.classification.LogisticRegression; 
import org.apache.spark.ml.evaluation.RegressionEvaluator; 
import org.apache.spark.ml.feature.LabeledPoint; 
import org.apache.spark.ml.linalg.DenseVector; 
import org.apache.spark.ml.linalg.Vector; 
import org.apache.spark.ml.param.ParamMap; 
import org.apache.spark.ml.tuning.ParamGridBuilder; 
import org.apache.spark.ml.tuning.TrainValidationSplit; 
import org.apache.spark.ml.tuning.TrainValidationSplitModel; 
import org.apache.spark.rdd.RDD; 
import org.apache.spark.sql.Dataset; 
import org.apache.spark.sql.Row; 
import org.apache.spark.sql.SparkSession; 

步骤 2:初始化必要的 Spark 环境

static SparkSession spark = SparkSession 
  .builder() 
  .appName("CrossValidationforSpamFiltering") 
  .master("local[*]") 
  .config("spark.sql.warehouse.dir", "C:/Exp/"). 
  getOrCreate(); 

在这里,我们将应用名称设置为CancerDiagnosis,主 URL 设置为local[*],Spark 上下文设置为程序的入口点。请相应地设置这些参数。

步骤 3:创建 Java RDD

解析癌症诊断数据并为字符串准备 Java RDD:

String path = "breastcancer/input/wdbc.data"; 
RDD<String> lines = spark.sparkContext().textFile(path, 3); 

步骤 4:准备癌症诊断 LabeledPoint RDD

正如在第六章中已经讨论的,癌症诊断数据集包含良性和恶性的两个标签BM。但是,我们需要将它们转换为数字标签。只需使用以下代码将它们全部从标签转换为LabeledPoint RDDs 准备:

JavaRDD<LabeledPoint> linesRDD = lines.toJavaRDD().map(new Function<String, LabeledPoint>() { 
      public LabeledPoint call(String lines) { 
      String[] tokens = lines.split(","); 
      double[] features = new double[30]; 
      for (int i = 2; i < features.length; i++) { 
          features[i - 2] =             
                 Double.parseDouble(tokens[i]); 
        } 
           Vector v = new DenseVector(features); 
           if (tokens[1].equals("B")) { 
      return new LabeledPoint(1.0, v); // benign 
    } else { 
    return new LabeledPoint(0.0, v); // malignant 
    } 
      } 
    }); 

基于癌症数据集的逻辑回归模型选择

图 12:标签点 RDDs 快照

正如您在图 9中所看到的,前 20 行,标签 B 和 M 已经转换为 1.0 和 0.0。现在我们需要从标签点 RDDs 创建一个数据框。

步骤 5:创建数据集并准备训练和测试集

通过指定标签点类从先前的 RDDs(即linesRDD)创建一个数据集:

Dataset<Row> data = spark.sqlContext().createDataFrame(linesRDD, LabeledPoint.class); 
data.show(); 

基于癌症数据集的逻辑回归模型选择

图 13:显示前 20 行的创建的数据集。

Dataset<Row>[] splits=data.randomSplit(new double[] {0.8, 0.2}); 
Dataset<Row> training = splits[0]; 
Dataset<Row> test = splits[1];

请注意,您将不得不根据您的数据和问题类型设置随机拆分的比率。

步骤 6:使用逻辑回归配置 ML 管道:

LogisticRegression lr = new LogisticRegression(); 

步骤 7:构建要搜索的参数网格

目前,Spark 使用ParamGridBuilder来构建要搜索的参数网格。在这方面,假设我们有三个值用于hashingTF.numFeatures和两个值用于lr.regParam,这个网格将有 3 x 2 = 6 个参数设置供CrossValidator选择:

ParamMap[] paramGrid = new ParamGridBuilder() 
.addGrid(lr.regParam(), new double[] {0.1, 0.01}) 
.addGrid(lr.fitIntercept()) 
.addGrid(lr.elasticNetParam(), new double[] {0.0, 0.5, 1.0}) 
.build();

请注意,您将不得不根据您的数据和问题类型设置上述参数的值。

步骤 8:创建一个 TrainValidationSplit 实例:

TrainValidationSplit trainValidationSplit = new TrainValidationSplit() 
.setEstimator(lr) 
.setEvaluator(new RegressionEvaluator()) 
.setEstimatorParamMaps(paramGrid) 
.setTrainRatio(0.8); 

在这种情况下,估计器只是我们在步骤 4中创建的线性回归。准备癌症诊断LabeledPoint RDDs。一个TrainValidationSplit需要一个估计器,一组估计器ParamMaps,以及一个支持二元分类的评估器,因为我们的数据集只有两个类,其中 80%用于训练,剩下的 20%用于验证。

步骤 9:运行 TrainValidationSplit 并选择参数

运行TrainValidationSplit,并使用训练集选择问题的最佳参数集。只需使用以下代码段:

TrainValidationSplitModel model = trainValidationSplit.fit(training); 

步骤 10:对测试集进行预测

对测试数据进行预测,其中模型是表现最佳的参数组合的模型。最后,显示预测结果。只需使用以下代码段:

Dataset<Row> per_param = model.transform(test); 
per_param.show(); 

基于癌症数据集的逻辑回归模型选择

图 14:针对每个特征和标签的预测

因此,如果您将这些结果与图 6中显示的结果进行比较,从数据集中新创建的标签和 ID;其中 ID 是行数,您会发现预测准确性增加了,用于识别可以优化错误率的更复杂的预测准确性测量方法,这些方法可以根据每种错误的成本来确定可以使用的地方。

总结

调整算法或机器学习应用可以被认为是一个简单的过程,通过这个过程,人们优化影响模型的参数,以使算法在运行时间和内存使用方面表现最佳。在本章中,我们展示了如何使用 Spark ML 的训练验证拆分和交叉验证技术来进行 ML 模型调优。

我们还想提到,直到 2016 年 10 月 14 日的当前 Spark 发布日期,调整相关的支持和算法仍然不够丰富。鼓励感兴趣的读者访问 Spark 调整页面spark.apache.org/docs/latest/ml-tuning.html以获取更多更新,因为我们相信 Spark 网站将添加更多功能,并且他们肯定会提供足够的文档。

在下一章中,我们将讨论如何使您的机器学习算法或模型适应新的数据集。本章涵盖了高级机器学习技术,以使算法能够适应新数据。它将主要关注批处理/流式架构和使用 Spark 流的在线学习算法。

最终目标是为静态机器学习模型带来活力。读者还将看到机器学习算法如何逐渐学习数据;也就是说,模型在看到新的训练实例时会进行更新。

第八章:调整您的机器学习模型

本章涵盖了高级机器学习ML)技术,以便能够使算法适应新数据。读者还将看到机器学习算法如何逐渐学习数据,也就是说,每次看到新的训练实例时,模型都会更新。还将讨论在动态环境中通过让步不同的约束来学习。总之,本章将涵盖以下主题:

  • 适应机器学习模型

  • ML 模型的泛化

  • 通过增量算法进行适应

  • 通过重用 ML 模型进行适应

  • 动态环境中的机器学习

机器学习模型的适应

正如我们之前讨论的,作为 ML 训练过程的一部分,模型是使用一组数据进行训练的(即训练、测试和验证集)。能够适应其环境并从经验中学习的机器学习模型吸引了来自不同领域的消费者和研究人员,包括计算机科学、工程、数学、物理学、神经科学和认知科学。在本节中,我们将提供如何为新数据和需求采用机器学习模型的技术概述。

技术概述

从技术上讲,如果需要,同样的模型可能需要在以后的阶段进行重新训练以改进。这实际上取决于几个因素,例如新数据何时可用,或者 API 的使用者是否有自己的数据来训练模型,或者数据是否需要被过滤并且模型需要用数据子集进行训练。在这些情景中,ML 算法应该提供足够的 API,以便为其消费者提供方便的方式,使他们能够一次或定期地使用客户端,以便他们可以使用自己的数据重新训练模型。

因此,客户将能够评估重新训练和更新 Web 服务 API 的结果。或者,他们将能够使用新训练的模型。在这方面,域自适应有几种不同的情境。但是,它们在考虑应用类型和需求方面有所不同:

  • 无监督域自适应:学习样本包含一组标记的源示例、一组未标记的源示例和一组未标记的目标示例

  • 半监督域自适应:在这种情况下,我们还考虑了一小部分标记的目标示例

  • 监督域自适应:所有考虑的示例都应该被标记:

技术概述

图 1:重新训练过程概述(虚线表示重新训练步骤)

从技术上讲,制作 ML 模型适应的方法应该有三种选择:

  • 最广泛使用的机器学习技术和算法包括决策树、决策规则、神经网络、统计分类器和概率图模型,它们都需要进行开发,以便能够适应新的需求

  • 其次,先前提到的算法或技术应该被泛化,以便能够以最小的努力使用

  • 此外,还需要开发更加健壮的理论框架和算法,如贝叶斯学习理论、经典统计理论、最小描述长度理论和统计力学方法,以便理解计算学习理论

这三种适应性属性和技术的好处将为实验结果提供见解,也将指导机器学习社区为不同的学习算法做出贡献。

ML 模型的泛化

在第五章中,通过示例进行监督和无监督学习,我们讨论了如何以及为什么将学习算法推广到适应半监督学习、主动学习、结构化预测和强化学习。在本节中,我们将讨论如何将线性回归算法推广到光学字符识别(OCR)数据集上,以展示线性回归模型的推广示例。

广义线性回归

如在第五章中讨论的,通过示例进行监督和无监督学习,线性回归和逻辑回归技术假设输出遵循高斯分布。另一方面,广义线性模型(GLMs)是线性模型的规范,其中响应变量 Yi 遵循来自分布的指数家族的线性分布。

Spark 的GeneralizedLinearRegression API 允许我们灵活地指定 GLMs。Spark 中广义线性回归的当前实现可用于多种类型的预测问题,例如线性回归、泊松回归、逻辑回归等。

然而,当前实现的 Spark GLM 算法仅支持指数家族分布的子集。此外,还存在另一个可伸缩性问题,即其GeneralizedLinearRegression API 仅支持 4096 个特征。因此,如果特征数量超过 4096,算法将抛出异常。

幸运的是,可以使用 LinearRegression 和 LogisticRegression 估计器训练具有增加特征数量的模型,正如在第六章中的几个示例中所示,构建可扩展的机器学习管道

使用 Spark 进行广义线性回归

在本小节中,我们将讨论一个逐步示例,展示如何在我们在第七章中讨论的光学字符识别(OCR)数据的libsvm版本上应用广义线性回归。由于这里将重复使用相同的数据集,我们决定不再详细描述它们。

步骤 1:加载必要的 API 和软件包

以下是加载必要 API 和软件包的代码:

import java.util.Arrays; 
import org.apache.spark.ml.regression.GeneralizedLinearRegression; 
import org.apache.spark.ml.regression.GeneralizedLinearRegressionModel; 
import org.apache.spark.ml.regression.GeneralizedLinearRegressionTrainingSummary; 
import org.apache.spark.sql.Dataset; 
import org.apache.spark.sql.Row; 
import org.apache.spark.sql.SparkSession; 

步骤 2:创建 Spark 会话

以下代码显示了如何创建 Spark 会话:

SparkSession spark = SparkSession 
    .builder() 
    .appName("JavaGeneralizedLinearRegressionExample") 
    .master("local[*]") 
    .config("spark.sql.warehouse.dir", "E:/Exp/") 
    .getOrCreate();  

步骤 3:加载和创建数据集

从 OCR 数据集加载和创建数据集。在这里,我们指定了数据集格式为libsvm

Dataset<Row>dataset = spark.read().format("libsvm").load("input/Letterdata_libsvm.data"); 

步骤 4:准备训练和测试集

以下代码说明了如何准备训练和测试集:

double[] weights = {0.8, 0.2}; 
long seed = 12345L; 
Dataset<Row>[] split = dataset.randomSplit(weights, seed); 
Dataset<Row> training = split[0]; 
Dataset<Row> test = split[1]; 

步骤 5:创建广义线性回归估计器

通过指定家族、链接和最大迭代和回归参数来创建广义线性回归估计器。在这里,我们选择了家族为“高斯”和链接为“身份”:

GeneralizedLinearRegression glr = new GeneralizedLinearRegression() 
.setFamily("gaussian") 
.setLink("identity") 
.setMaxIter(10) 
.setRegParam(0.3); 

请注意,根据 Spark 的 API 文档spark.apache.org/docs/latest/ml-classification-regression.html#generalized-linear-regression,此算法实现与 Spark 支持以下选项:

使用 Spark 进行广义线性回归

图 2:当前实现的广义线性回归支持的家族

步骤 6:拟合模型

以下是拟合模型的代码:

GeneralizedLinearRegressionModel model = glr.fit(training); 

步骤 7:检查系数和截距

打印我们在第 6 步创建的线性回归模型的系数和截距:

System.out.println("Coefficients: " + model.coefficients()); 
System.out.println("Intercept: " + model.intercept()); 

这两个参数的输出将类似于以下内容:

Coefficients: [-0.0022864381796305487,-0.002728958263362158,0.001582003618682323,-0.0027708788253722914,0.0021962329827476565,-0.014769839282003813,0.027752802299957722,0.005757124632688538,0.013869444611365267,-0.010555326094498824,-0.006062727351948948,-0.01618167221020619,0.02894330366681715,-0.006180003317929849,-0.0025768386348180294,0.015161831324693125,0.8125261496082304] 
Intercept: 1.2140016821111255  

需要注意的是System.out.println方法在集群模式下不起作用。这只在独立模式或伪模式下起作用。这仅用于验证结果。

步骤 8:总结模型

总结训练集上的模型并打印出一些指标:

GeneralizedLinearRegressionTrainingSummary summary = model.summary(); 

步骤 9:验证一些广义指标

让我们打印一些广义指标,如系数标准误差CSE)、T 值、P 值、离散度、零偏差、零残差自由度、AIC 和偏差残差。由于页面限制,我们没有展示这些值的重要性或计算过程:

System.out.println("Coefficient Standard Errors: " 
      + Arrays.toString(summary.coefficientStandardErrors())); 
System.out.println("T Values: " + Arrays.toString(summary.tValues())); 
System.out.println("P Values: " + Arrays.toString(summary.pValues())); 
System.out.println("Dispersion: " + summary.dispersion()); 
System.out.println("Null Deviance: " + summary.nullDeviance()); 
System.out.println("Residual Degree Of Freedom Null: " + summary.residualDegreeOfFreedomNull()); 
System.out.println("Deviance: " + summary.deviance()); 
System.out.println("Residual Degree Of Freedom: " + summary.residualDegreeOfFreedom()); 
    System.out.println("AIC: " + summary.aic()); 

让我们看看我们之前创建的训练集的值:

Coefficient Standard Errors:[2.877963555951775E-4, 0.0016618949921257992, 9.147115254397696E-4, 0.001633197607413805, 0.0013194682048354774, 0.001427648472211677, 0.0010797461071614422, 0.001092731825368789, 7.922778963434026E-4, 9.413717346009722E-4, 8.746375698587989E-4, 9.768068714323967E-4, 0.0010276211138097238, 0.0011457739746946476, 0.0015025626835648176, 9.048329671989396E-4, 0.0013145697411570455, 0.02274018067510297] 
T Values:[-7.944639100457261, -1.6420762300218703, 1.729510971146599, -1.6965974067032972, 1.6644834446931607, -10.345571455081481, 25.703081600282317, 5.2685613240426585, 17.50578259898057, -11.212707697212734, -6.931702411237277, -16.56588695621814, 28.165345454527458, -5.3937368577226055, -1.714962485760994, 16.756497468951743, 618.0928437414578, 53.385753589911985] 
P Values:[1.9984014443252818E-15, 0.10059394323065063, 0.08373705354670546, 0.0897923347927514, 0.09603552109755675, 0.0, 0.0, 1.3928712139232857E-7, 0.0, 0.0, 4.317657342767234E-12, 0.0, 0.0, 6.999167956323049E-8, 0.08637155105770145, 0.0, 0.0, 0.0] 
Dispersion: 0.07102433332236015  
Null Deviance: 41357.85510971454 
Residual Degree Of Freedom Null: 15949 
Deviance: 1131.5596784918419 
Residual Degree Of Freedom: 15932 
AIC: 3100.6418768238423  

步骤 10:显示偏差残差

以下代码用于显示偏差残差:

summary.residuals().show(); 

使用 Spark 进行广义线性回归

图 3:OCR 数据集的偏差残差总结

提示

有兴趣的读者应参考以下网页,以获取有关该算法及其实现细节的更多信息和见解:spark.apache.org/docs/latest/ml-classification-regression.html

通过增量算法进行适应

根据 Robi Polikar 等人的说法(Learn++: An Incremental Learning Algorithm for Supervised Neural Networks, IEEE Transactions on Systems, Man, And Cybernetics, V-21, No-4, November 2001),已经提出了各种算法用于增量学习。因此,增量学习被暗示用于解决不同的问题。在一些文献中,增量学习一词被用来指代分类器的生长或修剪。或者,它可能指的是以增量方式选择最具信息量的训练样本来解决问题。

在其他情况下,使常规的 ML 算法增量意味着通过对分类器中的权重进行一定形式的受控修改,通过对错误分类的信号进行重新训练。一些算法能够学习新信息;然而,它们并不同步满足先前提到的所有标准。此外,它们要么需要访问旧数据,要么需要在途中忘记先前的知识,由于它们无法适应新类别,因此对新数据集不具有适应性。

考虑到先前提到的问题,在本节中,我们将讨论如何使用原始算法的增量版本来采用 ML 模型。我们将简要讨论增量 SVM、贝叶斯网络和神经网络。此外,如果适用,我们将提供这些算法的常规 Spark 实现。

增量支持向量机

使常规 ML 算法增量是相当困难的。简而言之,这是可能的,但并不十分容易。如果您想要这样做,您必须更改您正在使用的 Spark 库的底层源代码,或者自己实现训练算法。

不幸的是,Spark 没有实现增量版本的 SVM。然而,在使线性 SVM 增量之前,您需要先了解线性 SVM 本身。因此,我们将在下一个子节中使用 Spark 为新数据集提供一些线性 SVM 的概念。

提示

据我们所知,我们发现了只有两种可能的解决方案,称为 SVMHeavy (people.eng.unimelb.edu.au/shiltona/svm/) 和 LaSVM (leon.bottou.org/projects/lasvm),它们支持增量训练。但我们没有使用任何一种。有兴趣的读者应该阅读这两篇关于增量 SVM 的论文以获取一些见解。这两篇论文都很简单,展示了很好的研究,如果您刚开始学习的话:

cbcl.mit.edu/cbcl/publications/ps/cauwenberghs-nips00.pdf

www.jmlr.org/papers/volume7/laskov06a/laskov06a.pdf

使用 Spark 适应新数据的 SVM

在本节中,我们将首先讨论如何使用 Spark 实现的线性 SVM 进行二元分类。然后我们将展示如何将相同的算法应用于新数据类型。

步骤 1:数据收集和探索

我们从www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/binary.html收集了一个结肠癌数据集。最初,数据集的标签为-1.0 和 1.0 如下:

使用 Spark 适应新数据的 SVM

图 4:原始结肠癌数据快照

提示

该数据集在以下出版物中使用:U. Alon, N. Barkai, D. A. Notterman, K. Gish, S.Ybarra, D.Mack, and A. J. Levine. Broad patterns of gene expression revealed by clustering analysis of tumour and normal colon tissues probed by oligonucleotide arrays. Cell Biology, 96:6745-6750, 1999。感兴趣的读者应参考该出版物以获取有关数据集的更多见解。

之后,进行实例归一化以使均值为零,方差为一。然后进行特征归一化以获得零均值和方差为一作为预处理步骤。然而,为简单起见,我们将-1.0 视为 0.1,因为 SVM 不识别符号(即+或-)。因此,数据集现在包含两个标签 1 和 0(即,这是一个二元分类问题)。经过预处理和缩放后,有两个类别和 2000 个特征。以下是数据集的示例图 5

使用 Spark 适应新数据的 SVM

图 5:预处理的结肠癌数据

步骤 2:加载必要的软件包和 API

以下是加载必要软件包的代码:

import org.apache.spark.api.java.JavaRDD; 
import org.apache.spark.api.java.function.Function; 
import org.apache.spark.mllib.classification.SVMModel; 
import org.apache.spark.mllib.classification.SVMWithSGD; 
import org.apache.spark.mllib.evaluation.BinaryClassificationMetrics; 
import org.apache.spark.mllib.evaluation.MulticlassMetrics; 
import org.apache.spark.mllib.optimization.L1Updater; 
import org.apache.spark.mllib.regression.LabeledPoint; 
import org.apache.spark.mllib.util.MLUtils; 
import org.apache.spark.sql.SparkSession; 

步骤 3:配置 Spark 会话

以下代码帮助我们创建 Spark 会话:

SparkSession spark = SparkSession 
    .builder() 
    .appName("JavaLDAExample") 
    .master("local[*]") 
    .config("spark.sql.warehouse.dir", "E:/Exp/") 
    .getOrCreate(); 

步骤 4:从数据中创建数据集

以下是创建数据集的代码:

String path = "input/colon-cancer.data"; 
JavaRDD<LabeledPoint>data = MLUtils.loadLibSVMFile(spark.sparkContext(), path).toJavaRDD(); 

步骤 5:准备训练和测试集

以下是准备训练和测试集的代码:

    JavaRDD<LabeledPoint>training = data.sample(false, 0.8, 11L); 
training.cache(); 
    JavaRDD<LabeledPoint>test = data.subtract(training); 

步骤 6:构建和训练 SVM 模型

以下代码说明了如何构建和训练 SVM 模型:

intnumIterations = 500; 
final SVMModel model = SVMWithSGD.train(training.rdd(), numIterations); 

步骤 7:在测试集上计算原始预测分数

以下是计算原始预测的代码:

JavaRDD<Tuple2<Object, Object>>scoreAndLabels = test.map( 
newFunction<LabeledPoint, Tuple2<Object, Object>>() { 
public Tuple2<Object, Object> call(LabeledPoint p) { 
          Double score = model.predict(p.features()); 
returnnew Tuple2<Object, Object>(score, p.label()); 
        }}); 

步骤 8:评估模型

以下是评估模型的代码:

BinaryClassificationMetrics metrics = new BinaryClassificationMetrics(JavaRDD.toRDD(scoreAndLabels)); 
System.out.println("Area Under PR = " + metrics.areaUnderPR()); 
System.out.println("Area Under ROC = " + metrics.areaUnderROC()); 
Area Under PR = 0.6266666666666666 
Area Under ROC = 0.875  

然而,ROC 的值在 0.5 和 1.0 之间。当值大于 0.8 时,这表明是一个好的分类器,如果 ROC 的值小于 0.8,则表示是一个糟糕的分类器。SVMWithSGD.train()方法默认执行二级(L2)正则化,正则化参数设置为 1.0。

如果要配置此算法,应通过直接创建新对象来进一步定制SVMWithSGD。之后,可以使用 setter 方法来设置对象的值。

有趣的是,所有其他 Spark MLlib 算法都可以以这种方式定制。然而,在定制完成后,您需要构建源代码以进行 API 级别的更改。有兴趣的读者可以加入 Apache Spark 邮件列表,如果他们想为开源项目做出贡献。

请注意,Spark 的源代码可以在 GitHub 上找到,网址为github.com/apache/spark,作为开源项目,它发送拉取请求来丰富 Spark。更多技术讨论可以在 Spark 网站上找到,网址为spark.apache.org/

例如,以下代码生成了一个正则化变体(L1)的 SVM,正则化参数设置为 0.1,并且运行训练算法 500 次,如下所示:

SVMWithSGD svmAlg = new SVMWithSGD(); 
svmAlg.optimizer() 
      .setNumIterations(500) 
      .setRegParam(0.1) 
      .setUpdater(new L1Updater()); 
final SVMModel model = svmAlg.run(training.rdd()); 

您的模型现在已经训练好了。现在,如果您执行步骤 7步骤 8,将生成以下指标:

Area Under PR = 0.9380952380952381 
Area Under ROC = 0.95 

如果您将这个结果与步骤 8中产生的结果进行比较,现在它会好得多,不是吗?然而,根据数据准备的不同,您可能会得到不同的结果。

它指示了更好的分类(请参见www.researchgate.net/post/What_is_the_value_of_the_area_under_the_roc_curve_AUC_to_conclude_that_a_classifier_is_excellent)。通过这种方式,支持向量机可以针对新的数据类型进行优化或自适应。

然而,参数(即迭代次数、回归参数和更新器)应该相应地设置。

增量神经网络

R 或 Matlab 中的神经网络的增量版本使用 adapt 函数提供了适应性。这种更新是否是迭代地而不是覆盖地进行?为了验证这个说法,读者可以尝试使用 R 或 Matlab 版本的增量神经网络分类器,可能需要选择第一个数据块的子集作为训练中的第二个块。如果是覆盖的,当您使用经过训练的网络与子集一起测试第一个数据块时,它可能会对不属于子集的数据进行糟糕的预测。

使用 Spark 进行多层感知器分类

迄今为止,Spark 尚未实现神经网络的增量版本。根据提供在spark.apache.org/docs/latest/ml-classification-regression.html#multilayer-perceptron-classifier的 API 文档,Spark 的多层感知器分类器MLPC)是基于前馈人工神经网络FANN)的分类器。MLPC 包括多层节点,包括隐藏层。每一层都与下一层等等完全连接在一起。输入层中的节点表示输入数据。所有其他节点通过输入的线性组合与节点的权重w和偏差b以及应用激活函数将输入映射到输出。输出层中的节点数N对应于类的数量。

MLPC 还执行反向传播来学习模型。Spark 使用逻辑损失函数进行优化,有限内存的 Broyden-Fletcher-Goldfarb-ShannoL-BFGS)作为优化例程。请注意,L-BFGS 是拟牛顿法QNM)家族中的一种优化算法,它使用有限的主内存来近似 Broyden-Fletcher-Goldfarb-Shanno 算法。为了训练多层感知器分类器,需要设置以下参数:

  • 迭代的容限

  • 学习的块大小

  • 种子大小

  • 最大迭代次数

请注意,层包括输入层、隐藏层和输出层。此外,收敛容限的较小值将导致更高的准确性,但需要更多的迭代。默认的块大小参数为 128,最大迭代次数默认设置为 100。我们建议您相应地和谨慎地设置这些值。

在这个小节中,我们将展示 Spark 如何通过 Iris 数据集实现了神经网络学习算法的多层感知器分类器。

步骤 1:数据集收集、处理和探索

原始的鸢尾植物数据集是从 UCI 机器学习仓库(www.ics.uci.edu/~mlearn/MLRepository.html)收集的,然后由 Chang 等人进行了预处理,缩放到 libsvm 格式,并放置在 libsvm 支持向量机的综合库中,网址为(www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/multiclass.html)用于二进制、多类别和多标签分类任务。鸢尾花数据集包含三个类别和四个特征,其中萼片和花瓣的长度根据 libsvm 格式进行了缩放。更具体地说,这是属性信息:

  • 类别:鸢尾花 Setosa、鸢尾花 Versicolour、鸢尾花 Virginica(第 1 列)

  • 厘米的萼片长度(第 2 列)

  • 厘米的萼片宽度(第 3 列)

  • 厘米的花瓣长度(第 4 列)

  • 厘米的花瓣宽度(第 5 列)

数据集的快照显示在图 6中:

使用 Spark 进行多层感知器分类

图 6:鸢尾花数据集快照

步骤 2:加载所需的包和 API

以下是加载所需包和 API 的代码:

import org.apache.spark.ml.classification.MultilayerPerceptronClassificationModel; 
import org.apache.spark.ml.classification.MultilayerPerceptronClassifier; 
import org.apache.spark.ml.evaluation.MulticlassClassificationEvaluator; 
import org.apache.spark.sql.Dataset; 
import org.apache.spark.sql.Row; 
import org.apache.spark.sql.SparkSession; 
import com.example.SparkSession.UtilityForSparkSession; 

步骤 3:创建一个 Spark 会话

以下代码帮助我们创建 Spark 会话:

SparkSession spark = UtilityForSparkSession.mySession(); 

注意,创建并返回 Spark 会话对象的mySession()方法如下:

public static SparkSession mySession() { 
SparkSession spark = SparkSession.builder() 
.appName("MultilayerPerceptronClassificationModel") 
.master("local[*]") 
.config("spark.sql.warehouse.dir", "E:/Exp/") 
.getOrCreate(); 
    return spark; 
  } 

步骤 4:解析和准备数据集

将输入数据加载为libsvm格式:

String path = "input/iris.data"; 
Dataset<Row> dataFrame = spark.read().format("libsvm").load(path); 

步骤 5:准备训练和测试集

准备训练和测试集:训练= 70%,测试= 30%,种子= 12345L:

Dataset<Row>[] splits = dataFrame.randomSplit(new double[] { 0.7, 0.3 }, 12345L); 
Dataset<Row> train = splits[0]; 
Dataset<Row> test = splits[1]; 

步骤 6:为神经网络指定层

为神经网络指定层。这里,输入层大小为 4(特征),两个中间层(即隐藏层)的大小分别为 4 和 3,输出大小为 3(类别):

int[] layers = newint[] { 4, 4, 3, 3 }; 

步骤 7:创建多层感知器估计器

创建MultilayerPerceptronClassifier训练器并设置其参数。在这里,使用步骤 6中的setLayers()方法设置param [[layers]]的值。使用setTol()方法设置迭代的收敛容限,因为较小的值会导致更高的准确性,但需要更多的迭代。

注意默认值为1E-4。使用setBlockSize()方法设置 Param [[blockSize]]的值,默认为 128KB。如果未设置权重,可以使用setInitialWeights()设置权重初始化的种子。最后,使用setMaxIter()方法设置最大迭代次数,默认为 100:

MultilayerPerceptronClassifier trainer = new MultilayerPerceptronClassifier() 
        .setLayers(layers)        
        .setTol(1E-4)         
        .setBlockSize(128)         
        .setSeed(12345L)  
        .setMaxIter(100); 

步骤 8:训练模型

使用步骤 7中的先前估计器训练MultilayerPerceptronClassificationModel

MultilayerPerceptronClassificationModel model = trainer.fit(train); 

步骤 9:在测试集上计算准确率

以下是在测试集上计算准确率的代码:

Dataset<Row> result = model.transform(test); 
Dataset<Row> predictionAndLabels = result.select("prediction", "label"); 

步骤 10:评估模型

评估模型,计算指标,并打印准确率、加权精度和加权召回率:

MulticlassClassificationEvaluator evaluator = new MulticlassClassificationEvaluator().setMetricName("accuracy"); 
MulticlassClassificationEvaluator evaluator2 = new MulticlassClassificationEvaluator().setMetricName("weightedPrecision"); 
MulticlassClassificationEvaluator evaluator3 = new MulticlassClassificationEvaluator().setMetricName("weightedRecall"); 
System.out.println("Accuracy = " + evaluator.evaluate(predictionAndLabels)); 
System.out.println("Precision = " + evaluator2.evaluate(predictionAndLabels)); 
System.out.println("Recall = " + evaluator3.evaluate(predictionAndLabels)); 

输出应如下所示:

Accuracy = 0.9545454545454546  
Precision = 0.9595959595959596 
Recall = 0.9545454545454546  

步骤 11:停止 Spark 会话

以下代码用于停止 Spark 会话:

spark.stop(); 

从先前的预测指标可以看出,分类任务相当令人印象深刻。现在轮到你使你的模型适应了。现在尝试使用新数据集进行训练和测试,并使你的 ML 模型适应。

增量贝叶斯网络

正如我们之前讨论的,朴素贝叶斯是一种简单的多类别分类算法,假设每对特征之间都是独立的。基于朴素贝叶斯的模型可以被训练得非常高效。该模型可以计算每个特征在给定标签的条件概率分布,因为通过对训练数据的传递。之后,它应用贝叶斯定理来计算用于进行预测的标签的条件概率分布。

然而,目前还没有将增量版本的贝叶斯网络实现到 Spark 中。根据提供的 API 文档spark.apache.org/docs/latest/mllib-naive-bayes.html,每个观察结果是一个文档,每个特征代表一个术语。观察结果的值是术语的频率或零或一。这个值表示了术语是否在多项式朴素贝叶斯和伯努利朴素贝叶斯的文档分类中是否被发现。

请注意,与基于线性 SVM 的学习一样,这里的特征值也必须是非负的。模型的类型是通过可选参数选择的,多项式或伯努利。默认模型类型是多项式。此外,可以通过设置参数λ来使用加法平滑(即 lambda)。请注意,默认的 lambda 值为 1.0。

有关基于大数据方法的贝叶斯网络学习的更多技术细节,请参阅论文:Jianwu W.等人的大数据贝叶斯网络学习的可扩展数据科学工作流方法 (users.sdsc.edu/~jianwu/JianwuWang_files/A_Scalable_Data_Science_Workflow_Approach_for_Big_Data_Bayesian_Network_Learning.pdf)。

对于增量贝叶斯网络的更多见解,感兴趣的读者还应参考以下出版物:

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

Spark MLlib 中的当前实现支持多项式朴素贝叶斯和伯努利朴素贝叶斯。然而,增量版本尚未实现。因此,在本节中,我们将向您展示如何使用 Spark MLlib 版本的朴素贝叶斯对车辆规模数据集进行分类,以便为您提供基于朴素贝叶斯的学习的一些概念。

请注意,由于使用 Spark ML 的低准确性和精度,我们没有提供 Pipeline 版本,而是仅使用 Spark MLlib 实现了相同的功能。此外,如果您有合适且更好的数据,可以轻松尝试实现 Spark ML 版本。

步骤 1:数据收集、预处理和探索

数据集是从www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/multiclass.html#aloi下载的,并由 David D. Lewis,Yiming Yang,Tony G. Rose 和 Fan Li 提供。RCV1:文本分类研究的新基准集。机器学习研究杂志,5:361-397,2004 年。

预处理: 对于预处理,考虑了两个步骤如下:

  • 标签层次结构通过将数据集映射到 RCV1 的第二级(即修订)主题层次结构进行重新组织。具有第三或第四级的文档仅映射到其第二级的父类别。因此,不考虑具有第一级的文档来创建映射。

  • 由于 Spark 中多级分类器的当前实现不够健壮,已删除了多标签实例。

完成这两个步骤后,最终收集到 53 个类别和 47,236 个特征。以下是数据集的快照,显示在图 7中:

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

图 7:RCV1 主题层次数据集

步骤 2:加载所需的库和包

以下是加载库和包的代码:

import org.apache.spark.api.java.JavaPairRDD; 
import org.apache.spark.api.java.JavaRDD; 
import org.apache.spark.api.java.function.Function; 
import org.apache.spark.api.java.function.PairFunction; 
import org.apache.spark.mllib.classification.NaiveBayes; 
import org.apache.spark.mllib.classification.NaiveBayesModel; 
import org.apache.spark.mllib.regression.LabeledPoint; 
import org.apache.spark.mllib.util.MLUtils; 
import org.apache.spark.sql.SparkSession; 
importscala.Tuple2; 

步骤 3:初始化 Spark 会话

以下代码帮助我们创建 Spark 会话:

static SparkSession spark = SparkSession 
      .builder() 
      .appName("JavaLDAExample").master("local[*]") 
      .config("spark.sql.warehouse.dir", "E:/Exp/") 
      .getOrCreate();  

第 4 步:准备 LabeledPoint RDDs

以 libsvm 格式解析数据集并准备LabeledPoint RDDs:

static String path = "input/rcv1_train.multiclass.data"; 
JavaRDD<LabeledPoint> inputData = MLUtils.loadLibSVMFile(spark.sparkContext(), path).toJavaRDD();  

对于文档分类,输入特征向量通常是稀疏的,应该提供稀疏向量作为输入以利用稀疏性。由于训练数据只使用一次,因此不需要将其缓存。

第 5 步:准备训练和测试集

以下是准备训练和测试集的代码:

JavaRDD<LabeledPoint>[] split = inputData.randomSplit(new double[]{0.8, 0.2}, 12345L); 
JavaRDD<LabeledPoint> training = split[0];  
JavaRDD<LabeledPoint> test = split[1]; 

第 6 步:训练朴素贝叶斯模型

通过指定模型类型为多项式和 lambda = 1.0 来训练朴素贝叶斯模型,这是默认值,适用于任何特征的多类分类。但是,请注意,伯努利朴素贝叶斯要求特征值为 0 或 1:

final NaiveBayesModel model = NaiveBayes.train(training.rdd(), 1.0, "multinomial"); 

第 7 步:计算测试数据集上的预测

以下是计算预测的代码:

JavaPairRDD<Double,Double> predictionAndLabel = 
test.mapToPair(new PairFunction<LabeledPoint, Double, Double>() { 
@Override 
public Tuple2<Double, Double> call(LabeledPoint p) { 
return new Tuple2<>(model.predict(p.features()), p.label()); 
          } 
        }); 

第 8 步:计算预测准确度

以下是计算预测准确度的代码:

double accuracy = predictionAndLabel.filter(new Function<Tuple2<Double, Double>, Boolean>() { 
@Override 
public Boolean call(Tuple2<Double, Double>pl) { 
returnpl._1().equals(pl._2()); 
        } 
      }).count() / (double) test.count(); 

第 9 步:打印准确度

以下是打印准确度的代码:

System.out.println("Accuracy of the classification: "+accuracy); 

这提供了以下输出:

Accuracy of the classification: 0.5941753719531497  

这很低,对吧?这正如我们在第七章中调整 ML 模型时所讨论的。通过选择适当的算法(即分类器或回归器)进行交叉验证和训练分割,可以进一步提高预测准确度。

通过重用 ML 模型进行适应

在本节中,我们将描述如何使机器学习模型适应新数据集。将展示一个用于预测心脏病的示例。首先我们将描述问题陈述,然后我们将探索心脏疾病数据集。在数据集探索之后,我们将训练并保存模型到本地存储。之后将评估模型的表现。最后,我们将重用/重新加载相同的模型,以适用于新的数据类型。

更具体地说,我们将展示如何使用 Spark 机器学习 API,包括 Spark MLlib、Spark ML 和 Spark SQL 来预测未来心脏病的可能性。

问题陈述和目标

机器学习和大数据的结合是一个激进的组合,在研究、学术界以及生物医学领域都产生了巨大的影响。在生物医学数据分析领域,这对诊断和预后的真实数据集产生了更好的影响,以实现更好的医疗保健。此外,生命科学研究也正在进入大数据领域,因为数据集以前所未有的方式被生成和产生。这给机器学习和生物信息学工具和算法带来了巨大的挑战,以从大数据标准(如容量、速度、多样性、真实性、可见性和价值)中找到价值。

数据探索

近年来,生物医学研究取得了巨大进展,越来越多的生命科学数据集正在生成,其中许多是开放源代码的。然而,为了简单和便利起见,我们决定使用克利夫兰数据库。迄今为止,大多数将机器学习技术应用于生物医学数据分析的研究人员都使用了这个数据集。根据数据集描述,这个心脏病数据集是生物医学数据分析和机器学习领域的研究人员最常使用和研究的数据集之一。

该数据集可以在 UCI 机器学习数据集存储库archive.ics.uci.edu/ml/machine-learning-databases/heart-disease/免费获取。该数据包含共 76 个属性,但大多数已发表的研究论文只使用了该领域的 14 个特征的子集。"goal"字段用于指代心脏疾病是否存在。它有 5 个可能的值,范围从 0 到 4。值 0 表示没有心脏疾病。而值 1 和 2 表示疾病存在,但处于初期阶段。另一方面,值 3 和 4 表示心脏疾病的强烈可能性。克利夫兰数据集的生物医学实验仅仅试图区分存在(值 1、2、3、4)和不存在(值 0)。简而言之,数值越高,疾病可能性越大,存在的证据也越多。另一件事是,隐私在生物医学数据分析领域以及所有类型的诊断和预后中都是一个重要关注点。因此,最近已从数据集中删除了患者的姓名和社会安全号码,以避免隐私问题。因此,这些值已被替换为虚拟值。

需要注意的是,已经处理了三个文件,其中包含了克利夫兰、匈牙利和瑞士的数据集。所有四个未处理的文件也存在于此目录中。为了演示示例,我们将使用克利夫兰数据集来训练和评估模型。然而,匈牙利数据集将用于重新使用保存的模型。正如我们已经说过的,尽管属性数量为 76(包括预测属性),但像其他 ML/生物医学研究人员一样,我们也将只使用 14 个属性,具体属性信息如下:

编号 属性名称 解释
1 age 年龄(以年为单位)
2 sex 男性或女性:性别(1 = 男性;0 = 女性)
3 cp 胸痛类型:— 值 1:典型心绞痛— 值 2:非典型心绞痛— 值 3:非心绞痛— 值 4:无症状
4 trestbps 静息血压(入院时以 mm Hg 为单位)
5 chol 血清胆固醇(以 mg/dl 为单位)
6 fbs 空腹血糖。如果> 120 mg/dl)(1 = 真; 0 = 假)
7 restecg 静息心电图结果:— 值 0:正常— 值 1:ST-T 波异常— 值 2:根据 Estes 标准显示可能或明确的左心室肥大。
8 thalach 达到的最大心率
9 exang 运动诱发的心绞痛(1 = 是; 0 = 否)
10 oldpeak 相对于休息引起的 ST 段压低
11 slope 峰值运动 ST 段的斜率— 值 1:上斜— 值 2:平坦— 值 3:下斜
12 ca 荧光镜检查染色的主要血管数(0-3)
13 thal 心率:—值 3 = 正常;—值 6 = 固定缺陷—值 7 = 可逆缺陷
14 num 心脏疾病诊断(血管造影疾病状态)— 值 0:<50%直径狭窄— 值 1:>50%直径狭窄

表 1:数据集特征

数据集的样本快照如下所示:

数据探索

图 8:心脏疾病数据集的样本快照

开发心脏疾病预测模型

步骤 1:加载所需的包和 API

为了我们的目的,需要导入以下包和 API。我们相信,如果您对 Spark 2.0.0 有最低的工作经验,这些包是不言自明的:

import java.util.HashMap; 
import java.util.List; 
import org.apache.spark.api.java.JavaPairRDD; 
import org.apache.spark.api.java.JavaRDD; 
import org.apache.spark.api.java.function.Function; 
import org.apache.spark.api.java.function.PairFunction; 
import org.apache.spark.ml.classification.LogisticRegression; 
import org.apache.spark.mllib.classification.LogisticRegressionModel; 
import org.apache.spark.mllib.classification.NaiveBayes; 
import org.apache.spark.mllib.classification.NaiveBayesModel; 
import org.apache.spark.mllib.linalg.DenseVector; 
import org.apache.spark.mllib.linalg.Vector; 
import org.apache.spark.mllib.regression.LabeledPoint; 
import org.apache.spark.mllib.regression.LinearRegressionModel; 
import org.apache.spark.mllib.regression.LinearRegressionWithSGD; 
import org.apache.spark.mllib.tree.DecisionTree; 
import org.apache.spark.mllib.tree.RandomForest; 
import org.apache.spark.mllib.tree.model.DecisionTreeModel; 
import org.apache.spark.mllib.tree.model.RandomForestModel; 
import org.apache.spark.rdd.RDD; 
import org.apache.spark.sql.Dataset; 
import org.apache.spark.sql.Row; 
import org.apache.spark.sql.SparkSession; 
import com.example.SparkSession.UtilityForSparkSession; 
import javassist.bytecode.Descriptor.Iterator; 
import scala.Tuple2; 

步骤 2:创建一个活动的 Spark 会话

以下代码帮助我们创建 Spark 会话:

SparkSession spark = UtilityForSparkSession.mySession(); 

这是UtilityForSparkSession类,它创建并返回一个活动的 Spark 会话:

import org.apache.spark.sql.SparkSession; 
public class UtilityForSparkSession { 
  public static SparkSession mySession() { 
    SparkSession spark = SparkSession 
                          .builder() 
                          .appName("UtilityForSparkSession") 
                          .master("local[*]") 
                          .config("spark.sql.warehouse.dir", "E:/Exp/") 
                          .getOrCreate(); 
    return spark; 
  } 
} 

请注意,在 Windows 7 平台上,我们已将 Spark SQL 仓库设置为E:/Exp/,但根据您的操作系统设置您的路径。

步骤 3:数据解析和标签点的 RDD 创建

将输入作为简单文本文件,将它们解析为文本文件,并创建一个用于分类和回归分析的标签点的 RDD。还要指定输入源和分区数。根据数据集大小调整分区数。这里分区数已设置为 2:

String input = "heart_diseases/processed_cleveland.data"; 
Dataset<Row> my_data = spark.read().format("com.databricks.spark.csv").load(input); 
my_data.show(false); 
RDD<String> linesRDD = spark.sparkContext().textFile(input, 2); 

由于JavaRDD无法直接从文本文件创建,我们已经创建了一个简单的 RDD,以便在必要时将它们转换为JavaRDD。现在让我们创建一个带有标签点的JavaRDD。但是,我们首先需要将 RDD 转换为JavaRDD以满足我们的目的,如下所示:

JavaRDD<LabeledPoint> data = linesRDD.toJavaRDD().map(new Function<String, LabeledPoint>() { 
      @Override 
  public LabeledPoint call(String row) throws Exception { 
      String line = row.replaceAll("\\?", "999999.0"); 
      String[] tokens = line.split(","); 
      Integer last = Integer.parseInt(tokens[13]); 
      double[] features = new double[13]; 
      for (int i = 0; i < 13; i++) { 
      features[i] = Double.parseDouble(tokens[i]); 
      } 
      Vector v = new DenseVector(features); 
      Double value = 0.0; 
      if (last.intValue() > 0) 
        value = 1.0; 
      LabeledPoint lp = new LabeledPoint(value, v); 
    return lp; 
      } 
    }); 

使用replaceAll()方法,我们处理了原始文件中指定的缺失值等无效值,使用了?字符。为了摆脱缺失或无效值,我们用一个非常大的值替换它们,这对原始分类或预测结果没有副作用。原因是缺失或稀疏数据可能导致高度误导性的结果。

步骤 4:将标签点的 RDD 分割为训练集和测试集

在上一步中,我们创建了可以用于回归或分类任务的 RDD 标签点数据。现在我们需要将数据分割为训练集和测试集,如下所示:

double[] weights = {0.7, 0.3}; 
long split_seed = 12345L; 
JavaRDD<LabeledPoint>[] split = data.randomSplit(weights, split_seed); 
JavaRDD<LabeledPoint> training = split[0]; 
JavaRDD<LabeledPoint> test = split[1]; 

如果您查看前面的代码段,您会发现我们已将 RDD 标签点分为 70%的训练集和 30%的测试集。randomSplit()方法执行此分割。请注意,我们已将此 RDD 的存储级别设置为在第一次计算后跨操作持久化其值。只有在 RDD 尚未设置存储级别时才能用于分配新的存储级别。拆分种子值是一个长整数,表示拆分将是随机的,但结果不会在模型构建或训练的每次运行或迭代中发生变化。

步骤 5:训练模型

首先我们将训练线性回归模型,这是最简单的回归分类器:

final double stepSize = 0.0000000009; 
final int numberOfIterations = 40;  
LinearRegressionModel model = LinearRegressionWithSGD.train(JavaRDD.toRDD(training), numberOfIterations, stepSize); 

如您所见,前面的代码使用随机梯度下降训练了一个没有正则化的线性回归模型。这解决了最小二乘回归公式 f (weights) = 1/n ||A weights-y||²^,即均方误差。这里数据矩阵有n行,输入 RDD 保存了 A 的一组行,每行都有其相应的右手边标签 y。此外,为了训练模型,它需要训练集、迭代次数和步长。我们在这里为最后两个参数提供了一些随机值。

步骤 6:将模型保存以备将来使用

现在让我们保存刚刚创建的模型以备将来使用。很简单 - 只需使用以下代码指定存储位置如下:

String model_storage_loc = "models/heartModel";   
model.save(spark.sparkContext(), model_storage_loc); 

一旦模型保存在您想要的位置,您将在 Eclipse 控制台中看到以下输出:

开发心脏疾病预测模型

图 9:模型保存到存储后的日志

步骤 7:使用测试集评估模型

现在让我们在测试数据集上计算预测分数:

JavaPairRDD<Double,Double> predictionAndLabel = 
  test.mapToPair(new PairFunction<LabeledPoint, Double, Double>() { 
            @Override 
    public Tuple2<Double, Double> call(LabeledPoint p) { 
       return new Tuple2<>(model.predict(p.features()), p.label()); 
            } 
          });   

预测预测的准确性:

double accuracy = predictionAndLabel.filter(new Function<Tuple2<Double, Double>, Boolean>() { 
          @Override 
          public Boolean call(Tuple2<Double, Double> pl) { 
            return pl._1().equals(pl._2()); 
          } 
        }).count() / (double) test.count(); 
System.out.println("Accuracy of the classification: "+accuracy);   

输出如下:

Accuracy of the classification: 0.0 

步骤 8:使用不同的分类器进行预测分析

不幸的是,根本没有预测准确性,对吧?可能有几个原因,包括以下原因:

  • 数据集特征

  • 模型选择

  • 参数选择 - 也称为超参数调整

为简单起见,我们假设数据集是可以接受的,因为正如我们已经说过的那样,它是一个广泛使用的数据集,被全球许多研究人员用于机器学习研究。现在,接下来呢?让我们考虑另一个分类器算法,例如随机森林或决策树分类器。随机森林呢?让我们选择随机森林分类器。只需使用以下代码使用训练集训练模型:

Integer numClasses = 26; //Number of classes 

现在使用HashMap来限制树构造中的细微差别:

HashMap<Integer, Integer> categoricalFeaturesInfo = new HashMap<Integer, Integer>(); 

现在声明训练随机森林分类器所需的其他参数:

Integer numTrees = 5; // Use more in practice 
String featureSubsetStrategy = "auto"; // Let algorithm choose the best 
String impurity = "gini"; // info. gain & variance also available 
Integer maxDepth = 20; // set the value of maximum depth accordingly 
Integer maxBins = 40; // set the value of bin accordingly 
Integer seed = 12345; //Setting a long seed value is recommended       
final RandomForestModel model = RandomForest.trainClassifier(training, numClasses,categoricalFeaturesInfo, numTrees, featureSubsetStrategy, impurity, maxDepth, maxBins, seed); 

我们认为trainClassifier()方法使用的参数是不言自明的,所以我们将让读者了解每个参数的重要性。太棒了!我们已经使用随机森林分类器训练了模型,并管理云端保存了模型以备将来使用。现在,如果您重用我们在使用测试集评估模型步骤中描述的相同代码,您应该会得到以下输出:

Accuracy of the classification: 0.7843137254901961  

现在预测准确度应该会好得多。如果您仍然不满意,可以尝试使用另一个分类器模型,比如朴素贝叶斯分类器,并进行第七章中讨论的超参数调整,调整机器学习模型

步骤 9:使模型适应新数据集

我们已经提到我们已经保存了模型以备将来使用,现在我们应该抓住机会使用相同的模型来处理新数据集。原因是,如果您回忆一下步骤,我们已经使用训练集训练了模型,并使用测试集进行了评估。现在,如果您有更多数据或新数据可供使用,您会怎么做?您会重新训练模型吗?当然不会,因为您将不得不迭代多个步骤,而且还要牺牲宝贵的时间和成本。

因此,明智的做法是使用已经训练好的模型,并预测在新数据集上的性能。好了,现在让我们重用存储的模型。请注意,您将不得不重用要训练相同模型的模型。例如,如果您使用随机森林分类器进行模型训练并保存了模型,那么在重用时,您将不得不使用相同的分类器模型来加载保存的模型。因此,我们将使用随机森林来加载模型,同时使用新数据集。使用以下代码来实现这一点。现在从新数据集(即具有相同 14 个属性的匈牙利数据库)创建一个 RDD 标签点:

String new_data = "heart_diseases/processed_hungarian.data"; 
RDD<String> linesRDD = spark.sparkContext().textFile(new_data, 2); 
JavaRDD<LabeledPoint> data = linesRDD.toJavaRDD().map(new Function<String, LabeledPoint>() { 
      @Override 
  public LabeledPoint call(String row) throws Exception { 
  String line = row.replaceAll("\\?", "999999.0"); 
  String[] tokens = line.split(","); 
  Integer last = Integer.parseInt(tokens[13]); 
    double[] features = new double[13]; 
             for (int i = 0; i < 13; i++) { 
          features[i] = Double.parseDouble(tokens[i]); 
                } 
      Vector v = new DenseVector(features); 
      Double value = 0.0; 
      if (last.intValue() > 0) 
        value = 1.0; 
      LabeledPoint p = new LabeledPoint(value, v); 
      return p; 
      } }); 

现在让我们使用随机森林模型算法加载保存的模型如下:

RandomForestModel model2 =  
RandomForestModel.load(spark.sparkContext(), model_storage_loc); 

现在让我们计算测试集上的预测:

JavaPairRDD<Double, Double> predictionAndLabel = 
  data.mapToPair(new PairFunction<LabeledPoint, Double, Double>() { 
          @Override 
          public Tuple2<Double, Double> call(LabeledPoint p) { 
      return new Tuple2<>(model2.predict(p.features()), p.label()); 
            } 
          }); 

现在按如下方式计算预测的准确度:

double accuracy = predictionAndLabel.filter(new Function<Tuple2<Double, Double>, Boolean>() { 
          @Override 
          public Boolean call(Tuple2<Double, Double> pl) { 
            return pl._1().equals(pl._2()); 
          } 
        }).count() / (double) data.count(); 
System.out.println("Accuracy of the classification: "+accuracy);   

我们应该得到以下输出:

Accuracy of the classification: 0.9108910891089109 

现在训练朴素贝叶斯分类器并查看预测性能。只需下载朴素贝叶斯分类器的源代码,并使用包含所需 JAR 和 API 依赖项的pom.xml文件将代码作为 Maven 友好项目运行。

以下表显示了三个分类器(即线性回归、随机森林和朴素贝叶斯分类器)之间的预测准确性的比较。请注意,根据训练,您得到的模型可能会有不同的输出,因为我们随机将数据集分成训练集和测试集。

分类器 模型构建时间 模型保存时间 准确度
线性回归 1199 毫秒 2563 毫秒 0.0%
朴素贝叶斯 873 毫秒 2514 毫秒 45%
随机森林 2120 毫秒 2538 毫秒 91%

表 2:三个分类器之间的比较

注意

我们在一台 Windows 7(64 位)、Core i7(2.90GHz)处理器和 32GB 主内存的机器上获得了上述输出。因此,根据您的操作系统类型和硬件配置,您可能会收到不同的结果。

这样,ML 模型可以适应新的数据类型。但是,请确保您使用相同的分类器或回归器来训练和重用模型,以使 ML 应用程序具有适应性。

动态环境中的机器学习

在动态环境中进行预测并不总是能够产生期望的结果,特别是在复杂和非结构化的数据中。

有几个原因。例如,如何从少量数据中推断出真实的结果,或者处理被发现过于繁琐的非结构化和高维数据?此外,使用有效策略对现实环境进行模型修订也是昂贵的。

此外,有时输入数据集的维度很高。因此,数据可能过于密集或非常稀疏。在这种情况下,如何处理非常大的设置以及如何将静态模型应用于新兴应用领域,如机器人技术、图像处理、深度学习、计算机视觉或网络挖掘,是具有挑战性的。另一方面,集成方法越来越受欢迎,用于从现有模型中选择和组合模型,使 ML 模型更具适应性。图 10显示了基于分层和动态环境的学习:

动态环境中的机器学习

图 10:动态环境中机器学习的层次结构

在这种情况下,诸如神经网络和基于统计的学习等 ML 技术也因其在生物系统等行业和研究中的成功而变得越来越受欢迎。特别是,诸如神经网络、决策树或矢量量化等经典学习算法通常受到纯前馈设置和简单矢量数据的限制,而不是动态环境。矢量化的特征通常提供更好的预测,因为具有丰富的结构。总之,在开发动态环境中的 ML 应用程序时存在三个挑战:

  • 数据结构如何在自主环境中形成?

  • 我们如何处理统计稀疏和高维的输入数据?更具体地说,如何使用在线算法对大规模数据集进行预测分析,应用降维等?

在只有有限的强化信号、不适当的域或部分未规定的设置下,我们如何在动态环境中开发受控和有效的策略?考虑到这些问题和研究中的有希望的进展,在本节中,我们将通过统计和对抗模型提供一些关于在线学习技术的见解。由于学习动态环境,如流式处理将在第九章中讨论,我们将不在本章讨论基于流式处理的学习。

在线学习

批处理学习技术通过一次性学习整个训练数据集生成最佳预测器,通常被称为静态学习。静态学习算法使用训练数据的批次来训练模型,然后使用测试样本和找到的关系进行预测,而在线学习算法则使用初始猜测模型,然后从训练人口中挑选一个观察值,并重新校准每个输入参数的权重。数据通常按顺序作为批次可用。顺序数据用于在每个步骤更新结果的最佳预测器,如图 11中所述。在线学习有三种用例:

  • 首先,当在整个数据集上训练 ML 模型在计算上是不可行的时,通常使用在线学习

  • 其次,它还用于需要算法动态适应数据中新模式的情况

  • 第三,当数据本身是随时间生成的函数时,例如股票价格预测

因此,在线学习需要考虑网络约束的算法,即可以执行的算法。存在两种用于在线学习模型的一般建模策略:

  • 统计学习模型:例如,随机梯度下降和感知器

  • 对抗模型:例如,垃圾邮件过滤属于这一类别,因为对手将根据垃圾邮件检测器的当前行为动态生成新的垃圾邮件。

虽然在线和增量学习技术相似,但也略有不同。在线学习通常是单次通过(epoch=1)或可以配置的若干次通过,而增量意味着您已经有一个模型。无论模型是如何构建的,新的示例都可以改变模型。此外,通常需要在线和增量的组合。

数据正在以前所未有的方式随处生成。这些庞大的数据对于构建能够处理高容量、高速度和高准确性数据的机器学习工具构成了巨大挑战。简而言之,在线生成的数据也是大数据。因此,我们需要了解学习处理这种高容量和高速度数据的在线学习算法的技术,而这些算法是为了在性能有限的机器上处理数据而设计的。

在线学习

图 11:批处理(静态)与在线学习,概述

统计学习模型

如已概述,在基于统计的学习模型中,如随机梯度下降(SGD)和人工神经网络或感知器,数据样本被假定为彼此独立。除此之外,还假定数据集是作为随机变量相同分布的。换句话说,它们不会随时间改变。因此,机器学习算法对数据的访问是有限的。

在统计学习模型领域,有两种被认为是重要的解释:

  • 第一种解释: 这将随机梯度下降方法视为最小化期望风险的问题。在无限的数据流中,预测分析被假定来自正态分布。因此,只使用随机梯度下降方法来限制偏差。这种解释对有限的训练集也是有效的。

  • 第二种解释: 这适用于有限的训练集情况,并将 SGD 算法视为增量梯度下降方法的一个实例。在这种情况下,人们会看到经验风险:由于增量梯度下降的梯度,迭代也是对梯度的随机估计,这种解释适用于最小化经验风险而不是期望风险。为什么允许多次通过数据,并且实际上会导致对偏差的更严格的界限。

对抗模型

经典机器学习,尤其是在课堂上教授的,强调静态环境,通常使用不变的数据进行预测。因此,与统计或因果推断或动态环境相比,形式上更容易。另一方面,在动态环境中将学习问题视为两个玩家之间的游戏,例如学习者与数据生成器之间的游戏,是对对抗模型的一个例子。这种建模和进行预测分析是非常繁琐的,因为世界并不知道您试图正式对其进行建模。

此外,您的模型对世界没有任何积极或消极的影响。因此,这种模型的最终目标是最小化由其他玩家的举动产生的情况所造成的损失。对手可以根据学习算法的输出在运行时或动态地调整生成的数据。由于对数据没有分布假设,因此在整个可能提前查看的序列中表现良好成为最终目标。此外,遗憾应该在最后一步的假设上最小化。根据 Cathy O.等人的说法(数学毁灭的武器,Cathy O'Neil 和 Crown,2016 年 9 月 6 日),对抗性机器学习可以定义如下:

对抗性机器学习是研究在假设这些类型的情况下让步甚至略微更现实的替代方案时会发生什么的正式名称(无害地称为放松假设)。

提示

在 Spark 2.0.0 发布之前,没有在发布中实现正式算法。因此,我们无法提供任何可以进一步详细解释的具体示例。感兴趣的读者应该查看最新的 Spark 发布以了解更新情况。

总结

在本章中,我们试图涵盖一些高级机器学习技术,使机器学习模型和应用程序能够适应新的问题和数据类型。

我们展示了几个机器学习算法的例子,这些算法通过批处理或静态学习来学习模型数据,每次看到新的训练实例时都会更新模型。

我们还讨论了如何通过泛化、增量学习、模型重用和动态环境使模型具有适应性。

在第九章中,流式和图数据的高级机器学习,我们将指导您如何使用 Spark MLlib 和 Spark ML 在流式和图数据上应用机器学习技术,例如主题建模。

第九章:流式和图形数据的高级机器学习

本章将指导读者如何借助 Spark MLlib、Spark ML 和 Spark Streaming 应用机器学习技术到流式和图形数据,使用 GraphX。例如,从 Twitter 的实时推文数据中进行主题建模。读者将能够使用现有的 API 从 Twitter 等流数据源构建实时和预测性应用程序。通过 Twitter 数据分析,我们将展示如何进行大规模社交情感分析。我们还将展示如何使用 Spark MLlib 开发大规模电影推荐系统,这是社交网络分析的一个隐含部分。简而言之,本章将涵盖以下主题:

  • 开发实时 ML 管道

  • 时间序列和社交网络分析

  • 使用 Spark 进行电影推荐

  • 从流式数据开发实时 ML 管道

  • 图形数据和半监督图形学习上的 ML 管道

然而,要成为一个有效和新兴的机器学习应用程序,实际上需要持续流动的标记数据。因此,对大规模非结构化数据进行预处理和准确标记数据本质上引入了许多不必要的延迟。

如今,我们经常听到和阅读关于实时机器学习的内容。人们通常在讨论从社交网络服务(SNS)、信用卡欺诈检测系统或从面向业务的交易数据中挖掘与客户相关的购买规则时,提供这种吸引人的业务场景。

根据许多机器学习专家的说法,实时不断地更新信用卡欺诈检测模型是可能的。这很棒,但对我来说并不现实,有几个原因。首先,确保这种数据的持续流动对于模型的重新训练并不是必要的。其次,在大多数机器学习系统中,创建标记数据可能是最慢、最昂贵的步骤。

开发实时 ML 管道

为了开发实时机器学习应用程序,我们需要持续获取数据。这些数据可能包括交易数据、简单文本、来自 Twitter 的推文、来自 Flume 或 Kafka 的消息或流式数据等,因为这些数据大多是非结构化的。

要部署这些类型的机器学习应用程序,我们需要经历一系列步骤。为我们的目的服务的数据最不可靠的来源是来自多个来源的实时数据。通常网络是性能瓶颈。

例如,并不保证您总是能够从 Twitter 收到大量推文。此外,即时对这些数据进行标记以构建一个 ML 模型并不是一个现实的想法。尽管如此,我们在这里提供了一个关于如何从实时流数据中开发和部署 ML 管道的真实见解。图 1显示了实时 ML 应用程序开发的工作流程。

流式数据收集作为非结构化文本数据

我们在这里要强调的是,实时流数据收集取决于:

  • 数据收集的目的。如果目的是开发在线信用卡欺诈检测系统,那么数据应该通过网络 API 从您自己的网络中收集。如果目的是收集社交媒体情感分析,那么数据可以从 Twitter、LinkedIn、Facebook 或报纸网站收集,如果目的是网络异常检测,数据可以从网络数据中收集。

  • 数据可用性是一个问题,因为并非所有社交媒体平台都提供公共 API 来收集数据。网络条件很重要,因为流数据量很大,需要非常快的网络连接。

  • 存储能力是一个重要考虑因素,因为几分钟的推文数据集合可能会产生数 GB 的数据。

此外,我们应该至少等待几天,然后将交易标记为欺诈非欺诈。相反,如果有人报告了欺诈交易,我们可以立即将该交易标记为欺诈以简化操作。

标记数据以进行监督式机器学习

标记的数据集在整个过程中起着核心作用。它确保很容易改变算法的参数,比如特征归一化或损失函数。在这种情况下,我们可以从逻辑回归、支持向量机SVM)或随机森林等多种算法中选择算法本身。

然而,我们不能改变标记的数据集,因为这些信息是预定义的,你的模型应该预测你已经拥有的标签。在之前的章节中,我们已经表明标记结构化数据需要相当长的时间。

现在想想我们将从流媒体或实时来源接收到的完全非结构化的流数据。在这种情况下,标记数据将需要相当长的时间。然而,我们还必须进行预处理,如标记化、清理、索引、去除停用词和去除非结构化数据中的特殊字符。

现在,基本上会有一个问题,数据标记过程需要多长时间?关于标记的数据集的最后一件事是,我们应该明白,如果我们不仔细进行标记,标记的数据集有时可能会存在偏见,这可能会导致模型性能出现很多问题。

创建和构建模型

用于训练情感分析、信用卡欺诈检测模型和关联规则挖掘模型,我们需要尽可能准确的交易数据示例。一旦我们有了标记的数据集,我们就可以开始训练和构建模型:

创建和构建模型

图 1:实时机器学习工作流程。

在第七章中,调整机器学习模型,我们讨论了如何选择适当的模型和 ML 算法,以产生更好的预测分析。该模型可以呈现为具有多个类的二进制或多类分类器。或者,使用 LDA 模型进行情感分析,使用主题建模概念。简而言之,图 1展示了实时机器学习工作流程。

实时预测分析

当您的 ML 模型经过适当的训练和构建后,您的模型已经准备好进行实时预测分析。如果您从模型中得到一个好的预测,那将是很棒的。然而,正如我们之前提到的,当讨论一些准确性问题时,例如真正的阳性和假阳性,如果假阳性的数量很高,那意味着模型的性能不尽人意。

这基本上意味着三件事:我们没有正确标记流数据集,在这种情况下迭代第二步(标记图 1中的数据),或者没有选择适当的 ML 算法来训练模型,最后我们没有调整最终能帮助我们找到适当超参数或模型选择;在这种情况下,直接进入第七步(图 1中的模型部署)。

调整 ML 模型以改进和模型评估

如在第四步(图 1中的模型评估)中提到的,如果模型的性能不令人满意或令人信服,那么我们需要调整模型。正如在第七章中讨论的,我们学习了如何选择适当的模型和 ML 算法,以产生更好的预测分析。有几种调整模型性能的技术,我们可以根据需求和情况选择其中的一种。当我们完成调整后,最后我们应该进行模型评估。

模型的适应性和部署

当我们拥有调整和找到最佳模型时,机器学习模型必须准备好在每次看到新的训练实例时逐渐学习新的数据类型。当我们的模型准备好为大规模流数据进行准确可靠的预测时,我们可以将其部署到现实生活中。

时间序列和社交网络分析

在本节中,我们将尝试提供一些见解和挑战,以处理和开发来自时间序列和社交网络数据的大规模 ML 管道。

时间序列分析

时间序列数据通常出现在监控工业过程或跟踪企业业务指标的情况下。通过时间序列方法对数据进行建模的一个基本区别是,时间序列分析考虑到随时间采集的数据点可能具有内部结构。

这可能包括应该考虑的自相关、趋势或季节性变化。在这方面,回归分析主要用于测试理论。目标是测试以确保一个或多个独立时间序列参数的当前值与其他时间序列数据的当前属性相关联。

为了开发大规模的预测分析应用程序,时间序列分析技术可以应用于实值、分类变量、连续数据、离散数值数据,甚至离散符号数据。时间序列是一系列浮点值,每个值都与时间戳相关联。特别是,我们尽可能坚持时间序列的含义是指单变量时间序列,尽管在其他情境中,它有时指的是在同一时间戳上的多个值的系列。

时间序列数据中的一个瞬时是与单个时间点对应的一系列时间序列中的值向量。一个观察是一个元组(时间戳、键、值),即时间序列或瞬时中的单个值。简而言之,时间序列主要具有四个特征:

  • 带有趋势的系列:由于观察结果随时间增加或减少,尽管趋势是持久的并且具有长期运动。

  • 带有季节性的系列数据:由于观察结果保持高位然后下降,并且一些模式从一个周期重复到下一个周期,并且包含定期的周期性波动,比如在 12 个月的周期内。

  • 带有循环成分的系列数据:由于业务模型定期变化,即,商业中的衰退有时是循环的。它还可能包含超过一年的重复波动或运动。

  • 随机变化:给时间序列图表带来不规则或锯齿状外观的不可预测的组成部分。它还包含不规则或残余的波动。

由于这些具有挑战性的特征,为了实际目的开发实际的机器学习应用变得非常困难。因此,到目前为止,只有一个用于时间序列数据分析的软件包可用,由 Cloudera 开发;它被称为 Spark-TS 库。这里每个时间序列通常都带有一个键,可以在一系列时间序列中识别它。

然而,Spark 的当前实现没有为时间序列数据分析提供任何实现的算法。然而,由于这是一个新兴和热门的话题,希望在未来的版本中至少会有一些算法在 Spark 中实现。在第十章中,配置和使用外部库,我们将更深入地介绍如何使用这些类型的第三方包与 Spark。

社交网络分析

社交网络由节点(点)和相关链接组成,其中节点、链接或边缘是可识别的分析类别。这些节点可能包括有关人、群体和组织的信息。通常,这些信息通常是任何类型的社交实验和分析的主要重点和关注点。这种分析中的链接侧重于以集体方式包括社交联系和可交换信息,以扩大社交互动,例如 Facebook、LinkedIn、Twitter 等。因此,很明显,嵌入在更大社交过程、链接和节点网络中的组织会影响其他人。

另一方面,根据 Otte E.et al.(社交网络分析:信息科学的强大策略,也用于信息科学,信息科学杂志,28:441-453),社交网络分析SNA)是研究发现连接的人或群体之间关系的映射和测量。它还用于找到人、群体、组织和信息处理实体之间的流动。

适当的 SNA 分析可用于显示三种最流行的个体中心性度量之间的区别:度中心性、中介中心性和亲近中心性。度中心性表示节点具有多少链接或事件,或者节点有多少联系。

中介中心性是图中顶点的中心性度量。这也考虑了中介边缘。此外,中介中心性表示节点作为桥梁的次数,通过考虑其他节点之间的最短路径。

另一方面,节点的亲近中心性是连接图中特定节点与所有其他节点之间最短路径的平均长度,例如社交网络。

提示

建议感兴趣的读者阅读更多关于特征向量中心性、Katz 中心性、PageRank 中心性、渗流中心性、交叉团中心性和 alpha 中心性,以便正确理解统计和社交网络中心性。

社交网络通常表示为连接的图(有向或无向)。因此,它还涉及图数据分析,其中人们充当节点,而连接或链接充当边缘。此外,从社交网络收集和分析大规模数据,然后开发预测性和描述性分析应用程序,例如 Facebook、Twitter 和 LinkedIn,也涉及社交网络数据分析,包括:链接预测,如预测关系或友谊,社交网络中的社区确定,如图上的聚类,以及确定网络中的意见领袖,如果在图数据上进行了适当的结构,则本质上是一个 PageRank 问题。

Spark 有专门用于图分析的 API,称为 GraphX。例如,可以使用此 API 搜索垃圾邮件,对搜索结果进行排名,在社交网络中确定社区,或搜索意见领袖,这并不是分析图形的应用方法的完整列表。我们将在本章后面更详细地讨论如何使用 GraphX。

使用 Spark 进行电影推荐

基于模型的协同过滤通常被许多公司使用,比如 Netflix,作为实时电影推荐的推荐系统。在本节中,我们将看到一个完整的示例,说明它是如何为新用户推荐电影的。

使用 Spark MLlib 进行基于模型的电影推荐

Spark MLlib 中的实现支持基于模型的协同过滤。在基于模型的协同过滤技术中,用户和产品由一小组因子描述,也称为潜在因子LFs)。然后使用这些 LFs 来预测缺失的条目。Spark API 提供了交替最小二乘(也被广泛称为ALS)算法的实现,用于通过考虑六个参数来学习这些潜在因子,包括:

  • numBlocks

  • rank

  • iterations

  • lambda

  • implicitPrefs

  • alpha

要了解更多关于这些参数的信息,请参考第五章中的推荐系统部分,通过示例进行监督和无监督学习。请注意,要使用默认参数构建 ALS 实例,可以根据您的需求设置值。默认值如下:numBlocks:-1,rank:10,iterations:10,lambda:0.01,implicitPrefs:false,alpha:1.0。

简而言之,构建 ALS 实例的方法如下:

  • 首先,ALS 是一个迭代算法,用于将评分矩阵建模为低排名用户和产品因子的乘积

  • 之后,通过最小化观察到的评分的重建误差,使用这些因子进行学习任务。

然而,未知的评分可以通过将这些因子相乘来逐步计算。

基于 Spark MLlib 中使用的协同过滤技术进行电影推荐或其他推荐的方法已被证明具有高预测准确性,并且在像 Netflix 这样的公司使用的商品集群上可扩展到数十亿的评分。通过遵循这种方法,Netflix 这样的公司可以根据预测的评分向其订阅者推荐电影。最终目标是增加销售,当然也是客户满意度。

数据探索

电影及其对应的评分数据集是从 MovieLens 网站(movielens.org)下载的。根据 MovieLens 网站上的数据描述,所有评分都在ratings.csv文件中描述。该文件的每一行(包括标题)代表一个用户对一部电影的评分。

CSV 数据集包含以下列:userIdmovieIdratingtimestamp,如图 2所示。行首先按userId在用户内部排序,然后按movieId排序。评分采用五星制,可以增加 0.5 星(0.5 星至 5.0 星)。时间戳表示自 1970 年 1 月 1 日协调世界时UTC)午夜以来的秒数,我们有 105,339 个评分,来自 668 个用户对 10,325 部电影进行评分:

数据探索

图 2:前 20 部电影的样本评分。

另一方面,电影信息包含在movies.csv文件中。除了标题信息之外,每一行代表一个电影,包含列:movieIdtitlegenres

电影标题可以手动创建、插入,或者从www.themoviedb.org/电影数据库网站导入。然而,发行年份显示在括号中。

由于电影标题是手动插入的,因此这些标题可能存在错误或不一致。因此,建议读者检查 IMDb 数据库(www.ibdb.com/)以确保没有不一致或不正确的标题及其对应的发行年份。

类型是一个分隔的列表,从以下类型类别中选择:

  • 动作,冒险,动画,儿童,喜剧,犯罪

  • 纪录片,戏剧,奇幻,黑色电影,恐怖,音乐

  • 神秘,浪漫,科幻,惊悚,西部,战争

数据探索

图 3:前 20 部电影的标题和类型。

使用 Spark MLlib 进行电影推荐

在本小节中,我们将通过逐步示例从数据收集到电影推荐来向您展示如何为其他用户推荐电影。从 Packt 的补充文件中下载movies.csvratings.csv文件,并将它们放在您的项目目录中。

步骤 1:配置您的 Spark 环境

以下是配置您的 Spark 环境的代码:

static SparkSession spark = SparkSession 
      .builder() 
      .appName("JavaLDAExample") 
         .master("local[*]") 
         .config("spark.sql.warehouse.dir", "E:/Exp/"). 
          getOrCreate(); 

步骤 2:加载、解析和探索电影和评分数据集

以下是代码示例:

String ratigsFile = "input/ratings.csv"; 
Dataset<Row> df1 = spark.read().format("com.databricks.spark.csv").option("header", "true").load(ratigsFile);     
Dataset<Row> ratingsDF = df1.select(df1.col("userId"), df1.col("movieId"), df1.col("rating"),  df1.col("timestamp")); 
ratingsDF.show(false); 

这段代码应该返回与图 2中相同的评分Dataset<Row>。另一方面,以下代码段显示了与图 3中相同的电影Dataset<Row>

String moviesFile = "input/movies.csv"; 
Dataset<Row> df2 = spark.read().format("com.databricks.spark.csv").option("header", "true").load(moviesFile); 
Dataset<Row> moviesDF = df2.select(df2.col("movieId"), df2.col("title"), df2.col("genres"));  
moviesDF.show(false); 

步骤 3:将两个数据集都注册为临时表

要注册这两个数据集,我们可以使用以下代码:

ratingsDF.createOrReplaceTempView("ratings"); 
moviesDF.createOrReplaceTempView("movies"); 

这将通过在内存中创建一个临时视图作为表来加快内存查询的速度。使用createOrReplaceTempView()方法创建的临时表的生命周期与用于创建此数据集的[[SparkSession]]相关联。

步骤 4:探索和查询相关统计数据

让我们来检查与评分相关的统计数据。只需使用以下代码行:

long numRatings = ratingsDF.count(); 
long numUsers = ratingsDF.select(ratingsDF.col("userId")).distinct().count(); 
long numMovies = ratingsDF.select(ratingsDF.col("movieId")).distinct().count(); 
System.out.println("Got " + numRatings + " ratings from " + numUsers + " users on " + numMovies + " movies."); 

您应该发现来自 668 个用户对 10,325 部电影的 105,339 个评分。现在,让我们获取最高和最低评分以及对电影进行评分的用户数量。

但是,您需要在上一步中刚刚在内存中创建的评分表上执行 SQL 查询。在这里进行查询很简单,类似于从 MySQL 数据库或 RDBMS 进行查询。

但是,如果您不熟悉基于 SQL 的查询,建议您查看 SQL 查询规范,以了解如何使用SELECT从特定表中进行选择,如何使用ORDER进行排序,以及如何使用JOIN关键字进行连接操作。

如果您了解 SQL 查询,您应该通过使用以下复杂的 SQL 查询得到一个新的数据集:

Dataset<Row> results = spark.sql("select movies.title, movierates.maxr, movierates.minr, movierates.cntu " + "from(SELECT ratings.movieId,max(ratings.rating) as maxr,"  + "min(ratings.rating) as minr,count(distinct userId) as cntu "  + "FROM ratings group by ratings.movieId) movierates " + "join movies on movierates.movieId=movies.movieId " + "order by movierates.cntu desc"); 
results.show(false); 

使用 Spark MLlib 进行电影推荐

图 4:最高和最低评分以及对电影进行评分的用户数量。

为了更深入地了解,我们需要了解更多关于用户及其评分的信息。现在让我们找出最活跃的用户以及他们对电影进行评分的次数:

Dataset<Row> mostActiveUsersSchemaRDD = spark.sql("SELECT ratings.userId, count(*) as ct from ratings " + "group by ratings.userId order by ct desc limit 10"); 
mostActiveUsersSchemaRDD.show(false); 

使用 Spark MLlib 进行电影推荐

图 5:单个用户提供的评分数量。

现在让我们看看特定用户,并找出比如说用户 668 给出了高于 4 分的电影:

Dataset<Row> results2 = spark.sql("SELECT ratings.userId, ratings.movieId," + "ratings.rating, movies.title FROM ratings JOIN movies "+ "ON movies.movieId=ratings.movieId " + "where ratings.userId=668 and ratings.rating > 4"); 
results2.show(false); 

使用 Spark MLlib 进行电影推荐

图 6:用户 668 的相关评分。

步骤 5:准备训练和测试评分数据并查看计数

以下是代码示例:

Dataset<Row> [] splits = ratingsDF.randomSplit(new double[] { 0.8, 0.2 }); 
Dataset<Row> trainingData = splits[0]; 
Dataset<Row> testData = splits[1]; 
long numTraining = trainingData.count(); 
long numTest = testData.count(); 
System.out.println("Training: " + numTraining + " test: " + numTest); 

您应该发现在训练集中有 84,011 个评分,在测试集中有 21,328 个评分。

步骤 6:准备数据以构建使用 ALS 的推荐模型

以下代码示例用于构建使用 API 的推荐模型:

JavaRDD<Rating> ratingsRDD = trainingData.toJavaRDD().map(new Function<Row, Rating>() { 
      @Override 
      public Rating call(Row r) throws Exception { 
        // TODO Auto-generated method stub 
        int userId = Integer.parseInt(r.getString(0)); 
        int movieId = Integer.parseInt(r.getString(1)); 
        double ratings = Double.parseDouble(r.getString(2)); 
        return new Rating(userId, movieId, ratings); 
      } 
    }); 

ratingsRDD RDD 将包含来自我们在上一步准备的训练数据集的userIdmovieId和相应的评分。另一方面,以下testRDD也包含来自我们在上一步准备的测试数据集的相同信息:

JavaRDD<Rating> testRDD = testData.toJavaRDD().map(new Function<Row, Rating>() { 
      @Override 
      public Rating call(Row r) throws Exception { 
        int userId = Integer.parseInt(r.getString(0)); 
        int movieId = Integer.parseInt(r.getString(1)); 
        double ratings = Double.parseDouble(r.getString(2)); 
        return new Rating(userId, movieId, ratings); 
      } 
    }); 

步骤 7:构建 ALS 用户产品矩阵

基于ratingsRDD构建一个 ALS 用户矩阵模型,指定 rank、iterations 和 lambda:

int rank = 20; 
int numIterations = 10; 
double lambda = 0.01; 
MatrixFactorizationModel model = ALS.train(JavaRDD.toRDD(ratingsRDD), rank, numIterations, 0.01); 

  • 请注意,我们已随机选择了 rank 的值为20,并且已对模型进行了 10 次学习迭代,lambda 为0.01。通过这个设置,我们得到了良好的预测准确性。建议读者应用超参数调整来了解这些参数的最佳值。

  • 然而,建议读者根据其数据集更改这两个参数的值。此外,如前所述,他们还可以使用和指定其他参数,如numberblockimplicitPrefsalpha,如果预测性能不理想。此外,将用户块和产品块的块数设置为pass -1,以将计算并行化为自动配置的块数。该值为-1

第 8 步:进行预测

让我们为用户 668 获取前六部电影的预测:

System.out.println("Rating:(UserID, MovieId, Rating)"); 
Rating[] topRecsForUser = model.recommendProducts(668, 6); 
for (Rating rating : topRecsForUser) 
System.out.println(rating.toString()); 

使用 Spark MLlib 进行电影推荐

图 7:用户 668 评分最高的六部电影。

第 9 步:获取预测评分以与测试评分进行比较

以下是所示的代码:

JavaRDD<Tuple2<Object, Object>> testUserProductRDD = testData.toJavaRDD() 
        .map(new Function<Row, Tuple2<Object, Object>>() { 
          @Override 
          public Tuple2<Object, Object> call(Row r) throws Exception { 

            int userId = Integer.parseInt(r.getString(0)); 
            int movieId = Integer.parseInt(r.getString(1)); 
            double ratings = Double.parseDouble(r.getString(2)); 
            return new Tuple2<Object, Object>(userId, movieId); 
          } 
        }); 
JavaRDD<Rating> predictionsForTestRDD = model.predict(JavaRDD.toRDD(testUserProductRDD)).toJavaRDD(); 

现在让我们检查 10 个用户的前 10 个预测:

System.out.println(predictionsForTestRDD.take(10).toString()); 

第 10 步:准备预测

在这里,我们将以两个步骤准备与 RDD 相关的预测。第一步包括从predictionsForTestRDD RDD 结构中准备比较的预测。步骤如下:

JavaPairRDD<Tuple2<Integer, Integer>, Double> predictionsKeyedByUserProductRDD = JavaPairRDD.fromJavaRDD( 
        predictionsForTestRDD.map(new Function<Rating, Tuple2<Tuple2<Integer, Integer>, Double>>() { 
          @Override 
          public Tuple2<Tuple2<Integer, Integer>, Double> call(Rating r) throws Exception { 
            return new Tuple2<Tuple2<Integer, Integer>, Double>( 
                new Tuple2<Integer, Integer>(r.user(), r.product()), r.rating()); 
          } 
        })); 

第二步包括准备测试以进行比较:

JavaPairRDD<Tuple2<Integer, Integer>, Double> testKeyedByUserProductRDD = JavaPairRDD  .fromJavaRDD(testRDD.map(new Function<Rating, Tuple2<Tuple2<Integer, Integer>, Double>>() { 
          @Override 
          public Tuple2<Tuple2<Integer, Integer>, Double> call(Rating r) throws Exception { 
            return new Tuple2<Tuple2<Integer, Integer>, Double>( 
                new Tuple2<Integer, Integer>(r.user(), r.product()), r.rating()); 
          } 
        })); 

第 11 步:将测试与预测结合起来,看看综合评分

testKeyedByUserProductRDDpredictionsKeyedByUserProductRDD RDD 连接起来,以获取每个用户和movieId的组合测试以及预测评分:

JavaPairRDD<Tuple2<Integer, Integer>, Tuple2<Double, Double>> testAndPredictionsJoinedRDD = testKeyedByUserProductRDD 
        .join(predictionsKeyedByUserProductRDD); 
System.out.println("(UserID, MovieId) => (Test rating, Predicted rating)"); 
System.out.println("----------------------------------"); 
for (Tuple2<Tuple2<Integer, Integer>, Tuple2<Double, Double>> t : testAndPredictionsJoinedRDD.take(6)) { 
      Tuple2<Integer, Integer> userProduct = t._1; 
      Tuple2<Double, Double> testAndPredictedRating = t._2; 
      System.out.println("(" + userProduct._1() + "," + userProduct._2() + ") => (" + testAndPredictedRating._1() 
          + "," + testAndPredictedRating._2() + ")"); 
    } 

使用 Spark MLlib 进行电影推荐

图 8:组合测试以及对每个用户和电影 ID 的预测评分。

第 12 步:评估模型对预测性能的表现

通过检查真正的正面预测和假阳性的数量来检查 ALS 模型的性能:

JavaPairRDD<Tuple2<Integer, Integer>, Tuple2<Double, Double>> truePositives = testAndPredictionsJoinedRDD 
        .filter(new Function<Tuple2<Tuple2<Integer, Integer>, Tuple2<Double, Double>>, Boolean>() { 
          @Override 
          public Boolean call(Tuple2<Tuple2<Integer, Integer>, Tuple2<Double, Double>> r) throws Exception { 
            return (r._2._1() <= 1 && r._2._2() < 5); 
          } 
        }); 

现在打印真正的正面预测数量。我们认为当预测评分低于最高评分(即5)时,预测是真正的预测。因此,如果预测评分大于或等于5,则将该预测视为假阳性:

for (Tuple2<Tuple2<Integer, Integer>, Tuple2<Double, Double>> t : truePositives.take(2)) { 
      Tuple2<Integer, Integer> userProduct = t._1; 
      Tuple2<Double, Double> testAndPredictedRating = t._2; 
    } 
System.out.println("Number of true positive prediction is: "+ truePositives.count()); 

您应该找到以下值。

真正的正面预测数量为 798。现在是时候打印假阳性的统计数据了:

JavaPairRDD<Tuple2<Integer, Integer>, Tuple2<Double, Double>> falsePositives = testAndPredictionsJoinedRDD 
        .filter(new Function<Tuple2<Tuple2<Integer, Integer>, Tuple2<Double, Double>>, Boolean>() { 
          @Override 
          public Boolean call(Tuple2<Tuple2<Integer, Integer>, Tuple2<Double, Double>> r) throws Exception { 
            return (r._2._1() <= 1 && r._2._2() >= 5); 
          } 
        }); 
for (Tuple2<Tuple2<Integer, Integer>, Tuple2<Double, Double>> t : falsePositives.take(2)) { 
      Tuple2<Integer, Integer> userProduct = t._1; 
      Tuple2<Double, Double> testAndPredictedRating = t._2; 
    } 
System.out.println("Number of false positive prediction is: "+ falsePositives.count()); 

在这个特定的例子中,我们只有 14 个假阳性预测,这是非常出色的。现在让我们通过测试和预测之间的平均绝对误差计算来检查预测的性能:

double meanAbsoluteError = JavaDoubleRDD        .fromRDD(testAndPredictionsJoinedRDD.values().map(new Function<Tuple2<Double, Double>, Object>() { 
          public Object call(Tuple2<Double, Double> pair) { 
            Double err = pair._1() - pair._2(); 
            return err * err; 
          } 
        }).rdd()).mean(); 
    System.out.printing("Mean Absolute Error: "+meanAbsoluteError); 

这将返回以下值:

Mean Absolute Error: 1.5800601618477566 

从流构建实时 ML 管道

根据 Spark 在spark.apache.org/docs/latest/streaming-programming-guide.html提供的 API 指南,技术上,Spark Streaming 接收实时输入数据流作为对象(对象可以是Java/Python/R对象)。随后,流被分成批次,然后由 Spark 引擎处理以生成批处理的最终输入流。为了使这个过程更加简单,Spark Streaming 提供了一个高级抽象,也称为离散流或 DStream。

DStream代表来自实时流数据源(如 Twitter、Kafka、Fume、Kinesis、传感器或其他源)的连续数据流。可以从这些源创建离散流,或者也可以对其他 DStreams 应用高级操作来进行处理。在内部,DStream 被表示为一系列 RDD,这意味着 RDD 抽象已被重用以处理 RDD 流。

如已在第六章中讨论的构建大规模机器学习管道,主题建模技术会自动推断讨论的主题,并将它们隐含地放置在文档集合中作为隐藏资源。这在自然语言处理NLP)和文本挖掘任务中常用。这些主题可以用于分析、总结和组织这些文档。或者,这些主题可以在机器学习ML)管道开发的后期阶段用于特征化和降维。最流行的主题建模算法是潜在狄利克雷分配LDA)和概率潜在语义分析pLSA)。之前我们讨论了如何应用 LDA 算法来处理已有的静态数据集。然而,如果主题建模是从实时流数据中准备的,那将会很棒,并且在了解 Twitter、LinkedIn 或 Facebook 等社交媒体趋势方面更加实时。

然而,由于 Facebook 或 LinkedIn 的有限 API 功能,从这些社交媒体平台收集实时数据将会很困难。Spark 还提供了用于访问来自 Twitter、Kafka、Flume 和 Kinesis 的数据的 API。从流数据开发近实时 ML 应用程序的工作流程应遵循图 9中呈现的工作流程:

从流中开发实时 ML 管道

图 9:使用 Spark 从流数据开发实时预测 ML 模型。

在本节中,我们将向您展示如何开发一个处理流数据的实时 ML 管道。更具体地说,我们将展示从 Twitter 流数据中逐步进行主题建模。这里的主题建模有两个步骤:Twitter 数据收集和使用 LDA 进行主题建模。

从 Twitter 收集实时推文数据

Spark 提供了 API 来访问来自 Twitter 时间线的实时推文。这些推文可以通过关键词或标签进一步制作。或者,也可以从某人的 Twitter 时间线下载推文。但是,在访问推文数据之前,您需要在 Twitter 上创建一个样本 Twitter 应用程序,并生成四个密钥:consumerKeyconsumerSecretaccessTokenaccessTokenSecret

提示

请注意,从 Spark 1.6.2 升级到 2.0.0 时,Twitter(和其他一些)驱动程序支持已被移除。这意味着在 Spark 2.0.0 中已删除了使用 Spark 进行流数据收集支持的较少使用的流连接器,包括 Twitter、Akka、MQTT 和 ZeroMQ。因此,不可能使用 Spark 2.0.0 开发用于 Twitter 数据收集的应用程序。因此,在本节中将使用 Spark 1.6.1 进行 Twitter 数据收集的演示。建议读者使用提供的 Maven 友好的pom.xml文件在 Eclipse 上创建一个 Maven 项目。

在对 Spark ML 应用程序进行身份验证以收集来自 Twitter 的数据之后,您将需要通过指定SparkConf和收集推文的持续时间来定义JavaStreamingContext。之后,通过使用 Spark 的TwitterUtils API,推文数据可以作为 DStream 或离散流下载,一旦使用JavaStreamingContext对象撤销start()方法。

开始接收推文后,您可以将推文数据保存在本地计算机、HDFS 或其他适用的文件系统上。但是,流将持续进行,直到您使用awaitTermination()终止流。

然而,接收到的推文也可以通过使用foreachRDD设计模式进行预处理或清理,然后可以保存到您想要的位置。由于页面限制,我们在这里限制了我们的讨论。

提示

此外,感兴趣的读者应该遵循 Spark Streaming 的 API 指南,网页地址为:spark.apache.org/docs/latest/streaming-programming-guide.html

使用 Spark 的 TwitterUtils API 进行推文收集

在这个小节中,首先,我们将向您展示如何使用TwitterUtils API 从 Twitter 收集实时推文数据。然后相同的推文数据将在下一个小节中用于主题建模。

步骤 1:加载所需的包和 API

以下是加载所需包的代码:

import org.apache.log4j.Level; 
import org.apache.log4j.Logger; 
import org.apache.spark.SparkConf; 
import org.apache.spark.api.java.JavaRDD; 
import org.apache.spark.api.java.function.Function; 
import org.apache.spark.streaming.Duration; 
import org.apache.spark.streaming.api.java.JavaDStream; 
import org.apache.spark.streaming.api.java.JavaStreamingContext; 
import org.apache.spark.streaming.twitter.TwitterUtils; 
import twitter4j.Status;  

步骤 2:设置日志记录器级别

为了设置日志记录器级别,我们使用以下代码:

Logger.getLogger("org").setLevel(Level.OFF); 
Logger.getLogger("akka").setLevel(Level.OFF); 
Logger.getLogger("org.apache.spark").setLevel(Level.WARN); 
Logger.getLogger("org.eclipse.jetty.server").setLevel(Level.OFF);  

步骤 3:设置 Spark 流环境

以下是 Spark 流的代码示例:

SparkConf conf = new SparkConf().setMaster("local[*]").setAppName("TwitterExample"); 
JavaStreamingContext jssc = new JavaStreamingContext(conf, new Duration(1000)); 

步骤 4:设置访问 Twitter 数据的身份验证

通过访问以下网址从示例 Twitter 应用程序获取认证值:apps.twitter.com/

String consumerKey = "VQINrM6ZcNqaCAawA6IN4xRTP"; 
String consumerSecret = "F2OsVEuJypOZSAoNDFrWgoCHyNJNXbTr8T3yEbp9cWEYjTctye"; 
String accessToken = "475468363-IfRcZnbkEVPRw6bwXovMnw1FsbxuetvEF2JvbAvD"; 
String accessTokenSecret = "vU7VtzZVyugUHO7ddeTvucu1wRrCZqFTPJUW8VAe6xgyf"; 

请注意,这里我们为这四个秘密密钥提供了相同的值。请根据自己的密钥替换这些值。现在,我们需要使用twitter4j.oauth为前面的四个密钥设置系统属性:

System.setProperty("twitter4j.oauth.consumerKey", consumerKey); 
System.setProperty("twitter4j.oauth.consumerSecret", consumerSecret); 
System.setProperty("twitter4j.oauth.accessToken", accessToken); 
System.setProperty("twitter4j.oauth.accessTokenSecret", accessTokenSecret); 

步骤 5:启用检查点

以下代码显示了如何启用检查点:

jssc.checkpoint("src/main/resources/twitterdata/"); 

元数据检查点主要用于从驱动程序故障中恢复,而数据或 RDD 检查点即使在使用有状态转换时也是基本功能所必需的。有关更多详细信息,请访问 Spark 的以下网页:spark.apache.org/docs/latest/streaming-programming-guide.html#checkpointing

步骤 6:开始接受推文流作为离散流

让我们只收集 100 条推文以简化,但可以收集任意数量的推文:

JavaDStream<Status> tweets = TwitterUtils.createStream(jssc); 
final String outputDirectory="src/main/resources/twitterdata/"; 
final long numTweetsToCollect = 100; 

步骤 7:筛选推文并保存为普通文本文件

筛选推文的代码显示在这里:

tweets.foreachRDD(new Function<JavaRDD<Status>, Void>() { 
      public long numTweetsCollected = 0; 
      @Override 
      public Void call(JavaRDD<Status> status) throws Exception {         
        long count = status.count(); 
        if (count > 0) { 
          status.saveAsTextFile(outputDirectory + "/tweets_" + System.currentTimeMillis()); 
          numTweetsCollected += count; 
          if (numTweetsCollected >= numTweetsToCollect) { 
               System.exit(0); 
          } 
        } 
        return null; 
      } 
    }); 

在这里,我们使用单例方法foreachRDD对推文进行预处理,该方法仅接受经过筛选的推文,即,如果状态计数至少为 1。当收集的推文数量等于或多于要收集的推文数量时,我们退出收集。最后,我们将推文保存为文本文件在输出目录中。

步骤 8:控制流开关

控制流开关的代码显示在这里:

jssc.start(); 
jssc.awaitTermination();  

最终,我们将在下一步中使用这些推文的文本进行主题建模。如果您还记得第六章中的主题建模,构建可扩展的机器学习管道,我们看到了相应的术语权重、主题名称和术语索引。然而,我们还需要实际的术语。在下一步中,我们将展示详细的检索术语的技术,这取决于需要为此创建的词汇表。

使用 Spark 进行主题建模

在这个小节中,我们使用 Spark 表示了一种半自动的主题建模技术。以下步骤展示了从数据读取到打印主题及其术语权重的主题建模。

步骤 1:加载必要的包和 API

以下是加载必要包的代码:

import java.io.Serializable; 
import java.util.ArrayList; 
import java.util.HashMap; 
import java.util.Map; 
import org.apache.log4j.Level; 
import org.apache.log4j.Logger; 
import org.apache.spark.SparkConf; 
import org.apache.spark.api.java.JavaPairRDD; 
import org.apache.spark.api.java.JavaRDD; 
import org.apache.spark.api.java.JavaSparkContext; 
import org.apache.spark.api.java.function.Function; 
import org.apache.spark.api.java.function.Function2; 
import org.apache.spark.api.java.function.PairFlatMapFunction; 
import org.apache.spark.api.java.function.PairFunction; 
import org.apache.spark.ml.feature.StopWordsRemover; 
import org.apache.spark.mllib.clustering.LDA; 
import org.apache.spark.mllib.clustering.LDAModel; 
import org.apache.spark.mllib.linalg.Vector; 
import org.apache.spark.mllib.linalg.Vectors; 
import org.apache.spark.sql.SQLContext; 
import scala.Tuple2; 

步骤 2:配置 Spark 环境

以下是配置 Spark 的代码:

private transient static SparkConf sparkConf = new SparkConf().setMaster("local[*]").setAppName("TopicModelingLDA"); 
private transient static JavaSparkContext jsc = new JavaSparkContext(sparkConf); 
private transient static SQLContext sqlContext = new org.apache.spark.sql.SQLContext(jsc); 

步骤 3:设置日志级别

以下是设置日志级别的代码:

Logger.getLogger("org").setLevel(Level.OFF); 
Logger.getLogger("akka").setLevel(Level.OFF); 
Logger.getLogger("org.apache.spark").setLevel(Level.WARN); 
Logger.getLogger("org.eclipse.jetty.server").setLevel(Level.OFF); 

注意

请注意,设置刚刚显示的日志级别是可选的。

步骤 4:创建 Java RDD 并将它们缓存在内存中

创建 Java RDD 并将它们缓存在上一步的推文数据中:

JavaRDD<String> data = jsc.wholeTextFiles("src/main/resources/test/*.txt") 
        .map(new Function<Tuple2<String, String>, String>() { 
          @Override 
          public String call(Tuple2<String, String> v1) throws Exception {     
            return v1._2; 
          } 
        }).cache(); 

步骤 5:对术语进行标记化

加载 Spark 提供的停用词列表,并通过应用三个约束条件对术语进行标记化:文本长度至少为 4,不是停用词,并将它们转换为小写。请注意,我们在第六章中讨论了停用词,构建可扩展的机器学习管道

public static String[] stopwords = new StopWordsRemover().getStopWords(); 
JavaRDD<String[]> tokenized = data.map(new Function<String, String[]>() { 
list.toArray(new String[0]); 
      }      @Override 
      public String[] call(String v1) throws Exception { 
        ArrayList<String> list = new ArrayList<>(); 
        for (String s : v1.split("\\s")) { 
          if (s.length() > 3 && !isStopWord(s) && isOnlyLetter(s)) 
            list.add(s.toLowerCase()); 
        } 
        return 
    }); 

步骤 6:准备术语计数

通过应用四个约束条件对术语计数进行准备:文本长度至少为 4,不是停用词,仅选择字符,并将它们全部转换为小写:

JavaPairRDD<String, Integer> termCounts = data 
        .flatMapToPair(new PairFlatMapFunction<String, String, Integer>() { 
          @Override 
          public Iterable<Tuple2<String, Integer>> call(String t) throws Exception {     
            ArrayList<Tuple2<String, Integer>> tc = new ArrayList<>();   
            for (String s : t.split("\\s")) { 

              if (s.length() > 3 && !isStopWord(s) && isOnlyLetter(s)) 
                tc.add(new Tuple2<String, Integer>(s.toLowerCase(), 1)); 
            } 
            return tc; 
          } 
        }).reduceByKey(new Function2<Integer, Integer, Integer>() { 
          @Override 
          public Integer call(Integer v1, Integer v2) throws Exception {     
            return v1 + v2; 
          } 
        }); 

请注意,这里的isStopWords()isOnlyLetters()是两个用户定义的方法,将在本步骤末尾讨论。

步骤 7:对术语计数进行排序

通过应用两个转换sortByKey()mapToPair()对术语计数进行排序:

JavaPairRDD<String, Integer> termCountsSorted = termCounts 
        .mapToPair(new PairFunction<Tuple2<String, Integer>, Integer, String>() { 
          @Override 
          public Tuple2<Integer, String> call(Tuple2<String, Integer> t) throws Exception { 
            return t.swap(); 
          } 
        }).sortByKey().mapToPair(new PairFunction<Tuple2<Integer, String>, String, Integer>() { 
          @Override 
          public Tuple2<String, Integer> call(Tuple2<Integer, String> t) throws Exception { 
            return t.swap(); 
          } 
        }); 

步骤 8:创建词汇表

通过映射排序的术语计数创建词汇表 RDD。最后,打印键值对:

JavaRDD<String> vocabArray = termCountsSorted.map(new Function<Tuple2<String, Integer>, String>() { 
      @Override 
      public String call(Tuple2<String, Integer> v1) throws Exception { 
        return v1._1; 
      } 
    }); 
final Map<String, Long> vocab = vocabArray.zipWithIndex().collectAsMap(); 
    for (Map.Entry<String, Long> entry : vocab.entrySet()) { 
      System.out.println(entry.getKey() + "/" + entry.getValue()); 
    } 

让我们看一下图 10中显示的词汇表术语及其索引的屏幕截图:

使用 Spark 进行主题建模

图 10:词汇表术语及其索引。

步骤 9:从标记化的单词/术语中创建文档矩阵

通过映射我们在上一步中创建的词汇表,将文档矩阵创建为JavaPairRDD。之后,将 RDD 缓存在内存中以加快处理速度:

JavaPairRDD<Long, Vector> documents = JavaPairRDD 
        .fromJavaRDD(tokenized.zipWithIndex().map(new Function<Tuple2<String[], Long>, Tuple2<Long, Vector>>() { 
          @Override 
          public Tuple2<Long, Vector> call(Tuple2<String[], Long> v1) throws Exception { 
            String[] tokens = v1._1; 
            Map<Integer, Double> counts = new HashMap(); 

            for (String s : tokens) { 
              if (vocab.containsKey(s)) { 
                long idx = vocab.get(s); 
                int a = (int) idx; 
                if (counts.containsKey(a)) { 
                  counts.put(a, counts.get(a) + 1.0); 
                } else 
                  counts.put(a, 0.0); 
              } 
            } 
            ArrayList<Tuple2<Integer, Double>> ll = new ArrayList<>(); 
            ArrayList<Double> dd = new ArrayList<>(); 

            for (Map.Entry<Integer, Double> entry : counts.entrySet()) { 
              ll.add(new Tuple2<Integer, Double>(entry.getKey(), entry.getValue())); 
              dd.add(entry.getValue()); 
            } 

            return new Tuple2<Long, Vector>(v1._2, Vectors.sparse(vocab.size(), ll)); 
          } 
        })).cache(); 

步骤 10:训练 LDA 模型

使用步骤 9 中的文档矩阵训练 LDA 模型,并简要描述四个主题的 10 个主题术语。

请注意,这里我们使用了潜在狄利克雷分配LDA),这是最常用于文本挖掘的主题建模算法之一。我们可以使用更健壮的主题建模算法,如概率潜在情感分析pLSA)、Pachinko 分配模型PAM)或分层狄利克雷过程HDP)算法。但是,pLSA 存在过拟合问题。

另一方面,HDP 和 PAM 是更复杂的主题建模算法,用于复杂的文本挖掘,例如从高维文本数据或非结构化文档中挖掘主题。此外,迄今为止,Spark 仅实现了一种主题建模算法,即 LDA。因此,我们必须合理使用 LDA:

LDAModel ldaModel = new LDA().setK(4).setMaxIterations(10).run(documents); 
Tuple2<int[], double[]>[] topicDesces = ldaModel.describeTopics(10); 
int topicCount = topicDesces.length; 

请注意,为了使主题生成简单,我们将主题数量设置为 4,并迭代 LDA 10 次。另一个原因是,在下一节中,我们想展示如何通过它们的共同术语连接这四个主题。建议读者根据自己的需求更改值。

步骤 11:获取主题术语、索引、术语权重和每个主题的总和

从步骤 10 和步骤 8 中描述的词汇表和主题描述中获取这些统计信息:

for (int t = 0; t < topicCount; t++) { 
      Tuple2<int[], double[]> topic = topicDesces[t]; 
      System.out.println("      Topic: " + t); 
      int[] indices = topic._1(); 
      double[] values = topic._2(); 
      double sum = 0.0d; 
      int wordCount = indices.length; 
      System.out.println("Terms |\tIndex |\tWeight"); 
      System.out.println("------------------------"); 
      for (int w = 0; w < wordCount; w++) { 
        double prob = values[w]; 
        int vocabIndex = indices[w]; 
        String vocabKey = ""; 
        for (Map.Entry<String, Long> entry : vocab.entrySet()) { 
          if (entry.getValue() == vocabIndex) { 
            vocabKey = entry.getKey(); 
            break; 
          } } 
System.out.format("%s \t %d \t %f \n", vocabKey, vocabIndex, prob); 
        sum += prob; 
      } 
      System.out.println("--------------------"); 
      System.out.println("Sum:= " + sum); 
      System.out.println(); 
    }  } 

如果仔细查看前面的代码段,vocabKey表示相应的主题术语,vocabIndex是索引,prob表示主题中每个术语的权重。打印语句已用于格式化输出。现在让我们看一下描述四个主题的输出,以简化图 11

使用 Spark 进行主题建模

图 11:描述四个主题。

正如我们在步骤 6中提到的,这里我们将展示如何开发isStopWord()方法。只需使用以下代码:

public static boolean isStopWord(String word) { 
    for (String s : stopwords) { 
      if (word.equals(s))   
        return true; 
    } 
    return false; 
  } 

isOnlyLetters()方法如下:

public static boolean isOnlyLetter(String word) { 
    for (Character ch : word.toCharArray()) { 
      if (!Character.isLetter(ch)) 
        return false; 
    } 
    return true; 
  } 

在下一节中,我们将介绍如何使用 Spark 的 GraphX API 解析和处理大规模图形数据,以从图 10中得到的主题数据中找到连接的组件。

图数据上的 ML 管道和半监督图形学习

由于大数据的泛滥,存在大量未标记的数据,以及非常少量的标记数据。正如已经讨论的那样,对这些数据进行标记和注释在计算上是昂贵的,并且是发现数据真正见解的障碍。此外,社交网络和媒体生产者图数据的增长也在规模上增长。这些数据努力开发实时和大规模的监督学习方法,可以利用输入分布中的信息。

基于图的半监督学习的想法是构建连接相似数据点或组件的图。这使得隐藏和未观察到的标签成为该图的节点上的随机变量。在这种类型的学习中,相似的数据点可以具有相似的标签,并且信息会从标记的数据点传播到其他数据点。类似的限制也限制了在典型处理和分析管道中表达许多重要步骤的能力。然而,基于图的学习并不适用于迭代扩散技术,例如 PageRank,因为许多计算方面是潜在的并且涉及到。

此外,由于 API 限制和不可用性,我们将不会详细讨论这种基于图的机器学习,并提供合适的示例。

然而,在本节中,我们将提供基于图的半监督应用程序开发,这基本上是我们在上一节中介绍的主题建模的延续。

GraphX 简介

GraphX 是 Spark 中用于图处理、图分析、图可视化和图并行计算的相对较新的组件。实际上,通过引入新的图抽象层,作为具有弹性分布式图计算的 Spark RDD 的原始计算方面得到了扩展,该图抽象层在图处理和存储中提供了弹性属性。

为了提供与图相关的计算,GraphX 公开了一组基本运算符,如子图、jointVerticesaggregateMessages。除此之外,它还继承了 GraphX 实现中 Pregel API 的优化变体。

此外,为了简化图分析任务,GraphX 正在丰富并增加一系列图算法和构建器。

在下一节中,我们将介绍如何使用 Spark 的 GraphX API 解析和处理大规模图数据,以从图 11中获得的主题数据中找到连接的组件。

使用 GraphX API 获取和解析图数据

在这个小节中,我们将向您展示如何使用 GraphX API 解析图数据,然后在下一个小节中描述图中的连接组件。由于 GraphX 中的 API 限制,我们无法在 Java 中提供相同的实现,但我们在 Scala 实现中做到了。

要运行以下源代码,请转到您的 Spark 分发并通过提供以下命令启动 Spark shell:

$ cd home/spark-2.0.0-bin-hadoop2.7/bin
$./spark-shell

然后 Spark shell 将可用于 Spark 会话。我们假设您的 Spark 分发保存在home/spark-2.0.0-bin-hadoop2.7路径中。请根据需要更改路径,以便运行 Spark shell。另外,请将图 11中显示的主题术语保存到单独的文本文件中,以便在继续以下步骤之前,您可以使用这些术语来分析作为图数据:

步骤 1:加载所需的包和 API

以下是加载所需包的代码:

package com.examples.graphs 
import org.apache.spark._ 
import org.apache.spark.graphx._ 
import org.apache.spark.rdd.RDD  

步骤 2:准备 Spark 环境

以下是准备 Spark 环境的代码:

val conf = new SparkConf().setAppName("GraphXDemo").setMaster("local[*]") 
val sc = new SparkContext(conf) 

步骤 3:解析主题术语并对其进行标记

以下代码说明了解析主题术语的方法:

val corpus: RDD[String] = sc.wholeTextFiles("home/ /topics/*.txt").map(_._2) 
val tokenized: RDD[Seq[String]] = corpus.map(_.toLowerCase.split("\\s")) 
tokenized.foreach { x => println(x) } 

步骤 4:创建文档的 RDD

创建文档的 RDD,格式为 RDD[(DocumentID,(nodeNamewordCount))]。例如,RDD[(1L,(Topic_0,4))]:

val nodes: RDD[(VertexId, (String, Long))] = tokenized.zipWithIndex().map{  
      case (tokens, id) => 
        val nodeName="Topic_"+id; 
        (id, (nodeName, tokens.size)) 

    } 
    nodes.collect().foreach{ 
      x =>  
        println(x._1+": ("+x._2._1+","+x._2._2+")")        
    } 

前面的打印方法生成了如图 12 所示的输出:

使用 GraphX API 获取和解析图数据

图 12:节点。

步骤 5:创建一个单词文档对

这是创建单词文档对的代码:

val wordPairs: RDD[(String, Long)] = 
      tokenized.zipWithIndex.flatMap { 
        case (tokens, id) => 
          val list = new Array(String, Long) 

          for (i <- 0 to tokens.length - 1) { 
            //tokens.foreach { term => 
            list(i) = (tokens(i), id) 
          } 
          list.toSeq 
      } 
    wordPairs.collect().foreach(x => println(x)) 
    println(wordPairs.count()) 

步骤 6:创建节点之间的图关系

以下代码显示了如何创建节点之间的图关系:

val relationships: RDD[Edge[String]] = wordPairs.groupByKey().flatMap{ 
      case(edge, nodes)=> 
        val nodesList = nodes.toArray 
        val list = new Array[Edge[String]](nodesList.length * nodesList.length) 
        if (nodesList.length>1){                   
          var count:Int=0; 
          for (i <- 0 to nodesList.length-2) { 
            for(j<-i+1 to nodesList.length-1){ 
         list(count) = new Edge(nodesList(i), nodesList(j), edge)  
         //list(count+1) = new Edge(nodesList(j), nodesList(i), edge) 
              count += 1; 
              //count += 2; 
            } 
          } 
        } 
        list.toSeq 
    }.filter { x => x!=null } 
    relationships.collect().foreach { x => println(x) } 

注意:如果要使图连接,但不是无向的,只需启用以下行:

list(count+1) = new Edge(nodesList(j), nodesList(i), edge) 

在以下行之后立即:

list(count) = new Edge(nodesList(i), nodesList(j), edge)  

将计数增加 2,即count += 2,以使更改保持一致。

步骤 7:初始化图

这里所示的代码显示了如何说明图:

val graph = Graph(nodes, relationships) 
println(graph.edges.count) 

查找连接的组件

根据spark.apache.org/docs/latest/graphx-programming-guide.html中的 API 文档,图的每个连接组件都是通过最低编号的顶点 ID 使用连接组件算法进行标记的。例如,在社交网络分析中,集群是由连接组件近似的。为了使这更加简单和快速,GraphX API 包含了算法的实现,即ConnectedComponents对象。

然而,目前没有用于查找连接组件的基于 Java、Python 或 R 的实现。因此,它允许我们通过 LDA 算法计算我们已经计算过的主题中的连接组件,如下所示:

val facts: RDD[String] = 
      graph.triplets.map(triplet => 
        triplet.srcAttr._1 + " contains the terms "" + triplet.attr + "" like as " + triplet.dstAttr._1) 
    facts.collect.foreach(println(_)) 

这应该产生如图 13所示的输出:

查找连接的组件

图 13:使用 GraphX 连接主题之间的关系。

如果仔细查看图 13中的输出,我们打印了一个三元组的关系。值得注意的是,除了支持顶点和边之外,Spark GraphX 还有三元组的概念。更具体地说,三元组是一个扩展 Edge 对象的对象。从图的角度来看,它存储了有关图中的边和相关顶点的信息。

摘要

在本章中,我们展示了如何从实时 Twitter 流数据和图数据开发大规模机器学习应用程序。我们讨论了社交网络和时间序列数据分析。此外,我们还使用 Spark MLlib 的基于内容的协同过滤算法开发了一个新兴的推荐应用程序,为用户推荐电影。然而,这些应用程序可以扩展和部署到其他用例中。

值得注意的是,当前的 Spark 实现包含了一些用于流或网络数据分析的算法。然而,我们希望 GraphX 在未来得到改进,并且不仅限于 Scala,还可以扩展到 Java、R 和 Python。在下一章中,我们将重点介绍如何与外部数据源交互,使 Spark 工作环境更加多样化。

第十章:配置和使用外部库

本章指导您如何使用外部库来扩展数据分析,使 Spark 更加多功能。将提供示例,用于部署第三方开发的软件包或库,用于 Spark 核心和 ML/MLlib 的机器学习应用。我们还将讨论如何编译和使用外部库与 Spark 的核心库进行时间序列分析。如约定,我们还将讨论如何配置 SparkR 以增加探索性数据操作和操作。简而言之,本章将涵盖以下主题:

  • 使用 Spark 的第三方 ML 库

  • 在集群上部署 Spark ML 时使用外部库

  • 使用 Cloudera 的 Spark-TS 包进行时间序列分析

  • 使用 RStudio 配置 SparkR

  • 在 Windows 上配置 Hadoop 运行时

为了为开发人员提供用户友好的环境,还可以将第三方 API 和库与 Spark Core 和其他 API(如 Spark MLlib/ML、Spark Streaming、GraphX 等)结合使用。感兴趣的读者应该参考 Spark 网站上列出的以下网站,该网站被列为第三方软件包spark-packages.org/

这个网站是 Apache Spark 的第三方软件包的社区索引。截至目前,该网站上注册了总共 252 个软件包,如表 1所示:

领域 软件包数量 URL
Spark 核心 9 spark-packages.org/?q=tags%3A%22Core%22
数据源 39 spark-packages.org/?q=tags%3A%22Data%20Sources%22
机器学习 55 spark-packages.org/?q=tags%3A%22Machine%20Learning%22
36 spark-packages.org/?q=tags%3A%22Streaming%22
图处理 13 spark-packages.org/?q=tags%3A%22Graph%22
使用 Python 的 Spark 5 spark-packages.org/?q=tags%3A%22PySpark%22
集群部署 10 spark-packages.org/?q=tags%3A%22Deployment%22
数据处理示例 18 spark-packages.org/?q=tags%3A%22Examples%22
应用程序 10 spark-packages.org/?q=tags%3A%22Applications%22
工具 24 spark-packages.org/?q=tags%3A%22Tools%22
总软件包数量:252

表 1:基于应用领域的 Spark 第三方库

使用 Spark 的第三方 ML 库

55 个第三方机器学习库包括神经数据分析库、广义聚类库、流处理库、主题建模库、特征选择库、矩阵分解库、分布式 DataFrame 库、模型矩阵库、用于 Spark 的 Stanford Core NLP 包装器、社交网络分析库、深度学习模块运行库、基本统计汇编库、二元分类器校准库和 DataFrame 的标记器。

表 2提供了基于使用情况和机器学习应用领域的最有用的软件包的摘要。感兴趣的读者应该访问相应的网站以获取更多见解:

Spark 的第三方 ML 库 使用情况
thunderScalaNetwork 神经网络使用 Scala 进行大规模神经数据分析的 Spark 实现。
generalized-kmeans-clusteringpatchworkbisecting-kmeansspark-knn 聚类这个项目将 Spark MLLIB K 均值聚类泛化,以支持任意距离函数。用于 Spark MLlib 的高度可扩展的网格密度聚类算法。这是 Bisecting K-Means 聚类在 Spark 上的原型实现。在 Spark 上的 k 最近邻算法。
spark-ml-streamingstreaming-matrix-factorizationtwitter-stream-ml 在 Spark 中可视化流式机器学习。使用矩阵分解和用户产品偏差的流式推荐引擎。在 Twitter 的流上进行机器学习。使用 Apache Spark,Web 服务器和 Lightning 图形服务器。
pipeline 基于 Docker 的管道化使用 Spark、Spark SQL、Spark Streaming、ML、MLlib、GraphX、Kafka、Cassandra、Redis、Apache Zeppelin、Spark-Notebook、iPython/Jupyter Notebook、Tableau、H2O Flow 和 Tachyon 的端到端、实时、高级分析大数据参考管道。
dllibCaffeOnSparkdl4j-spark-ml 深度学习 dllib 是在 Apache Spark 上运行的深度学习工具。用户需要下载工具作为.jar 文件,然后可以与 Spark 集成并开发基于深度学习的应用程序。CaffeOnSpark 是在 Spark 执行器上运行的可扩展的深度学习。它基于点对点(P2P)通信。dl4j-spark-ml可以通过与 Spark ML 集成来开发基于深度学习的 ML 应用程序。
kNN_ISsparkboostspark-calibration 分类 kNN-IS:用于大数据的 k 最近邻分类器的迭代式 Spark 设计。使用 Apache Spark 进行 AdaBoost.MH 和 MP-Boost 的分布式实现。在 Spark 中评估二元分类器的校准(即分类器输出与观察到的类比例匹配程度)。
Zen 回归 Zen 提供了一个在 Spark 上进行大规模高效机器学习的平台。例如,逻辑回归、线性回归、潜在狄利克雷分配(LDA)、因子分解机和深度神经网络(DNN)都在当前版本中实现了。
modelmatrixspark-infotheoretic-feature-selection 特征工程 spark-infotheoretic-feature-selection 工具为开发大规模机器学习应用提供了一种替代方案。它通过管道提供了稳健的特征工程,包括特征提取器和特征选择器。它专注于构建基于稀疏特征向量的管道。另一方面,它可以作为基于信息理论的特征选择框架。基于信息理论的算法包括 mRMR、InfoGain、JMI 和其他常用的 FS 过滤器。
spark-knn-graphs 图处理 Spark 算法用于构建和处理 k-nn 图
TopicModeling 主题建模在 Apache Spark 上进行分布式主题建模
Spark.statistics 统计除了 SparkR,Spark.statistics 作为基于 Spark 核心的基本统计实现的组装程序

表 2:基于 Spark 的机器学习用例和应用领域的最有用的第三方包的总结

使用 Spark Core 的外部库

为了使用这些外部库,而不是将 jar 文件放在任何特定的文件夹中,一个简单的解决方法是使用以下参数启动pyspark shell 或 spark-shell:

bin/pyspark --packages com.databricks:spark-csv_2.10:1.0.3
bin/spark-shell --packages com.databricks:spark-csv_2.10:1.0.3

这将自动加载所需的spark-csv jar 文件。但是,这两个 jar 文件必须使用以下命令在 Ubuntu 中下载到 Spark 分发中:

wget http://search.maven.org/remotecontent?filepath=org/apache/commons/commons-csv/1.1/commons-csv-1.1.jar
wget http://search.maven.org/remotecontent?filepath=com/databricks/spark-csv_2.10/1.0.0/spark-csv_2.10-1.0.0.jar

然后,要创建一个活跃的 Spark 会话,使用以下代码行:

static SparkSession spark = SparkSession 
        .builder() 
        .appName("JavaLDAExample") 
          .master("local[*]") 
          .config("spark.sql.warehouse.dir", "C:/Exp/")               
          .getOrCreate(); 

一旦您实例化了一个活跃的 Spark 会话,使用以下代码行来读取 csv 输入文件:

String input = "input/letterdata.data"; 
Dataset<Row> df = spark.read().format("com.databricks.spark.csv").option("header", "true").load(input);  
df.show();   

请注意,我们在这里使用format()方法定义了com.databricks.spark.csv输入格式,这是由 Databricks 专门开发的,用于更快的 CSV 文件读取和解析,并使用option()方法将辅助选项设置为 true。最后,load()方法从input/letterdata.data位置加载输入数据,例如。

接下来,在下一节中,我们将讨论配置 Spark-TS 库以进行时间序列数据分析。

提示

感兴趣的读者应该访问 Spark 的第三方 ML 包网页spark-packages.org/?q=tags%3A%22Machine%20Learning%22了解特定包的讨论、更新和配置程序。

使用 Cloudera Spark-TS 包进行时间序列分析

正如在第九章中讨论的,我们将看到如何配置由 Cloudera 开发的 Spark-TS 包。主要是,我们将在本节讨论 TimeSeriesRDD。

时间序列数据

时间序列数据由一系列测量组成,每个测量发生在某个时间点。有许多术语用于描述时间序列数据,其中许多适用于冲突或重叠的概念。为了清晰起见,在 Spark-TS 中,Cloudera 坚持使用特定的词汇。时间序列数据分析中有三个重要的对象:时间序列、瞬时和观察:

  • 时间序列是一系列实数(即浮点数)值,每个值都与特定的时间戳相关联。特别是,这与时间序列的含义一致,即一元时间序列。在 Scala 中,时间序列通常由 Breeze 在github.com/scalanlp/breeze中表示的向量表示,在 Python 中,是一个 1-D NumPy 数组(更多信息请参考www.numpy.org/),并且具有DateTimeIndex,如github.com/sryza/spark-timeseries/blob/master/src/main/scala/com/cloudera/sparkts/DateTimeIndex.scala所示。

  • 另一方面,瞬时是与单个时间点对应的时间序列集合中的值向量。在 Spark-TS 库中,每个时间序列通常都带有一个键,使其能够在时间序列集合中被识别。

  • 最后,观察是一个元组(时间戳,键,值),也就是时间序列或瞬时中的单个值。

然而,并非所有带有时间戳的数据都是时间序列数据。例如,日志不直接适用于时间序列,因为它们由离散事件组成,而不是在间隔中进行的标量测量。但是,每小时的日志消息测量将构成一个时间序列。

配置 Spark-TS

从 Scala 中访问 Spark-TS 的最直接的方法是在 Maven 项目中依赖它。通过在pom.xml中包含以下 repo 来实现:

<repositories> 
    <repository> 
      <id>cloudera-repos</id> 
      <name>Cloudera Repos</name> 
      <url>https://repository.cloudera.com/artifactory/cloudera-repos/</url> 
    </repository> 
</repositories> 
 And including the following dependency in the pom.xml:  
<dependency> 
      <groupId>com.cloudera.sparkts</groupId> 
      <artifactId>sparkts</artifactId> 
      <version>0.1.0</version> 
</dependency> 

要获取原始的pom.xml文件,感兴趣的读者应该访问以下网址:

github.com/sryza/spark-timeseries/blob/master/pom.xml

或者,要在 spark-shell 中访问它,请从repository.cloudera.com/cloudera/libs-release-local/com/cloudera/sparkts/sparkts/0.1.0/sparkts-0.1.0-jar-with-dependencies.jar下载 JAR 文件,然后按照本章的使用 Spark Core 的外部库部分中讨论的命令启动 shell。

spark-shell \
 --jars sparkts-0.1.0-jar-with-dependencies.jar \
 --driver-class-path sparkts-0.1.0-jar-with-dependencies.jar

TimeSeriesRDD

根据 Cloudera 网站上的 Spark-TS 工程博客blog.cloudera.com/blog/2015/12/spark-ts-a-new-library-for-analyzing-time-series-data-with-apache-spark/,TimeSeriesRDD 是 Spark-TS 的核心,RDD 中的每个对象存储完整的单变量系列。对于时间序列专用的操作效率更高。例如,如果您想从原始时间序列集生成一组滞后时间序列,每个滞后序列可以通过查看输入 RDD 中的单个记录来计算。

同样,通过根据周围值填充缺失值或为每个系列拟合时间序列模型,所有所需的数据都存在于单个数组中。因此,Spark-TS 库的核心抽象是 TimeSeriesRDD,它只是可以以分布式方式操作的时间序列集合。这种方法允许您避免为每个系列存储时间戳,而是存储一个单一的DateTimeIndex,所有系列向量都符合该索引。TimeSeriesRDD[K]扩展了RDD[(K, Vector[Double])],其中 K 是键类型(通常是字符串),元组中的第二个元素是表示时间序列的 Breeze 向量。

可以在 GitHub 网址github.com/sryza/spark-timeseries找到更多技术讨论。由于这是一个第三方包,详细讨论超出了本书的范围,我们认为。

使用 RStudio 配置 SparkR

假设您的计算机上安装了 RStudio。按照这里提到的步骤:

  1. 现在打开 RStudio 并创建一个新的 R 脚本;然后编写以下代码:
      SPARK_HOME = "/home/spark-2.0.0-bin-hadoop2.7/R/lib" 
      Sys.setenv(SPARK_MEM="8g") 
      Sys.setenv(SPARK_HOME = "/home/spark-2.0.0-bin-hadoop2.7") 
      .libPaths(c(file.path(Sys.getenv("SPARK_HOME"), "R",
      "lib"),.libPaths())) 

  1. 通过使用以下代码加载 SparkR 所需的软件包:
      library(SparkR, lib.loc = SPARK_HOME)
      ibrary(SparkR) 

  1. 配置 SparkR 环境如下:
      sc <- sparkR.init(appName = "SparkR-DataFrame-example", master =
      "local")
      sqlContext <- sparkRSQL.init(sc) 

  1. 现在让我们创建第一个 DataFrame 并打印前几行,如下所示:
      df <- createDataFrame(sqlContext, faithful) 
      head(df) 

  1. 您可能需要安装以下软件包才能使devtools软件包正常工作:
      install.packages("xml2", dependencies = TRUE) 
      install.packages("Rcpp", dependencies = TRUE) 
      install.packages("plyr", dependencies = TRUE) 
      install.packages("devtools", dependencies = TRUE) 
      install.packages("MatrixModels", dependencies = TRUE) 
      install.packages("quantreg", dependencies = TRUE)  
      install.packages("moments", dependencies = TRUE) 
      install.packages("xml2") 
      install.packages(c("digest", "gtable", "scales", "rversions",
      "lintr")) 

  1. 此外,您可能需要安装libcurl来支持 devtools 所依赖的 RCurl。只需运行此命令:
 sudo apt-get -y build-dep libcurl4-gnutls-dev 
      sudo apt-get install libcurl4-gnutls-dev 
      sudo apt-get install r-cran-plyr 
      sudo apt-get install r-cran-reshape2

  1. 现在使用以下代码从 GitHub 配置ggplot2.SparkR包:
      library(devtools) 
      devtools::install_github("SKKU-SKT/ggplot2.SparkR") 

  1. 现在让我们计算刚刚创建的样本 DataFrame 的偏度和峰度。在此之前,加载必要的软件包:
      library(moments) 
      library(ggplot2) 

  1. 让我们为特征工程和数据探索部分中的每日锻炼示例创建 DataFrame,显示前几行使用head命令:
      time_taken <- c (15, 16, 18, 17.16, 16.5, 18.6, 19.0, 20.4, 20.6, 
      25.15, 27.27, 25.24, 21.05, 21.65, 20.92, 22.61, 23.71, 35, 39, 50) 
      df_new <- data.frame(time_taken)  
      head(df_new)  
      df<- createDataFrame(sqlContext, data = df_new)  
      head(df) 

  1. 现在按照以下步骤计算偏度和峰度:
      skewness(df) 
      kurtosis(df_new) 

您可能已经注意到我们在特征工程部分中使用了偏度峰度这两个术语。如果您对这两个术语不熟悉,这里是它们的定义。从统计角度来看,偏度是对称性的度量。或者更准确地说,它表示数据集的分布中的对称性缺失。

现在您可能想知道对称是什么。嗯,如果数据集的分布在中心点的左侧和右侧看起来相同,则数据集是对称的。

另一方面,峰度是衡量数据相对于正态分布是重尾还是轻尾的指标:

  1. 最后,通过调用ggplot2.SparkR包的ggplot()方法来绘制密度图。
      ggplot(df, aes(x = time_taken)) + stat_density(geom="line",
      col= "green", size = 1, bw = 4) + theme_bw() 

如果您不熟悉ggplot2 R 包,请注意,ggplot2是基于 R 的绘图系统,基于基本和格栅图形的图形语法。它提供了许多使绘图成为一种麻烦的图形的琐碎细节,例如,在图形中放置或绘制图例,以及提供强大的图形模型。这将使您的生活更轻松,以便生成简单和复杂的多层图形。

提示

有关ggplot2及其文档的更多信息,请访问以下网站:docs.ggplot2.org/current/

在 Windows 上配置 Hadoop 运行时

如果您正在使用 Eclipse(当然是 Maven 项目)在 Windows 上开发您的机器学习应用程序,可能会遇到问题,因为 Spark 也希望在 Windows 上有 Hadoop 的运行时环境。

更具体地说,假设您正在运行一个用 Java 编写的 Spark 项目,主类为JavaNaiveBayes_ML.java,那么您将遇到一个 IO 异常,说:

16/10/04 11:59:52 ERROR Shell: Failed to locate the winutils binary in the hadoop binary path
java.io.IOException: Could not locate executable null\bin\winutils.exe in the Hadoop binaries.

在 Windows 上配置 Hadoop 运行时

图 1:由于缺少 Hadoop 运行时而导致的 IO 异常

原因是默认情况下,Hadoop 是为 Linux 环境开发的,如果您在 Windows 平台上开发 Spark 应用程序,则需要一个桥梁,它将为 Spark 提供 Hadoop 环境,以便正确执行 Hadoop 运行时。

现在,如何摆脱这个问题呢?解决方案很简单。正如错误消息所说,我们需要一个名为winutils.exe的可执行文件。现在从 Packt 的代码目录下载winutils.exe文件,并将其复制粘贴到 Spark 分发目录中,并配置 Eclipse。

更具体地说,假设您的包含 Hadoop 的 Spark 分发位于C:/Users/spark-2.0.0-bin-hadoop2.7。在 Spark 分发中有一个名为bin的目录。现在,将可执行文件粘贴到那里(即path = C:/Users/spark-2.0.0-binhadoop2.7/``bin/)。

解决方案的第二阶段是进入 Eclipse,选择主类(即在本例中为JavaNaiveBayes_ML.java),然后转到运行菜单。从运行菜单转到运行配置选项,然后从该选项中选择环境选项卡。如果选择该选项卡,您将有一个选项可以为 Eclipse 使用 JVM 创建一个新的环境变量。

现在创建一个新的环境变量,并将值设置为C:/Users/spark-2.0.0-bin-hadoop2.7/。现在点击应用,重新运行您的应用程序,您的问题应该得到解决。

更具体地说,IO 异常的细节可以在图 1 中描述如下:

摘要

在本章中,我们展示了如何使用 Spark 扩展数据分析的外部库。

越来越多的 Spark 以及第三方包正在由开源贡献者开发。读者应该及时了解 Spark 网站上的最新消息和发布。他们还应该得到有关最新机器学习 API 的通知,因为 Spark 的开发是持续和创新的,当然,有时在某个包变得过时或被弃用后。

在本书中,我们试图指导您如何使用由 Spark 开发的最流行和广泛使用的机器学习算法。然而,还有其他算法,我们无法讨论,而且越来越多的算法将被添加到 Spark ML 和 MLlib 包中。

这基本上是我们与 Spark 的小旅程的结束。现在我们向您作为读者的一般建议,或者如果您对机器学习、Java 或 Spark 相对较新,首先要了解一个问题是否真的是一个机器学习问题。如果是一个机器学习问题,试着猜测哪种类型的学习算法应该是最合适的,即分类、聚类、回归、推荐或频繁模式挖掘。

然后定义和规划问题。之后,您应该基于我们讨论过的 Spark 的特征工程概念生成或下载适当的数据。然后,您可以选择一个 ML 模型,该模型将在准确性方面提供更好的结果。但是,正如前面讨论的那样,模型选择确实取决于您的数据和问题类型。

现在您已经准备好训练模型的数据,可以直接开始训练模型以进行预测分析。

当您的模型训练好后,评估它以查看其表现并满足您的预测期望。如果您对性能不满意,请尝试切换到其他 ML 算法以进行模型选择。正如在第七章中讨论的那样,调整机器学习模型,即使适当的模型选择有时也无法提供最佳结果,因为您拥有的数据的性质。

那么应该做什么呢?很简单。使用可用的调整算法来调整您的 ML 模型,以正确设置超参数。您可能还需要使您的模型适应新的数据类型,特别是如果您正在为时间序列分析或流式分析等动态环境开发 ML 应用程序。

最后,部署您的模型,您将拥有一个强大的 ML 应用程序。

我们最终向读者推荐定期浏览 Spark 网站(位于spark.apache.org/),以获取更新,并尝试将常规提供的 Spark API 与其他第三方应用程序结合起来,以获得合作的最佳结果。

这本电子书是由 AlenMiler 在 AvaxHome 上发布的!

我的博客中有许多新的电子书: avxhome.in/blogs/AlenMiler

镜像: avxhome.unblocked.tw/blogs/AlenMiler

posted @ 2024-05-21 12:53  绝不原创的飞龙  阅读(35)  评论(0编辑  收藏  举报