实战:LLM 微调全流程
数据准备 → 数据预处理 → QLoRA 微调 → 模型合并 → 推理部署 全流程实战
项目概述
本文档以微调 Qwen2.5-7B-Instruct 模型为例,演示从数据准备到推理部署的完整微调流程。目标是让读者掌握如何使用 QLoRA 技术在消费级 GPU(如 RTX 3090/4090)上对大语言模型进行高效微调。
技术栈:
- transformers:加载预训练模型和 tokenizer
- peft:LoRA/QLoRA 参数高效微调
- bitsandbytes:4-bit 量化,大幅降低显存占用
- trl:SFTTrainer 简化监督微调流程
- datasets:Hugging Face 数据集加载与处理
Step 1:数据准备
数据集格式
微调数据的常见格式有两种:
ShareGPT 格式(多轮对话):
json
{
"conversations": [
{"from": "system", "value": "你是一个有用的 AI 助手。"},
{"from": "human", "value": "什么是机器学习?"},
{"from": "gpt", "value": "机器学习是人工智能的一个分支..."}
]
}Alpaca 格式(单轮指令):
json
{
"instruction": "解释什么是机器学习",
"input": "",
"output": "机器学习是人工智能的一个分支..."
}数据来源
可以从以下途径获取训练数据:
- 自定义 JSONL 文件:标注业务场景数据,每行一条 JSON
- Hugging Face Datasets:直接加载社区开源数据集,如
datasets.load_dataset("json", data_files="train.jsonl")
加载和预览数据集
python
from datasets import load_dataset
dataset = load_dataset("json", data_files="train.jsonl", split="train")
print(f"数据集大小: {len(dataset)}")
print(dataset[0])Step 2:数据预处理
Tokenizer 配置
加载 tokenizer 并进行关键参数配置:
python
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B-Instruct")
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"模板化处理
将原始数据按照对话模板拼接成模型所需的输入格式。对于 Qwen 模型,采用 System + User + Assistant 的结构:
python
def format_chat_template(example):
messages = [
{"role": "system", "content": "你是一个有用的 AI 助手。"},
{"role": "user", "content": example["instruction"]},
{"role": "assistant", "content": example["output"]}
]
text = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=False
)
return {"text": text}预处理函数
对数据集应用模板化并设定最大长度:
python
def preprocess_dataset(dataset, max_length=2048):
dataset = dataset.map(format_chat_template)
return dataset
train_dataset = preprocess_dataset(dataset)Step 3:QLoRA 微调
4-bit 量化配置
python
from transformers import BitsAndBytesConfig
import torch
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
)LoRA 配置
python
from peft import LoraConfig
lora_config = LoraConfig(
r=8,
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="CAUSAL_LM",
)训练参数配置
python
from transformers import TrainingArguments
training_args = TrainingArguments(
output_dir="./qwen-qlora-checkpoints",
per_device_train_batch_size=2,
gradient_accumulation_steps=4,
learning_rate=2e-4,
num_train_epochs=3,
logging_steps=10,
save_steps=500,
save_total_limit=2,
bf16=True,
optim="paged_adamw_8bit",
lr_scheduler_type="cosine",
warmup_ratio=0.03,
max_grad_norm=0.3,
report_to="none",
)SFTTrainer 训练
python
from trl import SFTTrainer
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=train_dataset,
tokenizer=tokenizer,
peft_config=lora_config,
max_seq_length=2048,
dataset_text_field="text",
)
trainer.train()
trainer.save_model("./qwen-qlora-final")完整训练代码
将以上所有片段整合为一个可执行的训练脚本:
python
import torch
from datasets import load_dataset
from transformers import (
AutoModelForCausalLM, AutoTokenizer,
BitsAndBytesConfig, TrainingArguments
)
from peft import LoraConfig
from trl import SFTTrainer
# 配置
model_name = "Qwen/Qwen2.5-7B-Instruct"
output_dir = "./qwen-qlora-final"
# 4-bit 量化
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
)
# 加载模型和 tokenizer
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
device_map="auto",
torch_dtype=torch.bfloat16,
trust_remote_code=True,
)
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
# 加载数据集
dataset = load_dataset("json", data_files="train.jsonl", split="train")
# 数据预处理
def format_func(example):
messages = [
{"role": "system", "content": "你是一个有用的 AI 助手。"},
{"role": "user", "content": example["instruction"]},
{"role": "assistant", "content": example["output"]}
]
return {"text": tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=False
)}
dataset = dataset.map(format_func)
# LoRA 配置
lora_config = LoraConfig(
r=8, 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="CAUSAL_LM",
)
# 训练参数
training_args = TrainingArguments(
output_dir="./checkpoints",
per_device_train_batch_size=2,
gradient_accumulation_steps=4,
learning_rate=2e-4,
num_train_epochs=3,
logging_steps=10,
save_steps=500,
save_total_limit=2,
bf16=True,
optim="paged_adamw_8bit",
lr_scheduler_type="cosine",
warmup_ratio=0.03,
report_to="none",
)
# 训练
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=dataset,
tokenizer=tokenizer,
peft_config=lora_config,
max_seq_length=2048,
dataset_text_field="text",
)
trainer.train()
trainer.save_model(output_dir)Step 4:模型合并
LoRA 微调保存的只是适配器权重(通常只有几十 MB),无法独立用于推理。需要将 LoRA 权重合并回基础模型,生成完整的模型文件。
python
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
# 基础模型路径(原始模型)
base_model_name = "Qwen/Qwen2.5-7B-Instruct"
# LoRA 适配器路径
adapter_path = "./qwen-qlora-final"
# 合并后输出路径
merged_path = "./qwen-merged"
# 加载基础模型
model = AutoModelForCausalLM.from_pretrained(
base_model_name,
torch_dtype=torch.bfloat16,
device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained(base_model_name)
# 加载 LoRA 权重并合并
model = PeftModel.from_pretrained(model, adapter_path)
merged_model = model.merge_and_unload()
# 保存合并后的完整模型
merged_model.save_pretrained(merged_path, safe_serialization=True)
tokenizer.save_pretrained(merged_path)
print(f"合并后的模型已保存至: {merged_path}")Step 5:推理部署
加载合并后的完整模型进行推理测试:
python
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
model_path = "./qwen-merged"
model = AutoModelForCausalLM.from_pretrained(
model_path,
torch_dtype=torch.bfloat16,
device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained(model_path)
def chat(prompt: str, max_new_tokens: int = 512):
messages = [
{"role": "system", "content": "你是一个有用的 AI 助手。"},
{"role": "user", "content": prompt},
]
text = tokenizer.apply_chat_template(
messages, tokenize=True, add_generation_prompt=True
)
inputs = tokenizer(text, return_tensors="pt").to(model.device)
outputs = model.generate(
**inputs,
max_new_tokens=max_new_tokens,
temperature=0.7,
top_p=0.9,
do_sample=True,
)
response = tokenizer.decode(outputs[0][inputs["input_ids"].shape[1]:],
skip_special_tokens=True)
return response
# 测试
print(chat("请用 Python 实现一个快速排序算法"))完整训练脚本
以下是一个端到端的训练脚本,整合了上述所有步骤,可直接在单卡 GPU 环境下运行:
python
import torch
from datasets import load_dataset
from transformers import (
AutoModelForCausalLM, AutoTokenizer,
BitsAndBytesConfig, TrainingArguments
)
from peft import LoraConfig, PeftModel
from trl import SFTTrainer
def train():
"""执行完整的 QLoRA 训练流程"""
model_name = "Qwen/Qwen2.5-7B-Instruct"
# 1. 4-bit 量化加载
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
),
device_map="auto",
torch_dtype=torch.bfloat16,
)
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
# 2. 数据准备
dataset = load_dataset("json", data_files="train.jsonl", split="train")
dataset = dataset.map(lambda x: {
"text": tokenizer.apply_chat_template(
[{"role": "system", "content": "你是一个有用的 AI 助手。"},
{"role": "user", "content": x["instruction"]},
{"role": "assistant", "content": x["output"]}],
tokenize=False, add_generation_prompt=False
)
})
# 3. QLoRA 微调
trainer = SFTTrainer(
model=model,
args=TrainingArguments(
output_dir="./checkpoints",
per_device_train_batch_size=2,
gradient_accumulation_steps=4,
learning_rate=2e-4,
num_train_epochs=3,
bf16=True,
logging_steps=10,
save_steps=500,
save_total_limit=2,
optim="paged_adamw_8bit",
lr_scheduler_type="cosine",
warmup_ratio=0.03,
report_to="none",
),
train_dataset=dataset,
tokenizer=tokenizer,
peft_config=LoraConfig(
r=8, 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="CAUSAL_LM",
),
max_seq_length=2048,
dataset_text_field="text",
)
trainer.train()
trainer.save_model("./lora-adapter")
def merge():
"""将 LoRA 权重合并回基础模型"""
model = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen2.5-7B-Instruct",
torch_dtype=torch.bfloat16,
device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B-Instruct")
model = PeftModel.from_pretrained(model, "./lora-adapter")
merged = model.merge_and_unload()
merged.save_pretrained("./qwen-merged", safe_serialization=True)
tokenizer.save_pretrained("./qwen-merged")
print("模型合并完成。")
if __name__ == "__main__":
train()
merge()总结
本文档覆盖了 LLM 微调的完整流程:
- 数据准备:选择 ShareGPT 或 Alpaca 格式,从 JSONL 文件或 Hugging Face Datasets 加载数据
- 数据预处理:配置 tokenizer,将原始数据格式化为对话模板
- QLoRA 微调:通过 4-bit 量化将 7B 模型的显存需求降至约 8-10 GB,配合 LoRA 实现单卡微调
- 模型合并:使用
merge_and_unload()将适配器权重合并到基础模型中 - 推理部署:加载合并后的模型,配置生成参数进行推理
整个流程可在 RTX 3090/4090(24 GB 显存)上完整运行,是低成本微调大模型的推荐实践方案。