大模型微调折腾手记

结果光显存这块就踩了一堆坑:先是用全参数微调直接 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。后来才知道几个关键点:

  1. batch size 不是越大越好:显存允许的情况下,用 4-8 通常就够
  2. gradient accumulation 很有用:真实 batch size = per_device_train_batch_size * gradient_accumulation_steps
  3. 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 虽然省显存,但有几个地方容易踩:

  1. 量化类型选择错误:我用过 bnb_4bit_quant_type="fp4",结果训练效果很差。改成 nf4 后明显好很多,因为它更适合模型权重的分布特性。

  2. 梯度累积溢出:Q-LoRA 对梯度更敏感,需要加上 max_grad_norm 限制:

training_args = TrainingArguments(
    # ... 其他参数
    max_grad_norm=1.0,  # 必须加,防止梯度溢出
)
  1. 学习率调整: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=4r=8
  • 数据量中等(1000-5000 条):r=8r=16
  • 数据量大(>5000 条):r=16r=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全参数微调~80GBA100 (80GB)
7BLoRA~24GBRTX 3090/4090
7BQ-LoRA~12GBRTX 3060/4060
13BLoRA~48GBA100 (40GB)
13BQ-LoRA~20GBRTX 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 了。

效果评估与迭代

微调完成后,要评估效果,而不是直接上线。

评估方法

我用两种方式评估:

  1. 定性评估:人工抽查一些典型问题
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")
  1. 定量评估:用测试集计算 BLEU、ROUGE 等指标(这个需要准备标注数据)

迭代优化

根据评估结果调整:

  • 效果不好:检查数据质量,增加训练轮数,调大 r
  • 过拟合:减少训练轮数,增加 lora_dropout,减小 r
  • 生成质量差:调整生成参数(temperature、top_p),优化数据格式

常见问题排查

训练不收敛

可能原因和解决方案:

  1. 学习率太大:降低到 1e-55e-5
  2. 数据质量差:检查是否有错误标注、格式混乱
  3. 梯度不稳定:加上 max_grad_norm=1.0

显存还是不够

尝试这些方法:

  1. 减小 batch size:降到 1 或 2
  2. 缩短序列长度:从 512 降到 256
  3. 用 Q-LoRA 而不是 LoRA
  4. 梯度检查点:加上 gradient_checkpointing=True

效果提升不明显

可能是这几个原因:

  1. 数据量太少:少于 500 条对子,微调效果有限
  2. 数据分布不均衡:某些类别样本太少
  3. 基础模型不合适:选一个更接近你场景的模型

最后一点建议

微调不是银弹,它只是工具。真正重要的是:

  1. 先想清楚问题:微调能解决什么?prompt 工程能不能解决?
  2. 准备高质量数据:垃圾进,垃圾出
  3. 理解硬件限制:不要强行上大模型
  4. 迭代优化:微调不是一次性的,要持续调整

我踩过的最大坑是:一开始就上 13B 模型,结果显存不够,数据也不够,最后还是退回来用 7B 的 Q-LoRA。先从小模型、LoRA 开始,效果好再逐步升级,这样更稳妥。

大模型微调这条路,从 LoRA 到 Q-LoRA,本质是在算力、效果、成本之间找平衡。没有绝对的最优方案,只有最适合你场景的选择。

版权声明: 本文首发于 指尖魔法屋-大模型微调折腾手记https://blog.thinkmoon.cn/post/137-practice-lora-qlora-model-finetuning/) 转载或引用必须申明原指尖魔法屋来源及源地址!