一文看懂 LoRA 微调和 QLoRA:原理、场景与代码实战
- AI
- 13小时前
- 6热度
- 0评论
一文看懂 LoRA 微调和 QLoRA:原理、场景与代码实战
大模型时代,很多人都会遇到同一个问题:
“我只有一块 24G 显卡,怎么微调一个几十亿参数的模型?”
如果你也有这样的困惑,那你基本上已经站在了 LoRA 和 QLoRA 的门口。
它们都是 参数高效微调(PEFT, Parameter-Efficient Fine-Tuning) 技术,让你在显存有限、存储有限的前提下,也能把大模型变成你的专属模型。
本文会从三个角度来讲清楚 LoRA 和 QLoRA:
- 原理:到底在模型里“动了哪里”
- 应用场景:什么场景适合用哪种方案
- 代码实战:基于
transformers + peft + bitsandbytes的最小可跑样例
一、为什么需要参数高效微调(PEFT)
传统的 全参数微调(Full Fine-tuning) 做法是:
- 把预训练好的大模型完整加载到显存里
- 对所有参数都进行反向传播和更新
这会带来几个现实问题:
-
显存压力巨大
- 一个 7B 模型,FP16 约 14GB;13B / 34B / 70B 更是轻松突破单卡极限。
- 训练时还要存梯度、优化器状态,显存直接爆掉。
-
存储成本高
- 每做一个下游任务,就要多存一份完整模型权重(几十 GB 起步)。
- 想做十几个任务,磁盘都吃不消。
-
部署不灵活
- 一个基础模型 + N 个任务 = N 份完整模型。
- 实际上很多任务之间共享了绝大部分知识,只是任务特定的“小差异”不同。
于是就有了 PEFT 这种思路:
冻结绝大部分预训练参数,只在少量新增参数上学习任务特定信息。
LoRA 和 QLoRA 就是目前最主流的两种 PEFT 技术路线。
二、LoRA:低秩适配(Low-Rank Adaptation)的核心原理
2.1 直观理解
在 Transformer 中,最“重”的模块就是各种线性层(矩阵乘法),例如:
- 自注意力层中的
W_q, W_k, W_v, W_o - 前馈网络中的
W_1, W_2
这些权重矩阵都很大,直接更新它们的代价也很高。
LoRA 的核心想法是:
- 不再直接更新原始权重矩阵
W(把它完全冻结) -
只学习一个“低秩”的增量矩阵
ΔW,并且让ΔW可以写成:[
\Delta W = B A
]其中:
- ( W \in \mathbb{R}^{d\text{out} \times d\text{in}} )
- ( B \in \mathbb{R}^{d_\text{out} \times r} )
- ( A \in \mathbb{R}^{r \times d_\text{in}} )
- ( r \ll \min(d\text{out}, d\text{in}) ) 是一个很小的秩(rank)超参数
推理时真正用到的权重是:
[
W_\text{eff} = W + \Delta W = W + B A
]
而训练时只更新 A 和 B 这两个小矩阵。
好处是:
- 原始矩阵参数量:(d\text{out} \times d\text{in})
- LoRA 新增参数量:(r \times (d\text{out} + d\text{in}))
当 r 远小于 d_in, d_out 时,新增的参数只是原来的一小部分,但依然能对模型行为产生足够大的影响。
2.2 LoRA 在 Transformer 里“插”在哪里?
一般来说,LoRA 会插在如下模块的线性层上:
- 注意力部分:
q_proj,k_proj,v_proj,o_proj - FFN 部分:前馈网络的线性层
做法是:对这些模块的权重 W,额外加一个 BA 分支,训练时只更新 A, B。
在 Hugging Face 的 peft 库中,你会通过配置 target_modules 来指定要加 LoRA 的模块名。
2.3 常见超参数与经验
r(秩):8 / 16 / 32- 越大表示表达能力越强,但参数和显存都会增加。
lora_alpha:16 / 32 / 64- 相当于缩放因子,影响更新幅度。
lora_dropout:0~0.1- 给 LoRA 分支加一点 dropout,一定程度上能防止过拟合。
target_modules:- 最常见是
["q_proj", "v_proj"],也有一起加上k_proj,o_proj的。
- 最常见是
直观理解:
LoRA 就是给每个被选中的线性层挂了一个“小外挂”,用极少的新增参数表达“任务特定偏移”。
三、QLoRA:在量化基础上的 LoRA
LoRA 解决的是“只改少量参数”的问题,但有一个前提:
你依然要能把基础模型(通常是 FP16/FP32)完整加载到显存中。
当你想玩的是 13B、34B、70B 这类模型时,仅仅是把模型 load 进显存本身就已经是一道坎。
QLoRA 的核心思路可以概括为两点:
- 把基础模型量化到 4-bit,极大压缩显存占用;
- 在量化后的模型上套一层 LoRA,只训练 LoRA 参数。
3.1 QLoRA 做了什么?
以 transformers + bitsandbytes 为例:
- 使用
BitsAndBytesConfig把模型权重加载为4-bit 量化权重(比如 NF4 格式); - 冻结这些 4-bit 权重,不更新它们;
- 在指定模块上加 LoRA 层(通常使用 16-bit / bfloat16)进行训练。
好处非常直接:
原本 7B 模型 FP16 需要十几 GB 显存,量化到 4-bit 后,显存需求直接砍到原来的 1/4 左右,配合 LoRA 的参数高效更新,可以让很多人在单 24G 显卡上完成大模型微调。
3.2 QLoRA 与 LoRA 的对比
可以用一张简单的表来总结:
| 维度 | LoRA | QLoRA |
|---|---|---|
| 基础模型精度 | FP16 / FP32 | 4-bit 量化(NF4 等) |
| 显存节省来源 | 只更新少量 LoRA 参数 | 基础模型量化 + 只更新 LoRA |
| 可支持模型规模 | 中等规模(7B~13B 很合适) | 更大规模(几十 B 模型也有机会) |
| 工程复杂度 | 相对简单 | 稍复杂,需要 bitsandbytes 等依赖 |
| 推理部署 | 可选合并 LoRA 或动态加载 | 同理,但要考虑量化推理的兼容性 |
一句话对比:
- LoRA: 适合你“模型勉强能 load 进显存”的情况;
- QLoRA: 适合你“模型根本 load 不进显存,但我又想玩大模型”的情况。
四、应用场景:什么时候用 LoRA,什么时候用 QLoRA?
可以从三个维度来决策:显存大小、模型规模、任务复杂度。
4.1 显存资源与模型规模
-
单卡 24G 左右,模型 7B / 8B:
- LoRA 和 QLoRA 都可行;
- 更推荐先用 LoRA,上手简单、依赖少、调试方便。
-
想玩 13B/33B/70B 这种更大模型:
- QLoRA 非常有价值;
- 4-bit 量化 + LoRA,可以在单机上把“不可能”变成“勉强可能”。
4.2 任务类型
-
轻量任务:分类、情感分析、小规模文本匹配
- 通常只需要对高层进行轻度适配,LoRA 已经足够;
- 有些场景甚至只微调输出头也能得到不错效果。
-
复杂生成任务:指令微调、角色扮演、多轮对话
- 适合用 LoRA 或 QLoRA;
- 任务越复杂、模型越大,QLoRA 的优势越明显(能使用更大的基础模型)。
-
多任务/多领域适配
- LoRA/QLoRA 的一个 bonus 是:
不同任务只需要各自保存一份 LoRA 权重(几十 MB),
推理时按需加载,或者做线性组合,部署成本远低于多份完整模型。
- LoRA/QLoRA 的一个 bonus 是:
五、实战代码:基于 LoRA 的微调示例
下面是一个使用 LoRA 微调 Causal LM 模型 的最小化示例,基于:
transformerspeftdatasets
你可以把它改成你自己的数据集和模型名称。
5.1 环境准备
pip install "transformers[torch]" peft datasets accelerate bitsandbytes
5.2 LoRA 微调代码
import torch
from datasets import load_dataset
from transformers import (
AutoTokenizer,
AutoModelForCausalLM,
TrainingArguments,
Trainer,
)
from peft import LoraConfig, get_peft_model, TaskType
# 1. 选择基础模型(这里以 LLaMA-2 为例,需有访问权限)
base_model_name = "meta-llama/Llama-2-7b-hf"
tokenizer = AutoTokenizer.from_pretrained(base_model_name, use_fast=False)
tokenizer.pad_token = tokenizer.eos_token
# 2. 加载基础模型(不做量化,普通 LoRA)
model = AutoModelForCausalLM.from_pretrained(
base_model_name,
torch_dtype=torch.float16,
device_map="auto", # 自动分配到可用 GPU
)
# 3. 配置 LoRA
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=8,
lora_alpha=16,
lora_dropout=0.05,
target_modules=["q_proj", "v_proj"], # 具体模块名称视模型实现而定
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# 4. 准备数据集(以 Alpaca 数据为例)
dataset = load_dataset("tatsu-lab/alpaca")
def format_example(example):
# 简单的指令微调格式:用户 + 助手
text = f"用户: {example['instruction']}\n助手: {example['output']}"
tokenized = tokenizer(
text,
truncation=True,
max_length=512,
padding="max_length",
)
tokenized["labels"] = tokenized["input_ids"].copy()
return tokenized
train_dataset = dataset["train"].map(
format_example,
remove_columns=dataset["train"].column_names,
)
# 5. 训练配置
training_args = TrainingArguments(
output_dir="./lora-llama2-7b",
per_device_train_batch_size=1,
gradient_accumulation_steps=8,
num_train_epochs=3,
learning_rate=2e-4,
fp16=True,
logging_steps=10,
save_steps=500,
save_total_limit=2,
report_to="none",
)
# 6. 启动训练
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
)
trainer.train()
# 7. 只保存 LoRA 适配器(而不是整个基础模型)
model.save_pretrained("./lora_adapter")
tokenizer.save_pretrained("./lora_adapter")
5.3 推理时加载 LoRA
推理阶段,你可以这样加载基础模型 + LoRA 适配器:
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
base_model_name = "meta-llama/Llama-2-7b-hf"
adapter_path = "./lora_adapter"
tokenizer = AutoTokenizer.from_pretrained(base_model_name, use_fast=False)
model = AutoModelForCausalLM.from_pretrained(
base_model_name,
torch_dtype=torch.float16,
device_map="auto",
)
# 加载 LoRA 适配器
model = PeftModel.from_pretrained(model, adapter_path)
model.eval()
prompt = "请用三句话介绍一下 LoRA 微调的原理。"
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=128,
do_sample=True,
temperature=0.7,
top_p=0.9,
)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
六、实战代码:基于 QLoRA 的微调示例(4-bit 量化 + LoRA)
QLoRA 的代码与 LoRA 很像,关键差异在于模型加载阶段:
我们需要用 BitsAndBytesConfig 指定 4-bit 量化参数。
6.1 QLoRA:4-bit 量化加载模型
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
base_model_name = "meta-llama/Llama-2-7b-hf"
# 1. 定义 4-bit 量化配置(以 NF4 为例)
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4", # NormalFloat4
bnb_4bit_compute_dtype=torch.bfloat16, # 运算精度,可根据硬件选择
)
# 2. 加载 4-bit 量化后的基础模型
model = AutoModelForCausalLM.from_pretrained(
base_model_name,
quantization_config=bnb_config,
device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained(base_model_name, use_fast=False)
tokenizer.pad_token = tokenizer.eos_token
6.2 在量化模型上叠加 LoRA 并训练(完整示例)
import torch
from datasets import load_dataset
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
TrainingArguments,
Trainer,
)
from peft import LoraConfig, get_peft_model, TaskType
base_model_name = "meta-llama/Llama-2-7b-hf"
# 1. 4-bit 量化配置
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16, # 若不支持 bf16,可改成 torch.float16
)
# 2. 加载量化基础模型
model = AutoModelForCausalLM.from_pretrained(
base_model_name,
quantization_config=bnb_config,
device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained(base_model_name, use_fast=False)
tokenizer.pad_token = tokenizer.eos_token
# 3. 配置 LoRA(与普通 LoRA 类似)
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=16,
lora_alpha=32,
lora_dropout=0.05,
target_modules=["q_proj", "v_proj"], # 可按需添加更多模块
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# 4. 准备数据集(仍以 Alpaca 为例)
dataset = load_dataset("tatsu-lab/alpaca")
def format_example(example):
text = f"用户: {example['instruction']}\n助手: {example['output']}"
tokenized = tokenizer(
text,
truncation=True,
max_length=512,
padding="max_length",
)
tokenized["labels"] = tokenized["input_ids"].copy()
return tokenized
train_dataset = dataset["train"].map(
format_example,
remove_columns=dataset["train"].column_names,
)
# 5. 训练配置(通常会搭配更大的梯度累积)
training_args = TrainingArguments(
output_dir="./qlora-llama2-7b",
per_device_train_batch_size=1,
gradient_accumulation_steps=16,
num_train_epochs=3,
learning_rate=2e-4,
bf16=True, # 如果 GPU 不支持 bf16,可改用 fp16=True
logging_steps=10,
save_steps=500,
save_total_limit=2,
report_to="none",
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
)
trainer.train()
# 6. 保存 QLoRA 适配器
model.save_pretrained("./qlora_adapter")
tokenizer.save_pretrained("./qlora_adapter")
推理阶段,与 LoRA 类似,只是基础模型依然需要用 4-bit 量化方式加载,再叠加 QLoRA 适配器。
七、总结:一张脑图式回顾
可以用一句话和一张“脑内图”来记住 LoRA 和 QLoRA:
-
LoRA:
冻结原始大模型,只在少量“低秩矩阵”上学习增量,从而大幅减少训练参数和显存开销。 -
QLoRA:
先把基础模型做 4-bit 量化,再叠加 LoRA 更新,用更少的显存在更大模型上做微调。
在实际项目中,你可以这样快速决策:
- 显存够用(7B/13B 勉强能放下): 先用 LoRA,上手快、问题少。
- 显存吃紧但想玩更大的模型: 尝试 QLoRA,量化 + LoRA 的组合让大模型“落地”变得可能。
- 多任务、多领域适配: 使用多个 LoRA/QLoRA 适配器按需加载,极大降低部署与存储成本。
