根据菜菜的课程进行整理,方便记忆理解
代码位置如下:
求解XGB的目标函数
有了如下目标函数,我们来看看如何求解它。时刻记得我们求解目标函数的目的:为了求得在第t次迭代中最优的树。在逻辑回归和支持向量机中,我们通常先将目标函数转化成一种容易求解的方式(比如对偶),然后使用梯度下降或者SMO之类的数学方法来执行我们的最优化过程。之前我们使用了逻辑回归的迭代过程来帮助大家理解在梯度提升树中树是如何迭代的,那我们是否可以使用逻辑回归的参数求解方式来求解XGB的目标函数呢?
很遗憾,在XGB中我们无法使用梯度下降,原因是XGB的损失函数没有需要求解的参数。我们在传统梯度下降中迭代的是参数,而我们在XGB中迭代的是树,树不是数字组成的向量,并且其结构不受到特征矩阵x取值大小的直接影响,尽管这个迭代过程可以被类比到梯度下降上,但真实的求解过程却是完全不同。
在求解XGB的目标函数的过程中,我们考虑的是如何能够将目标函数转化成更简单的,与树的结构直接相关的写法,以此来建立**树的结构与模型的效果(包括泛化能力与运行速度)**之间的直接联系。也因为这种联系的存在,XGB的目标函数又被称为“结构分数”。
首先,我们先来进行第一步转换。
其中和分别是在损失函数上对所求的一阶导数和二阶导数,他们被统称为每个样本的梯度统计量(gradient statisticts)。在许多算法的解法推导中,我们求解导数都是为了让一阶导数等于0来求解极值,而现在我们求解导数只是为了配合泰勒展开中的形式,仅仅是简化公式的目的罢了。所以GBDT和XGB的区别之中,GBDT求一阶导数,XGB求二阶导数,这两个过程根本是不可类比的。XGB在求解极值为目标的求导中也是求解一阶导数,后面会为大家详解。
可能的困惑:泰勒展开好像不是长这样
如果大家去单独查看泰勒展开的数学公式,你们会看到的大概是这样:
其中表示上对x求导后,令x的值等于c所取得的值。其中有假设:c与x非常接近, 非常接近0,于是我们可以将式子改写成:
如刚才所说, 需要很小,与x相比起来越小越好,那在我们的式子中,需要很小的这部分就是。这其实很好理解,对于一个集成算法来说,每次增加的一棵树对模型的影响其实非常小,尤其是当我们有许多树的时候——比如,n_estimators=500的情况, 与x相比总是非常小的,因此这个条件可以被满足,泰勒展开可以被使用。如此,我们的目标函数可以被顺利转化成:
这个式子中, 和只与传统损失函数相关,核心的部分是我们需要决定的树。接下来,我们就来研究一下我们的。
参数化决策树:参数alpha,lambda
class xgboost.XGBRegressor (max_depth=3, learning_rate=0.1, n_estimators=100, silent=True, objective='reg:linear', booster='gbtree', n_jobs=1, nthread=None, gamma=0, min_child_weight=1, max_delta_step=0, subsample=1, colsample_bytree=1, colsample_bylevel=1, reg_alpha=0, reg_lambda=1, scale_pos_weight=1, base_score=0.5, random_state=0, seed=None, missing=None, importance_type='gain',**kwargs)
在参数化决策树之前,我们先来简单复习一下回归树的原理。对于决策树而言,每个被放入模型的任意样本i最终一个都会落到一个叶子节点上。对于回归树,通常来说每个叶子节点上的预测值是这个叶子节点上所有样本的标签的均值。但值得注意的是,XGB作为普通回归树的改进算法,在上却有所不同。
对于XGB来说,每个叶子节点上会有一个预测分数(prediction score),也被称为叶子权重。这个叶子权重就是所有在这个叶子节点上的样本在这一棵树上的回归取值,用或者来表示。
当有多棵树的时候,集成模型的回归结果就是所有树的预测分数之和,假设这个集成模型中总共有K棵决策树,则整个模型在这个样本上给出的预测结果为:
基于这个理解,我们来考虑每一棵树。对每一棵树,它都有自己独特的结构,这个结构即是指叶子节点的数量,树的深度,叶子的位置等等所形成的一个可以定义唯一模型的树结构。在这个结构中,我们使用表示样本所在的叶子节点,并且使用来表示这个样本落到第t棵树上的第个叶子节点中所获得的分数,于是有:
这是对于每一个样本而言的叶子权重,然而在一个叶子节点上的所有样本所对应的叶子权重是相同的。设一棵树上总共包含了T个叶子节点,其中每个叶子节点的索引为j,则这个叶子节点上的样本权重是。依据这个,我们定义模型的复杂度为(注意这不是唯一可能的定义,我们当然还可以使用其他的定义,只要满足叶子越多/深度越大,复杂度越大的理论,我们可以自己决定我们的要是一个怎样的式子):
这个结构中有两部分内容,一部分是控制树结构的,另一部分则是我们的正则项。叶子数量可以代表整个树结构,这是因为在XGBoost中所有的树都是CART树(二叉树),所以我们可以根据叶子的数量判断出树的深度,而是我们自定的控制叶子数量的参数。
至于第二部分正则项,类比一下我们岭回归和Lasso的结构,参数和的作用其实非常容易理解,他们都是控制正则化强度的参数,我们可以二选一使用,也可以一起使用加大正则化的力度。当和都为0的时候,目标函数就是普通的梯度提升树的目标函数。
XGB vs GBDT 核心区别2:正则项的存在
在普通的梯度提升树GBDT中,我们是不在目标函数中使用正则项的。但XGB借用正则项来修正树模型天生容易过拟合这个缺陷,在剪枝之前让模型能够尽量不过拟合。
来看正则化系数分别对应的参数:
| 参数含义 | xgb.train() | xgb.XGBRegressor() |
|---|---|---|
| L1正则项的参数 | alpha,默认0,取值范围[0, +∞] | reg_alpha,默认0,取值范围[0, +∞] |
| L2正则项的参数 | lambda,默认1,取值范围[0, +∞] | reg_lambda,默认1,取值范围[0, +∞] |
根据我们以往的经验,我们往往认为两种正则化达到的效果是相似的,只不过细节不同。比如在逻辑回归当中,两种正则化都会压缩参数的大小,只不过L1正则化会让为0,而L2正则化不会。在XGB中也是如此。当和越大,惩罚越重,正则项所占的比例就越大,在尽全力最小化目标函数的最优化方向下,叶子节点数量就会被压制,模型的复杂度就越来越低,所以对于天生过拟合的XGB来说,正则化可以一定程度上提升模型效果。
对于两种正则化如何选择的问题,从XGB的默认参数来看,我们优先选择的是L2正则化。当然,如果想尝试L1也不是不可。两种正则项还可以交互,因此这两个参数的使用其实比较复杂。在实际应用中,正则化参数往往不是我们调参的最优选择,如果真的希望控制模型复杂度,我们会调整而不是调整这两个正则化参数,因此大家不必过于在意这两个参数最终如何影响了我们的模型效果。对于树模型来说,还是剪枝参数地位更高更优先。大家只需要理解这两个参数从数学层面上如何影响我们的模型就足够了。如果我们希望调整和,我们往往会使用网格搜索来帮助我们
寻找最佳树结构:求解与
在上一节中,我们定义了树和树的复杂度的表达式,树我们使用叶子节点上的预测分数来表达,而树的复杂度则是叶子数目加上正则项:
假设现在第t棵树的结构已经被确定为q,我们可以将树的结构带入我们的损失函数,来继续转化我们的目标函数。转化目标函数的目的是:建立树的结构(叶子节点的数量)与目标函数的大小之间的直接联系,以求出在第t次迭代中需要求解的最优的树。注意,我们假设我们使用的是L2正则化(这也是参数lambda和alpha的默认设置,lambda为1,alpha为0),因此接下来的推导也会根据L2正则化来进行。
橙色框中的转化是如何实现的?
于是我们可以有:
如此就实现了这个转化。
对于最终的式子,我们定义:
于是可以有:
其中每个j取值下都是一个以为自变量的二次函数,我们的目标是追求让Obj最小,只要单独的每一个叶子j取值下的二次函数都最小,那他们的加和必然也会最小。于是,我们在上对求导,让一阶导数等于0以求极值,可得:
把这个公式带入目标函数,则有:
到了这里,大家可能已经注意到了,比起最初的损失函数 + 复杂度的样子,我们的目标函数已经发生了巨大变化。我们的样本量i已经被归结到了每个叶子当中去,我们的目标函数是基于每个叶子节点,也就是树的结构来计算。所以,我们的目标函数又叫做“结构分数”(structure score),分数越低,树整体的结构越好。如此,我们就建立了树的结构(叶子)和模型效果的直接联系。
更具体一些,我们来看一个例子:
所以在XGB的运行过程中,我们会根据的表达式直接探索最好的树结构,也就是说找寻最佳的树。从式子中可以看出, 和是我们设定好的超参数, 和是由损失函数和这个特定结构下树的预测结果共同决定,而只由我们的树结构决定。则我们通过最小化所求解出的其实是,叶子的数量。所以本质也就是求解树的结构了。
在这个算式下,我们可以有一种思路,那就是枚举所有可能的树结构q,然后一个个计算我们的,待我们选定了最佳的树结构(最佳的)之后,我们使用这种树结构下计算出来的和就可以求解出每个叶子上的权重,如此就找到我们的最佳树结构,完成了这次迭代。
用求解?
一个大家可能会感到困惑的点是, 和的本质其实是损失函数上的一阶导数和二阶导数之和,而一阶和二阶导数本质是:
是已知的标签,但我们有预测值的求解公式:
这其实是说, 和的计算中带有,那先确定最佳的,再求出和,结合求出叶子权重的思路不觉得有些问题么?仿佛在带入求解?对于有这样困惑的大家,请注意我们的与我们现在要求解的其实不是在同一棵树上的。别忘记我们是在一直迭代的,我们现在求解的是第t棵树上的结果,而是前面的棵树的累积,是在前面所有的迭代中已经求解出来的已知的部分。
- 求解第一棵树时,没有“前面已经迭代完毕的部分”,怎么办?
那第二个问题又来了:那我们如何求解第一棵树在样本i下的预测值呢?在建立第一棵树时,并不存在任何前面的迭代已经计算出来的信息,但根据公式我们需要使用如下式子来求解,并且我们在求解过程中还需要对前面所有树的结果进行求导。
这时候,我们假设来解决我们的问题,事实是,由于前面没有已经测出来的树的结果,整个集成算法的结果现在也的确为0。
- 第0棵树的预测值假设为0,那求解第一棵树的和的过程是在对0求导?
这个问题其实很简单。在进行求导时,所有的求导过程都和之前推导的过程相一致,之所以能够这么做,是因为我们其实不是在对0求导,而是对一个变量求导。只是除了求导之外,我们还需要在求导后的结果中带入这个变量此时此刻的取值,而这个取值在求解第一棵树时刚好等于0而已。更具体地,可以看看下面,对0求导,和对变量求导后,变量取值为0的区别:
这些细节都理解了之后,相信大家对于先求解的最小值来求解树结构,然后以此为基础求解出的过程已经没有什么问题了。回忆一下我们刚才说的,为了找出最小的,我们需要枚举所有可能的树结构,这似乎又回到了我们最初的困惑——我们之所以要使用迭代和最优化的方式,就是因为我们不希望进行枚举,这样即浪费资源又浪费时间。那我们怎么办呢?来看下一节:贪婪算法寻找最佳结构。
寻找最佳分枝:结构分数之差
贪婪算法指的是控制局部最优来达到全局最优的算法,决策树算法本身就是一种使用贪婪算法的方法。XGB作为树的集成模型,自然也想到采用这样的方法来进行计算,所以我们认为,如果每片叶子都是最优,则整体生成的树结构就是最优,如此就可以避免去枚举所有可能的树结构。
回忆一下决策树中我们是如何进行计算:我们使用基尼系数或信息熵来衡量分枝之后叶子节点的不纯度,分枝前的信息熵与分治后的信息熵之差叫做信息增益,信息增益最大的特征上的分枝就被我们选中,当信息增益低于某个阈值时,就让树停止生长。在XGB中,我们使用的方式是类似的:我们首先使用目标函数来衡量树的结构的优劣,然后让树从深度0开始生长,每进行一次分枝,我们就计算目标函数减少了多少,当目标函数的降低低于我们设定的某个阈值时,就让树停止生长。
来个具体的例子,在这张图中,我们有中间节点“是否是男性”,这个中间节点下面有两个叶子节点,分别是样本弟弟和妹妹。我们来看看这个分枝点上,树的结构分数之差如何表示。
对于中间节点这一个叶子节点而言,我们的,则这个节点上的结构分数为:
对于弟弟和妹妹节点而言,则有:
则分枝后的结构分数之差为:
CART树全部是二叉树,因此这个式子是可以推广的。从这个式子我们可以总结出,其实分枝后的结构分数之差为:
其中和从左节点(弟弟节点)上计算得出, 和从有节点(妹妹节点)上计算得出,而和从中间节点上计算得出。对于任意分枝,我们都可以这样来进行计算。在现实中,我们会对所有特征的所有分枝点进行如上计算,然后选出让目标函数下降最快的节点来进行分枝。对每一棵树的每一层,我们都进行这样的计算,比起原始的梯度下降,实践证明这样的求解最佳树结构的方法运算更快,并且在大型数据下也能够表现不错。至此,我们作为XGBoost的使用者,已经将需要理解的XGB的原理理解完毕了