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
因此,现在要做的是两件事:
对于给定的树结构 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 γ,优化这个目标相当于进行树的剪枝。当引入的分裂带来的增益小于一个阀值的时候,不进行分裂操作。
一个树结构分数计算例子如下图:
因此,每次分裂,枚举所有可能的分裂方案,就和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。这个仍可以继续优化(之后再讲)。
精确地枚举所有可能的划分,十分耗时;当数据量大的时候,几乎不可能将数据全部加载进内存;精确划分在分布式中也会有问题。因此文章提出了近似的策略。
简单的说,就是根据特征 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进行累加,最后通过遍历所有的候选分裂点来找到最佳分裂点。方法过程如下图:
给定了候选切分点后,一个例子为:
那么,现在有两个问题:
近似方法分为全局和局部两种。**全局策略(Global)**是在建立第 k k k 棵树时(初始化阶段)利用样本的梯度对样本进行离散化,每一维的特征都建立buckets。在建树过程中,重复利用这些buckets进行分裂判断。**局部策略(Local)**是在每次进行分裂时,都重新计算每个样本的梯度并重新构建buckets,再进行分裂判断。局部选择的复杂度更高,实验中效果更好。
桶的个数等于 1 / eps, 可以看出:
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加权。
除了正则项以外,还有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的思想,每次只抽取部分样本进行训练,不使用全部样本。行采样增加了不同样本集合的差异性,从而不同基学习器之间的差异性也增大,避免过拟合。另一种是列采样(特征采样),相当于做随机特征筛选,进入模型的特征个数越少(即模型变量越少),模型越简单,根据机器学习理论(方差偏差理论),模型越简单,模型泛化性越好。
列采样的实现方式有两种,一种是按层随机(一般效果好一点),另一种是建树前就随机选择特征。按层随机:上文提到每次分裂一个节点时,我们都要遍历所有的特征和分割点,从而确定最优分割点。如果加入列采样,在对同一层的每个节点分裂之前,先随机选择一部分特征,于是只需要遍历这部分特征,来确定最优的分割点。建树前随机选择特征:在建树前就随机选择一部分特征,之后所有叶子结点的分裂都使用这部分特征。
在真实世界中,我们的特征往往是稀疏的,可能的原因有:
XGBoost能对缺失值自动进行处理,其思想是对于缺失值自动学习出它该被划分的方向(左子树or右子树):
注意,上述的算法只遍历非缺失值。划分的方向怎么学呢?很naive但是很有效的方法:
这样最后求出最大增益的同时,也知道了缺失值的样本应该往左边还是往右边。使用了该方法,相当于比传统方法多遍历了一次,但是它只在非缺失值的样本上进行迭代,因此其复杂度与非缺失值的样本成线性关系。在Allstate-10k数据集上,比传统方法快了50倍:
在建树的过程中,最耗时是找最优的切分点,而这个过程中,最耗时的部分是将数据排序。为了减少排序的时间,提出Block结构存储数据。
Block中的数据以稀疏格式CSC进行存储
Block中的特征进行排序(不对缺失值排序)
Block 中特征还需存储指向样本的索引,这样才能根据特征的值来取梯度。
一个Block中存储一个或多个特征的值
可以看出,只需在建树前排序一次,后面节点分裂时可以直接根据索引得到梯度信息。
Block结构还有其它好处,数据按列存储,可以同时访问所有的列,很容易实现并行的寻找分裂点算法。此外也可以方便实现之后要讲的out-of score计算。
缺点是空间消耗大了一倍。
当数据集大的时候使用近似算法
在特征分裂时,根据特征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的paper在KKD上发表,名为:《Xgboost: A scalable tree boosting system》,那么scalable体现在哪?
参考知乎上王浩的回答,修改如下:
目标函数的正则项, 叶子节点数+叶子节点数输出分数的平方和。相当于预剪枝。
Ω ( 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