Skip to content
This repository was archived by the owner on Oct 17, 2026. It is now read-only.

Commit dd9fbcf

Browse files
committed
training: prepare_jeff_mix.py — firelex/jeff public converters' output (92k rows: QA/NLI/hallucination/sentiment, exact-probability noul, capped long lists) to Von rows with a held-out leak filter
1 parent 3207373 commit dd9fbcf

1 file changed

Lines changed: 157 additions & 0 deletions

File tree

‎training/prepare_jeff_mix.py‎

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
"""Convert firelex/jeff training rows (Example format) into Von rows, with a leak filter against our held-out suites.
2+
3+
Jeff (MIT code, Apache weights; data sources listed in its docs/data-sources.md, each under its own licence) ships
4+
converters for public QA / NLI / hallucination / sentiment sets, code-built probability questions with exact targets,
5+
and 20-254-option long lists. We reuse its converters' *output* only. This script:
6+
7+
1. reads Jeff Example rows ({id, suite, state, question:{type, instructions, criteria}, label, target, source})
8+
2. drops any row that touches our held-out gates: jabr v2 states, Hagetino probes, judge_heldout, and the
9+
JevBench public tiers (exact normalised state, or a normalised option text unique to one held-out item)
10+
3. writes Von rows {state, question, options:[{id, description}], label[, target], source, licence}
11+
12+
Von's trainer packs one question per row; Jeff's long-list rows (up to 254 options) exceed the 4096-token schema
13+
budget, so lists are capped at --max-options (gold kept, distractors subsampled).
14+
15+
python training/prepare_jeff_mix.py --jeff ~/scratch/ext/jeff/data --out ~/scratch/data_jeff_mix/train.jsonl
16+
"""
17+
from __future__ import annotations
18+
19+
import argparse
20+
import collections
21+
import glob
22+
import json
23+
import os
24+
import random
25+
import re
26+
import sys
27+
from typing import Any, Dict, Iterable, List, Set
28+
29+
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
30+
sys.path.insert(0, ROOT)
31+
sys.path.insert(0, os.path.join(ROOT, "benchmarks"))
32+
33+
_WS = re.compile(r"\s+")
34+
35+
36+
def norm(text: Any) -> str:
37+
return _WS.sub(" ", str(text or "")).strip().lower()
38+
39+
40+
def heldout_rows() -> List[dict]:
41+
from benchmarks.gate_standard import jabr_rows, jev_rows, judge_rows, probe_rows
42+
rows = jabr_rows() + probe_rows() + judge_rows()
43+
for tier in ("easy", "standard", "hard"):
44+
try:
45+
rows += jev_rows(tier)
46+
except FileNotFoundError:
47+
print(f"warning: jev {tier} public file missing; leak filter does not cover it", file=sys.stderr)
48+
return rows
49+
50+
51+
class LeakGuard:
52+
"""Exact normalised state, or an option text (5+ words) that appears in exactly one held-out item."""
53+
54+
def __init__(self, rows: Iterable[dict]) -> None:
55+
self.states: Set[str] = set()
56+
opts: collections.Counter = collections.Counter()
57+
for r in rows:
58+
s = norm(r.get("state"))
59+
if s:
60+
self.states.add(s)
61+
crit = r.get("criteria") or {}
62+
vals = crit.values() if isinstance(crit, dict) else crit
63+
for v in set(norm(v) for v in vals):
64+
if len(v.split()) >= 5:
65+
opts[v] += 1
66+
self.options = {t for t, c in opts.items() if c == 1}
67+
68+
def leaks(self, state: str, descriptions: Iterable[str]) -> bool:
69+
if norm(state) in self.states:
70+
return True
71+
return any(norm(d) in self.options for d in descriptions)
72+
73+
74+
def convert(row: dict, rng: random.Random, max_options: int) -> Dict[str, Any] | None:
75+
q = row["question"]
76+
qtype = q["type"]
77+
label = str(row["label"])
78+
if qtype == "noul":
79+
crit = q.get("criteria") or {}
80+
options = [{"id": "yes", "description": crit.get("true") or "Yes."},
81+
{"id": "no", "description": crit.get("false") or "No."}]
82+
gold = "yes" if label.lower() in ("true", "yes") else "no"
83+
out = {"state": row["state"], "question": q["instructions"], "options": options, "label": gold}
84+
t = row.get("target")
85+
if isinstance(t, (int, float)) and not isinstance(t, bool):
86+
out["target"] = [float(t), 1.0 - float(t)] # exact probability from the code-built questions
87+
return out
88+
if qtype == "choice":
89+
crit = q["criteria"]
90+
if label not in crit:
91+
return None
92+
ids = list(crit)
93+
if len(ids) > max_options:
94+
keep = [i for i in ids if i != label]
95+
rng.shuffle(keep)
96+
ids = sorted([label] + keep[: max_options - 1], key=ids.index)
97+
# The trainer infers the question type from option ids: contiguous 0..K-1 reads as a Score ladder.
98+
# cosmos_qa / truthful_qa use numeric ids for plain choices, so relabel those to letters.
99+
if all(str(i).isdigit() for i in ids):
100+
letters = {i: chr(ord("A") + n) for n, i in enumerate(ids)}
101+
options = [{"id": letters[i], "description": str(crit[i])} for i in ids]
102+
return {"state": row["state"], "question": q["instructions"], "options": options, "label": letters[label]}
103+
options = [{"id": i, "description": str(crit[i])} for i in ids]
104+
return {"state": row["state"], "question": q["instructions"], "options": options, "label": label}
105+
return None # Jeff's open data carries no score rows
106+
107+
108+
def main() -> None:
109+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
110+
ap.add_argument("--jeff", required=True, help="Jeff data dir holding extra/, benchmark-train/, probability/, longlists/")
111+
ap.add_argument("--out", required=True)
112+
ap.add_argument("--max-options", type=int, default=24)
113+
ap.add_argument("--cap-per-suite", type=int, default=12000, help="rows per source suite (winogrande alone is 40k)")
114+
ap.add_argument("--seed", type=int, default=23)
115+
a = ap.parse_args()
116+
rng = random.Random(a.seed)
117+
118+
guard = LeakGuard(heldout_rows())
119+
print(f"leak guard: {len(guard.states)} states, {len(guard.options)} unique option texts", file=sys.stderr)
120+
121+
by_suite: Dict[str, List[dict]] = collections.defaultdict(list)
122+
stats: collections.Counter = collections.Counter()
123+
for path in sorted(glob.glob(os.path.join(a.jeff, "*", "train.jsonl"))):
124+
part = os.path.basename(os.path.dirname(path))
125+
for line in open(path, encoding="utf-8").read().split("\n"): # not splitlines(): U+2028 inside JSON strings
126+
if not line.strip():
127+
continue
128+
row = json.loads(line)
129+
suite = f"{part}/{row.get('suite', '?')}"
130+
von = convert(row, rng, a.max_options)
131+
if von is None:
132+
stats["unconvertible"] += 1
133+
continue
134+
if guard.leaks(von["state"], (o["description"] for o in von["options"])):
135+
stats["leak:" + suite] += 1
136+
continue
137+
src = row.get("source") or {}
138+
von["source"] = f"jeff:{suite}"
139+
von["licence"] = src.get("license") or "see firelex/jeff docs/data-sources.md"
140+
by_suite[suite].append(von)
141+
142+
out_rows: List[dict] = []
143+
for suite, rows in sorted(by_suite.items()):
144+
rng.shuffle(rows)
145+
kept = rows[: a.cap_per_suite]
146+
out_rows += kept
147+
print(f"{suite:40s} {len(rows):6d} -> {len(kept):6d}", file=sys.stderr)
148+
rng.shuffle(out_rows)
149+
os.makedirs(os.path.dirname(os.path.abspath(a.out)), exist_ok=True)
150+
with open(a.out, "w", encoding="utf-8") as f:
151+
for r in out_rows:
152+
f.write(json.dumps(r, ensure_ascii=False) + "\n")
153+
print(json.dumps({"rows": len(out_rows), "suites": len(by_suite), **stats}), file=sys.stderr)
154+
155+
156+
if __name__ == "__main__":
157+
main()

0 commit comments

Comments
 (0)