梯度提升機(Gradient Boosting Machine)之 LightGBM


隨着大數據時代的到來,GBDT正面臨着新的挑戰,特別是在精度和效率之間的權衡方面。傳統的GBDT實現需要對每個特征掃描所有數據實例,以估計所有可能的分割點的信息增益。因此,它們的計算復雜度將與特征數和實例數成正比。這使得這些實現在處理大數據時非常耗時。所以微軟亞洲研究院提出了 LightGBM ,其設計理念是:

  • 單個機器在不犧牲速度的情況下,盡可能使用上更多的數據
  • 多機並行的時候,通信的代價盡可能地低,並且在計算上可以做到線性加速。

LightGBM 與 XGBoost 相似,也是一種梯度提升機,但是與XGBoost不同的是,其選擇按葉生長(每一層只對一個節點進行分支),並且使用直方圖算法避免了每次尋找分割點時的排序操作,只需要在一開始對全部數據進行排序后找到分割點,每次尋找分割點時只需要簡單地分桶操作。同時其尋找最佳分割點的依據仍然是 XGBoost 中所提到的,根據一階導數和二階導數求出最佳的解和目標值,根據貪心算法窮舉所有分組,從而找出最佳分組,同時為了提高效率提出了兩個方法:

  • 單邊采樣:對於需要訓練的樣本給予重視,而不需要訓練的數據進行隨機采樣,同時為了保證減小對損失函數的影響對於隨機采集的數據予以權重。

  • 互斥特征融合:根據度(連接數,即與其他特征發生沖突的可能性)對其降序排序,使用貪心前向搜索算法,將沖突率小於要求值的特征進行綁定。然后使用直方圖進行橫向融合。

當前這里提出按特征值進行分桶與 XGBoost 中的分桶時根據二階導數進行排序的初衷相悖,是否真的存在沖突呢。歡迎討論😉。

決策樹學習算法(Decision Tree Learning Algorithm)

傳統的決策樹的生成方法有:按葉生長(Leaf-wise tree growth)和按層生長(Level-wise tree growth)兩種。

其中按層生長是將每一個節點都分割為兩個葉子節點。其雖然有天然的並行性,但是會有很多不必要的分裂產生,造成更多的計算代價。

在這里插入圖片描述

而按葉生長是只針對其中一個葉子節點進行子樹生長,並且對該節點進行分叉操作后損失值下降最多。

數學表達如下:

\[\begin{array} { l } \left( p _ { m } , f _ { m } , v _ { m } \right) = \arg \min _ { ( p , f , v ) } L \left( T _ { m - 1 } ( X ) . \text { split } ( p , f , v ) , Y \right) \\ T _ { m } ( X ) = T _ { m - 1 } ( X ) . \text { split } \left( p _ { m } , f _ { m } , v _ { m } \right) \end{array} \]

在這里插入圖片描述

在 LightGBM 中使用的是 leaf-wise 的方法,這樣的話在葉子個數一樣時,相對於 level-wise 有更高的精度,但是可能會導致生成較深的樹,所以 LightGBM 中也提出了限制最大深度來避免過擬合問題。

那么這種使用 leaf-wise tree growth 方法進行決策樹的學習的偽代碼如下:

\[\begin{array} { l } \text {Algorithm : DecisionTree} \\ \text {Input: Training data } ( X , Y ) , \text { number of leaf } C \text { , Loss function } l \\ \triangleright \text { put all data on root } \\ T _ { 1 } ( X ) = X \\ \text {For } m \text { in } ( 2 , C ) \text { : } \\ \qquad \begin{array} { l } \triangleright \text { find best split } \\ \left( p _ { m } , f _ { m } , v _ { m } \right) = \text { FindBestsplit } \left( X , Y , T _ { m - 1 } , l \right) \\ \triangleright \text { perform split } \\ T _ { m } ( X ) = T _ { m - 1 } ( X ) . \text { split } \left( p _ { m } , f _ { m } , v _ { m } \right) \end{array} \end{array} \]

其中計算消耗最多的地方是找出最佳的分割點,該分割點查找算法如下:

\[\begin{array} { l } \text {Algorithm : FindBestsplit } \\ \text { Input: Training data } ( X , Y ) , \text { Loss function } l \text { , Current Model } T _ { m - 1 } ( X ) \\ \text { For all Leaf } p \text { in } T _ { m - 1 } ( X ) \text { : } \\ \qquad \begin{array}{l} \text { For all } f \text { in X.Features: } \\ \qquad \begin{array}{l} \text { For all } v \text { in f.Thresholds: } \\ \qquad \begin{array}{l} ( \text {left, right} ) = \text {partition} ( p , f , v ) \\ \Delta \operatorname { loss } = L \left( X _ { p } , Y _ { p } \right) - L \left( X _ { \text {left} } , Y _ { \text {left} } \right) - L \left( X _ { \text {right} } , Y _ { \text {right} } \right) \\ \text {if } \Delta \text {loss} > \Delta \operatorname { loss } \left( p _ { m } , f _ { m } , v _ { m } \right) : \\ \left( p _ { m } , f _ { m } , v _ { m } \right) = ( p , f , v ) \end{array} \end{array} \end{array} \end{array} \]

那么 LightGBM 便是在此算法上進行的優化。第一個便是直方圖算法。

直方圖算法(Histogram Algorithm)

回顧 XGBoost 中,是使用預排序算法和加權分位數算法提出的估計分割法,什么意思呢?簡單來說就是對數據根據二階梯度值進行預排序,之后取其分位數 \(n * m\% (N*m=100,n=1,2,\cdots,N)\),作為代表或者說采樣后代表子集,對該子集窮舉選擇最優分割點。這樣的算法有兩個問題:

  1. 需要對每個特征按特征值進行排序
  2. 由於對特征進行了排序,但梯度並未排序,所以梯度值的獲取屬於隨機內存訪問。

這兩項都是極其消耗時間和空間的。

具體實現(Implementation)

在 LightGBM 中采用了更為高效的方法 —— 直方圖算法(Histogram algorithm)。什么意思呢?實際上就是對連續的浮點數據進行分桶操作,或者說離散為 k 個整數值。例如 \([ 0,0.1 ) \rightarrow 0,[ 0.1,0.3 ) \rightarrow 1\)。同時 LightGBM 對特征的每個桶進行梯度(一階和二階梯度)累加和個數統計。然后根據直方圖尋找最優點。下圖就是直方圖的獲取流程:


使用基於直方圖的尋找最優分割點時,需要 \(O ( \# \text { bin} \times \# \text { feature } )\) 的時間復雜度構建直方圖和 \(O ( \# \text { data } \times \# \text { feature } )\) 的時間復雜度尋找分割點。直方圖算法的偽代碼如下:

\[\begin{array} { l } \text { Input: } I : \text { training data, } d : \text { max depth } \\ \text { Input: } m : \text { feature dimension } \\ \text { nodeSet } \leftarrow \{ 0 \} \triangleright \text {tree nodes in current level } \\ \text { rowSet } \leftarrow \{ \{ 0,1,2 , \ldots \} \} \triangleright \text {data indices in tree nodes } \\ \text { for } i = 1 \text { to } d \text { do } \\ \qquad\begin{array}{l} \text {for node in nodeSet do } \\ \qquad \begin{array} { l } \text {usedRows } \leftarrow \text {rowSet} [ \text {node} ] \\ \text {for } k = 1 \text { to } m \text { do } \\ \qquad \begin{array}{l} H \leftarrow \text { new Histogram() } \\ \triangleright \text { Build histogram } \\ \text {for } j \text { in usedRows do } \\ \qquad \begin{array}{l} \text {bin } \leftarrow I . f [ \mathrm { k } ] [ \text { j].bin } \\ H [ \text { bin } ] . \mathrm { y } \leftarrow H [ \text { bin } ] . \mathrm { y } + \text { I.y } [ \mathrm { j } ] \\ H [ \text { bin } ] . \mathrm { n } \leftarrow H [ \text { bin } ] . \mathrm { n } + 1 \end{array} \\ \text {Find the best split on histogram } H \\ \cdots \end{array} \end{array} \end{array} \end{array} \]

分桶操作(Organization of Bins)

在偽代碼中的直方圖構建中,分桶操作並沒有體現,而是直接獲得了該特征值所對應的桶的編號。那這個編號是如何獲取的呢,或者說是如何進行分桶操作的呢?實際上這仍然需要一個排序操作,不過只需要在一開始做一步排序獲得分桶的分割點即可,之后便可以直接使用桶的分割點對每個特征進行分桶操作了。具體實現在數值類型和類別類型上又不一樣。下面介紹一下具體實現。

數值型特征:

  1. 對特征值去重后進行排序(從大到小)並統計每個特征值出現的次數 counts
  2. max_bindistinct_value.size 中的較小值作為 bins_num
  3. 計算每個桶可以分到的平均樣本個數 mean_bin_size,特征取值數 distinct_value.sizemax_bin 數量少,直接取distinct_values的中點作為桶間分割點,即無需分桶。反之則需要分桶,也就是說可能存在幾個特征值同分於一個桶中(多特征取值公用一個桶),但是有一點就是當該特征取值的計數值大於平均值 mean_bin_size 時,該特征取值需要單獨分桶,所以需要標記出符合該特點的全部特征,之后對不符合的重新計算 mean_bin_size
  4. 然后對於去重后的特征取值進行遍歷操作,如果當前的特征需要單獨成桶、或者當前桶中個特征計數超過了 mean_bin_size、或者下一個特征是需要獨立成桶的,那么當前的特征值將作為當前桶的上界,下一個桶的下界,也就是說需要本步需要結束當前桶的構建,下一步需要建立新的桶了。

看源碼漲知識:C++ 中的無窮大數的STL支持std::numeric_limits<double>::infinity()

類別型特征:

  1. 首先對特征取值按出現的次數排序(大到小)。
  2. 取前 min(max_bin, distinct_values_int.size()) 個特征做特征值到桶之間的映射(這樣可能會忽略一些出現次數較少的特征取值),也就是取 max_bindistinct_value.size 中的較小值作為 bins_num
  3. 然后用 bin_2_categorical_(vector類型)記錄桶對應的特征取值,以及用categorical_2_bin_(unordered_map類型) 將特征取值對應的桶。

分桶優點(Pros of Bins)

1.內存消耗優化(memory usage optimization),由於無需預排序,並且葉子節點的數據以直方圖的形式存儲,所以內存消耗可以減小 8 倍以上。

在這里插入圖片描述

2.利用直方圖做差加速特性,在擁有父節點和其中一個子節點的直方圖時,可以只消耗 \(O(\# \text{bin})\) 的時間復雜度便可以計算得另一節點的直方圖。

在這里插入圖片描述

3.提高緩存命中率(Increase cache hit chance),就是優化了內存訪問。

回歸在 XGBoost 中,需要兩種內存隨機訪問的過程,第一個是梯度值的隨機訪問,這就不用說了,由於特征的預排序導致的梯度的訪問變成了隨機內存訪問。同時為了提高分割速度,將每個樣本點映射到了葉子節點的索引,這樣獲取該索引時,也是隨機內存訪問。

在這里插入圖片描述

這兩處內存的隨機訪問,導致了效率下降。在 LightGBM 中不需要樣本點到葉子節點的索引值(這是因為采用了 leaf-wise 方法所以不需要存儲每個葉子節點所分到的數據樣本點),同時各個特征不需要排序,所以是連續內存訪問效率更高。

在這里插入圖片描述

當然,Histogram算法並不是完美的。由於特征被離散化后,找到的並不是很精確的分割點,所以會對結果產生影響。但在不同的數據集上的結果表明,離散化的分割點對最終的精度影響並不是很大,甚至有時候會更好一點。原因是決策樹本來就是弱模型,分割點是不是精確並不是太重要;較粗的分割點也有正則化的效果,可以有效地防止過擬合;即使單棵樹的訓練誤差比精確分割的算法稍大,但在梯度提升(Gradient Boosting)的框架下沒有太大的影響。

4.同時由於直方圖的特點,在進行數據並行時可大幅降低通信代價(數據並行的實現可見下文)。

算法對比(LightGBM VS XGBoost)

那么基於直方圖算法和按葉生長(Leaf-wise tree growth)策略的最佳分割點查找算法實現如下:

\[\begin{array} { l } \text { Algorithm: FindBestSplitByHistogram } \\ \text { Input: Training data X, Current Model } T _ { c - 1 } ( X ) \\ \text { First order gradient G, second order gradient H } \\ \text { For all Leaf p in } T _ { c - 1 } ( X ) \text { : } \\ \qquad \begin{array} { l } \text { For all f in X.Features: } \\ \qquad \begin{array} { l } \,\triangleright \text { construct histogram } \\ \text {H = new Histogram() } \\ \text {For i in (0, num\_of\_row) //go through all the data row } \\ \qquad \text { H[f.bins[i]]. } g += g _ { i } ; \text { H[f.bins[i]]. } n + = 1 \\ \,\triangleright \text { find best split from histogram } \\ \text { For i in (0,len(H)): //go through all the bins } \\ \qquad \begin{array} { l } S _ { L } + = H [ i ] . g ; n _ { L } + = H [ i ] . n \\ S _ { R } = S _ { P } - S _ { L } ; n _ { R } = n _ { P } - n _ { L } \\ \Delta l o s s = \frac { S _ { L } ^ { 2 } } { n _ { L } } + \frac { S _ { R } ^ { 2 } } { n _ { R } } - \frac { S _ { P } ^ { 2 } } { n _ { P } } \\ \text {if } \Delta l o s s > \Delta l o s s \left( p _ { m } , f _ { m } , v _ { m } \right) : \\ \qquad \left( p _ { m } , f _ { m } , v _ { m } \right) = ( p , f , H [ i ] . \text { value} ) \end{array} \end{array} \end{array} \end{array} \]

與 XGBoost 的對比圖如下:

\[\begin{array} { c|c | c } \hline & \text { XGBoost } & \text { LightGBM } \\ \hline \text { Tree growth algorithm } & \begin{array} { l } \text { Level-wise good for engineering } \\ \text { optimization , but not efficient } \\ \text { to learn model } \end{array} & \begin{array} { l } \text { Leaf-wise with max depth limitation get } \\ \text { better trees with smaller computation } \\ \text { cost, also can avoid overfitting } \end{array} \\ \hline \text { Split search algorithm } & \text { Pre-sorted algorithm } & \text { Histogram algorithm } \\ \text { memory cost } & \text { 2*\#feature*\#data*4Bytes } & \begin{array} { l } \text { \#feature*\#data*1Bytes (8x smaller) } \end{array} \\ \hline \text { Calculation of split gain } & \text { O(\#data* \#features) } & \text { O(\#bin *\#features) } \\ \hline \text { Cache-line aware optimization } & \text { n/a } & \text { 40\% speed-up on Higgs data } \\ \hline \text { Categorical feature support } & \text { n/a } & \text { 8} \times \text{ speed-up on Expo data } \\ \hline \end{array} \]

總體來說 LightGBM 一定程度上優於 XGBoost,實現了不損失精度的前提下提高了訓練效率。

基於梯度的單邊采樣(Gradient-based One-Side Sampling (GOSS))

除卻上述的基本操作外,LightGBM 還針對數據量過大作出以下優化。那對於數據量過大直接解決辦法便是減少樣本數據量和特征數,所以 LightGBM 據此提出來兩個方法:

  • 基於梯度的單邊采樣(Gradient-based One-Side Sampling (GOSS)):當對樣本進行采樣時,為了保持信息增益估計的准確性,應該更好地保留那些具有較大梯度的實例(梯度較大的保留,較小的采樣后放大),在相同的目標采樣率下,特別是當信息增益的取值范圍較大時,這種方法比均勻隨機采樣能得到更精確的增益估計。

  • 互斥特征捆綁(Exclusive Feature Bundling (EFB)):通常在實際應用中,雖然特征數量眾多,但特征空間相當稀疏,也就是說,在稀疏特征空間中,許多特征(幾乎)是相斥的,即它們很少同時取非零值(比如 one-hot 編碼)。所以可以安全地捆綁這樣的類似的互斥特征。為此,LightGBM 中提出了一個有效的算法,將最優捆綁問題歸結為圖的着色問題(如果兩個特征不是互斥的,則以特征為頂點,每兩個特征加一條邊),並用一個具有恆定逼近比的貪婪算法求解。

首先針對單邊采樣進行介紹。其想法是如果一個樣本的梯度很小,說明該樣本的訓練誤差很小,或者說該樣本已經得到了很好的訓練。與 AdaBoost 類似,其會對於分類錯誤較大的數據樣本給予更多的關注。什么意思呢?看一下基於梯度的單邊采樣(Gradient-based One-Side Sampling (GOSS))的偽代碼:

\[\begin{array} { l } \text { Algorithm: Gradient-based One-Side Sampling } \\ \text { Input: } I : \text { training data, } d \text { : iterations } \\ \text { Input: } a : \text { sampling ratio of large gradient data } \\ \text { Input: } b \text { : sampling ratio of small gradient data } \\ \text { Input: } \text {loss:} \text { loss function, } L \text { : weak learner } \\ \text { models } \leftarrow \{ \} , \text { fact } \leftarrow \frac { 1 - a } { b } \\ \text { topN } \leftarrow \mathrm { a } \times \operatorname { len } ( I ) , \operatorname { rand } \mathrm { N } \leftarrow \mathrm { b } \times \operatorname { len } ( I ) \\ \text { for } i = 1 \text { to } d \text { do } \\ \qquad \begin{array}{l} \text { preds } \leftarrow \text { models.predict } ( I ) \\ \, \mathrm { g } \leftarrow los s ( I , \text { preds } ) , \mathrm { w } \leftarrow \{ 1,1 , \ldots \} \\ \text { sorted } \leftarrow \text { GetSortedIndices } ( \mathrm { abs } ( \mathrm { g } ) ) \\ \text { topSet } \leftarrow \text { sorted[1:topN] } \\ \text { randSet } \leftarrow \text { RandomPick(sorted[topN:len(I)], randN) } \\ \text { usedSet } \leftarrow \text { topSet + randSet } \\ \text { w[randSet] } \times = \text { fact } \triangleright \text { Assign weight fact to the small gradient data. } \\ \text { newModel } \leftarrow \mathrm { L } ( I [ \text { usedSet } ] , - \mathrm { g } [ \text { usedSet } ] \text { w[usedSet]) } \\ \text { models.append(newModel) } \end{array} \end{array} \]

其中 g 具體的實現是一階梯度和二階梯度的乘積。這樣通過重新采樣的方式可以盡量減小對數據分布的影響。

其具體實現流程如下:

  1. 根據梯度的絕對值將樣本進行降序排序
  2. 選擇前a×100%的樣本作為 TopSet。
  3. 針對剩下的數據(1−a)×100% 的數據進行隨機抽取 b×100% 數據組成 RandSet。
  4. 由於樣本集的減少,在計算增益的時候,選擇將 RandSet 所對應的權重放大 (1−a)/b 倍。

那么未使用 GOSS 算法時,在特征 j 上的 d 點進行分割帶來的增益如下:

\[V _ { j | O } ( d ) = \frac { 1 } { n _ { O } } \left( \frac { \left( \sum _ { x _ { i } \in O : x _ { i } z d } g _ { i } \right) ^ { 2 } } { n _ { l | l O } ^ { j } ( d ) } + \frac { \left( \sum _ {\left. x _ { i } \in O : x _ { i } \right\rangle d } g _ { i } \right) ^ { 2 } } { n _ { r | O } ^ { j } ( d ) } \right) \]

\[\text {where } n _ { O } = \sum I \left[ x _ { i } \in O \right] , n _ { l | O } ^ { j } ( d ) = \sum I \left[ x _ { i } \in O : x _ { i j } \leq d \right] \text { and } n _ { r | O } ^ { j } ( d ) = \sum I \left[ x _ { i } \in O : x _ { i j } > d \right] \]

那么使用 GOSS 算法后,,在特征 j 上的 d 點進行分割帶來的增益變為:

\[V _ { j | O } ( d ) = \frac { 1 } { n _ { O } } \left( \frac { \left( \sum _ { x _ { i } \in A _ { l } } g _ { i } + \frac { 1 - a } { b } \sum _ { x _ { i } \in B _ { l } } g _ { i } \right) ^ { 2 } } { n _ { l } ^ { j } ( d ) } + \frac { \left( \sum _ { x _ { i } \in A _ { r } } g _ { i } + \frac { 1 - a } { b } \sum _ { x _ { i } \in B _ { l } } g _ { r } \right) ^ { 2 } } { n _ { r } ^ { j } ( d ) } \right) \]

\[\begin{array} { l } \text { where } A _ { l } = \left\{ x _ { i } \in A : x _ { i j } \leq d \right\} , A _ { r } = \left\{ x _ { i } \in A : x _ { i j } > d \right\} , B _ { l } = \left\{ x _ { i } \in B : x _ { i j } \leq d \right\} , B _ { r } = \left\{ x _ { i } \in B : x _ { i j } > d \right\} \\ \text { and the coefficient } \frac { 1 - a } { b } \text { is used to normalize the sum of the gradients over } B \text { back to the size of } A ^ { c } \text { . } \end{array} \]

這里 A 代表的是 TopSet,B 代表的是 RandSet。當然在 LightGBM 中也證明了誤差收斂性和 GOSS 的泛化性能。

GOSS的估計誤差 \(\mathcal { E } ( d ) = \left| \tilde { V } _ { j } ( d ) - V _ { j } ( d ) \right|\) 如下:

\[\mathcal { E } ( d ) \leq C _ { a , b } ^ { 2 } \ln 1 / \delta \cdot \max \left\{ \frac { 1 } { n _ { l } ^ { j } ( d ) } , \frac { 1 } { n _ { r } ^ { j } ( d ) } \right\} + 2 D C _ { a , b } \sqrt { \frac { \ln 1 / \delta } { n } } \]

\[\begin{array}{l} \text {where } C _ { a , b } = \frac { 1 - a } { \sqrt { b } } \max _ { x _ { i } \in A ^ { c } } \left| g _ { i } \right| , \text { and } D = \max \left( \bar { g } _ { l } ^ { j } ( d ) , \bar { g } _ { r } ^ { j } ( d ) \right) \\ \text{and }\bar { g } _ { l } ^ { j } ( d ) = \frac { \sum _ { x _ { i } \in \left( A \cup A ^ { c } \right) _ { l } } \left| g _ { i } \right| } { n _ { l } ^ { j } ( d ) } , \bar { g } _ { r } ^ { j } ( d ) = \frac { \sum _ { x _ { i } \in \left( A \cup A ^ { c } \right) _ { r } \left| g _ { i } \right| } } { n _ { r } ^ { j } ( d ) } \end{array} \]

該定理證明了 GOSS 的誤差估計將在最長 \(O(n)\) 的時間復雜度下實現逼近與收斂值。並且當已有數據足夠多且分布於全局數據保持一致時,該算法可以保證泛化性能。

互斥特征綁定(Exclusive Feature Bundling)

看到前文的互斥特征綁定定義,我是一頭霧水,忍不住把 GBM 讀成了 BGM 😅。這實際上針對的是一些特定情境下比如使用 one-hot 編碼組成的稀疏數據,這中特征是互斥的(也就是說 one-hot 編碼中只有一位為 1 ),而互斥特征綁定(EFB)實際上就是將這些特征綁定在一起,組成一個 bundle,從而實現特征的降維(減小特征數)。如果可實現,那么時間復雜度從 \(O ( \# \text { data} \times \# \text { feature } )\) 降低為了 \(O ( \# \text { data} \times \# \text { bundle} )\)。實現上分為兩個部分:如何找出互斥特征進行綁定(Greedy Bundling)以及綁定后如何融合(Merge Exclusive Features)。

貪心綁定(Greedy Bundling)

在 LightGBM 論文中已經做出證明,將特征划分為最小數量的互斥 bundle 是 NP 問題。所以這里使用了貪心算法。此算法中使用無向圖圖表示各特征之間的關系,也就是說圖中每個節點表示一個特征,特征之間使用邊進行聯通成為一個網絡,邊的權重代表了是否互斥。如果互斥那么代表兩個特征可以合並,使用邊進行連接。但是由於通常有少量的特征,雖然不是 100% 互斥,並且大多數情況下不會同時取非0值。若構建 Bundle 時允許少量的沖突,就能得到更少數的 bundle,進一步提高效率。可以證明,隨機的污染一部分特征的話最多影響訓練精度 \(\mathcal { O } \left( [ ( 1 - \gamma ) n ] ^ { - 2 / 3 } \right)\) ,其中 \(\gamma\) 是最大沖突率,與之相對應的是下面偽代碼中的最大沖突個數 \(K\)。所以這里選擇將邊賦予權重表示節點間的沖突程度,同時類似於前向搜索算法,只是從先向后搜索查找最優解。那么該貪心綁定(Greedy Bundling)的偽代碼實現如下:

\[\begin{array} { l }\text { Algorithm: Greedy Bundling } \\ \text { Input: } F : \text { features, } K : \text { max conflict count } \\ \text { Construct graph } G \\ \text { searchOrder } \leftarrow G \text { .sortByDegree } ( ) \\ \text { bundles } \leftarrow \{ \} , \text { bundlesconflict } \leftarrow \{ \} \\ \text { for } i \text { in searchOrder do } \\ \qquad \begin{array}{l} \text { needNew } \leftarrow \text { True } \\ \text { for } j = 1 \text { to len(bundles) } \mathbf { d } \mathbf { o } \\ \qquad \begin{array}{l} \text { cnt } \leftarrow \text { Conflict Cnt(bundles[j], } F [ \mathrm { i } ] ) \\ \text { if } c n t + \text { bundlesconflict } [ i ] \leq K \text { then } \\ \qquad \text { bundles[j].add } ( F [ \text { i] } ) , \text { needNew } \leftarrow \text { False } \\ \text { break } \end{array} \\ \text { if needNew then } \\ \qquad \text { Add } F [ i ] \text { as a new bundle to bundles } \end{array} \\ \text { Output: bundles } \end{array} \]

具體步驟是:

  1. 構建有權無向圖,節點是特征,邊是節點間的沖突程度
  2. 將圖按度(知識補充:每個節點邊的累加值或者說無權圖中節點擁有邊的個數)排序
  3. 對排序后的節點進行遍歷,並判斷現存的全部 bundle 是否與本節點符合互斥關系(判斷時仍然是從前向后遍歷 bundle),符合便加入該 bundle ,反之若不符合建立新的 bundle

該算法的時間復雜度為 \(O(\#feature^2)\),雖然只需要在訓練之前做一次處理,但是當特征數很大的時候,仍然效率不高。對此 LightGBM 提出了一種更為高效的排序策略,直接按特征的非0值的個數進行排序,這與按度排序的策略類似,因為非零值越大意味着沖突的可能性越大。

互斥特征融合(Merge Exclusive Features)

特征融合的關鍵是原有的不同特征在構建后的 feature bundles 中仍能夠識別。由於基於 histogram 的方法存儲的是離散的而不是連續的數值,因此可以通過添加偏移的方法將不同特征的 bins 設定在不同的區間。LightGBM 中舉出了這樣的例子:

Originally, feature A takes value from [0,10) and feature B takes value [0,20) . We then add an offset of 10 to the values of feature B so that the refined feature takes values from [10,30) . After that, it is safe to merge features A and B, and use a feature bundle with range [0,30] to replace the original features A and B.

根據例子可以很容易理解互斥特征融合的技巧,偽代碼如下:

\[\begin{array} { l }\text { Algorithm: Merge Exclusive Features} \\ \text { Input: } n u m \text { Data: number of data } \\ \text { Input: } F : \text { One bundle of exclusive features } \\ \text { binRanges } \leftarrow \{ 0 \} , \text { totalBin } \leftarrow 0 \\ \text { for } f \text { in } F \text { do } \\ \qquad \text { totalBin } + = \text { f.numBin } \\ \qquad \text { binRanges.append(totalBin) } \\ \text { newBin } \leftarrow \text { new Bin(numData) } \\ \text { for } i = 1 \text { to numData } \mathbf { d } \mathbf { o } \\ \qquad \text { newBin[i] } \leftarrow 0 \\ \qquad \text { for } j = 1 \text { to len} ( F ) \text { do } \\ \qquad \qquad \text { if } F [ j ] . \text { bin } [ i ] \neq 0 \text { then } \\\qquad \qquad \qquad \text { newBin[i] } \leftarrow F [ \text { j].bin[i] + binRanges[j] } \\ \text { Output: newBin, binRanges} \end{array} \]

具體步驟是:在該 bundle 中,將當前特征前已遍歷的全部特征擁有的桶的總個數作為偏移量,將全部的特征的桶進行直方圖合並,示意圖如下:

在這里插入圖片描述

EFB算法可以將大量的互斥特征捆綁到較少的密集特征上,有效地避免了對零特征值的不必要計算。同時實際上,也可以通過為每個特征使用一個表來記錄具有非零值的數據,忽略零特征值,進而達到優化基本的基於直方圖的算法的目的。通過掃描此表中的數據,特征的直方圖構建成本將從 \(O(\#data)\) 更改為\(O(\#non\_zero\_data)\)。然而,這種方法需要額外的內存和計算開銷來維護整個樹生長過程中的每個特征表。LightGBM 將這個優化方法集成為了一個基本函數來實現。注意,這個優化與 EFB 並不沖突,因為當 bundle 稀疏時仍然可以使用它。

並行學習的優化(Optimization in Parallel Learning)

並行計算在 LightGBM 的官方文檔中和微軟亞洲研究院發布的視頻 如何玩轉LightGBM 都做了介紹,這里我便簡單的翻譯和記錄一下,不再寫具體的證明。

特征並行(Feature Parallel)

在這里插入圖片描述

特征並行主要針對的是數據量較小、特征較多的情景。其是通過垂直的切分數據,使得全部機器上都有所有的數據樣本點,但是不同機器上所存儲的特征不一樣,這樣每個機器都計算出該機器上可以獲得的最優的局部分割點,然后通過全部的局部最優分割點獲得全局最優分割點。

數據並行(Data Parallel)

在這里插入圖片描述

數據並行主要針對的是數據量比較大、特征較少的情景。其是通過水平的切分數據,全部機器上擁有部分的數據樣本點,但是包含全部的特征,這樣每個機器可以構造出全部特征的局部(本地)直方圖,然后通過全部的局部直方圖獲取全局的全部特征的直方圖,在后在全局直方圖上查找最優分割點。

投票並行(Voting Parallel)

在這里插入圖片描述

投票並行主要針對數據量較大、特征較多的情景。主要是針對使用數據並行時,特征直方圖合並導致的通訊消耗。這里通過二階段投票的方式只合並部分直方圖來彌補這一缺陷。首先是通過本地的數據找出(局部投票獲得) Top k 的最優特征(用於分割),然后將這些特征整合在一起,並對這些特征通過全局投票獲取到可能是全局最優分割點的 Top 2*K 特征,之后只針對這些特征進行直方圖的合並。

LightGBM采用一種稱為 PV-Tree 的算法進行投票並行(Voting Parallel)其實這本質上也是一種數據並行。PV-Tree 和普通的決策樹差不多,只是在尋找最優切分點上有所不同。

具體的算法偽代碼如下:

\[\begin{array} { l } \text { Algorithm : PV-Tree FindBestSplit}\\ \text { Input: Dataset } D \\ \text { localHistograms = ConstructHistograms(D) } \\ \,\, \triangleright \text { Local Voting } \\ \text { splits = [] } \\ \text { for all H in localHistograms do } \\ \qquad \text { splits.Push(H.FindBestSplit()) } \\ \text { end for } \\ \text { localTop = splits.TopKByGain(K) } \\ \,\, \triangleright \text { Gather all candidates } \\ \text { allCandidates = AllGather(localTop) } \\ \,\, \triangleright \text { Global Voting } \\ \text { globalTop = allCandidates.TopKByMajority(2*K) } \\ \,\, \triangleright \text { Merge global histograms } \\ \text { globalHistograms = Gather(globalTop, localHistograms) } \\ \text { bestSplit = globalHistograms.FindBestSplit() } \\ \text { return bestSplit } \end{array} \]

代碼中的 FindBestSplit 函數也就是單機運行函數實現如下:

\[\begin{array} { l } \text { Algorithm : FindBestSplit}\\ \text { Input: DataSet } \\ \text { for all } \mathrm { X } \text { in D.Attribute } \mathrm { d } \mathbf { o } \\ \qquad \begin{array} { l } \,\, \triangleright \text { Construct Histogram } \\ \text { H = new Histogram() } \\ \text { for all } \mathrm { x } \text { in } \mathrm { X } \text { do } \\ \qquad \text { H.binAt(x.bin).Put(x.label) } \\ \text { end for } \\ \,\, \triangleright \text { Find Best Split } \\ \text { leftSum = new HistogramSum() } \\ \text { for all bin in H do } \\ \qquad \begin{array} { l } \text { leftSum = leftSum + H.binAt(bin) } \\ \text { rightSum = H.AllSum - leftSum } \\ \text { split.gain = CalSplitGain(leftSum, rightSum) } \\ \text { bestSplit = ChoiceBetterOne(split,bestSplit) } \end{array} \\ \text { end for } \end{array} \\ \text { end for } \\ \text { return bestSplit } \end{array} \]

使用經驗(Hands-on Experience)

更快的學習速度(Faster Learining Speed)

  • 使用 bagging 操作,對數據進行采用(子集)
  • 對特征進行子集采用
  • 可以直接使用類別特征無需離散化
  • 將數據存為二進制數據文件,這樣在多次訓練時可以做到更快
  • 使用並行學習

更好的精度(Better Accuracy)

  • 較小的學習率和較多的迭代次數
  • 較多葉子的個數
  • 交叉驗證
  • 更多的訓練數據
  • Try DART-use drop out during the training

處理過擬合(Deal with Overfitting)

  • small maxbin_feature——分桶略微粗一些
  • small num_leaves——不要在單棵樹上分的太細
  • Control min_data_in_leaf and min_sum_hessian_in_leaf——確保葉子節點還有足夠多的數據
  • Sub - sample——在構建每棵樹的時候,在data上做一些 sample
  • Sub - feature——在構建每棵樹的時候,在feature上做一些 sample
  • bigger training data——更多的訓練數據
  • lambda, lambda_l2 and min_gaint_ split to regularization——正則
  • max_ depth to avoid growing deep tree——控制樹深度

參考論文:LightGBM: A Highly Efficient Gradient Boosting Decision Tree

參考視頻:如何玩轉LightGBM集成學習:XGBoost, lightGBM


免責聲明!

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



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