一、问题背景:为什么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调对,效果提升比换方法明显得多。