XGBoost(eXtreme Gradient Boosting) 是梯度提升树算法的一种流行且高效的开源实现。梯度提升(Gradient Boosting)是一种监督式集成学习算法,它尝试将一组较简单、较弱的模型的一系列估计值结合在一起,从而准确地预测目标变量。XGBoost 在机器学习竞赛中表现出色。得益于大量的超参数可进行优化和调整来改进拟合效果,XGBoost能够Robust地处理各种数据类型、关系、分布。这种灵活性使 XGBoost 成为回归、分类 (二进制和多类) 和排名问题的坚定选择。
Objective and Bias Variance Trade-off
监督学习的目标函数通常是:
O
b
j
(
Θ
)
=
L
(
Θ
)
+
Ω
(
Θ
)
Obj(\Theta) = L(\Theta)+\Omega(\Theta)
Obj(Θ)=L(Θ)+Ω(Θ)
其中,训练损失
L
(
Θ
)
L(\Theta)
L(Θ)衡量模型在训练数据上拟合的好坏,
正则项
Ω
(
Θ
)
\Omega(\Theta)
Ω(Θ)衡量模型的复杂性。
损失函数记为:
L
=
∑
i
=
1
n
l
(
y
i
,
y
i
^
)
L=\sum\limits_{i=1}^{n}l(y_i,\hat{y_i})
L=i=1∑nl(yi,yi^)
例如对数损失:
l
(
y
i
,
y
i
^
)
=
−
y
i
l
o
g
(
h
θ
(
x
)
)
−
(
1
−
y
i
)
l
o
g
(
1
−
h
θ
(
x
)
)
,
h
θ
(
x
)
=
1
1
+
e
−
y
^
,
y
^
=
θ
T
x
=
y
i
l
o
g
(
1
+
e
−
y
^
)
+
(
1
−
y
i
)
l
o
g
(
1
+
e
y
^
)
\begin{aligned} l(y_i,\hat{y_i}) &= -y_ilog(h_\theta(x))-(1-y_i)log(1-h_\theta(x)),\, h_\theta(x)=\dfrac{1}{1+e^{-\hat{y}}},\,\hat{y}=\theta^T x \\ &= y_ilog(1+e^{-\hat{y}}) + (1-y_i)log(1+e^{\hat{y}}) \end{aligned}
l(yi,yi^)=−yilog(hθ(x))−(1−yi)log(1−hθ(x)),hθ(x)=1+e−y^1,y^=θTx=yilog(1+e−y^)+(1−yi)log(1+ey^)
正则化项例如,
L1正则时,
Ω
(
ω
)
=
λ
∥
ω
∥
1
\Omega(\omega)=\lambda \|\omega\|_1
Ω(ω)=λ∥ω∥1
L2正则时,
Ω
(
ω
)
=
λ
∥
ω
∥
2
\Omega(\omega)=\lambda \|\omega\|^2
Ω(ω)=λ∥ω∥2
为什么我们要在目标函数中包含这两个组件?
优化训练损失促进模型的预测能力,拟合的好说明更有望于接近数据潜在的分布,偏差较小。
优化正则项促进模型的简化,更简单的模型拥有较小的方差,使预测更稳定(减小泛化误差)。
训练机器学习模型的目的不仅仅是可以描述已有的数据,而且是对未知的新数据也可以做出较好的推测,这种推广到新数据的能力称作泛化(generalization)。我们称在训练集上的误差为训练误差(training error),而在新的数据上的误差的期望称为泛化误差(generalization error)或测试误差(test error)。通常我们用测试集上的数据对模型进行测试,将其结果近似为泛化误差。
欠拟合就是指模型的训练误差过大,过拟合就是指训练误差和测试误差间距过大。
加法训练 迭代提升
xgboost的思想,主要就是在GBDT的基础上,深度优化(训练损失和正则化)这两项。
XGBoost也是需要将多棵树的得分累加,得到最终的预测得分(每一次迭代,都在现有树的基础上,增加一棵树去拟合前面树的预测结果与真实值之间的残差)。
回想GBDT,假设有K棵树构成的预测模型:
y
i
^
=
∑
k
=
1
K
f
k
(
x
i
)
,
f
k
∈
F
\hat{y_i}=\sum_{k=1}^{K}f_k(x_i),\quad f_k\in \mathcal{F}
yi^=∑k=1Kfk(xi),fk∈F,
F
\mathcal{F}
F表示包含所有回归树的函数空间。我们是怎样学习树函数的呢?
就是加法迭代训练。
如何选择每一轮加入哪个树函数
f
t
f_t
ft呢?我们选择使损失函数最小的一个f(x)。
也就是说,定义目标函数然后优化它!
实质是把样本分配到叶子结点会对应一个obj,优化过程就是选取一个个使obj尽可能小的f(x),优化Obj就是优化分裂节点到叶子不同的组合。
总之,提升树我们学的是这些f(x),而不是数值向量的权重。不能直接用SGD之类的方法,只能用加法训练的方式,迭代求解:
设
y
i
^
(
t
)
\hat{y_i}^{(t)}
yi^(t)为第i个实例在第t轮的预测值:
y
i
^
(
t
)
=
y
i
^
(
t
−
1
)
+
f
t
(
x
i
)
\hat{y_i}^{(t)}=\hat{y_i}^{(t-1)}+f_t(x_i)
yi^(t)=yi^(t−1)+ft(xi),那么目标函数就是:
O
b
j
(
t
)
=
∑
i
=
1
n
l
(
y
i
,
y
i
^
(
t
−
1
)
+
f
t
(
x
i
)
)
+
Ω
(
f
t
)
+
c
o
n
s
t
a
n
t
Obj^{(t)}=\sum_{i=1}^{n}l(y_i, \, \hat{y_i}^{(t-1)}+f_t(x_i))+\Omega(f_t)+constant
Obj(t)=∑i=1nl(yi,yi^(t−1)+ft(xi))+Ω(ft)+constant
我们贪婪地添加 f t ( x ) f_t(x) ft(x),根据 O b j ( Θ ) = L ( Θ ) + Ω ( Θ ) Obj(\Theta) = L(\Theta)+\Omega(\Theta) Obj(Θ)=L(Θ)+Ω(Θ) 尽可能地提升模型。
损失函数二阶近似
使用二阶近似可以快速优化目标函数。
根据泰勒公式: f ( x + Δ x ) ≃ f ( x ) + f ′ ( x ) Δ x + 1 2 f ′ ′ ( x ) Δ x 2 f(x+\Delta x)\simeq 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,
目标函数的 ( y i ^ ( t − 1 ) + f t ( x i ) ) \big( \hat{y_i}^{(t-1)} + f_t(x_i) \big) (yi^(t−1)+ft(xi)) 对应泰勒公式的 ( x + Δ x ) (x+\Delta x) (x+Δx),于是
定义:
g
i
=
∂
y
^
(
t
−
1
)
l
(
y
i
,
y
^
(
t
−
1
)
)
g_i=\partial_{\hat{y}^{(t-1)}}l(y_i,\hat{y}^{(t-1)})
gi=∂y^(t−1)l(yi,y^(t−1)) 表示损失函数
l
l
l 对
y
^
(
t
−
1
)
\hat{y}^{(t-1)}
y^(t−1)的一阶导数;
h
i
=
∂
y
^
(
t
−
1
)
2
l
(
y
i
,
y
^
(
t
−
1
)
)
h_i=\partial_{\hat{y}^{(t-1)}}^2 l(y_i,\hat{y}^{(t-1)})
hi=∂y^(t−1)2l(yi,y^(t−1)) 表示损失函数
l
l
l 对
y
^
(
t
−
1
)
\hat{y}^{(t-1)}
y^(t−1)的二阶导数;
损失函数近似为:
O
b
j
(
t
)
≃
∑
i
=
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
)
+
c
o
n
s
t
Obj^{(t)}\simeq \sum_{i=1}^{n}\left[ l(y_i,\hat{y_i}^{(t-1)})+g_if_t(x_i)+\frac{1}{2}h_if_t^2(x_i) \right]+\Omega(f_t)+const
Obj(t)≃∑i=1n[l(yi,yi^(t−1))+gift(xi)+21hift2(xi)]+Ω(ft)+const
以GBDT的平方损失为例: O b j ( t ) = ∑ i = 1 n ( y i − ( y i ^ ( t − 1 ) + f t ( x i ) ) ) 2 + Ω ( f t ) + c o n s t = ∑ i = 1 n [ ( y i − y i ^ ( t − 1 ) ) 2 − 2 ( y i − y i ^ ( t − 1 ) ) f t ( x i ) + f t ( x i ) 2 ] + Ω ( f t ) + c o n s t ≃ ∑ i = 1 n [ 2 ( y i ^ ( t − 1 ) − y i ) + f t ( x i ) 2 ] + Ω ( f t ) + c o n s t \begin{aligned} Obj^{(t)} &= \sum_{i=1}^{n}(y_i-(\hat{y_i}^{(t-1)}+f_t(x_i)))^2 +\Omega(f_t)+const \\ &= \sum_{i=1}^{n}\left[ (y_i -\hat{y_i}^{(t-1)})^2-2(y_i -\hat{y_i}^{(t-1)})f_t(x_i)+f_t(x_i)^2 \right] +\Omega(f_t)+const \\ &\simeq \sum_{i=1}^{n}\left[ 2(\hat{y_i}^{(t-1)}-y_i)+f_t(x_i)^2 \right]+\Omega(f_t)+const \end{aligned} Obj(t)=i=1∑n(yi−(yi^(t−1)+ft(xi)))2+Ω(ft)+const=i=1∑n[(yi−yi^(t−1))2−2(yi−yi^(t−1))ft(xi)+ft(xi)2]+Ω(ft)+const≃i=1∑n[2(yi^(t−1)−yi)+ft(xi)2]+Ω(ft)+const
移除了常数项。其
一阶导数 g i = ∂ y ^ ( t − 1 ) ( y ^ ( t − 1 ) − y i ) 2 = 2 ( y ^ ( t − 1 ) − y i ) g_i=\partial_{\hat{y}^{(t-1)}}(\hat{y}^{(t-1)}-y_i)^2=2(\hat{y}^{(t-1)}-y_i) gi=∂y^(t−1)(y^(t−1)−yi)2=2(y^(t−1)−yi),这通常称为上一轮的残差;
二阶导数 h i = ∂ y ^ ( t − 1 ) 2 ( y ^ ( t − 1 ) − y i ) 2 = 2 h_i=\partial_{\hat{y}^{(t-1)}}^2(\hat{y}^{(t-1)}-y_i)^2=2 hi=∂y^(t−1)2(y^(t−1)−yi)2=2,这里成了常数项。
考虑到第 t 棵回归树是根据前面的t-1棵回归树的残差得来的,计算第t轮的损失时
y
^
(
t
−
1
)
\hat{y}^{(t-1)}
y^(t−1)是已知的,
y
i
y_i
yi是真实值,所以
l
(
y
i
,
y
i
^
(
t
−
1
)
)
l(y_i,\hat{y_i}^{(t-1)})
l(yi,yi^(t−1))是常数项。
移除常数项,从而获得如下所示的在t次迭代时的简化版目标函数:
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_i f_t(x_i) + \frac{1}{2} h_i f_t^{2}(x_i)] + \Omega(f_t)
Obj(t)=∑i=1n[gift(xi)+21hift2(xi)]+Ω(ft)
where
g
i
=
∂
y
^
(
t
−
1
)
l
(
y
i
,
y
^
(
t
−
1
)
)
,
h
i
=
∂
y
^
(
t
−
1
)
2
l
(
y
i
,
y
^
(
t
−
1
)
)
\; g_i=\partial_{\hat{y}^{(t-1)}}l(y_i,\hat{y}^{(t-1)}), \; h_i=\partial_{\hat{y}^{(t-1)}}^2 l(y_i,\hat{y}^{(t-1)})
gi=∂y^(t−1)l(yi,y^(t−1)),hi=∂y^(t−1)2l(yi,y^(t−1))
可见,最终的目标函数只依赖于每个数据点在损失函数上的一阶导数和二阶导数。
正则化目标函数
(1)定义树的复杂度
对于f(x)的定义做一下细化,把树拆分成结构部分q和叶子权重部分w。
树的结构 q 把输入映射到了叶子的索引号上面,而
ω
\omega
ω给定了每个索引号对应的叶子权重。
树的复杂度定义为:
把
I
I
I 定义为每个叶子上面样本的集合
I
j
=
{
i
∣
q
(
x
i
)
=
j
}
I_j=\{ i |q(x_i)=j \}
Ij={i∣q(xi)=j}
又由上面两个式子,改写目标函数:
O
b
j
(
t
)
≃
∑
i
=
1
n
[
g
i
f
t
(
x
i
)
+
1
2
h
i
f
t
2
(
x
i
)
]
+
Ω
(
f
t
)
=
∑
i
=
1
n
[
g
i
ω
q
(
x
i
)
+
1
2
h
i
ω
q
(
x
i
)
2
]
+
γ
T
+
1
2
λ
∑
j
=
1
T
∥
ω
j
∥
2
=
∑
i
=
1
n
[
(
∑
i
∈
I
j
g
i
)
ω
j
+
1
2
(
∑
i
∈
I
j
h
i
+
λ
)
ω
j
2
]
+
γ
T
\begin{aligned} Obj^{(t)} &\simeq \sum_{i=1}^{n}\left [ g_if_t(x_i)+\frac{1}{2}h_i f_t^2(x_i)\right ] + \Omega(f_t) \\ &= \sum_{i=1}^{n}\left [ g_i\omega_{q(x_i)}+\frac{1}{2}h_i \omega_{q(x_i)}^2\right ] + \gamma T + \frac{1}{2}\lambda\sum_{j=1}^{T}\|\omega_j\|^2 \\ &= \sum_{i=1}^{n}\left [ (\sum_{i\in I_j}g_i)\omega_j + \frac{1}{2}(\sum_{i\in I_j}h_i+\lambda)\omega_j^2 \right ] + \gamma T \end{aligned}
Obj(t)≃i=1∑n[gift(xi)+21hift2(xi)]+Ω(ft)=i=1∑n[giωq(xi)+21hiωq(xi)2]+γT+21λj=1∑T∥ωj∥2=i=1∑n⎣⎡(i∈Ij∑gi)ωj+21(i∈Ij∑hi+λ)ωj2⎦⎤+γT
这个目标函数包含了T个独立的二次函数。
(2)计算结构分数
Obj代表了当我们指定一个树的结构的时候,我们在目标上面最多减少多少。我们可以把它叫做结构分数(structure score)。
(3)分裂节点
对于一个叶子节点如何进行分裂,xgboost作者在其原始论文中给出了两种分裂节点的方法:
(1)枚举所有不同树结构的贪心法
(2)近似算法
枚举所有不同树结构的贪心法
每次尝试去对已有的叶子加入一个分割,计算分割后的增益:
对每个结点,枚举所有的特征,
- 对每个连续特征,先对特征的值排序
- 用线性扫描来决定这个特征的最佳分裂点
意思就是比如5个Age有四种划分方法,分别计算每种划分的各梯度G和H得到Gain,取最大的。 - 依次对所有特征使用最佳分裂法
生成一棵树的时间复杂度:
假设树深为K,那么复杂度是
O
(
n
d
K
log
n
)
O(ndK\log n)
O(ndKlogn)。共d个特征,线性扫描K层。对每一层,需要
O
(
n
log
n
)
O(n\log n)
O(nlogn)的复杂度来排序。
这可以被深度优化,比如使用近似计算或者缓存排序结果等。
而且,可以扩展到大规模的数据集。
对于类别型变量:
类别特征必须编码,因为xgboost把特征默认都当成数值型的。
- 对于类别有序的类别型变量,比如age等,当成数值型变量处理可以的。对于非类别有序的类别型变量,依据类别特征维度选择是做one-hot还是先one-hot再embeding。(one-hot会增加内存开销以及训练时间开销)
- 类别型变量在范围较小时(tqchen给出的是[10,100]范围内)推荐使用one-hot编码;
- 对于类别变量范围比较大时,把类别特征转成one-hot coding扔到NN里训练个embedding;
比如对于用户的ID,一个大的数据集里面可能有数亿个用户ID,对于这些ID我们可以都映射到一个64维的空间中。模型训练实际上就是更新这个用户ID,在64维的空间中对应的Embedding向量。这样每个用户ID可能包含的信息,都被包含在这个64维的实数向量中了。(比如对于用户的ID,一个大的数据集里面可能有数亿个用户ID,对于这些ID我们可以都映射到一个64维的空间中。模型训练实际上就是更新这个用户ID,在64维的空间中对应的Embedding向量。这样每个用户ID可能包含的信息,都被包含在这个64维的实数向量中了。)
系统设计
(1)Shrinkage和列的子抽样正则
除了正则化目标函数外,还会使用两种额外的技术来进一步阻止overfitting。
第一种技术是Friedman介绍的Shrinkage。Shrinkage会在每一步tree boosting时,会将新加入的weights通过一个因子η进行缩放。与随机优化中的learning rate相类似,对于用于提升模型的新增树(future trees),shrinkage可以减少每棵单独的树、以及叶子空间(leaves space)的影响。
第二个技术是列特征子抽样(column feature subsampling)。该技术也会在RandomForest中使用,在商业软件TreeNet中的gradient boosting也有实现,但开源包中没实现。列子抽样的使用可以加速并行算法的计算。
(2)SPLIT FINDING ALGORITHMS
Basic Exact Greedy Algorithm
XGBoost每一步选能使分裂后增益最大的分裂点进行分裂。
tree learning的其中一个关键问题是,找到最好划分(best split)。为了达到这个目标,split finding算法会在所有特征(features)上,枚举所有可能的划分(splits)。我们称它为“完全贪婪算法(exact greedy algorithm)”。许多单机版tree-boosting实现中,包括scikit-learn,R’s gbm以及单机版的XGBoost,都支持完全贪婪算法(exact greedy algorithm)。该算法会对连续型特征枚举所有可能的split。为了更高效,该算法必须首先根据特征值对数据进行排序,以有序的方式访问数据来枚举结构得分(structure score)的梯度统计。
当数据量十分庞大,以致于不能全部放入内存时,Exact Greedy 算法就会很慢。 因此XGBoost引入了近似的算法。
首先根据特征分布的百分位数(percentiles of feature distribution),提出候选划分点(candidate splitting points)。接着,该算法将连续型特征映射到由这些候选点划分的**分桶(buckets)**中,对每个桶(区间)内的样本统计值 , 进行累加统计,最后在这些累计的统计量上寻找最佳分裂点。
那么,现在有两个问题:
- 如何选取候选切分点 S k = { s k 1 , s k 2 , . . . , s k l } S_k=\{ s_{k1},s_{k2},...,s_{kl} \} Sk={sk1,sk2,...,skl} 呢?
- 什么时候进行候选切分点的选取?
对问题2,有两种实现方式:
- Global: 学习每棵树前, 提出候选切分点,后续不再改变
- Local: 每次分裂前, 重新提出候选切分点,在每个节点上都具体情况具体分析
当然,后者更好,但带来的复杂度也更大。通常情况下,都会优先选用global的形式。
对于问题1,可以采用分位数,也可以直接构造梯度统计的近似直方图等。
Weighted Quantile Sketch(加权分位数略图)
当一个序列无法全部加载到内存时,常常采用分位数缩略图近似地计算分位点,以近似获取特定的查询。
使用随机映射 (Random projections) 将数据流投射在一个小的存储空间内作为整个数据流的概要,这个小空间存储的概要数据称为略图,可用于近似回答特定的查询。需要保留原序列中的最小值和最大值。
通常一个特征的百分位数可以被用来让候选在数据上进行均匀地分布。创建一个 multi-set:
D
k
=
{
(
x
1
k
,
h
1
)
,
(
x
2
k
,
h
2
)
,
.
.
.
,
(
x
n
k
,
h
n
)
}
D_k=\left\{ \left( x_{1k},h_1 \right),\left( x_{2k},h_2 \right),...,\left( x_{nk},h_n \right) \right\}
Dk={(x1k,h1),(x2k,h2),...,(xnk,hn)},n是样本个数。
D
k
D_k
Dk表示第k个特征 与二阶偏导 H 之间的集合(每个训练实例的第k个特征值以及它的二阶梯度值统计)。
一般来说对于加权分位数缩略图的取值是根据 Rank 值进行的,Rank 计算公式如下:
定义序列函数:
r
k
(
z
)
=
∑
(
x
,
h
)
∈
D
k
,
x
<
z
h
∑
(
x
,
h
)
∈
D
k
h
r_k(z) = \dfrac{\sum_{(x,h) \in D_k, x<z}{h}}{\sum_{(x,h) \in D_k}{h}}
rk(z)=∑(x,h)∈Dkh∑(x,h)∈Dk,x<zh
该rank函数输入为某个特征值z,计算的是该特征所有可取值中小于z的特征值的总权重 占总的所有可取值的总权重和的比例,输出为一个比例值。
于是就使用下面这个不等式寻找候选分割点
{
s
k
1
,
s
k
2
,
.
.
.
,
s
k
l
}
\{ s_{k1},s_{k2},...,s_{kl} \}
{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
|r_k(s_{k,j}) - r_k(s_{k,j+1})| < \epsilon , \ \ s_{k1}=\min_i x_{ik} , s_{kl} = \max_i {x}_{ik}
∣rk(sk,j)−rk(sk,j+1)∣<ϵ, sk1=minixik,skl=maxixik
其中,
ϵ
\epsilon
ϵ是一个近似比例,或者说是扫描步幅。可以理解为在特征 k 的取值范围上,按照步幅,挑选出特征
ϵ
\epsilon
ϵ的取值候选点,组成候选点集。
此时特征 k 的取值中
min
i
x
i
k
,
max
i
x
i
k
\min_i {x}_{ik} , \max_i {x}_{ik}
minixik,maxixik 来自 multi-set
D
k
D_k
Dk,
对于
D
k
D_k
Dk 的数据集有两种定义:
(1) 一开始选好,然后每次树切分都不变,也就是说是在总体样本里选
min
i
x
i
k
,
max
i
x
i
k
\min_i x_{ik} , \max_i x_{ik}
minixik,maxixik,这就是我们之前定义的global proposal;
(2) 是树每次确定好切分点的分割后样本也需要进行分割,
min
i
x
i
k
,
max
i
x
i
k
\min_i x_{ik} , \max_i x_{ik}
minixik,maxixik来自子树的样本集
D
k
D_k
Dk,这就是local proposal。
Sparsity-aware Split Finding
在许多现实问题中,输入x是稀疏的。有多种可能的情况造成稀疏:
数据中的missing values、统计中常见的0条目、特征工程:比如one-hot encoding
当在split时相应的feature缺失时,一个样本可以被归类到缺省方向上
让算法意识到数据中的稀疏模式很重要。为了这么做,我们提出了在每个树节点上增加一个 缺省的方向(default direction),如图4所示。当稀疏矩阵x中的值缺失时,样本实例被归类到缺省方向上。在每个分枝上,缺省方向有两种选择,最优的缺省方向可以从数据中学到。
如算法3所示。关键的改进点是:只访问非缺失的条目 I k I_k Ik 。上述算法会将未出现值(non-presence)当成是一个missing value,学到最好的方向来处理 missing values。
XGBoost能对缺失值自动进行处理,其思想是对于缺失值自动学习出它该被划分的方向(左子树or右子树)
(3) 用于并行学习的Column Block
在建树的过程中,最耗时是找最优的切分点,而这个过程中,最耗时的部分是将数据排序。为了减少排序的时间,提出Block结构存储数据。
- Block中的数据以稀疏格式CSC进行存储
- Block中的特征进行排序(不对缺失值排序)
- Block 中特征还需存储指向样本的索引,这样才能根据特征的值来取梯度。
- 一个Block中存储一个或多个特征的值
只需在建树前排序一次,后面节点分裂时可以直接根据索引得到梯度信息。
优点: - 在Exact greedy算法中,将整个数据集存放在一个Block中。这样,复杂度从原来的
O
(
H
d
∣
∣
x
∣
∣
0
log
n
)
O(H d||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为训练集中非缺失值的个数。这样,Exact greedy算法就省去了每一步中的排序开销。 - 在近似算法中,使用多个Block,每个Block对应原来数据的子集。不同的Block可以在不同的机器上计算。该方法对Local策略尤其有效,因为Local策略每次分支都重新生成候选切分点。
Block结构还有其它好处,数据按列存储,可以同时访问所有的列,很容易实现并行的寻找分裂点算法。此外也可以方便实现out-of score计算。
缺点是空间消耗大了一倍。
(4) Cache-aware Access
使用Block结构的一个缺点是取梯度的时候,是通过索引来获取的,而这些梯度的获取顺序是按照特征的大小顺序的。这将导致非连续的内存访问,可能使得CPU cache缓存命中率低,从而影响算法效率。
因此,对于exact greedy算法中, 使用缓存预取。具体来说,对每个线程分配一个连续的buffer,读取梯度信息并存入Buffer中(这样就实现了非连续到连续的转化),然后再统计梯度信息。该方式在训练样本数大的时候特别有用。
在approximate 算法中,对Block的大小进行了合理的设置。定义Block的大小为Block中最多的样本数。选择一个过小的block size会导致每个thread会小负载(small workload)运行,并引起低效的并行化(inefficient parallelization)。在另一方面,过大的block size会导致cache miss,梯度统计将不能装载到CPU cache中。block size的好的选择会平衡两者。设置合适的大小是很重要的,设置过大则容易导致命中率低,过小则容易导致并行化效率不高。经过实验,发现2^16比较好。
缓存处理能力:对于有大量数据或者说分布式系统来说,我们不可能将所有的数据都放进内存里面。因此我们都需要将其放在外存上或者分布式存储。但是这有一个问题,这样做每次都要从外存上读取数据到内存,这将会是十分耗时的操作。因此我们使用预读取(prefetching)将下一块将要读取的数据预先放进内存里面。其实就是多开一个线程,该线程与训练的线程独立并负责数据读取。
(5) Blocks for Out-of-core Computation
当数据量太大不能全部放入主内存的时候,为了使得out-of-core计算称为可能,将数据划分为多个Block并存放在磁盘上。
- 计算的时候,使用独立的线程预先将Block放入主内存,因此可以在计算的同时读取磁盘
- Block压缩,貌似采用的是近些年性能出色的LZ4 压缩算法,按列进行压缩,读取的时候用另外的线程解压。对于行索引,只保存第一个索引值,然后用16位的整数保存与该block第一个索引的差值。
- Block Sharding, 将数据划分到不同硬盘上,提高磁盘吞吐率
块压缩(Block Compression) 块通过列(column)进行压缩,当加载进主存时可以由一个独立的线程即时解压(decompressed on the fly)。它会使用磁盘读开销来获得一些解压时的计算。我们使用一个通用目的的压缩算法来计算特征值。对于行索引(row index),我们从块的起始索引处开始抽取行索引,使用一个16bit的整数来存储每个偏移(offset)。这需要每个块有216216个训练样本,这证明是一个好的设置。在我们测试的大多数数据集中,我们达到大约26% ~ 29%的压缩率。
块分片(Block Sharding) 第二个技术是,在多个磁盘上以一种可选的方式共享数据。一个pre-fetcher thread被分配到每个磁盘上,取到数据,并装载进一个in-memory buffer中。训练线程(training thread)接着从每个bufer中选择性读取数据。当提供多个磁盘时,这可以帮助增加磁盘读(disk reading)的吞吐量。
XGBoost为什么快
- 当数据集大的时候使用近似算法
- Block与并行
- CPU cache 命中优化
- Block预取、Block压缩、Block Sharding等
注意xgboost的并行不是tree粒度的并行,xgboost也是一次迭代完才能进行下一次迭代的(第t次迭代的代价函数里包含了前面t-1次迭代的预测值)。xgboost的并行是在特征粒度上的。我们知道,决策树的学习最耗时的一个步骤就是对特征的值进行排序(因为要确定最佳分割点),xgboost在训练之前,预先对数据进行了排序,然后保存为block结构,后面的迭代中重复地使用这个结构,大大减小计算量。这个block结构也使得并行成为了可能,在进行节点的分裂时,需要计算每个特征的增益,最终选增益最大的那个特征去做分裂,那么各个特征的增益计算就可以开多线程进行。
XGBoost 防止过拟合的方法
- 目标函数的正则项, 叶子节点数+叶子节点数输出分数的平方和
- 行抽样和列抽样:训练的时候只用一部分样本和一部分特征
- 可以设置树的最大深度
- shrinkage 中的学习率、步长 η
- Early stopping:使用的模型不一定是最终的ensemble,可以根据测试集的测试情况,选择使用前若干棵树
Ref:
陈天奇的xgboost PPT
通俗理解大杀器xgboost
XGBoost-赵大宝
通俗易懂-xgboost算法