lightGBM從參數(shù)調(diào)優(yōu)到背景理論

時隔兩年,再次復盤之前寫的lightgbm的這篇文章,發(fā)現(xiàn)當時主要是為了實踐使用,并沒有寫很多的理論背景,這次在文章的前面部分,添加理論部分,后面依舊是簡單的使用和參數(shù)的含義。希望迎接更好的2020年~
——————————————————————————
在第二部分原理中,結合了原始作者的論文,還有網(wǎng)絡上比較好的幾篇的講解
lightGBM的論文

  1. LightGBM源碼閱讀+理論分析(處理特征類別,缺省值的實現(xiàn)細節(jié))
  2. LightGBM,面試會問到的都在這了(附代碼)
  3. Lightgbm如何處理類別特征

一、簡介——機器學習競賽的好工具

lightGBM是一個很好用的機器學習競賽算法實現(xiàn),他的本質(zhì)是GDBT算法的一種優(yōu)化實現(xiàn),重點在于light這個單詞上。作者在論文中提到,lightgbm可以比xgboost快6倍,同時使用更加小的內(nèi)存,并且保持算法的高準確率。那么是如何實現(xiàn)light的呢?
主要是通過下面這些方式實現(xiàn)的:

  1. GOSS(Gradient-based One-Side Sampling):減少樣本數(shù)
  2. EFB (Exclusive Feature Bundling ):減少特征數(shù)
  3. 直方圖差加速
  4. 自動處理缺省值,包括是否將0視為缺省值。
  5. 處理類別特征
  6. Leaf-wise生長策略

其中第一第二點,是在作者論文中就提到的,極大優(yōu)化的時間和空間開銷的方法,第三點是在分裂特征的時候使用的,第5點是最讓人感動的,可以在拿到數(shù)據(jù)之后,快速的進行訓練,不需要對離散特征進行one-hot encoding.第6點可以在相同的分裂次數(shù)時,獲得更高的精度和更低的誤差。下面將分別介紹這些優(yōu)化方法。

二、lightgbm的優(yōu)勢們

0. 直方圖算法

這個不是lightgbm首先提出的,但卻是是light的一個基石。在訓練樹的時候,需要找到最佳劃分節(jié)點,為此其中需要遍歷特征下的每一個value,這里通常有兩種做法:pre-sorted algorithm(預排序算法)和histogram-based algorithm(直方圖算法)。

許多提升工具對于決策樹的學習使用基于 pre-sorted 的算法。這是一個簡單的解決方案,但是不易于優(yōu)化。
LightGBM 利用基于 histogram 的算法 [3, 4, 5],通過將連續(xù)特征(屬性)值分段為 discrete bins 來加快訓練的速度并減少內(nèi)存的使用。

1. GOSS

我們知道傳統(tǒng)的Adaboost其實數(shù)據(jù)集都是有一個權值的,用來衡量其重要程度,沒有被好好訓練的樣本其權值就大,以便下一個基學習器對其多加訓練,于是就可以依據(jù)該權值對其采用,這樣就做到采用利用部分數(shù)據(jù)集。

但是在GBDT中,數(shù)據(jù)沒有權重這個概念,那我們應該怎么做呢?雖然我們沒有權重,但是我們每次使用決策樹訓練的是之前所有模型的殘差(平方損失的情況下),或者說是負梯度方向。那么我們可以將每個樣本的一階導數(shù)看做權重,一階導數(shù)大的說明離最優(yōu)解還遠,這部分樣本帶來的增益大,或者說這部分樣本還沒有被好好訓練,下一步我們應該重點訓練他們。

我們計算所有樣本的梯度,然后排序,取前top a%作為大梯度樣本,然后對剩下的(1-a)x n的樣本取b%作為小梯度樣本訓練。但是這樣會導致數(shù)據(jù)的分布發(fā)生變化,所以還有給小樣本一個補償,那就是乘以一個常數(shù)即(1-a)/b,可以看到當a=0時就變成了隨機采用啦,這樣抽的結果還是能保持準確率的。


GOSS算法

這樣訓練的樣本就只有原始樣本的an + (1-a) x n x b了。在論文的實驗中,a = 0.1, b=0.1(乃至更小)都沒有影響最終的結果,還是和xgboost的結果那么精確。

2. EFB優(yōu)化

通常,在GBDT和其最優(yōu)的實現(xiàn)xgboost中,我們需要將類別型的特征預處理為onehot編碼,否則就會認為是潛在有大小關系的。但是轉為one hot之后,類別數(shù)大,gbdt這種樹模型對高維稀疏特征的處理效果差。簡直就是兩難的問題啊。

我們先來理解一下為什么會效果差,GBDT是二叉樹,使用one-hot編碼的話,意味著在每一個決策節(jié)點上只能使用one vs rest(例如是不是狗,是不是貓等)的切分方式。當類別值很多時,每個類別上的數(shù)據(jù)可能會比較少,這時候切分會產(chǎn)生不平衡,這意味著切分增益也會很?。ū容^直觀的理解是,不平衡的切分和不切分沒有區(qū)別)。其次,可能因為訓練數(shù)據(jù)的問題(噪聲或者數(shù)據(jù)量太少),產(chǎn)生錯的分支,導致過擬合。最后,一個直觀的感覺是即使樹很深了,但是使用的特征可能也不多。

lightgbm反其道行之,在訓練的時候有意的將看起來像是one hot的好幾類稀疏高維的特征結合在意思。咋么做到的呢?直觀的就會問兩個問題:
1)到底那些特征需要合并到一起
2)怎么合并到一起

2.1

論文定義了一個概念叫Exclusive Feature,意思是很少同時取到非零值的特征列。回想一下one-hot編碼的特征,是不是當某一列特征為1時,其余的特征都是0,這些one-hot特征就是Exclusive Feature,還有其他一些潛在有關系的特征,也會有這種現(xiàn)象。EFB就是希望將這些特征合并起來。同時注意到有些特征并不是100%的互相排斥,但是呢?其也很少同時取非0值,如果我們允許一部分沖突,那么這部分特征就可以進一步進行合并,使得bundle進一步減少。

在實際算法中,作者首先計算每個特征和其他特征的沖突數(shù)量(同時取到非0的樣本數(shù)),然后將特征按照總沖突數(shù)排序,依次遍歷這些特征,檢查該特征和其他特征的沖突數(shù)是否少于閾值T,如果少于閾值,那么就合并成一個bundle,如果無法和其他特征結合,就新建一個bundle.

作者為了優(yōu)化速度,其不再建立圖了,而是統(tǒng)計非零的個數(shù),非零個數(shù)越多就說明沖突越大,互相排斥越小,越不能捆綁到一起。

2.2

EFB直覺上可以理解為one-hot的反向操作。那么對于已經(jīng)bundle到一起的特征,怎么在取值上區(qū)分出所有特征?方法也很簡單(一般人都能想到)假如A特征的范圍是[0,10),B特征的范圍是[0,20),那么就給B特征加一個偏值,比如10,那么B的范圍就變?yōu)閇10,30),所以捆綁為一個特征后范圍就是[0,30]

3. 直方圖加速

在GBDT中,在找到分裂點之后,需要統(tǒng)計左右樹上的樣本分布情況。在lightgbm中,因為我們使用了直方圖對樣本的特征進行預處理,那么只要知道父節(jié)點上的直方圖,然后得到左子樹上的直方圖分布,就可以做差得到右子圖上的直方圖,不需要再重復計算。

說到這個直方圖加速的概念,我們在腦中來模擬一下lightgbm劃分最優(yōu)分裂點的過程。

  1. 先看該特征下劃分出的bin容器的個數(shù),如果bin容器的數(shù)量小于4,直接使用one vs other方式, 逐個掃描每一個bin容器,找出最佳分裂點
  2. 對于bin容器較多的情況, 先進行過濾,只讓子集合較大的bin容器參加劃分閾值計算, 對每一個符合條件的bin容器進行公式計算(公式如下: 該bin容器下所有樣本的一階梯度之和/ 該bin容器下所有樣本的二階梯度之和 + 正則項(參數(shù)cat_smooth),可以聯(lián)想到在xgboost中,尋找最佳分裂點的時候,也是使用了類似的一個公式,只是分母是一階導數(shù)的平方。得到一個值,根據(jù)該值對bin容器從小到大進行排序,然后分從左到右、從右到左進行搜索,得到最優(yōu)分裂閾值。但是有一點,沒有搜索所有的bin容器,而是設定了一個搜索bin容器數(shù)量的上限值,程序中設定是32,即參數(shù)max_num_cat。
    LightGBM中對離散特征實行的是many vs many 策略,這32個bin中最優(yōu)劃分的閾值的左邊或者右邊所有的bin容器就是一個many集合,而其他的bin容器就是另一個many集合。
  3. 對于連續(xù)特征,劃分閾值只有一個,對于離散值可能會有多個劃分閾值,每一個劃分閾值對應著一個bin容器編號,當使用離散特征進行分裂時,只要數(shù)據(jù)樣本對應的bin容器編號在這些閾值對應的bin集合之中,這條數(shù)據(jù)就加入分裂后的左子樹,否則加入分裂后的右子樹。

這里有兩個問題需要注意:

  1. 為什么按照一階導數(shù)/二階導數(shù)的值作為bin排序的順序?其實可以看成前面說的熵(更簡單的可以理解對當前特征分的好壞),這樣做的目的就是熵大的在一邊,熵小的在一邊,假設排序后是a,b,c,d這樣進行many vs many的組合時盡可能的保持了分的好的放一邊,否則如果混亂的分的話,已經(jīng)分的好的特征就又會和沒有分的好的特征混在一起,那么其實后面的分就效率不高。
  2. 為什么需要左邊遍歷一次,右邊遍歷一次?其意義就在于缺省值到底是在哪里?其實這類問題叫做Sparsity-aware Split Finding稀疏感知算法,當從左到右,對于缺省值就規(guī)劃到了右面,當方向相反時,缺省值都規(guī)劃到了左面。還有一個用處,就是sorted_idx中的bin數(shù)和最大bin數(shù)可能不一樣,前面有講,bin中數(shù)據(jù)量大于一個閾值的才放進sorted_idx,后面是遍歷sorted_idx,所以左右遍歷的時候,分出某一邊樹的時候,剩下的bin分到另一邊,這個剩下的bin是包括空值和沒有在sorted_idx中的bin的,所以即使沒有空值,而有bin是不在sorted_idx中的時候,左右遍歷得到的結果也是不一樣。

4. 自動處理缺省值

在2中,我們提到在最優(yōu)分割點的選擇的時候,需要左右遍歷一次,遍歷的意義就是為了將缺省值放置在合適的位置。從左到右,對于缺省值就規(guī)劃到了右面,當方向相反時,缺省值都規(guī)劃到了左面。當從左到右時,我們記錄不論是當前一階導數(shù)和也好二階導數(shù)也罷,都是針對有值的(缺省值就沒有一階導數(shù)和二階導數(shù)),那么我們用差加速得到右子樹,既然左子樹沒有包括缺省值,那么總的減去左子樹自然就將缺省值歸到右子樹了

5. 處理類別特征

lightgbm相對于其他GBDT實現(xiàn)的優(yōu)點之一,就是不需要對類別特征做one-hot預處理。
為了解決one-hot編碼處理類別特征的不足。LGBM采用了Many vs many的切分方式,實現(xiàn)了類別特征的最優(yōu)切分。用Lightgbm可以直接輸入類別特征。

在上面的3中,在將lightgbm劃分最優(yōu)分裂點的過程的時候,已經(jīng)提到了針對bins較少的情況是怎么做的,對于類別較多的bins是按照數(shù)值來進行操作的。

6.Leaf-wise生長策略

大部分決策樹的學習算法通過 level(depth)-wise 策略生長樹,而lightgbm使用Leaf-wise (Best-first) 的決策樹生長策略。它將選取具有最大 delta loss 的葉節(jié)點來生長。 當生長相同的 #leaf,leaf-wise 算法可以比 level-wise 算法減少更多的損失。
當 #data 較小的時候,leaf-wise 可能會造成過擬合。 所以,LightGBM 可以利用額外的參數(shù) max_depth 來限制樹的深度并避免過擬合(樹的生長仍然通過 leaf-wise 策略)。

我們通常將類別特征轉化為 one-hot coding。 然而,對于學習樹來說這不是個好的解決方案。 原因是,對于一個基數(shù)較大的類別特征,學習樹會生長的非常不平衡,并且需要非常深的深度才能來達到較好的準確率。
事實上,最好的解決方案是將類別特征劃分為兩個子集,總共有 2^(k-1) - 1 種可能的劃分 但是對于回歸樹 [7] 有個有效的解決方案。為了尋找最優(yōu)的劃分需要大約k * log(k) .

三、 lightgbm的使用

lightgbm的使用起來也很簡單。大致步驟可以分為下面幾個

  • 首先用lgb包的DataSet類包裝一下需要測試的數(shù)據(jù);
  • 將lightgbm的參數(shù)構成一個dict字典格式的變量
  • 將參數(shù)字典,訓練樣本,測試樣本,評價指標一股腦的塞進lgb.train()方法的參數(shù)中去
  • 上一步的方法會自覺地得到最佳參數(shù)和最佳的模型,保存模型
  • 使用模型進行測試集的預測

其中比較重要的是第二步也就是設置參數(shù)。有很多很重要的參數(shù),在下面的第二部分(參數(shù)字典)中,我大概介紹一下使用的比較多的比較有意義的參數(shù)。

1. 安裝

在已經(jīng)安裝了anaconda的windows 7環(huán)境下,在cmd控制面板中輸入pip install lightgbm即實現(xiàn)了安裝。在此之前需要已經(jīng)下載了依賴包如setuptools, wheel, numpy 和 scipy。pip install setuptools wheel numpy scipy scikit-learn -U. 過程中沒有碰到問題。

2. 訓練數(shù)據(jù)包裝

lightgbm的一些特點:

  • LightGBM 支持 CSV, TSVLibSVM 格式的輸入數(shù)據(jù)文件。
  • LightGBM 可以直接使用 categorical feature(類別特征)(不需要單獨編碼)。 Expo data 實驗顯示,與 one-hot 編碼相比,其速度提高了 8 倍??梢栽诎b數(shù)據(jù)的時候指定哪些屬性是類別特征。
  • LightGBM 也支持加權訓練,可以在包裝數(shù)據(jù)的時候指定每條記錄的權重

LightGBM 中的 Dataset 對象由于只需要保存 discrete bins(離散的數(shù)據(jù)塊), 因此它具有很好的內(nèi)存效率. 然而, Numpy/Array/Pandas 對象的內(nèi)存開銷較大. 如果你關心你的內(nèi)存消耗. 您可以根據(jù)以下方式來節(jié)省內(nèi)存:

  • 在構造 Dataset 時設置 free_raw_data=True (默認為 True)
  • 在 Dataset 被構造完之后手動設置 raw_data=None
  • 調(diào)用 gc

LightGBM Python 模塊能夠使用以下幾種方式來加載數(shù)據(jù):

  • libsvm/tsv/csv txt format file(libsvm/tsv/csv 文本文件格式)
  • Numpy 2D array, pandas object(Numpy 2維數(shù)組, pandas 對象)
  • LightGBM binary file(LightGBM 二進制文件)
    加載后的數(shù)據(jù)存在 Dataset 對象中.

要加載 numpy 數(shù)組到 Dataset 中:

data = np.random.rand(500, 10)  # 500 個樣本, 每一個包含 10 個特征
label = np.random.randint(2, size=500)  # 二元目標變量,  0 和 1
train_data = lgb.Dataset(data, label=label)

在現(xiàn)實情況下,我們可能之前使用的是pandas的dataFrame格式在訓練數(shù)據(jù),那也沒有關系,可以先使用sklearn包對訓練集和測試集進行劃分,然后再使用DataSet類包裝。DataSet第一個參數(shù)是訓練特征,第二個參數(shù)是標簽

from sklearn.model_selection import train_test_split
X_train,X_val,y_train,y_val = train_test_split(X,Y,test_size=0.2)
xgtrain = lgb.Dataset(X_train, y_train)
xgvalid = lgb.Dataset(X_val, y_val)

在 LightGBM 中, 驗證數(shù)據(jù)應該與訓練數(shù)據(jù)一致(格式一致).
保存 Dataset 到 LightGBM 二進制文件將會使得加載更快速:

train_data = lgb.Dataset('train.svm.txt')
train_data.save_binary('train.bin')

指定 feature names(特征名稱)和 categorical features(分類特征),注意在你構造 Dataset 之前, 你應該將分類特征轉換為 int 類型的值。還可以指定每條數(shù)據(jù)的權重(比如在樣本規(guī)模不均衡的時候希望少樣本的標簽對應的記錄可以擁有較大的權重)

w = np.random.rand(500, )
train_data = lgb.Dataset(data, label=label, feature_name=['c1', 'c2', 'c3'], 
                   categorical_feature=['c3'],weight=w)

或者

train_data = lgb.Dataset(data, label=label)
w = np.random.rand(500, )
train_data.set_weight(w)

3. 設置參數(shù)

  1. 參數(shù)字典
    每個參數(shù)的含義后面介紹
lgb_params = {
    'boosting_type': 'gbdt',
    'objective': 'binary', #xentlambda
    'metric': 'auc',
    'silent':0,
    'learning_rate': 0.05,
    'is_unbalance': 'true',  #當訓練數(shù)據(jù)是不平衡的,正負樣本相差懸殊的時候,可以將這個屬性設為true,此時會自動給少的樣本賦予更高的權重
    'num_leaves': 64,  # 一般設為少于2^(max_depth)
    'max_depth': -1,  #最大的樹深,設為-1時表示不限制樹的深度
    'min_child_samples': 15,  # 每個葉子結點最少包含的樣本數(shù)量,用于正則化,避免過擬合
    'max_bin': 200,  # 設置連續(xù)特征或大量類型的離散特征的bins的數(shù)量
    'subsample': 0.8,  # Subsample ratio of the training instance.
    'subsample_freq': 1,  # frequence of subsample, <=0 means no enable
    'colsample_bytree': 0.5,  # Subsample ratio of columns when constructing each tree.
    'min_child_weight': 0,  # Minimum sum of instance weight(hessian) needed in a child(leaf)
    #'scale_pos_weight':100,
    'subsample_for_bin': 200000,  # Number of samples for constructing bin
    'min_split_gain': 0,  # lambda_l1, lambda_l2 and min_gain_to_split to regularization
    'reg_alpha': 2.99,  # L1 regularization term on weights
    'reg_lambda': 1.9,  # L2 regularization term on weights
    'nthread': 10,
    'verbose': 0,
}
  1. 評價函數(shù)
    評價函數(shù)可以是自定義的,也可以是sklearn中使用的。這里是一個自定義的評價函數(shù)寫法:
def feval_spec(preds, train_data):
    from sklearn.metrics import roc_curve
    fpr, tpr, threshold = roc_curve(train_data.get_label(), preds)
    tpr0001 = tpr[fpr <= 0.0005].max()
    tpr001 = tpr[fpr <= 0.001].max()
    tpr005 = tpr[fpr <= 0.005].max()
    #tpr01 = tpr[fpr.values <= 0.01].max()
    tprcal = 0.4 * tpr0001 + 0.3 * tpr001 + 0.3 * tpr005
    return 'spec_cal',tprcal,True

如果是自定義的評價函數(shù),那么需要函數(shù)的輸入是預測值、輸入數(shù)據(jù)。返回參數(shù)有三個,第一個是評價指標名稱、第二個是評價值、第三個是True表示成功。

4. 訓練

4.1基礎版

訓練一個模型時, 需要一個 parameter list(參數(shù)列表、字典)和 data set(數(shù)據(jù)集)這里使用上面定義的param參數(shù)字典和上面提到的訓練數(shù)據(jù):

num_round = 10
bst = lgb.train(param, train_data, num_round, valid_sets=[test_data])

4.2 交叉驗證

時間充足的時候,應該使用交叉驗證來選擇最好的訓練模型,使用 5-折 方式的交叉驗證來進行訓練(4 個訓練集, 1 個測試集):

num_round = 10
lgb.cv(param, train_data, num_round, nfold=5)

4.3 提前停止

如果您有一個驗證集, 你可以使用提前停止找到最佳數(shù)量的 boosting rounds(梯度次數(shù)). 提前停止需要在 valid_sets 中至少有一個集合. 如果有多個,它們都會被使用:

bst = lgb.train(param, train_data, num_round, valid_sets=valid_sets, 
      early_stopping_rounds=10)
bst.save_model('model.txt', num_iteration=bst.best_iteration)

該模型將開始訓練, 直到驗證得分停止提高為止. 驗證錯誤需要至少每個 early_stopping_rounds 減少以繼續(xù)訓練.

如果提前停止, 模型將有 1 個額外的字段: bst.best_iteration. 請注意 train() 將從最后一次迭代中返回一個模型, 而不是最好的一個.. 請注意, 如果您指定多個評估指標, 則它們都會用于提前停止.

提前停止可以節(jié)約訓練的時間。

5. 保存模型

在訓練完成后, 可以使用如下方式來存儲模型:
bst.save_model('model.txt')
已經(jīng)訓練或加載的模型都可以對數(shù)據(jù)集進行預測:

6. 預測

7 個樣本, 每一個包含 10 個特征

data = np.random.rand(7, 10)
ypred = bst.predict(data)

如果在訓練過程中啟用了提前停止, 可以用 bst.best_iteration 從最佳迭代中獲得預測結果:

ypred = bst.predict(data, num_iteration=bst.best_iteration)
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容