Text Generation
PEFT
Safetensors
lora
trl
grpo
gdpo
dpo
divpo
rlhf
diversity
creative-writing
mode-collapse
Instructions to use Mercity/creative-writing-llm with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use Mercity/creative-writing-llm with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
File size: 5,251 Bytes
cbc33fe | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | """
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])
|