xgboost原理及其工具库实际使用
前言
XGBoost的全称是eXtreme(极端) Gradient Boosting,是一个是大规模并行的 boosting tree开源工具包,由华盛顿大学的陈天奇博士提出,因其出众的效率与较高的预测准确度而引起了广泛的关注。
本文主要关注XGBoost的原理,以及其工具库的实际使用。
XGBoost原理
模型函数形式
给定数据集\(D = \{(x_i,y_i)\}\),XGBoost进行additive training,学习\(K\)棵树, 采用以下函数对样本进行预测:

这里\(F\)是假设空间,\(f(x)\)是回归树(CART):

\(q(x)\)表示将样本\(x\)分到了某个叶子节点上, \(w\)是叶子节点的分数(leaf score), 所以\(w_{q(x)})\)表示回归树对样本的预测值。
例子: 预测一个人是否喜欢电脑游戏

回归树的预测输出是实数分数,可以用于回归、分类、排序等任务中。对于回归问题,可以直接作为目标值,对于分类问题,需要映
射成概率(为正例的概率,原始的XGBoost只能用于二分类), 比如采用逻辑函数\(\sigma(z) = \frac{1}{1+e^{-z}}\)
目标函数
XGBoost的目标函数(函数空间)为:

正则项对每棵回归树的复杂度进行了惩罚,相比原始的GBDT,XGBoost的目标函数多了正则项, 使得学习出来的模型更加不容易过拟合。
对于树的复杂度,我们可以使用树的深度,内部节点个数,叶子节点个数(T),叶节点分数(w)等指标来衡量。
而XGBoost中采用的是:

gamma,/'gæmə/,\(\gamma\)
对叶子节点个数进行惩罚,相当于在训练过程中做了剪枝。
下面我们将误差函数进行二阶泰勒展开。
误差函数的二阶泰勒展开
第t次迭代后,模型的预测等于前t-1次的模型预测加上第t棵树的预测:

此时目标函数可写作:

公式中\(y_i,\tilde{y}_{i}^{t-1}\)都已知,模型要学习的只有第t棵树\(f_t\)。
将误差函数在\(\tilde{y}_{i}^{t-1}\)处进行二阶泰勒展开:

公式中,


将公式中的常数项去掉, 得到:

把\(f_t,\Omega(f_t)\)写成树结构的形式,即把下式带入目标函数中\(f(x) = w_{q(x)},\Omega(f) = \gamma T + \frac{1}{2}\lambda||w||^2\)
得到:

我们看到上式中有两种不同类型的求和,我们怎么才能统一成一种形式的求和呢?
我们定义每个叶节点\(j\)上的样本集合为:\(I_j = \{i|q(x_i) = j\}\)
则目标函数可以写成按叶节点累加的形式:

如果确定了树的结构(即\(q(x)\)确定),为了使目标函数最小,可以令其导数为0, 解得每个叶节点的最优预测分数为:

代入目标函数, 得到最小损失为:

回归树的学习策略
当回归树的结构确定时, 我们前面已经推导出其最优的叶节点分数以及对应的最小损失值,问题是怎么确定树的结构?
一个朴素的想法是暴力枚举所有可能的树结构, 选择损失值最小的,但是这是一个NP难问题。
我们退而求其次,采用贪心法,每次尝试分裂一个叶节点,计算分裂前后的增益,选择增益最大的。
我们回顾一下我们已经学过的决策树的建模工程,它们在寻找节点的最优切分点时,使用的增益分别为:
- ID3算法采用信息增益
- C4.5算法采用信息增益比
- CART采用Gini系数
熵:

熵越大,随机变量的不确定性就越大。
条件熵:

信息增益:
信息增益表示得知特征X的信息而使得类Y的信息的不确定性减少的程度。



信息增益比:

特征A取的值越多,特征A取值的不确定性就越大(因为能够取好几个,有好几种可能),\(H_A(D)\)的值就越大。
CART:



基尼指数:
分类树用基尼指数选择最优特征,同时决定该特征的最优二值切分点。

基尼指数周志华机器学习书上的定义为:任意抽取两个样本,这两个样本不同类的概率。

我们知道没有分裂前目标函数可以写为:
\(obj = -\frac{1}{2}\sum\limits_{j=1}^T\frac{G_{j}^2}{H_{j} + \lambda} +\gamma T\)
如果我们每次做左右子树分裂时,可以最大程度的减少损失函数就好了,则损失函数的减少程度即为增益。
因此, 对一个叶子节点进行分裂,分裂前后的增益定义为:
\(Gain = \frac{1}{2}(\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\)
Gain的值越大,分裂后损失函数减小越多。
注意该特征收益也可作为特征重要性输出的重要依据。
所以当对一个叶节点分割时,计算所有候选(feature,value)对应的gain,选取gain最大的进行分割.
树节点分裂方法(Split Finding)
Xgboost支持两种分裂节点的方法——贪心算法和近似算法。
精确贪心算法
遍历所有特征的所有可能的分割点,计算gain值,选取gain值最大的(feature, value)去分割。
我们可以发现对于所有的分裂点a,我们只要做一遍从左到右的线性扫描就可以枚举出所有分割的梯度和\(G_L\)和\(G_R\)。然后用上面的公式计算每个分割方案的分数就可以了。

近似算法
上面的精确贪心算法计算代价太大了,尤其是数据量很大,分割点很多的时候,计算起来非常复杂并且也无法读入内存进行计算。近似算法主要针对贪婪算法这一缺点给出了近似最优解。
对于每个特征,只考察分位点可以减少计算复杂度。
在提出候选切分点时有两种策略:
- Global:学习每棵树前就提出候选切分点,并在每次分裂时都采用这种分割;
- Local:每次分裂前将重新提出候选切分点。

- 第一个for循环:对特征k根据该特征分布的分位数找到切割点的候选集合\(S_k = \{s_{k1},s_{k2},...,s_{kl}\}\)。XGBoost支持Global策略和Local策略。
- 第二个for循环:针对每个特征的候选集合,将样本映射到由该特征对应的候选点集构成的分桶区间中,即\(s_{k,v} >= x_{jk} > s_{k,v-1}\) ,对每个桶统计G,H值,最后在这些统计量上寻找最佳分裂点。
直观上来看,
- Local策略需要更多的计算步骤,
- 而Global策略因为节点没有划分所以需要更多的候选点。
下图给出不同分裂策略的AUC变换曲线,横坐标为迭代次数,纵坐标为测试集AUC,eps为近似算法的精度,其倒数为桶的数量。

我们可以看到,
- Global策略在候选点数多时(eps小)可以和Local策略在候选点少时(eps大)具有相似的精度。
- 此外我们还发现,在\(\epsilon\)取值合理的情况下,分位数策略可以获得与贪婪算法相同的精度。
下图给出近似算法的具体例子,以三分位为例:

- 根据样本特征进行排序,
- 然后基于分位数(等频分桶,每个桶里三个样本)进行划分,
- 并统计三个桶内的 G,H 值,
- 最终求解节点划分的增益。
上面这个只是举个例子,事实上这样的分桶(无论是等频分桶,还是等宽分桶)都太随意了,缺乏可解释性。而XGBoost采用了一种对loss里作为样本的影响权重(二阶导数值 \(h_i\))的等值percentiles进行划分(百分比分位数划分算法(即加权分位数缩略图,Weight Quantile Sketch)。
作者进行候选点选取的时候,考虑的是想让loss在左右子树上分布的均匀一些,而不是样本数量的均匀,因为每个样本对loss的贡献可能不一样,按样本均分会导致分开之后左子树和右子树loss分布不均匀,取到的分位点会有偏差。如下所示:

那么问题来了:为什么要用\(h_i\)进行样本加权?
我们知道模型的目标函数为:

我们稍作整理,便可以看出\(h_i\)有对loss加权的作用。

这样化简够简洁明了了吧,你看到残差的身影了吗? 后面的每一个分类器都是在拟合每个样本的一个残差\(-\frac{g_i}{h_i}\),其实把上面化简的平方损失函数拿过来就一目了然了。而前面的\(h_i\)可以看做计算残差时某个样本的重要性,即每个样本对loss的贡献程度.
PS:这里加点题外话,
- Xgboost引入了二阶导之后,相当于在模型降低残差的时候,给各个样本,根据贡献度不同,加入了一个权重,这样就能更好的加速拟合和收敛,
- GBDT只用到了一阶导数,这样只知道梯度大的样本降低残差效果好,梯度小的样本降低残差不好(这个原因我会放到Lightgbm的GOSS那里说到),但是好与不好的这个程度,在GBDT中无法展现。
- 而xgboost这里就通过二阶导可以展示出来,这样模型训练的时候就有数了.
具体一点,作者是怎么样找到候选分位点的呢?
比如我们定义一个数据集\(D_k = \{(x_{1k},h_1),(x_{2k},h_2),...,(x_{nk},h_n)\}\)代表每个训练样本的第\(k\)个特征的取值和二阶梯度值,那么我们可以有一个排名函数\(r_k(z)\)

这个排名函数表示特征值小于z的样本的贡献度比例。它的目的就是去找相对准确的候选点\(S_k = \{s_{k1},s_{k2},...,s_{kl}\}\),这里的\(s_{k1} = min_i x_{ik},s_{kl} = max_i x_{ik}\)。而相邻两个候选点构成的桶里的贡献度应满足下面这个函数:
\(|r_k(s_{k,j})-r_k(s_{k,j+1})|< \epsilon\)
这个\(\epsilon\)控制每个桶中样本贡献度比例的大小,其实就是贡献度的分位点,我们自己设定。
上面这些公式看起来挺复杂,可是计算起来很简单,
- 就是计算一下总的贡献度,
- 然后指定\(\epsilon\),
- 两者相乘得到每个桶的贡献度进行分桶即可。
- 这样我们就可以确定合理的候选切分点,然后进行分箱了。
数据缺失时的分裂策略
当特征出现缺失值时,XGBoost使用了稀疏感知(Sparsity-aware)算法来解决数据缺失时的分裂策略.
- XGBoost在构建树的节点过程中只考虑非缺失值的数据遍历,
- 而且为每个节点增加了一个缺省方向,
- 当样本相应的特征值缺失时,可以被归类到缺省方向上,
- 最优的缺省方向可以从数据中学到。至于如何学到缺省值的分支,其实很简单,分别枚举特征缺省的样本归为左右分支后的增益,选择增益最大的枚举项即为最优缺省方向。

在构建树的过程中需要枚举特征缺失的样本,乍一看该算法的计算量增加了一倍,但其实该算法在构建树的过程中只考虑了特征未缺失的样本遍历,而特征值缺失的样本无需遍历只需直接分配到左右节点,故算法所需遍历的样本量减少,下图可以看到稀疏感知算法比basic算法速度快了超过50倍。

XGBoost的其它特性
- 行抽样(row sample)
- 列抽样(column sample)借鉴随机森林
- Shrinkage(缩减),每一次迭代,得到的新模型前面,有个\(\eta\)(这个是让树的叶子节点权重乘以这个系数),这个叫做收缩率,这个东西加入的目的是削弱每棵树的作用,让后面有更大的学习空间,有助于防止过拟合.也就是说,
- 我不完全信任每一个残差树,每棵树只学到了模型的一部分,希望通过更多棵树的累加来弥补.
- 这样让这个学习过程更平滑,而不会出现陡变。
- 这个和正则化防止过拟合的原理不一样,这里是削弱模型的作用,而前面正则化是控制模型本身的复杂度, 而这里是削弱每棵树的作用,都是防止过拟合,但是原理不一样。
- 支持自定义损失函数(需二阶可导)
XGBoost工程实现优化之系统设计
块结构(Column Block)设计
我们知道,决策树的学习最耗时的一个步骤就是,在每次寻找最佳分裂点时都需要对特征的值进行排序。而XGBoost提出column block数据结构来进行预排序,来降低节点分裂时的排序时间。
- 每一个块结构包括一个或多个已经排序好的特征;
- XGBoost在训练之前,根据特征,对数据进行了排序,然后保存到块结构中。这些块只需要在程序开始的时候计算一次,后续排序只需要线性扫描这些block即可。后面的训练过程中会重复地使用块结构,可以大大减小计算量;
- 在每个块结构中的数据都采用了稀疏矩阵存储格式(Compressed Sparse Columns Format,CSC)进行存储;
- 缺失特征值将不进行排序;
- 每个特征会存储指向样本梯度统计值的索引,方便计算一阶导和二阶导数值;
- 这种块结构存储的特征之间相互独立,方便计算机进行并行计算。在对节点进行分裂时,需要选择增益最大的特征作为分裂,这时,各个特征的增益计算可以同时进行,这也是 Xgboost 能够实现分布式或者多线程计算的原因;

缓存访问优化算法
块结构的设计可以减少节点分裂时的计算量,但特征值通过索引访问样本梯度统计值的设计,会造成相应的样本的梯度信息是分散的,导致访问操作的内存空间不连续,这样会造成缓存命中率低,从而影响到算法的效率。
为了解决缓存命中率低的问题,XGBoost提出了缓存访问优化算法:
- 为每个线程分配一个连续的缓存区,预取需要的梯度信息存放在缓冲区中,再统计梯度信息,这样就是实现了非连续空间到连续空间的转换,提高了算法效率。
- 此外适当调整块大小,也可以有助于缓存优化。
"核外"块计算
当数据量过大时,无法将数据全部加载到内存中,只能
- 先将无法加载到内存中的数据,暂存到硬盘中,
- 直到需要时,再进行加载计算,
而这种操作,必然涉及到,因内存与硬盘速度不同而造成的资源浪费和性能瓶颈。为了解决这个问题,XGBoost独立一个线程专门用于从硬盘读入数据,以实现处理数据和读入数据同时进行。
此外,XGBoost还用了两种方法来降低硬盘读写的开销:
- 块压缩:对Block进行按列压缩,并在读取时进行解压;
- 块拆分:将每个块存储到不同的磁盘中,从多个磁盘读取可以增加吞吐量。
小结
相比于GBDT,XGBoost有以下优点:
- GBDT是机器学习算法,XGBoost是GBDT的工程实现。
- 精度更高:GBDT只用到一阶泰勒展开,而XGBoost对损失函数进行了二阶泰勒展开.。XGBoost引入二阶导,一方面是为了增加精度,另一方面也是为了能够自定义损失函数,二阶泰勒展开可以近似大量损失函数。
- 灵活性更强:GBDT以CART作为基分类器,XGBoost不仅支持CART还支持线性分类器,(使用线性分类器的XGBoost相当于带L1和L2正则化项的逻辑斯蒂回归(分类问题)或者线性回归(回归问题))。此外,XGBoost工具支持自定义损失函数,只需函数支持一阶和二阶求导。
- 正则化:XGBoost在目标函数中加入了正则项,用于控制模型的复杂度。有助于降低模型方差,防止过拟合。正则项里包含了树的叶子节点个数、叶子节点权重的L2范式.这个东西的好处,就是XGBoost在构建树的过程中,就可以进行树复杂度的控制,而不是像GBDT那样, 等树构建好了之后再进行剪枝。
- Shrinkage(缩减):相当于学习速率。XGBoost在进行完一次迭代后,会将叶子节点的权重乘上该系数,主要是为了削弱每棵树的影响,让后面有更大的学习空间,学习过程更加的平缓。
- 列抽样:XGBoost借鉴了随机森林的做法,在建树的时候,不用遍历所有的特征列,可以进行抽样,一方面简化了计算,另一方面也有助于降低了过拟合。
- 缺失值处理:这个是XGBoost的稀疏感知算法,加快了节点分裂的速度,传统的GBDT没有设计对缺失值的处理,而XGBoost能自动学习出缺失值的处理策略。
- 行抽样:传统的GBDT在每轮迭代时使用全部的数据,XGBoost则采用了与随机森林相似的策略,支持对数据进行采样。
- 可以并行化操作:块结构可以很好的支持并行计算。
缺点:
- 虽然利用了预排序和近似算法可以降低寻找最优分裂点的计算量,但在节点分裂过程中仍需要遍历整个数据集。
- 预排序过程的空间复杂度过高,不仅需要存储特征值,还需要存储特征对应样本梯度统计值的索引,相当于消耗了两倍的内存。
所以,在内存和计算方面,还是有很大的优化空间的。
浙公网安备 33010602011771号