Download src/xdecision/evaluate.py from mccoysc/xDecision: direct link, hf CLI and curl.
- Browser
- Download file 11 kB
-
https://huggingface.co/mccoysc/xDecision/resolve/main/src/xdecision/evaluate.py
- Command line
-
hf download hf://mccoysc/xDecision/src/xdecision/evaluate.py
-
curl -L -o evaluate.py https://huggingface.co/mccoysc/xDecision/resolve/main/src/xdecision/evaluate.py
11 kB
| """Evaluate a checkpoint directory with laya's own PyTorch forward (the code path consumers load), | |
| and fit calibration temperatures. | |
| python -m xdecision.evaluate --ckpt DIR [--calibrate] [--suites typed_decisions,emotion,...] | |
| --calibrate fits one temperature per (question type, option-count bucket) on data/built/calib.pt | |
| by held-out NLL and writes them into DIR/rl_agent_config.json before evaluating. | |
| """ | |
| import argparse | |
| import json | |
| import math | |
| import os | |
| import time | |
| from collections import defaultdict | |
| import numpy as np | |
| import torch | |
| from laya.common import QTYPE_NAMES, clamp_temperature, ece_score, temp_bucket | |
| from .model_io import load_config, load_model, load_tokenizer | |
| from .train import collate | |
| from .devices import pick_device | |
| SUITES = ["typed_decisions", "emotion", "sst5", "prompt_injection", "xcopa", "xwinograd", "belebele", "xstory", | |
| "wd_heldout", "wiki_heldout", "hans_formats", "unseen_workflows", "market_heldout", "uncertainty", "fitness_unseen", | |
| "unseen_workflows_extra"] | |
| def logits_for(model, items, pad_id, device, max_tokens=16384): | |
| order = sorted(range(len(items)), key=lambda i: len(items[i]["ids"])) | |
| out = [None] * len(items) | |
| act_out = [None] * len(items) | |
| i = 0 | |
| while i < len(order): | |
| L = len(items[order[i]]["ids"]) | |
| bs = max(1, min(128, max_tokens // max(64, ((L + 63) // 64) * 64))) | |
| idx = order[i:i + bs] | |
| # the longest item in this slice sets the padded length | |
| while len(idx) > 1 and len(items[idx[-1]]["ids"]) * len(idx) > max_tokens * 1.5: | |
| idx = idx[:-1] | |
| b = collate([items[j] for j in idx], pad_id) | |
| b = {k: v.to(device) for k, v in b.items()} | |
| lg, act = model(b["input_ids"], b["attention_mask"], b["marker_pos"], b["marker_mask"], b["qtype"]) | |
| lg = lg.float().cpu().numpy() | |
| act = torch.softmax(act.float(), -1)[:, 0].cpu().numpy() | |
| for r, j in enumerate(idx): | |
| out[j] = lg[r, : len(items[j]["markers"])] | |
| act_out[j] = float(act[r]) | |
| i += len(idx) | |
| return out, act_out | |
| def temps_from_cfg(cfg): | |
| t = [clamp_temperature(x) for x in cfg.get("temperature", [1.0, 1.0, 1.0])] | |
| tb = {k: clamp_temperature(v) for k, v in (cfg.get("temperature_by_options") or {}).items()} | |
| return t, tb | |
| def probs(z, qtype, t, tb): | |
| T = tb.get(temp_bucket(qtype, len(z)), t[qtype]) | |
| z = np.asarray(z, dtype=np.float64) / T | |
| p = np.exp(z - z.max()) | |
| return p / p.sum() | |
| def fit_temperature(pairs, lo=0.5, hi=5.0): | |
| """1-D NLL minimisation over log T (golden section; the NLL is unimodal in log T).""" | |
| def nll(logT): | |
| T = math.exp(logT) | |
| s = 0.0 | |
| for z, tgt in pairs: | |
| z = np.asarray(z, dtype=np.float64) / T | |
| lse = z.max() + math.log(np.exp(z - z.max()).sum()) | |
| s -= float(np.dot(tgt, z - lse)) | |
| return s / len(pairs) | |
| a, b = math.log(lo), math.log(hi) | |
| g = (math.sqrt(5) - 1) / 2 | |
| c, d = b - g * (b - a), a + g * (b - a) | |
| fc, fd = nll(c), nll(d) | |
| for _ in range(40): | |
| if fc < fd: | |
| b, d, fd = d, c, fc | |
| c = b - g * (b - a) | |
| fc = nll(c) | |
| else: | |
| a, c, fc = c, d, fd | |
| d = a + g * (b - a) | |
| fd = nll(d) | |
| return math.exp((a + b) / 2) | |
| def calibrate(model, tok, device, calib_items, min_n=40): | |
| lg, _ = logits_for(model, calib_items, tok.pad_token_id, device) | |
| by_bucket, by_type = defaultdict(list), defaultdict(list) | |
| for z, it in zip(lg, calib_items): | |
| pair = (z, np.asarray(it["target"], dtype=np.float64)) | |
| by_bucket[temp_bucket(it["qtype"], len(z))].append(pair) | |
| by_type[it["qtype"]].append(pair) | |
| t = [round(fit_temperature(by_type[q]), 4) if len(by_type[q]) >= min_n else 1.0 for q in range(3)] | |
| tb = {k: round(fit_temperature(v), 4) for k, v in sorted(by_bucket.items()) if len(v) >= min_n} | |
| return t, tb, {k: len(v) for k, v in by_bucket.items()} | |
| def auroc(scores, labels): | |
| s, y = np.asarray(scores), np.asarray(labels).astype(bool) | |
| if y.all() or (~y).all(): | |
| return float("nan") | |
| ranks = s.argsort().argsort() + 1 | |
| return float((ranks[y].sum() - y.sum() * (y.sum() + 1) / 2) / (y.sum() * (~y).sum())) | |
| def score_items(items, lg, act, t, tb): | |
| rows = [] | |
| for it, z, a in zip(items, lg, act): | |
| q = it["qtype"] | |
| p = probs(z, q, t, tb) | |
| tgt = np.asarray(it["target"]) | |
| gold = int(it.get("label", int(tgt.argmax()))) | |
| pred = int(p.argmax()) | |
| row = {"src": it["src"], "lang": it.get("lang", "en"), "t": QTYPE_NAMES[q], "correct": pred == gold, | |
| "conf": float(p.max()), "soft": float((p * tgt).sum()), "act": a, "pred": pred, "gold": gold, | |
| "gold_prob": float(p[gold]), | |
| "brier": float(np.sum((p - np.eye(len(p), dtype=np.float64)[gold]) ** 2)), | |
| "determinate": bool(it.get("determinate", bool(tgt.max() >= 0.99) or "label" in it))} | |
| if q == 1: | |
| lv = np.arange(len(p)) | |
| row["mae"] = abs(float((p * lv).sum()) - gold) | |
| row["first"] = pred == 0 | |
| if "workflow" in it: | |
| row["workflow"] = it["workflow"] | |
| rows.append(row) | |
| return rows | |
| def summarize(rows): | |
| def agg(rs): | |
| det = [r for r in rs if r["determinate"]] | |
| if not det: | |
| conf = np.asarray([r["conf"] for r in rs], dtype=np.float64) | |
| act = np.asarray([r["act"] for r in rs], dtype=np.float64) | |
| return {"n": len(rs), "mean_conf": round(float(conf.mean()), 4), | |
| "p95_conf": round(float(np.percentile(conf, 95)), 4), | |
| "high_conf_rate_075": round(float(np.mean(conf >= 0.75)), 4), | |
| "mean_act": round(float(act.mean()), 4), | |
| "p95_act": round(float(np.percentile(act, 95)), 4)} | |
| conf = np.asarray([r["conf"] for r in det], dtype=np.float64) | |
| correct = np.asarray([r["correct"] for r in det], dtype=bool) | |
| accepted = conf >= 0.8 | |
| order = np.argsort(-conf) | |
| cumulative_risk = np.cumsum(~correct[order]) / np.arange(1, len(det) + 1) | |
| d = {"n": len(det), "acc": round(float(np.mean([r["correct"] for r in det])), 4), | |
| "soft_acc": round(float(np.mean([r["soft"] for r in det])), 4), | |
| "ece": round(ece_score(np.array([r["conf"] for r in det]), np.array([r["correct"] for r in det], float)), 4), | |
| "act_auroc": round(auroc([r["act"] for r in det], [r["correct"] for r in det]), 4), | |
| "nll": round(float(np.mean([-math.log(max(r["gold_prob"], 1e-12)) for r in det])), 4), | |
| "brier": round(float(np.mean([r["brier"] for r in det])), 4), | |
| "wrong_high_conf_rate_080": round(float(np.mean((~correct) & accepted)), 4), | |
| "coverage_at_080": round(float(accepted.mean()), 4), | |
| "accuracy_at_080": round(float(correct[accepted].mean()), 4) if accepted.any() else None, | |
| "aurc": round(float(cumulative_risk.mean()), 4)} | |
| sc = [r for r in det if "mae" in r] | |
| if sc: | |
| d["score_mae"] = round(float(np.mean([r["mae"] for r in sc])), 4) | |
| d["first_level_rate"] = round(float(np.mean([r["first"] for r in sc])), 4) | |
| d["first_level_gold_rate"] = round(float(np.mean([r["gold"] == 0 for r in sc])), 4) | |
| return d | |
| out = {"all": agg(rows)} | |
| for key in ("t", "lang", "workflow", "src"): | |
| groups = defaultdict(list) | |
| for r in rows: | |
| if key in r: | |
| groups[r[key]].append(r) | |
| if len(groups) > 1: | |
| out["by_" + key] = {k: agg(v) for k, v in sorted(groups.items())} | |
| und = [r for r in rows if not r["determinate"]] | |
| if und: | |
| conf = np.asarray([r["conf"] for r in und], dtype=np.float64) | |
| act = np.asarray([r["act"] for r in und], dtype=np.float64) | |
| out["undetermined_mean_conf"] = round(float(conf.mean()), 4) | |
| out["undetermined_p95_conf"] = round(float(np.percentile(conf, 95)), 4) | |
| out["undetermined_high_conf_rate_075"] = round(float(np.mean(conf >= 0.75)), 4) | |
| out["undetermined_mean_act"] = round(float(act.mean()), 4) | |
| out["undetermined_p95_act"] = round(float(np.percentile(act, 95)), 4) | |
| return out | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--ckpt", required=True) | |
| ap.add_argument("--data", default="data/built") | |
| ap.add_argument("--suites", default=",".join(SUITES + ["val"])) | |
| ap.add_argument("--calibrate", action="store_true") | |
| ap.add_argument("--calib-file", default="calib.pt") | |
| ap.add_argument("--device", default="auto", help="auto, cuda[:index], mps or cpu; evaluation remains FP32") | |
| ap.add_argument("--out", default=None) | |
| ap.add_argument("--max-per-suite", type=int, default=0) | |
| ap.add_argument("--threads", type=int, default=0) | |
| args = ap.parse_args() | |
| device = pick_device(args.device) | |
| print(f"device={device} precision=fp32", flush=True) | |
| if args.threads: | |
| torch.set_num_threads(args.threads) | |
| tok = load_tokenizer(args.ckpt) | |
| model, cfg = load_model(args.ckpt, device) | |
| model.eval() | |
| t0 = time.time() | |
| shipped = temps_from_cfg(cfg) | |
| if args.calibrate: | |
| calib = torch.load(os.path.join(args.data, args.calib_file), weights_only=False) | |
| t, tb, counts = calibrate(model, tok, device, calib) | |
| cfg["temperature"] = t | |
| cfg["temperature_by_options"] = tb | |
| cfg["calibration"] = {"items": len(calib), "per_bucket": counts, "method": "held-out NLL, T in [0.5, 5]"} | |
| json.dump(cfg, open(os.path.join(args.ckpt, "rl_agent_config.json"), "w"), indent=2, ensure_ascii=False) | |
| print(f"calibrated in {time.time()-t0:.0f}s: temperature={t} by_options={tb}", flush=True) | |
| t, tb = temps_from_cfg(cfg) | |
| report = {"ckpt": os.path.abspath(args.ckpt), "device": str(device), "precision": "fp32", | |
| "temperature": t, "temperature_by_options": tb, "suites": {}, | |
| "suites_as_shipped": {}} | |
| for name in args.suites.split(","): | |
| path = os.path.join(args.data, f"{name}.pt" if name == "val" else f"eval_{name}.pt") | |
| if not os.path.exists(path): | |
| continue | |
| items = torch.load(path, weights_only=False) | |
| if args.max_per_suite: | |
| items = items[: args.max_per_suite] | |
| lg, act = logits_for(model, items, tok.pad_token_id, device) | |
| report["suites"][name] = summarize(score_items(items, lg, act, t, tb)) | |
| if args.calibrate: | |
| report["suites_as_shipped"][name] = summarize(score_items(items, lg, act, *shipped)) | |
| a = report["suites"][name]["all"] | |
| print(f"[{time.time()-t0:5.0f}s] {name:17s} n={a.get('n')} acc={a.get('acc')} ece={a.get('ece')} " | |
| f"act_auroc={a.get('act_auroc')}", flush=True) | |
| out = args.out or os.path.join(args.ckpt, "eval_report.json") | |
| json.dump(report, open(out, "w"), indent=1, ensure_ascii=False) | |
| print("wrote", out) | |
| if __name__ == "__main__": | |
| main() | |