集成学习之Xgboost

XGBoost全名叫(eXtreme Gradient Boosting)极端梯度提升,或者叫极值梯度提升算法,经常被用在一些比赛中,其效果显著。它是大规模并行boosted tree的工具,它是目前最快最好的开源boosted tree工具包。XGBoost 所应用的算法就是 GBDT(gradient boosting decision tree)的改进,既可以用于分类也可以用于回归问题中。GBDT和xgboost在竞赛和工业界使用都非常频繁,GBDT是以决策树(CART)为基学习器的GB算法,xgboost扩展和改进了GDBT,xgboost算法更快,准确率也相对高一些。现在Kaggle 大赛的情况基本是这样的,凡是非结构化数据相关,比如语音、图像,基本都是深度学习获胜,凡是结构化数据上的竞赛,基本都是 XGBoost 获胜。Xgboost可以说集成思想达到顶峰的一个模型,至少目前是这样,所以学习机器学习算法,掌握这个是很有必要的。

学习Xgboost之前,需要了解决策树,集成学习,GBDT等算法的概念,会帮助更好的去理解Xgboost。

Gradient boosting回顾

机器学习中学习算法的目标是为了优化或者说最小化loss Function, Gradient boosting的思想是迭代生多个(M个)弱的模型,然后将每个弱模型的预测结果相加,后面的模型$F_{m+1}(x)$基于前面学习模型F_{m}(x)的的效果生成的,关系如下:$$F_{m+1}(x) = F_{m}(x)+ h(x) , 1 < m < M$$

GB算法的思想很简单,关键是怎么生成$h(x)$。如果目标函数是回归问题的均方误差,很容易想到最理想的$h(x)$应该是能够完全拟合$y - F_{m}(x)$,这就是常说基于残差的学习。残差学习在回归问题中可以很好的使用,但是为了一般性(分类,排序问题),实际中往往是基于loss Function 在函数空间的的负梯度学习,对于回归问题残差和负梯度也是相同的。因此基于Loss Function函数空间的负梯度的学习也称为“伪残差”。

回顾GBDT的回归算法迭代的流程:

输入是训练集样本:$T=\{(x_,y_1),(x_2,y_2), ...,(x_m,y_m)\}$, 最大迭代次数$T$, 损失函数$L$。

输出是强学习器:$f(x)$是一颗回归树。

1) 初始化弱学习器(估计使损失函数极小化的常数值,它是只有一个根节点的树,一般平方损失函数为节点的均值,而绝对损失函数为节点样本的中位数):$$f_0(x) = \underset{c}{arg\; min}\sum\limits_{i=1}^{m}L(y_i, c)$$

2) 对迭代轮数t=1,2,...,T有(即生成的弱学习器个数):

(2.1)对样本i=1,2,...,m,计算负梯度(损失函数的负梯度在当前模型的值将它作为残差的估计,对于平方损失函数为,它就是通常所说的残差;而对于一般损失函数,它就是残差的近似值(伪残差)):$$r_{ti} = -\bigg[\frac{\partial L(y_i, f(x_i)))}{\partial f(x_i)}\bigg]_{f(x) = f_{t-1}\;\; (x)}$$

(2.2)利用$(x_i,r_{ti})\;\; (i=1,2,..m)$,即$\{(x_1,r_{t1}), ...,(x_i,r_{ti})\}$,拟合一颗CART回归树,得到第t颗回归树。其对应的叶子节点区域为$R_{tj},j =1,2,...,J$。其中J为回归树t的叶子节点的个数。

(2.3)对叶子区域j =1,2,..J,计算最佳拟合值:$$c_{tj} = \underset{c}{arg\; min}\sum\limits_{x_i \in R_{tj}} L(y_i,f_{t-1}(x_i) +c)$$

(2.4)更新强学习器:$$f_{t}(x) = f_{t-1}(x) + \sum\limits_{j=1}^{J}c_{tj}I(x \in R_{tj}) $$

3) 得到最终的回归树,即强学习器f(x)的表达式:$$f(x) = f_T(x) =f_0(x) + \sum\limits_{t=1}^{T}\sum\limits_{j=1}^{J}c_{tj}I(x \in R_{tj})$$

从上面可以看出,对于GBDT的第t颗决策树,先是得到负梯度(2.1),或者是泰勒展开式的一阶导数;然后是第一个优化求解(2.2),即基于残差拟合一颗CART回归树,得到J个叶子节点区域;再然后是第二个优化求解(2.3),在第一个优化求解的结果上,对每个节点区域再做一次线性搜索,得到每个叶子节点区域的最优取值;最终得到当前轮的强学习器(2.4)。

从上面可以看出,我们要求解这个问题,需要求解当前决策树最优的所有J个叶子节点区域和每个叶子节点区域的最优解$c_{tj}$。GBDT采样的方法是分两步走,先求出最优的所有J个叶子节点区域,再求出每个叶子节点区域的最优解。

对于XGBoost,它期望把第(2.2)步拟合一个回归树(弱分类器)和第(2.3)步计算最佳拟合值合并在一起做,即一次求解出决策树最优的所有J个叶子节点区域和每个叶子节点区域的最优解$c_{tj}$。下文会具体介绍。

从GBDT到XGBoost

XGBoost作为GBDT的高效实现,XGBoost是一个上限特别高的算法,因此在算法竞赛中比较受欢迎。简单来说,对比原算法GBDT,XGBoost主要从下面三个方面做了优化:

(1). 算法本身的优化:在算法的弱学习器模型选择上,对比GBDT只支持决策树,XGBoost还可以支持很多其他的弱学习器。在算法的损失函数上,除了本身的损失,还加上了正则化部分。在算法的优化方式上,GBDT的损失函数只对误差部分做负梯度(一阶泰勒)展开,而XGBoost损失函数对误差部分做二阶泰勒展开,更加准确。算法本身的优化是我们后面讨论的重点。

(2). 算法运行效率的优化:对每个弱学习器,比如决策树建立的过程做并行选择,找到合适的子树分裂特征和特征值。在并行选择之前,先对所有的特征的值进行排序分组,方便前面说的并行选择。对分组的特征,选择合适的分组大小,使用CPU缓存进行读取加速。将各个分组保存到多个硬盘以提高IO速度。

(3). 算法健壮性的优化:对于缺失值的特征,通过枚举所有缺失值在当前节点是进入左子树还是右子树来决定缺失值的处理方式。算法本身加入了L1和L2正则化项,可以防止过拟合,泛化能力更强。

Xgboost必备概念梳理(知识点)

1. 回顾Boosting集成学习的模型预测函数和损失函数:

预测模型为:

$$\hat{y}_{i} = \sum_{k=1}^{K}f_{k}(x_{i})$$

其中$K$为树的总个数,$f_{k}$表示第$k$颗树,$\hat{y}_{i}$表示样本$x_{i}$的预测结果。

损失函数为:  

$$Obj(\theta) = \sum_{i=1}^{n}l(y_{i},\hat{y_{i}}) + \sum_{k=1}^{K}\Omega(f_{k})$$ 

其中$l(y_{i},\hat{y_{i}}$为样本$x_{i}$的训练误差,$\Omega(f_{k})$表示第$k$棵树的正则项。

【补充】一般的目标函数都包含下面两项:$$Obj(\Theta) = L(\Theta) + \Omega(\Theta )$$

误差/损失函数$ L(\Theta)$:指模型拟合数据的程度;

正则化项$\Omega(\Theta )$:指惩罚复杂模型。

其中,误差/损失函数鼓励我们的模型尽量去拟合训练数据,使得最后的模型会有比较少的 bias。而正则化项则鼓励更加简单的模型。因为当模型简单之后,有限数据拟合出来结果的随机性比较小,不容易过拟合,使得最后模型的预测更加稳定。

2. 回顾加法模型:

其中对于预测模型采用加法策略可以表示如下:

初始化(模型中没有树时,其预测结果为0):$\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})$

                  

往模型中加入第t棵树:$\hat{y}_{i}^{(t)} = \sum_{k=1}^{t}f_{k}(x_{i}) = \hat{y}_{i}^{(t-1)} + f_{t}(x_{i})$

其中$f_{k}$表示第$k$棵树,$\hat{y}_{i}^{(t)}$表示组合$t$棵树模型对样本$x_{i}$的预测结果。

3. 树的结构与复杂度:

从单一的树来考虑。对于其中每一棵回归树,其模型可以写成:$$f_{t}(x) = w_{q(x)},w \in R^T, q : R ^ d  —> \{1,2,...,T \}$$

树拆分成结构部分$q$和叶子权重部分$w$,其中$w$为叶子节点的得分值,$q(x)$表示样本$x$对应的叶子节点。$T$为该树的叶子节点个数。

Xgboost对树的复杂度包含了两个部分:(1)一个是树里面叶子节点的个数$T$;(2)一个是树上叶子节点的得分$w$的$L2$模平方(对$w$进行$L2$正则化,相当于针对每个叶结点的得分增加$L2$平滑,目的是为了避免过拟合)。

因此可以将该树的复杂度写成:$$\Omega(h_t) = \gamma T + \frac{1}{2}\lambda\sum\limits_{j=1}^{T}w_{j}^2$$

其中,$γ$为$L1$正则的惩罚项,$λ$为$L2$正则的惩罚项。

树的复杂度函数和样例:

定义树的结构和复杂度的原因很简单,这样就可以衡量模型的复杂度了啊,从而可以有效控制过拟合。

4. Xgboost中的boosting tree模型:

例如要预测一家人对电子游戏的喜好程度,考虑到年轻和年老相比,年轻更可能喜欢电子游戏,以及男性和女性相比,男性更喜欢电子游戏,故先根据年龄大小区分小孩和大人,然后再通过性别区分开是男是女,逐一给各人在电子游戏喜好程度上打分,如下图所示:

看上图训练出2棵树tree1和tree2,类似之前GBDT的原理(Xgboost与GBDT比较大的不同就是目标函数的定义,下文会具体介绍),两棵树的结论累加起来便是最终的结论,所以小孩的预测分数就是两棵树中小孩所落到的结点的分数相加:2 + 0.9 = 2.9。爷爷的预测分数同理:-1 + (-0.9)= -1.9。

和传统的boosting tree模型一样,Xgboost的提升模型也是采用的残差(或梯度负方向),不同的是分裂结点选取的时候不一定是最小平方损失。

5. Xgboost目标/损失函数:

因为XGBoost也是集成学习方法的一种,所以预测模型和损失函数都可用上式表示。

XGBoost预测模型:

$$\hat{y}_{i} = \phi(x_{i}) = \sum_{k=1}^{K}f_{k}(x_{i}) $$

$$where \ F = \{f_{t}(x) = w_{q(x)}\},(w \in R^T, q : R ^ d  —> \{1,2,...,T \})$$

$w_q(x)$为叶子节点$q$的分数,$F$对应了所有$K$棵回归树(regression tree)的集合,而$f(x)$为其中一棵回归树。XGBoost算法的核心就是不断地添加树,不断地进行特征分裂来生长一棵树,每次添加一个树,其实是学习一个新函数,去拟合上次预测的残差。当我们训练完成得到k棵树,我们要预测一个样本的分数,其实就是根据这个样本的特征,在每棵树中会落到对应的一个叶子节点,每个叶子节点就对应一个分数。最后只需要将每棵树对应的分数加起来就是该样本的预测值。显然,我们的目标是要使得树群的预测值尽量接近真实值,而且有尽量大的泛化能力。所以,从数学角度看这是一个泛函最优化问题,故把目标函数简化如下。

XGBoost损失函数:

$$L(\phi) = \sum_{i}l(\hat{y}_{i},y_{i}) + \sum_{k}\Omega (f_{k})$$

$$where \  \Omega(f) = \gamma T + \frac{1}{2}\lambda \left \| w \right \|^2$$

从上式可以看出,这个目标函数分为两部分:损失函数和正则化项。且损失函数揭示训练误差(即预测分数和真实分数的差距),正则化定义复杂度。对于上式而言,是整个累加模型的输出,正则化项是则表示树的复杂度的函数,值越小复杂度越低,泛化能力越强,其中T表示叶子节点的个数,w表示叶子节点的分数。直观上看,目标要求预测误差尽量小,且叶子节点T尽量少(γ控制叶子结点的个数),节点数值w尽量不极端(λ控制叶子节点的分数不会过大),防止过拟合。

6. Xgboost目标函数的改写:

我们知道,每次往模型中加入一棵树,其损失函数便会发生变化。另外在加入第$t$棵树时,则前面第$t-1$棵树已经训练完成,此时前面$t-1$棵树的正则项和训练误差都成已知常数项。再强调一下,考虑到第$t$轮的模型预测值$\hat{y_{i}}^{(t)}$  =  前$t-1$轮的模型预测$\hat{y_{i}}^{(t-1)}$  +  $f_{t}(x_{i})$,因此误差函数记为:$ l \left (y_{i},\hat{y_{i}}^{(t-1)} + f_{t}(x_{i}) \right )$,后面一项为正则化项。

所以目标函数可以写成:

$$\begin{align} Obj^{(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 \end{align}$$

注意对于上面这个误差函数的式子而言,在第$t$步,$y_{i}$是真实值,即已知,$\hat{y_{i}}^{(t-1)}$可由上一步第$t-1$步中的$\hat{y_{i}}^{(t-2)}$加上$f_{t-1}(x_{i})$计算所得,某种意义上也算已知值,故模型学习的是$f$。

上面那个Obj的公式表达的这样看有些过于抽象,看不出二次函数的概念,此时来看一个特例,当$l$采用平方误差的情况(相当于$ l(y_{i},\hat{y_{i}}) = (y_{i} - \hat{y_{i}})^2$),这个时候目标可以被写成下面这样的二次函数(下式中$2( \hat{y_{i}}^{(t-1)} - y_{i})f_{t}(x_{i})$部分表示的就是预测值和真实值之间的残差):

$$\begin{align} Obj^{(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 [ 2( \hat{y_{i}}^{(t-1)} - y_{i})f_{t}(x_{i}) +  f_{t}(x_{i})^2 \right ] + \Omega(f_{t}) + constant  \end{align}$$

如果不是特例,而是普通的损失函数,如果找到通用的方法,即考虑到损失函数不是二次函数该怎么办?此时需要利用泰勒展开,不是二次的想办法近似为二次(看下面Xgboost论文里的解释,定义了一阶导g和二阶导h):

从而保留二次项,最终的目标函数只依赖于每个数据点的在误差函数上的一阶导数和二阶导数。这么写的原因很明显,由于之前的目标函数求最优解的过程中只对平方损失函数时候方便求,对于其他的损失函数变得很复杂,通过二阶泰勒展开式的变换,这样求解其他损失函数变得可行了。

7. 泰勒公式

在数学中,泰勒公式(英语:Taylor's Formula)是一个用函数在某点的信息描述其附近取值的公式。这个公式来自于微积分的泰勒定理(Taylor's theorem),泰勒定理描述了一个可微函数,如果函数足够光滑的话,在已知函数在某一点的各阶导数值的情况之下,泰勒公式可以用这些导数值做系数构建一个多项式来近似函数在这一点的邻域中的值,这个多项式称为泰勒多项式(Taylor polynomial)。

相当于告诉我们可由利用泰勒多项式的某些次项做原函数的近似。
泰勒定理:
设 $n $是一个正整数。如果定义在一个包含$ a$ 的区间上的函数$ f$ 在 $a $点处 $n+1$ 次可导,那么对于这个区间上的任意 $x$,都有:

$$f(x) = f(a) + \frac{f{}'(a)}{1!}(x-a) + \frac{f^{(2)}(a)}{2!}(x-a)^2 + ... + \frac{f^{(n)}(a)}{n!}(x-a)^n + R_{n}(x)$$

其中的多项式称为函数在$a$ 处的泰勒展开式,剩余的$R_{n}(x)$是泰勒公式的余项,是$(x-a)^n$的高阶无穷小。

泰勒二阶展开:$$f(x + \Delta x) \simeq f(x) + f{}'(x)\Delta x + \frac{1}{2}f{}''(x)\Delta x^2$$

8. Xgboost的泰勒展开推导过程

对应到Xgboost的目标函数里头:$$Obj^{(t)}  = \sum_{i=1}^{n}l \left (y_{i},\hat{y_{i}}^{(t-1)} + f_{t}(x_{i}) \right ) + \Omega(f_{t}) + constant$$

忽略损失函数$l$中的$y_{i}$(因为上面说的“ 在第$t$步,$y_{i}$是真实值,即已知 ”,所以不影响后续目标函数对$\hat{y_{i}}^{(t-1)}$的偏导计算),做下一一对应:

泰勒二阶展开$f$ 里的$x$对应目标函数里的$\hat{y_{i}}^{(t-1)}$;

$f$ 里的$\Delta x$对应目标函数的$f_{t}(x_{i})$;

从而$f $对$x$求导数时,对应为目标函数对$\hat{y_{i}}^{(t-1)}$求偏导。

则原目标函数可以写成(泰勒展开具体推导步骤):

$$\begin{align} Obj^{(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   \\ & \approx \sum_{i=1}^{n} \left [ l(y_{i},\hat{y_{i}}^{(t-1)}) + \partial _{\hat{y}_{i}^{(t-1)}} \ \ l(y_{i},\hat{y}_{i}^{(t-1)})f_{t}(x_{i}) + \frac{1}{2}\partial^2 _{\hat{y_{i}}^{(t-1)}} \ \ l(y_{i},\hat{y_{i}}^{(t-1)})f_{t}(x_{i})^2 \right ] + \Omega(f_{t}) + constant \end{align}$$

其中,$g_{i} = \partial _{\hat{y}_{i} \ ^{(t-1)}} \ \ l(y_{i},\hat{y_{i}} \ ^{(t-1)}) $,$\partial^2 _{\hat{y}_{i} \ ^{(t-1)}} \ \ l(y_{i},\hat{y_{i}} \ ^{(t-1)})$。

则目标损失函数可以写成:$$Obj^{(t)} \simeq  \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^2_{t}(x_{i}) \right ] + \Omega(f_{t}) + constant $$

还需要注意这里的第$t $颗回归树是根据前面的$t-1$颗回归树的残差得来的,相当于$t-1$颗树的值$\hat{y_{i}}^{(t-1)}$是已知的。换句话说,损失函数里面$l(y_{i},\hat{y_{i}}^{(t)})$是常数,对目标函数的优化不影响(最小化),可以直接去掉,且常数项constant也可以移除,从而得到如下一个比较统一的目标函数:

$$Obj^{(t)} \simeq  \sum_{i=1}^{n} \left [  + g_{i}f_{t}(x_{i}) + \frac{1}{2}h_{i}f^2_{t}(x_{i}) \right ] + \Omega(f_{t})$$

这时,目标函数只依赖于每个数据点在误差函数上的一阶导数g和二阶导数h(这里体现xgboost和gbdt的不同了,目标函数保留了泰勒展开的二次项)。

回归前面树的复杂度介绍,我们知道:$f_{t}(x) = w_{q(x)},w \in R^T, q : R ^ d  —> \{1,2,...,T \}$,同时将目标函数全部转换成在第$t$棵树叶子节点的形式。对这里的$f_{t}(x_{i})$的理解:当一个样本进来的时候,不管是回归问题还是分类问题,最终都会掉到叶子结点上,而每颗树的每个叶子节点都会对应有一个得分(想象一下回归问题,每个叶子节点对应的是落入该叶子结点的所有训练样本的均值),所以$f_{t}(x_{i})$可以理解为一个样本在$t$轮最终的得分函数。

即每个决策树的第$j$个叶子节点的取值最终会是同一个值$w_{j}$。另外代入正则项。

损失函数可以继续化简:

$$ \begin{align} Obj^{(t)} & \simeq \sum_{i=1}^{n} \left [   g_{i}f_{t}(x_{i}) + \frac{1}{2}h_{i}f^2_{t}(x_{i}) \right ] + \Omega(f_{t}) \\ & = \sum_{i=1}^{n} \left [   g_{i}w_{q(x_{i})} + \frac{1}{2}h_{i}w^2_{q(x_{i}} \right ] +  \gamma T + \frac{1}{2}\lambda\sum\limits_{j=1}^{T}w_{j}^2 \\ & = \sum_{j=1}^{T} \left [ (\sum_{i\epsilon I_{j}}g_{i})w_{j} + \frac{1}{2}(\sum_{i\epsilon I_{j}}h_{i})w^2_{j} \right ] +  \gamma T + \frac{1}{2}\lambda\sum\limits_{j=1}^{T}w_{j}^2 \\ & = \sum_{j=1}^{T} \left [ (\sum_{i\epsilon I_{j}}g_{i})w_{j} + \frac{1}{2}(\sum_{i\epsilon I_{j}}h_{i} + \lambda )w^2_{j} \right ] + \gamma T  \end{align}$$

这里的$w_{q(x}$描述了一整颗树的模型,因此能分解成每个叶子结点的集合,而$w_{j}$刚好是每个叶子节点的得分函数,即$w_{j}$为第$j$个叶子节点的得分值,这两个可以相互转化。其中$T$为第t棵树中总叶子节点的个数,$I_{j}$被定义为每个叶节点 $j$上面样本下标的集合 $I_{j} = i | q(x_{i} = j)$,即表示$i$样本落在第$j$个叶子节点上,这个定义里的$q(x_{i})$要表达的是:每个样本值$x_{i}$ 都能通过函数$q(x_{i})$映射到树上的某个叶子节点,从而通过这个定义把两种累加统一到了一起。g是一阶导数,h是二阶导数。这一步是由于xgboost目标函数第二部分加了两个正则项,一个是叶子节点个数(T),一个是叶子节点的分数(w)。

继续把每个叶子节点区域样本的一阶和二阶导数的和单独表示如下:$G_{j} = \sum_{i\epsilon I_{j}}g_{i}$,$H_{j} = \sum_{i\epsilon I_{j}}h_{i}$

最终损失函数的形式可以表示为:

$$ Obj^{(t)}  = \sum_{j=1}^{T} \left [ G_{i}w_{j} + \frac{1}{2}(H_{i} + \lambda )w^2_{j} \right ] + \gamma T$$

现在我们得到了最终的损失函数,那么回到前面讲到的问题,如何一次求解出决策树最优的所有$J$个叶子节点区域和每个叶子节点区域的最优解$w_{j}$呢?

9. XGBoost损失函数的优化求解

关于如何一次求解出决策树最优的所有$J$个叶子节点区域和每个叶子节点区域的最优解$w_{j}$,可以把它拆分成2个问题:

1) 如果我们已经求出了第$t$个决策树的$J$个最优的叶子节点区域,如何求出每个叶子节点区域的最优解$w_{j}$?

2) 对当前决策树做子树分裂决策时,应该如何选择哪个特征和特征值进行分裂,使最终我们的损失函数$L$最小?

对于第一个问题,其实是比较简单的,直接基于损失函数对$w_{j}$求导并令导数为0得到:$G_{i} + (H_{i} + \lambda )w_{j} = 0$

从而可得到叶子节点区域的最优解$w_{j}$表达式:$$w^*_{j} = - \frac{G_{j}}{H_{j} + \lambda }$$

【补充】这个叶子节点的表达式不是XGBoost首创,实际上在GBDT的分类算法里,已经在使用了。大家在GBDT算法介绍中叶子节点区域值的近似解(2.3)步计算最佳拟合值$c_{ti}$:

$$c_{tj} = \sum\limits_{x_i \in R_{tj}}r_{ti}\bigg / \sum\limits_{x_i \in R_{tj}}|r_{ti}|(1-|r_{ti}|)$$

它其实就是使用了上式来计算最终的$c_{tj}$。例如回顾二元分类的损失函数是:$L(y, f(x)) = log(1+ exp(-yf(x)))$

其每个样本的一阶导数为:$g_i=-r_i= -y_i/(1+exp(y_if(x_i)))$

其每个样本的二阶导数为:$h_i =\frac{exp(y_if(x_i)}{(1+exp(y_if(x_i))^2} = |g_i|(1-|g_i|) $

由于没有正则化项,则$c_{tj} = -\frac{g_i}{h_i} $,即可得到GBDT二分类叶子节点区域的近似值。

现在我们回到XGBoost,我们已经解决了第一个问题。现在来看XGBoost优化拆分出的第二个问题:如何选择哪个特征和特征值进行分裂,使最终我们的损失函数$L$最小?在GBDT里面,我们是直接拟合的CART回归树,所以树节点分裂使用的是均方误差。XGBoost这里不使用均方误差,而是使用贪心法,即每次分裂都期望最小化我们的损失函数的误差。

注意到在$w_{j}$取最优解的时候,原损失函数对应的表达式为:$$Obj = -\frac{1}{2}\sum\limits_{j=1}^J\frac{G_{j}^2}{H_{j} + \lambda} +\gamma T$$

 Obj代表了当指定一个树的结构的时候,在目标上面最多减少多少。结构分数(structure score),这里结构分数越小代表这颗树的结构越好。

1) 树结构的打分函数

这里的结构分数(structure score)可以理解为类似于Gain系数一样更加一般的对于树打分的函数。

具体打分函数例子:

Xgboost算法的步骤和GB基本相同,都是首先初始化为一个常数,gb是根据一阶导数$r_{i}$,Xgboost是根据一阶导数$g_{i}$和二阶导数$h_{i}$,迭代生成基学习器,相加更新学习器。对于每一次尝试去对已有的叶子加入一个分割,每次做左右子树分裂时,目标是最大程度的减少损失函数的损失,也就是说,假设当前节点左右子树的一阶二阶导数和为$GL$,$HL$,$GR$,$HR$则我们期望最大化下式:

$$Gain = \frac{1}{2}\left [  \frac{G_L^2}{H_L + \lambda} + \frac{G_R^2}{H_R+\lambda} - \frac{(G_L+G_R)^2}{H_L+H_R+ \lambda} \right ] - \gamma$$

其中$\frac{G_{L}^2}{H_{L} + \lambda}$表示左子树分数,$\frac{G_{R}^2}{H_{R}+\lambda}$表示右子树分数,$\frac{(G_{L}+G_{R})^2}{H_{L}+H_{R}+ \lambda}$表示不可分割我们可以拿到的分数,$\gamma$表示加入新叶子节点引入的复杂度代价。

也就是说,Xgboost里的决策树分裂标准不再使用CART回归树的均方误差,而是上式了。这样就可以在建树的过程中动态的选择是否要添加一个结点。  联想决策树中信息增益,这里的原理类似。这一步实质上是为了寻找分裂结点的候选集。每次做左右子树分裂时,可以最大程度的减少损失函数的损失就最好了。

2) 寻找分裂结点标准

对于每次扩展,我们要枚举所有的分割方案,如何高效地枚举所有的分割呢?比如设置一个值a,然后枚举所有x < a,a  < x这样的条件,对于某个特定的分割a我们要计算a左边和右边的导数和。

具体如何分裂呢?举个简单的年龄特征的例子,假设我们选择年龄这个特征的值a作为决策树的分裂标准,x代表某个特征比如年龄age,把age从小到大排序:假定从左至右依次增大,则比a小的放在左边,比a大的放在右边,计算a左边和右边的导数和。

比如总共五个人,按年龄排好序后,一开始我们总共有如下4种划分方法:

  • 把第一个人和后面四个人划分开
  • 把前两个人和后面三个人划分开
  • 把前三个人和后面两个人划分开
  • 把前面四个人和后面一个人划分开

接下来,把上面4种划分方法全都各自计算一下Gain,看哪种划分方法得到的Gain值最大则选取哪种划分方法,经过计算,发现把第2种划分方法“前面两个人和后面三个人划分开”得到的Gain值最大,这样可以分别计算出左右子树的一阶和二阶导数和,进而求出最终的上式的值。


然后我们使用其他的不是值a的划分标准,可以得到其他组合的一阶和二阶导数和,进而求出上式的值。最终我们找出可以使上式最大的组合,以它对应的特征值来分裂子树。

可以发现对于所有的a,我们只要做一遍从左到右的扫描就可以枚举出所有分割的梯度和$G_{L}$和 $G_{R}$。然后用上面的公式计算每个分割方案的分数就可以了。

至此,我们解决了XGBoost的2个优化子问题的求解方法。

10. 寻找分裂结点

确定好损失函数以及最优解之后,接下来需要确定树的结构,即如何选出最优分裂节点。可以参考决策树算法:ID3选择信息增益为切分准则,C4.5选择信息增益率为切分准则。Xgboost基本思想和决策树一致,贪心法枚举所有节点,计算各个节点分裂前后的信息增益,选出信息增益最大的。

很显然,一棵树的生成是由一个节点一分为二,然后不断分裂最终形成为整棵树。那么树怎么分裂的就成为了接下来我们要探讨的关键。对于一个叶子节点如何进行分裂,根据Xgboost的Gain公式,Xgboost作者在其原始论文中给出了两种分裂节点的方法(切分算法)。

1) 枚举所有不同树结构的贪心法(精确贪心算法):

在所有特征上暴力枚举所有可能的切分点。

贪心法,从树深度0开始,每一节点都遍历所有的特征,比如年龄、性别等等,然后对于某个特征,先按照该特征里的值进行排序,然后线性扫描该特征进而确定最好的分割点,最后对所有特征进行分割后,我们选择所谓的增益Gain最高的那个特征。

2) 近似算法:

主要针对数据太大,不能直接进行计算。基本思想是通过特征的统计分布(分位数),按照百分比确定一组候选分裂点,(对于连续属性特征,根据候选切分点进行离散化)然后通过遍历所有的候选分裂点来找到最佳分裂点。近似算法有两种版本:global variant和local variant。global variant在树初始化时就确定了候选切分点;local variant是在每次节点分裂后重新选出候选点。

两种策略:全局策略和局部策略。在全局策略中,对每一个特征确定一个全局的候选分裂点集合,就不再改变;而在局部策略中,每一次分裂 都要重选一次分裂点。前者需要较大的分裂集合,后者可以小一点。对比补充候选集策略与分裂点数目对模型的影响。 全局策略需要更细的分裂点才能和局部策略差不多。

 

XGBoosting算法流程总结

这里总结下XGBoost的算法主流程,基于决策树弱分类器。不涉及运行效率的优化和健壮性优化的内容。

输入是训练集样本$I=\{(x_,y_1),(x_2,y_2), ...(x_m,y_m)\}$, 最大迭代次数$T$, 损失函数$L$, 正则化系数$\lambda,\gamma$;

输出是强学习器:$f(x)$。

对迭代轮数$t=1,2,...,T$有:

1) 计算第$i$个样本$(i-1,2,..,m)$在当前轮损失函数$L$基于$f_{t-1}(x_i)$的一阶导数$g_{ti}$,二阶导数$h_{ti}$,计算所有样本的一阶导数和$G_t = \sum\limits_{i=1}^mg_{ti}$,二阶导数和$H_t = \sum\limits_{i=1}^mh_{ti}$

2) 基于当前节点尝试分裂决策树,默认分数score=0,G和H为当前需要分裂的节点的一阶二阶导数之和。

 对特征序号 k=1,2...K:

 a) $G_L=0, H_L=0$

 b) 将样本按特征k从小到大排列,依次取出第i个样本,依次计算当前样本放入左子树后,左右子树一阶和二阶导数和:$$G_L = G_L+ g_{ti}, G_R=G-G_L$$$$H_L = H_L+ h_{ti}, H_R=H-H_L$$

 c) 尝试更新最大的分数:$$score = max(score, \frac{1}{2}\frac{G_L^2}{H_L + \lambda} + \frac{1}{2}\frac{G_R^2}{H_R+\lambda}  - \frac{1}{2}\frac{(G_L+G_R)^2}{H_L+H_R+ \lambda} -\gamma )$$

3) 基于最大score对应的划分特征和特征值分裂子树。

4) 如果最大score为0,则当前决策树建立完毕,计算所有叶子区域的$w_{tj}$, 得到弱学习器$h_t(x)$,更新强学习器$f_t(x)$,进入下一轮弱学习器迭代.如果最大score不是0,则转到第2)步继续尝试分裂决策树。

XGBoost算法运行效率的优化

Boosting算法的弱学习器是没法并行迭代的,但是单个弱学习器里面最耗时的是决策树的分裂过程,XGBoost针对这个分裂做了比较大的并行优化。对于不同的特征的特征划分点,XGBoost分别在不同的线程中并行选择分裂的最大增益。

同时,对训练的每个特征排序并且以块的的结构存储在内存中,方便后面迭代重复使用,减少计算量。计算量的减少参见上面算法流程总结,首先默认所有的样本都在右子树,然后从小到大迭代,依次放入左子树,并寻找最优的分裂点。这样做可以减少很多不必要的比较。

具体的过程如下图所示:

此外,通过设置合理的分块的大小,充分利用了CPU缓存进行读取加速(cache-aware access)。使得数据读取的速度更快。另外,通过将分块进行压缩(block compressoin)并存储到硬盘上,并且通过将分块分区到多个硬盘上实现了更大的IO。

XGBoosting涉及的算法工程优化策略:

1. 对内存的优化(列分块);

2. 对CPU Cache的优化:a) 提前取数(Prefetching),b) 合理设置分块大小;

3. 对IO的优化:a) Block压缩优化,b) Block 分片优化。

XGBoost算法健壮性的优化

XGBoost在算法健壮性的优化方面,除了上面讲到的正则化项提高算法的泛化能力外,XGBoost还对特征的缺失值做了处理。XGBoost没有假设缺失值一定进入左子树还是右子树,则是尝试通过枚举所有缺失值在当前节点是进入左子树,还是进入右子树更优来决定一个处理缺失值默认的方向,这样处理起来更加的灵活和合理。

也就是说,上面Xgboost算法流程总结的步骤a),b)和c)会执行2次,第一次假设特征k所有有缺失值的样本都走左子树,第二次假设特征k所有缺失值的样本都走右子树。然后每次都是针对没有缺失值的特征k的样本走上述流程,而不是所有的的样本。

如果是所有的缺失值走右子树,使用上面a),b)和c)即可。如果是所有的样本走左子树,则上面

a)步要变成:

$G_R=0, H_R=0$

b)步要更新为:

$G_R = G_R+g_{ti}, G_L=G-G_R$

$H_R = H_R+h_{ti}, H_L=H-H_R$

XGBoosting的优缺点总结

在分析XGBooting优缺点的时候,通过比较该算法与GBDT的差异,即可有较清楚的描述,具体表现在如下方面。

1)基分类器的差异

  • GBDT算法只能利用CART树作为基学习器,满足分类应用;
  • XGBoost算法除了以CART树中的回归树作为基分类器之外还支持线性的基学习器,因此其一方面可以解决带L1与L2正则化项的逻辑回归分类问题,也可以解决线性回问题。

2)节点分类方法的差异

  • GBDT算法主要是利用Gini impurity针对特征进行节点划分;
  • XGBoost经过公式推导,提出的weighted quantile sketch划分方法,依据影响Loss的程度来确定连续特征的切分值。

3)模型损失函数的差异

  • 传统GBDT在优化时只用到一阶导数信息;
  • xgboost则对代价函数进行了二阶泰勒展开,二阶导数有利于梯度下降的更快更准。

4)模型防止过拟合的差异

  • GBDT算法无正则项,可能出现过拟合;
  • Xgboost在代价函数里加入了正则项,用于控制模型的复杂度,降低了过拟合的可能性。

5)模型实现上的差异

决策树的学习最耗时的一个步骤就是对特征的值进行排序(因为要确定最佳分割点)。xgboost在训练之前,预先对数据进行了排序,然后保存为block结构,后面的迭代中重复地使用这个结构,大大减小计算量。其能够实现在特征粒度的并行。

 

 

参考文章:

https://homes.cs.washington.edu/~tqchen/pdf/BoostedTree.pdf

https://arxiv.org/pdf/1603.02754.pdf

https://zhuanlan.zhihu.com/p/90520307

https://www.cnblogs.com/pinard/p/10979808.html

https://blog.csdn.net/v_JULY_v/article/details/81410574

posted @ 2020-04-25 14:22  大-道-至-简  阅读(1270)  评论(0编辑  收藏  举报