(2020.4.9)再次閱讀的時候,大致梳理了一下行文的思路。
- Xgb原始論文先介紹了其損失函數,(2020.4.21跟進)損失函數用來指導每顆樹的生成,也就是決定了在給定數據情況下,葉子節點的最優分裂方式。
- 其次是如果更新CART樹的結構(也就是特征的划分方式),論文提出了一個基於貪心策略的特征划分方法以及近似估計特征分裂點的方法,也是文章的亮點之一
XGBoost是boosting算法的其中一種。Boosting算法的思想是將許多弱分類器集成在一起形成一個強分類器,其更關注與降低基模型的偏差。XGBoost是一種提升樹模型(Gradient boost machine),其將許多樹模型集成在一起,形成一個很強的分類器。而所用到的樹模型則是CART回歸樹模型。講解其原理前,先講解一下CART回歸樹。
一、CART回歸樹
CART回歸樹中定義樹為二叉樹,通過GINI增益函數選定最優划分屬性。由於CART為二叉樹,與其他決策樹相比其在選擇了最優分類特征之后,還需要選擇一個最優的特征值划分點。比如當前樹結點是基於第j個特征值進行分裂的,設該特征值小於s的樣本划分為左子樹,大於s的樣本划分為右子樹。
CART回歸樹實質上就是在該特征維度對樣本空間進行划分,而這種空間划分的優化是一種NP難問題,因此,在決策樹模型中是使用啟發式方法解決。典型CART回歸樹產生的目標函數為:
因此,當我們為了求解最優的切分特征j和最優的切分點s,就轉化為求解這么一個目標函數:
所以我們只要遍歷所有特征的的所有切分點,就能找到最優的切分特征和切分點。最終得到一棵回歸樹。
二、XGBoost基本思想
XGBoost的核心思想與GBM一致,其在實現的過程中通過不斷地添加新的樹(基模型),不斷地進行特征分裂來生長一棵樹,每次添加一個樹,其實是學習一個新函數,用來擬合上次預測的偽殘差。當我們訓練完成得到k棵樹,我們要預測一個樣本的分數,其實就是根據這個樣本的特征,在每棵樹中會落到對應的一個葉子節點,每個葉子節點就對應一個分數,最后只需要將每棵樹對應的分數加起來就是該樣本的預測值。
其中q(x)對應的是CART的結構,簡單來說指的是x在某一個CART樹葉子節點的位置信息.$W_{q(x)}$表示輸入x對於某棵CART樹的分數。T是樹中葉子節點的個數,每個$\mathrm{f}_k(x_i)$對於的是一個CART樹,由樹的結構q以及葉子節點值W確定。如下圖例子,訓練出了2棵決策樹,小孩的預測分數就是兩棵樹中小孩所落到的結點的分數相加。爺爺的預測分數同理。
三、XGBoost原理
了解一個學習器,主要是要去理解其損失函數,XGBoost損失函數定義為:
其中第一項為常規損失函數,第二項目為正則項目,用來限制模型的復雜度,降低過擬合的風險。正則化項同樣包含兩部分,T表示葉子結點的個數,w表示葉子節點的分數。γ、λ為參數分別控制CART樹的個數、葉子節點的分數值。
XGBoost采用的也是加性模型的方式,形式化之后的損失函數如下圖所示:
對於表達形式比較復雜,不易理解的函數我們可以嘗試使用泰勒展開的技巧,注意到$f_t(xi)$對於的是泰勒展開的$\Delta{x}$項(泰勒展開不清楚的話em....),展開之后的損失函數如下所示:
其中$g_i$為$l(y_i,\widetilde{y}^{t-1})$的一階導,$h_i$為其二階導。由於前t-1棵樹的預測分數與y的殘差為常數,對目標函數優化不影響,可以直接去掉。簡化目標函數為:
我們定義$I_j = (i|q(x_i) =j)$,$I_j$表示葉子節點對應的輸入實例集合,將損失函數項展開之后得到:
這里簡單的解釋一下,$\sum_{t=1}^ng_if_t(x_i)$與$\sum_{j=1}^T\sum_{i\in{I_j}}g_iw_j$是等價的,前者求的是所有實例對應在各個CART樹中分數的加權和(注意T指代的是葉子節點的個數別搞混淆了),后者也是如此,不過為了合並公式,換了一種表示方式罷了。對上述損失函數求導得:
回代之后得到最優情況的表達式,也是XGboost中的用來評估一顆CART數的標准函數:
我們記$\sum_{i\in{I}}gi = G$,$\sum_{i\in{I}}h_i = H$則上式可表述為:
文中還提到了Shrinkage以及subsample技術,Shrinkage與學習率衰減類似,隨着迭代次數的增加基函數的權值不斷減少,目的是減少每顆樹的影響給后續的基函數留夠空間,提高模型的robust。subsample在后續記錄RF的時候在細說吧。
四、分裂結點算法
在上述推導過程中,我們明確了在清晰的知道一顆樹的結構之后,如何求得每個葉子結點的分數。但我們還沒介紹如何確定樹結構,即每次特征分裂怎么尋找最佳特征,怎么尋找最佳分裂點。基於空間切分去構造一顆決策樹是一個NP難問題,我們不可能去遍歷所有樹結構,因此,XGBoost使用了和CART回歸樹一樣的想法,利用貪婪算法,遍歷所有特征的所有特征划分點,不同的是使用上式目標函數值作為評價函數。具體做法就是分裂后的目標函數值比單子葉子節點的目標函數的增益,同時為了限制樹生長過深,還加了個閾值,只有當增益大於該閾值才進行分裂。同時可以設置樹的最大深度、當樣本權重和小於設定閾值時停止生長去防止過擬合。論文中提到了一個貪心選擇分裂的算法,具體的做法是預先對特征值進行排序,這樣一次遍歷過程中通過對前綴和以及后綴和的計算便可以覆蓋所有的情況,算法流程如圖所示:
五、近似算法
對於連續型特征值,當樣本數量非常大,該特征取值過多時,遍歷所有取值會花費很多時間,且容易過擬合。因此XGBoost思想是先根據求得的特征權重(feature_importance 根據gini增益指數算出來的)對特征排序獲取一些分裂候選點,之后根據候選點對特征進行分桶,即找到l個划分點,將位於相鄰分位點之間的樣本分在一個桶中。在遍歷該特征的時候,只需要遍歷各個分位點,從而計算最優划分。從算法偽代碼中該流程還可以分為兩種,全局的近似是在新生成一棵樹之前就對各個特征計算分位點並划分樣本,之后在每次分裂過程中都采用近似划分,而局部近似就是在具體的某一次分裂節點的過程中采用近似算法。
六、按權分位算法(weighted quantile sketch)
如何選取近似划分候選點?10000個樣本取10個的話,每1000個樣本計算一次split value看似可行,其實是不可取的,我們要均分的是loss,而不是樣本的數量,而每個樣本對loss的貢獻可能是不一樣的,按樣本均分會導致loss分布不均勻,取到的分位點會有偏差。論文中定義了一個rank函數:
$\mathrm{D}_k =\{(x_1k,h_1),...(x_nk,h_n)\}$rank(z)指的是一個特征的特征值集合中,特征值x小於z的樣本中二階導之和所在總樣本二階導和的比例,也就是用二階導占比例加權。(2020.3.4)加一些解釋,首先給一個公式:
這個公式是和損失函數等價的,前面$h_{i}$就是某個樣本的二階導,后面$g_{i} / h_{i}$是個常數,所以$h_{i}$也就是二階導可以看做計算殘差時某個樣本的重要性。因為我們每個節點,要均分的是loss,而不是樣本的數量,而每個樣本對loss的貢獻可能是不一樣的,按樣本均分會導致loss分布不均勻,取到的分位點會有偏差。加上權重,就能夠避免在候選區間里面重要性高的節點扎堆,類似於下圖:
ok,繼續~論文通過定義這個rank函數用來選取候選的分割點:
接下來我們對原始的損失函數進行改寫:
通過這個函數大致解釋了解釋了為什么用二階導$h_i$作為權重是合理的。關於這個算法的證明我並沒有仔細的去看附錄的證明,論文中的符號是有一點問題的,參照別人的探討以及自己的推論做了一點修改,到目前位置我的理解是這個算法是一種能夠給定權重情況下,尋找候選分桶點的算法。算法圖也先不貼了,看后面怎么理解。
七、針對稀疏數據的算法(缺失值處理)
當樣本的第i個特征值缺失時,無法利用該特征進行划分時,XGBoost的作法不是采取取周圍特征值平均值的平滑方式,而是將該樣本分別划分到左結點和右結點,然后計算其增益,哪個大就划分到哪邊。從而確定一個默認的划分方向。也就是說當特征值不可取或者確實的時候,XGBoost會訓練出一個合適的默認划分方式,如下圖所示:
具體的算法流程如下所示:
八、分塊並行
在建樹的過程中,XGBoost開銷最大的部分在每一層選擇最優點的過程中,論文中提到的選擇最優點的兩個算法都需要對數據進行排序,如此多的排序過程是有一些冗余的,論文設計了一個Column Block的數據結構,數據結構存儲了每個block中存儲了一個特征對應的一個特征值,如果預先對特征值排序,那么排序的開銷就可以節省下來。(空間換時間了,想起了acm摸魚的時候常用的數據預處理打表.....)。使用了Columu Block結構之后,在選擇分裂點的算法中,我們可以開CPU多核對多個葉子節點的最優分裂點進行並行計算(選擇最優分裂點需要的就是排序好了的特征值以及 feature_importance,這兩個都能夠在預處理的過程中預先算出來),這樣對Block進行一次掃描就可以獲取所有葉子節點的分裂情況。Block中的特征還需要存儲指向對應樣本的index,這樣算法實現的過程中才能對特征的指來獲取對應計算的梯度。如下圖所示:
對於approximate算法而言,Xgboost使用了多個Block,存在多個機器上或者磁盤中。每個Block對應原來數據的子集。不同的Block可以在不同的機器上計算。該方法對Local策略尤其有效,因為Local策略每次分支都重新生成候選切分點。Block結構還有其它好處,數據按列存儲,可以同時訪問所有的列,很容易實現並行的尋找分裂點算法。缺點是空間消耗大了一倍。時間復雜度的分析不難理解我就偷個懶不再這里多說了。
九、緩存優化
這里涉及到一點點os的內容,也不是很多不清楚cache機制的簡單百度一下就可以了。使用Block結構的一個缺點是取梯度的時候,是通過索引來獲取的,而這些梯度的獲取順序是按照特征的大小順序的。這將導致非連續的內存訪問,可能使得CPU cache緩存命中率低,從而影響算法效率。如下圖所示:
因此,對於exact greedy算法中, 使用緩存預取(cache-aware prefetching)。具體來說,對每個線程分配一個連續的buffer,讀取梯度信息並存入Buffer中(這樣就實現了非連續到連續的轉化),然后再統計梯度信息。該方式在訓練樣本數大的時候特別有用,如下圖所示:
可以看到,對於大規模數據,效果十分明顯。在approximate 算法中,對Block的大小進行了合理的設置。定義Block的大小為Block中最多的樣本數。設置合適的大小是很重要的,設置過大則容易導致命中率低,過小則容易導致並行化效率不高。經過實驗,發現2^16比較好。如下圖:
Reference: