XGBoost原理解析


摘要

XGBoost是GBDT的一個高效實現,本文對xgboost的實現細節進行記錄。

算法原理

 正則化損失(regularized loss objective)

為避免過擬合,xgb使用帶正則化項的損失函數。正則化項包含兩部分:樹的葉子節點個數和每個葉子節點的分數。

 梯度樹提升(gradient tree boosting)

在第t步,損失函數可表示為:

 對損失函數進行二階泰勒展開:

 不考慮常數項,loss可簡化為:

 上式是損失的instance-wise的表示形式,為方便優化,可整理為以下leaf-wise的表示形式:

 對於固定的樹結構,為最小化loss,計算loss對w的導數,令導數為0,可得葉子節點的最優分值為:

將損失函數展開到二階是計算w的關鍵,如果展開到一階,那么就退化到gbdt了。

注意,在自定義loss時,loss函數必須為凸函數,只有這樣,上述計算w最優值才有意義!

 計算最優的w之后,可進一步計算最小loss為:

 上式可作為對樹結構q的評分函數。

理論上無法遍歷所有可能的樹結構,XGB采用的貪心法,每一次分裂,均最大化loss reduction,也即:

 收縮和列采樣(shrinkage and column sampling)

除了對損失函數進行正則化外,xgb還是用收縮和列采樣進一步防止過擬合。shrinkage對每棵樹的權重乘以因子η,和學習率的作用類似。列采樣和隨機森林類似。

分裂點搜索(split finding)

精確算法(exact greedy)

精確算法對數據進行線性掃描,遍歷每一個可能的分裂點。

近似算法(approximate)

當數據不能一次裝入內存或數據分散在多台機器時,精確算法往往效率不高,可使用近似算法。

近似算法的核心在於提出一系列候選分裂點,然后將數據划分為buckets,並計算每個bucket的梯度值(一階、二階)的和。然后基於buckets的梯度和計算最優分裂點。

分為global proposal和local proposal兩類。global proposal在單棵樹構建之前生成,每次迭代均使用該proposal,計算量相對較小,但為保證精度,proposal中需包含足夠多的候選分裂點。而local proposal是在每個葉子節點進一步分裂時實時生成,計算量較大,但無須包含像global proposal那么多的候選分裂點。local proposal可能更適合很深的樹。

加權分位數(weighted quantile sketch)

在計算分位數時,xgb使用二階梯度對樣本進行加權。大致思想是將所有樣本划分為k(≈1/ε)個bucket,每個bucket內樣本的二階梯度之和近似相等,具體描述見論文。至於為什么這樣做,可以將損失函數重寫為以下形式,其中gi/hi是t-1時刻在強學習器上樣本i的一二階梯度,在t時刻是一個常數。因此損失值是每個樣本以-gi/hi為標簽的加權方差損失,權重為hi。 

注意,原文中上式有誤。括號內應為ft-(-g/h), 原文為ft-g/h。

 稀疏感知(sparsity awareness)

為解決稀疏特征問題,為每個樹節點選擇默認的方向,如果某個樣本的對應特征缺失,那么直接划入默認方向。

系統設計

基於column block的並行

樹學習最耗時的部分通常是對數據進行排序,為了降低排序帶來的計算負荷,xgb使用基於block的結構對數據進行存儲。每個block中的數據以compressed format(CSC)格式存儲,每列按照數值大小進行排序。這樣的數據結構僅需要在訓練前計算一次,在后續的迭代過程中,可以重復使用。

對於精確算法,將所有數據保存在一個block中。建樹的過程可以實現特征級別的並行,即每個線程處理一個特征。在單個線程內部,對數據的單次掃描可計算所有葉子節點在該特征上的最優分裂點。

對於近似算法,可以將不同的數據(按行)分布在不同的block中,並可以將不同的block分布到不同的機器。使用排好序的數據,quantile finding算法只需要線性掃描數據即可完成。

利用這樣的結構,在對單個特征尋找分裂點時,也可以實現並行化,這也是xgb能夠進行分布式並行的關鍵。

緩存感知(cache aware access)

block結構降低了計算的復雜度,但也帶來了另外一個問題,那就是對梯度信息的讀取是不連續的,如果梯度信息不能全部裝進cache,這會導致cpu緩存命中率降低,從而影響性能。

在精確算法中,xgb使用cache-aware prefetch算法緩解這個問題。也就是為每個線程分配一個buffer,將梯度信息讀入buffer,並以mini-batch的方式進行梯度累加。

對於近似算法,解決緩存命中失效的方法是選擇合適的block size。過小的block size導致單線程的計算負載過小,並行不充分,過大的block size又會導致cache miss,因此需要做一個平衡。論文建議使用的block size為2^16。

核外計算(out of core computation)

為進行核外計算,將數據划分為多個block。計算期間,使用獨立線程將磁盤上的block結構數據讀進內存,因此計算和IO可以同步進行。然而,這只能解決一部分問題,因為IO占用的時間要遠多於計算。因此,xgb使用以下兩種方式優化:

數據壓縮(block compression):將block按列進行壓縮,並使用獨立線程解壓縮。這樣可以對IO和CPU的占用時間進行對沖。

數據分區(block sharding):將數據分散到多個磁盤,每個磁盤使用一個線程讀取數據,以此提高磁盤吞吐量。

單機精確算法實現

在XGBoost的實現中,對算法進行了模塊化的拆解,幾個重要的部分分別是:
I. ObjFunction:對應於不同的Loss Function,可以完成一階和二階導數的計算。
II. GradientBooster:用於管理Boost方法生成的Model,注意,這里的Booster Model既可以對應於線性Booster Model,也可以對應於Tree Booster Model。
III. Updater:用於建樹,根據具體的建樹策略不同,也會有多種Updater。比如,在XGBoost里為了性能優化,既提供了單機多線程並行加速,也支持多機分布式加速。也就提供了若干種不同的並行建樹的updater實現,按並行策略的不同,包括:
I). inter-feature exact parallelism (特征級精確並行)
II). inter-feature approximate parallelism(特征級近似並行,基於特征分bin計算,減少了枚舉所有特征分裂點的開銷)
III). intra-feature parallelism (特征內並行)
IV). inter-node parallelism (多機並行)
此外,為了避免overfit,還提供了一個用於對樹進行剪枝的updater(TreePruner),以及一個用於在分布式場景下完成結點模型參數信息通信的updater(TreeSyncher),這樣設計,關於建樹的主要操作都可以通過Updater鏈的方式串接起來,比較一致干凈,算是Decorator設計模式的一種應用。
XGBoost的實現中,最重要的就是建樹環節,而建樹對應的代碼中,最主要的也是Updater的實現。所以我們會以Updater的實現作為介紹的入手點。
以ColMaker(單機版的inter-feature parallelism,實現了精確建樹的策略)為例,其建樹操作大致如下:
updater_colmaker.cc:
ColMaker::Update()
     -> Builder builder;
     -> builder.Update()
          -> InitData()
          -> InitNewNode() // 為可用於split的樹結點(即葉子結點,初始情況下只有一個
                           // 葉結點,也就是根結點) 計算統計量,包括gain/weight等
          ->  for (depth = 0; depth < 樹的最大深度; ++depth)
               -> FindSplit()
                    -> for (each feature) // 通過OpenMP獲取
                                          // inter-feature parallelism
                         -> UpdateSolution()      
                              -> EnumerateSplit()  // 每個執行線程處理一個特征,
                                                   // 選出每個特征的
                                                   // 最優split point
                              -> ParallelFindSplit()   
                                   // 多個執行線程同時處理一個特征,選出該特征
                                   //的最優split point; 
                                   // 在每個線程里匯總各個線程內分配到的數據樣
                                   //本的統計量(grad/hess);
                                   // aggregate所有線程的樣本統計(grad/hess),       
                                   //計算出每個線程分配到的樣本集合的邊界特征值作為
                                   //split point的最優分割點;
                                   // 在每個線程分配到的樣本集合對應的特征值集合進
                                   //行枚舉作為split point,選出最優分割點
                         -> SyncBestSolution()  
                               // 上面的UpdateSolution()/ParallelFindSplit()
                               //會為所有待擴展分割的葉結點找到特征維度的最優split 
                               //point,比如對於葉結點A,OpenMP線程1會找到特征F1 
                               //的最優split point,OpenMP線程2會找到特征F2的最
                               //優split point,所以需要進行全局sync,找到葉結點A
                               //的最優split point。
                         -> 為需要進行分割的葉結點創建孩子結點     
               -> ResetPosition() 
                      //根據上一步的分割動作,更新樣本到樹結點的映射關系
                      // Missing Value(i.e. default)和非Missing Value(i.e. 
                      //non-default)分別處理
               -> UpdateQueueExpand() 
                      // 將待擴展分割的葉子結點用於替換qexpand_,作為下一輪split的
                      //起始基礎
               -> InitNewNode()  // 為可用於split的樹結點計算統計量

ColMaker的整個建樹操作中,最tricky的地方應該是用於支持intra-feature parallelism的ParallelFindSplit(),關於這個計算邏輯,上面有一些文字描述,輔助下圖可能會更為直觀:

XGB vs GBDT

  • 傳統GBDT以CART作為基分類器,xgboost還支持線性分類器,這個時候xgboost相當於帶L1和L2正則化項的邏輯斯蒂回歸(分類問題)或者線性回歸(回歸問題)。
  • 傳統GBDT在優化時只用到一階導數信息,xgboost則對代價函數進行了二階泰勒展開,同時用到了一階和二階導數。順便提一下,xgboost工具支持自定義代價函數,只要函數可一階和二階求導。
  • xgboost在代價函數里加入了正則項,用於控制模型的復雜度。正則項里包含了樹的葉子節點個數、每個葉子節點上輸出的score的L2模的平方和。從Bias-variance tradeoff角度來講,正則項降低了模型的variance,使學習出來的模型更加簡單,防止過擬合,這也是xgboost優於傳統GBDT的一個特性。
  • Shrinkage(縮減),相當於學習速率(xgboost中的eta)。xgboost在進行完一次迭代后,會將葉子節點的權重乘上該系數,主要是為了削弱每棵樹的影響,讓后面有更大的學習空間。實際應用中,一般把eta設置得小一點,然后迭代次數設置得大一點。(補充:傳統GBDT的實現也有學習速率)
  • 列抽樣(column subsampling)。xgboost借鑒了隨機森林的做法,支持列抽樣,不僅能降低過擬合,還能減少計算,這也是xgboost異於傳統gbdt的一個特性。
  • 對缺失值的處理。對於特征的值有缺失的樣本,xgboost可以自動學習出它的分裂方向。
  • xgboost工具支持並行。xgboost的並行不是tree粒度的並行,xgboost也是一次迭代完才能進行下一次迭代的(第t次迭代的代價函數里包含了前面t-1次迭代的預測值)。xgboost的並行是在特征粒度上的。我們知道,決策樹的學習最耗時的一個步驟就是對特征的值進行排序(因為要確定最佳分割點),xgboost在訓練之前,預先對數據進行了排序,然后保存為block結構,后面的迭代中重復地使用這個結構,大大減小計算量。這個block結構也使得並行成為了可能,在進行節點的分裂時,需要計算每個特征的增益,最終選增益最大的那個特征去做分裂,那么各個特征的增益計算就可以開多線程進行。
  • 可並行的近似直方圖算法。樹節點在進行分裂時,我們需要計算每個特征的每個分割點對應的增益,即用貪心法枚舉所有可能的分割點。當數據無法一次載入內存或者在分布式情況下,貪心算法效率就會變得很低,所以xgboost還提出了一種可並行的近似直方圖算法,用於高效地生成候選的分割點.

參考鏈接

https://arxiv.org/pdf/1603.02754.pdf

http://www.ra.ethz.ch/CDstore/www2011/proceedings/p387.pdf

https://stats.stackexchange.com/questions/202858/xgboost-loss-function-approximation-with-taylor-expansion

https://www.zhihu.com/question/41354392

https://weibo.com/p/1001603801281637563132


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM