第 12 章 · 微调生成模型
本章目标:掌握生成式 LLM 微调的两步法——先用 QLoRA 做指令微调(SFT),再用 DPO 做人类偏好对齐,全程使用 TRL 库在消费级 GPU 上完成。
12.1 环境准备(可选)
如果你在 Google Colab(或其他云平台)上查看本笔记本,需要取消注释并运行下面的代码块来安装本章依赖:
💡 注意:运行本章示例需要 GPU。在 Google Colab 中,进入 Runtime > Change runtime type > Hardware accelerator > GPU > GPU type > T4。
# %%capture
# !pip install -q accelerate==0.31.0 peft==0.11.1 bitsandbytes==0.43.1 transformers==4.41.2 trl==0.9.4 sentencepiece==0.2.0 triton==3.1.012.2 监督微调(SFT)
12.2.1 数据预处理
第一步是把对话数据格式化成模型使用的聊天模板。我们加载 TinyLlama 的分词器专门用来调用 apply_chat_template:
from transformers import AutoTokenizer
from datasets import load_dataset
# Load a tokenizer to use its chat template
template_tokenizer = AutoTokenizer.from_pretrained("TinyLlama/TinyLlama-1.1B-Chat-v1.0")
def format_prompt(example):
"""Format the prompt to using the <|user|> template TinyLLama is using"""
# Format answers
chat = example["messages"]
prompt = template_tokenizer.apply_chat_template(chat, tokenize=False)
return {"text": prompt}
# Load and format the data using the template TinyLLama is using
dataset = (
load_dataset("HuggingFaceH4/ultrachat_200k", split="test_sft")
.shuffle(seed=42)
.select(range(3_000))
)
dataset = dataset.map(format_prompt)看一条格式化后的 prompt 长什么样:
# Example of formatted prompt
print(dataset["text"][2576])12.2.2 模型量化
加载基座模型 TinyLlama-1.1B 并做 4-bit 量化(这就是 QLoRA 中的 "Q")。BitsAndBytesConfig 配置了 NF4 量化类型、float16 计算精度和嵌套量化:
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
model_name = "TinyLlama/TinyLlama-1.1B-intermediate-step-1431k-3T"
# 4-bit quantization configuration - Q in QLoRA
bnb_config = BitsAndBytesConfig(
load_in_4bit=True, # Use 4-bit precision model loading
bnb_4bit_quant_type="nf4", # Quantization type
bnb_4bit_compute_dtype="float16", # Compute dtype
bnb_4bit_use_double_quant=True, # Apply nested quantization
)
# Load the model to train on the GPU
model = AutoModelForCausalLM.from_pretrained(
model_name,
device_map="auto",
# Leave this out for regular SFT
quantization_config=bnb_config,
)
model.config.use_cache = False
model.config.pretraining_tp = 1
# Load LLaMA tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=False)
tokenizer.pad_token = "<PAD>"
tokenizer.padding_side = "left"12.2.3 配置
LoRA 配置
LoRA 不更新原模型权重,而是在注意力与 MLP 的投影层上注入低秩适配器。r=64 是秩,lora_alpha=32 是缩放系数:
from peft import LoraConfig, prepare_model_for_kbit_training, get_peft_model
# Prepare LoRA Configuration
peft_config = LoraConfig(
lora_alpha=32, # LoRA Scaling
lora_dropout=0.1, # Dropout for LoRA Layers
r=64, # Rank
bias="none",
task_type="CAUSAL_LM",
target_modules= # Layers to target
['k_proj', 'gate_proj', 'v_proj', 'up_proj', 'q_proj', 'o_proj', 'down_proj']
)
# prepare model for training
model = prepare_model_for_kbit_training(model)
model = get_peft_model(model, peft_config)训练配置
训练参数使用梯度累积(等效 batch size 为 8)与梯度检查点来节省显存:
from transformers import TrainingArguments
output_dir = "./results"
# Training arguments
training_arguments = TrainingArguments(
output_dir=output_dir,
per_device_train_batch_size=2,
gradient_accumulation_steps=4,
optim="paged_adamw_32bit",
learning_rate=2e-4,
lr_scheduler_type="cosine",
num_train_epochs=1,
logging_steps=10,
fp16=True,
gradient_checkpointing=True
)12.2.4 开始训练!
使用 TRL 的 SFTTrainer 执行监督微调,训练完成后保存 QLoRA 权重:
from trl import SFTTrainer
# Set supervised fine-tuning parameters
trainer = SFTTrainer(
model=model,
train_dataset=dataset,
dataset_text_field="text",
tokenizer=tokenizer,
args=training_arguments,
max_seq_length=512,
# Leave this out for regular SFT
peft_config=peft_config,
)
# Train model
trainer.train()
# Save QLoRA weights
trainer.model.save_pretrained("TinyLlama-1.1B-qlora")12.2.5 合并 Adapter
训练得到的是 LoRA 适配器权重,推理前需要把它合并回基座模型:
from peft import AutoPeftModelForCausalLM
model = AutoPeftModelForCausalLM.from_pretrained(
"TinyLlama-1.1B-qlora",
low_cpu_mem_usage=True,
device_map="auto",
)
# Merge LoRA and base model
merged_model = model.merge_and_unload()12.2.6 推理
用 TinyLlama 预定义的提示模板测试指令微调后的模型:
from transformers import pipeline
# Use our predefined prompt template
prompt = """<|user|>
Tell me something about Large Language Models.</s>
<|assistant|>
"""
# Run our instruction-tuned model
pipe = pipeline(task="text-generation", model=merged_model, tokenizer=tokenizer)
print(pipe(prompt)[0]["generated_text"])12.3 偏好对齐微调(PPO/DPO)
12.3.1 数据预处理
DPO 需要的数据形态是「同一个 prompt 对应一个 chosen 回答和一个 rejected 回答」。这里使用 argilla/distilabel-intel-orca-dpo-pairs 数据集,并过滤掉平局、低分和属于 GSM8K 训练集的样本:
from datasets import load_dataset
def format_prompt(example):
"""Format the prompt to using the <|user|> template TinyLLama is using"""
# Format answers
system = "<|system|>\n" + example['system'] + "</s>\n"
prompt = "<|user|>\n" + example['input'] + "</s>\n<|assistant|>\n"
chosen = example['chosen'] + "</s>\n"
rejected = example['rejected'] + "</s>\n"
return {
"prompt": system + prompt,
"chosen": chosen,
"rejected": rejected,
}
# Apply formatting to the dataset and select relatively short answers
dpo_dataset = load_dataset("argilla/distilabel-intel-orca-dpo-pairs", split="train")
dpo_dataset = dpo_dataset.filter(
lambda r:
r["status"] != "tie" and
r["chosen_score"] >= 8 and
not r["in_gsm8k_train"]
)
dpo_dataset = dpo_dataset.map(format_prompt, remove_columns=dpo_dataset.column_names)
dpo_dataset12.3.2 模型量化
重新加载 SFT 阶段保存的 QLoRA 权重并合并,为 DPO 阶段准备基座:
from peft import AutoPeftModelForCausalLM
from transformers import BitsAndBytesConfig, AutoTokenizer
# 4-bit quantization configuration - Q in QLoRA
bnb_config = BitsAndBytesConfig(
load_in_4bit=True, # Use 4-bit precision model loading
bnb_4bit_quant_type="nf4", # Quantization type
bnb_4bit_compute_dtype="float16", # Compute dtype
bnb_4bit_use_double_quant=True, # Apply nested quantization
)
# Merge LoRA and base model
model = AutoPeftModelForCausalLM.from_pretrained(
"TinyLlama-1.1B-qlora",
low_cpu_mem_usage=True,
device_map="auto",
quantization_config=bnb_config,
)
merged_model = model.merge_and_unload()
# Load LLaMA tokenizer
model_name = "TinyLlama/TinyLlama-1.1B-intermediate-step-1431k-3T"
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=False)
tokenizer.pad_token = "<PAD>"
tokenizer.padding_side = "left"12.3.3 配置
LoRA 配置与 SFT 阶段相同:
from peft import LoraConfig, prepare_model_for_kbit_training, get_peft_model
# Prepare LoRA Configuration
peft_config = LoraConfig(
lora_alpha=32, # LoRA Scaling
lora_dropout=0.1, # Dropout for LoRA Layers
r=64, # Rank
bias="none",
task_type="CAUSAL_LM",
target_modules= # Layers to target
['k_proj', 'gate_proj', 'v_proj', 'up_proj', 'q_proj', 'o_proj', 'down_proj']
)
# prepare model for training
model = prepare_model_for_kbit_training(model)
model = get_peft_model(model, peft_config)DPO 使用专门的 DPOConfig,学习率比 SFT 更小(1e-5),并用 max_steps 控制训练步数:
from trl import DPOConfig
output_dir = "./results"
# Training arguments
training_arguments = DPOConfig(
output_dir=output_dir,
per_device_train_batch_size=2,
gradient_accumulation_steps=4,
optim="paged_adamw_32bit",
learning_rate=1e-5,
lr_scheduler_type="cosine",
max_steps=200,
logging_steps=10,
fp16=True,
gradient_checkpointing=True,
warmup_ratio=0.1
)12.3.4 DPO 训练
DPOTrainer 通过 beta=0.1 控制偏离参考模型的强度,训练后保存 DPO 适配器:
from trl import DPOTrainer
# Create DPO trainer
dpo_trainer = DPOTrainer(
model,
args=training_arguments,
train_dataset=dpo_dataset,
tokenizer=tokenizer,
peft_config=peft_config,
beta=0.1,
max_prompt_length=512,
max_length=512,
)
# Fine-tune model with DPO
dpo_trainer.train()
# Save adapter
dpo_trainer.model.save_pretrained("TinyLlama-1.1B-dpo-qlora")最后把 SFT 模型与 DPO 适配器逐层合并,得到最终的偏好对齐模型:
from peft import PeftModel
# Merge LoRA and base model
model = AutoPeftModelForCausalLM.from_pretrained(
"TinyLlama-1.1B-qlora",
low_cpu_mem_usage=True,
device_map="auto",
)
sft_model = model.merge_and_unload()
# Merge DPO LoRA and SFT model
dpo_model = PeftModel.from_pretrained(
sft_model,
"TinyLlama-1.1B-dpo-qlora",
device_map="auto",
)
dpo_model = dpo_model.merge_and_unload()用同样的提示模板对比 DPO 前后的输出质量:
from transformers import pipeline
# Use our predefined prompt template
prompt = """<|user|>
Tell me something about Large Language Models.</s>
<|assistant|>
"""
# Run our instruction-tuned model
pipe = pipeline(task="text-generation", model=dpo_model, tokenizer=tokenizer)
print(pipe(prompt)[0]["generated_text"])本章小结
- 生成式 LLM 微调两步法:先 SFT 指令微调教会模型「怎么回答」,再 DPO 偏好对齐教会模型「回答得更好」;
- QLoRA = 4-bit NF4 量化 + LoRA:
BitsAndBytesConfig负责量化配置,让 1.1B 模型的微调能在单张 T4 上完成; - 聊天模板必须与基座一致:直接复用 TinyLlama 分词器的
apply_chat_template,避免格式错位; - SFT 用
SFTTrainer+TrainingArguments,DPO 用DPOTrainer+DPOConfig,后者额外需要 chosen/rejected 成对数据与beta强度参数; - 多阶段适配器可叠加合并:QLoRA → merge → 再挂 DPO LoRA → 再 merge,最终得到单一完整模型。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. QLoRA 中的 "Q" 指的是什么?
2. DPO 训练数据与 SFT 数据的核心区别是?
3. 关于 SFT 与 DPO 的训练配置,下列说法正确的是?
4. SFT 与 DPO 两个阶段的 LoRA 适配器如何组合成最终模型?
🛠️ 动手实践
- 把 SFT 数据量从
range(3_000)改为range(1_000),其余超参不变,对比两种数据规模下推理输出的连贯性差异。 - 将 LoRA 的
r从 64 改成 16、lora_alpha从 32 改成 16,重新走一遍 SFT 流程,比较训练显存占用与生成质量的变化。 - 在 DPO 数据过滤条件中把
chosen_score >= 8放宽为>= 5,观察可用样本数量的变化,并对比放宽前后模型的回答风格差异。