项目github地址:bitcarmanlee easy-algorithm-interview-and-practice
欢迎大家star,留言,一起学习进步
1.CART树回顾
在正式讲Xgboost之前,我们先回顾一下CART树。
CART树的生成过程,就是递归构建二叉树的过程,本质上是在某个特征维度对样本空间进行划分。这种空间划分是一种NP Hard问题,因此一般都是用启发式的方式去求解,即求某个特征j的且分点s,使得
min
j
,
s
[
min
c
1
∑
x
i
∈
R
1
(
j
,
s
)
(
y
i
−
c
1
)
2
+
min
c
2
∑
x
i
∈
R
2
(
j
,
s
)
(
y
i
−
c
2
)
2
]
\min_ { j , s } \left[ \min _ { c _ { 1 } } \sum _ { x _ { i } \in R _ { 1 } ( j , s ) } \left( y _ { i } - c _ { 1 } \right) ^ { 2 } + \min _ { c _ { 2 } } \sum _ { x _ { i } \in R _ { 2 } ( j , s ) } \left( y _ { i } - c _ { 2 } \right) ^ { 2 } \right]
j,smin⎣⎡c1minxi∈R1(j,s)∑(yi−c1)2+c2minxi∈R2(j,s)∑(yi−c2)2⎦⎤
2.XGBoost叶子节点取值推导
XGBoost的大体思路跟GBDT一样,也是通过不断进行特征分裂添加树,来学习一个新的弱学习器,来拟合上次预测结果的残差。
首先来看一下XGBoost的损失函数
L
(
ϕ
)
=
∑
i
l
(
y
i
,
y
^
i
)
+
∑
k
Ω
(
f
k
)
,
其
中
Ω
(
f
k
)
=
γ
T
+
1
2
λ
∣
∣
w
∣
∣
2
L ( \phi ) = \sum _ { i } l \left( y _ { i } , \hat { y } _ { i } \right) + \sum _ { k } \Omega \left( f _ { k } \right) ,\quad 其中 \quad \Omega \left( f _ { k } \right) = \gamma T + \frac { 1 } { 2 } \lambda | | w | | ^ { 2 }
L(ϕ)=i∑l(yi,y^i)+k∑Ω(fk),其中Ω(fk)=γT+21λ∣∣w∣∣2
很容易可以看出来
∑
i
l
(
y
i
,
y
^
i
)
\sum\limits_i l \left( y _ { i } , \hat { y } _ { i } \right)
i∑l(yi,y^i)是代表预测值与真实值的偏差,而
∑
k
Ω
(
f
k
)
\sum\limits_k \Omega \left( f _ { k } \right)
k∑Ω(fk)为正则项。
其中, y i y_i yi为样本的真实值, y ^ i \hat y_ i y^i为预测值。 f k f_k fk表示第k棵树, γ \gamma γ为叶子树的正则项,起到剪枝的作用,T为树的叶子节点个数, w w w为叶子节点的分数, λ \lambda λ为叶子节点分数的正则项,起到防止过拟合的作用。
前面树系列的文章提到过很多次,boost系列算法的核心思想是用新生成的树拟合上次预测的残差(对于GBDT来说是上次预测的负梯度方向)。生成第t棵树后,预测值可以写为
y
^
i
=
y
^
i
t
−
1
+
f
t
(
x
)
\hat y_i = \hat y_i ^{t-1} + f_t(x)
y^i=y^it−1+ft(x)
此时,损失函数可以表示为
L t = ∑ i = 1 l ( y i t , y ^ i t − 1 ) + f t ( x i ) + Ω ( f t ) L^t = \sum _{i=1} l ( y _i^t , \hat y_i ^{t-1}) + f_t(x_i) + \Omega (f _t) Lt=i=1∑l(yit,y^it−1)+ft(xi)+Ω(ft)
于是我们要找一个
f
t
f_t
ft让损失函数最小。对上面的式子进行泰勒展开
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(yi,y^(t−1))+gift(xi)+21hift2(xi)]+Ω(ft)
其中,
g
i
=
∂
l
(
y
i
,
y
^
i
t
−
1
)
∂
y
^
i
t
−
1
g_i = \frac{\partial l(y_i, \hat y_i ^{t-1})} {\partial \hat y_i ^{t-1}}
gi=∂y^it−1∂l(yi,y^it−1)
h
i
=
∂
2
l
(
y
i
,
y
^
i
t
−
1
)
∂
(
y
^
i
t
−
1
)
2
h_i = \frac{\partial ^ 2 l(y_i, \hat y_i ^{t-1})} {\partial (\hat y_i ^{t-1})^2}
hi=∂(y^it−1)2∂2l(yi,y^it−1)
其中
g
i
,
h
i
g_i, h_i
gi,hi的含义可以按如下方式理解
假设目前已经有t-1棵数,这t-1棵树组成的模型对第i个训练样本有一个预测值
y
^
i
\hat y_i
y^i,
y
^
i
\hat y_i
y^i与真实值
y
i
y_i
yi肯定有差距,这个差距可以用
l
(
y
i
,
y
^
i
)
l(y_i, \hat y_i)
l(yi,y^i)这个损失函数来衡量,所以此处的
g
i
g_i
gi与
h
i
h_i
hi就是对于该损失函数的一阶导和二阶导。(参考文献1)
搞定了前面的损失函数部分,接下来再观察一下正则项
Ω
(
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
Ω(ft)=γT+21λj=1∑Twj2
对于上面的正则项,我们最简单的理解方式为:要使模型尽可能简单,那就是叶子节点的个数T要小,同时叶子节点上的值也尽可能小。
因为前t-1棵树的预测分数与y的残差对目标函数优化不影响, 可以直接去掉,所以损失函数可以写成
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 ) } \simeq \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] + \Omega \left( f _ { t } \right) L(t)≃i=1∑n[gift(xi)+21hift2(xi)]+Ω(ft)
上面式子的含义是将每个样本的损失函数相加,而每个样本最终都会落到一个叶子节点上,所以我们可以将所有同一个叶子结点的样本重组起来
L
^
(
t
)
=
∑
i
=
1
n
[
g
i
f
t
(
x
i
)
+
1
2
h
t
f
t
2
(
x
i
)
]
+
Ω
(
f
t
)
=
∑
i
=
1
n
[
g
i
f
t
(
x
i
)
+
1
2
h
t
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
=
1
2
∑
j
=
1
T
(
H
j
+
λ
)
(
w
j
+
G
j
H
j
+
λ
)
2
+
γ
T
−
1
2
∑
j
=
1
T
G
j
2
H
j
+
λ
\begin{aligned} \hat{ \mathcal { L }}^{(t)} & =\sum_{i=1}^n [g_if_t(x_i) + \frac12h_tf_t^2(x_i)] + \Omega(f_t) \\ & =\sum_{i=1}^n [g_if_t(x_i) + \frac12h_tf_t^2(x_i)] + \gamma T+\frac{1}{2}\lambda\sum\limits_{j=1}^{T}w_j^2 \\ & = \sum\limits_{j=1}^{T} [(\sum\limits_{i\in I_j}g_i)w_j+\frac{1}{2}(\sum\limits_{i\in I_j}h_i+\lambda) w_j^2]+\gamma T \\ & = \frac{1}{2}\sum\limits_{j=1}^{T} (H_j+\lambda)(w_j + \frac{G_j}{H_j+\lambda})^2+\gamma T -\frac{1}{2}\sum\limits_{j=1}^{T}\frac{G_j^2}{H_j+\lambda} \end{aligned}
L^(t)=i=1∑n[gift(xi)+21htft2(xi)]+Ω(ft)=i=1∑n[gift(xi)+21htft2(xi)]+γT+21λj=1∑Twj2=j=1∑T[(i∈Ij∑gi)wj+21(i∈Ij∑hi+λ)wj2]+γT=21j=1∑T(Hj+λ)(wj+Hj+λGj)2+γT−21j=1∑THj+λGj2
其中, G j = ∑ i ∈ I j g i G_j = \sum _{i \in I_j} g_i Gj=∑i∈Ijgi是落入叶子节点i所有样本一阶梯度的总和,而 H j = ∑ i ∈ I j h i H_j = \sum _{i \in I_j} h_i Hj=∑i∈Ijhi是落入叶子节点i所有样本二阶梯度的总和。
基于中学数学的原理,我们可以得知:
当
w
j
∗
=
−
G
j
H
j
+
λ
w_j^* = -\frac{G_j}{H_j+\lambda}
wj∗=−Hj+λGj
时,损失函数最小。此时最终的损失函数大小为
L
^
∗
=
−
1
2
∑
j
=
1
T
G
j
2
H
j
+
λ
+
γ
T
\hat{\mathcal { L }}^{*} =-\frac{1}{2}\sum\limits_{j=1}^{T}\frac{G_j^2}{H_j+\lambda}+\gamma T
L^∗=−21j=1∑THj+λGj2+γT
3.与牛顿法的关系
上面我们推导过程中,用到了二阶导数。而常见的基于二阶导数的优化方法就是牛顿法,下面我们看看与牛顿法的关系。
假设有一个函数
f
(
x
)
f(x)
f(x)二阶可微,我们对其进行泰勒展开到二阶,有
f
(
x
)
=
f
(
x
k
)
+
f
′
(
x
k
)
(
x
−
x
k
)
+
f
′
′
(
x
k
)
(
x
−
x
k
)
2
f(x) = f(x_k) + f'(x_k)(x-x_k) + f''(x_k)(x-x_k)^2
f(x)=f(xk)+f′(xk)(x−xk)+f′′(xk)(x−xk)2
上面的式子对x求导,并令导数等于0
f
′
(
x
k
)
+
f
′
′
(
x
k
)
(
x
−
x
k
)
=
0
f'(x_k) + f''(x_k)(x-x_k) = 0
f′(xk)+f′′(xk)(x−xk)=0
可以得到x的迭代公式为
x
=
x
k
−
f
′
(
x
k
)
f
′
′
(
x
k
)
x = x_k - \frac{f'(x_k)}{f''(x_k)}
x=xk−f′′(xk)f′(xk)
这就是牛顿法的迭代公式。
对比一下叶子节点的取值方式
w
j
∗
=
−
G
j
H
j
+
λ
w_j^* = -\frac{G_j}{H_j+\lambda}
wj∗=−Hj+λGj
与牛顿法唯一的区别在于,分母上二阶导多了一个正则项
λ
\lambda
λ,形式上完全一致,就是一阶导除以二阶导,与牛顿法的形式完全一致。
4.XGBoost的树分裂方式
上面推导了这么一大串,都是在分析叶子节点怎么取值。提升树系列的算法,还有很重要的一点是树的分裂方式。
首先我们回顾一下其他算法比如GBDT是怎么进行分裂的。标准的做法是选择MSE作为损失函数,采用启发式的方式来选择某个特征j的切分点s进行分裂。这个分裂的过程,并不一定总与损失函数相关。比如在分类树中,我们一般都会用对数损失函数(交叉熵)来评估模型的最终效果,这个时候树分裂的结果与损失函数就没有直接的关系。
XGBoost的树分裂是直接与损失函数相关的。在分裂的时候,会计算损失函数的增益Gain
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} \left[\frac{G_L^2}{H_L+\lambda} + \frac{G_R^2}{H_R+\lambda} - \frac{(G_L + G_R)^2}{H_L + H_R+\lambda} \right] - \gamma
Gain=21[HL+λGL2+HR+λGR2−HL+HR+λ(GL+GR)2]−γ
上面这个式子其实很容易理解
(
G
L
+
G
R
)
2
H
L
+
H
R
+
λ
\frac{(G_L + G_R)^2}{H_L + H_R+\lambda}
HL+HR+λ(GL+GR)2是分裂前最终的损失函数值,而
G
L
2
H
L
+
λ
,
G
R
2
H
R
+
λ
\frac{G_L^2}{H_L+\lambda}, \frac{G_R^2}{H_R+\lambda}
HL+λGL2,HR+λGR2
分别为分裂后的左右子树的损失函数值
那树进行分裂的时候,自然希望损失函数减少得越多越好。
前面我们推导出了损失函数最终的表达式
L ^ ∗ = − 1 2 ∑ j = 1 T G j 2 H j + λ + γ T \hat{\mathcal { L }}^{*} =-\frac{1}{2}\sum\limits_{j=1}^{T}\frac{G_j^2}{H_j+\lambda}+\gamma T L^∗=−21j=1∑THj+λGj2+γT
那我们想要的结果就是分列前的损失函数减去分裂后的损失函数,差值最大,注意前面的负号,最后的分裂准则为
m
a
x
(
G
L
2
H
L
+
λ
+
G
R
2
H
R
+
λ
−
(
G
L
+
G
R
)
2
H
L
+
H
R
+
λ
)
max\left(\frac{G_L^2}{H_L+\lambda} + \frac{G_R^2}{H_R+\lambda} - \frac{(G_L + G_R)^2}{H_L + H_R+\lambda}\right)
max(HL+λGL2+HR+λGR2−HL+HR+λ(GL+GR)2)
5.XGBoost的各种Tricks总结
防止过拟合方法:
1.加入了正则项,对叶子节点数量,叶子节点分数加入了正则惩罚项。
2.加入了学习率,减少了单棵树影响,给后面树的优化留了更多空间。
3.列采样(特征采样),与随机森林类似,不仅来带来更好的效果,还可以提高运行速度。
缺失值的处理
在别的算法中,一般会使用中位数,均值或者两者进行融合计算的方式去处理缺失值。但是xgboost能处理缺失值,模型允许缺失值存在。
在寻找分裂点的时候,不对该特征缺失的样本进行遍历,只遍历该特征有的样。具体实现时,将该特征值缺失的样本分别分配到左叶子节点与右叶子节点,分别计算增益,然后选择增益较大的方向进行分裂即可。如果训练样本中没有缺失值,而预测时有缺失,默认将缺失值划分到右子节点。
分裂点的选择
并不把所有特征值间的间隔点作为候选分裂点,而是用分位数的方法将其作为分裂的候选集。
并行处理
XGBoost中虽然树之间是串行关系,但是同层级的节点可以并行。对于某个节点,节点内选择最佳分裂点与计算分裂点的增益是可以并行的。
基学习器(弱学习器)
传统的GBDT与CART树作为基分类器,XGBoost不仅支持CART树,还支持线性分类器,这个时候xgboost就相当于带L1与L2正则项的逻辑斯蒂回归(分类问题)或者线性回归(回归问题)。
6.XGBoost 与GBDT的对比
主要的区别其实就是上面的Tricks。
参考文献
1.https://www.jianshu.com/p/ac1c12f3fba1