unparallelled commited on
Commit
7c9b788
·
verified ·
1 Parent(s): 5e5c72f

Upload scripts/peer/peer_score.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. scripts/peer/peer_score.py +119 -0
scripts/peer/peer_score.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ peer_score.py -- score peer generations, matching longeval_score.py EXACTLY.
4
+
5
+ The scoring rule is copied deliberately, not reimplemented: norm() strips every
6
+ non-alphanumeric character and lowercases, and a prompt scores 1 only when EVERY
7
+ expected string appears in the normalised generation (RULER's recall criterion).
8
+ Any divergence here would make peer numbers incomparable to ours, which is the
9
+ entire point of the exercise.
10
+
11
+ Writes summary.csv and depth_summary.csv in the SAME shape run_longeval.sh emits,
12
+ so the existing notebook builders read peer results without modification.
13
+
14
+ usage: peer_score.py --gen-dir gen/ --meta-dir prompts/ --out-dir results/
15
+ --models qwen3-4b,spark-4b --tasks niah_single,val_backward
16
+ """
17
+
18
+ import argparse
19
+ import csv
20
+ import json
21
+ import math
22
+ import os
23
+
24
+
25
+ def norm(s):
26
+ return "".join(ch for ch in s.lower() if ch.isalnum())
27
+
28
+
29
+ def wilson(h, n):
30
+ if not n:
31
+ return float("nan")
32
+ z, p = 1.96, h / n
33
+ return 100.0 * z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n)) / (1 + z * z / n)
34
+
35
+
36
+ def score_one(gen_path, meta_path):
37
+ """-> (hits, n, {depth: [hits, n]})"""
38
+ meta = json.load(open(meta_path))
39
+ items = {it["i"]: it for it in meta["items"]}
40
+ hits = n = 0
41
+ by_depth = {}
42
+ with open(gen_path) as f:
43
+ for line in f:
44
+ line = line.strip()
45
+ if not line:
46
+ continue
47
+ g = json.loads(line)
48
+ it = items.get(g["i"])
49
+ if it is None:
50
+ continue
51
+ tn = norm(g["generated"])
52
+ ok = all(norm(e) in tn for e in it["expected"])
53
+ hits += ok
54
+ n += 1
55
+ d = it["depth"]
56
+ by_depth.setdefault(d, [0, 0])
57
+ by_depth[d][0] += ok
58
+ by_depth[d][1] += 1
59
+ return hits, n, by_depth
60
+
61
+
62
+ def main():
63
+ ap = argparse.ArgumentParser()
64
+ ap.add_argument("--gen-dir", required=True)
65
+ ap.add_argument("--meta-dir", required=True)
66
+ ap.add_argument("--out-dir", required=True)
67
+ ap.add_argument("--length", required=True,
68
+ help="prompt length; metas live at <meta-dir>/<model>/<length>/")
69
+ ap.add_argument("--models", required=True, help="comma list of model keys")
70
+ ap.add_argument("--tasks", required=True, help="comma list of task names")
71
+ args = ap.parse_args()
72
+
73
+ models = args.models.split(",")
74
+ tasks = args.tasks.split(",")
75
+ os.makedirs(args.out_dir, exist_ok=True)
76
+
77
+ per_task = {} # task -> model -> (hits, n)
78
+ per_depth = {} # depth -> model -> [hits, n]
79
+ for m in models:
80
+ for t in tasks:
81
+ g = os.path.join(args.gen_dir, f"{m}_{t}.jsonl")
82
+ # layout is <meta-dir>/<model>/<length>/<task>_meta.json -- the
83
+ # length level was missing here, so every meta lookup failed and the
84
+ # scorer wrote a header-only summary instead of erroring.
85
+ md = os.path.join(args.meta_dir, m, args.length, f"{t}_meta.json")
86
+ if not (os.path.exists(g) and os.path.exists(md)):
87
+ print(f" skip {m}/{t}: missing {'gen' if not os.path.exists(g) else 'meta'}")
88
+ continue
89
+ h, n, bd = score_one(g, md)
90
+ if not n:
91
+ print(f" skip {m}/{t}: 0 scorable"); continue
92
+ per_task.setdefault(t, {})[m] = (h, n)
93
+ for d, (dh, dn) in bd.items():
94
+ cell = per_depth.setdefault(d, {}).setdefault(m, [0, 0])
95
+ cell[0] += dh; cell[1] += dn
96
+ print(f" {m:<16} {t:<16} {h}/{n} = {100.0*h/n:5.1f}% +-{wilson(h,n):4.1f}")
97
+
98
+ have = [m for m in models if any(m in v for v in per_task.values())]
99
+ if not have:
100
+ raise SystemExit(f" FATAL: nothing scorable for length {args.length}. "
101
+ f"Checked gen={args.gen_dir}/<model>_<task>.jsonl and "
102
+ f"meta={args.meta_dir}/<model>/{args.length}/<task>_meta.json")
103
+ with open(os.path.join(args.out_dir, "summary.csv"), "w", newline="") as f:
104
+ w = csv.writer(f); w.writerow(["task"] + have)
105
+ for t in sorted(per_task):
106
+ w.writerow([t] + [f"{100.0*per_task[t][m][0]/per_task[t][m][1]:.1f}"
107
+ if m in per_task[t] else "" for m in have])
108
+ with open(os.path.join(args.out_dir, "depth_summary.csv"), "w", newline="") as f:
109
+ w = csv.writer(f); w.writerow(["depth", "model", "hits", "n", "pct", "wilson95"])
110
+ for d in sorted(per_depth):
111
+ for m in have:
112
+ if m in per_depth[d]:
113
+ h, n = per_depth[d][m]
114
+ w.writerow([d, m, h, n, f"{100.0*h/n:.1f}", f"{wilson(h,n):.1f}"])
115
+ print(f"\n wrote {args.out_dir}/summary.csv and depth_summary.csv")
116
+
117
+
118
+ if __name__ == "__main__":
119
+ main()