去年夏天我接了个活——用LLaMA-2 7B微调一个行业客服模型。客户给了两万条问答数据,我信心满满跑全量微调(Full Fine-tune),8张A100跑了一周。结果上线后,模型回答的质量还不如直接Zero-shot调用GPT-4。准确率低了12个百分点,而且回答语气僵硬得像机器人念稿。
后来复盘才发现——那两万条数据里,有至少30%的答案是运营同学直接从旧FAQ里复制粘贴的,长度超过200个字,还带着“尊敬的客户您好”这类模板。模型学了一堆废话,把原始预训练知识都给洗掉了。
所以第一个教训:微调的效果下限取决于数据质量,上限才取决于模型和参数。 数据清洗花了两天,去重、去噪声、截断过长样本、人工抽检一致性,重新跑一轮LoRA,效果直接反超。
数据质量比你想的更重要
说到数据清洗,我后来总结了一套流水线:
做完这步,同样用LoRA跑一晚上(4小时),BLEU从15涨到24,人工打分从3.2/5涨到4.1/5。
方法选择:全量微调 vs LoRA vs QLoRA
我拿同一批干净数据做了个对比,显存和速度差距很明显:
| 方法 | 7B模型显存占用 | 训练速度(1000步) | 效果(ROUGE-L) |
|------|--------------|----------------|----------------|
| Full Fine-tune | ~48GB | 45分钟 | 0.62 |
| LoRA (r=8) | ~14GB | 28分钟 | 0.58 |
| QLoRA (4bit) | ~6GB | 35分钟 | 0.54 |
看到没?LoRA只损失了0.04的ROUGE-L,但显存从48GB降到14GB——你单卡3090就能跑。QLoRA更省,但效果掉得明显些,适合低资源场景。我日常首选LoRA,除非客户明确要求必须全量微调(比如需要模型学会大量新知识时才会考虑全量)。
顺便说一个容易踩的坑:LoRA的秩(r)不是越大越好。我试过r=16和r=32,效果反而没r=8好——可能是数据量太小(总共800条训练),参数越多越容易过拟合。小数据建议r=4到8,大数据(>10万条)再考虑r=16以上。
训练策略:学习率和warmup比你想的敏感
我第一次跑LoRA直接用默认学习率2e-4,结果loss在500步后开始震荡。后来改成1e-4 + 10% warmup steps(总步数2000,warmup 200步),loss曲线平滑很多。
还有一件事情很关键:不要冻结embedding层。我之前看网上教程说冻结embedding可以节省显存,结果模型学了一周连“退款”和“退货”都分不清——因为底层的token embedding没更新。新知识对应不上原始语义。对于领域微调(比如医疗、法律),我建议放开所有attention层,只冻结MLP层的前几层。
评估不能只看loss
我见过太多人拿验证集loss一降就以为成功了。loss从2.1降到1.8,但实际回答还是胡言乱语。尤其是生成长文本任务,loss和人类偏好几乎没关系。
我的做法是:每个epoch跑一次自动评测(用GPT-4打分+ROUGE/BLEU),同时让内部同事人工抽检50条。人工抽检的维度就三个:准确性(有没有事实错误)、相关性(回答是否对得上问题)、语气(是否像人说话)。人工打分低于3.5分(5分制)的epoch直接重跑。
这里特别想提一句,如果你做的是问答类微调,一定要关注首句质量。很多模型前几个词就开始瞎编(比如“你好,我是XX助手”然后接废话),我把首句的loss权重提高了2倍,改善很明显。
微调后的部署:推理优化是另一座山
模型训练好了,上线又是一关。我试过DirectML推理,延迟高到客户想骂人。后来切到vLLM + FP16,把batch size调到8,单卡V100(16GB)上并发20个请求,P95延迟从2.1秒降到600ms。关于这块踩的坑,我之前写过一篇详细的: 大模型推理延迟优化 。
还有一个很tricky的点:微调后的模型在推理时,注意temperature和top_p要调。因为微调数据往往包含固定话术,temperature设太高模型会乱加语气词,设太低又像复读机。我一般用0.7 + top_p 0.9,再根据具体场景微调。
如果时间紧,试试few-shot + prompt工程
说实话,很多时候微调不是最优解。尤其数据量少于500条时,我宁愿花时间调prompt。比如用Claude 3.5做个few-shot模板,效果经常超过微调。当然如果数据量过万且领域独特(比如法律条款、医疗病例),微调还是有必要的。
关于prompt工程和对比测试,我另外写了一篇文章: Claude SEO优化实战 ,里面讲了我怎么用Claude做文本转换,比微调便宜了一百倍。
最后说一个玄学
微调时我在 wandb 上盯着loss,隔壁组用完全一样的数据和参数,但跑了两个epoch就停了——loss曲线一样。效果差了一截。后来发现他们忘记加载预训练权重,从随机初始化开始跑的。
这种低级错误我犯过两次。现在每次启动训练前,我都打印第一层参数的统计分布:如果全是接近0的随机数,说明没加载预训练。检查这一步比调参更救命。
以上都是自己用A100/4090跑出来的经验,不保证所有场景通用。但如果你也准备微调一个7B/13B模型,至少可以避开我掉进去的那些坑。