2.10 梯度提升与XGBoost

1.从一棵树到一片森林:Bagging和Boosting的差别

说起来,前面几章我们聊的都是神经网络那一套,从线性回归一路讲到多层感知机、卷积和注意力。这些方法在图像、文本、语音这类又大又密的数据上确实厉害,可一旦碰到表格数据(就是Excel里那种一行一条记录、一列一个字段的格式),深度学习常常打不过一种更老牌的模型,决策树以及由它搭出来的集成模型。今天我们就来讲其中最有名的一支,梯度提升以及它的工业级实现XGBoost。

要讲梯度提升,得先理清楚集成学习里的两条主流路子,Bagging和Boosting。Bagging这个词是Bootstrap Aggregating的缩写,中文叫自助聚合。它的做法特别朴素,从训练集里有放回地随机抽若干份子集,每一份子集上各练一棵树,最后让这些树一起投票(分类问题)或者取平均(回归问题)。这种思路的关键在于并行和独立,每棵树各自为政,谁也不依赖谁,最后把分歧平均掉。Bagging这一支最有名的代表是随机森林,它在抽样基础上还顺手对特征也做了一道随机筛选,让每棵树长得更加参差不齐,投票的时候分歧更彻底,方差也就压得更低。

Boosting走的则是串行这条路。每一棵新加进来的树,眼睛盯的是前面所有树加在一起还差在哪儿,专门挑前面犯下的错误去补。一棵补一点,再一棵再补一点,整个队伍的预测效果就这么一点点被推上去。

打个比方。Bagging有点像一家大医院请了十位主任医师会诊,每位大夫各自看一遍病历、各自写下诊断,最后表决,谁也不看谁的意见,靠人多抵消个体偏差。Boosting则像一位老师傅带徒弟,徒弟先上手干,干完师傅检查一遍,把徒弟还没处理好的部分标出来,下一个徒弟专攻这些没处理好的地方,如此一个接一个传下去。这两种思路对待误差的态度其实正好相反,Bagging希望每一棵树的误差互不相干,平均一下就消了。Boosting则希望每一棵树专门盯着前一棵的残差,把误差一点点啃下来。

2.梯度提升:把纠正错误这件事写成一个公式

Boosting这个想法听着挺顺,可一上手就有个问题,什么叫前面犯的错,到底用什么去量化它。最早的AdaBoost用的是错分样本的权重,给分错的样本加权,让下一棵树更重视它们。后来Friedman几个人在1999到2001年那几年想出来一个更通用的办法,叫梯度提升(Gradient Boosting)。这套做法把纠正错误这件事统一成了一件特别干净的事,去拟合损失函数的负梯度。

我们把模型当前对第 ii 个样本 xix_i 的预测记作 y^i(m1)\hat{y}_i^{(m-1)},这里 ii 是样本编号,xix_i 是这个样本的特征向量,m1m-1 表示已经长完了 m1m-1 棵树之后的累计预测值。损失函数 L(yi,y^i)L(y_i,\hat{y}_i) 衡量预测值和真实标签 yiy_i 之间的差距,它可以是回归问题里的平方损失 L=12(yiy^i)2L=\frac{1}{2}(y_i-\hat{y}_i)^2,也可以是分类问题里的交叉熵(还记得第04章吧,我们那时候就把交叉熵和熵那套讲透了)。所谓负梯度,就是对每个样本,把损失函数对当前预测值求偏导再取负号:

gi=L(yi,y^i)y^iy^i=y^i(m1)g_i = -\left.\frac{\partial L(y_i,\hat{y}_i)}{\partial \hat{y}_i}\right|_{\hat{y}_i=\hat{y}_i^{(m-1)}}

这里 \partial 是偏导数符号,L/y^i\partial L/\partial \hat{y}_i 表示损失 LL 沿着预测值 y^i\hat{y}_i 这个方向的变化率,前面的负号把它翻转一下,让它指向损失下降最快的方向。负梯度 gig_i 的大小告诉我们这个样本上模型还差多少,方向告诉我们该怎么补。梯度提升做的事,就是拿这堆 gig_i 当成伪残差,让第 mm 棵新树 fm(x)f_m(x) 去拟合它们,fm(x)f_m(x) 就是第 mm 棵树对输入 xx 给出的输出。

这里有个特别巧的细节。如果损失函数恰好取平方损失,那么负梯度正好就是真实残差 yiy^iy_i-\hat{y}_i。这件事可以亲手算一下,对 L=12(yiy^i)2L=\frac{1}{2}(y_i-\hat{y}_i)^2 求偏导再取负号,得到的就是 yiy^iy_i-\hat{y}_i。所以最早的残差提升其实就是梯度提升在平方损失下的一个特例,Friedman那套写法等于把残差提升推广到了任意可导损失上。换个损失函数,模型就能自然地切换任务,回归用平方损失,分类用交叉熵,整个框架一行代码都不用大改。

新树长好之后,整个模型的更新是这样的:

Fm(x)=Fm1(x)+ηfm(x)F_m(x) = F_{m-1}(x) + \eta \cdot f_m(x)

Fm(x)F_m(x) 是加上第 mm 棵树之后模型对输入 xx 给出的总预测,Fm1(x)F_{m-1}(x) 是之前的累计预测,η\eta 是一个手动设定的小正数,叫学习率或者收缩(shrinkage),它把新树的贡献打一个折扣再加进去。这种一棵棵往里加的形式叫加法模型,第 mm 棵树只负责补前面的不足,不重写前面的成果。我之前看过一本讲集成学习的老教材,里头有一句话我记得特别牢,说加法模型的妙处就在于每一步都站在前人肩膀上,谁也不抢谁的活。

3.XGBoost:把梯度提升做成工业级

梯度提升的思路虽然漂亮,可朴素实现跑起来又慢又容易过拟合。陈天奇2014年提出的XGBoost(Extreme Gradient Boosting的缩写,极端梯度提升)就是针对这些问题做的一套工业级改造,它把梯度提升的精度、速度和稳定性都往前推了一大截,也因此长期称霸Kaggle这类表格数据竞赛。

XGBoost最核心的一处改造,是用了二阶导数。前面梯度提升只用一阶负梯度 gig_i 去指导新树,XGBoost更进一步,把损失函数对预测值的二阶导数也拿出来:

hi=2L(yi,y^i)y^i2y^i=y^i(m1)h_i = \left.\frac{\partial^2 L(y_i,\hat{y}_i)}{\partial \hat{y}_i^2}\right|_{\hat{y}_i=\hat{y}_i^{(m-1)}}

这个 hih_i 就是损失函数的二阶导数,也叫Hessian,衡量损失曲线在当前这个预测值附近弯得有多厉害。有了 gig_ihih_i 这两样,XGBoost就能在当前预测值附近,把损失函数用泰勒展开近似成一个二次函数,然后用牛顿法直接把它的极小值解出来。一阶只告诉你坡面朝哪个方向斜,二阶还告诉你这个坡有多弯,弯得多就可以放心迈大步,弯得少就要小心。同样一棵树,二阶信息下分裂点挑得更准,叶子权重也算得更精确,整体收敛也更快。

XGBoost给每棵树本身也加了正则化。一棵树的复杂度记作:

Ω=γT+12λj=1Twj2\Omega = \gamma T + \frac{1}{2}\lambda \sum_{j=1}^{T} w_j^2

TT 是这棵树的叶子总数,wjw_j 是第 jj 个叶子上的输出权重,γ\gammaλ\lambda 都是手动调的超参数。前一项 γT\gamma T 按叶子数算钱,叶子越多罚得越狠,逼着树别长得太繁。后一项 12λwj2\frac{1}{2}\lambda\sum w_j^2 是叶子权重的L2惩罚,这个L2和我们第03章里讲权重衰减时提的L2范数是同一个东西,目的是把叶子输出压在一个温和的范围内,别让某一个叶子突然长得离谱。把正则化合进来以后,XGBoost选分裂点和算叶子权重,目标就是让损失近似加上正则项这个总目标最小化,每一处都把别过拟合明明白白写进公式里。

落实到叶子上,第 jj 个叶子的最优权重有一个特别干净的闭式解:

wj=GjHj+λw_j^{*} = -\frac{G_j}{H_j+\lambda}

Gj=ijgiG_j=\sum_{i\in j} g_i 是落在这个叶子里的所有样本的一阶导数之和,Hj=ijhiH_j=\sum_{i\in j} h_i 是这些样本的二阶导数之和,λ\lambda 还是上面那个L2惩罚强度。这个式子告诉我们,叶子的输出大小由一阶和二阶的比值决定,分母里多了一个 λ\lambda,相当于把权重往零的方向拽了拽。挑分裂点的时候也用同一套公式算,把一个叶子切成左右两个,看切完之后总目标能下降多少,下降得够多(超过 γ\gamma 这个门槛)才切,否则就停。这正好从公式上解释了XGBoost为什么自带剪枝能力,分裂前先掂量一下收益够不够本钱。

XGBoost的工程细节也很讲究。它对每个特征先排好序,把分裂点候选压成一种叫直方图的结构,每次找最优分裂就不用重新扫一遍全部数据,速度大大提升。它还支持特征并行和数据并行,海量特征或者海量数据都能拆给多台机器同时算。它对缺失值也有一套自洽的处理,碰到某个特征缺失就自动学一个默认分裂方向,用户不必先把缺失值填齐再喂给模型。这几条加在一起,让XGBoost在表格数据上稳稳压过传统实现,成了表格数据竞赛里的常胜将军。后来LightGBM在直方图上做了差加速,CatBoost在类别特征上做了巧妙编码,思路都继承了XGBoost这套二阶加正则化的骨架,只是各自在不同的细节上换了一种打法。

4.超参数怎么搭:学习率、树数、深度、采样

XGBoost调得动的好手是哪几个,大概有这么几样要照看好,learning_rate、n_estimators、max_depth、subsample,再加上几样采样的辅助。

learning_rate就是公式里那个 η\eta,每一棵新树加入前打的折扣,常见值在 0.010.010.30.3 之间。η\eta 越小,每一棵树的贡献越温和,整个模型走得更谨慎,过拟合的风险就低,代价是需要更多棵树才能把误差啃完。

n_estimators就是树的总数 MM,也就是加法模型里要串多少棵树。它和学习率是绑在一块儿用的。一般经验是,学习率压到 0.050.05 这种偏小的值,n_estimators就给到几百甚至几千。学习率放到 0.20.2 以上,树的数量就相应减少。这俩一起调,模型的拟合能力和过拟合风险都在它们俩的拉扯里。

max_depth限制每棵树最多长几层。深度越大,树能捕捉的交互越复杂,也越容易把训练集里的噪声死记硬背下来。表格任务里max_depth一般给到 3388 之间就够了,太深基本就过拟合。

subsample是每棵树训练时随机抽多少比例的样本,常见 0.70.70.90.9。它把Bagging那种随机抽样的精神借过来一点,让每棵树看到的数据略有不同,模型整体的方差就降下来了。类似的还有colsample_bytree,控制每棵树随机用多少比例的特征,效果相仿。

调这几个参数的常见套路是,先把学习率压小、树给够,再用max_depth和subsample压方差,最后用早停(在验证集上效果不再提升就提前停止训练)决定到底训多少棵树。这套搭配其实和第36章讲的正则化思路一脉相承,都是想办法让模型该学的学到,不该记的别瞎记。小明前段时间调一个金融风控模型,就是按这个顺序一步步来,先把学习率放到 0.10.1 跑通,再压到 0.030.03 配两千棵树细磨,最后验证集AUC硬是稳稳顶到了 0.920.92

5.两个例子:房价和广告点击率

我们拿两个具体场景把前面的东西落实一下。

第一个是房价预测。某城市的二手房数据集里,每条记录是一套房,字段有面积、楼层、朝向、地段、装修情况、建成年份等等。这种数据又杂又非线性,面积对价格的影响在不同地段完全不是一回事,神经网络硬学往往吃力不讨好。XGBoost在这种场合就特别灵,学习率设 0.050.05,max_depth给 66,subsample设 0.80.8,配合早停,跑出来的MAE(平均绝对误差)通常就是各类模型里最低的那一档。我之前看过一本讲美国一家房产数据网站算法故事的书,里头写到他们早期也是用线性回归估房价,误差大得让经纪人吐槽,后来换成树模型加集成,估值才慢慢靠谱起来,这正是梯度提升的主场。

第二个是广告点击率预测。电商平台上,用户每刷一条商品位,后台要在几毫秒内判断这位用户点不点。这种任务的样本量极大、特征极多,标签还极度不平衡(点击的样本本来就少)。XGBoost处理高维稀疏特征、应对不平衡样本的能力都不错,加上直方图加速,能在工程允许的时间里反复重训。有一部讲互联网公司职场的连续剧里有个情节,新人加班到深夜调广告模型,调的就是这一类东西。现实里大厂的广告系统、推荐系统里,GBDT(梯度提升决策树)加神经网络的混合结构至今仍是主力之一,物流平台预测包裹到达时间、医疗平台根据体检指标判断风险等级,背后也常能见到同一套模型,可见树模型在工业界的分量。

6.小结

今天这篇我们从Bagging和Boosting的分野讲起,把梯度提升的负梯度思路写成了公式,再一路推到XGBoost的二阶导数和正则化,最后串到学习率、树数、深度、采样这几个最常调的超参数。我之前看过一部叫《点球成金》的电影,讲的正是一家棒球队靠数据建模逆袭的故事,里头那种把每个球员、每个局面量化再一点点优化的劲头,其实和今天讲的这一套是同一脉的精神,把误差一点一点啃下去,把模型一步一步往前推。说到这里,熵、L2范数、交叉熵这些老朋友又都串起来了,掌握了梯度提升和XGBoost,你以后碰到任何一份表格数据,心里就有了一条特别稳的基准线。

练习

Q1. 梯度提升里,每一棵新树到底在拟合什么?为什么叫"伪残差"?

每一棵新树拟合的是损失函数对当前预测值的负梯度 gig_i,它指明每个样本上模型还差多少、该往哪补。之所以叫伪残差,是因为当损失取平方损失时负梯度正好等于真实残差 yiy^iy_i-\hat{y}_i,而换成别的损失(比如交叉熵)它就只是梯度的负值、不是严格残差,所以叫"伪"残差。这样一套框架对任意可导损失都通用。

Q2. 已知平方损失 L=12(yiy^i)2L=\frac12(y_i-\hat{y}_i)^2,请手算它的负梯度,验证它等于残差。

Ly^i=(yiy^i)\frac{\partial L}{\partial \hat{y}_i}=-(y_i-\hat{y}_i),取负号得 gi=Ly^i=yiy^ig_i=-\frac{\partial L}{\partial \hat{y}_i}=y_i-\hat{y}_i,正好是残差。这说明最早的残差提升其实是梯度提升在平方损失下的一个特例。

Q3. XGBoost 相比朴素梯度提升树,在数学上最核心的两处改造是什么?

一是用了二阶导数(Hessian hih_i),把损失在当前预测值附近用泰勒展开近似成二次函数,用牛顿法解极值,分裂点挑得更准、叶子权重算得更精确、收敛更快;二是给树本身加了正则化 Ω=γT+12λwj2\Omega=\gamma T+\frac12\lambda\sum w_j^2,按叶子数和叶子权重的 L2 一起罚,把"别过拟合"直接写进目标函数。

Q4.(面试题) 请写出 XGBoost 第 jj 个叶子的最优权重公式 wj=GjHj+λw_j^*=-\frac{G_j}{H_j+\lambda},并解释分母里的 λ\lambda 起什么作用,以及为什么这能说明 XGBoost 自带剪枝。

GjG_j 是该叶子里样本一阶导之和,HjH_j 是二阶导之和。λ\lambda 是 L2 惩罚强度,它出现在分母里相当于把叶子权重往零拽,防止某个叶子输出过极端。一个叶子要不要再分裂,看分裂后总目标能下降多少,只有下降量超过门槛 γ\gamma(分裂带来的叶子成本)才切,否则就停——这就是预剪枝,所以 XGBoost 天然自带剪枝能力。

相关标签
机器学习梯度提升XGBoostBoosting