1. 程式人生 > >LightGBM論文閱讀總結

LightGBM論文閱讀總結

1. Abstract

在之前我介紹過XGBoost,這次想跟大家分享一下LightGBM,它是一款常用的GBDT工具包,由微軟亞洲研究院(MSRA)進行開發。LightGBM論文的標題為A Highly Efficient Gradient Boosting Decision Tree。這說明LightGBM它是對於XGB提升效能的版本。而LightGBM相對於其他GBM來說具有相近的準確率而且是其訓練速度20倍。

2. Comparse with XGB

LightGBM主要的對比物件就是XGB,所以我們先說一下XGB有什麼優缺點先。

優點:

  • XGB利用了二階梯度來對節點進行劃分,相對其他GBM來說,精度更加高。
  • 利用區域性近似演算法對分裂節點的貪心演算法優化,取適當的eps時,可以保持演算法的效能且提高演算法的運算速度。
  • 在損失函式中加入了L1/L2項,控制模型的複雜度,提高模型的魯棒性。
  • 提供平行計算能力,主要是在樹節點求不同的候選的分裂點的Gain Infomation(分裂後,損失函式的差值)
  • Tree Shrinkage,column subsampling等不同的處理細節。

缺點:

  • 需要pre-sorted,這個會耗掉很多的記憶體空間(2 * #data * # features)
  • 資料分割點上,由於XGB對不同的資料特徵使用pre-sorted演算法而不同特徵其排序順序是不同的,所以分裂時需要對每個特徵單獨做依次分割,遍歷次數為#data * #features來將資料分裂到左右子節點上。
  • 儘管使用了局部近似計算,但是處理粒度還是太細了
  • 由於pre-sorted處理資料,在尋找特徵分裂點時(level-wise),會產生大量的cache隨機訪問。

因此LightGBM針對這些缺點進行了相應的改進。

  1. LightGBM基於histogram演算法代替pre-sorted所構建的資料結構,利用histogram後,會有很多有用的tricks。例如histogram做差,提高了cache命中率(主要是因為使用了leaf-wise)。
  2. 在機器學習當中,我們面對大資料量時候都會使用取樣的方式(根據樣本權值)來提高訓練速度。又或者在訓練的時候賦予樣本權值來關於於某一類樣本(如Adaboost)。LightGBM利用了GOSS來做取樣演算法。
  3. 由於histogram演算法對稀疏資料的處理時間複雜度沒有pre-sorted好。因為histogram並不管特徵值是否為0。因此我們採用了EFB來預處理稀疏資料。

下來我們針對這些改進來說明一下。

3 Histogram演算法

直方圖演算法的基本思想:先把連續的浮點特徵值離散化成k個整數,同時構造一個寬度為k的直方圖。遍歷資料時,根據離散化後的值作為索引在直方圖中累積統計量,當遍歷一次資料後,直方圖累積了需要的統計量,然後根據直方圖的離散值,遍歷尋找最優的分割點。

LightGBM裡預設的訓練決策樹時使用直方圖演算法,XGBoost裡現在也提供了這一選項,不過預設的方法是對特徵預排序,直方圖演算法是一種犧牲了一定的切分準確性而換取訓練速度以及節省記憶體空間消耗的演算法

  • 在訓練決策樹計算切分點的增益時,預排序需要對每個樣本的切分位置計算,所以時間複雜度是O(#data)而LightGBM則是計算將樣本離散化為直方圖後的直方圖切割位置的增益即可,時間複雜度為O(#bins),時間效率上大大提高了(初始構造直方圖是需要一次O(#data)的時間複雜度,不過這裡只涉及到加和操作)
  • 直方圖做差進一步提高效率,計算某一節點的葉節點的直方圖可以通過將該節點的直方圖與另一子節點的直方圖做差得到,所以每次分裂只需計算分裂後樣本數較少的子節點的直方圖然後通過做差的方式獲得另一個子節點的直方圖,進一步提高效率。
  • 節省記憶體
    • 將連續資料離散化為直方圖的形式,對於資料量較小的情形可以使用小型的資料型別來儲存訓練資料
    • 不必像預排序一樣保留額外的對特徵值進行預排序的資訊
  • 減少了並行訓練的通訊代價

現在來看看直方圖優化是如何優化的,當然這個優化也是在處理節點分裂的時候。在處理連續特徵的時候,如果你想要快速找到最佳的分裂節點要麼像之前說到的那樣對特徵值採用預排序的方式來快速得到最佳的分裂特徵值,在這裡直方圖就是先將特徵值先做裝箱處理,裝箱處理是特徵工程中常見的處理方式之一了,下面給個例子說明特徵裝箱操作。

[0,0.3)—>0

[0.3,0.7)—->1

就是將某個區間的資料對映到離散的資料值。

說完了裝箱操作現在看一下微軟論文中提到的直方圖優化的流程圖:

在這裡插入圖片描述

最外面的 for 迴圈表示的意思是對當前模型下所有的葉子節點處理,gbdt 會訓練多個樹模型,這個舉例可能是其中任何一個模型吧。

第二個 for 迴圈開始要對某個葉子分裂處理處理,這一步就開始遍歷所有的特徵了,你選取的樣本可能有 50、100 或者 1000 維等特徵,為了找出的最佳的特徵作為分裂的依據,那麼這一個 for 迴圈就是幹這件事情。

依次往下看,對於當前這個特徵新建一個直方圖,現在又碰到一個 for 迴圈了 ,這個 for 迴圈乾的事情就是遍歷所有的樣本來構建直方圖,哈哈,此時就用到了之前所描述的裝箱操作了,直方圖的每個 bin 中包含了一定的樣本,在此計算每個 bin 中的樣本的梯度之和並對 bin 中的樣本記數。

其中f.bins[i]為特徵f在樣本i的對應bin;H[f.bins[i]]則表示對bin進行累加,該bin就是特徵f在樣本i的對應bin。

下面就是最後一個 for 迴圈了,這個開始遍歷所有的 bin,找到適合分裂的最佳 bin,解釋一下這其中涉及到達變數定義

S L S_L 是當前分裂 bin 左邊所有 bin 的集合,對比理解 S R S_R ,那麼 S P S_P 其中的 P 就是 parent 的意思,就是父節點,傳統的決策樹會有分裂前後資訊增益的計算,典型的 ID3 或者 C45 之類,在這裡我們也會計算,但是 S R S_R 中所有 bin 的梯度之和不需要在額外計算了,直接使用父節點的減去左邊的就得到了,是不是覺得很厲害的樣子。反正我覺得這點是真的很神奇的地方,雖然看起來很簡單,但是也是一個小魔法。

公式的中 loss 就是來衡量分裂的好壞的,在遍歷完所有的特徵之後根據 loss,理論上 loss 最小的特徵會被選中作為最佳的分裂節點。

這樣的化這個這個直方圖優化處理的流程應該就說明白的,基本上都是按照論文中的流程來一步一步解釋。下面就來看看這種直方圖優化的優缺點吧!

直方圖演算法小結:

優點:

  • 首先,最明顯就是記憶體消耗的降低,因為pre-sorted演算法需要儲存起來每一個特徵的排序結構,所以其需要的記憶體大小是2 * #data * #feature * 4Bytes(不僅需要保持每個樣本對應的排序儲存索引,還要儲存每個樣本對應每個特徵的梯度值,即行為樣本,列為特徵,中間表格內容為各個樣本對應在該特徵的值,用float_32儲存特徵值;但進行split finding時候,需要對每列值進行從小到大排序,故而需要每行樣本不僅要儲存對應特徵下的特徵值,還需儲存對應特徵的排序索引,這個排序索引值用int_32儲存,而histogram只需儲存離散值bin value(EFB會談到bin)而且我們不需要原始的feature value,所以佔用的記憶體大小為:#data * # feature * 1Byte,因為離散值bin value使用uint8_t已經足夠了,記憶體消耗可以降低為原來的 1/8。(bin value本身就是按照大小進行分箱的結果,而histogram更多是將樣本特徵值對映到直方圖上,也正是因為這一特性,故而連排序也省了,再加上分箱的粗粒度操作,最後只需儲存bin value即可,用uint_8儲存bin value,總體記憶體消耗降為原來的 1/8。此外也正是這一特性,直方圖在記憶體空間連續儲存,這也進一步有助於提高快取命中率,因為它訪問梯度是連續的(直方圖);

    比如離散為256個Bin時,只需要用8位整形就可以儲存一個樣本被對映為哪個Bin(這個bin可以說就是轉換後的特徵),對比預排序的Exact greedy演算法來說(用int_32來儲存索引+ 用float_32儲存特徵值),可以節省7/8的空間。

  • 計算效率也得到提高,預排序的Exact greedy對每個特徵都需要遍歷一遍資料,並計算增益,複雜度為O(#feature×#data)。而直方圖演算法在建立完直方圖後,只需要對每個特徵遍歷直方圖即可,複雜度為O(#feature×#bins)。

  • 提高快取命中率,因為它訪問梯度是連續的(直方圖)。

  • 此外,在資料並行的時候,直方圖演算法可以大幅降低通訊代價。(資料並行、特徵並行在本文後面講解)

缺點:

  • 當然,Histogram演算法並不是完美的。由於特徵被離散化後,找到的並不是很精確的分割點,所以會對結果產生影響。但在不同的資料集上的結果表明,離散化的分割點對最終的精度影響並不是很大,甚至有時候會更好一點。原因是決策樹本來就是弱模型,分割點是不是精確並不是太重要;較粗的分割點也有正則化的效果,可以有效地防止過擬合;即使單棵樹的訓練誤差比精確分割的演算法稍大,但在梯度提升(Gradient Boosting)的框架下沒有太大的影響。
  • 預處理能夠忽略零值特徵,減少訓練代價;而直方圖不能對稀疏進行優化,只是計算累加值(累加梯度和樣本數)。但是,LightGBM 對稀疏進行了優化:只用非零特徵構建直方圖。

LightGBM 為何使用直方圖這種比較粗的分割節點方法,還能達到比較好的效果?

雖然分割的精度變差了,但是對最後結果的影響不是很大,主要由於決策樹是弱模型, 分割點是不是精確並不是太重要 ;較粗的分割點也有正則化的效果,可以有效地防止過擬合;即使單棵樹的訓練誤差比精確分割的演算法稍大,但在梯度提升(Gradient Boosting)的框架下沒有太大的影響。

4. 直方圖演算法改進

直方圖演算法仍有優化的空間,建立直方圖的複雜度為O(#feature×#data),如果能降低特徵數或者降低樣本數,訓練的時間會大大減少。以往的降低樣本數的方法中,要麼不能直接用在GBDT上,要麼會損失精度。而降低特徵數的直接想法是去除弱的特徵(通常用PCA完成),然而,這些方法往往都假設特徵是有冗餘的,然而通常特徵是精心設計的,去除它們中的任何一個可能會影響訓練精度。因此LightGBM提出了GOSS演算法和EFB演算法。

4.1 Gradient-based One-Side Sampling(GOSS)——用於減少訓練樣本數(行)

在AdaBoost中,權重向量w很好的反應了樣本的重要性。而在GBDT中,則沒有這樣的直接權重來反應樣本的重要程度。但是梯度是一個很好的指標,如果一個樣本的梯度很小,說明該樣本的訓練誤差很小,或者說該樣本已經得到了很好的訓練(well-trained)

要減少樣本數,一個直接的想法是拋棄那些梯度很小的樣本,但是這樣訓練集的分佈會被改變,可能會使得模型準確率下降。LightGBM提出 Gradient-based One-Side Sampling (GOSS)來解決這個問題。

GOSS的做法虛擬碼描述如下:

在這裡插入圖片描述

演算法分析:

  1. 根據梯度的絕對值將樣本進行降序排序
  2. 選擇前a×100%的樣本,這些樣本稱為A
  3. 剩下的資料(1−a)×100% 的資料中,隨機抽取b×100%的資料,這些樣本稱為B
  4. 在計算增益的時候,放大樣本B中的梯度(1−a)/b 倍
  5. 關於g,在具體的實現中是一階梯度和二階梯度的乘積,見Github的實現( LightGBM/src/boosting/goss.hpp)

使用GOSS進行取樣,使得訓練演算法更加的關注沒有充分訓練(under-trained)的樣本,並且只會稍微的改變原有的資料分佈。

原有的在特徵 j 值為 d 處分資料帶來的增益可以定義為:
V j O ( d ) = 1 n O ( ( x i O : x i j d g i ) 2 n l O j ( d ) + ( x i O : x i j > d g i ) 2 n r O j ( d ) ) V_{j|O}(d) = \frac{1}{n_O}\left(\frac{(\sum_{x_i\in O:x_{ij} \le d}g_i)^2}{n_{l|O}^j(d)} + \frac{(\sum_{x_i\in O:x_{ij} \gt d}g_i)^2}{n_{r|O}^j(d)} \right)

其中:

  • O為在決策樹待分裂節點的訓練集
  • n o = I ( x i O ) n_o = \sum I(x_i \in O)
  • n l O j ( d ) = I [ x i O : x i j d ]   a n d   n r O j ( d ) = I [ x i O : x i j > d ] n_{l|O}^j(d) = \sum I[x_i \in O: x_{ij} \le d]\ and\ n_{r|O}^j(d) = \sum I[x_i \in O: x_{ij} \gt d]

而使用GOSS後,增益定義為:

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{(\sum_{x_i\in A_l} g_i + \frac{1-a}{b} \sum_{x_i\in B_l} g_i)^2 }{n_{l}^j(d)} + \frac{(\sum_{x_i\in A_r} g_i + \frac{1-a}{b} \sum_{x_i\in B_l} g_r)^2 }{n_{r}^j(d)} \right)