XGBoost


本篇笔记是Introduction to Boosted Trees教程的翻译(请注意,属于渣翻系列)

XGBoost是Extreme Gradient Boosting的缩写,而Gradient Boosting起源于Friedman的文章Greedy Function Approximation: A Gradient Boosting Machine,这是一篇关于梯度提升树(gradient boosted trees)的教程,大部分内容基于Tianqi Chen的这些幻灯片,他是XGBoost的原始作者。

梯度提升树已经出现有一段时间了,可以查到许多关于这一主题的资料。本教程将使用监督学习的要素以一种独立的、有条理的方式解释提升树。我们认为这种解释更清晰、更正式,并激发了XGBoost中使用的模型公式。

1. 监督学习的要素

XGBoost用来解决监督学习问题,我们使用训练数据(具有多个特征)\(x_i\)来预测目标变量\(y_i\)。在学习具体的“树”之前,让我们先回顾一下监督学习中的基本要素。

模型和参数

监督学习中的模型通常是指由输入\(x_i\)预测\(y_i\)的数学结构。一个常见的例子是线性模型(linear model),其预测值表示为\(\hat{y}_i = \sum_j \theta_j x_{xij}\),是输入特征与对应权重的线性组合。根据任务的不同(回归或分类),预测值可以有不同的解释。例如,它可以是Logistic回归中,经Logistic函数转换得到的正例的概率,它还可以在我们需要将输出排序时用作排序分数。

参数(Parameter)是我们需要从训练数据中学习的未确定的部分。在线性回归问题中,参数是系数(coefficient) \(\theta\)。一般情况下,我们会用\(\theta\)表示参数(模型中会有很多的参数,我们在这里的定义是草率的)。

目标函数:训练损失 + 正则化

通过对\(y_i\)进行明智的选择,我们可以表达各种各样的任务,比如回归、分类和排序。训练模型的任务是找到能够最好的拟合训练数据\(x_i\)和标签\(y_i\)的最佳参数\(\theta\)。为了训练模型,我们需要定义一个目标函数(objective function),用来度量模型对训练数据的拟合程度。

目标函数的一个显著特征,它由两部分组成:训练损失(training loss)和正则化项(regularization term):

\[obj(\theta) = L(\theta) + \Omega(\theta) \tag{1.1} \]

其中,\(L\)表示训练损失函数,\(\Omega\)表示正则化项。训练损失衡量的是我们的模型对训练数据的预测能力。\(L\)的一个常见选择是均方误差,如下式所示

\[L(\theta) = \sum_i (y_i-\hat{y}_i)^2 \tag{1.2} \]

另一个常用的损失函数是logistic回归使用的logistic损失:

\[L(\theta) = \sum_i[ y_i\ln (1+e^{-\hat{y}_i}) + (1-y_i)\ln (1+e^{\hat{y}_i})] \tag{1.3} \]

人们通常会忘记添加正则项,正则项用来控制模型的复杂度,这有助于我们避免过拟合(overfitting)。听起来有点抽象,所以让我们在下图中考虑一下问题。在输入数据点为图像左上角所示的情况下,要求在视觉上拟合阶跃函数。你认为三种方法中哪一种是最佳拟合?


正确答案用红色标出。请考虑对你来说这在视觉上是否是一个合理的拟合。一般原则是,我们需要一个简单的并且具有预测能力的模型,两者之间的平衡在机器学习中也被称为**偏差-方差平衡**(bias-variance tradeoff)。

为什么介绍一般性原理

上面介绍的要素构成了监督学习的基本要素,它们是机器学习工具包的自然组成部分。例如,你应该能够描述梯度提升树和随机森林之间的差异和共性。以一种形式化的方式理解这个过程也有助于我们理解我们正在学习的目标,以及诸如剪枝(pruning)和平滑(smoothing)这类启发式背后的原因。

2. 决策树集成

我们已经介绍了监督学习的要素,让我们开始学习真正的树模型。首先,让我们了解一下XGBoost的模型选择:决策树集成。树集成模型由一系列的分类回归树(classification and regression trees, CART)组成。下面是一个简单的CART的例子,用以判断某个人是否会喜欢虚构的电脑游戏 X。


我们将一个家庭中的成员分类到不同叶结点中,并在相应的叶结点中给他们分配分数。CART与决策树略有不同,后者的叶结点只包含决策值。在CART中,一个真实的分数与每个叶结点相关联的,这给了我们比分类更丰富的解释。这也涉及到一个有条理的、统一的优化方法,我们将在本教程的后面部分看到。

通常情况下,在实践中,单棵树的预测能力是不足够使用的,实际使用的是集成模型,它将多棵树的预测加在一起。


上图所示是一棵集成了两颗树的树模型。每棵单一树的预测分数相加得到最终的预测分数。如果你仔细看这个例子,一个重要的事实是这两棵树试图互补。在数学上,我们可以把模型写成这种形式

\[\hat{y}_i = \sum_{k=1}^K f_k(x_i), f_k \in \mathcal{F} \tag{2.1} \]

其中,\(K\)表示树的数量,\(f\)表示函数空间\(\mathcal{F}\)中的一个函数,\(\mathcal{F}\)是所有可能的CART的集合。需要优化的目标函数为

\[\text{obj}(\theta) = \sum_i^n l(y_i, \hat{y}_i) + \sum_{k=1}^K \Omega(f_k) \tag{2.2} \]

现在有一个棘手的问题:在随机森林中使用的模型是什么?树集成!所以随机森林和提升树实际上是相同的模型;不同之处在于我们如何训练它们。这意味着,如果你为树集成编写一个预测服务,那么你只需要编写一个,并且它应该对随机森林和梯度提升树都有效。(参见Treelite获取实际示例。)一个例子说明了为什么监督学习的要素很有吸引力。

提升树

现在我们介绍模型,让我们转向模型训练:我们应该怎样学习这些树?答案是,像其他所有的监督学习模型一样:定义一个目标函数,然后优化它!

我们使用下面形式的目标函数(请注意它需要包含训练损失和正则项):

\[\text{obj} = \sum_{i=1}^n l(y_i, \hat{y}_i^{(t)}) + \sum_{i=1}^t\Omega(f_i) \tag{2.3} \]

前面提到过,\(f_i\)表示一棵树,显然,这里的正则化项是由\(t\)棵树的正则化项相加而来的。

加法训练

我们获得了模型,并定义了目标函数,那么接下来就是通过优化目标函数来求得最优参数。

我们的第一个问题是:树模型的参数是什么?能够发现,我们需要学习的是那些函数\(f_i\),每一个都包含了树结构和叶子结点的分数。学习树结构比传统的优化问题要困难得多,在传统的优化问题中,你可以简单地使用梯度。同时学习所有的树是很难的。相反,我们使用一种加法策略:固定我们已经学得的内容,然后每次添加一棵新树。我们把第\(t\)步的预测值写成\(\hat{y}^{(t)}_i\)。于是,我们有

\[\begin{array}{l}\hat{y}_i^{(0)} = 0\\ \hat{y}_i^{(1)} = f_1(x_i) = \hat{y}_i^{(0)} + f_1(x_i)\\ \hat{y}_i^{(2)} = f_1(x_i) + f_2(x_i)= \hat{y}_i^{(1)} + f_2(x_i)\\ \dots\\ \hat{y}_i^{(t)} = \sum_{k=1}^t f_k(x_i)= \hat{y}_i^{(t-1)} + f_t(x_i)\end{array} \tag{2.4}\]

我们已经熟悉CART树,知道如果要确定一棵CART树需要两部分:1)树的结构,这个结构能够将一个给定的样本映射(或者说划分)到一个确定的叶子结点上,其本质也可以看做是一个函数。2)各个叶子结点上的分数。

仍然有个问题:在每一步我们想要哪一棵树?一个自然的想法是添加使我们的目标函数最优化的那一个。

\[\begin{aligned} \text{obj}^{(t)} & = \sum_{i=1}^n l(y_i, \hat{y}_i^{(t)}) + \sum_{i=1}^t\Omega(f_i) \\ & = \sum_{i=1}^n l(y_i, \hat{y}_i^{(t-1)} + f_t(x_i)) + \Omega(f_t) + \mathrm{constant} \end{aligned} \tag{2.5}\]

这里将目标函数的正则化项拆分成了两部分:第\(t\)棵树的正则化项和前\(t-1\)棵树的正则项,由于前\(t-1\)棵树是已知的,所以可用常数(constant)表示。为什么是已知并可用常数表示,看后面就明白了。

\(f_t(x_i)\)是什么?它其实就是\(f_t\)的某个叶子结点的值。而叶子结点的值是可以作为模型的参数的。

如果我们考虑使用均方误差(mean squared error, MSE)作为我们的损失函数,则目标函数将变成

\[\begin{aligned} \text{obj}^{(t)} & = \sum_{i=1}^n (y_i - (\hat{y}_i^{(t-1)} + f_t(x_i)))^2 + \sum_{i=1}^t\Omega(f_i) \\ & = \sum_{i=1}^n [2(\hat{y}_i^{(t-1)} - y_i)f_t(x_i) + f_t(x_i)^2] + \Omega(f_t) + \mathrm{constant} \end{aligned} \tag{2.6}\]

\(MSE\)的形式是友好的,有一个一次项(通常称为残差)和一个二次项。对于其他的损失损失(如logistic损失),就不那么容易得到这么好的形式。所以在一般情况下,我们对损失函数进行二阶泰勒展开:

\[\text{obj}^{(t)} = \sum_{i=1}^n [l(y_i, \hat{y}_i^{(t-1)}) + g_i f_t(x_i) + \frac{1}{2} h_i f_t^2(x_i)] + \Omega(f_t) + \mathrm{constant} \tag{2.7} \]

其中,\(g_i\)\(h_i\)定义为

\[g_i = \partial_{\hat{y}_i^{(t-1)}} l(y_i, \hat{y}_i^{(t-1)}) \tag{2.8} \]

\[h_i = \partial_{\hat{y}_i^{(t-1)}}^2 l(y_i, \hat{y}_i^{(t-1)}) \tag{2.9} \]

看一下这个式子是怎么来的:

假设\(x^t = x^{t-1} + \Delta x\),将\(f(x^t)\)\(x^{t-1}\)处进行二阶泰勒展开,则

\[\begin{aligned} f(x^t) &= f(x^{t-1} + \Delta x) \\ &\approx f(x^{t-1})+f^{'}(x^{t-1})\Delta x + \cfrac{1}{2}f^{''}(x^{t-1}\Delta x^2) \end{aligned}\]

目标函数为:

\[\text{obj}^{(t)} = \sum_{i=1}^n l(y_i, \hat{y}_i^{(t-1)} + f_t(x_i)) + \Omega(f_t) + \mathrm{constant} \]

其中,将\(f_t(x_i))\)看做是\(\Delta x\),将函数\(l\)\(\hat{y}_i^{(t-1)}\)处展开就得到了前面的式\((2.7)\)

观察式\((2.7)\)\(\hat{y}_i^{(t-1)}\)是前\(t-1\)棵树的预测值,是已知可计算的,\(y_i\)是训练样本的真实值,所以\(l(y_i, \hat{y}_i^{(t-1)})\)可以看做是固定的常数;\(constant\)在前面已经提过了,也是常数。我们的目标是让这个目标函数最小化,常数项显然没有什么用,我们可以把它们去掉。

我们将所有的常数(constants)移除之后,具体的,在第\(t\)步的目标变成

\[\sum_{i=1}^n [g_i f_t(x_i) + \frac{1}{2} h_i f_t^2(x_i)] + \Omega(f_t) \tag{2.10} \]

这成为了我们将要添加的新树的优化目标。这个定义的一个重要优势在于目标函数的值只依赖于\(g_i\)\(h_i\)。这就是XGBoost支持自定义损失函数的方式。我们可以优化每一个损失函数,包括logistic回归和pairwise ranking,使用完全相同的求解器,以\(g_i\)\(h_i\)作为输入!

模型复杂度

使用决策树对样本做分类或回归,是从根结点到叶结点的细化过程(回想一下决策过程);落在相同叶结点的样本的预测值是相同的。

我们已经介绍了训练步骤,但是还有一个重要的事情:正则化项!我们需要定义树的复杂度\(\Omega(f)\)。为此,让我们首先细化树的定义\(f(x)\)

\[f_t(x) = w_{q(x)}, w \in R^T, q:R^d\rightarrow \{1,2,\cdots,T\} \tag{2.11} \]

这里,\(w\)是所有叶结点分数的向量,\(q\)是一个函数,将数据点映射到相应的叶子结点索引,\(T\)是叶子结点的数量。

\(q:R^d\rightarrow \{1,2,\cdots,T\}\)表示将一个\(d\)维特征的样本点映射到\(1\)\(T\)的某个值,也就是把它分到某个叶子结点。q(x)其实就代表了树的结构,给定一个样本\(x\),沿着树的不同分支最终将样本分到特定叶结点。\(w_{q(x)}\)就是这棵树对样本\(x\)的预测值,也称为叶权值。这么说可能还不够直观,举个例子,如图


假设样本\(x\)只有年龄和性别两个特征,\(x \in R^2\),其值为\((14, male)\)。如图所示,叶结点数\(T=3\),叶结点分数向量\(w=(w_1,w_2,w_3)=(+2,+0.1,-1)\),根据这个棵树的结构,\(q:R^2\rightarrow \{1,2,3\}\)会将这个样本映射到\(1\),即\(q(x)=1\)。此时有,\(w_{q(x)}=w_1=+2\)

在XGBoost中,我们将复杂度定义为

\[\Omega(f) = \gamma T + \frac{1}{2}\lambda \sum_{j=1}^T w_j^2 \tag{2.12} \]

这里出现了\(\gamma\)\(\lambda\),这是自己定义的,可以认为的设定它们的值,显然,\(\gamma\)越大,表示越希望获得结构简单的树,因为此时对较多叶子节点的树的惩罚越大。\(\lambda\)越大也是越希望获得结构简单的树。

当然,定义模型复杂度的方法不止一种,但是这种方法在实践中效果很好。正则化是大多数树模型工具包不太重视的一部分,或者干脆忽略掉。这是因为树学习的传统处理方法只强调提升“纯度”,而复杂性控制留给启发式。通过正式定义,我们可以更好地了解我们正在学习的东西,并获得在自然环境下表现良好的模型。

结构分数

这是推导过程中神奇的部分。在重新构造树模型之后,我们可以将第\(t\)棵树的目标值写成

\[\begin{aligned} \text{obj}^{(t)} &\approx \sum_{i=1}^n [g_i w_{q(x_i)} + \frac{1}{2} h_i w_{q(x_i)}^2] + \gamma T + \frac{1}{2}\lambda \sum_{j=1}^T w_j^2\\ &= \sum^T_{j=1} [(\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 \end{aligned} \tag{2.13}\]

其中,\(I_j = \{i|q(x_i)=j\}\)表示被分到了第\(j\)个叶子结点上的训练样本的集合。请注意,公式第二行我们改变了求和符号的索引,因为分配到相同叶子结点的所有训练样本具有相同的分数。

我自己看到这里的转换有点晕,于是给自己举了个例子:主要是记住落在相同叶结点的样本的预测值是相同的。

假设有\(n=6\)个样本\(\{x_1,x_2,x_3,x_4,x_5,x_6\}\)\(I_j = \{i|q(x_i)=j\}\)中的\(i\)表示样本的编号,当前这棵树的叶结点数\(T=2\),则有,叶结点分数向量\(w=(w_1,w_2)\)\(j\)表示叶结点的索引\(j=1,2\),我们令样本\(\{x_1,x_2,x_3\}\)落在第一个叶结点,\(\{x_4,x_5,x_6\}\)落在第二个叶结点。则有

\[q(x_1) = q(x_2)= q(x_3) = 1,\quad I_1=\{1,2,3\} \]

\[q(x_4) = q(x_5)= q(x_6) = 2,\quad I_2=\{4,5,6\} \]

落在相同叶结点的样本的预测值是相同的,则

\[w_{q(x_1)} = w_{q(x_2)}= w_{q(x_3)} = w_1 \]

\[w_{q(x_1)} = w_{q(x_2)}= w_{q(x_3)} = w_2 \]

于是

\[\begin{aligned} \sum_{i=1}^n g_i w_{q(x_i)} &= \sum_{i=1}^6 g_i w_{q(x_i)} \\ &= g_1(w_{q(x_1)}) + g_2(w_{q(x_2)}) +g_3(w_{q(x_3)})+g_4(w_{q(x_4)})+g_5(w_{q(x_5)})+g_6(w_{q(x_6)}) \\ & = (g_1 + g_2 +g_3)w_1 + (g_4 + g_5 +g_6)w_2 \\ & = (\sum_{i \in \{1,2,3\}}g_i)w_1 + (\sum_{i \in \{4,5,6\}}g_i)w_2 \\ & = \sum^T_{j=1} [(\sum_{i\in I_j} g_i) w_j] \end{aligned}\]

只做了一部分,但是应该已经很明白了。

我们可以进一步的简写表达式,定义\(G_j = \sum_{i\in I_j} g_i\)\(H_j = \sum_{i\in I_j} h_i\)

\[\text{obj}^{(t)} = \sum^T_{j=1} [G_jw_j + \frac{1}{2} (H_j+\lambda) w_j^2] +\gamma T \tag{2.14} \]

上式中,\(w_j\)是相互独立的,\(G_jw_j+\frac{1}{2}(H_j+\lambda)w_j^2\)是二次型的形式,对于给定的树结构\(q(x)\),求得最佳\(w_j\)和最佳目标值:

\((2.14)\)\(w_j\)求偏导\(\frac{\partial}{\partial w_j}\text{obj}^{(t)}\),并令其等于0,解得\(w_j^\ast\),然后将其代入式\((2.14)\)得到\(\text{obj}^\ast\)

\[w_j^\ast = -\frac{G_j}{H_j+\lambda} \tag{2.15} \]

\[\text{obj}^\ast = -\frac{1}{2} \sum_{j=1}^T \frac{G_j^2}{H_j+\lambda} + \gamma T \tag{2.16} \]

最后一个方程衡量了树结构\(q(x)\)的好坏。

\((2.16)\)衡量第\(t\)棵树的结构好坏。要注意的是,这个值仅仅是用来衡量结构好坏的,可以看出\(\text{obj}^\ast\)只与\(G_j\)\(H_j\)\(T\)有关,而它们又只与树结构有关,与叶子结点的值无关。\(\text{obj}^\ast\)分数越小,代表树的结构越好。


如果所有这些听起来有点复杂,让我们看看这张图,看看分数是如何计算的。基本上,对于给定的树结构,我们将统计值\(g_i\)\(h_i\)放到它们所属的叶结点上,将统计值加在一起,然后使用公式计算树的好坏程度。这个分数类似于决策树中的“纯度”,除此之外它还考虑了模型的复杂度。

请注意,上述结论都基于给定的树结构,就是说假定我们已经知道了树的结构(即知道了树到底长什么样),而一旦知道了树结构,我们就可以求解出最佳叶子结点的值。然而,实际上我们目前还不知道树的结构。

学习树结构

现在我们有了一种方法来衡量一棵树有多好,理想情况下,我们可以枚举所有可能的树,然后选出最好的一棵。在实践中,这很棘手,因此我们将尝试一次优化树的一层。具体来说,我们试着将一个叶子结点一分为二,它得到的分数是

\[Gain = \frac{1}{2} \left[\frac{G_L^2}{H_L+\lambda}+\frac{G_R^2}{H_R+\lambda}-\frac{(G_L+G_R)^2}{H_L+H_R+\lambda}\right] - \gamma \tag{2.17} \]

这个公式可以分解为:1)新的左边叶子结点的得分,2)新的右边叶子结点的得分,3)原始叶子结点的得分,4)新添加的叶子结点的正则化。这里我们可以看到一个重要的结论:如果\(gain\)\(\gamma\)小,我们最好不要再增加分支。这正是基于树的模型中的剪枝技术!通过使用监督学习的原则,我们可以很自然地得出这些技术起作用的原因。

对于真实值数据,我们通常需要寻找最优分割。为了有效地做到这一点,我们将所有实例按顺序排列,如下图所示。


从左到右的扫描足以计算出所有可能的分割方案的结构分数,可以有效地找到最优的分割方案。

一棵树由一层层结点组成,我们可以逐步的学习出最佳的树结构。借鉴ID3/C4.5/CART决策树的做法,

  1. 对于某可行划分,计算划分后的\(\text{obj}^\ast\)
  2. 对于所有可行划分,选择\(\text{obj}^\ast\)降低最小的分隔点。

仍然以判断一个人是否喜欢电脑游戏为例子。最简单的树结构就是一个结点的树。我们可以算出这棵单结点的树的好坏程度\(\text{obj}^\ast\)。假设我们现在想按照年龄将这棵单结点树进行分支,我们需要知道:

  1. 按照年龄分是否有效,也就是是否减少了\(\text{obj}^\ast\)的值;
  2. 如果可分,那么以哪个年龄值(分割点)来分。

遍历所有的分割点,找到最优切分点。

\((2.17)\)中的\(Gain\)实际上表示的是单结点的\(\text{obj}^\ast\)减去该结点切分后得到的两个结点\(\text{obj}^\ast\)之和,根据式\((2.16)\)整理得到。显然,如果\(Gain\)值为正,表示切分后\(\text{obj}^\ast\)的值小于单结点的值,该结点应该切分,\(Gain\)值越大越好。\(Gain\)的左半部分如果小于右侧的\(\gamma\),则\(Gain\)就是负的,表明切分后\(\text{obj}^\ast\)反而变大了,此时最好不要再增加分支。\(\gamma\)在这里实际上是一个临界值,它的值越大,表示我们对切分后\(\text{obj}^\ast\)下降幅度要求越严。

至此,我们得到了最优的树结构,根据前面的描述,确定树结构之后就可以计算出最优的叶子结点了,最终得到了由最优结构和最优叶子结点组成的第\(t\)棵树。

注意

加法树学习的局限性

由于难以枚举所有可能的树结构,所以我们一次添加一个拆分。这种方法在大多数情况下都工作得很好,但也有一些边界情况会因为这种方法而失败。对于这些边缘情况,训练的结果是退化的模型,因为我们一次只考虑一个特征维度。一个例子是Can Gradient Boosting Learn Simple Arithmetic?

关于XGBoost的最后一句话

现在你已经了解了什么是提升树,你可能会问,XGBoost的介绍在哪里?XGBoost正是由本教程中介绍的原则所激发的工具!更重要的是,它的开发基于对系统优化和机器学习原理的深入考虑。这个库的目标是将计算机的计算极限推向极致,以提供一个可伸缩、可移植和精确的库。确保你尝试过它,最重要的是,为社区贡献你的智慧(代码、示例、教程)!

 posted on 2020-06-23 14:26  WarningMessage  阅读(532)  评论(0编辑  收藏  举报