简介
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(ϕ)=∑il(y^i,yi)+∑kΩ(fk) where Ω(f)=γT+21λ∥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∑tfj(xi)=y^i(t−1)+ft(xi)
对目标函数进行改写,得到第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∑nl(yi,y^i(t−1)+ft(xi))+Ω(ft)
二阶泰勒展开
假设损失函数使用的是平方损失,则上式写为:
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(yi–(y^i(t−1)+ft(xi)))2+Ω(ft)=i=1∑N(残差
yi–y^i(t−1)–ft(xi))2+Ω(ft)
这就是之前我们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+21f′′(x)Δx2
我们得到近似的目标函数:
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
g_i
gi和
h
i
h_i
hi分别为每个数据点在损失函数上的一阶导数和二阶导数,则有:
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
gi=∂y^(t−1)(y^(t−1)−yi)2=2(y^(t−1)−yi)hi=∂y^(t−1)2(yi−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(yi,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(gift(xi)+21hift2(xi))+Ω(ft)
目标函数只依赖每个数据点在误差函数上的一阶导数和二阶导数。
正则项
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
Ω(ft)=γT+21λj=1∑Twj2
举例如下:
完整目标函数
进一步,对XGBoost来说,每一个数据点
x
i
x_i
xi 最终都会落到一个叶子结点上。对于落在同一个叶子节点上的数据点来说,其输出都是一样的。假设我们共有
J
J
J 个叶子结点,每个叶子结点对应的输出为
w
j
w_j
wj (
w
j
w_j
wj 也是我们要求解的最优权重),
I
j
=
{
i
∣
q
(
x
i
)
=
j
}
I_j = \{ i|q(x_i) = j\}
Ij={i∣q(xi)=j} 为落在叶子结点
j
j
j 的实例集合。则目标函数可以进一步改写(已知
y
i
y_i
yi和
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[gift(xi)+21hift2(xi)]+γT+21λj=1∑Twj2=j=1∑T⎣⎡⎝⎛i∈Ij∑gi⎠⎞wj+21⎝⎛i∈Ij∑hi+λ⎠⎞wj2⎦⎤+γT=j=1∑T[Gjwj+21(Hj+λ)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 \quad H_j = \sum_{i \in I_j} h_i
Gj=∑i∈IjgiHj=∑i∈Ijhi
因此,现在要做的是两件事:
- 确定树的结构, 这样,这一轮的目标函数就确定了下来
- 求使得当前这一轮(第t轮)的目标函数最小的叶结点分数w。(Obj代表了当我们指定一个树的结构的时候,我们在目标上面最多减少多少,也称为结构分数,structure score)
对于给定的树结构
q
(
x
)
q(\bf x)
q(x) 可以计算得到
w
j
w_j
wj的最优权重为:
w
j
∗
=
–
G
j
H
j
+
λ
w_{j}^{*}=– \frac{G_j}{H_j+\lambda}
wj∗=–Hj+λGj
将最优权重带回目标函数,目标函数变为:
(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(Gjwj+21(Hj+λ)wj2)+γT=j=1∑T(−Hj+λGj2+21Hj+λGj2)+γT=−21j=1∑T(Hj+λGj2)+γ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}
Gain=21[左子树分数
HL+λGL2+右子树分数
HR+λGR2–分裂前分数
HL+HR+λ(GL+GR)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
Lsplit=21[∑i∈ILhi+λ(∑i∈ILgi)2+∑i∈IRhi+λ(∑i∈IRgi)2−∑i∈Ihi+λ(∑i∈Igi)2]−γ
其中
I
L
I_L
IL和
I
R
I_R
IR 是分裂后属于左右节点的实例集合,
I
=
I
L
∪
I
R
I=I_{L} \cup I_{R}
I=IL∪IR;上式大于0选择分裂。注意分裂不一定会使情况变好,因为有一个引入新叶子的惩罚项
γ
\gamma
γ,优化这个目标相当于进行树的剪枝。当引入的分裂带来的增益小于一个阀值的时候,不进行分裂操作。
一个树结构分数计算例子如下图:
分裂算法
Basic Exact Greedy Algorithm
因此,每次分裂,枚举所有可能的分裂方案,就和CART中回归树进行划分一样,要枚举所有特征和特征的取值。该算法称为Exact Greedy Algorithm,如下图所示:遍历所有特征及分割点,选择增益最多的特征和分割点的组合
假设现在枚举的是年龄特征 x j x_j xj。现在要考虑划分点a,因此要计算枚举 x j < a x_j < a xj<a 和 a ≤ x j a\leq x_j a≤xj的导数和:
可以看出,对于一个特征,对特征取值排完序后,枚举所有的分裂点a,只要从左到右扫描一遍就可以枚举出所有分割的梯度 G L G_L GL和 G R G_R GR,然后用式2-9计算即可。这样假设树的高度为 H H H,特征数 d d d,则复杂度为 O ( H d n l o g n ) O(Hdnlogn) O(Hdnlogn)。 其中,排序为 O ( n l o g n ) O(nlogn) O(nlogn),每个特征都要排序所以乘以 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\} Sk={sk1,sk2,⋯skl},然后根据这些候选切分点把相应的样本放入对应的桶中,对每个桶的 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}\} Sk={sk1,sk2,⋯skl} 呢?
- 什么时候进行候选切分点的选取?
分界点选择时机
近似方法分为全局和局部两种。**全局策略(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\} Sk={sk1,sk2…skl},即怎么限定近似算法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.
Dk={(x1k,h1),(x2k,h2)⋯(xnk,hn) 代表每个训练实例的k-th特征值以及二阶梯度。定义rank函数
r
k
:
R
→
[
0
,
+
∞
)
r_{k} : \mathbb{R} \rightarrow[0,+\infty)
rk: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
rk(z)=∑(x,h)∈Dkh1(x,h)∈Dk,,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\}
Sk={sk1,sk2…skl} 要求:
∣
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}
∣rk(sk,j)−rk(sk,j+1)∣<ϵ,sk1=iminxik,skl=imaxxik
直观来看就是让相邻两个候选分裂点相差不超过某个值
ϵ
\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(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)(3-3)
推导第三行可以加入
g
i
2
h
i
2
\frac{g_{i}^{2}}{h_{i}^{2}}
hi2gi2是因为
g
i
g_i
gi 和
h
i
h_i
hi 是上一轮的损失函数求导,是常量。
从式3-3可以看出,目标函数就像是标签为 − g i / h i −g_i/h_i −gi/hi,权重为 h i h_i hi的平方损失,因此用 h i h_i hi加权。
一些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^it=y^i(t−1)+ηft(xi)(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结构存储数据。
-
Block中的数据以稀疏格式CSC进行存储
-
Block中的特征进行排序(不对缺失值排序)
-
Block 中特征还需存储指向样本的索引,这样才能根据特征的值来取梯度。
-
一个Block中存储一个或多个特征的值
可以看出,只需在建树前排序一次,后面节点分裂时可以直接根据索引得到梯度信息。
- 在Exact greedy算法中,将整个数据集存放在一个Block中。这样,复杂度从原来的 O ( H d ∣ ∣ x ∣ ∣ 0 l o g n ) O(Hd||x||_0logn) O(Hd∣∣x∣∣0logn)降为 O ( H d ∣ ∣ x ∣ ∣ 0 + ∣ ∣ x ∣ ∣ 0 l o g n ) O(Hd||x||_0+||x||_0logn) O(Hd∣∣x∣∣0+∣∣x∣∣0logn),其中 ∣ ∣ 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 Ω(ft)=γT+21λj=1∑Twj2 -
行抽样和列抽样:训练的时候只用一部分样本和一部分特征
-
可以设置树的最大深度
-
η \eta η: 可以叫学习率、步长或者shrinkage
-
Early stopping:使用的模型不一定是最终的ensemble,可以根据测试集的测试情况,选择使用前若干棵树
XGBoost的缺点
- 空间开销大。需要保存数据的特征值。XGBoost采用Block结构,存储指向样本的索引,需要消耗两倍的内存。
- 时间开销大(相对于lightGBM而言)。在寻找最优切分点时,要对每个特征都进行排序,还要对每个特征的每个值都进行了遍历,并计算增益。
- 对Cache不友好(相对于lightGBM而言)。使用Block块预排序后,特征对梯度的访问是按照索引来获取的,是一种随机访问,而不同特征访问顺序也不一样,容易照成命中率低的问题。同时,在每一层长树的时候,需要随机访问一个行索引到叶子索引的数组,并且不同特征访问的顺序也不一样,也会造成较大的Cachemiss。