一、xgboost的介绍
xgboost以及后续的lightGBM等在我的认知中算是目前数据挖掘中最常用的预测算法,在各种数据挖掘比赛中获得了Top的名次。如果想从事数据挖掘相关的岗位,该算法是一定要好好掌握的。
xgboost其实可以认为是GBDT算法的工程实现,GBDT的全名是Gradient Boosting Decision Tree,就是梯度提升树。该算法的逻辑是:首先训练一颗树,然后计算出每个样本的误差(也叫梯度),通过去拟合误差训练下一颗树,一直拟合直到训练结果符合要求。下图是GBDT算法的步骤图。
xgboost虽然大家都说是GBDT的工程实现,但是在很多地方都是做了不少优化的:
1、目标函数的二阶泰勒展开
xgboost与GBDT都是每一步都只优化当前模型,然后将所有模型加起来:
目标函数为:
其中𝐿为损失函数, 是所有树的累加结果。 是每棵树的正则项(xgb引入了正则项,因此目标函数加上了结构化风险)。
与GBDT的一阶不同,xgboost采用了二阶展开,这其实就是优化算法的改进,同时拟合一阶误差和二阶误差有利于提升精度。
二阶展开:
其中可以将 的结果视为 , 视为 , 为 的函数。我们将 展开,重新得到我们的目标函数:
在每次更新的时候,前𝑚−1个模型已经是确定了的,因此上面的式子中只有 是未知的(其实也就是我们即将生成的𝑚棵树)。我们将常数部分稍微处理以下:
其中:
也就是每个样本的前𝑚−1棵树与最终结果之间的一阶导数和二阶导数。在第𝑚−1树已经确定了的情况下,是很容易计算出来的。
2、xgboost的正则化
上文中我们提到了xgboost与GBDT相比还有一个正则项。其中:
其中𝑇为树𝑓的叶节点的个数, 为第𝑗个叶节点的权重。很容易理解叶节点越少越不容易过拟合,但是为什么𝑤叶节点的取值也可以作为正则项呢? 我的理解是:
xgboost是一个加法模型,每个样本最后的取值是每个树的值加起来。比如某个样本在n棵树中处于叶节点的取值分别为: 。那么该样本最后的取值为: (这么一看的话和逻辑输入特征为0-1的逻辑回归还是很像的)。我们会倾向于该样本在每个树中的取值尽可能的小一些,那样样本最后的取值受某一个树的影响就会变小,更不容易过拟合。
我们将正则化的式子代入到上面的目标函数中:
我们定义,处于叶节点𝑗上的样本集合为𝐼(𝑗),这样我们可以将所有的样本拆分为𝐽份,映射到各个叶节点中:
其中在各个叶节点中 。
同样我们可以进一步将式子简化:
其中:
上述所有的变量中,只有 是未知量,因此我们很容易根据求解一元二次方程,当函数取得最小值时:
将其代入函数我们有:
以上就是xgboost模型的推导了。下面我们来介绍该模型的优化方法。
3、算法优化
对于树模型来说,算法优化其实就是指导树的分裂。xgb算法的思路与CART一样,都是遍历所有特征,选择收益最高的分裂点进行分裂。假设我们有 , 为集合𝐼分裂之后的左右集合。那么在分裂后目标函数的减少为:
(1):精确贪婪算法
这种算法就是详细地遍历每个特征,然后遍历每个特征可能的取值,搜索出最优的分裂特征以及该特征的分裂值
采用这种精确贪婪算法的算法步骤如下:
(2):近似算法
在很多种情况下,每个特征的取值(离散的取值)可能有成千上万个,遍历每个取值的话时间开销会非常的大。很多时候,特征分裂的取值稍微差了一点也不会有太大的影响。为了对整个分裂过程进行加速,我们可以对特征值进行分箱。
比如将样本种某个特征是连续值,取值有几万个,我们可以将其分箱为几十个,然后遍历每个特征的取值的时候,我们只需要遍历每个箱。这样的话极快地减少了计算的次数。
关于分箱的方法在xgb的论文种提及了两种:global和local两种。global是在所有树生成开始之前,将需要的特征分好箱。整个训练过程每个特征都只需要分一次。local是在每次进行特征值遍历的时候进行分箱,一个训练过程会分裂很多很多次。
该算法的步骤为:
下图中是xgboost作者关于global和local还有精确贪婪的一个结果对比:
1/eps就是分箱的个数,可以看到使用global,local,还有精确的贪婪搜索都可以达到差不多的结果。同时使用global的话需要相比local分的更细,当然它也只需要分一次。
(3):加权分位数
这一步是对上一步近似算法补充,上一步我们说到对特征分箱,但是怎么对特征分箱?常用的方法是按照样本分布,比如100个样本我要分5箱,那么我可能会使得每箱中有20个样本,这样我就可以将样本排序后,在第20,40,60,80处对样本进行分箱。但是在xgboost中采用一种加权分为的分箱方法。
如下图所示,将作为权重,每个区间中的权重相等。
为什么要采用这种方法呢?理由就是每个样本对最终目标函数的贡献是不一样的,而它们与目标函数的贡献是与 相关的。具体的推导如下:
我们将这个函数稍微处理下:
我们知道当 时,目标函数取得最优值,代入上式得话可以发现最后我们的目标函数确实是与 成正相关的。
4、其余细节的优化方法
(1):列采用
列采样是随机森林中的做法,具体的操作细节就是每次分裂的时候并不选取全部的可以进行分裂的特征,而是选择其中一个子集,这样一是防止过拟合,二是减少运行时间。在随机森林中还有每次不选取全部训练样本而是选取部分样本进行训练的操作, xgb中并没有介绍到使用了。
(2):学习率
学习率也叫做步长,是在每个子模型前(即在每个叶节点的回归值上)乘上该系数,削弱每颗树的影响,使得迭代更稳定。与随机梯度下降中的学习率类似
(3):稀疏感知
在数据中经常会出现这些现象:1:缺失值,2:为0的值太多,3:特征one-hot(其实也是导致为0的值太多)
xgb采用稀疏感知策略来同时处理这类问题,将缺失值和稀疏0值等同视作缺失值,"绑定"在一起,节点分裂的时候不会遍历这些节点。然后将这类值分裂到某个节点。下面是该算法的步骤:
可以看到并没有说放置在左节点还是右节点中,这个需要进行判断。
(4):特征并行优化
分裂之前提前将将所有特征进行并行地分箱,然后计算每个箱里面地和,然后并行地计算每个特征地分裂收益。
(5):其余工程优化
xgb还有一些工程上的优化,比如Cache-aware Access(缓存访问),Blocks for Out-of-core Computation(核外计算模块),感兴趣的可以了解一下。
三、xgb实战
上面我们已经介绍了XGB的理论基础,了解它以后我们需要更好地应用它。下面我将来使用一个case来详细地介绍一下怎么使用xgb算法(也就是怎么优雅地调包)。我们采用五折交叉验证法进行训练。
import numpy as np
from sklearn import datasets
import pandas as pd
from sklearn.model_selection import train_test_split
X=datasets.load_breast_cancer()['data']
Y=datasets.load_breast_cancer()['target']
fea=datasets.load_breast_cancer()['feature_names']data=pd.DataFrame(X,columns=fea)
data['target']=Y
x_train=data[fea]
y_train=data['target']
data['target'].value_counts()'''
1 357
0 212
Name: target, dtype: int64
'''
xgb的包有两种,一般我们也会看到两种形式的xgb算法。一种是xgb原生的,一种是采用了sklearn包装之后的。
首先我们使用xgb的原生包进行训练:
from sklearn.model_selection import KFold,StratifiedKFold
import xgboost as xgb
import datetime
import sklearn
folds = KFold(n_splits=5, shuffle=True, random_state=1996)params = {'learning_rate': 0.05,'max_depth': 8, 'eval_metric': 'auc','objective': 'binary:logistic',
}import warnings
warnings.filterwarnings('ignore')oof=np.zeros(x_train.shape[0])
for fold_, (train_index, test_index) in enumerate(folds.split(x_train, y_train)):print("第{}折".format(fold_))train_x, test_x, train_y, test_y = x_train.iloc[train_index], x_train.iloc[test_index], y_train.iloc[train_index], y_train.iloc[test_index] num_round=5dtrain = xgb.DMatrix(train_x, train_y)dval = xgb.DMatrix(test_x, test_y)xgb_model = xgb.train(params,dtrain,num_boost_round=10000,evals=[(dtrain, 'Train'), (dval, 'Valid')],early_stopping_rounds=30,verbose_eval=50,)val_train=xgb_model.predict(dval)oof[test_index]=val_train
print("最终auc为:",sklearn.metrics.roc_auc_score(y_train,oof))'''
第0折
[0] Train-auc:0.989447 Valid-auc:0.987137
Multiple eval metrics have been passed: 'Valid-auc' will be used for early stopping.Will train until Valid-auc hasn't improved in 30 rounds.
[50] Train-auc:0.999918 Valid-auc:0.997661
Stopping. Best iteration:
[44] Train-auc:0.999918 Valid-auc:0.997995第1折
[0] Train-auc:0.998555 Valid-auc:0.951389
Multiple eval metrics have been passed: 'Valid-auc' will be used for early stopping.Will train until Valid-auc hasn't improved in 30 rounds.
Stopping. Best iteration:
[16] Train-auc:0.999959 Valid-auc:0.963624第2折
[0] Train-auc:0.98871 Valid-auc:0.984702
Multiple eval metrics have been passed: 'Valid-auc' will be used for early stopping.Will train until Valid-auc hasn't improved in 30 rounds.
Stopping. Best iteration:
[7] Train-auc:0.998815 Valid-auc:0.999034第3折
[0] Train-auc:0.994967 Valid-auc:0.968919
Multiple eval metrics have been passed: 'Valid-auc' will be used for early stopping.Will train until Valid-auc hasn't improved in 30 rounds.
[50] Train-auc:0.999836 Valid-auc:0.990878
[100] Train-auc:1 Valid-auc:0.99527
Stopping. Best iteration:
[93] Train-auc:1 Valid-auc:0.99527第4折
[0] Train-auc:0.99598 Valid-auc:0.929513
Multiple eval metrics have been passed: 'Valid-auc' will be used for early stopping.Will train until Valid-auc hasn't improved in 30 rounds.
[50] Train-auc:0.999917 Valid-auc:0.974802
Stopping. Best iteration:
[33] Train-auc:0.999711 Valid-auc:0.97859最终auc为: 0.9856178320384757
'''
下面我们采用sklearn包装后的包进行训练。sklearn包装的xgb的参数比较多,常见的有:
objective="binary:logistic" 目标函数,代表是二分类,损失函数是logistic
max_depth=6 树的深度
learning_rate=0.05 学习率
n_estimators=1000 树的数目
verbosity=10 每n次显示结果
booster="gbtree" xgb算法,默认就是GBDT
min_child_weight 样本权重,其实就是每个节点最少需要多少样本
subsample 样本采样
colsample_bytree="0.9",colsample_bynode="0.85" 特征采样
scale_pos_weight 处理样本不平衡的,scale_pos_weight的值为负样本数量除以正样本数量
random_state 随机种子,主要是复复现
oof=np.zeros(x_train.shape[0])
for fold_, (train_index, test_index) in enumerate(folds.split(x_train, y_train)):print("第{}折".format(fold_))train_x, test_x, train_y, test_y = x_train.iloc[train_index], x_train.iloc[test_index], y_train.iloc[train_index], y_train.iloc[test_index] model=xgb.XGBClassifier(objective="binary:logistic",booster="gbtree",max_depth=6, learning_rate=0.05, n_estimators=1000,n_jobs=1, gamma=1,
# min_child_weight=1, subsample=0.9,
# colsample_bytree=0.9,
# colsample_bynode=0.85, reg_alpha=1, reg_lambda=1,scale_pos_weight=5, random_state=2022)model.fit(train_x,train_y,sample_weight=None,eval_set=[(train_x,train_y),(test_x,test_y)],eval_metric="auc",early_stopping_rounds=50,verbose=20,)val_train=model.predict_proba(test_x)[:,1]oof[test_index]=val_train
print("最终auc为:",sklearn.metrics.roc_auc_score(y_train,oof))'''
第0折
[0] validation_0-auc:0.963656 validation_1-auc:0.950551
Multiple eval metrics have been passed: 'validation_1-auc' will be used for early stopping.Will train until validation_1-auc hasn't improved in 50 rounds.
[20] validation_0-auc:0.996221 validation_1-auc:0.997995
[40] validation_0-auc:0.996396 validation_1-auc:0.997327
[60] validation_0-auc:0.999341 validation_1-auc:0.998329
Stopping. Best iteration:
[22] validation_0-auc:0.996324 validation_1-auc:0.999332第1折
[0] validation_0-auc:0.984334 validation_1-auc:0.922288
Multiple eval metrics have been passed: 'validation_1-auc' will be used for early stopping.Will train until validation_1-auc hasn't improved in 50 rounds.
[20] validation_0-auc:0.999856 validation_1-auc:0.955026
[40] validation_0-auc:1 validation_1-auc:0.953704
[60] validation_0-auc:1 validation_1-auc:0.965939
[80] validation_0-auc:1 validation_1-auc:0.964616
[100] validation_0-auc:1 validation_1-auc:0.96379
Stopping. Best iteration:
[61] validation_0-auc:1 validation_1-auc:0.96627第2折
[0] validation_0-auc:0.962804 validation_1-auc:0.943639
Multiple eval metrics have been passed: 'validation_1-auc' will be used for early stopping.Will train until validation_1-auc hasn't improved in 50 rounds.
[20] validation_0-auc:0.99316 validation_1-auc:0.998068
[40] validation_0-auc:0.999771 validation_1-auc:0.999356
[60] validation_0-auc:1 validation_1-auc:0.999356
[80] validation_0-auc:1 validation_1-auc:0.999034
Stopping. Best iteration:
[48] validation_0-auc:0.999875 validation_1-auc:0.999678第3折
[0] validation_0-auc:0.970119 validation_1-auc:0.916216
Multiple eval metrics have been passed: 'validation_1-auc' will be used for early stopping.Will train until validation_1-auc hasn't improved in 50 rounds.
[20] validation_0-auc:0.992656 validation_1-auc:0.959628
[40] validation_0-auc:0.99624 validation_1-auc:0.983446
[60] validation_0-auc:0.998562 validation_1-auc:0.9875
[80] validation_0-auc:0.999322 validation_1-auc:0.991554
[100] validation_0-auc:0.999938 validation_1-auc:0.993919
[120] validation_0-auc:1 validation_1-auc:0.995946
[140] validation_0-auc:1 validation_1-auc:0.995946
[160] validation_0-auc:1 validation_1-auc:0.995946
[180] validation_0-auc:1 validation_1-auc:0.995946
Stopping. Best iteration:
[143] validation_0-auc:1 validation_1-auc:0.996284第4折
[0] validation_0-auc:0.986824 validation_1-auc:0.965744
Multiple eval metrics have been passed: 'validation_1-auc' will be used for early stopping.Will train until validation_1-auc hasn't improved in 50 rounds.
[20] validation_0-auc:0.996517 validation_1-auc:0.962615
[40] validation_0-auc:0.999308 validation_1-auc:0.967227
[60] validation_0-auc:0.999762 validation_1-auc:0.96805
[80] validation_0-auc:0.999938 validation_1-auc:0.966733
[100] validation_0-auc:1 validation_1-auc:0.972167
[120] validation_0-auc:1 validation_1-auc:0.973155
[140] validation_0-auc:1 validation_1-auc:0.974144
[160] validation_0-auc:1 validation_1-auc:0.972167
[180] validation_0-auc:1 validation_1-auc:0.972167
Stopping. Best iteration:
[130] validation_0-auc:1 validation_1-auc:0.975461最终auc为: 0.9810461920617303
'''
两种接口最后的结果其实也差不多,其实也就看大家习惯使用哪个。其实我比较倾向于第二种采用sklearn接口的,主要是参数较多,比如"样本权重"在原生接口中我没有看到过。
以上就是关于xgb的所有内容,再一次感叹xgb的强大,时至今日,很多还是应用于各种场景中,据我了解,很多非互联网场景的数据预测挖掘中还是工业界的首选。关于实战的部分可能较少,是因为我想把多一些的实战内容放到下一节LightGBM中。