模型微调(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: 从顶层(靠近输出)向底层(靠近输入)逐步解冻 ✅

七、思维导图

模型微调Fine-tuning

核心动机

预训练代价高

迁移通用知识

适配特定任务

方法分类

特征提取

冻结所有预训练层

只训练头部

全量微调

解冻全部层

效果最好

PEFT参数高效

LoRA低秩分解

Adapter适配器

Prefix Tuning

Prompt Tuning

关键技术

差异学习率

底层小lr

顶层大lr

逐步解冻

顶层→底层

分阶段训练

灾难性遗忘防御

小学习率

混合数据

LoRA详解

权重更新=B×A

r=8典型秩

0.1%~1%参数量

peft库实现

实践工具

HuggingFace PEFT

transformers

LoRA/QLoRA


📌 备考贴士:大模型微调是2024~2025年考试热点,LoRA 几乎是必考项。记住"低秩分解+只训练A/B矩阵+原始权重冻结"三个核心要点,选择题必拿分。差异学习率的"底层小、顶层大"也是常考简答点。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐