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: 6,417 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 147 148 149 150 151 152 153 154 155 156 157 158 | """
Qualitative diff across arms: actually READ the stories.
Aggregate metrics can say "effective rank 2.0/16" without conveying that six of
the sixteen ships are literally named *Aethel*. This module extracts the
concrete signature of collapse -- shared openings, repeated proper nouns,
shared closing cadence, tone/genre spread -- so a human can see what changed.
Works on either a scored pool (`--pool`) or an eval dump (`--eval`), so the
base model, every checkpoint and every arm are read the same way.
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from collections import Counter, defaultdict
from pathlib import Path
import numpy as np
ROOT = Path(__file__).resolve().parent.parent
OPEN_FRAME = re.compile(r"^The\s+\*?([A-Z][\w' ]{2,20})\*?\s+(\w+ed|\w+s)\s+"
r"(through|over|above|at|into|across)\b")
ITALIC_NAME = re.compile(r"\*([A-Z][\w' ]{2,24})\*")
CADENCE = [
(r"for the first time in (centuries|years|generations|decades|a long time)",
"'for the first time in X'"),
(r"\b(had )?begun to\b", "'begun to'"),
(r"\bwasn'?t\s+\w+[^.]{0,50},?\s+it\s+was\b", "'it wasn't X, it was Y'"),
(r"\n\n[^\n]{1,60}\.\s*$", "one-line closing paragraph"),
]
# crude genre/register probes -- presence of ANY is weak, but the SPREAD across
# a 16-sample set is the interesting number
REGISTER = {
"elegiac/literary": r"\b(silence|ache|hollow|dust|memory|grief|quiet)\b",
"comic": r"\b(ridiculous|absurd|joke|laughed|idiot|bureaucra|paperwork)\b",
"horror": r"\b(scream|blood|rot|teeth|corpse|terror|wrong)\b",
"technical/SF": r"\b(protocol|sensor|reactor|alloy|deploy|calibrat|drone)\b",
"dialogue-driven": r'"[^"]{10,}"',
"second person": r"\bYou (are|were|walk|feel|stand|know)\b",
"epistolary": r"\b(Dear |entry |log:|Report |memo)\b",
}
def first_sentence(t: str, n: int = 130) -> str:
return re.split(r"(?<=[.!?])\s", t.strip())[0][:n]
def last_sentence(t: str, n: int = 120) -> str:
return re.split(r"(?<=[.!?])\s", t.strip())[-1][:n]
def analyze_group(texts: list[str]) -> dict:
n = len(texts)
names = Counter(x for t in texts for x in ITALIC_NAME.findall(t))
opens_the = sum(1 for t in texts if t.strip().startswith("The "))
frame = sum(1 for t in texts if OPEN_FRAME.match(t.strip()))
# repeated content words across stories (excluding prompt-driven ones is
# impossible in general, so we report the top shared nouns as-is)
stop = set("the a an and or but of to in on it its was were is are that this "
"with for as at by from had have has been be he she they we you i "
"his her their our my not no so then than there here what when "
"which who all more most into over under out up down".split())
per_story_vocab = [set(w for w in re.sub(r"[^a-z\s]", " ", t.lower()).split()
if w not in stop and len(w) > 3) for t in texts]
shared = Counter(w for v in per_story_vocab for w in v)
ubiquitous = {w: c for w, c in shared.most_common(400) if c >= max(3, int(0.6 * n))}
cad = {}
for pat, label in CADENCE:
cad[label] = sum(1 for t in texts if re.search(pat, t, re.I | re.M))
reg = {}
for label, pat in REGISTER.items():
reg[label] = sum(1 for t in texts if re.search(pat, t, re.I))
return {
"n": n,
"opens_with_The": opens_the,
"opening_frame_match": frame,
"distinct_first_5_words": len({" ".join(t.strip().split()[:5]).lower() for t in texts}),
"top_names": dict(names.most_common(6)),
"max_name_reuse": max(names.values()) if names else 0,
"ubiquitous_words": dict(list(ubiquitous.items())[:12]),
"cadence": cad,
"register_spread": reg,
"registers_present": sum(1 for v in reg.values() if v >= max(2, int(0.15 * n))),
}
def load_pool(tag: str, split: str, prompt_id: str | None):
rows = [json.loads(l) for l in
open(ROOT / "outputs" / f"pool_{tag}" / f"pool_{split}.jsonl") if l.strip()]
by = defaultdict(list)
for r in rows:
by[r["prompt_id"]].append(r)
if prompt_id:
return {prompt_id: by[prompt_id]}
return dict(by)
def load_eval(label: str, out: str):
p = ROOT / out / "samples" / f"{label}_full.json"
data = json.load(open(p))
return {d["prompt_id"]: [{"text": t, "prompt": d["prompt"]} for t in d["texts"]]
for d in data}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--pool", help="pool tag, e.g. 4b")
ap.add_argument("--eval", help="eval label, e.g. E0-baseline")
ap.add_argument("--out", default="outputs/eval")
ap.add_argument("--split", default="train")
ap.add_argument("--prompt-id", default=None)
ap.add_argument("--show", type=int, default=16, help="first sentences to print")
ap.add_argument("--limit-prompts", type=int, default=40)
args = ap.parse_args()
groups = load_pool(args.pool, args.split, args.prompt_id) if args.pool \
else load_eval(args.eval, args.out)
keys = list(groups)[: args.limit_prompts]
agg = defaultdict(list)
for pid in keys:
texts = [r["text"] for r in groups[pid]]
if len(texts) < 4:
continue
a = analyze_group(texts)
agg["opens_with_The"].append(a["opens_with_The"] / a["n"])
agg["opening_frame"].append(a["opening_frame_match"] / a["n"])
agg["distinct_openers"].append(a["distinct_first_5_words"] / a["n"])
agg["max_name_reuse"].append(a["max_name_reuse"])
agg["registers_present"].append(a["registers_present"])
for k, v in a["cadence"].items():
agg[f"cadence:{k}"].append(v / a["n"])
name = args.eval or f"pool-{args.pool}"
print(f"\n===== QUALITATIVE PROFILE: {name} ({len(keys)} prompts) =====")
for k, v in agg.items():
print(f" {k:38} {np.mean(v):.3f}")
pid = args.prompt_id or keys[0]
texts = [r["text"] for r in groups[pid]][: args.show]
print(f"\n--- prompt {pid} ---")
print(" ", groups[pid][0].get("prompt", "")[:220])
print("\nOPENINGS:")
for i, t in enumerate(texts):
print(f" {i+1:2d}. {first_sentence(t)}")
print("\nCLOSINGS:")
for i, t in enumerate(texts):
print(f" {i+1:2d}. ...{last_sentence(t)}")
return 0
if __name__ == "__main__":
sys.exit(main())
|