Spark源码
大家好,我是一拳就能打爆A柱的A柱猛男
上次也写过一篇分析源码的文章,但是结构很乱,所以我决定重新再来一次。这一次我自认为写的很成功,你要是坚持看下去看不懂,我直播给你锤帕萨特A柱!
1、StreamingLinearRegressionWithSGD源码
我重新去看了DStream和RDD的关系,让我对他们有了更深的理解。RDD作为弹性分布式数据集,**RDD是对分发到各个节点的同一份数据集的不同段的数据的统一抽象,对RDD的操作就是对各个节点相应数据做相同的操作。**而DStream是建立在RDD上的更高级的抽象,**DStream将数据以时间间隔为单位,将源源不断的数据流切分成批式数据,然后对这一批批的数据做处理。因为DStream是对RDD的抽象,所以DStream对数据的操作(transformations、actions)最后都变成RDD对数据的操作。**RDD实现了分布式的计算,DStream实现了流式的计算。
在博客《实时流计算Spark Streaming原理介绍》的2.1也有相同的介绍:
计算流程:Spark Streaming是将流式计算分解成一系列短小的批处理作业。这里的批处理引擎是Spark Core,也就是把Spark Streaming的输入数据按照batch size(如1秒)分成一段一段的数据(Discretized Stream),每一段数据都转换成Spark中的RDD(Resilient Distributed Dataset),然后将Spark Streaming中对DStream的Transformation操作变为针对Spark中对RDD的Transformation操作,将RDD经过操作变成中间结果保存在内存中。整个流式计算根据业务的需求可以对中间的结果进行叠加或者存储到外部设备。下图显示了Spark Streaming的整个流程。
我认为理解这两个对象是关键,理解这两个对象后需要补齐的知识就是:流式算法的继承关系、流式数据如何读取、相关的机器学习算法内容,然后通过打断点看数据流向就可以理解流式机器学习算法了。
1.1 流式算法的继承关系
前段时间我也有翻过它的源码,而且我发现**mllib.regression.StreamingLinearRegressionWithSGD内部用的模型就是mllib.regression.LinearRegressionModel。**也就是说StreamingLinearRegressionWithSGD对一批批数据流训练,得到的模型就是LinearRegressionModel,而在对数据做预测的时候,直接调用模型的参数进行计算即可。
但是仅仅知道这一点还不够,我还需要了解整个继承树的结构,所以我画了下面这张图:
图1 整体集成结构
以StreamingLinearRegressionWithSGD为入口,他的整个继承树结构大概是这样的,StreamingLinearRegressionWithSGD继承了 StreamingLinearAlgorithm,所以在生成StreamingLinearRegressionWithSGD对象的时候可以使用trainOn、predictOn等方法:
val lr: StreamingLinearRegressionWithSGD = new StreamingLinearRegressionWithSGD().setInitialWeights(Vectors.zeros(1))
lr.trainOn(trainLabeledPointDS)
lr.predictOn(vectorsDS)
val resultDS: DStream[(Int, Double)] = lr.predictOnValues(predictVectorDS)
而在StreamingLinearAlgorithm内部定义了两个泛型M,A,分别对应两个抽象类: GeneralizedLinearModel(M) 和GeneralizedLinearAlgorithm(A) ,所以StreamingLinearAlgorithm是重写了两个抽象类或者其子类的方法。
在StreamingLinearRegressionWithSGD中实际使用的类型是GeneralizedLinearModel(M) 和**GeneralizedLinearAlgorithm(A)**的子类: LinearRegressionModel 和 LinearRegressionWithSGD 。所以传到StreamingLinearAlgorithm中使用的是 LinearRegressionModel 和 LinearRegressionWithSGD的方法。
具体控制流程在StreamingLinearAlgorithm中,StreamingLinearAlgorithm通过调用LinearRegressionModel 和 LinearRegressionWithSGD对数据的处理方法实现对数据的处理。:
图2 StreamingLinearRegressionWithSGD和StreamingLinearAlgorithm存在继承关系
而在 LinearRegressionModel 和 LinearRegressionWithSGD这两个类中,LinearRegressionModel 明显是定义模型的操作:保存(save)和预测(predictPoint);但在LinearRegressionWithSGD(继承自 GeneralizedLinearAlgorithm(A) )中更多的是定义权重的计算过程:
图3 linearRegressionWithSGD和GeneralizedLinearAlgorithm存在继承关系
StreamingLinearAlgorithm控制流程,LinearRegressionModel负责控制模型,LinearRegressionWithSGD负责计算权重。
而在 LinearRegressionWithSGD 中又用到 Gradient 和 Updater 的子类 LeastSquaresGradient 和 SimpleUpdater ,这两个显然是去做计算、优化的:
图4 梯度器和更新器的结构
所以整个继承树就大概看到这里就行,了解了每个部分大概的职责,接下来要看代码才知道如何实现。
1.2 训练部分代码(trainOn)
1.2.1 StreamingLinearRegressionWithSGD和StreamingLinearAlgorithm
val lr: StreamingLinearRegressionWithSGD = new StreamingLinearRegressionWithSGD().setInitialWeights(Vectors.zeros(1))
lr.trainOn(trainLabeledPointDS)
点击进入StreamingLinearRegressionWithSGD类内部,发现并没有trainOn方法定义,其内部有两个元素组合进来:
val algorithm = new LinearRegressionWithSGD(stepSize, numIterations, regParam, miniBatchFraction)
protected var model: Option[LinearRegressionModel] = None
而且通过点击trainOn方法,会跳转到StreamingLinearAlgorithm抽象类,这是因为StreamingLinearRegressionWithSGD继承自StreamingLinearAlgorithm:
class StreamingLinearRegressionWithSGD private[mllib] (
private var stepSize: Double,
private var numIterations: Int,
private var regParam: Double,
private var miniBatchFraction: Double)
extends StreamingLinearAlgorithm[LinearRegressionModel, LinearRegressionWithSGD]
既然调用的是StreamingLinearAlgorithm的trainOn,那么数据DStream就会执行trainOn中的逻辑:
def trainOn(data: DStream[LabeledPoint]): Unit = {
if (model.isEmpty) {
throw new IllegalArgumentException("Model must be initialized before starting training.")
}
data.foreachRDD { (rdd, time) =>
if (!rdd.isEmpty) {
model = Some(algorithm.run(rdd, model.get.weights))
logInfo(s"Model updated at time ${time.toString}")
val display = model.get.weights.size match {
case x if x > 100 => model.get.weights.toArray.take(100).mkString("[", ",", "...")
case _ => model.get.weights.toArray.mkString("[", ",", "]")
}
logInfo(s"Current model: weights, ${display}")
}
}
}
最核心的是data.foreachRDD {
这一部分,结合之前对DStream、RDD的重新理解,我知道这是DStream对不同时间段产生的RDDs中的数据做同样的处理。首先要判断RDD是否为空,若不为空则进入model = Some(algorithm.run(rdd, model.get.weights))
,经过这一句话返回的已经是一个model了,model的定义为:protected var model: Option[M]
,M是M <: GeneralizedLinearModel
即GeneralizedLinearModel或者其子类。
1.2.2 LinearRegressionWithSGD和 GeneralizedLinearAlgorithm(A)
前面在继承树可以知道LinearRegressionWithSGD继承自GeneralizedLinearAlgorithm,在LinearRegressionWithSGD显然没有找到run方法,而在GeneralizedLinearAlgorithm里有(图3)。在algorithm.run(rdd, model.get.weights)
中点击进入run方法可以跳转到GeneralizedLinearAlgorithm中的run方法,方法内容太长了,但是我经过仔细阅读发现前面和后面都是对权重向量和截距的处理,其核心只有一句:
val weightsWithIntercept = optimizer.optimize(data, initialWeightsWithIntercept)
这个优化器(optimizer)的定义在GeneralizedLinearAlgorithm的前几行:
def optimizer: Optimizer
好像是个方法,但是又像个变量,而且在其子类LinearRegressionWithSGD中有初始化一个optimizer对象:
//LinearRegressionWithSGD中有初始化optimizer对象
override val optimizer = new GradientDescent(gradient, updater)
.setStepSize(stepSize)
.setNumIterations(numIterations)
.setRegParam(regParam)
.setMiniBatchFraction(miniBatchFraction)
可能是我对scala语法的理解不够深,不是很理解这种写法是什么意思。但是可以猜到是父类optimizer调用了子类的optimizer,而且GradientDescent确实也继承自Optimizer类。
1.2.3 GradientDescent
那么,这里代码实际上又跑到了GradientDescent的optimize方法中(因为GeneralizedLinearAlgorithm调用其子类中定义的GradientDescent的对象):
def optimize(data: RDD[(Double, Vector)], initialWeights: Vector): Vector = {
val (weights, _) = GradientDescent.runMiniBatchSGD(
data,
gradient,
updater,
stepSize,
numIterations,
regParam,
miniBatchFraction,
initialWeights,
convergenceTol)
weights
}
可以看到optimize方法设置了一通参数,然后调用了GradientDescent的同名伴生对象的runMiniBatchSGD方法,这个方法很长,源码估计也比较复杂,总之最后得到了这个结构:(Vector, Array[Double])
的数据。对应val (weights, _) = GradientDescent.runMiniBatchSGD(
的tuple,最后optimize方法只返回了weights。经过这一轮的优化计算,新的权重向量总算是计算出来了。
接下来就是一路返回:
- 1、 返回到
val weightsWithIntercept = optimizer.optimize(data, initialWeightsWithIntercept)
----- GeneralizedLinearAlgorithm的run中 - 2、 经过一段处理,生成新的model:
createModel(weights, intercept)
----- GeneralizedLinearAlgorithm的run中 - 3、 返回一个model,覆盖旧的model:
model = Some(algorithm.run(rdd, model.get.weights))
-------StreamingLinearAlgorithm的trainOn中
最终实现了model的更新!
1.3 预测部分代码(predictOn)
1.3.1 StreamingLinearAlgorithm
同样的,点击predictOn部分的代码:
def predictOn(data: DStream[Vector]): DStream[Double] = {
if (model.isEmpty) {
throw new IllegalArgumentException("Model must be initialized before starting prediction.")
}
data.map{x => model.get.predict(x)}
}
进入到的还是StreamingLinearAlgorithm中,整个方法的代码核心就是model.get.predict(x)
这一句,这就是StreamingLinearAlgorithm负责的整个流程控制。
1.3.2 GeneralizedLinearModel
点击进入predict方法,会跳转进入GeneralizedLinearModel,果然跟之前画继承关系图的时候猜测差不多,GeneralizedLinearModel这一条线就是负责维护模型方面的一些东西的,下面是predict方法的代码:
def predict(testData: Vector): Double = {
predictPoint(testData, weights, intercept)
}
在GeneralizedLinearModel中发现只定义了接口:
protected def predictPoint(dataMatrix: Vector, weightMatrix: Vector, intercept: Double): Double
1.3.3 LinearRegressionModel
根据继承关系,这个方法应该是在LinearRegressionModel中实现:
override protected def predictPoint(
dataMatrix: Vector,
weightMatrix: Vector,
intercept: Double): Double = {
weightMatrix.asBreeze.dot(dataMatrix.asBreeze) + intercept
}
这里的做法是直接通过向量运算计算出结果,因为训练部分已经交给GeneralizedLinearAlgorithm这一条线了,也就是模型权重的更新由它负责,而GeneralizedLinearModel只需要使用模型的结果计算就可以了。
接下来也是结果一路的返回:
- 1、 返回到
data.map{x => model.get.predict(x)}
,Double变成DStream[Double]--------StreamingLinearAlgorithm的predictOn方法中 - 2、 返回DStream[Double]到
lr.predictOn()
接受 ------- 用户得到结果
1.4 线性模型机器学习算法内容
线性模型的目的就是求出权重的值,通过一定方法让损失降到最低使得求出的权重向量可用。计算误差的方法有很多,以RSS举例:
R
S
S
=
∑
i
=
1
n
(
y
i
′
−
y
i
)
2
RSS = \sum_{i=1}^n (y_i' - y_i)^2
RSS=i=1∑n(yi′−yi)2
假设目前有3个点:x1=2,x2=5,x3=8,真实值y1=4,y2=1,y3=9:
R
S
S
=
∑
i
=
1
3
(
f
(
x
i
)
−
y
i
)
2
=
∑
i
=
1
3
(
w
x
i
+
b
−
y
i
)
2
=
(
2
w
+
b
−
4
)
2
+
(
5
w
+
b
−
1
)
2
+
(
8
w
+
b
−
9
)
2
=
93
w
2
+
3
b
2
+
30
w
b
−
170
w
−
28
b
+
98
RSS = \sum_{i=1}^3 (f(x_i) - y_i)^2 \\ = \sum_{i=1}^3 (wx_i + b - y_i)^2 = (2w+b - 4)^2 + (5w+b - 1)^2 + (8w+b - 9)^2 \\ = 93w^2 + 3b^2 + 30wb - 170w - 28b + 98
RSS=i=1∑3(f(xi)−yi)2=i=1∑3(wxi+b−yi)2=(2w+b−4)2+(5w+b−1)2+(8w+b−9)2=93w2+3b2+30wb−170w−28b+98
这就是这三个点形成的RSS,想要得到使RSS最小的w,b,只需要对他们分别求导就可以了:
∂
∂
w
E
(
w
,
b
)
=
0
∂
∂
b
E
(
w
,
b
)
=
0
\frac{\partial}{\partial w}E(w,b) = 0 \\ \frac{\partial}{\partial b}E(w,b) = 0
∂w∂E(w,b)=0∂b∂E(w,b)=0
具体结果如下:
∂
∂
w
E
(
w
,
b
)
=
186
w
+
30
b
−
170
=
0
∂
∂
b
E
(
w
,
b
)
=
6
b
+
30
w
−
28
=
0
\frac{\partial}{\partial w}E(w,b) = 186w+30b - 170 = 0 \\ \frac{\partial}{\partial b}E(w,b) = 6b+30w-28 = 0
∂w∂E(w,b)=186w+30b−170=0∂b∂E(w,b)=6b+30w−28=0
求得w = 5/6 , b = 1/2,故weights向量 = [5/6] ,截距intercept = 0.5。
以上就是求解weights向量和截距intercept的一种方法,可以看出求解线性模型就是求出让损失函数最小化的weights和intercept。
1.4.1 GD和SGD
梯度下降法(GD)
在机器学习或者深度学习中,很多数据是超高维的,所以使用上面这个求导的方法(正规方程求解)并不适合,所以产生了梯度下降法。在《如何理解随机梯度下降(stochastic gradient descent,SGD)?》中Evan的回答说到:
我们知道曲面上方向导数的最大值的方向就代表了梯度的方向,因此我们在做梯度下降的时候,应该是沿着梯度的反方向进行权重的更新,可以有效的找到全局的最优解。
权 重 w i 的 更 新 过 程 可 以 描 述 为 : w j : = w j − α ∂ ∂ w j J ( w ) ∂ ∂ w j J ( w ) = ∂ ∂ w j 1 2 ( h w ( x ) − y ) 2 = 2 ∗ 1 2 ( h w ( x ) − y ) ∗ ∂ ∂ w j ( h w ( x ) − y ) = ( h w ( x ) − y ) ∗ ∂ ∂ w j ( ∑ i = 0 n w i x i − y ) = ( h w ( x ) − y ) x j 权重 w_i的更新过程可以描述为:\\ w_j := w_j - \alpha \frac{\partial}{\partial w_j} J(w) \\ \frac{\partial}{\partial w_j} J(w) = \frac{\partial}{\partial w_j} \frac{1}{2} (h_w(x) - y)^2 = 2*\frac{1}{2}(h_w(x) - y) * \frac{\partial}{\partial w_j}(h_w(x) - y)\\ = (h_w(x) - y) * \frac{\partial}{\partial w_j} (\sum_{i=0}^n w_ix_i - y) = (h_w(x) - y)x_j 权重wi的更新过程可以描述为:wj:=wj−α∂wj∂J(w)∂wj∂J(w)=∂wj∂21(hw(x)−y)2=2∗21(hw(x)−y)∗∂wj∂(hw(x)−y)=(hw(x)−y)∗∂wj∂(i=0∑nwixi−y)=(hw(x)−y)xj
也就是说,在一个多维曲面上,梯度向量就是对函数f的每个变量的导数。所以向量的大小也是梯度的大小。
梯度下降虽然可以计算梯度,但是每次更新都需要用到所有的样本数据,最后求出来的是一个标准梯度,这样做也可以达到最优解,而且因为一次梯度的调整幅度比较大所有收敛的速度比较快。但是在面对样本数较大的情况,更新梯度所需的时间也很多,在做即时性较强的计算的时候不适合。
随机梯度下降(SGD)
同样在Evan的回答中提到:
随机梯度下降:在每次更新时用1个样本,可以看到多了随机两个字,随机也就是说我们用样本中的一个例子来近似我所有的样本,来调整θ,因而随机梯度下降是会带来一定的问题,因为计算得到的并不是准确的一个梯度,**对于最优化问题,凸问题,**虽然不是每次迭代得到的损失函数都向着全局最优方向, 但是大的整体的方向是向全局最优解的,最终的结果往往是在全局最优解附近。但是相比于批量梯度,这样的方法更快,更快收敛,虽然不是全局最优,但很多时候是我们可以接受的,所以这个方法用的也比上面的多。
这段话总结来说就是,随机梯度下降在每次更新用一个样本来调整w以达到全体样本一起优化的目的。SGD的做法也很明显有缺陷:不是每次迭代都可以得到损失函数向着全局最优发展的结果,但是整体的方向是向全局最优解前进的。同时SGD训练起来快很多,所以满足即时性较强的计算。
mini-batch SGD
在GD和SGD之间存在一个折中的方案,因为SGD用的1个样本量太少了,所以使用一个mini-batch的量来做,得到的效果是收敛速度不会很慢,而且收敛的效果也比较好。
1.4.2 训练代码
在1.2.3 GradientDescent中我了解到optimize方法也是使用了mini-batch SGD的方案:
def optimize(data: RDD[(Double, Vector)], initialWeights: Vector): Vector = {
val (weights, _) = GradientDescent.runMiniBatchSGD(
在runMiniBatchSGD方法中有许多参数:
def runMiniBatchSGD(
data: RDD[(Double, Vector)], // 输入数据集
gradient: Gradient, // 用于计算一个样本损失函数的梯度的梯度器
updater: Updater, // 更新器,用于在给定方向做更新
stepSize: Double, // 步长
numIterations: Int, // 迭代次数
regParam: Double, // 正则化参数
miniBatchFraction: Double, // mini-batch的大小
initialWeights: Vector, // 进入训练前的权重向量
convergenceTol: Double // 收敛容忍度,达到容忍度阈值或迭代次数就停止训练
): (Vector, Array[Double]) = {
首先对mini-batch大小和迭代次数做判断:
// convergenceTol should be set with non minibatch settings
if (miniBatchFraction < 1.0 && convergenceTol > 0.0) {
logWarning("Testing against a convergenceTol when using miniBatchFraction " +
"< 1.0 can be unstable because of the stochasticity in sampling.")
}
if (numIterations * miniBatchFraction < 1.0) {
logWarning("Not all examples will be used if numIterations * miniBatchFraction < 1.0: " +
s"numIterations=$numIterations and miniBatchFraction=$miniBatchFraction")
}
设置了一些局部变量:
val stochasticLossHistory = new ArrayBuffer[Double](numIterations)
// 用于记录当前和之前计算出的权重 以计算解向量的差
// Record previous weight and current one to calculate solution vector difference
var previousWeights: Option[Vector] = None
// 上一轮迭代计算出的权重
var currentWeights: Option[Vector] = None
// 本轮迭代计算出的权重
val numExamples = data.count()
// 样本数量
还是做一些保护性的代码,判断无数据的情况,判断mini-batch设计是否合理:
// if no data, return initial weights to avoid NaNs
// 当没有数据进来时,就返回initial weights,这样就不会出现NaN
if (numExamples == 0) {
logWarning("GradientDescent.runMiniBatchSGD returning initial weights, no data found")
return (initialWeights, stochasticLossHistory.toArray)
}
// 限制mini-batch的大小
if (numExamples * miniBatchFraction < 1) {
logWarning("The miniBatchFraction is too small")
}
定义一些局部变量:
// Initialize weights as a column vector
// 将权重做成向量
var weights = Vectors.dense(initialWeights.toArray)
// 得到特征数 也是 权重数
val n = weights.size
初始化regVal局部变量,初始化converged:
/**
* For the first iteration, the regVal will be initialized as sum of weight squares
* if it's L2 updater; for L1 updater, the same logic is followed.
*/
// 对于L2正则化 第一次迭代的时候 regVal会被初始化成权重之和再开方
var regVal = updater.compute(
weights, Vectors.zeros(weights.size), 0, 1, regParam)._2
var converged = false // indicates whether converged based on convergenceTol
这里用到的updater的compute方法,因为前面继承关系知道实际上用的是SimpleUpdater,所以在SimpleUpdater中可以找到:
override def compute(
weightsOld: Vector,
gradient: Vector,
stepSize: Double,
iter: Int,
regParam: Double): (Vector, Double) = {
val thisIterStepSize = stepSize / math.sqrt(iter)
val brzWeights: BV[Double] = weightsOld.asBreeze.toDenseVector
brzAxpy(-thisIterStepSize, gradient.asBreeze, brzWeights)
(Vectors.fromBreeze(brzWeights), 0)
}
这个方法实际上就是计算本次迭代的步长(stepSize / math.sqrt(iter)),然后更新了一次权重,最后又以向量的形式返回(weights,0)。
上面都是做一些保护性的判断,定义一些变量,下面就是真正的训练过程,是一个while循环:
while (!converged && i <= numIterations) {
// dataRDD中获得SparkContext对象,调用broadcast方法,将weights向量广播到集群的所有节点
// bcWeights:Broadcast对象,表示缓存在其他节点的变量
val bcWeights = data.context.broadcast(weights)
// Sample a subset (fraction miniBatchFraction) of the total data
// compute and sum up the subgradients on this subset (this is one map-reduce)
// 从dataRDD中随机采样,参数:(不可以多次采样,采样大小,随机种子)
// 返回:(梯度和向量,损失和,size)
val (gradientSum, lossSum, miniBatchSize) = data.sample(false, miniBatchFraction, 42 + i)
.treeAggregate((BDV.zeros[Double](n), 0.0, 0L))(
seqOp = (c, v) => {
// c: (grad, loss, count), v: (label, features)
val l = gradient.compute(v._2, v._1, bcWeights.value, Vectors.fromBreeze(c._1))
(c._1, c._2 + l, c._3 + 1)
},
combOp = (c1, c2) => {
// c: (grad, loss, count)
(c1._1 += c2._1, c1._2 + c2._2, c1._3 + c2._3)
})
bcWeights.destroy()
if (miniBatchSize > 0) {
/**
* lossSum is computed using the weights from the previous iteration
* and regVal is the regularization value computed in the previous iteration as well.
*/
// lossSum是用上一次迭代的权重计算出总损失值 regVal是上一次迭代的正则化值
// 最终计算出上次的随机损失
stochasticLossHistory += lossSum / miniBatchSize + regVal
// 用SimpleUpdater的compute计算出更新后的权重和正则化值
val update = updater.compute(
weights, Vectors.fromBreeze(gradientSum / miniBatchSize.toDouble),
stepSize, i, regParam)
weights = update._1 // 更新后的权重
regVal = update._2 // 正则化值
previousWeights = currentWeights
currentWeights = Some(weights)
// 判断这次和上一次迭代的差 达到阈值则改变converged 结束训练
if (previousWeights != None && currentWeights != None) {
converged = isConverged(previousWeights.get,
currentWeights.get, convergenceTol)
}
} else {
logWarning(s"Iteration ($i/$numIterations). The size of sampled batch is zero")
}
i += 1
}
在这段代码中上半部分太复杂了,涉及很多没了解的库,这部分库应该就是计算梯度、损失的库,总之经过这一顿操作后达到了更新权重的目的。Updater是为了更新权重服务的,Gradient是为了计算梯度服务的。
1.5 通过断点查看模型训练过程
1.5.1 设计断点位置
重点在模型训练过程,predictOn部分无非就是调用model做向量乘法,所以关注的是trainOn部分。下面我打算分2层打断点。首先在最顶层,只观察模型的变化;然后在最底层,观察整个optimize的过程。
最顶层
在最顶层只需要在trainOn方法中的model更新处打断点就可以了:
def trainOn(data: DStream[LabeledPoint]): Unit = {
if (model.isEmpty) {
throw new IllegalArgumentException("Model must be initialized before starting training.")
}
data.foreachRDD { (rdd, time) =>
if (!rdd.isEmpty) {
model = Some(algorithm.run(rdd, model.get.weights))
在最后一行model处打断点。
最底层
在最底层,首先需要在GeneralizedLinearAlgorithm的run方法中调用优化器处打断点;因为LinearRegressionWithSGD定义了梯度下降对象GradientDescent,所以需要在GradientDescent的optimize打断点;然后还需要在GradientDescent的伴生对象的runMiniBatchSGD方法打断点。
断点分别打在:
StreamingLinearAlgorithm的最后一行:
def trainOn(data: DStream[LabeledPoint]): Unit = {
if (model.isEmpty) {
throw new IllegalArgumentException("Model must be initialized before starting training.")
}
data.foreachRDD { (rdd, time) =>
if (!rdd.isEmpty) {
model = Some(algorithm.run(rdd, model.get.weights))
GeneralizedLinearAlgorithm的run方法下面这一行:
val weightsWithIntercept = optimizer.optimize(data, initialWeightsWithIntercept)
观察weightsWithIntercept的值的变化。
GradientDescent的伴生对象的runMiniBatchSGD方法的下面第一行:
val update = updater.compute(
weights, Vectors.fromBreeze(gradientSum / miniBatchSize.toDouble),
stepSize, i, regParam)
weights = update._1
regVal = update._2
观察update就可以同时观察weights和regVal。
1.5.2 运行程序查看模型
最顶层
在kafkaUtils接收到数据,经过清洗后走到了trainOn,在最顶层里我只打了一个断点在model = Some(algorithm.run(rdd, model.get.weights))
,下面是第一次没开始训练时的model,weight向量和intercept都是0:
在第一轮结束后返回新的model,可以观察到新的model的weights和intercept,其中weights大约为1.0048,而intercept为0:
在第二轮训练结束后,weights更接近1了,intercept还是0:
经过在最顶层,只观察model发现model在每一轮训练结束后都会更新,而且通过模型的ID可以看到这个更新不是单纯的weights和intercept的值的更新,而是整个模型对象的更新。
最底层
同样的,在计划好的地方打上断电后,在model = Some(algorithm.run(rdd, model.get.weights))
可以观察到model,目前是第一轮还没还是训练的状态,初始化的weights和intercept都是0:
本来应该记录在GeneralizedLinearAlgorithm的run方法的 val weightsWithIntercept = optimizer.optimize(data, initialWeightsWithIntercept)
手点的快了,直接跳到了GradientDescent的伴生对象的runMiniBatchSGD方法的while循环里,在这里我主要观察的是update,因为里面装了weights和regVal,在第一轮的迭代结果如下:
前面几次迭代太慢了,我跳过了几次,最后权重向量收敛到1.14左右:
由于while循环终止的条件有两个:
- 指定迭代次数结束
- 当前权重与上一次迭代权重向量之差满足阈值
二者满足其一则停止循环。最终循环停止在0.998917这个数,最终返回到最开始的model处如下图:
总结
整个算法分析下来我真的收获蛮多的,首先我更能理解DStream在Spark流式计算中的地位了,而且对DStream和RDD的关系的理解更深刻。其次就是我以为在以后的日子里都不会用到UML,但实际上这次没有UML我是绝对不可能分析成功的,或者说分析出来的像一坨屎一样(就如上一篇文章)。还有就是在分析源码的时候一定要耐心,在分析前设计多层结构,从外到里一层层的深入我认为是最好的。就好像这次分析,在顶层用UML分析架构,到里层的时候对自己的判断多了很多信心。