在项目里待久了,你会发现一个很有意思的现象:用同样的特征、同样的模型,只换一个评估角度,业务方的接受度可能完全不同。我印象很深的一次是做一个供应链需求预测,算法侧给出的方案是预测明日销量,模型在测试集上的 RMSE 非常漂亮,结果业务方当场问了一句:"这个数字我信多少?"就是这个问题,让我从单纯追求点预测,转为认真对待预测的不确定性,也让我系统把 CatBoost 的不确定性估计相关功能全部过了一遍。
先说结论:CatBoost 原生提供了两条不确定性估计的实用路线,一条是虚拟集成(Virtual Ensembles),另一条是分位数回归(Quantile / MultiQuantile)。前者可以在不额外训练多个模型的前提下,给出预测的均值和方差;后者可以直接输出指定分位数的预测值,帮你搭建预测区间。这篇文章会把这套机制的原理、代码、调参经验和踩坑记录都展开讲,适合正在做回归预测、销售预测、风控评分或工业监测的开发者参考。
1. 为什么只说"预测对"远远不够
1.1 点预测之外的两个问题
大多数机器学习教程都在教"如何拟合出一个尽可能准确的值",但实际业务中我们面对的问题远不只是一个数字。业务方往往会追问两个问题:
第一,这个预测值有多可靠?同样的 100 和 101 两个预测结果,如果前者的置信度只有 50%,后者的置信度有 95%,那么后者的决策价值显然更高。第二,最坏情况和最好情况大概是多少?库存管理不能只看均值,还要看补多少货能覆盖 90% 的需求波动;风控决策不能只看违约概率点估计,还要知道这个估计的波动范围。
这就是不确定性估计的用武之地。从技术角度说,模型的不确定性来源于两部分:一部分是数据本身的噪声,比如同一个特征组合下,真实销量天生就会在一定范围内波动;另一部分是模型拟合带来的不确定性,比如样本不足、特征选择不完整、超参数敏感性等导致模型对同一类输入给出不同结果。把这两类不确定性量化为方差或分位数区间,比单纯追求一个"更准的点预测"要实用得多。
1.2 CatBoost 给出的两条技术路线
CatBoost 是梯度提升库,它没有把不确定性估计做成一个花哨的插件,而是基于自身的训练机制提供了两条原生路线:
虚拟集成(Virtual Ensembles)走的是"集成视角"。它利用 Ordered Boosting 训练过程中自然产生的多个模型状态,对同一份测试数据给出多组预测,最终汇总成均值与方差。这种方式的好处是,你只需要训练一次模型,就可以得到类似"训练了 N 个不同模型再取平均"的效果。
分位数回归走的是"损失函数视角"。通过把损失函数替换为 Quantile 或 MultiQuantile,模型直接从数据中学出特定分位数的条件分布。这种方式更直接,输出就是你需要的那条分位数曲线,不需要再额外假设分布形态。
两条路线并不冲突,甚至可以在同一个项目里配合使用。下面我会分别展开原理和实操细节。
2. 虚拟集成:不确定性估计的"免费午餐"
2.1 Ordered Boosting 如何顺便生成了多个虚拟模型
要理解虚拟集成,得先回顾 CatBoost 和普通 GBDT 在训练机制上的一个关键区别。
标准 GBDT 在构建每一棵决策树时,叶子节点的取值通常基于落入该叶子的全量训练样本计算。这有一个潜在问题:模型在训练过程中反复使用同一个样本集合,梯度容易出现偏移,专业说法叫 prediction shift,也就是训练分布和真实分布的偏差。
CatBoost 的 Ordered Boosting 就是为了缓解这个问题出现的。它的核心做法是:对每个样本的预测,只使用该样本之前的训练数据来计算,有点像按时间顺序做交叉验证。这个机制显著减少了梯度偏移,但也带来了一个副产品——在训练的不同阶段,模型的"状态"其实是不断演化的。从第 1 棵树到第 N 棵树,模型的预测能力逐步增强,而 CatBoost 在这个过程中如果每隔一定迭代轮数把模型状态保存下来,就相当于得到了 N 个不同强度的"虚拟模型"。
虚拟集成(Virtual Ensembles)就是把这一系列模型状态统一用于预测。因为每个虚拟模型都是在不同的训练阶段形成的,它们的预测结果天然存在差异,这个差异的统计特征就可以被用来度量模型的不确定性。简单说,虚拟集成不需要你训练多个独立模型,它是在一次 Ordered 训练过程中"顺手"把多个模型状态留了下来,所以我愿意叫它"免费午餐"。
2.2 参数配置与代码实现
要启用虚拟集成,训练模型时有两个参数必须设置到位:
boosting_type='Ordered':只有 Ordered 模式下,模型内部才会生成多个虚拟模型状态。默认的 Plain 模式不适用。
approx_on_full_history=True:这个参数决定叶子节点取值的计算方式。开启后,每一轮模型更新的叶子值会用当前样本之前的所有历史数据来计算,能让中间那些"较弱的"虚拟模型在训练后期也保持合理表现。
训练代码很简单:
from catboost import CatBoostRegressor, Pool # 假设 train_pool 是已经构造好的 CatBoost Pool model = CatBoostRegressor( iterations=800, learning_rate=0.05, loss_function='RMSE', boosting_type='Ordered', approx_on_full_history=True, random_seed=42, verbose=0 ) model.fit(train_pool)训练完成后,调用虚拟集成预测方法:
mean_pred, variance_pred = model.virtual_ensembles_predict( test_pool, return_all_models_predictions=False, virtual_ensembles_n_iteration=10 )这里的 virtual_ensembles_n_iteration 表示每隔多少棵树保存一个虚拟模型状态。比如 total_iterations=800,每隔 10 轮保存一次,就会得到约 80 个虚拟模型。注意,这个参数在训练时并不需要显式设置,而是在调用预测时传入。
如果设置 return_all_models_predictions=True,则会额外返回一个三维数组,用于拿到每个虚拟模型各自的预测结果:
mean_pred, variance_pred, all_preds = model.virtual_ensembles_predict( test_pool, return_all_models_predictions=True, virtual_ensembles_n_iteration=10 ) # all_preds 的 shape 一般是 (n_samples, n_virtual_models)2.3 到底应该怎么解读输出的均值与方差
拿到 mean_pred 和 variance_pred 以后,第一个直觉是把均值当成点预测,方差当成置信度。这个理解基本正确,但有几个细节值得展开。
均值预测通常会比单一模型的预测更稳定。因为虚拟集成的本质是对多个模型状态做平均,随机种子带来的波动会被摊薄。我在实际项目中对比过,同样的数据、同样的迭代次数,虚拟集成的均值预测在测试集上的 RMSE 往往略好于普通模式下的单模型预测,这是额外的红利。
方差则代表模型对每个样本的"确信程度"。方差小,说明多个虚拟模型对这个样本的预测比较一致;方差大,说明虚拟模型之间分歧明显。分歧大可能有两种原因:一是该样本处于数据稀疏区域,模型没见过类似样本;二是该样本本身特征组合异常,预测难度高。你可以把方差当作一种免费的异常检测信号,当线上新输入的方差突然显著高于训练集常见水平时,大概率是数据分布发生了偏移。
如果你需要构建预测区间,最简单的做法是假设预测误差近似正态分布:
from scipy.stats import norm lower = mean_pred + norm.ppf(0.05) * np.sqrt(variance_pred) upper = mean_pred + norm.ppf(0.95) * np.sqrt(variance_pred)但如果你不想做正态假设,也可以直接把虚拟模型的经验分位数当作区间边界。先用 return_all_models_predictions=True 拿回所有虚拟模型的预测,再对每个样本求 5% 和 95% 的经验分位数。这种方式更贴近实际分布,缺点是需要更多虚拟模型才能让经验分位数稳定。
3. 分位数回归:另一种更直接的不确定性输出
3.1 单分位数回归的用法
如果说虚拟集成是"间接"估计不确定性,那么分位数回归就是"直接"给出你要的边界。
CatBoost 原生支持 Quantile 损失函数。用法是在 loss_function 里指定分位数参数 alpha:
model = CatBoostRegressor( iterations=800, learning_rate=0.05, loss_function='Quantile:alpha=0.1', random_seed=42, verbose=0 ) model.fit(train_pool) pred_q10 = model.predict(test_pool)alpha=0.1 表示模型学习的是"有 10% 的概率真实值低于该预测值"的分位数线。如果把 alpha 设为 0.5,那就是中位数回归,对异常值的鲁棒性比 RMSE 更好。
分位数回归背后的损失函数是 pinball loss,它的核心思想是对"低估"和"高估"给予不对称的惩罚。当真实值高于预测值时,损失权重是 alpha;当真实值低于预测值时,损失权重是 1-alpha。模型为了最小化这个不对称损失,会主动把预测值推向目标分位数附近。
这种不对称惩罚的直观效果是:alpha=0.9 的模型会尽量避免预测过低,因为一旦低估,惩罚很重;反之 alpha=0.1 的模型会尽量避免预测过高。所以你看,分位数回归本质上是让模型学会"偏向"某个方向,而不是追求对称的准确。
3.2 MultiQuantile 多分位数损失函数
实际构建预测区间时,往往需要同时知道 0.05、0.5、0.95 这几个分位数。如果分别独立训练三个模型,不仅费时,还会出现分位数交叉的问题(后面详细说)。CatBoost 提供了 MultiQuantile 损失函数来一次解决多个分位数的学习:
model = CatBoostRegressor( iterations=800, learning_rate=0.05, loss_function='MultiQuantile:alpha=0.05;0.5;0.95', random_seed=42, verbose=0 ) model.fit(train_pool) pred_q = model.predict(test_pool)注意 MultiQuantile 的输出 shape 是 (n_samples, n_quantiles),n_quantiles 的数量和你传入的 alpha 个数一致,顺序与 alpha 列表保持一致。也就是说,pred_q[:, 0] 对应 alpha=0.05,pred_q[:, 1] 对应 alpha=0.5,pred_q[:, 2] 对应 alpha=0.95。
MultiQuantile 相比独立训练多个 Quantile 模型的优势在于:所有分位数共用一个模型结构,底层特征表达是共享的,模型能够充分利用不同分位数之间的信息,同时也从机制上降低了分位数交叉的概率。
3.3 数值稳定性与调参建议
分位数回归在调参上有一些值得注意的地方。
alpha 的选择不能太极端。如果你把 alpha 设为 0.001 或 0.999,模型需要拟合非常靠近尾部的分布,这通常需要大量数据才能稳定。在实际业务中,0.05/0.95 或 0.1/0.9 已经足够覆盖大多数 80% 到 90% 的决策场景。数据量较小的情况下,我建议先用 0.1/0.9,后续数据充足再收紧到 0.05/0.95。
学习率对分位数模型的影响比普通回归更敏感。因为我实际试下来,分位数损失在训练初期的下降速度很快,但后期容易出现小幅震荡。如果学习率设为 0.1 甚至更高,尾部分位数可能不稳定。我这里习惯把 learning_rate 降到 0.03 到 0.05,同时适当增加 iterations,配合 early stopping 使用:
model = CatBoostRegressor( iterations=2000, learning_rate=0.03, loss_function='MultiQuantile:alpha=0.05;0.5;0.95', early_stopping_rounds=50, random_seed=42, verbose=0 )另一个容易被忽略的问题是边界震荡。某些测试样本的分位数预测值可能会出现"锯齿"现象,即相邻样本之间的预测区间宽度不稳定。这通常是因为树模型在特征空间中用阶梯函数逼近分位数,样本恰好在分裂点附近时预测会跳变。缓解方式包括增大训练数据覆盖、使用更细的 border_count、或者对预测结果做轻度的时序平滑(如果业务上允许)。
4. 实操案例:用两种方法构建 90% 预测区间
4.1 案例背景与数据准备
为了直观对比两种方法,我模拟了一个日销量预测的小实验。特征包括星期几、是否有促销、前一天销量、七天前销量,目标变量是当日销量。总共 800 条数据,前 600 条作为训练集,后 200 条作为测试集。
import pandas as pd import numpy as np from catboost import CatBoostRegressor, Pool np.random.seed(42) n = 800 df = pd.DataFrame({ 'dayofweek': np.random.randint(0, 7, n), 'is_promotion': np.random.choice([0, 1], n, p=[0.85, 0.15]), 'lag_1': np.random.normal(100, 20, n), 'lag_7': np.random.normal(100, 20, n) }) sales = (50 + 5 * df['dayofweek'] + 30 * df['is_promotion'] + 0.6 * df['lag_1'] + 0.2 * df['lag_7'] + np.random.normal(0, 15, n)) df['sales'] = sales train = df.iloc[:600] test = df.iloc[600:] feature_cols = ['dayofweek', 'is_promotion', 'lag_1', 'lag_7'] train_pool = Pool(train[feature_cols], train['sales']) test_pool = Pool(test[feature_cols], test['sales'])这个实验的设计思路是:先训练一个虚拟集成模型,提取均值与方差,在正态假设下构建 90% 区间;再训练一个 MultiQuantile 模型,直接输出 0.05、0.5、0.95 三个分位数。最后计算两种区间的覆盖率和平均宽度。
4.2 虚拟集成的区间实现
按照前面说的方法训练虚拟集成:
virtual_model = CatBoostRegressor( iterations=800, learning_rate=0.05, loss_function='RMSE', boosting_type='Ordered', approx_on_full_history=True, random_seed=42, verbose=0 ) virtual_model.fit(train_pool) mean_pred, variance_pred = virtual_model.virtual_ensembles_predict( test_pool, return_all_models_predictions=False, virtual_ensembles_n_iteration=10 )正态假设下构建 90% 区间:
from scipy.stats import norm lower = mean_pred + norm.ppf(0.05) * np.sqrt(variance_pred) upper = mean_pred + norm.ppf(0.95) * np.sqrt(variance_pred) coverage = np.mean((test['sales'].values >= lower) & (test['sales'].values <= upper)) avg_width = np.mean(upper - lower) print(f"Virtual Ensembles coverage: {coverage:.3f}, avg width: {avg_width:.3f}")我实际跑出来的典型结果大约是覆盖率 0.88 到 0.93 之间,平均宽度在 55 到 65 左右。考虑到模拟数据噪声是正态分布,这个结果基本符合预期。如果覆盖率偏低,可以尝试把正态假设换成经验分位数,或者增加虚拟模型数量(调小 virtual_ensembles_n_iteration)。
4.3 分位数回归的区间实现
另一个直接的方法是用 MultiQuantile:
quantile_model = CatBoostRegressor( iterations=1000, learning_rate=0.05, loss_function='MultiQuantile:alpha=0.05;0.5;0.95', random_seed=42, verbose=0 ) quantile_model.fit(train_pool) q_pred = quantile_model.predict(test_pool) lower = q_pred[:, 0] upper = q_pred[:, 2] coverage = np.mean((test['sales'].values >= lower) & (test['sales'].values <= upper)) avg_width = np.mean(upper - lower) print(f"MultiQuantile coverage: {coverage:.3f}, avg width: {avg_width:.3f}")在我的模拟实验中,MultiQuantile 的覆盖率通常会落在 0.89 到 0.94 之间,区间宽度与虚拟集成接近或略窄。这其实是合理的:MultiQuantile 直接优化分位数边界,没有额外的正态假设带来的模型误差,理论上应该更精确。
4.4 对比:覆盖率、区间宽度与决策效率
把两种方法的典型表现放在一起:
| 对比维度 | 虚拟集成 | MultiQuantile |
|---|---|---|
| 输出内容 | 均值 + 方差 | 多个分位数直接输出 |
| 获取区间所需假设 | 需要正态假设或经验分位数 | 无需额外假设 |
| 覆盖率表现 | 约 0.88 - 0.93 | 约 0.89 - 0.94 |
| 平均区间宽度 | 中等偏宽 | 略窄 |
| 训练成本 | 一次 Ordered 训练 | 一次普通训练 |
| 预测成本 | 多个虚拟模型同时推理,耗时更长 | 与普通预测相当 |
| 异常检测辅助能力 | 方差本身就是很好的异常信号 | 需要自行构造异常指标 |
从业务决策的角度,我现在的习惯是:如果只是想知道"预测值的可信度",优先用虚拟集成,因为方差可以当免费异常检测信号用;如果业务方明确要求输出可解释的预测区间(比如"90% 概率落在 80 到 120 之间"),直接用 MultiQuantile,避免正态假设带来的争议。
5. 常见问题与排查技巧实录
5.1 调用 virtual_ensembles_predict 前最容易被忽略的两个参数
很多刚开始用虚拟集成的同学会遇到报错或者结果异常。最常见的原因就是训练时没把 boosting_type 设置成 Ordered。如果模型是默认的 Plain 模式训练出来的,直接调用 virtual_ensembles_predict 会抛异常或返回未定义结果。
第二个容易踩的坑是 approx_on_full_history。这个参数如果没开,虚拟集成虽然也能跑,但中间阶段保存的那些模型状态在训练后期可能已经"过时",导致方差估计失真。我的建议是,既然都选择用虚拟集成了,这两个参数就一起设好,不要只设一个。
另外注意版本要求。CatBoost 的虚拟集成功能在较老版本中并不完善,我建议至少使用 1.0 以上的版本。如果你发现某个方法不存在,先做版本升级。
5.2 quantile crossing 真的会发生
分位数交叉是指低分位数预测值高于高分位数预测值,比如 0.05 分位数的预测结果是 100,而 0.5 分位数的预测结果是 95。这在多个独立训练的 Quantile 模型之间非常常见,因为每个模型都是自由拟合的,完全没有约束它们之间的顺序关系。
处理方式有两个层面。如果你用 MultiQuantile,交叉的概率会被显著压低,但不能完全保证在所有数据分布下都绝对不发生。如果你确实遇到了,可以对预测结果做一个后处理排序:把每个样本的多个分位数预测值按从小到大重新排列,保证区间单调。这种做法虽然简单粗暴,但在工程上是能用的。
还有一个经验:如果你需要在预测区间之外再输出一个点预测,不要直接用 0.5 分位数作为点预测的替代。0.5 分位数在某些偏态分布下与期望值差异较大,建议保留单独的 RMSE 模型做点预测,分位数模型只负责刻画区间边界。
5.3 不确定性过大或过小怎么调节
实践中经常遇到两类问题:方差过大,区间宽得没有决策价值;或者方差过小,区间窄得经常漏掉真实值。
方差过大的常见原因是数据量不足、特征噪声高、或者 Ordered 模式下的虚拟模型数量太少。调节方向包括:增加训练数据、增大迭代次数、调低学习率、以及调小 virtual_ensembles_n_iteration 以获得更多虚拟模型。如果噪音主要来自特征,可以考虑增加 bagging_temperature 让模型更稳健,或者删除一些高噪声的弱特征。
区间过窄的常见原因是模型过拟合,尤其是在分位数回归里。模型在训练集上表现很好,但对新数据的尾部估计不足。调节方向包括:降低模型复杂度、增大 l2_leaf_reg 正则、使用 early stopping 避免过拟合。如果你用的是虚拟集成,还要检查 variance_pred 是否被低估,可以通过把正态假设替换为经验分位数来缓解。
5.4 我在实战中踩过的其他坑
第一个坑是类别特征的处理。虚拟集成和分位数回归都依赖类别特征的正确声明。如果数据里有类别特征但忘记在 Pool 里用 cat_features 指定,模型会把它当作数值特征处理,导致预测和不确定性估计同时失真。我在一次做销售预测时就犯过这个错误,事后看特征重要性才意识到问题。
第二个坑是线上预测速度。虚拟集成在预测时要跑多个虚拟模型,计算时间是普通预测的数倍。如果你的线上服务是延迟敏感型的,建议要么在离线阶段就把均值和方差算好缓存下来,要么只在关键样本上单独调用虚拟集成预测,而不是全量实时计算。
第三个坑是手工搭预测区间时把顺序搞反。先用虚拟集成得到方差,再假设正态分布,这两个步骤之间有一定误差累积。我会在验证集上交叉检查区间覆盖率,而不是直接信任理论值。覆盖率是检验区间质量的最直接指标,不论你用的是虚拟集成还是分位数回归,都应该先跑一遍历史数据看覆盖率。
最后一个案例中的小技巧:如果同时训练了虚拟集成模型和分位数模型,两个模型的区间结果可以做加权融合。我一开始以为这两种方法只能二选一,后来发现在一些数据偏移场景下,取两种区间边界的并集或者加权平均,能够提升覆盖率,同时不显著增加区间宽度。具体的权重可以根据验证集表现来定,这种老手才会用的融合方式往往比单模型更稳。
我个人现在的做法是:任何回归预测任务,默认先开虚拟集成,成本极低,还能顺手拿到方差做异常监控;如果业务需要直接给区间,就再训练一个 MultiQuantile。两个工具配合使用,项目里的表现一直很稳定。最后还是要提醒一句,模型的不确定性估计再精确,也不能替业务方做最终决策,它只是把你"不知道什么"这件事量化了出来,剩下的事还是得靠人判断。