目录
写在前面
毕业之后开始做推荐,接触了很多机器学习的知识和技能,依旧是从小白做起,认真学习,认真记录,从Xgb开始,当然一些很基础的知识,比如叶节点之类的就不在此赘述.
1.0 决策树
目前最流行的两类算法是 基于深度学习的-神经网络 和 基于机器学习的 树形算法 主要是决策树, 决策树分为 1. 分类决策树:处理离散数据 2. 回归决策树:处理连续数据
决策树是将空间用超平面进行划分,每次分割,都将当前的空间根据特征的取值进行划分,最终使每一个叶子节点都是在当前空间的一个不相交的区域,在决策的时候,会根据输入样本的特征,一步一步进行划分,最后使每个样本都落入不相交的N个区域.
分类回归树(Classification And Regression, CART)由特征选择,树的生成和剪枝构成
XGB的基础学习器是CART树.
2.0 XGB模型
2.1 举个栗子
首先,生成
K
K
K棵树,每棵树用
f
k
f_k
fk代表,输入样本
x
x
x,输出就是所有
f
k
(
x
)
f_k(x)
fk(x)的加和,因为每棵树都会将当前的所有样本进行划分,然后该样本
x
x
x会落到某一个划分空间,会有一个分值,将每棵树的这个分值进行加和就是最终的结果,公式为:
y
^
=
∑
k
=
1
k
f
k
(
x
i
)
\hat y = \sum^k_{k=1}f_k(x_i)
y^=k=1∑kfk(xi)
即将所有的树对该样本的权值进行累加.
图
1
谁
最
爱
打
游
戏
图1 谁最爱打游戏
图1谁最爱打游戏
图中, 两棵树构成了一个Boosting算法.训练样本有5个,
- 第一棵树 按照年龄和性别将样本分别划分为三个叶子节点,其中4个负样本一个正样本;
- 第二棵树按照是否使用电脑,将分为两类
那么,男孩玩游戏的预测值为两棵树之和.,如果是回归问题,则将值直接输出,如果是分类问题,带入到 S i g m o i d Sigmoid Sigmoid函数进行概率化.
2.2 目标函数原理
这里重点总结如何推导出最终的损失函数,即目标函数
其实为了解决上述问题,只需要知道每棵树的结构
f
k
f_k
fk和每个叶子节点的权重即可.
f
k
f_k
fk实则是关于样本
x
i
x_i
xi的一个函数,现在需要做的就是让
∑
k
=
1
K
f
k
(
x
i
)
\sum^K_{k=1} f_k(x_i)
∑k=1Kfk(xi)与真实样本的标签之间的损失最小,通过最小化差值,来逐渐学习这个函数,即构造这个树的结构.损失函数的形式为:
L
=
∑
i
=
1
n
l
(
y
i
,
y
^
i
)
+
∑
k
=
1
K
Ω
(
f
k
)
L = \sum^n_{i=1} l(y_i, \hat y_i) + \sum ^K_{k=1} \Omega(f_k)
L=i=1∑nl(yi,y^i)+k=1∑KΩ(fk)
其中
l
(
y
i
,
y
^
i
)
l(y_i, \hat y_i)
l(yi,y^i) 是训练集中每个样本点的真实值
y
i
y_i
yi和预测值
y
^
i
\hat y_i
y^i的差距, 第一棵树缩小的是
y
y
y和
y
^
\hat y
y^之间的差距,第二棵树则缩小的是
y
−
y
^
y-\hat y
y−y^与第二棵树的差距.
Ω
\Omega
Ω是正则项,用来控制模型的复杂度.
具体迭代过程如下:
- 初始值: y ^ i ( 0 ) = 0 = f 0 ( x i ) \hat y^{(0)}_i=0 = f_0(x_i) y^i(0)=0=f0(xi)
- 第1棵树: y ^ i ( 1 ) = y ^ i ( 0 ) + f 1 ( x i ) = f 0 ( x i ) + f 1 ( x i ) \hat y ^{(1)}_i= \hat y^{(0)}_i + f_1(x_i) = f_0(x_i) + f_1(x_i) y^i(1)=y^i(0)+f1(xi)=f0(xi)+f1(xi)
- 第2棵树: y ^ ( 2 ) = y ^ ( 1 ) + f 2 ( x i ) = f 0 ( x i ) + f 1 ( x i ) + f 2 ( x i ) \hat y^{(2)} = \hat y^{(1)} +f_2(x_i)=f_0(x_i)+f_1(x_i)+f_2(x_i) y^(2)=y^(1)+f2(xi)=f0(xi)+f1(xi)+f2(xi)
- 第t棵树: y ^ ( t ) = y ^ ( t − 1 ) + f t ( x i ) = ∑ k = 0 t f t ( x i ) \hat y^{(t)}=\hat y^{(t-1)} +f_t(x_i)=\sum _{k=0}^t f_t(x_i) y^(t)=y^(t−1)+ft(xi)=∑k=0tft(xi)
目标函数计算的则是标签
y
i
y_i
yi和第t棵树的输出
y
^
i
(
t
)
\hat y_i^{(t)}
y^i(t)之间的损失以及惩罚项
Ω
\Omega
Ω,并逐渐缩小差距,达到最终的预测效果。在这个过程中,采用了泰勒展开的二阶展开,原始泰勒展开为:
f
(
x
+
Δ
x
)
⋍
f
(
x
)
+
f
′
(
x
)
Δ
x
+
1
2
f
′
′
(
x
)
+
Δ
x
2
f(x+\Delta x)\backsimeq f(x)+f'(x) \Delta x + \frac{1}{2} f''(x)+\Delta x^2
f(x+Δx)⋍f(x)+f′(x)Δx+21f′′(x)+Δx2
xgb的目标函数为:
L
(
ϕ
)
=
∑
i
l
(
y
i
,
y
^
i
)
+
∑
k
Ω
(
f
k
)
L_{(\phi)}=\sum_i l(y_i, \hat y_i) +\sum _k \Omega (f_k)
L(ϕ)=i∑l(yi,y^i)+k∑Ω(fk)
其中,
Ω
\Omega
Ω为惩罚项,不做解释;
将xgb的损失函数的前半部分与泰勒二阶展开做一个对应,首先说梯度提升,其实每棵树的
f
(
t
)
(
x
i
)
f^{(t)}(x_i)
f(t)(xi)都是基于上一棵树的一个差值,其实相当于一个
Δ
\Delta
Δ差值,对应到泰勒二级展开式中的
Δ
x
\Delta x
Δx 那么每棵树最终的输出结果
y
^
i
\hat y_i
y^i为在这个
Δ
\Delta
Δ梯度上的一个加法结果,即提升结果,因此叫梯度提升树;
因此,将xgb的损失函数做一个二级泰勒展开后的结果为:
l
(
y
i
,
y
^
i
)
=
l
(
y
i
,
y
i
(
t
−
1
)
+
f
t
(
x
i
)
)
=
l
(
y
i
,
y
^
i
(
t
−
1
)
+
∂
y
^
(
t
−
1
)
l
(
y
i
,
y
^
(
t
−
1
)
)
f
t
(
x
i
)
+
1
2
∂
y
^
(
t
−
1
)
2
l
(
y
i
,
y
^
(
t
−
1
)
)
f
t
2
(
x
i
)
)
l(y_i, \hat y_i) = l(y_i, y^{(t-1)}_i + f_t(x_i)) \\ =l(y_i, \hat y_i^{(t-1)} + \partial_{\hat y^{(t-1)}}l(y_i, \hat y^{(t-1)}) f_t(x_i) + \frac{1}{2} \partial^2_{\hat y ^{(t-1)}}l(y_i, \hat y^{(t-1)}) f_t^2(x_i))
l(yi,y^i)=l(yi,yi(t−1)+ft(xi))=l(yi,y^i(t−1)+∂y^(t−1)l(yi,y^(t−1))ft(xi)+21∂y^(t−1)2l(yi,y^(t−1))ft2(xi))
因为
y
i
y_i
yi为常数项,对梯度下降没有影响,因此最终的损失函数,可以看成为:
定义
I
j
=
{
i
∣
q
(
x
i
)
=
j
}
I_j=\{i|q(x_i)=j \}
Ij={i∣q(xi)=j}叶子结点输出值的集合,此处
ω
\omega
ω等价于
f
(
t
)
(
x
i
)
f^{(t)}(x_i)
f(t)(xi)
L
(
t
)
=
∑
i
=
1
n
[
∂
y
^
(
t
−
1
)
l
(
y
i
,
y
^
(
t
−
1
)
)
f
t
(
x
i
)
+
1
2
∂
y
^
(
t
−
1
)
2
l
(
y
i
,
y
^
(
t
−
1
)
)
f
t
2
(
x
i
)
+
Ω
(
f
k
)
]
=
∑
i
=
1
n
[
g
i
f
t
(
x
i
)
+
1
2
h
i
f
k
2
(
x
i
)
]
+
γ
T
+
1
2
λ
∑
j
=
1
T
ω
j
2
=
∑
j
=
1
T
[
(
∑
i
∈
I
i
g
g
i
)
ω
j
+
1
2
(
∑
i
∈
I
j
h
i
+
λ
)
ω
j
2
]
+
λ
T
L^{(t)}=\sum _{i=1}^n[ \partial_{\hat y^{(t-1)}}l(y_i, \hat y^{(t-1)}) f_t(x_i) + \frac{1}{2} \partial^2_{\hat y ^{(t-1)}}l(y_i, \hat y^{(t-1)}) f_t^2(x_i)+\Omega(f_k)] \\ =\sum^n_{i=1}[g_if_t(x_i)+\frac{1}{2}h_if^2_k(x_i)]+\gamma T+\frac{1}{2}\lambda \sum^T_{j=1}\omega^2_j \\ =\sum^T_{j=1}[(\sum _{i \in I_i g} g_i)\omega_j+\frac{1}{2}(\sum_{i \in I_j}h_i+\lambda)\omega^2_j]+\lambda T
L(t)=i=1∑n[∂y^(t−1)l(yi,y^(t−1))ft(xi)+21∂y^(t−1)2l(yi,y^(t−1))ft2(xi)+Ω(fk)]=i=1∑n[gift(xi)+21hifk2(xi)]+γT+21λj=1∑Tωj2=j=1∑T[(i∈Iig∑gi)ωj+21(i∈Ij∑hi+λ)ωj2]+λT
其中,
n
n
n表示有n个样本,
k
k
k表示有k棵树;
然后将损失函数对
ω
\omega
ω求导得:
ω
j
∗
=
−
∑
i
∈
I
j
g
i
∑
i
∈
I
j
h
i
+
λ
\omega^*_j=-\frac{\sum_{i \in I_j}g_i}{\sum_{i \in I_j}h_i+\lambda}
ωj∗=−∑i∈Ijhi+λ∑i∈Ijgi
然后将
ω
j
∗
\omega^*_j
ωj∗回代之后,获取最优的解,也是XGB最终评估CART树的标准函数
L
(
t
)
(
q
)
=
−
1
2
∑
j
=
1
T
(
∑
i
∈
I
j
g
i
)
2
∑
i
∈
I
j
h
i
+
λ
+
γ
T
L^{(t)}(q)=-\frac{1}{2} \sum^T_{j=1}\frac{(\sum_{i \in I_j}g_i)^2}{\sum_{i \in I_j} h_i +\lambda}+\gamma T
L(t)(q)=−21j=1∑T∑i∈Ijhi+λ(∑i∈Ijgi)2+γT
至此,损失函数已经推导完毕。
2.3 分裂条件
在上述推导过程中,我们明确了在清晰的知道一棵树的结构之后,如何求得每个叶子结点的分数。但我们还没介绍如何确定树结构,即每次特征分裂怎么寻找最佳特征,怎么寻找最佳分裂点。
基于空间切分去构造一颗决策树是一个NP难问题,我们不可能去遍历所有树结构,因此,XGBoost使用了和CART回归树一样的想法,利用贪婪算法,遍历所有特征的所有特征划分点,不同的是使用上式目标函数值作为评价函数。具体做法就是分裂后的目标函数值比单子叶子节点的目标函数的增益。
同时为了限制树生长过深,还加了个阈值,只有当增益大于该阈值才进行分裂。同时可以设置树的最大深度、当样本权重和小于设定阈值时停止生长去防止过拟合。论文中提到了一个贪心选择分裂的算法,具体的做法是预先对特征值进行排序,这样一次遍历过程中通过对前缀和以及后缀和的计算便可以覆盖所有的情况。
2.4 Shrinkage and Column Subsampling
XGBoost还提出了两种防止过拟合的方法:Shrinkage and Column Subsampling。
- Shrinkage方法就是在每次迭代中对树的每个叶子结点的分数乘上一个缩减权重η,这可以使得每一棵树的影响力不会太大,留下更大的空间给后面生成的树去优化模型。
- Column Subsampling类似于随机森林中的选取部分特征进行建树。其可分为两种,一种是按层随机采样,在对同一层内每个结点分裂之前,先随机选择一部分特征,然后只需要遍历这部分的特征,来确定最优的分割点。另一种是随机选择特征,则建树前随机选择一部分特征然后分裂就只遍历这些特征。一般情况下前者效果更好。
2.5 近似算法
对于连续型特征值,当样本数量非常大,该特征取值过多时,遍历所有取值会花费很多时间,且容易过拟合。因此XGBoost思想是先根据求得的特征权重(feature_importance 根据gini增益指数算出来的)对特征排序获取一些分裂候选点,之后根据候选点对特征进行分桶,即找到l个划分点,将位于相邻分位点之间的样本分在一个桶中。在遍历该特征的时候,只需要遍历各个分位点,从而计算最优划分。
从算法伪代码中该流程还可以分为两种:
-
全局的近似是在新生成一棵树之前就对各个特征计算分位点并划分样本,之后在每次分裂过程中都采用近似划分,
-
局部近似就是在具体的某一次分裂节点的过程中采用近似算法。
2.6 针对稀疏数据的算法(缺失值处理)
当样本的第i个特征值缺失时,无法利用该特征进行划分时,XGBoost的想法是将该样本分别划分到左结点和右结点,然后计算其增益,哪个大就划分到哪边。
2.7 并行
大家知道,Boosting算法的弱学习器是没法并行迭代的,但是单个弱学习器里面最耗时的是决策树的分裂过程,XGBoost针对这个分裂做了比较大的并行优化。对于不同的特征的特征划分点,XGBoost分别在不同的线程中并行选择分裂的最大增益。
同时,对训练的每个特征排序并且以块的的结构存储在内存中,方便后面迭代重复使用,减少计算量。计算量的减少参见上面第4节的算法流程,首先默认所有的样本都在右子树,然后从小到大迭代,依次放入左子树,并寻找最优的分裂点。这样做可以减少很多不必要的比较。
注: xgboost的并行不是tree粒度的并行,xgboost也是一次迭代完才能进行下一次迭代的(第t次迭代的代价函数里包含了前面t-1次迭代的预测值)。 而是在选择最佳分裂点,进行枚举的时候并行。
- xgboost的并行是在特征粒度上的。我们知道,决策树的学习最耗时的一个步骤就是对特征的值进行排序(因为要确定最佳分割点),xgboost在训练之前,预先对数据进行了排序,然后保存为block结构,后面的迭代中重复地使用这个结构,大大减小计算量。这个block结构也使得并行成为了可能,在进行节点的分裂时,需要计算每个特征的增益,最终选增益最大的那个特征去做分裂,那么各个特征的增益计算就可以开多线程进行。
- 可并行的近似直方图算法。树节点在进行分裂时,我们需要计算每个特征的每个分割点对应的增益,即用贪心法枚举所有可能的分割点。当数据无法一次载入内存或者在分布式情况下,贪心算法效率就会变得很低,所以xgboost还提出了一种可并行的近似直方图算法,用于高效地生成候选的分割点。
- 这种近似直方图算法和LightGBM的直方图Histogram算法有什么区别呢?
LightGBM里默认的训练决策树时使用直方图算法,XGBoost里默认的方法是对特征预排序,直方图算法是一种牺牲了一定的切分准确性而换取训练速度以及节省内存空间消耗的算法
具体的过程如下图所示:
此外,通过设置合理的分块的大小,充分利用了CPU缓存进行读取加速(cache-aware access)。使得数据读取的速度更快。另外,通过将分块进行压缩(block compressoin)并存储到硬盘上,并且通过将分块分区到多个硬盘上实现了更大的IO。
参考链接
https://www.zhihu.com/question/58883125/answer/206813653
https://zhuanlan.zhihu.com/p/82054400
https://www.zhihu.com/question/41354392