Spark大数据分析-MLlib:线性回归实例

目录

  • 分析和准备数据
    • 分析数据分布
    • 分析列余弦相似性
    • 计算协方差矩阵
    • 转换为LabeledPoint
    • 拆分数据
    • 特征缩放和均值归一化
  • 拟合和使用线性回归模型
    • 预测目标值
    • 评估模型的性能
    • 解释模型参数
    • 加载和保存模型
  • 调整算法
    • 找到正确的步长和迭代次数
    • 添加高阶多项式
    • 偏差-方差的权衡和模型复杂度
    • 残差图
    • 使用正则化避免过度拟合
    • k折交叉验证

现在使用MLlib的API实现下载房屋数据集,准备数据,拟合线性回归模型,使用模型预测示例目标值的过程。

分析和准备数据

从在线存储库下载房屋数据集(housing.data)(GitHub),并使用以下代码加载数据:

val housingLines = sc.textFile("first-edition/ch07/housing.data", 6)
val housingVals = housingLines.map(x => Vectors.dense(x.split(",").map(_.trim().toDouble)))

为housingLines RDD使用了6个分区,但是可以根据集群环境选择其他值。现在已将数据解析并作为Vector对象使用。

分析数据分布

要了解数据,可以先对它进行汇总,可以从相应的RowMatrix对象获取该值:

val housingMat = new RowMatrix(housingVals)
val housingStats = housingMat.computeColumnSummaryStatistics()
housingStats.min

或者可以使用Statistics对象达到目的:

import org.apache.spark.mllib.stat.Statistics
val housingStats=Statistics.colStats(housingVals)

然后可以使用获取的MultivariateStatisticalSummary对象来检查矩阵每列中的平均值(mean)、最大值(max)和最小值(min),使用normL1和normL2方法获取每个列的L1范数和L2范数,使用variance方法获得每列的方差。
方差:是数据离散程度的度量,等于所有值与它们的平均值的平方差的均值。
标准差:是方差的二次方根。
协方差:是衡量两个变量相关的程度。

分析列余弦相似性

val housingColSims = housingMat.columnSimilarities()
//UTILITY METHOD FOR PRETTY-PRINTING MATRICES
def printMat(mat:BM[Double]) = {
   print("            ")
   for(j <- 0 to mat.cols-1)
   		print("%-10d".format(j));
   println
   for(i <- 0 to mat.rows-1) { 
	   	print("%-6d".format(i)); 
   		for(j <- 0 to mat.cols-1) 
   			print(" %+9.3f".format(mat(i, j))); 
   println
   }
}
printMat(toBreezeD(housingColSims))
/* SHOULD GIVE:
            0         1         2         3         4         5         6         7         8         9         10        11        12        13
0         +0,000    +0,004    +0,527    +0,052    +0,459    +0,363    +0,482    +0,169    +0,675    +0,563    +0,416    +0,288    +0,544    +0,224
1         +0,000    +0,000    +0,122    +0,078    +0,334    +0,467    +0,211    +0,673    +0,135    +0,297    +0,394    +0,464    +0,200    +0,528
2         +0,000    +0,000    +0,000    +0,256    +0,915    +0,824    +0,916    +0,565    +0,840    +0,931    +0,869    +0,779    +0,897    +0,693
3         +0,000    +0,000    +0,000    +0,000    +0,275    +0,271    +0,275    +0,184    +0,190    +0,230    +0,248    +0,266    +0,204    +0,307
4         +0,000    +0,000    +0,000    +0,000    +0,000    +0,966    +0,962    +0,780    +0,808    +0,957    +0,977    +0,929    +0,912    +0,873
5         +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,909    +0,880    +0,719    +0,906    +0,982    +0,966    +0,832    +0,949
6         +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,672    +0,801    +0,929    +0,930    +0,871    +0,918    +0,803
7         +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,485    +0,710    +0,856    +0,882    +0,644    +0,856
8         +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,917    +0,771    +0,642    +0,806    +0,588
9         +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,939    +0,854    +0,907    +0,789
10        +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,957    +0,887    +0,897
11        +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,799    +0,928
12        +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,670
13        +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000    +0,000

housingColSims 是包含上三角矩阵(上三角矩阵包含仅在其对角线之上的数据)的分布式CoordinateMatrix,其第i行和第j列的值给出了HousingMat矩阵中第i列和第j列之间的相似度度量。housingColSims 矩阵中值的范围是-1到1,值为-1表示两列具有完全相反的方向,值为0表示它们正交,值为1表示它们方向相同。

计算协方差矩阵

用于检查输入集的不同列之间的相似性。可以使用RowMatrix对象来计算

val housingCovar = housingMat.computeCovariance()
printMat(toBreezeM(housingCovar))
            0         1         2         3         4         5         6         7         8         9         10        11        12        13
0        +73,987   -40,216   +23,992    -0,122    +0,420    -1,325   +85,405    -6,877   +46,848  +844,822    +5,399  -302,382   +27,986   -30,719
1        -40,216  +543,937   -85,413    -0,253    -1,396    +5,113  -373,902   +32,629   -63,349 -1236,454   -19,777  +373,721   -68,783   +77,315
2        +23,992   -85,413   +47,064    +0,110    +0,607    -1,888  +124,514   -10,228   +35,550  +833,360    +5,692  -223,580   +29,580   -30,521
3         -0,122    -0,253    +0,110    +0,065    +0,003    +0,016    +0,619    -0,053    -0,016    -1,523    -0,067    +1,131    -0,098    +0,409
4         +0,420    -1,396    +0,607    +0,003    +0,013    -0,025    +2,386    -0,188    +0,617   +13,046    +0,047    -4,021    +0,489    -0,455
5         -1,325    +5,113    -1,888    +0,016    -0,025    +0,494    -4,752    +0,304    -1,284   -34,583    -0,541    +8,215    -3,080    +4,493
6        +85,405  -373,902  +124,514    +0,619    +2,386    -4,752  +792,358   -44,329  +111,771 +2402,690   +15,937  -702,940  +121,078   -97,589
7         -6,877   +32,629   -10,228    -0,053    -0,188    +0,304   -44,329    +4,434    -9,068  -189,665    -1,060   +56,040    -7,473    +4,840
8        +46,848   -63,349   +35,550    -0,016    +0,617    -1,284  +111,771    -9,068   +75,816 +1335,757    +8,761  -353,276   +30,385   -30,561
9       +844,822 -1236,454  +833,360    -1,523   +13,046   -34,583 +2402,690  -189,665 +1335,757 +28404,759  +168,153 -6797,911  +654,715  -726,256
10        +5,399   -19,777    +5,692    -0,067    +0,047    -0,541   +15,937    -1,060    +8,761  +168,153    +4,687   -35,060    +5,783   -10,111
11      -302,382  +373,721  -223,580    +1,131    -4,021    +8,215  -702,940   +56,040  -353,276 -6797,911   -35,060 +8334,752  -238,668  +279,990
12       +27,986   -68,783   +29,580    -0,098    +0,489    -3,080  +121,078    -7,473   +30,385  +654,715    +5,783  -238,668   +50,995   -48,448
13       -30,719   +77,315   -30,521    +0,409    -0,455    +4,493   -97,589    +4,840   -30,561  -726,256   -10,111  +279,990   -48,448   +84,587

方差-协方差矩阵的对角线上的值是每列的方差,其他位置上的值代表两个匹配列的协方差。如果两列的协方差为0,则它们不存在线性关系。若为负值,为负相关,正值则为正相关。

转换为LabeledPoint

检查完数据之后,可以对数据进行线性回归了,首先需要将数据集中的每一个示例放在一个称为LabeledPoint的结构中,这个结构在Spark的机器学习中比较常用,它包含标签Label和特征Vector。创建LabeledPoint时需要将特征和标签分开

import org.apache.spark.mllib.regression.LabeledPoint
val housingData = housingVals.map(x => {
	val a = x.toArray; 
	LabeledPoint(a(a.length-1), Vectors.dense(a.slice(0, a.length-1)))
	})

拆分数据

接着将数据拆分成训练集和验证集

val sets = housingData.randomSplit(Array(0.8, 0.2))
val housingTrain = sets(0)
val housingValid = sets(1)

特征缩放和均值归一化

检查数据的分布时,列之间的数据跨度有很大的差异,比如第一列中的数据0.00632 ~ 88.9762,第五列的数据0.385 ~ 0.871,这样的数据可能会让损失函数在向最低点收敛时遇到困难,或者是其结果会被某一维度的特征异常地影响。
特征缩放意味着将数据范围缩放到可比较的大小。归一化意味着数据平均值大致为0。做法是首先用数据来创建一个缩放器。

import org.apache.spark.mllib.feature.StandardScaler
val scaler = new StandardScaler(true, true).fit(housingTrain.map(x => x.features))

然后将这个缩放器应用到数据上。

val trainScaled = housingTrain.map(x => LabeledPoint(x.label, scaler.transform(x.features)))
val validScaled = housingValid.map(x => LabeledPoint(x.label, scaler.transform(x.features)))

拟合和使用线性回归模型

Spark中的线性回归模型由org.apache.spark.mllib.regression包中的LinearRegressionModel类实现。当使用数据拟合了一个模型对象后,可以使用其对各个Vector示例进行预测,方法是predict。

import org.apache.spark.mllib.regression.LinearRegressionWithSGD
val alg = new LinearRegressionWithSGD()
alg.setIntercept(true)
alg.optimizer.setNumIterations(200)
trainScaled.cache()
validScaled.cache()
val model = alg.run(trainScaled)

预测目标值

模型训练完毕后,就可以使用它来预测验证集中的数据,验证集的数据是有label的,所以预测出来的label可以与已有的label一起使用,进行比较。

val validPredicts = validScaled.map(x => (model.predict(x.features), x.label))
validPredicts.collect()

通过检查可以发现,一些预测与原始标签很近,有的则差很多。为了量化模型的有效性,可以计算均方根误差。

val RMSE = math.sqrt(validPredicts.map{case(p,l) => math.pow(p-l,2)}.mean())

评估模型的性能

Spark提供了RegressionMetrics类来对回归模型的性能进行评估,它返回几个有用的评估指标。

import org.apache.spark.mllib.evaluation.RegressionMetrics
val validMetrics = new RegressionMetrics(validPredicts)
validMetrics.rootMeanSquaredError
validMetrics.meanSquaredError

除了上面这些,还有meanAbsoluteError,r2,explaineddVariance。

解释模型参数

模型训练之后的权重集可以告诉我们单个维度对目标变量的影响。如果特定的权重接近于0,则相应的维度对label的影响就不会很显著。(这要在数据已经进行过缩放的前提上)

println(model.weights.toArray.map(x => x.abs).zipWithIndex.sortBy(_._1).mkString(", "))

加载和保存模型

Spark提供了一种将模型保存到文件系统的方法(Parquet),并且在有需要的时候加载它。

model.save(sc, "hdfs:///path/to/saved/model")

import org.apache.spark.mllib.regression.LinearRegressionModel
val model = LinearRegressionModel.load(sc, "hdfs:///path/to/saved/model")

调整算法

在这里插入图片描述
公式中的参数r是步长参数,有助于稳定梯度下降算法。此外还有迭代次数,如果迭代次数太大,则模型拟合会花费太多时间,如果它太小,则算法可能达不到最小值。

找到正确的步长和迭代次数

找到这两个参数的方法是多长尝试几个组合,并找到最好的结果。

import org.apache.spark.rdd.RDD
def iterateLRwSGD(iterNums:Array[Int], stepSizes:Array[Double], train:RDD[LabeledPoint], test:RDD[LabeledPoint]) = {
  for(numIter <- iterNums; step <- stepSizes)
  {
    val alg = new LinearRegressionWithSGD()
    alg.setIntercept(true).optimizer.setNumIterations(numIter).setStepSize(step)
    val model = alg.run(train)
    val rescaledPredicts = train.map(x => (model.predict(x.features), x.label))
    val validPredicts = test.map(x => (model.predict(x.features), x.label))
    val meanSquared = math.sqrt(rescaledPredicts.map({case(p,l) => math.pow(p-l,2)}).mean())
    val meanSquaredValid = math.sqrt(validPredicts.map({case(p,l) => math.pow(p-l,2)}).mean())
    println("%d, %5.3f -> %.4f, %.4f".format(numIter, step, meanSquared, meanSquaredValid))
    //Uncomment if you wish to see weghts and intercept values:
    //println("%d, %4.2f -> %.4f, %.4f (%s, %f)".format(numIter, step, meanSquared, meanSquaredValid, model.weights, model.intercept))
  }
}
iterateLRwSGD(Array(200, 400, 600), Array(0.05, 0.1, 0.5, 1, 1.5, 2, 3), trainScaled, validScaled)

上面的代码返回训练和验证集的RMSE:

// Our results:
// 200, 0.050 -> 7.5420, 7.4786
// 200, 0.100 -> 5.0437, 5.0910
// 200, 0.500 -> 4.6920, 4.7814
// 200, 1.000 -> 4.6777, 4.7756
// 200, 1.500 -> 4.6751, 4.7761
// 200, 2.000 -> 4.6746, 4.7771
// 200, 3.000 -> 108738480856.3940, 122956877593.1419
// 400, 0.050 -> 5.8161, 5.8254
// 400, 0.100 -> 4.8069, 4.8689
// 400, 0.500 -> 4.6826, 4.7772
// 400, 1.000 -> 4.6753, 4.7760
// 400, 1.500 -> 4.6746, 4.7774
// 400, 2.000 -> 4.6745, 4.7780
// 400, 3.000 -> 25240554554.3096, 30621674955.1730
// 600, 0.050 -> 5.2510, 5.2877
// 600, 0.100 -> 4.7667, 4.8332
// 600, 0.500 -> 4.6792, 4.7759
// 600, 1.000 -> 4.6748, 4.7767
// 600, 1.500 -> 4.6745, 4.7779
// 600, 2.000 -> 4.6745, 4.7783
// 600, 3.000 -> 4977766834.6285, 6036973314.0450

通过上面的结果可以得到步长为1,使用200次会更适合。

添加高阶多项式

通常情况下,数据不遵循简单的线性公式,可能是某种曲线,曲线通常可以使用高阶多项式来描述。
在这里插入图片描述
Spark不提供训练非线性回归模型的方法,但是可以通过将现有特征相乘所获得的附加特征来扩展数据集:

def addHighPols(v:Vector): Vector =
{
  Vectors.dense(v.toArray.flatMap(x => Array(x, x*x)))
}
val housingHP = housingData.map(v => LabeledPoint(v.label, addHighPols(v.features)))

查看housingHP RDD,扩展了附加特征,总共是26个特征

housingHP.first().features.size

接着拆分数据集,缩放数据:

val setsHP = housingHP.randomSplit(Array(0.8, 0.2))
val housingHPTrain = setsHP(0)
val housingHPValid = setsHP(1)
val scalerHP = new StandardScaler(true, true).fit(housingHPTrain.map(x => x.features))
val trainHPScaled = housingHPTrain.map(x => LabeledPoint(x.label, scalerHP.transform(x.features)))
val validHPScaled = housingHPValid.map(x => LabeledPoint(x.label, scalerHP.transform(x.features)))
trainHPScaled.cache()
validHPScaled.cache()

估计最佳步长和迭代次数:

iterateLRwSGD(Array(200, 400), Array(0.4, 0.5, 0.6, 0.7, 0.9, 1.0, 1.1, 1.2, 1.3, 1.5), trainHPScaled, validHPScaled)
// Our results:
// 200, 0.400 -> 4.5423, 4.2002
// 200, 0.500 -> 4.4632, 4.1532
// 200, 0.600 -> 4.3946, 4.1150
// 200, 0.700 -> 4.3349, 4.0841
// 200, 0.900 -> 4.2366, 4.0392
// 200, 1.000 -> 4.1961, 4.0233
// 200, 1.100 -> 4.1605, 4.0108
// 200, 1.200 -> 4.1843, 4.0157
// 200, 1.300 -> 165.8268, 186.6295
// 200, 1.500 -> 182020974.1549, 186781045.5643
// 400, 0.400 -> 4.4117, 4.1243
// 400, 0.500 -> 4.3254, 4.0795
// 400, 0.600 -> 4.2540, 4.0466
// 400, 0.700 -> 4.1947, 4.0228
// 400, 0.900 -> 4.1032, 3.9947
// 400, 1.000 -> 4.0678, 3.9876
// 400, 1.100 -> 4.0378, 3.9836
// 400, 1.200 -> 4.0407, 3.9863
// 400, 1.300 -> 106.0047, 121.4576
// 400, 1.500 -> 162153976.4283, 163000519.6179

从结果可以看出,RMSE在1.3的位置会出现暴涨,在1.1的位置获得最佳结果,在400次迭代中获得最佳RMSE为3.9836。当使用更大的迭代次数时:

iterateLRwSGD(Array(200, 400, 800, 1000, 3000, 6000), Array(1.1), trainHPScaled, validHPScaled)
//Our results:
// 200, 1.100 -> 4.1605, 4.0108
// 400, 1.100 -> 4.0378, 3.9836
// 800, 1.100 -> 3.9438, 3.9901
// 1000, 1.100 -> 3.9199, 3.9982
// 3000, 1.100 -> 3.8332, 4.0633
// 6000, 1.100 -> 3.7915, 4.1138

此时会发现迭代次数越多,测试集的RMSE会增加,为什么会增加?应该选择哪个步长?

偏差-方差的权衡和模型复杂度

测试RMSE在训练RMSE降低的时候增加的情况称为过度拟合。表现是,模型过于适应训练集中的“噪声”,而在分析不具有训练集相同属性的新的数据时变得不太准确。另外还有一个术语:欠拟合,模型过于简单,无法充分捕捉数据的复杂性。
Spark大数据分析-MLlib:线性回归实例_第1张图片
这导致了要进行偏差-方差的权衡。
首先需要知道模型是否具有较高的偏差(欠拟合)或高方差(过拟合)。通常,当模型复杂度和训练集大小的比率比较大时,会发生过度拟合。如果有一个复杂的模型,同时也有一个相对较大的训练集时,发生过度拟合的可能性就会比较低。所以结论是,加入高阶多项式给模型带来更多的复杂性,这样的模型在适合的迭代次数下可以很好的拟合数据(偏差),但是一旦迭代次数过多,就会出现过度拟合的情况(方差)。

残差图

残差图可以判断是否需要继续添加高阶多项式,或者是添加哪些多项式等等问题,残差是目标变量的预测值和实际值之间的差值,残差图的y轴是残差,x轴是预测值。
如果模型拟合得很好,那么残差图整体应该是较为平坦的。如果残差图显示出了整体或局部字母U或者倒U的形状,那意味着非线性模型会更加适合某些维度。
Spark大数据分析-MLlib:线性回归实例_第2张图片
可以发现右边的残差图在形状上比左边的表现得更加平衡。
残差图还可以在其他情况有所帮助,如果图表显示扇入或者扇出形状(也就是残差在图的一端表现出了比另一端更大的方差,称为异方差),除了加上高阶多项式之外,还有一个解决方案是以对数方式转换目标变量,以便于模型预测log(y),而不是y。

使用正则化避免过度拟合

可以使用称为正则化的方法避免过度拟合,从而增加模型的偏差,或者通过惩罚模型参数中的大值来减少方差。正则化为cost function添加了一个额外的元素来惩罚模型中的复杂性。正则化有很多种类型,最常见的是L1和L2正则化。具有L1正则化的线性回归称为Lasso回归,具有L2正则化的线性回归称为Ridge回归。正则化后的cost function如下:
在这里插入图片描述
在spark中,可以通过更改LinearRegressionWithSGD.optimizer对象的regParam和updater属性,或使用LassoWithSGD和RidgeRegressionWithSGD类来手动设置Lasso和Ridge回归。

def iterateRidge(iterNums:Array[Int], stepSizes:Array[Double], regParam:Double, train:RDD[LabeledPoint], test:RDD[LabeledPoint]) = {
  import org.apache.spark.mllib.regression.RidgeRegressionWithSGD
  for(numIter <- iterNums; step <- stepSizes)
  {
    val alg = new RidgeRegressionWithSGD()
    alg.setIntercept(true)
    alg.optimizer.setNumIterations(numIter).setRegParam(regParam).setStepSize(step)
    val model = alg.run(train)
    val rescaledPredicts = train.map(x => (model.predict(x.features), x.label))
    val validPredicts = test.map(x => (model.predict(x.features), x.label))
    val meanSquared = math.sqrt(rescaledPredicts.map({case(p,l) => math.pow(p-l,2)}).mean())
    val meanSquaredValid = math.sqrt(validPredicts.map({case(p,l) => math.pow(p-l,2)}).mean())
    println("%d, %5.3f -> %.4f, %.4f".format(numIter, step, meanSquared, meanSquaredValid))
  }
}
iterateRidge(Array(200, 400, 1000, 3000, 6000, 10000), Array(1.1), 0.01, trainHPScaled, validHPScaled)

// Our results:
// 200, 1.100 -> 4.2354, 4.0095
// 400, 1.100 -> 4.1355, 3.9790
// 1000, 1.100 -> 4.0425, 3.9661
// 3000, 1.100 -> 3.9842, 3.9695
// 6000, 1.100 -> 3.9674, 3.9728
// 10000, 1.100 -> 3.9607, 3.9745
def iterateLasso(iterNums:Array[Int], stepSizes:Array[Double], regParam:Double, train:RDD[LabeledPoint], test:RDD[LabeledPoint]) = {
  import org.apache.spark.mllib.regression.LassoWithSGD
  for(numIter <- iterNums; step <- stepSizes)
  {
    val alg = new LassoWithSGD()
    alg.setIntercept(true).optimizer.setNumIterations(numIter).setStepSize(step).setRegParam(regParam)
    val model = alg.run(train)
    val rescaledPredicts = train.map(x => (model.predict(x.features), x.label))
    val validPredicts = test.map(x => (model.predict(x.features), x.label))
    val meanSquared = math.sqrt(rescaledPredicts.map({case(p,l) => math.pow(p-l,2)}).mean())
    val meanSquaredValid = math.sqrt(validPredicts.map({case(p,l) => math.pow(p-l,2)}).mean())
    println("%d, %5.3f -> %.4f, %.4f".format(numIter, step, meanSquared, meanSquaredValid))
    println("\tweights: "+model.weights)
  }
}
iterateLasso(Array(200, 400, 1000, 3000, 6000, 10000, 15000), Array(1.1), 0.01, trainHPScaled, validHPScaled)
// 200, 1.100 -> 4.1762, 4.0223
// 400, 1.100 -> 4.0632, 3.9964
// 1000, 1.100 -> 3.9496, 3.9987
// 3000, 1.100 -> 3.8636, 4.0362
// 6000, 1.100 -> 3.8239, 4.0705
// 10000, 1.100 -> 3.7985, 4.1014
// 15000, 1.100 -> 3.7806, 4.1304

从结果上看,Ridge给出了比Lasso更低的RMSE。但是不管是Ridge还是Lasso,在迭代次数超过400次时,也发生了过度拟合的现象。

k折交叉验证

k折交叉验证是模型验证的方法,它包括将数据集划分为大致相等大小的k个子集和训练k个模型,计算所有k个模型的平均误差,最后选择一组参数,给出最小平均误差。

你可能感兴趣的:(Spark)