如何获取Spark MLlib的训练进度
大家好,我是一拳打不死小强的害怕超人
太久不写博客了,因为我最近在找方法展示Spark MLlib中典型批式算法的进度条,今天给大家分析一下如何获取MLlib训练进度条。其实也很简单,但是我中间因为粗心忽略了很多问题,所以记录一下。
先说结论:与SKlearn的做法相同,都是在训练时在外部循环即可。
背景
现在要做的是把Spark MLlib中那些典型批式算法的训练进度表现出来,类似于SKlearn:
for epoch in range(5):
train
print(cost)
但是在SKlearn中模型是可以被重复训练的,而且内部只做一次优化(比如梯度下降),那么每次训练通过epoch就可以展示出进度。
但是在Spark中算法是分布式的,并且模型不能继续训练。那么就有两个问题:要弄清楚算法内部如何操作,要解决模型不能继续训练。
算法内部如何操作(以KMeans为例)
根据GitHub上关于Kmeans的例子:KMeansExample.scala,我们可以进入内部具体的训练代码:
-
通过train进入
-
def train( data: RDD[Vector], k: Int, maxIterations: Int): KMeansModel = { new KMeans().setK(k) .setMaxIterations(maxIterations) .run(data) }
-
-
点击run进入:
-
def run(data: RDD[Vector]): KMeansModel = { val instances = data.map(point => (point, 1.0)) val handlePersistence = data.getStorageLevel == StorageLevel.NONE runWithWeight(instances, handlePersistence, None) }
-
-
点击runWithWeight进入:
-
private[spark] def runWithWeight( instances: RDD[(Vector, Double)], handlePersistence: Boolean, instr: Option[Instrumentation]): KMeansModel = { val norms = instances.map { case (v, _) => Vectors.norm(v, 2.0) } val vectors = instances.zip(norms) .map { case ((v, w), norm) => new VectorWithNorm(v, norm, w) } if (handlePersistence) { vectors.persist(StorageLevel.MEMORY_AND_DISK) } else { // Compute squared norms and cache them. norms.persist(StorageLevel.MEMORY_AND_DISK) } val model = runAlgorithmWithWeight(vectors, instr) if (handlePersistence) { vectors.unpersist() } else { norms.unpersist() } model }
-
-
点击runAlgorithmWeight进入实际的训练代码(源码太长了我就不贴出来了)
我摘取部分关键代码,给大家大概讲解一下:
// 前面的代码都在设置参数
while (iteration < maxIterations && !converged) { // 以Iteration和Converged作为结束条件
val costAccum = sc.doubleAccumulator
val bcCenters = sc.broadcast(centers)
// 这里实现了KMeans++算法
val collected = data.mapPartitions { points =>
val thisCenters = bcCenters.value
val dims = thisCenters.head.vector.size
val sums = Array.fill(thisCenters.length)(Vectors.zeros(dims))
val clusterWeightSum = Array.ofDim[Double](thisCenters.length)
points.foreach { point =>
val (bestCenter, cost) = distanceMeasureInstance.findClosest(thisCenters, point)
costAccum.add(cost * point.weight)
distanceMeasureInstance.updateClusterSum(point, sums(bestCenter))
clusterWeightSum(bestCenter) += point.weight
}
clusterWeightSum.indices.filter(clusterWeightSum(_) > 0)
.map(j => (j, (sums(j), clusterWeightSum(j)))).iterator
}.reduceByKey { (sumweight1, sumweight2) =>
axpy(1.0, sumweight2._1, sumweight1._1)
(sumweight1._1, sumweight1._2 + sumweight2._2)
}.collectAsMap()
if (iteration == 0) {
instr.foreach(_.logNumExamples(costAccum.count))
instr.foreach(_.logSumOfWeights(collected.values.map(_._2).sum))
}
val newCenters = collected.mapValues { case (sum, weightSum) =>
distanceMeasureInstance.centroid(sum, weightSum)
}
bcCenters.destroy()
converged = true
// 这里通过遍历所有的簇心,对比新旧簇心的移动来判断是否收敛
newCenters.foreach { case (j, newCenter) =>
if (converged &&
!distanceMeasureInstance.isCenterConverged(centers(j), newCenter, epsilon)) {
converged = false
}
centers(j) = newCenter
}
cost = costAccum.value
iteration += 1
}
// 后面的代码都在打log以及使用while训练好的簇心来创建新的Model
经过观察算法内部的操作可以了解到,Spark 3.0.1中对KMeans算法的操作使用的是KMeans++算法,并且经过内部的while迭代可以返回出一个新的KMeansModel。同时,在案例中可以知道k和iteration是可以由开发者自定义的参数,通过iteration的设置可以决定内部算法train多少次。
其实在这里我就有一个疑问,KMeans内部训练的代码已经有一个while循环在训练了,那么我还怎么通过for epoch来体现进度?其实这一切的根源都是因为Spark官方提供的数据集kmeans_data.txt过小的问题,由于数据集过小所以导致哪怕一次迭代都会收敛从而导致我后续的各种小案例都没能在外部看到指标的变化。我误以为内部的训练不可能看到。(也是因为我笨=。=)
如何二次训练以及具体的代码
在看完内部训练的代码后,还有一个问题要解决,就是如何将KMeans模型保存下来方便二次训练?我翻阅了源码以及相关的对象,写出了下面这个案例:
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("repeat train kmeans")
val sc = new SparkContext(conf)
val filePath = "path\\kmeans_data.txt"
val data: RDD[String] = sc.textFile(filePath)
val parsedData: RDD[linalg.Vector] = data.map(line => Vectors.dense(line.split(" ").map(_.toDouble))).cache()
val numClusters = 2
val numIterations = 1
// 反复训练
val epochs = 1 to 4
var model: KMeansModel = null
var centers: Array[linalg.Vector] = null
for (epoch <- epochs) {
println(s"第 $epoch 次训练")
if (1.equals(epoch)) {
model = KMeans.train(parsedData, numClusters, numIterations)
centers = model.clusterCenters
} else {
// 重新加载
model = new KMeansModel(centers)
val kmeans: KMeans = new KMeans().setInitialModel(model)
// 继续训练
model = kmeans.run(parsedData)
centers = model.clusterCenters
}
println(s"epoch ${epoch} ,cost = ${model.computeCost(parsedData)}")
}
}
在这个案例中,我通过模型获取簇心clusterCenters,并且在后续二次训练中重新new KMeansModel,通过KMeans对象的setInitialModel来设置模型,从而达到二次训练的结果。其实我也不知道其他算法支不支持这样的做法,接下来我的工作就是要把典型算法是否支持这类操作全都测试一遍。
通过这个案例我得到了Cost,结果如下:
第 1 次训练
epoch 1 ,cost = 0.11999999999999958
第 2 次训练
epoch 2 ,cost = 0.11999999999999958
第 3 次训练
epoch 3 ,cost = 0.11999999999999958
第 4 次训练
epoch 4 ,cost = 0.11999999999999958
这是在Iteration=1的情况下的结果,可以看到一次迭代就收敛了,并且毫无变化。也是因为Spark官方数据量少的原因,我以为是Spark必须将模型训练完成才能返回新模型。理所当然的,在Iteration=20的情况下得到的结果一样,这里就不贴出来了。
使用UCI数据集
我在UCI找到一个数据集Wholesale customers Data Set,整个数据集共有440条数据,8个特征,2个分类。在只对数据清洗部分代码做调整的前提下,Iteration=1,得到的结果如下:
第 1 次训练
epoch 1 ,cost = 1.1586348221546838E11
第 2 次训练
epoch 2 ,cost = 1.1321752887879837E11
第 3 次训练
epoch 3 ,cost = 1.1321752887879837E11
第 4 次训练
epoch 4 ,cost = 1.1321752887879837E11
可能是数据没有归一化之类的问题,Cost很大,但是这不是重点,重点是在1、2次训练时看到Cost的变化。
在Iteration=20的情况下,结果如下:
第 1 次训练
epoch 1 ,cost = 1.1321752887879837E11
第 2 次训练
epoch 2 ,cost = 1.1321752887879837E11
第 3 次训练
epoch 3 ,cost = 1.1321752887879837E11
第 4 次训练
epoch 4 ,cost = 1.1321752887879837E11
全都一样,显然对于少量数据集如果要看到变化就需要调整Iter,但是在实际生产环境,应当设计一个适当的Iter来让用户不至于等太久才能看到指标,也不至于重复的申请释放资源来降低性能。
总结
其实哪怕在分布式环境,还是需要将各个节点训练的结果返回到Driver,而且其内部算法的设计有迭代次数,所以不一定在一轮训练就能很好地拟合数据,所以整体的写法还是跟SKlearn中一样。至于我为什么会出那么垃圾的错误,可能还是因为我笨没有想到那么多,而且太冲动了,在程序设计规划的时候就应当想到各方面条件的影响因素。各位看官不要学我。