1. 程式人生 > >【機器學習】資料分析王者 CatBoost vs. Light GBM vs. XGBoost

【機器學習】資料分析王者 CatBoost vs. Light GBM vs. XGBoost

機器學習領域的一個特點就是日新月異,在資料競賽中,一件趁手的工具對比賽結果有重要影響。boosting是一種將弱分類器組合成強分類器的方法,它包含多種演算法,如GDBT、AdaBoost、XGBoost等等。如果你參加過Kaggle之類的資料競賽,你可能聽說過XGBoost在資料江湖上的領導地位,也可能好奇過LGBM的快速崛起。但是,你聽說過俄羅斯最大搜索引擎Yandex開發的CatBoost嗎?

近日,南佛羅里達大學資料科學碩士Alvira Swalin就為我們做了一份測試。以下是論智對原文的編譯:

誰是資料競賽王者?CatBoost vs. Light GBM vs. XGBoost

最近我參加了一個Kaggle比賽(斯坦福大學的WIDS Datathon),依靠各種boosting演算法,我最後擠進了前十名。雖然成績很好,但從那之後我就對模型整合學習的細節感到十分好奇:那些模型是怎麼組合的?引數怎麼調整?它們各自的優點和缺點又是什麼?

考慮到自己曾經用過許多boosting演算法,我決定寫一篇文章來重點分析XGBoost、LGBM和CatBoost的綜合表現。雖然最近神經網路很流行,但就我個人的觀點來看,boosting演算法在訓練資料有限時更好用,訓練時間更短,調參所需的專業知識也較少。

誰是資料競賽王者?CatBoost vs. Light GBM vs. XGBoost

XGBoost是陳天奇於2014年提出的一種演算法,被稱為GBM Killer,因為介紹它的文章有很多,所以本文會在介紹CatBoost和LGBM上用更大的篇幅。以下是我們將要討論的幾個主題:

  • 結構差異

  • 處理分類變數

  • 引數簡介

  • 資料集實現

  • 演算法效能

LightGBM和XGBoost的結構差異

雖然LightGBM和XGBoost都是基於決策樹的boosting演算法,但它們之間存在非常顯著的結構差異。

LGBM採用leaf-wise生長策略,也就是基於梯度的單側取樣(GOSS)來找出用於分裂的資料例項,當增長到相同的葉子節點時,LGBM會直接找出分裂增益最大的葉子(通常是資料最大坨的那個),只分裂一個。

誰是資料競賽王者?CatBoost vs. Light GBM vs. XGBoost

LightGBM

而XGBoost採用的則是level(depth)-wise生長策略,它用預排序演算法+直方圖演算法為每一層的葉子找出最佳分裂,簡而言之,就是它是不加區分地分裂同一層所有葉子。

誰是資料競賽王者?CatBoost vs. Light GBM vs. XGBoost

XGBoost

我們先來看看預排序演算法(pre-sorted)的工作方式:

  • 對每個葉子(分割點)遍歷所有特徵;

  • 對每個特徵,按特徵值對資料點進行排序;

  • 確定當前特徵的基本分裂增益,用線性掃描決定最佳分裂方法;

  • 基於所有特徵採取最佳分裂方法。

而直方圖演算法的工作方式則是根據當前特徵把所有資料點分割稱離散區域,然後利用這些區域查詢直方圖的分割值。雖然比起預排序演算法那種在排序好的特徵值上列舉所有可能的分割點的做法,直方圖演算法的效率更高,但它在速度上還是落後於GOSS。

那麼為什麼GOSS這麼高效呢?

這裡我們需要提到經典的AdaBoost。在AdaBoost中,資料點的權重是資料點重要與否的一個直觀指標,但梯度提升決策樹(GBDT)不自帶這種權重,因此也就無法沿用AdaBoost的取樣方法。

基於梯度的取樣:梯度指的是損失函式切線的斜率,所以從邏輯上說,如果一些資料點的梯度很大,那它們對於找到最佳分裂方法來說就很重要,因為它們具有較高的誤差。

GOSS保留了所有具有大梯度的資料點,並對梯度小的資料點進行隨機取樣。例如,假設我有50萬行資料,其中1萬行梯度高,剩下的49萬行梯度低,那我的演算法就會選擇1萬行+49萬行×x%(隨機)。設x=10,最終演算法選出的就是50萬行資料中的5.9萬行。

這裡存在一個基本假設,即梯度較小的資料點具有更低的誤差,而且已經訓練好了

為了保持相同的資料分佈,在計算分裂增益時,GOSS會為這些梯度小的資料點引入一個常數乘數。以上就是它能在減少資料點數量和保證決策樹準確性之間取得平衡的方法。

處理分類變數

CatBoost

CatBoost在分類變數索引方面具有相當的靈活性,它可以用在各種統計上的分類特徵和數值特徵的組合將分類值編碼成數字(one_hot_max_size:如果feature包含的不同值的數目超過了指定值,將feature轉化為float)。

如果你沒有在cat_features語句中傳遞任何內容,CatBoost會把所有列視為數值變數。

注:如果在cat_features中未提供具有字串值的列,CatBoost會報錯。此外,具有預設int型別的列在預設情況下也會被視為數字,所以你要提前手動定義。

誰是資料競賽王者?CatBoost vs. Light GBM vs. XGBoost

對於分類值大於one_hot_max_size的那些分類變數,CatBoost也有一種有效的方法。它和均值編碼類似,但可以防止過擬合:

  • 對輸入樣本重新排序,並生成多個隨機排列;

  • 將label值從浮點或類別轉換為整型;

  • 用以下公式把所有分類特徵值轉換為數值,其中CountInClass表示截至當前樣本,label值=1的次數(相同樣本總數);Prior表示平滑因子,它由起始引數確定;而TotalCount則代表截至當前樣本,所有樣本的總數。

誰是資料競賽王者?CatBoost vs. Light GBM vs. XGBoost

如果轉換為數學公式,它長這樣:

誰是資料競賽王者?CatBoost vs. Light GBM vs. XGBoost

LightGBM

和CatBoost類似,LightGBM也可以通過輸入特徵名稱來處理分類特徵。它無需進行獨熱編碼(one-hot coding),而是使用一種特殊的演算法直接查詢分類特徵的拆分值,最後不僅效果相似,而且速度更快。

誰是資料競賽王者?CatBoost vs. Light GBM vs. XGBoost

注:在為LGBM構造資料集之前,應將分類特徵轉換為整型資料,即便你用了categorical_feature引數,演算法也無法識別字符串值。

XGBoost

XGBoost無法單獨處理分類特徵,它是基於CART的,所以只接收數值。在把分類資料提供給演算法前,我們先得執行各種編碼,如標籤編碼、均值編碼、獨熱編碼等。

相似的超引數

這三種演算法涉及的超引數有很多,這裡我們只介紹幾個重要的。下表是它們的對比:

誰是資料競賽王者?CatBoost vs. Light GBM vs. XGBoost

資料集實現

我使用的是2015年航班延誤的Kaggle資料集,因為它同時包含分類特徵和數字特徵,而且大約有500萬行資料,無論是從訓練速度上看還是從模型的準確率上看,它都可以作為一個很好的效能判斷工具。

我從資料集中抽取10%(50萬行)作為實驗資料,以下是建模使用的特徵:

  • MONTH,DAY,DAY_OF_WEEK:整型資料

  • AIRLINE和FLIGHT_NUMBER:整型資料

  • ORIGIN_AIRPORT和DESTINATION_AIRPORT:字串

  • DEPARTURE_TIME:float

  • ARRIVAL_DELAY:預測目標,航班延遲是否超過10分鐘?

  • DISTANCE和AIR_TIME:float

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調參時,我發現它很難為分類特徵傳遞索引。所以我針對沒傳遞的特徵調整了引數,最後有了兩個模型:一個包含分類特徵,一個不包含。因為one_hot_max_size不影響其他引數,所以我單獨對它做了調整。

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 vs. Light GBM vs. XGBoost

現在我們就能從訓練速度和準確率兩個維度對3種演算法進行評價了。

如上表所示,CatBoost在測試集上的準確率高達0.816,同時過擬合程度最低,訓練時長短,預測時間也最少。但它真的打敗其他兩種算髮了嗎?很可惜,沒有。0.816這個準確率的前提是我們考慮了分類變數,而且單獨調整了one_hot_max_size。如果沒有充分利用演算法的特性,CatBoost的表現是最差的,準確率只有0.752。

所以我們可以得出這樣一個結論:如果資料中存在分類變數,我們可以充分利用CatBoost的特性得到一個更好的訓練結果

接著就是我們的資料競賽王者XGBoost。它的表現很穩定,如果忽略之前的資料轉換工作,單從準確率上看它和CatBoost非常接近。但是XGBoost的缺點是太慢了,尤其是調參過程,簡直令人絕望(我花了6小時擺弄GridSearchCV)。

最後就是Light GBM,這裡我想提一點,就是用cat_features時它的速度和準確率會非常糟糕,我猜測這可能是因為這時演算法會在分類資料中用某種改良過的均值編碼,之後就過擬合了。如果我們能像XGBoost一樣操作,它也許可以在速度秒殺XGBoost的同時達到後者的精度。

綜上所述,這些觀察結果都是對應這個資料集得出的結論,它們可能不適用於其他資料集。我在文中沒有展示交叉驗證過程,但我確實嘗試了,結果差不多。話雖如此,但有一個結果是千真萬確的:XGBoost確實比其他兩種演算法更慢