Pranav2748's picture
Add src
cbc33fe verified
Raw
History Blame Contribute Delete
5.25 kB
"""
WritingPrompts data prep. Deterministic and seeded: every arm sees the exact
same train split and the exact same 50 held-out eval prompts.
On the SYSTEM PROMPT (rewritten from the prior run)
---------------------------------------------------
The prior run used, with a ~512-token cap and no length guidance:
"Write a short creative story based on this prompt:\\n\\n{text}"
Result: 100% of eval samples truncated mid-word. The model had no budget to
plan an ending, so it never planned one. Since the judge rubric caps quality at
<=3 for unfinished stories, quality became near-constant within every group ->
zero GRPO advantage -> the quality signal was dead for the whole run.
The fix is a word budget + an explicit ending instruction + a raised token cap.
What the system prompt deliberately does NOT do: it says nothing about being
original, varied, surprising, or avoiding cliche. That would be a confound.
Mode collapse is the dependent variable; instructing the model to diversify
would mask exactly the effect every arm is being measured on. The prompt is
identical and neutral for base, E0, E1, E2, E3 and E4 -- all differences
between arms must come from the training objective, not the prompt.
"""
from __future__ import annotations
import hashlib
import json
import re
from pathlib import Path
from datasets import load_dataset
SYSTEM_PROMPT = (
"You are a fiction writer. Write a complete short story of 200-500 words "
"responding to the writing prompt.\n"
"Write only the story: no title, no preamble, no commentary, no author's note.\n"
"Finish inside the word budget. The story must reach a real ending, not stop mid-scene."
)
MIN_PROMPT_WORDS = 10
MAX_PROMPT_WORDS = 60
SEED = 42
_TAG = re.compile(r"^\s*\[\s*(WP|EU|CW|TT|RF|IP|PI|PM|MP|OT|SP|FF)\s*\]\s*", re.I)
_WS = re.compile(r"\s+")
def clean_prompt(text: str) -> str:
"""Strip the subreddit tag and normalize the mangled WritingPrompts spacing.
The euclaise mirror is tokenizer-detokenized: "did n't phase us ." etc.
Left as-is this leaks a distinctive artifact into every prompt and wastes
judge tokens, so we repair the obvious cases.
"""
t = _TAG.sub("", text or "").strip()
t = t.replace("`` ", '"').replace(" ''", '"').replace("''", '"')
t = re.sub(r"([“‘(\[])\s+", r"\1", t) # "“ What" -> "“What"
t = re.sub(r"\s+([”’)\]])", r"\1", t) # "cockroach! ”" -> "cockroach!”"
t = re.sub(r"\s+([,.!?;:])", r"\1", t)
t = re.sub(r"\bn't\b", "n't", t)
t = re.sub(r"\s+n't", "n't", t)
t = re.sub(r"\s+'(s|re|ve|ll|d|m)\b", r"'\1", t)
t = re.sub(r"<\s*newline\s*>", " ", t, flags=re.I)
t = _WS.sub(" ", t).strip()
return t
def _key(t: str) -> str:
"""Dedupe key: lowercase alphanumerics only, so punctuation/spacing
variants of the same prompt collapse together."""
return hashlib.sha1(re.sub(r"[^a-z0-9]", "", t.lower()).encode()).hexdigest()
def build_splits(
n_train: int = 1000,
n_eval: int = 50,
out_dir: str | Path = "data",
seed: int = SEED,
) -> dict:
ds = load_dataset("euclaise/writingprompts", split="train")
col = "prompt" if "prompt" in ds.column_names else ds.column_names[0]
seen: set[str] = set()
kept: list[str] = []
for rec in ds:
t = clean_prompt(rec[col])
n = len(t.split())
if not (MIN_PROMPT_WORDS <= n <= MAX_PROMPT_WORDS):
continue
k = _key(t)
if k in seen:
continue
seen.add(k)
kept.append(t)
if len(kept) >= (n_train + n_eval) * 3: # oversample, then shuffle
break
import random
random.Random(seed).shuffle(kept)
need = n_train + n_eval
if len(kept) < need:
raise RuntimeError(f"only {len(kept)} usable prompts, need {need}")
sel = kept[:need]
eval_prompts, train_prompts = sel[:n_eval], sel[n_eval:need]
out = Path(out_dir)
out.mkdir(parents=True, exist_ok=True)
for name, rows in (("train", train_prompts), ("eval", eval_prompts)):
p = out / f"{name}_prompts.jsonl"
with open(p, "w") as f:
for i, t in enumerate(rows):
f.write(json.dumps({"id": f"{name}-{i:04d}", "prompt": t}) + "\n")
meta = {
"seed": seed, "n_train": len(train_prompts), "n_eval": len(eval_prompts),
"total_scanned": len(ds), "usable_after_filter": len(kept),
"min_words": MIN_PROMPT_WORDS, "max_words": MAX_PROMPT_WORDS,
"system_prompt": SYSTEM_PROMPT,
}
(out / "split_meta.json").write_text(json.dumps(meta, indent=2))
return meta
def load_prompts(split: str, out_dir: str | Path = "data") -> list[dict]:
p = Path(out_dir) / f"{split}_prompts.jsonl"
return [json.loads(l) for l in open(p) if l.strip()]
def chat_messages(prompt: str) -> list[dict]:
return [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": prompt},
]
if __name__ == "__main__":
m = build_splits()
print(json.dumps(m, indent=2))
for split in ("train", "eval"):
rows = load_prompts(split)
print(f"\n{split}: {len(rows)}")
for r in rows[:3]:
print(" ", r["id"], "|", r["prompt"][:110])