1 概述
XGBoost是陈天奇等人开发的一个开源机器学习项目,高效地实现了GBDT算法并进行了算法和工程上的许多改进,被广泛应用在许多机器学习竞赛中并取得了不错的成绩,在工业界也广受欢迎。
XGBoost本质上还是一个GBDT,但是力争把速度和效率发挥到极致,所以叫X (Extreme) GBoosted。
XGBoost算法的在性能、速度方面都超越了许多其他算法,主要得益于以下几个方面:
- 是一个基于多个弱分类器组成的模型,可以有效降低过拟合的风险
- 提出了一种加权分桶的方法,减少计算量
- 对稀疏数据进行默认划分,减少计算量
- 对内存的优化和做了并行化处理
- 对CPU缓存的优化
- 对存储的优化
2 算法原理
2.1 算法主流程
这里简要总结XGBoost的算法主流程,基于决策树弱分类器。不涉及运行效率的优化和健壮性优化的内容。
输入是训练集样本
I
=
{
(
x
1
,
y
1
)
,
(
x
2
,
y
2
)
,
…
,
(
x
m
,
y
m
)
}
I=\{(x_1,y_1),(x_2,y_2),\dots,(x_m,y_m)\}
I={(x1,y1),(x2,y2),…,(xm,ym)},最大迭代次数T, 损失函数L, 正则化系数
λ
,
γ
\lambda,\gamma
λ,γ。
输出是强学习器
f
(
x
)
f(x)
f(x)
对迭代轮数
t
=
1
,
2
,
.
.
.
T
t=1,2,...T
t=1,2,...T有:
- 计算第 i i i个样本 ( i − 1 , 2 , . . m ) (i-1,2,..m) (i−1,2,..m)在当前轮损失函数 L L L基于 f t − 1 ( x i ) f_{t-1}(x_i) ft−1(xi)的一阶导数 g t i g_{ti} gti,二阶导数 h t i h_{ti} hti,计算所有样本的一阶导数和 G t = ∑ i = 1 m g t i G_t=\sum^m_{i=1}g_{ti} Gt=∑i=1mgti,二阶导数和 H t = ∑ t = 1 m h t i H_t=\sum^m_{t=1}h_{ti} Ht=∑t=1mhti
- 基于当前节点尝试分裂决策树,默认分数score=0,G和H为当前需要分裂的节点的一阶二阶导数之和。
对特征序号 k = 1 , 2... K k=1,2...K k=1,2...K :
a) G L = 0 , H L = 0 G_L=0,H_L=0 GL=0,HL=0
b.1) 将样本按特征k从小到大排列,依次取出第i个样本,依次计算当前样本放入左子树后,左右子树一阶和二阶导数和:
G L = G L + g t i , G R = G − G L G_L=G_L+g_{ti},G_R=G-G_L GL=GL+gti,GR=G−GL
H L = H L + h t i , H R = H − H L H_L=HL+h_{ti},H_R=H-H_L HL=HL+hti,HR=H−HL
b.2) 尝试更新最大的分数:
s c o r e = m a x ( 1 2 G L 2 H L + λ + 1 2 G R 2 H R + λ − 1 2 ( G L + G R ) 2 H L + H R + λ − γ ) score=max \left(\frac{1}{2}\frac{G^2_L}{H_L+\lambda} + \frac{1}{2}\frac{G^2_R}{H_R+\lambda} -\frac{1}{2}\frac{(G_L+G_R)^2}{H_L+H_R+\lambda}-\gamma\right) score=max(21HL+λGL2+21HR+λGR2−21HL+HR+λ(GL+GR)2−γ) - 基于最大score对应的划分特征和特征值分裂子树。
- 如果最大score为0,则当前决策树建立完毕,计算所有叶子区域的,得到弱学习器 h t ( x ) h_t(x) ht(x),更新强学习器 f t ( x ) f_t(x) ft(x),进入下一轮弱学习器迭代.如果最大score不是0,则转到第2步继续尝试分裂决策树。
备注:关于损失函数以及score的定义在随后两节中有详细讲述。
2.2 损失函数
由于XGBoost是基于GBDT的优化,所以在看XGBoost本身的优化内容前,我们先回顾下GBDT的回归算法迭代的流程,对于GBDT的第t颗决策树,主要是走下面4步:
1)对样本i=1,2,…m,计算负梯度
r
t
i
=
−
[
∂
L
(
y
i
,
f
(
x
i
)
)
∂
f
(
x
i
)
]
f
(
x
)
=
f
t
−
1
(
x
)
r_{ti}=−\left[\frac{∂L(yi,f(xi))}{∂f(xi)}\right]f(x)=f_{t−1}(x)
rti=−[∂f(xi)∂L(yi,f(xi))]f(x)=ft−1(x)
2)利用
(
x
i
,
r
t
i
)
(
i
=
1
,
2
,
.
.
m
)
(x_i,r_{ti})(i=1,2,..m)
(xi,rti)(i=1,2,..m), 拟合一颗CART回归树,得到第t颗回归树,其对应的叶子节点区域为
R
t
j
,
j
=
1
,
2
,
.
.
.
,
J
R_{tj},j=1,2,...,J
Rtj,j=1,2,...,J。其中
J
J
J为回归树t的叶子节点的个数。
3) 对叶子区域
j
=
1
,
2
,
.
.
J
j =1,2,..J
j=1,2,..J,计算最佳拟合值
c
t
j
=
a
r
g
m
i
n
⏟
c
∑
x
i
∈
R
t
j
L
(
y
i
,
f
t
−
1
(
x
i
)
+
c
)
c_{tj}=\underbrace{arg\;min}_{c}\sum_{x_i\in R_{tj}}L(y_i, f_{t-1}(x_i)+c)
ctj=c
argmin∑xi∈RtjL(yi,ft−1(xi)+c)
4) 更新强学习器
f
t
(
x
)
=
f
t
−
1
(
x
)
+
∑
j
=
1
J
c
t
j
I
(
x
∈
R
t
j
)
f_t(x)=f_{t-1}(x)+\sum^{J}_{j=1}c_{tj}I(x\in R_{tj})
ft(x)=ft−1(x)+∑j=1JctjI(x∈Rtj)
第一步是得到负梯度,或者是泰勒展开式的一阶导数。第二步是第一个优化求解,即基于残差拟合一颗CART回归树,得到J个叶子节点区域。第三步是第二个优化求解,在第二步优化求解的结果上,对每个节点区域再做一次线性搜索,得到每个叶子节点区域的最优取值。最终得到当前轮的强学习器。
从上面可以看出,我们要求解这个问题,需要求解当前决策树最优的所有J个叶子节点区域和每个叶子节点区域的最优解
c
t
j
c_{tj}
ctj。
GBDT采样的方法是分两步走,先求出最优的所有J个叶子节点区域,再求出每个叶子节点区域的最优解
c
t
j
c_{tj}
ctj。
对于XGBoost,它期望把第2步和第3步合并在一起做,即一次求解出决策树最优的所有J个叶子节点区域和每个叶子节点区域的最优解
c
t
j
c_{tj}
ctj。
在讨论如何求解前,我们先看看XGBoost的损失函数的形式。
在GBDT损失函数
L
(
y
,
f
t
−
1
(
x
)
+
h
t
(
x
)
)
L(y,f_{t−1}(x)+h_{t}(x))
L(y,ft−1(x)+ht(x))的基础上,我们加入正则化项如下:
Ω
(
h
t
)
=
γ
J
+
λ
2
∑
j
=
1
J
ω
t
j
2
\Omega(h_{t})=\gamma J+\frac{\lambda}{2}\sum^{J}_{j=1}\omega^{2}_{tj}
Ω(ht)=γJ+2λ∑j=1Jωtj2
这里的
J
J
J是叶子节点的个数,而
ω
t
j
\omega_{tj}
ωtj是第j个叶子节点的最优值。这里的
ω
t
j
\omega_{tj}
ωtj和我们GBDT里使用的
c
t
j
c_{tj}
ctj是一个意思,只是XGBoost的论文里用的是
ω
\omega
ω表示叶子区域的值,因此这里和论文保持一致。
XGBoost的损失函数可以表达为:
L
t
=
∑
i
=
1
m
L
(
y
i
,
f
t
−
1
(
x
i
)
+
h
t
(
x
i
)
)
+
γ
J
+
λ
2
∑
j
=
1
J
ω
t
j
2
L_{t}=\sum^{m}_{i=1}L(y_i,f_{t-1}(x_i)+h_t(x_i))+\gamma J+\frac{\lambda}{2}\sum^{J}_{j=1}\omega^{2}_{tj}
Lt=∑i=1mL(yi,ft−1(xi)+ht(xi))+γJ+2λ∑j=1Jωtj2
最终我们要极小化上面这个损失函数,得到第
t
t
t个决策树最优的所有
J
J
J个叶子节点区域和每个叶子节点区域的最优解
ω
t
j
\omega_{tj}
ωtj。XGBoost没有和GBDT一样去拟合泰勒展开式的一阶导数,而是期望直接基于损失函数的二阶泰勒展开式来求解。现在我们来看看这个损失函数的二阶泰勒展开式:
为了方便,我们把第i个样本在第t个弱学习器的一阶和二阶导数分别记为:
g
t
i
=
∂
L
(
y
i
,
f
t
−
1
(
x
i
)
)
∂
f
t
−
1
(
x
i
)
,
h
t
i
=
∂
2
L
(
y
i
,
f
t
−
1
(
x
i
)
∂
f
t
−
1
2
(
x
i
)
g_{ti}=\frac{\partial{L}(y_i,f_{t-1}(x_i))}{\partial{f_{t-1}}(x_i)},h_{ti}=\frac{\partial^2{L}(y_i,f_{t-1}(x_i)}{\partial{f^2_{t-1}}(x_i)}
gti=∂ft−1(xi)∂L(yi,ft−1(xi)),hti=∂ft−12(xi)∂2L(yi,ft−1(xi)
则我们的损失函数现在可以表达为:
L
t
≈
∑
i
=
1
m
(
L
(
y
i
,
f
t
−
1
(
x
i
)
)
+
g
t
i
h
t
(
x
i
)
+
1
2
h
t
i
h
t
2
(
x
i
)
)
+
γ
J
+
λ
2
∑
j
=
1
J
ω
t
j
2
L_t\approx \sum^{m}_{i=1}\left(L(y_i,f_{t-1}(x_i))+g_{ti}h_t(x_i)+\frac{1}{2}h_{ti}h^2_t(x_i)\right)+\gamma J+\frac{\lambda}{2}\sum^{J}_{j=1}\omega^{2}_{tj}
Lt≈∑i=1m(L(yi,ft−1(xi))+gtiht(xi)+21htiht2(xi))+γJ+2λ∑j=1Jωtj2
损失函数里面
L
(
y
i
,
f
t
−
1
(
x
i
)
)
L(y_i,f_{t-1}(x_i))
L(yi,ft−1(xi))是常数,对最小化无影响,可以去掉,同时由于每个决策树的第j个叶子节点的取值最终会是同一个值
ω
t
j
\omega_{tj}
ωtj,因此我们的损失函数可以继续化简。
我们把每个叶子节点区域样本的一阶和二阶导数的和单独表示如下:
G
t
j
=
∑
x
i
∈
R
t
j
g
t
j
,
H
t
j
=
∑
x
i
∈
R
t
j
h
t
i
G_{tj}=\sum_{x_i\in R_{tj}}g_{tj},\;H_{tj}=\sum_{x_i\in R_{tj}}h_{ti}
Gtj=∑xi∈Rtjgtj,Htj=∑xi∈Rtjhti
最终损失函数的形式可以表示为:
L
t
=
∑
j
=
1
J
[
G
t
j
ω
t
j
+
1
2
(
H
t
j
+
λ
)
ω
t
j
2
]
+
γ
J
L_{t}=\sum^J_{j=1}\left[G_{tj}\omega_{tj}+\frac{1}{2}(H_{tj}+\lambda)\omega^2_{tj}\right]+\gamma J
Lt=∑j=1J[Gtjωtj+21(Htj+λ)ωtj2]+γJ
2.3 优化求解方法
关于如何一次求解出决策树最优的所有 J J J个叶子节点区域和每个叶子节点区域的最优解 ω t j \omega_{tj} ωtj,我们可以把它拆分成2个问题:
- 如果我们已经求出了第t个决策树的 J J J个最优的叶子节点区域,如何求出每个叶子节点区域的最优解 ω t j \omega_{tj} ωtj?
- 对当前决策树做子树分裂决策时,应该如何选择哪个特征和特征值进行分裂,使最终我们的损失函数 L t L_{t} Lt最小?
对于第一个问题,其实是比较简单的,我们直接基于损失函数对
ω
t
j
\omega_{tj}
ωtj求导并令导数为0即可。这样我们得到叶子节点区域的最优解表达式为:
w
t
j
=
−
G
t
j
H
t
j
+
λ
w_{tj}=-\frac{G_{tj}}{H{tj}+\lambda}
wtj=−Htj+λGtj
现在来看XGBoost优化拆分出的第二个问题:如何选择哪个特征和特征值进行分裂,使最终我们的损失函数
L
t
L_{t}
Lt最小?
在GBDT里面,我们是直接拟合的CART回归树,所以树节点分裂使用的是均方误差。XGBoost这里不使用均方误差,而是使用贪心法,即每次分裂都期望最小化我们的损失函数的误差。
注意到在对取最优解的时候,原损失函数对应的表达式为:
L
t
=
−
1
2
∑
j
=
1
J
G
t
j
2
H
t
j
+
λ
+
γ
J
L_{t}=-\frac{1}{2}\sum^J_{j=1}\frac{G^2_{tj}}{H_{tj}+\lambda}+\gamma J
Lt=−21∑j=1JHtj+λGtj2+γJ
如果我们每次做左右子树分裂时,可以最大程度的减少损失函数的损失就最好了。也就是说,假设当前节点左右子树的一阶二阶导数和为
G
L
,
H
L
,
G
R
,
H
L
G_L,H_L,G_R,H_L
GL,HL,GR,HL,则我们期望最大化下式:
−
1
2
(
G
L
+
G
R
)
2
H
L
+
H
R
+
λ
+
γ
J
−
(
−
1
2
G
L
2
H
L
+
λ
−
1
2
G
R
2
H
R
+
λ
+
γ
(
J
+
1
)
)
-\frac{1}{2}\frac{(G_L+G_R)^2}{H_L+H_R+\lambda}+\gamma J-\left(-\frac{1}{2}\frac{G^2_L}{H_L+\lambda}-\frac{1}{2}\frac{G^2_R}{H_R+\lambda}+\gamma(J+1)\right)
−21HL+HR+λ(GL+GR)2+γJ−(−21HL+λGL2−21HR+λGR2+γ(J+1))
整理下上式后,我们期望最大化的是:
m
a
x
(
1
2
G
L
2
H
L
+
λ
+
1
2
G
R
2
H
R
+
λ
−
1
2
(
G
L
+
G
R
)
2
H
L
+
H
R
+
λ
−
γ
)
max \left(\frac{1}{2}\frac{G^2_L}{H_L+\lambda} + \frac{1}{2}\frac{G^2_R}{H_R+\lambda} -\frac{1}{2}\frac{(G_L+G_R)^2}{H_L+H_R+\lambda}-\gamma\right)
max(21HL+λGL2+21HR+λGR2−21HL+HR+λ(GL+GR)2−γ)
也就是说,我们的决策树分裂标准不再使用CART回归树的均方误差,而是上式了。
具体如何分裂呢?举个简单的年龄特征的例子如下,假设我们选择年龄这个 特征的值a作为决策树的分裂标准,则可以得到左子树2个人,右子树三个人,这样可以分别计算出左右子树的一阶和二阶导数和,进而求出最终的上式的值。
然后我们使用其他的不是值a的划分标准,可以得到其他组合的一阶和二阶导数和,进而求出上式的值。最终我们找出可以使上式最大的组合,以它对应的特征值来分裂子树。
至此,我们解决了XGBoost的2个优化子问题的求解方法。
3 工程实现
3.1 列块并行学习
在树生成过程中,最耗时的一个步骤就是在每次寻找最佳分裂点时都需要对特征的值进行排序。而 XGBoost 在训练之前会根据特征对数据进行排序,然后保存到块结构中,并在每个块结构中都采用了稀疏矩阵存储格式(Compressed Sparse Columns Format,CSC)进行存储,后面的训练过程中会重复地使用块结构,可以大大减小计算量。
作者提出通过按特征进行分块并排序,在块里面保存排序后的特征值及对应样本的引用,以便于获取样本的一阶、二阶导数值。具体方式如图:
通过顺序访问排序后的块遍历样本特征的特征值,方便进行切分点的查找。此外分块存储后多个特征之间互不干涉,可以使用多线程同时对不同的特征进行切分点查找,即特征的并行化处理。在对节点进行分裂时需要选择增益最大的特征作为分裂,这时各个特征的增益计算可以同时进行,这也是 XGBoost 能够实现分布式或者多线程计算的原因。
3.2 缓存访问
列块并行学习的设计可以减少节点分裂时的计算量,在顺序访问特征值时,访问的是一块连续的内存空间,但通过特征值持有的索引(样本索引)访问样本获取一阶、二阶导数时,这个访问操作访问的内存空间并不连续,这样可能造成cpu缓存命中率低,影响算法效率。
为了解决缓存命中率低的问题,XGBoost 提出了缓存访问算法:为每个线程分配一个连续的缓存区,将需要的梯度信息存放在缓冲区中,这样就实现了非连续空间到连续空间的转换,提高了算法效率。此外适当调整块大小,也可以有助于缓存优化。
3.3 “核外”块计算
当数据量非常大时,我们不能把所有的数据都加载到内存中。那么就必须将一部分需要加载进内存的数据先存放在硬盘中,当需要时再加载进内存。这样操作具有很明显的瓶颈,即硬盘的IO操作速度远远低于内存的处理速度,肯定会存在大量等待硬盘IO操作的情况。针对这个问题作者提出了“核外”计算的优化方法。具体操作为,将数据集分成多个块存放在硬盘中,使用一个独立的线程专门从硬盘读取数据,加载到内存中,这样算法在内存中处理数据就可以和从硬盘读取数据同时进行。此外,XGBoost 还用了两种方法来降低硬盘读写的开销:
- 块压缩(Block
Compression)。论文使用的是按列进行压缩,读取的时候用另外的线程解压。对于行索引,只保存第一个索引值,然后用16位的整数保存与该block第一个索引的差值。作者通过测试在block设置为个 2 16 2^{16} 216样本大小时,压缩比率几乎达到26%-29%。 - 块分区(Block Sharding )。块分区是将特征block分区存放在不同的硬盘上,以此来增加硬盘IO的吞吐量。
4 模型参数
XGBoost的参数一共分为三类:
- 通用参数:宏观函数控制。
- Booster参数:控制每一步的booster(tree/regression)。booster参数一般可以调控模型的效果和计算代价。我们所说的调参,很这是大程度上都是在调整booster参数。
- 学习目标参数:控制训练目标的表现。我们对于问题的划分主要体现在学习目标参数上。比如我们要做分类还是回归,做二分类还是多分类,这都是目标参数所提供的。
4.1 通用参数
- booster:我们有两种参数选择,gbtree和gblinear。gbtree是采用树的结构来运行数据,而gblinear是基于线性模型。
- silent:静默模式,为1时模型运行不输出。
- nthread: 使用线程数,一般我们设置成-1,使用所有线程。如果有需要,我们设置成多少就是用多少线程。
4.2 Booster参数
- n_estimator: 也作num_boosting_rounds这是生成的最大树的数目,也是最大的迭代次数。
- learning_rate: 有时也叫作eta,系统默认值为0.3,。每一步迭代的步长,很重要。太大了运行准确率不高,太小了运行速度慢。我们一般使用比默认值小一点,0.1左右就很好。
- gamma:系统默认为0,我们也常用0。在节点分裂时,只有分裂后损失函数的值下降了,才会分裂这个节点。gamma指定了节点分裂所需的最小损失函数下降值。这个参数的值越大,算法越保守。因为gamma值越大的时候,损失函数下降更多才可以分裂节点。所以树生成的时候更不容易分裂节点。范围:[0,∞]
- subsample:系统默认为1。这个参数控制对于每棵树,随机采样的比例。减小这个参数的值,算法会更加保守,避免过拟合。但是,如果这个值设置得过小,它可能会导致欠拟合。典型值:0.5-1,0.5代表平均采样,防止过拟合. 范围: (0,1],注意不可取0
- colsample_bytree:系统默认值为1。我们一般设置成0.8左右。用来控制每棵随机采样的列数的占比(每一列是一个特征)。典型值:0.5-1范围: (0,1]
- colsample_bylevel:默认为1,我们也设置为1.这个就相比于前一个更加细致了,它指的是每棵树每次节点分裂的时候列采样的比例
- max_depth:系统默认值为6我们常用3-10之间的数字。这个值为树的最大深度。这个值是用来控制过拟合的。max_depth越大,模型学习的更加具体。设置为0代表没有限制,范围: [0,∞]
- max_delta_step:默认0,我们常用0.这个参数限制了每棵树权重改变的最大步长,如果这个参数的值为0,则意味着没有约束。如果他被赋予了某一个正值,则是这个算法更加保守。通常,这个参数我们不需要设置,但是当个类别的样本极不平衡的时候,这个参数对逻辑回归优化器是很有帮助的。
- lambda:也称reg_lambda,默认值为0。权重的L2正则化项。(和Ridge regression类似)。这个参数是用来控制XGBoost的正则化部分的。这个参数在减少过拟合上很有帮助。
- alpha:也称reg_alpha默认为0,权重的L1正则化项。(和Lasso regression类似)。可以应用在很高维度的情况下,使得算法的速度更快。
- scale_pos_weight:默认为1在各类别样本十分不平衡时,把这个参数设定为一个正值,可以使算法更快收敛。通常可以将其设置为负样本的数目与正样本数目的比值。
4.3 学习目标参数
4.3.1 objective
reg:linear– 线性回归
reg:logistic – 逻辑回归
binary:logistic – 二分类逻辑回归,输出为概率
binary:logitraw – 二分类逻辑回归,输出的结果为wTx
count:poisson – 计数问题的poisson回归,输出结果为poisson分布。在poisson回归中,max_delta_step的缺省值为0.7 (used to safeguard optimization)
multi:softmax – 设置 XGBoost 使用softmax目标函数做多分类,需要设置参数num_class(类别个数)
multi:softprob – 如同softmax,但是输出结果为ndata*nclass的向量,其中的值是每个数据分为每个类的概率。
4.3.2 eval_metric
rmse: 均方根误差
mae: 平均绝对值误差
logloss: negative log-likelihood
error: 二分类错误率。其值通过错误分类数目与全部分类数目比值得到。对于预测,预测值大于0.5被认为是正类,其它归为负类。 error@t: 不同的划分阈值可以通过 ‘t’进行设置
merror: 多分类错误率,计算公式为(wrong cases)/(all cases)
mlogloss: 多分类log损失
auc: 曲线下的面积
ndcg: Normalized Discounted Cumulative Gain
map: 平均正确率
一般来说,我们都会使用xgboost.train(params, dtrain)函数来训练我们的模型。这里的params指的是booster参数。
5 使用方法
先来用 Xgboost 做一个简单的二分类问题,以下面这个数据为例,来判断病人是否会在 5 年内患糖尿病,这个数据前 8 列是变量,最后一列是预测值为 0 或 1。
数据描述:
https://archive.ics.uci.edu/ml/datasets/Pima+Indians+Diabetes
下载数据集,并保存为 “pima-indians-diabetes.csv“ 文件:
https://archive.ics.uci.edu/ml/machine-learning-databases/pima-indians-diabetes/pima-indians-diabetes.data
5.1 基础应用
引入xgboost等包
from numpy import loadtxt
from xgboost import XGBClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
分出变量和标签
dataset = loadtxt('pima-indians-diabetes.csv', delimiter=",")
X = dataset[:,0:8]
Y = dataset[:,8]
将数据分为训练集和测试集,测试集用来预测,训练集用来学习模型
seed = 7
test_size = 0.33
X_train, X_test, y_train, y_test = train_test_split(X, Y, test_size=test_size, random_state=seed)
xgboost 有封装好的分类器和回归器,可以直接用 XGBClassifier 建立模型,这里是 XGBClassifier 的文档:
http://xgboost.readthedocs.io/en/latest/python/python_api.html#module-xgboost.sklearn
model = XGBClassifier()
model.fit(X_train, y_train)
xgboost 的结果是每个样本属于第一类的概率,需要用 round 将其转换为 0 1 值
y_pred = model.predict(X_test)
predictions = [round(value) for value in y_pred]
得到 Accuracy: 77.95%
accuracy = accuracy_score(y_test, predictions)
print("Accuracy: %.2f%%" % (accuracy * 100.0))
5.2 监控模型表现
xgboost可以在模型训练时,评价模型在测试集上的表现,也可以输出每一步的分数,只需要将
model = XGBClassifier()
model.fit(X_train, y_train)
变为:
model = XGBClassifier()
eval_set = [(X_test, y_test)]
model.fit(X_train, y_train, early_stopping_rounds=10, eval_metric="logloss", eval_set=eval_set, verbose=True)
那么它会在每加入一颗树后打印出 logloss
[31] validation_0-logloss:0.487867
[32] validation_0-logloss:0.487297
[33] validation_0-logloss:0.487562
并打印出 Early Stopping 的点:
Stopping. Best iteration:
[32] validation_0-logloss:0.487297
5.3 输出特征重要度
gradient boosting还有一个优点是可以给出训练好的模型的特征重要性,
这样就可以知道哪些变量需要被保留,哪些可以舍弃。
需要引入下面两个类:
from xgboost import plot_importance
from matplotlib import pyplot
和前面的代码相比,就是在 fit 后面加入两行画出特征的重要性
model.fit(X, y)
plot_importance(model)
pyplot.show()
5.4 调参
如何调参呢,下面是三个超参数的一般实践最佳值,可以先将它们设定为这个范围,然后画出 learning curves,再调解参数找到最佳模型:
learning_rate = 0.1 或更小,越小就需要多加入弱学习器;
tree_depth = 2~8;
subsample = 训练集的 30%~80%;
接下来我们用 GridSearchCV 来进行调参会更方便一些:
可以调的超参数组合有:
树的个数和大小 (n_estimators and max_depth).
学习率和树的个数 (learning_rate and n_estimators).
行列的 subsampling rates (subsample, colsample_bytree and colsample_bylevel).
下面以学习率为例:
先引入这两个类
from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import StratifiedKFold
设定要调节的 learning_rate = [0.0001, 0.001, 0.01, 0.1, 0.2, 0.3],和原代码相比就是在 model 后面加上 grid search 这几行:
model = XGBClassifier()
learning_rate = [0.0001, 0.001, 0.01, 0.1, 0.2, 0.3]
param_grid = dict(learning_rate=learning_rate)
kfold = StratifiedKFold(n_splits=10, shuffle=True, random_state=7)
grid_search = GridSearchCV(model, param_grid, scoring="neg_log_loss", n_jobs=-1, cv=kfold)
grid_result = grid_search.fit(X, Y)
最后会给出最佳的学习率为 0.1
Best: -0.483013 using {‘learning_rate’: 0.1}
print("Best: %f using %s" % (grid_result.best_score_, grid_result.best_params_))
我们还可以用下面的代码打印出每一个学习率对应的分数:
means = grid_result.cv_results_['mean_test_score']
stds = grid_result.cv_results_['std_test_score']
params = grid_result.cv_results_['params']
for mean, stdev, param in zip(means, stds, params):
print("%f (%f) with: %r" % (mean, stdev, param))
-0.689650 (0.000242) with: {'learning_rate': 0.0001}
-0.661274 (0.001954) with: {'learning_rate': 0.001}
-0.530747 (0.022961) with: {'learning_rate': 0.01}
-0.483013 (0.060755) with: {'learning_rate': 0.1}
-0.515440 (0.068974) with: {'learning_rate': 0.2}
-0.557315 (0.081738) with: {'learning_rate': 0.3}
6 与其他算法的对比
6.1 从GBDT到XGBoost
作为GBDT的高效实现,XGBoost是一个上限特别高的算法,因此在算法竞赛中比较受欢迎。简单来说,对比原算法GBDT,XGBoost主要从下面三个方面做了优化:
一是算法本身的优化:在算法的弱学习器模型选择上,对比GBDT只支持决策树,还可以直接很多其他的弱学习器。在算法的损失函数上,除了本身的损失,还加上了正则化部分。在算法的优化方式上,GBDT的损失函数只对误差部分做负梯度(一阶泰勒)展开,而XGBoost损失函数对误差部分做二阶泰勒展开,更加准确。算法本身的优化是我们后面讨论的重点。
二是算法运行效率的优化:对每个弱学习器,比如决策树建立的过程做并行选择,找到合适的子树分裂特征和特征值。在并行选择之前,先对所有的特征的值进行排序分组,方便前面说的并行选择。对分组的特征,选择合适的分组大小,使用CPU缓存进行读取加速。将各个分组保存到多个硬盘以提高IO速度。
三是算法健壮性的优化:对于缺失值的特征,通过枚举所有缺失值在当前节点是进入左子树还是右子树来决定缺失值的处理方式。算法本身加入了L1和L2正则化项,可以防止过拟合,泛化能力更强。
6.2 从XGBoost到LightGBM
LightGBMLight Gradient Boosting Machine)是一种梯度提升框架,它使用决策树作为基学习器。LightGBM 为高效并行计算而生,它的 Light 体现在以下几个点上:
- 更快的训练速度
- 更低的内存使用
- 支持单机多线程,多机并行计算,以及 GPU 训练
- 能够处理大规模数据
LightGBM 可以说是在 XGBoost 上做的优化,二者的主要区别如下:
- 树的切分策略不同:XGBoost是level-wise而LightGBM是leaf-wise
- 实现并行的方式不同:XGBoost是通过预排序的方式,LightGBM则是通过直方图算法
- LightGBM直接支持类别特征,对类别特征不必进行独热编码处理
7 优缺点
7.1 优点
精度更高: GBDT 只用到一阶泰勒展开,而 XGBoost 对损失函数进行了二阶泰勒展开。XGBoost 引入二阶导一方面是为了增加精度,另一方面也是为了能够自定义损失函数,二阶泰勒展开可以近似大量损失函数;
灵活性更强: GBDT 以 CART 作为基分类器,XGBoost 不仅支持 CART 还支持线性分类器,使用线性分类器的 XGBoost 相当于带和正则化项的逻辑斯蒂回归(分类问题)或者线性回归(回归问题)。此外,XGBoost 工具支持自定义损失函数,只需函数支持一阶和二阶求导;
正则化: XGBoost 在目标函数中加入了正则项,用于控制模型的复杂度。正则项里包含了树的叶子节点个数、叶子节点权重的 范式。正则项降低了模型的方差,使学习出来的模型更加简单,有助于防止过拟合,这也是XGBoost优于传统GBDT的一个特性。
Shrinkage(缩减): 相当于学习速率。XGBoost 在进行完一次迭代后,会将叶子节点的权重乘上该系数,主要是为了削弱每棵树的影响,让后面有更大的学习空间。传统GBDT的实现也有学习速率;
列抽样: XGBoost 借鉴了随机森林的做法,支持列抽样,不仅能降低过拟合,还能减少计算。这也是XGBoost异于传统GBDT的一个特性;
缺失值处理: 对于特征的值有缺失的样本,XGBoost 采用的稀疏感知算法可以自动学习出它的分裂方向;
XGBoost工具支持并行: boosting不是一种串行的结构吗?怎么并行的?注意XGBoost的并行不是tree粒度的并行,XGBoost也是一次迭代完才能进行下一次迭代的(第 次迭代的代价函数里包含了前面 次迭代的预测值)。XGBoost的并行是在特征粒度上的。我们知道,决策树的学习最耗时的一个步骤就是对特征的值进行排序(因为要确定最佳分割点),XGBoost在训练之前,预先对数据进行了排序,然后保存为block结构,后面的迭代中重复地使用这个结构,大大减小计算量。这个block结构也使得并行成为了可能,在进行节点的分裂时,需要计算每个特征的增益,最终选增益最大的那个特征去做分裂,那么各个特征的增益计算就可以开多线程进行。
可并行的近似算法: 树节点在进行分裂时,我们需要计算每个特征的每个分割点对应的增益,即用贪心法枚举所有可能的分割点。当数据无法一次载入内存或者在分布式情况下,贪心算法效率就会变得很低,所以XGBoost还提出了一种可并行的近似算法,用于高效地生成候选的分割点。
7.2 缺点
虽然利用预排序和近似算法可以降低寻找最佳分裂点的计算量,但在节点分裂过程中仍需要遍历数据集;
预排序过程的空间复杂度过高,不仅需要存储特征值,还需要存储特征对应样本的梯度统计值的索引,相当于消耗了两倍的内存。
8 参考链接:
XGBoost:A Scalable Tree Boosting System Tianqi Chen
XGBoost算法原理小结 刘建平Pinard
终于有人说清楚了–XGBoost算法 mantch
XGBoost论文详解 hobbit
深入理解XGBoost,优缺点分析,原理推导及工程实现 Datawhale
XGBoost 重要参数(调参使用) Timcode
神器xgboost简单入门和运用 leofionn
XGBoost、LightGBM的详细对比介绍 Infaraway
LightGBM 详细讲解 北方一瓶川