《XGBoost缺失值引发的问题及其深度分析.docx》由会员分享,可在线阅读,更多相关《XGBoost缺失值引发的问题及其深度分析.docx(12页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。
1、XGBoost缺失值引发的问题及其深度分析|兆军美团配送事业部算法平台团队技术专家来源|美团技术团队*点击浏览原文查看美团技术团队更多文章背景XGBoost模型作为机器学习中的一大“杀器被广泛应用于数据科学竞赛以及工业领域XGBoost官方也提供了可运行于各种平台以及环境的对应代码如适用于Spark分布式训练的XGBoostonSpark。然而在XGBoostonSpark的官方实现中却存在一个因XGBoost缺失值以及Spark稀疏表示机制而带来的不稳定问题。事情起源于美团内部某机器学习平台使用方同学的反应在该平台上训练出的XGBoost模型使用同一个模型、同一份测试数据在本地调用Java引
2、擎与平台Spark引擎计算的结果不一致。但是该同学在本地运行两种引擎Python引擎以及Java引擎进展测试两者的执行结果是一致的。因此质疑平台的XGBoost预测结果会不会有问题该平台对XGBoost模型进展太多次定向优化在XGBoost模型测试时并没有出现过本地调用Java引擎与平台Spark引擎计算结果不一致的情形。而且平台上运行的版本以及该同学本地使用的版本都来源于Dmlc的官方版本JNI底层调用的应该是同一份代码理论上结果应该是完全一致的但实际中却不同。从该同学给出的测试代码上并没有发现什么问题/测试结果中的一行41列doubleinputnewdouble1,2,5,0,0,6.6
3、66666666666667,31.14,29.28,0,1.303333,2.8555,2.37,701,463,3.989,3.85,14400.5,15.79,11.45,0.915,7.05,5.5,0.023333,0.0365,0.0275,0.123333,0.4645,0.12,15.082,14.48,0,31.8425,29.1,7.7325,3,5.88,1.08,0,0,0,32;/转化为floatfloattestInputnewfloatinput.length;for(inti0,totalinput.length;itotal;i)testInputinewDo
4、uble(inputi).floatValue();/加载模型BoosterboosterXGBoost.loadModel($model/转为DMatrix一行41列DMatrixtestMatnewDMatrix(testInput,1,41);/调用模型floatpredictsbooster.predict(testMat);上述代码在本地执行的结果是333.67892而平台上执行的结果却是328.1694030761719。两次结果怎么会不一样问题出如今哪里呢执行结果不一致问题排查历程怎样排查首先想到排查方向就是两种处理方式中输入的字段类型会不会不一致。假如两种输入中字段类型不一致或
5、小数精度不同那结果出现不同就是可解释的了。仔细分析模型的输入注意到数组中有一个6.666666666666667是不是它的原因一个个Debug仔细比对两侧的输入数据及其字段类型完全一致。这就排除了两种方式处理时字段类型以及精度不一致的问题。第二个排查思路是XGBoostonSpark按照模型的功能提供了XGBoostClassifier以及XGBoostRegressor两个上层API这两个上层API在JNI的根底上参加了很多超参数封装了很多上层才能。会不会是在这两种封装经过中新参加的某些超参数对输入结果有着特殊的处理进而导致结果不一致与反应此问题的同学沟通后得知其Python代码中设置的超参
6、数与平台设置的完全一致。仔细检查XGBoostClassifier以及XGBoostRegressor的源代码两者对输出结果并没有做任何特殊处理。再次排除了XGBoostonSpark超参数封装问题。再一次检查模型的输入这次的排查思路是检查一下模型的输入中有没有特殊的数值比方讲NaN、-1、0等。果然输入数组中有好几个0出现会不会是因为缺失值处理的问题快速找到两个引擎的源码发现两者对缺失值的处理真的不一致XGBoost4j中缺失值的处理XGBoost4j缺失值的处理经过发生在构造DMatrix经过中默认将0.0f设置为缺失值/*createDMatrixfromdensematrix*para
7、mdatadatavalues*paramnrownumberofrows*paramncolnumberofcolumns*throwsXGBoostErrornativeerror*/publicDMatrix(floatdata,intnrow,intncol)throwsXGBoostErrorlongoutnewlong1;/0.0f作为missing的值XGBoostJNI.checkCall(XGBoostJNI.XGDMatrixCreateFromMat(data,nrow,ncol,0.0f,out);handleout0;XGBoostonSpark中缺失值的处理而XGB
8、oostonSpark将NaN作为默认的缺失值。scala/*returnAtupleoftheboosterandthemetricsusedtobuildtrainingsummary*/throws(classOfXGBoostError)deftrainDistributed(trainingDataIn:RDDXGBLabeledPoint,params:MapString,Any,round:Int,nWorkers:Int,obj:ObjectiveTraitnull,eval:EvalTraitnull,useExternalMemory:Booleanfalse,/NaN作为
9、missing的值missing:FloatFloat.NaN,hasGroup:Booleanfalse):(Booster,MapString,ArrayFloat)/.也就是讲本地Java调用构造DMatrix时假如不设置缺失值默认值0被当作缺失值进展处理。而在XGBoostonSpark中默认NaN会被为缺失值。原来Java引擎以及XGBoostonSpark引擎默认的缺失值并不一样。而平台以及该同学调用时都没有设置缺失值造成两个引擎执行结果不一致的原因就是因为缺失值不一致修改测试代码在Java引擎代码上设置缺失值为NaN执行结果为328.1694与平台计算结果完全一致。/测试结果中的
10、一行41列doubleinputnewdouble1,2,5,0,0,6.666666666666667,31.14,29.28,0,1.303333,2.8555,2.37,701,463,3.989,3.85,14400.5,15.79,11.45,0.915,7.05,5.5,0.023333,0.0365,0.0275,0.123333,0.4645,0.12,15.082,14.48,0,31.8425,29.1,7.7325,3,5.88,1.08,0,0,0,32;floattestInputnewfloatinput.length;for(inti0,totalinput.le
11、ngth;itotal;i)testInputinewDouble(inputi).floatValue();BoosterboosterXGBoost.loadModel($model/一行41列DMatrixtestMatnewDMatrix(testInput,1,41,Float.NaN);floatpredictsbooster.predict(testMat);XGBoostonSpark源码中缺失值引入的不稳定问题然而事情并没有这么简单。SparkML中还有隐藏的缺失值处理逻辑SparseVector即稀疏向量。SparseVector以及DenseVector都用于表示一个向量
12、两者之间仅仅是存储构造的不同。其中DenseVector就是普通的Vector存储按序存储Vector中的每一个值。而SparseVector是稀疏的表示用于向量中0值非常多场景下数据的存储。SparseVector的存储方式是仅仅记录所有非0值忽略掉所有0值。详细来讲用一个数组记录所有非0值的位置另一个数组记录上述位置所对应的数值。有了上述两个数组再加受骗前向量的总长度即可将原始的数组复原回来。因此对于0值非常多的一组数据SparseVector能大幅节省存储空间。SparseVector存储例如见下列图如上图所示SparseVector中不保存数组中值为0的局部仅仅记录非0值。因此对于值为
13、0的位置其实不占用存储空间。下述代码是SparkML中VectorAssembler的实当代码从代码中可见假如数值是0在SparseVector中是不进展记录的。scalaprivatefeaturedefassemble(vv:Any*):VectorvalindicesArrayBuilder.makeIntvalvaluesArrayBuilder.makeDoublevarcur0vv.foreachcasev:Double/0不进展保存if(v!0.0)indicescurvaluesvcur1casevec:Vectorvec.foreachActivecase(i,v)/0不进展
14、保存if(v!0.0)indicescurivaluesvcurvec.sizecasenullthrownewSparkException(Valuestoassemblecannotbenull.)caseothrownewSparkException(s$ooftype$o.getClass.getNameisnotsupported.)Vectors.sparse(cur,indices.result(),values.result()pressed不占用存储空间的值也是某种意义上的一种缺失值。SparseVector作为SparkML中的数组的保存格式被所有的算法组件使用包括XGBo
15、ostonSpark。而事实上XGBoostonSpark也确实将SparseVector中的0值直接当作缺失值进展处理scalavalinstances:RDDXGBLabeledPointdataset.select(col($(featuresCol),col($(labelCol).cast(FloatType),baseMargin.cast(FloatType),weight.cast(FloatType).rdd.mapcaseRow(features:Vector,label:Float,baseMargin:Float,weight:Float)val(indices,val
16、ues)featuresmatch/SparseVector格式仅仅将非0的值放入XGBoost计算casev:SparseVector(v.indices,v.values.map(_.toFloat)casev:DenseVector(null,v.values.map(_.toFloat)XGBLabeledPoint(label,indices,values,baseMarginbaseMargin,weightweight)XGBoostonSpark将SparseVector中的0值作为缺失值为什么会引入不稳定的问题呢重点来了SparkML中对Vector类型的存储是有优化的它会自
17、动根据Vector数组中的内容选择是存储为SparseVector还是DenseVector。也就是讲一个Vector类型的字段在Spark保存时同一列会有两种保存格式SparseVector以及DenseVector。而且对于一份数据中的某一列两种格式是同时存在的有些行是Sparse表示有些行是Dense表示。选择使用哪种格式表示通过下述代码计算得到scala/*Returnsavectorineitherdenseorsparseformat,whicheveruseslessstorage.*/Since(2.0.0)defcompressed:VectorvalnnznumNonzer
18、os/Adensevectorneeds8*size8bytes,whileasparsevectorneeds12*nnz20bytes.if(1.5*(nnz1.0)size)toSparseelsetoDense在XGBoostonSpark场景下默认将Float.NaN作为缺失值。假如数据集中的某一行存储构造是DenseVector实际执行时该行的缺失值是Float.NaN。而假如数据集中的某一行存储构造是SparseVector由于XGBoostonSpark仅仅使用了SparseVector中的非0值也就导致该行数据的缺失值是Float.NaN以及0。也就是讲假如数据集中某一行数据
19、合适存储为DenseVector那么XGBoost处理时该行的缺失值为Float.NaN。而假如该行数据合适存储为SparseVector那么XGBoost处理时该行的缺失值为Float.NaN以及0。即数据集中一局部数据会以Float.NaN以及0作为缺失值另一局部数据会以Float.NaN作为缺失值也就是讲在XGBoostonSpark中0值会因为底层数据存储构造的不同同时会有两种含义而底层的存储构造是完全由数据集决定的。因为线上Serving时只能设置一个缺失值因此被选为SparseVector格式的测试集可能会导致线上Serving时计算结果与期望结果不符。问题解决查了一下XGBoos
20、tonSpark的最新源码仍然没解决这个问题。赶紧把这个问题反应给XGBoostonSpark同时修改了我们自己的XGBoostonSpark代码。scalavalinstances:RDDXGBLabeledPointdataset.select(col($(featuresCol),col($(labelCol).cast(FloatType),baseMargin.cast(FloatType),weight.cast(FloatType).rdd.mapcaseRow(features:Vector,label:Float,baseMargin:Float,weight:Float)/
21、这里需要对原来代码的返回格式进展修改valvaluesfeaturesmatch/SparseVector的数据先转成Densecasev:SparseVectorv.toArray.map(_.toFloat)casev:DenseVectorv.values.map(_.toFloat)XGBLabeledPoint(label,null,values,baseMarginbaseMargin,weightweight)scala/*ConvertsaVectortoadatapointwithadummylabel.*Thisisneededforconstructingaml.dmlc
22、.xgboost4j.scala.DMatrix*forprediction.*/defasXGB:XGBLabeledPointvmatchcasev:DenseVectorXGBLabeledPoint(0.0f,null,v.values.map(_.toFloat)casev:SparseVector/SparseVector的数据先转成DenseXGBLabeledPoint(0.0f,null,v.toArray.map(_.toFloat)问题得到解决而且用新代码训练出来的模型评价指标还会有些许提升也算是意外之喜。祈望本文对遇到XGBoost缺失值问题的同学可以有所帮助也欢送大众一起沟通讨论。技术的道路一个人走着极为困难一身的本领得不施展优质的文章得不到曝光别担忧即刻起CSDN将为你带来创新创造创变展现的大舞台扫描下方二维码欢送参加CSDN原力方案*本文为AI科技大本营转载文章转载请联络原精彩公开课推荐浏览滴滴开源在2019你点的每个“在看我都认真当成了AI