LightGBM原理解析


摘要

本文對lgb的基本原理進行簡要概括。

基於直方圖的節點分裂

lgbm使用基於直方圖的分裂點選擇算法,分裂准則為最小化方差,也即最大化方差增益variance gain:

 對比xgb的loss reduction:

 可以發現,兩者是一致的,不同點在於,xgb的loss reduction包含了正則化因子λ,而lgbm未作正則化,因為lgbm的損失函數為均方誤差,因此其二階梯度hi=1,體現在loss reduction上,lgbm的分母為n(n=Σhi)。

因此,lgbm的均方誤差實際是一種特例,其最優化方法仍然與xgb一致。仍然可以認為其對直方圖統計了一階和二階梯度,其中,一階梯度為,二階梯度為

方差增益的推導

我們考慮當前樹中某個具體的結點,對於某個當前結點來說,定義當前結點上樣本集合為 [公式] ,我們期望遍歷所有候選特征以及候選分裂點,來找到一個特征j和對應的分裂點d,使得分裂當前結點以后左右子結點上的樣本總體方差更小,轉化為公式,就是使得 [公式] 最大,這個公式第一樣式是指沒分裂之前結點的均方誤差,后面兩項分別是分裂以后左右孩子結點的樣本均方誤差之和。

由於 [公式] 對於當前結點分裂增益的計算是個常量,上式即等同於最小化 [公式]

我們知道 [公式] ,那么上面式子進一步化簡為 [公式] ,由於第一項又是一個常數項,因此我們的問題最終轉化為最大化 [公式] ,進一步地再化簡: [公式] ,這里結果就是Lightgbm中的variance gain,即最大化左右葉子結點樣本負梯度和的平方。

第二種理解就是參考XGBoost的信息增益計算方式,這里我直接引用XGBoost里面的增益計算公式:

[公式] ,同樣地,第三項是個常數項,實際上就是在最大化前兩項。對於GBDT使用平方誤差作為損失函數來說,樣本的二階導數 [公式] 等於1(損失函數 [公式] ),且忽略XGBoost中正則項系數 [公式] ,就會得到和上述式子同樣的結果。

基於梯度的單邊采樣(gradient-based one-side sampling, GOSS)

lgb采用基於梯度的單邊采樣(GOSS)減少樣本數量。GOSS首先按照樣本的梯度大小對數據進行降序排序,選取前a%的大梯度樣本,然后從剩余的小梯度樣本中采樣b%,為保持樣本分布不變,對小梯度樣本加權(1-a)/b。

 互斥特征綁定(exclusive feature bundling, EFB)

對於高維稀疏數據,很多特征之間都是互斥的,也就是對於同一個樣本,不同特征通常不會同時取非零值。這樣的性質給可以幾乎無損失地減少特征數量提供了可能性。

EFB包括兩個步驟:特征分簇和特征合並。

特征分簇:將每個特征視為一個節點,在非互斥節點之間添加邊,將特征bundle問題轉換為圖着色問題,算法如下:

1、將特征作為節點,特征之間的沖突(兩個特征同時非0的樣本數量)作為邊的權重,構建圖;

2、按照節點度對特征進行降序排序;

3、按順序遍歷特征,將其加入已有bundle(若加入后bundle的沖突數小於閾值)或為其創建新的bundle;

算法3的時間復雜度是O(feature2),訓練之前只處理一次,其時間復雜度在特征不是特別多的情況下是可以接受的,但難以應對百萬維的特征。為了繼續提高效率,lgb提出了一個更加高效的無圖的排序策略:將特征按照非零值個數排序,這和使用圖節點的度排序相似,因為更多的非零值通常會導致沖突,新算法在算法3基礎上改變了排序策略。

特征合並:通過增加偏移量的方式,將多個互斥特征合並到同一個特征,例如特征A的取值范圍為(0,10),特征B的取值范圍為(0,20),那么可以為特征B增加偏移量10,合並后B的取值范圍為(10,30),新特征的取值范圍為(0,30)。

具體如下:

 

離散特征處理

 為解決one-hot編碼處理類別特征的不足,lgb采用many vs many 的切分方式,實現類別特征的最優切分。

具體方式為:

1、離散特征建立直方圖的過程:

統計該特征下每一種離散值出現的次數,並從高到低排序,並過濾掉出現次數較少的特征值, 然后為每一個特征值,建立一個bin容器, 對於在bin容器內出現次數較少的特征值直接過濾掉,不建立bin容器。

2、計算分裂閾值的過程:

2.1、先看該特征下划分出的bin容器的個數,如果bin容器的數量小於4,直接使用one vs other方式, 逐個掃描每一個bin容器,找出最佳分裂點;

2.2、對於bin容器較多的情況, 先進行過濾,只讓子集合較大的bin容器參加划分閾值計算, 對每一個符合條件的bin容器進行公式計算(公式如下: 該bin容器下所有樣本的一階梯度之和 / 該bin容器下所有樣本的二階梯度之和 + 正則項(參數cat_smooth),這里為什么不是label的均值呢?其實上例中只是為了便於理解,只針對了學習一棵樹且是回歸問題的情況, 這時候一階導數是Y, 二階導數是1),得到一個值,根據該值對bin容器從小到大進行排序,然后分從左到右、從右到左進行搜索,得到最優分裂閾值。但是有一點,沒有搜索所有的bin容器,而是設定了一個搜索bin容器數量的上限值,程序中設定是32,即參數max_num_cat。

LightGBM中對離散特征實行的是many vs many 策略,這32個bin中最優划分的閾值的左邊或者右邊所有的bin容器就是一個many集合,而其他的bin容器就是另一個many集合。

2.3、對於連續特征,划分閾值只有一個,對於離散值可能會有多個划分閾值,每一個划分閾值對應着一個bin容器編號,當使用離散特征進行分裂時,只要數據樣本對應的bin容器編號在這些閾值對應的bin集合之中,這條數據就加入分裂后的左子樹,否則加入分裂后的右子樹。

並行計算

lgbm的並行方式有三種,分別是特征並行、數據並行、投票並行(voting parallel)。

1、特征並行

傳統的gbdt並行方法為:不同的worker存儲不同的特征集,再找到全局的最佳分裂點后,具有該划分點的worker進行節點分裂,然后廣播切分后的左右子樹數據結果,其他worker收到結果后也進行廣播;

而在lgbm中,每個worker保留了所有的特征集,在找到全局的最佳分割點后每個worker可自行進行划分,不再依賴廣播,減少了網絡通信,但存儲代價變高;

2、數據並行

數據並行的目標是並行化整個決策過程。每個worker擁有部分數據,獨立地構建局部直方圖,合並后得到全局直方圖,在全局直方圖中尋找最優切分點。

3、投票並行

lgbm采用一種稱為pv-tree的算法進行投票並行,本質上也是一種數據並行。pv-tree和普通的決策樹差不多,只是在尋找最優切分點上有所不同。

每個worker擁有部分數據,獨自構建數據並找到最優的k個划分特征,中心worker聚合得到最優的2k個划分特征,在想每個worker收集2k個直方圖,進行合並得到最優化分,最后廣播到每一個worker進行本地划分。

 圖着色代碼

def matrix_graph(g, v_num):
    """生成二維矩陣"""
    return [[1 if col in g[row] else 0 for col in range(v_num)] for row in range(v_num)]
 
def nodes_inorder_des(m, v_num):
    """將結點按度數降序排好"""
    deg = {}
    for v in range(v_num):
        deg[v] = m[v].count(1)
    return sorted(deg, key=deg.get, reverse=True)
 
def graph_color(m, n, v_num):
    """將節點按照染色顏色划分"""
    painted = set()
    res = list()
    for v in n:
        if v not in painted:  # 每次找未着色的點
            group = list([v])  # 保存相同顏色的結點
            painted.add(v)
            for v_other in range(v_num):
                if m[v][v_other] == 0 and v_other not in painted:  # v與v_other不鄰接 and 其鄰接點沒有被染色
                    if all(m[v_group][v_other] == 0 for v_group in  # v_other與同組節點不鄰接
                           group[1:]):  # 拷貝group,保持不變性,並且從新加入節點開始比較
                        painted.add(v_other)
                        group.append(v_other)
            res.append(group)
 
            if len(painted) == total_v_num:
                break
    return res
 
if __name__ == '__main__':
    total_v_num = len(graph)
 
    matrix = matrix_graph(graph, total_v_num)
 
    nodes = nodes_inorder_des(matrix, total_v_num)
 
    res = graph_color(matrix, nodes, total_v_num)
 
    print('染色情況:', res)
    print('染色數:', len(res))

參考鏈接

https://www.microsoft.com/en-us/research/wp-content/uploads/2017/11/lightgbm.pdf

https://papers.nips.cc/paper/2017/hash/6449f44a102fde848669bdd9eb6b76fa-Abstract.html

https://blog.csdn.net/anshuai_aw1/article/details/83275299

https://www.zhihu.com/question/386159856/answer/1145984790

https://blog.csdn.net/qq_43658387/article/details/103129196

https://blog.csdn.net/csg3140100993/article/details/107813598


免責聲明!

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



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