Can Conformal Prediction Take You from Accurate to Reliable Machine Learning?
TL;DR · AI 摘要
Can Conformal Prediction Take You from Accurate to Reliable Machine Learning? Towards Data Science Machine Learning Can...
核心要点
- 主题聚焦:Can Conformal Prediction Take You from Accurate
- 来源:Towards Data Science,建议结合原文判断细节。
- AI 分析暂不可用,本条为保底评分与摘要。
共形预测能否将你从准确的机器学习带到可靠的机器学习? | Towards Data Science
机器学习
共形预测能否将你从准确的机器学习带到可靠的机器学习?
机器学习模型的采用并不止步于对准确性的认可,而是从这里开始。
Devashish Thakar
2026年10月19日
16分钟阅读
在Google上关注我们
分享
照片由Ioann-Mark Kuznietsov在Unsplash上提供
一个带有箭头的目标
每次有人构建机器学习模型时,焦点会迅速从整体数字和模型健康状况的高技术性衡量指标转移到我们对每个预测结果的信心上。本质上,我的模型能否在所有情况下可靠地提供准确的结果?汇总指标如准确率、召回率、精确率、R²以及AUPRC/AUROC通常不足以说服利益相关者模型的实际表现——无论解释得多么仔细。
这就是共形预测(Conformal Prediction,简称CP)可能提供帮助的地方。除了预测本身,CP还会在预测周围创建一个区间,以量化不确定性。通过预测区间进行的不确定性量化弥合了模型性能的技术性衡量指标与利益相关者信任之间的差距。这使得关于模型可靠性的讨论比依赖整体模型健康状况要容易得多。关于数学的详细解释,请参见Vovk、Gammerman和Shafer于2005年的著作。对于更易理解的概述,可以查阅Angelopoulos和Bates于2022年的研究。Dancker于2024年清晰地解释了CP,而Allen于2023年提供了有用的Python代码演示。
CP算法是一个直观的想法,它在不同预测模型(回归、提升/装袋模型、神经网络等)中具有灵活性,并且无需任何专门的机器学习软件包即可轻松应用。CP算法通过在预测模型周围包裹一个统计过程,为每个预测值生成预测区间。直观地说,应用CP的价值在于解决预测可靠性的问题——77%的客户购买概率很好,但如果这个概率低至50%或高达97%,那么围绕预测概率的这种宽泛范围会使预测的可靠性降低,而不是说预测概率仍将在70%到85%之间。
你可能会想知道为什么选择CP,而不是使用其他知名的不确定性量化方法(如用于神经网络中不确定性量化的引导法或丢弃法)?主要有三个原因:
1)CP不需要特定的模型规格,对黑箱模型也能很好地工作
2)计算成本非常低廉,尤其是与依赖重复重采样的引导法相比;
3)可交换性:根据Vovk、Gammerman和Shafer于1999年的研究,可交换性确保校准(用于计算预测区间)和测试点(用于测试计算出的预测区间)可以被视为来自同一分布。因此,校准集上计算出的预测区间提供了有效的覆盖率保证。如果校准集平均有α比例的校准点落在区间外(而1−α比例在区间内),那么测试点的平均覆盖率水平也将保持相同。
考虑到这些因素,让我们使用公开的在线购物者购买意向数据集(Sakar & Kastro, 2018)来逐步演示算法。该数据集可在UCI机器学习仓库获取,采用Creative Commons Attribution 4.0 International (CC BY 4.0)许可协议,允许商业使用。该数据集非常适合作为本例的演示对象,因为它是一个典型的客户购买预测数据集。
首先,让我们导入本练习所需的所有基础包。
python
# 导入所有必要的包
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
import xgboost as xgb
from sklearn.metrics.pairwise import euclidean_distances接下来,我们加载购物者购买意向数据集。由于某些原因,我无法通过API调用获取数据,因此我直接访问了UCI机器学习仓库并下载了CSV文件。
# 加载在线购物者数据集
df = pd.read_csv("online_shoppers_intention.csv")
# 特征和目标变量
X = df.drop("Revenue", axis=1)
y = df["Revenue"].astype(int)
# 将字符串列转换为分类类型(针对Month和VisitorType)
X_encoded = pd.get_dummies(X, drop_first=True)CP算法需要将数据划分为三个部分:
- 训练集用于学习模型(如分类器、神经网络等)
- 校准集用于通过符合分数量化模型的不确定性,从而构建预测区间(稍后会详细说明)
- 测试集用于评估模型的可靠性及其不确定性量化效果
通过将训练集/校准集/测试集按70%/15%/15%的比例划分实现上述目标:
# 将数据划分为70%训练集和30%临时数据集
X_train, X_temp, y_train, y_temp = train_test_split(
X_encoded, y, test_size=0.3, random_state=42, stratify=y
)
# 将30%临时数据集划分为15%校准集和15%测试集
X_cal, X_test, y_cal, y_test = train_test_split(
X_temp, y_temp, test_size=0.5, random_state=42, stratify=y_temp
)基于这种样本设计,让我们逐步进行:
第一步:训练模型
我首先使用XGBoost模型对在线购买意向数据集进行训练,以理解所有可用特征对购买概率的影响(Revenue=1表示用户会话导致了电子商务购买,0表示未购买)。为简洁展示CP算法,本文中不进行任何额外的特征工程。
在建模之前,让我们首先查看Revenue变量的分布,以确定是否存在类别不平衡问题。
# 计算结果的计数和百分比
counts = y.value_counts()
percentages = y.value_counts(normalize=True) * 100
# 创建柱状图
plt.figure(figsize=(6, 4))
sns.countplot(x=y, palette="Set2")
# 添加标题和标签
plt.title("目标变量(Revenue)类别分布", fontsize=14)
plt.xlabel("类别(0 = False, 1 = True)", fontsize=12)
plt.ylabel("数量", fontsize=12)
plt.xticks(ticks=[0, 1], labels=[f"0 ({percentages[0]:.1f}%)", f"1 ({percentages[1]:.1f}%)"])
plt.tight_layout()
plt.show()图1:类别不平衡可视化(作者原创)
/think
基于图1中Revenue变量中1和0的分布情况,仅有15%的数据点属于正类(=1)。因此我们需要进行不平衡处理。同样,为了简洁性,我使用加权XGBoost,尽管我们也可以使用SMOTE和随机欠采样等其他方法进行更严格的筛选。
# 加权XGBoost
pos = sum(y_train == 1)
neg = sum(y_train == 0)
scale = neg/pos
# 定义模型
model_weighted = xgb.XGBClassifier(
n_estimators=200,
max_depth=5,
learning_rate=0.05,
subsample=0.8,
colsample_bytree=0.8,
use_label_encoder=False,
eval_metric="logloss",
scale_pos_weight=scale)
# 在训练集上训练模型
model_weighted.fit(X_train, y_train)现在我们已经拟合了模型,让我们看看按重要性排序的前10个特征
xgb.plot_importance(model_weighted, max_num_features=10, importance_type="gain")
plt.title("Top 10 Features by Importance (XGBoost)")
plt.show()图2:特征重要性评分图(作者提供)
直观来看,特征重要性是有意义的。例如,客户访问网页的次数(PageValues)显然是购买决策最重要的指标,其次是购买月份(例如11月可能受感恩节假期影响)。同样,客户在做出决策前通常会多次访问产品页面,因此VisitorType_Returning_Visitor出现在重要性列表中也合乎逻辑。
现在模型已经训练完成,让我们转向校准其不确定性。
第2步:校准模型
校准帮助我们衡量预测与现实之间的差距,并围绕预测构建置信区间。它包含四个步骤。
- 在校准集上预测概率 p(x)
- 计算符合性分数,即预测概率 p(x) 与真实标签(y)的接近程度。它可以采用多种形式;我选择一个简单直观的形式:|y-p(x)|,其中y是真实标签(1或0)。这确保了分数越大,与真实标签的差距越大。你可以通过几个例子自行验证这一点。当然还有其他方法可以衡量符合性分数,如逆概率权重、标准化残差等。同样,出于简洁性考虑,我在这里选择使用绝对残差。
- 选择可接受的误差容忍度 α。将 α 看作我们愿意接受的预测落在置信区间外的观测比例。假设 α = 30%,这意味着30%的预测会落在置信区间外。如果我们反过来思考,这意味着置信区间将包含至少70%的真实标签。
- 找出上述符合性分数的1-α百分位值(70百分位)以获得符合性分位数(q)。我们也可以将此称为置信区间,我们用它来在测试数据的预测概率周围构建置信带。
我在下表1中用一个简单的例子来解释这一点。对于每个客户,我们计算了购买的预测概率以及基于与真实标签差异的符合性分数(最后一列)。如果我们按升序排列符合性分数:[0.1,0.2,0.25,0.35,0.4],70百分位的分数将是0.35 → 符合性分位数。
表1:计算符合分数的示例
以下代码块用于预测概率并计算校准集上的符合分数和符合分位数。
# 校准步骤
p_cal = model_weighted.predict_proba(X_cal)[:, 1]
scores = np.abs(y_cal - p_cal)
alpha = 0.3 # 可根据可接受的容忍度由用户进行修改
q = np.quantile(scores, 1 - alpha) # 确定的符合分位数校准为我们提供了基于分位数的区间,帮助我们判断预测在未见过的(测试)数据上的可靠性,这将在下一节中进行说明。
第3步:应用于测试集
第3.a步:固定区间方法
首先,我们使用第2步中从校准数据得出的符合分位数,对测试集上的预测概率生成预测区间。如前一步所示,符合分位数为0.35。我再次参考下表2中的假设示例,使用预测概率和符合分位数来开发预测区间。
表2:使用符合分位数计算预测区间的示例
通过这个虚拟示例可以看出,客户F具有较高的购买预测概率(80%),但预测区间较宽。与客户H相比,客户H不仅具有较高的预测概率(90%),还具有较窄的预测区间,因此预测可靠性较低。
请注意,由于我们处理的是二分类模型,概率范围不能超出0到1,因此需要将区间截断到0和1。
以下是将此方法应用于测试集并围绕预测生成预测区间的代码块。
# 将基本符合预测应用于测试集
p_test = model_weighted.predict_proba(X_test)[:, 1]
# 根据确定的分位数提取预测区间
intervals_const = np.vstack([
np.clip(p_test - q, 0, 1),
np.clip(p_test + q, 0, 1)
]).T此练习的目的是检查每个预测是否具有不同的预测区间。因此,让我们在图3中绘制每个测试数据点,观察预测区间是否因数据点而异。
# 按预测概率从低到高排序测试数据点
order = np.argsort(p_test)
p_sorted = p_test[order]
intervals_sorted = intervals_const[order]
# # 绘制预测概率
plt.figure(figsize=(10,6))
plt.plot(p_sorted, color="blue", label="测试数据上的预测概率")
plt.fill_between(np.arange(len(p_sorted)),
intervals_sorted[:,0], intervals_sorted[:,1],
color="gray", alpha=0.3, label="预测区间")
plt.xlabel("测试数据点索引")
plt.ylabel("类别1的概率")
plt.legend()
plt.show()图3:使用固定区间方法绘制的预测概率(蓝色)和预测区间(灰色带)(图片由作者提供)
请注意,这是预测区间的朴素版本——测试数据点之间的区间差异是由于在0和1处的截断造成的。理论上,如果没有截断,这将等于2q。然而,从本质上讲,每个测试数据点周围几乎相等的预测区间并不能帮助我们解决原始问题,即每个预测点具有不同水平的可靠性或置信度。接下来,我将介绍该方法的扩展,考虑局部自适应区间。
第3.b步:自适应区间方法
不再使用从校准样本中提取的固定预测区间,而是根据每个校准点与每个测试数据点的相似性对校准点进行加权。因此,如果x表示测试数据点的特征集合,x’表示第i个校准集的特征,那么我们可以计算它们之间的距离。让我们使用下面定义的高斯核(Bishop,2006)来确定欧几里得距离,尽管这里也可以使用其他许多距离度量方法。同时,我将σ设为计算出的距离的25百分位数。σ越小,权重对校准数据点变化的适应性越强。
让我们以一个单变量示例进行说明,变量为PageValues,并参考表3中展示的客户数据点示例。假设我们有一个测试数据点,其PageValues=45,预测概率为0.75。在三个校准客户中,他们的PageValues(第二列)以及计算出的符合分数(第三列)如下。假设σ=10,我们计算欧几里得距离。对于这三个客户,他们与测试数据点的原始距离分别为(45–20=25)、(45–40=5)和(45–60=15)。根据上述公式,我们可以估算出每个数据点的原始权重分别为0.044、0.882和0.324。如果将这些权重归一化(总和约为1.25),则最后一列显示归一化后的权重。显然,PageValues为45的测试点最接近客户B(获得71%的权重),而距离客户A最远(仅获得4%的权重)。
为简化说明,本例仅使用了一个特征(即PageValues)。不过,欧几里得函数可以纳入数据中的所有特征来计算点对点距离。
表3:估计高斯距离的示例
接下来,如果我们按排序后的符合分数计算累积权重,结果将是客户A→0.04,客户B→0.75(0.04+0.71),客户C→1(0.04+0.71+0.26)。同样,当α=30%时,1-α百分位数是累积权重大于或等于1-α(=70%)的最小分数。因此,这个值将是0.2。因此,这个特定测试数据点的预测区间将是0.2。如果我们对每个测试数据点重复此过程,将得到一个根据其与校准数据相似性自适应调整的预测区间,而不是之前估计的单一标准分位数。
以下代码块将为每个测试数据点构建自适应预测区间:
# 测试到校准距离
dist = euclidean_distances(np.array(X_test), np.array(X_cal))
# Sigma
sigma = np.percentile(dist, 25)
# 为每个测试点计算q值
q_local = []
for i in range(len(X_test)):
d = dist[i].ravel() # 到校准点的距离
w = np.exp(-d**2 / (2 * sigma**2))
w = w / w.sum()
# 将评分转换为NumPy数组
scores_np = np.array(scores)
# 联合排序评分和权重
idx = np.argsort(scores_np)
scores_sorted = scores_np[idx]
weights_sorted = w[idx]
# 累积权重
cum_weights = np.cumsum(weights_sorted)
# 加权分位数的截断值
cutoff = (1 - alpha) * weights_sorted.sum()
q_i = scores_sorted[cum_weights >= cutoff][0]
q_local.append(q_i)
q_local = np.array(q_local)
# 构建自适应区间
intervals_adaptive = np.vstack([
np.clip(p_test - q_local, 0, 1),
np.clip(p_test + q_local, 0, 1)
]).T与固定预测区间图类似,如果我们想绘制这些以直观感受每个预测的波动性,可以这样做:
# 按预测概率对测试预测和区间进行排序
order = np.argsort(p_test)
p_sorted = p_test[order]
intervals_sorted = intervals_adaptive[order]
# 绘制预测概率
plt.figure(figsize=(10,6))
plt.plot(p_sorted, color="blue", label="测试数据上的预测概率")
plt.fill_between(np.arange(len(p_sorted)),
intervals_sorted[:,0], intervals_sorted[:,1],
color="gray", alpha=0.3, label="核加权预测区间")
plt.xlabel("测试数据点索引")
plt.ylabel("类别1的概率")
plt.title("核加权预测区间")
plt.legend()
plt.show()图4:展示预测概率(蓝色)和预测区间(灰色带+尖峰)的图表,采用自适应区间方法(图像由作者提供)
如图4所示,大量尖峰表明我们正在根据测试集中的每个数据点调整预测区间。
第4步:整合所有步骤以实现可靠预测
现在让我们来估算可能进行购买的客户数量。假设我们认为70%的预测购买概率是一个合理的阈值,用于区分可能购买者和非购买者。
# 预测概率大于70%的测试点
mask_highprob = p_test >= 0.7
p_high = p_test[mask_highprob]
p_low = p_test[~mask_highprob]
# 定义数据
labels = ["可能购买", "不太可能购买"]
counts = [len(p_high), len(p_low)]
# 柱状图
plt.figure(figsize=(6,5))
bars = plt.bar(labels, counts, color=["green", "red"], alpha=0.7)
for bar in bars:
height = bar.get_height()
plt.text(bar.get_x() + bar.get_width()/2.0, height, f'n = {height}',
ha='center', va='bottom', fontsize=10, fontweight='bold')
plt.ylabel("客户数量")
plt.title("可能购买的客户数量")
plt.show()图5:展示可能购买的客户数量(预测概率≥70%)与不太可能购买的客户数量(预测概率<70%)的对比(图像由作者提供)
从图5可以看出,在1850个客户中(16.5%),模型预测有305个客户属于可能购买群体。但这些估计值是否可靠呢?
为了回答这个问题,我首先查看为每个预测测试数据点创建的预测区间。具体来说,我试图理解每个预测点的预测区间(即上下分位数之间的差异)在实际预测概率上的变化情况。
# 构建DataFrame
df = pd.DataFrame({
"p_test": p_test,
"low": intervals_adaptive[:,0],
"high": intervals_adaptive[:,1]
})
# 创建数据集副本
df_class1 = df.copy()
# 区间宽度
df_class1["width"] = df_class1["high"] - df_class1["low"]
# 散点图
plt.figure(figsize=(8,6))
plt.scatter(df_class1["p_test"], df_class1["width"], alpha=0.1, color="purple")
plt.xlabel("预测概率(类别1)")
plt.ylabel("预测区间")
plt.title("预测概率与区间宽度的相关性")
plt.show()图6:展示预测区间与预测概率关系的图表(作者提供)
图6表明,平均而言,预测区间在达到50%标记前持续增加,随后又开始下降。这种现象是符合预期的,因为处于分布极端值的预测通常会有明确的理由支撑。自然地,当模型对预测结果非常确定时(例如明显属于1或0的情况),预测区间会更窄。而中间部分由于存在不确定性,预测区间反而会扩大(可靠性下降)。
然而,需要注意的是,对于预测概率超过70%的n=305个数据点,预测区间可能有显著差异,如下图7所示。
# 统计超过阈值概率的观测总数
df_class2 = df_class1[df_class1['p_test'] >= 0.7]
num_obs = len(df_class2)
# 创建直方图
ax = df_class2['width'].hist()
ax.set_title(f'可能购买者的预测区间分布(n = {num_obs})')
ax.set_xlabel('预测区间')
ax.set_ylabel('频率')
# 在每个直方图条形上方添加计数
for rect in ax.patches:
height = rect.get_height()
if height > 0:
ax.annotate(f'{int(height)}',
xy=(rect.get_x() + rect.get_width() / 2, height),
xytext=(0, 5),
textcoords="offset points",
ha='center', va='bottom')
# 在x=0.3处添加垂直红线
ax.axvline(x=0.3, color='red', linestyle='--', linewidth=2, label='预测区间=30%')
ax.legend()
plt.show()图7:可能购买者(预测概率≥70%)的预测区间分布(作者提供)
选择最可靠预测有两种方法:
- 方案A:预测区间的边界应在预测值的某个百分比范围内。例如预测值为80%时,若希望预测宽度控制在该预测值的30%以内,则认为当边界下限>56%且上限≤104%时预测可靠。这会使305个潜在购买者中有174个(57%)被判定为可靠。
# 统计预测区间在预测值某个范围内的预测数量
within_30pct = len(df_class2[
(df_class2['low'] >= df_class2['p_test'] * 0.70) &
(df_class2['high'] <= df_class2['p_test'] * 1.30)
])
# 输出结果
print(f"预测区间在p_test±30%范围内的数据点数量: {within_30pct}")- 选项 B:预测宽度在某个截止值以内,比如 30% → 即任何预测区间宽度小于或等于 30% 的潜在购买者都被视为可靠。在这种情况下,305 个潜在购买者中有 129 个观察值被认定为可靠(=37.4%)。
# 预测宽度在特定值以内的预测
less_than_30_pc = len(df_class2[df_class2['width'] <= 0.3])
print(f"预测宽度 <= 30% 的观测值数量: {less_than_30_pc}")- 选项 C:预测宽度必须在预测概率的某个阈值范围内。假设我们继续使用 30% 的宽度阈值。如果预测概率为 80%,则宽度不应超过 24%。在这种情况下,305 个潜在购买者中有 103 个观察值被认定为可靠(=29.9%)。
# 每个测试点预测值的 30% 范围内的所有观测值
within_30_pc_pred = len(df_class2[df_class2['width'] <= (0.30 * df_class2['p_test'])])
print(f"宽度不超过 p_test 30% 的观测点数量: {within_30_pc_pred}")结论
正如你所看到的,CP 算法通过用户定义的阈值,为我们提供了选择可靠预测的额外保障,这些阈值可以广泛且轻松地应用于大多数预测场景。它告诉我们每个预测的可信程度,而不仅仅依赖于整体模型的健康状况。从实际角度来看,这可以作为机器学习模型的有力补充,有助于建立利益相关者的信任。这种可见性也提高了机器学习模型的可操作性。尽管这种方法可能不提供正式的覆盖率保证,但对数据科学家来说,自适应方法可能是一个特别有用的工具包。
如引言中前所述,这并不是量化模型不确定性的唯一方法。根据数据和上下文,其他方法可能更适合。所展示的结果是实验结果,而非 CP 算法数学保证的证明。因此,请将本文和结果视为实用指南,而非对该主题的最终结论。此外,这反映了我自己的学习历程,如果你发现错误或有任何建议,我非常欢迎你的反馈和想法。
免责声明:观点仅代表我个人,不反映任何雇主的观点。
参考文献:
- Allen, M. (2023). 从基础开始:机器学习分类的符合性预测 . [Towards Data Science]
- Angelopoulos, A. N., & Bates, S. (2022). 符合性预测与分布自由不确定性量化的温和介绍 . arXiv 预印本 arXiv:2107.07511v6
- Bishop, C. M. (2006). 模式识别与机器学习 . Springer
- Dancker, J. (2024). 你只需要符合性预测 . [Towards Data Science]
- Sakar, S., & Kastro, Y. (2018). 在线购物者购买意向数据集 . UCI 机器学习存储库. https://doi.org/10.24432/C5F88Q . 遵循 CC BY 4.0 许可证
- Vovk, V., Gammerman, A., & Shafer, G. (2005). 随机世界中的算法学习 . Springer