LoRA 微调实战

从任务定义、数据准备、PEFT 配置到评测回归,完整走一遍用 LoRA 微调开源大模型的最小闭环。

难度

进阶

阅读时长

约 120 分钟

更新日期

2026/03/24

主题

高效微调 / LoRA / SFT / 训练工程

先修知识

PyTorch 基础Transformer Block 基础大模型训练流水线总览

学习目标

这篇教程不追求把所有微调框架都讲一遍,而是帮助你建立一条可执行的 LoRA 实战路径。读完后你应该能:

  1. 解释为什么 LoRA 能在显存和效果之间取得好平衡。
  2. 准备一份适合 SFT 的最小数据集,并统一模板格式。
  3. 使用 transformers + peft 写出可运行的 LoRA 训练脚本。
  4. 判断训练结果到底是“真的学到了”,还是只是记住了模板。

如果你已经读过 LoRA: Low-Rank Adaptation of Large Language Models,这篇文章会把论文里的低秩思想接到工程实践上。

为什么很多团队先做 LoRA,而不是全参数微调

全参数微调的直觉很简单:既然要适配新任务,就把整个模型都训练一遍。但在真实项目里,立刻全量更新往往并不是最划算的选择,因为它同时带来三类成本:

  • 参数量巨大,优化器状态和梯度占用显存很高。
  • 实验迭代慢,一次调参就要动整个模型。
  • 每个业务场景都维护一份全量权重,交付和版本管理都很重。

LoRA 的核心想法是:不直接改原始大矩阵,而是在旁边学习一个低秩更新量。这意味着底座模型仍然保持冻结,训练的只是少量新增参数。结果是:

  • 显存更省,单卡也能做小规模实验。
  • 切换任务更轻,一个底座可以挂多个 LoRA 适配器。
  • 出问题时更容易回滚,因为底座权重没有被直接改坏。

这也是为什么 LoRA 特别适合小团队、垂直场景和快速验证阶段。

先定义一个可控任务,而不是一上来就追求“通用变强”

LoRA 最容易失败的原因之一,是任务定义太散。一个合理的起点,通常具备三个特点:

  1. 输出风格明确,例如技术问答、客服回复、表格抽取。
  2. 评测标准清晰,例如是否答对、是否结构化、是否少幻觉。
  3. 数据规模可控,先做几十到几千条高质量样本,而不是盲目扩展。

例如我们可以设计一个“论文概念解释助手”:

  • 用户输入一个术语,例如“KV Cache”
  • 模型需要给出定义、工作原理、应用场景和常见误区
  • 输出格式固定为 Markdown 小节

这种任务的优点是:它足够具体,评测也容易人工判断。

第一步:把数据格式统一好

LoRA 微调本质上仍然是监督式训练,所以数据格式的一致性非常关键。一个常见的对话样本可以写成:

{
  "messages": [
    { "role": "system", "content": "你是一个严谨、简洁的中文大模型论文助手。" },
    { "role": "user", "content": "请解释什么是 MoE,并给出它和 Dense Transformer 的区别。" },
    { "role": "assistant", "content": "## 定义\nMoE(Mixture of Experts)是一种稀疏激活架构...\n\n## 与 Dense Transformer 的区别\n1. ..." }
  ]
}

这里最重要的不是字段名本身,而是三件事:

  • 同一任务尽量使用同一套 system 设定。
  • assistant 输出结构保持稳定,不要一条是段落、一条是 JSON、一条又变成列表。
  • 训练集和评测集使用相同模板,否则结果很容易被“提示词格式不一致”污染。

如果你还没有建立数据质检流程,建议配合阅读 SFT 数据构造与质量控制

第二步:选模型与训练目标时,要先算资源账

LoRA 虽然比全量微调轻,但并不意味着资源可以完全不看。你至少要先确认:

  • 底座模型多大,例如 3B、7B 还是 14B。
  • 是直接 FP16 训练,还是 4-bit QLoRA。
  • 显存预算是多少,是单卡 24GB、48GB,还是多卡环境。
  • 训练目标是“格式对齐”,还是“领域知识迁移”。

一个非常常见的起步组合是:

  • 模型:7B 左右开源指令模型
  • 训练方式:4-bit QLoRA 或 BF16 LoRA
  • batch:通过梯度累积扩展有效 batch
  • 训练轮数:1 到 3 epoch 起步

起步时不要把 ralphadropoutlearning rate 全部开到极端。先做一组稳妥配置拿基线,再逐步调整。

第三步:理解 LoRA 配置里最关键的几个参数

LoRA 常见配置看起来很多,但真正最关键的是下面几个:

target_modules

它决定你把低秩适配器插到哪些线性层。对 LLM 来说,常见位置是注意力投影层和 FFN 投影层,例如:

  • q_proj
  • k_proj
  • v_proj
  • o_proj
  • up_proj
  • down_proj

如果你只打在注意力层,更新更省,但任务适配能力可能不足;如果同时覆盖 FFN,表达能力会更强,但参数也会增加。

r

r 是低秩矩阵的秩,可以理解成适配器的容量。r 太小可能学不动,r 太大又会失去 PEFT 的轻量优势。实践里常从 8 / 16 / 32 开始试。

lora_alpha

它控制 LoRA 更新的缩放强度。可以把它理解成“低秩分支的放大倍率”,通常和 r 联动选择。

lora_dropout

小数据场景下,适当 dropout 有助于减少过拟合;如果数据本来就不多、任务又很格式化,完全不开或开很小也常见。

一个最小训练脚本

下面给出一份足够理解流程的最小代码。真实项目里你可以换成 trlaccelerateunsloth 或自研框架,但核心结构相似:

from datasets import load_dataset
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments
from peft import LoraConfig, get_peft_model
from trl import SFTTrainer

model_name = "Qwen/Qwen2.5-7B-Instruct"

tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=False)
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    torch_dtype="bfloat16",
    device_map="auto",
)

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"],
)

model = get_peft_model(model, lora_config)
dataset = load_dataset("json", data_files={"train": "train.jsonl", "eval": "eval.jsonl"})

trainer = SFTTrainer(
    model=model,
    train_dataset=dataset["train"],
    eval_dataset=dataset["eval"],
    args=TrainingArguments(
        output_dir="outputs/lora-paper-assistant",
        per_device_train_batch_size=2,
        gradient_accumulation_steps=8,
        learning_rate=2e-4,
        num_train_epochs=2,
        logging_steps=10,
        eval_steps=100,
        save_steps=100,
        bf16=True,
        report_to="none",
    ),
)

trainer.train()
model.save_pretrained("outputs/lora-paper-assistant/final")
tokenizer.save_pretrained("outputs/lora-paper-assistant/final")

这段代码背后的重点不是 API 细节,而是流程:

  1. 读入底座模型。
  2. 注入 LoRA 适配器。
  3. 加载统一格式数据。
  4. 训练并保留 adapter 权重。

如果你能把这四步讲清楚,说明已经掌握了 LoRA 实战主线。

第四步:训练时重点观察什么

很多人训练 LoRA 时,只看 loss 下降就结束了。但 loss 只是一个局部信号,还需要同时观察:

  • 训练集和评测集 loss 是否一起下降。
  • 输出是否越来越稳定,而不是越来越啰嗦。
  • 是否开始照抄训练模板,丢失泛化能力。
  • 某些关键格式位是否始终正确,例如标题层级、JSON 键名、拒答模板。

一个务实做法是:在训练前先准备 20 到 50 条小评测集,覆盖几类典型问题:

  • 训练集同分布样本
  • 近邻改写样本
  • 轻微越界样本
  • 明显不该回答的风险样本

如果模型只在第一类上表现很好,说明它更可能是在“记格式”,而不是“学行为”。

QLoRA 和 LoRA 的关系

很多时候你会同时听到 LoRA 和 QLoRA。两者并不是互斥关系:

  • LoRA:强调参数高效微调,只训练低秩适配器。
  • QLoRA:在 LoRA 的基础上,把底座模型以更低比特加载,进一步降低显存占用。

所以 QLoRA 可以理解成“更节省资源的 LoRA 实现路径”。当你的设备预算有限,但又想做 7B 或更大模型实验时,它非常有吸引力。若你想补这部分基础,可以继续看 模型量化入门:INT8、INT4、GPTQ 与 AWQ

评测时不要只问“答得像不像”,还要问“有没有副作用”

LoRA 微调后常见的表面成功是:

  • 风格统一了
  • 领域术语更多了
  • 更愿意按格式输出了

但它也可能带来副作用:

  • 拒答能力变差
  • 通用问答能力下降
  • 回答变得过于模板化
  • 喜欢编造领域细节来迎合任务风格

因此,评测至少要分成三层:

  1. 目标任务评测:是否更符合产品场景。
  2. 通用退化评测:是否把底座原有能力压坏。
  3. 风险行为评测:是否更容易幻觉、越界或过度自信。

一个好的 LoRA 项目,不是“某个垂直 benchmark 变高”,而是“目标收益明显,副作用可控”。

常见误区

1. 以为 LoRA 不需要认真做数据

LoRA 省的是参数,不是数据质量要求。如果样本格式混乱、标签不稳、问题定义含糊,LoRA 一样会学歪。

2. 只调 r,不检查 target_modules

很多效果问题不是秩不够,而是适配器插错层、插得太少,或者根本没有覆盖任务敏感模块。

3. 训练集很小却跑很多轮

小数据 + 多轮数最容易过拟合。比起盲目增加 epoch,更应该先看输出退化趋势和验证集表现。

4. 只保存合并后的大权重,不保存 adapter

LoRA 最有价值的工程优势之一就是 adapter 轻量。如果只留合并后权重,就丢掉了版本切换和复用上的灵活性。

练习与思考题

  1. 为什么说 LoRA 解决的是“更新成本”问题,而不是“数据成本”问题?
  2. 如果你的任务重点是格式化输出,target_modules 更适合优先覆盖哪些层?为什么?
  3. 在什么情况下,你会优先选择 QLoRA,而不是普通 BF16 LoRA?
  4. 如果目标任务效果提升了,但通用能力明显下降,你会先从数据、超参数还是评测设计入手排查?

延伸阅读

相关阅读

从相近主题继续深入,建立连续学习链路。