表格里一堆「城市 / 店铺 / 用户 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 用有序提升 / 有序统计降低这类目标泄漏,并强化原生类别支持。
比喻:算某个品类的「历史平均点击率」时,只准看排在当前样本之前的记录,不准翻后面的答案——考试只能参考已做过的卷子,不能偷看尚未开封的。

2. 直觉:相对前代多了什么
发展坐标(本系列):AdaBoost(改权重)→ GBDT(拟合残差)→ XGBoost(正则 + 二阶)→ CatBoost(类别 + 有序) → LightGBM(直方图加速)→ NGBoost(分布参数)。
CatBoost 相对「朴素 GBDT / 粗糙目标编码」:
- Ordered boosting:按排列构造,减少残差估计中的偏置与泄漏。
- 类别组合与统计:类别可直接声明;内部用有序目标统计等,少做爆炸 one-hot。
- 对称树(oblivious tree):同一层共用分裂条件,结构规整,利于速度与正则。
3. 核心链路
主链路仍是加法树:$F_M(x)=\sum_m \nu f_m(x)$。
差异在 $f_m$ 怎么用类别、怎么估梯度。教学上把两件事分开看:
- 有序目标统计(生成 $\phi$):把类别编成数值特征。对样本 $t$,只用排列中更早的同类别真实标签 $y$ 估均值(加先验平滑)。同一类别、不同行的 $\phi_t$ 可以不同。
- 其后的提升迭代:本篇手算里,$\phi$ 在首轮遍历算完后即冻结,当作普通数值特征送进各轮树;每轮变的是残差 $r$、树 $f_m$ 与预测 $F$,不再用 $y$ 重算 $\phi$。
- 有序提升(ordered boosting,实现层):完整 CatBoost 还会在估计残差/梯度时尽量「只看过去」,那是另一套防泄漏,不等于每轮改写 $\phi$。本篇手算不展开该层。
- 读参数:
depth、learning_rate、iterations+ 早停;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 | """CatBoost:声明类别列,二分类训练与预测。""" |
说明:随机示意数据示例,与上文有序统计手算表无关。
重要配置参数(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。
参考文献
- Prokhorenkova L. et al. CatBoost: unbiased boosting with categorical features. NeurIPS 2018.
- Dorogush A.V. et al. Fighting biases with dynamic boosting. arXiv:1706.09516 2017.
- CatBoost 文档