1. 程式人生 > >Lightgbm 直方圖優化演算法深入理解

Lightgbm 直方圖優化演算法深入理解

一、概述

在之前的介紹Xgboost的眾多博文中,已經介紹過,在樹分裂計算分裂特徵的增益時,xgboost 採用了預排序的方法來處理節點分裂,這樣計算的分裂點比較精確。但是,也造成了很大的時間開銷。為了解決這個問題,Lightgbm 選擇了基於 histogram 的決策樹演算法。相比於 pre-sorted演算法,histogram 在記憶體消耗和計算代價上都有不少優勢。

histogram演算法簡單來說,就是先對特徵值進行裝箱處理,形成一個一個的bins。對於連續特徵來說,裝箱處理就是特徵工程中的離散化:如[0,0.3)—>0,[0.3,0.7)—->1等。在Lightgbm中預設的#bins為256(1個位元組的能表示的長度,可以設定)。對於分類特徵來說,則是每一種取值放入一個bin,且當取值的個數大於max bin數時,會忽略那些很少出現的category值。

在節點分裂的時候,這時候就不需要按照預排序演算法那樣,對於每個特徵都計算#data遍了,而是隻需要計算#bins遍,這樣就大大加快了訓練速度。

二、演算法流程

下面是訓練過程中利用直方圖尋找最佳分割點的演算法。
在這裡插入圖片描述

從演算法中可以看到:直方圖優化演算法需要在訓練前預先把特徵值轉化為bin value,也就是對每個特徵的取值做個分段函式,將所有樣本在該特徵上的取值劃分到某一段(bin)中。最終把特徵取值從連續值轉化成了離散值。需要注意得是:feature value對應的bin value在整個訓練過程中是不會改變的

最外面的 for 迴圈表示的意思是對當前模型下所有的葉子節點處理,需要遍歷所有的特徵,來找到增益最大的特徵及其劃分值,以此來分裂該葉子節點。

第二個 for 迴圈開始要對某個葉子分裂處理處理,這一步就開始遍歷所有的特徵了。對於每個特徵,首先為其建立一個直方圖。這個直方圖儲存了兩類資訊,分別是每個bin中樣本的梯度之和(H[f.bins[i]].gH[ f.bins[i] ].g),還有就是每個bin中樣本數量(H[f.bins[i]].nH[ f.bins[i] ].n

第三個 for 迴圈遍歷所有樣本,累積上述的兩類統計值到樣本所屬的bin中。即直方圖的每個 bin 中包含了一定的樣本,在此計算每個 bin 中的樣本的梯度之和並對 bin 中的樣本記數。

最後一個for迴圈,遍歷所有bin,分別以當前bin作為分割點,累加其左邊的bin至當前bin的梯度和(S

LS_L)以及樣本數量(nLn_L),並與父節點上的總梯度和(SpS_p)以及總樣本數量(npn_p)相減,得到右邊所有bin的梯度和(SRS_R)以及樣本數量(nRn_R),帶入公式,計算出增益,在遍歷過程中取最大的增益,以此時的特徵和bin的特徵值作為分裂節點的特徵和分裂特徵取值。

三、histogram演算法與 pre-sorted演算法對比

3.1 優勢

  • Pre-sorted 演算法需要的記憶體約是訓練資料的兩倍(2 * #data * #features* 4Bytes),它需要用32位浮點(4Bytes)來儲存 feature value,並且對每一列特徵,都需要一個額外的排好序的索引,這也需要32位(4Bytes)的儲存空間。因此是(2 * #data * #features* 4Bytes)。而對於 histogram 演算法,則只需要(#data * #features * 1Bytes)的記憶體消耗,僅為 pre-sorted演算法的1/8。因為 histogram 演算法僅需要儲存 feature bin value (離散化後的數值),不需要原始的 feature value,也不用排序,而 bin value 用 1Bytes(256 bins) 的大小一般也就足夠了。

  • 在計算上的優勢則主要體現在“資料分割”。決策樹演算法有兩個主要操作組成,一個是“尋找分割點”,另一個是“資料分割”。從演算法時間複雜度來看,Histogram 演算法和 pre-sorted 演算法在“尋找分割點”的代價是一樣的,都是O(#feature*#data)。而在“資料分割”時,pre-sorted 演算法需要O(#feature*#data),而 histogram 演算法是O(#data)。因為 pre-sorted 演算法的每一列特徵的順序都不一樣,分割的時候需要對每個特徵單獨進行一次分割。Histogram演算法不需要排序,所有特徵共享同一個索引表,分割的時候僅需對這個索引表操作一次就可以。(更新: 這一點不完全正確,pre-sorted 與 level-wise 結合的時候,其實可以共用一個索引表(row_idx_to_tree_node_idx)。然後在尋找分割點的時候,同時操作同一層的節點,省去分割的步驟。但這樣做的問題是會有非常多隨機訪問,有很大的chche miss,速度依然很慢。)
    (這裡也不是特別明白,需要再深入研究)

  • 另一個計算上的優勢則是大幅減少了計算分割點增益的次數。對於每一個特徵,pre-sorted 需要對每一個不同特徵值都計算一次分割增益,而 histogram 只需要計算 #bins次。

  • 最後,在資料並行的時候,用 histgoram 可以大幅降低通訊代價。用 pre-sorted 演算法的話,通訊代價是非常大的(幾乎是沒辦法用的)。所以 xgoobst 在並行的時候也使用 histogram 進行通訊。
    (資料並行的優化是Lightgbm的令一個亮點,這裡不是特別理解,需要再深入研究

3.2 劣勢

  • histogram 演算法不能找到很精確的分割點,訓練誤差沒有 pre-sorted 好。但從實驗結果來看, histogram 演算法在測試集的誤差和 pre-sorted 演算法差異並不是很大,甚至有時候效果更好。實際上可能決策樹對於分割點的精確程度並不太敏感,而且較“粗”的分割點也自帶正則化的效果,再加上boosting演算法本身就是弱分類器的整合。

四、直方圖做差加速

在histogram演算法上一個trick是histogram 做差加速。一個容易觀察到的現象:一個葉子的直方圖可以由它的父親節點的直方圖與它兄弟的直方圖做差得到。利用這個方法,Lightgbm 可以在構造一個葉子(含有較少資料)的直方圖後,可以用非常微小的代價得到它兄弟葉子(含有較多資料)的直方圖。

因為構建兄弟葉子的直方圖是做差得到的,時間複雜度僅為O(#bins),幾乎可以忽略,因此,比起不做差得到的兄弟節點的直方圖,在速度上可以提升一倍。

舉例來說明什麼是histogram 做差加速。

假設我們共有10個樣本,2個特徵。
特徵f1f_1為類別特徵,共有2個不同的屬性值,分成了桶b11b_{11}b12b_{12};桶b11b_{11}的樣本數是4個,桶b12b_{12}的樣本數是6個。
特徵f2f_2為連續特徵,離散化後分成了桶b21b_{21}b22b_{22}b23b_{23};桶b21b_{21}的樣本數是2個,桶b22b_{22}的樣本數是4個,桶b23b_{23}的樣本數是4個。

我們依次計算每個bin作為分割點的增益,假設在桶b11b_{11}作為分割點時增益最大,那麼以桶b11b_{11}分割,這時候:

a. 左子節點有4個樣本。特徵f1f_1的桶b11b_{11}的樣本數為4個,桶b12b_{12}樣本為0個。假設特徵f2f_2仍有3個桶b21b_{21}b22b_{22}b23b_{23},且桶b21b_{21}的樣本數是1個,桶b22b_{22}的樣本數是2個,桶b23b_{23}的樣本數是1個。這時候左子節點2個特徵的直方圖已經構建成功。

b. 左子節點有4個樣本,右子節點自然有6個樣本。這時候右子節點的2個特徵的直方圖就可以根據父節點和左子節點的2個特徵的直方圖做差得到:
特徵f1f_1只有桶b12b_{12},且樣本數為6個(6-0=0)。桶b11b_{11}樣本數為0個(4-4=0)。
特徵f2f_2仍有3個桶b21b_{21}b22b_{22}b23b_{23},且桶b21b_{21}的樣本數是1個(2-1=1),桶b22b_{22}的樣本數是2個(4-2=2),桶b23b_{23}的樣本數是3個(4-1=3)。
這時候右子節點2個特徵的直方圖也已經構建成功。

下圖表示了整個過程:
在這裡插入圖片描述

深入分析就可以知道,左子節點計算直方圖的複雜度是基於樣本個數的,而左子節點計算直方圖的複雜度卻是基於桶的個數的。因此,大大節省了構建直方圖的時間。

五、參考文獻