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)。这套做法把纠正错误这件事统一成了一件特别干净的事,去拟合损失函数的负梯度。
我们把模型当前对第 个样本 的预测记作 ,这里 是样本编号, 是这个样本的特征向量, 表示已经长完了 棵树之后的累计预测值。损失函数 衡量预测值和真实标签 之间的差距,它可以是回归问题里的平方损失 ,也可以是分类问题里的交叉熵(还记得第04章吧,我们那时候就把交叉熵和熵那套讲透了)。所谓负梯度,就是对每个样本,把损失函数对当前预测值求偏导再取负号:
这里 是偏导数符号, 表示损失 沿着预测值 这个方向的变化率,前面的负号把它翻转一下,让它指向损失下降最快的方向。负梯度 的大小告诉我们这个样本上模型还差多少,方向告诉我们该怎么补。梯度提升做的事,就是拿这堆 当成伪残差,让第 棵新树 去拟合它们, 就是第 棵树对输入 给出的输出。
这里有个特别巧的细节。如果损失函数恰好取平方损失,那么负梯度正好就是真实残差 。这件事可以亲手算一下,对 求偏导再取负号,得到的就是 。所以最早的残差提升其实就是梯度提升在平方损失下的一个特例,Friedman那套写法等于把残差提升推广到了任意可导损失上。换个损失函数,模型就能自然地切换任务,回归用平方损失,分类用交叉熵,整个框架一行代码都不用大改。
新树长好之后,整个模型的更新是这样的:
是加上第 棵树之后模型对输入 给出的总预测, 是之前的累计预测, 是一个手动设定的小正数,叫学习率或者收缩(shrinkage),它把新树的贡献打一个折扣再加进去。这种一棵棵往里加的形式叫加法模型,第 棵树只负责补前面的不足,不重写前面的成果。我之前看过一本讲集成学习的老教材,里头有一句话我记得特别牢,说加法模型的妙处就在于每一步都站在前人肩膀上,谁也不抢谁的活。
3.XGBoost:把梯度提升做成工业级
梯度提升的思路虽然漂亮,可朴素实现跑起来又慢又容易过拟合。陈天奇2014年提出的XGBoost(Extreme Gradient Boosting的缩写,极端梯度提升)就是针对这些问题做的一套工业级改造,它把梯度提升的精度、速度和稳定性都往前推了一大截,也因此长期称霸Kaggle这类表格数据竞赛。
XGBoost最核心的一处改造,是用了二阶导数。前面梯度提升只用一阶负梯度 去指导新树,XGBoost更进一步,把损失函数对预测值的二阶导数也拿出来:
这个 就是损失函数的二阶导数,也叫Hessian,衡量损失曲线在当前这个预测值附近弯得有多厉害。有了 和 这两样,XGBoost就能在当前预测值附近,把损失函数用泰勒展开近似成一个二次函数,然后用牛顿法直接把它的极小值解出来。一阶只告诉你坡面朝哪个方向斜,二阶还告诉你这个坡有多弯,弯得多就可以放心迈大步,弯得少就要小心。同样一棵树,二阶信息下分裂点挑得更准,叶子权重也算得更精确,整体收敛也更快。
XGBoost给每棵树本身也加了正则化。一棵树的复杂度记作:
是这棵树的叶子总数, 是第 个叶子上的输出权重, 和 都是手动调的超参数。前一项 按叶子数算钱,叶子越多罚得越狠,逼着树别长得太繁。后一项 是叶子权重的L2惩罚,这个L2和我们第03章里讲权重衰减时提的L2范数是同一个东西,目的是把叶子输出压在一个温和的范围内,别让某一个叶子突然长得离谱。把正则化合进来以后,XGBoost选分裂点和算叶子权重,目标就是让损失近似加上正则项这个总目标最小化,每一处都把别过拟合明明白白写进公式里。
落实到叶子上,第 个叶子的最优权重有一个特别干净的闭式解:
是落在这个叶子里的所有样本的一阶导数之和, 是这些样本的二阶导数之和, 还是上面那个L2惩罚强度。这个式子告诉我们,叶子的输出大小由一阶和二阶的比值决定,分母里多了一个 ,相当于把权重往零的方向拽了拽。挑分裂点的时候也用同一套公式算,把一个叶子切成左右两个,看切完之后总目标能下降多少,下降得够多(超过 这个门槛)才切,否则就停。这正好从公式上解释了XGBoost为什么自带剪枝能力,分裂前先掂量一下收益够不够本钱。
XGBoost的工程细节也很讲究。它对每个特征先排好序,把分裂点候选压成一种叫直方图的结构,每次找最优分裂就不用重新扫一遍全部数据,速度大大提升。它还支持特征并行和数据并行,海量特征或者海量数据都能拆给多台机器同时算。它对缺失值也有一套自洽的处理,碰到某个特征缺失就自动学一个默认分裂方向,用户不必先把缺失值填齐再喂给模型。这几条加在一起,让XGBoost在表格数据上稳稳压过传统实现,成了表格数据竞赛里的常胜将军。后来LightGBM在直方图上做了差加速,CatBoost在类别特征上做了巧妙编码,思路都继承了XGBoost这套二阶加正则化的骨架,只是各自在不同的细节上换了一种打法。
4.超参数怎么搭:学习率、树数、深度、采样
XGBoost调得动的好手是哪几个,大概有这么几样要照看好,learning_rate、n_estimators、max_depth、subsample,再加上几样采样的辅助。
learning_rate就是公式里那个 ,每一棵新树加入前打的折扣,常见值在 到 之间。 越小,每一棵树的贡献越温和,整个模型走得更谨慎,过拟合的风险就低,代价是需要更多棵树才能把误差啃完。
n_estimators就是树的总数 ,也就是加法模型里要串多少棵树。它和学习率是绑在一块儿用的。一般经验是,学习率压到 这种偏小的值,n_estimators就给到几百甚至几千。学习率放到 以上,树的数量就相应减少。这俩一起调,模型的拟合能力和过拟合风险都在它们俩的拉扯里。
max_depth限制每棵树最多长几层。深度越大,树能捕捉的交互越复杂,也越容易把训练集里的噪声死记硬背下来。表格任务里max_depth一般给到 到 之间就够了,太深基本就过拟合。
subsample是每棵树训练时随机抽多少比例的样本,常见 到 。它把Bagging那种随机抽样的精神借过来一点,让每棵树看到的数据略有不同,模型整体的方差就降下来了。类似的还有colsample_bytree,控制每棵树随机用多少比例的特征,效果相仿。
调这几个参数的常见套路是,先把学习率压小、树给够,再用max_depth和subsample压方差,最后用早停(在验证集上效果不再提升就提前停止训练)决定到底训多少棵树。这套搭配其实和第36章讲的正则化思路一脉相承,都是想办法让模型该学的学到,不该记的别瞎记。小明前段时间调一个金融风控模型,就是按这个顺序一步步来,先把学习率放到 跑通,再压到 配两千棵树细磨,最后验证集AUC硬是稳稳顶到了 。
5.两个例子:房价和广告点击率
我们拿两个具体场景把前面的东西落实一下。
第一个是房价预测。某城市的二手房数据集里,每条记录是一套房,字段有面积、楼层、朝向、地段、装修情况、建成年份等等。这种数据又杂又非线性,面积对价格的影响在不同地段完全不是一回事,神经网络硬学往往吃力不讨好。XGBoost在这种场合就特别灵,学习率设 ,max_depth给 ,subsample设 ,配合早停,跑出来的MAE(平均绝对误差)通常就是各类模型里最低的那一档。我之前看过一本讲美国一家房产数据网站算法故事的书,里头写到他们早期也是用线性回归估房价,误差大得让经纪人吐槽,后来换成树模型加集成,估值才慢慢靠谱起来,这正是梯度提升的主场。
第二个是广告点击率预测。电商平台上,用户每刷一条商品位,后台要在几毫秒内判断这位用户点不点。这种任务的样本量极大、特征极多,标签还极度不平衡(点击的样本本来就少)。XGBoost处理高维稀疏特征、应对不平衡样本的能力都不错,加上直方图加速,能在工程允许的时间里反复重训。有一部讲互联网公司职场的连续剧里有个情节,新人加班到深夜调广告模型,调的就是这一类东西。现实里大厂的广告系统、推荐系统里,GBDT(梯度提升决策树)加神经网络的混合结构至今仍是主力之一,物流平台预测包裹到达时间、医疗平台根据体检指标判断风险等级,背后也常能见到同一套模型,可见树模型在工业界的分量。
6.小结
今天这篇我们从Bagging和Boosting的分野讲起,把梯度提升的负梯度思路写成了公式,再一路推到XGBoost的二阶导数和正则化,最后串到学习率、树数、深度、采样这几个最常调的超参数。我之前看过一部叫《点球成金》的电影,讲的正是一家棒球队靠数据建模逆袭的故事,里头那种把每个球员、每个局面量化再一点点优化的劲头,其实和今天讲的这一套是同一脉的精神,把误差一点一点啃下去,把模型一步一步往前推。说到这里,熵、L2范数、交叉熵这些老朋友又都串起来了,掌握了梯度提升和XGBoost,你以后碰到任何一份表格数据,心里就有了一条特别稳的基准线。
练习
Q1. 梯度提升里,每一棵新树到底在拟合什么?为什么叫"伪残差"?
每一棵新树拟合的是损失函数对当前预测值的负梯度 ,它指明每个样本上模型还差多少、该往哪补。之所以叫伪残差,是因为当损失取平方损失时负梯度正好等于真实残差 ,而换成别的损失(比如交叉熵)它就只是梯度的负值、不是严格残差,所以叫"伪"残差。这样一套框架对任意可导损失都通用。
Q2. 已知平方损失 ,请手算它的负梯度,验证它等于残差。
,取负号得 ,正好是残差。这说明最早的残差提升其实是梯度提升在平方损失下的一个特例。
Q3. XGBoost 相比朴素梯度提升树,在数学上最核心的两处改造是什么?
一是用了二阶导数(Hessian ),把损失在当前预测值附近用泰勒展开近似成二次函数,用牛顿法解极值,分裂点挑得更准、叶子权重算得更精确、收敛更快;二是给树本身加了正则化 ,按叶子数和叶子权重的 L2 一起罚,把"别过拟合"直接写进目标函数。
Q4.(面试题) 请写出 XGBoost 第 个叶子的最优权重公式 ,并解释分母里的 起什么作用,以及为什么这能说明 XGBoost 自带剪枝。
是该叶子里样本一阶导之和, 是二阶导之和。 是 L2 惩罚强度,它出现在分母里相当于把叶子权重往零拽,防止某个叶子输出过极端。一个叶子要不要再分裂,看分裂后总目标能下降多少,只有下降量超过门槛 (分裂带来的叶子成本)才切,否则就停——这就是预剪枝,所以 XGBoost 天然自带剪枝能力。