逻辑回归作为分类算法的一种,在互联网领域中的预测、判别中应用的非常广泛,像广告投放中的点击率预估,推荐算法中的模型融合等等。本文简要介绍逻辑回归的算法,以及在MLlib中的实现解析。
逻辑回归其实是一个分类问题,此类问题的模型训练,基本上分3步骤,
第一步要寻找假设预测函数h,构造的假设函数为在线性回归的函数基础上,加上一个Sigmoid函数进行Norm,把函数值输出在0到1的范围内,函数的值有特殊的含义,它表示结果取1的概率,因此对于输入x分类结果为类别1和类别0的概率分别为:
第二步要构造损失函数J,基于最大似然估计推导出,
其中:
第三步求得使最小值时的参数,解决这个问题的做法是随机给定一个初始值,通过迭代,在每次迭代中计算损失函数的下降方向并更新,直到目标函数收敛稳定在最小点。
迭代优化算法就是损失函数的下降方向的计算,有梯度下降、牛顿迭代算法、拟牛顿迭代算法(BFGS算法和L-BFGS算法)
下面对这些优化算法做简单介绍。
(1)梯度下降
对损失函数求偏导,更新过程可以写成:
基于导数,基于梯度的方法优化方法有一个问题,在两次函数中,函数等高线是一个非常扁的椭圆,收敛速度是很慢的,比如在模型训练中有大量的特征,他们的物理意义有时候是不明确的,无法对他们进行归一化处理操作。
梯度下降每次更新都需要遍历所有data,当数据量太大或者一次无法获取全部数据时,这种方法并不可行。
针对梯度下降每一步都是收敛速度慢的问题,引进随机梯度下降,在每一次计算之后便更新参数,而不需要首先将所有的训练集求和,在梯度下降算法还没有完成一次迭代时,随机梯度下降算法便已经走出了很远。但是这样的算法存 在的问题是,不是每一步都是朝着“正确”的方向迈出的。因此算法虽然会逐渐走向全局最小值的位置,但是可能无法站到那个最小值的那一点,而是在最小值点附近徘徊。
(2) 牛顿法(Newton Methods)
牛顿法是在当前参数下,利用二次泰勒展开近似目标函数,然后利用该近似函数来求解目标函数的下降方向。该算法需要计算海森矩阵,因此算法需要花费大量的时间,迭代时间较长。牛顿法要求Hession矩阵是正定的,但在实际问题中,很难保证是正定的。
(3) 拟牛顿法(Quasi-Newton Methods):
使用近似算法,计算海森矩阵,从而降低算法每次迭代的时间,提高算法运行的效率。在拟牛顿算法中较为经典的算法有两种:BFGS算法和L-BFGS算法。BFGS算法是利用原有的所有历史计算结果,近似计算海森矩阵,虽然提高了整个算法的效率,但是由于需要保存大量历史结果,因此该算法受到内存的大小的局限,限制了算法的应用范围;而L-BFGS则是正是针对BFGS消耗内存较大的特点,为了解决空间复杂度的问题,只保存有限的计算结果,大大降低了算法对于内存的依赖。L-BFGS在特征量大时比BFGS实用,可以非常容易用map/reduce实现分布式求解,mapper求部分数据上的梯度,reducer求和并更新参数。它与梯度法实现复杂一点的地方在,它需要保存前几次的模型,才能计算当前迭代的更新值。
在实际应该过程中,为了增强模型的泛化能力,防止我们训练的模型过拟合,特别是对于大量的稀疏特征,模型复杂度比较高,需要进行降维,我们需要保证在训练误差最小化的基础上,通过加上正则化项减小模型复杂度。在逻辑回归中,有L1、L2进行正则化。
损失函数如下:
在损失函数里加入一个正则化项,正则化项就是权重的L1或者L2范数乘以一个,用来控制损失函数和正则化项的比重,直观的理解,首先防止过拟合的目的就是防止最后训练出来的模型过分的依赖某一个特征,当最小化损失函数的时候,某一维度很大,拟合出来的函数值与真实的值之间的差距很小,通过正则化可以使整体的cost变大,从而避免了过分依赖某一维度的结果。当然加正则化的前提是特征值要进行归一化。
L2正则化假设模型参数服从高斯分布,L2正则化函数比L1更光滑,所以更容易计算;L1假设模型参数服从拉普拉斯分布,L1正则化具备产生稀疏解的功能,从而具备feature selection的能力
L1的计算公式
使用了L1 regularization(R(w) = ||w||),利用soft-thresholding方法求解,参数weight更新规则为:
signum是符号函数,它的取值如下:
使用了L2 regularization(R(w) = 1/2 ||w||^2),参数weights更新规则为:
基于逻辑回归的方法有独特的优势,比较简单,计算的代价不高,可以在线的进行训练;缺点是分类的精度可能不高,只支持线性,对非线性的数据支持能力比较弱,需要进行特征处理,包括特征的预处理、正则化L1\L2等等。
在MLlib中,逻辑回归的训练模型是LogisticRegressionModel,包括:
override val weights: Vector//每个特征的权重
override val intercept: Double,//a
val numFeatures: Int,
val numClasses: Int)
根据训练的模型利用如下公式来对新的向量计算进行预测分类
def predict(testData: Vector): Double = {
predictPoint(testData, weights, intercept)
}
override protected def predictPoint(
dataMatrix: Vector, //待预测数据
weightMatrix: Vector, //模型中的weight权重向量
intercept: Double)//代表上面公式中的a
所以逻辑回归的重点是利用标记的数据集合,结合算法把模型训练出来。
我们先分析一下MLlib回归分类的类图和训练的关键方法
如上图所见,MLlib中逻辑回归有两种,一个是梯度下降LogisticRegressionWithSGD,一个是L-BFGSLogisticRegressionWithLBFGS,基类是GeneralizedLinearAlgorithm,进行模型的训练,在训练过程中调用相关优化器进行optimize工作,在对应的优化器Optimizer中会调用正则化Updater进行模型的正则化。
训练的核心方法是GeneralizedLinearAlgorithm.run,我们来看一下,基本步骤是:
1、 对特征进行相关的预处理
StandardScaler能够把feature按照列转换成mean=0,standarddeviation=1的正态分布。
2、 优化,这是核心,利用最优化算法进行迭代训练,得出最优解 optimizer.optimize
3、 创建模型 createModel(weights,intercept)
看一下梯度下降SGD在MLlib中的实现
LogisticRegressionWithSGD
def train(
input: RDD[LabeledPoint],//输入数据,每一个向量是LabelPoint,包括label、features数组
numIterations: Int,//迭代次数
stepSize: Double,//迭代步长
miniBatchFraction: Double,// 每次迭代参与计算的样本比例,默认1.0
initialWeights: Vector//weight向量初值
regParam:regularization//正则化控制参数
): LogisticRegressionModel = {
new LogisticRegressionWithSGD(stepSize, numIterations, 0.0, miniBatchFraction)
.run(input, initialWeights)//调用基类GeneralizedLinearAlgorithm进行模型训练
}
看一下梯度下降优化器的在MLlib中算法实现,可以参见代码中的注解
GradientDescent
def runMiniBatchSGD(
data: RDD[(Double, Vector)],
gradient: Gradient,
updater: Updater,
stepSize: Double,
numIterations: Int,
regParam: Double,
miniBatchFraction: Double,
initialWeights: Vector): (Vector, Array[Double]) = {
//迭代过程中的损失历史
val stochasticLossHistory = new ArrayBuffer[Double](numIterations)
val numExamples = data.count()
// if no data, return initial weights to avoid NaNs
if (numExamples == 0) {
logWarning("GradientDescent.runMiniBatchSGD returning initial weights, no data found")
return (initialWeights, stochasticLossHistory.toArray)
}
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
/**
* 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.
*/
var regVal = updater.compute(
weights, Vectors.dense(new Array[Double](weights.size)), 0, 1, regParam)._2
for (i <- 1 to numIterations) {
//weights向量在下一次迭代计算的过程中,参与计算的Executor都需要,使用了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)
// RDD.treeAggregate会把Job提交任务到Executor中执行,参数seqOp被并行执行,comOp在Job中task成功执行返回后调用
sample是根据miniBatchFraction指定的比例随机采样相应数量的样本
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)
// grad就是对J(Θ)求导的计算结果,loss为J(Θ)的计算结果
调用gradient.compute逐个计算gradient和loss并累加在一起,得到这一轮迭代总的gradient和loss的变化。
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)
})
if (miniBatchSize > 0) {
/**
* NOTE(Xinghao): lossSum is computed using the weights from the previous iteration
* and regVal is the regularization value computed in the previous iteration as well.
*/
stochasticLossHistory.append(lossSum / miniBatchSize + regVal)
//updater.compute更新weights矩阵和regVal(正则化项)。根据本轮迭代中的gradient和loss的变化以及正则化项计算更新之后的weights和regVal。
val update = updater.compute(
weights, Vectors.fromBreeze(gradientSum / miniBatchSize.toDouble), stepSize, i, regParam)
weights = update._1
regVal = update._2
} else {
logWarning(s"Iteration ($i/$numIterations). The size of sampled batch is zero")
}
}
logInfo("GradientDescent.runMiniBatchSGD finished. Last 10 stochastic losses %s".format(
stochasticLossHistory.takeRight(10).mkString(", ")))
(weights, stochasticLossHistory.toArray)
}
L-BFGS优化算法在MLlib中的算法实现,大伙可以看一下LBFS.runLBFS详细实现,对loss function、梯度gradient的计算过程和SGD计算过程差不多,也支持Executor分布式的求解数据梯度,并更新参数,这里就不详细罗列了。
在实际应用的场景中, L-BFGS 比 SGD 更容易收敛,效果更好一些,推荐大家用 L-BFGS 。