大模型微调折腾手记
结果光显存这块就踩了一堆坑:先是用全参数微调直接 OOM,换 LoRA 还是顶不住,最后上 Q-LoRA 才勉强在 24G 显存上跑起来。
"
去年我接了个项目,需要把一个 7B 模型微调成客服机器人。
为什么需要微调
直接用基础模型确实能干活,但总有点不对味:它不懂你的行业黑话、记不住产品细节、回复风格也不统一。这些问题不是单纯靠 prompt 就能解决的。微调的核心价值,就是让模型真正"学会"你的特定场景。
但微调不是万能药。先问清楚三件事:
- 你有多少数据?少于 1000 条对子,效果提升可能有限
- 有多少算力?7B 模型至少要 24G 显存起步
- 期望什么样的提升?如果是风格调整,微调比 prompt 工程更划算;如果是知识补充,可能需要 RAG
LoRA 实战入门
LoRA(Low-Rank Adaptation)的核心思路很简单:不直接微调模型的所有参数,而是通过低秩矩阵来调整。实际操作中,你要改的参数量能少 90% 以上,显存压力也跟着降下来。
环境准备
首先安装必要的依赖:
pip install transformers datasets peft bitsandbytes accelerate torch
基础 LoRA 训练脚本
这是一个简化版的 LoRA 训练脚本,适合快速上手:
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments, Trainer
from peft import LoraConfig, get_peft_model, TaskType
from datasets import load_dataset
import torch
# 模型和数据路径
model_name = "huggingface/CodeBERTa-small-v1"
data_path = "./data/customer_service.json"
# 加载模型和分词器
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.float16,
device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
# 配置 LoRA
peft_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
inference_mode=False,
r=8, # 低秩维度,通常用 8 或 16
lora_alpha=32, # 缩放因子,通常是 r 的 4 倍
lora_dropout=0.1,
target_modules=["q_proj", "v_proj"] # 只微调注意力层
)
# 应用 LoRA
model = get_peft_model(model, peft_config)
model.print_trainable_parameters()
# 加载数据
dataset = load_dataset("json", data_files=data_path, split="train")
def preprocess_function(examples):
# 简单的数据预处理
inputs = [f"用户:{q}\n客服:{a}" for q, a in zip(examples["question"], examples["answer"])]
model_inputs = tokenizer(inputs, max_length=512, truncation=True, padding="max_length")
model_inputs["labels"] = model_inputs["input_ids"].copy()
return model_inputs
tokenized_dataset = dataset.map(preprocess_function, batched=True)
# 训练参数
training_args = TrainingArguments(
output_dir="./results",
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=2e-4,
num_train_epochs=3,
logging_steps=10,
save_steps=500,
fp16=True,
)
# 创建训练器
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_dataset,
)
# 开始训练
trainer.train()
踩坑:显存不足
第一次跑这段代码的时候,我用了 batch_size=8,结果直接 OOM。后来才知道几个关键点:
- batch size 不是越大越好:显存允许的情况下,用 4-8 通常就够
- gradient accumulation 很有用:真实 batch size =
per_device_train_batch_size * gradient_accumulation_steps - fp16 能省显存:但要小心数值稳定性问题
# 调整后的训练参数
training_args = TrainingArguments(
output_dir="./results",
per_device_train_batch_size=2, # 减小单卡 batch size
gradient_accumulation_steps=8, # 通过累积达到等效 batch size 16
learning_rate=2e-4,
num_train_epochs=3,
logging_steps=10,
save_steps=500,
fp16=True,
max_grad_norm=1.0, # 防止梯度爆炸
)
踩坑:数据格式混乱
另一个常见问题是数据格式不一致。我的原始数据是这样的:
[
{"q": "怎么退款?", "a": "您可以在订单页面点击退款按钮"},
{"question": "发货要多久", "answer": "通常1-3个工作日"},
{"user_input": "价格能优惠吗", "bot_response": "不好意思,价格是固定的"}
]
字段名都不统一,直接训练肯定崩。先清洗数据:
import json
def normalize_data(raw_data):
normalized = []
for item in raw_data:
# 尝试不同的字段名
question = item.get("q") or item.get("question") or item.get("user_input", "")
answer = item.get("a") or item.get("answer") or item.get("bot_response", "")
if question and answer:
normalized.append({
"question": question.strip(),
"answer": answer.strip()
})
return normalized
# 读取并清洗数据
with open("raw_data.json", "r") as f:
raw_data = json.load(f)
clean_data = normalize_data(raw_data)
# 保存清洗后的数据
with open("customer_service.json", "w") as f:
json.dump(clean_data, f, ensure_ascii=False, indent=2)
Q-LoRA:显存不够时的救星
当你发现 LoRA 还是跑不动时,就该考虑 Q-LoRA 了。它通过 4-bit 量化进一步压缩模型,显存需求能再降 60% 左右。
Q-LoRA 核心改动
和 LoRA 相比,Q-LoRA 主要多了一个量化步骤:
from transformers import BitsAndBytesConfig
# 配置 4-bit 量化
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4", # 推荐用 nf4,适合正态分布权重
bnb_4bit_compute_dtype=torch.float16,
bnb_4bit_use_double_quant=True, # 二次量化,进一步节省显存
)
# 用量化配置加载模型
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
device_map="auto"
)
完整的 Q-LoRA 训练脚本
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
TrainingArguments,
Trainer,
BitsAndBytesConfig,
DataCollatorForLanguageModeling
)
from peft import LoraConfig, get_peft_model, TaskType, prepare_model_for_kbit_training
from datasets import load_dataset
import torch
# 模型和数据路径
model_name = "huggingface/CodeBERTa-small-v1"
data_path = "./data/customer_service.json"
# 量化配置
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.float16,
bnb_4bit_use_double_quant=True,
)
# 加载量化模型
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
device_map="auto"
)
# 准备模型进行 k-bit 训练
model = prepare_model_for_kbit_training(model)
# LoRA 配置
peft_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
inference_mode=False,
r=16, # Q-LoRA 可以稍微增大 r
lora_alpha=64,
lora_dropout=0.05,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], # 微调更多层
bias="none",
)
# 应用 LoRA
model = get_peft_model(model, peft_config)
model.print_trainable_parameters()
# 加载分词器
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
# 数据处理
dataset = load_dataset("json", data_files=data_path, split="train")
def preprocess_function(examples):
inputs = [f"用户:{q}\n客服:{a}{tokenizer.eos_token}" for q, a in zip(examples["question"], examples["answer"])]
model_inputs = tokenizer(inputs, max_length=512, truncation=True, padding="max_length")
model_inputs["labels"] = model_inputs["input_ids"].copy()
return model_inputs
tokenized_dataset = dataset.map(preprocess_function, batched=True)
# 数据整理器
data_collator = DataCollatorForLanguageModeling(
tokenizer=tokenizer,
mlm=False,
)
# 训练参数
training_args = TrainingArguments(
output_dir="./results",
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=1e-4, # Q-LoRA 可以用稍小的学习率
num_train_epochs=3,
logging_steps=10,
save_steps=500,
fp16=True,
optim="paged_adamw_8bit", # 用 8-bit 优化器节省显存
max_grad_norm=1.0,
warmup_ratio=0.03,
lr_scheduler_type="cosine",
)
# 创建训练器
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_dataset,
data_collator=data_collator,
)
# 开始训练
trainer.train()
Q-LoRA 的特殊坑
Q-LoRA 虽然省显存,但有几个地方容易踩:
量化类型选择错误:我用过
bnb_4bit_quant_type="fp4",结果训练效果很差。改成nf4后明显好很多,因为它更适合模型权重的分布特性。梯度累积溢出:Q-LoRA 对梯度更敏感,需要加上
max_grad_norm限制:
training_args = TrainingArguments(
# ... 其他参数
max_grad_norm=1.0, # 必须加,防止梯度溢出
)
- 学习率调整:Q-LoRA 的学习率通常要比 LoRA 小一些,我一般用
1e-4而不是2e-4。
参数调优实战
参数调优不是漫无目的的尝试,而是要有方向地调整。几个关键的调优维度:
LoRA rank (r) 的选择
r 控制低秩矩阵的维度,直接影响可训练参数量:
# 小 r:参数少,训练快,但表达能力有限
peft_config = LoraConfig(r=4, ...)
# 中等 r:平衡效果和效率
peft_config = LoraConfig(r=8, ...)
# 大 r:参数多,表达能力更强,但容易过拟合
peft_config = LoraConfig(r=16, ...)
我的经验是:
- 数据量少(<1000 条):
r=4或r=8 - 数据量中等(1000-5000 条):
r=8或r=16 - 数据量大(>5000 条):
r=16或r=32
学习率和训练轮数
学习率和训练轮数是联动的:
# 数据少的时候,用小学习率,多跑几轮
training_args = TrainingArguments(
learning_rate=5e-5,
num_train_epochs=5,
# ...
)
# 数据多的时候,用大学习率,少跑几轮
training_args = TrainingArguments(
learning_rate=2e-4,
num_train_epochs=2,
# ...
)
批处理大小和梯度累积
显存允许的情况下,优先增大 per_device_train_batch_size,然后用 gradient_accumulation_steps 调节有效 batch size:
# 理想情况:显存够用
training_args = TrainingArguments(
per_device_train_batch_size=8,
gradient_accumulation_steps=2, # 有效 batch size = 16
# ...
)
# 显存紧张:减小单卡 batch size,增加累积步数
training_args = TrainingArguments(
per_device_train_batch_size=2,
gradient_accumulation_steps=8, # 有效 batch size = 16
# ...
)
硬件需求实战总结
不同模型和方法的显存需求差异很大,下面是实际测试的数据:
| 模型大小 | 微调方法 | 显存需求 | 可用硬件 |
|---|---|---|---|
| 7B | 全参数微调 | ~80GB | A100 (80GB) |
| 7B | LoRA | ~24GB | RTX 3090/4090 |
| 7B | Q-LoRA | ~12GB | RTX 3060/4060 |
| 13B | LoRA | ~48GB | A100 (40GB) |
| 13B | Q-LoRA | ~20GB | RTX 3090/4090 |
这些数字不是绝对的,受 batch size、序列长度、模型结构影响,但能给你一个大致的参考。
我的硬件配置
- CPU: AMD Ryzen 9 5950X
- GPU: NVIDIA RTX 3090 (24GB)
- 内存: 64GB DDR4
- 存储: 2TB NVMe SSD
这个配置能跑 7B 的 LoRA,但 Q-LoRA 会更稳。如果显卡显存更小,比如 12GB,那就只能上 Q-LoRA 了。
效果评估与迭代
微调完成后,要评估效果,而不是直接上线。
评估方法
我用两种方式评估:
- 定性评估:人工抽查一些典型问题
from transformers import pipeline
# 加载微调后的模型
model = AutoModelForCausalLM.from_pretrained("./results/checkpoint-500")
tokenizer = AutoTokenizer.from_pretrained(model_name)
# 创建生成管道
generator = pipeline("text-generation", model=model, tokenizer=tokenizer)
# 测试几个问题
test_questions = [
"怎么退款?",
"发货要多久",
"价格能优惠吗"
]
for question in test_questions:
prompt = f"用户:{question}\n客服:"
outputs = generator(prompt, max_length=100, num_return_sequences=1)
print(f"问题:{question}")
print(f"回答:{outputs[0]['generated_text'][len(prompt):]}\n")
- 定量评估:用测试集计算 BLEU、ROUGE 等指标(这个需要准备标注数据)
迭代优化
根据评估结果调整:
- 效果不好:检查数据质量,增加训练轮数,调大
r - 过拟合:减少训练轮数,增加
lora_dropout,减小r - 生成质量差:调整生成参数(temperature、top_p),优化数据格式
常见问题排查
训练不收敛
可能原因和解决方案:
- 学习率太大:降低到
1e-5或5e-5 - 数据质量差:检查是否有错误标注、格式混乱
- 梯度不稳定:加上
max_grad_norm=1.0
显存还是不够
尝试这些方法:
- 减小 batch size:降到 1 或 2
- 缩短序列长度:从 512 降到 256
- 用 Q-LoRA 而不是 LoRA
- 梯度检查点:加上
gradient_checkpointing=True
效果提升不明显
可能是这几个原因:
- 数据量太少:少于 500 条对子,微调效果有限
- 数据分布不均衡:某些类别样本太少
- 基础模型不合适:选一个更接近你场景的模型
最后一点建议
微调不是银弹,它只是工具。真正重要的是:
- 先想清楚问题:微调能解决什么?prompt 工程能不能解决?
- 准备高质量数据:垃圾进,垃圾出
- 理解硬件限制:不要强行上大模型
- 迭代优化:微调不是一次性的,要持续调整
我踩过的最大坑是:一开始就上 13B 模型,结果显存不够,数据也不够,最后还是退回来用 7B 的 Q-LoRA。先从小模型、LoRA 开始,效果好再逐步升级,这样更稳妥。
大模型微调这条路,从 LoRA 到 Q-LoRA,本质是在算力、效果、成本之间找平衡。没有绝对的最优方案,只有最适合你场景的选择。
版权声明: 本文首发于 指尖魔法屋-大模型微调折腾手记(https://blog.thinkmoon.cn/post/137-practice-lora-qlora-model-finetuning/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。