Pranav2748 commited on
Commit
89eb5e8
Β·
verified Β·
1 Parent(s): 5acd9d3

Add model card

Browse files
Files changed (1) hide show
  1. README.md +276 -0
README.md ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ base_model: Qwen/Qwen3-4B-Instruct-2507
4
+ library_name: peft
5
+ pipeline_tag: text-generation
6
+ tags:
7
+ - lora
8
+ - peft
9
+ - trl
10
+ - grpo
11
+ - gdpo
12
+ - dpo
13
+ - divpo
14
+ - rlhf
15
+ - diversity
16
+ - creative-writing
17
+ - mode-collapse
18
+ ---
19
+
20
+ # Diversity-aware post-training for creative story generation (Qwen3-4B)
21
+
22
+ Research artifacts for a controlled study of **whether RL post-training can raise the
23
+ *semantic* diversity of short-story generation without paying for it in quality**.
24
+
25
+ Everything here is measured, not asserted: the repo carries the LoRA adapters, the
26
+ scored generation pool they were selected from, the per-step reward telemetry, the
27
+ judge-calibration evidence, the experiment reports, and the source that produced all
28
+ of it.
29
+
30
+ > ⚠️ **Work in progress.** Only **E0** (quality-only baseline) has finished its full
31
+ > 300-step run. **E1** was still training when this snapshot was uploaded β€” its
32
+ > checkpoints stop at **step 200 of 300**. **E2, E3 and E4 have not been trained yet**;
33
+ > their configs are present, their adapters are not. No cross-arm comparison exists yet,
34
+ > and none should be read into this repo.
35
+
36
+ ---
37
+
38
+ ## The finding this study is built around
39
+
40
+ Sampling the **base** policy 16 times per prompt over 1,000 prompts (16,000 scored
41
+ stories, `outputs/pool_4b/`) gives a mean **effective rank of 2.006 out of a ceiling of
42
+ 16**. Sixteen independent samples of the same prompt span roughly two effective
43
+ directions in embedding space; mean pairwise distance is 0.132, i.e. same-prompt stories
44
+ sit at about 0.87 cosine similarity.
45
+
46
+ Quality-only RL does not fix this. E0 ran 300 steps of GRPO on the judge's quality score
47
+ alone and moved judge quality **+0.30** (6.557 β†’ 6.860, first vs. last quarter of the run)
48
+ while the diversity statistics stayed flat (mean pairwise deviation 0.1354 β†’ 0.1391;
49
+ mean group log-det βˆ’12.81 β†’ βˆ’12.64). The held-out checkpoint study
50
+ (`outputs/ckpt_study/`, 10 prompts Γ— 6 samples per checkpoint) tells the same story:
51
+
52
+ | step | judge quality | effective rank | deviation |
53
+ |---|---|---|---|
54
+ | 0 (base) | 6.65 | 1.677 | 0.130 |
55
+ | 100 | 6.78 | 1.655 | 0.128 |
56
+ | 200 | 6.75 | 1.700 | 0.135 |
57
+ | 300 | 7.03 | 1.712 | 0.139 |
58
+
59
+ Craft improves; the model still writes the same story six times. That gap is what the
60
+ diversity arms exist to close.
61
+
62
+ ---
63
+
64
+ ## Method
65
+
66
+ **Base policy.** `Qwen/Qwen3-4B-Instruct-2507`, LoRA **r=32, alpha=64, dropout 0.0** on all
67
+ attention and MLP projections (`q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj`).
68
+
69
+ **Online arms.** GRPO via **TRL 1.10** with `multi_objective_aggregation="normalize_then_sum"`,
70
+ which is a line-for-line implementation of **GDPO** (arXiv 2601.05242): each reward channel is
71
+ normalized *within its prompt group* before the channels are summed and the advantage is
72
+ normalized batch-wise. Generation is **vLLM colocated** in the training process. G=8 samples per
73
+ prompt, generation batch 16 (2 prompts Γ— 8), `max_completion_length=1024`, lr 3e-5 with
74
+ `constant_with_warmup`, beta (KL) 0.02, 300 steps. Everything ran on a **single RTX 5090 (32 GB)** β€”
75
+ the 32 GB budget is what forced the 4B policy, since colocated GRPO needs two resident copies of
76
+ the weights.
77
+
78
+ **Offline arms.** DPO over preference pairs mined from the same 16k pool
79
+ (beta 0.1, sigmoid loss, lr 5e-6, epochs chosen for optimizer-step parity across arms).
80
+
81
+ **Reward channels.**
82
+
83
+ - `quality` β€” judge score, per story.
84
+ - `deviation` `d_i` β€” mean embedding distance from a sample to the rest of its group
85
+ (`BAAI/bge-base-en-v1.5`), credited only if judge quality β‰₯ tau=5.
86
+ - `marginal` `m_i` β€” the sample's marginal contribution to the group's log-determinant
87
+ (a set-level diversity volume), z-scored within group.
88
+
89
+ Every diversity credit is **per-sample by construction**. A single set-level scalar shared by a
90
+ whole group has zero within-group variance and therefore contributes exactly nothing to a GRPO
91
+ advantage β€” the failure mode diagnosed in `prior_run/` and written up in
92
+ `logs/experiments/00_prior_run_autopsy.md`.
93
+
94
+ **Judge.** `deepseek-v4-flash-0731` via OpenRouter, scoring **one story at a time on an absolute
95
+ rubric** (not batched relative scoring), with reasoning disabled. Calibrated before any training
96
+ against four cells with known ordering: detects incoherence (Ξ” 2.25), detects repetition (Ξ” 4.67),
97
+ not saturated (sd 0.76). See `logs/experiments/01_judge_calibration.md`.
98
+
99
+ **Programmatic gates.** Completeness, 150–600 word window, 4-gram / line loop detection and
100
+ `finish_reason == "length"` are checked deterministically before a story is ever judged, so
101
+ truncated or degenerate text cannot earn reward through any channel.
102
+
103
+ ### The arms
104
+
105
+ | arm | type | reward / objective | status in this repo |
106
+ |---|---|---|---|
107
+ | **E0-baseline** | GRPO | quality only β€” the collapse control | βœ… complete, 300 steps + `final` |
108
+ | **E1-div-individual** | GRPO | quality + pairwise deviation `d_i` (alpha 0.5, gated at tau=5) | 🟑 partial, steps 50–200 of 300 |
109
+ | **E2-div-group** | GRPO | quality + deviation + log-det marginal `m_i` (alpha 0.5, gamma 0.5) | ⬜ config only, not trained |
110
+ | **E3-multipos** | DPO | multi-positive, deviation-weighted loss; 4 greedy-diverse chosens vs. rotating negatives (3,593 rows) | ⬜ config + pairs only |
111
+ | **E4a-divpo-emb** | DPO | faithful DivPO, embedding-deviation criterion (956 rows, rho=7) | ⬜ config + pairs only |
112
+ | **E4b-divpo-prob** | DPO | faithful DivPO, lowest length-normalized logprob among quality β‰₯ rho | ⬜ config + pairs only |
113
+
114
+ ---
115
+
116
+ ## Repository layout
117
+
118
+ ```
119
+ outputs/
120
+ E0-baseline/ LoRA adapters: checkpoint-{50..300} + final
121
+ reward_history.json per-step reward telemetry (300 steps)
122
+ trl_log_history.json full TRL log history
123
+ judge_cost.json judge call/token/USD accounting
124
+ E1-div-individual/ same structure, checkpoints 50-200 (run in progress)
125
+ pool_4b/ pool_train.jsonl 16,000 scored base-policy stories
126
+ emb_train.npy their bge-base-en-v1.5 embeddings
127
+ summary_train.json pool-level summary stats
128
+ pairs_4b/ divpo_emb_train.jsonl, divpo_prob_train.jsonl,
129
+ multipos_train.jsonl, pair_stats_train.json
130
+ ckpt_study/E0-baseline/ stories.md, raw.json, metrics.csv (per-checkpoint story dumps)
131
+ logs/
132
+ experiments/ the written reports (start here)
133
+ figures/ plots referenced by the reports
134
+ configs/ one YAML per arm, with the reasoning for every knob in comments
135
+ src/ all Python: training, rewards, diversity, judge, gates, eval, tests
136
+ data/ train/eval prompt splits + split metadata
137
+ prior_run/ the previous, failed run's artifacts (see autopsy)
138
+ ```
139
+
140
+ **Read the reports in this order:** `00_prior_run_autopsy.md` β†’ `01_judge_calibration.md` β†’
141
+ `02_setup_and_deviations.md` β†’ `03_pool_4b_baseline.md` β†’ `04_E0_checkpoint_story_study.md` β†’
142
+ `E0-baseline.md`.
143
+
144
+ ### Data schemas
145
+
146
+ `outputs/pool_4b/pool_train.jsonl` β€” one story per line:
147
+ `prompt_id, prompt, idx, text, n_words, n_tokens, finish_reason, gate_passed, gate_reasons,
148
+ ends_cleanly, quality, novelty, deviation, marginal, group_logdet, mean_logprob, cumlogprob`.
149
+ Row *i* of `emb_train.npy` corresponds to line *i* of the JSONL.
150
+
151
+ `outputs/pairs_4b/*.jsonl` β€” DPO rows: `prompt_id, prompt, chosen, rejected` (+ per-row weight
152
+ for the multi-positive set).
153
+
154
+ `data/` β€” 1,000 train and 50 eval prompts (seed 42), filtered to 10–60 words from 272,600 scanned;
155
+ `split_meta.json` also carries the exact system prompt used for every generation in the study.
156
+
157
+ ---
158
+
159
+ ## Using an adapter
160
+
161
+ Each checkpoint directory is a standard PEFT adapter (`adapter_model.safetensors` +
162
+ `adapter_config.json`, ~253 MB each).
163
+
164
+ ```python
165
+ from huggingface_hub import snapshot_download
166
+ from transformers import AutoModelForCausalLM, AutoTokenizer
167
+ from peft import PeftModel
168
+
169
+ path = snapshot_download(
170
+ "Mercity/creative-writing-llm",
171
+ allow_patterns="outputs/E0-baseline/final/*",
172
+ )
173
+
174
+ tok = AutoTokenizer.from_pretrained("Qwen/Qwen3-4B-Instruct-2507")
175
+ base = AutoModelForCausalLM.from_pretrained(
176
+ "Qwen/Qwen3-4B-Instruct-2507", dtype="auto", device_map="auto"
177
+ )
178
+ model = PeftModel.from_pretrained(base, f"{path}/outputs/E0-baseline/final")
179
+
180
+ system = (
181
+ "You are a fiction writer. Write a complete short story of 200-500 words responding "
182
+ "to the writing prompt.\nWrite only the story: no title, no preamble, no commentary, "
183
+ "no author's note.\nFinish inside the word budget. The story must reach a real ending, "
184
+ "not stop mid-scene."
185
+ )
186
+ msgs = [{"role": "system", "content": system},
187
+ {"role": "user", "content": "A lighthouse keeper receives a letter addressed to the sea."}]
188
+ ids = tok.apply_chat_template(msgs, add_generation_prompt=True, return_tensors="pt").to(model.device)
189
+ out = model.generate(ids, max_new_tokens=1024, temperature=1.0, top_p=1.0, do_sample=True)
190
+ print(tok.decode(out[0][ids.shape[-1]:], skip_special_tokens=True))
191
+ ```
192
+
193
+ Swap `outputs/E0-baseline/final` for `outputs/E0-baseline/checkpoint-150`,
194
+ `outputs/E1-div-individual/checkpoint-200`, etc. The study sampled at T=1.0 during training and
195
+ T=0.9 / top_p=0.95 for the checkpoint story study.
196
+
197
+ ---
198
+
199
+ ## Selected numbers
200
+
201
+ **Base-policy pool** (`logs/experiments/03_pool_4b_baseline.md`, n=16,000):
202
+
203
+ | metric | value |
204
+ |---|---|
205
+ | gate pass rate | 0.992 |
206
+ | judge quality (mean Β± sd) | 6.447 Β± 0.833 |
207
+ | mean pairwise deviation | 0.1321 |
208
+ | mean group log-det | βˆ’30.42 |
209
+ | **mean effective rank (ceiling 16)** | **2.006 Β± 0.323** |
210
+ | corr(quality, effective rank) | βˆ’0.108 |
211
+ | corr(deviation, effective rank) | 0.992 |
212
+
213
+ Quality and diversity are **largely independent** across prompts (r = βˆ’0.108), so a method that
214
+ raises diversity without lowering quality is exploiting existing slack rather than defying a
215
+ tradeoff.
216
+
217
+ **E0 run** (`logs/experiments/E0-baseline.md`, 300 steps, 4,800 stories judged, verdict *healthy*):
218
+
219
+ | metric | early (first 25%) | late (last 25%) | Ξ” |
220
+ |---|---|---|---|
221
+ | judge quality (passing) | 6.557 | 6.860 | **+0.303** |
222
+ | mean deviation | 0.1354 | 0.1391 | +0.004 |
223
+ | mean log-det | βˆ’12.81 | βˆ’12.64 | +0.165 |
224
+ | gate pass | 0.9925 | 0.9875 | βˆ’0.005 |
225
+ | policy entropy | 1.2535 | 1.2274 | βˆ’0.026 (βˆ’2.1%) |
226
+
227
+ No entropy collapse, and the reward-hacking trip conditions (diversity up while quality or
228
+ validity falls) were not tripped. Judge cost for the whole E0 run: **$0.84** over 5,109 calls.
229
+
230
+ **Qualitative** (`logs/experiments/04_E0_checkpoint_story_study.md`): the collapse is a *tonal
231
+ monoculture*, not a lexical one β€” 92–95% of stories carry solemn/elegiac vocabulary against ~15%
232
+ comic, even on explicitly comic prompts, and verbatim opening duplication *rises* with
233
+ quality-only training. This predicts that n-gram metrics (distinct-4, self-BLEU) will separate the
234
+ arms far less than effective rank does.
235
+
236
+ ---
237
+
238
+ ## Environment
239
+
240
+ torch 2.13.0+cu130 Β· vLLM 0.27.1 Β· transformers 5.15.0 Β· TRL 1.10.0 Β· PEFT 0.20.0 Β·
241
+ sentence-transformers 5.7.0 (`BAAI/bge-base-en-v1.5`) Β· single RTX 5090, 32.6 GB, sm_120.
242
+ FlashInfer is disabled (it misdetects sm_120); Liger is disabled because it suppresses TRL's
243
+ `entropy` metric.
244
+
245
+ ## Caveats and honest notes
246
+
247
+ - **E1 is mid-flight.** Its checkpoints are a snapshot at step 200/300 and its reports do not
248
+ exist yet. E2/E3/E4 are unrun. Do not read arm comparisons out of this repo.
249
+ - **`04_E0_checkpoint_story_study.md` documents an earlier, partial 163-step E0 run** (at the
250
+ pre-correction lr 3e-6). `outputs/ckpt_study/E0-baseline/metrics.csv` is the *regenerated*
251
+ version covering all checkpoints of the final 300-step run; where they disagree, trust the CSV.
252
+ - **Learning rate deviates from the plan** (3e-5, not 3e-6): at 3e-6 the adapter was effectively
253
+ frozen β€” KL pinned near 8e-4 for 171 steps with every metric inside its noise band. The evidence
254
+ is recorded in the config comments and `02_setup_and_deviations.md`.
255
+ - **Optimizer/scheduler/RNG state is not included** for E1's checkpoints. It is resume-only state,
256
+ is rotated away by `save_total_limit`, and none of it is needed to load or evaluate an adapter.
257
+ - **`prior_run/train.py` is a redacted copy** β€” the original contained a hard-coded OpenRouter API
258
+ key, replaced here with a placeholder. Nothing else in the file was changed.
259
+ - **Judge is not perfect.** It fails the truncation-sensitivity check (Aβˆ’B = 1.33 against a 2.0
260
+ bar), which is tolerated only because truncation is caught deterministically by the gates before
261
+ any story reaches the judge. Pool-scoring judge failure rate was 0.45%.
262
+ - Every deviation from the original plan is enumerated in `logs/experiments/02_setup_and_deviations.md`
263
+ rather than discovered in a footnote.
264
+
265
+ ## References
266
+
267
+ - GDPO β€” group-wise reward normalization for multi-objective RL, arXiv 2601.05242 (Liu et al., NVIDIA)
268
+ - DivPO β€” Lanchantin et al., 2025 (diverse preference optimization)
269
+ - Effective rank β€” Roy & Vetterli, the exp-entropy of the Gram spectrum, used here as the primary
270
+ continuous mode-count metric (k-means + silhouette was measured to be unable to separate collapse
271
+ from spread and was demoted)
272
+
273
+ ## License
274
+
275
+ Apache-2.0, matching the `Qwen/Qwen3-4B-Instruct-2507` base model. The prompt splits derive from a
276
+ public writing-prompts corpus; stories in `outputs/pool_4b/` are model generations.