
深入剖析全量微调、LoRA、QLoRA 等主流方法,手把手带你完成一个完整的微调项目
大型语言模型(LLM)在预训练阶段从海量文本中学习了丰富的语言知识和世界知识,但它们本质上是一个“通才”——能回答通用问题,但在特定领域(如医疗、法律、金融)往往表现欠佳,且无法很好地遵循特定指令或输出格式。微调(Fine-tuning) 就是让通用模型“专精”的关键技术。
随着模型参数规模从数十亿跃升至千亿级,全量微调的成本急剧攀升,催生了参数高效微调(PEFT)方法的繁荣。本文将系统性地梳理微调的技术谱系,并以一个完整的代码示例演示如何用 LoRA 微调一个 7B 模型,同时涵盖数据构建、训练优化、评估与部署等工程细节。
类型 | 更新参数量 | 显存需求 | 适用场景 |
|---|---|---|---|
全量微调(Full Fine-tuning) | 100% | 极高(如 7B 需 60GB+) | 数据充足、算力充裕的研究/企业场景 |
参数高效微调(PEFT) | <1%~5% | 低(7B 可降至 16GB 以内) | 绝大多数应用场景,尤其资源受限时 |
提示微调(Prompt Tuning) | 极少量(嵌入层) | 极低 | 多任务切换、快速实验 |
核心思想:冻结预训练权重,在 Transformer 层的注意力模块(Q、K、V、O)旁路添加可训练的低秩分解矩阵。假设原始权重为 W∈Rd×kW∈Rd×k,LoRA 引入 W′=W+ΔW=W+BAW′=W+ΔW=W+BA,其中 B∈Rd×rB∈Rd×r,A∈Rr×kA∈Rr×k,且 r≪min(d,k)r≪min(d,k)。训练时只更新 A,BA,B。
优点:
秩 r 的选择:通常 4~64,r 越大表达能力越强,但显存和过拟合风险也增加。推荐从 8 或 16 开始尝试。
QLoRA 在 LoRA 基础上引入4-bit 量化的基座模型,同时使用 NF4 数据类型和双重量化技术,进一步将显存需求降低至原有 LoRA 的 1/3 左右。例如,用 QLoRA 微调 70B 模型仅需 48GB 显存。其关键技术包括:
本节以 Meta LLaMA 2 7B 为基座,使用 QLoRA 进行微调,目标是对抗法律领域的问答指令。我们将使用 Hugging Face 生态的 transformers、peft、bitsandbytes、trl 等库。
pip install torch transformers accelerate peft bitsandbytes datasets trl scikit-learn我们构造一个简易的法律指令数据集(JSON Lines 格式),每条包含 instruction(指令)、input(可选上下文)、output(期望回答)。
{"instruction": "什么是合同违约?", "input": "", "output": "合同违约是指合同一方或双方未按合同约定履行义务,包括完全不履行、迟延履行或不完全履行。根据《民法典》第577条,违约方应承担继续履行、赔偿损失等违约责任。"}
{"instruction": "如何计算迟延履行的违约金?", "input": "合同约定违约金为每日万分之五", "output": "根据《民法典》第585条,违约金可按合同约定计算。若约定过高,违约方可请求法院适当减少。您提到的万分之五约为年化18.25%,司法实践中通常支持不超过实际损失的30%为上限。"}
...使用 datasets 加载,并构建 prompt 模板:
from datasets import load_dataset
dataset = load_dataset("json", data_files="law_data.jsonl")["train"]
def format_prompt(example):
return {
"text": f"### 指令:\n{example['instruction']}\n\n### 输入:\n{example['input'] if example['input'] else '无'}\n\n### 回答:\n{example['output']}"
}
dataset = dataset.map(format_prompt)import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
model_id = "meta-llama/Llama-2-7b-hf"
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16
)
tokenizer = AutoTokenizer.from_pretrained(model_id, use_fast=True)
tokenizer.pad_token = tokenizer.eos_token # 避免无 pad_token 警告
model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=bnb_config,
device_map="auto",
torch_dtype=torch.bfloat16,
trust_remote_code=True
)使用 peft 的 LoraConfig:
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
# 准备模型进行 k-bit 训练(需启用梯度检查点)
model.gradient_checkpointing_enable()
model = prepare_model_for_kbit_training(model)
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() # 输出可训练参数占比(约 0.1%)from transformers import TrainingArguments, Trainer
from trl import SFTTrainer
training_args = TrainingArguments(
output_dir="./llama2-law-lora",
per_device_train_batch_size=4,
gradient_accumulation_steps=8, # 有效 batch = 32
num_train_epochs=3,
learning_rate=2e-4,
fp16=True,
logging_steps=10,
save_steps=200,
report_to="none",
optim="paged_adamw_8bit", # QLoRA 推荐分页优化器
)
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
args=training_args,
train_dataset=dataset,
formatting_func=lambda x: x["text"], # 直接使用格式化后的文本
max_seq_length=1024,
packing=False,
)
trainer.train()# 仅保存 LoRA 权重(推荐,轻量)
model.save_pretrained("./llama2-law-lora-adapter")
# 合并并保存完整权重(可选,方便部署)
from peft import PeftModel
base_model = AutoModelForCausalLM.from_pretrained(
model_id,
torch_dtype=torch.bfloat16,
device_map="auto"
)
merged_model = PeftModel.from_pretrained(base_model, "./llama2-law-lora-adapter").merge_and_unload()
merged_model.save_pretrained("./llama2-law-merged")参数 | 推荐范围 | 说明 |
|---|---|---|
学习率(LR) | 1e-5 ~ 5e-4 | LoRA 通常比全量微调 LR 略高(因参数少) |
批次大小(Batch) | 16~128(借助梯度累积) | 影响训练稳定性和泛化 |
Epoch 数 | 2~5 | 过少欠拟合,过多过拟合(尤其小数据集) |
序列长度 | 512~2048 | 根据任务和显存调整 |
LoRA r | 8~32 | 可先尝试 16 |
fp16 或 bf16,降低显存并加速。除了 Loss 曲线,建议在训练过程中定期运行BLEU / ROUGE(生成式任务)或精确匹配/ F1(分类任务)。更可靠的方式是人工评估或使用 LLM-as-a-judge(如 GPT-4 打分)。
from evaluate import load
rouge = load("rouge")
# 在验证集上生成并计算
predictions = [tokenizer.decode(generate(...)) for ...]
scores = rouge.compute(predictions=predictions, references=references)很多开发者会纠结:什么时候微调,什么时候用检索增强生成(RAG)?
维度 | 微调 | RAG |
|---|---|---|
知识更新 | 需重新训练 | 只需更新知识库 |
事实准确性 | 可能幻觉 | 较高(可溯源) |
推理开销 | 低(单次生成) | 较高(检索+生成) |
领域适应性 | 强(改变模型行为) | 中等(依赖检索质量) |
实现复杂度 | 高(需算力、数据) | 低(仅需向量库) |
实践建议:若数据量大(>1000 条)、需要改变模型风格或推理方式,优先选择微调;若仅需引入最新事实或私有文档,RAG 更轻量。两者也可结合——微调优化指令遵循能力,RAG 提供外部知识。
from peft import PeftModel
from transformers import AutoTokenizer, AutoModelForCausalLM
base = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-hf", device_map="auto")
model = PeftModel.from_pretrained(base, "./llama2-law-lora-adapter")
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-hf")
prompt = "### 指令:\n什么是不可抗力?\n\n### 输入:\n无\n\n### 回答:\n"
inputs = tokenizer(prompt, return_tensors="pt").to("cuda")
outputs = model.generate(**inputs, max_new_tokens=256, temperature=0.7)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))Q1:微调后模型输出重复或崩坏?
do_sample 和 temperature 设置。Q2:显存不足(OOM)怎么办?
max_seq_length。Q3:微调效果不如预期?
微调是大模型走向产业应用的“最后一公里”。本文从原理到代码,覆盖了全量微调、LoRA、QLoRA 等主流技术,并给出了完整的训练与部署范例。随着模型规模的增长,参数高效微调已成为绝大多数开发者的首选;而开源社区(Hugging Face PEFT、Unsloth、Axolotl 等)正不断降低微调门槛。
未来,微调将与 RAG、Agent 等技术深度融合,共同构建更智能、更可靠的 AI 系统。希望本文能助你在微调实践中少走弯路,快速落地领域专属大模型。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。