隨着大數據時代的到來,GBDT正面臨着新的挑戰,特別是在精度和效率之間的權衡方面。傳統的GBDT實現需要對每個特徵掃描所有數據實例,以估計所有可能的分割點的信息增益。因此,它們的計算複雜度將與特徵數和實例數成正比。這使得這些實現在處理大數據時非常耗時。所以微軟亞洲研究院提出了 LightGBM ,其設計理念是:
單個機器在不犧牲速度的情況下,儘可能使用上更多的數據
多機並行的時候,通信的代價儘可能地低,並且在計算上可以做到線性加速。
決策樹學習算法(Decision Tree Learning Algorithm)
傳統的決策樹的生成方法有:按葉生長(Leaf-wise tree growth)和按層生長(Level-wise tree growth)兩種。
其中按層生長是將每一個節點都分割爲兩個葉子節點。其雖然有天然的並行性,但是會有很多不必要的分裂產生,造成更多的計算代價。
而按葉生長是隻針對其中一個葉子節點進行子樹生長,並且對該節點進行分叉操作後損失值下降最多。
數學表達如下:
( p m , f m , v m ) = arg min ( p , f , v ) L ( T m − 1 ( X ) . split ( p , f , v ) , Y ) T m ( X ) = T m − 1 ( X ) . split ( p m , f m , v m )
\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}
( p m , f m , v m ) = arg min ( p , f , v ) L ( T m − 1 ( X ) . split ( p , f , v ) , Y ) T m ( X ) = T m − 1 ( X ) . split ( p m , f m , v m )
在 LightGBM 中使用的是 leaf-wise 的方法,這樣的話在葉子個數一樣時,相對於 level-wise 有更高的精度,但是可能會導致生成較深的樹,所以 LightGBM 中也提出了限制最大深度來避免過擬合問題。
那麼這種使用 leaf-wise tree growth 方法進行決策樹的學習的僞代碼如下:
Algorithm : DecisionTree Input: Training data ( X , Y ) , number of leaf C , Loss function l ▹ put all data on root T 1 ( X ) = X For m in ( 2 , C ) : ▹ find best split ( p m , f m , v m ) = FindBestsplit ( X , Y , T m − 1 , l ) ▹ perform split T m ( X ) = T m − 1 ( X ) . split ( p m , f m , v m )
\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}
Algorithm : DecisionTree Input: Training data ( X , Y ) , number of leaf C , Loss function l ▹ put all data on root T 1 ( X ) = X For m in ( 2 , C ) : ▹ find best split ( p m , f m , v m ) = FindBestsplit ( X , Y , T m − 1 , l ) ▹ perform split T m ( X ) = T m − 1 ( X ) . split ( p m , f m , v m )
其中計算消耗最多的地方是找出最佳的分割點,該分割點查找算法如下:
Algorithm : FindBestsplit Input: Training data ( X , Y ) , Loss function l , Current Model T m − 1 ( X ) For all Leaf p in T m − 1 ( X ) : For all f in X.Features: For all v in f.Thresholds: ( left, right ) = partition ( p , f , v ) Δ loss = L ( X p , Y p ) − L ( X left , Y left ) − L ( X right , Y right ) if Δ loss > Δ loss ( p m , f m , v m ) : ( p m , f m , v m ) = ( p , f , v )
\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}
Algorithm : FindBestsplit Input: Training data ( X , Y ) , Loss function l , Current Model T m − 1 ( X ) For all Leaf p in T m − 1 ( X ) : For all f in X.Features: For all v in f.Thresholds: ( left, right ) = partition ( p , f , v ) Δ l o s s = L ( X p , Y p ) − L ( X left , Y left ) − L ( X right , Y right ) if Δ loss > Δ l o s s ( p m , f m , v m ) : ( p m , f m , v m ) = ( p , f , v )
那麼 LightGBM 便是在此算法上進行的優化。第一個便是直方圖算法。
直方圖算法(Histogram Algorithm)
回顧 XGBoost 中,是使用預排序算法和加權分位數算法提出的估計分割法,什麼意思呢?簡單來說就是對數據根據二階梯度值進行預排序,之後取其分位數 n ∗ m % ( N ∗ m = 100 , n = 1 , 2 , ⋯ , N ) n * m\% (N*m=100,n=1,2,\cdots,N) n ∗ m % ( N ∗ m = 1 0 0 , n = 1 , 2 , ⋯ , N ) ,作爲代表或者說採樣後代表子集,對該子集窮舉選擇最優分割點。這樣的算法有兩個問題:
需要對每個特徵按特徵值進行排序
由於對特徵進行了排序,但梯度並未排序,所以梯度值的獲取屬於隨機內存訪問。
這兩項都是極其消耗時間和空間的。
具體實現(Implementation)
在 LightGBM 中採用了更爲高效的方法 —— 直方圖算法(Histogram algorithm)。什麼意思呢?實際上就是對連續的浮點數據進行分桶操作,或者說離散爲 k 個整數值。例如 [ 0 , 0.1 ) → 0 , [ 0.1 , 0.3 ) → 1 [ 0,0.1 ) \rightarrow 0,[ 0.1,0.3 ) \rightarrow 1 [ 0 , 0 . 1 ) → 0 , [ 0 . 1 , 0 . 3 ) → 1 。同時 LightGBM 對特徵的每個桶進行梯度(一階和二階梯度)累加和個數統計。然後根據直方圖尋找最優點。下圖就是直方圖的獲取流程:
使用基於直方圖的尋找最優分割點時,需要 O ( # bin × # feature ) O ( \# \text { bin} \times \# \text { feature } ) O ( # bin × # feature ) 的時間複雜度構建直方圖和 O ( # data × # feature ) O ( \# \text { data } \times \# \text { feature } ) O ( # data × # feature ) 的時間複雜度尋找分割點。直方圖算法的僞代碼如下:
Input: I : training data, d : max depth Input: m : feature dimension nodeSet ← { 0 } ▹ tree nodes in current level rowSet ← { { 0 , 1 , 2 , … } } ▹ data indices in tree nodes for i = 1 to d do for node in nodeSet do usedRows ← rowSet [ node ] for k = 1 to m do H ← new Histogram() ▹ Build histogram for j in usedRows do bin ← I . f [ k ] [ j].bin H [ bin ] . y ← H [ bin ] . y + I.y [ j ] H [ bin ] . n ← H [ bin ] . n + 1 Find the best split on histogram H ⋯
\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}
Input: I : training data, d : max depth Input: m : feature dimension nodeSet ← { 0 } ▹ tree nodes in current level rowSet ← { { 0 , 1 , 2 , … } } ▹ data indices in tree nodes for i = 1 to d do for node in nodeSet do usedRows ← rowSet [ node ] for k = 1 to m do H ← new Histogram() ▹ Build histogram for j in usedRows do bin ← I . f [ k ] [ j].bin H [ bin ] . y ← H [ bin ] . y + I.y [ j ] H [ bin ] . n ← H [ bin ] . n + 1 Find the best split on histogram H ⋯
分桶操作(Organization of Bins)
在僞代碼中的直方圖構建中,分桶操作並沒有體現,而是直接獲得了該特徵值所對應的桶的編號。那這個編號是如何獲取的呢,或者說是如何進行分桶操作的呢?實際上這仍然需要一個排序操作,不過只需要在一開始做一步排序獲得分桶的分割點即可,之後便可以直接使用桶的分割點對每個特徵進行分桶操作了。具體實現在數值類型和類別類型上又不一樣。下面介紹一下具體實現。
數值型特徵:
對特徵值去重後進行排序(從大到小)並統計每個特徵值出現的次數 counts
。
取 max_bin
和 distinct_value.size
中的較小值作爲 bins_num
計算每個桶可以分到的平均樣本個數 mean_bin_size
,特徵取值數 distinct_value.size
比max_bin
數量少,直接取distinct_values
的中點作爲桶間分割點,即無需分桶。反之則需要分桶,也就是說可能存在幾個特徵值同分於一個桶中(多特徵取值公用一個桶),但是有一點就是當該特徵取值的計數值大於平均值 mean_bin_size
時,該特徵取值需要單獨分桶,所以需要標記出符合該特點的全部特徵,之後對不符合的重新計算 mean_bin_size
。
然後對於去重後的特徵取值進行遍歷操作,如果當前的特徵需要單獨成桶、或者當前桶中個特徵計數超過了 mean_bin_size
、或者下一個特徵是需要獨立成桶的,那麼當前的特徵值將作爲當前桶的上界,下一個桶的下界,也就是說需要本步需要結束當前桶的構建,下一步需要建立新的桶了。
看源碼漲知識:C++ 中的無窮大數的STL支持std::numeric_limits<double>::infinity()
類別型特徵:
首先對特徵取值按出現的次數排序(大到小)。
取前 min(max_bin, distinct_values_int.size())
個特徵做特徵值到桶之間的映射(這樣可能會忽略一些出現次數較少的特徵取值),也就是取 max_bin
和 distinct_value.size
中的較小值作爲 bins_num
。
然後用 bin_2_categorical_
(vector類型)記錄桶對應的特徵取值,以及用categorical_2_bin_
(unordered_map類型) 將特徵取值對應的桶。
分桶優點(Pros of Bins)
1.內存消耗優化(memory usage optimization),由於無需預排序,並且葉子節點的數據以直方圖的形式存儲,所以內存消耗可以減小 8 倍以上。
2.利用直方圖做差加速特性,在擁有父節點和其中一個子節點的直方圖時,可以只消耗 O ( # bin ) O(\# \text{bin}) O ( # bin ) 的時間複雜度便可以計算得另一節點的直方圖。
3.提高緩存命中率(Increase cache hit chance),就是優化了內存訪問。
迴歸在 XGBoost 中,需要兩種內存隨機訪問的過程,第一個是梯度值的隨機訪問,這就不用說了,由於特徵的預排序導致的梯度的訪問變成了隨機內存訪問。同時爲了提高分割速度,將每個樣本點映射到了葉子節點的索引,這樣獲取該索引時,也是隨機內存訪問。
這兩處內存的隨機訪問,導致了效率下降。在 LightGBM 中不需要樣本點到葉子節點的索引值(這是因爲採用了 leaf-wise 方法所以不需要存儲每個葉子節點所分到的數據樣本點),同時各個特徵不需要排序,所以是連續內存訪問效率更高。
當然,Histogram算法並不是完美的。由於特徵被離散化後,找到的並不是很精確的分割點,所以會對結果產生影響。但在不同的數據集上的結果表明,離散化的分割點對最終的精度影響並不是很大,甚至有時候會更好一點。原因是決策樹本來就是弱模型,分割點是不是精確並不是太重要;較粗的分割點也有正則化的效果,可以有效地防止過擬合;即使單棵樹的訓練誤差比精確分割的算法稍大,但在梯度提升(Gradient Boosting)的框架下沒有太大的影響。
4.同時由於直方圖的特點,在進行數據並行時可大幅降低通信代價(數據並行的實現可見下文)。
算法對比(LightGBM VS XGBoost)
那麼基於直方圖算法和按葉生長(Leaf-wise tree growth)策略的最佳分割點查找算法實現如下:
Algorithm: FindBestSplitByHistogram Input: Training data X, Current Model T c − 1 ( X ) First order gradient G, second order gradient H For all Leaf p in T c − 1 ( X ) : For all f in X.Features: ▹ construct histogram H = new Histogram() For i in (0, num_of_row) //go through all the data row H[f.bins[i]]. g + = g i ; H[f.bins[i]]. n + = 1 ▹ find best split from histogram For i in (0,len(H)): //go through all the bins S L + = H [ i ] . g ; n L + = H [ i ] . n S R = S P − S L ; n R = n P − n L Δ l o s s = S L 2 n L + S R 2 n R − S P 2 n P if Δ l o s s > Δ l o s s ( p m , f m , v m ) : ( p m , f m , v m ) = ( p , f , H [ i ] . value )
\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}
Algorithm: FindBestSplitByHistogram Input: Training data X, Current Model T c − 1 ( X ) First order gradient G, second order gradient H For all Leaf p in T c − 1 ( X ) : For all f in X.Features: ▹ construct histogram H = new Histogram() For i in (0, num_of_row) //go through all the data row H[f.bins[i]]. g + = g i ; H[f.bins[i]]. n + = 1 ▹ find best split from histogram For i in (0,len(H)): //go through all the bins S L + = H [ i ] . g ; n L + = H [ i ] . n S R = S P − S L ; n R = n P − n L Δ l o s s = n L S L 2 + n R S R 2 − n P S P 2 if Δ l o s s > Δ l o s s ( p m , f m , v m ) : ( p m , f m , v m ) = ( p , f , H [ i ] . value )
與 XGBoost 的對比圖如下:
XGBoost LightGBM Tree growth algorithm Level-wise good for engineering optimization , but not efficient to learn model Leaf-wise with max depth limitation get better trees with smaller computation cost, also can avoid overfitting Split search algorithm Pre-sorted algorithm Histogram algorithm memory cost 2*#feature*#data*4Bytes #feature*#data*1Bytes (8x smaller) Calculation of split gain O(#data* #features) O(#bin *#features) Cache-line aware optimization n/a 40% speed-up on Higgs data Categorical feature support n/a 8 × speed-up on Expo data
\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}
Tree growth algorithm Split search algorithm memory cost Calculation of split gain Cache-line aware optimization Categorical feature support XGBoost Level-wise good for engineering optimization , but not efficient to learn model Pre-sorted algorithm 2*#feature*#data*4Bytes O(#data* #features) n/a n/a LightGBM Leaf-wise with max depth limitation get better trees with smaller computation cost, also can avoid overfitting Histogram algorithm #feature*#data*1Bytes (8x smaller) O(#bin *#features) 40% speed-up on Higgs data 8 × speed-up on Expo data
總體來說 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))的僞代碼:
Algorithm: Gradient-based One-Side Sampling Input: I : training data, d : iterations Input: a : sampling ratio of large gradient data Input: b : sampling ratio of small gradient data Input: loss: loss function, L : weak learner models ← { } , fact ← 1 − a b topN ← a × len ( I ) , rand N ← b × len ( I ) for i = 1 to d do preds ← models.predict ( I ) g ← l o s s ( I , preds ) , w ← { 1 , 1 , … } sorted ← GetSortedIndices ( a b s ( g ) ) topSet ← sorted[1:topN] randSet ← RandomPick(sorted[topN:len(I)], randN) usedSet ← topSet + randSet w[randSet] × = fact ▹ Assign weight fact to the small gradient data. newModel ← L ( I [ usedSet ] , − g [ usedSet ] w[usedSet]) models.append(newModel)
\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}
Algorithm: Gradient-based One-Side Sampling Input: I : training data, d : iterations Input: a : sampling ratio of large gradient data Input: b : sampling ratio of small gradient data Input: loss: loss function, L : weak learner models ← { } , fact ← b 1 − a topN ← a × l e n ( I ) , r a n d N ← b × l e n ( I ) for i = 1 to d do preds ← models.predict ( I ) g ← l o s s ( I , preds ) , w ← { 1 , 1 , … } sorted ← GetSortedIndices ( a b s ( g ) ) topSet ← sorted[1:topN] randSet ← RandomPick(sorted[topN:len(I)], randN) usedSet ← topSet + randSet w[randSet] × = fact ▹ Assign weight fact to the small gradient data. newModel ← L ( I [ usedSet ] , − g [ usedSet ] w[usedSet]) models.append(newModel)
其中 g 具體的實現是一階梯度和二階梯度的乘積。這樣通過重新採樣的方式可以儘量減小對數據分佈的影響。
其具體實現流程如下:
根據梯度的絕對值將樣本進行降序排序
選擇前a×100%的樣本作爲 TopSet。
針對剩下的數據(1−a)×100% 的數據進行隨機抽取 b×100% 數據組成 RandSet。
由於樣本集的減少,在計算增益的時候,選擇將 RandSet 所對應的權重放大 (1−a)/b 倍。
那麼未使用 GOSS 算法時,在特徵 j 上的 d 點進行分割帶來的增益如下:
V j ∣ O ( d ) = 1 n O ( ( ∑ x i ∈ O : x i z d g i ) 2 n l ∣ l O j ( d ) + ( ∑ x i ∈ O : x i ⟩ d g i ) 2 n r ∣ O 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)
V j ∣ O ( d ) = n O 1 ⎝ ⎜ ⎛ n l ∣ l O j ( d ) ( ∑ x i ∈ O : x i z d g i ) 2 + n r ∣ O j ( d ) ( ∑ x i ∈ O : x i ⟩ d g i ) 2 ⎠ ⎟ ⎞
where n O = ∑ I [ x i ∈ O ] , n l ∣ O j ( d ) = ∑ I [ x i ∈ O : x i j ≤ d ] and n r ∣ O j ( d ) = ∑ I [ x i ∈ O : x i j > d ]
\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]
where n O = ∑ I [ x i ∈ O ] , n l ∣ O j ( d ) = ∑ I [ x i ∈ O : x i j ≤ d ] and n r ∣ O j ( d ) = ∑ I [ x i ∈ O : x i j > d ]
那麼使用 GOSS 算法後,,在特徵 j 上的 d 點進行分割帶來的增益變爲:
V j ∣ O ( d ) = 1 n O ( ( ∑ x i ∈ A l g i + 1 − a b ∑ x i ∈ B l g i ) 2 n l j ( d ) + ( ∑ x i ∈ A r g i + 1 − a b ∑ x i ∈ B l g r ) 2 n r 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)
V j ∣ O ( d ) = n O 1 ( n l j ( d ) ( ∑ x i ∈ A l g i + b 1 − a ∑ x i ∈ B l g i ) 2 + n r j ( d ) ( ∑ x i ∈ A r g i + b 1 − a ∑ x i ∈ B l g r ) 2 )
where A l = { x i ∈ A : x i j ≤ d } , A r = { x i ∈ A : x i j > d } , B l = { x i ∈ B : x i j ≤ d } , B r = { x i ∈ B : x i j > d } and the coefficient 1 − a b is used to normalize the sum of the gradients over B back to the size of A c .
\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}
where A l = { x i ∈ A : x i j ≤ d } , A r = { x i ∈ A : x i j > d } , B l = { x i ∈ B : x i j ≤ d } , B r = { x i ∈ B : x i j > d } and the coefficient b 1 − a is used to normalize the sum of the gradients over B back to the size of A c .
這裏 A 代表的是 TopSet,B 代表的是 RandSet。當然在 LightGBM 中也證明了誤差收斂性和 GOSS 的泛化性能。
GOSS的估計誤差 E ( d ) = ∣ V ~ j ( d ) − V j ( d ) ∣ \mathcal { E } ( d ) = \left| \tilde { V } _ { j } ( d ) - V _ { j } ( d ) \right| E ( d ) = ∣ ∣ ∣ V ~ j ( d ) − V j ( d ) ∣ ∣ ∣ 如下:
E ( d ) ≤ C a , b 2 ln 1 / δ ⋅ max { 1 n l j ( d ) , 1 n r j ( d ) } + 2 D C a , b ln 1 / δ n
\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 } }
E ( d ) ≤ C a , b 2 ln 1 / δ ⋅ max { n l j ( d ) 1 , n r j ( d ) 1 } + 2 D C a , b n ln 1 / δ
where C a , b = 1 − a b max x i ∈ A c ∣ g i ∣ , and D = max ( g ˉ l j ( d ) , g ˉ r j ( d ) ) and g ˉ l j ( d ) = ∑ x i ∈ ( A ∪ A c ) l ∣ g i ∣ n l j ( d ) , g ˉ r j ( d ) = ∑ x i ∈ ( A ∪ A c ) r ∣ g i ∣ n r j ( d )
\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}
where C a , b = b 1 − a max x i ∈ A c ∣ g i ∣ , and D = max ( g ˉ l j ( d ) , g ˉ r j ( d ) ) and g ˉ l j ( d ) = n l j ( d ) ∑ x i ∈ ( A ∪ A c ) l ∣ g i ∣ , g ˉ r j ( d ) = n r j ( d ) ∑ x i ∈ ( A ∪ A c ) r ∣ g i ∣
該定理證明了 GOSS 的誤差估計將在最長 O ( n ) O(n) O ( n ) 的時間複雜度下實現逼近與收斂值。並且當已有數據足夠多且分佈於全局數據保持一致時,該算法可以保證泛化性能。
互斥特徵綁定(Exclusive Feature Bundling)
看到前文的互斥特徵綁定定義,我是一頭霧水,忍不住把 GBM 讀成了 BGM 😅。這實際上針對的是一些特定情境下比如使用 one-hot 編碼組成的稀疏數據,這中特徵是互斥的(也就是說 one-hot 編碼中只有一位爲 1 ),而互斥特徵綁定(EFB)實際上就是將這些特徵綁定在一起,組成一個 bundle,從而實現特徵的降維(減小特徵數)。如果可實現,那麼時間複雜度從 O ( # data × # feature ) O ( \# \text { data} \times \# \text { feature } ) O ( # data × # feature ) 降低爲了 O ( # data × # bundle ) O ( \# \text { data} \times \# \text { bundle} ) O ( # data × # bundle ) 。實現上分爲兩個部分:如何找出互斥特徵進行綁定(Greedy Bundling)以及綁定後如何融合(Merge Exclusive Features)。
貪心綁定(Greedy Bundling)
在 LightGBM 論文中已經做出證明,將特徵劃分爲最小數量的互斥 bundle 是 NP 問題。所以這裏使用了貪心算法。此算法中使用無向圖圖表示各特徵之間的關係,也就是說圖中每個節點表示一個特徵,特徵之間使用邊進行聯通成爲一個網絡,邊的權重代表了是否互斥。如果互斥那麼代表兩個特徵可以合併,使用邊進行連接。但是由於通常有少量的特徵,雖然不是 100% 互斥,並且大多數情況下不會同時取非0值。若構建 Bundle 時允許少量的衝突,就能得到更少數的 bundle,進一步提高效率。可以證明,隨機的污染一部分特徵的話最多影響訓練精度 O ( [ ( 1 − γ ) n ] − 2 / 3 ) \mathcal { O } \left( [ ( 1 - \gamma ) n ] ^ { - 2 / 3 } \right) O ( [ ( 1 − γ ) n ] − 2 / 3 ) ,其中 γ \gamma γ 是最大沖突率,與之相對應的是下面僞代碼中的最大沖突個數 K K K 。所以這裏選擇將邊賦予權重表示節點間的衝突程度,同時類似於前向搜索算法,只是從先向後搜索查找最優解。那麼該貪心綁定(Greedy Bundling)的僞代碼實現如下:
Algorithm: Greedy Bundling Input: F : features, K : max conflict count Construct graph G searchOrder ← G .sortByDegree ( ) bundles ← { } , bundlesconflict ← { } for i in searchOrder do needNew ← True for j = 1 to len(bundles) d o cnt ← Conflict Cnt(bundles[j], F [ i ] ) if c n t + bundlesconflict [ i ] ≤ K then bundles[j].add ( F [ i] ) , needNew ← False break if needNew then Add F [ i ] as a new bundle to bundles Output: bundles
\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}
Algorithm: Greedy Bundling Input: F : features, K : max conflict count Construct graph G searchOrder ← G .sortByDegree ( ) bundles ← { } , bundlesconflict ← { } for i in searchOrder do needNew ← True for j = 1 to len(bundles) d o cnt ← Conflict Cnt(bundles[j], F [ i ] ) if c n t + bundlesconflict [ i ] ≤ K then bundles[j].add ( F [ i] ) , needNew ← False break if needNew then Add F [ i ] as a new bundle to bundles Output: bundles
具體步驟是:
構建有權無向圖,節點是特徵,邊是節點間的衝突程度
將圖按度(知識補充:每個節點邊的累加值或者說無權圖中節點擁有邊的個數)排序
對排序後的節點進行遍歷,並判斷現存的全部 bundle 是否與本節點符合互斥關係(判斷時仍然是從前向後遍歷 bundle),符合便加入該 bundle ,反之若不符合建立新的 bundle
該算法的時間複雜度爲 O ( # f e a t u r e 2 ) O(\#feature^2) O ( # f e a t u r e 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.
根據例子可以很容易理解互斥特徵融合的技巧,僞代碼如下:
Algorithm: Merge Exclusive Features Input: n u m Data: number of data Input: F : One bundle of exclusive features binRanges ← { 0 } , totalBin ← 0 for f in F do totalBin + = f.numBin binRanges.append(totalBin) newBin ← new Bin(numData) for i = 1 to numData d o newBin[i] ← 0 for j = 1 to len ( F ) do if F [ j ] . bin [ i ] ≠ 0 then newBin[i] ← F [ j].bin[i] + binRanges[j] Output: newBin, binRanges
\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}
Algorithm: Merge Exclusive Features Input: n u m Data: number of data Input: F : One bundle of exclusive features binRanges ← { 0 } , totalBin ← 0 for f in F do totalBin + = f.numBin binRanges.append(totalBin) newBin ← new Bin(numData) for i = 1 to numData d o newBin[i] ← 0 for j = 1 to len ( F ) do if F [ j ] . bin [ i ] = 0 then newBin[i] ← F [ j].bin[i] + binRanges[j] Output: newBin, binRanges
具體步驟是:在該 bundle 中,將當前特徵前已遍歷的全部特徵擁有的桶的總個數作爲偏移量,將全部的特徵的桶進行直方圖合併,示意圖如下:
EFB算法可以將大量的互斥特徵捆綁到較少的密集特徵上,有效地避免了對零特徵值的不必要計算。同時實際上,也可以通過爲每個特徵使用一個表來記錄具有非零值的數據,忽略零特徵值,進而達到優化基本的基於直方圖的算法的目的。通過掃描此表中的數據,特徵的直方圖構建成本將從 O ( # d a t a ) O(\#data) O ( # d a t a ) 更改爲O ( # n o n _ z e r o _ d a t a ) O(\#non\_zero\_data) O ( # n o n _ z e r o _ d a t a ) 。然而,這種方法需要額外的內存和計算開銷來維護整個樹生長過程中的每個特徵表。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 和普通的決策樹差不多,只是在尋找最優切分點上有所不同。
具體的算法僞代碼如下:
Algorithm : PV-Tree FindBestSplit Input: Dataset D localHistograms = ConstructHistograms(D) ▹ Local Voting splits = [] for all H in localHistograms do splits.Push(H.FindBestSplit()) end for localTop = splits.TopKByGain(K) ▹ Gather all candidates allCandidates = AllGather(localTop) ▹ Global Voting globalTop = allCandidates.TopKByMajority(2*K) ▹ Merge global histograms globalHistograms = Gather(globalTop, localHistograms) bestSplit = globalHistograms.FindBestSplit() return bestSplit
\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}
Algorithm : PV-Tree FindBestSplit Input: Dataset D localHistograms = ConstructHistograms(D) ▹ Local Voting splits = [] for all H in localHistograms do splits.Push(H.FindBestSplit()) end for localTop = splits.TopKByGain(K) ▹ Gather all candidates allCandidates = AllGather(localTop) ▹ Global Voting globalTop = allCandidates.TopKByMajority(2*K) ▹ Merge global histograms globalHistograms = Gather(globalTop, localHistograms) bestSplit = globalHistograms.FindBestSplit() return bestSplit
代碼中的 FindBestSplit 函數也就是單機運行函數實現如下:
Algorithm : FindBestSplit Input: DataSet for all X in D.Attribute d o ▹ Construct Histogram H = new Histogram() for all x in X do H.binAt(x.bin).Put(x.label) end for ▹ Find Best Split leftSum = new HistogramSum() for all bin in H do leftSum = leftSum + H.binAt(bin) rightSum = H.AllSum - leftSum split.gain = CalSplitGain(leftSum, rightSum) bestSplit = ChoiceBetterOne(split,bestSplit) end for end for return bestSplit
\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}
Algorithm : FindBestSplit Input: DataSet for all X in D.Attribute d o ▹ Construct Histogram H = new Histogram() for all x in X do H.binAt(x.bin).Put(x.label) end for ▹ Find Best Split leftSum = new HistogramSum() for all bin in H do leftSum = leftSum + H.binAt(bin) rightSum = H.AllSum - leftSum split.gain = CalSplitGain(leftSum, rightSum) bestSplit = ChoiceBetterOne(split,bestSplit) end for end for return bestSplit
使用經驗(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 。