GBDT几问
本篇文章主要介绍GBDT基本原理以及一些细节性的东西,这些东西更多在面试使用,或者对于二次创新使用,主要内容有以下几个方面:
如果读者对以上各个方面都很熟悉,那么恭喜你已经成功掌握GBDT了。
Boosting算法Bagging算法介绍
在正式开讲GBDT之前,我先熟悉一下江湖中传说的集成学习的两个派系,分别是Boosting和Bagging。所谓的集成学习主要是通过学习多个弱学习器结合组合策略组成强学习以达到“多个臭皮匠顶个诸葛亮”的作用。集成学习中最典型的两个代表就是Boosting家族和Bagging家族。Boosting家族最典型的代码属于AdaBoosting以及提升树(典型的GBDT)。Bagging家族最典型的代表是Random Forest(随机森林,以下简称RF)。
以下两张图片可以清楚描述Boosting和Bagging的差异:Boosting中的弱学习器之间存在依赖关系,这也是GBDT为什么不能并行的原因。
Bagging算法
Boosting算法
GBDT基本原理
GBDT基本原理是 通过多轮迭代,每轮迭代产生一个弱分类器(利用cart回归树构建),每个分类器在上一轮分类器的残差基础上进行训练。
数学语言描述:利用前一轮迭代的学习器ft−1(x)ft−1(x)以及损失L(y,ft−1(x))L(y,ft−1(x)) 构建一棵cart回归树弱学习器模型ht(x)ht(x) 使得本次迭代的损失L(y,ft(x))=L(y,ft−1(x)+ht(x))L(y,ft(x))=L(y,ft−1(x)+ht(x)) 最小。
GBDT的思想可以用一个通俗的例子解释,假如有个人30岁,我们首先用20岁去拟合,发现损失有10岁,这时我们用6岁去拟合剩下的损失,发现差距还有4岁,第三轮我们用3岁拟合剩下的差距,差距就只有一岁了。如果我们的迭代轮数还没有完,可以继续迭代下面,每一轮迭代,拟合的岁数误差都会减小。
正如前面提到的GBDT每轮迭代都采用的是损失拟合的方法,但是如何拟合呢?针对这个问题,大牛Freidman提出了用损失函数的负梯度来拟合本轮损失的近似值,进而拟合一个CART回归树。第t轮的第i个样本的损失函数的负梯度表示:
rti=−[∂L(yi,f(xi)))∂f(xi)]f(x)=ft−1(x)rti=−[∂L(yi,f(xi)))∂f(xi)]f(x)=ft−1(x)
利用(xi,rti)(xi,rti)构建一颗cart回归树(具体如何构建请参考决策树),这样就得到了第t颗回归树以及其对应的叶节点区域Rtj,j=1,2,...,JRtj,j=1,2,...,J其中J为叶子节点的个数。然后针对每个叶子节点里面的样本,求出使损失函数最小,拟合叶子节点最好的的值ctjctj
ctj=argminc∑xi∈RtjL(yi,ft−1(xi)+c)ctj=argmin⏟c∑xi∈RtjL(yi,ft−1(xi)+c)
这样我们就得到本轮的cart回归树拟合函数:
ht(x)=∑j=1JctjI(x∈Rtj)ht(x)=∑j=1JctjI(x∈Rtj)
最终得到本轮的学习器:
ft(x)=ft−1(x)+ht(x)ft(x)=ft−1(x)+ht(x)
这就是通过损失函数的负梯度来拟合误差的办法。
对弱分类器的要求一般是足够简单,并且是低方差和高偏差的。因为训练的过程是通过降低偏差来不断提高最终分类器的精度。
主要优点:
- 可以灵活处理各种类型的数据,包括连续值和离散值。
- 在相对少的调参时间情况下,预测的准备率也可以比较高。这个是相对SVM来说的。
- 使用一些健壮的损失函数,对异常值的鲁棒性非常强。比如 Huber损失函数和Quantile损失函数。
主要缺点:
- 由于弱学习器之间存在依赖关系,难以并行训练数据。不过可以通过自采样的SGBT来达到部分并行。
GBDT如何正则化
GBDT正则化有三种办法:
- Shrinkage ,即在每一轮迭代获取最终学习器的时候按照一定的步长进行更新。
ft(x)=ft−1(x)+ηht(x)ft(x)=ft−1(x)+ηht(x)
其中0<η≤10<η≤1是步长,对于同样的训练集学习效果,较小的ηη意味着需要更多的迭代次数;通常用步长和迭代最大次数一起来决定算法的拟合效果。 - subsample(子采样),取值为(0,1],采用的不放回采样,如果取值为1,则全部样本都使用,等于没有使用子采样;如果取值小于1,则只有一部分样本会去做GBDT的决策树拟合,选择小于1的比例可以减少方差,即防止过拟合,但是会增加样本拟合的偏差,因此取值不能太低,推荐在[0.5, 0.8]之间。
- CART回归树进行正则化剪枝,这里不详细介绍。
GBDT分裂规则
利用(xi,rti)(xi,rti)来构建cart回归树的时候,GBDT分裂会选取使得误差下降最多(如果cart树采用的是均方差作为损失,那么就是最小均方差)的特征进行分裂,如果这棵树不能拟合好,那么就要通过负梯度计算出新的残差向量来拟合新的cart回归树。对初始cart树可以直接用0,也可以采用样本的均值。既然已经有了分裂规则,那么如何停止继续分裂呢?主要可以通过以下手段进行操作:1、节点分裂时的最小样本数;2、树的最大深度;3、树的最大叶子节点数;4、loss满足约束条件。
GBDT的“梯度提升”体现在那个阶段
前面提到,在构建cart树时使用了损失函数的负梯度,而不是所谓的残差=真值-预测值;实际上是一种更宽广的概念,但是在平方损失的情况下,上面等式是成立的。另外使用损失函数的梯度可以保证损失函数最小值。所以GBDT的梯度提升体现在构建cart树的所需的负梯度阶段,其利用最速下降的近似方法。
GBDT如何做特征选择
特征j的全局重要度通过特征j在单颗树中的重要度的平均值来衡量
J2j^=1M∑m=1MJ2j^(Tm)Jj2^=1M∑m=1MJj2^(Tm)
其中M是树的数量。特征j在单颗树中的重要度的如下
J2j^(T)=∑t=1L−1i2t^I(vt=j)Jj2^(T)=∑t=1L−1it2^I(vt=j)
其中,L为树的叶子节点数量,L−1即为树的非叶子节点数量(构建的树都是具有左右孩子的二叉树)vtvt和节点t相关联的特征,i2t^it2^是节点t分裂之后平方损失的减少值。
GBDT为什么使用cart回归树而不是使用分类树
GBDT主要是利用残差逼近的方式,这就意味每棵树的值是连续的可叠加的,这一点和回归树输出连续值不谋而合,如果采用分类树,那么残差逼近进行叠加就会使得这种叠加没有意义,比如男+男+女=到底是男是女。这个是GBDT基本原理决定的。
为什么GBDT的树深度较RF通常都比较浅
对于机器学习来说,泛化误差可以理解为两部分,分别是偏差(bias)和方差(variance);偏差指的是算法的期望预测与真实预测之间的偏差程度,反应了模型本身的拟合能力;方差度量了同等大小的训练集的变动导致学习性能的变化,刻画了数据扰动所导致的影响。当模型越复杂时,拟合的程度就越高,模型的训练偏差就越小;但此时如果换一组数据可能模型的变化就会很大,即模型的方差很大,所以模型过于复杂的时候会导致过拟合。对于RF来说由于并行训练很多不同的分类器的目的就是降低这个方差(variance)。所以对于每个基分类器来说,目标就是如何降低这个偏差(bias),所以我们会采用深度很深甚至不剪枝的决策树。而对于GBDT来说由于利用的是残差逼近的方式,即在上一轮的基础上更加拟合原数据,所以可以保证偏差(bias),所以对于每个基分类器来说,问题就在于如何选择 variance 更小的分类器,即更简单的分类器,所以我们选择了深度很浅的决策树。
GBDT那些部分可以并行
1、计算每个样本的负梯度;
2、分裂挑选最佳特征及其分割点时,对特征计算相应的误差及均值时;
3、更新每个样本的负梯度时;
4、最后预测过程中,每个样本将之前的所有树的结果累加的时候。
GBDT与RF的区别
相同点:
1、GBDT与RF都是采用多棵树组合作为最终结果;这是两者共同点。
不同点:
1、RF的树可以是回归树也可以是分类树,而GBDT只能是回归树。
2、RF中树是独立的,相互之间不影响,可以并行;而GBDT树之间有依赖,是串行。
3、RF最终的结果是有多棵树表决决定,而GBDT是有多棵树叠加组合最终的结果。
4、RF对异常值不敏感,原因是多棵树表决,而GBDT对异常值比较敏感,原因是当前的错误会延续给下一棵树。
5、RF是通过减少模型的方差来提高性能,而GBDT是减少模型的偏差来提高性能的。(原理之前分析过)
6、RF不需要进行数据预处理,即特征归一化。而GBDT则需要进行特征归一化。
GBDT与XGBoost的区别
在讲两者的区别时,我们首先要知道XGBoost针对GBDT做了那些改进,首先,XGBoost在计算损失函数的时候,针对损失函数做了一次二阶泰勒展开,具体如下:
L(y,ft(x))=L(y,ft−1(x)+ht(x))≃L(y,ft−1(x))+[∂L(y,ft−1(x))∂ft−1(x)]ht(x)+12[∂2L(y,ft−1(x))∂2ft−1(x)]h2t(x)L(y,ft(x))=L(y,ft−1(x)+ht(x))≃L(y,ft−1(x))+[∂L(y,ft−1(x))∂ft−1(x)]ht(x)+12[∂2L(y,ft−1(x))∂2ft−1(x)]ht2(x)
令g(x)=∂L(y,ft−1(x))∂ft−1(x)g(x)=∂L(y,ft−1(x))∂ft−1(x) , h(x)=∂2L(y,ft−1(x))∂2ft−1(x)h(x)=∂2L(y,ft−1(x))∂2ft−1(x) 那么:
L(y,ft(x))≃L(y,ft−1(x))+g(x)ht(x)+h(x)h2t(x)L(y,ft(x))≃L(y,ft−1(x))+g(x)ht(x)+h(x)ht2(x)
从上式可以看出,因为L(y,ft−1(x))L(y,ft−1(x))是前一次迭代最小损失,对于本次迭代而言,主要是对:
L~=g(x)ht(x)+h(x)h2t(x)L~=g(x)ht(x)+h(x)ht2(x)
进行拟合,以达到最小损失的结果,如果加入正则化项那么:
L~=g(x)ht(x)+h(x)h2t(x)+Ω(ht(x))L~=g(x)ht(x)+h(x)ht2(x)+Ω(ht(x))
其中
Ω(ht(x))=ηht(x)+12λ∑j=1Jw2tjΩ(ht(x))=ηht(x)+12λ∑j=1Jwtj2
是正则化项,在这里我们可以将ht(x)ht(x)理解成树,它是关于wtjwtj的函数,wtjwtj是参数(权重),通过L~L~对wtjwtj求导可以解出wtjwtj。那么节点是如何分裂的呢?有两种办法:
- 一种办法是贪心算法,遍历一个节点内的所有特征,按照公式计算出按照每一个特征分割的信息增益,找到信息增益最大的点进行树的分割。增加的新叶子惩罚项对应了树的剪枝,当gain小于某个阈值的时候,我们可以剪掉这个分割。但是这种办法不适用于数据量大的时候,因此,我们需要运用近似算法。
- 另一种方法:XGBoost在寻找splitpoint的时候,不会枚举所有的特征值,而会对特征值进行聚合统计,按照特征值的密度分布,构造直方图计算特征值分布的面积,然后划分分布形成若干个bucket(桶),每个bucket的面积相同,将bucket边界上的特征值作为splitpoint的候选,遍历所有的候选分裂点来找到最佳分裂点。
实际上对损失函数的优化就是对第t棵树结构进行分裂,启发式的找到最优树结构的过程。而每次分裂,对应于将属于一个叶结点(初始情况下只有一个叶结点,即根结点)下的训练样本分配到分裂出的两个新叶结点上,每个叶结点上的训练样本都会对应一个模型学出的概率值,而损失函数本身满足样本之间的累加特性,所以,可以通过将分裂前的叶结点上样本的损失函数和与分裂之后的两个新叶结点上的样本的损失函数之和进行对比,从而找到可用的分裂特征以及特征分裂点。
XGBoost的创新
GBDT以cart回归树作为基分类器,XGBoost还支持线性分类器,这个时候XGBoost相当于带L1和L2正则化项的逻辑斯蒂回归(分类问题)或者线性回归(回归问题)。
- 传统GBDT在优化时只用到一阶导数信息,XGBoost则对代价函数进行了二阶泰勒展开,同时用到了一阶和二阶导数。
- XGBoost在代价函数里加入了正则项,用于控制模型的复杂度。正则项里包含了树的叶子节点个数、每个叶子节点上输出的score的L2模的平方和。从偏差、方差逼近的角度来讲,正则项降低了模型的方差,使学习出来的模型更加简单,防止过拟合,这也是XGBoost优于传统GBDT的一个特性
- 列抽样(column subsampling)。XGBoost借鉴了随机森林的做法,支持列抽样(即每次的输入特征不是全部特征),不仅能降低过拟合,还能减少计算,这也是XGBoost异于传统GBDT的一个特性。
- 忽略缺失值:在寻找分裂点的时候,不会对该缺失特征的样本进行遍历统计,只对该列特非缺失征值的样本上对应的特征值进行遍历,通过这个工程技巧来减少了为稀疏离散特征寻找分裂点的时间开销。
- 指定缺失值的分隔方向:可以为缺失值或者指定的值指定分支的默认方向,为了保证完备性,会分别处理缺失特征值的样本分配到左叶子结点和右叶子结点的两种情形,分到那个子节点带来的增益大,默认的方向就是哪个子节点,这能大大提升算法的效率。
- 并行化处理:在训练之前,预先对每个特征内部进行了排序找出候选切割点,然后保存为block结构,后面的迭代中重复地使用这个结构,大大减小计算量。在进行节点的分裂时,需要计算每个特征的增益,最终选增益最大的那个特征去做分裂,那么各个特征的增益计算就可以开多线程进行,即在不同的特征属性上采用多线程并行方式寻找最佳分割点