模型输出一个预测值只是任务的一半,另一半是回答这个预测值有多可信。如果房价模型预测某套房子值500万,误差可能是10万也可能是200万,这两种情况对应的决策完全不同。置信区间就是量化这种可信程度的工具,它给出一个范围,并声明真实值以某个概率落在这个范围内。本文系统介绍机器学习中不确定性估计的核心方法,涵盖频率学派和贝叶斯学派两条路线,并配上可直接运行的代码。

先分清两种不确定性:认知不确定性与偶然不确定性
在动手计算置信区间之前,必须先理解不确定性的来源,否则选错方法会导致结果失真。机器学习中通常把不确定性分为两类。第一类是认知不确定性(epistemic uncertainty),也叫模型不确定性,来源于模型参数没有学到位。数据量少、模型没见过类似样本时,这种不确定性就大。它的特点是可以通过增加训练数据来缩小。第二类是偶然不确定性(aleatoric uncertainty),来源于数据本身的噪声。比如预测明日股价,即使模型完美,随机波动依然存在,这类不确定性无法靠收集更多数据消除。
区分这两者的实际意义在于:如果你的模型在某个区域认知不确定性很高,说明这块数据采样不足,应该去补数据;如果偶然不确定性高,说明任务本身有随机性,应该在产品层面预留容错空间。简单的做法是把两者拆开建模,让网络同时输出预测均值和方差。对于回归任务,可以把方差的对数作为额外输出头,用高斯负对数似然作为损失函数:
import torch
import torch.nn as nn
class HeteroscedasticModel(nn.Module):
def __init__(self, in_dim, hidden=64):
super().__init__()
self.shared = nn.Sequential(
nn.Linear(in_dim, hidden), nn.ReLU(),
nn.Linear(hidden, hidden), nn.ReLU()
)
# 一个头输出均值,一个头输出 log 方差(保证方差为正)
self.mean_head = nn.Linear(hidden, 1)
self.logvar_head = nn.Linear(hidden, 1)
def forward(self, x):
h = self.shared(x)
return self.mean_head(h), self.logvar_head(h)
def gaussian_nll_loss(mean, logvar, target):
# 高斯负对数似然,同时学习均值和方差
var = torch.exp(logvar)
return torch.mean(0.5 * (logvar + (target - mean) ** 2 / var))
model = HeteroscedasticModel(in_dim=10)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
# 训练循环示例
for x, y in dataloader:
mean, logvar = model(x)
loss = gaussian_nll_loss(mean, logvar, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
这个模型输出的方差就是数据驱动的偶然不确定性估计:输入处于噪声大的区域时,模型自动给出更宽的区间。需要注意的是,训练初期方差头可能坍缩到很小的值,导致损失爆炸,实践中常对logvar做裁剪,例如限制在[-6, 6]之间。
Bootstrap:最简单粗暴的置信区间估计法
Bootstrap 的思路非常直觉:训练多个模型,每个模型用不同的数据子集训练,预测时让所有模型各给出一个值,用这些值的分布构造区间。它属于频率学派的方法,实现成本几乎为零,特别适合已有成熟训练管线的团队快速验证。假设我们训练100个模型,对同一个输入得到100个预测,取第2.5百分位和第97.5百分位,就得到95%置信区间。
完整的实现配合bagging采样如下:
import numpy as np
from sklearn.ensemble import RandomForestRegressor
from sklearn.utils import resample
class BootstrapInterval:
def __init__(self, n_models=100, alpha=0.05):
self.n_models = n_models
self.alpha = alpha
self.models = []
def fit(self, X, y):
for i in range(self.n_models):
# 有放回采样,每次约 63.2% 的样本被抽到
X_sample, y_sample = resample(X, y, random_state=i)
model = RandomForestRegressor(n_estimators=30, random_state=i)
model.fit(X_sample, y_sample)
self.models.append(model)
return self
def predict_interval(self, x):
preds = np.stack([m.predict(x) for m in self.models])
lower = np.percentile(preds, 100 * self.alpha / 2, axis=0)
upper = np.percentile(preds, 100 * (1 - self.alpha / 2), axis=0)
mean = preds.mean(axis=0)
return mean, lower, upper
# 使用示例
# bi = BootstrapInterval(n_models=100).fit(X_train, y_train)
# mean, lo, hi = bi.predict_interval(X_test)
Bootstrap的缺点也很明显。第一是计算成本线性增长,100个模型意味着100倍训练开销,对深度网络来说通常不可承受。第二是它只能捕捉认知不确定性,多个模型都数据不足时会给出一致但错误的自信结果。第三是区间的覆盖率没有严格理论保证,只是一个启发式估计。因此Bootstrap适合传统机器学习模型或小型网络,在深度学习场景下更适合用下一节的方法。
MC Dropout 与贝叶斯神经网络:深度模型的不确定性方案
训练多个深度网络不现实,但有办法用一个网络近似等效。Dropout原本是正则化手段,训练时随机丢弃神经元。研究发现,在推理阶段保持Dropout开启,对同一输入做多次前向传播,等价于从近似的后验分布中采样模型参数,这就是MC Dropout。每次前向传播得到一个预测,多次预测的均值和方差分别对应预测值和认知不确定性。
实现上只需要在推理时不调用eval()模式,或者显式启用dropout层:
import torch
import torch.nn.functional as F
def enable_mc_dropout(model):
# 让所有 Dropout 层在推理时保持训练行为
for m in model.modules():
if isinstance(m, nn.Dropout):
m.train()
@torch.no_grad()
def predict_with_uncertainty(model, x, n_samples=50):
enable_mc_dropout(model)
model.eval() # 先设 eval 再单独打开 dropout,避免 BatchNorm 受影响
enable_mc_dropout(model)
preds = torch.stack([
model(x) for _ in range(n_samples)
]) # 形状: [n_samples, batch, 1]
mean = preds.mean(dim=0)
# 方差包含认知与偶然两部分(若模型输出异方差则更大)
std = preds.std(dim=0)
lower = mean - 1.96 * std
upper = mean + 1.96 * std
return mean, lower, upper
MC Dropout的成本是推理时间乘以采样次数,通常50次就够稳定。它的理论基础是高斯过程近似,虽然被批评近似质量有限,但工程上效果出奇地好,尤其在分布外检测任务中,MC Dropout的不确定性会明显升高,可以用来触发人工审核。
更彻底的方案是贝叶斯神经网络(BNN):不再学习单个权重值,而是学习权重的概率分布。由于精确后验不可解,实践中多用变分推断,用一个简单分布族去逼近后验,优化证据下界(ELBO)。也可以用Pyro这类概率编程库,它把变分推断的细节封装好了。BNN的区间的理论性质比MC Dropout更好,但训练复杂度和调参难度都显著上升,一般建议先试MC Dropout,效果不达标再考虑BNN。
共形预测:有理论保证的区间构造方法
前面方法的置信水平大多是近似的,而共形预测(Conformal Prediction)能在极弱假设下给出有限样本覆盖率保证:只要数据可交换,构造出的区间以至少95%的概率覆盖真实值。这个保证不依赖模型具体是什么,黑盒模型也适用。Split Conformal是工程上最常用的变体,流程分三步。第一步,把数据分成训练集和校准集,用训练集正常训练模型。第二步,在校准集上计算每个样本的非一致性分数,回归任务中通常取绝对误差。第三步,取分数的经验分位数作为区间半径。
import numpy as np
def split_conformal(model, X_cal, y_cal, X_test, alpha=0.05):
# 1. 在校准集上计算非一致性分数(绝对误差)
cal_pred = model.predict(X_cal)
scores = np.abs(y_cal - cal_pred)
# 2. 取经验分位数(带有限样本修正)
n = len(scores)
q = np.quantile(scores, min(1.0, np.ceil((n + 1) * (1 - alpha)) / n))
# 3. 对新样本给出区间
test_pred = model.predict(X_test)
return test_pred - q, test_pred + q
# lower, upper = split_conformal(model, X_cal, y_cal, X_test, alpha=0.05)
共形预测的妙处在于它是模型无关的包装层:你可以把它套在前面任何方法外面,先得到模型的不确定性缩放,再用共形校正覆盖率。缺点是标准共形给出的区间宽度全局一致,不随样本变化。改进方案如CQR(Conformalized Quantile Regression)用分位数回归先得到自适应宽度的区间,再用共形步骤校正,兼顾了自适应性和理论保证,是目前学术界和工业界都认可的主流方案。
方法选型建议与总结
选方法时可以从三个维度考量:计算预算、理论需求和实现成本。下表做一个简单对比:
| 方法 | 额外训练成本 | 额外推理成本 | 覆盖率保证 | 捕捉的不确定性 |
|---|---|---|---|---|
| Bootstrap | N倍 | N倍 | 无 | 认知 |
| MC Dropout | 几乎为零 | 采样次数倍 | 无 | 以认知为主 |
| 异方差NLL | 零 | 零 | 无 | 偶然 |
| 贝叶斯神经网络 | 高 | 采样次数倍 | 无 | 认知+偶然 |
| 共形预测 | 需校准集 | 零 | 有(可交换性下) | 依赖底层模型 |
实践中一个稳健的组合拳是:用异方差损失建模偶然不确定性,用MC Dropout建模认知不确定性,两者相加得到总方差,最后在校准集上用共形步骤校正覆盖率。这个组合实现简单、各部分职责清晰,在高风险场景的落地中已经被反复验证。
最后提醒一点常见的坑:置信区间并非越窄越好。如果你在测试集上发现区间覆盖率远低于标称值,多半是分布偏移导致的——训练分布和线上分布不一致时,任何基于历史数据的方法都会失效,此时应该监控输入分布的漂移信号,而不是单纯调整区间宽度。不确定性估计的价值不在于给出一个漂亮的数字,而在于让系统知道自己的边界,并在不确定时选择求助而不是硬答。