XGBoost解析系列-原理


0.前言

  解析源碼之前,還是介紹說明下XGBoost原理,網上對於XGBoost原理已有各種版本的解讀。而這篇博客,筆者主要想根據自己的理解,梳理看過的XGBoost資料,包括陳天奇的論文以及引用論文內容,本文主要內容基於陳天奇的論文與PPT,希望能夠做到系統地介紹XGBoost,同時加入源碼新特性讓內容上有增量。

  XGBoost不僅能在單機上通過OMP實現高度並行化,還能通過MPI接口與近似分位點算法(論文中是weighted quantiles sketch)實現高效的分佈式並行化。其中近似分位點算法(approximate quantiles)會附加一篇博客進行詳細說明,分位點算法在分佈式系統、流式系統中真的是個很天才的想法,很多分佈式算法的基石。最早由M.Greenwald和S. Khanna與2001年提出的GK Summay算法,直到到2007年被Q. Zhang和W. Wang提出的多層level的merge與compress/prune框架進行高度優化,而被稱爲A fast algorithm for approximate quantiles,詳情見下一篇博客。

1.Boosting算法框架

  XGBoost算法屬於集成學習中的boosting分支,其算法框架遵循1999年Friedman提出的boosting框架,該分支還有GBDT(Gradient Boosting Decision Tree),boosting集成是後一個模型是對前一個模型產生誤差信息進行矯正。gradient boost更具體,新模型的引入是爲了減少上個模型的殘差(residual),我們可以在殘差減少的梯度(Gradient)方向上建立一個新的模型。Friedman論文中針對迴歸過程提出boost框架如下:


這裏寫圖片描述

  Friedman提出boost算法框架過程描述如下:

  1. 設定函數初始值F0 ,爲一個恆值函數,論文中基於變量優化出恆值,實際上也可以給定任意值或者直接爲0。
  2. 根據參數M ,進行M 次迭代,不斷將當前函數Fm1 往最優函數F 空間上逼近,逼近方向就是當前函數下的函數負梯度方向L(y,F)F=Fm1 。由於優化函數,而非變量,本質上屬於泛函優化。
  3. 每次迭代計算出函數負梯度,基於訓練數據構建模型來擬合負梯度。原則上可以選擇任何模型:樹模型,線性模型或者神經網絡等等,很少框架支持神經網絡,推測:神經網絡容易過擬合,後續函數負梯度恆爲0就無法繼續迭代優化下去。如果用樹模型進行擬合,就是我們熟悉的CART建樹過程。
  4. 優化步長,根據目標函數來最優步長ρm ,屬於變量優化,並更新當前函數,繼續迭代。框架並沒有shrinkage機制來控制過擬合,採用樹模型和線性模型也可能過度擬合,目前現代的boosting框架都支持shrinkage,即最終的優化步長應乘以shrinkage參數:ρm=ρmγ

  該框架實際上是泛函梯度下降優化過程,儘管中間局部包含變量優化步驟,對比變量優化迭代不難發現相似之處。準確來說適合變量優化的其他策略同樣適合泛函優化:1)基於梯度下降優化,步長優化可以是精確優化和非精確優化。2)基於牛頓法,根據二階梯度直接計算步長f(x)1 ,即更新變量xn+1=xnf(x)f(x) ,本質上XGBoost屬於牛頓法,而且加入正則化,二階導數恆大於0;3)擬牛頓法,用於二階不可導時情況等等

  談到集成學習,不得不說bagging集成,比如隨機森林,1)建樹前對樣本隨機抽樣(行採樣),2)每個特徵分裂隨機採樣生成特徵候選集(列採樣),3)根據增益公式選取最優分裂特徵和對應特徵分裂值建樹。建樹過程完全獨立,不像boosting訓練中下一顆樹需要依賴前一顆樹訓練構建完成,因此能夠完全並行化。Python機器學習包sklearn中隨機森林RF能完全並行訓練,而GBDT算法不行,訓練過程還是單線程,無法利用多核導致速度慢。希望後續優化實現並行,Boosting並行不是同時構造N顆樹,而是單顆樹構建中遍歷最優特徵時的並行,類似XGBoost實現過程。隨機森林中行採樣與列採樣有效抑制模型過擬合,XGBoost也支持這2種特性,此外其還支持Dropout抗過擬合。

2. XGBoost原理推導

  1. XGBoost考慮正則化項,目標函數定義如下:

L(ϕ)=il(yi,ŷ i)+kΩ(fk)Ω(fk)=γT+12λ||w||2

   其中ŷ i 爲預測輸出,yi 爲label值,fk 爲第k 樹模型,T 爲樹葉子節點數, w 爲葉子權重值,γ 爲葉子樹懲罰正則項,具有剪枝作用,λ 爲葉子權重懲罰正則項,防止過擬合。XGBoost也支持一階正則化,容易優化葉子節點權重爲0,不過不常用。

  根據Boosting框架,可以優化出樹的建模函數ft(x)

L(t)=i=1l(yi,ŷ (t1)i+ft(xi))+Ω(ft)i=1n[l(yi,ŷ (t1)i)+gift(xi)+12htf2t(xi)]+Ω(ft)=i=1n[gift(xi)+12htf2t(xi)]+Ω(ft)+constant

  2. 因此,每次建樹優化以下目標:
L̂ (t)=i=1n[gift(xi)+12htf2t(xi)]+Ω(ft)

  其中gi=ŷ (t1)il(yi,ŷ (t1)i)hi=2ŷ (t1)il(yi,ŷ (t1)i) ,而且:

Ω(ft)=γT+12λj=1Tw2j

  3. 假設我們已知樹結構q ,即每個樣本xi 能通過該結構q 找到對應的葉子節點j ,可以定義 Ij={i|q(xi)=j} 爲在樹結構q 下,落入葉子節點j 所有樣本序號的集合。展開上述表達式並通過配方法不難得到:

L̂ (t)=i=1n[gift(xi)+12htf2t(xi)]+Ω(ft)=i=1n[gift(xi)+12htf2t(xi)]+γT+12λj=1Tw2j=j=1T[(iIjgi)wj+12(iIjhi+λ)w2j]+γT=12j=1T(Hj+λ)(wj+GjHj+λ)2+γT12j=1TG2jHj+λ

  其中Gj=iIjgi 爲落入葉子i 所有樣本一階梯度統計值總和, Hj=iIjhi 爲落入葉子i 所有樣本二階梯度統計值總和。最終得到葉子權重值爲:

wj=GjHj+λ

  4. 最終的目標值爲:
L̂ =12j=1TG2jHj+λ+γT

  下圖爲樹的目標值計算樣例:

這裏寫圖片描述

   5. 回顧步驟3,可以發現前提假設是已知樹結構q ,除非遍歷所有樹結構,否則無法優化最優目標值,而且爲了優化目標值,我們也不可能遍歷所有樹結構。論文提出了貪婪的算法,類似於CART定義增益公式來啓發式的尋找最優樹結構,若當前樹結構I 能被分裂成ILIRI=ILIR ,XGBoost的增益公式:
Lsplit=12[G2LHL+λ+G2RHR+λ(GL+GR)2HL+HR+λ]γ

3. XGBoost算法

1)XGBoost精確貪婪算法

  構建樹流程如下:1.遍歷每個特徵k ,2)遍歷當前特徵k 下每個取值xjk ,對於特徵分裂值將前節點樣本樣本劃分到左右子樹,根據上述公式通過計算增益,選取增益最大對應的特徵以及特徵分裂值,執行節點分裂,Lsplit 最大值小於0則停止分裂,γ 可以視爲分裂閾值,起到一定程度的預剪枝的作用,再不斷重複。下圖爲根據特徵值排序,從左到右進行掃描來找出當前特徵下最優分裂值。


這裏寫圖片描述

  論文提出的精確貪婪算法流程如下:


這裏寫圖片描述

2)XGBoost近似算法

  精確算法由於需要遍歷特徵的所有取值,計算效率低,適合單機小數據,對於大數據、分佈式場景並不適合。論文基於Weighted Quantile Sketch分位點算法提出相應的近似算法,也證明了該分位點的正確性。通過設置ϵ 來設置近似程度,而且論文給出近似算法的2種方案:

  1. 在建樹之前預先將數據進行全局分桶,需要設置更小的ϵ ,產生更多的桶,特徵分裂查找基於候選點多,計算較慢,但只需在全局執行一次。
  2. 每次分裂重新局部分桶,可以設置較大的ϵ ,產生更少的桶,每次特徵分裂查找基於候選點少,計算速度快,但是需要每次節點分裂後重新執行,論文中說該方案更適合樹深的場景。
  論文給出Higgs案例下,方案1全局分桶設置ϵ=0.05 與精確算法效果差不多,方案2局部分桶設置ϵ=0.3 與精確算法僅稍差點,方案1全局分桶設置ϵ=0.3 則效果極差。

  近似算法爲什麼能用於分佈式?主要原因是分桶是基於分位點算法,分位點算法支持merge和prune操作,想了解該過程可以移步《分位點算法詳解》,而且XGBoost場景屬於weighted分位點算法,作者在論文後面也證明weighted分位點算法支持merge和prune操作,因此適合與分佈式場景。近似算法主要對數據分佈進行分桶,同時希望每個桶儘量均勻。考慮數據集:

Dk={(x1k,h1),(x2k,h2),(xnk,hn)}

  定義rank函數爲rk:R[0,+) , 二階導數hi 一定大於等於0,而一階導數gi 則不具備該條件,所以無法構建分位點。實際上XGBoost源代碼不僅會構建hi 的分位點,也會對gi 進行拆分,分別構建gi>0 集合分位點和gi<0 集合分位點(取負),目前按照論文中僅考慮二階導數統計值hi

rk(z)=1(x,h)Dkh(x,h)Dk,x<zh

  rk(z) 表示特徵值小於z 的樣本集合中,h 累計值的百分佔比。在這個排序函數下,我們找到一組點sk1,sk2,...,skl ,滿足:
|rk(sk,j)rk(sk,j+1)|<ε,sk1=minixik,skl=maxixik

  上述條件1爲均勻條件,條件2爲邊界條件。這樣就能得到1/ε 個特徵值分割候選點,假設數據量爲1kw,設置ϵ=0.01 ,則由候選點1kw降低爲100,速度提升10w倍,  論文提出的精確貪婪算法流程如下:

這裏寫圖片描述

3)XGBoost近似算法

  對於數據缺失數據、one-hot編碼等造成的特徵稀疏現象,作者在論文中提出可以處理稀疏特徵的分裂算法,主要是對稀疏特徵值miss的樣本學習出默認節點分裂方向:
  1. 默認miss value進右子樹,對non-missing value的樣本在左子樹的統計值GLHL ,右子樹爲GGLHHL ,其中包含miss的樣本。
  2. 默認miss value進左子樹,對non-missing value的樣本在右子樹的統計值GRHR ,左子樹爲GGRHHR ,其中包含miss的樣本。
  最後,找出增益最大對於的特徵、特徵對於的值、以及miss value的分裂方向,作者在論文中提出基於稀疏分裂算法:


這裏寫圖片描述

4. XGBoost工程優化

內部數據存儲格式

  從算法上看,每種算法都依賴特徵排序,然後掃描,爲了減少特徵排序,XGBoost引入一種名爲block的數據存儲結構,將數據存儲在內存單元,並對每一種特徵進行排序。block中的數據以CSC格式存儲。實際上源代碼中XGBoost會把文件數據讀入先生成CSR格式,然後轉化爲CSC格式。其中CSR格式如下:


這裏寫圖片描述

  CSR包含非0數據塊values,行偏移offsets,列下標indices。offsets數組大小爲(總行數目+1),CSR是對稠密矩陣的壓縮,實際上直接訪問稠密矩陣元素(i,j) 並不高效,畢竟損失部分信息,訪問過程如下:

  1. 根據行i 得到偏移區間開始位置offsets[i]與區間結束位置offsets[i+1]-1,得到i 行數據塊values[offsets[i]..(offsets[i+1]-1)], 與非0的列下表indices[offsets[i]..(offsets[i+1]-1)],
  2. 在列下標數據塊中二分查找j ,找不到則返回0,否則找到下標值k ,返回values[offsets[i]+k]

  從訪問單個元素來說,從O(1) 時間複雜度升到O(logN) , N爲該行非稀疏數據項個數。但是如果要遍歷訪問整行非0數據,則無需訪問indices數組,時間複雜度反而更低,因爲少了大量的稀疏爲0的數據訪問。

   CSC與CSR變量結構上並無差別,只是變量意義不同,其中values仍然爲非0數據塊,offsets爲列偏移,即特徵id對應數組,indices爲行下標,對應樣本id數組,XBGoost使用CSC主要用於對特徵的全局預排序。預先將CSR數據轉化爲無序的CSC數據,遍歷每個特徵,並對每個特徵i 進行排序:sort(&values[offsets[i]], &values[offsets[i+1]-1])。全局特徵排序後,後期節點分裂可以複用全局排序信息,而不需要重新排序。

Cache-aware Access

  CSC存儲優化會導致獲取每個樣本獲取統計值而不連續,造成樣本計算cache不斷切換而導致cache-miss,XGBoost通過選擇適當的block size來緩存數據解決小樣本量帶來的資源浪費以及大樣本量帶來的cache-miss之間的權衡問題,XGBoost選擇的block size爲216

Out-of-core Computation

  XGBoost中提出Out-of-core Computation優化,解決了在硬盤上讀取數據耗時過長,吞吐量不足:

  1)Block Compression基於block,數據分塊,每塊216 個樣例,使用16bit來存儲offset。利用壓縮算法將硬盤中的數據進行壓縮,在讀取數據進內存的過程中利用一個獨立的線程對數據進行解壓縮,將disk reading cost轉換爲解壓縮所消耗的計算資源。

  2)Block Sharding將數據shard到多塊硬盤上,每塊硬盤分配一個預取線程,將數據fetche到in-memory buffer中。訓練線程交替讀取多塊buffer,提升了硬盤總體的吞吐量。

5. XGBoost算法複雜度

  針對精確貪婪算法,考慮數據樣本量爲N ,特徵數量爲M , 設置樹的個數爲K , 樹深爲D ,不考慮行採樣與列採樣,其時間複雜度分析如下:
  1. 全局特徵預排序,由於全局排序,後期節點再分裂可以複用全局排序信息,而不需要重新排序,因此排序複雜度爲O(MNlog(N))
  2. 構建單樹複雜度:由於XGBoost實現基於level-wise, 每層的時間複雜度是爲O(MN) ,K顆樹複雜度爲O(KMND)
  3. 最終時間複雜度爲:O(MNlog(N)) + O(KMND) , 注意:跟論文的分析不同,主要按照筆者的理解,後期仔細分析後,如果有出入會修正。


參考資料

  1. Friedman Boosting框架論文:https://statweb.stanford.edu/~jhf/ftp/trebst.pdf
  2. 陳天奇XGBoost論文:https://arxiv.org/pdf/1603.02754.pdf
  3. XGBoost項目:https://github.com/dmlc/xgboost
  4. GK Summary算法論文:http://infolab.stanford.edu/~datar/courses/cs361a/papers/quantiles.pdf
  5. A fast algorithm for approximate quantiles論文: https://pdfs.semanticscholar.org/03a0/f978de91f70249dc39de75e8958c49df4583.pdf
  6. wepon GDBT ppt:http://202.38.196.91/cache/2/03/wepon.me/5aa84bcab4e621a09cc475c348590c35/gbdt.pdf
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章