10 Xgboost 简介及其模型形式
XGBOOST 是 GBDT 的一种,也是加法模型和前向优化算法。在监督学习中,可以分为:模型,参数,目标函数和学习方法
-
模型:给定输入 x 后预测输出 y 的方法,比如说回归,分类,排序等。
-
参数:模型中的参数,比如线性回归中的权重和偏置
-
目标函数:即损失函数,包含正则化项
-
学习方法:给定目标函数后求解模型和参数的方法,比如梯度下降法,数学推导等。
这四方面的内容也指导着 XGBOOST 系统的设计。
模型形式
假设要判断一个人是否喜欢电脑游戏,输入为年龄,性别,职业等特征。可以得到如下的回归树
在叶子节点上会有一个分数,利用这个分数我们可以回归,或映射成概率进行分类等
一棵 CART 树的拟合能力有限,我们可以进行集成学习,比如用两个树进行预测,结果是两个树的和:
给定数据集:
D
=
(
X
i
,
y
i
)
(
∣
D
∣
=
n
,
x
i
∈
R
m
,
y
i
∈
R
)
D=(X_i,y_i)(|D|=n,x_i\in R^m,y_i\in R)
D=(Xi,yi)(∣D∣=n,xi∈Rm,yi∈R)
XGBOOST 利用前向分布算法,学习到包含 K 棵树的加法模型
y
^
i
=
∑
t
=
1
K
f
t
(
x
i
)
,
f
t
∈
F
\hat y_i=\sum_{t=1}^Kf_t(x_i),f_t\in \mathcal{F}
y^i=t=1∑Kft(xi),ft∈F
其中有 K 棵树,$\mathcal{f}$
是回归树,而$\mathcal{F}$
对应回归树组成的函数空间。那怎么得到这些树,也就是树的结构和叶子节点的预测结果
11 XGBOOSTE 的目标函数
定义目标函数,包含正则项
O
b
j
(
Θ
)
=
∑
i
−
1
N
l
(
y
i
,
y
^
i
)
+
∑
j
=
1
t
Ω
(
f
j
)
,
f
j
∈
F
Obj(\Theta)=\sum_{i-1}^Nl(y_i,\hat y_i)+\sum_{j=1}^t\Omega (f_j),f_j\in \mathcal{F}
Obj(Θ)=i−1∑Nl(yi,y^i)+j=1∑tΩ(fj),fj∈F
如何优化这个目标函数呢?因为 f 是决策树,而不是数值型的向量,我们不能使用梯度下降的算法进行优化
Xgboost 是前向分布算法,我们通过贪心算法寻找局部最优解
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}^tf_j(x_i)=\hat y_i^{(t-1)}+f_t(x_i)
y^i(t)=j=1∑tfj(xi)=y^i(t−1)+ft(xi)
每一次迭代我们寻找使损失函数降低最大的 f (CART 树),因此目标函数可改写为
O
b
j
(
t
)
=
∑
i
=
1
N
l
(
y
i
,
y
^
i
(
t
)
)
+
∑
j
=
1
t
Ω
(
f
j
)
=
∑
i
=
1
N
l
(
y
i
,
y
^
i
(
t
−
1
)
)
+
f
t
(
x
i
)
+
Ω
(
f
t
)
+
c
o
n
s
t
a
n
t
≈
∑
i
=
1
N
l
(
y
i
,
y
^
i
(
t
−
1
)
)
+
f
t
(
x
i
)
+
Ω
(
f
t
)
\begin{aligned} Obj^{(t)}&=\sum_{i=1}^Nl(y_i,\hat y_i^{(t)})+\sum_{j=1}^t\Omega(f_j)\\ &=\sum_{i=1}^Nl(y_i,\hat y_i^{(t-1)})+f_t(x_i)+\Omega(f_t)+constant\\ &\approx \sum_{i=1}^Nl(y_i,\hat y_i^{(t-1)})+f_t(x_i)+\Omega(f_t) \end{aligned}
Obj(t)=i=1∑Nl(yi,y^i(t))+j=1∑tΩ(fj)=i=1∑Nl(yi,y^i(t−1))+ft(xi)+Ω(ft)+constant≈i=1∑Nl(yi,y^i(t−1))+ft(xi)+Ω(ft)
接下来,我们采用泰勒展开对目标参数进行近似
O b j ( t ) = ∑ i = 1 N l ( y i , y ^ i ( t − 1 ) + f t ( x i ) ) + Ω ( f t ) = ∑ t = 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 ) \begin{aligned} Obj^{(t)}&=\sum_{i=1}^Nl(y_i,\hat y_i^{(t-1)}+f_t(x_i))+\Omega(f_t)\\ &=\sum_{t=1}^N(l(y_i,\hat y_i^{(t-1)})+g_if_t(x_i)+\frac{1}{2}h_if_t^2(x_i))+\Omega(f_t) \end{aligned} Obj(t)=i=1∑Nl(yi,y^i(t−1)+ft(xi))+Ω(ft)=t=1∑N(l(yi,y^i(t−1))+gift(xi)+21hift2(xi))+Ω(ft)
g i = ∂ l ( y i , y ^ i ( t − 1 ) ) ∂ y ^ i ( t − 1 ) , h i = ∂ 2 l ( y i , y ^ i ( t − 1 ) ) ∂ 2 y ^ i ( t − 1 ) g_i=\frac{\partial l(y_i,\hat y_i^{(t-1)})}{\partial \hat y_i^{(t-1)}},h_i=\frac{\partial ^2l(y_i,\hat y_i^{(t-1)})}{\partial ^2\hat y_i^{(t-1)}} gi=∂y^i(t−1)∂l(yi,y^i(t−1)),hi=∂2y^i(t−1)∂2l(yi,y^i(t−1))
移除对第 t 轮迭代来说的常数项$l(y_i,\hat y_i^{(t-1)})$
得到
O
b
j
(
t
)
=
∑
i
=
1
N
(
g
i
f
t
(
x
i
)
+
1
2
h
i
f
t
2
(
x
i
)
)
+
Ω
(
f
t
)
Obj^{(t)}=\sum_{i=1}^N(g_if_t(x_i)+\frac{1}{2}h_if_t^2(x_i))+\Omega(f_t)
Obj(t)=i=1∑N(gift(xi)+21hift2(xi))+Ω(ft)
所以目标函数只依赖于每条数据在误差函数上的一阶导数和二阶导数
目标函数(正则项)
树的复杂度可以用如树的深度,内部节点个数,叶节点个数等来衡量
Xgboostl 中正则项用来衡量树的复杂度:树的叶子节点个数 T 和每棵树的叶子节点输出分数 W 的平方和(相当于 L2 正则化)
Ω
(
f
t
)
=
γ
T
+
1
2
γ
∑
j
=
1
T
w
j
2
\Omega(f_t)=\gamma T+\frac{1}{2}\gamma\sum_{j=1}^Tw_j^2
Ω(ft)=γT+21γj=1∑Twj2
Ω
=
γ
3
+
1
3
λ
(
4
+
0.01
+
1
)
\Omega=\gamma3+\frac{1}{3}\lambda(4+0.01+1)
Ω=γ3+31λ(4+0.01+1)
XGBOOST 的目标函数改写为
O
b
j
(
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
Obj^{(t)}=\sum_{i=1}^N(g_if_t(x_i)+\frac{1}{2}h_if_t^2(x_i))+\lambda T+\frac{1}{2}\lambda\sum_{j=1}^Tw_j^2
Obj(t)=i=1∑N(gift(xi)+21hift2(xi))+λT+21λj=1∑Twj2
上式中第一部分是对样本的累加,而后面的部分是正则项,是对叶节点的累加。
定义 q 函数将输入ⅹ映射到某个叶子节点上,则有
f
t
(
x
)
=
w
q
(
x
)
,
w
∈
R
T
,
q
:
R
d
→
{
1
,
2
,
.
.
.
,
T
}
f_t(x)=w_{q(x)},w\in R^T,q:R^d \to \{1,2,...,T\}
ft(x)=wq(x),w∈RT,q:Rd→{1,2,...,T}
定义每个叶子节点 j 上的样本集合为$I_j=\{i|q(x_i)=j\}$
,则目标函数可以改写为:
O
b
j
(
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
=
∑
i
=
1
N
(
g
i
w
q
(
x
i
)
+
1
2
h
i
w
q
(
x
i
)
2
)
+
λ
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
w
j
2
=
∑
j
=
1
T
(
G
i
w
j
+
1
2
(
H
i
+
λ
)
w
j
2
)
+
λ
T
\begin{aligned} Obj^{(t)}&=\sum_{i=1}^N(g_if_t(x_i)+\frac{1}{2}h_if_t^2(x_i))+\lambda T+\frac{1}{2}\lambda\sum_{j=1}^Tw_j^2\\ &=\sum_{i=1}^N(g_iw_{q(x_i)}+\frac{1}{2}h_iw_{q(x_i)}^2)+\lambda T+\frac{1}{2}\lambda\sum_{j=1}^Tw_j^2\\ &=\sum_{j=1}^T(\sum_{i\in I_j}g_iw_j+\frac{1}{2}\sum_{I\in I_j}h_iw_j^2)+\lambda T+\frac{1}{2}\lambda\sum_{j=1}^Tw_j^2\\ &=\sum_{j=1}^T(G_iw_j+\frac{1}{2}(H_i+\lambda)w_j^2)+\lambda T \end{aligned}
Obj(t)=i=1∑N(gift(xi)+21hift2(xi))+λT+21λj=1∑Twj2=i=1∑N(giwq(xi)+21hiwq(xi)2)+λT+21λj=1∑Twj2=j=1∑T(i∈Ij∑giwj+21I∈Ij∑hiwj2)+λT+21λj=1∑Twj2=j=1∑T(Giwj+21(Hi+λ)wj2)+λT
这就是目标函数最终的结果,其
G
j
=
∑
i
∈
I
j
g
i
,
H
j
=
∑
i
∈
I
j
h
i
G_j=\sum_{i\in I_j}g_i,H_j=\sum_{i\in I_j}h_i
Gj=∑i∈Ijgi,Hj=∑i∈Ijhi
XGBOOST(优化目标函数)
接下来我们进行目标函数的优化,即计算第 t 轮时使目标函数最小的叶节点的输出分数 w,直接对 w 求导,使得导数为 0, 得:
w
j
=
−
G
j
H
j
+
λ
w_j=-\frac{G_j}{H_j+\lambda}
wj=−Hj+λGj
将其带入损失函数中
O
b
j
(
t
)
=
∑
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} Obj^{(t)}&=\sum_{j=1}^T(G_jw_j+\frac{1}{2}(H_j+\lambda)w_j^2)+\lambda T\\ &=\sum_{j=1}^T(-\frac{G_j^2}{H_j+\lambda}+\frac{1}{2}\frac{G_j^2}{H_j+\lambda})+\lambda T\\ &=-\frac{1}{2}\sum_{j=1}^T(\frac{G_j^2}{H_j+\lambda})+\lambda T \end{aligned}
Obj(t)=j=1∑T(Gjwj+21(Hj+λ)wj2)+λT=j=1∑T(−Hj+λGj2+21Hj+λGj2)+λT=−21j=1∑T(Hj+λGj2)+λT
上式越小越好,即目标函数越小。
计算目标函数的例子
12 XGBOOST 的学习策略
采用贪心算法,每次尝试分裂一个叶节点,计算分裂后的增益,选择增益最大的。类似于在 ID3 中的信息增益,和 CART 树中的基尼指数,那 XGBOOST 中怎么计算增益呢?损失函数是:
O
b
j
(
t
)
=
−
1
2
∑
j
=
1
T
(
G
j
2
H
j
+
λ
)
+
λ
T
Obj^{(t)}=-\frac{1}{2}\sum_{j=1}^T(\frac{G_j^2}{H_j+\lambda})+\lambda T
Obj(t)=−21j=1∑T(Hj+λGj2)+λT
其中红色部分衡量了叶子节点对总体损失的贡献,目标函数越小越好,则红色部分就越大越好,在 XGBOOST 中增益计算方法是
G
a
i
n
=
1
2
[
G
j
2
H
L
+
λ
⏟
L
−
T
r
e
e
+
G
j
2
H
R
+
λ
⏟
L
−
T
r
e
e
−
G
j
2
H
L
+
H
R
+
λ
⏟
F
−
T
r
e
e
]
−
λ
Gain=\frac{1}{2}[\underbrace{\frac{G_j^2}{H_L+\lambda}}_{L-Tree}+\underbrace{\frac{G_j^2}{H_R+\lambda}}_{L-Tree}-\underbrace{\frac{G_j^2}{H_L+H_R+\lambda}}_{F-Tree}]-\lambda
Gain=21[L−Tree
HL+λGj2+L−Tree
HR+λGj2−F−Tree
HL+HR+λGj2]−λ
Gain 值越大,说明分裂后能使目标函数减小的越多,也就是越好
树结构的确定-精确贪心算法
就像 CART 树一样,枚举所有的特征和特征值,计算树的分裂方式
论文-XGBoost: A Scalable Tree Boosting System
假设枚举年龄特征 xj,考虑划分点 a,计算枚举 yj <a 和 a≤xj 的导数和
对于一个特征,对特征取值排完序后,枚举所有的分裂点 a,只要从左到右扫描就可以枚举出所有分割的梯度 GL 和 GR,计算增益。假设树的高度为 H,特征数 d,则复杂度为 O ( H d n log n O(Hdn\log n O(Hdnlogn其中,排序为 O ( n log n ) O(n\log n) O(nlogn),每个特征都要排序乘以 d,每一层都要这样一遍,所以乘以高度 H
树结构的确定一近似算法
当数据量庞大,无法全部存入内存中时,精确算法很慢,因此引入近似算法。根据特征 k 的分布确定个候选切分点 S k = { s k 1 , s k 2 , . . . , s k l } S_k=\{s_{k1},s_{k2},...,s_{kl}\} Sk={sk1,sk2,...,skl}然后根据候选切分点把相应的样本放入对应的桶中,对每个桶的 G H 进行累加,在候选切分点集合上进行精确贪心査找。算法描述如下
根据分位数给出对应的候选切分点,简单的例子如下所示
何时选取切分点?全局策略(Global)和局部策略(Local)
全局策略:学习每棵树前,提出候选的切分点,当切分点数足够多时,和精确的贪心算法性能相当
局部策略:树节点分裂时,重新提出候选切分点,切分点个数不需要那么多,性能与精确贪心算法相差不多
如何选取切分点?XGBOOST 并没有采用简单的分位数方法,而是提出了以二阶梯度 h 为权重的分位数算法(Weighted Quantile Sketch)。
对特征 k 构造 muti-set 的数据集:
D
k
=
(
x
1
k
,
h
1
)
,
(
x
2
k
,
h
2
)
,
.
.
.
,
(
x
n
k
,
h
n
)
D_k=(x_{1k,h_1}),(x_{2k,h_2}),...,(x_{nk,h_n})
Dk=(x1k,h1),(x2k,h2),...,(xnk,hn)
其中,
x
i
k
x_{ik}
xik 表示样本 i 的特征 k 的取值,而 hi 是对应的二阶梯度。定义一个 rank function,表示第 k 个特征小于 z 的样本比例
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 D_k}h}\sum_{(x,h)\in D_k,x<z}h
rk(z)=∑(x,h)∈Dkh1(x,h)∈Dk,x<z∑h
切分点
S
k
l
,
S
k
2
,
.
.
.
,
S
k
n
{S_{kl}, S_{k2},...,S_{kn}}
Skl,Sk2,...,Skn应满足
∣
r
k
(
S
k
j
)
−
r
k
(
S
k
,
j
+
1
∣
<
ϵ
,
s
k
1
=
min
x
i
k
,
s
k
l
=
max
x
i
k
|r_k(S_{kj})-r_k(S_{k,j+1}|<\epsilon ,s_{k1}=\min x_{ik},s_{kl}=\max x_{ik}
∣rk(Skj)−rk(Sk,j+1∣<ϵ,sk1=minxik,skl=maxxik
也就是说相邻的两个候选切分点相差不超过某个值
ϵ
\epsilon
ϵ。
Features | 1 | 1 | 3 | 4 | 5 | 12 | 45 | 50 | 99 |
---|---|---|---|---|---|---|---|---|---|
Hi | 0.1 | 0.1 | 0.1 | 0.1 | 0.1 | 0.1 | 0.4 | 0.2 | 0.6 |
定义 ϵ \epsilon ϵ为 0.6, 总和为 1.8, 那就至少分为 3 bins,因此第 1 个在 0.6处。第 2 个在1.2 处
为什么要用二阶梯度分桶?将目标函数的泰勒展开推导如下
∑
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(g_if_t(x_i)+\frac{1}{2}h_if_t^2(x_i))+\Omega(f_t)\\ &=\sum_{i=1}^N\frac{1}{2}h_i(2\frac{g_i}{h_i}f_t(x_i)+f_t^2(x_i))+\Omega(f_t)\\ &\sim\sum_{i=1}^N\frac{1}{2}h_i(\frac{g_i^2}{h_i^2}+2\frac{g_i}{h_i}f_t(x_i)+f_t^2(x_i))+\Omega(f_t)\\ &=\sum_{i=1}^N\frac{1}{2}h_i(f_t(x_i)-(-\frac{g_i}{h_i}))^2+\Omega(f_t) \end{aligned}
i=1∑N(gift(xi)+21hift2(xi))+Ω(ft)=i=1∑N21hi(2higift(xi)+ft2(xi))+Ω(ft)∼i=1∑N21hi(hi2gi2+2higift(xi)+ft2(xi))+Ω(ft)=i=1∑N21hi(ft(xi)−(−higi))2+Ω(ft)
树结构的确定一稀疏值处理
稀疏值产生的原因:数据缺失值,大量的零值,One-hot 编码
Xgboosti 能对缺失值自动进行处理思想是对于缺失值自动学习出它该划分的方向,流程如右图所示
- 将特征 k 的缺失值都放在右子树枚举划分点,计算最大的 gain
- 将特征 k 的缺失值都放在左子树,枚举划分点,计算最大的 gain
最后求出最大増益,确定缺失值的划分
树结构的确定-步长
在 Xgboostl(加法运算中) 中也加入了步长η,也叫做收缩率 shrinkage
y
^
i
t
=
y
^
i
(
t
−
1
)
+
η
f
t
(
x
i
)
\hat y_i^t=\hat y_i^{(t-1)}+\eta f_t(x_i)
y^it=y^i(t−1)+ηft(xi)
这有助于防止过拟合,步长
η
\eta
η通常取为 0.1
树结构的确定-列采样
列抽样技术:一种是按层随机,另一种是按树随机(构建树前就随机选择特征)。
按层随机方式,在每次分裂一个结点的时候,对同一层内的每个结点分裂之前,先随机选择一部分特征,这时候只需要遍历一部分特征,来确定最后分割点
按树随机方式,即构建树结构前就随机选择特征,之后所有叶子结点的分裂都只使用这部分特征
13 Xgboost的系统设计
系统设计:分块并行
在建树的过程中,最耗时是找最优的切分点,而这个过程中,最耗时的部分是将数据排序。为了减少排序的时间,提出 Block 结构存储数据
-
Block中的数据以稀疏格式 CSC 进行存储
-
Block 中的特征进行排序(不对缺失值排序)
-
Block中特征还需存储指向样本的索引,这样才能根据特征的值来取梯度
-
一个 Block 中存储一个或多个特征的值
只需在建树前排序一次,后面节点分裂时可以直接根据索引得到梯度信息。
在精确算法中,将整个数据集存放在一个Block 中。这样,复杂度从原来的 O ( H d ∣ ∣ x ∣ ∣ 0 log n ) O(Hd||x||_0\log n) O(Hd∣∣x∣∣0logn)降为 O ( H d ∣ ∣ x ∣ ∣ 0 + ∣ ∣ x ∣ ∣ 0 log n ) O(Hd||x||_0+||x||_0\log n) O(Hd∣∣x∣∣0+∣∣x∣∣0logn),其中 ∣ ∣ x ∣ ∣ 0 ||x||_0 ∣∣x∣∣0为训练集中非缺失值的个数。这样,就省去了每一步中的排序开销。
在近似算法中,使用多个 Block,每个 Bloc k 对应原来数据的子集。不同的 Block 可以在不同的机器上计算。该方法对 Local策略尤其有效,因为 Local策略每次分支都重新生成候选切分点。
系统设计:缓存优化
使用 Block 结构的缺点是取梯度的时候,是通过索引来获取的,而这些梯度的获取顺序是按照特征的大小顺序的。这将导致非连续的内存访问,可能使得 CPU cache 缓存命中率低,从而影响算法效率
对于精确算法中,使用缓存预取。具体来说,对每个线程分配个连续的 buffer,读取梯度信息并存入 Bufferp 中(这样就实现了非连续到连续的转化),然后再统计梯度信息。该方式在训练术本数大的时候特别有用,见下图
在 approximate 算法中,对 Block 的大小进行了合理的设置。定义 Block 的大小为 Block中最多的样本数。设置合适的大小是很重要的,设置过大则容易导致命中率低,过小则容易导致并行化效率不高。经过实验,发现 2 16 2^{16} 216 比较好
系统设计- out of core Computation
当数据量太大不能全部放入主内存的时候,为了使得 out-of-core 计算成为可能,将数据划分为多个 Block 并存放在磁盘上。
-
计算的时候,使用独立的线程预先将 Block 放入主内存,因此可以在计算的同时读取磁盘
-
Block 压缩,貌似采用的是近些年性能出色的 LZ4 压缩算法,按列进行压缩,读取的时候用另外的线程解压。对于行索引只保存第一个索引值,然后用 16 位的整数保存与该 block 第个索引的差值。
-
Block Sharding,将数据划分到不同硬盘上,提高磁盘吞吐率