spark 2.x ML概念与应用

# spark 2.x ML概念与应用

@(SPARK)[spark]

一、基础

1、核心概念

DataFrame:通用的API数据集,源自Spark SQL。

Transformer:根据原来的DF计算一些新的列,并附在原有DF上。如一个模型,或者是将原有的类作一些变换(如构造向量)后附在原有DF上。

Estimator:就是一个算法,它根据一个DF算出一个Transformer。

Pipeline:就是一系列的Transformer/Estimator组合。

2、Transformer

一般而言,transformer会实现transform()方法,它包括以下2种类型:
(1)特征变换:读取一个DataFrame,然后将这个DF中的列转化为一个向量,并将结果附在原来的DF中。
(2)学习模型:诈取一个DataFrame,使用DF中包括的列或者向量,为每个数据计算一个预测结果或者分类,并将结果附在原有的DF中

3、Estimator

Estimator实现了fit()方法,它使用DataFame来计算出来一个Transformer。比如说,LogisticRegression是一个Estimator,它使用fit()训练出一个LogisticRegressionModel,后者是一个Transformer。

4、Pileline

pipeline就是一系列的Transformer/Estimator。比如一个文本处理包括以下过程

  • Split each document’s text into words.
  • Convert each document’s words into a numerical feature vector.
  • Learn a prediction model using the feature vectors and labels.

5、同一实例

同一个pipeline中不能重复出现2次同一个transformer/estimator实例,但可以是同一个类的2个实例。
Unique Pipeline stages: A Pipeline’s stages should be unique instances. E.g., the same instance myHashingTF should not be inserted into the Pipeline twice since Pipeline stages must have unique IDs. However, different instances myHashingTF1 and myHashingTF2 (both of type HashingTF) can be put into the same Pipeline since different instances will be created with different IDs.

6、保存模型

Often times it is worth it to save a model or a pipeline to disk for later use. In Spark 1.6, a model import/export functionality was added to the Pipeline API. Most basic transformers are supported as well as some of the more basic ML models. Please refer to the algorithm’s API documentation to see if saving and loading is supported.

二、基本数据结构

完整内容请参考官方文档 https://spark.apache.org/docs/latest/mllib-data-types.html

(一)核心概念

1、本地向量 LocalVecotr

MLlib的本地向量主要分为两种,DenseVector和SparseVector,顾名思义,前者是用来保存稠密向量,后者是用来保存稀疏向量,其创建方式主要有一下三种(三种方式均创建了向量(1.0, 0.0, 2.0):

注意,ml package中有同样的类。

import org.apache.spark.ml.linalg.{Vector, Vectors}  

//创建一个稠密向量  
val dv : Vector = Vectors.dense(1.0,0.0,3.0);  
//创建一个稀疏向量(第一种方式)  
val sv1: Vector = Vectors.sparse(3, Array(0,2), Array(1.0,3.0));  
//创建一个稀疏向量(第二种方式)  
val sv2 : Vector = Vectors.sparse(3, Seq((0,1.0),(2,3.0)))  

对于稠密向量:很直观,你要创建什么,就加入什么,其函数声明为Vectors.dense(values : Array[Double])
对于稀疏向量,当采用第一种方式时,3表示此向量的长度,第一个Array(0,2)表示的索引,第二个Array(1.0, 3.0)与前面的Array(0,2)是相互对应的,表示第0个位置的值为1.0,第2个位置的值为3
对于稀疏向量,当采用第二种方式时,3表示此向量的长度,后面的比较直观,Seq里面每一对都是(索引,值)的形式。

tips:由于scala中会默认包含scal.collection.immutalbe.Vector,所以当使用MLlib中的Vector时,需要显式的指明import路径

2、向量标签 LabelVector

向量标签和向量是一起的,简单来说,可以理解为一个向量对应的一个特殊值,这个值的具体内容可以由用户指定,比如你开发了一个算法A,这个算法对每个向量处理之后会得出一个特殊的标记值p,你就可以把p作为向量标签。同样的,更为直观的话,你可以把向量标签作为行索引,从而用多个本地向量构成一个矩阵(当然,MLlib中已经实现了多种矩阵)
其使用代码为:

import org.apache.spark.ml.linalg.Vectors  
import org.apache.spark.ml.feature.LabeledPoint  
val pos = LabeledPoint(1.0, Vectors.dense(1.0, 0.0, 3.0))  

对于pos变量,第一个参数表示这个数据的分类,1.0的具体含义只有你自己知道咯,可以使行索引,可以使特殊值神马的
从文件中直接读入一个LabeledPoint

MLlib提供了一种快捷的方法,可以让用户直接从文件中读取LabeledPoint格式的数据。规定其输入文件的格式为:

label index1:value1 index2:value2.....  

然后通过

val spark = SparkSession.builder.appName("NaiveBayesExample").getOrCreate()
val data = spark.read.format("libsvm").load("/tmp/ljhn1829/aplus/training_data3")

直接读入即可。
关于libsvm格式的详细说明请见下面内容。

3、本地矩阵

既然是算数运算包,肯定少不了矩阵包,先上代码:

import org.apache.spark.mllib.linalg.{Matrix, Matrices}  
val dm : Matrix = Matrices.dense(3,2, Array(1.0,3.0,5.0,2.0,4.0,6.0))  

上面的代码段创建了一个稠密矩阵:

1.0     2.0
3.0     4.0
5.0     6.0

很明显,创建的时候是将原来的矩阵按照列变成一个一维矩阵之后再初始化的。

稀疏矩阵:

val eye = Matrices.sparse(3, 3, Array(0, 1, 2, 3), Array(0, 1, 2), Array(1, 1, 1))

4、分布式矩阵

(ml package未找到类似的类)
MLlib提供了三种分布式矩阵的实现,依据你数据的不同的特点,你可以选择不同类型的数据:
(1)RowMatrix

RowMatrix矩阵只是将矩阵存储起来,要注意的是,此种矩阵不能按照行号访问。(我也不知道为什么这样鸟。。)

import org.apache.spark.mllib.linalg.Vector  
import org.apache.spark.mllib.linalg.distributed.RowMatrix  
val rows: RDD[Vector] = ...//  
val mat: RowMatrix = new RowMatrix(rows)  

val m = mat.numRows()  
val n = mat.numCols()  

RowMatrix要从RDD[Vector]构造,m是mat的行数,n是mat的列
Multivariate summary statistics

顾名思义,这个类里面包含了矩阵中的很多常见信息,怎么使用呢?

import org.apache.spark.mllib.linalg.Matrix  
import org.apache.spark.mllib.linalg.distributed.RowMatrix  
import org.apache.spark.mllib.stat.MultivariateStatisticalSummary  

val mat: RowMatrix = ..  

val summy : MultivariateStatisticalSummary = mat.computeColumnSummaryStatistics()  
println(summy.mean)//平均数  

通过这个类,可以得到平均数,矩阵中非0个数,具体的数据看看帮助文档
(2)IndexedRowMatrix

IndexedRowMatrix矩阵和RowMatrix矩阵的不同之处在于,你可以通过索引值来访问每一行。其他的,没啥区别。。
(3)CoordinateMatrix

当你的数据特别稀疏的时候怎么办?采用这种矩阵吧。先上代码:

import org.apache.spark.mllib.linalg.distributed.{CoordinatedMatrix, MatrixEntry}  

val entries : RDD[MatrixEntry] = ..  
val mat: CoordinateMatrix = new CoordinateMatrix(entries)  

CoordinateMatrix矩阵中的存储形式是(row,col,value),就是原始的最稀疏的方式,所以如果矩阵比较稠密,别用这种数据格式

(二)libsvm数据格式

首先介绍一下 libSVM的数据格式

Label 1:value 2:value …
Label:是类别的标识,比如上节train.model中提到的1 -1,你可以自己随意定,比如-10,0,15。当然,如果是回归,这是目标值,就要实事求是了。
Value:就是要训练的数据,从分类的角度来说就是特征值,数据之间用空格隔开
比如:
-15 1:0.708 2:1056 3:-0.3333
需要注意的是,如果特征值为0,则这列数据可以不写,因此特征冒号前面的(姑且称做序号)可以不连续。如:
-15 1:0.708 3:-0.3333
表明第2个特征值为0,从编程的角度来说,这样做可以减少内存的使用,并提高做矩阵内积时的运算速度。我们平时在matlab中产生的数据都是没有序号的常规矩阵,所以为了方便最好编一个程序进行转化。

spark提供了方便的工具类来加载这些数据

val spark = SparkSession.builder.appName("NaiveBayesExample").getOrCreate()
val data = spark.read.format("libsvm").load("/tmp/ljhn1829/aplus/training_data3")

3、fit()/transform()方法的参数DF包含哪些列

  • 基于DataFrame,借助于抽象,将模型抽象为三个基本类,estimators(实现fit方法), transformers(实现transform方法), pipelines
  • 一个正常的模型应该同时实现 fit 和 transform 两个方法
  • transform 将生成一个新的DataFrame,包含了预测的结果
  • fit 的DataFrame需要包含两列 featuresCol 和 labelCol 默认名字为 label
    *
    transform 之前的DataFrame需要包含一列 featuresCol,默认名字为features,输出三列(依赖于参数),三列有默认名字,都可以通过setter函数进行设置。

    • predictedCol 预测的标签,默认名字为 prediction
    • rawPredictedCol 预测的裸数据?向量?逻辑回归是wx貌似,默认名字为 rawPrediction
    • probabilityCol 预测的概率,默认名字为 probability

三、朴素贝叶斯与逻辑回归示例

(一)准备学习数据

1、数据格式

我们使用上面介绍的libsvm数据格式作为训练数据:

0 31607:17
0 111905:36
0 109:3 506:41 1509:1 2106:4 5309:1 7209:5 8406:1 27108:1 27709:1 30209:8 36109:20 41408:1 42309:1 46509:1 47709:5 57809:1 58009:1 58709:2 112109:4 123305:48 142509:1
0 407:14 2905:2 5209:2 6509:2 6909:2 14509:2 18507:10
0 604:3 3505:9 6401:3 6503:2 6505:3 7809:8 10509:3 12109:3 15207:19 31607:19
0 19109:7 29705:4 123305:32
0 15309:1 43005:1 108509:1
1 604:1 6401:1 6503:1 15207:4 31607:40
0 1807:19
0 301:14 501:1 1502:14 2507:12 123305:4
0 607:14 19109:460 123305:448
0 5406:14 7209:4 10509:3 19109:6 24706:10 26106:4 31409:1 123305:48 128209:1
1 1606:1 2306:3 3905:19 4408:3 4506:8 8707:3 19109:50 24809:1 26509:2 27709:2 56509:8 122705:62 123305:31 124005:2
0 15309:1 19109:1 43005:1 108509:1 124005:1
0 107:79 507:2 607:1 37907:1 121409:5
1 2905:12 19109:17 31607:16
0 701:6 1101:2 2301:3 4701:2 10601:1 20201:3 32707:20
0 1401:10 14202:3 14402:3 18902:2 19009:3 19109:3 38002:3
0 31607:15
0 15309:4 19109:99 108509:4
0 708:7 1908:4 2008:4 19109:4 123305:4 123905:23

2、数据排序

正如上面如言,libsvm需要将属性排序,提供一个方法作转换,如果本身已经是排序的,则不需要

//将ip标签按标签id进行排序
import java.util
import scala.collection.JavaConversions._
val SEPARATOR = " ";
def sortLabel(line:String):String={
  val contents = line.split(SEPARATOR);
  val label = contents(0);
  val map = new util.TreeMap[Integer,Integer]()
  for(i <- 1 to contents.length-1){
    println(contents(i))
    map.put(contents(i).split(":")(0).toInt,contents(i).split(":")(1).toInt)
  }
  var returnLine = label;
  for(key<-map.keySet()){
    returnLine +=( SEPARATOR + key + ":" + map.get(key))
  }

  return returnLine;
}

sc.textFile("/tmp/ljhn1829/aplus/training_data").map(line=>sortLabel(line)).coalesce(1,true).saveAsTextFile("/tmp/ljhn1829/aplus/training_data2")

(二)朴素贝叶斯算法

1、训练模型

import org.apache.spark.ml.classification.NaiveBayes
import org.apache.spark.ml.evaluation.BinaryClassificationEvaluator
import org.apache.spark.ml.evaluation.MulticlassClassificationEvaluator
import org.apache.spark.sql.SparkSession

val spark = SparkSession.builder.appName("NaiveBayesExample").getOrCreate()
val data = spark.read.format("libsvm").option("numFeatures","999999").load("/tmp/ljhn1829/aplus/training_data2")//training_data2:正样本22W,负样本2000万。training_data3:正负样本各22W.
val Array(trainingData, testData) = data.randomSplit(Array(0.99, 0.01), seed = 1234L)
//val model = new NaiveBayes().fit(trainingData)
val model = new NaiveBayes().setThresholds(Array(10.0,1.0)).fit(trainingData)
val predictions = model.transform(testData)
predictions.show()

2、模型评估方法1–使用spark API -ROC曲线

可以用上述所列的testData作为检验数据,也可以使用全量数据作为检验数据:

val data2 = spark.read.format("libsvm").load("/tmp/ljhn1829/aplus/training_data2")
val Array(trainingData2, testData2) = data2.randomSplit(Array(0.01, 0.99), seed = 1234L)
val predictions = model.transform(testData2)
predictions.show()

(1)将其作为多分类结果进行评估,可计算f1、精度、召回率、准确度(见MulticlassClassificationEvaluator源代码)

val multiclassClassificationEvaluator = new MulticlassClassificationEvaluator()
def printlnMetric(metricName: String): Unit = {
  println(metricName + " = " + multiclassClassificationEvaluator.setMetricName(metricName).evaluate(predictions))
}

printlnMetric("f1")
printlnMetric("weightedPrecision")
printlnMetric("weightedRecall")
printlnMetric("accuracy")

结果:

scala> printlnMetric("f1")
f1 = 0.8942599227319046

scala> printlnMetric("weightedPrecision")
weightedPrecision = 0.9766525369524176

scala> printlnMetric("weightedRecall")
weightedRecall = 0.8280687304878339

scala> printlnMetric("accuracy")
accuracy = 0.8280687304878339

(2)将其作为二分类结果进行评估,可计算areaUnderROC、areaUnderPR(见BinaryClassificationEvaluator源代码)

val binaryClassificationEvaluator = new BinaryClassificationEvaluator()
def printlnMetric(metricName: String): Unit = {
  println(metricName + " = " + binaryClassificationEvaluator.setMetricName(metricName).evaluate(predictions))
}

printlnMetric("areaUnderROC")
printlnMetric("areaUnderPR")

3、模型评估方法2—-自己计算

不使用spark API,自己来计算
(1)检验样本的总数

 predictions.count
res49: Long = 5245125

(2)检验样本中分类正确的数量

 predictions.filter($"label" === $"prediction").count
res55: Long = 4343324

(3)样本中分类为1的样本数量

predictions.filter($"label" === 1).count
res60: Long = 68726

(4)样本中分类为0的样本数量

predictions.filter($"label" === 0).count
res61: Long = 5176399

(5)分类正确且分类为1的样本数量 TP 21931 35898 119454 146188

 predictions.filter($"label" === $"prediction").filter($"label"===1).count

(6)分类正确且分类为0的样本数量 TN 4321393 44030 11478102 9280601

predictions.filter($"label" === $"prediction").filter($"label"===0).count

(7)分类错误且分类为1的样本数量 FP 855006 21747 5615212 7812713

 predictions.filter($"label" !== $"prediction").filter($"prediction"===1).count

(8)分类错误且分类为0的样本数量 FN 46795 32574 80404.0

 predictions.filter($"label" !== $"prediction").filter($"prediction"===0).count

使用均衡训练数据,未设阈值
准确率:(11478102+119454)/(11478102+119454+5615212+107138.0)=0.6696084840183313
召回率:TP/(P) = 119454/(119454+107138.0) = 0.5271765993503742
精度:TP/(TP+FP)=21931/(21931+855006)=0.025008638020747216
准确率60%,如回率58%,精度62%

使用均衡数据,设置10:1阈值
准确率:(146188+9280601)/(9280601+146188+7812713+80404.0)=0.5442748361336373
召回率:TP/(P) = 146188/(146188+80404.0) = 0.6451595819799464
精度:TP/(TP+FP)=146188/(146188+7812713)= 0.01836786259811499
准确率54.4%,召回率64.5%,精度18.3%

4、模型调优

(1)new NaiveBayes().setThresholds(Array(100.0,1.0))
为每个分类设置一个阈值,参数的长度必须和类的个数相等。最终的分类结果会是p/t最大的那个分类,其中p是通过Bayes计算出来的结果,t是阈值。
这对于训练样本严重不均衡的情况尤其重要,比如分类1只有20W数据,而分类0有2000万数据,此时应用new NaiveBayes().setThresholds(Array(100.0,1.0))
Param for Thresholds in multi-class classification to adjust the probability of predicting each class. Array must have length equal to the number of classes, with values > 0 excepting that at most one value may be 0. The class with largest value p/t is predicted, where p is the original probability of that class and t is the class’s threshold.

(三)逻辑回归算法示例

1、训练模型

import org.apache.spark.ml.classification.LogisticRegression

val data =spark.read.format("libsvm").option("numFeatures","999999").load("/tmp/ljhn1829/aplus/training_data3")
val Array(trainingData, testData) = data.randomSplit(Array(0.99, 0.01), seed = 1234L)

val lr = new LogisticRegression()
//.setThreshold(0.4152415581588802).setMaxIter(10).setRegParam(0.3).setElasticNetParam(0.8)
val lrModel = lr.fit(trainingData)
println(s"Coefficients: ${lrModel.coefficients} Intercept: ${lrModel.intercept}")

val predictions = lrModel.transform(testData)
predictions.show()

2、评估模型方法1–使用API

import org.apache.spark.ml.classification.BinaryLogisticRegressionSummary
import org.apache.spark.sql.functions;

//获得回归模型训练的Summary
val trainingSummary = lrModel.summary

// Obtain the loss per iteration.
//每次迭代的损失,一般会逐渐减小
val objectiveHistory = trainingSummary.objectiveHistory
for (lossPerIteration<-objectiveHistory) {
  println(lossPerIteration);
}

// Obtain the metrics useful to judge performance on test data.
// We cast the summary to a BinaryLogisticRegressionSummary since the problem is a binary
// classification problem.
//强制类型转换为二类LR的Summary,然后就可以用混淆矩阵,ROC等评估方法了。Spark2.0还无法针对多类
val binarySummary = trainingSummary.asInstanceOf[BinaryLogisticRegressionSummary]
// Obtain the receiver-operating characteristic as a dataframe and areaUnderROC.
val roc = binarySummary.roc //获得ROC
roc.show //显示ROC数据表,可以用这个数据自己画ROC曲线
roc.select("FPR").show();
System.out.println(binarySummary.areaUnderROC);//AUC

3、模型评估方法2–使用spark API

可以用上述所列的testData作为检验数据,也可以使用全量数据作为检验数据:

val data2 = spark.read.format("libsvm").load("/tmp/ljhn1829/aplus/training_data2")
val Array(trainingData2, testData2) = data2.randomSplit(Array(0.01, 0.99), seed = 1234L)
val predictions = lrModel.transform(testData2)
predictions.show()

(1)将其作为多分类结果进行评估,可计算f1、精度、召回率、准确度(见MulticlassClassificationEvaluator源代码)

val multiclassClassificationEvaluator = new MulticlassClassificationEvaluator()
def printlnMetric(metricName: String): Unit = {
  println(metricName + " = " + multiclassClassificationEvaluator.setMetricName(metricName).evaluate(predictions))
}

printlnMetric("f1")
printlnMetric("weightedPrecision")
printlnMetric("weightedRecall")
printlnMetric("accuracy")

结果:

scala> printlnMetric("f1")
f1 = 0.8942599227319046

scala> printlnMetric("weightedPrecision")
weightedPrecision = 0.9766525369524176

scala> printlnMetric("weightedRecall")
weightedRecall = 0.8280687304878339

scala> printlnMetric("accuracy")
accuracy = 0.8280687304878339

(2)将其作为二分类结果进行评估,可计算areaUnderROC、areaUnderPR(见BinaryClassificationEvaluator源代码)

val binaryClassificationEvaluator = new BinaryClassificationEvaluator()
def printlnMetric(metricName: String): Unit = {
  println(metricName + " = " + binaryClassificationEvaluator.setMetricName(metricName).evaluate(predictions))
}

printlnMetric("areaUnderROC")
printlnMetric("areaUnderPR")

4、模型评估方法3:自己写

(5)分类正确且分类为1的样本数量 TP 146188

predictions.filter($"label" === $"prediction").filter($"label"===1).count

(6)分类正确且分类为0的样本数量 TN 9280601

predictions.filter($"label" === $"prediction").filter($"label"===0).count

(7)分类错误且分类为1的样本数量 FP 7812713

predictions.filter($"label" !== $"prediction").filter($"prediction"===1).count

(8)分类错误且分类为0的样本数量 FN 80404

predictions.filter($"label" !== $"prediction").filter($"prediction"===0).count

准确率:( 146188+ 9280601)/( 146188+ 9280601+7812713+80404.0)=0.5442748361336373
召回率:TP/(TP+FN) =  146188/( 146188+80404) =0.6451595819799464
精度:TP/(TP+FP)= 146188/( 146188+7812713.0) =0.01836786259811499

设置最优threshole之后:

准确率:( 213151+ 2803234)/( 213151+ 2803234+14290080+13441.0)=0.1741571230236469
召回率:TP/(TP+FN) =  213151/( 213151+13441) = 0.9406819305182884
精度:TP/(TP+FP)= 213151/( 213151+14290080.0) =0.014696794114359759
准确率54.4%,召回率64.5%,精度18.3%

5、模型调优

这些参数用于设置列名:
setPredictionCol setLabelCol setProbabilityCol setFeaturesCol setWeightCol setRawPredictionCol
以下这些参数用于设置各种学习参数:

(1)setThreshold/setThresholds

setThreshold设置了阈值,大于这个阈值则分类为1,小于则分类为0。转为值为0.5
setThresholds用于多分类的情况。

以下示例如何找到最优的threshold。

// Get the threshold corresponding to the maximum F-Measure and rerun LogisticRegression with
// this selected threshold.
//不同的阈值,计算不同的F1,然后通过最大的F1找出并重设模型的最佳阈值。
val fMeasure = binarySummary.fMeasureByThreshold
val maxFMeasure = fMeasure.select(functions.max("F-Measure")).head().getDouble(0);//获得最大的F1值
val bestThreshold = fMeasure.where(fMeasure.col("F-Measure").equalTo(maxFMeasure)).select("threshold").head().getDouble(0);//找出最大F1值对应的阈值(最佳阈值)
lrModel.setThreshold(bestThreshold);//并将模型的Threshold设置为选择出来的最佳分类阈值

(2)setRegParam:正则化参数
默认值为0.
正则化参数主要是为了解决过度拟合的问题,详细理论请参考《逻辑回归原理与实现》

lrModel.getRegParam  // 正则化参数>=0

当设置正则化参数为0~3时,计算检验样本,分别得到召回率与精度如下:

0.0  0.9205683447008687  0.9527194528239897
0.05 0.9239064559263499  0.958818361519877
0.1  0.9223865090282922  0.9569734714653572
0.15 0.9201643081836635  0.9546774724781172
0.2  0.9183653836903926  0.952554380363201
0.3  0.9148060143721561  0.9484744816030162
0.5  0.9084953487700936  0.9416216324007418
3.0  0.8789044838433493  0.9037430510218213

这组数据中可以看出当正则化参数为0.05时,分类效果最优。。但对于其它数据可能就会有过拟合的问题了,所以要视样本情况而调整正则化参数。

正则化参数过小,则可能过拟合。过大则可能欠拟合。

(3) setMaxIter
最大的迭代次数,当达到这个次数时,不管是否已经收敛到最小误差,均会结束训练。默认值为100。

(4) setTol
算法的收敛阈值,当小于这个值时,结束迭代计算,默认值为1.0E-6。

(5)setStandardization
是否对特征值进行标准化,默认为true。

(6)setElasticNetParam
默认值为0.0,这是一个L2惩罚。用于防止过拟合的另一种方式,理论详见《L0/L1/L2》
对于α= 0,惩罚是L2惩罚。 对于alpha = 1,它是一个L1惩罚。 对于0 <α<1,惩罚是L1和L2的组合。

(7)setFitIntercept
Param for whether to fit an intercept term.

举个简单情况 y= w1 * x + w0,这里w0就是一个截距,调节直线不穿过原点。从这个角度想想,w0确实不应该正则化,值是多少就多少。
如果设置为false,则intercept=0.0,否则为实际值。

(8) setFamily
这是2.1才引入的参数,可设置二项式还是多项式模型。

(四)使用模型预测数据

1、基本使用

关键在于如何根据一个文件创建一个RDD,然后再转成DataFrame。

import org.apache.spark.ml.linalg.Vectors
import org.apache.spark.ml.linalg.Vector
import scala.collection.mutable

   def lineToVector(line:String ):Vector={
    val seq = new mutable.Queue[(Int,Double)]
    val content = line.split(" ");
    for( s <- content){
      val index = s.split(":")(0).toInt -1 //notice:看下面的大坑
      val value = s.split(":")(1).toDouble
       seq += ((index,value))
    }
    return Vectors.sparse(999999, seq)
  }

 val df = sc.sequenceFile[org.apache.hadoop.io.LongWritable, org.apache.hadoop.io.Text]("/data/gamein/gameall_sdc/wh/gameall.db/edt_udid_label_format/ds=20170312/001006_0").map(line=>line._2).map(line => (line.toString.split("\t")(0),lineToVector(line.toString.split("\t")(1)))).toDF("udid", "features")

 val predictionResult = lrModel.transform(df)
 predictionResult.show()

2、保存计算结果

predictionResult.select($"udid",$"probability").rdd.saveAsTextFile("/tmp/ljhn1829/test")

3、保存模型与重新加载模型

model.save("/tmp/ljhn1829/model")
val model2 = NaiveBayesModel.load("/tmp/ljhn1829/model")

4、关于Vector的维度数量的说明

用于训练模型时的维度数据必须与预测时使用的维度数量相同

如上面的:

val data = spark.read.format("libsvm").option("numFeatures","9999999").load("/tmp/ljhn1829/aplus/training_data3")

如果不指定option的话,则维度数量会以实际出现值的列维度数量相同

return Vectors.sparse(9999999, seq)

如果上述二者不相同,则会出现以下异常:

Caused by: java.lang.IllegalArgumentException: requirement failed: You may not write an element to index 804201 because the declared size of your vector is 144109
 at scala.Predef$.require(Predef.scala:224)
 at org.apache.spark.ml.linalg.Vectors$.sparse(Vectors.scala:219)
 at lineToVector(<console>:55)
 at $anonfun$4.apply(<console>:50)
 at $anonfun$4.apply(<console>:50)
 at scala.collection.Iterator$$anon$11.next(Iterator.scala:409)
	 at scala.collection.Iterator$$anon$11.next(Iterator.scala:409)
 at scala.collection.Iterator$$anon$11.next(Iterator.scala:409)
	 at org.apache.spark.sql.catalyst.expressions.GeneratedClass$GeneratedIterator.processNext(generated.java:84)
	 at org.apache.spark.sql.execution.BufferedRowIterator.hasNext(BufferedRowIterator.java:43)
	 at org.apache.spark.sql.execution.WholeStageCodegenExec$$anonfun$8$$anon$1.hasNext(WholeStageCodegenExec.scala:370)
 at org.apache.spark.sql.execution.SparkPlan$$anonfun$4.apply(SparkPlan.scala:246)
	 at org.apache.spark.sql.execution.SparkPlan$$anonfun$4.apply(SparkPlan.scala:240)
 at org.apache.spark.rdd.RDD$$anonfun$mapPartitionsInternal$1$$anonfun$apply$24.apply(RDD.scala:803)
 at org.apache.spark.rdd.RDD$$anonfun$mapPartitionsInternal$1$$anonfun$apply$24.apply(RDD.scala:803)

So I change    

    return Vectors.sparse(144109, seq)

to 

    return Vectors.sparse(804202, seq)

Another error occurs:

    Caused by: java.lang.IllegalArgumentException: requirement failed: The columns of A don't match the number of elements of x. A: 144109, x: 804202
      at scala.Predef$.require(Predef.scala:224)
      at org.apache.spark.ml.linalg.BLAS$.gemv(BLAS.scala:521)
      at org.apache.spark.ml.linalg.Matrix$class.multiply(Matrices.scala:110)
      at org.apache.spark.ml.linalg.DenseMatrix.multiply(Matrices.scala:176)

5、 一个超级大坑:索引从0开始还是从1开始

libsvm格式中,索引是从1开始的,但spark load svm的时候会将它改为从0开始,即将文件中所有的索引ID-1.

另一方面, 自己读取文件中的index时,一般使用的就是维度的index!!!
此时,注意将读取到的index-1,如上面的代码:

      val index = s.split(":")(0).toInt -1 //notice:看下面的大坑

(五)各个算法间的结果对比

准备需要用于测试的数据:

SET hive.exec.compress.output=false;
create external table ljh_g60_aplus_compare_data (
 udid string,
  ip string,
   label string
)
row format delimited fields terminated by '\t'
STORED AS textfile
location '/tmp/ljhn1829/compare/ljh_g60_aplus_compare_data';

insert overwrite table  ljh_g60_aplus_compare_data select udid,ip_data,if(isnew=1,1,0) from ftrl_train_g60data where ip_data is not null;

计算预测结果:

 val df = sc.textFile("/tmp/ljhn1829/compare/ljh_g60_aplus_compare_data").map(line => (line.toString.split("\t")(0),lineToVector(line.toString.split("\t")(1)))).toDF("udid", "features")

 val predictionResult = lrModel.transform(df)
 predictionResult.show()

将spark的计算结果保存在一个hive表中(不需要预先创建表):

默认保存为parquet格式:

val options = Map("path" -> "/tmp/ljhn1829/compare/ljh_g60_aplus_predict_result1")
predictionResult.write.options(options).mode(SaveMode.Append).saveAsTable("ljh_g60_aplus_predict_result1")

试一下text格式(未测试):

val options = Map("path" -> "/tmp/ljhn1829/compare/ljh_g60_aplus_predict_result2")
predictionResult.write.format("text").options(options).mode(SaveMode.Append).saveAsTable("ljh_g60_aplus_predict_result2")

保存后表的结构如下:

udid                    string
features                struct<type:tinyint,size:int,indices:array<int>,values:array<double>>
rawprediction           struct<type:tinyint,size:int,indices:array<int>,values:array<double>>
probability             struct<type:tinyint,size:int,indices:array<int>,values:array<double>>
prediction              double

其中几个字段是struct类型,可以直接取其中的某些字段值:

select udid, probability.values[1],prediction from ljh_g60_aplus_predict_result1 limit 10;

SET hive.exec.compress.output=false;
create external table udid_ftrl_predict_01g60train2 (
 udid string,
  label int,
   predict double,
   ip_data string
)
row format delimited fields terminated by '\t'
STORED AS textfile
location '/user/gzquyajun/test/label/udid_ftrl_predict_01g60train/';

总的正确数量:228836
1、算法1正确的结果数量:

select count(1) from (select udid,predict,label from udid_ftrl_predict_01g60train2  order by predict desc limit 5000000 ) t where t.udid in (select udid from ljh_g60_aplus_compare_data where label='1');

116863 180318

2、算法2正确的结果:

select count(1) from (select udid, probability.values[1] as p from ljh_g60_aplus_predict_result1 order by p desc  limit 5000000 ) t where t.udid in (select udid from ljh_g60_aplus_compare_data where label='1');

77876 132865

3、2个算法重合的数量

select count(1) from (select udid,predict from udid_ftrl_predict_01g60train2 order by predict desc limit 5000000) t1, (select udid, probability.values[1] as p from ljh_g60_aplus_predict_result1 order by p desc  limit 5000000) t2 where t1.udid=t2.udid;

4、2个算法正确结果的重合数量

select count(1) from (select udid from (select udid,predict,label from udid_ftrl_predict_01g60train2  order by predict desc limit 5000000 ) t where t.label=1 limit 10) a1, (select t3.udid from (select udid,predict from udid_ftrl_predict_01g60train2 order by predict desc limit 5000000) t3, (select udid, probability.values[1] as p from ljh_g60_aplus_predict_result1 order by p desc  limit 5000000) t2 where t3.udid=t2.udid) a2  where a1.udid=a2.udid;
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值