目录
- 微调基础:Full Fine-tuning vs PEFT
- LoRA / QLoRA / DoRA 原理
- 数据集准备
- PEFT + TRL 完整训练流程
- Unsloth 加速训练
- DPO 与对齐训练
- 模型合并与评估
- 部署转换:GGUF / AWQ / ONNX
- 微调 vs RAG 决策树
1. 微调基础:Full Fine-tuning vs PEFT
| 方法 | 训练参数量 | 显存需求(7B) | 适用场景 | 缺点 |
|---|---|---|---|---|
| Full Fine-tuning | 100% | 160GB+ | 数据充足、领域迁移 | 计算成本高、灾难性遗忘 |
| LoRA | 0.1-1% | 16GB | 绝大多数场景 | 表达能力有限(可通过 rank 调节) |
| QLoRA | 0.1-1% | 10GB | 消费级 GPU | 量化引入误差 |
| Prefix Tuning | 0.1% | 14GB | 分类/摘要 | 不适用生成任务 |
| Prompt Tuning | <0.01% | 12GB | 少样本场景 | 对提示敏感 |
PEFT 核心思想:冻结预训练模型的大部分参数,只训练少量新增参数(低秩矩阵、前缀嵌入等),大幅降低成本的同时保留模型能力。
2. LoRA / QLoRA / DoRA 原理
2.1 LoRA(Low-Rank Adaptation)
对于权重矩阵 $W \in \mathbb{R}^{d \times k}$,LoRA 不直接训练 $W$,而是训练低秩分解:
$$W’ = W + \Delta W = W + BA$$
其中 $B \in \mathbb{R}^{d \times r}$,$A \in \mathbb{R}^{r \times k}$,$r \ll \min(d, k)$(典型值 $r=8,16,64$)。
训练时 $W$ 冻结,只训练 $A$(高斯初始化)和 $B$(零初始化)。
from peft import LoraConfig, get_peft_model, TaskType
lora_config = LoraConfig(
r=16, # 低秩维度,越大表达越强,显存越高
lora_alpha=32, # 缩放因子,通常 = 2 * r
target_modules=[ # 应用 LoRA 的模块
"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, # 自回归语言模型
)
# 应用 LoRA 到基础模型
model = get_peft_model(base_model, lora_config)
model.print_trainable_parameters() # 通常 < 1%
2.2 QLoRA(4-bit 量化 + LoRA)
QLoRA 将基础模型量化为 4-bit(NormalFloat),大幅降低显存占用:
import torch
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
from peft import prepare_model_for_kbit_training
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True, # 嵌套量化,进一步压缩
bnb_4bit_quant_type="nf4", # Normal Float 4-bit
)
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b-hf",
quantization_config=bnb_config,
device_map="auto", # 自动分配到 GPU/CPU
trust_remote_code=True,
)
# 为量化模型准备训练
model = prepare_model_for_kbit_training(model)
model = get_peft_model(model, lora_config)
QLoRA 显存对比(Llama-2-7B):
- Full Fine-tuning: ~80GB
- LoRA (16-bit): ~22GB
- QLoRA (4-bit): ~10GB
2.3 DoRA(Weight-Decomposed Low-Rank Adaptation,2024)
DoRA 将权重分解为幅度(magnitude)和方向(direction),对方向应用 LoRA:
$$W’ = m \cdot \frac{W + BA}{|W + BA|_c}$$
表现优于 LoRA 且训练更稳定,阿里巴巴 Qwen3 已默认使用。
from peft import LoraConfig
# PEFT 0.11+ 支持 use_dora
lora_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "v_proj"],
use_dora=True, # 启用 DoRA
)
2.4 LoRA 超参数调优
| 参数 | 推荐范围 | 说明 |
|---|---|---|
| r | 8-128 | 简单任务 8-16,复杂任务 64-128 |
| lora_alpha | 16-256 | 通常 2*r,越大影响越强 |
| lora_dropout | 0.0-0.1 | 数据少时提高,大时设为 0 |
| target_modules | attention + mlp | 全部 target = 更强但更多参数 |
| modules_to_save | embed_tokens, lm_head | 需要时也训练输入/输出层 |
3. 数据集准备
3.1 标准格式(Alpaca / ShareGPT)
// Alpaca 格式
{
"instruction": "解释什么是 LoRA",
"input": "",
"output": "LoRA(Low-Rank Adaptation)是一种参数高效微调方法..."
}
// ShareGPT 格式(对话式)
{
"conversations": [
{"from": "human", "value": "什么是 QLoRA?"},
{"from": "gpt", "value": "QLoRA 结合了 4-bit 量化和 LoRA..."},
{"from": "human", "value": "和 LoRA 有什么区别?"},
{"from": "gpt", "value": "主要区别在于量化..."}
]
}
// OpenAI messages 格式(推荐)
{
"messages": [
{"role": "system", "content": "你是一个有帮助的AI助手。"},
{"role": "user", "content": "解释 LoRA"},
{"role": "assistant", "content": "LoRA 是..."}
]
}
3.2 数据集处理管道
from datasets import load_dataset, Dataset
import json
def load_alpaca_dataset(path: str) -> Dataset:
"""加载 Alpaca 格式数据集并转换为 messages 格式。"""
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
messages = []
for item in data:
user_content = item["instruction"]
if item.get("input"):
user_content += f"\n\n输入:{item['input']}"
messages.append({
"messages": [
{"role": "user", "content": user_content},
{"role": "assistant", "content": item["output"]},
]
})
return Dataset.from_list(messages)
# 加载 HuggingFace 公开数据集
class DatasetBuilder:
def __init__(self, tokenizer, max_length: int = 2048):
self.tokenizer = tokenizer
self.max_length = max_length
# 对话模板
self.tokenizer.chat_template = "{% for message in messages %}{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\n' }}{% endif %}"
def format_chat(self, example: dict) -> str:
"""将 messages 列表格式化为训练文本。"""
return self.tokenizer.apply_chat_template(
example["messages"],
tokenize=False,
add_generation_prompt=False,
)
def tokenize(self, examples: dict):
"""Tokenize 并创建标签掩码(只计算 assistant 部分的 loss)。"""
texts = [self.format_chat(ex) for ex in examples["messages"]]
# Tokenize
batch = self.tokenizer(
texts,
truncation=True,
max_length=self.max_length,
padding="max_length",
return_tensors=None,
)
# 创建 labels:user 部分设为 -100(不计算 loss)
labels = []
for text in texts:
tokens = self.tokenizer(text, truncation=True, max_length=self.max_length)
label = tokens["input_ids"].copy()
# 标记 assistant 回答的起始位置
# 策略:找到 "assistant" 对应 token,之后为训练目标
# 简化方案:找到最后的 "<|im_start|>assistant" 并设置此后的 token 为有效 label
# 实际实现需根据 tokenizer 精确匹配
labels.append(label)
batch["labels"] = labels
return batch
3.3 数据质量检查
def validate_dataset(dataset: Dataset) -> dict:
"""数据集质量检查。"""
stats = {
"total_samples": len(dataset),
"avg_input_length": 0,
"avg_output_length": 0,
"empty_outputs": 0,
"duplicates": 0,
}
seen = set()
for ex in dataset:
messages = ex.get("messages", [])
if not messages:
continue
user_msgs = [m for m in messages if m["role"] == "user"]
assistant_msgs = [m for m in messages if m["role"] == "assistant"]
stats["avg_input_length"] += sum(len(m["content"]) for m in user_msgs)
stats["avg_output_length"] += sum(len(m["content"]) for m in assistant_msgs)
if assistant_msgs and not assistant_msgs[-1]["content"].strip():
stats["empty_outputs"] += 1
key = json.dumps(messages, ensure_ascii=False)
if key in seen:
stats["duplicates"] += 1
seen.add(key)
stats["avg_input_length"] /= len(dataset)
stats["avg_output_length"] /= len(dataset)
return stats
4. PEFT + TRL 完整训练流程
import torch
from transformers import (
AutoModelForCausalLM, AutoTokenizer,
TrainingArguments, DataCollatorForSeq2Seq,
)
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from trl import SFTTrainer
from datasets import load_dataset
def train_lora_model(
model_name: str = "Qwen/Qwen2.5-7B-Instruct",
dataset_path: str = "data/alpaca.json",
output_dir: str = "./lora-output",
):
# 1. Tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
# 2. 4-bit 量化模型
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
)
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
device_map="auto",
trust_remote_code=True,
torch_dtype=torch.bfloat16,
)
# 3. PEFT 配置
peft_config = LoraConfig(
r=64,
lora_alpha=128,
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",
)
model = prepare_model_for_kbit_training(model)
model = get_peft_model(model, peft_config)
model.print_trainable_parameters()
# 4. 数据集
dataset = load_dataset("json", data_files=dataset_path, split="train")
# 5. 训练参数
training_args = TrainingArguments(
output_dir=output_dir,
num_train_epochs=3,
per_device_train_batch_size=2,
gradient_accumulation_steps=4, # 有效 batch = 2 * 4 = 8
learning_rate=2e-4,
max_grad_norm=0.3,
warmup_ratio=0.03,
lr_scheduler_type="cosine",
logging_steps=10,
save_strategy="epoch",
fp16=False,
bf16=True,
optim="paged_adamw_8bit", # QLoRA 推荐优化器
group_by_length=True, # 相似长度样本分组,提高效率
report_to="tensorboard",
)
# 6. 训练
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
train_dataset=dataset,
peft_config=peft_config,
dataset_text_field="text",
max_seq_length=2048,
args=training_args,
packing=True, # 多个短样本打包进一个序列
)
trainer.train()
trainer.model.save_pretrained(f"{output_dir}/final")
tokenizer.save_pretrained(f"{output_dir}/final")
return trainer
5. Unsloth 加速训练
Unsloth 通过手动优化 CUDA kernel,使训练速度提升 2-5x,显存降低 50-80%。
from unsloth import FastLanguageModel
from trl import SFTTrainer
from transformers import TrainingArguments
# Unsloth 支持的模型列表见 https://github.com/unslothai/unsloth
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="unsloth/Llama-3.2-3B-Instruct",
max_seq_length=2048,
dtype=None, # auto detect fp16/bf16
load_in_4bit=True,
)
# 添加 LoRA
model = FastLanguageModel.get_peft_model(
model,
r=16,
target_modules=[
"q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj",
],
lora_alpha=16,
lora_dropout=0,
bias="none",
use_gradient_checkpointing="unsloth", # Unsloth 优化版 checkpoint
random_state=42,
)
# 训练
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
train_dataset=dataset,
dataset_text_field="text",
max_seq_length=2048,
args=TrainingArguments(
per_device_train_batch_size=2,
gradient_accumulation_steps=4,
warmup_steps=5,
max_steps=60,
learning_rate=2e-4,
fp16=not torch.cuda.is_bf16_supported(),
bf16=torch.cuda.is_bf16_supported(),
logging_steps=1,
optim="adamw_8bit",
weight_decay=0.01,
seed=42,
output_dir="unsloth-output",
),
)
trainer.train()
# 推理加速:Unsloth 自动应用 optimized generation
from unsloth.chat_templates import get_chat_template
tokenizer = get_chat_template(tokenizer, chat_template="llama-3.1")
6. DPO 与对齐训练
6.1 DPO(Direct Preference Optimization)
DPO 直接用偏好数据(chosen > rejected)训练,无需单独的 reward model:
from trl import DPOTrainer
from peft import PeftModel
# DPO 数据格式:prompt + chosen + rejected
dpo_dataset = load_dataset("json", data_files="dpo_data.json")
dpo_trainer = DPOTrainer(
model=model, # SFT 后的模型
ref_model=ref_model, # 原始参考模型
args=TrainingArguments(
output_dir="./dpo-output",
per_device_train_batch_size=1,
num_train_epochs=1,
learning_rate=5e-7, # DPO 学习率更低
beta=0.1, # DPO 温度参数
),
train_dataset=dpo_dataset["train"],
tokenizer=tokenizer,
)
dpo_trainer.train()
6.2 ORPO(Odds Ratio Preference Optimization)
将 SFT 和偏好对齐合并到一步完成,省去参考模型:
from trl import ORPOTrainer
orpo_trainer = ORPOTrainer(
model=model,
train_dataset=dataset,
tokenizer=tokenizer,
args=TrainingArguments(
output_dir="./orpo-output",
learning_rate=1e-5,
lr_scheduler_type="cosine",
optim="adamw_torch",
),
)
orpo_trainer.train()
7. 模型合并与评估
7.1 合并 LoRA 权重到基础模型
from peft import PeftModel
from transformers import AutoModelForCausalLM
base_model = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen2.5-7B-Instruct",
torch_dtype=torch.bfloat16,
device_map="auto",
)
# 加载 LoRA 权重
peft_model = PeftModel.from_pretrained(base_model, "./lora-output/final")
# 合并并保存完整模型
merged_model = peft_model.merge_and_unload()
merged_model.save_pretrained("./merged-model")
7.2 持续预训练(Domain Adaptation)
# 在领域数据上先进行继续预训练(CPT),再做 SFT
cpt_args = TrainingArguments(
output_dir="./cpt-output",
per_device_train_batch_size=4,
num_train_epochs=1,
learning_rate=1e-5, # CPT 学习率比 SFT 更低
warmup_ratio=0.01,
save_steps=500,
logging_steps=10,
bf16=True,
)
# CPT 使用无监督数据,labels = input_ids(自回归预测下一个 token)
trainer = Trainer(
model=base_model,
args=cpt_args,
train_dataset=unsupervised_dataset,
data_collator=DataCollatorForLanguageModeling(tokenizer, mlm=False),
)
trainer.train()
7.3 评估指标
from evaluate import load
class ModelEvaluator:
def __init__(self, model, tokenizer):
self.model = model
self.tokenizer = tokenizer
self.bleu = load("bleu")
self.rouge = load("rouge")
def evaluate_generation(self, test_data: list[dict]) -> dict:
predictions = []
references = []
for item in test_data:
prompt = item["input"]
expected = item["output"]
inputs = self.tokenizer(prompt, return_tensors="pt").to(self.model.device)
outputs = self.model.generate(
**inputs,
max_new_tokens=256,
temperature=0.7,
top_p=0.9,
do_sample=True,
)
generated = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
predictions.append(generated)
references.append([expected])
bleu = self.bleu.compute(predictions=predictions, references=references)
rouge = self.rouge.compute(predictions=predictions, references=[r[0] for r in references])
return {"bleu": bleu, "rouge": rouge}
8. 部署转换:GGUF / AWQ / ONNX
8.1 GGUF(llama.cpp 格式)
最适合 CPU 推理和边缘设备:
# 安装 llama.cpp 转换工具
pip install llama-cpp-python
# 从 transformers 模型转 GGUF(使用 HuggingFace 转换脚本)
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp
python convert_hf_to_gguf.py ../merged-model \
--outfile model-f16.gguf \
--outtype f16
# 量化(推荐 Q4_K_M,平衡质量与速度)
./llama-quantize model-f16.gguf model-Q4_K_M.gguf Q4_K_M
# Python 推理
from llama_cpp import Llama
llm = Llama(
model_path="model-Q4_K_M.gguf",
n_ctx=4096,
n_threads=8,
verbose=False,
)
output = llm(
"Q: 什么是 LoRA?\nA: ",
max_tokens=256,
temperature=0.7,
stop=["Q:", "\n"],
)
print(output["choices"][0]["text"])
8.2 AWQ(Activation-aware Weight Quantization)
保持激活精度,适合 GPU 推理:
from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer
# 量化
model = AutoAWQForCausalLM.from_pretrained("merged-model")
quant_config = {"zero_point": True, "q_group_size": 128, "w_bit": 4, "version": "GEMM"}
model.quantize(tokenizer, quant_config=quant_config)
model.save_quantized("awq-model")
# 推理
from autoawq import AutoAWQForCausalLM
model = AutoAWQForCausalLM.from_quantized("awq-model")
8.3 ONNX 导出
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
model = AutoModelForCausalLM.from_pretrained("merged-model")
tokenizer = AutoTokenizer.from_pretrained("merged-model")
# 导出为 ONNX
dummy_input = tokenizer("Hello", return_tensors="pt")
torch.onnx.export(
model,
(dummy_input["input_ids"],),
"model.onnx",
input_names=["input_ids"],
output_names=["logits"],
dynamic_axes={
"input_ids": {0: "batch", 1: "sequence"},
"logits": {0: "batch", 1: "sequence"},
},
opset_version=17,
)
# ONNX Runtime 推理
import onnxruntime as ort
session = ort.InferenceSession("model.onnx", providers=["CUDAExecutionProvider"])
outputs = session.run(None, {"input_ids": dummy_input["input_ids"].numpy()})
8.4 部署格式对比
| 格式 | 大小(7B) | 推理速度 | 硬件 | 适用场景 |
|---|---|---|---|---|
| FP16 | 14GB | 基准 | GPU | 开发/高精度需求 |
| AWQ 4-bit | 4GB | 1.5x | GPU | 生产 GPU 部署 |
| GPTQ 4-bit | 4GB | 1.5x | GPU | 生产 GPU 部署 |
| GGUF Q4_K_M | 4GB | 0.3x (CPU) | CPU | 边缘设备/桌面 |
| ONNX | 14GB | 1.2x | CPU/GPU | 跨平台部署 |
9. 微调 vs RAG 决策树
新任务?
├── 需要模型掌握新知识(如内部文档)?
│ ├── 知识频繁更新? → RAG(动态检索)
│ └── 知识固定?
│ ├── 数据量 < 10K? → Few-shot Prompt
│ ├── 10K-100K? → LoRA 微调
│ └── > 100K? → Full Fine-tuning 或 QLoRA
├── 需要改变模型行为/风格?
│ ├── 对话风格 → SFT(LoRA/QLoRA)
│ ├── 对齐价值观 → DPO/ORPO
│ └── 拒绝某些回答 → 安全微调 + 输出过滤
├── 需要提升特定任务准确率?
│ ├── 已有充足标注数据 → LoRA 微调
│ └── 标注数据少 → RAG + Few-shot
└── 延迟要求 < 100ms?
└── 是 → 考虑模型蒸馏为小模型后再微调
组合策略(生产中常见):
- 基础模型 + LoRA 领域适配(CPT + SFT)
- RAG 提供实时知识
- DPO 对齐输出偏好
- vLLM 加速推理部署
交叉链接:
- Python 数据科学与 AI — PyTorch 基础与 GPU 环境
- RAG 架构实战 — 微调 vs RAG 的选择
- LLM 推理部署与优化 — GGUF/AWQ 部署后的推理优化
- Python 现代工具链 — uv 管理训练依赖环境
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。