深入理解XGBoost

目标函数推导

目标函数的基本形式

模型对于某个样本的预测值:
在这里插入图片描述
f k f_{k} fk是基学习器,最终模型是多个基学习器
最初的目标函数可以写成
在这里插入图片描述
y ^ i ( t − 1 ) \hat{y}_{i}^{(t-1)} y^i(t1):是前t-1个集成学习器对样本的预测值
f t ( x i ) f_{t}\left(\mathbf{x}_{i}\right) ft(xi):是当前学习器对样本的预测值
Ω ( f t ) \Omega\left(f_{t}\right) Ω(ft):是第t个学习器的正则项

对目标函数进行泰勒二阶展开

在这里插入图片描述
g i = ∂ y ^ ( t − 1 ) l ( y i , y ^ ( t − 1 ) ) g_{i}=\partial_{\hat{y}^{(t-1)}} l\left(y_{i}, \hat{y}^{(t-1)}\right) gi=y^(t1)l(yi,y^(t1)) :为 l l l函数对 y ^ ( t − 1 ) \hat{y}^{(t-1)} y^(t1)的一阶导数
h i = ∂ y ^ ( t − 1 ) 2 l ( y i , y ^ ( t − 1 ) ) h_{i}=\partial_{\hat{y}^{(t-1)}}^{2} l\left(y_{i}, \hat{y}^{(t-1)}\right) hi=y^(t1)2l(yi,y^(t1)):为 l l l函数对 y ^ ( t − 1 ) \hat{y}^{(t-1)} y^(t1)的二阶导数

正则项

接下来,将正则项具体化为如下的式子
在这里插入图片描述
T T T表示叶子节点的个数,模型中将叶子节点的个数作为L1正则项,将叶子节点的权重值作为L2正则项。
这个叶子节点的权重,有的资料上成为叶子节点上输出的score,实际上就是预测值。

化简

并将原本单独的样本按照最终所在的叶子节点进行归类,令
在这里插入图片描述
q ( x i ) = j q(x_{i})=j q(xi)=j表示样本 x i x_{i} xi被划分到编号为j的叶子节点上

最终形式

在这里插入图片描述

对待求变量求导

L ~ ( t ) \tilde{\mathcal{L}}^{(t)} L~(t) w w w求偏导并令其等于0,我们就能得到最终的w的解析解如下:
在这里插入图片描述

目标函数的值

w w w代入原来的目标函数,我们就能得到原来目标函数的最终值。
在这里插入图片描述
这个式子表示在给定的决策树q下的目标函数的值,一定要注意,括号里加个q表示这是确定的一个树。

分裂点选择

分裂依据

由上面推倒出Xgboost 目标函数(损失函数)的表达形式为
在这里插入图片描述
其实际意义表示按照特定分裂点(即按照某种树的结构)分裂后产生的损失值
其中

  • I j I_{j} Ij表示被分到编号为j的个叶子节点的样本

这样,我们可以得到样本群 I I I依据某个特征值分裂后的损失函数的减小值 L s p l i t \mathcal{L}_{s p l i t} Lsplit,将其作为分裂时的依据。
其中:

  • I L 和 I R I_{L} 和 I_{R} ILIR表示分裂后形成的两拨样本

在这里插入图片描述
有了节点分裂的依据,那么我们在基学习器中就可以生成树的结构,也就如何选择分裂点。
论文中共介绍了三种分裂点选择方法,

  • 第一种“Exact Greedy Algorithm for Split Finding”,是一种利用穷举法选择最佳的分裂节点
  • 第二种“Approximate Algorithm for Split Finding”,通过加权分位数方法近似选择最佳的分裂节点
  • 第三种“Sparsity-aware Split Finding”,针对稀疏特征的分裂点选择法

(一)Exact Greedy Algorithm for Split Finding 精确贪心算法

在这里插入图片描述
算法如上图所示,核心思想如下

  • 两个for循环,第一个for遍历所有特征,第二个for找出最佳的特征值作为分裂点
  • 选分裂点的依据score为分裂前后损失函数的减少量
  • 第二个for循环中,先对数据按照特征值进行排序,这样做的目的为了后面一次遍历就能求出所有分裂点的score值。 G L 、 G R G_{L}、G_{R} GLGR只需要在当前的基础上进行加减,不需要再扫描所有数据
  • 贪心算法体现在,当前分裂点的选择只考虑能使得当前损失函数减少量最大

(二)Approximate Algorithm for Split Finding 近似分割算法

精确贪心算法虽然很强大,但是当数据无法完全加载到内存中或者在分布式的条件下,这种基于穷举的分裂点寻找方法效率就会非常低下。于是作者提出了一种近似分割的算法,这种算法首先通过加权分位数的算法选出了一些可能的分裂点,然后再遍历这些较少的分裂点来找到最佳分裂点。

加权分位数算法

近似分割算法的核心是加权分位数算法,首先介绍分位数点。比如有下面10个样本数据,已经排好序了

分位数点
value: 1 1 2 3 4 5 5 6 7 8
rank :  1 2 3 4 5 6 7 8 9 10

那么0.3分数点的值是多少呢,对应的rank为10*0.3 =3,对应的值为2。这就是分位数点的基本思想,我们只要选出一些合适的分位数点然后遍历然后取遍历他们,就能达到和穷举法相同的精确程度。

加权分位数

因为我们要均分的是loss,而不是样本的数量,而每个样本对loss的贡献可能是不一样的,按样本均分会导致loss分布不均匀,取到的分位点会有偏差。所以要在每个样本前面加个权重。而目标函数又可以化简为如下的形式,所谓我们可以将二阶偏导 h i h_{i} hi作为权重。
在这里插入图片描述
样本的权重定义好之后,我们就能定义rank函数
在这里插入图片描述
r k ( z ) r_{k}(z) rk(z)表示第k个特征中特征值小于z的样本所占的加权比例。

举个例子来理解加权分位点:
在这里插入图片描述
99对应的分位点就是(0.1 * 6+0.4+0.2)/(0.1 * 6+0.4+0.2+0.6) = 2/3

这样我们就能选出一些候选分割点 { s k 1 , s k 2 , ⋯ s k l } \left\{s_{k 1}, s_{k 2}, \cdots s_{k l}\right\} {sk1,sk2,skl},并让他们满足下面的条件:
在这里插入图片描述
这样,我们就选出了大约 1 / ϵ 1 / \epsilon 1/ϵ候选分割点,对这些分割点进行枚举遍历,选出最佳的分割点即可。

需要注意是:引入的分割不一定会使得情况变好,因为在引入分割的同时也引入新叶子的惩罚项。所以通常需要设定一个阈值,如果引入的分割带来的增益小于一个阀值的时候,我们可以剪掉这个分割。此外在XGBoost的具体实践中,通常会设置树的深度来控制树的复杂度,避免单个树过于复杂带来的过拟合问题。

关于上述两个算法的小结

QA:为什么近似分割算法比精确贪心算法要快?
首先我们得捋一下这两个寻找最佳分裂点的时候都有哪些公共的时间开销

  • 预排序:每个特征都是按照排序好的顺序存储的,这一部分在存储的时候就已经完成了
  • 计算所有样本的一阶导G和二阶导L,这个过程只需要进行一次
  • 对样本的G和L累加求和,这个过程也只需要进行一次,为了后续做差加速
    • 精确贪心算法中是将所有样本G、L累加
    • 近似分割算法中是按桶累加
  • 算法中的两个for循环,第一个循环是遍历所有特征,这一步两个算法相同

不同的开销在于,两个算法中第二个for循环中:

  • 精确贪心算法遍历的所有样本 O(#feature x # samples)
  • 近似贪心算法遍历了所有桶 : O(#feature x # bins )

因为桶的数目远小于样本数,所以得以加速

(三)Sparsity-aware Split Finding 稀疏特征处理

在这里插入图片描述
Xgboost 在处理带缺失值的特征时,先对非缺失的样本进行排序,对该特征缺失的样本先不处理,然后在遍历每个分裂点时,将这些缺失样本分别划入左子树和右子树来计算损失然后求最优。
如果训练样本中没有缺失值,而预测过程中出现了缺失值,那么样本会被默认分到右子树。

分块并行与缓存优化

分块并行

XGBoost的并行不是树粒度的而是特征粒度的,随机森林就是树粒度的并行。

寻找分裂点的时候,算法中先是遍历所有特征再遍历每个特征下的所有值。

遍历特征下所有值时要求值是排序好的,这样就可以使用差加速。
如果不排序,那么计算分类时候的损失函数减少量就没法达到O(1)的复杂度,因为二叉树的分裂是> x,分到a子树这样的形式。

在这里插入图片描述

在建树的过程中,最耗时是找最优的切分点,而这个过程中,最耗时的部分是将数据排序。为了减少排序的时间,提出Block结构存储数据。

  • Block中的数据以稀疏格式CSC进行存储
  • Block中的特征进行排序(不对缺失值排序,排序只有一次
  • Block 中特征还需存储指向样本的索引,这样才能根据特征的值来取梯度。
  • 一个Block中存储一个或多个特征的值

个人理解是,这个block是原样本的一种映射,在这个block里,"样本"是按照列存储的,其实他存储的是列,而不是样本。因为样本是按照行来组织的,block中存储的是原样本的各个排序后的列。

所以就要有列中的每个元素与原样本之间的映射关系,因为在分裂节点的选择时,不仅要遍历某个特征(即列)中的所有元素,还要用到原样本的梯度(一阶导和二阶导), 所以就要通过列中的元素找到原样本。

按照block存储的好处就是,不同列之间可以并行查找,并且因为预排序了所以使得分裂节点查找时更快。坏处是空间大了一倍

缓存优化

在分块并行中,block存储了排序的列,并建立和原来样本的一一映射,这样可以通过索来找到原始样本获得梯度,但是原始样本是存放是按照列值的原始顺序的(相邻内存的样本他们对应的列值可能不是连续的,而我们现在根据排序后的列值来找原样本,那么肯定会出现在内存上的跳跃式查找,就非连续内存访问,可能导致CPU cache命中率降低。

CPU cache命中率低对于加权分位数选择分裂点的方法没太大影响,因为其选择分裂点的时候本来就是跳跃着选的,但是对于精确贪心算法的效率影响就非常大了,因为其要遍历所有样本。

下图中,calculate 上下两个部分表示连续列值上计算G和H但是其对应的样本不连续。
红色字说明了连续列值对应的样本不连续性。
在这里插入图片描述
原论文中说

A naive implementation of split enumeration introduces immediate read/write dependency between the accumulation and the non-continuous memory fetch operation

通过降低读写的依赖性来解决cache miss的问题
1.对于精确贪心算法,对每个线程分配一个连续的缓存空间,预取接下来要读取的数据,这样就降低了直接从内存读取并且cache miss消耗的时间。
2. 对于近似分割算法,选取适当的block大小即可(2^16 * each_sample_size)

参考资料

https://www.hrwhisper.me/machine-learning-xgboost/
https://arxiv.org/pdf/1603.02754.pdf

你可能感兴趣的:(机器学习)