XGBoost缺失值引發的問題及其深度分析|CSDN博文精選

2021-01-13 AI科技大本營

作者 | 兆軍(美團配送事業部算法平臺團隊技術專家)

來源 | 美團技術團隊

(*點擊閱讀原文,查看美團技術團隊更多文章)

背景

XGBoost模型作為機器學習中的一大「殺器」,被廣泛應用於數據科學競賽和工業領域,XGBoost官方也提供了可運行於各種平臺和環境的對應代碼,如適用於Spark分布式訓練的XGBoost on Spark。然而,在XGBoost on Spark的官方實現中,卻存在一個因XGBoost缺失值和Spark稀疏表示機制而帶來的不穩定問題。

事情起源於美團內部某機器學習平臺使用方同學的反饋,在該平臺上訓練出的XGBoost模型,使用同一個模型、同一份測試數據,在本地調用(Java引擎)與平臺(Spark引擎)計算的結果不一致。但是該同學在本地運行兩種引擎(Python引擎和Java引擎)進行測試,兩者的執行結果是一致的。因此質疑平臺的XGBoost預測結果會不會有問題?

該平臺對XGBoost模型進行過多次定向優化,在XGBoost模型測試時,並沒有出現過本地調用(Java引擎)與平臺(Spark引擎)計算結果不一致的情形。而且平臺上運行的版本,和該同學本地使用的版本,都來源於Dmlc的官方版本,JNI底層調用的應該是同一份代碼,理論上,結果應該是完全一致的,但實際中卻不同。

從該同學給出的測試代碼上,並沒有發現什麼問題:

//測試結果中的一行,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];//轉化為floatfloat 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}");//轉為DMatrix,一行,41列DMatrix testMat = new DMatrix(testInput, 1, 41);//調用模型float predicts = booster.predict(testMat);上述代碼在本地執行的結果是333.67892,而平臺上執行的結果卻是328.1694030761719。

兩次結果怎麼會不一樣,問題出現在哪裡呢?

執行結果不一致問題排查歷程

如何排查?首先想到排查方向就是,兩種處理方式中輸入的欄位類型會不會不一致。如果兩種輸入中欄位類型不一致,或者小數精度不同,那結果出現不同就是可解釋的了。仔細分析模型的輸入,注意到數組中有一個6.666666666666667,是不是它的原因?

一個個Debug仔細比對兩側的輸入數據及其欄位類型,完全一致。

這就排除了兩種方式處理時,欄位類型和精度不一致的問題。

第二個排查思路是,XGBoost on Spark按照模型的功能,提供了XGBoostClassifier和XGBoostRegressor兩個上層API,這兩個上層API在JNI的基礎上,加入了很多超參數,封裝了很多上層能力。會不會是在這兩種封裝過程中,新加入的某些超參數對輸入結果有著特殊的處理,從而導致結果不一致?

與反饋此問題的同學溝通後得知,其Python代碼中設置的超參數與平臺設置的完全一致。仔細檢查XGBoostClassifier和XGBoostRegressor的原始碼,兩者對輸出結果並沒有做任何特殊處理。

再次排除了XGBoost on Spark超參數封裝問題。

再一次檢查模型的輸入,這次的排查思路是,檢查一下模型的輸入中有沒有特殊的數值,比方說,NaN、-1、0等。果然,輸入數組中有好幾個0出現,會不會是因為缺失值處理的問題?

快速找到兩個引擎的源碼,發現兩者對缺失值的處理真的不一致!

XGBoost4j中缺失值的處理

XGBoost4j缺失值的處理過程發生在構造DMatrix過程中,默認將0.0f設置為缺失值:

/*** 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作為默認的缺失值。

scala/*** @return A tuple ofthe 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 = ,eval: EvalTrait = ,useExternalMemory: Boolean = false,//NaN作為missing的值missing: Float = Float.NaN,hasGroup: Boolean = false): (Booster, Map[String, Array[Float]]) = {//...}也就是說,本地Java調用構造DMatrix時,如果不設置缺失值,默認值0被當作缺失值進行處理。而在XGBoost on Spark中,默認NaN會被為缺失值。原來Java引擎和XGBoost on Spark引擎默認的缺失值並不一樣。而平臺和該同學調用時,都沒有設置缺失值,造成兩個引擎執行結果不一致的原因,就是因為缺失值不一致!

修改測試代碼,在Java引擎代碼上設置缺失值為NaN,執行結果為328.1694,與平臺計算結果完全一致。

//測試結果中的一行,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);

XGBoost on Spark源碼中缺失值引入的不穩定問題

然而,事情並沒有這麼簡單。

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

SparseVector和DenseVector都用於表示一個向量,兩者之間僅僅是存儲結構的不同。其中,DenseVector就是普通的Vector存儲,按序存儲Vector中的每一個值。而SparseVector是稀疏的表示,用於向量中0值非常多場景下數據的存儲。SparseVector的存儲方式是:僅僅記錄所有非0值,忽略掉所有0值。具體來說,用一個數組記錄所有非0值的位置,另一個數組記錄上述位置所對應的數值。有了上述兩個數組,再加上當前向量的總長度,即可將原始的數組還原回來。因此,對於0值非常多的一組數據,SparseVector能大幅節省存儲空間。

SparseVector存儲示例見下圖:

如上圖所示,SparseVector中不保存數組中值為0的部分,僅僅記錄非0值。因此對於值為0的位置其實不佔用存儲空間。下述代碼是Spark ML中VectorAssembler的實現代碼,從代碼中可見,如果數值是0,在SparseVector中是不進行記錄的。

scalaprivate[feature] def assemble(vv: Any*): Vector = {val indices = ArrayBuilder.make[Int]val values = ArrayBuilder.make[Double]var cur = 0vv.foreach {case v: Double =>//0不進行保存if (v != 0.0) {indices += curvalues += v}cur += 1case vec: Vector =>vec.foreachActive { case (i, v) =>//0不進行保存if (v != 0.0) {indices += cur + ivalues += v}}cur += vec.sizecase =>throw new SparkException("Values to assemble cannot be .")caseo =>throw new SparkException(s"$o of type ${o.getClass.getName} is not supported.")}Vectors.sparse(cur, indices.result, values.result).compressed}不佔用存儲空間的值,也是某種意義上的一種缺失值。SparseVector作為Spark ML中的數組的保存格式,被所有的算法組件使用,包括XGBoost on Spark。而事實上XGBoost on Spark也的確將Sparse Vector中的0值直接當作缺失值進行處理:

scalaval 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計算case v: SparseVector => (v.indices, v.values.map(_.toFloat))case v: DenseVector => (, v.values.map(_.toFloat))}XGBLabeledPoint(label, indices, values, baseMargin = baseMargin, weight = weight)}XGBoost on Spark將SparseVector中的0值作為缺失值為什麼會引入不穩定的問題呢?

重點來了,Spark ML中對Vector類型的存儲是有優化的,它會自動根據Vector數組中的內容選擇是存儲為SparseVector,還是DenseVector。也就是說,一個Vector類型的欄位,在Spark保存時,同一列會有兩種保存格式:SparseVector和DenseVector。而且對於一份數據中的某一列,兩種格式是同時存在的,有些行是Sparse表示,有些行是Dense表示。選擇使用哪種格式表示通過下述代碼計算得到:

scala/*** 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場景下,默認將Float.NaN作為缺失值。如果數據集中的某一行存儲結構是DenseVector,實際執行時,該行的缺失值是Float.NaN。而如果數據集中的某一行存儲結構是SparseVector,由於XGBoost on Spark僅僅使用了SparseVector中的非0值,也就導致該行數據的缺失值是Float.NaN和0。

也就是說,如果數據集中某一行數據適合存儲為DenseVector,則XGBoost處理時,該行的缺失值為Float.NaN。而如果該行數據適合存儲為SparseVector,則XGBoost處理時,該行的缺失值為Float.NaN和0。

即,數據集中一部分數據會以Float.NaN和0作為缺失值,另一部分數據會以Float.NaN作為缺失值!也就是說在XGBoost on Spark中,0值會因為底層數據存儲結構的不同,同時會有兩種含義,而底層的存儲結構是完全由數據集決定的。

因為線上Serving時,只能設置一個缺失值,因此被選為SparseVector格式的測試集,可能會導致線上Serving時,計算結果與期望結果不符。

問題解決

查了一下XGBoost on Spark的最新源碼,依然沒解決這個問題。

趕緊把這個問題反饋給XGBoost on Spark, 同時修改了我們自己的XGBoost on Spark代碼。

scalaval 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 values = features match {//SparseVector的數據,先轉成Densecase v: SparseVector => v.toArray.map(_.toFloat)case v: DenseVector => v.values.map(_.toFloat)}XGBLabeledPoint(label, , values, baseMargin = baseMargin, weight = weight)}scala/*** Converts a [[Vector]] to a data point with a dummy label.** Thisis needed for constructing a [[ml.dmlc.xgboost4j.scala.DMatrix]]* for prediction.*/def asXGB: XGBLabeledPoint = v match {case v: DenseVector =>XGBLabeledPoint(0.0f, , v.values.map(_.toFloat))case v: SparseVector =>//SparseVector的數據,先轉成DenseXGBLabeledPoint(0.0f, , v.toArray.map(_.toFloat))}問題得到解決,而且用新代碼訓練出來的模型,評價指標還會有些許提升,也算是意外之喜。

希望本文對遇到XGBoost缺失值問題的同學能夠有所幫助,也歡迎大家一起交流討論。

技術的道路一個人走著極為艱難?

一身的本領得不施展?

優質的文章得不到曝光?

(*本文為AI科技大本營轉載文章,轉載請聯繫原作者)

相關焦點

  • 大戰三回合:XGBoost、LightGBM和Catboost一決高低
    XGBoost(eXtreme Gradient Boosting) 特點是計算速度快,模型表現好,可以用於分類和回歸問題中,號稱「比賽奪冠的必備殺器」。事實上,CatBoost 的文檔明確地說明不要在預處理期間使用熱編碼,因為「這會影響訓練速度和最終的效果」;(3)通過執行有序地增強操作,可以更好地處理過度擬合,尤其體現在小數據集上;(4)支持即用的 GPU 訓練(只需設置參數task_type =「GPU」);(5)可以處理缺失的值
  • 資料| 陳天奇介紹Xgboost原理的PPT
    【 圖片來源:https://xgboost.apachecn.org/  所有者:https://xgboost.apachecn.org/ 】內容簡介陳天奇介紹Xgboost原理的PPT,用於學習XGBoost提供並行樹提升(也稱為GBDT,GBM),可以快速準確地解決許多數據科學問題。相同的代碼在主要的分布式環境(Hadoop,SGE,MPI)上運行,並且可以解決數十億個示例之外的問題。
  • XGboost算法在不同作業系統中安裝方法乾貨分享
    安裝Xgboost方法最近小編想安裝一下xgboost軟體包,用pip install xgboost 安裝有問題,今天將對主流作業系統下的安裝方法進行了匯總,用到的時候拿來即用,省事省力,內容包括三大主流作業系統的,其他系統的沒有環境,暫時不列舉。
  • wxPython + PyOpenGL 打造三維數據分析的利器!|CSDN 博文精選
    比 Vispy,雖然支持以 wx 或者 Qt 作為後端,但綁定後端以後,在窗口管理、交互操作等方面還是存在不少問題。PyOpenGl 做得更簡單,提供一個 GLUT 庫就算是對 UI 的支持了。事實上,在複雜的三維展示系統中,UI 的重要性並不亞於 OpenGL。如果能為 OpenGL 找到一位 UI 搭檔,必將提高程序的可靠性和可操作性,增強用戶感受。
  • 中信建投證券:xgboost中證500指數增強7月持倉組合發布
    xgboost模型是一種強學習模型,其是由眾多弱學習模型集成,其採用弱學習模型為CART,即分類與回歸樹。該模型重在擬合特徵和標籤間的非線性關係。組成該模型的眾多弱學習器之間的關係是補充彌補的關係,弱學習器的訓練有先後,每個新的弱學習器的學習目標都是之前已訓練好的弱學習器的殘差。
  • 計算機大數乘法引發的思考 | CSDN 博文精選
    但是這裡面有個根本的問題,猜猜看是什麼?一位乘法對於人類而言是可以直接計算的,99乘法表都會背,我們計算4×7的時候,沒有必要擺4排的7,然後數一數一共有多少,而是脫口而出28。對於人類而言,超過一位的數字乘法就屬於大數了,人們不會把12×89這種計算的結果背下來,那就需要某種技巧去拆解多位數字,利用巧算來減少計算步驟了。換句話說, 超過一位的十進位乘法計算,對於人類而言,就需要動用算法了。
  • 卡方檢驗結果分析專題及常見問題 - CSDN
    R語言卡方檢驗與結果可視化1,卡方分析簡介與實例2,R語言chisq.test()3,基於ggstatsplot包的可視化分析卡方分析簡介與實例:卡方檢驗是生物學中應用很廣的一種假設檢驗,可以通過對構成比,率進行檢驗,進而判斷分類資料間的偏差程度。
  • XGBoost 重要關鍵參數及調優步驟
    3. max_depth [default=6]樹的最大深度,值越大,樹越大,模型越複雜 可以用來防止過擬合,典型值是3-10。4. gamma [default=0, alias: min_split_loss]分裂節點時,損失函數減小值只有大於等於gamma節點才分裂,gamma值越大,算法越保守,越不容易過擬合,但性能就不一定能保證,需要平衡。
  • |CSDN 博文精選
    1994年,數學家彼得·肖爾在理論計算機頂級會議FOCS上提出一種可以在量子計算機上實現的量子算法[2],該算法可以在多項式時間內解決大整數分解問題,這使得基於經典計算複雜性理論的現代密碼學算法(如RSA)變得不再安全。同時,由於該算法的提出,使人們看到量子計算機的巨大應用前景,從而開啟了量子計算機的研究熱潮。
  • 從結構到性能,一文概述XGBoost、Light GBM和CatBoost的同與不同
    從那時開始,我就對這些算法的內在工作原理非常好奇,包括調參及其優劣勢,所以有了這篇文章。儘管最近幾年神經網絡復興,並變得流行起來,但我還是更加關注 boosting 算法,因為在訓練樣本量有限、所需訓練時間較短、缺乏調參知識的場景中,它們依然擁有絕對優勢。
  • 計算機大數乘法引發的思考|CSDN 博文精選
    但是這裡面有個根本的問題,猜猜看是什麼?…一位乘法對於人類而言是可以直接計算的,99乘法表都會背,我們計算4×7的時候,沒有必要擺4排的7,然後數一數一共有多少,而是脫口而出28。對於人類而言,超過一位的數字乘法就屬於大數了,人們不會把12×89這種計算的結果背下來,那就需要某種技巧去拆解多位數字,利用巧算來減少計算步驟了。
  • 數據的預處理基礎:如何處理缺失值
    圖片來源: thermofisher數據集缺少值? 讓我們學習如何處理:數據清理/探索性數據分析階段的主要問題之一是處理缺失值。 缺失值表示未在觀察值中作為變量存儲的數據值。 這個問題在幾乎所有研究中都是常見的,並且可能對可從數據得出的結論產生重大影響。
  • 第五十三講 R-缺失值的注意事項及處理
    在數據分析過程中,我們經常會遇到缺失值的情況。比如要研究血壓、血糖、胰島素水平、懷孕次數與糖尿病的關係。我們需要使用多元邏輯回歸。但是可能數據有10%的血壓確實,10%的胰島素水平缺失,10%的血糖缺失。
  • 一個框架解決幾乎所有機器學習問題
    我最近也在準備參加 Kaggle,之前看過幾個例子,自己也總結了一個分析的流程,今天看了這篇文章,裡面提到了一些高效的方法,最乾貨的是,他做了一個表格,列出了各個算法通常需要訓練的參數。這個問題很重要,因為大部分時間都是通過調節參數,訓練模型來提高精度。作為一個初學者,第一階段,最想知道的問題,就是如何調節參數。
  • 如何評價周志華深度森林模型,熱議會否取代深度學習 DNN
    【新智元導讀】昨天,新智元報導了南京大學周志華教授和馮霽的論文「深度森林」,引發很多討論。今天,新智元整理了網上一些評價。中文內容來自知乎,已經取得授權。外網內容來自 Hacker News,由新智元編譯整理。正在看這篇文章的你,也歡迎留下你的看法。
  • XGBoost之切分點算法
    最後比較不同block塊結構最優切分點的目標函數下降值,選擇下降最大的特徵作為最優切分點。流程圖如下:切分點算法之分位點算法若特徵是連續值,按照上述的貪婪算法,運算量極大 。當樣本量足夠大的時候,使用特徵分位點來切分特徵。流程圖如下:
  • sklearn 神經網絡預測專題及常見問題 - CSDN
    >>> dnn_betaarray([0.86741717, 0.87501334, 0.90299861, 0.84398677, 0.85943336])>>> dnn_beta.mean()0.8697698493525869使用xgboost>>> import xgboost
  • |CSDN博文精選
    上網一搜,發現這個問題好像是 Python 的專屬問題,其他語言很難用一行代碼做點什麼。知乎上有一篇名為《一行 Python 能實現什麼喪心病狂的功能?》(https://www.zhihu.com/question/37046157)的帖子,其鏡像貼只有 Java 的和 JS 的,點進去發現,和 Python 的完全不是一個概念。