Groot-1 / groot1 /evaluate.py
marcyano's picture
Groot 1 v1.1: Folha correction (87.8% vs Jev 85.8%), S+M ensemble
17a55cc verified
Raw History Blame Contribute Delete
5.05 kB
"""Evaluation of a Groot checkpoint on decision rows and public benchmarks.
groot-eval --model runs/groot-1-s/final --rows data/test.jsonl [--max-rows 2000] [--out eval.json]
groot-eval --model ... --bench typed_decisions # LocalLLaMA/typed-decisions test split
Reports accuracy per type, NLL, Brier, ECE (with and without the stored
temperatures), coverage/accuracy at confidence thresholds, and per-source
breakdowns. Rows are the JSONL format of data.py.
"""
from __future__ import annotations
import argparse
import json
from collections import defaultdict
from pathlib import Path
import numpy as np
import torch
from .calibration import Record, evaluate_records, softmax
from .config import QTYPES
from .data import CONVERTERS, read_jsonl
from .model import GrootDecisionModel
from .schema import confidence_choice, confidence_noul, confidence_score
from .serialize import Serializer
from .train import evaluate
def confidence_of(probabilities: list[float], qtype: str) -> float:
if qtype == "choice":
return confidence_choice(probabilities)
if qtype == "score":
return confidence_score(probabilities)
return confidence_noul(probabilities[1])
def coverage_table(records: list[Record], temps: list[float], thresholds=(0.3, 0.5, 0.7, 0.9)) -> dict:
"""Accuracy of answers whose System One confidence passes each threshold."""
rows = []
for r in records:
p = softmax(r.logits, temps[r.qtype]).tolist()
c = confidence_of(p, QTYPES[r.qtype])
rows.append((c, int(np.argmax(p) == int(r.target.argmax()))))
out = {}
for th in thresholds:
kept = [a for c, a in rows if c >= th]
out[str(th)] = {"coverage": len(kept) / max(1, len(rows)), "accuracy": float(np.mean(kept)) if kept else None}
return out
def per_source(rows: list[dict], records: list[Record], temps: list[float]) -> dict:
groups: dict[str, list[Record]] = defaultdict(list)
for row, rec in zip(rows, records):
groups[row.get("source", "?")].append(rec)
return {src: evaluate_records(recs, temps).to_dict() for src, recs in groups.items()}
def main(argv: list[str] | None = None) -> None:
ap = argparse.ArgumentParser(prog="groot-eval")
ap.add_argument("--model", required=True)
ap.add_argument("--rows", default=None)
ap.add_argument("--bench", default=None, help="name of a converter in data.py (uses its test split)")
ap.add_argument("--max-rows", type=int, default=None)
ap.add_argument("--batch-size", type=int, default=16)
ap.add_argument("--device", default=None)
ap.add_argument("--out", default=None)
ap.add_argument("--set", action="append", default=[], help="config override key=value (e.g. state_retrieval=true)")
ap.add_argument("--calibrate-on", default=None, help="fit per-type temperatures on this JSONL before evaluating")
ap.add_argument("--save-calibration", action="store_true", help="write the fitted temperatures into the checkpoint")
args = ap.parse_args(argv)
from transformers import AutoTokenizer
device = torch.device(args.device or ("cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"))
model = GrootDecisionModel.from_pretrained(args.model, device=device)
tok = AutoTokenizer.from_pretrained(Path(args.model) / "tokenizer")
if args.set:
from .config import GrootConfig, parse_overrides
model.cfg = GrootConfig.from_dict({**model.cfg.to_dict(), **parse_overrides(args.set)})
ser = Serializer(tok, model.cfg)
if args.calibrate_on:
from .calibration import fit_temperatures
cal_rows = list(read_jsonl(args.calibrate_on))
cal_records, _ = evaluate(model, cal_rows, ser, device, args.batch_size, None)
temps = fit_temperatures(cal_records, n_types=len(model.cfg.qtypes))
model.temperature.copy_(torch.tensor(temps, dtype=model.temperature.dtype, device=model.temperature.device))
print(json.dumps({"fitted_temperatures": temps, "on": args.calibrate_on, "rows": len(cal_rows)}), flush=True)
if args.save_calibration:
(Path(args.model) / "calibration.json").write_text(json.dumps({"temperature": temps}, indent=2) + "\n")
if args.rows:
rows = list(read_jsonl(args.rows, args.max_rows))
elif args.bench:
rows = [r for r in CONVERTERS[args.bench](limit=args.max_rows) if r["split"] == "test"]
else:
raise SystemExit("pass --rows or --bench")
records, raw = evaluate(model, rows, ser, device, args.batch_size, None)
temps = model.temperature.tolist()
report = {
"model": args.model,
"rows": len(rows),
"raw": raw,
"calibrated": evaluate_records(records, temps).to_dict(),
"coverage": coverage_table(records, temps),
"per_source": per_source(rows, records, temps),
}
text = json.dumps(report, indent=2)
print(text)
if args.out:
Path(args.out).write_text(text + "\n")
if __name__ == "__main__":
main()