2.11 LightGBM与CatBoost

1.话说XGBoost之后,为什么还要有LightGBM和CatBoost

上一章我们聊过XGBoost,它把梯度提升树这件事做得很扎实,精度也漂亮,曾很长一段时间都是表格数据上的王者。说起来,工业界对模型的要求从来不会停在一个点上。数据规模越堆越大,几百万、上千万条样本稀松平常,类别特征也越来越多,省份、商品类目、用户职业,一张表里几十列类别都不稀奇。XGBoost虽然准,可在这种场景下有两个地方让人犯难,一是训练慢,二是类别特征要先做one-hot(把一个有 KK 种取值的列展开成 KK 个取0或1的列),类别一多维度就爆炸,KK 表示该列的取值种类数。

LightGBM和CatBoost就是从这两个痛点各自切入的。LightGBM由微软在2017年放出来,主打一个字,快,同样的数据它常常能比XGBoost快上好几倍,内存还省。CatBoost由俄罗斯的Yandex在同一年开源,主攻的方向是类别特征,省去做人工编码的麻烦,同时顺手处理一个叫目标泄漏的隐患。两家思路不同,但都让Boosting这个门派在工业界站稳了脚跟。下面我分开讲。

2.LightGBM:用直方图把分裂查找做轻

梯度提升树每长一棵新树,都要在每一个叶子节点上挑一个特征、再挑一个分裂点,让分裂之后两边子节点的纯度尽量高。这一步叫分裂查找,是Boosting训练里最耗时的环节。XGBoost最朴素的做法,是把所有样本按特征排序,再逐一尝试不同的分裂点,样本量一大这开销就非常可观。

LightGBM换了个思路,叫直方图算法。它先把每一个连续特征的取值范围切成若干个等宽的小桶(bin),把落在同一个桶里的样本统计成一个总数,再把梯度和与二阶导数和也按桶累加起来。这样一棵树只需要扫一遍桶,桶的数量远比样本数少,分裂查找就从对样本做比较,变成了对桶做比较。设一个连续特征被切成 BB 个桶,BB 表示桶的个数,通常取 B=255B=255,那么不管这个特征上有几十万还是几百万个样本,找分裂点时都只需要在最多 B1B-1 个分界里挑,速度立马上来了。这里 255255 这个数也讲究,刚好能用一个字节来存桶号,既省内存又便于底层优化。

光是这样还不够,LightGBM还有几样配套的小心思。一是Leaf-wise生长。XGBoost默认按层生长(Level-wise),把同一层所有节点都裂开再进入下一层,LightGBM不同,它每次挑当前所有可分裂叶子里增益最大的那一个先裂。这么做同样的树数能学到更深的结构,精度常常更漂亮,代价是如果不加限制容易长得很深从而过拟合,所以要用最大深度(max_depth)和最大叶子数(num_leaves)把它看住。二是GOSS(基于梯度的单边采样),训练时把梯度大的样本全留着,梯度小的样本只随机抽一部分,既保留了难学的样本,又把数据量压下来。三是EFB(互斥特征绑定),把那些很少同时取非零值的稀疏特征绑成一捆,当成一条特征来算,进一步省内存。

我记得看过一本讲算法工程的书,里头有句话说在大规模数据上速度本身就是精度,因为同样的时间你能试更多的方案。这话放在LightGBM上格外贴切。小张第一次拿它跑一份电商点击日志,大概一千二百万条样本、一百二十个特征,同样的树数和深度,训练时间从XGBoost的四十多分钟降到七八分钟,验证集AUC还略微高出零点三个百分点。这个加速,说穿了就是直方图加Leaf-wise共同作用的结果。

3.CatBoost:把类别特征和目标泄漏一起收拾干净

类别特征在Boosting里向来是个麻烦事。你拿省份举例,34个省级行政区,one-hot一下就是34列,模型还得自己学它们之间的关系。要是遇到商品编号、用户ID这种取值成千上万的列,one-hot下来维度直接上天,树的分裂也更费劲。

CatBoost的做法是原生支持类别特征,你只要在建模型时把这一列声明为cat_features,它内部就会做一种叫目标统计(Target Statistics)的编码。具体来说,对一个类别取值 cccc 表示某个具体的类别(比如省份这个列里的浙江),它把这个类别对应的目标值 yy 在训练集里的条件平均 yˉc\bar{y}_c 当作新的数值特征,yy 表示该样本的真实标签。直接这么做有个坑,叫做目标泄漏,就是计算编码时用到了整张训练集的标签,模型可能偷看到不该看的信息,验证集表现虚高,上线一跑效果就差。还记得1.4 节讲信息熵时提到的,那种用统计量构造特征要小心的道理吧,这里是一脉相承的。

为了缓解泄漏,CatBoost用了一种叫Ordered Target Statistics的技巧。它先给训练样本排个随机序,对第 ii 个样本只用排在它前面的那些样本来估 yˉc\bar{y}_cii 表示样本在随机序里的位置,再加一点平滑(拿一个先验值 pp 和权重 α\alpha 拉一下平均),写成 y^c(i)=j<i[xj=c]yj+αpj<i[xj=c]+α\hat{y}_c^{(i)}=\frac{\sum_{j<i}[x_j=c]\,y_j+\alpha p}{\sum_{j<i}[x_j=c]+\alpha},其中 [xj=c][x_j=c] 当样本 jj 的该列取值等于 cc 时取1、否则取0。这里 j<ij<i 表示排在第 ii 个样本之前的样本下标,pp 通常取整个数据集目标值的平均,α\alpha 是一个控制先验强度的小正数(比如取 α=2\alpha=2)。这样一来每个样本的编码只来自它前面的历史,泄漏就被压下去了。直方图这块CatBoost也做,只是它为类别特征量身定制,先把类别按目标统计值排序再分桶,能从容处理取值极多的列。

更值得说道的是它的Ordered Boosting。Boosting每一轮都要算残差(或者说梯度),残差是拿当前模型去预测训练样本得到的。问题在于,同一份数据既要训模型又要算残差,模型容易对自己见过的数据过度自信。CatBoost维护多条随机排列,对第 ii 个样本只用排在它前面的样本训出来的那部分模型去算它的残差,这样残差和当前模型之间就独立了,过拟合的口子进一步收紧。这点有点像连续剧里那种破案套路,侦探复盘案情时,只允许看案发时间之前的线索,后来的证据一概不许偷瞄,结论才站得住。

CatBoost最讨喜的地方大概就是调参少。XGBoost和LightGBM通常要把学习率、深度、叶子数、列采样比例来回试,CatBoost在大多数任务上把迭代次数和学习率设一下,开箱的精度就不错。小明之前做一份信用卡欺诈检测的练习,类别特征十几列,他直接拿CatBoost默认参数跑,AUC就有0.92,调了一下午LightGBM才追平。

4.三家怎么选

把XGBoost、LightGBM、CatBoost放一起对比,说穿了就是三个维度的事。

一是数据量。样本量到百万级以上,LightGBM通常是首选,速度快、内存省。样本量不大(几万到几十万),三家的精度差距很小,XGBoost和CatBoost都能用,CatBoost尤其省心。

二是类别特征。类别列不多、取值也不多,XGBoost做one-hot完全够。类别列多、取值还杂(像商品编号、用户ID这种),CatBoost原生处理更顺手,省去一堆编码和踩坑。LightGBM也支持类别特征,策略相对简单,取值特别多时不如CatBoost稳。

三是调参预算。时间紧、想快出结果,CatBoost默认参数就够看。想榨干最后零点几个百分点的精度,LightGBM和XGBoost可调的旋钮更多,配上网格搜索或贝叶斯优化能再往上拱一点。还有个实务上的习惯,行业里多半是三家都跑一遍,按验证集表现挑最好的那个用,并不存在谁通吃谁。

顺带一提,三家的正则化思路都是L2(对叶子权重加平方惩罚)加上对树结构的约束,这个L2我们在1.3 节讲正则化时已经讲过,含义一致,只是这里作用在叶子权重上,写成 twt2\sum_{t} w_t^2wtw_t 表示第 tt 个叶子的权重,tt 是叶子的编号。叶子权重越受约束,单棵树就越不容易走极端,整套Boosting也就更稳。

5.两个具体的例子

先说电商订单预测。某家电商平台想预测用户下单后30天内会不会复购,拿出来的训练集有三千万条订单,特征里既有价格、浏览时长这种连续量,也有商品一级类目、收货省份、设备类型这种类别量,类别列一共四十多个。小张他们的方案是直接上CatBoost,把类别列声明好,学习率0.03、迭代八千轮、深度8,验证集AUC做到0.88,整轮训练在一台单机(128G内存)上跑两个多小时。改用LightGBM的话需要先做目标编码,工程上多一道工序,精度差不多。这个场景里CatBoost省心的优势体现得很明显。

再说金融风控。某城商行要给小微企业贷款做违约预测,样本量不大,三万条出头,但类别特征密集,行业代码、纳税等级、担保方式,加上一堆征信分箱。小明先用XGBoost对类别列做one-hot,跑出AUC0.78。同样数据换CatBoost,不做任何编码,AUC直接到0.81,多半是因为类别特征处理得更细致,模型没浪费信息。这种表格数据量级不大、类别特征密集的场景,CatBoost确实讨巧。要是换成一份上千万条的物流延误预测(特征里有始发城市、承运商编号、货物类别这些高基数列),那就轮到LightGBM登场了,CatBoost在这种量级上训练慢得多,反而LightGBM能在一个下午出结果。

Boosting三件套就先讲到这儿,下一篇我们换个方向,聊聊SVM和支持向量那一套老而弥坚的思路。

练习

Q1. LightGBM 的直方图算法为什么比朴素 XGBoost 快?它把"比较样本"换成了"比较什么"?

它先把每个连续特征切成若干个等宽小桶(通常 B=255B=255),把落在同一桶里的样本数、梯度和、二阶导数和都按桶累加。这样找分裂点时不再逐一比较几十万个样本,而是在最多 B1B-1 个分界里挑,速度立马上来。255 这个数还能用一个字节存桶号,既省内存又便于底层优化。

Q2. CatBoost 处理类别特征时用的"目标统计"是什么?直接拿整张训练集算条件平均为什么有坑?

目标统计把类别取值 cc 对应的目标值 yy 在训练集里的条件平均 yˉc\bar{y}_c 当作新的数值特征。直接用整张训练集算会埋下"目标泄漏"——计算编码时偷看了不该看的标签,验证集表现虚高、上线就垮。CatBoost 用 Ordered Target Statistics:给样本排随机序,第 ii 个样本只用排在它前面的样本估 yˉc\bar{y}_c,再加点先验平滑,泄漏就被压下去了。

Q3. LightGBM 的 Leaf-wise 生长和 XGBoost 默认的 Level-wise 有什么区别?Leaf-wise 为什么容易过拟合?

Level-wise 按层长,把同一层所有节点都裂开再进下一层;Leaf-wise 每次只挑当前增益最大的那一个叶子先裂。同样树数 Leaf-wise 能学到更深、精度常更漂亮,但如果不加限制会一直往深里钻,所以要用 max_depth 和 num_leaves 看住它,否则容易过拟合。

Q4.(面试题) Boosting 训练里"同一份数据既训模型又算残差"会带来什么问题?CatBoost 的 Ordered Boosting 怎么解决?

问题在于模型对自己见过的数据过度自信,算出来的残差有偏、过拟合的口子收不紧。Ordered Boosting 维护多条随机排列,对第 ii 个样本只用排在它前面样本训出来的那部分模型去算它的残差,这样残差和当前模型相互独立,就像侦探复盘案情只许看案发时间之前的线索,结论才站得住,过拟合被进一步压住。

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