LightGBM(lgb)詳解

1. LightGBM簡介

GBDT (Gradient Boosting Decision Tree) 是機器學習中一個長盛不衰的模型,其主要思想是利用弱分類器(決策樹)迭代訓練以得到最優模型,該模型具有訓練效果好、不易過擬合等優點。GBDT不僅在工業界應用廣泛,通常被用於多分類、點擊率預測、搜索排序等任務;在各種數據挖掘競賽中也是致命武器,據統計Kaggle上的比賽有一半以上的冠軍方案都是基於GBDT。而LightGBM(Light Gradient Boosting Machine)是一個實現GBDT算法的框架,支持高效率的並行訓練,並且具有更快的訓練速度、更低的內存消耗、更好的準確率、支持分佈式可以快速處理海量數據等優點。

1.1 LightGBM提出的動機

常用的機器學習算法,例如神經網絡等算法,都可以以mini-batch的方式訓練,訓練數據的大小不會受到內存限制。而GBDT在每一次迭代的時候,都需要遍歷整個訓練數據多次。如果把整個訓練數據裝進內存則會限制訓練數據的大小;如果不裝進內存,反覆地讀寫訓練數據又會消耗非常大的時間。尤其面對工業級海量的數據,普通的GBDT算法是不能滿足其需求的。

LightGBM提出的主要原因就是爲了解決GBDT在海量數據遇到的問題,讓GBDT可以更好更快地用於工業實踐。

1.2 XGBoost的缺點及LightGBM的優化

(1)XGBoost的缺點

在LightGBM提出之前,最有名的GBDT工具就是XGBoost了,它是基於預排序方法的決策樹算法。這種構建決策樹的算法基本思想是:

  • 首先,對所有特徵都按照特徵的數值進行預排序。
  • 其次,在遍歷分割點的時候用O(#data)的代價找到一個特徵上的最好分割點。
  • 最後,在找到一個特徵的最好分割點後,將數據分裂成左右子節點。

這樣的預排序算法的優點是能精確地找到分割點。但是缺點也很明顯:

首先,空間消耗大。這樣的算法需要保存數據的特徵值,還保存了特徵排序的結果(例如,爲了後續快速的計算分割點,保存了排序後的索引),這就需要消耗訓練數據兩倍的內存。

其次,時間上也有較大的開銷,在遍歷每一個分割點的時候,都需要進行分裂增益的計算,消耗的代價大。

最後,對cache優化不友好。在預排序後,特徵對梯度的訪問是一種隨機訪問,並且不同的特徵訪問的順序不一樣,無法對cache進行優化。同時,在每一層長樹的時候,需要隨機訪問一個行索引到葉子索引的數組,並且不同特徵訪問的順序也不一樣,也會造成較大的cache miss。

(2)LightGBM的優化

爲了避免上述XGBoost的缺陷,並且能夠在不損害準確率的條件下加快GBDT模型的訓練速度,lightGBM在傳統的GBDT算法上進行了如下優化:

  • 基於Histogram的決策樹算法。
  • 單邊梯度採樣 Gradient-based One-Side Sampling(GOSS):使用GOSS可以減少大量只具有小梯度的數據實例,這樣在計算信息增益的時候只利用剩下的具有高梯度的數據就可以了,相比XGBoost遍歷所有特徵值節省了不少時間和空間上的開銷。
  • 互斥特徵捆綁 Exclusive Feature Bundling(EFB):使用EFB可以將許多互斥的特徵綁定爲一個特徵,這樣達到了降維的目的。
  • 帶深度限制的Leaf-wise的葉子生長策略:大多數GBDT工具使用低效的按層生長 (level-wise)的決策樹生長策略,因爲它不加區分的對待同一層的葉子,帶來了很多沒必要的開銷。實際上很多葉子的分裂增益較低,沒必要進行搜索和分裂。LightGBM使用了帶有深度限制的按葉子生長 (leaf-wise) 算法。
  • 直接支持類別特徵(Categorical Feature)
  • 支持高效並行
  • Cache命中率優化

下面我們就詳細介紹以上提到的lightGBM優化算法。

2. LightGBM的基本原理

2.1 基於Histogram的決策樹算法

(1)直方圖算法

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

在這裏插入圖片描述直方圖算法簡單理解爲:首先確定對於每一個特徵需要多少個箱子(bin)併爲每一個箱子分配一個整數;然後將浮點數的範圍均分成若干區間,區間個數與箱子個數相等,將屬於該箱子的樣本數據更新爲箱子的值;最後用直方圖(#bins)表示。看起來很高大上,其實就是直方圖統計,將大規模的數據放在了直方圖中。

我們知道特徵離散化具有很多優點,如存儲方便、運算更快、魯棒性強、模型更加穩定等。對於直方圖算法來說最直接的有以下兩個優點:

  • 內存佔用更小:直方圖算法不僅不需要額外存儲預排序的結果,而且可以只保存特徵離散化後的值,而這個值一般用88位整型存儲就足夠了,內存消耗可以降低爲原來的1/81/8。也就是說XGBoost需要用3232位的浮點數去存儲特徵值,並用3232位的整形去存儲索引,而 LightGBM只需要用3232位去存儲直方圖,內存相當於減少爲1/81/8

  • 計算代價更小:預排序算法XGBoost每遍歷一個特徵值就需要計算一次分裂的增益,而直方圖 算法LightGBM只需要計算 kk 次( kk 可以認爲是常數),直接將時間複雜度從 O(# data # feature )O(\# \text { data } * \# \text { feature }) 降低到 O(k# feature ),O(k * \# \text { feature }), 而我們爲道 #data>>k\# d a t a>>k

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

(2)直方圖做差加速

LightGBM另一個優化是Histogram(直方圖)做差加速。一個葉子的直方圖可以由它的父親節點的直方圖與它兄弟的直方圖做差得到,在速度上可以提升一倍。通常構造直方圖時,需要遍歷該葉子上的所有數據,但直方圖做差僅需遍歷直方圖的k個桶。在實際構建樹的過程中,LightGBM還可以先計算直方圖小的葉子節點,然後利用直方圖做差來獲得直方圖大的葉子節點,這樣就可以用非常微小的代價得到它兄弟葉子的直方圖。

在這裏插入圖片描述注意:XGBoost 在進行預排序時只考慮非零值進行加速,而 LightGBM 也採用類似策略:只用非零特徵構建直方圖。

2.2 帶深度限制的 Leaf-wise 算法

在Histogram算法之上,LightGBM進行進一步的優化。首先它拋棄了大多數GBDT工具使用的按層生長 (level-wise) 的決策樹生長策略,而使用了帶有深度限制的按葉子生長 (leaf-wise) 算法。

XGBoost 採用 Level-wise 的增長策略,該策略遍歷一次數據可以同時分裂同一層的葉子,容易進行多線程優化,也好控制模型複雜度,不容易過擬合。但實際上Level-wise是一種低效的算法,因爲它不加區分的對待同一層的葉子,實際上很多葉子的分裂增益較低,沒必要進行搜索和分裂,因此帶來了很多沒必要的計算開銷。
在這裏插入圖片描述

LightGBM採用Leaf-wise的增長策略,該策略每次從當前所有葉子中,找到分裂增益最大的一個葉子,然後分裂,如此循環。因此同Level-wise相比,Leaf-wise的優點是:在分裂次數相同的情況下,Leaf-wise可以降低更多的誤差,得到更好的精度;Leaf-wise的缺點是:可能會長出比較深的決策樹,產生過擬合。因此LightGBM會在Leaf-wise之上增加了一個最大深度的限制,在保證高效率的同時防止過擬合。
在這裏插入圖片描述
2.3 單邊梯度採樣算法

Gradient-based One-Side Sampling 應該被翻譯爲單邊梯度採樣(GOSS)。GOSS算法從減少樣本的角度出發,排除大部分小梯度的樣本,僅用剩下的樣本計算信息增益,它是一種在減少數據量和保證精度上平衡的算法。

AdaBoost中,樣本權重是數據重要性的指標。然而在GBDT中沒有原始樣本權重,不能應用權重採樣。幸運的是,我們觀察到GBDT中每個數據都有不同的梯度值,對採樣十分有用。即梯度小的樣本,訓練誤差也比較小,說明數據已經被模型學習得很好了,直接想法就是丟掉這部分梯度小的數據。然而這樣做會改變數據的分佈,將會影響訓練模型的精確度,爲了避免此問題,提出了GOSS算法。

GOSS是一個樣本的採樣算法,目的是丟棄一些對計算信息增益沒有幫助的樣本留下有幫助的。根 據計算信息增益的定義,梯度大的樣本對信息增益有更大的影響。因此,GOSS在進行數據採樣的 時候只保留了梯度較大的數據,但是如果直接將所有梯度較小的數據都丟棄掉勢必會影響數據的總 體分佈。所以,GOSS首先將要進行分裂的特徵的所有取值按照絕對值大小降序排序(XGBoost一 樣也進行了排序,但是LightGBM不用保存排序後的結果),選取絕對值最大的 a100%a * 100 \% 個數 據。然後在剩下的較小梯度數據中隨機選擇 b100%b * 100 \% 個數據。接着將這 b100%b * 100 \% 個數據乘 以一個常數 1ab,\frac{1-a}{b}, 這樣算法就會更關注訓練不足的樣本, 而不會過多改變原數據集的分佈。最 後使用這 (a+b)100%(a+b) * 100 \% 個數據來計算信息增益。下圖是GOSS的具體算法。

在這裏插入圖片描述
2.4 互斥特徵捆綁算法

高維度的數據往往是稀疏的,這種稀疏性啓發我們設計一種無損的方法來減少特徵的維度。通常被 抹綁的特徵都是互斥的(即特徵不會同時爲非零值,像one-hot),這樣兩個特徵描綁起來纔不會 丟失信息。如果兩個特徵並不是完全互斥 (部分情況下兩個特徵都是非零值),可以用一個指標對 特徵不互斥程度進行像量,稱之爲衝突比率,當這個值較小時,我們可以選擇把不完全互斥的兩個 特徵肺綁,而不影響最後的精度。

互斥特徵肺綁算法 (Exclusive Feature Bundling, EFB) 指出如 果將一些特徵進行融合綁定,則可以降低特徵數量。這樣在構建直方圖時的時間複雜度從O(#data* # feature)變爲 O(#data* # bundle), 這裏 #\#bundle 指特徵融合綁 定後特徵包的個數, 且 #bundle 遠小於 # feature 。

3. LightGBM的工程優化

我們將論文《Lightgbm: A highly efficient gradient boosting decision tree》中沒有提到的優化方案,而在其相關論文《A communication-efficient parallel algorithm for decision tree》中提到的優化方案,放到本節作爲LightGBM的工程優化來向大家介紹。

3.1 直接支持類別特徵

實際上大多數機器學習工具都無法直接支持類別特徵,一般需要把類別特徵,通過 one-hot 編碼,轉化到多維的0/1特徵,降低了空間和時間的效率。但我們知道對於決策樹來說並不推薦使用 one-hot 編碼,尤其當類別特徵中類別個數很多的情況下,會存在以下問題:

1,會產生樣本切分不平衡問題,導致切分增益非常小(即浪費了這個特徵)。使用 one-hot編碼,意味着在每一個決策節點上只能使用one vs rest(例如是不是狗,是不是貓等)的切分方式。

例如,動物類別切分後,會產生是否狗,是否貓等一系列特徵,這一系列特徵上只有少量樣本爲 1,大量樣本爲 0,這時候切分樣本會產生不平衡,這意味着切分增益也會很小。較小的那個切分樣本集,它佔總樣本的比例太小,無論增益多大,乘以該比例之後幾乎可以忽略;較大的那個拆分樣本集,它幾乎就是原始的樣本集,增益幾乎爲零。比較直觀的理解就是不平衡的切分和不切分沒有區別。

2,會影響決策樹的學習。因爲就算可以對這個類別特徵進行切分,獨熱編碼也會把數據切分到很多零散的小空間上,如下圖左邊所示。而決策樹學習時利用的是統計信息,在這些數據量小的空間上,統計信息不準確,學習效果會變差。但如果使用下圖右邊的切分方法,數據會被切分到兩個比較大的空間,進一步的學習也會更好。下圖右邊葉子節點的含義是X=A或者X=C放到左孩子,其餘放到右孩子。
在這裏插入圖片描述
而類別特徵的使用在實踐中是很常見的。且爲了解決one-hot編碼處理類別特徵的不足, LightGBM優化了對類別特徵的支持,可以直接輸入類別特徵,不需要額外的0/1展開。LightGBM 採用 many-vs-many 的切分方式將類別特徵分爲兩個子集,實現類別特徵的最優切分。假設某維 特徵有 k 個類別,則有 2(k1)12^{(k-1)}-1 種可能, 時間複雜度爲 O(2k),O\left(2^{k}\right), LightGBM 基於 Fisher的 《On Grouping For Maximum Homogeneity》論文實現了 O(klogk) 的時間複雜度。

算法流程如下圖所示,在枚舉分割點之前,先把直方圖按照每個類別對應的label均值進行排序; 然後按照排序的結果依次枚舉最優分割點。從下圖可以看到, Sum(y)Count(y)\frac{S u m(y)}{\operatorname{Count}(y)} 爲類別的均值。當然,這個方法很容易過擬合,所以LightGBM裏面還增加了很多對於這個方法的約束和正則化。

在這裏插入圖片描述在Expo數據集上的實驗結果表明,相比0/1展開的方法,使用LightGBM支持的類別特徵可以使訓練速度加速8倍,並且精度一致。更重要的是,LightGBM是第一個直接支持類別特徵的GBDT工具。

3.2 支持高效並行

(1)特徵並行

特徵並行的主要思想是不同機器在不同的特徵集合上分別尋找最優的分割點,然後在機器間同步最優的分割點。XGBoost使用的就是這種特徵並行方法。這種特徵並行方法有個很大的缺點:就是對數據進行垂直劃分,每臺機器所含數據不同,然後使用不同機器找到不同特徵的最優分裂點,劃分結果需要通過通信告知每臺機器,增加了額外的複雜度。

LightGBM 則不進行數據垂直劃分,而是在每臺機器上保存全部訓練數據,在得到最佳劃分方案後可在本地執行劃分而減少了不必要的通信。具體過程如下圖所示。
在這裏插入圖片描述(2)數據並行

傳統的數據並行策略主要爲水平劃分數據,讓不同的機器先在本地構造直方圖,然後進行全局的合 並,最後在合併的直方圖上面尋找最優分割點。這種數據劃分有一個很大的缺點:通訊開銷過大。 如果使用點對點通信,一臺機器的通訊開銷大約爲 O(#machine* # feature*#bin) 如果使用集成的通信,則通訊開銷爲 O(2# feature # bin )O(2 * \# \text { feature } * \# \text { bin })

LightGBM在數據並行中使用分散規約 (Reduce scatter) 把直方圖合併的任務分攤到不同的機器,降低通信和計算,並利用直方圖做差,進一步減少了一半的通信量。具體過程如下圖所示。
在這裏插入圖片描述(3)投票並行

基於投票的數據並行則進一步優化數據並行中的通信代價,使通信代價變成常數級別。在數據量很大的時候,使用投票並行的方式只合並部分特徵的直方圖從而達到降低通信量的目的,可以得到非常好的加速效果。具體過程如下圖所示。

大致步驟爲兩步:

  • 本地找出 Top K 特徵,並基於投票篩選出可能是最優分割點的特徵;
  • 合併時只合並每個機器選出來的特徵
    在這裏插入圖片描述
    3.3 Cache命中率優化

XGBoost對cache優化不友好,如下圖所示。在預排序後,特徵對梯度的訪問是一種隨機訪問,並且不同的特徵訪問的順序不一樣,無法對cache進行優化。同時,在每一層長樹的時候,需要隨機訪問一個行索引到葉子索引的數組,並且不同特徵訪問的順序也不一樣,也會造成較大的cache miss。爲了解決緩存命中率低的問題,XGBoost 提出了緩存訪問算法進行改進。

在這裏插入圖片描述而 LightGBM 所使用直方圖算法對 Cache 天生友好:

  • 首先,所有的特徵都採用相同的方式獲得梯度(區別於XGBoost的不同特徵通過不同的索引獲得梯度),只需要對梯度進行排序並可實現連續訪問,大大提高了緩存命中率;
  • 其次,因爲不需要存儲行索引到葉子索引的數組,降低了存儲消耗,而且也不存在 Cache Miss的問題。
    在這裏插入圖片描述

4. LightGBM的優缺點

4.1 優點
這部分主要總結下 LightGBM 相對於 XGBoost 的優點,從內存和速度兩方面進行介紹。

(1)速度更快

  • LightGBM 採用了直方圖算法將遍歷樣本轉變爲遍歷直方圖,極大的降低了時間複雜度;
  • LightGBM 在訓練過程中採用單邊梯度算法過濾掉梯度小的樣本,減少了大量的計算;
  • LightGBM 採用了基於 Leaf-wise 算法的增長策略構建樹,減少了很多不必要的計算量;
  • LightGBM 採用優化後的特徵並行、數據並行方法加速計算,當數據量非常大的時候還可以採用投票並行的策略;
  • LightGBM 對緩存也進行了優化,增加了緩存命中率;

(2)內存更小

  • XGBoost使用預排序後需要記錄特徵值及其對應樣本的統計值的索引,而 LightGBM 使用了直方圖算法將特徵值轉變爲 bin值,且不需要記錄特徵到樣本的索引,將空間複雜度從O(2*#data)降低爲O(#bin),極大的減少了內存消耗;
  • LightGBM 採用了直方圖算法將存儲特徵值轉變爲存儲 bin 值,降低了內存消耗;
  • LightGBM 在訓練過程中採用互斥特徵捆綁算法減少了特徵數量,降低了內存消耗。

4.2 缺點

  • 可能會長出比較深的決策樹,產生過擬合。因此LightGBM在Leaf-wise之上增加了一個最大深度限制,在保證高效率的同時防止過擬合;
  • Boosting族是迭代算法,每一次迭代都根據上一次迭代的預測結果對樣本進行權重調整,所以隨着迭代不斷進行,誤差會越來越小,模型的偏差(bias)會不斷降低。由於LightGBM是基於偏差的算法,所以會對噪點較爲敏感;
  • 在尋找最優解時,依據的是最優切分變量,沒有將最優解是全部特徵的綜合這一理念考慮進去;

實例操作

訓練配置
6307410個樣本做訓練集

num_trees = 2 // 樹的棵樹

categorical_feature=1,2,3,5,6,8,299 //類別特徵編號

num_leaves = 6 // 每棵樹的葉子數

max_depth = 3 // 樹的深度

learning_rate = 0.1 // 學習率

bagging_fraction = 0.8 // 樣本採樣比例

訓練出的LightGBM模型文件及其含義解析:

ree // 模型中子模型的名字,gbdt的子模型是tree

num_class=1 // 類別數量,二分類問題變成了概率問題

label_index=0 // lable所在列

max_feature_idx=1365 //最大的特徵index, 0~1365,LightGBM認爲特徵從0開始編碼

objective=binary //學習目標

sigmoid=1 //結果輸出時sigmoid的參數 output[0] = 1.0f / (1.0f + std::exp(-sigmoid_ * input[0]));,越大sigmoid越陡峭

feature_names=Column_0 Column_1 Column_2 ... Column_1363 Column_1364 Column_1365 // 特徵名字,就是”Column_” + 數據中特徵index

feature_infos=none 0:1 [0:10742] 1487112:0 [0:3999191] ...

// 沒有“[]”的是category 特徵的bin中的特徵取值

// 有“[]”是數值特徵的bin中的最小、最大值

// none表示此特徵沒有使用

第1棵樹

Tree=0 // 樹的編號,從0開始

num_leaves=6 // 樹中葉子的數量

split_feature=150 197 381 63 197 //6個葉子,分裂5次,有5個內部節點和分裂特徵,這個特徵編號是在原樣本中的特徵編號

split_gain=579239.62681873201 101591.49813184602 78186.521895228478 75276.734034747526 57589.418844881991 // 每次分裂的增益

threshold=0.028499999999999998 0.016500000000000001 554.04549999999995 3.1340000000000003 0.043499999999999997 // 分裂特徵的特徵值分界點

decision_type=0 0 0 0 0 //5個內部節點的判定類型值,判定類型值是int8_t,以bit形式,第一低位存儲是否是category特徵,第二低位存儲是否使用左子節點作爲默認去向,第三、第四低位存儲是None(0)、Zero(1)、NaN(2)中的哪種

left_child=1 3 -2 -1 -4

right_child=2 -3 4 -5 -6

leaf_parent=3 2 1 4 3 4

樹的結構

// 有5個內部節點,默認編號是:0、1、2、3、4

// 有6個葉子節點,編號分別是:-1、-2、-3、-4、-5、-6

// left_child表示這5個內部節點的左子節點,正值表示內部節點的節點編號,負值表示葉子節點的節點編號

// right_child表示這5個內部節點的左子節點

// leaf_parent 表示-1、-2、-3、-4、-5、-6這6個葉子節點的父節點編號

// 於是這個樹的結構就是這樣

在這裏插入圖片描述

leaf_value=0.013151525839652695 -0.0031140914212769983 -0.017382907119786403 0.038475160439658297 -0.10110187665371873 0.091299535945193661 //各個葉子節點的預測值

leaf_count=171831 511580 1078379 893167 1432378 958588 // 各個葉子節點的樣本數量,這裏總共有5045923個

internal_value=0 -0.55733646225250466 0.54728595683818304 -0.85735596237957235 0.67893796844992116 // 各個中間節點的預測值

internal_count=5045923 2682588 2363335 1604209 1851755 // 各個中間節點的樣本數,1604209[中間節點3的樣本數] = 171831 [葉子節點-1的樣本數] + 1432378[葉子節點-5的樣本數]

//可以看出這棵樹的訓練只用了5045923個樣本,而實際訓練集樣本有6307410個,這是因爲在模型配置文件中設置了採樣比例爲0.8

shrinkage=0.1 // 設定的學習率

第二棵樹,含義參考第一棵樹

Tree=1

num_leaves=6

split_feature=145 161 198 11 381

split_gain=474253.30131810816 93455.112333323515 62969.704987476958 55878.668231101008 32961.303899061735

threshold=0.026500000000000003 0.018500000000000003 0.043499999999999997 8.4154999999999998 663.125

decision_type=0 0 0 0 0

left_child=1 3 4 -1 -2

right_child=2 -3 -4 -5 -6

leaf_parent=3 4 1 2 3 4

leaf_value=0.010494795842311992 -0.024170274578830017 -0.010405728632592726 0.075110240965977765 -0.08865782202254327 0.038228215007066219

leaf_count=167445 301508 975432 1063548 1556038 981952

internal_value=0 -0.50125289035240339 0.49837677764421778 -0.76617891719378095 0.25393645325883307

internal_count=5045923 2698915 2347008 1723483 1283460

shrinkage=0.1

特徵重要性

feature importances:

Column_197=2 // 特徵197,重要性排最高,重要性值爲2,表示在所有樹中有兩次作爲中間節點的分裂特徵

Column_381=2 // 所有樹中有兩次作爲中間節點的分裂特徵

Column_11=1 // 所有樹中有一次作爲中間節點的分裂特徵

Column_63=1

Column_145=1

Column_150=1

Column_161=1

Column_198=1

重要性值是統計特徵在所有樹中作爲中間節點(分裂節點)的分裂特徵且分裂增益爲正的次數,可以理解成是對分裂作用越大的特徵越重要

參考自:
Microstrong
魚達爾

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章