🔧 大模型微调
让通用大模型适配你的业务场景 —— 从数据准备到模型部署的完整指南
为什么需要微调?
通用大模型(如 GPT-4、Llama 3)虽然能力强大,但在特定领域和任务上往往不够精准。微调(Fine-tuning)是在预训练模型基础上,使用特定领域或任务的数据继续训练,使模型适应用户需求。
迁移学习:微调的理论基础
微调的本质是迁移学习(Transfer Learning)的核心应用。预训练阶段让模型学会了语言的通用规律(语法、语义、常识推理等),这些知识以参数权重的形式存储在模型中。微调则是将这些通用知识「迁移」到特定任务上——只需要在已有知识的基础上进行小幅度调整,而不需要从头学习。
# 迁移学习的直觉理解
预训练模型 = 一个已经学会「阅读和写作」的学生
微调 = 让这个学生学习「医学」「法律」「编程」等专业知识
# 为什么有效?
# 1. 语言理解能力已具备(语法、语义、逻辑)
# 2. 只需要学习「领域知识」和「任务格式」
# 3. 所需数据量远小于从头训练(500条 vs 数万亿 token)
微调 vs Prompt Engineering vs RAG
三种让大模型适配业务需求的方式,各有优劣:
| 维度 | Prompt Engineering | RAG | 微调 |
|---|---|---|---|
| 原理 | 设计提示词引导模型 | 检索外部知识注入上下文 | 用领域数据训练模型参数 |
| 成本 | 零训练成本 | 需搭建检索系统 | 需 GPU 训练资源 |
| 效果稳定性 | 波动较大 | 依赖检索质量 | 最稳定可控 |
| 推理成本 | 高(长 prompt 消耗 token) | 高(检索 + 长上下文) | 低(无需额外 prompt) |
| 知识更新 | 即时 | 即时(更新知识库) | 需重新训练 |
| 适用场景 | 快速验证、简单任务 | 知识密集型问答 | 格式/风格要求严格的任务 |
💡 最佳实践:组合使用
实际项目中往往组合使用这三种方式:用 RAG 提供实时知识,用微调控制输出格式和风格,用 Prompt Engineering 做最后的引导。例如:微调后的模型 + RAG 检索 + System Prompt = 最佳效果。
微调的典型应用场景
以下场景特别适合通过微调来提升模型表现:
🏥 垂直领域问答
医疗、法律、金融等专业领域的问答系统。微调让模型掌握专业术语和领域知识,回答更准确。
💬 客服对话系统
让模型学习企业的客服话术、产品知识和回复风格,输出符合品牌调性的回答。
📝 特定格式输出
要求模型严格按 JSON、SQL、特定模板等格式输出。微调比 Prompt 更稳定可靠。
🌐 翻译与本地化
针对特定领域(如技术文档、医学论文)的翻译任务,微调可大幅提升术语准确性和行文流畅度。
💻 代码生成
针对特定框架、API 或内部代码规范微调,生成符合团队编码标准的代码。
🎭 角色扮演与风格
让模型扮演特定角色(如教师、顾问),或模仿特定写作风格(如学术论文、营销文案)。
微调方法分类
1. 全量微调(Full Fine-tuning)
更新模型的所有参数,效果通常最好,但需要与预训练相当的显存和计算资源。
- 优势:效果上限最高,模型完全适应目标数据
- 劣势:显存需求大(7B 模型约需 8×A100),训练成本高
- 适用场景:追求极致效果、资源充足的企业级项目
2. LoRA(Low-Rank Adaptation)
微软提出的参数高效微调方法,是目前最主流的微调方案。核心思想:冻结原始模型权重,在旁路注入两个低秩矩阵 A 和 B,只训练这两个小矩阵。
# LoRA 核心公式
W' = W + ΔW = W + B × A
# W: 原始权重矩阵(冻结)
# A: 降维矩阵 (d × r)
# B: 升维矩阵 (r × d)
# r: 秩(rank),通常取 8-64
- 优势:可训练参数仅原模型的 0.1%-1%,显存占用大幅降低
- 劣势:效果略低于全量微调(但差距很小)
- 适用场景:绝大多数微调任务的首选方案
3. QLoRA(Quantized LoRA)
在 LoRA 基础上,将基础模型量化为 4-bit(NF4 数据类型),进一步降低显存需求。使得在单张消费级 GPU(如 24GB 的 RTX 4090)上微调 7B-13B 模型成为可能。
4. 其他 PEFT 方法
- Prompt Tuning:在输入前加可学习的 soft prompt,只优化这些向量
- Prefix Tuning:在注意力键值前加可学习前缀
- Adapter:在 Transformer 层间插入小型瓶颈模块
🎯 方法选择建议
\n90% 的场景推荐 LoRA / QLoRA:性价比最高,效果接近全量微调。显存不足选 QLoRA(4-bit),显存充裕选 LoRA(BF16)。只有追求极致效果且资源充足时才考虑全量微调。
数据准备
数据质量是微调成功的最关键因素。好的数据比好的算法更重要。
数据格式
最常见的微调数据格式是指令式(Instruction),每条数据包含指令、可选输入和期望输出:
[
{
"instruction": "将以下英文翻译成中文",
"input": "Artificial intelligence is transforming the world.",
"output": "人工智能正在改变世界。"
},
{
"instruction": "根据以下代码解释其功能",
"input": "def fib(n):\n return n if n <= 1 else fib(n-1) + fib(n-2)",
"output": "这是一个递归实现的斐波那契数列函数..."
},
{
"instruction": "总结以下文章的核心观点",
"input": "(文章内容...)",
"output": "本文的核心观点包括三个方面:..."
}
]
数据质量要求
- 数量:LoRA 微调通常 500-5000 条高质量数据即可见效,10000+ 条可获得更好效果
- 质量:输出应准确、格式一致、无噪声。建议人工审核或过滤低质量样本
- 多样性:覆盖不同场景和表达方式,避免模型过拟合到单一模式
- 长度:控制合理的输入输出长度,过长的样本会浪费计算资源
数据构建策略
- 人工标注:质量最高但成本最大,适合构建种子数据集
- 大模型生成(Self-Instruct):用 GPT-4 等强模型生成训练数据,再人工筛选
- Evol-Instruct:对已有指令进行渐进式演化,增加复杂度和多样性
- 真实对话数据:收集用户真实交互日志,筛选高质量部分
实战:使用 Unsloth 微调 Llama
Unsloth 是目前最高效的开源 LLM 微调库,可将 LoRA 训练速度提升 2 倍,显存占用降低 50%。
环境安装
pip install unsloth
# 依赖:PyTorch >= 2.0, CUDA >= 11.8, transformers, peft, trl
完整训练代码
from unsloth import FastLanguageModel
from trl import SFTTrainer
from transformers import TrainingArguments
from datasets import load_dataset
# 1. 加载模型(4-bit 量化,节省显存)
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="unsloth/llama-3-8b-bnb-4bit",
max_seq_length=2048,
load_in_4bit=True,
)
# 2. 添加 LoRA 适配器
model = FastLanguageModel.get_peft_model(
model,
r=16, # LoRA 秩
lora_alpha=32, # 缩放系数
target_modules=[ # 应用 LoRA 的模块
"q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj",
],
lora_dropout=0.05,
bias="none",
)
# 3. 准备数据
def format_prompt(example):
text = f"""### 指令:
{example['instruction']}
### 输入:
{example.get('input', '')}
### 回答:
{example['output']}"""
return {"text": text}
dataset = load_dataset("json", data_files="train_data.json")["train"]
dataset = dataset.map(format_prompt)
# 4. 配置训练器
trainer = SFTTrainer(
model=model,
train_dataset=dataset,
args=TrainingArguments(
output_dir="./output",
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
num_train_epochs=3,
learning_rate=2e-4,
warmup_steps=10,
logging_steps=10,
save_strategy="steps",
save_steps=200,
fp16=True,
seed=42,
),
)
# 5. 开始训练
trainer.train()
# 6. 保存模型
model.save_pretrained("./finetuned_model")
tokenizer.save_pretrained("./finetuned_model")
推理测试
# 加载微调后的模型
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="./finetuned_model",
load_in_4bit=True,
)
# 生成回答
inputs = tokenizer(
"### 指令:\n解释什么是量子计算\n\n### 回答:\n",
return_tensors="pt"
).to("cuda")
outputs = model.generate(**inputs, max_new_tokens=512)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
Chat 对话格式微调
实际应用中,大多数场景需要模型以多轮对话的方式工作。Chat 微调使用 ChatML 格式,让模型学会「system / user / assistant」的角色区分。
ChatML 数据格式
# 多轮对话训练数据
{
"messages": [
{"role": "system", "content": "你是一个专业的Python编程导师,擅长用简单例子解释复杂概念。"},
{"role": "user", "content": "什么是递归?"},
{"role": "assistant", "content": "递归就是函数调用自己。就像照镜子时看到镜子里还有镜子一样..."},
{"role": "user", "content": "能给个代码例子吗?"},
{"role": "assistant", "content": "当然!最简单的例子是计算阶乘:\n\ndef factorial(n):\n if n <= 1:\n return 1\n return n * factorial(n-1)"}
]
}
Chat 微调代码
from trl import SFTTrainer
from transformers import TrainingArguments
# 使用 ChatML 模板格式化数据
def apply_chat_template(example, tokenizer):
messages = example["messages"]
text = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=False
)
return {"text": text}
dataset = dataset.map(
lambda ex: apply_chat_template(ex, tokenizer)
)
# 训练配置
trainer = SFTTrainer(
model=model,
train_dataset=dataset,
args=TrainingArguments(
output_dir="./chat-output",
per_device_train_batch_size=2,
gradient_accumulation_steps=8,
num_train_epochs=3,
learning_rate=2e-4,
warmup_ratio=0.03,
logging_steps=5,
fp16=True,
),
max_seq_length=2048,
)
trainer.train()
💬 对话微调注意事项
多轮对话微调时,建议使用 apply_chat_template 而非手动拼接文本,确保与模型预训练时的格式一致。同时注意 system prompt 不要过长,保持简洁的角色设定即可。
DPO 偏好对齐微调
SFT 让模型学会「怎么回答」,而 DPO(Direct Preference Optimization)让模型学会「哪种回答更好」。这是通过人类偏好数据来对齐模型输出的关键技术。
DPO 数据格式
DPO 需要「优选回答」和「淘汰回答」的成对数据:
{
"prompt": "解释什么是黑洞",
"chosen": "黑洞是宇宙中引力极强的区域,连光都无法逃脱。它由大质量恒星坍缩形成...",
"rejected": "黑洞就是一个很黑的洞,什么都出不来。"
}
DPO 训练代码
from trl import DPOTrainer, DPOConfig
# 基于 SFT 模型继续做 DPO 对齐
dpo_trainer = DPOTrainer(
model=model, # SFT 后的模型
ref_model=None, # None 表示隐式参考模型
args=DPOConfig(
output_dir="./dpo-output",
per_device_train_batch_size=2,
learning_rate=5e-5, # DPO 学习率通常比 SFT 更小
beta=0.1, # KL 散度惩罚系数
num_train_epochs=2,
logging_steps=10,
fp16=True,
),
train_dataset=dpo_dataset,
tokenizer=tokenizer,
)
dpo_trainer.train()
🎯 SFT → DPO 完整流程
完整的对齐流程是:预训练模型 → SFT(学会对话格式)→ DPO(学会偏好对齐)。SFT 让模型能按照指令回答,DPO 让模型的回答更符合人类期望(更有帮助、更安全、更诚实)。
训练监控与调试
训练过程中实时监控是保证质量的关键:
关键监控指标
- Training Loss:应持续下降并趋于稳定。如果突然升高,可能是学习率过大或数据异常
- Eval Loss:在验证集上的损失。如果 train loss 下降但 eval loss 上升,说明过拟合
- Learning Rate:观察学习率变化曲线是否符合 warmup + decay 的预期
- Gradient Norm:梯度范数,过大说明训练不稳定,建议降低学习率
使用 Weights & Biases 监控
import wandb
# 初始化 W&B
wandb.init(project="llm-finetune", name="lora-llama3-medical")
# 在 TrainingArguments 中启用
args = TrainingArguments(
...,
report_to="wandb", # 自动上报指标到 W&B
logging_steps=10,
eval_strategy="steps", # 每隔 eval_steps 评估一次
eval_steps=50,
)
# W&B 面板会自动生成:
# - Loss 曲线(train / eval)
# - 学习率变化图
# - 梯度范数监控
# - 超参数记录与对比
常见 Loss 曲线解读
# 正常收敛:Loss 平稳下降
t=0 t=100 t=200 t=300
2.5 ──╮
│ ╲
1.8 ──┤ ╲
│ ╲───
1.2 ──┤ ╲───
│ ╲──
0.8 ──┴──────────────────
# 过拟合:Train Loss 下降,Eval Loss 上升
# 发散:Loss 突然飙升 → 降低学习率
# 震荡:Loss 上下波动 → 减小学习率或增加 batch size
模型合并与部署
LoRA 微调后,需要将适配器权重与基础模型合并,才能部署到生产环境。
合并 LoRA 权重
from peft import PeftModel
from transformers import AutoModelForCausalLM
# 加载基础模型
base_model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-3-8B",
torch_dtype="auto",
)
# 加载 LoRA 适配器
model = PeftModel.from_pretrained(
base_model,
"./finetuned_model", # LoRA 权重目录
)
# 合并权重(将 LoRA 参数融入基础模型)
merged_model = model.merge_and_unload()
# 保存合并后的完整模型
merged_model.save_pretrained("./merged_model")
tokenizer.save_pretrained("./merged_model")
导出为 GGUF 格式(本地部署)
# 转换为 llama.cpp 格式,支持 CPU/Mac 本地运行
# 1. 转换为 safetensors
python convert_hf_to_gguf.py ./merged_model --outfile model-f16.gguf
# 2. 量化为 Q4_K_M(推荐,体积和质量的平衡点)
./llama-quantize model-f16.gguf model-q4_k_m.gguf Q4_K_M
# 3. 使用 Ollama 运行
# 创建 Modelfile
echo 'FROM ./model-q4_k_m.gguf' > Modelfile
ollama create my-model -f Modelfile
ollama run my-model
使用 vLLM 部署 API 服务
# 生产环境推荐使用 vLLM 提供高性能推理服务
pip install vllm
# 启动 OpenAI 兼容 API 服务
python -m vllm.entrypoints.openai.api_server \
--model ./merged_model \
--host 0.0.0.0 \
--port 8000 \
--gpu-memory-utilization 0.9
# 测试调用
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model": "./merged_model", "messages": [{"role": "user", "content": "你好"}]}'
微调工具生态对比
| 工具 | 特点 | 适合场景 | 上手难度 |
|---|---|---|---|
| Unsloth | 速度 2x、显存 -50%,自定义 CUDA 内核优化 | 追求训练效率,消费级 GPU | ⭐⭐ |
| LLaMA-Factory | Web UI + CLI,支持 100+ 模型,内置多种数据集 | 快速实验,不想写太多代码 | ⭐ |
| Axolotl | YAML 配置驱动,支持多种微调方法 | 喜欢配置化、可重复的实验 | ⭐⭐ |
| HF PEFT | 官方库,生态最完善,灵活度最高 | 需要完全控制训练流程 | ⭐⭐⭐ |
| Mistral FT | Mistral 官方工具,针对 Mistral 模型优化 | Mistral 模型微调 | ⭐⭐ |
关键超参数
| 参数 | 推荐值 | 说明 |
|---|---|---|
| r(rank) | 8 - 64 | LoRA 秩,越大效果越好但参数越多。简单任务 8-16,复杂任务 32-64 |
| lora_alpha | r 的 2 倍 | 缩放系数,通常设为 rank 的 2 倍(如 r=16, alpha=32) |
| learning_rate | 1e-4 ~ 3e-4 | 学习率。LoRA 微调通常用 2e-4,过大容易不稳定 |
| epochs | 2 - 5 | 训练轮次。数据量大用 2-3 轮,数据量小可用 4-5 轮 |
| batch_size | 4 - 16 | 结合 gradient_accumulation 使用,有效 batch = batch × accum_steps |
| max_seq_length | 512 - 4096 | 最大序列长度,根据数据长度分布设置,过长浪费显存 |
| lora_dropout | 0.05 - 0.1 | LoRA 层的 Dropout,防止过拟合 |
模型评估
微调完成后需要系统评估模型效果,确保质量达标:
自动评估指标
- Perplexity(困惑度):衡量模型对测试文本的预测能力,越低越好
- BLEU / ROUGE:用于翻译、摘要等生成任务,衡量与参考答案的重叠度
- Accuracy / F1:用于分类任务,衡量预测准确率
人工评估维度
- 准确性:回答内容是否正确、有无事实错误
- 格式一致性:输出是否符合期望的格式和风格
- 安全性:是否产生有害内容或泄露敏感信息
- 泛化能力:在未见过的同类输入上表现是否稳定
# 使用 LLM-as-Judge 自动评估
from openai import OpenAI
judge = OpenAI()
def evaluate_response(question, response):
result = judge.chat.completions.create(
model="gpt-4",
messages=[{
"role": "user",
"content": f"请评估以下回答的质量(1-10分):\n\n问题:{question}\n回答:{response}\n\n请从准确性、完整性、流畅性三个维度打分并说明理由。"
}]
)
return result.choices[0].message.content
常见问题与解决方案
⚠️ 过拟合
模型只在训练数据上表现好,新数据表现差。解决:增加数据量、减少 epochs、增大 lora_dropout、使用早停策略。
⚠️ 灾难性遗忘
微调后丢失了预训练时的通用能力。解决:混入少量通用数据、降低学习率、使用较小的 rank。
⚠️ 训练不稳定
Loss 震荡或突然飙升。解决:降低学习率、增加 warmup steps、检查数据中的异常样本。
⚠️ 显存不足(OOM)
训练时 GPU 显存溢出。解决:使用 4-bit 量化(QLoRA)、减小 batch_size、缩短 max_seq_length。
显存需求参考
| 模型大小 | 全量微调 | LoRA (BF16) | QLoRA (4-bit) |
|---|---|---|---|
| 1B - 3B | 24GB+ | 8GB | 4GB |
| 7B - 8B | 8×A100 | 24GB | 8-12GB |
| 13B - 14B | 16×A100 | 40GB | 16GB |
| 70B | 8×A100 80GB | 160GB+ | 48GB |
🚀 实践建议
新手推荐路线:先用 Unsloth + QLoRA 在消费级 GPU(RTX 3090/4090)上微调 7B-8B 模型,快速验证效果。确认方案可行后,再考虑更大模型或更高精度的训练。记住:数据质量 > 模型大小 > 训练技巧。
📖 补充知识
以下章节对正文中的核心概念做深入展开,帮助理解微调背后的数学原理与关键细节。
补充 1:低秩矩阵与 LoRA 的数学原理
什么是「秩」(Rank)?
矩阵的秩是指矩阵中线性无关的行(或列)的最大数目,直觉上可以理解为矩阵所包含的「独立信息维度数」。一个 m×n 的矩阵,秩最大为 min(m, n)。秩越低,矩阵能用越少的独立成分来描述。
低秩分解的核心思想
任何一个秩为 r 的矩阵 W(m×n),都可以分解为两个更小的矩阵的乘积:
# 低秩分解
W ≈ B × A
# W: m × n 矩阵(秩为 r)
# B: m × r 矩阵(列满秩)
# A: r × n 矩阵(行满秩)
# r << min(m, n) ← 关键!秩远小于矩阵维度
# 参数量对比
# 原始矩阵:m × n 个参数
# 分解后: m × r + r × n = r(m + n) 个参数
# 当 r 很小时,参数量大幅减少!
LoRA 如何应用低秩分解
在 Transformer 中,权重更新量 ΔW 被认为是低秩的——虽然权重矩阵本身维度很高(如 4096×4096),但微调时真正需要改变的「有效信息」维度很低。LoRA 利用这一点:
# LoRA 的核心公式
h = Wx + ΔWx = Wx + BAx
# W: 原始权重矩阵(冻结,不训练)
# ΔW = BA: 低秩分解的增量矩阵
# B: d × r (r 通常 8~64)
# A: r × k
# x: 输入向量
# 参数量对比(以 d=k=4096 为例)
# 全量微调:4096 × 4096 = 16,777,216 参数
# LoRA r=8:4096×8 + 8×4096 = 65,536 参数(仅为原来的 0.39%!)
# LoRA r=16:4096×16 + 16×4096 = 131,072 参数(0.78%)
秩 r 的选择
| r 值 | 可训练参数 | 适用场景 | 说明 |
|---|---|---|---|
| r = 4~8 | 极少 | 简单任务(格式调整、风格迁移) | 参数效率高,但表达能力有限 |
| r = 16~32 | 适中 | 大多数微调任务(推荐默认值) | 平衡效果与效率 |
| r = 64~128 | 较多 | 复杂任务(知识注入、跨领域适配) | 接近全量微调效果 |
| r > 128 | 很多 | 特殊需求 | 优势逐渐减弱,不如直接用全量微调 |
💡 直觉理解
想象一个 1000×1000 的像素图片(100 万参数),但如果它只是一个渐变背景,实际上只需要几个颜色值和方向就能描述——这就是「低秩」的含义。LoRA 假设微调时模型权重的变化也是这种「简单变化」,所以用很少的参数就能捕捉到。
补充 2:量化(Quantization)原理
为什么需要量化?
大模型的参数占用了大量显存。一个 7B 参数的模型,用 FP32 存储需要约 28GB 显存(7B × 4 bytes),而大多数消费级 GPU 只有 8-24GB。量化通过降低每个参数的存储精度来大幅减少显存占用。
不同精度的数值表示
| 精度 | 位数 | 数值范围 | 7B 模型显存 | 质量损失 |
|---|---|---|---|---|
| FP32 | 32 bit | ±3.4×10³⁸ | ~28 GB | 无(基准) |
| FP16 | 16 bit | ±6.5×10⁴ | ~14 GB | 极小 |
| BF16 | 16 bit | ±3.4×10³⁸ | ~14 GB | 极小(范围同 FP32) |
| INT8 | 8 bit | -128 ~ 127 | ~7 GB | 较小 |
| INT4 | 4 bit | -8 ~ 7 | ~3.5 GB | 有一定损失 |
主流量化方案
- GPTQ:训练后量化(PTQ),需要少量校准数据,INT4 推理速度快,适合 GPU
- AWQ:激活感知量化,保护重要权重通道,质量优于 GPTQ
- bitsandbytes(BNB):QLoRA 使用的量化库,支持 4/8 bit,与 LoRA 无缝配合
- GGUF(llama.cpp):CPU 友好的量化格式,适合本地/边缘部署
💡 量化选择建议
QLoRA 微调选 bitsandbytes 4-bit(与 LoRA 原生集成);生产推理部署选 AWQ/GPTQ(质量更好);本地 CPU 运行选 GGUF。量化到 4-bit 通常只损失 1-3% 的性能,但显存节省 75%。
补充 3:灾难性遗忘(Catastrophic Forgetting)
什么是灾难性遗忘?
微调时模型在新数据上训练,可能会「忘记」原有的通用能力。例如:微调医疗问答后,模型可能不再擅长写代码或回答常识问题。这是因为新任务的梯度更新覆盖了之前学到的知识。
遗忘发生在哪里?
- 注意力层(Attention):存储通用语言理解能力,相对不容易遗忘
- 前馈网络层(FFN/MLP):存储具体知识事实,最容易遗忘
- Embedding 层:词义理解,微调时通常冻结可保护
缓解策略
| 策略 | 原理 | 效果 |
|---|---|---|
| 冻结部分层 | 只训练少量层(如 LoRA),保留其余层的知识 | ⭐⭐⭐⭐ |
| 数据混合 | 混入 10-20% 通用数据(如对话、指令数据) | ⭐⭐⭐⭐⭐ |
| 低学习率 | 小步更新,减少对已有知识的破坏 | ⭐⭐⭐ |
| 弹性权重合并(EWC) | 限制「重要参数」的变化幅度 | ⭐⭐⭐⭐ |
| 模型合并(Model Merging) | 微调后与原始模型加权平均(如 DARE、TIES) | ⭐⭐⭐⭐ |
如何检测遗忘?
# 在微调前后分别评估通用 benchmark
benchmarks = {
"MMLU": "综合知识(57 个学科)",
"HumanEval": "代码生成",
"HellaSwag": "常识推理",
"ARC": "科学问答",
"TruthfulQA": "事实准确性",
}
# 如果微调后通用 benchmark 下降 > 5%,说明存在明显遗忘
# 解决方案:混入通用数据或降低学习率
补充 4:学习率预热(Warmup)与调度策略
为什么需要 Warmup?
训练初期,模型参数是随机初始化的(或从预训练权重开始),此时梯度方向不稳定。如果直接用大学习率,参数会被「推」到错误的方向,导致训练不稳定甚至发散。Warmup 让学习率从 0 逐步增大到目标值,给模型一个「热身」阶段。
常见调度策略
- Linear Warmup + Cosine Decay:最常用,Transformer 标配。学习率先线性升高,再按余弦曲线下降
- Linear Warmup + Linear Decay:简单有效,学习率线性升再线性降
- Constant(无调度):学习率固定,简单任务可用
- Warmup + Step Decay:每隔固定步数降低学习率
Warmup 步数设置
# 经验法则:warmup 步数 = 总步数的 3%~10%
# 示例:10000 步训练
warmup_steps = 500 # 5% warmup
# 学习率变化过程
# Step 0: lr = 0
# Step 100: lr = 0.4e-4 (20% warmup)
# Step 250: lr = 1.0e-4 (50% warmup)
# Step 500: lr = 2.0e-4 (100% warmup,到达目标值)
# Step 2500: lr = ~1.8e-4 (cosine decay 开始)
# Step 5000: lr = ~1.0e-4 (中间值)
# Step 10000: lr = 0 (衰减到 0)
💡 实用建议
微调时推荐 Linear Warmup + Cosine Decay,warmup 比例 5%。如果训练 loss 初期震荡严重,可以增大 warmup 比例到 10%。Unsloth 默认已配置好调度策略,通常不需要手动调整。
补充 5:混合精度训练(Mixed Precision)
什么是混合精度?
混合精度训练是指在训练过程中同时使用多种数值精度:前向传播和反向传播用 FP16/BF16(快、省显存),参数更新用 FP32(精确)。这样既享受低精度带来的速度优势,又保持训练的数值稳定性。
FP16 vs BF16 的关键区别
| 特性 | FP16 | BF16 |
|---|---|---|
| 指数位 | 5 bit(范围小) | 8 bit(范围大,同 FP32) |
| 尾数位 | 10 bit(精度高) | 7 bit(精度略低) |
| 数值范围 | ±6.5×10⁴ | ±3.4×10³⁸ |
| 溢出风险 | 较高(需要 Loss Scaling) | 极低(无需 Loss Scaling) |
| 硬件支持 | Volta+ (2017) | Ampere+ (2020) |
| 推荐度 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐(首选) |
Loss Scaling(FP16 专用)
FP16 范围小,梯度值可能低于最小可表示值变成 0(下溢)。Loss Scaling 将 loss 乘以一个大数(如 1024),梯度相应放大,更新前再除回来:
# FP16 训练流程
loss = model(inputs) # FP16 前向
scaled_loss = loss * 1024 # 放大 loss
scaled_loss.backward() # 梯度也被放大 1024 倍
optimizer.step() # 内部自动除以 1024 恢复
# BF16 不需要这步,因为数值范围足够大,梯度不会下溢
💡 实践建议
优先使用 BF16(A100/RTX 3090+ 支持)。如果 GPU 不支持 BF16,用 FP16 + Loss Scaling。QLoRA 默认使用 BF16,无需额外配置。
补充 6:评估指标深入理解
Perplexity(困惑度)
衡量模型对文本的「预测困难程度」。值越低,模型对文本越「不意外」。
# Perplexity = exp(交叉熵损失)
# PPL = 1:完美预测(不可能达到)
# PPL = 10:模型比较确定,每步约 10 个候选词
# PPL = 100:模型很不确定,每步约 100 个候选词
# PPL > 500:模型基本无法预测
# 局限:只衡量语言建模能力,不反映指令遵循或事实准确性
文本生成评估指标
| 指标 | 衡量什么 | 适用场景 | 局限 |
|---|---|---|---|
| BLEU | n-gram 精确率(生成与参考的重叠) | 翻译任务 | 不关心语义,只看词面匹配 |
| ROUGE | 召回率(参考文本被覆盖的程度) | 摘要任务 | 同样不关心语义 |
| BERTScore | 基于 BERT 嵌入的语义相似度 | 通用文本生成 | 计算成本高 |
| LLM-as-Judge | 用 GPT-4 等强模型打分评估 | 对话质量、指令遵循 | 成本高、可能有偏见 |
人工评估 vs 自动评估
- 自动评估:快速、可复现、成本低。但指标与人类判断的相关性有限
- 人工评估:最接近真实质量,但昂贵、慢、主观性强
- 最佳实践:自动指标做快速筛选 + 人工评估做最终验证。至少准备 100-200 条人工评估样本
微调评估 Checklist
# 微调后应该评估的维度
✅ 任务准确率:在目标任务上的表现(核心指标)
✅ 通用能力保持:MMLU / HellaSwag 是否下降
✅ 格式遵循:输出是否符合要求的格式
✅ 安全性:是否产生有害/不当内容
✅ 一致性:多次采样结果是否稳定
✅ 延迟/吞吐:推理速度是否满足要求