机器学习领域的一个特点就是日新月异,在数据竞赛中,一件趁手的工具对比赛结果有重要影响。boosting是一种将弱分类器组合成强分类器的方法,它包含多种算法,如GDBT、AdaBoost、XGBoost等等。如果你参加过Kaggle之类的数据竞赛,你可能听说过XGBoost在数据江湖上的领导地位,也可能好奇过LGBM的快速崛起。但是,你听说过俄罗斯最大搜索引擎Yandex开发的CatBoost吗?
近日,南佛罗里达大学数据科学硕士Alvira Swalin就为我们做了一份测试。以下是论智对原文的编译:
最近我参加了一个Kaggle比赛(斯坦福大学的WIDS Datathon),依靠各种boosting算法,我最后挤进了前十名。虽然成绩很好,但从那之后我就对模型集成学习的细节感到十分好奇:那些模型是怎么组合的?参数怎么调整?它们各自的优点和缺点又是什么?
考虑到自己曾经用过许多boosting算法,我决定写一篇文章来重点分析XGBoost、LGBM和CatBoost的综合表现。虽然最近神经网络很流行,但就我个人的观点来看,boosting算法在训练数据有限时更好用,训练时间更短,调参所需的专业知识也较少。
XGBoost是陈天奇于2014年提出的一种算法,被称为GBM Killer,因为介绍它的文章有很多,所以本文会在介绍CatBoost和LGBM上用更大的篇幅。以下是我们将要讨论的几个主题:
-
结构差异
-
处理分类变量
-
参数简介
-
数据集实现
-
算法性能
虽然LightGBM和XGBoost都是基于决策树的boosting算法,但它们之间存在非常显著的结构差异。
LGBM采用leaf-wise生长策略,也就是基于梯度的单侧采样(GOSS)来找出用于分裂的数据实例,当增长到相同的叶子节点时,LGBM会直接找出分裂增益最大的叶子(通常是数据最大坨的那个),只分裂一个。
LightGBM
而XGBoost采用的则是level(depth)-wise生长策略,它用预排序算法+直方图算法为每一层的叶子找出最佳分裂,简而言之,就是它是不加区分地分裂同一层所有叶子。
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类型的列在默认情况下也会被视为数字,所以你要提前手动定义。
对于分类值大于one_hot_max_size
的那些分类变量,CatBoost也有一种有效的方法。它和均值编码类似,但可以防止过拟合:
-
对输入样本重新排序,并生成多个随机排列;
-
将label值从浮点或类别转换为整型;
-
用以下公式把所有分类特征值转换为数值,其中CountInClass表示截至当前样本,label值=1的次数(相同样本总数);Prior表示平滑因子,它由起始参数确定;而TotalCount则代表截至当前样本,所有样本的总数。
如果转换为数学公式,它长这样:
LightGBM
和CatBoost类似,LightGBM也可以通过输入特征名称来处理分类特征。它无需进行独热编码(one-hot coding),而是使用一种特殊的算法直接查找分类特征的拆分值,最后不仅效果相似,而且速度更快。
注:在为LGBM构造数据集之前,应将分类特征转换为整型数据,即便你用了categorical_feature参数,算法也无法识别字符串值。
XGBoost
XGBoost无法单独处理分类特征,它是基于CART的,所以只接收数值。在把分类数据提供给算法前,我们先得执行各种编码,如标签编码、均值编码、独热编码等。
这三种算法涉及的超参数有很多,这里我们只介绍几个重要的。下表是它们的对比:
我使用的是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)
现在我们就能从训练速度和准确率两个维度对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确实比其他两种算法更慢。
原文地址:towardsdatascience.com/catboost-vs-light-gbm-vs-xgboost-5f93620723db