SPB Git

Repository Public Research preview

decode_search.py 6.9 KB 132 lines python

1#!/usr/bin/env python32"""Decoding-side search (no training): for one checkpoint, generate greedy + K nucleus samples per validation pair, then score3candidate-selection rules against the originals — greedy, MBR with chrF++ utility (Eikema & Aziz 2020; Kumar & Byrne 2004),4MBR with the *source* added as a weighted pseudo-reference (identity anchoring), and an oracle (best candidate, upper bound).5Writes artifacts/metrics/<name>/decode_search.json and the selected outputs per rule (outputs_<system>__<rule>.jsonl) so they6can be scored by the regular evaluation runner. Exploratory: tuned on validation only; the chosen rule is then applied once to test."""7from __future__ import annotations89import argparse10import json11import sys12import time13from pathlib import Path1415import numpy as np16import torch1718ROOT = Path(__file__).resolve().parents[1]19sys.path.insert(0, str(ROOT / "src"))20from htr.evaluation.metrics import _chrf, factual  # noqa: E40221from htr.inference.pipeline import HTRReconstructor  # noqa: E40222from htr.parse.protect import protect, restore_placeholders  # noqa: E40223from htr.training.data import meta_prefix  # noqa: E40224from htr.data.taxonomy import era_of  # noqa: E40225from htr.utils.io import read_jsonl, write_json, write_jsonl  # noqa: E40226from htr.utils.provenance import run_header  # noqa: E402272829def chrf(h: str, r: str) -> float:30    return float(_chrf.sentence_score(h, [r]).score)313233def main() -> None:34    ap = argparse.ArgumentParser()35    ap.add_argument("--pairs", required=True)36    ap.add_argument("--splits", required=True)37    ap.add_argument("--split", default="val")38    ap.add_argument("--checkpoint", required=True)39    ap.add_argument("--system", required=True)40    ap.add_argument("--name", required=True, help="metrics dir name")41    ap.add_argument("--limit", type=int, default=1000)42    ap.add_argument("--k", type=int, default=8, help="samples per input")43    ap.add_argument("--temperature", type=float, default=0.7)44    ap.add_argument("--top-p", type=float, default=0.9)45    ap.add_argument("--batch-size", type=int, default=16)46    ap.add_argument("--device", default="auto")47    ap.add_argument("--seed", type=int, default=0)48    ap.add_argument("--out", default=None, help="predictions dir to write the selected outputs (optional)")49    a = ap.parse_args()50    rec = HTRReconstructor.from_pretrained(a.checkpoint, device=a.device)51    model, tok, dev = rec.model, rec.tok, rec.device52    splits = {s["doc_id"]: s["split"] for s in read_jsonl(a.splits)}53    pairs = [p for p in read_jsonl(a.pairs) if splits.get(p["doc_id"]) == a.split][: a.limit]54    print(f"{len(pairs)} pairs ({a.split}), K={a.k}, T={a.temperature}, p={a.top_p}", flush=True)55    torch.manual_seed(a.seed)56    pad = model.cfg.pad_id57    prepared = []58    for i, p in enumerate(pairs):59        m = p["metadata"]60        prot = protect(p["source_text"])61        src_text = (meta_prefix({"metadata": {**m, "era": m.get("era") or era_of(m.get("pub_year"))}}) if rec.text_meta_prefix else "") + prot.text62        prepared.append((i, p, prot, tok.encode(src_text, add_eos=True, max_len=model.cfg.max_len)))63    prepared.sort(key=lambda x: len(x[3]))64    cands: dict[int, list[str]] = {}65    t0 = time.time()66    for b in range(0, len(prepared), a.batch_size):67        chunk = prepared[b : b + a.batch_size]68        L = max(len(x[3]) for x in chunk)69        src = torch.tensor([x[3] + [pad] * (L - len(x[3])) for x in chunk], device=dev)70        meta_t = None71        if model.cfg.arch in ("m2", "m3"):72            parts = [rec._meta_tensors(x[1]["metadata"]) for x in chunk]73            meta_t = {k: torch.cat([q[k] for q in parts]) for k in parts[0]}74        mt = min(model.cfg.max_len - 2, int(L * 1.6) + 16)75        with torch.no_grad():76            g = model.generate(src, meta_t, max_new_tokens=mt, beam=1)77            srcK = src.repeat_interleave(a.k, 0)78            metaK = {k: v.repeat_interleave(a.k, 0) for k, v in meta_t.items()} if meta_t else None79            smp = model.generate(srcK, metaK, max_new_tokens=mt, beam=1, sample=True, temperature=a.temperature, top_p=a.top_p)80        for j, (i, p, prot, _) in enumerate(chunk):81            outs = [restore_placeholders(tok.decode(g[j].tolist()), prot)]82            outs += [restore_placeholders(tok.decode(smp[j * a.k + k].tolist()), prot) for k in range(a.k)]83            cands[i] = outs84        if (b // a.batch_size) % 10 == 0:85            print(f"  {b + len(chunk)}/{len(prepared)} ({time.time() - t0:.0f}s)", flush=True)86    # ---- selection rules ----87    rules = {"greedy": None, "mbr": 0.0, "mbr_src0.5": 0.5, "mbr_src1": 1.0, "mbr_src2": 2.0}88    sel: dict[str, list[str]] = {r: [] for r in rules}89    sel["identity"], sel["oracle"], sel["sample1"] = [], [], []90    per_rule_scores: dict[str, list[float]] = {r: [] for r in list(rules) + ["identity", "oracle", "sample1"]}91    nrec: dict[str, list[float]] = {r: [] for r in per_rule_scores}92    for i, p in enumerate(pairs):93        C = cands[i]94        src_t, tgt = p["source_text"], p["target_text"]95        n = len(C)96        U = np.zeros((n, n))97        for x in range(n):98            for y in range(n):99                if x != y:100                    U[x, y] = chrf(C[x], C[y])101        base = U.sum(1) / (n - 1)102        to_src = np.array([chrf(c, src_t) for c in C])103        vs_tgt = np.array([chrf(c, tgt) for c in C])104        picks = {"greedy": 0, "sample1": 1, "oracle": int(vs_tgt.argmax())}105        for r, w in rules.items():106            if w is not None:107                picks[r] = int((base + w * to_src).argmax())108        for r, k in picks.items():109            sel[r].append(C[k])110            per_rule_scores[r].append(float(vs_tgt[k]))111            nrec[r].append(factual(C[k], src_t)["number_recall"])112        sel["identity"].append(src_t)113        per_rule_scores["identity"].append(chrf(src_t, tgt))114        nrec["identity"].append(1.0)115    rep = {**run_header({"name": a.name, "checkpoint": a.checkpoint, "system": a.system, "split": a.split, "n": len(pairs), "k": a.k, "temperature": a.temperature, "top_p": a.top_p, "seed": a.seed}), "rules": {}}116    ident = np.array(per_rule_scores["identity"])117    for r, v in per_rule_scores.items():118        v = np.array(v)119        d = v - ident120        rep["rules"][r] = {"chrf_mean": float(v.mean()), "delta_vs_identity": float(d.mean()), "delta_se": float(d.std(ddof=1) / np.sqrt(len(d))), "number_recall": float(np.mean(nrec[r]))}121        print(f"{r:12s} chrF++ {v.mean():6.2f}  Δ vs identity {d.mean():+.2f} ± {1.96 * d.std(ddof=1) / np.sqrt(len(d)):.2f}  number recall {np.mean(nrec[r]):.3f}", flush=True)122    out_dir = ROOT / "artifacts/metrics" / a.name123    write_json(out_dir / "decode_search.json", rep)124    if a.out:125        for r in ("mbr", "mbr_src0.5", "mbr_src1", "mbr_src2"):126            write_jsonl(Path(a.out) / f"outputs_{a.system}__{r}.jsonl", [{"pair_id": p["pair_id"], "doc_id": p["doc_id"], "system": f"{a.system}__{r}", "output": sel[r][i]} for i, p in enumerate(pairs)])127    print("wrote", out_dir / "decode_search.json")128129130if __name__ == "__main__":131    main()132
Files main
Menu

Theme