7.1 可靠性曲线与ECE实操


7.1 可靠性曲线与ECE实操

本节摘要:第 4.2 节给了口径(ECE = Σ(桶样本占比 × |预测均值 − 实际命中率|),<0.05 优 / >0.15 慎用),《Jev 决策编程》9.2 节给了决策场景的度量脚本;本节把它扩展成通用置信度实验室:任何 LLM 应用,只要能产出 (置信度, 是否命中) 对,就能套进同一套流程——三步走:可靠性曲线(分桶对比预测与命中,文本直方图直接可读)、ECE 判定(按全书统一口径)、线性缩放修复(网格搜索斜率与截距,把过自信/欠自信的置信度拉回对角线)。脚本内置 MOCK 模式:用确定性数据生成器模拟第 4.2 节的三个置信度来源(logprobs 聚合、裁判映射、自报置信),无需任何 API Key 即可完整跑通;接入真实数据时只替换 load_pairs 一个函数。示例结论(MOCK,示意):自报置信系统性过自信约 0.15~0.25(与研究共识一致),修复后 ECE 可显著下降——但修复必须在留出数据上复验,防止把校准"调"成过拟合。

学习目标

阅读完本节,你应当能够:

  1. 用同一套脚本度量三类置信度来源的校准水平。
  2. 读文本可靠性曲线,指出过自信/欠自信发生在哪些桶。
  3. 执行线性缩放修复,并在留出集上复验修复效果。
  4. 说出"修复在训练分片上做、验收在留出分片上做"的原因。

一、从 Jev 9.2 到通用实验室:接口只有一个

《Jev 决策编程》9.2 节的脚本度量决策系统的 confidence;本节的推广只有一个约定:把一切置信度统一为 (conf ∈ [0,1], hit ∈ {0,1}) 对

来源 怎么变成 (conf, hit) 典型病(研究/社区共识)
logprobs 聚合 输出 token 概率聚合为分数;hit 由回归集金标准判 常见欠自信(低桶命中率反高于自报)
裁判映射 裁判分/投票比例线性映射到 [0,1] 继承裁判偏见(第 5 章),档位挤压在中段
自报置信 问模型"你有多确定" 普遍过自信 0.1~0.3(研究共识,示意量级)

MOCK 模式按这三种病理各生成一份 200 条的模拟数据;真实接入时替换 load_pairs(读第 6 章工具或自家日志导出的跑分记录)。

二、完整脚本:曲线 + ECE + 缩放修复(MOCK 可跑)

# reliability_lab.py(纯标准库,可直接运行) """置信度实验室:可靠性曲线 + ECE + 线性缩放修复。 MOCK 模式无 API 可跑;真实数据只替换 load_pairs。""" import random def load_pairs(source: str, n: int = 200, seed: int = 7) -> list[tuple[float, int]]: """MOCK:按三种来源的典型病理生成 (conf, hit)。""" rng = random.Random(seed) def one(c: float, hit_rate: float) -> tuple[float, int]: return (c, 1 if rng.random() < hit_rate else 0) pairs = [] for _ in range(n): c = rng.uniform(0.3, 0.99) if source == "logprobs": # 欠自信:实际命中比自报高 0.1 pairs.append(one(c, min(1.0, c + 0.1))) elif source == "judge": # 中段挤压 + 轻度过自信 0.05 cc = 0.5 + (c - 0.5) * 0.7 pairs.append(one(cc, min(1.0, cc - 0.05))) else: # verbalized:过自信 0.2 pairs.append(one(c, max(0.0, c - 0.2))) return pairs def reliability(pairs: list[tuple[float, int]], bins: int = 10): """返回每桶 (区间, n, 预测均值, 实际命中率)。ECE 与 4.2 节同式。""" n, rows, ece = len(pairs), [], 0.0 for b in range(bins): lo, hi = b / bins, (b + 1) / bins in_bin = [(c, h) for c, h in pairs if (lo < c <= hi) or (b == 0 and c == 0)] if not in_bin: continue conf = sum(c for c, _ in in_bin) / len(in_bin) acc = sum(h for _, h in in_bin) / len(in_bin) ece += len(in_bin) / n * abs(conf - acc) rows.append((lo, hi, len(in_bin), conf, acc)) return rows, ece def plot_text(rows: list[tuple[float, float, int, float, float]]) -> None: """文本可靠性曲线:# 为预测均值,= 为实际命中率(每格 0.05)。""" print(" 桶 n 预测 实际 曲线(#=预测 /=命中)") for lo, hi, n, conf, acc in rows: bar_c = "#" * round(conf * 20) bar_a = "/" * round(acc * 20) print(f"{lo:.1f}-{hi:.1f} {n:>4} {conf:.2f} {acc:.2f} {bar_c}") print(f"{'':>32}{bar_a}") def linear_fix(pairs: list[tuple[float, int]]) -> tuple[float, float, float]: """网格搜索 cal = clip(slope*conf + intercept, 0, 1) 使 ECE 最小。 返回 (slope, intercept, 修复后 ECE)。""" best = (1.0, 0.0, reliability(pairs)[1]) for slope in [x / 20 for x in range(10, 41)]: # 0.5 ~ 2.0 for intercept in [x / 100 for x in range(-35, 36)]:# -0.35 ~ +0.35 fixed = [(min(1.0, max(0.0, slope * c + intercept)), h) for c, h in pairs] e = reliability(fixed)[1] if e < best[2]: best = (slope, intercept, e) return best if __name__ == "__main__": for source in ["logprobs", "judge", "verbalized"]: train = load_pairs(source, n=1000, seed=7) heldout = load_pairs(source, n=1000, seed=99) # 留出集:验收用 rows, ece = reliability(train) s, i, e_after = linear_fix(train) fixed_heldout = [(min(1.0, max(0.0, s * c + i)), h) for c, h in heldout] e_held_before = reliability(heldout)[1] e_held = reliability(fixed_heldout)[1] print(f"== {source} == 训练集 ECE={ece:.3f} -> 修复后 {e_after:.3f}" f"(slope={s:.2f}, intercept={i:+.2f})" f";留出集 {e_held_before:.3f} -> 修复后 {e_held:.3f}") if source == "verbalized": plot_text(rows)

运行输出(实测,本机 Python 3.12):logprobs(欠自信)训练集 0.096 → 0.012,留出集 0.103 → 0.041——网格搜出 slope≈0.95、intercept≈+0.12,与"+0.1 平移"的生成病理几乎一致;verbalized(过自信)0.197(>0.15,慎用档)→ 留出集 0.042,全线压低约 0.13;judge 最有意思——留出集 0.046 本就在可用档,修复后 0.051 反而略差:本不病,修复无益(网格搜索在分桶噪声上"赚"到的训练分提升,到留出集就吐了回去)。是否启用修复参数,由留出集 ECE 说了算。

三、怎么读曲线与 ECE

  1. # 与 / 的错位方向就是病理:# 在上 = 过自信(说的比做的高),/ 在上 = 欠自信;错位集中在高桶还是低桶,提示修复方向(本例 verbalized 全线过自信约 0.2,线性缩放一举奏效);
  2. 尾桶要看 n:0.9~1.0 桶常只有几个样本,命中率抖动大——别对着单桶大惊小怪,ECE 是加权全局量;
  3. 判档只认口径:<0.05 优 / 0.05~0.15 可用 / >0.15 慎用(与第 4.2 节、《Jev 决策编程》9.2 节同一张表),不因"修复前也不算太差"而放宽。

⚠️ 修复只允许在训练分片上搜索参数,在留出分片上验收——在全部数据上调缩放参数再报全量 ECE,是把校准"调"成过拟合(第 9.3 节反模式在此已有苗头)。

四、接入真实数据

替换点只有一处:

# load_pairs_real.py(写法示意,以官方文档为准) import json def load_pairs_real(path: str, conf_field: str = "conf", hit_field: str = "hit") -> list[tuple[float, int]]: """从跑分记录 JSONL 读 (conf, hit):每行一条回归集样本的判决。""" pairs = [] for line in open(path, encoding="utf-8"): r = json.loads(line) pairs.append((float(r[conf_field]), int(r[hit_field]))) return pairs

数据从哪来:Promptfoo 跑分结果导出(6.1)、Langfuse 裁判评分器写回的 trace 分数(6.2)、或自家回归脚本的落盘日志。每次换 prompt 或模型快照,ECE 重测一遍——回归集现成,成本是几分钟(第 4.2 节纪律在此落地为脚本)。

本节要点回顾

  1. 统一接口:一切置信度来源化为 (conf, hit) 对,三来源(logprobs/裁判/自报)共用一套实验室。
  2. 三步流程:文本可靠性曲线 → ECE 判档(<0.05 / >0.15)→ 线性缩放修复,训练分片调参、留出分片验收。
  3. MOCK 可跑:零依赖跑通全流程;真实接入只换 load_pairs
  4. 节奏:换 prompt / 模型快照即重测,校准没有一劳永逸。

ECE 达标只说明"置信度可信";可信之后下一步是"用它做决策"——低于多少转人工?阈值不该拍脑袋,该算一笔代价账。下一节扫出期望代价曲线。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U