作者:字节移动技术——许瑶坤
首先明确一下,XGBoost是一种训练决策树模型的学习方式,准确的说是训练Tree Ensemble Model的方式,这里把Tree Ensemble Model翻译成树集成模型。
举个例子
这里是一个简单的数据集,横坐标表示药物的剂量,绿色表示药物有效,红色表示药物无效。
首先给出来训练后得到的决策树模型,
这里每个非叶子节点是一个判断条件,因为数据集比较简单,这里只有一个特征的判断,实际使用过程中会有多个特征。每个叶子节点对应有一个权重,注意这里设定了初始的预测概率是0.5,也就是刚开始我们什么都不知道的时候,认定一种剂量下药物有效的概率是0.5,然后这棵树给出来的是相对于最开始猜想的一个偏移量,比如当Dosage<5时,给出的叶子节点权重是-0.5,那么这个时候我们预测的概率就是0.5+(-0.5)=0,也就是模型输出这个剂量下药物没有用。
多棵树就是有多个这样的决策树,然后预测结果等于所有决策树输出的和。
大概有了这个简单的例子,这里来做一下简单的数学推导。
理论部分
树集成模型
简单来说,树集成模型是由多棵决策树组成的,对于给定输入,树集成模型的预测结果等于每一个决策树预测结果的和。
这里给一下原论文的形式化定义。
给定一个数据集,数据集中有个训练数据,每条数据有个特征,
一个集成了颗决策树的树集成模型可以被表示为,
这里表示一个决策树对于一个输入的预测结果,表示所有可能的决策树。这里需要注意,一棵决策树由两个方面构成,一是树的形态,就是树长成什么样,有多少分支,深度有多少;二是树对应叶子节点的权重。在推理的时候,决策树把测试样本划分到对应的叶子节点里,这个叶子节点的权值就是测试样本在该决策树下的得分,也就是这里的.
如何训练树集成模型
训练模型就是确定模型的参数,在XGBoost的语境中,就是确定决策树的形态,以及决策树叶子节点的权值。
损失函数表示了模型预测值和真实值的差距,如果损失函数到了一个比较小的位置,这里就认为已经训练好模型了。训练模型就是调整模型的参数,使得损失函数越来越小的过程。
一般来说,树集成模型的损失函数定义如下,
其中被称为损失函数,可以看到,损失函数是的一个函数,注意这里是树集成模型。第一项表示的是模型的预测值和真实值的差距,第二项表示的是所有树的复杂程度的累加,在的展开式中,表示某棵树中叶子节点的个数,表示某棵树中所有叶子节点权值的和。
简单来说,对于给定的输入,模型会有对应的输出。给定一个损失函数,得到,使用数学中求极小值的方法可以得到对应的参数。
可以想象,上述的解法试图同时求解所有决策树的形态以及权重参数,这样做比较困难。
Gradient Tree Boosting
这里把Gradient Tree Boosting翻译成梯度提升树方法,这是个训练决策树的算法。算法的想法比较简介,既然同时求解所有的参数比较困难,那就一次只求解一棵树对应的参数。进一步,如果每一次求解了一棵树的参数,用后面的树来修正前面树的误差。
给出梯度提升树算法的损失函数,
这里的损失函数是一个递推公式,和上一节描述的损失函数差别在于,之前的预测值是,目标优化的参数是所有决策树的参数,而这里的预测值是,即前t-1棵树的预测结果加上第t棵树的预测结果,目标优化的参数是第t棵树的参数。
梯度提升树方法的大概训练流程可以总结为,依次求解每棵树的参数(主要是确定这棵树的结构,以及树的叶子的权重)。在训练的时候会预先设定一个阈值,例如树的最大深度,或者树再往下分裂之后得到的损失减少到小于某个阈值,则停止训练这棵树,进行下一棵树的训练。
XGBoost
明确正常的梯度提升树之后,XGBoost利用泰勒展开对递推的损失函数进行了近似,然后求解树集成模型的参数。
是相对于的一个函数,在这里对进行泰勒展开得到,
这里,,即对的一阶导数和二阶导数。利用泰勒展开式,这里将损失函数转化成了一个二次函数,而且这里二次项的系数为正,可以很方便的求得函数的最小值。
再次说明一下的含义是第个输入在第棵树上的输出,这里的求和符号的上限表示的是训练数据集总的样本个数。
到这个地方,我们目标求解的是,在上式中以变量的形式出现,在二次函数中,在对称轴取到极值。为了简单起见,把常数项也就是与无关的项删掉。
上文提到过,决定的主要是两个纬度,一是这颗树的形态,二是树的叶子节点的权重。这里用数学表达式来表示就是,
等式左边的意义很明显,就是所有样本在第棵树上输出的和,等式右边用另一种方式表达了这个值,第一个求和符号表示所有的叶子节点,第二个求和符号表示被分到每个叶子节点的样本集合,表示被分到第个叶子节点的样本集合。代表第个叶子节点的权重。可以这么理解,样本分到哪一个叶子节点上表示了树的结构。
把这个细化的表达式带入到泰勒展开近似的损失函数中得到,
这里给定树的结构,我们就可以推断出每个叶子节点权重的在近似条件下的最优解,这里给定树的结构隐含在了集合中,因为在上式计算的时候需要固定这个集合能进行下一步计算。
最优解记为,
至此,已经推导得到了树参数权重的解。有了这个解,回代到损失函数里就得到了最小的损失值。
那么怎么确定树的结构呢?
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 Dosage | Effectiveness | ||
|---|---|---|---|
| 4 | 0 | -0.5 | 0.75 |
| 9 | 1 | 0.5 | 0.25 |
| 11 | 1 | 0.5 | 0.25 |
| 17 | 0 | -0.5 | 0.75 |
首先明确,对于所有的样本初始预测值是0.5,对应上文公式中的,负样本的标签是0,正样本的标签是1,对应上文公式中的,在训练的最开始,所有的样本都在叶子节点上。
图示给出刚开始训练时叶子节点的状态,所有训练样本都在叶子结点上,叶子结点以其特征(Drug Dosage)表示:
把数据代入到计算的公式中计算出叶子节点的权重,再带入对应的公式中求得这时的损失,为了方便计算把正则向去掉,即,得到.
我们首先尝试把特征从剂量小于15开始分割,于是得到下面这棵树,
此时这棵树的损失等于左子树的损失加上右子树的损失,代入数据得到,损失减少了,我们认定这种节点的分裂方式优于所有的节点都在根结点上。
然后依次枚举,求Dosage < 10以及Dosage < 5的情况下损失的减少幅度,取损失减少最多的情况作为节点分裂的方式。然后递归分裂每一个子树,直到每个叶子节点都只有一个样本,或者树的深度到达了事先设定的限制,然后训练停止,决策树训练完毕。
剩下的树训练的方式相同,但是最初的猜测不再是0.5,而是0.5加上所有之前的树的预测的和。
Note
- 文中公式大部分来自原论文,但是下述公式是笔者加的,希望对公式的推导和理解有所帮助
- 文中省略了分类问题损失函数一阶导和二阶导的求解,有兴趣可以看这里。
- 除了利用泰勒展开式的二阶展开求极值之外,XGBoost在实现方面有很多算法和硬件层面的优化,列举如下:
-
对每个特征的分割决策使用并行策略:首先把每个特征都排序,因为对特征在不同的位置进行分割是独立的(例如上文例子中的Dosage<15和Dosage<10这两个分割点),所以可以使用并行的线程进行计算,从而加速训练的速度。
-
梯度数据缓存策略:在原始的数据中,样本不是按照特征值顺序存储的,或者对于不同的特征值,样本的存储不是连续的。那么在计算损失函数的时候,取对应的一阶导数和二阶导数时,对内存的访问就不是连续的。XGBoost在实现时会把需要的梯度数据放到一个额外的内存里,使用预取和缓存的方式来提高缓存的命中率,从而提升数据IO的速度。
-
去中心化内存策略:为了实现去中心化的计算,例如有时候数据量太大无法在一台机器上运行,这里将数据分割成不同的块,然后将所有块存储在磁盘上。在计算过程中,利用一个单独的线程来预取磁盘中的数据,保证运算和取数据可以同时发生。
Reference
关于字节移动平台团队
字节跳动移动平台团队(Client Infrastructure)是大前端基础技术行业领军者,负责整个字节跳动的中国区大前端基础设施建设,提升公司全产品线的性能、稳定性和工程效率,支持的产品包括但不限于抖音、今日头条、西瓜视频、火山小视频等,在移动端、Web、Desktop等各终端都有深入研究。
就是现在!客户端/前端/服务端/端智能算法/测试开发 面向全球范围招聘!一起来用技术改变世界,感兴趣可以联系邮箱 chenxuwei.cxw@bytedance.com,邮件主题 简历-姓名-求职意向-期望城市-电话。