Upload source/files/longeval_score.py with huggingface_hub
Browse files- source/files/longeval_score.py +105 -0
source/files/longeval_score.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
longeval_score.py -- score CP_GENERATE output against longeval_gen.py metadata.
|
| 4 |
+
|
| 5 |
+
This is REAL benchmark scoring (substring match on generated text), not the NLL
|
| 6 |
+
proxy needle_score.py computes. A point is awarded only when every expected
|
| 7 |
+
string appears in the generation, which is RULER's recall-style criterion.
|
| 8 |
+
|
| 9 |
+
usage: longeval_score.py --gen <generations.csv> --meta <task_meta.json>
|
| 10 |
+
[--out results.csv] [--show 3]
|
| 11 |
+
|
| 12 |
+
Reports overall accuracy plus a per-depth breakdown -- the depth axis is the
|
| 13 |
+
lost-in-the-middle curve, so a model that only retrieves from the end of the
|
| 14 |
+
window shows a strong depth gradient even at high overall accuracy.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
import argparse
|
| 18 |
+
import csv
|
| 19 |
+
import json
|
| 20 |
+
import sys
|
| 21 |
+
|
| 22 |
+
try:
|
| 23 |
+
import tiktoken
|
| 24 |
+
except ImportError:
|
| 25 |
+
sys.exit("longeval_score.py needs tiktoken: pip install tiktoken")
|
| 26 |
+
|
| 27 |
+
ENC = tiktoken.get_encoding("gpt2")
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def norm(s):
|
| 31 |
+
return "".join(ch for ch in s.lower() if ch.isalnum())
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def main():
|
| 35 |
+
ap = argparse.ArgumentParser()
|
| 36 |
+
ap.add_argument("--gen", required=True, help="CP_GEN_OUT csv: i,tok,tok,...")
|
| 37 |
+
ap.add_argument("--meta", required=True, help="<task>_meta.json")
|
| 38 |
+
ap.add_argument("--out", default="", help="optional per-item csv")
|
| 39 |
+
ap.add_argument("--show", type=int, default=3, help="print N sample generations")
|
| 40 |
+
args = ap.parse_args()
|
| 41 |
+
|
| 42 |
+
meta = json.load(open(args.meta))
|
| 43 |
+
items = {it["i"]: it for it in meta["items"]}
|
| 44 |
+
|
| 45 |
+
gens = {}
|
| 46 |
+
with open(args.gen) as f:
|
| 47 |
+
for row in csv.reader(f):
|
| 48 |
+
if not row:
|
| 49 |
+
continue
|
| 50 |
+
i = int(row[0])
|
| 51 |
+
gens[i] = [int(t) for t in row[1:] if t.strip() != ""]
|
| 52 |
+
|
| 53 |
+
missing = sorted(set(items) - set(gens))
|
| 54 |
+
if missing:
|
| 55 |
+
print(f" WARN: {len(missing)} prompts have no generation "
|
| 56 |
+
f"(first: {missing[:5]}) -- scoring only what is present")
|
| 57 |
+
|
| 58 |
+
rows, n_ok = [], 0
|
| 59 |
+
by_depth = {}
|
| 60 |
+
for i in sorted(gens):
|
| 61 |
+
if i not in items:
|
| 62 |
+
continue
|
| 63 |
+
it = items[i]
|
| 64 |
+
text = ENC.decode(gens[i])
|
| 65 |
+
tn = norm(text)
|
| 66 |
+
hits = [e for e in it["expected"] if norm(e) in tn]
|
| 67 |
+
ok = len(hits) == len(it["expected"])
|
| 68 |
+
n_ok += ok
|
| 69 |
+
d = it["depth"]
|
| 70 |
+
by_depth.setdefault(d, [0, 0])
|
| 71 |
+
by_depth[d][0] += ok
|
| 72 |
+
by_depth[d][1] += 1
|
| 73 |
+
rows.append({"i": i, "depth": d, "ok": int(ok),
|
| 74 |
+
"n_expected": len(it["expected"]), "n_hit": len(hits),
|
| 75 |
+
"expected": "|".join(it["expected"]),
|
| 76 |
+
"generated": text.replace("\n", "\\n")[:200]})
|
| 77 |
+
|
| 78 |
+
n = len(rows)
|
| 79 |
+
if n == 0:
|
| 80 |
+
sys.exit(" no scorable prompts -- check --gen and --meta pair up")
|
| 81 |
+
|
| 82 |
+
print(f"\n=== {meta['task']} (prompt_len={meta['prompt_len']}, n={n}) ===")
|
| 83 |
+
print(f" accuracy: {n_ok}/{n} = {100.0 * n_ok / n:.1f}%")
|
| 84 |
+
print(f" {'depth':>7} {'acc':>8} (needle position in context)")
|
| 85 |
+
for d in sorted(by_depth):
|
| 86 |
+
ok, tot = by_depth[d]
|
| 87 |
+
print(f" {d:>7.2f} {ok:>3}/{tot:<3} {100.0 * ok / tot:5.1f}%")
|
| 88 |
+
|
| 89 |
+
if args.show:
|
| 90 |
+
print(f"\n --- {min(args.show, n)} sample generations ---")
|
| 91 |
+
for r in rows[:args.show]:
|
| 92 |
+
mark = "HIT " if r["ok"] else "MISS"
|
| 93 |
+
print(f" [{mark}] expect={r['expected']}")
|
| 94 |
+
print(f" got={r['generated'][:150]}")
|
| 95 |
+
|
| 96 |
+
if args.out:
|
| 97 |
+
with open(args.out, "w", newline="") as f:
|
| 98 |
+
w = csv.DictWriter(f, fieldnames=list(rows[0]))
|
| 99 |
+
w.writeheader()
|
| 100 |
+
w.writerows(rows)
|
| 101 |
+
print(f"\n wrote {args.out}")
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
if __name__ == "__main__":
|
| 105 |
+
main()
|