01
微调全景图
Fine-Tuning Landscape
全参数微调 (Full Fine-Tuning) 需更新模型所有参数,成本高昂;参数高效微调 (PEFT) 仅训练极少量附加参数,在接近全量微调性能的同时大幅降低显存与存储开销。
方法对比 — 可训练参数占比
Full Fine-Tuning
更新模型
100% 参数。性能上限最高,但需为每个任务保存完整模型副本,显存开销极大。LoRA
低秩矩阵分解
W = W₀ + BA,仅训练 0.1%~1% 参数,可热插拔切换任务。QLoRA
4-bit 量化基座 + FP16 LoRA 适配器,可在单张
48GB GPU 上微调 65B 模型。Adapter
在 Transformer 层间插入瓶颈网络
d→r→d,参数量小但引入额外推理延迟。Prefix Tuning
在每层 attention 的 K/V 前拼接可学习前缀向量,不修改模型权重,参数量极小。
Prompt Tuning
仅在输入层添加可学习 soft prompt,参数量最小,但容量有限、适合大模型。
python peft_overview.py复制
# PEFT — 参数高效微调概述 from peft import LoraConfig, get_peft_model, TaskType # 1. 定义 LoRA 配置 config = LoraConfig( task_type=TaskType.CAUSAL_LM, r=8, # 秩 lora_alpha=16, # 缩放因子 lora_dropout=0.05, target_modules=["q_proj", "v_proj"], # 目标层 ) # 2. 包装基础模型 — 冻结原始权重,仅训练适配器 model = get_peft_model(base_model, config) model.print_trainable_parameters() # trainable params: 1,572,864 || all params: 675,235,840 || trainable%: 0.23%
02
LoRA 低秩适配
LoRA: Low-Rank Adaptation
LoRA 假设权重更新具有低秩结构,将增量矩阵分解为两个小矩阵的乘积 W = W₀ + BA,其中 B∈ℝ^(d×r)、A∈ℝ^(r×d),秩 r 远小于 d,使可训练参数从 d² 降至 2dr。
矩阵分解可视化 — W = W₀ + B × A
内在维度假说
预训练模型具有低 "内在维度",权重的有效更新可以用远低于原始维度的低秩矩阵近似。
秩的选择
通常
r=4~64 即可。秩越大表达力越强但参数增多,收益递减。缩放因子 α
实际更新为
ΔW = (α/r) · BA,α 用于调节适配器强度,可不重新调参即切换 r。python lora_layer.py复制
import torch import torch.nn as nn class LoRALinear(nn.Module): def __init__(self, in_features, out_features, r=8, alpha=16): super().__init__() self.W0 = nn.Linear(in_features, out_features, bias=False) self.W0.weight.requires_grad = False # 冻结原始权重 self.B = nn.Parameter(torch.zeros(out_features, r)) # d_out × r self.A = nn.Parameter(torch.randn(r, in_features) * 0.01) # r × d_in self.scaling = alpha / r def forward(self, x): # W = W₀ + (α/r) · B·A return self.W0(x) + self.scaling * (x @ self.A.T) @ self.B.T
03
QLoRA 量化微调
QLoRA: Quantized LoRA
QLoRA 将基座模型权重量化为 4-bit NF4,同时以 FP16 训练 LoRA 适配器,配合双重量化和分页优化器,在几乎不损失性能的前提下将显存占用降至全量微调的数分之一。
显存对比 — Full FT vs LoRA vs QLoRA
NF4 量化
Normal Float 4-bit:基于正态分布信息论最优的 4-bit 数据类型,量化误差最小。
双重量化
对量化常数本身再次量化,每个参数额外节省
0.37 bit,进一步降低显存。分页优化器
利用 NVIDIA 统一内存,在显存不足时自动将优化器状态分页到 CPU 内存,避免 OOM。
python qlora_setup.py复制
from transformers import AutoModelForCausalLM, BitsAndBytesConfig from peft import LoraConfig, prepare_model_for_kbit_training # 1. 4-bit NF4 量化配置 bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", # NormalFloat4 bnb_4bit_compute_dtype=torch.float16, bnb_4bit_use_double_quant=True, # 双重量化 ) # 2. 加载 4-bit 量化模型 model = AutoModelForCausalLM.from_pretrained( "meta-llama/Llama-2-7B", quantization_config=bnb_config, device_map="auto", ) # 3. 准备 k-bit 训练 + 应用 LoRA model = prepare_model_for_kbit_training(model) model = get_peft_model(model, LoraConfig(r=64, lora_alpha=16, target_modules=["q_proj","k_proj","v_proj","o_proj"]))
04
Adapter 层
Adapter Layers
Adapter 在 Transformer 每层内部插入瓶颈结构:先降维 (d→r)、经非线性激活、再升维 (r→d),仅训练瓶颈参数。推理时可通过移除 Adapter 恢复原模型,实现零延迟部署。
瓶颈结构 — down-project → activation → up-project
Adapter (Houlsby)
在每层 attention 和 FFN 后各插入一个 Adapter,参数量约为模型的
0.5%~8%。AdapterFusion
先为每个任务单独训练 Adapter,再用 Fusion 层学习加权组合,实现多任务知识共享。
Compacter
用低秩参数化矩阵 (PHM) 进一步压缩 Adapter 参数,参数量可降至
0.05%。python adapter_module.py复制
import torch.nn as nn class Adapter(nn.Module): def __init__(self, d_model=768, bottleneck=64): super().__init__() self.down = nn.Linear(d_model, bottleneck) # 降维 d → r self.act = nn.GELU() # 非线性激活 self.up = nn.Linear(bottleneck, d_model) # 升维 r → d self.scale = nn.Parameter(torch.ones(1)) # 残差缩放 def forward(self, h): # 残差连接:h + scale · up(act(down(h))) return h + self.scale * self.up(self.act(self.down(h)))
05
Prefix Tuning
Prefix Tuning
Prefix Tuning 在每层 attention 的 Key 和 Value 前拼接 l 个可学习前缀向量,通过 MLP 重参数化稳定训练。模型权重完全冻结,仅训练前缀参数。
前缀注入 — 可学习前缀向量进入 Attention
Prefix Tuning
在每层注入前缀,用 MLP 重参数化
Pθ = MLP(P) 稳定训练,参数量 ~0.1%。Prompt Tuning
仅在输入嵌入层添加 soft prompt,不涉及深层,参数量最小但适合超大模型。
P-tuning v2
Prefix Tuning 的通用化版本,在所有层添加前缀,对小模型也有效。
python prefix_tuning.py复制
class PrefixTuning(nn.Module): def __init__(self, num_layers=12, num_heads=12, prefix_len=20, d_head=64): super().__init__() self.prefix_len = prefix_len d_kv = num_heads * d_head # 可学习前缀参数(重参数化) self.prefix_keys = nn.Parameter(torch.randn(num_layers, prefix_len, d_kv)) self.prefix_vals = nn.Parameter(torch.randn(num_layers, prefix_len, d_kv)) def forward(self, layer_idx, K, V): # 在第 layer_idx 层的 K/V 前拼接前缀 pk = self.prefix_keys[layer_idx] # [prefix_len, d_kv] pv = self.prefix_vals[layer_idx] K = torch.cat([pk.unsqueeze(0), K], dim=1) # 拼接 V = torch.cat([pv.unsqueeze(0), V], dim=1) return K, V
06
指令微调 SFT
Supervised Fine-Tuning
指令微调使用 "指令-回复" 对训练模型遵循人类指令。关键技巧是损失掩码:仅对回复部分计算损失,避免模型学习复述指令,提升指令遵循能力。
损失掩码 — 仅在回复 token 上计算损失
Alpaca 格式
instruction / input / output 三字段,Stanford Alpaca 风格。ChatML 格式
<|im_start|>user/assistant<|im_end|> 角色标记,OpenAI 兼容。Vicuna 格式
USER: / ASSISTANT: 前缀,多轮对话分隔。python sft_training.py复制
def sft_loss(logits, labels, attention_mask): # 损失掩码:仅在回复 token 上计算损失 # labels 中 instruction 部分设为 -100 (忽略) shift_logits = logits[..., :-1, :].contiguous() shift_labels = labels[..., 1:].contiguous() loss_fct = nn.CrossEntropyLoss(ignore_index=-100) loss = loss_fct( shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1), ) return loss # 构造带掩码的标签 def build_labels(input_ids, response_start): labels = input_ids.clone() labels[:, :response_start] = -100 # 掩码指令部分 return labels
07
分布式训练
Distributed Training
大模型训练需要分布式策略。数据并行 (DP) 复制完整模型到每张 GPU;张量并行 (TP) 切分矩阵到不同 GPU;流水线并行 (PP) 按层切分;FSDP/ZeRO 则切分优化器状态、梯度与参数。
并行策略对比 — GPU 显存与通信
DDP
数据并行,每 GPU 持有完整模型副本,反向传播后 AllReduce 同步梯度。简单高效。
FSDP
Fully Sharded Data Parallel,切分参数/梯度/优化器状态,按需 AllGather 还原。
ZeRO Stage 1/2/3
Stage1 切优化器状态,Stage2 加梯度,Stage3 再加参数,逐级降显存、增通信。
Tensor Parallel
将权重矩阵按列/行切分到多 GPU,层内通信量大但无需存储完整副本。
python ddp_training.py复制
import torch.distributed as dist from torch.nn.parallel import DistributedDataParallel as DDP def setup_ddp(): dist.init_process_group(backend="nccl") torch.cuda.set_device(dist.get_rank()) def train_step(model, optimizer, batch): # model 是 DDP 包装后的模型 inputs, labels = batch outputs = model(inputs) # 前向(含自动同步准备) loss = loss_fn(outputs, labels) optimizer.zero_grad() loss.backward() # 反向 → AllReduce 同步梯度 optimizer.step() # 各 GPU 独立更新(梯度相同) return loss.item() # FSDP 等价 — 只需替换包装器 # from torch.distributed.fsdp import FullyShardedDataParallel as FSDP # model = FSDP(model, sharding_strategy=ShardingStrategy.FULL_SHARD)
08
灾难性遗忘
Catastrophic Forgetting
微调新任务时模型会遗忘旧任务知识。通过经验回放、弹性权重巩固 (EWC)、L2 正则化等方法可缓解遗忘;LoRA 因冻结基座权重,天然具有抗遗忘优势。
知识保留曲线 — 旧任务准确率随微调步数衰减
经验回放 Replay
在训练集中混入旧任务数据,简单有效但需保存旧数据,可能违反隐私。
EWC 弹性权重巩固
基于 Fisher 信息矩阵约束重要参数变化:
L = L_task + λ Σ F_i(θ_i-θ*_i)²。L2 正则化
向原始权重施加 L2 惩罚,简单但对所有参数一视同仁,效果弱于 EWC。
LoRA 抗遗忘
基座权重完全冻结,新任务仅训练适配器,旧任务知识不会被动摇。
python ewc_penalty.py复制
class EWC: def __init__(self, model, fisher=None, params_star=None): self.fisher = fisher # Fisher 信息矩阵 self.params_star = params_star # 旧任务最优参数 def penalty(self, model): # L_ewc = Σ F_i · (θ_i - θ*_i)² loss = 0 for n, p in model.named_parameters(): if n in self.fisher: loss += (self.fisher[n] * (p - self.params_star[n]) ** 2).sum() return loss # 训练时:总损失 = 任务损失 + λ · EWC惩罚 # total_loss = task_loss + lambda_ewc * ewc.penalty(model)