d1-2B / calibration.py
paulpak58's picture mlabonne's picture Aurelien-Lac's picture
d1-2B
97c5704
Raw History Blame Contribute Delete
2.87 kB
"""Temperature scaling of option scores, and tables for the typed answers' probabilities.
- `calibration.json` holds a global temperature and optional per-type ones; scaling keeps an argmax but can move a
score's median or expected level.
- A typed probability goes through its table, a monotone map fitted on held-out rows: `found:text`,
`found:number`, `found:date`, `found:box`, `box` (a box's confidence), `found:boxes` (P(count >= k)), `boxes`
(each listed box's confidence), `multiple` (each option's P(yes)), `estimate` (a quantile level, read backwards by
`Calibration.level`).
"""
from __future__ import annotations
import json
from bisect import bisect_left
from dataclasses import dataclass, field
from pathlib import Path
from .prompt import Noul, Question, Score, cardinality
def bucket(kind: str, k: int) -> str:
if kind == "noul":
return "noul"
if kind == "score":
return f"score:{k}"
if k <= 3:
return "choice:small"
if k <= 8:
return "choice:mid"
if k <= 26:
return "choice:large"
return "choice:inventory"
def question_bucket(q: Question) -> str:
kind = "noul" if isinstance(q, Noul) else "score" if isinstance(q, Score) else "choice"
return bucket(kind, cardinality(q))
def through(value: float, knots: list[list[float]] | None) -> float:
"""`value` through increasing (raw, calibrated) knots, linear between them, flat past the ends."""
if not knots:
return value
index = bisect_left([raw for raw, _ in knots], value)
if index == 0:
return knots[0][1]
if index == len(knots):
return knots[-1][1]
(a, pa), (b, pb) = knots[index - 1], knots[index]
return pa + (value - a) * (pb - pa) / (b - a)
@dataclass
class Calibration:
temperatures: dict[str, float] = field(default_factory=dict)
default: float = 1.0
counts: dict[str, int] = field(default_factory=dict)
tables: dict[str, list[list[float]]] = field(default_factory=dict)
def temperature(self, q: Question) -> float:
return self.temperatures.get(question_bucket(q), self.default)
def apply(self, q: Question, scores: list[float]) -> list[float]:
t = self.temperature(q)
return scores if t == 1.0 else [s / t for s in scores]
def through(self, name: str, value: float) -> float:
return through(value, self.tables.get(name))
def level(self, name: str, share: float) -> float:
"""The level whose observed share is `share`: the table read backwards (both columns increase)."""
return through(share, [[y, x] for x, y in self.tables.get(name) or ()])
@classmethod
def load(cls, path: Path | str) -> "Calibration":
d = json.loads(Path(path).read_text())
return cls(d["temperatures"], d.get("default", 1.0), d.get("counts", {}), d.get("tables", {}))