一次Qwen3-8B SFT实践¶
目标¶
输入中文长篇小说,在4070 Super 12GB上使用QLoRA训练Qwen3-8B,使模型学习特定叙事方式。
整个实践分成两轮:
- 第一轮的prompt设计较差,SFT数据少,训练步数也少,输入原prompt后生成效果不明显;
- 第二轮重写了prompt,增加SFT数据和测试集合,并提高训练强度。这是当前所在的阶段。
数据处理¶
原始小说不能简单按固定字数切分。实际流程是:
自然段和句子切分
-> 组合成500—1500 tokens的窗口
-> Qwen3-4B选择场景边界并生成scene spec
-> 程序从原文截取completion
-> scene spec渲染成prompt
数据编辑使用Qwen3-4B,正式训练使用Qwen3-8B。completion保留原文,避免数据编辑模型重写正文。
第一轮的prompt比较抽象,而且会要求“写5000字”,对应completion却只有约1000 tokens。prompt与completion的条件关系不够具体,长度要求也互相矛盾。
第二轮改成结构化scene spec,补充人物关系、场景目标、冲突和required beats;字数要求则由程序根据completion长度生成。
数据pipeline还补充了断点恢复、坏行跳过、错误日志、JSONL检查和prompt去重。否则一次异常输出就会中断数小时的处理。
第一轮:prompt较差,训练量不足¶
第一次配置:
batch size为1,因此总共只有22个optimizer steps。训练loss约3.2,eval loss约3.16—3.24,数值没有发散,但输入训练时的prompt后,生成结果仍然接近base model。
第一轮同时存在两个问题:prompt监督关系较弱,训练量也不足。训练可以正常结束,不代表模型已经学会目标行为。
日志怎么看¶
第一轮日志:
{'loss': '3.326', 'grad_norm': '1.359', 'learning_rate': '5e-05',
'entropy': '2.04', 'num_tokens': '4.9e+04',
'mean_token_accuracy': '0.4184', 'epoch': '0.09467'}
45%|████████████████████ | 10/22 [05:37<06:36, 33.04s/it]
71%|████████████████████ | 27/38 [00:16<00:07, 1.55it/s]
loss:参考token的负对数似然,越低通常越好,但不会逐step下降;grad_norm:梯度大小,用于观察梯度爆炸;learning_rate:当前学习率;entropy:输出概率分布的不确定性;num_tokens:累计处理的token数量;mean_token_accuracy:预测概率最高的token与参考token一致的比例;epoch:训练集已经遍历的比例。
it是iteration。这里通常表示一次optimizer step,不是一条样本。
33.04s/it表示一次参数更新约33秒。因为gradient accumulation为16,每次更新前需要执行16个micro-batch。下面的1.55it/s是eval速度;eval不反向传播,所以更快。
两个进度条分别表示:
第二轮:扩大数据并改进prompt¶
第二轮重新生成数据,使用更具体的prompt,将SFT训练集扩大到1229条、验证集扩大到136条,同时增加固定测试集合。训练配置也被加强:
train: 1229条
eval: 136条
LoRA r=32, alpha=64, dropout=0
gradient accumulation=4
learning rate=5e-5
5 epochs
共1540个optimizer steps,保存了五个checkpoint:
| checkpoint | epoch | eval loss |
|---|---|---|
| 308 | 1 | 4.534 |
| 616 | 2 | 4.668 |
| 924 | 3 | 4.610 |
| 1232 | 4 | 4.587 |
| 1540 | 5 | 4.573 |
第三个epoch附近的日志:
{'loss': '4.276', 'grad_norm': '5.656', 'learning_rate': '1.762e-05',
'entropy': '2.139', 'num_tokens': '6.625e+06',
'mean_token_accuracy': '0.4548', 'epoch': '2.996'}
{'loss': '4.94', 'grad_norm': '1.625', 'learning_rate': '1.752e-05',
'entropy': '2.008', 'num_tokens': '6.634e+06',
'mean_token_accuracy': '0.4463', 'epoch': '3'}
60%|████████████████████ | 924/1540 [4:06:37<1:16:31, 7.45s/it]
91%|████████████████████ | 124/136 [01:33<00:06, 1.80it/s]
单个step的loss从4.276变成4.94不能证明过拟合,因为batch难度不同。判断过拟合需要同时看验证集趋势和实际生成。
少量人工测试中,checkpoint-616开始出现训练痕迹,checkpoint-924相对可用,checkpoint-1232之后逐渐出现病句、重复和语言退化。但这些观察还不足以确定最佳checkpoint。
相同prompt为什么仍然生成不好¶
SFT学习的是参考token的条件概率,不是把prompt保存成数据库key。长文本生成时,teacher forcing与实际推理也存在差异:训练时每一步都能看到正确前文,推理时一次偏离会继续累积。
但如果训练prompt下连人物、事件和场景结构都无法稳定复现,仍然说明SFT没有达标。可能原因包括:
- prompt与completion的对应关系不够具体;
- completion太长,关键行为被大量普通叙事token稀释;
- LoRA容量或训练参数不合适;
- 推理时adapter、chat template或解码参数使用错误;
- sampling放大了生成偏差。
当前仍未解决的问题¶
第二轮虽然改进了prompt、数据量和训练量,但仍然存在:
- 缺少数据质量检测,无法批量判断prompt与completion是否匹配;
- 缺少统一的checkpoint评测,无法确定308、616、924等版本哪个更好;
- 缺少固定的模型测试流程,生成结果无法稳定复现和横向比较;
- 使用训练集中的相同prompt,生成质量仍然不稳定,尚未确定是数据、训练、adapter加载还是解码造成的。
因此,loss从3变成4不能直接说明过拟合,单次人工生成也不能说明某个checkpoint更好。当前缺少的是统一评测流水线。
下一步:建立评测流水线¶
首先做训练集重建测试:从train中抽取固定prompt,关闭sampling,对比base和各checkpoint。检查核心情节是否覆盖、语言是否退化,以及adapter是否真正生效。
训练集测试通过后,再使用未参与训练的固定prompt测试泛化。所有模型必须使用相同的system prompt、seed、解码参数和最大输出长度,并输出A/B结果供人工比较。
checkpoint不应该按训练轮数或单个loss选择。应选择最早达到目标能力、同时尚未出现明显语言退化的版本。
如果训练prompt都无法生成基本合格的候选,就不适合进入DPO。下一步应先检查数据关联、loss mask、chat template、adapter加载和解码方式,再降低学习率、减少epoch、增加dropout并重新训练。