Files
decision-models-benchmark/analysis/calibrate.py
2026-10-07 18:24:10 -03:00

115 lines
4.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Calibração e limiares de abstenção, só com a partição de calibração (tarefas 60–61; Q5, Q10).
Para cada candidato × dispositivo:
- temperature scaling sobre log-probabilidades (um parâmetro T, minimizando NLL);
- o T só é aceito se reduzir o ECE em validação cruzada de 5 dobras por `group_id`; senão T = 1;
- limiar de abstenção sobre max(p) calibrado: a maior cobertura com taxa de rota errada com
confiança alta ≤ 2% (Q5). Abster-se = encaminhar para desambiguação.
Uso: python -m analysis.calibrate reports/calibration → config/calibration.json
"""
import json
import math
import sys
from collections import defaultdict
from pathlib import Path
from analysis.metrics import calibration, selective
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "config" / "calibration.json"
MAX_CONFIDENT_WRONG = 0.02
MIN_COVERAGE = 0.80
GRID = [math.exp(x / 40) for x in range(-160, 121)] # T de ~0,018 a ~20
def group_of(example_id: str) -> str:
return example_id.rsplit("-", 2)[0] # "<group_id>-<nn>-D1"
def apply_temperature(probs: dict, t: float) -> dict:
logits = {k: math.log(max(v, 1e-12)) / t for k, v in probs.items()}
m = max(logits.values())
exp = {k: math.exp(v - m) for k, v in logits.items()}
z = sum(exp.values())
return {k: v / z for k, v in exp.items()}
def calibrated_rows(rows: list[dict], t: float) -> list[dict]:
out = []
for r in rows:
p = apply_temperature(r["probabilities"], t)
out.append({**r, "probabilities": p, "pred": max(p, key=p.get)})
return out
def nll(rows: list[dict], t: float) -> float:
return -sum(math.log(max(apply_temperature(r["probabilities"], t)[r["gold"]], 1e-12)) for r in rows) / len(rows)
def fit_temperature(rows: list[dict]) -> float:
return min(GRID, key=lambda t: nll(rows, t))
def cross_validated_ece(rows: list[dict], folds: int = 5) -> tuple[float, float]:
groups = sorted({group_of(r["id"]) for r in rows})
fold_of = {g: i % folds for i, g in enumerate(groups)}
raw, cal = [], []
for k in range(folds):
train = [r for r in rows if fold_of[group_of(r["id"])] != k]
test = [r for r in rows if fold_of[group_of(r["id"])] == k]
if not test or not train:
continue
t = fit_temperature(train)
raw.append(calibration(test)["ece"] * len(test))
cal.append(calibration(calibrated_rows(test, t))["ece"] * len(test))
n = len(rows)
return sum(raw) / n, sum(cal) / n
def choose_threshold(rows: list[dict]) -> dict:
confidences = sorted({round(max(r["probabilities"].values()), 6) for r in rows})
best = selective(rows, 1.01) # abstém de tudo: cobertura 0
for thr in confidences:
s = selective(rows, thr)
if s["confident_wrong_rate"] <= MAX_CONFIDENT_WRONG:
best = s
break # menor limiar que respeita o risco = maior cobertura
return best
def main():
src = Path(sys.argv[1])
result = {"method": "temperature_scaling", "fit_split": "calibration",
"max_confident_wrong_rate": MAX_CONFIDENT_WRONG, "min_coverage": MIN_COVERAGE, "candidates": {}}
for f in sorted(src.glob("predictions-*.jsonl")):
rows = [json.loads(l) for l in f.open()]
rows = [r for r in rows if r.get("probabilities")]
cand = json.loads((src / f.name.replace("predictions-", "summary-").replace(".jsonl", ".json"))
.read_text())["candidate"]["name"]
device = f.stem.rsplit("-", 1)[1]
t = fit_temperature(rows)
ece_cv_raw, ece_cv_cal = cross_validated_ece(rows)
accepted = ece_cv_cal < ece_cv_raw
final_t = t if accepted else 1.0
cal_rows = calibrated_rows(rows, final_t)
thr = choose_threshold(cal_rows)
result["candidates"].setdefault(cand, {})[device] = {
"temperature": round(final_t, 4), "fitted_temperature": round(t, 4), "accepted": accepted,
"n": len(rows), "ece_raw": round(calibration(rows)["ece"], 4),
"ece_calibrated": round(calibration(cal_rows)["ece"], 4),
"ece_cv_raw": round(ece_cv_raw, 4), "ece_cv_calibrated": round(ece_cv_cal, 4),
"nll_raw": round(nll(rows, 1.0), 4), "nll_calibrated": round(nll(rows, final_t), 4),
"threshold": round(thr["threshold"], 6), "coverage": round(thr["coverage"] or 0, 4),
"selective_accuracy": round(thr["selective_accuracy"] or 0, 4),
"confident_wrong_rate": round(thr["confident_wrong_rate"] or 0, 4),
"meets_min_coverage": (thr["coverage"] or 0) >= MIN_COVERAGE,
}
print(cand, device, json.dumps(result["candidates"][cand][device]))
OUT.write_text(json.dumps(result, indent=2, ensure_ascii=False) + "\n")
print("→", OUT.relative_to(ROOT))
if __name__ == "__main__":
main()