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

新术语重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会以这种形式出现在文本中:"在右上角的帐户名称下单击安全凭据。"

注意

警告或重要说明会以这种形式出现在方框中。

提示

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

第一章: 开始使用 Apache Spark

在本章中,我们将设置和配置 Spark。本章分为以下教程:

  • 从二进制文件安装 Spark

  • 使用 Maven 构建 Spark 源代码

  • 在 Amazon EC2 上启动 Spark

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

  • 在 Mesos 集群上部署 Spark

  • 在 YARN 集群上部署 Spark

  • 使用 Tachyon 作为离堆存储层

介绍

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 有多个带有供应商增强的发行版可用。

以下图显示了 Spark 生态系统:

Introduction

Spark 运行时在各种集群管理器上运行,包括 YARN(Hadoop 的计算框架)、Mesos 和 Spark 自己的集群管理器独立模式。Tachyon 是一个以内存为中心的分布式文件系统,可以在集群框架之间以内存速度可靠地共享文件。简而言之,它是内存中的离堆存储层,有助于在作业和用户之间共享数据。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

  1. 解压二进制文件:
$ tar -zxf spark-1.4.0-bin-hadoop2.4.tgz

  1. 通过剥离版本信息重命名包含二进制文件的文件夹:
$ sudo mv spark-1.4.0-bin-hadoop2.4 spark

  1. 将配置文件夹移动到/etc文件夹,以便稍后可以将其创建为符号链接:
$ sudo mv spark/conf/* /etc/spark

  1. /opt目录下创建您公司特定的安装目录。由于本书中的示例在infoobjects沙箱上进行了测试,我们将使用infoobjects作为目录名称。创建/opt/infoobjects目录:
$ sudo mkdir -p /opt/infoobjects

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

  1. 更改spark主目录的所有权为root
$ sudo chown -R root:root /opt/infoobjects/spark

  1. 更改spark主目录的权限,0755 = 用户:读-写-执行组:读-执行世界:读-执行
$ sudo chmod -R 755 /opt/infoobjects/spark

  1. 转到spark主目录:
$ cd /opt/infoobjects/spark

  1. 创建符号链接:
$ sudo ln -s /etc/spark conf

  1. .bashrc中追加PATH
$ echo "export PATH=$PATH:/opt/infoobjects/spark/bin" >> /home/hduser/.bashrc

  1. 打开一个新的终端。

  2. /var中创建log目录:

$ sudo mkdir -p /var/log/spark

  1. hduser设置为 Spark log目录的所有者。
$ sudo chown -R hduser:hduser /var/log/spark

  1. 创建 Spark tmp目录:
$ mkdir /tmp/spark

  1. 使用以下命令行配置 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

  1. 打开一个新的终端窗口并从 GitHub 下载 Spark 源代码:
$ wget https://github.com/apache/spark/archive/branch-1.4.zip

  1. 解压缩存档:
$ gunzip branch-1.4.zip

  1. 转到spark目录:
$ cd spark

  1. 使用以下标志编译源代码:启用 Yarn,Hadoop 版本 2.4,启用 Hive,并跳过测试以加快编译速度:
$ mvn -Pyarn -Phadoop-2.4 -Dhadoop.version=2.4.0 -Phive -DskipTests clean package

  1. conf文件夹移动到etc文件夹,以便稍后可以将其创建为符号链接:
$ sudo mv spark/conf /etc/

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

  1. 更改spark主目录的所有权为root
$ sudo chown -R root:root /opt/infoobjects/spark

  1. 更改spark主目录的权限0755 = 用户:rwx 组:r-x 世界:r-x
$ sudo chmod -R 755 /opt/infoobjects/spark

  1. 转到spark主目录:
$ cd /opt/infoobjects/spark

  1. 创建一个符号链接:
$ sudo ln -s /etc/spark conf

  1. 通过编辑.bashrc将 Spark 可执行文件放入路径中:
$ echo "export PATH=$PATH:/opt/infoobjects/spark/bin" >> /home/hduser/.bashrc

  1. /var中创建log目录:
$ sudo mkdir -p /var/log/spark

  1. hduser设置为 Spark log目录的所有者:
$ sudo chown -R hduser:hduser /var/log/spark

  1. 创建 Spark tmp目录:
$ mkdir /tmp/spark

  1. 使用以下命令行配置 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 Elastic Compute CloudAmazon 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>

  1. 使用示例值启动集群:
$ 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>: 这是集群的名称

  1. 有时,默认的可用区不可用;在这种情况下,通过指定您正在请求的特定可用区来重试发送请求:
$ 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

  1. 如果您的应用程序需要在实例关闭后保留数据,请将 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

  1. 如果您使用 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)。

  1. 一切都启动后,通过转到最后打印的 web UI URL 来检查集群的状态。如何做...

  2. 检查集群的状态:如何做...

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

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

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

如何做...

  1. 检查主节点中的目录并查看它们的作用:
目录 描述
ephemeral-hdfs 这是 Hadoop 实例,其中的数据是暂时的,当您停止或重新启动机器时会被删除。
persistent-hdfs 每个节点都有非常少量的持久存储(大约 3 GB)。如果使用此实例,数据将保留在该空间中。
hadoop-native 这些是支持 Hadoop 的本地库,如 snappy 压缩库。
Scala 这是 Scala 安装。
shark 这是 Shark 安装(Shark 不再受支持,已被 Spark SQL 取代)。
spark 这是 Spark 安装
spark-ec2 这些是支持此集群部署的文件。
tachyon 这是 Tachyon 安装
  1. 使用以下命令检查暂时实例中的 HDFS 版本:
$ ephemeral-hdfs/bin/hadoop version
Hadoop 2.0.0-chd4.2.0

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

  1. 更改日志中的配置级别:
$ cd spark/conf

  1. 默认的日志级别信息太冗长了,所以让我们将其更改为错误:如何做...

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

$ mv log4j.properties.template log4j.properties

  1. 在 vi 或您喜欢的编辑器中打开 log4j.properties
$ vi log4j.properties

  1. 将第二行从 | log4j.rootCategory=INFO, console 更改为 | log4j.rootCategory=ERROR, console

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

$ spark-ec2/copydir spark/conf

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

如何做...

  1. 销毁 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

  1. 启动独立的主服务器(首先 SSH 到主节点):
hduser@m1.zettabytes.com~] start-master.sh

默认情况下,Master 在端口 7077 上启动,从节点使用该端口连接到 Master。它还在端口 8088 上有一个 Web UI。

  1. 请 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 核心数(仅限 worker)
- -m <memory>,--memory <memory> 机器上可以用于 Spark 应用程序的总 RAM(仅限 worker)
- -d <dir>,--work-dir <dir> 用于临时空间和作业输出日志的目录
  1. 与手动在每个节点上启动主和从守护程序相比,也可以使用集群启动脚本来完成。

  2. 首先,在主节点上创建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 在 slaves 文件中的每个节点上启动一个从节点实例
- start-all.sh 启动主节点和从节点
- stop-master.sh 停止主机上的主实例
- stop-slaves.sh 停止 slaves 文件中所有节点上的从节点实例
- stop-all.sh 停止主节点和从节点
  1. 通过 Scala 代码将应用程序连接到集群:
val sparkContext = new SparkContext(new SparkConf().setMaster("spark://m1.zettabytes.com:7077")

  1. 通过 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 和 worker 都非常轻量级。通常,500 MB 到 1 GB 之间的内存分配就足够了。可以通过在conf/spark-env.sh中设置SPARK_DAEMON_MEMORY参数来设置这个值。例如,以下配置将为 master 和 worker daemon 设置内存为 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 worker 默认使用从属机器上的所有核心作为其执行器。如果要限制工作程序可以使用的核心数,可以通过以下配置将其设置为该数字(例如 12):

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

Spark worker 默认使用所有可用的 RAM(执行器为 1 GB)。请注意,您无法分配每个特定执行器将使用多少内存(您可以从驱动程序配置中控制此操作)。要为所有执行器组合使用的总内存(例如,24 GB)分配另一个值,请执行以下设置:

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

在驱动程序级别可以进行一些设置:

  • 要指定集群中给定应用程序可以使用的最大 CPU 核心数,可以在 Spark submit 或 Spark shell 中设置spark.cores.max配置如下:
$ spark-submit --conf spark.cores.max=12

  • 要指定每个执行器应分配的内存量(最低建议为 8 GB),可以在 Spark submit 或 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

  1. 更新存储库:
$ sudo apt-get -y update

  1. 安装 Mesos:
$ sudo apt-get -y install mesos

  1. 将 Spark 连接到 Mesos 以将 Spark 与 Mesos 集成,使 Spark 二进制文件可用于 Mesos,并配置 Spark 驱动程序以连接到 Mesos。

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

$ 
hdfs dfs
 -put spark-1.4.0-bin-hadoop2.4.tgz spark-1.4.0-bin-hadoop2.4.tgz

  1. 单主 Mesos 的主 URL 是mesos://host:5050,而 ZooKeeper 管理的 Mesos 集群的主 URL 是mesos://zk://host:2181

  2. 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

  1. 从 Scala 程序运行:
val conf = new SparkConf().setMaster("mesos://host:5050")
val sparkContext = new SparkContext(conf)

  1. 从 Spark shell 运行:
$ spark-shell --master mesos://host:5050

注意

Mesos 有两种运行模式:

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

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

  1. 要在粗粒度模式下运行,设置spark.mesos.coarse属性:
conf.set("spark.mesos.coarse","true")

在具有 YARN 的集群上部署

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

YARN 遵循主从架构。主守护程序称为ResourceManager,从守护程序称为NodeManager。除此应用程序外,生命周期管理由ApplicationMaster完成,它可以在任何从节点上生成,并在应用程序的生命周期内保持活动状态。

当 Spark 在 YARN 上运行时,ResourceManager扮演 Spark master 的角色,而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

您可以在以下截图中看到这一点:

如何操作...

  1. 以下命令在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

  1. 以下命令在yarn-client模式下启动 Spark shell:
$ spark-shell --master yarn-client

  1. 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 主节点,因为它是从 Hadoop 配置中选择的,主参数是yarn-clientyarn-cluster

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

工作原理...

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

工作原理...

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

  • --num-executors:配置将分配多少个 executor

  • --executor-memory:每个 executor 的 RAM

  • --executor-cores:每个 executor 的 CPU 核心

使用 Tachyon 作为离堆存储层

Spark RDD 是一种在内存中存储数据集的好方法,同时在不同应用程序中产生相同数据的多个副本。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

  1. 解压源代码:
$ unzip  v-<version>.zip

  1. 为方便起见,从tachyon源文件夹名称中删除版本:
$ mv tachyon-<version> tachyon

  1. 切换到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

  1. 注释以下行:
export TACHYON_UNDERFS_ADDRESS=$TACHYON_HOME/underfs

  1. 取消注释以下行:
export TACHYON_UNDERFS_ADDRESS=hdfs://localhost:9000

  1. 更改以下属性:
-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

  1. ${tachyon.home}替换为/var/log/tachyon

  2. 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

  1. <tachyon home>/bin添加到路径中:
$ echo "export PATH=$PATH:/opt/infoobjects/tachyon/bin" >> /home/hduser/.bashrc

  1. 重新启动 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

如何操作...

  1. 运行示例程序,查看 Tachyon 是否正常运行:
$ tachyon runTest Basic CACHE_THROUGH

如何操作...

  1. 您可以随时通过运行以下命令停止 Tachyon:
$ tachyon-stop.sh

  1. 在 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 shell

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

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

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

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

介绍

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

探索 Spark shell

Spark 自带一个 REPL shell,它是 Scala shell 的包装器。尽管 Spark shell 看起来像是用于简单事务的命令行,但实际上也可以使用它执行许多复杂的查询。本章探讨了可以开发 Spark 应用程序的不同开发环境。

如何做...

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

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

  1. 进入words目录:
$ cd words

  1. 创建一个sh.txt文本文件,并在其中输入"to be or not to be"
$ echo "to be or not to be" > sh.txt

  1. 启动 Spark shell:
$ spark-shell

  1. words目录加载为 RDD:
Scala> val words = sc.textFile("hdfs://localhost:9000/user/hduser/words")

  1. 统计行数(结果:1):
Scala> words.count

  1. 将行(或行)分成多个单词:
Scala> val wordsFlatMap = words.flatMap(_.split("\\W+"))

  1. word转换为(word,1)—即,将1作为每个word出现的值作为键输出:
Scala> val wordsMap = wordsFlatMap.map( w => (w,1))

  1. 使用reduceByKey方法将每个单词的出现次数作为键相加(该函数在两个连续值上运行,由ab表示):
Scala> val wordCount = wordsMap.reduceByKey( (a,b) => (a+b))

  1. 对结果进行排序:
Scala> val wordCountSorted = wordCount.sortByKey(true)

  1. 打印 RDD:
Scala> wordCountSorted.collect.foreach(println)

  1. 将所有前述操作合并为一步如下:
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. 选择<eclipse 版本>更新站点。

  4. 单击协作工具

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

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

重新启动 Eclipse 后,将会出现提示安装 Maven。

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

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

  2. 单击“工作区”下拉菜单。

  3. 选择<eclipse 版本>更新站点。

  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 开发!

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

Simple Build ToolSBT)是专为基于 Scala 的开发而制作的构建工具。SBT 遵循 Maven 的命名约定和声明性依赖管理。

SBT 相对于 Maven 提供了以下增强功能:

  • 依赖关系以build.sbt文件中的键值对的形式提供,而不是 Maven 中的pom.xml

  • 它提供了一个 shell,非常方便执行构建操作

  • 对于没有依赖关系的简单项目,甚至不需要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

  1. 不带任何参数启动sbt shell:
$sbt

  1. 键入eclipse,它将创建一个准备好的 Eclipse 项目:
$ eclipse

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

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

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

IntelliJ IDEA 自带了对 Maven 的支持。我们将看到如何在本食谱中创建一个新的 Maven 项目。

如何做...

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

  1. 在新项目窗口中选择Maven,然后单击下一步如何做...

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

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

  4. 单击完成,Maven 项目已准备就绪。

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

在 Eclipse 成名之前,IntelliJ IDEA 被认为是最优秀的 IDE 之一。IDEA 至今仍然保持着它以前的荣耀,很多开发者喜爱 IDEA。IDEA 也有一个免费的社区版。IDEA 对 SBT 提供了原生支持,这使得它非常适合 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 加载数据

  • 从亚马逊 S3 加载数据

  • 从 Apache Cassandra 加载数据

  • 从关系数据库加载数据

介绍

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

Hadoop 支持的存储意味着可以与 Hadoop 的InputFormatOutputFormat接口一起使用的存储格式。InputFormat负责从输入数据创建InputSplits,并将其进一步划分为记录。OutputFormat负责写入存储。

我们将从本地文件系统开始写入,然后转移到从 HDFS 加载数据。在从 HDFS 加载数据的示例中,我们将介绍最常见的文件格式:常规文本文件。在下一个示例中,我们将介绍如何在 Spark 中使用任何InputFormat接口来加载数据。我们还将探讨如何加载存储在亚马逊 S3 中的数据,这是一个领先的云存储平台。

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

从本地文件系统加载数据

尽管本地文件系统不适合存储大数据,因为磁盘大小限制和缺乏分布式特性,但从技术上讲,你可以使用本地文件系统在分布式系统中加载数据。但是你要访问的文件/目录必须在每个节点上都可用。

请注意,如果您计划使用此功能来加载辅助数据,这不是一个好主意。为了加载辅助数据,Spark 有一个广播变量功能,将在接下来的章节中讨论。

在这个示例中,我们将看看如何从本地文件系统中加载数据到 Spark 中。

如何做...

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

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

  1. 进入words目录:
$ cd words

  1. 创建sh.txt文本文件,并在其中输入"to be or not to be"
$ echo "to be or not to be" > sh.txt

  1. 启动 Spark shell:
$ spark-shell

  1. words目录加载为 RDD:
scala> val words = sc.textFile("file:///home/hduser/words")

  1. 计算行数:
scala> words.count

  1. 将行(或行)分成多个单词:
scala> val wordsFlatMap = words.flatMap(_.split("\\W+"))

  1. word转换为(word,1)—即,将1作为每个word的出现次数的值输出为键:
scala> val wordsMap = wordsFlatMap.map( w => (w,1))

  1. 使用reduceByKey方法将每个单词的出现次数作为键添加(此函数一次处理两个连续的值,表示为ab):
scala> val wordCount = wordsMap.reduceByKey( (a,b) => (a+b))

  1. 打印 RDD:
scala> wordCount.collect.foreach(println)

  1. 在一个步骤中执行所有前面的操作如下:
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方法允许您这样做。它创建一个 RDD,其中文件名和路径(例如hdfs://localhost:9000/user/hduser/words)作为键,整个文件的内容作为值。

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

在本教程中,我们将学习如何从 HDFS 中的 Spark shell 加载数据。

如何做...

让我们进行单词计数,计算每个单词的出现次数。在本教程中,我们将从 HDFS 加载数据。

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

  1. 更改目录到words
$ cd words

  1. 创建sh.txt text文件并在其中输入"to be or not to be"
$ echo "to be or not to be" > sh.txt

  1. 启动 Spark shell:
$ spark-shell

  1. words目录加载为 RDD:
scala> val words = sc.textFile("hdfs://localhost:9000/user/hduser/words")

注意

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

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

  1. 计算行数(结果将为1):
scala> words.count

  1. 将行(或行)分成多个单词:
scala> val wordsFlatMap = words.flatMap(_.split("\\W+"))

  1. 将单词转换为(word,1)——也就是说,将word作为键的每次出现输出1作为值:
scala> val wordsMap = wordsFlatMap.map( w => (w,1))

  1. 使用reduceByKey方法将每个单词的出现次数作为键相加(此函数一次处理两个连续的值,由ab表示):
scala> val wordCount = wordsMap.reduceByKey( (a,b) => (a+b))

  1. 打印 RDD:
scala> wordCount.collect.foreach(println)

  1. 在一步中执行所有前面的操作如下:
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 是天气局陆军海军位置编号。

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

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

  1. 下载数据:
$ wget -r ftp://ftp.ncdc.noaa.gov/pub/data/noaa/

  1. 在 HDFS 中加载天气数据:
$ hdfs dfs -put ftp.ncdc.noaa.gov/pub/data/noaa weather/

  1. 启动 Spark shell:
$ spark-shell

  1. 在 RDD 中加载 1901 年的天气数据:
scala> val weatherFileRDD = sc.wholeTextFiles("hdfs://localhost:9000/user/hduser/weather/1901")

  1. 将天气缓存在 RDD 中,以便每次访问时不需要重新计算:
scala> val weatherRDD = weatherFileRDD.cache

注意

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

  1. 计算元素的数量:
scala> weatherRDD.count

  1. 由于整个文件的内容被加载为一个元素,我们需要手动解释数据,因此让我们加载第一个元素:
scala> val firstElement = weatherRDD.first

  1. 读取第一个 RDD 的值:
scala> val firstValue = firstElement._2

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

  • 使用从_1开始的位置函数。

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

  1. 通过行来分割firstValue
scala> val firstVals = firstValue.split("\\n")

  1. 计算firstVals中的元素数量:
scala> firstVals.size

  1. 天气数据的模式非常丰富,文本的位置作为分隔符。您可以在国家气象局网站上获取有关模式的更多信息。让我们获取风速,它来自 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
  1. 将当前目录更改为currency
$ cd currency
  1. 创建na.txt文本文件,并以制表符分隔的键值格式输入货币值(键:国家,值:货币):
$ vi na.txt
United States of America        US Dollar
Canada  Canadian Dollar
Mexico  Peso

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

  1. currency文件夹上传到 HDFS:
$ hdfs dfs -put currency /user/hduser/currency

  1. 启动 Spark shell:
$ spark-shell

  1. 导入语句:
scala> import org.apache.hadoop.io.Text
scala> import org.apache.hadoop.mapreduce.lib.input.KeyValueTextInputFormat

  1. currency目录加载为 RDD:
val currencyFile = sc.newAPIHadoopFile("hdfs://localhost:9000/user/hduser/currency",classOf[KeyValueTextInputFormat],classOf[Text],classOf[Text])

  1. 将其从(Text,Text)元组转换为(String,String)元组:
val currencyRDD = currencyFile.map( t => (t._1.toString,t._2.toString))

  1. 计算 RDD 中的元素数量:
scala> currencyRDD.count

  1. 打印值:
scala> currencyRDD.collect.foreach(println)

如何操作...

注意

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

从 Amazon S3 加载数据

亚马逊简单存储服务S3)为开发人员和 IT 团队提供了一个安全、耐用和可扩展的存储平台。Amazon S3 的最大优势在于没有预先的 IT 投资,公司可以根据需要构建容量(只需点击一个按钮)。

尽管 Amazon S3 可以与任何计算平台一起使用,但它与亚马逊的云服务(如亚马逊弹性计算云EC2)和亚马逊弹性块存储EBS))结合得非常好。因此,使用Amazon Web ServicesAWS)的公司可能已经在 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

  1. 导航至Words | 上传 | 添加文件,并从对话框中选择sh.txt,如下图所示:如何操作...

  2. 单击开始上传

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

  4. AWS_ACCESS_KEYAWS_SECRET_ACCESS_KEY设置为环境变量。

  5. 打开 Spark shell 并从s3中的words目录加载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 非常适合流数据访问,但对于随机访问效果不佳。例如,当你的平均文件大小为 100MB 并且想要读取整个文件时,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的 keyspace:
cqlsh> CREATE KEYSPACE people WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1 };

  1. 在较新版本的 Cassandra 中创建一个列族(从 CQL 3.0 开始,也可以称为person
cqlsh> create columnfamily person(id int primary key,first_name varchar,last_name varchar);

  1. 在列族中插入几条记录:
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');

  1. 将 Cassandra 连接器依赖项添加到 SBT:
"com.datastax.spark" %% "spark-cassandra-connector" % 1.2.0

  1. 您还可以将 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,请参考更多内容...部分。

  1. 现在启动 Spark shell。

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

scala> sc.getConf.set("spark.cassandra.connection.host", "localhost")

  1. 导入特定于 Cassandra 的库:
scala> import com.datastax.spark.connector._

  1. person列族加载为 RDD:
scala> val personRDD = sc.cassandraTable("people","person")

  1. 计算 RDD 中的记录数:
scala> personRDD.count

  1. 打印 RDD 中的数据:
scala> personRDD.collect.foreach(println)

  1. 检索第一行:
scala> val firstRow = personRDD.first

  1. 获取列名:
scala> firstRow.columnNames

  1. Cassandra 也可以通过 Spark SQL 访问。它在SQLContext周围有一个名为CassandraSQLContext的包装器;让我们加载它:
scala> val cc = new org.apache.spark.sql.cassandra.CassandraSQLContext(sc)

  1. person数据加载为SchemaRDD
scala> val p = cc.sql("select * from people.person")

  1. 检索person数据:
scala> p.collect.foreach(println)

更多内容...

Spark Cassandra 的连接器库有很多依赖项。连接器本身和它的一些依赖项是 Spark 的第三方,不作为 Spark 安装的一部分提供。

这些依赖项需要在驱动程序和执行器运行时提供。一种方法是捆绑所有传递依赖项,但这是一个费力且容易出错的过程。推荐的方法是将所有依赖项与连接器库一起捆绑。这将产生一个 fat JAR,通常称为uber JAR。

SBT 提供了sb-assembly插件,可以很容易地创建uber JAR。以下是创建spark-cassandra-connectoruber JAR 的步骤。这些步骤足够通用,可以用来创建任何uber JAR:

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

  1. 将目录更改为uber
$ cd uber

  1. 打开 SBT 提示符:
$ sbt

  1. 给这个项目命名为sc-uber
> set name := "sc-uber"

  1. 保存会话:
> session save

  1. 退出会话:
> exit

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

更多内容...

  1. build.sbt的末尾添加spark-cassandra-driver依赖项,留下一个空行,如下面的屏幕截图所示:
$ vi buid.sbt

更多内容...

  1. 我们将使用MergeStrategy.first作为默认选项。此外,有一些文件,如manifest.mf,每个 JAR 都会捆绑用于元数据,我们可以简单地丢弃它们。我们将使用MergeStrategy.discard。以下是带有assemblyMergeStrategybuild.sbt的屏幕截图:更多内容...

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

addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.12.0")

  1. 我们现在准备构建(装配)一个 JAR:
$ sbt assembly

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

  1. 将其复制到一个适当的位置,您可以在那里保存所有第三方 JAR 文件,例如/home/hduser/thirdparty,并将其重命名为更简单的名称(除非您喜欢更长的名称):
$ mv thirdparty/sc-uber-assembly-0.1-SNAPSHOT.jar  thirdparty/sc-uber.jar

  1. 使用--jars加载uber JAR 启动 Spark shell:
$ spark-shell --jars thirdparty/sc-uber.jar

  1. 要将 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 的数据源。这应该优先于当前的教程,因为结果将作为 DataFrame(将在下一章中介绍)返回,可以很容易地由 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');
)
  1. 插入一些数据:
Insert into person values('Barack','Obama','M');
Insert into person values('Bill','Clinton','M');
Insert into person values('Hillary','Clinton','F');
  1. dev.mysql.com/downloads/connector/j/下载mysql-connector-java-x.x.xx-bin.jar

  2. 使 MySQL 驱动程序可用于 Spark shell 并启动它:

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

注意

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

  1. 创建用户名、密码和 JDBC URL 的变量:
scala> val url="jdbc:mysql://localhost:3306/hadoopdb"
scala> val username = "hduser"
scala> val password = "******"

  1. 导入 JdbcRDD:
scala> import org.apache.spark.rdd.JdbcRDD

  1. 导入与 JDBC 相关的类:
scala> import java.sql.{Connection, DriverManager, ResultSet}

  1. 创建 JDBC 驱动程序的实例:
scala> Class.forName("com.mysql.jdbc.Driver").newInstance

  1. 加载 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"))

  1. 现在查询结果:
scala> myRDD.count
scala> myRDD.foreach(println)

  1. 将 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,并从 HDFS 导出数据。

第四章:Spark SQL

Spark SQL 是用于处理结构化数据的 Spark 模块。本章分为以下几个部分:

  • 了解 Catalyst 优化器

  • 创建 HiveContext

  • 使用案例类推断模式

  • 以编程方式指定模式

  • 使用 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 兼容,因此,HiveContext上面创建了一个新的包装器上下文SQLContext

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

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

Spark SQL 使用了一个名为 DataFrame 的编程抽象。它是一个以命名列组织的分布式数据集合。DataFrame 相当于数据库表,但提供了更精细的优化级别。DataFrame API 还确保了 Spark 在不同语言绑定中的性能是一致的。

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

关于模式的额外信息使得许多优化成为可能,这是其他情况下不可能的。

DataFrame 还可以透明地从各种数据源加载,如 Hive 表、Parquet 文件、JSON 文件和使用 JDBC 的外部数据库。DataFrame 可以被视为一组行对象的 RDD,允许用户调用过程式的 Spark API,如 map。

DataFrame API 在 Spark 1.4 开始提供 Scala、Java、Python 和 R。

用户可以使用领域特定语言DSL)在 DataFrame 上执行关系操作。DataFrame 支持所有常见的关系操作符,它们都使用有限的 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,创建一个逻辑计划(仍未解析)(引用的列可能不存在或类型错误),然后使用 Catalog 对象解析此计划(连接到物理数据源),并创建一个逻辑计划,如下图所示:

分析

逻辑计划优化

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

我想特别注意这里的谓词下推规则。这个概念很简单;如果您在一个地方发出查询来运行对大量数据的查询,这个数据存储在另一个地方,它可能导致大量不必要的数据在网络上移动。

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

逻辑计划优化

物理规划

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

物理规划

代码生成

查询优化的最后阶段涉及生成 Java 字节码以在每台机器上运行。它使用了一种特殊的 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)装配 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

  1. 创建HiveContext的实例:
scala> val hc = new org.apache.spark.sql.hive.HiveContext(sc)

  1. 创建一个 Hive 表Person,其中first_namelast_nameage作为列:
scala>  hc.sql("create table if not exists person(first_name string, last_name string, age int) row format delimited fields terminated by ','")

  1. 在另一个 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

  1. person表中加载数据:
scala> hc.sql("load data local inpath \"/home/hduser/person\" into table person")

  1. 或者,从 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

  1. 使用 HiveQL 选择人员数据:
scala> val persons = hc.sql("from person select first_name,last_name,age")
scala> persons.collect.foreach(println)

  1. select查询的输出创建新表:
scala> hc.sql("create table person2 as select first_name, last_name from person;")

  1. 您还可以直接从一个表复制到另一个表:
scala> hc.sql("create table person2 like person location '/user/hive/warehouse/person'")

  1. 创建两个表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)")

  1. 您还可以使用 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; """)

使用案例类推断模式

案例类是 Scala 中的特殊类,为您提供了构造函数、getter(访问器)、equals 和 hashCode 的样板实现,并实现了Serializable。案例类非常适合封装数据作为对象。熟悉 Java 的读者可以将其与普通旧的 Java 对象POJOs)或 Java bean 相关联。

案例类的美妙之处在于,所有在 Java 中需要的繁重工作都可以在案例类中用一行代码完成。Spark 使用案例类的反射来推断模式。

如何做...

  1. 启动 Spark shell 并为其提供一些额外的内存:
$ spark-shell --driver-memory 1G

  1. 导入隐式转换:
scala> import sqlContext.implicits._

  1. 创建一个Person案例类:
scala> case class Person(first_name:String,last_name:String,age:Int)

  1. 在另一个 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

  1. person目录加载为 RDD:
scala> val p = sc.textFile("hdfs://localhost:9000/user/hduser/person")

  1. 根据逗号将每行拆分为字符串数组,作为分隔符:
val pmap = p.map( line => line.split(","))

  1. 将 Array[String]的 RDD 转换为Person案例对象的 RDD:
scala> val personRDD = pmap.map( p => Person(p(0),p(1),p(2).toInt))

  1. personRDD转换为personDF DataFrame:
scala> val personDF = personRDD.toDF

  1. personDF注册为表:
scala> personDF.registerTempTable("person")

  1. 对其运行 SQL 查询:
scala> val people = sql("select * from person")

  1. persons获取输出值:
scala> people.collect.foreach(println)

以编程方式指定模式

有些情况下案例类可能不起作用;其中之一是案例类不能拥有超过 22 个字段。另一种情况可能是您事先不知道模式。在这种方法中,数据被加载为Row对象的 RDD。模式是使用StructTypeStructField对象分别创建的,它们分别表示表和字段。模式应用于Row RDD 以创建 DataFrame。

如何做...

  1. 启动 Spark shell 并为其提供一些额外的内存:
$ spark-shell --driver-memory 1G

  1. 导入隐式转换:
scala> import sqlContext.implicit._

  1. 导入 Spark SQL 数据类型和Row对象:
scala> import org.apache.spark.sql._
scala> import org.apache.spark.sql.types._

  1. 在另一个 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

  1. 在 RDD 中加载person数据:
scala> val p = sc.textFile("hdfs://localhost:9000/user/hduser/person")

  1. 根据逗号将每行拆分为字符串数组,作为分隔符:
scala> val pmap = p.map( line => line.split(","))

  1. 将 array[string]的 RDD 转换为Row对象的 RDD:
scala> val personData = pmap.map( p => Row(p(0),p(1),p(2).toInt))

  1. 使用StructTypeStructField对象创建模式。StructField对象以参数名、参数类型和可空性的形式接受参数:
scala> val schema = StructType(
 Array(StructField("first_name",StringType,true),
StructField("last_name",StringType,true),
StructField("age",IntegerType,true)
))

  1. 应用模式以创建personDF DataFrame:
scala> val personDF = sqlContext.createDataFrame(personData,schema)

  1. personDF注册为表:
scala> personDF.registerTempTable("person")

  1. 对其运行 SQL 查询:
scala> val persons = sql("select * from person")

  1. 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:这显示了该字段是否可以为 null。

  • 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

  1. person目录上传到 HDFS:
$ hdfs dfs -put person /user/hduser/person

  1. 启动 Spark shell 并为其提供一些额外的内存:
$ spark-shell --driver-memory 1G

  1. 导入隐式转换:
scala> import sqlContext.implicits._

  1. Person创建一个 case 类:
scala> case class Person(firstName: String, lastName: String, age:Int)

  1. 从 HDFS 加载person目录并将其映射到Person case 类:
scala> val personRDD = sc.textFile("hdfs://localhost:9000/user/hduser/person").map(_.split("\t")).map(p => Person(p(0),p(1),p(2).toInt))

  1. personRDD转换为person DataFrame:
scala> val person = personRDD.toDF

  1. person DataFrame 注册为临时表,以便可以对其运行 SQL 查询。请注意,DataFrame 名称不必与表名相同。
scala> person.registerTempTable("person")

  1. 选择所有年龄超过 60 岁的人:
scala> val sixtyPlus = sql("select * from person where age > 60")

  1. 打印值:
scala> sixtyPlus.collect.foreach(println)

  1. 让我们以 Parquet 格式保存这个sixtyPlus RDD:
scala> sixtyPlus.saveAsParquetFile("hdfs://localhost:9000/user/hduser/sp.parquet")

  1. 上一步在 HDFS 根目录中创建了一个名为sp.parquet的目录。您可以在另一个 shell 中运行hdfs dfs -ls命令来确保它已创建:
$ hdfs dfs -ls sp.parquet

  1. 在 Spark shell 中加载 Parquet 文件的内容:
scala> val parquetDF = sqlContext.load("hdfs://localhost:9000/user/hduser/sp.parquet")

  1. 将加载的parquet DF 注册为temp表:
scala> 
parquetDF
.registerTempTable("sixty_plus")

  1. 对上述temp表运行查询:
scala> sql("select * from sixty_plus")

它是如何工作的…

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

年龄
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

  1. 通过从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)

  • null("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 }

  1. jsondata目录上传到 HDFS:
$ hdfs dfs -put jsondata /user/hduser/jsondata

  1. 启动 Spark shell 并为其提供一些额外的内存:
$ spark-shell --driver-memory 1G

  1. 创建SQLContext的实例:
scala> val sqlContext = new org.apache.spark.sql.SQLContext(sc)

  1. 导入隐式转换:
scala> import sqlContext.implicits._

  1. 从 HDFS 加载jsondata目录:
scala> val person = sqlContext.jsonFile("hdfs://localhost:9000/user/hduser/jsondata")

  1. person DF 注册为temp表,以便对其运行 SQL 查询:
scala> person.registerTempTable("person")

  1. 选择所有年龄超过 60 岁的人:
scala> val sixtyPlus = sql("select * from person where age > 60")

  1. 打印值:
scala> sixtyPlus.collect.foreach(println)
  1. 让我们以 JSON 格式保存这个sixtyPlus数据框
scala> sixtyPlus.toJSON.saveAsTextFile("hdfs://localhost:9000/user/hduser/sp")

  1. 上一步在 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. 在 MySQL 中创建名为person的表,使用以下 DDL:
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')
)
  1. 插入一些数据:
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);
  1. dev.mysql.com/downloads/connector/j/下载mysql-connector-java-x.x.xx-bin.jar

  2. 使 MySQL 驱动程序可用于 Spark shell 并启动它:

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

注意

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

  1. 构建 JDBC URL:
scala> val url="jdbc:mysql://localhost:3306/hadoopdb"

  1. 创建一个包含用户名和密码的连接属性对象:
scala> val prop = new java.util.Properties
scala> prop.setProperty("user","hduser")
scala> prop.setProperty("password","********")

  1. 使用 JDBC 数据源加载 DataFrame(url、表名、属性):
 scala> val people = sqlContext.read.jdbc(url,"person",prop)

  1. 通过执行以下命令以漂亮的表格格式显示结果:
scala> people.show

  1. 这已经加载了整个表。如果我只想加载男性(url、表名、谓词、属性)怎么办?要做到这一点,请运行以下命令:
scala> val males = sqlContext.read.jdbc(url,"person",Array("gender='M'"),prop)
scala> males.show

  1. 通过执行以下命令只显示名字:
scala> val first_names = people.select("first_name")
scala> first_names.show

  1. 通过执行以下命令只显示年龄低于 60 岁的人:
scala> val below60 = people.filter(people("age") < 60)
scala> below60.show

  1. 按性别对人进行分组:
scala> val grouped = people.groupBy("gender")

  1. 通过执行以下命令找到男性和女性的数量:
scala> val gender_count = grouped.count
scala> gender_count.show

  1. 通过执行以下命令找到男性和女性的平均年龄:
scala> val avg_age = grouped.avg("age")
scala> avg_age.show

  1. 现在,如果您想将这个avg_age数据保存到一个新表中,请运行以下命令:
scala> gender_count.write.jdbc(url,"gender_count",prop)

  1. 将 people DataFrame 以 Parquet 格式保存:
scala> people.write.parquet("people.parquet")

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

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

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

在本教程中,我们将介绍从任意数据源加载和保存数据。

如何做到这一点...

  1. 启动 Spark shell 并为其提供一些额外的内存:
$ spark-shell --driver-memory 1G

  1. 从 Parquet 加载数据;由于parquet是默认数据源,您不必指定它:
scala> val people = sqlContext.read.load("hdfs://localhost:9000/user/hduser/people.parquet") 

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

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

注意

在写入数据时,有四种保存模式:appendoverwriteerrorIfExistsignoreappend模式将数据添加到数据源,overwrite将其覆盖,errorIfExists在数据已经存在时抛出异常,ignore在数据已经存在时不执行任何操作。

  1. 将 people 保存为 JSON 格式,使用append模式:
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。

Introduction

批处理间隔对于优化流式应用程序非常重要。理想情况下,您希望至少以数据获取的速度进行处理;否则,您的应用程序将产生积压。Spark Streaming 在一个批处理间隔的持续时间内收集数据,比如 2 秒。一旦这个 2 秒的间隔结束,该间隔内收集的数据将被交给 Spark 进行处理,而流式处理将专注于收集下一个批处理间隔的数据。现在,这个 2 秒的批处理间隔是 Spark 处理数据的全部时间,因为它应该空闲以接收下一个批处理的数据。如果 Spark 能够更快地处理数据,您可以将批处理间隔减少到 1 秒。如果 Spark 无法跟上这个速度,您必须增加批处理间隔。

Spark Streaming 中的 RDD 的连续流需要以一种抽象的形式表示,通过这种抽象可以对其进行处理。这种抽象称为离散流DStream)。对 DStream 应用的任何操作都会导致对底层 RDD 的操作。

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

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

  • 高级来源,如 Kafka 和 Flume

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

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

  • 滑动间隔:这表示您希望多频繁执行操作。比如您希望每 10 秒执行一次操作;这意味着每 10 秒,窗口的 1 分钟将有 50 秒的数据与上一个窗口相同,以及 10 秒的新数据。

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

DStream 还具有输出操作,允许将数据推送到外部系统。它们类似于 RDD 上的操作(在 DStream 上发生的抽象级别更高)。

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

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

使用流媒体进行单词计数

让我们从一个简单的流媒体示例开始,在其中一个终端中,我们将输入一些文本,流媒体应用程序将在另一个窗口中捕获它。

如何做...

  1. 启动 Spark shell 并为其提供一些额外的内存:
$ spark-shell --driver-memory 1G

  1. 流特定的导入:
scala> import org.apache.spark.SparkConf
scala> import org.apache.spark.streaming.{Seconds, StreamingContext}
scala> import org.apache.spark.storage.StorageLevel
scala> import StorageLevel._

  1. 隐式转换的导入:
scala> import org.apache.spark._
scala> import org.apache.spark.streaming._
scala> import org.apache.spark.streaming.StreamingContext._

  1. 使用 2 秒批处理间隔创建StreamingContext
scala> val ssc = new StreamingContext(sc, Seconds(2))

  1. 在本地主机上使用端口8585创建一个SocketTextStream Dstream,并使用MEMORY_ONLY缓存:
scala> val lines = ssc.socketTextStream("localhost",8585,MEMORY_ONLY)

  1. 将行分成多个单词:
scala> val wordsFlatMap = lines.flatMap(_.split(" "))

  1. 将单词转换为(单词,1),即将1作为单词的每次出现的值输出为键:
scala> val wordsMap = wordsFlatMap.map( w => (w,1))

  1. 使用reduceByKey方法为每个单词的出现次数添加一个数字作为键(该函数一次处理两个连续的值,由ab表示):
scala> val wordCount = wordsMap.reduceByKey( (a,b) => (a+b))

  1. 打印wordCount
scala> wordCount.print

  1. 启动StreamingContext;记住,直到启动StreamingContext之前什么都不会发生:
scala> ssc.start

  1. 现在,在一个单独的窗口中,启动 netcat 服务器:
$ nc -lk 8585

  1. 输入不同的行,例如to be or not to be
to be or not to be

  1. 检查 Spark shell,您将看到类似以下截图的单词计数结果:如何做...

流媒体 Twitter 数据

Twitter 是一个著名的微博平台。它每天产生大量数据,大约有 5 亿条推文。Twitter 允许通过 API 访问其数据,这使其成为测试任何大数据流应用程序的典范。

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

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

如何做...

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

  2. 转到apps.twitter.com

  3. 点击创建新应用

  4. 输入名称描述网站回调 URL,然后点击创建您的 Twitter 应用程序如何做...

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

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

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

消费者密钥(API 密钥)

消费者密钥(API 密钥)

访问令牌

访问令牌密钥

  1. 我们将需要在一段时间内在这个屏幕上提供这些值,但是,现在,让我们从 Maven 中央库下载所需的第三方库:
$ 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

  1. 打开 Spark shell,提供前面三个 JAR 作为依赖项:
$ spark-shell --jars spark-streaming-twitter_2.10-1.2.0.jar, twitter4j-stream-4.0.2.jar,twitter4j-core-4.0.2.jar

  1. 执行特定于 Twitter 的导入:
scala> import org.apache.spark.streaming.twitter._
scala> import twitter4j.auth._
scala> import twitter4j.conf._

  1. 流特定的导入:
scala> import org.apache.spark.streaming.{Seconds, StreamingContext}

  1. 导入隐式转换:
scala> import org.apache.spark._
scala> import org.apache.spark.streaming._
scala> import org.apache.spark.streaming.StreamingContext._

  1. 使用 10 秒批处理间隔创建StreamingContext
scala> val ssc = new StreamingContext(sc, Seconds(10))

  1. 使用 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)

注意

这些是示例值,您应该放入自己的值。

  1. 创建 Twitter DStream:
scala> val tweets = TwitterUtils.createStream(ssc,auth)

  1. 过滤掉英文推文:
scala> val englishTweets = tweets.filter(_.getLang()=="en")

  1. 从推文中获取文本:
scala> val status = englishTweets.map(status => status.getText)

  1. 设置检查点目录:
scala> ssc.checkpoint("hdfs://localhost:9000/user/hduser/checkpoint")

  1. 启动StreamingContext
scala> ssc.start
scala> ssc.awaitTermination

  1. 您可以使用: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 中,消息的保留不取决于消费者的消费。消息将保留一段可配置的时间。每个消费者可以以任何他们喜欢的顺序读取消息。它需要保留的只是一个偏移量。另一个类比可以是阅读一本书,其中页码类似于偏移量,而页内容类似于消息。只要他们记住书签(当前偏移量),读者可以以任何方式阅读。

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

在极端情况下,当所有消费者都在一个消费者组中时,Kafka 集群的行为类似于 PTP/队列。在另一个极端情况下,如果每个消费者都属于不同的组,它的行为类似于发布/订阅。在实践中,每个消费者组有一定数量的消费者。

使用 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

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

  1. 创建一个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

  1. 启动 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

  1. 流特定导入:
scala> import org.apache.spark.streaming.{Seconds, StreamingContext}

  1. 隐式转换导入:
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

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

  1. 设置 Kafka 特定变量:
scala> val zkQuorum = "localhost:2181"
scala> val group = "test-group"
scala> val topics = "test"
scala> val numThreads = 1

  1. 创建topicMap
scala> val topicMap = topics.split(",").map((_,numThreads.toInt)).toMap

  1. 创建 Kafka DStream:
scala> val lineMap = KafkaUtils.createStream(ssc, zkQuorum, group, topicMap)

  1. 从 lineMap 中取出值:
scala> val lines = lineMap.map(_._2)

  1. 创建值的flatMap
scala> val words = lines.flatMap(_.split(" "))

  1. 创建(单词,出现次数)的键值对:
scala> val pair = words.map( x => (x,1))

  1. 对滑动窗口进行单词计数:
scala> val wordCounts = pair.reduceByKeyAndWindow(_ + _, _ - _, Minutes(10), Seconds(2), 2)
scala> wordCounts.print

  1. 设置checkpoint目录:
scala> ssc.checkpoint("hdfs://localhost:9000/user/hduser/checkpoint")

  1. 启动StreamingContext
scala> ssc.start
scala> ssc.awaitTermination

  1. 在另一个窗口的 Kafka 中的test主题上发布一条消息:
$ /opt/infoobjects/kafka/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test

  1. 现在,通过在第 15 步按Enter并在每条消息后按Enter来在 Kafka 上发布消息。

  2. 现在,当您在 Kafka 上发布消息时,您将在 Spark shell 中看到它们:如何做...

还有更多...

假设您想要维护每个单词出现次数的运行计数。Spark Streaming 具有名为updateStateByKey操作的功能。updateStateByKey操作允许您在更新时维护任意状态并使用新提供的信息进行更新。

这种任意状态可以是聚合值,也可以是状态的改变(比如 Twitter 用户的心情)。执行以下步骤:

  1. 让我们在对 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 步的每次调用中,通过将当前值的总和添加到先前状态来更新先前状态。

  1. 打印结果:
scala> runningCounts.print

  1. 以下是使用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

  1. 按下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

  1. 显式导入 MLlib 向量(不要与其他向量类混淆):
Scala> import org.apache.spark.mllib.linalg.{Vectors,Vector}

  1. 创建密集向量:
scala> val dvPerson = Vectors.dense(160.0,69.0,24.0)

  1. 创建稀疏向量:
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

  1. 显式导入 MLlib 向量:
scala> import org.apache.spark.mllib.linalg.{Vectors,Vector}

  1. 导入LabeledPoint
scala> import org.apache.spark.mllib.regression.LabeledPoint

  1. 使用正标签和密集向量创建一个带标签的点:
scala> val willBuySUV = LabeledPoint(1.0,Vectors.dense(300.0,80,40))

  1. 使用负标签和密集向量创建一个带标签的点:
scala> val willNotBuySUV = LabeledPoint(0.0,Vectors.dense(150.0,60,25))

  1. 使用正标签和稀疏向量创建一个带标签的点:
scala> val willBuySUV = LabeledPoint(1.0,Vectors.sparse(3,Array(0,1,2),Array(300.0,80,40)))

  1. 使用负标签和稀疏向量创建一个带标签的点:
scala> val willNotBuySUV = LabeledPoint(0.0,Vectors.sparse(3,Array(0,1,2),Array(150.0,60,25)))

  1. 创建一个包含相同数据的libsvm文件:
$vi person_libsvm.txt (libsvm indices start with 1)
0  1:150 2:60 3:25
1  1:300 2:80 3:40

  1. person_libsvm.txt上传到hdfs
$ hdfs dfs -put person_libsvm.txt person_libsvm.txt

  1. 做更多的导入:
scala> import org.apache.spark.mllib.util.MLUtils
scala> import org.apache.spark.rdd.RDD

  1. libsvm文件加载数据:
scala> val persons = MLUtils.loadLibSVMFile(sc,"person_libsvm.txt")

创建矩阵

矩阵只是一个表示多个特征向量的表。可以存储在一台机器上的矩阵称为本地矩阵,可以分布在集群中的矩阵称为分布式矩阵

本地矩阵具有基于整数的索引,而分布式矩阵具有基于长整数的索引。两者的值都是双精度。

有三种类型的分布式矩阵:

  • RowMatrix:每行都是一个特征向量。

  • IndexedRowMatrix:这也有行索引。

  • CoordinateMatrix:这只是一个MatrixEntry的矩阵。MatrixEntry表示矩阵中的一个条目,由其行和列索引表示。

如何做…

  1. 启动 Spark shell:
$spark-shell

  1. 导入与矩阵相关的类:
scala> import org.apache.spark.mllib.linalg.{Vectors,Matrix, Matrices}

  1. 创建一个密集的本地矩阵:
scala> val people = Matrices.dense(3,2,Array(150d,60d,25d, 300d,80d,40d))

  1. 创建一个personRDD作为向量的 RDD:
scala> val personRDD = sc.parallelize(List(Vectors.dense(150,60,25), Vectors.dense(300,80,40)))

  1. 导入RowMatrix和相关类:
scala> import org.apache.spark.mllib.linalg.distributed.{IndexedRow, IndexedRowMatrix,RowMatrix, CoordinateMatrix, MatrixEntry}

  1. 创建一个personRDD的行矩阵:
scala> val personMat = new RowMatrix(personRDD)

  1. 打印行数:
scala> print(personMat.numRows)

  1. 打印列数:
scala> print(personMat.numCols)

  1. 创建一个索引行的 RDD:
scala> val personRDD = sc.parallelize(List(IndexedRow(0L, Vectors.dense(150,60,25)), IndexedRow(1L, Vectors.dense(300,80,40))))

  1. 创建一个索引行矩阵:
scala> val pirmat = new IndexedRowMatrix(personRDD)

  1. 打印行数:
scala> print(pirmat.numRows)

  1. 打印列数:
scala> print(pirmat.numCols)

  1. 将索引行矩阵转换回行矩阵:
scala> val personMat = pirmat.toRowMatrix

  1. 创建一个矩阵条目的 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)
))

  1. 创建一个坐标矩阵:
scala> val pcmat = new CoordinateMatrix(meRDD)

  1. 打印行数:
scala> print(pcmat.numRows)

  1. 打印列数:
scala> print(pcmat.numCols)

计算摘要统计

汇总统计用于总结观察结果,以获得对数据的整体感觉。摘要包括以下内容:

  • 数据的中心趋势-均值、众数、中位数

  • 数据的分布-方差、标准差

  • 边界条件-最小值、最大值

这个示例介绍了如何生成摘要统计信息。

如何做…

  1. 启动 Spark shell:
$ spark-shell

  1. 导入与矩阵相关的类:
scala> import org.apache.spark.mllib.linalg.{Vectors,Vector}
scala> import org.apache.spark.mllib.stat.Statistics

  1. 创建一个personRDD作为向量的 RDD:
scala> val personRDD = sc.parallelize(List(Vectors.dense(150,60,25), Vectors.dense(300,80,40)))

  1. 计算列的摘要统计:
scala> val summary = Statistics.colStats(personRDD)

  1. 打印这个摘要的均值:
scala> print(summary.mean)

  1. 打印方差:
scala> print(summary.variance)

  1. 打印每列中非零值的数量:
scala> print(summary.numNonzeros)

  1. 打印样本大小:
scala> print(summary.count)

  1. 打印每列的最大值:
scala> print(summary.max)

计算相关性

相关性是两个变量之间的统计关系,当一个变量改变时,会导致另一个变量的改变。相关性分析衡量了这两个变量相关的程度。

如果一个变量的增加导致另一个变量的增加,这被称为正相关。如果一个变量的增加导致另一个变量的减少,这是负相关

Spark 支持两种相关算法:Pearson 和 Spearman。Pearson 算法适用于两个连续变量,例如一个人的身高和体重或房屋大小和房价。Spearman 处理一个连续和一个分类变量,例如邮政编码和房价。

准备就绪

让我们使用一些真实数据,这样我们可以更有意义地计算相关性。以下是 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

  1. 导入统计和相关类:
scala> import org.apache.spark.mllib.linalg._
scala> import org.apache.spark.mllib.stat.Statistics

  1. 创建一个房屋面积的 RDD:
scala> val sizes = sc.parallelize(List(2100, 2300, 2046, 4314, 1244, 4608, 2173, 2750, 4010, 1959.0))

  1. 创建一个房价的 RDD:
scala> val prices = sc.parallelize(List(1620000 , 1690000, 1400000, 2000000, 1060000, 3830000, 1230000, 2400000, 3380000, 1480000.00))

  1. 计算相关性:
scala> val correlation = Statistics.corr(sizes,prices)
correlation: Double = 0.8577177736252577 

0.85 表示非常强的正相关性。

由于这里没有特定的算法,所以默认是 Pearson。corr方法被重载以将算法名称作为第三个参数。

  1. 用 Pearson 计算相关性:
scala> val correlation = Statistics.corr(sizes,prices)

  1. 用 Spearman 计算相关性:
scala> val correlation = Statistics.corr(sizes,prices,"spearman")

在前面的例子中,两个变量都是连续的,所以 Spearman 假设大小是离散的。Spearman 使用的更好的例子是邮政编码与价格。

进行假设检验

假设检验是确定给定假设为真的概率的一种方法。假设一个样本数据表明女性更倾向于投票给民主党。这可能对更大的人口来说是真的,也可能不是。如果这个模式只是样本数据中的偶然现象呢?

观察假设检验目标的另一种方式是回答这个问题:如果一个样本中有一个模式,那么这个模式存在的机会是多少?

我们怎么做?有一句话说,证明某事最好的方法是试图证伪它。

要证伪的假设被称为零假设。假设检验适用于分类数据。让我们看一个党派倾向的民意调查的例子。

党派 男性 女性
民主党 32 41
共和党 28 25
独立 34 26

如何做…

  1. 启动 Spark shell:
$ spark-shell

  1. 导入相关的类:
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}

  1. 为民主党创建一个向量:
scala> val dems = Vectors.dense(32.0,41.0)

  1. 为共和党创建一个向量:
scala> val reps= Vectors.dense(28.0,25.0)

  1. 为独立党创建一个向量:
scala> val indies = Vectors.dense(34.0,26.0)

  1. 对观察数据进行卡方拟合度检验:
scala> val dfit = Statistics.chiSqTest(dems)
scala> val rfit = Statistics.chiSqTest(reps)
scala> val ifit = Statistics.chiSqTest(indies)

  1. 打印拟合度检验结果:
scala> print(dfit)
scala> print(rfit)
scala> print(ifit)

  1. 创建输入矩阵:
scala> val mat = Matrices.dense(2,3,Array(32.0,41.0, 28.0,25.0, 34.0,26.0))

  1. 进行卡方独立性检验:
scala> val in = Statistics.chiSqTest(mat)

  1. 打印独立性检验结果:
scala> print(in)

使用 ML 创建机器学习管道

Spark ML 是 Spark 中构建机器学习管道的新库。这个库正在与 MLlib 一起开发。它有助于将多个机器学习算法组合成一个单一的管道,并使用 DataFrame 作为数据集。

准备就绪

让我们首先了解一些 Spark ML 中的基本概念。它使用转换器将一个 DataFrame 转换为另一个 DataFrame。简单转换的一个例子可以是追加列。你可以把它看作是关系世界中的"alter table"的等价物。

另一方面,估计器代表一个机器学习算法,它从数据中学习。估计器的输入是一个 DataFrame,输出是一个转换器。每个估计器都有一个fit()方法,它的工作是训练算法。

机器学习管道被定义为一系列阶段;每个阶段可以是估计器或者转换器。

我们在这个示例中要使用的例子是某人是否是篮球运动员。为此,我们将有一个估计器和一个转换器的管道。

估计器获取训练数据来训练算法,然后转换器进行预测。

暂时假设LogisticRegression是我们正在使用的机器学习算法。我们将在随后的章节中解释LogisticRegression的细节以及其他算法。

如何做…

  1. 启动 Spark shell:
$ spark-shell

  1. 进行导入:
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

  1. 为篮球运动员 Lebron 创建一个标记点,身高 80 英寸,体重 250 磅:
scala> val lebron = LabeledPoint(1.0,Vectors.dense(80.0,250.0))

  1. 为不是篮球运动员的 Tim 创建一个标记点,身高 70 英寸,体重 150 磅:
scala> val tim = LabeledPoint(0.0,Vectors.dense(70.0,150.0))

  1. 为篮球运动员 Brittany 创建一个标记点,身高 80 英寸,体重 207 磅:
scala> val brittany = LabeledPoint(1.0,Vectors.dense(80.0,207.0))

  1. 为不是篮球运动员的 Stacey 创建一个标记点,身高 65 英寸,体重 120 磅:
scala> val stacey = LabeledPoint(0.0,Vectors.dense(65.0,120.0))

  1. 创建一个训练 RDD:
scala> val trainingRDD = sc.parallelize(List(lebron,tim,brittany,stacey))

  1. 创建一个训练 DataFrame:
scala> val trainingDF = trainingRDD.toDF

  1. 创建一个LogisticRegression估计器:
scala> val estimator = new LogisticRegression

  1. 通过拟合训练 DataFrame 来创建一个转换器:
scala> val transformer = estimator.fit(trainingDF)

  1. 现在,让我们创建一个测试数据—John 身高 90 英寸,体重 270 磅,是篮球运动员:
scala> val john = Vectors.dense(90.0,270.0)

  1. 创建另一个测试数据—Tom 身高 62 英寸,体重 150 磅,不是篮球运动员:
scala> val tom = Vectors.dense(62.0,120.0)

  1. 创建一个训练 RDD:
scala> val testRDD = sc.parallelize(List(john,tom))

  1. 创建一个Features case 类:
scala> case class Feature(v:Vector)

  1. testRDD映射到Features的 RDD:
scala> val featuresRDD = testRDD.map( v => Feature(v))

  1. featuresRDD转换为具有列名"features"的 DataFrame:
scala> val featuresDF = featuresRDD.toDF("features")

  1. 通过向其添加predictions列来转换featuresDF
scala> val predictionsDF = transformer.transform(featuresDF)

  1. 打印predictionsDF
scala> predictionsDF.foreach(println)

  1. PredictionDF,如您所见,除了保留特征之外,还创建了三列—rawPredictionprobabilityprediction。让我们只选择featuresprediction
scala> val shorterPredictionsDF = predictionsDF.select("features","prediction")

  1. 将预测重命名为isBasketBallPlayer
scala> val playerDF = shorterPredictionsDF.toDF("features","isBasketBallPlayer")

  1. 打印playerDF的模式:
scala> playerDF.printSchema

第七章:使用 MLlib 进行监督学习 - 回归

本章分为以下几个部分:

  • 使用线性回归

  • 理解成本函数

  • 使用套索进行线性回归

  • 进行岭回归

介绍

以下是维基百科对监督学习的定义:

“监督学习是从标记的训练数据中推断函数的机器学习任务。”

监督学习有两个步骤:

  • 使用训练数据集训练算法;这就像是先提出问题和它们的答案

  • 使用测试数据集向训练好的算法提出另一组问题。

有两种监督学习算法:

  • 回归:这预测连续值输出,比如房价。

  • 分类:这预测离散值输出(0 或 1)称为标签,比如一封电子邮件是否是垃圾邮件。分类不仅限于两个值;它可以有多个值,比如标记一封电子邮件为重要、不重要、紧急等等(0, 1, 2…)。

注意

本章将介绍回归,下一章将介绍分类。

作为回归的示例数据集,我们将使用加利福尼亚州萨拉托加市最近售出的房屋数据作为训练集来训练算法。一旦算法训练好了,我们将要求它根据房屋的尺寸来预测房价。下图说明了工作流程:

介绍

这里的假设,对于它的作用来说,可能听起来像一个误称,你可能会认为预测函数可能是一个更好的名字,但是假设这个词是出于历史原因而使用的。

如果我们只使用一个特征来预测结果,就称为双变量分析。当我们有多个特征时,就称为多变量分析。事实上,我们可以有任意多个特征。其中一种算法,支持向量机SVM),我们将在下一章中介绍,实际上允许你拥有无限数量的特征。

本章将介绍如何使用 MLlib,Spark 的机器学习库进行监督学习。

注意

数学解释已尽可能简单地提供,但你可以随意跳过数学,直接转到如何做……部分。

使用线性回归

线性回归是一种基于一个或多个预测变量或特征x来建模响应变量y值的方法。

准备工作

让我们使用一些房屋数据来预测房屋的价格,基于它的大小。以下是 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

  1. 导入统计和相关类:
scala> import org.apache.spark.mllib.linalg.Vectors
scala> import org.apache.spark.mllib.regression.LabeledPoint
scala> import org.apache.spark.mllib.regression.LinearRegressionWithSGD

  1. 创建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))
)

  1. 创建上述数据的 RDD:
scala> val pricesRDD = sc.parallelize(points)

  1. 使用这些数据训练模型,进行 100 次迭代。这里,步长被保持得很小,以适应响应变量的非常大的值,也就是房价。第四个参数是每次迭代使用的数据集的一部分,最后一个参数是要使用的初始权重集(不同特征的权重):
scala> val model = LinearRegressionWithSGD.train(pricesRDD,100,0.0000006,1.0,Vectors.zeros(1))

  1. 预测一个 2500 平方英尺的房屋的价格:
scala> val prediction = model.predict(Vectors.dense(2500))

房屋大小只是一个预测变量。房价取决于其他变量,比如地块大小,房屋年龄等等。你拥有的变量越多,你的预测就会越准确。

理解成本函数

成本函数或损失函数在机器学习算法中非常重要。大多数算法都有某种形式的成本函数,目标是最小化它。影响成本函数的参数,比如上一个步骤中的stepSize,需要手动设置。因此,理解成本函数的整个概念非常重要。

在这个步骤中,我们将分析线性回归的成本函数。线性回归是一个简单的算法,可以帮助读者理解成本函数对于复杂算法的作用。

让我们回到线性回归。目标是找到最佳拟合线,使得误差的均方最小。这里,我们将误差定义为最佳拟合线的值与训练数据集中响应变量的实际值之间的差异。

对于单个自变量的简单情况,最佳拟合线可以写成:

理解成本函数

这个函数也被称为假设函数,可以写成:

理解成本函数

线性回归的目标是找到最佳拟合线。在这条线上,θ[0]代表y轴上的截距,θ[1]代表线的斜率,如下方程所示:

理解成本函数

我们必须选择θ[0]和θ[1],使得h(x)对于训练数据集中的y最接近。因此,对于第i个数据点,线与数据点之间的距离的平方为:

理解成本函数

换句话说,这是预测房价与房屋实际售价之间的差的平方。现在,让我们计算训练数据集中这个值的平均值:

理解成本函数

上述方程被称为线性回归的成本函数J。目标是最小化这个成本函数。

理解成本函数

这个成本函数也被称为平方误差函数。如果它们分别针对J绘制,θ[0]和θ[1]都会遵循凸曲线。

让我们举一个非常简单的数据集的例子,包括三个值,(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。以下将是成本函数的值:

理解成本函数

如您在两个图中所见,当斜率或曲线的梯度为 0 时,J的最小值是。

理解成本函数

当θ[0]和θ[1]都映射到 3D 空间时,它就像一个碗的形状,成本函数的最小值在其底部。

到达最小值的这种方法称为梯度下降。在 Spark 中,实现是随机梯度下降。

使用套索进行线性回归

套索是线性回归的收缩和选择方法。它最小化了通常的平方误差和系数绝对值之和的边界。它基于原始套索论文,可在statweb.stanford.edu/~tibs/lasso/lasso.pdf找到。

我们在上一个示例中使用的最小二乘法也称为普通最小二乘法OLS)。OLS 有两个挑战:

  • 预测准确性:使用 OLS 进行的预测通常具有较低的预测偏差和较高的方差。通过缩小一些系数(甚至使它们为零),可以提高预测准确性。偏差会有所增加,但整体预测准确性会提高。

  • 解释:对于预测变量的数量较多,希望找到其中表现最强的子集(相关性)。

注意

偏差与方差

预测误差背后有两个主要原因:偏差和方差。理解偏差和方差的最佳方法是看一个情况,我们在同一数据集上多次进行预测。

偏差是预测结果与实际值之间的估计差距,方差是不同预测值之间的差异的估计。

通常,添加更多的特征有助于减少偏差,这是很容易理解的。如果在构建预测模型时,我们遗漏了一些具有显著相关性的特征,这将导致显著的误差。

如果您的模型方差很高,可以删除特征以减少它。更大的数据集也有助于减少方差。

在这里,我们将使用一个简单的数据集,这是一个不适当的数据集。不适当的数据集是指样本数据量小于预测变量的数量。

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。我们将使用这个数据集和套索算法来验证其有效性。

如何做…

  1. 启动 Spark shell:
$ spark-shell

  1. 导入统计和相关类:
scala> import org.apache.spark.mllib.linalg.Vectors
scala> import org.apache.spark.mllib.regression.LabeledPoint
scala> import org.apache.spark.mllib.regression.LassoWithSGD

  1. 创建带有房价作为标签的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))
)

  1. 创建一个 RDD 的前述数据:
scala> val rdd = sc.parallelize(points)

  1. 使用这些数据训练一个模型,使用 100 次迭代。在这里,步长和正则化参数已经手动设置:
scala> val model = LassoWithSGD.train(rdd,100,0.02,2.0)

  1. 检查有多少预测变量的系数被设置为零:
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]

如您所见,九个预测变量中有六个的系数被设置为零。这是套索的主要特征:它认为不实用的任何预测变量,通过将它们的系数设置为零,从方程中移除它们。

进行岭回归

改进预测质量的套索的另一种方法是岭回归。在套索中,许多特征的系数被设置为零,因此从方程中消除,在岭回归中,预测变量或特征受到惩罚,但永远不会被设置为零。

如何做…

  1. 启动 Spark shell:
$ spark-shell

  1. 导入统计和相关类:
scala> import org.apache.spark.mllib.linalg.Vectors
scala> import org.apache.spark.mllib.regression.LabeledPoint
scala> import org.apache.spark.mllib.regression.RidgeRegressionWithSGD

  1. 创建带有房价作为标签的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))
)

  1. 创建一个包含上述数据的 RDD:
scala> val rdd = sc.parallelize(points)

  1. 使用这些数据训练一个模型,进行 100 次迭代。在这里,步长和正则化参数已经手动设置:
scala> val model = RidgeRegressionWithSGD.train(rdd,100,0.02,2.0)

  1. 检查有多少预测变量的系数被设为零:
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]

如您所见,与套索不同,岭回归不会将任何预测变量的系数设为零,但它确实使一些系数非常接近于零。

第八章:监督学习与 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 否则。如果我们再次看一下 S 形函数图,我们会意识到,当 使用逻辑回归进行分类 S 形函数是 使用逻辑回归进行分类,也就是说,对于 t 的正值,它将预测为正类:

自从使用逻辑回归进行分类,这意味着对于使用逻辑回归进行分类的情况下,将会预测正类。为了更好地说明这一点,让我们将其扩展到双变量情况的非矩阵形式:

使用逻辑回归进行分类

由方程使用逻辑回归进行分类表示的平面将决定给定向量属于正类还是负类。这条线被称为决策边界。

这个边界不一定是线性的,取决于训练集。如果训练数据不能在线性边界上分离,可以添加更高级别的多项式特征来促进它。一个例子是通过平方 x1 和 x2 来添加两个新特征,如下所示:

使用逻辑回归进行分类

请注意,对于学习算法来说,这种增强与以下方程式完全相同:

使用逻辑回归进行分类

学习算法将把多项式的引入视为另一个特征。这给了你在拟合过程中很大的权力。这意味着通过正确选择多项式和参数,可以创建任何复杂的决策边界。

让我们花一些时间来理解如何选择参数的正确值,就像我们在线性回归的情况下所做的那样。线性回归的成本函数J是:

使用逻辑回归进行分类

正如你所知,我们在这个成本函数中对成本进行了平均。让我们用成本项来表示这一点:

使用逻辑回归进行分类

换句话说,成本项是算法在预测h(x)的真实响应变量值y时必须支付的成本:

使用逻辑回归进行分类

这个成本对于线性回归来说效果很好,但是对于逻辑回归来说,这个成本函数是非凸的(也就是说,它会导致多个局部最小值),我们需要找到一个更好的凸方式来估计成本。

逻辑回归中效果很好的成本函数如下:

使用逻辑回归进行分类

让我们通过结合这两个成本函数将它们合并成一个:

使用逻辑回归进行分类

让我们将这个成本函数重新放回到J中:

使用逻辑回归进行分类

目标是最小化成本,也就是最小化使用逻辑回归进行分类的值。这是通过梯度下降算法来实现的。Spark 有两个支持逻辑回归的类:

  • LogisticRegressionWithSGD

  • LogisticRegressionWithLBFGS

LogisticRegressionWithLBFGS类更受欢迎,因为它消除了优化步长的步骤。

准备工作

2006 年,铃木、鹤崎和光岡在日本不同海滩上对一种濒临灭绝的穴居蜘蛛的分布进行了一些研究。

让我们看一些关于颗粒大小和蜘蛛存在的数据:

颗粒大小(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

  1. 导入统计和相关类:
scala> import org.apache.spark.mllib.linalg.Vectors
scala> import org.apache.spark.mllib.regression.LabeledPoint
scala> import org.apache.spark.mllib.classification.LogisticRegressionWithLBFGS

  1. 创建一个带有蜘蛛存在或不存在的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)))

  1. 创建前述数据的 RDD:
scala> val spiderRDD = sc.parallelize(points)

  1. 使用这些数据训练模型(当所有预测因子为零时,截距是该值):
scala> val lr = new LogisticRegressionWithLBFGS().setIntercept(true)
scala> val model = lr.run(spiderRDD)

  1. 预测粒度为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

  1. 启动 Spark shell:
$ spark-shell

  1. 执行所需的导入:
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

  1. 将数据加载为 RDD:
scala> val svmData = MLUtils.loadLibSVMFile(sc,"sample_libsvm_data.txt")

  1. 记录的数量:
scala> svmData.count

  1. 现在让我们将数据集分成一半训练数据和一半测试数据:
scala> val trainingAndTest = svmData.randomSplit(Array(0.5,0.5))

  1. 分配trainingtest数据:
scala> val trainingData = trainingAndTest(0)
scala> val testData = trainingAndTest(1)

  1. 训练算法并构建模型进行 100 次迭代(您可以尝试不同的迭代次数,但您会发现,在某个时候,结果开始收敛,这是一个不错的选择):
scala> val model = SVMWithSGD.train(trainingData,100)

  1. 现在我们可以使用这个模型来预测任何数据集的标签。让我们预测测试数据中第一个点的标签:
scala> val label = model.predict(testData.first.features)

  1. 让我们创建一个元组,第一个值是测试数据的预测值,第二个值是实际标签,这将帮助我们计算算法的准确性:
scala> val predictionsAndLabels = testData.map( r => (model.predict(r.features),r.label))

  1. 您可以计算有多少记录预测和实际标签不匹配:
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

  1. 执行所需的导入:
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

  1. 加载文件:
scala> val data = sc.textFile("tennis.csv")
  1. 解析数据并将其加载到LabeledPoint中:
scala> val parsedData = data.map {
line =>  val parts = line.split(',').map(_.toDouble)
 LabeledPoint(parts(0), Vectors.dense(parts.tail)) }

  1. 用这些数据训练算法:
scala> val model = DecisionTree.train(parsedData, Classification, Entropy, 3)

  1. 为无雨、大风和凉爽的温度创建一个向量:
scala> val v=Vectors.dense(0.0,1.0,0.0)

  1. 预测是否应该打网球:
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 使用三种方法来确定不纯度:

  • 基尼不纯度(分类)

  • 熵(分类)

  • 方差(回归)

信息增益是父节点杂质与两个子节点杂质的加权和之差。让我们看一下第一个分裂,将大小为 8 的数据分成大小为 3(左)和 5(右)的两个数据集。让我们称第一个分裂为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 个特征。

在这种情况下,为了给故事增添趣味,我们不会提供特征,而是会问不同的人为什么他们认为一个人信用好或坏。现在根据定义,不同的人暴露于一个人的不同特征(有时是重叠的),这给了我们与随机选择特征相同的功能。

我们的第一个例子是 Jack,他被贴上了“坏信用”的标签。我们将从 Jack 最喜欢的酒吧——大象酒吧的 Joey 开始。一个人能够推断为什么给定一个标签的唯一方法是通过问是/否的问题。让我们看看 Joey 说了什么:

Q1: Jack 是否慷慨地给小费?(特征:慷慨)

A: 不

Q2:杰克每次至少花 60 美元吗?(特征:挥霍)

A:是的

Q3:他是否倾向于在最小的挑衅下卷入酒吧斗殴?(特征:易怒)

A:是的

这就解释了为什么杰克信用不好。

现在我们问杰克的女朋友斯泰西:

Q1:我们一起出去玩时,杰克是否总是买单?(特征:慷慨)

A:不

Q2:杰克是否还我 500 美元?(特征:责任)

A:不

Q3:他是否有时为了炫耀而过度花钱?(特征:挥霍)

A:是的

这就解释了为什么杰克信用不好。

现在我们问杰克的好朋友乔治:

Q1:当杰克和我在我的公寓里玩时,他会自己清理吗?(特征:有组织)

A:不

Q2:杰克在我超级碗聚餐时是空手而来吗?(特征:关心)

A:是的

Q3:他是否曾经用“我忘了在家里带钱包”这个借口让我付他在餐馆的账单?(特征:责任)

A:是的

这就解释了为什么杰克信用不好。

现在我们谈谈信用良好的杰西卡。让我们问杰西卡的姐姐斯泰西:

Q1:每当我钱不够时,杰西卡是否会主动帮忙?(特征:慷慨)

A:是的

Q2:杰西卡是否按时支付账单?(特征:责任)

A:是的

Q3:杰西卡是否愿意帮我照顾孩子?(特征:关心)

A:是的

这就解释了为什么杰西卡信用良好。

现在我们问乔治,他碰巧是她的丈夫:

Q1:杰西卡是否保持房子整洁?(特征:有组织)

A:是的

Q2:她是否期望昂贵的礼物?(特征:挥霍)

A:不

Q3:当你忘记割草时,她会生气吗?(特征:易怒)

A:不

这就解释了为什么杰西卡信用良好。

现在让我们问大象酒吧的调酒师乔伊:

Q1:每当她和朋友一起来酒吧时,她是否大多是指定司机?(特征:负责)

A:是的

Q2:她是否总是带剩菜回家?(特征:挥霍)

A:是的

Q3:她是否慷慨地给小费?(特征:慷慨)

A:是的

随机森林的工作方式是在两个级别上进行随机选择:

  • 数据的一个子集

  • 一些特征的子集来分割数据

这两个子集可能会重叠。

在我们的例子中,我们有六个特征,我们将为每棵树分配三个特征。这样,我们有很大的机会会有重叠。

让我们将另外八个人添加到我们的训练数据集中:

名字 标签 慷慨 责任 关心 组织 挥霍 易怒
杰克 0 0 0 0 0 1 1
杰西卡 1 1 1 1 1 0 0
珍妮 0 0 0 1 0 1 1
瑞克 1 1 1 0 1 0 0
帕特 0 0 0 0 0 1 1
杰布:1 1 1 1 0 0 0
杰伊 1 0 1 1 1 0 0
纳特 0 1 0 0 0 1 1
罗恩 1 0 1 1 1 0 0
马特 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

  1. 执行所需的导入:
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

  1. 加载和解析数据:
scala> val data =
 MLUtils.loadLibSVMFile(sc, "rf_libsvm_data.txt")

  1. 将数据分割成“训练”和“测试”数据集:
scala> val splits = data.randomSplit(Array(0.7, 0.3))
scala> val (trainingData, testData) = (splits(0), splits(1))

  1. 创建分类作为树策略(随机森林也支持回归):
scala> val treeStrategy = Strategy.defaultStrategy("Classification")

  1. 训练模型:
scala> val model = RandomForest.trainClassifier(trainingData,
 treeStrategy, numTrees=3, featureSubsetStrategy="auto", seed = 12345)

  1. 在测试实例上评估模型并计算测试错误:
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)

  1. 检查模型:
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 一次训练一棵树,所以它们可能比随机森林需要更长的时间。

准备好了

我们将使用前一个配方中使用的相同数据。

如何做…

  1. 启动 Spark shell:
$ spark-shell

  1. 执行所需的导入操作:
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

  1. 加载并解析数据:
scala> val data =
 MLUtils.loadLibSVMFile(sc, "rf_libsvm_data.txt")

  1. 将数据分成“训练”和“测试”数据集:
scala> val splits = data.randomSplit(Array(0.7, 0.3))
scala> val (trainingData, testData) = (splits(0), splits(1))

  1. 创建一个分类作为增强策略,并将迭代次数设置为3
scala> val boostingStrategy =
 BoostingStrategy.defaultParams("Classification")
scala> boostingStrategy.numIterations = 3

  1. 训练模型:
scala> val model = GradientBoostedTrees.train(trainingData, boostingStrategy)

  1. 在测试实例上评估模型并计算测试误差:
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)

  1. 检查模型:
scala> println("Learned Random Forest:n" + model.toDebugString)

在这种情况下,模型的准确率为 0.9,低于我们在随机森林情况下得到的准确率。

使用朴素贝叶斯进行分类

让我们考虑使用机器学习构建电子邮件垃圾邮件过滤器。在这里,我们对两类感兴趣:垃圾邮件表示未经请求的消息,非垃圾邮件表示常规电子邮件:

使用朴素贝叶斯进行分类

第一个挑战是,当给定一封电子邮件时,我们如何将其表示为特征向量x。一封电子邮件只是一堆文本或一组单词(因此,这个问题领域属于更广泛的文本分类类别)。让我们用一个长度等于字典大小的特征向量来表示一封电子邮件。如果字典中的给定单词出现在电子邮件中,则值为 1;否则为 0。让我们构建一个表示内容为在线药店销售的电子邮件的向量:

使用朴素贝叶斯进行分类

该特征向量中的单词字典称为词汇表,向量的维度与词汇表的大小相同。如果词汇表大小为 10,000,则该特征向量中的可能值将为 210,000。

我们的目标是对y给定x的概率进行建模。为了对P(x|y)进行建模,我们将做出一个强烈的假设,即x是有条件独立的。这个假设被称为朴素贝叶斯假设,基于这个假设的算法被称为朴素贝叶斯分类器

例如,对于y=1,表示垃圾邮件,出现“在线”和“药店”这两个词的概率是独立的。这是一个与现实无关的强烈假设,但在获得良好预测时效果非常好。

准备就绪

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

  1. 执行所需的导入操作:
scala> import org.apache.spark.mllib.classification.NaiveBayes
scala> import org.apache.spark.mllib.linalg.Vectors
scala> import org.apache.spark.mllib.regression.LabeledPoint

  1. 将数据加载到 RDD 中:
scala> val data = sc.textFile("sample_naive_bayes_data.txt")

  1. 将数据解析为LabeledPoint
scala> val parsedData = data.map { line =>
 val parts = line.split(',')
 LabeledPoint(parts(0).toDouble, Vectors.dense(parts(1).split(' ').map(_.toDouble)))
}

  1. 将数据一分为二,分别放入“训练”和“测试”数据集中:
scala> val splits = parsedData.randomSplit(Array(0.5, 0.5), seed = 11L)
scala> val training = splits(0)
scala> val test = splits(1)

  1. 使用“训练”数据集训练模型:
val model = NaiveBayes.train(training, lambda = 1.0)

  1. 预测“测试”数据集的标签:
val predictionAndLabel = test.map(p => (model.predict(p.features), p.label))

第九章:使用 MLlib 进行无监督学习

本章将介绍如何使用 MLlib、Spark 的机器学习库进行无监督学习。

本章分为以下几个部分:

  • 使用 k-means 进行聚类

  • 使用主成分分析进行降维

  • 使用奇异值分解进行降维

介绍

以下是维基百科对无监督学习的定义:

"在机器学习中,无监督学习的问题是尝试在未标记的数据中找到隐藏的结构。"

与监督学习相比,我们有标记数据来训练算法,在无监督学习中,我们要求算法自行找到结构。让我们来看下面的样本数据集:

介绍

从上图可以看出,数据点形成了两个簇,如下所示:

介绍

事实上,聚类是最常见的无监督学习算法类型。

使用 k-means 进行聚类

聚类分析或聚类是将数据分成多个组的过程,使得一组中的数据类似于其他组中的数据。

以下是聚类使用的一些示例:

  • 市场细分:将目标市场分成多个细分,以便更好地满足每个细分的需求

  • 社交网络分析:通过社交网络网站(如 Facebook)找到社交网络中一致的人群进行广告定位

  • 数据中心计算集群:将一组计算机放在一起以提高性能

  • 天文数据分析:理解天文数据和事件,如星系形成

  • 房地产:根据相似特征识别社区

  • 文本分析:将小说或散文等文本文档分成流派

k-means 算法最好通过图像来说明,所以让我们再次看看我们的样本图:

使用 k-means 进行聚类

k-means 的第一步是随机选择两个点,称为聚类中心

使用 k-means 进行聚类

k-means 算法是一个迭代算法,分为两个步骤:

  • 簇分配步骤:该算法将遍历每个数据点,并根据其距离更近的质心,将其分配给该质心,从而分配给它代表的簇

  • 移动质心步骤:该算法将取每个质心并将其移动到簇中数据点的平均值

让我们看看在簇分配后我们的数据是什么样子:

使用 k-means 进行聚类

现在让我们将聚类中心移动到簇中数据点的平均值,如下所示:

使用 k-means 进行聚类

在这种情况下,一次迭代就足够了,进一步的迭代不会移动聚类中心。对于大多数真实数据,需要多次迭代才能将质心移动到最终位置。

k-means 算法需要输入一定数量的簇。

准备工作

让我们使用加利福尼亚州萨拉托加市的一些不同的住房数据。这次,我们将考虑地块面积和房价:

地块面积 房价(以千美元计)
--- ---
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 sv的逗号分隔值(CSV)文件,并将其绘制为散点图:

准备工作

找到簇的数量是一项棘手的任务。在这里,我们有视觉检查的优势,而对于超平面上的数据(超过三个维度),这是不可用的。让我们粗略地将数据分成四个簇,如下所示:

准备工作

我们将运行 k-means 算法来做同样的事情,并看看我们的结果有多接近。

如何做…

  1. sarataga.csv加载到 HDFS:
$ hdfs dfs -put saratoga.csv saratoga.csv

  1. 启动 Spark shell:
$ spark-shell

  1. 导入统计和相关类:
scala> import org.apache.spark.mllib.linalg.Vectors
scala> import org.apache.spark.mllib.clustering.KMeans

  1. saratoga.csv作为 RDD 加载:
scala> val data = sc.textFile("saratoga.csv")

  1. 将数据转换为密集向量的 RDD:
scala> val parsedData = data.map( line => Vectors.dense(line.split(',').map(_.toDouble)))

  1. 为四个簇和五次迭代训练模型:
scala> val kmmodel= KMeans.train(parsedData,4,5)

  1. parsedData收集为本地 scala 集合:
scala> val houses = parsedData.collect

  1. 预测第 0 个元素的簇:
scala> val prediction = kmmodel.predict(houses(0))

  1. 现在让我们比较 k-means 与我们单独完成的簇分配。k-means 算法从 0 开始给出簇 ID。一旦你检查数据,你会发现我们给出的 A 到 D 簇 ID 与 k-means 之间的以下映射:A=>3, B=>1, C=>0, D=>2。

  2. 现在,让我们从图表的不同部分挑选一些数据,并预测它属于哪个簇。

  3. 让我们看看房屋(18)的数据,占地面积为 876 平方英尺,售价为 665K 美元:

scala> val prediction = kmmodel.predict(houses(18))
resxx: Int = 3

  1. 现在,看看占地面积为 15,750 平方英尺,价格为 1.12 百万美元的房屋(35)的数据:
scala> val prediction = kmmodel.predict(houses(35))
resxx: Int = 1

  1. 现在看看房屋(6)的数据,占地面积为 38,768 平方英尺,售价为 2.725 百万美元:
scala> val prediction = kmmodel.predict(houses(6))
resxx: Int = 0

  1. 现在看看房屋(15)的数据,占地面积为 69,696 平方英尺,售价为 275 万美元:
scala>  val prediction = kmmodel.predict(houses(15))
resxx: Int = 2

你可以用更多的数据测试预测能力。让我们进行一些邻域分析,看看这些簇承载着什么含义。簇 3 中的大多数房屋都靠近市中心。簇 2 中的房屋位于多山的地形上。

在这个例子中,我们处理了一组非常小的特征;常识和视觉检查也会导致相同的结论。k-means 算法的美妙之处在于它可以对具有无限数量特征的数据进行聚类。当你有原始数据并想了解数据中的模式时,它是一个很好的工具。

使用主成分分析进行降维

降维是减少维度或特征数量的过程。很多真实数据包含非常多的特征。拥有成千上万个特征并不罕见。现在,我们需要深入研究重要的特征。

降维有几个目的,比如:

  • 数据压缩

  • 可视化

当维度减少时,它会减少磁盘占用和内存占用。最后但同样重要的是;它可以帮助算法运行得更快。它还可以将高度相关的维度减少到一个维度。

人类只能可视化三个维度,但数据可以拥有更高的维度。可视化可以帮助发现数据中隐藏的模式。降维可以通过将多个特征压缩成一个特征来帮助可视化。

降维最流行的算法是主成分分析(PCA)。

让我们看看以下数据集:

使用主成分分析进行降维

假设目标是将这个二维数据分成一维。做法是找到一条我们可以将这些数据投影到的线。让我们找一条适合将这些数据投影的线:

使用主成分分析进行降维

这是与数据点具有最短投影距离的线。让我们通过从每个数据点到这条投影线的最短线来进一步解释:

使用主成分分析进行降维

另一种看待的方式是,我们必须找到一条线来投影数据,使得数据点到这条线的平方距离之和最小化。这些灰色线段也被称为投影误差

准备好了

让我们来看看萨拉托加市的房屋数据的三个特征,即房屋大小、地块大小和价格。使用 PCA,我们将房屋大小和地块大小特征合并为一个特征—z。让我们称这个特征为房屋密度

值得注意的是,并不总是可能赋予新特征以意义。在这种情况下,很容易,因为我们只有两个特征要合并,我们可以用常识来结合这两者的效果。在更实际的情况下,您可能有 1000 个特征要投影到 100 个特征。可能不可能给这 100 个特征中的每一个赋予现实生活中的意义。

在这个练习中,我们将使用 PCA 推导出房屋密度,然后我们将进行线性回归,看看这个密度如何影响房价。

在我们深入 PCA 之前有一个预处理阶段:特征缩放。当两个特征的范围相差很大时,特征缩放就会出现。在这里,房屋大小的范围在 800 平方英尺到 7000 平方英尺之间变化,而地块大小在 800 平方英尺到几英亩之间变化。

为什么我们之前不需要进行特征缩放?答案是我们真的不需要让特征处于一个公平的水平上。梯度下降是另一个特征缩放非常有用的领域。

有不同的特征缩放方法:

  • 将特征值除以最大值,这将使每个特征处于Getting ready范围内

  • 将特征值除以范围,即最大值减最小值

  • 通过减去特征值的平均值,然后除以范围

  • 通过减去特征值的平均值,然后除以标准差

我们将使用最佳的第四种选择来进行缩放。以下是我们将用于此示例的数据:

房屋大小 地块大小 缩放后的房屋大小 缩放后的地块大小 房屋价格(以 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

  1. 启动 Spark shell:
$ spark-shell

  1. 导入统计和相关类:
scala> import org.apache.spark.mllib.linalg.Vectors
scala> import org.apache.spark.mllib.linalg.distributed.RowMatrix

  1. saratoga.csv加载为一个 RDD:
scala> val data = sc.textFile("scaledhousedata.csv")

  1. 将数据转换为密集向量的 RDD:
scala> val parsedData = data.map( line => Vectors.dense(line.split(',').map(_.toDouble)))

  1. parsedData创建一个RowMatrix
scala> val mat = new RowMatrix(parsedData)

  1. 计算一个主成分:
scala> val pc= mat.computePrincipalComponents(1)

  1. 将行投影到由主成分张成的线性空间:
scala> val projected = mat.multiply(pc)

  1. 将投影的RowMatrix转换回 RDD:
scala> val projectedRDD = projected.rows

  1. projectedRDD保存回 HDFS:
scala> projectedRDD.saveAsTextFile("phdata")

现在我们将使用这个投影特征,我们决定称之为住房密度,将其与房价绘制在一起,看看是否出现任何新的模式:

  1. 将 HDFS 目录phdata下载到本地目录phdata
scala> hdfs dfs -get phdata phdata

  1. 修剪数据中的起始和结束括号,并将数据加载到 MS Excel 中,放在房价旁边。

以下是房价与住房密度的图表:

如何做到这一点…

让我们按照以下数据画一些模式:

如何做到这一点…

我们在这里看到了什么模式?从高密度到低密度住房的转移,人们愿意支付高昂的溢价。随着住房密度的降低,这种溢价趋于平稳。例如,人们愿意支付高额溢价,从公寓和联排别墅搬到独栋住宅,但是在一个可比的建成区域内,拥有 3 英亩地块大小的独栋住宅与拥有 2 英亩地块大小的独栋住宅的溢价并不会有太大的不同。

奇异值分解降维

通常,原始维度并不能最好地表示数据。正如我们在 PCA 中看到的,有时可以将数据投影到更少的维度,仍然保留大部分有用的信息。

有时,最好的方法是沿着展现大部分变化的特征对齐维度。这种方法有助于消除不代表数据的维度。

让我们再次看一下下图,它显示了两个维度上的最佳拟合线:

奇异值分解降维

投影线显示了对原始数据的最佳近似,使用了一个维度。如果我们取灰线与黑线相交的点,并隔离黑线,我们将得到原始数据的减少表示,尽可能保留了尽可能多的变化,如下图所示:

奇异值分解降维

让我们画一条垂直于第一投影线的线,如下图所示:

奇异值分解降维

这条线尽可能多地捕捉了原始数据集的第二维度上的变化。它在近似原始数据方面做得不好,因为这个维度本来就变化较少。可以使用这些投影线来生成一组不相关的数据点,这些数据点将显示原始数据中一开始看不到的子分组。

这就是 SVD 的基本思想。将高维度、高变异性的数据点集合减少到一个更低维度的空间,更清晰地展现原始数据的结构,并按照变化最大到最小的顺序排列。SVD 非常有用的地方,尤其是对于 NLP 应用,是可以简单地忽略某个阈值以下的变化,从而大幅减少原始数据,确保保留原始关系的兴趣。

现在让我们稍微深入理论。SVD 基于线性代数中的一个定理,即一个矩阵 A 可以分解为三个矩阵的乘积——一个正交矩阵 U,一个对角矩阵 S,和一个正交矩阵 V 的转置。我们可以如下展示:

奇异值分解降维

UV是正交矩阵:

奇异值分解降维奇异值分解降维

U的列是奇异值分解降维的正交归一化特征向量,V的列是奇异值分解降维的正交归一化特征向量。S是一个对角矩阵,按降序包含来自UV的特征值的平方根。

准备就绪

让我们看一个术语-文档矩阵的例子。我们将看两篇关于美国总统选举的新闻。以下是两篇文章的链接:

让我们用这两条新闻构建总统候选人矩阵:

准备就绪准备就绪

让我们把这个矩阵放在一个 CSV 文件中,然后把它放在 HDFS 中。我们将对这个矩阵应用 SVD 并分析结果。

如何做…

  1. scaledhousedata.csv加载到 HDFS 中:
$ hdfs dfs -put pres.csv scaledhousedata.csv

  1. 启动 Spark shell:
$ spark-shell

  1. 导入统计和相关类:
scala> import org.apache.spark.mllib.linalg.Vectors
scala> import org.apache.spark.mllib.linalg.distributed.RowMatrix

  1. pres.csv加载为 RDD:
scala> val data = sc.textFile("pres.csv")

  1. 将数据转换为密集向量的 RDD:
scala> val parsedData = data.map( line => Vectors.dense(line.split(',').map(_.toDouble)))

  1. parsedData创建RowMatrix
scala> val mat = new RowMatrix(parsedData)

  1. 计算svd
scala> val svd = mat.computeSVD(2,true)

  1. 计算U因子(特征向量):
scala> val U = svd.U

  1. 计算奇异值(特征值)矩阵:
scala> val s = svd.s

  1. 计算V因子(特征向量):
scala> val s = svd.s

如果你看S,你会意识到它给 Npr 文章的评分比 Fox 文章高得多。

第十章:推荐系统

在本章中,我们将介绍以下内容:

  • 使用显式反馈的协同过滤

  • 使用隐式反馈的协同过滤

介绍

以下是维基百科对推荐系统的定义:

“推荐系统是信息过滤系统的一个子类,旨在预测用户对物品的‘评分’或‘偏好’。”

推荐系统近年来变得非常受欢迎。亚马逊用它们来推荐书籍,Netflix 用来推荐电影,Google 新闻用来推荐新闻故事。以下是一些推荐的影响的例子(来源:Celma,Lamere,2008):

  • Netflix 上观看的电影有三分之二是推荐的

  • 谷歌新闻点击量的 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 大数据沙箱中加载了 100k 部电影评分。您还可以从 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 进行推荐,MLlib 是 Spark 的机器学习库。

使用显式反馈的协同过滤

协同过滤是推荐系统中最常用的技术。它有一个有趣的特性——它自己学习特征。因此,在电影评分的情况下,我们不需要提供有关电影是浪漫还是动作的实际人类反馈。

正如我们在介绍部分看到的,电影有一些潜在特征,比如类型,同样用户也有一些潜在特征,比如年龄,性别等。协同过滤不需要它们,并且自己找出潜在特征。

在这个例子中,我们将使用一种名为交替最小二乘法ALS)的算法。该算法基于少量潜在特征解释电影和用户之间的关联。它使用三个训练参数:秩、迭代次数和 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

  1. 导入 ALS 和评分类:
scala> import org.apache.spark.mllib.recommendation.ALS
scala> import org.apache.spark.mllib.recommendation.Rating

  1. 将评分数据加载到 RDD 中:
scala> val data = sc.textFile("moviedata/u.data")

  1. val data转换为评分的 RDD:
scala> val ratings = data.map { line => 
 val Array(userId, itemId, rating, _) = line.split("\t") 
 Rating(userId.toInt, itemId.toInt, rating.toDouble) 
}

  1. 将个性化评分数据加载到 RDD 中:
scala> val pdata = sc.textFile("p.data")

  1. 将数据转换为个性化评分的 RDD:
scala> val pratings = pdata.map { line => 
 val Array(userId, itemId, rating) = line.split(",")
 Rating(userId.toInt, itemId.toInt, rating.toDouble) 
}

  1. 将评分与个性化评分结合:
scala> val movieratings = ratings.union(pratings)

  1. 使用秩为 5 和 10 次迭代以及 0.01 作为 lambda 构建 ALS 模型:
scala> val model = ALS.train(movieratings, 10, 10, 0.01)

  1. 让我们根据这个模型预测我对给定电影的评分会是多少。

  2. 让我们从原始的终结者开始,电影 ID 为 195:

scala> model.predict(sc.parallelize(Array((944,195)))).collect.foreach(println)
Rating(944,195,4.198642954004738)

由于我给终结者 2评了 5 分,这是一个合理的预测。

  1. 让我们尝试一下,电影 ID 为 402:
scala> model.predict(sc.parallelize(Array((944,402)))).collect.foreach(println)
Rating(944,402,2.982213836456829)

这是一个合理的猜测。

  1. 让我们尝试一下鬼与黑暗,这是我已经评分的电影,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

  1. 将歌曲数据上传到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")

  1. 将用户数据加载为 RDD:
scala> val users = sc.textFile("songdata/kaggle_users.txt")

  1. 将三元组(用户、歌曲、播放次数)数据加载为 RDD:
scala> val triplets = sc.textFile("songdata/kaggle_visible_evaluation_triplets.txt")

  1. 将歌曲数据转换为PairRDD
scala> val songIndex = songs.map(_.split("\\W+")).map(v => (v(0),v(1).toInt))

  1. 收集songIndex作为 Map:
scala> val songMap = songIndex.collectAsMap

  1. 将用户数据转换为PairRDD
scala> val userIndex = users.zipWithIndex.map( t => (t._1,t._2.toInt))

  1. 收集userIndex作为 Map:
scala> val userMap = userIndex.collectAsMap

我们需要songMapuserMap来替换三元组中的userIdsongId。Spark 会根据需要自动在集群上提供这两个映射。这样做效果很好,但每次需要发送到集群时都很昂贵。

更好的方法是使用 Spark 的一个特性叫做broadcast变量。broadcast变量允许 Spark 作业在每台机器上保留一个只读副本的变量缓存,而不是在每个任务中传输一个副本。Spark 使用高效的广播算法来分发广播变量,因此网络上的通信成本可以忽略不计。

正如你可以猜到的,songMapuserMap都是很好的候选对象,可以包装在broadcast变量周围。执行以下步骤:

  1. 广播userMap
scala> val broadcastUserMap = sc.broadcast(userMap)

  1. 广播songMap
scala> val broadcastSongMap = sc.broadcast(songMap)

  1. triplet转换为数组:
scala> val tripArray = triplets.map(_.split("\\W+"))

  1. 导入评分:
scala> import org.apache.spark.mllib.recommendation.Rating

  1. 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

  1. 使用 ALS 构建一个具有 rank 10 和 10 次迭代的模型:
scala> val model = ALS.trainImplicit(ratings, 10, 10)

  1. 从三元组中提取用户和歌曲元组:
scala> val usersSongs = ratings.map( r => (r.user, r.product) )

  1. 为用户和歌曲元组做出预测:
scala> val predictions = model.predict(usersSongs)

它是如何工作的…

我们的模型需要四个参数才能工作,如下所示:

参数名称 描述
Rank 模型中的潜在特征数
Iterations 用于运行此因子分解的迭代次数
Lambda 过拟合参数
Alpha 观察交互的相对权重

正如你在梯度下降的情况下看到的,这些参数需要手动设置。我们可以尝试不同的值,但最好的值是 rank=50,iterations=30,lambda=0.00001,alpha=40。

还有更多…

快速测试不同参数的一种方法是在 Amazon EC2 上生成一个 Spark 集群。这样可以灵活地选择一个强大的实例来快速测试这些参数。我已经创建了一个名为com.infoobjects.songdata的公共 s3 存储桶,以便将数据传输到 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 的关注者是有向图的一个很好的例子。如果约翰是大卫的 Twitter 关注者,这并不意味着大卫是约翰的关注者。另一方面,Facebook 是常规图的一个很好的例子。如果约翰是大卫的 Facebook 朋友,大卫也是约翰的 Facebook 朋友。

多重图是允许具有多个边(也称为平行边)的图。由于 GraphX 中的每条边都有属性,因此每条边都有自己的标识。

传统上,对于分布式图处理,有两种类型的系统:

  • 数据并行

  • 图并行

GraphX 旨在将两者结合在一个系统中。GraphX API 使用户能够在不移动数据的情况下将数据同时视为图和集合(RDD)。

图的基本操作

在这个示例中,我们将学习如何创建图并对其进行基本操作。

准备工作

作为一个起始示例,我们将有三个顶点,分别代表加利福尼亚州的三个城市的市中心——圣克拉拉、弗里蒙特和旧金山。以下是这些城市之间的距离:

目的地 距离(英里)
圣克拉拉,加利福尼亚 弗里蒙特,加利福尼亚 20
弗里蒙特,加利福尼亚 旧金山,加利福尼亚 44
旧金山,加利福尼亚 圣克拉拉,加利福尼亚 53

如何做…

  1. 导入与 GraphX 相关的类:
scala> import org.apache.spark.graphx._
scala> import org.apache.spark.rdd.RDD

  1. 将顶点数据加载到数组中:
scala> val vertices = Array((1L, ("Santa Clara","CA")),(2L, ("Fremont","CA")),(3L, ("San Francisco","CA")))

  1. 将顶点数组加载到顶点的 RDD 中:
scala> val vrdd = sc.parallelize(vertices)

  1. 将边数据加载到数组中:
scala> val edges = Array(Edge(1L,2L,20),Edge(2L,3L,44),Edge(3L,1L,53))

  1. 将数据加载到边的 RDD 中:
scala> val erdd = sc.parallelize(edges)

  1. 创建图:
scala> val graph = Graph(vrdd,erdd)

  1. 打印图的所有顶点:
scala> graph.vertices.collect.foreach(println)

  1. 打印图的所有边:
scala> graph.edges.collect.foreach(println)

  1. 打印边的三元组;通过向边添加源和目的地属性来创建三元组:
scala> graph.triplets.collect.foreach(println)

  1. 图的入度是它具有的内向边的数量。打印每个顶点的入度(作为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._

  1. 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文件以“源链接:link1 link2…”的格式包含链接。

  1. 展平并将其转换为“link1,link2”格式,然后将其转换为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))

  1. hdfs加载顶点,使用 20 个分区:
scala> val verticesFile = sc.textFile("wiki/nodes.txt",20)

  1. 或者,从 Amazon S3 加载边缘:
scala> val verticesFile = sc.textFile("s3n:// com.infoobjects.wiki/nodes",20)

  1. 为顶点提供索引,然后交换它以使其成为(索引,标题)格式:
scala> val vertices = verticesFile.zipWithIndex.map(_.swap)

  1. 创建graph对象:
scala> val graph = Graph(vertices,edges)

  1. 运行 PageRank 并获取顶点:
scala> val ranks = graph.pageRank(0.001).vertices

  1. 由于排名是以(顶点 ID,pagerank)格式,因此交换它以使其成为(pagerank,顶点 ID)格式:
scala> val swappedRanks = ranks.map(_.swap)

  1. 排序以首先获取排名最高的页面:
scala> val sortedRanks = swappedRanks.sortByKey(false)

  1. 获取排名最高的页面:
scala> val highest = sortedRanks.first

  1. 前面的命令给出了顶点 ID,您仍然需要查找以查看具有排名的实际标题。让我们进行连接:
scala> val join = sortedRanks.join(vertices)

  1. 在将格式从(顶点 ID,(页面排名,标题))转换为(页面排名,(顶点 ID,标题))格式后,再次对连接的 RDD 进行排序:
scala> val final = join.map ( v => (v._2._1, (v._1,v._2._2))).sortByKey(false)

  1. 打印排名前五的页面
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 标记图的每个连接组件。

准备好了

我们将在这里为我们知道的集群构建一个小图,并使用连接的组件来对它们进行分隔。让我们看看以下数据:

准备就绪

追随者 跟随者
约翰 帕特
帕特 戴夫
加里 克里斯
克里斯 比尔

前面的数据是一个简单的数据,有六个顶点和两个集群。让我们将这些数据放在两个文件的形式中: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,约翰)标识,第二个由(4,加里)标识。

您可以使用以下命令将文件加载到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

  1. 导入与 GraphX 相关的类:
scala> import org.apache.spark.graphx._

  1. hdfs加载边缘:
scala> val edgesFile = sc.textFile("hdfs://localhost:9000/user/hduser/data/cc/edges.csv")

  1. edgesFile RDD 转换为边的 RDD:
scala> val edges = edgesFile.map(_.split(",")).map(e => Edge(e(0).toLong,e(1).toLong,e(2)))

  1. hdfs加载顶点:
scala> val verticesFile = sc.textFile("hdfs://localhost:9000/user/hduser/data/cc/nodes.csv")

  1. 映射顶点:
scala> val vertices = verticesFile.map(_.split(",")).map( e => (e(0).toLong,e(1)))

  1. 创建graph对象:
scala> val graph = Graph(vertices,edges)

  1. 计算连接的组件:
scala> val cc = graph.connectedComponents

  1. 找到连接组件的顶点(这是一个子图):
scala> val ccVertices = cc.vertices

  1. 打印ccVertices
scala> ccVertices.collect.foreach(println)

如您在输出中所见,顶点 1,2,3 指向 1,而 4,5,6 指向 4。这两个都是它们各自集群中索引最低的顶点。

执行邻域聚合

GraphX 通过隔离每个顶点及其邻居来进行大部分计算。这使得在分布式系统上处理大规模图数据变得更加容易。这使得邻域操作非常重要。GraphX 有一种机制可以在每个邻域级别进行,即aggregateMessages方法。它分两步进行:

  1. 在第一步(方法的第一个函数)中,消息被发送到目标顶点或源顶点(类似于 MapReduce 中的 Map 函数)。

  2. 在第二步(方法的第二个函数)中,对这些消息进行聚合(类似于 MapReduce 中的 Reduce 函数)。

准备好了

让我们构建一个追随者的小数据集:

追随者 跟随者
约翰 巴拉克
帕特 巴拉克
加里 巴拉克
克里斯 米特
罗布 米特

我们的目标是找出每个节点有多少关注者。让我们以两个文件的形式加载这些数据: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

  1. 导入与 GraphX 相关的类:
scala> import org.apache.spark.graphx._

  1. hdfs加载边:
scala> val edgesFile = sc.textFile("hdfs://localhost:9000/user/hduser/data/na/edges.csv")

  1. 将边转换为边的 RDD:
scala> val edges = edgesFile.map(_.split(",")).map(e => Edge(e(0).toLong,e(1).toLong,e(2)))

  1. hdfs加载顶点:
scala> val verticesFile = sc.textFile("hdfs://localhost:9000/user/hduser/data/cc/nodes.csv")

  1. 映射顶点:
scala> val vertices = verticesFile.map(_.split(",")).map( e => (e(0).toLong,e(1)))

  1. 创建graph对象:
scala> val graph = Graph(vertices,edges)

  1. 通过向关注者发送消息,消息中包含每个关注者的关注者数量,即 1,然后添加关注者数量来进行邻域聚合:
scala> val followerCount = graph.aggregateMessages(Int), (a, b) => (a+b))

  1. 以(被关注者,关注者数量)的形式打印followerCount
scala> followerCount.collect.foreach(println)

您应该获得类似以下的输出:

(1,3)
(5,2)

第十二章:优化和性能调优

本章涵盖了在使用 Spark 时的各种优化和性能调优最佳实践。

本章分为以下几个配方:

  • 优化内存

  • 使用压缩以提高性能

  • 使用序列化以提高性能

  • 优化垃圾收集

  • 优化并行级别

  • 理解优化的未来-项目钨

介绍

在研究各种优化 Spark 的方法之前,最好先了解 Spark 的内部情况。到目前为止,我们已经在较高级别上看待了 Spark,重点是各种库提供的功能。

让我们重新定义一个 RDD。从外部来看,RDD 是一个分布式的不可变对象集合。在内部,它由以下五个部分组成:

  • 一组分区(rdd.getPartitions

  • 对父 RDD 的依赖列表(rdd.dependencies

  • 计算分区的函数,给定其父级

  • 分区器(可选)(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 输入拆分/块一个分区(org.apache.spark.rdd.HadoopPartition
依赖
计算函数 读取块
首选位置 hdfs 块位置
分区器
  1. words RDD 中的单词标记化,每个单词占一行:
scala> val wordsFlatMap = words.flatMap(_.split("\\W+"))

以下是wordsFlatMap RDD 的五个部分:

分区 与父 RDD 相同,即wordsorg.apache.spark.rdd.HadoopPartition
依赖 与父 RDD 相同,即wordsorg.apache.spark.OneToOneDependency
计算函数 计算父级并拆分每个元素并展平结果
首选位置 询问父母
分区器
  1. wordsFlatMap RDD 中的每个单词转换为(单词,1)元组:
scala> val wordsMap = wordsFlatMap.map( w => (w,1))

以下是wordsMap RDD 的五个部分:

分区 与父 RDD 相同,即 wordsFlatMap(org.apache.spark.rdd.HadoopPartition)
依赖 与父 RDD 相同,即 wordsFlatMap(org.apache.spark.OneToOneDependency)
计算函数 计算父级并将其映射到 PairRDD
首选位置 询问父母
分区器
  1. 将给定键的所有值减少并求和:
scala> val wordCount = wordsMap.reduceByKey(_+_)

以下是wordCount RDD 的五个部分:

分区 每个 reduce 任务一个(org.apache.spark.rdd.ShuffledRDDPartition
依赖 每个父级的 Shuffle 依赖(org.apache.spark.ShuffleDependency
计算函数 对洗牌数据进行加法
首选位置
分区器 HashPartitioner(org.apache.spark.HashPartitioner

这是wordCount在分区级别视图下的 RDD 图的样子:

介绍

优化内存

Spark 是一个复杂的分布式计算框架,有许多组成部分。各种集群资源,如内存、CPU 和网络带宽,可能在各个点成为瓶颈。由于 Spark 是一个内存计算框架,内存的影响最大。

另一个问题是,Spark 应用程序通常使用大量内存,有时超过 100GB。这种内存使用量在传统的 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)。

堆被分为两个空间或代:年轻空间和老年空间。年轻空间用于分配新对象。年轻空间包括一个称为伊甸园的区域和两个较小的幸存者空间。当幼儿园变满时,通过运行称为年轻收集的特殊过程来收集垃圾,其中所有已经存在足够长时间的对象都被提升到老年空间。当老年空间变满时,通过运行称为老年收集的过程来在那里收集垃圾。

优化内存

幼儿园背后的逻辑是,大多数对象的寿命非常短。年轻收集旨在快速找到新分配的对象并将它们移动到老年空间。

JVM 使用标记和清除算法进行垃圾回收。标记和清除收集包括两个阶段。

在标记阶段,所有具有活动引用的对象都被标记为活动的,其余的被假定为垃圾收集的候选对象。在清除阶段,垃圾收集候选对象占用的空间被添加到空闲列表中,即它们可以分配给新对象。

标记和清除有两个改进。一个是并发标记和清除CMS),另一个是并行标记和清除。CMS 专注于较低的延迟,而后者专注于更高的吞吐量。这两种策略都有性能权衡。CMS 不进行压缩,而并行垃圾收集器GC)执行整个堆的压缩,这会导致暂停时间。作为经验法则,对于实时流处理,应该使用 CMS,否则使用并行 GC。

如果您希望同时具有低延迟和高吞吐量,Java 1.7 更新 4 之后还有另一个选项,称为垃圾优先 GCG1)。G1 是一种服务器式垃圾收集器,主要用于具有大内存的多核机器。它计划作为 CMS 的长期替代品。因此,为了修改我们的经验法则,如果您使用 Java 7 及以上版本,只需使用 G1。

G1 将堆分成一组大小相等的区域,每个区域都是虚拟内存的连续范围。每个区域被分配了一个角色,如伊甸园、幸存者和老年。G1 执行并发全局标记阶段,以确定整个堆中对象的活动引用。标记阶段结束后,G1 知道哪些区域大部分是空的。它首先在这些区域中进行收集,从而释放更多的内存。

优化内存

G1 选择的用于垃圾收集的区域使用疏散进行垃圾收集。G1 将对象从堆的一个或多个区域复制到堆上的单个区域,并且它既压缩又释放内存。这种疏散是在多个核心上并行执行的,以减少暂停时间并增加吞吐量。因此,每次垃圾收集循环都会减少碎片化,同时在用户定义的暂停时间内工作。

在 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

  1. 导入与之相关的StorageLevel和隐式转换:
scala> import org.apache.spark.storage.StorageLevel._

  1. 创建一个 RDD:
scala> val words = sc.textFile("words")

  1. 持久化 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

  1. Kryo自动注册大部分核心 Scala 类,但如果您想注册自己的类,可以使用以下命令:
scala> sc.getConf.registerKryoClasses(Array(classOf[com.infoobjects.CustomClass1],classOf[com.infoobjects.CustomClass2])

优化垃圾收集

如果有大量短寿命的 RDD,JVM 垃圾收集可能会成为一个挑战。 JVM 需要检查所有对象以找到需要进行垃圾回收的对象。垃圾收集的成本与 GC 需要检查的对象数量成正比。因此,使用更少的对象和使用更少对象的数据结构(更简单的数据结构,如数组)有助于减少垃圾收集的成本。

序列化在这里也很出色,因为一个字节数组只需要一个对象进行垃圾回收。

默认情况下,Spark 使用 60%的执行器内存来缓存 RDD,其余 40%用于常规对象。有时,您可能不需要 60%的 RDD,并且可以减少此限制,以便为对象创建提供更多空间(减少对 GC 的需求)。

如何做到…

您可以通过启动 Spark shell 并设置内存分数来将 RDD 缓存的内存分配设置为 40%:

$ spark-shell --conf spark.storage.memoryFraction=0.4

优化并行级别

优化并行级别对充分利用集群容量非常重要。在 HDFS 的情况下,这意味着分区的数量与InputSplits的数量相同,这与块的数量大致相同。

在本教程中,我们将介绍优化分区数量的不同方法。

如何做到…

在加载文件到 RDD 时指定分区数量,具体步骤如下:

  1. 启动 Spark shell:
$ spark-shell

  1. 使用自定义分区数量作为第二个参数加载 RDD:
scala> sc.textFile("hdfs://localhost:9000/user/hduser/words",10)

另一种方法是通过执行以下步骤更改默认并行度:

  1. 使用新的默认并行度值启动 Spark shell:
$ spark-shell --conf spark.default.parallelism=10

  1. 检查默认并行度的值:
scala> sc.defaultParallelism

您还可以使用 RDD 方法coalesce(numPartitions)来减少分区数量,其中numPartitions是您希望的最终分区数量。如果您希望数据在网络上重新分配,可以调用 RDD 方法repartition(numPartitions),其中numPartitions是您希望的最终分区数量。

了解优化的未来-项目钨

项目钨从 Spark 1.4 版本开始,旨在将 Spark 更接近裸金属。该项目的目标是大幅提高 Spark 应用程序的内存和 CPU 效率,并推动底层硬件的极限。

在分布式系统中,传统智慧一直是始终优化网络 I/O,因为这一直是最稀缺和最瓶颈的资源。这一趋势在过去几年已经改变。在过去 5 年中,网络带宽已经从每秒 1 千兆位增加到每秒 10 千兆位。

在类似的情况下,磁盘带宽已经从 50MB/s 增加到 500MB/s,SSD 的部署也越来越多。另一方面,CPU 时钟速度在 5 年前是~3GHz,现在仍然是一样的。这使得网络不再是瓶颈,而使 CPU 成为分布式处理中的新瓶颈。

另一个增加 CPU 性能负担的趋势是新的压缩数据格式,比如 Parquet。正如我们在本章的前几个示例中看到的,压缩和序列化会导致更多的 CPU 周期。这一趋势也推动了减少 CPU 周期成本的 CPU 优化的需求。

在类似的情况下,让我们看看内存占用。在 Java 中,GC 进行内存管理。GC 在将内存管理从程序员手中拿走并使其透明方面做得很好。为了做到这一点,Java 必须付出很大的开销,这大大增加了内存占用。例如,一个简单的字符串"abcd",在 Java 中应该占用 4 个字节,实际上占用了 48 个字节。

如果我们放弃 GC,像在 C 等低级编程语言中那样手动管理内存会怎样?自 Java 1.7 版本以来,Java 确实提供了一种方法来做到这一点,称为sun.misc.Unsafe。Unsafe 基本上意味着您可以构建长区域的内存而不进行任何安全检查。这是项目钨的第一个特点。

通过利用应用程序语义进行手动内存管理

通过利用应用程序语义进行手动内存管理,如果你不知道自己在做什么,这可能非常危险,但在 Spark 中是一种福音。我们利用数据模式(DataFrames)的知识直接布局内存。这不仅可以摆脱 GC 开销,还可以最小化内存占用。

第二点是将数据存储在 CPU 缓存与内存中。每个人都知道 CPU 缓存很棒,因为从主内存获取数据需要三个周期,而缓存只需要一个周期。这是项目钨的第二个特点。

使用算法和数据结构

算法和数据结构被用来利用内存层次结构,实现更多的缓存感知计算。

CPU 缓存是存储 CPU 下一个需要的数据的小内存池。CPU 有两种类型的缓存:指令缓存和数据缓存。数据缓存按照 L1、L2 和 L3 的层次结构排列:

  • L1 缓存是计算机中最快、最昂贵的缓存。它存储最关键的数据,是 CPU 查找信息的第一个地方。

  • L2 缓存比 L1 稍慢,但仍位于同一处理器芯片上。这是 CPU 查找信息的第二个地方。

  • L3 缓存仍然较慢,但由所有核心共享,例如 DRAM(内存)。

这些可以在以下图表中看到:

使用算法和数据结构

第三点是,Java 在字节码生成方面不太擅长,比如表达式求值。如果这种代码生成是手动完成的,效率会更高。代码生成是 Tungsten 项目的第三个特性。

代码生成

这涉及利用现代编译器和 CPU,以便直接在二进制数据上进行高效操作。目前,Tungsten 项目还处于起步阶段,在 1.5 版本中将有更广泛的支持。

posted @ 2024-05-21 12:53  绝不原创的飞龙  阅读(5)  评论(0编辑  收藏  举报