推荐博文
https://blog.csdn.net/sb19931201/article/details/52557382
https://www.jianshu.com/p/7467e616f227
xgb讲起来还有点复杂,刚开始看算法的时候也是一愣一愣的。白话讲一讲吧。
先确定一个概念,xgboost是什么?就是一堆二叉树,准确来讲是CART树,和GBDT一样,在GBDT中,无论是分类还是回归,也都是一堆CART树。当然xgboost还支持其它的基分类器。
直接上与GBDT的一些比较吧,从比较中来分析,为什么XGB能这么牛叉。
XGB的改进
- 传统GBDT以CART作为基分类器,xgboost还支持线性分类器,这个时候xgboost相当于带L1和L2正则化项的逻辑斯蒂回归(分类问题)或者线性回归(回归问题)。 —可以通过booster [default=gbtree]设置参数:gbtree: tree-based models/gblinear: linear models
- xgboost在代价函数里加入了正则项,用于控制模型的复杂度。正则项里包含了树的叶子节点个数、每个叶子节点上输出的score的L2模的平方和。从Bias-variance tradeoff角度来讲,正则项降低了模型variance,使学习出来的模型更加简单,防止过拟合,这也是xgboost优于传统GBDT的一个特性
—正则化包括了两个部分,都是为了防止过拟合,剪枝是都有的,叶子结点输出L2平滑是新增的。下面的式子就是正则化的式子。加号左边为叶子节点树的复杂度惩罚,右边是L2正则化。 - 传统GBDT在优化时只用到一阶导数信息,xgboost则对代价函数进行了二阶泰勒展开,同时用到了一阶和二阶导数。顺便提一下,xgboost工具支持自定义代价函数,只要函数可一阶和二阶求导。 —对损失函数做了改进(泰勒展开,一阶信息g和二阶信息h),下图就具体展示了目标函数。
去掉常数项之后就长这个样子。这个损失函数就是用来建树的,可以理解成cart树的MSE建树过程。后面会做一些变形。
- Shrinkage(缩减),相当于学习速率(xgboost中的eta)。xgboost在进行完一次迭代后,会将叶子节点的权重乘上该系数,主要是为了削弱每棵树的影响,让后面有更大的学习空间。实际应用中,一般把eta设置得小一点,然后迭代次数设置得大一点。(补充:传统GBDT的实现也有学习速率)
- column subsampling列(特征)抽样,说是从随机森林那边学习来的,防止过拟合的效果比传统的行抽样还好(行抽样功能也有),并且有利于后面提到的并行化处理算法。
- xgboost工具支持并行。boosting不是一种串行的结构吗?怎么并行的?注意xgboost的并行不是tree粒度的并行,xgboost也是一次迭代完才能进行下一次迭代的(第t次迭代的代价函数里包含了前面t-1次迭代的预测值)。xgboost的并行是在特征粒度上的。我们知道,决策树的学习最耗时的一个步骤就是对特征的值进行排序(因为要确定最佳分割点),xgboost在训练之前,预先对数据进行了排序,然后保存为block结构,后面的迭代中重复地使用这个结构,大大减小计算量。这个block结构也使得并行成为了可能,在进行节点的分裂时,需要计算每个特征的增益,最终选增益最大的那个特征去做分裂,那么各个特征的增益计算就可以开多线程进行。(其实就是在特征分类的时候,传统的做法是遍历每个特征再遍历每个特征的所有分裂点,然后去寻找一个损失最小的特征的分裂点。这些特征与特征间的选择是独立的,所以给了实现并行计算的可能性。同时在分裂点的选取的时候还需要对特征进行排序,这个也是独立的,所以也给了实现并行计算的可能。)
- 对缺失值的处理。对于特征的值有缺失的样本,xgboost可以自动学习出它的分裂方向。 —稀疏感知算法,论文3.4节,Algorithm 3: Sparsity-aware Split Finding
- split finding algorithms(划分点查找算法):
(1)exact greedy algorithm—贪心算法获取最优切分点
(2)approximate algorithm— 近似算法,提出了候选分割点概念,先通过直方图算法获得候选分割点的分布情况,然后根据候选分割点将连续的特征信息映射到不同的buckets中,并统计汇总信息。详细见论文3.3节
(3)Weighted Quantile Sketch—分布式加权直方图算法,论文3.4节
这里的算法(2)、(3)是为了解决数据无法一次载入内存或者在分布式情况下算法(1)效率低的问题,以下引用的还是wepon大神的总结:
可并行的近似直方图算法。树节点在进行分裂时,我们需要计算每个特征的每个分割点对应的增益,即用贪心法枚举所有可能的分割点。当数据无法一次载入内存或者在分布式情况下,贪心算法效率就会变得很低,所以xgboost还提出了一种可并行的近似直方图算法,用于高效地生成候选的分割点。
好了,讲完了优缺点,大概可以知道了XGBoost改进了哪些地方,接下来就来介绍一下具体的算法流程。
争取讲的简单一点。
监督学习中总会有目标函数和模型。上图中就是我们最终想要得到的模型和优化模型所用的目标函数。这里可能什么都看不懂,后面会慢慢解释。
xbg也是一个加法模型,通GBDT一样,下一个模型是之前所有模型的累加。但有一点区别,在GBDT中第二步是计算残差,但在xgb中新模型的输出就是实际的预测值了,我们不计算残差而是直接用预测值与损失函数去得到下一时刻的决策树。
是t时刻的预测,我们希望能够越接近真实值越好,而由加法公式可以知道,是t-1时刻的预测值,我们已经知道了。可想而知,t时刻的任务就是拟合使得结果尽可能好,这就是上图中第二个公式所表达的意思。第三个公式只是把式子展开来之后,把与当前时刻t无关的变量通通扔到了const中去。
这一步对应回GBDT中就是计算负梯度(MSE损失函数的化就是残差),不过这边用了二阶导数,海森矩阵。这也是xgb性能更强大的原因之一。(废话,计算复杂度高了,精度不提高要它干嘛T_T)
树分裂的打分函数是什么,就是cart分类树中的基尼系数增益或者回归树中的mse
xgb也允许我们自定义损失函数,只要它是一阶二阶可导的
这样我们就得到了新的目标函数,白话一点也就是t时刻的损失函数。去掉常数项时因为是t-1时刻的损失,所以在t时刻是已知的,当作常数项就去掉了。
后面一串英文讲的是,为什么我们要话费如此大的精力去得到目标函数,而不是直接去生成树。论文从两方面来讲,一方面从理论层面上讲,我们是做什么,让模型达到最优解,最优解怎么达到,降低误差,误差怎么降低,让损失函数收敛。从工程层面上来理解,就是为了方便实现,可以把模型分离开来,就是损失函数不依赖于树的生成过程,只依赖于一阶导数与二阶导数,这样就可以分离开来。
做个小结吧,前面做了这么多事都在干什么呢!其实都是在定义目标函数。仅仅完成了GBDT中对应的1,2两步,第一步就是初始化模型为常数0,第二步对应的是计算负梯度(残差)。因为在xgb中用到了一阶导二阶导,所以目标函数的定义比较复杂。前面这么长的篇幅都只是在解释,当xgb是如何在gbdt的基础上改进了这个目标函数。利用了二阶导信息和更新了目标函数的公式使得模型具有更强大 性能。
补充:其实到这里xgb第二步“负梯度”还没有计算出来,只是计算了损失函数L,后面还要推导真正的对应“负梯度”的叶节点值。
好了,大家回想一下gbdt做完这两步接下来要做什么了!bingo,接下来就是根据计算得到的负梯度建新的cart树。那来回想一下在gbdt中cart树是怎么被构建的(这里再提一嘴,gbdt无论是分类还是回归,都是cart回归树,对于分类问题只是在最后加了一层激活层,将数值型变量转换成对应类别的概率输出而已。),很简单,cart树的构建是通过最小化均方误差损失来构建的。
在xgb中也差不多类似,不过就是最小化的目标函数变了一个东西。
先要铺垫一点东西,xgb做了一些新的定义。
这整个都算是正则项,模型的惩罚项,就在前面的目标函数中已经有了。
在上面我们已经得到了目标函数最后长这个样子
现在我们把最后的正则项带进去,然后把替换掉,表示这个样本在t时刻被树预测成什么值,其实就是,表示被分到的叶子节点的值。这么一替换,公式就好看多了
接下来就是一个树特征分裂的判断标准了。
卧槽,其实到这边才真正搞出来对应GDBT中残差或者负梯度的叶子节点值W的计算。……
损失函数的作用在这里才用到了。求增益最大的特征进行分裂。gain越大越好
好,特征分裂完,最后一步就是更新强学习器,用加法模型加上去就结束了。
接下来贴一下完整的算法流程。
好了,接下讲一下XGB的一些注意事项
- 多类别分类时,类别需要从0开始编码
- Watchlist不会影响模型训练。
- 类别特征必须编码,因为xgboost把特征默认都当成数值型的
- 调参:Notes on Parameter Tuning 以及 Complete Guide to Parameter Tuning in XGBoost (with codes in Python)
- 训练的时候,为了结果可复现,记得设置随机数种子。
- XGBoost的特征重要性是如何得到的?某个特征的重要性(feature score),等于它被选中为树节点分裂特征的次数的和,比如特征A在第一次迭代中(即第一棵树)被选中了1次去分裂树节点,在第二次迭代被选中2次…..那么最终特征A的feature score就是 1+2+….
正则项:
LightGBM
以后有时间再另开一章。