《XGBoost算法》
推荐的学习路径:
【快速实现XGBoost、跑通代码】- 第一部分
【快速掌握XGBoost应用、达到自由调参水平】- 第一部分~第三部分
【快速掌握XGBoost原理、面试得以通关】- 第一部分1 + 第二部分1.2、2.2 + 第四部分
目录
- 《XGBoost算法》
- [一 XGBoost的基础思想与实现](#一 XGBoost的基础思想与实现)
-
- [1 XGBoost pk 梯度提升树](#1 XGBoost pk 梯度提升树)
- [2 XGBoost回归的sklearnAPI实现](#2 XGBoost回归的sklearnAPI实现)
- [3 XGBoost回归的原生代码实现](#3 XGBoost回归的原生代码实现)
- [4 XGBoost分类的代码实现](#4 XGBoost分类的代码实现)
- [二 XGBoost的参数](#二 XGBoost的参数)
-
- [1 迭代过程](#1 迭代过程)
-
- [1.1 迭代次数/学习率/初始 H 0 H_0 H0/最大迭代值](#1.1 迭代次数/学习率/初始 H 0 H_0 H0/最大迭代值)
- [1.2 xgboost的目标函数](#1.2 xgboost的目标函数)
- [2 XGBoost的弱评估器](#2 XGBoost的弱评估器)
-
- [2.1 三大评估器与DART树](#2.1 三大评估器与DART树)
- [2.2 弱评估器的分枝](#2.2 弱评估器的分枝)
- [2.3 控制复杂度(一):弱评估器的剪枝](#2.3 控制复杂度(一):弱评估器的剪枝)
- [2.4 控制复杂度(二):弱评估器的训练数据](#2.4 控制复杂度(二):弱评估器的训练数据)
- [3 XGBoost的其他参数与方法](#3 XGBoost的其他参数与方法)
- [三 XGBoost的参数空间与超参数优化](#三 XGBoost的参数空间与超参数优化)
-
- [1 确定XGBoost优化的参数空间](#1 确定XGBoost优化的参数空间)
- [2 基于TEP对XGBoost进行优化](#2 基于TEP对XGBoost进行优化)
一 XGBoost的基础思想与实现
1 XGBoost pk 梯度提升树
极限提升树XGBoost(Extreme Gradient Boosting,XGB,发音/æks-g-boost/)是基于梯度提升树GBDT全面升级的新一代提升算法,也是提升家族中最富盛名、最灵活、最被机器学习竞赛所青睐的算法。不同于我们之前学过的任意单一算法,XGBoost是一个以提升树为核心的算法系统,它覆盖了至少3+建树流程、10+损失函数,可以实现各种类型的梯度提升树,灵活性无与伦比。同时,XGBoost天生被设计成支持巨量数据,因此可以自由接入GPU/分布式/数据库等系统、还创新了众多工程上对传统提升算法进行加速的新方法。可以说,XGBoost是21世纪中Boosting算法的又一个里程碑,它开创了后GBDT时代中Boosting算法的新辉煌。
需要注意的是,学习XGBoost需要大量梯度提升树(GBDT)相关知识,本课程将假设你已经非常熟悉梯度提升树的原理与特点、并且熟悉sklearn中的交叉验证的用法。如果你不熟悉梯度提升树,强烈建议你回顾之前的课程。
作为Boosting算法,XGBoost中自然包含Boosting三要素:
- 损失函数 L ( y , y ^ ) L(y,\hat{y}) L(y,y^) :用以衡量模型预测结果与真实结果的差异。
- 弱评估器 f ( x ) f(x) f(x) :(一般为)决策树,不同的boosting算法使用不同的建树过程。
- 综合集成结果 H ( x ) H(x) H(x):即集成算法具体如何输出集成结果
并且,XGBoost也遵循Boosting算法的基本流程进行建模:
依据上一个弱评估器 f ( x ) k − 1 f(x)_{k-1} f(x)k−1的结果,计算损失函数 L L L,
并使用 L L L自适应地影响下一个弱评估器 f ( x ) k f(x)_k f(x)k的构建。
集成模型输出的结果,受到整体所有弱评估器 f ( x ) 0 f(x)_0 f(x)0 ~ f ( x ) K f(x)_K f(x)K的影响。
当然,XGBoost真实的流程比上述过程复杂得多。虽然梯度提升树的流程本身已经比较复杂,XGBoost还是在此流程上做出了众多关键的改进。综合来看,这些改进都是基于XGBoost中两种非常关键的思想实现的:
- 第一,实现精确性与复杂度之间的平衡
树的集成模型是机器学习中最为强大的学习器之一,这一族学习器的特点是精确性好、适用于各种场景,但运行缓慢、且过拟合风险很高,因此从学习单一决策树时起,我们就持续为大家提供丰富的剪枝策略,目的就是为了降低各种树模型的模型复杂度,从而控制住过拟合。树模型的学习能力与过拟合风险之间的平衡,就是预测精确性与模型复杂度之间的平衡,也是经验风险与结构风险之间的平衡 ,这一平衡对决策树以及树的集成模型来说是永恒的议题。
在过去,我们总是先建立效果优异的模型,再依赖于手动剪枝来调节树模型的复杂度,但在XGBoost中,精确性与复杂度会在训练的每一步被考虑到。主要体现在:
1. XGBoost为损失函数 L ( y , y ^ ) L(y,\hat{y}) L(y,y^)加入结构风险项,构成目标函数 O ( y , y ^ ) O(y,\hat{y}) O(y,y^)
在AdaBoost与GBDT当中,我们的目标是找到损失函数 L ( y , y ^ ) L(y,\hat{y}) L(y,y^)的最小值,也就是让预测结果与真实结果差异最小,这一流程只关心精确性、不关心复杂度和过拟合情况。为应对这个问题,XGBoost从决策树的预剪枝流程、逻辑回归、岭回归、Lasso等经典算法的抗过拟合流程吸取经验,在损失函数中加入了控制过拟合的结构风险项,并将【 L ( y , y ^ ) L(y,\hat{y}) L(y,y^) + 结构风险】定义为目标函数 O ( y , y ^ ) O(y,\hat{y}) O(y,y^)。
这一变化让XGBoost在许多方面都与其他Boosting算法不同:例如,XGBoost是向着令目标函数最小化的目标进行训练,而不是令损失函数最小化的方向。再比如,XGBoost会优先利用结构风险中的参数来控制过拟合,而不像其他树的集成模型一样依赖于树结构参数(例如
max_depth
,min_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的平行学习、估计贪婪算法等知识点将不会被包括在本次课程当中。在讲解LightGBM的时候,我们将详解基于直方图的估计算法,那时我们就能够很容易地理解XGBoost所使用的估计贪婪算法了。
除此之外,XGBoost还保留了部分与梯度提升树类似的属性,包括:
- 弱评估器的输出类型与集成算法输出类型不一致
对于AdaBoost或随机森林算法来说,当集成算法执行的是回归任务时,弱评估器也是回归器,当集成算法执行分类任务时,弱评估器也是分类器。但对于GBDT以及基于GBDT的复杂Boosting算法们而言,无论集成算法整体在执行回归/分类/排序任务,弱评估器一定是回归器。GBDT通过sigmoid或softmax函数输出具体的分类结果,但实际弱评估器一定是回归器,XGBoost也是如此。
- 拟合负梯度,且当损失函数是0.5倍MSE时,拟合残差
任意Boosting算法都有自适应调整弱评估器的步骤。在GBDT当中,每次用于建立弱评估器的是样本 X X X以及当下集成输出 H ( x i ) H(x_i) H(xi)与真实标签 y y y之间的伪残差(也就是负梯度)。当损失函数是 1 2 M S E \frac{1}{2}MSE 21MSE时,负梯度在数学上等同于残差(Residual),因此GBDT是通过拟合残差来影响后续弱评估器结构。XGBoost也是依赖于拟合残差来影响后续弱评估器结构,但是与GBDT一样,这一点需要通过数学来证明。
- 抽样思想
GBDT借鉴了大量Bagging算法中的抽样思想,XGBoost也继承了这一属性,因此在XGBoost当中,我们也可以对样本和特征进行抽样来增大弱评估器之间的独立性
因为存在这些相似之处,因此我们将会在XGBoost的参数中看到部分熟悉的参数,如果你对梯度提升树足够熟悉,那XGBoost的许多参数对你来说应该并不难懂。需要注意的是,作为2014年才被正式提出的Boosting算法,XGBoost是一个独立于经典算法的算法系统,因此xgboost库是需要单独安装的。
2 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)
可以看到,两个类的参数两都很多,其中不乏一些我们非常熟悉的参数,例如n_estimators
,learning_rate
, max_depth
等。但大部分参数还是需要我们重新学习和认识,这与xgboost复杂的原理有很大的关系,但由于是sklearn API,所以所有这些参数都有相应的默认值。我们可以在不认识参数的情况下调用这个类。以回归类为例我们来看:
python
from xgboost import XGBRegressor
from sklearn.model_selection import cross_validate, KFold
from sklearn.model_selection import train_test_split
data = pd.read_csv("train_encode.csv",index_col=0)
#回归数据
X = data.iloc[:,:-1]
y = data.iloc[:,-1]
#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)
result_xgb_sk
3 XGBoost回归的原生代码实现
XGBoost的原生代码与我们已经习惯了的sklearn代码有很大的不同。首先,原生代码必须使用XGBoost自定义的数据结构DMatrix ,这一数据结构能够保证xgboost算法运行更快,并且能够自然迁移到GPU上运行,类似于列表、数组、Dataframe等结构都不能用于原生代码,因此使用原生代码的第一步就是要更换数据结构。
当设置好数据结构后,我们需要以字典形式设置参数 。XGBoost也可以接受像sklearn一样,将所有参数都写在训练所用的类当中,然而由于xgboost的参数列表过长、参数类型过多,直接将所有参数混写在训练模型的类中会显得代码冗长且混乱,因此我们往往会使用字典单独呈现参数。准备好参数列表后,我们将使用xgboost中自带的方法xgb.train
或xgb.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.train
和xgb.cv
的第一个参数params
就是我们需要使用字典自定义的参数列表,第二个参数dtrain
就是DMatrix结构的训练数据,第三个参数num_boost_round
其实就等同于sklearn中的n_estimators
,表示总共建立多少棵提升树,也就是提升过程中的迭代次数。
在之后的课程中,我们将会详细讲解训练中涉及到的每一个参数,在这里我们了解前三个参数就可以。和sklearn中一样,xgboost中的这些参数也都拥有默认值,因此我们可以不填写任何参数就运行xgboost算法。现在,我们来简单看看原生代码是如何实现的:
- 将数据转换为DMatrix
XGBoost模块的三步走:将数据转换为DMatrix,定义需要输入的参数params,直接调用训练。第一步,先转换数据格式:
python
import xgboost as xgb
data_xgb = xgb.DMatrix(X,y)
#如果有分割训练集和测试集
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 = {"num_boost_round":100,"max_depth":5,"seed":1412}
reg = xgb.train(params, data_xgb)
y_pred = reg.predict(data_xgb)
- 使用交叉验证进行训练
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
4 XGBoost分类的代码实现
XGBoost默认会实现回归算法,因此在执行分类的时候,我们需要主动声明算法的类型。xgboost是通过当前算法所使用的损失函数来判断任务类型的,即是通过在params中填写的objective
参数来判断任务类型。当不再执行回归任务时,模型的评估指标也会发生变化,因此xgboost分类所需要的参数会更多。objective
参数中可以输入数十种不同的选项,常见的有:
- 用于回归
reg:squarederror :平方损失,即 1 2 ( y − y ^ ) 2 \frac{1}{2}(y - \hat{y})^2 21(y−y^)2,其中1/2是为了计算简便
reg:squaredlogerror :平方对数损失,即 1 2 [ l o g ( y ^ + 1 ) − l o g ( y + 1 ) ] 2 \frac{1}{2}[log(\hat{y} + 1) - log(y + 1)]^2 21[log(y^+1)−log(y+1)]2,其中1/2是为了计算简便
- 用于分类
binary:logistic :二分类交叉熵损失,使用该损失时
predict
接口输出概率。如果你对该损失不熟悉,你需要学习逻辑回归算法。binary:logitraw :二分类交叉熵损失,使用该损失时
predict
接输出执行sigmoid变化之前的值multi:softmax :多分类交叉熵损失,使用该损失时
predict
接口输出具体的类别。如果你对该损失不熟悉,你需要学习AdaBoost与GBDT。multi:softprob :多分类交叉熵,适用该损失时
predict
接口输出每个样本每个类别下的概率
除此之外,还有众多用于排序算法、计数算法的损失函数。xgboost几乎适用于所有可微的损失函数,不同的损失函数会影响predict
的输出,但却不会影响交叉验证方法xgb.cv
的输出。当不填写任何内容时,参数objective
的默认值为reg:squarederror
。接下来我们来看看xgboost分类器的实现:
- 导入数据
python
#导入2个最简单的分类数据集:乳腺癌数据集与手写数字数据集
from sklearn.datasets import load_breast_cancer, load_digits
#二分类数据
X_binary = load_breast_cancer().data
y_binary = load_breast_cancer().target
data_binary = xgb.DMatrix(X_binary,y_binary)
#多分类数据
X_multi = load_digits().data
y_multi = load_digits().target
data_multi = xgb.DMatrix(X_multi, y_multi)
- 设置params,进行训练
相比起默认的回归算法,xgboost分类算法所需的参数会更多一些。二分类损失函数一般需要搭配参数eval_matric
,用于设置分类的评估指标。xgboost中默认的二分类指标是对数损失(也就是交叉熵损失logloss
),在更老的版本中,xgboost中默认的二分类指标是错误率error
,在某些xgboost版本当中,不设置参数eval_matric
将引发警告。
python
params1 = {"seed":1412, "objective":"binary:logistic"
,"eval_metric":"logloss" #二分类交叉熵损失
}
clf_binary = xgb.train(params1, data_binary, num_boost_round=100)
对多分类算法来说,除了设置损失函数和评估指标,还需要设置参数num_class
。参数num_class
用于多分类状况下、具体的标签类别数量,例如,如果是三分类,则需设置{"num_calss":3}。通常来说,算法应该能够根据标签的情况自主判断实际类别为多少,但不知为何,在xgboost原生代码及sklearn API当中,部分版本的xgboost都存在无法判断标签类别数量的问题。为避免报错,建议在执行多分类时写上该参数。
python
params2 = {"seed":1412, "objective":"multi:softmax"
,"eval_metric":"mlogloss" #多分类交叉熵损失 #"merror"
,"num_class":10}
clf_multi = xgb.train(params2, data_multi, num_boost_round=100)
- 预测与评估
python
y_pred_binary = clf_binary.predict(data_binary)
y_pred_multi = clf_multi.predict(data_multi)
y_pred_binary[:20] #二分类直接返回概率,不返回类别,需要自己转换
y_pred_multi #多分类,选择`multi:softmax`时返回具体类别,也可以选择`multi:softprob`返回概率。
- 交叉验证
分类算法与回归算法执行交叉验证的流程基本一致,但需要注意的是,当使用xgb.train
时,我们会将评估指标参数eval_matric
写在params中,在使用xgb.cv
时,我们却需要将评估指标参数写在xgb.cv
当中,否则有时候会报出警告。这是一个奇怪的bug,可能会随着xgboost库的迭代更新而消失。如果不介意警告,可以继续将评估指标写在params里的eval_matric
参数下。在xgb.cv
当中,我们需要将评估指标打包成元组,写在参数metrics
内部,如下所示:
python
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过程的随机数种子
)
result #返回多分类交叉熵损失
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过程的随机数种子
)
result #可以执行多个指标,让输出结果的列数翻倍
- sklearn API的实现
python
from xgboost import XGBClassifier
clf = XGBClassifier()
clf.get_params()
clf = XGBClassifier(objective="multi:softmax"
, eval_metric="mlogloss" #设置评估指标避免警告
, num_class = 10
# , use_label_encoder=False
)
clf = clf.fit(X_multi,y_multi)
clf.predict(X_multi) #输出具体数值 - 具体的预测类别
clf.predict_proba(X_multi).shape #输出概率值
clf.score(X_multi,y_multi) #虽然设置了评估指标,但score接口还是准确率
到这里,你已经基本掌握了xgboost建模的代码。如果你没有提升模型的需求,只需要跑通代码结果,那这部分内容就足够你使用了。从下一部分开始,我们将详细讲解xgboost中涉及到的所有重要参数。
二 XGBoost的参数
在之前的课程当中,我们已经认识了几个xgboost的参数,包括设置迭代次数的num_boost_round
,设置损失函数的objective
,设置评估指标的eval_matric
,以及设置多分类类别数的num_class
。这些参数有些是在params中设置的参数,有些是需要在方法.train
或者.cv
中设置的参数,但这些参数只是xgboost参数的九牛一毛。由于xgboost本身是一个复杂的算法系统,其超参数的数量十分惊人,我已经将可能用到的参数总结在了如下表格当中,其中标注为绿色的是我们未曾学过、或必须讲解的重要参数。
类型 | 参数 |
---|---|
迭代过程/目标函数 | 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的两种方式中(原生代码与sklearn API),参数的名称可能不同。在后续课程当中,我将会持续使用原生代码进行讲解,并附上该参数在sklearn API下的名称。有的参数可能只能在原生代码中使用,如果出现该情况,则会特殊标注出来。在后续案例课程当中,我们将会大量使用xgboost的原生代码,强烈建议以xgboost原生代码为核心进行学习,如果你考虑继续使用sklearn API,可以参考之前课程中所有适用于sklearn的代码,大部分代码都适用。
1 迭代过程
1.1 迭代次数/学习率/初始 H 0 H_0 H0/最大迭代值
作为Boosting算法,XGBoost的迭代流程与GBDT高度相似,因此XGBoost自然而然也有设置具体迭代次数的参数num_boost_round
、学习率参数eta
以及设置初始迭代值的base_score
。
具体地来说,对于样本 x i x_i xi,集成算法当中一共有 K K K棵树,则参数num_boost_round
的取值为K。假设现在正在建立第 k k k个弱评估器,则第 k k k个弱评估器上 x i x_i xi的结果可以表示为 f k ( x i ) f_k(x_i) fk(xi)。假设整个Boosting算法对样本 x i x_i xi输出的结果为 H ( x i ) H(x_i) H(xi),则该结果一般可以被表示为k=1~k=K过程当中,所有弱评估器结果的加权求和:
KaTeX parse error: Got function '\boldsymbol' with no arguments as superscript at position 22: ... = \sum_{k=1}^\̲b̲o̲l̲d̲s̲y̲m̲b̲o̲l̲{\color{red}K}\...
其中, ϕ k \phi_k ϕk为第k棵树的权重。特别的,XGBoost算法不计算树权重,因此XGBoost的输出结果为:
KaTeX parse error: Got function '\boldsymbol' with no arguments as superscript at position 22: ... = \sum_{k=1}^\̲b̲o̲l̲d̲s̲y̲m̲b̲o̲l̲{\color{red}K}f...
对于第 k k k次迭代来说,则有:
H k ( x i ) = H k − 1 ( x i ) + f k ( x i ) H_k(x_i) = H_{k-1}(x_i) + f_k(x_i) Hk(xi)=Hk−1(xi)+fk(xi)
在这个一般过程中,每次将本轮建好的决策树加入之前的建树结果时,可以增加参数 η \color{red}\eta η,表示为第k棵树加入整体集成算法时的学习率,对标参数eta
。
H k ( x i ) = H k − 1 ( x i ) + η f k ( x i ) H_k(x_i) = H_{k-1}(x_i) + \boldsymbol{\color{red}\eta} f_k(x_i) Hk(xi)=Hk−1(xi)+ηfk(xi)
该学习率参数控制Boosting集成过程中 H ( x i ) H(x_i) H(xi)的增长速度,是相当关键的参数。当学习率很大时, H ( x i ) H(x_i) H(xi)增长得更快,我们所需的num_boost_round
更少,当学习率较小时, H ( x i ) H(x_i) H(xi)增长较慢,我们所需的num_boost_round
就更多,因此boosting算法往往会需要在num_boost_round
与eta
中做出权衡。在XGBoost当中,num_boost_round
的默认值为10,eta
的默认值为0.3,如果你熟悉GBDT算法,那你也一定熟悉这两个参数,故此不再赘述。
- 参数
base_score
在上述过程中,我们建立第一个弱评估器时有:
H 1 ( x i ) = H 0 ( x i ) + η f 1 ( x i ) H_1(x_i) = H_{0}(x_i) + \eta f_1(x_i) H1(xi)=H0(xi)+ηf1(xi)
由于没有第0棵树的存在,因此 H 0 ( x i ) H_0(x_i) H0(xi)的值在数学过程及算法具体实现过程中都需要进行单独的确定,而这个值就由base_score
确定。在xgboost中,我们可以对base_score
输出任何数值 ,但并不支持类似于GBDT当中输入评估器的操作。当不填写时,该参数的默认值为0.5,即对所有样本都设置0.5为起始值。当迭代次数足够多、数据量足够大时,调整算法的 H 0 ( x i ) H_0(x_i) H0(xi)意义不大,因此我们基本不会调整这个参数。
- 参数
max_delta_step
在迭代过程当中,XGBoost有一个独特的参数max_delta_step
。这个参数代表了每次迭代时被允许的最大 η f k ( x i ) \eta f_k(x_i) ηfk(xi)。当参数max_delta_step
被设置为0,则说明不对每次迭代的 η f k ( x i ) \eta f_k(x_i) ηfk(xi)大小做限制,如果该参数被设置为正数C,则代表 η f k ( x i ) ≤ C \eta f_k(x_i) \leq C ηfk(xi)≤C,否则就让算法执行:
H k ( x i ) = H k − 1 ( x i ) + C H_k(x_i) = H_{k-1}(x_i) + C Hk(xi)=Hk−1(xi)+C
通常来说这个参数是不需要的,但有时候这个参数会对极度不均衡的数据有效。如果样本极度不均衡,那可以尝试在这个参数中设置1~10左右的数。
总结:
参数含义 | 原生代码 | sklearn API |
---|---|---|
迭代次数/树的数量 | num_boost_round (xgb.train) | n_estimators |
学习率 | eta (params) | learning_rate |
初始迭代值 | base_score (params) | base_score |
一次迭代中所允许的最大迭代值 | max_delta_step (params) | max_delta_step |
注意,在XGBoost原生论文当中使用 Φ ( x ) \Phi(x) Φ(x)作为树输出结果的表示,并且让 f ( x ) = η Φ ( x ) f(x) = \eta \Phi(x) f(x)=ηΦ(x),并且全程避免了使用字母 H H H,因为在xgboost的体系当中,字母 H H H代表着相当重要的另一个存在。在我们的课程中,为与之前的课程保持一致,我们还是使用 H ( x ) H(x) H(x)来表示最终模型的输出结果, f ( x ) f(x) f(x)作为树输出结果的表示,并将学习率 η \eta η单独呈现,但在阅读XGBoost原论文时,注意避免混淆。
1.2 xgboost的目标函数
在之前的课程当中,我们已经简单介绍过xgboost常用的几种损失函数。与GBDT一样,xgboost的损失函数理论上可以推广到任意可微函数,因此只要是我们在之前的课程中介绍过的损失函数都可以被用于xgboost。但与GBDT不同的是,xgboost并不向着损失函数最小化的方向运行,而是如我们在前面所提到的,xgboost向着令目标函数最小化的方向运行。
需要注意的是,损失函数可以针对单个样本进行计算,也可以针对整个算法进行计算,但在XGBoost的定义中,目标函数是针对每一棵树的,而不是针对一个样本或整个算法 。对任意树 f k f_k fk来说,目标函数有两个组成部分,一部分是任意可微的损失函数,它控制模型的经验风险 。从数值上来说,它等于现在树上所有样本上损失函数之和,其中单一样本的损失为 l ( y i , y i ^ ) l(y_i,\hat{y_i}) l(yi,yi^)。另一部分是控制模型复杂度的 Ω ( f k ) \Omega(f_k) Ω(fk),它控制当前树的结构风险 。
Obj_k = \\sum_{i=1}\^Ml(y_i,\\hat{y_i}) + \\Omega(f_k)
其中 M M M表示现在这棵树上一共使用了M个样本, l l l表示单一样本的损失函数。当模型迭代完毕之后,最后一棵树上的目标函数就是整个XGBoost算法的目标函数。
-
经验风险:模型对数据学习越深入,损失越小(经验风险越小),模型对数据学习得越浅显,损失越大(经验风险越大)。
-
结构风险:树结构越复杂、模型复杂度越高,过拟合风险越大(结构风险越大)。树模型结构越简单、模型复杂度越低、过拟合风险越小(结构风险越小)。
通常来说,模型需要达到一定的复杂度,才能保证较小的损失,但如果只追求最小的经验风险,反而容易导致过拟合。相对的,如果只追求模型复杂度低、结构风险低,那模型又容易陷入欠拟合的困局、损失函数过高,因此平衡结构风险与经验风险十分关键。XGBoost向着目标函数最小化的方向运行,可以保证在迭代过程中,经验风险和结构风险都不会变得太大,因此模型的损失不会太大、同时又不会太容易过拟合。
在具体的公式当中,结构风险 Ω ( f k ) \Omega(f_k) Ω(fk)又由两部分组成,一部分是控制树结构的 γ T \gamma T γT,另一部分则是正则项:
Ω ( f k ) = γ T + λ 2 ∑ j = 1 T w j 2 + α ∑ j = 1 T w j \Omega(f_k) = \gamma T + \frac{\lambda}{2} \sum_{j=1}^T w_j^2 + \alpha \sum_{j=1}^T w_j Ω(fk)=γT+2λj=1∑Twj2+αj=1∑Twj
其中 γ \gamma γ, λ \lambda λ与 α \alpha α都是可以自由设置的系数,而 T T T表示当前第 k k k棵树上的叶子总量, w j w_j wj则代表当前树上第 j j j片叶子的叶子权重(leaf weights)。叶子权重是XGBoost数学体系中非常关键的一个因子,它实际上就是当前叶子 j j j的预测值 ,这一指标与数据的标签量纲有较大的关系,因此当标签的绝对值较大、 w j w_j wj值也会倾向于越大。因此正则项有两个:使用平方的L2正则项与使用绝对值的L1正则项,因此完整的目标函数表达式为:
O b j k = ∑ i = 1 M l ( y i , y i ^ ) + γ T + 1 2 λ ∑ j = 1 T w j 2 + α ∑ j = 1 T w j 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 Objk=i=1∑Ml(yi,yi^)+γT+21λj=1∑Twj2+αj=1∑Twj
不难发现,所有可以自由设置的系数都与结构风险有关,这三个系数也正对应着xgboost中的三个参数:gamma
,alpha
与lambda
。
-
参数
gamma
:乘在一棵树的叶子总量 T T T之前,依照叶子总量对目标函数施加惩罚的系数,默认值为0,可填写任何[0, ∞]之间的数字。当叶子总量固定时,gamma
越大,结构风险项越大;同时,当gamma
不变时,叶子总量越多、模型复杂度越大,结构风险项也会越大。在以上两种情况下,目标函数受到的惩罚都会越大,因此调大gamma
可以控制过拟合 。 -
参数
alpha
与lambda
:乘在正则项之前,依照叶子权重的大小对目标函数施加惩罚的系数,也就是正则项系数。lambda
的默认值为1,alpha
的默认值为0,因此xgboost默认使用L2正则化。通常来说,我们不会同时使用两个正则化,但我们也可以尝试这么做。 ∑ j = 1 T w j \sum_{j=1}^Tw_j ∑j=1Twj是当前树上所有叶子的输出值之和,因此当树上的叶子越多、模型复杂度越大时, ∑ j = 1 T w j \sum_{j=1}^Tw_j ∑j=1Twj自然的数值自然会更大,因此当正则项系数固定时,模型复杂度越高,对整体目标函数的惩罚就越重。当 w w w固定时,正则项系数越大,整体目标函数越大,因此调大alpha
或lambda
可以控制过拟合。
参数含义 | 原生代码 | sklearn API |
---|---|---|
乘在叶子节点数量前的系数 | gamma (params) | gamma |
L2正则项系数 | lambda (params) | reg_lambda |
L1正则项系数 | alpha (params) | reg_alpha |
总结一下,在整个迭代过程中,我们涉及到了如下参数:
类型 | 参数 |
---|---|
迭代过程/损失函数 | num_boost_round:集成算法中弱分类器数量,对Boosting算法而言为实际迭代次数 eta:Boosting算法中的学习率,影响弱分类器结果的加权求和过程 objective:选择需要优化的损失函数 base_score:初始化预测结果 H 0 H_0 H0的设置 max_delta_step:一次迭代中所允许的最大迭代值 gamma:乘在叶子数量前的系数,放大可控制过拟合 lambda:L2正则项系数,放大可控制过拟合 alpha:L1正则项系数,放大可控制过拟合 |
2 XGBoost的弱评估器
2.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树的参数需要单独进行说明。DART树的建树过程与普通提升树gbtree完全一致,但在集成树结果的过程中与传统gbtree有所区别。具体地来说,提升树的模型输出结果往往等于所有树结果的加权求和:
H ( x i ) = ∑ k = 1 K f k ( x i ) H(x_i) = \sum_{k=1}^{\boldsymbol{\color{red}K}} f_k(x_i) H(xi)=k=1∑Kfk(xi)
在第 k k k次迭代中建立新的树时,迭代后的结果等于之前所有 k − 1 {k-1} k−1棵树的结果加新建立的树的结果:
H k ( x i ) = H k − 1 ( x i ) + η f k ( x i ) H_k(x_i) = H_{k-1}(x_i) + \boldsymbol{\color{red}\eta} f_k(x_i) Hk(xi)=Hk−1(xi)+ηfk(xi)
DART树在每一次迭代 前都会随机地抛弃部份树,即不让这些树参与 H k − 1 ( x i ) H_{k-1}(x_i) Hk−1(xi)的计算,这种随机放弃的方式被叫做"Dropout"(抛弃)。举例说明,假设现在一共有5棵树,结果分别如下:
k=1 | k=2 | k=3 | k=4 | k=5 | |
---|---|---|---|---|---|
η f k ( x i ) \eta f_k(x_i) ηfk(xi) | 1 | 0.8 | 0.6 | 0.5 | 0.3 |
当建立第6棵树时,普通提升树的 H k − 1 ( x i ) H_{k-1}(x_i) Hk−1(xi) = 1+0.8+0.6+0.5+0.3 = 3.2。对于DART树来说,我们可以认为设置抛弃率rate_drop
,假设抛弃率为0.2,则DART树会随机从5棵树中抽样一棵树进行抛弃。假设抛弃了第二棵树,则DART树的 H k − 1 ( x i ) H_{k-1}(x_i) Hk−1(xi) = 1+0.6+0.5+0.3 = 2.4。通过影响 H k − 1 ( x i ) H_{k-1}(x_i) Hk−1(xi),DART树影响损失函数、影响整个算法的输出结果 H ( x ) H(x) H(x),以此就可以在每一次迭代中极大程度地影响整个xgboost的方向。
在一般的抗过拟合方法当中,我们只能从单棵树的学习能力角度入手花式对树进行剪枝,但DART树的方法是对整体迭代过程进行控制。在任意以"迭代"为核心的算法当中,我们都面临同样的问题,即最开始的迭代极大程度地影响整个算法的走向,而后续的迭代只能在前面的基础上小修小补。这一点从直觉上来说很好理解,毕竟当我们在绘制损失函数的曲线时,会发现在刚开始迭代时,损失函数急剧下降,但随后就逐渐趋于平缓。在这个过程中,没有任何过拟合手段可以从流程上影响到那些先建立的、具有巨大影响力的树,但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树也会带来相应的问题,最明显的缺点就是:
- 用于微调模型的一些树可能被抛弃,微调可能失效
- 由于存在随机性,模型可能变得不稳定,因此提前停止等功能可能也会变得不稳定
- 由于要随机抛弃一些树的结果,在工程上来说就无法使用每一轮之前计算出的 H k − 1 H_{k-1} Hk−1,而必须重新对选中的树结果进行加权求和,可能导致模型迭代变得略微缓慢
2.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 j j,对该节点来说结构分数的公式为:
S c o r e j = ( ∑ i ∈ j g i ) 2 ∑ i ∈ j h i + λ Score_j = \frac{(\sum_{i \in j}g_i)^2}{\sum_{i \in j}h_i + \lambda} Scorej=∑i∈jhi+λ(∑i∈jgi)2
其中, g i g_i gi是样本 i i i在损失函数 L L L上对预测标签求的一阶导数, h i h_i hi是样本 i i i在损失函数 L L L上对预测标签求的二阶导数, i ∈ j i \in j i∈j表示对叶子 j j j上的所有样本进行计算, λ \lambda λ就是L2正则化的正则化系数。所以不难发现,结构分数实际上就是:
S c o r e j = 节点 j 上所有样本的一阶导数之和的平方 节点 j 上所有样本的二阶导数之和 + λ Score_j = \frac{节点j上所有样本的一阶导数之和的平方}{节点j上所有样本的二阶导数之和 + \lambda} Scorej=节点j上所有样本的二阶导数之和+λ节点j上所有样本的一阶导数之和的平方
需要注意结构分数是针对节点计算的,我们以前学习的不纯度衡量指标如基尼系数、信息熵等也是如此。在此基础上,我们依赖于结构分数增益进行分枝,结构分数增益表现为:
G a i n = S c o r e L + S c o r e R − S c o r e P = ( ∑ i ∈ L g i ) 2 ∑ i ∈ L h i + λ + ( ∑ i ∈ R g i ) 2 ∑ i ∈ R h i + λ − ( ∑ i ∈ P g i ) 2 ∑ i ∈ P h i + λ (见原论文 7 号公式) \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=ScoreL+ScoreR−ScoreP=∑i∈Lhi+λ(∑i∈Lgi)2+∑i∈Rhi+λ(∑i∈Rgi)2−∑i∈Phi+λ(∑i∈Pgi)2(见原论文7号公式)
这即是说,结构分数增益实际上就是:
G a i n = 左节点的结构分数 + 右节点的结构分数 − 父节点的结构分数 Gain = 左节点的结构分数 + 右节点的结构分数 - 父节点的结构分数 Gain=左节点的结构分数+右节点的结构分数−父节点的结构分数
我们选择增益 G a i n Gain Gain最大的点进行分枝。
你是否注意到,XGBoost中的分枝规则与经典CART树的分枝规则在细节上有所不同?CART树中所使用的信息增益是:
C A R T 树中的信息增益 = 父节点的不纯度 − (左节点的不纯度 + 右节点的不纯度) 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,公式为 1 2 ( y − y ^ ) 2 \frac{1}{2}(y - \hat{y})^2 21(y−y^)2,假设lambda=1。那基于MSE的一阶导数为:
l = 1 2 ( y i − y i ^ ) 2 l ′ = ∂ ∂ y i ^ 1 2 ( y i − y i ^ ) 2 = − ( y i − y i ^ ) = y i ^ − y i \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} ll′=21(yi−yi^)2=∂yi^∂21(yi−yi^)2=−(yi−yi^)=yi^−yi
基于MSE的二阶导数为:
l ′ ′ = ∂ ∂ y i ^ ( y i ^ − y i ) = 1 \begin{align} l'' &= \frac{\partial}{\partial \hat{y_i}} (\hat{y_i} - y_i)\\ \\ &= 1 \end{align} l′′=∂yi^∂(yi^−yi)=1
因此无论如何划分, g i = y i ^ − y i g_i = \hat{y_i} - y_i gi=yi^−yi, h i = 1 h_i = 1 hi=1。现在来计算父节点和两个子节点上每个样本的 g i g_i gi与 h i h_i 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 |
因此父节点的结构分数为:
S c o r e P = ( ∑ i ∈ P g i ) 2 ∑ i ∈ P h i + λ = ( − 0.5 + 2.5 + 2.5 ) 2 3 + 1 = 5.0625 \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} ScoreP=∑i∈Phi+λ(∑i∈Pgi)2=3+1(−0.5+2.5+2.5)2=5.0625
- 方案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下两个子节点的结构分数为:
S c o r e L 1 = ( ∑ i ∈ L 1 g i ) 2 ∑ i ∈ L 1 h i + λ = ( − 0.5 ) 2 1 + 1 = 0.125 \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} ScoreL1=∑i∈L1hi+λ(∑i∈L1gi)2=1+1(−0.5)2=0.125
S c o r e R 1 = ( ∑ i ∈ R 1 g i ) 2 ∑ i ∈ R 1 h i + λ = ( 2.5 + 2.5 ) 2 2 + 1 = 8.333 \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} ScoreR1=∑i∈R1hi+λ(∑i∈R1gi)2=2+1(2.5+2.5)2=8.333
因此增益等于:
G a i n = S c o r e L 1 + S c o r e R 1 − S c o r e P = 0.125 + 8.333 − 5.6025 = 3.395 \begin{align} Gain &= Score_{L1} + Score_{R1} - Score_P \\ \\ &= 0.125 + 8.333 - 5.6025 \\ \\ &= 3.395 \end{align} Gain=ScoreL1+ScoreR1−ScoreP=0.125+8.333−5.6025=3.395
- 方案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下两个子节点的结构分数为:
S c o r e L 1 = ( ∑ i ∈ L 1 g i ) 2 ∑ i ∈ L 1 h i + λ = ( − 0.5 + 2.5 ) 2 2 + 1 = 1.333 \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} ScoreL1=∑i∈L1hi+λ(∑i∈L1gi)2=2+1(−0.5+2.5)2=1.333
S c o r e R 1 = ( ∑ i ∈ R 1 g i ) 2 ∑ i ∈ R 1 h i + λ = ( 2.5 ) 2 1 + 1 = 3.125 \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} ScoreR1=∑i∈R1hi+λ(∑i∈R1gi)2=1+1(2.5)2=3.125
因此增益等于:
G a i n = S c o r e L 1 + S c o r e R 1 − S c o r e P = 1.333 + 3.125 − 5.0625 = − 0.604 \begin{align} Gain &= Score_{L1} + Score_{R1} - Score_P \\ \\ &= 1.333 + 3.125 - 5.0625 \\ \\ &= -0.604 \end{align} Gain=ScoreL1+ScoreR1−ScoreP=1.333+3.125−5.0625=−0.604
方案 | 左侧结构分数 | 右侧结构分数 | 父节点结构分数 | 增益 |
---|---|---|---|---|
(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)。
2.3 控制复杂度(一):弱评估器的剪枝
对于树模型,除了了解树是如何建立的,也需要了解如何控制树的整体结构复杂度。一般来说,控制树模型复杂度的方式有两种:一种是对树进行剪枝,一种是从训练数据上下功夫。本节我们先来看弱评估器的剪枝。
与其他树模型中五花八门的剪枝参数不同,XGBoost只有三个剪枝参数和一个侧面影响树生长的参数,其中最为我们熟知的剪枝参数是max_depth
,它的用法与其他树模型中一致,在XGBoost中默认值为6,因此在对抗过拟合方面影响力不是很大。需要重点来说明的是以下三个参数:
- 参数
min_child_weight
:可以被广义理解为任意节点上所允许的样本量(样本权重)。
更严谨的说法是,
min_child_weight
是在任意节点 j j j上所允许的最小的 ∑ i ∈ j h i \sum_{i \in j}h_i ∑i∈jhi值。如果一个节点上的 ∑ i ∈ j h i \sum_{i \in j}h_i ∑i∈jhi小于该参数中设置的值,该节点被剪枝。如果你仔细学习了上一节内容,你会很容易理解 ∑ i ∈ j h i \sum_{i \in j}h_i ∑i∈jhi其实就是结构分数的分母:
S c o r e j = ( ∑ i ∈ j g i ) 2 ∑ i ∈ j h i + λ Score_j = \frac{(\sum_{i \in j}g_i)^2}{\sum_{i \in j}h_i + \lambda} Scorej=∑i∈jhi+λ(∑i∈jgi)2
其中, h i h_i hi是样本 i i i的损失函数 l l l在预测值 f ( x i ) f(x_i) f(xi)上的二阶导数, ∑ i ∈ j h i \sum_{i \in j}h_i ∑i∈jhi就是该节点上所有样本的 h i h_i hi之和。
在上一节中,假设损失函数为 1 2 M S E \frac{1}{2}MSE 21MSE,我们推导出任意样本的 h i = 1 h_i = 1 hi=1,因此 ∑ i ∈ j h i \sum_{i \in j}h_i ∑i∈jhi应该等于该叶子节点上的总样本量。因为这个原因, h i h_i hi在XGBoost原始论文和官方说明中有时被称为"样本权重"(instance weight)。因此,当MSE为损失函数时,参数
min_child_weight
很类似于sklearn中的min_sample_leaf
,即一个节点上所允许的最小样本量。然而,如果我们使用的损失函数不是MSE,那 h i h_i hi也就不会等于1了。不过官方依然将 h i h_i hi称之为样本权重,当损失函数更换时,样本的权重也随之变化。当损失函数不为MSE时,参数
min_child_weight
时一个节点上所允许的最小样本权重量。很显然,参数
min_child_weight
越大,模型越不容易过拟合,同时学习能力也越弱。
- 参数
gamma
:目标函数中叶子数量 T T T前的系数,同时也是允许分枝的最低结构分数增益。当分枝时结构增益不足gamma
中设置的值,该节点被剪枝。
在目标函数当中,
gamma
是叶子数量 T T T前的系数,放大gamma可以将目标函数的重点转移至结构风险,从而控制过拟合:O b j k = ∑ i = 1 M l ( y i , y i ^ ) + γ T + 1 2 λ ∑ j = 1 T w j 2 + α ∑ j = 1 T w j 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 Objk=i=1∑Ml(yi,yi^)+γT+21λj=1∑Twj2+αj=1∑Twj
在上一节中介绍结构分数时,我们曾做出假设
gamma
为0,当gamma
不为0时,结构分数增益的公式如下:G a i n = 1 2 ( S c o r e L + S c o r e R − S c o r e P ) − γ = 1 2 ( ( ∑ i ∈ L g i ) 2 ∑ i ∈ L h i + λ + ( ∑ i ∈ R g i ) 2 ∑ i ∈ R h i + λ − ( ∑ i ∈ P g i ) 2 ∑ i ∈ P h i + λ ) − γ \begin{align} Gain &= \frac{1}{2} ( Score_L + Score_R - Score_P ) - \gamma \\ \\ &= \frac{1}{2} \left( \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} \right) - \gamma \end{align} Gain=21(ScoreL+ScoreR−ScoreP)−γ=21(∑i∈Lhi+λ(∑i∈Lgi)2+∑i∈Rhi+λ(∑i∈Rgi)2−∑i∈Phi+λ(∑i∈Pgi)2)−γ
在XGBoost中,我们追求一棵树整体的结构分数最大,因此XGBoost规定任意结构的分数增益不能为负,任意增益为负的节点都会被剪枝 ,因此可以默认有:
1 2 ( ( ∑ i ∈ L g i ) 2 ∑ i ∈ L h i + λ + ( ∑ i ∈ R g i ) 2 ∑ i ∈ R h i + λ − ( ∑ i ∈ P g i ) 2 ∑ i ∈ P h i + λ ) − γ > 0 \frac{1}{2} \left( \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} \right) - \gamma > 0 21(∑i∈Lhi+λ(∑i∈Lgi)2+∑i∈Rhi+λ(∑i∈Rgi)2−∑i∈Phi+λ(∑i∈Pgi)2)−γ>0
因此: 1 2 ( ( ∑ i ∈ L g i ) 2 ∑ i ∈ L h i + λ + ( ∑ i ∈ R g i ) 2 ∑ i ∈ R h i + λ − ( ∑ i ∈ P g i ) 2 ∑ i ∈ P h i + λ ) > γ \frac{1}{2} \left( \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} \right) > \gamma 21(∑i∈Lhi+λ(∑i∈Lgi)2+∑i∈Rhi+λ(∑i∈Rgi)2−∑i∈Phi+λ(∑i∈Pgi)2)>γ
这是说,当参数
gamma
为0时,任意增益为负的节点都会被剪枝。当gamma
为任意正数时,任意增益小于gamma
设定值的节点都会被剪枝。不难发现,gamma
在剪枝中的作用就相当于sklearn中的min_impurity_decrease
。很显然,
gamma
值越大,算法越不容易过拟合,同时学习能力也越弱。
- 参数
lambda
和alpha
:正则化系数,同时也位于结构分数中间接影响树的生长和分枝。
当使用L2正则化时,结构分数为:
S c o r e j = ( ∑ i ∈ j g i ) 2 ∑ i ∈ j h i + λ Score_j = \frac{(\sum_{i \in j}g_i)^2}{\sum_{i \in j}h_i + \lambda} Scorej=∑i∈jhi+λ(∑i∈jgi)2
然而,当使用L1正则化时,结构分数为:
S c o r e j = ( ∑ i ∈ j g i ) 2 + α ∑ i ∈ j h i Score_j = \frac{(\sum_{i \in j}g_i)^2 + \alpha}{\sum_{i \in j}h_i} Scorej=∑i∈jhi(∑i∈jgi)2+α
因此,当
lambda
越大,结构分数会越小,参数gamma
的力量会被放大,模型整体的剪枝会变得更加严格,同时,由于lambda
还可以通过目标函数将模型学习的重点拉向结构风险,因此lambda
具有双重扛过拟合能力。然而,当
alpha
越大时,结构分数会越大,参数gamma
的力量会被缩小,模型整体的剪枝会变得更宽松。然而,alpha
还可以通过目标函数将模型学习的重点拉向结构风险,因此alpha
会通过放大结构分数抵消一部分扛过拟合的能力。整体来看,alpha
是比lambda
更宽松的剪枝方式。在XGBoost当中,我们可以同时使用两种正则化,则结构分数为:
S c o r e j = ( ∑ i ∈ j g i ) 2 + α ∑ i ∈ j h i + λ Score_j = \frac{(\sum_{i \in j}g_i)^2 + \alpha}{\sum_{i \in j}h_i + \lambda} Scorej=∑i∈jhi+λ(∑i∈jgi)2+α
此时,影响模型变化的因子会变得过多,我们难以再从中找到规律,调参会因此变得略有困难。但是当你感觉到L2正则化本身不足以抵抗过拟合的时候,可以使用L1+L2正则化的方式尝试调参。
不难发现,由于在目标函数中加入了正则项和控制叶子数量的结构风险项,XGBoost控制过拟合的方式与其他树模型差异很大。在之后调参的章节当中,我们将尝试将上述4个参数共同调参。
2.4 控制复杂度(二):弱评估器的训练数据
除了通过剪枝来控制模型复杂度之外,XGBoost也继承了GBDT和随机森林的优良传统:可以通过对样本和特征进行抽样来增加弱评估器多样性、从而控制过拟合。在这一部分所使用的参数都是我们曾经见过的,只不过在XGBoost当中,我们可以进行更丰富的数据抽样。具体来看:
样本的抽样
- 参数
subsample
:对样本进行抽样的比例,默认为1,可输入(0,1]之间的任何浮点数。例如,输入0.5,则表示随机抽样50%的样本进行建树。
当该参数设置为1时,表示使用原始数据进行建模,不进行抽样。同时,XGBoost中的样本抽样是不放回抽样 ,因此不像GBDT或者随机森林那样存在袋外数据的问题,同时也无法抽样比原始数据更多的样本量。因此,抽样之后样本量只能维持不变或变少,如果样本量较少,建议保持
subsample
=1。
- 参数
sampling_method
:对样本进行抽样时所使用的抽样方法,默认均匀抽样。
输入"uniform":表示使用均匀抽样,每个样本被抽到的概率一致。如果使用均匀抽样,建议
subsample
的比例最好在0.5或以上。需要注意的是,该参数还包含另一种可能的输入"gradient_based":表示使用有权重的抽样,并且每个样本的权重等于该样本的 g i 2 + λ h i 2 \sqrt{g_i^2 +\lambda h_i^2} gi2+λhi2 。但该输入目前还不支持XGBoost当中主流的gbtree等建树方法,因此一般我们不会用到。
特征的抽样
- 参数
colsample_bytree
,colsample_bylevel
,colsample_bynode
,这几个参数工沟通控制对特征所进行的抽样。
所有形似
colsample_by*
的参数都是抽样比例,可输入(0,1]之间的任何浮点数,默认值都为1。对于GBDT、随机森林来说,特征抽样是发生在每一次建树之前。但对XGBoost来说,特征的抽样可以发生在建树之前(由
colsample_bytree
控制)、生长出新的一层树之前(由colsample_bylevel
控制)、或者每个节点分枝之前(由colsample_bynode
控制)。三个参数之间会互相影响,全特征集 >= 建树所用的特征子集 >= 建立每一层所用的特征子集 >= 每个节点分枝时所使用的特征子集 。
举例说明:假设原本有64个特征,参数
colsample_bytree
等于0.5,则用于建树的特征就只有32个。此时,如果colsample_bylevel
不为1,也为0.5,那新建层所用的特征只能由16个,并且这16个特征只能从当前树已经抽样出的32特征中选择 。同样的,如果colsample_bynode
也不为1,为0.5,那每次分枝之前所用的特征就只有8个,并且这8个特征只能从当前层已经抽样出的16个特征中选择。在实际使用时,我们可以让任意抽样参数的比例为1,可以在某一环节不进行抽样。一般如果特征量太少(例如,10个以下),不建议同时使用三个参数。
现在我们已经详细介绍了XGBoost当中最简单的弱评估器,按照CART树规则或DART树规则、使用结构分数增益进行分枝的树在XGBoost的系统中被称为"贪婪树"(Greedy Tree)。大部分时候我们都会使用贪婪树来运行XGBoost算法,但在XGBoost当中还有其他几种不同的建树模式,包括基于直方图的估计贪婪树(approx greedy tree)、快速直方图贪婪树(Fast Histogram Approximate Greedy Tree)、以及基于GPU运行的快速直方图贪婪树等内容。这些算法在XGBoost原始论文中占了较大篇幅,并且在后续的LightGBM算法中被发扬光大,我们将在LGBM算法中详细讲解直方图方法。在使用XGBoost时,我们将专注于贪婪树本身。
到这里,关于XGBoost弱评估器的内容就全部讲解完毕了,总结一下,我们学习了如下参数:
类型 | 参数 |
---|---|
弱评估器 | booster:选择迭代过程中的弱评估器类型,包括gbtree,DART和线性模型 sample_type:DART树中随机抽样树的具体方法 rate_drop:DART树中所使用的抛弃率 one_drop:每轮迭代时至少需要抛弃的树的数量 skip_drop:在迭代中不进行抛弃的概率 normalized_type:根据被抛弃的树的权重控制新增树权重 max_depth:允许的弱评估器的最大深度 min_child_weight:(广义上)叶子节点上的最小样本权重/最小样本量 gamma:目标函数中叶子数量 T T T的系数,同时也是分枝时所需的最小结构分数增益值 lambda 与alpha:正则项系数,同时也位于结构分数的公式中,间接影响模型的剪枝 sample_type:对样本进行抽样具体方式 subsample:对样本进行抽样的具体比例 colsample_bytree, colsample_bylevel, colsample_bynode:在建树过程中对特征进行抽样的比例 |
需要注意的是,以上全部参数都需要被写在parmas中,没有任何需要写在xgb.train
或xgb.cv
中的参数,故而没有给大家呈现具体的代码。在后续调参章节中,我们将展示使用这些参数的代码。
3 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,请帮我打印更多的信息。
python
data = xgb.DMatrix(X,y)
params = {"objective":"reg:squarederror"
,"eta":0.3
,"verbosity":1 #如果有警告就打印警告,没有警惕则不打印任何内容
,"seed":1412} #随机数种子
reg = xgb.train(params,data,num_boost_round=10)
python
params = {"objective":"reg:squarederror"
,"eta":0.3
,"verbosity":2 #打印进度 - 没有相应的数字来告诉我现在的进度
,"seed":1412} #随机数种子
reg = xgb.train(params,data,num_boost_round=10)
python
params = {"objective":"reg:squarederror"
,"eta":0.3
,"verbosity":3
,"seed":1412} #随机数种子
reg = xgb.train(params,data,num_boost_round=10)
- 样本不均衡
参数
scale_pos_weight
:调节样本不均衡问题,类似于sklearn中的class_weight,仅在算法执行分类任务时有效。参数scale_pos_weight
的值时负样本比正样本的比例,默认为1,因此XGBoost时默认调节样本不均衡的。同时,如果你需要手动设置这个参数,可以输入(负样本总量)/(正样本总量)这样的值。
- 并行的线程
参数
nthread
:允许并行的最大线程数,类似于sklearn中的n_jobs,默认为最大,因此xgboost在默认运行时就会占用大量资源。如果数据量较大、模型体量较大,可以设置比最大线程略小的线程,为其他程序运行留出空间。
类型 | 参数 |
---|---|
迭代过程/目标函数 | 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全部的参数,但上面表格中的参数已经覆盖了95%你需要用到的参数,在理解这些参数的基础上,再使用XGBoost的其他参数也会相对容易。从下边将讲解XGBoost使用贝叶斯优化的调参流程。
三 XGBoost的参数空间与超参数优化
1 确定XGBoost优化的参数空间
丰富的超参数为集成算法提供了无限的可能,以降低偏差为目的的Boosting算法们在调参之后的表现更是所向披靡,因此XGBoost的超参数自动优化也是一个重要的课题。在过去的课程当中我们已经讲解过大量关于树模型参数影响力的内容,因此在阅读本章之前,强烈建议学习GBDT课程当中超参数空间相关的内容。属于GBDT的参数空间会极大程度地帮助你理解XGBoost的参数空间。
对任意集成算法进行超参数优化之前,我们需要明确两个基本事实:
1、不同参数对算法结果的影响力大小
2、确定用于搜索的参数空间
对XGBoost来说,我们可以大致如下排列各个参数对算法的影响:
影响力 | 参数 |
---|---|
⭐⭐⭐⭐⭐ 几乎总是具有巨大影响力 | num_boost_round(整体学习能力) eta(整体学习速率) |
⭐⭐⭐⭐ 大部分时候具有影响力 | booster(整体学习能力) colsample_by*(随机性) gamma(结构风险 + 精剪枝) lambda(结构风险 + 间接剪枝) min_child_weight(精剪枝) |
⭐⭐ 可能有大影响力 大部分时候影响力不明显 | max_depth(粗剪枝) alpha(结构风险 + 精剪枝) subsamples(随机性) objective(整体学习能力) scale_pos_weight(样本不均衡) |
⭐ 当数据量足够大时,几乎无影响 | seed base_score(初始化) |
比起其他树的集成算法,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
建树前随机更能带来多样性、更能对抗过拟合,但同时也可能严重地伤害模型的学习能力。在这里,我将尝试同时使用两个参数进行调参。
在这样的基本思想下,再结合硬件与运行时间因素,我将选择如下参数进行调整,并使用基于TPE贝叶斯优化(HyperOpt)对XGBoost进行优化------
参数 |
---|
num_boost_round |
eta |
booster |
colsample_bynode |
colsample_bytree |
gamma |
lambda |
min_child_weight |
max_depth |
subsamples |
objective |
在此基础上,我们需要进一步确认参数空间:
-
对于有界的参数(比如
colsample_bynode
,subsamples
等),或者有固定选项的参数(比如booster
,objective
),无需确认参数空间。 -
对取值较小的参数(例如学习率
eta
,一般树模型的min_impurity_decrease
等),或者通常会向下调整的参数(比如max_depth
),一般是围绕默认值向两边展开构建参数空间。 -
对于取值可大可小,且原则上可取到无穷值的参数(
num_boost_round
,gamma
、lambda
、min_child_weight
等),一般需要绘制学习曲线进行提前探索,或者也可以设置广而稀的参数空间,来一步步缩小范围。
参数 | 范围 |
---|---|
num_boost_round |
学习曲线探索,最后定为 (50,200,10) |
eta |
以0.3为中心向两边延展,最后定为 (0.05,2.05,0.05) |
booster |
两种选项 ["gbtree","dart"] |
colsample_bytree |
设置为(0,1]之间的值,但由于还有参数bynode ,因此整体不宜定得太小,因此定为 (0.3,1,0.1) |
colsample_bynode |
设置为(0,1]之间的值,定为 (0.1,1,0.1) |
gamma |
学习曲线探索,有较大可能需要改变,定为 (1e6,1e7,1e6) |
lambda |
学习曲线探索,定为 (0,3,0.2) |
min_child_weight |
学习曲线探索,定为 (0,50,2) |
max_depth |
以6为中心向两边延展,右侧范围定得更大 (2,30,2) |
subsample |
设置为(0,1]之间的值,定为 (0.1,1,0.1) |
objective |
两种回归类模型的评估指标 ["reg:squarederror", "reg:squaredlogerror"] |
rate_drop |
如果选择"dart"树所需要补充的参数,设置为(0,1]之间的值 (0.1,1,0.1) |
一般在初次搜索时,我们会设置范围较大、较为稀疏的参数空间,然后在多次搜索中逐渐缩小范围、降低参数空间的维度。不过这一次设置的参数空间都较为密集,参数也较多,大家在实际进行设置的时候可以选择与我设置的不同的范围或密度。
2 基于TEP对XGBoost进行优化
python
#日常使用库与算法
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("train_encode.csv",index_col=0)
X = data.iloc[:,:-1]
y = data.iloc[:,-1]
Step 1.建立benchmark
算法 | RF (TPE) | AdaBoost (TPE) | GBDT (TPE) |
---|---|---|---|
5折验证 运行时间 | 0.22s | 0.27s | 1.54s(↑) |
测试最优分数 (RMSE) | 28346.673 | 35169.730 | 26415.835(↓) |
Step 2.定义目标函数、参数空间、优化函数、验证函数
目标函数
python
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(params,data_xgb, seed=1412, metrics=("rmse")
,num_boost_round=int(params["num_boost_round"]))
return result.iloc[-1,2]
参数空间
python
param_grid_simple = {'num_boost_round': hp.quniform("num_boost_round",50,200,10)
,"eta": hp.quniform("eta",0.05,2.05,0.05)
,"booster":hp.choice("booster",["gbtree","dart"])
,"colsample_bytree":hp.quniform("colsample_bytree",0.3,1,0.1)
,"colsample_bynode":hp.quniform("colsample_bynode",0.1,1,0.1)
,"gamma":hp.quniform("gamma",1e6,1e7,1e6)
,"lambda":hp.quniform("lambda",0,3,0.2)
,"min_child_weight":hp.quniform("min_child_weight",0,50,2)
,"max_depth":hp.choice("max_depth",[*range(2,30,2)])
,"subsample":hp.quniform("subsample",0.1,1,0.1)
,"objective":hp.choice("objective",["reg:squarederror","reg:squaredlogerror"])
,"rate_drop":hp.quniform("rate_drop",0.1,1,0.1)
}
优化函数
python
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
Step 3.训练贝叶斯优化器
XGBoost中涉及到前所未有多的随机性,因此模型可能表现得极度不稳定,我们需要多尝试几次贝叶斯优化来观察模型的稳定性。因此在这里我们完成了5次贝叶斯优化,查看如下的结果:
python
params_best, trials = param_hyperopt(100) #由于参数空间巨大,给与100次迭代的空间
Step 4.验证参数
验证函数
python
def hyperopt_validation(params):
paramsforxgb = {"eta":params["eta"]
,"booster":"dart"
,"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"]
,"rate_drop":params["rate_drop"]
,"nthred":14
,"verbosity":0
,"seed":1412}
result = xgb.cv(params,data_xgb, seed=1412, metrics=("rmse")
,num_boost_round=int(params["num_boost_round"]))
return result.iloc[-1,2]
bestparams = {'colsample_bynode': 0.45
, 'colsample_bytree': 1.0
, 'eta': 0.05
, 'gamma': 13000000.0
, 'lambda': 0.5
, 'max_depth': 6
, 'min_child_weight': 0.5
, 'num_boost_round': 150.0
, 'rate_drop': 0.65
, 'subsample': 0.8500000000000001}
算法 | RF (TPE) | AdaBoost (TPE) | GBDT (TPE) | XGB (TPE) |
---|---|---|---|---|
5折验证 运行时间 | 0.22s | 0.27s | 1.54s(↑) | 1.14s(↓) |
测试最优分数 (RMSE) | 28346.673 | 35169.730 | 26415.835(↓) | 25368.487(↓) |