spark导出PMML模型bug排查纪实

当遇到大规模逻辑回归LR时,原生spark是解决不了问题的

项目场景:

本项目需要使用LR模型作为排序模型,输入矩阵为独热编码后的稀疏矩阵。不考虑PMML存储方式的实现很简单,使用的是官方API(我用的是spark2.4.0版本)

通过独热编码One-hotCode产生高维稀疏矩阵时,此时还想通过JPMML-spark工具和pipelineModel方式生成PMML文件是不可行。


问题描述:

一开始我也以为LR模型模型训练后很容易导出为PMML文件。

通过下文我开启了PMML探索之旅。

模型在线预测服务之模型转换PMML(https://blog.csdn.net/frank110503/article/details/103502982)①

我也是Vector输入,所以直接报错。看到他也提到的这个问题

spark导出PMML模型bug排查纪实_第1张图片他这一段不明不白,然后呢,尤其是他的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

spark导出PMML模型bug排查纪实_第2张图片

偶然间我又看到了一个大佬的博文,也提到了Vector的输入问题,详情如下。

spark-ml和jpmml-sparkml生成pmml模型过程种遇到的问题(https://blog.csdn.net/NOT_GUY/article/details/100054852)③

奈何研究了半天他的博文,我还是没解决我的问题 


原因分析:

现在来复盘一下原因:

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,如图所示

spark导出PMML模型bug排查纪实_第3张图片

old和Eden区一直100%动不了了,然后下一次GC就OOM了。问题待解决(小公司是不可能研究太多的,要做事情的)

(http://itpcb.com/a/583146)④这位博主提到了这个事情,spark开源本身就不能解决大规模LR

ML库的feature process及LR的问题

interaction、assembly之类的问题

 

官方的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)
	}

 

 

 

你可能感兴趣的:(大数据学习,机器学习工程问题,spark,机器学习)