目录
前言
(一)Pi Iteration
总结
(二)KMeans
Spark 例子中的本地实现 :
KMeans的Spark 版本
总结
(三)逻辑回归 LR Logistic regression
Local
SparkLR
SparkHdfsLR
Spark LR 总结
HdfsTest
这段时间会做一系列 Spark 的Example 的分析,主要是对于官方提供的例子,对于RDD, Streaming,ML 等相关的例子进行分析,也包括对于平时使用 Spark 应用的场景进行约简之后的pattern。 想进行这次总结的原因是发现日常开发虽然经常用spark ,但是场景并不多, 对于 Spark 的使用 Pattern 没有一个完整全面的理解。 希望经过这些总结加深一些 Spark 使用层面的印象。
https://github.com/apache/spark/blob/master/examples/src/main/scala/org/apache/spark/examples/LocalPi.scala
var count = 0
for (i <- 1 to 100000) {
val x = random * 2 - 1
val y = random * 2 - 1
if (x*x + y*y <= 1) count += 1
}
println(s"Pi is roughly ${4 * count / 100000.0}")
单机版算法是一个迭代算法, 通过每一次循环迭代 count值, 来估计Pi值
Spark 版本https://github.com/apache/spark/blob/master/examples/src/main/scala/org/apache/spark/examples/SparkPi.scala
/** Computes an approximation to pi */
object SparkPi {
def main(args: Array[String]): Unit = {
val spark = SparkSession
.builder
.appName("Spark Pi")
.getOrCreate()
val slices = if (args.length > 0) args(0).toInt else 2
val n = math.min(100000L * slices, Int.MaxValue).toInt // avoid overflow
val count = spark.sparkContext.parallelize(1 until n, slices).map { i =>
val x = random * 2 - 1
val y = random * 2 - 1
if (x*x + y*y <= 1) 1 else 0
}.reduce(_ + _)
println(s"Pi is roughly ${4.0 * count / (n - 1)}")
spark.stop()
}
}
通过Spark 的RDD, 原来单机中内存变量 i 的100000次迭代,转化成 RDD , RDD 的map转换 每次处理一个 i 值, 将单机版本中的count 累加改成 reduce ,得到单机版本同样的count 结果,在 Driver 中计算最终的Pi值。
这个Pi的例子,关键点是将单机算法中的循环变量编程 RDD ,表示迭代次数,然后迭代之间可以并行计算,最后使用累积的值 0 或 1 , 累计成count 。
这个例子其实并没有太多用到RDD的特性,只是对于计算Pi的这个算法,计算是能够并形成多个任务,然后通过reduce 来聚合计算需要的值。好处就是能利用分布式特点,利用多个机器并行计算,而local 版本只能窜行。
但其实通过把计算count 放到多核上计算也是可以的,例如JDK中 CyclicBarrior 并发控制,通过线程的方式共享count,最后计算是计算partial count 的进程全部完成再计算 Pi = 4.0 * count / (n - 1)} ,这样就不需要构造RDD中的数据
kmeans 是经典的聚类算法, 对于向量数组 data Array[DenseVector[Double]] , 计算 K = 10 个聚类, convergeDist = 0.001 。
https://github.com/apache/spark/blob/master/examples/src/main/scala/org/apache/spark/examples/LocalKMeans.scala
函数 generateData 是通过随机数生成的方法生成 1000个元素,每一个元素是一个向量,D = 10 ,也就是每一个向量有十个元素。
scala 的 Array.tabulate 通过参数和输入的函数构造指定的多维度向量
/** Returns an array containing values of a given function over a range of integer * values starting from 0. * * @param n The number of elements in the array * @param f The function computing element values * @return A traversable consisting of elements `f(0),f(1), ..., f(n - 1)` */ def tabulate[T: ClassTag](n: Int)(f: Int => T): Array[T] = {
def generateData: Array[DenseVector[Double]] = { def generatePoint(i: Int): DenseVector[Double] = { DenseVector.fill(D) {rand.nextDouble * R} } Array.tabulate(N)(generatePoint) }
函数 closestPoint 是计算点 p 距离 中心点集合 最近的中心点是哪一个 ,也就是 p点 和 centers 中所有点计算距离,算出距离最近是哪一个
def closestPoint(p: Vector[Double], centers: HashMap[Int, Vector[Double]]): Int = { var bestIndex = 0 var closest = Double.PositiveInfinity for (i <- 1 to centers.size) { val vCurr = centers(i) val tempDist = squaredDistance(p, vCurr) if (tempDist < closest) { closest = tempDist bestIndex = i } } bestIndex }
到这里也就是重温了一下基本的聚类 Kmeans 了
本地版本的main 函数基本就是这么个处理流程
val data = generateData //生成随机数据1000条 , 每条是10-d 的vector val points = new HashSet[Vector[Double]] //初始化中心点 val kPoints = new HashMap[Int, Vector[Double]] //带lable 的中心点 var tempDist = 1.0 //收敛值 //初始化中心点 while (points.size < K) { points.add(data(rand.nextInt(N))) } //初始化带label的中心点 val iter = points.iterator for (i <- 1 to points.size) { kPoints.put(i, iter.next()) } println(s"Initial centers: $kPoints") //迭代block while(tempDist > convergeDist) { //计算每一个p点 到中心点的归属,形成 (label, (point, count)) 格式 val closest = data.map (p => (closestPoint(p, kPoints), (p, 1))) //形成分组,按照划分的中心点分组, (label , [(label, (point, count))]) val mappings = closest.groupBy[Int] (x => x._1) //新中心点计算预处理,相同集合的点算出新的点 val pointStats = mappings.map { pair => pair._2.reduceLeft [(Int, (Vector[Double], Int))] { case ((id1, (p1, c1)), (id2, (p2, c2))) => (id1, (p1 + p2, c1 + c2)) } } //相同集合的点算出新的点 var newPoints = pointStats.map {mapping => (mapping._1, mapping._2._1 * (1.0 / mapping._2._2))} //计算迭代前后中心点的误差,用于判断是否收敛 tempDist = 0.0 for (mapping <- newPoints) { tempDist += squaredDistance(kPoints(mapping._1), mapping._2) } //更新中心点,如果没有收敛,用于下一轮计算 for (newP <- newPoints) { kPoints.put(newP._1, newP._2) } } println(s"Final centers: $kPoints")
https://github.com/apache/spark/blob/master/examples/src/main/scala/org/apache/spark/examples/SparkKMeans.scala
总体逻辑和local 版本肯定是差不多的,关键是如果利用RDD,如何使用分布式能力
函数 closestPoint 的实现和本地版本一样, 因为这个函数的使用属于一个基本的计算单元,也是在计算分布式下发后在某一个Executor 里边执行,无需进行改造
然后就是main函数了,step by step 来看一下~
参数, file是算法的输入数据, k 是聚类数量, convergeDist 是收敛误差
if (args.length < 3) { System.err.println("Usage: SparkKMeans
") System.exit(1) } showWarning() val spark = SparkSession .builder .appName("SparkKMeans") .getOrCreate()
初始化,数据从文件中以 text方式读取,形成 rdd lines, 通过parseVector 函数解析,这个函数很简单就是分隔符分割成向量, 初始化中心点是通过RDD 的action takeSample 抽样
val lines = spark.read.textFile(args(0)).rdd val data = lines.map(parseVector _).cache() val K = args(1).toInt val convergeDist = args(2).toDouble val kPoints = data.takeSample(withReplacement = false, K, 42) var tempDist = 1.0
接下来是迭代计算, 利用RDD的map 计算每一个point 和 中心点的距离,生成的 rdd closest 通过reducebykey 相当于把相同 中心点集合中的点group 到一起,只是这里不需要每一个具体的 p1 p2 ,而只需要聚合值 p1+p2,所以用一个reduceByKey 生成RDD pointStats,接下来通过map 计算把 intermediate 结果计算生新的中心点 newPoints ,这个RDD 中的元素还是有中心点lable的,通过collectAsMap() action 将结果收集到Driver内存,因为newPoints和points都是 Driver内存变量了,所以和local 方法一样了就~
while(tempDist > convergeDist) { val closest = data.map (p => (closestPoint(p, kPoints), (p, 1))) val pointStats = closest.reduceByKey{case ((p1, c1), (p2, c2)) => (p1 + p2, c1 + c2)} val newPoints = pointStats.map {pair => (pair._1, pair._2._1 * (1.0 / pair._2._2))}.collectAsMap() tempDist = 0.0 for (i <- 0 until K) { tempDist += squaredDistance(kPoints(i), newPoints(i)) } for (newP <- newPoints) { kPoints(newP._1) = newP._2 } println(s"Finished iteration (delta = $tempDist)") }
KMeans 这个例子相对于 Pi ,更适合与Spark 计算,同时这个算法本身属于Data Mining性质的,需要迭代计算,也就是需要在Driver端action控制多次 spark job提交,这里是collectionAsMap 方法 。所以这个例子复杂的地方是没法在一次 Spark Job的执行中,就完成计算,而是每一次循环中的计算都需要前一次 Job的结果。 但是这个算法实现幸运的地方是迭代中没有过长的 依赖链条 depencies chain,因为更新的是本地变量,不是RDD ,所以相当于RDD data为 Root的Tree 树的多个分支的个数就是迭代的次数~
里边具体会涉及一些 DAGScheduler 的内容,这里暂不分析 。
Logistic regression 是机器学习中分类的基础算法,Spark也给了一些实现的例子~
https://github.com/apache/spark/blob/master/examples/src/main/scala/org/apache/spark/examples/LocalLR.scala
下边是一些量的定义,注释很清楚了
val N = 10000 // Number of data points val D = 10 // Number of dimensions val R = 0.7 // Scaling factor val ITERATIONS = 5 val rand = new Random(42)
这个DataPoint 可以认为是样本把, x是特征, y是值
case class DataPoint(x: Vector[Double], y: Double)
首先构造数据集data 是一个 Array[DataPoint] 类型的数组 , w 是输入值,一组特征。
for 循环进行ITERATIONS 次迭代,每一次迭代gradient 首先构造一个空向量,然后在对所有数据data p 进行累计 accumulate 到 gradient 中, w-=gradient 算出本次 迭代的结果
循环通过次数控制,无收敛
def main(args: Array[String]) { showWarning() val data = generateData // Initialize w to a random value val w = DenseVector.fill(D) {2 * rand.nextDouble - 1} println(s"Initial w: $w") for (i <- 1 to ITERATIONS) { println(s"On iteration $i") val gradient = DenseVector.zeros[Double](D) for (p <- data) { val scale = (1 / (1 + math.exp(-p.y * (w.dot(p.x)))) - 1) * p.y gradient += p.x * scale } w -= gradient } println(s"Final w: $w") }
具体LR 算法的含义请自行Google 搜索 。
https://github.com/apache/spark/blob/master/examples/src/main/scala/org/apache/spark/examples/SparkLR.scala
源代码中的一些参数,类型与 local 模式一样,主要看main 中的计算逻辑
数据是从内存构造的,然后通过 SparkContext.parallelize 方法构造 RDD的,因为迭代中多次需要计算 points,所以调用了 RDD.cache 重复使用这个RDD 。
迭代循环中points的处理,map 可以并行计算模型,reduce 将结果收到 Driver 内存中的 gradient 量,然后 w 的更新是在Driver中 。
def main(args: Array[String]) { showWarning() val spark = SparkSession .builder .appName("SparkLR") .getOrCreate() val numSlices = if (args.length > 0) args(0).toInt else 2 val points = spark.sparkContext.parallelize(generateData, numSlices).cache() // Initialize w to a random value val w = DenseVector.fill(D) {2 * rand.nextDouble - 1} println(s"Initial w: $w") for (i <- 1 to ITERATIONS) { println(s"On iteration $i") val gradient = points.map { p => p.x * (1 / (1 + exp(-p.y * (w.dot(p.x)))) - 1) * p.y }.reduce(_ + _) w -= gradient } println(s"Final w: $w") spark.stop() }
这个实现只是改变了输入数据,不是内存构造而是从HDFS 读取并解析,其他并无不同
val inputPath = args(0) val lines = spark.read.textFile(inputPath).rdd
Spark 官方选取的LR 算法比较简单, 计算LR的中间算法完全可以并行化单独计算,并无依赖关系, 就是一个Map Reduce 的过程,只是整个计算过程牵涉迭代, 迭代中用到 Driver 端变量 和 RDD 计算的交互 。但是输入的RDD 是同一个,且更新的内容保存在Driver端变量,所以作业的依赖关系还比较简单 。
https://github.com/apache/spark/blob/master/examples/src/main/scala/org/apache/spark/examples/HdfsTest.scala
比较简单,略
这个没有什么实际含义
sparkContext.parallelize(0 until numMappers, numMappers) 就是内存构建 RDD 的一种方式
flatMap 要注意返回要是一个collection类似的东西 这里用的是Array[(Int, Array[Byte])] ,可以被traverse,有一些语法糖的东西
/** * Usage: GroupByTest [numMappers] [numKVPairs] [KeySize] [numReducers] */ object GroupByTest { def main(args: Array[String]) { val spark = SparkSession .builder .appName("GroupBy Test") .getOrCreate() val numMappers = if (args.length > 0) args(0).toInt else 2 val numKVPairs = if (args.length > 1) args(1).toInt else 1000 val valSize = if (args.length > 2) args(2).toInt else 1000 val numReducers = if (args.length > 3) args(3).toInt else numMappers val pairs1 = spark.sparkContext.parallelize(0 until numMappers, numMappers).flatMap { p => val ranGen = new Random val arr1 = new Array[(Int, Array[Byte])](numKVPairs) for (i <- 0 until numKVPairs) { val byteArr = new Array[Byte](valSize) ranGen.nextBytes(byteArr) arr1(i) = (ranGen.nextInt(Int.MaxValue), byteArr) } arr1 }.cache() // Enforce that everything has been calculated and in cache pairs1.count() println(pairs1.groupByKey(numReducers).count()) spark.stop() } }