Xgboost原理-XGBoost论文精读与总结-A Scalable Tree Boosting System

1. xgboost介绍

xgboost特点:

  1. 提出了高效的、可扩展的、端到端的树提升系统;
  2. 针对系数数据和加权分位数架构提出了一种创新性的稀疏感知算法,用于近似树学习;
  3. 针对缓存访问模式、数据压缩和分片,提出相应的解决方案来构建可扩展的树提升系统。

结果:通过整合上面几个优势,xgboost可以做到以更少的资源,扩展到数十亿的训练样本。

paper的唯一的keywords-Large-scale Machine Learning 也正是反应了这一点,大规模机器学习。

在introduction部分,paper着重提到了xgboost成功秘籍:
xgboost成功背后最大的因素是其在所有场景下的可扩展性

The most important factor behind the success of XGBoost is its scalability in all scenarios

而可扩展性主要来源于两个方面:

  1. 针对稀疏数据提出的一个创新性的树学习算法;
  2. 具有理论支撑的加权分位数结构化的步骤,可以帮助在近似树学习中处理样本权重问题。

More importantly, XGBoost exploits out-of-core computation and enables data scientists to process hundred millions of examples on a desktop. Finally, it is even more exciting to combine these techniques to make an end-to-end system that scales to even larger data with the least amount of cluster resources.

哈哈,paper又是一顿推广xgboost的优势,不过那是真的呆佩服,能够把这么多的改进同时结合起来,造就了xgboost这一神作。

作者再正式介绍正文之前总结了下本paper的重要贡献:

  1. 建立了一个具有高扩展性能的端到端树提升系统;
  2. 以进行有效的计算,提出了一个有理论依据的加权分位草图;
  3. 为并行化的树学习,提出了一个创新性的稀疏感知算法;
  4. 为核外树学习,提出了一个有效的缓存感知块结构。

在上述主要贡献之外,还有其他能够带来提升的贡献,包括不限于正则化的学习方法等。

2. 树提升模型

2.1 正则化的学习目标

在这里插入图片描述

再来回顾下集成模型中加性树算法函数:
y^i=ϕ(xi)=k=1Kfk(xi),fkϝ(1) \hat y_i = \phi(x_i) = \sum_{k=1}^{K}f_k(x_i), \qquad f_k\in \digamma \qquad (1)
其中,F表示回归树的空间,F中每一个组成部分fk可以用来刻画第k棵树,fk是指每棵树的结构(用q表示),以及树上每个叶子结点代表的分数(用w表示),T表示树中叶子节点的总个数,因此可以对公式(1)进行下面条件的限制:
ϝ={f(x)=wq(x)}(q:RmT,wRT) \digamma = \{f(x) = w_{q(x)}\}(q: \mathbb{R}^m\rightarrow T, w\in \mathbb{R}^T)
与普通决策树使用的是分类树不同的是,这里使用的是回归树,每个回归树的叶子结点是一个连续的分数值。对于每一个样本,在每棵树上都会依据决策规则(也就是树的结构q)将其分类到相应的叶子结点上,而在计算该样本的预测结果时,是将所有树的叶子结点分数(w)做加和即可。

模型即公式(1)中的函数需要学习出来,因此需要通过loss函数进行优化,xgboost需要最优化的是下面的带有正则化的loss函数:
ζ(ϕ)=il(y^i,yi)+kΩ(fk)(2) \zeta(\phi) = \sum_i l(\hat y_i, y_i) + \sum_k \Omega(f_k) \qquad (2)
whereΩ(f)=γT+12λw2 where \quad \Omega(f) = \gamma T + \frac{1}{2}\lambda ||w||^2
其中fk表示第k棵树的正则化项,公式(2)中的l作为损失函数,是一个可微的凸函数,公式(2)中的第二项是用来惩罚模型的复杂度的,这个正则项可以用来平滑最终学习到的权重参数以防止过拟合,因此模型更倾向于学习到更加简单和泛化性能更好的参数结果。当正则项参数置为0时,模型的目标函数就等同于传统的梯度树提升模型了。

2.2 梯度树提升算法

由于公式(2)中包含函数式参数(fk),因此无法使用传统的优化算法在欧氏空间中对其进行优化,而作为替代方案的是,模型需要通过加性的方式进行训练,典型的情况是,第 i 个样本在 t 次迭代后的预测结果为
y^i(t) \hat y_i^{(t)}
,我们需要在上次预测结果的基础上,加上ft后来最小化损失函数,如下所示:
ζ(t)=il(yi,y^i(t1)+ft(xi))+Ω(ft)(3) \zeta(t) = \sum_i l(y_i, \hat y_i^{(t-1)} + f_t(x_i)) + \Omega(f_t) \qquad (3)
公式(3)表示第t次迭代后的损失函数,这是一种贪心的方法,即保证加上ft后当前优化效果最好即可。下面对公式(3)进行二阶泰勒展开为:
ζ(t)i=1n[l(yi,y^i(t1))+gift(xi)+12hift2(xi)]+Ω(ft)(4) \zeta(t) \simeq \sum_{i=1}^{n} [l(y_i, \hat y_i^{(t-1)}) + g_i f_t(x_i) + \frac{1}{2}h_i f_t^2(x_i)] + \Omega(f_t) \qquad (4)
公式(4)中的n表示每一次优化时一个batch的数据量大小,另外:
gi=y^(t1)l(yi,y^(t1))(5) g_i = \partial_{\hat y_{(t-1)}}l(y_i, \hat y_{(t-1)}) \qquad (5)
hi=y^(t1)2l(yi,y^(t1))(6) h_i = \partial_{\hat y_{(t-1)}}^2l(y_i, \hat y_{(t-1)}) \qquad (6)
上面公式(5)和公式(6)分别是损失函数的一阶导数和二阶导数,由于公式(4)中的第一项是常数项(因为yi和上一迭代的预测结果这两项都是常数值),所以可以将公式(4)中的常数项去除(这不影响后面的一系列求导等过程),得到下面在第t次迭代时简化版的损失函数为:
ζ(t)=i=1n[gift(xi)+12hift2(xi)]+Ω(ft)(7) \zeta(t) = \sum_{i=1}^{n} [g_i f_t(x_i) + \frac{1}{2}h_i f_t^2(x_i)] + \Omega(f_t) \qquad (7)
下面我们对公式(7)做两个改变:一个是将常数项展开,另一个是引入下面的公式(8):
Ii={iq(xi)=j}(8) I_i = {\{i|q(x_i) = j \}} \qquad (8)
上面公式(8)表示叶子结点j上的所有样本集,q表示树的结构,因此我们可以将公式(7)进行如下转换:
ζ~(t)=i=1n[gift(xi)+12hift2(xi)]+γT+12λw2 \widetilde\zeta^{(t)} = \sum_{i=1}^{n} [g_i f_t(x_i) + \frac{1}{2}h_i f_t^2(x_i)] + \gamma T + \frac{1}{2}\lambda ||w||^2
=j=1T[(iIjgi)wj+12(iIjhi+λ)wj2]+γT(9) =\sum_{j=1}^{T}[(\sum_{i\in I_j}g_i)w_j + \frac{1}{2}(\sum_{i\in I_j}h_i + \lambda)w_j^2] + \gamma T \qquad (9)
上述公式(9)是一个二阶多项式,对于一个结构固定的树来说(必须要强调树的结构固定,这样T才是固定的,不然T的大小也是变量),可以通过对其计算一阶倒数的方式并令其一阶导数等于0得到极值点为:
wj=iIjgiiIjhi+λ(10) w_j^* = -\frac{\sum_{i\in I_j}g_i}{\sum_{i\in I_j}h_i + \lambda} \qquad (10)
经公式(10)中的极值点代入公式(9)中得:
ζ~(t)(q)=12j=1T(iIjgi)2iIjhi+λ+γT(11) \widetilde\zeta^{(t)}(q) = -\frac{1}{2}\sum_{j=1}^{T} \frac{(\sum_{i\in I_j} g_i)^2}{\sum_{i\in I_j} h_i + \lambda} + \gamma T \qquad (11)
上述公式(11)就是用来评估树结构q的质量的打分函数,它相当于在评估决策树时的杂质函数(例如熵函数),但是有一点不同的是,这里的公式(11)是针对更加广泛的目标函数形式而言(只要可以计算一阶导数和二阶导数值),下面图2中也给出了如何计算损失函数分数的方式。
在这里插入图片描述

通常如果枚举出来所有的可能的树结构q是不现实的,因此采取了一个贪心算法是:从单个叶子结点开始,不断迭代地往树中加入树枝。假设树分裂之后的左节点和右结点包含的样本集分别为ILIR,树分裂前节点所包含的样本集为I,即I=IL+IR,那么在树节点分裂成左右节点之后,损失函数降低了:
ζsplit=12[(iILgi)2iILhi+λ+(iIRgi)2iIRhi+λ(iIgi)2iIhi+λ]γ(12) \zeta_{split} = -\frac{1}{2} [\frac{(\sum_{i\in I_L} g_i)^2}{\sum_{i\in I_L} h_i + \lambda} + \frac{(\sum_{i\in I_R} g_i)^2}{\sum_{i\in I_R} h_i + \lambda} - \frac{(\sum_{i\in I} g_i)^2}{\sum_{i\in I} h_i + \lambda}] -\gamma \qquad (12)
此时T=1,因此公式(12)中就省略了,公式(12)也是常用来评估分裂的的效果。

2.3 学习率和列采样

除了前面提到的对叶子结点数量、叶子结点分数值进行正则化,引入了更多的防止过拟合方法:

  1. 学习率的加入(来源于Friedman):减少了单棵树的影响,给后面树优化模型留更多的空间
  2. 列采样即特征采样(来源于随机森林):在某些情况下,使用列采样甚至能够获得比行采样更好的效果;列采样还可以加速并行化算法的计算速度
  3. 行采样

3. 分裂发现算法

3.1 精确贪心算法

树学习模型的一个关键点就是找到最优的分裂特征和分裂值,而为了实现最优,精确的贪心算法会枚举所有可能的分裂点,而当前一些单机树提升算法都实现了精确贪心算法,例如scikit-learn、gbm、以及paper的xgboost。精确贪心算法的实现如Alg.1所示,它在计算上要求枚举连续特征的所有可能分裂值,因此必须首先对特征值进行排序,然后按照排序顺序访问数据并计算公式(12)中的梯度统计结果。

在这里插入图片描述

3.2 近似贪心算法

将缺贪心算法尽管十分强大,但是一个明显的缺点就是需要耗费太大计算量以及内存,特别是当内存不够或者需要处理分布式任务配置时,精确贪心算法将无法work,因此需要一个近似的算法。

在这里插入图片描述

近似算法为Alog.2,步骤如下:

  1. 根据特征分布的百分位数提出候选分割点;
  2. 基于候选分割点,将连续特征映射到各个桶里;
  3. 根据聚合后的分割点效果(可基于公式(12)计算)找出最优的分割点。

近似算法还有两个变体,全局变体和局部变体:

  1. 全局变体建议在算法初始阶段使用所有候选分割,并在之后的所有层级使用相同的候选分割;局部变体在每个分割之后会重新生成候选建议分割;
  2. 全局变体需要更少的建议步骤,而局部变体需要更多,因为每次分割后,局部变体都要重新建议;
  3. 全局变体需要更多的候选分割点,因此每次分裂之后没有纪念性重新建议,而局部变体则使用更少分割点;
  4. 局部变体每次分割完都重新建议候选分割点的特性使得其更加适合较深层的树学习模型。

不同的算法在数据集上的效果对比如图3所示,实际发现:

  1. 局部变体确实需要更少的候选分割点;
  2. 当有足够多的候选分割点时,全局变体可以达到可以局部变体一样好的效果(具体体现在图中,全局变体eps=0.5时小狗较差,但是当eps=0.03也就是分割点更多点,效果达到了和局部变体一样的效果)

在这里插入图片描述

对于近似算法,还可以进行其他的尝试,例如:

  1. 直接构建梯度统计的近似直方图
  2. 使用分箱策略的其他变体代替分位数策略;

同时paper总结了之所以使用分位数策略,是因为其受益于可分布以及可计算。并且而且分箱策略可以达到和精确贪心算法一样的效果。

xgboost不仅实现了单机上的精确贪心算法,也实现了所有配置下的包含全局变体及局部变体的近似算法。

3.3 加权分位数骨架

通常情况下,数据的百分位点是用于使候选点在数据分布上更加均匀,并且在下面paper给出了常见的数据的百分位点计算的方法,并指出了当前方法的两个缺点:

  1. 对于大型数据集合而言,计算候选分割点比较困难;
  2. 当数据具有不同的权重时,目前没有稳妥的方法来计算候选分割点。
    为了解决这个问题,paper提出了分布式的加权分位数架构的算法

3.4 稀疏感知分裂发现算法

在真实任务中,稀疏的x形式的输入是非常常见的。通常有三种情况产生稀疏的输入:

  1. 数据中出现了确实值;
  2. 统计数据中频繁出现了0值特征;
  3. 人工特征工程产生了的one-hot的编码

我们需要让算法感知到稀疏模式数据的存在,解决方式是:
在树节点中提出一个默认的方向,可以参考图4中:在稀疏矩阵中当一个值是确实值时,那么这个样本将被划分到默认的方向上去,这每个分支上会有2个默认方向的选择,而默认的方向时通过算法从数据中学习出来的,相应的学习算法见Alg.3,这里的一个关键的提升在于只访问非缺失值的样本,算法会将不存在视为缺失值,并将其最好的分裂方向学习出来以处理缺失值。而且当一个值与指定的特征值对应不上时,xgboost会将其限制到一个统一的值上(预测时候使用?)。

在这里插入图片描述

据我们所知,目前已有的树学习算法要么是只对连续特征进行优化,要么是需要特定的程度来处理有限的情况例如分类编码类型的特征。而xgboost中提出了一个解决稀疏模式数据的统一方法,而且其实现的复杂度与非稀疏数据的规模大小成线性关系,paper在数据集上实验的结果显示稀疏感知算法的计算速度比原始版本在速度上提升了50倍。
在这里插入图片描述

4. 系统设计

4.1 用于并行学习的列block

树学习过程最耗时的部分是在将数据排成有序,为了降低排序的成本,我们将数据存储在内存单元中,并称之为block,每个block中的数据是以压缩列(CSC)格式存储的,并且对列是按照其对应的特征值来排序的。这样的话,输入数据的布局仅仅需要在训练前计算一次,之后可以复用。

在精确贪心算法中,我们将整个数据集在一个block中存储,并且通过线性扫描预排序好的数据来执行分裂节点查找算法,我们共同进行所有叶子结点的分裂点查找,因此对block的一次扫描将搜集到所有叶子分支中分裂候选节点的统计信息。Fig 6中显示如何将数据转换成block以及使用block寻找最优分裂节点的过程。

在这里插入图片描述

当使用近似算法时,block结构仍然能发挥作用,由于是近似算法使用的是全量数据集的子数据集,这个时候使用多个blocks,因此每个block中对应的是全量数据集的行采样的子数据集,不同的blocks可以进行跨机器做分布式,或者存储在磁盘的核外配置中,block的存储结构对于尤其对于局部近似算法变体有很大价值,因为其需要进行频繁的提出候选分割点。

4.1.1 时间复杂度分析

参数定义:

  1. d:树的最大深度
  2. K:树的总棵树
  3. n:训练样本的数量
  4. ||x||0:训练样本中非缺失值样本的数量
  5. q:数据集中需要的候选分割点的数量,通常在32-100之间
  6. B:每个block中的行的最大数量
  1. 对于精确贪心算法,原始算法的时间复杂度为:
    O(Kdx0logn) O(Kd||x||_0logn)

  2. 对于精确贪心算法,使用block结构的树提升算法中,时间复杂度为:
    O(Kdx0+x0logn) O(Kd||x||_0 + ||x||_0logn)
    上面加号的后面部分表示一次性预排序的时间开销,这是可以分摊的。上面2和1的对比,block结构节省了一个logn因子的时间,这在n较大是就显得非常重要了。

  3. 对于近似算法,原始算法的时间复杂度为:
    O(Kdx0logq) O(Kd||x||_0logq)

  4. 对于近似算法,使用block结构的树提升算法的时间复杂度为:
    O(Kdx0+x0logB) O(Kd||x||_0 + ||x||_0logB)
    上面4和3的对比,block结构节省了一个logq因子的时间,而q通常在32-100之间,这也是一笔不小的开销。

4.2 缓存感知访问

之后更新

4.3 用于核外计算的block

之后更新

5. xgboost各种tricks总结:

一、xgboost防止过拟合方法:

  1. 对叶子结点数量、叶子结点分数(权重)值进行正则化;
  2. 学习率的加入(来源于Friedman):减少了单棵树的影响,给后面树的优化上留有更多的空间;
  3. 列采样即特征采样(来源于随机森林):在某些情况下,使用列采样甚至能够获得比行采样更好的效果,列采样还可以加速并行化算法的计算速度;
  4. 行采样

二、树的分裂点发现算法:

  1. 精确贪心算法:在其他树学习模型中也都有相同的实现;
  2. 近似贪心算法:连续特征按照分位点进行特征分桶;
    a. 全局算法
    b. 局部算法
  3. 加权分位点骨架算法
  4. 稀疏特征:
    a. 不存在即缺失值
    b. 统计中出现的0值特征
    c. 人工特征工程中的编码特征如ont-hotd等
    xgboost对于稀疏特征的处理方式统一为通过训练数据学习出来特征稀疏值的默认划分方向。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章