XGBoost 是大规模并行 boosting tree 的工具,它是目前最快最好的开源 boosting tree 工具包,比常见的工具包快 10 倍以上。Xgboost 和 GBDT 两者都是 boosting 方法,除了工程实现、解决问题上的一些差异外,最大的不同就是目标函数的定义。故本文将从数学原理和工程实现上进行介绍,并在最后介绍下 Xgboost 的优点。
XGBoost是由k个基模型组成的加法运算式:
y ^ i = ∑ t = 1 k f t ( x i ) \hat{y}_{i}=\sum_{t=1}^{k} f_{t}\left(x_{i}\right) y^i=t=1∑kft(xi)
其中 f k f_k fk为第 k k k个基模型, y ^ i \hat{y}_{i} y^i为第 i i i个样本的预测值。
损失函数可由预测值 y ^ i \hat{y}_{i} y^i与真实值 y i y_i yi进行表示: L = ∑ i = 1 n l ( y i , y ^ i ) L=\sum_{i=1}^{n} l\left(y_{i}, \hat{y}_{i}\right) L=i=1∑nl(yi,y^i)
其中 n n n为样本数量。
我们知道模型的预测精度由模型的偏差和方差共同决定,损失函数代表了模型的偏差,想要方差小则需要简单的模型,所以目标函数由模型的损失函数 L L L与抑制模型复杂度的正则项 Ω \Omega Ω 组成,所以我们有:
O b j = ∑ i = 1 n l ( y ^ i , y i ) + ∑ t = 1 k Ω ( f t ) O b j=\sum_{i=1}^{n} l\left(\hat{y}_{i}, y_{i}\right)+\sum_{t=1}^{k} \Omega\left(f_{t}\right) Obj=i=1∑nl(y^i,yi)+t=1∑kΩ(ft) Ω \Omega Ω为模型的正则项,XGBoost支持决策树也支持线性模型。
boosting模型是前向加法,以第 t t t步的模型为例,模型对第 i i i个样本 x i x_i xi的预测为: y ^ i t = y ^ i t − 1 + f t ( x i ) \hat{y}_{i}^{t}=\hat{y}_{i}^{t-1}+f_{t}\left(x_{i}\right) y^it=y^it−1+ft(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)是需要加入的新模型的预测值,此时,目标函数可写为: O b j ( t ) = ∑ i = 1 n l ( y i , y ^ i t ) + ∑ i = 1 t Ω ( f i ) = ∑ i = 1 n l ( y i , y ^ i t − 1 + f t ( x i ) ) + ∑ i = 1 t Ω ( f i ) \begin{aligned} O b j^{(t)} &=\sum_{i=1}^{n} l\left(y_{i}, \hat{y}_{i}^{t}\right)+\sum_{i=1}^{t} \Omega\left(f_{i}\right) \\ &=\sum_{i=1}^{n} l\left(y_{i}, \hat{y}_{i}^{t-1}+f_{t}\left(x_{i}\right)\right)+\sum_{i=1}^{t} \Omega\left(f_{i}\right) \end{aligned} Obj(t)=i=1∑nl(yi,y^it)+i=1∑tΩ(fi)=i=1∑nl(yi,y^it−1+ft(xi))+i=1∑tΩ(fi)
求此时最优化目标函数,就相当于求解 f t ( x i ) f_t(x_i) ft(xi)。
根据泰勒公式可以将目标函数改写为: O b j ( t ) = ∑ i = 1 n [ l ( y i , y ^ i t − 1 ) + g i f t ( x i ) + 1 2 h i f t 2 ( x i ) ] + ∑ i = 1 t Ω ( f i ) O b j^{(t)}=\sum_{i=1}^{n}\left[l\left(y_{i}, \hat{y}_{i}^{t-1}\right)+g_{i} f_{t}\left(x_{i}\right)+\frac{1}{2} h_{i} f_{t}^{2}\left(x_{i}\right)\right]+\sum_{i=1}^{t} \Omega\left(f_{i}\right) Obj(t)=i=1∑n[l(yi,y^it−1)+gift(xi)+21hift2(xi)]+i=1∑tΩ(fi)
其中 g i g_i gi为损失函数的一阶导, h i h_i hi为损失函数的二阶导,注意这里的导是对 y ^ i t − 1 \hat{y}_i^{t-1} y^it−1求导。
最终目标函数可以改写为: O b j ( t ) ≈ ∑ i = 1 n [ g i f t ( x i ) + 1 2 h i f t 2 ( x i ) ] + ∑ i = 1 t Ω ( f i ) O b j^{(t)} \approx \sum_{i=1}^{n}\left[g_{i} f_{t}\left(x_{i}\right)+\frac{1}{2} h_{i} f_{t}^{2}\left(x_{i}\right)\right]+\sum_{i=1}^{t} \Omega\left(f_{i}\right) Obj(t)≈i=1∑n[gift(xi)+21hift2(xi)]+i=1∑tΩ(fi)所以我们只需要求出每一步损失函数的一阶导和二阶导的值(由于前一步的 y ^ t − 1 \hat{y}^{t-1} y^t−1是已知的,所以这两个值就是常数),然后最优化目标函数,就可以得到每一步的 f ( x ) f(x) f(x) ,最后根据加法模型得到一个整体模型。
我们知道 Xgboost 的基模型不仅支持决策树,还支持线性模型,这里我们主要介绍基于决策树的目标函数。
我们可以将决策树定义为 f t ( x ) = w q ( x ) f_t(x)=w_{q(x)} ft(x)=wq(x) , x x x 为某一样本,这里的 q ( x ) q(x) q(x)代表了该样本在哪个叶子结点上,而 w q w_q wq则代表了叶子结点取值 w w w ,所以 w q ( x ) w_{q(x)} wq(x) 就代表了每个样本的取值 w w w (即预测值)。
决策树的复杂度可由叶子树 T T T组成,叶子节点越少模型越简单,此外叶子节点也不应该含有过高的权重 w w w(类比LR的每个变量的权重),所以目标函数的正则项可以定义为: Ω ( f t ) = γ T + 1 2 λ ∑ j = 1 T w j 2 \Omega\left(f_{t}\right)=\gamma T+\frac{1}{2} \lambda \sum_{j=1}^{T} w_{j}^{2} Ω(ft)=γT+21λj=1∑Twj2即决策树模型的复杂度由生成的所有决策树的叶子节点数量,和所有节点权重所组成的向量的 L 2 L_2 L2 范式共同决定。
这张图给出了基于决策树的 XGBoost 的正则项的求解方式。
我们设 I j = { i ∣ q ( x i ) = j } I_{j}=\left\{i \mid q\left(x_{i}\right)=j\right\} Ij={i∣q(xi)=j}为第 j j j个叶子节点的样本集合,故我们的目标函数可以写成: O b j ( 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 w q ( x i ) + 1 2 h i w q ( x i ) 2 ] + γ T + 1 2 λ ∑ j = 1 T w j 2 = ∑ j = 1 T [ ( ∑ i ∈ I j g i ) w j + 1 2 ( ∑ i ∈ I j h i + λ ) w j 2 ] + γ T \begin{aligned} O b j^{(t)} & \approx \sum_{i=1}^{n}\left[g_{i} f_{t}\left(x_{i}\right)+\frac{1}{2} h_{i} f_{t}^{2}\left(x_{i}\right)\right]+\Omega\left(f_{t}\right) \\ &=\sum_{i=1}^{n}\left[g_{i} w_{q\left(x_{i}\right)}+\frac{1}{2} h_{i} w_{q\left(x_{i}\right)}^{2}\right]+\gamma T+\frac{1}{2} \lambda \sum_{j=1}^{T} w_{j}^{2} \\ &=\sum_{j=1}^{T}\left[\left(\sum_{i \in I_{j}} g_{i}\right) w_{j}+\frac{1}{2}\left(\sum_{i \in I_{j}} h_{i}+\lambda\right) w_{j}^{2}\right]+\gamma T \end{aligned} Obj(t)≈i=1∑n[gift(xi)+21hift2(xi)]+Ω(ft)=i=1∑n[giwq(xi)+21hiwq(xi)2]+γT+21λj=1∑Twj2=j=1∑T⎣⎡⎝⎛i∈Ij∑gi⎠⎞wj+21⎝⎛i∈Ij∑hi+λ⎠⎞wj2⎦⎤+γT第二步到第三步可能看的不是特别明白,这边做些解释:第二步是遍历所有的样本后求每个样本的损失函数,但样本最终会落在叶子节点上,所以我们也可以遍历叶子节点,然后获取叶子节点上的样本集合,最后在求损失函数。即我们之前样本的集合,现在都改写成叶子结点的集合,由于一个叶子结点有多个样本存在,因此才有了 ∑ 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 j = ∑ i ∈ I i g i , H j = ∑ i ∈ I i h i G_{j}=\sum_{i \in I_{i}} g_{i}, \quad H_{j}=\sum_{i \in I_{i}} h_{i} Gj=∑i∈Iigi,Hj=∑i∈Iihi,目标函数可以为: O b j ( t ) = ∑ j = 1 T [ G j w j + 1 2 ( H j + λ ) w j 2 ] + γ T O b j^{(t)}=\sum_{j=1}^{T}\left[G_{j} w_{j}+\frac{1}{2}\left(H_{j}+\lambda\right) w_{j}^{2}\right]+\gamma T Obj(t)=j=1∑T[Gjwj+21(Hj+λ)wj2]+γT这里我们要注意 G j G_{j} Gj 和 H j H_{j} Hj 是前 t − 1 t-1 t−1 步得到的结果,其值已知可视为常数,只有最后一棵树 的叶子节点 w j w_{j} wj 不确定, 那么将目标函数对 w j w_{j} wj 求一阶导,并令其等于 0 , 0 , 0, 则可以求得叶子结 点 j j j 对应的权值:
w j ∗ = − G j H j + λ w_{j}^{*}=-\frac{G_{j}}{H_{j}+\lambda} wj∗=−Hj+λGj所以目标函数可以化简为:
O b j = − 1 2 ∑ j = 1 T G j 2 H j + λ + γ T O b j=-\frac{1}{2} \sum_{j=1}^{T} \frac{G_{j}^{2}}{H_{j}+\lambda}+\gamma T Obj=−21j=1∑THj+λGj2+γT
上图给出目标函数计算的例子,求每个节点每个样本的一阶导数 g i g_i gi 和二阶导数 h i h_i hi ,然后针对每个节点对所含样本求和得到的 G i G_i Gi 和 H i H_i Hi ,最后遍历决策树的节点即可得到目标函数。
在决策树的生长过程中,一个非常关键的问题是如何找到叶子的节点的最优切分点,Xgboost 支持两种分裂节点的方法——贪心算法和近似算法。
1)贪心算法
那么如何计算每个特征的分裂收益呢?
假设我们在某一节点完成特征分裂,则分列前的目标函数可以写为:
O b j 1 = − 1 2 [ ( G L + G R ) 2 H L + H R + λ ] + γ O b j_{1}=-\frac{1}{2}\left[\frac{\left(G_{L}+G_{R}\right)^{2}}{H_{L}+H_{R}+\lambda}\right]+\gamma Obj1=−21[HL+HR+λ(GL+GR)2]+γ分裂后的目标函数为;
O b j 2 = − 1 2 [ G L 2 H L + λ + G R 2 H R + λ ] + 2 γ O b j_{2}=-\frac{1}{2}\left[\frac{G_{L}^{2}}{H_{L}+\lambda}+\frac{G_{R}^{2}}{H_{R}+\lambda}\right]+2 \gamma Obj2=−21[HL+λGL2+HR+λGR2]+2γ则对于目标函数来说, 分裂后的收益为:
Gain = 1 2 [ G L 2 H L + λ + G R 2 H R + λ − ( G L + G R ) 2 H L + H R + λ ] − γ \text { Gain }=\frac{1}{2}\left[\frac{G_{L}^{2}}{H_{L}+\lambda}+\frac{G_{R}^{2}}{H_{R}+\lambda}-\frac{\left(G_{L}+G_{R}\right)^{2}}{H_{L}+H_{R}+\lambda}\right]-\gamma Gain =21[HL+λGL2+HR+λGR2−HL+HR+λ(GL+GR)2]−γ注意该特征收益也可作为特征重要性输出的重要依据。
对于每次分裂,我们都需要枚举所有特征可能的分割方案,如何高效地枚举所有的分割呢?
我假设我们要枚举所有 x < a xx<a这样的条件,对于某个特定的分割点 a a a 我们要计算 a a a 左边和右边的导数和。
我们可以发现对于所有的分裂点 a a a ,我们只要做一遍从左到右的扫描就可以枚举出所有分割的梯度和 G L G_L GL 和 G R G_R GR 。然后用上面的公式计算每个分割方案的分数就可以了。
观察分裂后的收益,我们会发现节点划分不一定会使得结果变好,因为我们有一个引入新叶子的惩罚项,也就是说引入的分割带来的增益如果小于一个阀值的时候,我们可以剪掉这个分割。
2)近似算法
贪婪算法可以的到最优解,但当数据量太大时则无法读入内存进行计算,近似算法主要针对贪婪算法这一缺点给出了近似最优解。
对于每个特征,只考察分位点可以减少计算复杂度。
该算法会首先根据特征分布的分位数提出候选划分点,然后将连续型特征映射到由这些候选点划分的桶中,然后聚合统计信息找到所有区间的最佳分裂点。
在提出候选切分点时有两种策略:
直观上来看,Local 策略需要更多的计算步骤,而 Global 策略因为节点没有划分所以需要更多的候选点。
下图给出不同种分裂策略的 AUC 变换曲线,横坐标为迭代次数,纵坐标为测试集 AUC,eps 为近似算法的精度,其倒数为桶的数量。
我们可以看到 Global 策略在候选点数多时(eps 小)可以和 Local 策略在候选点少时(eps 大)具有相似的精度。此外我们还发现,在 eps 取值合理的情况下,分位数策略可以获得与贪婪算法相同的精度。
下图给出近似算法的具体例子,以三分位为例:
根据样本特征进行排序,然后基于分位数进行划分,并统计三个桶内的 G , H G,H G,H值,最终求解节点划分的增益。
事实上, XGBoost 不是简单地按照样本个数进行分位,而是以二阶导数值 [公式] 作为样本的权重进行划分,如下:
那么问题来了:为什么要用 h i h_{i} hi 进行样本加权?
我们知道模型的目标函数为:
O b j ( t ) ≈ ∑ i = 1 n [ g i f t ( x i ) + 1 2 h i f t 2 ( x i ) ] + ∑ i = 1 t Ω ( f i ) O b j^{(t)} \approx \sum_{i=1}^{n}\left[g_{i} f_{t}\left(x_{i}\right)+\frac{1}{2} h_{i} f_{t}^{2}\left(x_{i}\right)\right]+\sum_{i=1}^{t} \Omega\left(f_{i}\right) Obj(t)≈i=1∑n[gift(xi)+21hift2(xi)]+i=1∑tΩ(fi)我们稍作整理,便可以看出 h i h_{i} hi 有对 loss 加权的作用。
O b j ( t ) ≈ ∑ i = 1 n [ g i f t ( x i ) + 1 2 h i f t 2 ( x i ) ] + ∑ i = 1 t Ω ( f i ) = ∑ i = 1 n [ g i f t ( x i ) + 1 2 h i f t 2 ( x i ) + 1 2 g i 2 h i ] + Ω ( f t ) + C = ∑ i = 1 n 1 2 h i [ f t ( x i ) − ( − g i h i ) ] 2 + Ω ( f t ) + C \begin{aligned} O b j^{(t)} & \approx \sum_{i=1}^{n}\left[g_{i} f_{t}\left(x_{i}\right)+\frac{1}{2} h_{i} f_{t}^{2}\left(x_{i}\right)\right]+\sum_{i=1}^{t} \Omega\left(f_{i}\right) \\ &=\sum_{i=1}^{n}\left[g_{i} f_{t}\left(x_{i}\right)+\frac{1}{2} h_{i} f_{t}^{2}\left(x_{i}\right)+\frac{1}{2} \frac{g_{i}^{2}}{h_{i}}\right]+\Omega\left(f_{t}\right)+C \\ &=\sum_{i=1}^{n} \frac{1}{2} h_{i}\left[f_{t}\left(x_{i}\right)-\left(-\frac{g_{i}}{h_{i}}\right)\right]^{2}+\Omega\left(f_{t}\right)+C \end{aligned} Obj(t)≈i=1∑n[gift(xi)+21hift2(xi)]+i=1∑tΩ(fi)=i=1∑n[gift(xi)+21hift2(xi)+21higi2]+Ω(ft)+C=i=1∑n21hi[ft(xi)−(−higi)]2+Ω(ft)+C其中 1 2 g i 2 h i \frac{1}{2} \frac{g_{i}^{2}}{h_{i}} 21higi2 与 C C C 皆为常数。我们可以看到 h i h_{i} hi 就是平方损失函数中样本的权重。
对于样本权值相同的数据集来说,找到候选分位点已经有了解决方案(GK 算法),但是当样本权值不一样时,该如何找到候选分位点呢?(作者给出了一个 Weighted Quantile Sketch 算法,这里将不做介绍。)
在决策树的第一篇文章中我们介绍 CART 树在应对数据缺失时的分裂策略,XGBoost 也给出了其解决方案。
XGBoost 在构建树的节点过程中只考虑非缺失值的数据遍历,而为每个节点增加了一个缺省方向,当样本相应的特征值缺失时,可以被归类到缺省方向上,最优的缺省方向可以从数据中学到。至于如何学到缺省值的分支,其实很简单,分别枚举特征缺省的样本归为左右分支后的增益,选择增益最大的枚举项即为最优缺省方向。
在构建树的过程中需要枚举特征缺失的样本,乍一看该算法的计算量增加了一倍,但其实该算法在构建树的过程中只考虑了特征未缺失的样本遍历,而特征值缺失的样本无需遍历只需直接分配到左右节点,故算法所需遍历的样本量减少,下图可以看到稀疏感知算法比 basic 算法速度块了超过 50 倍。
优点:
缺点:
LightGBM 由微软提出,主要用于解决 GDBT 在海量数据中遇到的问题,以便其可以更好更快地用于工业实践中。
从 LightGBM 名字我们可以看出其是轻量级(Light)的梯度提升机(GBM),其相对 XGBoost 具有训练速度快、内存占用低的特点。下图分别显示了 XGBoost、XGBoost_hist(利用梯度直方图的 XGBoost) 和 LightGBM 三者之间针对不同数据集情况下的内存和训练时间的对比:
那么 LightGBM 到底如何做到更快的训练速度和更低的内存使用的呢?
我们刚刚分析了 XGBoost 的缺点,LightGBM 为了解决这些问题提出了以下几点解决方案:
GBDT 算法的梯度大小可以反应样本的权重,梯度越小说明模型拟合的越好,单边梯度抽样算法(Gradient-based One-Side Sampling, GOSS)利用这一信息对样本进行抽样,减少了大量梯度小的样本,在接下来的计算锅中只需关注梯度高的样本,极大的减少了计算量。
GOSS 算法保留了梯度大的样本,并对梯度小的样本进行随机抽样,为了不改变样本的数据分布,在计算增益时为梯度小的样本引入一个常数进行平衡。
我们可以看到 GOSS 事先基于梯度的绝对值对样本进行排序(无需保存排序后结果),然后拿到前 a% 的梯度大的样本,和总体样本的 b%,在计算增益时,通过乘上 1 − a b \frac{1-a}{b} b1−a 来放大梯度小的样本的权重。一方面算法将更多的注意力放在训练不足的样本上,另一方面通过乘上权重来防止采样对原始数据分布造成太大的影响。
1) 直方图算法
直方图算法的基本思想是将连续的特征离散化为 k 个离散特征,同时构造一个宽度为 k 的直方图用于统计信息(含有 k 个 bin)。利用直方图算法我们无需遍历数据,只需要遍历 k 个 bin 即可找到最佳分裂点。
我们知道特征离散化的具有很多优点,如存储方便、运算更快、鲁棒性强、模型更加稳定等等。对于直方图算法来说最直接的有以下两个优点(以 k=256 为例):
2) 直方图加速
在构建叶节点的直方图时,我们还可以通过父节点的直方图与相邻叶节点的直方图相减的方式构建,从而减少了一半的计算量。在实际操作过程中,我们还可以先计算直方图小的叶子节点,然后利用直方图作差来获得直方图大的叶子节点。
3) 稀疏特征优化
XGBoost 在进行预排序时只考虑非零值进行加速,而 LightGBM 也采用类似策略:只用非零特征构建直方图。
高维特征往往是稀疏的,而且特征间可能是相互排斥的(如两个特征不同时取非零值),如果两个特征并不完全互斥(如只有一部分情况下是不同时取非零值),可以用互斥率表示互斥程度。互斥特征捆绑算法(Exclusive Feature Bundling, EFB)指出如果将一些特征进行融合绑定,则可以降低特征数量。
针对这种想法,我们会遇到两个问题:
对于问题一:EFB 算法利用特征和特征间的关系构造一个加权无向图,并将其转换为图着色算法。我们知道图着色是个 NP-Hard 问题,故采用贪婪算法得到近似解,具体步骤如下:
算法允许两两特征并不完全互斥来增加特征捆绑的数量,通过设置最大互斥率 γ \gamma γ来平衡算法的精度和效率。
对于问题二:论文给出特征合并算法,其关键在于原始特征能从合并的特征中分离出来。假设 Bundle 中有两个特征值,A 取值为 [0, 10]、B 取值为 [0, 20],为了保证特征 A、B 的互斥性,我们可以给特征 B 添加一个偏移量转换为 [10, 30],Bundle 后的特征其取值为 [0, 30],这样便实现了特征合并
在建树的过程中有两种策略:
XGBoost 采用 Level-wise 的增长策略,方便并行计算每一层的分裂节点,提高了训练速度,但同时也因为节点增益过小增加了很多不必要的分裂,降低了计算量;LightGBM 采用 Leaf-wise 的增长策略减少了计算量,配合最大深度的限制防止过拟合,由于每次都需要计算增益最大的节点,所以无法并行分裂。
大部分的机器学司算法都不能直接支持类别特征,一般都会对类别特征进行编码,然后再输入到模 型中。常见的处理类别特征的方法为 one-hot 编码, 但我们知道对于决策树来说并不推荐使用 one-hot 编码:
LightGBM 原生支持类别特征,采用 many-vs-many 的切分方式将类别特征分为两个子集, 实现 类别特征的最优切分。假设有某维特征有 k k k 个类别,则有 2 ( k − 1 ) − 1 2^{(k-1)}-1 2(k−1)−1 中可能, 时间复杂度为 O ( 2 k ) O\left(2^{k}\right) O(2k), LightGBM 基于 Fisher 大佬的《On Grouping For Maximum Homogeneity》实现 了 O ( k log k ) O(k \log k) O(klogk) 的时间复杂度。
上图为左边为基于 one-hot 编码进行分裂,后图为 LightGBM 基于 many-vs-many 进行分裂,在给定深度情况下,后者能学出更好的模型。
其基本思想在于每次分组时都会根据训练目标对类别特征进行分类,根据其累积值 ∑ gradient ∑ hessian \frac{\sum \text { gradient }}{\sum \text { hessian }} ∑ hessian ∑ gradient 对直方图进行排序,然后在排序的直方图上找到最佳分割。此外,LightGBM 还加了约束条件正则化,防止过拟合。
我们可以看到这种处理类别特征的方式使得 AUC 提高了 1.5 个点,且时间仅仅多了 20%。
本节主要总结下 LightGBM 相对于 XGBoost 的优点,从内存和速度两方面进行介绍。
更多请见:https://zhuanlan.zhihu.com/p/87885678