简介
XGBoost是陈天奇等人开发的一个开源机器学习项目,高效地实现了GBDT算法并进行了算法和工程上的许多改进,被广泛应用在许多机器学习竞赛中取得不错的成绩。
基本思想
XGboost模型的损失函数包含两部分,模型的经验损失以及正则化项:
L
(
ϕ
)
=
∑
i
l
(
y
^
i
,
y
i
)
+
∑
k
Ω
(
f
k
)
where
Ω
(
f
)
=
γ
T
+
1
2
λ
∥
w
∥
2
\begin{array}{l}{\mathcal{L}(\phi)=\sum_{i} l\left(\hat{y}_{i}, y_{i}\right)+\sum_{k} \Omega\left(f_{k}\right)} \\ {\text { where } \Omega(f)=\gamma T+\frac{1}{2} \lambda\|w\|^{2}}\end{array}
L ( ϕ ) = ∑ i l ( y ^ i , y i ) + ∑ k Ω ( f k ) where Ω ( f ) = γ T + 2 1 λ ∥ w ∥ 2 其中
T
T
T 为基模型CART树的叶子数。如何优化上面的目标函数?我们不能用诸如梯度下降的方法,因为
f
f
f 是树,而非数值型的向量。我们采用前向分步算法 ,即贪心法找局部最优解模型,每一步找一个使得我们的损失函数降低最大的f(贪心法体现在这)
y
^
i
(
t
)
=
∑
j
=
1
t
f
j
(
x
i
)
=
y
^
i
(
t
−
1
)
+
f
t
(
x
i
)
\hat{y}_{i}^{(t)}=\sum_{j=1}^{t} f_{j}\left(x_{i}\right)=\hat{y}_{i}^{(t-1)}+f_{t}\left(x_{i}\right)
y ^ i ( t ) = j = 1 ∑ t f j ( x i ) = y ^ i ( t − 1 ) + f t ( x i ) 对目标函数进行改写,得到第t次迭代的损失函数:
L
(
t
)
=
∑
i
=
1
n
l
(
y
i
,
y
^
i
(
t
−
1
)
+
f
t
(
x
i
)
)
+
Ω
(
f
t
)
\mathcal{L}^{(t)}=\sum_{i=1}^{n} l\left(y_{i}, \hat{y}_{i}^{(t-1)}+f_{t}\left(\mathbf{x}_{i}\right)\right)+\Omega\left(f_{t}\right)
L ( t ) = i = 1 ∑ n l ( y i , y ^ i ( t − 1 ) + f t ( x i ) ) + Ω ( f t )
二阶泰勒展开
假设损失函数使用的是平方损失,则上式写为:
L
(
t
)
=
∑
i
=
1
N
(
y
i
–
(
y
^
i
(
t
−
1
)
+
f
t
(
x
i
)
)
)
2
+
Ω
(
f
t
)
=
∑
i
=
1
N
(
y
i
–
y
^
i
(
t
−
1
)
⎵
残
差
–
f
t
(
x
i
)
)
2
+
Ω
(
f
t
)
\begin{aligned} \mathcal{L}^{(t)} &=\sum_{i=1}^N \left(y_i – \left(\hat y_i^{(t-1)} + f_t({\bf x_i})\right)\right)^2 + \Omega(f_t) \\ &= \sum_{i=1}^N (\underbrace {y_i – \hat y_i^{(t-1)}}_{残差} – f_t({\bf x_i}))^2 + \Omega(f_t) \end{aligned}
L ( t ) = i = 1 ∑ N ( y i – ( y ^ i ( t − 1 ) + f t ( x i ) ) ) 2 + Ω ( f t ) = i = 1 ∑ N ( 残 差
y i – y ^ i ( t − 1 ) – f t ( x i ) ) 2 + Ω ( f t ) 这就是之前我们GBDT中使用平方损失,然后每一轮拟合的残差。
更一般的,我们之前使用“负梯度”。这里利用二阶泰勒展开 :
f
(
x
+
Δ
x
)
≃
f
(
x
)
+
f
′
(
x
)
Δ
x
+
1
2
f
′
′
(
x
)
Δ
x
2
f(x+\Delta x) \simeq f(x)+f^{\prime}(x) \Delta x+\frac{1}{2} f^{\prime \prime}(x) \Delta x^{2}
f ( x + Δ x ) ≃ f ( x ) + f ′ ( x ) Δ x + 2 1 f ′ ′ ( x ) Δ x 2 我们得到近似的目标函数:
L
(
t
)
≃
∑
i
=
1
n
[
l
(
y
i
,
y
^
(
t
−
1
)
)
+
g
i
f
t
(
x
i
)
+
1
2
h
i
f
t
2
(
x
i
)
]
+
Ω
(
f
t
)
\mathcal{L}^{(t)} \simeq \sum_{i=1}^{n}\left[l\left(y_{i}, \hat{y}^{(t-1)}\right)+g_{i} f_{t}\left(\mathbf{x}_{i}\right)+\frac{1}{2} h_{i} f_{t}^{2}\left(\mathbf{x}_{i}\right)\right]+\Omega\left(f_{t}\right)
L ( t ) ≃ i = 1 ∑ n [ l ( y i , y ^ ( t − 1 ) ) + g i f t ( x i ) + 2 1 h i f t 2 ( x i ) ] + Ω ( f t ) 其中如果我们定义损失函数为平方损失,
g
i
g_i
g i 和
h
i
h_i
h i 分别为每个数据点在损失函数上的一阶导数和二阶导数,则有:
g
i
=
∂
y
^
(
t
−
1
)
(
y
^
(
t
−
1
)
−
y
i
)
2
=
2
(
y
^
(
t
−
1
)
−
y
i
)
h
i
=
∂
y
^
(
t
−
1
)
2
(
y
i
−
y
^
(
t
−
1
)
)
2
=
2
g_{i}=\partial_{\hat{y}^{(t-1)}}\left(\hat{y}^{(t-1)}-y_{i}\right)^{2}=2\left(\hat{y}^{(t-1)}-y_{i}\right) \quad h_{i}=\partial_{\hat{y}^{(t-1)}}^{2}\left(y_{i}-\hat{y}^{(t-1)}\right)^{2}=2
g i = ∂ y ^ ( t − 1 ) ( y ^ ( t − 1 ) − y i ) 2 = 2 ( y ^ ( t − 1 ) − y i ) h i = ∂ y ^ ( t − 1 ) 2 ( y i − y ^ ( t − 1 ) ) 2 = 2 为什么只用到二阶泰勒展开呢?因为在平方损失时,三阶展开已经为0。
此时移除对当前t轮来说是常数项的
l
(
y
i
,
y
^
i
(
t
−
1
)
)
l\left(y_{i}, \hat{y}_{i}^{(t-1)}\right)
l ( y i , y ^ i ( t − 1 ) ) 得到:
L
(
t
)
=
∑
i
=
1
N
(
g
i
f
t
(
x
i
)
+
1
2
h
i
f
t
2
(
x
i
)
)
+
Ω
(
f
t
)
\mathcal{L}^{(t)}=\sum_{i=1}^{N}\left(g_{i} f_{t}\left(\mathbf{x}_{\mathbf{i}}\right)+\frac{1}{2} h_{i} f_{t}^{2}\left(\mathbf{x}_{i}\right)\right)+\Omega\left(f_{t}\right)
L ( t ) = i = 1 ∑ N ( g i f t ( x i ) + 2 1 h i f t 2 ( x i ) ) + Ω ( f t ) 目标函数只依赖每个数据点在误差函数上的一阶导数和二阶导数 。
正则项
XGBoost采用衡量树复杂度的方式为:一棵树里面叶子节点的个数T ,以及每棵树叶子节点上面输出分数w 的平方和(相当于L2正则):
Ω
(
f
t
)
=
γ
T
+
1
2
λ
∑
j
=
1
T
w
j
2
\Omega(f_t) =\gamma T +\frac{1}{2}\lambda\sum_{j=1}^{T} w_j^2
Ω ( f t ) = γ T + 2 1 λ j = 1 ∑ T w j 2 举例如下:
完整目标函数
进一步,对XGBoost来说,每一个数据点
x
i
x_i
x i 最终都会落到一个叶子结点上。对于落在同一个叶子节点上的数据点来说,其输出都是一样的。假设我们共有
J
J
J 个叶子结点,每个叶子结点对应的输出为
w
j
w_j
w j (
w
j
w_j
w j 也是我们要求解的最优权重),
I
j
=
{
i
∣
q
(
x
i
)
=
j
}
I_j = \{ i|q(x_i) = j\}
I j = { i ∣ q ( x i ) = j } 为落在叶子结点
j
j
j 的实例集合。则目标函数可以进一步改写(已知
y
i
y_i
y i 和
y
^
i
\hat{y}_i
y ^ i 所以第一项为常数项可忽略):
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
=
∑
j
=
1
T
[
(
∑
i
∈
I
j
g
i
)
w
j
+
1
2
(
∑
i
∈
I
j
h
i
+
λ
)
w
j
2
]
+
γ
T
=
∑
j
=
1
T
[
G
j
w
j
+
1
2
(
H
j
+
λ
)
w
j
2
]
+
γ
T
\begin{aligned} \tilde{\mathcal{L}}^{(t)} &=\sum_{i=1}^{n}\left[g_{i} f_{t}\left(\mathbf{x}_{i}\right)+\frac{1}{2} h_{i} f_{t}^{2}\left(\mathbf{x}_{i}\right)\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 \\&= \sum_{j=1}^{T}\left[G_{j} w_{j}+\frac{1}{2}\left(H_{j}+\lambda\right) w_{j}^{2}\right]+\gamma T \end{aligned}
L ~ ( t ) = i = 1 ∑ n [ g i f t ( x i ) + 2 1 h i f t 2 ( x i ) ] + γ T + 2 1 λ j = 1 ∑ T w j 2 = j = 1 ∑ T ⎣ ⎡ ⎝ ⎛ i ∈ I j ∑ g i ⎠ ⎞ w j + 2 1 ⎝ ⎛ i ∈ I j ∑ h i + λ ⎠ ⎞ w j 2 ⎦ ⎤ + γ T = j = 1 ∑ T [ G j w j + 2 1 ( H j + λ ) w j 2 ] + γ T 其中
G
j
=
∑
i
∈
I
j
g
i
H
j
=
∑
i
∈
I
j
h
i
G_j = \sum_{i \in I_j} g_i \quad H_j = \sum_{i \in I_j} h_i
G j = ∑ i ∈ I j g i H j = ∑ i ∈ I j h i
因此,现在要做的是两件事:
确定树的结构, 这样,这一轮的目标函数就确定了下来
求使得当前这一轮(第t轮)的目标函数最小的叶结点分数w。(Obj代表了当我们指定一个树的结构的时候,我们在目标上面最多减少多少,也称为结构分数 ,structure score)
对于给定的树结构
q
(
x
)
q(\bf x)
q ( x ) 可以计算得到
w
j
w_j
w j 的最优权重为:
w
j
∗
=
–
G
j
H
j
+
λ
w_{j}^{*}=– \frac{G_j}{H_j+\lambda}
w j ∗ = – H j + λ G j 将最优权重带回目标函数,目标函数变为:
(2-8)
L
~
(
t
)
(
q
)
=
∑
j
=
1
T
(
G
j
w
j
+
1
2
(
H
j
+
λ
)
w
j
2
)
+
γ
T
=
∑
j
=
1
T
(
−
G
j
2
H
j
+
λ
+
1
2
G
j
2
H
j
+
λ
)
+
γ
T
=
−
1
2
∑
j
=
1
T
(
G
j
2
H
j
+
λ
)
+
γ
T
\begin{aligned} \tilde{\mathcal{L}}^{(t)}(q) &=\sum_{j=1}^T \left(G_jw_j + \frac{1}{2} (H_j + \lambda) w_j^2\right) +\gamma T\\ &=\sum_{j=1}^T \left(- \frac{G_j^2}{H_j+\lambda} + \frac{1}{2} \frac{G_j^2}{H_j+\lambda} \right) +\gamma T\\ &=- \frac{1}{2}\sum_{j=1}^T \left({\color{red}{\frac{G_j^2}{H_j+\lambda}} } \right) +\gamma T \tag{2-8} \end{aligned}
L ~ ( t ) ( q ) = j = 1 ∑ T ( G j w j + 2 1 ( H j + λ ) w j 2 ) + γ T = j = 1 ∑ T ( − H j + λ G j 2 + 2 1 H j + λ G j 2 ) + γ T = − 2 1 j = 1 ∑ T ( H j + λ G j 2 ) + γ T ( 2 - 8 ) 所表示的目标函数越小越好。
树的结构确定
接下来要解决的就是上面提到的问题,即如何确定树的结构。
暴力枚举所有的树结构,然后选择结构分数最小的。 树的结构太多了,这样枚举一般不可行。
通常采用贪心法,每次尝试分裂一个叶节点,计算分裂后的增益 ,选增益最大的。这个方法在之前的决策树算法中大量被使用。而增益的计算方式比如ID3的信息增益,C4.5的信息增益率,CART的Gini系数等。那XGBoost呢?
回想式子2-8标红色的部分,衡量了每个叶子节点对总体损失的贡献,我们希望目标函数越小越好,因此红色的部分越大越好。
XGBoost使用下面的公式计算增益:
(2-9)
G
a
i
n
=
1
2
[
G
L
2
H
L
+
λ
⎵
左
子
树
分
数
+
G
R
2
H
R
+
λ
⎵
右
子
树
分
数
–
(
G
L
+
G
R
)
2
H
L
+
H
R
+
λ
⎵
分
裂
前
分
数
]
–
γ
⎵
新
叶
节
点
复
杂
度
Gain = \frac{1}{2}[\underbrace{\frac{G_L^2}{H_L+\lambda}}_{左子树分数} + \underbrace{\frac{G_R^2}{H_R+\lambda}}_{右子树分数} – \underbrace{\frac{(G_L+G_R)^2}{H_L+H_R+\lambda}}_{分裂前分数}] – \underbrace{\gamma}_{新叶节点复杂度}\tag{2-9}
G a i n = 2 1 [ 左 子 树 分 数
H L + λ G L 2 + 右 子 树 分 数
H R + λ G R 2 – 分 裂 前 分 数
H L + H R + λ ( G L + G R ) 2 ] – 新 叶 节 点 复 杂 度
γ ( 2 - 9 ) 式2-9即2-8红色部分的分裂后 – 分裂前 的分数。Gain值越大,说明分裂后能使目标函数减少越多,就越好 。
该式子可以作为一个打分函数 衡量树结构
q
q
q 的质量,也就是说可以作为是否可以进行分裂的判断标准 ,即分裂之后两边的目标函数之和是否能够小于不分裂的目标函数值 :
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}_{s p l i t}=\frac{1}{2}\left[\frac{\left(\sum_{i \in I_{L}} g_{i}\right)^{2}}{\sum_{i \in I_{L}} h_{i}+\lambda}+\frac{\left(\sum_{i \in I_{R}} g_{i}\right)^{2}}{\sum_{i \in I_{R}} h_{i}+\lambda}-\frac{\left(\sum_{i \in I} g_{i}\right)^{2}}{\sum_{i \in I} h_{i}+\lambda}\right]-\gamma
L s p l i t = 2 1 [ ∑ i ∈ I L h i + λ ( ∑ i ∈ I L g i ) 2 + ∑ i ∈ I R h i + λ ( ∑ i ∈ I R g i ) 2 − ∑ i ∈ I h i + λ ( ∑ i ∈ I g i ) 2 ] − γ 其中
I
L
I_L
I L 和
I
R
I_R
I R 是分裂后属于左右节点的实例集合,
I
=
I
L
∪
I
R
I=I_{L} \cup I_{R}
I = I L ∪ I R ;上式大于0选择分裂。注意分裂不一定会使情况变好,因为有一个引入新叶子的惩罚项
γ
\gamma
γ ,优化这个目标相当于进行树的剪枝。当引入的分裂带来的增益小于一个阀值的时候,不进行分裂操作。
一个树结构分数计算例子如下图:
分裂算法
Basic Exact Greedy Algorithm
因此,每次分裂,枚举所有可能的分裂方案,就和CART中回归树进行划分一样,要枚举所有特征和特征的取值。该算法称为Exact Greedy Algorithm ,如下图所示:遍历所有特征及分割点,选择增益最多的特征和分割点的组合
假设现在枚举的是年龄特征
x
j
x_j
x j 。现在要考虑划分点a,因此要计算枚举
x
j
<
a
x_j < a
x j < a 和
a
≤
x
j
a\leq x_j
a ≤ x j 的导数和:
可以看出,对于一个特征,对特征取值排完序后,枚举所有的分裂点a,只要从左到右扫描一遍就可以枚举出所有分割的梯度
G
L
G_L
G L 和
G
R
G_R
G R ,然后用式2-9计算即可。这样假设树的高度为
H
H
H ,特征数
d
d
d ,则复杂度为
O
(
H
d
n
l
o
g
n
)
O(Hdnlogn)
O ( H d n l o g n ) 。 其中,排序为
O
(
n
l
o
g
n
)
O(nlogn)
O ( n l o g n ) ,每个特征都要排序所以乘以
d
d
d ,每一层都要这样一遍,所以乘以高度H。这个仍可以继续优化(之后再讲)。
树节点划分算法-Approximate Algorithm
精确地枚举所有可能的划分,十分耗时;当数据量大的时候,几乎不可能将数据全部加载进内存;精确划分在分布式中也会有问题。因此文章提出了近似的策略。
简单的说,就是根据特征
k
k
k 的分布来确定
l
l
l 个候选切分点
S
k
=
{
s
k
1
,
s
k
2
,
⋯
s
k
l
}
S_{k}=\left\{s_{k 1}, s_{k 2}, \cdots s_{k l}\right\}
S k = { s k 1 , s k 2 , ⋯ s k l } ,然后根据这些候选切分点把相应的样本放入对应的桶 中,对每个桶的
G
,
H
G,H
G , H 进行累加,最后通过遍历所有的候选分裂点来找到最佳分裂点。方法过程如下图:
给定了候选切分点后,一个例子为:
那么,现在有两个问题:
如何选取候选切分点
S
k
=
{
s
k
1
,
s
k
2
,
⋯
s
k
l
}
S_k = \{s_{k1},s_{k2},\cdots s_{kl}\}
S k = { s k 1 , s k 2 , ⋯ s k l } 呢?
什么时候进行候选切分点的选取?
分界点选择时机
近似方法分为全局和局部两种。**全局策略(Global)**是在建立第
k
k
k 棵树时(初始化阶段)利用样本的梯度对样本进行离散化,每一维的特征都建立buckets。在建树过程中,重复利用这些buckets进行分裂判断。**局部策略(Local)**是在每次进行分裂时,都重新计算每个样本的梯度并重新构建buckets,再进行分裂判断。局部选择的复杂度更高,实验中效果更好。
桶的个数等于 1 / eps, 可以看出:
全局切分点的个数够多的时候,和Exact greedy算法性能相当。
局部切分点个数不需要那么多,因为每一次分裂都重新进行了选择。
切分点的选取-Weighted Quantile Sketch
quantile是分位数,先回答一下什么是分位数,WIKI百科上是这么说的
quantiles are cut points dividing the range of a probability distribution into contiguous intervals with equal probabilities, or dividing the observations in a sample in the same way.
即把概率分布划分为连续的区间,每个区间的概率相同。
以统计学常见的四分位数为例,就是:
四分位数(Quartile)把所有数值由小到大排列并分成四等份,处于三个分割点位置的数值就是四分位数。
1)第一四分位数 (Q1),又称“较小四分位数”,等于该样本中所有数值由小到大排列后第25%的数字;
2)第二四分位数 (Q2),又称“中位数”,等于该样本中所有数值由小到大排列后第50%的数字;
3)第三四分位数 (Q3),又称“较大四分位数”,等于该样本中所有数值由小到大排列后第75%的数字。
可以看出,简单的分位数就是先把数值进行排序,然后根据你采用的几分位数把数据分为几份即可。
上文近似算法的关键在于怎么选择
S
k
=
{
s
k
1
,
s
k
2
…
s
k
l
}
S_k = \left\{s_{k 1}, s_{k 2} \dots s_{k l}\right\}
S k = { s k 1 , s k 2 … s k l } ,即怎么限定近似算法buckets的边界。Quantile Sketch的思想是用k分位点来选取。但是实际中,我们要均分的是loss,而不是样本的数量 ,而每个样本对loss的贡献可能是不一样的,按样本均分会导致loss分布不均匀,取到的分位点会有偏差。怎么衡量每个样本都loss的贡献呢?
我们定义集合
D
k
=
{
(
x
1
k
,
h
1
)
,
(
x
2
k
,
h
2
)
⋯
(
x
n
k
,
h
n
)
\mathcal{D}_{k}=\left\{\left(x_{1 k}, h_{1}\right),\left(x_{2 k}, h_{2}\right) \cdots\left(x_{n k}, h_{n}\right)\right.
D k = { ( x 1 k , h 1 ) , ( x 2 k , h 2 ) ⋯ ( x n k , h n ) 代表每个训练实例的k-th特征值以及二阶梯度。定义rank函数
r
k
:
R
→
[
0
,
+
∞
)
r_{k} : \mathbb{R} \rightarrow[0,+\infty)
r k : R → [ 0 , + ∞ ) 如下:
r
k
(
z
)
=
1
∑
(
x
,
h
)
∈
D
k
h
∑
(
x
,
h
)
∈
D
k
,
,
x
<
z
h
r_{k}(z)=\frac{1}{\sum_{(x, h) \in \mathcal{D}_{k}} h} \sum_{(x, h) \in \mathcal{D}_{k,}, x<z} h
r k ( z ) = ∑ ( x , h ) ∈ D k h 1 ( x , h ) ∈ D k , , x < z ∑ h rank函数计算的是对某一个特征上,样本特征值小于
z
z
z 的二阶梯度除以所有二阶梯度的总和。式3-2表达了第k个特征小于z的样本比例,和之前的分位数挺相似。
而候选切分点
S
k
=
{
s
k
1
,
s
k
2
…
s
k
l
}
S_k = \left\{s_{k 1}, s_{k 2} \dots s_{k l}\right\}
S k = { s k 1 , s k 2 … s k l } 要求:
∣
r
k
(
s
k
,
j
)
−
r
k
(
s
k
,
j
+
1
)
∣
<
ϵ
,
s
k
1
=
min
i
x
i
k
,
s
k
l
=
max
i
x
i
k
\left|r_{k}\left(s_{k, j}\right)-r_{k}\left(s_{k, j+1}\right)\right|<\epsilon, \quad s_{k 1}=\min _{i} \mathbf{x}_{i k}, s_{k l}=\max _{i} \mathbf{x}_{i k}
∣ r k ( s k , j ) − r k ( s k , j + 1 ) ∣ < ϵ , s k 1 = i min x i k , s k l = i max x i k 直观来看就是让相邻两个候选分裂点相差不超过某个值
ϵ
\epsilon
ϵ 。因此总共会得到 1
/
ϵ
/ \epsilon
/ ϵ 个切分点(桶)。我们对这么多个桶进行分支判断,显然比起对n个样本找分裂节点更快捷。
ϵ
\epsilon
ϵ 越大桶数量越少,粒度越粗。
一个例子如下:
要切分为3个,总和为1.8,因此第1个在0.6处,第2个在1.2处。
那么,为什么要用二阶梯度加权 ?将前面我们泰勒二阶展开后的目标函数2-4进行配方:
(3-3)
∑
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
(
2
g
i
h
i
f
t
(
x
i
)
+
f
t
2
(
x
i
)
)
+
Ω
(
f
t
)
=
∑
i
=
1
N
1
2
h
i
(
g
i
2
h
i
2
+
2
g
i
h
i
f
t
(
x
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
)
\begin{aligned} &\sum_{i=1}^N\left(g_if_t({\bf x_i}) + \frac{1}{2}h_if_t^2({\bf x_i})\right) + \Omega(f_t)\\ = &\sum_{i=1}^N\frac{1}{2}h_i\left(2\frac{g_i}{h_i}f_t({\bf x_i}) + f_t^2({\bf x_i})\right) + \Omega(f_t) \\ =&\sum_{i=1}^N \frac{1}{2}h_i\left(\frac{g_i^2}{h_i^2} +2\frac{g_i}{h_i}f_t({\bf x_i}) + f_t^2({\bf x_i})\right) + \Omega(f_t) \\ =&\sum_{i=1}^N \frac{1}{2}{\color{red}h_i}\left( f_t({\bf x_i}) – ({\color{red}- \frac{g_i}{h_i}})\right)^2 + \Omega(f_t) \tag{3-3} \end{aligned}
= = = i = 1 ∑ N ( g i f t ( x i ) + 2 1 h i f t 2 ( x i ) ) + Ω ( f t ) i = 1 ∑ N 2 1 h i ( 2 h i g i f t ( x i ) + f t 2 ( x i ) ) + Ω ( f t ) i = 1 ∑ N 2 1 h i ( h i 2 g i 2 + 2 h i g i f t ( x i ) + f t 2 ( x i ) ) + Ω ( f t ) i = 1 ∑ N 2 1 h i ( f t ( x i ) – ( − h i g i ) ) 2 + Ω ( f t ) ( 3 - 3 ) 推导第三行可以加入
g
i
2
h
i
2
\frac{g_{i}^{2}}{h_{i}^{2}}
h i 2 g i 2 是因为
g
i
g_i
g i 和
h
i
h_i
h i 是上一轮的损失函数求导,是常量。
从式3-3可以看出,目标函数就像是标签为
−
g
i
/
h
i
−g_i/h_i
− g i / h i ,权重为
h
i
h_i
h i 的平方损失,因此用
h
i
h_i
h i 加权。
一些Trick
Shrinkage和采样
除了正则项以外,还有shrinkage与采样技术来避免过拟合 。
Shrinkage
shrinkage (收缩率)就是在每步迭代添加树的过程中,对叶子节点乘以一个缩减权重
η
\eta
η ,类似于随机梯度下降中的学习率。该操作的作用是减少每棵树的影响力,留更多的空间给后来的树提升。
(3-1)
y
^
i
t
=
y
^
i
(
t
−
1
)
+
η
f
t
(
x
i
)
\hat y_i^t = \hat y_i^{(t-1)} + {\color{red} \eta} f_t(x_i)\tag{3-1}
y ^ i t = y ^ i ( t − 1 ) + η f t ( x i ) ( 3 - 1 ) 通常步长
η
\eta
η 取值为0.1。
采样
采样的技术有两种,一种是行采样 (样本采样),是bagging的思想,每次只抽取部分样本进行训练,不使用全部样本。行采样增加了不同样本集合的差异性,从而不同基学习器之间的差异性也增大,避免过拟合。另一种是列采样 (特征采样),相当于做随机特征筛选,进入模型的特征个数越少(即模型变量越少),模型越简单,根据机器学习理论(方差偏差理论),模型越简单,模型泛化性越好。
列采样的实现方式有两种,一种是按层随机(一般效果好一点),另一种是建树前就随机选择特征。按层随机 :上文提到每次分裂一个节点时,我们都要遍历所有的特征和分割点,从而确定最优分割点。如果加入列采样,在对同一层的每个节点分裂之前,先随机选择一部分特征,于是只需要遍历这部分特征,来确定最优的分割点。建树前随机选择特征 :在建树前就随机选择一部分特征,之后所有叶子结点的分裂都使用这部分特征。
稀疏值处理 – Sparsity-aware Split Finding
在真实世界中,我们的特征往往是稀疏的,可能的原因有:
数据缺失值
大量的0值(比如统计出现的)
进行了One-hot 编码
XGBoost能对缺失值自动进行处理,其思想是对于缺失值自动学习出它该被划分的方向 (左子树or右子树):
注意,上述的算法只遍历非缺失值 。划分的方向怎么学呢?很naive但是很有效的方法:
让特征k的所有缺失值的都到右子树,然后和之前的一样,枚举划分点,计算最大的gain
让特征k的所有缺失值的都到左子树,然后和之前的一样,枚举划分点,计算最大的gain
这样最后求出最大增益的同时,也知道了缺失值的样本应该往左边还是往右边。使用了该方法,相当于比传统方法多遍历了一次,但是它只在非缺失值的样本上进行迭代,因此其复杂度与非缺失值的样本成线性关系。在Allstate-10k数据集上,比传统方法快了50倍:
分块并行 – Column Block for Parallel Learning
在建树的过程中,最耗时是找最优的切分点,而这个过程中,最耗时的部分是将数据排序。为了减少排序的时间,提出Block结构存储数据。
可以看出,只需在建树前排序一次 ,后面节点分裂时可以直接根据索引得到梯度信息。
在Exact greedy算法中,将整个数据集存放在一个Block中。这样,复杂度从原来的
O
(
H
d
∣
∣
x
∣
∣
0
l
o
g
n
)
O(Hd||x||_0logn)
O ( H d ∣ ∣ x ∣ ∣ 0 l o g n ) 降为
O
(
H
d
∣
∣
x
∣
∣
0
+
∣
∣
x
∣
∣
0
l
o
g
n
)
O(Hd||x||_0+||x||_0logn)
O ( H d ∣ ∣ x ∣ ∣ 0 + ∣ ∣ x ∣ ∣ 0 l o g n ) ,其中
∣
∣
x
∣
∣
0
||x||_0
∣ ∣ x ∣ ∣ 0 为训练集中非缺失值的个数。这样,Exact greedy算法就省去了每一步中的排序开销。
在近似算法中,使用多个Block,每个Block对应原来数据的子集。不同的Block可以在不同的机器上计算。该方法对Local策略尤其有效,因为Local策略每次分支都重新生成候选切分点。
Block结构还有其它好处,数据按列存储,可以同时访问所有的列,很容易实现并行的寻找分裂点算法。此外也可以方便实现之后要讲的out-of score计算。
缺点是空间消耗大了一倍。
总结
XGBoost为什么快
当数据集大的时候使用近似算法
在特征分裂时,根据特征k的分布确定
l
l
l 个候选切分点。根据这些切分点把相应的样本放入对应的桶中,对每个桶的
G
,
H
G,H
G , H 进行累加,最后通过遍历所有的候选分裂点来找到最佳分裂点。我们对这么多个桶进行分支判断,显然比起对n个样本找分裂节点更快捷。
Block与并行
XGBoost工具支持并行
当然这个并行是在特征的粒度 上,而非tree粒度,因为本质还是boosting算法。我们知道,决策树的学习最耗时的一个步骤是对特征的值进行排序(因为要确定最佳分割点)。xgboost在训练之前,预先对数据进行了排序,然后保存为block结构,后面的迭代中重复地使用这个结构,大大减小计算量。这个block结构也使得并行成为可能。在进行节点分裂时,需要计算每个特征的增益,最终选增益最大的那个特征去做分裂,那么各个特征的增益计算就可以开多线程进行。
CPU cache 命中优化
Block预取、Block压缩、Block Sharding等
XGBoost与GBDT的异同
GBDT是机器学习算法,XGBoost是该算法的工程实现
传统GBDT以CART作为基分类器,XGBoost还支持线性分类器 ,这个时候XGBoost相当于带L1和L2正则化项的Logistic回归(分类问题)或者线性回归(回归问题)。
传统的GBDT只用了一阶导数信息(使用牛顿法的除外),而XGBoost对损失函数做了二阶泰勒展开 。并且XGBoost支持自定义损失函数,只要损失函数一阶、二阶可导。
在使用CART作为基分类器时,XGBoost的目标函数多了正则项 控制模型复杂度, 相当于预剪枝 ,使得学习出来的模型更加不容易过拟合。
传统的GBDT在每轮迭代时使用全部数据,XGBoost则采用了与随机森林相似的策略,支持对数据进行采样(行采样和列采样 )。
对缺失值的处理 。传统的GBDT没有涉及对缺失值进行处理,XGBoost能够自动学习出缺失值的处理策略。
XGBoost工具支持并行 。当然这个并行是在特征的粒度 上,而非tree粒度,因为本质还是boosting算法。我们知道,决策树的学习最耗时的一个步骤是对特征的值进行排序(因为要确定最佳分割点)。xgboost在训练之前,预先对数据进行了排序,然后保存为block结构,后面的迭代中重复地使用这个结构,大大减小计算量。这个block结构也使得并行成为可能。在进行节点分裂时,需要计算每个特征的增益,最终选增益最大的那个特征去做分裂,那么各个特征的增益计算就可以开多线程进行。
可并行的近似直方图计算。
XGBoost Scalable的体现
XGBoost的paper在KKD上发表,名为:《Xgboost: A scalable tree boosting system》,那么scalable体现在哪?
参考知乎上王浩的回答 ,修改如下:
模型的scalability:弱分类器可以支持cart也可以支持lr和linear, 但其实这是Boosting算法做的事情,XGBoost只是实现了而已。
目标函数的scalability: 支持不同的loss function, 支持自定义loss function,只要一、二阶可导。有这个特性是因为泰勒二阶展开,得到通用的目标函数形式。
学习方法的scalability:Block结构支持并行化,支持 Out-of-core计算(这点和王浩的看法不一样,他写的是优化的trick)
XGBoost 防止过拟合的方法
目标函数的正则项, 叶子节点数+叶子节点数输出分数的平方和。相当于预剪枝。
Ω
(
f
t
)
=
γ
T
+
1
2
λ
∑
j
=
1
T
w
j
2
\Omega(f_t) =\gamma T +\frac{1}{2}\lambda\sum_{j=1}^{T} w_j^2
Ω ( f t ) = γ T + 2 1 λ j = 1 ∑ T w j 2
行抽样和列抽样:训练的时候只用一部分样本和一部分特征
可以设置树的最大深度
η
\eta
η : 可以叫学习率、步长或者shrinkage
Early stopping:使用的模型不一定是最终的ensemble,可以根据测试集的测试情况,选择使用前若干棵树
XGBoost的缺点
空间开销大。需要保存数据的特征值。XGBoost采用Block结构,存储指向样本的索引 ,需要消耗两倍的内存 。
时间开销大(相对于lightGBM而言)。在寻找最优切分点时,要对每个特征都进行排序,还要对每个特征的每个值都进行了遍历,并计算增益。
对Cache不友好(相对于lightGBM而言)。使用Block块预排序后,特征对梯度的访问是按照索引来获取的,是一种随机访问,而不同特征访问顺序也不一样,容易照成命中率低的问题。同时,在每一层长树的时候,需要随机访问一个行索引到叶子索引的数组,并且不同特征访问的顺序也不一样,也会造成较大的Cachemiss。
参考资料
『我爱机器学习』集成学习(三)XGBoost