1. 程式人生 > >入門 | 從結構到效能,一文概述XGBoost、Light GBM和CatBoost的同與不同

入門 | 從結構到效能,一文概述XGBoost、Light GBM和CatBoost的同與不同

最近,我參加了 kaggle 競賽 WIDS Datathon,並通過使用多種 boosting 演算法,最終排名前十。從那時開始,我就對這些演算法的內在工作原理非常好奇,包括調參及其優劣勢,所以有了這篇文章。儘管最近幾年神經網路復興,並變得流行起來,但我還是更加關注 boosting 演算法,因為在訓練樣本量有限、所需訓練時間較短、缺乏調參知識的場景中,它們依然擁有絕對優勢。

這裡寫圖片描述

  • 2014 年 3 月,XGBOOST 最早作為研究專案,由陳天奇提出
  • 2017 年 1 月,微軟釋出首個穩定版 LightGBM
  • 2017 年 4 月,俄羅斯頂尖技術公司 Yandex 開源 CatBoost

由於 XGBoost(通常被稱為 GBM 殺手)已經在機器學習領域出現了很久,如今有非常多詳細論述它的文章,所以本文將重點討論 CatBoost 和 LGBM,在下文我們將談到:

  • 演算法結構差異
  • 每個演算法的分類變數時的處理
  • 如何理解引數
  • 演算法在資料集上的實現
  • 每個演算法的表現

LightGBM 和 XGBoost 的結構差異

在過濾資料樣例尋找分割值時,LightGBM 使用的是全新的技術:基於梯度的單邊取樣(GOSS);而 XGBoost 則通過預分類演算法和直方圖演算法來確定最優分割。這裡的樣例(instance)表示觀測值/樣本。

首先讓我們理解預分類演算法如何工作:

  • 對於每個節點,遍歷所有特徵
  • 對於每個特徵,根據特徵值分類樣例
  • 進行線性掃描,根據當前特徵的基本資訊增益,確定最優分割
  • 選取所有特徵分割結果中最好的一個

簡單說,直方圖演算法在某個特徵上將所有資料點劃分到離散區域,並通過使用這些離散區域來確定直方圖的分割值。雖然在計算速度上,和需要在預分類特徵值上遍歷所有可能的分割點的預分類演算法相比,直方圖演算法的效率更高,但和 GOSS 演算法相比,其速度仍然更慢。

為什麼 GOSS 方法如此高效?

在 Adaboost 中,樣本權重是展示樣本重要性的很好的指標。但在梯度提升決策樹(GBDT)中,並沒有天然的樣本權重,因此 Adaboost 所使用的取樣方法在這裡就不能直接使用了,這時我們就需要基於梯度的取樣方法。

梯度表徵損失函式切線的傾斜程度,所以自然推理到,如果在某些意義上資料點的梯度非常大,那麼這些樣本對於求解最優分割點而言就非常重要,因為算其損失更高。

GOSS 保留所有的大梯度樣例,並在小梯度樣例上採取隨機抽樣。比如,假如有 50 萬行資料,其中 1 萬行資料的梯度較大,那麼我的演算法就會選擇(這 1 萬行梯度很大的資料+x% 從剩餘 49 萬行中隨機抽取的結果)。如果 x 取 10%,那麼最後選取的結果就是通過確定分割值得到的,從 50 萬行中抽取的 5.9 萬行。

在這裡有一個基本假設:如果訓練集中的訓練樣例梯度很小,那麼演算法在這個訓練集上的訓練誤差就會很小,因為訓練已經完成了。

為了使用相同的資料分佈,在計算資訊增益時,GOSS 在小梯度資料樣例上引入一個常數因子。因此,GOSS 在減少資料樣例數量與保持已學習決策樹的準確度之間取得了很好的平衡。

這裡寫圖片描述

高梯度/誤差的葉子,用於 LGBM 中的進一步增長

每個模型是如何處理屬性分類變數的?

CatBoost

CatBoost 可賦予分類變數指標,進而通過獨熱最大量得到獨熱編碼形式的結果(獨熱最大量:在所有特徵上,對小於等於某個給定引數值的不同的數使用獨熱編碼)。

如果在 CatBoost 語句中沒有設定「跳過」,CatBoost 就會將所有列當作數值變數處理。

注意,如果某一列資料中包含字串值,CatBoost 演算法就會丟擲錯誤。另外,帶有預設值的 int 型變數也會預設被當成數值資料處理。在 CatBoost 中,必須對變數進行宣告,才可以讓演算法將其作為分類變數處理。

這裡寫圖片描述

對於可取值的數量比獨熱最大量還要大的分類變數,CatBoost 使用了一個非常有效的編碼方法,這種方法和均值編碼類似,但可以降低過擬合情況。它的具體實現方法如下:

  1. 將輸入樣本集隨機排序,並生成多組隨機排列的情況。

  2. 將浮點型或屬性值標記轉化為整數。

  3. 將所有的分類特徵值結果都根據以下公式,轉化為數值結果。

這裡寫圖片描述

其中 CountInClass 表示在當前分類特徵值中,有多少樣本的標記值是「1」;Prior 是分子的初始值,根據初始引數確定。TotalCount 是在所有樣本中(包含當前樣本),和當前樣本具有相同的分類特徵值的樣本數量。

可以用下面的數學公式表示:

這裡寫圖片描述

LightGBM

和 CatBoost 類似,LighGBM 也可以通過使用特徵名稱的輸入來處理屬性資料;它沒有對資料進行獨熱編碼,因此速度比獨熱編碼快得多。LGBM 使用了一個特殊的演算法來確定屬性特徵的分割值。

這裡寫圖片描述

注意,在建立適用於 LGBM 的資料集之前,需要將分類變數轉化為整型變數;此演算法不允許將字串資料傳給分類變數引數。

XGBoost

和 CatBoost 以及 LGBM 演算法不同,XGBoost 本身無法處理分類變數,而是像隨機森林一樣,只接受數值資料。因此在將分類資料傳入 XGBoost 之前,必須通過各種編碼方式:例如標記編碼、均值編碼或獨熱編碼對資料進行處理。

超引數中的相似性

所有的這些模型都需要調節大量引數,但我們只談論其中重要的。以下是將不同演算法中的重要引數按照功能進行整理的表格。

這裡寫圖片描述

實現

在這裡,我使用了 2015 年航班延誤的 Kaggle 資料集,其中同時包含分類變數和數值變數。這個資料集中一共有約 500 萬條記錄,因此很適合用來同時評估比較三種 boosting 演算法的訓練速度和準確度。我使用了 10% 的資料:50 萬行記錄。

以下是建模使用的特徵:

  • 月、日、星期:整型資料
  • 航線或航班號:整型資料
  • 出發、到達機場:數值資料
  • 出發時間:浮點資料
  • 到達延誤情況:這個特徵作為預測目標,並轉為二值變數:航班是否延誤超過 10 分鐘
  • 距離和飛行時間:浮點資料
import pandas as pd, numpy as np, time
from sklearn.model_selection import train_test_split

data = pd.read_csv("flights.csv")
data = data.sample(frac = 0.1, random_state=10)

data = data[["MONTH","DAY","DAY_OF_WEEK","AIRLINE","FLIGHT_NUMBER","DESTINATION_AIRPORT",
                 "ORIGIN_AIRPORT","AIR_TIME", "DEPARTURE_TIME","DISTANCE","ARRIVAL_DELAY"]]
data.dropna(inplace=True)

data["ARRIVAL_DELAY"] = (data["ARRIVAL_DELAY"]>10)*1

cols = ["AIRLINE","FLIGHT_NUMBER","DESTINATION_AIRPORT","ORIGIN_AIRPORT"]
for item in cols:
    data[item] = data[item].astype("category").cat.codes +1

train, test, y_train, y_test = train_test_split(data.drop(["ARRIVAL_DELAY"], axis=1), data["ARRIVAL_DELAY"],
                                                random_state=10, test_size=0.25)

XGBoost

import xgboost as xgb from sklearn import metrics

def auc(m, train, test): 
    return (metrics.roc_auc_score(y_train,m.predict_proba(train)[:,1]),
                            metrics.roc_auc_score(y_test,m.predict_proba(test)[:,1]))

# Parameter Tuning
model = xgb.XGBClassifier()
param_dist = {"max_depth": [10,30,50],
              "min_child_weight" : [1,3,6],
              "n_estimators": [200],
              "learning_rate": [0.05, 0.1,0.16],}
grid_search = GridSearchCV(model, param_grid=param_dist, cv = 3, 
                                   verbose=10, n_jobs=-1)
grid_search.fit(train, y_train)

grid_search.best_estimator_

model = xgb.XGBClassifier(max_depth=50, min_child_weight=1,  n_estimators=200,\
                          n_jobs=-1 , verbose=1,learning_rate=0.16)
model.fit(train,y_train)

auc(model, train, test)

Light GBM

import lightgbm as lgb
from sklearn import metrics

def auc2(m, train, test): 
    return (metrics.roc_auc_score(y_train,m.predict(train)),
                            metrics.roc_auc_score(y_test,m.predict(test)))

lg = lgb.LGBMClassifier(silent=False)
param_dist = {"max_depth": [25,50, 75],
              "learning_rate" : [0.01,0.05,0.1],
              "num_leaves": [300,900,1200],
              "n_estimators": [200]
             }
grid_search = GridSearchCV(lg, n_jobs=-1, param_grid=param_dist, cv = 3, scoring="roc_auc", verbose=5)
grid_search.fit(train,y_train)
grid_search.best_estimator_

d_train = lgb.Dataset(train, label=y_train)
params = {"max_depth": 50, "learning_rate" : 0.1, "num_leaves": 900,  "n_estimators": 300}

# Without Categorical Features
model2 = lgb.train(params, d_train)
auc2(model2, train, test)

#With Catgeorical Features
cate_features_name = ["MONTH","DAY","DAY_OF_WEEK","AIRLINE","DESTINATION_AIRPORT",
                 "ORIGIN_AIRPORT"]
model2 = lgb.train(params, d_train, categorical_feature = cate_features_name)
auc2(model2, train, test)

CatBoost

在對 CatBoost 調參時,很難對分類特徵賦予指標。因此,我同時給出了不傳遞分類特徵時的調參結果,並評估了兩個模型:一個包含分類特徵,另一個不包含。我單獨調整了獨熱最大量,因為它並不會影響其他引數。

import catboost as cb
cat_features_index = [0,1,2,3,4,5,6]

def auc(m, train, test): 
    return (metrics.roc_auc_score(y_train,m.predict_proba(train)[:,1]),
                            metrics.roc_auc_score(y_test,m.predict_proba(test)[:,1]))

params = {'depth': [4, 7, 10],
          'learning_rate' : [0.03, 0.1, 0.15],
         'l2_leaf_reg': [1,4,9],
         'iterations': [300]}
cb = cb.CatBoostClassifier()
cb_model = GridSearchCV(cb, params, scoring="roc_auc", cv = 3)
cb_model.fit(train, y_train)

With Categorical features
clf = cb.CatBoostClassifier(eval_metric="AUC", depth=10, iterations= 500, l2_leaf_reg= 9, learning_rate= 0.15)
clf.fit(train,y_train)
auc(clf, train, test)

With Categorical features
clf = cb.CatBoostClassifier(eval_metric="AUC",one_hot_max_size=31, \
                            depth=10, iterations= 500, l2_leaf_reg= 9, learning_rate= 0.15)
clf.fit(train,y_train, cat_features= cat_features_index)
auc(clf, train, test)

結語

這裡寫圖片描述

為了評估模型,我們應該同時考慮模型的速度和準確度表現。

請記住,CatBoost 在測試集上表現得最好,測試集的準確度最高(0.816)、過擬合程度最小(在訓練集和測試集上的準確度很接近)以及最小的預測和除錯時間。但這個表現僅僅在有分類特徵,而且調節了獨熱最大量時才會出現。如果不利用 CatBoost 演算法在這些特徵上的優勢,它的表現效果就會變成最差的:僅有 0.752 的準確度。因此我們認為,只有在資料中包含分類變數,同時我們適當地調節了這些變數時,CatBoost 才會表現很好。

第二個使用的是 XGBoost,它的表現也相當不錯。即使不考慮資料集包含有轉換成數值變數之後能使用的分類變數,它的準確率也和 CatBoost 非常接近了。但是,XGBoost 唯一的問題是:它太慢了。尤其是對它進行調參,非常令人崩潰(我用了 6 個小時來執行 GridSearchCV——太糟糕了)。更好的選擇是分別調參,而不是使用 GridSearchCV。

最後一個模型是 LightGBM,這裡需要注意的一點是,在使用 CatBoost 特徵時,LightGBM 在訓練速度和準確度上的表現都非常差。我認為這是因為它在分類資料中使用了一些修正的均值編碼方法,進而導致了過擬合(訓練集準確率非常高:0.999,尤其是和測試集準確率相比之下)。但如果我們像使用 XGBoost 一樣正常使用 LightGBM,它會比 XGBoost 更快地獲得相似的準確度,如果不是更高的話(LGBM—0.785, XGBoost—0.789)。

最後必須指出,這些結論在這個特定的資料集下成立,在其他資料集中,它們可能正確,也可能並不正確。但在大多數情況下,XGBoost 都比另外兩個演算法慢。

所以,你更喜歡哪個演算法呢?