XGBoost其实是GBDT的一个工程实现,在Kaggle等比赛中被广泛应用。同时XGBoost也对GBDT做了一些近似和优化,具体内容也发了Paper(华盛顿大学陈天奇博士),本篇博客将会对论文进行解读。
xgboost的优点,几个关键词:scalable,end-to-end,used widely,sparsity-aware。先不用管这些花里胡哨的优点,我们就关注XGBoost是怎么发展起来的,是为了解决什么问题,怎样有效地解决了这个问题。
XGBoost的安装
有的人使用whl很快就安装好了,如果你的vs版本和python不对应的话,只能自行编译。有的人即便是安装好了,但是导入报错,明明有xgboost.dll,依然导入不成功,提示缺少vc2015的dll,即便是下载了扔在windows32中,依然无解。同时编译过程也有很多坑,首先是cmake版本太低,重新安装;configuration的时候继续报错,提示工程有问题。其实确实是工程有问题,因为我是直接下载zip的,这样有一些依赖就不会自动下载,所以还是推荐使用git clone --recursive https://github.com/dmlc/xgboost,如果是在公司,还会碰到需要使用代理的问题。但其实clone也会出问题,所以其实可以手动下载XGBoost下缺失的子文件夹。然后使用cmake configuration之后再generate,得到sln。然而我编译时报错3000多个,最后又试了稳定的09版本才编译成功,得到了dll。
git config –global http.proxy http://user:password@
http://10.10.10.10
:8080
git config --global http.sslVerify false
Boosting
了解过集成学习的都知道集成学习分为三大类:bagging,boosting,stacking。boosting是串行的。训练过程:每一个基分类器都要依靠前一个的结果,对于i出错的样本加大权重,再送入i+1分类器中继续学习。预测过程:对n个基分类器的结果加权求和。这里有两个权重:训练样本的权重(训练时用);基学习器的权重(预测时用)。这两个权重怎么得到呢,继续往下看。
boosting的思想其实就很类似于深度学习了,都是有一个优化目标,不断迭代地去逼近。不同的是深度学习模型结构是固定的,通过多次重复地输入样本来调整权重;而boosting中模型是不断生长的,变化的是样本的权重。同样是调整权重,一个是调整模型中参数,一个是调整样本的权重,其实没有什么不同。
像深度学习一样,boosting算法也有三要素:1.训练数据。对于我们来说训练数据是一个个样本,但其实对于boosting来说是由训练样本初步得到的弱学习器(一般是决策树);2.模型。模型决定了如何由弱学习器得到最终的输出,在boosting中,模型一般是加法模型additive model:对多棵树的结果进行加权相加。3.损失函数。很显然,有了损失函数,才能对模型进行优化。
加法模型
损失函数-Adaboost
加法模型其实没什么好说的,更重要的是损失函数,而损失函数的选取,就决定了四种不同的方法。
https://zhuanlan.zhihu.com/p/42740654
表-深入浅出http://www.52caml.com/head_first_ml/ml-chapter6-boosting-family/
AdaBoost采用的是e指数损失。。AdaBoost其实是加法模型下,用前向分步算法的一个特例:损失函数为指数函数。下面简要说明:
前向分步算法在优化求解时是逐个基函数求解,即是逐个求解.当损失函数是指数函数:,那么
上面这个公式的意思是将个基分类器的最终输出分解第个的输出和第m个基分类器的输出之和(这也是加法模型的本意),这样exp指数位置由两个加法项构成:第一项是标签和第m-1的输出,它和要求解的是无关的(我们关心的是m)。令,那么可以由下式求得:
可以得到:
注意到之前定义的虽然与最小化第m个基函数无关,但它包含了m-1,所以在每轮迭代中是变化的:。事实上,这个对应了AdaBoost中样本权重的变化:原来的权重乘以该样本对应的损失作为新权重,损失越大,新的权重越大。
手把手Adaboosthttps://zhuanlan.zhihu.com/p/27126737
推导https://blog.csdn.net/watermelon12138/article/details/84785736
刘建平https://www.cnblogs.com/pinard/p/6133937.html
AdaBoost采用的是加权表决法,所以得到基分类器和分类器系数就可以得到最终的分类函数:
损失函数-GBDT
从名字上来看,GBDT分为两部分,一个是GB(Grdient Boosting),它引入了梯度的概念。GBDT与AdaBoost的不同主要是思想不同,同样是加法模型,后者是通过每次迭代中调整样本权重,而前者是调整每次迭代时的拟合目标:Freidman提出了梯度提升(gradient boosting)算法:使用损失函数的负梯度来拟合本轮的损失近似值。
GBDT中使用的损失函数可以是多样的,可以是均方差,绝对损失,指数损失函数。当损失函数是均方差时(CART树一般使用平方误差函数),对其求梯度(导数)就变成残差了。这就是一般意义上大家把GBDT下一棵树将要学习的目标是残差(和残差网络类似)的原因,但我们需要知道其实拟合残差是拟合负梯度的一个特例。当使用的是绝对损失,前向分布算法不好求解,使用损失函数的负梯度来拟合本轮的损失近似值更易求解。当使用指数损失函数时,GBDT退化为AdaBoost算法。
使用负梯度的好处还有就是可以应对新增的正则项,因为只是经验损失最小的话容易过拟合,为了防止过拟合需要增加正则项,而如果有正则项,那么损失函数就不仅仅是残差了。https://www.zhihu.com/question/63560633
GBDT的第二部分DT(Dcision Tree),字面意思是决策树,但实际上在GBDT中的决策树必须是回归树,它限定了弱学习器必须是回归树。因为预测残差的过程是一个回归的过程,是对数值的预测。这一点看GBDT的另外一个名字就更清楚了: MART(Multiple Additive Regression Tree)
GBDT除了GB和DT,后来还演进了一个重要的部分:Shrinkage。在最开始提到的GBDT使用的加法模型对各个树是没有添加权重的,因为每一棵树都是最大程度去去拟合残差。之前提到在拟合残差的过程近似于梯度下降的过程,有时候为了拟合残差有可能矫枉过正,所以我们其实可以对回归树增加权重,等价于梯度下降中的步长,意义是渐进地去拟合。
原来前i棵树的预测结果:y(1~i)=sum(y1,...,yi)
使用Shrinkage之后:y(1~i)=y(1~i-1)+step*yi
白开水加糖https://www.cnblogs.com/peizhe123/p/6105696.html
从Grdient Descend 到GradientBoostinghttps://blog.csdn.net/qq_19446965/article/details/82079624
XGBoost
首先定义了树的复杂度,而为了定义复杂度,需要将树的结构分为树结构部分q和叶子权重部分w。这里的叶子节点权重怎么理解呢?就算使用了shrinkage,也只是每棵树有一个权重啊。其实这里的权重是叶子节点输出,当做回归时,这个输出是值,当做分类时,输出是标签。而因为GBDT一般使用CART回归树,其在分裂节点时使用的是MSE均方差准则,MSE越小,意味着该部分数据越“纯”,这样在最终预测的时候就可以以所有落在该节点的数据值的均值作为预测值。
从上式中可以看到,复杂度由叶子节点构成:叶子节点的个数T,叶子节点的权重。为什么叶子节点这么重要呢,不考虑树的深度吗?定义复杂度是为了控制过拟合,叶子节点个数太多分得太细容易过拟合;叶子节点权重???
具体到第t棵树,以前t-1棵树为基础,那么优化目标是
不能直接期望第t棵树就达到目标值,所以会有一个constant的存在。注意我们把看作,利用泰勒二阶展开,达到优化目标的近似值,只需要关心损失函数的一阶导和二阶导(当然要求损失函数是可导的)。GBDT的负梯度其实是一阶泰勒展开,XGBoost二阶泰勒展开更加准确。
去掉只与t-1有关的常数项,得到:
的输出其实就是叶子节点的取值(叶子节点权重)。到这里可以看到第t棵树的损失函数以n个样本点的和构成,我们可以换个思路,以叶子节点进行分组:对每个叶子统计落在该叶子节点的所有样本,最后按照叶子节点进行相加。那么第t棵树的优化目标可以写为:
各个叶子目标子式相独立,每一个可以看作是一元二次方程,则可以得到每个叶子节点的权重值:
第t棵树对应的最小损失:
注意,当树结构确定时才能使用上式求出叶子节点权重,而我们首先要确定树结构。因为每种树结构的损失是可以计算出来的,所以一个方法是穷举树结构,对每一个树结构计算其对应的损失函数,然后选取其最小的那个。更常见的是贪心法:通过分裂节点得到树结构,在每次分裂的时候只考虑当前的节点,使得当前分裂前后的增益最大,而增益就是分裂前后两种树结构的损失函数之差:
既然是一个个分裂节点试,总还是要有规律一点:看作两层for循环,外循环是遍历各个特征,内循环是遍历该特征下的每个值。两层循环的分裂过程是最耗时的,XGBoost分别对特征的取和分裂点的选取做了优化。
针对特征选取的外循环,其实是可以使用多线程并行化的。Boosting不同弱学习器之间无法并行,但是对于单个弱学习器来说,可以并行地寻找分裂的最大增益。
内循环在访问特征值的时候也需要按大小顺序遍历,所以可以提前将每个特征下的顺序排好,存放在block结构中。block的大小需要合理设置,一方面充分利用CPU缓存进行读取加速(cache-aware access),一方面将block compression储存到硬盘得到更大的IO。为了进一步增加速度,在内循环的时候遍历粒度可以增大,只选择分位点(如四分位,四分之三分位点)进行分裂。这就是XGBoost中的分位点近似法。对每一个特征确定一个近似点集合,根据这个集合是重复使用还是每次分裂时重新确定,近似分位法又分为全局策略和局部策略。因为是Boosting算法,树之间无法并行,但是计算增益点的时候可以并行化:不再是两层循环,而是不同的特征同时计算各自的增益。
陈天奇提出了一种带权重的分位草图Weighted Quantile Sketch。这里我们假定所使用的特征是第k个特征,目的是在这个维度中找到分离点,相当于一个阈值,把树分成左右两部分。对数据在这个维度上排序,我们可以精确得到将数据分成某个比例的分割点。可以定义一个函数,表示第k个特征的值k小于z的样本占全部样本的比值,那么近似分位数方法就不是寻找满足特定比例的一个分割点,而是比例在一个区间内的几个分割点。在这个区间中,eps是分辨率,所以理论上会得到1/eps个分割点。重写目标函数,将其配方,可以看作是一个平方误差函数:标签是g/h,二阶导数h是权重。当损失函数是square loss时,二阶导数恒为2,此时每个样本的权重相等;当损失函数是log loss时,二阶导数是一元二次函数,开口朝下,pred=0.5时权重最大,这意味着预测越是不准确,权重越大,分桶时被切分得更细。
对XGBoost的总结
XGBoost的一大特点是scalability,这个尺度可变指的是数据量,意味着XGBoost可以在单机上实现十几倍的加速,在分布式中可以应对billion是级别的数据。这得益于对于稀疏数据sparse data的处理,并且使用了带权重的分位数草图近似算法。当然,为了加速还可以使用分布式和并行计算。XGBoost还使用了核外计算out-of-core computation,这部分结构称为cache-aware block。
集成学习其实是利用多个弱学习器(树)的输出,对其加权求和得到预测。这里的树使用的是回归树,如CART。树包含两个属性,一个是树的结构,将n维的输入转换为T维的输出,T即是叶子结点数;第二个是叶子结点的权重,因为回归树的输出不是简单地投票给某个叶子结点,而是对每一个叶子结点输出一个连续的得分,所以我们可以对每一个叶子结点赋予一个权重。那么最终对X的输出就是每棵树。树的叶子节点个数的权重平方和一起用来控制模型的复杂度,防止过拟合。当正则项为0时,就退化为传统的GBDT。为了防止过拟合,除了之前添加的正则项,还有两种方法。方法一是Shrinkage,它的思想和learning rate是类似的,对于较新的树,以一定比例增加其权重;方法二是对特征(列)进行下采样,这个方法其实在随机森林算法中也用到了。
在优化目标函数中,因为变量是函数,不能使用传统的优化方法在欧式空间中优化,而是使用了叫做追加训练法additive manner的训练方法。在这种方法中,树依旧是一棵棵训练的,和boosting的思想一样,需要注意的是这里使用的贪婪算法的思想,每加一棵树保持之前的树不变,寄希望于新加的树使得优化目标函数最小。参考文献12,可以使用泰勒二阶展开对此时的目的函数求近似:将新加的树看作是自变量的增长量detla。
二阶近似时,目标函数可以看作是权重w的二次函数,所以可以解得目标函数最小时w的取值,这时目标函数的表达式也可以得到,并且可以用于衡量树结构的质量水平,类似于不纯度impurity score。注意在第t棵树的结构是固定时才可以这样计算,换言之我们只能通过枚举树的结构选择目标函数最小的那个树的结构作为下一棵树,但是这样明显是不现实的。
XGBoost使用了贪心算法,从单一的叶子结点开始,不断地添加分支,计算分裂前后的loss变化。贪心算法是在确定结点之后怎么分的问题,那么如何确定最优的结点呢?这里继续使用贪心算法,叫做exact possible algorithm。没有枚举所有树的结构,这里枚举的是所有的特征,在连续的特征上枚举所有可能的splits,为了减少计算量,需要对特征进行排序,按序遍历,计算梯度统计。
算法的框架是首先根据特征分布的百分比找到候选的splitting points,然后将连续的特征根据分离点映射到桶buckets中,由aggregated statistics找到最优解。根据何时提供候选点,有两种近似方法,一种是全局的global variant,在初始构建树的过程中就提供候选点,local variant则是在每次slit之后re-proposal,全局的方法提供的次数更少,但是需要更多的候选点,而local因为在每次splits之后对候选点进行了refine提纯,所以适合于更深的树。
对稀疏数据的处理。造成稀疏性的原因大致有三种:缺失值的存在;某项统计值高频率地取0;人为的编码,如独热编码。论文在每个树结点处增加了一个默认的方向(左子树或者右子树),用于处理系数值,这也是为什么XGBoost可以处理缺失值的原因。XGBoost不是简单地指定左或者右,而是根据数据自动设定的。缺失值影响的其实是分割点的确定。分割点的确定方法和之前一样,无非是排序,遍历,求导数,增益。对于缺失值,排序方法不同,位置也不同:增序排序,缺失值排在第一个,被分进左子树;降序则被分在右子树,最后同样是选择增益最大的那种结构。
最耗时的步骤是排序,所以这篇文章在系统上做了一些设计。设计了一个名为block的结构用于存放数据,在这里数据不是以样本为单位,而是以特征为单位,每一列是一个特征,并对该特征的数值进行排序。在近似算法中,可以使用多个block,这样寻找分位数的操作变成线性的扫描,这对于local proposal是更有意义的,因为候选会更频繁地被产生。block这种结构还可以减少寻找分割点过程中的计算量。
使用block带来的问题是统计梯度的过程中对于内存的访问是不连续的。对于excat greedy算法,为每一个线程分配一个外接buffer。对于近似算法,做法是选择一个合适大小的block size,size太小会导致并行效率低;太大会造成无法载入CPU,丢失缓存。这里选择的是存储在一个block中最多的样本树,这反映了统计梯度的内存成本。
模型的保存与加载,推荐使用序列化的pickle.dump与pickle.load,否则predict的时候会报奇怪的错误。
Reference:
1.字节跳动架构师https://zhuanlan.zhihu.com/p/30339807
2.浅谈https://www.jianshu.com/p/d55f7aaac4a7
3.https://www.jianshu.com/p/6fba1bcb303c
4类别特征https://www.biaodianfu.com/categorical-features.html
5.讲义https://www.cnblogs.com/yumoye/p/10421153.html
6.近似分位算法https://blog.csdn.net/anshuai_aw1/article/details/83025168?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase
7.终于明白了https://cloud.tencent.com/developer/article/1513111
8.https://www.cnblogs.com/jiangxinyang/p/9248154.html
9.标点符https://www.biaodianfu.com/xgboost.html