xgboost实例_XGBoost:从精通到放弃

XGBoost详解


1.CART回归树

CART回归树是假设树为二叉树,通过不断将特征进行分裂。比如当前树结点是基于第j个特征值进行分裂的,设该特征值小于s的样本划分为左子树,大于s的样本划分为右子树。

CART回归树实质上就是在该特征维度对样本空间进行划分, 而这种空间划分的优化是一种NP难问题,因此在决策树模型中是使用启发式方法解决。典型CART回归树产生的目标函数为:

当我们为了求解最优的切分特征j和最优的切分点s,就转化为求解这么一个目标函数:

2.XGBoost数学原理推导

该算法思想就是不断地添加树,不断地进行特征分裂来生长一棵树,每次添加一个树,其实是学习一个新函数,去拟合上次预测的残差。当我们训练完成得到k棵树,我们要预测一个样本的分数,其实就是根据这个样本的特征,在每棵树中会落到对应的一个叶子节点,每个叶子节点就对应一个分数,最后只需要将每棵树对应的分数加起来就是该样本的预测值。

如下图例子,训练出了2棵决策树,小孩的预测分数就是两棵树中小孩所落到的结点的分数相加。爷爷的预测分数同理。

69a56c2172f0f783d38c9cdd76e0d86a.png
爷爷的预测分数

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 个叶子节点中

449a116b932ff33c29e88eb970603a9b.pngb759d112044073e5a6583985b504e93d.png159ed1ccd06279f5f35062c8b3ab975f.png

3.手动还原XGBoost实例过程

aae119c06d8106e12b8338d9adddc1fb.png
e4e5764a7e437125df3dccb5112db2b3.png
5375082d13e6b8e837edc8b6e1361189.png
6c679c46b6800ed41073b705b4aad843.png
87d1ac6d57366b28f4aa258efd1531a8.png
35f5214ecfdf69a3335b39e7da14cf4f.png
44166c2b97ed03eeabba6e38f52421e3.png
9065ad985d4e81cca5707cbfc676b265.png
49b6289e4f41e66e19758e4295af259a.png
b0eddfb39309f587e2eaf125873b3779.png
3d16c2082e33d0a10fb14674c0208b82.png
a0b05e6411462cb7319e44b66e3ef2e4.png
448c687c1d8b484bcd99a5fb8d97c435.png
c8326c4ce6243d1a24a5d7fb6185c5e4.png
9ebabaa563ebfbf225d36b8e11e6f3c0.png
0e3390e7a38f033252a517793f3ea66b.png
e48ee4f050068c68dcd773367e015c31.png

54165240f54ecbc18a853fbb42ea83c8.pngb07d8bf3115d14c10e0ac8df7e818aab.png

9ef11d1bcd51799af84f4a6541f1861c.png
9ef11d1bcd51799af84f4a6541f1861c.png
5146f1c2ce787716799f530babc4dfad.png
9165b132c0483c0134e945672dd14cdd.png
04ba6a3ceaaa1a7b266f90bdefebe088.png
a3f9538126fa12fc703ee07ff8404512.png
b9a35012e7be68e13991a0c47117bb83.png
dcdc15be6073cffbcf313622e3580f40.png
6e85b50aceb933752f1f243e687503c2.png
afdc834cf8f89d2bab57e738cb1459e7.png
363bb9399a88bda450890767289f0661.png
0e3390e7a38f033252a517793f3ea66b.png
31b4ae3e1f610939080a28352191a471.png
ddb6d516112e1028dce17a8343d4ca0e.png
967e4090d5079a514ee69a5070ef3dea.png
cab759d7a0a77e674fda70adfab12561.png
891a17fcae0b8b289e6787ae003bce24.png

6.xgboost与传统GBDT的区别与联系?

总结一下xgboostGBDT的区别以及联系。

区别:

  1. xgboostGBDT的一个区别在于目标函数上。在xgboost中,损失函数+正则项。GBDT中一般只有损失函数。

  2. xgboost中利用二阶导数的信息,而GBDT只利用了一阶导数, 即在GBDT回归中利用了残差的概念。

  3. xgboost在建树的时候利用的准则来源于目标函数推导,即可以理解为牛顿法。而GBDT建树利用的是启发式准则。

  4. xgboost中可以自动处理空缺值,自动学习空缺值的分裂方向,GBDT(sklearn版本)不允许包含空缺值。

相似点:

  1. xgboostGBDT的学习过程都是一样的,都是基于Boosting的思想,先学习前n-1个学习器,然后基于前n-1个学习器学习第n个学习器。而且其都是将损失函数和分裂点评估函数分开了。

  2. 建树过程都利用了损失函数的导数信息,。

  3. 都使用了学习率来进行Shrinkage,从前面我们能看到不管是GBDT还是xgboost,我们都会利用学习率对拟合结果做缩减以减少过拟合的风险。

7.Python实现

安装

pip install xgboost

说明

xgboost是一个独立的包,可以与numpysklearn兼容,可以直接使用sklearnGridSearchCV进行参数寻优,具体代码如下:

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_

c3b00c7522c34662e83def91e960718c.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值