第六章:微调 — 02 LoRA 低秩适配微调#
LoRA(Low-Rank Adaptation) 只训练少量参数(< 1%),大幅降低微调成本。
传统全参数微调需要更新模型所有参数(如 7B 模型 = 70亿个参数),对显存和计算的要求极高。LoRA 的核心思想是:预训练模型的权重矩阵在任务适配时,其变化量是低秩的。
本章目标:
理解 LoRA 的数学原理
计算实际参数节省量
准备微调数据集格式
编写完整训练代码框架
了解 QLoRA(量化 + LoRA)进一步节省显存
# 依赖说明
# 本章完整运行需要安装:
# pip install transformers datasets peft torch trl bitsandbytes
# 部分章节(原理、计算)无需 GPU 即可运行
import json
import os
# 尝试导入深度学习库,给出友好提示
try:
import torch
TORCH_AVAILABLE = True
print(f"PyTorch 版本: {torch.__version__}")
print(f"CUDA 可用: {torch.cuda.is_available()}")
if torch.cuda.is_available():
print(f"GPU: {torch.cuda.get_device_name(0)}")
print(f"显存: {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB")
except ImportError:
TORCH_AVAILABLE = False
print("PyTorch 未安装。Section 1-3 仍可运行,Section 4-6 需要 PyTorch。")
print("安装命令:pip install torch transformers peft trl bitsandbytes datasets")
try:
from peft import LoraConfig, get_peft_model, TaskType
from peft import PeftModel
PEFT_AVAILABLE = True
print("PEFT 已安装")
except ImportError:
PEFT_AVAILABLE = False
print("PEFT 未安装,LoRA 训练代码需要:pip install peft")
try:
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments, BitsAndBytesConfig
TRANSFORMERS_AVAILABLE = True
print("Transformers 已安装")
except ImportError:
TRANSFORMERS_AVAILABLE = False
print("Transformers 未安装:pip install transformers")
PyTorch 未安装。Section 1-3 仍可运行,Section 4-6 需要 PyTorch。
安装命令:pip install torch transformers peft trl bitsandbytes datasets
PEFT 未安装,LoRA 训练代码需要:pip install peft
Transformers 未安装:pip install transformers
Section 1:LoRA 原理图解#
核心思想#
原始模型权重 W₀(冻结,不更新)+ 低秩增量 ΔW = B × A(可训练)
传统微调:
┌─────────────────────────────────┐
│ W (d × k,全部更新) │ 参数量 = d × k
└─────────────────────────────────┘
LoRA 微调:
┌─────────────────────────────────┐
│ W₀ (d × k,冻结,不更新) │
└─────────────────────────────────┘
+
┌──────┐ ┌──────────────────────┐
│ B │ × │ A │
│(d×r) │ │ (r×k) │
└──────┘ └──────────────────────┘
可训练 可训练
参数量 = r×d + r×k = r×(d+k)
其中 r << min(d, k)
推理时的前向传播#
输入 x
│
├──→ W₀ × x ──────────────────┐
│ │
└──→ A × x → B × (Ax) × (α/r) ─┤
↓
输出 h = W₀x + BAx·(α/r)
关键超参数#
参数 |
含义 |
典型值 |
|---|---|---|
|
低秩矩阵的秩,越大参数越多效果越好 |
4, 8, 16, 32, 64 |
|
缩放因子,影响学习率的有效值 |
通常为 r 或 2r |
|
防过拟合的 dropout |
0.0 - 0.1 |
|
对哪些层应用 LoRA |
q_proj, v_proj, … |
参数节省公式#
$$\text{原始参数} = d \times k$$ $$\text{LoRA 参数} = r \times (d + k)$$ $$\text{节省率} = 1 - \frac{r \times (d + k)}{d \times k}$$
# Section 2:参数量计算(纯 Python,无需 GPU)
def calc_lora_params(model_total_params, target_layers_config, rank, alpha):
"""
计算 LoRA 微调的参数量
Args:
model_total_params: 模型总参数量
target_layers_config: list of (layer_name, d, k) 元组
rank: LoRA rank r
alpha: lora_alpha
Returns:
dict with detailed breakdown
"""
lora_params = 0
layer_breakdown = []
for layer_name, d, k in target_layers_config:
original_params = d * k
lora_layer_params = rank * (d + k)
saving_ratio = 1 - lora_layer_params / original_params
lora_params += lora_layer_params
layer_breakdown.append({
"layer": layer_name,
"shape": f"{d}×{k}",
"original_params": original_params,
"lora_params": lora_layer_params,
"saving": f"{saving_ratio:.1%}"
})
trainable_ratio = lora_params / model_total_params
effective_alpha = alpha / rank # 缩放因子
return {
"model_total_params": model_total_params,
"lora_trainable_params": lora_params,
"trainable_ratio": trainable_ratio,
"rank": rank,
"alpha": alpha,
"effective_scaling": effective_alpha,
"layer_breakdown": layer_breakdown
}
# Llama 3 7B 模型示例
# 隐藏维度 d_model = 4096
# 注意力头:32 个头,每个头 128 维
# Transformer 层数:32 层
D_MODEL = 4096
NUM_LAYERS = 32
NUM_HEADS = 32
HEAD_DIM = D_MODEL // NUM_HEADS # 128
# 典型的 LoRA 目标层(每个 Transformer 层)
target_layers = []
for layer_idx in range(NUM_LAYERS):
# Q, K, V, O 投影矩阵
target_layers.extend([
(f"layer{layer_idx}.self_attn.q_proj", D_MODEL, D_MODEL),
(f"layer{layer_idx}.self_attn.v_proj", D_MODEL, D_MODEL),
])
LLAMA_7B_TOTAL_PARAMS = 7_000_000_000
for rank in [8, 16, 64]:
result = calc_lora_params(
model_total_params=LLAMA_7B_TOTAL_PARAMS,
target_layers_config=target_layers,
rank=rank,
alpha=rank * 2
)
print(f"\nRank = {rank}:")
print(f" 模型总参数: {result['model_total_params'] / 1e9:.1f}B")
print(f" LoRA 可训练: {result['lora_trainable_params'] / 1e6:.1f}M ({result['trainable_ratio']:.3%})")
print(f" 缩放系数: {result['effective_scaling']:.1f}")
# 显存估算
# 全参数微调:模型(16GB fp16) + 梯度(16GB) + 优化器状态(32GB) ≈ 64GB
# LoRA:模型(14GB 量化) + LoRA参数梯度(很小) + 优化器(很小)
lora_params_gb = result['lora_trainable_params'] * 4 / 1e9 # fp32
optimizer_gb = lora_params_gb * 2 # Adam 需要 2x
base_model_gb = 14 # 4-bit 量化后
total_vram = base_model_gb + lora_params_gb + optimizer_gb
print(f" 预估显存: {total_vram:.1f} GB (vs 全参微调 ~64GB)")
Rank = 8:
模型总参数: 7.0B
LoRA 可训练: 4.2M (0.060%)
缩放系数: 2.0
预估显存: 14.1 GB (vs 全参微调 ~64GB)
Rank = 16:
模型总参数: 7.0B
LoRA 可训练: 8.4M (0.120%)
缩放系数: 2.0
预估显存: 14.1 GB (vs 全参微调 ~64GB)
Rank = 64:
模型总参数: 7.0B
LoRA 可训练: 33.6M (0.479%)
缩放系数: 2.0
预估显存: 14.4 GB (vs 全参微调 ~64GB)
# 详细展示 rank=16 的层级分解
result = calc_lora_params(
model_total_params=LLAMA_7B_TOTAL_PARAMS,
target_layers_config=target_layers[:4], # 只显示前2层
rank=16,
alpha=32
)
print("前 2 层的 LoRA 参数分解(rank=16):")
print(f"{'层名':<40} {'形状':>12} {'原始参数':>12} {'LoRA参数':>10} {'节省'}")
print("-" * 85)
for layer in result["layer_breakdown"]:
print(f"{layer['layer']:<40} {layer['shape']:>12} {layer['original_params']:>12,} {layer['lora_params']:>10,} {layer['saving']}")
前 2 层的 LoRA 参数分解(rank=16):
层名 形状 原始参数 LoRA参数 节省
-------------------------------------------------------------------------------------
layer0.self_attn.q_proj 4096×4096 16,777,216 131,072 99.2%
layer0.self_attn.v_proj 4096×4096 16,777,216 131,072 99.2%
layer1.self_attn.q_proj 4096×4096 16,777,216 131,072 99.2%
layer1.self_attn.v_proj 4096×4096 16,777,216 131,072 99.2%
Section 3:数据集准备#
微调数据通常使用 Alpaca 格式(instruction + input + output)或 ChatML 格式。
JSONL 格式(Alpaca Style)#
{"instruction": "任务描述", "input": "输入内容(可选)", "output": "期望输出"}
ChatML 格式(对话格式)#
{"messages": [{"role": "system", ...}, {"role": "user", ...}, {"role": "assistant", ...}]}
import json
# 为客服场景生成 5 个合成训练样本
alpaca_examples = [
{
"instruction": "你是一位专业的电商客服,用友好、简洁的语气回复客户问题。",
"input": '我昨天下单了,为什么还没发货?',
"output": "您好!感谢您的耐心等待。订单通常在下单后 1-2 个工作日内发货,节假日可能稍有延迟。您可以在'我的订单'页面查看实时物流状态。如果超过 3 个工作日仍未发货,请联系我们,我们会优先处理。祝您购物愉快!"
},
{
"instruction": "你是一位专业的电商客服,用友好、简洁的语气回复客户问题。",
"input": '收到的商品有破损,怎么办?',
"output": "非常抱歉给您带来不便!请您按以下步骤操作:\n1. 拍摄破损商品照片(包括包装和商品本身)\n2. 在订单页点击'申请售后'\n3. 上传照片,选择'商品破损'\n\n我们会在 24 小时内审核并为您安排补发或退款。如需紧急处理,可直接联系在线客服。"
},
{
"instruction": "你是一位专业的电商客服,用友好、简洁的语气回复客户问题。",
"input": '可以修改收货地址吗?订单已经提交了。',
"output": "可以修改!但需要在发货前操作:\n- **未发货**:进入订单详情→'修改地址'即可\n- **已发货**:需要联系快递公司改签,或等待退回后重新发货\n\n建议您尽快确认订单状态。如订单状态不允许自行修改,请告诉我您的订单号,我来帮您处理!"
},
{
"instruction": "你是一位专业的电商客服,用友好、简洁的语气回复客户问题。",
"input": '退款多久到账?',
"output": "退款到账时间取决于支付方式:\n- **支付宝/微信**:1-3 个工作日\n- **银行卡**:3-7 个工作日\n- **信用卡**:7-15 个工作日(取决于发卡行)\n\n退款申请审核通过后会自动退回原支付账户。如超时未收到,请联系我们提供退款凭证,我们协助您催款。"
},
{
"instruction": "你是一位专业的电商客服,用友好、简洁的语气回复客户问题。",
"input": '商品和图片不一样,能退货吗?',
"output": "当然可以!商品与描述不符属于质量问题,您享有以下权益:\n1. **7天无理由退换货**(自收货之日起)\n2. **运费由我们承担**(质量/描述问题)\n\n操作方法:订单页→申请退货→选择'与描述不符'→上传对比照片。审核通过后,我们寄出退货标签,您无需自费。非常抱歉给您带来不便!"
},
]
print("Alpaca 格式训练数据示例:")
print("=" * 60)
for i, ex in enumerate(alpaca_examples, 1):
print(f"\n样本 {i}:")
print(f" instruction: {ex['instruction'][:40]}...")
print(f" input: {ex['input']}")
print(f" output: {ex['output'][:80]}...")
# 保存到 JSONL 文件
output_path = "/tmp/customer_service_train.jsonl"
with open(output_path, "w", encoding="utf-8") as f:
for ex in alpaca_examples:
f.write(json.dumps(ex, ensure_ascii=False) + "\n")
print(f"\n已保存 {len(alpaca_examples)} 个样本到: {output_path}")
Alpaca 格式训练数据示例:
============================================================
样本 1:
instruction: 你是一位专业的电商客服,用友好、简洁的语气回复客户问题。...
input: 我昨天下单了,为什么还没发货?
output: 您好!感谢您的耐心等待。订单通常在下单后 1-2 个工作日内发货,节假日可能稍有延迟。您可以在'我的订单'页面查看实时物流状态。如果超过 3 个工作日仍未发货,...
样本 2:
instruction: 你是一位专业的电商客服,用友好、简洁的语气回复客户问题。...
input: 收到的商品有破损,怎么办?
output: 非常抱歉给您带来不便!请您按以下步骤操作:
1. 拍摄破损商品照片(包括包装和商品本身)
2. 在订单页点击'申请售后'
3. 上传照片,选择'商品破损'
我...
样本 3:
instruction: 你是一位专业的电商客服,用友好、简洁的语气回复客户问题。...
input: 可以修改收货地址吗?订单已经提交了。
output: 可以修改!但需要在发货前操作:
- **未发货**:进入订单详情→'修改地址'即可
- **已发货**:需要联系快递公司改签,或等待退回后重新发货
建议您尽快...
样本 4:
instruction: 你是一位专业的电商客服,用友好、简洁的语气回复客户问题。...
input: 退款多久到账?
output: 退款到账时间取决于支付方式:
- **支付宝/微信**:1-3 个工作日
- **银行卡**:3-7 个工作日
- **信用卡**:7-15 个工作日(取决于发...
样本 5:
instruction: 你是一位专业的电商客服,用友好、简洁的语气回复客户问题。...
input: 商品和图片不一样,能退货吗?
output: 当然可以!商品与描述不符属于质量问题,您享有以下权益:
1. **7天无理由退换货**(自收货之日起)
2. **运费由我们承担**(质量/描述问题)
操作方...
已保存 5 个样本到: /tmp/customer_service_train.jsonl
# 将 Alpaca 格式转换为 ChatML 格式(训练对话模型)
def alpaca_to_chatml(examples):
"""将 Alpaca 格式转换为 ChatML 对话格式"""
chatml_examples = []
for ex in examples:
messages = [
{"role": "system", "content": ex["instruction"]},
{"role": "user", "content": ex["input"]},
{"role": "assistant", "content": ex["output"]}
]
chatml_examples.append({"messages": messages})
return chatml_examples
def format_chatml_string(messages):
"""将 messages 格式化为 ChatML token 字符串(用于 tokenizer)"""
result = ""
for msg in messages:
result += f"<|im_start|>{msg['role']}\n{msg['content']}<|im_end|>\n"
result += "<|im_start|>assistant\n" # 最后一个 assistant 开始
return result
chatml_examples = alpaca_to_chatml(alpaca_examples)
print("ChatML 格式示例(样本 1):")
print("-" * 60)
for msg in chatml_examples[0]["messages"]:
print(f"[{msg['role']}]")
print(f"{msg['content'][:100]}..." if len(msg['content']) > 100 else msg['content'])
print()
print("\nChatML Token 字符串格式:")
print("-" * 60)
chatml_str = format_chatml_string(chatml_examples[0]["messages"])
print(chatml_str[:300] + "...")
ChatML 格式示例(样本 1):
------------------------------------------------------------
[system]
你是一位专业的电商客服,用友好、简洁的语气回复客户问题。
[user]
我昨天下单了,为什么还没发货?
[assistant]
您好!感谢您的耐心等待。订单通常在下单后 1-2 个工作日内发货,节假日可能稍有延迟。您可以在'我的订单'页面查看实时物流状态。如果超过 3 个工作日仍未发货,请联系我们,我们会优先处理。祝您购物愉快...
ChatML Token 字符串格式:
------------------------------------------------------------
<|im_start|>system
你是一位专业的电商客服,用友好、简洁的语气回复客户问题。<|im_end|>
<|im_start|>user
我昨天下单了,为什么还没发货?<|im_end|>
<|im_start|>assistant
您好!感谢您的耐心等待。订单通常在下单后 1-2 个工作日内发货,节假日可能稍有延迟。您可以在'我的订单'页面查看实时物流状态。如果超过 3 个工作日仍未发货,请联系我们,我们会优先处理。祝您购物愉快!<|im_end|>
<|im_start|>assistant
...
Section 4:LoRA 配置与训练(代码框架)#
以下是完整的 LoRA 训练代码。使用 facebook/opt-125m(最小可用模型)演示,实际使用时换成目标模型。
注意:实际训练需要 GPU,以下代码在有 PyTorch 环境时可运行 10 步演示。
# LoRA 训练配置展示
if PEFT_AVAILABLE and TRANSFORMERS_AVAILABLE and TORCH_AVAILABLE:
from datasets import Dataset
try:
from trl import SFTTrainer, SFTConfig
TRL_AVAILABLE = True
except ImportError:
TRL_AVAILABLE = False
print("trl 未安装:pip install trl")
# === Step 1:LoRA 配置 ===
lora_config = LoraConfig(
r=16, # 秩:参数量与效果的权衡
lora_alpha=32, # 缩放:通常为 r 的 2 倍
target_modules=["q_proj", "v_proj"], # 对注意力层的 Q、V 应用 LoRA
lora_dropout=0.05, # 轻微正则化
bias="none", # 不训练 bias
task_type=TaskType.CAUSAL_LM # 因果语言模型(自回归生成)
)
print("LoRA 配置:")
print(f" rank (r): {lora_config.r}")
print(f" lora_alpha: {lora_config.lora_alpha}")
print(f" 缩放因子 (α/r): {lora_config.lora_alpha / lora_config.r}")
print(f" 目标模块: {lora_config.target_modules}")
print(f" dropout: {lora_config.lora_dropout}")
print(f" 任务类型: {lora_config.task_type}")
# === Step 2:加载模型(小模型演示)===
print("\n正在加载 facebook/opt-125m(125M 参数,CPU 可运行)...")
model_name = "facebook/opt-125m"
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
base_model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.float32, # CPU 用 float32
)
print(f"基础模型总参数: {sum(p.numel() for p in base_model.parameters()) / 1e6:.1f}M")
# 应用 LoRA
# opt 模型的注意力层名称不同,需要调整
lora_config_opt = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.05,
bias="none",
task_type=TaskType.CAUSAL_LM
)
peft_model = get_peft_model(base_model, lora_config_opt)
trainable, total = 0, 0
for param in peft_model.parameters():
total += param.numel()
if param.requires_grad:
trainable += param.numel()
print(f"LoRA 后可训练参数: {trainable / 1e6:.2f}M ({trainable/total:.2%})")
print(f"参数节省: {1 - trainable/total:.2%}")
else:
print("跳过实际加载(PyTorch/PEFT 未安装)")
print("\n以下是等价的代码逻辑展示:")
print("""
lora_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.05,
bias="none",
task_type=TaskType.CAUSAL_LM
)
# 对于 Llama 3 7B:
# 可训练参数 ≈ 8M(0.11%),节省 99.9% 的参数更新
""")
跳过实际加载(PyTorch/PEFT 未安装)
以下是等价的代码逻辑展示:
lora_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.05,
bias="none",
task_type=TaskType.CAUSAL_LM
)
# 对于 Llama 3 7B:
# 可训练参数 ≈ 8M(0.11%),节省 99.9% 的参数更新
# 训练配置和 SFTTrainer 设置
training_args_config = {
"output_dir": "./lora_output",
"num_train_epochs": 3,
"per_device_train_batch_size": 4,
"gradient_accumulation_steps": 4, # 等效 batch_size = 16
"learning_rate": 2e-4, # LoRA 通常比全参微调用更大的 lr
"fp16": True, # GPU 半精度训练
"logging_steps": 10,
"save_steps": 100,
"warmup_ratio": 0.03, # 3% 的步骤做 warmup
"lr_scheduler_type": "cosine", # 余弦退火
"max_steps": 10, # 演示只跑 10 步
}
print("TrainingArguments 配置说明:")
print("=" * 60)
explanations = {
"per_device_train_batch_size": "每块 GPU 的 batch 大小,受显存限制",
"gradient_accumulation_steps": "梯度累积步数,变相增大 batch size",
"learning_rate": "LoRA 微调推荐 1e-4 到 3e-4",
"fp16": '半精度训练节省显存(GPU 支持时开启)',
"warmup_ratio": '先小 lr 稳定训练,再升到目标 lr',
"lr_scheduler_type": "cosine:训练后期 lr 缓慢降低",
}
for key, val in training_args_config.items():
exp = explanations.get(key, "")
print(f" {key:<35} = {str(val):<10} # {exp}")
print("\n完整训练代码框架(Llama 3 + LoRA):")
print("-" * 60)
training_code = '''
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments
from peft import LoraConfig, get_peft_model, TaskType
from trl import SFTTrainer
from datasets import load_dataset
# 1. 加载模型和 tokenizer
model_name = "meta-llama/Meta-Llama-3-8B"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype="auto")
# 2. LoRA 配置
lora_config = LoraConfig(
r=16, lora_alpha=32,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
lora_dropout=0.05, task_type=TaskType.CAUSAL_LM
)
# 3. 应用 LoRA
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# 4. 训练配置
training_args = TrainingArguments(
output_dir="./lora_output",
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=2e-4,
fp16=True,
save_steps=100,
logging_steps=10,
)
# 5. 开始训练
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=dataset["train"],
tokenizer=tokenizer,
)
trainer.train()
'''
print(training_code)
TrainingArguments 配置说明:
============================================================
output_dir = ./lora_output #
num_train_epochs = 3 #
per_device_train_batch_size = 4 # 每块 GPU 的 batch 大小,受显存限制
gradient_accumulation_steps = 4 # 梯度累积步数,变相增大 batch size
learning_rate = 0.0002 # LoRA 微调推荐 1e-4 到 3e-4
fp16 = True # 半精度训练节省显存(GPU 支持时开启)
logging_steps = 10 #
save_steps = 100 #
warmup_ratio = 0.03 # 先小 lr 稳定训练,再升到目标 lr
lr_scheduler_type = cosine # cosine:训练后期 lr 缓慢降低
max_steps = 10 #
完整训练代码框架(Llama 3 + LoRA):
------------------------------------------------------------
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments
from peft import LoraConfig, get_peft_model, TaskType
from trl import SFTTrainer
from datasets import load_dataset
# 1. 加载模型和 tokenizer
model_name = "meta-llama/Meta-Llama-3-8B"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype="auto")
# 2. LoRA 配置
lora_config = LoraConfig(
r=16, lora_alpha=32,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
lora_dropout=0.05, task_type=TaskType.CAUSAL_LM
)
# 3. 应用 LoRA
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# 4. 训练配置
training_args = TrainingArguments(
output_dir="./lora_output",
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=2e-4,
fp16=True,
save_steps=100,
logging_steps=10,
)
# 5. 开始训练
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=dataset["train"],
tokenizer=tokenizer,
)
trainer.train()
Section 5:加载与使用 LoRA 模型#
# 加载和使用 LoRA adapter 的完整流程
if PEFT_AVAILABLE and TRANSFORMERS_AVAILABLE and TORCH_AVAILABLE:
# 假设训练完成,展示如何加载和使用
# 方法 1:加载基础模型 + LoRA adapter(灵活,可切换不同 adapter)
print("方法 1:加载基础模型 + LoRA Adapter")
print("-" * 50)
# 用前面训练的 peft_model 演示保存
save_path = "/tmp/lora_adapter_demo"
peft_model.save_pretrained(save_path)
print(f"LoRA Adapter 已保存到: {save_path}")
import os
saved_files = os.listdir(save_path)
print(f'保存的文件: {saved_files}')
# 加载:先加载基础模型,再加载 adapter
loaded_model = AutoModelForCausalLM.from_pretrained(
"facebook/opt-125m",
torch_dtype=torch.float32,
)
loaded_with_adapter = PeftModel.from_pretrained(loaded_model, save_path)
print("\nLoRA Adapter 加载成功!")
# 方法 2:合并 LoRA 权重到基础模型(推理更快,不可恢复)
print("\n方法 2:合并权重(merge_and_unload)")
print("-" * 50)
merged_model = loaded_with_adapter.merge_and_unload()
print("权重已合并,现在是普通 HuggingFace 模型")
print(f"合并后模型类型: {type(merged_model).__name__}")
# 保存合并后的完整模型
merged_save_path = "/tmp/merged_model_demo"
merged_model.save_pretrained(merged_save_path)
tokenizer.save_pretrained(merged_save_path)
print(f"合并模型已保存到: {merged_save_path}")
# 推理演示
print("\n推理演示:")
print("-" * 50)
test_input = "Customer: I haven't received my order yet."
inputs = tokenizer(test_input, return_tensors="pt")
with torch.no_grad():
outputs = merged_model.generate(
**inputs,
max_new_tokens=50,
temperature=0.7,
do_sample=True,
pad_token_id=tokenizer.eos_token_id
)
generated = tokenizer.decode(outputs[0], skip_special_tokens=True)
print(f'输入: {test_input}')
print(f"输出: {generated[len(test_input):]}")
print("(注意:125M 模型很小,效果仅供演示,实际用 7B+ 模型)")
else:
print("PyTorch/PEFT 未安装,展示等价代码:")
code = '''
# 加载 LoRA adapter
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
base_model = AutoModelForCausalLM.from_pretrained("meta-llama/Meta-Llama-3-8B")
model_with_lora = PeftModel.from_pretrained(base_model, "./lora_output")
# 合并权重(推理加速)
merged_model = model_with_lora.merge_and_unload()
merged_model.save_pretrained("./merged_model")
'''
print(code)
PyTorch/PEFT 未安装,展示等价代码:
# 加载 LoRA adapter
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
base_model = AutoModelForCausalLM.from_pretrained("meta-llama/Meta-Llama-3-8B")
model_with_lora = PeftModel.from_pretrained(base_model, "./lora_output")
# 合并权重(推理加速)
merged_model = model_with_lora.merge_and_unload()
merged_model.save_pretrained("./merged_model")
Section 6:QLoRA — 量化 + LoRA#
QLoRA(Quantized LoRA)将基础模型压缩到 4-bit 精度,再应用 LoRA 微调。
显存对比#
方法 |
Llama 3 7B 显存需求 |
GPU 要求 |
|---|---|---|
全参微调 (fp16) |
~56GB |
至少 2× A100 80GB |
LoRA (fp16) |
~16GB |
A100 40GB |
QLoRA (4-bit + LoRA) |
~6GB |
RTX 3090/4090 即可 |
4-bit 量化原理:将 16-bit 浮点数压缩到 4-bit 整数存储,推理时动态反量化。精度损失极小(< 1%),显存节省 4x。
# QLoRA 配置示例(需要 bitsandbytes 库)
print("QLoRA 配置代码(需要 GPU + bitsandbytes):")
print("=" * 60)
qlora_code = '''
from transformers import BitsAndBytesConfig, AutoModelForCausalLM
from peft import LoraConfig, get_peft_model, TaskType, prepare_model_for_kbit_training
import torch
# Step 1:4-bit 量化配置
bnb_config = BitsAndBytesConfig(
load_in_4bit=True, # 4-bit 加载
bnb_4bit_quant_type="nf4", # NF4 量化(QLoRA 论文推荐)
bnb_4bit_compute_dtype=torch.bfloat16, # 计算时用 bf16
bnb_4bit_use_double_quant=True, # 嵌套量化,进一步省显存
)
# Step 2:加载量化后的基础模型(7B 模型只需 ~6GB 显存)
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Meta-Llama-3-8B",
quantization_config=bnb_config,
device_map="auto", # 自动分配 GPU/CPU
)
# Step 3:为量化模型准备 LoRA 微调
model = prepare_model_for_kbit_training(model) # 重要!
# Step 4:应用 LoRA(与普通 LoRA 相同)
lora_config = LoraConfig(
r=64, # QLoRA 通常用更大 rank 补偿量化误差
lora_alpha=16,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"], # 覆盖更多层
lora_dropout=0.05,
bias="none",
task_type=TaskType.CAUSAL_LM
)
model = get_peft_model(model, lora_config)
# 显存使用:~6-8GB(原本需要 56GB)
'''
print(qlora_code)
# 显存节省计算
print("\n量化后的显存节省:")
print("-" * 50)
model_params_b = 7 # 7B 模型
bytes_per_param = {
"fp32 (全参微调)": 4,
"fp16/bf16": 2,
"int8 (8-bit)": 1,
"nf4 (4-bit QLoRA)": 0.5,
}
print(f"{'精度':<20} {'模型显存':>10} {'+ 训练开销':>12} {'总计'}")
print("-" * 55)
for precision, bpp in bytes_per_param.items():
model_gb = model_params_b * 1e9 * bpp / 1e9
if '全参' in precision:
train_overhead_gb = model_gb * 3 # 梯度 + Adam 状态
else:
train_overhead_gb = 2 # LoRA 参数很少
total = model_gb + train_overhead_gb
print(f"{precision:<20} {model_gb:>8.1f}GB {train_overhead_gb:>10.1f}GB {total:>8.1f}GB")
QLoRA 配置代码(需要 GPU + bitsandbytes):
============================================================
from transformers import BitsAndBytesConfig, AutoModelForCausalLM
from peft import LoraConfig, get_peft_model, TaskType, prepare_model_for_kbit_training
import torch
# Step 1:4-bit 量化配置
bnb_config = BitsAndBytesConfig(
load_in_4bit=True, # 4-bit 加载
bnb_4bit_quant_type="nf4", # NF4 量化(QLoRA 论文推荐)
bnb_4bit_compute_dtype=torch.bfloat16, # 计算时用 bf16
bnb_4bit_use_double_quant=True, # 嵌套量化,进一步省显存
)
# Step 2:加载量化后的基础模型(7B 模型只需 ~6GB 显存)
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Meta-Llama-3-8B",
quantization_config=bnb_config,
device_map="auto", # 自动分配 GPU/CPU
)
# Step 3:为量化模型准备 LoRA 微调
model = prepare_model_for_kbit_training(model) # 重要!
# Step 4:应用 LoRA(与普通 LoRA 相同)
lora_config = LoraConfig(
r=64, # QLoRA 通常用更大 rank 补偿量化误差
lora_alpha=16,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"], # 覆盖更多层
lora_dropout=0.05,
bias="none",
task_type=TaskType.CAUSAL_LM
)
model = get_peft_model(model, lora_config)
# 显存使用:~6-8GB(原本需要 56GB)
量化后的显存节省:
--------------------------------------------------
精度 模型显存 + 训练开销 总计
-------------------------------------------------------
fp32 (全参微调) 28.0GB 84.0GB 112.0GB
fp16/bf16 14.0GB 2.0GB 16.0GB
int8 (8-bit) 7.0GB 2.0GB 9.0GB
nf4 (4-bit QLoRA) 3.5GB 2.0GB 5.5GB
Section 7:LoRA vs 全参微调 — 何时选择哪个?#
# LoRA vs 全参微调 对比
comparison = {
'维度': ['训练参数量', '显存需求', '训练时间', '效果(任务适配)', '灾难性遗忘', '可逆性', '多任务', '推荐场景'],
"LoRA / QLoRA": [
"< 1%(几百万参数)",
'低(6-16GB)',
'快(小时级)',
"95%(接近全参)",
'几乎无',
'可(卸载 adapter)',
'多个 adapter 切换',
'资源有限、任务适配、快速迭代'
],
'全参微调': [
"100%(数十亿参数)",
'高(64GB+)',
'慢(天级)',
"100%(最佳)",
'可能发生',
'不可逆',
'需要多个模型副本',
'数据充足、追求极致效果'
]
}
print(f"{'维度':<18} {'LoRA / QLoRA':<35} {'全参微调'}")
print("-" * 85)
for i in range(len(comparison['维度'])):
dim = comparison['维度'][i]
lora = comparison["LoRA / QLoRA"][i]
full = comparison['全参微调'][i]
print(f"{dim:<18} {lora:<35} {full}")
维度 LoRA / QLoRA 全参微调
-------------------------------------------------------------------------------------
训练参数量 < 1%(几百万参数) 100%(数十亿参数)
显存需求 低(6-16GB) 高(64GB+)
训练时间 快(小时级) 慢(天级)
效果(任务适配) 95%(接近全参) 100%(最佳)
灾难性遗忘 几乎无 可能发生
可逆性 可(卸载 adapter) 不可逆
多任务 多个 adapter 切换 需要多个模型副本
推荐场景 资源有限、任务适配、快速迭代 数据充足、追求极致效果
总结:LoRA 超参数调优指南#
超参数选择表#
超参数 |
低资源场景 |
标准场景 |
追求最佳效果 |
|---|---|---|---|
|
4-8 |
16 |
32-64 |
|
r 的 2 倍 |
r 的 2 倍 |
r 或 r 的 2 倍 |
|
0.05 |
0.05 |
0.1 |
|
q_proj, v_proj |
q_proj, k_proj, v_proj, o_proj |
所有线性层 |
|
3e-4 |
2e-4 |
1e-4 |
|
4 |
16 |
32+ |
关键经验#
rank 越大效果越好,但参数量线性增长,通常 r=16 是最佳平衡点
alpha 通常设为 r 的 2 倍,相当于有效 learning rate = lr × (alpha/r) = lr × 2
target_modules 越多覆盖越全,但训练更慢;从 q_proj+v_proj 开始实验
数据质量 > 数据量 > 超参数调优,先确保数据质量
QLoRA 是资源受限时的首选,效果损失 < 1%,显存节省 4x
下一步#
下一章(07_production/01_evaluation.ipynb):如何评估微调后的模型效果
推荐实践:使用 LLaMA-Factory 一键完成 LoRA 微调