rudeparis commited on
Commit
a5e240c
·
verified ·
1 Parent(s): 6dcffbd

Upload score_baseline.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. score_baseline.py +164 -0
score_baseline.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Offline scorer for eval_baseline.py dumps.
2
+
3
+ Reads a dump JSON (from eval_baseline.py) and computes metrics under
4
+ multiple slot-matching strategies without re-running inference.
5
+
6
+ Strategies:
7
+ exact: pred dict == gold dict (case-sensitive)
8
+ ci: case-insensitive keys/values
9
+ subset: every gold key+value present in pred (tolerates extra pred slots)
10
+ ci-subset: case-insensitive subset (the V32-baseline-equivalent semantic)
11
+ intent_snap: also snap pred intent to nearest 50-enum (post-hoc constraint)
12
+
13
+ Usage:
14
+ uv run python poc/llm-finetune/training/score_baseline.py \\
15
+ poc/llm-finetune/training/eval_dump_q3b_adapter_v7_r16.json \\
16
+ --strategy ci-subset --by intent
17
+
18
+ uv run python poc/llm-finetune/training/score_baseline.py <dump> --strategy all
19
+ """
20
+ from __future__ import annotations
21
+ import argparse
22
+ import json
23
+ import re
24
+ from collections import defaultdict
25
+ from difflib import get_close_matches
26
+ from pathlib import Path
27
+
28
+ INTENTS_50 = sorted(json.loads(Path("/Users/jean-patricksmith/digital/kingly/apps/production/naac/poc/deberta_intent/checkpoints-base/label_mapping.json").read_text())["intent2id"].keys())
29
+ INTENT_SET = set(INTENTS_50)
30
+
31
+
32
+ def normalize(d: dict, ci: bool) -> dict:
33
+ if not isinstance(d, dict):
34
+ return {}
35
+ if ci:
36
+ return {str(k).upper(): str(v).strip().lower() for k, v in d.items() if v is not None and str(v) != ""}
37
+ return {str(k): str(v) for k, v in d.items() if v is not None}
38
+
39
+
40
+ def slot_match(pred_slots: dict, gold_slots: dict, strategy: str) -> bool:
41
+ if strategy == "exact":
42
+ return pred_slots == gold_slots
43
+ if strategy == "ci":
44
+ return normalize(pred_slots, ci=True) == normalize(gold_slots, ci=True)
45
+ if strategy == "subset":
46
+ return all(pred_slots.get(k) == v for k, v in gold_slots.items())
47
+ if strategy == "ci-subset":
48
+ p = normalize(pred_slots, ci=True)
49
+ g = normalize(gold_slots, ci=True)
50
+ return all(p.get(k) == v for k, v in g.items())
51
+ raise ValueError(f"unknown strategy: {strategy}")
52
+
53
+
54
+ def snap_intent(intent_str: str | None) -> str | None:
55
+ if not intent_str or intent_str in INTENT_SET:
56
+ return intent_str
57
+ matches = get_close_matches(intent_str, INTENTS_50, n=1, cutoff=0.4)
58
+ return matches[0] if matches else intent_str
59
+
60
+
61
+ def score(rows: list[dict], strategy: str, intent_snap: bool = False) -> dict:
62
+ schema_ok = intent_em = slots_em = 0
63
+ invented_intents = 0
64
+ by_intent = defaultdict(lambda: [0, 0, 0])
65
+ by_scenario = defaultdict(lambda: [0, 0, 0])
66
+ failures = []
67
+
68
+ for r in rows:
69
+ gold_intent = r["gold_intent"]
70
+ gold_params = r["gold_parameters"] or {}
71
+ parsed = r["parsed"]
72
+
73
+ if parsed:
74
+ schema_ok += 1
75
+
76
+ pred_intent = (parsed or {}).get("intent")
77
+ if pred_intent and pred_intent not in INTENT_SET:
78
+ invented_intents += 1
79
+ if intent_snap:
80
+ pred_intent = snap_intent(pred_intent)
81
+
82
+ is_intent = bool(parsed and pred_intent == gold_intent)
83
+ is_slots = bool(parsed and slot_match((parsed or {}).get("slots") or {}, gold_params, strategy))
84
+
85
+ intent_em += int(is_intent); slots_em += int(is_slots)
86
+ by_intent[gold_intent][0] += 1
87
+ by_intent[gold_intent][1] += int(is_intent)
88
+ by_intent[gold_intent][2] += int(is_slots)
89
+ by_scenario[r["scenario_id"]][0] += 1
90
+ by_scenario[r["scenario_id"]][1] += int(is_intent)
91
+ by_scenario[r["scenario_id"]][2] += int(is_slots)
92
+
93
+ if not (is_intent and is_slots) and len(failures) < 20:
94
+ failures.append({
95
+ "scenario": r["scenario_id"],
96
+ "turn": r["turn_index"],
97
+ "text": r["text"][:120],
98
+ "gold": {"intent": gold_intent, "slots": gold_params},
99
+ "pred": parsed,
100
+ "intent_ok": is_intent, "slots_ok": is_slots,
101
+ })
102
+
103
+ n = len(rows)
104
+ return {
105
+ "n": n, "strategy": strategy, "intent_snap": intent_snap,
106
+ "schema_valid": schema_ok, "intent_em": intent_em, "slots_em": slots_em,
107
+ "schema_pct": schema_ok / max(n, 1), "intent_pct": intent_em / max(n, 1), "slots_pct": slots_em / max(n, 1),
108
+ "invented_intents": invented_intents,
109
+ "by_intent": dict(by_intent), "by_scenario": dict(by_scenario),
110
+ "failures": failures,
111
+ }
112
+
113
+
114
+ def main():
115
+ p = argparse.ArgumentParser(description=__doc__)
116
+ p.add_argument("dump", help="JSON dump from eval_baseline.py")
117
+ p.add_argument("--strategy", choices=["exact", "ci", "subset", "ci-subset", "all"], default="ci-subset")
118
+ p.add_argument("--snap-intent", action="store_true", help="post-hoc snap pred intent to nearest 50-enum")
119
+ p.add_argument("--by", choices=["intent", "scenario", "both", "none"], default="intent")
120
+ p.add_argument("--top", type=int, default=20)
121
+ p.add_argument("--show-fails", action="store_true")
122
+ p.add_argument("--out", default=None)
123
+ args = p.parse_args()
124
+
125
+ dump = json.loads(Path(args.dump).read_text())
126
+ rows = dump["rows"]
127
+ print(f"loaded {len(rows)} rows from {args.dump}")
128
+ print(f"adapter: {dump.get('adapter')}\n")
129
+
130
+ strategies = ["exact", "ci", "subset", "ci-subset"] if args.strategy == "all" else [args.strategy]
131
+ results = []
132
+ for strat in strategies:
133
+ r = score(rows, strat, intent_snap=args.snap_intent)
134
+ results.append(r)
135
+ print(f"=== strategy={strat}{'+snap' if args.snap_intent else ''} ===")
136
+ print(f" schema_valid: {r['schema_valid']}/{r['n']} = {r['schema_pct']:.1%}")
137
+ print(f" intent_em: {r['intent_em']}/{r['n']} = {r['intent_pct']:.1%} (V32 baseline: 87.4%)")
138
+ print(f" slots_em: {r['slots_em']}/{r['n']} = {r['slots_pct']:.1%} (V32 baseline: 80.6%)")
139
+ print(f" invented_intents: {r['invented_intents']}")
140
+
141
+ if args.by in ("intent", "both"):
142
+ print(f" --- per-intent (top {args.top}) ---")
143
+ for intent, (n_i, i_em, s_em) in sorted(r["by_intent"].items(), key=lambda kv: -kv[1][0])[:args.top]:
144
+ print(f" {intent:30s} n={n_i:4d} intent={i_em}/{n_i} ({i_em/n_i:.0%}) slots={s_em}/{n_i} ({s_em/n_i:.0%})")
145
+ if args.by in ("scenario", "both"):
146
+ print(f" --- per-scenario (top {args.top}) ---")
147
+ for scn, (n_s, i_em, s_em) in sorted(r["by_scenario"].items(), key=lambda kv: -kv[1][0])[:args.top]:
148
+ print(f" {scn:30s} n={n_s:4d} intent={i_em}/{n_s} ({i_em/n_s:.0%}) slots={s_em}/{n_s} ({s_em/n_s:.0%})")
149
+ if args.show_fails:
150
+ print(f" --- first {len(r['failures'])} failures ---")
151
+ for f in r["failures"]:
152
+ print(f" [{f['scenario']}/{f['turn']}] intent_ok={f['intent_ok']} slots_ok={f['slots_ok']}")
153
+ print(f" text: {f['text']}")
154
+ print(f" gold: {f['gold']}")
155
+ print(f" pred: {f['pred']}")
156
+ print()
157
+
158
+ if args.out:
159
+ Path(args.out).write_text(json.dumps({"adapter": dump.get("adapter"), "results": results}, indent=2))
160
+ print(f"saved scoring report to {args.out}")
161
+
162
+
163
+ if __name__ == "__main__":
164
+ main()