第四讲:SFT实战——LoRA微调技术、QLoRA量化微调、SFT训练脚本编写、训练参数调优
各位同学,欢迎来到实战环节。前面几章我们把RLHF的理论框架讲透了,今天咱们直接撸袖子干——手把手带你跑通SFT(Supervised Fine-Tuning)的完整流程。这一章我会重点讲四个东西:LoRA微调、QLoRA量化微调、训练脚本怎么写、以及参数怎么调。嗯,都是我在项目里踩过坑之后总结出来的经验。
4.1 为什么需要LoRA?
先问一个问题:你手头有一个70B的大模型,想微调一下让它学会客服对话风格。你会怎么做?
传统做法是全参数微调。说白了就是把模型所有参数都更新一遍。但70B模型光参数就有700亿个,单卡A100(80G)连模型都装不下,更别说训练了。我刚开始做这个项目时,天真地试了一次全参数微调,结果OOM报错直接把我整懵了。
LoRA(Low-Rank Adaptation)就是来解决这个问题的。它的核心思想很简单:冻结原始模型参数,只训练一小部分低秩矩阵。你想想看,模型参数其实存在很大的冗余,我们不需要动全部参数,只需要在关键路径上插入一些可训练的“小补丁”就行。
LoRA的核心公式:
h = W₀x + ΔWx = W₀x + BAx
其中W₀是冻结的原始权重,B和A是低秩矩阵(r << d),训练时只更新B和A。
我在实际项目中,用LoRA微调一个13B的LLaMA模型,显存占用从原来的80G降到了16G左右。训练速度也快了将近10倍。说白了,LoRA就是让你用消费级显卡也能玩转大模型微调。
4.2 LoRA的关键参数与配置
LoRA有几个关键参数,我一个个说清楚:
| 参数 | 含义 | 推荐值 | 我的经验 |
|---|---|---|---|
| r | 低秩矩阵的秩 | 8-64 | r=16是甜点值,再大收益递减 |
| alpha | 缩放系数 | 16-32 | 一般设为r的2倍 |
| target_modules | 应用LoRA的模块 | q_proj, v_proj | 我习惯加上k_proj和o_proj |
| dropout | 随机丢弃率 | 0.05-0.1 | 数据量小时用0.1防过拟合 |
小技巧:我个人习惯把LoRA应用到所有attention层的q_proj和v_proj上。为什么?因为attention层是模型理解语义的关键路径,改这里效果最明显。我曾经试过只改feed-forward层,结果效果差了一大截。
4.3 QLoRA:量化+LoRA的黄金组合
LoRA已经帮我们省了不少显存,但如果你只有一张RTX 4090(24G显存),想微调33B甚至70B的模型怎么办?这时候QLoRA就派上用场了。
QLoRA = 4-bit量化 + LoRA。说白了,先把模型权重压缩到4-bit(原来32-bit的1/8),然后再在上面挂LoRA。这样模型体积直接缩到原来的1/8,显存占用自然就下来了。
我记得第一次跑QLoRA时,用一张4090微调了LLaMA-2-70B,显存占用才18G左右。当时我同事都惊呆了,以为我在吹牛。
注意:量化会带来一定的精度损失。我做过对比实验:4-bit QLoRA微调后的模型,在MMLU基准上比全精度LoRA低了约1-2个百分点。但如果你用的是NF4(NormalFloat4)量化格式,这个损失可以控制在0.5%以内。所以我的建议是:能用NF4就别用普通的4-bit。
QLoRA的配置示例:
from transformers import BitsAndBytesConfig
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-70b-hf",
quantization_config=bnb_config,
device_map="auto"
)
4.4 SFT训练脚本编写
好了,理论讲完了,咱们直接上代码。下面是我在实际项目中用的SFT训练脚本核心部分:
from transformers import TrainingArguments, Trainer
from peft import LoraConfig, get_peft_model
# 1. 配置LoRA
lora_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "v_proj", "k_proj", "o_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM"
)
# 2. 包装模型
model = get_peft_model(base_model, lora_config)
# 3. 训练参数
training_args = TrainingArguments(
output_dir="./sft_output",
per_device_train_batch_size=4,
gradient_accumulation_steps=8,
learning_rate=2e-4,
num_train_epochs=3,
logging_steps=10,
save_steps=500,
fp16=True,
optim="paged_adamw_8bit",
lr_scheduler_type="cosine",
warmup_ratio=0.03,
)
# 4. 启动训练
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
data_collator=data_collator,
)
trainer.train()
避坑指南:我曾经犯过一个低级错误——忘记设置gradient_accumulation_steps。结果batch_size设成4,显存直接爆了。后来我改成batch_size=4 + gradient_accumulation_steps=8,等效batch_size=32,显存稳稳的。你想想看,这个参数其实就是用时间换空间,多攒几个小batch再更新一次参数。
4.5 训练参数调优实战
参数调优是门手艺活。我总结了一套自己的调参流程,分享给你:
- 先定学习率:从1e-4开始,观察loss曲线。如果震荡厉害就降到5e-5,如果收敛太慢就提到3e-4。
- 再调batch size:显存允许的情况下尽量大。我一般用等效batch_size=32或64。
- 然后看warmup:warmup_ratio设0.03-0.1,让模型慢慢进入状态。
- 最后调LoRA的r:从16开始,如果欠拟合就升到32,如果过拟合就降到8。
为什么会这样?因为学习率是最敏感的参数,它决定了模型能不能收敛。batch size影响的是梯度估计的稳定性。warmup则是为了防止一开始梯度太大把模型搞崩了。
我的经验值:
- 7B模型:lr=2e-4, batch_size=32, r=16
- 13B模型:lr=1e-4, batch_size=16, r=16
- 70B模型(QLoRA):lr=1e-4, batch_size=8, r=8
4.6 本章知识体系
下面这张图是我画的SFT实战知识体系,帮你理清脉络:
这张图把SFT实战的四个核心模块串起来了。你从中心出发,沿着四个方向走,每个方向都有具体的知识点。我个人建议你按照这个结构来组织自己的学习路径,先搞懂LoRA原理,再上手QLoRA,然后写脚本跑实验,最后调参优化。
好了,这一章的内容就到这里。记住一句话:SFT不是玄学,是工程。把每个参数吃透,把每行代码写对,效果自然就出来了。
公众号:蓝海资料掘金营,微信deep3321