日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) >

XGBoost缺失值引发的问题及其深度分析

發(fā)布時(shí)間:2025/3/21 52 豆豆
生活随笔 收集整理的這篇文章主要介紹了 XGBoost缺失值引发的问题及其深度分析 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

XGBoost缺失值引發(fā)的問(wèn)題及其深度分析

2019年08月15日?作者: 李兆軍?文章鏈接?3969字?8分鐘閱讀

1. 背景

XGBoost模型作為機(jī)器學(xué)習(xí)中的一大“殺器”,被廣泛應(yīng)用于數(shù)據(jù)科學(xué)競(jìng)賽和工業(yè)領(lǐng)域,XGBoost官方也提供了可運(yùn)行于各種平臺(tái)和環(huán)境的對(duì)應(yīng)代碼,如適用于Spark分布式訓(xùn)練的XGBoost on Spark。然而,在XGBoost on Spark的官方實(shí)現(xiàn)中,卻存在一個(gè)因XGBoost缺失值和Spark稀疏表示機(jī)制而帶來(lái)的不穩(wěn)定問(wèn)題。

事情起源于美團(tuán)內(nèi)部某機(jī)器學(xué)習(xí)平臺(tái)使用方同學(xué)的反饋,在該平臺(tái)上訓(xùn)練出的XGBoost模型,使用同一個(gè)模型、同一份測(cè)試數(shù)據(jù),在本地調(diào)用(Java引擎)與平臺(tái)(Spark引擎)計(jì)算的結(jié)果不一致。但是該同學(xué)在本地運(yùn)行兩種引擎(Python引擎和Java引擎)進(jìn)行測(cè)試,兩者的執(zhí)行結(jié)果是一致的。因此質(zhì)疑平臺(tái)的XGBoost預(yù)測(cè)結(jié)果會(huì)不會(huì)有問(wèn)題?

該平臺(tái)對(duì)XGBoost模型進(jìn)行過(guò)多次定向優(yōu)化,在XGBoost模型測(cè)試時(shí),并沒(méi)有出現(xiàn)過(guò)本地調(diào)用(Java引擎)與平臺(tái)(Spark引擎)計(jì)算結(jié)果不一致的情形。而且平臺(tái)上運(yùn)行的版本,和該同學(xué)本地使用的版本,都來(lái)源于Dmlc的官方版本,JNI底層調(diào)用的應(yīng)該是同一份代碼,理論上,結(jié)果應(yīng)該是完全一致的,但實(shí)際中卻不同。

從該同學(xué)給出的測(cè)試代碼上,并沒(méi)有發(fā)現(xiàn)什么問(wèn)題:

//測(cè)試結(jié)果中的一行,41列 double[] input = new double[]{1, 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]; //轉(zhuǎn)化為float[] float[] testInput = new float[input.length]; for(int i = 0, total = input.length; i < total; i++){testInput[i] = new Double(input[i]).floatValue(); } //加載模型 Booster booster = XGBoost.loadModel("${model}"); //轉(zhuǎn)為DMatrix,一行,41列 DMatrix testMat = new DMatrix(testInput, 1, 41); //調(diào)用模型 float[][] predicts = booster.predict(testMat);

上述代碼在本地執(zhí)行的結(jié)果是333.67892,而平臺(tái)上執(zhí)行的結(jié)果卻是328.1694030761719。

兩次結(jié)果怎么會(huì)不一樣,問(wèn)題出現(xiàn)在哪里呢?

2. 執(zhí)行結(jié)果不一致問(wèn)題排查歷程

如何排查?首先想到排查方向就是,兩種處理方式中輸入的字段類(lèi)型會(huì)不會(huì)不一致。如果兩種輸入中字段類(lèi)型不一致,或者小數(shù)精度不同,那結(jié)果出現(xiàn)不同就是可解釋的了。仔細(xì)分析模型的輸入,注意到數(shù)組中有一個(gè)6.666666666666667,是不是它的原因?

一個(gè)個(gè)Debug仔細(xì)比對(duì)兩側(cè)的輸入數(shù)據(jù)及其字段類(lèi)型,完全一致。

這就排除了兩種方式處理時(shí),字段類(lèi)型和精度不一致的問(wèn)題。

第二個(gè)排查思路是,XGBoost on Spark按照模型的功能,提供了XGBoostClassifier和XGBoostRegressor兩個(gè)上層API,這兩個(gè)上層API在JNI的基礎(chǔ)上,加入了很多超參數(shù),封裝了很多上層能力。會(huì)不會(huì)是在這兩種封裝過(guò)程中,新加入的某些超參數(shù)對(duì)輸入結(jié)果有著特殊的處理,從而導(dǎo)致結(jié)果不一致?

與反饋此問(wèn)題的同學(xué)溝通后得知,其Python代碼中設(shè)置的超參數(shù)與平臺(tái)設(shè)置的完全一致。仔細(xì)檢查XGBoostClassifier和XGBoostRegressor的源代碼,兩者對(duì)輸出結(jié)果并沒(méi)有做任何特殊處理。

再次排除了XGBoost on Spark超參數(shù)封裝問(wèn)題。

再一次檢查模型的輸入,這次的排查思路是,檢查一下模型的輸入中有沒(méi)有特殊的數(shù)值,比方說(shuō),NaN、-1、0等。果然,輸入數(shù)組中有好幾個(gè)0出現(xiàn),會(huì)不會(huì)是因?yàn)槿笔е堤幚淼膯?wèn)題?

快速找到兩個(gè)引擎的源碼,發(fā)現(xiàn)兩者對(duì)缺失值的處理真的不一致!

XGBoost4j中缺失值的處理

XGBoost4j缺失值的處理過(guò)程發(fā)生在構(gòu)造DMatrix過(guò)程中,默認(rèn)將0.0f設(shè)置為缺失值:

/*** create DMatrix from dense matrix** @param data data values* @param nrow number of rows* @param ncol number of columns* @throws XGBoostError native error*/public DMatrix(float[] data, int nrow, int ncol) throws XGBoostError {long[] out = new long[1];//0.0f作為missing的值XGBoostJNI.checkCall(XGBoostJNI.XGDMatrixCreateFromMat(data, nrow, ncol, 0.0f, out));handle = out[0];}

XGBoost on Spark中缺失值的處理

而xgboost on Spark將NaN作為默認(rèn)的缺失值。

/*** @return A tuple of the booster and the metrics used to build training summary*/@throws(classOf[XGBoostError])def trainDistributed(trainingDataIn: RDD[XGBLabeledPoint],params: Map[String, Any],round: Int,nWorkers: Int,obj: ObjectiveTrait = null,eval: EvalTrait = null,useExternalMemory: Boolean = false,//NaN作為missing的值missing: Float = Float.NaN,hasGroup: Boolean = false): (Booster, Map[String, Array[Float]]) = {//...}

也就是說(shuō),本地Java調(diào)用構(gòu)造DMatrix時(shí),如果不設(shè)置缺失值,默認(rèn)值0被當(dāng)作缺失值進(jìn)行處理。而在XGBoost on Spark中,默認(rèn)NaN會(huì)被為缺失值。原來(lái)Java引擎和XGBoost on Spark引擎默認(rèn)的缺失值并不一樣。而平臺(tái)和該同學(xué)調(diào)用時(shí),都沒(méi)有設(shè)置缺失值,造成兩個(gè)引擎執(zhí)行結(jié)果不一致的原因,就是因?yàn)槿笔е挡灰恢?#xff01;

修改測(cè)試代碼,在Java引擎代碼上設(shè)置缺失值為NaN,執(zhí)行結(jié)果為328.1694,與平臺(tái)計(jì)算結(jié)果完全一致。

//測(cè)試結(jié)果中的一行,41列double[] input = new double[]{1, 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];float[] testInput = new float[input.length];for(int i = 0, total = input.length; i < total; i++){testInput[i] = new Double(input[i]).floatValue();}Booster booster = XGBoost.loadModel("${model}");//一行,41列DMatrix testMat = new DMatrix(testInput, 1, 41, Float.NaN);float[][] predicts = booster.predict(testMat);

3. XGBoost on Spark源碼中缺失值引入的不穩(wěn)定問(wèn)題

然而,事情并沒(méi)有這么簡(jiǎn)單。

Spark ML中還有隱藏的缺失值處理邏輯:SparseVector,即稀疏向量。

SparseVector和DenseVector都用于表示一個(gè)向量,兩者之間僅僅是存儲(chǔ)結(jié)構(gòu)的不同。

其中,DenseVector就是普通的Vector存儲(chǔ),按序存儲(chǔ)Vector中的每一個(gè)值。

而SparseVector是稀疏的表示,用于向量中0值非常多場(chǎng)景下數(shù)據(jù)的存儲(chǔ)。

SparseVector的存儲(chǔ)方式是:僅僅記錄所有非0值,忽略掉所有0值。具體來(lái)說(shuō),用一個(gè)數(shù)組記錄所有非0值的位置,另一個(gè)數(shù)組記錄上述位置所對(duì)應(yīng)的數(shù)值。有了上述兩個(gè)數(shù)組,再加上當(dāng)前向量的總長(zhǎng)度,即可將原始的數(shù)組還原回來(lái)。

因此,對(duì)于0值非常多的一組數(shù)據(jù),SparseVector能大幅節(jié)省存儲(chǔ)空間。

SparseVector存儲(chǔ)示例見(jiàn)下圖:

如上圖所示,SparseVector中不保存數(shù)組中值為0的部分,僅僅記錄非0值。因此對(duì)于值為0的位置其實(shí)不占用存儲(chǔ)空間。下述代碼是Spark ML中VectorAssembler的實(shí)現(xiàn)代碼,從代碼中可見(jiàn),如果數(shù)值是0,在SparseVector中是不進(jìn)行記錄的。

private[feature] def assemble(vv: Any*): Vector = {val indices = ArrayBuilder.make[Int]val values = ArrayBuilder.make[Double]var cur = 0vv.foreach {case v: Double =>//0不進(jìn)行保存if (v != 0.0) {indices += curvalues += v}cur += 1case vec: Vector =>vec.foreachActive { case (i, v) =>//0不進(jìn)行保存if (v != 0.0) {indices += cur + ivalues += v}}cur += vec.sizecase null =>throw new SparkException("Values to assemble cannot be null.")case o =>throw new SparkException(s"$o of type ${o.getClass.getName} is not supported.")}Vectors.sparse(cur, indices.result(), values.result()).compressed}

不占用存儲(chǔ)空間的值,也是某種意義上的一種缺失值。SparseVector作為Spark ML中的數(shù)組的保存格式,被所有的算法組件使用,包括XGBoost on Spark。而事實(shí)上XGBoost on Spark也的確將Sparse Vector中的0值直接當(dāng)作缺失值進(jìn)行處理:

val instances: RDD[XGBLabeledPoint] = dataset.select(col($(featuresCol)),col($(labelCol)).cast(FloatType),baseMargin.cast(FloatType),weight.cast(FloatType)).rdd.map { case Row(features: Vector, label: Float, baseMargin: Float, weight: Float) =>val (indices, values) = features match {//SparseVector格式,僅僅將非0的值放入XGBoost計(jì)算case v: SparseVector => (v.indices, v.values.map(_.toFloat))case v: DenseVector => (null, v.values.map(_.toFloat))}XGBLabeledPoint(label, indices, values, baseMargin = baseMargin, weight = weight)}

XGBoost on Spark將SparseVector中的0值作為缺失值為什么會(huì)引入不穩(wěn)定的問(wèn)題呢?

重點(diǎn)來(lái)了,Spark ML中對(duì)Vector類(lèi)型的存儲(chǔ)是有優(yōu)化的,它會(huì)自動(dòng)根據(jù)Vector數(shù)組中的內(nèi)容選擇是存儲(chǔ)為SparseVector,還是DenseVector。也就是說(shuō),一個(gè)Vector類(lèi)型的字段,在Spark保存時(shí),同一列會(huì)有兩種保存格式:SparseVector和DenseVector。而且對(duì)于一份數(shù)據(jù)中的某一列,兩種格式是同時(shí)存在的,有些行是Sparse表示,有些行是Dense表示。選擇使用哪種格式表示通過(guò)下述代碼計(jì)算得到:

/*** Returns a vector in either dense or sparse format, whichever uses less storage.*/@Since("2.0.0")def compressed: Vector = {val nnz = numNonzeros// A dense vector needs 8 * size + 8 bytes, while a sparse vector needs 12 * nnz + 20 bytes.if (1.5 * (nnz + 1.0) < size) {toSparse} else {toDense}}

在XGBoost on Spark場(chǎng)景下,默認(rèn)將Float.NaN作為缺失值。如果數(shù)據(jù)集中的某一行存儲(chǔ)結(jié)構(gòu)是DenseVector,實(shí)際執(zhí)行時(shí),該行的缺失值是Float.NaN。而如果數(shù)據(jù)集中的某一行存儲(chǔ)結(jié)構(gòu)是SparseVector,由于XGBoost on Spark僅僅使用了SparseVector中的非0值,也就導(dǎo)致該行數(shù)據(jù)的缺失值是Float.NaN和0。

也就是說(shuō),如果數(shù)據(jù)集中某一行數(shù)據(jù)適合存儲(chǔ)為DenseVector,則XGBoost處理時(shí),該行的缺失值為Float.NaN。而如果該行數(shù)據(jù)適合存儲(chǔ)為SparseVector,則XGBoost處理時(shí),該行的缺失值為Float.NaN和0。

即,數(shù)據(jù)集中一部分?jǐn)?shù)據(jù)會(huì)以Float.NaN和0作為缺失值,另一部分?jǐn)?shù)據(jù)會(huì)以Float.NaN作為缺失值!?也就是說(shuō)在XGBoost on Spark中,0值會(huì)因?yàn)榈讓訑?shù)據(jù)存儲(chǔ)結(jié)構(gòu)的不同,同時(shí)會(huì)有兩種含義,而底層的存儲(chǔ)結(jié)構(gòu)是完全由數(shù)據(jù)集決定的。

因?yàn)榫€上Serving時(shí),只能設(shè)置一個(gè)缺失值,因此被選為SparseVector格式的測(cè)試集,可能會(huì)導(dǎo)致線上Serving時(shí),計(jì)算結(jié)果與期望結(jié)果不符。

4. 問(wèn)題解決

查了一下XGBoost on Spark的最新源碼,依然沒(méi)解決這個(gè)問(wèn)題。

趕緊把這個(gè)問(wèn)題反饋給XGBoost on Spark, 同時(shí)修改了我們自己的XGBoost on Spark代碼。

val instances: RDD[XGBLabeledPoint] = dataset.select(col($(featuresCol)),col($(labelCol)).cast(FloatType),baseMargin.cast(FloatType),weight.cast(FloatType)).rdd.map { case Row(features: Vector, label: Float, baseMargin: Float, weight: Float) =>//這里需要對(duì)原來(lái)代碼的返回格式進(jìn)行修改val values = features match {//SparseVector的數(shù)據(jù),先轉(zhuǎn)成Densecase v: SparseVector => v.toArray.map(_.toFloat)case v: DenseVector => v.values.map(_.toFloat)}XGBLabeledPoint(label, null, values, baseMargin = baseMargin, weight = weight)} /*** Converts a [[Vector]] to a data point with a dummy label.** This is needed for constructing a [[ml.dmlc.xgboost4j.scala.DMatrix]]* for prediction.*/def asXGB: XGBLabeledPoint = v match {case v: DenseVector =>XGBLabeledPoint(0.0f, null, v.values.map(_.toFloat))case v: SparseVector =>//SparseVector的數(shù)據(jù),先轉(zhuǎn)成DenseXGBLabeledPoint(0.0f, null, v.toArray.map(_.toFloat))}

問(wèn)題得到解決,而且用新代碼訓(xùn)練出來(lái)的模型,評(píng)價(jià)指標(biāo)還會(huì)有些許提升,也算是意外之喜。

希望本文對(duì)遇到XGBoost缺失值問(wèn)題的同學(xué)能夠有所幫助,也歡迎大家一起交流討論。

作者簡(jiǎn)介

  • 兆軍,美團(tuán)配送事業(yè)部算法平臺(tái)團(tuán)隊(duì)技術(shù)專(zhuān)家。

招聘信息

美團(tuán)配送事業(yè)部算法平臺(tái)團(tuán)隊(duì),負(fù)責(zé)美團(tuán)一站式大規(guī)模機(jī)器學(xué)習(xí)平臺(tái)圖靈平臺(tái)的建設(shè)。圍繞算法整個(gè)生命周期,利用可視化拖拽方式定義模型訓(xùn)練和預(yù)測(cè)流程,提供強(qiáng)大的模型管理、線上模型預(yù)測(cè)和特征服務(wù)能力,提供多維立體的AB分流支持和線上效果評(píng)估支持。團(tuán)隊(duì)的使命是為算法相關(guān)同學(xué)提供統(tǒng)一的,端到端的,一站式自助服務(wù)平臺(tái),幫助算法同學(xué)降低算法研發(fā)復(fù)雜度,提升算法迭代效率。

總結(jié)

以上是生活随笔為你收集整理的XGBoost缺失值引发的问题及其深度分析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。