一、从“基座模型”到“可用助手”:为什么必须微调

上周接到一个内部需求:让模型能够理解“报销流程”相关指令,并给出符合公司制度的回答。直接调用Llama-2-7B-chat,效果惨不忍睹——模型要么答非所问,要么一本正经地编造流程。基座模型的知识截止于2023年初,且没有领域指令数据,根本无法应对垂直场景。

全量微调7B模型需要至少56GB显存(AdamW优化器状态+梯度+参数),我们手里的RTX 3090根本跑不动。于是采用LoRA(Low-Rank Adaptation)方案:冻结原模型参数,仅训练低秩分解矩阵。配合4bit量化(QLoRA),显存占用直接降到6.2GB,训练时间也缩短到2.5小时。

二、环境与版本:每一步都不能错

强烈建议使用以下版本组合,我试过多个组合,最终这套最稳:

# 核心依赖
torch==2.1.2+cu118
transformers==4.36.2
peft==0.7.1
bitsandbytes==0.41.3
datasets==2.16.1
accelerate==0.25.0

注意:bitsandbytes 0.41.3是支持RTX 30系卡的最后稳定版本,0.43+会出现奇怪的显存泄漏。CUDA 11.8,驱动版本≥520。

三、方案设计:QLoRA + 指令微调

整体架构:
- 基座模型:meta-llama/Llama-2-7b-hf(非chat版,避免预训练对话偏置)
- 量化:4bit NF4(NormalFloat4),double quantization开启
- LoRA配置:r=8, alpha=16, dropout=0.05,只作用于q_proj, v_proj
- 训练数据:3000条中文指令(80%通用指令,20%报销领域定制)
- 优化器:paged_adamw_8bit,学习率2e-4,cosine调度

为什么r=8?经过实验,r=16在C-Eval上提升仅0.3%,但训练时间增加40%。q_projv_proj是最敏感层,用LoRA官方论文的结论直接生效。

四、核心实现:数据准备与训练脚本

4.1 数据清洗与格式化

原始数据是CSV格式,包含instruction, input, output三列。清洗逻辑:

import pandas as pd
from datasets import Dataset

df = pd.read_csv('instructions.csv')
# 过滤空值及超长样本(超过512 token)
df = df.dropna(subset=['instruction', 'output'])
df = df[df['instruction'].apply(lambda x: len(x) [INST] {row['instruction']} [/INST] {row['output']} "
    }

dataset = Dataset.from_pandas(df).map(format_example)
dataset = dataset.train_test_split(test_size=0.05)

4.2 QLoRA训练配置

from transformers import AutoModelForCausalLM, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training

# 4bit量化配置
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True
)

model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-2-7b-hf",
    quantization_config=bnb_config,
    device_map="auto",
    use_cache=False
)

# 冻结原模型,准备kbit训练
model = prepare_model_for_kbit_training(model)

# LoRA配置
lora_config = LoraConfig(
    r=8,
    lora_alpha=16,
    target_modules=["q_proj", "v_proj"],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM"
)

model = get_peft_model(model, lora_config)
model.print_trainable_parameters()  # 仅约4.2M参数可训练,占总参数量0.06%

训练参数:per_device_train_batch_size=4gradient_accumulation_steps=8max_steps=500logging_steps=10。这里用max_steps而非num_epochs,方便监控loss曲线。

五、踩坑与优化:三个血泪教训

坑1:loss不下降且出现NaN
现象:前20步loss直接变成NaN。
原因:bnb_4bit_compute_dtype误设为float32,导致量化权重反传时溢出。
解决:改为bfloat16,同时将torch_dtype设置为bfloat16

坑2:显存不足但batch_size已经很小
现象:8G显存,batch_size=4直接OOM。
原因:use_cache=True导致缓存了所有层的KV状态。
解决:use_cache=False,显存从8.2GB降至6.2GB。

坑3:训练速度慢,每step要40秒
现象:3090跑7B模型,应该每step “报销餐费需要提供用餐发票,发票上应有餐厅名称、日期和金额。建议保留水单以备审计。”

微调后:

“根据《公司财务报销制度》第4.2条,报销餐费需提交:1)增值税普通发票或电子发票;2)用餐水单(需包含餐厅名称、就餐日期、明细菜品);3)OA系统填写的《费用报销单》,并在摘要中注明事由。若金额超过500元,需额外提供部门经理审批邮件。”

问题2:“用一句话说明什么是LoRA”

微调前:(英文回答)LoRA是一种低秩适应方法...
微调后:(中文回答)LoRA通过冻结预训练模型参数,仅训练两个低秩矩阵来模拟权重更新,从而将微调参数量减少99%以上。

6.3 客观评测数据

指标 微调前 微调后
C-Eval (5-shot) 34.2 41.8
中文流畅度(人工评分1-5) 2.3 4.1
领域指令准确率(50条测试) 18% 74%
推理速度(tokens/s) 12.3 12.1

推理速度几乎无损耗,因为LoRA参数在推理时可直接合并回原模型。

七、总结:低资源微调的可行性验证

QLoRA让7B模型的微调门槛降到了消费级显卡。关键收益:
1. 显存需求降低约80%(从全量微调的56GB降至6.2GB)
2. 训练时间可控(2.5小时内完成)
3. 模型能力提升显著(C-Eval +7.6分)

踩坑的核心就一句话:版本锁死,量化类型选对,手动控制device_map。如果后续要微调13B模型,建议将r降至4,并使用gradient_checkpointing=True(额外节省2GB显存)。

完整训练脚本已上传到我的GitHub仓库(链接略),欢迎交流。下一步我计划用SFT+DPO做对齐微调,看能否把领域回答的“合规性”再拉高一个档次。