为什么不全量微调
拿到一个开源大模型,想让它学会你公司的专业术语和业务流程,最直接的想法是把全部参数都重新训一遍。但全量微调的代价非常高:一个 7B 模型全量训练,光是优化器状态就要占几十 GB 显存,加上梯度和模型权重,单张显卡根本跑不动。就算你有多卡服务器,训练一次的电费和时间成本也不低。
参数高效微调(PEFT)换了个思路:冻结原始模型的大部分参数,只训练一小部分额外的参数。这样做的前提假设是——大模型已经学到了通用语言能力,领域适配只需要动很小一部分参数就能完成。实际效果证明这个假设基本成立:用不到百分之一的可训练参数,就能达到接近全量微调的效果。
PEFT 的价值不是「效果差不多」,而是把微调从需要多卡集群的实验,变成了一张消费级显卡就能跑的日常任务。
- 全量微调更新所有参数,效果好但显存和算力成本极高。
- LoRA冻结原模型,只训练低秩矩阵,参数量减少到原来的百分之一。
- QLoRA在 LoRA 基础上把原模型量化到 4-bit,进一步降低显存。
- PEFT是一族方法的统称,LoRA 是其中最常用的一种。
LoRA 的核心思想
LoRA 的想法很巧妙:既然原始权重矩阵已经很好了,我们不需要直接改它,而是在旁边加一个小的「旁路矩阵」。原始权重保持冻结,只训练这个旁路矩阵。推理时把旁路矩阵加回原始权重,效果等同于微调过的模型,但训练时只需要更新旁路的参数。
数学上,原始权重矩阵是 d×d 的大矩阵,LoRA 用两个小矩阵 A 和 B 来近似它的变化:A 是 r×d,B 是 d×r,其中 r 远小于 d。这样可训练参数量从 d×d 降到了 2×r×d,通常只有原来的百分之一甚至更少。
# LoRA 的核心公式
# 原始权重 W (d x d) 冻结不变
# 旁路: W + B @ A, 其中 A (r x d), B (d x r)
# r 通常取 8 到 64,远小于 d
# 训练时只更新 A 和 B
# 推理时合并: W_merged = W + B @ A
# 合并后推理速度和原模型一样,没有额外开销
实际使用时,你不需要自己实现 LoRA。Hugging Face 的 PEFT 库已经把它封装好了,只要几行代码就能给模型挂上 LoRA 适配器。训练完之后,你得到的不是一个完整的新模型,而是一个很小的适配器文件(几十 MB),可以随时挂载到不同的基础模型上。
QLoRA:量化加微调
LoRA 已经把可训练参数压得很小了,但原始模型本身的权重还是要占显存。一个 7B 模型 FP16 就要 14GB,一张 24GB 的卡刚好够。QLoRA 在 LoRA 的基础上更进一步:把原始模型权重量化到 4-bit,显存直接砍到原来的四分之一,这样一张 16GB 的卡就能微调 7B 模型。
量化不影响训练效果的原因是:LoRA 训练时梯度只流过旁路矩阵,原始量化权重只是前向传播用,不需要高精度。QLoRA 还引入了双重量化和分页优化器等技术,进一步把显存压到最低。
# 用 PEFT 库做 QLoRA 的最小示例
from peft import LoraConfig, get_peft_model
from transformers import BitsAndBytesConfig
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype="float16",
)
lora_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.05,
)
model = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen2.5-7B",
quantization_config=bnb_config,
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters() # 通常显示 0.1% 左右
QLoRA 让微调的门槛降到了消费级显卡。你不需要申请公司的 GPU 集群,自己的游戏显卡就能跑领域微调实验。这也是为什么这两年垂直领域大模型应用爆发的原因之一。
其他 PEFT 方法速览
LoRA 是目前最流行的 PEFT 方法,但不是唯一的。了解其他方法有助于在不同场景下做选择。
| 方法 | 思路 | 特点 |
|---|---|---|
| LoRA | 旁路低秩矩阵 | 最常用,效果稳定,可合并 |
| Prefix Tuning | 在输入前加可训练前缀 | 参数极少,但效果略逊 |
| P-Tuning | 连续向量提示 | 适合分类和抽取任务 |
| Adapter | 每层加小瓶颈模块 | 推理有额外延迟 |
大多数场景下直接选 LoRA 就够了。它的生态最成熟、工具链最完善、效果也最稳定。除非你有特殊的显存或延迟要求,否则不需要折腾其他方法。
数据准备与训练流程
微调效果好不好,七分在数据三分在调参。准备数据时要注意几点:格式统一成「指令-输入-输出」的对话结构;数据量不需要太大,几百到几千条高质量样本就够;一定要覆盖你真实业务里的典型问题,不要拿通用对话数据凑数。
- 质量优先一百条精标数据胜过一千条噪声数据。
- 覆盖典型把业务里最常见的问题都覆盖到,长尾问题留给 RAG。
- 格式统一所有样本用同一种对话模板,不要混用格式。
# 典型的微调数据格式
{
"instruction": "根据用户问题回答报销政策",
"input": "出差住宿报销标准是多少?",
"output": "一线城市住宿标准为每晚 500 元,二线城市为 400 元。"
}
# 训练超参数参考
# learning_rate: 1e-4 到 5e-4(比全量微调高 10 倍左右)
# num_epochs: 3 到 5
# batch_size: 看显存,配合梯度累积
# lora_r: 8 到 64,任务越复杂 r 越大
常见问题与调参经验
微调时最常遇到的几个问题:模型学不到东西,loss 降不下去——多半是数据格式不对或者学习率太低;模型过拟合,只会背训练集——数据量太少或者 epoch 太多;模型变得很死板,不会灵活回答——学习率太高或者训练数据太单一。
调参的基本顺序是:先确认数据格式正确,再从默认超参跑一版,然后根据 loss 曲线判断是欠拟合还是过拟合,针对性调学习率和 epoch。不要一上来就改一堆参数,每次只改一个变量。
微调后的合并与部署
训练完成后,你得到一个 LoRA 适配器文件。部署时有两种方式:一种是推理时动态挂载适配器,适合需要切换多个适配器的场景;另一种是把适配器合并回基础权重,生成一个完整模型,适合上线后不再切换的场景。
# 合并 LoRA 适配器
model = PeftModel.from_pretrained(base_model, "lora-adapter-path")
merged = model.merge_and_unload()
merged.save_pretrained("./merged-model")
# 合并后的模型就是一个标准的大模型文件
# 可以直接用 vLLM 或 Ollama 部署,和原模型没有区别
微调的终极目标不是「训出一个更好的模型」,而是「用最小的成本让模型在你的业务里更有用」。LoRA 把这件事的门槛降到了一张显卡。