Skip to content

一次Qwen3-8B SFT实践

目标

输入中文长篇小说,在4070 Super 12GB上使用QLoRA训练Qwen3-8B,使模型学习特定叙事方式。

整个实践分成两轮:

  1. 第一轮的prompt设计较差,SFT数据少,训练步数也少,输入原prompt后生成效果不明显;
  2. 第二轮重写了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较差,训练量不足

第一次配置:

train: 约338条
eval: 38条
LoRA r=8, alpha=16, dropout=0.05
gradient accumulation=16
1 epoch

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不反向传播,所以更快。

两个进度条分别表示:

10/22:训练optimizer steps
27/38:当前eval batch

第二轮:扩大数据并改进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、数据量和训练量,但仍然存在:

  1. 缺少数据质量检测,无法批量判断prompt与completion是否匹配;
  2. 缺少统一的checkpoint评测,无法确定308、616、924等版本哪个更好;
  3. 缺少固定的模型测试流程,生成结果无法稳定复现和横向比较;
  4. 使用训练集中的相同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并重新训练。

Comments