Xgboost是GBDT算法的高效實現,在工業界的傳統算法中,Xgboost幾乎占據了半壁江山。這里,我們將深度探討xgboost原理以及其高效實現。
原理部分參考集成學習
目標函數
事實上,如果不考慮工程實現、解決問題上的一些差異,xgboost與gbdt比較大的不同就是目標函數的定義。xgboost的目標函數如下所示:
通過二階泰勒展開,可得:
其中:
泰勒展開:$f(x+\Delta x)\approx f(x)+f'(x)\Delta x+\frac{1}{2}f''(x) {\Delta x}^2 $;在展開時 $ x $ 對應目標函數里的 $ \hat{y}_i^{(t-1)} $, $ \Delta x $對應 $ f_t(x_i) $
最終的目標函數只依賴於每個數據點在誤差函數上的一階導數和二階導數。
另外,對CART樹正則項作一番定義:
需要解釋下這個定義,首先,一棵樹有\(T\)個葉子節點,這\(T\)個葉子節點的值組成了一個\(T\)維向量\(w\),\(q(x)\)是一個映射,用來將樣本映射成1到\(T\)的某個值,也就是把它分到某個葉子節點,\(q(x)\)其實就代表了CART樹的結構。\(w_q(x)\)自然就是這棵樹對樣本\(x\)的預測值了。
xgboost使用了如下的正則化項:
Note:其中, \(T\)表示葉子節點的個數,\(w\)表示葉子節點的分數 。也就是說,正則項包括葉子結點的數量和葉子結點權重向量的L2范數.
直觀上看,目標要求預測誤差盡量小,且葉子節點\(T\)盡量少(\(γ\)控制葉子結點的個數),節點數值\(w\)盡量不極端(\(λ\)控制葉子節點的分數不會過大),防止過擬合。
至此,我們關於第t棵樹的優化目標已然很清晰,下面我們對它做如下變形
其中\(I_j\)代表一個集合,集合中每個值代表一個訓練樣本的序號,整個集合就是被第\(t\)棵CART樹分到了第\(j\)個葉子節點上的訓練樣本。
進一步,我們可以做如下簡化:
其中,\(G_j\)代表葉子結點 \(j\) 所包含樣本的一階偏導數累加之和,是一個常量;\(Hj\)代表葉子結點 \(j\) 所包含樣本的二階偏導數累加之和,是一個常量。
通過對\(w\)求導等於0,可以得到:
實質是把樣本分配到葉子結點會對應一個obj,優化過程就是obj優化。也就是分裂節點到葉子不同的組合,不同的組合對應不同obj,所有的優化圍繞這個思想展開。
\(Obj\)代表了當我們指定一個樹的結構的時候,我們在目標上面最多減少多少。我們可以把它叫做 結構分數(structure score) 。
分裂節點
對於一個葉子節點如何進行分裂,xgboost作者在其原始論文中給出了兩種分裂節點的方法。
枚舉所有不同樹結構的貪心法
貪心法,即從樹深度0開始,每一節點都遍歷所有的特征,比如年齡、性別等等,然后對於某個特征,先按照該特征里的值進行排序,然后線性掃描該特征進而確定最好的分割點,最后對所有特征進行分割后,我們選擇所謂的增益Gain最高的那個特征,而Gain如何計算呢?
在上面我們得到
其中,目標函數中的\(\frac{G_j^2}{H_j+\lambda}\)部分,表示着每一個葉子節點對當前模型損失的貢獻程度,融合一下,得到Gain的計算表達式,如下所示:
另外,要注意“對於某個特征,先按照該特征里的值進行排序”。比如設置一個值a,然后枚舉所有\(x < a\)、\(a < x\)這樣的條件(\(x\)代表某個特征比如年齡age,把age從小到大排序:假定從左至右依次增大,則比\(a\)小的放在左邊,比\(a\)大的放在右邊),對於某個特定的分割\(a\),我們要計算\(a\)左邊和右邊的導數和。
第二個值得注意的事情就是引入分割不一定會使得情況變好,所以我們有一個引入新葉子的懲罰項。優化這個目標對應了樹的剪枝, 當引入的分割帶來的增益小於一個閥值\(γ\)的時候,則忽略這個分割。
下面是論文中的算法
但當數據量過大導致內存無法一次載入或者在分布式情況下,貪心算法的效率就會變得很低,全局掃描法不再適用。
基於此,XGBoost提出了一系列加快尋找最佳分裂點的方案:
- 特征預排序+緩存:XGBoost在訓練之前,預先對每個特征按照特征值大小進行排序,然后保存為block結構,后面的迭代中會重復地使用這個結構,使計算量大大減小。
- 分位點近似法:對每個特征按照特征值排序后,采用類似分位點選取的方式,僅僅選出常數個特征值作為該特征的候選分割點,在尋找該特征的最佳分割點時,從候選分割點中選出最優的一個。即近似算法。
- 並行查找:由於各個特性已預先存儲為block結構,XGBoost支持利用多個線程並行地計算每個特征的最佳分割點,這不僅大大提升了結點的分裂速度,也極利於大規模訓練集的適應性擴展。
近似算法
主要針對數據太大,不能直接進行計算。在尋找splitpoint的時候,不會枚舉所有的特征值,而會對特征值進行聚合統計,按照特征值的密度分布,構造直方圖計算特征值分布的面積,然后划分分布形成若干個bucket(桶),每個bucket的面積相同,將bucket邊界上的特征值作為split point的候選,遍歷所有的候選分裂點來找到最佳分裂點。
近似算法首先按照特征取值的統計分布的一些百分位點確定一些候選分裂點,然后算法將連續的值映射到 buckets中,然后匯總統計數據,並根據聚合統計數據在候選節點中找到最佳節點。近似算法有兩個變體, global variant和 local variant。
把樣本從根分配到葉子結點,就是個排列組合。不同的組合對應的cost不同。求最好的組合你就要try,一味窮舉是不可能的,所以才出來貪婪法。不看從頭到尾 就看當下節點怎么分配最好。這才有了那個exact greddy方法,后來還想加速才有了histogram的做法。
總而言之,XGBoost使用了和CART回歸樹一樣的想法,利用貪婪算法,遍歷所有特征的所有特征划分點,不同的是使用的目標函數不一樣。具體做法就是分裂后的目標函數值比單子葉子節點的目標函數的增益,同時為了限制樹生長過深,還加了個閾值,只有當增益大於該閾值才進行分裂。
停止生長
一棵樹不會一直生長下去,下面是一些常見的限制條件。
(1) 當新引入的一次分裂所帶來的增益Gain<0時,放棄當前的分裂。這是訓練損失和模型結構復雜度的博弈過程。
(2) 當樹達到最大深度時,停止建樹,因為樹的深度太深容易出現過擬合,這里需要設置一個超參數max_depth。
(3) 當引入一次分裂后,重新計算新生成的左、右兩個葉子結點的樣本權重和。如果任一個葉子結點的樣本權重低於某一個閾值,也會放棄此次分裂。這涉及到一個超參數:最小樣本權重和,是指如果一個葉子節點包含的樣本數量太少也會放棄分裂,防止樹分的太細,這也是過擬合的一種措施。
特征重要性排名
使用梯度提升算法的好處是在提升樹被創建后,可以相對直接地得到每個屬性的重要性得分。一般來說,重要性分數,衡量了特征在模型中的提升決策樹構建中價值。一個屬性越多的被用來在模型中構建決策樹,它的重要性就相對越高。
屬性重要性是通過對數據集中的每個屬性進行計算,並進行排序得到。在單個決策書中通過每個屬性分裂點改進性能度量的量來計算屬性重要性,由節點負責加權和記錄次數。也就說一個屬性對分裂點改進性能度量越大(越靠近根節點),權值越大;被越多提升樹所選擇,屬性越重要。性能度量可以是選擇分裂節點的Gini純度,也可以是其他度量函數。
最終將一個屬性在所有提升樹中的結果進行加權求和后然后平均,得到重要性得分。
對於所選擇的度量,官方有五種方案:
- ‘weight’: the number of times a feature is used to split the data across all trees.
- ‘gain’: the average gain across all splits the feature is used in.
- ‘cover’: the average coverage across all splits the feature is used in.
- ‘total_gain’: the total gain across all splits the feature is used in.
- ‘total_cover’: the total coverage across all splits the feature is used in.
下面用一個例子來說明。假設有10個樣例的樣本,每個樣例有兩維特征\(f_0\)與\(f_1\),標簽為0或1,做二分類問題。訓練時只用一棵樹,得到xgboost結果如下:
結合這張圖,解釋下各指標含義:
- weight: \(\{‘f_0’: 1, ‘f_1’: 2 \}\)。在所有樹中,某特征被用來分裂節點的次數,在本例中,可見分裂第1個節點時用到\(f_0\),分裂第2,3個節點時用到\(f_1\),所以weight_\(f_0\) = 1, weight_\(f_1\) = 2。
- total_cover: \(\{‘f_0’: 10.0, ‘f_1’: 8.0\}\)。第1個節點,\(f_0\)被用來對所有10個樣例進行分裂,之后的節點中\(f_0\)沒再被用到,所以\(f_0\)的total_cover為10.0,此時\(f_0\) >= 0.855563045的樣例有5個,落入右子樹;第2個節點,\(f_1\)被用來對上面落入右子樹的5個樣例進行分裂,其中\(f_1\) >= -0.178257734的樣例有3個,落入右子樹;第3個節點,\(f_1\)被用來對上面落入右子樹的3個樣例進行分裂。總結起來,\(f_0\)在第1個節點分裂了10個樣例,所以total_cover_\(f_0\) = 10,\(f_1\)在第2、3個節點分別用於分裂5、3個樣例,所以total_cover_\(f_1\) = 5 + 3 = 8。total_cover表示在所有樹中,某特征在每次分裂節點時處理(覆蓋)的所有樣例的數量。
- cover: \(\{‘f_0’: 10.0, ‘f_1’: 4.0\}\)。cover = total_cover / weight,在本例中,cover_\(f_0\) = 10 / 1,cover_\(f_1\) = 8 / 2 = 4.
- total_gain: \(\{‘f_0’: 0.265151441, ‘f_1’: 0.75000003\}\)在所有樹中,某特征在每次分裂節點時帶來的總增益,如果用熵或基尼不純衡量分裂前后的信息量分別為i0和i1,則增益為(i0 - i1)。
- gain: \(\{‘f_0’: 0.265151441, ‘f_1’: 0.375000015\}\)
gain = total_gain / weight,在本例中,gain_\(f_0\) = 0.265151441 / 1,gain_\(f_1\) = 75000003 / 2 = 375000015.
在平時的使用中,多用total_gain來對特征重要性進行排序。
工程實現
單機實現
對XGBoost的源碼進行走讀分析之后,能夠看到下面的主流程:
cli_main.cc:
main()
-> CLIRunTask()
-> CLITrain()
-> DMatrix::Load()
-> learner = Learner::Create()
-> learner->Configure()
-> learner->InitModel()
-> for (i = 0; i < param.num_round; ++i)
-> learner->UpdateOneIter()
-> learner->Save()
learner.cc:
Create()
-> new LearnerImpl()
Configure()
InitModel()
-> LazyInitModel()
-> obj_ = ObjFunction::Create()
-> objective.cc
Create()
-> SoftmaxMultiClassObj(multiclass_obj.cc)/
LambdaRankObj(rank_obj.cc)/
RegLossObj(regression_obj.cc)/
PoissonRegression(regression_obj.cc)
-> gbm_ = GradientBooster::Create()
-> gbm.cc
Create()
-> GBTree(gbtree.cc)/
GBLinear(gblinear.cc)
-> obj_->Configure()
-> gbm_->Configure()
UpdateOneIter()
-> PredictRaw()
-> obj_->GetGradient()
-> gbm_->DoBoost()
gbtree.cc:
Configure()
-> for (up in updaters)
-> up->Init()
DoBoost()
-> BoostNewTrees()
-> new_tree = new RegTree()
-> for (up in updaters)
-> up->Update(new_tree)
tree_updater.cc:
Create()
-> ColMaker/DistColMaker(updater_colmaker.cc)/
SketchMaker(updater_skmaker.cc)/
TreeRefresher(updater_refresh.cc)/
TreePruner(updater_prune.cc)/
HistMaker/CQHistMaker/
GlobalProposalHistMaker/
QuantileHistMaker(updater_histmaker.cc)/
TreeSyncher(updater_sync.cc)
從上面的代碼主流程可以看到,在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設計模式[4]的一種應用。
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會找到特征$f_1$
//的最優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的樹結點計算統計量
整個操作流程還是比較直觀,上面直接在代碼塊級別的介紹可能過於detail,稍微抽象一些的流程圖描述如下:
單機版本的實現中,另一個比較重要的細節是對於稀疏離散特征的支持,在這方面,XGBoost的實現還是做了比較細致的工程優化考量。在XGBoost里,對於稀疏性的離散特征,在尋找split point的時候,不會對該特征為missing的樣本進行遍歷統計,只對該列特征值為non-missing的樣本上對應的特征值進行遍歷,通過這個工程trick來減少了為稀疏離散特征尋找split point的時間開銷。在邏輯實現上,為了保證完備性,會分別處理將missing該特征值的樣本分配到左葉子結點和右葉子結點的兩種情形。
在XGBoost里,單機多線程,並沒有通過顯式的pthread這樣的方式來實現,而是通過OpenMP來完成多線程的處理,這可能跟XGBoost里多線程的處理邏輯相對簡單,沒有復雜的線程之間同步的需要,所以通過OpenMP可以支持得比較好,也簡化了代碼的開發和維護負擔。
單機實現中,另一個重要的updater是TreePruner,這是一個為了減少overfit,在loss函數的正則項之外提供的額外正則化手段,實現邏輯也比較直觀,對於已經構造好的Tree結構,判斷每個葉子結點,如果這個葉子結點的父結點分裂所帶來的loss變化小於配置文件中規定的閾值,就會把這個葉子結點和它的兄弟結點合並回父結點里,並且這個pruning操作會遞歸下去。
上面介紹的是精確的建模算法,在XGBoost中,出於性能優化的考慮,也提供了近似的建模算法支持,核心思想是在尋找split point的時候,不會枚舉所有的特征值,而會對特征值進行聚合統計,然后形成若干個bucket,只將bucket邊界上的特征值作為split point的候選,從而獲得性能提升。
分布式實現
關於XGBoost的分布式實現,一共提供了兩種支持,一種基於RABIT,另一種則基於Spark。其中XGBoost4j的底層通信實際上還是用到了RABIT。
Distributed XGBoost里針對核心算法分布式的主要邏輯還是基於RABIT完成的,XGBoost4j更像是在RABIT-based XGBoost上做了一層wrapper,工程量並不小,但是涉及到XGBoost核心算法的分布式細節並不多,所以后續的介紹,我也會主要cover基於RABIT的 XGBoost分布式實現。
把握Distributed XGBoost,需要從計算任務的調度管理和核心算法分布式實現這兩個角度展開。
計算任務的調度管理,在RABIT里提供了native MPI/Sun Grid Engine/YARN這三種方式。native MPI這種方式,實際上除了計算任務的調度管理以外,也提供了相應的通信原語(在RABIT里,針對native MPI這種任務管理方式,只是在MPI_allreduce/MPI_broadcast這兩個通信原語上做了一層簡單的wrapper),所以更像一個純粹的MPI計算任務,在這里我也不打算詳述。XGboost on YARN這種模式涉及到的細節則最多,包括YARN ApplicationMaster/Client的開發、Tracker腳本的開發、RABIT容錯通信原語的開發以及基於RABIT原語的XGBoost算法分布式實現,會是我介紹的重點。下面這張鳥噉圖有助於建立起XGBoost on YARN的整體認識。
在這個圖里,有幾個重要的角色,分別介紹一下。
I. Tracker:這其實是一個Python寫的腳本程序,主要完成的工作有
- I). 啟動daemon服務,提供worker結點注冊聯接所需的end point,所有的worker結點都可以通過與Tracker程序通信來完成自身狀態信息的注冊
- II). co-ordinate worker結點的執行:為worker結點分配Rank編號。 基於收到的worker注冊信息完成網絡結構的構建,並廣播給worker結點,以確保worker結點之間建立起合規的網絡拓撲。 當所有的worker結點都建立起完備的網絡拓撲關系以后,就可以啟動計算任務監控整個執行過程。
II. Application Master:這其實是基於YARN AM接口的一個實現,完成的就是常規的YARN Application Master的功能,此處不再多述。
III. Client:這其實是基於YARN Client接口的一個實現。
IV. Worker:對應於實際的計算任務,本質上,每個worker結點(在YARN里應該稱之為一個容器,因為一個結點上可以啟動多個YARN容器)里都會啟動一個XGBoost進程。這些XGBoost進程在初始化階段,會通過與Tracker之間通信,完成自身信息的注冊,同時會從Tracker里獲取到完整的網絡結構信息,從而完成通信所需的網絡拓撲結構的構建。
V. RABIT Library:RABIT實現的通信原語,目前只支持allreduce和broadcast這兩個原語,並且提供了一定的fault-tolerance支持(RABIT通信框架中存在Tracker這個單點,所以只能在一定程度上支持Worker上的錯誤異常,基本的實現套路是,基於YARN的failure recovery機制,對於transient network error以及硬件down機這樣的異常都提供了一定程度的支持)。
VI. XGBoost Process:在單機版的邏輯之外,還提供了用於Worker之間通信的相關邏輯,主要的通信數據包括:樹模型的最新參數(從Rank 0結點到其他結點)每次分裂葉子結點時,為了計算最優split point,所需從各個結點匯總的統計量,包括近似算法里為了propose split point所需的bucket信息、訓練樣本的梯度信息等(從其他結點到Rank 0結點) XGBoost4j的實現,我就不再詳述,本質上就是一個XGBoost YARN的Spark wrapper,示意圖:
從上圖可以看出,在XGBoost4j里,XGBoost的分布式邏輯其實還是通過RABIT來完成的,並且是通過RabitTracker完成任務的co-ordination。
高頻面試題
簡單介紹一下XGBoost
首先需要說一說GBDT,它是一種基於boosting增強策略的加法模型,訓練的時候采用前向分布算法進行貪婪的學習,每次迭代都學習一棵CART樹來擬合之前 t-1 棵樹的預測結果與訓練樣本真實值的殘差。
XGBoost對GBDT進行了一系列優化,比如損失函數進行了二階泰勒展開、目標函數加入正則項、支持並行和默認缺失值處理等,在可擴展性和訓練速度上有了巨大的提升,但其核心思想沒有大的變化。
XGBoost與GBDT有什么不同
- 基分類器:XGBoost的基分類器不僅支持CART決策樹,還支持線性分類器,此時XGBoost相當於帶L1和L2正則化項的Logistic回歸(分類問題)或者線性回歸(回歸問題)。
- 導數信息:XGBoost對損失函數做了二階泰勒展開,GBDT只用了一階導數信息,並且XGBoost還支持自定義損失函數,只要損失函數一階、二階可導。
- 正則項:XGBoost的目標函數加了正則項, 相當於預剪枝,使得學習出來的模型更加不容易過擬合。
- 列抽樣:XGBoost支持列采樣,與隨機森林類似,用於防止過擬合。
- 缺失值處理:對樹中的每個非葉子結點,XGBoost可以自動學習出它的默認分裂方向。如果某個樣本該特征值缺失,會將其划入默認分支。
- 並行化:注意不是tree維度的並行,而是特征維度的並行。XGBoost預先將每個特征按特征值排好序,存儲為塊結構,分裂結點時可以采用多線程並行查找每個特征的最佳分割點,極大提升訓練速度。
XGBoost為什么使用泰勒二階展開
- 精准性:相對於GBDT的一階泰勒展開,XGBoost采用二階泰勒展開,可以更為精准的逼近真實的損失函數
- 可擴展性:損失函數支持自定義,只需要新的損失函數二階可導。
XGBoost為什么可以並行訓練
- XGBoost的並行,並不是說每棵樹可以並行訓練,XGB本質上仍然采用boosting思想,每棵樹訓練前需要等前面的樹訓練完成才能開始訓練。
- XGBoost的並行,指的是特征維度的並行:在訓練之前,每個特征按特征值對樣本進行預排序,並存儲為Block結構,在后面查找特征分割點時可以重復使用,而且特征已經被存儲為一個個block結構,那么在尋找每個特征的最佳分割點時,可以利用多線程對每個block並行計算。
XGBoost為什么快
- 分塊並行:訓練前每個特征按特征值進行排序並存儲為Block結構,后面查找特征分割點時重復使用,並且支持並行查找每個特征的分割點
- 候選分位點:每個特征采用常數個分位點作為候選分割點
- CPU cache 命中優化: 使用緩存預取的方法,對每個線程分配一個連續的buffer,讀取每個block中樣本的梯度信息並存入連續的Buffer中。
- Block 處理優化:Block預先放入內存;Block按列進行解壓縮;將Block划分到不同硬盤來提高吞吐
XGBoost防止過擬合的方法
XGBoost在設計時,為了防止過擬合做了很多優化,具體如下:
- 目標函數添加正則項:葉子節點個數+葉子節點權重的L2正則化
- 列抽樣:訓練的時候只用一部分特征(不考慮剩余的block塊即可)
- 子采樣:每輪計算可以不使用全部樣本,使算法更加保守
- shrinkage: 可以叫學習率或步長,為了給后面的訓練留出更多的學習空間
XGBoost如何處理缺失值
XGBoost模型的一個優點就是允許特征存在缺失值。對缺失值的處理方式如下:
- 在特征k上尋找最佳 split point 時,不會對該列特征 missing 的樣本進行遍歷,而只對該列特征值為 non-missing 的樣本上對應的特征值進行遍歷,通過這個技巧來減少了為稀疏離散特征尋找 split point 的時間開銷。
- 在邏輯實現上,為了保證完備性,會將該特征值missing的樣本分別分配到左葉子結點和右葉子結點,兩種情形都計算一遍后,選擇分裂后增益最大的那個方向(左分支或是右分支),作為預測時特征值缺失樣本的默認分支方向。
- 如果在訓練中沒有缺失值而在預測中出現缺失,那么會自動將缺失值的划分方向放到右子結點。
XGBoost中葉子結點的權重如何計算出來
XGBoost目標函數最終推導形式如下:
利用一元二次函數求最值的知識,當目標函數達到最小值\(Obj^*\)時,每個葉子結點的權重為\(w_j^*\)。
具體公式如下:
RF和GBDT的區別
相同點:
- 都是由多棵樹組成,最終的結果都是由多棵樹一起決定。
不同點: - 集成學習:RF屬於bagging思想,而GBDT是boosting思想
- 偏差-方差權衡:RF不斷的降低模型的方差,而GBDT不斷的降低模型的偏差
- 訓練樣本:RF每次迭代的樣本是從全部訓練集中有放回抽樣形成的,而GBDT每次使用全部樣本
- 並行性:RF的樹可以並行生成,而GBDT只能順序生成(需要等上一棵樹完全生成)
- 最終結果:RF最終是多棵樹進行多數表決(回歸問題是取平均),而GBDT是加權融合
- 數據敏感性:RF對異常值不敏感,而GBDT對異常值比較敏感
- 泛化能力:RF不易過擬合,而GBDT容易過擬合
XGBoost如何處理不平衡數據
對於不平衡的數據集,例如用戶的購買行為,肯定是極其不平衡的,這對XGBoost的訓練有很大的影響,XGBoost有兩種自帶的方法來解決:
第一種,如果你在意AUC,采用AUC來評估模型的性能,那你可以通過設置scale_pos_weight來平衡正樣本和負樣本的權重。例如,當正負樣本比例為1:10時,scale_pos_weight可以取10;
第二種,如果你在意概率(預測得分的合理性),你不能重新平衡數據集(會破壞數據的真實分布),應該設置max_delta_step為一個有限數字來幫助收斂(基模型為LR時有效)。
源碼中通過增大了少數樣本的權重來平衡樣本。
比較LR和GBDT,說說什么情景下GBDT不如LR
先說說LR和GBDT的區別:
- LR是線性模型,可解釋性強,很容易並行化,但學習能力有限,需要大量的人工特征工程
- GBDT是非線性模型,具有天然的特征組合優勢,特征表達能力強,但是樹與樹之間無法並行訓練,而且樹模型很容易過擬合;
當在高維稀疏特征的場景下,LR的效果一般會比GBDT好。因為現在的模型普遍都會帶着正則項,而 LR 等線性模型的正則項是對權重的懲罰,也就是 W1一旦過大,懲罰就會很大,進一步壓縮 W1的值,使他不至於過大。但是,樹模型則不一樣,樹模型的懲罰項通常為葉子節點數和深度等,而如果一個特征能夠很好地划分正負樣本,樹只需要一個節點就可以完美分割樣本,一個結點,最終產生的懲罰項極其之小。
這也就是為什么在高維稀疏特征的時候,線性模型會比非線性模型好的原因了:帶正則化的線性模型比較不容易對稀疏特征過擬合。
XGBoost中如何對樹進行剪枝
- 在目標函數中增加了正則項:使用葉子結點的數目和葉子結點權重的L2模的平方,控制樹的復雜度。
- 在結點分裂時,定義了一個閾值,如果分裂后目標函數的增益小於該閾值,則不分裂。
- 當引入一次分裂后,重新計算新生成的左、右兩個葉子結點的樣本權重和。如果任一個葉子結點的樣本權重低於某一個閾值(最小樣本權重和),也會放棄此次分裂。
- XGBoost 先從頂到底建立樹直到最大深度,再從底到頂反向檢查是否有不滿足分裂條件的結點,進行剪枝。
XGBoost如何選擇最佳分裂點?
XGBoost在訓練前預先將特征按照特征值進行了排序,並存儲為block結構,以后在結點分裂時可以重復使用該結構。
因此,可以采用特征並行的方法利用多個線程分別計算每個特征的最佳分割點,根據每次分裂后產生的增益,最終選擇增益最大的那個特征的特征值作為最佳分裂點。
如果在計算每個特征的最佳分割點時,對每個樣本都進行遍歷,計算復雜度會很大,這種全局掃描的方法並不適用大數據的場景。XGBoost還提供了一種直方圖近似算法,對特征排序后僅選擇常數個候選分裂位置作為候選分裂點,極大提升了結點分裂時的計算效率。
XGBoost的Scalable性如何體現
- 基分類器的scalability:弱分類器可以支持CART決策樹,也可以支持LR和Linear。
- 目標函數的scalability:支持自定義loss function,只需要其一階、二階可導。有這個特性是因為泰勒二階展開,得到通用的目標函數形式。
- 學習方法的scalability:Block結構支持並行化,支持 Out-of-core計算。
XGBooost參數調優的一般步驟
首先需要初始化一些基本變量,例如:
max_depth = 5
min_child_weight = 1
gamma = 0
subsample, colsample_bytree = 0.8
scale_pos_weight = 1
(1) 確定learning rate和estimator的數量
learning rate可以先用0.1,用cv來尋找最優的estimators
(2) max_depth和 min_child_weight
我們調整這兩個參數是因為,這兩個參數對輸出結果的影響很大。我們首先將這兩個參數設置為較大的數,然后通過迭代的方式不斷修正,縮小范圍。
max_depth,每棵子樹的最大深度,check from range(3,10,2)。
min_child_weight,子節點的權重閾值,check from range(1,6,2)。
如果一個結點分裂后,它的所有子節點的權重之和都大於該閾值,該葉子節點才可以划分。
(3) gamma
也稱作最小划分損失min_split_loss,check from 0.1 to 0.5,指的是,對於一個葉子節點,當對它采取划分之后,損失函數的降低值的閾值。
如果大於該閾值,則該葉子節點值得繼續划分
如果小於該閾值,則該葉子節點不值得繼續划分
(4) subsample, colsample_bytree
subsample是對訓練的采樣比例
colsample_bytree是對特征的采樣比例
both check from 0.6 to 0.9
(5) 正則化參數
alpha 是L1正則化系數,try 1e-5, 1e-2, 0.1, 1, 100
lambda 是L2正則化系數
(6) 降低學習率
降低學習率的同時增加樹的數量,通常最后設置學習率為0.01~0.1
XGBoost模型如果過擬合了怎么解決
當出現過擬合時,有兩類參數可以緩解:
第一類參數:用於直接控制模型的復雜度。包括max_depth,min_child_weight,gamma 等參數
第二類參數:用於增加隨機性,從而使得模型在訓練時對於噪音不敏感。包括subsample,colsample_bytree
還有就是直接減小learning rate,但需要同時增加estimator 參數。
XGBoost和LightGBM的區別
(1)樹生長策略:XGB采用level-wise的分裂策略,LGB采用leaf-wise的分裂策略。XGB對每一層所有節點做無差別分裂,但是可能有些節點增益非常小,對結果影響不大,帶來不必要的開銷。Leaf-wise是在所有葉子節點中選取分裂收益最大的節點進行的,但是很容易出現過擬合問題,所以需要對最大深度做限制 。
(2)分割點查找算法:XGB使用特征預排序算法,LGB使用基於直方圖的切分點算法,其優勢如下:
- 減少內存占用,比如離散為256個bin時,只需要用8位整形就可以保存一個樣本被映射為哪個bin(這個bin可以說就是轉換后的特征),對比預排序的exact greedy算法來說(用int_32來存儲索引+ 用float_32保存特征值),可以節省7/8的空間。
- 計算效率提高,預排序的Exact greedy對每個特征都需要遍歷一遍數據,並計算增益,復雜度為𝑂(#𝑓𝑒𝑎𝑡𝑢𝑟𝑒×#𝑑𝑎𝑡𝑎)。而直方圖算法在建立完直方圖后,只需要對每個特征遍歷直方圖即可,復雜度為𝑂(#𝑓𝑒𝑎𝑡𝑢𝑟𝑒×#𝑏𝑖𝑛𝑠)。
- LGB還可以使用直方圖做差加速,一個節點的直方圖可以通過父節點的直方圖減去兄弟節點的直方圖得到,從而加速計算
但實際上xgboost的近似直方圖算法也類似於lightgbm這里的直方圖算法,為什么xgboost的近似算法比lightgbm還是慢很多呢?xgboost在每一層都動態構建直方圖, 因為xgboost的直方圖算法不是針對某個特定的feature,而是所有feature共享一個直方圖(每個樣本的權重是二階導),所以每一層都要重新構建直方圖,而lightgbm中對每個特征都有一個直方圖,所以構建一次直方圖就夠了。
(3)支持離散變量:無法直接輸入類別型變量,因此需要事先對類別型變量進行編碼(例如獨熱編碼),而LightGBM可以直接處理類別型變量。
(4)緩存命中率:XGB使用Block結構的一個缺點是取梯度的時候,是通過索引來獲取的,而這些梯度的獲取順序是按照特征的大小順序的,這將導致非連續的內存訪問,可能使得CPU cache緩存命中率低,從而影響算法效率。而LGB是基於直方圖分裂特征的,梯度信息都存儲在一個個bin中,所以訪問梯度是連續的,緩存命中率高。
(5)LightGBM 與 XGboost 的並行策略不同:
- 特征並行 :LGB特征並行的前提是每個worker留有一份完整的數據集,但是每個worker僅在特征子集上進行最佳切分點的尋找;worker之間需要相互通信,通過比對損失來確定最佳切分點;然后將這個最佳切分點的位置進行全局廣播,每個worker進行切分即可。XGB的特征並行與LGB的最大不同在於XGB每個worker節點中僅有部分的列數據,也就是垂直切分,每個worker尋找局部最佳切分點,worker之間相互通信,然后在具有最佳切分點的worker上進行節點分裂,再由這個節點廣播一下被切分到左右節點的樣本索引號,其他worker才能開始分裂。二者的區別就導致了LGB中worker間通信成本明顯降低,只需通信一個特征分裂點即可,而XGB中要廣播樣本索引。
- 數據並行 :當數據量很大,特征相對較少時,可采用數據並行策略。LGB中先對數據水平切分,每個worker上的數據先建立起局部的直方圖,然后合並成全局的直方圖,采用直方圖相減的方式,先計算樣本量少的節點的樣本索引,然后直接相減得到另一子節點的樣本索引,這個直方圖算法使得worker間的通信成本降低一倍,因為只用通信以此樣本量少的節點。XGB中的數據並行也是水平切分,然后單個worker建立局部直方圖,再合並為全局,不同在於根據全局直方圖進行各個worker上的節點分裂時會單獨計算子節點的樣本索引,因此效率賊慢,每個worker間的通信量也就變得很大。
- 投票並行(LGB):當數據量和維度都很大時,選用投票並行,該方法是數據並行的一個改進。數據並行中的合並直方圖的代價相對較大,尤其是當特征維度很大時。大致思想是:每個worker首先會找到本地的一些優秀的特征,然后進行全局投票,根據投票結果,選擇top的特征進行直方圖的合並,再尋求全局的最優分割點。
最后,附一份備忘單,希望能夠幫助大家系統化的掌握XGB原理的整個推導過程,同時又能夠起到快速回憶的作用。
參考:
知乎
Datawhale
PPT by wepon
xgboost官方網站
xgboost算法總結
XGBoost的python源碼實現
通俗理解kaggle比賽大殺器xgboost
機器學習競賽大殺器XGBoost--原理篇
xgboost特征重要性指標: weight, gain, cover