摘要
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):將數據分散到多個磁盤,每個磁盤使用一個線程讀取數據,以此提高磁盤吞吐量。
單機精確算法實現
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設計模式的一種應用。
以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的樹結點計算統計量

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