作为GBDT的高效实现,XGBoost是一个上限特别高的算法,因此在算法竞赛中比较受欢迎。简单来说,对比原算法GBDT,XGBoost主要从下面三个 方面做了优化: 一是算法本身的优化: 在算法的弱学习器模型选择上,对比GBDT只支持决策树,还可以直接很多其他的弱学习器。在算法的损失函数上,除了本身的 损失,还加上了正则化部分。在算法的优化方式上,GBDT的损失函数只对误差部分做负梯度 (一阶泰勒) 展开,而XGBoost损失函数对误差部分做二阶泰勒展 开,更加准确。算法本身的优化是我们后面讨论的重点。 二是算法运行效率的优化:对每个弱学习器,比如决策树建立的过程做并行选择,找到合适的子树分裂特征和特征值。在并行选择之前,先对所有的特 征的值进行排序分组,方便前面说的并行选择。对分组的特征,选择合适的分组大小,使用CPU缓存进行读取加速。将各个分组保存到多个硬盘以提高IO速度。 三是算法健壮性的优化: 对于缺失值的特征,通过枚举所有缺失值在当前节点是进入左子树还是右子树来决定缺失值的处理方式。算法本身加入了L1和 L2正则化项,可以防止过拟合,泛化能力更强。 在上面三方面的优化中,第一部分算法本身的优化是重点也是难点。现在我们就来看看算法本身的优化内容。
在看XGBoost本身的优化内容前,我们先回顾下GBDT的回归算法迭代的流程,详细算法流程见梯度提升树(GBDT)原理小结第三节,对于GBDT的第t 颗决策树,主要是走下面4步:
2)利用 $\left(x_i, r_{t i}\right) \quad(i=1,2, \ldots m)$ ,拟合一颗CART回归树,得到第 $\mathrm{t}$ 颗回归树,其对应的叶子节点区域为 $R_{t j}, j=1,2, \ldots, J$ 。其中J为回归树的叶 子节点的个数。
3) 对叶子区域 $j=1,2, . . J$, 计算最佳拟合值 $$ c_{t j}=\underbrace{\arg \min }_c \sum_{x_i \in R_{t j}} L\left(y_i, f_{t-1}\left(x_i\right)+c\right) $$
最终我们要极小化上面这个损失函数,得到第 $\mathrm{t}$ 个决策树最优的所有 $]$ 个叶子节点区域和每个叶子节点区域的最优解 $w_{t j}$ 。 XGBoost没有和GBDT一样去 拟合泰勒展开式的一阶导数,而是期望直接基于损失函数的二阶泰勒展开式来求解。现在我们来看看这个损失函数的二阶泰勒展开式: $$ \begin{aligned} L_t &=\sum_{i=1}^m L\left(y_i, f_{t-1}\left(x_i\right)+h_t\left(x_i\right)\right)+\gamma J+\frac{\lambda}{2} \sum_{j=1}^J w_{t j}^2 \\ & \approx \sum_{i=1}^m\left(L\left(y_i, f_{t-1}\left(x_i\right)\right)+\frac{\partial L\left(y_i, f_{t-1}\left(x_i\right)\right.}{\partial f_{t-1}\left(x_i\right)} h_t\left(x_i\right)+\frac{1}{2} \frac{\partial^2 L\left(y_i, f_{t-1}\left(x_i\right)\right.}{\partial f_{t-1}^2\left(x_i\right)} h_t^2\left(x_i\right)\right)+\gamma J+\frac{\lambda}{2} \sum_{j=1}^J w_{t j}^2 \end{aligned} $$ 为了方便,我们把第 $\mathrm{i}$ 个样本在第 $\mathrm{t}$ 个弱学习器的一阶和二阶导数分别记为 $$ g_{t i}=\frac{\partial L\left(y_i, f_{t-1}\left(x_i\right)\right.}{\partial f_{t-1}\left(x_i\right)}, h_{t i}=\frac{\partial^2 L\left(y_i, f_{t-1}\left(x_i\right)\right.}{\partial f_{t-1}^2\left(x_i\right)} $$ 则我们的损失函数现在可以表达为: $$ L_t \approx \sum_{i=1}^m\left(L\left(y_i, f_{t-1}\left(x_i\right)\right)+g_{t i} h_t\left(x_i\right)+\frac{1}{2} h_{t i} h_t^2\left(x_i\right)\right)+\gamma J+\frac{\lambda}{2} \sum_{j=1}^J w_{t j}^2 $$ 损失函数里面 $L\left(y_i, f_{t-1}\left(x_i\right)\right)$ 是常数,对最小化无影响,可以去掉,同时由于每个决策树的第 $\mathrm{j}$ 个叶子节点的取值最终会是同一个值 $w_{t j}$, 因此我们的损失函数可以继续化简。
$$ \begin{aligned} L_t &\left.\approx \sum_{i=1}^m g_{t i} h_t\left(x_i\right)+\frac{1}{2} h_{t i} h_t^2\left(x_i\right)\right)+\gamma J+\frac{\lambda}{2} \sum_{j=1}^J w_{t j}^2 \\ &=\sum_{j=1}^J\left(\sum_{x_i \in R_{t j}} g_{t i} w_{t j}+\frac{1}{2} \sum_{x_i \in R_{t j}} h_{t i} w_{t j}^2\right)+\gamma J+\frac{\lambda}{2} \sum_{j=1}^J w_{t j}^2 \\ &=\sum_{j=1}^J\left[\left(\sum_{x_i \in R_{t j}} g_{t i}\right) w_{t j}+\frac{1}{2}\left(\sum_{x_i \in R_{t j}} h_{t i}+\lambda\right) w_{t j}^2\right]+\gamma J \end{aligned} $$我们把每个叶子节点区域样本的一阶和二阶导数的和单独表示如下: $$ G_{t j}=\sum_{x_i \in R_{t j}} g_{t i}, H_{t j}=\sum_{x_i \in R_{t j}} h_{t i} $$ 最终损失函数的形式可以表示为: $$ L_t=\sum_{j=1}^J\left[G_{t j} w_{t j}+\frac{1}{2}\left(H_{t j}+\lambda\right) w_{t j}^2\right]+\gamma J $$ 现在我们得到了最终的损失函数,那么回到前面讲到的问题,我们如何一次求解出决策树最优的所有]个叶子节点区域和每个叶子节点区域的最优解 $w_{t j}$ 呢?
关于如何一次求解出决策树最优的所有 $J$ 个叶子节点区域和每个叶子节点区域的最优解 $w_{t j}$ ,我们可以把它拆分成2个问题:
对于第一个问题,其实是比较简单的,我们直接基于损失函数对 $w_{t j}$ 求导并令导数为 0 即可。这样我们得到叶子节点区域的最优解 $w_{t j}$ 表达式为: $$ w_{t j}=-\frac{G_{t j}}{H_{t j}+\lambda} $$ 这个叶子节点的表达式不是XGBoost首创,实际上在GBDT的分类算法里,已经在使用了。节点区域值的近似解表达式为: $$ c_{t j}=\sum_{x_i \in R_{t j}} r_{t i} / \sum_{x_i \in R_{t j}}\left|r_{t i}\right|\left(1-\left|r_{t i}\right|\right) $$ 它其实就是使用了上式来计算最终的 $c_{t j \text { 。 }}$ 注意到二元分类的损失函数是: $$ L(y, f(x))=\log (1+\exp (-y f(x))) $$ 其每个样本的一阶导数为: $$ g_i=-r_i=-y_i /\left(1+\exp \left(y_i f\left(x_i\right)\right)\right) $$ 其每个样本的二阶导数为: $$ h_i=\frac{\exp \left(y_i f\left(x_i\right)\right.}{\left(1+\exp \left(y_i f\left(x_i\right)\right)^2\right.}=\left|g_i\right|\left(1-\left|g_i\right|\right) $$ 由于没有正则化项,则 $c_{t j}=-\frac{g_i}{h i}$ , 即可得到GBDT二分类叶子节点区域的近似值。 现在我们回到XGBoost,我们已经解决了第一个问题。现在来看XGBoost优化拆分出的第二个问题: 如何选择哪个特征和特征值进行分裂,使最终我们 的损失函数 $L_t$ 最小? 在GBDT里面,我们是直接拟合的CART回归树,所以树节点分裂使用的是均方误差。XGBoost这里不使用均方误差,而是使用贪心法,即每次分裂都期望最小化我们的损失函数的误差。 注意到在我们 $w_{t j}$ 取最优解的时候,原损失函数对应的表达式为: $$ L_t=-\frac{1}{2} \sum_{j=1}^J \frac{G_{t j}^2}{H_{t j}+\lambda}+\gamma J $$ 如果我们每次做左右子树分裂时,可以最大程度的减少损失函数的损失就最好了。也就是说,假设当前节点左右子树的一阶二阶导数和为 $G_L, H_L, G_R, H_L$ ,则我们期望最大化下式: $$ -\frac{1}{2} \frac{\left(G_L+G_R\right)^2}{H_L+H_R+\lambda}+\gamma J-\left(-\frac{1}{2} \frac{G_L^2}{H_L+\lambda}-\frac{1}{2} \frac{G_R^2}{H_R+\lambda}+\gamma(J+1)\right) $$ 整理下上式后,我们期望最大化的是: $$ \max \frac{1}{2} \frac{G_L^2}{H_L+\lambda}+\frac{1}{2} \frac{G_R^2}{H_R+\lambda}-\frac{1}{2} \frac{\left(G_L+G_R\right)^2}{H_L+H_R+\lambda}-\gamma $$ 也就是说,我们的决策树分裂标准不再使用CART回归树的均方误差,而是上式了。 具体如何分裂呢? 举个简单的年龄特征的例子如下,假设我们选择年龄这个特征的值a作为决策树的分裂标准,则可以得到左子树 2 个人,右子树三个人,这样可以分别计算出左右子树的一阶和二阶导数和,进而求出最终的上式的值。
然后我们使用其他的不是值a的划分标准,可以得到其他组合的一阶和二阶导数和,进而求出上式的值。最终我们找出可以使上式最大的组合,以它对应 的特征值来分裂子树。 至此,我们解决了XGBoost的2个优化子问题的求解方法。
这里我们总结下XGBoost的算法主流程,基于决策树弱分类器。不涉及运行效率的优化和健壮性优化的内容。 输入是训练集样本 $I=\left\{\left(x, y_1\right),\left(x_2, y_2\right), \ldots\left(x_m, y_m\right)\right\}$ ,最大迭代次数 $T$ T, 损失函数 $\mathrm{L}$ ,正则化系数 $\lambda, \gamma$ 。 输出是强学习器 $f(x)$ 对迭代轮数 $\mathrm{t}=1,2, \ldots$ T有: 1) 计算第 1 个样本 $(i-1,2, . . \mathrm{m})$ 在当前轮损失函数 L基于 $f_{t-1}\left(x_i\right)$ 的一阶导数 $g_{t i}$ ,二阶导数 $h_{t i}$, 计算所有样本的一阶导数和 $G_t=\sum_{i=1}^m g_{t i}$ 二阶导数和 $H_t=\sum_{i=1}^m h_{t i}$
2) 基于当前节点尝试分裂决策树,默认分数 score $=0$ , G和H为当前需要分裂的节点的一阶二阶导数之和。 对特征序号 $k=1,2 \ldots K$ : a) $G_L=0, H_L=0$ b.1) 将样本按特征 $\mathrm{k}$ 从小到大排列,依次取出第i个样本,依次计算当前样本放入左子树后,左右子树一阶和二阶导数和: $$ \begin{aligned} G_L &=G_L+g_{t i}, G_R=G-G_L \\ H_L &=H_L+h_{t i}, H_R=H-H_L \end{aligned} $$
b. 2) 尝试更新最大的分数: $$ \text { score }=\max \left(\text { score }, \frac{1}{2} \frac{G_L^2}{H_L+\lambda}+\frac{1}{2} \frac{G_R^2}{H_R+\lambda}-\frac{1}{2} \frac{\left(G_L+G_R\right)^2}{H_L+H_R+\lambda}-\gamma\right) $$
同时,对训练的每个特征排序并且以块的的结构存储在内存中,方便后面迭代重复使用,减少计算量。计算量的减少参见上面第4节的算法流程,首先默 认所有的样本都在右子树,然后从小到大迭代,依次放入左子树,并寻找最优的分裂点。这样做可以减少很多不必要的比较。 具体的过程如下图所示:
此外,通过设置合理的分块的大小,充分利用了CPU缓存进行读取加速 (cache-aware access) 。使得数据读取的速度更快。另外,通过将分块进行 压缩 (block compressoin) 并存储到硬盘上,并且通过将分块分区到多个硬盘上实现了更大的IO。 6.XGBoost算法健壮性的优化 最后我们再来看看XGBoost在算法健壮性的优化,除了上面讲到的正则化项提高算法的泛化能力外,XGBoost还对特征的缺失值做了处理。 XGBoost没有假设缺失值一定进入左子树还是右子树,则是尝试通过枚举所有缺失值在当前节点是进入左子树,还是进入右子树更优来决定一个处理缺 失值默认的方向,这样处理起来更加的灵活和合理。 也就是说,上面第 4 节的算法的步骤a),b.1)和b. 2)会执行 2 次,第一次假设特征 $k$ 所有有缺失值的样本都走左子树,第二次假设特征 $k$ 所有缺失值的样本 都走右子树。然后每次都是针对没有缺失值的特征 $\mathrm{k}$ 的样本走上述流程,而不是所有的的样本。 如果是所有的缺失值走右子树,使用上面第4节的 $a), b .1$ )和 b.2)即可。如果是所有的样本走左子树,则上面第4节的a)步要变成: $$ G_R=0, H_R=0 $$ b.1)步要更新为: $$ \begin{aligned} &G_R=G_R+g_{t i}, G_L=G-G_R \\ &H_R=H_R+h_{t i}, H_L=H-H_R \end{aligned} $$
不考虑深度学习,则XGBoost是算法竞赛中最热门的算法,它将GBDT的优化走向了一个极致。当然,后续微软又出了LightGBM,在内存占用和运行 速度上又做了不少优化,但是从算法本身来说,优化点则并没有XGBoost多。
何时使用XGBoost,何时使用LightGBM呢? 个人建议是优先选择XGBoost,毕竟调优经验比较多一些,可以参考的资料也多一些。如果你使用 XGBoost遇到的内存占用或者运行速度问题,那么尝试LightGBM是个不错的选择。
转载自: