前言

XGBoost是一种针对GBDT的高效实现算法,其在GBDT的基础上,做了多方面的优化,使其具有更高精度的同时还增加了运行效率。使得其在天池,kaggle等机器学习竞赛中常常被用于刷分,并且其在工业上也很方便落地。虽然后续还有微软的LightGBM针对XGBoost再进行优化,但是这不影响XGBoost在机器学习算法中仍具有举足轻重的地位。XGBoost大概有以下几方面的优化

  1. 相对于GBDT对损失函数使用的一阶泰勒展开,XGBoost使用了二阶泰勒展开,其精度更高
  2. XGBoost在传统的损失函数的基础上增加了正则项,可以有效防止弱学习器的过拟合
  3. 相比于GBDT使用CART回归树做为弱学习器,XGBoost支持多种不同的弱学习器,如线性回归
  4. XGBoost对算法运行效率作了优化,如在决策树的特征选择阶段可以进行并行运算

本文主要介绍其相对GBDT的优化部分,至于与GBDT相同的部分则一笔带过。同时还会介绍XGBoost的代码实现,由于XGBoost有两种代码实现防止,分别为sklearnXGBoost库,本文都会介绍。

注1:本文基于GBDT算法,对此不了解的可以移步本站的GBDT博客,GBDT算法原理及其代码实现

注2:本文主要参考XGBoost作者陈天奇的原始论文PPT,建议各位都去看看。

XGBoost算法推导

首先我们来介绍XGBoost算法优化最大的部分,就是对每一轮弱学习器的训练流程的优化,在AdaBoost的blog中我是采用了先给出算法流程再进行推导的讲法,这里我们反过来,先对算法进行推导,再给出其流程。

GBDT回顾

XGBoost是基于GBDT的,因此我们首先来回顾一下GBDT的流程,当然这里我们写详细一点,把构建决策树的内容也详细写出,因为XGBoost在构建决策树的过程中不是使用决策树原本的构建方法,而是对其进行了改进

输入:训练集T={(xi,yi),i=1,2,,m}T = \{ (x_i, y_i), i = 1,2,\cdots,m \},训练次数KK

输出:强学习器f(x)f(x)

(1) 使用训练集TT训练初始弱学习器f0(x)=h0(x)=minci=1mL(yi,c)f_0(x) = h_0(x) = \min_{c} \sum_{i = 1}^{m} L(y_i,c),其中h0(x)h_0(x)为只有根节点的CART回归树

(2) 对于第kk轮训练,计算负梯度rki=[L(yi,f(xi)))f(xi)]f(x)=fk1(x)r_{ki}=-\Big[\frac{\partial L(y_i,f(x_i)))}{\partial f(x_i)}\Big]_{f(x)=f_{k-1}(x)},得到训练集Tk={(xi,rki),i=1,2,,m}T_k = \{ (x_i, r_{ki}), i = 1,2,\cdots,m \}

(3) 使用训练集TkT_k训练CART回归树hk(x)h_k(x)

(a) 在特征选择阶段,遍历特征与划分(j,s)(j,s),计算

minj,s[minc1xiR1(j,s)L(rki,c1)+minc2xiR2(j,s)L(rki,c1)]\begin{aligned}\min_{j,s}\left[\min_{c_1} \sum_{x_i\in R_1(j,s)} L(r_{ki},c_1) + \min_{c_2}\sum_{x_i\in R_2(j,s)} L(r_{ki},c_1) \right]\end{aligned}

得到最优的特征与划分,进行节点分裂
(b) 完成决策树的构建后,得到JJ个叶子节点,对每个叶子节点计算其取值

ckj=mincxiRkjL(rki,c)c_{kj} = \operatorname*{min}_{c}\sum_{x_i\in R_{kj}} L(r_{ki},c)

(c) 得到CART回归树hk(x)=j=1JckjI(xRkj)h_k(x) = \sum_{j=1}^{J} c_{kj} I(x \in R_{kj}),更新强学习器为fk(x)=fk1(x)+hk(x)f_k(x) = f_{k - 1}(x) + h_k(x)

(4) 经过KK轮训练后得到强学习器f(x)=fK(x)=k=0Khk(x)f(x) = f_K(x) = \sum_{k = 0}^{K} h_k(x)

其实就是GBDTCART树结合起来,对此不熟悉的可以跟GBDT与决策树对比着看。

损失函数二阶泰勒展开

GBDT中,我们对损失函数进行了一阶泰勒展开以构造负梯度,而在XGBoost中我们使用了二阶泰勒展开以提高算法精度。二阶泰勒展开公式为

f(x)f(x0)+f(x0)(xx0)+12f(x0)(xx0)2f(x) \approx f(x_0) + f^{\prime}(x_0)(x-x_0) + \frac{1}{2}f^{\prime \prime}(x_0)(x-x_0)^2

将损失函数L(y,f(x))L(y,f(x))f(x)=fk1(x)f(x) = f_{k-1}(x)处进行二阶泰勒展开

L(y,f(x))L(y,fk1(x))+[L(y,f(x))f(x)]f(x)=fk1(x)(f(x)fk1(x))+12[2L(y,f(x))f2(x)]f(x)=fk1(x)(f(x)fk1(x))2L(y,f(x)) \approx L(y,f_{k-1}(x)) + \Big[\frac{\partial L(y,f(x))}{\partial f(x)}\Big]_{f(x)=f_{k-1}(x)} \left( f(x) - f_{k-1}(x) \right) + \frac{1}{2} \Big[\frac{\partial^2 L(y,f(x))}{\partial f^2(x)}\Big]_{f(x)=f_{k-1}(x)} \left( f(x) - f_{k-1}(x) \right)^2

f(x)=fk(x)f(x) = f_k(x),且fk(x)=fk1(x)+hk(x)f_k(x) = f_{k-1}(x) + h_k(x),得

\begin{align*}\label{2} & L(y,f_k(x)) \approx L(y,f_{k-1}(x)) + \Big[\frac{\partial L(y,f(x))}{\partial f(x)}\Big]_{f(x) = f_{k-1}(x)} \left( f_k(x) - f_{k-1}(x) \right) + \frac{1}{2} \Big[\frac{\partial^2 L(y,f(x))}{\partial f^2(x)}\Big]_{f(x)=f_{k-1}(x)} \left( f_k(x) - f_{k-1}(x) \right)^2 \\ & = L(y,f_{k-1}(x)) + \Big[\frac{\partial L(y,f(x))}{\partial f(x)}\Big]_{f(x)=f_{k-1}(x)} h_k(x) + \frac{1}{2} \Big[\frac{\partial^2 L(y,f(x))}{\partial f^2(x)}\Big]_{f(x)=f_{k-1}(x)} h_k^2(x) \end{align*}

对比GBDT,在XGBoost中,第kk轮训练中,对于数据xix_i,其除了需要计算一阶偏导,还需要计算二阶偏导,记为

gki=L(y,fk1(xi))fk1(xi),hki=2L(y,fk1(xi))fk12(xi)g_{ki} = \frac{\partial L(y,f_{k-1}(x_i))}{\partial f_{k-1}(x_i)},h_{ki} = \frac{\partial^2 L(y,f_{k-1}(x_i))}{\partial f_{k-1}^2(x_i)}

也就是说损失函数可以记为

L(y,fk(xi))L(y,fk1(xi))+gkihk(xi)+12hkihk2(xi)L(y,f_k(x_i)) \approx L(y,f_{k-1}(x_i)) + g_{ki} h_k(x_i) + \frac{1}{2} h_{ki} h_k^2(x_i)

损失函数正则项

根据上文,第kk轮训练的总损失函数可以写为

i=1mL(yi,fk(xi))\sum_{i=1}^{m}L(y_{i},f_{k}(x_{i}))

XGBoost在此基础上加入了正则项Ω(hk)\Omega(h_{k}),对于弱学习器为CART树的情况,正则项为

Ω(hk)=γJ+λ2j=1Jckj2\Omega(h_{k}) = \gamma J + \frac{\lambda}{2} \sum_{j=1}^{J} c_{kj}^{2}

其中JJ是叶子节点个数,ckjc_{kj}是叶子节点取值,γ\gamma为正则化参数,可以控制正则化的强度,hkh_k为本轮训练的弱学习器。

因此,第kk轮训练的总损失函数可以写为

Lk=i=1mL(yi,fk(xi))+Ω(hk)=i=1mL(yi,fk(xi))+γJ+λ2j=1Jckj2L_{k} = \sum_{i=1}^{m}L(y_{i},f_{k}(x_{i})) + \Omega(h_{k}) = \sum_{i=1}^{m}L(y_{i},f_{k}(x_{i})) + \gamma J + \frac{\lambda}{2} \sum_{j=1}^{J} c_{kj}^{2}

结合上述提到的二阶泰勒展开,损失函数可以写为

Lki=1m[L(yi,fk1(xi))+gkihk(xi)+12hkihk2(xi)]+γJ+λ2j=1Jckj2L_{k} \approx \sum_{i=1}^{m} \left[ L(y_i,f_{k-1}(x_i)) + g_{ki} h_k(x_i) + \frac{1}{2} h_{ki} h_k^2(x_i) \right] + \gamma J + \frac{\lambda}{2} \sum_{j=1}^{J} c_{kj}^{2}

由于对与第kk轮训练来说,L(yi,fk1(xi))L(y_i,f_{k-1}(x_i))为常数,因此可以忽略。再注意到,ckjc_{kj}hk(x)h_k(x)的输出,因此我们可以将损失函数改写为ckjc_{kj}的形式,也就是

Lk=j=1J[xiRkjgkickj+12xiRkjhkickj2]+γJ+λ2j=1Jckj2L_{k} = \sum_{j=1}^{J} \left[ \sum_{x_i\in R_{kj}} g_{ki} c_{kj} + \frac{1}{2} \sum_{x_i\in R_{kj}} h_{ki} c_{kj}^2 \right] + \gamma J + \frac{\lambda}{2} \sum_{j=1}^{J} c_{kj}^{2}

整理可得

Lk=j=1J[xiRkjgkickj+12xiRkjhkickj2+λ2ckj2]+γJ=j=1J[xiRkjgkickj+12(xiRkjhki+λ)ckj2]+γJL_{k} = \sum_{j=1}^{J} \left[ \sum_{x_i\in R_{kj}} g_{ki} c_{kj} + \frac{1}{2} \sum_{x_i\in R_{kj}} h_{ki} c_{kj}^2 + \frac{\lambda}{2} c_{kj}^{2} \right] + \gamma J \\ = \sum_{j=1}^{J} \left[ \sum_{x_i\in R_{kj}} g_{ki} c_{kj} + \frac{1}{2} \left( \sum_{x_i\in R_{kj}} h_{ki} + \lambda \right) c_{kj}^{2} \right] + \gamma J

Gkj=xiRkjgki,Hkj=xiRkjhkiG_{kj} = \sum_{x_i\in R_{kj}} g_{ki},H_{kj} = \sum_{x_i\in R_{kj}} h_{ki}

也就是第kk轮训练出的决策树的每个叶子节点中的样本的一阶导与二阶导之和。则损失函数可以简化为

Lk=j=1J[Gkjckj+12(Hkj+λ)ckj2]+γJL_{k} = \sum_{j=1}^{J} \left[ G_{kj} c_{kj} + \frac{1}{2} \left( H_{kj} + \lambda \right) c_{kj}^{2} \right] + \gamma J

损失函数求解

接着我们对损失函数进行求解,我们的目的是计算出JJckjc_{kj}使得损失函数最小,这分别对应了决策树构建的特征选择部分与叶子节点取值部分,也就是说,求解损失函数就可以得到XGBoost特有的决策树构建方法。

首先我们来看叶子节点取值的部分,由于叶子节点取值就包含在损失函数中,我们只需要直接对每个ckjc_{kj}求偏导再令其等于0,对于给定的ckjc_{kj},求偏导得

Lkckj=Gkj+(Hkj+λ)ckj\frac{\partial L_k}{\partial c_{kj}} = G_{kj} + (H_{kj} + \lambda) c_{kj}

注:这里是对所有ckjc_{kj}中的一个求导,不是对全体ckjc_{kj}求导

令其等与0可以解得

ckj=GkjHkj+λc_{kj} = -\frac{G_{kj}}{H_{kj} + \lambda}

至此我们就得到了叶子节点取值的计算公式。

最后就是特征选择的策略了,首先我们将上文得到的计算公式带入损失函数,可以得到

Lk=12j=1JGkj2Hkj+λ+γJL_k = -\frac{1}{2} \sum_{j=1}^{J} \frac{G_{kj}^2}{H_{kj} + \lambda} + \gamma J

现在假设我们要对一个节点进行分裂,对于一个分裂,记原节点的样本集合为RR,左节点的样本集合为RLR_L,右节点的样本集合为RRR_R,那么对于集合RR,我们可以计算其所有样本的一阶与二阶导数之和,分别记为GGHH,也就是

G=xiRgki=xiRL(yi,fk1(xi))fk1(xi),H=xiRhki=xiR2L(yi,fk1(xi))fk12(xi)G = \sum_{x_i\in R} g_{ki} = \sum_{x_i\in R} \frac{\partial L(y_i,f_{k-1}(x_i))}{\partial f_{k-1}(x_i)},H = \sum_{x_i\in R} h_{ki} = \sum_{x_i\in R} \frac{\partial^2 L(y_i,f_{k-1}(x_i))}{\partial f_{k-1}^2(x_i)}

同理,对RLR_LRRR_R也可以计算所有样本的一阶与二阶导数之和,记为GLG_LHLH_LGRG_RHRH_R,且G=GL+GRG = G_L + G_RH=HL+HRH = H_L + H_R。我们的目的是使得损失函数最小,在分裂前损失函数值为

12G2H+λ+γJ=12(GL+GR)2(HL+HR)+λ+γJ-\frac{1}{2} \frac{G^2}{H + \lambda} + \gamma J = -\frac{1}{2} \frac{(G_L + G_R)^2}{(H_L + H_R) + \lambda} + \gamma J

注:这里的损失函数值指该节点的损失函数值,如果该节点是根节点则为整颗决策树的损失函数值

分裂后的损失函数值为

12GL2HL+λ12GR2HR+λ+γ(J+1)-\frac{1}{2} \frac{G_L^2}{H_L + \lambda} - \frac{1}{2} \frac{G_R^2}{H_R + \lambda} + \gamma (J + 1)

我们当然希望进行分裂后的损失函数值尽可能的小,因此我们可以得到目标函数为

score=12(GL+GR)2(HL+HR)+λ+γJ(12GL2HL+λ12GR2HR+λ+γ(J+1))=12GL2HL+λ+12GR2HR+λ12(GL+GR)2(HL+HR)+λ+γscore = -\frac{1}{2} \frac{(G_L + G_R)^2}{(H_L + H_R) + \lambda} + \gamma J - (-\frac{1}{2} \frac{G_L^2}{H_L + \lambda} - \frac{1}{2} \frac{G_R^2}{H_R + \lambda} + \gamma (J + 1)) \\ = \frac{1}{2} \frac{G_L^2}{H_L + \lambda} + \frac{1}{2} \frac{G_R^2}{H_R + \lambda} - \frac{1}{2} \frac{(G_L + G_R)^2}{(H_L + H_R) + \lambda} + \gamma

注1:分裂后会使得叶子节点数+1

注2:我们希望分裂后的损失函数值尽可能小,为什么目标函数要用原节点的损失函数值减去分裂后的损失函数值,而不是直接最小化分裂后的损失函数值呢?

我们可以注意到

12GL2HL+λ12(GL+GR)2(HL+HR)-\frac{1}{2} \frac{G_L^2}{H_L + \lambda} \le -\frac{1}{2} \frac{(G_L + G_R)^2}{(H_L + H_R)}

也就是说在没有正则化项的情况下,进行分裂是一定会使损失函数减小的,而如果其减小的值过小,那么在正则项的影响下,目标函数值就会大于0,则这个分裂一定不会被采纳。也就是说,目标函数的设计会使得在分裂的贡献过小的情况下,将其筛选掉,毕竟没必要为了这一点优化增加模型的复杂度。

我们只需要令上述目标函数最大化即可,在XGBoost中使用贪心算法来求解目标函数的最大值,也就是遍历所有特征及其划分,采用目标函数最大值对应的特征与划分进行节点分裂,当然如果所有特征与划分计算出的目标函数值均大于等于0,那就可以停止了。

补充:这里大家可能会有疑问,传统决策树中求解目标函数中设计了特征与划分,也就是(j,s)(j,s),这里的目标函数中都没有特征与划分,那怎么进行求解呢?

我们举一个简单的例子,例如我们现在从样本集中取两个样本出来,记为样本1与样本2,样本1的特征1取值为1,样本2的特征1取值为2,他们的特征1取值恰好是样本集中特征1取值的最小值跟第二小的值。如果说第三小的值为3,现在我们在2.5处做划分,那我们就会得到一个左集合中为样本1,样本2,右集合是剩下所有样本的分裂,然后我们发现这个分裂及其的优秀,假设这个分裂就是最好的分裂。现在我们来试试用特征2做划分会怎么样,我们会发现,样本1与样本2的特征2取值必须占据最小的两个位置,否则都无法做出上述分裂。

发现问题了吗,我们用不同的特征与划分做出相同的分裂是很难的一件事情,因此,虽然目标函数中没有特征与划分,但是特征与划分可以用分裂来代表,其求解过程为特征与划分-对应分裂-目标函数。

XGBoost算法流程

回归树

经过上述推导,我们可以给出XGBoost回归树的流程

输入:训练集T={(xi,yi),i=1,2,,m}T = \{ (x_i, y_i), i = 1,2,\cdots,m \},训练次数KK,损失函数LL,正则化参数γ\gamma

输出:强学习器f(x)f(x)

(1) 使用训练集TT训练初始弱学习器f0(x)=h0(x)=minci=1mL(yi,c)f_0(x) = h_0(x) = \min_{c} \sum_{i = 1}^{m} L(y_i,c),其中h0(x)h_0(x)为只有根节点的CART回归树

(2) 对于第kk轮训练,对每一个样本计算其损失函数基于fk1(x)f_{k-1}(x)的一阶导数gkig_{ki}与二阶导数hkih_{ki}

(3) 构建CART回归树hk(x)h_k(x)

(a) 进行决策树构建

(a1) 选定一个未经过分裂计算的叶子节点,对于特征aa,初始化GL=0,HL=0G_L = 0,H_L = 0,并计算该节点的一阶二阶导数和GGHH

(a2) 将样本全部放入右节点中,并按特征aa的取值从小到大排序,依次取出样本,并更新

GL=GL+gki,GR=GGLHL=HL+hki,HR=HHRG_L = G_L + g_{ki},G_R = G - G_L \\ H_L = H_L + h_{ki},H_R = H - H_R

(a3) 每次更新都计算一次

score=12GL2HL+λ+12GR2HR+λ12(GL+GR)2(HL+HR)+λ+γscore = \frac{1}{2} \frac{G_L^2}{H_L + \lambda} + \frac{1}{2} \frac{G_R^2}{H_R + \lambda} - \frac{1}{2} \frac{(G_L + G_R)^2}{(H_L + H_R) + \lambda} + \gamma

(a4) 遍历完所有特征后,计算最大scorescore,如果其大于等于0,则该叶子节点不需要继续分裂,返回(a1)。否则,使用最大scorescore对应的特征与划分进行分裂,并生成新的叶子节点

(a5) 若所有叶子节点均被判定为无需继续分裂,则结束(a)

(b) 完成决策树的构建后,得到JJ个叶子节点,对每个叶子节点计算其取值

ckj=GkjHkj+λc_{kj} = -\frac{G_{kj}}{H_{kj} + \lambda}

(c) 得到CART回归树hk(x)=j=1JckjI(xRkj)h_k(x) = \sum_{j=1}^{J} c_{kj} I(x \in R_{kj}),更新强学习器为fk(x)=fk1(x)+hk(x)f_k(x) = f_{k - 1}(x) + h_k(x)

(4) 经过KK轮训练后得到强学习器f(x)=fK(x)=k=0Khk(x)f(x) = f_K(x) = \sum_{k = 0}^{K} h_k(x)

注:这里的并按特征aa的取值从小到大排序会被储存并重复利用,这也是XGBoost优化运算速度的一种方式。

分类树

XGBoost处理分类问题的方法与GBDT相同,这里以多分类问题为例

输入:训练集T={(xi,yi),i=1,2,,m}T = \{ (x_i, y_i), i = 1,2,\cdots,m \},训练次数KK,样本类别数RR,损失函数LL,正则化参数γ\gamma

输出:强学习器F(x)F(x)

(1) 对训练集TT进行独热编码,得到RR个训练集T(r),r=1,2,,RT^{(r)},r = 1,2,\cdots,R

(2) 对于第rr个学习器的训练,使用训练集T(r)T^{(r)}训练初始弱学习器f0(r)(x)=h0(x)=minci=1mL(yi,c)f_0^{(r)}(x) = h_0(x) = \min_{c} \sum_{i = 1}^{m} L(y_i,c),其中h0(x)h_0(x)为只有根节点的CART回归树

(2) 对于第kk轮训练,对每一个样本计算其损失函数基于fk1(r)(x)f_{k-1}^{(r)}(x)的一阶导数gkig_{ki}与二阶导数hkih_{ki}

(3) 构建CART回归树hk(x)h_k(x)

(a) 进行决策树构建

(a1) 选定一个未经过分裂计算的叶子节点,对于特征aa,初始化GL=0,HL=0G_L = 0,H_L = 0,并计算该节点的一阶二阶导数和GGHH

(a2) 将样本全部放入右节点中,并按特征aa的取值从小到大排序,依次取出样本,并更新

GL=GL+gki,GR=GGLHL=HL+hki,HR=HHRG_L = G_L + g_{ki},G_R = G - G_L \\ H_L = H_L + h_{ki},H_R = H - H_R

(a3) 每次更新都计算一次

score=12GL2HL+λ+12GR2HR+λ12(GL+GR)2(HL+HR)+λ+γscore = \frac{1}{2} \frac{G_L^2}{H_L + \lambda} + \frac{1}{2} \frac{G_R^2}{H_R + \lambda} - \frac{1}{2} \frac{(G_L + G_R)^2}{(H_L + H_R) + \lambda} + \gamma

(a4) 遍历完所有特征后,计算最大scorescore,如果其大于等于0,则该叶子节点不需要继续分裂,返回(a1)。否则,使用最大scorescore对应的特征与划分进行分裂,并生成新的叶子节点

(a5) 若所有叶子节点均被判定为无需继续分裂,则结束(a)

(b) 完成决策树的构建后,得到JJ个叶子节点,对每个叶子节点计算其取值

ckj=GkjHkj+λc_{kj} = -\frac{G_{kj}}{H_{kj} + \lambda}

(c) 得到CART回归树hk(x)=j=1JckjI(xRkj)h_k(x) = \sum_{j=1}^{J} c_{kj} I(x \in R_{kj}),更新强学习器为fk(r)(x)=fk1(r)(x)+hk(x)f_k^{(r)}(x) = f_{k - 1}^{(r)}(x) + h_k(x)

(5) 对于第rr个学习器的训练,经过KK轮训练后得到学习器f(r)(x)=fK(r)(x)=k=0Khk(x)f^{(r)}(x) = f_K^{(r)}(x) = \sum_{k = 0}^{K} h_k(x)

(6) 经过RR次GBDT学习器的构建,将RR个GBDT学习器结合为一个向量f(x)=(f(1)(x),f(2)(x),,f(R)(x))f(x) = (f^{(1)}(x),f^{(2)}(x),\cdots,f^{(R)}(x))

(7) 最终输出为f(x)f(x)中取值最大的分量对应的类别,即F(x)=maxrf(r)(x)F(x) = max_r f^{(r)}(x)

XGBoost代码实现

python中,XGBoost算法由XGBoost库实现,由于sklearn在机器学习中常被使用,XGBoost库提供了两种风格的建模流程,一种是XGBoost原生的建模流程,另一种是sklearn风格的建模流程。在这里我们两种建模方法都会讲到,但是在实际使用中,更推荐使用XGBoost原生风格的建模方法,其运算速度会更快,效果更好。以下会使用到的python库有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns

import xgboost as xgb # 原生XGBoost
# sklearn风格接口
from xgboost import XGBClassifier # XGBoost分类
from xgboost import XGBRegressor # XGBoost回归

from sklearn.datasets import load_wine # 红酒数据集
from sklearn.datasets import load_boston # 波士顿房价数据集

from sklearn.model_selection import train_test_split # 切分数据
from sklearn.model_selection import cross_val_score

# 评价指标
from sklearn.metrics import r2_score
from sklearn.metrics import mean_squared_error as MSE
from sklearn.metrics import accuracy_score

from scipy import stats

import warnings
warnings.filterwarnings("ignore")

使用红酒数据集与波士顿房价数据集作为示例数据

1
2
3
# 读取数据
wine = load_wine()
boston = load_boston()

两种建模流程

首先是我们很熟悉的sklearn风格的建模流程,使用XGBClassifierXGBRegressor函数,分别用于分类与回归问题,其导入代码为

1
2
from xgboost import XGBClassifier  # XGBoost分类
from xgboost import XGBRegressor # XGBoost回归

其建模流程代码大概是下面这样

1
2
3
4
# XGBoost
clf = XGBRegressor(n_estimators=100)
clf.fit(Xtrain, Ytrain)
clf.score(Xtest, Ytest)

这里什么数据参数的都先别管,就是看个建模流程,是不是跟sklearn一模一样,交叉验证,网格搜索等也是跟sklearn一样的写法。

接着我们来看XGBoost原生风格的建模流程,其不分分类与回归问题,全部集成在一个函数里,我们直接导入XGBoost库就可以使用

1
import xgboost as xgb  # 原生XGBoost

其建模流程大概如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 使用DMatrix读取数据
dtrain = xgb.DMatrix(Xtrain, Ytrain) # 特征矩阵和标签都传入
dtest = xgb.DMatrix(Xtest, Ytest)

# 参数字典
param = {"objective":'reg:squarederror'
,"booster":'gbtree'
,"eta":0.05
,"gamma":20
,"lambda":3.5
,"alpha":0.2
,"max_depth":4
,"colsample_bytree":0.4
,"colsample_bylevel":0.6
,"colsample_bynode":1}
num_round = 180 # 弱学习器数量

bst = xgb.train(param, dtrain, num_boost_round=num_round) # 使用train进行训练,导入param,训练数据,树的数量
preds = bst.predict(dtest) # 使用predict进行预测
r2_score(Ytest, preds)

由于是新的建模流程,这里我们详细讲讲,具体如下

  1. DMatrix()读取数据
  2. 给出参数字典
  3. train()进行训练
  4. predict进行预测,若需要进行评分,可以使用sklearn.metrics中的评分函数

只要记住这个建模流程,以后当成模板用就可以了。

常用参数

接着我们来介绍XGBoost的参数,需要注意,对于两种风格的建模函数来说,参数名会有一些区别,但是其作用是一样的,下面我们介绍一些常用的参数。

框架参数

首先是XGBoost的框架参数,涉及所用弱学习器损失函数等,常用参数如下

booster

使用的弱学习器,可填{'gbtree','gblinear','dart'},默认为'gbtree',代表梯度提升树,'gblinear'指线性模型,'dart'指抛弃提升树,在建模过程中会抛弃一部分树,用于防止过拟合,一般都是选用梯度提升树。

objective

损失函数,一般结构为问题种类:损失函数,例如reg:squarederror,代表回归问题,使用平方损失函数。这个参数经过更新,如果使用旧版本可能会不适用,具体可以参考XGBoost的官方文档。下面列举一些常用的输入

  • reg:squarederror:回归问题,使用平方损失函数
  • reg:absoluteerror:回归问题,使用绝对值损失函数
  • reg:squaredlogerror:回归问题,使用对数平方损失函数
  • reg:pseudohubererror:回归问题,使用Huber损失函数
  • binary:logistic:二分类问题,使用logistic采用的对数似然损失函数,输出为概率向量
  • multi:softmax:多分类问题,使用softmax采用的最大熵损失函数,使用时需要设置参数num_class,代表类别数
  • multi:softprob:多分类问题,与上一个的区别是输出为概率向量

num_class

进行多分类问题时使用,代表样本类别。

num_boost_round/n_estimators

弱学习器数量,在XGBoost中叫num_boost_round,一般不写入参数字典,而是写在train()中,在sklearn中叫n_estimators,跟其他集成学习算法是一样的。

seed/random_state

随机数种子,用于保证运行结果可复现。

决策树参数

当我们确定弱学习器之后,我们还可以填入弱学习器的参数,由于XGBoost大多数情况下都是使用CART树作为其弱学习器,而且我们也是以决策树作为理论推导的基础,因此我们这里主要关注树相关的参数,需要特别注意的如下

eta/learning_rate

学习率,同AdaBoostGBDT,用于调整模型学习的速度,在XGBoost中叫eta,在sklearn中叫learning_rate,是需要重点调整的参数。

gamma

就是损失函数正则项中的γ\gamma,其主要作用是在进行决策树节点分裂时限制树的生长,其值越大,树的复杂度越低,主要用于防止过拟合。

lambda/reg_lambda

L2正则化参数,就是损失函数正则项中的λ\lambda,用于防止模型过拟合。

alpha/reg_alpha

L1正则化参数,这里需要补充一下,在理论推导时我们给出的正则项为

Ω(hk)=γJ+λ2j=1Jckj2\Omega(h_{k}) = \gamma J + \frac{\lambda}{2} \sum_{j=1}^{J} c_{kj}^{2}

但其实,完整的正则项为

Ω(hk)=γJ+α2j=1Jckj+λ2j=1Jckj2\Omega(h_{k}) = \gamma J + \frac{\alpha}{2} \sum_{j=1}^{J} |c_{kj}| + \frac{\lambda}{2} \sum_{j=1}^{J} c_{kj}^{2}

这里的L1正则化参数就是指的α\alpha,当我们不想使用某个正则项时,只需要将对应的正则化参数设置为0即可,并且,我们还可以同时使用L1与L2正则化。

tree_method

进行决策树分裂时使用的优化计算方法,可填{'auto', 'exact', 'approx', 'hist'},分别代表

  • auto:自动选择,与hist方法相同
  • exact:精确的贪心算法,会完整的遍历所有特征与划分
  • approx:使用量子草图和梯度直方图的近似贪婪算法
  • hist:更快的直方图优化近似贪婪算法

而后就是决策树的参数,主要是用于剪枝的参数,由于XGBoost使用了与CART树不同的构建策略,因此其剪枝参数也会有不同,其主要限制的是遍历特征的数量,常用参数如下

max_depth

最大深度,这与传统的决策树是相同的,也是最实用的剪枝参数。

colsample_bytree

构建树时抽取特征的比例,也就是每次训练新的树前要抽取一部分特征,注意这里是抽取的比例,也就是说其值属于0-1之间,如输入0.9则表示每次训练使用90%的特征,而输入1表示使用全部特征。

colsample_bylevel

构建树的一层时所抽取的特征比例,相比于上面的参数,这个参数的做法是每生成一层,就要重新抽取一次特征,其值也是比例。

colsample_bynode

构建叶子节点时抽取特征的比例,就是每生成一个叶子节点就要重新抽取一次特征。该特征仅XGBoost有!!

min_child_weight

限制叶子节点的HH值,也就是样本二阶导数之和,HH值小于设定值的叶子节点将不会生成。

示例

接着我们用示例数据集跑两个demo,XGBoostsklearn风格的代码都写一遍

1
2
3
4
5
6
7
# 提取特征与标签
data = pd.DataFrame(wine.data)
data.columns = wine.feature_names
target = pd.DataFrame(wine.target)
target.columns = ['class']
# 划分训练集与测试集
Xtrain, Xtest, Ytrain, Ytest = train_test_split(data, target, test_size=0.2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 使用DMatrix读取数据
dtrain = xgb.DMatrix(Xtrain, Ytrain) # 特征矩阵和标签都传入
dtest = xgb.DMatrix(Xtest, Ytest)

# 参数字典
param = {"objective":'multi:softmax'
,"booster":'gbtree'
,"num_class":3
,"eta":0.05
,"gamma":20
,"lambda":3.5
,"alpha":0.2
,"max_depth":4
,"colsample_bytree":0.4
,"colsample_bylevel":0.6
,"colsample_bynode":1
,"seed":1008
}
num_round = 180 # 弱学习器数量

bst = xgb.train(param, dtrain, num_boost_round=num_round) # 使用train进行训练,导入param,训练数据,树的数量
preds = bst.predict(dtest) # 使用predict进行预测
accuracy_score(Ytest, preds)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# XGBoost
clf = XGBRegressor(objective='multi:softmax'
,booster='gbtree'
,num_class=3
,learning_rate=0.05
,gamma=20
,reg_lambda=3.5
,reg_alpha=0.2
,max_depth=4
,colsample_bytree=0.4
,colsample_bylevel=0.6
,random_state=1008
,n_estimators=180
)
clf.fit(Xtrain, Ytrain)
clf.score(Xtest, Ytest)
1
2
3
4
5
6
7
# 提取特征与标签
data = pd.DataFrame(boston.data)
data.columns = boston.feature_names
target = pd.DataFrame(boston.target)
target.columns = ['MEDV']
# 划分训练集与测试集
Xtrain, Xtest, Ytrain, Ytest = train_test_split(data, target, test_size=0.2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 使用DMatrix读取数据
dtrain = xgb.DMatrix(Xtrain, Ytrain) # 特征矩阵和标签都传入
dtest = xgb.DMatrix(Xtest, Ytest)

# 参数字典
param = {"objective":'reg:squarederror'
,"booster":'gbtree'
,"eta":0.05
,"gamma":20
,"lambda":3.5
,"alpha":0.2
,"max_depth":4
,"colsample_bytree":0.4
,"colsample_bylevel":0.6
,"colsample_bynode":1
,"seed":1008
}
num_round = 180 # 弱学习器数量

bst = xgb.train(param, dtrain, num_boost_round=num_round) # 使用train进行训练,导入param,训练数据,树的数量
preds = bst.predict(dtest) # 使用predict进行预测
r2_score(Ytest, preds)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# XGBoost
clf = XGBRegressor(objective='reg:squarederror'
,booster='gbtree'
,learning_rate=0.05
,gamma=20
,reg_lambda=3.5
,reg_alpha=0.2
,max_depth=4
,colsample_bytree=0.4
,colsample_bylevel=0.6
,random_state=1008
,n_estimators=180
)
clf.fit(Xtrain, Ytrain)
clf.score(Xtest, Ytest)

XGBoost调参指南

由于XGBoost参数繁多,其调参过程也较为复杂,这里我们从经验层面介绍一下调参的基本顺序。