XGBoost
GB的高效实现--XGBoost
xgb paper
XGBoost的全称是eXtreme Gradient Boosting,是GBDT的更高阶的版本实现,因为他或多或少还是存在一些gbdt的影子,其建树的过程的cart树是相似的,gbdt的话用的也是cart树,确切的说是cart回归树,其次拟合每一颗树的时候还是根据残差去做拟合,只是各自内部的方法不一样。那么其实只要去了解xgb和gbdt这些不同的地方,就能知道xgb是怎么一回事了。
xgboost本身还是属于boosting家族,不过跟adaboost还是不太一样的,adaboost的话是通过更新样本权重这样的一种boosting,而像xgb和gbdt的话,是通过残差这种形式。
xgb在目标函数中使用了对损失函数进行二阶泰勒展开来最小化损失,加入了正则来最小化树的结构风险,展开后可以得到在上次训练的那颗树下,每一个样本的一阶导数和二阶导数,这些导数后面会用来计算分裂节点的收益。
那最关心的还是怎么去生成这个树,总体上来说,他生成树的过程是这样的:
结构上其实和cart差不多
- 寻找最佳分裂点的时候,使用了预排序算法, 对所有特征都按照特征的数值进行预排序, 然后遍历所有特征上的所有分裂点位,计算按照这些候选分裂点位分裂后的全部样本的目标函数增益,找到最大的那个增益对应的特征和候选分裂点位,从而进行分裂。这个增益的话,他不像gbdt那也计算平方误差,而是和损失函数挂钩的,确切的说就是计算按这个特征值分裂后,计算分裂前和分裂后的增益。他推导出的一个和2个导数值相关公式可以直接计算这个增益。
- 叶子节点的值,或者说权重其实也是通过这两个梯度,外加一个参数lambda定义下来的。
这样一层一层的完成建树过程, xgboost训练的时候,是通过加法的方式进行训练,也就是每一次通过聚焦残差训练一棵树出来, 最后的预测结果是所有树的加和表示
XGB-损失函数
节点如何分裂
xgb支持2种分裂算法
贪心算法
其步骤如下:
- 从树的深度为0开始:
- 对每个叶节点枚举所有的可用特征;
- 针对每个特征,把属于该节点的训练样本根据该特征值进行升序排列,通过线性扫描的方式来决定该特征的最佳分裂点,并记录该特征的分裂收益;
选择收益最大的特征作为分裂特征,用该特征的最佳分裂点作为分裂位置,在该节点上分裂出左右两个新的叶节点,并为每个新节点关联对应的样本集;- 回到第1步,递归执行直到满足特定条件为止;
如何计算收益最大的特征?
对比GBDT, 其如何选择feature, 本质上是CART如何选择feature. CART选择feature用Gini系数, 其并不和GBDT的损失函数挂钩, 而xgb则与损失函数挂钩, XGB分裂的时候, 同样先计算信息增益, 分裂好以后, 根据下式计算分裂后的损失
近似算法
贪心算法可以得到最优解,但当数据量太大时则无法读入内存进行计算,近似算法主要针对贪心算法这一缺点给出了近似最优解。
对于每个特征,只考察分位点可以减少计算复杂度。
- 该算法首先根据特征分布的分位数提出候选划分点,然后将连续型特征映射到由这些候选点划分的桶中,然后聚合统计信息找到所有区间的最佳分裂点。
在提出候选切分点时有两种策略:
.Global:学习每棵树前就提出候选切分点,并在每次分裂时都采用这种分割;
Local:每次分裂前将重新提出候选切分点。直观上来看,Local策略需要更多的计算步骤,而Global策略因为节点已有划分所以需要更多的候选点。
XGB的优缺点
优点
- 精度更高: GBDT 只用到一阶泰勒展开,而 XGBoost 对损失函数进行了二阶泰勒展开。XGBoost 引入二阶导一方面是为了增加精度,另一方面也是为了能够自定义损失函数,二阶泰勒展开可以近似大量损失函数;
- GBDT 以 CART 作为基分类器,XGBoost 不仅支持 CART 还支持线性分类器,使用线性分类器的 XGBoost 相当于带 \(L1\)和 \(L2\)正则化项的逻辑斯蒂回归(分类问题)或者线性回归(回归问题)。此外,XGBoost 工具支持自定义损失函数,只需函数支持一阶和二阶求导;
- 正则化: XGBoost 在目标函数中加入了正则项,用于控制模型的复杂度。正则项里包含了树的叶子节点个数、叶子节点权重的\(L2\)范式。正则项降低了模型的方差,使学习出来的模型更加简单,有助于防止过拟合,这也是XGBoost优于传统GBDT的一个特性。
- 相当于学习速率。XGBoost 在进行完一次迭代后,会将叶子节点的权重乘上该系数,主要是为了削弱每棵树的影响,让后面有更大的学习空间。传统GBDT的实现也有学习速率;
- XGBoost 借鉴了随机森林的做法,支持列抽样,不仅能降低过拟合,还能减少计算。这也是XGBoost异于传统GBDT的一个特性
- 缺失值处理: 对于特征的值有缺失的样本,XGBoost 采用的稀疏感知算法可以自动学习出它的分裂方向;
缺点
虽然利用预排序和近似算法可以降低寻找最佳分裂点的计算量,但在节点分裂过程中仍需要遍历数据集;
预排序过程的空间复杂度过高,不仅需要存储特征值,还需要存储特征对应样本的梯度统计值的索引,相当于消耗了两倍的内存。
XGB工程实现
列块并行学习
在树生成过程中,最耗时的一个步骤就是在每次寻找最佳分裂点时都需要对特征的值进行排序。而 XGBoost 在训练之前会根据特征对数据进行排序,然后保存到块结构中,并在每个块结构中都采用了稀疏矩阵存储格式(Compressed Sparse Columns Format,CSC)进行存储,后面的训练过程中会重复地使用块结构,可以大大减小计算量。
作者提出通过按特征进行分块并排序,在块里面保存排序后的特征值及对应样本的引用,以便于获取样本的一阶、二阶导数值。具体方式如图:
通过顺序访问排序后的块遍历样本特征的特征值,方便进行切分点的查找。此外分块存储后多个特征之间互不干涉,可以使用多线程同时对不同的特征进行切分点查找,即特征的并行化处理。在对节点进行分裂时需要选择增益最大的特征作为分裂,这时各个特征的增益计算可以同时进行,这也是 XGBoost 能够实现分布式或者多线程计算的原因。
缓存访问
列块并行学习的设计可以减少节点分裂时的计算量,在顺序访问特征值时,访问的是一块连续的内存空间,但通过特征值持有的索引(样本索引)访问样本获取一阶、二阶导数时,这个访问操作访问的内存空间并不连续,这样可能造成cpu缓存命中率低,影响算法效率。
为了解决缓存命中率低的问题,XGBoost 提出了缓存访问算法:为每个线程分配一个连续的缓存区,将需要的梯度信息存放在缓冲区中,这样就实现了非连续空间到连续空间的转换,提高了算法效率。此外适当调整块大小,也可以有助于缓存优化。
核外计算
当数据量非常大时,我们不能把所有的数据都加载到内存中。那么就必须将一部分需要加载进内存的数据先存放在硬盘中,当需要时再加载进内存。这样操作具有很明显的瓶颈,即硬盘的IO操作速度远远低于内存的处理速度,肯定会存在大量等待硬盘IO操作的情况。针对这个问题作者提出了“核外”计算的优化方法。具体操作为,将数据集分成多个块存放在硬盘中,使用一个独立的线程专门从硬盘读取数据,加载到内存中,这样算法在内存中处理数据就可以和从硬盘读取数据同时进行。此外,XGBoost 还用了两种方法来降低硬盘读写的开销:
块压缩(Block Compression)。论文使用的是按列进行压缩,读取的时候用另外的线程解压。对于行索引,只保存第一个索引值,然后用16位的整数保存与该block第一个索引的差值。作者通过测试在block设置为 个样本大小时,压缩比率几乎达到 。
块分区(Block Sharding )。块分区是将特征block分区存放在不同的硬盘上,以此来增加硬盘IO的吞吐量。
xgb一些需要注意的点
xgb如何处理缺失值?
在某列特征上寻找分裂节点时,不会对缺失的样本进行遍历,只会对非缺失样本上的特征值进行遍历,这样减少了为稀疏离散特征寻找分裂节点的时间开销。
另外,为了保证完备性,对于含有缺失值的样本,会分别把它分配到左叶子节点和右叶子节点,然后再选择分裂后增益最大的那个方向,作为预测时特征值缺失样本的默认分支方向。
如果训练集中没有缺失值,但是测试集中有,那么默认将缺失值划分到右叶子节点方向
XGB中树停止生长的条件
- 当树达到最大深度时,停止建树,因为树的深度太深容易出现过拟合,这里需要设置一个超参数max_depth。
- 当新引入的一次分裂所带来的增益Gain<0时,放弃当前的分裂。这是训练损失和模型结构复杂度的博弈过程。
- 当引入一次分裂后,重新计算新生成的左、右两个叶子结点的样本权重和。如果任一个叶子结点的样本权重低于某一个阈值,也会放弃此次分裂。这涉及到一个超参数:最小样本权重和,是指如果一个叶子节点包含的样本数量太少也会放弃分裂,防止树分的太细。
XGB如何处理类别特征?
可以参看这副图,既有离散值,又有连续值。离散值如果one-hot的话,那么分裂点非0即1
数据常常是多种类别混在一起的,比如离散的,连续的的特征,还有一些未被编码的类别特征
下面是一篇总结的比较好的文章
XGB如何处理类别(离散)特征
XGB如何评价特征重要性
XGBoost中有三个参数可以用于评估特征重要性:
- weight :该特征在所有树中被用作分割样本的总次数。
- gain :该特征在其出现过的所有树中产生的平均增益。
- cover :该特征在其出现过的所有树中的平均覆盖范围。覆盖范围这里指的是一个特征用作分割点后,其影响的样本数量,即有多少样本经过该特征分割到两个子节点。
XGB如何进行特征剪枝
- 在目标函数中增加了正则项:使用叶子结点的数目和叶子结点权重的L2模的平方,控制树的复杂度。
- 在结点分裂时,定义了一个阈值,如果分裂后目标函数的增益小于该阈值,则不分裂。
- 当引入一次分裂后,重新计算新生成的左、右两个叶子结点的样本权重和。如果任一个叶子结点的样本权重低于某一个阈值(最小样本权重和),也会放弃此次分裂。
- XGBoost 先从顶到底建立树直到最大深度,再从底到顶反向检查是否有不满足分裂条件的结点,进行剪枝。
XGB如何选择分裂点
XGB分裂点的分裂方式和CART很不一样,主要是我们要在每个分裂点上算出一 二阶导数来计算分裂的增益。XGBoost在训练前预先将特征按照特征值进行了排序,并存储为block结构,以后在结点分裂时可以重复使用该结构。
因此,可以采用特征并行的方法利用多个线程分别计算每个特征的最佳分割点,根据每次分裂后产生的增益,最终选择增益最大的那个特征的特征值作为最佳分裂点。如果在计算每个特征的最佳分割点时,对每个样本都进行遍历,计算复杂度会很大,这种全局扫描的方法并不适用大数据的场景。XGBoost还提供了一种直方图近似算法,对特征排序后仅选择常数个候选分裂位置作为候选分裂点,极大提升了结点分裂时的计算效率。
XGB与GBDT有何不同?
既然是GBDT的更高阶实现,那么他和gbdt有很多的相似之处:
相同点
- 他们都有基于CART树的实现
- 他们都是前向加法模型
- 他们都拟合的是残差
不同点
1.对于基学习器CART树,XGB显示的添加了正则项用于控制树的复杂度,能有效的避免过拟合
2.节点分裂方式不同,GBDT本质上遵循的还是CART本身的节点分裂规则,而XGB会与它的损失函数挂钩,具体的是与一阶和二阶的残差挂钩
3. GBDT求损失梯度的时候只是使用了一阶导,二XGB则使用了二阶导,可以更加逼近损失的真实值
4. GBDT训练的时候,建每一棵树都是用的全部的数据,而xgb则和rf一样支持列采样
5. XGB支持缺失值,虽然理论上GBDT也是支持的,毕竟这个东西在C4.5里面就已经支持,更为优秀的CART也是支持的。但是像常用的scikit-learn里源码的实现其实是不支持的。
其次有一个地方要小心的是spark版本中,XGB的缺失值处理在java/scala和python版本中是不一样的,scala中是0都是缺失值,而py版本中NaN才是。
6. XGB支持特征并行,XGBoost预先将每个特征按特征值排好序,存储为块结构,分裂结点时可以采用多线程并行查找每个特征的最佳分割点,极大提升训练速度。
7. XGB存在有early stoping机制,当我们的metric在val set上经历了我们设置的step数量后没有得到提升,XGB就会停止训练避免过拟合