集成学习——XGBoost原理理解

XGBoost全称为Extreme Gradient Boosting,它是2016年陈天奇和Carlos Guestrin在国际AI顶会ACM SIGKDD(知识发现与数据挖掘会议)上的正式发布,论文详见《XGBoost: A Scalable Tree Boosting System》。在此之前,XGBoost的作者已经在各种国际数据竞赛网站例如kaggle等用该算法和这套系统取得了瞩目的成绩,也提供了开源软件包。这个算法或者说这个系统在大规模机器学习中的重要性应该学习机器学习的人都有耳闻,因此这篇博客算是在精度了原始论文(可以参考别人写的XGBoost原论文阅读翻译)之后对XGBoost做的一个理解和总结吧。

总结

由XGBoost的全称可以看出来,这个算法是在梯度提升Gradient Boosting的框架下进行的改进和优化,因此它也是串行生成的集成学习中的一类,也就是下一个学习器(CART回归树)和上一个学习器是有联系的,最后得到很多个学习器之后再求和得到最后的学习结果,但是XGBoost也稍微借鉴了一点随机森林的列子采样的手段来防止过拟合,不过总体上还是把XGBoost归为串行生成的集成学习。

作者在提出这个树提升系统时巧妙的设计了很多方法来使得这个系统可以更快的处理大量的数据,在原论文中前面主要是算法的一些推导和改进的地方,后面主要讲计算机的一些设计来并行化处理,我作为非计算机专业出身,自然比较关注前半部分,后面系统设计这块就只是大概知道,并不深入了解,这也算是我的弱项,所以这篇博客重点在于对算法原理和推导进行梳理。前面先讲算法的关键原理,它与Adaboost和GBDT的差别与联系,后面大概描述了加权分位数草图和稀疏感知算法,最后精炼总结一下系统设计的一些问题。

总的来说在算法上,XGBoost的改进主要体现在利用目标函数的二阶展式,将决策树划分的规则和权重精确给出解释表达式,而且还加上了新的惩罚项来防止过拟合。除此之外,还有两点XGBoost考虑十分周全也大大改进了算法的速度:一是给出了加权分位数草图,给出了决策树建树过程中寻找分割点的近似算法,用具有代表性的部分分割点来替代遍历,节约运行内存和时间;另外一个是给出了并行化的稀疏感知算法,令缺失值有了默认方向。

算法关键原理

与GBDT一样,XGBoost最后的输出结果也是一个加性模型的形式,即 y ^ i = F K ( x ) = k = 1 K f k ( x i ) ,    f k F \hat y_i=F_K(x)=\sum_{k=1}^Kf_k(x_i), \ \ f_k\in \mathcal{F} 其中 F \mathcal{F} 表示所有回归树的函数空间,也称为假设空间,即 f k f_k 具有这样的形式, f k ( x ) = j = 1 T w j 1 j ( x ) f_k(x)=\sum_{j=1}^Tw_j1_j(x) ,这里 1 j ( x ) 1_j(x) 表示特征空间的一个划分 j j 上的示性函数, I j I_j 表示落在划分 j j 上的集合, w j w_j 表示在该划分的单元上的权重。

与一般的机器学习问题类似,为了评估这个输出结果,需要考虑特定的损失函数,假定为 \ell ;为了防止过拟合,采用结构风险最小化(可参考李航老师的《统计学习方法》最前面部分),于是我们的目标是最小化 L ( F K ) = i = 1 n ( y i , y ^ i ) + k Ω ( f k ) \mathcal{L}(F_K)=\sum_{i=1}^n\ell(y_i,\hat y_i)+\sum_k\Omega(f_k) 其中 Ω ( f k ) = γ T + 1 2 λ w 2 \Omega(f_k)=\gamma T+{1\over 2}\lambda\|w\|^2 是惩罚项,惩罚模型的复杂度(一方面是决策树叶结点的数量 T T ,一方面是每个叶结点的权重的 L 2 L_2 范数)。关于防止过拟合,作者还受随机梯度提升树SGBT和随机森林RF的启发,可以像SGBT那样在每次迭代时增加一个学习率 η \eta ,降低对每个学习器的信任度;还可以采用RF那样的特征列子采样,增加随机性,对于只有几个关键变量起作用的模型来说效果更好。

考虑到加性模型,利用前向分步算法每次贪婪的学习一颗决策树并惩罚,所以第 t t 次迭代时的目标函数就变成了 L ( t ) = i = 1 n ( y i , y ^ i ( t 1 ) + f t ( x i ) ) + Ω ( f t ) \mathcal{L}^{(t)}=\sum_{i=1}^n\ell(y_i,\hat y_i^{(t-1)}+f_t(x_i))+\Omega(f_t)

XGBoost不像之前学的Adaboost(拟合带权重的样本集)和GBDT(拟合负梯度)一样以拟合为核心,它是以最小化损失函数为核心。其实那两种算法做拟合只是表象,他们本质都是在做优化,只是视角不一样,在理解的方式上有些微差别。

对于之前说的GBDT,它只是用了目标函数的一阶信息,让新的学习器来拟合上一轮目标函数的负梯度方向,而XGBoost用了目标函数的二阶信息。这类似于梯度下降法和牛顿法的关系,梯度下降利用一次函数逼近只是提供函数减小的方向(负梯度方向),并不告诉你要走多少步长,具体走多长还需要尝试,而牛顿法使用二次函数进行逼近,由于二次函数具有极值点,因此可以明确的给出在一次逼近中的最优值在哪里。具体来说,考虑二阶泰勒展式 f ( x + Δ ) f ( x ) + Δ f ( x ) + 1 2 Δ 2 f ( x ) f(x+\Delta)\approx f(x)+\Delta f'(x)+{1\over2}\Delta^2f''(x) 然后将 t t 次迭代的学习器当做 Δ \Delta ,损失函数当做 f f ,则 L ( t ) \mathcal{L}^{(t)} 展开为 L ( t ) i = 1 n [ ( y i , y ^ i ( t 1 ) ) + f t ( x i ) g i + 1 2 f t 2 ( x i ) h i ] + Ω ( f t ) \begin{aligned}\mathcal{L}^{(t)}\approx \sum_{i=1}^n\big[\ell(y_i,\hat y_i^{(t-1)})+f_t(x_i)g_i+{1\over2}f_t^2(x_i)h_i\big]+\Omega(f_t)\end{aligned} 其中 g i = y ^ i ( t 1 ) ( y i , y ^ i ( t 1 ) ) ,   h i = y ^ i ( t 1 ) 2 ( y i , y ^ i ( t 1 ) ) g_i=\partial_{\hat y_i^{(t-1)}}\ell(y_i,\hat y_i^{(t-1)}),\ h_i=\partial^2_{\hat y_i^{(t-1)}}\ell(y_i,\hat y_i^{(t-1)}) 。由于 t 1 t-1 已经是在前面迭代中已经定下来的了,在这一次迭代中可以当做常数,优化与它无关,所以目标函数又可以写成 L ~ ( t ) = i = 1 n [ g i f t ( x i ) + 1 2 h i f t 2 ( x i ) ] + Ω ( f t ) = i = 1 n [ g i f t ( x i ) + 1 2 h i f t 2 ( x i ) ] + γ T + 1 2 λ w 2    (带入惩罚项) = i = 1 n [ g i j = 1 T w j 1 j ( x i ) + 1 2 h i j = 1 T w j 2 1 j ( x i ) ] + γ T + 1 2 λ j = 1 T w j 2      ( T 个叶结点 f t = j = 1 T w j 1 j ( x i ) ) = i = 1 n [ j = 1 T g i w j 1 j ( x i ) + 1 2 j = 1 T h i w j 2 1 j ( x i ) ] + j = 1 T 1 2 λ w j 2 + γ T     (与j无关的放进去) (利用决策树划分单元互不相交的性质,即 { 1 , 2 , , n } = j = 1 T I j ,所以 i = 1 n = j = 1 T i I j ) = j = 1 T [ i I j g i w j + 1 2 i I j h i w j 2 + 1 2 λ w j 2 ] + γ T = j = 1 T [ ( i I j g i ) w j + 1 2 ( i I j h i + λ ) w j 2 ] + γ T \begin{aligned} \tilde{\mathcal{L}}^{(t)}&= \sum_{i=1}^n\big[g_if_t(x_i)+{1\over2}h_if_t^2(x_i)\big]+\Omega(f_t)\\ &=\sum_{i=1}^n\big[g_if_t(x_i)+{1\over2}h_if_t^2(x_i)\big]+\gamma T+{1\over 2}\lambda\|w\|^2\ \ \ \text{(带入惩罚项)}\\ &=\sum_{i=1}^n\big[g_i\sum_{j=1}^Tw_j1_j(x_i)+{1\over2}h_i\sum_{j=1}^Tw^2_j1_j(x_i)\big]+\gamma T+{1\over 2}\lambda\sum_{j=1}^Tw_j^2\ \ \ \ (T\text{个叶结点}f_t=\sum_{j=1}^Tw_j1_j(x_i))\\ &=\sum_{i=1}^n\big[\sum_{j=1}^Tg_iw_j1_j(x_i)+{1\over2}\sum_{j=1}^Th_iw^2_j1_j(x_i)\big]+\sum_{j=1}^T{1\over 2}\lambda w_j^2+\gamma T\ \ \ \ \text{(与j无关的放进去)}\\ &\text{(利用决策树划分单元互不相交的性质,即}\{1,2,\dots,n\}=\cup_{j=1}^TI_j\text{,所以}\sum_{i=1}^n=\sum_{j=1}^T\sum_{i\in I_j})\\ &=\sum_{j=1}^T\big[\sum_{i\in I_j}g_iw_j+{1\over2}\sum_{i\in I_j}h_iw^2_j+{1\over 2}\lambda w_j^2\big]+\gamma T\\ &=\sum_{j=1}^T\big[(\sum_{i\in I_j}g_i)w_j+{1\over2}(\sum_{i\in I_j}h_i+\lambda) w_j^2\big]+\gamma T \end{aligned}

这样我们需要最小化的目标函数就可以看成是关于 w j w_j 的二次函数,由二次函数的极值点公式 b / 2 a -b/2a 可以得到最优的 w j w_j w j = i I j g i i I j h i + λ w_j^*=-{\sum_{i\in I_j}g_i\over \sum_{i\in I_j}h_i+\lambda} 带入到 L ~ ( t ) \tilde{\mathcal{L}}^{(t)} 中求得最优的目标函数为 L ~ ( t ) ( I j ) = 1 2 j = 1 T ( i I j g i ) 2 i I j h i + λ + γ T \tilde{\mathcal{L}}^{(t)}(I_j)=-{1\over2}\sum_{j=1}^T{(\sum_{i\in I_j}g_i)^2\over \sum_{i\in I_j}h_i+\lambda}+\gamma T

这个最优的目标函数在某种程度上就意味着当前这种划分结构 I j I_j 对最后整个输出结果的优化目标做出的贡献,所以可以用它来作为对应划分的“评价标准”。因此,理论上说,遍历了下一次划分之后的分数 L ~ ( t + 1 ) \tilde{\mathcal{L}}^{(t+1)} 与当前的这个分数 L ~ ( t ) \tilde{\mathcal{L}}^{(t)} 的差值,下降的最多的,也就是差值越大,说明这个划分越好。这种评价标准类似信息增益或者基尼系数,但不同的是它以整个模型的优化目标为出发点,所以每一步都是在朝着最优的方向走。

假设 I L I_L I R I_R 分别表示划分之后的左子树和右子树的样本集,即 I = I L I R I=I_L\cup I_R ,则上述的评价标准为 L s p l i t = L ~ ( I ) ( L ~ ( I L ) + L ~ ( I R ) ) \mathcal{L}_{split}=\tilde{\mathcal{L}}(I)-\big(\tilde{\mathcal{L}}(I_L)+\tilde{\mathcal{L}}(I_R)\big) ,即
L s p l i t = 1 2 [ ( i I L g i ) 2 i I L h i + λ + ( i I R g i ) 2 i I R h i + λ ( i I g i ) 2 i I h i + λ ] γ \mathcal{L}_{split}={1\over2}\bigg[{(\sum_{i\in I_L}g_i)^2\over \sum_{i\in I_L}h_i+\lambda}+{(\sum_{i\in I_R}g_i)^2\over \sum_{i\in I_R}h_i+\lambda}-{(\sum_{i\in I}g_i)^2\over \sum_{i\in I}h_i+\lambda}\bigg]-\gamma

由此就得到了整个算法的关键步骤:

  1. 初始化第一颗回归树 f 1 ( x ) f_1(x)
  2. t = 2 t=2 T T 循环:
    (1) 对 i = 1 , 2 , , n i=1,2,\dots,n ,计算一阶导数和二阶导数: g i = y ^ i ( t 1 ) ( y i , y ^ i ( t 1 ) ) ,   h i = y ^ i ( t 1 ) 2 ( y i , y ^ i ( t 1 ) ) g_i=\partial_{\hat y_i^{(t-1)}}\ell(y_i,\hat y_i^{(t-1)}),\ h_i=\partial^2_{\hat y_i^{(t-1)}}\ell(y_i,\hat y_i^{(t-1)}) (2)用贪婪算法尝试不同的分割,选择 L s p l i t \mathcal{L}_{split} 最大的那个分割结构作为回归树 f t ( x ) f_t(x) L s p l i t = 1 2 [ ( i I L g i ) 2 i I L h i + λ + ( i I R g i ) 2 i I R h i + λ ( i I g i ) 2 i I h i + λ ] γ \mathcal{L}_{split}={1\over2}\bigg[{(\sum_{i\in I_L}g_i)^2\over \sum_{i\in I_L}h_i+\lambda}+{(\sum_{i\in I_R}g_i)^2\over \sum_{i\in I_R}h_i+\lambda}-{(\sum_{i\in I}g_i)^2\over \sum_{i\in I}h_i+\lambda}\bigg]-\gamma (3) 更新 F t ( x ) = F t 1 ( x ) + f t ( x ) F_t(x)=F_{t-1}(x)+f_t(x)
    或者 F t ( x ) = F t 1 ( x ) + η f t ( x ) F_t(x)=F_{t-1}(x)+\eta \cdot f_t(x) η \eta 是学习率,通常设为0.1
  3. 输出结果 y ^ = F T ( x ) \hat y=F_T(x)

其他重点

如果XGBoost只是用上述一系列步骤来进行计算的话,在实际应用中可能会比GBDT花费更长的训练时间和空间,因为它需要计算的导数不只是一阶的,还有二阶的。而且只是这样不足以让XGBoost能在大规模数据学习中脱颖而出受到大家青睐,作者在提出这个系统时就不只是单单改进这个算法而已,下面这些部分就是作者为了加速这个算法所做的一些设计。

加权分位数草图(weighted quantile sketch)

在建树过程中一般需要遍历所有的分割点,然后选择最优的分割点来建造这棵树,这样做能精确地构造出一颗较好的树,XGBoost也支持这样的做法,但是这样需要大量的时间和内存,特别是在数据量大的时候算法就显得太慢而且硬件要求也会增加。所以作者提出用只尝试部分候选的分割点,近似地用特征的加权分位点来表示,这也是XGBoost能表现很好的最主要的一个原因。

这里作者给出了两种变体:一种是global的,就是在建造树之前就给出所有候选的分割点,在树后续更深的建树中使用相同的候选点用于分裂,这样一开始就需要选择较多的候选点;另一种是local的,就是在每次分裂之后再重新给出候选分割点,这样每次需要的候选点较少,但是步骤会多一些。当给出足够的候选节点,global可以达到与local一样的准确率。

所以理解这个做法的关键就是怎么构建这样的加权分位点。首先我们重新看下算法的目标函数,并配方 L ~ ( t ) = i = 1 n [ g i f t ( x i ) + 1 2 h i f t 2 ( x i ) ] + Ω ( f t ) = i = 1 n 1 2 h i ( f t ( x i ) g i / h i ) 2 + Ω ( f t ) + c o n s t a n t \begin{aligned} \tilde{\mathcal{L}}^{(t)}&= \sum_{i=1}^n\big[g_if_t(x_i)+{1\over2}h_if_t^2(x_i)\big]+\Omega(f_t)\\ &=\sum_{i=1}^n{1\over2}h_i\big(f_t(x_i)-g_i/h_i\big)^2+\Omega(f_t)+constant \end{aligned}

这说明新的学习器其实在优化关于当前学习器的加权平方损失,因为 g i , h i g_i,h_i 都是和新的学习器无关只是当前学习器的一些导数,而这个权重就是 h i h_i 。从这个角度出发,我们在考虑候选的分割点时就应该按照这个权重来给出一些具有代表性的候选点。也就是可能我们需要这个权重为0的点、权重累积到10%的那个点、权重累积到20%的那个点、30%、40%…直到100%,如果我们需要11个候选点的话。因为这样提供的点更能体现这个特征对于目标函数的意义。

所以对于某个特征 k k l l 个这样的候选点 { s k 1 , s k 2 , , s k l } \{s_{k1},s_{k2},\dots,s_{kl}\} ,原本的样本数据就可以看做是一个带权重的数据集 D k = { ( x 1 k , h 1 ) , ( x 2 k , h 2 ) , , ( x n k , h n ) } \mathcal{D_k}=\{(x_{1k},h_1),(x_{2k},h_2),\dots,(x_{nk},h_n)\} ,然后定义一个排序方程(相当于这个特征的分布函数,不过这个不是概率而是权重) r k : R [ 0 , + ) r_k:\mathbb{R}\to[0,+\infty) r k ( z ) = 1 ( x , h ) D k h ( x , h ) D k , x < z h r_k(z)={1\over \sum_{(x,h)\in \mathcal{D}_k}h} \sum_{(x,h)\in \mathcal{D}_k,x<z}h 则我们只需要让这 l l 个候选点满足 r k ( s k , j ) r k ( s k , j + 1 ) < 1 l ,    s k 1 = min i x i k ,   s k l = max i x i k |r_k(s_{k,j})-r_k(s_{k,j+1})|<{1\over l},\ \ s_{k1}=\min_ix_{ik},\ s_{kl}=\max_ix_{ik} 即相邻两个候选点之间的累积标准化权重差小于 1 / l 1/l ,这样就得到了 l l 个这样的候选点。作者还在附录中提出了具体的支持合并和删除操作的数据结构来得到这样的候选点,并证明了每个操作都可以保持一定准确度。

稀疏感知算法

对于现实世界中经常存在的稀疏数据,或者一些需要将非偏序关系经过独热编码处理之后的数据稀疏数据,XGBoost也提供了内置的稀疏感知算法,给每个树的结点都加了一个默认方向,当一个值是缺失值时,系统就把这个值分到默认方向,这个默认方向是由非缺失的数据来学到的,将缺失的数据枚举向左和向右的情况,哪边分裂后的增益较大,就将哪边选做默认方向。

这样处理之后使得XGBoost可以以一种一致的方式处理数据,不需要其他特定的步骤,而且这个方法的复杂度和输入数据的个数呈线性关系,可以很快的运行。

其他的系统设计

除了上述的设计外,作者还从计算角度为XGBoost设计了一个更好的运行方式,使得它更快更高效的运行:

  • 采用列压缩的block形式:每个特征数据以压缩形式分别存到block里,不同的block可以分布式存储。在选择特征分裂点的时候,可以并行的处理这些列数据,用多线程来实现加速。
  • 设置内部缓存区:当数据排序后,索引是乱的,可能指向了不同的内存地址,找的时候数据是不连续的,这里在每个线程里申请了一个内部缓存区以此来增加缓存感知,让以后找的时候能找到小批量的连续地址,以实现加速,这个优化在小数据下看不出来,数据越多越明显。
  • 充分利用核外资源:将数据分成多个块并将每个块存储在磁盘上。一方面是采用块压缩,并在加载到主存储器时通过独立的线程进行解压,这样降低了磁盘读取成本;另一方面将数据分到多个磁盘上,为每个磁盘分配一个实现预取的线程,并将数据提取到内存缓冲区中,当有多个磁盘可用时,这有助于提高磁盘读取的吞吐量。

参考资料:

Chen, Tianqi , and C. Guestrin . “XGBoost: A Scalable Tree Boosting System.” Proceedings of the 22nd ACM SIGKDD International Conference on Knowledge Discovery and Data Mining ACM, 2016.
XGBoost原论文阅读翻译
XGBoost文档里陈天奇的slide

发布了32 篇原创文章 · 获赞 33 · 访问量 6618

猜你喜欢

转载自blog.csdn.net/weixin_44750583/article/details/103794334