XGBoost简介:打开树模型的黑盒子

2,660 阅读5分钟

作者:字节移动技术——许瑶坤

首先明确一下,XGBoost是一种训练决策树模型的学习方式,准确的说是训练Tree Ensemble Model的方式,这里把Tree Ensemble Model翻译成树集成模型。

举个例子

img

这里是一个简单的数据集,横坐标表示药物的剂量,绿色表示药物有效,红色表示药物无效。

首先给出来训练后得到的决策树模型,

img

这里每个非叶子节点是一个判断条件,因为数据集比较简单,这里只有一个特征的判断,实际使用过程中会有多个特征。每个叶子节点对应有一个权重,注意这里设定了初始的预测概率是0.5,也就是刚开始我们什么都不知道的时候,认定一种剂量下药物有效的概率是0.5,然后这棵树给出来的是相对于最开始猜想的一个偏移量,比如当Dosage<5时,给出的叶子节点权重是-0.5,那么这个时候我们预测的概率就是0.5+(-0.5)=0,也就是模型输出这个剂量下药物没有用。

多棵树就是有多个这样的决策树,然后预测结果等于所有决策树输出的和。

大概有了这个简单的例子,这里来做一下简单的数学推导。

理论部分

树集成模型

简单来说,树集成模型是由多棵决策树组成的,对于给定输入,树集成模型的预测结果等于每一个决策树预测结果的和。

这里给一下原论文的形式化定义。

给定一个数据集D\mathcal{D},数据集中有nn个训练数据,每条数据有mm个特征,

D={(xi,yi)}(D=n,xiRm,yiR)\mathcal{D} = \{(x_{i}, y_{i})\}(|\mathcal{D}|=n, x_{i}\in\mathbb{R}^{m}, y_{i}\in\mathbb{R})

一个集成了KK颗决策树的树集成模型可以被表示为,

yi^=ϕ(xi)=k=1Kfk(xi),fkF\hat{y_{i}} = \phi(x_{i})=\sum_{k=1}^{K}f_{k}(x_{i}), f_{k}\in\mathcal{F}

这里fk()f_k(\cdot)表示一个决策树对于一个输入的预测结果,F\mathcal{F}表示所有可能的决策树。这里需要注意,一棵决策树由两个方面构成,一是树的形态,就是树长成什么样,有多少分支,深度有多少;二是树对应叶子节点的权重。在推理的时候,决策树把测试样本划分到对应的叶子节点里,这个叶子节点的权值就是测试样本在该决策树下的得分,也就是这里的fk(xi)f_{k}(x_{i}).

如何训练树集成模型

训练模型就是确定模型的参数,在XGBoost的语境中,就是确定决策树的形态,以及决策树叶子节点的权值。

损失函数表示了模型预测值和真实值的差距,如果损失函数到了一个比较小的位置,这里就认为已经训练好模型了。训练模型就是调整模型的参数,使得损失函数越来越小的过程。

一般来说,树集成模型的损失函数定义如下,

L(ϕ)=il(yi^,yi)+kΩ(fk)\mathcal{L}(\phi)=\sum_{i}l(\hat{y_{i}},y_{i})+\sum_{k}\Omega(f_{k})
Ω(f)=γT+12λω2\Omega(f)=\gamma T+\frac{1}{2}\lambda||\omega||^{2}

其中L()\mathcal{L}(\cdot)被称为损失函数,可以看到,损失函数是ϕ\phi的一个函数,注意这里ϕ\phi是树集成模型。第一项il(yi^,yi)\sum_{i}l(\hat{y_{i}},y_{i})表示的是模型的预测值和真实值的差距,第二项kΩ(fk)\sum_{k}\Omega(f_{k})表示的是所有树的复杂程度的累加,在Ω(f) \Omega(f)的展开式中,TT表示某棵树中叶子节点的个数,ω\omega表示某棵树中所有叶子节点权值的和。

简单来说,对于给定的输入xix_{i},模型会有对应的输出ϕ(xi)\phi(x_{i})。给定一个损失函数,得到L(ϕ(xi))\mathcal{L}(\phi(x_{i})),使用数学中求极小值的方法可以得到对应ϕ\phi的参数。

可以想象,上述的解法试图同时求解所有决策树的形态以及权重参数,这样做比较困难。

Gradient Tree Boosting

这里把Gradient Tree Boosting翻译成梯度提升树方法,这是个训练决策树的算法。算法的想法比较简介,既然同时求解所有的参数比较困难,那就一次只求解一棵树对应的参数。进一步,如果每一次求解了一棵树的参数,用后面的树来修正前面树的误差。

给出梯度提升树算法的损失函数,

L(t)=i=1nl(yi,yi^(t1)+ft(xi))+Ω(ft)\mathcal{L}^{(t)} = \sum_{i=1}^{n}l(y_{i}, \hat{y_{i}}^{(t-1)}+f_{t}(x_{i}))+\Omega(f_{t})

这里的损失函数是一个递推公式,和上一节描述的损失函数差别在于,之前的预测值是yi^\hat{y_{i}},目标优化的参数是所有决策树的参数,而这里的预测值是yi^(t1)+ft(xi)\hat{y_{i}}^{(t-1)}+f_{t}(x_{i}),即前t-1棵树的预测结果加上第t棵树的预测结果,目标优化的参数是第t棵树的参数。

梯度提升树方法的大概训练流程可以总结为,依次求解每棵树的参数(主要是确定这棵树的结构,以及树的叶子的权重)。在训练的时候会预先设定一个阈值,例如树的最大深度,或者树再往下分裂之后得到的损失减少到小于某个阈值,则停止训练这棵树,进行下一棵树的训练。

XGBoost

明确正常的梯度提升树之后,XGBoost利用泰勒展开对递推的损失函数进行了近似,然后求解树集成模型的参数。

L(t)\mathcal{L}^{(t)}是相对于ft(xi)f_{t}(x_{i})的一个函数,在这里对L(t)\mathcal{L}^{(t)}进行泰勒展开得到,

L(t)i=1n[l(yi,y^(t1))+gift(xi)+12hift2(xi)]+Ω(ft)\mathcal{L}^{(t)} \simeq{\sum_{i=1}^{n}[l(y_{i},\hat{y}^{(t-1)})+g_{i}f_{t}(x_{i})+\frac{1}{2}h_{i}f_{t}^{2}(x_{i})]+\Omega(f_{t})}

这里gi=l(yi,y^(t1))y^(t1)g_{i}=\frac{\partial l(y_{i},\hat{y}^{(t-1)})}{\partial \hat{y}^{(t-1)}}hi=2l(yi,y^(t1))y^(t1)2h_{i}=\frac{\partial ^{2}l(y_{i},\hat{y}^{(t-1)})}{\partial \hat{y}^{(t-1)^{2}}},即l(yi,y^(t1)l(y_{i},\hat{y}^{(t-1)})y^(t1)\hat{y}^{(t-1)}的一阶导数和二阶导数。利用泰勒展开式,这里将损失函数转化成了一个二次函数,而且这里二次项的系数为正,可以很方便的求得函数的最小值。

再次说明一下ft(xi)f_{t}(x_{i})的含义是第ii个输入在第tt棵树上的输出,这里的求和符号的上限nn表示的是训练数据集总的样本个数。

到这个地方,我们目标求解的是ft()f_t(\cdot),在上式中以变量的形式出现,在二次函数中,在对称轴取到极值。为了简单起见,把常数项也就是与ft()f_t(\cdot)无关的项删掉。

L(t)i=1n[gift(xi)+12hift2(xi)]+Ω(ft)\mathcal{L}^{(t)} \simeq{\sum_{i=1}^{n}[g_{i}f_{t}(x_{i})+\frac{1}{2}h_{i}f_{t}^{2}(x_{i})]+\Omega(f_{t})}

上文提到过,决定ft()f_t(\cdot)的主要是两个纬度,一是这颗树的形态,二是树的叶子节点的权重。这里用数学表达式来表示就是,

i=1nft(xi)=j=1TiIjωj\sum_{i=1}^{n}f_t(x_i) = \sum_{j=1}^{T}\sum_{i\in{I_j}}\omega_{j}

等式左边的意义很明显,就是所有样本在第tt棵树上输出的和,等式右边用另一种方式表达了这个值,第一个求和符号表示所有的叶子节点,第二个求和符号表示被分到每个叶子节点的样本集合,{iiIj}\{i|i\in{I_j}\}表示被分到第jj个叶子节点的样本集合。ωj\omega_{j}代表第jj个叶子节点的权重。可以这么理解,样本分到哪一个叶子节点上表示了树的结构。

把这个细化的表达式带入到泰勒展开近似的损失函数中得到,

Ltj=1T[(iIjgi)wj+12(iIjhi+λ)wj2]+γT\mathcal{L}^{t} \simeq{\sum_{j=1}^{T}[(\sum_{i\in{I_{j}}}g_i)w_j+\frac{1}{2}(\sum_{i\in{I_{j}}}h_i+\lambda)w_j^{2}]}+\gamma T

这里给定树的结构,我们就可以推断出每个叶子节点权重的在近似条件下的最优解,这里给定树的结构隐含在了{iiIj}\{i|i\in{I_j}\}集合中,因为在上式计算的时候需要固定这个集合能进行下一步计算。

最优解记为,

wj=iIjgiiIjhi+λw^{\ast}_{j}=-\frac{\sum_{i\in{I_{j}}}g_{i}}{\sum_{i\in{I_{j}}}h_{i}+\lambda}

至此,已经推导得到了树参数权重的解。有了这个解,回代到损失函数里就得到了最小的损失值。

那么怎么确定树的结构呢?

One of the key problems in tree learning is to find the best split. In order to do so, a split finding algorithm enumerates over all the possible splits on all the features.

答案是枚举,在一个节点那里想要做分裂节点的操作,哪些样本要分到左边,哪些节点要分到右边,XGBoost就把所有的样本按某个特征排序,然后切分,以此确定树的结构,枚举下来算出最小损失值,就作为最优结构。

训练的例子

把文章开始的图用表格表示,

Drug DosageEffectivenessgig_{i}hih_{i}
40-0.50.75
910.50.25
1110.50.25
170-0.50.75

首先明确,对于所有的样本初始预测值是0.5,对应上文公式中的y^(t1)\hat{y}^{(t-1)},负样本的标签是0,正样本的标签是1,对应上文公式中的yiy_{i},在训练的最开始,所有的样本都在叶子节点上。

图示给出刚开始训练时叶子节点的状态,所有训练样本都在叶子结点上,叶子结点以其特征(Drug Dosage)表示:

img

把数据代入到计算wjw^{\ast}_{j}的公式中计算出叶子节点的权重,再带入对应的Lt\mathcal{L}^{t}公式中求得这时的损失,为了方便计算把正则向去掉,即γ=0\gamma =0,得到Lt=0\mathcal{L}^{t}=0.

我们首先尝试把特征从剂量小于15开始分割,于是得到下面这棵树,

img

此时这棵树的损失等于左子树的损失加上右子树的损失,代入数据得到Lt=1.33\mathcal{L}^{t}=-1.33,损失减少了,我们认定这种节点的分裂方式优于所有的节点都在根结点上。

然后依次枚举,求Dosage < 10以及Dosage < 5的情况下损失的减少幅度,取损失减少最多的情况作为节点分裂的方式。然后递归分裂每一个子树,直到每个叶子节点都只有一个样本,或者树的深度到达了事先设定的限制,然后训练停止,决策树训练完毕。

剩下的树训练的方式相同,但是最初的猜测不再是0.5,而是0.5加上所有之前的树的预测的和。

Note

  1. 文中公式大部分来自原论文,但是下述公式是笔者加的,希望对公式的推导和理解有所帮助
i=1nft(xi)=j=1TiIjωj\sum_{i=1}^{n}f_t(x_i) = \sum_{j=1}^{T}\sum_{i\in{I_j}}\omega_{j}
  1. 文中省略了分类问题损失函数一阶导和二阶导的求解,有兴趣可以看这里
  2. 除了利用泰勒展开式的二阶展开求极值之外,XGBoost在实现方面有很多算法和硬件层面的优化,列举如下:
  • 对每个特征的分割决策使用并行策略:首先把每个特征都排序,因为对特征在不同的位置进行分割是独立的(例如上文例子中的Dosage<15和Dosage<10这两个分割点),所以可以使用并行的线程进行计算,从而加速训练的速度。

  • 梯度数据缓存策略:在原始的数据中,样本不是按照特征值顺序存储的,或者对于不同的特征值,样本的存储不是连续的。那么在计算损失函数的时候,取对应的一阶导数和二阶导数时,对内存的访问就不是连续的。XGBoost在实现时会把需要的梯度数据放到一个额外的内存里,使用预取和缓存的方式来提高缓存的命中率,从而提升数据IO的速度。

  • 去中心化内存策略:为了实现去中心化的计算,例如有时候数据量太大无法在一台机器上运行,这里将数据分割成不同的块,然后将所有块存储在磁盘上。在计算过程中,利用一个单独的线程来预取磁盘中的数据,保证运算和取数据可以同时发生。

Reference

  1. 原论文
  2. 例子来源

关于字节移动平台团队

字节跳动移动平台团队(Client Infrastructure)是大前端基础技术行业领军者,负责整个字节跳动的中国区大前端基础设施建设,提升公司全产品线的性能、稳定性和工程效率,支持的产品包括但不限于抖音、今日头条、西瓜视频、火山小视频等,在移动端、Web、Desktop等各终端都有深入研究。

就是现在!客户端/前端/服务端/端智能算法/测试开发 面向全球范围招聘!一起来用技术改变世界,感兴趣可以联系邮箱 chenxuwei.cxw@bytedance.com,邮件主题 简历-姓名-求职意向-期望城市-电话