跳转至

LoRA微调

LoRA(Low-Rank Adaptation,低秩适配)是目前最流行的参数高效微调方法。它只训练不到 1% 的参数,就能达到接近全量微调的效果。

1. LoRA 核心思想

预训练模型的权重矩阵 W 通常很大(比如 4096 × 4096)。LoRA 不直接更新 W,而是在它旁边加一对小矩阵 A 和 B:

原始计算:    h = W · x
LoRA 后:    h = W · x + B · A · x
        训练时只更新 A 和 B

其中: - A 的形状是 r × d(小) - B 的形状是 d × r(小) - r 是 LoRA 的"秩"(rank),通常取 8、16、32

如果 d=4096, r=16,那么:

  • 原始 W 参数量:4096 × 4096 ≈ 1678 万
  • LoRA AB 参数量:16 × 4096 × 2 ≈ 13 万

参数量减少了 128 倍

2. LoRA 的优势

方面 优势
显存 大幅降低(不存梯度和优化器状态)
存储 只保存 LoRA 权重(几十 MB),原模型不变
速度 训练更快,反向传播参数少
多任务 一个基座 + 多个 LoRA 适配器,按需切换
效果 接近全量微调,多数任务无明显差距

3. 安装依赖

pip install transformers datasets accelerate peft trl

4. LoRA 训练完整脚本

新建 train_lora.py

import torch
from datasets import load_dataset
from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    TrainingArguments,
    Trainer,
    DataCollatorForLanguageModeling,
)
from peft import LoraConfig, get_peft_model, TaskType

MODEL_NAME = "Qwen/Qwen2.5-1.5B-Instruct"
DATA_PATH = "translate_data.jsonl"
OUTPUT_DIR = "./output_lora"
MAX_LENGTH = 512

# === 加载模型和分词器 ===
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
model = AutoModelForCausalLM.from_pretrained(
    MODEL_NAME,
    torch_dtype=torch.bfloat16,
    device_map="auto",
)

# === 配置 LoRA ===
lora_config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    r=16,                          # LoRA 秩
    lora_alpha=32,                 # 缩放因子(通常 = 2r)
    lora_dropout=0.05,
    bias="none",
    target_modules=[               # 注入 LoRA 的模块
        "q_proj", "k_proj", "v_proj", "o_proj",
        "gate_proj", "up_proj", "down_proj",
    ],
)

model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# trainable params: ~18M || all params: ~1.5B || trainable%: ~1.2

# === 数据预处理 ===
def preprocess(example):
    user_content = example["instruction"]
    if example.get("input"):
        user_content += "\n" + example["input"]

    messages = [
        {"role": "user", "content": user_content},
        {"role": "assistant", "content": example["output"]},
    ]
    text = tokenizer.apply_chat_template(messages, tokenize=False)
    encoded = tokenizer(text, truncation=True, max_length=MAX_LENGTH, padding=False)
    encoded["labels"] = encoded["input_ids"].copy()
    return encoded

dataset = load_dataset("json", data_files=DATA_PATH, split="train")
dataset = dataset.map(preprocess, remove_columns=dataset.column_names)
dataset = dataset.train_test_split(test_size=0.1, seed=42)

collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)

# === 训练参数 ===
training_args = TrainingArguments(
    output_dir=OUTPUT_DIR,
    num_train_epochs=3,
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,
    learning_rate=2e-4,            # LoRA 用大学习率(比全量微调大 10 倍)
    warmup_ratio=0.05,
    lr_scheduler_type="cosine",
    bf16=True,
    logging_steps=10,
    eval_strategy="epoch",
    save_strategy="epoch",
    save_total_limit=2,
    report_to="tensorboard",
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=dataset["train"],
    eval_dataset=dataset["test"],
    data_collator=collator,
    tokenizer=tokenizer,
)

trainer.train()

# === 保存 LoRA 权重 ===
model.save_pretrained(OUTPUT_DIR)
tokenizer.save_pretrained(OUTPUT_DIR)

启动:

python train_lora.py

保存的 LoRA 权重通常只有几十 MB(相比原模型几个 GB)。

5. LoRA 关键参数详解

r(秩)

控制 LoRA 矩阵的"宽度":

r 参数量 适用场景
4-8 极少 简单任务、风格调整
16-32 适中 大多数任务的默认选择
64-128 较多 复杂任务、领域适配

r 越大效果不一定越好,先从 16 开始尝试

lora_alpha

缩放因子,控制 LoRA 的"强度"。实际生效的缩放是 alpha / r。经验:

  • alpha = 2 * r(最常见,1:2 比例)
  • alpha = r(保守)
  • alpha = 4 * r(激进,效果更强但可能不稳定)

target_modules

决定在哪些层注入 LoRA。常见选择:

# 只注入注意力层(最早的 LoRA 论文做法,参数最少)
target_modules = ["q_proj", "v_proj"]

# 全部注意力(推荐起点)
target_modules = ["q_proj", "k_proj", "v_proj", "o_proj"]

# 注意力 + MLP(QLoRA 论文推荐,效果最好)
target_modules = ["q_proj", "k_proj", "v_proj", "o_proj",
                  "gate_proj", "up_proj", "down_proj"]

# 让 PEFT 自动找所有线性层
target_modules = "all-linear"

不同模型架构的命名不同,可以打印模型结构查看:

for name, _ in model.named_modules():
    if "proj" in name:
        print(name)

lora_dropout

LoRA 内部的 dropout,防止过拟合。常用 0.05 ~ 0.1。

6. 加载 LoRA 权重做推理

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel

BASE_MODEL = "Qwen/Qwen2.5-1.5B-Instruct"
LORA_DIR = "./output_lora"

# 加载基座模型
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL)
model = AutoModelForCausalLM.from_pretrained(
    BASE_MODEL,
    torch_dtype=torch.bfloat16,
    device_map="auto",
)

# 加载 LoRA 权重
model = PeftModel.from_pretrained(model, LORA_DIR)
model.eval()

# 推理
messages = [
    {"role": "user", "content": "将以下英文翻译成中文\nThe weather is nice today."}
]
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer(text, return_tensors="pt").to(model.device)
outputs = model.generate(**inputs, max_new_tokens=128, do_sample=False)
print(tokenizer.decode(outputs[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True))

7. 合并 LoRA 权重到基座模型

如果你想得到一个"完整"的微调模型(方便部署),可以将 LoRA 权重合并到基座中:

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel

BASE_MODEL = "Qwen/Qwen2.5-1.5B-Instruct"
LORA_DIR = "./output_lora"
MERGED_DIR = "./output_merged"

tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL)
model = AutoModelForCausalLM.from_pretrained(
    BASE_MODEL,
    torch_dtype=torch.bfloat16,
)

model = PeftModel.from_pretrained(model, LORA_DIR)
model = model.merge_and_unload()    # 合并 LoRA 权重

model.save_pretrained(MERGED_DIR)
tokenizer.save_pretrained(MERGED_DIR)

合并后的模型可以直接用 AutoModelForCausalLM.from_pretrained 加载,跟普通模型一样。

8. 切换多个 LoRA 适配器

LoRA 一个很酷的能力:一个基座模型可以挂多个适配器,按场景切换:

from peft import PeftModel

model = AutoModelForCausalLM.from_pretrained(BASE_MODEL, ...)

# 加载第一个适配器
model = PeftModel.from_pretrained(model, "./lora_translate", adapter_name="translate")

# 加载第二个适配器
model.load_adapter("./lora_summarize", adapter_name="summarize")

# 切换适配器
model.set_adapter("translate")
# 推理...

model.set_adapter("summarize")
# 推理...

这样一台机器就能服务多个微调任务,省显存又灵活

9. 常见问题与调优

问题 1:LoRA 训练 loss 不降

  • 检查 target_modules 是否覆盖了关键层(至少要包含 q_proj, v_proj
  • 学习率不要太小,LoRA 用 1e-4 ~ 5e-4
  • 检查 lora_alpha / r 是否过小

问题 2:训练效果不如全量微调

  • 增大 r(16 → 32 → 64)
  • 增加 target_modules 到所有线性层
  • 增加训练数据或 epoch

问题 3:模型变得很奇怪、说话颠三倒四

  • 学习率过大或训练过头
  • 数据质量有问题(重复、错误、格式不一致)
  • 减小 lora_alpha 或减少 epoch

总结

  • LoRA 通过低秩矩阵 A、B 实现参数高效微调,只训练 < 1% 参数
  • 关键参数:r(默认 16)、lora_alpha(默认 2r)、target_modules(推荐全部线性层)
  • 学习率要比全量微调大 10 倍左右(1e-4 ~ 5e-4)
  • 推理时用 PeftModel.from_pretrained 加载,可合并为完整模型
  • 多 LoRA 适配器切换是 LoRA 独有的部署优势

评论