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: 23,385 Bytes
471fe4d | 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 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 | """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())
|