指南
教學:分類器和迴歸器
一份針對三個從命令行評估的預測模型的可執行演練:一個二分類流失分類器,一個逐個類別評估的三分類工單路由器,以及一個在宣告的範圍內按絕對誤差打分的送達時間迴歸器。每個都在本機執行,無需提供者、密鑰或網路,並且每個都以一次針對基準量測的候選變更結束。
本頁是分類器與迴歸器的動手配套頁面,那一頁解釋了每個欄位;參考請讀那一頁,從頭到尾實際做一遍請讀本頁。區間、決策狀態和發布動作等術語在核心概念中有定義。
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.label | true 或 false,即真實值 | 評估器 |
| metadata.plan | 任意 JSON | 僅切片 |
| 返回的 label | true 或 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 523 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_candidateoloproof run --config candidate.yamlGate: 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.yamlComparison 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 runGate: 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 228 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 runGate: 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_000output: {
"days": 6.1
}
judgments:
days_error: score 4.9切片指明瞭該看哪裡:缺貨訂單偏差五天。候選(estimate_candidate)對缺貨商品加五天:
oloproof run --config candidate.yamlGate: 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 部分所示。
- 範例是確定性的合成資料。它們的決策展示的是機制,而不是真實模型在真實資料上的表現。
下一步
- 分類器與迴歸器是逐欄位的參考。
- 將候選與基準進行比較和比較規則介紹比較的工作流程。
- 切片介紹 confidence: 分段和切片支援度。
- CI 閘門列出了結束代碼。