首页/知识库/迁移学习(二):预训练 + 微调范式

迁移学习(二):预训练 + 微调范式

✍️ AI Master📅 创建 2026-04-12📖 16 min 阅读
💡

文章摘要

站在巨人的肩膀上,掌握迁移学习的策略与最佳实践

1为什么需要迁移学习

深度学习模型通常需要海量标注数据才能达到理想性能,但现实场景中高质量标注数据往往稀缺且昂贵。迁移学习的核心思想是将从源领域学到的知识迁移到目标领域,从而大幅降低对目标数据量的需求。以 ImageNet 预训练模型为例,一个在 1400 万张图片上训练过的 ResNet 已经学会了丰富的视觉特征表示,这些通用特征(边缘、纹理、形状)可以直接复用到医学影像、卫星图像等完全不同的领域。迁移学习不仅解决了数据稀缺问题,还能显著缩短训练时间——从零训练一个大型模型可能需要数天甚至数周,而基于预训练权重微调只需数小时。此外,迁移学习已被证明在泛化能力上优于从头训练,因为预训练模型已经在多样化数据上学习到了更鲁棒的特征表示。当前主流的大语言模型(LLM)同样遵循这一范式:先在海量语料上预训练,再通过指令微调适配下游任务。

python
# 对比:从零训练 vs 迁移学习的效率差异
import torch
import torchvision.models as models

# 从零训练:需要大量数据和时间
# 预训练模型:加载即具备丰富特征表示
pretrained_model = models.resnet50(
    weights=models.ResNet50_Weights.IMAGENET1K_V2
)
print(f"预训练权重加载完成")
print(f"参数量: {sum(p.numel() for p in pretrained_model.parameters()):,}")
python
# 迁移学习的核心优势量化
import time
from torchvision import datasets, transforms

# 场景模拟:小样本目标域训练
train_time_scratch = "约 72 小时 (从零训练 ResNet-50)"
train_time_transfer = "约 2 小时 (微调预训练模型)"

print(f"从零训练: {train_time_scratch}")
print(f"迁移学习: {train_time_transfer}")
print(f"时间节省: {((72 - 2) / 72 * 100):.0f}%")
训练方式所需数据量训练时间准确率

从零训练

100万+ 标注

~72小时

78.5%

迁移学习

1万 标注

~2小时

85.2%

少样本微调

500 标注

~30分钟

82.1%

优先选择与目标领域数据分布接近的预训练模型,例如医学图像使用 MedPretrain 权重而非 ImageNet 权重

源领域和目标领域差异过大时,负迁移会导致性能下降而非提升

2迁移学习三大策略

迁移学习并非一种固定方法,而是一套策略体系。根据目标数据量和任务相似度,有三种主流策略可供选择。第一种是特征提取(Feature Extraction),即冻结预训练模型的绝大部分层,仅替换并训练最后的分类头。这种策略适合目标数据极少(几百到几千样本)且任务与源任务相似的场景。第二种是微调(Fine-tuning),在特征提取的基础上,解冻部分或全部网络层,使用较低的学习率对整个模型进行端到端训练。这是最常用的策略,适用于中等规模数据和中等任务差异。第三种是线性探测(Linear Probing),仅训练一个线性分类器而不改动预训练特征提取器,适合快速验证预训练特征的有效性。在实际工程中,通常会先做线性探测评估特征质量,再决定是否进入微调阶段。PyTorch 的参数requires_grad控制使得这几种策略可以灵活切换。

python
# 策略一:特征提取(冻结全部层)
import torch.nn as nn
import torchvision.models as models

model = models.resnet50(weights="IMAGENET1K_V2")

# 冻结所有预训练参数
for param in model.parameters():
    param.requires_grad = False

# 替换分类头
num_features = model.fc.in_features
model.fc = nn.Sequential(
    nn.Linear(num_features, 256),
    nn.ReLU(),
    nn.Dropout(0.5),
    nn.Linear(256, 10)  # 10类目标
)

# 只有分类头的参数可训练
trainable = [p.numel() for p in model.parameters() if p.requires_grad]
print(f"可训练参数: {sum(trainable):,}")
python
# 策略二:微调(部分解冻 + 低学习率)
def setup_fine_tune(model, unfreeze_from: str = "layer3"):
    """从指定层开始解冻"""
    freeze = True
    for name, param in model.named_parameters():
        if name.startswith(unfreeze_from):
            freeze = False
        param.requires_grad = not freeze
    return model

model = setup_fine_tune(model, unfreeze_from="layer3")

# 不同层使用不同学习率
optimizer = torch.optim.Adam([
    {"params": model.layer3.parameters(), "lr": 1e-5},
    {"params": model.layer4.parameters(), "lr": 1e-4},
    {"params": model.fc.parameters(), "lr": 1e-3},
])
策略解冻层数适用数据量训练速度

线性探测

仅分类头

< 500

极快

特征提取

仅分类头

500 - 5000

全量微调

全部层

5000+

较慢

先用线性探测验证预训练特征有效性,再做特征提取,最后考虑全量微调,这是一个稳妥的递进流程

全量微调在小数据集上极易过拟合,务必配合早停和数据增强

3冻结策略与学习率调度

迁移学习中,冻结策略与学习率调度的配合是决定成败的关键细节。简单冻结全部参数然后训练分类头只是起点,更精细的做法是逐层解冻(Progressive Unfreezing):先训练分类头若干轮,然后逐步解冻深层网络,最后解冻浅层网络。这种渐进式策略避免了早期大梯度对预训练权重的破坏。学习率方面,推荐使用差异化学习率(Discriminative Learning Rate),即靠近输出的深层使用较高学习率,靠近输入的浅层使用更低学习率,因为浅层学到的是通用特征(边缘检测等),改动应更谨慎。配合余弦退火或 OneCycleLR 等学习率调度策略,可以在训练初期快速收敛,后期精细调优。PyTorch 的 torch.optim.lr_scheduler 提供了丰富的调度选项,合理组合这些工具可以让迁移训练效果提升 3-5 个百分点。

python
# 渐进式解冻 + 差异化学习率
import torch.nn as nn

def progressive_unfreeze(model, epochs_per_stage=3):
    """分阶段解冻网络层"""
    stage_groups = [
        model.fc,              # 阶段1: 仅分类头
        model.layer4,          # 阶段2: 解冻layer4
        model.layer3,          # 阶段3: 解冻layer3
        [model.layer1, model.layer2]  # 阶段4: 解冻浅层
    ]
    return stage_groups

# 每个阶段使用递减学习率
lr_stages = [1e-3, 5e-4, 1e-4, 5e-5]
print(f"共 {len(lr_stages)} 个训练阶段")
for i, lr in enumerate(lr_stages):
    print(f"阶段 {i+1}: lr={lr:.0e}")
python
# 学习率调度组合拳
from torch.optim.lr_scheduler import (
    OneCycleLR, CosineAnnealingWarmRestarts
)

# 方案A: OneCycleLR(适合快速微调)
scheduler_one_cycle = OneCycleLR(
    optimizer,
    max_lr=1e-3,
    total_steps=1000,
    pct_start=0.3,       # 30%时间预热
    div_factor=25,       # 初始lr = max_lr / 25
    final_div_factor=1e4 # 最终lr = max_lr / 1e4
)

# 方案B: 余弦退火 + 热重启(适合精细微调)
scheduler_cosine = CosineAnnealingWarmRestarts(
    optimizer, T_0=10, T_mult=2, eta_min=1e-6
)
调度策略适用场景优势缺点

OneCycleLR

快速微调

收敛快,泛化好

需预设总步数

余弦退火

精细微调

平滑收敛

可能陷入局部最优

Warm + Cosine

全量微调

兼顾稳定与精度

超参数较多

ReduceLROnPlateau

自适应

自动调整

需监控指标

使用 torch.optim.lr_scheduler.ReduceLROnPlateau 在验证指标不再提升时自动降低学习率,这是最安全的默认选择

解冻浅层网络后学习率必须极低(1e-5 以下),否则预训练的通用特征会被快速破坏

4数据增强与领域适配

迁移学习的效果很大程度上取决于源域与目标域之间的分布差异。当差异较大时,仅靠调整网络结构和训练策略是不够的,还需要通过数据增强和领域适配技术来桥接这一鸿沟。数据增强是成本最低且最有效的适配手段:通过随机裁剪、色彩抖动、MixUp 等变换扩充训练集的多样性,使模型对领域偏移更加鲁棒。对于图像任务,Albumentations 库提供了超过 70 种增强变换,可以组合出极其丰富的增强流水线。在更复杂的场景下,可以使用领域自适应(Domain Adaptation)方法,如对抗训练(DANN)或风格迁移(Style Transfer),显式地减少源域和目标域特征分布的差异。此外,测试时增强(Test-Time Augmentation)和伪标签(Pseudo-labeling)也能在无目标标签的情况下进一步提升性能。

python
# 领域适配型数据增强流水线
import albumentations as A
from albumentations.pytorch import ToTensorV2

def get_train_augmentations(img_size=224):
    """针对领域偏移的鲁棒增强策略"""
    return A.Compose([
        A.RandomResizedCrop(img_size, img_size,
                           scale=(0.6, 1.0)),
        A.HorizontalFlip(p=0.5),
        A.ColorJitter(
            brightness=0.3, contrast=0.3,
            saturation=0.3, hue=0.1, p=0.8
        ),
        A.GaussianBlur(blur_limit=7, p=0.3),
        A.Normalize(mean=[0.485, 0.456, 0.406],
                    std=[0.229, 0.224, 0.225]),
        ToTensorV2()
    ])
python
# MixUp 数据增强(缓解领域偏移)
import torch.nn.functional as F

def mixup_data(x, y, alpha=0.2):
    """MixUp: 线性插值样本对"""
    if alpha > 0:
        lam = torch.distributions.Beta(
            alpha, alpha
        ).sample().item()
    else:
        lam = 1.0

    batch_size = x.size(0)
    index = torch.randperm(batch_size).to(x.device)
    
    mixed_x = lam * x + (1 - lam) * x[index]
    y_a, y_b = y, y[index]
    return mixed_x, y_a, y_b, lam

# 损失函数调整
def mixup_criterion(pred, y_a, y_b, lam):
    return lam * F.cross_entropy(pred, y_a) + \
           (1 - lam) * F.cross_entropy(pred, y_b)
增强技术领域差异大时数据量少时计算开销

基础变换

有效

必要

MixUp/CutMix

很有效

推荐

极低

色彩扰动

关键

可选

风格迁移

最有效

可选

在目标域上运行 t-SNE 可视化源域和目标域特征分布,直观判断领域差异大小,再选择对应增强强度

过度增强会降低训练效率,导致模型学到的都是扭曲数据而非真实模式,监控训练损失曲线至关重要

5预训练模型选择

选择正确的预训练模型是迁移学习成功的前提。当前可用的预训练模型生态极其丰富,从经典的 ResNet、VGG 到现代的 Vision Transformer(ViT)、Swin Transformer,再到多模态的 CLIP 模型。选择时需要综合考虑多个维度:首先是任务类型,分类任务首选 ResNet/EfficientNet,检测任务选 FPN/DETR 架构,分割任务选 DeepLab/SAM 架构。其次是模型容量,小数据集用较小模型(ResNet-18/34)避免过拟合,大数据集用较大模型(ResNet-101/ViT-L)获取更好性能。第三是计算资源约束,移动端部署必须考虑模型大小和推理延迟。第四是预训练数据的质量和相关性,通用 ImageNet 预训练是安全基线,但如果有领域特定的预训练模型(如 BioMedCLIP 用于医学影像),优先使用领域模型。最后是许可证和商业使用限制,部分预训练权重有非商用条款。

python
# 多模型基准对比
import torchvision.models as models
from timm import list_models, create_model

def compare_models():
    """对比候选预训练模型"""
    candidates = {
        "resnet50": models.resnet50(
            weights="IMAGENET1K_V2"),
        "efficientnet_b0": models.efficientnet_b0(
            weights="IMAGENET1K_V1"),
        "convnext_tiny": models.convnext_tiny(
            weights="IMAGENET1K_V1"),
    }
    
    results = []
    for name, model in candidates.items():
        params = sum(p.numel() for p in model.parameters())
        results.append((name, params))
    
    for name, params in sorted(results, key=lambda x: x[1]):
        print(f"{name:20s} {params:>10,} params")

compare_models()
python
# TIMM 库:一站式预训练模型仓库
import timm

# 列出所有可用预训练模型
all_models = timm.list_models(pretrained=True)
print(f"可用预训练模型: {len(all_models)} 个")

# 加载特定预训练权重
model = timm.create_model(
    "convnext_large",
    pretrained=True,
    num_classes=10,        # 自定义类别数
    drop_path_rate=0.2     # 随机深度正则化
)

# 查看模型默认配置
data_cfg = timm.data.resolve_model_data_config(model)
print(f"输入尺寸: {data_cfg['input_size']}")
print(f"归一化均值: {data_cfg['mean']}")
print(f"归一化标准差: {data_cfg['std']}")
模型参数量Top-1 精度适用场景

ResNet-50

25.6M

80.8%

通用分类基线

EfficientNet-B3

12.2M

81.6%

移动端部署

ConvNeXt-Large

198M

87.8%

高精度场景

ViT-Base

86.6M

84.2%

大数据 + 强算力

CLIP-ViT

151M

N/A*

多模态/零样本

使用 TIMM 库的 timm.list_models(pretrained=True) 快速浏览可用模型,再用 timm.create_model 一行加载,比 torchvision 更灵活

ViT 等 Transformer 架构在数据量少于 1 万时通常不如 CNN,不要盲目追求最新架构

6小样本学习技巧

当目标域标注数据极少(几十到几百样本)时,标准微调策略仍然容易过拟合。小样本学习(Few-shot Learning)需要一整套针对性的技巧。首先是更激进的冻结策略,只解冻最后 1-2 层,保持预训练特征的完整性。其次使用更强的正则化:增大权重衰减系数、提高 Dropout 比例、添加标签平滑。第三是元学习(Meta-Learning)思路,如原型网络(Prototypical Networks)和支持集-查询集(Support-Query)范式,通过模拟少样本场景让模型学会如何快速适应新类别。第四是数据层面的策略,包括更强的数据增强、半监督学习(利用未标注数据)、以及自训练(Self-training)用模型自身预测生成伪标签。此外,提示学习(Prompt Learning)在视觉领域也开始崭露头角,通过优化可学习的提示向量而非修改模型权重,实现更高效的小样本适配。

python
# 原型网络(Prototypical Network)
import torch
import torch.nn as nn
import torch.nn.functional as F

class PrototypicalNetwork(nn.Module):
    def __init__(self, encoder):
        super().__init__()
        self.encoder = encoder  # 冻结的预训练特征提取器
    
    def forward(self, support_x, support_y, query_x):
        # 提取支持集特征并计算类原型
        support_feat = self.encoder(support_x)
        prototypes = self.compute_prototypes(
            support_feat, support_y
        )
        # 计算查询样本到原型的距离
        query_feat = self.encoder(query_x)
        dists = self.euclidean_dist(query_feat, prototypes)
        return -dists  # 负距离作为 logits
    
    def compute_prototypes(self, feat, labels):
        classes = torch.unique(labels)
        prototypes = torch.stack([
            feat[labels == c].mean(dim=0) for c in classes
        ])
        return prototypes
    
    def euclidean_dist(self, x, y):
        return torch.cdist(x, y, p=2)
python
# 小样本微调的超参数配置
few_shot_config = {
    # 冻结策略
    "unfreeze_layers": 1,        # 仅解冻最后1层
    
    # 激进正则化
    "weight_decay": 1e-3,        # 比常规大10倍
    "dropout_rate": 0.6,         # 高Dropout
    "label_smoothing": 0.15,     # 标签平滑
    
    # 学习率
    "lr": 5e-5,                  # 极低学习率
    "warmup_epochs": 5,          # 较长预热
    
    # 训练轮次
    "max_epochs": 50,            # 多轮次精细训练
    "early_stop_patience": 10,   # 耐心早停
}

for k, v in few_shot_config.items():
    print(f"{k}: {v}")
技巧数据量 <100数据量 100-1000实施难度

仅解冻分类头

必须

可选

强正则化

必须

推荐

原型网络

推荐

可选

半监督学习

强烈推荐

推荐

提示学习

可选

可选

5-way 1-shot 场景下,原型网络配合强数据增强是最稳定的基线方案,先跑通再考虑更复杂方法

小样本场景下验证集划分极其重要,确保验证集和测试集来自相同分布,否则早停策略会失效

7PyTorch 实战:ImageNet 预训练到自定义数据集微调

理论再完善也需要落地到代码。本节以完整的 PyTorch 实战流程为例,展示从加载 ImageNet 预训练的 ResNet-50 到在自定义数据集上微调的全部步骤。整个流程包括:数据准备(使用 ImageFolder 加载自定义目录结构)、模型构建(加载预训练权重 + 替换分类头)、训练循环(带学习率调度和早停机制)、以及模型评估。关键细节在于使用参数分组实现差异化学习率,配合 ReduceLROnPlateau 调度器在验证损失不下降时自动降低学习率,并通过早停防止过拟合。训练完成后保存最佳权重和训练日志,方便后续分析和部署。这个完整流程可以直接套用到图像分类任务,只需替换数据路径和类别数即可。对于非分类任务(如检测、分割),核心思路相同,只需替换最后的任务头。

python
# 完整迁移学习训练流程
import torch
import torch.nn as nn
from torchvision import datasets, transforms, models
from torch.utils.data import DataLoader
import os

# 1. 数据加载
train_transform = transforms.Compose([
    transforms.RandomResizedCrop(224),
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
    transforms.Normalize(
        [0.485, 0.456, 0.406],
        [0.229, 0.224, 0.225]
    )
])

train_dataset = datasets.ImageFolder(
    "data/train", transform=train_transform
)
train_loader = DataLoader(
    train_dataset, batch_size=32, shuffle=True, num_workers=4
)

# 2. 模型构建
model = models.resnet50(weights="IMAGENET1K_V2")
num_ftrs = model.fc.in_features
model.fc = nn.Linear(num_ftrs, 10)  # 自定义10类

# 3. 差异化学习率
optimizer = torch.optim.AdamW([
    {"params": model.conv1.parameters(), "lr": 1e-5},
    {"params": model.layer1.parameters(), "lr": 1e-5},
    {"params": model.layer2.parameters(), "lr": 3e-5},
    {"params": model.layer3.parameters(), "lr": 1e-4},
    {"params": model.layer4.parameters(), "lr": 3e-4},
    {"params": model.fc.parameters(), "lr": 1e-3},
], weight_decay=1e-4)
python
# 4. 训练循环 + 早停机制
def train_model(model, train_loader, val_loader, epochs=30):
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    model = model.to(device)
    
    criterion = nn.CrossEntropyLoss(label_smoothing=0.1)
    scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
        optimizer, mode="min", factor=0.5, patience=3
    )
    
    best_acc = 0.0
    patience_counter = 0
    max_patience = 7
    
    for epoch in range(epochs):
        # 训练阶段
        model.train()
        train_loss = 0.0
        for inputs, labels in train_loader:
            inputs, labels = inputs.to(device), labels.to(device)
            optimizer.zero_grad()
            outputs = model(inputs)
            loss = criterion(outputs, labels)
            loss.backward()
            optimizer.step()
            train_loss += loss.item()
        
        # 验证阶段
        model.eval()
        correct = 0
        total = 0
        with torch.no_grad():
            for inputs, labels in val_loader:
                inputs, labels = inputs.to(device), labels.to(device)
                outputs = model(inputs)
                _, predicted = outputs.max(1)
                total += labels.size(0)
                correct += predicted.eq(labels).sum().item()
        
        val_acc = correct / total
        scheduler.step(val_acc)
        
        # 保存最佳模型
        if val_acc > best_acc:
            best_acc = val_acc
            patience_counter = 0
            torch.save(model.state_dict(), "best_model.pth")
        else:
            patience_counter += 1
        
        print(f"Epoch {epoch+1}: val_acc={val_acc:.4f}, best={best_acc:.4f}")
        
        # 早停检查
        if patience_counter >= max_patience:
            print(f"Early stopping at epoch {epoch+1}")
            break
    
    return best_acc

best_acc = train_model(model, train_loader, val_loader)
print(f"最佳验证准确率: {best_acc:.4f}")
训练阶段学习率解冻范围监控指标

Warmup (1-3轮)

1e-5 -> 1e-3

仅分类头

训练损失

微调 (4-15轮)

自适应

后两层+分类头

验证准确率

精调 (16-30轮)

1e-5 以下

全部层

验证损失

早停触发

停止

全部

patience计数

使用 torch.save(model.state_dict(), 'best_model.pth') 只保存权重而非整个模型对象,这样可以在不同代码环境中灵活加载

训练前务必验证数据加载正确:打印一个 batch 的 shapes 和 label 分布,避免数据管道错误导致训练无声失败

继续你的 AI 学习之旅

浏览更多 AI 知识库文章,或者探索 GitHub 上的优质 AI 项目