当遇到大规模逻辑回归LR时,原生spark是解决不了问题的
本项目需要使用LR模型作为排序模型,输入矩阵为独热编码后的稀疏矩阵。不考虑PMML存储方式的实现很简单,使用的是官方API(我用的是spark2.4.0版本)
通过独热编码One-hotCode产生高维稀疏矩阵时,此时还想通过JPMML-spark工具和pipelineModel方式生成PMML文件是不可行。
一开始我也以为LR模型模型训练后很容易导出为PMML文件。
通过下文我开启了PMML探索之旅。
我也是Vector输入,所以直接报错。看到他也提到的这个问题
他这一段不明不白,然后呢,尤其是他的1.4节问题思考,对新手极不友好。
可能也是从stackflow上学到的方法
import org.apache.spark.sql.functions._
import org.apache.spark.ml._
val df = Seq( (1 , linalg.Vectors.dense(1,0,1,1,0) ) ).toDF("id", "features")
//df: org.apache.spark.sql.DataFrame = [id: int, features: vector]
df.show
//+---+---------------------+
//|id |features |
//+---+---------------------+
//|1 |[1.0,0.0,1.0,1.0,0.0]|
//+---+---------------------+
// A UDF to convert VectorUDT to ArrayType
val vecToArray = udf( (xs: linalg.Vector) => xs.toArray )
// Add a ArrayType Column
val dfArr = df.withColumn("featuresArr" , vecToArray($"features") )
// Array of element names that need to be fetched
// ArrayIndexOutOfBounds is not checked.
// sizeof `elements` should be equal to the number of entries in column `features`
val elements = Array("f1", "f2", "f3", "f4", "f5")
// Create a SQL-like expression using the array
val sqlExpr = elements.zipWithIndex.map{ case (alias, idx) => col("featuresArr").getItem(idx).as(alias) }
// Extract Elements from dfArr
dfArr.select(sqlExpr : _*).show
//+---+---+---+---+---+
//| f1| f2| f3| f4| f5|
//+---+---+---+---+---+
//|1.0|0.0|1.0|1.0|0.0|
//+---+---+---+---+---+
(https://stackoverflow.com/questions/49911608/scala-spark-split-vector-column-into-separate-columns-in-a-spark-dataframe)②
老外还是舒服啊,没有老板逼着写代码一样,注释写这么清楚,服了!
所以它这里说明了问题,数组不检测大小的。拉链形式去拆分成列。我就这么写了,所以我的高维稀疏矩阵我也不想拆成几万column那种。结果一拉,只保留了前几位,都是0
偶然间我又看到了一个大佬的博文,也提到了Vector的输入问题,详情如下。
奈何研究了半天他的博文,我还是没解决我的问题
现在来复盘一下原因:
pipelineModel模式不需要我解释太多了吧。关键的问题在于Pipeline需要输入基本数据类型,所以Vector这类的输入无处藏身。博文③中的作者其实解决的问题是躲过类型检测,把Vector装成String类型躲过检测而已(string的其实他没用到),其实他使用的仍然是基础数据类型。而我的情况是,Vector就是我的输入,而且是多个列都是Vector。我尝试了他的方法,也使用到了转成String躲过集合器的检测并且在transform方法中把string类型又转出为Vector。我的是稀疏矩阵,和博主③不一样,代码如下:
override def transform(df: Dataset[_]):DataFrame = {
// 这个transform函数只是对df中某一列数据进行处理
var string2vector = (x: String) => {
getSparseVectorFromString(x)
}
var str2vec = udf(string2vector)
// str2vec函数中传入你要处理的df中的列名
df.withColumn($(outputCol), str2vec(col("featuresString")))
}
def getSparseVectorFromString(input:String):linalg.Vector ={
val tmp = input.substring(1,input.length-1).split("\\[")
val tmp0 = tmp(0).substring(0,tmp(0).indexOf(",")).toInt
var tmp1=Array(1)
var tmp2=Array(1.0)
if(tmp(1)!= null && !tmp(1).equals("],")){
tmp1 = tmp(1).substring(0,tmp(1).length-2).split(",").map(_.toInt)
}
if(tmp(2)!= null && !tmp(2).equals("]")){
tmp2 = tmp(2).substring(0,tmp(2).length-1).split(",").map(_.toDouble)
}
val vs = Vectors.sparse(tmp0, tmp1,tmp2 )
vs
}
那么问题来了,模型输入是高维(万级别),但是pipeline中没有高维输入,在pipeline中输入的String。IDEA直接报错:
java.lang.IllegalArgumentException: Expected 23001 feature(s), got 1 feature(s)
。这样做对我来说是不合适的。所以,如果输入矩阵里有非基本类型的,拆分成了多列,那么pipeline输入和模型实际用到的维数要一致。我拿一个2万维的稀疏矩阵变成String又训练了2万维的LR模型,那么你的pipeline输入也要能抽取得出2万个维度。。
我还是老老实实的研究博主①的方法了。理论上可实现了,代码如下:
def getPipelineModel(training:DataFrame,
spark:SparkSession,array: Array[String]): PMML = {
//创建大规模字符串数组版本(内存溢出)
val lr = new LogisticRegression()
.setLabelCol(CommonName.labelColName)
.setFeaturesCol("features")
.setMaxIter(CommonName.maxIter)
val vectorAssem = getVectorAssemble(array)
val trainData = vectorAssem.transform(training)
val vecToArray = udf((x: linalg.Vector) => x.toArray)
val dfArr = trainData.withColumn("featuresArr", vecToArray(col("features")))
val tmp = dfArr.select("featuresArr").first().toString()
val featureSize = tmp.substring(13, tmp.length - 2).split(",").size
val arr = new Array[String](featureSize)
for (i <- 0 until featureSize) {
arr(i) = "col" + i
}
val sqlExpr = arr.zipWithIndex.map { case (alias, idx) => col("featuresArr").getItem(idx).as(alias) }
val sqlExprWithLabel = sqlExpr.+:(col(CommonName.labelColName))
val featuresAndLabelDF = dfArr.select(sqlExprWithLabel: _*)
println("featuresAndLabelDF,看是不是被截断了")
val pipeline = new Pipeline().setStages(Array(getVectorAssemble(arr), lr))
val pipelineModel = pipeline.fit(featuresAndLabelDF)
pipelineModel
val pmml = new PMMLBuilder(featuresAndLabelDF.schema, pipelineModel).build()
pmml
}
出乎意料的事情发现了:OOM,如图所示
old和Eden区一直100%动不了了,然后下一次GC就OOM了。问题待解决(小公司是不可能研究太多的,要做事情的)
(http://itpcb.com/a/583146)④这位博主提到了这个事情,spark开源本身就不能解决大规模LR
官方的api实现的时候并没有考虑大规模的场景,基本不可用。
为了推进工作,我还是要解决问题啊,于是乎我在官方API
https://spark.apache.org/docs/2.4.0/mllib-pmml-model-export.html
看到了希望。就用官方的toPMML方式。
只有mllib支持PMML导出形式,ml中已经不支持该方式导出了。而且mllib原生的数据输入方式必须是RDD[LabeledPoint],所以我的输入要调整,实现如下:
def getPipelineModel(training:DataFrame,
spark:SparkSession,array: Array[String]): LogisticRegressionModel ={
//* @param input RDD of (label, array of features) pairs.
val vector = getVectorAssemble(array)
val trainFeature = vector.transform(training)
val vecToArray = udf( (x : linalg.Vector) => x.toArray)//这里的array是scala.collection.mutable.WrappedArray
val tmp =trainFeature.withColumn("features",vecToArray(col("features")))
.withColumn(CommonName.labelColName, col(CommonName.labelColName).cast("double"))
tmp.printSchema()
val input =tmp.rdd.map(
row=>LabeledPoint(row.getAs[Double](CommonName.labelColName)
,Vectors.dense(row.getAs[mutable.WrappedArray[Double]]("features").toArray)))
val lrModel =new LogisticRegressionWithLBFGS().run(input)
lrModel
}
def savePMMLmodel(lrModel:LogisticRegressionModel): Unit ={
val modelPath ="lrTest.pmml"
lrModel.toPMML(modelPath)
}