跳到主要内容

指南

教程:分类器和回归器

一份针对三个从命令行评估的预测模型的可运行演练:一个二分类流失分类器,一个逐个类别评估的三分类工单路由器,以及一个在声明的范围内按绝对误差打分的送达时间回归器。每个都在本地运行,无需提供方、密钥或网络,并且每个都以一次针对基线度量的候选变更结束。

本页是分类器与回归器的动手配套页面,那一页解释了每个字段;参考请读那一页,从头到尾实际做一遍请读本页。区间、决策状态和发布动作等术语在核心概念中有定义。

Oloproof 为预测模型度量什么、不度量什么

模型你能得到什么你得不到什么
二分类器准确率、召回率、精确率、Brier 分数、对数损失、ROC-AUC 和平均精确率,外加混淆计数、校准表和阈值扫描推荐的阈值
多分类器每个类别一个召回率和一个精确率,每个都是以该类别为正类的二分类指标(一对其余)宏平均或微平均指标
回归器平均绝对误差,以你声明的 target_range 为界平方误差、R 平方,或任何没有声明范围的误差

模型本身仍然归你所有。Oloproof 调用你指向的一个 Python 函数,读取它返回的预测,从不查看特征、权重或内部结构。

前提条件

  • Python 3.11 或更高版本,并在虚拟环境中执行 pip install oloproof,与快速入门中一样。
  • 示例文件,它们随软件包一起提供。把它们复制到一个新目录,让各次运行的存储落在那里:
oloproof init --example predictive ~/oloproof-predictive
cd ~/oloproof-predictive

下面的每条命令都在它的三个子目录之一中运行。每次运行都把证据写入它所读取的 oloproof.yaml 旁边的 .oloproof/ 目录。

predictive/
  binary/        app.py  oloproof.yaml  candidate.yaml  release.yaml  comparison.yaml  data/accounts.jsonl
  multiclass/    app.py  oloproof.yaml  candidate.yaml  release.yaml  data/tickets.jsonl
  regression/    app.py  oloproof.yaml  candidate.yaml  release.yaml  data/orders.jsonl

第 1 部分:二分类器

适配器

binary/app.py 在一个文件中同时包含模型和适配器。模型与 churn_model 示例(oloproof init --example churn_model)中的确定性流失模型相同;适配器是 Oloproof 调用的那个带装饰器的函数:

from oloproof import system


@system(name="churn-model", version="baseline")
def run(account):
    score = churn_score(account)
    return {"label": score >= 0.5, "score": round(score, 4)}


@system(name="churn-model", version="candidate-cutoff-0.4")
def run_candidate(account):
    score = churn_score(account)
    return {"label": score >= 0.4, "score": round(score, 4)}

要评估你自己的模型,保持这个结构,把 churn_score 替换为对你模型的调用,例如对一个在导入时加载一次的 scikit-learn 模型使用 model.predict_proba([features(account)])[0][1]。每当模型或它的截断点发生变化时,修改 version:版本是缓存键的一部分,所以在同一版本下重新训练的模型会复用旧的预测。

输入和输出的结构

data/accounts.jsonl 的一行就是一个用例:

{"expected": {"label": false}, "id": "account_000", "input": {"recent_upgrade": true, "support_contacts": 0, "tenure_months": 0}, "metadata": {"plan": "enterprise"}}
部分结构由谁读取
input你的函数接收到的对象你的适配器
expected.labeltrue 或 false,即真实值评估器
metadata.plan任意 JSON仅切片
返回的 labeltrue 或 false,即预测评估器
返回的 score[0, 1] 中的一个数,即正类的概率Brier、对数损失、排序、校准、扫描

评估器,以及为什么选这些

binary/oloproof.yaml:

version: 1
project: churn-tutorial
dataset: data/accounts.jsonl
system:
  name: churn-model
  version: baseline
  callable: app:run
evaluators:
  - {type: predictive_correct, criterion: accuracy}
  - {type: predictive_recall, criterion: recall}
  - {type: predictive_precision, criterion: precision}
  - {type: predictive_brier, criterion: brier}
  - {type: predictive_log_loss, criterion: log_loss, clip: 0.02}
  - {type: predictive_ranking, criterion: rank}
metrics:
  - {id: roc_auc, type: ranking, criterion: rank, statistic: roc_auc}
  - {id: pr_auc, type: ranking, criterion: rank, statistic: average_precision}
predictive:
  label_field: label
  score_field: score
  expected_field: label
  positive: true
  calibration_bins: 10
  thresholds: [0.3, 0.4, 0.5, 0.6, 0.7]
slices: [metadata.plan, "confidence:0.5"]
min_slice_support: 20
  • 准确率、召回率和精确率是基于三组不同的行计算的三个比率:所有账户、已流失的账户,以及模型标记的账户。流失模型三者都需要,因为大约三分之一的账户会流失,所以一个预测没有人流失的模型准确率有 68%,却一个都找不到。
  • Brier 和对数损失为标签背后的概率打分。对数损失需要 clip,否则一次自信的错误就会是无穷大。
  • predictive_ranking 加上两个 metrics: 条目给出 ROC-AUC 和平均精确率。它们回答的是模型对账户排序的好坏,与截断点放在哪里无关。
  • predictive: 块说明标签、分数和真实值在哪里,并在指标旁边生成混淆计数、校准表和阈值扫描。

策略

binary/release.yaml 为每个比率设定下限:

version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
rules:
  - {id: accuracy-floor, metric: accuracy, min: 0.75}
  - {id: recall-floor, metric: recall, min: 0.60}
  - {id: precision-floor, metric: precision, min: 0.60}

当区间下界越过下限时规则通过,当上界低于下限时规则失败,否则显示 INSUFFICIENT_EVIDENCE。

运行

cd binary
oloproof run

有删节:

Run run_... [DECIDED/COMPLETE]
Gate: ALLOW (exit 0)
│ accuracy-floor  │ accuracy  │ PASS  │ lower_bound_meets_minimum │
│ recall-floor    │ recall    │ PASS  │ lower_bound_meets_minimum │
│ precision-floor │ precision │ PASS  │ lower_bound_meets_minimum │

│ accuracy  │ 88.5%    │ [83.2%, 92.6%]  │ 177 / 200 observed · 0 missing · 0 excluded                                │
│ recall    │ 81.2%    │ [69.5%, 90.0%]  │ 52 / 64 observed · 0 missing · 136 excluded                                │
│ precision │ 82.5%    │ [70.9%, 91.0%]  │ 52 / 63 observed · 0 missing · 137 excluded                                │
│ brier     │ 0.120    │ [0.094, 0.154]  │ mean of 200 observed · 0 missing · 0 excluded                              │
│ log_loss  │ 0.389    │ [0.323, 0.499]  │ mean of 200 observed · 0 missing · 0 excluded                              │
│ roc_auc   │ 92.3%    │ [69.3%, 100.0%] │ roc_auc over 64 positive · 136 negative · 0 missing · 0 excluded           │
│ pr_auc    │ 86.5%    │                 │ average_precision over 64 positive · 136 negative · 0 missing · 0 excluded │

如何阅读:

  • Gate: ALLOW (exit 0) 表示没有规则达到策略要阻止的状态。它并不是在断言模型在你写下的三个下限之外也是好的。
  • excluded 计数就是分母在起作用:召回率是基于已流失的 64 个账户度量的,所以没有流失的 136 个被排除在外,而不是被计为失败。
  • pr_auc 没有区间。在 200 行时,引擎不会给出它无法支撑的界限;针对它的规则会显示 INSUFFICIENT_EVIDENCE,原因为 interval_unavailable。

在指标下方,同一份输出打印混淆计数、校准表和阈值扫描:

│ actually positive │ 52                 │ 12                 │
│ actually negative │ 11                 │ 125                │

│ 0.2-0.3 │ 26.7%   │ 0.0%     │ 34 rows │
│ 0.5-0.6 │ 53.4%   │ 81.0%    │ 21 rows │

│ 0.3     │ 57.4%     │ 96.9%  │ 62/108 predicted positive · 62/64 actual positive │
│ 0.4     │ 62.6%     │ 89.1%  │ 57/91 predicted positive · 57/64 actual positive  │
│ 0.5     │ 82.5%     │ 81.2%  │ 52/63 predicted positive · 52/64 actual positive  │
│ 0.6     │ 83.3%     │ 54.7%  │ 35/42 predicted positive · 35/64 actual positive  │
│ 0.7     │ 100.0%    │ 37.5%  │ 24/24 predicted positive · 24/64 actual positive  │

计数不是比率,任何规则都不能指定它们。校准行表明模型的概率有偏差:在 0.2 到 0.3 区间,它声称大约每四个中有一个会流失,而 34 个中没有一个流失。扫描的标题是 Thresholds (exploratory; recommends nothing):它展示每个截断点本会度量出什么,把选择留给你,因为只有你知道漏掉一个流失客户与白打一个挽留电话相比代价如何。

查看失败项

oloproof inspect RUN_ID --failures --limit 5
23 of 200 cases failed, errored or did not finish

account_032
  output: {"label": false, "score": 0.07}
  accuracy: failed
  recall: failed

account_037
  output: {"label": false, "score": 0.37}
  accuracy: failed
  recall: failed
...

RUN_ID 是运行输出第一行中的 id。oloproof inspect RUN_ID --case account_037 显示一个用例的输入、预期值、输出和每个评判结果。有几个被漏掉的流失客户刚好在 0.5 截断点之下(0.37、0.43),这正是扫描中 0.4 那一行已经暗示的。

有意义的下一步来自你看到的东西,而不是来自门禁:这里的遗漏集中在截断点之下,所以候选尝试一个更低的截断点。如果它们是自信的遗漏(0.07),下一步就应该是模型的特征,任何截断点都帮不上忙。

一次候选变更,以及比较

binary/candidate.yaml 是改了两行的 oloproof.yaml:

system:
  name: churn-model
  version: candidate-cutoff-0.4
  callable: app:run_candidate
oloproof run --config candidate.yaml
Gate: BLOCK (exit 3)
│ accuracy-floor  │ accuracy  │ INSUFFICIENT_EVIDENCE │ interval_overlaps_threshold │
│ recall-floor    │ recall    │ PASS                  │ lower_bound_meets_minimum   │
│ precision-floor │ precision │ INSUFFICIENT_EVIDENCE │ interval_overlaps_threshold │

│ accuracy  │ 79.5%    │ [73.2%, 84.9%]  │ 159 / 200 observed · 0 missing · 0 excluded                                │
│ recall    │ 89.1%    │ [78.7%, 95.5%]  │ 57 / 64 observed · 0 missing · 136 excluded                                │
│ precision │ 62.6%    │ [51.8%, 72.6%]  │ 57 / 91 observed · 0 missing · 109 excluded                                │

与扫描中 0.4 那一行完全一致:召回率上升,精确率下降。退出码 3 是 INSUFFICIENT_EVIDENCE,而不是 FAIL:下限落在区间之内,所以 200 个账户无法说明候选在哪一侧。分数没有变化,所以 Brier、对数损失和 ROC-AUC 都相同。

现在逐个用例比较这两次运行。binary/comparison.yaml 包含比较规则:

version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
rules:
  - {id: recall-no-worse, kind: non_inferiority, metric: recall, margin: 0.05}
  - {id: accuracy-no-worse, kind: non_inferiority, metric: accuracy, margin: 0.05}
oloproof compare CANDIDATE_RUN_ID BASELINE_RUN_ID --policy comparison.yaml
Comparison sha256:... of run_... against run_... · 200 paired cases
accuracy: -9.0 points [-17.9, -1.4] · 200 paired · 0 missing · 0 excluded
recall: +7.8 points [-2.8, +21.9] · 64 paired · 0 missing · 136 excluded
  excluded 136: not_a_positive_case
precision: +0.0 points [-8.3, +8.3] · 63 paired · 0 missing · 137 excluded
  excluded 137: not_predicted_positive
...
Decisions
  recall-no-worse  recall  non-inferiority, margin 5.0 points  PASS  lower_bound_above_margin
  accuracy-no-worse  accuracy  non-inferiority, margin 5.0 points  INSUFFICIENT_EVIDENCE  interval_overlaps_margin
    no sample size would make this PASS: the difference itself (-9.0 points) is outside the margin, so more cases would move it toward FAIL
Gate: BLOCK (exit 3)

仔细读精确率那一行。候选自身的精确率从 82.5% 降到了 62.6%,但配对差异在 63 对上是 +0.0。比较是按行配对的:精确率差异只在两个模型都标记了的账户上度量,而在这 63 个账户上两者都是对的。候选额外标记的 28 个账户(其中 23 个是误报)不在这个集合中。这就是为什么对于截断点变更,comparison.yaml 守护的是准确率而不是精确率;规则类型见比较规则。

决策才是有用的部分:召回率被证明不比基线差,而准确率无法在五个百分点内得到证明;建议行说明更多数据会把它推向 FAIL。用九个百分点的准确率换八个百分点的召回率是否值得,是一个业务决策,而门禁让它变得可见了。

第 2 部分:多分类器,逐个类别

multiclass/app.py 把客服工单路由到三个队列之一,并且有一个故意设置的缺陷:任何从移动应用发送的工单都会进入 technical。

@system(name="ticket-router", version="baseline")
def route(ticket):
    if ticket["channel"] == "app":
        return {"queue": "technical"}
    return {"queue": classify(str(ticket["subject"]))}

一个用例:

{"expected": {"queue": "billing"}, "id": "ticket_000", "input": {"channel": "email", "subject": "update the card on file"}, "metadata": {"channel": "email"}}

没有可以开启的多分类指标。每个类别都有自己的二分类块:billing 的召回率就是以 billing 为正类的二分类召回率。multiclass/oloproof.yaml:

evaluators:
  - {type: predictive_correct, criterion: accuracy, field: queue, expected_field: queue}
  - {type: predictive_recall, criterion: recall_billing, field: queue, expected_field: queue, positive: billing}
  - {type: predictive_precision, criterion: precision_billing, field: queue, expected_field: queue, positive: billing}
  - {type: predictive_recall, criterion: recall_technical, field: queue, expected_field: queue, positive: technical}
  - {type: predictive_precision, criterion: precision_technical, field: queue, expected_field: queue, positive: technical}
  - {type: predictive_recall, criterion: recall_account, field: queue, expected_field: queue, positive: account}
  - {type: predictive_precision, criterion: precision_account, field: queue, expected_field: queue, positive: account}
slices: [metadata.channel]

Oloproof 不会在这些之上计算宏平均或微平均。如果你需要,它是你自己从按类别的计数中推导出来的一个数,任何规则都不能依据它做门禁。策略为重要的类别设定下限,因为一个路由器可能整体准确,却丢掉了一个队列:

rules:
  - {id: billing-recall-floor, metric: recall_billing, min: 0.80}
  - {id: account-recall-floor, metric: recall_account, min: 0.80}
  - {id: technical-precision-floor, metric: precision_technical, min: 0.80}
cd ../multiclass
oloproof run
Gate: BLOCK (exit 1)
│ billing-recall-floor      │ recall_billing      │ FAIL                  │ upper_bound_below_minimum   │
│ account-recall-floor      │ recall_account      │ INSUFFICIENT_EVIDENCE │ interval_overlaps_threshold │
│ technical-precision-floor │ precision_technical │ FAIL                  │ upper_bound_below_minimum   │

│ accuracy            │ 81.3%    │ [74.1%, 87.3%]  │ 122 / 150 observed · 0 missing · 0 excluded │
│ recall_billing      │ 66.0%    │ [51.2%, 78.8%]  │ 33 / 50 observed · 0 missing · 100 excluded │
│ precision_billing   │ 100.0%   │ [89.4%, 100.0%] │ 33 / 33 observed · 0 missing · 117 excluded │
│ recall_technical    │ 100.0%   │ [92.8%, 100.0%] │ 50 / 50 observed · 0 missing · 100 excluded │
│ precision_technical │ 64.1%    │ [52.4%, 74.7%]  │ 50 / 78 observed · 0 missing · 72 excluded  │
│ recall_account      │ 78.0%    │ [64.0%, 88.5%]  │ 39 / 50 observed · 0 missing · 100 excluded │
│ precision_account   │ 100.0%   │ [90.9%, 100.0%] │ 39 / 39 observed · 0 missing · 111 excluded │

退出码 1 表示至少有一条规则是 FAIL。每个类别都有自己的分母:有 78 个工单被判为 technical,所以这就是 technical 精确率的分母。切片表指向了原因;切片是探索性的,从不用于门禁,但被标记的切片是值得一读的线索:

│ metadata.channel=app   │ accuracy            │ 42.9%    │ [28.8%, 57.8%]  │ 21 / 49 observed · ... │ marked (p 0.0001)    │
oloproof inspect RUN_ID --failures --limit 2
28 of 150 cases failed, errored or did not finish

ticket_011
  output: {"queue": "technical"}
  accuracy: failed
  precision_technical: failed
  recall_account: failed
...

候选(route_candidate,用 oloproof run --config candidate.yaml 运行)去掉了按渠道的捷径。在这份合成数据上,它把每个工单都路由正确,门禁放行:

Gate: ALLOW (exit 0)
│ billing-recall-floor      │ recall_billing      │ PASS  │ lower_bound_meets_minimum │
│ account-recall-floor      │ recall_account      │ PASS  │ lower_bound_meets_minimum │
│ technical-precision-floor │ precision_technical │ PASS  │ lower_bound_meets_minimum │

oloproof compare 在这里的用法与第 1 部分完全相同,每个类别指标一个差异。

第 3 部分:按绝对误差打分的回归器

regression/app.py 估计送达天数,却忽略了商品是否有货:

@system(name="delivery-estimator", version="baseline")
def estimate(order):
    return {"days": round(base_days(order), 1)}

一个用例,真实值是一个数字:

{"expected": {"days": 11}, "id": "order_000", "input": {"distance_km": 468, "express": false, "in_stock": false}, "metadata": {"in_stock": false}}

唯一的回归评估器是绝对误差,它需要知道每个目标值所在的范围。绝对误差是一个有界均值,它的区间只在该范围内成立,所以范围必须声明,绝不会有默认值。这里的送达需要 0 到 20 天:

evaluators:
  - type: predictive_absolute_error
    criterion: days_error
    field: days
    expected_field: days
    target_range: [0, 20]
slices: [metadata.in_stock]

策略是一个预算,即一条 max: 规则:当区间上界不高于它时通过。

rules:
  - {id: error-budget, metric: days_error, max: 2.0}
cd ../regression
oloproof run
Gate: BLOCK (exit 1)
│ error-budget │ days_error │ FAIL  │ lower_bound_above_maximum │

│ days_error │ 2.66     │ [2.01, 3.57] │ mean of 120 observed · 0 missing · 0 excluded │

│ metadata.in_stock=false │ days_error │ 5.18     │ [4.60, 6.65] │ mean of 53 observed · ... │
│ metadata.in_stock=true  │ days_error │ 0.67     │ [0.51, 2.19] │ mean of 67 observed · ... │

结果是 FAIL,因为就连区间下界都高于两天的预算。分数型评估器对每个用例没有通过或失败之分,所以 oloproof inspect RUN_ID --failures 什么都不列出;改为读一个用例:

oloproof inspect RUN_ID --case order_000
output: {
  "days": 6.1
}
judgments:
  days_error: score 4.9

切片指明了该看哪里:缺货订单偏差五天。候选(estimate_candidate)对缺货商品加五天:

oloproof run --config candidate.yaml
Gate: ALLOW (exit 0)
│ error-budget │ days_error │ PASS  │ upper_bound_meets_maximum │
│ days_error │ 0.70     │ [0.56, 1.57] │ mean of 120 observed · 0 missing · 0 excluded │

故障排除

症状原因与修复
Configuration error: evaluator 'recall' counts 'churned' as the positive class, and no case's 'label' is 'churned'positive: 指定了一个没有任何用例具有的值。完全按照 expected 下出现的样子使用该值,包括 true 与 "true" 的区别。
release rule ... refers to unknown metric 'false_positives'混淆计数不是指标。依据召回率或精确率做门禁。
回归评估器在运行之前就被拒绝target_range 缺失或为空。声明目标值实际可能取的范围;范围越宽,区间越宽。
排序规则显示 INSUFFICIENT_EVIDENCE (interval_unavailable)测试套件太小,无法给出该统计量的区间,通常是 pr_auc。依据 roc_auc 做门禁,或增加用例。
候选报告的是基线的数字两次运行共用一个 version,所以复用了缓存的预测。为每次变更设定自己的版本。
某个用例是 missing 而不是错误你的函数抛出了异常,或评估器读取的字段不存在或不是数字。oloproof inspect RUN_ID --case ID 会显示错误。

局限

  • 没有推荐的阈值。扫描报告的是每个声明的截断点本会度量出什么。
  • 多分类没有宏平均或微平均指标,除了每个标签一个块之外也不支持多标签。
  • 回归只有在声明的 target_range 内的绝对误差:没有平方误差、R 平方或无界误差。
  • 校准以表格形式展示,不用于门禁,也不被校正。
  • 配对精确率差异只覆盖两个模型都标记了的行,如第 1 部分所示。
  • 示例是确定性的合成数据。它们的决策展示的是机制,而不是真实模型在真实数据上的表现。

下一步