Unsloth 微调小模型生成合成数据
一、为什么自己造一个「合成数据生成器」
大模型训练最贵的不是显卡,是高质量数据 。公开语料越洗越薄,标注外包又慢又贵。于是很多团队开始用大模型造数据------但用 GPT 级 API 批量生成,成本随规模线性爆炸。一个务实的出路是:用 QLoRA 把一个 4B 小模型微调成专用「合成数据生成器」,让它定点产出你需要的训练/微调样本,一次训练、无限复用。
本文用 Unsloth + QLoRA,把 Gemma-3-4B 微调成一个「能按格式产出(问题, 推理, 答案)三元组」的生成器,全程单卡 24GB 可跑,附带可直接抄的训练脚本与踩坑。
二、准备工作与数据
思路:给小模型看几百条「示范样本」,让它学会你的输出格式与风格。先造约 500 条种子数据,格式如下:
json
{
"instruction": "生成一个关于分布式锁的面试问答",
"output": "问:Redis 分布式锁为什么要用 SET NX PX 而不是先 SET 再 EXPIRE?\n答:因为两步之间进程可能崩溃,导致锁没有过期时间、永久死锁。SET NX PX 把『占锁』和『设超时』合成原子操作......"
}
造种子数据可以先用强模型生成再人工校验,比如让大模型产出 500 条再筛掉格式错的。重点是格式统一,后面 loss 才稳。
三、训练脚本(Unsloth + QLoRA)
Unsloth 把 LoRA 训练速度拉高、显存压低,4B 模型单卡 24GB 轻松跑。完整可复制:
python
import torch
from unsloth import FastModel
from unsloth import is_bfloat16_supported
from trl import SFTTrainer, SFTConfig
from datasets import load_dataset
model, tokenizer = FastModel.from_pretrained(
model_name="google/gemma-3-4b-it",
max_seq_length=2048,
load_in_4bit=True, # QLoRA:4bit 量化底座
dtype=torch.bfloat16 if is_bfloat16_supported() else torch.float16,
)
model = FastModel.get_peft_model(
model,
r=32, lora_alpha=64, lora_dropout=0,
target_modules=["q_proj","k_proj","v_proj","o_proj","gate_proj","up_proj","down_proj"],
)
ds = load_dataset("json", data_files="seeds.jsonl")["train"]
def fmt(ex):
return {"text": f"### 指令\n{ex['instruction']}\n\n### 回答\n{ex['output']}"}
ds = ds.map(fmt)
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
train_dataset=ds,
config=SFTConfig(
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
max_steps=300,
learning_rate=2e-4,
optim="adamw_8bit",
warmup_steps=20,
logging_steps=10,
output_dir="outputs_gemma_synth",
),
)
trainer.train()
model.save_pretrained("gemma_synth_lora")
四、用它批量造数据
训练完,加载 LoRA 直接生成:
python
from unsloth import FastModel
model, tokenizer = FastModel.from_pretrained("google/gemma-3-4b-it", load_in_4bit=True)
model = FastModel.from_pretrained("gemma_synth_lora", model) # 合并 LoRA
def gen(instruction, n=1):
out = []
for _ in range(n):
m = tokenizer.apply_chat_template(
[{"role":"user","content":instruction}], return_tensors="pt").to("cuda")
txt = model.generate(m, max_new_tokens=512, temperature=0.9, top_p=0.95)
out.append(tokenizer.decode(txt[0], skip_special_tokens=True))
return out
# 一次产出 2000 条候选,再过滤
prompts = [f"生成一个关于{s}的技术问答" for s in ["缓存击穿","CAP 定理","幂等设计"]]
data = [g for p in prompts for g in gen(p, n=700)]
比如要扩充一个「后端面试」数据集,就让生成器按主题轮转产出,再用规则过滤掉长度异常、格式缺失的样本,留下干净的几千条去训练下游任务模型。
五、工程取舍
- 底座选 4B 还是 7B:4B 快、便宜、可控,但生成多样性略弱;7B 质量更好但显存翻倍。比如数据格式复杂、需要长推理链时,建议直接上 7B。
- QLoRA 4bit 的精度损失:量化底座会轻微掉点,但对「格式生成」这种任务几乎无感;若用来生成高难度数学推理,建议改 fp8 或全参微调小模型。
- 种子数据质量 > 数量:500 条干净示范,胜过 5000 条噪声数据。比如示范里混了 10% 格式错的,生成器会稳定复现这个错误。
- 生成要加去重与过滤:小模型容易「背」训练样本,需做相似度去重,否则下游模型过拟合到这几句话。
六、踩坑清单
- max_seq_length 不够:生成样本被截断,下游任务学到半截格式。比如输出常超 1k token,设 2048 才稳。
- LoRA target_modules 漏层:只挂 q/v 会训不动,生成质量上不去;建议挂全投影层。
- 学习率过高炸 loss:QLoRA 常用 2e-4,调太高会震荡,loss 突然变 nan。
- 生成温度太低=复读:temperature=0 时小模型反复吐同一句;0.9 左右多样性才好。
- 忘记保存 tokenizer:只存了 LoRA 权重,加载时 tokenizer 不匹配,输出乱码。
七、辩证:小模型生成器不是万能水源
把它当「无限水源」会翻车。第一,生成器只能学会种子数据的分布,天花板就是你的示范质量------示范烂,产出烂。第二,合成数据用多了会造成「模型教模型」的退化闭环,需要持续混入真实数据对冲。第三,某些高专业性领域(如医疗、法律)小模型生成易出现看似合理实则错误的「幻觉样本」,必须经过人工或强模型校验才能进训练集。所以正确姿势是:小模型生成器做「扩量」,真实数据做「定标」,二者配比而非替代。
八、动手题
- 如果你的种子数据只有 100 条,你觉得训出来的生成器能直接投产吗?
- 在「4B 快但弱」和「7B 强但贵」之间,你的数据生成任务会怎么选?
- 你是怎么给合成数据做去重和过滤的?
欢迎在评论区说说你的经验、分享你的流水线。
数据与事件来源:
- 来源:Unsloth 官方文档(FastModel / QLoRA / SFTTrainer 用法与显存优化说明)
- 来源:Google Gemma-3-4B 模型卡(Hugging Face,4B 指令微调基座)
- 来源:Hugging Face TRL 库 SFTConfig 训练参数参考