Spaces:
Sleeping
Sleeping
| """Build the SFT training set and the JFLEG evaluation set as JSONL. | |
| Training JSONL schema (one example per line): | |
| { | |
| "source": "I goes to school .", # ungrammatical input | |
| "target": "I go to school .", # gold plain corrected sentence | |
| "completion":"I {goes=>go} to school .", # bracketed string the model must emit | |
| "messages": [system, user, assistant] # chat-template-ready format | |
| } | |
| Evaluation JSONL schemas: | |
| JFLEG row (multi-reference, fluency-oriented): | |
| { | |
| "source": "...", | |
| "corrections": ["ref1", "ref2", "ref3", "ref4"], | |
| } | |
| BEA W&I+LOCNESS dev row (single-reference, canonical ERRANT F0.5): | |
| { | |
| "source": "...", | |
| "target": "...", | |
| "completion": "...", # gold bracketed string from M2 | |
| } | |
| Usage:: | |
| python -m scripts.build_dataset \ | |
| --m2 data/raw/wi+locness/m2/ABC.train.gold.bea19.m2 \ | |
| --m2 data/raw/fce/m2/fce.train.gold.bea19.m2 \ | |
| --train-out data/processed/train.jsonl \ | |
| --eval-out data/processed/eval.jsonl \ | |
| --max-train 10000 --seed 3407 | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import random | |
| from pathlib import Path | |
| from datasets import load_dataset | |
| from tqdm import tqdm | |
| from gec.m2 import iter_m2 | |
| from gec.prompts import SYSTEM_PROMPT, build_user_message | |
| from gec.render import render_inline | |
| def build_train( | |
| m2_paths: list[Path], | |
| max_train: int, | |
| seed: int, | |
| keep_identity_fraction: float = 0.05, | |
| ) -> list[dict]: | |
| """Read M2 files, render bracketed completions, return a list of examples. | |
| Identity examples (source == target, no edits) are mostly dropped: we | |
| keep ``keep_identity_fraction`` so the model still learns to recognise | |
| already-correct sentences. The rest of the budget goes to edited ones. | |
| """ | |
| rng = random.Random(seed) | |
| edited: list[dict] = [] | |
| identity: list[dict] = [] | |
| for path in m2_paths: | |
| for sent in iter_m2(path): | |
| if len(sent.source_tokens) < 3 or len(sent.source_tokens) > 80: | |
| continue # skip very short / very long sentences | |
| rendered = render_inline(sent.source_tokens, sent.edits) | |
| example = { | |
| "source": sent.source, | |
| "target": sent.target, | |
| "completion": rendered, | |
| } | |
| if sent.edits: | |
| edited.append(example) | |
| else: | |
| identity.append(example) | |
| rng.shuffle(edited) | |
| rng.shuffle(identity) | |
| identity_budget = int(max_train * keep_identity_fraction) | |
| edited_budget = max_train - identity_budget | |
| chosen = edited[:edited_budget] + identity[:identity_budget] | |
| rng.shuffle(chosen) | |
| for ex in chosen: | |
| ex["messages"] = [ | |
| {"role": "system", "content": SYSTEM_PROMPT}, | |
| {"role": "user", "content": build_user_message(ex["source"])}, | |
| {"role": "assistant", "content": ex["completion"]}, | |
| ] | |
| return chosen | |
| def build_eval_bea(m2_path: Path) -> list[dict]: | |
| """Read a BEA M2 dev/test file and return source/target/completion rows.""" | |
| rows: list[dict] = [] | |
| for sent in iter_m2(m2_path): | |
| if not sent.source_tokens: | |
| continue | |
| rows.append({ | |
| "source": sent.source, | |
| "target": sent.target, | |
| "completion": render_inline(sent.source_tokens, sent.edits), | |
| }) | |
| return rows | |
| def build_eval_jfleg() -> tuple[list[dict], list[dict]]: | |
| """Return (validation, test) lists of {source, corrections}.""" | |
| ds = load_dataset("jhu-clsp/jfleg") | |
| def _clean(s: str) -> str: | |
| return " ".join(s.strip().split()) | |
| def _split(name: str) -> list[dict]: | |
| out = [] | |
| for row in ds[name]: | |
| src = _clean(row["sentence"]) | |
| refs = [_clean(c) for c in row["corrections"]] | |
| if not src: | |
| continue | |
| out.append({"source": src, "corrections": refs}) | |
| return out | |
| return _split("validation"), _split("test") | |
| def write_jsonl(path: Path, rows: list[dict]) -> None: | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| with path.open("w", encoding="utf-8") as f: | |
| for row in rows: | |
| f.write(json.dumps(row, ensure_ascii=False) + "\n") | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument( | |
| "--m2", action="append", required=True, | |
| help="Path to a BEA M2 file. Pass --m2 multiple times to concatenate.", | |
| ) | |
| ap.add_argument("--train-out", type=Path, default=Path("data/processed/train.jsonl")) | |
| ap.add_argument( | |
| "--eval-out", type=Path, default=Path("data/processed/eval_jfleg_dev.jsonl"), | |
| help="JFLEG dev split, the multi-reference fluency benchmark.", | |
| ) | |
| ap.add_argument( | |
| "--eval-test-out", type=Path, default=Path("data/processed/eval_jfleg_test.jsonl"), | |
| help="JFLEG test split, kept separate from the dev split used for tuning.", | |
| ) | |
| ap.add_argument( | |
| "--bea-dev-m2", type=Path, | |
| default=Path("data/raw/wi+locness/m2/ABCN.dev.gold.bea19.m2"), | |
| help="BEA W&I+LOCNESS dev M2 — the canonical single-reference ERRANT F0.5 set.", | |
| ) | |
| ap.add_argument( | |
| "--bea-dev-out", type=Path, | |
| default=Path("data/processed/eval_bea_dev.jsonl"), | |
| ) | |
| ap.add_argument("--max-train", type=int, default=10000) | |
| ap.add_argument("--seed", type=int, default=3407) | |
| args = ap.parse_args() | |
| m2_paths = [Path(p) for p in args.m2] | |
| for p in m2_paths: | |
| if not p.exists(): | |
| raise SystemExit(f"M2 file not found: {p}") | |
| print(f"Building training set from {len(m2_paths)} M2 file(s)…") | |
| train = build_train(m2_paths, max_train=args.max_train, seed=args.seed) | |
| write_jsonl(args.train_out, train) | |
| print(f" -> wrote {len(train)} examples to {args.train_out}") | |
| print("Building JFLEG eval set…") | |
| dev, test = build_eval_jfleg() | |
| write_jsonl(args.eval_out, dev) | |
| write_jsonl(args.eval_test_out, test) | |
| print(f" -> wrote {len(dev)} JFLEG dev / {len(test)} JFLEG test examples") | |
| if args.bea_dev_m2.exists(): | |
| print(f"Building BEA dev eval set from {args.bea_dev_m2}…") | |
| bea_dev = build_eval_bea(args.bea_dev_m2) | |
| write_jsonl(args.bea_dev_out, bea_dev) | |
| print(f" -> wrote {len(bea_dev)} BEA dev examples to {args.bea_dev_out}") | |
| else: | |
| print(f"Skipping BEA dev (file not found: {args.bea_dev_m2})") | |
| if __name__ == "__main__": | |
| main() | |