|
| 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