知识体系篇-数据标注与处理(06)模型训练与调优:模型微调(Fine-tuning)技术详解
·
模型微调(Fine-tuning)技术详解
专栏:人工智能训练师(三级)备考全攻略
模块:卷三·知识体系 — 第三部分·模型训练与优化
难度:⭐⭐⭐⭐☆
考试权重:高频(大模型时代必考,选择+简答+综合)
一、为什么需要微调?
从零训练大模型代价极高,微调让我们站在巨人肩膀上:
预训练 vs 微调的资源对比
┌─────────────────────────────────────────────────────────┐
│ GPT-3 预训练:3500亿 tokens,355 GPU年,~ $460万美元 │
│ GPT-3 微调: 数千条数据,单卡几小时,~ 几十美元 │
└─────────────────────────────────────────────────────────┘
核心思想:
预训练模型学到的是"通用知识"(Universal Features)
微调让模型将通用知识迁移到"特定任务"(Task-Specific)
二、迁移学习与微调的关系
迁移学习(Transfer Learning)
│
├── 特征提取(Feature Extraction)
│ 冻结预训练层,只训练新增的头部层
│ 适合:数据极少,目标任务与预训练接近
│
└── 微调(Fine-tuning)
解冻部分/全部预训练层,用新数据更新权重
适合:有一定数据量,目标任务有差异
│
├── 全量微调(Full Fine-tuning)
│ 解冻所有层,全部重新训练
│ 优点:效果最好
│ 缺点:计算量大,易遗忘原始知识
│
├── 部分微调(Partial Fine-tuning)
│ 只解冻顶层N层
│ 平衡效果与计算开销
│
└── 参数高效微调(PEFT)
只训练极少量参数
LoRA / Adapter / Prefix Tuning
迁移学习策略选择矩阵
数据量小 数据量大
─────────────────────────────
目标任务 特征提取 全量微调
相似度高 (冻结大部分层) (较小学习率)
目标任务 谨慎:先特征提取 全量微调
相似度低 再逐步解冻 (较大学习率)
三、经典微调方法详解
3.1 差异学习率(Discriminative Learning Rates)
直觉:越靠近输入的层学到越通用的特征(边缘/颜色),
越靠近输出的层学到越任务相关的特征(语义/类别)。
因此底层应该用更小的学习率(少改动),顶层用更大的学习率。
Input → Layer1 → Layer2 → Layer3 → Output
lr/4 lr/2 lr lr×2
import torch
import torch.nn as nn
import torchvision.models as models
# =========================================
# 全量微调 + 差异学习率(ResNet50示例)
# =========================================
def build_finetuning_model(num_classes=10, pretrained=True):
model = models.resnet50(pretrained=pretrained)
# 替换最后的全连接层(适配新任务)
in_features = model.fc.in_features
model.fc = nn.Sequential(
nn.Dropout(0.3),
nn.Linear(in_features, 512),
nn.ReLU(),
nn.Linear(512, num_classes)
)
return model
model = build_finetuning_model(num_classes=5)
# 差异学习率:不同层设置不同lr
optimizer = torch.optim.Adam([
{'params': model.layer1.parameters(), 'lr': 1e-5}, # 底层,最小lr
{'params': model.layer2.parameters(), 'lr': 2e-5},
{'params': model.layer3.parameters(), 'lr': 5e-5},
{'params': model.layer4.parameters(), 'lr': 1e-4}, # 高层
{'params': model.fc.parameters(), 'lr': 1e-3}, # 新增头部,最大lr
], lr=1e-4)
print("差异学习率设置完成")
3.2 逐步解冻(Gradual Unfreezing)
# =========================================
# 逐步解冻训练策略(ULMFiT风格)
# =========================================
def freeze_layers(model, freeze_until_layer=None):
"""冻结指定层及以前的所有层"""
for name, param in model.named_parameters():
param.requires_grad = False
if freeze_until_layer is not None:
# 解冻指定层之后的参数
unfreeze = False
for name, param in model.named_parameters():
if freeze_until_layer in name:
unfreeze = True
if unfreeze:
param.requires_grad = True
def count_trainable_params(model):
return sum(p.numel() for p in model.parameters() if p.requires_grad)
# 第1阶段:只训练新增的头部层
freeze_layers(model, freeze_until_layer=None) # 冻结全部
for param in model.fc.parameters():
param.requires_grad = True
print(f"阶段1 - 可训练参数: {count_trainable_params(model):,}")
# 训练3~5 epochs...
# 第2阶段:解冻最后一个ResNet block
freeze_layers(model, freeze_until_layer='layer4')
print(f"阶段2 - 可训练参数: {count_trainable_params(model):,}")
# 训练3~5 epochs(学习率÷10)...
# 第3阶段:全量解冻
for param in model.parameters():
param.requires_grad = True
print(f"阶段3 - 可训练参数: {count_trainable_params(model):,}")
四、参数高效微调(PEFT)
大模型时代,全量微调成本过高,PEFT 方法应运而生。
4.1 PEFT 方法全景
| 方法 | 原理 | 可训练参数比例 | 代表作 |
|---|---|---|---|
| LoRA | 低秩矩阵分解 | 0.1%~1% | LoRA论文 2021 |
| Adapter | 插入小型适配模块 | 1%~5% | Adapter BERT 2019 |
| Prefix Tuning | 在序列前加可学习前缀 | <1% | Prefix-Tuning 2021 |
| Prompt Tuning | 只训练soft prompt token | <0.1% | Lester et al. 2021 |
| IA³ | 缩放激活值 | <0.1% | IA³ 2022 |
4.2 LoRA 原理深度解析
LoRA(Low-Rank Adaptation)核心思想:
原始权重矩阵 W ∈ R^(d×k),参数量 = d×k
LoRA 假设权重更新矩阵 ΔW 是低秩的:
ΔW = B × A
其中 B ∈ R^(d×r), A ∈ R^(r×k), r << min(d, k)
前向传播:
h = W₀x + ΔWx = W₀x + BAx
(冻结) (训练)
参数节省:
原始:d×k
LoRA:d×r + r×k = r(d+k)
当 r=8, d=k=4096: 原始2100万 → LoRA 65536 (0.3%)
┌─────────────────────────────────────────────┐
│ 输入 x │
│ │ │
│ ┌──┴──┐ ┌─────┐ ┌─────┐ │
│ │ W₀ │ + │ A │ → │ B │ │
│ │(冻结)│ │r×k │ │d×r │ │
│ └──┬──┘ └──┬──┘ └──┬──┘ │
│ │ │ │ │
│ └─────┬─────┘ │ │
│ │ ΔW = B×A │ │
│ └───────────────┘ │
│ │ │
│ 输出 h │
└─────────────────────────────────────────────┘
# =========================================
# LoRA 实现(简化版,理解原理)
# =========================================
import torch
import torch.nn as nn
import math
class LoRALinear(nn.Module):
"""
用LoRA包装线性层
原始权重W₀冻结,只训练低秩矩阵A和B
"""
def __init__(self, original_linear: nn.Linear, r: int = 8, alpha: float = 16):
super().__init__()
d_in = original_linear.in_features
d_out = original_linear.out_features
# 冻结原始权重
self.weight = original_linear.weight
self.bias = original_linear.bias
self.weight.requires_grad = False
if self.bias is not None:
self.bias.requires_grad = False
# 低秩矩阵 A 和 B
self.lora_A = nn.Parameter(torch.randn(r, d_in) / math.sqrt(r))
self.lora_B = nn.Parameter(torch.zeros(d_out, r)) # B初始化为0
self.r = r
self.scaling = alpha / r # 缩放因子
def forward(self, x: torch.Tensor) -> torch.Tensor:
# 原始路径(冻结)
original_out = nn.functional.linear(x, self.weight, self.bias)
# LoRA路径(可训练)
lora_out = (x @ self.lora_A.T) @ self.lora_B.T * self.scaling
return original_out + lora_out
def apply_lora_to_model(model, r=8, alpha=16, target_modules=('query', 'value')):
"""将模型中指定模块替换为LoRA版本"""
for name, module in model.named_modules():
if isinstance(module, nn.Linear):
if any(t in name for t in target_modules):
# 替换为LoRA层
parent_name = '.'.join(name.split('.')[:-1])
child_name = name.split('.')[-1]
parent = model.get_submodule(parent_name)
lora_module = LoRALinear(module, r=r, alpha=alpha)
setattr(parent, child_name, lora_module)
print(f" LoRA applied to: {name}")
return model
# 统计可训练参数
def print_trainable_parameters(model):
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
total = sum(p.numel() for p in model.parameters())
pct = 100 * trainable / total
print(f"可训练参数: {trainable:,} / {total:,} ({pct:.2f}%)")
4.3 使用 HuggingFace PEFT 库(生产级)
# =========================================
# 使用 peft 库进行 LoRA 微调(生产推荐)
# pip install peft transformers
# =========================================
from transformers import AutoModelForSequenceClassification, AutoTokenizer
from peft import LoraConfig, get_peft_model, TaskType
# 加载预训练模型
model_name = "bert-base-chinese"
model = AutoModelForSequenceClassification.from_pretrained(
model_name, num_labels=2
)
tokenizer = AutoTokenizer.from_pretrained(model_name)
# 配置 LoRA
lora_config = LoraConfig(
task_type=TaskType.SEQ_CLS, # 序列分类任务
r=8, # 低秩维度
lora_alpha=32, # 缩放因子
target_modules=["query", "value"], # 应用到Q和V矩阵
lora_dropout=0.1,
bias="none"
)
# 应用 LoRA
peft_model = get_peft_model(model, lora_config)
peft_model.print_trainable_parameters()
# 输出类似: trainable params: 296,448 || all params: 102,268,416 (0.29%)
# 保存和加载 LoRA 权重(只需保存少量参数!)
peft_model.save_pretrained("./lora-bert-chinese") # 只保存LoRA权重
# 加载时:
# from peft import PeftModel
# base_model = AutoModelForSequenceClassification.from_pretrained(model_name)
# peft_model = PeftModel.from_pretrained(base_model, "./lora-bert-chinese")
五、微调的常见问题与解决
5.1 灾难性遗忘(Catastrophic Forgetting)
问题:微调新任务时,模型忘记了预训练时学到的知识。
原始知识 微调后
↓ ↓
BERT BERT-Chinese-NER
良好英文理解 → 英文能力下降!
解决方案:
1. 较小的学习率(保护预训练权重)
2. 逐步解冻(Gradual Unfreezing)
3. 弹性权重巩固 EWC(正则化重要参数)
4. 混合预训练数据(在微调数据中混入部分原始数据)
5.2 微调最佳实践检查清单
| 步骤 | 注意事项 |
|---|---|
| 数据准备 | 与预训练分布一致的格式;标注质量 > 数量 |
| 学习率设置 | 比预训练小1~2个数量级(典型:1e-5 ~ 5e-5) |
| 批量大小 | 大batch更稳定,但显存限制时用梯度累积 |
| 训练轮次 | 几个到十几个epoch,防止过拟合 |
| 验证策略 | 每个epoch验证,Early Stopping |
| 权重保存 | 保存验证集最优的checkpoint |
六、考试重点总结
6.1 核心概念辨析
| 概念 | 一句话说清 |
|---|---|
| 微调 vs 预训练 | 预训练学通用知识,微调适配特定任务 |
| 全量微调 vs PEFT | 全量效果好但代价高,PEFT以极少参数接近全量效果 |
| LoRA 核心 | 将权重更新矩阵分解为两个低秩矩阵的乘积 |
| 灾难性遗忘 | 微调新任务导致模型忘记旧知识 |
| 差异学习率 | 底层小学习率保护通用特征,顶层大学习率快速适配 |
6.2 高频选择题
Q: LoRA 微调中,训练时哪些参数被更新?
A: 只有低秩矩阵 A 和 B(原始权重W₀冻结)✅
Q: 以下哪个不是 PEFT 方法?
A: 全量微调(Full Fine-tuning)✅
Q: Prefix Tuning 的训练对象是?
A: 在输入序列前添加的可学习"前缀向量" ✅
Q: 微调时学习率应该比从头训练大还是小?
A: 小(通常小1~2个数量级,防止破坏预训练权重)✅
Q: 逐步解冻(Gradual Unfreezing)的顺序是?
A: 从顶层(靠近输出)向底层(靠近输入)逐步解冻 ✅
七、思维导图
📌 备考贴士:大模型微调是2024~2025年考试热点,LoRA 几乎是必考项。记住"低秩分解+只训练A/B矩阵+原始权重冻结"三个核心要点,选择题必拿分。差异学习率的"底层小、顶层大"也是常考简答点。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)