1204.机器学习-集成学习-2.Boosting-4.CatBoost

表格里一堆「城市 / 店铺 / 用户 ID」这类高基数类别:若用全表目标均值做编码,训练时容易把验证标签的味道渗进特征。GBDT 能吃数值,但类别怎么进树、怎么避免泄漏,仍是实务痛点。

CatBoost(Category Boosting,Yandex,约 2017)仍是梯度提升树,主打两件事:原生类别特征 + 有序提升(ordered boosting) 降低目标泄漏;默认参数往往较稳,推理也偏快。

段末注释:CatBoost 与 XGBoost、LightGBM 同属 GBDT 工程实现族;发表时段与 LightGBM 接近(均为 2017),本系列按公开材料时间排在 XGBoost 之后、LightGBM 之前。后文沿用 CatBoost。

配图目录:./1204.机器学习-集成学习-2.Boosting-4.CatBoost/


1. 一句话定位

维度 一句话
学习范式 监督学习;GBDT 族(约 2017)
输入 → 输出 数值 + 类别列 → 多棵对称树分数之和
在优化什么 提升损失;用有序原则估计类别统计与残差,减轻「用未来标签编码现在」

出现背景:Yandex 团队约 2017 提出(如 Fighting biases with dynamic boosting,arXiv:1706.09516;类别特征专论见 NeurIPS 2018 CatBoost 文)。当时高基数类别常用全表目标统计,易渗进标签信息;CatBoost 用有序提升 / 有序统计降低这类目标泄漏,并强化原生类别支持。

比喻:算某个品类的「历史平均点击率」时,只准看排在当前样本之前的记录,不准翻后面的答案——考试只能参考已做过的卷子,不能偷看尚未开封的。

图 1 全表目标编码 vs 有序统计


2. 直觉:相对前代多了什么

发展坐标(本系列):AdaBoost(改权重)→ GBDT(拟合残差)→ XGBoost(正则 + 二阶)→ CatBoost(类别 + 有序) → LightGBM(直方图加速)→ NGBoost(分布参数)。

CatBoost 相对「朴素 GBDT / 粗糙目标编码」:

  1. Ordered boosting:按排列构造,减少残差估计中的偏置与泄漏。
  2. 类别组合与统计:类别可直接声明;内部用有序目标统计等,少做爆炸 one-hot。
  3. 对称树(oblivious tree):同一层共用分裂条件,结构规整,利于速度与正则。

3. 核心链路

主链路仍是加法树:$F_M(x)=\sum_m \nu f_m(x)$。
差异在 $f_m$ 怎么用类别、怎么估梯度。教学上把两件事分开看:

  1. 有序目标统计(生成 $\phi$):把类别编成数值特征。对样本 $t$,只用排列中更早的同类别真实标签 $y$ 估均值(加先验平滑)。同一类别、不同行的 $\phi_t$ 可以不同。
  2. 其后的提升迭代:本篇手算里,$\phi$ 在首轮遍历算完后即冻结,当作普通数值特征送进各轮树;每轮变的是残差 $r$、树 $f_m$ 与预测 $F$,不再用 $y$ 重算 $\phi$。
  3. 有序提升(ordered boosting,实现层):完整 CatBoost 还会在估计残差/梯度时尽量「只看过去」,那是另一套防泄漏,不等于每轮改写 $\phi$。本篇手算不展开该层。
  4. 读参数:depthlearning_rateiterations + 早停;cat_features 声明类别列。

抓住一句:先有序地生成类别编码特征,再(在教学简化下)用固定 $\phi$ 做提升;编码与提升都尽量「只看过去」。


4. 手算完整实例:有序类别统计 + 2 轮提升

A. 问题与原始表

类别「城市」+ 数值点击标签 $y\in{0,1}$(示意)。排列顺序按下表行序(先出现的在前)。

序号 $t$ 城市 $y$
1 0
2 1
3 1
4 1
5 0

B. 初始化

有序目标统计:对样本 $t$,只用更早的同城样本估均值,再加先验平滑。先验取全局均值 $m=\frac{3}{5}=0.6$,平滑强度 $a=1$:

$$
\phi_t=\frac{\sum_{t’<t,,c_{t’}=c_t} y_{t’} + a,m}{n_{t,<}+a}
$$

$F_0=0.6$;$\nu=1$。第 1、2 轮在冻结的 $\phi$ 上各训一棵 stump:叶值 = 该叶残差均值;阈值不事先拍脑袋,而由「相邻 $\phi$ 中点」候选里选出使拟合后 $\sum(r-f)^2$ 最小的那一刀(平方损失下 GBDT 式教学简化;非 CatBoost 全量实现)。

C. 训练过程

先算每行 $\phi_t$:

$t$ 城市 更早同城个数 $n_{<}$ 更早同城 $\sum y$ $\phi_t=\dfrac{\sum y + a m}{n_{<}+a}$
1 $0$ $0$(尚无历史) $(0+0.6)/(0+1)=0.60$
2 $0$ $0$ $0.60$
3 $1$(仅 $t_1$) $y_1=0$ $(0+0.6)/(1+1)=0.30$
4 $2$($t_1,t_3$) $0+1=1$ $(1+0.6)/(2+1)\approx0.53$
5 $1$(仅 $t_2$) $y_2=1$ $(1+0.6)/(1+1)=0.80$

对 $t_3$:同城历史就是 $t_1$($y_1=0$),所以分子是「历史点击和 $0$ + 先验 $0.6$」,分母是「历史条数 $1$ + 平滑 $a=1$」。若没有 $t_1$,会退化成和 $t_1$ 一样的 $(0+0.6)/1=0.60$,而不是 $0.30$。

$\phi$ 与后面提升轮次的关系(本篇约定)

  • 上表是首轮遍历:用真实标签 $y$ 按有序规则生成编码特征 $\phi_t$($\phi$ 不是预测值 $F$)。
  • 生成完毕后,把各行的 $\phi_t$ 固定下来,后面第 1、2 轮 stump 都读同一列 $\phi$,只更新 $r$、$f_m$、$F$。
  • 因此「沪」在 $t_1/t_3/t_4$ 上 $\phi$ 不同,是编码阶段造成的;进入提升后不会每轮再改这些 $\phi$。

若用全表目标编码,「沪」均值 $(0+1+1)/3\approx0.67$ 会写进第 1 行——把后面标签味道提前泄露;有序统计避免这一点。

如何定 stump 阈值(每轮都做):将本轮用到的 $\phi$ 去重排序得 ${0.30,,0.53,,0.60,,0.80}$,在相邻取值的中点形成候选

$$
\tau\in{0.415,;0.565,;0.70}
$$

(即 $(0.30+0.53)/2$、$(0.53+0.60)/2$、$(0.60+0.80)/2$)。对每个 $\tau$:左叶 $\phi<\tau$、右叶 $\phi\geq\tau$,叶值取该叶残差均值,再算拟合后的 $\sum_i(r_i-f(x_i))^2$,取最小者为 $\tau^*$。任意落在同一相邻区间内的阈值(例如旧写法 $0.55$)与 $0.565$ 分法相同;下文写中点,避免「数字从哪来」不清。

第 1 轮:残差 $r=y-F_0$($t_1$:$0-0.6=-0.6$)。候选比较:

候选 $\tau$ 左叶 右叶 $f^{\mathrm{L}}$ $f^{\mathrm{R}}$ 拟合后 $\sum(r-f)^2$
$0.415$ $t_3$ $t_1,t_2,t_4,t_5$ $+0.40$ $-0.10$ $1.00$
$0.565$ $t_3,t_4$ $t_1,t_2,t_5$ $+0.40$ $-0.267$ $\mathbf{0.667}$(最优)
$0.70$ $t_1..t_4$ $t_5$ $+0.15$ $-0.60$ $0.75$

故 $\tau_1^*=0.565$,$f_1^{\mathrm{L}}=+0.4$,$f_1^{\mathrm{R}}=(-0.6+0.4-0.6)/3=-0.8/3\approx-0.267$。

$t$ $y$ $\phi$ $F_0$ $r=y-F_0$ $f_1$ $F_1=F_0+f_1$
1 0 0.60 0.6 −0.6 −0.267 ≈0.333
2 1 0.60 0.6 +0.4 −0.267 ≈0.333
3 1 0.30 0.6 +0.4 +0.4 1.0
4 1 0.53 0.6 +0.4 +0.4 1.0
5 0 0.80 0.6 −0.6 −0.267 ≈0.333

平方残差和:$\sum r_0^2=1.20$ → $\sum(y-F_1)^2\approx0.667$。$t_2$ 与右叶负残差同伴折中,单点可变差,但整体下降。

第 2 轮:残差 $r=y-F_1$,重新枚举同一组候选 $\tau$(每棵新树都要重搜,不沿用 $\tau_1^*$):

候选 $\tau$ 左叶 右叶 $f^{\mathrm{L}}$ $f^{\mathrm{R}}$ 拟合后 $\sum(r-f)^2$
$0.415$ $t_3$ 其余 $0$ $0$ $0.667$(无改进)
$0.565$ $t_3,t_4$ $t_1,t_2,t_5$ $0$ $0$ $0.667$(无改进)
$0.70$ $t_1..t_4$ $t_5$ $+0.083$ $-0.333$ $\mathbf{0.528}$(最优)

故 $\tau_2^*=0.70$,$f_2^{\mathrm{L}}=(-\tfrac13+\tfrac23+0+0)/4=\tfrac1{12}\approx0.083$,$f_2^{\mathrm{R}}=-\tfrac13$。

$t$ $y$ $\phi$ $F_1$ $r=y-F_1$ 叶(相对 $\tau_2^*$) $f_2$ $F_2=F_1+f_2$
1 0 0.60 ≈0.333 ≈−0.333 +0.083 ≈0.417
2 1 0.60 ≈0.333 ≈+0.667 +0.083 ≈0.417
3 1 0.30 1.0 0 +0.083 ≈1.083
4 1 0.53 1.0 0 +0.083 ≈1.083
5 0 0.80 ≈0.333 ≈−0.333 −0.333 0

平方残差和 ≈ $0.528$。第 2 棵树把阈值改到 $0.70$,才能再削误差——若死守第 1 轮的 $0.565$,叶均值全为 $0$,提升停住。

D. 可部署对象

有序编码规则(含先验 $m,a$)+ 两棵 stump:
$f_1$ :$\tau_1^=0.565$,叶值 ${+0.4,,-0.267}$;
$f_2$ :$\tau_2^
=0.70$,叶值 ${+0.083,,-0.333}$。
推理阶段不再给「同一类别、每一行各不相同的训练期 $\phi$」;而是对每个类别水平准备一份(或实现内部约定的)部署用统计,再进树。

E. 预测 / 推断

预测期 $\phi$ 怎么定(与训练期「同行不同 $\phi$」对照)

阶段 同一类别(如「沪」)的 $\phi$
训练编码 每个样本 $t$ 只看排列中更早的同城 $y$,故 $t_1/t_3/t_4$ 的 $\phi$ 可以不同(防泄漏)
预测 / 上线 新样本没有「自己在训练排列里的位置」,也不能用自己的 $y$(尚未发生或不可用)。通常把训练集里该类别的全部真实 $y$(加同一套先验平滑)聚成一个部署用 $\phi_{\text{cat}}$;同城新样本共用这个值

教学约定(与上表数据一致):对类别 $c$,

$$
\phi_{\text{deploy}}(c)=\frac{\sum_{i:,c_i=c} y_i + a,m}{n_c+a}
$$

即:**会把训练阶段该类别的样本都当作「前置历史」**来估一个固定编码;不是沿用某一次训练行上的 $\phi_t$,也不是在线再编造排列。未见过的新类别则退回先验 $m$(或实现里的未知水平策略)。

  • 训练内复核($t=3$):$\phi_3=0.30$ → $f_1$ 走左($<0.565$)得 $+0.4$;$f_2$ 走左($<0.70$)得 $+0.083$ ⇒ $F_2\approx1.083$,阈值 0.5 ⇒ 判 1。
  • 新样本(城市=沪):$\phi_{\text{deploy}}(\text{沪})=(0+1+1+0.6)/4=0.65$ → $f_1$ 走右($\geq0.565$)得 $-0.267$;$f_2$ 走左($<0.70$)得 $+0.083$ ⇒
    $F=0.6-0.267+0.083\approx0.416$,阈值 0.5 ⇒ 判 0。
    (部署 $\phi$ 与训练行 $t_3$ 的有序 $\phi$ 不同,两棵树的左右叶归属也不同——对照「训练逐行 $\phi$ ≠ 部署聚合 $\phi$」。)

段末注释:完整 CatBoost 在库内还会存更细的统计与组合特征;上表是教学口径。要点不变——训练用有序、逐行不同的 $\phi$ 防泄漏;预测用基于训练集聚合的类别统计;每轮 stump 阈值由候选切分按残差拟合误差选出,叶值取叶内残差均值。


5. 适用 / 不适用

维度 判定 要求或边界 具体例子
特征 适用 类别多、高基数;想少手工编码;可混数值 电商:城市、店铺 ID、品类 + 价格、停留时长
特征 不适用 纯数值、极小表;或类别已有可靠嵌入表征 仅 5 个标准化数值列的小实验,XGB/sklearn 即可
训练目标 适用 分类、回归、排序等常见监督任务 CTR、违约、搜索相关性
训练目标 不适用 必须输出完整条件分布(均值+方差带) 要预测区间时看 NGBoost 或分位数/贝叶斯方案
训练数据 适用 中等至大规模表格;验证集早停 数十万行业务表,类别列直接 cat_features
训练数据 不适用 类别水平在线上会狂出新值且无兜底;样本极少却深度很大 训练集未见过的 ID 占一半流量,却无未知水平策略

6. 优缺点与常见坑

优点:类别友好;默认较稳;有序思路降低一种常见泄漏;推理效率不错。
缺点:超大纯数值表上不一定快过 LightGBM;原理细节比「开箱调用」重;GPU/集群场景要查当前版本文档。

:忘了声明 cat_features 却把字符串当数值;早停指标与业务不一致;和 LightGBM 比速度时数据预处理不对齐。


7. 最小可运行示例

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
26
27
28
29
30
31
32
"""CatBoost:声明类别列,二分类训练与预测。"""
import numpy as np
from catboost import CatBoostClassifier, Pool
from sklearn.model_selection import train_test_split

# 示意:最后一列为类别城市编码
rng = np.random.default_rng(42)
n = 2000
X_num = rng.normal(size=(n, 3))
X_cat = rng.integers(0, 10, size=(n, 1))
X = np.hstack([X_num, X_cat])
y = (X_num[:, 0] + 0.3 * X_cat[:, 0] + rng.normal(scale=0.5, size=n) > 0).astype(int)

X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
cat_idx = [3]
train_pool = Pool(X_train, y_train, cat_features=cat_idx)
test_pool = Pool(X_test, y_test, cat_features=cat_idx)

model = CatBoostClassifier(
iterations=200,
depth=6,
learning_rate=0.1,
loss_function="Logloss",
eval_metric="AUC",
random_seed=42,
verbose=False,
)
model.fit(train_pool, eval_set=test_pool, early_stopping_rounds=30)
print("best iteration:", model.get_best_iteration())
print("test pred[:5]:", model.predict_proba(X_test)[:5, 1])

说明:随机示意数据示例,与上文有序统计手算表无关。

重要配置参数(CatBoost)

参数(库内常用名) 训练中的作用与影响 参考起点 / 常用范围 配置指导
iterations 树/迭代次数;过多易过拟合 500~3000必须配早停 get_best_iteration(),勿默认跑满
depth 对称树深度;过大易过拟合、变慢 4~8;起点可 6 过拟合优先减 depth 或加 l2_leaf_reg
learning_rate 步长;小更稳、常需更多迭代 0.03~0.2;起点可 0.1 iterations/早停联动;lr↓ 时放宽最大迭代
l2_leaf_reg 叶正则;越大越保守 常从默认附近试;过拟合可增大 比盲目减轮数更直接抑制叶爆炸
cat_features 声明类别列索引/名;走有序目标统计 凡类别列都显式声明 漏声明会当数值乱切;与手算「有序 φ」同一条产品路径
早停 early_stopping_rounds 验证集停滞则停 20~50 务必提供 eval_set;生产用 best iteration

8. 和近邻算法怎么挑

需求 更优先考虑
教学:加重错分 AdaBoost
残差提升框架 GBDT
通用数值表强基线 XGBoost
类别多、防目标编码泄漏 CatBoost
行数极大、要更快训练 LightGBM
要整段预测分布 NGBoost

9. 小结

  • CatBoost ≈ GBDT + 有序提升 + 原生类别
  • 类别型业务表的常用默认之一;与 LightGBM 选型看「类别复杂度 vs 纯速度」。
  • 最易踩的坑:类别列未声明进 cat_features

参考文献

  1. Prokhorenkova L. et al. CatBoost: unbiased boosting with categorical features. NeurIPS 2018.
  2. Dorogush A.V. et al. Fighting biases with dynamic boosting. arXiv:1706.09516 2017.
  3. CatBoost 文档
-------------本文结束感谢您的阅读-------------