《XGBoost: A Scalable Tree Boosting System》论文总结

前言:

本文是对Tianqi Chen, Carlos Guestrin于2016年发表在kdd的XGBoost: A Scalable Tree Boosting System的一个粗略总结,主要针对模型原理和一些理论细节,不涉及过多的代码实现。由于XGBoost 本身是基于决策树,essemble learning,GBDT等概念上面提出的更优的算法,本文预设读者对决策树和集成学习都有基本了解,所以不会在这些问题上进行说明。GBDT由于是对理解本文非常重要的一个概念,所以本文会简单的回顾一下GBDT的原理。综上,由于本篇论文的知识背景较为复杂,牵涉到的内容不少,本文会在第一部分简单交代论文提出的背景和GBDT回顾,这里会涉及到原文Abstract部分和第一小节的内容。然后会在第二部分讨论XGBoost的数学模型和原理,这部分主要涉及原文的第二和三小节。接下来在本文的第三部分会对原文的第四部分进行简单总结,主要是针对一些实现上的技巧和优化问题。最后一部分则会对原文的第五、六、七部分进行总结,主要是针对该算法在实验中的表现已经对该算法的优缺点进行总结。

————————————————————————————————————————————————————

一、GDBT回顾及Xgboost的介绍

在传统的机器学习里面,我们往往要面对两个问题,一个是模型的泛化能力,另一个则是模型的精确度。一个复杂的模型,往往在某一个数据集上能训练出很好的表现而在其他的数据样本上泛化能力较差,这里所说的复杂是指其参数数量和模型结构的复杂,并不单指其计算复杂度。而一些简单的模型,虽然在某一个数据集任务上表现很一般,但它们却具有较强的泛化能力,换句话说他们在不同的数据样本上表现能力大同小异。为了平衡这两种损失,我们提出了集成学习。集成学习最基础的概念在于,如何使用多个最基础的模型(比如一个深度很浅,节点很少的树模型)来组合起来学习复杂的模型,这一点在概念上和深度学习十分的相似。我们可以把每个单一的模型看成是不同的神经元,然后每一个神经元又都能学习不同的特征或者规律。当然,和深度学习所不同的是,最终我们对于某一个数据的预测,可能是基于对每个小模型的预测的majority vote或者是average值得到。集成学习的常见模型有三种,bagging,random forests 和 boosting,今天我们的主角就是boosting 集成模型。

Boosting模型的大概原理就是让每一个新的基础模型,根据前面基础模型预测里面的判断错误的数据去进行学习。换句话说,我们期望每一个新的基础模型都能够在原有的基础上增加模型的准确度(降低损失)。

GBDT是一种基于树模型的boosting算法。它的模型可以表示为许多决策树的加法模型:

\sum_m^M T(x_i;\theta_m)  其中M代表树的数量;而我们定义第m个模型如下: f_m(x) = f_{m-1}+T(x;\theta_m)

而GBDT的目标函数如下:

Obj = \sum_{i=1}^NL(y_i, \hat{y}_i)=\sum_{i=1}^NL(y_i, f_{m-1}(x_i)+T(x_i);\theta_m) 其中L是我们的损失函数,可以选用mean square error或者是cross entropy loss 取决于任务是分类还是回归。而优化这个目标函数就是要找到合适的参数,这些参数就是各个叶子节点的权重。

二、Xgboost的原理和算法简介

Xgboost相较于GDBT的定义方式,加上了新的惩罚项,并且进行了变换。下面我们来看一下Xgbosst 目标函数的具体推导,这一部分对于理解Xgboost的理解十分重要,包括对齐算法的关键步骤的理解都很重要。

首先,我们定义在Xgboost里面的一些关键的基本函数和参数表达:

第一个是每一个树模型,我们定义为fk如下:

f_k(x) = w_{q(x)}; w\in \mathbb{R}^T(q:\mathbb{R}^m \rightarrow T), 这里每一个f被看作一个树结构的模型,将一个数据变成一个权重w。而w则是T维度的,这里的T是节点数量。q则是一个将x从m维度映射到某一个节点的映射,换句话就是树将数据分类到某一个子群里面。m在这里是特征数量。这个函数就是说将一个有m个特征的数据x划分到一个拥有权重w的某一个节点下的叶子的模型fk。

理解了这个表达式,我们就来定义我们的目标函数,Xgboost在思想和GBDT没有太多区别,但是他在目标函数里面施加了很多变换使得这个目标函数更加利于求出最好的w。其目标函数如下:

Obj = \sum_{i=1}^nl(y_i, \hat{y}^{t-1} + f_t(x_i)) + \Omega (f_t)+constant, 其中l函数仍然是代表损失函数。\hat{y}^{t-1}则是代表上一时刻的预测值,f_t则是这个时刻的模型的输出。 \Omega(f_t)则是regularization项。其定义为:\Omega(f_t) = \gamma T + \frac{1}{2} \lambda \left \| w\right \|^2

对于形如l(x + \Delta x )的损失函数,我们可以用泰勒展开来用一阶和二阶导数模拟其近似,推导过程如下:

得到图中式子1之后,我们可以将之前定义好的f_t\Omega(f_t)的表达式带入到式子1当中,并进行一系列变化。具体如下:

这个式子的变换技巧在于,将i=1到n的相加拆分成了两个加和公式。一个是j=1...T另一个是针对集合I的。从图中的定义不难看出,T*I 正好等于n。

得到了上面的式子以后,我们再用G_j = \sum_{i\in I_j}(g_i)和 H_j = \sum_{i\in I_j}(h_i)来简化我们的式子。并且对式子求关于w的偏导,并使得导数等于0,来求出最优的w表达如下:

至此,我们就已经得到了w的最优解,并且带入到原来的目标方程。然而,对于一个树算法来说,我们要在什么时候分裂节点呢?这时候我们就可以用简化过后的目标方程来进行对比。公式如下:

这个式子很重要,我们可以来详细看下。其中\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的模型公式的推导,并且理解了它的split算法和怎样迭代多个树模型。简单说,Xgboost的步骤大概是,首先将数据根据每个特征分别排序,然后对每个特征分别计算分节点的分裂增益,并从所有特征里面的所有分裂点里面选择给出分裂增益最大的那个特征分裂点作为分裂,然后再往复循环。值得注意的是,这里我们的一阶和二阶导数都分别只需要计算一次就够了,存起来后续使用可以避免浪费时间。而当你要建立一个新的树模型的时候,就必须先从上个树模型的预测概率基础上再加上当前的预测概率再来求导。

当然,上面的数学公式可能仍然十分抽象,如果仍然对具体的过程有疑惑,那么不妨阅读这篇(https://blog.csdn.net/qq_22238533/article/details/79477547)从例子上推算xgboost算法流程的解读博客,实在是写得非常详细,值得一读。

这个部分另外值得注意的两个地方是两个训练的细节,一个是使用一个scale factor(\eta)来控制每个叶子节点的权重大小,这一点跟梯度下降法里面的学习率大同小异。在这里加上一个scale factor的好处在于,防止某一个树的权重对于整个模型的影响太大。第二个技巧则是对feature进行随机采样,这一个技巧其实跟随机森林里面用的办法是一样的。这么做个人觉得出于两个考虑,一个是减少不少潜在计算量,一个是可以更好的用不同的单独树模型去捕捉几个feature直接的correlation。

接下来原文的第三小节主要讲了下怎样去寻找一个分裂点,虽然我们刚才已经给出了分裂点的分裂标准以及计算公式,但是我们还是要面对一个问题,那就是怎样去挑选可能的分裂点。在工程实现中,我们面对数以亿计的数据,全部遍历一次毫无疑问是非常消耗时间的,但是基于增益的分裂算法却是一个贪心算法,是要在一堆可能的候选节点里面选择最好的那一个。那是否有办法在保证仍然能选择到理想的分裂节点的情况下,尽量避免遍历所有分裂点带来的成本呢?作者在3.2小节提出了一个近似算法来解决这个问题。

作者首先指出,遍历贪心的算法固然很强大,因为能找到局部最优的分裂点,但是如果数据太大无法全部放在内存里面,那这种办法可能会很低效率。所以怎样提出潜在的候选节点很重要,作者给出的算法如下:

其实这个算法写得十分简略,大意就是从原来的数据里根据某种百分比顺序宣传l个候选分裂点(对feature k),然后再根据这些候选节点把数据分段,去计算一阶导数和二阶导数最终还是用同样的公式来计算分裂增益,再从这些候选的分裂点当中选择最好的那一个。这些候选点的选择方式还可以分为global和local两种,前者是一开始就拟定,并且之后的每一次分裂都从这些candidate里面去找,后者则是在每一步分裂之后重新拟定候选分裂点。前者计算量较小,后者对较深的树有着更好的表现。

紧接着我们就要讨论,怎样去寻找这些候选点。作者提出的一个办法叫做Weighted Quantile Sketch, 简单说就是将数据根据feature值先排一个rank,然后再通过一个固定的间隔来选取不同的分裂点。具体公式如下,已经做了一定的批注:

在这一部分,作者除了讨论了关于怎样选取候选分裂点之外还特定探讨了另外一个问题,那就是怎样处理缺失值。Xgboost有这一套自动的对缺失值进行处理的算法,具体如下:

这个算法其实很好理解,简单来说就是首选我们把包含有缺失值的数据的总的一阶导数和以及二阶导数和算好,存为G,H两个变量。之后不考虑缺失数据,只针对非确实数据来分别计算这个时候左子树的一阶导数和二阶导数和。例如,我们计算所有未缺失数据左子树的一阶导数和G_L,然后这时候就是作者巧妙的地方,他这时候用G减去G_L,从而得到一个G_R, 而这个G_R值则正是包含了缺失值的一阶导数和的值。对右子树我们做同样的事情,还有二阶导数和H也是一样。最终我们还是使用增益公式来计算,然后对比把缺失值放在左子树和右子树哪个能给我们更好的增益。

三、系统设计和并行化处理

在了解完了Xgboost核心的原理和训练技巧以后,我们就来看一下在工程实现上作者采用的一些并行化的处理技巧。

首先作者提出第一个并行化结构block,用来储存经过列压缩过后的数据(column compressed),这么做的原因是作者认为xgboost的效能瓶颈是庞大的排序计算量,毕竟需要针对不同的特征都对数据排序。将数据按照行方向切分成不同的block。这样的好处在于我们可以把一切计算全部都进行并行化处理,我们完全可以在不同的cpu上处理不同的分段的参数计算(比如导数计算,乃至分段计算)

这里作者进行一个简要的算法复杂度分析,如果我们不使用任何并行手段并且用确切贪心算法来计算,那我们的复杂度大概是O(Kd\left \| x \right \|_0logn)。这里的K代表树的数量,d则代表最大深度,\left \| x \right \|_0 则是所有没有缺失值的数据数量。而如果我们用了block方式来处理数据排序,则我们的复杂度可以降低大概一个logn的乘数,在数据足够大的情况下,可能就是几十倍的差别。如果我们使用近似算法的话则可以将logn变为logq,q这里是一个你打算取的candidates的数量。

接下来作者又提出了一个缓存读取数据的方法来减少I/O成本,简单来说就是不先对G和H等参数进行计算,而是将他们分为一个个mini batch然后放入缓存里面再来计算,这样就可以节省不少的读取数据的时间。

还有一个更加精细的优化办法是优化对disk的使用,这同样可以节省大量的I/O成本。这个办法的具体原理是,首先我们确保用一个独立的线程来读取数据并放入到一个内存的缓冲区,在这个线程进行读取的同时,其它的线程可以并行地进行计算而不用等待数据读取。然而这还不能回避最重要的IO计算成本,作者又提出了Block Compression和Block Sharding算法。前者的原理很简单,就是将数据纵向的压缩(这样一次可以读入更多的数据),然后放到内存当中应用一个独立地线程去解压。后者则是将数据放到不同的磁盘分区,这样就可以并行的读取一个数据了(存疑)。

四、实验和评估

在实验部分,作者分别使用Allstate insurance,希格斯玻色子,雅虎问答,和criteo terabyte click log数据集来测试,并且分别针对精确贪心算法,近似算法,以及后面的并行和读取优化算法来进行实验。这里就不再赘述具体的实验过程和配置了,感兴趣的可以去自己阅读。总之作者在总结里面提到,如果要处理真正的大数据问题,那么之前提到的读取优化算法,block优化,缓存优化技巧全部都是至关重要的。

以上就是对Xgboost一文的解读。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值