XGBoost原理简介

一、简述

       这里先简单介绍下RF(Random Forest)、GBDT(Gradient Boosting Decision Tree)和XGBoost算法的原理。
       RF:从M个训练样本中随机选取m个样本,从N个特征中随机选取n个特征,然后建立一颗决策树。这样训练出T棵树后,让这k颗树对测试集进行投票产生决策值。RF是一种bagging的思路。可以并行化处理。
       GBDT:总共构建T棵树。当构建到第t棵树的时候,需要对前t-1棵树的训练样本分类回归产生的残差进行拟合。每次构建树的方式以及数据集一样,只不过拟合的目标变成了t-1棵树输出的残差。不可并行化处理。GBDT是一种boosting算法。
       XGBoost:总共构建T颗树。当构建到第t颗树的时候,需要对前t-1颗树对训练样本分类回归产生的残差进行拟合。每次拟合产生新的树的时候,遍历所有可能的树,并选择使得目标函数值(cost)最小的树。但是这样在实践中难以实现,因此需要将步骤进行分解,在构造新的树的时候,每次只产生一个分支,并选择最好的那个分支。如果产生分支的目标函数值(cost)比不产生的时候大或者改进效果不明显,那么就放弃产生分支(相当于truncate,截断)。可以并行化处理,效率比GBDT高,效果比GBDT好。XGBoost是一种boosting算法。

二、原理介绍

1、基学习器 – 分类和回归树(CART)

       Boosted tree最基本的组成部分叫做回归树(regression tree),也叫做CART5。
XGBoost原理简介
       上面就是一个CART的例子。CART会根据输入的属性把输入分配到各个叶子节点,而每个叶子节点上面都会对应一个实数分数。上面的例子是预测一个人是否会喜欢电脑游戏的CART,你可以把叶子的分数理解为这个人喜欢电脑游戏的程度。有人可能会问它和decision tree的关系,其实我们可以简单地把它理解为decision tree的一个扩展。从简单的类标记到分数之后,我们可以做很多事情,如概率预测,排序。

2、Tree Ensemble

       一个CART往往过于简单无法进行有效地预测,因此一个更加强力的模型叫做Tree Ensemble。 如下图,我们用两棵树进行预测,对于每个样本的预测结果就是每棵树预测分数的和。
XGBoost原理简介
       常见的随机森林和boosted tree的模型都是Tree Ensemble,只是构造(学习)模型参数的方法不同;同时这里的参数对应了树的结构,以及每个叶子节点上面的预测分数;最后我们通过定义一个合理的目标函数,并且尝试优化这个目标函数来进行求解。
       对于Tree Ensemble,我们可以严格的把模型写成是:
yi^=k=1Kfk(xi),fkΓ\hat{y_{i}}=\sum_{k=1}^{K}f_{k}(x_{i}), f_{k}\in \Gamma
       其中每个ff是一个在函数空间Γ\Gamma里面的函数,而Γ\Gamma对应了所有regression tree的集合。我们设计的目标函数也需要遵循前面的主要原则,包含两部分:前半部分i=1nl(yi,yi^)\sum_{i=1}^{n}l(y_{i},\hat{y_{i}})是损失函数,用于描述模型拟合数据的程度;后半部分k=1KΩ(fk)\sum_{k=1}^{K}\Omega (f_{k})是正则项,用于控制模型的复杂度。
Obj(θ)=i=1nl(yi,yi^)+k=1KΩ(fk)Obj(\theta )=\sum_{i=1}^{n}l(y_{i},\hat{y_{i}})+\sum_{k=1}^{K}\Omega (f_{k})

3、模型学习 – Additive Training

       第一部分是损失函数,比较常见的是平方误差,Logistic Loss等。第二部分是每棵树的复杂程度的和。因为现在我们的参数可以认为是在一个函数空间里面,我们不能采用传统的如SGD之类的算法来学习我们的模型,因此我们会采用一种叫做Additive Training的方式。每一次保留原来的模型不变,加入一个新的函数ff到我们的模型中。
yi^(0)=0yi^(1)=f1(xi)=yi^(0)+f1(xi)yi^(2)=f1(xi)+f2(xi)=yi^(1)+f2(xi)...yi^(t)=k=1tfk(xi)=yi^(t1)+ft(xi)\hat{y_{i}}^{(0)}=0\\ \hat{y_{i}}^{(1)}=f_{1}(x_{i})=\hat{y_{i}}^{(0)}+f_{1}(x_{i})\\ \hat{y_{i}}^{(2)}=f_{1}(x_{i})+f_{2}(x_{i})=\hat{y_{i}}^{(1)}+f_{2}(x_{i})\\ ...\\ \hat{y_{i}}^{(t)}=\sum_{k=1}^{t}f_{k}(x_{i})=\hat{y_{i}}^{(t-1)}+f_{t}(x_{i})
其中,yi^(t)\hat{y_{i}}^{(t)}是第tt轮的模型预测,yi^(t1)\hat{y_{i}}^{(t-1)}保留t1t-1轮的模型预测,ft(xi)f_{t}(x_{i})是新加入的函数。
       接下来,我们需要确定一个ff来使得我们的目标函数尽量最大地降低。
Obj(t)=i=1nl(yi,yi^(t))+i=1tΩ(fi)=i=1nl[yi,yi^(t1)+ft(xi)]+Ω(ft)+constantObj^{(t)}=\sum_{i=1}^{n}l(y_{i},\hat{y_{i}}^{(t)})+\sum_{i=1}^{t}\Omega (f_{i})\\ =\sum_{i=1}^{n}l\left [ y_{i},\hat{y_{i}}^{(t-1)}+f_{t}(x_{i}) \right ]+\Omega (f_{t})+constant
这个公式比较抽象,下面我们考虑当ll是平方误差的情况。此时,目标函数被写成如下的二次函数:
Obj(t)=i=1nl[yi,yi^(t1)+ft(xi)]+Ω(ft)+constant=i=1n{yi[yi^(t1)+ft(xi)]}2+Ω(ft)+constant=i=1n[2(yi^(t1)yi)ft(xi)+ft(xi)2]+Ω(ft)+constantObj^{(t)}=\sum_{i=1}^{n}l\left [ y_{i},\hat{y_{i}}^{(t-1)}+f_{t}(x_{i}) \right ]+\Omega (f_{t})+constant\\ =\sum_{i=1}^{n}\left \{ y_{i} - \left [ \hat{y_{i}}^{(t-1)} + f_{t}(x_{i}) \right ] \right \}^{2}+\Omega (f_{t})+constant\\ =\sum_{i=1}^{n}\left [ 2(\hat{y_{i}}^{(t-1)}-y_{i})f_{t}(x_{i}) + f_{t}(x_{i})^{2} \right ]+\Omega (f_{t})+constant\\
其中2(yi^(t1)yi)2(\hat{y_{i}}^{(t-1)}-y_{i})一般叫做残差。
       更一般地,对于不少平方误差的情况,我们会采用如下泰勒展开来近似地定义一个目标函数,方便我们进行计算。
Obj(t)=i=1nl[yi,yi^(t1)+ft(xi)]+Ω(ft)+constantf(x+Δx)f(x)+f(x)Δx+12f(x)Δx2gi=yi^(t1)l(yi,y^(t1)),hi=yi^(t1)2l(yi,y^(t1))目标:Obj^{(t)}=\sum_{i=1}^{n}l\left [ y_{i},\hat{y_{i}}^{(t-1)}+f_{t}(x_{i}) \right ]+\Omega (f_{t})+constant\\ 泰勒展开:f(x+\Delta x)\cong f(x)+f^{'}(x)\Delta x + \frac{1}{2}f^{''}(x)\Delta x^{2}\\ 定义:g_{i}=\partial _{\hat{y_{i}}^{(t-1)}}l(y_{i},\hat{y}^{(t-1)}), h_{i}=\partial^{2} _{\hat{y_{i}}^{(t-1)}}l(y_{i},\hat{y}^{(t-1)}) \\
因此,
Obj(t)i=1n[l(yi,yi^(t1))+gift(xi)+12hift2(xi)]+Ω(ft)+constantObj^{(t)}\cong \sum_{i=1}^{n}\left [ l(y_{i}, \hat{y_{i}}^{(t-1)}) + g_{i}f_{t}(x_{i})+\frac{1}{2}h_{i}f_{t}^{2}(x_{i}) \right ]+\Omega (f_{t})+constant
       当我们把常数项移除之后,我们会发现如下一个比较统一的目标函数。这一个目标函数有一个非常明显的特点,它只依赖于每个数据点的在误差函数上的一阶导数和二阶导数。
i=1n[gift(xi)+12hift2(xi)]+Ω(ft)gi=yi^(t1)l(yi,y^(t1)),hi=yi^(t1)2l(yi,y^(t1)) \sum_{i=1}^{n}\left [g_{i}f_{t}(x_{i})+\frac{1}{2}h_{i}f_{t}^{2}(x_{i}) \right ]+\Omega (f_{t})\\ 其中,g_{i}=\partial _{\hat{y_{i}}^{(t-1)}}l(y_{i},\hat{y}^{(t-1)}), h_{i}=\partial^{2} _{\hat{y_{i}}^{(t-1)}}l(y_{i},\hat{y}^{(t-1)})
       通过上述推导,可以使我们很清楚地理解整个目标是什么,并且一步一步推导出如何进行树的学习。 这一个抽象的形式对于实现机器学习工具也是非常有帮助的,这样一个形式包含所有可以求导的目标函数。也就是说有了这个形式,我们写出来的代码可以用来求解包括回归,分类和排序的各种问题,正式的推导可以使得机器学习的工具更加一般。

4、树的复杂度

       接下来我们对目标函数中的第二部分进行探讨,也即是讨论如何定义树的复杂度。我们先对于ff的定义做一下细化,把树拆分成结构部分qq和叶子权重部分ww。下 图是一个具体的例子。结构函数qq把输入映射到叶子的索引号上面去,而ww给定了每个索引号对应的叶子分数是什么。
XGBoost原理简介
       当我们给定了如上定义之后,我们可以定义一棵树的复杂度如下。这个复杂度包含了一棵树里面节点的个数,以及每个树叶子节点上面输出分数的L2L2模平方。 当然这不是唯一的一种定义方式,不过这一定义方式学习出的树效果一般都比较不错。下图还给出了复杂度计算的一个例子。
XGBoost原理简介

5、目标函数求解

       结合损失函数和树的复杂度定义,我们可以把目标函数写成如下形式,其中II被定义为每个叶子上面样本集合Ij={iq(xi)=j}I_{j}=\left \{ i|q(x_{i})=j \right \}
Obj(t)i=1n[gift(xi)+12hift2(xi)]+Ω(ft)=i=1n[giwq(xi)+12hiwq(xi)2]+γT+12λj=1Twj2=j=1T[(iIjgi)wj+12(iIjhi+λ)wj2]+γT Obj^{(t)}\cong\sum_{i=1}^{n}\left [g_{i}f_{t}(x_{i})+\frac{1}{2}h_{i}f_{t}^{2}(x_{i}) \right ]+\Omega (f_{t})\\ =\sum_{i=1}^{n}\left [g_{i}w_{q(x_{i})}+\frac{1}{2}h_{i}w_{q(x_{i})}^{2}\right ]+\gamma T+\frac{1}{2}\lambda \sum_{j=1}^{T}w_{j}^{2}\\ =\sum_{j=1}^{T}\left [(\sum_{i\in I_{j}}g_{i})w_{j}+\frac{1}{2}(\sum_{i\in I_{j}}h_{i}+\lambda )w_{j}^{2}\right ]+\gamma T
       接下来,我们定义
Gj=iIjgi,Hj=iIjhiG_{j}=\sum_{i\in I_{j}}g_{i},H_{j}=\sum_{i\in I_{j}}h_{i}
因此,目标函数可以进一步改写成如下的形式:
Obj(t)=j=1T[(iIjgi)wj+12(iIjhi+λ)wj2]+γT=j=1T[Gjwj+12(Hj+λ)wj2]+γT Obj^{(t)}=\sum_{j=1}^{T}\left [(\sum_{i\in I_{j}}g_{i})w_{j}+\frac{1}{2}(\sum_{i\in I_{j}}h_{i}+\lambda )w_{j}^{2}\right ]+\gamma T\\ =\sum_{j=1}^{T}\left [G_{j}w_{j}+\frac{1}{2}(H_{j}+\lambda )w_{j}^{2}\right ]+\gamma T
       如果确定了树的结构(即确定了q(x)q(x)),为了使目标函数最小,可以令其导数为0,求解结果如下(wjw_{j}^{*}是最好的ww值,ObjObj是这个ww对应的目标函数的值):
wj=GjHj+λw_{j}^{*}=-\frac{G_{j}}{H_{j}+\lambda }
Obj=12j=1TGj2Hj+λ+γTObj=-\frac{1}{2}\sum_{j=1}^{T}\frac{G_{j}^{2}}{H_{j}+\lambda }+\gamma T

6、打分函数计算举例

       ObjObj代表了当我们指定一个树的结构的时候,我们在目标上面最多减少多少。我们可以把它叫做结构分数(structure score)。你可以认为这个就是类似吉尼系数一样更加一般的对于树结构进行打分的函数。下面是一个具体的打分函数计算的例子。
XGBoost原理简介

7、树节点分裂方法

7.1 暴力枚举

       遍历所有特征的所有可能的分割点,计算gain值,选取值最大的(feature,value)去分割。
       为了达到这个目标,split finding算法会在所有特征 (features) 上,枚举所有可能的划分(splits)。为了更高效,该算法必须首先根据特征值对数据进行排序,以有序的方式访问数据来枚举Gain公式中的结构得分 (structure score) 的梯度统计 (gradient statistics)。
XGBoost原理简介

7.2 近似算法

       对于每个特征,只考察分位点,减少计算复杂度。
       该算法会首先根据特征分布的百分位数 (percentiles of feature distribution),提出候选划分点 (candidate splitting points)。接着,该算法将连续型特征映射到由这些候选点划分的分桶(buckets) 中,聚合统计信息,基于该聚合统计找到在 proposal 间的最优解。
       Global:学习每棵树前,提出候选切分点;
       Local:每次分裂前,重新提出候选切分点;
XGBoost原理简介
算法讲解
       第一个for循环做的工作:对特征K根据该特征分布的分位数找到切割点的候选集合Sk={sk1,sk2...skl}S_{k}=\left \{ s_{k1},s_{k2}...s_{kl} \right \},这样做的目的是提取出部分的切分点不用遍历所有的切分点。其中获取某个特征K的候选切割点的方式叫proposal。主要有两种proposal方式:global proposal和local proposal。
       第二个for循环的工作:将每个特征的取值映射到由这些该特征对应的候选点集划分的分桶(buckets)区间sk,vxjksk,v1s_{k,v}\geq x_{jk} \geq s_{k,v-1}中,对每个桶(区间)内的样本统计值 G,H 进行累加统计,最后在这些累计的统计量上寻找最佳分裂点。这样做的主要目的是获取每个特征的候选分割点的 G,H 量。

7.3 稀疏感知的划分查找算法(Sparsity-aware Split Finding)

       在许多现实问题中,输入xx是稀疏的。有多种可能的情况造成稀疏:
(1)数据中的missing values;
(2)统计中常见的零条目;
(3) 特征工程:比如one-hot encoding;
XGBoost原理简介
       上图为带缺省方向的树结构。当在split时相应的feature缺失时,一个样本可以被归类到缺省方向上(缺省方向唯一,非左即右)。
       让算法意识到数据中的稀疏模式很重要。为了这么做,我们提出了在每个树节点上增加一个缺省的方向(default direction),如上图所示。当稀疏矩阵中的值缺失时,样本实例被归类到缺省方向上。在每个分枝上,缺省方向有两种选择 (左分支和右分支)。最优的缺省方向可以从数据中学到。如算法3所示。关键的改进点是:只访问非缺失的条目IkI_{k}
       至于缺省值的实例分到哪个分支方向,如何学习的问题。稀疏感知的划分查找算法计算切分后的评判标准 Gain,然后再通过将实例以枚举的方式,枚举项为默认分到左分支和默认分到右分支,计算切分前后相差最大的Score,然后将缺失值的实例分到该分支上。
XGBoost原理简介

三、五、XGBoost的优点

(1)高度灵活性
       传统GBDT以CART作为基分类器,XGBoost还支持线性分类器,这个时候XGBoost相当于带L1和L2正则化项的逻辑斯蒂回归(分类问题)或者线性回归(回归问题)。传统GBDT在优化时只用到一阶导数信息,XGBoost则对代价函数进行了二阶泰勒展开,同时用到了一阶和二阶导数。顺便提一下,xgboost工具支持自定义代价函数,只要函数可一阶和二阶求导。

(2)正则化
       xgboost在代价函数里加入了正则项,用于控制模型的复杂度。正则项里包含了树的叶子节点个数、每个叶子节点上输出的score的L2模的平方和。从Bias-variance tradeoff角度来讲,正则项降低了模型的variance,使学习出来的模型更加简单,防止过拟合,这也是XGBoost优于传统GBDT的一个特性。

(3)Shrinkage(缩减)
       相当于学习速率(XGBoost中的eta)。XGBoost在进行完一次迭代后,会将叶子节点的权重乘上该系数,主要是为了削弱每棵树的影响,让后面有更大的学习空间。实际应用中,一般把eta设置得小一点,然后迭代次数设置得大一点。

(4)剪枝
       当分裂时遇到一个负损失时,GBM会停止分裂。因此GBM实际上是一个贪心算法。XGBoost会一直分裂到指定的最大深度(max_depth),然后回过头来剪枝。如果某个节点之后不再有正值,它会去除这个分裂。 这种做法的优点,当一个负损失(如-2)后面有个正损失(如+10)的时候,就显现出来了。GBM会在-2处停下来,因为它遇到了一个负值。但是XGBoost会继续分裂,然后发现这两个分裂综合起来会得到+8,因此会保留这两个分裂。

(5)缺失值处理
       对缺失值的处理。对于特征的值有缺失的样本,XGBoost内置处理缺失值的规则,可以自动学习出它的分裂方向。

(6)并行操作
       XGBoost工具支持并行。boosting不是一种串行的结构吗?怎么并行的?注意XGBoost的并行不是tree粒度的并行,XGBoost也是一次迭代完才能进行下一次迭代的(第t次迭代的代价函数里包含了前面t-1次迭代的预测值)。XGBoost的并行是在特征粒度上的。
       我们知道,决策树的学习最耗时的一个步骤就是对特征的值进行排序(因为要确定最佳分割点),XGBoost在训练之前,预先对数据进行了排序,然后保存为block结构,后面的迭代中重复地使用这个结构,大大减小计算量。这个block结构也使得并行成为了可能,在进行节点的分裂时,需要计算每个特征的增益,最终选增益最大的那个特征去做分裂,那么各个特征的增益计算就可以开多线程进行。
       可并行的近似直方图算法。树节点在进行分裂时,我们需要计算每个特征的每个分割点对应的增益,即用贪心法枚举所有可能的分割点。当数据无法一次载入内存或者在分布式情况下,贪心算法效率就会变得很低,所以XGBoost还提出了一种可并行的近似直方图算法,用于高效地生成候选的分割点。

参考:Newton Boosting Tree 算法原理:详解 XGBoost
RF、GBDT和xgboost原理简述
十分钟入门XGBoost(原理解析、优点介绍、参数详解)