LLM Fine-Tuning
深入解析 LLM 微调技术,涵盖 SFT 监督微调和 LoRA 低秩适配两大核心方法,从数学原理到实践代码全面讲解。
LLM Fine-Tuning 全面指南:SFT 与 LoRA 详解
引言
大语言模型(Large Language Model,LLM)的崛起标志着人工智能领域的一个转折点。以 GPT、LLaMA、Claude 为代表的预训练语言模型,通过在海量文本数据上进行自监督学习,获得了惊人的语言理解和生成能力。然而,预训练模型如同未经雕琢的璞玉,虽然掌握了语言的统计规律,却缺乏明确的任务导向和人类偏好对齐能力。
**微调(Fine-Tuning)**正是将预训练模型转化为实际应用的关键技术。微调是指在预训练模型的基础上,使用特定领域或任务的数据进行进一步训练,使模型获得执行特定任务的能力。根据是否更新全部参数,微调可分为全参数微调(Full Parameter Fine-Tuning)和参数高效微调(Parameter-Efficient Fine-Tuning,PEFT)两大类。
本文将深入探讨两种最重要的微调技术:SFT(监督微调)和LoRA(低秩适配),从理论原理到实践细节进行全面解析。
第一部分:SFT(监督微调)
1.1 什么是 SFT
**SFT(Supervised Fine-Tuning,监督微调)**是一种传统的模型微调方法,通过在标注数据上进行监督学习,使预训练模型适应特定任务。在 SFT 阶段,模型学习的是输入与输出之间的映射关系,这与预训练阶段的语言建模任务有本质区别。
预训练阶段,模型学习的是”给定前文,预测下一个token”,这是一种自监督学习范式,不需要人工标注。而 SFT 阶段则需要人类标注的问答对或多轮对话数据,模型学习的是”给定指令和问题,生成符合人类期望的回答”。
SFT 的核心目标是:
- 任务适配:让模型学会执行特定任务(如问答、摘要、翻译)
- 格式对齐:让模型学习符合人类期望的输出格式
- 能力激发:激活预训练模型中已经存在但未被充分激发的能力
1.2 SFT 与预训练的关系
理解 SFT 与预训练的关系对于把握微调的本质至关重要。
graph TD
subgraph Stage["模型训练阶段对比"]
P["预训练阶段<br/>语料:互联网大规模文本(数十亿到万亿token)<br/>任务:Next Token Prediction<br/>目标:学习通用语言知识和世界知识<br/>特点:自监督学习,不需要人工标注"]
S["SFT 阶段<br/>语料:人类标注的指令-响应对(数千到数万条)<br/>任务:给定指令,生成期望回答<br/>目标:任务适配、格式对齐、能力激发<br/>特点:监督学习,需要高质量人工标注"]
R["RLHF 阶段(可选)<br/>语料:人类偏好排序数据<br/>任务:学习人类偏好,优化生成质量<br/>目标:与人类价值观对齐 (Helpful, Honest, Harmless)<br/>特点:强化学习,结合奖励模型"]
end
P --> S --> R
关键认知:
- 预训练模型已经包含了大量知识,SFT 的目的不是从头教授知识,而是激活和对齐
- SFT 数据量虽小,但质量要求极高
- SFT 可以看作是一种”教会模型遵循指令”的过程
1.3 SFT 数据格式
SFT 训练数据主要有两种格式:指令格式和对话格式。
1.3.1 指令格式(Instruction Format)
指令格式是最常用的 SFT 数据格式,包含三个核心组成部分:
┌─────────────────────────────────────────────────────────────┐
│ 指令格式数据示例 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 输入(Instruction):请将以下英文翻译成中文 │
│ │
│ 输入(Input):The quick brown fox jumps over the lazy dog │
│ │
│ 输出(Output):敏捷的棕色狐狸跳过了懒惰的狗 │
│ │
└─────────────────────────────────────────────────────────────┘
完整的训练样本构建方式:
{
"instruction": "请将以下英文翻译成中文",
"input": "The quick brown fox jumps over the lazy dog",
"output": "敏捷的棕色狐狸跳过了懒惰的狗"
}
在训练时,模型看到的序列是:
[INST] 请将以下英文翻译成中文
The quick brown fox jumps over the lazy dog
[/INST] 敏捷的棕色狐狸跳过了懒惰的狗
1.3.2 对话格式(Chat Format)
对话格式用于多轮对话场景,数据结构为消息列表:
{
"messages": [
{"role": "user", "content": "什么是大语言模型?"},
{"role": "assistant", "content": "大语言模型是一类使用深度学习技术..."},
{"role": "user", "content": "它和传统NLP模型有什么区别?"},
{"role": "assistant", "content": "传统NLP模型通常是任务特定的..."}
]
}
常见的对话格式包括:
| 格式 | 特殊标记 | 应用场景 |
|---|---|---|
| ChatML | <|im_start|> / <|im_end|> | Qwen、Yi |
| Llama 3 | [INST] / [/INST] | Llama 3 |
| Claude | \n\nHuman: / \n\nAssistant: | Claude |
| GPT-4 | system / user / assistant | OpenAI API |
# ChatML 格式示例
messages = [
{"role": "system", "content": "你是一个有帮助的AI助手"},
{"role": "user", "content": "你好,请介绍一下自己"},
{"role": "assistant", "content": "你好!我是..."}
]
# 转换为训练文本
text = ""
for msg in messages:
text += f"<|im_start|>{msg['role']}\n{msg['content']}<|im_end|>\n"
1.4 SFT 训练过程与损失计算
1.4.1 训练流程
graph TD
A["预训练模型"]
B["添加特殊标记"]
C["计算 Loss / 反向传播 / 更新权重"]
D["微调模型"]
A -- "加载预训练权重作为初始化" --> B
B -- "添加对话格式需要的特殊token" --> C
C -- "仅在 output 位置计算交叉熵损失" --> D
1.4.2 损失计算
SFT 采用标准的语言建模损失函数。给定训练样本 $(x_1, x_2, …, x_n)$,模型学习预测每个 token $x_t$($t > 1$),损失为负对数似然:
$$ \mathcal{L}{SFT} = -\sum{t=2}^{n} \log P_{\theta}(x_t | x_1, x_2, …, x_{t-1}) $$
关键点:在 SFT 训练中,我们仅在 output 区域计算损失,instruction 和 input 部分不参与损失计算。这是为了让模型学习的是”根据指令和问题生成回答”,而不是”根据回答生成回答”。
# SFT 损失计算示意
def compute_sft_loss(model, input_ids, attention_mask, labels):
"""
Args:
input_ids: 完整的输入序列token IDs
labels: 与input_ids相同的序列,但在instruction位置被设置为-100(忽略)
"""
outputs = model(
input_ids=input_ids,
attention_mask=attention_mask
)
# 仅计算 output 位置的损失(labels中非-100的位置)
loss = outputs.loss
return loss
# labels 构建示例
# input: [INST] 指令 [INST] 回答开始...
# label: [-100, -100, ..., -100, 输出token_ids, -100...]
# instruction 部分 output 部分
# ↓ ↓
# labels = [-100, -100, ..., 1234, 5678, 9012, ...]
1.5 关键超参数
SFT 训练中有几个至关重要的超参数,需要根据数据规模和任务特点进行调优。
1.5.1 学习率(Learning Rate)
| 模型规模 | 推荐学习率 | 说明 |
|---|---|---|
| 7B 模型 | 1e-5 ~ 2e-5 | 较小模型需要相对较高的学习率 |
| 13B 模型 | 5e-6 ~ 1e-5 | 中等规模模型 |
| 70B 模型 | 1e-6 ~ 5e-6 | 大模型需要较小学习率 |
| 100B+ 模型 | 5e-7 ~ 2e-6 | 超大模型 |
学习率调度策略:
┌─────────────────────────────────────────────────────────────────┐
│ 学习率调度曲线 │
│ │
│ lr │
│ │ │
│ │ ┌──────────────┐ │
│ │ │ │ │
│ │ │ 峰值学习率 │ │
│ │ │ (peak lr) │ │
│ │ │ │ │
│ │─────────┘ └──────────── │
│ │ ────── │
│ │ ── │
│ │ ── │
│ │ ── │
│ │ ── │
│ └──────────────────────────────────────────────────────────→ │
│ 训练步数/迭代次数 │
│ │
│ warmup plateau (cosine decay) │
│ │
└─────────────────────────────────────────────────────────────────┘
典型的学习率调度:
- Warmup:前 1-3% 的步数从很小的学习率逐渐增加到峰值
- Cosine Decay:之后余弦退火到最小学习率
- Linear Decay:线性下降到最小学习率
1.5.2 训练轮数(Epochs)
| 数据规模 | 推荐 Epochs | 说明 |
|---|---|---|
| < 1万条 | 3-10 | 小数据集需要更多轮数以充分学习 |
| 1-10万条 | 2-5 | 中等规模数据 |
| > 10万条 | 1-3 | 大规模数据,避免过拟合 |
警惕过拟合:SFT 阶段模型收敛很快,当验证损失开始上升时,应立即停止训练。
1.5.3 数据量与模型能力
┌─────────────────────────────────────────────────────────────────┐
│ 数据量与模型能力关系示意图 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 能力水平 │
│ │ │
│ │ ╭─── 最终能力上限 │
│ │ ╭───╯ │
│ │ ╭───╯ │
│ │ ╭───╯ │
│ │ ╭───╯ │
│ │ ╭───╯ ←── 小数据集也能达到较高能力 │
│ │ ╭───╯ 但可能欠拟合某些模式 │
│ │──╯ │
│ └────────────────────────────────────────────────────────→ │
│ 数据量 │
│ │
│ 低质量大数据 ←————————→ 高质量小数据 │
│ │
└─────────────────────────────────────────────────────────────────┘
核心洞见:对于 SFT 而言,数据质量比数据数量更重要。1000条高质量标注数据往往比10000条低质量数据更有价值。
1.6 数据质量的重要性
SFT 的成功在很大程度上取决于训练数据的质量。低质量数据不仅无法提升模型性能,反而可能损害模型能力。
1.6.1 优质 SFT 数据的特征
| 特征 | 描述 | 重要性 |
|---|---|---|
| 准确性 | 回答内容正确无误 | ★★★★★ |
| 相关性 | 回答与指令高度相关 | ★★★★★ |
| 完整性 | 回答覆盖问题的所有方面 | ★★★★☆ |
| 清晰性 | 表达清晰,逻辑连贯 | ★★★★☆ |
| 多样性 | 覆盖不同任务类型和表达方式 | ★★★☆☆ |
| 格式一致性 | 遵循统一的输出格式规范 | ★★★☆☆ |
1.6.2 常见数据质量问题
| 问题类型 | 负面影响 | 解决方案 |
|---|---|---|
| 错误答案 | 模型学到错误知识 | 多轮质量审核 |
| 指令不匹配 | 格式学习混乱 | 重新标注或过滤 |
| 回答过于简短 | 能力激发不足 | 要求更详细的回答 |
| 风格不一致 | 输出不稳定 | 制定标注规范手册 |
| 领域偏差过大 | 预训练知识遗忘 | 混合通用数据 |
1.6.3 灾难性遗忘(Catastrophic Forgetting)
灾难性遗忘是 SFT 阶段面临的一个重要问题,指的是模型在学习新任务时,遗忘预训练阶段获得的知识和能力。
┌─────────────────────────────────────────────────────────────────┐
│ 灾难性遗忘示意图 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 能力水平 │
│ │ │
│ │ ╭───────────── 原始预训练能力 │
│ │ ╱ ╲ │
│ │───╱ ╲─── │
│ │ ╱ ← 遗忘预训练知识 ╲ │
│ │ ╱ ╲ │
│ │╱ ╲ │
│ └──────────────────────────────────────────────────────────→ │
│ 训练进程 │
│ │
│ [预训练知识] [SFT学习] [能力状态] │
│ ↓ ↓ ↓ │
│ 保留完整 学习新任务 部分知识丢失 │
│ │
└─────────────────────────────────────────────────────────────────┘
应对策略:
- 混合预训练数据:在 SFT 数据中混入一定比例(通常 5-10%)的预训练语料
- 降低学习率:使用较预训练更小的学习率
- 早停法:监控验证集损失,及时停止
- 正则化:对权重更新施加约束
# 混合预训练数据的训练示例
def create_sft_data_loader(sft_data, pretrain_data, mix_ratio=0.1):
"""
Args:
sft_data: SFT 训练数据
pretrain_data: 预训练语料数据
mix_ratio: 预训练数据混合比例
"""
# 每 N 条 SFT 数据中混入 1 条预训练数据
combined = []
for i, sft_sample in enumerate(sft_data):
combined.append(sft_sample)
if (i + 1) % int(1 / mix_ratio) == 0 and pretrain_data:
combined.append(pretrain_data[len(combined) % len(pretrain_data)])
return combined
1.7 SFT 与 RLHF 的关系
SFT 和 RLHF 是 LLM 对齐训练的两个关键阶段,它们既有联系又有区别。
graph TD
P["预训练模型"]
S["SFT<br/>学习格式和基本任务完成能力"]
R["RM<br/>训练奖励模型学习人类偏好"]
PP["PPO<br/>强化学习优化策略,最大化人类偏好"]
A["对齐模型"]
P --> S --> R --> PP --> A
SFT 的局限性:
- 依赖标注质量:SFT 的能力上限受限于标注者的水平和一致性
- 缺乏偏好学习:无法学习到”哪个回答更好”,只能学习”什么是正确的”
- 奖励信号单一:只有关键token的奖励,无法捕捉整体质量差异
RLHF 的优势:
- 偏好学习:通过对比学习理解人类偏好
- 连续奖励:为每个回答提供细粒度的质量信号
- 避免机械性:生成的回答更自然、更符合人类表达习惯
实践中的权衡:
| 方面 | SFT | RLHF |
|---|---|---|
| 计算成本 | 较低 | 高(需要训练RM和PPO) |
| 数据需求 | 需要高质量标注 | 需要偏好排序数据 |
| 训练稳定性 | 稳定 | 复杂(需要KL约束) |
| 最终效果 | 良好 | 更好(但收益递减) |
| 适用场景 | 资源受限、任务明确 | 追求最优对齐效果 |
第二部分:LoRA(低秩适配)
2.1 LoRA 背景与动机
2.1.1 全参数微调的困境
随着 LLM 规模的爆发式增长(从7B到70B甚至1000B参数),全参数微调面临前所未有的挑战:
┌─────────────────────────────────────────────────────────────────┐
│ 全参数微调的三大困境 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 显存困境(VRAM Bottleneck) │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 7B 模型:fp16 精度下全参数训练需要 ~14GB VRAM │ │
│ │ 13B 模型:fp16 精度下全参数训练需要 ~26GB VRAM │ │
│ │ 70B 模型:fp16 精度下全参数训练需要 ~140GB VRAM │ │
│ │ 100B+ 模型:需要分布式多卡训练,硬件要求极高 │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ 2. 存储困境(Storage Bottleneck) │
│ 每个任务需要保存一份完整模型权重 │
│ N 个任务 = N × 模型大小的存储空间 │
│ │
│ 3. 效率困境(Efficiency Bottleneck) │
│ 每个下游任务都需要重新训练全部参数 │
│ 训练时间长,能耗巨大 │
│ │
└─────────────────────────────────────────────────────────────────┘
2.1.2 参数高效微调的兴起
为了解决上述困境,**参数高效微调(Parameter-Efficient Fine-Tuning,PEFT)**技术应运而生。PEFT 的核心思想是:冻结预训练模型的全部参数,仅添加少量可训练参数,通过训练这些新增参数来实现任务适配。
主流 PEFT 方法对比:
| 方法 | 添加参数位置 | 可训练参数量 | 性能 |
|---|---|---|---|
| LoRA | Linear 层 | 低(万级) | 接近全参数 |
| Adapter | FFN 层 | 中(百万级) | 良好 |
| Prefix Tuning | Token 前缀 | 低(千级) | 良好 |
| Prompt Tuning | Embedding | 极低(百级) | 一般 |
| IA³ | Attention | 极低 | 良好 |
2.2 LoRA 数学原理
2.2.1 核心思想:低秩分解
LoRA 的数学基础是低秩分解(Low-Rank Decomposition)。其核心假设是:模型微调过程中权重的更新矩阵 $\Delta W$ 具有低秩特性,即可以用两个小矩阵的乘积来近似表示。
┌─────────────────────────────────────────────────────────────────┐
│ LoRA 低秩分解示意图 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 全参数微调: │
│ W ∈ ℝ^{d×k} ΔW = W' - W │
│ │
│ 训练参数量 = d × k │
│ │
│ LoRA 微调: │
│ ┌───────────┐ │
│ W ∈ ℝ^{d×k} ───│→ 冻结 ←──│─── + ΔW │
│ └───────────┘ ↓ │
│ ΔW = BA │
│ B ∈ ℝ^{d×r} │
│ A ∈ ℝ^{r×k} │
│ │
│ 训练参数量 = d×r + r×k = r(d+k) │
│ 当 r << min(d, k) 时,参数量大大减少 │
│ │
└─────────────────────────────────────────────────────────────────┘
2.2.2 数学推导
对于预训练权重矩阵 $W_0 \in \mathbb{R}^{d \times k}$,LoRA 使用低秩分解来近似更新:
$$ W’ = W_0 + \Delta W = W_0 + BA $$
其中:
- $B \in \mathbb{R}^{d \times r}$:降维矩阵
- $A \in \mathbb{R}^{r \times k}$:升维矩阵
- $r \ll \min(d, k)$:秩(Rank),控制低秩近似的维度
前向传播时: $$ h = W’x = (W_0 + BA)x = W_0x + BAx $$
梯度更新时,仅更新 $A$ 和 $B$: $$ \frac{\partial \mathcal{L}}{\partial A} = \frac{\partial \mathcal{L}}{\partial (BA)} \cdot B^T $$ $$ \frac{\partial \mathcal{L}}{\partial B} = \frac{\partial \mathcal{L}}{\partial (BA)} \cdot A^T $$
2.2.3 秩的选择与影响
┌─────────────────────────────────────────────────────────────────┐
│ 秩 r 对模型性能的影响 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 性能 │
│ │ │
│ │ ╭─────────────── 高秩 │
│ │ ╭─╯ │
│ │ ╭─╯ ←── 中等秩(推荐起点) │
│ │ ╭─╯ │
│ │╭─╯ ←── 低秩(可能欠拟合) │
│ │ │
│ └──────────────────────────────────────────────→ │
│ 秩 r │
│ 小 ←————————→ 大 │
│ r=2,4,8 r=32,64 │
│ │
│ 推荐选择策略: │
│ - 通用任务:r = 4~8 │
│ - 复杂任务:r = 16~32 │
│ - 追求效果:r = 64~128(接近全参数效果) │
│ │
└─────────────────────────────────────────────────────────────────┘
2.3 LoRA 超参数详解
2.3.1 核心超参数
| 超参数 | 说明 | 推荐值 | 影响 |
|---|---|---|---|
| rank (r) | 低秩维度 | 4~64 | 越大越接近全参数,但增加参数量 |
| alpha (α) | 缩放因子 | 1~2×r | 控制 LoRA 层的影响强度 |
| dropout | LoRA 参数 dropout | 0~0.1 | 防止过拟合 |
| target_modules | 应用 LoRA 的层 | Q, K, V, O | 影响微调效果 |
2.3.2 Alpha 缩放
LoRA 的输出会乘以一个缩放因子 $\frac{\alpha}{r}$:
$$ h = W_0x + \frac{\alpha}{r} BAx $$
这个设计的意义在于:当 $r$ 变化时,可以通过调整 $\alpha$ 来保持 LoRA 层的影响大致一致。通常设置 $\alpha = 2r$ 或 $\alpha = r$。
# LoRA 缩放示意
output = base_output + (lora_weight @ lora_input) * (alpha / rank)
2.4 LoRA 应用位置
2.4.1 Transformer 中的 LoRA 应用
在 Transformer 架构中,LoRA 通常应用在 Attention 层的 $Q$、$K$、$V$、$O$ 四个投影矩阵上:
graph TD
X["输入 x"]
subgraph MHA["Multi-Head Attention"]
QKV["Q = W_q x ← LoRA 位置 1<br/>K = W_k x ← LoRA 位置 2<br/>V = W_v x ← LoRA 位置 3"]
AT["Attention Score = QK^T"]
O["O = W_o h ← LoRA 位置 4"]
end
H["输出 h"]
X --> QKV --> AT --> O --> H
2.4.2 各位置效果对比
| 应用位置 | 参数量 | 效果 | 说明 |
|---|---|---|---|
| 仅 Q | 最低 | ★★★☆☆ | 效果有限 |
| Q + K | 低 | ★★★★☆ | 平衡选择 |
| Q + V | 中 | ★★★★☆ | 常用配置 |
| Q + K + V | 中高 | ★★★★★ | 最佳效果 |
| Q + K + V + O | 最高 | ★★★★★ | 接近全参数 |
推荐配置:对于大多数场景,使用 Q + V 或 Q + K + V 即可获得良好效果。
2.5 LoRA 变体
2.5.1 QLoRA(量化 LoRA)
QLoRA 由 Tim Dettmers 等人提出,是一种结合量化技术的高效微调方法,可以在极低显存下微调大模型。
核心创新:
- 4-bit NormalFloat (NF4) 量化:一种针对正态分布权重优化的4位量化
- 双重量化:对量化常数本身也进行量化
- 分页优化器:处理内存峰值
┌─────────────────────────────────────────────────────────────────┐
│ QLoRA vs LoRA 显存对比 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 70B 模型全参数微调: │
│ FP16: ~140GB VRAM(不可行) │
│ │
│ LoRA: │
│ FP16: ~80GB VRAM(需要8×A100 80GB) │
│ │
│ QLoRA: │
│ NF4 + 双量化: ~40GB VRAM(4×A100 80GB 可行) │
│ INT4 + 分页优化器: ~24GB VRAM(2×A100 80GB 可行) │
│ │
└─────────────────────────────────────────────────────────────────┘
QLoRA 的量化流程:
graph TD
A["1. 模型加载 (4-bit NF4)<br/>FP16 权重 → 4-bit NF4 量化 → 存储在内存中"]
B["2. 训练时反量化<br/>4-bit NF4 → FP16 → 计算"]
C["3. LoRA 更新<br/>仅在 LoRA 参数上计算梯度,更新"]
A --> B --> C
note["关键洞察:量化损失由 LoRA 低秩适配来弥补"]
2.5.2 DoRA(权重分解微调)
DoRA(Weight-Decomposed Fine-Tuning) 将预训练权重分解为幅度(magnitude)和方向(direction)两部分:
$$ W = m \cdot \frac{W}{|W|} = m \cdot \hat{W} $$
其中 $m$ 是可学习的幅度标量,$\hat{W}$ 是方向矩阵。对两者分别应用 LoRA:
- 方向更新:$W’ = W + \Delta W = W + BA$
- 幅度更新:$m’ = m + \Delta m$
# DoRA 示意
def dora_forward(x, W, m, lora_A, lora_B, alpha, rank):
# 方向:标准 LoRA 更新
direction = W + (lora_B @ lora_A) * (alpha / rank)
# 幅度:可学习标量
magnitude = m
# 归一化方向
normalized = direction / (direction.norm(dim=-1, keepdim=True) + 1e-8)
# 结合幅度
output = magnitude * normalized @ x.T
return output.T
DoRA 的优势在于更好地利用了预训练权重的结构信息,在某些任务上取得了比 LoRA 更好的效果。
2.5.3 AdaLoRA(自适应 LoRA)
AdaLoRA 根据各层的重要性动态分配不同的秩,重要程度高的层分配更大的秩。
┌─────────────────────────────────────────────────────────────────┐
│ AdaLoRA 动态秩分配 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Layer 1: r=4 ████ │
│ Layer 2: r=64 ████████████████████████████████ │
│ Layer 3: r=32 ████████████████ │
│ Layer 4: r=8 ████ │
│ ... │
│ │
│ 重要性高的层(如 Layer 2)获得更大的秩 │
│ 重要性低的层(如 Layer 1, 4)获得更小的秩 │
│ │
└─────────────────────────────────────────────────────────────────┘
AdaLoRA 通过奇异值分解(SVD)来评估各层的重要性,并迭代调整秩的分配。
2.5.4 LoRA+ 与 VeRA
| 变体 | 核心改进 | 特点 |
|---|---|---|
| LoRA+ | A/B 使用不同学习率 | B 使用更大的学习率 |
| VeRA | 随机投影共享 | 进一步减少参数量 |
| LoRA-FA | Freezing A | 仅训练 B,节省一半参数 |
| LoftQ | 量化感知训练 | 量化与 LoRA 联合优化 |
2.6 LoRA 训练与部署
2.6.1 训练配置示例
# 使用 transformers 和 peft 库配置 LoRA
from peft import LoraConfig, get_peft_model, TaskType
# LoRA 配置
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM, # 任务类型
r=16, # 秩
lora_alpha=32, # 缩放因子
lora_dropout=0.05, # Dropout
target_modules=[ # 应用 LoRA 的模块
"q_proj",
"k_proj",
"v_proj",
"o_proj"
],
bias="none", # 不训练 bias
inference_mode=False, # 训练模式
)
# 将 LoRA 应用到模型
model = get_peft_model(base_model, lora_config)
# 查看可训练参数
model.print_trainable_parameters()
# 输出: trainable params: 4,194,304 || all params: 6,738,415,616 || trainable%: 0.0622
2.6.2 训练流程
graph TD
A["1. 加载预训练模型(冻结)<br/>AutoModelForCausalLM.from_pretrained(...)<br/>param.requires_grad = False"]
B["2. 应用 LoRA 适配器<br/>get_peft_model(base_model, lora_config)"]
C["3. 标准训练循环(仅更新 LoRA 参数)<br/>outputs = model(**batch) → loss.backward() → optimizer.step()"]
D["4. 保存 LoRA 权重<br/>model.save_pretrained('lora_weights')"]
A --> B --> C --> D
2.6.3 推理部署
# 方式1:合并权重后推理(适合一次性部署)
from peft import PeftModel
# 加载基础模型
base_model = AutoModelForCausalLM.from_pretrained("base_model_path")
# 加载 LoRA 权重并合并
model = PeftModel.from_pretrained(base_model, "lora_weights")
model = model.merge_and_unload()
# 推理
output = model.generate(**inputs)
# 方式2:动态加载 LoRA(适合多任务切换)
from peft import PeftModel
base_model = AutoModelForCausalLM.from_pretrained("base_model_path")
# 任务A的 LoRA
model_a = PeftModel.from_pretrained(base_model, "lora_task_a")
output_a = model_a.generate(**inputs)
# 切换到任务B的 LoRA
model_b = PeftModel.from_pretrained(base_model, "lora_task_b")
output_b = model_b.generate(**inputs)
2.7 LoRA 代码实战
2.7.1 完整训练示例
import torch
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
TrainingArguments,
Trainer
)
from peft import LoraConfig, get_peft_model, TaskType
# 1. 加载模型和 tokenizer
model_name = "meta-llama/Llama-2-7b-hf"
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
# 2. 配置 LoRA
lora_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "v_proj", "k_proj", "o_proj"],
lora_dropout=0.05,
bias="none",
task_type=TaskType.CAUSAL_LM
)
# 3. 应用 LoRA
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# 4. 准备数据集
def format_example(example):
return f"### 指令:\n{example['instruction']}\n\n### 回答:\n{example['output']}"
# 5. 训练参数
training_args = TrainingArguments(
output_dir="./lora_llama2",
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=2e-4,
warmup_ratio=0.03,
lr_scheduler_type="cosine",
logging_steps=10,
save_steps=100,
fp16=True,
optim="adamw_torch"
)
# 6. Trainer
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
data_collator=data_collator,
)
trainer.train()
# 7. 保存
model.save_pretrained("./lora_weights")
2.7.2 合并与推理
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
# 加载基础模型
base_model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b-hf",
torch_dtype=torch.float16,
device_map="auto"
)
# 方式1:直接使用 LoRA 推理(不合并)
peft_model = PeftModel.from_pretrained(base_model, "./lora_weights")
def chat_with_model(prompt):
inputs = tokenizer(prompt, return_tensors="pt").to("cuda")
outputs = peft_model.generate(**inputs, max_new_tokens=256)
return tokenizer.decode(outputs[0], skip_special_tokens=True)
# 方式2:合并后推理
merged_model = peft_model.merge_and_unload()
def chat_with_merged(prompt):
inputs = tokenizer(prompt, return_tensors="pt").to("cuda")
outputs = merged_model.generate(**inputs, max_new_tokens=256)
return tokenizer.decode(outputs[0], skip_special_tokens=True)
2.8 LoRA 最佳实践
2.8.1 超参数选择指南
| 场景 | rank | alpha | target_modules | 说明 |
|---|---|---|---|---|
| 简单任务 | 4-8 | 8-16 | q, v | 如分类、实体识别 |
| 复杂任务 | 16-32 | 32-64 | q, k, v, o | 如问答、对话 |
| 追求效果 | 64+ | 2×rank | q, k, v, o | 接近全参数 |
| 资源受限 | 2-4 | 4-8 | q, v | 最低资源消耗 |
2.8.2 训练技巧
┌─────────────────────────────────────────────────────────────────┐
│ LoRA 训练技巧汇总 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 数据预处理 │
│ - 使用与预训练相同的 tokenizer │
│ - 添加适当的特殊 token(如对话格式) │
│ - 数据量不在多,在于质量 │
│ │
│ 2. 学习率设置 │
│ - LoRA 通常使用 1e-4 ~ 3e-4 │
│ - 高于全参数微调的学习率 │
│ - 使用 warmup + cosine decay │
│ │
│ 3. 防止过拟合 │
│ - 增加 dropout (0.05-0.1) │
│ - 减少 rank │
│ - 减少训练 epochs │
│ │
│ 4. 多任务学习 │
│ - 可以同时训练多个任务的 LoRA │
│ - 或为每个任务单独训练一个 LoRA │
│ - 推理时动态切换 │
│ │
│ 5. 推理优化 │
│ - 合并权重到基础模型(减少延迟) │
│ - 使用量化(INT8/INT4)减少显存 │
│ - 批量推理提高吞吐率 │
│ │
└─────────────────────────────────────────────────────────────────┘
2.8.3 常见问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 效果不佳 | rank 过低 | 增加 r 到 16 或 32 |
| 过拟合 | 数据量小或 rank 过高 | 减小 r,增加 dropout |
| 训练不稳定 | 学习率过高 | 使用 warmup,降低学习率 |
| 推理慢 | 未合并权重 | 合并权重或使用动态加载 |
| 显存不足 | 模型太大 | 使用 QLoRA 或进一步量化 |
第三部分:SFT 与 LoRA 的结合
3.1 SFT + LoRA 的典型范式
在实际应用中,SFT 和 LoRA 通常结合使用,以兼顾效果和效率:
graph TD
P["预训练模型"]
S["阶段1:SFT(全参数或 LoRA)<br/>学会遵循指令、格式对齐"]
R["阶段2:RLHF(可选,通常用 LoRA)<br/>奖励模型 + PPO 优化"]
P --> S
P --> R
note["推荐配置:<br/>SFT: LoRA r=16~32, Q + K + V + O<br/>RLHF: LoRA r=4~8, 仅 Q + V"]
3.2 训练策略对比
| 策略 | 描述 | 适用场景 | 资源需求 |
|---|---|---|---|
| 纯 SFT | 全参数监督微调 | 任务明确、数据充足 | 高 |
| 纯 LoRA | 仅 LoRA 训练 | 资源受限、多任务 | 低 |
| SFT+LoRA | 先 SFT 后 LoRA | 标准流程 | 中 |
| QLoRA | 量化 + LoRA | 超大模型 | 极低 |
总结
SFT 与 LoRA 核心要点
┌─────────────────────────────────────────────────────────────────┐
│ SFT vs LoRA 对比总结 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ SFT(监督微调) │
│ ├─ 原理:全参数监督学习,使用标注数据训练 │
│ ├─ 优势:效果最好,能充分激发模型能力 │
│ ├─ 劣势:资源消耗大,存在遗忘风险 │
│ └─ 关键:数据质量 > 数据数量 │
│ │
│ LoRA(低秩适配) │
│ ├─ 原理:冻结原模型,仅训练低秩分解的增量 │
│ ├─ 优势:参数量小、训练快、可动态切换 │
│ ├─ 劣势:效果略逊于全参数(但差距在减小) │
│ └─ 关键:选择合适的 rank 和 target_modules │
│ │
│ 实践建议: │
│ - 资源充足 → 全参数 SFT + RLHF │
│ - 资源中等 → LoRA SFT + RLHF │
│ - 资源受限 → QLoRA │
│ - 多任务场景 → LoRA + 动态加载 │
│ │
└─────────────────────────────────────────────────────────────────┘
未来展望
LLM 微调技术仍在快速发展,几个值得关注的方向:
- 更高效的微调方法:Beyond LoRA,探索更高效的参数更新机制
- 多模态微调:将微调技术扩展到视觉-语言模型
- 持续学习:避免微调过程中的灾难性遗忘
- 自动化微调:AutoML + PEFT,自动搜索最优微调配置
参考资源
- LoRA 原始论文:Hu et al., “LoRA: Low-Rank Adaptation of Large Language Models” (2021)
- QLoRA 论文:Dettmers et al., “QLoRA: Efficient Finetuning of Quantized LLMs” (2023)
- DoRA 论文:Liu et al., “DoRA: Weight-Decomposed Low-Rank Adaptation” (2024)
- AdaLoRA 论文:Zhang et al., “AdaLoRA: Adaptive Budget Allocation for Parameter-Efficient Fine-Tuning” (2023)
- Hugging Face PEFT:https://github.com/huggingface/peft
- trl 库:https://github.com/huggingface/trl