精通-R-Spark-全-

精通 R Spark(全)

原文:zh.annas-archive.org/md5/43c9ad07897496fab08c23cefc09a8d5

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

Apache Spark 是一个建立在可扩展性之上的分布式计算平台:Spark 的 API 使得从多个数据源获取输入并使用不同的编程语言和算法进行处理来构建数据应用变得容易。R 语言是数据科学和统计学中最强大的语言之一,因此将 R 与 Spark 连接起来非常有意义。幸运的是,R 丰富的语言特性使得从 R 调用 Spark 的 API 变得简单,看起来就像在本地数据源上运行 R 一样。只需了解这两个系统的一些背景知识,你就能够在 Spark 中调用大规模计算,或者从你喜爱的 R 编程环境中并行运行你的 R 代码。

本书详细探讨了如何使用 Spark 进行 R 编程,重点介绍了sparklyr包,该包支持dplyr和其他为 R 社区所熟知的包。它详细介绍了所有主要的使用案例,从使用 Spark 引擎查询数据到探索性数据分析、机器学习、R 代码的并行执行以及流处理。它还提供了一个自包含的介绍,讲述了如何运行 Spark 并监控作业的执行情况。书中的作者——Javier、Kevin 和 Edgar,自项目开始以来一直参与了sparklyr的开发。我对他们如何精心组织这本关于在 R 中使用 Spark 的清晰而专注的指南感到非常兴奋。

希望你喜欢这本书,并用它来扩展你的 R 工作负载,并将其连接到更广泛的 Spark 生态系统的能力中去。因为这里所有的基础设施都是开源的,所以请毫不犹豫地向开发人员提供关于改进这些工具的反馈。

Matei Zaharia

斯坦福大学助理教授,

Databricks 的首席技术专家,

和 Apache Spark 的原创者

前言

在信息呈指数级增长的世界中,领先的工具如 Apache Spark 提供支持,以解决今天我们面临的许多相关问题。从寻求基于数据驱动决策改进的公司,到在健康保健、金融、教育和能源领域解决问题的研究机构,Spark 使得分析更多信息比以往任何时候更快速、更可靠地实现。

已经有各种书籍写作用于学习 Apache Spark;例如,Spark: The Definitive Guide是一个全面的资源,而Learning Spark则是一本入门书籍,旨在帮助用户快速上手(均出自 O’Reilly)。然而,截至目前,尚无一本专门用于学习使用 R 语言进行 Apache Spark 的书籍,也没有专门为 R 用户或有意成为 R 用户的人设计的书籍。

有一些在线资源可供学习使用 R 语言学习 Apache Spark,尤其是spark.rstudio.com网站和 Spark 文档网站在spark.apache.org。这两个网站都是很好的在线资源;然而,内容并不打算从头到尾阅读,并假定您作为读者对 Apache Spark、R 和集群计算有一定的了解。

本书的目标是帮助任何人开始使用 R 语言进行 Apache Spark 的学习。另外,由于 R 编程语言的创建是为了简化数据分析,我们相信本书为您提供了学习使用 Spark 解决数据分析问题的最简单途径。前几章提供了一个介绍,帮助任何人快速掌握这些概念,并介绍在您自己的计算机上处理这些问题所需的工具。然后我们迅速进入相关的数据科学主题、集群计算和即使对经验丰富的用户也感兴趣的高级主题。

因此,本书旨在成为广泛用户群的有用资源,从初学者对学习 Apache Spark 感兴趣,到有经验的读者希望了解为什么以及如何从 R 语言使用 Apache Spark。

本书的一般概述如下:

介绍

在前两章中,第一章,介绍,和第二章,入门,您将了解 Apache Spark、R 语言以及使用 Spark 和 R 进行数据分析的工具。

分析

在第三章,分析中,您将学习如何使用 R 语言在 Apache Spark 中分析、探索、转换和可视化数据。

建模

在第四章,建模和第五章,管道中,您将学习如何创建统计模型,目的是提取信息、预测结果,并在生产准备好的工作流程中自动化此过程。

扩展

截至目前,本书集中讨论了在个人计算机上执行操作以及使用有限数据格式。第六章,集群,第七章,连接,第八章,数据 和 第九章,调整,介绍了分布式计算技术,这些技术用于跨多台机器和数据格式执行分析和建模,以解决 Apache Spark 设计用于处理的大规模数据和计算问题。

扩展

第十章,扩展,描述了特定相关用例适用的可选组件和扩展功能。您将了解替代建模框架、图处理、深度学习预处理数据、地理空间分析和大规模基因组学。

高级

本书以一组高级章节结束,第十一章,分布式 R,第十二章,流处理 和 第十三章,贡献;这些章节对高级用户最感兴趣。然而,当您到达本节时,内容不会显得那么令人生畏;相反,这些章节与前面的章节一样相关、有用和有趣。

第一组章节,第一章–第五章,提供了在规模化数据科学和机器学习上执行温和介绍。如果您计划在阅读本书的同时按照代码示例进行操作,那么这些章节是考虑逐行执行代码的好选择。因为这些章节使用您的个人计算机教授所有概念,所以您不会利用 Spark 被设计用于使用的多台计算机。但不要担心:接下来的章节将详细教授这一点!

第二组章节,第六章–第九章,介绍了在使用 Spark 进行集群计算的令人兴奋的世界中的基本概念。说实话,它们也介绍了集群计算中一些不那么有趣的部分,但请相信我们,学习我们提出的概念是值得的。此外,每章的概述部分特别有趣、信息丰富且易于阅读,有助于您对集群计算的工作原理形成直观的理解。对于这些章节,我们实际上不建议逐行执行代码——特别是如果您试图从头到尾学习 Spark。在您拥有适当的 Spark 集群之后,您随时可以回来执行代码。然而,如果您在工作中已经有了一个集群,或者您真的很有动力想要得到一个集群,您可能希望使用 第六章 来选择一个,然后使用 第七章 连接到它。

第三组章节,10–13,介绍了对大多数读者都很有趣的工具,将有助于更好地跟踪内容。这些章节涵盖了许多高级主题,自然而然地,你可能对某些主题更感兴趣;例如,你可能对分析地理数据集感兴趣,或者你可能更喜欢处理实时数据集,甚至两者兼顾!根据你的个人兴趣或手头的问题,我们鼓励你执行最相关的代码示例。这些章节中的所有代码都是为了在你的个人计算机上执行而编写的,但是我们也鼓励你使用适当的 Spark 集群,因为你将拥有解决问题和调优大规模计算所需的工具。

格式化

从代码生成的表格的格式如下:

# A tibble: 3 x 2
  numbers text
    <dbl> <chr>
1       1 one
2       2 two
3       3 three

表格的尺寸(行数和列数)在第一行描述,接着是第二行的列名和第三行的列类型。此外,我们在整本书中还使用了 tibble 包提供的各种微小视觉改进。

大多数图表使用 ggplot2 包及附录中提供的自定义主题进行渲染;然而,由于本书不侧重数据可视化,我们仅提供了一个基本绘图的代码,可能与我们应用的格式不符合。如果你有兴趣学习更多关于 R 中可视化的内容,可以考虑专门的书籍,比如 R Graphics Cookbook(O'Reilly)。

致谢

感谢使 Spark 与 R 集成的包作者们:Javier Luraschi、Kevin Kuo、Kevin Ushey 和 JJ Allaire(sparklyr);Romain François 和 Hadley Wickham(dbplyr);Hadley Wickham 和 Edgar Ruiz(dpblyr);Kirill Mülller(DBI);以及 Apache Spark 项目本身的作者及其原始作者 Matei Zaharia。

感谢那些发布扩展以丰富 Spark 和 R 生态系统的包作者们:Akhil Nair(crassy);Harry Zhu(geospark);Kevin Kuo(graphframesmleapsparktfsparkxgb);Jakub Hava、Navdeep Gill、Erin LeDell 和 Michal Malohlava(rsparkling);Jan Wijffels(spark.sas7bdat);Aki Ariga(sparkavro);Martin Studer(sparkbq);Matt Pollock(sparklyr.nested);Nathan Eastwood(sparkts);以及 Samuel Macêdo(variantspark)。

感谢我们出色的编辑 Melissa Potter,她为我们提供了指导、鼓励和无数小时的详细反馈,使这本书成为我们能够写出的最好的作品。

致谢 Bradley Boehmke、Bryan Adams、Bryan Jonas、Dusty Turner 和 Hossein Falaki,感谢你们的技术审查、时间和坦诚反馈,以及与我们分享专业知识。多亏了你们,许多读者将会有更愉快的阅读体验。

感谢 RStudio、JJ Allaire 和 Tareef Kawaf 支持这项工作,以及 R 社区本身对其持续的支持和鼓励。

Max Kuhn,感谢您在第四章中对模型的宝贵反馈,我们在此章节中经过他的允许,从他精彩的书籍Feature Engineering and Selection: A Practical Approach for Predictive Models(CRC Press)中改编了示例。

我们也要感谢那些间接参与但未在本节明确列出的每个人;我们确实站在巨人的肩膀上。

本书本身是使用bookdown(由谢益辉开发)、rmarkdown(由 JJ Allaire 和谢益辉开发)以及knitr(由谢益辉开发)编写的,我们使用ggplot2(由 Hadley Wickham 和 Winston Chang 开发)绘制了可视化,使用nomnoml(由 Daniel Kallin 和 Javier Luraschi 开发)创建了图表,并使用pandoc(由 John MacFarlane 开发)进行了文档转换。

本书使用的约定

本书使用了以下排版约定:

斜体

表示新术语、URL、电子邮件地址、文件名和文件扩展名。

等宽

用于程序清单以及段落内引用程序元素,例如变量或函数名、数据库、数据类型、环境变量、语句和关键字。

等宽粗体

显示用户应按原样输入的命令或其他文本。

等宽斜体

显示应替换为用户提供值或由上下文确定值的文本。

提示

此元素表示提示或建议。

注意

此元素表示一般说明。

使用代码示例

补充材料(代码示例、练习等)可从https://github.com/r-spark/the-r-in-spark下载。

本书旨在帮助您完成工作。一般情况下,如果本书提供了示例代码,您可以在您的程序和文档中使用它。除非您要复制代码的大部分,否则无需征得我们的许可。例如,编写一个使用本书多个代码块的程序不需要许可。售卖或分发包含 O’Reilly 书籍示例的 CD-ROM 则需要许可。引用本书并引述示例代码以回答问题也不需要许可。将本书中大量示例代码整合到产品文档中则需要许可。

我们感谢,但不要求署名。署名通常包括书名、作者、出版商和 ISBN。例如:“Mastering Spark with R by Javier Luraschi, Kevin Kuo, and Edgar Ruiz (O’Reilly). Copyright 2020 Javier Luraschi, Kevin Kuo, and Edgar Ruiz, 978-1-492-04637-0.”

如果您认为您对代码示例的使用超出了合理使用范围或上述许可的限制,请随时通过permissions@oreilly.com联系我们。

O’Reilly 在线学习

注意

近 40 年来,O’Reilly Media一直致力于为公司提供技术和商业培训、知识和见解,帮助它们取得成功。

我们独特的专家和创新者网络通过图书、文章、会议和我们的在线学习平台分享他们的知识和专业知识。O’Reilly 的在线学习平台让您随时访问现场培训课程、深度学习路径、交互式编码环境以及来自 O’Reilly 和其他 200 多家出版商的大量文本和视频。欲了解更多信息,请访问http://oreilly.com

如何联系我们

请将有关本书的评论和问题发送至出版商:

  • O’Reilly Media,Inc.

  • Gravenstein Highway North,1005 号

  • 加利福尼亚州,Sebastopol,95472

  • 800-998-9938(美国或加拿大)

  • 707-829-0515(国际或本地)

  • 707-829-0104(传真)

我们为本书设有网页,列出勘误、示例和任何额外信息。您可以访问此页面https://oreil.ly/SparkwithR

要就本书发表评论或提出技术问题,请发送电子邮件至bookquestions@oreilly.com

关于我们的图书、课程、会议和新闻的更多信息,请访问我们的网站http://www.oreilly.com

在 Facebook 上找到我们:http://facebook.com/oreilly

在 Twitter 上关注我们:http://twitter.com/oreillymedia

在 YouTube 上关注我们:http://www.youtube.com/oreillymedia

第一章·介绍

你什么也不知道,琼·雪诺。

——伊格丽特

随着信息以指数速度增长,历史学家们将这段历史称为信息时代并不足为奇。数据收集速度的增加创造了新的机会,并肯定会创造更多。本章介绍了用于解决大规模数据挑战的工具。首先,它介绍了 Apache Spark 作为领先的工具,使我们能够处理大型数据集。在此背景下,我们介绍了专门设计简化数据分析的 R 计算语言。最后,我们介绍了sparklyr,这是一个将 R 和 Spark 合并为一个强大工具的项目,非常易于所有人使用。

第二章,入门介绍了在个人计算机上使 Spark 和 R 工作所需的先决条件、工具和步骤。您将学习如何安装和初始化 Spark,介绍常见操作,并完成您的第一个数据处理和建模任务。该章的目标是帮助任何人掌握开始解决大规模数据挑战所需的概念和工具,这些挑战直到最近只有少数组织才能访问。

然后,您将进入学习如何分析大规模数据,接着是构建能够预测趋势并发现大量信息中隐藏信息的模型。到那时,您将拥有在规模上执行数据分析和建模所需的工具。随后的章节将帮助您摆脱本地计算机,转向解决许多实际问题所需的计算集群。最后几章介绍了额外的主题,如实时数据处理和图分析,这些是您真正掌握在任何规模上分析数据的艺术所需的工具。本书的最后一章为您提供了考虑回馈 Spark 和 R 社区的工具和灵感。

我们希望这是一段让你享受的旅程,它将帮助你解决职业生涯中的问题,并推动世界做出更好的决策,从而造福我们所有人。

概述

作为人类,自从苏美尔人在公元前 3000 年左右发展出文字以来,我们一直在存储、检索、操纵和传播信息。根据所采用的存储和处理技术,可以区分出四个不同的发展阶段:前机械时代(公元前 3000 年至公元 1450 年)、机械时代(1450 年至 1840 年)、电机械时代(1840 年至 1940 年)和电子时代(1940 年至今)。¹

数学家乔治·斯蒂比茨在 1942 年首次使用 digital 这个词来描述快速的电脉冲,² 至今,我们仍将以电子方式存储的信息称为数字信息。相比之下,analog 信息代表我们以非电子方式存储的所有内容,如手写笔记、书籍、报纸等。

世界银行关于数字发展的报告提供了过去几十年存储的数字和模拟信息的估计。³ 报告指出,数字信息在 2003 年左右超过了模拟信息。那时,大约有 1000 万 TB 的数字信息,大致相当于今天的 1000 万个存储驱动器。然而,报告中更重要的发现是,我们的数字信息足迹正以指数速率增长。图 1-1 展示了这份报告的发现;请注意,每隔两年,世界的信息量增长了十倍。

许多公司怀揣着提供工具的雄心,可以搜索所有这些新的数字信息,而今天我们称之为搜索引擎,在搜索网页时使用。鉴于大量的数字信息,管理这种规模的信息是一个具有挑战性的问题。搜索引擎无法在单台计算机上存储支持网页搜索所需的所有网页信息。这意味着它们必须将信息分割成多个文件并存储在许多计算机上。这种方法被称为Google 文件系统,并在谷歌于 2003 年发表的研究论文中提出。⁴

全球信息存储能力

图 1-1. 全球信息存储能力

Hadoop

一年后,谷歌发布了一篇新论文,描述如何在谷歌文件系统上执行操作,这种方法后来被称为MapReduce。⁵ 如您所预料,MapReduce 中有两种操作:map 和 reduce。Map 操作 提供了一种任意转换每个文件为新文件的方式,而reduce 操作 则将两个文件合并。这两种操作都需要定制的计算机代码,但 MapReduce 框架会自动在许多计算机上执行它们。这两种操作足以处理网页上的所有数据,并且提供足够的灵活性从中提取有意义的信息。

例如,正如在图 1-2 中所示,我们可以使用 MapReduce 来统计存储在不同机器上的两个不同文本文件中的单词。映射操作将原始文件中的每个单词拆分,并输出一个新的单词计数文件,其中包含单词和计数的映射。Reduce 操作可以定义为获取两个单词计数文件并通过聚合每个单词的总数来组合它们;最后生成的文件将包含所有原始文件中单词的计数列表。

计算单词通常是最基本的 MapReduce 示例,但我们也可以将 MapReduce 用于更复杂和有趣的应用。例如,我们可以用它来在 Google 的PageRank算法中对网页进行排名,该算法根据指向某个网页的超链接数以及指向它的页面的等级来分配网页排名。

跨文件计算单词的 MapReduce 示例

图 1-2. 跨文件计算单词的 MapReduce 示例

在 Google 发布这些论文后,Yahoo 团队致力于实现 Google 文件系统和 MapReduce 作为一个开源项目。这个项目于 2006 年发布为Hadoop,其中 Google 文件系统实现为Hadoop 分布式文件系统(HDFS)。Hadoop 项目使得分布式基于文件的计算对更广泛的用户和组织可用,使得 MapReduce 在 Web 数据处理之外也变得有用。

尽管 Hadoop 支持在分布式文件系统上执行 MapReduce 操作,但仍需要每次运行数据分析时编写 MapReduce 操作的代码。为了改善这一繁琐的过程,Facebook 在 2008 年发布了Hive项目,为 Hadoop 引入了结构化查询语言(SQL)支持。这意味着现在可以在大规模上执行数据分析,而无需为每个 MapReduce 操作编写代码;相反,可以使用 SQL 编写通用的数据分析语句,这样更易于理解和编写。

Spark

2009 年,Apache Spark作为加州大学伯克利分校 AMPLab 的一个研究项目开始,旨在改进 MapReduce。具体来说,Spark 提供了比 MapReduce 更丰富的动词集,以便于优化在多台机器上运行的代码。Spark 还将数据加载到内存中,使操作比 Hadoop 在磁盘存储中更快。最早的结果之一显示,运行逻辑回归(一种我们将在第四章介绍的数据建模技术)时,Spark 的运行速度比 Hadoop 快了 10 倍,通过利用内存数据集实现了这一点。⁶ 原始研究出版物中也呈现了类似于图 1-3 的图表。

在 Hadoop 和 Spark 中的逻辑回归性能

图 1-3. 在 Hadoop 和 Spark 中的逻辑回归性能

尽管 Spark 以其内存性能而闻名,但它设计成通用执行引擎,既可在内存中运行,也可在磁盘上运行。例如,Spark 在大规模排序方面创造了新纪录,数据并非加载到内存中;相反,Spark 通过改进网络序列化、网络洗牌和有效利用 CPU 缓存显著提升了性能。如果您需要对大量数据进行排序,没有比 Spark 更快的系统了。

为了让您感受到 Spark 更快、更高效的程度,使用 Hadoop 对 100 TB 数据进行排序需要 72 分钟和 2,100 台计算机,而使用 Spark 仅需 23 分钟和 206 台计算机。此外,Spark 保持了云端排序记录,使其成为处理大数据集的最具成本效益的解决方案。

Hadoop 记录 Spark 记录
数据大小 102.5 TB 100 TB
经过时间 72 分钟 23 分钟
节点 2,100 206
核心数 50,400 6,592
磁盘 3,150 GB/s 618 GB/s
网络 10 GB/s 10 GB/s
排序速率 1.42 TB/min 4.27 TB/min
每节点排序速率 0.67 GB/min 20.7 GB/min

Spark 也比 Hadoop 更易于使用;例如,Hadoop 中的单词计数 MapReduce 示例大约需要 50 行代码,而在 Spark 中仅需 2 行代码。正如您所见,Spark 比 Hadoop 更快、更高效,更易于使用。

2010 年,Spark 作为一个开源项目发布,然后在 2013 年捐赠给 Apache 软件基金会。Spark 使用Apache 2.0许可证,允许您自由使用、修改和分发它。Spark 随后达到了超过 1,000 名贡献者,使其成为 Apache 软件基金会中最活跃的项目之一。

这提供了 Spark 发展历程的概述,现在我们可以正式介绍 Apache Spark,如其在项目的网站定义的那样:

Apache Spark 是用于大规模数据处理的统一分析引擎。

为了帮助我们理解 Apache Spark 的这一定义,我们将其分解如下:

统一的

Spark 支持许多库、集群技术和存储系统。

分析

分析是发现和解释数据以产生和传达信息的过程。

引擎

Spark 预计将是高效且通用的。

大规模

您可以将大规模解释为集群规模,一组连接在一起工作的计算机。

Spark 被描述为一个引擎,因为它是通用且高效的。它是通用的,因为它优化和执行通用代码;也就是说,您可以在 Spark 中编写任何类型的代码而没有限制。它是高效的,因为正如我们之前提到的,Spark 通过有效利用内存、网络和 CPU 加速数据处理算法,比其他技术快得多。

这使得 Spark 在许多分析项目中成为理想选择,例如像Netflix 电影排名蛋白质序列对齐,或分析 CERN 的高能物理

作为统一平台,Spark 预期支持许多集群技术和多个数据源,这些你可以在第六章和第八章学习。它还预期支持许多不同的库,如 Spark SQL、MLlib、GraphX 和 Spark Streaming;这些库可用于分析、建模、图处理和实时数据处理,分别介绍在图 1-4 中。总之,Spark 是一个平台,提供访问集群、数据源和库,用于大规模计算。

Spark 作为大规模数据处理的统一分析引擎

图 1-4. Spark 作为大规模数据处理的统一分析引擎

描述 Spark 为大规模意味着 Spark 适合解决需要多台机器处理的问题。例如,当数据无法容纳在单个磁盘驱动器或内存中时,Spark 是一个值得考虑的良好选择。然而,你也可以考虑将其应用于可能不是大规模的问题,但使用多台计算机可以加速计算的情况。例如,CPU 密集型模型和科学仿真也受益于在 Spark 中运行。

因此,Spark 擅长处理大规模数据处理问题,通常称为大数据(比传统数据集更庞大和复杂的数据集),但它也擅长处理大规模计算问题,称为大计算(使用大量 CPU 和内存资源以协调方式的工具和方法)。大数据通常需要大计算,但大计算并不一定需要大数据

大数据和大计算问题通常很容易识别——如果数据无法容纳在单台机器中,你可能面临大数据问题;如果数据适合单台机器但处理时间需要数天、数周或甚至数月,你可能面临大计算问题。

然而,还有第三个问题领域,既不需要大规模数据也不需要大规模计算,但使用像 Spark 这样的集群计算框架仍然能带来显著的好处。对于这第三个问题领域,有几个应用案例:

速度

假设你有一个 10 GB 的数据集,并且一个处理这些数据需要 30 分钟运行的流程——从任何角度来看,这既不是大计算也不是大数据。然而,如果你正在研究如何提高模型准确性的方法,将运行时间缩短到三分钟是一项显著的改进,这可以通过增加数据分析速度来实现有意义的进展和生产力增益。或者,你可能需要更快地处理数据——比如股票交易。尽管三分钟可能看起来足够快,但对于实时数据处理来说可能太慢了,你可能需要在几秒钟甚至几毫秒内处理数据。

多样性

你也可以有一个高效的流程,从多个来源收集数据到一个单一的位置,通常是一个数据库;这个过程可能已经在高效地运行,并且接近实时。这些过程被称为提取、转换、加载(ETL);数据从多个来源提取,转换为所需格式,然后加载到单一数据存储中。尽管这种方法多年来一直有效,但其缺点是添加新数据源的成本较高。因为系统是集中管理和严格控制的,进行更改可能会导致整个过程停止;因此,添加新数据源通常需要太长时间来实施。相反,你可以以其自然格式存储所有数据,并根据需要使用集群计算处理它,这种架构被称为数据湖。此外,以原始格式存储数据使你能够处理各种新文件格式,如图像、音频和视频,而无需考虑如何将它们适应传统的结构化存储系统中。

真实性

当使用许多数据源时,你可能会发现它们的数据质量差异很大,这需要特殊的分析方法来提高其准确性。例如,假设你有一个包含类似于旧金山、西雅图和波士顿等值的城市表。当数据包含类似于“Bston”的拼写错误条目时会发生什么?在关系数据库中,这个无效条目可能会被删除。然而,在所有情况下,删除值并不一定是最好的方法;你可能希望通过使用地理编码、交叉引用数据源或尝试最佳匹配来更正此字段。因此,理解和改善原始数据源的真实性可以带来更准确的结果。

如果我们将“volume”作为大数据的同义词,您将得到人们称之为大数据的四个 V的记忆法;其他人将其扩展为五个 V,甚至十个 V 的大数据。除了记忆法之外,集群计算正在以更加创新的方式使用,不少组织正在实验新的工作流程和传统上不常见的各种任务。大数据所带来的大部分炒作都集中在这个领域,严格来说,并不是在处理大数据,但仍然可以从使用为大数据和大计算设计的工具中获益。

我们希望本书能帮助您了解集群计算的机会和限制,特别是使用 Apache Spark 进行 R 编程的机会和限制。

R

R 计算语言源自于贝尔实验室创造的 S 语言。Rick Becker 在 useR 2016 中解释过,当时在贝尔实验室,计算是通过调用用 Fortran 语言编写的子程序来完成的,显然这种方式并不理想。S 计算语言被设计为一个接口语言,用来解决特定问题,而不需要担心其他语言(如 Fortran)。S 的创造者之一,John Chambers,在图 1-5 中展示了 S 的设计,旨在提供简化数据处理的接口;他的合作者在 useR! 2016 中展示了这幅图,这幅图也是创造 S 的灵感来源。

John Chambers 的接口语言图表(Rick Becker 在 useR 2016

图 1-5. John Chambers 的接口语言图表(Rick Becker 在 useR 2016

R 是 S 的现代和免费实现。根据统计计算的 R 项目

R 是用于统计计算和绘图的编程语言和自由软件环境。

在处理数据时,我们认为使用 R 有两个强有力的理由:

R 语言

R 是由统计学家为统计学家设计的,这意味着这是为非程序员设计的少数几种成功语言之一,因此学习 R 可能会感觉更自然。此外,因为 R 语言被设计为其他工具和语言的接口,所以可以更专注于理解数据,而不是计算机科学和工程学的细节。

R 社区

R 社区提供了由全面的 R 存档网络 (CRAN) 提供的丰富的软件包存档,允许您安装用于执行许多任务的即用软件包,其中包括高质量的数据操作、可视化和统计模型,其中许多仅在 R 中提供。此外,R 社区是一个热情活跃的群体,由才华横溢的个体组成,致力于帮助您成功。R 社区提供的许多软件包使得 R 显然成为统计计算的最佳选择。一些最常下载的 R 软件包包括:dplyr 用于数据操作,cluster 用于分析聚类,以及 ggplot2 用于数据可视化。图 1-6 通过绘制 CRAN 中 R 软件包的每日下载量来量化 R 社区的增长。

CRAN 软件包的每日下载量

图 1-6. CRAN 包的每日下载量

除了统计学,R 还在许多其他领域中使用。以下领域特别与本书相关:

数据科学

数据科学基于统计学和计算机科学的知识和实践,通过使用数据分析和建模技术将原始数据转化为理解⁷。统计方法为理解世界和进行预测提供了坚实的基础,而计算方法提供的自动化则使我们能够简化统计分析并使其更加易于接触。一些人主张应将统计学重新命名为数据科学;⁸ 然而,数据科学不仅仅局限于统计学,还融入了计算科学的进展。⁹ 本书介绍了统计学中常见的分析和建模技术,但应用于大数据集,这需要融合分布式计算的进展。

机器学习

机器学习使用统计学和计算机科学中的实践,但它更加专注于自动化和预测。例如,Arthur Samuel 在自动化一个计算机程序以玩跳棋时创造了术语机器学习。¹⁰ 虽然我们可以对特定游戏执行数据科学,但编写一个玩跳棋的程序需要我们自动化整个过程。因此,这属于机器学习的范畴,而不是数据科学。机器学习使得许多用户能够利用统计方法而无需意识到使用它们。机器学习的一个最早的重要应用是过滤垃圾邮件。在这种情况下,对每个电子邮件账户进行数据分析和建模是不可行的;因此,机器学习自动化了发现垃圾邮件并过滤掉它们的整个过程,而无需完全依赖用户。本书介绍了将数据科学工作流程转换为完全自动化机器学习方法的方法——例如,通过支持构建和导出可以在自动化环境中轻松重用的 Spark 流水线。

深度学习

深度学习建立在统计学、数据科学和机器学习的知识基础上,定义了部分受生物神经系统启发的模型。深度学习模型是在解决消失梯度问题后,通过逐层训练逐步演变而来的神经网络模型,¹¹ 并且已经被证明在图像和语音识别任务中非常有用。例如,在像 Siri、Alexa、Cortana 或 Google Assistant 这样的语音助手中,执行音频转文本的模型很可能基于深度学习模型。尽管图形处理单元(GPU)已成功用于训练深度学习模型,¹² 但有些数据集不能在单个 GPU 上处理。此外,深度学习模型需要大量数据,在进入单个 GPU 进行训练之前,需要在许多机器上对其进行预处理。本书并未直接涉及深度学习模型;然而,您可以使用本书中介绍的方法准备深度学习数据,并且在未来几年内,使用深度学习进行大规模计算将成为常见做法。事实上,最新版本的 Spark 已经引入了为在 Spark 中训练深度学习优化的执行模型。

在从事前述任何领域时,您将面临越来越大的数据集或越来越复杂的计算,这些计算执行起来速度缓慢,有时甚至在单台计算机上处理起来不可能。然而,重要的是要理解,Spark 并不需要成为所有计算问题的答案;相反,在面对 R 中的计算挑战时,使用以下技术可能同样有效:

抽样

首先尝试的方法是通过采样来减少处理的数据量。然而,我们必须通过应用 sound 统计原则正确地对数据进行采样。例如,在排序数据集中选择前几个结果是不够的;通过简单随机抽样,可能会存在代表不足的组;我们可以通过分层抽样来克服这些问题,但这又增加了正确选择类别的复杂性。本书的范围不包括教授如何正确进行统计抽样,但是关于这个主题有很多资源可供参考。

分析

您可以尝试理解为什么计算速度慢,并进行必要的改进。分析器是一种检查代码执行以帮助识别瓶颈的工具。在 R 中,R 分析器,profvis R 包和RStudio 分析器功能允许您轻松检索和可视化配置文件;然而,优化并不总是简单的。

扩展

通常可以通过购买更快或更强大的硬件(例如增加机器内存,升级硬盘驱动器,或者购买具有更多 CPU 的机器)来加速计算,这种方法称为扩展。然而,单台计算机能够扩展的限制通常是很严格的,即使具有大量 CPU,您也需要找到能够有效并行化计算的框架。

扩展出

最后,我们可以考虑将计算和存储分布到多台机器上。这种方法提供了最高程度的可扩展性,因为您可以潜在地使用任意数量的机器来执行计算。这种方法通常称为扩展出。然而,有效地跨多台机器分布计算是一项复杂的工作,特别是在没有使用专门的工具和框架(如 Apache Spark)的情况下。

这最后一点让我们更接近这本书的目的,即利用 Apache Spark 提供的分布式计算系统的能力来解决数据科学及相关领域中的有意义的计算问题,使用 R。

sparklyr

当你考虑到 Spark 提供的计算能力和 R 语言的易用性时,自然而然地希望它们可以无缝地一起工作。这也是 R 社区的期望:一个能够提供与其他 R 包兼容、易于使用,并且可以在 CRAN 上获取的接口,用于连接 Spark 的 R 包。基于这个目标,我们开始开发sparklyr。第一个版本,sparklyr 0.4,是在useR! 2016会议期间发布的。这个版本包括对dplyrDBI、使用MLlib进行建模的支持,以及一个可扩展的 API,支持像H2Orsparkling包等扩展。从那时起,通过sparklyr 0.50.60.70.80.91.0发布了许多新功能和改进。

正式地说,sparklyr是 Apache Spark 的 R 接口。它可以在 CRAN 上获取,工作方式与其他 CRAN 包相同,这意味着它与 Spark 版本无关,安装简便,服务于 R 社区,支持 R 社区的其他包和实践等等。它托管在GitHub,根据 Apache 2.0 许可,允许您克隆、修改并贡献回这个项目。

当考虑到谁应该使用sparklyr时,以下角色值得一提:

新用户

对于新用户,我们相信sparklyr提供了使用 Spark 的最简单方式。我们希望本书的前几章能够让你轻松上手,并为你长期的成功打下基础。

数据科学家

对于已经喜欢并使用 R 的数据科学家,sparklyr与许多其他 R 实践和包集成,如dplyrmagrittrbroomDBItibblerlang等,使您在使用 Spark 时感到如同在家。对于新接触 R 和 Spark 的人来说,sparklyr提供的高级工作流和低级可扩展性机制相结合,使其成为满足每位数据科学家需求和技能的高效环境。

专家用户

对于那些已经深入研究 Spark 并能够在 Scala 中编写代码的用户,考虑将您的 Spark 库作为 R 包提供给 R 社区。R 社区是一个多样化且技能娴熟的社区,能够很好地利用您的贡献,同时推动开放科学的发展。

我们撰写本书的目的是描述和教授 Apache Spark 与 R 之间令人兴奋的重叠部分。sparklyr 是一个将这两个社区、期望、未来方向、包和包扩展汇集在一起的 R 包。我们相信,通过本书,可以将 R 和 Spark 社区连接起来:向 R 社区展示 Spark 为何令人兴奋,向 Spark 社区展示 R 的优秀之处。这两个社区都在用一套不同的技能和背景解决非常相似的问题;因此,我们希望 sparklyr 能成为创新的沃土,对新手来说是一个友好的地方,对经验丰富的数据科学家来说是一个高效的环境,并且是一个能够让集群计算、数据科学和机器学习相结合的开放社区。

总结

本章将 Spark 描述为一个现代而强大的计算平台,R 作为一个易于使用且在统计方法方面有坚实基础的计算语言,而 sparklyr 则是一个连接这两种技术和社区的项目。在信息总量呈指数增长的今天,学会如何进行规模化数据分析将帮助您解决人类面临的问题和机遇。然而,在开始分析数据之前,第二章 将为您提供本书其余部分所需的工具。请务必仔细跟随每一个步骤,并花时间安装推荐的工具,我们希望这些工具能成为您熟悉和喜爱的资源。

¹ Laudon KC, Traver CG, Laudon JP (1996). “信息技术与系统。” Cambridge, MA: Course Technology.

² Ceruzzi PE (2012). 计算:简明历史. MIT Press.

³ 世界银行小组 (2016). 数据革命. World Bank Publications.

⁴ Ghemawat S, Gobioff H, Leung S (2003). “Google 文件系统。” 在 第十九届 ACM 操作系统原理研讨会 (OSDI) 中的论文集。 ISBN 1-58113-757-5.

⁵ Dean J, Ghemawat S (2004). “MapReduce:简化大型集群上的数据处理。” 在 USENIX 操作系统设计与实现研讨会 (OSDI) 中。

⁶ Zaharia M, Chowdhury M, Franklin MJ, Shenker S, Stoica I (2010). “Spark: 使用工作集的集群计算。” HotCloud, 10(10-10), 95.

⁷ Wickham H, Grolemund G (2016). 数据科学中的 R:导入、整理、转换、可视化和建模数据. O’Reilly Media, Inc.

⁸ 吴 CJ (1997). “统计学 = 数据科学?”

⁹ 克利夫兰 WS (2001). “数据科学:扩展统计学领域的行动计划?”

¹⁰ Samuel AL (1959). “使用跳棋游戏进行机器学习的一些研究。” IBM 研究与发展杂志, 3(3), 210–229.

¹¹ Hinton GE, Osindero S, Teh Y (2006). “深度信念网络的快速学习算法。” 神经计算, 18(7), 1527–1554.

¹² Krizhevsky A, Sutskever I, Hinton GE (2012). “使用深度卷积神经网络的 Imagenet 分类。” 在 神经信息处理系统进展 中,1097–1105.

第二章:入门

我一直想成为一个巫师。

——萨姆威尔·塔利

在阅读第一章后,您现在应该熟悉 Spark 可以帮助您解决的问题类型。并且应该清楚,当数据无法在单台机器上容纳或计算速度太慢时,Spark 通过利用多台计算机来解决问题。如果您是 R 的新手,结合数据科学工具如 ggplot2 进行可视化和 dplyr 进行数据转换,与 Spark 结合使用为进行大规模数据科学带来了一个充满希望的前景。我们也希望您能够兴奋地成为大规模计算的专家。

在本章中,我们将带您了解成为 Spark 熟练使用者所需的工具。我们鼓励您逐步阅读本章的代码,因为这将强迫您通过分析、建模、阅读和写入数据的过程。换句话说,在您完全沉浸于 Spark 的世界之前,您需要进行一些“上蜡,下蜡,反复”操作。

在第三章中,我们深入探讨分析,然后是建模,展示了在单一集群机器(即您的个人计算机)上使用的示例。随后的章节介绍了集群计算以及您将需要成功在多台机器上运行代码的概念和技术。

概览

从 R 开始使用 sparklyr 和本地集群来启动 Spark 是非常简单的,只需安装和加载 sparklyr 包,然后使用 sparklyr 安装 Spark;不过,我们假设您是从运行 Windows、macOS 或 Linux 的全新计算机开始,因此在连接到本地 Spark 集群之前,我们将为您讲解必备的先决条件。

尽管本章旨在帮助您准备在个人计算机上使用 Spark,但也有可能一些读者已经有了 Spark 集群或更喜欢从在线 Spark 集群开始。例如,Databricks 提供了一个免费社区版的 Spark,您可以轻松从 Web 浏览器访问。如果您选择这条路径,请直接跳至 “先决条件”,但确保为现有或在线 Spark 集群咨询适当的资源。

无论哪种方式,在完成先决条件后,您将首先学习如何连接到 Spark。然后,我们介绍您在本书的其余部分中将使用的最重要的工具和操作。我们不太强调教授概念或如何使用它们——在单独的一章中我们无法详尽解释建模或流处理。但是,通过本章的学习,您应该对未来的挑战性问题有一个简要的了解,并且可以自信地确保正确配置了工具以处理更具挑战性的问题。

您将使用的工具主要分为 R 代码和 Spark Web 界面。所有 Spark 操作都从 R 运行;然而,监视分布式操作的执行是从 Spark 的 Web 界面进行的,您可以从任何 Web 浏览器加载。然后断开与本地集群的连接,这很容易忘记但强烈建议在使用本地集群和共享 Spark 集群时执行!

我们通过向您介绍sparklyr实现的 RStudio 扩展功能来结束本章。然而,如果您倾向于使用 Jupyter Notebooks 或者您的集群已经配备了不同的 R 用户界面,可以放心使用纯 R 代码通过 RStudio 更轻松地使用 Spark。让我们继续并正确配置您的先决条件。

先决条件

R 可以在许多平台和环境中运行;因此,无论您使用 Windows、Mac 还是 Linux,第一步是从r-project.org安装 R;详细说明请参见“安装 R”。

大多数人使用带有提高生产力工具的编程语言;对于 R 语言来说,RStudio 就是这样一款工具。严格来说,RStudio 是一个集成开发环境(IDE),它也支持多个平台和环境。如果您还没有安装 RStudio,我们强烈建议您安装;详细信息请参见“安装 RStudio”。

提示

当使用 Windows 时,建议避免路径中带有空格的目录。如果从 R 运行getwd()返回带有空格的路径,请考虑使用setwd("path")切换到没有空格的路径,或者通过在没有空格的路径中创建 RStudio 项目来解决。

另外,由于 Spark 是使用 Scala 编程语言构建的,而 Scala 是由 Java 虚拟机(JVM)运行的,因此您还需要在系统上安装 Java 8。您的系统可能已经安装了 Java,但您仍应检查版本并根据“安装 Java”中的描述进行更新或降级。您可以使用以下 R 命令检查系统上安装的版本:

system("java -version")
java version "1.8.0_201"
Java(TM) SE Runtime Environment (build 1.8.0_201-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.201-b09, mixed mode)

你还可以使用JAVA_HOME环境变量通过运行Sys.setenv(JAVA_HOME = "path-to-java-8")来指向特定的 Java 版本;无论哪种方式,在安装sparklyr之前,请确保 Java 8 是 R 可用的版本。

安装sparklyr

与许多其他 R 包一样,您可以按照以下步骤从CRAN安装sparklyr

install.packages("sparklyr")

本书中的示例假设您正在使用最新版本的sparklyr。您可以通过运行以下命令验证您的版本是否与我们使用的版本一样新:

packageVersion("sparklyr")
[1] '1.0.2'

安装 Spark

首先加载sparklyr

library(sparklyr)

这样可以在 R 中使所有 sparklyr 函数可用,这非常有帮助;否则,你需要在每个 sparklyr 命令之前加上 sparklyr::

通过运行 spark_install() ,你可以轻松安装 Spark。这会在你的计算机上下载、安装并配置最新版本的 Spark;但是,因为我们是在 Spark 2.3 上编写本书的,你也应该安装这个版本,以确保你能够顺利地跟随所有提供的示例,没有任何意外:

spark_install("2.3")

你可以运行以下命令显示所有可安装的 Spark 版本:

spark_available_versions()
##   spark
## 1   1.6
## 2   2.0
## 3   2.1
## 4   2.2
## 5   2.3
## 6   2.4

你可以通过指定 Spark 版本和可选的 Hadoop 版本安装特定版本。例如,要安装 Spark 1.6.3,你可以运行:

spark_install(version = "1.6.3")

你还可以通过运行此命令检查已安装的版本:

spark_installed_versions()
  spark hadoop                              dir
7 2.3.1    2.7 /spark/spark-2.3.1-bin-hadoop2.7

Spark 安装路径被称为 Spark 的主目录,在 R 代码和系统配置设置中使用 SPARK_HOME 标识符定义。当你使用通过 sparklyr 安装的本地 Spark 集群时,此路径已知,并且不需要进行额外配置。

最后,要卸载特定版本的 Spark,你可以运行 spark_uninstall(),并指定 Spark 和 Hadoop 的版本,如下所示:

spark_uninstall(version = "1.6.3", hadoop = "2.6")
注意

在 macOS 和 Linux 下,默认安装路径是 ~/spark,在 Windows 下是 %LOCALAPPDATA%/spark。要自定义安装路径,你可以在运行 spark_install()spark_connect() 之前运行 options(spark.install.dir = "installation-path")

连接

需要强调的是,到目前为止,我们只安装了一个本地 Spark 集群。本地集群非常有助于开始、测试代码以及轻松排除故障。稍后的章节将解释如何找到、安装和连接具有多台机器的真实 Spark 集群,但在前几章中,我们将专注于使用本地集群。

要连接到这个本地集群,只需运行以下命令:

library(sparklyr)
sc <- spark_connect(master = "local", version = "2.3")
注意

如果你正在使用自己的或在线的 Spark 集群,请确保按照集群管理员或在线文档指定的方式连接。如果需要一些指引,你可以快速查看第七章,其中详细解释了如何连接到任何 Spark 集群。

master 参数标识了 Spark 集群中的“主”机器;这台机器通常称为驱动节点。在实际使用多台机器的真实集群时,你会发现大多数机器是工作机器,而其中一台是主节点。由于我们只有一个本地集群,并且只有一台机器,所以我们暂时默认使用 "local"

连接建立后,spark_connect() 将获取一个活动的 Spark 连接,大多数代码通常会将其命名为 sc;然后你将使用 sc 执行 Spark 命令。

如果连接失败,第七章包含一个可以帮助你解决连接问题的故障排除部分。

使用 Spark

现在你已连接,我们可以运行一些简单的命令。例如,让我们通过使用copy_to()mtcars数据集复制到 Apache Spark 中:

cars <- copy_to(sc, mtcars)

数据已复制到 Spark 中,但我们可以通过cars引用从 R 中访问它。要打印其内容,我们只需输入*cars*

cars
# Source: spark<mtcars> [?? x 11]
     mpg   cyl  disp    hp  drat    wt  qsec    vs    am  gear  carb
   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
 1  21       6  160    110  3.9   2.62  16.5     0     1     4     4
 2  21       6  160    110  3.9   2.88  17.0     0     1     4     4
 3  22.8     4  108     93  3.85  2.32  18.6     1     1     4     1
 4  21.4     6  258    110  3.08  3.22  19.4     1     0     3     1
 5  18.7     8  360    175  3.15  3.44  17.0     0     0     3     2
 6  18.1     6  225    105  2.76  3.46  20.2     1     0     3     1
 7  14.3     8  360    245  3.21  3.57  15.8     0     0     3     4
 8  24.4     4  147\.    62  3.69  3.19  20       1     0     4     2
 9  22.8     4  141\.    95  3.92  3.15  22.9     1     0     4     2
10  19.2     6  168\.   123  3.92  3.44  18.3     1     0     4     4
# … with more rows

恭喜!你已成功连接并将第一个数据集加载到 Spark 中。

让我们解释copy_to()中发生的情况。第一个参数sc给函数提供了一个对之前使用spark_connect()创建的活动 Spark 连接的引用。第二个参数指定要加载到 Spark 中的数据集。现在,copy_to()返回 Spark 中数据集的引用,R 会自动打印出来。每当 Spark 数据集被打印时,Spark 都会收集一些记录并显示给你。在这种特定情况下,该数据集仅包含描述汽车型号及其规格(如马力和预期每加仑英里数)的几行。

网络界面

大多数 Spark 命令都是从 R 控制台执行的;然而,监视和分析执行是通过 Spark 的网络界面完成的,如图 2-1 所示。这个界面是由 Spark 提供的一个 Web 应用程序,你可以通过运行以下命令访问:

spark_web(sc)

Apache Spark 网络界面

图 2-1. Apache Spark 网络界面

打印cars数据集收集了一些记录,以在 R 控制台中显示。你可以在 Spark 网络界面中看到启动了一个作业来从 Spark 中收集这些信息。你还可以选择存储标签,查看在 Spark 中缓存的mtcars数据集,如图 2-2 所示。

注意,这个数据集完全加载到内存中,如“内存占用”列所示,显示为 100%;因此,通过“内存大小”列,你可以准确查看此数据集使用了多少内存。

Apache Spark 网络界面上的存储标签

图 2-2. Apache Spark 网络界面上的存储标签

图 2-3 显示的执行者标签提供了集群资源的视图。对于本地连接,你将只找到一个活动执行者,为 Spark 分配了 2 GB 内存,并为计算可用了 384 MB。在第九章中,你将学习如何请求更多计算实例和资源,以及如何分配内存。

Apache Spark 网络界面上的执行者标签

图 2-3. Apache Spark 网络界面上的执行者标签

探索的最后一个选项卡是环境选项卡,如 Figure 2-4 所示;此选项卡列出了此 Spark 应用程序的所有设置,我们将在 Chapter 9 中研究。正如您将了解的那样,大多数设置不需要显式配置,但是要正确地按规模运行它们,您需要熟悉其中一些设置。

Apache Spark Web 界面上的环境选项卡

图 2-4. Apache Spark Web 界面上的环境选项卡

接下来,您将使用我们在 Chapter 3 中深入介绍的实践的一个小子集。

分析

当从 R 使用 Spark 分析数据时,您可以使用 SQL(结构化查询语言)或dplyr(数据操作语法)。您可以通过DBI包使用 SQL;例如,要计算我们的cars数据集中有多少条记录,我们可以运行以下命令:

library(DBI)
dbGetQuery(sc, "SELECT count(*) FROM mtcars")
  count(1)
1       32

当使用dplyr时,你会写更少的代码,通常比 SQL 更容易编写。这也正是为什么在本书中我们不会使用 SQL 的原因;然而,如果你精通 SQL,这对你来说是一个可行的选择。例如,在dplyr中计算记录更加紧凑且易于理解:

library(dplyr)
count(cars)
# Source: spark<?> [?? x 1]
      n
  <dbl>
1    32

通常,我们通常从 Spark 中使用dplyr开始分析数据,然后对行进行抽样并选择可用列的子集。最后一步是从 Spark 中收集数据,在 R 中进行进一步的数据处理,如数据可视化。让我们通过在 Spark 中选择、抽样和绘制cars数据集来执行一个非常简单的数据分析示例:

select(cars, hp, mpg) %>%
  sample_n(100) %>%
  collect() %>%
  plot()

在 Figure 2-5 的图中显示,随着车辆马力的增加,其每加仑英里数的燃油效率会降低。尽管这很有见地,但要数值化地预测增加马力如何影响燃油效率是困难的。建模可以帮助我们克服这一问题。

马力与每加仑英里数

图 2-5. 马力与每加仑英里数

建模

尽管数据分析可以帮助您深入了解数据,但构建描述和概括数据集的数学模型是非常强大的。在 Chapter 1 中,您了解到机器学习和数据科学领域利用数学模型进行预测和发现额外的见解。

例如,我们可以使用线性模型来近似燃油效率和马力之间的关系:

model <- ml_linear_regression(cars, mpg ~ hp)
model
Formula: mpg ~ hp

Coefficients:
(Intercept)          hp
30.09886054 -0.06822828

现在我们可以使用这个模型来预测原始数据集中没有的值。例如,我们可以为马力超过 250 的汽车添加条目,并可视化预测值,如 Figure 2-6 所示。

model %>%
  ml_predict(copy_to(sc, data.frame(hp = 250 + 10 * 1:10))) %>%
  transmute(hp = hp, mpg = prediction) %>%
  full_join(select(cars, hp, mpg)) %>%
  collect() %>%
  plot()

具有预测的马力与每加仑英里数

图 2-6. 具有预测的马力与每加仑英里数

即使前面的示例缺乏你在建模时应该使用的许多适当技术,它也是一个简单的示例,可以简要介绍 Spark 的建模能力。我们在第四章中介绍了所有 Spark 模型、技术和最佳实践。

数据

为简单起见,我们将 mtcars 数据集复制到了 Spark 中;然而,通常情况下数据并不会复制到 Spark 中。相反,数据是从现有的各种格式的数据源中读取的,如纯文本、CSV、JSON、Java 数据库连接(JDBC)等,我们会在第八章中详细讨论这些内容。例如,我们可以将我们的 cars 数据集导出为 CSV 文件:

spark_write_csv(cars, "cars.csv")

在实践中,我们将从分布式存储系统(如 HDFS)读取现有数据集,但我们也可以从本地文件系统读取:

cars <- spark_read_csv(sc, "cars.csv")

扩展

就像 R 以其充满活力的包作者社区而闻名一样,在较小的尺度上,许多 Spark 和 R 的扩展已经被编写并对您可用。第十章介绍了许多有趣的扩展,用于执行高级建模、图分析、深度学习数据集预处理等操作。

例如,sparkly.nested 扩展是一个 R 包,它扩展了 sparklyr 以帮助您管理包含嵌套信息的值。一个常见的用例涉及包含需要预处理才能进行有意义数据分析的嵌套列表的 JSON 文件。要使用这个扩展,我们首先需要按如下方式安装它:

install.packages("sparklyr.nested")

然后,我们可以使用 sparklyr.nested 扩展将所有汽缸数上的马力数据点进行分组:

sparklyr.nested::sdf_nest(cars, hp) %>%
  group_by(cyl) %>%
  summarise(data = collect_list(data))
# Source: spark<?> [?? x 2]
    cyl data
  <int> <list>
1     6 <list [7]>
2     4 <list [11]>
3     8 <list [14]>

即使嵌套数据使阅读变得更加困难,但在处理像 JSON 这样的嵌套数据格式时,使用 spark_read_json()spark_write_json() 函数是必需的。

分布式 R

对于少数情况下 Spark 中没有特定功能并且也没有开发扩展的情况,你可以考虑将你自己的 R 代码分布到 Spark 集群中。这是一个强大的工具,但它带来了额外的复杂性,因此你应该将其作为最后的选择。

假设我们需要在数据集的所有列上四舍五入所有值。一种方法是运行自定义的 R 代码,利用 R 的 round() 函数:

cars %>% spark_apply(~round(.x))
# Source: spark<?> [?? x 11]
     mpg   cyl  disp    hp  drat    wt  qsec    vs    am  gear  carb
   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
 1    21     6   160   110     4     3    16     0     1     4     4
 2    21     6   160   110     4     3    17     0     1     4     4
 3    23     4   108    93     4     2    19     1     1     4     1
 4    21     6   258   110     3     3    19     1     0     3     1
 5    19     8   360   175     3     3    17     0     0     3     2
 6    18     6   225   105     3     3    20     1     0     3     1
 7    14     8   360   245     3     4    16     0     0     3     4
 8    24     4   147    62     4     3    20     1     0     4     2
 9    23     4   141    95     4     3    23     1     0     4     2
10    19     6   168   123     4     3    18     1     0     4     4
# … with more rows

如果你是一个熟练的 R 用户,可能会很容易地为所有事情使用 spark_apply(),但请不要这样做!spark_apply() 是为 Spark 功能不足的高级用例设计的。您将学习如何在不必将自定义 R 代码分布到集群中的情况下进行正确的数据分析和建模。

流处理

虽然处理大型静态数据集是 Spark 的典型用例,实时处理动态数据集也是可能的,并且对某些应用程序而言是必需的。您可以将流式数据集视为一个连续接收新数据的静态数据源,例如股票市场行情。通常从 Kafka(一个开源流处理软件平台)或连续接收新数据的分布式存储中读取流式数据。

要尝试流式处理,让我们首先创建一个包含一些数据的 input/ 文件夹,作为该流的输入使用:

dir.create("input")
write.csv(mtcars, "input/cars_1.csv", row.names = F)

然后,我们定义一个流,处理来自 input/ 文件夹的输入数据,在 R 中执行自定义转换,并将输出推送到 output/ 文件夹:

stream <- stream_read_csv(sc, "input/") %>%
    select(mpg, cyl, disp) %>%
    stream_write_csv("output/")

一旦实时数据流开始,input/ 文件夹将被处理,并转换为output/ 文件夹下的一组新文件,其中包含新转换的文件。由于输入只包含一个文件,输出文件夹也将包含一个单独的文件,这是应用自定义 spark_apply() 转换的结果。

dir("output", pattern = ".csv")
[1] "part-00000-eece04d8-7cfa-4231-b61e-f1aef8edeb97-c000.csv"

到目前为止,这类似于静态数据处理;然而,我们可以继续向 input/ 位置添加文件,Spark 将自动并行处理数据。让我们再添加一个文件并验证它是否被自动处理:

# Write more data into the stream source
write.csv(mtcars, "input/cars_2.csv", row.names = F)

等待几秒钟,并验证数据是否被 Spark 流处理:

# Check the contents of the stream destination
dir("output", pattern = ".csv")
[1] "part-00000-2d8e5c07-a2eb-449d-a535-8a19c671477d-c000.csv"
[2] "part-00000-eece04d8-7cfa-4231-b61e-f1aef8edeb97-c000.csv"

然后,您应该停止流:

stream_stop(stream)

您可以使用 dplyr、SQL、Spark 模型或分布式 R 来实时分析数据流。在第十二章中,我们将详细介绍您可以执行的所有有趣的转换来分析实时数据。

日志

日志记录绝对比实时数据处理无聊得多;然而,这是您应该熟悉或了解的工具。日志只是 Spark 附加到集群中任务执行相关信息的文本文件。对于本地集群,我们可以通过运行以下命令检索所有最近的日志:

spark_log(sc)
18/10/09 19:41:46 INFO Executor: Finished task 0.0 in stage 5.0 (TID 5)...
18/10/09 19:41:46 INFO TaskSetManager: Finished task 0.0 in stage 5.0...
18/10/09 19:41:46 INFO TaskSchedulerImpl: Removed TaskSet 5.0, whose...
18/10/09 19:41:46 INFO DAGScheduler: ResultStage 5 (collect at utils...
18/10/09 19:41:46 INFO DAGScheduler: Job 3 finished: collect at utils...

或者,我们可以使用 filter 参数检索包含 sparklyr 的特定日志条目,如下所示:

spark_log(sc, filter = "sparklyr")
## 18/10/09 18:53:23 INFO SparkContext: Submitted application: sparklyr
## 18/10/09 18:53:23 INFO SparkContext: Added JAR...
## 18/10/09 18:53:27 INFO Executor: Fetching spark://localhost:52930/...
## 18/10/09 18:53:27 INFO Utils: Fetching spark://localhost:52930/...
## 18/10/09 18:53:27 INFO Executor: Adding file:/private/var/folders/...

大多数情况下,您不需要担心 Spark 日志,除非需要排除故障的失败计算;在这些情况下,日志是一个宝贵的资源需要注意。现在您知道了。

断开连接

对于本地集群(实际上,任何集群),在处理数据完成后,应通过运行以下命令断开连接:

spark_disconnect(sc)

这会终止与集群的连接以及集群任务。如果有多个 Spark 连接处于活动状态,或者连接实例 sc 不再可用,您也可以通过运行以下命令断开所有 Spark 连接:

spark_disconnect_all()

请注意,退出 R 或 RStudio,或重新启动您的 R 会话,也会导致 Spark 连接终止,从而终止未显式保存的 Spark 集群和缓存数据。

使用 RStudio

由于在 RStudio 中使用 R 非常普遍,sparklyr提供了 RStudio 扩展,帮助简化你在使用 Spark 时的工作流程并提高生产力。如果你对 RStudio 不熟悉,请快速查看“使用 RStudio”。否则,还有几个值得强调的扩展功能。

首先,不要从 RStudio 的 R 控制台使用spark_connect()开始新的连接,而是使用 Connections 选项卡中的 New Connection 操作,然后选择 Spark 连接,这将打开图 2-7 中显示的对话框。然后,你可以自定义版本并连接到 Spark,这将为你简单生成正确的spark_connect()命令,并在 R 控制台中执行。

RStudio 新的 Spark 连接对话框

图 2-7. RStudio 新的 Spark 连接对话框

连接到 Spark 后,RStudio 会在 Connections 选项卡中显示你可用的数据集,如图 2-8 所示。这是跟踪你现有数据集并轻松探索每个数据集的有用方式。

RStudio Connections 选项卡

图 2-8. RStudio Connections 选项卡

此外,活动连接提供以下自定义操作:

Spark UI

打开 Spark 的 Web 界面;一个spark_web(sc)的快捷方式。

日志

打开 Spark 的 Web 日志;一个spark_log(sc)的快捷方式。

SQL

打开一个新的 SQL 查询。有关DBI和 SQL 支持的更多信息,请参阅第三章。

帮助

在新的 Web 浏览器窗口中打开参考文档。

断开连接

断开与 Spark 的连接;一个spark_disconnect(sc)的快捷方式。

本书的其余部分将使用纯粹的 R 代码。你可以选择在 R 控制台、RStudio、Jupyter Notebooks 或任何支持执行 R 代码的工具中执行此代码,因为本书提供的示例在任何 R 环境中均可执行。

资源

虽然我们已经大力简化了入门流程,但还有许多额外的资源可以帮助你解决特定问题,帮助你了解更广泛的 Spark 和 R 社区,以便获取具体答案、讨论主题并与许多正在使用 Spark 的用户联系:

文档

托管在RStudio 的 Spark 网站上的文档站点应该是你学习使用 R 时了解更多关于 Spark 的首选站点。该文档随时更新,包括示例、参考函数以及许多其他相关资源。

博客

要及时了解主要的sparklyr公告,请关注RStudio 博客

社区

对于一般的sparklyr问题,你可以在RStudio 社区中发布标记为sparklyr的帖子。

Stack Overflow

对于一般的 Spark 问题,Stack Overflow是一个很好的资源;此外,还有许多关于sparklyr的专题

GitHub

如果你认为有什么需要修正的地方,请打开一个GitHub问题或发送一个拉取请求。

Gitter

对于紧急问题或保持联系,你可以在Gitter上与我们交流。

Recap

在本章中,你学习了与 Spark 一起工作所需的先决条件。你了解了如何使用spark_connect()连接到 Spark;使用spark_install()安装本地集群;加载简单数据集;启动 Web 界面,并分别使用spark_web(sc)spark_log(sc)显示日志;最后使用spark_disconnect()断开与 RStudio 的连接。我们通过介绍sparklyr提供的 RStudio 扩展来结束。

到目前为止,我们希望你已经准备好在接下来的两章中处理实际的 Spark 和 R 数据分析与建模问题。第三章将介绍数据分析,这是通过检查、清洗和转换数据来发现有用信息的过程。建模则是第四章的主题,虽然它是数据分析的一部分,但它需要一个独立的章节来全面描述和利用 Spark 中可用的建模功能。

第三章:分析

第一课:用尖锐的一端戳它们。

—琼·雪诺

前几章重点介绍了使用 R 与 Spark,使您快速上手,并鼓励您尝试基本的数据分析工作流程。然而,它们并没有恰当地介绍数据分析的含义,特别是在 Spark 中。它们呈现了您在本书中需要的工具——这些工具将帮助您花更多时间学习,而不是疑难解答。

本章介绍了在 R 中使用 Spark 进行数据分析的工具和概念。剧透警告:这些工具与您在纯 R 中使用的工具相同!这不仅仅是巧合;我们希望数据科学家可以在一个技术被隐藏的世界中使用他们熟悉和喜爱的 R 包,并且它们在 Spark 中“只是工作”!现在,我们还没有完全实现这一点,但我们也不远了。因此,在本章中,您将学习广泛使用的 R 包和实践,以执行数据分析——如dplyrggplot2、公式、rmarkdown等,这些在 Spark 中同样适用。

第四章 将专注于创建统计模型来预测、估计和描述数据集,但首先,让我们开始分析吧!

概述

在数据分析项目中,主要目标是理解数据试图“告诉我们”的内容,希望它能回答特定的问题。大多数数据分析项目遵循一套步骤,如图 3-1 所示。

正如图所示,我们首先将数据导入到我们的分析流程中,通过尝试不同的数据转换(如聚合)来整理它。然后,我们可视化数据以帮助我们感知关系和趋势。为了深入了解,我们可以对样本数据拟合一个或多个统计模型。这将帮助我们找出这些模式在应用新数据时是否成立。最后,结果会公开或私下与同事和利益相关者交流。

数据分析的一般步骤

图 3-1 数据分析的一般步骤

当处理不大规模数据集(即适合内存的数据集)时,我们可以使用 R 执行所有这些步骤,而无需使用 Spark。然而,当数据量过大无法放入内存或计算速度太慢时,我们可以略微调整此方法,引入 Spark。但是如何做呢?

对于数据分析,理想的方法是让 Spark 发挥其所擅长的功能。Spark 是一个并行计算引擎,能够在大规模上工作,并提供 SQL 引擎和建模库。您可以使用这些功能执行大多数与 R 相同的操作,包括数据选择、转换和建模。此外,Spark 还包括用于执行特殊计算工作的工具,如图分析、流处理等等。在此,我们将跳过那些非矩形数据集,并在后续章节中介绍它们。

在 Spark 内部可以执行数据导入整理建模。您也可以部分使用 Spark 进行可视化,这在本章稍后讨论。其思想是使用 R 告诉 Spark 要运行哪些数据操作,然后只将结果带入 R。如图 3-2 所示,理想的方法将计算推送到 Spark 集群,然后收集结果到 R 中。

Spark 在计算时收集结果

图 3-2. Spark 在计算时收集结果

sparklyr包有助于使用“推送计算,收集结果”的原则。它的大多数功能都是在 Spark API 调用的顶部进行封装。这使我们能够利用 Spark 的分析组件,而不是 R 的。例如,当您需要拟合线性回归模型时,您不会使用 R 熟悉的lm()函数,而是使用 Spark 的ml_linear_regression()函数。然后,这个 R 函数调用 Spark 来创建这个模型。图 3-3 描绘了这个具体的例子。

R 函数调用 Spark 功能

图 3-3. R 函数调用 Spark 功能

对于更常见的数据处理任务,sparklyrdplyr提供了后端支持。这意味着您可以在 R 中使用您已经熟悉的dplyr动词,然后sparklyrdplyr将这些操作转换为 Spark SQL 语句,通常比 SQL 语句更简洁且更易读(参见图 3-4)。因此,如果您已经熟悉 R 和dplyr,则没有什么新东西需要学习。这可能会感到有点令人失望——确实如此——但这也很好,因为您可以将精力集中在学习进行大规模计算所需的其他技能上。

dplyr 在 Spark 中写 SQL

图 3-4. dplyr 在 Spark 中写 SQL

为了在学习过程中进行实践,本章其余部分的代码使用在本地 Spark 主节点上运行的单个练习。这样,您可以在个人计算机上复制代码。确保sparklyr已经可以工作,如果您已经完成了第二章,那么应该可以工作。

本章将使用您可能尚未安装的包。因此,请首先确保通过运行以下命令安装这些包:

install.packages("ggplot2")
install.packages("corrr")
install.packages("dbplot")
install.packages("rmarkdown")

首先,加载sparklyrdplyr包,然后打开一个新的本地连接。

library(sparklyr)
library(dplyr)

sc <- spark_connect(master = "local", version = "2.3")

环境已准备就绪,因此我们下一个任务是导入稍后可以分析的数据。

导入

当使用 Spark 与 R 时,您需要以不同方式处理数据导入。通常,导入意味着 R 将读取文件并将其加载到内存中;当您使用 Spark 时,数据将导入到 Spark 而不是 R 中。在图 3-5 中,请注意数据源如何连接到 Spark 而不是连接到 R。

将数据导入 Spark 而非 R

图 3-5. 将数据导入 Spark 而非 R
注意

当您对大规模数据集执行分析时,大多数必要的数据已经可用于您的 Spark 集群(通常通过 Hive 表或直接访问文件系统向用户提供)。第八章将详细介绍这一点。

与其将所有数据导入 Spark,您可以请求 Spark 访问数据源而不导入它——这是基于速度和性能进行的决策。将所有数据导入 Spark 会产生一次性的前期成本,因为 Spark 需要等待数据加载才能进行分析。如果数据没有导入,通常在每次 Spark 操作时会产生成本,因为 Spark 需要从集群存储中检索子集,这通常是磁盘驱动器,速度比从 Spark 内存读取慢得多。关于这个话题的更多内容将在第九章中讨论。

让我们通过使用copy_to()mtcars导入 Spark 来准备一些数据会话;您还可以从多种不同的文件格式中导入分布式文件中的数据,我们将在第八章中讨论这些。

cars <- copy_to(sc, mtcars)
注意

在使用真实集群时,应使用copy_to()从 R 中仅传输小表;大数据传输应使用专门的数据传输工具进行。

现在可以访问数据到 Spark,并且可以轻松应用转换;下一节将介绍如何在 Spark 中运行转换以整理数据,使用dplyr

整理

数据整理使用转换来理解数据。通常被称为将数据从一种“原始”形式转换为另一种适合数据分析的格式的过程。

数据中常见的问题包括格式错误或缺失值以及具有多个属性的列,这可能需要修复,因为它们会阻碍你理解数据集。例如,“name”字段包含客户的姓和名。一个单独的列中包含两个属性(姓和名)。为了能够使用,我们需要转换“name”字段,改变它成为“first_name”和“last_name”字段。

在数据清理完成后,您仍然需要了解其内容的基础知识。其他转换如聚合可以帮助完成此任务。例如,请求所有客户平均余额的结果将返回一行和一列。该值将是所有客户的平均值。当我们查看单个或分组客户余额时,这些信息将为我们提供背景。

主要目标是尽可能使用 R 语法编写数据转换。这样可以避免在完成单一任务时需要在多个计算技术之间切换所带来的认知成本。在这种情况下,最好利用dplyr而不是编写 Spark SQL 语句进行数据探索。

在 R 环境中,cars 可以被视为本地 DataFrame,因此您可以使用 dplyr 动词。例如,我们可以通过使用 summarise_all() 找出所有列的均值:

summarise_all(cars, mean)
# Source: spark<?> [?? x 11]
    mpg   cyl  disp    hp  drat    wt  qsec    vs    am  gear  carb
  <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1  20.1  6.19  231\.  147\.  3.60  3.22  17.8 0.438 0.406  3.69  2.81

尽管这段代码与在没有 Spark 的情况下使用 dplyr 运行的代码完全相同,但在幕后发生了很多事情。数据并没有被导入到 R 中;相反,dplyr 将此任务转换为 SQL 语句,然后将其发送到 Spark。show_query() 命令使我们能够查看 sparklyrdplyr 创建并发送给 Spark 的 SQL 语句。我们还可以利用这个时间来介绍管道操作符 (%>%),这是 magrittr 包中的自定义操作符,将计算管道化到下一个函数的第一个参数中,使得数据分析变得更加易读:

summarise_all(cars, mean) %>%
  show_query()
<SQL>
SELECT AVG(`mpg`) AS `mpg`, AVG(`cyl`) AS `cyl`, AVG(`disp`) AS `disp`,
       AVG(`hp`) AS `hp`, AVG(`drat`) AS `drat`, AVG(`wt`) AS `wt`,
       AVG(`qsec`) AS `qsec`, AVG(`vs`) AS `vs`, AVG(`am`) AS `am`,
       AVG(`gear`) AS `gear`, AVG(`carb`) AS `carb`
FROM `mtcars`

显然,dplyr 比 SQL 更加简洁,但请放心,在使用 dplyr 时您不需要查看或理解 SQL。您的焦点可以继续集中在从数据中获取洞察力上,而不是弄清楚如何在 SQL 中表达给定的转换集。这里还有一个示例,按 transmission 类型对 cars 数据集进行分组:

cars %>%
  mutate(transmission = ifelse(am == 0, "automatic", "manual")) %>%
  group_by(transmission) %>%
  summarise_all(mean)
# Source: spark<?> [?? x 12]
  transmission   mpg   cyl  disp    hp  drat    wt  qsec    vs    am  gear  carb
  <chr>        <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 automatic     17.1  6.95  290\.  160\.  3.29  3.77  18.2 0.368     0  3.21  2.74
2 manmual       24.4  5.08  144\.  127\.  4.05  2.41  17.4 0.538     1  4.38  2.92

dplyr 提供的大多数数据转换操作都可以与本地 DataFrame 一起使用 Spark 连接。这意味着您可以先专注于学习 dplyr,然后在与 Spark 工作时重复使用这些技能。由 Hadley Wickham 和 Garrett Grolemund (O’Reilly) 合著的 R for Data Science 第五章是深入学习 dplyr 的好资源。如果您对 dplyr 的熟练掌握不成问题,我们建议您花些时间尝试对 cars 表使用不同的 dplyr 函数。

有时,我们可能需要执行 dplyrsparklyr 尚未提供的操作。与其将数据下载到 R 中,通常在 Spark 中有一个 Hive 函数可以完成我们所需的任务。下一节将介绍这种情况。

内置函数

Spark SQL 基于 Hive 的 SQL 约定和函数,可以使用 dplyr 调用所有这些函数。这意味着我们可以使用任何 Spark SQL 函数来执行可能不通过 dplyr 提供的操作。我们可以通过调用它们就像调用 R 函数一样访问这些函数。dplyr 不识别的函数会原样传递给查询引擎,而不是失败。这使我们在使用函数方面具有了很大的灵活性。

例如,percentile() 函数返回组中列的精确百分位数。该函数期望一个列名,以及一个单一的百分位值或百分位值数组。我们可以使用 dplyr 中的这个 Spark SQL 函数,如下所示:

summarise(cars, mpg_percentile = percentile(mpg, 0.25))
# Source: spark<?> [?? x 1]
  mpg_percentile
           <dbl>
1           15.4

在 R 中没有 percentile() 函数,因此 dplyr 将代码部分原样传递给生成的 SQL 查询:

summarise(cars, mpg_percentile = percentile(mpg, 0.25)) %>%
  show_query()
<SQL>
SELECT percentile(`mpg`, 0.25) AS `mpg_percentile`
FROM `mtcars_remote`

要传递多个值给 percentile(),我们可以调用另一个名为 array() 的 Hive 函数。在这种情况下,array() 的工作方式类似于 R 的 list() 函数。我们可以通过逗号分隔的方式传递多个值。Spark 的输出是一个数组变量,它作为一个列表变量列导入到 R 中:

summarise(cars, mpg_percentile = percentile(mpg, array(0.25, 0.5, 0.75)))
# Source: spark<?> [?? x 1]
  mpg_percentile
  <list>
1 <list [3]>

您可以使用 explode() 函数将 Spark 数组值结果分隔成它们自己的记录。为此,请在 mutate() 命令中使用 explode(),并传递包含百分位操作结果的变量:

summarise(cars, mpg_percentile = percentile(mpg, array(0.25, 0.5, 0.75))) %>%
  mutate(mpg_percentile = explode(mpg_percentile))
# Source: spark<?> [?? x 1]
  mpg_percentile
           <dbl>
1           15.4
2           19.2
3           22.8

我们在 “Hive 函数” 部分包含了所有 Hive 函数的详尽列表。快速浏览一下,了解您可以使用它们完成的各种操作范围。

相关性

非常常见的探索技术是计算和可视化相关性,我们经常计算它们以找出成对变量之间存在的统计关系。Spark 提供了计算整个数据集相关性的函数,并将结果作为 DataFrame 对象返回给 R:

ml_corr(cars)
# A tibble: 11 x 11
      mpg    cyl   disp     hp    drat     wt    qsec
    <dbl>  <dbl>  <dbl>  <dbl>   <dbl>  <dbl>   <dbl>
 1  1     -0.852 -0.848 -0.776  0.681  -0.868  0.419
 2 -0.852  1      0.902  0.832 -0.700   0.782 -0.591
 3 -0.848  0.902  1      0.791 -0.710   0.888 -0.434
 4 -0.776  0.832  0.791  1     -0.449   0.659 -0.708
 5  0.681 -0.700 -0.710 -0.449  1      -0.712  0.0912
 6 -0.868  0.782  0.888  0.659 -0.712   1     -0.175
 7  0.419 -0.591 -0.434 -0.708  0.0912 -0.175  1
 8  0.664 -0.811 -0.710 -0.723  0.440  -0.555  0.745
 9  0.600 -0.523 -0.591 -0.243  0.713  -0.692 -0.230
10  0.480 -0.493 -0.556 -0.126  0.700  -0.583 -0.213
11 -0.551  0.527  0.395  0.750 -0.0908  0.428 -0.656
# ... with 4 more variables: vs <dbl>, am <dbl>,
#   gear <dbl>, carb <dbl>

corrr R 包专门用于相关性。它包含友好的函数来准备和可视化结果。在该包中还包含了一个用于 Spark 的后端,因此当在 corrr 中使用 Spark 对象时,实际的计算也发生在 Spark 中。在后台,correlate() 函数运行 sparklyr::ml_corr(),因此在运行命令之前无需将任何数据收集到 R 中:

library(corrr)
correlate(cars, use = "pairwise.complete.obs", method = "pearson")
# A tibble: 11 x 12
   rowname     mpg     cyl    disp      hp     drat      wt
   <chr>     <dbl>   <dbl>   <dbl>   <dbl>    <dbl>   <dbl>
 1 mpg      NA      -0.852  -0.848  -0.776   0.681   -0.868
 2 cyl      -0.852  NA       0.902   0.832  -0.700    0.782
 3 disp     -0.848   0.902  NA       0.791  -0.710    0.888
 4 hp       -0.776   0.832   0.791  NA      -0.449    0.659
 5 drat      0.681  -0.700  -0.710  -0.449  NA       -0.712
 6 wt       -0.868   0.782   0.888   0.659  -0.712   NA
 7 qsec      0.419  -0.591  -0.434  -0.708   0.0912  -0.175
 8 vs        0.664  -0.811  -0.710  -0.723   0.440   -0.555
 9 am        0.600  -0.523  -0.591  -0.243   0.713   -0.692
10 gear      0.480  -0.493  -0.556  -0.126   0.700   -0.583
11 carb     -0.551   0.527   0.395   0.750  -0.0908   0.428
# ... with 5 more variables: qsec <dbl>, vs <dbl>,
#   am <dbl>, gear <dbl>, carb <dbl>

我们可以将结果传输给其他 corrr 函数。例如,shave() 函数将所有重复的结果转换为 NAs。再次强调,虽然这感觉像是使用现有 R 包的标准 R 代码,但 Spark 实际上在执行相关性计算。

另外,如图 Figure 3-6 所示,可以使用 rplot() 函数轻松地进行可视化,如下所示:

correlate(cars, use = "pairwise.complete.obs", method = "pearson") %>%
  shave() %>%
  rplot()

使用 rplot() 可视化相关性

Figure 3-6. 使用 rplot() 可视化相关性

看出哪些关系是正向的或负向的要容易得多:正向关系是灰色的,负向关系是黑色的。圆圈的大小表示它们的关系有多显著。数据可视化的威力在于它能让我们更容易地理解结果。下一节将扩展这个过程的步骤。

可视化

可视化是帮助我们在数据中找到模式的重要工具。当在图表中绘制 1000 条观察数据集时,我们更容易识别异常值,而不是从列表中读取它们。

R 在数据可视化方面非常出色。通过许多专注于此分析步骤的 R 包,它的绘图能力得到了扩展。不幸的是,大多数创建图表的 R 函数都依赖于已经在 R 本地内存中的数据,因此在使用 Spark 中的远程表时会失败。

可以从存在于 Spark 中的数据源创建 R 中的可视化。要理解如何做到这一点,让我们首先分解计算机程序如何构建图表。首先,程序获取原始数据并执行某种转换。然后,转换后的数据被映射到一组坐标上。最后,映射的值被绘制成图表。Figure 3-7 总结了每个步骤。

R 绘图的阶段

Figure 3-7. R 绘图的阶段

本质上,可视化的方法与数据整理的方法相同:将计算推送到 Spark,然后在 R 中收集结果进行绘制。正如在 Figure 3-8 中所示,准备数据的繁重工作(例如按组或区间聚合数据)可以在 Spark 中完成,然后将较小的数据集收集到 R 中。在 R 中,绘制图表变成了更基本的操作。例如,对于直方图,Spark 计算了区间,然后在 R 中使用简单的柱状图进行绘制,而不是直方图,因为无需重新计算区间。

使用 Spark 和 R 进行绘图

Figure 3-8. 使用 Spark 和 R 进行绘图

在使用 ggplot2 时,让我们应用这个概念模型。

使用 ggplot2

要使用 ggplot2 创建条形图,我们只需调用一个函数:

library(ggplot2)
ggplot(aes(as.factor(cyl), mpg), data = mtcars) + geom_col()

在这种情况下,mtcars 的原始数据被 自动 转换为三个离散聚合的数字。接下来,每个结果被映射到 xy 平面上。然后绘制图表。作为 R 用户,构建图表的所有阶段都为我们方便地抽象化了。

在 Spark 中,编码“推送计算,收集结果”的关键步骤有几个。首先,确保转换操作发生在 Spark 内部。在接下来的示例中,group_by()summarise() 将在 Spark 内部运行。第二个步骤是在数据被转换后将结果带回 R。确保按照转换然后收集的顺序进行;如果先运行 collect(),R 将尝试从 Spark 中摄取整个数据集。根据数据大小,收集所有数据将减慢系统速度,甚至可能使系统崩溃。

car_group <- cars %>%
  group_by(cyl) %>%
  summarise(mpg = sum(mpg, na.rm = TRUE)) %>%
  collect() %>%
  print()
# A tibble: 3 x 2
    cyl   mpg
  <dbl> <dbl>
1     6  138.
2     4  293.
3     8  211.

在这个例子中,现在数据已经预先聚合并收集到 R 中,只传递了三条记录给绘图函数:

ggplot(aes(as.factor(cyl), mpg), data = car_group) +
  geom_col(fill = "#999999") + coord_flip()

Figure 3-9 展示了生成的图表。

在 Spark 中进行聚合绘图

Figure 3-9. 在 Spark 中进行聚合绘图

使用此方法可以制作任何其他 ggplot2 可视化;但是,这超出了本书的范围。相反,我们建议您阅读 R Graphics Cookbook,作者 Winston Chang(O’Reilly),以了解适用于 Spark 的其他可视化技术。现在,在可视化之前简化此转换步骤,dbplot 包提供了一些自动化聚合在 Spark 中使用的准备就绪的可视化。

使用 dbplot

dbplot 包提供了用于处理远程数据的辅助函数。R 代码 dbplot 被编写成可以转换为 Spark。然后使用这些结果使用 ggplot2 包创建图表,数据转换和绘图都由一个单一函数触发。

dbplot_histogram() 函数使 Spark 计算 bins 和每个 bin 的计数,并输出一个 ggplot 对象,我们可以通过添加更多步骤来进一步优化绘图对象。dbplot_histogram() 还接受一个 binwidth 参数来控制用于计算 bins 的范围:

library(dbplot)

cars %>%
dbplot_histogram(mpg, binwidth = 3) +
labs(title = "MPG Distribution",
     subtitle = "Histogram over miles per gallon")

图 3-10 展示了生成的绘图。

由 dbplot 创建的直方图

图 3-10. 由 dbplot 创建的直方图

直方图提供了分析单个变量的绝佳方式。要分析两个变量,通常使用散点图或光栅图。

散点图用于比较两个连续变量之间的关系。例如,散点图将显示汽车重量与其燃油消耗之间的关系。图 3-11 中的图表显示,重量越大,燃油消耗越高,因为点几乎聚集成一条从左上到右下的线。以下是生成该图表的代码:

ggplot(aes(mpg, wt), data = mtcars) +
  geom_point()

Spark 中的散点图示例

图 3-11. Spark 中的散点图示例

然而,对于散点图来说,无论如何“将计算推向” Spark 都无法解决这个问题,因为数据必须以单个点的形式绘制。

最佳的替代方案是找到一种能够以易于感知且“物理”绘制的方式表示 x/y 关系和浓度的图表类型。光栅图可能是最佳答案。光栅图返回一个 x/y 位置网格和给定聚合结果,通常由方块的颜色表示。

您可以使用 dbplot_raster() 在 Spark 中创建类似散点图的图表,同时只检索(收集)远程数据集的小部分:

dbplot_raster(cars, mpg, wt, resolution = 16)

如 图 3-12 所示,生成的绘图返回的网格不超过 5 x 5。这限制了需要收集到 R 中的记录数为 25。

使用 Spark 创建的光栅图

图 3-12. 使用 Spark 的光栅图
提示

您还可以使用 dbplot 通过其他方式检索原始数据并进行可视化;要检索聚合数据但不是绘图,请使用 db_compute_bins()db_compute_count()db_compute_raster()db_compute_boxplot()

虽然可视化是不可或缺的,但您可以通过统计模型来补充数据分析,以深入了解我们的数据。下一节描述了如何准备数据以用于 Spark 建模。

模型

接下来的两章完全专注于建模,因此我们不打算在本章中详细介绍建模,而是想覆盖在进行数据分析时如何与模型进行交互。

首先,分析项目经历了多次转换和模型以找到答案。这就是为什么我们在图 3-2 中介绍了第一个数据分析图表,展示了可视化、数据处理和建模的循环——我们知道你不仅在 R 中,甚至在使用 Spark 时也不仅仅停留在建模。

因此,理想的数据分析语言使您能够在每个数据处理-可视化-建模迭代中快速调整。幸运的是,使用 Spark 和 R 时就是这种情况。

要说明在 Spark 中迭代数据处理和建模有多容易,请考虑以下示例。我们将从针对所有特征执行线性回归开始,并预测每加仑英里数:

cars %>%
  ml_linear_regression(mpg ~ .) %>%
  summary()
Deviance Residuals:
    Min      1Q  Median      3Q     Max
-3.4506 -1.6044 -0.1196  1.2193  4.6271

Coefficients:
(Intercept)         cyl        disp          hp        drat          wt
12.30337416 -0.11144048  0.01333524 -0.02148212  0.78711097 -3.71530393
      qsec          vs          am        gear        carb
0.82104075  0.31776281  2.52022689  0.65541302 -0.19941925

R-Squared: 0.869
Root Mean Squared Error: 2.147

此时,试验不同特征非常容易,我们可以简单地更改 R 公式,例如从mpg ~ .mpg ~ hp + cyl,仅使用马力和汽缸作为特征:

cars %>%
  ml_linear_regression(mpg ~ hp + cyl) %>%
  summary()
Deviance Residuals:
    Min      1Q  Median      3Q     Max
-4.4948 -2.4901 -0.1828  1.9777  7.2934

Coefficients:
(Intercept)          hp         cyl
 36.9083305  -0.0191217  -2.2646936

R-Squared: 0.7407
Root Mean Squared Error: 3.021

另外,使用其他类型的模型进行迭代也非常容易。以下示例将线性模型替换为广义线性模型:

cars %>%
  ml_generalized_linear_regression(mpg ~ hp + cyl) %>%
  summary()
Deviance Residuals:
    Min      1Q  Median      3Q     Max
-4.4948 -2.4901 -0.1828  1.9777  7.2934

Coefficients:
(Intercept)          hp         cyl
 36.9083305  -0.0191217  -2.2646936

(Dispersion parameter for gaussian family taken to be 10.06809)

   Null  deviance: 1126.05 on 31 degress of freedom
Residual deviance: 291.975 on 29 degrees of freedom
AIC: 169.56

通常,在拟合模型之前,您需要使用多个dplyr转换来准备数据,以便模型能够有效使用。为了确保模型能够尽可能高效地拟合,您应该在拟合之前缓存数据集,如下所述。

缓存

本章的示例是使用一个非常小的数据集构建的。在实际场景中,会使用大量数据来进行建模。如果需要先转换数据,则数据的数量可能会对 Spark 会话产生重大影响。在拟合模型之前,将所有转换的结果保存在新表中,并加载到 Spark 内存中是个好主意。

compute()命令可以接受dplyr命令的末尾,并将结果保存到 Spark 内存中:

cached_cars <- cars %>%
  mutate(cyl = paste0("cyl_", cyl)) %>%
  compute("cached_cars")
cached_cars %>%
  ml_linear_regression(mpg ~ .) %>%
  summary()
Deviance Residuals:
     Min       1Q   Median       3Q      Max
-3.47339 -1.37936 -0.06554  1.05105  4.39057

Coefficients:
(Intercept) cyl_cyl_8.0 cyl_cyl_4.0        disp          hp        drat
16.15953652  3.29774653  1.66030673  0.01391241 -0.04612835  0.02635025
          wt        qsec          vs          am       gear        carb
 -3.80624757  0.64695710  1.74738689  2.61726546 0.76402917  0.50935118

R-Squared: 0.8816
Root Mean Squared Error: 2.041

随着从数据中获得更多见解,可能会提出更多问题。这就是为什么我们希望通过数据处理、可视化和建模的迭代多次来获得更多见解。每次迭代应该为我们解读数据提供增量见解。当我们达到满意的理解水平时,我们将准备好分享分析结果。这是下一节的主题。

沟通

清楚地传达分析结果同样重要——正如分析工作本身一样重要!公众、同事或利益相关者需要理解您发现的内容及其意义。

要有效地进行沟通,我们需要使用报告和演示文稿等工件;这些是我们可以使用R Markdown在 R 中创建的常见输出格式。

R Markdown 文档允许你将叙述文本和代码交织在一起。多种输出格式提供了非常强大的理由去学习和使用 R Markdown。有许多可用的输出格式,如 HTML、PDF、PowerPoint、Word、Web 幻灯片、网站、书籍等等。

大多数这些输出格式都包含在 R Markdown 的核心 R 包 knitrrmarkdown 中。你可以通过其他的 R 包扩展 R Markdown。例如,本书就是使用了 bookdown 包提供的扩展来撰写的 R Markdown。深入了解 R Markdown 的最佳资源是官方书籍。¹

在 R Markdown 中,一个单一的文档可能以不同的格式呈现。例如,你可以通过更改报告本身的设置,将同一报告渲染为 HTML 或 PDF 文件。反之亦然,多种类型的文档也可以渲染为相同的输出。例如,一个演示文稿和一个报告都可以渲染为 HTML。

创建一个新的使用 Spark 作为计算引擎的 R Markdown 报告很容易。在顶部,R Markdown 需要一个 YAML 头部。第一行和最后一行是三个连字符 (---)。介于连字符之间的内容因文档类型不同而异。YAML 头部中唯一必需的字段是 output 值。R Markdown 需要知道需要将报告渲染成什么样的输出。这个 YAML 头部称为 frontmatter。在 frontmatter 之后是代码块的部分,称为 代码块。这些代码块可以与叙述文本交织在一起。当使用 Spark 和 R Markdown 时没有特别需要注意的地方;一切都像往常一样。

由于 R Markdown 文档是自包含的且旨在可复制,渲染文档之前,我们应首先断开与 Spark 的连接以释放资源:

spark_disconnect(sc)

下面的示例展示了创建一个完全可复制的报告并使用 Spark 处理大规模数据集是多么容易。叙述性文字、代码以及最重要的是代码输出都记录在生成的 HTML 文件中。你可以复制并粘贴以下代码到一个文件中。将文件保存为 .Rmd 扩展名,并选择任何你喜欢的名称:

---
title: "mtcars analysis"
output:
  html_document:
    fig_width: 6
    fig_height: 3
---
```{r, setup, include = FALSE}

library(sparklyr)

library(dplyr)

sc <- spark_connect(master = "local", version = "2.3")

cars <- copy_to(sc, mtcars)

Visualize

Aggregate data in Spark, visualize in R.


library(ggplot2)

cars %>%

group_by(cyl) %>% summarise(mpg = mean(mpg)) %>%

ggplot(aes(cyl, mpg)) + geom_bar(stat="identity")

Model

The selected model was a simple linear regression that
uses the weight as the predictor of MPG


cars %>%

ml_linear_regression(wt ~ mpg) %>%

summary()


spark_disconnect(sc)


要编织此报告,请将文件保存为 *.Rmd* 扩展名,例如 *report.Rmd*,并从 R 中运行 `render()`。输出应如 图 3-13 所示。

rmarkdown::render("report.Rmd")


![R Markdown HTML 输出](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0313.png)

###### 图 3-13\. R Markdown HTML 输出

现在,您可以轻松分享此报告,而查看者无需使用 Spark 或 R 来阅读和消化其内容;它只是一个自包含的 HTML 文件,在任何浏览器中打开都很容易。

将报告的见解提炼到许多其他输出格式中也很常见。切换非常容易:在前言的顶部,将`output`选项更改为`powerpoint_presentation`、`pdf_document`、`word_document`或类似选项。或者,您甚至可以从同一份报告中生成多个输出格式:


title: "mtcars analysis"
output:
word_document: default
pdf_document: default
powerpoint_presentation: default


结果将是一个 PowerPoint 演示文稿、一个 Word 文档和一个 PDF。所有原始 HTML 报告中显示的信息都是在 Spark 中计算并在 R 中渲染的。您可能需要编辑 PowerPoint 模板或代码块的输出。

这个简单示例展示了从一种格式转换到另一种格式有多么容易。当然,需要在 R 用户的一侧进行更多编辑,以确保幻灯片只包含相关信息。主要问题在于,不需要学习不同的标记或代码约定就能从一个文档转换到另一个文档。

# 总结

本章介绍了使用 R 和 Spark 进行数据分析的基础知识。许多介绍的技术看起来与仅使用 R 而不是 Spark 非常相似,虽然有些平淡,但确实是帮助已熟悉 R 的用户轻松过渡到 Spark 的正确设计。对于不熟悉 R 的用户,本章还是对 R 中一些最流行(和有用!)包的简要介绍。

现在应该很明显,R 和 Spark 一起是一个强大的组合——一个大规模计算平台,以及一个非常强大的 R 包生态系统,使其成为一个理想的分析平台。

在 Spark 中使用 R 进行分析时,请记住将计算推送到 Spark 并专注于在 R 中收集结果。这种范式应该为通过在各种输出中分享结果来进行数据处理、可视化和沟通设立一个成功的方法。

第四章将深入探讨如何使用更有趣的数据集(比约会数据集更有趣的是什么?)在 Spark 中构建统计模型。您还将从本章的简要建模部分学习更多技术,我们甚至没有提及。

¹ Xie Allaire G(2018)。《R Markdown: The Definite Guide》,第 1 版。CRC Press。


# 第四章:模型化

> 多年来,我一直相信你的幻象和预言。
> 
> —斯坦尼斯·拜拉席恩

在第三章中,您学习了如何使用 Spark 扩展数据分析到大数据集。在本章中,我们详细介绍了在 Spark 中构建预测模型所需的步骤。我们探索了`MLlib`,这是 Spark 的一个组件,允许您编写高级代码在分布式数据上执行预测建模,并在特征工程和探索性数据分析的上下文中使用数据整理。

我们将从介绍 Spark 中的建模和本章节中将使用的数据集开始。然后我们展示一个包括探索性数据分析、特征工程和模型构建的监督学习工作流程。接着,我们将演示一个使用非结构化文本数据的无监督主题建模示例。请记住,我们的目标是展示在大数据上执行数据科学任务的各种技术,而不是进行严格和连贯的分析。在本章中,还有许多其他 Spark 中可用但本章未涵盖的模型,但到了本章结束时,您将具备自行尝试其他模型的正确工具。

虽然手动预测数据集通常是一个合理的方法(“手动”指的是将数据集导入 Spark 并使用已拟合的模型来丰富或预测值),但这引出了一个问题,我们是否可以将这一过程自动化到任何人都可以使用的系统中?例如,我们如何构建一个系统,可以自动识别电子邮件是否为垃圾邮件,而无需手动分析每个电子邮件账户?第五章 提供了用于通过流水线自动化数据分析和建模的工具,但要实现这一目标,我们首先需要了解如何“手动”训练模型。

# 概览

R 接口到 Spark 提供了模型算法,这对 R 用户来说应该是熟悉的。例如,我们已经使用了 `ml_linear_regression(cars, mpg ~ .)`,但我们同样可以运行 `ml_logistic_regression(cars, am ~ .)`。

请花点时间查看本书附录中包含的`MLlib`函数的长列表;快速浏览此列表可以看出,Spark 支持决策树、梯度提升树、加速失效时间生存回归、等距回归、*K*-均值聚类、高斯混合聚类等等。

如您所见,Spark 提供了广泛的算法和特征转换器,在这里我们涉及了功能的代表部分。对预测建模概念的全面讨论超出了本书的范围,因此我们建议结合 Hadley Wickham 和 Garrett Grolemund G(O'Reilly)的[*R for Data Science*](https://r4ds.had.co.nz/)以及*Feature Engineering and Selection: A Practical Approach for Predictive Models*¹,本章中的一些示例和可视化有时候是直接引用的。

本章重点介绍预测建模,因为 Spark 的目标是实现机器学习,而不是统计推断。机器学习通常更关注预测未来,而不是推断生成数据的过程²,然后用于创建自动化系统。机器学习可分为*监督学习*(预测建模)和*无监督学习*。在监督学习中,我们试图学习一个函数,将从数据集中的(x, y)示例映射到 Y。在无监督学习中,我们只有 X 而没有 Y 标签,因此我们尝试学习有关 X 结构的信息。监督学习的一些实际用例包括预测明天的天气、确定信用卡交易是否欺诈以及为您的汽车保险报价。无监督学习的例子包括自动分组个人照片、基于购买历史对客户进行分段以及文档聚类。

`sparklyr` 中的 ML 接口旨在最小化从本地内存中的本机 R 工作流程到集群的认知工作量,以及反向过程。虽然 Spark 生态系统非常丰富,但 CRAN 中仍然有大量的软件包,其中一些实现了您可能需要的功能。此外,您可能希望利用您在 R 中的技能和经验来保持生产力。我们在第三章中学到的内容在这里同样适用——重要的是要跟踪您执行计算的位置,并根据需要在集群和您的 R 会话之间移动。

本章的示例使用了[`OkCupid`数据集](https://oreil.ly/Uv9r_)³。该数据集包含来自在线约会网站的用户资料数据,包括性别、职业等生物特征以及与个人兴趣相关的自由文本字段。数据集中约有 60,000 个用户资料,可以轻松地在现代笔记本电脑的内存中运行,不被视为“大数据”,因此您可以轻松地在本地模式下运行 Spark 进行跟踪。

您可以按以下方式下载此数据集:

download.file(
"https://github.com/r-spark/okcupid/raw/master/profiles.csv.zip",
"okcupid.zip")

unzip("okcupid.zip", exdir = "data")
unlink("okcupid.zip")


我们不建议对该数据集进行抽样,因为模型将远不如丰富;但是,如果您的硬件资源有限,您可以按以下方式对其进行抽样:

profiles <- read.csv("data/profiles.csv")
write.csv(dplyr::sample_n(profiles, 10³),
"data/profiles.csv", row.names = FALSE)


###### 注意

本章中的示例使用了小型数据集,以便您可以轻松在本地模式下跟踪。 实际上,如果您的数据集在本地机器的内存中能轻松容纳,您可能最好使用一种高效的、非分布式的建模算法实现。 例如,您可能想使用`ranger`包,而不是`ml_random_forest_classifier()`。

此外,为了跟进,您需要安装一些额外的包:

install.packages("ggmosaic")
install.packages("forcats")
install.packages("FactoMineR")


为了说明示例的动机,我们考虑以下问题:

> 预测某人目前是否在积极工作——即不是退休、学生或失业。

接下来,我们来探索这个数据集。

# 探索性数据分析

在预测建模的背景下,探索性数据分析(EDA)是查看数据摘录和摘要的练习。 EDA 阶段的具体目标受业务问题的启发,但以下是一些常见目标:

+   检查数据质量;确认缺失值的含义和普遍性,并将统计数据与现有控制进行调和。

+   了解变量之间的单变量关系。

+   对要包括的变量进行初步评估,以及对它们要进行的变换进行评估。

首先,我们连接到 Spark,加载库,并读取数据:

library(sparklyr)
library(ggplot2)
library(dbplot)
library(dplyr)

sc <- spark_connect(master = "local", version = "2.3")

okc <- spark_read_csv(
sc,
"data/profiles.csv",
escape = """,
memory = FALSE,
options = list(multiline = TRUE)
) %>%
mutate(
height = as.numeric(height),
income = ifelse(income == "-1", NA, as.numeric(income))
) %>%
mutate(sex = ifelse(is.na(sex), "missing", sex)) %>%
mutate(drinks = ifelse(is.na(drinks), "missing", drinks)) %>%
mutate(drugs = ifelse(is.na(drugs), "missing", drugs)) %>%
mutate(job = ifelse(is.na(job), "missing", job))


我们在这里指定了`escape = "\""`和`options = list(multiline = TRUE)`,以适应论文字段中嵌入的引号字符和换行符。 我们还将`height`和`income`列转换为数值类型,并重新编码字符串列中的丢失值。 请注意,可能需要多次尝试指定不同的参数才能正确进行初始数据摄取,有时在您在建模过程中了解更多关于数据后,您可能需要重新访问这一步。

现在,我们可以使用`glimpse()`来快速查看我们的数据:

glimpse(okc)


Observations: ??
Variables: 31
Database: spark_connection
$ age 22, 35, 38, 23, 29, 29, 32, 31, 24, 37, 35…
$ body_type "a little extra", "average", "thin", "thin…
$ diet "strictly anything", "mostly other", "anyt…
$ drinks "socially", "often", "socially", "socially…
$ drugs "never", "sometimes", "missing", "missing"…
$ education "working on college/university", "working …
$ essay0 "about me:
\n
\ni would love to …
$ essay1 "currently working as an international age…
$ essay2 "making people laugh.
\nranting about…
$ essay3 "the way i look. i am a six foot half asia…
$ essay4 "books:
\nabsurdistan, the republic, …
$ essay5 "food.
\nwater.
\ncell phone.<br…
$ essay6 "duality and humorous things", "missing", …
$ essay7 "trying to find someone to hang out with. …
$ essay8 "i am new to california and looking for so…
$ essay9 "you want to be swept off your feet!

$ ethnicity "asian, white", "white", "missing", "white…
$ height 75, 70, 68, 71, 66, 67, 65, 65, 67, 65, 70…
$ income NaN, 80000, NaN, 20000, NaN, NaN, NaN, NaN…
$ job "transportation", "hospitality / travel", …
$ last_online "2012-06-28-20-30", "2012-06-29-21-41", "2…
$ location "south san francisco, california", "oaklan…
$ offspring "doesn’t have kids, but might want t…
$ orientation "straight", "straight", "straight", "strai…
$ pets "likes dogs and likes cats", "likes dogs a…
$ religion "agnosticism and very serious about it", "…
$ sex "m", "m", "m", "m", "m", "m", "f", "f", "f…
$ sign "gemini", "cancer", "pisces but it doesn&r…
$ smokes "sometimes", "no", "no", "no", "no", "no",…
$ speaks "english", "english (fluently), spanish (p…
$ status "single", "single", "available", "single",…


现在,我们将我们的响应变量作为数据集中的一列,并查看其分布:

okc <- okc %>%
mutate(
not_working = ifelse(job %in% c("student", "unemployed", "retired"), 1 , 0)
)

okc %>%
group_by(not_working) %>%
tally()


Source: spark<?> [?? x 2]

not_working n

1 0 54541
2 1 5405


在我们进一步进行之前,让我们将数据进行初步分割,分为训练集和测试集,并将后者放在一边。 在实践中,这是一个关键的步骤,因为我们希望在建模过程的最后留出一个留置集,以评估模型性能。 如果我们在 EDA 期间包含整个数据集,测试集的信息可能会“泄漏”到可视化和摘要统计数据中,并且偏向我们的模型构建过程,即使数据并没有直接用在学习算法中。 这将损害我们性能指标的可信度。 我们可以使用`sdf_random_split()`函数轻松地将数据进行分割:

data_splits <- sdf_random_split(okc, training = 0.8, testing = 0.2, seed = 42)
okc_train <- data_splits\(training okc_test <- data_splits\)testing


我们可以快速查看我们响应变量的分布:

okc_train %>%
group_by(not_working) %>%
tally() %>%
mutate(frac = n / sum(n))


Source: spark<?> [?? x 3]

not_working n frac

1 0 43785 0.910
2 1 4317 0.0897


使用`sdf_describe()`函数,我们可以获得特定列的数值摘要:

sdf_describe(okc_train, cols = c("age", "income"))


Source: spark<?> [?? x 3]

summary age income

1 count 48102 9193
2 mean 32.336534863415245 104968.99815076689
3 stddev 9.43908920033797 202235.2291773537
4 min 18 20000.0
5 max 110 1000000.0


正如我们在第三章中所见,我们也可以利用` dbplot `包来绘制这些变量的分布。在图 4-1 中,我们展示了` age `变量的直方图分布,其代码如下所示:

dbplot_histogram(okc_train, age)


![年龄分布](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0401.png)

###### 图 4-1\. 年龄分布

一种常见的探索性数据分析(EDA)练习是查看响应变量和各个预测变量之间的关系。通常情况下,您可能已经具备了这些关系应该是什么样子的业务知识,因此这可以作为数据质量检查。此外,意外的趋势可以提示您可能希望在模型中包含的变量交互。例如,我们可以探索` religion `变量:

prop_data <- okc_train %>%
mutate(religion = regexp_extract(religion, "^\\w+", 0)) %>%
group_by(religion, not_working) %>%
tally() %>%
group_by(religion) %>%
summarise(
count = sum(n),
prop = sum(not_working * n) / sum(n)
) %>%
mutate(se = sqrt(prop * (1 - prop) / count)) %>%
collect()

prop_data


A tibble: 10 x 4

religion count prop se

1 judaism 2520 0.0794 0.00539
2 atheism 5624 0.118 0.00436
3 christianity 4671 0.120 0.00480
4 hinduism 358 0.101 0.0159
5 islam 115 0.191 0.0367
6 agnosticism 7078 0.0958 0.00346
7 other 6240 0.0841 0.00346
8 missing 16152 0.0719 0.002
9 buddhism 1575 0.0851 0.007
10 catholicism 3769 0.0886 0.00458


注意,` prop_data `是一个小的 DataFrame,已经在我们的 R 会话中收集到内存中,我们可以利用` ggplot2 `创建一个信息丰富的可视化(参见图 4-2):

prop_data %>%
ggplot(aes(x = religion, y = prop)) + geom_point(size = 2) +
geom_errorbar(aes(ymin = prop - 1.96 * se, ymax = prop + 1.96 * se),
width = .1) +
geom_hline(yintercept = sum(prop_data\(prop * prop_data\)count) /
sum(prop_data$count))


![不同宗教信仰的当前失业者比例](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0402.png)

###### 图 4-2\. 不同宗教信仰的当前失业者比例

接下来,我们来看一下两个预测变量之间的关系:酒精使用和药物使用。我们预计它们之间会存在一定的相关性。您可以通过` sdf_crosstab() `计算一个列联表:

contingency_tbl <- okc_train %>%
sdf_crosstab("drinks", "drugs") %>%
collect()

contingency_tbl


A tibble: 7 x 5

drinks_drugs missing never often sometimes

1 very often 54 144 44 137
2 socially 8221 21066 126 4106
3 not at all 146 2371 15 109
4 desperately 72 89 23 74
5 often 1049 1718 69 1271
6 missing 1121 1226 10 59
7 rarely 613 3689 35 445


我们可以使用马赛克图可视化这个列联表(参见图 4-3):

library(ggmosaic)
library(forcats)
library(tidyr)

contingency_tbl %>%
rename(drinks = drinks_drugs) %>%
gather("drugs", "count", missing:sometimes) %>%
mutate(
drinks = as_factor(drinks) %>%
fct_relevel("missing", "not at all", "rarely", "socially",
"very often", "desperately"),
drugs = as_factor(drugs) %>%
fct_relevel("missing", "never", "sometimes", "often")
) %>%
ggplot() +
geom_mosaic(aes(x = product(drinks, drugs), fill = drinks,
weight = count))


![药物和酒精使用的马赛克图](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0403.png)

###### 图 4-3\. 药物和酒精使用的马赛克图

为了进一步探索这两个变量之间的关系,我们可以使用` FactoMineR `包执行对应分析⁴。这种技术使我们能够通过将每个水平映射到平面上的一个点来总结高维因子水平之间的关系。我们首先使用` FactoMineR::CA() `获取映射,如下所示:

dd_obj <- contingency_tbl %>%
tibble::column_to_rownames(var = "drinks_drugs") %>%
FactoMineR::CA(graph = FALSE)


然后,我们可以使用` ggplot `绘制结果,您可以在图 4-4 中看到:

dd_drugs <-
dd_obj\(row\)coord %>%
as.data.frame() %>%
mutate(
label = gsub("_", " ", rownames(dd_obj\(row\)coord)),
Variable = "Drugs"
)

dd_drinks <-
dd_obj\(col\)coord %>%
as.data.frame() %>%
mutate(
label = gsub("_", " ", rownames(dd_obj\(col\)coord)),
Variable = "Alcohol"
)

ca_coord <- rbind(dd_drugs, dd_drinks)

ggplot(ca_coord, aes(x = Dim 1, y = Dim 2,
col = Variable)) +
geom_vline(xintercept = 0) +
geom_hline(yintercept = 0) +
geom_text(aes(label = label)) +
coord_equal()


![药物和酒精使用的对应分析主坐标](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0404.png)

###### 图 4-4\. 药物和酒精使用的对应分析主坐标

在图 4-4 中,我们看到对应分析过程已将因子转换为称为*主坐标*的变量,这些坐标对应于图中的轴,并表示它们在列联表中包含的信息量。例如,我们可以解释“经常饮酒”和“非常频繁使用药物”的接近表示它们之间的关联。

这就结束了我们对 EDA 的讨论。让我们继续进行特征工程。

# 特征工程

特征工程的练习包括将数据转换以提高模型的性能。这可以包括将数值值居中和缩放以及执行字符串操作以提取有意义的变量。它通常还包括变量选择——选择在模型中使用哪些预测变量的过程。

在图 4-1 中,我们看到`age`变量的范围从 18 到 60 多岁。一些算法,特别是神经网络,在训练时如果我们对输入进行归一化使其具有相同的量级,会更快。现在让我们通过计算其均值和标准差来归一化`age`变量:

scale_values <- okc_train %>%
summarise(
mean_age = mean(age),
sd_age = sd(age)
) %>%
collect()

scale_values


A tibble: 1 x 2

mean_age sd_age

1 32.3 9.44


然后我们可以使用这些来转换数据集:

okc_train <- okc_train %>%
mutate(scaled_age = (age - !!scale_values\(mean_age) / !!scale_values\)sd_age)


dbplot_histogram(okc_train, scaled_age)


在图 4-5 中,我们看到缩放年龄变量的值接近零。现在我们继续讨论其他类型的转换,在特征工程工作流程中,您可能希望对要包含在模型中的所有数值变量执行归一化。

![缩放年龄分布](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0405.png)

###### 图 4-5\. 缩放年龄分布

由于一些个人资料特征是多选的——换句话说,一个人可以选择为变量关联多个选项——我们需要在构建有意义的模型之前对它们进行处理。例如,如果我们看一下种族列,我们会发现有许多不同的组合:

okc_train %>%
group_by(ethnicity) %>%
tally()


Source: spark<?> [?? x 2]

ethnicity n

1 hispanic / latin, white 1051
2 black, pacific islander, hispanic / latin 2
3 asian, black, pacific islander 5
4 black, native american, white 91
5 middle eastern, white, other 34
6 asian, other 78
7 asian, black, white 12
8 asian, hispanic / latin, white, other 7
9 middle eastern, pacific islander 1
10 indian, hispanic / latin 5

… with more rows


一种处理方法是将每种种族组合视为一个单独的级别,但这将导致许多算法中的级别数量非常庞大,从而产生问题。为了更好地编码这些信息,我们可以为每个种族创建虚拟变量,如下所示:

ethnicities <- c("asian", "middle eastern", "black", "native american", "indian",
"pacific islander", "hispanic / latin", "white", "other")
ethnicity_vars <- ethnicities %>%
purrr::map(~ expr(ifelse(like(ethnicity, !!.x), 1, 0))) %>%
purrr::set_names(paste0("ethnicity_", gsub("\s|/", "", ethnicities)))
okc_train <- mutate(okc_train, !!!ethnicity_vars)
okc_train %>%
select(starts_with("ethnicity_")) %>%
glimpse()


Observations: ??
Variables: 9
Database: spark_connection
$ ethnicity_asian 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
$ ethnicity_middleeastern 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
$ ethnicity_black 0, 1, 0, 0, 0, 0, 0, 0, 0, 0…
$ ethnicity_nativeamerican 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
$ ethnicity_indian 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
$ ethnicity_pacificislander 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
$ ethnicity_hispaniclatin 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
$ ethnicity_white 1, 0, 1, 0, 1, 1, 1, 0, 1, 0…
$ ethnicity_other 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…


对于自由文本字段,提取特征的一种简单方法是计算字符的总数。我们将使用`compute()`将训练数据集存储在 Spark 的内存中,以加快计算速度。

okc_train <- okc_train %>%
mutate(
essay_length = char_length(paste(!!!syms(paste0("essay", 0:9))))
) %>% compute()


dbplot_histogram(okc_train, essay_length, bins = 100)


我们可以看到在图 4-6 中`essay_length`变量的分布。

![文章长度分布](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0406.png)

###### 图 4-6\. 文章长度分布

我们在第五章中使用这个数据集,所以让我们首先将其保存为 Parquet 文件——这是一个非常适合数值数据的高效文件格式:

spark_write_parquet(okc_train, "data/okc-train.parquet")


现在我们有了更多特征可以使用,我们可以开始运行一些无监督学习算法了。

# 监督学习

一旦我们对数据集有了充分的掌握,我们可以开始构建一些模型。然而,在此之前,我们需要制定一个计划来调整和验证“候选”模型——在建模项目中,我们通常尝试不同类型的模型和拟合方式,以确定哪些模型效果最佳。由于我们处理的是二元分类问题,可以使用的指标包括准确率、精确度、敏感度和接收者操作特征曲线下面积(ROC AUC),等等。优化的指标取决于具体的业务问题,但在本次练习中,我们将专注于 ROC AUC。

很重要的一点是,在最后阶段之前我们不能窥视测试保留集,因为我们获取的任何信息都可能影响我们的建模决策,从而使我们对模型性能的估计不够可信。为了调整和验证,我们执行 10 折交叉验证,这是模型调整的标准方法。该方案的工作方式如下:首先将数据集分成 10 个大致相等的子集。我们将第 2 到第 10 个集合作为算法的训练集,并在第 1 个集合上验证结果模型。接下来,我们将第 2 个集合作为验证集,并在第 1 和第 3 到第 10 个集合上训练算法。总共,我们训练了 10 个模型并平均性能。如果时间和资源允许,您还可以多次使用不同的随机数据分割执行此过程。在我们的案例中,我们将演示如何执行一次交叉验证。此后,我们将每个分割的训练集称为*分析*数据,将验证集称为*评估*数据。

使用`sdf_random_split()`函数,我们可以从我们的`okc_train`表中创建一个子集列表:

vfolds <- sdf_random_split(
okc_train,
weights = purrr::set_names(rep(0.1, 10), paste0("fold", 1:10)),
seed = 42
)


接着,我们按以下方式创建我们的第一个分析/评估分割:

analysis_set <- do.call(rbind, vfolds[2:10])
assessment_set <- vfolds[[1]]


这里我们需要特别注意的一点是变量的缩放。我们必须确保不会从评估集泄漏任何信息到分析集,因此我们仅在分析集上计算均值和标准差,并将相同的变换应用于两个集合。以下是我们如何处理`age`变量的方法:

make_scale_age <- function(analysis_data) {
scale_values <- analysis_data %>%
summarise(
mean_age = mean(age),
sd_age = sd(age)
) %>%
collect()

function(data) {
data %>%
mutate(scaled_age = (age - !!scale_values\(mean_age) / !!scale_values\)sd_age)
}
}

scale_age <- make_scale_age(analysis_set)
train_set <- scale_age(analysis_set)
validation_set <- scale_age(assessment_set)


为简洁起见,这里只展示如何转换`age`变量。然而,在实践中,您会希望归一化每一个连续预测变量,例如我们在前一节中推导的`essay_length`变量。

逻辑回归通常是二元分类问题的合理起点,所以让我们试试看。假设我们的领域知识还提供了一组初始预测变量。然后我们可以使用`Formula`接口拟合模型:

lr <- ml_logistic_regression(
analysis_set, not_working ~ scaled_age + sex + drinks + drugs + essay_length
)
lr


Formula: not_working ~ scaled_age + sex + drinks + drugs + essay_length

Coefficients:
(Intercept) scaled_age sex_m drinks_socially
-2.823517e+00 -1.309498e+00 -1.918137e-01 2.235833e-01
drinks_rarely drinks_often drinks_not at all drinks_missing
6.732361e-01 7.572970e-02 8.214072e-01 -4.456326e-01
drinks_very often drugs_never drugs_missing drugs_sometimes
8.032052e-02 -1.712702e-01 -3.995422e-01 -7.483491e-02
essay_length
3.664964e-05


要获得评估集上性能指标的摘要,我们可以使用`ml_evaluate()`函数:

validation_summary <- ml_evaluate(lr, assessment_set)


您可以打印`validation_summary`以查看可用的指标:

validation_summary


BinaryLogisticRegressionSummaryImpl
Access the following via $ or ml_summary().

  • features_col()
  • label_col()
  • predictions()
  • probability_col()
  • area_under_roc()
  • f_measure_by_threshold()
  • pr()
  • precision_by_threshold()
  • recall_by_threshold()
  • roc()
  • prediction_col()
  • accuracy()
  • f_measure_by_label()
  • false_positive_rate_by_label()
  • labels()
  • precision_by_label()
  • recall_by_label()
  • true_positive_rate_by_label()
  • weighted_f_measure()
  • weighted_false_positive_rate()
  • weighted_precision()
  • weighted_recall()
  • weighted_true_positive_rate()

我们可以通过收集`validation_summary$roc()`的输出并使用`ggplot2`来绘制 ROC 曲线:

roc <- validation_summary$roc() %>%
collect()

ggplot(roc, aes(x = FPR, y = TPR)) +
geom_line() + geom_abline(lty = "dashed")


图 4-7 展示了绘图结果。

ROC 曲线将真正例率(灵敏度)绘制为分类阈值的假正例率(1–特异性)。在实践中,业务问题有助于确定在曲线上设置分类阈值的位置。AUC 是用于确定模型质量的总结性测量,我们可以通过调用 `area_under_roc()` 函数来计算它。

![逻辑回归模型的 ROC 曲线](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0407.png)

###### 图 4-7\. 逻辑回归模型的 ROC 曲线

validation_summary$area_under_roc()


[1] 0.7872754


###### 注意

对于广义线性模型(包括线性模型和逻辑回归),Spark 提供了评估方法。对于其他算法,您可以使用评估器函数(例如,在预测 DataFrame 上使用 `ml_binary_classification_evaluator()`)或计算自己的指标。

现在,我们可以轻松重复已有的逻辑,并将其应用于每个分析/评估拆分:

cv_results <- purrr::map_df(1:10, function(v) {
analysis_set <- do.call(rbind, vfolds[setdiff(1:10, v)]) %>% compute()
assessment_set <- vfolds[[v]]

scale_age <- make_scale_age(analysis_set)
train_set <- scale_age(analysis_set)
validation_set <- scale_age(assessment_set)

model <- ml_logistic_regression(
analysis_set, not_working ~ scaled_age + sex + drinks + drugs + essay_length
)
s <- ml_evaluate(model, assessment_set)
roc_df <- s\(roc() %>% collect() auc <- s\)area_under_roc()

tibble(
Resample = paste0("Fold", stringr::str_pad(v, width = 2, pad = "0")),
roc_df = list(roc_df),
auc = auc
)
})


这给我们提供了 10 条 ROC 曲线:

unnest(cv_results, roc_df) %>%
ggplot(aes(x = FPR, y = TPR, color = Resample)) +
geom_line() + geom_abline(lty = "dashed")


图 4-8 展示了绘图结果。

![逻辑回归模型的交叉验证 ROC 曲线](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0408.png)

###### 图 4-8\. 逻辑回归模型的交叉验证 ROC 曲线

平均 AUC 指标可以通过以下方式获取:

mean(cv_results$auc)


[1] 0.7715102


## 广义线性回归

如果您对广义线性模型(GLM)诊断感兴趣,还可以通过指定 `family = "binomial"` 来通过广义线性回归界面拟合逻辑回归。因为结果是一个回归模型,所以 `ml_predict()` 方法不会给出类别概率。然而,它包括系数估计的置信区间:

glr <- ml_generalized_linear_regression(
analysis_set,
not_working ~ scaled_age + sex + drinks + drugs,
family = "binomial"
)

tidy_glr <- tidy(glr)


我们可以将系数估计提取到一个整洁的 DataFrame 中,然后进一步处理——例如,创建一个系数图,您可以在 图 4-9 中看到:

tidy_glr %>%
ggplot(aes(x = term, y = estimate)) +
geom_point() +
geom_errorbar(
aes(ymin = estimate - 1.96 * std.error,
ymax = estimate + 1.96 * std.error, width = .1)
) +
coord_flip() +
geom_hline(yintercept = 0, linetype = "dashed")


![系数估计及其 95% 置信区间](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0409.png)

###### 图 4-9\. 系数估计及其 95% 置信区间

###### 注意

`ml_logistic_regression()` 和 `ml_linear_regression()` 都支持通过 `reg_param` 和 `elastic_net_param` 参数进行弹性网正则化⁵。`reg_param` 对应于 <math><mi>λ</mi></math> ,而 `elastic_net_param` 对应于 <math><mi>α</mi></math> 。`ml_generalized_linear_regression()` 仅支持 `reg_param`。

## 其他模型

Spark 支持许多标准建模算法,并且可以轻松将这些模型和超参数(控制模型拟合过程的值)应用于特定问题。您可以在附录中找到支持的 ML 相关函数列表。访问这些功能的接口基本相同,因此可以轻松进行实验。例如,要拟合神经网络模型,我们可以运行以下操作:

nn <- ml_multilayer_perceptron_classifier(
analysis_set,
not_working ~ scaled_age + sex + drinks + drugs + essay_length,
layers = c(12, 64, 64, 2)
)


这为我们提供了一个具有两个隐藏层,每层 64 个节点的前馈神经网络模型。请注意,您必须在`layers`参数中指定输入和输出层的正确值。我们可以使用`ml_predict()`在验证集上获取预测结果:

predictions <- ml_predict(nn, assessment_set)


然后,我们可以通过`ml_binary_classification_evaluator()`计算 AUC:

ml_binary_classification_evaluator(predictions)


[1] 0.7812709


到目前为止,我们尚未深入研究除了进行简单字符计数之外的论文字段中的非结构化文本。在接下来的部分中,我们将更深入地探索文本数据。

# 无监督学习

除了语音、图像和视频外,文本数据是大数据爆炸的组成部分之一。在现代文本挖掘技术及其支持的计算资源出现之前,公司很少使用自由文本字段。如今,文本被视为从医生的笔记到客户投诉等各处获得洞察的丰富信息源。在本节中,我们展示了`sparklyr`的一些基本文本分析功能。如果您希望了解更多有关文本挖掘技术背景的信息,我们建议阅读 David Robinson 和 Julie Silge 的《*Text Mining with R*》(O’Reilly)。

在本节中,我们展示了如何在`OKCupid`数据集中的论文数据上执行基本的主题建模任务。我们的计划是将每个档案的 10 个论文字段连接起来,并将每个档案视为一个文档,然后尝试使用潜在狄利克雷分配(LDA)来发现*主题*(我们很快会定义这些主题)。

## 数据准备

在分析数据集(或其子集)之前,我们总是希望快速查看以对其进行定位。在这种情况下,我们对用户在其约会档案中输入的自由文本感兴趣。

essay_cols <- paste0("essay", 0:9)
essays <- okc %>%
select(!!essay_cols)
essays %>%
glimpse()


Observations: ??
Variables: 10
Database: spark_connection
$ essay0 "about me:
\n
\ni would love to think that…
$ essay1 "currently working as an international agent for a f…
$ essay2 "making people laugh.
\nranting about a good sa…
$ essay3 "the way i look. i am a six foot half asian, half ca…
$ essay4 "books:
\nabsurdistan, the republic, of mice an…
$ essay5 "food.
\nwater.
\ncell phone.
\nshelt…
$ essay6 "duality and humorous things", "missing", "missing",…
$ essay7 "trying to find someone to hang out with. i am down …
$ essay8 "i am new to california and looking for someone to w…
$ essay9 "you want to be swept off your feet!
\nyou are …


就从这个输出中,我们看到以下内容:

+   文本包含 HTML 标签

+   文本包含换行符(`\n`)字符

+   数据中存在缺失值。

HTML 标签和特殊字符会污染数据,因为它们不是用户直接输入的,并且不提供有趣的信息。类似地,由于我们使用“*missing*”字符串对缺失的字符字段进行了编码,我们需要将其删除。(请注意,通过这样做,我们也在删除用户写入的“missing”单词的实例,但是由于这种删除而丢失的信息可能很少。)

在分析您自己的文本数据时,您将很快遇到并熟悉特定数据集的特殊情况。与表格数值数据一样,预处理文本数据是一个迭代过程,经过几次尝试后,我们有了以下转换:

essays <- essays %>%

Replace missing with empty string.

mutate_all(list(~ ifelse(. == "missing", "", .))) %>%

Concatenate the columns.

mutate(essay = paste(!!!syms(essay_cols))) %>%

Remove miscellaneous characters and HTML tags

mutate(words = regexp_replace(essay, "\n| |<[>]*>|[A-Za-z|']", " "))


请注意,这里我们使用了`regex_replace()`,这是一个 Spark SQL 函数。接下来,我们讨论 LDA 以及如何将其应用于我们的清理数据集。

## 主题建模

LDA 是一种用于识别文档集中抽象“主题”的主题模型。它是一种无监督算法,因为我们不为输入文档提供任何标签或主题。LDA 假定每个文档是主题的混合物,而每个主题是单词的混合物。在训练期间,它试图同时估计这两者。主题模型的典型应用包括对许多文档进行分类,其中文档的大量数量使得手动方法不可行。应用领域涵盖从 GitHub 问题到法律文件等多个领域。

在完成上一节中的工作流程后,我们可以使用 `ml_lda()` 拟合一个 LDA 模型:

stop_words <- ml_default_stop_words(sc) %>%
c(
"like", "love", "good", "music", "friends", "people", "life",
"time", "things", "food", "really", "also", "movies"
)

lda_model <- ml_lda(essays, ~ words, k = 6, max_iter = 1, min_token_length = 4,
stop_words = stop_words, min_df = 5)


我们还包括一个 `stop_words` 向量,包含常用的英语单词和数据集中常见的单词,指示算法忽略它们。模型拟合后,我们可以使用 `tidy()` 函数从模型中提取相关的 beta 值,即每个主题每个单词的概率。

betas <- tidy(lda_model)
betas


A tibble: 256,992 x 3

topic term beta

1 0 know 303.
2 0 work 250.
3 0 want 367.
4 0 books 211.
5 0 family 213.
6 0 think 291.
7 0 going 160.
8 0 anything 292.
9 0 enjoy 145.
10 0 much 272.

… with 256,982 more rows


我们可以通过查看每个主题的单词概率来可视化这些输出。在 图 4-10 和 图 4-11 中,我们展示了在 1 次迭代和 100 次迭代时的结果。生成 图 4-10 的代码如下;要生成 图 4-11,你需要在运行 `ml_lda()` 时设置 `max_iter = 100`,但要注意,在单台机器上这可能需要很长时间——这是 Spark 集群可以轻松处理的大计算问题。

betas %>%
group_by(topic) %>%
top_n(10, beta) %>%
ungroup() %>%
arrange(topic, -beta) %>%
mutate(term = reorder(term, beta)) %>%
ggplot(aes(term, beta, fill = factor(topic))) +
geom_col(show.legend = FALSE) +
facet_wrap(~ topic, scales = "free") +
coord_flip()


![第一次迭代中每个主题中最常见的术语](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0410.png)

###### 图 4-10\. 第一次迭代中每个主题中最常见的术语

在 100 次迭代后,“主题”开始显现出来。如果你正在研究一大批你不熟悉的文件,这些信息本身可能是很有趣的。学到的主题还可以作为下游监督学习任务中的特征;例如,在我们的预测建模示例中,我们可以考虑使用主题编号作为模型中的预测因子来预测就业状态。

![100 次迭代后每个主题中最常见的术语](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0411.png)

###### 图 4-11\. 100 次迭代后每个主题中最常见的术语

最后,在结束本章时,你应该从 Spark 断开连接。第 第五章 也使用了 `OKCupid` 数据集,但我们提供了从头重新加载该数据集的指导:

spark_disconnect(sc)


# 总结

在本章中,我们通过介绍 EDA、特征工程和构建监督模型的主题,探讨了使用逻辑回归和神经网络等多种模型(从 Spark 的数十种模型中挑选出来的)构建预测模型的基础,涵盖了在 Spark 中使用 R 构建预测模型的基础知识。

然后,我们探讨了如何使用无监督学习处理原始文本,您创建了一个可以自动将配置文件分组为六个类别的主题模型。我们演示了使用单台机器构建主题模型可能需要大量时间,这几乎是引入全尺寸计算集群的完美过渡!但请先暂停思考:我们首先需要考虑如何自动化数据科学工作流程。

正如我们在介绍本章时提到的,重点放在了预测建模上。Spark 可以帮助规模化数据科学,但它也可以将数据科学工作流程投入自动化流程中,许多人称之为机器学习。第五章介绍了我们将需要的工具,以将我们的预测模型,甚至整个训练工作流程,带入可以持续运行或导出并在 Web 应用程序、移动应用程序等中消费的自动化环境中。

¹ Kuhn M,Johnson K(2019)。*特征工程和选择:预测模型的实用方法*。(CRC 出版社。)

² 我们承认这些术语可能对不同的人有不同的含义,并且这两种方法之间存在一个连续体;然而,它们是被定义好的。

³ Kim AY,Escobedo-Land A(2015)。“用于初步统计和数据科学课程的 OKCupid 数据。”*统计教育杂志*,23(2)。

⁴ Greenacre M(2017)。*实践中的对应分析*。Chapman and Hall/CRC 出版社。

⁵ Zou H,Hastie T(2005)。“通过弹性网络进行正则化和变量选择。”*皇家统计学会:B 系列(统计方法学)*,67(2),301–320。


# 第五章:管道

> 你再也不会步行,但你会飞!
> 
> —三眼乌鸦

在第四章中,您学习了如何使用 Spark 提供的高级功能以及与 Spark 良好配合的知名 R 包来构建预测模型。您首先了解了监督方法,然后通过原始文本完成了该章节的无监督方法。

在本章中,我们深入探讨了 Spark Pipelines,这是支持我们在第四章中演示功能的引擎。例如,当您通过 R 中的公式接口调用 MLlib 函数,例如`ml_logistic_regression(cars, am ~ .)`,则在幕后为您构建了一个*pipeline*。因此,管道还允许您利用高级数据处理和建模工作流程。此外,管道还通过允许您将管道部署到生产系统、Web 应用程序、移动应用程序等,促进了数据科学和工程团队之间的协作。

该章节也恰好是鼓励您将本地计算机作为 Spark 集群使用的最后一章。您只需再阅读一章,就可以开始执行能够扩展到最具挑战性计算问题的数据科学或机器学习。

# 概述

管道的构建模块是称为*transformers*和*estimators*的对象,它们被统称为*pipeline stages*。*Transformer*用于对 DataFrame 应用转换并返回另一个 DataFrame;结果 DataFrame 通常包含原始 DataFrame 及其附加的新列。另一方面,*estimator*可用于根据一些训练数据创建 transformer。考虑以下示例来说明这种关系:一个“中心和缩放”的 estimator 可以学习数据的均值和标准差,并将统计信息存储在生成的 transformer 对象中;然后可以使用此 transformer 来规范化训练数据以及任何新的未见数据。

下面是定义 estimator 的示例:

library(sparklyr)
library(dplyr)

sc <- spark_connect(master = "local", version = "2.3")

scaler <- ft_standard_scaler(
sc,
input_col = "features",
output_col = "features_scaled",
with_mean = TRUE)

scaler


StandardScaler (Estimator)
<standard_scaler_7f6d46f452a1>
(Parameters -- Column Names)
input_col: features
output_col: features_scaled
(Parameters)
with_mean: TRUE
with_std: TRUE


现在我们可以创建一些数据(我们知道其均值和标准差),然后使用`ml_fit()`函数将我们的缩放模型拟合到数据中:

df <- copy_to(sc, data.frame(value = rnorm(100000))) %>%
ft_vector_assembler(input_cols = "value", output_col = "features")

scaler_model <- ml_fit(scaler, df)
scaler_model


StandardScalerModel (Transformer)
<standard_scaler_7f6d46f452a1>
(Parameters -- Column Names)
input_col: features
output_col: features_scaled
(Transformer Info)
mean: num 0.00421
std: num 0.999


###### 注意

在 Spark ML 中,许多算法和特征转换器要求输入为向量列。函数`ft_vector_assembler()`执行此任务。您还可以使用该函数初始化一个用于 pipeline 中的 transformer。

我们看到均值和标准差非常接近 0 和 1,这是我们预期的结果。然后我们可以使用 transformer 来*transform*一个 DataFrame,使用`ml_transform()`函数:

scaler_model %>%
ml_transform(df) %>%
glimpse()


Observations: ??
Variables: 3
Database: spark_connection
$ value 0.75373300, -0.84207731, 0.59365113, -…
$ features [0.753733, -0.8420773, 0.5936511, -0.…
$ features_scaled [0.7502211, -0.8470762, 0.58999, -0.4…


现在您已经看到了 estimators 和 transformers 的基本示例,我们可以继续讨论管道。

# 创建

*管道*只是一系列转换器和评估器,*管道模型*是在数据上训练过的管道,因此其所有组件都已转换为转换器。

在`sparklyr`中构建管道的几种方法,都使用`ml_pipeline()`函数。

我们可以用`ml_pipeline(sc)`初始化一个空管道,并将阶段追加到其中:

ml_pipeline(sc) %>%
ft_standard_scaler(
input_col = "features",
output_col = "features_scaled",
with_mean = TRUE)


Pipeline (Estimator) with 1 stage
<pipeline_7f6d6a6a38ee>
Stages
|--1 StandardScaler (Estimator)
| <standard_scaler_7f6d63bfc7d6>
| (Parameters -- Column Names)
| input_col: features
| output_col: features_scaled
| (Parameters)
| with_mean: TRUE
| with_std: TRUE


或者,我们可以直接将阶段传递给`ml_pipeline()`:

pipeline <- ml_pipeline(scaler)


我们像拟合评估器一样拟合管道:

pipeline_model <- ml_fit(pipeline, df)
pipeline_model


PipelineModel (Transformer) with 1 stage
<pipeline_7f6d64df6e45>
Stages
|--1 StandardScalerModel (Transformer)
| <standard_scaler_7f6d46f452a1>
| (Parameters -- Column Names)
| input_col: features
| output_col: features_scaled
| (Transformer Info)
| mean: num 0.00421
| std: num 0.999


pipeline


###### 注意

由于 Spark ML 的设计,管道始终是评估器对象,即使它们只包含转换器。这意味着,如果您有一个只包含转换器的管道,仍然需要对其调用`ml_fit()`来获取转换器。在这种情况下,“拟合”过程实际上不会修改任何转换器。

# 使用案例

现在您已经了解了 ML Pipelines 的基本概念,让我们将其应用于前一章节中的预测建模问题,我们试图通过查看其档案来预测人们当前是否就业。我们的起点是具有相关列的`okc_train` DataFrame。

okc_train <- spark_read_parquet(sc, "data/okc-train.parquet")

okc_train <- okc_train %>%
select(not_working, age, sex, drinks, drugs, essay1:essay9, essay_length)


我们首先展示管道,包括特征工程和建模步骤,然后逐步介绍它:

pipeline <- ml_pipeline(sc) %>%
ft_string_indexer(input_col = "sex", output_col = "sex_indexed") %>%
ft_string_indexer(input_col = "drinks", output_col = "drinks_indexed") %>%
ft_string_indexer(input_col = "drugs", output_col = "drugs_indexed") %>%
ft_one_hot_encoder_estimator(
input_cols = c("sex_indexed", "drinks_indexed", "drugs_indexed"),
output_cols = c("sex_encoded", "drinks_encoded", "drugs_encoded")
) %>%
ft_vector_assembler(
input_cols = c("age", "sex_encoded", "drinks_encoded",
"drugs_encoded", "essay_length"),
output_col = "features"
) %>%
ft_standard_scaler(input_col = "features", output_col = "features_scaled",
with_mean = TRUE) %>%
ml_logistic_regression(features_col = "features_scaled",
label_col = "not_working")


前三个阶段索引`sex`、`drinks`和`drugs`列,这些列是字符型,通过`ft_string_indexer()`将它们转换为数值索引。这对于接下来的`ft_one_hot_encoder_estimator()`是必要的,后者需要数值列输入。当所有预测变量都是数值类型时(回想一下`age`已经是数值型),我们可以使用`ft_vector_assembler()`创建特征向量,它将所有输入连接到一个向量列中。然后,我们可以使用`ft_standard_scaler()`来归一化特征列的所有元素(包括分类变量的独热编码 0/1 值),最后通过`ml_logistic_regression()`应用逻辑回归。

在原型设计阶段,您可能希望将这些转换*急切地*应用于数据的一个小子集,通过将 DataFrame 传递给`ft_`和`ml_`函数,并检查转换后的 DataFrame。即时反馈允许快速迭代想法;当您已经找到所需的处理步骤时,可以将它们整合成一个管道。例如,您可以执行以下操作:

okc_train %>%
ft_string_indexer("sex", "sex_indexed") %>%
select(sex_indexed)


Source: spark<?> [?? x 1]

sex_indexed

1 0
2 0
3 1
4 0
5 1
6 0
7 0
8 1
9 1
10 0

… with more rows


找到数据集的适当转换后,您可以用`ml_pipeline(sc)`替换 DataFrame 输入,结果将是一个可以应用于具有适当架构的任何 DataFrame 的管道。在下一节中,我们将看到管道如何使我们更容易测试不同的模型规格。

## 超参数调整

回到我们之前创建的管道,我们可以使用 `ml_cross_validator()` 来执行我们在前一章中演示的交叉验证工作流,并轻松测试不同的超参数组合。在这个例子中,我们测试了是否通过中心化变量以及逻辑回归的各种正则化值来改进预测。我们定义交叉验证器如下:

cv <- ml_cross_validator(
sc,
estimator = pipeline,
estimator_param_maps = list(
standard_scaler = list(with_mean = c(TRUE, FALSE)),
logistic_regression = list(
elastic_net_param = c(0.25, 0.75),
reg_param = c(1e-2, 1e-3)
)
),
evaluator = ml_binary_classification_evaluator(sc, label_col = "not_working"),
num_folds = 10)


`estimator` 参数就是我们要调整的估算器,本例中是我们定义的 `pipeline`。我们通过 `estimator_param_maps` 参数提供我们感兴趣的超参数值,它接受一个嵌套的命名列表。第一级的名称对应于我们想要调整的阶段的唯一标识符(UID),这些 UID 是与每个管道阶段对象关联的唯一标识符(如果提供部分 UID,`sparklyr` 将尝试将其与管道阶段匹配),第二级的名称对应于每个阶段的参数。在上面的片段中,我们正在指定我们要测试以下内容:

标准缩放器

值 `TRUE` 和 `FALSE` 对于 `with_mean`,它表示预测值是否被居中。

逻辑回归

值 `0.25` 和 `0.75` 对于 <math><mi>α</mi></math> ,以及值 `1e-2` 和 `1e-3` 对于 <math><mi>λ</mi></math> 。

我们预计这将产生 <math><mrow><mn>2</mn> <mo>×</mo> <mn>2</mn> <mo>×</mo> <mn>2</mn> <mo>=</mo> <mn>8</mn></mrow></math> 种超参数组合,我们可以通过打印 `cv` 对象来确认:

cv


CrossValidator (Estimator)
<cross_validator_d5676ac6f5>
(Parameters -- Tuning)
estimator: Pipeline
<pipeline_d563b0cba31>
evaluator: BinaryClassificationEvaluator
<binary_classification_evaluator_d561d90b53d>
with metric areaUnderROC
num_folds: 10
[Tuned over 8 hyperparameter sets]


与任何其他估算器一样,我们可以使用 `ml_fit()` 来拟合交叉验证器

cv_model <- ml_fit(cv, okc_train)


然后检查结果:

ml_validation_metrics(cv_model) %>%
arrange(-areaUnderROC)


areaUnderROC elastic_net_param_1 reg_param_1 with_mean_2
1 0.7722700 0.75 0.001 TRUE
2 0.7718431 0.75 0.010 FALSE
3 0.7718350 0.75 0.010 TRUE
4 0.7717677 0.25 0.001 TRUE
5 0.7716070 0.25 0.010 TRUE
6 0.7715972 0.25 0.010 FALSE
7 0.7713816 0.75 0.001 FALSE
8 0.7703913 0.25 0.001 FALSE


现在我们已经看到管道 API 在操作中的样子,让我们更正式地讨论它们在各种上下文中的行为。

# 操作模式

到目前为止,您可能已经注意到管道阶段函数(如 `ft_string_indexer()` 和 `ml_logistic_regression()`)根据传递给它们的第一个参数返回不同类型的对象。表 5-1 展示了完整的模式。

表 5-1\. 机器学习函数中的操作模式

| 第一个参数 | 返回值 | 示例 |
| --- | --- | --- |
| Spark 连接 | 估算器或转换器对象 | `ft_string_indexer(sc)` |
| 管道 | 管道 | `ml_pipeline(sc) %>% ft_string_indexer()` |
| 不带公式的 DataFrame | DataFrame | `ft_string_indexer(iris, "Species", "indexed")` |
| 带公式的 DataFrame | sparklyr ML 模型对象 | `ml_logistic_regression(iris, Species ~ .)` |

这些函数是使用 [S3](https://adv-r.hadley.nz/s3.html) 实现的,它是 R 提供的最流行的面向对象编程范式。对于我们的目的,知道 `ml_` 或 `ft_` 函数的行为由提供的第一个参数的类别决定就足够了。这使我们能够提供广泛的功能而不引入额外的函数名称。现在我们可以总结这些函数的行为:

+   如果提供了 Spark 连接,则该函数返回一个转换器或估计器对象,可以直接使用`ml_fit()`或`ml_transform()`,也可以包含在管道中。

+   如果提供了管道,则该函数返回一个具有附加到其中的阶段的管道对象。

+   如果将 DataFrame 提供给特征转换器函数(带有前缀`ft_`)或不提供公式就提供 ML 算法,则该函数实例化管道阶段对象,如果必要的话(如果阶段是估计器),将其拟合到数据,然后转换 DataFrame 并返回一个 DataFrame。

+   如果将 DataFrame 和公式提供给支持公式接口的 ML 算法,`sparklyr`在后台构建一个管道模型,并返回一个包含附加元数据信息的 ML 模型对象。

公式接口方法是我们在第四章中学习的内容,也是我们建议刚接触 Spark 的用户从这里开始的原因,因为它的语法类似于现有的 R 建模包,并且可以摆脱一些 Spark ML 的特殊性。然而,要充分利用 Spark ML 的全部功能并利用管道进行工作流组织和互操作性,学习 ML 管道 API 是值得的。

掌握了管道的基础知识后,我们现在可以讨论在本章引言中提到的协作和模型部署方面的内容。

# 互操作性

管道最强大的一点之一是它们可以序列化到磁盘,并且与其他 Spark API(如 Python 和 Scala)完全兼容。这意味着您可以轻松地在使用不同语言的 Spark 用户之间共享它们,这些用户可能包括其他数据科学家、数据工程师和部署工程师。要保存管道模型,请调用`ml_save()`并提供路径:

model_dir <- file.path("spark_model")
ml_save(cv_model$best_model, model_dir, overwrite = TRUE)


Model successfully saved.


让我们看一下刚写入的目录:

list.dirs(model_dir,full.names = FALSE) %>%
head(10)


[1] ""
[2] "metadata"
[3] "stages"
[4] "stages/0_string_indexer_5b42c72817b"
[5] "stages/0_string_indexer_5b42c72817b/data"
[6] "stages/0_string_indexer_5b42c72817b/metadata"
[7] "stages/1_string_indexer_5b423192b89f"
[8] "stages/1_string_indexer_5b423192b89f/data"
[9] "stages/1_string_indexer_5b423192b89f/metadata"
[10] "stages/2_string_indexer_5b421796e826"


我们可以深入几个文件,看看保存了什么类型的数据:

spark_read_json(sc, file.path(
file.path(dir(file.path(model_dir, "stages"),
pattern = "1_string_indexer.*",
full.names = TRUE), "metadata")
)) %>%
glimpse()


Observations: ??
Variables: 5
Database: spark_connection
$ class "org.apache.spark.ml.feature.StringIndexerModel"
$ paramMap [["error", "drinks", "drinks_indexed", "frequencyDesc"]]
$ sparkVersion "2.3.2"
$ timestamp 1.561763e+12
$ uid "string_indexer_ce05afa9899"


spark_read_parquet(sc, file.path(
file.path(dir(file.path(model_dir, "stages"),
pattern = "6_logistic_regression.*",
full.names = TRUE), "data")
))


Source: spark [?? x 5]

numClasses numFeatures interceptVector coefficientMatr… isMultinomial

1 2 12 <dbl [1]> <-1.27950828662… FALSE


我们看到已导出了相当多的信息,从`dplyr`转换器中的 SQL 语句到逻辑回归的拟合系数估计。然后(在一个新的 Spark 会话中),我们可以使用`ml_load()`来重建模型:

model_reload <- ml_load(sc, model_dir)


让我们看看是否可以从这个管道模型中检索逻辑回归阶段:

ml_stage(model_reload, "logistic_regression")


LogisticRegressionModel (Transformer)
<logistic_regression_5b423b539d0f>
(Parameters -- Column Names)
features_col: features_scaled
label_col: not_working
prediction_col: prediction
probability_col: probability
raw_prediction_col: rawPrediction
(Transformer Info)
coefficient_matrix: num [1, 1:12] -1.2795 -0.0915 0 0.126 -0.0324 ...
coefficients: num [1:12] -1.2795 -0.0915 0 0.126 -0.0324 ...
intercept: num -2.79
intercept_vector: num -2.79
num_classes: int 2
num_features: int 12
threshold: num 0.5
thresholds: num [1:2] 0.5 0.5


请注意,导出的 JSON 和 parquet 文件与导出它们的 API 无关。这意味着在多语言机器学习工程团队中,您可以从使用 Python 的数据工程师那里获取数据预处理管道,构建一个预测模型,然后将最终管道交给使用 Scala 的部署工程师。在下一节中,我们将更详细地讨论模型的部署。

###### 注意

当为使用公式接口创建的`sparklyr` ML 模型调用`ml_save()`时,关联的管道模型将被保存,但不会保存任何`sparklyr`特定的元数据,如索引标签。换句话说,保存一个`sparklyr`的`ml_model`对象,然后加载它将产生一个管道模型对象,就像您通过 ML 流水线 API 创建它一样。这种行为对于在其他编程语言中使用流水线是必需的。

在我们继续讨论如何在生产环境中运行流水线之前,请确保断开与 Spark 的连接:

spark_disconnect(sc)


这样,我们可以从全新的环境开始,这也是在部署流水线时预期的情况。

# 部署

我们刚刚展示的值得强调的是:通过在 ML 流水线框架内协作,我们减少了数据科学团队中不同角色之间的摩擦。特别是,我们缩短了从建模到部署的时间。

在许多情况下,数据科学项目并不仅仅以幻灯片展示洞见和建议而告终。相反,手头的业务问题可能需要按计划或按需实时评分新数据点。例如,银行可能希望每晚评估其抵押贷款组合的风险或即时决策信用卡申请。将模型转化为其他人可以消费的服务的过程通常称为*部署*或*产品化*。历史上,在构建模型的分析师和部署模型的工程师之间存在很大的鸿沟:前者可能在 R 中工作,并且开发了关于评分机制的详尽文档,以便后者可以用 C++或 Java 重新实现模型。这种做法在某些组织中可能需要数月时间,但在今天已经不那么普遍,而且在 Spark ML 工作流中几乎总是不必要的。

上述的夜间投资组合风险和信用申请评分示例代表了两种机器学习部署模式,称为*批处理*和*实时*。粗略地说,批处理意味着同时处理许多记录,执行时间不重要,只要合理(通常在几分钟到几小时的范围内)。另一方面,实时处理意味着一次评分一个或几个记录,但延迟至关重要(在小于 1 秒的范围内)。现在让我们看看如何将我们的`OKCupid`流水线模型带入“生产”环境。

## 批量评分

对于批处理和实时两种评分方法,我们将以 Web 服务的形式公开我们的模型,通过超文本传输协议(HTTP)的 API 提供。这是软件进行通信的主要媒介。通过提供 API,其他服务或最终用户可以使用我们的模型,而无需了解 R 或 Spark。[`plumber`](https://www.rplumber.io/) R 包使我们可以通过注释我们的预测函数来轻松实现这一点。

您需要确保通过运行以下命令安装`plumber`、`callr`和`httr`包:

install.packages(c("plumber", "callr", "httr"))


`callr` 包支持在单独的 R 会话中运行 R 代码;虽然不是必需的,但我们将使用它来在后台启动 Web 服务。`httr` 包允许我们从 R 使用 Web API。

在批处理评分用例中,我们只需初始化一个 Spark 连接并加载保存的模型。将以下脚本保存为 *plumber/spark-plumber.R*:

library(sparklyr)
sc <- spark_connect(master = "local", version = "2.3")

spark_model <- ml_load(sc, "spark_model")

* @post /predict

score_spark <- function(age, sex, drinks, drugs, essay_length) {
new_data <- data.frame(
age = age,
sex = sex,
drinks = drinks,
drugs = drugs,
essay_length = essay_length,
stringsAsFactors = FALSE
)
new_data_tbl <- copy_to(sc, new_data, overwrite = TRUE)

ml_transform(spark_model, new_data_tbl) %>%
dplyr::pull(prediction)
}


然后我们可以通过执行以下操作来初始化服务:

service <- callr::r_bg(function() {
p <- plumber::plumb("plumber/spark-plumber.R")
p$run(port = 8000)
})


这会在本地启动 Web 服务,然后我们可以用新数据查询服务进行评分;但是,您可能需要等待几秒钟以便 Spark 服务初始化:

httr::content(httr::POST(
"http://127.0.0.1:8000/predict",
body = '{"age": 42, "sex": "m", "drinks": "not at all",
"drugs": "never", "essay_length": 99}'
))


[[1]]
[1] 0


此回复告诉我们,这个特定的配置文件可能不会是失业的,即是受雇用的。现在我们可以通过停止 `callr` 服务来终止 `plumber` 服务:

service$interrupt()


如果我们计时这个操作(例如使用 `system.time()`),我们会发现延迟在数百毫秒的数量级上,这对于批处理应用可能是合适的,但对于实时应用来说不够。主要瓶颈是将 R DataFrame 序列化为 Spark DataFrame,然后再转换回来。此外,它需要一个活跃的 Spark 会话,这是一个重量级的运行时要求。为了改善这些问题,接下来我们讨论一个更适合实时部署的部署方法。

## 实时评分

对于实时生产,我们希望尽可能保持依赖项轻量化,以便可以针对更多平台进行部署。现在我们展示如何使用 [`mleap`](http://bit.ly/2Z7jgSV) 包,它提供了一个接口给 [MLeap](http://bit.ly/33G271R) 库,用于序列化和提供 Spark ML 模型。MLeap 是开源的(Apache License 2.0),支持广泛的 Spark ML 转换器,尽管不是所有。在运行时,环境的唯一先决条件是 Java 虚拟机(JVM)和 MLeap 运行时库。这避免了 Spark 二进制文件和昂贵的将数据转换为 Spark DataFrames 的开销。

由于 `mleap` 是 `sparklyr` 的扩展和一个 R 包,因此我们首先需要从 CRAN 安装它:

install.packages("mleap")


当调用 `spark_connect()` 时,必须加载它;所以让我们重新启动您的 R 会话,建立一个新的 Spark 连接,并加载我们之前保存的管道模型:

library(sparklyr)
library(mleap)


sc <- spark_connect(master = "local", version = "2.3")


spark_model <- ml_load(sc, "spark_model")


保存模型到 MLeap bundle 格式的方法与使用 Spark ML Pipelines API 保存模型非常相似;唯一的附加参数是 `sample_input`,它是一个具有我们期望对新数据进行评分的模式的 Spark DataFrame:

sample_input <- data.frame(
sex = "m",
drinks = "not at all",
drugs = "never",
essay_length = 99,
age = 25,
stringsAsFactors = FALSE
)

sample_input_tbl <- copy_to(sc, sample_input)

ml_write_bundle(spark_model, sample_input_tbl, "mleap_model.zip", overwrite =
TRUE)


现在我们可以在运行 Java 并具有开源 MLeap 运行时依赖项的任何设备上部署我们刚刚创建的文件 **mleap_model.zip**,而不需要 Spark 或 R!事实上,我们可以继续断开与 Spark 的连接:

spark_disconnect(sc)


在使用此 MLeap 模型之前,请确保安装了运行时依赖项:

mleap::install_maven()
mleap::install_mleap()


要测试此模型,我们可以创建一个新的 plumber API 来公开它。脚本 *plumber/mleap-plumber.R* 与前面的示例非常相似:

library(mleap)

mleap_model <- mleap_load_bundle("mleap_model.zip")

* @post /predict

score_spark <- function(age, sex, drinks, drugs, essay_length) {
new_data <- data.frame(
age = as.double(age),
sex = sex,
drinks = drinks,
drugs = drugs,
essay_length = as.double(essay_length),
stringsAsFactors = FALSE
)
mleap_transform(mleap_model, new_data)$prediction
}


启动服务的方式也完全相同:

service <- callr::r_bg(function() {
p <- plumber::plumb("plumber/mleap-plumber.R")
p$run(port = 8000)
})


我们可以运行与之前相同的代码来测试这个新服务中的失业预测:

httr::POST(
"http://127.0.0.1:8000/predict",
body = '{"age": 42, "sex": "m", "drinks": "not at all",
"drugs": "never", "essay_length": 99}'
) %>%
httr::content()


[[1]]
[1] 0


如果我们计时这个操作,我们会看到现在服务在几十毫秒内返回预测结果。

让我们停止这项服务,然后结束本章:

service$interrupt()


# 总结

在本章中,我们讨论了 Spark 管道,这是引擎在第四章介绍的建模功能背后的驱动力。您学会了通过将数据处理和建模算法组织到管道中来整理预测建模工作流程。您了解到管道还通过共享一种语言无关的序列化格式促进了多语言数据科学和工程团队成员之间的协作——您可以从 R 导出一个 Spark 管道,让其他人在 Python 或 Scala 中重新加载您的管道到 Spark 中,这使他们可以在不改变自己选择的语言的情况下进行协作。

您还学会了如何使用`mleap`部署管道,这是一个提供另一种将 Spark 模型投入生产的 Java 运行时——您可以导出管道并将其集成到支持 Java 的环境中,而不需要目标环境支持 Spark 或 R。

你可能已经注意到,一些算法,尤其是无监督学习类型的算法,对于可以加载到内存中的`OKCupid`数据集来说,速度较慢。如果我们能够访问一个合适的 Spark 集群,我们就可以花更多时间建模,而不是等待!不仅如此,我们还可以利用集群资源来运行更广泛的超参数调整作业和处理大型数据集。为了达到这个目标,第六章介绍了计算集群的具体内容,并解释了可以考虑的各种选项,如建立自己的集群或按需使用云集群。

¹ 截至本书撰写时,MLeap 不支持 Spark 2.4。


# 第六章:群集

> 我有一支非常庞大的军队和非常庞大的龙。
> 
> —丹妮莉丝·坦格利安

之前的章节侧重于在单个计算实例(您的个人计算机)上使用 Spark。在本章中,我们介绍了在多个计算实例(也称为*计算*集群)上运行 Spark 的技术。本章及后续章节将介绍并利用适用于计算集群的概念;然而,并不需要使用计算集群来跟随我们的内容,因此您仍然可以使用您的个人计算机。值得一提的是,尽管之前的章节侧重于单个计算实例,您也可以在计算集群中使用我们介绍的所有数据分析和建模技术,而无需更改任何代码。

如果您的组织已经拥有一个 Spark 集群,您可以考虑跳到第七章,该章节将教您如何连接到现有的集群。否则,如果您没有集群或者正在考虑改进您现有的基础设施,本章将介绍当今可用的集群趋势、管理者和供应商。

# 概述

簇计算中有三个值得讨论的主要趋势:*本地*、*云*计算和*Kubernetes*。随着时间的推移来描绘这些趋势将帮助我们理解它们是如何形成的,它们是什么,以及它们的未来可能性。为了说明这一点,图 6-1 使用来自谷歌趋势的数据,绘制了这些趋势随时间的变化。

对于本地集群,您或您的组织中的某人购买了旨在用于集群计算的物理计算机。该集群中的计算机由*现成的*硬件制成,这意味着某人下单购买了通常可以在商店货架上找到的计算机,或者*高性能*硬件,这意味着计算供应商提供了高度定制的计算硬件,还优化了高性能网络连接、功耗消耗等。

![本地(大型机)、云计算和 Kubernetes 的谷歌趋势](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0601.png)

###### 图 6-1 本地(大型机)、云计算和 Kubernetes 的谷歌趋势

当购买数百或数千个计算实例时,将它们放在我们都熟悉的普通计算箱中是没有意义的;相反,将它们尽可能有效地堆叠在一起以减少使用空间是有意义的。这组高效堆叠的计算实例称为[*机架*](https://oreil.ly/zKOr-)。当一个集群增长到数千台计算机时,您还需要托管数百个机架的计算设备;在这个规模下,您还需要显著的物理空间来托管这些机架。

一个提供计算实例机架的建筑通常被称为*数据中心*。在数据中心的规模上,你还需要找到方法使建筑更加高效,特别是冷却系统、电源供应、网络连接等。由于这是耗时的,一些组织联合起来在[Open Compute Project](http://www.opencompute.org/)倡议下开源了他们的基础设施,提供了一套数据中心蓝图供任何人免费使用。

没有什么能阻止你建造自己的数据中心,事实上,许多组织已经走上了这条路。例如,亚马逊起初是一家在线书店,但随着时间的推移,它发展到不仅仅销售书籍。随着在线商店的增长,其数据中心也在规模上增长。2002 年,亚马逊考虑过在他们的数据中心中[向公众出租服务器](https://oreil.ly/Nx3BD),两年后,亚马逊网络服务(AWS)作为一种方式推出,让任何人都可以按需租用公司数据中心中的服务器,这意味着您不需要购买、配置、维护或拆除自己的集群,而是可以直接从 AWS 租用它们。

这种按需计算模型就是我们今天所知的*云计算*。在云中,您使用的集群不是您拥有的,也不在您的物理建筑中;相反,它是由别人拥有和管理的数据中心。今天,这个领域有许多云服务提供商,包括 AWS、Databricks、Google、Microsoft、Qubole 等等。大多数云计算平台通过 Web 应用程序或命令行提供用户界面来请求和管理资源。

尽管多年来处理云中数据的好处显而易见,但选择云服务提供商却意外地使组织与特定的提供商锁定在一起,使得在不同提供商之间或者回到本地集群之间切换变得困难。*Kubernetes*由 Google 于 2014 年宣布,是一种[用于跨多个主机管理容器化应用程序的开源系统](https://oreil.ly/u6H5X)。在实践中,它使得跨多个云提供商和本地环境部署变得更加容易。

总结来说,我们看到了从本地到云计算,以及最近的 Kubernetes 的过渡。这些技术通常被宽泛地描述为*私有云*、*公有云*和作为可以实现*混合云*的编排服务之一。本章将带您了解 Spark 和 R 在各个集群计算趋势中的背景。

# 本地环境

正如概述部分中提到的,在本地集群代表了一组由您组织的工作人员采购和管理的计算实例。这些集群可以高度定制和控制;然而,它们也可能带来更高的初始费用和维护成本。

当使用本地 Spark 集群时,有两个概念您应该考虑:

集群管理器

就像操作系统(如 Windows 或 macOS)允许您在同一台计算机上运行多个应用程序一样,集群管理器允许在同一集群中运行多个应用程序。在处理本地集群时,您需要自行选择一个集群管理器。

Spark 分发版本

虽然您可以从 Apache Spark 网站安装 Spark,但许多组织与能够为 Apache Spark 提供支持和增强的公司合作,我们通常称之为 Spark 的*分发版本*。

## 管理器

要在计算集群中运行 Spark,您需要运行能够在每台物理机器上初始化 Spark 并注册所有可用计算节点的软件。这种软件称为[集群管理器](https://oreil.ly/Ye4zH)。Spark 中可用的集群管理器包括*Spark Standalone*、*YARN*、*Mesos*和*Kubernetes*。

###### 注意

在分布式系统和集群文献中,我们通常将每台物理机器称为*计算实例*、*计算节点*、*实例*或*节点*。

### Standalone

在*Spark Standalone*中,Spark 使用自身作为其集群管理器,这使您可以在集群中使用 Spark 而无需安装其他软件。如果计划仅使用集群运行 Spark 应用程序,则此方法非常有用;如果此集群不专用于 Spark,则像 YARN、Mesos 或 Kubernetes 这样的通用集群管理器更适合。Spark Standalone 的[文档](http://bit.ly/307YtM6)详细介绍了配置、启动、监控和启用高可用性,如图 6-2 所示。

但是,由于 Spark Standalone 包含在 Spark 安装中,在完成第二章后,您现在可以使用自己的机器上的本地 Spark Standalone 集群初始化可用的 Spark 安装。实际上,您可能希望在不同的机器上启动工作节点,但为简单起见,我们提供了在单台机器上启动独立集群的代码。

首先,通过运行**`spark_home_dir()`**来获取`SPARK_HOME`目录,然后按以下步骤启动主节点和工作节点:

Retrieve the Spark installation directory

spark_home <- spark_home_dir()

Build paths and classes

spark_path <- file.path(spark_home, "bin", "spark-class")

Start cluster manager master node

system2(spark_path, "org.apache.spark.deploy.master.Master", wait = FALSE)


![Spark Standalone 网站](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0602.png)

###### 图 6-2\. Spark Standalone 网站

前一个命令初始化了主节点。您可以在[*localhost:8080*](http://localhost:8080)访问主节点界面,如图 6-3 所示。请注意,Spark 主 URL 指定为*spark://address:port*;您将需要此 URL 来初始化工作节点。

然后,我们可以使用主 URL 初始化单个工作节点;但是,您可以通过多次运行代码和潜在地跨不同机器初始化多个工作节点:

Start worker node, find master URL at http://localhost:8080/

system2(spark_path, c("org.apache.spark.deploy.worker.Worker",
"spark://address:port"), wait = FALSE)


![Spark Standalone 网页界面](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0603.png)

###### 图 6-3\. Spark Standalone 网页界面

在 Spark 独立模式中有一个工作节点注册。点击该工作节点的链接,以查看该特定工作节点的详细信息,如图 6-4 所示。

![Spark 独立工作节点 Web 界面](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0604.png)

###### 图 6-4. Spark 独立工作节点 Web 界面

当你完成了在这个集群中的计算后,你需要停止主节点和工作节点。你可以使用`jps`命令来识别需要终止的进程号。在以下示例中,`15330`和`15353`是你可以终止的进程,以完成集群的最终操作。要终止一个进程,你可以在 Windows 中使用`system("Taskkill /PID ##### /F")`,或者在 macOS 和 Linux 中使用`system("kill -9 #####")`。

system("jps")


15330 Master
15365 Jps
15353 Worker
1689 QuorumPeerMain


你可以按照类似的方法来配置集群,通过在集群中的每台机器上运行初始化代码来实现。

虽然可以初始化一个简单的独立集群,但配置一个能够从计算机重启和故障中恢复,并支持多个用户、权限等的合适的 Spark 独立集群通常是一个更长的过程,超出了本书的范围。接下来的章节将介绍几种在本地或通过云服务上更易于管理的替代方案。我们将从介绍 YARN 开始。

### YARN

Hadoop YARN,简称 YARN,是 Hadoop 项目的资源管理器。它最初在 Hadoop 项目中开发,但在 Hadoop 2 中重构为独立项目。正如我们在第一章中提到的,Spark 是为了加速在 Hadoop 上的计算而构建的,因此在安装了 Hadoop 集群的地方很常见找到 Spark。

YARN 的一个优势是,它很可能已经安装在许多支持 Hadoop 的现有集群中;这意味着你可以在不需要对现有集群基础设施做出任何重大更改的情况下,轻松地在许多现有的 Hadoop 集群上使用 Spark。由于许多集群最初是 Hadoop 集群,随后升级以支持 Spark,因此在 YARN 集群中部署 Spark 也是非常常见的。

你可以以两种模式提交 YARN 应用程序:*yarn-client*和*yarn-cluster*。在 yarn-cluster 模式下,驱动程序可能是远程运行的,而在 yarn-client 模式下,驱动程序是在本地运行的。两种模式都受支持,我们将在第七章中进一步解释它们。

YARN 提供了一个资源管理用户界面,用于访问日志、监控可用资源、终止应用程序等。当你从 R 语言连接到 Spark 后,你将能够在 YARN 中管理运行的应用程序,如图 6-5 所示。

由于 YARN 是 Hadoop 项目的集群管理器,你可以在[hadoop.apache.org](http://bit.ly/2TDGsCX)找到 YARN 的文档。你还可以参考在[spark.apache.org](http://bit.ly/306WsQx)上的“在 YARN 上运行 Spark”指南。

![YARN 的资源管理器运行一个 sparklyr 应用程序](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0605.png)

###### 图 6-5. YARN 的资源管理器运行一个 sparklyr 应用程序

### Apache Mesos

Apache Mesos 是一个管理计算机集群的开源项目。Mesos 最初是 UC Berkeley RAD 实验室的一个研究项目。它利用 Linux 的[Cgroups](http://bit.ly/2Z9KEeW)来提供 CPU、内存、I/O 和文件系统访问的隔离。

Mesos 和 YARN 一样,支持执行许多集群框架,包括 Spark。然而,Mesos 的一个特定优势在于它允许像 Spark 这样的集群框架实现自定义的任务调度器。调度器是集群中协调应用程序分配执行时间和分配资源的组件。Spark 使用[粗粒度调度器](https://oreil.ly/9WQvg),为应用程序的整个执行周期安排资源;然而,其他框架可能使用 Mesos 的细粒度调度器,通过在更短的间隔内调度任务来增加集群的整体效率,使它们能够共享资源。

Mesos 提供了一个 Web 界面来管理正在运行的应用程序、资源等。在从 R 连接到 Spark 后,您的应用程序将像在 Mesos 中运行的任何其他应用程序一样注册。图 6-6 展示了成功从 R 连接到 Spark 的情况。

Mesos 是一个 Apache 项目,其文档可以在[mesos.apache.org](https://mesos.apache.org/)找到。如果选择将 Mesos 作为您的集群管理器,[*在 Mesos 上运行 Spark*](http://bit.ly/31H4LCT)指南也是一个很好的资源。

![Mesos web 界面运行 Spark 从 R](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0606.png)

###### 图 6-6. Mesos web 界面运行 Spark 和 R

## 发行版

您可以在本地集群中使用集群管理器,如前一节所述;然而,许多组织,包括但不限于 Apache Spark,选择与提供额外管理软件、服务和资源的公司合作来管理其集群中的应用程序。一些本地集群提供商包括*Cloudera*、*Hortonworks*和*MapR*,我们将在下文简要介绍。

*Cloudera*,Inc.,是一家总部位于美国的软件公司,提供基于 Apache Hadoop 和 Apache Spark 的软件、支持和服务以及培训给商业客户。Cloudera 的混合开源 Apache Hadoop 发行版,Cloudera 分布包括 Apache Hadoop(CDH),针对企业级部署该技术。Cloudera 向组成 Apache Hadoop 平台的各种 Apache 许可的开源项目(如 Apache Hive、Apache Avro、Apache HBase 等)捐赠超过 50%的工程输出。[Cloudera](http://bit.ly/2KJmcfe)也是 Apache 软件基金会的赞助商。

Cloudera 集群使用 [*parcels*](http://bit.ly/33LHpxU),这些是包含程序文件和元数据的二进制分发物。在 Cloudera 中,Spark 安装为一个 parcel。本书不涵盖如何配置 Cloudera 集群,但资源和文档可以在 [cloudera.com](http://bit.ly/33yUUkp) 和 Cloudera 博客上的 [“Introducing sparklyr, an R Interface for Apache Spark”](http://bit.ly/2HbAtjY) 找到。

Cloudera 提供 Cloudera Manager 网页界面,用于管理资源、服务、parcels、诊断等等。图 6-7 展示了在 Cloudera Manager 中运行的一个 Spark parcel,稍后可以用来从 R 进行连接。

![Cloudera Manager 运行 Spark parcel](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0607.png)

###### 图 6-7\. Cloudera Manager 运行 Spark parcel

[`sparklyr` 已与 Cloudera 认证](http://bit.ly/2z1yydc),这意味着 Cloudera 的支持团队已了解 `sparklyr`,并能够有效地帮助使用 Spark 和 R 的组织。表 6-1 总结了目前认证的版本。

表 6-1\. 与 Cloudera 认证的 sparklyr 版本

| Cloudera 版本 | 产品 | 版本 | 组件 | Kerberos |
| --- | --- | --- | --- | --- |
| CDH5.9 | sparklyr | 0.5 | HDFS, Spark | Yes |
| CDH5.9 | sparklyr | 0.6 | HDFS, Spark | Yes |
| CDH5.9 | sparklyr | 0.7 | HDFS, Spark | Yes |

*Hortonworks* 是一家总部位于加利福尼亚州圣克拉拉的大数据软件公司。该公司开发、支持并提供广泛的完全开源软件,旨在管理从物联网(IoT)到高级分析和机器学习的数据和处理。[Hortonworks](http://bit.ly/2KTufpV) 认为自己是一家数据管理公司,架设云端与数据中心之间的桥梁。

[Hortonworks 与 Microsoft 合作](http://bit.ly/2NbfuBH) 提升了在 Microsoft Windows 上对 Hadoop 和 Spark 的支持,这曾是与 Cloudera 的区别点;然而,比较 Hortonworks 和 Cloudera 在今天已不那么相关,因为这两家公司于 2019 年 1 月 [合并](http://bit.ly/2Mk1UMt)。尽管合并,Cloudera 和 Hortonworks 的 Spark 发行版仍可获得支持。有关在 Hortonworks 下配置 Spark 的额外资源,请访问 [hortonworks.com](http://bit.ly/2Z8M8Kh)。

*MapR* 是一家总部位于加利福尼亚州圣克拉拉的商业软件公司。[MapR](http://bit.ly/33DU8Cs)提供从单个计算机集群访问各种数据源的服务,包括 Apache Hadoop 和 Apache Spark 等大数据工作负载,分布式文件系统,多模型数据库管理系统以及事件流处理,将实时分析与操作应用程序结合在一起。其技术可在商品硬件和公共云计算服务上运行。

# 云

如果你既没有本地集群也没有多余的机器可供重复使用,从云集群开始会非常方便,因为它将允许你在几分钟内访问一个合适的集群。本节简要提到了一些主要的云基础设施提供商,并为你提供了资源,以帮助你开始使用云服务提供商。

在云服务中,计算实例的计费时间与 Spark 集群运行时间一致;你的计费从集群启动开始算起,集群停止时结束。这项费用需要按照为你的集群预留的实例数量进行乘算。例如,如果一个云服务提供商每小时每个计算实例收费 $1.00,你启动了一个三节点的集群并使用了一小时零十分钟,那么你可能会收到一张 $1.00 x 2 小时 x 3 节点 = $6.00 的账单。有些云服务提供商按分钟计费,但至少你可以依赖它们都是按计算小时计费。

请注意,尽管小规模集群的计算成本可能非常低,但意外地让集群保持运行可能导致显著的计费费用。因此,在你不再需要集群时,花额外的时间检查两次确保集群已经终止是值得的。在使用集群时,每天监控成本以确保你的预期与每日账单相匹配也是一个好习惯。

根据过去的经验,在处理大规模项目时,你还应提前请求计算资源;各种云服务提供商通常不允许你在通过支持请求明确要求之前启动数百台机器的集群。虽然这可能有些繁琐,但这也是帮助你控制组织成本的一种方式。

由于集群大小是灵活的,最佳实践是从小集群开始,根据需要扩展计算资源。即使你事先知道需要一个大型集群,从小开始也提供了以较低成本解决问题的机会,因为第一次尝试时你的数据分析不太可能无缺陷地在大规模上运行。作为经验法则,指数级增加实例;如果你需要在八节点集群上运行计算,从一个节点和八分之一的整个数据集开始,然后两个节点和四分之一,然后四个节点和半个数据集,最后八个节点和整个数据集。随着经验的积累,你会培养出良好的问题解决感觉,了解所需集群的大小,并能跳过中间步骤,但作为初学者,这是一个很好的实践方法。

您还可以使用云提供商获取裸计算资源,然后自行安装前一节介绍的本地分布;例如,您可以在 Amazon Elastic Compute Cloud(Amazon EC2)上运行 Cloudera 分布。这种模型避免了采购共同托管的硬件,但仍允许您紧密管理和自定义集群。本书仅介绍云提供商提供的完全托管的 Spark 服务概述;不过,您通常可以轻松找到在线有关如何在云中安装本地分布的说明。

一些主要的云计算基础设施提供商包括 Amazon、Databricks、Google、IBM、Microsoft 和 Qubole。接下来的小节简要介绍了每个提供商。

## Amazon

Amazon 通过[AWS](https://aws.amazon.com/)提供云服务;具体来说,它通过[Amazon EMR](https://aws.amazon.com/emr/)提供按需 Spark 集群。

使用 Amazon EMR 与 R 的详细说明已在 Amazon 的大数据博客中发布,名为[“在 Amazon EMR 上运行 sparklyr”](https://amzn.to/2OYWMQ5)。该文章介绍了`sparklyr`的启动和配置 Amazon EMR 集群的说明。例如,它建议您可以使用[Amazon 命令行界面](https://aws.amazon.com/cli/)启动一个包含三个节点的集群:

aws emr create-cluster --applications Name=Hadoop Name=Spark Name=Hive
--release-label emr-5.8.0 --service-role EMR_DefaultRole --instance-groups
InstanceGroupType=MASTER,InstanceCount=1,InstanceType=m3.2xlarge
InstanceGroupType=CORE,InstanceCount=2,InstanceType=m3.2xlarge
--bootstrap-action Path=s3://aws-bigdata-blog/artifacts/aws-blog-emr-
rstudio-sparklyr/rstudio_sparklyr_emr5.sh,Args=["--user-pw", "",
"--rstudio", "--arrow"] --ec2-attributes InstanceProfile=EMR_EC2_DefaultRole


您随后可以在 AWS 门户下看到集群的启动和运行情况,如图 6-8 所示。

然后,您可以导航到主公共 DNS 并在端口 8787 下找到 RStudio,例如`ec2-12-34-567-890.us-west-1.compute.amazonaws.com:8787`,然后使用用户名`hadoop`和密码`password`登录。

![启动 Amazon EMR 集群](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0608.png)

###### 图 6-8\. 启动 Amazon EMR 集群

也可以通过 Web 界面启动 Amazon EMR 集群;同一篇介绍性文章包含了专门为 Amazon EMR 设计的额外细节和操作说明。

记得关闭您的集群,以避免不必要的费用,并在启动 Amazon EMR 集群进行敏感数据分析时使用适当的安全限制。

关于成本,您可以在[Amazon EMR 定价](https://amzn.to/2YRGb5r)处找到最新信息。表 6-2 展示了在`us-west-1`地区(截至本文撰写时)可用的一些实例类型;这旨在提供云处理资源和成本的一瞥。请注意,“EMR 价格另外加上 Amazon EC2 价格(即底层服务器的价格)。”

表 6-2\. Amazon EMR 定价信息

| 实例 | CPU | 内存 | 存储 | EC2 成本 | EMR 成本 |
| --- | --- | --- | --- | --- | --- |
| c1.medium | 2 | 1.7 GB | 350 GB | 每小时$0.148 \| 每小时$0.030 |
| m3.2xlarge | 8 | 30 GB | 160 GB | 每小时$0.616 \| 每小时$0.140 |
| i2.8xlarge | 32 | 244 GB | 6400 GB | 每小时$7.502 \| 每小时$0.270 |

###### 注

我们仅展示了截至 2019 年 Amazon 和其他云服务提供商提供的计算实例的部分列表;然而,请注意硬件(CPU 速度、硬盘速度等)在供应商和地点之间会有所不同;因此,您不能将这些硬件表格用作准确的价格比较依据。准确的比较需要运行特定的工作负载并考虑计算实例成本之外的其他因素。

## Databricks

[Databricks](https://databricks.com) 是由 Apache Spark 的创始人创建的公司,旨在帮助客户通过 Spark 进行基于云的大数据处理。Databricks 起源于加州大学伯克利分校的 [AMPLab](https://oreil.ly/W2Eoe) 项目。

Databricks 提供企业级集群计算方案以及用于探索功能并熟悉其环境的免费/社区层级。

在启动集群后,您可以按照 第二章 中提供的步骤,在 Databricks 笔记本中使用 R 和 `sparklyr`,或者通过在 [Databricks 上安装 RStudio](http://bit.ly/2KCDax6) 来进行操作。图 6-9 展示了使用 `sparkylyr` 在 Databricks 笔记本上运行 Spark 的情况。

![Databricks 社区笔记本运行 sparklyr](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0609.png)

###### 图 6-9\. Databricks 社区笔记本运行 sparklyr

额外资源可在 Databricks 工程博客文章 [“在 Databricks 中使用 sparklyr”](http://bit.ly/2N59jyR) 和 [Databricks `sparklyr` 文档](http://bit.ly/2MkOYWC) 中找到。

您可以在 [*databricks.com/product/pricing*](http://bit.ly/305Rnrt) 查找最新的定价信息。表 6-3 列出了撰写本文时提供的计划。

表 6-3\. Databricks 产品信息

| 方案 | 基础 | 数据工程 | 数据分析 |
| --- | --- | --- | --- |
| AWS 标准 | $0.07/DBU \| $0.20/DBU | $0.40/DBU |
| Azure 标准 |  | $0.20/DBU \| $0.40/DBU |
| Azure 高级 |  | $0.35/DBU \| $0.55/DBU |

注意,定价是基于每小时的 DBU 成本。根据 Databricks 的说法,“[Databricks Unit](https://oreil.ly/3muQq)(DBU)是每小时的 Apache Spark 处理能力单位。对于多种实例,DBU 是一种更透明的使用方式,而不是节点小时。”

## Google

Google 提供 Google Cloud Dataproc 作为一种基于云的托管 Spark 和 Hadoop 服务,提供在 Google Cloud Platform(GCP)上运行的服务。Dataproc 利用许多 GCP 技术,如 Google Compute Engine 和 Google Cloud Storage,为运行流行的数据处理框架(如 Apache Hadoop 和 Apache Spark)的完全托管集群提供支持。

您可以通过 Google Cloud 控制台或 Google Cloud 命令行界面(CLI)轻松创建集群,如 图 6-10 所示。

![启动 Dataproc 集群](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0610.png)

###### 图 6-10\. 启动 Dataproc 集群

创建集群后,可以将端口转发,以便从您的机器访问此集群,例如,通过启动 Chrome 来使用此代理并安全连接到 Dataproc 集群。配置此连接如下所示:

gcloud compute ssh sparklyr-m --project= --zone= -- -D 1080
-N "" --proxy-server="socks5://localhost:1080"
--user-data-dir="/tmp/sparklyr-m" http://sparklyr-m:8088


有多种[可用的教程](http://bit.ly/2OYyo18)(cloud.google.com/dataproc/docs/tutorials),包括一个详细的[tutorial 配置 RStudio 和`sparklyr`](http://bit.ly/2MhSgKg)。

您可以在[*cloud.google.com/dataproc/pricing*](http://bit.ly/31J0uyC)找到最新的定价信息。在表 6-4 中请注意,成本分为计算引擎和 Dataproc 高级部分。

表 6-4\. Google Cloud Dataproc 定价信息

| 实例 | CPU | 内存 | 计算引擎 | Dataproc 高级部分 |
| --- | --- | --- | --- | --- |
| n1-standard-1 | 1 | 3.75 GB | $0.0475/小时 \| $0.010/小时 |
| n1-standard-8 | 8 | 30 GB | $0.3800/小时 \| $0.080/小时 |
| n1-standard-64 | 64 | 244 GB | $3.0400/小时 \| $0.640/小时 |

## IBM

IBM 云计算是一套面向企业的云计算服务。IBM 云包括基础设施即服务(IaaS)、软件即服务(SaaS)和平台即服务(PaaS),通过公共、私有和混合云交付模型提供,除此之外还包括组成这些云的各个组件。

在 IBM Cloud 内部,打开 Watson Studio 并创建一个数据科学项目,在项目设置下添加一个 Spark 集群,然后在启动 IDE 菜单上启动 RStudio。请注意,截至撰写本文时,提供的`sparklyr`版本并非 CRAN 中最新版本,因为`sparklyr`已经修改以在 IBM Cloud 上运行。无论如何,请遵循 IBM 的文档作为运行 R 和 Spark 在 IBM Cloud 上以及如何适当升级`sparklyr`的权威参考。图 6-11 捕捉了 IBM 云门户启动 Spark 集群的场景。

![IBM Watson Studio 启动支持 R 的 Spark](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0611.png)

###### 图 6-11\. IBM Watson Studio 启动支持 R 的 Spark

最新的定价信息请访问[*ibm.com/cloud/pricing*](https://www.ibm.com/cloud/pricing)。在表 6-5 中,计算成本使用每月成本的 31 天进行了标准化。

表 6-5\. IBM 云定价信息

| 实例 | CPU | 内存 | 存储 | 成本 |
| --- | --- | --- | --- | --- |
| C1.1x1x25 | 1 | 1 GB | 25 GB | $0.033/小时 |
| C1.4x4x25 | 4 | 4 GB | 25 GB | $0.133/小时 |
| C1.32x32x25 | 32 | 25 GB | 25 GB | $0.962/小时 |

## 微软

Microsoft Azure 是由微软创建的用于构建、测试、部署和管理应用程序和服务的云计算服务,通过全球网络的 Microsoft 托管数据中心提供。它支持 SaaS、PaaS 和 IaaS,并支持许多不同的编程语言、工具和框架,包括 Microsoft 特定和第三方软件和系统。

从 Azure 门户,Azure HDInsight 服务支持按需创建 Spark 集群。你可以通过选择 ML Services 集群类型轻松创建支持 Spark 和 RStudio 的 HDInsight 集群。请注意,提供的 `sparklyr` 版本可能不是 CRAN 中最新的版本,因为默认的软件包存储库似乎是使用 Microsoft R 应用程序网络(MRAN)的快照初始化的,而不是直接从 CRAN 获取的。图 6-12 展示了 Azure 门户启动支持 R 的 Spark 集群。

![创建 Azure HDInsight Spark 集群](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0612.png)

###### 图 6-12\. 创建 Azure HDInsight Spark 集群

HDInsight 的最新定价信息可在 [*azure.microsoft.com/en-us/pricing/details/hdinsight*](http://bit.ly/2H9Ce0X) 上找到;表 6-6 列出了本文撰写时的定价。

表 6-6\. Azure HDInsight 定价信息

| 实例 | CPUs | 内存 | 总成本 |
| --- | --- | --- | --- |
| D1 v2 | 1 | 3.5 GB | $0.074/小时 |
| D4 v2 | 8 | 28 GB | $0.59/小时 |
| G5 | 64 | 448 GB | $9.298/小时 |

## Qubole

[Qubole](https://www.qubole.com) 成立于 2013 年,旨在缩小数据可访问性差距。Qubole 提供一个建立在亚马逊、微软、谷歌和甲骨文云上的自助式大数据分析平台。在 Qubole 中,你可以启动 Spark 集群,并可以从 [Qubole notebooks](http://bit.ly/33ChKYk) 或 RStudio Server 使用。图 6-13 展示了一个使用 RStudio 和 `sparklyr` 初始化的 Qubole 集群。

![运行 sparklyr 的 Qubole 笔记本](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0613.png)

###### 图 6-13\. 使用 RStudio 和 sparklyr 初始化的 Qubole 集群

你可以在 [Qubole 的定价页面](http://bit.ly/33AuKh8) 找到最新的定价信息。表 6-7 列出了 Qubole 当前计划的价格,截至本文撰写时。请注意,定价是基于每小时 QCU(Qubole 计算单元)的成本计算,企业版需要签订年度合同。

表 6-7\. Qubole 定价信息

| 试用 | 完整试用 | 企业版 |
| --- | --- | --- |
| $0 \| $0 | $0.14/QCU |

# Kubernetes

Kubernetes 是一个由 Google 最初设计并现由 [Cloud Native Computing Foundation](https://www.cncf.io/)(CNCF)维护的开源容器编排系统,用于自动化部署、扩展和管理容器化应用程序。Kubernetes 最初基于 [Docker](https://www.docker.com/) 开发,与 Mesos 类似,也基于 Linux Cgroups。

Kubernetes 可以执行许多集群应用程序和框架,通过使用具有特定资源和库的容器映像进行高度定制。这使得单个 Kubernetes 集群可以用于超出数据分析之外的许多不同目的,从而帮助组织轻松管理其计算资源。使用自定义映像的一个权衡是它们增加了进一步的配置开销,但使 Kubernetes 集群极其灵活。尽管如此,这种灵活性已被证明对于许多组织轻松管理集群资源至关重要,正如在“概述”中指出的那样,Kubernetes 正在成为一个非常受欢迎的集群框架。

Kubernetes 被所有主要的云服务提供商支持。它们都提供了关于如何启动、管理和撤销 Kubernetes 集群的广泛文档;图 6-14 展示了在创建 Kubernetes 集群时的 GCP 控制台。你可以在任何 Kubernetes 集群上部署 Spark,并且可以使用 R 来连接、分析、建模等。

![在 Google Cloud 上创建用于 Spark 和 R 的 Kubernetes 集群](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0614.png)

###### 图 6-14\. 在 Google Cloud 上创建用于 Spark 和 R 的 Kubernetes 集群

你可以在[kubernetes.io](https://kubernetes.io/)上了解更多信息,并阅读来自[spark.apache.org](http://bit.ly/2KAZze7)的*在 Kubernetes 上运行 Spark*指南。

严格来说,Kubernetes 是一种集群技术,而不是一种特定的集群架构。然而,Kubernetes 代表了一个更大的趋势,通常被称为*混合云*。混合云是利用本地和公共云服务,并在各个平台之间进行编排的计算环境。现在精确分类将形成混合集群计算的主导技术尚为时过早;尽管如此,如前所述,Kubernetes 是其中领先的技术之一,还可能会有更多技术来补充或甚至替代现有技术。

# 工具

虽然只使用 R 和 Spark 对一些集群来说可能已足够,但通常在集群中安装一些辅助工具是常见的,以改善监控、SQL 分析、工作流协调等,例如[Ganglia](http://ganglia.info/)、[Hue](http://gethue.com/)和[Oozie](https://oozie.apache.org/)。本节并不意味着涵盖所有工具,而是提到了常用的工具。

## RStudio

从阅读第一章可以知道,RStudio 是一个著名且免费的 R 桌面开发环境;因此,你很可能正在使用 RStudio Desktop 来跟随本书中的示例。然而,你可能不知道你可以在 Spark 集群内作为 Web 服务运行 RStudio。这个版本的 RStudio 称为*RStudio Server*。你可以在图 6-15 中看到 RStudio Server 正在运行。与 Spark UI 在集群中运行类似,你可以在集群中安装 RStudio Server。然后你可以连接到 RStudio Server,并且以与使用 RStudio Desktop 相同的方式使用 RStudio,但具有对 Spark 集群运行代码的能力。正如你在图 6-15 中看到的那样,RStudio Server 看起来和感觉都像 RStudio Desktop,但通过位于集群中来运行命令可以高效运行。

![RStudio Server](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0615.png)

###### 图 6-15\. 在 AWS 中运行的 RStudio Server Pro

如果你熟悉 R,Shiny Server 是一个非常流行的工具,用于从 R 构建交互式 Web 应用程序。我们建议你直接在你的 Spark 集群中安装 Shiny。

RStudio Server 和 Shiny Server 是免费开源的;然而,RStudio 还提供专业产品如 RStudio Server、[RStudio Server Pro](http://bit.ly/2KCaxQn)、[Shiny Server Pro](http://bit.ly/30aV0fK)和[RStudio Connect](http://bit.ly/306fHcY),你可以在集群内安装以支持额外的 R 工作流。虽然`sparklyr`不需要任何额外的工具,但它们提供了显著的生产力提升,值得考虑。你可以在[*rstudio.com/products/*](http://bit.ly/2MihHLP)了解更多信息。

## Jupyter

[Jupyter](http://jupyter.org/)项目旨在开发跨多种编程语言的交互式计算的开源软件、开放标准和服务。Jupyter 笔记本支持包括 R 在内的各种编程语言。你可以使用`sparklyr`与 Jupyter 笔记本一起使用 R 内核。图 6-16 展示了`sparklyr`在本地 Jupyter 笔记本中的运行。

![Jupyter 笔记本运行 sparklyr](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0616.png)

###### 图 6-16\. Jupyter 笔记本运行 sparklyr

## Livy

[Apache Livy](http://bit.ly/2L2TZAn)是 Apache 的孵化项目,通过 Web 界面提供支持,使得可以远程使用 Spark 集群。它非常适合直接连接到 Spark 集群;然而,在无法直接连接到集群的情况下,你可以考虑在集群中安装 Livy,并适当地进行安全设置,以便通过 Web 协议进行远程使用。但要注意,使用 Livy 会带来显著的性能开销。

为了帮助在本地测试 Livy,`sparklyr`提供了支持通过执行`livy_available_versions()`列出、安装、启动和停止本地 Livy 实例的功能。

livy

1 0.2.0

2 0.3.0

3 0.4.0

4 0.5.0


这里列出了您可以安装的版本;我们建议安装最新版本,并按以下方式进行验证:

Install default Livy version

livy_install()

List installed Livy services

livy_installed_versions()

Start the Livy service

livy_service_start()


然后,您可以访问本地的 Livy 会话 [*http://localhost:8998*](http://localhost:8998)。第七章 将详细介绍如何通过 Livy 进行连接。连接成功后,您可以访问 Livy Web 应用程序,如 图 6-17 所示。

![作为本地服务运行的 Apache Livy](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0617.png)

###### 图 6-17\. Apache Livy 作为本地服务运行

在使用本地 Livy 实例时,请确保也停止 Livy 服务(对于在集群中正常运行的 Livy 服务,则无需停止):

Stops the Livy service

livy_service_stop()


# 总结

本章讲解了本地部署和云计算的历史和权衡,并介绍了 Kubernetes 作为一个有前途的框架,可以在本地或多个云服务提供商之间提供灵活性。它还介绍了集群管理器(Spark Standalone、YARN、Mesos 和 Kubernetes)作为运行 Spark 作为集群应用所需的软件。本章简要提到了像 Cloudera、Hortonworks 和 MapR 这样的本地集群提供商,以及主要的 Spark 云提供商:亚马逊、Databricks、IBM、谷歌、微软和 Qubole。

虽然本章为理解当前集群计算趋势、工具和服务提供商奠定了坚实的基础,有助于在规模上进行数据科学,但未提供一个全面的框架来帮助您决定选择哪种集群技术。相反,请将本章作为概述和寻找额外资源的起点,帮助您找到最适合组织需求的集群堆栈。

第七章 将专注于理解如何连接到现有的集群;因此,它假定您已经可以访问像我们在本章中介绍的 Spark 集群之一。


# 第七章:连接

> 他们没有选择权。
> 
> —丹妮莉丝·坦格利安

第六章 展示了主要的集群计算趋势、集群管理器、分发和云服务提供商,帮助您选择最适合您需求的 Spark 集群。相比之下,本章介绍了 Spark 集群的内部组件以及如何连接到特定的 Spark 集群。

在阅读本章时,请不要尝试执行每一行代码;这将非常困难,因为您需要准备不同的 Spark 环境。相反,如果您已经有了 Spark 集群,或者前一章激发了您注册按需集群的兴趣,现在是学习如何连接的时候了。本章帮助您连接到您已经选择的集群。如果没有集群,我们建议您先学习这些概念,然后再回来执行代码。

此外,本章提供了各种故障排除连接技术。虽然希望您不需要使用它们,但本章将您准备好使用它们作为解决连接问题的有效技术。

虽然本章可能会让人感到有些枯燥——连接和故障排除连接显然不是大规模计算中最令人兴奋的部分——它介绍了 Apache Spark 的组件以及它们如何交互,通常被称为 Apache Spark 的*架构*。本章与第八章和第九章一起,将详细介绍 Spark 的工作原理,帮助您成为能够真正理解使用 Apache Spark 进行分布式计算的中级用户。

# 概览

Spark 集群的整体连接架构由三种类型的计算实例组成:*驱动节点*、*工作节点*和*集群管理器*。集群管理器是一个允许 Spark 在集群中执行的服务;这在 “管理器” 中有详细说明。工作节点(也称为*执行者*)执行分区数据上的计算任务,并与其他工作节点或驱动节点交换中间结果。驱动节点负责将工作委派给工作节点,同时汇总它们的结果并控制计算流程。在大多数情况下,聚合发生在工作节点中;然而,即使在节点聚合数据之后,驱动节点通常也需要收集工作节点的结果。因此,驱动节点通常至少具有,但通常比工作节点拥有更多的计算资源(内存、CPU、本地存储等)。

严格来说,驱动节点和工作节点只是赋予特定角色机器的名称,而驱动节点中的实际计算由*Spark 上下文*执行。Spark 上下文是 Spark 功能的主要入口点,负责调度任务、管理存储、跟踪执行状态、指定访问配置设置、取消作业等。在工作节点中,实际计算是由*Spark executor*执行的,它是负责针对特定数据分区执行子任务的 Spark 组件。

图 7-1 说明了这一概念,驱动节点通过集群管理器协调工作节点的工作。

![Apache Spark 架构](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0701.png)

###### 图 7-1\. Apache Spark 连接架构

如果您的组织中已经有了 Spark 集群,您应该向集群管理员请求连接到该集群的连接信息,仔细阅读其使用政策,并遵循其建议。由于一个集群可以被多个用户共享,您希望确保仅请求您所需的计算资源。我们在第 9 章中介绍了如何请求资源。系统管理员将指定集群是*本地*还是*云*集群,使用的集群管理器,支持的*连接*和支持的*工具*。您可以使用这些信息直接跳转到适合您情况的 Local、Standalone、YARN、Mesos、Livy 或 Kubernetes。

###### 注意

使用 `spark_connect()` 连接后,您可以使用 `sc` 连接使用前面章节中介绍的所有技术;例如,您可以使用相同代码进行数据分析或建模。

## 边缘节点

计算集群配置为在节点之间实现高带宽和快速网络连接。为了优化网络连接,集群中的节点被配置为相互信任并禁用安全功能。这样可以提高性能,但需要关闭所有外部网络通信,使整个集群在整体上变得安全,除了一些仔细配置为接受外部连接的集群机器之外;从概念上讲,这些机器位于集群的“边缘”,称为*边缘节点*。

因此,在连接到 Apache Spark 之前,您可能需要先连接到集群中的一个边缘节点。有两种方法可以连接:

终端

使用计算机终端应用程序,您可以使用[安全外壳](http://bit.ly/2TE8cY9)建立到集群的远程连接;连接到集群后,您可以启动 R 然后使用`sparklyr`。然而,对于一些任务,比如探索性数据分析,终端可能不太方便,因此通常仅在配置集群或解决问题时使用。

Web 浏览器

虽然可以从终端使用`sparklyr`,但通常在边缘节点上安装*Web 服务器*更具生产力,该服务器提供通过 Web 浏览器运行带有`sparklyr`的 R 的访问。大多数情况下,您可能希望考虑使用 RStudio 或 Jupyter 而不是从终端连接。

图 7-2 通过可视化方式解释了这些概念。左边的块通常是您的 Web 浏览器,右边的块是边缘节点。在使用 Web 浏览器时,客户端和边缘节点通过 HTTP 进行通信;在使用终端时,通过安全外壳(SSH)进行通信。

![连接到 Spark 的边缘节点](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0702.png)

###### 图 7-2\. 连接到 Spark 的边缘节点

## Spark 主页

连接到边缘节点后的下一步是确定 Spark 安装的位置,这个位置称为`SPARK_HOME`。在大多数情况下,您的集群管理员将已经设置了`SPARK_HOME`环境变量以指向正确的安装路径。如果没有,则必须获取正确的*SPARK_HOME*路径,并在运行`spark_connect()`时使用`spark_home`参数显式指定。

如果您的集群提供程序或集群管理员已为您提供了`SPARK_HOME`,则以下代码应返回路径而不是空字符串:

Sys.getenv("SPARK_HOME")


如果此代码返回空字符串,则意味着您的集群中未设置`SPARK_HOME`环境变量,因此您需要在使用`spark_connect()`时指定`SPARK_HOME`,如下所示:

sc <- spark_connect(master = "master", spark_home = "local/path/to/spark")


在这个示例中,`master`被设置为 Spark Standalone,YARN,Mesos,Kubernetes,或 Livy 的正确集群管理器主节点。

# 本地

当您连接到本地模式的 Spark 时,Spark 会启动一个单进程,该进程运行大部分集群组件,如 Spark 上下文和单个执行器。这非常适合学习 Spark、离线工作、故障排除问题或在运行大型计算集群之前测试代码。图 7-3 展示了连接到 Spark 的本地连接。

![本地连接图表](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0703.png)

###### 图 7-3\. 本地连接图表

注意,在本地模式下,既没有集群管理器也没有工作进程,因为一切都在驱动应用程序内部运行。值得注意的是,`sparklyr`通过`spark-submit`启动 Spark 上下文,这是每个 Spark 安装中都有的脚本,允许用户提交自定义应用程序到 Spark。如果你感兴趣,第十三章解释了在`sparklyr`中提交此应用程序并从 R 正确连接时发生的内部流程。

要执行此本地连接,我们可以使用前几章节中熟悉的代码:

Connect to local Spark instance

sc <- spark_connect(master = "local")


# 独立模式

连接到 Spark 独立集群需要集群管理器主实例的位置,您可以在集群管理器 Web 界面上找到它,如“独立”章节所述。您可以通过查找以`spark://`开头的 URL 来找到此位置。

在独立模式下的连接始于`sparklyr`,它启动`spark-submit`,然后提交`sparklyr`应用程序并创建 Spark 上下文,该上下文请求来自指定`master`地址下运行的 Spark 独立实例的执行器。

图 7-4 说明了这个过程,它与图 7-1 中的整体连接架构非常相似,但包含了针对独立集群和`sparklyr`的特定细节。

![Spark 独立连接图解](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0704.png)

###### 图 7-4. Spark 独立连接图解

要连接,请在`spark_connect()`中使用`master = "spark://hostname:port"`,如下所示:

sc <- spark_connect(master = "spark://hostname:port")


# YARN

Hadoop YARN 是来自 Hadoop 项目的集群管理器。它是最常见的集群管理器,您可能会在以 Hadoop 集群为基础的集群中找到,包括 Cloudera、Hortonworks 和 MapR 发行版,以及在使用 Amazon EMR 时。YARN 支持两种连接模式:YARN 客户端和 YARN 集群。然而,与 YARN 集群相比,YARN 客户端模式更为常见,因为它更高效且更容易设置。

## YARN 客户端

当你以 YARN 客户端模式连接时,驱动实例运行 R、`sparklyr`和 Spark 上下文,它请求 YARN 从 YARN 获取工作节点以运行 Spark 执行器,如图 7-5 所示。

![YARN 客户端连接图解](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0705.png)

###### 图 7-5. YARN 客户端连接图解

要连接,只需运行`master = "yarn"`,如下所示:

sc <- spark_connect(master = "yarn")


在幕后,当你以 YARN 客户端模式运行时,集群管理器会执行你期望集群管理器执行的操作:从集群分配资源并将它们分配给你的 Spark 应用程序,由 Spark 上下文为您管理。在“YARN”中需要注意的重要部分是,Spark 上下文驻留在您运行 R 代码的同一台机器上;而在集群模式下运行 YARN 时则不同。

## YARN 集群

在集群模式和客户端模式下运行 YARN 的主要区别在于,在集群模式下,驱动节点不需要是运行 R 和 `sparklyr` 的节点;相反,驱动节点仍然是指定的驱动节点,通常是运行 R 的边缘节点的不同节点。当边缘节点具有过多并发用户、缺乏计算资源或需要独立管理工具(如 RStudio 或 Jupyter)时,考虑使用集群模式可能会有所帮助。

图 7-6 显示了在集群模式下运行时不同组件如何解耦。请注意,仍然存在一条连接线将客户端与集群管理器连接在一起;但是,在分配资源后,客户端直接与驱动节点通信,驱动节点再与工作节点通信。从 图 7-6 可以看出,集群模式看起来比客户端模式复杂得多——这种评估是正确的;因此,如果可能的话,最好避免使用集群模式,因为它会增加额外的配置开销。

![YARN 集群连接图示](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0706.png)

###### 图 7-6\. YARN 集群连接图示

要在 YARN 集群模式下连接,只需运行以下命令:

sc <- spark_connect(master = "yarn-cluster")


集群模式假设运行 `spark_connect()` 的节点已经正确配置,即 `yarn-site.xml` 存在并且 `YARN_CONF_DIR` 环境变量已经正确设置。当使用 Hadoop 作为文件系统时,您还需要正确配置 `HADOOP_CONF_DIR` 环境变量。此外,您需要确保客户端和驱动节点之间的网络连接良好——不仅仅是两台机器可以相互访问,还要确保它们之间有足够的带宽。通常情况下,这些配置由系统管理员提供,不是您需要手动配置的内容。

# Livy

与需要在集群中使用边缘节点的其他连接方法相反,Livy 提供了一个 *web API*,可以从集群外访问 Spark 集群,并且不需要在客户端安装 Spark。通过 web API 连接后,*Livy 服务* 通过向集群管理器请求资源并像往常一样分发工作来启动 Spark 上下文。图 7-7 展示了一个 Livy 连接示例;请注意,客户端通过 web API 远程连接到驱动节点。

![Livy 连接图示](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0707.png)

###### 图 7-7\. Livy 连接图示

通过 Livy 连接需要 Livy 服务的 URL,类似于 `https://hostname:port/livy`。由于允许远程连接,连接通常至少需要基本认证:

sc <- spark_connect(
master = "https://hostname:port/livy",
method = "livy", config = livy_config(
spark_version = "2.3.1",
username = "",
password = ""
))


要在本地机器上尝试 Livy,您可以安装和运行 Livy 服务,如“Livy”章节所述,然后按以下方式连接:

sc <- spark_connect(
master = "http://localhost:8998",
method = "livy",
version = "2.3.1")


连接通过 Livy 后,您可以使用任何`sparklyr`功能;但是,Livy 不适合探索性数据分析,因为执行命令会有显著的性能成本。尽管如此,在运行长时间计算时,这种开销可能被认为是不重要的。总的来说,您应该尽量避免使用 Livy,并直接在集群的边缘节点上工作;当不可行时,使用 Livy 可能是一个合理的方法。

###### 注意

通过`spark_version`参数指定 Spark 版本是可选的;但是,当指定版本时,通过部署与给定版本兼容的预编译 Java 二进制文件,可以显著提高性能。因此,连接到 Spark 使用 Livy 时最佳实践是指定 Spark 版本。

# Mesos

与 YARN 类似,Mesos 支持客户端模式和集群模式;但是,`sparklyr`目前仅支持 Mesos 下的客户端模式。因此,图 7-8 所示的图表与仅将集群管理器从 YARN 更改为 Mesos 的 YARN 客户端图表相当。

![Mesos 连接图](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0708.png)

###### 图 7-8\. Mesos 连接图

连接需要 Mesos 主节点的地址,通常形式为`mesos://host:port`或者对于使用 ZooKeeper 的 Mesos 为`mesos://zk://host1:2181,host2:2181,host3:2181/mesos`:

sc <- spark_connect(master = "mesos://host:port")


当您在本地机器上运行 Mesos 时,需要由系统管理员或手动设置`MESOS_NATIVE_JAVA_LIBRARY`环境变量。例如,在 macOS 上,您可以从终端安装和初始化 Mesos,然后手动设置`mesos`库并使用`spark_connect()`连接:

brew install mesos
/usr/local/Cellar/mesos/1.6.1/sbin/mesos-master --registry=in_memory
--ip=127.0.0.1 MESOS_WORK_DIR=. /usr/local/Cellar/mesos/1.6.1/sbin/mesos-slave
--master=127.0.0.1:5050


Sys.setenv(MESOS_NATIVE_JAVA_LIBRARY =
"/usr/local/Cellar/mesos/1.6.1/lib/libmesos.dylib")

sc <- spark_connect(master = "mesos://localhost:5050",
spark_home = spark_home_dir())


# Kubernetes

Kubernetes 集群不支持像 Mesos 或 YARN 那样的客户端模式;相反,连接模型类似于 YARN 集群,其中由 Kubernetes 分配驱动节点,如图 7-9 所示。

![Kubernetes 连接图](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0709.png)

###### 图 7-9\. Kubernetes 连接图

要使用 Kubernetes,您需要准备一个安装了 Spark 并正确配置的虚拟机;但是,本书不涵盖如何创建虚拟机的范围。一旦创建,连接到 Kubernetes 的工作方式如下:

library(sparklyr)
sc <- spark_connect(config = spark_config_kubernetes(
"k8s://https://:",
account = "default",
image = "docker.io/owner/repo:version",
version = "2.3.1"))


如果您的计算机已配置为使用 Kubernetes 集群,您可以使用以下命令查找`apiserver-host`和`apiserver-port`:

system2("kubectl", "cluster-info")


# 云端

当您使用云服务提供商时,有几点连接差异。例如,从 Databricks 连接需要以下连接方法:

sc <- spark_connect(method = "databricks")


由于 Amazon EMR 使用 YARN,您可以使用`master = "yarn"`连接:

sc <- spark_connect(master = "yarn")


当使用 IBM 的 Watson Studio 连接到 Spark 时,需要通过 IBM 提供的 `load_spark_kernels()` 函数检索配置对象:

kernels <- load_spark_kernels()
sc <- spark_connect(config = kernels[2])


在 Microsoft Azure HDInsights 和使用 ML 服务(R Server)时,Spark 连接初始化如下:

library(RevoScaleR)
cc <- rxSparkConnect(reset = TRUE, interop = "sparklyr")
sc <- rxGetSparklyrConnection(cc)


从 Qubole 连接需要使用 `qubole` 连接方法:

sc <- spark_connect(method = "qubole")


如果需要帮助,请参考您的云服务提供商的文档和支持渠道。

# 批处理

大部分时间,您会交互式地使用 `sparklyr`;也就是说,您会明确使用 `spark_connect()` 进行连接,然后执行命令来分析和建模大规模数据。然而,您也可以通过调度使用 `sparklyr` 的 Spark 作业来自动化流程。Spark 并没有提供调度数据处理任务的工具;相反,您可以使用其他工作流管理工具。这对于在夜间转换数据、准备模型和得分数据或通过其他系统利用 Spark 非常有用。

例如,您可以创建一个名为 `batch.R` 的文件,其内容如下:

library(sparklyr)

sc <- spark_connect(master = "local")

sdf_len(sc, 10) %>% spark_write_csv("batch.csv")

spark_disconnect(sc)


您随后可以使用 `spark_submit()` 将此应用程序以批处理模式提交到 Spark;`master` 参数应适当设置:

spark_submit(master = "local", "batch.R")


您还可以直接从 shell 中通过以下方式调用 `spark-submit`:

/spark-home-path/spark-submit
--class sparklyr.Shell '/spark-jars-path/sparklyr-2.3-2.11.jar'
8880 12345 --batch /path/to/batch.R


最后的参数表示端口号 `8880` 和会话号 `12345`,您可以将其设置为任何唯一的数值标识符。您可以使用以下 R 代码获取正确的路径:

Retrieve spark-home-path

spark_home_dir()

Retrieve spark-jars-path

system.file("java", package = "sparklyr")


您可以通过向 `spark-submit` 传递额外的命令行参数来自定义脚本,然后在 R 中使用 `commandArgs()` 读取这些参数。

# 工具

当使用像 Jupyter 和 RStudio 这样的工具连接到 Spark 集群时,您可以运行本章中提供的相同连接参数。然而,由于许多云提供商使用 Web 代理来保护 Spark 的 Web 界面,要使用 `spark_web()` 或 RStudio Connections 窗格扩展,您需要正确配置 `sparklyr.web.spark` 设置,然后通过 `config` 参数传递给 `spark_config()`。

例如,在使用 Amazon EMR 时,您可以通过动态检索 YARN 应用程序和构建 EMR 代理 URL 来配置 `sparklyr.web.spark` 和 `sparklyr.web.yarn`:

domain <- "http://ec2-12-345-678-9.us-west-2.compute.amazonaws.com"
config <- spark_config()
config\(sparklyr.web.spark <- ~paste0( domain, ":20888/proxy/", invoke(spark_context(sc), "applicationId")) config\)sparklyr.web.yarn <- paste0(domain, ":8088")

sc <- spark_connect(master = "yarn", config = config)


# 多连接

通常只需一次连接到 Spark。但是,您也可以通过连接到不同的集群或指定 `app_name` 参数来打开多个连接到 Spark。这对于比较 Spark 版本或在提交到集群之前验证分析结果非常有帮助。以下示例打开到 Spark 1.6.3、2.3.0 和 Spark Standalone 的连接:

Connect to local Spark 1.6.3

sc_16 <- spark_connect(master = "local", version = "1.6")

Connect to local Spark 2.3.0

sc_23 <- spark_connect(master = "local", version = "2.3", appName = "Spark23")

Connect to local Spark Standalone

sc_standalone <- spark_connect(master = "spark://host:port")


最后,您可以断开每个连接:

spark_disconnect(sc_1_6_3)
spark_disconnect(sc_2_3_0)
spark_disconnect(sc_standalone)


或者,您也可以一次断开所有连接:

spark_disconnect_all()


# 故障排除

最后但同样重要的是,我们介绍以下排查技术:*日志记录*、*Spark 提交*和*Windows*。当对如何开始感到犹豫不决时,请从使用 Windows 系统的 Windows 部分开始,然后是日志记录,最后是 Spark 提交。在使用`spark_connect()`时失败并显示错误消息时,这些技术非常有用。

## 日志记录

排查连接问题的第一技术是直接将 Spark 日志打印到控制台,以帮助您发现额外的错误消息:

sc <- spark_connect(master = "local", log = "console")


此外,当连接时,您可以通过将`sparklyr.verbose`选项设置为`TRUE`来启用详细日志记录:

sc <- spark_connect(master = "local", log = "console",
config = list(sparklyr.verbose = TRUE))


## 提交 Spark 作业

您可以通过运行示例作业通过`spark-submit`来诊断连接问题是否特定于 R 或 Spark:

Find the spark directory using an environment variable

spark_home <- Sys.getenv("SPARK_HOME")

Or by getting the local spark installation

spark_home <- sparklyr::spark_home_dir()


然后,通过将`"local"`替换为您正在排查的正确主节点参数,执行样本计算 Pi 示例:

Launching a sample application to compute Pi

system2(
file.path(spark_home, "bin", "spark-submit"),
c(
"--master", "local",
"--class", "org.apache.spark.examples.SparkPi",
dir(file.path(spark_home, "examples", "jars"),
pattern = "spark-examples", full.names = TRUE),
100),
stderr = FALSE
)


Pi is roughly 3.1415503141550314


如果未显示前述消息,则需要调查您的 Spark 集群为何未正确配置,这超出了本书的范围。作为开始,重新运行 Pi 示例但删除`stderr = FALSE`;这会将错误打印到控制台,您可以使用这些错误来调查可能出现的问题。在使用云提供商或 Spark 分发时,您可以联系其支持团队以帮助您进一步排查;否则,Stack Overflow 是一个很好的起点。

如果您看到该消息,则表示您的 Spark 集群已正确配置,但某种方式 R 无法使用 Spark,因此您需要详细排查问题,如我们将在接下来解释的那样。

### 详细排查

要详细排查连接过程,您可以手动复制两步连接过程,这通常非常有助于诊断连接问题。首先,从 R 触发`spark-submit`,将应用程序提交到 Spark;其次,R 连接到运行中的 Spark 应用程序。

首先,确定 Spark 安装目录和运行以下命令来查找正确的`sparklyr*.jar`文件的路径:

dir(system.file("java", package = "sparklyr"),
pattern = "sparklyr", full.names = T)


确保您识别与您的 Spark 集群匹配的正确版本,例如,适用于 Spark 2.1 的`sparklyr-2.1-2.11.jar`。

然后,从终端运行以下命令:

$SPARK_HOME/bin/spark-submit --class sparklyr.Shell $PATH_TO_SPARKLYR_JAR 8880 42


18/06/11 12:13:53 INFO sparklyr: Session (42) found port 8880 is available
18/06/11 12:13:53 INFO sparklyr: Gateway (42) is waiting for sparklyr client
to connect to port 8880


参数`8880`代表在`sparklyr`中使用的默认端口,而 42 是会话号码,它是由`sparklyr`生成的密码安全数字,但出于排查目的可以简单地是`42`。

如果此第一连接步骤失败,则表示集群无法接受该应用程序。这通常意味着资源不足或存在权限限制。

第二步是从 R 连接如下(请注意,有一个 60 秒的超时,因此您需要在运行终端命令后运行 R 命令;如果需要,您可以按照第九章中描述的配置此超时):

library(sparklyr)
sc <- spark_connect(master = "sparklyr://localhost:8880/42", version = "2.3")


如果第二次连接尝试失败,通常意味着 R 和驱动节点之间存在连接问题。你可以尝试使用不同的连接端口。

## Windows

在大多数情况下,从 Windows 连接与从 Linux 和 macOS 连接一样简单。但是,有一些常见的连接问题需要注意:

+   防火墙和防病毒软件可能会阻止连接端口。`sparklyr` 使用的默认端口是 `8880`;请确保此端口未被阻止。

+   长路径名可能会导致问题,特别是在像 Windows 7 这样的旧系统中。当使用这些系统时,尝试使用最多八个字符且名称中不包含空格的所有文件夹安装的 Spark 进行连接。

# 小结

本章概述了 Spark 的架构、连接概念,并提供了在本地模式、独立模式、YARN、Mesos、Kubernetes 和 Livy 连接的示例。还介绍了边缘节点及其在连接到 Spark 集群时的角色。这些信息应足以帮助你成功连接到任何 Apache Spark 集群。

若要解决本章未描述的连接问题,请建议在 Stack Overflow、[`sparklyr` 问题 GitHub 页面](http://bit.ly/2Z72XWa),以及必要时在 [新的 `sparklyr` GitHub 问题](http://bit.ly/2HasCmq) 中进一步协助。

在 第八章 中,我们介绍了如何使用 Spark 从各种数据源和格式读取和写入数据,这使得您在添加新数据源进行数据分析时更加灵活。过去可能需要花费几天、几周甚至几个月的工作,现在可以在几小时内完成。


# 第八章:数据

> 你有没有想过她可能并不是一个可靠的信息来源?
> 
> —琼恩·雪诺

借助前几章所学,你现在具备了开始规模化分析和建模的知识!然而,到目前为止,我们还没有详细解释如何将数据读入 Spark。我们探讨了如何使用`copy_to()`上传小数据集或者像`spark_read_csv()`或`spark_write_csv()`这样的函数,但并没有详细解释其具体操作和原因。

所以,你即将学习如何使用 Spark 读取和写入数据。而且,虽然这本身很重要,但本章还将向你介绍*数据湖*——一个以其自然或原始格式存储数据的仓库,相比现有的存储架构,它提供了各种好处。例如,你可以轻松地集成来自外部系统的数据,而无需将其转换为通用格式,也无需假设这些来源与你内部数据源一样可靠。

此外,我们还将讨论如何扩展 Spark 的能力,以处理默认情况下无法访问的数据,并提出几条关于优化读写数据性能的建议。要读取大型数据集通常需要微调你的 Spark 集群配置,但这是第九章的主题。

# 概述

在第一章中,你了解到除了大数据和大计算之外,你还可以使用 Spark 来提高数据任务中的速度、多样性和真实性。虽然你可以将本章的学习应用于任何需要加载和存储数据的任务,但将本章放在处理多种数据源的背景下尤为有趣。要理解原因,我们首先应该快速了解一下当前许多组织如何处理数据。

多年来,将大型数据集存储在关系型*数据库*中已经成为一种常见做法,这个系统最早由 Edgar F. Codd 在 1970 年提出。¹ 你可以将数据库视为一个相互关联的表集合,每个表都精心设计以容纳特定的数据类型和与其他表的关系。大多数关系型数据库系统使用*结构化查询语言*(SQL)进行查询和维护数据库。数据库至今仍然被广泛使用,理由很充分:它们可靠而一致地存储数据;事实上,你的银行可能正是将账户余额存储在数据库中,这是一个良好的实践。

然而,数据库还被用来存储来自其他应用程序和系统的信息。例如,您的银行还可能存储其他银行产生的数据,如进账支票。为了完成这个任务,外部数据需要从外部系统中提取出来,转换为适合当前数据库的形式,最后加载到其中。这被称为*提取、转换和加载*(ETL),这是将数据从一个或多个源复制到表示与源数据不同的目标系统的一般过程。ETL 过程在 1970 年代变得流行起来。

除了数据库外,数据通常还加载到*数据仓库*中,这是用于报告和数据分析的系统。数据通常以增加数据分析速度的格式存储和索引,但通常不适合建模或运行定制分布式代码。挑战在于改变数据库和数据仓库通常是一个漫长而微妙的过程,因为需要重新索引数据,并且来自多个数据源的数据需要小心地转换为在所有数据源之间共享的单一表格。

而不是试图将所有数据源转换为通用格式,您可以在*数据湖*中接受这种多样化的数据源,即一种以其自然格式存储的数据系统或数据存储库(参见图 8-1)。由于数据湖使数据以其原始格式可用,因此无需事先仔细转换它;任何人都可以用它进行分析,这增加了与 ETL 相比的显著灵活性。然后,您可以使用 Spark 通过一个可在所有这些数据源上扩展的单一接口统一来自数据湖、数据库和数据仓库的数据处理。一些组织还使用 Spark 替换其现有的 ETL 过程;然而,这超出了本书的范围,我们在图 8-1 中用虚线来说明这一点。

![Spark 处理来自数据湖、数据库和数据仓库的原始数据](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0801.png)

###### 图 8-1\. Spark 处理来自数据湖、数据库和数据仓库的原始数据

为了支持广泛的数据源,Spark 需要能够读取和写入多种不同的文件格式(CSV、JSON、Parquet 等),并且在存储在多个文件系统中的数据(HDFS、S3、DBFS 等)中访问它们,并且可能与其他存储系统(数据库、数据仓库等)进行互操作。我们会讲到所有这些内容,但首先,我们将开始介绍如何使用 Spark 读取、写入和复制数据。

# 读取数据

如果您是 Spark 的新手,在开始处理大型数据集之前,强烈建议您先审查本节内容。我们将介绍几种提高读取数据速度和效率的技术。每个子节都介绍了利用 Spark 读取文件的特定方法,例如能够将整个文件夹视为数据集以及能够描述它们以在 Spark 中更快地读取数据集。

## 路径

在分析数据时,将多个文件加载到单个数据对象中是一种常见情况。在 R 中,我们通常使用循环或函数式编程指令来完成这个任务。这是因为 R 必须将每个文件单独加载到您的 R 会话中。让我们先在一个文件夹中创建几个 CSV 文件,然后用 R 读取它们:

letters <- data.frame(x = letters, y = 1:length(letters))

dir.create("data-csv")
write.csv(letters[1:3, ], "data-csv/letters1.csv", row.names = FALSE)
write.csv(letters[1:3, ], "data-csv/letters2.csv", row.names = FALSE)

do.call("rbind", lapply(dir("data-csv", full.names = TRUE), read.csv))


x y
1 a 1
2 b 2
3 c 3
4 a 1
5 b 2
6 c 3


在 Spark 中,有一个将文件夹视为数据集的概念。而不是枚举每个文件,只需传递包含所有文件的路径。Spark 假定该文件夹中的每个文件都是同一个数据集的一部分。这意味着目标文件夹应仅用于数据目的。这一点尤为重要,因为像 HDFS 这样的存储系统会将文件存储在多台机器上,但从概念上讲,它们存储在同一个文件夹中;当 Spark 从此文件夹读取文件时,实际上是在执行分布式代码以在每台机器上读取每个文件 —— 在分布式文件读取时,不会在机器之间传输数据:

library(sparklyr)
sc <- spark_connect(master = "local", version = "2.3")

spark_read_csv(sc, "data-csv/")


Source: spark [?? x 2]

x y

1 a 1
2 b 2
3 c 3
4 d 4
5 e 5
6 a 1
7 b 2
8 c 3
9 d 4
10 e 5


“文件夹作为表格”的想法在其他开源技术中也可以找到。在底层,Hive 表的工作方式相同。当您查询 Hive 表时,映射是在同一文件夹中的多个文件上完成的。该文件夹的名称通常与用户可见的表格名称相匹配。

接下来,我们将介绍一种技术,允许 Spark 更快地读取文件,并通过提前描述数据集的结构来减少读取失败的情况。

## 模式

在读取数据时,Spark 能够确定数据源的列名和列类型,也称为*模式*。然而,猜测模式会带来成本;Spark 需要对数据进行初始扫描来猜测数据类型。对于大型数据集,这可能会为数据摄取过程增加显著的时间成本,即使对于中等大小的数据集也是如此。对于反复读取的文件,额外的读取时间会随着时间的推移累积。

为了避免这种情况,Spark 允许您通过提供 `columns` 参数来定义列,描述您的数据集。您可以通过自己取样原始文件的一个小部分来创建这个模式:

spec_with_r <- sapply(read.csv("data-csv/letters1.csv", nrows = 10), class)
spec_with_r


    x         y

"factor" "integer"


或者,您可以将列规范设置为包含显式列类型的向量。向量的值被命名以匹配字段名称:

spec_explicit <- c(x = "character", y = "numeric")
spec_explicit


      x           y

"character" "numeric"


接受的变量类型包括:`integer`、`character`、`logical`、`double`、`numeric`、`factor`、`Date` 和 `POSIXct`。

然后,在使用`spark_read_csv()`读取时,可以将`spec_with_r`传递给`columns`参数,以匹配原始文件的名称和类型。这有助于提高性能,因为 Spark 无需确定列类型。

spark_read_csv(sc, "data-csv/", columns = spec_with_r)


Source: spark [?? x 2]

x y

1 a 1
2 b 2
3 c 3
4 a 1
5 b 2
6 c 3


下面的示例显示如何将字段类型设置为不同的内容。但是,新字段类型必须是原始数据集中的兼容类型。例如,您不能将`character`字段设置为`numeric`。如果使用不兼容的类型,文件读取将失败并显示错误。此外,以下示例还更改了原始字段的名称:

spec_compatible <- c(my_letter = "character", my_number = "character")

spark_read_csv(sc, "data-csv/", columns = spec_compatible)


Source: spark [?? x 2]

my_letter my_number

1 a 1
2 b 2
3 c 3
4 a 1
5 b 2
6 c 3


在 Spark 中,格式错误的条目可能会在读取时引发错误,特别是对于非字符字段。为了避免这种错误,我们可以使用一个文件规范,将它们导入为字符,然后使用`dplyr`将字段强制转换为所需的类型。

本小节回顾了如何更快地读取文件并减少失败,这使我们能够更快地开始分析。加速分析的另一种方法是将更少的数据加载到 Spark 内存中,这将在下一节中进行探讨。

## 内存

默认情况下,在使用 R 语言与 Spark 时,当您读取数据时,数据会被复制到 Spark 的分布式内存中,使得数据分析和其他操作非常快速。有些情况下,比如数据量太大时,加载所有数据可能并不切实际,甚至是不必要的。对于这些情况,Spark 可以只是“映射”文件,而不将数据复制到内存中。

映射在 Spark 中创建了一种虚拟表。其含义是,当针对该表运行查询时,Spark 需要在那时从文件中读取数据。之后的任何连续读取也将执行相同操作。实际上,Spark 成为数据的传递。该方法的优点是几乎没有“读取”文件的前期时间成本;映射非常快速。缺点是实际提取数据的查询将需要更长时间。

这由读取函数的`memory`参数控制。将其设置为`FALSE`可以防止数据复制(默认为`TRUE`):

mapped_csv <- spark_read_csv(sc, "data-csv/", memory = FALSE)


这种方法有很好的使用案例,其中之一是当不需要表的所有列时。例如,假设有一个包含许多列的非常大的文件。假设这不是您第一次与这些数据互动,您将知道分析所需的列。当您知道需要哪些列时,可以使用`memory = FALSE`读取文件,然后使用`dplyr`选择所需的列。然后可以将生成的`dplyr`变量缓存到内存中,使用`compute()`函数。这将使 Spark 查询文件(们),提取所选字段,并仅将该数据复制到内存中。结果是一个内存中的表,相对较少的时间用于摄入:

mapped_csv %>%
dplyr::select(y) %>%
dplyr::compute("test")


下一节介绍了一个简短的技术,使得更容易携带导入数据的原始字段名称。

## 列

Spark 1.6 要求列名进行过滤,所以 R 默认会这样做。也许有些情况下你希望保留原始的列名,或者在使用 Spark 2.0 或更高版本时。要实现这一点,将 `sparklyr.sanitize.column.names` 选项设置为 `FALSE`:

options(sparklyr.sanitize.column.names = FALSE)
copy_to(sc, iris, overwrite = TRUE)


Source: table [?? x 5]

Database: spark_connection

Sepal.Length Sepal.Width Petal.Length Petal.Width Species

1 5.1 3.5 1.4 0.2 setosa
2 4.9 3 1.4 0.2 setosa
3 4.7 3.2 1.3 0.2 setosa
4 4.6 3.1 1.5 0.2 setosa
5 5 3.6 1.4 0.2 setosa
6 5.4 3.9 1.7 0.4 setosa
7 4.6 3.4 1.4 0.3 setosa
8 5 3.4 1.5 0.2 setosa
9 4.4 2.9 1.4 0.2 setosa
10 4.9 3.1 1.5 0.1 setosa

... with more rows


在这次对如何将数据读入 Spark 进行回顾之后,我们继续探讨如何从 Spark 会话中写入数据。

# 写入数据

一些项目要求在 Spark 中生成的新数据被写回到远程源。例如,数据可能是由 Spark 模型返回的新预测值。作业处理了大量的预测生成,但接下来需要存储这些预测结果。本节重点讨论了如何使用 Spark 将数据从 Spark 移动到外部目的地。

许多新用户开始通过下载 Spark 数据到 R,然后再上传到目标位置,如 图 8-2 所示。这对较小的数据集有效,但对于较大的数据集则效率低下。数据通常会增长到无法让 R 成为中间环节的规模。

![在写入大型数据集时误用 Spark](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0802.png)

###### 图 8-2\. 在写入大型数据集时误用 Spark

应尽一切努力让 Spark 连接到目标位置。这样一来,读取、处理和写入就都在同一个 Spark 会话中完成。

如 图 8-3 所示,一个更好的方法是使用 Spark 来读取、处理和写入目标位置。这种方法能够扩展到 Spark 集群允许的规模,并且避免了 R 成为瓶颈。

![在写入大型数据集时正确使用 Spark](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/ms-spk-r/img/mswr_0803.png)

###### 图 8-3\. 在写入大型数据集时正确使用 Spark

考虑以下情景:一个 Spark 作业刚刚处理了一个大型数据集的预测结果,生成了大量的预测值。选择如何写入结果将取决于你所工作的技术基础设施。更具体地说,它将取决于 Spark 和目标是否在同一个集群中运行。

回到我们的情景,我们有一个需要保存的大型 Spark 数据集。当 Spark 和目标位于同一个集群中时,复制结果并不成问题;数据传输可以在同一集群的内存和磁盘之间高效地进行或通过高带宽连接进行有效的洗牌。

但是如果目标不在 Spark 集群内怎么办?有两个选择,选择其中一个将取决于数据的大小和网络速度:

Spark 传输

在这种情况下,Spark 连接到远程目标位置并复制新数据。如果这是在同一个数据中心或云服务提供商内完成的,数据传输可能足够快,以便让 Spark 直接写入数据。

外部传输和其他

Spark 可以将结果写入磁盘,并通过第三方应用程序传输它们。Spark 将结果写入文件,然后单独的作业将文件复制过去。在目标位置,你将使用单独的进程将数据转移到目标位置。

最好认识到 Spark、R 和其他技术都只是工具。没有工具可以做到所有事情,也不应该期望它可以。接下来我们描述如何将数据复制到 Spark 或收集不适合内存的大型数据集,这可以用于跨集群传输数据或帮助初始化分布式数据集。

# 复制数据

之前的章节使用 `copy_to()` 作为一个方便的辅助工具将数据复制到 Spark 中;然而,你只能使用 `copy_to()` 来传输已加载到内存中的内存数据集。这些数据集通常比你想要复制到 Spark 中的数据集小得多。

例如,假设我们有一个生成的 3 GB 数据集:

dir.create("largefile.txt")
write.table(matrix(rnorm(10 * 10⁶), ncol = 10), "largefile.txt/1",
append = T, col.names = F, row.names = F)
for (i in 2:30)
file.copy("largefile.txt/1", paste("largefile.txt/", i))


如果驱动节点只有 2 GB 内存,我们将无法使用 `copy_to()` 将这个 3 GB 文件加载到内存中。相反,当在你的集群中使用 HDFS 作为存储时,你可以使用 `hadoop` 命令行工具从终端将文件从磁盘复制到 Spark 中。请注意,以下代码仅在使用 HDFS 的集群中有效,而不适用于本地环境。

hadoop fs -copyFromLocal largefile.txt largefile.txt


然后,你可以按照 “文件格式” 部分中描述的方式读取上传的文件;对于文本文件,你可以运行:

spark_read_text(sc, "largefile.txt", memory = FALSE)


Source: spark [?? x 1]

line

1 0.0982531064914565 -0.577567317599452 -1.66433938237253 -0.20095089489…
2 -1.08322304504007 1.05962389624635 1.1852771207729 -0.230934710049462 …
3 -0.398079835552421 0.293643382374479 0.727994248743204 -1.571547990532…
4 0.418899768227183 0.534037617828835 0.921680317620166 -1.6623094393911…
5 -0.204409401553028 -0.0376212693728992 -1.13012269711811 0.56149527218…
6 1.41192628218417 -0.580413572014808 0.727722566256326 0.5746066486689 …
7 -0.313975036262443 -0.0166426329807508 -0.188906975208319 -0.986203251…
8 -0.571574679637623 0.513472254005066 0.139050812059352 -0.822738334753…
9 1.39983023148955 -1.08723592838627 1.02517804413913 -0.412680186313667…
10 0.6318328148434 -1.08741784644221 -0.550575696474202 0.971967251067794…

… with more rows


`collect()` 存在一个类似的限制,即它只能收集适合驱动器内存的数据集;然而,如果你需要从 Spark 中通过驱动节点提取大型数据集,可以使用分布式存储提供的专用工具。对于 HDFS,你可以运行以下命令:

hadoop fs -copyToLocal largefile.txt largefile.txt


或者,你也可以通过向 `collect()` 提供回调来收集不适合内存的数据集。回调只是一个将在每个 Spark 分区上调用的 R 函数。然后你可以将这些数据集写入磁盘或通过网络推送到其他集群。

即使驱动节点收集此数据集的内存少于 3 GB,你仍然可以使用以下代码收集 3 GB 数据。尽管如此,正如第三章所解释的那样,应避免将大型数据集收集到单台机器上,因为这会导致显著的性能瓶颈。为了简洁起见,我们只收集前一百万行;如果你有几分钟可以花费,可以删除 `head(10⁶)`:

dir.create("large")
spark_read_text(sc, "largefile.txt", memory = FALSE) %>%
head(10⁶) %>%
collect(callback = function(df, idx) {
writeLines(df$line, paste0("large/large-", idx, ".txt"))
})


确保清理这些大文件并清空回收站:

unlink("largefile.txt", recursive = TRUE)
unlink("large", recursive = TRUE)


在大多数情况下,数据已经存储在集群中,因此你不需要担心复制大型数据集;相反,你通常可以专注于读取和写入不同的文件格式,我们接下来会描述。

# 文件格式

Spark 可以直接与多种文件格式交互,如 CSV、JSON、LIBSVM、ORC 和 Parquet。表 8-1 将文件格式映射到应在 Spark 中使用的函数。

表 8-1\. Spark 读写文件格式的函数

| 格式 | 读取 | 写入 |
| --- | --- | --- |
| CSV | `spark_read_csv()` | `spark_write_csv()` |
| JSON | `spark_read_json()` | `spark_write_json()` |
| LIBSVM | `spark_read_libsvm()` | `spark_write_libsvm()` |
| ORC | `spark_read_orc()` | `spark_write_orc()` |
| Apache Parquet | `spark_read_parquet()` | `spark_write_parquet()` |
| 文本 | `spark_read_text()` | `spark_write_text()` |

下面的部分将描述特定于每种文件格式的特殊考虑事项,以及一些流行文件格式的优缺点,从广为人知的 CSV 文件格式开始。

## CSV

CSV 格式可能是当今最常用的文件类型。它由一个文本文件组成,以给定字符分隔,通常是逗号。读取 CSV 文件应该相当简单;然而,值得一提的是一些可以帮助您处理不完全符合规范的 CSV 文件的技术。Spark 提供了以下处理解析问题的模式:

宽容模式

插入 NULL 值以代替缺失的令牌

放弃格式不正确的行

删除格式不正确的行

快速失败

如果遇到任何格式不正确的行则中止

在`sparklyr`中,您可以通过将它们传递给`options`参数来使用这些。以下示例创建了一个带有损坏条目的文件。然后展示了如何将其读入 Spark:

Creates bad test file

writeLines(c("bad", 1, 2, 3, "broken"), "bad.csv")

spark_read_csv(
sc,
"bad3",
"bad.csv",
columns = list(foo = "integer"),
options = list(mode = "DROPMALFORMED"))


Source: spark [?? x 1]

foo
1 1 2 2 3 3 ```

Spark 提供了一个默认隐藏的问题追踪列。要启用它,请将_corrupt_record添加到columns列表中。您可以将其与使用PERMISSIVE模式结合使用。所有行将被导入,无效条目将接收NA,并在_corrupt_record列中跟踪问题:

spark_read_csv(
  sc,
  "bad2",
  "bad.csv",
  columns = list(foo = "integer", "_corrupt_record" = "character"),
  options = list(mode = "PERMISSIVE")
)
# Source: spark<bad2> [?? x 2]
    foo `_corrupt_record`
  <int> <chr>
1     1 NA
2     2 NA
3     3 NA
4    NA broken

将数据读取和存储为 CSV 文件是相当常见的,并且在大多数系统中都得到支持。对于表格数据集,这仍然是一个流行的选项,但对于包含嵌套结构和非表格数据的数据集,通常更喜欢使用 JSON。

JSON

JSON 是最初源自 JavaScript 并因其灵活性和普遍支持而变得与语言无关且非常流行的文件格式。读写 JSON 文件非常简单:

writeLines("{'a':1, 'b': {'f1': 2, 'f3': 3}}", "data.json")
simple_json <- spark_read_json(sc, "data.json")
simple_json
# Source: spark<data> [?? x 2]
      a b
  <dbl> <list>
1     1 <list [2]>

然而,当处理包含像这个例子中那样的嵌套字段的数据集时,值得指出如何提取嵌套字段。一种方法是使用 JSON 路径,这是一种常用的领域特定语法,用于提取和查询 JSON 文件。您可以使用get_json_object()to_json()的组合来指定您感兴趣的 JSON 路径。要提取f1,您可以运行以下转换:

simple_json %>% dplyr::transmute(z = get_json_object(to_json(b), '$.f1'))
# Source: spark<?> [?? x 3]
      a b          z
  <dbl> <list>     <chr>
1     1 <list [2]> 2

另一种方法是从 CRAN 安装sparklyr.nested,然后使用sdf_unnest()展开嵌套数据:

sparklyr.nested::sdf_unnest(simple_json, "b")
# Source: spark<?> [?? x 3]
      a    f1    f3
  <dbl> <dbl> <dbl>
1     1     2     3

虽然 JSON 和 CSV 非常简单且多用途,但它们并非针对性能优化;相比之下,ORC、AVRO 和 Parquet 等其他格式则是。

Parquet

Apache Parquet、Apache ORC 和 Apache AVRO 都是专为性能设计的文件格式。Parquet 和 ORC 以列格式存储数据,而 AVRO 是基于行的。它们都是二进制文件格式,可以减少存储空间并提高性能。这样做的代价是它们在外部系统和库中读取时可能稍微复杂一些;但在 Spark 中用作中间数据存储时通常不会成为问题。

为了说明这一点,图 8-4 展示了使用 bench 包运行的百万行写入速度基准测试结果;在决定哪种格式最适合您的需求时,请随意使用您自己的基准测试数据集:

numeric <- copy_to(sc, data.frame(nums = runif(10⁶)))
bench::mark(
  CSV = spark_write_csv(numeric, "data.csv", mode = "overwrite"),
  JSON = spark_write_json(numeric, "data.json", mode = "overwrite"),
  Parquet = spark_write_parquet(numeric, "data.parquet", mode = "overwrite"),
  ORC = spark_write_parquet(numeric, "data.orc", mode = "overwrite"),
  iterations = 20
) %>% ggplot2::autoplot()

从现在开始,每当我们出现新的 spark_connect() 命令时,请务必断开与 Spark 的连接:

spark_disconnect(sc)

这就结束了对一些内置支持的文件格式的介绍。接下来,我们描述需要外部包和定制化处理的格式的处理方法。

CSV、JSON、Parquet 和 ORC 之间的百万行写入基准测试

图 8-4. CSV、JSON、Parquet 和 ORC 之间的百万行写入基准测试

其他

Spark 是一个非常灵活的计算平台。通过使用称为包的扩展程序,它可以添加功能。您可以通过使用适当的包来访问新的数据源类型或文件系统。

包在连接时需要加载到 Spark 中。要加载包,Spark 需要知道其位置,可以是集群内部、文件共享或互联网上的位置。

sparklyr 中,包位置通过 spark_connect() 传递。所有包应列在连接配置的 sparklyr.connect.packages 项中。

可以访问我们之前未列出的数据源类型。加载 Spark 的适当默认包是两个步骤中的第一步。第二步是实际读取或写入数据。spark_read_source()spark_write_source() 函数就是这样做的。它们是通用函数,可以使用默认包导入的库。

例如,我们可以如下读取 XML 文件:

sc <- spark_connect(master = "local", version = "2.3", config = list(
  sparklyr.connect.packages = "com.databricks:spark-xml_2.11:0.5.0"))

writeLines("<ROWS><ROW><text>Hello World</text></ROW>", "simple.xml")
spark_read_source(sc, "simple_xml", "simple.xml", "xml")
# Source: spark<data> [?? x 1]
  text
  <chr>
1 Hello World

您也可以轻松地将数据写回 XML,如下所示:

tbl(sc, "simple_xml") %>%
  spark_write_source("xml", options = list(path = "data.xml"))

此外,R 社区还开发了一些扩展来加载额外的文件格式,例如用于嵌套数据的sparklyr.nested,用于从 SAS 读取数据的spark.sas7bdat,用于 AVRO 格式数据的sparkavro,以及用于读取 WARC 文件的sparkwarc,这些都使用了第十章中引入的可扩展机制。第十一章介绍了使用 R 包加载额外文件格式的技术,而第十三章介绍了使用 Java 库进一步补充这一点的技术。但首先,让我们探讨如何从几种不同的文件系统中检索和存储文件。

文件系统

Spark 默认使用当前运行的文件系统。在 YARN 管理的集群中,默认文件系统将是 HDFS。例如路径/home/user/file.csv将从集群的 HDFS 文件夹中读取,而不是 Linux 文件夹。操作系统的文件系统将用于其他部署,如独立部署和sparklyr的本地部署。

在读取或写入时可以更改文件系统协议。您可以通过sparklyr函数的path参数来实现这一点。例如,完整路径file://home/user/file.csv将强制使用本地操作系统的文件系统。

还有许多其他的文件系统协议,如用于 Databricks 文件系统的_dbfs://_,用于 Amazon S3 服务的_s3a://_,用于 Microsoft Azure 存储的_wasb://_,以及用于 Google 存储的_gs://_

Spark 并没有直接提供对所有这些功能的支持;相反,它们根据需要进行配置。例如,访问“s3a”协议需要将一个包添加到sparklyr.connect.packages配置设置中,而连接和指定适当的凭据可能需要使用AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY环境变量。

Sys.setenv(AWS_ACCESS_KEY_ID = my_key_id)
Sys.setenv(AWS_SECRET_ACCESS_KEY = my_secret_key)

sc <- spark_connect(master = "local", version = "2.3", config = list(
  sparklyr.connect.packages = "org.apache.hadoop:hadoop-aws:2.7.7"))

my_file <- spark_read_csv(sc, "my-file", path =  "s3a://my-bucket/my-file.csv")

访问其他文件协议需要加载不同的包,虽然在某些情况下,提供 Spark 环境的供应商可能会为您加载这些包。请参考您供应商的文档以了解是否适用。

存储系统

数据湖和 Spark 通常是密切相关的,可选择访问诸如数据库和数据仓库之类的存储系统。展示所有不同的存储系统并提供适当的示例将非常耗时,因此我们选择展示一些常用的存储系统。

作为一个开始,Apache Hive 是一个数据仓库软件,利用 SQL 方便地读取、写入和管理分布式存储中的大型数据集。事实上,Spark 直接内置了来自 Hive 的组件。在安装了 Spark 或 Hive 并存的情况下非常常见,所以我们将从介绍 Hive 开始,接着是 Cassandra,最后看一下 JDBC 连接。

Hive

在 YARN 管理的集群中,Spark 与 Apache Hive 有更深入的集成。在打开 Spark 连接后,可以轻松访问 Hive 表。

使用DBI引用 SQL 语句中的 Hive 表数据:

sc <- spark_connect(master = "local", version = "2.3")
spark_read_csv(sc, "test", "data-csv/", memory = FALSE)

DBI::dbGetQuery(sc, "SELECT * FROM test limit 10")

另一种引用表的方式是使用dplyrtbl()函数,该函数检索表的引用:

dplyr::tbl(sc, "test")

强调重要的是,不会将任何数据导入 R;tbl()函数仅创建引用。然后,您可以在tbl()命令之后使用更多的dplyr动词:

dplyr::tbl(sc, "test") %>%
  dplyr::group_by(y) %>%
  dplyr::summarise(totals = sum(y))

Hive 表引用假定默认的数据库源。通常,所需的表位于元数据存储中的不同数据库中。要通过 SQL 访问它,请在表前缀数据库名称。使用一个句点将它们分开,如本例所示:

DBI::dbSendQuery(sc, "SELECT * FROM databasename.table")

dplyr中,可以使用in_schema()函数。该函数用于tbl()调用内部:

tbl(sc, dbplyr::in_schema("databasename", "table"))

您还可以使用tbl_change_db()函数来设置当前会话的默认数据库。随后通过DBIdplyr调用将使用所选名称作为默认数据库:

tbl_change_db(sc, "databasename")

以下示例需要额外的 Spark 包和数据库,可能难以理解,除非您恰好可以访问 JDBC 驱动程序或 Cassandra 数据库。

接下来,我们探索一个不那么结构化的存储系统,通常称为NoSQL 数据库

Cassandra

Apache Cassandra是一个免费开源的分布式宽列存储 NoSQL 数据库管理系统,设计用于处理大量数据,跨多个廉价服务器。虽然除了 Cassandra 之外还有许多其他数据库系统,但快速了解如何从 Spark 使用 Cassandra 将使您了解如何利用其他数据库和存储系统,如 Solr、Redshift、Delta Lake 等。

以下示例代码展示了如何使用datastax:spark-cassandra-connector包从 Cassandra 读取数据。关键是使用org.apache.spark.sql.cassandra库作为source参数。它提供了 Spark 可以用来理解数据源的映射。除非你有一个 Cassandra 数据库,否则请跳过执行以下语句:

sc <- spark_connect(master = "local", version = "2.3", config = list(
  sparklyr.connect.packages = "datastax:spark-cassandra-connector:2.3.1-s_2.11"))

spark_read_source(
  sc,
  name = "emp",
  source = "org.apache.spark.sql.cassandra",
  options = list(keyspace = "dev", table = "emp"),
  memory = FALSE)

处理外部数据库和数据仓库时,Spark 的一个最有用的功能是它可以将计算推送到数据库,这一功能称为推送谓词。简而言之,推送谓词通过向远程数据库提出智能问题来提高性能。当您对通过spark_read_source()引用的远程表执行包含filter(age > 20)表达式的查询时,而不是将整个表带入 Spark 内存中,它将被传递到远程数据库,并且仅检索远程表的子集。

尽管理想情况下是找到支持远程存储系统的 Spark 包,但有时会没有可用的包,您需要考虑供应商的 JDBC 驱动程序。

JDBC

当没有 Spark 包可用以提供连接时,您可以考虑使用 JDBC 连接。JDBC 是 Java 编程语言的接口,定义了客户端如何访问数据库。

使用spark_read_jdbc()spark_write_jdbc()连接到远程数据库非常简单;只要您有适当的 JDBC 驱动程序访问权限,有时是微不足道的,有时则是一次相当的冒险。为了保持简单,我们可以简要考虑如何连接到远程 MySQL 数据库。

首先,您需要从 MySQL 的开发者门户网站下载适当的 JDBC 驱动程序,并将此附加驱动程序指定为sparklyr.shell.driver-class-path连接选项。由于 JDBC 驱动程序是基于 Java 的,代码包含在一个JAR(Java ARchive)文件中。一旦使用适当的驱动程序连接到 Spark,您就可以使用jdbc://协议访问特定的驱动程序和数据库。除非您愿意自行下载和配置 MySQL,否则请跳过执行以下语句:

sc <- spark_connect(master = "local", version = "2.3", config = list(
  "sparklyr.shell.driver-class-path" =
    "~/Downloads/mysql-connector-java-5.1.41/mysql-connector-java-5.1.41-bin.jar"
))

spark_read_jdbc(sc, "person_jdbc",  options = list(
  url = "jdbc:mysql://localhost:3306/sparklyr",
  user = "root", password = "<password>",
  dbtable = "person"))

如果您是特定数据库供应商的客户,则利用供应商提供的资源通常是开始寻找适当驱动程序的最佳方式。

总结

本章详细介绍了使用 Spark 连接和处理各种数据源的方法及其原因,通过称为数据湖的新数据存储模型——这种存储模式比标准 ETL 过程提供了更大的灵活性,使您能够使用原始数据集,可能具有更多信息来丰富数据分析和建模。

我们还提出了有关在 Spark 中读取、写入和复制数据的最佳实践。然后,我们回顾了数据湖的组成部分:文件格式和文件系统,前者表示数据存储方式,后者表示数据存储位置。然后,您学习了如何处理需要额外 Spark 包的文件格式和存储系统,审查了不同文件格式之间的性能权衡,并学习了在 Spark 中使用存储系统(数据库和数据仓库)所需的概念。

虽然阅读和写入数据集对您来说应该是自然而然的,但在读写大型数据集时,您可能仍会遇到资源限制。为了处理这些情况,第九章向您展示了 Spark 如何跨多台机器管理任务和数据,从而进一步提高分析和建模任务的性能。

¹ Codd EF(1970)。"大型共享数据库的数据关系模型。"

第九章:调优

混乱不是一个坑。混乱是一把梯子。

— 佩蒂尔·贝利希

在之前的章节中,我们假设 Spark 集群内的计算效率高。虽然在某些情况下这是正确的,但通常需要对 Spark 内部运行的操作有所了解,以微调配置设置,使计算能够高效运行。本章解释了 Spark 如何处理大数据集的数据计算,并提供了优化操作的详细信息。

例如,在本章中,您将学习如何请求更多的计算节点和增加内存量,这些内容您可以从 第二章 中了解,默认情况下在本地实例中仅为 2 GB。您将了解到 Spark 如何通过分区、洗牌和缓存统一计算。正如几章前提到的,这是描述 Spark 内部的最后一章;在您完成本章之后,我们相信您将具备使用 Spark 所需的中级技能。

在第 10–12 章中,我们探讨了处理特定建模、扩展和计算问题的激动人心的技术。然而,我们必须首先了解 Spark 如何执行内部计算,我们可以控制哪些部分,以及原因。

概述

Spark 通过配置、分区、执行、洗牌、缓存和序列化数据、任务和资源在多台机器上执行分布式计算:

  • 配置 请求集群管理器获取资源:总机器数、内存等。

  • 分区 将数据分割到各种机器中。分区可以是隐式的也可以是显式的。

  • 执行 意味着对每个分区运行任意转换。

  • 洗牌 重新分配数据到正确的机器上。

  • 缓存 在不同的计算周期中保留内存中的数据。

  • 序列化 将数据转换为可以发送到其他工作节点或返回到驱动节点的网络数据。

为了说明每个概念,让我们创建三个包含无序整数的分区,然后使用 arrange() 进行排序:

data <- copy_to(sc,
  data.frame(id = c(4, 9, 1, 8, 2, 3, 5, 7, 6)),
  repartition = 3)

data %>% arrange(id) %>% collect()

图 9-1 显示了这个排序 作业 在一个机器集群中如何概念上运行。首先,Spark 将 配置 集群以使用三台工作机器。在本例中,数字 19 被分区到三个存储实例中。由于 数据 已经被分区,每个工作节点加载这个隐式 分区;例如,第一个工作节点加载了 491。然后,一个 任务 被分发到每个工作节点,以在每个工作节点中的每个数据分区应用一个转换;这个任务用 f(x) 表示。在这个例子中,f(x) 执行 了一个排序操作在一个分区内。由于 Spark 是通用的,对分区的执行可以根据需要简单或复杂。

理解给定操作(例如排序)的 Spark 计算图的最佳方法是在 Spark 的 Web 界面 的 SQL 选项卡上打开最后 完成的查询。 图 9-2 显示了此排序操作的结果图,其中包含以下操作:

最后,一小部分结果通过连接集群机器的网络 串行化,返回到驱动节点,以预览此排序示例。

注意,虽然 图 9-1 描述了一个排序操作,但类似的方法也适用于过滤或连接数据集,并在规模上进行数据分析和建模。Spark 提供支持来执行自定义分区、自定义洗牌等操作,但这些较低级别的操作大多数不会通过 sparklyr 暴露出来;相反,sparklyr 通过数据 分析 工具如 dplyrDBI,建模以及使用许多 扩展,使这些操作可通过更高级别的命令使用。

要有效地调整 Spark,我们将首先熟悉 Spark 的计算 和 Spark 的事件 时间线。这两者都可以通过 Spark 的 Web 界面 访问。

然后将结果 洗牌 到正确的机器上完成整个数据集的排序操作,这完成了一个阶段。一个 阶段 是 Spark 可以在不同机器之间传输数据的一组操作。在数据在集群中排序之后,可以选择将排序后的结果 缓存 在内存中,以避免多次运行此计算。

图 9-1. 使用 Apache Spark 对分布式数据进行排序

Spark 使用有向无环图(DAG)描述所有计算步骤,这意味着 Spark 中的所有计算都向前推进,不会重复之前的步骤,这有助于 Spark 有效优化计算。

对于可能需要实现低级别操作的少数情况,最好使用 Spark 的 Scala API 通过 sparklyr 扩展或运行自定义 分布式 R 代码。

WholeStageCodegen

该块指定它包含的操作用于生成有效转换为字节码的计算机代码。通常,将操作转换为字节码会有一些小成本,但这是值得付出的代价,因为此后可以从 Spark 更快地执行这些操作。一般情况下,您可以忽略此块,并专注于其中包含的操作。

InMemoryTableScan

这意味着原始数据集 data 被存储在内存中,并且逐行遍历了一次。

Exchange

分区被交换—也就是在集群中的执行器之间进行了洗牌。

Sort

在记录到达适当的执行器后,它们在最终阶段进行了排序。

用于排序查询的 Spark 图

图 9-2. 用于排序查询的 Spark 图

从查询详细信息中,您可以打开最后一个到达作业详情页面的 Spark 作业,使用“DAG 可视化”扩展以创建类似于 图 9-3 的图表。此图显示了一些额外的细节和本作业中的阶段。请注意,没有箭头指向前面的步骤,因为 Spark 使用无环图。

用于排序作业的 Spark 图

图 9-3. 用于排序作业的 Spark 图

接下来,我们深入了解 Spark 阶段并探索其事件时间轴。

时间轴

事件时间轴 是 Spark 如何在每个阶段花费计算周期的一个很好的总结。理想情况下,您希望看到这个时间轴主要由 CPU 使用组成,因为其他任务可以被视为开销。您还希望看到 Spark 在您可用的所有集群节点上使用所有 CPU。

选择当前作业中的第一个阶段,并展开事件时间轴,其结果应该类似于 图 9-4。请注意,我们明确请求了三个分区,这在可视化中用三条通道表示。

Spark 事件时间轴

图 9-4. Spark 事件时间轴

由于我们的机器配备了四个 CPU,我们可以通过显式重新分区数据使用 sdf_repartition() 进一步并行化这个计算,其结果显示在 图 9-5 中:

data %>% sdf_repartition(4) %>% arrange(id) %>% collect()

具有额外分区的 Spark 事件时间轴

图 9-5. 具有额外分区的 Spark 事件时间轴

图 9-5 现在显示了四条执行通道,大部分时间都在执行器计算时间下,这表明该特定操作更好地利用了我们的计算资源。当您在集群上工作时,从集群请求更多计算节点应该能缩短计算时间。相反,对于显示出大量时间用于洗牌的时间轴,请求更多计算节点可能不会缩短时间,实际上可能会使一切变慢。优化阶段没有具体的规则可供遵循;然而,随着您在多个操作中理解此时间轴的经验增加,您将会对如何正确优化 Spark 操作有更深刻的见解。

配置

在调优 Spark 应用程序时,最常见的配置资源是内存和核心数,具体包括:

驱动节点中的内存

驱动节点所需的内存量

每个工作节点的内存

工作节点上所需的内存量

每个工作节点的核心数

工作节点上所需的 CPU 数量

工作节点数

本次会话所需的工作节点数量

注意

建议请求的驱动程序内存显著多于每个工作节点可用的内存。在大多数情况下,您将希望为每个工作节点请求一个核心。

在本地模式下没有工作节点,但我们仍然可以通过以下方式配置内存和核心:

# Initialize configuration with defaults
config <- spark_config()

# Memory
config["sparklyr.shell.driver-memory"] <- "2g"

# Cores
config["sparklyr.connect.cores.local"] <- 2

# Connect to local cluster with custom configuration
sc <- spark_connect(master = "local", config = config)

在使用 Spark Standalone 和 Mesos 集群管理器时,默认情况下会分配所有可用的内存和核心;因此,除非要限制资源以允许多个用户共享此集群,否则不需要进行其他配置更改。在这种情况下,您可以使用total-executor-cores来限制请求的总执行者。Spark StandaloneSpark on Mesos指南提供了有关共享集群的额外信息。

在运行 YARN 客户端时,您可以按以下方式配置内存和核心:

# Memory in driver
config["sparklyr.shell.driver-memory"] <- "2g"

# Memory per worker
config["sparklyr.shell.executor-memory"] <- "2g"

# Cores per worker
config["sparklyr.shell.executor-cores"] <- 1

# Number of workers
config["sparklyr.shell.num-executors"] <- 3

在使用 YARN 集群模式时,您可以使用sparklyr.shell.driver-cores来配置驱动程序节点中请求的总核心数。Spark on YARN指南提供了可以使您受益的额外配置设置。

有几种类型的配置设置:

连接

这些设置是作为参数传递给spark_connect()。它们是在连接时使用的常见设置。

提交

这些设置是在通过spark-submit向 Spark 提交sparklyr时设置的;其中一些取决于正在使用的集群管理器。

运行时

这些设置在创建 Spark 会话时配置 Spark。它们与集群管理器无关,特定于 Spark。

sparklyr

使用这些配置sparklyr行为。这些设置与集群管理器无关,特定于 R 语言。

以下小节详细列出了所有可用设置的广泛列表。在调整 Spark 时,并不需要完全理解它们,但浏览可能在将来解决问题时有用。如果愿意,您可以跳过这些小节,并根据需要将其用作参考材料。

连接设置

您可以使用在表 9-1 中列出的参数与spark_connect()。它们配置了定义连接方法、Spark 安装路径和要使用的 Spark 版本的高级设置。

表 9-1. 连接到 Spark 时使用的参数

名称
master Spark 集群的 URL 以连接到。使用"local"以连接到通过spark_install()安装的本地 Spark 实例。
SPARK_HOME Spark 安装路径。默认为由SPARK_HOME环境变量提供的路径。如果定义了SPARK_HOME,则始终会使用它,除非通过指定版本参数来强制使用本地安装版本。
method 用于连接到 Spark 的方法。默认连接方法为 "shell",使用 spark-submit 进行连接。使用 "livy" 进行使用 HTTP 进行远程连接,或者在使用 Databricks 集群时使用 "databricks",或者在使用 Qubole 集群时使用 "qubole"
app_name 在 Spark 集群中运行时使用的应用程序名称。
version 要使用的 Spark 版本。这仅适用于 "local""livy" 连接。
config 生成的 Spark 连接的自定义配置。详见 spark_config

您可以通过在 config 参数中指定一个列表来配置额外的设置。现在让我们看看这些设置可以是什么。

提交设置

当运行 spark-submit(启动 Spark 的终端应用程序)时,必须指定某些设置。例如,由于 spark-submit 启动作为 Java 实例运行的驱动节点,需要指定分配多少内存作为 spark-submit 的参数。

您可以通过运行以下命令列出所有可用的 spark-submit 参数:

spark_home_dir() %>% file.path("bin", "spark-submit") %>% system2()

为了方便阅读,我们已经在 表 9-2 中提供了此命令的输出,将 spark-submit 参数替换为适当的 spark_config() 设置,并删除不适用或已在本章中介绍的参数。

表 9-2. 可用于配置 spark-submit 的设置

Name Value
sparklyr.shell.jars spark_connect() 中指定为 jars 参数。
sparklyr.shell.packages 包含在驱动程序和执行程序类路径中的 JAR 文件的 Maven 坐标的逗号分隔列表。将搜索本地 Maven 仓库,然后搜索 Maven 中心和 sparklyr.shell.repositories 给出的任何附加远程仓库。坐标的格式应为 groupId:artifactId:version
sparklyr.shell.exclude-packages 逗号分隔的 groupId:artifactId 列表,用于解析 sparklyr.shell.packages 中提供的依赖项时排除依赖冲突。
sparklyr.shell.repositories 逗号分隔的附加远程仓库列表,用于搜索 sparklyr.shell.packages 提供的 Maven 坐标。
sparklyr.shell.files 要放置在每个执行器的工作目录中的文件的逗号分隔列表。通过 SparkFiles.get(fileName) 可以访问这些文件在执行器中的文件路径。
sparklyr.shell.conf 设置为 PROP=VALUE 的任意 Spark 配置属性。
sparklyr.shell.properties-file 要加载额外属性的文件路径。如果未指定,则将查找 conf/spark-defaults.conf
sparklyr.shell.driver-java-options 传递给驱动程序的额外 Java 选项。
sparklyr.shell.driver-library-path 传递给驱动程序的额外库路径条目。
sparklyr.shell.driver-class-path 传递给驱动程序的额外类路径条目。请注意,使用 sparklyr.shell.jars 添加的 JAR 包会自动包含在类路径中。
sparklyr.shell.proxy-user 提交应用程序时要模拟的用户。此参数不适用于 sparklyr.shell.principal/ sparklyr.shell.keytab
sparklyr.shell.verbose 打印额外的调试输出。

剩余的设置如 Table 9-3 所示,专门针对 YARN。

Table 9-3. 在使用 YARN 时配置 spark-submit 的设置

Name Value
sparklyr.shell.queue 提交到的 YARN 队列(默认:“default”)。
--- ---
sparklyr.shell.archives 要提取到每个执行者工作目录的归档文件的逗号分隔列表。
--- ---
sparklyr.shell.principal 在运行安全 HDFS 时用于登录 KDC 的主体。
--- ---
sparklyr.shell.keytab 包含刚才指定的主体的 keytab 文件的完整路径。此 keytab 将通过安全分布式缓存(Secure Distributed Cache)复制到运行应用程序主节点的节点,以定期更新登录票据和委托令牌。
--- ---

通常情况下,任何 spark-submit 设置都是通过 sparklyr.shell.X 配置的,其中 X 是 spark-submit 参数的名称,不包含 -- 前缀。

运行时设置

如前所述,某些 Spark 设置配置会话运行时。运行时设置是给定的 submit settings 的超集,即使不能更改设置,通常也有助于检索当前配置。

要列出当前 Spark 会话中设置的 Spark 设置,可以运行以下命令:

spark_session_config(sc)

Table 9-4 描述了运行时设置。

Table 9-4. 配置 Spark 会话的可用设置

Name Value
spark.master local[4]
spark.sql.shuffle.partitions 4
spark.driver.port 62314
spark.submit.deployMode client
spark.executor.id driver
spark.jars /Library/…/sparklyr/java/sparklyr-2.3-2.11.jar
spark.app.id local-1545518234395
spark.env.SPARK_LOCAL_IP 127.0.0.1
spark.sql.catalogImplementation hive
spark.spark.port.maxRetries 128
spark.app.name sparklyr
spark.home /Users/…/spark/spark-2.3.2-bin-hadoop2.7
spark.driver.host localhost

然而,在 Spark Configuration 指南中描述的 Spark 中还有许多其他配置设置。本书无法详细描述所有设置,因此,如果可能的话,请花些时间找出那些可能与您特定用例相关的设置。

sparklyr 设置

除了 Spark 设置外,还有一些特定于sparklyr的设置。在调整 Spark 时,通常不使用这些设置;相反,在从 R 中解决 Spark 问题时会很有帮助。例如,您可以使用sparklyr.log.console = TRUE将 Spark 日志输出到 R 控制台;这在故障排除时是理想的,但在其他情况下会太吵。以下是如何列出这些设置(结果在表 9-5 中呈现):

spark_config_settings()

表 9-5。可用于配置 sparklyr 包的设置

名称 描述
sparklyr.apply.packages 配置spark_apply()中 packages 参数的默认值。
sparklyr.apply.rlang 实验性特性。打开spark_apply()的改进序列化。
sparklyr.apply.serializer 配置spark_apply()用于序列化闭包的版本。
sparklyr.apply.schema.infer spark_apply()中指定列类型时,用于推断模式的收集行数。
sparklyr.arrow 使用 Apache Arrow 序列化数据?
sparklyr.backend.interval sparklyr检查后端操作的总秒数。
sparklyr.backend.timeout sparklyr放弃等待后端操作完成之前的总秒数。
sparklyr.collect.batch 使用批量收集时要收集的总行数;默认为 100,000。
sparklyr.cancellable R 会话中断时取消 Spark 作业?
sparklyr.connect.aftersubmit spark-submit执行后调用的 R 函数。
sparklyr.connect.app.jar spark_connect()中使用的sparklyr JAR 的路径。
sparklyr.connect.cores.local spark_connect(master = "local")中使用的核心数,默认为parallel::detectCores()
sparklyr.connect.csv.embedded 与需要包扩展支持 CSV 的 Spark 版本匹配的常规表达式。
sparklyr.connect.csv.scala11 在使用嵌入式 CSV JARS 时,使用 Scala 2.11 JARs 在 Spark 1.6.X 中。
sparklyr.connect.jars 提交应用程序到 Spark 时要包含的附加 JARs。
sparklyr.connect.master 作为spark_connect()主参数的集群主机;通常首选spark.master设置。
sparklyr.connect.packages 连接到 Spark 时要包含的 Spark 包。
sparklyr.connect.ondisconnect spark_disconnect()后调用的 R 函数。
sparklyr.connect.sparksubmit 在连接时执行的命令,而不是spark-submit
sparklyr.connect.timeout 在初始化时连接到sparklyr网关之前的总秒数。
sparklyr.dplyr.period.splits dplyr是否应将列名拆分为数据库和表?
sparklyr.extensions.catalog 扩展 JAR 所在的目录路径。默认为TRUEFALSE为禁用。
sparklyr.gateway.address 驱动机器的地址。
sparklyr.gateway.config.retries 从配置中检索端口和地址的重试次数;在 Kubernetes 中使用函数查询端口或地址时很有用。
sparklyr.gateway.interval sparkyr将检查网关连接的总秒数。
sparklyr.gateway.port sparklyr网关在驱动程序机器上使用的端口。
sparklyr.gateway.remote sparklyr网关是否允许远程连接?在 YARN 集群模式下是必需的。
sparklyr.gateway.routing sparklyr网关服务是否应路由到其他会话?在 Kubernetes 中考虑禁用。
sparklyr.gateway.service sparklyr网关是否应作为服务运行,而不在最后一个连接断开时关闭?
sparklyr.gateway.timeout 在初始化后连接到sparklyr网关之前等待的总秒数。
sparklyr.gateway.wait 在重新尝试联系sparklyr网关之前等待的总秒数。
sparklyr.livy.auth Livy 连接的身份验证方法。
sparklyr.livy.headers Livy 连接的额外 HTTP 头部。
sparklyr.livy.sources 连接时是否应加载sparklyr源?如果为 false,则需要手动注册sparklyr JAR 包。
sparklyr.log.invoke 是否应该将每次调用invoke()打印到控制台?可以设置为callstack以记录调用堆栈。
sparklyr.log.console 是否应将驱动程序日志打印到控制台?
sparklyr.progress 是否应向 RStudio 报告作业进度?
sparklyr.progress.interval 在尝试检索 Spark 作业进度之前等待的总秒数。
sparklyr.sanitize.column.names 是否应清理部分不受支持的列名?
sparklyr.stream.collect.timeout sdf_collect_stream()中停止收集流样本之前的总秒数。
sparklyr.stream.validate.timeout 在创建过程中检查流是否有错误之前等待的总秒数。
sparklyr.verbose 是否在所有sparklyr操作中使用详细日志记录?
sparklyr.verbose.na 在处理 NA 时是否使用详细日志记录?
sparklyr.verbose.sanitize 在清理列和其他对象时是否使用详细日志记录?
sparklyr.web.spark Spark 的 Web 界面 URL。
sparklyr.web.yarn YARN 的 Web 界面 URL。
sparklyr.worker.gateway.address 工作机器的地址,很可能是localhost
sparklyr.worker.gateway.port sparklyr网关在驱动程序机器上使用的端口。
sparklyr.yarn.cluster.accepted.timeout 在 YARN 集群模式下等待集群资源被接受之前的总秒数。
sparklyr.yarn.cluster.hostaddress.timeout 在 YARN 集群模式下等待集群分配主机地址之前的总秒数。
sparklyr.yarn.cluster.lookup.byname 在搜索已提交作业时是否应使用当前用户名来过滤 YARN 集群作业?
sparklyr.yarn.cluster.lookup.prefix 用于在搜索已提交的 YARN 集群作业时过滤应用程序名称前缀。
sparklyr.yarn.cluster.lookup.username 在搜索已提交的 YARN 集群作业时用于过滤 YARN 集群作业的用户名。
sparklyr.yarn.cluster.start.timeout 在放弃等待 YARN 集群应用程序注册之前的总秒数。

分区

如 第一章 所述,MapReduce 和 Spark 的设计目的是对存储在多台机器上的数据执行计算。每个计算实例可用于计算的数据子集称为 分区

默认情况下,Spark 在每个现有 隐式 分区上进行计算,因为在数据已经位于的位置运行计算更有效。然而,有些情况下,您需要设置 显式 分区以帮助 Spark 更有效地利用您的集群资源。

隐式分区

如 第八章 所述,Spark 可以读取存储在许多格式和不同存储系统中的数据;然而,由于数据洗牌是一项昂贵的操作,Spark 在执行任务时重用存储系统中的分区。因此,这些分区对于 Spark 来说是隐式的,因为它们已经被定义并且重排是昂贵的。

对于 Spark 的每个计算,总是有一个由分布式存储系统定义的隐式分区,即使对于您不希望创建分区的操作,比如 copy_to()

你可以通过使用 sdf_num_partitions() 来探索计算所需的分区数量:

sdf_len(sc, 10) %>% sdf_num_partitions()
[1] 2

尽管在大多数情况下,默认分区工作正常,但有些情况下,您需要明确选择分区。

显式分区

有时您的计算实例数量比数据分区多得多或少得多。在这两种情况下,通过重新分区数据来匹配您的集群资源可能会有所帮助。

各种 数据 函数,如 spark_read_csv(),已支持 repartition 参数,以请求 Spark 适当地重新分区数据。例如,我们可以按照以下方式创建一个由 10 个数字分区为 10 的序列:

sdf_len(sc, 10, repartition = 10) %>% sdf_num_partitions()
[1] 10

对于已分区的数据集,我们还可以使用 sdf_repartition()

sdf_len(sc, 10, repartition = 10) %>%
  sdf_repartition(4) %>%
  sdf_num_partitions()
[1] 4

分区数量通常显著改变速度和使用的资源;例如,以下示例计算了在不同分区大小下的 1000 万行的均值:

library(microbenchmark)
library(ggplot2)

microbenchmark(
    "1 Partition(s)" = sdf_len(sc, 10⁷, repartition = 1) %>%
      summarise(mean(id)) %>% collect(),
    "2 Partition(s)" = sdf_len(sc, 10⁷, repartition = 2) %>%
      summarise(mean(id)) %>% collect(),
    times = 10
) %>% autoplot() + theme_light()

图 9-6 显示,使用两个分区对数据进行排序几乎快了一倍。这是因为两个 CPU 可以用于执行此操作。然而,并不一定高分区产生更快的计算结果;相反,数据分区是特定于您的计算集群和正在执行的数据分析操作的。

使用额外显式分区的计算速度

图 9-6. 使用额外显式分区的计算速度

缓存

从第一章中回忆起,Spark 的设计目的是通过使用内存而不是磁盘来存储数据来比其前身更快。这在正式上被称为 Spark 弹性分布式数据集(RDD)。RDD 在许多机器上分发相同数据的副本,因此如果一台机器失败,其他机器可以完成任务,因此称为“弹性”。在分布式系统中,弹性是很重要的,因为尽管一台机器通常会正常工作,但在数千台机器上运行时,发生故障的可能性要高得多。发生故障时,最好是具有容错性,以避免丢失所有其他机器的工作。RDD 通过跟踪数据血统信息,在故障时自动重建丢失的数据来实现这一点。

sparklyr中,您可以使用tbl_cache()tbl_uncache()控制 RDD 何时从内存中加载或卸载。

大多数sparklyr操作检索 Spark DataFrame 并将结果缓存在内存中。例如,运行spark_read_parquet()copy_to()将提供一个已经缓存在内存中的 Spark DataFrame。作为 Spark DataFrame,该对象可以在大多数sparklyr函数中使用,包括使用dplyr进行数据分析或机器学习:

library(sparklyr)
sc <- spark_connect(master = "local")
iris_tbl <- copy_to(sc, iris, overwrite = TRUE)

您可以通过导航到 Spark UI 使用spark_web(sc),点击存储选项卡,然后点击特定的 RDD 来检查哪些表被缓存,如图 9-7 所示。

在 Spark Web 界面中缓存的 RDD

图 9-7. 在 Spark Web 界面中缓存的 RDD

当 R 会话终止时,加载到内存中的数据将被释放,无论是显式还是隐式的,通过重启或断开连接;但是,为了释放资源,您可以使用tbl_uncache()

tbl_uncache(sc, "iris")

检查点

检查点是一种稍有不同的缓存类型;虽然它也保存数据,但它还会打破计算图血统。例如,如果缓存的分区丢失,则可以从计算图中计算,这在检查点中是不可能的,因为计算来源已丢失。

在执行创建昂贵计算图的操作时,通过检查点来保存和打破计算血统可能是有意义的,以帮助 Spark 减少图计算资源的使用;否则,Spark 可能会尝试优化一个实际上并不需要优化的计算图。

您可以通过保存为 CSV、Parquet 和其他文件格式显式进行检查点,或者使用sparklyr中的sdf_checkpoint()让 Spark 为您检查点,如下所示:

# set checkpoint path
spark_set_checkpoint_dir(sc, getwd())

# checkpoint the iris dataset
iris_tbl %>% sdf_checkpoint()

注意,检查点会截断计算血统图,如果多次使用相同的中间结果,可以加快性能。

内存

Spark 中的内存分为保留用户执行存储

保留

保留内存是 Spark 运行所需的内存,因此是必需的开销,不应该进行配置。此值默认为 300 MB。

用户

用户内存是用于执行自定义代码的内存。 sparklyr 在执行 dplyr 表达式或对数据集建模时间接使用这些内存。

执行

执行内存主要用于由 Spark 执行代码,大多用于处理来自分区的结果和执行洗牌。

存储

存储内存用于缓存 RDD,例如在使用 sparklyrcompute() 时。

作为执行调优的一部分,您可以考虑通过创建具有与 Spark 提供的默认值不同的值的 Spark 连接来调整为用户、执行和存储分配的内存量:

config <- spark_config()

# define memory available for storage and execution
config$spark.memory.fraction <- 0.75

# define memory available for storage
config$spark.memory.storageFraction <- 0.5

例如,如果希望使用 Spark 在内存中存储大量数据,以便快速过滤和检索子集,您可以预期 Spark 使用的执行或用户内存很少。因此,为了最大化存储内存,可以调整 Spark 如下:

config <- spark_config()

# define memory available for storage and execution
config$spark.memory.fraction <- 0.90

# define memory available for storage
config$spark.memory.storageFraction <- 0.90

然而,请注意,如果需要且可能的话,Spark 将从存储中借用执行内存,反之亦然;因此,实际上应该很少需要调整内存设置。

洗牌

洗牌是将数据重新分布到各台机器的操作;通常昂贵,因此应尽量减少。您可以通过查看 事件时间线 来轻松识别是否花费了大量时间在洗牌上。通过重新构架数据分析问题或适当提示 Spark,可以减少洗牌。

例如,在连接大小显著不同的 DataFrame(即一个数据集比另一个小几个数量级)时会变得相关;您可以考虑使用 sdf_broadcast() 将一个 DataFrame 标记为足够小,以便在广播连接中将一个较小的 DataFrame 推送到每个工作节点,从而减少大 DataFrame 的洗牌。以下是 sdf_broadcast() 的一个示例:

sdf_len(sc, 10000) %>%
    sdf_broadcast() %>%
    left_join(sdf_len(sc, 100))

序列化

序列化是将数据和任务转换为可以在机器之间传输并在接收端重建的格式的过程。

在调优 Spark 时,通常不需要调整序列化;然而,值得一提的是,存在替代序列化模块,如 Kryo Serializer,它可以比默认的 Java Serializer 提供性能改进。

您可以通过以下方法在 sparklyr 中启用 Kryo Serializer:

config <- spark_config()

config$spark.serializer <- "org.apache.spark.serializer.KryoSerializer"
sc <- spark_connect(master = "local", config = config)

配置文件

在连接之前配置 spark_config() 设置是调优 Spark 最常见的方法。然而,在识别连接中的参数后,您应考虑切换到使用配置文件,因为它会消除连接代码中的混乱,并允许您在项目和同事之间共享配置设置。

例如,而不是像这样连接到 Spark:

config <- spark_config()
config["sparklyr.shell.driver-memory"] <- "2G"
sc <- spark_connect(master = "local", config = config)

您可以定义一个名为config.yml的文件,其中包含所需的设置。该文件应位于当前工作目录或父目录中。例如,您可以创建以下config.yml文件以修改默认驱动程序内存:

default:
  sparklyr.shell.driver-memory: 2G

然后,通过使用如下更清晰的相同配置设置进行连接:

sc <- spark_connect(master = "local")

您还可以通过在spark_config()中设置file参数来指定替代配置文件名或位置。使用配置文件的另一个好处是系统管理员可以通过更改R_CONFIG_ACTIVE环境变量的值来更改默认配置。有关更多信息,请参阅 GitHub 的rstudio/config存储库。

总结

本章提供了关于 Spark 内部结构的广泛概述和详细的配置设置,帮助您加快计算速度并支持高计算负载。它为理解瓶颈提供了基础,并提供了常见配置考虑的指导。然而,精细调整 Spark 是一个广泛的主题,需要更多章节来全面覆盖。因此,在调试 Spark 的性能和可伸缩性时,经常需要在网上搜索并咨询在线社区,还需要根据您特定的环境进行精细调整。

第十章介绍了在 R 中可用的 Spark 扩展生态系统。大多数扩展都非常专业化,但在特定情况下和特定需求的读者中,它们将证明极其有用。例如,它们可以处理嵌套数据,执行图分析,并使用来自 H20 的不同建模库,如rsparkling。此外,接下来的几章介绍了许多高级数据分析和建模主题,这些主题对于掌握 R 中的大规模计算是必要的。

第十章:扩展

我尽量认识尽可能多的人。你永远不知道哪一个你会需要。

——泰利昂·兰尼斯特

在 第九章 中,您学习了 Spark 如何通过允许用户配置集群资源、隐式或显式分区数据、在分布式计算节点上执行命令、在需要时在它们之间进行数据洗牌、缓存数据以提高性能以及高效地在网络上序列化数据来处理大规模数据。您还学习了如何配置连接、提交作业和运行应用程序时使用的不同 Spark 设置,以及仅适用于 R 和 R 扩展的特定设置,在本章中我们将介绍。

第 3、4 和 8 章提供了阅读和理解大多数数据集的基础。然而,所呈现的功能范围仅限于 Spark 的内置功能和表格数据集。本章将超越表格数据,探讨如何通过图处理分析和建模网络中相互连接的对象,分析基因组数据集,为深度学习准备数据,分析地理数据集,并使用像 H2O 和 XGBoost 这样的高级建模库处理大规模数据集。

在前几章介绍的所有内容应该可以满足大多数您的大规模计算需求。然而,对于仍然缺乏功能的少数用例,以下章节提供了通过自定义 R 转换、自定义 Scala 代码或 Spark 中的最新执行模式来扩展 Spark 的工具,该模式使得能够分析实时数据集。但在重新发明轮子之前,让我们先看看 Spark 中可用的一些扩展。

概述

在 第一章 中,我们将 R 社区描述为一个充满活力的个体群体,通过多种方式相互合作,例如通过创建您可以从 CRAN 安装的 R 包来推动开放科学。类似地,尽管规模小得多,但 R 社区也贡献了增强最初在 Spark 和 R 中支持的功能的扩展。Spark 本身还支持创建 Spark 扩展,实际上,许多 R 扩展使用了 Spark 扩展。

扩展不断被创建,所以这部分内容在某个时刻可能会过时。此外,我们可能甚至不知道许多 Spark 和 R 的扩展。然而,至少我们可以通过查看在 CRAN 中 sparklyr 的“反向导入” 来追踪 CRAN 中可用的扩展。在 CRAN 中发布的扩展和 R 包通常是最稳定的,因为当一个包在 CRAN 中发布时,它经过了审查流程,提高了贡献的整体质量。

尽管我们希望能够呈现所有扩展,但我们将此章节范围限定在应该对您最有趣的扩展上。您可以在 github.com/r-spark 组织或通过使用 sparklyr 标签在 GitHub 上搜索仓库中找到其他扩展。

rsparkling

rsparkling 扩展允许您从 R 中使用 H2O 和 Spark。这个扩展是我们认为在 Spark 中进行高级建模的一部分。虽然 Spark 的内置建模库 Spark MLlib 在许多情况下非常有用,但 H2O 的建模能力可以计算额外的统计指标,并且在性能和可扩展性上比 Spark MLlib 有所提升。我们没有进行 MLlib 和 H2O 之间的详细比较或基准测试;这是您需要自行研究的内容,以形成何时使用 H2O 能力的完整图景。

graphframes

graphframes 扩展增加了在 Spark 中处理图形的支持。图是一种描述一组对象的结构,其中某些对象对在某种意义上相关。正如您在第一章中所学到的,排名网页是开发以 MapReduce 为基础的 Spark 前身的早期动机;如果将页面之间的链接视为页面对之间的关系,网页恰好形成图形。在网页搜索和社交网络中,计算诸如 PageRank 等操作在图形上可以非常有用。

sparktf

sparktf 扩展提供了在 Spark 中编写 TensorFlow 记录的支持。TensorFlow 是领先的深度学习框架之一,通常与大量数值数据一起使用,这些数据表示为 TensorFlow 记录,这是一种针对 TensorFlow 优化的文件格式。Spark 经常用于将非结构化和大规模数据集处理为可以轻松适应 GPU 的较小数值数据集。您可以使用此扩展来保存 TensorFlow 记录文件格式的数据集。

xgboost

xgboost 扩展将著名的 XGBoost 建模库引入到大规模计算的世界中。XGBoost 是一个可扩展、便携和分布式的梯度提升库。在希格斯玻色子机器学习挑战赛中使用后,它在机器学习竞赛圈中广为人知,并且自那时以来在其他 Kaggle 竞赛中仍然很受欢迎。

variantspark

variantspark 扩展提供了使用 Variant Spark 的接口,这是一个用于全基因组关联研究(GWAS)的可扩展工具包。它目前提供了构建随机森林模型、估计变量重要性和读取变异调用格式(VCF)文件的功能。虽然 Spark 中有其他随机森林实现,但大多数并未经过优化,不能处理通常带有数千个样本和数百万个变量的 GWAS 数据集。

geospark

geospark扩展使我们能够加载和查询大规模地理数据集。通常,包含纬度和经度点或复杂区域的数据集以 Well-Known Text(WKT)格式定义,这是一种在地图上表示矢量几何对象的文本标记语言。

在您学习如何以及何时使用每个扩展之前,我们应该首先简要解释如何在 R 和 Spark 中使用扩展。

首先,Spark 扩展只是一个意识到 Spark 的 R 包。与任何其他 R 包一样,您首先需要安装该扩展。安装完成后,重要的是要知道,在可以使用扩展之前,您需要重新连接到 Spark。因此,一般而言,您应该遵循以下模式:

library(sparkextension)
library(sparklyr)

sc <- spark_connect(master = "<master>")

注意,在扩展注册之前加载sparklyr,以确保扩展能够正确注册。如果您需要安装和加载新的扩展,首先需要使用spark_disconnect(sc)断开连接,重新启动 R 会话,然后使用新的扩展重复前述步骤。

从 R 中安装和使用 Spark 扩展并不困难;然而,每个扩展都可能是一个独立的世界,因此大多数情况下,您需要花时间了解扩展是什么、何时使用以及如何正确使用。您将首先了解的第一个扩展是rsparkling扩展,它使您能够在 Spark 中使用 H2O 与 R。

H2O

H2O由 H2O.ai 创建,是用于大规模建模的开源软件,允许您在发现数据模式的过程中拟合成千上万个潜在模型。您可以考虑使用 H2O 来补充或替换 Spark 的默认建模算法。通常在 Spark 的算法不足或需要高级功能(如额外的建模指标或自动模型选择)时,会使用 H2O。

我们无法在一段话中充分展示 H2O 强大的建模能力;要适当地解释 H2O,需要一本专著。我们建议阅读 Darren Cook 的Practical Machine Learning with H2O(O'Reilly),深入探索 H2O 的建模算法和功能。同时,您可以使用本节作为开始在 Spark 中使用 H2O 与 R 的简要指南。

要在 Spark 中使用 H2O,了解涉及的四个组件非常重要:H2O,Sparkling Water,rsparkling,和 Spark。Sparkling Water 允许用户将 H2O 的快速可扩展的机器学习算法与 Spark 的功能结合起来。您可以将 Sparkling Water 视为将 Spark 与 H2O 桥接的组件,rsparkling是 Sparkling Water 的 R 前端,如图 10-1 所示。

H2O 组件与 Spark 和 R

图 10-1. H2O 组件与 Spark 和 R

首先,按照rsparkling文档网站上的说明安装rsparklingh2o

install.packages("h2o", type = "source",
  repos = "http://h2o-release.s3.amazonaws.com/h2o/rel-yates/5/R")
install.packages("rsparkling", type = "source",
  repos = "http://h2o-release.s3.amazonaws.com/sparkling-water/rel-2.3/31/R")

需要注意的是,你需要按照它们的文档使用兼容版本的 Spark、Sparkling Water 和 H2O;我们提供了适用于 Spark 2.3 的说明,但使用不同版本的 Spark 将需要安装不同版本。因此,让我们从运行以下检查 H2O 版本开始:

packageVersion("h2o")
## [1] '3.24.0.5'
packageVersion("rsparkling")
## [1] '2.3.31'

然后,我们可以按照支持的 Spark 版本进行连接(你需要根据你的特定集群调整master参数):

library(rsparkling)
library(sparklyr)
library(h2o)

sc <- spark_connect(master = "local", version = "2.3",
                    config = list(sparklyr.connect.timeout = 3 * 60))

cars <- copy_to(sc, mtcars)

H2O 提供了一个 Web 界面,可以帮助你监控训练并访问 H2O 的许多功能。你可以通过h2o_flow(sc)访问 Web 界面(称为 H2O Flow),如图 10-2 所示。

当使用 H2O 时,你需要通过as_h2o_frame将你的 Spark DataFrame 转换为 H2O DataFrame:

cars_h2o <- as_h2o_frame(sc, cars)
cars_h2o
   mpg cyl disp  hp drat    wt  qsec vs am gear carb
1 21.0   6  160 110 3.90 2.620 16.46  0  1    4    4
2 21.0   6  160 110 3.90 2.875 17.02  0  1    4    4
3 22.8   4  108  93 3.85 2.320 18.61  1  1    4    1
4 21.4   6  258 110 3.08 3.215 19.44  1  0    3    1
5 18.7   8  360 175 3.15 3.440 17.02  0  0    3    2
6 18.1   6  225 105 2.76 3.460 20.22  1  0    3    1

[32 rows x 11 columns]

使用 Spark 和 R 的 H2O Flow 界面

图 10-2. 使用 Spark 和 R 的 H2O Flow 界面

然后,你可以轻松使用h2o包中提供的许多建模功能。例如,我们可以轻松拟合广义线性模型:

model <- h2o.glm(x = c("wt", "cyl"),
                 y = "mpg",
                 training_frame = cars_h2o,
                 lambda_search = TRUE)

H2O 提供了额外的度量标准,这些度量标准在 Spark 的建模算法中可能不一定可用。我们刚刚拟合的模型,残差偏差,在模型中提供了,而在使用 Spark MLlib 时,这不是标准度量标准。

model
...
MSE:  6.017684
RMSE:  2.453097
MAE:  1.940985
RMSLE:  0.1114801
Mean Residual Deviance :  6.017684
R² :  0.8289895
Null Deviance :1126.047
Null D.o.F. :31
Residual Deviance :192.5659
Residual D.o.F. :29
AIC :156.2425

然后,你可以运行广义线性模型(GLM)的预测。对于 H2O 中可用的许多其他模型,类似的方法也适用:

predictions <- as_h2o_frame(sc, copy_to(sc, data.frame(wt = 2, cyl = 6)))
h2o.predict(model, predictions)
   predict
1 24.05984

[1 row x 1 column]

你也可以使用 H2O 执行许多模型的自动训练和调优,这意味着 H2O 可以选择为你使用哪个模型,使用AutoML

automl <- h2o.automl(x = c("wt", "cyl"), y = "mpg",
                     training_frame = cars_h2o,
                     max_models = 20,
                     seed = 1)

对于这个特定的数据集,H2O 确定深度学习模型比 GLM 更适合。¹ 具体来说,H2O 的 AutoML 探索了使用 XGBoost、深度学习、GLM 和堆叠集成模型:

automl@leaderboard
model_id              mean_residual_dev…     rmse      mse      mae     rmsle
1 DeepLearning_…                6.541322 2.557601 6.541322 2.192295 0.1242028
2 XGBoost_grid_1…               6.958945 2.637981 6.958945 2.129421 0.1347795
3 XGBoost_grid_1_…              6.969577 2.639996 6.969577 2.178845 0.1336290
4 XGBoost_grid_1_…              7.266691 2.695680 7.266691 2.167930 0.1331849
5 StackedEnsemble…              7.304556 2.702694 7.304556 1.938982 0.1304792
6 XGBoost_3_…                   7.313948 2.704431 7.313948 2.088791 0.1348819

与其使用排行榜,你可以通过automl@leader专注于最佳模型;例如,你可以查看以下深度学习模型的特定参数:

tibble::tibble(parameter = names(automl@leader@parameters),
               value = as.character(automl@leader@parameters))
# A tibble: 20 x 2
   parameter                         values
   <chr>                             <chr>
 1 model_id                          DeepLearning_grid_1_AutoML…
 2 training_frame                    automl_training_frame_rdd…
 3 nfolds                            5
 4 keep_cross_validation_models      FALSE
 5 keep_cross_validation_predictions TRUE
 6 fold_assignment                   Modulo
 7 overwrite_with_best_model         FALSE
 8 activation                        RectifierWithDropout
 9 hidden                            200
10 epochs                            10003.6618461538
11 seed                              1
12 rho                               0.95
13 epsilon                           1e-06
14 input_dropout_ratio               0.2
15 hidden_dropout_ratios             0.4
16 stopping_rounds                   0
17 stopping_metric                   deviance
18 stopping_tolerance                0.05
19 x                                 c("cyl", "wt")
20 y                                 mpg

然后,你可以像下面这样使用领导者进行预测:

h2o.predict(automl@leader, predictions)
   predict
1 30.74639

[1 row x 1 column]

还有许多其他示例可以查看,你也可以从官方GitHub 代码库获取rsparkling包的帮助。

接下来的扩展,graphframes,允许你处理大规模关系数据集。在开始使用之前,请确保使用spark_disconnect(sc)断开连接并重新启动你的 R 会话,因为使用不同的扩展需要重新连接到 Spark 并重新加载sparklyr

图表

图论史上的第一篇论文是由莱昂哈德·欧拉于 1736 年在康斯堡七桥上写的。问题是设计一条穿过城市的路线,每座桥只过一次且仅一次。图 10-3 展示了原始图表。

来自欧拉档案的康斯贝格七桥

图 10-3. 来自欧拉档案的康斯贝格七桥

如今,图被定义为一个有序对 G = ( V , E ),其中 V 是顶点(节点或点)的集合,E { { x , y } | ( x , y ) V 2 x y } 是边(链接或线)的集合,它们可以是无序对(用于 无向图)或有序对(用于 有向图)。前者描述的是方向无关的链接,而后者描述的是方向相关的链接。

作为一个简单的例子,我们可以使用 ggraph 包中的 highschool 数据集,该数据集跟踪高中男孩之间的友谊关系。在这个数据集中,顶点是学生,边描述的是在特定年份中成为朋友的学生对:

install.packages("ggraph")
install.packages("igraph")
ggraph::highschool
# A tibble: 506 x 3
    from    to  year
   <dbl> <dbl> <dbl>
 1     1    14  1957
 2     1    15  1957
 3     1    21  1957
 4     1    54  1957
 5     1    55  1957
 6     2    21  1957
 7     2    22  1957
 8     3     9  1957
 9     3    15  1957
10     4     5  1957
# … with 496 more rows

虽然高中数据集可以在 R 中轻松处理,但即使是中等规模的图数据集,如果没有将工作分布到机器群集中进行处理,也可能难以处理,而 Spark 正是为此而设计的。Spark 支持通过 graphframes 扩展来处理图,该扩展进而使用 GraphX Spark 组件。GraphX 是 Apache Spark 的图形和图并行计算 API,其性能可与最快的专业图处理系统相媲美,并提供一个日益丰富的图算法库。

Spark 中的图也被表示为边和顶点的 DataFrame;然而,我们的格式稍有不同,因为我们需要构建一个顶点的 DataFrame。让我们首先安装 graphframes 扩展:

install.packages("graphframes")

接下来,我们需要连接,复制 highschool 数据集并转换为此扩展所期望的格式。在这里,我们将数据集范围限定为 1957 年的友谊关系:

library(graphframes)
library(sparklyr)
library(dplyr)

sc <- spark_connect(master = "local", version = "2.3")
highschool_tbl <- copy_to(sc, ggraph::highschool, "highschool") %>%
  filter(year == 1957) %>%
  transmute(from = as.character(as.integer(from)),
            to = as.character(as.integer(to)))

from_tbl <- highschool_tbl %>% distinct(from) %>% transmute(id = from)
to_tbl <- highschool_tbl %>% distinct(to) %>% transmute(id = to)

vertices_tbl <- distinct(sdf_bind_rows(from_tbl, to_tbl))
edges_tbl <- highschool_tbl %>% transmute(src = from, dst = to)

vertices_tbl 表预期只包含一个 id 列:

vertices_tbl
# Source: spark<?> [?? x 1]
   id
   <chr>
 1 1
 2 34
 3 37
 4 43
 5 44
 6 45
 7 56
 8 57
 9 65
10 71
# … with more rows

edges_tbl 应包含 srcdst 列:

edges_tbl
# Source: spark<?> [?? x 2]
   src   dst
   <chr> <chr>
 1 1     14
 2 1     15
 3 1     21
 4 1     54
 5 1     55
 6 2     21
 7 2     22
 8 3     9
 9 3     15
10 4     5
# … with more rows

现在可以创建一个 GraphFrame:

graph <- gf_graphframe(vertices_tbl, edges_tbl)

现在我们可以使用这个图来开始分析这个数据集。例如,我们将找出每个男孩平均有多少朋友,这被称为 顶点价值

gf_degrees(graph) %>% summarise(friends = mean(degree))
# Source: spark<?> [?? x 1]
  friends
    <dbl>
1    6.94

然后我们可以找到到某个特定顶点(对于此数据集为一个男孩)的最短路径。由于数据是匿名化的,我们只能选择被标识为 33 的男孩,并查找他们之间存在多少个分离度:

gf_shortest_paths(graph, 33) %>%
  filter(size(distances) > 0) %>%
  mutate(distance = explode(map_values(distances))) %>%
  select(id, distance)
# Source: spark<?> [?? x 2]
   id    distance
   <chr>    <int>
 1 19           5
 2 5            4
 3 27           6
 4 4            4
 5 11           6
 6 23           4
 7 36           1
 8 26           2
 9 33           0
10 18           5
# … with more rows

最后,我们还可以计算这个图上的 PageRank,这在 第一章 中讨论了 Google 页面排名算法的部分:

gf_graphframe(vertices_tbl, edges_tbl) %>%
  gf_pagerank(reset_prob = 0.15, max_iter = 10L)
GraphFrame
Vertices:
  Database: spark_connection
  $ id       <dbl> 12, 12, 14, 14, 27, 27, 55, 55, 64, 64, 41, 41, 47, 47, 6…
  $ pagerank <dbl> 0.3573460, 0.3573460, 0.3893665, 0.3893665, 0.2362396, 0.…
Edges:
  Database: spark_connection
  $ src    <dbl> 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 12, 12, 12,…
  $ dst    <dbl> 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,…
  $ weight <dbl> 0.25000000, 0.25000000, 0.25000000, 0.25000000, 0.25000000,…

为了让您对这个数据集有些见解,图 10-4 使用ggraph绘制了此图表,并突出显示了以下数据集的最高 PageRank 分数:

highschool_tbl %>%
  igraph::graph_from_data_frame(directed = FALSE) %>%
  ggraph(layout = 'kk') +
    geom_edge_link(alpha = 0.2,
                   arrow = arrow(length = unit(2, 'mm')),
                   end_cap = circle(2, 'mm'),
                   start_cap = circle(2, 'mm')) +
    geom_node_point(size = 2, alpha = 0.4)

高中 ggraph 数据集中最高 PageRank 被突出显示

图 10-4. 高中 ggraph 数据集中最高 PageRank 被突出显示

graphframes中提供了许多图算法,例如广度优先搜索、连通组件、用于检测社区的标签传播、强连通分量和三角形计数。有关此扩展的问题,请参阅官方GitHub 仓库。现在我们介绍一个流行的梯度增强框架——确保在尝试下一个扩展之前断开并重新启动。

XGBoost

决策树是一种类似流程图的结构,其中每个内部节点表示对属性的测试,每个分支表示测试的结果,每个叶节点表示一个类标签。例如,图 10-5 显示了一个决策树,该决策树可以帮助分类员工是否有可能离开,给定诸如工作满意度和加班情况等因素。当决策树用于预测连续变量而不是离散结果时——比如,某人离开公司的可能性——它被称为回归树

基于已知因素预测工作流失的决策树

图 10-5. 基于已知因素预测工作流失的决策树

决策树表示法虽然易于理解和解释,但要找出树中的决策则需要像梯度下降这样的数学技术来寻找局部最小值。梯度下降是指按当前点的负梯度方向前进的步骤。梯度由表示,学习率由γ表示。你从给定状态a n开始,并通过以下梯度方向计算下一个迭代a n+1

a n+1 = a n - γ F ( a n )

XGBoost 是一个开源软件库,提供梯度增强框架。其目标是为训练梯度增强决策树(GBDT)和梯度增强回归树(GBRT)提供可伸缩、可移植和分布式支持。梯度增强意味着 XGBoost 使用梯度下降和增强技术,这是一种依次选择每个预测器的技术。

sparkxgb是一个扩展,您可以使用它在 Spark 中训练 XGBoost 模型;然而,请注意,目前不支持 Windows。要使用此扩展,请首先从 CRAN 安装它:

install.packages("sparkxgb")

然后,您需要导入sparkxgb扩展,然后是您通常的 Spark 连接代码,根据需要调整master

library(sparkxgb)
library(sparklyr)
library(dplyr)

sc <- spark_connect(master = "local", version = "2.3")

对于此示例,我们使用rsample包中的attrition数据集,您需要通过install.packages("rsample")安装该包。这是 IBM 数据科学家创建的一个虚构数据集,旨在揭示导致员工离职的因素:

attrition <- copy_to(sc, rsample::attrition)
attrition
# Source: spark<?> [?? x 31]
     Age Attrition BusinessTravel DailyRate Department DistanceFromHome
   <int> <chr>     <chr>              <int> <chr>                 <int>
 1    41 Yes       Travel_Rarely       1102 Sales                     1
 2    49 No        Travel_Freque…       279 Research_…                8
 3    37 Yes       Travel_Rarely       1373 Research_…                2
 4    33 No        Travel_Freque…      1392 Research_…                3
 5    27 No        Travel_Rarely        591 Research_…                2
 6    32 No        Travel_Freque…      1005 Research_…                2
 7    59 No        Travel_Rarely       1324 Research_…                3
 8    30 No        Travel_Rarely       1358 Research_…               24
 9    38 No        Travel_Freque…       216 Research_…               23
10    36 No        Travel_Rarely       1299 Research_…               27
# … with more rows, and 25 more variables: Education <chr>,
#   EducationField <chr>, EnvironmentSatisfaction <chr>, Gender <chr>,
#   HourlyRate <int>, JobInvolvement <chr>, JobLevel <int>, JobRole <chr>,
#   JobSatisfaction <chr>, MaritalStatus <chr>, MonthlyIncome <int>,
#   MonthlyRate <int>, NumCompaniesWorked <int>, OverTime <chr>,
#   PercentSalaryHike <int>, PerformanceRating <chr>,
#   RelationshipSatisfaction <chr>, StockOptionLevel <int>,
#   TotalWorkingYears <int>, TrainingTimesLastYear <int>,
#   WorkLifeBalance <chr>, YearsAtCompany <int>, YearsInCurrentRole <int>,
#   YearsSinceLastPromotion <int>, YearsWithCurrManager <int>

要在 Spark 中构建 XGBoost 模型,请使用xgboost_classifier()。我们将使用Attrition ~ .公式计算所有其他特征对离职的影响,并指定2作为类别数量,因为离职属性仅跟踪员工是否离职。然后,您可以使用ml_predict()来预测大规模数据集:

xgb_model <- xgboost_classifier(attrition,
                                Attrition ~ .,
                                num_class = 2,
                                num_round = 50,
                                max_depth = 4)

xgb_model %>%
  ml_predict(attrition) %>%
  select(Attrition, predicted_label, starts_with("probability_")) %>%
  glimpse()
Observations: ??
Variables: 4
Database: spark_connection
$ Attrition       <chr> "Yes", "No", "Yes", "No", "No", "No", "No", "No", "No", …
$ predicted_label <chr> "No", "Yes", "No", "Yes", "Yes", "Yes", "Yes", "Yes", "Y…
$ probability_No  <dbl> 0.753938094, 0.024780750, 0.915146366, 0.143568754, 0.07…
$ probability_Yes <dbl> 0.24606191, 0.97521925, 0.08485363, 0.85643125, 0.927375…

在 Higgs 机器学习挑战的获胜解决方案中使用后,XGBoost 在竞赛界变得广为人知,该挑战使用 ATLAS 实验来识别希格斯玻色子。从那时起,它已成为一种流行的模型,并用于大量的 Kaggle 竞赛。然而,决策树可能会在非表格数据(如图像、音频和文本)的数据集中受限,您可以通过深度学习模型更好地处理这些数据—我们需要提醒您断开并重新启动吗?

深度学习

一个感知器是由 Frank Rosenblatt 引入的数学模型,²他将其发展为一个假设的神经系统理论。感知器将刺激映射到数字输入,这些输入被加权到一个阈值函数中,只有在足够的刺激存在时才会激活,数学上表达为:

f ( x ) = 1 i=1 m w i x i + b > 0 0 否则

Minsky 和 Papert 发现单个感知器只能分类线性可分的数据集;然而,在他们的书《感知器》中,他们还揭示了层叠感知器会带来额外的分类能力。³ 图 10-6 展示了原始图表,展示了多层感知器。

分层感知器,如《感知器》书中所示

图 10-6. 分层感知器,如《感知器》书中所示

在我们开始之前,让我们先安装所有即将使用的包:

install.packages("sparktf")
install.packages("tfdatasets")

使用 Spark,我们可以创建一个多层感知器分类器,使用ml_multilayer_perceptron_classifier()和梯度下降来对大型数据集进行分类和预测。梯度下降是由 Geoff Hinton 在分层感知器中引入的。⁴

library(sparktf)
library(sparklyr)

sc <- spark_connect(master = "local", version = "2.3")

attrition <- copy_to(sc, rsample::attrition)

nn_model <- ml_multilayer_perceptron_classifier(
  attrition,
  Attrition ~ Age + DailyRate + DistanceFromHome + MonthlyIncome,
  layers = c(4, 3, 2),
  solver = "gd")

nn_model %>%
  ml_predict(attrition) %>%
  select(Attrition, predicted_label, starts_with("probability_")) %>%
  glimpse()
Observations: ??
Variables: 4
Database: spark_connection
$ Attrition       <chr> "Yes", "No", "Yes", "No", "No", "No", "No", "No", "No"…
$ predicted_label <chr> "No", "No", "No", "No", "No", "No", "No", "No", "No", …
$ probability_No  <dbl> 0.8439275, 0.8439275, 0.8439275, 0.8439275, 0.8439275,…
$ probability_Yes <dbl> 0.1560725, 0.1560725, 0.1560725, 0.1560725, 0.1560725,…

注意,列必须是数值的,因此你需要使用第四章中介绍的特征转换技术手动转换它们。尝试添加更多层来分类更复杂的数据集是很自然的;然而,添加太多层会导致梯度消失,需要使用这些深层网络的其他技术,也称为深度学习模型

深度学习模型通过使用特殊的激活函数、dropout、数据增强和 GPU 来解决梯度消失问题。你可以使用 Spark 从大型数据集中检索和预处理成只含数值的数据集,这些数据集可以适应 GPU 进行深度学习训练。TensorFlow 是最流行的深度学习框架之一。正如之前提到的,它支持一种称为 TensorFlow 记录的二进制格式。

你可以使用sparktf在 Spark 中编写 TensorFlow 记录,然后可以准备在 GPU 实例中处理,使用像 Keras 或 TensorFlow 这样的库。

你可以在 Spark 中预处理大型数据集,然后使用spark_write_tf()将它们写为 TensorFlow 记录:

copy_to(sc, iris) %>%
  ft_string_indexer_model(
    "Species", "label",
    labels = c("setosa", "versicolor", "virginica")
  ) %>%
  spark_write_tfrecord(path = "tfrecord")

在使用 Keras 或 TensorFlow 训练数据集后,你可以使用tfdatasets包来加载它。你还需要使用install_tensorflow()安装 TensorFlow 运行时,并自行安装 Python。要了解更多关于使用 Keras 训练深度学习模型的信息,我们建议阅读《深度学习与 R》。⁵

tensorflow::install_tensorflow()
tfdatasets::tfrecord_dataset("tfrecord/part-r-00000")
<DatasetV1Adapter shapes: (), types: tf.string>

对于大多数应用程序来说,在单个本地节点上使用一个或多个 GPU 训练深度学习模型通常已经足够了;然而,最新的深度学习模型通常使用像 Apache Spark 这样的分布式计算框架进行训练。分布式计算框架用于实现每天训练这些模型所需的更高的 petaflops。OpenAI分析了人工智能(AI)和集群计算领域的趋势(见图 10-7)。从图中可以明显看出,近年来使用分布式计算框架的趋势日益明显。

使用 OpenAI 分析基于分布式系统进行训练

图 10-7。使用 OpenAI 分析基于分布式系统进行训练

使用 Horovod 等框架可以在 Spark 和 TensorFlow 中训练大规模深度学习模型。今天,通过reticulate包从 R 使用 Horovod 与 Spark 结合使用是可能的,因为 Horovod 需要 Python 和 Open MPI,这超出了本书的范围。接下来,我们将介绍基因组学领域中不同的 Spark 扩展。

基因组学

人类基因组 由 23 对染色体内大约 30 亿个碱基对的两份拷贝组成。图 10-8(#extensions-genomics-diagram)显示了染色体的基因组组织。DNA 链由核苷酸组成,每个核苷酸包含四种含氮碱基之一:胞嘧啶(C)、鸟嘌呤(G)、腺嘌呤(A)或胸腺嘧啶(T)。由于所有人类的 DNA 几乎相同,我们只需要以变异调用格式(VCF)文件的形式存储与参考基因组的差异。

理想化的人类二倍体核型显示染色体的基因组组织

图 10-8. 理想化的人类二倍体核型显示染色体的基因组组织

variantspark 是基于 Scala 和 Spark 的框架,用于分析基因组数据集。它由澳大利亚 CSIRO 生物信息团队开发。variantspark 在包含 80 百万特征的 3,000 个样本数据集上进行了测试,可以进行无监督聚类或监督分类和回归等应用。variantspark 可以读取 VCF 文件并在使用熟悉的 Spark DataFrames 时运行分析。

要开始,请从 CRAN 安装variantspark,连接到 Spark,并获取到variantsparkvsc连接:

library(variantspark)
library(sparklyr)

sc <- spark_connect(master = "local", version = "2.3",
                    config = list(sparklyr.connect.timeout = 3 * 60))

vsc <- vs_connect(sc)

我们可以通过加载 VCF 文件开始:

vsc_data <- system.file("extdata/", package = "variantspark")

hipster_vcf <- vs_read_vcf(vsc, file.path(vsc_data, "hipster.vcf.bz2"))
hipster_labels <- vs_read_csv(vsc, file.path(vsc_data, "hipster_labels.txt"))
labels <- vs_read_labels(vsc, file.path(vsc_data, "hipster_labels.txt"))

variantspark 使用随机森林为每个测试变体分配重要性评分,反映其与感兴趣表型的关联。具有较高重要性评分的变体意味着其与感兴趣表型的关联更强。您可以计算重要性并将其转换为 Spark 表,如下所示:

importance_tbl <- vs_importance_analysis(vsc, hipster_vcf,
                                         labels, n_trees = 100) %>%
  importance_tbl()

importance_tbl
# Source: spark<?> [?? x 2]
   variable    importance
   <chr>            <dbl>
 1 2_109511398 0
 2 2_109511454 0
 3 2_109511463 0.00000164
 4 2_109511467 0.00000309
 5 2_109511478 0
 6 2_109511497 0
 7 2_109511525 0
 8 2_109511527 0
 9 2_109511532 0
10 2_109511579 0
# … with more rows

然后,您可以使用dplyrggplot2转换输出并进行可视化(参见 Figure 10-9):

library(dplyr)
library(ggplot2)

importance_df <- importance_tbl %>%
  arrange(-importance) %>%
  head(20) %>%
  collect()

ggplot(importance_df) +
  aes(x = variable, y = importance) +
  geom_bar(stat = 'identity') +
  scale_x_discrete(limits =
    importance_df[order(importance_df$importance), 1]$variable) +
  coord_flip()

使用 variantspark 进行基因组重要性分析

图 10-9. 使用 variantspark 进行基因组重要性分析

这就结束了对使用variantspark扩展进行基因组分析的简要介绍。接下来,我们将从微观基因转向包含世界各地地理位置的宏观数据集。

空间

geospark 支持使用与dplyrsf 兼容的语法进行分布式地理空间计算,提供了一套用于处理地理空间向量的工具。

您可以按照以下步骤从 CRAN 安装geospark

install.packages("geospark")

然后,初始化geospark扩展并连接到 Spark:

library(geospark)
library(sparklyr)

sc <- spark_connect(master = "local", version = "2.3")

接下来,我们加载包含多边形和点的空间数据集:

polygons <- system.file("examples/polygons.txt", package="geospark") %>%
  read.table(sep="|", col.names = c("area", "geom"))

points <- system.file("examples/points.txt", package="geospark") %>%
  read.table(sep = "|", col.names = c("city", "state", "geom"))

polygons_wkt <- copy_to(sc, polygons)
points_wkt <- copy_to(sc, points)

geospark中定义了各种空间操作,如图 10-10(#extensions-geospark-operations)所示。这些操作允许您根据重叠、交集、不相交集等来查询地理空间数据应如何处理。

geospark 中的空间操作

图 10-10. Geospark 中可用的空间操作

例如,我们可以使用这些操作来查找包含给定点集的多边形,使用 st_contains()

library(dplyr)
polygons_wkt <- mutate(polygons_wkt, y = st_geomfromwkt(geom))
points_wkt <- mutate(points_wkt, x = st_geomfromwkt(geom))

inner_join(polygons_wkt,
           points_wkt,
           sql_on = sql("st_contains(y,x)")) %>%
  group_by(area, state) %>%
  summarise(cnt = n())
# Source: spark<?> [?? x 3]
# Groups: area
  area            state   cnt
  <chr>           <chr> <dbl>
1 california area CA       10
2 new york area   NY        9
3 dakota area     ND       10
4 texas area      TX       10
5 dakota area     SD        1

您还可以通过在收集它们之前在 Spark 中收集整个数据集的子集或聚合几何图形来绘制这些数据集。您应该查看的一个包是 sf

我们通过介绍适用于所有扩展的几种故障排除技术来结束本章。

故障排除

当您第一次使用新扩展时,我们建议增加连接超时时间(因为 Spark 通常需要下载扩展依赖项)并将日志级别更改为详细,以帮助您在下载过程未完成时进行故障排除:

config <- spark_config()
config$sparklyr.connect.timeout <- 3 * 60
config$sparklyr.log.console = TRUE

sc <- spark_connect(master = "local", config = config)

此外,您应该知道 Apache IVY 是一个专注于灵活性和简易性的流行依赖管理器,被 Apache Spark 用于安装扩展。当您使用扩展时连接失败时,考虑通过运行以下命令清除您的 IVY cache

unlink("~/.ivy2", recursive = TRUE)

此外,您还可以考虑从以下扩展存储库中开启 GitHub 问题以获得扩展作者的帮助:

总结

本章简要介绍了在 R 中使用一些 Spark 扩展的概述,这与安装软件包一样简单。然后您学习了如何使用 rsparkling 扩展,该扩展提供了在 Spark 中访问 H2O 的能力,进而提供了增强的模型功能,如增强指标和自动选择模型。接着我们转向 graphframes,这是一个帮助您处理形式上称为图的关系数据集的扩展。您还学习了如何计算简单的连接度指标或运行像 PageRank 这样的复杂算法。

XGBoost 和深度学习部分提供了使用梯度下降的备选建模技术:前者是决策树,后者是深度多层感知器,其中我们可以使用 Spark 将数据集预处理为稍后可以通过 sparktf 扩展由 TensorFlow 和 Keras 消耗的记录。最后两节介绍了通过 variantsparkgeospark 扩展处理基因组和空间数据集。

这些扩展及更多内容提供了一个高级功能的全面库,结合所介绍的分析和建模技术,应该可以涵盖大多数在计算集群中运行所需的任务。然而,当功能不足时,您可以考虑编写自己的扩展,这是我们在第十三章中讨论的内容,或者您可以使用 R 代码在每个分区上应用自定义转换,正如我们在第十一章中描述的那样。

¹ 注意,AutoML 使用了交叉验证,而我们在 GLM 中没有使用。

² Rosenblatt F (1958). “感知器:大脑中信息存储和组织的概率模型。” 心理评论

³ Minsky M, Papert SA (2017). 感知机:计算几何导论. MIT 出版社。

⁴ Ackley DH, Hinton GE, Sejnowski TJ (1985). “Boltzmann 机的学习算法。” 认知科学

⁵ Chollet F, Allaire J (2018). Deep Learning with R. Manning 出版社。

第十一章:分布式 R

不像这样。不像这样。不像这样。

—瑟曦·兰尼斯特

在之前的章节中,您学习了如何在本地 Spark 实例和适当的 Spark 集群中进行数据分析和建模。具体来说,在 第十章 中,我们讨论了如何利用 Spark 和 R 社区提供的额外功能。在大多数情况下,Spark 功能和扩展的组合已经足以执行几乎任何计算。然而,对于那些 Spark 和它们的扩展功能不足的情况,您可以考虑将 R 计算分发到工作节点。

您可以在每个工作节点上运行任意的 R 代码来执行任何计算 — 比如运行模拟、从网络上爬取内容、转换数据等。此外,您还可以使用 CRAN 中可用的任何包和您组织中可用的私有包,这样可以减少您需要编写的代码量,帮助您保持高效率。

如果您已经熟悉 R,您可能会倾向于将这种方法用于所有 Spark 操作;然而,这并不是推荐使用 spark_apply() 的方式。之前的章节提供了更高效的技术和工具来解决已知问题;相比之下,spark_apply() 引入了额外的认知负担、额外的故障排除步骤、性能折衷,以及一般上要避免的额外复杂性。并不是说 spark_apply() 绝对不能使用;而是说,spark_apply() 专门用于支持之前工具和技术无法满足的用例。

概述

第一章 引入了作为处理大规模数据集技术的 MapReduce。还描述了 Apache Spark 提供了一组操作的超集,以便更轻松和更有效地执行 MapReduce 计算。第九章 通过在分布式数据集的每个分区上应用自定义转换来展示 Spark 的工作原理。例如,如果我们将分布式数值数据集的每个元素乘以 10,则 Spark 将通过多个工作节点对每个分区应用映射操作。这个过程的概念视图如 图 11-1 所示。

乘以十的映射操作

图 11-1. 乘以十的映射操作

本章介绍如何使用 spark_apply() 定义自定义 f(x) 映射操作;对于前面的示例,spark_apply() 支持定义 10 * x,如下所示:

sdf_len(sc, 3) %>% spark_apply(~ 10 * .x)
# Source: spark<?> [?? x 1]
     id
* <dbl>
1    10
2    20
3    30

注意 ~ 10 * .x 是在所有工作节点上执行的普通 R 代码。~ 运算符定义在 rlang 包中,提供了一个相当于 function(.x) 10 * .x 的紧凑函数定义;这种紧凑形式也被称为匿名函数lambda 表达式

f(x) 函数必须接受 R DataFrame(或可以自动转换为其的内容)作为输入,并且必须输出 R DataFrame,如 图 11-2 所示。

spark_apply() 映射中预期的函数签名

图 11-2. spark_apply() 映射中预期的函数签名

我们可以回顾来自 第一章 的原始 MapReduce 示例,其中定义了将句子拆分为单词并将总唯一单词计数为减少操作。

在 R 中,我们可以使用 tidytext R 包中的 unnest_tokens() 函数,在连接到 Spark 之前需要从 CRAN 安装此包。然后,您可以使用 spark_apply() 将这些句子标记为单词表:

sentences <- copy_to(sc, data_frame(text = c("I like apples", "I like bananas")))

sentences %>%
  spark_apply(~tidytext::unnest_tokens(.x, word, text))
# Source: spark<?> [?? x 1]
  word
* <chr>
1 i
2 like
3 apples
4 i
5 like
6 bananas

我们可以通过使用 dplyr 完成 MapReduce 示例中的减少操作,如下所示:

sentences %>%
  spark_apply(~tidytext::unnest_tokens(., word, text)) %>%
  group_by(word) %>%
  summarise(count = count())
# Source: spark<?> [?? x 2]
  word    count
* <chr>   <dbl>
1 i           2
2 apples      1
3 like        2
4 bananas     1

本章的其余部分将详细解释在定义通过 spark_apply() 进行自定义映射时所需的用例、特性、注意事项、考虑因素和故障排除技术。

注意

之前的句子标记化示例可以通过前几章的概念更有效地实现,具体来说是通过 sentences %>% ft_tokenizer("text", "words") %>% transmute(word = explode(words))

Use Cases

现在我们已经介绍了一个示例,帮助您理解 spark_apply() 的工作原理,接下来我们将讨论几个它的实际用例:

导入

您可以考虑使用 R 从外部数据源和格式导入数据。例如,当 Spark 或其扩展中未本地支持文件格式时,您可以考虑使用 R 代码来实现使用 R 包的分布式 自定义解析器

模型

利用已经在 R 中可用的丰富建模功能与 Spark 是很自然的事情。在大多数情况下,R 模型无法跨大数据使用;然而,我们将介绍两个特定的用例,展示 R 模型在大规模情况下的有用性。例如,当数据适合单台机器时,您可以使用 网格搜索 并行优化其参数。在数据可以分区以创建多个模型来跨数据子集进行计算的情况下,您可以使用 R 中的 分区建模 来计算跨分区的模型。

转换

您可以使用 R 丰富的数据转换能力来补充 Spark。我们将呈现一个通过 web API 调用外部系统评估数据的用例。

计算

当您需要在 R 中执行大规模计算,或者如 第一章 中所述的 大型计算 时,Spark 是分布此计算的理想选择。我们将展示 模拟 作为 R 中大规模计算的一个特定用例。

现在我们将详细探讨每个用例,并提供一个工作示例,帮助您有效地使用 spark_apply()

自定义解析器

尽管 Spark 及其各种扩展支持许多文件格式(CSV、JSON、Parquet、AVRO 等),您可能需要其他格式以便规模化使用。您可以使用 spark_apply() 和许多现有的 R 包解析这些额外的格式。在本节中,我们将看一下如何解析日志文件,虽然可以按照类似的方法解析其他文件格式。

使用 Spark 分析日志文件是很常见的,例如来自 Amazon S3 跟踪下载数据的日志。 webreadr 包可以通过支持加载存储在 Amazon S3、Squid 和常见日志格式的日志来简化解析日志的过程。在连接到 Spark 之前,您应该从 CRAN 安装 webreadr

例如,Amazon S3 日志如下所示:

#Version: 1.0
#Fields: date time x-edge-location sc-bytes c-ip cs-method cs(Host) cs-uri-stem
  sc-status cs(Referer) cs(User-Agent) cs-uri-query cs(Cookie) x-edge-result-type
  x-edge-request-id x-host-header cs-protocol cs-bytes time-taken

2014-05-23  01:13:11    FRA2    182 192.0.2.10  GET d111111abcdef8.cloudfront.net
  /view/my/file.html    200 www.displaymyfiles.com  Mozilla/4.0%20
  (compatible;%20MSIE%205.0b1;%20Mac_PowerPC)   -   zip=98101   RefreshHit
  MRVMF7KydIvxMWfJIglgwHQwZsbG2IhRJ07sn9AkKUFSHS9EXAMPLE==
  d111111abcdef8.cloudfront.net http    -   0.001

这可以通过 read_aws() 轻松解析,如下所示:

aws_log <- system.file("extdata/log.aws", package = "webreadr")
webreadr::read_aws(aws_log)
# A tibble: 2 x 18
  date                edge_location bytes_sent ip_address http_method host  path
  <dttm>              <chr>              <int> <chr>      <chr>       <chr> <chr>
1 2014-05-23 01:13:11 FRA2                 182 192.0.2.10 GET         d111… /vie…
2 2014-05-23 01:13:12 LAX1             2390282 192.0.2.2… GET         d111… /sou…
# ... with 11 more variables: status_code <int>, referer <chr>, user_agent <chr>,
#   query <chr>, cookie <chr>, result_type <chr>, request_id <chr>,
#   host_header <chr>, protocol <chr>, bytes_received <chr>, time_elapsed <dbl>

为了扩展此操作,我们可以使用 spark_apply() 使用 read_aws()

spark_read_text(sc, "logs", aws_log, overwrite = TRUE, whole = TRUE) %>%
  spark_apply(~webreadr::read_aws(.x$contents))
# Source: spark<?> [?? x 18]
  date                edge_location bytes_sent ip_address http_method host  path
* <dttm>              <chr>              <int> <chr>      <chr>       <chr> <chr>
1 2014-05-23 01:13:11 FRA2                 182 192.0.2.10 GET         d111… /vie…
2 2014-05-23 01:13:12 LAX1             2390282 192.0.2.2… GET         d111… /sou…
# ... with 11 more variables: status_code <int>, referer <chr>, user_agent <chr>,
#   query <chr>, cookie <chr>, result_type <chr>, request_id <chr>,
#   host_header <chr>, protocol <chr>, bytes_received <chr>, time_elapsed <dbl>

纯 R 和 spark_apply() 使用的代码类似;但是,使用 spark_apply(),日志会在集群中所有可用的工作节点上并行解析。

这结束了自定义解析器讨论;您可以遵循类似的方法从 R 中扩展解析许多其他文件格式。接下来,我们将看一下分区建模作为另一个重点在多个数据集之间并行建模的用例。

分区建模

R 中有许多建模包也可以通过将数据分区为适合单台计算机资源的可管理组来运行。

例如,假设您有一个跨多个城市的 1 TB 销售数据集,并且您的任务是在每个城市上创建销售预测。对于这种情况,您可以考虑按城市对原始数据集进行分区,比如每个城市 10 GB 的数据,可以由单个计算实例管理。对于这种可分区数据集,您还可以考虑使用 spark_apply(),通过在每个城市上训练每个模型来训练。

作为分区建模的一个简单示例,我们可以使用 iris 数据集按物种分区运行线性回归:

iris <- copy_to(sc, datasets::iris)

iris %>%
  spark_apply(nrow, group_by = "Species")
# Source: spark<?> [?? x 2]
  Species    result
  <chr>       <int>
1 versicolor     50
2 virginica      50
3 setosa         50

然后,你可以使用 spark_apply() 对每个物种运行线性回归:

iris %>%
  spark_apply(
    function(e) summary(lm(Petal_Length ~ Petal_Width, e))$r.squared,
    names = "r.squared",
    group_by = "Species")
# Source: spark<?> [?? x 2]
  Species    r.squared
  <chr>          <dbl>
1 versicolor     0.619
2 virginica      0.104
3 setosa         0.110

如您从 r.squared 结果和直觉上在 图 11-3 中看到的,对于 versicolor,线性模型更适合回归线:

purrr::map(c("versicolor", "virginica", "setosa"),
  ~dplyr::filter(datasets::iris, Species == !!.x) %>%
    ggplot2::ggplot(ggplot2::aes(x = Petal.Length, y = Petal.Width)) +
    ggplot2::geom_point())

在物种上建模

图 11-3。在 iris 数据集中对物种建模

这结束了我们关于如何在几个不同的可分区数据集上执行建模的简要概述。可以应用类似的技术来在相同数据集上使用不同的建模参数执行建模,接下来我们将介绍。

网格搜索

许多 R 包提供需要定义多个参数来配置和优化的模型。当这些参数的值未知时,我们可以将此未知参数列表分发到一组机器中,以找到最佳参数组合。如果列表包含多个要优化的参数,则通常会针对参数 A 和参数 B 之间的所有组合进行测试,创建一个参数网格。在这个参数网格上搜索最佳参数的过程通常称为 网格搜索

例如,我们可以定义一个参数网格来优化决策树模型,如下所示:

grid <- list(minsplit = c(2, 5, 10), maxdepth = c(1, 3, 8)) %>%
  purrr:::cross_df() %>%
  copy_to(sc, ., repartition = 9)
grid
# Source: spark<?> [?? x 2]
  minsplit maxdepth
     <dbl>    <dbl>
1        2        1
2        5        1
3       10        1
4        2        3
5        5        3
6       10        3
7        2        8
8        5        8
9       10        8

使用 repartition = 9 将网格数据集复制,以确保每个分区都包含在一个机器中,因为网格也有九行。现在,假设原始数据集适合每台机器,我们可以将此数据集分发到多台机器,并进行参数搜索,以找到最适合此数据的模型:

spark_apply(
  grid,
  function(grid, cars) {
    model <- rpart::rpart(
      am ~ hp + mpg,
      data = cars,
      control = rpart::rpart.control(minsplit = grid$minsplit,
                                     maxdepth = grid$maxdepth)
    )
    dplyr::mutate(
      grid,
      accuracy = mean(round(predict(model, dplyr::select(cars, -am))) == cars$am)
    )
  },
  context = mtcars)
# Source: spark<?> [?? x 3]
  minsplit maxdepth accuracy
     <dbl>    <dbl>    <dbl>
1        2        1    0.812
2        5        1    0.812
3       10        1    0.812
4        2        3    0.938
5        5        3    0.938
6       10        3    0.812
7        2        8    1
8        5        8    0.938
9       10        8    0.812

对于此模型,minsplit = 2maxdepth = 8 生成了最准确的结果。现在,你可以使用这个特定的参数组合来正确训练你的模型。

Web API

Web API 是通过 Web 接口提供有用功能的程序,其他程序可以重用它们。例如,像 Twitter 这样的服务提供 Web API,允许你在使用 R 和其他编程语言编写的程序中自动读取推文。你可以通过使用 spark_apply() 发送 R 代码来利用 Web API。

例如,Google 提供了一个 Web API 来使用深度学习技术对图像进行标注;你可以从 R 中使用此 API,但对于较大的数据集,你需要从 Spark 访问其 API。你可以使用 Spark 准备数据以供 Web API 使用,然后使用 spark_apply() 调用并在 Spark 中处理所有传入的结果。

下一个示例利用 googleAuthR 包对 Google Cloud 进行身份验证,使用 RoogleVision 包在 Google Vision API 上执行标注,并使用 spark_apply() 在 Spark 和 Google 的深度学习服务之间进行交互。要运行以下示例,首先需要断开与 Spark 的连接,并从 Google 开发者门户下载你的 cloudml.json 文件:

sc <- spark_connect(
  master = "local",
  config = list(sparklyr.shell.files = "cloudml.json"))

images <- copy_to(sc, data.frame(
  image = "http://pbs.twimg.com/media/DwzcM88XgAINkg-.jpg"
))

spark_apply(images, function(df) {
  googleAuthR::gar_auth_service(
    scope = "https://www.googleapis.com/auth/cloud-platform",
    json_file = "cloudml.json")

  RoogleVision::getGoogleVisionResponse(
    df$image,
    download = FALSE)
})
# Source: spark<?> [?? x 4]
  mid       description score topicality
  <chr>     <chr>       <dbl>      <dbl>
1 /m/04rky  Mammal      0.973      0.973
2 /m/0bt9lr Dog         0.958      0.958
3 /m/01z5f  Canidae     0.956      0.956
4 /m/0kpmf  Dog breed   0.909      0.909
5 /m/05mqq3 Snout       0.891      0.891

要成功地在 Web API 上运行大规模的分布式计算,API 需要能够扩展以支持来自所有 Spark 执行器的负载。我们可以相信主要服务提供商可能支持来自你的集群的所有请求。但当你调用内部 Web API 时,请确保 API 能够处理负载。此外,当你使用第三方服务时,考虑在你的集群中的所有执行器上调用其 API 的成本,以避免潜在的昂贵和意外的费用。

接下来,我们将描述一个使用案例,其中 R 用于执行分布式渲染。

模拟

您可以结合使用 R 和 Spark 进行大规模计算。我们在这里探讨的用例是使用 rayrender 软件包渲染计算密集型图像,该软件包使用光线追踪技术,这是电影制作中常用的一种逼真技术。

让我们使用这个软件包来渲染一个包含几个球体的简单场景(见 Figure 11-4),这些球体使用 lambertian material,一种漫反射材料或“哑光”。首先,使用 install.packages("rayrender") 安装 rayrender。然后,确保您已断开并重新连接 Spark:

library(rayrender)

scene <- generate_ground(material = lambertian()) %>%
  add_object(sphere(material = metal(color="orange"), z = -2)) %>%
  add_object(sphere(material = metal(color="orange"), z = +2)) %>%
  add_object(sphere(material = metal(color="orange"), x = -2))

render_scene(scene, lookfrom = c(10, 5, 0), parallel = TRUE)

在 Spark 中使用 R 和 rayrender 进行光线追踪

图 11-4. 在 Spark 中使用 R 和 rayrender 进行光线追踪

在更高的分辨率下,比如 1920 x 1080,之前的例子需要几分钟来渲染来自 Figure 11-4 的单帧;以每秒 30 帧的速度渲染几秒钟将在单台机器上花费数小时。然而,我们可以通过在多台机器上并行计算来减少这个时间。例如,使用相同数量的 CPU 的 10 台机器将减少渲染时间十倍:

system2("hadoop", args = c("fs", "-mkdir", "/rendering"))

sdf_len(sc, 628, repartition = 628) %>%
  spark_apply(function(idx, scene) {
    render <- sprintf("%04d.png", idx$id)
    rayrender::render_scene(scene, width = 1920, height = 1080,
                            lookfrom = c(12 * sin(idx$id/100),
                                         5, 12 * cos(idx$id/100)),
                            filename = render)

    system2("hadoop", args = c("fs", "-put", render, "/user/hadoop/rendering/"))
  }, context = scene, columns = list()) %>% collect()

渲染所有图像之后,最后一步是从 HDFS 中收集它们,并使用 ffmpeg 这样的工具将单独的图像转换为动画:

hadoop fs -get rendering/
ffmpeg -s 1920x1080 -i rendering/%d.png -vcodec libx264 -crf 25
       -pix_fmt yuv420p rendering.mp4
注意

此示例假设 HDFS 用作 Spark 的存储技术,并且以 hadoop 用户身份运行,您需要根据您的特定存储或用户进行调整。

我们已经涵盖了一些 spark_apply() 的常见用例,但您当然可以找到其他符合您特定需求的用例。接下来的章节将介绍您需要理解的技术概念,以创建额外的用例并有效地使用 spark_apply()

分区

大多数使用 dplyr 分析数据或者使用 MLlib 建模的 Spark 操作不需要理解 Spark 如何分区数据;它们会自动运行。然而,对于分布式 R 计算来说并非如此。对于这些操作,您需要学习和理解 Spark 如何分区您的数据,并提供与之兼容的转换。这是因为 spark_apply() 接收每个分区并允许您执行任何转换,而不是整个数据集。您可以通过 Chapter 9 中的图表和示例刷新分区和转换的概念。

要帮助您理解 spark_apply() 中如何表示分区,请考虑以下代码:

sdf_len(sc, 10) %>%
  spark_apply(~nrow(.x))
# Source: spark<?> [?? x 1]
  result
*  <int>
1      5
2      5

我们应该期望输出是总行数吗?从结果中可以看出,一般来说答案是否定的;Spark 假设数据将分布在多台机器上,因此即使是小数据集,你通常也会发现它已经被分区。因为我们不应该期望 spark_apply() 在单个分区上操作,让我们找出 sdf_len(sc, 10) 包含多少个分区:

sdf_len(sc, 10) %>% sdf_num_partitions()
[1] 2

这解释了为什么在spark_apply()下通过nrow()计算行数会检索到两行,因为有两个分区,而不是一个。spark_apply()正在检索每个分区中的行数计数,每个分区包含 5 行,而不是总共 10 行,这可能是您所期望的。

对于这个特定的示例,我们可以通过重新分区并进行累加来进一步聚合这些分区——这将类似于使用spark_apply()进行简单的 MapReduce 操作:

sdf_len(sc, 10) %>%
  spark_apply(~nrow(.x)) %>%
  sdf_repartition(1) %>%
  spark_apply(~sum(.x))
# Source: spark<?> [?? x 1]
  result
*  <int>
1     10

现在您已了解如何使用spark_apply()处理分区,我们将转而使用group_by来控制分区。

分组

当使用spark_apply()时,我们可以要求 Spark 从生成的 DataFrame 中获取显式分区。例如,如果我们需要在一个分区中处理小于四的数字,而在第二个分区中处理剩余的数字,我们可以显式创建这些组,然后请求spark_apply()使用它们:

sdf_len(sc, 10) %>%
  transmute(groups = id < 4) %>%
  spark_apply(~nrow(.x), group_by = "groups")
# Source: spark<?> [?? x 2]
  groups result
* <lgl>   <int>
1 TRUE        3
2 FALSE       7

注意,spark_apply()仍在处理两个分区,但在这种情况下,我们期望这些分区,因为我们在spark_apply()中明确请求了它们;因此,您可以安全地解释结果为“有三个小于四的整数”。

注意

您只能按照适合单台机器的分区对数据进行分组;如果其中一个分组过大,将抛出异常。为了在超出单个节点资源的组上执行操作,您可以考虑将其分区为较小单元,或者使用dplyr::do,它目前针对大分区进行了优化。

本节的要点是在处理spark_apply()时始终考虑分区。接下来,我们将深入研究spark_apply(),以了解列的解释方式。

默认情况下,spark_apply()会自动检查正在生成的 DataFrame 以学习列名和类型。例如:

sdf_len(sc, 1) %>%
  spark_apply(~ data.frame(numbers = 1, names = "abc"))
# Source: spark<?> [?? x 2]
  numbers names
*   <dbl> <chr>
1       1 abc

然而,这样做效率低下,因为spark_apply()需要运行两次:首先通过计算所有数据的子集来找到列,然后计算实际所需的值。

为了提高性能,可以通过columns参数显式指定列。此参数接受一个命名列表,列出预期在生成的 DataFrame 中的类型。我们可以通过为numbers列指定正确的类型,重写之前的示例以仅运行一次:

sdf_len(sc, 1) %>%
  spark_apply(
    ~ data.frame(numbers = 1, names = "abc"),
    columns = list(numbers = "double", names = "character"))
# Source: spark<?> [?? x 2]
  numbers names
*   <dbl> <chr>
1       1 abc

现在我们已经介绍了行和列如何与spark_apply()交互,让我们继续利用处理分布式数据集时有时需要的上下文信息。

上下文

要使用spark_apply()处理分区,可能需要包含足够小以适应每个节点的辅助数据。这在网格搜索用例中是成立的,数据集传递到所有分区并保持不分区。

我们可以修改本章中初始的f(x) = 10 * x示例以自定义乘数。它最初设置为10,但我们可以通过将其指定为context参数来使其可配置:

sdf_len(sc, 4) %>%
  spark_apply(
    function(data, context) context * data,
    context = 100
  )
# Source: spark<?> [?? x 1]
     id
  <dbl>
1   100
2   200
3   300
4   400

图 11-5 从概念上说明了这个例子。请注意,数据分区仍然是可变的;但是,上下文参数分发到所有节点。

在乘以上下文时的映射操作

图 11-5. 在乘以上下文时的映射操作

网格搜索示例使用此参数将 DataFrame 传递给每个工作节点;但是,由于上下文参数被序列化为 R 对象,它可以包含任何内容。例如,如果需要传递多个值——甚至多个数据集——可以传递一个包含值的列表。

下面的示例定义了一个f(x) = m * x + b函数,并运行m = 10b = 2

sdf_len(sc, 4) %>%
  spark_apply(
    ~.y$m * .x + .y$b,
    context = list(b = 2, m = 10)
  )
# Source: spark<?> [?? x 1]
     id
  <dbl>
1    12
2    22
3    32
4    42

请注意,我们已将context重命名为.y以缩短变量名。这是有效的,因为spark_apply()假设上下文是函数和表达式中的第二个参数。

您会发现context参数非常有用;例如,下一节将介绍如何正确构建函数,而高级用例中使用context来构建依赖于其他函数的函数。

函数

之前我们介绍了spark_apply()作为执行自定义转换的操作,使用函数或表达式。在编程文献中,具有上下文的函数也被称为闭包

表达式可用于定义短转换,如~ 10 * .x。对于表达式,.x包含一个分区,.y包含上下文(如果有)。但是,对于跨多行的复杂代码,定义表达式可能很困难。对于这些情况,函数更为合适。

函数使复杂且多行的转换成为可能,并被定义为function(data, context) {},您可以在{}中提供任意代码。在使用 Google Cloud 将图像转换为图像标题时,我们在前面的部分中看到了它们。

传递给spark_apply()的函数使用serialize()进行序列化,该函数被描述为“用于序列化到连接的简单低级接口”。serialize()当前的限制之一是,它不会序列化在其环境外引用的对象。例如,由于闭包引用了external_value,因此以下函数会报错:

external_value <- 1
spark_apply(iris, function(e) e + external_value)

作为解决此限制的变通方法,您可以将闭包所需的函数添加到context中,然后将这些函数分配给全局环境:

func_a <- function() 40
func_b <- function() func_a() + 1
func_c <- function() func_b() + 1

sdf_len(sc, 1) %>% spark_apply(function(df, context) {
  for (name in names(context)) assign(name, context[[name]], envir = .GlobalEnv)
  func_c()
}, context = list(
  func_a = func_a,
  func_b = func_b,
  func_c = func_c
))
# Source: spark<?> [?? x 1]
  result
   <dbl>
1     42

当这不可行时,您还可以创建自己的 R 包,其中包含您需要的功能,然后在spark_apply()中使用您的包。

您已经学会了spark_apply()中提供的所有功能,使用纯 R 代码。在下一节中,我们将介绍在分布计算中使用包时的方法。当您创建有用的转换时,R 包至关重要。

使用spark_apply(),您可以在 Spark 内部使用任何 R 包。例如,您可以使用broom包从线性回归输出中创建整洁的 DataFrame:

spark_apply(
  iris,
  function(e) broom::tidy(lm(Petal_Length ~ Petal_Width, e)),
  names = c("term", "estimate", "std.error", "statistic", "p.value"),
  group_by = "Species")
# Source: spark<?> [?? x 6]
  Species    term        estimate std.error statistic  p.value
  <chr>      <chr>          <dbl>     <dbl>     <dbl>    <dbl>
1 versicolor (Intercept)    1.78     0.284       6.28 9.48e- 8
2 versicolor Petal_Width    1.87     0.212       8.83 1.27e-11
3 virginica  (Intercept)    4.24     0.561       7.56 1.04e- 9
4 virginica  Petal_Width    0.647    0.275       2.36 2.25e- 2
5 setosa     (Intercept)    1.33     0.0600     22.1  7.68e-27
6 setosa     Petal_Width    0.546    0.224       2.44 1.86e- 2

第一次调用 spark_apply() 时,将会复制您本地 .libPaths() 中的所有内容(其中包含所有 R 包)到每个 Spark 工作节点中。包只会复制一次,并在连接保持打开状态时持续存在。R 库的大小可能会达到数千兆字节,因此在复制 R 包到 Spark 集群期间需要一次性的时间和资源。您可以通过设置 packages = FALSE 来禁用包分发。

注意

因为包只在 spark_connect() 连接期间复制一次,所以在连接活跃时不支持安装额外的包。因此,如果需要安装新包,需先 spark_disconnect() 断开连接,修改包,然后重新连接。此外,在本地模式下不会复制 R 包,因为这些包已经存在于本地系统中。

虽然本节内容简短,但使用分布式 R 代码的包打开了许多有趣的用例。本章介绍了一些这些用例,但通过查看当今可用的丰富 R 包生态系统,您会发现更多。

本节完成了我们讨论分发 R 代码所需功能的部分。现在我们将介绍一些您的集群需要满足以使用 spark_apply() 的要求。

集群要求

在之前的章节中介绍的功能不需要对 Spark 集群进行特殊配置,只要您的 Spark 集群正确配置,就可以使用 R。然而,对于此处介绍的功能,您的集群管理员、云提供商或您将不得不通过安装以下内容来配置您的集群:

  • 在整个集群中的每个节点都需要安装 R,以便在您的集群中执行 R 代码。

  • 在使用 Spark 2.3 或更高版本时,每个节点都需要安装 Apache Arrow(Arrow 提供的性能改进使得分布式 R 代码更接近本地 Scala 代码)。

让我们逐个审视每个要求,确保您正确考虑到它们提供的权衡或好处。

安装 R

从第一个要求开始,期望在集群的每个节点预先安装 R 运行时;这是 spark_apply() 特定的要求。

如果未在每个节点安装 R,则在尝试使用 spark_apply() 时将触发 Cannot run program, no such file or directory 错误。

请联系您的集群管理员,考虑让 R 运行时在整个集群中可用。如果 R 已安装,您可以指定安装路径以供 spark.r.command 配置设置使用,如下所示:

config <- spark_config()
config["spark.r.command"] <- "<path-to-r-version>"

sc <- spark_connect(master = "local", config = config)
sdf_len(sc, 10) %>% spark_apply(function(e) e)

同质集群是必需的,因为驱动节点分发并可能编译包到工作节点。例如,驱动程序和工作节点必须具有相同的处理器架构、系统库等。对于大多数集群来说,这通常是符合条件的,但对您的集群可能不一定成立。

不同的集群管理器、Spark 分布和云提供商支持安装额外软件(如 R)在集群的每个节点上;在安装 R 时,请遵循相应的说明。以下是一些示例:

Spark Standalone

需要连接到每台机器并安装 R;像pssh这样的工具允许您对多台机器运行单个安装命令。

Cloudera

提供一个 R 包(参见 Cloudera 博客文章“如何使用 sparklyr 和 Cloudera 数据科学工作台分发您的 R 代码”),它允许在每个工作节点上使用 R。

Amazon EMR

当提到“Amazon”时,启动 EMR 集群时预安装 R。

微软 HDInsight

当启动 EMR 集群时,R 是预安装的,不需要额外的步骤。

Livy

Livy 连接不支持分发软件包,因为客户机(预编译库的位置)可能与集群机器具有不同的处理器架构或操作系统。

严格来说,这完成了集群的最后一个要求。然而,我们强烈建议您使用spark_apply()与 Apache Arrow 支持大规模计算,以减少额外开销。

Apache Arrow

在介绍 Apache Arrow 之前,我们将讨论数据在 Spark 和 R 之间是如何存储和传输的。由于 R 从一开始就设计用于执行快速的数值计算,因此找出存储数据的最佳方式非常重要。

一些计算系统将数据内部存储为行;但是,大多数有趣的数值操作需要按列处理数据。例如,计算列的平均值需要单独处理每一列,而不是整行。Spark 默认按行存储数据,因为它更容易分区;相比之下,R 按列存储数据。因此,在数据在 Spark 和 R 之间传输时,需要某种方式转换这两种表示,如图 11-6 所示。

Spark 和 R 之间的数据转换

图 11-6. Spark 和 R 之间的数据转换

每个分区都需要进行从行到列的转换。此外,数据还必须从 Scala 的内部表示转换为 R 的内部表示。Apache Arrow 减少了这些浪费大量 CPU 周期的转换过程。

Apache Arrow 是一个跨语言的内存数据开发平台。在 Spark 中,它通过定义与许多编程语言兼容的通用数据格式来加快 Scala 和 R 之间数据传输的速度。不必转换 Scala 的内部表示和 R 的,可以同时使用两种语言相同的结构。此外,将数据从基于行的存储转换为列存储是在 Spark 中并行进行的,可以通过使用第八章中介绍的列存储格式进一步优化。改进的转换如图 11-7 所示(#distributed-r-using-arrow)。

使用 Arrow 在 Spark 和 R 之间进行数据转换

图 11-7。使用 Apache Arrow 在 Spark 和 R 之间进行数据转换

在使用spark_apply()时,虽然不需要 Apache Arrow,但强烈推荐使用。自 Spark 2.3.0 起已经可用,但需要系统管理员在每个节点上安装 Apache Arrow 运行时(参见http://arrow.apache.org/install/))。

此外,要在sparklyr中使用 Apache Arrow,还需要安装arrow包:

install.packages("arrow")

在使用arrow之前,让我们进行一次测量以验证:

system.time(
  sdf_len(sc, 10⁴) %>% spark_apply(nrow) %>% collect()
)
   user  system elapsed
  0.240   0.020   7.957

在我们的特定系统中,处理 10,000 行大约需要 8 秒钟。要启用 Arrow,只需包含该库并像往常一样使用spark_apply()。我们来测量一下spark_apply()处理 100 万行所需的时间:

library(arrow)
system.time(
  sdf_len(sc, 10⁶) %>% spark_apply(nrow) %>% collect()
)
   user  system elapsed
  0.317   0.021   3.922

在我们的系统中,Apache Arrow 可以在半时间内处理 100 倍数据:仅需 4 秒。

大多数arrow功能只需在后台运行,改善性能和数据序列化;但是,有一个设置你应该注意。spark.sql.execution.arrow.maxRecordsPerBatch配置设置了每个 Arrow 数据传输的默认大小。它与其他 Spark 组件共享,默认为 10,000 行:

library(arrow)
sdf_len(sc, 2 * 10⁴) %>% spark_apply(nrow)
# Source: spark<?> [?? x 1]
  result
   <int>
1  10000
2  10000

你可能需要根据系统处理能力调整此数字,对于大数据集可能需要减少,对于需要一起处理记录的操作可能需要增加。我们可以将此设置更改为 5,000 行,并验证分区是否相应更改:

config <- spark_config()
config$spark.sql.execution.arrow.maxRecordsPerBatch <- 5 * 10³

sc <- spark_connect(master = "local", config = config)
sdf_len(sc, 2 * 10⁴) %>% spark_apply(nrow)
# Source: spark<?> [?? x 1]
  result
   <int>
1   5000
2   5000
3   5000
4   5000

到目前为止,我们已经介绍了用例、主要操作和集群要求。现在我们将讨论在分发 R 代码时有用的故障排除技术。

故障排除

自定义转换可能因多种原因而失败。要了解如何排除错误,请通过触发错误来模拟一个问题:

sdf_len(sc, 1) %>% spark_apply(~stop("force an error"))
Error in force(code) :
  sparklyr worker rscript failure, check worker logs for details
    Log: wm_bx4cn70s6h0r5vgsldm0000gn/T/Rtmpob83LD/file2aac1a6188_spark.log

---- Output Log ----
19/03/11 14:12:24 INFO sparklyr: Worker (1) completed wait using lock for RScript

请注意,错误消息提到检查日志。在本地模式下运行时,您可以简单地运行以下命令:

spark_log(sc, filter = "terminated unexpectedly")
19/03/11 14:12:24 ERROR sparklyr: RScript (1) terminated unexpectedly:
                                              force an error

这指向我们提到的人为stop("force an error")错误。但是,如果您不是在本地模式下工作,则需要从集群管理器中检索工作节点日志。由于这可能很麻烦,一个替代方法是重新运行spark_apply(),但返回自己的错误消息:

sdf_len(sc, 1) %>% spark_apply(~tryCatch(
    stop("force an error"),
    error = function(e) e$message
))
# Source: spark<?> [?? x 1]
  result
  <chr>
1 force an error

在适用于spark_apply()的其他更高级故障排除技术中,以下部分按顺序介绍了这些技术。您应该首先尝试使用工作节点日志来进行故障排除,然后识别分区错误,最后尝试调试工作节点。

工作节点日志

每当执行spark_apply()时,会在每个工作节点上写入有关执行的信息。您可以使用此日志编写自定义消息来帮助诊断和微调您的代码。

例如,假设我们不知道df的第一列名称是什么。我们可以使用worker_log()从工作节点编写自定义日志消息如下:

sdf_len(sc, 1) %>% spark_apply(function(df) {
  worker_log("the first column in the data frame is named ", names(df)[[1]])
  df
})
# Source: spark<?> [?? x 1]
     id
* <int>
1     1

在本地运行时,我们可以按以下方式筛选工作节点的日志条目:

spark_log(sc, filter = "sparklyr: RScript")
18/12/18 11:33:47 INFO sparklyr: RScript (3513) the first column in
the dataframe is named id
18/12/18 11:33:47 INFO sparklyr: RScript (3513) computed closure
18/12/18 11:33:47 INFO sparklyr: RScript (3513) updating 1 rows
18/12/18 11:33:47 INFO sparklyr: RScript (3513) updated 1 rows
18/12/18 11:33:47 INFO sparklyr: RScript (3513) finished apply
18/12/18 11:33:47 INFO sparklyr: RScript (3513) finished

请注意,日志打印出我们的自定义日志条目,显示id是给定 DataFrame 中第一列的名称。

此功能在故障排除时非常有用;例如,如果我们使用stop()函数强制引发错误:

sdf_len(sc, 1) %>% spark_apply(function(df) {
  stop("force an error")
})

我们将收到类似以下的错误:

 Error in force(code) :
  sparklyr worker rscript failure, check worker logs for details

正如错误建议的那样,我们可以查看工作节点日志以获取具体的错误信息,如下所示:

spark_log(sc)

这将显示一个包含错误和调用堆栈的条目:

18/12/18 11:26:47 INFO sparklyr: RScript (1860) computing closure
18/12/18 11:26:47 ERROR sparklyr: RScript (1860) terminated unexpectedly:
                                                 force an error
18/12/18 11:26:47 ERROR sparklyr: RScript (1860) collected callstack:
11: stop("force and error")
10: (function (df)
{
    stop("force and error")
})(structure(list(id = 1L), class = "data.frame", row.names = c(NA,
-1L)))

请注意,只有在使用本地集群时,spark_log(sc)才会检索到工作节点的日志。在具有多台机器的正式集群中运行时,您必须使用集群管理器提供的工具和用户界面来查找这些日志条目。

解决超时问题

当您使用数百个执行器运行时,某些任务无限期挂起的可能性增加。在这种情况下,您的作业中大多数任务会成功完成,但其中少数任务仍在运行且未失败或成功。

假设您需要计算许多网页的大小。您可以使用类似以下内容的spark_apply()

sdf_len(sc, 3, repartition = 3) %>%
  spark_apply(~ download.file("https://google.com", "index.html") +
                file.size("index.html"))

有些网页可能不存在或下载时间过长。在这种情况下,大多数任务会成功,但少数任务会挂起。为了防止这些少数任务阻塞所有计算,可以使用spark.speculation Spark 设置。启用此设置后,当所有任务成功完成的比例达到 75%时,Spark 会查找执行时间超过中位数的任务并进行重试。可以使用spark.speculation.multiplier设置来配置用于确定任务运行缓慢的时间倍增器。

因此,在本例中,您可以配置 Spark 以按照四倍于中位数时间的任务进行重试,如下所示:

config <- spark_config()
config["spark.speculation"] <- TRUE
config["spark.speculation.multiplier"] <- 4

检查分区

如果特定分区失败,您可以通过计算摘要来检测损坏的分区,然后检索该特定分区。通常情况下,您可以在连接到 Spark 之前从 CRAN 安装digest

sdf_len(sc, 3) %>% spark_apply(function(x) {
    worker_log("processing ", digest::digest(x), " partition")
    # your code
    x
})

这将添加类似以下条目:

18/11/03 14:48:32 INFO sparklyr: RScript (2566)
  processing f35b1c321df0162e3f914adfb70b5416 partition

在集群中执行此操作时,请查看未完成任务的日志。一旦获取了该摘要,您可以取消作业。然后,您可以使用该摘要从 R 中检索特定的 DataFrame,如下所示:

sdf_len(sc, 3) %>% spark_apply(function(x) {
    if (identical(digest::digest(x),
                  "f35b1c321df0162e3f914adfb70b5416")) x else x[0,]
}) %>% collect()
# A tibble: 1 x 1
  result
   <int>
1      1

然后,您可以在 R 中运行此命令以进一步进行故障排除。

调试工作节点

调试器是一个工具,允许您逐行执行代码;您可以使用此工具来调试本地连接的spark_apply()。您可以通过使用debug参数启动调试模式,然后按照以下说明操作:

sdf_len(sc, 1) %>% spark_apply(function() {
  stop("Error!")
}, debug = TRUE)
Debugging spark_apply(), connect to worker debugging session as follows:
  1\. Find the workers <sessionid> and <port> in the worker logs, from RStudio
     click 'Log' under the connection, look for the last entry with contents:
     'Session (<sessionid>) is waiting for sparklyr client to connect to
      port <port>'
  2\. From a new R session run:
     debugonce(sparklyr:::spark_worker_main)
     sparklyr:::spark_worker_main(<sessionid>, <port>)

正如这些说明所示,您需要从不同的 R 会话中连接“作为工作节点”,然后逐步执行代码。这种方法比以前的方法更不直接,因为您还需要逐行查看一些sparklyr代码;因此,我们仅建议作为最后的手段使用。(您也可以尝试第二章中描述的在线资源。)

现在让我们通过简要回顾本章节所呈现的功能来总结本章节。

回顾

本章介绍了spark_apply()作为一种高级技术,您可以用它来填补 Spark 或其许多扩展功能中的空白。我们展示了spark_apply()的示例用例,用于解析数据、并行建模多个小数据集、执行网格搜索和调用 Web API。您了解了分区如何与spark_apply()相关联,以及如何创建自定义组,将上下文信息分发到所有节点,并解决问题、限制和集群配置注意事项。

我们还强烈建议在使用 Spark 与 R 时使用 Apache Arrow 作为库,并介绍了您应该注意的安装、用例和注意事项。

到目前为止,我们仅使用静态数据的大型数据集进行了工作,这些数据不会随时间而变化,在我们分析、建模和可视化它们时保持不变。在第十二章中,我们将介绍处理数据集的技术,这些数据集不仅大,而且以类似信息流的方式增长。

第十二章:流处理

我们的故事还没有结束。

—艾亚·史塔克

回顾之前的章节,我们已经涵盖了很多内容,但并非所有内容。我们分析了表格数据集,在原始文本上执行了无监督学习,分析了图形和地理数据集,甚至使用自定义 R 代码转换了数据!现在怎么办呢?

虽然我们没有明确提到,但到目前为止,我们假设您的数据是静态的,并且随时间不变。但是假设你的工作是分析交通模式以向交通部门提供建议。一个合理的方法是分析历史数据,然后设计能够在夜间计算预测的预测模型。过夜?这非常有用,但交通模式每小时甚至每分钟都在变化。您可以尝试预处理并更快地进行预测,但最终这种模型会崩溃——您无法加载大规模数据集、转换它们、评分它们、卸载它们,然后每秒重复这个过程。

相反,我们需要引入一种不同类型的数据集——一种不是静态而是动态的,一种像表格但不断增长的数据集。我们将这种数据集称为

概述

我们知道如何处理大规模静态数据集,但如何处理大规模实时数据集呢?具有无限条目的数据集被称为

对于静态数据集,如果我们使用预先训练的主题模型进行实时评分,条目将是文本行;对于实时数据集,我们将在无限数量的文本行上执行相同的评分。现实中,您永远不会处理无限数量的记录。最终您会停止流——或者这个宇宙可能会结束,看哪个先到。不管怎样,将数据集视为无限使得推理更容易。

当处理实时数据时,流最相关——例如分析 Twitter 动态或股票价格。这两个示例都有明确定义的列,如“推文”或“价格”,但总会有新的数据行需要分析。

Spark Streaming 提供了可伸缩和容错的数据处理,适用于数据流。这意味着您可以使用多台机器处理多个数据流源,与其他流或静态源进行连接,并通过至少一次保证从故障中恢复(确保每条消息被传递,但可能会多次传递)。

在 Spark 中,您通过定义转换来创建流;您可以将这些步骤视为读取、转换和写入流,如图 12-1 所示。

使用 Spark Streaming

图 12-1. 使用 Spark Streaming

让我们仔细看看每一个:

阅读

流使用任何stream_read_*()函数读取数据;读操作定义了流的。您可以定义一个或多个要读取的源。

转换

流可以使用dplyrSQL、特征转换器、评分管道或分布式 R 代码执行一个或多个转换。转换不仅可以应用于一个或多个流,还可以使用流和静态数据源的组合;例如,使用spark_read_()函数加载到 Spark 中——这意味着您可以轻松地结合静态数据和实时数据源。

写入

写操作使用stream_write_*()函数系列执行,而读操作则定义了流的接收端。您可以指定一个或多个接收端来写入数据。

您可以在多种不同的文件格式(如 CSV、JSON、Parquet、优化行列式(ORC)和文本)中读取和写入流;另请参阅表 12-1。您还可以从 Kafka 读取和写入数据,我们稍后将介绍。

表 12-1. 用于读取和写入流的 Spark 函数

格式 读取 写入
CSV stream_read_csv stream_write_csv
JSON stream_read_json stream_write_json
Kafka stream_read_kafka stream_write_kafka
ORC stream_read_orc stream_write_orc
Parquet stream_read_parquet stream_write_parquet
文本 stream_read_text stream_write_text
内存 stream_write_memory

由于转换步骤是可选的,我们可以定义的最简单的流是一个持续复制文本文件的流,从源到目的地。

首先,使用install.packages("future")安装future包并连接到 Spark。

由于流需要存在的源,创建一个_source_文件夹:

dir.create("source")

现在我们准备定义我们的第一个流!

stream <- stream_read_text(sc, "source/") %>%
  stream_write_text("destination/")

流使用stream_write_*()开始运行;执行后,流将监视source路径,并在数据到达时将数据处理到destination /路径。

我们可以使用stream_generate_test()每秒生成一个文件,其中包含遵循给定分布的文本行;您可以在附录 A 中详细了解。在实践中,您将连接到现有的源,而不必人工生成数据。然后,我们可以使用view_stream()来跟踪在源和目的地中每秒处理的行数(rps)及其随时间变化的最新值:

future::future(stream_generate_test(interval = 0.5))
stream_view(stream)

结果显示在图 12-2 中。

监控生成符合二项分布的行的流

图 12-2. 监控生成符合二项分布的行的流

注意目标流中的 rps 速率高于源流中的速率。这是预期且希望的,因为 Spark 会测量从源流接收的入站速率,同时还会测量目标流中的实际行处理时间。例如,如果每秒写入source/路径的行数为 10 行,则入站速率为 10 rps。但是,如果 Spark 仅花费 0.01 秒就可以写完这 10 行,则输出速率为 100 rps。

使用stream_stop()来正确停止从此流程中处理数据:

stream_stop(stream)

本练习介绍了如何轻松启动一个 Spark 流,根据模拟流读取和写入数据。让我们做些比仅仅复制数据更有趣的事情,进行适当的转换。

转换

在实际情况中,从流中接收的数据不会直接写入输出。Spark Streaming 作业将对数据进行转换,然后写入转换后的数据。

可以使用dplyr、SQL 查询、ML 管道或 R 代码对流进行转换。可以像对 Spark DataFrames 进行转换一样,使用任意数量的转换。

转换的源可以是流或 DataFrame,但输出始终是流。如果需要,可以从目标流中获取快照,然后将输出保存为 DataFrame。如果未指定目标流,则sparklyr将为您完成这一操作。

以下各小节涵盖了由sparklyr提供的选项,用于对流进行转换。

分析

您可以使用dplyr动词和使用DBI的 SQL 来分析流。作为一个快速示例,我们将在流上过滤行并添加列。我们不会显式调用stream_generate_test(),但如果您感到有必要验证数据是否持续处理,可以通过later包自行调用它:

library(dplyr)

stream_read_csv(sc, "source") %>%
  filter(x > 700) %>%
  mutate(y = round(x / 100))
# Source: spark<?> [inf x 2]
       x     y
   <int> <dbl>
 1   701     7
 2   702     7
 3   703     7
 4   704     7
 5   705     7
 6   706     7
 7   707     7
 8   708     7
 9   709     7
10   710     7
# … with more rows

也可以对流的整个历史执行聚合。历史可以进行过滤或不进行过滤:

stream_read_csv(sc, "source") %>%
  filter(x > 700) %>%
  mutate(y = round(x / 100)) %>%
  count(y)
# Source: spark<?> [inf x 2]
      y     n
  <dbl> <dbl>
1     8 25902
2     9 25902
3    10 13210
4     7 12692

流中最新数据的分组聚合需要一个时间戳。时间戳会记录读取函数(在本例中是stream_read_csv())第一次“看到”该特定记录的时间。在 Spark Streaming 术语中,时间戳被称为水印spark_watermark()函数会添加这个时间戳。在这个例子中,由于这五个文件是在流读取它们创建后被读取的,因此所有记录的水印都是相同的。请注意,只有 Kafka 和内存输出支持水印:

stream_read_csv(sc, "source") %>%
  stream_watermark()
# Source: spark<?> [inf x 2]
       x timestamp
   <int> <dttm>
 1   276 2019-06-30 07:14:21
 2   277 2019-06-30 07:14:21
 3   278 2019-06-30 07:14:21
 4   279 2019-06-30 07:14:21
 5   280 2019-06-30 07:14:21
 6   281 2019-06-30 07:14:21
 7   282 2019-06-30 07:14:21
 8   283 2019-06-30 07:14:21
 9   284 2019-06-30 07:14:21
10   285 2019-06-30 07:14:21
# … with more rows

创建了水印之后,可以在group_by()动词中使用它。然后可以将其输入到summarise()函数中以获取流的一些统计信息:

stream_read_csv(sc, "source") %>%
  stream_watermark() %>%
  group_by(timestamp) %>%
  summarise(
    max_x = max(x, na.rm = TRUE),
    min_x = min(x, na.rm = TRUE),
    count = n()
  )
# Source: spark<?> [inf x 4]
  timestamp           max_x min_x  count
  <dttm>              <int> <int>  <dbl>
1 2019-06-30 07:14:55  1000     1 259332

建模

目前 Spark 流不支持在实时数据集上进行训练。除了技术上的挑战之外,即使可能,训练模型也将是相当困难的,因为模型本身需要随时间而适应。作为在线学习的一种形式,这可能是 Spark 未来将支持的内容。

话虽如此,我们可以利用流使用其他建模概念,比如特征转换器和评分。让我们尝试在流中使用一个特征转换器,并且将评分留到下一节,因为我们需要训练一个模型。

下一个示例使用ft_bucketizer()特征转换器修改流,然后使用常规的dplyr函数,您可以像处理静态数据集一样使用它们:

stream_read_csv(sc, "source") %>%
  mutate(x = as.numeric(x)) %>%
  ft_bucketizer("x", "buckets", splits = 0:10 * 100) %>%
  count(buckets)  %>%
  arrange(buckets)
# Source:     spark<?> [inf x 2]
# Ordered by: buckets
   buckets     n
     <dbl> <dbl>
 1       0 25747
 2       1 26008
 3       2 25992
 4       3 25908
 5       4 25905
 6       5 25903
 7       6 25904
 8       7 25901
 9       8 25902
10       9 26162

流水线

Spark 流水线可以用于对流进行评分,但不能用于在流数据上进行训练。前者得到了完全支持,而后者是 Spark 社区正在积极开发的功能。

要对流进行评分,首先需要创建我们的模型。因此,让我们构建、拟合并保存一个简单的流水线:

cars <- copy_to(sc, mtcars)

model <- ml_pipeline(sc) %>%
  ft_binarizer("mpg", "over_30", 30) %>%
  ft_r_formula(over_30 ~ wt) %>%
  ml_logistic_regression() %>%
  ml_fit(cars)
提示

如果您选择,可以利用第五章中介绍的其他概念,如通过ml_save()ml_load()保存和重新加载流水线,然后对流进行评分。

我们可以使用stream_generate_test()基于mtcars生成流,并使用ml_transform()对模型进行评分:

future::future(stream_generate_test(mtcars, "cars-stream", iterations = 5))

ml_transform(model, stream_read_csv(sc, "cars-stream"))
# Source: spark<?> [inf x 17]
     mpg   cyl  disp    hp  drat    wt  qsec    vs    am  gear  carb over_30
   <dbl> <int> <dbl> <int> <dbl> <dbl> <dbl> <int> <int> <int> <int>   <dbl>
 1  15.5     8 318     150  2.76  3.52  16.9     0     0     3     2       0
 2  15.2     8 304     150  3.15  3.44  17.3     0     0     3     2       0
 3  13.3     8 350     245  3.73  3.84  15.4     0     0     3     4       0
 4  19.2     8 400     175  3.08  3.84  17.0     0     0     3     2       0
 5  27.3     4  79      66  4.08  1.94  18.9     1     1     4     1       0
 6  26       4 120\.     91  4.43  2.14  16.7     0     1     5     2       0
 7  30.4     4  95.1   113  3.77  1.51  16.9     1     1     5     2       1
 8  15.8     8 351     264  4.22  3.17  14.5     0     1     5     4       0
 9  19.7     6 145     175  3.62  2.77  15.5     0     1     5     6       0
10  15       8 301     335  3.54  3.57  14.6     0     1     5     8       0
# … with more rows, and 5 more variables: features <list>, label <dbl>,
#   rawPrediction <list>, probability <list>, prediction <dbl>

尽管这个例子只用了几行代码,但我们刚刚完成的实际上相当令人印象深刻。您复制数据到 Spark 中,进行特征工程,训练了一个模型,并对实时数据集进行了评分,仅仅用了七行代码!现在让我们尝试使用自定义转换,实时进行。

分布式 R

还可以使用任意的 R 代码来使用spark_apply()对流进行转换。这种方法遵循了在第十一章中讨论的相同原则,其中spark_apply()在可用数据的集群中的每个执行程序上运行 R 代码。这使得可以处理高吞吐量的流,并满足低延迟的要求:

stream_read_csv(sc, "cars-stream") %>%
  select(mpg) %>%
  spark_apply(~ round(.x), mpg = "integer") %>%
  stream_write_csv("cars-round")

正如您期望的那样,通过运行自定义的round() R 函数,它从cars-stream处理数据到cars-round。让我们窥探一下输出的目标:

spark_read_csv(sc, "cars-round")
# Source: spark<carsround> [?? x 1]
     mpg
   <dbl>
 1    16
 2    15
 3    13
 4    19
 5    27
 6    26
 7    30
 8    16
 9    20
10    15
# … with more rows

再次确保在使用流时应用您已经了解的spark_apply()的概念;例如,您应该考虑使用arrow来显著提高性能。在我们继续之前,断开与 Spark 的连接:

spark_disconnect(sc)

这是我们对流的最后一个转换。现在我们将学习如何使用 Spark Streaming 与 Kafka。

Kafka

Apache Kafka 是由 LinkedIn 开发并捐赠给 Apache 软件基金会的开源流处理软件平台。它用 Scala 和 Java 编写。用类比来描述它,Kafka 对于实时存储来说就像 Hadoop 对于静态存储一样。

Kafka 将流存储为记录,记录由键、值和时间戳组成。它可以处理包含不同信息的多个流,通过主题对它们进行分类。Kafka 通常用于连接多个实时应用程序。生产者是将数据流入 Kafka 的应用程序,而消费者则是从 Kafka 读取数据的应用程序;在 Kafka 术语中,消费者应用程序订阅主题。因此,我们可以通过 Kafka 实现的最基本的工作流程是一个具有单一生产者和单一消费者的工作流程;这在图 12-3 中有所说明。

基本工作流程

图 12-3. 基本 Kafka 工作流程

如果你是 Kafka 的新手,我们不建议你从本节运行代码。但是,如果你真的有兴趣跟随,你首先需要按照附录 A 中的说明安装 Kafka 或在你的集群中部署它。

使用 Kafka 还需要在连接到 Spark 时具备 Kafka 包。确保在你的连接config中指定了这一点:

library(sparklyr)
library(dplyr)

sc <- spark_connect(master = "local",version = "2.3", config = list(
  sparklyr.shell.packages = "org.apache.spark:spark-sql-kafka-0-10_2.11:2.3.1"
))

一旦连接成功,从流中读取数据就非常简单:

stream_read_kafka(
  sc,
  options = list(
    kafka.bootstrap.server = "host1:9092, host2:9092",
    subscribe = "<topic-name>"
    )
  )

请注意,你需要正确配置options列表;kafka.bootstrap.server需要 Kafka 主机的列表,而topicsubscribe分别定义了在写入或从 Kafka 读取时应该使用的主题。

虽然我们开始时介绍了一个简单的单一生产者和单一消费者的用例,但 Kafka 也允许更复杂的交互。接下来,我们将从一个主题中读取数据,处理其数据,然后将结果写入到另一个主题。从同一个主题生产者和消费者的系统被称为流处理器。在图 12-4 中,流处理器读取主题 A,然后将结果写入主题 B。这允许给定的消费者应用程序读取结果而不是“原始”提要数据。

Kafka 工作流程

图 12-4. 使用流处理器的 Kafka 工作流程

处理 Kafka 流时,Spark 提供了三种模式:completeupdateappendcomplete模式在每次有新批次时为每个组提供总计;update仅为最新批次中有更新的组提供总计;而append则将原始记录添加到目标主题。append模式不适用于聚合,但对于将过滤后的子集传递到目标主题非常有效。

在我们的下一个示例中,生产者将随机字母流入letters主题的 Kafka。然后,Spark 将作为流处理器,读取letters主题并计算唯一字母,然后将其写回到totals主题的 Kafka。我们将在写回到 Kafka 时使用update模式;也就是说,仅发送变化的总计到 Kafka。这些变化是在每个letters主题的批次之后确定的。

hosts  <- "localhost:9092"

read_options <- list(kafka.bootstrap.servers = hosts, subscribe = "letters")
write_options <- list(kafka.bootstrap.servers = hosts, topic = "totals")

stream_read_kafka(sc, options = read_options) %>%
  mutate(value = as.character(value)) %>%         # coerce into a character
  count(value) %>%                                # group and count letters
  mutate(value = paste0(value, "=", n)) %>%       # kafka expects a value field
  stream_write_kafka(mode = "update",
                     options = write_options)

你可以通过从 Kafka 中读取来快速查看总计:

stream_read_kafka(sc, options = totals_options)

使用新的终端会话,使用 Kafka 的命令行工具手动将单个字母添加到letters主题:

kafka-console-producer.sh --broker-list localhost:9092 --topic letters
>A
>B
>C

您输入的字母被推送到 Kafka,由 Spark 读取,在 Spark 内聚合,然后再次推送回 Kafka,最后由 Spark 消费,向您展示totals主题的一瞥。这确实是一个相当复杂的设置,但也是实时处理项目中常见的现实配置。

接下来,我们将使用 Shiny 框架实时可视化流!

Shiny

Shiny 的响应式框架非常适合支持流信息,您可以使用它来显示来自 Spark 的实时数据,使用reactiveSpark()。关于 Shiny,我们这里可能呈现的内容远远不止这些。然而,如果您已经熟悉 Shiny,这个例子应该很容易理解。

我们有一个修改过的k-means Shiny 示例,不是从静态iris数据集获取数据,而是使用stream_generate_test()生成数据,由 Spark 消费,通过reactiveSpark()检索到 Shiny,并显示如图 12-5 所示:

要运行此示例,请将以下 Shiny 应用程序存储在shiny/shiny-stream.R下:

library(sparklyr)
library(shiny)

unlink("shiny-stream", recursive = TRUE)
dir.create("shiny-stream", showWarnings = FALSE)

sc <- spark_connect(
  master = "local", version = "2.3",
  config = list(sparklyr.sanitize.column.names = FALSE))

ui <- pageWithSidebar(
  headerPanel('Iris k-means clustering from Spark stream'),
  sidebarPanel(
    selectInput('xcol', 'X Variable', names(iris)),
    selectInput('ycol', 'Y Variable', names(iris),
                selected=names(iris)[[2]]),
    numericInput('clusters', 'Cluster count', 3,
                 min = 1, max = 9)
  ),
  mainPanel(plotOutput('plot1'))
)

server <- function(input, output, session) {
  iris <- stream_read_csv(sc, "shiny-stream",
                          columns = sapply(datasets::iris, class)) %>%
    reactiveSpark()

  selectedData <- reactive(iris()[, c(input$xcol, input$ycol)])
  clusters <- reactive(kmeans(selectedData(), input$clusters))

  output$plot1 <- renderPlot({
    par(mar = c(5.1, 4.1, 0, 1))
    plot(selectedData(), col = clusters()$cluster, pch = 20, cex = 3)
    points(clusters()$centers, pch = 4, cex = 4, lwd = 4)
  })
}

shinyApp(ui, server)

然后可以使用runApp()启动这个 Shiny 应用程序:

shiny::runApp("shiny/shiny-stream.R")

在 Shiny 应用程序运行时,从同一目录启动一个新的 R 会话,并使用stream_generate_test()创建一个测试流。这将生成一系列连续数据,Spark 可以处理,并且 Shiny 可以可视化(如图 12-5 所示):

sparklyr::stream_generate_test(datasets::iris, "shiny/shiny-stream",
                               rep(5, 10³))

Spark 响应式加载数据到 Shiny 应用程序的进展

图 12-5. Spark 响应式加载数据进入 Shiny 应用程序的进展

在本节中,您学习了创建一个可以用于监控和仪表板等多种不同目的的 Shiny 应用程序是多么容易。

在更复杂的实现中,源更可能是一个 Kafka 流。

在过渡之前,断开与 Spark 的连接并清除我们使用的文件夹:

spark_disconnect(sc)

unlink(c("source", "destination", "cars-stream",
         "car-round", "shiny/shiny-stream"), recursive = TRUE)

总结

从静态数据集到实时数据集,您真正掌握了许多大规模计算技术。特别是在本章中,您学习了如果将静态数据想象为无限表格,可以将其泛化到实时数据。然后,我们能够创建一个简单的流,不进行任何数据转换,仅将数据从 A 点复制到 B 点。

当您了解到可以应用于流数据的几种不同转换时,从使用dplyrDBI包进行数据分析转换,到在建模时引入的特征转换器,再到能够进行实时评分的完整管道,最后再到使用自定义 R 代码转换数据集,这些都是需要消化的大量内容。

接着,我们介绍了 Apache Kafka 作为实时数据可靠且可扩展的解决方案。我们向您展示了如何通过介绍消费者、生产者和主题来构建实时系统。当这些要素正确结合时,它们可以创建强大的抽象来处理实时数据。

接着我们“樱桃在冰淇淋上”的结尾:展示如何在 Shiny 中使用 Spark Streaming。由于流可以转换为响应式(这是响应性世界的通用语言),这种方法的简易性让人感到惊喜。

现在是时候进入我们的最后一个(而且相当简短的)章节了,第十三章;在那里,我们将试图说服您利用您新获得的知识造福于 Spark 和 R 社区。

第十三章:贡献

留住门,留住门。

—霍德尔

在第十二章中,我们为您提供了使用 R 在 Spark 中处理大规模和实时数据的工具。在这最后一章中,我们不再专注于学习,而是更多地回馈给 Spark 和 R 社区,或者您职业生涯中的同事。要让这一切持续下去,真的需要整个社区的参与,所以我们期待您的加入!

有许多方式可以贡献,从帮助社区成员和开放 GitHub 问题到为您自己、同事或 R 和 Spark 社区提供新功能。然而,我们将在这里重点介绍编写和共享扩展 Spark 的代码,以帮助其他人使用您可以作为 Spark 扩展作者提供的新功能。具体来说,您将学习什么是扩展,可以构建哪些类型的扩展,可用的构建工具,以及从头开始构建扩展的时间和方式。

您还将学习如何利用 Spark 中的数百个扩展和 Java 中的数百万组件,这些组件可以轻松在 R 中使用。我们还将介绍如何在 Scala 中创建原生的利用 Spark 的代码。您可能知道,R 是一个很好的语言,可以与其他语言(如 C++、SQL、Python 等)进行接口操作。因此,与 R 一起使用 Scala 将遵循使 R 成为理想选择的类似实践,提供易于使用的接口,使数据处理高效且受到许多人喜爱。

概述

当您考虑回馈您更大的编码社区时,您可以询问关于您编写的任何代码的最重要的问题是:这段代码对其他人有用吗?

让我们从考虑本书中呈现的第一行最简单的代码之一开始。这段代码用于加载一个简单的 CSV 文件:

spark_read_csv(sc, "cars.csv")

这种基础的代码可能对其他人没有用处。然而,您可以将同样的例子调整为生成更多兴趣的内容,也许是以下内容:

spark_read_csv(sc, "/path/that/is/hard/to/remember/data.csv")

这段代码与第一个非常相似。但是,如果您与正在使用这个数据集的其他人一起工作,关于有用性的问题的答案将是肯定的——这很可能对其他人有用!

这很令人惊讶,因为这意味着并不是所有有用的代码都需要高级或复杂。但是,为了对其他人有用,它确实需要以易于消费的格式进行打包、呈现和分享。

首先尝试将其保存为teamdata.R文件,并编写一个包装它的函数:

load_team_data <- function() {
  spark_read_text(sc, "/path/that/is/hard/to/remember/data.csv")
}

这是一个进步,但它将要求用户一遍又一遍地手动共享这个文件。幸运的是,这个问题在 R 中很容易解决,通过R 包

一个 R 包包含了 R 代码,可以通过函数 install.packages() 进行安装。一个例子是 sparklyr。还有许多其他的 R 包可用;你也可以创建自己的包。对于那些刚开始创建包的人,我们鼓励你阅读 Hadley Wickham 的书,R Packages(O’Reilly)。创建一个 R 包允许你通过在组织中分享包文件轻松地与其他人分享你的函数。

一旦创建了一个包,有许多方法可以与同事或全世界分享它。例如,对于要保密的包,考虑使用 Drat 或像 RStudio Package Manager 这样的产品。用于公共消费的 R 包可以在 CRAN(全面的 R 存档网络)中向 R 社区提供。

这些 R 包的存储库允许用户通过 install.packages("teamdata") 安装包,而不必担心从哪里下载包。它还允许其他包重用你的包。

除了使用像 sparklyrdplyrbroom 等 R 包创建扩展 Spark 的新 R 包外,还可以使用 Spark API 或 Spark 扩展的所有功能,或编写自定义 Scala 代码。

例如,假设有一种类似 CSV 但不完全相同的新文件格式。我们可能想编写一个名为 spark_read_file() 的函数,该函数将获取到这种新文件类型的路径并在 Spark 中读取它。一种方法是使用 dplyr 处理每行文本或任何其他 R 库使用 spark_apply()。另一种方法是使用 Spark API 访问 Spark 提供的方法。第三种方法是检查 Spark 社区中是否已经提供了支持这种新文件格式的 Spark 扩展。最后但并非最不重要的是,你可以编写自定义的 Scala 代码,其中包括使用任何 Java 库,包括 Spark 及其扩展。这在 图 13-1 中显示。

使用 Spark API、Spark 扩展或 Scala 代码扩展 Spark

图 13-1. 使用 Spark API 或 Spark 扩展,或编写 Scala 代码扩展 Spark

我们将首先专注于使用 Spark API 扩展 Spark,因为调用 Spark API 所需的技术在调用 Spark 扩展或自定义 Scala 代码时也是适用的。

Spark API

在介绍 Spark API 之前,让我们考虑一个简单而又众所周知的问题。假设我们想要计算一个分布式且可能很大的文本文件中行数的问题,比如说,cars.csv,我们初始化如下:

library(sparklyr)
library(dplyr)
sc <- spark_connect(master = "local")

cars <- copy_to(sc, mtcars)
spark_write_csv(cars, "cars.csv")

现在,要计算此文件中有多少行,我们可以运行以下命令:

spark_read_text(sc, "cars.csv") %>% count()
# Source: spark<?> [?? x 1]
      n
  <dbl>
1    33

很简单:我们使用 spark_read_text() 读取整个文本文件,然后使用 dplyrcount() 来计算行数。现在,假设你既不能使用 spark_read_text(),也不能使用 dplyr 或任何其他 Spark 功能,你将如何要求 Spark 统计 cars.csv 中的行数?

如果您在 Scala 中这样做,您会发现在 Spark 文档中,通过使用 Spark API,可以像下面这样计算文件中的行数:

val textFile = spark.read.textFile("cars.csv")
textFile.count()

因此,要从 R 中使用类似 spark.read.textFile 的 Spark API 功能,可以使用 invoke()invoke_static()invoke_new()。 (正如它们的名称所示,第一个从对象中调用方法,第二个从静态对象中调用方法,第三个创建一个新对象。)然后,我们使用这些函数调用 Spark 的 API 并执行类似 Scala 中提供的代码:

spark_context(sc) %>%
  invoke("textFile", "cars.csv", 1L) %>%
  invoke("count")
[1] 33

虽然 invoke() 函数最初设计用于调用 Spark 代码,但也可以调用 Java 中的任何可用代码。例如,我们可以使用以下代码创建一个 Java 的 BigInteger

invoke_new(sc, "java.math.BigInteger", "1000000000")
<jobj[225]>
  java.math.BigInteger
  1000000000

正如您所见,创建的对象不是 R 对象,而是一个真正的 Java 对象。在 R 中,这个 Java 对象由 spark_jobj 表示。这些对象可以与 invoke() 函数或 spark_dataframe()spark_connection() 一起使用。spark_dataframe()spark_jobj 转换为 Spark DataFrame(如果可能的话),而 spark_connect() 则检索原始的 Spark 连接对象,这对于避免在函数间传递 sc 对象很有用。

虽然在某些情况下调用 Spark API 可以很有用,但 Spark 中大部分功能已经在 sparklyr 中支持。因此,扩展 Spark 的一个更有趣的方法是使用其众多现有的扩展。

Spark 扩展

在开始本节之前,请考虑导航至 spark-packages.org,这是一个跟踪由 Spark 社区提供的 Spark 扩展的网站。使用前一节中介绍的相同技术,您可以从 R 中使用这些扩展。

例如,有 Apache Solr,一个专为在大型数据集上执行全文搜索而设计的系统,目前 Apache Spark 并不原生支持。此外,在撰写本文时,还没有为 R 提供支持 Solr 的扩展。因此,让我们尝试使用 Spark 扩展来解决这个问题。

首先,如果您搜索“spark-packages.org”以找到 Solr 扩展,您应该能够找到 spark-solr。扩展的“如何”部分提到应加载 com.lucidworks.spark:spark-solr:2.0.1。我们可以在 R 中使用 sparklyr.shell.packages 配置选项来完成这个任务。

config <- spark_config()
config["sparklyr.shell.packages"] <- "com.lucidworks.spark:spark-solr:3.6.3"
config["sparklyr.shell.repositories"] <-
  "http://repo.spring.io/plugins-release/,http://central.maven.org/maven2/"

sc <- spark_connect(master = "local", config = config)

尽管通常指定 sparklyr.shell.packages 参数就足够了,但是对于这个特定的扩展,依赖项未能从 Spark 包仓库下载。你需要在 sparklyr.shell.repositories 参数下手动查找失败的依赖项,并添加更多的仓库。

注意

当你使用扩展时,Spark 将连接到 Maven 包仓库以检索它。这可能需要很长时间,具体取决于扩展和你的下载速度。在这种情况下,考虑使用 sparklyr.connect.timeout 配置参数来允许 Spark 下载所需的文件。

根据 spark-solr 文档,你可以使用以下 Scala 代码:

val options = Map(
  "collection" -> "{solr_collection_name}",
  "zkhost" -> "{zk_connect_string}"
)

val df = spark.read.format("solr")
  .options(options)
  .load()

我们可以将其翻译为 R 代码:

spark_session(sc) %>%
  invoke("read") %>%
  invoke("format", "solr") %>%
  invoke("option", "collection", "<collection>") %>%
  invoke("option", "zkhost", "<host>") %>%
  invoke("load")

然而,这段代码将失败,因为它需要一个有效的 Solr 实例,并且配置 Solr 超出了本书的范围。但是这个例子揭示了你如何创建 Spark 扩展的见解。值得一提的是,你可以使用 spark_read_source() 从通用源中读取,而不必编写自定义的 invoke() 代码。

正如在 “概述” 中指出的那样,考虑使用 R 包与其他人共享代码。虽然你可以要求你的包的用户指定 sparklyr.shell.packages,但你可以通过在你的 R 包中注册依赖项来避免这样做。依赖项在 spark_dependencies() 函数下声明;因此,对于本节中的示例:

spark_dependencies <- function(spark_version, scala_version, ...) {
  spark_dependency(
    packages = "com.lucidworks.spark:spark-solr:3.6.3",
    repositories = c(
      "http://repo.spring.io/plugins-release/",
      "http://central.maven.org/maven2/")
  )
}

.onLoad <- function(libname, pkgname) {
  sparklyr::register_extension(pkgname)
}

当你的库加载时,onLoad 函数会被 R 自动调用。它应该调用 register_extension(),然后回调 spark_dependencies(),以允许你的扩展提供额外的依赖项。本例支持 Spark 2.4,但你还应该支持将 Spark 和 Scala 版本映射到正确的 Spark 扩展版本。

你可以使用约 450 个 Spark 扩展;此外,你还可以从 Maven 仓库 使用任何 Java 库,Maven Central 包含超过 300 万个构件。虽然并非所有 Maven Central 的库都与 Spark 相关,但 Spark 扩展和 Maven 仓库的结合无疑会为你带来许多有趣的可能性!

但是,在没有 Spark 扩展可用的情况下,下一节将教你如何从你自己的 R 包中使用自定义的 Scala 代码。

使用 Scala 代码

在 Spark 中运行时,Scala 代码使你能够使用 Spark API、Spark 扩展或 Java 库中的任何方法。此外,在 Spark 中编写 Scala 代码可以提高性能,超过使用 spark_apply() 的 R 代码。通常情况下,你的 R 包结构将包含 R 代码和 Scala 代码;然而,Scala 代码需要编译为 JAR 文件(Java ARchive 文件)并包含在你的包中。图 13-2 展示了这种结构。

使用 Scala 代码时的 R 包结构

图 13-2. 使用 Scala 代码时的 R 包结构

如往常一样,R 代码应放在顶层的R文件夹下,Scala 代码放在java文件夹下,编译后的 JAR 包放在inst/java文件夹下。虽然您当然可以手动编译 Scala 代码,但可以使用辅助函数下载所需的编译器并编译 Scala 代码。

要编译 Scala 代码,您需要安装 Java 开发工具包 8(简称 JDK8)。从Oracle 的 Java 下载页面下载 JDK,这将需要您重新启动 R 会话。

您还需要Scala 2.11 和 2.12 的 Scala 编译器。Scala 编译器可以使用download_scalac()自动下载和安装:

download_scalac()

接下来,您需要使用compile_package_jars()编译您的 Scala 源代码。默认情况下,它使用spark_compilation_spec(),该函数为以下 Spark 版本编译您的源代码:

## [1] "1.5.2" "1.6.0" "2.0.0" "2.3.0" "2.4.0"

您还可以通过使用spark_compilation_spec()创建自定义条目来自定义此规范。

虽然您可以从头开始创建 Scala 代码的项目结构,但也可以简单地调用spark_extension(path)来在指定路径创建一个扩展。该扩展将主要为空,但将包含适当的项目结构以调用 Scala 代码。

由于spark_extension()在 RStudio 中注册为自定义项目扩展,您还可以通过“文件”菜单中的新项目,选择“使用 Spark 的 R 包”,如图 13-3 所示。

从 RStudio 创建 Scala 扩展包

图 13-3. 从 RStudio 创建 Scala 扩展包

一旦准备好编译您的包 JARs,您可以简单地运行以下命令:

compile_package_jars()

由于默认情况下 JAR 包编译到inst/包路径中,在构建 R 包时所有 JAR 包也将包含在内。这意味着您可以分享或发布您的 R 包,并且 R 用户可以完全使用它。对于大多数专业的 Scala 高级 Spark 用户来说,考虑为 R 用户和 R 社区编写 Scala 库,然后轻松将其打包成易于消费、使用和分享的 R 包,这是非常吸引人的。

如果您有兴趣开发带有 R 的 Spark 扩展,并且在开发过程中遇到困难,请考虑加入sparklyrGitter 频道,我们很乐意帮助这个美好的社区成长。期待您的加入!

小结

本章介绍了一组新工具,用于扩展 Spark 的功能,超越了当前 R 和 R 包的支持范围。这个庞大的新库空间包括超过 450 个 Spark 扩展和数百万个可以在 R 中使用的 Java 工件。除了这些资源,您还学习了如何使用 Scala 代码构建 Java 工件,这些工件可以轻松地从 R 中嵌入和编译。

这使我们回到了本书早期提出的目的;虽然我们知道您已经学会了如何在 R 中使用 Spark 执行大规模计算,但我们也确信您已经掌握了通过 Spark 扩展帮助其他社区成员所需的知识。我们迫不及待地想看到您的新创作,这些创作肯定会帮助扩展整个 Spark 和 R 社区。

最后,我们希望前几章为您介绍了 Spark 和 R 的简易入门。在此之后,我们提出了分析和建模作为使用 Spark 的基础,结合您已经熟悉和喜爱的 R 包。您进一步学习了如何在合适的 Spark 集群中执行大规模计算。本书的最后部分重点讨论了高级主题:使用扩展、分发 R 代码、处理实时数据,最后通过使用 R 和 Scala 代码使用 Spark 扩展来回馈社区。

我们努力呈现最佳内容。但是,如果您知道任何改进本书的方法,请在 the-r-in-spark 存储库下开启 GitHub 问题,我们将在即将推出的修订版本中解决您的建议。希望您喜欢阅读本书,并且在写作过程中像我们一样学到了很多。我们希望本书值得您的时间——您作为读者是我们的荣幸。

附录 A. 补充代码引用

本书中我们包含了对本附录的引用。这里我们包括了重要内容(并列出了可以找到这些材料的章节)。

前言

格式化

本书中用于格式化所有图形的ggplot2主题如下:

plot_style <- function() {
  font <- "Helvetica"

  ggplot2::theme_classic() +
  ggplot2::theme(
    plot.title = ggplot2::element_text(
      family = font, size=14, color = "#222222"),
    plot.subtitle = ggplot2::element_text(
      family=font, size=12, color = "#666666"),

    legend.position = "right",
    legend.background = ggplot2::element_blank(),
    legend.title = ggplot2::element_blank(),
    legend.key = ggplot2::element_blank(),
    legend.text = ggplot2::element_text(
      family=font, size=14, color="#222222"),

    axis.title.y = ggplot2::element_text(
      margin = ggplot2::margin(t = 0, r = 8, b = 0, l = 0),
      size = 14, color="#666666"),
    axis.title.x = ggplot2::element_text(
      margin = ggplot2::margin(t = -2, r = 0, b = 0, l = 0),
      size = 14, color = "#666666"),
    axis.text = ggplot2::element_text(
      family=font, size=14, color="#222222"),
    axis.text.x = ggplot2::element_text(
      margin = ggplot2::margin(5, b = 10)),
    axis.ticks = ggplot2::element_blank(),
    axis.line = ggplot2::element_blank(),

    panel.grid.minor = ggplot2::element_blank(),
    panel.grid.major.y = ggplot2::element_line(color = "#eeeeee"),
    panel.grid.major.x = ggplot2::element_line(color = "#ebebeb"),

    panel.background = ggplot2::element_blank(),

    strip.background = ggplot2::element_rect(fill = "white"),
    strip.text = ggplot2::element_text(size  = 20,  hjust = 0)
  )
}

您可以通过以下方式激活此功能:

ggplot2::theme_set(plot_style())

第一章

世界的信息存储能力

以下脚本用于生成图 1-1:

library(ggplot2)
library(dplyr)
library(tidyr)
read.csv("data/01-worlds-capacity-to-store-information.csv", skip = 8) %>%
  gather(key = storage, value = capacity, analog, digital) %>%
  mutate(year = X, terabytes = capacity / 1e+12) %>%
  ggplot(aes(x = year, y = terabytes, group = storage)) +
    geom_line(aes(linetype = storage)) +
    geom_point(aes(shape = storage)) +
    scale_y_log10(
      breaks = scales::trans_breaks("log10", function(x) 10^x),
      labels = scales::trans_format("log10", scales::math_format(10^x))
    ) +
    theme_light() +
    theme(legend.position = "bottom")

CRAN 软件包的每日下载

图 1-6 是通过以下代码生成的:

downloads_csv <- "data/01-intro-r-cran-downloads.csv"
if (!file.exists(downloads_csv)) {
  downloads <- cranlogs::cran_downloads(from = "2014-01-01", to = "2019-01-01")
  readr::write_csv(downloads, downloads_csv)
}

cran_downloads <- readr::read_csv(downloads_csv)

ggplot(cran_downloads, aes(date, count)) +
  labs(title = "CRAN Packages",
       subtitle = "Total daily downloads over time") +
  geom_point(colour="black", pch = 21, size = 1) +
  scale_x_date() + xlab("year") + ylab("downloads") +
  scale_x_date(date_breaks = "1 year",
               labels = function(x) substring(x, 1, 4)) +
  scale_y_continuous(
      limits = c(0, 3.5 * 10⁶),
      breaks = c(0.5 * 10⁶, 10⁶, 1.5 * 10⁶, 2 * 10⁶, 2.5 * 10⁶, 3 * 10⁶,
      3.5 * 10⁶),
      labels = c("", "1M", "", "2M", "", "3M", "")
    )

第二章

先决条件

安装 R

下载R 安装程序(参见图 A-1),并在 Windows、Mac 或 Linux 平台上启动它。

统计计算的 R 项目

图 A-1. 统计计算的 R 项目

安装 Java

下载Java 安装程序(参见图 A-2),并在 Windows、Mac 或 Linux 平台上启动它。

Java 下载页面

图 A-2. Java 下载页面

从 Spark 2.1 开始,需要 Java 8;然而,Spark 的早期版本支持 Java 7。不过,我们建议安装 Java Runtime Engine 8(JRE 8)。

注意

如果您是一个已经在使用 Java 开发工具包(JDK)的高级读者,请注意当前不支持 JDK 9+。因此,您需要通过卸载 JDK 9+或适当设置JAVA_HOME来降级到 JDK 8。

安装 RStudio

虽然安装 RStudio 不是与 R 一起使用 Spark 的严格要求,但它将使您更加高效,因此我们建议您安装它。下载RStudio 安装程序(参见图 A-3),然后在 Windows、Mac 或 Linux 平台上启动它。

RStudio 下载页面

图 A-3. RStudio 下载页面

启动 RStudio 后,您可以使用其控制台面板执行本章提供的代码。

使用 RStudio

如果您对 RStudio 不熟悉(见图 A-4),您应该注意以下面板:

控制台

您可以使用独立的 R 控制台来执行本书中呈现的所有代码。

软件包

该面板允许您轻松安装sparklyr,检查其版本,浏览帮助内容等。

连接

该面板允许您连接到 Spark,管理您的活动连接并查看可用数据集。

RStudio 概览

图 A-4. RStudio 概述

第三章

Hive 函数

名称 描述
size(Map<K.V>) 返回映射类型中的元素数量。
size(Array) 返回数组类型中的元素数量。
map_keys(Map<K.V>) 返回包含输入映射键的无序数组。
map_values(Map<K.V>) 返回包含输入映射值的无序数组。
array_contains(Array, value) 如果数组包含某个值,则返回TRUE
sort_array(Array) 按照数组元素的自然顺序升序排序并返回数组。
binary(string or binary) 将参数转换为二进制。
cast(expr as a given type) 将表达式expr的结果转换为给定类型。
from_unixtime(bigint unixtime[, string format]) 将 Unix 纪元(1970-01-01 00:00:00 UTC)的秒数转换为字符串。
unix_timestamp() 获取当前时间的 Unix 时间戳(秒)。
unix_timestamp(string date) 将格式为 yyyy-MM-dd HH:mm:ss 的时间字符串转换为 Unix 时间戳(秒)。
to_date(string timestamp) 返回时间戳字符串的日期部分。
year(string date) 返回日期的年份部分。
quarter(date/timestamp/string) 返回日期的季度。
month(string date) 返回日期或时间戳字符串的月份部分。
day(string date) dayofmonth(date) 返回日期或时间戳字符串的日部分。
hour(string date) 返回时间戳的小时数。
minute(string date) 返回时间戳的分钟数。
second(string date) 返回时间戳的秒数。
weekofyear(string date) 返回时间戳字符串的周数。
extract(field FROM source) 从源中检索诸如天数或小时的字段。源必须是可以转换为日期、时间戳、间隔或字符串的日期或时间戳。
datediff(string enddate, string startdate) 返回从startdateenddate的天数。
date_add(date/timestamp/string startdate, tinyint/smallint/int days) 将一定天数添加到startdate
date_sub(date/timestamp/string startdate, tinyint/smallint/int days) startdate减去一定天数。
from_utc_timestamp(\{any primitive type} ts, string timezone) 将 UTC 时间戳转换为指定时区的时间戳。
to_utc_timestamp(\{any primitive type} ts, string timezone) 将指定时区的时间戳转换为 UTC 时间戳。
current_date 返回当前日期。
current_timestamp 返回当前时间戳。
add_months(string start_date, int num_months, output_date_format) 返回start_datenum_months个月的日期。
last_day(string date) 返回日期所属月份的最后一天。
next_day(string start_date, string day_of_week) 返回晚于start_date且命名为day_of_week的第一个日期。
trunc(string date, string format) 返回按指定格式截断的日期。
months_between(date1, date2) 返回日期date1date2之间的月数。
date_format(date/timestamp/string ts, string fmt) 将日期/时间戳/字符串ts转换为指定日期格式fmt的字符串值。
if(boolean testCondition, T valueTrue, T valueFalseOrNull) testCondition为真时返回valueTrue;否则返回valueFalseOrNull
isnull( a ) 如果a为 NULL 则返回true,否则返回false
isnotnull( a ) 如果a不为 NULL 则返回true,否则返回false
nvl(T value, T default_value) 如果valueNULL,则返回默认值default_value;否则返回value
COALESCE(T v1, T v2, …) 返回第一个不为 NULL 的v,如果所有的v都为 NULL 则返回 NULL。
CASE a WHEN b THEN c [WHEN d THEN e]* [ELSE f] END a等于b时返回c;当a等于d时返回e;否则返回f
nullif( a, b ) 如果a等于b,则返回NULL;否则返回a
assert_true(boolean condition) 如果condition不为真则抛出异常;否则返回NULL
ascii(string str) 返回字符串str的第一个字符的数值值。
base64(binary bin) 将参数从二进制转换为 base64 字符串。
character_length(string str) 返回字符串str中包含的 UTF-8 字符数。
chr(bigint double A)
concat(stringǀbinary A, stringǀbinary B…) 返回按顺序传递的参数连接而成的字符串或字节。例如,concat(foo, bar)结果为foobar
context_ngrams(array<array>, array, int K, int pf) 返回从一组分词句子中提取的前 K 个上下文 N-gram。
concat_ws(string SEP, string A, string B…) 类似于concat(),但使用自定义分隔符SEP
decode(binary bin, string charset) 使用提供的字符集(US-ASCII、ISO-8859-1、UTF-8、UTF-16BE、UTF-16LE 或 UTF-16)将第一个参数解码为字符串。如果任一参数为NULL,则结果也为NULL
elt(N int,str1 string,str2 string,str3 string,…) 返回索引号上的字符串;elt(2,hello,world)返回world
encode(string src, string charset) 使用提供的字符集(US-ASCII、ISO-8859-1、UTF-8、UTF-16BE、UTF-16LE 或 UTF-16)将第一个参数编码为 BINARY。
field(val T,val1 T,val2 T,val3 T,…) 返回valval1,val2,val3,…列表中的索引,如果未找到则返回0
find_in_set(string str, string strList) 返回strList中第一次出现的str,其中strList是逗号分隔的字符串。
format_number(number x, int d) 将数字x格式化为类似'#,###,###.##'的格式,四舍五入到d位小数,并将结果作为字符串返回。如果d0,则结果没有小数点或小数部分。
get_json_object(string json_string, string path) 根据指定的 JSON 路径从 JSON 字符串json_string中提取 JSON 对象,并返回提取的 JSON 对象的 JSON 字符串。
in_file(string str, string filename) 如果字符串 str 作为文件 filename 中的整行出现,则返回 true
instr(string str, string substr) 返回字符串 str 中第一次出现 substr 的位置。
length(string A) 返回字符串 A 的长度。
locate(string substr, string str[, int pos]) 返回从位置 pos 开始,在字符串 str 中第一次出现 substr 的位置。
lower(string A) lcase(string A) 返回将字符串 A 中所有字符转换为小写的结果。
lpad(string str, int len, string pad) 返回左填充 pad 到长度 len 的字符串 str。如果 str 长度超过 len,则返回值被截短为 len 字符。
ltrim(string A) 返回修剪自字符串 A 开头(左侧)空格后的结果字符串。
ngrams(array<array>, int N, int K, int pf) 从一组标记化的句子(如 sentences() 返回的句子)中返回前 k 个 N 元组。
octet_length(string str) 返回用于存储字符串 str 的 UTF-8 编码所需的八位组数。
parse_url(string urlString, string partToExtract [, string keyToExtract]) 返回 URL 中指定部分的内容。partToExtract 的有效值包括 HOSTPATHQUERYREFPROTOCOLAUTHORITYFILEUSERINFO
printf(String format, Obj… args) 根据 printf 样式的格式字符串返回格式化后的输入。
regexp_extract(string subject, string pattern, int index) 返回使用模式 pattern 提取的字符串。
regexp_replace(string INITIAL_STRING, string PATTERN, string REPLACEMENT) 返回用 REPLACEMENT 替换 INITIAL_STRING 中所有匹配 PATTERN 的子字符串的结果字符串。
repeat(string str, int n) 将字符串 str 重复 n 次。
replace(string A, string OLD, string NEW) 返回将字符串 A 中所有非重叠出现的 OLD 替换为 NEW 后的结果。
reverse(string A) 返回反转后的字符串。
rpad(string str, int len, string pad) 返回右填充 pad 到长度 len 的字符串 str
rtrim(string A) 返回修剪自字符串 A 末尾(右侧)空格后的结果字符串。
sentences(string str, string lang, string locale) 将自然语言文本字符串分词成单词和句子,每个句子在适当的句子边界处断开,并作为单词数组返回。
space(int n) 返回包含 n 个空格的字符串。
split(string str, string pat) 使用正则表达式 pat 拆分字符串 str
str_to_map(text[, delimiter1, delimiter2]) 使用两个分隔符将文本拆分为键值对。delimiter1 用于分隔键值对,delimiter2 用于分隔每个键值对中的键和值。默认分隔符为 ,(对于 delimiter1)和 :(对于 delimiter2)。
substr(string binary A, int start) 返回从 A 的 start 位置开始直到字符串 A 的末尾的子字符串或切片。
substring_index(string A, string delim, int count) 返回字符串 A 中第count个定界符delim之前的子字符串。
translate(stringǀcharǀvarchar input, stringǀcharǀvarchar from, stringǀcharǀvarchar to) 使用from字符串中的字符替换to字符串中的对应字符来翻译输入字符串。
trim(string A) 返回从 A 两端去除空格后的字符串。
unbase64(string str) 将 base64 字符串转换为二进制。
upper(string A) ucase(string A) 返回将 A 的所有字符转换为大写的字符串。例如,upper(*fOoBaR*) 结果为 FOOBAR。
initcap(string A) 返回字符串,每个单词的首字母大写,其他字母小写。单词由空格分隔。
levenshtein(string A, string B) 返回两个字符串之间的 Levenshtein 距离。
soundex(string A) 返回字符串的soundex编码。
mask(string str[, string upper[, string lower[, string number]]]) 返回str的掩码版本。
mask_first_n(string str[, int n]) 返回str的掩码版本,首个n个值掩码。mask_first_n("1234-5678-8765-4321", 4) 结果为 nnnn-5678-8765-4321。
mask_last_n(string str[, int n]) 返回str的掩码版本,最后n个值掩码。
mask_show_first_n(string str[, int n]) 返回str的掩码版本,显示前n个字符不掩码。
mask_show_last_n(string str[, int n]) 返回str的掩码版本,显示最后n个字符不掩码。
mask_hash(stringǀcharǀvarchar str) 基于str返回一个哈希值。
java_method(class, method[, arg1[, arg2..]]) 反射的同义词。
reflect(class, method[, arg1[, arg2..]]) 使用反射调用 Java 方法,匹配参数签名。
hash(a1[, a2…]) 返回参数的哈希值。
current_user() 从配置的认证管理器返回当前用户名。
logged_in_user() 从会话状态返回当前用户名。
current_database() 返回当前数据库名称。
md5(string/binary) 计算字符串或二进制的 MD5 128 位校验和。
sha1(string/binary) sha(string/binary) 计算字符串或二进制的 SHA-1 摘要,并以十六进制字符串返回其值。
crc32(string/binary) 计算字符串或二进制参数的循环冗余校验值,并返回 bigint 值。
sha2(string/binary, int) 计算 SHA-2 系列的哈希函数(SHA-224,SHA-256,SHA-384 和 SHA-512)。
aes_encrypt(input string/binary, key string/binary) 使用 AES 加密输入。
aes_decrypt(input binary, key string/binary) 使用 AES 解密输入。
version() 返回 Hive 的版本。
count(expr) 返回检索行的总数。
sum(col), sum(DISTINCT col) 返回组中元素的总和或组中列的唯一值的总和。
avg(col), avg(DISTINCT col) 返回组中元素的平均值或组中列唯一值的平均值。
min(col) 返回组中列的最小值。
max(col) 返回组中列的最大值。
variance(col), var_pop(col) 返回组中数值列的方差。
var_samp(col) 返回组中数值列的无偏样本方差。
stddev_pop(col) 返回组中数值列的标准差。
stddev_samp(col) 返回组中数值列的无偏样本标准差。
covar_pop(col1, col2) 返回组中一对数值列的总体协方差。
covar_samp(col1, col2) 返回组中一对数值列的样本协方差。
corr(col1, col2) 返回组中一对数值列的皮尔逊相关系数。
percentile(BIGINT col, p) 返回组中列的精确第 p 个百分位数(不适用于浮点类型)。p必须介于 0 和 1 之间。
percentile(BIGINT col, array(p1 [, p2]…)) 返回组中列的精确百分位数 p1, p2, …。 pi必须介于 0 和 1 之间。
percentile_approx(DOUBLE col, p [, B]) 返回组中数值列(包括浮点类型)的近似第 p 个百分位数。 B参数控制内存成本下的近似精度。较高的值提供更好的近似值,默认为 10,000。当col中的唯一值数小于B时,这提供精确的百分位数值。
percentile_approx(DOUBLE col, array(p1 [, p2]…) [, B]) 与前一条相同,但接受并返回一个百分位值数组而不是单个值。
regr_avgx(independent, dependent) 等同于avg(dependent)
regr_avgy(independent, dependent) 等同于avg(independent)
regr_count(independent, dependent) 返回用于拟合线性回归线的非空对数。
regr_intercept(independent, dependent) 返回线性回归线的 y 截距,即方程 dependent = a * independent + b中的b值。
regr_r2(independent, dependent) 返回回归的决定系数。
regr_slope(independent, dependent) 返回线性回归线的斜率,即方程 dependent = a * independent + b中的a值。
regr_sxx(independent, dependent) 等同于 regr_count(independent, dependent) * var_pop(dependent)
regr_sxy(independent, dependent) 等同于 regr_count(independent, dependent) * covar_pop(independent, dependent)
regr_syy(independent, dependent) 等同于 regr_count(independent, dependent) * var_pop(independent)
histogram_numeric(col, b) 使用 b 个非均匀间隔的箱体计算组中数值列的直方图。输出是一个大小为 b 的双值(x, y)坐标数组,表示箱体的中心和高度。
collect_set(col) 返回一个消除了重复元素的集合。
collect_list(col) 返回一个包含重复对象的列表。
ntile(INTEGER x) 将有序分区分为 x 组称为,并为分区中的每行分配一个桶号。这允许轻松计算四分位数、四分位数、十分位数、百分位数和其他常见汇总统计信息。
explode(ARRAY a) 将数组展开为多行。返回一个包含单列(col)的行集,数组中的每个元素对应一行。
explode(MAP<Tkey,Tvalue> m) 将映射展开为多行。返回一个包含两列(keyvalue)的行集,输入映射中的每对键值对对应一行。
posexplode(ARRAY a) 将数组展开为多行,并附加一个 int 类型的位置列(原始数组中项的位置,从 0 开始)。返回一个包含两列(posval)的行集,数组中的每个元素对应一行。
inline(ARRAY<STRUCT<f1:T1,…,fn:Tn>> a) 将结构数组展开为多行。返回一个包含 N 列(N = 结构中顶级元素的数量)的行集,每个结构对应一行。
stack(int r,T1 V1,…,Tn/r Vn) n 个值 V1,…,Vn 分成 r 行。每行将有 n/r 列。 r 必须是常量。
json_tuple(string jsonStr,string k1,…,string kn) 接受 JSON 字符串和 n 个键,返回一个包含 n 个值的元组。
parse_url_tuple(string urlStr,string p1,…,string pn) 接受 URL 字符串和 n 个 URL 部分,返回一个包含 n 个值的元组。

第四章

MLlib 函数

下表展示了 sparklyr 支持的机器学习算法:

分类

Algorithm Function
Decision trees ml_decision_tree_classifier()
Gradient-boosted trees ml_gbt_classifier()
Linear support vector machines ml_linear_svc()
Logistic regression ml_logistic_regression()
Multilayer perceptron ml_multilayer_perceptron_classifier()
Naive-Bayes ml_naive_bayes()
One vs rest ml_one_vs_rest()
Random forests ml_random_forest_classifier()

回归

Algorithm Function
Accelerated failure time survival regression ml_aft_survival_regression()
Decision trees ml_decision_tree_regressor()
广义线性回归 ml_generalized_linear_regression()
梯度提升树 ml_gbt_regressor()
保序回归 ml_isotonic_regression()
线性回归 ml_linear_regression()

聚类

算法 函数
二分k-均值聚类 ml_bisecting_kmeans()
高斯混合聚类 ml_gaussian_mixture()
k-均值聚类 ml_kmeans()
潜在狄利克雷分配 ml_lda()

推荐

算法 函数
交替最小二乘因子分解 ml_als()

频繁模式挖掘

算法 函数
FP 增长 ml_fpgrowth()

特征转换器

转换器 函数
二值化 ft_binarizer()
桶分割 ft_bucketizer()
卡方特征选择器 ft_chisq_selector()
文档集合中的词汇表 ft_count_vectorizer()
离散余弦变换 ft_discrete_cosine_transform()
使用dplyr进行转换 ft_dplyr_transformer()
哈达玛积 ft_elementwise_product()
特征哈希 ft_feature_hasher()
使用哈希术语频率 export(ft_hashing_tf)
逆文档频率 ft_idf()
补全缺失值 export(ft_imputer)
索引到字符串 ft_index_to_string()
特征交互转换 ft_interaction()
缩放到[-1, 1]范围 ft_max_abs_scaler()
缩放到[min, max]范围 ft_min_max_scaler()
局部敏感哈希 ft_minhash_lsh()
转换为 n-gram ft_ngram()
使用给定 P-范数进行归一化 ft_normalizer()
独热编码 ft_one_hot_encoder()
多项式空间中的特征扩展 ft_polynomial_expansion()
映射到分箱分类特征 ft_quantile_discretizer()
SQL 转换 ft_sql_transformer()
使用修正的标准化特征 ft_standard_scaler()
过滤停用词 ft_stop_words_remover()
映射到标签索引 ft_string_indexer()
按空格切分 ft_tokenizer()
合并向量为行向量 ft_vector_assembler()
索引分类特征 ft_vector_indexer()
原始特征的子数组 ft_vector_slicer()
将单词转换为代码 ft_word2vec()

第六章

Google 趋势关于 On-Premises(主机)、云计算和 Kubernetes 的数据

创建图 6-1 的数据可以从https://bit.ly/2YnHkNI下载。

library(dplyr)

read.csv("data/clusters-trends.csv", skip = 2) %>%
  mutate(year = as.Date(paste(Month, "-01", sep = ""))) %>%
    mutate(`On-Premise` = `mainframe...Worldwide.`,
           Cloud = `cloud.computing...Worldwide.`,
           Kubernetes = `kubernetes...Worldwide.`) %>%
    tidyr::gather(`On-Premise`, Cloud, Kubernetes,
                  key = "trend", value = "popularity") %>%
    ggplot(aes(x=year, y=popularity, group=trend)) +
      geom_line(aes(linetype = trend, color = trend)) +
      scale_x_date(date_breaks = "2 year", date_labels = "%Y") +
      labs(title = "Cluster Computing Trends",
           subtitle = paste("Search popularity for on-premise (mainframe)",
                            "cloud computing and kubernetes ")) +
      scale_color_grey(start = 0.6, end = 0.2) +
      geom_hline(yintercept = 0, size = 1, colour = "#333333") +
      theme(axis.title.x = element_blank())

第十二章

流生成器

第十二章介绍的stream_generate_test()函数在第十二章中创建了一个本地测试流。此功能独立于 Spark 连接运行。下面的示例将在source子文件夹中每个文件的创建时间间隔为 1 秒:

library(sparklyr)

stream_generate_test(iterations = 5, path = "source", interval = 1)

函数完成后,所有文件应显示在文件夹中。请注意,文件大小不同:这样可以模拟真实流的行为:

file.info(file.path("source", list.files("source")))[1]
##                     size
## source/stream_1.csv   44
## source/stream_2.csv  121
## source/stream_3.csv  540
## source/stream_4.csv 2370
## source/stream_5.csv 7236

stream_generate_test() 默认创建一个单一的数值变量 DataFrame。

readr::read_csv("source/stream_5.csv")
## # A tibble: 1,489 x 1
##        x
##    <dbl>
##  1   630
##  2   631
##  3   632
##  4   633
##  5   634
##  6   635
##  7   636
##  8   637
##  9   638
## 10   639
## # ... with 1,479 more rows

安装 Kafka

这些说明是使用来自官方 Kafka 网站当前快速入门页面的信息编制的。(此书出版后不久,将会有更新版本的 Kafka 可用。)这里的想法是在“Kafka”示例中“时间戳”使用的版本:

  1. 下载 Kafka。

    wget http://apache.claz.org/kafka/2.2.0/kafka_2.12-2.2.0.tgz
    
  2. 展开tar文件并进入新文件夹。

    tar -xzf kafka_2.12-2.2.0.tgz
    cd kafka_2.12-2.2.0
    
  3. 启动随 Kafka 一起提供的 Zookeeper 服务。

    bin/zookeeper-server-start.sh config/zookeeper.properties
    
  4. 启动 Kafka 服务。

    bin/kafka-server-start.sh config/server.properties
    

确保始终先启动 Zookeeper,然后再启动 Kafka。

posted @ 2025-11-21 09:10  绝不原创的飞龙  阅读(11)  评论(0)    收藏  举报