AI 模型微调进阶:LoRA、QLoRA、训练评估和上线流程
wxk1991 Lv5

AI 模型微调进阶:LoRA、QLoRA、训练评估和上线流程

前两篇讲了两个基础问题:

1
2
什么时候该微调
微调数据集怎么准备

到了第三篇,就可以进入工程层面了。

如果你微调的是开源大模型,最常见的路线不是全量微调,而是参数高效微调:

1
LoRA / QLoRA / PEFT

它们的目标很现实:

1
2
3
不要训练整个大模型
只训练一小部分可插拔参数
尽量降低显存、存储和训练成本

这篇文章讲一个从训练到上线的完整思路。

不是追求论文细节,而是回答工程上真正会遇到的问题。


一、为什么很少直接全量微调

全量微调的意思是:

1
把模型所有参数都参与训练。

这听起来最直接。

但对大模型来说,成本很高:

  • 显存压力大
  • 训练速度慢
  • checkpoint 很大
  • 每个任务都要保存一份完整模型
  • 容易过拟合
  • 部署和回滚成本高

如果你只是想让模型适配一个客服分类、固定 JSON 输出、内部写作风格,没必要动整个模型。

这就是 LoRA 这类方法流行的原因。


二、LoRA 的直觉理解

LoRA 的核心思想是:

1
2
3
冻结原始模型权重
在部分层旁边插入小的可训练矩阵
训练时只更新这些小矩阵

你可以把它理解成:

1
2
基础模型保持不动
为某个任务加一个轻量适配器

这样有几个好处:

  • 训练参数少
  • 显存需求更低
  • 训练更快
  • 每个任务只保存 adapter
  • 可以切换不同 adapter
  • 对基础模型破坏更小

LoRA 论文里强调,它通过低秩矩阵来近似权重更新,从而显著减少可训练参数。

工程上你不一定要先懂完整数学。

先理解这句话就够了:

1
LoRA 不是重新训练整个模型,而是在模型旁边训练一组小插件。

三、QLoRA 又是什么

QLoRA 可以粗略理解为:

1
量化基础模型 + LoRA 训练 adapter

也就是把基础模型用更低位宽加载,进一步降低显存占用,然后训练 LoRA adapter。

它适合:

  • 单卡资源有限
  • 想微调 7B、14B、32B 这类模型
  • 训练成本敏感
  • 任务不需要全量微调

但也要注意:

1
显存省了,不代表训练就没有成本。

你仍然要处理数据、tokenizer、上下文长度、batch size、学习率、评估和上线。

QLoRA 只是降低了门槛,不会替你解决数据质量问题。


四、一个典型训练流程

开源模型微调可以按这个流程走:

1
2
3
4
5
6
7
8
9
10
1. 选择基础模型
2. 准备训练集和评估集
3. 统一样本格式
4. 加载 tokenizer
5. 加载基础模型
6. 配置 LoRA
7. 开始 SFT 训练
8. 保存 adapter
9. 跑固定 eval
10. 合并或挂载 adapter 部署

这里最容易出问题的是第 2、3、9 步。

也就是:

1
2
数据格式
评估方式

工具本身反而不是最难的。


五、示意代码:Hugging Face + PEFT + TRL

下面是一个简化示例,只展示结构。

真实项目里你还要处理数据读取、模板格式、显存配置、日志、checkpoint、评估脚本等。

1
pip install -U transformers datasets peft trl accelerate bitsandbytes

训练代码示意:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
from datasets import load_dataset
from peft import LoraConfig
from transformers import AutoModelForCausalLM, AutoTokenizer
from trl import SFTConfig, SFTTrainer

model_name = "your-base-model"

dataset = load_dataset("json", data_files={
"train": "train.jsonl",
"validation": "valid.jsonl",
})

tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
model_name,
device_map="auto",
trust_remote_code=True,
)

peft_config = LoraConfig(
r=16,
lora_alpha=32,
lora_dropout=0.05,
target_modules=["q_proj", "v_proj"],
task_type="CAUSAL_LM",
)

training_args = SFTConfig(
output_dir="./outputs/my-lora",
num_train_epochs=3,
per_device_train_batch_size=2,
gradient_accumulation_steps=8,
learning_rate=2e-4,
logging_steps=10,
eval_strategy="steps",
eval_steps=100,
save_steps=100,
)

trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
train_dataset=dataset["train"],
eval_dataset=dataset["validation"],
peft_config=peft_config,
args=training_args,
)

trainer.train()
trainer.save_model("./outputs/my-lora/final")

这段代码不是万能模板。

不同模型的 target_modules 可能不一样。

不同版本的 TRL / Transformers 参数名也可能变化。

正式项目里,不要复制完就直接跑生产训练。先用 20 条样本跑通最小流程。


六、几个重要参数怎么理解

1. r

r 是 LoRA 的秩。

可以粗略理解为 adapter 的表达能力。

越大,参数越多,可能能力更强,但也更占资源、更容易过拟合。

常见可以从:

1
8、16、32

开始试。

不要一上来就拉很大。

2. lora_alpha

lora_alpha 会影响 LoRA 更新的缩放。

常见会配合 r 使用。

比如:

1
2
r = 16
lora_alpha = 32

不用一开始就纠结它。

先固定一组常见配置,把注意力放在数据和 eval。

3. lora_dropout

lora_dropout 用来降低过拟合风险。

数据少时可以适当保留。

如果数据非常小,还训练很多 epoch,模型很容易记住样本。

4. learning_rate

学习率太大,模型可能学坏。

学习率太小,训练变化不明显。

LoRA 微调常见会比全量微调学习率更大一些,但具体要看模型、数据和任务。

不要靠感觉。

看验证集和固定 eval。

5. epoch

epoch 不是越多越好。

数据少时,多跑几轮很容易过拟合。

一个实用做法:

1
2
3
每个 epoch 保存 checkpoint
用同一套 eval 比较
选验证效果最好的一版

不要默认最后一个 checkpoint 最好。


七、评估不要只看 loss

训练 loss 降低,不代表业务效果一定变好。

微调评估至少要看:

  • 固定测试集准确率
  • 输出格式成功率
  • JSON 解析成功率
  • 安全拒答是否稳定
  • 幻觉是否增加
  • 老任务是否退化
  • 延迟和成本是否可接受

比如一个 JSON 抽取任务,可以用脚本自动评估:

1
2
3
4
5
6
def score(pred, expected):
return {
"json_valid": is_valid_json(pred),
"category_ok": pred["category"] == expected["category"],
"risk_ok": pred["risk_level"] == expected["risk_level"],
}

同时还要人工抽查。

尤其是开放式生成任务,只靠自动指标很容易误判。


八、上线前要做 A/B 对比

微调模型上线前,不要直接全量替换。

更稳的方式是:

1
2
基础模型:baseline
微调模型:candidate

用同一批线上真实请求离线回放。

比较:

  • 成功率
  • 格式稳定性
  • 人工评分
  • 错误类型
  • 延迟
  • token 消耗
  • 失败样本

如果 candidate 明显更好,再灰度上线。

上线后继续观察。

不要因为“训练成功”就认为“业务成功”。

训练成功只说明流程跑完了。

业务成功要靠指标证明。


九、部署 adapter 还是合并模型

LoRA 训练后通常得到的是 adapter。

部署有两种方式:

1. 挂载 adapter

推理时加载基础模型,再加载 adapter。

优点:

  • 方便切换任务
  • 存储成本低
  • 回滚简单
  • 可以保留多个 adapter

缺点:

  • 推理服务需要支持 adapter
  • 管理上多一层复杂度

2. 合并 adapter 到基础模型

把 adapter 权重合并回模型权重,导出一个新模型。

优点:

  • 推理部署更简单
  • 不需要运行时管理 adapter

缺点:

  • 模型文件更大
  • 多任务切换不方便
  • 回滚和版本管理要更谨慎

我的建议:

1
2
开发和灰度阶段挂载 adapter
稳定后再考虑合并

如果你的推理框架原生支持 adapter,多数场景可以一直不合并。


十、版本管理一定要做好

微调产物不是一个文件。

至少要记录:

  • 基础模型版本
  • tokenizer 版本
  • 训练数据版本
  • 训练脚本版本
  • LoRA 配置
  • 超参数
  • checkpoint
  • eval 结果
  • 上线时间
  • 回滚方式

可以建立一个训练记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
run_id: ft-20260623-001
base_model: your-base-model
dataset_version: customer-support-v3
method: lora
lora:
r: 16
alpha: 32
dropout: 0.05
training:
epochs: 3
learning_rate: 0.0002
eval:
json_valid_rate: 0.992
category_accuracy: 0.914

没有版本记录,模型上线后出了问题,你会很难定位:

1
2
3
4
是数据变了?
参数变了?
基础模型变了?
还是推理模板变了?

十一、常见失败原因

1. 数据太少或太脏

这是最常见原因。

不要先怪 LoRA。

先看数据。

2. prompt 模板和训练模板不一致

训练时一种格式,上线时另一种格式,效果会明显下降。

3. 只看训练集,不看 holdout

模型把训练样本背熟了,不代表能处理新输入。

4. 训练太久导致过拟合

数据少时尤其明显。

表现是训练集很好,验证集和真实输入变差。

5. 忘记比较基础模型

如果微调模型只是“看起来不错”,但没有超过基础模型,那这次微调就没有意义。

微调不是为了拥有一个自定义模型。

微调是为了在明确指标上超过 baseline。


十二、我的建议

开源模型微调可以按这个策略起步:

1
2
3
4
5
先选一个小而明确的任务
先用 50 到 200 条高质量样本跑通流程
先做 LoRA,不要上来全量微调
先用固定 eval 比较 baseline 和 candidate
先灰度,不要直接全量上线

真正成熟的微调流程,不是一次训练脚本。

而是一整套闭环:

1
2
3
4
5
6
7
8
数据版本
训练配置
评估集
checkpoint
部署方式
监控指标
回滚策略
错误样本回流

微调越往后,越不像“模型技术”,越像“工程系统”。

把这个系统搭好,比纠结某个单独参数更重要。


参考资料