XGBoost是陈天奇等人开发的一个开源机器学习项目,高效地实现了GBDT算法并进行了算法和工程上的许多改进,被广泛应用在Kaggle竞赛及其他许多机器学习竞赛中并取得了不错的成绩。XGBoost本质上还是一个GBDT,但是力争把速度和效率发挥到极致,所以叫X (Extreme) GBoosted。XGBoost是一个优化的分布式梯度增强库,旨在实现高效,灵活和便携。 它在Gradient Boosting框架下实现机器学习算法。 XGBoost提供了并行树提升(也称为GBDT,GBM),可以快速准确地解决许多数据科学问题。在数据科学方面,有大量的Kaggle选手选用XGBoost进行数据挖掘比赛,是各大数据科学比赛的必杀武器;在工业界大规模数据方面,XGBoost的分布式版本有广泛的可移植性,支持在Kubernetes、Hadoop、SGE、MPI、 Dask等各个分布式环境上运行,使得它可以很好地解决工业界大规模数据的问题。XGBoost利用了核外计算并且能够使数据科学家在一个主机上处理数亿的样本数据。最终,将这些技术进行结合来做一个端到端的系统以最少的集群系统来扩展到更大的数据集上。Xgboost以CART决策树为子模型,通过Gradient Tree Boosting实现多棵CART树的集成学习,得到最终模型。
XGBoost和GBDT两者都是boosting方法,boosting方法实际采用加法模型(基函数的线性组合)与前向分布算法。XGBoost和GBDT除了工程实现、解决问题上的一些差异外,最大的不同就是目标函数的定义。因此,本文我们从目标函数开始探究XGBoost的基本原理。
XGBoost是由 k k k 个基模型组成的一个加法模型,假设我们第 t t t 次迭代要训练的树模型是 f t ( x ) f_{t}(x) ft(x) ,则有:
损失函数可由预测值 y ^ i \hat{y}_{i} y^i 与真实值 y i {y}_{i} yi进行表示:
其中, n n n为样本数量。
模型的预测精度是由模型的偏差与方差共同决定的,损失函数代表了模型的偏差,想要方差小则需要在目标函数中添加正则项,用于防止过拟合。
所以目标函数由模型的损失函数 L L L与抑制模型复杂度的正则项 Ω Ω Ω组成,目标函数的定义如下:
其中, ∑ i = 1 t Ω ( f i ) \sum_{i=1}^{t}{\Omega(f_{i})} ∑i=1tΩ(fi)是将全部 t t t颗树的复杂度进行求和,添加到目标函数作为正则项,用于防止模型过拟合。
由于xgboost是boosting方法,实际采用了加法模型与前向分布算法,以第 t t t个模型为例,模型对第 i i i个样本 x i x_i xi的预测值为:
其中, y ^ i t − 1 \hat{y}_{i}^{t-1} y^it−1是由第 t − 1 t-1 t−1个模型给出的预测值,是已知常数, f t ( x i ) f_{t}(x_i) ft(xi)是第 t t t个模型的预测值。此时,目标函数写成:
注意:上式中,只有 f t ( x i ) f_{t}(x_i) ft(xi)是变量,其余的都是已知量或可以通过已知量来计算出来的,上式中第二行到第三行的变形是将正则化项进行了拆分,由于前 t − 1 t-1 t−1颗树的结构已经确定,因此将前 t − 1 t-1 t−1颗树的复杂度之和用一个常量来表示。表示如下:
泰勒公式是将一个在 x = x 0 x=x_0 x=x0处具有 n n n阶导数的函数 f ( x ) f(x) f(x)利用关于 ( x − x 0 ) (x-x_0) (x−x0)的 n n n次多项式来逼近函数的方法。若函数 f ( x ) f(x) f(x)在包含 x 0 x_0 x0的某个闭区间 [ a , b ] [a,b] [a,b]上具有 n n n阶导数,且在开区间 ( a , b ) (a,b) (a,b)上具有 n + 1 n+1 n+1阶导数,则对闭区间 [ a , b ] [a,b] [a,b]上任意一点 x x x来说有:
其中多项式称为函数在 x 0 x_0 x0处的泰勒展开式, R n ( x ) R_{n}(x) Rn(x)是泰勒公式的余项,且是 ( x − x 0 ) (x-x_0) (x−x0)的高阶无穷小。
根据泰勒公式,把函数 f ( x + Δ x ) f(x+Δx) f(x+Δx)在 x x x处进行泰勒的二阶展开,可得如下等式:
回到xgboost的目标函数上来, f ( x ) f(x) f(x)对应的损失函数 l ( y i , y ^ i ( t − 1 ) + f t ( x i ) ) l(y_i,\hat{y}_{i}^{(t-1)}+f_{t}(x_i)) l(yi,y^i(t−1)+ft(xi)),与 f ( x + Δ x ) f(x+Δx) f(x+Δx)相比, x x x对应前 t − 1 t-1 t−1颗树的预测值 y ^ i ( t − 1 ) \hat{y}_{i}^{(t-1)} y^i(t−1), Δ x Δx Δx对应于正在训练的第 t t t颗树 f t ( x i ) f_{t}(x_i) ft(xi),损失函数可写为:
其中, g i g_i gi是损失函数的一阶导, h i h_i hi是损失函数的二阶导,这里的导是指对 y ^ i ( t − 1 ) \hat{y}_{i}^{(t-1)} y^i(t−1)求导。以平方损失函数为例:
将上述的泰勒二阶展开式,带入到xgboost的目标函数中,得到的目标函数近似值为:
由于第 t t t步时, y ^ i ( t − 1 ) \hat{y}_{i}^{(t-1)} y^i(t−1)已经是一个已知值,所以 l ( y i , y ^ i ( t − 1 ) ) l(y_i,\hat{y}_{i}^{(t-1)}) l(yi,y^i(t−1))是一个常数,其对函数优化不会产生影响。去掉全部常数项,得到的目标函数为:
我们可以得到,我们只需要求出每一步损失函数的一阶导和二阶导的值(在每一步求损失函数时,前一步的 y ^ i ( t − 1 ) \hat{y}_{i}^{(t-1)} y^i(t−1)是一个已知值,一阶导与二阶导都是常数)然后最优化目标函数,就可以得到每一步的 f ( x ) f(x) f(x),最后根据加法模型得到一个整体模型。
我们知道XGBoost的基模型不仅支持决策树,还支持线性模型,本文我们主要介绍基于决策树的目标函数。我们可以重新定义一棵决策树,其包括两个部分:
决策树的复杂度 Ω \Omega Ω 可由叶子数 T T T 组成,叶子节点越少模型越简单,此外叶子节点也不应该含有过高的权重 w w w(类比 LR 的每个变量的权重),所以目标函数的正则项由生成的所有决策树的叶子节点数量,和所有节点权重所组成的向量的 L 2 L 2 L2范式共同决定。
我们将属于第 j j j个叶子结点的所有样本 x i x_{i} xi划入到一个叶子结点的样本集合中,数学表示为: I j = { i ∣ q ( x i ) = j } I_{j} = \left\{ i|q(x_{i}) = j \right\} Ij={ i∣q(xi)=j},那么XGBoost的目标函数可以写成:
上式中第二行到第三行的解释:第二行是遍历所有样本后求每一个样本的损失函数。但是样本最终会落在叶子节点上,所以我们可以遍历叶子节点,然后获取叶子节点上的样本集合,最后再求损失函数。即我们之前是单个样本,现在改成了叶子节点集合,由于一个叶子节点有多个样本存在,因此有了 ∑ i ∈ I j g i \sum_{i\in{I_j}}g_i ∑i∈Ijgi和 ∑ i ∈ I j h i \sum_{i\in{I_j}}h_i ∑i∈Ijhi这两项, w j w_j wj是第 j j j个叶子节点的权重
为了简化表达式,我们定义 G i = ∑ i ∈ I j g i G_i=\sum_{i\in{I_j}}g_i Gi=∑i∈Ijgi, H j = ∑ i ∈ I j h i H_j=\sum_{i\in{I_j}}h_i Hj=∑i∈Ijhi,含义如下:
将 G i G_i Gi和 H i H_i Hi代入目标函数,则最终目标函数的表达式为:
这里我们要注意 G i G_i Gi和 H i H_i Hi是前 t − 1 t − 1 t−1 步得到的结果,其值已知可视为常数,只有最后一棵树的叶子节点 w j w_j wj不确定。
假如有一个一元二次函数形式如下:
利用一元二次函数最值公式很容易得到最值点:
那么xgboost的最终目标函数 O b j ( t ) Obj^{(t)} Obj(t),该如何求出它的最值?
那么,假设目前树的结构已经固定,套用一元二次函数的最值公式,将目标函数对 w j w_j wj求一阶导,并令其等于0,则可以求得叶子节点 j j j对应的权值:
目标函数可以化简为:
上图给出目标函数计算的例子,求每个节点每个样本的一阶导数 g i g_{i} gi和二阶导数 h i h_{i} hi,然后针对每个节点对所含样本求和得到 G j G_{j} Gj和 H j H_{j} Hj,最后遍历决策树的节点即可得到目标函数。
在实际训练过程中,当建立第 t t t颗树时,一个最关键的问题是如何找到叶子节点的最优切分点,xgboost支持两种分裂节点的方法—贪心法和近似算法。
从树的深度为0开始:
那么如何计算每一个特征的分裂收益呢?
假设我们在某一个节点完成特征分裂,则分裂前的目标函数为:
分裂后的目标函数为:
则对于目标函数来说,分裂后的收益为:
观察分裂后的收益,我们会发现节点划分不一定会使得结果变好,因为我们有一个引入新叶子的惩罚项,也就是说引入的分割带来的增益如果小于一个阀值的时候,我们可以剪掉这个分割。
注意:该特征收益可以作为特征重要性输出的重要依据
对于每次分裂,我们需要枚举出所有特征可能的分割方案,如何高效地枚举所有的分割呢?
假设我们要枚举某个特征所有 x < a xx<a条件的样本,对于某个特征的分割点 a a a,我们要计算 a a a左边与右边的导数和。
我们可以发现对于所有的分裂点 a a a ,只要做一遍从左到右的扫描就可以枚举出所有分割的梯度和 G L 、 G R G_L、G_R GL、GR。然后用上面的公式计算每个分割方案的收益就可以了。
贪心算法可以给出精确解,但是当数据不能完全加载到内存时,精确贪心算法会变得 非常低效,算法在计算过程中需要不断在内存与磁盘之间进行数据交换,这是个非常耗时的过程, 并且在分布式环境中面临同样的问题。为了能够更高效地选 择最优特征及切分点, XGBoost提出一种近似算法来解决该问题。 基于直方图的近似算法的主要思想是:对某一特征寻找最优切分点时,首先对该特征的所有切分点按分位数 (如百分位) 分桶, 得到一个候选切分点集。特征的每一个切分点都可以分到对应的分桶; 然后,对每个桶计算特征统计G和H得到直方图, G为该桶内所有样本一阶特征统计g之和, H为该桶内所有样本二阶特征统计h之和; 最后,选择所有候选特征及候选切分点中对应桶的特征统计收益最大的作为最优特征及最优切分点。
对于每个特征,只考察分位点可以减小计算复杂度。近似算法首先根据特征分布的分位数提出了候选划分点,然后将连续型特征映射到由这些候选点划分的桶中(分桶),然后聚合统计信息找到所有区间的最佳分裂点。在提出候选划分点时有两种策略:
下图给出不同种分裂策略的AUC变化曲线,横坐标为迭代次数,纵坐标为测试集AUC,eps
为近似算法的精度,其倒数为桶的数量。
从上图我们可以看到, Global 策略在候选点数多时(eps
小)可以和 Local 策略在候选点少时(eps
大)具有相似的精度。此外我们还发现,在eps
取值合理的情况下,分位数策略可以获得与贪心算法相同的精度。全局策略需要更细的分桶才能达到本地策略的精确度, 但全局策略在选取候选切分点集合时比本地策略更简单。在XGBoost系统中, 用户可以根据需求自由选择使用精确贪心算法、近似算法全局策略、近似算法本地策略, 算法均可通过参数进行配置。
近似算法简单来说,就是根据特征 k k k的分布来确定 l l l个候选分位点 S k = { s k 1 , s k 2 , . . . , s k l } S_k=\left\{s_{k1},s_{k2},...,s_{kl}\right\} Sk={ sk1,sk2,...,skl},然后根据这些候选切分点把相应的样本放入对应的桶中,对每个桶的 G 、 H G、H G、H进行累加,最后在候选集合上贪心查找。该算法描述如下:
算法讲解:
下图给出近似算法的具体例子,以三分位为例:
根据样本特征进行排序,然后基于分位数进行划分,并统计三个桶内的 G 、 H G、H G、H值,最终求解节点划分的增益。
实际上,XGBoost不是简单地按照样本个数进行分位,而是以二阶导数值 h i h_i hi作为样本的权重进行划分。为了处理带权重的候选切分点的选取,作者提出了Weighted Quantile Sketch
算法。加权分位数略图算法提出了一种数据结构,这种数据结构支持merge和prune操作。在arXiv的最新版XGBoost论文中APPENDIX部分有该算法详细的描述,地址:https://arxiv.org/abs/1603.02754
。现在我们简单介绍加权分位数略图侯选点的选取方式,如下:
那么为什么要用二阶梯度 h i h_i hi进行样本加权呢?
我们知道模型的目标函数为:
我们把目标函数配方整理成以下形式,可以看出 h i h_i hi有对loss
的加权作用。
其中,加入 1 2 g i 2 h i \frac{1}{2}\frac{g_i^2}{h_i} 21higi2是因为 g i g_i gi和 h i h_i hi是上一轮的损失函数求导是常数。我们可以看到 h i h_i hi就是平方损失函数中样本的权重。
实际工程中一般会出现输入值稀疏的情况。比如数据的缺失、one-hot编码都会造成输入数据稀疏。XGBoost在构建树的节点过程中只考虑非缺失值的数据遍历,而为每个节点增加了一个缺省方向,当样本相应的特征值缺失时,可以被归类到缺省方向上,最优的缺省方向可以从数据中学到。至于如何学到缺省值的分支,其实很简单,分别枚举特征缺省的样本归为左右分支后的增益,选择增益最大的枚举项即为最优缺省方向。
在构建树的过程中需要枚举特征缺失的样本,乍一看这个算法会多出相当于一倍的计算量,但其实不是的。因为在算法的迭代中只考虑了非缺失值数据的遍历,缺失值数据直接被分配到左右节点,所需要遍历的样本量大大减小。作者通过在Allstate-10K数据集上进行了实验,从结果可以看到稀疏算法比普通算法在处理数据上快了超过50倍。
在树的生成过程中,最耗时的一个步骤是在每次寻找最佳分裂点时都需要对特征的值进行排序。而xgboost在训练之前会根据特征对数据进行排序,然后保存到块结构中,并在每个块结构中都采用了稀疏矩阵存储格式(CSC)进行存储,后面的训练过程中会重复的使用块结构,可以大大减小工作量。
作者提出通过按特征进行分块并排序,在块里面保存排序后的特征值及对应样本的引用,以便于获取样本的一阶、二阶导数值。具体方式如图:
通过顺序访问排序后的块遍历样本特征的特征值,方便进行切分点的查找。此外分块存储后多个特征之间互不干涉,可以使用多线程同时对不同的特征进行切分点查找,即特征的并行化处理。在对节点进行分裂时需要选择增益最大的特征作为分裂,这时各个特征的增益计算可以同时进行,这也是 XGBoost 能够实现分布式或者多线程计算的原因。
列块并行学习的设计可以减少节点分裂时的计算量,在顺序访问特征值时,访问的是一块连续的内存空间,但通过特征值持有的索引(样本索引)访问样本获取一阶、二阶导数时,这个访问操作访问的内存空间并不连续,这样可能造成cpu缓存命中率低,影响算法效率。
为了解决缓存命中率低的问题,XGBoost 提出了缓存访问算法:为每个线程分配一个连续的缓存区,将需要的梯度信息存放在缓冲区中,这样就实现了非连续空间到连续空间的转换,提高了算法效率。此外适当调整块大小,也可以有助于缓存优化。
当数据量非常大时,我们不能把所有的数据都加载到内存中。那么就必须将一部分需要加载进内存的数据先存放在硬盘中,当需要时再加载进内存。这样操作具有很明显的瓶颈,即硬盘的IO操作速度远远低于内存的处理速度,肯定会存在大量等待硬盘IO操作的情况。针对这个问题作者提出了“核外”计算的优化方法。具体操作为,将数据集分成多个块存放在硬盘中,使用一个独立的线程专门从硬盘读取数据,加载到内存中,这样算法在内存中处理数据就可以和从硬盘读取数据同时进行。此外,XGBoost 还用了两种方法来降低硬盘读写的开销:
附:
上文转载于深入了解XGBoost
如果对您有帮助,麻烦点赞关注,这真的对我很重要!!!如果需要互关,请评论留言!