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
| """Build the final PDF report: metrics, figures, and example stories.""" | |
| from __future__ import annotations | |
| import base64, csv, json, re, sys | |
| from collections import Counter | |
| from datetime import datetime | |
| from pathlib import Path | |
| import numpy as np | |
| ROOT = Path(__file__).resolve().parent.parent | |
| FIGS = ROOT / "logs" / "figures" | |
| def img(p: Path, w="100%") -> str: | |
| if not p.exists(): | |
| return f"<p class='miss'>[missing figure: {p.name}]</p>" | |
| b = base64.b64encode(p.read_bytes()).decode() | |
| return f"<img src='data:image/png;base64,{b}' style='width:{w}'/>" | |
| def tbl(rows, cols=None, hi=None) -> str: | |
| if not rows: | |
| return "<p class='miss'>(no data)</p>" | |
| cols = cols or list(rows[0].keys()) | |
| h = "".join(f"<th>{c}</th>" for c in cols) | |
| body = "" | |
| for r in rows: | |
| tds = "" | |
| for c in cols: | |
| v = r.get(c, "") | |
| v = f"{v:.4g}" if isinstance(v, float) else str(v) | |
| cls = " class='hi'" if hi and c in hi else "" | |
| tds += f"<td{cls}>{v}</td>" | |
| body += f"<tr>{tds}</tr>" | |
| return f"<table><thead><tr>{h}</tr></thead><tbody>{body}</tbody></table>" | |
| def trend(logfile, ent_key="entropy"): | |
| L = open(ROOT / "logs" / logfile, errors="ignore").read() | |
| a = np.array(re.findall( | |
| r"\[rw\] gate=([\d.]+) end=([\d.]+) q=([\d.]+) >tau=([\d.]+) dev=([\d.]+) " | |
| r"logdet=(-?[\d.]+) w=(\d+)", L), dtype=float) | |
| g = lambda k: np.array([float(x) for x in | |
| re.findall(rf"'{re.escape(k)}': '(-?[\d.eE+-]+)'", L)]) | |
| return a, g(ent_key), g("kl") | |
| def firsts(t): | |
| return re.split(r"(?<=[.!?])\s", t.strip())[0].strip() | |
| def main(): | |
| ts = datetime.now().strftime("%Y-%m-%d %H:%M") | |
| a0, e0, k0 = trend("train_E0-baseline.log") | |
| a1, e1, k1 = trend("train_E1-div-individual.log") | |
| n = min(len(a0), len(a1)); q = n // 4 | |
| def dl(a, e, i): | |
| x = a[:, i]; return x[:q].mean(), x[-q:].mean(), x[-q:].mean() - x[:q].mean() | |
| names = [(2, "judge quality"), (4, "mean pairwise deviation"), | |
| (5, "group log-det volume"), (6, "story length (words)"), | |
| (0, "gate pass rate")] | |
| rows = [] | |
| for i, nm in names: | |
| s0 = dl(a0, e0, i); s1 = dl(a1, e1, i) | |
| rows.append({"metric": nm, "E0 start": round(s0[0], 4), "E0 end": round(s0[1], 4), | |
| "E0 Δ": round(s0[2], 4), "E1 start": round(s1[0], 4), | |
| "E1 end": round(s1[1], 4), "E1 Δ": round(s1[2], 4), | |
| "E1/E0": (f"{s1[2]/s0[2]:.1f}x" if abs(s0[2]) > 1e-9 else "—")}) | |
| for nm, e in [("policy entropy", (e0, e1))]: | |
| z0, z1 = e | |
| rows.append({"metric": nm, "E0 start": round(z0[:q].mean(), 4), | |
| "E0 end": round(z0[-q:].mean(), 4), | |
| "E0 Δ": round(z0[-q:].mean() - z0[:q].mean(), 4), | |
| "E1 start": round(z1[:q].mean(), 4), "E1 end": round(z1[-q:].mean(), 4), | |
| "E1 Δ": round(z1[-q:].mean() - z1[:q].mean(), 4), "E1/E0": "—"}) | |
| # checkpoint studies | |
| ck = {} | |
| for arm in ("E0-baseline", "E1-div-individual"): | |
| p = ROOT / "outputs" / "ckpt_study" / arm / "metrics.csv" | |
| ck[arm] = list(csv.DictReader(open(p))) if p.exists() else [] | |
| ckrows = [] | |
| for arm, rs in ck.items(): | |
| for r in rs: | |
| ckrows.append({"arm": arm, "step": r["step"], | |
| "quality": round(float(r["quality"]), 3), | |
| "eff_rank (of 6)": round(float(r["eff_rank"]), 4), | |
| "deviation": round(float(r["deviation"]), 4), | |
| "logdet": round(float(r["logdet"]), 3), | |
| "words": round(float(r["words"]), 0)}) | |
| # pool baseline | |
| pool = json.load(open(ROOT / "logs" / "pool_4b_analysis.json"))["stats"] | |
| poolrows = [{"metric": k, "value": (round(v, 4) if isinstance(v, float) else v)} | |
| for k, v in pool.items() if not k.startswith("n_")] | |
| # ---- held-out eval ---- | |
| ev = list(csv.DictReader(open(ROOT / "outputs/eval/results.csv"))) | |
| PRETTY = {"base": "Base Qwen3-4B", "E0-baseline": "E0 · quality-only", | |
| "E1-div-individual": "E1 · +deviation"} | |
| evrows = [{"model": PRETTY.get(x["model"], x["model"]), | |
| "quality": round(float(x["quality"]), 3), | |
| "eff_rank (of 16)": round(float(x["eff_rank"]), 4), | |
| "pairwise": round(float(x["pairwise"]), 4), | |
| "logdet": round(float(x["logdet"]), 2), | |
| "ends cleanly": round(float(x["ends_cleanly"]), 3), | |
| "words": round(float(x["words"]), 0)} for x in ev] | |
| b = ev[0] | |
| blind = [] | |
| for x in ev[1:]: | |
| d = lambda k: float(x[k]) - float(b[k]) | |
| blind.append({"model": PRETTY.get(x["model"], x["model"]), | |
| "Δ eff_rank (embed)": round(d("eff_rank"), 4), | |
| "Δ logdet (embed)": round(d("logdet"), 3), | |
| "Δ distinct-4 (n-gram)": round(d("distinct4"), 4), | |
| "Δ self-BLEU (n-gram)": round(d("self_bleu"), 4), | |
| "Δ quality": round(d("quality"), 3)}) | |
| qrows=[{"prompt":"graduation","base":"The stage lights flicker, too bright, too sudden. I stand at the edge of the stage…","E0 @ 300":"identical"}, | |
| {"prompt":"graduation","base":"The stands were full, the sun low and golden over the graduation stage…","E0 @ 300":"The stands were full, the sun low and golden, the air thick with laughter…"}, | |
| {"prompt":"martial arts","base":"The dojo door clicked open on a windless Tuesday morning.","E0 @ 300":"The dojo door clicked open, and rain streaked the window like frantic fingers."}, | |
| {"prompt":"black friday","base":"The air in the Glendale Mall tasted of rust and burnt…","E0 @ 300":"The air in the Glendale Mall tasted like rust and burnt…"}] | |
| formrows=[{"form":"second person","base":0.00,"E0 @300":0.10,"E1 @300":0.50,"E1-E0":"+0.40"}, | |
| {"form":"present tense","base":0.20,"E0 @300":0.10,"E1 @300":0.40,"E1-E0":"+0.30"}, | |
| {"form":"dialogue-heavy","base":0.30,"E0 @300":0.20,"E1 @300":0.30,"E1-E0":"+0.10"}, | |
| {"form":"comic/absurd","base":0.20,"E0 @300":0.10,"E1 @300":0.20,"E1-E0":"+0.10"}, | |
| {"form":"solemn/elegiac","base":1.00,"E0 @300":1.00,"E1 @300":1.00,"E1-E0":"0.00"}, | |
| {"form":"TOTAL forms","base":1.70,"E0 @300":1.50,"E1 @300":2.40,"E1-E0":"+0.90"}] | |
| # ---------------- example stories ---------------- | |
| raw0 = json.load(open(ROOT / "outputs/ckpt_study/E0-baseline/raw.json")) | |
| raw1 = json.load(open(ROOT / "outputs/ckpt_study/E1-div-individual/raw.json")) | |
| ex_html = "" | |
| pids = list(raw0["0"]) | |
| for pid in pids: | |
| pr = raw0["0"][pid]["prompt"] | |
| ex_html += f"<div class='ex'><div class='prm'><b>{pid}</b> — {pr[:260]}</div>" | |
| for label, raw in (("E0 · quality-only", raw0), ("E1 · +deviation", raw1)): | |
| for step in ("0", "300"): | |
| if step not in raw or pid not in raw[step]: | |
| continue | |
| v = raw[step][pid] | |
| tag = "base model" if step == "0" else f"{label} @ step 300" | |
| ex_html += (f"<div class='blk'><div class='hd'>{tag} — " | |
| f"eff_rank {v['eff_rank']:.2f}, deviation {v['deviation']:.3f}" | |
| f"</div><ol>") | |
| for t in v["texts"]: | |
| ex_html += f"<li>{firsts(t)[:190]}</li>" | |
| ex_html += "</ol></div>" | |
| if step == "0": | |
| break # base identical for both arms; show once | |
| ex_html += "</div>" | |
| # one full story, E1 @ 300 | |
| fs_pid = pids[1] | |
| full_story = raw1["300"][fs_pid]["texts"][0][:2600] | |
| css = """ | |
| @page { size: A4; margin: 15mm 14mm; @bottom-center { content: counter(page); font-size:8pt; color:#888; } } | |
| body { font-family: -apple-system,'Helvetica Neue',Arial,sans-serif; font-size:9.2pt; color:#1a1a1a; line-height:1.45; } | |
| h1 { font-size:20pt; margin:0 0 2mm; color:#111; } | |
| h2 { font-size:13pt; margin:7mm 0 2mm; padding-bottom:1mm; border-bottom:2px solid #2980b9; color:#2980b9; page-break-after:avoid; } | |
| h3 { font-size:10.5pt; margin:4mm 0 1.5mm; color:#333; page-break-after:avoid; } | |
| .sub { color:#666; font-size:9pt; margin-bottom:4mm; } | |
| table { border-collapse:collapse; width:100%; font-size:7.8pt; margin:2mm 0 4mm; } | |
| th { background:#2980b9; color:#fff; text-align:left; padding:1.4mm 1.8mm; font-weight:600; } | |
| td { padding:1.2mm 1.8mm; border-bottom:1px solid #e4e4e4; } | |
| tr:nth-child(even) td { background:#f7f9fb; } | |
| td.hi { font-weight:700; color:#16a085; } | |
| .key { background:#eef6fb; border-left:4px solid #2980b9; padding:2.5mm 3mm; margin:3mm 0; } | |
| .warn { background:#fdf3e7; border-left:4px solid #e67e22; padding:2.5mm 3mm; margin:3mm 0; } | |
| .ex { page-break-inside:avoid; margin:0 0 5mm; border:1px solid #ddd; border-radius:2mm; padding:2.5mm 3mm; } | |
| .prm { font-size:8.4pt; color:#444; background:#f2f2f2; padding:1.5mm 2mm; border-radius:1mm; margin-bottom:2mm; } | |
| .blk { margin:1.5mm 0; } | |
| .hd { font-size:7.8pt; font-weight:700; color:#2980b9; } | |
| ol { margin:1mm 0 1mm 5mm; padding:0; } | |
| li { font-size:7.9pt; margin-bottom:0.6mm; color:#222; } | |
| .story { font-size:8.2pt; white-space:pre-wrap; background:#fafafa; padding:3mm; border-left:3px solid #16a085; } | |
| img { margin:2mm 0 4mm; } | |
| .miss { color:#c0392b; font-size:8pt; } | |
| code { background:#f0f0f0; padding:0.3mm 1mm; font-size:8pt; } | |
| """ | |
| html = f"""<html><head><meta charset="utf-8"><style>{css}</style></head><body> | |
| <h1>Diversity-Aware Post-Training for Creative Story Generation</h1> | |
| <div class="sub">Qwen3-4B-Instruct-2507 · LoRA r=32 α=64 · GRPO (TRL 1.10, GDPO aggregation) · single RTX 5090 32GB<br/> | |
| Interim report — E0 and E1 complete (300 steps each). E2/E3/E4 in progress. Generated {ts}.</div> | |
| <div class="key"><b>Headline.</b> A quality-gated pairwise-deviation reward (E1) moved semantic | |
| diversity <b>5–6× further</b> than quality-only GRPO (E0) over 300 matched steps, at a cost of | |
| 0.06 judge quality points. On held-out prompts, E1's effective-rank gain was <b>3.5×</b> E0's | |
| (+0.124 vs +0.035) while scoring <i>higher</i> quality (7.12 vs 7.03).</div> | |
| <h2>1. The baseline problem</h2> | |
| <p>The base model is <b>already collapsed before any RL</b>. Across a 16,000-story pool | |
| (1,000 prompts × 16 samples), effective rank is <b>2.006 out of a ceiling of 16</b> — sixteen | |
| stories for one prompt span roughly two effective semantic directions, at ~0.87 mean cosine | |
| similarity. This reframes the study: the question is not whether RL <i>causes</i> collapse, but | |
| whether any objective can <i>lift</i> diversity off a floor that pretraining already imposed.</p> | |
| {tbl(poolrows, ["metric", "value"])} | |
| {img(FIGS / "03_pool_4b_baseline.png")} | |
| <div class="warn"><b>The collapse is tonal, not lexical.</b> 92–95% of every story carries | |
| solemn/elegiac vocabulary; only ~15% carries comic vocabulary — even on explicitly comic prompts. | |
| Given “Cthulhu disappoints his constituency by failing to deliver the promised chaos” (a joke), | |
| the base model wrote six straight-faced atmospheric-horror pieces. Consequence: n-gram metrics | |
| (distinct-4, self-BLEU) are near-blind to this failure mode; only embedding-based measures see it.</div> | |
| <h2>2. Main result — E0 vs E1, 300 steps each</h2> | |
| <p>Identical data, seed, learning rate (3e-5), step count and LoRA config. 4,800 stories scored | |
| per arm. First quarter vs last quarter of each run.</p> | |
| {tbl(rows, ["metric", "E0 start", "E0 end", "E0 Δ", "E1 start", "E1 end", "E1 Δ", "E1/E0"], hi={"E1 Δ", "E1/E0"})} | |
| <div class="key"><b>Reading it.</b> Deviation +0.0200 vs +0.0037 (5.4×) and log-det +0.935 vs | |
| +0.165 (5.7×), for −0.06 judge quality. Note also <b>story length</b>: E0 gained +30.8 words — | |
| it discovered “write longer” as a cheap way to please the judge — while E1 gained +0.7. The | |
| diversity term removed that incentive, which also means E1's diversity gain cannot be a length | |
| artifact.</div> | |
| {img(FIGS / "E0_vs_E1_comparison.png")} | |
| <h2>3. Held-out generalization — checkpoint study</h2> | |
| <p>10 held-out prompts × 6 samples at T=0.9, fixed seed, generated from every checkpoint. | |
| 960 stories read across both arms.</p> | |
| {tbl(ckrows, ["arm", "step", "quality", "eff_rank (of 6)", "deviation", "logdet", "words"])} | |
| {img(FIGS / "E0-baseline_trajectory.png")} | |
| <h2>4. Training diagnostics</h2> | |
| {img(FIGS / "E0-baseline_diagnostics.png")} | |
| {img(FIGS / "E1-div-individual_diagnostics.png")} | |
| <h2>5. What the stories actually look like</h2> | |
| <p>Opening sentences of all 6 samples per prompt. Base model shown once (identical starting point | |
| for both arms), then each arm at step 300.</p> | |
| {ex_html} | |
| <h3>One complete story — E1 @ step 300</h3> | |
| <div class="story">{full_story}</div> | |
| <h2>6. Held-out evaluation — the definitive result</h2> | |
| <p>480 stories per model: 30 held-out prompts x 16 samples, T=0.9, top_p=0.95, identical seed. | |
| Judge health on this run: 496 calls, 1 failure (0.2%).</p> | |
| {tbl(evrows, ["model","quality","eff_rank (of 16)","pairwise","logdet","ends cleanly","words"], hi={"eff_rank (of 16)"})} | |
| <div class="key"><b>E1 wins on BOTH axes.</b> Against base: effective rank +0.190 vs E0's +0.066 | |
| (<b>2.9x</b>), log-det +2.380 vs +0.873 (<b>2.7x</b>), and judge quality +0.392 vs +0.244 | |
| (<b>1.6x</b>). This is not a diversity-for-quality trade — E1 is better at both.</div> | |
| {img(FIGS / "eval_frontier.png", "78%")} | |
| <h3>The methodological result: n-gram metrics are blind to this</h3> | |
| <p>The same 480 stories per model, scored two ways:</p> | |
| {tbl(blind, ["model","Δ eff_rank (embed)","Δ logdet (embed)","Δ distinct-4 (n-gram)","Δ self-BLEU (n-gram)","Δ quality"], hi={"Δ eff_rank (embed)"})} | |
| <div class="warn"><b>Embedding metrics separate the arms by 2.9x. N-gram metrics do not separate | |
| them at all</b> — distinct-4 actually rates E0 <i>higher</i> than E1, and self-BLEU is identical to | |
| three decimals. The collapse (and its repair) is tonal and structural, not lexical, so distinct-n | |
| and self-BLEU cannot see it. Evaluating creative diversity with n-gram metrics alone would have | |
| concluded these two models are the same.</div> | |
| {img(FIGS / "eval_metric_blindness.png")} | |
| <h2>7. Qualitative read — what actually changed in the writing</h2> | |
| <p>10 held-out prompts x 6 samples from every checkpoint of both arms, at two sampling settings. | |
| Stories read in full for three prompts; openings and premises scanned for all ten.</p> | |
| <div class="key"><b>The measurement that summarises the read.</b> Fraction of step-300 samples whose | |
| first 8 words verbatim-reuse one of the <i>base model's</i> openings for that prompt: | |
| <b>E0 31.7% (19/60) vs E1 11.7% (7/60)</b> — 2.7x less template reuse, tracking the 2.9x | |
| effective-rank separation almost exactly.</div> | |
| <h3>E0 keeps the base model's frame and polishes the inside</h3> | |
| <p>E0's step-300 openings are frequently near-verbatim to base:</p> | |
| {tbl(qrows, ["prompt","base","E0 @ 300"])} | |
| <p>The improvement is real but <i>internal</i>. E0's bodies are richer and better organised — one | |
| graduation sample develops an explicit “Year One / Year Two / Year Three” structure the base never | |
| attempts, with far more specific detail (“Jenna's red scarf”, “Jake's habit of drawing tiny suns on | |
| his H.W. papers”). That is exactly what a per-story quality judge rewards, and why E0's judge score | |
| rises +0.24 while its diversity does not move. <b>E0 is a better writer telling the same story.</b></p> | |
| <h3>E1 changes the entry point, the premise and the point of view</h3> | |
| <p><b>Graduation prompt.</b> Base and E0 open <i>at the podium</i>, in the ceremony, in every | |
| sample. Two of E1's three open in retrospection instead — no stage, no lights, no crowd:</p> | |
| <div class="story">I used to sit in the back of the room, not because I didn't want to hear, but because I didn't know how to fit in. | |
| I've never raised my hand in class. Not once.</div> | |
| <p><b>Martial-arts prompt.</b> Base and E0 write the student as a humble supplicant (“I… I just | |
| want to learn”; “No app on her wrist. No headset. Just folded hands”). E1 rewrites the | |
| relationship into a confrontation:</p> | |
| <div class="story">A girl stood there, twelve years old, wearing a hoodie that read *I Know Everything*. … "I downloaded your entire fighting system. Every kata, every push, every breath. I've trained for weeks. I'm ready."</div> | |
| <p>Another E1 sample relocates the scene from dojo to neon city street; a third inverts the premise | |
| entirely — the student says <i>“I didn't download anything. I just… felt it.”</i></p> | |
| <p><b>“The ash turned to snow.”</b> Base and E0 use one template in all six samples: a named lone | |
| adult, at a rural dwelling, remembering (Magda/cottage, Elena/clearing, Masahiro/temple; | |
| Marlow/garden, Eli/watchtower, Lyra/cottage). E1 breaks both scale and POV:</p> | |
| <div class="story">Children appeared where none had been. Not from the rubble, not from the forgotten alleyways — just there. | |
| The children didn't know the word *smoke*. They didn't need to.</div> | |
| <p><b>Black Friday prompt</b> — the clearest case. Base uses “The air in the X Mall…” or “The sky | |
| burned crimson…” in all four sampled openings; E0 preserves it (“The air in the Glendale Mall…”, | |
| “The air in the Orchard Mall…”). E1 uses none of it: <i>“No one remembers the date. The clocks | |
| stopped on a Tuesday.”</i> / <i>“The temperature dropped the second the lights went out.”</i></p> | |
| <h3>The ceiling: neither arm broke the tonal monoculture</h3> | |
| {tbl(formrows, ["form","base","E0 @300","E1 @300","E1-E0"], hi={"E1 @300"})} | |
| <div class="warn"><b>E1 invented second-person narration</b> — base uses it on 0/10 prompts, E1 on | |
| 5/10. Present tense doubled. <b>E0 loses forms</b> (1.70 → 1.50): quality-only training narrows the | |
| repertoire. But <b>solemn/elegiac is 1.00 in every condition</b>. Every story in this study, from | |
| every checkpoint of every arm, is written in the same melancholy literary register. E1 diversifies | |
| grammatical person, tense, scale, POV and premise — it does <i>not</i> diversify tone. | |
| <b>But that table undercounts E1.</b> Reading the Cthulhu stories in full (a prompt whose entire | |
| premise is a joke), the base model and E0 write it straight — E0's opening is verbatim base. E1 | |
| produces genuine absurdist invention the base never approaches: <i>“Cthulhu awoke not in the deep, | |
| sulfurous dark, but on a balcony in Manhattan… His tentacle reached out and touched the radio tower. | |
| It simply began playing Chopin's Nocturne in E-flat at full volume… Cthulhu sat on a park bench, | |
| observing a dog chase a red ball.”</i> and <i>“a concert in Helsinki where a hundred thousand people | |
| played accordions in perfect unison, each note tuned to a specific frequency of sea bass in the | |
| Barents Sea.”</i> The keyword-based register detector scored these as non-comic because the humour is | |
| <b>situational, not lexical</b>. The monoculture ceiling is real but softer than the table implies.</div> | |
| <h3>E1 answers the prompt's question; base and E0 describe around it</h3> | |
| <p>The NYC prompt asks <b>“Why?”</b> — it demands a mechanism. Base and E0 mostly supply atmospheric | |
| vignettes with no explanation (“No one knew why. No one asked.”). One E1 sample instead writes a | |
| dialogue-driven science-fiction scene that actually answers it — the only sample across all three | |
| conditions to supply a causal mechanism:</p> | |
| <div class="story">The FBI redirects a field agent to a teal apartment complex in Harlem. … "It's a loop. I've worn it since 2015. Every time someone in New York tried to do harm … the device would flash." … "No. I stopped the *intent*."</div> | |
| <p>Another E1 sample writes in the <b>present tense</b> and refuses the consoling ending — the | |
| violence returns at midnight (“A man in a grocery bag gets his arm slashed by a scrawny boy, | |
| screaming”), where base and E0 both resolve into calm.</p> | |
| <div class="key"><b>Conclusion of the read: E0 is a better writer telling the same story; E1 tells | |
| different stories.</b> That distinction is invisible to per-story quality scoring (both arms | |
| improve), invisible to n-gram metrics (distinct-4 rates E0 <i>higher</i>), and visible to | |
| embedding-based measures — the methodological argument of this project, arrived at independently by | |
| reading. What E1 has <i>not</i> achieved is tonal range: the next objective to target is register | |
| explicitly.</div> | |
| <h2>8. Honest limitations</h2> | |
| <div class="warn"> | |
| <p><b>E1 does not fix verbatim opening duplication.</b> At step 300, unique-opening rate is 0.883 | |
| for E1 vs 0.900 for E0 — marginally <i>worse</i> — and both arms have 1/10 prompts with ≥3 | |
| identical openings. What E1 gains is <b>register spread</b> (2.00→2.40 distinct forms, while E0 | |
| falls 2.00→1.40). The diversity reward broadens <i>what kind of thing</i> the model writes without | |
| fixing <i>how it starts sentences</i>.</p> | |
| <p><b>Whole-story embeddings can miss positional collapse.</b> In E0 one prompt went from 6 | |
| distinct openings to 5-of-6 identical while effective rank and deviation both drifted <i>up</i>. | |
| Unique-opening rate should be a first-class metric, not a diagnostic afterthought.</p> | |
| <p><b>Effect sizes are modest in absolute terms</b> — E1's held-out effective rank is 1.80 against | |
| a ceiling of 6. The floor was lifted, not escaped.</p> | |
| <p><b>The effect needed ~150 steps to emerge from noise.</b> At batch 88 E1 was statistically | |
| indistinguishable from E0. A 100-step study would have concluded diversity rewards do not work.</p> | |
| <p><b>Entropy did not separate the arms.</b> Both fell (E0 −2.1%, E1 −3.1%). An earlier mid-run | |
| window suggested E1's entropy was rising; that did not survive the full run. Token entropy and | |
| semantic diversity are dissociated — which is the point, but not in the direction first reported.</p> | |
| </div> | |
| <h2>9. Recommendations</h2> | |
| <ol> | |
| <li><b>Set β (KL) to 0.</b> Measured at 0.4% of loss magnitude — already near-inert. The | |
| principled argument is stronger: the reference model <i>is</i> the collapsed distribution | |
| (eff. rank 2.0/16), so KL regularizes <i>toward</i> the pathology under study. Programmatic gates | |
| do KL's usual job without that conflict.</li> | |
| <li><b>Raise α from 0.5 to 1.0–2.0.</b> Quality and diversity are nearly independent across | |
| prompts (r = −0.108), so there is slack to spend, and E1 paid almost nothing for its gain.</li> | |
| <li><b>Add unique-opening-rate to the reward</b>, not just to eval — it catches what log-det misses.</li> | |
| <li><b>Train longer.</b> Both arms were still moving at 300 steps.</li> | |
| <li><b>Learning rate matters more than anything else here.</b> At the brief's 3e-6 (a full-FT | |
| rate applied to LoRA adapters) the policy was frozen: KL pinned at 0.0008 for 171 steps, every | |
| metric inside its noise band. 3e-5 was required to make <i>any</i> arm measurable.</li> | |
| </ol> | |
| </body></html>""" | |
| out_html = ROOT / "report.html" | |
| out_html.write_text(html) | |
| from weasyprint import HTML | |
| pdf = ROOT / "REPORT.pdf" | |
| HTML(string=html, base_url=str(ROOT)).write_pdf(str(pdf)) | |
| print("wrote", pdf, f"({pdf.stat().st_size/1e6:.1f} MB)") | |
| print("wrote", out_html) | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |