XGBoost

XGBoost

参考

  1. xgboost: 速度快效果好的 boosting 模型
  2. 菜菜的sklearn课堂11 -XGBoost
  3. 课件
  4. GBDT、XGBoost、LightGBM 的使用及参数调优

概述

XGBoost全称是eXtreme Gradient Boosting,可译为极限梯度提升算法它由陈天奇所设计,致力于让提升树突破自身的计算极限,以实现运算快速,性能优秀的工程目标。和传统的梯度提升算法相比,XGBoost进行了许多改进,它能够比其他使用梯度提升的集成算法更加快速,并且已经被认为是在分类和回归上都拥有超高性能的先进评估器。

从2016年开始,各大竞赛平台排名前列的解决方案逐渐由XGBoost算法统治,业界甚至将其称之为“机器学习竞赛的胜利女神”。

xgboost库与XGB的sklearn API

  1. xgboost是一个独立的,开源的,专门提供梯度提升树以及XGBoost算法应用的算法库。它和sklearn类似,有一个详细的官方网站可以供我们查看,并且可以与C,Python,R,Julia等语言连用,但需要我们单独安装和下载
    官方文档:xgboost documents
    xgboost建模流程:对数据格式有特定要求,参数需要提前设置(字典格式),
    xgboost建模
  2. sklearn的API,建模流程和其他的sklearn算法类似,属性和接口也类似
    比如:class xgboost.XGBRegressor (max_depth=3, learning_rate=0.1, n_estimators=100, silent=True,objective='reg:linear', booster='gbtree', n_jobs=1, nthread=None, gamma=0, min_child_weight=1, max_delta_step=0, subsample=1, colsample_bytree=1, colsample_bylevel=1, reg_alpha=0, reg_lambda=1, scale_pos_weight=1, base_score=0.5, random_state=0, seed=None, missing=None, importance_type='gain', **kwargs)

XGBoost的三大板块

XGBoost本身的核心是基于梯度提升树实现的集成算法,整体来说可以有三个核心部分:集成算法本身,用于集成的
弱评估器,以及应用中的其他过程。

参数 集成算法 弱评估器 其他过程
n_estimators
learning_rate
silent
subsample
max_depth
objective
booster
gamma
min_child_weight
max_delta_step
colsample_bytree
colsample_bylevel
reg_alpha
reg_lambda
nthread
n_jobs
scale_pos_weight
base_score
seed
random_state
missing
importance_type

提升集成算法

XGBoost的基础是梯度提升算法,梯度提升(Gradient boosting)是构建预测模型的最强大技术之一,它是集成算法中提升法(Boosting)的代表算法。集成算法通过在数据上构建多个弱评估器,汇总所有弱评估器的建模结果,以获取比单个模型更好的回归或分类表现。弱评估器被定义为是表现至少比随机猜测更好的模型,即预测准确率不低于50%的任意模型。

提升法的中最著名的算法包括Adaboost和梯度提升树(GBDT),XGBoost就是由梯度提升树发展而来的。梯度提升树中可以有回归树也可以有分类树,两者都以CART树算法作为主流,XGBoost背后也是CART树,这意味着XGBoost中所有的树都是二叉的。

就以梯度提升回归树为例,梯度提升回归树是专注于回归的树模型的提升集成模型,其建模过程大致如下:

  • 最开始先建立一棵树,然后逐渐迭代,每次迭代过程中都增加一棵树,逐渐形成众多树模型集成的强评估器。

GBDT
先以分类树为例:

  • 对于决策树,每个样本最终都会有一个叶子节点归属,而分类树所给的预测则是少数服从多数的原则,即返回这个叶子上数量最多的标签。

分类树

对于回归树,每个叶子节点上的值就是这个叶子节点上所有样本的均值。对于梯度提升回归树来说,每个样本的预测结果可以表示为所有树上的结果的加权求和:
梯度提升回归树
其中,K 是树的总数量,k 代表第 k 棵树, γk是这棵树的权重,hk 表示这棵树上的预测结果。

梯度提升归回树

注意:XGB vs GBDT 核心区别1:求解预测值 y_hat 的方式不同

  • GBDT中预测值是由所有弱分类器上的预测结果的加权求和,其中每个样本上的预测结果就是样本所在的叶子节点的均值。而XGBT中的预测值是所有弱分类器上的叶子权重直接求和得到,计算叶子权重是一个复杂的过程

在集成中我们需要的考虑的第一件事是我们的超参数 K ,究竟要建多少棵树?

参数含义 xgb.train() xgb.XGBRegressor()
集成中弱评估器的数量 num_round,默认10 n_estimators,默认100
训练中是否打印每次训练的结果 slient,默认False slient,默认True

不同模型的简单比对

  • 导入需要的库,模块以及数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    # 三种回归模型
    from xgboost import XGBRegressor as XGBR
    from sklearn.ensemble import RandomForestRegressor as RFR
    from sklearn.linear_model import LogisticRegression as LinearR

    # 数据集
    from sklearn.datasets import load_boston
    from sklearn.model_selection import KFold,cross_val_score as CVS,train_test_split as TTS

    # 模型评估,这里使用均方误差
    from sklearn.metrics import mean_squared_error as MSE

    # 其他辅助库
    import pandas as pd
    import numpy as np
    import matplotlib.pyplot as plt
    from time import time

    data=load_boston()
    Xtrain,Xtest,Ytrain,Ytest = TTS(data.data,data.target,test_size=0.3,random_state=10)
  • 建模,查看其他接口和属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    xgbr=XGBR(n_estimators=100).fit(Xtrain,Ytrain)
    print(xgbr.predict(Xtest).shape) # 传统接口predict,返回没个样本的预测结果,为了方便显示这里只看形状
    print(xgbr.score(Xtest,Ytest)) # 默认使用R2作为模型评估指标,越接近1越好
    print(data.target.mean(),MSE(Ytest,xgbr.predict(Xtest))) # 均方误差,可以与均值对比进行评估
    print(xgbr.feature_importances_) # 树模型的优势之一:能够查看模型的重要性分数,可以使用嵌入法(selectfrommodel)进行特征选择
    '''
    (152,)
    0.8963630811711718
    22.532806324110677 10.11843177713594
    [0.02106727 0.00336115 0.0069604 0.01292224 0.04037843 0.19033682
    0.00970811 0.03515113 0.00808811 0.01782032 0.05347034 0.01479358
    0.5859421 ]
    '''
  • 交叉验证,与线性回归&随机森林回归进行对比

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
     # 查看一下sklearn中所有的模型评估指标
    import sklearn
    print(sorted(sklearn.metrics.SCORERS.keys()))
    '''
    ['accuracy', 'adjusted_mutual_info_score', 'adjusted_rand_score', 'average_precision', 'balanced_accuracy', 'brier_score_loss', 'completeness_score', 'explained_variance', 'f1', 'f1_macro', 'f1_micro', 'f1_samples', 'f1_weighted', 'fowlkes_mallows_score', 'homogeneity_score', 'jaccard', 'jaccard_macro', 'jaccard_micro', 'jaccard_samples', 'jaccard_weighted', 'max_error', 'mutual_info_score', 'neg_log_loss', 'neg_mean_absolute_error', 'neg_mean_squared_error', 'neg_mean_squared_log_error', 'neg_median_absolute_error', 'normalized_mutual_info_score', 'precision', 'precision_macro', 'precision_micro', 'precision_samples', 'precision_weighted', 'r2', 'recall', 'recall_macro', 'recall_micro', 'recall_samples', 'recall_weighted', 'roc_auc', 'v_measure_score']
    '''

    rfr = RFR(n_estimators=100)
    print(CVS(rfr,Xtrain,Ytrain,cv=5).mean()) # 默认使用R2
    print(CVS(rfr,Xtrain,Ytrain,cv=5,scoring='neg_mean_squared_error').mean())
    '''
    0.8237244585460111
    -13.459232065191156
    '''

    lr = LinearR()
    print(CVS(lr,Xtrain,Ytrain,cv=5).mean())
    print(CVS(lr,Xtrain,Ytrain,cv=5,scoring='neg_mean_squared_error').mean())
    '''
    0.7085045269202359
    -22.00708992916433
    '''

    reg = XGBR(n_estimators=100,silent=False) # 数据巨大预料到算法运行会非常缓慢的时候可以使用silent来监控模型的训练进度
    print(CVS(reg,Xtrain,Ytrain,cv=5,scoring='neg_mean_squared_error').mean())
    '''
    -11.092755292508123
    '''

有一个地方需要商议一下,在交叉验证的时候改用训练集还是所有数据呢?正确做法应该是用训练集进行交叉验证,因为使用所有数据就存在泄露数据的嫌疑,这样是不严谨的做法。然而,如果用只用训练集可能无法提高模型的泛化能力,同时用所有数据也无法保证模型的泛化能力。实际上,导入全数据还是训练集并没有太大的改变,尤其是样本数量较少而模型较复杂的情况。

调参

首先观察过拟合情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
data=load_boston()
Xtrain,Xtest,Ytrain,Ytest = TTS(data.data,data.target,test_size=0.3,random_state=10)

# 自定义绘图函数
def plot_learning_curve(estimator, title, X, y,
ax=None, # 选择子图
ylim=None, # 设置纵坐标的取值范围
cv=None, # 交叉验证
n_jobs=None # 设定所要使用的线程
):
from sklearn.model_selection import learning_curve
train_sizes, train_scores, test_scores = learning_curve(estimator, X, y
, shuffle=True
, cv=cv
,random_state=420
, n_jobs=n_jobs)
if ax == None:
ax = plt.gca()
else:
ax = plt.figure()
ax.set_title(title)
if ylim is not None:
ax.set_ylim(*ylim)
ax.set_xlabel("Training examples")
ax.set_ylabel("Score")
ax.grid() # 绘制网格,不是必须
ax.plot(train_sizes, np.mean(train_scores, axis=1), 'o-', color="r", label="Training score")
ax.plot(train_sizes, np.mean(test_scores, axis=1), 'o-', color="g", label="Test score")
ax.legend(loc="best")
return ax

cv = KFold(n_splits=5, shuffle = True, random_state=10) # Split dataset into k consecutive folds (without shuffling by default).Each fold is then used once as a validation while the k - 1 remaining folds form the training set
plot_learning_curve(XGBR(n_estimators=100,random_state=10,silent=True),"XGB",Xtrain,Ytrain,ax=None,cv=cv)
plt.show()

过拟合
可以看出模型在训练集上表现很好,但在测试集上还是存在不足,这种就行典型的过拟合情况,我们希望训练集和测试集的表现趋近一致,这种情况是存在调参空间的。
关于learning_curve:python基础之learning_curve(学习曲线)

我们可以根据参数n_estimators来绘制学习曲线,但效果可能和预期的有所差别,比如可能出现的一种情况是学习曲线上得分最高的点和比较适合的点相差不大,但参数却千差万别,我们可能不需要为了这0.0000000001的提高而耗费巨大的计算资源。

进化的学习曲线:方差与泛化误差

首先来回顾一下什么是泛化误差:衡量模型在未知数据上的准确率的指标
由偏差(bais),方差(var)和噪声(ε)共同决定。其中偏差就是训练集上的拟合程度决定,方差是模型的稳定性决定,噪音是不可控的
泛化误差

在过去我们往往直接取学习曲线获得的分数的最高点,即考虑偏差最小的点,是因为模型极度不稳定,方差很大的情
况其实比较少见,但有的时候也可以使用方差来确定参数,即将方差考虑进去使用泛化误差曲线找到最小的点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
cv = KFold(n_splits=5, shuffle = True, random_state=10)
axisx = range(50,1050,50)
rs = []
var = []
ge = []
for i in axisx:
reg = XGBR(n_estimators=i,random_state=420)
cvresult = CVS(reg,Xtrain,Ytrain,cv=cv)
rs.append(cvresult.mean()) #记录1-偏差
var.append(cvresult.var()) #记录方差
ge.append((1 - cvresult.mean())**2+cvresult.var()) #计算泛化误差的可控部分

print(axisx[rs.index(max(rs))],max(rs),var[rs.index(max(rs))]) #打印R2最高所对应的参数取值,并打印这个参数下的方差
print(axisx[var.index(min(var))],rs[var.index(min(var))],min(var)) #打印方差最低时对应的参数取值,并打印这个参数下的R2
print(axisx[ge.index(min(ge))],rs[ge.index(min(ge))],var[ge.index(min(ge))],min(ge)) #打印泛化误差可控部分的参数取值,并打印这个参数下的R2,方差以及泛化误差的可控部分
plt.figure(figsize=(20,5))
plt.plot(axisx,rs,c="red",label="XGB")
plt.legend()
plt.show()

'''
350 0.8610046069434762 0.0017082134672292432
50 0.833683748803972 0.0014732754886999603
350 0.8610046069434762 0.0017082134672292432 0.02102793275816677
'''

R^2
根据泛化误差,最佳参数也就是树的数量在350棵效果最好。
接下来可以细化学习曲线,将参数调整为(200,400,10)的区间,找到最佳n_estimators。

观察n_estimators参数对模型的影响,我们可以得出以下结论:

  1. XGB中的树的数量决定了模型的学习能力,树的数量越多,模型的学习能力越强
  2. XGB中树的数量很少的时候,对模型的影响较大,当树的数量已经很多的时候,对模型的影响比较小,只能有微弱的变化。当数据本身就处于过拟合的时候,再使用过多的树能达到的效果甚微,反而浪费计算资源。当唯一指标或者准确率给出的n_estimators看起来不太可靠的时候,我们可以改造学习曲线来帮助我们
  3. 树的数量提升对模型的影响有极限,最开始,模型的表现会随着XGB的树的数量一起提升,但到达某个点之后,树的数量越多,模型的效果会逐步下降,这也说明了暴力增加n_estimators不一定有效果

有放回随机抽样:重要参数subsample

对于所有提升集成算法,每构建一个评估器,集成模型的效果都会比之前的好。
树模型是天生过拟合的模型,如果数据量太大,树模型的计算非常缓慢,因此,我们要对我们的原始数据集进行有放回抽样(bootstrap)。有放回的抽样每次只能抽取一个样本,若我们需要总共N个样本,就需要抽取N次。每次抽取一个样本的过程是独立的,这一次被抽到的样本会被放回数据集中,下一次还可能被抽到,因此抽出的数据集中,可能有一些重复的数据。

有放回抽样第一次是完全随机的,而后每一次抽样会加大前一棵树中被预测错误的样本权重,不停地进行迭代重复建模,最终总会成功预测的。

在xgb和sklearn中都使用subsample来控制随机抽样,这个参数都默认为1且不能取到0。这说明我们无法控制模型是否进行随机有放回抽样,只能控制抽样抽出来的样本量大概是多少。

参数含义 xgb.train() xgb.XGBRegressor()
随机抽样时抽取的样本比例,范围(0,1] subsample,默认1 subsample,默认1

采样会减少样本数量,而从学习曲线来看样本数量越少模型的过拟合会越严重,因为对模型来说,数据量越少模型学习越容易,学到的规则也会越具体越不适用于测试样本。所以subsample参数通常是在样本量本身很大的时候来调整和使用。

如果模型本身就处于过拟合状态,还可能会降低模型的效果。

迭代决策树:重要参数eta

在逻辑回归中,我们自定义步长α;来干涉我们的迭代速率,而在XGB中,完整的迭代公式应该写作:
梯度提升
其中η是迭代决策树中时的步长,又叫学习率(learnin rate),&eta越大,迭代的速度越快,算法的极限很快被达到,有可能无法收敛到真正的最佳。但越小,迭代的速度越慢。
在sklearn中,我们使用参数learning_rate来干涉我们的学习速率:

参数含义 xgb.train() xgb.XGBRegressor()
集成中的学习率,又称为步长以控制迭代速率,常用于防止过拟合 eta,默认0.3,取值范围[0,1] learning_rate,默认0.1,取值范围[0,1]

现在来看,我们的梯度提升树可是说是由三个重要的部分组成

  1. 一个能够衡量集成算法效果的,能够被最优化的损失函数Obj
  2. 一个能够实现预测的弱评估器fk(x)
  3. 一种能够让弱评估器集成的手段,包括迭代方法,抽样手段,样本加权等等过程

XGBoost是在梯度提升树的这三个核心要素上运行,它重新定义了损失函数和弱评估器,并且对提升算法的集成手段
进行了改进,实现了运算速度和模型效果的高度平衡