一、问题背景:为什么7B模型也需要微调
先说结论:通用大模型在垂直领域就是“什么都懂一点,什么都不精”。
我手上的场景是医疗问诊辅助。直接拿Qwen2.5-7B-Instruct去回答“二甲双胍的禁忌症”,它会给你一段看起来正确但缺少关键细节的答案,比如漏掉“eGFR`等特殊token,不要手动拼。
4.2 训练配置
这是LoRA组的核心代码,QLoRA只需改model加载部分:
import torch
from transformers import AutoModelForCausalLM, TrainingArguments, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from trl import SFTTrainer
# ---- QLoRA 4bit配置(LoRA组删掉这一段,模型用bf16加载)----
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.bfloat16,
)
model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=bnb_config, # LoRA组去掉这行,加 torch_dtype=torch.bfloat16
device_map="auto",
trust_remote_code=True,
)
model = prepare_model_for_kbit_training(model) # LoRA组可省略
lora_config = LoraConfig(
r=16,
lora_alpha=32,
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
target_modules=["q_proj","k_proj","v_proj","o_proj",
"gate_proj","up_proj","down_proj"],
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# 输出: trainable params: 40,370,176 || all params: 7,655,986,688 || 0.5273%
args = TrainingArguments(
output_dir="./qwen2.5-7b-medical-lora",
num_train_epochs=3,
per_device_train_batch_size=2,
gradient_accumulation_steps=8, # 等效batch=16
learning_rate=2e-4,
lr_scheduler_type="cosine",
warmup_ratio=0.03,
logging_steps=10,
save_strategy="epoch",
eval_strategy="epoch",
bf16=True,
gradient_checkpointing=True,
optim="paged_adamw_8bit",
max_grad_norm=0.3,
report_to="none",
)
trainer = SFTTrainer(
model=model,
args=args,
train_dataset=dataset["train"],
eval_dataset=dataset["test"],
dataset_text_field="text",
max_seq_length=1024,
packing=False,
)
trainer.train()
trainer.model.save_pretrained("./lora_adapter")
关键参数解释:
- learning_rate=2e-4:LoRA常用范围1e-4~3e-4,我试过5e-4会震荡。
- max_grad_norm=0.3:QLoRA官方推荐值,比默认1.0更稳。
- optim="paged_adamw_8bit":省显存,QLoRA必开。
- packing=False:医疗样本长度差异大,packing会混入无关上下文,实测掉点。
4.3 loss曲线
训练3 epoch,共480步。Loss变化:
| Step | Train Loss | Eval Loss |
|---|---|---|
| 10 | 1.42 | - |
| 100 | 0.98 | 0.94 |
| 200 | 0.79 | 0.81 |
| 320 | 0.68 | 0.73 |
| 480 | 0.61 | 0.69 |
LoRA组和QLoRA组的train loss差距在0.02以内,eval loss QLoRA略高0.03左右,属于量化噪声范围。没有出现过拟合,eval loss持续下降。
4.4 推理对比
推理时把adapter合并回base模型,避免每次前向都走LoRA分支:
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch, json
base = AutoModelForCausalLM.from_pretrained(
model_id, torch_dtype=torch.bfloat16, device_map="auto"
)
model = PeftModel.from_pretrained(base, "./lora_adapter")
model = model.merge_and_unload()
model.eval()
tokenizer = AutoTokenizer.from_pretrained(model_id)
prompt = "患者男,58岁,2型糖尿病,eGFR 25,能否使用二甲双胍?"
messages = [
{"role": "system", "content": "你是一名专业的医疗问诊助手,请严格按JSON格式输出。"},
{"role": "user", "content": prompt},
]
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer(text, return_tensors="pt").to(model.device)
with torch.no_grad():
out = model.generate(**inputs, max_new_tokens=256, temperature=0.1, do_sample=False)
print(tokenizer.decode(out[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True))
base模型输出(截取):
二甲双胍主要通过肾脏排泄,肾功能不全患者需谨慎使用。建议根据eGFR调整剂量……
漏掉了“eGFR<30禁用”这一关键结论,且没有JSON结构。
微调后输出:
{
"diagnosis_suggestion": "eGFR 25属于重度肾功能不全,二甲双胍禁用。",
"drug_interaction": "建议改用胰岛素或格列喹酮等肾排泄少的降糖药。",
"risk_level": "high"
}
格式正确率从base的71%提升到98%,领域术语准确率(人工抽检320条test集)从62%提升到89%。
五、踩坑与优化
坑1:loss NaN。用transformers 4.45.1 + QLoRA时,lm_head被量化导致首次反向就NaN。降级到4.44.2解决。如果你非要用新版,显式设置bnb_4bit_quant_storage=torch.bfloat16并确保lm_head不被量化。
坑2:显存OOM。batch_size=2、seq=1024时,QLoRA峰值9.8GB,LoRA峰值18.6GB。如果你只有16GB卡,QLoRA是唯一选择。再不够就把max_seq_length降到768。
坑3:推理速度。QLoRA训练完的adapter合并到bf16 base后,推理速度和原模型一致(约42 tokens/s on 4090)。但如果你不合并、直接在4bit base上挂adapter,速度会掉到28 tokens/s左右,因为每次前向都要做反量化。
优化点:gradient_checkpointing=True能省约30%显存,但训练速度慢15%。我最终保留开启,因为省显存更关键。另外warmup_ratio=0.03比默认0.1更合适小数据集,warmup太长前期loss降得慢。
六、总结
回到最初的问题:7B模型微调,LoRA和QLoRA怎么选?
- 显存≥24GB:直接LoRA,bf16训练更稳,eval loss略低。
- 显存12-24GB:QLoRA,效果损失在可接受范围(<0.05 eval loss)。
- 显存<12GB:QLoRA + 减小r到8 + seq降到512,但效果会明显打折。
这次实践中,3200条数据、3 epoch、单卡4090,LoRA组总训练时长约2小时18分,QLoRA组约2小时47分(反量化有开销)。最终adapter文件只有155MB,部署时按需加载,非常灵活。
如果你的场景也是垂直领域+固定输出格式,LoRA基本是性价比最高的方案。别一上来就想着全量微调,先把数据和target_modules调对,效果提升比换方法明显得多。