
目录
- 第一章:微调全景图------建立全局认知
- 第二章:全量微调 (Full Fine-Tuning)
- 第三章:LoRA系列算法
- 第四章:前缀微调系列算法
- 第五章:适配器系列算法 (Adapter)
- 第六章:提示微调系列算法 (Prompt Tuning)
- 第七章:IA³与其他高效微调方法
- 第八章:基于人类反馈的微调方法 (RLHF/DPO/GRPO)
- 第九章:微调算法对比与选型指南
- 第十章:工程实践与最佳实践
- 附录:术语表与参考资料
第一章:微调全景图------建立全局认知
1.1 为什么需要微调?
预训练大模型(如GPT、Qwen、LLaMA)的问题:
- 通用但不专业:什么都懂一点,但在特定领域不够深入
- 格式不可控:输出格式可能不符合业务需求
- 幻觉问题:可能生成不准确的内容
- 成本高:直接使用大模型API成本高,微调小模型可能更经济
微调的价值:
- 领域专业化:让模型成为特定领域的专家
- 格式对齐:让模型按照特定格式输出
- 知识注入:将企业内部知识注入模型
- 降低成本:微调后的小模型可能媲美大模型在特定任务上的表现
1.2 微调方法分类全景图
大模型微调方法
│
┌───────────────┼───────────────┐
│ │ │
全参数微调 参数高效微调(PEFT) 基于反馈的微调
(Full FT) (RLHF/DPO/...)
│ │
│ ┌───────┼───────┬───────────┐
│ │ │ │ │
│ 加法式 重参数化 选择式 混合式
│ │ │ │ │
│ Adapter LoRA Pruning MAM Adapter
│ Prefix DoRA BitFit
│ Prompt QLoRA
│ IA³ AdaLoRA
│ rsLoRA
│ GaLore
│ LoRA+
│ VeRA
│ PiSSA
│ MosLoRA
1.3 核心概念解释
在深入具体算法之前,先搞清楚几个核心概念:
1. 可训练参数比例
全量微调:100%参数都需要训练
LoRA:约0.1%-1%的参数
Adapter:约1%-5%的参数
Prefix Tuning:约0.1%的参数
2. 原始模型参数是否被修改
全量微调:是,所有参数都被更新
LoRA:否,原始参数冻结,只训练旁路
Adapter:否,在层间插入新模块
Prefix Tuning:否,在输入前添加可训练向量
3. 推理时的额外开销
全量微调:无额外开销
LoRA(合并后):无额外开销
LoRA(不合并):有少量计算开销
Adapter:有推理延迟(额外的前向传播)
Prefix Tuning:有少量推理开销(增加序列长度)
4. 显存需求
全量微调:极高(7B模型需要120GB+)
LoRA:低(7B模型约16-24GB)
QLoRA:极低(7B模型约6-10GB)
Adapter:低
Prefix Tuning:极低
1.4 大模型的结构基础
理解微调算法需要先了解Transformer的基本结构:
一个Transformer层的结构:
输入 x
│
├──→ [Self-Attention] ──→ 残差连接 ──→ LayerNorm
│ ├── Q_proj (查询投影)
│ ├── K_proj (键投影)
│ ├── V_proj (值投影)
│ └── O_proj (输出投影)
│
├──→ [Feed-Forward Network] ──→ 残差连接 ──→ LayerNorm
│ ├── Gate_proj (门控投影)
│ ├── Up_proj (上投影)
│ └── Down_proj (下投影)
│
└──→ 输出
一个7B模型通常有:
- 32层Transformer
- 每层有7个线性投影层(Q/K/V/O/Gate/Up/Down)
- 总共 32 × 7 = 224个线性层
- 总参数量约70亿
不同微调方法"插"在哪里:
┌─────────────────────────────────────────────────┐
│ 微调方法与模型结构的关系 │
│ │
│ LoRA: 在Q/K/V/O/Gate/Up/Down旁边加旁路 │
│ Adapter: 在每层的输出后面插入小型网络 │
│ Prefix: 在注意力计算的K/V前面加可训练向量 │
│ Prompt: 在输入embedding前面加可训练向量 │
│ IA³: 对K/V/FFN的输出做可学习的缩放 │
│ BitFit: 只训练所有层的bias参数 │
│ 全量微调: 所有参数都训练 │
└─────────────────────────────────────────────────┘
第二章:全量微调 (Full Fine-Tuning)
2.1 原理
全量微调 = 更新模型的所有参数
数学表达:
θ_new = θ_pretrained - η × ∇L(θ_pretrained, D_finetune)
其中:
- θ_pretrained: 预训练模型的所有参数
- D_finetune: 微调数据集
- L: 损失函数(通常是交叉熵)
- η: 学习率
- θ_new: 微调后的所有参数
2.2 优缺点
优点:
✓ 效果最好(所有参数都在优化)
✓ 无额外推理开销(模型结构不变)
✓ 实现最简单(标准训练流程)
✓ 可以最大程度适应新任务
缺点:
✗ 显存需求极高(需要存储参数+梯度+优化器状态)
✗ 容易过拟合(小数据集上)
✗ 灾难性遗忘(可能丢失预训练的通用能力)
✗ 存储成本高(每个任务一个完整模型副本)
✗ 训练时间长
显存需求估算(7B模型,bf16精度):
模型参数: 7B × 2 bytes = 14 GB
梯度: 7B × 2 bytes = 14 GB
优化器状态(Adam): 7B × 8 bytes = 56 GB
激活值: 约20-40 GB(取决于batch size和序列长度)
─────────────────────────
总计: 约104-124 GB(至少需要2张A100-80G)
2.3 显存优化技术
技术1:混合精度训练 (AMP)
- 用bf16/fp16做前向和反向传播
- 用fp32维护主权重副本
- 节省约50%显存
技术2:梯度检查点 (Gradient Checkpointing)
- 不保存中间激活值,反向传播时重新计算
- 用计算换显存
- 节省约60%激活值显存,但训练慢约30%
技术3:梯度累积 (Gradient Accumulation)
- 多个mini-batch的梯度累积后再更新
- 等效于更大的batch size,但显存不增加
技术4:DeepSpeed ZeRO
- ZeRO-1: 分片优化器状态 → 显存节省约4倍
- ZeRO-2: 分片优化器状态+梯度 → 显存节省约8倍
- ZeRO-3: 分片优化器状态+梯度+参数 → 显存节省约N倍(N=GPU数)
技术5:CPU Offload
- 将优化器状态和部分参数卸载到CPU内存
- 显存大幅节省,但训练速度下降
2.4 代码示例
python
# 全量微调的核心代码(使用HuggingFace Trainer)
from transformers import AutoModelForCausalLM, TrainingArguments, Trainer
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-7B-Instruct")
training_args = TrainingArguments(
output_dir="./output_full_ft",
num_train_epochs=3,
per_device_train_batch_size=1,
gradient_accumulation_steps=16,
learning_rate=2e-5, # 全量微调用较小的学习率
bf16=True,
gradient_checkpointing=True, # 必须开启,否则显存不够
deepspeed="ds_config.json", # 使用DeepSpeed ZeRO
)
trainer = Trainer(model=model, args=training_args, train_dataset=dataset)
trainer.train()
第三章:LoRA系列算法
3.1 LoRA (Low-Rank Adaptation)
3.1.1 核心原理
LoRA的核心思想:
预训练权重矩阵 W 的更新量 ΔW 是低秩的
可以用两个小矩阵的乘积来近似:ΔW = B × A
数学表达:
原始: h = Wx
LoRA: h = Wx + (B × A) × x = Wx + B × (Ax)
其中:
- W ∈ R^{d×k} (原始权重,冻结不训练)
- A ∈ R^{r×k} (下投影矩阵,r << min(d,k))
- B ∈ R^{d×r} (上投影矩阵,初始化为0)
- r: LoRA的秩(rank),通常取8, 16, 32, 64
初始化策略:
- A: 使用高斯随机初始化(或Kaiming初始化)
- B: 初始化为全零矩阵
- 这样训练开始时 ΔW = B×A = 0,不影响原始模型
缩放因子:
ΔW = (α/r) × B × A
α通常设为r的2倍(如r=16, α=32)
α/r = 2,相当于将LoRA的更新乘以2
3.1.2 为什么有效?
关键洞察:预训练模型的权重更新矩阵是低秩的
直觉理解:
- 预训练模型已经学到了丰富的知识
- 微调只需要做"微小的调整"
- 这种调整可以用低秩矩阵来近似
- r=16 意味着只用16个自由度来描述调整方向
实验验证:
- 原始论文发现 r=4 在很多任务上就够用了
- 增大r带来的提升有限
- 说明微调确实只需要低秩的调整
3.1.3 代码实现
python
from peft import LoraConfig, get_peft_model, TaskType
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=16, # LoRA秩
lora_alpha=32, # 缩放因子
lora_dropout=0.05, # Dropout
target_modules=[ # 应用LoRA的目标层
"q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj",
],
bias="none",
)
model = get_peft_model(base_model, lora_config)
model.print_trainable_parameters()
# trainable params: 13,631,488 || all params: 7,615,616,000 || trainable%: 0.1790
3.1.4 超参数选择指南
r (LoRA秩):
- r=4-8: 简单任务(格式调整、风格迁移)
- r=16-32: 一般任务(领域问答、指令微调)
- r=64-128: 复杂任务(大量新知识注入)
- 经验法则:先用r=16,效果不够再增大
lora_alpha:
- 通常设为 r 的2倍
- 也可以设为 r(此时缩放因子为1)
- 更大的alpha意味着LoRA更新的影响更大
target_modules:
- 只选注意力层: ["q_proj", "v_proj"] → 最少参数
- 选全部注意力: ["q_proj", "k_proj", "v_proj", "o_proj"] → 推荐
- 选全部层: 加上["gate_proj", "up_proj", "down_proj"] → 效果最好但参数多
lora_dropout:
- 0.0-0.1(数据少时用较大的dropout)
- 数据充足时可以设为0
3.2 QLoRA (Quantized LoRA)
3.2.1 核心原理
QLoRA = 4bit量化基座模型 + LoRA微调
三个关键创新:
1. 4-bit NormalFloat (NF4) 量化
- 将模型权重从16bit压缩到4bit
- 显存减少约4倍
- NF4对正态分布的权重最优
2. 双重量化 (Double Quantization)
- 对量化的缩放因子也进行量化
- 进一步节省约0.37bit/参数
3. 分页优化器 (Paged Optimizers)
- 使用NVIDIA统一内存管理
- 在GPU显存不足时自动将优化器状态转移到CPU
显存对比(7B模型):
LoRA (bf16): 约16-24 GB
QLoRA (4bit): 约6-10 GB ← 单张RTX 3090/4090就能跑
3.2.2 代码实现
python
from transformers import BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
import torch
# 4bit量化配置
bnb_config = BitsAndBytesConfig(
load_in_4bit=True, # 4bit加载
bnb_4bit_quant_type="nf4", # NF4量化类型
bnb_4bit_compute_dtype=torch.bfloat16, # 计算时用bf16
bnb_4bit_use_double_quant=True, # 开启双重量化
)
# 以4bit加载模型
model = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen2.5-7B-Instruct",
quantization_config=bnb_config,
device_map="auto",
)
# 准备模型进行量化训练
model = prepare_model_for_kbit_training(model)
# 配置LoRA
lora_config = LoraConfig(
r=16, lora_alpha=32, lora_dropout=0.05,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"],
bias="none", task_type="CAUSAL_LM",
)
model = get_peft_model(model, lora_config)
3.3 DoRA (Weight-Decomposed Low-Rank Adaptation)
3.3.1 核心原理
DoRA的核心思想:
将权重矩阵分解为"方向"和"大小"两个组件
分别用不同的策略进行更新
数学表达:
W = m × (W / ||W||) (方向-大小分解)
其中:
- m: 大小向量(magnitude),可训练
- W/||W||: 方向(direction),用LoRA更新
更新后:
W' = (m + Δm) × ((W + BA) / ||W + BA||)
为什么比LoRA好?
- 分析发现:全量微调倾向于同时调整方向和大小
- LoRA只能间接调整这两个组件
- DoRA显式分离,每个组件用最优策略更新
3.3.2 代码实现
python
from peft import LoraConfig, get_peft_model
# DoRA只需在LoRA配置中添加一个参数
dora_config = LoraConfig(
r=16,
lora_alpha=32,
lora_dropout=0.05,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"],
use_dora=True, # ← 开启DoRA
bias="none",
task_type="CAUSAL_LM",
)
model = get_peft_model(base_model, dora_config)
3.4 AdaLoRA (Adaptive LoRA)
3.4.1 核心原理
AdaLoRA的核心思想:
不同层、不同模块对微调的重要性不同
应该给重要的模块分配更高的秩(rank)
方法:
1. 初始化所有LoRA为相同秩
2. 训练过程中评估每个LoRA的重要性
3. 重要的LoRA增加秩,不重要的减少秩
4. 总参数预算不变,但分配更合理
重要性评估:
基于SVD分解的奇异值大小
奇异值大的方向更重要,保留
奇异值小的方向不太重要,裁剪
3.4.2 代码实现
python
from peft import AdaLoraConfig, get_peft_model
adalora_config = AdaLoraConfig(
init_r=12, # 初始秩
target_r=4, # 目标秩(裁剪后)
beta1=0.85,
beta2=0.85,
tinit=200, # 开始裁剪的步数
tfinal=1000, # 结束裁剪的步数
deltaT=10, # 裁剪间隔
lora_alpha=32,
lora_dropout=0.05,
target_modules=["q_proj", "v_proj"],
task_type="CAUSAL_LM",
)
model = get_peft_model(base_model, adalora_config)
3.5 rsLoRA (Rank-Stabilized LoRA)
3.5.1 核心原理
标准LoRA的问题:
缩放因子 α/r 中,当r增大时,缩放因子减小
这导致增大r时,LoRA的更新幅度反而减小
使得增大r的收益递减
rsLoRA的解决方案:
将缩放因子从 α/r 改为 α/√r
效果:
- 当r增大时,更新幅度不会过度缩小
- 使得增大r能带来更一致的性能提升
- 特别适合使用较大r值的场景
3.5.2 代码实现
python
from peft import LoraConfig, get_peft_model
rslora_config = LoraConfig(
r=64,
lora_alpha=32,
use_rslora=True, # ← 开启rsLoRA
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
task_type="CAUSAL_LM",
)
model = get_peft_model(base_model, rslora_config)
3.6 LoRA+
3.6.1 核心原理
LoRA+的核心发现:
LoRA中的A矩阵和B矩阵应该用不同的学习率
标准LoRA: lr_A = lr_B
LoRA+: lr_B = λ × lr_A,其中 λ >> 1(通常λ=16)
为什么?
- B矩阵初始化为0,需要更大的步长来"逃离"零点
- A矩阵已经随机初始化,需要更小的步长来精细调整
- 实验表明 λ=16 效果最好
3.6.2 代码实现
python
from peft import LoraConfig, get_peft_model
loraplus_config = LoraConfig(
r=16,
lora_alpha=32,
loraplus_lr_ratio=16.0, # ← B矩阵的学习率是A矩阵的16倍
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
task_type="CAUSAL_LM",
)
model = get_peft_model(base_model, loraplus_config)
3.7 GaLore (Gradient Low-Rank Projection)
3.7.1 核心原理
GaLore的核心思想:
不是限制权重更新的秩,而是限制梯度的秩
方法:
1. 计算梯度 G
2. 对梯度做低秩投影: G_lr = P × G × Q
3. 用低秩梯度更新全量参数
优势:
- 理论上可以达到全量微调的效果
- 显存需求和LoRA相当
- 不需要修改模型结构
与LoRA的区别:
LoRA: 只更新低秩的旁路参数
GaLore: 更新全部参数,但梯度是低秩的
3.7.2 代码实现
python
from transformers import TrainingArguments
# GaLore通过优化器配置实现
training_args = TrainingArguments(
output_dir="./output_galore",
optim="galore_adamw",
optim_target_modules=["attn", "mlp"],
galore_rank=128, # 梯度投影的秩
galore_update_proj_gap=200, # 投影矩阵更新间隔
galore_scale=0.25,
learning_rate=2e-5,
# ... 其他参数
)
3.8 VeRA (Vector-based Random matrix Adaptation)
3.8.1 核心原理
VeRA的核心思想:
比LoRA更极端的参数效率
LoRA: ΔW = B × diag(d) × A
VeRA: ΔW = B × diag(d_b) × Λ × diag(d_a) × A
其中:
- A, B: 共享的随机矩阵(所有层共享,不训练)
- d_a, d_b: 可训练的缩放向量
效果:
- 参数量比LoRA少10倍以上
- 但效果接近LoRA
- 适合极端资源受限的场景
3.9 PiSSA (Principal Singular values and Singular vectors Adaptation)
3.9.1 核心原理
PiSSA的核心改进:
LoRA的A、B矩阵是随机初始化的
PiSSA用SVD分解来初始化,让LoRA一开始就"有意义"
方法:
1. 对预训练权重W做SVD: W = U × Σ × V^T
2. 用最大的r个奇异值和向量初始化LoRA
3. 剩余的奇异值保留在残差中
W = (U_r × Σ_r × V_r^T) + (U_remaining × Σ_remaining × V_remaining^T)
↑ LoRA部分(可训练) ↑ 残差部分(冻结)
效果:
- 收敛更快
- 最终效果更好
- 特别适合小数据集微调
3.10 LoRA家族总结
LoRA家族算法对比:
算法 核心改进 额外参数 推理开销 效果提升
────────────────────────────────────────────────────────
LoRA 基准方法 ~0.2% 无(合并后) 基准
QLoRA 4bit量化基座 ~0.2% 无(合并后) 略低于LoRA
DoRA 方向-大小分解 ~0.25% 无(合并后) +1-3%
AdaLoRA 自适应秩分配 ~0.2% 无(合并后) +1-2%
rsLoRA 稳定缩放因子 ~0.2% 无(合并后) +0.5-1%
LoRA+ 不同学习率 ~0.2% 无(合并后) +0.5-1%
GaLore 低秩梯度投影 0(全参数) 无 +1-3%
VeRA 共享随机矩阵 ~0.02% 无(合并后) 略低于LoRA
PiSSA SVD初始化 ~0.2% 无(合并后) +1-2%
MosLoRA 多尺度LoRA ~0.3% 无(合并后) +1-2%
第四章:前缀微调系列算法
4.1 Prefix Tuning (前缀微调)
4.1.1 核心原理
Prefix Tuning的核心思想:
在每一层的注意力计算中,为Key和Value添加可训练的"前缀"向量
这些前缀向量就像是"虚拟token",引导模型的注意力分布
具体做法:
原始注意力计算:
Q = X × W_Q, K = X × W_K, V = X × W_V
Attention = softmax(QK^T/√d) × V
添加前缀后:
K = [P_K; X × W_K], V = [P_V; X × W_V]
其中 P_K, P_V 是可训练的前缀矩阵
前缀长度通常为10-200个token
┌─────────────────────────────────────────────┐
│ Prefix Tuning 示意图 │
│ │
│ 可训练前缀: [P₁][P₂]...[Pₙ] │
│ ↓ │
│ 每一层的K/V: [P_K₁, P_K₂, ..., x₁, x₂, ...] │
│ [P_V₁, P_V₂, ..., v₁, v₂, ...] │
│ │
│ 原始模型参数: 冻结 │
│ 前缀参数: 可训练 │
└─────────────────────────────────────────────┘
4.1.2 参数量
前缀参数量 = n_layers × 2 × prefix_length × hidden_dim
示例(Qwen2.5-7B):
n_layers = 28
hidden_dim = 3584
prefix_length = 20
参数量 = 28 × 2 × 20 × 3584 = 4,014,080(约400万,占总参数0.005%)
4.1.3 代码实现
python
from peft import PrefixTuningConfig, get_peft_model, TaskType
prefix_config = PrefixTuningConfig(
task_type=TaskType.CAUSAL_LM,
num_virtual_tokens=20, # 前缀token数量
prefix_projection=True, # 是否使用投影层
encoder_hidden_size=1024, # 投影层隐藏维度
)
model = get_peft_model(base_model, prefix_config)
4.1.4 优缺点
优点:
✓ 参数量极低
✓ 不修改模型结构
✓ 不同任务可以共享同一个基座模型
✓ 推理时只需替换前缀
缺点:
✗ 增加序列长度,有推理开销
✗ 占用有效上下文窗口
✗ 在复杂任务上效果通常不如LoRA
✗ 训练可能不稳定
4.2 P-Tuning v1
4.2.1 核心原理
P-Tuning的核心思想:
在输入层插入可训练的"伪token" embedding
这些embedding不对应真实的词表token
与Prefix Tuning的区别:
- Prefix Tuning: 在每一层都加前缀(K和V)
- P-Tuning v1: 只在输入层加可训练embedding
实现方式:
使用LSTM或MLP将可训练向量编码后插入输入序列
4.2.2 代码实现
python
from peft import PromptEncoderConfig, get_peft_model, TaskType
p_tuning_config = PromptEncoderConfig(
task_type=TaskType.CAUSAL_LM,
num_virtual_tokens=20,
encoder_hidden_size=1024,
encoder_num_layers=2, # 编码器层数
encoder_dropout=0.1,
)
model = get_peft_model(base_model, p_tuning_config)
4.3 P-Tuning v2
4.3.1 核心原理
P-Tuning v2 = Prefix Tuning的改进版
核心改进:
1. 在每一层都添加可训练前缀(而不是只在输入层)
2. 去掉了复杂的编码器(直接用可训练参数)
3. 对不同任务有更好的泛化性
与Prefix Tuning的关系:
P-Tuning v2 在技术上非常接近 Prefix Tuning
主要区别在于实现细节和默认配置
4.3.2 代码实现
python
from peft import PrefixTuningConfig, get_peft_model, TaskType
ptuning_v2_config = PrefixTuningConfig(
task_type=TaskType.CAUSAL_LM,
num_virtual_tokens=100,
prefix_projection=False, # P-Tuning v2通常不用投影
)
model = get_peft_model(base_model, ptuning_v2_config)
4.4 Multitask Prompt Tuning (MPT)
核心思想:
多个相关任务共享一个基础前缀
每个任务有自己的任务特定前缀
总前缀 = 共享前缀 + 任务特定前缀
优势:
- 共享前缀学到通用知识
- 任务前缀学到特定知识
- 多任务联合训练效果更好
第五章:适配器系列算法 (Adapter)
5.1 Houlsby Adapter
5.1.1 核心原理
Adapter的核心思想:
在Transformer的每一层中插入小型的"适配器"模块
这些适配器是可训练的,原始模型参数冻结
Houlsby Adapter的结构:
原始输出 → LayerNorm → Down-projection → 非线性激活 → Up-projection → 残差连接
具体地:
- Down-projection: h → W_down × h + b_down (d → r, 如 4096 → 64)
- 非线性激活: ReLU 或 GELU
- Up-projection: h' → W_up × h' + b_up (r → d, 如 64 → 4096)
- 残差连接: output = adapter(h) + h
┌─────────────────────────────────────┐
│ Adapter模块结构 │
│ │
│ 输入 x (维度d) │
│ ├──→ [Down Projection] → (维度r) │
│ │ ↓ │
│ │ [激活函数 ReLU] │
│ │ ↓ │
│ │ [Up Projection] → (维度d) │
│ │ ↓ │
│ └──→ [残差相加] │
│ ↓ │
│ 输出 (维度d) │
└─────────────────────────────────────┘
瓶颈维度r是关键参数:
r越小:参数越少,但表达能力越弱
r越大:参数越多,但效果越好
通常r取64-256
5.1.2 代码实现
python
from peft import IA3Config, get_peft_model, TaskType
# 注:PEFT库中Adapter的实现可能需要自定义
# 以下是概念性代码
class AdapterLayer(nn.Module):
def __init__(self, hidden_dim, adapter_dim=64):
super().__init__()
self.down_proj = nn.Linear(hidden_dim, adapter_dim)
self.up_proj = nn.Linear(adapter_dim, hidden_dim)
self.act = nn.ReLU()
nn.init.zeros_(self.up_proj.weight)
nn.init.zeros_(self.up_proj.bias)
def forward(self, x):
residual = x
x = self.down_proj(x)
x = self.act(x)
x = self.up_proj(x)
return x + residual
5.2 AdapterFusion
核心思想:
训练多个任务特定的Adapter
然后用注意力机制融合它们的输出
方法:
1. 在每个任务上分别训练Adapter
2. 新任务到来时,冻结所有已训练的Adapter
3. 训练一个注意力层来融合各Adapter的输出
优势:
- 可以利用多个任务的知识
- 不需要重新训练
- 新任务的学习不会影响旧任务
5.3 MAM Adapter
核心思想:
将Prefix Tuning、Adapter和LoRA的优势结合
MAM = Mix-and-Match Adapter
具体做法:
- FFN层使用Adapter
- 注意力层使用Prefix
- 两者结合,取长补短
效果:
- 参数量和Prefix/Adapter相当
- 效果优于单独使用任一方法
第六章:提示微调系列算法 (Prompt Tuning)
6.1 Prompt Tuning
6.1.1 核心原理
Prompt Tuning的核心思想:
在输入文本前面添加一组可训练的"软提示"(soft prompt) embedding
这些embedding不对应任何真实的词表token
原始输入: [x₁, x₂, ..., xₙ] (n个token embedding)
添加软提示: [p₁, p₂, ..., pₘ, x₁, x₂, ..., xₙ] (m个可训练 + n个原始)
训练时:只更新 p₁...pₘ
模型参数完全冻结
6.1.2 与Prefix Tuning的区别
Prompt Tuning:
- 只在输入层添加可训练embedding
- 参数量更少
- 实现更简单
Prefix Tuning:
- 在每一层的K和V都添加前缀
- 参数量稍多
- 效果通常更好
类比:
Prompt Tuning = 只在门口放一个"提示牌"
Prefix Tuning = 在每个房间都放一个"提示牌"
6.1.3 代码实现
python
from peft import PromptTuningConfig, get_peft_model, TaskType, PromptTuningInit
prompt_config = PromptTuningConfig(
task_type=TaskType.CAUSAL_LM,
num_virtual_tokens=100, # 软提示长度
prompt_tuning_init=PromptTuningInit.TEXT, # 用文本初始化
prompt_tuning_init_text="请回答以下问题:", # 初始化文本
tokenizer_name_or_path="Qwen/Qwen2.5-7B-Instruct",
)
model = get_peft_model(base_model, prompt_config)
6.2 WARP (Word-level Adversarial ReProgramming)
核心思想:
在输入层添加可训练的token embedding
同时使用对抗训练来增强鲁棒性
与Prompt Tuning的区别:
- 增加了对抗扰动
- 对输入变化更鲁棒
6.3 提示微调方法总结
提示微调系列对比:
方法 插入位置 参数量 效果 适用场景
──────────────────────────────────────────────────────────────
Prompt Tuning 输入层 极少 一般 简单分类任务
P-Tuning v1 输入层+编码器 少 较好 生成任务
P-Tuning v2 每一层 少 好 通用
Prefix Tuning 每一层的K/V 少 好 通用
Multitask PT 每一层(多任务) 少 最好 多任务学习
共同特点:
- 不修改模型结构
- 参数量极少
- 推理时有序列长度开销
- 适合资源极其有限的场景
第七章:IA³与其他高效微调方法
7.1 IA³ (Infused Adapter by Inhibiting and Amplifying Inner Activations)
7.1.1 核心原理
IA³的核心思想:
不添加新模块,而是对现有激活值做可学习的缩放
具体做法:
- 对Key的输出乘以可学习向量 l_k
- 对Value的输出乘以可学习向量 l_v
- 对FFN的输出乘以可学习向量 l_ff
数学表达:
K' = l_k ⊙ K (⊙ 表示逐元素乘法)
V' = l_v ⊙ V
FFN_out' = l_ff ⊙ FFN_out
参数量极低:只有3个向量 × 层数
7.1.2 代码实现
python
from peft import IA3Config, get_peft_model, TaskType
ia3_config = IA3Config(
task_type=TaskType.CAUSAL_LM,
target_modules=["k_proj", "v_proj", "down_proj"], # 对这些层做缩放
feedforward_modules=["down_proj"], # FFN层
)
model = get_peft_model(base_model, ia3_config)
7.1.3 参数量
IA3的参数量(Qwen2.5-7B示例):
每层需要3个缩放向量:
- l_k: hidden_dim = 3584
- l_v: hidden_dim = 3584
- l_ff: ffn_dim = 18944
28层总计: 28 × (3584 + 3584 + 18944) = 728,448(约73万参数)
占总参数: 73万 / 70亿 = 0.001% ← 极少!
7.2 BitFit
7.2.1 核心原理
BitFit的核心发现:
只训练模型中的bias参数,也能达到不错的效果!
一个Transformer层中的bias参数:
- Self-Attention: Q/K/V/O的bias
- FFN: 两层的bias
- LayerNorm: bias
参数量:约0.1%的总参数
为什么有效?
- bias参数虽然少,但控制着每层输出的"基线"
- 调整基线可以显著改变模型行为
- 类似于调整"门控"信号
7.2.2 代码实现
python
# BitFit的实现非常简单:冻结所有权重,只训练bias
for name, param in model.named_parameters():
if "bias" not in name:
param.requires_grad = False
else:
param.requires_grad = True
7.3 Compacter
核心思想:
Adapter的改进版,使用:
1. 共享的超网络(HyperNetwork)生成Adapter参数
2. 低秩分解进一步减少参数
3. 结构化dropout
比标准Adapter参数量少很多
7.4 S4 (Structured State Spaces for Sequence Modeling)
虽然S4不是严格的微调方法,但在长序列微调中很有价值:
- 基于状态空间模型(SSM)
- 替代注意力机制处理长序列
- 推理复杂度O(1),不像注意力的O(n²)
- 最新的Mamba架构就是基于此
第八章:基于人类反馈的微调方法 (RLHF/DPO/GRPO)
8.1 RLHF (Reinforcement Learning from Human Feedback)
8.1.1 三阶段流程
RLHF的完整流程:
阶段1:监督微调(SFT)
数据:人工编写的高质量问答对
方法:标准的监督学习
目标:让模型学会基本的指令遵循能力
阶段2:奖励模型训练(RM)
数据:人类对模型输出的排序(A比B好)
方法:训练一个奖励模型,输入(问题,回答)→输出分数
目标:让奖励模型学会人类的偏好
阶段3:PPO强化学习优化
数据:模型生成的回答 + 奖励模型的评分
方法:用PPO算法优化语言模型,最大化奖励模型的评分
目标:让模型生成更符合人类偏好的回答
8.1.2 奖励模型训练
python
from transformers import AutoModelForSequenceClassification
from trl import RewardTrainer, RewardConfig
# 奖励模型:输入(问题+回答) → 输出一个标量分数
reward_model = AutoModelForSequenceClassification.from_pretrained(
"Qwen/Qwen2.5-7B-Instruct",
num_labels=1, # 输出一个标量分数
)
# 训练数据格式
# {"chosen": "好的回答", "rejected": "差的回答"}
reward_config = RewardConfig(
output_dir="./reward_model",
num_train_epochs=1,
per_device_train_batch_size=2,
learning_rate=1e-5,
)
trainer = RewardTrainer(
model=reward_model,
args=reward_config,
train_dataset=dataset,
processing_class=tokenizer,
)
trainer.train()
8.1.3 PPO训练
python
from trl import PPOTrainer, PPOConfig, AutoModelForCausalLMWithValueHead
# 给语言模型加一个Value Head(用于PPO的Critic)
model = AutoModelForCausalLMWithValueHead.from_pretrained("sft_model_path")
ppo_config = PPOConfig(
learning_rate=1e-5,
batch_size=16,
mini_batch_size=4,
ppo_epochs=4,
kl_penalty="kl", # KL散度惩罚
init_kl_coef=0.2, # KL惩罚系数
target_kl=6.0, # 目标KL散度
)
ppo_trainer = PPOTrainer(
config=ppo_config,
model=model,
ref_model=ref_model, # 参考模型(SFT模型的副本)
tokenizer=tokenizer,
dataset=dataset,
)
# 训练循环
for batch in dataloader:
# 1. 生成回答
response_tensors = ppo_trainer.generate(batch["input_ids"])
# 2. 奖励模型评分
rewards = reward_model(batch["input_ids"], response_tensors)
# 3. PPO更新
stats = ppo_trainer.step(batch["input_ids"], response_tensors, rewards)
8.2 DPO (Direct Preference Optimization)
8.2.1 核心原理
DPO的核心创新:
不需要单独训练奖励模型!
直接从偏好数据优化策略
RLHF流程: SFT → 训练RM → PPO优化 (三步)
DPO流程: SFT → DPO优化 (两步)
DPO的损失函数:
L_DPO = -E[log σ(β × (log π(y_w|x)/π_ref(y_w|x) - log π(y_l|x)/π_ref(y_l|x)))]
其中:
- π: 当前策略(正在优化的模型)
- π_ref: 参考策略(SFT模型,冻结)
- y_w: 人类偏好的回答(chosen)
- y_l: 人类不偏好的回答(rejected)
- β: 温度参数(控制偏离参考策略的程度)
- σ: sigmoid函数
8.2.2 代码实现
python
from trl import DPOTrainer, DPOConfig
# 训练数据格式
# {"prompt": "问题", "chosen": "好的回答", "rejected": "差的回答"}
dpo_config = DPOConfig(
output_dir="./dpo_output",
num_train_epochs=1,
per_device_train_batch_size=2,
learning_rate=5e-7, # DPO用较小的学习率
beta=0.1, # 温度参数
loss_type="sigmoid", # 损失类型
remove_unused_columns=False,
)
# DPO需要两个模型:当前策略和参考模型
trainer = DPOTrainer(
model=sft_model, # 当前策略(会被更新)
ref_model=ref_model, # 参考模型(冻结)
args=dpo_config,
train_dataset=dataset,
processing_class=tokenizer,
)
trainer.train()
8.3 KTO (Kahneman-Tversky Optimization)
8.3.1 核心原理
KTO的核心改进:
DPO需要成对的偏好数据(chosen, rejected)
KTO只需要单独的"好"或"坏"标签
为什么更好?
- 收集成对偏好数据很昂贵
- 收集单独的好/坏标签更容易
- 例如:用户点"赞"就是好,点"踩"就是坏
KTO的损失函数基于前景理论(Prospect Theory):
- 人类对损失比收益更敏感
- 损失厌恶系数 λ > 1
8.3.2 代码实现
python
from trl import KTOTrainer, KTOConfig
# 训练数据格式
# {"prompt": "问题", "completion": "回答", "label": true/false}
kto_config = KTOConfig(
output_dir="./kto_output",
num_train_epochs=1,
per_device_train_batch_size=2,
learning_rate=5e-7,
beta=0.1,
desirable_weight=1.0, # 好样本的权重
undesirable_weight=1.0, # 坏样本的权重
)
trainer = KTOTrainer(
model=sft_model,
ref_model=ref_model,
args=kto_config,
train_dataset=dataset,
processing_class=tokenizer,
)
trainer.train()
8.4 ORPO (Odds Ratio Preference Optimization)
8.4.1 核心原理
ORPO的核心创新:
不需要参考模型!(DPO和KTO都需要)
将SFT和偏好对齐合并为一步
优势:
- 训练流程更简单(一步到位)
- 不需要维护两个模型
- 显存需求更低
损失函数 = SFT损失 + 偏好对齐损失
8.4.2 代码实现
python
from trl import ORPOTrainer, ORPOConfig
orpo_config = ORPOConfig(
output_dir="./orpo_output",
num_train_epochs=1,
per_device_train_batch_size=2,
learning_rate=5e-7,
beta=0.1, # ORPO的β参数
)
# ORPO不需要ref_model
trainer = ORPOTrainer(
model=base_model, # 直接从基座模型开始
args=orpo_config,
train_dataset=dataset, # {"prompt", "chosen", "rejected"}
processing_class=tokenizer,
)
trainer.train()
8.5 GRPO (Group Relative Policy Optimization)
8.5.1 核心原理
GRPO的核心创新(DeepSeek提出):
不需要Critic网络(PPO需要)
用组内相对排名来估计优势函数
方法:
1. 对每个问题,生成一组回答(如8个)
2. 用奖励模型或规则给每个回答打分
3. 组内排名归一化作为优势估计
4. 用PPO-Clip的目标函数更新策略
优势:
- 不需要训练Critic网络(省显存、省时间)
- 奖励可以是稀疏的(只需要排名,不需要绝对分数)
- 特别适合数学推理等有明确对错的任务
8.5.2 代码实现
python
from trl import GRPOTrainer, GRPOConfig
grpo_config = GRPOConfig(
output_dir="./grpo_output",
num_train_epochs=1,
per_device_train_batch_size=2,
learning_rate=1e-6,
num_generations=8, # 每个问题生成8个回答
max_completion_length=512,
beta=0.04, # KL惩罚系数
)
def reward_func(completions, **kwargs):
"""奖励函数(可以用规则或奖励模型)"""
rewards = []
for completion in completions:
# 这里用简单的规则判断
if "正确" in completion or check_answer(completion):
rewards.append(1.0)
else:
rewards.append(-1.0)
return rewards
trainer = GRPOTrainer(
model=base_model,
args=grpo_config,
train_dataset=dataset, # {"prompt": "问题"}
reward_funcs=reward_func,
processing_class=tokenizer,
)
trainer.train()
8.6 SimPO (Simple Preference Optimization)
核心改进:
- DPO需要参考模型,SimPO不需要
- 用序列长度归一化的对数概率作为隐式奖励
- 引入了奖励边际(reward margin)参数γ
损失函数:
L_SimPO = -E[log σ(β/|y_w| × log π(y_w|x) - β/|y_l| × log π(y_l|x) - γ)]
优势:
- 更简单(不需要参考模型)
- 更稳定
- 效果通常优于DPO
8.7 偏好微调方法总结
偏好微调方法对比:
方法 需要RM 需要Ref 数据格式 复杂度 效果
────────────────────────────────────────────────────────
RLHF 是 是 (prompt, response) 高 好
DPO 否 是 (chosen, rejected) 中 好
KTO 否 是 (好/坏标签) 低 较好
ORPO 否 否 (chosen, rejected) 低 好
GRPO 否 否 (prompt) 中 很好
SimPO 否 否 (chosen, rejected) 低 好
推荐:
- 数据充足且有GPU资源 → RLHF/GRPO
- 有成对偏好数据 → DPO/ORPO/SimPO
- 只有好/坏标签 → KTO
- 追求简单 → ORPO/SimPO
- 数学推理任务 → GRPO
第九章:微调算法对比与选型指南
9.1 综合对比表
┌──────────────────────────────────────────────────────────────────────┐
│ 微调算法综合对比 │
├──────────┬──────────┬──────────┬──────────┬──────────┬──────────────┤
│ 算法 │可训练参数 │ 训练显存 │ 推理开销 │ 效果 │ 适用场景 │
│ │ (7B模型) │ (7B模型) │ │ │ │
├──────────┼──────────┼──────────┼──────────┼──────────┼──────────────┤
│全量微调 │ 100% │ >120GB │ 无 │ ★★★★★ │ 数据充足+大GPU│
│LoRA │ 0.1-0.5% │ 16-24GB │ 无(合并) │ ★★★★ │ 通用推荐 │
│QLoRA │ 0.1-0.5% │ 6-10GB │ 无(合并) │ ★★★☆ │ 显存受限 │
│DoRA │ 0.1-0.5% │ 16-24GB │ 无(合并) │ ★★★★+ │ 追求更好效果 │
│AdaLoRA │ 0.1-0.5% │ 18-26GB │ 无(合并) │ ★★★★ │ 不确定最佳秩 │
│Prefix │ <0.1% │ 8-12GB │ 有 │ ★★★ │ 极端资源受限 │
│P-Tuning │ <0.1% │ 8-12GB │ 有 │ ★★★ │ 简单任务 │
│Adapter │ 1-5% │ 12-20GB │ 有 │ ★★★☆ │ 多任务学习 │
│IA³ │ <0.01% │ 8-10GB │ 有 │ ★★☆ │ 极端资源受限 │
│BitFit │ ~0.1% │ 10-14GB │ 无 │ ★★☆ │ 简单任务 │
│GaLore │ 100% │ 16-24GB │ 无 │ ★★★★ │ 接近全量效果 │
├──────────┼──────────┼──────────┼──────────┼──────────┼──────────────┤
│DPO │ 0.1-100% │ 32-48GB │ 无 │ ★★★★ │ 偏好对齐 │
│KTO │ 0.1-100% │ 16-32GB │ 无 │ ★★★ │ 简单反馈 │
│ORPO │ 0.1-100% │ 16-32GB │ 无 │ ★★★★ │ 简单偏好对齐 │
│GRPO │ 0.1-100% │ 24-40GB │ 无 │ ★★★★★ │ 数学推理 │
│RLHF │ 0.1-100% │ 64-120GB │ 无 │ ★★★★★ │ 全面对齐 │
└──────────┴──────────┴──────────┴──────────┴──────────┴──────────────┘
9.2 决策树
我应该用什么微调方法?
Q1: 你有多少GPU显存?
├── <10GB → QLoRA / IA³ / Prefix Tuning
├── 10-24GB → LoRA (推荐) / QLoRA / DoRA
├── 24-80GB → LoRA / DoRA / GaLore / DPO
└── >80GB (多卡) → 全量微调 / RLHF
Q2: 你有多少训练数据?
├── <100条 → LoRA (小r值) / Prompt Tuning
├── 100-1000条 → LoRA / QLoRA
├── 1000-10000条 → LoRA / DoRA / 全量微调
└── >10000条 → 全量微调 / LoRA (大r值)
Q3: 你的任务是什么?
├── 知识注入 → LoRA / 全量微调
├── 格式对齐 → LoRA (小r值)
├── 偏好对齐 → DPO / ORPO / GRPO
├── 数学推理 → GRPO
└── 多任务 → Adapter / LoRA
Q4: 你需要多快的训练速度?
├── 最快 → IA³ / Prefix Tuning
├── 较快 → LoRA / QLoRA
└── 可以等待 → 全量微调 / RLHF
通用推荐:
不确定用什么?→ 先试LoRA (r=16, lr=2e-4)
效果不够?→ 增大r / 加DoRA / 换全量微调
显存不够?→ 换QLoRA
需要对齐?→ LoRA SFT → DPO
9.3 常见场景推荐
场景1:企业知识问答系统
推荐:LoRA SFT → DPO
数据:2000-5000条问答对 + 500条偏好对
预期效果:专业度提升50%+
场景2:代码生成助手
推荐:LoRA SFT → GRPO
数据:5000+代码样本
特别注意:代码任务需要较长的max_seq_length
场景3:数学推理
推荐:LoRA SFT → GRPO
数据:数学题+解题过程
GRPO特别适合:答案有明确对错
场景4:对话风格调整
推荐:LoRA (小r=8) SFT
数据:500-1000条风格样本
不需要RLHF:风格调整用SFT就够了
场景5:极致资源受限(单张消费级GPU)
推荐:QLoRA (r=8-16)
数据:不限
可以在RTX 3090/4090上微调7B模型
第十章:工程实践与最佳实践
10.1 数据质量比数量重要
经验法则:
- 100条高质量数据 > 1000条低质量数据
- 数据质量检查清单:
□ 回答是否准确?
□ 格式是否一致?
□ 是否有拼写/语法错误?
□ 长度是否合适(不过长也不过短)?
□ 是否覆盖了目标领域的关键场景?
10.2 学习率选择
不同微调方法的推荐学习率:
全量微调: 1e-5 ~ 5e-5
LoRA: 1e-4 ~ 3e-4 (比全量微调大一个数量级)
QLoRA: 1e-4 ~ 2e-4
DPO: 5e-7 ~ 5e-6 (比SFT小一个数量级)
GRPO: 1e-6 ~ 1e-5
Prefix: 1e-3 ~ 5e-3 (比LoRA更大)
10.3 防止过拟合
1. 使用验证集监控
2. Early Stopping
3. 增大LoRA的dropout
4. 减小LoRA的r值
5. 增大数据量或数据增强
6. 使用更小的学习率
7. 减少训练轮数
10.4 灾难性遗忘的应对
1. 混合训练:在微调数据中混入一部分通用数据
2. 低学习率:使用较小的学习率
3. LoRA:天然减轻遗忘(原始参数不更新)
4. 弹性权重合并(EWC):保护重要参数
5. 正则化:对与预训练模型的差异施加惩罚
10.5 多阶段微调策略
推荐的多阶段微调流程:
阶段1:SFT(监督微调)
目标:注入领域知识
数据:高质量问答对
方法:LoRA
阶段2:偏好对齐(可选)
目标:提升回答质量
数据:偏好对数据
方法:DPO / ORPO
阶段3:任务特定优化(可选)
目标:针对特定任务优化
数据:任务特定数据
方法:LoRA (小r值) 精调
每个阶段的模型可以作为下一阶段的基座模型
附录:术语表与参考资料
术语表
PEFT: Parameter-Efficient Fine-Tuning,参数高效微调
LoRA: Low-Rank Adaptation,低秩自适应
QLoRA: Quantized LoRA,量化LoRA
DoRA: Weight-Decomposed Low-Rank Adaptation
AdaLoRA: Adaptive LoRA
SFT: Supervised Fine-Tuning,监督微调
RLHF: Reinforcement Learning from Human Feedback
DPO: Direct Preference Optimization
GRPO: Group Relative Policy Optimization
KTO: Kahneman-Tversky Optimization
ORPO: Odds Ratio Preference Optimization
IA³: Infused Adapter by Inhibiting and Amplifying Inner Activations
KL散度: Kullback-Leibler Divergence,衡量两个分布的差异
BF16: Brain Float 16,一种16位浮点数格式
NF4: NormalFloat 4-bit,一种4bit量化格式
ZeRO: Zero Redundancy Optimizer,DeepSpeed的显存优化技术
AMP: Automatic Mixed Precision,自动混合精度
参考论文
1. LoRA: Hu et al., "LoRA: Low-Rank Adaptation of Large Language Models", 2021
2. QLoRA: Dettmers et al., "QLoRA: Efficient Finetuning of Quantized LLMs", 2023
3. DoRA: Liu et al., "DoRA: Weight-Decomposed Low-Rank Adaptation", 2024
4. AdaLoRA: Zhang et al., "AdaLoRA: Adaptive Budget Allocation for PEFT", 2023
5. GaLore: Zhao et al., "GaLore: Memory-Efficient LLM Training by Gradient Low-Rank Projection", 2024
6. Prefix Tuning: Li & Liang, "Prefix-Tuning: Optimizing Continuous Prompts for Generation", 2021
7. P-Tuning v2: Liu et al., "P-Tuning v2: Prompt Tuning Can Be Comparable to Fine-tuning", 2022
8. Adapter: Houlsby et al., "Parameter-Efficient Transfer Learning for NLP", 2019
9. IA³: Liu et al., "Few-Shot Parameter-Efficient Fine-Tuning is Better and Cheaper than In-Context Learning", 2022
10. DPO: Rafailov et al., "Direct Preference Optimization", 2023
11. RLHF: Ouyang et al., "Training language models to follow instructions with human feedback", 2022
12. GRPO: Shao et al., "DeepSeekMath: Pushing the Limits of Mathematical Reasoning", 2024
13. KTO: Ethayarajh et al., "KTO: Model Alignment as Prospect Theoretic Optimization", 2024
14. ORPO: Hong et al., "ORPO: Monolithic Preference Optimization without Reference Model", 2024
15. SimPO: Meng et al., "SimPO: Simple Preference Optimization with a Reference-Free Reward", 2024