[机器学习] Boosting算法3 --- XGBoost

[机器学习] Boosting算法1 --- AdaBoost

[机器学习] Boosting算法2 --- GBDT

[机器学习] Boosting算法3 --- XGBoost

[机器学习] Boosting算法4 --- LightGBM

目录

一  XGBoost是什么

二  基础知识GBDT

三  XGBoost目标函数

四  XGBoost算法说明

1. boosting tree模型

2. 树的集成

3. 学习第t棵树

4. 目标函数中的损失函数部分(第一部分)泰勒公式展开

5. 目标函数中的损失函数部分(第二部分)

6. 损失函数的转换

7. 树结构打分

五  一棵树的生成过程

1. 分裂一个结点

2. 寻找最佳分裂点

3. 停止生长

六  为什么要用 XGBoost?

七  XGBoost VS GBDT

 树节点分裂优化

 八  XGboost/GBDT调参


一  XGBoost是什么

  • 全称:eXtreme Gradient Boosting
  • 作者:陈天奇(华盛顿大学博士)   (Tianqi Chen http://homes.cs.washington.edu/~tqchen/ )
  • 基础:GBDT
  • 所属:boosting迭代型、树类算法。
  • 适用范围:分类、回归
  • 项目地址:https://github.com/dmlc/xgboost
  • 官方文档:http://xgboost.readthedocs.io/en/latest/

    xgboost论文:http://cran.fhcrc.org/web/packages/xgboost/vignettes/xgboost.pdf

    陈天奇的boosting tree的ppt:http://homes.cs.washington.edu/~tqchen/pdf/BoostedTree.pdf

XGBoost是2016年由华盛顿大学陈天奇老师带领开发的一个可扩展机器学习系统。它是Gradient Boosting Machine的一个c++实现, 严格意义上讲XGBoost并不是一种模型,而是一个可供用户轻松解决分类、回归或排序问题的软件包。它内部实现了梯度提升树(GBDT)模型,并对模型中的算法进行了诸多优化,在取得高精度的同时又保持了极快的速度

XGBoost最大的特点在于,它能够自动利用CPU的多线程进行并行,同时在算法上加以改进提高了精度。

二  基础知识GBDT

XGBoost是在GBDT的基础上对boosting算法进行的改进,内部决策树使用的是回归树,简单回顾GBDT如下:

回归树的分裂结点对于平方损失函数,拟合的就是残差;对于一般损失函数(梯度下降),拟合的就是残差的近似值,分裂结点划分时枚举所有特征的值,选取划分点。最后预测的结果是每棵树的预测结果相加。

XGBoost是boosting算法的一种,是以决策树为基础的一种梯度提升算法。通过多轮迭代,每轮迭代产生一个弱分类器,每个分类器在上一轮分类器的残差基础上进行训练。对弱分类器的要求一般是足够简单,并且是低方差和高偏差的。因为训练的过程是通过降低偏差来不断提高最终分类器的精度。 弱分类器一般会选择为CART TREE(也就是分类回归树,同时XGBoost还支持线性分类器)。由于上述高偏差和简单的要求 每个分类回归树的深度不会很深。最终的总分类器 是将每轮训练得到的弱分类器加权求和得到的(也就是加法模型)

通常,一棵树过于简单,所以集成学习将多棵树进行结合,常常获得比单棵树优越的泛化性能。XGBoost是加法模型,它会把多棵树的预测加到一起,预测得分是每棵树的预测分数之和,如下图所示

这里写图片描述
 

三  XGBoost目标函数

XGBoost不是直接最小化l 损失函数作为训练的目标,而是要在上述公式基础上加上树的复杂度。为什么要加树的复杂度?就是为了避免过拟合(在训练集上表现很好,但是在测试集或待预测数据上表现很差),这里的复杂度就是常说的正则项
所以,最后的目标函数就变为:

变量解释:

(1)l 代表损失函数

      梯度提升算法中都存在着损失函数。不同于逻辑回归和SVM等算法中固定的损失函数写法,集成算法中的损失函数是可选的,要选用什么损失函数取决于我们希望解决什么问题,以及希望使用怎样的模型。比如说,如果我们的目标是进行回归预测,那我们可以选择调节后的均方误差RMSE作为我们的损失函数。如果我们是进行分类预测,那我们可 以选择错误率error或者对数损失log_loss。只要我们选出的函数是一个可微的,能够代表某种损失的函数,它就可以是我们XGB中的损失函数。

(2)y\widehat{}_{i}是第 i 个样本 x_{i} 的预测值。由于XGBoost是一个加法模型,因此,预测得分是每棵树打分的累加之和。

先给出模型最终预测结果,假设第k次生成的CART树(也可以称为残差树)是 f_{k},则经过T轮之后(也就是一共有T棵树),最终模型对于样本x_{i}的预测值为y\widehat{}_{i}x_{i}为第i个样本的输入值,T代表树的数量)

其中,第t棵树时,总模型的预测值是前t-1棵树的预测值加上第t棵树的预测值(代表第t棵树):

(3)将全部k棵树的复杂度进行求和,添加到目标函数中作为正则化项,用于防止模型过拟合。

其中树复杂度是k棵树复杂度的求和。在模型求第k棵树时,其实前k-1棵树的复杂度已经知道了。

由于训练第t棵树时,前面的t-1棵树已经全部训练好了,其参数已经确定,所以前面树的复杂度也已经确定,这里用const表示,目标函数变为:

我们还可以从另一个角度去理解我们的目标函数。

回忆一下我们曾经在随机森林中讲解过的方差-偏差困境。在机器 学习中,我们用来衡量模型在未知数据上的准确率的指标,叫做泛化误差(Genelization Error)。一个集成模型(f) 在未知数据集(D)上的泛化误差 ,由方差(var),偏差(bais)和噪声(ε)共同决定,而泛化误差越小,模型就越理想。从下面的图可以看出来,方差和偏差是此消彼长的,并且模型的复杂度越高,方差越大,偏差越小。

方差可以被简单地解释为模型在不同数据集上表现出来地稳定性,而偏差是模型预测的准确度。那方差-偏差困境就可以对应到我们的object中

第一项是衡量我们的偏差,模型越不准确,第一项就会越大。

第二项是衡量我们的方差,模型越复杂,模型的学习就会越具体,到不同数据集上的表现就会差异巨大,方差就会越大。

所以我们求解 的最小值,其实是在求解方差与偏差的平衡点,以求模型的泛化误差最小,运行速度最快。我们知道树模型和树的集成模型都是学习天才,是天生过拟合的模型,因此大多数树模型最初都会出现在图像的右上方,我们必须通过剪枝来控制模型不要过拟合。现在 XGBoost的损失函数中自带限制方差变大的部分,也就是说XGBoost会比其他的树模型更加聪明,不会轻易落到图像 的右上方。可见,这个模型在设计的时候的确是考虑了方方面面,无怪XGBoost会如此强大了。

四  XGBoost算法说明

1. Boosting tree模型

XGBoost 所应用的算法就是 gradient boosting decision tree,既可以用于分类也可以用于回归问题中。

Gradient boosting 就是通过加入新的弱学习器,来努力纠正前面所有弱学习器的残差,最终这样多个学习器相加在一起用来进行最终预测,准确率就会比单独的一个要高。之所以称为Gradient,是因为在添加新模型时使用了梯度下降算法来最小化的损失。和传统的boosting tree模型一样,xgboost的提升模型也是采用的残差(或梯度负方向),不同的是分裂结点选取的时候不一定是最小平方损失。

在XGBoost中我们无法使用梯度下降,原因是XGB的损失函数没有需要求解的参数。我们在传统梯度下降中迭代的是参数,而我们在XGB中迭代的是树,树不是数字组成的向量,并且其结构不受到特征矩阵取值大小的直接影响,尽管这个迭代过程可以被类比到梯度下降上,但真实的求解过程却是完全不同。

在求解XGB的目标函数的过程中,我们考虑的是如何能够将目标函数转化成更简单的,与树的结构直接相关的写法, 以此来建立树的结构与模型的效果(包括泛化能力与运行速度)之间的直接联系。也因为这种联系的存在,XGB的目标函数又被称为“结构分数“

2. 树的集成

我们要学习的是那些函数𝑓,每个函数都包含了树的结构和叶节点的分数。一次性地学习出所有的树是很棘手的,在XGBoost中采用“加性策略”(additive strategy)学习模型:保持学习到的结果不变,每次添加一棵新的树。 如果y\widehat{}_{i}^{(t)}表示第t步的预测值,则有:

3. 学习第t棵树

那么我们每次都要留下哪棵树?一个很自然的想法就是选择可以优化我们目标函数的那棵树。

XGBoost 是一个加法模型,假设我们第t次迭代要训练的树模型是 f(t) ,则有:

前面 t−1棵树的模型复杂度是一个常数,将上式带入目标函数 Obj ,可以得到:

这里我们将正则化项进行了拆分,由于前 t-1棵树的结构已经确定,因此,前 t-1 棵树的复杂度之和可以用一个常量表示:

采用平方损失作为损失函数,可以变为下面的形式

4. 目标函数中的损失函数部分(第一部分)泰勒公式展开

在许多算法的解法推导中,求解导数都是为了让一阶导数等于0来求解极值,而现在求解导数只是为了配合泰勒展开中的形式,仅仅是简化公式的目的罢了

我们把\widehat{y}_{i}^{t-1}当作x,f_{t}(x_{i})当作\bigtriangleup x,则损失函数的第一部分变为:

求导后,将第t棵树之前的预测值(前面所有树预测值的和)带进去之后的结果,最终的目标函数只依赖于每个数据点的在误差函数上的一阶导数和二阶导数。即损失函数 l 关于 y‘(t-1) 的一阶偏导数二阶偏导数

这么写的原因很明显,由于之前的目标函数求最优解的过程中只对平方损失函数时候方便求,对于其他的损失函数变得很复杂,通过二阶泰勒展开式的变换,这样求解其他损失函数变得可行了

通过以上分析可知:

  1. 对于N个样本,则会有N个g_{i}h_{i}需要计算
  2. 每一个样本可以求得一个g_{i}与一个h_{i},所以这里可以以并行的方式求取各个样本对应的与(这也是XGBoost快的原因之一)
  3. 对于每棵树的求取过程中,涉及到对损失函数的二次求导,所以XGBoost可以自定义损失函数,只需损失函数二次可微即可

此时,目标函数变为:
 

l(y_{i}, \widehat{y}_{i}^{t-1})就是前t-1棵树与真实值的一个误差值,是一个常量,故最小化目标函数可以将其移除。

5. 目标函数中的损失函数部分(第二部分)

通过前文分析,f(x)就是代表一棵树模型,设想一下,树模型就是给定一个输入样本,经过模型之后会将其划分到某个叶子节点上,并会给出该叶子节点预测值

我们重新定义一颗树,包括两个部分:

  • 叶子结点的权重向量 ω

  • 实例 -> 叶子结点的映射关系q(本质是树的分支结构)

一棵树的表达形式定义如下:

我们定义一颗树的复杂度 Ω,它由两部分组成:

  • 叶子结点的数量;

  • 叶子结点权重向量的L2范数;

把树拆分成结构部分q和叶子权重部分w,树越复杂,叶子节点越多,越容易过拟合,所以要限制

树的复杂度函数和样例(惩罚项),最终的模型公式中控制这部分的比重, 对应XGBoost参数中的 lambda ,gamma

完整的过程如下所示:

6. 损失函数的转换

对于决策树而言,每个被放入模型的任意样本最终一个都会落到一个叶子节点上。对于回归树,通常来说每个叶子节点上的预测值是这个叶子节点上所有样本的标签的均值。但值得注意的是,XGB作为普通回归树的改进算法,在pred上却有所不同, 对于XGB来说,每个叶子节点上会有一个预测分数(prediction score),也被称为叶子权重。这个叶子权重就是所有在这个叶子节点上的样本在这一棵树上的回归取值, 用f_{k}(x_{i})w 表示

当有多棵树的时候,集成模型的回归结果就是所有树的预测分数之和,假设这个集成模型中总共有K棵决策树,则整个模型在这个样本i上给出的预测结果为:

基于这个理解,我们来考虑每一棵树。对每一棵树,它都有自己独特的结构,这个结构即是指叶子节点的数量,树的深度,叶子的位置等等所形成的一个可以定义唯一模型的树结构。在这个结构中,我们使用q(x_{i})表示样本x{_{i}}所在的叶子节点,并且使用w_{q(x_{i})}来表示这个样本落到第t棵树上的第q(x_{i})个叶子节点中所获得的分数,于是有:

这是对于每一个样本而言的叶子权重,然而在一个叶子节点上的所有样本所对应的叶子权重是相同的。设一棵树上总共包含了T个叶子节点,其中每个叶子节点的索引j ,则这个叶子节点上的样本权重是w_{j}, 依据这个,我们定义模型的复杂度(注意这不是唯一可能的定义,我们当然还可以使用其他的定义,只要满足叶子越多/深度越大, 复杂度越大的理论,我们可以自己决定自己的模型的复杂度)

我们定义了树和树的复杂度的表达式,树我们使用叶子节点上的预测分数来表达,而树的复杂度则是叶子数目加上正则项:

现在第t棵树的结构已经被确定为q,我们可以将树的结构带入我们的损失函数,来继续转化我们的目标函数。转化目标函数的目的是:建立树的结构(叶子节点的数量)与目标函数的大小之间的直接联系,以求出在第t次迭代中需要求解的最优的树 f_{t}。注意,我们假设我们使用的是L2正则化(这也是参数lambda和alpha的默认设置,lambda 为1,alpha为0),因此接下来的推导也会根据L2正则化来进行。

橙色框中的转化是如何实现的?

为进一步简化该式,我们进行如下定义:

含义如下:

  • Gj :叶子结点 j 所包含样本一阶偏导数累加之和,是一个常量;

  • Hj :叶子结点 j 所包含样本二阶偏导数累加之和,是一个常量;

将 Gj 和 Hj 带入目标式Obj,得到我们最终的目标函数(注意,此时式中的变量只剩下第t棵树的权重向量W):

7. 树结构打分

那回到我们的目标函数 Obj,该如何求出它的最值呢?

对于每个叶子结点 j , 可以将其从目标式 Obj 中拆解出来:

Gj 和 Hj 相对于第 t 棵树来说是可以计算出来的。那么,这个式子就是一个只包含一个变量 叶子结点权重wj 的一元二次函数,上面也提到了,我们可以通过最值公式求出它的最值点。

再次分析一下目标函数Obj,可以发现,各个叶子结点的目标子式是相互独立的,也就是说,当每个叶子结点的子式都达到最值点时,整个目标函数式Obj才达到最值点。

其中每个取值下都是一个以 为自变量的二次函数 ,我们的目标是追求让最小,只要单独的每一个叶子 取 值下的二次函数都最小,那他们的加和必然也会最小。于是,我们在上对w_{j}求导,让一阶导数等于0以求极值, 可得:

那么,假设目前树的结构已经固定,,我们可以轻易求出,每个叶子结点的权重 wj* 及其此时达到最优的 Obj 的目标值:

到了这里,大家可能已经注意到了,比起最初的损失函数 + 复杂度的样子,我们的目标函数已经发生了巨大变化。我们的样本量已经被归结到了每个叶子当中去,我们的目标函数是基于每个叶子节点,也就是树的结构来计算。所以我们的目标函数又叫做“结构分数”(structure score),分数越低,树整体的结构越好。如此,我们就建立了树的结构(叶子)和模型效果的直接联系

树结构的打分演示

五  一棵树的生成过程

1. 分裂一个结点

贪婪算法指的是控制局部最优来达到全局最优的算法,决策树算法本身就是一种使用贪婪算法的方法。XGB作为树的 集成模型,自然也想到采用这样的方法来进行计算,所以我们认为,如果每片叶子都是最优,则整体生成的树结构就 是最优,如此就可以避免去枚举所有可能的树结构。
回忆一下决策树中我们是如何进行计算:我们使用基尼系数或信息熵来衡量分枝之后叶子节点的不纯度,分枝前的信 息熵与分治后的信息熵之差叫做信息增益,信息增益最大的特征上的分枝就被我们选中,当信息增益低于某个阈值 时,就让树停止生长。

在XGB实际训练过程中,使用的方式是类似的: 首先使用目标函数来衡量树的结构的优劣,然后让树从深度0开始生长,每进行一次分枝,我们就计算目标函数减少了多少,当目标函数的降低低于我们设定的某个阈值时,就让树停止生长。

从树深为0时开始:

  • 对树中的每个叶子结点尝试进行分裂;

  • 每次分裂后,原来的一个叶子结点继续分裂为左右两个子叶子结点,原叶子结点中的样本集将根据该结点的判断规则分散到左右两个叶子结点中;

  • 新分裂一个结点后,我们需要检测这次分裂是否会给损失函数带来增益,增益的定义如下:

来个具体的例子,在这张图中,我们有中间节点“是否是男性”,这个中间节点下面有两个叶子节点,分别是样本弟弟和妹妹。我们来看看这个分枝点上,树的结构分数之差如何表示。

对于中间节点这一个叶子节点而言,我们的 T=1,则这个节点上的结构分数为:

对于弟弟和妹妹节点而言,则有:

则分枝后的结构分数之差为:

其中,T=1,划分之前只有一个叶子节点。

分割后的损失值为:

其中T=2,因为划分之后就变成两个叶子节点。则用分割前损失减去分割后损失值,得:

 如果增益Gain>0,即分裂为两个叶子节点后,目标函数下降了,那么我们会考虑此次分裂的结果。

公式说明:该公式代表根据划分属性及划分节点对样本划分之后其损失值减小了多少,所以每次选择最大的Gain值对应的划分属性及划分节点即可,这里求计算各个划分属性及节点是并行计算的。

但是,在一个结点分裂时,可能有很多个分裂点,每个分裂点都会产生一个增益,如何才能寻找到最优的分裂点呢?

其中G_{L}H_{L}从左节点(弟弟节点)上计算得出, G_{R}H_{R}从右节点(妹妹节点)上计算得出,(G_{L}+G_{R})和 (H_{L}+H_{R})从中间节点上计算得出。对于任意分枝,我们都可以这样来进行计算。在现实中,我们会对所有特征的所有分枝点进行如上计算,然后选出让目标函数下降最快的节点来进行分枝。对每一棵树的每一层,我们都进行这样 的计算,比起原始的梯度下降,实践证明这样的求解最佳树结构的方法运算更快,并且在大型数据下也能够表现不错。

2. 寻找最佳分裂点

在分裂一个结点时,我们会有很多个候选分割点,寻找最佳分割点的大致步骤如下:

  1. 遍历每个结点的每个特征;

  2. 对每个特征,按特征值大小将特征值排序;

  3. 线性扫描,找出每个特征的最佳分裂特征值;

  4. 在所有特征中找出最好的分裂点(分裂后增益最大的特征及特征值)

上面是一种贪心的方法,每次进行分裂尝试都要遍历一遍全部候选分割点,也叫做全局扫描法。

但当数据量过大导致内存无法一次载入或者在分布式情况下,贪心算法的效率就会变得很低,全局扫描法不再适用。

基于此,XGBoost提出了一系列加快寻找最佳分裂点的方案:

  • 特征预排序+缓存:XGBoost在训练之前,预先对每个特征按照特征值大小进行排序,然后保存为block结构,后面的迭代中会重复地使用这个结构,使计算量大大减小。

  • 分位点近似法:对每个特征按照特征值排序后,采用类似分位点选取的方式,仅仅选出常数个特征值作为该特征的候选分割点,在寻找该特征的最佳分割点时,从候选分割点中选出最优的一个。

  • 并行查找:由于各个特性已预先存储为block结构,XGBoost支持利用多个线程并行地计算每个特征的最佳分割点,这不仅大大提升了结点的分裂速度,也极利于大规模训练集的适应性扩展。

3. 停止生长

一棵树不会一直生长下去,下面是一些常见的限制条件。

(1) 当新引入的一次分裂所带来的增益Gain<0时,放弃当前的分裂。这是训练损失和模型结构复杂度的博弈过程。

(2) 当树达到最大深度时,停止建树,因为树的深度太深容易出现过拟合,这里需要设置一个超参数max_depth。

(3) 当引入一次分裂后,重新计算新生成的左、右两个叶子结点的样本权重和。如果任一个叶子结点的样本权重低于某一个阈值,也会放弃此次分裂。这涉及到一个超参数:最小样本权重和,是指如果一个叶子节点包含的样本数量太少也会放弃分裂,防止树分的太细,这也是过拟合的一种措施。每个叶子结点的样本权值和计算方式如下:

六  为什么要用 XGBoost?

XGBoost 就是对 gradient boosting decision tree 的实现,但是一般来说,gradient boosting 的实现比较慢的,因为每次都要先构造出一个树并添加到整个模型序列中。而 XGBoost 的特点就是计算速度快,模型表现好,这两点也正是这个项目的目标。

表现快是因为它具有这样的设计:

  • Parallelization:
    训练时可以用所有的 CPU 内核来并行化建树。
  • Distributed Computing :
    用分布式计算来训练非常大的模型。
  • Out-of-Core Computing:
    对于非常大的数据集还可以进行 Out-of-Core Computing。
  • Cache Optimization of data structures and algorithms:
    更好地利用硬件。

下图就是 XGBoost 与其它 gradient boosting 和 bagged decision trees 实现的效果比较,可以看出它比 R, Python,Spark,H2O 中的基准配置要更快。

另外一个优点就是在预测问题中模型表现非常好,下面是几个 kaggle winner 的赛后采访链接,可以看出 XGBoost 的在实战中的效果。

XGBoost的更多细节

XGBoost的基本内容和推导基本如上, 接下我们讨论一些XGBoost的细节, 进一步理解这个算法.

1.缩减因子

记不记得在Gradient Boosting的部分里, 我们为每一个基学习器都乘上了一个参数ρ? 这个参数在GBDT里是没有的, 而在XGBoost中我们加上了它, 并且将其称为缩减因子(shrinkage). 它可以降低每棵子树的权重, 进一步防止过拟合. 这个值不将其作为需要训练的参数, 一般设为0.1.

2.缺失值处理

XGBoost 还自带了缺失值处理方法. 输入的特征, 可能由于本身原因, 或者进行了One-Hot处理, 会有大量的零值. XGBoost会将这些特征分类到默认分支上. 通过枚举所有缺失值在当前结点是进入左子树还是进入右子树得到的增益最大, 来决定处理缺失值默认的方向(这也是一种贪心的思想).

3. 并行化与Block

XGBoost的另一个优点是它支持并行运算. 可Boosting不是一种串行算法吗? 实际上, XGBoost的并行指的不是在生成树方面的并行, 而是在特征和数据处理上的并行, 这得益于XGBoost系统设计中存储数据的Block结构.

XGBoost存储训练集用的数据结构称为Block, 每个Block内的数据采用CSC格式存储. CSC格式会用3个数组来存储一个稀疏矩阵, 这三个数组分别是

  • values[]: 存储非零值, 按照column-major顺序存储;
  • rowIndices[]: 存储values[]中每个元素对于的行下标;
  • colPtrs[]: colPtrs[0]=0, colPtrs[i]=colPtrs[i-1]+(原始矩阵中第i-1列非0元素的个数)

可以发现, 上面对values[]中的数据进行了排序, 而且仅需排序一次, 在以后的过程中就可以复用. 而排序这一步, 在分裂树的时候花费的时间是最多的(需要找到最大的增益), 这样就节省了大量的时间. 而通过第二和第三个数组, 我们就可以根据需要索引到原矩阵对应的位置.

而且这样做的另一个好处是在寻找最佳分裂点时可以开启多线程, 进行并行化计算, 也就是说树的多个结点可以同时进行训练, 同时进行分裂. 虽然树之间的训练无法并行, 但树的各个结点之间的训练可以做到并行.

不过这种做法的坏处是我们在取梯度时要通过索引来选取, 这会导致非连续的内存访问(因为现在的储存是按大小顺序进行的), 这可能使得CPU缓存命中率低, 从而影响算法效率, 在采用exact greedy作为分裂方法时尤为如此.

因此, 当使用exact greedy时, 可以采用缓存预取, 具体来说, 就是对每个线程分配一个连续的buffer,读取梯度信息并存入Buffer中, 然后再统计梯度信息. 该方式在训练样本数大的时候特别有用.

而在近似算法中, 可以对Block的大小进行合理设置, 设置合适的大小是很重要的, 设置过大则容易导致命中率低, 过小则容易导致并行化效率不高. 定义Block的大小为Block中最多的样本数, 经过实验, 发现216比较好.

最后, 如果数据量大到不能全部载入内存, XGBoost还能将数据划分为多个Block储存到硬盘上. 而为了加快这种情况下的IO速度, 还采用了Block压缩和Block拆分两种策略. XGBoost还提供了分布式计算的支持, 可以参考我的其他博客文章

七  XGBoost VS GBDT

区别:
1.xgboost和GBDT的一个区别在于目标函数上。
在xgboost中,损失函数+正则项。 GBDT中一般只有损失函数。
2.xgboost中利用二阶导数的信息,而GBDT只利用了一阶导数, 即在GBDT回归中利用了残差的概念。
3.xgboost在建树的时候利用的准则来源于目标函数推导,即可以理解为牛顿法。而GBDT建树利用的是启发式准则。
4.xgboost中可以自动处理空缺值,自动学习空缺值的分裂方向,GBDT(sklearn版本)不允许包含空缺值。

相似点:
1.xgboost和GBDT的学习过程都是一样的,都是基于Boosting的思想,先学习前n-1个学习器,然后基于前n-1个学习器学习第n个学习器。而且其都是将损失函数和分裂点评估函数分开了。
2.建树过程都利用了损失函数的导数信息,。
3.都使用了学习率来进行Shrinkage,从前面我们能看到不管是GBDT还是xgboost,我们都会利用学习率对拟合结果做缩减以减少过拟合的风险。

  • 传统GBDT以CART作为基分类器,XGBoost是GB算法的高效实现,XGBoost中的基学习器除了可以是CART(gbtree)也可以是线性分类器(gblinear), 传统GBDT以CART作为基分类器,XGBoost支持线性分类器,这个时候XGBoost相当于带L1和L2正则化项的逻辑斯蒂回归(分类问题)或者线性回归(回归问题)
  • 传统GBDT在优化时只用到一阶导数信息,XGBoost则对代价函数进行了二阶泰勒展开,同时用到了一阶和二阶导数, 二阶的泰勒展开,精度更高。顺便提一下,XGBoost工具支持自定义代价函数,只要函数可一阶和二阶求导。
  • XGBoost在代价函数里加入了正则项,用于控制模型的复杂度。正则项里包含了树的叶子节点个数、每个叶子节点上输出的score的L2模的平方和。从Bias-variance tradeoff角度来讲,正则项降低了模型的variance,使学习出来的模型更加简单,防止过拟合,这也是XGBoost优于传统GBDT的一个特性。
  • 列抽样(column subsampling)。XGBoost借鉴了随机森林的做法,支持列抽样,不仅能降低过拟合,还能减少计算,这也是XGBoost异于传统GBDT的一个特性。
  • xgboost考虑了训练数据为稀疏值的情况,可以为缺失值或者指定的值指定分支的默认方向,这能大大提升算法的效率,paper提到50倍。
  • XGBoost工具支持并行。boosting不是一种串行的结构吗?怎么并行的?注意XGBoost的并行不是tree粒度的并行,XGBoost也是一次迭代完才能进行下一次迭代的(第t次迭代的代价函数里包含了前面t-1次迭代的预测值)。XGBoost的并行是在特征粒度上的。我们知道,决策树的学习最耗时的一个步骤就是对特征的值进行排序(因为要确定最佳分割点),XGBoost在训练之前,预先对数据进行了排序,然后保存为block结构,后面的迭代中重复地使用这个结构,大大减小计算量。这个block结构也使得并行成为了可能,在进行节点的分裂时,需要计算每个特征的增益,最终选增益最大的那个特征去做分裂,那么各个特征的增益计算就可以开多线程进行。
  • 可并行的近似直方图算法。树节点在进行分裂时,我们需要计算每个特征的每个分割点对应的增益,即用贪心法枚举所有可能的分割点。当数据无法一次载入内存或者在分布式情况下,贪心算法效率就会变得很低,所以XGBoost还提出了一种可并行的近似直方图算法,用于高效地生成候选的分割点。
  • 在寻找最佳分割点时,考虑传统的枚举每个特征的所有可能分割点的贪心法效率太低,xgboost实现了一种近似的算法。大致的思想是根据百分位法列举几个可能成为分割点的候选者,然后从候选者中根据上面求分割点的公式计算找出最佳的分割点。
  • 按照特征列方式存储能优化寻找最佳的分割点,但是当以行计算梯度数据时会导致内存的不连续访问,严重时会导致cache miss,降低算法效率。paper中提到,可先将数据收集到线程内部的buffer,然后再计算,提高算法的效率。
  • xgboost 还考虑了当数据量比较大,内存不够时怎么有效的使用磁盘,主要是结合多线程、数据压缩、分片的方法,尽可能的提高算法的效率。

 树节点分裂优化

选择候选分割点针对GBDT进行了多个优化。正常的树节点分裂时公式如下:

对于特征的值有缺失的样本,xgboost可以自动学习出它的分裂方向。XGBoost树节点分裂时,虽然也是通过计算分裂后的某种值减去分裂前的某种值,从而得到增益。但是相比GBDT,它做了如下改进:

  • 通过添加阈值gamma进行了剪枝来限制树的生成
  • 通过添加系数lambda对叶子节点的值做了平滑,防止过拟合。
  • 在寻找最佳分割点时,考虑传统的枚举每个特征的所有可能分割点的贪心法效率太低,XGBoost实现了一种近似的算法,即:根据百分位法列举几个可能成为分割点的候选者,然后从候选者中根据上面求分割点的公式计算找出最佳的分割点。
  • 特征列排序后以块的形式存储在内存中,在迭代中可以重复使用;虽然boosting算法迭代必须串行,但是在处理每个特征列时可以做到并行。

xgboost算法的步骤和GB基本相同,都是首先初始化为一个常数,gb是根据一阶导数ri,xgboost是根据一阶导数gi和二阶导数hi,迭代生成基学习器,相加更新学习器

寻找最佳分割点

1.贪心算法

对每个特征都做线性搜索,根据样本特征的排序值,逐步将更多的样本加入到左子树,根据增益函数得到的最大的点,即为最优的分割点。

  1. 从当前节点出发,依次为m个特征,将属于该节点上的样本按照特征的取值进行升序排列,依次枚举出分裂点,根据增益函数得到该分裂点的增益值,保存中间结果;
  2. 选择增益最大的分裂点,作为当前节点的最佳分裂点
  3. 回到第一步,并行地为其他的节点选择最佳的分裂点(layer-wise树生长方式容易并行)

2.近似算法

由于贪心算法需要对每个特征进行排序,因此成为计算速度的瓶颈。近似算法就是根据特征的分布提前计算得到l个分位点,有了这些分位点,就可以将样本映射到对应的区间内,然后再次聚合区间内的信息,得到所有区间的最佳分裂点。很直观, 这种方式免去了贪心算法需要多次对每个特征进行排序的操作。

 从算法流程图中,可以看到,第一步是提前计算,或者每次分裂的时候计算各个特征的分位点,而在第二个for循环中,是分别为每个特征,统计得到落在各个区间的样本的一阶导数和二阶导数汇总起来,得到对应区间的G,H.对比一下贪心算法中的第二个for循环,是在样本级别统计分在当前节点的左,右子树的一阶导数和二阶导之和GL GR.

下图,是从网上找到一张示意图, 用于直观的描述近似算法是如何找到最佳分裂点的过程;

  1.  根据特征的分布,找到3分位点,将当前节点下的样本分成了3份;
  2. 根据损失函数,得到各个样本在某次迭代过程中的一阶导数g,和二阶导h
  3. 将各个区间内的样本的一阶导数和二阶导数进行求和汇总,分别得到(G1, H1) (G2, H2), (G3, H3)
  4. 利用与贪心算法相同的方法,寻找区间最佳的分裂点,由于只有3个区间,因此只有2两组合1与2,3  或 1,2与3.

简单总结贪心算法与近似算法

  1. 贪心算法需要为每个特征进行线性搜索,枚举出所有的分裂点,而近似算法只需要统计分位点信息,枚举分位的取值即可;
  2. 贪心算法在样本级别寻找特征的最佳分裂点,而近似算法在区间级别;查找的次数大大减少

xgboost和深度学习的关系

陈天奇在Quora上的解答如下:
不同的机器学习模型适用于不同类型的任务。深度神经网络通过对时空位置建模,能够很好地捕获图像、语音、文本等高维数据。而基于树模型的XGBoost则能很好地处理表格数据,同时还拥有一些深度神经网络所没有的特性(如:模型的可解释性、输入数据的不变性、更易于调参等)。
这两类模型都很重要,并广泛用于数据科学竞赛和工业界。举例来说,几乎所有采用机器学习技术的公司都在使用tree boosting,同时XGBoost已经给业界带来了很大的影响。

 八  XGboost/GBDT调参

推荐GBDT树的深度6;(横向比较:DecisionTree/RandomForest需要把树的深度调到15或更高)

以下摘自知乎上的一个问答,问题和回复都很好的阐述了这个参数设置的数学原理。

【问】xgboost/gbdt在调参时为什么树的深度很少就能达到很高的精度?
  用xgboost/gbdt在在调参的时候把树的最大深度调成6就有很高的精度了。但是用DecisionTree/RandomForest的时候需要把树的深度调到15或更高。用RandomForest所需要的树的深度和DecisionTree一样我能理解,因为它是用bagging的方法把DecisionTree组合在一起,相当于做了多次DecisionTree一样。但是xgboost/gbdt仅仅用梯度上升法就能用6个节点的深度达到很高的预测精度,使我惊讶到怀疑它是黑科技了。请问下xgboost/gbdt是怎么做到的?它的节点和一般的DecisionTree不同吗?


【答】这是一个非常好的问题,题主对各算法的学习非常细致透彻,问的问题也关系到这两个算法的本质。这个问题其实并不是一个很简单的问题,我尝试用我浅薄的机器学习知识对这个问题进行回答。
  一句话的解释,来自周志华老师的机器学习教科书( 机器学习-周志华):Boosting主要关注降低偏差,因此Boosting能基于泛化性能相当弱的学习器构建出很强的集成;Bagging主要关注降低方差,因此它在不剪枝的决策树、神经网络等学习器上效用更为明显。
  随机森林(random forest)和GBDT都是属于集成学习(ensemble learning)的范畴。集成学习下有两个重要的策略Bagging和Boosting。

  • Bagging算法是这样做的:每个分类器都随机从原样本中做有放回的采样,然后分别在这些采样后的样本上训练分类器,然后再把这些分类器组合起来。简单的多数投票一般就可以。其代表算法是随机森林。
  •  Boosting的意思是这样,他通过迭代地训练一系列的分类器,每个分类器采用的样本分布都和上一轮的学习结果有关。其代表算法是AdaBoost, GBDT。

  其实就机器学习算法来说,其泛化误差可以分解为两部分,偏差(bias)和方差(variance)。这个可由下图的式子导出(这里用到了概率论公式D(X)=E(X2)-[E(X)]2)。偏差指的是算法的期望预测与真实预测之间的偏差程度,反应了模型本身的拟合能力;方差度量了同等大小的训练集的变动导致学习性能的变化,刻画了数据扰动所导致的影响。这个有点儿绕,不过你一定知道过拟合。
  当模型越复杂时,拟合的程度就越高,模型的训练偏差就越小。但此时如果换一组数据可能模型的变化就会很大,即模型的方差很大。所以模型过于复杂的时候会导致过拟合。
  当模型越简单时,即使我们再换一组数据,最后得出的学习器和之前的学习器的差别就不那么大,模型的方差很小。还是因为模型简单,所以偏差会很大。

  也就是说,当我们训练一个模型时,偏差和方差都得照顾到,漏掉一个都不行。
  对于Bagging算法来说,由于我们会并行地训练很多不同的分类器的目的就是降低这个方差(variance) ,因为采用了相互独立的基分类器多了以后,h的值自然就会靠近.所以对于每个基分类器来说,目标就是如何降低这个偏差(bias), 所以我们会采用深度很深甚至不剪枝的决策树。
  对于Boosting来说,每一步我们都会在上一轮的基础上更加拟合原数据,所以可以保证偏差(bias),所以对于每个基分类器来说,问题就在于如何选择方差(variance)更小的分类器,即更简单的分类器,所以我们选择了深度很浅的决策树。

 九  XGboost 具体建树例子

一个简单的UCI数据集,一步一步的演算整个xgboost的过程。数据集如下:

这里为了简单起见,树的深度设置为3(max_depth=3),树的颗数设置为2(num_boost_round=2),学习率为0.1(eta=0.1)。另外再设置两个正则的参数λ=1, γ=0
损失函数选择logloss。 由于后面需要用到logloss的一阶导数以及二阶导数,这里先简单推导:

 1. 建立第一颗树(k=1)

建树的时候从根节点开始(Top-down greedy),在根节点上的样本有ID1~ID15。那么回顾Xgboost的算法流程,我们需要在根节点处进行分裂,分裂的时候需要计算式子

表达是:在结点处把样本分成左子结点和右子结点两个集合,然后计算增益。

而这里,我其实可以先计算每个样本的一阶导数值和二阶导数值,即按照式子(3)和(4)计算,但是这里你可能碰到了一个问题,那就是第一颗树的时候每个样本的预测的概率 是多少?这里和GBDT一样,应该说和所有的Boosting算法一样,都需要一个初始值。而在XGBoost里面,对于分类任务只需要初始化为(0,1)中的任意一个数都可以。具体来说就是参数base_score。(其默认值是0.5)

(值得注意的是base_score是一个经过sigmod映射的值,可以理解为一个概率值,提这个原因是后面建第二颗树会用到,需要注意这个细节)

这里我们也设base_score=0.5。然后我们就可以计算每个样本的一阶导数值和二阶导数值了。具体如下表:

 比如说对于ID=1样本, 由上面得到的一阶二阶导公式可得:

 那么把样本如何分成两个集合呢?这里就是上面说到的选取一个最佳的特征以及分裂点使得Gain最大。
比如说对于特征x1,一共有[1,2,3,6,7,8,9,10]8种取值(注意上表的ID不代表取值)。可以得到以下这么多划分方式。

 

 

 其他的值[6,7,8,9,10]类似,计算归总到下面表:

 从上表我们可以到,如果特征x1以x1<10分裂时可以得到最大的增益0.615205。

按照算法的流程,这个时候需要遍历下一个特征x2,对于特征x2也是重复上面这些步骤,可以得到类似的表如下:

 可以看到,以x2特征来分裂时,最大的增益是0.2186<0.615205。所以在根节点处,我们以x1<10来进行分裂。

由于设置的最大深度是3,此时只有1层,所以还需要继续往下分裂。
左子节点的样本集合  L = [1,2,3,4,5,6,7,8,9,10,11,12,14,15]
右子节点的样本集合  R = [13]
右子节点此时只剩一个样本,不需要分裂了,也就是已经是叶子结点。

计算R [13]的叶子节点w.

 那么下面就是对左子结点L 进行分裂。分裂的时候把此时的结点看成根节点,其实就是循环上面的过程,同样也是需要遍历所有特征(x1,x2) 的所有取值作为分裂点,选取增益最大的点。
这里为了说的比较清晰,也重复一下上面的过程:

先考虑特征x1,此时x1的取值有[1, 2, 3, 6, 7, 8, 9]

现在用的还是之前求出来的一阶二阶导, 并没有重新计算, 因为此时是在构建当前这棵树的第二层, 而不是构建新树, 在构建新树时才会。

 

 

 可以看到,x1选择x<3时能够获得最大的增益0.2539。

 同理,我们对x2再次遍历可以得到下表:

以看到x2在x2<2时分裂可以获得最大的增益0.4444。
比较知道,应该选择x2<2作为分裂点。
分裂后左右叶子结点的集合如下:

左子节点的样本集合  L = [1,3,5,6,8,9,10,11,15]
右子节点的样本集合  R = [2,4,7,9,12,14]

 然后继续分裂,这里就不在啰嗦了。这里直接给出第一个树的结构。 

这里有的人可能对叶子结点取值感到困惑。为何我算出来的是-0.4,可图上却是-0.04? 

这里其实和我们在GBDT中的处理一样,我们会以一个学习率来乘这个值,当完全取-0.4时说明学习率取1,这个时候很容易过拟合。所以每次得到叶子结点的值后需要乘上学习率eta=0.1,这里也是GBDT和xgboost一个共同点,大家都是通过学习率来进行Shrinkage,以减少过拟合的风险。

至此,我们学习完了第一颗树。

 2. 建立第一颗树(k=2)

这里,我们开始拟合我们第二颗树。其实过程和第一颗树完全一样。只不过对于 y_{(i,pred)} 需要进行更新,也就是拟合第二颗树是在第一颗树预测的结果基础上。这和GBDT一样,因为大家都是Boosting思想的算法。

在第一颗树里面由于前面没有树,所以初始y_{(i,pred)}=0.5 (系统or用户自己设置)

假设此时,模型只有这一颗树(K=1),那么模型对样例xi进行预测时,预测的结果表达是什么呢?
由加法模型

f_{1}(x_{i})的值是样例xi落在第一棵树上的叶子结点值. 那f_{0}(x_{i}) 是什么?这里就涉及前面提到一个问题base_score是一个经过sigmod映射后的值。

 所以,我们可以得到第一棵树预测的结果为下表(预测后将其映射成概率)

 比如对于ID=1的样本,其落在-0.04这个节点,经过sigmod后

 

 由之前计算的一阶导和二阶导的公式, 注意此处的yi是最开始的UCI数据集中的样本的标签值:

 我们可以接着得到新一轮所有样本的一阶和二阶导, 具体如下表, 注意此处之前在第一棵树中被分出去的譬如ID为13的样本,又重新放回训练数据中,因为此时是第二颗树的训练开始。

 拟合完后第二颗树如下图:

 后面的所有过程都是重复这个过程,这里就不再啰嗦了。

至此,整个xgboost的训练过程已经完了,但是其实里面还有一些细节的东西,下面已单独一个部分来说明这个部分。

xgboost如何用于特征选择?

官方文档:

(1)weight - the number of times a feature is used to split the data across all trees. 

(2)gain - the average gain of the feature when it is used in trees. 

(3)cover - the average coverage of the feature when it is used in trees.

  • weight :该特征在所有树中被用作分割样本的特征的总次数。

  • gain :该特征在其出现过的所有树中产生的平均增益。

  • cover :该特征在其出现过的所有树中的平均覆盖范围。

注意:覆盖范围这里指的是一个特征用作分割点后,其影响的样本数量,即有多少样本经过该特征分割到两个子节点。

相信很多做过数据挖掘比赛的人都利用xgboost来做特征选择。
一般我们调用xgb库的get_fscore()。但其实xgboost里面有三个指标用于对特征进行评价,而get_fscore()只是其中一个指标weight。

这个指标大部分人都很熟悉,其代表着某个特征被选作分裂的次数。
比如在前面举的例子里,我们得到这两颗树:

 

可以看到特征x1被选作分裂点的次数为6 (在同一棵树中可以反复选择一个特征),x2被选做分裂点的次数为2。 get_fscore()就是返回这个指标。

 而XGB还提供了另外两个指标,一个叫gain,一个叫cover。可以利用get_score()来选择。

那么gain是指什么呢?其代表着某个特征的平均增益

 比如,特征x1被选了6次作为分裂的特征,每次的增益假如为Gain1,Gain2,…Gain6,那么其平均增益为 (Gain1 + Gain2 + ... +Gain6) / 6

最后一个cover指的是什么呢?其代表着每个特征在分裂时结点处的平均二阶导数。
这个为了加深理解,我举个例子,这个例子用的还是UCI数据集,不过此时只有max_depth=2,num_boost_round=1

 

建树完之后,其结构如上。
在第一个结点分裂时,x1的特征增益表如下: 

第二个结点分裂时,x2的特征增益表如下:

那么特征x1的cover是如下计算的:
在x1在第一个结点处被选择分裂特征,在特征值为10时进行分裂。此时结点处的总二阶导数. 此时结点处的总二阶导数 G= 3.5+ 0.25 = 3.75

故x1的cover值为3.75。这里x1只是被分裂了一次,如果后续还有就是求平均。同样地, x2的cover值为G = 2.25+1.25 =3.5

注意:
为什么需要选择二阶导数?这个二阶导数背后有什么意义吗?我们先看看官网对cover的定义:

‘cover’ - the average coverage of the feature when it is used in trees。大概的意义就是特征覆盖了多少个样本。
里,我们不妨假设损失函数是mse,也就是我们求其二阶导数,很容易得到为常数1。也就是每个样本对应的二阶导数都是1,那么这个cover指标不就是意味着,在某个特征在某个结点进行分裂时所覆盖的样本个数吗?

  1. XGBoost详解
  2. XGBoost的树到底是怎么长出来的?分类和回归的区别在哪里
  3. (二)提升树模型:Xgboost原理与实践_anshuai_aw1的博客-CSDN博客

珍藏版 | 20道XGBoost面试题


参考:

xgboost算法原理: xgboost 算法原理_哆啦A梦的博客-CSDN博客_xgboost算法原理

xgboost原理: 树模型(六):XGBoost_雪伦的专栏-CSDN博客

XGBoost超详细推导,终于有人讲明白了!

视频学习: 机器学习第二阶段:机器学习经典算法(4)——Xgboost_哔哩哔哩_bilibili

机器学习-通俗易懂推导XGBoost公式 - 简书

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值