Introduction
XGBoost对大部分人而言应该是不陌生的,在kaggle等数据科学竞赛的平台上凭借其准确快速的特性吸引了一大批拥趸。这次花了几天时间自己读了一下该论文,作者的基于GBDT的算法改进以及计算机系统设计层面的优化非常有创新性。不仅如此,作者在paper中将复杂度分析以及伪代码的部分也写的非常的详细。总的来说这是一篇很有价值的paper,适合精读一次。
Goal of the paper
Describe a scalable end-to-end tree boosting system called XGBoost, which is used widely by data scientist to achieve SOTA results.
这是paper中的原话,而关键也在于这个system。相较于别的提升树模型,XGboost的一个主要优势也是对不同问题的高度兼容性以及稳定性。稳定主要体现在算法层面及系统层面。算法层面优化针对稀疏数据的处理方法以及对连续特征的分组方法。而系统层面提出了一种缓存管理的方法用来降低资源损耗。二者结合后使得XGBoost用相对当时现有的系统更少的计算资源在亿级数据层面获得不错效果。
Major contribution from this paper
- 提出一个highly scalable的端对端提升树系统。
- 提出加权版的quantile sketch用来处理连续数据分段。
- 提出适合稀疏数据的并行学习算法。
- 提出一种高效的缓存结构用来辅助out-of-core learning.
Pre-defined Notations
在正文之前,先来统一一下数学符号以及一些关键性的概念。
-
D = { ( x i , y i ) } ( ∣ D ∣ = n , x i ∈ R m , y i ∈ R ) \mathcal{D} = \{(x_i, y_i)\} (|\mathcal{D}| = n,x_i \in\mathbb{R}^m, y_i\in \mathbb{R}) D={(xi,yi)}(∣D∣=n,xi∈Rm,yi∈R) : 样本数为 n n n的数据集,其中feature的个数为 m m m。
-
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}),...,(x_{nk}, h_{n})\} Dk={(x1k,h1),(x2k,h2),...,(xnk,hn)} : 第k个feature里的值及对应的二阶导数。
-
K K K : boosting过程中子模型个数。
-
F = { f ( x ) = w q ( x ) } ( q : R m → T , w ∈ R T ) \mathcal{F} = \{f(\mathbf{x}) = w_{q(\mathbf{x})}\} (q:\mathbb{R}^m \rightarrow T,w\in \mathbb{R}^T) F={f(x)=wq(x)}(q:Rm→T,w∈RT) : F \mathcal{F} F为所有回归树的空间, q q q表示树的一个结构。
-
∣ ∣ x ∣ ∣ 0 ||\mathbf{x}||_0 ∣∣x∣∣0 : x \mathbf{x} x中有效元素的个数。
-
l l l : 二阶可导的单样本损失函数。
-
L \mathcal{L} L : 整个模型的损失函数。
-
L ( t ) \mathcal{L}^{(t)} L(t) : 在第 t t t步模型损失函数。
-
ϕ ( x i ) = ∑ k = 1 K f k ( x i ) , f k ∈ F \phi (\mathbf{x}_i) = \sum_{k=1}^K f_k(\mathbf{x}_i), f_k \in \mathcal{F} ϕ(xi)=∑k=1Kfk(xi),fk∈F : ϕ \phi ϕ代表最终模型的输出, f k f_k fk指构成模型的一系列子模型。
-
g i g_i gi : loss function 对 y i ( t − 1 ) y_i^{(t-1)} yi(t−1)的一阶导数。
-
h i h_i hi : loss function 对 y i ( t − 1 ) y_i^{(t-1)} yi(t−1)的二阶导数。
-
r k ( z ) = 1 ∑ ( x , h ) ∈ D k h ∑ ( x , h ) ∈ D k h I ( x < z ) r_k(z) = \frac{1}{\sum_{(x,h)\in \mathcal{D}_k}h}\sum_{(x,h)\in \mathcal{D}_k}h\mathbb{I}(x<z) rk(z)=∑(x,h)∈Dkh1∑(x,h)∈DkhI(x<z) : rank function,feature k中小于z的样本点占的比重。
-
s k j s_{kj} skj : (quantile)分割点,其中 s k 1 = m i n i x i k s_{k1}=min_i \mathbf{x}_{ik} sk1=minixik, s k l = m a x i x i k s_{kl}=max_i\mathbf{x}_{ik} skl=maxixik。
Methodology Part-1: Gradient Boosting
(a) Regularized Learning objective
模型的损失函数为所有单个样本的损失和。相对与GBDT,XGBoost加入了一个正则项,用来控制树的深度和参数的数值来做到防过拟合。当正则项系数设为0的时候,XGBoost和GBDT在优化方向方面是一致的。加入正则化后,模型的损失函数变成了
L
(
ϕ
)
=
∑
i
l
(
y
i
^
,
y
i
)
+
∑
k
Ω
(
f
k
)
\mathcal{L}(\phi) = \sum_i l(\hat{y_i}, y_i) + \sum_k \Omega(f_k)
L(ϕ)=i∑l(yi^,yi)+k∑Ω(fk)
Ω
(
f
)
=
γ
T
+
1
2
λ
∣
∣
w
∣
∣
2
\Omega(f) = \gamma T + \frac{1}{2}\lambda ||w||^2
Ω(f)=γT+21λ∣∣w∣∣2
(b) Gradient Tree Boosting
XGBoost本质还是一个梯度提升树,所以训练loss的时候跟GBDT有很大一部分是相似的。例如同为加法模型,在每一步迭代(假设当前为
t
t
t步),我们需要找到一个
f
t
f_t
ft,来使得当前loss最小
L
(
t
)
=
∑
i
l
(
y
i
^
,
y
i
(
t
−
1
)
+
f
t
(
x
i
)
)
+
Ω
(
f
t
)
\mathcal{L}^{(t)} = \sum_i l(\hat{y_i}, y_i^{(t-1)} + f_t(\mathbf{x}_i)) + \Omega(f_t)
L(t)=i∑l(yi^,yi(t−1)+ft(xi))+Ω(ft)
但是在加速收敛方面,GBDT采用的是first-order approximation,对loss做了关于
y
i
(
t
−
1
)
y_i^{(t-1)}
yi(t−1)的一阶泰勒展开,而XGBoost采用的是second-order approximation,对loss做了二阶泰勒展开。列在一起对比如下(当然GBDT是没有regularized部分的)
L
(
t
)
≊
∑
i
=
1
n
l
(
y
i
,
y
i
^
(
t
−
1
)
)
+
g
i
f
t
(
x
i
)
+
Ω
(
f
t
)
\mathcal{L}^{(t)} \approxeq \sum_{i=1}^nl(y_i, \hat{y_i}^{(t-1)}) + g_if_t(\mathbf{x}_i) + \Omega(f_t)
L(t)≊i=1∑nl(yi,yi^(t−1))+gift(xi)+Ω(ft)
L
(
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
)
+
Ω
(
f
t
)
\mathcal{L}^{(t)} \approxeq \sum_{i=1}^nl(y_i, \hat{y_i}^{(t-1)}) + g_if_t(\mathbf{x}_i) + \frac{1}{2}h_if_t^2(\mathbf{x}_i) + \Omega(f_t)
L(t)≊i=1∑nl(yi,yi^(t−1))+gift(xi)+21hift2(xi)+Ω(ft)
在针对
f
t
(
x
)
f_t(x)
ft(x) 进行优化的时候,两个式子的首项都与
f
t
f_t
ft无关,所以我们都可以提前将他们删掉。因此GBDT最后的优化目标即为每一步残差的一阶导数,而XGBoost则同时拥有一阶及二阶导数。两者在最终结果上应该是一致的,但是不同approximation方式使得XGBoost的收敛速度变得快了几分。两者的算法对比可以参考梯度下降法和牛顿法的对比。
最后将
Ω
(
f
t
)
\Omega(f_t)
Ω(ft)代入式子,并去掉首项的常数项,我们最后获得一个关于
f
t
f_t
ft的函数,并把
f
t
(
x
i
)
=
w
q
(
x
)
f_t(\mathbf{x}_i) = w_{q(\mathbf{x})}
ft(xi)=wq(x)也代入,依照树模型的原理,每一个
x
i
\mathbf{x}_i
xi会被分到一个区间内,对应的模型输出为
w
j
w_j
wj。描述的比较抽象但是写出来还是蛮清晰的。
L
ˉ
(
t
)
=
∑
i
=
1
n
[
g
i
f
t
(
x
i
)
+
1
2
h
i
f
t
2
(
x
i
)
]
+
γ
T
+
1
2
λ
∑
j
=
1
T
w
j
2
L
ˉ
(
t
)
=
∑
j
=
1
T
[
(
∑
i
∈
I
j
g
i
)
w
j
+
1
2
(
∑
i
∈
I
j
h
i
+
λ
)
w
j
2
]
+
γ
T
\begin{array}{ll} \bar{\mathcal{L}}^{(t)}=&\sum_{i=1}^n[g_if_t(\mathbf{x}_i) + \frac{1}{2}h_if_t^2(\mathbf{x}_i)] + \gamma T + \frac{1}{2}\lambda \sum_{j=1}^T w_j^2\\ \bar{\mathcal{L}}^{(t)}=&\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 \end{array}
Lˉ(t)=Lˉ(t)=∑i=1n[gift(xi)+21hift2(xi)]+γT+21λ∑j=1Twj2∑j=1T[(∑i∈Ijgi)wj+21(∑i∈Ijhi+λ)wj2]+γT
w
j
w_j
wj就是我们要获得的用来定义
f
t
f_t
ft的模型参数,所以现在既然已有了函数,我们可以直接对
w
j
w_j
wj求导来得到关于
w
j
w_j
wj的极值点。
w
j
∗
=
−
∑
i
∈
I
j
g
i
∑
i
∈
I
j
h
i
+
λ
w_j^* = -\frac{\sum_{i \in I_j}g_i}{\sum_{i\in I_j}h_i + \lambda}
wj∗=−∑i∈Ijhi+λ∑i∈Ijgi
代入上式我们即获得
L
ˉ
(
t
)
=
−
1
2
∑
j
=
1
T
(
∑
i
∈
I
j
g
i
)
2
∑
i
∈
I
j
h
i
+
λ
+
γ
T
\bar{\mathcal{L}}^{(t)}=-\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
Lˉ(t)=−21j=1∑T∑i∈Ijhi+λ(∑i∈Ijgi)2+γT
以上,我们获得了一个新的loss function,而这个function除了
T
T
T以外,只与一阶导数和二阶导数有关。而作为树模型,同样也有一个类似于信息增益那样基于loss reduction的分割子数的评价标准。假设当前feature被分为
I
L
I_L
IL和
I
R
I_R
IR而且有
I
j
=
I
L
∪
I
R
I_j = I_L \cup I_R
Ij=IL∪IR,那么对应的loss reduction即为
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
j
g
i
)
2
∑
i
∈
I
j
h
i
+
λ
]
\mathcal{L}_{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_j}g_i)^2}{\sum_{i\in I_j}h_i+\lambda}]
Lsplit=21[∑i∈ILhi+λ(∑i∈ILgi)2+∑i∈IRhi+λ(∑i∈IRgi)2−∑i∈Ijhi+λ(∑i∈Ijgi)2]
该式子在形式还是挺直观,在本质上与信息增益等传统方法工作原理是一样的。分割用的特征以及分割点需要使loss reduction最大。
(cc) Techniques to Avoid over-fitting
为了解决过拟合,除了正则化项,XGBoost同时还加入了两种“传统”办法,所谓传统就是之前模型中出现的方法。
例如:
- Shringkage: 出自GBDT,简单原理就是在boosting过程中,当准备加入一棵新树的来更新模型残差的时候,工作原理类似梯度下降法中的step size。
- Column(feature) Subsampling: 出自random forest,指的是在基于某个feature进行树分割的时候,只对一部分feature值进行遍历。因为遍历的点少了,所以这个方法同时还能加速计算。
Methodology Part-2: Split Finding Algorithm
(a.) Exact Greedy Algorithm
构建树模型的时候最重要的仍旧是分割点的搜寻问题。最直接的办法还是Exact Greedy Search,也就是在要分割的时候测试每一个feature的每一个样本点,选择loss reduction最大的那个feature及相应的样本点作为分割点。paper中提供了一个XGBoost版的:
这个算法是要求我们算出每个feature的在每个点分割后,产生的loss reduction,在这里预先对feature里的值进行排序使这个搜索过程变成y一个动态规划的过程。降低了求loss reduction的成本。虽然排序的过程很耗时间,但XGBoost的一个系统层面上的优化点,就是预先对所有的feature进行了排序并存储。
(b.) Approximate Algorithm
与上面提到的Exact greedy算法不同,为了高效处理数据我们很多时候不太可能对所有feature所有点进行遍历。Approximate Algorithm就是为了在不影响整体结果的情况下,适当地对feature进行重组,分成好多个bucket,每个bucket里输出一个可以代表整个bucket的整合后的值。这样我们在进行搜索的时候,只需要测试这几个bucket的值就可以了。对应的XGBoost版本为:
请注意算法中提到的了global和local,这里指的是分桶的时候的两种变体。
- Global: 每个feature分好bucket后,在接下来每次split finding的时候,都用这次分好的bucket。
- Local: 每次split finding的时候,额外分一次bucket。
(c.) Weighted Quantile Sketch
分桶的过程还是挺讲究的。分桶时候还是要保证不会对模型的质量产生影响的,所以要确保各个桶里的样本点产生的loss是接近的,这样在分割后不会出现左子树和右子树loss不平衡。当各个样本点产生的loss的权重相等时,可以用一个叫quantile sketch的方法来分桶。但是在paper中,作者提出了一个改进方法,首先将XGBoost的loss function重写成了weighted loss的形式
∑
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
\sum_{i=1}^n\frac{1}{2} h_i(f_t(\textbf{x}_i)-\frac{g_i}{h_i})^2+\Omega(f_t) + constant
i=1∑n21hi(ft(xi)−higi)2+Ω(ft)+constant
可以看出
h
i
h_i
hi在这里扮演了个类似权重的作用。可以理解为每个样本点产生的对bucket产生的loss不是等价的。为了配合这种表达,作者顺便提出了一个weighted quantile sketch算法来解决这个问题。由于这部分内容并没有在paper中说明,有需要可以参考作者批注的supplementary material。
另外,作者还是提供了明确的数学定义来描述算法的目的。假设手上有
D
k
\mathcal{D}_k
Dk,以及阈值
ϵ
\epsilon
ϵ,算法的目的为找到一组quantile的分割点,
{
s
k
1
,
s
k
2
,
s
k
3
,
.
.
.
s
k
l
}
\{s_{k1},s_{k2},s_{k3},...s_{kl}\}
{sk1,sk2,sk3,...skl}使
∣
r
k
(
s
k
,
j
)
−
r
k
(
s
k
,
j
+
1
)
∣
<
ϵ
|r_k(s_{k,j})-r_k(s_{k,j+1})| < \epsilon
∣rk(sk,j)−rk(sk,j+1)∣<ϵ
可以估计,大约有
1
/
ϵ
1/\epsilon
1/ϵ个分割点。参照前面列出来的
r
k
r_k
rk的定义,权重是由
h
i
h_i
hi来构定义的,分母是一个normalization项从而使
r
k
r_k
rk的输出为一个分数。
(d.) Sparsity-aware split finding.
这部分主要是讲解如何处理缺失值的,我认为非常有借鉴意义。虽然名字里带着sparsity(稀疏),但是产生的原因主要也是因为存在缺失点。作者在这里的处理方式很巧妙,大致可以归纳为:
- 只处理非缺失的部分。
- 假设missing data会被分到右边,并得出最优的模型以及score。
- 假设missing data会被分到左边,并得出最优的模型以及score。
- 比较两个score,得z最优模型及最佳missing value的默认方向。
算法截图为
System Design
这部分讲的全是系统层面上的优化,主要包括XGBoost在分布式下的并行实现。同时也是我读的最吃力的地方。由于非计算机科班我就不对这些描述太多了。主要贡献包括
- block structure: 预存各个特征的排序结j及相应index来优化split finding
- cache-aware access: 来达到高速访问缓存,来实现依照index快速调取gradient的目的。
Time complexity analysis
如题:
假设一共需要K个子模型,每个模型由一个深度为d的树构成。那么paper中列举的算法复杂度为.
- Exact Greedy Algorithm,
O
(
K
d
∣
∣
x
∣
∣
0
l
o
g
n
)
O(Kd||\mathbf{x}||_0logn)
O(Kd∣∣x∣∣0logn)
- K d Kd Kd次split finding。
- ∣ ∣ x ∣ ∣ 0 ||x||_0 ∣∣x∣∣0个有效元素需要被排序。
- 被排序元素大概需要遍历 l o g n logn logn步
- Tree boosting + block structure,
O
(
K
d
∣
∣
x
∣
∣
0
+
∣
∣
x
∣
∣
0
l
o
g
n
)
O(Kd||\mathbf{x}||_0 + ||\mathbf{x}||_0logn)
O(Kd∣∣x∣∣0+∣∣x∣∣0logn)
- K d Kd Kd次split finding,以及每次 ∣ ∣ x ∣ ∣ 0 ||\mathbf{x}||_0 ∣∣x∣∣0次遍历。
- 对 x \mathbf{x} x进行一次排序, ∣ ∣ x ∣ ∣ 0 l o g n ||\mathbf{x}||_0logn ∣∣x∣∣0logn。
- Approximate Algorithm, 假设q个quantile,
O
(
K
d
∣
∣
x
∣
∣
0
l
o
g
q
)
O(Kd||\mathbf{x}||_0logq)
O(Kd∣∣x∣∣0logq)
- 由于这次其实只有 q q q值元素在被排序,所以替换为 l o g q logq logq,其余与exact greedy 保持一致。
- Approximate Algorithm using block structure,假设每个Block中有
B
B
B行,
O
(
K
d
∣
∣
x
∣
∣
0
+
∣
∣
x
∣
∣
0
l
o
g
B
)
O(Kd||\mathbf{x}||_0 + ||\mathbf{x}||_0logB)
O(Kd∣∣x∣∣0+∣∣x∣∣0logB)
- 每个block中只有 B B B个元素被排序,所以为 l o g B logB logB,其余与2保持一致。
Conclusion.
作者同时从数学及计算机的角度对Boosting tree的方法进行了优化,整个paper的信息量还是挺大的,尤其是system design的部分对操作系统方面也是有要求的。本质上XGBoost还是一个gradient boosting tree,在后面的算法对比中XGBoost也在不同任务里,与GBDT达到了同等高度的模型准确度,但是在这么多优化方法下,时间效率上以及资源利用率上提升了非常多。