288 lines
11 KiB
Markdown
288 lines
11 KiB
Markdown
|
|
---
|
|||
|
|
license: agpl-3.0
|
|||
|
|
base_model: Qwen/Qwen3-4B-Base
|
|||
|
|
pipeline_tag: text-generation
|
|||
|
|
language:
|
|||
|
|
- en
|
|||
|
|
tags:
|
|||
|
|
- prose
|
|||
|
|
- rewriting
|
|||
|
|
- style-transfer
|
|||
|
|
- creative-writing
|
|||
|
|
- deslop
|
|||
|
|
- qwen3
|
|||
|
|
library_name: transformers
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# prose-rewriter-4b-v2.2
|
|||
|
|
|
|||
|
|
A paragraph-level **prose rewriter**: it takes prose written by a large model and
|
|||
|
|
re-renders it to be more human, preserving the semantics it was given.
|
|||
|
|
|
|||
|
|
`Qwen/Qwen3-4B-Base` with a rank-32 LoRA merged in at strength 1.20.
|
|||
|
|
|
|||
|
|
**A third less repetition.** Every training target that repeats itself -- the
|
|||
|
|
same sentence twice, a phrase said again, three sentences opening on the same
|
|||
|
|
words, `happy, happy` -- is now rejected, so the pool only ever shows the model
|
|||
|
|
the repetition going away. Rewrites that repeat themselves fall from 13.5% to
|
|||
|
|
8.7%.
|
|||
|
|
|
|||
|
|
**Less unsupported content, less slop vocabulary**, at the same strength as
|
|||
|
|
v2.1. See [Evaluation](#evaluation).
|
|||
|
|
|
|||
|
|
## Variants
|
|||
|
|
|
|||
|
|
| Path | Format | Use with |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `/` | safetensors bf16, `qwen3` arch | transformers |
|
|||
|
|
| `GGUF/prose-rewriter-4b-v2.2-Q8_0.gguf` | GGUF Q8_0, 4.69 GB | llama.cpp / llama-cpp-python |
|
|||
|
|
| `GGUF/prose-rewriter-4b-v2.2-Q4_K_M.gguf` | GGUF Q4_K_M, 2.72 GB | llama.cpp / llama-cpp-python |
|
|||
|
|
|
|||
|
|
The quants carry the chat template and stop on `<|im_end|>`, and the adapted
|
|||
|
|
output head is kept separate from the token embeddings, stored at Q8_0.
|
|||
|
|
|
|||
|
|
## Prompt format
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
<|im_start|>source
|
|||
|
|
{paragraph}<|im_end|>
|
|||
|
|
<|im_start|>rewrite
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
The chat template in this repo builds exactly that string, byte for byte, from
|
|||
|
|
one message:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
messages = [{"role": "source", "content": paragraph}]
|
|||
|
|
tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
It is not a chat model. Every message is rendered as the source paragraph, so a
|
|||
|
|
runtime that probes the template with a `user` message still gets a valid
|
|||
|
|
prompt. Send one message per call.
|
|||
|
|
|
|||
|
|
## Serving recipe
|
|||
|
|
|
|||
|
|
Sampled at `temperature=0.9, top_p=0.9`. However, temperature 0.9 hallucinates harder
|
|||
|
|
because this release edits harder than v2. It's recommended you use 0.8-0.85 instead.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
import torch
|
|||
|
|
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|||
|
|
|
|||
|
|
repo = "chartreuse-verte/prose-rewriter-4b-v2.2"
|
|||
|
|
tok = AutoTokenizer.from_pretrained(repo)
|
|||
|
|
model = AutoModelForCausalLM.from_pretrained(repo, dtype=torch.bfloat16, device_map="cuda").eval()
|
|||
|
|
|
|||
|
|
def rewrite(paragraph):
|
|||
|
|
text = tok.apply_chat_template(
|
|||
|
|
[{"role": "source", "content": paragraph}],
|
|||
|
|
tokenize=False, add_generation_prompt=True,
|
|||
|
|
)
|
|||
|
|
ids = tok(text, return_tensors="pt", add_special_tokens=False).input_ids.to(model.device)
|
|||
|
|
out = model.generate(ids, max_new_tokens=512, do_sample=True, temperature=0.9, top_p=0.9)
|
|||
|
|
return tok.decode(out[0, ids.shape[1]:], skip_special_tokens=True).strip()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Same thing under llama.cpp. The role is `source`, which no chat API models, so
|
|||
|
|
build the string yourself; `<|im_end|>` stops it:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
llama-cli -m GGUF/prose-rewriter-4b-v2.2-Q8_0.gguf -no-cnv -n 512 --temp 0.9 --top-p 0.9 \
|
|||
|
|
-p '<|im_start|>source
|
|||
|
|
{paragraph}<|im_end|>
|
|||
|
|
<|im_start|>rewrite
|
|||
|
|
'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Input length
|
|||
|
|
|
|||
|
|
The training pool's median input is 37 words and 80% of it is under 70, so serve
|
|||
|
|
it on anything from a full sentence up.
|
|||
|
|
|
|||
|
|
The practical floor is about **15 words**. Below it the failure mode is padding,
|
|||
|
|
cutting and fabrication rather than gibberish. Below 80 bytes, pass the text
|
|||
|
|
through unchanged.
|
|||
|
|
|
|||
|
|
## Evaluation
|
|||
|
|
|
|||
|
|
Both releases measured **as they ship**, baked at strength 1.20, on 365 held-out
|
|||
|
|
paragraphs of LLM-written prose that neither model saw in training, at
|
|||
|
|
`temperature=0.9, top_p=0.95`, three swipes each. 1,095 generations an arm.
|
|||
|
|
|
|||
|
|
Pronoun invention numbers for this release, on 400 single-gender roleplay
|
|||
|
|
paragraphs:
|
|||
|
|
|
|||
|
|
| | v2.1 | v2.2 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| a gendered pronoun the input never licensed | 0.8% | 0.8% |
|
|||
|
|
| ... belonging to the other gender | 0.8% | 0.8% |
|
|||
|
|
| a proper noun the input does not have | 0.3% | 0.3% |
|
|||
|
|
|
|||
|
|
Paired over the 400 inputs, none of these separates.
|
|||
|
|
|
|||
|
|
Repetition on the 365 paragraphs -- a rewrite that repeats a sentence, a phrase,
|
|||
|
|
or a sentence opening:
|
|||
|
|
|
|||
|
|
| | input | v2.1 | v2.2 |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| any repetition | 2.2% | 13.5% | **8.7%** |
|
|||
|
|
| ... a whole sentence | | 0.4% | 0.3% |
|
|||
|
|
| ... a phrase | | 7.3% | **4.6%** |
|
|||
|
|
| ... a sentence opening (anaphora) | | 4.5% | **2.6%** |
|
|||
|
|
|
|||
|
|
Paired over the 365 inputs, any repetition falls (t = -3.6), repeated phrases
|
|||
|
|
fall (t = -2.8) and anaphora falls (t = -2.5).
|
|||
|
|
|
|||
|
|
Structural, on the same paragraphs:
|
|||
|
|
|
|||
|
|
| | v2.1 | v2.2 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| words changed | 44.0% | 43.7% |
|
|||
|
|
| passed through unchanged | 3.1% | 2.5% |
|
|||
|
|
| near-verbatim outputs | 1.4% | 0.9% |
|
|||
|
|
| below the training edit floor | 35.4% | 35.9% |
|
|||
|
|
| sentence count moved | 75.5% | 79.2% |
|
|||
|
|
| sentence-length variety vs input | +0.115 | +0.116 |
|
|||
|
|
| words kept from the input | 0.616 | 0.620 |
|
|||
|
|
| length preserved | 0.851 | 0.857 |
|
|||
|
|
| truncated below 0.75x | 23.7% | 21.4% |
|
|||
|
|
| repeated 3-grams | 0.005 | 0.004 |
|
|||
|
|
|
|||
|
|
Paired over the 365 inputs, only sentence count moved separates (t = +2.2). Over
|
|||
|
|
the 303 inputs of 51-90 words, no column does.
|
|||
|
|
|
|||
|
|
Unsupported content -- a sentence in the rewrite the input does not entail,
|
|||
|
|
scored by NLI with the input alone as the premise:
|
|||
|
|
|
|||
|
|
| | v2.1 | v2.2 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| unsupported sentences, mean per rewrite | 12.7% | **11.1%** |
|
|||
|
|
| unsupported sentences, all rewrites | 11.5% | **10.2%** |
|
|||
|
|
| coverage of the input (reverse entailment) | 0.487 | 0.484 |
|
|||
|
|
|
|||
|
|
Paired over the 365 inputs, the unsupported rate falls (t = -2.3); coverage
|
|||
|
|
does not move.
|
|||
|
|
|
|||
|
|
The register numbers on the same paragraphs, against the input:
|
|||
|
|
|
|||
|
|
| | input | v2.1 | v2.2 | human corpus |
|
|||
|
|
|---|---|---|---|---|
|
|||
|
|
| banned constructions /1k | 7.52 | 2.51 | 2.50 | 0.00 |
|
|||
|
|
| slop lexicon density | 0.081 | 0.048 | **0.044** | 0.019 |
|
|||
|
|
| purple score | 0.498 | 0.314 | 0.316 | -- |
|
|||
|
|
|
|||
|
|
Paired over all 365 inputs, lexicon density falls (t = -2.6). Banned
|
|||
|
|
constructions and purple do not separate.
|
|||
|
|
|
|||
|
|
Dialogue: the number of spoken lines changes on 1.6% of rewrites against v2.1's
|
|||
|
|
0.8% (t = +1.7, does not separate), and of the rewrites whose input carried
|
|||
|
|
quoted speech, 1.0% lost the quotes entirely -- the same for both.
|
|||
|
|
|
|||
|
|
Markup round-trip on 365 tagged paragraphs -- every marker handed back in order,
|
|||
|
|
with the same counts: 79.5% for v2.1, 78.1% here.
|
|||
|
|
|
|||
|
|
### A note on strength
|
|||
|
|
|
|||
|
|
Every number above is measured at the strength the release is baked at, which is
|
|||
|
|
not the same as measuring the adapter. LoRA strength is spent at merge time
|
|||
|
|
(`W + (B @ A) * 2.40` here), and the metrics move with it: read at the adapter's
|
|||
|
|
natural 1.0 this same checkpoint is a materially different artifact.
|
|||
|
|
|
|||
|
|
Both releases ship at 1.20, so every difference above is the training pool and
|
|||
|
|
not the strength. 1.20 is where banned constructions bottomed out on the
|
|||
|
|
strength sweep: 1.15 carries a third more of them.
|
|||
|
|
|
|||
|
|
## Training
|
|||
|
|
|
|||
|
|
**Corrupt forward, train backward.** The human paragraph is the target; an
|
|||
|
|
on-policy LLM manufactures the input by slop-ifying it.
|
|||
|
|
|
|||
|
|
The target side is human prose: published contemporary literary fiction,
|
|||
|
|
r/WritingPrompts
|
|||
|
|
([`Mollymo/Human-to-AI-writing`](https://huggingface.co/datasets/Mollymo/Human-to-AI-writing)),
|
|||
|
|
AO3
|
|||
|
|
([`midwestern-simulation-active/ao3_random_subset`](https://huggingface.co/datasets/midwestern-simulation-active/ao3_random_subset)),
|
|||
|
|
a scrape of `bluemoonroleplaying.com` -- the only human writing in the pool
|
|||
|
|
already in the deployment's own register -- and a sliver of fanfiction.net
|
|||
|
|
([`atom-in-the-universe/fanfics-10k-10k`](https://huggingface.co/datasets/atom-in-the-universe/fanfics-10k-10k)).
|
|||
|
|
|
|||
|
|
The input side is manufactured from those targets, weighted and share-capped so
|
|||
|
|
that no single generator's tics dominate:
|
|||
|
|
|
|||
|
|
| pool axis | composition |
|
|||
|
|
|---|---|
|
|||
|
|
| rows | 15,290 over 10,982 distinct target paragraphs |
|
|||
|
|
| corruption band | medium 35%, heavy 31%, light 30%, identity/no-op 3%, curated real slop 1% |
|
|||
|
|
| `len_mode` | match only |
|
|||
|
|
| markup | 15% of rows, identical on both sides |
|
|||
|
|
| kind | prose 91%, dialogue 8%, structural no-ops 1% |
|
|||
|
|
| target source | published fiction 39%, r/WritingPrompts 29%, AO3 20%, roleplay forum 8%, other 4% |
|
|||
|
|
|
|||
|
|
Pairs pass invariant gates before they reach the GPU: POV, tense, who is in the
|
|||
|
|
scene, the gender of who is in the scene, the number of spoken lines,
|
|||
|
|
grammatical correctness on the target side, no repetition on the target side,
|
|||
|
|
content recall stratified by target length, and NLI entailment both ways. Two further screens shape what reaches
|
|||
|
|
training -- a floor on how much a pair actually changes, and a floor on the
|
|||
|
|
sentence-length variety of the target, applied at a higher threshold for long
|
|||
|
|
paragraphs than short ones.
|
|||
|
|
|
|||
|
|
**Loss on the target paragraph only.** Everything before `rewrite` is masked.
|
|||
|
|
|
|||
|
|
| | |
|
|||
|
|
|---|---|
|
|||
|
|
| LoRA | r=32, alpha=64, dropout 0.05 |
|
|||
|
|
| target modules | q, k, v, o, gate, up, down, **and `lm_head`** |
|
|||
|
|
| trainable | 71,004,160 params (1.73%) |
|
|||
|
|
| schedule | 2 epochs, lr 1e-4 cosine, batch 8 × accum 4, seq 2048 |
|
|||
|
|
| steps | 920 on one RTX 3090 |
|
|||
|
|
|
|||
|
|
## The merge
|
|||
|
|
|
|||
|
|
Merged at strength 1.20. Rank 32 with alpha 64 is a LoRA scaling of 2.0, so the
|
|||
|
|
effective scaling is **2.40**: `W + (B @ A) * 2.40`. Merged in float32, stored
|
|||
|
|
bfloat16.
|
|||
|
|
|
|||
|
|
`lm_head` is adapted, and `Qwen3-4B-Base` ties `lm_head.weight` to
|
|||
|
|
`embed_tokens.weight`. **This checkpoint is untied**: the merged output head is
|
|||
|
|
stored separately and the input embeddings are bit-identical to the base
|
|||
|
|
model's, which is what training assumed. `config.json` says
|
|||
|
|
`tie_word_embeddings: false` and it means it. Do not re-tie it, and if you
|
|||
|
|
convert to another format, check that the head survived.
|
|||
|
|
|
|||
|
|
## Limitations
|
|||
|
|
|
|||
|
|
- **Not an instruct model.** It has one job and one prompt. There is nothing to
|
|||
|
|
ask it.
|
|||
|
|
- **Works on fictional prose only.** May not work on technical documentation.
|
|||
|
|
- **One paragraph per call.** Longer input degrades; split it.
|
|||
|
|
- **Still invents people occasionally.** A phantom pronoun appears in 0.8% of
|
|||
|
|
rewrites. Check the output when the cast matters.
|
|||
|
|
- **Truncates.** It drops below 0.75x the input's length on 21.4% of paragraphs.
|
|||
|
|
Usually that is the slop going; check the output when length matters.
|
|||
|
|
- **Still repeats itself.** 8.7% of rewrites repeat a sentence, a phrase or a
|
|||
|
|
sentence opening the input did not. Check the output for loops.
|
|||
|
|
- **Will not pass AI detectors.** Pangram and such will still know because this
|
|||
|
|
model preserves word choices and certain sentence structures.
|
|||
|
|
- **English only**, narrative register (third and first person fiction, dialogue
|
|||
|
|
with quoted speech).
|
|||
|
|
- **Short input pads, cuts and invents.** The floor is about 15 words. See
|
|||
|
|
[Input length](#input-length).
|
|||
|
|
- **Repeats a noun sooner than an LLM would.** Human prose reuses a plain noun
|
|||
|
|
where generated prose uses a synonym, and this model has learned that habit.
|
|||
|
|
|
|||
|
|
## License
|
|||
|
|
|
|||
|
|
The weights in this repository are released under the **GNU Affero General
|
|||
|
|
Public License, version 3**. The full text is in `LICENSE`.
|
|||
|
|
|
|||
|
|
This is a derivative of [`Qwen/Qwen3-4B-Base`](https://huggingface.co/Qwen/Qwen3-4B-Base),
|
|||
|
|
which is licensed under **Apache License 2.0**. That license is preserved and
|
|||
|
|
its terms continue to apply to the base weights this model was built from; the
|
|||
|
|
AGPL covers the combined work as distributed here. Apache-2.0 is one-way
|
|||
|
|
compatible with AGPLv3, which is what makes this combination possible.
|
|||
|
|
|
|||
|
|
If you run a modified version of this model as a network service, AGPL section
|
|||
|
|
13 requires you to offer the corresponding source of your modifications to its
|
|||
|
|
users.
|