
引言
XGBoost是eXtreme Gradient Boosting的缩写称呼,它是一个非常强大的Boosting算法工具包,优秀的性能(效果与速度)让其在很长一段时间内霸屏数据科学比赛解决方案榜首,现在很多大厂的机器学习方案依旧会首选这个模型。
XGBoost在并行计算效率、缺失值处理、控制过拟合、预测泛化能力上都变现非常优秀。本文我们给大家详细展开介绍XGBoost,包含「算法原理」和「工程实现」两个方面。
关于XGBoost的工程应用实践,欢迎大家参考ShowMeAI的另外一篇实战文章 XGBoost工具库建模应用详解。
(本篇XGBoost部分内容涉及到机器学习基础知识、决策树/回归树/GBDT算法,没有先序知识储备的宝宝可以查看ShowMeAI的文章 图解机器学习 | 机器学习基础知识 、决策树模型详解 、回归树模型详解)及图解机器学习 | GBDT模型详解)。
1.算法原理可视化解读
关于XGBoost的原理,其作者陈天奇本人有一个非常详尽的Slides做了系统性的介绍,我们在这里借助于这个资料给大家做展开讲解。
1)监督学习中的一些重要概念
在开始介绍Boosted Tree之前,我们先来回顾一下机器学习中的一些重要的概念。
(1)监督学习核心要素

符号(Notations):xi∈Rd表示训练集中的第i个样本。
模型(Model):对于已知的xi如何预测y^i?
线性模型(Linear Model)
y^i=Σjwjxij(包括线性回归和逻辑回归),预测值y^i根据不同的任务有不同的解释:
-
线性回归(Linear Regression):y^i表示预测的分数。
-
逻辑回归(Logistic Regression):1/(1+e−y^i)预测了实例为正的概率。
-
其他:例如在排名任务中y^i可以是排名分数。

参数(Parameters):需要从数据中学习的东西。
- 线性模型(Linear Model):Θ={wj∣j=1,2,…,d}。
目标函数(Objective function)Obj(Θ)=L(Θ)+Ω(Θ)
训练数据损失函数(Training Loss)L=Σi=1nl(yi,y^i)
-
平方损失(Square Loss):l(yi,y^i)=(yi−y^i)2
-
逻辑损失(Logistic Loss):l(yi,y^i)=yiln(1+e−y^i)+(1−yi)ln(1+ey^i)
正则化项(Regularization):描述了模型的复杂程度。
(2)监督学习进阶知识

Ridge回归:Σi=1n(yi−wTxi)2+λ∣∣w∣∣2
- Ridge是线性模型(Linear Model),用的是平方损失(Square Loss),正则化项是L2 Norm。
Lasso:Σi=1n(yi−wTxi)2+λ∣∣w∣∣1
- Lasso是线性模型(Linear Model),用的是平方损失(Square Loss),正则化项是L1 Norm。
逻辑回归(Logistic Regression):Σi=1n(yiln(1+e−wTxi)+(1−yi)ln(1+ewTxi))+λ∣∣w∣∣2
- 逻辑回归是线性模型(Linear Model),用的是逻辑损失(Logistic Loss),正则化项是L2 Norm。
(3)目标函数及偏差方差权衡
回顾一下目标函数Obj(Θ)=L(Θ)+Ω(Θ),为什么目标函数需要两部分组成呢?

2)回归树(Regression Tree)和集成(Ensemble)
(1)回归树(Regression Tree)
回归树,也就是分类回归树(Classification and Regression Tree)(详情请查看ShowMeAI文章回归树模型详解)
(2)回归树集成(Regression Tree Ensemble)

从上图的左图可以看出,共有5个训练样本。
从上图的中图可以看出,每个叶子节点都有预测值:第一个叶子结点的预测值是2,第二个叶子结点的预测值是0.1,第三个叶子结点的预测值是-1。
- 小男孩被分到第一个叶子结点中,所以小男孩的预测值是2;
- 小女儿被分到第二个叶子结点,她的预测值是0.1;
- 剩余的三个人(样本)被分到第三个叶子结点中,他们的值都是-1。
最终的预测值就是样本在每颗树中所在的叶子结点的预测值的和。
(3)树集成方法
树集成的方法使用非常广泛,像GBM、随机森林等(详见ShowMeAI文章 图解机器学习 | GBDT模型详解 和 图解机器学习 | 随机森林分类模型详解)。多达半数的数据挖掘竞赛通过使用各种各样的树集成方法而获胜。
- 不受输入量纲的影响,因此不需要对特性进行细致的标准化。
- 学习特征之间的高阶交互(高阶交叉特征)。
- 可以扩展,用于不同的行业。
(4)Boosted Tree的模型和参数

模型:假设我们有K棵树:y^i=Σk=1Kfk(xi),fk∈F。其中,F为包含所有回归树的函数空间。
参数:包括每棵树的结构和叶子结点中的分数。或者使用函数当作参数:Θ={f1,f2,…,fK}。
- 这里我们不学习Rd的权重,我们在Boosted Tree中学习函数(树)。
(5)在单变量上学习Boosted Tree
单变量也就是单个特征,通过了解如何在单变量上学习Boosted Tree,我们可以对Boosted Tree的学习模式有个简单的概念。
举例:假设回归树只有一个输入变量t(时间),希望预测小哥在t时刻有多喜欢浪漫音乐。

从上左图可以看到,这位小哥在单身的时候,对浪漫音乐的喜欢程度很低;但是当他遇到了女朋友,随着体内荷尔蒙的分布增加,他对浪漫音乐的喜欢程度增加了;但是有一天分手了,他对浪漫音乐的喜欢程度又变低了。当然,我们也可以发现,上右图的回归树很容易能表达上左图。
为了构建上右图的树,我们需要学习两个东西:

单变量回归树的目标(阶跃函数)
- 训练损失:函数如何拟合点?
- 正则化:如何定义函数的复杂度?(分裂点的个数和每段高度的L2 Norm?)

- 图(1)是小哥在每个时间上对浪漫音乐的喜欢程度的散点图;
- 图(2)可以看到有太多的分割,模型的复杂度很高,所以模型的Ω(f)很高;
- 图(3)可以看到模型的拟合程度很差,所以L(f)很高;
- 图(4)是最好的,无论是拟合程度和复杂程度都非常合适;
(6)一般情形的Boosted Tree
首先回顾上面我们对Boosted Tree的定义:
模型:假设我们有K棵树: y^i=Σk=1Kfk(xi),fk∈F
目标函数:Obj=Σi=1nl(yi,y^i)+Σk=1KΩ(fk)
-
Σi=1nl(yi,y^i)是成本函数
-
Σk=1KΩ(fk)是正则化项,代表树的复杂程度,树越复杂正则化项的值越高(正则化项如何定义我们会在后面详细说)。
当我们讨论树的时候,通常是启发式的:

- 通过信息增益来做分裂
- 修剪树木
- 最大深度
- 平滑叶值
大多数启发式算法都能很好地映射到目标函数,采用形式(目标)视图让我们知道我们正在学习什么:

- 信息增益 → 训练损失
- 修剪 → 对节点的正则化
- 最大深度 - 函数空间上的约束
- 平滑叶片值 - L2正则化对叶片的权重
回归树集成定义了如何得到预测值,它不仅仅可以做回归,同样还可以做分类和排序。具体做什么任务依赖于「目标函数」的定义:
- 使用平方误差:可以得到用于回归问题的gradient boosted machine。
- 使用对数损失:可以得到LogitBoost用于分类。
3)Gradient Boosting(如何学习)
在这一节中我们将正式学习Gradient Boosting。这里,xgboost的处理大家可以对比GBDT模型(可参考ShowMeAI文章 图解机器学习 | GBDT模型详解)来理解核心差异。
(1)解决方案
目标函数:Obj=Σi=1nl(yi,y^i)+Σk=1KΩ(fk)
在做GBDT的时候,我们没有办法使用SGD,因为它们是树,而非数值向量——也就是说从原来我们熟悉的参数空间变成了函数空间。
- 参数空间:学习模型中的权重。
- 函数空间:学习函数f,包括函数的结构和其中的权重。

解决方案:初始化一个预测值,每次迭代添加一个新函数(f):
y^i(0)y^i(1)y^i(2)…y^i(t)=0=f1(xi)=y^i(0)+f1(xi)=f1(xi)+f2(xi)=y^i(1)+f2(xi)=Σk=1tfk(xi)=y^i(t−1)+ft(xi)
-
y^i(t)是第t次迭代的预测值。
-
y^i(t−1)是t−1次迭代的预测值。
-
ft(xi)是第t颗树,也就是我们第t次迭代需要得到的树。
(2)目标函数变换
第一步:根据上面的公式,目标函数可以做如下变形
Obj(t)=Σi=1nl(yi,y^i(t))+Σk=1tΩ(fk)=Σi=1nl(yi,y^i(t−1)+ft(xi))+Ω(ft)+constant
这里我们考虑平方损失,此时目标函数又可以变形为:
Obj(t)=Σi=1n(2(yi−y^i(t−1))ft(xi)+ft(xi)2)+Ω(ft)+constant

第二步:所以我们的目的就是找到ft使得目标函数最低。然而,经过上面初次变形的目标函数仍然很复杂,目标函数会产生二次项。
在这里我们引入泰勒展开公式:
f(x+Δx)≃f(x)+f(x)Δx+1/2f(x)Δx2
目标函数利用泰勒展开式就可以变成:
Obj(t)≃Σi=1n(l(yi,y^i(t−1))+gift(xi)+1/2hift(xi)2)+Ω(ft)+constant
-
gi=∂y^(t−1)l(yi,y^(t−1))
-
hi=∂y^(t−1)2l(yi,y^(t−1))

第三部:把常数项提出来,目标函数可以简化为
Obj(t)≃Σi=1n[gift(xi)+1/2hift(xi)2]+Ω(ft)+constant

思考:为什么要做这么多变化而不直接生成树?
-
理论好处:知道我们在学习什么,收敛。
-
工程好处:回顾监督学习的要素。
- gi,hi都来自损失函数的定义。
- 函数的学习只通过gi,hi依赖于目标函数。
- 当被要求为平方损失和逻辑损失实现Bootsted Tree时,可以考虑如何分离代码模块。
(3)重新定义树
在前面,我们使用ft(x)代表一颗树,在本小节,我们重新定义一下树:我们通过叶子结点中的分数向量和将实例映射到叶子结点的索引映射函数来定义树:(有点儿抽象,具体请看下图)
ft(x)=wq(x),w∈RT,q:Rd→{1,2,3,…,T}
- w代表树中叶子结点的权重
- q代表的是树的结构

从上图可以看出,共有3个叶子结点,第一个叶子结点的权重为+1,第二个叶子结点的权重为0.1,第三个叶子结点的权重为-1;其中,小男孩属于第1个叶子结点,老奶奶属于第3个叶子结点。
(4)定义树的复杂程度
通过下面的式子定义树的复杂程度(定义并不是唯一的)
Ω(ft)=γT+21λΣj=1Twj2

(5)重新审视目标函数

定义在叶子结点j中的实例的集合为:
Ij={i∣q(xi)=j}
根据每棵叶子重新定义目标函数:
Obj(t)≃Σi=1n[gift(xi)+21hift(xi)2]+Ω(ft)+constant=Σi=1n[giwq(xi)+21hiwq(xi)2]+γT+21λΣj=1Twj2+constant=Σj=1T[(ΣiϵIjgi)wj+21(ΣiϵIjhi+λ)wj2]+γT+constant
(6)计算叶子结点的值
一些知识需要先了解。对于一元二次函数:Gx+21Hx2,我们很容易得到这个函数的最小值和最小值对应的x。

-
最小值对应的x相当于求Gx+21Hx2的导数,使导数等于0时的值,即Gx+Hx=0,所以x=−G/H。
-
当x=−G/H,对应的Gx+21Hx2的值为:−21∗G2/H
也就是:
argminxGx+21Hx2minxGx+21Hx2=−HG,H>0=−21HG2
如何求叶子结点最优的值?接着继续变化目标函数:
Obj(t)=Σj=1T[(ΣiϵIjgi)wj+21(ΣiϵIjhi+λ)wj2]+γT+constant=Σj=1T[Gjwj+21(Hj+λ)wj2]+γT+constant

假设树的结构q(x)是固定的,那么每一个叶子结点的权重的最优值为
wj∗=−Gj/(Hj+λ)
目标函数的最优值为
Obj=−21Σj=1THj+λGj2+γT
下图是前面公式讲解对应的一个实际例子。

这里再次总结一下,我们已经把目标函数变成了仅与G、H、γ、λ、T这五项已知参数有关的函数,把之前的变量ft消灭掉了,也就不需要对每一个叶子进行打分了!
那么现在问题来,刚才我们提到,以上这些是假设树结构确定的情况下得到的结果。但是树的结构有好多种,我们应该如何确定呢?
(7)贪婪算法生成树
上一部分中我们假定树的结构是固定的。但是,树的结构其实是有无限种可能的,本小节我们使用贪婪算法生成树:
Gain=21(HL+λGL2+HR+λGR2−HL+HR+λ(GL+GR)2)−γ
-
HL+λGL2是左面叶子结点的值
-
HR+λGR2是右面叶子结点的值
-
HL+HR+λ(GL+GR)2是未分裂前的值
-
γ是引入了多一个的叶子结点增加的复杂度
接下来要考虑的是如何寻找最佳分裂点。
例如,如果xj是年龄,当分裂点是a的时候的增益gain是多少?
我们需要做的仅仅只是计算每一边的g和h,然后计算
Gain=21(HL+λGL2+HR+λGR2−HL+HR+λ(GL+GR)2)−γ

对排序后的实例进行从左到右的线性扫描就足以决定特征的最佳分裂点。
所以,分裂一个结点的方法是:

- 对于每个节点,枚举所有特性
- 对于每个特性,按特性值对实例排序
- 使用线性扫描来决定该特征的最佳分裂点
- 采用所有特征中最好的分裂方案
时间复杂度:
(8)如何处理分类型变量
一些树学习算法分别处理分类变量和连续变量,我们可以很容易地使用我们推导出的基于分类变量的评分公式。但事实上,我们没有必要单独处理分类变量:
我们可以使用one-hot方式处理分类变量:
zj={10 if x is in category j otherwise
如果有太多的分类的话,矩阵会非常稀疏,算法会优先处理稀疏数据。
(9)修剪和正则化
回顾之前的增益,当训练损失减少的值小于正则化带来的复杂度时,增益有可能会是负数:
Gain=21(HL+λGL2+HR+λGR2−HL+HR+λ(GL+GR)2)−γ

此时就是模型的简单性和可预测性之间的权衡。
- 前停止(Pre-stopping):当最佳分裂是负数时,停止分裂;但是一个分裂可能会对未来的分裂有益;
- 后剪枝(Post-Pruning):把一颗树生长到最大深度,递归修剪所有分裂为负增益的叶子。
2.XGBoost核心原理归纳解析
1)目标函数与泰勒展开
XGBoost也是一个Boosting加法模型,每一步迭代只优化当前步中的子模型。
第m步我们有:
Fm(xi)=Fm−1(xi)+fm(xi)
目标函数设计为「经验风险」+「结构风险」(正则项):
Obj=i=1∑NL[Fm(xi),yi]+j=1∑mΩ(fj)=i=1∑NL[Fm−1(xi)+fm(xi),yi]+j=1∑mΩ(fj)
- 正则项Ω(f)表示子模型f的复杂度,用于控制过拟合。

在数学中,我们可以用泰勒公式近似f(x),具体如下式。XGBoost对损失函数运用二阶展开来近似。
f(x0+Δx)≈f(x0)+f′(x0)Δx+2f′′(x0)(Δx)2
(更多数学知识推荐阅读ShowMeAI的AI数学基础系列教程 图解AI数学基础:从入门到精通系列教程)。
对应XGBoost的损失函数,我们在上式里将Fm−1(xi)视作x0,fm(xi)视作Δx,L(yi^,yi)视作关于yi^的函数,得到:
Obj=i=1∑N[L[Fm−1(xi),yi]+∂Fm−1(xi)∂Lfm(xi)+21∂2Fm−1(xi)∂2Lfm2(xi)]+j=1∑mΩ(fj)

又因前m−1个子模型已经确定了,故上式中除了关于fm(x)的部分都是常数,不影响对fm(x)的优化求解。目标函数可转化为:
Obj=i=1∑N[gifm(xi)+21hifm2(xi)]+Ω(fm)
-
gi=∂Fm−1(xi)∂L
-
hi=∂2Fm−1(xi)∂2L
-
这里的L代表损失函数,衡量一次预测的好坏程度
-
在Fm−1(x)确定了的情况下,对每个样本点i都可以轻易计算出一个gi和hi
2)XGBoost的正则化
实际上,XGBoost的基分类器对决策树和线性模型都支持,这里我们只讨论更常见的「基于树」的情况。为防止过拟合,XGBoost设置了基于树的复杂度作为正则项:
Ω(f)=γT+21λ∣∣w∣∣2
作为回归树,叶子节点越多、输出的回归值越大,树的复杂度越高。
最终目标函数如下:
Obj=i=1∑N[gifm(xi)+21hifm2(xi)]+γT+21λj=1∑Twj2

下面是一个数学转换处理,为了使正则项和经验风险项合并到一起。我们把在样本层面上求和的经验风险项,转换为叶节点层面上的求和。
定义节点j上的样本集为I(j)={xi∣q(xi)=j},其中q(xi)为将样本映射到叶节点上的索引函数,叶节点j上的回归值为wj=fm(xi),i∈I(j)。
Obj=j=1∑T[(i∈I(j)∑gi)wj+21(i∈I(j)∑hi+λ)wj2]+γT
进一步简化表达,令∑i∈I(j)gi=Gj,∑i∈I(j)hi=Hj注意这里G和H都是关于j的函数:
Obj=j=1∑T[Gjwj+21(Hj+λ)wj2]+γT

转化到这个形式后,我们可以看出,若一棵树的结构已经确定,则各个节点内的样本(xi,yi,gi,hi)也是确定的,即Gj,Hj、T被确定,每个叶节点输出的回归值应该使得上式最小,由二次函数极值点:
wj∗=−Hj+λGj

按此规则输出回归值后,目标函数值也就是树的评分如下公式,其值越小代表树的结构越好。观察下式,树的评分也可以理解成所有叶节点的评分之和:
Obj∗=j=1∑T(−21Hj+λGj2+γ)
3)节点分裂准则
我们之前文章【决策树模型详解】里给大家讲到了决策树模型是递归生长形成的,而XGBoost的子模型树也一样,需要要依赖节点递归分裂的贪心准则来实现树的生成。
我们之前文章决策树模型详解里给大家讲到了决策树模型是递归生长形成的,而XGBoost的子模型树也一样,需要要依赖节点递归分裂的贪心准则来实现树的生成。
(1)贪心准则
XGBoost子树的基本处理思路和CART一样,会对特征值排序后遍历划分点,将其中最优的分裂收益作为该特征的分裂收益,选取具有最优分裂收益的特征作为当前节点的划分特征,按其最优划分点进行二叉划分,得到左右子树。

上图是一次节点分裂过程,很自然地,分裂收益是树A的评分减去树B的评分。虚线框外的叶节点,即非分裂节点的评分均被抵消,只留下分裂后的LR节点和分裂前的S节点进行比较,因此分裂收益的表达式为:
Gain=21[HL+λGL2+HR+λGR2−HL+HR+λ(GL+GR)2]−γ
(2)近似算法
基于性能的考量,XGBoost还对贪心准则做了一个近似版本,简单说,处理方式是「将特征分位数作为划分候选点」。这样将划分候选点集合由全样本间的遍历缩减到了几个分位数之间的遍历。
展开来看,特征分位数的选取还有global和local两种可选策略:
- global在全体样本上的特征值中选取,在根节点分裂之前进行一次即可;
- local则是在待分裂节点包含的样本特征值上选取,每个节点分裂前都要进行。
这两种情况里,由于global只能划分一次,其划分粒度需要更细。
XGB原始paper中对Higgs Boson数据集进行了实验,比较了精确贪心准则、global近似和local近似三类配置的测试集AUC,用eps代表取分位点的粒度,如eps=0.25代表将数据集划分为1/0.25=4个buckets,发现global(eps=0.05)和local(eps=0.3)均能达到和精确贪心准则几乎相同的性能。
XGBoost工具包支持上述提到的3类配置。
(3)加权分位数
查看目标函数Obj=∑i=1N[gifm(xi)+21hifm2(xi)]+Ω(fm),令偏导为0易得fm∗(xi)=−higi,此目标函数可理解为以hi为权重,−higi为标签的二次损失函数:
Obj=i=1∑N[gifm(xi)+21hifm2(xi)]+Ω(fm)=i=1∑N21hi[fm(xi)−(−higi)]2+Ω(fm)+C
在近似算法取分位数时,实际上XGBoost会取以二阶导hi为权重的分位数(Weighted Quantile Sketch),如下图表示的三分位。

4)列采样与学习率
XGBoost为了进一步优化效果,在以下2个方面进行了进一步设计:
-
列采样
- 和随机森林做法一致,每次节点分裂并不是用全部特征作为候选集,而是一个子集。
- 这么做能更好地控制过拟合,还能减少计算开销。
-
学习率
- 也叫步长、shrinkage,具体的操作是在每个子模型前(即每个叶节点的回归值上)乘上该系数,不让单颗树太激进地拟合,留有一定空间,使迭代更稳定。XGBoost默认设定为0.3。
5)特征缺失与稀疏性
XGBoost也能对缺失值处理,也对特征稀疏问题(特征中出现大量的0或one-hot encoding结果)做了一些优化。XGBoost用「稀疏感知」策略来同时处理这两个问题:
- 简单说,它的做法是将缺失值和稀疏0值等同视作缺失值,将其「绑定」在一起,分裂节点的遍历会跳过缺失值的整体。这样大大提高了运算效率。
0值在XGB中被处理为数值意义上的0还是NA,需要结合具体平台的设置,预处理区分开作为数值的0(不应该被处理为NA)和作为稀疏值的0(应该被处理为NA)。

依然通过遍历得到分裂节点,NA的方向有两种情况,在此基础上对非缺失值进行切分遍历。
如上图所示,若某个特征值取值为1,2,5和大量的NA,XGBoost会遍历以上6种情况(3个非缺失值的切分点×缺失值的两个方向),最大的分裂收益就是本特征上的分裂收益,同时,NA将被分到右节点。
3.XGBoost工程优化
1)并行列块设计
XGBoost将每一列特征提前进行排序,以块(Block)的形式储存在缓存中,并以索引将特征值和梯度统计量对应起来,每次节点分裂时会重复调用排好序的块。而且不同特征会分布在独立的块中,因此可以进行分布式或多线程的计算。

2)缓存访问优化
特征值排序后通过索引来取梯度gi,hi会导致访问的内存空间不一致,进而降低缓存的命中率,影响算法效率。为解决这个问题,XGBoost为每个线程分配一个单独的连续缓存区,用来存放梯度信息。
3)核外块计算
数据量非常大的情形下,无法同时全部载入内存。XGBoost将数据分为多个blocks储存在硬盘中,使用一个独立的线程专门从磁盘中读取数据到内存中,实现计算和读取数据的同时进行。
为了进一步提高磁盘读取数据性能,XGBoost还使用了两种方法:
- ① 压缩block,用解压缩的开销换取磁盘读取的开销。
- ② 将block分散储存在多个磁盘中,提高磁盘吞吐量。
4.XGBoost vs GBDT
我们对之前讲解过的GBDT(参考ShowMeAI文章【GBDT算法详解】)和这里的XGBoost做一个对比总结:

-
GBDT是机器学习算法,XGBoost在算法基础上还有一些工程实现方面的优化。
-
GBDT使用的是损失函数一阶导数,相当于函数空间中的梯度下降;XGBoost还使用了损失函数二阶导数,相当于函数空间中的牛顿法。
-
正则化:XGBoost显式地加入了正则项来控制模型的复杂度,能有效防止过拟合。
-
列采样:XGBoost采用了随机森林中的做法,每次节点分裂前进行列随机采样。
-
缺失值:XGBoost运用稀疏感知策略处理缺失值,GBDT无缺失值处理策略。
-
并行高效:XGBoost的列块设计能有效支持并行运算,效率更优。
更多监督学习的算法模型总结可以查看ShowMeAI的文章 AI知识技能速查 | 机器学习-监督学习。
ShowMeAI相关文章推荐
ShowMeAI系列教程推荐
