LoRA微调
LoRA(Low-Rank Adaptation,低秩适配)是目前最流行的参数高效微调方法。它只训练不到 1% 的参数,就能达到接近全量微调的效果。
1. LoRA 核心思想
预训练模型的权重矩阵 W 通常很大(比如 4096 × 4096)。LoRA 不直接更新 W,而是在它旁边加一对小矩阵 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. 安装依赖
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)
启动:
保存的 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"
不同模型架构的命名不同,可以打印模型结构查看:
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 独有的部署优势