Download decider/calibrate.py from hayriyigit/decider-4b-tr-rag: direct link, hf CLI and curl.
- Browser
- Download file 12 kB
-
https://huggingface.co/hayriyigit/decider-4b-tr-rag/resolve/main/decider/calibrate.py
- Command line
-
hf download hf://hayriyigit/decider-4b-tr-rag/decider/calibrate.py
-
curl -L -o calibrate.py https://huggingface.co/hayriyigit/decider-4b-tr-rag/resolve/main/decider/calibrate.py
12 kB
| """Fit decider_config.json "temperature_by_type" by NLL, one temperature per answer type (decider.temperature). | |
| python -m decider.calibrate records.jsonl [--min-rows 50] | |
| -> {"temperature_by_type": {"choice": 1.48, "noul": 2.22, "score": 1.38}, "temperature": 1.6, "rows": {...}, "nll": {...}} | |
| A record is one answer with its gold, read at temperature 1: | |
| {"type": "choice" | "noul" | "score", "logits": [one value per option], "gold": option index} | |
| {"type": "score", "level_logits": [[no, yes], one pair per level], "gold": level index} Score with isolated levels | |
| "probs" / "level_probs" (probabilities at temperature 1) may stand in for "logits" / "level_logits": log p is the logit up to a | |
| constant, which the softmax ignores. A noul gold is 1 for true, 0 for false. Every record is checked first (check_record): | |
| a malformed one (gold out of range or not an integer, wrong shape, NaN, a row without a finite logit, an isolated Score whose | |
| levels all have P(yes) = 0) raises ValueError naming its position. | |
| An isolated-levels Score answer is fitted through the readout the server uses: every level row is softmax(row / T), and the | |
| answer is P(yes) of each level divided by their sum (decider.systemone.combine_isolated). So the "score" temperature is fitted on | |
| Score answers whichever readout the model serves them with; fit it on records collected with the same isolated_levels setting. | |
| `collect(decider, examples)` produces records from a decider.infer.Decider and labelled /v1/systemone-shaped examples. | |
| "temperature" in the output is one temperature fitted on all records together, for comparison; a type with fewer than | |
| --min-rows records is left out of the map (it then uses "temperature" of the config). | |
| """ | |
| import json, math, sys | |
| import numpy as np | |
| from decider.temperature import TYPES | |
| GRID = np.exp(np.linspace(math.log(0.05), math.log(20.0), 801)) # 0.05 .. 20, about 0.75 % apart | |
| def _log(x): | |
| x = np.asarray(x, dtype=np.float64) | |
| with np.errstate(divide="ignore"): | |
| return np.where(x > 0, np.log(np.clip(x, 1e-300, None)), -np.inf) | |
| def _logits(rec, key): | |
| if key in rec: | |
| return np.asarray(rec[key], dtype=np.float64) | |
| alt = {"logits": "probs", "level_logits": "level_probs"}[key] | |
| return _log(rec[alt]) | |
| def _lse(z, axis=-1): | |
| m = np.max(z, axis=axis, keepdims=True) | |
| m = np.where(np.isfinite(m), m, 0.0) | |
| with np.errstate(divide="ignore"): # an all -inf row gives -inf, handled by the callers | |
| return (m + np.log(np.sum(np.exp(z - m), axis=axis, keepdims=True))).squeeze(axis) | |
| def nll(rec, T): | |
| """Negative log-likelihood of one record's gold at temperature T.""" | |
| return float(_Batch([rec]).nll(T)) | |
| def _isolated(rec): | |
| return "level_logits" in rec or "level_probs" in rec | |
| def check_record(rec, i=None): | |
| """Raise ValueError unless `rec` is a well-formed record (module docstring): an integral gold within the options or levels, | |
| a 1-D row of at least two options or an [levels, 2] array of at least two levels, logits that are not NaN or +inf, | |
| probabilities in [0, 1] with positive mass, and isolated levels only on a Score record.""" | |
| where = f"record {i}" if i is not None else "record" | |
| if not isinstance(rec, dict): | |
| raise ValueError(f"{where}: not a JSON object") | |
| if rec.get("type") not in TYPES: | |
| raise ValueError(f"{where}: type {rec.get('type')!r}, expected one of {', '.join(TYPES)}") | |
| iso = _isolated(rec) | |
| keys = ("level_logits", "level_probs") if iso else ("logits", "probs") | |
| if sum(k in rec for k in keys) != 1 or (iso and ("logits" in rec or "probs" in rec)): | |
| raise ValueError(f"{where}: give exactly one of logits, probs, level_logits, level_probs") | |
| if iso and rec["type"] != "score": | |
| raise ValueError(f"{where}: isolated levels (level_logits / level_probs) belong to a score record, not {rec['type']!r}") | |
| key = keys[0] if keys[0] in rec else keys[1] | |
| try: | |
| a = np.asarray(rec[key], dtype=np.float64) | |
| except (TypeError, ValueError): | |
| raise ValueError(f"{where}: {key} is not a numeric array") from None | |
| if iso and (a.ndim != 2 or a.shape[1] != 2 or a.shape[0] < 2): | |
| raise ValueError(f"{where}: {key} must be one [no, yes] pair per level, at least two levels; got shape {a.shape}") | |
| if not iso and (a.ndim != 1 or a.shape[0] < 2): | |
| raise ValueError(f"{where}: {key} must be one value per option, at least two options; got shape {a.shape}") | |
| if key.endswith("probs"): | |
| if not np.all(np.isfinite(a)) or np.any(a < 0) or np.any(a > 1) or np.any(a.sum(-1) <= 0): | |
| raise ValueError(f"{where}: {key} must be probabilities in [0, 1] with positive mass per row") | |
| elif np.any(np.isnan(a)) or np.any(a == np.inf) or not np.all(np.any(np.isfinite(a), axis=-1)): | |
| raise ValueError(f"{where}: {key} contains NaN or +inf, or a row without a finite value") | |
| if rec["type"] == "noul" and a.shape[0] != 2: | |
| raise ValueError(f"{where}: a noul record has exactly two options (false, true); got {a.shape[0]}") | |
| if iso and not np.any(a[:, 1] > 0 if key == "level_probs" else np.isfinite(a[:, 1])): | |
| raise ValueError(f"{where}: no level has a yes probability above 0, so the served Score answer has no distribution") | |
| g = rec.get("gold") | |
| if isinstance(g, bool) or not isinstance(g, (int, np.integer)) or not 0 <= g < a.shape[0]: | |
| raise ValueError(f"{where}: gold must be an integer index in 0..{a.shape[0] - 1}, got {g!r}") | |
| return rec | |
| class _Batch: | |
| """Records packed into padded arrays, so one temperature is scored over all of them at once.""" | |
| def __init__(self, records): | |
| records = [check_record(r, i) for i, r in enumerate(records)] | |
| lists = [r for r in records if not _isolated(r)] | |
| isos = [r for r in records if _isolated(r)] | |
| self.n = len(lists) + len(isos) | |
| self.L = self.Lg = self.I = self.Im = self.Ig = None | |
| if lists: | |
| zs = [_logits(r, "logits") for r in lists] | |
| K = max(len(z) for z in zs) | |
| self.L = np.full((len(zs), K), -np.inf) | |
| for i, z in enumerate(zs): | |
| self.L[i, :len(z)] = z | |
| self.Lg = np.array([int(r["gold"]) for r in lists]) | |
| if isos: | |
| zs = [_logits(r, "level_logits").reshape(-1, 2) for r in isos] | |
| n = max(len(z) for z in zs) | |
| self.I = np.zeros((len(zs), n, 2)); self.Im = np.zeros((len(zs), n), dtype=bool) | |
| for i, z in enumerate(zs): | |
| self.I[i, :len(z)] = z; self.Im[i, :len(z)] = True | |
| self.Ig = np.array([int(r["gold"]) for r in isos]) | |
| def nll(self, T): | |
| """Sum of the records' NLL at temperature T.""" | |
| tot = 0.0 | |
| if self.L is not None: | |
| z = self.L / T | |
| zg = z[np.arange(len(z)), self.Lg] | |
| tot += float(np.sum(np.where(np.isfinite(zg), _lse(z) - zg, 690.0))) | |
| if self.I is not None: # in log space: P(yes) of very confident rows underflows otherwise | |
| z = self.I / T | |
| with np.errstate(invalid="ignore"): | |
| lp = np.where(self.Im, z[..., 1] - _lse(z), -np.inf) # log P(yes) per level, -inf on padding | |
| norm = _lse(lp) # log of the summed P(yes) | |
| lg = lp[np.arange(len(z)), self.Ig] | |
| with np.errstate(invalid="ignore"): | |
| per = np.where(np.isfinite(lg), norm - lg, 690.0) # check_record guarantees a finite norm | |
| tot += float(np.sum(per)) | |
| return tot | |
| def mean_nll(records, T): | |
| b = records if isinstance(records, _Batch) else _Batch(records) | |
| return b.nll(T) / b.n | |
| def fit(records, grid=GRID): | |
| """The temperature with the lowest mean NLL on the grid, refined by a golden-section search between its neighbours.""" | |
| records = records if isinstance(records, _Batch) else _Batch(list(records)) | |
| if not records.n: | |
| raise ValueError("no records to fit") | |
| vals = [mean_nll(records, T) for T in grid] | |
| i = int(np.argmin(vals)) | |
| lo, hi = math.log(grid[max(i - 1, 0)]), math.log(grid[min(i + 1, len(grid) - 1)]) | |
| r = (math.sqrt(5) - 1) / 2 | |
| a, b = hi - r * (hi - lo), lo + r * (hi - lo) | |
| fa, fb = mean_nll(records, math.exp(a)), mean_nll(records, math.exp(b)) | |
| for _ in range(40): | |
| if fa < fb: | |
| hi, b, fb = b, a, fa; a = hi - r * (hi - lo); fa = mean_nll(records, math.exp(a)) | |
| else: | |
| lo, a, fa = a, b, fb; b = lo + r * (hi - lo); fb = mean_nll(records, math.exp(b)) | |
| T = math.exp((lo + hi) / 2) | |
| return T if mean_nll(records, T) <= vals[i] else float(grid[i]) | |
| def fit_by_type(records, min_rows=50): | |
| """-> {"temperature_by_type": {type: T}, "temperature": pooled T, "rows": {type: n}, "nll": {type: {"T=1": .., "fitted": ..}}}""" | |
| records = [check_record(r, i) for i, r in enumerate(records)] | |
| groups = {t: [] for t in TYPES} | |
| for r in records: | |
| groups[r["type"]].append(r) | |
| out = {"temperature_by_type": {}, "temperature": round(fit(records), 3) if records else None, | |
| "rows": {t: len(g) for t, g in groups.items()}, "nll": {}} | |
| for t, g in groups.items(): | |
| if len(g) < max(1, min_rows): | |
| continue | |
| b = _Batch(g); T = fit(b) | |
| out["temperature_by_type"][t] = round(T, 3) | |
| out["nll"][t] = {"T=1": round(mean_nll(b, 1.0), 4), "fitted": round(mean_nll(b, T), 4)} | |
| return out | |
| def collect(decider, examples, independent=True): | |
| """Records for fit_by_type from a decider.infer.Decider, read at temperature 1 on the uncached state-first path. | |
| examples: iterable of (state, questions, golds) with questions as /v1/systemone takes them and golds {question id: gold}, | |
| the gold being a Choice option name, a Noul true/false, or a Score level index. Questions without a gold are skipped. | |
| Score questions are read with the Decider's isolated_levels setting, as system_one serves them.""" | |
| recs = [] | |
| for state, questions, golds in examples: | |
| rqs, index, items = decider._system_one_items(state, questions, independent, layout="state_first") | |
| rows = decider._system_one_probs(items, "state_first", temperature=1.0) | |
| for k, kind, s, n in index: | |
| if k not in golds: | |
| continue | |
| rq, g = rqs[k], golds[k] | |
| if rq["type"] == "noul" and not (isinstance(g, bool) or g in (0, 1)): | |
| raise ValueError(f"gold of noul question {k!r} must be true or false, got {g!r}") | |
| if rq["type"] == "score" and (isinstance(g, bool) or not isinstance(g, int) or not 0 <= g < len(rq["names"])): | |
| raise ValueError(f"gold of score question {k!r} must be a level index in 0..{len(rq['names']) - 1}, got {g!r}") | |
| key = bool(g) if rq["type"] == "noul" else g | |
| if key not in rq["names"]: | |
| raise ValueError(f"gold of choice question {k!r} is not one of its options: {g!r}") | |
| gold = rq["names"].index(key) | |
| if kind == "iso": | |
| recs.append({"type": rq["type"], "level_probs": [rows[s + j][:2] for j in range(n)], "gold": gold}) | |
| else: | |
| recs.append({"type": rq["type"], "probs": rows[s][:len(rq["options"])], "gold": gold}) | |
| return recs | |
| def main(argv=None): | |
| import argparse | |
| ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) | |
| ap.add_argument("records", help="JSON lines, one record per line (see the module docstring)") | |
| ap.add_argument("--min-rows", type=int, default=50, help="fit a type only when it has at least this many records") | |
| a = ap.parse_args(argv) | |
| with open(a.records) as f: | |
| recs = [json.loads(line) for line in f if line.strip()] | |
| print(json.dumps(fit_by_type(recs, a.min_rows), indent=1)) | |
| if __name__ == "__main__": | |
| main(sys.argv[1:]) | |