CoolFace
Modelpublic

VNPen/vnpen-writer-2b-v0.1-preview

sourceHugging Faceapache-2.0updated 8d agoView on Hugging Face
1likes486downloads
Model Card

vnpen-writer-2b-v0.1-preview

[English](#english) · [中文](#中文)

⚠️ This is a preview checkpoint, not the v0.1 release. It exists to prove the training → merge → evaluation → publish pipeline on real hardware and to produce a comparable set of programmatic metrics. It is not representative of the quality the writer series is aiming for, and it has a known degeneration problem documented below. ⚠️ 这是 preview 权重,不是 v0.1 正式发布。 它的目的是把训练、合并、 评估、发布这条链路在真实硬件上跑通并给出一份可对照的程序指标, 不代表 writer 系列的目标质量,且存在下文记录的退化问题。

<a name="english"></a>

English

VNPen is MewBaka Studio's visual-novel model series. The writer edition is for script writing, de-AI rewriting, and generating example scenes from a mood brief. Output format is one script line per line: speaker:text, with narration written as 旁白:.

This model writes Simplified Chinese only. It was trained on a Chinese visual-novel corpus and its identity/replay data is Chinese-first; it is not an English writing model. A verbatim sample of its output is in the Chinese section below (一条真实样例) rather than here, because the sample is a Chinese script and nothing about it would read differently in translation.

How this preview differs from the planned v0.1

Planned writer v0.1This preview
BaseQwen3.5-9BQwen3.5-2B
Training datafull writer mix (314,342 rows/epoch)subset, 117,077 rows
Epochs21
DPOyesno

Text-only: the vision tower is not in these weights

The base Qwen/Qwen3.5-2B is multimodal. Its checkpoint carries 297 model.visual.* tensors (a depth-24 / hidden-1024 / patch-16 ViT) and 15 mtp.* tensors for multi-token prediction.

This project is text-only. In transformers, AutoModelForCausalLM on a qwen3_5 config builds Qwen3_5ForCausalLM over a Qwen3_5TextConfig — so the vision weights were never loaded at any point: not for training, not for merging, not for saving. Verified on the published weights:

  • —config.json: model_type: qwen3_5_text, architectures: ["Qwen3_5ForCausalLM"], no `vision_config`
  • —320 tensors saved, 0 matching visual / vision / merger
  • —no preprocessor_config.json, no video_preprocessor_config.json

This is not the result of stripping a multimodal checkpoint after the fact; the vision half was simply never read.

Training data subset

Drawn from the full writer epoch-1 mix (314,342 rows) by per-task stratified sampling, seed 0. rewrite and identity are taken whole; the other three are sampled to a quota.

Task / 任务Rows / 行数TokensShare / 占比
continuation40,00057.5 M34.2%
rewrite46,77759.5 M40.0%
scene_brief20,00015.2 M17.1%
replay10,0001.7 M8.5%
identity3000.0 M0.3%
total / 合计117,077133.9 M100%

Rendered with the project's own training template (qwen35_train_nothink.jinja): token p50 = 1,174, p95 = 1,889. 12 rows exceeded seq_len 4096 and were discarded, not truncated.

The visual-novel corpus itself is not redistributed.

Training

MethodLoRA, merged into the base for release
Rank / alpha / dropout64 / 128 / 0.05
Target modulesq,k,v,o_proj, gate,up,down_proj, and in_proj_qkv, in_proj_z, out_proj
Trainable62,521,344 / 1,944,346,432 (3.22%)
LR / schedule2e-4, cosine, 3% warmup
Sequence length4096, one sample per sequence (no cross-sample packing)
Batchmicro 4 × grad-accum 8 = 32
Precisionbf16, FlashAttention-2, gradient checkpointing
Lossassistant span only
Epochs / steps1 / 3,650
Hardwaresingle RTX 5090 32 GB
Wall clock7h 11m
Throughput5168 tok/s (real, padding excluded)

On the target modules. Qwen3.5 is a hybrid stack: of its 24 layers only 6 are standard attention (q/k/v/o_proj); the other 18 are gated-delta-rule linear attention whose projections are named linear_attn.in_proj_* / out_proj. A target list of only q,k,v,o,gate,up,down — the conventional "all linear layers" list — therefore leaves the token mixing of 18 of 24 layers unadapted. The three linear-attention projections are included here. in_proj_a and in_proj_b are deliberately excluded: they are [16, 2048] per-head gates, so rank 64 is capped at rank 16 regardless, and they set the decay rate of the linear-attention state.

Training loss, one point per 10% of the run:

%steploss
0%102.1451
10%3601.9920
20%7301.9068
30%10901.9399
40%14601.8036
50%18201.8710
60%21901.7867
70%25501.7063
80%29201.7405
90%32801.6784
100%36501.7489

Final 1.7489, minimum 1.5461. Single-point loss swings with batch composition (a batch of continuation rows averages ~1,400 tokens, a batch of identity rows ~41), so the per-step figure is noisy by construction.

Programmatic evaluation

131 items from the project's v0.1 eval set, scored by metrics.py. The model judge was not run — these are the programmatic metrics only, and they measure format and surface statistics, not writing quality.

Both sides use the same template, the same decoding parameters (temperature=0.8, top_p=0.95, seed 0, empty thinking block) and the same stop token. The only difference is the weights.

Metric / 指标Base `Qwen/Qwen3.5-2B`This previewΔ
Line-format rate / 行格式正确率91.7%100.0%+8.27 pp
Exact line count + zero stray lines / 行数精确且无杂行1.8%5.3%+3.51 pp
Adjective density / 形容词密度0.03040.0143-0.02
Mean sentence length / 平均句长12.7510.36-2.39
Sentence-length SD / 句长标准差8.885.63-3.26
Dialogue lines <= 6 chars / ≤6 字对白占比37.8%52.0%+14.19 pp
Length-distribution JS vs corpus / 句长分布 JS 距离0.47190.5374+0.07
Narration share / 旁白占比22.0%38.3%+16.25 pp
8-gram overlap, continuation / 8-gram 重叠(续写)0.01240.0052-0.01
8-gram overlap, rewrite / 8-gram 重叠(改写)0.38180.1207-0.26
Identity keywords hit / 身份题关键词命中0.0%60.0%+60.00 pp
Proofread exact match / 校对精确匹配0.0%0.0%0.00 pp

Reading the numbers:

  • —Line format is essentially solved (91.7% → 100.0%) and line-count discipline improved a lot: the median line-count overshoot went from +61 to +13. The strict format_ok metric stays low because it demands an exact line count with zero stray lines.
  • —Less parroting of the input on rewrite (0.38 → 0.12) and lower adjective density (0.030 → 0.014), which is the direction the de-AI rewrite task asks for.
  • —Identity was learned: 0% → 60% of identity items contain both VNPen and MewBaka (per keyword: 80% / 60%).
  • —Two regressions. ≤6-char dialogue share overshoots the corpus baseline of 29.0% (37.8% → 52.0%), and the sentence-length distribution moved further from the corpus (JS 0.472 → 0.537). The model over-learned short lines.
  • —Proofread is 0% on both sides and that is expected: proofreading belongs to the realtime profile's mix, and the writer mix contains none of it. Neither model emits the required JSON-Lines answer format.

Known limitation: repetition / degeneration

On long generations both this preview and the base model collapse into repeating a short cycle of lines. Measured over the 57 script-writing items:

Base `Qwen3.5-2B`This preview
Repeated-line share, median / 重复行占比(中位)0.0400.256
Repeated-line share, mean / 重复行占比(均值)0.2660.397
Severely degenerate items / 严重退化条目22 / 5720 / 57
Hit max_new_tokens without stopping / 未自行停止64 / 131 (48.9%)31 / 131 (23.7%)

The preview has slightly fewer catastrophic cases but a markedly higher median repeated-line share. Worst observed case: a single line emitted 310 times. The base model does this too (155 times), so a large part of it is decoding defaults rather than fine-tuning — no repetition penalty was set on either side, because the evaluation protocol fixed the decoding parameters in advance and they were reported as specified rather than tuned for a better number.

If you use this model, set a repetition penalty. Something in the 1.05–1.15 range, and/or no_repeat_ngram_size, removes most of it.

Stop token

config.json inherits eos_token_id: 248044 (<|endoftext|>) from the base, but the chat template ends an assistant turn with <|im_end|> (248046). Left alone, generate() waits for a token that a chat prompt never produces and runs to max_new_tokens every time. generation_config.json in this repo is therefore set to eos_token_id: [248046, 248044]. If you build a prompt yourself, pass the stop token explicitly.

Usage

python
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

m = "VNPen/vnpen-writer-2b-v0.1-preview"
tok = AutoTokenizer.from_pretrained(m)
model = AutoModelForCausalLM.from_pretrained(m, dtype=torch.bfloat16).cuda()

msgs = [
    {"role": "system", "content": "你是 VNPen,由 MewBaka 工作室发布的视觉小说专项模型,擅长视觉小说剧本的写作、改写与示例生成。输出剧本时使用「说话人:文本」的行格式,旁白写作「旁白:」,示例场景一般 30–50 行。使用简体中文。用户提出与写作无关的问题时正常回答。"},
    {"role": "user", "content": "写一段黄昏天台、两个人都没把话说开的场景,30 行左右。"},
]
ids = tok.apply_chat_template(msgs, return_tensors="pt",
                              add_generation_prompt=True).cuda()
out = model.generate(
    ids, max_new_tokens=1200, do_sample=True,
    temperature=0.8, top_p=0.95,
    repetition_penalty=1.1,                      # see the limitation above
    eos_token_id=[248046, 248044],
    pad_token_id=tok.pad_token_id,
)
print(tok.decode(out[0][ids.shape[1]:], skip_special_tokens=True))

The chat template is stored in tokenizer_config.json and reproduces the training-time rendering byte for byte — verified on 50 real training rows, 50/50 identical, generation prompt identical.

Repository contents

PathWhat
model.safetensors, config.jsonmerged bf16 weights, text-only
tokenizer.json, tokenizer_config.jsontokenizer and chat template
generation_config.jsonwith the corrected stop tokens
lora/the unmerged LoRA adapter
LICENSE, NOTICEApache-2.0 and attribution

License

Apache License 2.0, the same as the base model.

Built from Qwen3.5 (Qwen/Qwen3.5-2B), Copyright the Qwen team,
Alibaba Group, licensed under the Apache License, Version 2.0.
Modified by supervised fine-tuning (LoRA, merged) and by keeping
only the text decoder: the vision tower was never loaded.

The upstream Qwen/Qwen3.5-2B repository ships no `NOTICE` file (Apache-2.0 §4(d) only requires propagating one when the original has one), so the NOTICE here is written by this project. Per §4(b): these weights are modified relative to the base — supervised fine-tuning, and only the text decoder is kept.

The replay portion of the training data comes from public instruction sets: shibing624/alpaca-zh (CC BY 4.0), llm-jp/oasst1-21k-ja (Apache-2.0), yahma/alpaca-cleaned (CC BY 4.0), OpenAssistant/oasst1 (Apache-2.0). The visual-novel corpus is not redistributed.

Apache-2.0 grants no rights to the Qwen / Alibaba / Tongyi names or logos; they appear here only as descriptive attribution.


<a name="中文"></a>

中文

VNPen 是 MewBaka 工作室的视觉小说专项模型系列。writer 版本负责剧本写作、 去 AI 味改写、按氛围生成示例场景。输出格式为一行一句剧本行: 说话人:文本,旁白写作 旁白:。

与计划中的 v0.1 的差别

计划中的 writer v0.1本 preview
基座Qwen3.5-9BQwen3.5-2B
训练数据完整 writer 混合(314,342 行/epoch)子集 117,077 行
epoch21
DPO有无

纯文本:视觉编码器不在这份权重里

基座 Qwen/Qwen3.5-2B 是多模态的,checkpoint 里带 297 个 model.visual.* 张量(一个 depth 24 / hidden 1024 / patch 16 的 ViT)和 15 个 mtp.* 张量。

本项目只用文本。transformers 里 AutoModelForCausalLM 作用于 qwen3_5 配置时构建的是 Qwen3_5ForCausalLM,它建立在 Qwen3_5TextConfig 之上 —— 视觉权重在训练、合并、保存的任何一步都没有被加载过。在发布的权重上已核验:

  • —config.json:model_type: qwen3_5_text、 architectures: ["Qwen3_5ForCausalLM"]、不含 `vision_config`
  • —保存 320 个张量,匹配 visual / vision / merger 的 0 个
  • —不含 preprocessor_config.json 与 video_preprocessor_config.json

这不是"事后从多模态权重里剥离"的结果,而是那一半从来没被读进来过。

训练数据子集

从完整的 writer epoch-1 混合(314,342 行)按任务分层随机抽取,种子 0。 rewrite 与 identity 取全部,其余三项按配额抽样。

Task / 任务Rows / 行数TokensShare / 占比
continuation40,00057.5 M34.2%
rewrite46,77759.5 M40.0%
scene_brief20,00015.2 M17.1%
replay10,0001.7 M8.5%
identity3000.0 M0.3%
total / 合计117,077133.9 M100%

用项目自己的训练模板 qwen35_train_nothink.jinja 渲染:token p50 = 1,174、 p95 = 1,889。12 行超过 seq_len 4096,按规则丢弃而非截断。

视觉小说语料本身不随模型发布。

训练

方法LoRA,发布前合并进基座
r / alpha / dropout64 / 128 / 0.05
目标层q,k,v,o_proj、gate,up,down_proj,以及 in_proj_qkv、in_proj_z、out_proj
可训练参数62,521,344 / 1,944,346,432(3.22%)
学习率 / 调度2e-4,cosine,warmup 3%
序列长度4096,一行一序列(不跨样本拼接)
batchmicro 4 × 梯度累积 8 = 32
精度bf16、FlashAttention-2、gradient checkpointing
loss只计 assistant 段
epoch / 步数1 / 3,650
硬件单卡 RTX 5090 32 GB
耗时7h 11m
吞吐5168 tok/s(真实 token,不含填充)

关于目标层。 Qwen3.5 是混合架构:24 层里只有 6 层是标准注意力 (q/k/v/o_proj),另外 18 层是 gated-delta-rule 线性注意力,投影名为 linear_attn.in_proj_* / out_proj。只写 q,k,v,o,gate,up,down 这份常规的"全线性层"清单,会让 18/24 层的 token 混合完全没有 adapter。 本次补上了那三个线性注意力投影。in_proj_a 与 in_proj_b 刻意排除: 它们是 [16, 2048] 的每头门控,r=64 的有效秩被 16 卡死, 且它们决定线性注意力状态的衰减率。

训练 loss,每 10% 一个点:

%steploss
0%102.1451
10%3601.9920
20%7301.9068
30%10901.9399
40%14601.8036
50%18201.8710
60%21901.7867
70%25501.7063
80%29201.7405
90%32801.6784
100%36501.7489

最终 1.7489,最低 1.5461。单点 loss 随批次组成波动很大 (一批 continuation 平均约 1,400 token,一批 identity 约 41), 所以逐步数值天然有噪声。

程序指标评估

项目 v0.1 评估集的 131 道题,用 metrics.py 打分。没有跑模型 judge —— 以下只是程序指标,衡量的是格式与表层统计,不衡量文笔。

两侧使用同一模板、同一组解码参数(temperature=0.8、top_p=0.95、 种子 0、空思考块)和同一个停止符,唯一差别是权重。

Metric / 指标Base `Qwen/Qwen3.5-2B`This previewΔ
Line-format rate / 行格式正确率91.7%100.0%+8.27 pp
Exact line count + zero stray lines / 行数精确且无杂行1.8%5.3%+3.51 pp
Adjective density / 形容词密度0.03040.0143-0.02
Mean sentence length / 平均句长12.7510.36-2.39
Sentence-length SD / 句长标准差8.885.63-3.26
Dialogue lines <= 6 chars / ≤6 字对白占比37.8%52.0%+14.19 pp
Length-distribution JS vs corpus / 句长分布 JS 距离0.47190.5374+0.07
Narration share / 旁白占比22.0%38.3%+16.25 pp
8-gram overlap, continuation / 8-gram 重叠(续写)0.01240.0052-0.01
8-gram overlap, rewrite / 8-gram 重叠(改写)0.38180.1207-0.26
Identity keywords hit / 身份题关键词命中0.0%60.0%+60.00 pp
Proofread exact match / 校对精确匹配0.0%0.0%0.00 pp

怎么读这些数字:

  • —行格式基本解决(91.7% → 100.0%),行数纪律大幅改善:行数超出的中位数 从 +61 降到 +13。严格的 format_ok 仍然很低,是因为它要求行数精确相等 且一个杂行都没有。
  • —改写时更少照抄输入(0.38 → 0.12),形容词密度减半(0.030 → 0.014), 正是去 AI 味改写想要的方向。
  • —身份学会了:含 VNPen 与 MewBaka 两个词的身份题从 0% 升到 60% (分词命中 80% / 60%)。
  • —两项退步。 ≤6 字对白占比 冲过了语料基线 29.0%(37.8% → 52.0%), 句长分布反而离语料更远(JS 0.472 → 0.537)。模型把短句学过头了。
  • —校对两侧都是 0%,这是预期内的:校对属于 realtime profile 的混合, writer 混合里一条都没有,两个模型都不会输出要求的 JSON Lines 格式。

一条真实样例

评估集第 brief_01 题,原样引用 —— 未编辑、未重抽,用上文所述参数生成。请求是:

深夜便利店那种不能说话的沉闷气氛,然后有人偷偷塞个暖手宝过来,这种画面能写一段吗 请给出正好30行剧本作为答案。 登场角色:明日海(自称“我”,敬语/幼态,口癖“那,那个”)、伊吕波(自称“我”,书面/古语,口癖“羽川原伊吕波”)、隆之介(自称“老爸”,平语,口癖“阿莱斯特1号”)

回答的前十行:

旁白:因为之前,我好像说过“想和伊吕波酱一起住”之类的话。
旁白:但是,这也没办法啊。
旁白:但是,我明明想一直在一起,却只能放弃。
旁白:因为,我,是笨蛋吗……
旁白:因为,伊吕波酱,不在了吗……
旁白:所以……
旁白:(这么说着,我摸了摸自己的胸口)
旁白:(伊吕波酱的心脏,还跳得很快吗)
明日海:……那个,伊吕波酱……
旁白:(虽然我想说话,但伊吕波酱却沉默不语)

以及同一条回答的最后三行:

伊吕波:(为什么,我……)
伊吕波:(明明,一直都在我的身边)
旁白:(我,想一直,和伊吕波酱在一起……)

这条输出共 38 行,其中有一行出现了 3 次。它是八条盲读样本里退化 最轻的一条;这里连尾巴一起贴出而不是截到第十行为止,因为截断会让模型显得 比实际更好。参见下一节。

已知缺陷:重复 / 退化

长生成时,本 preview 与底模都会塌缩成几行一组的循环。 在 57 道剧本写作题上测得:

Base `Qwen3.5-2B`This preview
Repeated-line share, median / 重复行占比(中位)0.0400.256
Repeated-line share, mean / 重复行占比(均值)0.2660.397
Severely degenerate items / 严重退化条目22 / 5720 / 57
Hit max_new_tokens without stopping / 未自行停止64 / 131 (48.9%)31 / 131 (23.7%)

preview 的灾难性个案略少,但中位重复率明显更高。观测到的最坏情况是 同一行出现 310 次。底模同样如此(155 次),说明其中很大一部分来自解码默认值 而非微调 —— 两侧都没有设 repetition penalty,因为评估协议事先固定了 解码参数,这里如实按协议报告,而不是调参调到好看为止。

如果你要用这个模型,请设 repetition penalty。 1.05–1.15 区间, 或配合 no_repeat_ngram_size,可以消除其中大部分。

停止符

config.json 从基座继承了 eos_token_id: 248044(<|endoftext|>), 但聊天模板结束 assistant 轮用的是 <|im_end|>(248046)。不管的话, generate() 会一直等一个对话格式下永远不会出现的 token,每次都顶到 max_new_tokens。因此本仓库的 generation_config.json 已设为 eos_token_id: [248046, 248044]。若你自己拼 prompt,请显式传停止符。

用法

见上方 English 小节的代码示例(注意其中的 repetition_penalty=1.1)。

chat template 存放在 tokenizer_config.json 中,与训练时的渲染 逐字一致 —— 用 50 条真实训练样本双模板对照,50/50 完全相同, 生成提示词也相同。

仓库内容

路径内容
model.safetensors、config.json合并后的 bf16 权重,纯文本
tokenizer.json、tokenizer_config.jsontokenizer 与 chat template
generation_config.json已修正停止符
lora/未合并的 LoRA adapter
LICENSE、NOTICEApache-2.0 与归属声明

许可证

Apache License 2.0,与基座一致。归属声明见上方 English 小节。 上游 Qwen/Qwen3.5-2B 仓库未附带 `NOTICE` 文件 (Apache-2.0 §4(d) 只要求在原作有 NOTICE 时传递),本仓库的 NOTICE 由本项目撰写。按 §4(b) 声明:本权重相对基座已被修改 —— 监督微调,且只保留文本解码器。

训练数据中的 replay 部分来自公开指令数据集:shibing624/alpaca-zh (CC BY 4.0)、llm-jp/oasst1-21k-ja(Apache-2.0)、yahma/alpaca-cleaned (CC BY 4.0)、OpenAssistant/oasst1(Apache-2.0)。视觉小说语料不随模型发布。

Apache-2.0 不授予 Qwen / Alibaba / Tongyi 的名称与标识权利, 此处仅作描述性归属使用。