Spark-秘籍-全-

Spark 秘籍(全)

原文:zh.annas-archive.org/md5/bf1fae88e839f4d0a5a0fd250cec5835

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

作为大数据平台,Hadoop 的成功提高了用户在解决不同分析挑战以及降低延迟方面的期望。随着时间的推移,各种工具不断发展,但当 Apache Spark 出现时,它提供了一种单一运行时来应对所有这些挑战。它消除了需要结合具有各自挑战和学习曲线的多个工具的需求。通过使用内存进行持久存储以及计算,Apache Spark 消除了在磁盘上存储中间数据的需求,并将处理速度提高了 100 倍。它还提供了一个单一运行时,使用各种库来满足各种分析需求,如机器学习和实时流处理。

本书涵盖了 Apache Spark 的安装和配置,以及使用 Spark Core、Spark SQL、Spark Streaming、MLlib 和 GraphX 库构建解决方案。

注意

如需了解本书的食谱更多信息,请访问infoobjects.com/spark-cookbook

本书涵盖内容

第一章,Apache Spark 入门,解释了如何在各种环境和集群管理器上安装 Spark。

第二章,使用 Spark 开发应用程序,讨论了在不同 IDE 上开发 Spark 应用程序以及使用不同的构建工具。

第三章,外部数据源,介绍了如何读取和写入各种数据源。

第四章,Spark SQL,带您了解 Spark SQL 模块,该模块可以帮助您使用 SQL 界面访问 Spark 功能。

第五章,Spark Streaming,探讨了 Spark Streaming 库,用于分析来自实时数据源(如 Kafka)的数据。

第六章,使用 MLlib 开始机器学习,涵盖了机器学习简介以及如向量和矩阵等基本元素。

第七章,使用 MLlib 的监督学习 – 回归,介绍了当结果变量是连续时的监督学习。

第八章,使用 MLlib 的监督学习 – 分类,讨论了当结果变量是离散时的监督学习。

第九章,使用 MLlib 的无监督学习,涵盖了 k-means 等无监督学习算法。

第十章,推荐系统,介绍了使用各种技术构建推荐系统,例如 ALS。

第十一章,使用 GraphX 进行图处理,讨论了使用 GraphX 的各种图处理算法。

第十二章,优化和性能调整,涵盖了 Apache Spark 的各种优化和性能调整技术。

你需要这本书的物品

您需要 InfoObjects Big Data Sandbox 软件才能继续使用本书中的示例。此软件可以从www.infoobjects.com下载。

这本书面向谁

如果您是一位希望利用 Apache Spark 的力量从大数据中获得更好洞察力的数据工程师、应用程序开发者或数据科学家,那么这本书就是为您准备的。

部分

在这本书中,您会发现一些频繁出现的标题(准备工作、如何做、它是如何工作的、还有更多、以及参见)。

为了清楚地说明如何完成食谱,我们按照以下方式使用这些部分:

准备工作

这一部分告诉您在食谱中可以期待什么,并描述了如何设置任何软件或任何为食谱所需的初步设置。

如何做…

这一部分包含遵循食谱所需的步骤。

它是如何工作的…

这一部分通常包含对上一部分发生情况的详细解释。

还有更多…

这一部分包含有关食谱的附加信息,以便使读者对食谱有更多的了解。

参见

这一部分提供了指向其他有用信息的链接,这些信息对食谱有帮助。

惯例

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

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名如下所示:“Spark 期望已安装 Java,并设置JAVA_HOME环境变量。”

代码块如下设置:

lazy val root = (project in file("."))
  settings(
    name := "wordcount"
  )

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

$ wget http://d3kbcqa49mib13.cloudfront.net/spark-1.4.0-bin-hadoop2.4.tgz

新术语重要单词以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:“在右上角您的账户名称下点击安全凭证。”

注意

警告或重要注意事项以这种方式出现在框中。

小贴士

小贴士和技巧看起来像这样。

读者反馈

我们读者的反馈总是受欢迎的。请告诉我们您对这本书的看法——您喜欢或不喜欢的地方。读者反馈对我们很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。

要向我们发送一般反馈,只需发送电子邮件至 <feedback@packtpub.com>,并在邮件主题中提及书籍的标题。

如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在您已经是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。

下载本书的彩色图像

我们还为您提供了一个包含本书中使用的截图/图表彩色图像的 PDF 文件。彩色图像将帮助您更好地理解输出的变化。您可以从以下链接下载此文件:www.packtpub.com/sites/default/files/downloads/7061OS_ColorImages.pdf

勘误

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然会发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。

要查看之前提交的勘误表,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将在勘误部分显示。

盗版

在互联网上对版权材料的盗版是所有媒体中持续存在的问题。在 Packt,我们非常重视保护我们的版权和许可证。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过链接mailto:copyright@packtpub.com与我们联系,并提供疑似盗版材料的链接。

我们感谢您在保护我们作者和我们提供有价值内容的能力方面的帮助。

询问

如果您对本书的任何方面有问题,您可以通过链接mailto:questions@packtpub.com与我们联系,我们将尽力解决问题。

第一章. Apache Spark 入门

在本章中,我们将设置 Spark 并对其进行配置。本章分为以下食谱:

  • 从二进制文件安装 Spark

  • 使用 Maven 构建 Spark 源代码

  • 在 Amazon EC2 上启动 Spark

  • 在独立模式下在集群上部署 Spark

  • 在 Mesos 集群上部署 Spark

  • 在 YARN 集群上部署 Spark

  • 使用 Tachyon 作为 off-heap 存储层

简介

Apache Spark 是一个通用的集群计算系统,用于处理大数据工作负载。Spark 与众不同的地方,比如 MapReduce,是其速度、易用性和复杂的分析能力。

Apache Spark 最初于 2009 年在加州大学伯克利分校的 AMPLab 开发。它在 2010 年以 BSD 许可证开源,并在 2013 年切换到 Apache 2.0 许可证。到了 2013 年的后期,Spark 的创造者成立了 Databricks,专注于 Spark 的开发和未来版本。

谈到速度,Spark 可以在大数据工作负载上实现亚秒级延迟。为了实现如此低的延迟,Spark 利用内存进行存储。在 MapReduce 中,内存主要用于实际计算。Spark 既用于计算也用于存储对象。

Spark 还提供了一个统一的运行时,可以连接到各种大数据存储源,如 HDFS、Cassandra、HBase 和 S3。它还提供了一套丰富的更高级别的库,用于不同的大数据计算任务,如机器学习、SQL 处理、图处理和实时流。这些库使开发更快,并且可以任意组合。

虽然 Spark 是用 Scala 编写的,而且这本书只关注 Scala 的食谱,但 Spark 也支持 Java 和 Python。

Spark 是一个开源社区项目,每个人都在部署时使用纯开源的 Apache 发行版,与 Hadoop 不同,Hadoop 有多种带有供应商增强功能的发行版可用。

下图显示了 Spark 生态系统:

简介

Spark 运行时运行在多种集群管理器之上,包括 YARN(Hadoop 的计算框架)、Mesos 以及 Spark 自己的集群管理器,称为独立模式。Tachyon 是一个以内存为中心的分布式文件系统,它能够在集群框架之间以内存速度实现可靠的文件共享。简而言之,它是一个内存中的 off-heap 存储层,有助于跨作业和用户共享数据。Mesos 是一个集群管理器,它正在演变成为一个数据中心操作系统。YARN 是 Hadoop 的计算框架,它具有强大的资源管理功能,Spark 可以无缝使用。

从二进制文件安装 Spark

Spark 可以从源代码构建,也可以从spark.apache.org下载预编译的二进制文件。对于标准用例,二进制文件就足够了,这个食谱将专注于使用二进制文件安装 Spark。

准备工作

本书中的所有菜谱都是使用 Ubuntu Linux 开发的,但应在任何 POSIX 环境中运行良好。Spark 预期 Java 已安装,并且 JAVA_HOME 环境变量已设置。

在 Linux/Unix 系统中,文件和目录的位置有一定的标准,我们将遵循本书中的这些标准。以下是一个快速速查表:

目录 描述
/bin 必要的命令二进制文件
/etc 主机特定的系统配置
/opt 附加应用程序软件包
/var 可变数据
/tmp 临时文件
/home 用户主目录

如何操作...

在撰写本文时,Spark 的当前版本是 1.4。请从 Spark 的下载页面 spark.apache.org/downloads.html 检查最新版本。二进制文件使用最新且稳定的 Hadoop 版本开发。要使用特定的 Hadoop 版本,建议从源代码构建,这将在下一道菜谱中介绍。

以下为安装步骤:

  1. 打开终端并使用以下命令下载二进制文件:

    $ wget http://d3kbcqa49mib13.cloudfront.net/spark-1.4.0-bin-hadoop2.4.tgz
    
    
  2. 解压二进制文件:

    $ tar -zxf spark-1.4.0-bin-hadoop2.4.tgz
    
    
  3. 通过删除版本信息重命名包含二进制的文件夹:

    $ sudo mv spark-1.4.0-bin-hadoop2.4 spark
    
    
  4. 将配置文件夹移动到 /etc 文件夹,以便稍后可以创建符号链接:

    $ sudo mv spark/conf/* /etc/spark
    
    
  5. /opt 下创建您公司特定的安装目录。由于本书中的菜谱在 infoobjects 沙盒上进行了测试,我们将使用 infoobjects 作为目录名称。创建 /opt/infoobjects 目录:

    $ sudo mkdir -p /opt/infoobjects
    
    
  6. spark 目录移动到 /opt/infoobjects,因为它是一个附加软件包:

    $ sudo mv spark /opt/infoobjects/
    
    
  7. spark 主目录的所有权更改为 root

    $ sudo chown -R root:root /opt/infoobjects/spark
    
    
  8. 修改 spark 主目录的权限,0755 = 用户:读写执行 组:读执行 世界:读执行

    $ sudo chmod -R 755 /opt/infoobjects/spark
    
    
  9. 切换到 spark 主目录:

    $ cd /opt/infoobjects/spark
    
    
  10. 创建符号链接:

    $ sudo ln -s /etc/spark conf
    
    
  11. PATH 追加到 .bashrc

    $ echo "export PATH=$PATH:/opt/infoobjects/spark/bin" >> /home/hduser/.bashrc
    
    
  12. 打开一个新的终端。

  13. /var 中创建 log 目录:

    $ sudo mkdir -p /var/log/spark
    
    
  14. hduser 设置为 Spark log 目录的所有者。

    $ sudo chown -R hduser:hduser /var/log/spark
    
    
  15. 创建 Spark 的 tmp 目录:

    $ mkdir /tmp/spark
    
    
  16. 使用以下命令行配置 Spark:

    $ cd /etc/spark
    $ echo "export HADOOP_CONF_DIR=/opt/infoobjects/hadoop/etc/hadoop" >> spark-env.sh
    $ echo "export YARN_CONF_DIR=/opt/infoobjects/hadoop/etc/Hadoop" >> spark-env.sh
    $ echo "export SPARK_LOG_DIR=/var/log/spark" >> spark-env.sh
    $ echo "export SPARK_WORKER_DIR=/tmp/spark" >> spark-env.sh
    
    

使用 Maven 构建 Spark 源代码

使用二进制文件安装 Spark 在大多数情况下都运行良好。对于高级情况,例如以下情况(但不限于),从源代码编译是一个更好的选择:

  • 为特定 Hadoop 版本编译

  • 添加 Hive 集成

  • 添加 YARN 集成

准备工作

以下是为使此菜谱正常工作所需的先决条件:

  • Java 1.6 或更高版本

  • Maven 3.x

如何操作...

以下是用 Maven 构建 Spark 源代码的步骤:

  1. 增加 MaxPermSize 以扩大堆:

    $ echo "export _JAVA_OPTIONS=\"-XX:MaxPermSize=1G\""  >> /home/hduser/.bashrc
    
    
  2. 打开一个新的终端窗口并从 GitHub 下载 Spark 源代码:

    $ wget https://github.com/apache/spark/archive/branch-1.4.zip
    
    
  3. 解压存档:

    $ gunzip branch-1.4.zip
    
    
  4. 切换到 spark 目录:

    $ cd spark
    
    
  5. 使用以下标志编译源代码:启用 Yarn,Hadoop 版本 2.4,启用 Hive,跳过测试以加快编译速度:

    $ mvn -Pyarn -Phadoop-2.4 -Dhadoop.version=2.4.0 -Phive -DskipTests clean package
    
    
  6. conf文件夹移动到etc文件夹,以便将其设置为符号链接:

    $ sudo mv spark/conf /etc/
    
    
  7. spark目录移动到/opt,因为它是一个附加的软件包:

    $ sudo mv spark /opt/infoobjects/spark
    
    
  8. spark主目录的所有权更改为root

    $ sudo chown -R root:root /opt/infoobjects/spark
    
    
  9. 修改spark主目录的权限为0755 = user:rwx group:r-x world:r-x

    $ sudo chmod -R 755 /opt/infoobjects/spark
    
    
  10. 移动到spark主目录:

    $ cd /opt/infoobjects/spark
    
    
  11. 创建符号链接:

    $ sudo ln -s /etc/spark conf
    
    
  12. 通过编辑.bashrc将 Spark 可执行文件放入路径:

    $ echo "export PATH=$PATH:/opt/infoobjects/spark/bin" >> /home/hduser/.bashrc
    
    
  13. /var中创建log目录。

    $ sudo mkdir -p /var/log/spark
    
    
  14. 使hduser成为 Sparklog目录的所有者:

    $ sudo chown -R hduser:hduser /var/log/spark
    
    
  15. 创建 Spark 的tmp目录:

    $ mkdir /tmp/spark
    
    
  16. 使用以下命令行配置 Spark:

    $ cd /etc/spark
    $ echo "export HADOOP_CONF_DIR=/opt/infoobjects/hadoop/etc/hadoop" >> spark-env.sh
    $ echo "export YARN_CONF_DIR=/opt/infoobjects/hadoop/etc/Hadoop" >> spark-env.sh
    $ echo "export SPARK_LOG_DIR=/var/log/spark" >> spark-env.sh
    $ echo "export SPARK_WORKER_DIR=/tmp/spark" >> spark-env.sh
    
    

在 Amazon EC2 上启动 Spark

亚马逊弹性计算云Amazon EC2)是一种提供可调整计算实例的云服务。Amazon EC2 提供以下功能:

  • 通过互联网按需交付 IT 资源

  • 提供您喜欢的任意数量的实例

  • 使用实例的小时数付费,就像您的电费一样

  • 没有设置成本,没有安装,也没有任何开销

  • 当您不再需要实例时,您可以关闭或终止并离开

  • 这些实例在所有熟悉的操作系统上的可用性

EC2 提供不同类型的实例以满足所有计算需求,例如通用实例、微型实例、内存优化实例、存储优化实例等。它们提供免费的小型实例供试用。

准备就绪

spark-ec2脚本与 Spark 捆绑在一起,使得在 Amazon EC2 上启动、管理和关闭集群变得容易。

在开始之前,您需要做以下事情:

  1. 登录到 Amazon AWS 账户(aws.amazon.com)。

  2. 在右上角您的账户名称下点击安全凭证

  3. 在右上角您的账户名称下点击访问密钥创建新访问密钥准备就绪

  4. 记下访问密钥 ID 和秘密访问密钥。

  5. 现在转到服务 | EC2

  6. 在网络与安全左侧菜单下点击密钥对

  7. 点击创建密钥对并输入kp-spark作为密钥对名称:准备就绪

  8. 下载私钥文件并将其复制到/home/hduser/keypairs文件夹中。

  9. 将密钥文件的权限设置为600

  10. 设置环境变量以反映访问密钥 ID 和秘密访问密钥(请用您的实际值替换示例值):

    $ echo "export AWS_ACCESS_KEY_ID=\"AKIAOD7M2LOWATFXFKQ\"" >> /home/hduser/.bashrc
    $ echo "export AWS_SECRET_ACCESS_KEY=\"+Xr4UroVYJxiLiY8DLT4DLT4D4sxc3ijZGMx1D3pfZ2q\"" >> /home/hduser/.bashrc
    $ echo "export PATH=$PATH:/opt/infoobjects/spark/ec2" >> /home/hduser/.bashrc
    
    

如何操作...

  1. Spark 附带启动 Amazon EC2 上的 Spark 集群的脚本。让我们使用以下命令启动集群:

    $ cd /home/hduser
    $ spark-ec2 -k <key-pair> -i <key-file> -s <num-slaves> launch <cluster-name>
    
    
  2. 使用示例值启动集群:

    $ spark-ec2 -k kp-spark -i /home/hduser/keypairs/kp-spark.pem --hadoop-major-version 2  -s 3 launch spark-cluster
    
    

    注意

    • <key-pair>:这是在 AWS 中创建的 EC2 密钥对名称

    • <key-file>:这是您下载的私钥文件

    • <num-slaves>:这是要启动的从节点数量

    • <cluster-name>:这是集群的名称

  3. 有时,默认的可用区域不可用;在这种情况下,通过指定您请求的具体可用区域来重试发送请求:

    $ spark-ec2 -k kp-spark -i /home/hduser/keypairs/kp-spark.pem -z us-east-1b --hadoop-major-version 2  -s 3 launch spark-cluster
    
    
  4. 如果你的应用程序需要在实例关闭后保留数据,请将其附加到 EBS 卷上(例如,10 GB 空间):

    $ spark-ec2 -k kp-spark -i /home/hduser/keypairs/kp-spark.pem --hadoop-major-version 2 -ebs-vol-size 10 -s 3 launch spark-cluster
    
    
  5. 如果你使用 Amazon spot 实例,以下是操作方法:

    $ spark-ec2 -k kp-spark -i /home/hduser/keypairs/kp-spark.pem -spot-price=0.15 --hadoop-major-version 2  -s 3 launch spark-cluster
    
    

    注意

    Spot 实例允许你为 Amazon EC2 计算能力设定自己的价格。你只需对额外的 Amazon EC2 实例进行投标,并在你的投标超过当前 spot 价格时运行它们,该价格根据供需实时变化(来源:amazon.com)。

  6. 一切启动后,通过查看最后打印的 web UI URL 来检查集群的状态。如何操作...

  7. 检查集群状态:如何操作...

  8. 现在,要访问 EC2 上的 Spark 集群,让我们使用安全外壳协议SSH)连接到主节点:

    $ spark-ec2 -k kp-spark -i /home/hduser/kp/kp-spark.pem  login spark-cluster
    
    

    你应该得到类似以下的内容:

    如何操作...

  9. 检查主节点上的目录并查看它们的功能:

    目录 描述
    ephemeral-hdfs 这是数据为临时性的 Hadoop 实例,当你停止或重启机器时,数据将被删除。
    persistent-hdfs 每个节点都有非常小的持久存储空间(大约 3 GB)。如果你使用此实例,数据将保留在该空间中。
    hadoop-native 这些是支持 Hadoop 的本地库,例如 snappy 压缩库。
    Scala 这是 Scala 安装。
    shark 这是 Shark 安装(Shark 不再受支持,已被 Spark SQL 取代)。
    spark 这是 Spark 安装
    spark-ec2 这些是支持此集群部署的文件。
    tachyon 这是 Tachyon 安装
  10. 在临时实例中检查 HDFS 版本:

    $ ephemeral-hdfs/bin/hadoop version
    Hadoop 2.0.0-chd4.2.0
    
    
  11. 使用以下命令检查持久实例中的 HDFS 版本:

    $ persistent-hdfs/bin/hadoop version
    Hadoop 2.0.0-chd4.2.0
    
    
  12. 在日志中更改配置级别:

    $ cd spark/conf
    
    
  13. 默认日志级别信息过于冗长,因此让我们将其更改为错误:如何操作...

    1. 通过重命名模板创建log4.properties文件:

      $ mv log4j.properties.template log4j.properties
      
      
    2. 在 vi 或你喜欢的编辑器中打开log4j.properties

      $ vi log4j.properties
      
      
    3. 将第二行从| log4j.rootCategory=INFO, console更改为| log4j.rootCategory=ERROR, console

  14. 在更改后,将配置复制到所有从节点:

    $ spark-ec2/copydir spark/conf
    
    

    你应该得到类似以下的内容:

    如何操作...

  15. 销毁 Spark 集群:

    $ spark-ec2 destroy spark-cluster
    
    

参见

以独立模式在集群上部署

在分布式环境中,需要管理计算资源,以确保资源利用效率高,每个作业都有公平的机会运行。Spark 自带一个方便的集群管理器,称为独立模式。Spark 还支持与 YARN 和 Mesos 集群管理器一起工作。

应该选择的集群管理器通常是由遗留问题以及是否其他框架,例如 MapReduce,是否共享相同的计算资源池所驱动的。如果你的集群有正在运行的遗留 MapReduce 作业,并且它们都不能转换为 Spark 作业,那么使用 YARN 作为集群管理器是一个好主意。Mesos 正在成为一个数据中心操作系统,可以方便地在框架之间管理作业,并且与 Spark 非常兼容。

如果 Spark 框架是集群中唯一的框架,那么独立模式就足够了。随着 Spark 作为技术的演变,你将看到越来越多的 Spark 作为独立框架用于满足所有大数据计算需求的使用案例。例如,一些作业可能目前正在使用 Apache Mahout,因为 MLlib 没有特定于机器学习的库,而作业需要这个库。一旦 MLlib 获得这个库,这个特定的作业就可以迁移到 Spark。

准备工作

让我们以一个由六个节点组成的集群为例:一个主节点和五个从节点(用你集群中的实际节点名称替换它们):

Master
m1.zettabytes.com
Slaves
s1.zettabytes.com
s2.zettabytes.com
s3.zettabytes.com
s4.zettabytes.com
s5.zettabytes.com

如何操作...

  1. 由于 Spark 的独立模式是默认的,你只需要在主节点和从节点上都安装 Spark 二进制文件。将/opt/infoobjects/spark/sbin添加到每个节点的路径中:

    $ echo "export PATH=$PATH:/opt/infoobjects/spark/sbin" >> /home/hduser/.bashrc
    
    
  2. 启动独立的主服务器(首先 SSH 到主节点):

    hduser@m1.zettabytes.com~] start-master.sh
    
    

    主节点默认在 7077 端口启动,从节点使用此端口连接到它。它还有一个位于 8088 端口的 Web UI。

  3. 请 SSH 到主节点并启动从节点:

    hduser@s1.zettabytes.com~] spark-class org.apache.spark.deploy.worker.Worker spark://m1.zettabytes.com:7077
    
    
    参数(对于细粒度配置,以下参数对主节点和从节点都适用) 含义
    -i <ipaddress>,-ip <ipaddress> 监听的 IP 地址/DNS 服务
    -p <port>, --port <port> 监听的端口
    --webui-port <port> Web UI 的端口(默认情况下,主节点为 8080,工作节点为 8081)
    -c <cores>,--cores <cores> 在一台机器上 Spark 应用程序可以使用的总 CPU 核心(仅限工作节点)
    -m <memory>,--memory <memory> 在一台机器上 Spark 应用程序可以使用的总 RAM(仅限工作节点)
    -d <dir>,--work-dir <dir> 用于临时空间和作业输出日志的目录
  4. 而不是在每个节点上手动启动主从守护进程,也可以使用集群启动脚本完成。

  5. 首先,在主节点上创建conf/slaves文件,并为每个从机主机名添加一行(以五个从节点为例,用你集群中从节点的 DNS 替换):

    hduser@m1.zettabytes.com~] echo "s1.zettabytes.com" >> conf/slaves
    hduser@m1.zettabytes.com~] echo "s2.zettabytes.com" >> conf/slaves
    hduser@m1.zettabytes.com~] echo "s3.zettabytes.com" >> conf/slaves
    hduser@m1.zettabytes.com~] echo "s4.zettabytes.com" >> conf/slaves
    hduser@m1.zettabytes.com~] echo "s5.zettabytes.com" >> conf/slaves
    
    

    一旦设置了从机,你可以调用以下脚本以启动/停止集群:

    脚本名称 目的
    start-master.sh 在主机机器上启动一个主实例
    start-slaves.sh 在从机文件中的每个节点上启动一个从实例
    start-all.sh 启动主节点和从节点
    stop-master.sh 停止主机机器上的主实例
    stop-slaves.sh 停止从机文件中所有节点的从实例
    stop-all.sh 停止主节点和从属节点
  6. 通过 Scala 代码将应用程序连接到集群:

    val sparkContext = new SparkContext(new SparkConf().setMaster("spark://m1.zettabytes.com:7077")
    
    
  7. 通过 Spark shell 连接到集群:

    $ spark-shell --master spark://master:7077
    
    

它是如何工作的...

在独立模式下,Spark 遵循主从架构,非常类似于 Hadoop、MapReduce 和 YARN。计算主守护进程被称为Spark master,运行在一个主节点上。Spark master 可以使用 ZooKeeper 实现高可用性。如果需要,您还可以动态添加更多备用主节点。

计算从属守护进程被称为worker,位于每个从属节点上。worker 守护进程执行以下操作:

  • 向 Spark master 报告从属节点上的计算资源可用性,例如核心数、内存和其他资源

  • 当 Spark master 请求时启动执行器

  • 如果执行器死亡,则重启执行器

每个从属机器上最多有一个执行器对应一个应用程序。

Spark master 和工作节点都非常轻量级。通常,500 MB 到 1 GB 的内存分配就足够了。此值可以通过设置conf/spark-env.sh中的SPARK_DAEMON_MEMORY参数来设置。例如,以下配置将设置主节点和工作节点守护进程的内存为 1 GB。在运行之前,请确保您有sudo作为超级用户权限:

$ echo "export SPARK_DAEMON_MEMORY=1g" >> /opt/infoobjects/spark/conf/spark-env.sh

默认情况下,每个从属节点上运行一个工作实例。有时,您可能有一些比其他机器更强大的机器。在这种情况下,您可以通过以下配置在该机器上启动多个工作实例(仅在这些机器上):

$ echo "export SPARK_WORKER_INSTANCES=2" >> /opt/infoobjects/spark/conf/spark-env.sh

Spark 工作节点默认情况下使用从属机器上的所有核心为其执行器。如果您想限制工作节点可以使用的核心数,您可以通过以下配置将其设置为该数值(例如,12):

$ echo "export SPARK_WORKER_CORES=12" >> /opt/infoobjects/spark/conf/spark-env.sh

Spark 工作节点默认情况下使用所有可用的 RAM(执行器为 1 GB)。请注意,您不能分配每个特定执行器将使用的内存量(您可以从驱动器配置中控制这一点)。要将所有执行器组合使用的总内存(例如,24 GB)设置为另一个值,请执行以下设置:

$ echo "export SPARK_WORKER_MEMORY=24g" >> /opt/infoobjects/spark/conf/spark-env.sh

您可以在驱动器级别进行一些设置:

  • 要指定集群中给定应用程序可使用的最大 CPU 核心数,您可以在 Spark 提交或 Spark shell 中设置spark.cores.max配置,如下所示:

    $ spark-submit --conf spark.cores.max=12
    
    
  • 要指定每个执行器应分配的内存量(最低推荐为 8 GB),您可以在 Spark 提交或 Spark shell 中设置spark.executor.memory配置,如下所示:

    $ spark-submit --conf spark.executor.memory=8g
    
    

以下图显示了 Spark 集群的高级架构:

它是如何工作的...

参见

在 Mesos 集群上部署

Mesos 正逐渐成为数据中心操作系统,用于管理数据中心内的所有计算资源。Mesos 可以在运行 Linux 操作系统的任何计算机上运行。Mesos 是使用与 Linux 内核相同的原理构建的。让我们看看如何安装 Mesos。

如何做…

Mesosphere 提供了 Mesos 的二进制发行版。可以通过执行以下步骤从 Mesosphere 仓库安装 Mesos 的最新发行版包:

  1. 在 Ubuntu OS 的 trusty 版本上执行 Mesos:

    $ sudo apt-key adv --keyserver keyserver.ubuntu.com --recv E56151BF DISTRO=$(lsb_release -is | tr '[:upper:]' '[:lower:]') CODENAME=$(lsb_release -cs)
    $ sudo vi /etc/apt/sources.list.d/mesosphere.list
    
    deb http://repos.mesosphere.io/Ubuntu trusty main
    
    
  2. 更新仓库:

    $ sudo apt-get -y update
    
    
  3. 安装 Mesos:

    $ sudo apt-get -y install mesos
    
    
  4. 要将 Spark 连接到 Mesos 以集成 Spark 与 Mesos,使 Spark 二进制文件对 Mesos 可用,并配置 Spark 驱动程序以连接到 Mesos。

  5. 使用第一个配方中的 Spark 二进制文件并将其上传到 HDFS:

    $ 
    hdfs dfs
     -put spark-1.4.0-bin-hadoop2.4.tgz spark-1.4.0-bin-hadoop2.4.tgz
    
    
  6. 单个主机的 Mesos 主 URL 是mesos://host:5050,而对于由 ZooKeeper 管理的 Mesos 集群,它是mesos://zk://host:2181

  7. spark-env.sh中设置以下变量:

    $ sudo vi spark-env.sh
    export MESOS_NATIVE_LIBRARY=/usr/local/lib/libmesos.so
    export SPARK_EXECUTOR_URI= hdfs://localhost:9000/user/hduser/spark-1.4.0-bin-hadoop2.4.tgz
    
    
  8. 从 Scala 程序运行:

    val conf = new SparkConf().setMaster("mesos://host:5050")
    val sparkContext = new SparkContext(conf)
    
    
  9. 从 Spark shell 运行:

    $ spark-shell --master mesos://host:5050
    
    

    注意

    Mesos 有两种运行模式:

    细粒度:在细粒度(默认)模式下,每个 Spark 任务都作为一个单独的 Mesos 任务运行

    粗粒度:此模式将在每台 Mesos 机器上仅启动一个长时间运行的 Spark 任务

  10. 要在粗粒度模式下运行,设置spark.mesos.coarse属性:

    conf.set("spark.mesos.coarse","true")
    
    

在具有 YARN 的集群上部署

另一个资源协调器YARN)是运行在 HDFS(Hadoop 的存储层)之上的 Hadoop 计算框架。

YARN 遵循主从架构。主守护进程称为ResourceManager,从守护进程称为NodeManager。除了这个应用程序外,生命周期管理由ApplicationMaster完成,它可以在任何从节点上产生,并且在整个应用程序的生命周期内保持活跃。

当 Spark 在 YARN 上运行时,ResourceManager扮演 Spark 主的角色,而NodeManagers作为执行节点工作。

当使用 YARN 运行 Spark 时,每个 Spark 执行器都作为 YARN 容器运行。

准备工作

在 YARN 上运行 Spark 需要一个具有 YARN 支持的 Spark 二进制发行版。在两个 Spark 安装配方中,我们都已经注意到了这一点。

如何做…

  1. 在 YARN 上运行 Spark 的第一步是设置配置:

    HADOOP_CONF_DIR: to write to HDFS
    YARN_CONF_DIR: to connect to YARN ResourceManager
    $ cd /opt/infoobjects/spark/conf (or /etc/spark)
    $ sudo vi spark-env.sh
    export HADOOP_CONF_DIR=/opt/infoobjects/hadoop/etc/Hadoop
    export YARN_CONF_DIR=/opt/infoobjects/hadoop/etc/hadoop
    
    

    你可以在以下屏幕截图中看到这一点:

    如何做…

  2. 以下命令以yarn-client模式启动 YARN Spark:

    $ spark-submit --class path.to.your.Class --master yarn-client [options] <app jar> [app options]
    
    

    这里有一个例子:

    $ spark-submit --class com.infoobjects.TwitterFireHose --master yarn-client --num-executors 3 --driver-memory 4g --executor-memory 2g --executor-cores 1 target/sparkio.jar 10
    
    
  3. 以下命令以yarn-client模式启动 Spark shell:

    $ spark-shell --master yarn-client
    
    
  4. yarn-cluster模式启动的命令如下:

    $ spark-submit --class path.to.your.Class --master yarn-cluster [options] <app jar> [app options]
    
    

    这里有一个例子:

    $ spark-submit --class com.infoobjects.TwitterFireHose --master yarn-cluster --num-executors 3 --driver-memory 4g --executor-memory 2g --executor-cores 1 targe
    t/sparkio.jar 10
    
    

它是如何工作的…

YARN 上的 Spark 应用程序以两种模式运行:

  • yarn-client:Spark Driver 在 YARN 集群外的客户端进程中运行,而ApplicationMaster仅用于从 ResourceManager 协商资源

  • yarn-cluster:Spark Driver 在从从节点上的NodeManager产生的ApplicationMaster中运行

yarn-cluster模式推荐用于生产部署,而yarn-client模式适合开发调试,当你希望看到即时输出时。在两种模式下都不需要指定 Spark master,因为它从 Hadoop 配置中选取,master 参数是yarn-clientyarn-cluster

以下图显示了在客户端模式下使用 YARN 运行 Spark 的方式:

如何工作…

以下图显示了在集群模式下使用 YARN 运行 Spark 的方式:

如何工作…

在 YARN 模式下,可以设置以下配置参数:

  • --num-executors: 配置将分配多少个执行器

  • --executor-memory: 每个执行器的 RAM

  • --executor-cores: 每个执行器的 CPU 核心数

作为堆外存储层使用 Tachyon

Spark RDDs 是存储数据集的一种好方法,可以在内存中存储多个相同数据的不同应用中的副本。Tachyon 解决了 Spark RDD 管理中的一些挑战。其中一些包括:

  • RDD 仅在 Spark 应用程序的运行期间存在

  • 相同的过程执行计算和 RDD 内存存储;因此,如果进程崩溃,内存存储也会消失

  • 即使不同的作业针对相同的基础数据,它们也无法共享 RDD,例如,导致 HDFS 块的:

    • 磁盘写入速度慢

    • 内存中数据的重复,更大的内存占用

  • 如果一个应用程序的输出需要与其他应用程序共享,由于磁盘上的复制,速度会变慢

Tachyon 提供堆外内存层来解决这些问题。这个层因为是堆外,所以对进程崩溃免疫,也不受垃圾回收的影响。这也允许 RDD 在应用程序之间共享,并且可以超出特定作业或会话的寿命;本质上,数据的一个副本驻留在内存中,如下所示:

作为堆外存储层使用 Tachyon

如何操作...

  1. 让我们下载并编译 Tachyon(Tachyon 默认配置为 Hadoop 1.0.4,因此需要从源代码编译以获得正确的 Hadoop 版本)。将版本替换为当前版本。本书撰写时的当前版本是 0.6.4:

    $ wget https://github.com/amplab/tachyon/archive/v<version>.zip
    
    
  2. 解压源代码:

    $ unzip  v-<version>.zip
    
    
  3. 为了方便,从tachyon源文件夹名称中移除版本号:

    $ mv tachyon-<version> tachyon
    
    
  4. 将目录更改为tachyon文件夹:

    $ cd tachyon
    $ mvn -Dhadoop.version=2.4.0 clean package -DskipTests=true
    $ cd conf
    $ sudo mkdir -p /var/tachyon/journal
    $ sudo chown -R hduser:hduser /var/tachyon/journal
    $ sudo mkdir -p /var/tachyon/ramdisk
    $ sudo chown -R hduser:hduser /var/tachyon/ramdisk
    
    $ mv tachyon-env.sh.template tachyon-env.sh
    $ vi tachyon-env.sh
    
    
  5. 取消注释以下行:

    export TACHYON_UNDERFS_ADDRESS=$TACHYON_HOME/underfs
    
    
  6. 取消注释以下行:

    export TACHYON_UNDERFS_ADDRESS=hdfs://localhost:9000
    
    
  7. 修改以下属性:

    -Dtachyon.master.journal.folder=/var/tachyon/journal/
    
    export TACHYON_RAM_FOLDER=/var/tachyon/ramdisk
    
    $ sudo mkdir -p /var/log/tachyon
    $ sudo chown -R hduser:hduser /var/log/tachyon
    $ vi log4j.properties
    
    
  8. ${tachyon.home}替换为/var/log/tachyon

  9. conf目录中创建一个新的core-site.xml文件:

    $ sudo vi core-site.xml
    <configuration>
    <property>
     <name>fs.tachyon.impl</name>
     <value>tachyon.hadoop.TFS</value>
     </property>
    </configuration>
    $ cd ~
    $ sudo mv tachyon /opt/infoobjects/
    $ sudo chown -R root:root /opt/infoobjects/tachyon
    $ sudo chmod -R 755 /opt/infoobjects/tachyon
    
    
  10. <tachyon home>/bin添加到路径中:

    $ echo "export PATH=$PATH:/opt/infoobjects/tachyon/bin" >> /home/hduser/.bashrc
    
    
  11. 重新启动 shell 并格式化 Tachyon:

    $ tachyon format
    $ tachyon-start.sh local //you need to enter root password as RamFS needs to be formatted
    
    

    Tachyon 的 Web 界面是http://hostname:19999:

    如何操作...

  12. 运行示例程序以查看 Tachyon 是否运行正常:

    $ tachyon runTest Basic CACHE_THROUGH
    
    

    如何操作...

  13. 你可以通过运行以下命令随时停止 Tachyon:

    $ tachyon-stop.sh
    
    
  14. 在 Tachyon 上运行 Spark:

    $ spark-shell
    scala> val words = sc.textFile("tachyon://localhost:19998/words")
    scala> words.count
    scala> words.saveAsTextFile("tachyon://localhost:19998/w2")
    scala> val person = sc.textFile("hdfs://localhost:9000/user/hduser/person")
    scala> import org.apache.spark.api.java._
    scala> person.persist(StorageLevels.OFF_HEAP)
    
    

参见

第二章:使用 Spark 开发应用程序

在本章中,我们将介绍:

  • 探索 Spark 命令行

  • 在 Eclipse 中使用 Maven 开发 Spark 应用程序

  • 在 Eclipse 中使用 SBT 开发 Spark 应用程序

  • 在 Intellij IDEA 中使用 Maven 开发 Spark 应用程序

  • 在 Intellij IDEA 中使用 SBT 开发 Spark 应用程序

简介

要创建生产质量的 Spark 作业/应用程序,使用各种集成开发环境IDEs)和构建工具是有用的。本章将介绍各种 IDE 和构建工具。

探索 Spark 命令行

Spark 随附了一个 REPL 命令行界面,它是 Scala 命令行的一个包装器。虽然 Spark 命令行看起来像是简单的命令行,但实际上,许多复杂的查询也可以用它来执行。本章将探讨不同的开发环境,在这些环境中可以开发 Spark 应用程序。

如何做到这一点...

使用 Spark 命令行,Hadoop MapReduce 的词频统计变得非常简单。在这个菜谱中,我们将创建一个简单的单行文本文件,将其上传到Hadoop 分布式文件系统HDFS),并使用 Spark 来统计单词的出现次数。让我们看看如何:

  1. 使用以下命令创建 words 目录:

    $ mkdir words
    
    
  2. 进入 words 目录:

    $ cd words
    
    
  3. 创建一个名为 sh.txt 的文本文件,并在其中输入 "to be or not to be"

    $ echo "to be or not to be" > sh.txt
    
    
  4. 启动 Spark 命令行:

    $ spark-shell
    
    
  5. words 目录作为 RDD 加载:

    Scala> val words = sc.textFile("hdfs://localhost:9000/user/hduser/words")
    
    
  6. 计算行数(结果:1):

    Scala> words.count
    
    
  7. 将行(或行)分割成多个单词:

    Scala> val wordsFlatMap = words.flatMap(_.split("\\W+"))
    
    
  8. word 转换为 (word,1)——即输出每个 word 出现作为键的值 1

    Scala> val wordsMap = wordsFlatMap.map( w => (w,1))
    
    
  9. 使用 reduceByKey 方法将每个单词的出现次数作为键(该函数一次处理两个连续的值,分别由 ab 表示):

    Scala> val wordCount = wordsMap.reduceByKey( (a,b) => (a+b))
    
    
  10. 对结果进行排序:

    Scala> val wordCountSorted = wordCount.sortByKey(true)
    
    
  11. 打印 RDD:

    Scala> wordCountSorted.collect.foreach(println)
    
    
  12. 将所有前面的操作一步完成如下:

    Scala> sc.textFile("hdfs://localhost:9000/user/hduser/words"). flatMap(_.split("\\W+")).map( w => (w,1)). reduceByKey( (a,b) => (a+b)).sortByKey(true).collect.foreach(println)
    
    

这将给我们以下输出:

(or,1)
(to,2)
(not,1)
(be,2)

现在你已经了解了基础知识,加载 HDFS 中的大量文本——例如,故事——并看看魔法:

如果你有的文件是压缩格式,你可以在 HDFS 中直接加载它们。Hadoop 和 Spark 都有解压缩编解码器,它们根据文件扩展名使用它们。

wordsFlatMap 转换为 wordsMap RDD 时,有一个隐式转换。这会将 RDD 转换为 PairRDD。这是一个隐式转换,不需要做任何事情。如果你在 Scala 代码中这样做,请添加以下 import 语句:

import org.apache.spark.SparkContext._

在 Eclipse 中使用 Maven 开发 Spark 应用程序

Maven 作为构建工具,多年来已经成为事实上的标准。如果我们深入挖掘 Maven 带来的承诺,这并不令人惊讶。Maven 有两个主要特性,它们是:

  • 约定优于配置:在 Maven 之前的构建工具允许开发者自由决定源文件、测试文件、编译文件等存放的位置。Maven 剥夺了这种自由。有了这种自由,所有关于位置上的困惑也消失了。在 Maven 中,每个项目都有一个特定的目录结构。以下表格展示了其中一些最常见的位置:

    /src/main/scala Scala 源代码
    /src/main/java Java 源代码
    /src/main/resources 源代码使用的资源,例如配置文件
    /src/test/scala Scala 测试代码
    /src/test/java Java 测试代码
    /src/test/resources 测试代码使用的资源,例如配置文件
  • 声明式依赖管理:在 Maven 中,每个库都通过以下三个坐标进行定义:

    groupId 类似于 Java/Scala 中包的逻辑分组方式,必须至少包含你拥有的域名,例如 org.apache.spark
    artifactId 项目和 JAR 的名称
    version 标准版本号

pom.xml(一个配置文件,它告诉 Maven 关于项目的所有信息)中,依赖项以以下三种坐标的形式声明。无需在互联网上搜索并下载、解压和复制库。你所需要做的就是提供所需依赖 JAR 的三种坐标,Maven 会为你完成剩余的工作。以下是一个使用 JUnit 依赖项的示例:

<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.12</version>
</dependency>

这使得依赖项管理,包括传递依赖项,变得非常简单。在 Maven 之后出现的构建工具,如 SBT 和 Gradle,也遵循这两条规则,并在其他方面提供增强。

准备工作

从本食谱开始,本章假设你已经安装了 Eclipse。请访问 www.eclipse.org 获取详细信息。

如何操作...

让我们看看如何安装 Eclipse 的 Maven 插件:

  1. 打开 Eclipse,导航到 帮助 | 安装新软件

  2. 点击 操作 下拉菜单。

  3. 选择 更新站点。

  4. 点击 协作工具

  5. 检查 Maven 与 Eclipse 的集成,如下截图所示:如何操作...

  6. 点击 下一步,然后点击 完成

    将会有提示要求重启 Eclipse,Maven 将在重启后安装。

现在,让我们看看如何安装 Eclipse 的 Scala 插件:

  1. 打开 Eclipse,导航到 帮助 | 安装新软件

  2. 点击 操作 下拉菜单。

  3. 选择 更新站点。

  4. 输入 http://download.scala-ide.org/sdk/helium/e38/scala210/stable/site.

  5. Enter

  6. 选择 Scala IDE for Eclipse如何操作...

  7. 点击 下一步,然后点击 完成。你将提示重启 Eclipse,Scala 将在重启后安装。

  8. 导航到 窗口 | 打开透视图 | Scala

现在 Eclipse 已经准备好进行 Scala 开发了!

使用 SBT 在 Eclipse 中开发 Spark 应用程序

简单构建工具SBT)是一个专门为 Scala 开发制作的构建工具。SBT 遵循基于 Maven 的命名约定和声明式依赖管理。

SBT 相比 Maven 提供以下增强功能:

  • 依赖项以键值对的形式存在于 build.sbt 文件中,而不是 Maven 中的 pom.xml 文件。

  • 它提供了一个使执行构建操作变得非常方便的壳。

  • 对于没有依赖项的简单项目,甚至不需要 build.sbt 文件

build.sbt 中,第一行是项目定义:

lazy val root = (project in file("."))

每个项目都有一个不可变的键值对映射。这个映射可以通过 SBT 中的设置进行更改,如下所示:

lazy val root = (project in file("."))
  settings(
    name := "wordcount"
  )

任何设置的改变都会导致一个新的映射,因为这是一个不可变的映射。

如何操作...

这是添加 sbteclipse 插件的方法:

  1. 将以下内容添加到全局插件文件中:

    $ mkdir /home/hduser/.sbt/0.13/plugins
    $ echo addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.5.0" )  > /home/hduser/.sbt/0.12/plugins/plugin.sbt
    
    

    或者,您还可以添加以下内容到您的项目中:

    $ cd <project-home>
    $ echo addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.5.0" )  > plugin.sbt
    
    
  2. 在没有任何参数的情况下启动 sbt 命令行:

    $sbt
    
    
  3. 输入 eclipse 并将其转换为 Eclipse 兼容的项目:

    $ eclipse
    
    
  4. 现在,您可以导航到 文件 | 导入 | 将现有项目导入工作区 来将项目加载到 Eclipse 中。

现在,您可以使用 Eclipse 和 SBT 在 Scala 中开发 Spark 应用程序。

使用 Maven 在 IntelliJ IDEA 中开发 Spark 应用程序

IntelliJ IDEA 内置了对 Maven 的支持。我们将在这个菜谱中看到如何创建一个新的 Maven 项目。

如何操作...

执行以下步骤以在 IntelliJ IDEA 中使用 Maven 开发 Spark 应用程序:

  1. 在新项目窗口中选择 Maven 并点击 下一步如何操作...

  2. 输入项目的三个维度:如何操作...

  3. 输入项目的名称和位置:如何操作...

  4. 点击 完成,Maven 项目就准备好了。

使用 SBT 在 IntelliJ IDEA 中开发 Spark 应用程序

在 Eclipse 变得著名之前,IntelliJ IDEA 被认为是 IDE 中的佼佼者。IDEA 仍然保持着过去的辉煌,许多开发者都喜欢 IDEA。IDEA 还有一个社区版,它是免费的。IDEA 为 SBT 和 Scala 开发提供了原生支持,使其成为 SBT 和 Scala 开发的理想选择。

如何操作...

执行以下步骤以在 IntelliJ IDEA 中使用 SBT 开发 Spark 应用程序:

  1. 添加 sbt-idea 插件。

  2. 将以下内容添加到全局插件文件中:

    $mkdir /home/hduser/.sbt/0.13/plugins
    $echo addSbtPlugin("com.github.mpeltone" % "sbt-idea" % "1.6.0" )  > /home/hduser/.sbt/0.12/plugins/plugin.sbt
    
    

    或者,您也可以添加到您的项目中:

    $cd <project-home>
    $ echo addSbtPlugin("com.github.mpeltone" % "sbt-idea" % "1.6.0" ) > plugin.sbt
    
    

IDEA 与 SBT 兼容并准备好使用。

现在,您可以使用 Scala 开发 Spark 代码,并使用 SBT 进行构建。

第三章. 外部数据源

Spark 的一项优势是它提供了一个单一的运行时,可以连接到各种底层数据源。

在本章中,我们将连接到不同的数据源。本章分为以下食谱:

  • 从本地文件系统加载数据

  • 从 HDFS 加载数据

  • 使用自定义 InputFormat 从 HDFS 加载数据

  • 从 Amazon S3 加载数据

  • 从 Apache Cassandra 加载数据

  • 从关系型数据库加载数据

简介

Spark 提供了一个统一的大数据运行时。HDFS,即 Hadoop 的文件系统,是 Spark 最常用的存储平台,因为它为通用硬件上的非结构化和半结构化数据提供了成本效益的存储。Spark 不限于 HDFS,并且可以与任何 Hadoop 支持的存储一起工作。

Hadoop 支持的存储意味着一种可以与 Hadoop 的 InputFormatOutputFormat 接口一起工作的存储格式。"InputFormat" 负责从输入数据创建 InputSplits 并将其进一步分割成记录。"OutputFormat" 负责写入存储。

我们将从写入本地文件系统开始,然后过渡到从 HDFS 加载数据。在 从 HDFS 加载数据 食谱中,我们将介绍最常用的文件格式:常规文本文件。在下一个食谱中,我们将介绍如何在 Spark 中使用任何 InputFormat 接口加载数据。我们还将探索从 Amazon S3 加载数据,这是一个领先的云存储平台。

我们将探索从 Apache Cassandra 加载数据,这是一个 NoSQL 数据库。最后,我们将探索从关系型数据库加载数据。

从本地文件系统加载数据

虽然由于磁盘大小限制和缺乏分布式特性,本地文件系统不适合存储大数据,但从技术上讲,您可以使用本地文件系统在分布式系统中加载数据。但此时您访问的文件/目录必须在每个节点上可用。

请注意,如果您计划使用此功能来加载旁路数据,这不是一个好主意。要加载旁路数据,Spark 有广播变量功能,将在后续章节中讨论。

在这个食谱中,我们将探讨如何在 Spark 中从本地文件系统加载数据。

如何做到这一点...

让我们从莎士比亚的 "to be or not to be" 的例子开始:

  1. 使用以下命令创建 words 目录:

    $ mkdir words
    
    
  2. 进入 words 目录:

    $ cd words
    
    
  3. 创建 sh.txt 文本文件,并在其中输入 "to be or not to be"

    $ echo "to be or not to be" > sh.txt
    
    
  4. 启动 Spark shell:

    $ spark-shell
    
    
  5. words 目录作为 RDD 加载:

    scala> val words = sc.textFile("file:///home/hduser/words")
    
    
  6. 计算行数:

    scala> words.count
    
    
  7. 将行(或行)分割成多个单词:

    scala> val wordsFlatMap = words.flatMap(_.split("\\W+"))
    
    
  8. word 转换为 (word,1)—即,将 1 作为 word 作为键的每个出现的值:

    scala> val wordsMap = wordsFlatMap.map( w => (w,1))
    
    
  9. 使用 reduceByKey 方法将每个键的单词出现次数相加(此函数一次处理两个连续的值,分别用 ab 表示):

    scala> val wordCount = wordsMap.reduceByKey( (a,b) => (a+b))
    
    
  10. 打印 RDD:

    scala> wordCount.collect.foreach(println)
    
    
  11. 将所有前面的操作合并为一步如下:

    scala> sc.textFile("file:///home/hduser/ words"). flatMap(_.split("\\W+")).map( w => (w,1)). reduceByKey( (a,b) => (a+b)).foreach(println)
    
    

这将产生以下输出:

如何做这件事...

从 HDFS 加载数据

HDFS 是最广泛使用的海量数据存储系统。HDFS 得到广泛采用的一个原因是它支持在读取时进行模式定义。这意味着 HDFS 在数据写入时不对数据进行任何限制。任何类型的数据都受欢迎,并且可以以原始格式存储。这一特性使其成为原始非结构化数据和半结构化数据的理想存储。

当涉及到读取数据时,即使是非结构化数据也需要一些结构才能有意义。Hadoop 使用 InputFormat 来确定如何读取数据。Spark 为 Hadoop 的 InputFormat 提供了完全支持,因此任何可以被 Hadoop 读取的数据也可以被 Spark 读取。

默认的 InputFormatTextInputFormatTextInputFormat 以行的字节偏移量作为键,以行的内容作为值。Spark 使用 sc.textFile 方法通过 TextInputFormat 读取。它忽略字节偏移量,并创建一个字符串 RDD。

有时文件名本身就包含有用的信息,例如时间序列数据。在这种情况下,您可能希望单独读取每个文件。sc.wholeTextFiles 方法允许您这样做。它使用文件名和路径(例如,hdfs://localhost:9000/user/hduser/words)作为键,并将整个文件的内容作为值。

Spark 还支持使用 DataFrames 读取各种序列化和压缩友好的格式,如 Avro、Parquet 和 JSON。这些格式将在后续章节中介绍。

在这个菜谱中,我们将探讨如何在 Spark shell 中从 HDFS 加载数据。

如何做这件事...

让我们进行词频统计,即统计每个单词出现的次数。在这个菜谱中,我们将从 HDFS 加载数据:

  1. 使用以下命令创建 words 目录:

    $ mkdir words
    
    
  2. 将目录更改为 words

    $ cd words
    
    
  3. 创建 sh.txt text 文件,并在其中输入 "to be or not to be"

    $ echo "to be or not to be" > sh.txt
    
    
  4. 启动 Spark shell:

    $ spark-shell
    
    
  5. words 目录作为 RDD 加载:

    scala> val words = sc.textFile("hdfs://localhost:9000/user/hduser/words")
    
    

    注意

    sc.textFile 方法也支持传递一个额外的参数来指定分区数。默认情况下,Spark 为每个 InputSplit 类创建一个分区,这大致对应于一个块。

    您可以请求更高的分区数。这对于计算密集型作业,如机器学习,效果非常好。由于一个分区不能包含多个块,因此不允许分区数少于块数。

  6. 计算行数(结果将是 1):

    scala> words.count
    
    
  7. 将行(或行)分割成多个单词:

    scala> val wordsFlatMap = words.flatMap(_.split("\\W+"))
    
    
  8. 将单词转换为(单词,1)——即对于每个作为键的 word 的出现,输出 1 作为值:

    scala> val wordsMap = wordsFlatMap.map( w => (w,1))
    
    
  9. 使用 reduceByKey 方法将每个单词出现的次数作为键(此函数一次处理两个连续的值,分别表示为 ab):

    scala> val wordCount = wordsMap.reduceByKey( (a,b) => (a+b))
    
    
  10. 打印 RDD:

    scala> wordCount.collect.foreach(println)
    
    
  11. 将所有前面的操作合并为一步如下:

    scala> sc.textFile("hdfs://localhost:9000/user/hduser/words"). flatMap(_.split("\\W+")).map( w => (w,1)). reduceByKey( (a,b) => (a+b)).foreach(println)
    
    

这将给出以下输出:

如何做这件事...

更多内容...

有时我们需要一次性访问整个文件。有时文件名包含有用的数据,例如在时间序列的情况下。有时需要将多行作为一个记录来处理。sparkContext.wholeTextFiles在这里提供了帮助。我们将查看来自 ftp://ftp.ncdc.noaa.gov/pub/data/noaa/的天气数据集。

这是一个顶级目录的外观:

还有更多…

查看特定年份的目录,例如,1901 看起来如下截图所示:

还有更多…

数据以这种方式划分,每个文件名都包含有用的信息,即 USAF-WBAN-year,其中 USAF 是美国空军站编号,WBAN 是气象局陆军海军位置编号。

你还会注意到所有文件都压缩为 gzip 格式,带有.gz扩展名。压缩是自动处理的,所以你只需要将数据上传到 HDFS。我们将在接下来的章节中回到这个数据集。

由于整个数据集不大,也可以在伪分布式模式下上传到 HDFS:

  1. 下载数据:

    $ wget -r ftp://ftp.ncdc.noaa.gov/pub/data/noaa/
    
    
  2. 在 HDFS 中加载天气数据:

    $ hdfs dfs -put ftp.ncdc.noaa.gov/pub/data/noaa weather/
    
    
  3. 启动 Spark shell:

    $ spark-shell
    
    
  4. 在 RDD 中加载 1901 年的天气数据:

    scala> val weatherFileRDD = sc.wholeTextFiles("hdfs://localhost:9000/user/hduser/weather/1901")
    
    
  5. 将天气缓存到 RDD 中,以便每次访问时不需要重新计算:

    scala> val weatherRDD = weatherFileRDD.cache
    
    

    注意

    在 Spark 中,RDD 可以持久化到各种 StorageLevels。rdd.cacherdd.persist(MEMORY_ONLY) StorageLevel 的简写。

  6. 计算元素数量:

    scala> weatherRDD.count
    
    
  7. 由于整个文件内容被作为一个元素加载,我们需要手动解释数据,所以让我们加载第一个元素:

    scala> val firstElement = weatherRDD.first
    
    
  8. 读取第一个 RDD 的值:

    scala> val firstValue = firstElement._2
    
    

    firstElement包含形式为(字符串,字符串)的元组。元组可以通过两种方式访问:

    • 使用以_1开始的定位函数。

    • 使用productElement方法,例如,tuple.productElement(0)。这里的索引从0开始,就像大多数其他方法一样。

  9. 按行拆分firstValue

    scala> val firstVals = firstValue.split("\\n")
    
    
  10. 计算firstVals中的元素数量:

    scala> firstVals.size
    
    
  11. 天气数据的模式非常丰富,文本的位置作为分隔符。你可以在国家气象服务网站上获取更多关于模式的信息。让我们获取风速,它位于第 66-69 节(以米/秒为单位):

    scala> val windSpeed = firstVals.map(line => line.substring(65,69)
    
    

使用自定义 InputFormat 从 HDFS 加载数据

有时你需要以特定格式加载数据,而TextInputFormat并不适合这种情况。Spark 为此提供了两种方法:

  • sparkContext.hadoopFile:这支持旧的 MapReduce API

  • sparkContext.newAPIHadoopFile:这支持新的 MapReduce API

这两种方法为 Hadoop 的所有内置 InputFormats 接口以及任何自定义InputFormat提供了支持。

如何做到这一点...

我们将使用KeyValueTextInputFormat加载键值格式的文本数据并将其加载到 Spark 中:

  1. 使用以下命令创建currency目录:

    $ mkdir currency
    
  2. 将当前目录更改为currency

    $ cd currency
    
  3. 创建na.txt文本文件,并以制表符分隔键值格式输入货币值(键:国家,值:货币):

    $ vi na.txt
    United States of America        US Dollar
    Canada  Canadian Dollar
    Mexico  Peso
    
    

    您可以为每个大洲创建更多文件。

  4. currency文件夹上传到 HDFS:

    $ hdfs dfs -put currency /user/hduser/currency
    
    
  5. 启动 Spark shell:

    $ spark-shell
    
    
  6. 导入语句:

    scala> import org.apache.hadoop.io.Text
    scala> import org.apache.hadoop.mapreduce.lib.input.KeyValueTextInputFormat
    
    
  7. currency目录作为 RDD 加载:

    val currencyFile = sc.newAPIHadoopFile("hdfs://localhost:9000/user/hduser/currency",classOf[KeyValueTextInputFormat],classOf[Text],classOf[Text])
    
    
  8. 将其从(Text,Text)元组转换为(String,String)元组:

    val currencyRDD = currencyFile.map( t => (t._1.toString,t._2.toString))
    
    
  9. 计算 RDD 中元素的数量:

    scala> currencyRDD.count
    
    
  10. 打印值:

    scala> currencyRDD.collect.foreach(println)
    
    

    如何操作...

注意

您可以使用这种方法加载任何 Hadoop 支持的InputFormat接口的数据。

从 Amazon S3 加载数据

亚马逊简单存储服务S3)为开发人员和 IT 团队提供了一个安全、持久和可扩展的存储平台。亚马逊 S3 的最大优势是无需前期 IT 投资,公司可以根据需要构建容量(只需点击一下按钮即可)。

虽然 Amazon S3 可以与任何计算平台一起使用,但它与亚马逊的云服务(如亚马逊弹性计算云EC2)和亚马逊弹性块存储EBS))集成得非常好。因此,使用亚马逊网络服务AWS)的公司很可能已经在 Amazon S3 上存储了大量的数据。

这正是从 Amazon S3 加载数据到 Spark 的好案例,这正是本菜谱的内容。

如何操作...

让我们从 AWS 门户开始:

  1. 前往aws.amazon.com,使用您的用户名和密码登录。

  2. 登录后,导航到存储与内容分发 | S3 | 创建存储桶如何操作...

  3. 输入存储桶名称——例如,com.infoobjects.wordcount。请确保您输入了一个唯一的存储桶名称(全球范围内没有两个 S3 存储桶可以具有相同的名称)。

  4. 选择区域,点击创建,然后点击您创建的存储桶名称,您将看到以下屏幕:如何操作...

  5. 点击创建文件夹,并将文件夹名称输入为words

  6. 在本地文件系统上创建sh.txt文本文件:

    $ echo "to be or not to be" > sh.txt
    
    
  7. 导航到单词 | 上传 | 添加文件,并在对话框中选择sh.txt,如图所示:如何操作...

  8. 点击开始上传

  9. 选择sh.txt并点击属性,它将显示文件的详细信息:如何操作...

  10. AWS_ACCESS_KEYAWS_SECRET_ACCESS_KEY设置为环境变量。

  11. 打开 Spark shell,并将words目录从s3加载到words RDD 中:

    scala>  val words = sc.textFile("s3n://com.infoobjects.wordcount/words")
    
    

现在 RDD 已加载,您可以在 RDD 上继续进行常规转换和操作。

注意

有时会在s3://s3n://之间产生混淆。s3n://表示位于 S3 存储桶中的常规文件,但对外部世界来说是可读和可写的。这个文件系统对文件大小设置了 5GB 的限制。

s3://表示位于 S3 桶中的 HDFS 文件。它是一个基于块的文件系统。该文件系统要求你为这个文件系统指定一个桶。在这个系统中,文件大小没有限制。

从 Apache Cassandra 加载数据

Apache Cassandra 是一个无主环集群结构的 NoSQL 数据库。虽然 HDFS 适合流式数据访问,但它不适合随机访问。例如,当你的平均文件大小为 100 MB 且你想读取整个文件时,HDFS 会工作得很好。如果你经常访问文件中的n行或其他部分作为记录,HDFS 会太慢。

关系型数据库传统上提供了解决方案,提供了低延迟和随机访问,但它们不适合大数据。例如,Cassandra 这样的 NoSQL 数据库通过提供关系型数据库类型的访问来填补这一空白,但是在分布式架构和商用服务器上。

在这个菜谱中,我们将从 Cassandra 加载数据作为 Spark RDD。为此,Cassandra 背后的公司 Datastax 贡献了spark-cassandra-connector。这个连接器允许你将 Cassandra 表加载为 Spark RDD,将 Spark RDD 写回 Cassandra,并执行 CQL 查询。

如何做到这一点...

执行以下步骤从 Cassandra 加载数据:

  1. 使用 CQL shell 在 Cassandra 中创建一个名为people的键空间:

    cqlsh> CREATE KEYSPACE people WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1 };
    
    
  2. 在 Cassandra 的新版本中创建一个名为person的列族(从 CQL 3.0 开始,也可以称为):

    cqlsh> create columnfamily person(id int primary key,first_name varchar,last_name varchar);
    
    
  3. 在列族中插入一些记录:

    cqlsh> insert into person(id,first_name,last_name) values(1,'Barack','Obama');
    cqlsh> insert into person(id,first_name,last_name) values(2,'Joe','Smith');
    
    
  4. 将 Cassandra 连接器依赖项添加到 SBT:

    "com.datastax.spark" %% "spark-cassandra-connector" % 1.2.0
    
    
  5. 你也可以将 Cassandra 依赖项添加到 Maven 中:

    <dependency>
      <groupId>com.datastax.spark</groupId>
      <artifactId>spark-cassandra-connector_2.10</artifactId>
      <version>1.2.0</version>
    </dependency>
    

    或者,你也可以下载spark-cassandra-connector JAR,直接与 Spark shell 一起使用:

    $ wget http://central.maven.org/maven2/com/datastax/spark/spark-cassandra-connector_2.10/1.1.0/spark-cassandra-connector_2.10-1.2.0.jar
    
    

    注意

    如果你想要构建包含所有依赖项的uber JAR,请参阅还有更多…部分。

  6. 现在启动 Spark shell。

  7. 在 Spark shell 中设置spark.cassandra.connection.host属性:

    scala> sc.getConf.set("spark.cassandra.connection.host", "localhost")
    
    
  8. 导入 Cassandra 特定的库:

    scala> import com.datastax.spark.connector._
    
    
  9. person列族加载为 RDD:

    scala> val personRDD = sc.cassandraTable("people","person")
    
    
  10. 计算 RDD 中的记录数:

    scala> personRDD.count
    
    
  11. 在 RDD 中打印数据:

    scala> personRDD.collect.foreach(println)
    
    
  12. 检索第一行:

    scala> val firstRow = personRDD.first
    
    
  13. 获取列名:

    scala> firstRow.columnNames
    
    
  14. Cassandra 也可以通过 Spark SQL 访问。它有一个围绕SQLContext的包装器,称为CassandraSQLContext;让我们加载它:

    scala> val cc = new org.apache.spark.sql.cassandra.CassandraSQLContext(sc)
    
    
  15. person数据加载为SchemaRDD:

    scala> val p = cc.sql("select * from people.person")
    
    
  16. 检索person数据:

    scala> p.collect.foreach(println)
    
    

还有更多...

Spark Cassandra 的连接器库有很多依赖项。连接器本身及其一些依赖项是 Spark 的第三方组件,并且不是 Spark 安装的一部分。

这些依赖项需要在运行时对驱动程序和执行器都可用。一种方法是将所有传递依赖项捆绑在一起,但这是一个费时且容易出错的过程。推荐的方法是将所有依赖项以及连接器库捆绑在一起。这将导致一个胖 JAR,通常称为uber JAR。

SBT 提供了 sbt-assembly 插件,这使得创建 uber JAR 非常容易。以下是为 spark-cassandra-connector 创建 uber JAR 的步骤。这些步骤足够通用,你可以使用它们来创建任何 uber JAR:

  1. 创建一个名为 uber 的文件夹:

    $ mkdir uber
    
    
  2. 将目录更改为 uber

    $ cd uber
    
    
  3. 打开 SBT 提示符:

    $ sbt
    
    
  4. 给这个项目命名为 sc-uber

    > set name := "sc-uber"
    
    
  5. 保存会话:

    > session save
    
    
  6. 退出会话:

    > exit
    
    

    这将在 uber 文件夹中创建 build.sbtprojecttarget 文件夹,如下截图所示:

    还有更多...

  7. build.sbt 文件的末尾添加 spark-cassandra-driver 依赖项,并在下面留一个空行,如下截图所示:

    $ vi buid.sbt
    
    

    还有更多...

  8. 我们将使用 MergeStrategy.first 作为默认策略。除此之外,还有一些文件,例如 manifest.mf,每个 JAR 都会打包用于元数据,我们可以简单地丢弃它们。我们将使用 MergeStrategy.discard 来处理这些文件。以下是在 build.sbt 中添加了 assemblyMergeStrategy 的截图:还有更多...

  9. 现在在 project 文件夹中创建 plugins.sbt 并为 sbt-assembly 插件输入以下内容:

    addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.12.0")
    
    
  10. 我们现在准备好构建(assembly)一个 JAR:

    $ sbt assembly
    
    

    uber JAR 已在 target/scala-2.10/sc-uber-assembly-0.1-SNAPSHOT.jar 中创建。

  11. 将其复制到保存所有第三方 JAR 的合适位置,例如 /home/hduser/thirdparty,并将其重命名为一个更容易的名字(除非你喜欢更长的名字):

    $ mv thirdparty/sc-uber-assembly-0.1-SNAPSHOT.jar  thirdparty/sc-uber.jar
    
    
  12. 使用 --jars 选项加载带有 uber JAR 的 Spark shell:

    $ spark-shell --jars thirdparty/sc-uber.jar
    
    
  13. 要将 Scala 代码提交到集群,你可以使用相同的 JARS 选项调用 spark-submit

    $ spark-submit --jars thirdparty/sc-uber.jar
    
    

sbt-assembly 中的合并策略

如果多个 JAR 有相同名称和相同相对路径的文件,sbt-assembly 插件的默认合并策略是验证所有文件的内容是否相同,如果不相同则报错。这种策略称为 MergeStrategy.deduplicate

以下是在 sbt-assembly 插件 中可用的合并策略:

策略名称 描述
MergeStrategy.deduplicate 默认策略
MergeStrategy.first 根据类路径选择第一个文件
MergeStrategy.last 根据类路径选择最后一个文件
MergeStrategy.singleOrError 报错(预期不会出现合并冲突)
MergeStrategy.concat 将所有匹配的文件连接在一起
MergeStrategy.filterDistinctLines 连接,忽略重复项
MergeStrategy.rename 重命名文件

从关系型数据库加载数据

许多重要数据都存储在 Spark 需要查询的关系型数据库中。JdbcRDD 是 Spark 的一个功能,允许关系型表被加载为 RDD。本食谱将解释如何使用 JdbcRDD。

在下一章中将要介绍的 Spark SQL 包含了一个 JDBC 数据源。这应该比当前的配方更受欢迎,因为结果以 DataFrames(将在下一章介绍)的形式返回,这些数据帧可以很容易地由 Spark SQL 处理,并且还可以与其他数据源联合。

准备工作

请确保 JDBC 驱动程序 JAR 在客户端节点上可见,以及在所有将运行执行器的从节点上。

如何操作…

执行以下步骤从关系型数据库加载数据:

  1. 使用以下 DDL 在 MySQL 中创建一个名为 person 的表:

    CREATE TABLE 'person' (
      'person_id' int(11) NOT NULL AUTO_INCREMENT,
      'first_name' varchar(30) DEFAULT NULL,
      'last_name' varchar(30) DEFAULT NULL,
      'gender' char(1) DEFAULT NULL,
      PRIMARY KEY ('person_id');
    )
    
  2. 插入一些数据:

    Insert into person values('Barack','Obama','M');
    Insert into person values('Bill','Clinton','M');
    Insert into person values('Hillary','Clinton','F');
    
  3. dev.mysql.com/downloads/connector/j/ 下载 mysql-connector-java-x.x.xx-bin.jar

  4. 使 MySQL 驱动程序对 Spark shell 可用并启动它:

    $ spark-shell --jars /path-to-mysql-jar/mysql-connector-java-5.1.29-bin.jar
    
    

    注意

    请注意,path-to-mysql-jar 不是实际的路径名称。您应该使用实际的路径名称。

  5. 为用户名、密码和 JDBC URL 创建变量:

    scala> val url="jdbc:mysql://localhost:3306/hadoopdb"
    scala> val username = "hduser"
    scala> val password = "******"
    
    
  6. 导入 JdbcRDD:

    scala> import org.apache.spark.rdd.JdbcRDD
    
    
  7. 导入 JDBC 相关类:

    scala> import java.sql.{Connection, DriverManager, ResultSet}
    
    
  8. 创建 JDBC 驱动程序的实例:

    scala> Class.forName("com.mysql.jdbc.Driver").newInstance
    
    
  9. 加载 JdbcRDD:

    scala> val myRDD = new JdbcRDD( sc, () =>
    DriverManager.getConnection(url,username,password) ,
    "select first_name,last_name,gender from person limit ?, ?",
    1, 5, 2, r => r.getString("last_name") + ", " + r.getString("first_name"))
    
    
  10. 现在查询结果:

    scala> myRDD.count
    scala> myRDD.foreach(println)
    
    
  11. 将 RDD 保存到 HDFS:

    scala> myRDD.saveAsTextFile("hdfs://localhost:9000/user/hduser/person")
    
    

它是如何工作的…

JdbcRDD 是一个在 JDBC 连接上执行 SQL 查询并检索结果的 RDD。以下是一个 JdbcRDD 构造函数:

JdbcRDD( SparkContext, getConnection: () => Connection,
sql: String, lowerBound: Long, upperBound: Long,
numPartitions: Int,  mapRow: (ResultSet) => T =
 JdbcRDD.resultSetToObjectArray)

两个 ? 是 JdbcRDD 内部预处理语句的绑定变量。第一个 ? 是偏移量(下限),即我们应该从哪一行开始计算,第二个 ? 是限制(上限),即我们应该读取多少行。

JdbcRDD 是一种在 Spark 中直接从关系型数据库加载数据的便捷方式。如果您想从 RDBMS 批量加载数据,还有其他更有效的方法,例如,Apache Sqoop 是一个强大的工具,可以从关系型数据库导入和导出到 HDFS。

第四章:Spark SQL

Spark SQL 是 Spark 模块,用于处理结构化数据。本章分为以下食谱:

  • 理解 Catalyst 优化器

  • 创建 HiveContext

  • 使用 case 类推断模式

  • 以编程方式指定模式

  • 使用 Parquet 格式加载数据和保存数据

  • 使用 JSON 格式加载数据和保存数据

  • 从关系数据库加载数据和保存数据

  • 从任意源加载数据和保存数据

简介

Spark 可以从各种数据源处理数据,例如 HDFS、Cassandra、HBase 和关系数据库,包括 HDFS。大数据框架(与关系数据库系统不同)在写入时不会强制执行模式。HDFS 是写入阶段欢迎任何任意文件的完美例子。然而,读取数据是另一回事。即使是完全非结构化的数据,也需要提供一些结构,以便从中获得意义。有了这种结构化数据,当涉及到分析时,SQL 就非常方便了。

Spark SQL 是 Spark 生态系统中的一个相对较新的组件,首次在 Spark 1.0 版本中引入。它包含一个名为 Shark 的项目,该项目的目的是让 Hive 在 Spark 上运行。

Hive 本质上是一种关系抽象,它将 SQL 查询转换为 MapReduce 作业。

简介

Shark 用 Spark 替换了 MapReduce 部分,同时保留了大部分代码库。

简介

初始时,它运行良好,但很快 Spark 的开发者遇到了瓶颈,无法进一步优化它。最终,他们决定从头开始编写 SQL 引擎,这催生了 Spark SQL。

简介

Spark SQL 解决了所有性能挑战,但必须与 Hive 保持兼容,因此,在SQLContext之上创建了一个新的包装上下文HiveContext

Spark SQL 支持使用标准 SQL 查询和 HiveQL(Hive 使用的类似 SQL 的查询语言)访问数据。在本章中,我们将探讨 Spark SQL 的不同特性。它支持 HiveQL 和 SQL 92 的子集。它可以在现有的 Hive 部署旁边或替换它们运行 SQL/HiveQL 查询。

运行 SQL 只是创建 Spark SQL 的部分原因。一个很大的原因是它有助于更快地创建和运行 Spark 程序。它让开发者编写更少的代码,读取更少的数据,并让 Catalyst 优化器完成所有繁重的工作。

Spark SQL 使用一种名为DataFrame的编程抽象。它是有名列组织的数据的分布式集合。DataFrame 相当于数据库表,但提供了更细粒度的优化。DataFrame API 还确保 Spark 的性能在不同语言绑定之间保持一致。

让我们对比 DataFrame 和 RDD。RDD 是一个不透明的对象集合,对底层数据的格式一无所知。相比之下,DataFrame 与它们关联了模式。您也可以将 DataFrame 看作是添加了模式的 RDD。实际上,直到 Spark 1.2,有一个名为SchemaRDD的工件,现在已经演变成了 DataFrame。它们提供了比 SchemaRDDs 更丰富的功能。

这额外的模式信息使得进行许多优化成为可能,而这些优化在其他情况下是不可能的。

DataFrames 也可以透明地从各种数据源加载,例如 Hive 表、Parquet 文件、JSON 文件和外部数据库使用 JDBC。DataFrames 可以被看作是行对象的 RDD,使用户能够调用如 map 这样的过程式 Spark API。

DataFrame API 在 Scala、Java、Python 中可用,从 Spark 1.4 开始也支持 R。

用户可以使用领域特定语言DSL)在 DataFrames 上执行关系操作。DataFrames 支持所有常见的关联操作,并且它们都接受在有限的 DSL 中的表达式对象,这使得 Spark 能够捕获表达式的结构。

我们将从 Spark SQL 的入口点开始,即 SQLContext。我们还将涵盖 HiveContext,它是 SQLContext 的包装器,用于支持 Hive 功能。请注意,HiveContext 经过了更多的实战检验,并提供了更丰富的功能,因此即使您不打算连接到 Hive,也强烈建议使用它。慢慢地,SQLContext 将达到与 HiveContext 相同的功能水平。

有两种方法可以将模式与 RDD 关联起来以创建 DataFrame。简单的方法是利用 Scala 的 case 类,我们将会首先介绍。Spark 使用 Java 反射从 case 类中推断模式。还有一种方法可以程序化地指定用于高级需求的模式,我们将在下一部分介绍。

Spark SQL 提供了一个简单的方法来加载和保存 Parquet 文件,这也会被涵盖。最后,我们将涵盖从 JSON 加载和保存数据。

理解 Catalyst 优化器

Spark SQL 的大部分功能都得益于 Catalyst 优化器,因此花些时间来理解它是很有意义的。

理解 Catalyst 优化器

它是如何工作的…

Catalyst 优化器主要利用 Scala 的函数式编程结构,如模式匹配。它提供了一个通用的框架来转换树,我们使用它来进行分析、优化、规划和运行时代码生成。

Catalyst 优化器有两个主要目标:

  • 使添加新的优化技术变得容易

  • 允许外部开发者扩展优化器

Spark SQL 在四个阶段使用 Catalyst 的转换框架:

  • 分析逻辑计划以解析引用

  • 逻辑计划优化

  • 物理规划

  • 代码生成,将查询的部分编译成 Java 字节码

分析

分析阶段涉及查看一个 SQL 查询或 DataFrame,从中创建一个逻辑计划,该计划仍然未解决(所引用的列可能不存在或数据类型可能不正确),然后使用目录对象(该对象连接到物理数据源)解决此计划,并创建一个逻辑计划,如下所示图所示:

分析

逻辑计划优化

逻辑计划优化阶段将对逻辑计划应用基于规则的标准化优化。这包括常量折叠、谓词下沉、投影剪枝、空值传播、布尔表达式简化以及其他规则。

我要特别关注谓词下沉规则。概念很简单;如果你在一个地方发出查询以运行针对大量数据,而数据存储在另一个地方,这可能导致大量不必要的数据在网络中移动。

如果我们可以将查询的一部分推送到数据存储的地方,从而过滤掉不必要的数据,这将显著减少网络流量。

逻辑计划优化

物理规划

在物理规划阶段,Spark SQL 将一个逻辑计划生成一个或多个物理计划。然后它测量每个物理计划的成本,并基于此生成一个物理计划。

物理规划

代码生成

查询优化的最后阶段涉及生成在每个机器上运行的 Java 字节码。它使用一个名为Quasi quotes的特殊 Scala 功能来完成此操作。

创建 HiveContext

SQLContext及其子类HiveContext是进入 Spark SQL 世界的两个入口点。HiveContext提供了比SQLContext提供的功能更全面的功能。附加功能包括:

  • 更完整且经过实战检验的 HiveQL 解析器

  • 访问 Hive UDFs

  • 能够从 Hive 表中读取数据

从 Spark 1.3 版本开始,Spark shell 自带 sqlContext(它是HiveContext的实例,而不是SQLContext)。如果你在 Scala 代码中创建SQLContext,可以使用SparkContext创建,如下所示:

val sc: SparkContext
val sqlContext = new org.apache.spark.sql.SQLContext(sc)

在本食谱中,我们将介绍如何创建HiveContext实例,然后通过 Spark SQL 访问 Hive 功能。

准备工作

要启用 Hive 功能,请确保你已经在所有工作节点上启用了 Hive(-Phive)assembly JAR,并且已将hive-site.xml复制到 Spark 安装的conf目录中。重要的是 Spark 能够访问hive-site.xml;否则,它将创建自己的 Hive 元数据存储,并且不会连接到你的现有 Hive 仓库。

默认情况下,Spark SQL 创建的所有表都是 Hive 管理的表,即 Hive 完全控制表的生命周期,包括使用 drop table 命令删除表元数据时。这仅适用于持久表。Spark SQL 还有一种机制,可以从 DataFrame 中创建临时表以简化查询编写,并且它们不由 Hive 管理。

请注意,Spark 1.4 支持 Hive 版本 0.13.1。您可以在使用 Maven 构建时,通过 -Phive-<version> build 选项指定您想要构建的 Hive 版本。例如,要使用 0.12.0 版本构建,您可以使用 -Phive-0.12.0

如何操作...

  1. 启动 Spark shell 并为其提供额外的内存:

    $ spark-shell --driver-memory 1G
    
    
  2. 创建一个 HiveContext 实例:

    scala> val hc = new org.apache.spark.sql.hive.HiveContext(sc)
    
    
  3. 创建一个具有 first_namelast_nameage 列的 Hive 表 Person

    scala>  hc.sql("create table if not exists person(first_name string, last_name string, age int) row format delimited fields terminated by ','")
    
    
  4. 在另一个 shell 中创建 person 数据到本地文件中:

    $ mkdir person
    $ echo "Barack,Obama,53" >> person/person.txt
    $ echo "George,Bush,68" >> person/person.txt
    $ echo "Bill,Clinton,68" >> person/person.txt
    
    
  5. person 表中的数据加载进来:

    scala> hc.sql("load data local inpath \"/home/hduser/person\" into table person")
    
    
  6. 或者,您也可以从 HDFS 将数据加载到 person 表中:

    scala> hc.sql("load data inpath \"/user/hduser/person\" into table person")
    
    

    注意

    请注意,使用 load data inpath 将数据从另一个 HDFS 位置移动到 Hive 的 warehouse 目录,默认情况下为 /user/hive/warehouse。您也可以指定完全限定的路径,例如 hdfs://localhost:9000/user/hduser/person

  7. 使用 HiveQL 选择人员数据:

    scala> val persons = hc.sql("from person select first_name,last_name,age")
    scala> persons.collect.foreach(println)
    
    
  8. select 查询的结果创建一个新的表:

    scala> hc.sql("create table person2 as select first_name, last_name from person;")
    
    
  9. 您还可以直接从一个表复制到另一个表:

    scala> hc.sql("create table person2 like person location '/user/hive/warehouse/person'")
    
    
  10. 创建两个表 people_by_last_namepeople_by_age 以保持计数:

    scala> hc.sql("create table people_by_last_name(last_name string,count int)")
    scala> hc.sql("create table people_by_age(age int,count int)")
    
    
  11. 您还可以使用 HiveQL 查询将记录插入到多个表中:

    scala> hc.sql("""from person
     insert overwrite table people_by_last_name
     select last_name, count(distinct first_name)
     group by last_name
    insert overwrite table people_by_age
     select age, count(distinct first_name)
     group by age; """)
    
    

使用 case 类推断模式:

Case 类是 Scala 中的特殊类,它们为您提供了构造函数、获取器(访问器)、equals 和 hashCode 的样板实现,并实现了 Serializable。Case 类非常适合将数据封装为对象。熟悉 Java 的读者可以将它们与 plain old Java objectsPOJOs)或 Java bean 相关联。

Case 类的美丽之处在于,在 Java 中需要做的所有繁琐工作都可以用单行代码在 case 类中完成。Spark 使用反射来推断 case 类的模式。

如何操作...

  1. 启动 Spark shell 并为其提供额外的内存:

    $ spark-shell --driver-memory 1G
    
    
  2. 导入隐式转换:

    scala> import sqlContext.implicits._
    
    
  3. 创建一个 Person case 类:

    scala> case class Person(first_name:String,last_name:String,age:Int)
    
    
  4. 在另一个 shell 中创建一些样本数据以放入 HDFS:

    $ mkdir person
    $ echo "Barack,Obama,53" >> person/person.txt
    $ echo "George,Bush,68" >> person/person.txt
    $ echo "Bill,Clinton,68" >> person/person.txt
    $ hdfs dfs -put person person
    
    
  5. person 目录加载为 RDD:

    scala> val p = sc.textFile("hdfs://localhost:9000/user/hduser/person")
    
    
  6. 根据逗号分隔符将每一行拆分为字符串数组:

    val pmap = p.map( line => line.split(","))
    
    
  7. 将 Array[String] 的 RDD 转换为 Person case 对象的 RDD:

    scala> val personRDD = pmap.map( p => Person(p(0),p(1),p(2).toInt))
    
    
  8. personRDD 转换为 personDF DataFrame:

    scala> val personDF = personRDD.toDF
    
    
  9. personDF 注册为表:

    scala> personDF.registerTempTable("person")
    
    
  10. 对其运行 SQL 查询:

    scala> val people = sql("select * from person")
    
    
  11. persons 获取输出值:

    scala> people.collect.foreach(println)
    
    

以编程方式指定模式:

有几种情况,case 类可能不起作用;其中之一是 case 类不能超过 22 个字段。另一种情况可能是你事先不知道模式。在这种情况下,数据被加载为 Row 对象的 RDD。模式是单独使用 StructTypeStructField 对象创建的,分别代表表和字段。模式应用于 Row RDD 以创建 DataFrame。

如何做到这一点…

  1. 启动 Spark shell 并给它一些额外的内存:

    $ spark-shell --driver-memory 1G
    
    
  2. 导入用于隐式转换:

    scala> import sqlContext.implicit._
    
    
  3. 导入 Spark SQL 数据类型和 Row 对象:

    scala> import org.apache.spark.sql._
    scala> import org.apache.spark.sql.types._
    
    
  4. 在另一个 shell 中,创建一些要放入 HDFS 中的样本数据:

    $ mkdir person
    $ echo "Barack,Obama,53" >> person/person.txt
    $ echo "George,Bush,68" >> person/person.txt
    $ echo "Bill,Clinton,68" >> person/person.txt
    $ hdfs dfs -put person person
    
    
  5. 在 RDD 中加载 person 数据:

    scala> val p = sc.textFile("hdfs://localhost:9000/user/hduser/person")
    
    
  6. 根据逗号分隔符将每一行拆分为字符串数组:

    scala> val pmap = p.map( line => line.split(","))
    
    
  7. array[string] 的 RDD 转换为 Row 对象的 RDD:

    scala> val personData = pmap.map( p => Row(p(0),p(1),p(2).toInt))
    
    
  8. 使用 StructTypeStructField 对象创建模式。StructField 对象以参数名、参数类型和可空性形式接收参数:

    scala> val schema = StructType(
     Array(StructField("first_name",StringType,true),
    StructField("last_name",StringType,true),
    StructField("age",IntegerType,true)
    ))
    
    
  9. 应用模式以创建 personDF DataFrame:

    scala> val personDF = sqlContext.createDataFrame(personData,schema)
    
    
  10. personDF 注册为表:

    scala> personDF.registerTempTable("person")
    
    
  11. 对其运行 SQL 查询:

    scala> val persons = sql("select * from person")
    
    
  12. persons 获取输出值:

    scala> persons.collect.foreach(println)
    
    

在这个菜谱中,我们学习了如何通过程序指定模式来创建 DataFrame。

它是如何工作的…

StructType 对象定义了模式。你可以将其视为关系世界中表或行的等价物。StructType 接收一个 StructField 对象的数组,如下面的签名所示:

StructType(fields: Array[StructField])

StructField 对象具有以下签名:

StructField(name: String, dataType: DataType, nullable: Boolean = true, metadata: Metadata = Metadata.empty)

这里有一些关于使用参数的更多信息:

  • name:这代表字段的名称。

  • dataType:这显示了该字段的数据类型。

    允许以下数据类型:

    IntegerType FloatType
    BooleanType ShortType
    LongType ByteType
    DoubleType StringType
  • nullable:这显示了该字段是否可以为空。

  • metadata:这显示了该字段的元数据。元数据是 Map[String,Any] 的包装器,因此它可以包含任何任意元数据。

使用 Parquet 格式加载数据和保存数据

Apache Parquet 是一种列式数据存储格式,专门为大数据存储和处理而设计。Parquet 基于 Google Dremel 论文中的记录切割和组装算法。在 Parquet 中,单个列中的数据是连续存储的。

列式格式给 Parquet 带来一些独特的优势。例如,如果你有一个有 100 列的表,而你主要访问 10 列,在基于行的格式中,你将不得不加载所有 100 列,因为粒度级别是行级别。但在 Parquet 中,你将只加载 10 列。另一个好处是,由于给定列中的所有数据都是相同的数据类型(根据定义),压缩效率要高得多。

如何做到这一点…

  1. 打开终端并在本地文件中创建 person 数据:

    $ mkdir person
    $ echo "Barack,Obama,53" >> person/person.txt
    $ echo "George,Bush,68" >> person/person.txt
    $ echo "Bill,Clinton,68" >> person/person.txt
    
    
  2. person 目录上传到 HDFS:

    $ hdfs dfs -put person /user/hduser/person
    
    
  3. 启动 Spark shell 并给它一些额外的内存:

    $ spark-shell --driver-memory 1G
    
    
  4. 导入用于隐式转换:

    scala> import sqlContext.implicits._
    
    
  5. Person创建一个案例类:

    scala> case class Person(firstName: String, lastName: String, age:Int)
    
    
  6. 从 HDFS 加载person目录并将其映射到Person案例类:

    scala> val personRDD = sc.textFile("hdfs://localhost:9000/user/hduser/person").map(_.split("\t")).map(p => Person(p(0),p(1),p(2).toInt))
    
    
  7. personRDD转换为person DataFrame:

    scala> val person = personRDD.toDF
    
    
  8. person DataFrame 注册为临时表,以便可以对其运行 SQL 查询。请注意,DataFrame 的名称不必与表名相同。

    scala> person.registerTempTable("person")
    
    
  9. 选择所有 60 岁以上的个人:

    scala> val sixtyPlus = sql("select * from person where age > 60")
    
    
  10. 打印值:

    scala> sixtyPlus.collect.foreach(println)
    
    
  11. 让我们将这个sixtyPlus RDD 以 Parquet 格式保存:

    scala> sixtyPlus.saveAsParquetFile("hdfs://localhost:9000/user/hduser/sp.parquet")
    
    
  12. 上一步在 HDFS 根目录中创建了一个名为sp.parquet的目录。您可以在另一个 shell 中运行hdfs dfs -ls命令以确保它已创建:

    $ hdfs dfs -ls sp.parquet
    
    
  13. 在 Spark shell 中加载 Parquet 文件的正文:

    scala> val parquetDF = sqlContext.load("hdfs://localhost:9000/user/hduser/sp.parquet")
    
    
  14. 将加载的parquet DF 注册为temp表:

    scala> 
    parquetDF
    .registerTempTable("sixty_plus")
    
    
  15. 对前面的temp表运行查询:

    scala> sql("select * from sixty_plus")
    
    

它是如何工作的...

让我们花些时间更深入地了解 Parquet 格式。以下是以表格格式表示的示例数据:

First_Name Last_Name Age
Barack Obama 53
George Bush 68
Bill Clinton 68

在行格式中,数据将存储如下:

Barack Obama 53 George Bush 68 Bill Clinton 68

在列式布局中,数据将存储如下:

行组 => Barack George Bill Obama Bush Clinton 53 68 68
列块 列块 列块

这里简要描述了不同部分:

  • 行组:这显示了数据按行进行水平分区。行组由列块组成。

  • 列块:列块包含行组中给定列的数据。列块始终在物理上是连续的。每个列只有一个列块。

  • 页面:列块被分成页面。页面是存储单元,不能进一步分割。页面在列块中连续写入。页面的数据可以进行压缩。

如果 Hive 表中已有数据,例如person表,您可以直接通过以下步骤以 Parquet 格式保存它:

  1. 创建一个名为person_parquet的表,其模式与person相同,但存储格式为 Parquet(从 Hive 0.13 开始):

    hive> create table person_parquet like person stored as parquet
    
    
  2. 通过从person表导入数据来在person_parquet表中插入数据:

    hive> insert overwrite table person_parquet select * from person;
    
    

小贴士

有时,从其他来源导入的数据,如 Impala,将字符串保存为二进制。在读取时将其转换为字符串,请在SparkConf中设置以下属性:

scala> sqlContext.setConf("spark.sql.parquet.binaryAsString","true")

还有更多...

如果您使用的是 Spark 1.4 或更高版本,则有一个新的接口既可以写入也可以读取 Parquet。要将数据写入 Parquet(步骤 11 重写),让我们将这个sixtyPlus RDD 保存到 Parquet 格式(RDD 隐式转换为 DataFrame):

scala>sixtyPlus.write.parquet("hdfs://localhost:9000/user/hduser/sp.parquet")

要从 Parquet(步骤 13 重写;结果是 DataFrame)读取,请在 Spark shell 中加载 Parquet 文件的正文:

scala>val parquetDF = sqlContext.read.parquet("hdfs://localhost:9000/user/hduser/sp.parquet")

使用 JSON 格式加载数据和保存数据

JSON 是一种轻量级的数据交换格式。它基于 JavaScript 编程语言的一个子集。JSON 的流行度与 XML 的不流行度直接相关。XML 是一种为纯文本格式中的数据提供结构的优秀解决方案。随着时间的推移,XML 文档变得越来越重,开销不值得。

JSON 通过提供最小开销的结构解决了这个问题。有些人称 JSON 为 无脂肪 XML

JSON 语法遵循以下规则:

  • 数据以键值对的形式存在:

    "firstName" : "Bill"
    
  • JSON 有四种数据类型:

    • 字符串 ("firstName" : "Barack")

    • 数字 ("age" : 53)

    • 布尔 ("alive": true)

    • 空值 ("manager" : null)

  • 数据由逗号分隔

  • 花括号 {} 代表一个对象:

    { "firstName" : "Bill", "lastName": "Clinton", "age": 68 }
    
  • 方括号 [] 代表一个数组:

    [{ "firstName" : "Bill", "lastName": "Clinton", "age": 68 },{"firstName": "Barack","lastName": "Obama", "age": 43}]
    

在本食谱中,我们将探讨如何以 JSON 格式保存和加载数据。

如何做…

  1. 打开终端并在 JSON 格式中创建 person 数据:

    $ mkdir jsondata
    $ vi jsondata/person.json
    {"first_name" : "Barack", "last_name" : "Obama", "age" : 53}
    {"first_name" : "George", "last_name" : "Bush", "age" : 68 }
    {"first_name" : "Bill", "last_name" : "Clinton", "age" : 68 }
    
    
  2. jsondata 目录上传到 HDFS:

    $ hdfs dfs -put jsondata /user/hduser/jsondata
    
    
  3. 启动 Spark shell 并给它一些额外的内存:

    $ spark-shell --driver-memory 1G
    
    
  4. 创建 SQLContext 的实例:

    scala> val sqlContext = new org.apache.spark.sql.SQLContext(sc)
    
    
  5. 导入隐式转换:

    scala> import sqlContext.implicits._
    
    
  6. 从 HDFS 加载 jsondata 目录:

    scala> val person = sqlContext.jsonFile("hdfs://localhost:9000/user/hduser/jsondata")
    
    
  7. person DF 注册为 temp 表,以便可以针对它运行 SQL 查询:

    scala> person.registerTempTable("person")
    
    
  8. 选择所有年龄超过 60 岁的人:

    scala> val sixtyPlus = sql("select * from person where age > 60")
    
    
  9. 打印值:

    scala> sixtyPlus.collect.foreach(println)
    
  10. 让我们将这个 sixtyPlus DF 以 JSON 格式保存

    scala> sixtyPlus.toJSON.saveAsTextFile("hdfs://localhost:9000/user/hduser/sp")
    
    
  11. 最后一步在 HDFS 根目录下创建了一个名为 sp 的目录。您可以在另一个 shell 中运行 hdfs dfs -ls 命令以确保它已创建:

    $ hdfs dfs -ls sp
    
    

它是如何工作的…

sc.jsonFile 内部使用 TextInputFormat,它一次处理一行。因此,一个 JSON 记录不能跨多行。如果您使用多行,它将是有效的 JSON 格式,但与 Spark 不兼容,并会抛出异常。

一行中可以包含多个对象。例如,您可以将两个人的信息放在一行中作为一个数组,如下所示:

[{"firstName":"Barack", "lastName":"Obama"},{"firstName":"Bill", "lastName":"Clinton"}]

本食谱总结了使用 Spark 保存和加载数据的 JSON 格式。

更多内容…

如果您使用 Spark 版本 1.4 或更高版本,SqlContext 提供了一个更简单的接口来从 HDFS 加载 jsondata 目录:

scala> val person = sqlContext.read.json ("hdfs://localhost:9000/user/hduser/jsondata")

sqlContext.jsonFile 在 1.4 版本中已弃用,sqlContext.read.json 是推荐的方法。

从关系型数据库加载数据和保存数据

在上一章中,我们学习了如何使用 JdbcRDD 从关系型数据中加载数据到 RDD。Spark 1.4 支持直接从 JDBC 资源加载数据到 Dataframe。本食谱将探讨如何实现它。

准备工作

请确保 JDBC 驱动程序 JAR 文件在客户端节点以及所有将运行执行器的从节点上可见。

如何做…

  1. 使用以下 DDL 在 MySQL 中创建一个名为 person 的表:

    CREATE TABLE 'person' (
      'person_id' int(11) NOT NULL AUTO_INCREMENT,
      'first_name' varchar(30) DEFAULT NULL,
      'last_name' varchar(30) DEFAULT NULL,
      'gender' char(1) DEFAULT NULL,
      'age' tinyint(4) DEFAULT NULL,
      PRIMARY KEY ('person_id')
    )
    
  2. 插入一些数据:

    Insert into person values('Barack','Obama','M',53);
    Insert into person values('Bill','Clinton','M',71);
    Insert into person values('Hillary','Clinton','F',68);
    Insert into person values('Bill','Gates','M',69);
    Insert into person values('Michelle','Obama','F',51);
    
  3. dev.mysql.com/downloads/connector/j/ 下载 mysql-connector-java-x.x.xx-bin.jar

  4. 将 MySQL 驱动程序提供给 Spark shell 并启动它:

    $ spark-shell --driver-class-path/path-to-mysql-jar/mysql-connector-java-5.1.34-bin.jar
    
    

    注意

    请注意,path-to-mysql-jar 不是实际的路径名称。您需要使用自己的路径名称。

  5. 构建 JDBC URL:

    scala> val url="jdbc:mysql://localhost:3306/hadoopdb"
    
    
  6. 使用用户名和密码创建连接属性对象:

    scala> val prop = new java.util.Properties
    scala> prop.setProperty("user","hduser")
    scala> prop.setProperty("password","********")
    
    
  7. 使用 JDBC 数据源(url、表名、属性)加载数据框:

     scala> val people = sqlContext.read.jdbc(url,"person",prop)
    
    
  8. 通过执行以下命令以美观的表格格式显示结果:

    scala> people.show
    
    
  9. 这已经加载了整个表。如果我只想加载男性(url、表名、谓词、属性)怎么办?为此,请运行以下命令:

    scala> val males = sqlContext.read.jdbc(url,"person",Array("gender='M'"),prop)
    scala> males.show
    
    
  10. 通过执行以下命令仅显示名字:

    scala> val first_names = people.select("first_name")
    scala> first_names.show
    
    
  11. 通过执行以下命令仅显示 60 岁以下的人:

    scala> val below60 = people.filter(people("age") < 60)
    scala> below60.show
    
    
  12. 按性别分组人员如下:

    scala> val grouped = people.groupBy("gender")
    
    
  13. 通过执行以下命令查找男性和女性的数量:

    scala> val gender_count = grouped.count
    scala> gender_count.show
    
    
  14. 通过执行以下命令查找男性和女性的平均年龄:

    scala> val avg_age = grouped.avg("age")
    scala> avg_age.show
    
    
  15. 现在如果您想将此 avg_age 数据保存到新表中,请运行以下命令:

    scala> gender_count.write.jdbc(url,"gender_count",prop)
    
    
  16. 将 people DataFrame 保存为 Parquet 格式:

    scala> people.write.parquet("people.parquet")
    
    
  17. 将 people DataFrame 保存为 JSON 格式:

    scala> people.write.json("people.json")
    
    

从任意源加载数据和保存数据

到目前为止,我们已经介绍了三种内置在 DataFrame 中的数据源——parquet(默认)、jsonjdbc。DataFrame 并不仅限于这三种,可以通过手动指定格式来加载和保存到任何任意数据源。

在本菜谱中,我们将介绍从任意源加载数据和保存数据的方法。

如何操作...

  1. 启动 Spark shell 并为其提供额外的内存:

    $ spark-shell --driver-memory 1G
    
    
  2. 从 Parquet 加载数据;由于 parquet 是默认的数据源,您无需指定它:

    scala> val people = sqlContext.read.load("hdfs://localhost:9000/user/hduser/people.parquet") 
    
    
  3. 通过手动指定格式从 Parquet 加载数据:

    scala> val people = sqlContext.read.format("org.apache.spark.sql.parquet").load("hdfs://localhost:9000/user/hduser/people.parquet") 
    
    
  4. 对于内置数据类型(parquetjsonjdbc),您无需指定完整的格式名称,只需指定 "parquet""json""jdbc" 即可:

    scala> val people = sqlContext.read.format("parquet").load("hdfs://localhost:9000/user/hduser/people.parquet") 
    
    

    注意

    在写入数据时,有四种保存模式:appendoverwriteerrorIfExistsignoreappend 模式向数据源添加数据,overwrite 覆盖它,errorIfExists 抛出一个异常,表示数据已存在,而 ignore 在数据已存在时什么都不做。

  5. append 模式将 people 保存为 JSON:

    scala> val people = people.write.format("json").mode("append").save ("hdfs://localhost:9000/user/hduser/people.json") 
    
    

还有更多...

Spark SQL 的数据源 API 支持保存到多种数据源。要获取更多信息,请访问 spark-packages.org/

第五章:Spark Streaming

Spark Streaming 为 Apache Spark 添加了大数据处理的圣杯——即实时分析。它使 Spark 能够摄取实时数据流,并在几秒钟的低延迟下提供实时智能。

在本章中,我们将介绍以下食谱:

  • 使用流式处理进行词频统计

  • 流式处理 Twitter 数据

  • 使用 Kafka 进行流式处理

简介

流式处理是将持续流动的输入数据划分为离散单元的过程,以便可以轻松处理。现实生活中的熟悉例子是流式视频和音频内容(尽管用户可以在观看之前下载完整电影,但更快的解决方案是流式传输数据的小块,这些小块开始播放给用户,而其余的数据则在后台下载)。

除了多媒体之外,流式处理的现实世界例子还包括处理市场数据、天气数据、电子股票交易数据等。所有这些应用都以非常快的速度产生大量数据,并需要特殊处理数据,以便可以从数据中实时提取见解。

在我们专注于 Spark Streaming 之前,了解一些基本概念会更好。流式应用程序接收数据的速率称为数据速率,并以千字节每秒kbps)或兆字节每秒mbps)的形式表示。

流式处理的一个重要用例是复杂事件处理CEP)。在 CEP 中,控制正在处理的数据范围非常重要。这个范围被称为窗口,可以是基于时间或大小的。一个基于时间的窗口示例是分析最后一分钟内到达的数据。一个基于大小的窗口示例可以是给定股票最后 100 笔交易的平均询问价格。

Spark Streaming 是 Spark 的库,它提供了处理实时数据的支持。这个流可以从任何来源获取,例如 Twitter、Kafka 或 Flume。

Spark Streaming 有几个基本构建块,在深入到食谱之前,我们需要很好地理解它们。

Spark Streaming 有一个名为StreamingContext的上下文包装器,它围绕SparkContext包装,是 Spark Streaming 功能的入口点。根据定义,流式数据是连续的,需要被时间切片以进行处理。这个时间切片被称为批处理间隔,在创建StreamingContext时指定。RDD 和批处理之间存在一对一的映射,即每个批次产生一个 RDD。正如您在以下图像中可以看到的,Spark Streaming 获取连续数据,将其划分为批次,并馈送到 Spark。

简介

批处理间隔对于优化你的流式应用程序非常重要。理想情况下,你希望以至少与数据摄入相同的速度处理数据;否则,你的应用程序将产生积压。Spark Streaming 在批处理间隔期间收集数据,例如,2 秒。当这个 2 秒间隔结束时,该间隔收集的数据将被交给 Spark 进行处理,而 Streaming 将专注于收集下一个批处理间隔的数据。现在,这个 2 秒的批处理间隔是 Spark 处理数据的时间,因为它应该有自由接收下一个批处理数据的空间。如果 Spark 可以更快地处理数据,你可以将批处理间隔减少到,例如,1 秒。如果 Spark 无法跟上这个速度,你必须增加批处理间隔。

Spark Streaming 中 RDD 的连续流需要通过一个抽象来表示,这样它就可以被处理。这个抽象被称为离散流DStream)。对 DStream 应用的任何操作都会导致对底层 RDD 的操作。

每个输入 DStream 都与一个接收器相关联(除了文件流)。接收器从输入源接收数据并将其存储在 Spark 的内存中。有两种类型的流式源:

  • 基本源,例如文件和套接字连接

  • 高级源,例如 Kafka 和 Flume

Spark Streaming 还提供了窗口计算,你可以对数据的滑动窗口应用转换。滑动窗口操作基于两个参数:

  • 窗口长度:这是窗口的持续时间。例如,如果你想获取最后 1 分钟数据的分析,窗口长度将是 1 分钟。

  • 滑动间隔:这描述了你希望多久执行一次操作。比如说,你想每 10 秒执行一次操作;这意味着每 10 秒,1 分钟的窗口将有 50 秒的数据与上一个窗口相同,并且有 10 秒的新数据。

这两个参数都作用于底层的 RDDs,显然,它们不能被拆分;因此,这两个都应该是最小批处理间隔的倍数。窗口长度也必须是滑动间隔的倍数。

DStream 也有输出操作,允许数据被推送到外部系统。它们类似于 RDD 上的操作(在 DStream 中发生的事情实际上是 RDD 的一个更高层次的抽象)。

除了打印 DStream 的内容外,还支持标准 RDD 操作,如saveAsTextFilesaveAsObjectFilesaveAsHadoopFile,分别有类似的对应操作,如saveAsTextFilessaveAsObjectFilessaveAsHadoopFiles

一个非常有用的输出操作是foreachRDD(func),它将任何任意函数应用于所有 RDDs。

使用 Streaming 进行词频统计

让我们从 Streaming 的一个简单例子开始,在这个例子中,我们将在一个终端中输入一些文本,而 Streaming 应用程序将在另一个窗口中捕获它。

如何做到这一点...

  1. 启动 Spark shell 并给它一些额外的内存:

    $ spark-shell --driver-memory 1G
    
    
  2. 流特定导入:

    scala> import org.apache.spark.SparkConf
    scala> import org.apache.spark.streaming.{Seconds, StreamingContext}
    scala> import org.apache.spark.storage.StorageLevel
    scala> import StorageLevel._
    
    
  3. 导入隐式转换:

    scala> import org.apache.spark._
    scala> import org.apache.spark.streaming._
    scala> import org.apache.spark.streaming.StreamingContext._
    
    
  4. 创建具有 2 秒批处理间隔的StreamingContext

    scala> val ssc = new StreamingContext(sc, Seconds(2))
    
    
  5. 在本地主机上创建具有8585端口的SocketTextStream Dstream,并使用MEMORY_ONLY缓存:

    scala> val lines = ssc.socketTextStream("localhost",8585,MEMORY_ONLY)
    
    
  6. 将行分割成多个单词:

    scala> val wordsFlatMap = lines.flatMap(_.split(" "))
    
    
  7. 将单词转换为(单词,1),即输出1作为每个单词出现的键的值:

    scala> val wordsMap = wordsFlatMap.map( w => (w,1))
    
    
  8. 使用reduceByKey方法将每个单词的出现次数作为键(该函数一次处理两个连续的值,分别表示为ab):

    scala> val wordCount = wordsMap.reduceByKey( (a,b) => (a+b))
    
    
  9. 打印wordCount

    scala> wordCount.print
    
    
  10. 启动StreamingContext;记住,直到StreamingContext启动之前,没有任何事情发生:

    scala> ssc.start
    
    
  11. 现在,在另一个窗口中,启动 netcat 服务器:

    $ nc -lk 8585
    
    
  12. 输入不同的行,例如to be or not to be

    to be or not to be
    
    
  13. 检查 Spark shell,您将看到如下截图所示的单词计数结果:如何操作...

流式传输 Twitter 数据

Twitter 是一个著名的微博平台。它每天产生约 5 亿条推文,产生了大量数据。Twitter 允许通过 API 访问其数据,这使得它成为测试任何大数据流式应用的最佳示例。

在这个菜谱中,我们将看到如何使用 Twitter 流式库在 Spark 中实时流式传输数据。Twitter 只是向 Spark 提供流式数据的一个来源,并没有特殊地位。因此,没有为 Twitter 提供内置库。尽管如此,Spark 确实提供了一些 API 来简化与 Twitter 库的集成。

一个使用实时 Twitter 数据流的示例用途是找到过去 5 分钟内的热门推文。

如何操作...

  1. 如果您还没有,请创建一个 Twitter 账户。

  2. 前往apps.twitter.com

  3. 点击创建新应用

  4. 输入名称描述网站,然后点击创建您的 Twitter 应用如何操作...

  5. 您将到达应用程序管理屏幕。

  6. 导航到密钥和访问令牌 | 创建我的访问令牌如何操作...

  7. 在此屏幕上记下我们将用于第 14 步的四个值:

    消费者密钥(API 密钥

    消费者密钥(API 密钥

    访问令牌

    访问令牌密钥

  8. 我们将需要在一段时间内提供此屏幕上的值,但现在,让我们从 Maven central 下载所需的第三方库:

    $ wget http://central.maven.org/maven2/org/apache/spark/spark-streaming-twitter_2.10/1.2.0/spark-streaming-twitter_2.10-1.2.0.jar
    $ wget http://central.maven.org/maven2/org/twitter4j/twitter4j-stream/4.0.2/twitter4j-stream-4.0.2.jar
    $ wget http://central.maven.org/maven2/org/twitter4j/twitter4j-core/4.0.2/twitter4j-core-4.0.2.jar
    
    
  9. 打开 Spark shell,提供前面的三个 JARS 作为依赖项:

    $ spark-shell --jars spark-streaming-twitter_2.10-1.2.0.jar, twitter4j-stream-4.0.2.jar,twitter4j-core-4.0.2.jar
    
    
  10. 执行特定的 Twitter 导入:

    scala> import org.apache.spark.streaming.twitter._
    scala> import twitter4j.auth._
    scala> import twitter4j.conf._
    
    
  11. 流特定导入:

    scala> import org.apache.spark.streaming.{Seconds, StreamingContext}
    
    
  12. 导入隐式转换:

    scala> import org.apache.spark._
    scala> import org.apache.spark.streaming._
    scala> import org.apache.spark.streaming.StreamingContext._
    
    
  13. 创建具有 10 秒批处理间隔的StreamingContext

    scala> val ssc = new StreamingContext(sc, Seconds(10))
    
    
  14. 创建具有 2 秒批处理间隔的StreamingContext

    scala> val cb = new ConfigurationBuilder
    scala> cb.setDebugEnabled(true)
    .setOAuthConsumerKey("FKNryYEKeCrKzGV7zuZW4EKeN")
    .setOAuthConsumerSecret("x6Y0zcTBOwVxpvekSCnGzbi3NYNrM5b8ZMZRIPI1XRC3pDyOs1")
     .setOAuthAccessToken("31548859-DHbESdk6YoghCLcfhMF88QEFDvEjxbM6Q90eoZTGl")
    .setOAuthAccessTokenSecret("wjcWPvtejZSbp9cgLejUdd6W1MJqFzm5lByUFZl1NYgrV")
    val auth = new OAuthAuthorization(cb.build)
    
    

    注意事项

    这些是示例值,您应该使用自己的值。

  15. 创建 Twitter DStream:

    scala> val tweets = TwitterUtils.createStream(ssc,auth)
    
    
  16. 过滤掉英文推文:

    scala> val englishTweets = tweets.filter(_.getLang()=="en")
    
    
  17. 从推文中获取文本:

    scala> val status = englishTweets.map(status => status.getText)
    
    
  18. 设置检查点目录:

    scala> ssc.checkpoint("hdfs://localhost:9000/user/hduser/checkpoint")
    
    
  19. 启动StreamingContext

    scala> ssc.start
    scala> ssc.awaitTermination
    
    
  20. 您可以使用:paste将这些命令组合在一起:

    scala> :paste
    import org.apache.spark.streaming.twitter._
    import twitter4j.auth._
    import twitter4j.conf._
    import org.apache.spark.streaming.{Seconds, StreamingContext}
    import org.apache.spark._
    import org.apache.spark.streaming._
    import org.apache.spark.streaming.StreamingContext._
    val ssc = new StreamingContext(sc, Seconds(10))
    val cb = new ConfigurationBuilder
    cb.setDebugEnabled(true).setOAuthConsumerKey("FKNryYEKeCrKzGV7zuZW4EKeN")
     .setOAuthConsumerSecret("x6Y0zcTBOwVxpvekSCnGzbi3NYNrM5b8ZMZRIPI1XRC3pDyOs1")
     .setOAuthAccessToken("31548859-DHbESdk6YoghCLcfhMF88QEFDvEjxbM6Q90eoZTGl")
     .setOAuthAccessTokenSecret("wjcWPvtejZSbp9cgLejUdd6W1MJqFzm5lByUFZl1NYgrV")
    val auth = new OAuthAuthorization(cb.build)
    val tweets = TwitterUtils.createStream(ssc,Some(auth))
    val englishTweets = tweets.filter(_.getLang()=="en")
    val status = englishTweets.map(status => status.getText)
    status.print
    ssc.checkpoint("hdfs://localhost:9000/checkpoint")
    ssc.start
    ssc.awaitTermination
    
    

使用 Kafka 进行流式传输:

Kafka 是一个分布式、分区和复制的提交日志服务。简单来说,它是一个分布式消息服务器。Kafka 在称为主题的分类中维护消息源。主题的一个例子是你想获取新闻的公司的股票代码,例如,Cisco 的 CSCO。

产生消息的过程称为生产者,而消费消息的过程称为消费者。在传统消息传递中,消息传递服务有一个中央消息服务器,也称为代理。由于 Kafka 是一个分布式消息传递服务,它有一个由代理组成的集群,这些代理在功能上充当一个 Kafka 代理,如图所示:

使用 Kafka 进行流式处理

对于每个主题,Kafka 维护一个分区的日志。这个分区日志由一个或多个跨集群的分区组成,如下图所示:

使用 Kafka 进行流式处理

Kafka 借鉴了 Hadoop 和其他大数据框架的许多概念。分区概念与 Hadoop 中的InputSplit概念非常相似。在最简单的形式中,当使用TextInputFormat时,InputSplit与一个块相同。块以键值对的形式读取,在TextInputFormat中,键是行的字节偏移量,值是行的内容本身。以类似的方式,在 Kafka 分区中,记录以键值对的形式存储和检索,其中键是一个称为偏移量的顺序 ID,值是实际的消息。

在 Kafka 中,消息保留不依赖于消费者的消费。消息保留一个可配置的时间段。每个消费者都可以自由地以任何顺序读取消息。它只需要保留一个偏移量。另一个类比是阅读一本书,其中页码类似于偏移量,而页面内容类似于消息。读者可以自由地以任何方式阅读,只要他们记得书签(当前偏移量)。

为了提供类似于传统消息系统中 pub/sub 和 PTP(队列)的功能,Kafka 有消费者组的概念。消费者组是一组消费者,Kafka 集群将其视为一个单一单元。在消费者组中,只需要一个消费者接收消息。如果以下图中的消费者 C1 接收了主题 T1 的第一个消息,那么该主题的所有后续消息也将发送给这个消费者。使用这种策略,Kafka 保证了给定主题的消息传递顺序。

在极端情况下,当所有消费者都在一个消费者组中时,Kafka 集群表现得像 PTP/队列。在另一个极端情况下,如果每个消费者都属于不同的组,它表现得像 pub/sub。在实践中,每个消费者组都有一定数量的消费者。

使用 Kafka 进行流式处理

这个菜谱将向您展示如何使用来自 Kafka 的数据执行单词计数。

准备工作

此配方假设 Kafka 已经安装。Kafka 与 ZooKeeper 一起打包。我们假设 Kafka 的家目录在 /opt/infoobjects/kafka

  1. 启动 ZooKeeper:

    $ /opt/infoobjects/kafka/bin/zookeeper-server-start.sh /opt/infoobjects/kafka/config/zookeeper.properties
    
    
  2. 启动 Kafka 服务器:

    $ /opt/infoobjects/kafka/bin/kafka-server-start.sh /opt/infoobjects/kafka/config/server.properties
    
    
  3. 创建一个 test 主题:

    $ /opt/infoobjects/kafka/bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test
    
    

如何操作...

  1. 下载 spark-streaming-kafka 库及其依赖项:

    $ wget http://central.maven.org/maven2/org/apache/spark/spark-streaming-kafka_2.10/1.2.0/spark-streaming-kafka_2.10-1.2.0.jar
    $ wget http://central.maven.org/maven2/org/apache/kafka/kafka_2.10/0.8.1/kafka_2.10-0.8.1.jar
    $ wget http://central.maven.org/maven2/com/yammer/metrics/metrics-core/2.2.0/metrics-core-2.2.0.jar
    $ wget http://central.maven.org/maven2/com/101tec/zkclient/0.4/zkclient-0.4.jar
    
    
  2. 启动 Spark shell 并提供 spark-streaming-kafka 库:

    $ spark-shell --jars spark-streaming-kafka_2.10-1.2.0.jar, kafka_2.10-0.8.1.jar,metrics-core-2.2.0.jar,zkclient-0.4.jar
    
    
  3. 流特定导入:

    scala> import org.apache.spark.streaming.{Seconds, StreamingContext}
    
    
  4. 导入以进行隐式转换:

    scala> import org.apache.spark._
    scala> import org.apache.spark.streaming._
    scala> import org.apache.spark.streaming.StreamingContext._
    scala> import org.apache.spark.streaming.kafka.KafkaUtils
    
    
  5. 使用 2 秒批处理间隔创建 StreamingContext:

    scala> val ssc = new StreamingContext(sc, Seconds(2))
    
    
  6. 设置 Kafka 特定变量:

    scala> val zkQuorum = "localhost:2181"
    scala> val group = "test-group"
    scala> val topics = "test"
    scala> val numThreads = 1
    
    
  7. 创建 topicMap:

    scala> val topicMap = topics.split(",").map((_,numThreads.toInt)).toMap
    
    
  8. 创建 Kafka DStream:

    scala> val lineMap = KafkaUtils.createStream(ssc, zkQuorum, group, topicMap)
    
    
  9. 从 lineMap 中提取值:

    scala> val lines = lineMap.map(_._2)
    
    
  10. 创建值的 flatMap:

    scala> val words = lines.flatMap(_.split(" "))
    
    
  11. 创建 (word,occurrence) 的键值对:

    scala> val pair = words.map( x => (x,1))
    
    
  12. 对滑动窗口进行单词计数:

    scala> val wordCounts = pair.reduceByKeyAndWindow(_ + _, _ - _, Minutes(10), Seconds(2), 2)
    scala> wordCounts.print
    
    
  13. 设置 checkpoint 目录:

    scala> ssc.checkpoint("hdfs://localhost:9000/user/hduser/checkpoint")
    
    
  14. 启动 StreamingContext:

    scala> ssc.start
    scala> ssc.awaitTermination
    
    
  15. 在另一个窗口中发布 Kafka 的 test 主题消息:

    $ /opt/infoobjects/kafka/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test
    
    
  16. 现在,通过在步骤 15 按 Enter 键并在每条消息后发布消息。

  17. 现在,当你向 Kafka 发布消息时,你将在 Spark shell 中看到它们:如何操作...

更多...

假设你想维护每个单词出现的运行计数。Spark Streaming 有一个名为 updateStateByKey 操作的功能。updateStateByKey 操作允许你在更新时维护任何任意状态。

这个任意状态可以是聚合值,也可以是状态的变化(如推特上用户的情绪)。执行以下步骤:

  1. 在 pairs RDD 上调用 updateStateByKey

    scala> val runningCounts = wordCounts.updateStateByKey( (values: Seq[Int], state: Option[Int]) => Some(state.sum + values.sum))
    
    

    注意

    updateStateByKey 操作返回一个新的 "状态" DStream,其中每个键的状态通过在键的先前状态和新值上应用给定的函数来更新。这可以用来为每个键维护任意状态数据。

    实现此操作涉及两个步骤:

    • 定义状态

    • 定义状态 update 函数

    updateStateByKey 操作对每个键调用一次,值表示与该键关联的值的序列,非常类似于 MapReduce,状态可以是任何任意状态,我们选择将其设置为 Option[Int]。在步骤 18 的每次调用中,通过将当前值的总和添加到它来更新先前状态。

  2. 打印结果:

    scala> runningCounts.print
    
    
  3. 以下是将任意状态维护使用 updateStateByKey 操作的所有步骤的组合:

    Scala> :paste
    import org.apache.spark.streaming.{Seconds, StreamingContext}
     import org.apache.spark._
     import org.apache.spark.streaming._
     import org.apache.spark.streaming.kafka._
     import org.apache.spark.streaming.StreamingContext._
     val ssc = new StreamingContext(sc, Seconds(2))
     val zkQuorum = "localhost:2181"
     val group = "test-group"
     val topics = "test"
     val numThreads = 1
     val topicMap = topics.split(",").map((_,numThreads.toInt)).toMap
     val lineMap = KafkaUtils.createStream(ssc, zkQuorum, group, topicMap)
     val lines = lineMap.map(_._2)
     val words = lines.flatMap(_.split(" "))
     val pairs = words.map(x => (x,1))
     val runningCounts = pairs.updateStateByKey( (values: Seq[Int], state: Option[Int]) => Some(state.sum + values.sum))
     runningCounts.print
    ssc.checkpoint("hdfs://localhost:9000/user/hduser/checkpoint")
     ssc.start
     ssc.awaitTermination
    
    
  4. 通过按 Ctrl + D 运行它(这将执行使用 :paste 粘贴的代码)。

第六章. 使用 MLlib 开始机器学习

本章分为以下食谱:

  • 创建向量

  • 创建一个标记的点

  • 创建矩阵

  • 计算汇总统计量

  • 计算相关性

  • 进行假设检验

  • 使用 ML 创建机器学习管道

简介

以下是维基百科对机器学习的定义:

"机器学习是一个科学学科,它探索了构建和研究的算法,这些算法可以从数据中学习。"

实质上,机器学习是利用过去的数据来对未来进行预测。机器学习高度依赖于统计分析和方法。

在统计学中,有四种类型的测量尺度:

尺度类型 描述
名义尺度 =, ≠标识类别不能是数字示例:男性,女性
序数尺度 =, ≠, <, >名义尺度 + 从最不重要到最重要的排名示例:公司等级
间隔尺度 =, ≠, <, >, +, -间隔尺度 + 观测之间的距离分配给观测的数字表示顺序任何连续值之间的差异与其他值相同 60°的温度不是 30°的两倍
比例尺度 =, ≠, <, >, +, ×, ÷间隔尺度 + 观测之间的比例$20 是$10 的两倍

在数据之间可以做出的另一个区分是连续数据和离散数据之间的区别。连续数据可以取任何值。属于间隔和比例尺度的多数数据是连续的。

离散变量只能取特定的值,并且值之间存在明确的界限。例如,一栋房子可以有二或三个房间,但不能有 2.75 个房间。属于名义和序数尺度的数据总是离散的。

MLlib 是 Spark 的机器学习库。在本章中,我们将重点关注机器学习的基础知识。

创建向量

在理解向量之前,让我们先关注什么是点。点只是一组数字。这组数字或坐标定义了点在空间中的位置。坐标的数量决定了空间的维度。

我们可以用最多三个维度来可视化空间。超过三个维度的空间被称为超空间。让我们将这个空间隐喻付诸实践。

让我们从一个人开始。一个人有以下维度:

  • 重量

  • 身高

  • 年龄

我们在这里工作在三维空间中。因此,点(160,69,24)的解释将是 160 磅体重,69 英寸身高,24 岁年龄。

注意

点和向量是同一件事。向量中的维度被称为特征。另一种方式,我们可以将特征定义为一个现象的观察中的单个可测量属性。

Spark 有本地向量和矩阵,还有分布式矩阵。分布式矩阵由一个或多个 RDD 支持。本地向量具有数字索引和双精度值,并存储在单个机器上。

MLlib 中有两种类型的本地向量:密集和稀疏。密集向量由其值的数组支持,而稀疏向量由两个并行数组支持,一个用于索引,另一个用于值。

因此,人员数据(160,69,24)将使用密集向量表示为 [160.0,69.0,24.0],使用稀疏向量格式表示为 (3,[0,1,2],[160.0,69.0,24.0])。

是否将向量制作成稀疏或密集,取决于它有多少空值或 0。让我们以一个包含 10,000 个值且其中 9,000 个值为 0 的向量的例子。如果我们使用密集向量格式,它将是一个简单的结构,但 90% 的空间将被浪费。稀疏向量格式在这里会更好,因为它只会保留非零的索引。

稀疏数据非常常见,Spark 支持用于它的 libsvm 格式,该格式每行存储一个特征向量。

如何做到这一点…

  1. 启动 Spark shell:

    $ spark-shell
    
    
  2. 显式导入 MLlib 向量(不要与其他向量类混淆):

    Scala> import org.apache.spark.mllib.linalg.{Vectors,Vector}
    
    
  3. 创建一个密集向量:

    scala> val dvPerson = Vectors.dense(160.0,69.0,24.0)
    
    
  4. 创建一个稀疏向量:

    scala> val svPerson = Vectors.sparse(3,Array(0,1,2),Array(160.0,69.0,24.0))
    
    

它是如何工作的…

以下为 vectors.dense 方法的签名:

def dense(values: Array[Double]): Vector

这里,值表示向量中元素的双精度数组。

以下为 Vectors.sparse 方法的签名:

def sparse(size: Int, indices: Array[Int], values: Array[Double]): Vector

这里,size 表示向量的大小,indices 是索引数组,values 是值数组,作为双精度值。请确保您指定 double 作为数据类型或至少在一个值中使用小数;否则,它将为只有整数的数据集抛出异常。

创建一个标记点

标记点是一个带有相关标签的本地向量(稀疏/密集),在监督学习中用于帮助训练算法。你将在下一章中了解更多关于它的信息。

标签存储在 LabeledPoint 中的双精度值。这意味着当您有分类标签时,它们需要映射到双精度值。您分配给类别的值无关紧要,这只是方便的问题。

类型 标签值
二元分类 0 或 1
多类分类 0, 1, 2…
回归 十进制值

如何做到这一点…

  1. 启动 Spark shell:

    $spark-shell
    
    
  2. 显式导入 MLlib 向量:

    scala> import org.apache.spark.mllib.linalg.{Vectors,Vector}
    
    
  3. 导入 LabeledPoint

    scala> import org.apache.spark.mllib.regression.LabeledPoint
    
    
  4. 创建一个带有正标签的标记点和密集向量:

    scala> val willBuySUV = LabeledPoint(1.0,Vectors.dense(300.0,80,40))
    
    
  5. 创建一个带有负标签的标记点和密集向量:

    scala> val willNotBuySUV = LabeledPoint(0.0,Vectors.dense(150.0,60,25))
    
    
  6. 创建一个带有正标签的标记点和稀疏向量:

    scala> val willBuySUV = LabeledPoint(1.0,Vectors.sparse(3,Array(0,1,2),Array(300.0,80,40)))
    
    
  7. 创建一个带有负标签的标记点和稀疏向量:

    scala> val willNotBuySUV = LabeledPoint(0.0,Vectors.sparse(3,Array(0,1,2),Array(150.0,60,25)))
    
    
  8. 创建一个包含相同数据的 libsvm 文件:

    $vi person_libsvm.txt (libsvm indices start with 1)
    0  1:150 2:60 3:25
    1  1:300 2:80 3:40
    
    
  9. person_libsvm.txt 上传到 hdfs

    $ hdfs dfs -put person_libsvm.txt person_libsvm.txt
    
    
  10. 进行更多导入:

    scala> import org.apache.spark.mllib.util.MLUtils
    scala> import org.apache.spark.rdd.RDD
    
    
  11. libsvm 文件加载数据:

    scala> val persons = MLUtils.loadLibSVMFile(sc,"person_libsvm.txt")
    
    

创建矩阵

矩阵是一个简单的表格,用于表示多个特征向量。可以存储在一台机器上的矩阵称为本地矩阵,而可以分布在整个集群上的矩阵称为分布式矩阵

本地矩阵具有基于整数的索引,而分布式矩阵具有基于长整型的索引。两者都有双精度值。

分布式矩阵有三种类型:

  • RowMatrix:这每一行都是一个特征向量。

  • IndexedRowMatrix:这也有行索引。

  • CoordinateMatrix:这是一个简单的 MatrixEntry 矩阵。MatrixEntry 代表矩阵中的一个条目,由其行和列索引表示。

如何做到这一点…

  1. 启动 Spark shell:

    $spark-shell
    
    
  2. 导入矩阵相关类:

    scala> import org.apache.spark.mllib.linalg.{Vectors,Matrix, Matrices}
    
    
  3. 创建一个密集的本地矩阵:

    scala> val people = Matrices.dense(3,2,Array(150d,60d,25d, 300d,80d,40d))
    
    
  4. 创建一个 personRDD 作为向量的 RDD:

    scala> val personRDD = sc.parallelize(List(Vectors.dense(150,60,25), Vectors.dense(300,80,40)))
    
    
  5. 导入 RowMatrix 和相关类:

    scala> import org.apache.spark.mllib.linalg.distributed.{IndexedRow, IndexedRowMatrix,RowMatrix, CoordinateMatrix, MatrixEntry}
    
    
  6. 创建 personRDD 的行矩阵:

    scala> val personMat = new RowMatrix(personRDD)
    
    
  7. 打印行数:

    scala> print(personMat.numRows)
    
    
  8. 打印列数:

    scala> print(personMat.numCols)
    
    
  9. 创建一个索引行的 RDD:

    scala> val personRDD = sc.parallelize(List(IndexedRow(0L, Vectors.dense(150,60,25)), IndexedRow(1L, Vectors.dense(300,80,40))))
    
    
  10. 创建一个索引行矩阵:

    scala> val pirmat = new IndexedRowMatrix(personRDD)
    
    
  11. 打印行数:

    scala> print(pirmat.numRows)
    
    
  12. 打印列数:

    scala> print(pirmat.numCols)
    
    
  13. 将索引行矩阵转换回行矩阵:

    scala> val personMat = pirmat.toRowMatrix
    
    
  14. 创建一个矩阵条目的 RDD:

    scala> val meRDD = sc.parallelize(List(
     MatrixEntry(0,0,150),
     MatrixEntry(1,0,60),
    MatrixEntry(2,0,25),
    MatrixEntry(0,1,300),
    MatrixEntry(1,1,80),
    MatrixEntry(2,1,40)
    ))
    
    
  15. 创建一个坐标矩阵:

    scala> val pcmat = new CoordinateMatrix(meRDD)
    
    
  16. 打印行数:

    scala> print(pcmat.numRows)
    
    
  17. 打印列数:

    scala> print(pcmat.numCols)
    
    

计算摘要统计

摘要统计用于总结观察结果,以获得数据的整体感觉。摘要包括以下内容:

  • 数据的中心趋势——均值、众数、中位数

  • 数据的分布——方差、标准差

  • 边界条件——最小值、最大值

这个菜谱涵盖了如何生成摘要统计。

如何做到这一点…

  1. 启动 Spark shell:

    $ spark-shell
    
    
  2. 导入矩阵相关类:

    scala> import org.apache.spark.mllib.linalg.{Vectors,Vector}
    scala> import org.apache.spark.mllib.stat.Statistics
    
    
  3. 创建一个 personRDD 作为向量的 RDD:

    scala> val personRDD = sc.parallelize(List(Vectors.dense(150,60,25), Vectors.dense(300,80,40)))
    
    
  4. 计算列的摘要统计:

    scala> val summary = Statistics.colStats(personRDD)
    
    
  5. 打印这个摘要的平均值:

    scala> print(summary.mean)
    
    
  6. 打印方差:

    scala> print(summary.variance)
    
    
  7. 打印每列的非零值:

    scala> print(summary.numNonzeros)
    
    
  8. 打印样本大小:

    scala> print(summary.count)
    
    
  9. 打印每列的最大值:

    scala> print(summary.max)
    
    

计算相关性

相关性是两个变量之间的统计关系,当一个变量变化时,会导致另一个变量的变化。相关性分析衡量两个变量相关性的程度。

如果一个变量的增加导致另一个变量的增加,这被称为正相关。如果一个变量的增加导致另一个变量的减少,这被称为负相关

Spark 支持两种相关算法:皮尔逊和斯皮尔曼。皮尔逊算法适用于两个连续变量,例如一个人的身高和体重或房屋大小和房屋价格。斯皮尔曼处理一个连续变量和一个分类变量,例如邮编和房屋价格。

准备工作

让我们使用一些真实数据,这样我们才能更有意义地计算相关性。以下是美国加利福尼亚州萨拉托加市 2014 年初的房屋面积和价格:

房屋面积(平方英尺) 价格
2100 $1,620,000
2300 $1,690,000
2046 $1,400,000
4314 $2,000,000
1244 $1,060,000
4608 $3,830,000
2173 $1,230,000
2750 $2,400,000
4010 $3,380,000
1959 $1,480,000

如何做到这一点…

  1. 启动 Spark shell:

    $ spark-shell
    
    
  2. 导入统计和相关类:

    scala> import org.apache.spark.mllib.linalg._
    scala> import org.apache.spark.mllib.stat.Statistics
    
    
  3. 创建一个房屋面积的 RDD:

    scala> val sizes = sc.parallelize(List(2100, 2300, 2046, 4314, 1244, 4608, 2173, 2750, 4010, 1959.0))
    
    
  4. 创建一个房屋价格的 RDD:

    scala> val prices = sc.parallelize(List(1620000 , 1690000, 1400000, 2000000, 1060000, 3830000, 1230000, 2400000, 3380000, 1480000.00))
    
    
  5. 计算相关性:

    scala> val correlation = Statistics.corr(sizes,prices)
    correlation: Double = 0.8577177736252577 
    
    

    0.85 表示非常强的正相关。

    由于这里没有特定的算法,默认使用皮尔逊相关系数。corr 方法被重载,可以接受算法名称作为第三个参数。

  6. 使用皮尔逊相关系数计算相关性:

    scala> val correlation = Statistics.corr(sizes,prices)
    
    
  7. 使用斯皮尔曼相关系数计算相关性:

    scala> val correlation = Statistics.corr(sizes,prices,"spearman")
    
    

在前面的例子中,两个变量都是连续的,因此斯皮尔曼假设大小是离散的。斯皮尔曼的一个更好的例子可能是邮编与价格。

进行假设检验

假设检验是一种确定给定假设是否为真的概率的方法。假设样本数据表明女性倾向于为民主党投票。这种模式对于更大的人群来说可能或可能不是真的。如果这种模式仅仅因为偶然出现在样本数据中怎么办?

另一种看待假设检验目标的方式是回答这个问题:如果一个样本中存在某种模式,那么这种模式仅仅因为偶然出现的概率是多少?

我们如何操作?有句话说,最好的证明方式就是试图证明其相反。

要证伪的假设被称为零假设。假设检验与分类数据一起工作。让我们看看政党归属的民意调查的例子。

党派 男性 女性
民主党派 32 41
共和党派 28 25
独立党派 34 26

如何操作...

  1. 启动 Spark shell:

    $ spark-shell
    
    
  2. 导入相关类:

    scala> import org.apache.spark.mllib.stat.Statistics
    scala> import org.apache.spark.mllib.linalg.{Vector,Vectors}
    scala> import org.apache.spark.mllib.linalg.{Matrix, Matrices}
    
    
  3. 为民主党派创建一个向量:

    scala> val dems = Vectors.dense(32.0,41.0)
    
    
  4. 为共和党派创建一个向量:

    scala> val reps= Vectors.dense(28.0,25.0)
    
    
  5. 为独立党派创建一个向量:

    scala> val indies = Vectors.dense(34.0,26.0)
    
    
  6. 对观察数据与均匀分布进行卡方拟合优度检验:

    scala> val dfit = Statistics.chiSqTest(dems)
    scala> val rfit = Statistics.chiSqTest(reps)
    scala> val ifit = Statistics.chiSqTest(indies)
    
    
  7. 打印拟合优度结果:

    scala> print(dfit)
    scala> print(rfit)
    scala> print(ifit)
    
    
  8. 创建输入矩阵:

    scala> val mat = Matrices.dense(2,3,Array(32.0,41.0, 28.0,25.0, 34.0,26.0))
    
    
  9. 进行卡方独立性检验:

    scala> val in = Statistics.chiSqTest(mat)
    
    
  10. 打印独立性检验结果:

    scala> print(in)
    
    

使用 ML 创建机器学习管道

Spark ML 是 Spark 中用于构建机器学习管道的新库。这个库与 MLlib 一起开发。它有助于将多个机器学习算法组合成一个单一的管道,并使用 DataFrame 作为数据集。

准备工作

让我们先了解 Spark ML 中的一些基本概念。它使用转换器将一个 DataFrame 转换为另一个 DataFrame。一个简单的转换示例可以是添加一个列。你可以将其视为关系世界中“alter table”的等价物。

估计器另一方面代表机器学习算法,它从数据中学习。估计器的输入是 DataFrame,输出是转换器。每个估计器都有一个 fit() 方法,用于训练算法。

机器学习管道被定义为一系列阶段;每个阶段可以是估计器或转换器。

我们将要在这个食谱中使用的例子是某人是否是篮球运动员。为此,我们将有一个包含一个估计器和一个转换器的管道。

估计器获取训练数据以训练算法,然后转换器进行预测。

目前,假设我们使用的是机器学习算法LogisticRegression。我们将在后续章节中解释LogisticRegression以及其他算法的细节。

如何操作...

  1. 启动 Spark shell:

    $ spark-shell
    
    
  2. 导入必要的库:

    scala> import org.apache.spark.mllib.linalg.{Vector,Vectors}
    scala> import org.apache.spark.mllib.regression.LabeledPoint
    scala> import org.apache.spark.ml.classification.LogisticRegression
    
    
  3. 为是一名篮球运动员、身高 80 英寸、体重 250 磅的勒布朗创建一个标记点:

    scala> val lebron = LabeledPoint(1.0,Vectors.dense(80.0,250.0))
    
    
  4. 为不是篮球运动员、身高 70 英寸、体重 150 磅的蒂姆创建一个标记点:

    scala> val tim = LabeledPoint(0.0,Vectors.dense(70.0,150.0))
    
    
  5. 为是一名篮球运动员、身高 80 英寸、体重 207 磅的布里特尼创建一个标记点:

    scala> val brittany = LabeledPoint(1.0,Vectors.dense(80.0,207.0))
    
    
  6. 为不是篮球运动员、身高 65 英寸、体重 120 磅的斯泰西创建一个标记点:

    scala> val stacey = LabeledPoint(0.0,Vectors.dense(65.0,120.0))
    
    
  7. 创建一个训练 RDD:

    scala> val trainingRDD = sc.parallelize(List(lebron,tim,brittany,stacey))
    
    
  8. 创建一个训练 DataFrame:

    scala> val trainingDF = trainingRDD.toDF
    
    
  9. 创建一个LogisticRegression估计器:

    scala> val estimator = new LogisticRegression
    
    
  10. 通过拟合估计器与训练 DataFrame 来创建一个转换器:

    scala> val transformer = estimator.fit(trainingDF)
    
    
  11. 现在,让我们创建一些测试数据——约翰身高 90 英寸,体重 270 磅,是一名篮球运动员:

    scala> val john = Vectors.dense(90.0,270.0)
    
    
  12. 创建另一组测试数据——汤姆身高 62 英寸,体重 150 磅,不是篮球运动员:

    scala> val tom = Vectors.dense(62.0,120.0)
    
    
  13. 创建一个训练 RDD:

    scala> val testRDD = sc.parallelize(List(john,tom))
    
    
  14. 创建一个Features案例类:

    scala> case class Feature(v:Vector)
    
    
  15. testRDD映射到一个Features的 RDD:

    scala> val featuresRDD = testRDD.map( v => Feature(v))
    
    
  16. featuresRDD转换为具有列名"features"的 DataFrame:

    scala> val featuresDF = featuresRDD.toDF("features")
    
    
  17. 通过向featuresDF添加predictions列来转换featuresDF

    scala> val predictionsDF = transformer.transform(featuresDF)
    
    
  18. 打印predictionsDF

    scala> predictionsDF.foreach(println)
    
    
  19. 如您所见,PredictionDF除了保留特征外,还创建了三个列——rawPredictionprobabilityprediction。让我们只选择featuresprediction

    scala> val shorterPredictionsDF = predictionsDF.select("features","prediction")
    
    
  20. 将预测结果重命名为isBasketBallPlayer

    scala> val playerDF = shorterPredictionsDF.toDF("features","isBasketBallPlayer")
    
    
  21. 打印playerDF的 schema:

    scala> playerDF.printSchema
    
    

第七章. 使用 MLlib 进行监督学习 – 回归

本章分为以下几个部分:

  • 使用线性回归

  • 理解成本函数

  • 使用 lasso 进行线性回归

  • 进行岭回归

简介

下面是维基百科对监督学习的定义:

"监督学习是从标记的训练数据中推断函数的机器学习任务。"

监督学习有两个步骤:

  • 使用训练数据集训练算法;这就像先给出问题和答案。

  • 使用测试数据集向训练好的算法提出另一组问题

监督学习有两种类型的算法:

  • 回归:这预测的是连续值输出,例如房价。

  • 分类:这预测的是离散值输出(0 或 1)称为标签,例如一封电子邮件是否是垃圾邮件。分类不仅限于两个值;它可以有多个值,例如标记一封电子邮件为重要、不重要、紧急等(0,1,2…)。

注意

我们将在本章介绍回归,在下一章介绍分类。

作为回归的示例数据集,我们将使用加利福尼亚州萨拉托加市最近售出的房屋数据作为训练集来训练算法。一旦算法被训练,我们将要求它根据该房屋的大小预测房价。以下图示了工作流程:

简介

假设,就其功能而言,可能在这里听起来像是一个误称,你可能认为预测函数可能是一个更好的名称,但“假设”这个词是历史原因而使用的。

如果我们只使用一个特征来预测结果,这被称为双变量分析。当我们有多个特征时,这被称为多元分析。实际上,我们可以有我们喜欢的任意数量的特征。其中一种算法是支持向量机SVM),我们将在下一章介绍,实际上,它允许你拥有无限数量的特征。

本章将介绍如何使用 MLlib 进行监督学习,MLlib 是 Spark 的机器学习库。

注意

数学解释已经尽可能地简化,但你可以自由地跳过数学部分,直接进入如何操作…部分。

使用线性回归

线性回归是建模响应变量y的值的方法,基于一个或多个预测变量或特征x

准备工作

让我们使用一些住房数据来预测房屋的价格,基于其大小。以下是在 2014 年初加利福尼亚州萨拉托加市房屋的大小和价格:

房屋面积(平方英尺) 价格
2100 $ 1,620,000
2300 $ 1,690,000
2046 $ 1,400,000
4314 $ 2,000,000
1244 $ 1,060,000
4608 $ 3,830,000
2173 $ 1,230,000
2750 $ 2,400,000
4010 $ 3,380,000
1959 $ 1,480,000

下面是同样的图形表示:

准备工作

如何操作…

  1. 启动 Spark shell:

    $ spark-shell
    
    
  2. 导入统计和相关类:

    scala> import org.apache.spark.mllib.linalg.Vectors
    scala> import org.apache.spark.mllib.regression.LabeledPoint
    scala> import org.apache.spark.mllib.regression.LinearRegressionWithSGD
    
    
  3. 创建带有房价作为标签的 LabeledPoint 数组:

    scala> val points = Array(
    LabeledPoint(1620000,Vectors.dense(2100)),
    LabeledPoint(1690000,Vectors.dense(2300)),
    LabeledPoint(1400000,Vectors.dense(2046)),
    LabeledPoint(2000000,Vectors.dense(4314)),
    LabeledPoint(1060000,Vectors.dense(1244)),
    LabeledPoint(3830000,Vectors.dense(4608)),
    LabeledPoint(1230000,Vectors.dense(2173)),
    LabeledPoint(2400000,Vectors.dense(2750)),
    LabeledPoint(3380000,Vectors.dense(4010)),
    LabeledPoint(1480000,Vectors.dense(1959))
    )
    
    
  4. 创建前面数据的 RDD:

    scala> val pricesRDD = sc.parallelize(points)
    
    
  5. 使用 100 次迭代训练这个数据集的模型。在这里,步长被保持得很小,以考虑到响应变量(即房价)的非常大的值。第四个参数是每次迭代要使用的数据集的分数,最后一个参数是要使用的初始权重集(不同特征的权重):

    scala> val model = LinearRegressionWithSGD.train(pricesRDD,100,0.0000006,1.0,Vectors.zeros(1))
    
    
  6. 预测一栋 2500 平方英尺房子的价格:

    scala> val prediction = model.predict(Vectors.dense(2500))
    
    

房屋面积只是一个预测变量。房价取决于其他变量,如地块大小、房屋年龄等。你拥有的变量越多,你的预测就会越好。

理解成本函数

成本函数或损失函数是机器学习算法中一个非常重要的函数。大多数算法都有某种形式的成本函数,目标是使其最小化。影响成本函数的参数,如上一个菜谱中的 stepSize,需要手动设置。因此,理解成本函数的整个概念非常重要。

在这个菜谱中,我们将分析线性回归的成本函数。线性回归是一个简单的算法,它将帮助读者理解成本函数在复杂算法中的作用。

让我们回到线性回归。目标是找到最佳拟合线,使得均方误差最小。在这里,我们将误差定义为最佳拟合线的值与训练数据集中响应变量的实际值之间的差异。

对于单变量谓词的简单情况,最佳拟合线可以写成:

理解成本函数

这个函数也称为假设函数,可以写成:

理解成本函数

线性回归的目标是找到最佳拟合线。在这条线上,θ[0] 代表 y 轴上的截距,θ[1] 代表线的斜率,从以下方程中可以明显看出:

理解成本函数

我们必须选择 θ[0] 和 θ[1],使得 h(x) 最接近训练数据集中的 y。因此,对于 i^(th) 个数据点,直线和数据点之间的距离平方是:

理解成本函数

用话来说,这是预测房价和实际售价之间差异的平方。现在,让我们取训练数据集中这个值的平均值:

理解成本函数

上述方程称为线性回归的成本函数 J。目标是使这个成本函数最小化。

理解成本函数

这个成本函数也称为平方误差函数。如果将 θ[0] 和 θ[1] 分别与 J 绘制,它们各自遵循凸曲线。

让我们以一个包含三个值的数据集为例,(1,1),(2,2),和 (3,3),以简化计算:

理解损失函数

假设 θ[1] 为 0,即最佳拟合线平行于 x 轴。在第一种情况下,假设最佳拟合线是 x 轴,即 y=0。以下将是损失函数的值:

理解损失函数

现在,让我们将这条线稍微向上移动到 y=1。以下将是损失函数的值:

理解损失函数

现在,让我们将这条线进一步向上移动到 y=2。然后,以下将是损失函数的值:

理解损失函数

现在,当我们将这条线进一步向上移动到 y=3 时,以下将是损失函数的值:

理解损失函数

现在,让我们将这条线进一步向上移动到 y=4。以下将是损失函数的值:

理解损失函数

因此,您可以看到损失函数的值首先下降,然后再次像这样上升:

理解损失函数

现在,让我们通过将 θ[0] 设置为 0 并使用不同的 θ[1] 值来重复这个练习:

在第一种情况下,假设最佳拟合线是 x 轴,即 y=0。以下将是损失函数的值:

理解损失函数

现在,让我们使用斜率为 0.5。以下将是损失函数的值:

理解损失函数

现在,让我们使用斜率为 1。以下将是损失函数的值:

理解损失函数

现在,当我们使用斜率为 1.5 时,以下将是损失函数的值:

理解损失函数

现在,让我们使用斜率为 2.0。以下将是损失函数的值:

理解损失函数

如图中所示,J 的最小值出现在曲线的斜率或梯度为 0 时。

理解损失函数

当 θ[0] 和 θ[1] 都映射到三维空间时,它变成了一个碗的形状,损失函数的最小值位于其底部。

这种达到最小值的方法称为 梯度下降。在 Spark 中,其实现是随机梯度下降。

使用 Lasso 进行线性回归

Lasso 是线性回归的一种收缩和选择方法。它最小化通常的平方误差和,同时限制系数绝对值之和。它基于可在 statweb.stanford.edu/~tibs/lasso/lasso.pdf 找到的原始 Lasso 论文。

在上一个菜谱中我们使用的最小二乘法也称为 普通最小二乘法OLS)。OLS 有两个挑战:

  • 预测精度:使用 OLS 进行的预测通常具有低预测偏差和高方差。可以通过缩小一些系数(甚至将它们设置为 0)来提高预测精度。虽然会有一些偏差增加,但总体预测精度将提高。

  • 解释:在具有大量预测变量的情况下,找到其中表现出最强效应(相关性)的子集是可取的。

注意

偏差与方差

预测误差背后的两个主要原因是偏差和方差。理解偏差和方差的最佳方式是查看我们在同一数据集上进行多次预测的情况。

偏差是预测结果与实际值之间距离的估计,方差是不同预测中预测值差异的估计。

通常,添加更多特征有助于减少偏差,这很容易理解。如果在构建预测模型时遗漏了一些具有显著相关性的特征,会导致显著的误差。

如果你的模型具有高方差,你可以移除特征来减少它。更大的数据集也有助于减少方差。

这里,我们将使用一个简单的数据集,它是不良设定的。不良设定的数据集是指样本数据量小于预测变量数量的数据集。

y x0 x1 x2 x3 x4 x5 x6 x7 x8
1 5 3 1 2 1 3 2 2 1
2 9 8 8 9 7 9 8 7 9

你可以很容易地猜出,在这里,九个预测变量中只有两个与y有强相关性,即x0x1。我们将使用这个数据集和 lasso 算法来验证其有效性。

如何操作...

  1. 启动 Spark shell:

    $ spark-shell
    
    
  2. 导入统计和相关类:

    scala> import org.apache.spark.mllib.linalg.Vectors
    scala> import org.apache.spark.mllib.regression.LabeledPoint
    scala> import org.apache.spark.mllib.regression.LassoWithSGD
    
    
  3. 使用房价作为标签创建LabeledPoint数组:

    scala> val points = Array(
    LabeledPoint(1,Vectors.dense(5,3,1,2,1,3,2,2,1)),
    LabeledPoint(2,Vectors.dense(9,8,8,9,7,9,8,7,9))
    )
    
    
  4. 创建前述数据的 RDD:

    scala> val rdd = sc.parallelize(points)
    
    
  5. 使用此数据通过 100 次迭代训练模型。在这里,步长和正则化参数已经手动设置:

    scala> val model = LassoWithSGD.train(rdd,100,0.02,2.0)
    
    
  6. 检查有多少预测变量的系数被设置为 0:

    scala> model.weights
    org.apache.spark.mllib.linalg.Vector = [0.13455106581619633,0.02240732644670294,0.0,0.0,0.0,0.01360995990267153,0.0,0.0,0.0]
    
    

如您所见,九个预测变量中有六个将其系数设置为 0。这是 lasso 的主要特征:任何它认为无用的预测变量,它实际上通过将其系数设置为 0 将其从方程中移除。

进行岭回归

为了提高预测质量,lasso 的另一种方法是岭回归。在 lasso 中,许多特征将其系数设置为 0 并被从方程中消除,而在岭回归中,预测变量或特征会受到惩罚,但永远不会被设置为 0。

如何操作...

  1. 启动 Spark shell:

    $ spark-shell
    
    
  2. 导入统计和相关类:

    scala> import org.apache.spark.mllib.linalg.Vectors
    scala> import org.apache.spark.mllib.regression.LabeledPoint
    scala> import org.apache.spark.mllib.regression.RidgeRegressionWithSGD
    
    
  3. 使用房价作为标签创建LabeledPoint数组:

    scala> val points = Array(
    LabeledPoint(1,Vectors.dense(5,3,1,2,1,3,2,2,1)),
    LabeledPoint(2,Vectors.dense(9,8,8,9,7,9,8,7,9))
    )
    
    
  4. 创建前述数据的 RDD:

    scala> val rdd = sc.parallelize(points)
    
    
  5. 使用此数据通过 100 次迭代训练模型。在这里,步长和正则化参数已经手动设置:

    scala> val model = RidgeRegressionWithSGD.train(rdd,100,0.02,2.0)
    
    
  6. 检查有多少预测变量的系数被设置为零:

    scala> model.weights
    org.apache.spark.mllib.linalg.Vector = [0.049805969577244584,0.029883581746346748,0.009961193915448916,0.019922387830897833,0.009961193915448916,0.029883581746346748,0.019922387830897833,0.019922387830897833,0.009961193915448916]
    
    

如您所见,与 lasso 回归不同,岭回归不会将任何预测变量的系数设置为零,但它确实会让一些系数非常接近零。

第八章. 使用 MLlib 进行监督学习 – 分类

本章分为以下食谱:

  • 使用逻辑回归进行分类

  • 使用支持向量机进行二分类

  • 使用决策树进行分类

  • 使用随机森林进行分类

  • 使用梯度提升树进行分类

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

简介

分类问题类似于上一章讨论的回归问题,除了结果变量 y 只取几个离散值。在二分类中,y 只取两个值:0 或 1。你也可以将响应变量在分类中可以取的值视为代表类别。

使用逻辑回归进行分类

在分类中,响应变量 y 有离散值,而不是连续值。一些例子包括电子邮件(垃圾邮件/非垃圾邮件)、交易(安全/欺诈)等等。

在以下方程中的 y 变量可以取两个值,0 或 1:

使用逻辑回归进行分类

在这里,0 被称为负类,1 表示正类。尽管我们称它们为正类或负类,但这只是为了方便。算法对此分配是中立的。

线性回归虽然对于回归任务效果很好,但在分类任务中存在一些限制。这些包括:

  • 调整过程非常容易受到异常值的影响

  • 无法保证假设函数 h(x) 将会适应范围 0(负类)到 1(正类)

逻辑回归保证 h(x) 将会适应在 0 和 1 之间。尽管逻辑回归中包含回归这个词,但它更多的是一个误称,它是一个非常典型的分类算法:

使用逻辑回归进行分类

在线性回归中,假设函数如下所示:

使用逻辑回归进行分类

在逻辑回归中,我们稍微修改了假设方程,如下所示:

使用逻辑回归进行分类

g 函数被称为 Sigmoid 函数逻辑函数,对于实数 t 定义如下:

使用逻辑回归进行分类

这是 Sigmoid 函数作为图形的样子:

使用逻辑回归进行分类

如你所见,当 t 接近负无穷大时,g(t) 接近 0,而当 t 接近无穷大时,g(t) 接近 1。因此,这保证了假设函数的输出永远不会超出 0 到 1 的范围。

现在,假设函数可以重写为:

使用逻辑回归进行分类

h(x) 是给定预测变量 xy = 1 的估计概率,因此 h(x) 也可以重写为:

使用逻辑回归进行分类

换句话说,假设函数显示了在特征矩阵 x 和参数 使用逻辑回归进行分类y 为 1 的概率。这个概率可以是 0 到 1 之间的任何实数,但我们的分类目标不允许我们拥有连续值;我们只能有两个值 0 或 1,表示负类或正类。

假设我们预测 y = 1 如果

使用逻辑回归进行分类

否则预测 y = 0。如果我们再次查看 sigmoid 函数图,我们会意识到,当 使用逻辑回归进行分类 sigmoid 函数是 使用逻辑回归进行分类,即对于 t 的正值,它将预测正类:

由于 使用逻辑回归进行分类,这意味着对于 使用逻辑回归进行分类,将预测正类。为了更好地说明这一点,让我们将其扩展到二元情况的非矩阵形式:

使用逻辑回归进行分类

由方程 使用逻辑回归进行分类 表示的平面将决定给定向量属于正类还是负类。这条线被称为决策边界。

这个边界不一定要根据训练集线性。如果训练数据不能通过线性边界分开,可以添加更高阶的多项式特征来帮助实现。一个例子是通过平方 x1 和 x2 来添加两个新特征,如下所示:

使用逻辑回归进行分类

请注意,对于学习算法来说,这种增强与以下方程式完全相同:

使用逻辑回归进行分类

学习算法会将多项式的引入视为另一个特征。这在拟合过程中赋予你巨大的力量。这意味着可以通过正确选择多项式和参数来创建任何复杂的决策边界。

让我们花些时间来理解我们如何选择像线性回归案例中那样的参数值。线性回归中的成本函数 J 是:

使用逻辑回归进行分类

如你所知,我们在成本函数中平均成本。让我们用成本项来表示这一点:

使用逻辑回归进行分类

换句话说,成本项是算法在预测真实响应变量值 yh(x) 时必须付出的代价:

使用逻辑回归进行分类

这种成本对于线性回归来说效果很好,但对于逻辑回归来说,这种成本函数是非凸的(即,它导致多个局部最小值),我们需要找到一种更好的凸方法来估计成本。

对于逻辑回归来说效果良好的成本函数如下:

使用逻辑回归进行分类

让我们将这两个成本函数合并为一个,通过结合两个:

使用逻辑回归进行分类

让我们将这个成本函数放回 J 中:

使用逻辑回归进行分类

目标是使成本最小化,即最小化 使用逻辑回归进行分类 的值。这通过梯度下降算法来完成。Spark 有两个支持逻辑回归的类:

  • LogisticRegressionWithSGD

  • LogisticRegressionWithLBFGS

LogisticRegressionWithLBFGS 类更受欢迎,因为它消除了优化步长步骤。

准备工作

在 2006 年,铃木、鶴崎和冈田在日本不同海滩上对一种濒危穴居蜘蛛的分布进行了研究 (www.jstage.jst.go.jp/article/asjaa/55/2/55_2_79/_pdf)。

让我们看看一些关于粒度大小和蜘蛛存在情况的数据:

粒度大小(mm) 蜘蛛存在
0.245 不存在
0.247 不存在
0.285 当前
0.299 当前
0.327 当前
0.347 当前
0.356 不存在
0.36 当前
0.363 不存在
0.364 当前
0.398 不存在
0.4 当前
0.409 不存在
0.421 当前
0.432 不存在
0.473 当前
0.509 当前
0.529 当前
0.561 不存在
0.569 不存在
0.594 当前
0.638 当前
0.656 当前
0.816 当前
0.853 当前
0.938 当前
1.036 当前
1.045 当前

我们将使用这些数据来训练算法。不存在将表示为 0,存在将表示为 1。

如何做到这一点…

  1. 启动 Spark shell:

    $ spark-shell
    
    
  2. 导入统计和相关类:

    scala> import org.apache.spark.mllib.linalg.Vectors
    scala> import org.apache.spark.mllib.regression.LabeledPoint
    scala> import org.apache.spark.mllib.classification.LogisticRegressionWithLBFGS
    
    
  3. 创建一个带有蜘蛛存在或不存在作为标签的 LabeledPoint 数组:

    scala> val points = Array(
    LabeledPoint(0.0,Vectors.dense(0.245)),
    LabeledPoint(0.0,Vectors.dense(0.247)),
    LabeledPoint(1.0,Vectors.dense(0.285)),
    LabeledPoint(1.0,Vectors.dense(0.299)),
    LabeledPoint(1.0,Vectors.dense(0.327)),
    LabeledPoint(1.0,Vectors.dense(0.347)),
    LabeledPoint(0.0,Vectors.dense(0.356)),
    LabeledPoint(1.0,Vectors.dense(0.36)),
    LabeledPoint(0.0,Vectors.dense(0.363)),
    LabeledPoint(1.0,Vectors.dense(0.364)),
    LabeledPoint(0.0,Vectors.dense(0.398)),
    LabeledPoint(1.0,Vectors.dense(0.4)),
    LabeledPoint(0.0,Vectors.dense(0.409)),
    LabeledPoint(1.0,Vectors.dense(0.421)),
    LabeledPoint(0.0,Vectors.dense(0.432)),
    LabeledPoint(1.0,Vectors.dense(0.473)),
    LabeledPoint(1.0,Vectors.dense(0.509)),
    LabeledPoint(1.0,Vectors.dense(0.529)),
    LabeledPoint(0.0,Vectors.dense(0.561)),
    LabeledPoint(0.0,Vectors.dense(0.569)),
    LabeledPoint(1.0,Vectors.dense(0.594)),
    LabeledPoint(1.0,Vectors.dense(0.638)),
    LabeledPoint(1.0,Vectors.dense(0.656)),
    LabeledPoint(1.0,Vectors.dense(0.816)),
    LabeledPoint(1.0,Vectors.dense(0.853)),
    LabeledPoint(1.0,Vectors.dense(0.938)),
    LabeledPoint(1.0,Vectors.dense(1.036)),
    LabeledPoint(1.0,Vectors.dense(1.045)))
    
    
  4. 创建一个包含前面数据的 RDD:

    scala> val spiderRDD = sc.parallelize(points)
    
    
  5. 使用这些数据训练一个模型(截距是所有预测变量为零时的值):

    scala> val lr = new LogisticRegressionWithLBFGS().setIntercept(true)
    scala> val model = lr.run(spiderRDD)
    
    
  6. 预测粒度大小为 0.938 的蜘蛛存在情况:

    scala> val predict = model.predict(Vectors.dense(0.938))
    
    

使用 SVM 进行二元分类

分类是一种根据其效用将数据放入不同类别的技术。例如,一家电子商务公司可以将“会购买”或“不会购买”这两个标签应用于潜在访客。

这种分类是通过向机器学习算法提供一些已标记的数据来完成的,这些算法被称为训练数据。挑战是如何标记两个类别之间的边界。让我们以以下图示中的简单例子为例:

使用 SVM 进行二元分类

在前一个案例中,我们将灰色和黑色指定为“不购买”和“购买”标签。在这里,在两个类别之间画一条线就像以下这样简单:

使用 SVM 进行二元分类

这是不是我们能做的最好?其实不是,让我们尝试做得更好。黑色分类器并不真正与“购买”和“不购买”的车等距。让我们尝试以下更好的方法:

使用 SVM 进行二元分类

现在看起来不错。这实际上就是 SVM 算法所做的事情。你可以在前面的图表中看到,实际上只有三个车决定了线的斜率:两条在直线上的黑色车,以及一条在直线下的灰色车。这些车被称为支持向量,其余的车,即向量,是不相关的。

有时候很难画一条线,可能需要曲线来分离两个类别,如下所示:

使用 SVM 进行二元分类

有时候即使这样也不够。在这种情况下,我们需要超过两个维度来解决问题。而不是一个分类线,我们需要的是一个超平面。事实上,当数据过于杂乱时,添加额外的维度有助于找到分离类别的超平面。以下图表说明了这一点:

使用 SVM 进行二元分类

这并不意味着添加额外的维度总是一个好主意。大多数时候,我们的目标是减少维度,只保留相关的维度/特征。有一整套算法致力于降维;我们将在后面的章节中介绍这些算法。

如何做到这一点...

  1. Spark 库自带了样本libsvm数据。我们将使用这个数据并将数据加载到 HDFS 中:

    $ hdfs dfs -put /opt/infoobjects/spark/data/mllib/sample_libsvm_data.txt /user/hduser/sample_libsvm_data.txt
    
    
  2. 启动 Spark shell:

    $ spark-shell
    
    
  3. 执行所需的导入:

    scala> import org.apache.spark.mllib.classification.SVMWithSGD
    scala> import org.apache.spark.mllib.evaluation.BinaryClassificationMetrics
    scala> import org.apache.spark.mllib.regression.LabeledPoint
    scala> import org.apache.spark.mllib.linalg.Vectors
    scala> import org.apache.spark.mllib.util.MLUtils
    
    
  4. 将数据作为 RDD 加载:

    scala> val svmData = MLUtils.loadLibSVMFile(sc,"sample_libsvm_data.txt")
    
    
  5. 计算记录数:

    scala> svmData.count
    
    
  6. 现在让我们将数据集分成一半的训练数据和一半的测试数据:

    scala> val trainingAndTest = svmData.randomSplit(Array(0.5,0.5))
    
    
  7. 指定训练测试数据:

    scala> val trainingData = trainingAndTest(0)
    scala> val testData = trainingAndTest(1)
    
    
  8. 训练算法并构建 100 次迭代的模型(你可以尝试不同的迭代次数,但你将看到,在某个点上,结果开始收敛,这是一个不错的选择):

    scala> val model = SVMWithSGD.train(trainingData,100)
    
    
  9. 现在我们可以使用这个模型来预测任何数据集的标签。让我们预测测试数据中的第一个点的标签:

    scala> val label = model.predict(testData.first.features)
    
    
  10. 让我们创建一个元组,第一个值是对测试数据的预测,第二个值是实际标签,这将帮助我们计算算法的准确率:

    scala> val predictionsAndLabels = testData.map( r => (model.predict(r.features),r.label))
    
    
  11. 你可以计算有多少记录的预测和实际标签不匹配:

    scala> predictionsAndLabels.filter(p => p._1 != p._2).count
    
    

使用决策树进行分类

决策树是机器学习算法中最直观的。我们在日常生活中经常使用决策树。

决策树算法有很多有用的特性:

  • 容易理解和解释

  • 可以处理分类和连续特征

  • 可以处理缺失特征

  • 不需要特征缩放

决策树算法以倒序工作,其中每个级别的特征表达式都会被评估,并将数据集分为两个类别。我们将通过一个简单的哑剧例子来帮助你理解,这是我们大多数人上大学时都玩过的。我猜了一个动物,然后让我的同事问我问题来猜出我的选择。她的提问如下:

Q1: 它是一个大动物吗?

A: 是的

Q2: 这个动物活过 40 年吗?

A: 是的

Q3: 这个动物是象吗?

A: 是的

这是一个显然过于简化的例子,她知道我假设了一个象(在大数据世界中你还能猜到什么?)。让我们扩展这个例子,包括一些更多的动物,如下面的图所示(灰色框是类别):

使用决策树进行分类

上述例子是一个多类分类的例子。在这个菜谱中,我们将专注于二元分类。

准备中

每当我们的儿子早上要上网球课时,前一天晚上教练会检查天气报告,并决定第二天早上是否适合打网球。这个菜谱将使用这个例子来构建决策树。

让我们决定影响早上是否打网球的决定的天气特征:

  • 风速

  • 温度

让我们构建一个不同组合的表格:

多风 温度 打网球?
正常
正常

现在我们如何构建决策树?我们可以从三个特征之一开始:雨、多风或温度。规则是开始于一个特征,以便尽可能实现最大信息增益。

在雨天,正如你在表中看到的,其他特征都不重要,没有游戏。高风速也是如此。

决策树,像大多数其他算法一样,只将特征值作为双精度值。所以,让我们进行映射:

准备中

正类是 1.0,负类是 0.0。让我们使用 CSV 格式加载数据,使用第一个值作为标签:

$vi tennis.csv
0.0,1.0,1.0,2.0
0.0,1.0,1.0,1.0
0.0,1.0,1.0,0.0
0.0,0.0,1.0,2.0
0.0,0.0,1.0,0.0
1.0,0.0,0.0,2.0
1.0,0.0,0.0,1.0
0.0,0.0,0.0,0.0

如何做这件事...

  1. 启动 Spark shell:

    $ spark-shell
    
    
  2. 执行所需的导入:

    scala> import org.apache.spark.mllib.tree.DecisionTree
    scala> import org.apache.spark.mllib.regression.LabeledPoint
    scala> import org.apache.spark.mllib.linalg.Vectors
    scala> import org.apache.spark.mllib.tree.configuration.Algo._
    scala> import org.apache.spark.mllib.tree.impurity.Entropy
    
    
  3. 加载文件:

    scala> val data = sc.textFile("tennis.csv")
    
  4. 解析数据并将其加载到 LabeledPoint

    scala> val parsedData = data.map {
    line =>  val parts = line.split(',').map(_.toDouble)
     LabeledPoint(parts(0), Vectors.dense(parts.tail)) }
    
    
  5. 使用这些数据训练算法:

    scala> val model = DecisionTree.train(parsedData, Classification, Entropy, 3)
    
    
  6. 为无雨、高风和凉爽的温度创建一个向量:

    scala> val v=Vectors.dense(0.0,1.0,0.0)
    
    
  7. 预测是否应该打网球:

    scala> model.predict(v)
    
    

它是如何工作的…

让我们绘制这个菜谱中创建的网球决策树:

它是如何工作的…

这个模型有三个级别的深度。选择哪个属性取决于我们如何最大化信息增益。它是通过测量分割的纯度来衡量的。纯度意味着,无论确定性是否增加,给定的数据集将被视为正面或负面。在这个例子中,这相当于打网球的机会是增加还是不打网球的机会增加。

纯度是通过熵来衡量的。熵是系统中无序程度的度量。在这个上下文中,它更容易理解为不确定性的度量:

如何工作…

纯度最高为 0,最低为 1。让我们尝试使用公式来确定纯度。

当雨为是时,打网球的概率 p+ 是 0/3 = 0。不打网球的概率 p_ 是 3/3 = 1:

如何工作…

这是一个纯集合。

当雨为无时,打网球的概率 p+ 是 2/5 = 0.4。不打网球的概率 p_ 是 3/5 = 0.6:

如何工作…

这几乎是一个不纯的集合。最不纯的情况是概率为 0.5。

Spark 使用三个指标来确定不纯度:

  • 吉尼不纯度(分类)

  • 熵(分类)

  • 方差(回归)

信息增益是父节点不纯度与两个子节点不纯度加权的平均值之间的差异。让我们看看第一个分割,它将大小为八的数据分割成两个大小为三(左侧)和五个(右侧)的数据集。让我们称第一个分割为 s1,父节点为 rain,左侧子节点为 no rain,右侧子节点为 wind。因此,信息增益将是:

如何工作…

由于我们已经为熵计算了 no rainwind 的不纯度,让我们计算 rain 的熵:

如何工作…

现在我们来计算信息增益:

如何工作…

因此,在第一个分割中,信息增益是 0.2。这是我们能达到的最好效果吗?让我们看看我们的算法能提出什么。首先,让我们找出树的深度:

scala> model.depth
Int = 2

在这里,深度是 2,与我们直观构建的 3 相比,所以这个模型似乎优化得更好。让我们看看树的结构:

scala> model.toDebugString
String =  "DecisionTreeModel classifier of depth 2 with 5 nodes
If (feature 1 <= 0.0)
 If (feature 2 <= 0.0)
 Predict: 0.0
 Else (feature 2 > 0.0)
 Predict: 1.0
Else (feature 1 > 0.0)
 Predict: 0.0

让我们通过视觉方式构建它,以获得更好的理解:

如何工作…

我们不会在这里详细说明,因为我们已经用前面的模型做了这个。我们将直接计算信息增益:0.44

正如你所见,在这个案例中,信息增益是 0.44,这是第一个模型的超过两倍。

如果你看看第二级节点,不纯度为零。在这种情况下,这是很好的,因为我们已经在深度 2 时得到了它。想象一下深度为 50 的情况。在这种情况下,决策树对于训练数据工作得很好,但对于测试数据工作得不好。这种情况被称为 过拟合

避免过拟合的一个解决方案是剪枝。你将训练数据分成两个集合:训练集和验证集。你使用训练集来训练模型。现在,通过逐渐移除左侧节点,你用模型对验证集进行测试。如果移除叶节点(这通常是单例——也就是说,它只包含一个数据点)可以提高模型的性能,那么这个叶节点就会被从模型中剪除。

使用随机森林进行分类

有时候一个决策树是不够的,所以会使用一组决策树来产生更强大的模型。这些被称为集成学习算法。集成学习算法不仅限于使用决策树作为基础模型。

在集成学习算法中最受欢迎的是随机森林。在随机森林中,而不是生长一棵单独的树,会生长K棵树。每棵树都得到一个随机子集S的训练数据。为了增加一点变化,每棵树只使用特征的一个子集。在做出预测时,对树进行多数投票,这成为预测。

让我们用一个例子来解释这一点。目标是针对给定的人预测他/她是否有良好信用或不良信用。

为了做到这一点,我们将提供标记的训练数据——也就是说,在这种情况下,一个人具有特征以及他/她是否有良好信用或不良信用的标签。现在我们不想创建特征偏差,因此我们将提供随机选择的特征集。提供随机选择的特征子集的另一个原因是,大多数现实世界数据有数百个甚至数千个特征。例如,文本分类算法通常有 50k-100k 个特征。

在这种情况下,为了给故事增添趣味,我们不会提供特征,但我们会询问不同的人为什么他们认为一个人有良好或不良的信用。根据定义,不同的人会接触到一个人(有时重叠)的不同特征,这给我们带来了与随机选择特征相同的功能。

我们的第一个例子是杰克,他有一个“不良信用”的标签。我们将从杰克最喜欢的酒吧,大象酒吧工作的乔伊开始。一个人能够推断出为什么给出了特定的标签的唯一方法是通过提出是/否问题。让我们看看乔伊怎么说:

Q1: 杰克给小费给得好吗?(特征:慷慨)

A: 不是

Q2: 杰克每次访问至少花费 60 美元吗?(特征:挥霍)

A: 是的

Q3: 他是否即使在最小的挑衅下也倾向于卷入酒吧斗殴?(特征:易怒)

A: 是的

这解释了为什么杰克信用不良。

我们现在询问杰克的女友,斯泰西:

Q1: 当我们出去玩时,杰克总是付账吗?(特征:慷慨)

A: 不是

Q2: 杰克还了我欠他的 500 美元了吗?(特征:责任感)

A: 不是

Q3: 他有时是不是为了炫耀而过度消费?(特征:挥霍)

A: 是的

这解释了为什么杰克信用不良。

我们现在询问杰克最好的朋友乔治:

Q1:当我们在我公寓里闲逛时,他是否自己打扫?(特征:有组织)

A: 否

Q2:在我超级碗聚餐期间,杰克空手而来吗?(特征:关心)

A: 是的

Q3:他是否用“我忘记带钱包了”的借口让我在餐厅付账?(特征:责任感)

A: 是的

这解释了为什么杰克有不良信用。

现在我们来谈谈信用良好的杰西卡。让我们问问斯泰西,她恰好是杰西卡的女 sibling:

Q1:当我缺钱时,杰西卡是否提出帮忙?(特征:慷慨)

A: 是的

Q2:杰西卡是否按时支付账单?(特征:责任感)

A: 是的

Q3:杰西卡是否愿意帮我照看孩子?(特征:关心)

A: 是的

这解释了为什么杰西卡有良好的信用。

现在我们来问乔治,他恰好是她的丈夫:

Q1:杰西卡是否保持房子整洁?(特征:有组织)

A: 是的

Q2:她期望收到昂贵的礼物吗?(特征:挥霍)

A: 否

Q3:你忘记修剪草坪时,她会生气吗?(特征:易怒)

A: 否

这解释了为什么杰西卡有良好的信用。

现在我们来问大象酒吧的酒保乔伊:

Q1:每次她和朋友来酒吧时,她是否大多是指定司机?(特征:负责任)

A: 是的

Q2:她总是带剩菜回家吗?(特征:挥霍)

A: 是的

Q3:她是否给小费?(特征:慷慨)

A: 是的

随机森林的工作方式是在两个层面上进行随机选择:

  • 数据的一个子集

  • 用于分割数据的特征子集

这两个子集可以重叠。

在我们的例子中,我们有六个特征,我们打算将三个特征分配给每棵树。这样,我们有很大的机会会有重叠。

让我们在训练数据集中添加八个人:

Names Label Generosity Responsibility Care Organization Spendthrift Volatile
Jack 0 0 0 0 0 1 1
Jessica 1 1 1 1 1 0 0
Jenny 0 0 0 1 0 1 1
Rick 1 1 1 0 1 0 0
Pat 0 0 0 0 0 1 1
Jeb 1 1 1 1 0 0 0
Jay 1 0 1 1 1 0 0
Nat 0 1 0 0 0 1 1
Ron 1 0 1 1 1 0 0
Mat 0 1 0 0 0 1 1

准备中

让我们把创建的数据放入以下文件中的libsvm格式:

rf_libsvm_data.txt
0 5:1 6:1
1 1:1 2:1 3:1 4:1
0 3:1 5:1 6:1
1 1:1 2:1 4:1
0 5:1 6:1
1 1:1 2:1 3:1 4:1
0 1:1 5:1 6:1
1 2:1 3:1 4:1
0 1:1 5:1 6:1

现在将其上传到 HDFS:

$ hdfs dfs -put rf_libsvm_data.txt

如何做到这一点…

  1. 启动 Spark shell:

    $ spark-shell
    
    
  2. 执行所需的导入:

    scala> import org.apache.spark.mllib.tree.RandomForest
    scala> import org.apache.spark.mllib.tree.configuration.Strategy
    scala> import org.apache.spark.mllib.util.MLUtils
    
    
  3. 加载数据并解析:

    scala> val data =
     MLUtils.loadLibSVMFile(sc, "rf_libsvm_data.txt")
    
    
  4. 将数据分为训练测试数据集:

    scala> val splits = data.randomSplit(Array(0.7, 0.3))
    scala> val (trainingData, testData) = (splits(0), splits(1))
    
    
  5. 将分类作为一个树策略(随机森林也支持回归):

    scala> val treeStrategy = Strategy.defaultStrategy("Classification")
    
    
  6. 训练模型:

    scala> val model = RandomForest.trainClassifier(trainingData,
     treeStrategy, numTrees=3, featureSubsetStrategy="auto", seed = 12345)
    
    
  7. 在测试实例上评估模型并计算测试错误:

    scala> val testErr = testData.map { point =>
     val prediction = model.predict(point.features)
     if (point.label == prediction) 1.0 else 0.0
    }.mean()
    scala> println("Test Error = " + testErr)
    
    
  8. 检查模型:

    scala> println("Learned Random Forest:n" + model.toDebugString)
    Learned Random Forest:nTreeEnsembleModel classifier with 3 trees
     Tree 0:
     If (feature 5 <= 0.0)
     Predict: 1.0
     Else (feature 5 > 0.0)
     Predict: 0.0
     Tree 1:
     If (feature 3 <= 0.0)
     Predict: 0.0
     Else (feature 3 > 0.0)
     Predict: 1.0
     Tree 2:
     If (feature 0 <= 0.0)
     Predict: 0.0
     Else (feature 0 > 0.0)
     Predict: 1.0
    
    

它是如何工作的…

如您在这样一个小型示例中可以看到,三棵树正在使用不同的特征。在具有数千个特征和训练数据的实际用例中,这种情况不会发生,但大多数树在观察特征和投票时会有所不同,多数票将获胜。请记住,在回归的情况下,是通过树的平均值来得到最终值的。

使用梯度提升树进行分类

另一种集成学习算法是梯度提升树GBTs)。GBTs 一次训练一棵树,其中每棵新树都改进先前训练的树的不足之处。

由于梯度提升树(GBTs)一次训练一棵树,它们可能比随机森林(Random Forest)花费更长的时间。

准备工作

我们将使用之前配方中使用的相同数据。

如何做…

  1. 启动 Spark shell:

    $ spark-shell
    
    
  2. 执行所需的导入:

    scala> import org.apache.spark.mllib.tree.GradientBoostedTrees
    scala> import org.apache.spark.mllib.tree.configuration.BoostingStrategy
    scala> import org.apache.spark.mllib.util.MLUtils
    
    
  3. 加载数据并解析:

    scala> val data =
     MLUtils.loadLibSVMFile(sc, "rf_libsvm_data.txt")
    
    
  4. 将数据分为训练测试数据集:

    scala> val splits = data.randomSplit(Array(0.7, 0.3))
    scala> val (trainingData, testData) = (splits(0), splits(1))
    
    
  5. 创建一个分类作为提升策略,并将迭代次数设置为3

    scala> val boostingStrategy =
     BoostingStrategy.defaultParams("Classification")
    scala> boostingStrategy.numIterations = 3
    
    
  6. 训练模型:

    scala> val model = GradientBoostedTrees.train(trainingData, boostingStrategy)
    
    
  7. 在测试实例上评估模型并计算测试错误:

    scala> val testErr = testData.map { point =>
     val prediction = model.predict(point.features)
     if (point.label == prediction) 1.0 else 0.0
    }.mean()
    scala> println("Test Error = " + testErr)
    
    
  8. 检查模型:

    scala> println("Learned Random Forest:n" + model.toDebugString)
    
    

在这种情况下,模型的准确率为 0.9,这低于我们在随机森林的情况中得到的准确率。

使用朴素贝叶斯进行分类

让我们考虑使用机器学习构建一个电子邮件垃圾邮件过滤器。我们感兴趣的两个类别是:垃圾邮件用于未经请求的消息,非垃圾邮件用于常规电子邮件:

使用朴素贝叶斯进行分类

第一个挑战是,当给定一封电子邮件时,我们如何将其表示为特征向量x。一封电子邮件只是一堆文本或一组单词(因此,这个问题域属于一个更广泛的类别,称为文本分类)。让我们用一个长度等于字典大小的特征向量来表示电子邮件。如果字典中的一个单词出现在电子邮件中,则其值将为 1;否则为 0。让我们构建一个表示内容为online pharmacy sale的电子邮件的向量:

使用朴素贝叶斯进行分类

这个特征向量中的单词字典称为词汇表,向量的维度与词汇表的大小相同。如果词汇表大小为 10,000,则特征向量中的可能值将是 210,000。

我们的目标是建模给定yx的概率。为了建模P(x|y),我们将做出一个强烈的假设,这个假设是x的条件独立。这个假设被称为朴素贝叶斯假设,基于这个假设的算法被称为朴素贝叶斯分类器

例如,对于y =1,表示垃圾邮件,"online"出现和"pharmacy"出现的概率是独立的。这是一个与现实无关的强烈假设,但在获得良好的预测时效果非常好。

准备工作

Spark 附带了一个用于与朴素贝叶斯一起使用的示例数据集。让我们将此数据集加载到 HDFS 中:

$ hdfs dfs -put /opt/infoobjects/spark/data/mllib/sample_naive_bayes_data.txt
 sample_naive_bayes_data.txt

如何做到这一点...

  1. 启动 Spark shell:

    $ spark-shell
    
    
  2. 执行所需的导入:

    scala> import org.apache.spark.mllib.classification.NaiveBayes
    scala> import org.apache.spark.mllib.linalg.Vectors
    scala> import org.apache.spark.mllib.regression.LabeledPoint
    
    
  3. 将数据加载到 RDD 中:

    scala> val data = sc.textFile("sample_naive_bayes_data.txt")
    
    
  4. 将数据解析为LabeledPoint

    scala> val parsedData = data.map { line =>
     val parts = line.split(',')
     LabeledPoint(parts(0).toDouble, Vectors.dense(parts(1).split(' ').map(_.toDouble)))
    }
    
    
  5. 将数据平均分成训练测试数据集:

    scala> val splits = parsedData.randomSplit(Array(0.5, 0.5), seed = 11L)
    scala> val training = splits(0)
    scala> val test = splits(1)
    
    
  6. 使用训练数据集训练模型:

    val model = NaiveBayes.train(training, lambda = 1.0)
    
    
  7. 预测测试数据集的标签:

    val predictionAndLabel = test.map(p => (model.predict(p.features), p.label))
    
    

第九章。使用 MLlib 的无监督学习

本章将介绍如何使用 MLlib 进行无监督学习,MLlib 是 Spark 的机器学习库。

本章分为以下食谱:

  • 使用 k-means 进行聚类

  • 主成分分析进行降维

  • 使用奇异值分解进行降维

简介

以下是维基百科对无监督学习的定义:

"在机器学习中,无监督学习的问题在于试图在未标记的数据中找到隐藏的结构。"

与有监督学习不同,在有监督学习中,我们有标记数据来训练算法,在无监督学习中,我们要求算法自己找到结构。让我们看看以下示例数据集:

介绍

如前图所示,数据点正在形成以下两个簇:

介绍

事实上,聚类是最常见的无监督学习算法。

使用 k-means 进行聚类

聚类分析或聚类是将数据分组到多个组的过程,使得一个组中的数据与其他组中的数据相似。

以下是一些聚类应用的例子:

  • 市场细分:将目标市场细分为多个细分市场,以便更好地满足每个细分市场的需求

  • 社交网络分析:在社交网络中找到一组有组织的人群,通过社交网站如 Facebook 进行广告定位

  • 数据中心计算集群:将一组计算机组合起来以提高性能

  • 天文数据分析:理解天文数据以及如星系形成的事件

  • 房地产:根据相似特征识别社区

  • 文本分析:将小说或论文等文本文档分为不同类型

k-means 算法最好通过图像来展示,让我们再次看看我们的示例图:

使用 k-means 进行聚类

k-means 的第一步是随机选择两个称为簇质心的点:

使用 k-means 进行聚类

k-means 算法是一个迭代算法,分为两个步骤:

  • 簇分配步骤:此算法将遍历每个数据点,根据它更接近哪个质心,将其分配给该质心,进而分配给它所代表的簇

  • 移动质心步骤:此算法将每个质心移动到簇中数据点的平均值

让我们看看聚类分配后我们的数据看起来如何:

使用 k-means 进行聚类

现在让我们将簇质心移动到簇中数据点的平均值,如下所示:

使用 k-means 进行聚类

在这种情况下,一次迭代就足够了,进一步的迭代不会移动聚类中心。对于大多数真实数据,需要多次迭代才能将中心移动到最终位置。

k-means 算法接受多个聚类作为输入。

准备中

让我们使用来自加利福尼亚州萨拉托加市的不同的住房数据。这次,我们将使用占地面积和房屋价格:

占地面积 房屋价格(以 1000 美元为单位)
12839 2405
10000 2200
8040 1400
13104 1800
10000 2351
3049 795
38768 2725
16250 2150
43026 2724
44431 2675
40000 2930
1260 870
15000 2210
10032 1145
12420 2419
69696 2750
12600 2035
10240 1150
876 665
8125 1430
11792 1920
1512 1230
1276 975
67518 2400
9810 1725
6324 2300
12510 1700
15616 1915
15476 2278
13390 2497.5
1158 725
2000 870
2614 730
13433 2050
12500 3330
15750 1120
13996 4100
10450 1655
7500 1550
12125 2100
14500 2100
10000 1175
10019 2047.5
48787 3998
53579 2688
10788 2251
11865 1906

让我们将这些数据转换为名为 saratoga.c逗号分隔值 (CSV) 文件,并将其绘制成散点图:

准备中

找到合适的聚类数量是一个棘手的问题。在这里,我们有视觉检查的优势,这在超平面(超过三个维度)的数据中是不存在的。让我们大致将数据分为四个聚类,如下所示:

准备中

我们将运行 k-means 算法来完成同样的任务,并看看我们的结果有多接近。

如何做到这一点…

  1. saratoga.csv 加载到 HDFS:

    $ hdfs dfs -put saratoga.csv saratoga.csv
    
    
  2. 启动 Spark shell:

    $ spark-shell
    
    
  3. 导入统计和相关类:

    scala> import org.apache.spark.mllib.linalg.Vectors
    scala> import org.apache.spark.mllib.clustering.KMeans
    
    
  4. saratoga.csv 加载为 RDD:

    scala> val data = sc.textFile("saratoga.csv")
    
    
  5. 将数据转换为密集向量的 RDD:

    scala> val parsedData = data.map( line => Vectors.dense(line.split(',').map(_.toDouble)))
    
    
  6. 训练模型以四个聚类和五个迭代:

    scala> val kmmodel= KMeans.train(parsedData,4,5)
    
    
  7. parsedData 收集为本地 Scala 集合:

    scala> val houses = parsedData.collect
    
    
  8. 预测第 0 个元素的聚类:

    scala> val prediction = kmmodel.predict(houses(0))
    
    
  9. 现在,让我们比较 k-means 算法与我们所做的单个聚类分配。k-means 算法从 0 开始给出聚类 ID。一旦检查数据,你就会发现我们给出的 A 到 D 聚类 ID 与 k-means 之间的以下映射:A=>3, B=>1, C=>0, D=>2。

  10. 现在,让我们从图表的不同部分选取一些数据,并预测它属于哪个聚类。

  11. 让我们看看房屋(18)的数据,其占地面积为 876 平方英尺,价格为 66.5 万美元:

    scala> val prediction = kmmodel.predict(houses(18))
    resxx: Int = 3
    
    
  12. 现在,看看房屋(35)的数据,其占地面积为 15,750 平方英尺,价格为 112 万美元:

    scala> val prediction = kmmodel.predict(houses(35))
    resxx: Int = 1
    
    
  13. 现在,看看房屋(6)的数据,其占地面积为 38,768 平方英尺,价格为 2.725 百万美元:

    scala> val prediction = kmmodel.predict(houses(6))
    resxx: Int = 0
    
    
  14. 现在,看看房屋(15)的数据,其占地面积为 69,696 平方英尺,价格为 275 万美元:

    scala>  val prediction = kmmodel.predict(houses(15))
    resxx: Int = 2
    
    

你可以用更多的数据来测试预测能力。让我们做一些邻里分析,看看这些聚类有什么含义。大多数属于第 3 个聚类的房屋都靠近市中心。第 2 个聚类的房屋位于丘陵地带。

在这个例子中,我们处理了一个非常小的特征集;常识和视觉检查也会得出相同的结论。k-means 算法的美丽之处在于,它可以在具有无限数量特征的数据上进行聚类。当你有一堆原始数据并想了解其中的模式时,这是一个非常棒的工具。

主成分分析进行降维

降维是将维度或特征数量减少的过程。许多真实数据包含非常高的特征数量。拥有数千个特征并不罕见。现在,我们需要深入挖掘那些重要的特征。

降维有几个用途,例如:

  • 数据压缩

  • 可视化

当维度数量减少时,它减少了磁盘占用和内存占用。最后但同样重要的是;它帮助算法运行得更快。它还有助于将高度相关的维度减少到一个。

人类只能可视化三维,但数据可以具有许多更高的维度。可视化可以帮助发现数据中的隐藏模式。降维通过将多个特征压缩成一个来帮助可视化。

降维最流行的算法是主成分分析PCA)。

让我们看看以下数据集:

主成分分析进行降维

假设目标是把这二维数据分成一维。实现这一目标的方法是找到一个可以投影这些数据的直线。让我们找到一个适合投影这些数据的直线:

主成分分析进行降维

这是离数据点最近投影距离的直线。让我们通过从每个数据点到这个投影线的最短线来进一步解释它:

主成分分析进行降维

另一种看待方式是,我们需要找到一个可以投影数据的直线,使得数据点到这条直线的平方距离之和最小化。这些灰色线段也被称为投影误差

准备工作

让我们看看加利福尼亚州萨拉托加市住房数据的三个特征——即房屋大小、地块大小和价格。使用 PCA,我们将房屋大小和地块大小特征合并为一个特征——z。让我们称这个特征为房屋 z 密度

值得注意的是,并不总是可能为新创建的特征赋予意义。在这种情况下,这很容易,因为我们只有两个特征要组合,我们可以用我们的常识来组合这两个特征的效果。在更实际的案例中,你可能有一千个特征,你试图将它们投影到 100 个特征。可能无法为那 100 个特征中的每一个赋予现实生活的意义。

在这个练习中,我们将使用 PCA 推导住房密度,然后我们将进行线性回归以查看这种密度如何影响房价。

在我们深入研究 PCA 之前,有一个预处理阶段:特征缩放。当两个特征的量级相差很大时,特征缩放就会变得重要。在这里,房屋面积在 800 平方英尺到 7000 平方英尺的范围内变化,而地块面积在 800 平方英尺到几英亩之间变化。

为什么我们之前不需要进行特征缩放?答案是,我们真的不需要将特征放在同一起跑线上。梯度下降是另一个特征缩放非常有用的领域。

特征缩放有不同的方法:

  • 将特征值除以一个最大值,使得每个特征值都在准备中范围内

  • 将特征值除以范围,即最大值减去最小值

  • 从特征值中减去其均值,然后除以范围

  • 从特征值中减去其均值,然后除以标准差

我们将使用第四种方法以最佳方式缩放。以下是我们将用于此菜谱的数据:

房屋面积 地块面积 缩放后的房屋面积 缩放后的地块面积 房屋价格(以 1000 美元为单位)
2524 12839 -0.025 -0.231 2405
2937 10000 0.323 -0.4 2200
1778 8040 -0.654 -0.517 1400
1242 13104 -1.105 -0.215 1800
2900 10000 0.291 -0.4 2351
1218 3049 -1.126 -0.814 795
2722 38768 0.142 1.312 2725
2553 16250 -0.001 -0.028 2150
3681 43026 0.949 1.566 2724
3032 44431 0.403 1.649 2675
3437 40000 0.744 1.385 2930
1680 1260 -0.736 -0.92 870
2260 15000 -0.248 -0.103 2210
1660 10032 -0.753 -0.398 1145
3251 12420 0.587 -0.256 2419
3039 69696 0.409 3.153 2750
3401 12600 0.714 -0.245 2035
1620 10240 -0.787 -0.386 1150
876 876 -1.414 -0.943 665
1889 8125 -0.56 -0.512 1430
4406 11792 1.56 -0.294 1920
1885 1512 -0.564 -0.905 1230
1276 1276 -1.077 -0.92 975
3053 67518 0.42 3.023 2400
2323 9810 -0.195 -0.412 1725
3139 6324 0.493 -0.619 2300
2293 12510 -0.22 -0.251 1700
2635 15616 0.068 -0.066 1915
2298 15476 -0.216 -0.074 2278
2656 13390 0.086 -0.198 2497.5
1158 1158 -1.176 -0.927 725
1511 2000 -0.879 -0.876 870
1252 2614 -1.097 -0.84 730
2141 13433 -0.348 -0.196 2050
3565 12500 0.852 -0.251 3330
1368 15750 -0.999 -0.058 1120
5726 13996 2.672 -0.162 4100
2563 10450 0.008 -0.373 1655
1551 7500 -0.845 -0.549 1550
1993 12125 -0.473 -0.274 2100
2555 14500 0.001 -0.132 2100
1572 10000 -0.827 -0.4 1175
2764 10019 0.177 -0.399 2047.5
7168 48787 3.887 1.909 3998
4392 53579 1.548 2.194 2688
3096 10788 0.457 -0.353 2251
2003 11865 -0.464 -0.289 1906

让我们将缩放后的房屋大小和缩放后的房屋价格数据保存为 scaledhousedata.csv

如何做…

  1. scaledhousedata.csv 加载到 HDFS:

    $ hdfs dfs -put scaledhousedata.csv scaledhousedata.csv
    
    
  2. 启动 Spark shell:

    $ spark-shell
    
    
  3. 导入统计和相关类:

    scala> import org.apache.spark.mllib.linalg.Vectors
    scala> import org.apache.spark.mllib.linalg.distributed.RowMatrix
    
    
  4. saratoga.csv 加载为 RDD:

    scala> val data = sc.textFile("scaledhousedata.csv")
    
    
  5. 将数据转换为密集向量的 RDD:

    scala> val parsedData = data.map( line => Vectors.dense(line.split(',').map(_.toDouble)))
    
    
  6. parsedData 创建一个 RowMatrix

    scala> val mat = new RowMatrix(parsedData)
    
    
  7. 计算一个主成分:

    scala> val pc= mat.computePrincipalComponents(1)
    
    
  8. 将行投影到由主成分张成的线性空间:

    scala> val projected = mat.multiply(pc)
    
    
  9. 将投影的 RowMatrix 转换回 RDD:

    scala> val projectedRDD = projected.rows
    
    
  10. projectedRDD 保存回 HDFS:

    scala> projectedRDD.saveAsTextFile("phdata")
    
    

现在,我们将使用这个投影特征,我们决定称之为住房密度,将其与房价对比,看看是否会出现任何新的模式:

  1. 将 HDFS 目录 phdata 下载到本地目录 phdata

    scala> hdfs dfs -get phdata phdata
    
    
  2. 在数据中修剪起始和结束括号,并将数据加载到 MS Excel 中,紧邻房价。

下面的图是房价与住房密度的对比图:

如何做…

让我们按照以下方式绘制一些数据模式:

如何做…

我们在这里看到了什么模式?从高密度住房到低密度住房,人们愿意支付高额溢价。随着住房密度的降低,这种溢价趋于平稳。例如,人们愿意支付高额溢价从公寓和联排别墅搬到独立住宅,但拥有 3 英亩地块的独立住宅的溢价与在相似建成区域拥有 2 英亩地块的独立住宅的溢价不会有太大差异。

使用奇异值分解进行降维

原始维度通常不能以最佳方式表示数据。正如我们在 PCA 中所看到的,有时可以将数据投影到更少的维度,同时仍然保留大部分有用的信息。

有时,最好的方法是将维度沿着表现出最多变化的特征对齐。这种方法有助于消除不代表数据的维度。

让我们再次看看以下图形,它显示了两个维度上的最佳拟合线:

使用奇异值分解进行降维

投影线显示了原始数据的一维最佳近似。如果我们取灰色线与黑色线相交的点,并隔离黑色线,我们将得到尽可能保留变化的原始数据的降维表示,如图所示:

奇异值分解的降维

让我们画一条垂直于第一条投影线的线,如图所示:

奇异值分解的降维

这条线尽可能多地捕捉原始数据集的第二维度的变化。它在这个维度上近似原始数据的效果不佳,因为这个维度一开始就表现出较少的变化。可以使用这些投影线生成一组不相关的数据点,这些数据点将显示原始数据中的子分组,而这些子分组在第一眼看来是不可见的。

这就是奇异值分解(SVD)背后的基本思想。将一个高维、高度可变的数据点集降低到较低维度的空间,可以更清晰地展示原始数据的结构,并按从最大变化到最小变化的顺序排列。奇异值分解之所以非常实用,尤其是在自然语言处理(NLP)应用中,是因为你可以简单地忽略低于某个阈值的变异,从而大量减少原始数据,同时确保原始关系的兴趣得到保留。

现在我们稍微深入理论。奇异值分解基于线性代数中的一个定理,即一个矩形矩阵 A 可以分解为三个矩阵的乘积——一个正交矩阵 U、一个对角矩阵 S 和一个正交矩阵 V 的转置。我们可以如下表示:

奇异值分解的降维

UV 是正交矩阵:

奇异值分解的降维奇异值分解的降维

U 的列是 奇异值分解的降维 的正交特征向量,而 V 的列是 奇异值分解的降维 的正交特征向量。S 是一个对角矩阵,包含从 UV 中按降序排列的特征值的平方根。

准备工作

让我们看看一个术语-文档矩阵的例子。我们将探讨关于美国总统选举的两个新项目。以下是两个文档的链接:

让我们利用这两条新闻构建总统候选人矩阵:

准备中准备中

让我们将这个矩阵放入 CSV 文件中,然后将其放入 HDFS。我们将对此矩阵应用奇异值分解(SVD)并分析结果。

如何做到这一点……

  1. scaledhousedata.csv加载到 HDFS:

    $ hdfs dfs -put pres.csv scaledhousedata.csv
    
    
  2. 启动 Spark shell:

    $ spark-shell
    
    
  3. 导入统计和相关类:

    scala> import org.apache.spark.mllib.linalg.Vectors
    scala> import org.apache.spark.mllib.linalg.distributed.RowMatrix
    
    
  4. pres.csv加载为 RDD:

    scala> val data = sc.textFile("pres.csv")
    
    
  5. 将数据转换为一个密集向量的 RDD:

    scala> val parsedData = data.map( line => Vectors.dense(line.split(',').map(_.toDouble)))
    
    
  6. parsedData创建一个RowMatrix

    scala> val mat = new RowMatrix(parsedData)
    
    
  7. 计算svd

    scala> val svd = mat.computeSVD(2,true)
    
    
  8. 计算特征向量U因子:

    scala> val U = svd.U
    
    
  9. 计算奇异值(特征值)矩阵:

    scala> val s = svd.s
    
    
  10. 计算特征向量V因子:

    scala> val s = svd.s
    
    

如果你查看s,你会意识到它给 Npr 文章的评分远高于 Fox 文章。

第十章。推荐系统

在本章中,我们将介绍以下食谱:

  • 基于显式反馈的协同过滤

  • 基于隐式反馈的协同过滤

简介

以下是对推荐系统的维基百科定义:

"推荐系统是信息过滤系统的一个子类,旨在预测用户对项目的'评分'或'偏好'。"

近年来,推荐系统获得了巨大的普及。亚马逊使用它们推荐书籍,Netflix 推荐电影,谷歌新闻推荐新闻故事。正如俗话所说,“实践是检验真理的唯一标准”,以下是一些推荐可能产生的影响的例子(来源:Celma, Lamere, 2008):

  • 净 flix 上观看的影片中有三分之二是通过推荐观看的

  • 38% 的谷歌新闻点击是通过推荐实现的

  • 亚马逊 35%的销售额是通过推荐实现的

如前几章所见,特征和特征选择在机器学习算法的有效性中起着重要作用。推荐引擎算法会自动发现这些特征,称为潜在特征。简而言之,存在一些潜在特征,使一个用户喜欢一部电影而讨厌另一部。如果另一个用户有相应的潜在特征,那么这个人也很可能对电影有相似的品味。

为了更好地理解这一点,让我们看看一些示例电影评分:

电影 Rich Bob Peter Chris
Titanic 5 3 5 ?
GoldenEye 3 2 1 5
Toy Story 1 ? 2 2
Disclosure 4 4 ? 4
Ace Ventura 4 ? 4 ?

我们的目标是预测带有?符号的缺失条目。让我们看看我们是否能找到与电影相关的某些特征。一开始,你将查看类型,如下所示:

电影 类型
Titanic 动作,浪漫
GoldenEye 动作,冒险,惊悚
Toy Story 动画,儿童,喜剧
Disclosure 剧情,惊悚
Ace Ventura 喜剧

现在每部电影都可以对每个类型从 0 到 1 进行评分。例如,GoldenEye 并非主要是一部浪漫电影,因此它可能对浪漫的评分为 0.1,但对动作的评分为 0.98。因此,每部电影都可以表示为一个特征向量。

注意

在本章中,我们将使用来自 grouplens.org/datasets/movielens/ 的 MovieLens 数据集。

InfoObjects 大数据沙盒预装了 10 万条电影评分。从 GroupLens,你还可以下载 100 万或甚至高达 1000 万的评分,如果你想要分析更大的数据集以获得更好的预测。

我们将使用这个数据集的两个文件:

  • u.data:这是一个制表符分隔的电影评分列表,其格式如下:

    user id | item id | rating | epoch time
    

    由于我们不需要时间戳,我们将从我们的食谱中过滤掉数据中的时间戳。

  • u.item:这是一个制表符分隔的电影列表,其格式如下:

    movie id | movie title | release date | video release date |               IMDb URL | unknown | Action | Adventure | Animation |               Children's | Comedy | Crime | Documentary | Drama | Fantasy |               Film-Noir | Horror | Musical | Mystery | Romance | Sci-Fi |               Thriller | War | Western |
    

本章将介绍如何使用 MLlib(Spark 的机器学习库)进行推荐。

使用显式反馈的协同过滤

协同过滤是推荐系统中最常用的技术。它有一个有趣的特性——它能够自主学习特征。因此,在电影评分的情况下,我们不需要提供关于电影是否浪漫或动作的实际人类反馈。

正如我们在简介部分所看到的,电影有一些潜在特征,例如类型,同样用户也有一些潜在特征,例如年龄、性别等。协同过滤不需要这些特征,并且能够自主学习潜在特征。

在这个例子中,我们将使用一个名为交替最小二乘法(ALS)的算法。这个算法基于少数潜在特征解释电影与用户之间的关联。它使用三个训练参数:rank、迭代次数和 lambda(本章后面将解释)。确定这三个参数最佳值的方法是尝试不同的值,并查看哪个值具有最小的均方根误差(RMSE)。这个误差类似于标准差,但它基于模型结果而不是实际数据。

准备中

将从 GroupLens 下载的moviedata上传到hdfs中的moviedata文件夹:

$ hdfs dfs -put moviedata moviedata

我们将向这个数据库添加一些个性化评分,以便我们可以测试推荐的准确性。

你可以通过查看u.item来挑选一些电影并对其进行评分。以下是我选择的一些电影及其评分。请随意选择你想要评分的电影并提供你自己的评分。

电影 ID 电影名称 评分(1-5)
313 泰坦尼克号 5
2 黄金眼 3
1 玩具总动员 1
43 披露 4
67 猫鼠游戏 4
82 侏罗纪公园 5
96 终结者 2 5
121 独立日 4
148 鬼影与黑暗 4

最大的用户 ID 是 943,因此我们将新用户添加为 944。让我们创建一个新的以逗号分隔的文件p.data,其中包含以下数据:

944,313,5
944,2,3
944,1,1
944,43,4
944,67,4
944,82,5
944,96,5
944,121,4
944,148,4

如何做到这一点...

  1. 将个性化电影数据上传到hdfs

    $ hdfs dfs -put p.data p.data
    
    
  2. 导入 ALS 和评分类:

    scala> import org.apache.spark.mllib.recommendation.ALS
    scala> import org.apache.spark.mllib.recommendation.Rating
    
    
  3. 将评分数据加载到 RDD 中:

    scala> val data = sc.textFile("moviedata/u.data")
    
    
  4. val data转换为评分 RDD:

    scala> val ratings = data.map { line => 
     val Array(userId, itemId, rating, _) = line.split("\t") 
     Rating(userId.toInt, itemId.toInt, rating.toDouble) 
    }
    
    
  5. 将个性化评分数据加载到 RDD 中:

    scala> val pdata = sc.textFile("p.data")
    
    
  6. 将数据转换为个性化评分 RDD:

    scala> val pratings = pdata.map { line => 
     val Array(userId, itemId, rating) = line.split(",")
     Rating(userId.toInt, itemId.toInt, rating.toDouble) 
    }
    
    
  7. 将评分与个性化评分合并:

    scala> val movieratings = ratings.union(pratings)
    
    
  8. 使用具有 5 个 rank 和 10 次迭代以及 0.01 作为 lambda 的 ALS 构建模型:

    scala> val model = ALS.train(movieratings, 10, 10, 0.01)
    
    
  9. 让我们根据这个模型预测我对给定电影的评分。

  10. 让我们从原始的电影 ID 为 195 的终结者开始:

    scala> model.predict(sc.parallelize(Array((944,195)))).collect.foreach(println)
    Rating(944,195,4.198642954004738)
    
    

    由于我给终结者 2评了 5 分,这是一个合理的预测。

  11. 让我们尝试电影 ID 为 402 的鬼影

    scala> model.predict(sc.parallelize(Array((944,402)))).collect.foreach(println)
    Rating(944,402,2.982213836456829)
    
    

    这是一个合理的猜测。

  12. 让我们尝试《鬼影与黑暗》这部电影,我已经对其进行了评分,ID 为 148:

    scala> model.predict(sc.parallelize(Array((944,402)))).collect.foreach(println)
    Rating(944,148,3.8629938805450035)
    
    

    预测非常接近,知道我给这部电影评了 4 分。

您可以将更多电影添加到train数据集中。还有 100 万和 1000 万评分数据集可用,这将进一步优化算法。

使用隐式反馈进行协同过滤

有时可用的反馈不是评分的形式,而是播放的音频轨道、观看的电影等形式。乍一看,这些数据可能不如用户明确给出的评分看起来那么好,但它们的信息量要大得多。

准备工作

我们将使用来自www.kaggle.com/c/msdchallenge/data的百万歌曲数据。您需要下载三个文件:

  • kaggle_visible_evaluation_triplets

  • kaggle_users.txt

  • kaggle_songs.txt

现在执行以下步骤:

  1. hdfs中创建一个songdata文件夹,并将所有三个文件放在这里:

    $ hdfs dfs -mkdir songdata
    
    
  2. 将歌曲数据上传到hdfs

    $ hdfs dfs -put kaggle_visible_evaluation_triplets.txt songdata/
    $ hdfs dfs -put kaggle_users.txt songdata/
    $ hdfs dfs -put kaggle_songs.txt songdata/
    
    

我们还需要做一些更多的预处理。MLlib 中的 ALS 接受用户和产品 ID 作为整数。Kaggle_songs.txt文件中包含歌曲 ID 和序列号,而Kaggle_users.txt文件没有。我们的目标是替换triplets数据中的useridsongid为相应的整数序列号。为此,请按照以下步骤操作:

  1. kaggle_songs数据加载为 RDD:

    scala> val songs = sc.textFile("songdata/kaggle_songs.txt")
    
    
  2. 将用户数据加载为 RDD:

    scala> val users = sc.textFile("songdata/kaggle_users.txt")
    
    
  3. 将三元组(用户,歌曲,播放次数)数据加载为 RDD:

    scala> val triplets = sc.textFile("songdata/kaggle_visible_evaluation_triplets.txt")
    
    
  4. 将歌曲数据转换为PairRDD

    scala> val songIndex = songs.map(_.split("\\W+")).map(v => (v(0),v(1).toInt))
    
    
  5. 收集songIndex为 Map:

    scala> val songMap = songIndex.collectAsMap
    
    
  6. 将用户数据转换为PairRDD

    scala> val userIndex = users.zipWithIndex.map( t => (t._1,t._2.toInt))
    
    
  7. 收集userIndex为 Map:

    scala> val userMap = userIndex.collectAsMap
    
    

我们将需要songMapuserMap来替换userIdsongId在三元组中的值。Spark 会自动在集群上根据需要提供这两个 map。这工作得很好,但每次需要时在集群间发送它都很昂贵。

一个更好的方法是使用 Spark 的一个名为broadcast变量的功能。broadcast变量允许 Spark 作业在每个机器上缓存变量的只读副本,而不是在每个任务中发送副本。Spark 使用高效的广播算法分发广播变量,因此网络通信成本可以忽略不计。

如你所猜,songMapuserMap都是很好的候选变量,可以围绕broadcast变量进行包装。执行以下步骤:

  1. 广播userMap

    scala> val broadcastUserMap = sc.broadcast(userMap)
    
    
  2. 广播songMap

    scala> val broadcastSongMap = sc.broadcast(songMap)
    
    
  3. triplet转换为数组:

    scala> val tripArray = triplets.map(_.split("\\W+"))
    
    
  4. 导入评分:

    scala> import org.apache.spark.mllib.recommendation.Rating
    
    
  5. triplet数组转换为评分对象的 RDD:

    scala> val ratings = tripArray.map { case Array(user, song, plays) =>
     val userId = broadcastUserMap.value.getOrElse(user, 0)
     val songId = broadcastUserMap.value.getOrElse(song, 0)
     Rating(userId, songId, plays.toDouble)
    }
    
    

现在,我们的数据已经准备好进行建模和预测。

如何做到这一点…

  1. 导入 ALS:

    scala> import org.apache.spark.mllib.recommendation.ALS
    
    
  2. 使用排名 10 和 10 次迭代的 ALS 构建模型:

    scala> val model = ALS.trainImplicit(ratings, 10, 10)
    
    
  3. 从三元组中提取用户和歌曲元组:

    scala> val usersSongs = ratings.map( r => (r.user, r.product) )
    
    
  4. 为用户和歌曲元组进行预测:

    scala> val predictions = model.predict(usersSongs)
    
    

它是如何工作的…

我们的模式需要四个参数来工作,如下所示:

参数名称 描述
排名 模型中的潜在特征数量
迭代次数 此分解运行的迭代次数
Lambda 过拟合参数
Alpha 观察到的交互的相对权重

正如你在梯度下降的例子中所看到的,这些参数需要手动设置。我们可以尝试不同的值,但最佳值是 rank=50,iterations=30,lambda=0.00001,以及 alpha= 40。

还有更多...

快速测试不同参数的一种方法是在 Amazon EC2 上启动一个 Spark 集群。这让你有灵活性,可以选择一个强大的实例来快速测试这些参数。我已经创建了一个公共的 s3 存储桶 com.infoobjects.songdata,用于将数据拉入 Spark。

你需要遵循以下步骤从 S3 加载数据并运行 ALS:

sc.hadoopConfiguration.set("fs.s3n.awsAccessKeyId", "<your access key>")
sc.hadoopConfiguration.set("fs.s3n.awsSecretAccessKey","<your secret key>")
val songs = sc.textFile("s3n://com.infoobjects.songdata/kaggle_songs.txt")
val users = sc.textFile("s3n://com.infoobjects.songdata/kaggle_users.txt")
val triplets = sc.textFile("s3n://com.infoobjects.songdata/kaggle_visible_evaluation_triplets.txt")
val songIndex = songs.map(_.split("\\W+")).map(v => (v(0),v(1).toInt))
val songMap = songIndex.collectAsMap
val userIndex = users.zipWithIndex.map( t => (t._1,t._2.toInt))
val userMap = userIndex.collectAsMap
val broadcastUserMap = sc.broadcast(userMap)
val broadcastSongMap = sc.broadcast(songMap)
val tripArray = triplets.map(_.split("\\W+"))
import org.apache.spark.mllib.recommendation.Rating
val ratings = tripArray.map{ v =>
 val userId: Int = broadcastUserMap.value.get(v(0)).fold(0)(num => num)
 val songId: Int = broadcastSongMap.value.get(v(1)).fold(0)(num => num)
 Rating(userId,songId,v(2).toDouble)
 }
import org.apache.spark.mllib.recommendation.ALS
val model = ALS.trainImplicit(ratings, 50, 30, 0.000001, 40)
val usersSongs = ratings.map( r => (r.user, r.product) )
val predictions =model.predict(usersSongs)

这些是基于 usersSongs 矩阵做出的预测。

第十一章。使用 GraphX 进行图处理

本章将介绍如何使用 GraphX 进行图处理,Spark 的图处理库。

本章分为以下食谱:

  • 图的基本操作

  • 使用 PageRank

  • 寻找连通分量

  • 执行邻域聚合

简介

图分析在我们的生活中比我们想象的更普遍。最常见的一个例子是,当我们要求 GPS 找到目的地最短路线时,它使用图处理算法。

让我们先了解图。图是一组顶点的表示,其中一些顶点对通过边连接。当这些边从一个方向移动到另一个方向时,它被称为有向图有向图

GraphX 是 Spark 的图处理 API。它提供了一个围绕 RDD 的包装器,称为弹性分布式属性图。属性图是一个带有属性附加到每个顶点和边的有向多重图。

有两种类型的图——有向图(有向图)和正则图。有向图的边只有一个方向,例如,从顶点 A 到顶点 B。Twitter 关注者是一个很好的有向图例子。如果 John 是 David 的 Twitter 关注者,并不意味着 David 是 John 的关注者。另一方面,Facebook 是一个很好的正则图例子。如果 John 是 David 的 Facebook 朋友,那么 David 也是 John 的 Facebook 朋友。

多重图是允许有多个边(也称为并行边)的图。由于 GraphX 中的每个边都有属性,每个边都有自己的标识。

传统上,对于分布式图处理,有两种类型的系统:

  • 数据并行

  • 图并行

GraphX 旨在将两者结合在一个系统中。GraphX API 允许用户在不移动数据的情况下将数据视为图和集合(RDD)。

图的基本操作

在这个食谱中,我们将学习如何创建图以及如何在它们上执行基本操作。

准备工作

作为起始示例,我们将有三个顶点,每个顶点代表加利福尼亚州三个城市的市中心——圣克拉拉、弗里蒙特和旧金山。以下是这些城市之间的距离:

目标 距离(英里)
圣克拉拉,CA 弗里蒙特,CA 20
弗里蒙特,CA 旧金山,CA 44
旧金山,CA 圣克拉拉,CA 53

如何实现...

  1. 导入 GraphX 相关类:

    scala> import org.apache.spark.graphx._
    scala> import org.apache.spark.rdd.RDD
    
    
  2. 将顶点数据加载到数组中:

    scala> val vertices = Array((1L, ("Santa Clara","CA")),(2L, ("Fremont","CA")),(3L, ("San Francisco","CA")))
    
    
  3. 将顶点数组加载到顶点 RDD 中:

    scala> val vrdd = sc.parallelize(vertices)
    
    
  4. 将边数据加载到数组中:

    scala> val edges = Array(Edge(1L,2L,20),Edge(2L,3L,44),Edge(3L,1L,53))
    
    
  5. 将数据加载到边 RDD 中:

    scala> val erdd = sc.parallelize(edges)
    
    
  6. 创建图:

    scala> val graph = Graph(vrdd,erdd)
    
    
  7. 打印图中所有顶点:

    scala> graph.vertices.collect.foreach(println)
    
    
  8. 打印图中所有边:

    scala> graph.edges.collect.foreach(println)
    
    
  9. 打印边三元组;边三元组是通过向边添加源和目标属性创建的:

    scala> graph.triplets.collect.foreach(println)
    
    
  10. 图的入度是指它拥有的内向边的数量。打印每个顶点的入度(作为VertexRDD[Int]):

    scala> graph.inDegrees
    
    

使用 PageRank

PageRank 衡量图中的每个顶点的重要性。PageRank 是由谷歌的创始人发起的,他们使用理论认为,互联网上最重要的页面是那些有最多链接指向它们的页面。PageRank 还考虑了指向目标页面的页面的重要性。因此,如果一个给定的网页有来自高排名页面的入链,它将被排名更高。

准备工作

我们将使用维基百科页面链接数据来计算页面排名。维基百科以数据库转储的形式发布其数据。我们将使用来自haselgrove.id.au/wikipedia.htm的链接数据,这些数据包含在两个文件中:

  • links-simple-sorted.txt

  • titles-sorted.txt

我已经将它们都放在 Amazon S3 的s3n://com.infoobjects.wiki/linkss3n://com.infoobjects.wiki/nodes上。由于数据量较大,建议您在 Amazon EC2 或本地集群上运行。沙盒可能非常慢。

您可以使用以下命令将文件加载到hdfs

$ hdfs dfs -mkdir wiki
$ hdfs dfs -put links-simple-sorted.txt wiki/links.txt
$ hdfs dfs -put titles-sorted.txt wiki/nodes.txt

如何做到这一点...

  1. 导入 GraphX 相关类:

    scala> import org.apache.spark.graphx._
    
    
  2. hdfs加载边,使用 20 个分区:

    scala> val edgesFile = sc.textFile("wiki/links.txt",20)
    
    

    或者,从 Amazon S3 加载边:

    scala> val edgesFile = sc.textFile("s3n:// com.infoobjects.wiki/links",20)
    
    

    注意

    links文件中的链接格式为“sourcelink: link1 link2 …”。

  3. 展开并转换为“link1,link2”格式的 RDD,然后将其转换为Edge对象的 RDD:

    scala> val edges = edgesFile.flatMap { line =>
     val links = line.split("\\W+")
     val from = links(0)
     val to = links.tail
     for ( link <- to) yield (from,link)
     }.map( e => Edge(e._1.toLong,e._2.toLong,1))
    
    
  4. hdfs加载顶点,使用 20 个分区:

    scala> val verticesFile = sc.textFile("wiki/nodes.txt",20)
    
    
  5. 或者,从 Amazon S3 加载边:

    scala> val verticesFile = sc.textFile("s3n:// com.infoobjects.wiki/nodes",20)
    
    
  6. 提供一个顶点的索引,然后将其交换成(索引,标题)格式:

    scala> val vertices = verticesFile.zipWithIndex.map(_.swap)
    
    
  7. 创建graph对象:

    scala> val graph = Graph(vertices,edges)
    
    
  8. 运行 PageRank 并获取顶点:

    scala> val ranks = graph.pageRank(0.001).vertices
    
    
  9. 由于排名是按照(顶点 ID,pagerank)格式,将其交换成(pagerank,顶点 ID)格式:

    scala> val swappedRanks = ranks.map(_.swap)
    
    
  10. 排序以首先获取最高排名的页面:

    scala> val sortedRanks = swappedRanks.sortByKey(false)
    
    
  11. 获取最高排名的页面:

    scala> val highest = sortedRanks.first
    
    
  12. 前面的命令给出了顶点 ID,您还需要查找以查看带有排名的实际标题。让我们进行连接操作:

    scala> val join = sortedRanks.join(vertices)
    
    
  13. 在将(顶点 ID,(页面排名,标题))格式转换为(页面排名,(顶点 ID,标题))格式后,再次对连接的 RDD 进行排序:

    scala> val final = join.map ( v => (v._2._1, (v._1,v._2._2))).sortByKey(false)
    
    
  14. 打印排名前五的页面

    scala> final.take(5).collect.foreach(println)
    
    

这是输出应该的样子:

(12406.054646736622,(5302153,United_States'_Country_Reports_on_Human_Rights_Practices))
(7925.094429748747,(84707,2007,_Canada_budget)) (7635.6564216408515,(88822,2008,_Madrid_plane_crash)) (7041.479913258444,(1921890,Geographic_coordinates)) (5675.169862343964,(5300058,United_Kingdom's))

寻找连通分量

连通分量是一个子图(一个顶点是原始图顶点集的子集,边是原始图边集的子集的图),其中任何两个顶点都通过一条边或一系列边相互连接。

理解它的一个简单方法是通过查看夏威夷的道路网络图。这个州有众多岛屿,它们之间没有道路相连。在每一个岛屿内,大多数道路都会相互连接。找到连通分量的目标是找到这些集群。

连通分量算法使用其最低编号顶点的 ID 来标记图中的每个连通分量。

准备工作

我们将构建一个小的图来表示我们已知的集群,并使用连通分量来分离它们。让我们看看以下数据:

准备工作

关注者 关注者
John Pat
Pat Dave
Gary Chris
Chris Bill

前面的数据是一个简单的数据,包含六个顶点和两个簇。让我们以两个文件的形式(nodes.csvedges.csv)存储这些数据。

以下为nodes.csv的内容:

1,John
2,Pat
3,Dave
4,Gary
5,Chris
6,Bill

以下为edges.csv的内容:

1,2,follows
2,3,follows
4,5,follows
5,6,follows

我们应该期望连通分量算法识别出两个簇,第一个由(1,John)识别,第二个由(4,Gary)识别。

您可以使用以下命令将文件加载到hdfs

$ hdfs dfs -mkdir data/cc
$ hdfs dfs -put nodes.csv data/cc/nodes.csv
$ hdfs dfs -put edges.csv data/cc/edges.csv

如何做……

  1. 加载 Spark shell:

    $ spark-shell
    
    
  2. 导入 GraphX 相关类:

    scala> import org.apache.spark.graphx._
    
    
  3. hdfs加载边:

    scala> val edgesFile = sc.textFile("hdfs://localhost:9000/user/hduser/data/cc/edges.csv")
    
    
  4. edgesFile RDD 转换为边的 RDD:

    scala> val edges = edgesFile.map(_.split(",")).map(e => Edge(e(0).toLong,e(1).toLong,e(2)))
    
    
  5. hdfs加载顶点:

    scala> val verticesFile = sc.textFile("hdfs://localhost:9000/user/hduser/data/cc/nodes.csv")
    
    
  6. 映射顶点:

    scala> val vertices = verticesFile.map(_.split(",")).map( e => (e(0).toLong,e(1)))
    
    
  7. 创建graph对象:

    scala> val graph = Graph(vertices,edges)
    
    
  8. 计算连通分量:

    scala> val cc = graph.connectedComponents
    
    
  9. 找到连通分量的顶点(这是一个子图):

    scala> val ccVertices = cc.vertices
    
    
  10. 打印ccVertices

    scala> ccVertices.collect.foreach(println)
    
    

如输出所示,顶点 1、2、3 指向 1,而 4、5、6 指向 4。这两个都是它们各自簇中索引最低的顶点。

执行邻域聚合

GraphX 通过隔离每个顶点和其邻居来进行大部分计算。这使得在分布式系统上处理大规模图数据变得更容易。这使得邻域操作非常重要。GraphX 有一个机制,以aggregateMessages方法的形式在每个邻域级别执行它。它分两步进行:

  1. 在第一步(方法的第一个函数)中,消息被发送到目标顶点或源顶点(类似于 MapReduce 中的 Map 函数)。

  2. 在第二步(方法的第二个函数)中,对这些消息进行聚合(类似于 MapReduce 中的 Reduce 函数)。

准备工作

让我们构建一个包含关注者的小型数据集:

关注者 关注者
John Barack
Pat Barack
Gary Barack
Chris Mitt
Rob Mitt

我们的目标是找出每个节点有多少关注者。让我们以两个文件的形式加载这些数据:nodes.csvedges.csv

以下为nodes.csv的内容:

1,Barack
2,John
3,Pat
4,Gary
5,Mitt
6,Chris
7,Rob

以下为edges.csv的内容:

2,1,follows
3,1,follows
4,1,follows
6,5,follows
7,5,follows

您可以使用以下命令将文件加载到hdfs

$ hdfs dfs -mkdir data/na
$ hdfs dfs -put nodes.csv data/na/nodes.csv
$ hdfs dfs -put edges.csv data/na/edges.csv

如何做……

  1. 加载 Spark shell:

    $ spark-shell
    
    
  2. 导入 GraphX 相关类:

    scala> import org.apache.spark.graphx._
    
    
  3. hdfs加载边:

    scala> val edgesFile = sc.textFile("hdfs://localhost:9000/user/hduser/data/na/edges.csv")
    
    
  4. 将边转换为边的 RDD:

    scala> val edges = edgesFile.map(_.split(",")).map(e => Edge(e(0).toLong,e(1).toLong,e(2)))
    
    
  5. hdfs加载顶点:

    scala> val verticesFile = sc.textFile("hdfs://localhost:9000/user/hduser/data/cc/nodes.csv")
    
    
  6. 映射顶点:

    scala> val vertices = verticesFile.map(_.split(",")).map( e => (e(0).toLong,e(1)))
    
    
  7. 创建graph对象:

    scala> val graph = Graph(vertices,edges)
    
    
  8. 通过向每个关注者发送包含其关注者数量的消息来进行邻域聚合,即 1,然后添加关注者数量:

    scala> val followerCount = graph.aggregateMessages(Int), (a, b) => (a+b))
    
    
  9. 以(关注者,关注者数量)的形式打印followerCount

    scala> followerCount.collect.foreach(println)
    
    

您应该得到以下类似的结果:

(1,3)
(5,2)

第十二章. 优化和性能调整

本章涵盖了与 Spark 一起工作时各种优化和性能调整的最佳实践。

本章分为以下食谱:

  • 优化内存

  • 使用压缩来提高性能

  • 使用序列化来提高性能

  • 优化垃圾回收

  • 优化并行级别

  • 理解优化的未来——Tungsten 项目

介绍

在探讨优化 Spark 的各种方法之前,了解 Spark 的内部结构是一个好主意。到目前为止,我们已经在较高层次上了解了 Spark,重点是各种库提供的功能。

让我们从重新定义一个 RDD 开始。从外部来看,RDD 是一个分布式的不可变对象集合。从内部来看,它由以下五个部分组成:

  • 分区集合(rdd.getPartitions)

  • 父 RDD 的依赖关系列表(rdd.dependencies)

  • 根据其父 RDD 计算分区的函数

  • 分区器(可选)(rdd.partitioner)

  • 每个分区的首选位置(可选)(rdd.preferredLocations)

前三项是 RDD 重新计算所需的,以防数据丢失。当结合在一起时,它被称为血缘。最后两部分是优化。

一组分区是将数据划分到节点的方式。在 HDFS 的情况下,这意味着InputSplits,它们基本上与块相同(除非记录跨越块边界;在这种情况下,它将略大于块)。

让我们回顾一下wordCount示例,以了解这五个部分。这是在数据集级别视图下wordCount的 RDD 图:

介绍

基本上,流程是这样的:

  1. words文件夹加载为 RDD:

    scala> val words = sc.textFile("hdfs://localhost:9000/user/hduser/words")
    
    

    以下为words RDD 的五个部分:

    分区 每个 hdfs 输入 split/block 有一个分区(org.apache.spark.rdd.HadoopPartition)
    依赖关系
    计算函数 读取块
    首选位置 HDFS 块位置
    分区器
  2. words RDD 中的单词分词,每个单词占一行:

    scala> val wordsFlatMap = words.flatMap(_.split("\\W+"))
    
    

    以下为wordsFlatMap RDD 的五个部分:

    分区 与父 RDD 相同,即words (org.apache.spark.rdd.HadoopPartition)
    依赖关系 与父 RDD 相同,即words (org.apache.spark.OneToOneDependency)
    计算函数 计算父 RDD 并分割每个元素,然后将结果展平
    首选位置 询问父 RDD
    分区器
  3. wordsFlatMap RDD 中的每个单词转换为(word,1)元组:

    scala> val wordsMap = wordsFlatMap.map( w => (w,1))
    
    

    以下为wordsMap RDD 的五个部分:

    分区 与父 RDD 相同,即 wordsFlatMap (org.apache.spark.rdd.HadoopPartition)
    依赖关系 与父 RDD 相同,即 wordsFlatMap (org.apache.spark.OneToOneDependency)
    计算函数 计算父 RDD 并将其映射到 PairRDD
    首选位置 询问父 RDD
    分区器
  4. 对给定键的所有值进行归约并求和:

    scala> val wordCount = wordsMap.reduceByKey(_+_)
    
    

    以下为wordCount RDD 的五个部分:

    分区 每个 reduce 任务一个(org.apache.spark.rdd.ShuffledRDDPartition
    依赖关系 对每个父 RDD 的 shuffle 依赖(org.apache.spark.ShuffleDependency
    计算函数 对 shuffle 数据进行加法运算
    首选位置
    分区器 HashPartitioner(org.apache.spark.HashPartitioner

这是在分区级别视图下wordCount RDD 图的样子:

介绍

优化内存

Spark 是一个复杂的分布式计算框架,有许多组成部分。各种集群资源,如内存、CPU 和网络带宽,在各个点上可能会成为瓶颈。由于 Spark 是一个内存计算框架,内存的影响最大。

另一个问题是在 Spark 应用程序中,使用大量内存是很常见的,有时甚至超过 100 GB。这种内存使用量在传统的 Java 应用程序中并不常见。

在 Spark 中,有两个地方需要进行内存优化,那就是在驱动程序和执行器级别。

您可以使用以下命令设置驱动程序内存:

  • Spark shell:

    $ spark-shell --drive-memory 4g
    
    
  • Spark 提交:

    $ spark-submit --drive-memory 4g
    
    

您可以使用以下命令设置执行器内存:

  • Spark shell:

    $ spark-shell --executor-memory 4g
    
    
  • Spark 提交:

    $ spark-submit --executor-memory 4g
    
    

要理解内存优化,了解 Java 中的内存管理机制是很有帮助的。在 Java 中,对象位于堆中。堆在 JVM 启动时创建,并在需要时可以调整大小(基于最小和最大大小,即配置中分别指定的-Xms-Xmx)。

堆被分为两个空间或代:年轻代和老年代。年轻代是为新对象分配预留的。年轻代包括一个称为伊甸园的区域和两个较小的幸存者空间。当 nursery 填满时,通过运行一个称为年轻收集的特殊过程进行垃圾回收,其中所有存活时间足够长的对象被提升到老年代。当老年代填满时,通过运行一个称为老年代收集的过程在那里进行垃圾回收。

优化内存

幼儿园背后的逻辑是,大多数对象的生命周期非常短。年轻收集被设计成快速找到新分配的对象并将它们移动到老年代。

JVM 使用标记-清除算法进行垃圾回收。标记-清除收集包括两个阶段。

在标记阶段,所有具有活动引用的对象都被标记为存活,其余的被认为是垃圾回收的候选者。在清除阶段,垃圾收集的候选者所占用的空间被添加到空闲列表中,即它们可以被分配给新的对象。

标记和清除有两个改进。一个是并发标记和清除CMS),另一个是并行标记和清除。CMS 侧重于降低延迟,而后者侧重于提高吞吐量。两种策略都有性能权衡。CMS 不执行压缩,而并行垃圾回收器GC)仅执行整个堆的压缩,这导致了暂停时间。作为一个经验法则,对于实时流,应使用 CMS,否则使用并行 GC。

如果你希望同时拥有低延迟和高吞吐量,从 Java 1.7 更新 4 开始,还有一个名为垃圾回收优先级 GCG1)的选项。G1 是一种服务器风格的垃圾回收器,主要用于具有大内存的多核机器。它计划作为 CMS 的长期替代品。因此,为了修改我们的经验法则,如果你使用的是 Java 7 及以上版本,只需简单地使用 G1。

G1 将堆划分为一组等大小的区域,其中每个集合是一个连续的虚拟内存范围。每个区域被分配一个角色,如 Eden、Survivor 和 Old。G1 执行一个并发全局标记阶段,以确定整个堆中对象的存活引用。在标记阶段完成后,G1 知道哪些区域大部分是空的。它首先在这些区域进行收集,从而释放出更多的内存。

优化内存

G1 选定的垃圾回收候选区域使用 evacuation 进行垃圾回收。G1 将堆中的一个或多个区域的对象复制到堆上的一个单独区域,并且同时压缩和释放内存。这种 evacuation 在多个核心上并行执行以减少暂停时间并提高吞吐量。因此,每次垃圾回收周期都会在用户定义的暂停时间内减少碎片。

Java 内存优化有三个方面的内容:

  • 内存占用

  • 访问内存中对象的成本

  • 垃圾回收的成本

通常,Java 对象访问速度快,但比它们内部实际数据消耗的空间要多得多。

使用压缩来提高性能

数据压缩涉及使用比原始表示更少的位来编码信息。压缩在大数据技术中扮演着重要的角色。它使得数据的存储和传输更加高效。

当数据被压缩时,它变得更小,因此磁盘 I/O 和网络 I/O 都变得更快。它还节省了存储空间。每个优化都有成本,压缩的成本是以压缩和解压缩数据时增加的 CPU 周期形式出现的。

Hadoop 需要分割数据以将它们放入块中,无论数据是否被压缩。只有少数压缩格式是可分割的。

大数据负载中最流行的两种压缩格式是 LZO 和 Snappy。Snappy 是不可分割的,而 LZO 是可分割的。另一方面,Snappy 是一种格式速度更快的格式。

如果压缩格式可分割,如 LZO,则首先将输入文件分割成块,然后压缩。由于压缩发生在块级别,解压缩也可以在块级别以及节点级别发生。

如果压缩格式不可分割,压缩将在文件级别发生,然后将其分割成块。在这种情况下,必须在解压缩之前将块合并回文件,因此解压缩不能在节点级别发生。

对于支持的压缩格式,Spark 将自动部署编解码器来自动解压缩,用户不需要采取任何操作。

使用序列化来提高性能

序列化在分布式计算中扮演着重要的角色。有两种持久化(存储)级别,支持序列化 RDD:

  • MEMORY_ONLY_SER:这以序列化对象的形式存储 RDD。它将为每个分区创建一个字节数组

  • MEMORY_AND_DISK_SER:这与MEMORY_ONLY_SER类似,但它将无法适应内存的分区溢出到磁盘

以下是将适当的持久化级别添加的步骤:

  1. 启动 Spark shell:

    $ spark-shell
    
    
  2. 导入与StorageLevel相关的StorageLevel和隐式转换:

    scala> import org.apache.spark.storage.StorageLevel._
    
    
  3. 创建一个 RDD:

    scala> val words = sc.textFile("words")
    
    
  4. 持久化 RDD:

    scala> words.persist(MEMORY_ONLY_SER)
    
    

虽然序列化大大减少了内存占用,但它由于反序列化而增加了额外的 CPU 周期。

默认情况下,Spark 使用 Java 的序列化。由于 Java 序列化速度慢,更好的方法是使用Kryo库。Kryo要快得多,有时甚至比默认的快 10 倍。

如何操作...

你可以通过在SparkConf中进行以下设置来使用Kryo

  1. 通过设置Kryo作为序列化器来启动 Spark shell:

    $ spark-shell --conf spark.serializer=org.apache.spark.serializer.KryoSerializer
    
    
  2. Kryo自动注册大多数核心 Scala 类,但如果你想要注册自己的类,可以使用以下命令:

    scala> sc.getConf.registerKryoClasses(Array(classOf[com.infoobjects.CustomClass1],classOf[com.infoobjects.CustomClass2])
    
    

优化垃圾回收

如果你有很多生命周期短的 RDD,JVM 垃圾回收可能是一个挑战。JVM 需要遍历所有对象以找到需要垃圾回收的对象。垃圾回收的成本与 GC 需要遍历的对象数量成正比。因此,使用较少的对象和使用较少对象的数据结构(如数组)的数据结构有助于。

序列化在这里也表现出色,因为只需要一个对象进行垃圾回收。

默认情况下,Spark 使用执行器内存的 60%来缓存 RDD,其余 40%用于常规对象。有时,你可能不需要为 RDD 分配 60%,可以降低这个限制,以便为对象创建提供更多空间(减少对 GC 的需求)。

如何操作...

你可以通过启动 Spark shell 并设置内存分数来将分配给 RDD 缓存的内存设置为 40%:

$ spark-shell --conf spark.storage.memoryFraction=0.4

优化并行级别

优化并行级别对于充分利用集群容量非常重要。在 HDFS 的情况下,这意味着分区数与InputSplits数相同,这通常与块数相同。

在这个菜谱中,我们将介绍不同的方法来优化分区数量。

如何做到这一点…

在将文件加载到 RDD 时指定分区数,按照以下步骤:

  1. 启动 Spark shell:

    $ spark-shell
    
    
  2. 将 RDD 加载为自定义分区数作为第二个参数:

    scala> sc.textFile("hdfs://localhost:9000/user/hduser/words",10)
    
    

另一种方法是按照以下步骤更改默认的并行度:

  1. 使用新的默认并行度值启动 Spark shell:

    $ spark-shell --conf spark.default.parallelism=10
    
    
  2. 检查并行度的默认值:

    scala> sc.defaultParallelism
    
    

注意

您还可以使用名为coalesce(numPartitions)的 RDD 方法来减少分区数量,其中numPartitions是您希望得到的最终分区数。如果您希望数据在网络中重新洗牌,可以调用名为repartition(numPartitions)的 RDD 方法,其中numPartitions是您希望得到的最终分区数。

理解优化的未来——项目钨丝

项目钨丝,从 Spark 版本 1.4 开始,旨在将 Spark 与裸金属更紧密地结合。该项目的目标是显著提高 Spark 应用程序的内存和 CPU 效率,并推动底层硬件的极限。

在分布式系统中,传统的做法是始终优化网络 I/O,因为那一直是稀缺和瓶颈资源。这种趋势在最近几年发生了变化。在过去 5 年里,网络带宽从每秒 1 千兆比特增加到每秒 10 千兆比特。

类似地,磁盘带宽也从 50 MB/s 增加到 500 MB/s,SSD 的使用也越来越广泛。另一方面,CPU 时钟速度在 5 年前大约是 3 GHz,现在仍然是这个速度。这已经取代了网络,使 CPU 成为分布式处理中的新瓶颈。

注意

另一种对 CPU 性能造成更大负担的趋势是新的压缩数据格式,如 Parquet。正如我们在本章前面的菜谱中看到的,压缩和序列化都需要更多的 CPU 周期。这种趋势也推动了 CPU 优化的需求,以减少 CPU 周期成本。

在类似的方向上,让我们看看内存占用。在 Java 中,GC 负责内存管理。GC 在将内存管理从程序员手中拿走并使其透明化方面做得非常出色。为此,Java 必须投入大量的开销,这大大增加了内存占用。例如,一个简单的字符串"abcd",理想情况下应该占用 4 字节,但在 Java 中却占用了 48 字节。

如果我们放弃 GC 并像在 C 等低级编程语言中那样手动管理内存会怎样?从 1.7 版本开始,Java 提供了一种方法来做这件事,它被称为sun.misc.Unsafe。Unsafe 基本上意味着您可以在没有任何安全检查的情况下构建长内存区域。这是项目钨丝的第一个特性。

通过利用应用语义进行手动内存管理

通过利用应用语义进行手动内存管理,如果你不知道自己在做什么,这可能会非常危险,但在 Spark 中却是一种祝福。我们利用对数据模式(DataFrames)的了解来直接布局内存。这不仅消除了 GC 开销,还让你最小化内存占用。

第二点是存储数据在 CPU 缓存与内存之间。众所周知,CPU 缓存非常出色,因为它从主内存获取数据需要三个周期,而缓存中只需要一个周期。这是 Tungsten 项目的第二个特性。

使用算法和数据结构

算法和数据结构被用来利用内存层次结构并实现更缓存感知的计算。

CPU 缓存是存储 CPU 即将需要的数据的小型内存池。CPU 有两种类型的缓存:指令缓存和数据缓存。数据缓存按 L1、L2 和 L3 的层次结构排列:

  • L1 缓存是计算机中最快且最昂贵的缓存。它存储最关键的数据,是 CPU 寻找信息的第一个地方。

  • L2 缓存比 L1 缓存略慢,但仍然位于同一处理器芯片上。这是 CPU 寻找信息的第二个地方。

  • L3 缓存仍然较慢,但由所有核心共享,例如 DRAM(内存)。

这些可以在以下图中看到:

使用算法和数据结构

第三点是 Java 在生成诸如表达式评估之类的字节码方面并不十分出色。如果这种代码生成是手动完成的,它将更加高效。代码生成是 Tungsten 项目的第三个特性。

代码生成

这涉及到利用现代编译器和 CPU 来允许直接在二进制数据上高效操作。Tungsten 项目目前还处于起步阶段,将在 1.5 版本中提供更广泛的支持。

posted @ 2025-10-24 10:02  绝不原创的飞龙  阅读(5)  评论(0)    收藏  举报