Loading

26-XGBoost学习2

1.XGBoost的基本思想与实现

1.1 XGBoost vs GBDT

极限提升树XGBoost(Extreme Gradient Boosting,XGB,发音/æks-g-boost/)是基于梯度提升树GBDT全面升级的新一代提升算法,也是提升家族中最富盛名、最灵活、最被机器学习竞赛所青睐的算法。不同于我们之前学过的任意单一算法,XGBoost是一个以提升树为核心的算法系统,它覆盖了至少3+建树流程、10+损失函数,可以实现各种类型的梯度提升树,灵活性无与伦比。同时,XGBoost天生被设计成支持巨量数据,因此可以自由接入GPU/分布式/数据库等系统、还创新了众多工程上对传统提升算法进行加速的新方法。可以说,XGBoost是21世纪中Boosting算法的又一个里程碑,它开创了后GBDT时代中Boosting算法的新辉煌。

作为Boosting算法,XGBoost中自然包含Boosting三要素:

  • 损失函数\(L(y,\widehat{y})\):用以衡量模型预测结果与真实结果的差异。
  • 弱评估器\(f(x)\) :(一般为)决策树,不同的boosting算法使用不同的建树过程。
  • 综合集成结果\(H(x)\):即集成算法具体如何输出集成结果

并且,XGBoost也遵循Boosting算法的基本流程进行建模:


依据上一个弱评估器\(f(x)_{k-1}\)的结果,计算损失函数\(L\),并使用\(L\)自适应地影响下一个弱评估器\(f(x)_k\)的构建。集成模型输出的结果,受到整体所有弱评估器\(f(x)_0\) ~ \(f(x)_K\)的影响。

当然,XGBoost真实的流程比上述过程复杂得多。虽然梯度提升树的流程本身已经比较复杂,XGBoost还是在此流程上做出了众多关键的改进。综合来看,这些改进都是基于XGBoost中两种非常关键的思想实现的:

  • 第一,实现精确性与复杂度之间的平衡

树的集成模型是机器学习中最为强大的学习器之一,这一族学习器的特点是精确性好、适用于各种场景,但运行缓慢、且过拟合风险很高,因此从学习单一决策树时起,我们就持续为大家提供丰富的剪枝策略,目的就是为了降低各种树模型的模型复杂度,从而控制住过拟合。树模型的学习能力与过拟合风险之间的平衡,就是预测精确性与模型复杂度之间的平衡,也是经验风险与结构风险之间的平衡,这一平衡对决策树以及树的集成模型来说是永恒的议题。

在过去,我们总是先建立效果优异的模型,再依赖于手动剪枝来调节树模型的复杂度,但在XGBoost中,精确性与复杂度会在训练的每一步被考虑到。主要体现在:

  • 1. XGBoost为损失函数\(L(y,\hat{y})\)加入结构风险项,构成目标函数\(O(y,\hat{y})\)

    在AdaBoost与GBDT当中,我们的目标是找到损失函数\(L(y,\hat{y})\)的最小值,也就是让预测结果与真实结果差异最小,这一流程只关心精确性、不关心复杂度和过拟合情况。为应对这个问题,XGBoost从决策树的预剪枝流程、逻辑回归、岭回归、Lasso等经典算法的抗过拟合流程吸取经验,在损失函数中加入了控制过拟合的结构风险项,并将【\(L(y,\hat{y})\) + 结构风险】定义为目标函数\(O(y,\hat{y})\)

    这一变化让XGBoost在许多方面都与其他Boosting算法不同:例如,XGBoost是向着令目标函数最小化的目标进行训练,而不是令损失函数最小化的方向。再比如,XGBoost会优先利用结构风险中的参数来控制过拟合,而不像其他树的集成模型一样依赖于树结构参数(例如max_depthmin_impurity_decrease(降低的最小不纯度)等)。

  • 2. 使用全新不纯度衡量指标,将复杂度纳入分枝规则
    在之前学过的算法当中,无论Boosting流程如何进化,建立单棵决策树的规则基本都遵循我们曾经学过的CART树流程,在分类树中,我们使用信息增益(information gain)来衡量叶子的质量,在回归树中,我们使用MSE或者弗里德曼MSE来衡量叶子的质量。这一流程有成熟的剪枝机制、预测精度高、能够适应各种场景,但却可能建立复杂度很高的树。
    为实现精确性与复杂度之间的平衡,XGBoost重新设定了分枝指标【结构分数】(原论文中写作Structure Score,也被称为质量分数Quality Score),以及基于结构分数的【结构分数增益】(Gain of structure score),结构分数增益可以逼迫决策树向整体结构更简单的方向生长。
    这一变化让XGBoost使用与传统CART略有区别的建树流程,同时在建树过程中大量使用残差(Residuals)或类残差对象作为中间变量,因此XGBoost的数学过程比其他Boosting算法更复杂。

  • 第二,极大程度地降低模型复杂度、提升模型运行效率,将算法武装成更加适合于大数据的算法

在任意决策树的建树过程中,都需要对每一个特征上所有潜在的分枝节点进行不纯度计算,当数据量巨大时,这一计算将消耗巨量的时间,因此树集成模型的关键缺点之一就是计算缓慢,而这一缺点在实际工业环境当中是相当致命的。为了提升树模型的运算速度、同时又不极大地伤害模型的精确性,XGBoost使用多种优化技巧来实现效率提升:

  • 1. 使用估计贪婪算法、平行学习、分位数草图算法等方法构建了适用于大数据的全新建树流程
  • 2. 使用感知缓存访问技术与核外计算技术,提升算法在硬件上的运算性能
  • 3. 引入Dropout技术,为整体建树流程增加更多随机性、让算法适应更大数据
    不仅在数学方法上有所改进,XGBoost正式拉开了Boosting算法工程优化的序幕。后续更多的Boosting算法,包括LightGBM,CatBoost等也都是在工程方法上做出了大量的优化

除此之外,XGBoost还保留了部分与梯度提升树类似的属性,包括:

  • 弱评估器的输出类型与集成算法输出类型不一致

对于AdaBoost或随机森林算法来说,当集成算法执行的是回归任务时,弱评估器也是回归器,当集成算法执行分类任务时,弱评估器也是分类器。但对于GBDT以及基于GBDT的复杂Boosting算法们而言,无论集成算法整体在执行回归/分类/排序任务,弱评估器一定是回归器。GBDT通过sigmoid或softmax函数输出具体的分类结果,但实际弱评估器一定是回归器,XGBoost也是如此。

  • 拟合负梯度,且当损失函数是0.5倍MSE时,拟合残差

任意Boosting算法都有自适应调整弱评估器的步骤。在GBDT当中,每次用于建立弱评估器的是样本\(X\)以及当下集成输出\(H(x_i)\)与真实标签\(y\)之间的伪残差(也就是负梯度)。当损失函数是\(\frac{1}{2}MSE\)时,负梯度在数学上等同于残差(Residual),因此GBDT是通过拟合残差来影响后续弱评估器结构。XGBoost也是依赖于拟合残差来影响后续弱评估器结构,但是与GBDT一样,这一点需要通过数学来证明。

  • 抽样思想

GBDT借鉴了大量Bagging算法中的抽样思想,XGBoost也继承了这一属性,因此在XGBoost当中,我们也可以对样本和特征进行抽样来增大弱评估器之间的独立性

因为存在这些相似之处,因此我们将会在XGBoost的参数中看到部分熟悉的参数,如果你对梯度提升树足够熟悉,那XGBoost的许多参数对你来说应该并不难懂。需要注意的是,作为2014年才被正式提出的Boosting算法,XGBoost是一个独立于经典算法的算法系统,因此xgboost库是需要单独安装的。

需要注意的是,windows与linux系统下支持单GPU运算,但Mac系统不支持GPU运算。同时,只有Linux系统支持多GPU联合运算,其他系统不支持。如果pip安装失败,则可以删除后参考该页面进行具体安装:https://xgboost.readthedocs.io/en/stable/install.html

1.2 参数建议

1.3 XGBoost回归的sklearnAPI实现

不同于内嵌在sklearn框架中的其他算法,xgboost是独立的算法库,因此它有一套不同于sklearn代码的原生代码。大部分时候我们使用原生代码来运行xgboost,因为这套原生代码是完全为集成学习所设计的,不仅可以无缝使用交叉验证、默认输出指标为RMSE,还能够默认输出训练集上的结果帮我们监控模型。然而对于熟悉sklearn的我们来说,这一套代码略有难度,因此许多人也会倾向于使用xgboost自带的sklearn接口来实现算法。

XGBoost自带sklearn接口(sklearn API),通过这个接口,我们可以使用跟sklearn代码一样的方式来实现xgboost,即可以通过fit和predict等接口来执行训练预测过程,也可以调用属性比如coef_等。在XGBoost的sklearn API中,我们可以看到下面五个类:

说明
XGBRegressor() 实现xgboost回归
XGBClassifier() 实现xgboost分类
XGBRanker() 实现xgboost排序
XGBRFClassifier() 基于xgboost库实现随机森林分类
XGBRFRegressor() 基于xgboost库实现随机森林回归

其中XGBRF的两个类是以XGBoost方式建树、但以bagging方式构建森林的类,通常只有在我们使用普通随机森林效果不佳、但又不希望使用Boosting的时候使用。这种使用XGBoost方式建树的森林在sklearn中已经开始了实验,不过还没有正式上线。

另外两个类就很容易理解了,一个是XGBoost的回归,一个是XGBoost的分类。这两个类的参数高度相似,我们可以以XGBoost回归为例查看:

class xgboost.XGBRegressor(n_estimators, max_depth, learning_rate, verbosity, objective, booster, tree_method, n_jobs, gamma, min_child_weight, max_delta_step, subsample, colsample_bytree, colsample_bylevel, colsample_bynode, reg_alpha, reg_lambda, scale_pos_weight, base_score, random_state, missing, num_parallel_tree, monotone_constraints, interaction_constraints, importance_type, gpu_id, validate_parameters, predictor, enable_categorical, eval_metric, early_stopping_rounds, callbacks,**kwargs)

class xgboost.XGBClassifier(n_estimators, use_label_encoder, max_depth, learning_rate, verbosity, objective, booster, tree_method, n_jobs, gamma, min_child_weight, max_delta_step, subsample, colsample_bytree, colsample_bylevel, colsample_bynode, reg_alpha, reg_lambda, scale_pos_weight, base_score, random_state, missing, num_parallel_tree, monotone_constraints, interaction_constraints, importance_type, gpu_id, validate_parameters, predictor, enable_categorical, **kwargs)

import xgboost
xgboost.XGBClassifier()合并在回归中说,只有objective不一样
xgboost.XGBRegressor(
                     learning_rate=0.1, # 学习率,和n_estimators树个数相对应
                     n_estimators=100,# 梯度提升树的数量,一般不超过300
                     
#---------------------------限制过拟合的主要参数值有一下几个------------------------
					 max_depth=3, # 每棵树的最大深度	
                     gamma=0,#在叶子上进行进一步分区所需的最小损失减少树的节点
                     colsample_bytree=1,# 构建每棵树时的子特征比率
                     colsample_bylevel=1,#构建每层的子特征比率
                     colsample_bynode=1, #构建每个节点能使用的特征比例
                     subsample=1,#训练实例的子样本比率
# 这两个正则项系数虽然能抗过闭合,不过我们通常使用gamma抗过拟合,默认使用L2范式,系数为1
                     reg_alpha=0,#  L1 权重正则化项系数
                     reg_lambda=1,#  L2 权重正则化项系数
                     
                     silent=True,# 是否显示过程
                     objective='reg:squarederror',
                     #objective代表了我们要解决的问题(目标函数)是分类还是回归,或其他问题,以及对应的损失函数。具体可以取的值很多,一般我们只关心在分类和回归的时候使用的参数。
                     #在回归问题objective一般使用reg:squarederror ,即MSE均方误差。二分类问题一般使用binary:logistic, 多分类问题一般使用multi:softmax
                     booster='gbtree',# 弱学习器基于树模型 gbtree、gblinear 或 dart
                     #booster决定了XGBoost使用的弱学习器类型,可以是默认的gbtree, 也就是CART决策树,还可以是线性弱学习器gblinear以及DART。
                     #一般来说,我们使用gbtree就可以了,不需要调参
                     n_jobs=1,
                     min_child_weight=1,# 样本点所以的权重的和若小于1则不分节点
                     max_delta_step=0,# 允许每棵树的权重估计的最大增量步长
                     scale_pos_weight=1,# 平衡正负权重
                     base_score=0.5,
                     random_state=0,
                     verbosity = 0 ,# 0 (silent) - 3 (debug
                     missing=None,# 处理空值,填充什么
                     importance_type='gain'#特征重要性类型
                     #树模型 "gain", "weight", "cover", "total_gain" or"total_cover".
        			 #线性模型只有"weight",归一化系数,未有偏差 
                    )
                    
# eval_metric参数设置模型使用什么评估指标:这个参数在sklearnAPI中要在实例化模型后在fit方法中添加。
from xgboost import XGBRegressor
xgb_sk = XGBRegressor(max_depth=5,random_state=1412).fit(X,y)
#查看特征重要性
xgb_sk.feature_importances_
#调出其中一棵树,不过无法展示出树的细节,只能够调出建树的Booster对象
xgb_sk.get_booster()[2]
#查看一共建立了多少棵树,相当于是n_estimators的取值
xgb_sk.get_num_boosting_rounds()
#获取每一个参数的取值
xgb_sk.get_params()

可以看到,两个类的参数两都很多,其中不乏一些我们非常熟悉的参数,例如n_estimatorslearning_rate, max_depth等。但大部分参数还是需要我们重新学习和认识,这与xgboost复杂的原理有很大的关系,但由于是sklearn API,所以所有这些参数都有相应的默认值。我们可以在不认识参数的情况下调用这个类。以回归类为例我们来看:

from xgboost import XGBRegressor
from sklearn.model_selection import cross_validate, KFold
from sklearn.model_selection import train_test_split

#sklearn普通训练代码三步走:实例化,fit,score

Xtrain,Xtest,Ytrain,Ytest = train_test_split(X,y,test_size=0.3,random_state=1412)

xgb_sk = XGBRegressor(random_state=1412) #实例化模型
xgb_sk.fit(Xtrain,Ytrain)
xgb_sk.score(Xtest,Ytest) #默认指标R2

#sklearn交叉验证三步走:实例化,交叉验证,对结果求平均

xgb_sk = XGBRegressor(random_state=1412) #实例化模型

#定义所需的交叉验证方式
cv = KFold(n_splits=5,shuffle=True,random_state=1412)

result_xgb_sk = cross_validate(xgb_sk,X,y,cv=cv
                               ,scoring="neg_root_mean_squared_error" #负根均方误差
                               ,return_train_score=True
                               ,verbose=True
                               ,n_jobs=-1)

当sklearn API训练完毕之后,我们可以调用sklearn中常见的部分属性对训练后的模型进行查看,例如查看特征重要性的属性feature_importances_,以及查看XGB下每一棵树的get_booster()方法、查看总共有多少棵树的get_num_boosting_rounds()方法、以及查看当前所有参数的方法get_params

查看参数对xgboost来说很有意义,因为XGBRegressor的说明中没有注明默认参数,因此通过查看参数,我们可以了解到xgboost在sklearn API中都设置了怎样的参数,作为未来调参的参考。对于xgboost分类器,我们还可以调用predict_proba这样的方法来输出概率值,除此之外我们一般不会再用到xgboost sklearn API中的其他功能。

1.4 XGBoost回归的原生代码实现

XGBoost的原生代码与我们已经习惯了的sklearn代码有很大的不同。首先,原生代码必须使用XGBoost自定义的数据结构DMatrix,这一数据结构能够保证xgboost算法运行更快,并且能够自然迁移到GPU上运行,类似于列表、数组、Dataframe等结构都不能用于原生代码,因此使用原生代码的第一步就是要更换数据结构。

当设置好数据结构后,我们需要以字典形式设置参数。XGBoost也可以接受像sklearn一样,将所有参数都写在训练所用的类当中,然而由于xgboost的参数列表过长、参数类型过多,直接将所有参数混写在训练模型的类中会显得代码冗长且混乱,因此我们往往会使用字典单独呈现参数。准备好参数列表后,我们将使用xgboost中自带的方法xgb.trainxgb.cv进行训练,训练完毕后,我们可以使用predict方法对结果进行预测。虽然xgboost原生代码库所使用的数据结构是DMatrix,但在预测试输出的数据结构却是普通的数组,因此可以直接使用sklearn中的评估指标,或者python编写的评估指标进行评估。接下来,我们来认识一下xgboost原生代码中最关键的方法:

class xgboost.DMatrix(data, label=None, *, weight=None, base_margin=None, missing=None, silent=False, feature_names=None, feature_types=None, nthread=None, group=None, qid=None, label_lower_bound=None, label_upper_bound=None, feature_weights=None, enable_categorical=False)

function xgboost.train(*params, dtrain, num_boost_round=10, *, evals=None, obj=None, feval=None, maximize=None, early_stopping_rounds=None, evals_result=None, verbose_eval=True, xgb_model=None, callbacks=None, custom_metric=None)

function xgboost.cv(*params, dtrain, num_boost_round=10, nfold=3, stratified=False, folds=None, metrics=(), obj=None, feval=None, maximize=None, early_stopping_rounds=None, fpreproc=None, as_pandas=True, verbose_eval=None, show_stdv=True, seed=0, callbacks=None, shuffle=True, custom_metric=None)

其中,方法xgb.trainxgb.cv的第一个参数params就是我们需要使用字典自定义的参数列表,第二个参数dtrain就是DMatrix结构的训练数据,第三个参数num_boost_round其实就等同于sklearn中的n_estimators,表示总共建立多少棵提升树,也就是提升过程中的迭代次数。

和sklearn中一样,xgboost中的这些参数也都拥有默认值,因此我们可以不填写任何参数就运行xgboost算法。现在,我们来简单看看原生代码是如何实现的:

XGBoost模块的三步走:将数据转换为DMatrix,定义需要输入的参数params,直接调用训练。第一步,先转换数据格式:

data_xgb = xgb.DMatrix(X,y)

如上所示,DMatrix会将特征矩阵与标签打包在同一个对象中,且一次只能转换一组数据。并且,我们无法通过索引或循环查看内部的内容,一旦数据被转换为DMatrix,就难以调用或修改了:

因此,数据预处理需要在转换为DMatrix之前做好。如果我们有划分训练集和测试集,则需要分别将训练集和测试集转换为DMatrix:

#如果有分割训练集和测试集
from sklearn.model_selection import train_test_split
Xtrain,Xtest,Ytrain,Ytest = train_test_split(X,y,test_size=0.3,random_state=1412)

dtrain = xgb.DMatrix(Xtrain,Ytrain)
dtest = xgb.DMatrix(Xtest,Ytest)

params = {"max_depth":5,"seed":1412}
reg = xgb.train(params, data_xgb, num_boost_round=100)

不难发现,XGBoost不需要实例化,xgb.train函数包揽了实例化和训练的功能,一行代码解决所有问题。同时,XGBoost在训练时没有区分回归和分类器,它默认是执行回归算法,因此当我们执行回归任务时,代码是最为简单的。

在这行简单的代码中,比较令人困惑的可能是参数num_boost_round的位置。作为控制树的数量的参数,它的性质应该与控制树深度的max_depth差不多,因此一般来说,我们会倾向于将与模型学习过程相关的参数应该都在params中设置。然而,如果我们将num_boost_round写在params当中,则会出现警告。

这个警告说明,xgboost不推荐将参数num_boost_round写在params里,即便现在这样可以运行,但在之后的版本迭代中也会逐渐舍弃这个功能。这既是说,xgboost将参数分为了两大部分,一部分可以通过params进行设置,另一部分则需要在方法xgb.train或者xgb.cv中进行设置。遗憾的是,xgboost并没有明确对参数分割的条件和理由,但一般来说,除了建树棵树、提前停止这两个关键元素,其他参数基本都被设置在params当中。如果在实际运行过程中,出现了警告或报错,则根据实际情况进行调整。

# 使用交叉验证进行训练
params = {"max_depth":5,"seed":1412}
result = xgb.cv(params,data_xgb,num_boost_round=100
                ,nfold=5 #补充交叉验证中所需的参数,nfold=5表示5折交叉验证
                ,seed=1412 #交叉验证的随机数种子,params中的是管理boosting过程的随机数种子
               )

如上所示,result返回了一个100行,4列的矩阵,格式为DataFrame。

该矩阵行数与迭代次数一致,当我们规定迭代次数为100时,这个矩阵就有100行,如果我们规定的迭代次数为10,这个矩阵就只会有10行。每一行代表了每次迭代后进行交叉验证的结果的均值,例如索引为0的行就表示迭代了一次时(刚建立第一棵树时),进行5折交叉验证的结果,最后一行的结果也就是当前模型迭代完毕后(建好了全部的nun_boost_round棵树时)输出的结果,也是之前我们使用sklearn API时得到过的结果:测试集上5折交叉验证结果28623.22。

每次迭代后xgboost会执行5折交叉验证,并收集交叉验证上的训练集RMSE均值、训练集RMSE的标准差、测试集RMSE的均值、测试集RMSE的标准差,这些数据构成了4列数据。实际上,这个矩阵展示了每次迭代过后,进行5折交叉验证的结果,也展示出了随着迭代次数增多,模型表现变化的趋势,因此输出结果可以被用于绘制图像。

params = {"eta": 0.3,# 相当于learning_rate
           "max_depth": 3,
           "gamma": 0,
           "objective": "reg:squarederror",# 同sklearn,不同问题不同参数
           "colsample_bytree": 1,
           "colsample_bylevel": 1,
           "colsample_bynode": 1,
           "lambda": 1,# L2范数
           "alpha": 0,# L1范数
           "subsample": 1,
           "seed": 100,# random_state
           
           "eval_metric":"mae"#重要
           """ 
           这个参数表示用哪个评估指标:
           rmse: 回归中的均方误差
           mae: 回归中的绝对平均误差
           logloss:二分类对数损失
           mlogloss: 多分类对数损失
           error: 多分类误差,相当于 1-准确率
           auc:多分类中的AUC面积
           """
           }

1.5 objective损失函数

# 调用xgboost.train

# 二分类参数示例
params1 = {"seed":1412, "objective":"binary:logistic"
           ,"eval_metric":"logloss" #二分类交叉熵损失
          }

# 多分类参数示例
params2 = {"seed":1412, "objective":"multi:softmax"
           ,"eval_metric":"mlogloss" #多分类交叉熵损失 #"merror"
           ,"num_class":10}
#对多分类算法来说,除了设置损失函数和评估指标,还需要设置参数`num_class`。
#参数`num_class`用于多分类状况下、具体的标签类别数量,例如,如果是三分类,则需设置{"num_calss":3}。

"""
分类算法与回归算法执行交叉验证的流程基本一致,但需要注意的是,当使用xgb.train时,我们会将评估指标参数eval_matric写在params中,在使用xgb.cv时,我们却需要将评估指标参数写在xgb.cv当中,否则有时候会报出警告。在xgb.cv当中,我们需要将评估指标打包成元组,写在参数metrics内部,如下所示:
"""
params2 = {"seed":1412
           , "objective":"multi:softmax" #无论填写什么损失函数都不影响交叉验证的评估指标
           , "num_class":10}
result = xgb.cv(params2,data_multi,num_boost_round=100
                ,metrics = ("mlogloss") #交叉验证的评估指标由cv中的参数metrics决定
                ,nfold=5 #补充交叉验证中所需的参数,nfold=5表示5折交叉验证
                ,seed=1412 #交叉验证的随机数种子,params中的是管理boosting过程的随机数种子
               )

参数`metrics`支持多个评估指标:但是范围值就有len('metrics')*4列
params3 = {"seed":1412
           , "objective":"multi:softmax" #无论填写什么损失函数都不影响交叉验证的评估指标
           , "num_class":10}
result = xgb.cv(params3,data_multi,num_boost_round=100
                ,metrics = ("mlogloss","merror")
                ,nfold=5 #补充交叉验证中所需的参数,nfold=5表示5折交叉验证
                ,seed=1412 #交叉验证的随机数种子,params中的是管理boosting过程的随机数种子
               )

1.6参数迭代过程

  • num_boost_round&eta

  • base_score

  • max_delta_step

1.7 XGBoost分类实战

"""
xgboost有两大类接口:    
1)XGBoost原生接口,及陈天奇开源的xgboost项目,import xgboost as xgb    
2)scikit-learn api接口,及python的sklearn库     
并且xgboost能够实现 分类和回归两种任务。 
并且对于分类任务,xgboost可以实现二分类和多分类 

“reg:linear” —— 线性回归。  
“reg:logistic”—— 逻辑回归。 
“binary:logistic”—— 二分类的逻辑回归问题,输出为概率。 
“binary:logitraw”—— 二分类的逻辑回归问题,输出的结果为wTx。 
“count:poisson”—— 计数问题的poisson回归,输出结果为poisson分布。在poisson回归中,max_delta_step的缺省值为0.7。(used to safeguard optimization)   
“multi:softmax” –让XGBoost采用softmax目标函数处理多分类问题,同时需要设置参数num_class(类别个数)                   
“multi:softprob” –和softmax一样,但是输出的是ndata * nclass结构的向量,可以将该向量reshape(映射)成ndata行nclass列的矩阵。每一行数据表示被预测样本所属于每个类别的概率。  
“rank:pairwise” –set XGBoost to do ranking task by minimizing the pairwise loss
"""
"""
一些名词术语:

正样本
负样本
accuracy:准确率,
     针对所有样本(正样本和负样本)而言,即所有实际正负样本中,判定正确的样本所占的比例。
     accuracy = (TP + TN)/(TP + TN + FP + FN)

precision:精确率(又称为查准率)
     针对所有判定为正的样本而言,即所有判定为正的样本中,实际为正的样本所占的比例。
     precision = TP/(TP + FP)

recall:召回率(又称为查全率):
     针对所有实际为正的样本而言,即所有实际为正的样本中,判定为正的样本所占的比例。
     recall = TP/(TP + FN)

f1-score: 是分类问题中的一个衡量指标。它是精确率和召回率的调和平均数,取之0-1之间。

f1-score认为召回率和精确率一样重要。
f2-score认为召回率的重要程度是精确率的2倍。
f0.5-score认为召回率的重要程度是精确率的一半。
"""
"""
二分类,特征是连续型的数据
label是0和1
"""
# 使用鸢尾花的数据来说明二分类的问题

import xgboost as xgb
from sklearn import metrics
from sklearn.cross_validation import train_test_split
from sklearn import datasets
iris = datasets.load_iris()

# 特征数据,100行,一共有4个特征
data = iris.data[:100] 
print(data)
print(data.shape) 

# 标签数据
label = iris.target[:100]
print(label)

# 划分训练数据集,测试数据集
train_x, test_x, train_y, test_y = train_test_split(data, label, random_state=0)

# 调用xgb中的DMatrix()函数,把数据格式转换为xgb需要的模式
dtrain = xgb.DMatrix(train_x, label=train_y)
dtest = xgb.DMatrix(test_x)

# 参数准备
params={'booster':'gbtree',  # 弱学习器的类型,默认就是gbtree,及cart决策树
        'objective': 'binary:logistic',   # 目标函数,二分类:逻辑回归,输出的是概率
        'eval_metric': 'auc',
        'max_depth':4,  # 最大深度
        'lambda':10,
        'subsample':0.75,
        'colsample_bytree':0.75,
        'min_child_weight':2,
        'eta': 0.025,  # 步长
        'seed':0,
        'nthread':8,
        'silent':1}


watchlist = [(dtrain,'train')]

# 开始训练模型
# params是传入模型的各个参数,以字典的形式传入
model = xgb.train(params,
                  dtrain,
                  num_boost_round= 100,  # 迭代的次数,及弱学习器的个数
                  evals= watchlist)

# 对测试集合预测
ypred = model.predict( dtest )

# 设置阈值, 输出一些评价指标,选择概率大于0.5的为1,其他为0类
y_pred = (ypred >= 0.5)*1


# 输出各种指标,评价模型的优劣
# 比如auc曲线,准确率,精确率,召回率等
print('AUC: %.4f' % metrics.roc_auc_score(test_y, ypred))
print('ACC: %.4f' % metrics.accuracy_score(test_y, y_pred))        # 准确率
print('Precesion: %.4f' % metrics.precision_score(test_y, y_pred)) # 精确率
print('Recall: %.4f' % metrics.recall_score(test_y, y_pred))       # 召回率
print('F1-score: %.4f' % metrics.f1_score(test_y, y_pred))         # 二者的结合
print(metrics.confusion_matrix(test_y, y_pred))  # 混淆矩阵
# 目的是判断病人是否会在 5 年内患糖尿病,给出的数据为csv文件。
# 一共9列数据,前 8 列是特征,最后一列是标签label,为 0 或 1。

# 导入必要的包
from numpy import loadtxt
from xgboost.sklearn import XGBClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score  # 准确率

# 加载数据,分出特征和标签
dataset = loadtxt('dataset_001.csv', delimiter=",") 
X = dataset[:,0:8]  # 前8列是特征
Y = dataset[:,8] # 最后一列是标签

# 把数据集分为训练集和测试集
# 训练集用来训练模型,测试集用来测试模型的效果
seed = 7
X_train, X_test, y_train, y_test = train_test_split(X, Y,
                                                    test_size=0.33, 
                                                    random_state=seed)

# 直接使用xgboost开源项目中封装好的分类器和回归器,可以直接使用XGBClassifier建立模型
# 不可视化数据集loss
#model = XGBClassifier()
#model.fit(X_train, y_train)

# 可视化测试集的loss
# 改为True就能可视化loss
xgboost_model = XGBClassifier()
eval_set = [(X_test, y_test)]
xgboost_model.fit(X_train, 
                  y_train, 
                  early_stopping_rounds=10, 
                  eval_metric="logloss",  # 损失函数的类型,分类一般都是用对数作为损失函数
                  eval_set=eval_set,
                  verbose=False)

# xgboost的结果是每一个样本属于第一类的概率,要使用round将其转换为0 1值
y_pred = xgboost_model.predict( X_test )
predictions = [round(i) for i in y_pred] 

# 计算准确率,也就是把准确率作为衡量模型好坏的指标
# 当正负类样本分布不均匀的时候(比如之前做的判断一架飞机上的恐怖分子),用准确率是不合适的
accuracy = accuracy_score(y_test, predictions)
print("Accuracy: %.2f%%" % (accuracy * 100.0))
##Accuracy: 77.56%

上述代码可视化每个特征的重要程度:

from xgboost import plot_importance
from matplotlib import pyplot
xgboost_model.fit(X, Y)
plot_importance( xgboost_model )
pyplot.show()

调整参数:

"""
下面是三个超参数的一般实践最佳值,可以先将它们设定为这个范围,然后画出 learning curves,再调解参数找到最佳模型:

learning_rate = 0.1 或更小,越小就需要多加入弱学习器; 
tree_depth = 2~8;
subsample = 训练集的 30%~80%;

接下来我们用 GridSearchCV 来进行调参会更方便一些: 可以调的超参数组合有:
树的个数和大小 (n_estimators and max_depth).
学习率和树的个数 (learning_rate and n_estimators).
行列的 subsampling rates (subsample, colsample_bytree and colsample_bylevel).

下面以学习率 learning rate 为例
"""
# 通过遍历的方式,在不同的学习率的情况下,看哪个学习率的情况下,模型最优
from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import StratifiedKFold

# 设定要调节的 learning_rate = [0.0001, 0.001, 0.01, 0.1, 0.2, 0.3] 

# 和原代码相比就是在 model 后面加上 grid search 这几行:
model = XGBClassifier()
learning_rate = [0.0001, 0.001, 0.01, 0.1, 0.2, 0.3]
param_grid = dict(learning_rate=learning_rate)
kfold = StratifiedKFold(n_splits=10, shuffle=True, random_state=7)
grid_search = GridSearchCV(model, param_grid, scoring="neg_log_loss", n_jobs=-1, cv=kfold)
grid_result = grid_search.fit(X, Y)

# 输出最佳学习率和其对应的分数
print("Best: %f using %s" % (grid_result.best_score_, grid_result.best_params_))
#Best: -0.483013 using {'learning_rate': 0.1}

# 打印出每一个学习率对应的分数
means = grid_result.cv_results_['mean_test_score']
stds = grid_result.cv_results_['std_test_score']
params = grid_result.cv_results_['params']
for mean, stdev, param in zip(means, stds, params):
    print("%f (%f) with: %r" % (mean, stdev, param))

# 结果如下:
-0.689650 (0.000242) with: {‘learning_rate’: 0.0001}
-0.661274 (0.001954) with: {‘learning_rate’: 0.001}
-0.530747 (0.022961) with: {‘learning_rate’: 0.01}
-0.483013 (0.060755) with: {‘learning_rate’: 0.1}
-0.515440 (0.068974) with: {‘learning_rate’: 0.2}
-0.557315 (0.081738) with: {‘learning_rate’: 0.3}

2.XGBoost的目标函数

与GBDT一样,xgboost的损失函数理论上可以推广到任意可微函数,因此只要是集成类算法的损失函数都可以被用于xgboost。但与GBDT不同的是,xgboost并不向着损失函数最小化的方向运行,而是向着令目标函数最小化的方向运行

需要注意的是,损失函数可以针对单个样本进行计算,也可以针对整个算法进行计算,但在XGBoost的定义中,目标函数是针对每一棵树的,而不是针对一个样本或整个算法。对任意树\(f_k\)来说,目标函数有两个组成部分,一部分是任意可微的损失函数,它控制模型的经验风险。从数值上来说,它等于现在树上所有样本上损失函数之和,其中单一样本的损失为\(l(y_i,\hat{y_i})\)。另一部分是控制模型复杂度的\(\Omega(f_k)\),它控制当前树的结构风险

\(Obj_k = \sum_{i=1}^Ml(y_i,\hat{y_i}) + \Omega(f_k)\)

其中\(M\)表示现在这棵树上一共使用了M个样本,\(l\)表示单一样本的损失函数。当模型迭代完毕之后,最后一棵树上的目标函数就是整个XGBoost算法的目标函数。

  • 经验风险:模型对数据学习越深入,损失越小(经验风险越小),模型对数据学习得越浅显,损失越大(经验风险越大)。

  • 结构风险:树结构越复杂、模型复杂度越高,过拟合风险越大(结构风险越大)。树模型结构越简单、模型复杂度越低、过拟合风险越小(结构风险越小)。

通常来说,模型需要达到一定的复杂度,才能保证较小的损失,但如果只追求最小的经验风险,反而容易导致过拟合。相对的,如果只追求模型复杂度低、结构风险低,那模型又容易陷入欠拟合的困局、损失函数过高,因此平衡结构风险与经验风险十分关键。XGBoost向着目标函数最小化的方向运行,可以保证在迭代过程中,经验风险和结构风险都不会变得太大,因此模型的损失不会太大、同时又不会太容易过拟合。这一目标函数的设计可谓相当聪明。

在具体的公式当中,结构风险\(\Omega(f_k)\)又由两部分组成,一部分是控制树结构的\(\gamma T\),另一部分则是正则项:

\(\Omega(f_k) = \boldsymbol{\color{red}\gamma} T + \frac{1}{2}\boldsymbol{\color{red}\lambda}\sum_{j=1}^Tw_j^2 + \boldsymbol{\color{red}\alpha}\sum_{j=1}^Tw_j\)

其中\(\gamma\)\(\lambda\)\(\alpha\)都是可以自由设置的系数,而\(T\)表示当前第\(k\)棵树上的叶子总量,\(w_j\)则代表当前树上第\(j\)片叶子的叶子权重(leaf weights)。叶子权重是XGBoost数学体系中非常关键的一个因子,它实际上就是当前叶子\(j\)的预测值,这一指标与数据的标签量纲有较大的关系,因此当标签的绝对值较大、\(w_j\)值也会倾向于越大。因此正则项有两个:使用平方的L2正则项与使用绝对值的L1正则项,因此完整的目标函数表达式为:

\[Obj_k = \sum_{i=1}^Ml(y_i,\hat{y_i}) + \boldsymbol{\color{red}\gamma} T + \frac{1}{2}\boldsymbol{\color{red}\lambda}\sum_{j=1}^Tw_j^2 + \boldsymbol{\color{red}\alpha}\sum_{j=1}^Tw_j \]

不难发现,所有可以自由设置的系数都与结构风险有关,这三个系数也正对应着xgboost中的三个参数:gammaalphalambda

  • 参数gamma:乘在一棵树的叶子总量\(T\)之前,依照叶子总量对目标函数施加惩罚的系数,默认值为0,可填写任何[0, ∞]之间的数字。当叶子总量固定时,gamma越大,结构风险项越大;同时,当gamma不变时,叶子总量越多、模型复杂度越大,结构风险项也会越大。在以上两种情况下,目标函数受到的惩罚都会越大,因此调大gamma可以控制过拟合
  • 参数alphalambda:乘在正则项之前,依照叶子权重的大小对目标函数施加惩罚的系数,也就是正则项系数。lambda的默认值为1,alpha的默认值为0,因此xgboost默认使用L2正则化。通常来说,我们不会同时使用两个正则化,但我们也可以尝试这么做。\(\sum_{j=1}^Tw_j\)是当前树上所有叶子的输出值之和,因此当树上的叶子越多、模型复杂度越大时,\(\sum_{j=1}^Tw_j\)自然的数值自然会更大,因此当正则项系数固定时,模型复杂度越高,对整体目标函数的惩罚就越重。当\(w\)固定时,正则项系数越大,整体目标函数越大,因此调大alphalambda可以控制过拟合
参数含义 原生代码 sklearn API
乘在叶子节点数量前的系数 gamma(params) gamma
L2正则项系数 lambda(params) reg_lambda
L1正则项系数 alpha(params) reg_alpha

然而,在实际控制过拟合的过程中,大家可能经常会发现这几个参数“无效”。

实际上,对于没有上限或下限的参数,我们要关注参数的敏感度。如果参数值稍稍移动,模型就变化很大,那参数敏感,如果参数值移动很多,模型才能有变化,那参数不敏感。当树的结构相对复杂时,gamma会比敏感,否则gamma可能非常迟钝。当原始标签数值很大、且叶子数量不多时,lambdaalpha就会敏感,如果原始标签数值很小,这两个参数就不敏感。因此在使用这些参数之前,最好先对参数的敏感程度有一个探索,这里很容易看到:

  • 当前树结构不复杂,gamma不敏感
  • 当前标签值较大,因此预测标签的值也较大,lambda会更敏感

可见,现在lambdagamma有效。当然,在实际使用时,并不是在所有数据情况下都如此,需要根据具体情况具体分析。因此在使用和调节这些参数时,要先考虑适合的参数范围,否则再多的搜索也是无用。总结一下,在整个迭代过程中,我们涉及到了如下参数:

类型 参数
迭代过程/损失函数 num_boost_round:弱分类器数量,对Boosting算法而言为实际迭代次数
eta:Boosting算法中的学习率,影响弱分类器结果的加权求和过程
objective:选择需要优化的损失函数
base_score:初始化预测结果\(H_0\)的设置
max_delta_step:一次迭代中所允许的最大迭代值
gamma:乘在叶子数量前的系数,放大可控制过拟合
lambda:L2正则项系数,放大可控制过拟合
alpha:L1正则项系数,放大可控制过拟合

3.XGBoost的弱评估器

3.1 DART树

梯度提升算法当中不只有梯度提升树,也可集成其他模型作为弱评估器,而作为梯度提升树进化版的XGBoost算法,自然也不是只有CART树一种弱评估器。在XGBoost当中,我们还可以选型线性模型,比如线性回归或逻辑回归来集成,同时还可以选择与CART树有区别的另一种树:DART树。在XGBoost当中,我们使用参数booster来控制我们所使用的具体弱评估器。

  • 参数booster:使用哪种弱评估器

可以输入"gbtree"、“gblinear"或者"dart”。

输入"gbtree"表示使用遵循XGBoost规则的CART树,我们之前提到的XGBoost在GBDT上做出的改善基本都是针对这一类型的树。这一类型的树又被称为“XGBoost独有树”,XGBoost Unique Tree。
输入"dart"表示使用抛弃提升树,DART是Dropout Multiple Additive Regression Tree的简称。这种建树方式受深度学习中的Dropout技巧启发,在建树过程中会随机抛弃一些树的结果,可以更好地防止过拟合。在数据量巨大、过拟合容易产生时,DART树经常被使用,但由于会随机地抛弃到部分树,可能会伤害模型的学习能力,同时可能会需要更长的迭代时间。
输入"gblinear"则表示使用线性模型,当弱评估器类型是"gblinear"而损失函数是MSE时,表示使用xgboost方法来集成线性回归。当弱评估器类型是"gblinear"而损失函数是交叉熵损失时,则代表使用xgboost来集成逻辑回归。
每一种弱评估器都有自己的params列表,例如只有树模型才会有学习率等参数,只有DART树才会有抛弃率等参数。评估器必须与params中的参数相匹配,否则一定会报错。其中,由于DART树是从gbtree的基础上衍生而来,因此gbtree的所有参数DART树都可以使用。

参数含义 原生代码 sklearn API
选择使用不同的弱评估器 booster(params) booster
  • 关于DART抛弃树的简要说明

关于随机抛弃的过程中,我们涉及到以下的几个参数:

  • 参数rate_drop:每一轮迭代时抛弃树的比例:

    设置为0.3,则表示有30%的树会被抛弃。只有当参数booster="dart"时能够使用,只能填写[0.0,1.0]之间的浮点数,默认值为0。

  • 参数one_drop:每一轮迭代时至少有one_drop棵树会被抛弃

    可以设置为任意正整数,例如one_drop = 10,则意味着每轮迭代中至少有10棵树会被抛弃。

    当参数one_drop的值高于rate_drop中计算的结果时,则按照one_drop中的设置执行Dropout。例如,总共有30棵树,rate_drop设置为0.3,则需要抛弃9棵树。但one_drop中设置为10,则一定会抛弃10棵树。当one_drop的值低于rate_drop的计算结果时,则按rate_drop的计算结果执行Dropout。

  • 参数skip_drop:每一轮迭代时可以不执行dropout的概率

    即便参数booster=‘dart’,每轮迭代也有skip_drop的概率可以不执行Dropout,是所有设置的概率值中拥有最高权限的参数。该参数只能填写[0.0,1.0]之间的浮点数,默认值为0。当该参数为0时,则表示每一轮迭代都一定会抛弃树。如果该参数不为0,则有可能不执行Dropout,直接按照普通提升树的规则建立新的提升树。

    需要注意的是,skip_drop的权限高于one_drop。即便one_drop中有所设置,例如每次迭代必须抛弃至少10棵树,但只要skip_drop不为0,每轮迭代则必须经过skip_drop的概率筛选。如果skip_drop说本次迭代不执行Dropout,则忽略one_drop中的设置。

  • 参数sample_type:抛弃时所使用的抽样方法

    填写字符串"uniform":表示均匀不放回抽样。

    填写字符串"weighted":表示按照每棵树的权重进行有权重的不放回抽样。

    注意,该不放回是指在一次迭代中不放回。每一次迭代中的抛弃是相互独立的,因此每一次抛弃都是从所有树中进行抛弃。上一轮迭代中被抛弃的树在下一轮迭代中可能被包括。

  • 参数normalize_type:增加新树时,赋予新树的权重(树模型抽中的概率

    当随机抛弃已经建好的树时,可能会让模型结果大幅度偏移,因此往往需要给与后续的树更大的权重,让新增的、后续的树在整体算法中变得更加重要。所以DART树在建立新树时,会有意地给与后续的树更大的权重。我们有两种选择:

    填写字符串"tree",表示新生成的树的权重等于所有被抛弃的树的权重的均值。

    填写字符串"forest",表示新生成的树的权重等于所有被抛弃的树的权重之和。

    算法默认为"tree",当我们的dropout比例较大,且我们相信希望给与后续树更大的权重时,会选择"forest"模式。

你是否注意到,我们的两个参数sample_type与normalize_type都使用了概念“树的权重”,但我们在之前讲解XGBoost的基本流程时提到过,XGBoost并不会针对每一棵树计算特定的权重。这个树的权重其实指的是整棵树上所有叶子权重之和。那究竟是怎样让新增加的树的权重刚好就等于原本被抛弃的树的权重的均值或和呢?这就需要一个相对复杂的数学过程来进行解答了,如果你感兴趣,可以查看这一篇说明:https://xgboost.readthedocs.io/en/stable/tutorials/dart.html

当我们在应用的时候,这个点并不会对我们造成影响,只要知道参数如何使用即可。同时,所有dart树相关的参数在原生代码与sklearn代码中都完全一致。
关于DART抛弃树带来的缺陷:

当模型容易过拟合时,我们可以尝试让模型使用DART树来减轻过拟合。不过DART树也会带来相应的问题,最明显的缺点就是:

  • 用于微调模型的一些树可能被抛弃,微调可能失效
  • 由于存在随机性,模型可能变得不稳定,因此提前停止等功能可能也会变得不稳定
  • 由于要随机抛弃一些树的结果,在工程上来说就无法使用每一轮之前计算出的\(H_{k-1}\),而必须重新对选中的树结果进行加权求和,可能导致模型迭代变得略微缓慢
  • dart树带来的抗过拟合效果比gammalambda等参数更强,不过在提升模型的测试集表现上,dart树还是略逊一筹,毕竟dart树会伤害模型的学习能力。

3.2弱评估器的分枝

当参数booster的值被设置为gbtree时,XGBoost所使用的弱评估器是改进后的的CART树,其分枝过程与普通CART树高度一致:向着叶子质量提升/不纯度下降的方向分枝、并且每一层都是二叉树。在CART树的基础上,XGBoost创新了全新的分枝指标:结构分数(Structure Score)与结构分数增益(Gain of Structure Score)(也被叫做结构分数之差),更大程度地保证了CART树向减小目标函数的方向增长。需要注意的是,XGBoost不接受其他指标作为分枝指标,因此你会发现在众多的xgboost的参数中,并不存在criterion参数:

类型 参数
迭代过程/目标函数 params: eta, base_score, objective, lambda, gamma, alpha, max_delta_step
xgb.train(): num_boost_round
弱评估器结构 params: max_depth, booster, min_child_weight
dart树 params: sample_type, normalized_type, rate_drop, one_drop, skip_drop
弱评估器的训练数据 params: subsample, sampling_method, colsamle_bytree, colsample_bylevel, colsample_bynode
提前停止 xgb.train(): early_stopping_rounds, evals, eval_metric
其他 params: seed, verbosity, scale_pos_weight, nthread

幸运的是,没有任何参数与结构分数的公式本身相关,因此从应用xgboost的角度来看,我们并不需要对结构分数以及相应的分枝过程理解太深,只需对公式稍作了解即可。不过,结构分数是XGBoost整个运行流程中非常核心的概念,它即精又巧,串起了整个XGBoost几乎所有的数学流程。在原始论文中,作者陈天奇使用了一整节的篇幅来推导结构分数的公式,因此这部分原理非常值得学习。如果你渴望学习相关原理,可以查看数学的最后一节。现在,我们来了解结构分数的相关公式:

假设现在目标函数使用L2正则化,控制叶子数量的参数gamma为0。现在存在一个叶子节点\(j\),对该节点来说结构分数的公式为:

\[Score_j = \frac{(\sum_{i \in j}g_i)^2}{\sum_{i \in j}h_i + \lambda} \]

其中,\(g_i\)是样本\(i\)在损失函数\(L\)上对预测标签求的一阶导数,\(h_i\)是样本\(i\)在损失函数\(L\)上对预测标签求的二阶导数,\(i \in j\)表示对叶子\(j\)上的所有样本进行计算,\(\lambda\)就是L2正则化的正则化系数。所以不难发现,结构分数实际上就是:

\[Score_j = \frac{节点j上所有样本的一阶导数之和的平方}{节点j上所有样本的二阶导数之和 + \lambda} \]

需要注意结构分数是针对节点计算的,我们以前学习的不纯度衡量指标如基尼系数、信息熵等也是如此。在此基础上,我们依赖于结构分数增益进行分枝,结构分数增益表现为:

\[\begin{align} Gain &= Score_L + Score_R - Score_P \\ \\ &= \frac{(\sum_{i \in L}g_i)^2}{\sum_{i \in L}h_i + \lambda} + \frac{(\sum_{i \in R}g_i)^2}{\sum_{i \in R}h_i + \lambda} - \frac{(\sum_{i \in P}g_i)^2}{\sum_{i \in P}h_i + \lambda}\\ \\ &(见原论文7号公式) \end{align}\]

这即是说,结构分数增益实际上就是:

\[Gain = 左节点的结构分数 + 右节点的结构分数 - 父节点的结构分数 \]

我们选择增益\(Gain\)最大的点进行分枝

你是否注意到,XGBoost中的分枝规则与经典CART树的分枝规则在细节上有所不同?CART树中所使用的信息增益是:

\[CART树中的信息增益 = 父节点的不纯度 - (左节点的不纯度 + 右节点的不纯度) \]

我们追求的是最大的信息增益,这意味着随着CART树的建立整体不纯度是在逐渐降低的。无论不纯度衡量指标是基尼系数还是信息熵,不纯度是越小越好。然而在XGBoost当中,增益的计算公式与CART树相反,但我们依然追求最大增益,所以这意味着随着XGBoost树的建立,整体结构分数是逐渐上升的。因此我们可以认为结构分数越大越好

那结构分数的含义是什么呢?它也像信息熵一样,可以衡量叶子节点的某种属性吗?为什么结构分数需要越大越好呢?这些问题需要大家了解数学推导过程后才能解答,但我们在这里可以举一个很简单的例子来证实结构分数增益越大、选出的分枝越好。

假设现在我们有一个超简单的节点需要分割,该节点中所包含的样本如下:

样本 y y_hat
1 1 0.5
2 -2 0.5
3 -2 0.5

众所周知,在决策树中一个节点只能有一个输出值,因此同一片叶子上所有样本的预测值都一致,不同的树模型使用不同的方法来计算叶子节点上的输出值,大部分模型都直接使用样本的真实值的均值作为输出,但XGBoost有自己不同的手段。现在我们可以暂时忽略这一点,先假设当前的节点预测值为0.5。

现在要对该节点进行分割,你知道从哪里分枝会最有效吗?因为一片叶子只会输出一个预测值,所以相同标签的样本最好在一片叶子上。因此很明显,因为2、3号叶子的真实值一致,我们应该将该节点从1号样本和2号样本中间分开,让1号样本单独在一片叶子上,而2、3号样本在一片叶子上(1,23)。但实际在进行分枝时,我们需要尝试所有可能的方式,并分别计算以下方式的结构分数增益:

  • 分割方案1:(1,23)
左子节点 y y_hat 右子节点 y y_hat
1 1 0.5 2 -2 0.5
3 -2 0.5
  • 分割方案2:(12,3)
左子节点 y y_hat 右子节点 y y_hat
1 1 0.5 3 -2 0.5
2 -2 0.5

假设现在执行的是XGBoost回归,损失函数为0.5倍MSE,公式为\(\frac{1}{2}(y - \hat{y})^2\),假设lambda=1。那基于MSE的一阶导数为:

\[\begin{align} l&= \frac{1}{2}(y_i - \hat{y_i})^2 \\ \\ l' &= \frac{\partial}{\partial \hat{y_i}} \frac{1}{2}(y_i - \hat{y_i})^2\\ \\ &= - (y_i - \hat{y_i})\\ \\ &= \hat{y_i} - y_i\\ \\ \end{align}\]

基于MSE的二阶导数为:

\[\begin{align} l'' &= \frac{\partial}{\partial \hat{y_i}} (\hat{y_i} - y_i)\\ \\ &= 1 \end{align}\]

因此无论如何划分,\(g_i = \hat{y_i} - y_i\)\(h_i = 1\)。现在来计算父节点和两个子节点上每个样本的\(g_i\)\(h_i\)

  • 父节点:
样本 y y_hat gi hi
1 1 0.5 -0.5 1
2 -2 0.5 2.5 1
3 -2 0.5 2.5 1

因此父节点的结构分数为:

\[\begin{align} Score_P &= \frac{(\sum_{i \in P}g_i)^2}{\sum_{i \in P}h_i + \lambda} \\ \\ &= \frac{(-0.5 + 2.5 + 2.5)^2}{3 + 1} \\ \\ &= 5.0625 \end{align}\]

  • 方案1
左子节点 y y_hat gi hi 右子节点 y y_hat gi hi
1 1 0.5 -0.5 1 2 -2 0.5 2.5 1
3 -2 0.5 2.5 1

方案1下两个子节点的结构分数为:

\[\begin{align} Score_{L1} &= \frac{(\sum_{i \in {L1}}g_i)^2}{\sum_{i \in {L1}}h_i + \lambda} \\ \\ &= \frac{(-0.5)^2}{1 + 1} \\ \\ &= 0.125 \end{align}\]


\[\begin{align} Score_{R1} &= \frac{(\sum_{i \in {R1}}g_i)^2}{\sum_{i \in {R1}}h_i + \lambda} \\ \\ &= \frac{(2.5+2.5)^2}{2 + 1} \\ \\ &= 8.333 \end{align}\]

因此增益等于:

\[\begin{align} Gain &= Score_{L1} + Score_{R1} - Score_P \\ \\ &= 0.125 + 8.333 - 5.6025 \\ \\ &= 3.395 \end{align}\]

  • 方案2
左子节点 y y_hat gi hi 右子节点 y y_hat gi hi
1 1 0.5 -0.5 1 3 -2 0.5 2.5 1
2 -2 0.5 2.5 1

方案1下两个子节点的结构分数为:

\[\begin{align} Score_{L1} &= \frac{(\sum_{i \in {L1}}g_i)^2}{\sum_{i \in {L1}}h_i + \lambda} \\ \\ &= \frac{(-0.5 + 2.5)^2}{2 + 1} \\ \\ &= 1.333 \end{align}\]


\[\begin{align} Score_{R1} &= \frac{(\sum_{i \in {R1}}g_i)^2}{\sum_{i \in {R1}}h_i + \lambda} \\ \\ &= \frac{(2.5)^2}{1 + 1} \\ \\ &= 3.125 \end{align}\]

因此增益等于:

\[\begin{align} Gain &= Score_{L1} + Score_{R1} - Score_P \\ \\ &= 1.333 + 3.125 - 5.0625 \\ \\ &= -0.604 \end{align}\]

方案 左侧结构分数 右侧结构分数 父节点结构分数 增益
(1,23) 0.125 8.333 5.0625 3.3958
(12,3) 1.333 3.125 5.0625 -0.6041

很明显,方案1(1,23)的增益为3.395,远远大于方案2(12,3)的增益-0.604,因此根据结构分数增益的指示,我们应该使用第一种分割方式,这与我们经验判断的一致。在XGBoost建树过程中,我们需要对每一个节点进行如上计算,不断来选出令增益更大的分枝。

  • 结构分数与信息熵的关键区别

不知道你是否注意到一个问题。在之前我们提到过,结构分数是越大越好。在方案1当中,左侧叶子节点上的结构分数为0.125,右侧叶子节点上的结构分数为8.333,这是否意味着左侧叶子比右侧叶子更好呢?答案是否定的。与信息熵、基尼系数等可以评价单一节点的指标不同,结构分数只能够评估结构本身的优劣,不能评估节点的优劣

比如说,方案1中的树结构有更高的分数之和,方案2中的树结构的分数之和较低,所以方案1更好。但我们不能说,方案1中的左节点分数低,右节点分数高,所以右节点比左节点更好。因此,在XGBoost原始论文当中,我们利用一棵树上所有叶子的结构分数之和来评估整棵树的结构的优劣,分数越高则说明树结构质量越高,因此在原论文中,结构分数也被称为质量分数(quality score)。

3.3控制复杂度(一):弱评估器的剪枝

3.4控制复杂度(二):弱评估器的训练数据

3.5 XGBoost的其他参数与方法

目前为止,我们已经将与XGBoost的训练、建树相关的参数全部讲解完毕了,剩余的参数是一些功能性的参数,如果你已经熟悉课程中其他算法,那这些参数对你来说应该非常容易,包括:

  • 提前停止

    参数early_stopping_rounds:位于xgb.train方法当中。如果规定的评估指标不能连续early_stopping_rounds次迭代提升,那就触发提前停止。

  • 模型监控与评估

    参数evals:位于xgb.train方法当中,用于规定训练当中所使用的评估指标,一般都与损失函数保持一致,也可选择与损失函数不同的指标。该指标也用于提前停止。

    参数verbosity:用于打印训练流程和训练结果的参数。在最早的版本中该参数为silent,后来经过更新变成了今天的verbosity。然而,经过改进之后的verbosity更倾向于帮助我们打印建树相关的信息,而不像原来的silent一样帮助我们展示训练过程中的模型评估信息,因此verbosity现在不那么实用了我们可以在verbosity中设置数字[0,1,2,3],参数默认值为1。

    • 0:不打印任何内容
    • 1:表示如果有警告,请打印警告
    • 2:请打印建树的全部信息
    • 3:我正在debug,请帮我打印更多的信息
  • 样本不均衡

    参数scale_pos_weight:调节样本不均衡问题,类似于sklearn中的class_weight,仅在算法执行分类任务时有效。参数scale_pos_weight的值时负样本比正样本的比例,默认为1,因此XGBoost时默认调节样本不均衡的。同时,如果你需要手动设置这个参数,可以输入(负样本总量)/(正样本总量)这样的值。

    # 在一些特定情况下,我们更加倾向于在保证准确率的情况下看中召回率,这就是scale_pos_weight参数的作用,而不是一味的追求准确率
    from sklearn.datasets import make_blobs
    from sklearn.metrics import confusion_matrix as cm, recall_score as recall, roc_auc_score as auc
    import xgboost as xgb
    class_1 = 500
    class_2 = 50
    centers = [[0.0, 0.0], [2.0, 2.0]]
    clusters_std = [1.5, 0.5]
    [X, y] = make_blobs(n_samples=[class_1, class_2],
                        n_features=2,
                        centers=centers,
                        cluster_std=clusters_std,
                        shuffle=True,
                        random_state=0,
                        return_centers=False, )
    X_train, X_test, y_train, y_test = TTS(X, y, test_size=0.3, random_state=0)
    
    # 使用scale_pos_weight默认值1
    sklearn_model = xgb.XGBClassifier(random_state=1, scale_pos_weight=1)
    sklearn_model.fit(X_train, y_train, eval_metric="logloss")
    y_pre = sklearn_model.predict(X_test)
    
    # 混淆矩阵
    cm(y_test,y_pre,labels=[1,0])
    # array([[ 15,   3],
    #        [  5, 142]], dtype=int64)
    
    # 准确率
    sklearn_model.score(X_test, y_test)
    # 0.9515151515151515
    
    # 召回率
    recall(y_test,y_pre)
    # 0.8333333333333334
    
    # AUC
    auc(y_test,sklearn_model.predict_proba(X_test)[:,1])
    # 0.9792139077853362
    
    #****************************scale_pos_weight变成10,因为class_1 : class_2 = 500 : 50
    
    sklearn_model_scale = xgb.XGBClassifier( random_state=1, scale_pos_weight=10)
    sklearn_model_scale.fit(X_train, y_train, eval_metric="logloss")
    y_pre_scale = sklearn_model_scale.predict(X_test)
    cm(y_test,y_pre_scale,labels=[1,0])
    #array([[ 16,   2],
    #       [  4, 143]], dtype=int64)
    
    sklearn_model_scale.score(X_test, y_test)
    #0.9636363636363636
    
    recall(y_test,y_pre_scale)
    #0.8888888888888888
    
    auc(y_test,sklearn_model_scale.predict_proba(X_test)[:,1])
    #0.9792139077853362
    
    # ************************************不同scale_pos_weight下准确率,召回率,AUC曲线
    rang=[1,5,10,20,30]
    acu_list=[]
    recall_list=[]
    auc_list=[]
    for i in rang:
        clf=xgb.XGBClassifier(scale_pos_weight=i).fit(X_train,y_train)
        y_pr=clf.predict(X_test)
        acu_list.append(clf.score(X_test,y_test))
        recall_list.append(recall(y_test,y_pr))
        auc_list.append(auc(y_test,clf.predict_proba(X_test)[:,1]))
    
    ax = plt.figure(figsize=(15, 8)).add_subplot(111)
    ax.plot(rang,acu_list,label="Accuracy")
    ax.plot(rang,recall_list,label="Recall")
    ax.plot(rang,auc_list,label="AUC")
    plt.legend(fontsize = "xx-large")
    plt.show()
    
  • 并行的线程

    参数nthread:允许并行的最大线程数,类似于sklearn中的n_jobs,默认为最大,因此xgboost在默认运行时就会占用大量资源。如果数据量较大、模型体量较大,可以设置比最大线程略小的线程,为其他程序运行留出空间。

3.6超参数优化

比起其他树的集成算法,XGBoost有大量通过影响建树过程而影响整体模型的参数(比如gamma,lambda等)。这些参数以较为复杂的方式共同作用、影响模型的最终结果,因此他们的影响力不是线性的,也不总是能在调参过程中明显地展露出来,但调节这些参数大多数时候都能对模型有影响,因此大部分与结构风险相关的参数都被评为4星参数了。相对的,对XGBoost来说总是具有巨大影响力的参数就只有迭代次数与学习率了。

在上述影响力排名当中,需要特别说明以下几点:

  • 在随机森林中影响力巨大的max_depth在XGBoost中默认值为6,比GBDT中的调参空间略大,但还是没有太多的空间,因此影响力不足。

  • 在GBDT中影响力巨大的max_features对标XGBoost中的colsample_by*系列参数,原则上来说影响力应该非常大,但由于三个参数共同作用,调参难度较高,在只有1个参数作用时效果略逊于max_features。

  • 精剪枝参数往往不会对模型有太大的影响,但在XGBoost当中,min_child_weight与结构分数的计算略微相关,因此有时候会展现出较大的影响力。故而将这个精剪枝参数设置为4星参数。

  • 类似于objective这样影响整体学习能力的参数一般都有较大的影响力,但XGBoost当中每种任务可选的损失函数不多,因此一般损失函数不在调参范围之内,故认为该参数的影响力不明显。

  • XGBoost的初始化分数只能是数字,因此当迭代次数足够多、数据量足够大时,起点的影响会越来越小。因此我们一般不会对base_score进行调参。

首先会考虑所有影响力巨大的参数(5星参数),当算力足够/优化算法运行较快的时候,我们可以考虑将大部分时候具有影响力的参数(4星)也都加入参数空间。一般来说,只要样本量足够,我们还是愿意尝试subsample以及max_depth,如果算力充足,我们还可以加入obejctive这样或许会有效的参数。

需要说明的是,一般不会同时使用三个colsample_by参数、更不会同时调试三个colsample_by参数。首先,参数colsample_bylevel较为不稳定,不容易把握,因此当训练资源充足时,会同时调整colsample_bytree和colsample_bynode。如果计算资源不足,或者优先考虑节约计算时间,则会先选择其中一个参数、尝试将特征量控制在一定范围内来建树,并观察模型的结果。在这三个参数中,使用bynode在分枝前随机,比使用bytree建树前随机更能带来多样性、更能对抗过拟合,但同时也可能严重地伤害模型的学习能力。

参数空间的确定:

  • 对于有界的参数(比如colsample_bynode,subsamples等),或者有固定选项的参数(比如booster,objective),无需确认参数空间。

  • 对取值较小的参数(例如学习率eta,一般树模型的min_impurity_decrease等),或者通常会向下调整的参数(比如max_depth),一般是围绕默认值向两边展开构建参数空间。

  • 对于取值可大可小,且原则上可取到无穷值的参数(num_boost_round,gamma、lambda、min_child_weight等),一般需要绘制学习曲线进行提前探索,或者也可以设置广而稀的参数空间,来一步步缩小范围。

# hyperopt 优化
#日常使用库与算法
import pandas as pd
import numpy as np
import sklearn
import matplotlib as mlp
import matplotlib.pyplot as plt
import time
import xgboost as xgb

#导入优化算法
import hyperopt
from hyperopt import hp, fmin, tpe, Trials, partial
from hyperopt.early_stop import no_progress_loss

data = pd.read_csv(r"D:\Pythonwork\2021ML\PART 2 Ensembles\datasets\House Price\train_encode.csv",index_col=0)
X = data.iloc[:,:-1]
y = data.iloc[:,-1]

def hyperopt_objective(params):
    paramsforxgb = {"eta":params["eta"]
                    ,"booster":params["booster"]
                    ,"colsample_bytree":params["colsample_bytree"]
                    ,"colsample_bynode":params["colsample_bynode"]
                    ,"gamma":params["gamma"]
                    ,"lambda":params["lambda"]
                    ,"min_child_weight":params["min_child_weight"]
                    ,"max_depth":int(params["max_depth"])
                    ,"subsample":params["subsample"]
                    ,"objective":params["objective"]
                    ,"rate_drop":params["rate_drop"]
                    ,"nthread":14
                    ,"verbosity":0
                    ,"seed":1412}
    result = xgb.cv(paramsforxgb,data_xgb, seed=1412, metrics=("rmse")
                    ,num_boost_round=int(params["num_boost_round"]))
    return result.iloc[-1,2]

def param_hyperopt(max_evals=100):
    
    #保存迭代过程
    trials = Trials()
    
    #设置提前停止
    early_stop_fn = no_progress_loss(30)
    
    #定义代理模型
    params_best = fmin(hyperopt_objective
                       , space = param_grid_simple
                       , algo = tpe.suggest
                       , max_evals = max_evals
                       , verbose=True
                       , trials = trials
                       , early_stop_fn = early_stop_fn
                      )
    
    #打印最优参数,fmin会自动打印最佳分数
    print("\n","\n","best params: ", params_best,
          "\n")
    return params_best, trials

params_best, trials = param_hyperopt(100)
"""
由于高斯最优化模型具有随机性,所以要运行多次(M次)代码:
params_best, trials = param_hyperopt(100)
得出M个最优参数组合,根据这几个最优化模型参数组合保留参数相同的一些参数像:criterion,loss,但是如果尽管max_depth参数可能相同,但是不能确定,因为后续进一步调参具有随机性。
然后根据这M次最优化组合,取得近边缘值则调整参数范围、取得中值减小参数范围增加参数密度,进一步调参,运行param_hyperopt代码.
"""
# optuna优化
from optuna import Trial
from optuna.samplers import TPESampler

4.XGBoost数学原理

4.1算法流程

  • 存在的问题

4.2化简XGBoost的目标函数,令XGBoost的目标函数最小

posted @ 2023-02-12 12:22  lotuslaw  阅读(190)  评论(0编辑  收藏  举报