迁移学习实战:用预训练模型做图像分类
摘要:在第六篇文章中,我们从零搭建了一个 CNN 训练 CIFAR-10,达到了约 84% 的准确率。但如果用上"迁移学习"——把别人在大规模数据集上训练好的模型拿来微调——只需要几行代码改动,就能把准确率提升到 95% 以上。这篇文章讲清楚迁移学习为什么有效,并给出完整的实战代码。
一、什么是迁移学习?
核心思想
迁移学习就是站在巨人的肩膀上:
传统训练(从零开始): 随机初始化 → 在目标数据集上训练 → 需要大量数据和算力 迁移学习: ImageNet 预训练模型(已学会通用特征) → 在目标数据集上微调(只需少量数据) → 效果好、训练快为什么有效?
深度学习模型在训练过程中学到了层次化的特征:
预训练模型已经学会的: 第 1 层:检测边缘、纹理、颜色块 ← 通用,所有图像任务通用 第 2 层:检测形状、图案 ← 通用,所有图像任务通用 第 3 层:检测部件(眼睛、轮子) ← 较通用,多数任务有用 第 4-5 层:检测具体物体(人脸、汽车)← 特定任务,需要微调 我们只需要: 保留第 1-3 层(通用特征提取器) 替换第 4-5 层(适应我们的具体任务) 用我们的数据微调迁移学习 vs 从零训练
| 对比 | 从零训练 | 迁移学习 |
|---|---|---|
| 所需数据量 | 需要大量数据(数万张) | 少量数据即可(几百张) |
| 训练时间 | 长(数小时到数天) | 短(数分钟到数小时) |
| GPU 需求 | 高 | 低 |
| 最终准确率 | 取决于数据量 | 通常更高 |
| 代码复杂度 | 中等 | 低(torchvision 几行加载) |
二、准备工作:加载预训练模型
PyTorch 的torchvision.models提供了丰富的预训练模型,一行代码即可加载。
import torch import torch.nn as nn import torch.optim as optim import torchvision import torchvision.transforms as transforms from torch.utils.data import DataLoader import matplotlib.pyplot as plt import numpy as np device = torch.device("cuda" if torch.cuda.is_available() else "cpu") print(f"Using: {device}")支持的预训练模型
# torchvision 提供的预训练模型(2026 年) models = [ "resnet18", "resnet50", "resnet101", "vgg16", "vgg19", "densenet121", "densenet169", "efficientnet_b0", "efficientnet_b3", "efficientnet_b7", "vit_b_16", "vit_l_32", # Vision Transformer "convnext_tiny", "convnext_base", "swin_t", "swin_b", # Swin Transformer "maxvit_t", # MaxViT ]加载 ResNet-18 预训练模型
# ===== 加载预训练模型 ===== model = torchvision.models.resnet18(weights='IMAGENET1K_V1') # weights='IMAGENET1K_V1' = 在 ImageNet(1000 类、1400 万张图)上训练好的权重 print(model) # ResNet( # (conv1): Conv2d(3, 64, kernel_size=7, stride=2, padding=3) # (bn1): BatchNorm2d(64) # (relu): ReLU() # (maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1) # (layer1): Sequential(...) ← 4 个残差块,64 通道 # (layer2): Sequential(...) ← 4 个残差块,128 通道 # (layer3): Sequential(...) ← 4 个残差块,256 通道 # (layer4): Sequential(...) ← 4 个残差块,512 通道 # (avgpool): AdaptiveAvgPool2d((1, 1)) # (fc): Linear(512, 1000) ← ImageNet 的 1000 分类头 # )理解预训练模型的架构
ResNet-18 结构: 输入 (3×224×224) │ conv1 (7×7, stride=2) 输出: 64×112×112 │ batch_norm + ReLU + maxpool │ layer1 (2 个残差块, 64 通道) 输出: 64×56×56 ← 检测基础特征 │ layer2 (2 个残差块, 128 通道) 输出: 128×28×28 ← 检测纹理 │ layer3 (2 个残差块, 256 通道) 输出: 256×14×14 ← 检测部件 │ layer4 (2 个残差块, 512 通道) 输出: 512×7×7 ← 检测高级语义 │ avgpool 输出: 512 │ fc (全连接层) 输出: 1000(ImageNet 分类)三、迁移学习的两种策略
策略 1:特征提取(冻结骨干网络)
只替换分类头,冻结所有卷积层——适合小数据集。
def freeze_feature_extractor(model): """冻结所有卷积层(不计算梯度,不更新参数)""" for param in model.parameters(): param.requires_grad = False # 1. 加载预训练模型 model = torchvision.models.resnet18(weights='IMAGENET1K_V1') # 2. 冻结所有层 freeze_feature_extractor(model) # 3. 替换分类头(适应我们的任务) num_classes = 10 # 以 CIFAR-10 为例 model.fc = nn.Linear(512, num_classes) # 只有分类头的参数需要梯度 trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad) frozen_params = sum(p.numel() for p in model.parameters() if not p.requires_grad) print(f"可训练: {trainable_params:,} | 已冻结: {frozen_params:,}") # 可训练: 5,130 | 已冻结: 11,176,512 # → 只需要训练 5000 个参数,其他 1100 万参数不动!策略 2:微调(全模型参与)
所有层都参与训练,但对不同层使用不同的学习率——适合中等规模数据集。
def fine_tune_setup(model, num_classes, lr_backbone=1e-5, lr_head=1e-3): """ 微调设置: - 骨干网络:小学习率(1e-5)——在预训练基础上微调 - 分类头:大学习率(1e-3)——从头学习 """ # 1. 替换分类头 in_features = model.fc.in_features model.fc = nn.Linear(in_features, num_classes) # 2. 为不同层设置不同学习率 backbone_params = [] head_params = [] for name, param in model.named_parameters(): if 'fc' in name: head_params.append(param) else: backbone_params.append(param) optimizer = optim.AdamW([ {'params': backbone_params, 'lr': lr_backbone}, # 骨干:小学习率 {'params': head_params, 'lr': lr_head}, # 分类头:大学习率 ]) return model, optimizer # 使用 model = torchvision.models.resnet18(weights='IMAGENET1K_V1') model, optimizer = fine_tune_setup(model, num_classes=10)两种策略的选型指南
| 条件 | 推荐策略 | 原因 |
|---|---|---|
| 数据量 < 1000 张 | 特征提取(冻结骨干) | 数据太少,微调容易过拟合 |
| 数据量 1000-10000 张 | 微调(小学习率) | 足够数据调整,但不宜变动过大 |
| 数据量 > 10000 张 | 微调(正常学习率) | 数据充足,可以较大幅调整 |
| 目标任务与 ImageNet 差异大 | 微调(解冻更多层) | 医学影像、卫星图等特殊领域 |
四、完整实战:用 ResNet18 微调 CIFAR-10
数据准备
# ===== 数据预处理 ===== # 注意:预训练模型要求特定的归一化参数 transform_train = transforms.Compose([ transforms.Resize(224), # ResNet 要求 224×224 transforms.RandomHorizontalFlip(), transforms.RandomCrop(224, padding=28), # 大尺寸裁剪增强 transforms.ToTensor(), transforms.Normalize( # ImageNet 的归一化参数 mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] ), ]) transform_test = transforms.Compose([ transforms.Resize(224), transforms.ToTensor(), transforms.Normalize( mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] ), ]) train_dataset = torchvision.datasets.CIFAR10( root='./data', train=True, download=True, transform=transform_train ) test_dataset = torchvision.datasets.CIFAR10( root='./data', train=False, download=True, transform=transform_test ) train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True, num_workers=4) test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False, num_workers=4)构建模型
# ===== 模型:特征提取策略 ===== model = torchvision.models.resnet18(weights='IMAGENET1K_V1') # 冻结所有层 for param in model.parameters(): param.requires_grad = False # 替换分类头 model.fc = nn.Sequential( nn.Dropout(0.3), nn.Linear(512, 256), nn.ReLU(), nn.Dropout(0.2), nn.Linear(256, 10), ) model = model.to(device) # 只有新加的层需要梯度 criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.fc.parameters(), lr=0.001) total_params = sum(p.numel() for p in model.parameters()) trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad) print(f"总参数量: {total_params:,} | 可训练: {trainable_params:,}") # 总参数量: 11,180,234 | 可训练: 131,850 # → 参数量是之前 CNN 的 10 倍,但只训练其中 1%训练循环(复用第 06 篇的模板)
def train_epoch(model, loader, criterion, optimizer, device): model.train() running_loss = 0.0 correct = 0 total = 0 for inputs, labels in loader: inputs, labels = inputs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() _, predicted = outputs.max(1) total += labels.size(0) correct += predicted.eq(labels).sum().item() return running_loss / len(loader), 100.0 * correct / total @torch.no_grad() def evaluate(model, loader, criterion, device): model.eval() running_loss = 0.0 correct = 0 total = 0 for inputs, labels in loader: inputs, labels = inputs.to(device), labels.to(device) outputs = model(inputs) loss = criterion(outputs, labels) running_loss += loss.item() _, predicted = outputs.max(1) total += labels.size(0) correct += predicted.eq(labels).sum().item() return running_loss / len(loader), 100.0 * correct / total执行训练
# ===== 执行训练 ===== num_epochs = 20 best_acc = 0.0 for epoch in range(1, num_epochs + 1): train_loss, train_acc = train_epoch( model, train_loader, criterion, optimizer, device ) test_loss, test_acc = evaluate( model, test_loader, criterion, device ) if test_acc > best_acc: best_acc = test_acc torch.save(model.state_dict(), 'resnet18_cifar10.pth') if epoch % 2 == 0 or epoch == 1: print(f"Epoch {epoch:2d} | " f"Train Loss={train_loss:.3f} Acc={train_acc:.2f}% | " f"Test Loss={test_loss:.3f} Acc={test_acc:.2f}%") print(f"\n✅ 最佳测试准确率: {best_acc:.2f}%")输出示例:
Epoch 1 | Train Loss=1.113 Acc=66.42% | Test Loss=0.543 Acc=81.37% Epoch 2 | Train Loss=0.526 Acc=82.95% | Test Loss=0.345 Acc=87.63% Epoch 4 | Train Loss=0.302 Acc=89.88% | Test Loss=0.240 Acc=91.23% Epoch 6 | Train Loss=0.216 Acc=92.67% | Test Loss=0.215 Acc=92.18% Epoch 8 | Train Loss=0.173 Acc=94.25% | Test Loss=0.196 Acc=93.05% Epoch 10 | Train Loss=0.139 Acc=95.36% | Test Loss=0.191 Acc=93.52% Epoch 12 | Train Loss=0.114 Acc=96.21% | Test Loss=0.180 Acc=94.07% Epoch 14 | Train Loss=0.095 Acc=96.92% | Test Loss=0.175 Acc=94.31% Epoch 16 | Train Loss=0.079 Acc=97.52% | Test Loss=0.173 Acc=94.18% Epoch 18 | Train Loss=0.065 Acc=98.08% | Test Loss=0.182 Acc=94.33% Epoch 20 | Train Loss=0.055 Acc=98.40% | Test Loss=0.178 Acc=94.48% ✅ 最佳测试准确率: 94.48%结果对比
| 方法 | 从零训练的 CNN | 迁移学习(特征提取) |
|---|---|---|
| 准确率 | 84.2% | 94.5% |
| 训练时间 | 30 分钟 | 5 分钟 |
| 参数量 | 1.2M | 11.2M(只训练 132K) |
迁移学习用 1/6 的时间,提升了 10 个百分点的准确率!
五、进阶:选择合适的预训练模型
模型大小 vs 准确率
def get_pretrained_model(name, num_classes, freeze=True): """获取不同预训练模型""" weights_enum = { 'resnet18': torchvision.models.ResNet18_Weights.IMAGENET1K_V1, 'resnet50': torchvision.models.ResNet50_Weights.IMAGENET1K_V1, 'efficientnet_b0': torchvision.models.EfficientNet_B0_Weights.IMAGENET1K_V1, 'vit_b_16': torchvision.models.ViT_B_16_Weights.IMAGENET1K_V1, 'convnext_tiny': torchvision.models.ConvNeXt_Tiny_Weights.IMAGENET1K_V1, } model = torchvision.models.get_model(name, weights=weights_enum[name]) if freeze: for param in model.parameters(): param.requires_grad = False # 替换分类头(不同模型的分类头名称不同) if 'vit' in name: model.heads.head = nn.Linear(model.heads.head.in_features, num_classes) elif 'convnext' in name: model.classifier[-1] = nn.Linear(model.classifier[-1].in_features, num_classes) else: model.fc = nn.Linear(model.fc.in_features, num_classes) return model| 模型 | 参数量 | CIFAR-10 准确率(迁移学习) | 推理速度 |
|---|---|---|---|
| ResNet-18 | 11M | ~94% | 快 |
| ResNet-50 | 25M | ~96% | 中等 |
| EfficientNet-B0 | 5.3M | ~95% | 最快 |
| ConvNeXt-Tiny | 28M | ~97% | 中等 |
| ViT-B/16 | 86M | ~98% | 慢 |
选型建议:
- 移动端/实时:EfficientNet-B0(体积小、速度快)
- 通用场景:ResNet-50(成熟可靠、生态好)
- 追求精度:ConvNeXt-Tiny 或 ViT(效果好,但慢)
六、迁移学习的常见问题
问题 1:我的数据和 ImageNet 差异很大怎么办?
如果目标图像和自然图像差异大(如医学影像、卫星图、手绘图),建议:
1. 不要冻结太多层(解冻 layer3 和 layer4) 2. 使用更大的学习率微调 3. 考虑在相似领域的数据上做预训练 (如医学影像 → 在 CheXpert 上预训练的模型)# 选择性地解冻部分层 model = torchvision.models.resnet18(weights='IMAGENET1K_V1') # 冻结前 3 层,解冻最后 1 层卷积和分类头 for name, param in model.named_parameters(): if 'layer4' in name or 'fc' in name: param.requires_grad = True else: param.requires_grad = False问题 2:微调时过拟合怎么办?
# 解决方案组合: # 1. 更强的数据增强 transform_train = transforms.Compose([ transforms.Resize(224), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ColorJitter(0.2, 0.2, 0.2, 0.1), transforms.RandomCrop(224, padding=28), transforms.ToTensor(), transforms.Normalize(mean, std), ]) # 2. 增加 Dropout model.fc = nn.Sequential( nn.Dropout(0.5), # 加大 Dropout nn.Linear(512, 256), nn.ReLU(), nn.Dropout(0.3), nn.Linear(256, 10), ) # 3. 权重衰减 optimizer = optim.AdamW(model.fc.parameters(), lr=0.001, weight_decay=1e-3)问题 3:微调和特征提取哪个更好?
数据量很小(<500 张):特征提取(冻结骨干) ✅ 数据量中等(500-5000 张):微调(小学习率 1e-5~1e-4) ✅ 数据量充足(>5000 张):微调(正常学习率) ✅ 不确定时:先试特征提取,如果训练集准确率已接近 100% 说明数据足够微调七、总结
| 概念 | 一句话 |
|---|---|
| 迁移学习 | 把别人训练好的模型拿来改一改,适应自己的任务 |
| 预训练模型 | 在 ImageNet(1400 万张图)上训练好的特征提取器 |
| 特征提取 | 冻结卷积层,只训练分类头——适合小数据 |
| 微调 | 所有层参与训练,但骨干用小学习率——适合中大数据 |
| 为什么有效 | 低层特征(边缘、纹理)在所有图像任务中通用 |
核心三句话:
- 迁移学习是深度学习最快见效的技巧——用几行代码就能提升 10 个百分点的准确率
- torchvision 一行代码加载预训练模型——不要从零训练,除非你有特殊理由
- 先特征提取,再尝试微调——小数据用冻结策略,数据充足再全模型微调
在 2026 年,除了极特殊的场景,没有人会从零训练一个图像模型。迁移学习已经是标准做法。
