XGBoost详解
1.CART回归树
CART
回归树是假设树为二叉树,通过不断将特征进行分裂。比如当前树结点是基于第j
个特征值进行分裂的,设该特征值小于s
的样本划分为左子树,大于s
的样本划分为右子树。
而CART
回归树实质上就是在该特征维度对样本空间进行划分, 而这种空间划分的优化是一种NP
难问题,因此在决策树模型中是使用启发式方法解决。典型CART
回归树产生的目标函数为:
当我们为了求解最优的切分特征j
和最优的切分点s
,就转化为求解这么一个目标函数:
2.XGBoost数学原理推导
该算法思想就是不断地添加树,不断地进行特征分裂来生长一棵树,每次添加一个树,其实是学习一个新函数,去拟合上次预测的残差。当我们训练完成得到k棵树,我们要预测一个样本的分数,其实就是根据这个样本的特征,在每棵树中会落到对应的一个叶子节点,每个叶子节点就对应一个分数,最后只需要将每棵树对应的分数加起来就是该样本的预测值。
如下图例子,训练出了2棵决策树,小孩的预测分数就是两棵树中小孩所落到的结点的分数相加。爷爷的预测分数同理。
XGBoost考虑正则化项,目标函数定义如下:
其中
很显然代表损失函数,而代表正则项
其中为预测输出,为label的值,为第树模型,为树叶子节点数,为叶子权重值,为叶子树惩罚正则项,具有剪枝作用,为叶子权重惩罚正则项,防止过拟合。XGBoost也支持一阶正则化,容易优化叶子节点权重为0,不过不常用。
是叶子结点的分数,是叶子节点的编号,是其中一颗回归树。也就是说对于任意的一个样本,其最后会落在树的某个叶子节点上。其值为
正如上文所说,新生成的数是要拟合上次预测的残差的,即当生成棵树后,预测分数可以写成:
同时可以将目标函数改写成(其中代表当前树上某个叶子节点上的值)
很明显,我们接下来就是要去找到一个能够最小化目标函数。CXGBoost的想法是利用其在处的泰勒二阶展开近似它。所以目标函数近似为:
其中为一阶导数,为二阶导数,其中前面的为泰勒二阶展开后的二阶导数的系数:
为什么此处可以用二阶导数呢?
首先我们需要明确的是一个概念,我们的boosting每一轮迭代是在优化什么呢?换句话说我们在用损失函数在干什么呢?其实我们看Boosting的框架,无论是GBDT还是Adaboost,其在每轮迭代中,根本就没有理会损失函数具体是什么,仅仅用到了损失函数的一阶导数。仅仅用一阶导数的问题就是,我们根本无法保证我们找到的是全局最优。除非问题本身是强凸的而且最好是smooth的。每轮迭代相当于就是最优化负梯度。即下式中的,因为是负梯度所以前面要加负号,代表学习率。
有没有感觉这个公式形式很熟悉,是不是就是一般常见的linear regression的stochastic梯度更新公式。既然GBDT用的是Stochastic Gradient Descent,我们回想一下,是不是还有别的梯度更新的公式,这时,牛顿法Newton's method就出现了,可以说,XGBoost是用了牛顿法进行的梯度更新。
我们先来看一下泰勒展开式:
而对于上面这个公式值,其与之间的误差,可以用如下公式表示:
我们保留二阶泰勒展开:
这个式子是成立的。当且仅当趋近于0,我们对上式求导(对求导)并令其导数为0.
即得到:
所以得出迭代公式:
将损失函数与对应起来:
所以实际上即为,而即为。故对求导数时即对求偏导,故根据二阶泰勒展开,可以表示为:
用和替代上式中的和,即得到:
这里有必要在此明确一下,和的含义,怎么理解呢?假设现有棵树,这棵树组成的模型对第个训练样本有一个预测值。这个与第个样本的真实标签肯定有差距,这个差距可以用这个损失函数来衡量。所以此处的和就是对于该损失函数的一阶导数和二阶导数。
我们来看一个具体的例子,假设我们正在优化第11棵CART树,也就是说前10棵CART树已经确定了。这10棵树对于样本的预测值是,假设我们现在是做分类,我们的损失函数是:
在类别标签时,故此时损失函数变成了:
我们可以求出这个损失函数对于的梯度,如下所示:
将代入上面的式子,计算得到。这个就是。该值就是负的,也就是说,如果我们想要减少这10棵树在该样本点上的预测损失,我们应该沿着梯度反方向去走,也就是要增大的值,使其趋向于正,因为我们的就是正的。
那么在优化第棵树时,有多少个和要计算?嗯,没错就是各有个,是训练样本的数量,如果有10万个样本,在优化第棵树时,就要计算出10万个和。很显然,这10万个之间并没有什么关系,那么是不是可以并行计算呢?这就是XGBoost速度如此之快的原因。
再总结一下,和是可以并行的求出的,而且,在并行计算的时候和是不依赖于损失函数的形式的,只要这个损失函数二次可微就可以了。这有什么好处呢?好处就是XGBoost可以支持自定义损失函数,只需要,满足二次可微即可。
不过此处所说的不依赖损失函数形式的意思不是说在自定义损失函数的时候,给XGBoost传入一个损失函数,XGBoost就会自动计算其一阶和二阶导数。相反的我们需要给XGBoost传入一个损失函数,同时还需要给XGBoost传入这个损失函数的一阶导和二阶导的形式。一阶二阶导数和损失函数之间其实算是独立的,XGBoost在每次并行计算的时候独立调用损失函数和一阶二阶导数。
我们来看一下python接口自定义损失函数调用XGBoost的方式(代码地址:
#coding:utf-8
import numpy as np
import xgboost as xgb
print('start running example to used customized objective function')
dtrain = xgb.DMatrix('../data/agaricus.txt.train')
dtest = xgb.DMatrix('../data/agaricus.txt.test')
param = {'max_depth': 2, 'eta': 1, 'silent': 1}
watchlist = [(dtest, 'eval'), (dtrain, 'train')]
num_round = 2
#下面这部分就是自定义损失函数, 可以看到, 函数接收两个向量, 一个是预测值pred, 一个是训练数据, 训练数据中包含标签值。在这个函数中, 计算了当前预测值和标签值的损失函数的一阶梯度和二阶导, 并将计算结果返回
#可以看出, 返回的是一阶导和二阶导, 二阶导相当于是Hessian矩阵, 所以此处用了hess作为变量名。很显然, 既然这么设计函数的话, logregobj是可以并行的, 只要每个并行的程序自己调用logregobj即可, 他们之间完全可以互不影响。
def logregobj(preds, dtrain):
labels = dtrain.get_label()
preds = 1.0 / (1.0 + np.exp(-preds))
grad = preds - labels
hess = preds * (1.0 - preds)
return grad, hess
#在上面定义完一阶导和二阶导后, 我们此时要计算损失函数, 需要注意的是, 上面的一阶导和二阶导都是矩阵形式的, 不过此处的损失函数的返回值需要是单一的值, 又可称作margin值。回想一下树形结构的公式, 在每个分裂的节点, 该叶节点都对应一个值, 这个值是落在这个叶节点下所有样本的综合值。
def evalerror(preds, dtrain):
labels = dtrain.get_label()
# return a pair metric_name, result. The metric name must not contain a colon (:) or a space
# since preds are margin(before logistic transformation, cutoff at 0)
return 'my-error', float(sum(labels != (preds > 0.0))) / len(labels)
# training with customized objective, we can also do step by step training
# simply look at xgboost.py's implementation of train
#在obj中传入返回一阶二阶导的函数, 在feval中传入损失函数。
bst = xgb.train(param, dtrain, num_round, watchlist, obj=logregobj, feval=evalerror)
我们回到之前的公式推导环节, 我们已经理解了一阶二阶导, 现在来看正则化项:
其实正则为什么可以控制模型复杂度呢?有很多角度可以看这个问题,最直观就是,我们为了使得目标函数最小,自然正则项也要小,正则项要小,叶子节点个数T要小(叶子节点个数少,树就简单)。
而为什么要对叶子节点的值进行L2正则,这个可以参考一下LR里面进行正则的原因,简单的说就是(对此困惑的话可以跑一个不带正则的LR,每次出来的权重w都不一样,但是loss都是一样的,加了L2正则后,每次得到的w都是一样的)
由于前t-1棵树的预测分数与y的残差对目标函数优化不影响, 可以直接去掉, 所以有:
上式是将每个样本的损失函数值加起来,我们知道,每个样本都最终会落到一个叶子结点中,所以我们可以将所有同一个叶子结点的样本重组起来,过程如下:
其中为落入叶子所有样本一阶梯度统计值总和,为落入叶子所有样本二阶梯度统计值总和
因为XGBoost的基本框架是boosting, 也就是一棵树接着一棵树的形式, 所以此处的中的t代表第t颗树, 注意上式中 j 代表树的一个叶子节点, i 也代表一个叶子节点, 根据树的判别条件, n 个样本点被分到了 T 个叶子节点中
3.手动还原XGBoost实例过程
6.xgboost与传统GBDT的区别与联系?
总结一下xgboost
和GBDT
的区别以及联系。
区别:
xgboost
和GBDT
的一个区别在于目标函数上。在xgboost
中,损失函数+正则项。GBDT
中一般只有损失函数。xgboost
中利用二阶导数的信息,而GBDT
只利用了一阶导数, 即在GBDT回归中利用了残差的概念。xgboost
在建树的时候利用的准则来源于目标函数推导,即可以理解为牛顿法。而GBDT
建树利用的是启发式准则。xgboost
中可以自动处理空缺值,自动学习空缺值的分裂方向,GBDT(sklearn版本)
不允许包含空缺值。
相似点:
xgboost
和GBDT
的学习过程都是一样的,都是基于Boosting
的思想,先学习前n-1
个学习器,然后基于前n-1
个学习器学习第n
个学习器。而且其都是将损失函数和分裂点评估函数分开了。建树过程都利用了损失函数的导数信息,。
都使用了学习率来进行
Shrinkage
,从前面我们能看到不管是GBDT
还是xgboost
,我们都会利用学习率对拟合结果做缩减以减少过拟合的风险。
7.Python实现
安装
pip install xgboost
说明
xgboost
是一个独立的包,可以与numpy
和sklearn
兼容,可以直接使用sklearn
的GridSearchCV
进行参数寻优,具体代码如下:
import numpy as np
import pandas as pd
import xgboost as xgb
from sklearn.model_selection import KFold
from sklearn.model_selection import GridSearchCV
inputx = inputs.values
inputy = target.values
kf = KFold(10)
datasets = [(train_index, test_index)
for train_index, test_index in kf.split(inputx)]
cv_params = {
'n_estimators': range(100, 1000, 50), ### 梯度增强树的数量
'learning_rate': [0.01, 0.05, 0.07, 0.1, 0.2], ### 学习率
'max_depth': [3, 4, 5, 6, 7, 8, 9, 10], ### 基学习器的最大树深度
'gamma': [0.1, 0.2, 0.3, 0.4, 0.5, 0.6], ### 在树的叶节点上进行进一步分区所需的最小损失减少
'reg_alpha': [0.05, 0.1, 1, 2, 3], ### 权重的L1正则化项
'reg_lambda': [0.05, 0.1, 1, 2, 3] ### 权重的L2正则化项
}
# cv_params = {'max_depth': range(4)}
result_json = {}
model = xgb.XGBRegressor(n_jobs=10)
model = GridSearchCV(model, cv_params, cv=datasets, n_jobs=10)
model.fit(inputx, inputy)
best_params = model.best_params_
cv_results = model.cv_results_
best_score = model.best_score_
result_json['best_params'] = [best_params]
result_json['cv_results'] = [cv_results]
result_json['best_score'] = [best_score]
save_model(result_json,'./result/best_result.pkl')
save_model(model.best_estimator_, './result/xgb_best_model.pkl')
model = model.best_estimator_