gec-inline / scripts /build_dataset.py
Lopato4ka's picture
Upload folder using huggingface_hub
32387a0 verified
Raw
History Blame Contribute Delete
6.55 kB
"""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()