Spark-秘籍-三-

69 阅读34分钟

Spark 秘籍(三)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

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

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

本章分为以下几个部分:

  • 使用 k-means 进行聚类

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

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

介绍

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

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

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

介绍

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

介绍

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

使用 k-means 进行聚类

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

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

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

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

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

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

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

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

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

使用 k-means 进行聚类

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

使用 k-means 进行聚类

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

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

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

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

使用 k-means 进行聚类

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

使用 k-means 进行聚类

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

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

准备工作

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

地块面积房价(以千美元计)
------
128392405
100002200
80401400
131041800
100002351
3049795
387682725
162502150
430262724
444312675
400002930
1260870
150002210
100321145
124202419
696962750
126002035
102401150
876665
81251430
117921920
15121230
1276975
675182400
98101725
63242300
125101700
156161915
154762278
133902497.5
1158725
2000870
2614730
134332050
125003330
157501120
139964100
104501655
75001550
121252100
145002100
100001175
100192047.5
487873998
535792688
107882251
118651906

让我们将这些数据转换为一个名为saratoga.c sv的逗号分隔值(CSV)文件,并将其绘制为散点图:

准备工作

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

准备工作

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

如何做…

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

  1. 启动 Spark shell:
$ spark-shell

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

使用主成分分析进行降维

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

降维有几个目的,比如:

  • 数据压缩

  • 可视化

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

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

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

让我们看看以下数据集:

使用主成分分析进行降维

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

使用主成分分析进行降维

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

使用主成分分析进行降维

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

准备好了

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

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

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

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

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

有不同的特征缩放方法:

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

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

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

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

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

房屋大小地块大小缩放后的房屋大小缩放后的地块大小房屋价格(以 1000 美元计)
252412839-0.025-0.2312405
2937100000.323-0.42200
17788040-0.654-0.5171400
124213104-1.105-0.2151800
2900100000.291-0.42351
12183049-1.126-0.814795
2722387680.1421.3122725
255316250-0.001-0.0282150
3681430260.9491.5662724
3032444310.4031.6492675
3437400000.7441.3852930
16801260-0.736-0.92870
226015000-0.248-0.1032210
166010032-0.753-0.3981145
3251124200.587-0.2562419
3039696960.4093.1532750
3401126000.714-0.2452035
162010240-0.787-0.3861150
876876-1.414-0.943665
18898125-0.56-0.5121430
4406117921.56-0.2941920
18851512-0.564-0.9051230
12761276-1.077-0.92975
3053675180.423.0232400
23239810-0.195-0.4121725
313963240.493-0.6192300
229312510-0.22-0.2511700
2635156160.068-0.0661915
229815476-0.216-0.0742278
2656133900.086-0.1982497.5
11581158-1.176-0.927725
15112000-0.879-0.876870
12522614-1.097-0.84730
214113433-0.348-0.1962050
3565125000.852-0.2513330
136815750-0.999-0.0581120
5726139962.672-0.1624100
2563104500.008-0.3731655
15517500-0.845-0.5491550
199312125-0.473-0.2742100
2555145000.001-0.1322100
157210000-0.827-0.41175
2764100190.177-0.3992047.5
7168487873.8871.9093998
4392535791.5482.1942688
3096107880.457-0.3532251
200311865-0.464-0.2891906

让我们将经过缩放的房屋大小和经过缩放的房价数据保存为scaledhousedata.csv

如何做到这一点…

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

  1. 启动 Spark shell:
$ spark-shell

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

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

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

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

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

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

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

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

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

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

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

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

如何做到这一点…

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

如何做到这一点…

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

奇异值分解降维

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

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

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

奇异值分解降维

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

奇异值分解降维

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

奇异值分解降维

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

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

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

奇异值分解降维

UV是正交矩阵:

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

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

准备就绪

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

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

准备就绪准备就绪

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

如何做…

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

  1. 启动 Spark shell:
$ spark-shell

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

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

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

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

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

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

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

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

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

第十章:推荐系统

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

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

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

介绍

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

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

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

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

  • 谷歌新闻点击量的 38%是推荐的

  • 亚马逊销售额的 35%是推荐的结果

正如我们在前几章中看到的,特征和特征选择在机器学习算法的有效性中起着重要作用。推荐引擎算法会自动发现这些特征,称为潜在特征。简而言之,有一些潜在特征决定了用户喜欢一部电影而不喜欢另一部电影。如果另一个用户具有相应的潜在特征,那么这个人也很可能对电影有相似的口味。

为了更好地理解这一点,让我们看一些样本电影评分:

电影RichBobPeterChris
Titanic535?
GoldenEye3215
Toy Story1?22
Disclosure44?4
Ace Ventura4?4?

我们的目标是预测用?符号表示的缺失条目。让我们看看是否能找到一些与电影相关的特征。首先,您将查看电影类型,如下所示:

电影类型
Titanic动作,爱情
GoldenEye动作,冒险,惊悚
Toy Story动画,儿童,喜剧
Disclosure戏剧,惊悚
Ace Ventura喜剧

现在每部电影可以根据每种类型进行评分,评分范围从 0 到 1。例如,GoldenEye不是一部主要的爱情片,所以它可能在爱情方面的评分为 0.1,但在动作方面的评分为 0.98。因此,每部电影可以被表示为一个特征向量。

注意

在本章中,我们将使用grouplens.org/datasets/mo…的 MovieLens 数据集。

InfoObjects 大数据沙箱中加载了 100k 部电影评分。您还可以从 GroupLens 下载 100 万甚至高达 1000 万的评分,以便分析更大的数据集以获得更好的预测。

我们将使用这个数据集中的两个文件:

  • u.data:这是一个以制表符分隔的电影评分列表,格式如下:
user id | item id | rating | epoch time

由于我们不需要时间戳,我们将从我们的配方数据中将其过滤掉

  • u.item:这是一个以制表符分隔的电影列表,格式如下:
movie id | movie title | release date | video release date |               IMDb URL | unknown | Action | Adventure | Animation |               Children's | Comedy | Crime | Documentary | Drama | Fantasy |               Film-Noir | Horror | Musical | Mystery | Romance | Sci-Fi |               Thriller | War | Western |

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

使用显式反馈的协同过滤

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

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

在这个例子中,我们将使用一种名为交替最小二乘法ALS)的算法。该算法基于少量潜在特征解释电影和用户之间的关联。它使用三个训练参数:秩、迭代次数和 lambda(在本章后面解释)。找出这三个参数的最佳值的最佳方法是尝试不同的值,看哪个值的均方根误差RMSE)最小。这个误差类似于标准差,但是它是基于模型结果而不是实际数据的。

准备工作

将从 GroupLens 下载的moviedata上传到hdfs中的moviedata文件夹:

$ hdfs dfs -put moviedata moviedata

我们将向这个数据库添加一些个性化评分,以便测试推荐的准确性。

你可以查看u.item来挑选一些电影并对其进行评分。以下是我选择的一些电影,以及我的评分。随意选择你想评分的电影并提供你自己的评分。

电影 ID电影名称评分(1-5)
313泰坦尼克号5
2黄金眼3
1玩具总动员1
43揭秘4
67玩具总动员4
82侏罗纪公园5
96终结者 25
121独立日4
148鬼与黑暗4

最高的用户 ID 是 943,所以我们将把新用户添加为 944。让我们创建一个新的逗号分隔的文件p.data,其中包含以下数据:

944,313,5
944,2,3
944,1,1
944,43,4
944,67,4
944,82,5
944,96,5
944,121,4
944,148,4

如何做…

  1. 将个性化电影数据上传到hdfs
$ hdfs dfs -put p.data p.data

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

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

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

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

  1. 将数据转换为个性化评分的 RDD:
scala> val pratings = pdata.map { line => 
 val Array(userId, itemId, rating) = line.split(",")
 Rating(userId.toInt, itemId.toInt, rating.toDouble) 
}

  1. 将评分与个性化评分结合:
scala> val movieratings = ratings.union(pratings)

  1. 使用秩为 5 和 10 次迭代以及 0.01 作为 lambda 构建 ALS 模型:
scala> val model = ALS.train(movieratings, 10, 10, 0.01)

  1. 让我们根据这个模型预测我对给定电影的评分会是多少。

  2. 让我们从原始的终结者开始,电影 ID 为 195:

scala> model.predict(sc.parallelize(Array((944,195)))).collect.foreach(println)
Rating(944,195,4.198642954004738)

由于我给终结者 2评了 5 分,这是一个合理的预测。

  1. 让我们尝试一下,电影 ID 为 402:
scala> model.predict(sc.parallelize(Array((944,402)))).collect.foreach(println)
Rating(944,402,2.982213836456829)

这是一个合理的猜测。

  1. 让我们尝试一下鬼与黑暗,这是我已经评分的电影,ID 为 148:
scala> model.predict(sc.parallelize(Array((944,402)))).collect.foreach(println)
Rating(944,148,3.8629938805450035)

非常接近的预测,知道我给这部电影评了 4 分。

你可以将更多电影添加到train数据集中。还有 100 万和 1000 万的评分数据集可用,这将进一步完善算法。

使用隐式反馈的协同过滤

有时,可用的反馈不是评分的形式,而是音轨播放、观看的电影等形式。这些数据乍一看可能不如用户的明确评分好,但这更加详尽。

准备工作

我们将使用来自www.kaggle.com/c/msdchallenge/data的百万首歌数据。你需要下载三个文件:

  • kaggle_visible_evaluation_triplets

  • kaggle_users.txt

  • kaggle_songs.txt

现在执行以下步骤:

  1. hdfs中创建一个songdata文件夹,并将所有三个文件放在这里:
$ hdfs dfs -mkdir songdata

  1. 将歌曲数据上传到hdfs
$ hdfs dfs -put kaggle_visible_evaluation_triplets.txt songdata/
$ hdfs dfs -put kaggle_users.txt songdata/
$ hdfs dfs -put kaggle_songs.txt songdata/

我们仍然需要做一些预处理。MLlib 中的 ALS 需要用户和产品 ID 都是整数。Kaggle_songs.txt文件有歌曲 ID 和其后的序列号,而Kaggle_users.txt文件没有。我们的目标是用相应的整数序列号替换triplets数据中的useridsongid。为此,请按照以下步骤操作:

  1. kaggle_songs数据加载为 RDD:
scala> val songs = sc.textFile("songdata/kaggle_songs.txt")

  1. 将用户数据加载为 RDD:
scala> val users = sc.textFile("songdata/kaggle_users.txt")

  1. 将三元组(用户、歌曲、播放次数)数据加载为 RDD:
scala> val triplets = sc.textFile("songdata/kaggle_visible_evaluation_triplets.txt")

  1. 将歌曲数据转换为PairRDD
scala> val songIndex = songs.map(_.split("\\W+")).map(v => (v(0),v(1).toInt))

  1. 收集songIndex作为 Map:
scala> val songMap = songIndex.collectAsMap

  1. 将用户数据转换为PairRDD
scala> val userIndex = users.zipWithIndex.map( t => (t._1,t._2.toInt))

  1. 收集userIndex作为 Map:
scala> val userMap = userIndex.collectAsMap

我们需要songMapuserMap来替换三元组中的userIdsongId。Spark 会根据需要自动在集群上提供这两个映射。这样做效果很好,但每次需要发送到集群时都很昂贵。

更好的方法是使用 Spark 的一个特性叫做broadcast变量。broadcast变量允许 Spark 作业在每台机器上保留一个只读副本的变量缓存,而不是在每个任务中传输一个副本。Spark 使用高效的广播算法来分发广播变量,因此网络上的通信成本可以忽略不计。

正如你可以猜到的,songMapuserMap都是很好的候选对象,可以包装在broadcast变量周围。执行以下步骤:

  1. 广播userMap
scala> val broadcastUserMap = sc.broadcast(userMap)

  1. 广播songMap
scala> val broadcastSongMap = sc.broadcast(songMap)

  1. triplet转换为数组:
scala> val tripArray = triplets.map(_.split("\\W+"))

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

  1. triplet数组转换为评分对象的 RDD:
scala> val ratings = tripArray.map { case Array(user, song, plays) =>
 val userId = broadcastUserMap.value.getOrElse(user, 0)
 val songId = broadcastUserMap.value.getOrElse(song, 0)
 Rating(userId, songId, plays.toDouble)
}

现在,我们的数据已经准备好进行建模和预测。

如何做…

  1. 导入 ALS:
scala> import org.apache.spark.mllib.recommendation.ALS

  1. 使用 ALS 构建一个具有 rank 10 和 10 次迭代的模型:
scala> val model = ALS.trainImplicit(ratings, 10, 10)

  1. 从三元组中提取用户和歌曲元组:
scala> val usersSongs = ratings.map( r => (r.user, r.product) )

  1. 为用户和歌曲元组做出预测:
scala> val predictions = model.predict(usersSongs)

它是如何工作的…

我们的模型需要四个参数才能工作,如下所示:

参数名称描述
Rank模型中的潜在特征数
Iterations用于运行此因子分解的迭代次数
Lambda过拟合参数
Alpha观察交互的相对权重

正如你在梯度下降的情况下看到的,这些参数需要手动设置。我们可以尝试不同的值,但最好的值是 rank=50,iterations=30,lambda=0.00001,alpha=40。

还有更多…

快速测试不同参数的一种方法是在 Amazon EC2 上生成一个 Spark 集群。这样可以灵活地选择一个强大的实例来快速测试这些参数。我已经创建了一个名为com.infoobjects.songdata的公共 s3 存储桶,以便将数据传输到 Spark。

以下是您需要遵循的步骤,从 S3 加载数据并运行 ALS:

sc.hadoopConfiguration.set("fs.s3n.awsAccessKeyId", "<your access key>")
sc.hadoopConfiguration.set("fs.s3n.awsSecretAccessKey","<your secret key>")
val songs = sc.textFile("s3n://com.infoobjects.songdata/kaggle_songs.txt")
val users = sc.textFile("s3n://com.infoobjects.songdata/kaggle_users.txt")
val triplets = sc.textFile("s3n://com.infoobjects.songdata/kaggle_visible_evaluation_triplets.txt")
val songIndex = songs.map(_.split("\\W+")).map(v => (v(0),v(1).toInt))
val songMap = songIndex.collectAsMap
val userIndex = users.zipWithIndex.map( t => (t._1,t._2.toInt))
val userMap = userIndex.collectAsMap
val broadcastUserMap = sc.broadcast(userMap)
val broadcastSongMap = sc.broadcast(songMap)
val tripArray = triplets.map(_.split("\\W+"))
import org.apache.spark.mllib.recommendation.Rating
val ratings = tripArray.map{ v =>
 val userId: Int = broadcastUserMap.value.get(v(0)).fold(0)(num => num)
 val songId: Int = broadcastSongMap.value.get(v(1)).fold(0)(num => num)
 Rating(userId,songId,v(2).toDouble)
 }
import org.apache.spark.mllib.recommendation.ALS
val model = ALS.trainImplicit(ratings, 50, 30, 0.000001, 40)
val usersSongs = ratings.map( r => (r.user, r.product) )
val predictions =model.predict(usersSongs)

这些是在usersSongs矩阵上做出的预测。

第十一章:使用 GraphX 进行图处理

本章将介绍如何使用 GraphX 进行图处理,即 Spark 的图处理库。

本章分为以下几个部分:

  • 图的基本操作

  • 使用 PageRank

  • 查找连接的组件

  • 执行邻域聚合

介绍

图分析在我们的生活中比我们想象的更常见。以最常见的例子为例,当我们要求 GPS 找到到达目的地的最短路径时,它使用图处理算法。

让我们从理解图开始。图是顶点集合的表示,其中一些顶点对由边连接。当这些边从一个方向移动到另一个方向时,称为有向图有向图

GraphX 是用于图处理的 Spark API。它提供了一个围绕 RDD 的包装器,称为弹性分布式属性图。属性图是一个具有属性附加到每个顶点和边的有向多重图。

有两种类型的图——有向图(有向图)和常规图。有向图具有沿一个方向运行的边,例如,从顶点 A 到顶点 B。Twitter 的关注者是有向图的一个很好的例子。如果约翰是大卫的 Twitter 关注者,这并不意味着大卫是约翰的关注者。另一方面,Facebook 是常规图的一个很好的例子。如果约翰是大卫的 Facebook 朋友,大卫也是约翰的 Facebook 朋友。

多重图是允许具有多个边(也称为平行边)的图。由于 GraphX 中的每条边都有属性,因此每条边都有自己的标识。

传统上,对于分布式图处理,有两种类型的系统:

  • 数据并行

  • 图并行

GraphX 旨在将两者结合在一个系统中。GraphX API 使用户能够在不移动数据的情况下将数据同时视为图和集合(RDD)。

图的基本操作

在这个示例中,我们将学习如何创建图并对其进行基本操作。

准备工作

作为一个起始示例,我们将有三个顶点,分别代表加利福尼亚州的三个城市的市中心——圣克拉拉、弗里蒙特和旧金山。以下是这些城市之间的距离:

目的地距离(英里)
圣克拉拉,加利福尼亚弗里蒙特,加利福尼亚20
弗里蒙特,加利福尼亚旧金山,加利福尼亚44
旧金山,加利福尼亚圣克拉拉,加利福尼亚53

如何做…

  1. 导入与 GraphX 相关的类:
scala> import org.apache.spark.graphx._
scala> import org.apache.spark.rdd.RDD

  1. 将顶点数据加载到数组中:
scala> val vertices = Array((1L, ("Santa Clara","CA")),(2L, ("Fremont","CA")),(3L, ("San Francisco","CA")))

  1. 将顶点数组加载到顶点的 RDD 中:
scala> val vrdd = sc.parallelize(vertices)

  1. 将边数据加载到数组中:
scala> val edges = Array(Edge(1L,2L,20),Edge(2L,3L,44),Edge(3L,1L,53))

  1. 将数据加载到边的 RDD 中:
scala> val erdd = sc.parallelize(edges)

  1. 创建图:
scala> val graph = Graph(vrdd,erdd)

  1. 打印图的所有顶点:
scala> graph.vertices.collect.foreach(println)

  1. 打印图的所有边:
scala> graph.edges.collect.foreach(println)

  1. 打印边的三元组;通过向边添加源和目的地属性来创建三元组:
scala> graph.triplets.collect.foreach(println)

  1. 图的入度是它具有的内向边的数量。打印每个顶点的入度(作为VertexRDD[Int]):
scala> graph.inDegrees

使用 PageRank

PageRank 衡量了图中每个顶点的重要性。PageRank 是由谷歌的创始人发起的,他们使用了这样一个理论,即互联网上最重要的页面是链接到它们的链接最多的页面。PageRank 还考虑了指向目标页面的页面的重要性。因此,如果给定的网页从排名较高的页面接收到传入链接,它将排名较高。

准备工作

我们将使用维基百科页面链接数据来计算页面排名。维基百科以数据库转储的形式发布其数据。我们将使用来自haselgrove.id.au/wikipedia.htm的链接数据,该数据以两个文件的形式存在:

  • links-simple-sorted.txt

  • titles-sorted.txt

我已经将它们都放在了 Amazon S3 上,路径为s3n://com.infoobjects.wiki/linkss3n://com.infoobjects.wiki/nodes。由于数据量较大,建议您在 Amazon EC2 或本地集群上运行。沙箱可能会非常慢。

您可以使用以下命令将文件加载到hdfs中:

$ hdfs dfs -mkdir wiki
$ hdfs dfs -put links-simple-sorted.txt wiki/links.txt
$ hdfs dfs -put titles-sorted.txt wiki/nodes.txt

如何做…

  1. 导入与 GraphX 相关的类:
scala> import org.apache.spark.graphx._

  1. hdfs加载边缘,使用 20 个分区:
scala> val edgesFile = sc.textFile("wiki/links.txt",20)

或者,从 Amazon S3 加载边缘:

scala> val edgesFile = sc.textFile("s3n:// com.infoobjects.wiki/links",20)

注意

links文件以“源链接:link1 link2…”的格式包含链接。

  1. 展平并将其转换为“link1,link2”格式,然后将其转换为Edge对象的 RDD:
scala> val edges = edgesFile.flatMap { line =>
 val links = line.split("\\W+")
 val from = links(0)
 val to = links.tail
 for ( link <- to) yield (from,link)
 }.map( e => Edge(e._1.toLong,e._2.toLong,1))

  1. hdfs加载顶点,使用 20 个分区:
scala> val verticesFile = sc.textFile("wiki/nodes.txt",20)

  1. 或者,从 Amazon S3 加载边缘:
scala> val verticesFile = sc.textFile("s3n:// com.infoobjects.wiki/nodes",20)

  1. 为顶点提供索引,然后交换它以使其成为(索引,标题)格式:
scala> val vertices = verticesFile.zipWithIndex.map(_.swap)

  1. 创建graph对象:
scala> val graph = Graph(vertices,edges)

  1. 运行 PageRank 并获取顶点:
scala> val ranks = graph.pageRank(0.001).vertices

  1. 由于排名是以(顶点 ID,pagerank)格式,因此交换它以使其成为(pagerank,顶点 ID)格式:
scala> val swappedRanks = ranks.map(_.swap)

  1. 排序以首先获取排名最高的页面:
scala> val sortedRanks = swappedRanks.sortByKey(false)

  1. 获取排名最高的页面:
scala> val highest = sortedRanks.first

  1. 前面的命令给出了顶点 ID,您仍然需要查找以查看具有排名的实际标题。让我们进行连接:
scala> val join = sortedRanks.join(vertices)

  1. 在将格式从(顶点 ID,(页面排名,标题))转换为(页面排名,(顶点 ID,标题))格式后,再次对连接的 RDD 进行排序:
scala> val final = join.map ( v => (v._2._1, (v._1,v._2._2))).sortByKey(false)

  1. 打印排名前五的页面
scala> final.take(5).collect.foreach(println)

这是输出应该是什么样子的:

(12406.054646736622,(5302153,United_States'_Country_Reports_on_Human_Rights_Practices))
(7925.094429748747,(84707,2007,_Canada_budget)) (7635.6564216408515,(88822,2008,_Madrid_plane_crash)) (7041.479913258444,(1921890,Geographic_coordinates)) (5675.169862343964,(5300058,United_Kingdom's))

查找连接的组件

连接的组件是原始图的子图(其顶点是原始图的顶点集的子集,其边是原始图的边集的子集),其中任何两个顶点都通过边或一系列边连接到彼此。

理解它的一种简单方法是看一下夏威夷的道路网络图。这个州有许多岛屿,它们之间没有通过道路连接。在每个岛屿内,大多数道路将相互连接。找到连接的组件的目标是找到这些集群。

连接的组件算法使用其最低编号的顶点的 ID 标记图的每个连接组件。

准备好了

我们将在这里为我们知道的集群构建一个小图,并使用连接的组件来对它们进行分隔。让我们看看以下数据:

准备就绪

追随者跟随者
约翰帕特
帕特戴夫
加里克里斯
克里斯比尔

前面的数据是一个简单的数据,有六个顶点和两个集群。让我们将这些数据放在两个文件的形式中:nodes.csvedges.csv

以下是nodes.csv的内容:

1,John
2,Pat
3,Dave
4,Gary
5,Chris
6,Bill

以下是edges.csv的内容:

1,2,follows
2,3,follows
4,5,follows
5,6,follows

我们应该期望连接组件算法识别出两个集群,第一个由(1,约翰)标识,第二个由(4,加里)标识。

您可以使用以下命令将文件加载到hdfs中:

$ hdfs dfs -mkdir data/cc
$ hdfs dfs -put nodes.csv data/cc/nodes.csv
$ hdfs dfs -put edges.csv data/cc/edges.csv

如何做…

  1. 加载 Spark shell:
$ spark-shell

  1. 导入与 GraphX 相关的类:
scala> import org.apache.spark.graphx._

  1. hdfs加载边缘:
scala> val edgesFile = sc.textFile("hdfs://localhost:9000/user/hduser/data/cc/edges.csv")

  1. edgesFile RDD 转换为边的 RDD:
scala> val edges = edgesFile.map(_.split(",")).map(e => Edge(e(0).toLong,e(1).toLong,e(2)))

  1. hdfs加载顶点:
scala> val verticesFile = sc.textFile("hdfs://localhost:9000/user/hduser/data/cc/nodes.csv")

  1. 映射顶点:
scala> val vertices = verticesFile.map(_.split(",")).map( e => (e(0).toLong,e(1)))

  1. 创建graph对象:
scala> val graph = Graph(vertices,edges)

  1. 计算连接的组件:
scala> val cc = graph.connectedComponents

  1. 找到连接组件的顶点(这是一个子图):
scala> val ccVertices = cc.vertices

  1. 打印ccVertices
scala> ccVertices.collect.foreach(println)

如您在输出中所见,顶点 1,2,3 指向 1,而 4,5,6 指向 4。这两个都是它们各自集群中索引最低的顶点。

执行邻域聚合

GraphX 通过隔离每个顶点及其邻居来进行大部分计算。这使得在分布式系统上处理大规模图数据变得更加容易。这使得邻域操作非常重要。GraphX 有一种机制可以在每个邻域级别进行,即aggregateMessages方法。它分两步进行:

  1. 在第一步(方法的第一个函数)中,消息被发送到目标顶点或源顶点(类似于 MapReduce 中的 Map 函数)。

  2. 在第二步(方法的第二个函数)中,对这些消息进行聚合(类似于 MapReduce 中的 Reduce 函数)。

准备好了

让我们构建一个追随者的小数据集:

追随者跟随者
约翰巴拉克
帕特巴拉克
加里巴拉克
克里斯米特
罗布米特

我们的目标是找出每个节点有多少关注者。让我们以两个文件的形式加载这些数据:nodes.csvedges.csv

以下是nodes.csv的内容:

1,Barack
2,John
3,Pat
4,Gary
5,Mitt
6,Chris
7,Rob

以下是edges.csv的内容:

2,1,follows
3,1,follows
4,1,follows
6,5,follows
7,5,follows

您可以使用以下命令将文件加载到hdfs

$ hdfs dfs -mkdir data/na
$ hdfs dfs -put nodes.csv data/na/nodes.csv
$ hdfs dfs -put edges.csv data/na/edges.csv

如何做…

  1. 加载 Spark shell:
$ spark-shell

  1. 导入与 GraphX 相关的类:
scala> import org.apache.spark.graphx._

  1. hdfs加载边:
scala> val edgesFile = sc.textFile("hdfs://localhost:9000/user/hduser/data/na/edges.csv")

  1. 将边转换为边的 RDD:
scala> val edges = edgesFile.map(_.split(",")).map(e => Edge(e(0).toLong,e(1).toLong,e(2)))

  1. hdfs加载顶点:
scala> val verticesFile = sc.textFile("hdfs://localhost:9000/user/hduser/data/cc/nodes.csv")

  1. 映射顶点:
scala> val vertices = verticesFile.map(_.split(",")).map( e => (e(0).toLong,e(1)))

  1. 创建graph对象:
scala> val graph = Graph(vertices,edges)

  1. 通过向关注者发送消息,消息中包含每个关注者的关注者数量,即 1,然后添加关注者数量来进行邻域聚合:
scala> val followerCount = graph.aggregateMessages(Int), (a, b) => (a+b))

  1. 以(被关注者,关注者数量)的形式打印followerCount
scala> followerCount.collect.foreach(println)

您应该获得类似以下的输出:

(1,3)
(5,2)

第十二章:优化和性能调优

本章涵盖了在使用 Spark 时的各种优化和性能调优最佳实践。

本章分为以下几个配方:

  • 优化内存

  • 使用压缩以提高性能

  • 使用序列化以提高性能

  • 优化垃圾收集

  • 优化并行级别

  • 理解优化的未来-项目钨

介绍

在研究各种优化 Spark 的方法之前,最好先了解 Spark 的内部情况。到目前为止,我们已经在较高级别上看待了 Spark,重点是各种库提供的功能。

让我们重新定义一个 RDD。从外部来看,RDD 是一个分布式的不可变对象集合。在内部,它由以下五个部分组成:

  • 一组分区(rdd.getPartitions

  • 对父 RDD 的依赖列表(rdd.dependencies

  • 计算分区的函数,给定其父级

  • 分区器(可选)(rdd.partitioner

  • 每个分区的首选位置(可选)(rdd.preferredLocations

前三个是 RDD 重新计算所需的,以防数据丢失。当组合在一起时,称为血统。最后两个部分是优化。

一组分区是数据如何分布到节点的。在 HDFS 的情况下,这意味着InputSplits,它们大多与块相同(除非记录跨越块边界;在这种情况下,它将比块稍大)。

让我们重新审视我们的wordCount示例,以了解这五个部分。这是wordCount在数据集级别视图下的 RDD 图的样子:

介绍

基本上,流程如下:

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

以下是words RDD 的五个部分:

分区每个 hdfs 输入拆分/块一个分区(org.apache.spark.rdd.HadoopPartition
依赖
计算函数读取块
首选位置hdfs 块位置
分区器
  1. words RDD 中的单词标记化,每个单词占一行:
scala> val wordsFlatMap = words.flatMap(_.split("\\W+"))

以下是wordsFlatMap RDD 的五个部分:

分区与父 RDD 相同,即wordsorg.apache.spark.rdd.HadoopPartition
依赖与父 RDD 相同,即wordsorg.apache.spark.OneToOneDependency
计算函数计算父级并拆分每个元素并展平结果
首选位置询问父母
分区器
  1. wordsFlatMap RDD 中的每个单词转换为(单词,1)元组:
scala> val wordsMap = wordsFlatMap.map( w => (w,1))

以下是wordsMap RDD 的五个部分:

分区与父 RDD 相同,即 wordsFlatMap(org.apache.spark.rdd.HadoopPartition)
依赖与父 RDD 相同,即 wordsFlatMap(org.apache.spark.OneToOneDependency)
计算函数计算父级并将其映射到 PairRDD
首选位置询问父母
分区器
  1. 将给定键的所有值减少并求和:
scala> val wordCount = wordsMap.reduceByKey(_+_)

以下是wordCount RDD 的五个部分:

分区每个 reduce 任务一个(org.apache.spark.rdd.ShuffledRDDPartition
依赖每个父级的 Shuffle 依赖(org.apache.spark.ShuffleDependency
计算函数对洗牌数据进行加法
首选位置
分区器HashPartitioner(org.apache.spark.HashPartitioner

这是wordCount在分区级别视图下的 RDD 图的样子:

介绍

优化内存

Spark 是一个复杂的分布式计算框架,有许多组成部分。各种集群资源,如内存、CPU 和网络带宽,可能在各个点成为瓶颈。由于 Spark 是一个内存计算框架,内存的影响最大。

另一个问题是,Spark 应用程序通常使用大量内存,有时超过 100GB。这种内存使用量在传统的 Java 应用程序中并不常见。

在 Spark 中,有两个地方需要进行内存优化,即在驱动程序和执行程序级别。

您可以使用以下命令来设置驱动程序内存:

  • Spark shell:
$ spark-shell --drive-memory 4g

  • Spark 提交:
$ spark-submit --drive-memory 4g

您可以使用以下命令来设置执行程序内存:

  • Spark shell:
$ spark-shell --executor-memory 4g

  • Spark 提交:
$ spark-submit --executor-memory 4g

要理解内存优化,了解 Java 中内存管理的工作原理是一个好主意。对象驻留在 Java 堆中。堆在 JVM 启动时创建,并且可以根据需要调整大小(基于配置中分配的最小和最大大小,即-Xms-Xmx)。

堆被分为两个空间或代:年轻空间和老年空间。年轻空间用于分配新对象。年轻空间包括一个称为伊甸园的区域和两个较小的幸存者空间。当幼儿园变满时,通过运行称为年轻收集的特殊过程来收集垃圾,其中所有已经存在足够长时间的对象都被提升到老年空间。当老年空间变满时,通过运行称为老年收集的过程来在那里收集垃圾。

优化内存

幼儿园背后的逻辑是,大多数对象的寿命非常短。年轻收集旨在快速找到新分配的对象并将它们移动到老年空间。

JVM 使用标记和清除算法进行垃圾回收。标记和清除收集包括两个阶段。

在标记阶段,所有具有活动引用的对象都被标记为活动的,其余的被假定为垃圾收集的候选对象。在清除阶段,垃圾收集候选对象占用的空间被添加到空闲列表中,即它们可以分配给新对象。

标记和清除有两个改进。一个是并发标记和清除CMS),另一个是并行标记和清除。CMS 专注于较低的延迟,而后者专注于更高的吞吐量。这两种策略都有性能权衡。CMS 不进行压缩,而并行垃圾收集器GC)执行整个堆的压缩,这会导致暂停时间。作为经验法则,对于实时流处理,应该使用 CMS,否则使用并行 GC。

如果您希望同时具有低延迟和高吞吐量,Java 1.7 更新 4 之后还有另一个选项,称为垃圾优先 GCG1)。G1 是一种服务器式垃圾收集器,主要用于具有大内存的多核机器。它计划作为 CMS 的长期替代品。因此,为了修改我们的经验法则,如果您使用 Java 7 及以上版本,只需使用 G1。

G1 将堆分成一组大小相等的区域,每个区域都是虚拟内存的连续范围。每个区域被分配了一个角色,如伊甸园、幸存者和老年。G1 执行并发全局标记阶段,以确定整个堆中对象的活动引用。标记阶段结束后,G1 知道哪些区域大部分是空的。它首先在这些区域中进行收集,从而释放更多的内存。

优化内存

G1 选择的用于垃圾收集的区域使用疏散进行垃圾收集。G1 将对象从堆的一个或多个区域复制到堆上的单个区域,并且它既压缩又释放内存。这种疏散是在多个核心上并行执行的,以减少暂停时间并增加吞吐量。因此,每次垃圾收集循环都会减少碎片化,同时在用户定义的暂停时间内工作。

在 Java 中内存优化有三个方面:

  • 内存占用

  • 访问内存中的对象的成本

  • 垃圾收集的成本

一般来说,Java 对象访问速度快,但占用的空间比其中的实际数据多得多。

使用压缩来提高性能

数据压缩涉及使用比原始表示更少的位对信息进行编码。压缩在大数据技术中发挥着重要作用。它使数据的存储和传输更加高效。

当数据经过压缩时,它变得更小,因此磁盘 I/O 和网络 I/O 都变得更快。它还节省了存储空间。每种优化都有成本,压缩的成本体现在增加的 CPU 周期上,用于压缩和解压缩数据。

Hadoop 需要将数据分割成块,无论数据是否经过压缩。只有少数压缩格式是可分割的。

大数据加载的两种最流行的压缩格式是 LZO 和 Snappy。 Snappy 不可分割,而 LZO 可以。另一方面,Snappy 是一种更快的格式。

如果压缩格式像 LZO 一样是可分割的,输入文件首先被分割成块,然后进行压缩。由于压缩发生在块级别,因此解压缩可以在块级别以及节点级别进行。

如果压缩格式不可分割,则压缩发生在文件级别,然后将其分割成块。在这种情况下,块必须合并回文件,然后才能进行解压缩,因此无法在节点级别进行解压缩。

对于支持的压缩格式,Spark 将自动部署编解码器进行解压缩,用户无需采取任何操作。

使用序列化来提高性能

序列化在分布式计算中起着重要作用。有两种支持序列化 RDD 的持久性(存储)级别:

  • MEMORY_ONLY_SER:将 RDD 存储为序列化对象。它将为每个分区创建一个字节数组

  • MEMORY_AND_DISK_SER:这类似于MEMORY_ONLY_SER,但会将不适合内存的分区溢出到磁盘

以下是添加适当持久性级别的步骤:

  1. 启动 Spark shell:
$ spark-shell

  1. 导入与之相关的StorageLevel和隐式转换:
scala> import org.apache.spark.storage.StorageLevel._

  1. 创建一个 RDD:
scala> val words = sc.textFile("words")

  1. 持久化 RDD:
scala> words.persist(MEMORY_ONLY_SER)

尽管序列化大大减少了内存占用,但由于反序列化而增加了额外的 CPU 周期。

默认情况下,Spark 使用 Java 的序列化。由于 Java 序列化速度较慢,更好的方法是使用Kryo库。 Kryo要快得多,有时甚至比默认值紧凑 10 倍。

如何做到…

您可以通过在SparkConf中进行以下设置来使用Kryo

  1. 通过设置Kryo作为序列化器启动 Spark shell:
$ spark-shell --conf spark.serializer=org.apache.spark.serializer.KryoSerializer

  1. Kryo自动注册大部分核心 Scala 类,但如果您想注册自己的类,可以使用以下命令:
scala> sc.getConf.registerKryoClasses(Array(classOf[com.infoobjects.CustomClass1],classOf[com.infoobjects.CustomClass2])

优化垃圾收集

如果有大量短寿命的 RDD,JVM 垃圾收集可能会成为一个挑战。 JVM 需要检查所有对象以找到需要进行垃圾回收的对象。垃圾收集的成本与 GC 需要检查的对象数量成正比。因此,使用更少的对象和使用更少对象的数据结构(更简单的数据结构,如数组)有助于减少垃圾收集的成本。

序列化在这里也很出色,因为一个字节数组只需要一个对象进行垃圾回收。

默认情况下,Spark 使用 60%的执行器内存来缓存 RDD,其余 40%用于常规对象。有时,您可能不需要 60%的 RDD,并且可以减少此限制,以便为对象创建提供更多空间(减少对 GC 的需求)。

如何做到…

您可以通过启动 Spark shell 并设置内存分数来将 RDD 缓存的内存分配设置为 40%:

$ spark-shell --conf spark.storage.memoryFraction=0.4

优化并行级别

优化并行级别对充分利用集群容量非常重要。在 HDFS 的情况下,这意味着分区的数量与InputSplits的数量相同,这与块的数量大致相同。

在本教程中,我们将介绍优化分区数量的不同方法。

如何做到…

在加载文件到 RDD 时指定分区数量,具体步骤如下:

  1. 启动 Spark shell:
$ spark-shell

  1. 使用自定义分区数量作为第二个参数加载 RDD:
scala> sc.textFile("hdfs://localhost:9000/user/hduser/words",10)

另一种方法是通过执行以下步骤更改默认并行度:

  1. 使用新的默认并行度值启动 Spark shell:
$ spark-shell --conf spark.default.parallelism=10

  1. 检查默认并行度的值:
scala> sc.defaultParallelism

您还可以使用 RDD 方法coalesce(numPartitions)来减少分区数量,其中numPartitions是您希望的最终分区数量。如果您希望数据在网络上重新分配,可以调用 RDD 方法repartition(numPartitions),其中numPartitions是您希望的最终分区数量。

了解优化的未来-项目钨

项目钨从 Spark 1.4 版本开始,旨在将 Spark 更接近裸金属。该项目的目标是大幅提高 Spark 应用程序的内存和 CPU 效率,并推动底层硬件的极限。

在分布式系统中,传统智慧一直是始终优化网络 I/O,因为这一直是最稀缺和最瓶颈的资源。这一趋势在过去几年已经改变。在过去 5 年中,网络带宽已经从每秒 1 千兆位增加到每秒 10 千兆位。

在类似的情况下,磁盘带宽已经从 50MB/s 增加到 500MB/s,SSD 的部署也越来越多。另一方面,CPU 时钟速度在 5 年前是~3GHz,现在仍然是一样的。这使得网络不再是瓶颈,而使 CPU 成为分布式处理中的新瓶颈。

另一个增加 CPU 性能负担的趋势是新的压缩数据格式,比如 Parquet。正如我们在本章的前几个示例中看到的,压缩和序列化会导致更多的 CPU 周期。这一趋势也推动了减少 CPU 周期成本的 CPU 优化的需求。

在类似的情况下,让我们看看内存占用。在 Java 中,GC 进行内存管理。GC 在将内存管理从程序员手中拿走并使其透明方面做得很好。为了做到这一点,Java 必须付出很大的开销,这大大增加了内存占用。例如,一个简单的字符串"abcd",在 Java 中应该占用 4 个字节,实际上占用了 48 个字节。

如果我们放弃 GC,像在 C 等低级编程语言中那样手动管理内存会怎样?自 Java 1.7 版本以来,Java 确实提供了一种方法来做到这一点,称为sun.misc.Unsafe。Unsafe 基本上意味着您可以构建长区域的内存而不进行任何安全检查。这是项目钨的第一个特点。

通过利用应用程序语义进行手动内存管理

通过利用应用程序语义进行手动内存管理,如果你不知道自己在做什么,这可能非常危险,但在 Spark 中是一种福音。我们利用数据模式(DataFrames)的知识直接布局内存。这不仅可以摆脱 GC 开销,还可以最小化内存占用。

第二点是将数据存储在 CPU 缓存与内存中。每个人都知道 CPU 缓存很棒,因为从主内存获取数据需要三个周期,而缓存只需要一个周期。这是项目钨的第二个特点。

使用算法和数据结构

算法和数据结构被用来利用内存层次结构,实现更多的缓存感知计算。

CPU 缓存是存储 CPU 下一个需要的数据的小内存池。CPU 有两种类型的缓存:指令缓存和数据缓存。数据缓存按照 L1、L2 和 L3 的层次结构排列:

  • L1 缓存是计算机中最快、最昂贵的缓存。它存储最关键的数据,是 CPU 查找信息的第一个地方。

  • L2 缓存比 L1 稍慢,但仍位于同一处理器芯片上。这是 CPU 查找信息的第二个地方。

  • L3 缓存仍然较慢,但由所有核心共享,例如 DRAM(内存)。

这些可以在以下图表中看到:

使用算法和数据结构

第三点是,Java 在字节码生成方面不太擅长,比如表达式求值。如果这种代码生成是手动完成的,效率会更高。代码生成是 Tungsten 项目的第三个特性。

代码生成

这涉及利用现代编译器和 CPU,以便直接在二进制数据上进行高效操作。目前,Tungsten 项目还处于起步阶段,在 1.5 版本中将有更广泛的支持。