"我能不能把开源模型微调成我的领域专家?" —— 几乎每个企业接触 LLM 后都会问这个问题。但全参数微调一个 70B 模型要 8 张 A100,普通团队负担不起。LoRA(Low-Rank Adaptation)和它的量化版本 QLoRA 解决了这个问题 —— 只训练几百万额外参数,就能让模型在特定领域表现接近全量微调。这篇文章把 LoRA 的原理、代码、数据准备、效果评估一次讲透。
问题:全量微调太贵
假设你想把 Llama 3 70B 微调成"医疗问诊专家"。全参数微调的成本:
- 显存:70B × 16(fp16) + 70B × 16(梯度) + 70B × 16(优化器) ≈ 1.6TB。
- 硬件:至少 16 张 H100,几十万美元。
- 训练时间:几天到几周。
对中小团队这是非选项。LoRA 把这个成本降到 1/100 甚至更低。
LoRA 的核心思想
2021 年微软论文《LoRA: Low-Rank Adaptation of Large Language Models》提出:微调时模型权重的"更新量"通常具有低秩结构。即原始权重矩阵 W ∈ R^{d×k} 的更新 ΔW 可以分解成两个小矩阵的乘积:
W_new = W_original + ΔW
ΔW ≈ B × A where A ∈ R^{r×k}, B ∈ R^{d×r}, r << min(d, k)
原始矩阵可能有几百万个参数,但 A 和 B 加起来只有几千几万。训练时只更新 A 和 B,原始 W 冻结。推理时把 BA 加到 W 上即可,无额外计算开销。
# LoRA 数学示意
import torch.nn as nn
class LoRALinear(nn.Module):
def __init__(self, original_linear, r=8, alpha=16):
super().__init__()
self.original = original_linear # 冻结
for p in self.original.parameters():
p.requires_grad = False
d_in, d_out = original_linear.in_features, original_linear.out_features
self.lora_A = nn.Linear(d_in, r, bias=False)
self.lora_B = nn.Linear(r, d_out, bias=False)
nn.init.zeros_(self.lora_B.weight) # B 初始为 0,初始等于不变
self.scale = alpha / r
def forward(self, x):
return self.original(x) + self.scale * self.lora_B(self.lora_A(x))
关键超参:
- r(秩):LoRA 的核心维度。通常 4-64。越大越接近全量微调,但参数也越多。
- alpha:缩放系数,通常等于或两倍于 r。
- target_modules:对哪些层加 LoRA。常见做
q_proj和v_proj(注意力的 Q、V 投影),进阶可加全部 attention + FFN。
QLoRA:把模型量化到 4-bit 再 LoRA
LoRA 让训练参数少,但原始模型权重还是要加载到显存。70B fp16 仍要 140GB,普通显卡装不下。
QLoRA(2023)的解法:把原始模型量化到 4-bit 后冻结,LoRA adapter 仍然 fp16。这把模型加载显存降到 1/4,70B 模型在一张 A100 80GB 上能微调,32B 模型甚至能在 24GB RTX 4090 上微调。
# QLoRA 用法(transformers + peft + bitsandbytes)
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
model_name = "meta-llama/Llama-3.1-8B-Instruct"
# 1. 4-bit 量化配置
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4", # NormalFloat 4
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
)
# 2. 加载量化模型
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
device_map="auto",
)
model = prepare_model_for_kbit_training(model)
# 3. 添加 LoRA
lora_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# trainable params: 41,943,040 || all params: 8,072,204,288 || trainable%: 0.5196
训练参数从 80 亿降到 4000 万 —— 0.5% 的参数,显存需求也降到 1/16 左右。
准备训练数据
SFT(监督微调)数据格式通常是"指令 + 回答"对:
# 用 datasets 库准备
from datasets import Dataset
raw_data = [
{
"instruction": "病人主诉头痛三天,该怎么问诊?",
"output": "建议询问:1. 头痛的具体位置...2. 疼痛性质..."
},
{
"instruction": "心电图 ST 段压低 0.1mV 代表什么?",
"output": "ST 段压低 0.1mV 提示心肌缺血..."
},
# ... 至少 1000-5000 条
]
def format_prompt(example):
return {
"text": f"<|user|>{example['instruction']}<|assistant|>{example['output']}<|end|>"
}
dataset = Dataset.from_list(raw_data).map(format_prompt)
数据质量决定微调成败,几个原则:
- 数据要纯净:错误的回答会把模型带偏。审核每条数据。
- 数据要多样:覆盖该领域的不同场景、不同问法。
- 数据格式要一致:prompt 模板和你推理时用的格式完全一致。
- 不要太少:< 500 条数据微调几乎没效果。1k-10k 是常见范围。
- 不要太多:> 50k 条 LoRA 不够,需要全量微调。
训练循环
from transformers import TrainingArguments, Trainer
from trl import SFTTrainer
training_args = TrainingArguments(
output_dir="./medical-llama",
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4, # 等效 batch_size = 16
learning_rate=2e-4,
fp16=False,
bf16=True,
save_strategy="epoch",
logging_steps=10,
optim="paged_adamw_8bit", # 显存友好优化器
warmup_ratio=0.1,
lr_scheduler_type="cosine",
)
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=dataset,
tokenizer=tokenizer,
dataset_text_field="text",
max_seq_length=2048,
)
trainer.train()
# 保存 LoRA adapter(只有几十 MB)
model.save_pretrained("./medical-lora-adapter")
推理:加载基础模型 + LoRA Adapter
from peft import PeftModel
base_model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-3.1-8B-Instruct",
torch_dtype=torch.bfloat16,
device_map="auto",
)
model = PeftModel.from_pretrained(base_model, "./medical-lora-adapter")
# 推理
prompt = "<|user|>病人持续低烧两周,可能病因?<|assistant|>"
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
outputs = model.generate(**inputs, max_new_tokens=512)
print(tokenizer.decode(outputs[0]))
# 或者合并 LoRA 到基础模型,导出独立模型
merged = model.merge_and_unload()
merged.save_pretrained("./medical-llama-merged")
合并后的模型可以像普通模型一样用,推理框架(vLLM、TGI)都能直接加载。但合并后失去了"和基础模型共享"的优势 —— 一台机器装 N 个 LoRA adapter 用同一个基础模型,显存利用率最高。
评估:微调真的有用吗
微调最容易踩的坑是"训练 loss 在降,但实际表现没变好甚至变差"。必须有严格的评估:
1. 准备评估集
从训练数据里独立切出 100-500 条不参与训练,作为评估集。覆盖该领域的代表性问题。
2. 自动评估
def evaluate(model, eval_set):
scores = []
for item in eval_set:
response = generate(model, item["instruction"])
# 用更强的 LLM 当裁判
score = judge_llm.score(
question=item["instruction"],
reference=item["output"],
response=response,
)
scores.append(score)
return sum(scores) / len(scores)
before = evaluate(base_model, eval_set) # 4.2/5
after = evaluate(finetuned_model, eval_set) # 4.7/5
3. 人工评估
领域专家盲评 30-50 个样例。这是金标准。所有自动评估都要至少和人工评估对齐过一次。
4. 通用能力评估
微调可能让模型"越来越专一,通用能力下降"(catastrophic forgetting)。务必跑一遍 MMLU / GSM8K / HumanEval 等通用 benchmark,确认通用能力没退化太多。
常见坑
坑 1:数据太少。 < 500 条数据微调几乎看不出效果,只是 loss 在降。增加数据量,或换更高效的方法(prompt engineering + few-shot)。
坑 2:学习率太大。 LoRA 标准学习率 2e-4 比全量微调(1e-5)大,但太大会让模型"忘掉"通用能力。从 1e-4 起步,看 loss 曲线调。
坑 3:Prompt 格式不一致。 训练时用 <|user|>...<|assistant|>,推理时用 ### Instruction: —— 效果会大幅下降。两边格式必须严格一致。
坑 4:训练数据"假"。用 ChatGPT 批量生成训练数据 → 模型微调后表现"像 ChatGPT 而不像专家"。真领域数据 > 合成数据,合成时也要让 GPT-4o / Claude 这类强模型生成,且要审核。
坑 5:过拟合。 Epochs 太多 / 数据少 → 训练集表现完美但泛化差。一般 LoRA 3 epoch 已经足够,看 eval loss 提前停止。
LoRA 之外的高效微调方法
- Prefix Tuning / Prompt Tuning:只训练"虚拟 prompt"。参数极少,效果通常不如 LoRA。
- P-Tuning v2:多层 prompt tuning。介于 prompt tuning 和 LoRA 之间。
- DoRA(Decomposed LoRA):LoRA 的进化版,把权重分解成方向和大小分别调,效果略好。
- Adapter Tuning:在每层插入小神经网络,LoRA 之前的主流方法,现在用得少。
- 全量微调 + DeepSpeed Zero-3:数据多 + 资源够的话效果最好,但工程复杂度高。
什么时候应该微调
这个判断比"怎么微调"更重要。决策树:
- 需求能用 Prompt Engineering 解决吗?能 → 别微调。
- 能用 RAG(给资料)解决吗?能 → 别微调。
- 需要的是风格 / 格式 / 领域术语的稳定遵循?这时微调有用。
- 需要的是知识本身?优先 RAG。微调能"记住"知识但效率低于 RAG。
- 有至少 1k 高质量训练数据?没有的话先收集数据。
2024-2025 年很多团队的微调项目失败,根本原因是"该用 RAG 的场景非要微调"。微调适合改"模型行为方式",不擅长"加新知识"。
RAG vs Fine-tuning vs Long Context:决策对比
这三种方法都能让 LLM "用新知识回答",选哪个直接决定项目成本和效果。完整对比:
维度 RAG 微调 长上下文
知识更新 实时(改数据库) 难(要重训练) 实时(每次塞)
准确性 高(可溯源) 中(可能遗忘) 中(lost in middle)
延迟 中(检索+生成) 低(直接生成) 高(token 多)
单次成本 低 低 高(token 巨多)
训练成本 无 中(LoRA几百刀) 无
扩展知识量 极大 中 小(受上下文限)
适合场景 知识库 / 文档 风格 / 格式 / 术语 临时小知识包
DPO:不需要奖励模型的 RLHF 替代
传统 RLHF 需要训练奖励模型 + PPO,工程复杂。2023 年提出的 DPO(Direct Preference Optimization)直接用偏好数据训练:
# 偏好数据格式
{
"prompt": "解释什么是 KMP 算法",
"chosen": "KMP 算法是一种字符串匹配算法,核心思想是利用已匹配的信息避免回溯...",
"rejected": "KMP 是个算法,用来匹配字符串的。"
}
# 训练
from trl import DPOTrainer
trainer = DPOTrainer(
model=sft_model,
ref_model=ref_model,
args=training_args,
train_dataset=preference_dataset,
beta=0.1,
)
trainer.train()
DPO 把 RLHF 简化到接近普通 SFT 的复杂度,效果在多个 benchmark 上和 PPO 持平甚至更好。2024 年起几乎所有开源对齐都用 DPO 或其变种(KTO、IPO)。这是想做"偏好对齐"的团队首选方案。
合成数据生成:数据不够怎么办
真实场景里高质量微调数据稀缺。用 GPT-4 / Claude / DeepSeek 生成合成数据是标配:
SYNTHESIS_PROMPT = """
你是一位资深 [医疗 / 法律 / 编程] 专家。
请基于下面的"主题",生成 5 组 (问题, 回答) 对。
要求:
- 问题要多样化:具体场景、概念解释、常见误区都涵盖
- 回答要专业准确,200-500 字
- 回答里要带具体例子或代码
主题:{topic}
输出 JSON 数组:
[{"question": "...", "answer": "..."}, ...]
"""
topics = ["TCP 握手", "数据库索引", "K8s Service", ...]
all_data = []
for t in topics:
data = json.loads(llm(SYNTHESIS_PROMPT.format(topic=t)))
all_data.extend(data)
# 关键:人工审核一遍,删错的、改不准的
audited = [d for d in all_data if review(d)]
合成数据能把 100 条种子扩展到几千几万条。但必须审核,合成错误也会被模型学。生产经验:种子数据精细打磨 100-500 条,合成扩展到 5k-20k,再审核一遍,效果最好。
写在最后
LoRA / QLoRA 让"定制专属领域 LLM"从"超级土豪游戏"变成"有几张消费级 GPU 就能做"。这是开源 LLM 生态繁荣的根本原因 —— 每个领域都能基于 Llama / Qwen / DeepSeek 微调出自己的版本,而不需要从头训。
给一个工程心得:微调是工程而不是科学。决定成败的不是"用什么 r、什么 lr",而是"数据质量、评估机制、是否真的该微调"。把这三件事做扎实,LoRA 的具体超参用默认值就能拿到 90% 的效果。把这三件事做不好,再调参也救不回来。下一篇我们把 AI 主题暂告一段落,进入系统设计的第一批文章。
—— 别看了 · 2026