Spark介绍
什么是Spark
- Spark 是一个分布式计算平台。所谓分布式,指的是计算节点之间不共享内存,需要通过网络通信的方式交换数据。Spark 最典型的应用方式就是建立在大量廉价的计算节点上,这些节点可以是廉价主机,也可以是虚拟的 Docker Container(Docker 容器)。
Spark架构
- Spark架构图
- Spark 程序由 Manager Node(管理节点)进行调度组织,由 Worker Node(工作节点)进行具体的计算任务执行,最终将结果返回给 Drive Program(驱动程序)。在物理的 Worker Node 上,数据还会分为不同的 partition(数据分片),可以说 partition 是 Spark 的基础数据单元。
- Spark 计算集群能够比传统的单机高性能服务器具备更强大的计算能力,就是由这些成百上千,甚至达到万以上规模的工作节点并行工作带来的。
Spark工作过程
- 先从本地硬盘读取文件 textFile,再从分布式文件系统 HDFS 读取文件 hadoopFile,然后分别对它们进行处理,再把两个文件按照 ID 都 join 起来得到最终的结果。
- 在 Spark 平台上处理这个任务的时候,会将这个任务拆解成一个子任务 DAG(Directed Acyclic Graph,有向无环图),再根据 DAG 决定程序各步骤执行的方法。从有向无环图中我们可以看到,这个 Spark 程序分别从 textFile 和 hadoopFile 读取文件,再经过一系列 map、filter 等操作后进行 join,最终得到了处理结果。
- Spark程序任务的有向无环图如下
- 最关键的过程是需要理解哪些是可以纯并行处理的部分,哪些是必须 shuffle(混洗)和reduce的部分。这里的shuffle指的是所有 partition 的数据必须进行洗牌后才能得到下一步的数据,最典型的操作就是groupBykey 操作和 join 操作。以 join 操作为例,我们必须对 textFile 数据和 hadoopFile 数据做全量的匹配才可以得到 join 后的 Dataframe。而 groupBykey 操作则需要对数据中所有相同的 key 进行合并,也需要全局的 shuffle 才能完成。
- 与之相比,map、filter 等操作仅需要逐条地进行数据处理和转换,不需要进行数据间的操作,因此各 partition 之间可以完全并行处理。
- 在得到最终的计算结果之前,程序需要进行reduce的操作,从各 partition上汇总统计结果,随着partition的数量逐渐减小,reduce操作的并行程度逐渐降低,直到将最终的计算结果汇总到 master 节点上。 可以说,shuffle和reduce操作的触发决定了纯并行处理阶段的边界。
- 被shuffle操作分割的DAG stages如下图
- 重中之重:shuffle 操作需要在不同计算节点之间进行数据交换,非常消耗计算、通信及存储资源,因此shuffle操作是spark程序应该尽量避免的。
- 总结:Stage内部数据高效并行计算,Stage边界处进行消耗资源的 shuffle操作或者最终的reduce操作。
类别型特征处理
One-hot独热编码
- 所有的特征都可以分为两大类。第一类是类别、ID 型特征(类别型特征);第二类是数值型特征,能用数字直接表示的特征就是数值型特征。
- 对于第一类我们需要如何处理呢?这里我们就要用到One-hot 编码(独热编码),它是将类别、ID 型特征转换成数值向量的一种最典型的编码方式。它通过把所有其他维度置为 0,单独将当前类别或者 ID 对应的维度置为 1 的方式生成特征向量。如下图举例:
- 在spark中我们可以通过Spark MLlib库来实现one-hot编码的处理,具体代码如下(所有开源代码可以参考此链接)
def oneHotEncoderExample(samples:DataFrame): Unit ={
//samples样本集中的每一条数据代表一部电影的信息,其中movieId为电影id
val samplesWithIdNumber = samples.withColumn("movieIdNumber", col("movieId").cast(sql.types.IntegerType))
//利用Spark的机器学习库Spark MLlib创建One-hot编码器
val oneHotEncoder = new OneHotEncoderEstimator()
.setInputCols(Array("movieIdNumber"))
.setOutputCols(Array("movieIdVector"))
.setDropLast(false)
//训练One-hot编码器,并完成从id特征到One-hot向量的转换
val oneHotEncoderSamples = oneHotEncoder.fit(samplesWithIdNumber).transform(samplesWithIdNumber)
//打印最终样本的数据结构
oneHotEncoderSamples.printSchema()
//打印10条样本查看结果
oneHotEncoderSamples.show(10)
- 当然往往项目上用户会和多个物品产生交互行为,并且一个物品并不仅仅只有一个标签,这时我们就需要使用multi-hot(多热编码)来实现了,具体代码,可以参考SparrowRecsys项目中的multiHotEncoderExample了。
数值型特征处理
- 对于数值型数据,直接放入特征向量不就好了,为什么还要处理呢?实际上,实际上主要有两点原因特征的尺度、特征的分布。
归一化与分桶
- 特征的尺度问题不难理解,比如在电影推荐中有两个特征,评价次数fr与平均评分fs。评价次数其实是一个数值无上限的特征,在 SparrowRecsys 所用 MovieLens 数据集上,fr 的范围一般在[0,10000]之间。而对于fs,只采用了 5 分为满分的评分,fs的取值范围在[0,5]之间。由于 fr 和 fs 两个特征的尺度差距太大,如果特征的原始数值直接输入推荐模型,就会导致这两个特征对于模型的影响程度有显著的区别。如果模型中未做特殊处理的话,fr 这个特征由于波动范围高出 fs 几个量级,可能会完全掩盖 fs 作用。所以我们需要把两个特征的尺度拉平到一个区域内,通常是[0,1]范围,这就是所谓归一化。
- 归一化虽然能够解决特征取值范围不统一的问题,但无法改变特征值的分布,此时我们就需要使用使用分桶方法来解决特征值分布极不均匀的问题。分桶(Bucketing),就是将样本按照某特征的值从高到低排序,然后按照桶的数量找到分位数,将样本分到各自的桶中,再用桶 ID 作为特征值。
- 归一化与分桶代码如下:
def ratingFeatures(samples:DataFrame): Unit ={
samples.printSchema()
samples.show(10)
//利用打分表ratings计算电影的平均分、被打分次数等数值型特征
val movieFeatures = samples.groupBy(col("movieId"))
.agg(count(lit(1)).as("ratingCount"),
avg(col("rating")).as("avgRating"),
variance(col("rating")).as("ratingVar"))
.withColumn("avgRatingVec", double2vec(col("avgRating")))
movieFeatures.show(10)
//分桶处理,创建QuantileDiscretizer进行分桶,将打分次数这一特征分到100个桶中
val ratingCountDiscretizer = new QuantileDiscretizer()
.setInputCol("ratingCount")
.setOutputCol("ratingCountBucket")
.setNumBuckets(100)
//归一化处理,创建MinMaxScaler进行归一化,将平均得分进行归一化
val ratingScaler = new MinMaxScaler()
.setInputCol("avgRatingVec")
.setOutputCol("scaleAvgRating")
//创建一个pipeline,依次执行两个特征处理过程
val pipelineStage: Array[PipelineStage] = Array(ratingCountDiscretizer, ratingScaler)
val featurePipeline = new Pipeline().setStages(pipelineStage)
val movieProcessedFeatures = featurePipeline.fit(movieFeatures).transform(movieFeatures)
//打印最终结果
movieProcessedFeatures.show(
- 总结:特征处理并没有标准答案,不存在一种特征处理方式一定是好于另一种的。在实践中,我们需要多进行一些尝试,找到那个最能够提升模型效果的一种或一组处理方式,对于多种方式我们可以把他们全部输入到模型中,让模型自己去选择哪种最好。