qinghuan
← 回到知识探索

AI 直觉 / 可下载小项目

区间写着九成覆盖,为什么某一类样本一次也没包住?

从八条校准误差和九个可能名次出发,理解共形预测的取整、无穷阈值与覆盖边界。

展开本文目录
  1. 下载完整练习包
  2. 先动手看看
  3. 先固定预测器,再观察它的误差
  4. 为什么取名次时多算一个新样本
  5. 样本太少时,正确结果可能是不设有限边界
  6. 整体九成,仍能和某组零覆盖同时发生
  7. 完整枚举名次,再检查构造反例
  8. 拿到“90%”时,先补齐它的主语
  9. 记住这些要点
  10. 别把例子用过头
  11. 自己试一小步
  12. 查原始资料
开始前,会这些就够了
遇到不熟悉的词,先看这 3 个解释
边际覆盖率
预测区间或集合包含真实结果的比例,要对校准数据和新测试样本的随机性一起考虑。它描述整体层面的保证,不自动给每个输入或每个分组相同保证。
可交换性
把校准样本与未来样本的位置交换后,整体随机规律保持相同;数据来自同一分布并独立采样是一个常见充分条件。
边际覆盖
把校准数据和未来测试点的随机性一起平均,讨论真实值有多大比例被区间包住。
打开完整术语小抄 ↗

某个交付时长预测器给出一个区间,整体上有九成真实时长落在区间里。你可能接着以为:每种订单也至少有九成,或者眼前这一单有九成把握。前一句整体统计并不能自动推出后面两句。

共形预测用一份独立校准数据,把已有预测器的误差转成区间或候选集合。它的保证很有用,但要连同数据可交换、校准方式和“对谁取平均”一起读,才不会把保证用到没有承诺的地方。

把例子带到自己的电脑上

下载完整练习包

需要 Python 3.10 或更新版本,无第三方依赖;制作时实测 Windows / Python 3.14.0。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。

下载 ZIP · 5 个文件
包里有什么
  • conformal_ranks.py:完整可运行示例
  • README.md:运行说明与独立练习
  • observed.stdout.txt:本次实际输出
  • verification.json:源码和产物验证记录
  • SHA256SUMS.txt:内容摘要

在这个文件夹打开 PowerShell,逐行运行

python --version
python -B conformal_ranks.py

解压到独立练习文件夹,在含 conformal_ranks.py 的目录打开终端运行。给定残差的有限名次枚举与人为分组反例;未训练预测器、未评估真实数据、未运行模型 API。

找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。

只差一个秩,覆盖了多少次?

固定 9 个误差分数:1—9。每次留一个作测试,其余 8 个校准;分数越大表示误差越大。逐个换测试项,比较两种阈值位置。

校准分数(从小到大):1、2、3、4、5、6、7、8。n = 8,计算得到 k = 8。

阈值 q = 8:取校准分数中第 8 小的值。

测试分数 9 > q,这一次没有覆盖。

9 种留出位置中覆盖 8/9,约 88.9%。

  1. 测试 1阈值 9覆盖
  2. 测试 2阈值 9覆盖
  3. 测试 3阈值 9覆盖
  4. 测试 4阈值 9覆盖
  5. 测试 5阈值 9覆盖
  6. 测试 6阈值 9覆盖
  7. 测试 7阈值 9覆盖
  8. 测试 8阈值 9覆盖
  9. 测试 9阈值 8未覆盖

这是固定分数的穷举演示,没有训练预测模型。共形预测的边际覆盖结论需要校准样本与新样本满足可交换性等前提;它不保证每个子群或某个具体输入都有相同覆盖率。无限大阈值虽然覆盖,却失去区分能力。

先固定预测器,再观察它的误差

以回归为例,已有预测函数 f(x)。在没有用于训练或调参的校准样本上,计算 s_i=|y_i−f(x_i)|。把这些绝对误差从小到大排序,选一个阈值 q;新输入输出 [f(x)−q, f(x)+q]。

本例直接给出残差数字,不训练预测器。这样可以隔离“怎样选名次”这个问题。真实系统中如果反复查看校准标签、选择模型、修改特征后仍复用同一份校准数据,简单的保证就不能原样照搬。

基本 split conformal 依靠校准样本与新测试样本的可交换性,常见充分条件是它们来自同一分布且独立采样。所谓分布无关不是允许随意分布漂移,而是不需要事先指定误差服从正态分布等具体形状。

为什么取名次时多算一个新样本

给定 n 条校准误差与目标错误率 α,采用 k=ceil((n+1)×(1−α)),q 取第 k 小的校准误差。这里多出来的 1 为未来测试样本留了一个名次。使用 ceil(n×(1−α)) 会把这个名次遗漏。

取九个互不相同的误差 1 到 9,每次留一个作测试,其余八个作校准;目标 α=0.2。正确公式需要 k=8,等价于取八个校准误差中的最大值。九种可能测试名次里,只有最大者 9 没被包住,覆盖为 8/9≈88.9%。

漏掉 +1 时会取 k=7,九种名次里只覆盖 7/9≈77.8%,低于目标 80%。这不是通过大量随机试验碰巧得到的结果;程序完整枚举了九个测试名次。它说明这个有限名次模型中的算术差别。

样本太少时,正确结果可能是不设有限边界

仍只有 n=8 条校准误差,如果目标变成 90% 覆盖,k=ceil(9×0.9)=9,已经超过校准样本数。按本文定义应把 q 设为正无穷,输出整个实数轴。不能为了得到好看的有限区间,偷偷把 k 截成 8。

无穷区间几乎没有决策价值,但它诚实地揭示:这份样本量与这条通用秩保证不足以产生有限阈值。工程实现可以提示样本不足,不能悄悄把保证改弱后仍沿用原标签。

误差可能并列。本文使用 s≤q 的包含规则,九个误差都等于 5 时覆盖全部九个。并列会带来保守性;本文没有随机打破并列,因此不声称覆盖率总是恰好等于 1−α。

整体九成,仍能和某组零覆盖同时发生

另造一份固定校准残差:90 个 1 与 9 个 10。n=99、α=0.1,k=90,阈值 q=1。再检查一个人为构造的测试表:90 个容易样本误差为 0.5,10 个困难样本误差为 5。

结果是整体覆盖 90%,容易组 100%,困难组 0%。这只是明确构造的分组反例,不是假装从某个真实总体随机抽到的数据,也不是对一个训练模型覆盖保证的实测。它说明整体比例无法推出每组比例。

若后续输入全部变成困难样本,沿用原阈值的观测覆盖降为 0%。这也提醒我们检查校准分布与上线输入的关系。需要分组保证时,必须使用满足相应条件的方法与足够的组内校准数据;不能把边际保证直接改名为逐组保证。

完整枚举名次,再检查构造反例

保存为 conformal_ranks.py,运行 python -B conformal_ranks.py。它会枚举九个留出名次,检查 α=0.1 的无穷阈值、并列误差和两组反例。report.json 把无穷写成字符串,避免把非标准 Infinity 塞进 JSON。

程序使用 Fraction 保留示例十进制 α 的精确值,避免浮点数在整数边界附近导致 ceil 意外多取一位。它没有调用统计库、训练模型或模型 API;不能把这次数学演示当成部署覆盖率验收。

展开收起代码 · 小实验python · 68 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

小实验 · python
"""Finite-rank conformal arithmetic with synthetic residuals; no model training."""
import json
import math
from fractions import Fraction
from pathlib import Path


def threshold(scores, alpha):
    if not scores or not 0 < alpha < 1:
        raise ValueError("nonempty scores and alpha strictly between zero and one required")
    if any(not math.isfinite(s) or s < 0 for s in scores):
        raise ValueError("finite nonnegative residuals required")
    # Fraction keeps the displayed decimal alpha exact at integer boundaries.
    a = Fraction(str(alpha))
    rank = math.ceil((len(scores)+1)*(1-a))
    return (sorted(scores)[rank-1] if rank <= len(scores) else math.inf), rank


def naive_threshold(scores, alpha):
    rank = math.ceil(len(scores)*(1-Fraction(str(alpha))))
    return sorted(scores)[rank-1]


def main():
    all_scores = list(range(1, 10))
    trials = []
    # Condition on nine distinct scores; each one is equally likely to be the test score.
    for index, test in enumerate(all_scores):
        calibration = all_scores[:index]+all_scores[index+1:]
        q, rank = threshold(calibration, 0.2)
        wrong_q = naive_threshold(calibration, 0.2)
        trials.append({"test_score": test, "q":q,"rank":rank,
                       "covered":test <= q,"naive_covered":test <= wrong_q})
    covered, naive_covered = sum(t["covered"] for t in trials), sum(t["naive_covered"] for t in trials)
    assert covered == 8 and naive_covered == 7
    print(f"nine possible held-out ranks: corrected={covered}/9 naive={naive_covered}/9")
    q, rank = threshold(list(range(1, 9)), 0.1)
    assert rank == 9 and math.isinf(q)
    print(f"n=8 alpha=0.1: required_rank={rank} threshold=infinity")
    tie_q, _ = threshold([5]*8, 0.2)
    assert tie_q == 5 and all(5 <= tie_q for _ in range(9))
    print("tied residuals: inclusive <= threshold covers 9/9")
    calibration = [1.0]*90+[10.0]*9
    group_q, group_rank = threshold(calibration, 0.1)
    groups = {"easy": [0.5]*90, "hard": [5.0]*10}
    coverage = {name:sum(s <= group_q for s in scores)/len(scores) for name,scores in groups.items()}
    pooled = sum(s <= group_q for scores in groups.values() for s in scores)/100
    shifted = sum(s <= group_q for s in [5.0]*100)/100
    assert group_q == 1 and group_rank == 90
    assert pooled == 0.9 and coverage == {"easy":1.0,"hard":0.0} and shifted == 0.0
    print(f"constructed groups: q={group_q:.1f} pooled={pooled:.1%} easy={coverage['easy']:.1%} hard={coverage['hard']:.1%}")
    print(f"all-hard shifted input: observed coverage={shifted:.1%}")
    invalid = 0
    for scores, alpha in [([], 0.1), ([1], 0), ([1], 1), ([-1], 0.1), ([math.nan], 0.1)]:
        try:
            threshold(scores, alpha)
        except ValueError:
            invalid += 1
    assert invalid == 5
    print("checks=11 passed; ranks and constructed residuals only")
    Path("report.json").write_text(json.dumps({"rank_trials":trials,"rank_corrected_hits":covered,
        "naive_hits":naive_covered,"unbounded_case":{"n":8,"alpha":0.1,"rank":9,"q":"infinity"},
        "group_threshold":group_q,"group_coverage":coverage,"pooled_coverage":pooled,
        "shifted_coverage":shifted,"checks":11}, indent=2, allow_nan=False), encoding="utf-8")


if __name__ == "__main__":
    main()

怎样运行:在独立练习文件夹中执行 python -B conformal_ranks.py。2026-10-17 已在 Windows、Python 3.14.0 运行此份源码并通过自带检查。给定残差的有限名次枚举与人为分组反例;未训练预测器、未评估真实数据、未运行模型 API。

运行后对照结果
nine possible held-out ranks: corrected=8/9 naive=7/9
n=8 alpha=0.1: required_rank=9 threshold=infinity
tied residuals: inclusive <= threshold covers 9/9
constructed groups: q=1.0 pooled=90.0% easy=100.0% hard=0.0%
all-hard shifted input: observed coverage=0.0%
checks=11 passed; ranks and constructed residuals only

拿到“90%”时,先补齐它的主语

基本边际覆盖对校准数据和未来测试点的随机性一起取平均。它不是说任意固定的一份校准集都必然达到 90%,也不是说每一个特征值或人群都获得 90%。只写“90% 置信”会掩盖这些区别。

对预测区间还要同时报告宽度。覆盖所有数值当然不会漏,但无法帮助行动;一个实际有用的系统需要在保证适用的条件下取得可接受的区间大小,并观察困难组、数据变化和校准样本数。

本次核对的是标准 split conformal 的秩构造与边际/条件覆盖的区别,没有实现时序共形、加权共形、逐组校准或条件覆盖算法。它们需要另外的假设与验证。

记住这些要点

  • 有限样本名次采用 ceil((n+1)(1−α)),不能漏掉新样本那一位。
  • k 超过 n 时不能偷偷截断,本文用正无穷表达没有有限阈值。
  • 边际覆盖同时平均校准与测试随机性,不保证任意固定校准集或每个分组。
  • 覆盖率要和区间宽度、分组表现及分布条件一起看。

别把例子用过头

  • 把分布无关理解成不受任何分布变化影响。
  • 把校准集重复用于选模型、挑参数,再沿用原始保证。
  • 用插值分位数替代约定的秩统计量,却不检查有限样本边界。
  • 把人为构造的分组反例写成真实模型覆盖实测。

自己试一小步

如果两类订单的误差范围差得很大,独立按组校准会带来怎样的区间与样本量问题?

  1. 准备两个互不用于拟合的组内校准残差表,先分别记录样本量。
  2. 使用相同 α 对两组取阈值,显示 k、q 和区间宽度。
  3. 逐步减少其中一组样本,观察何时出现无穷阈值。
  4. 预先固定测试表,分别汇总整体与组内覆盖,不根据结果回头挑 α。

做到这些,就算完成

  • 保留每组 k>n 的处理。
  • 解释分组如何在观察测试标签前确定。
  • 明确这只是示例验证,真实组内保证仍依赖组内数据条件。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。