迁移学习实战:用预训练模型做图像分类

迁移学习实战:用预训练模型做图像分类 摘要在第六篇文章中我们从零搭建了一个 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(fUsing: {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(weightsIMAGENET1K_V1) # weightsIMAGENET1K_V1 在 ImageNet1000 类、1400 万张图上训练好的权重 print(model) # ResNet( # (conv1): Conv2d(3, 64, kernel_size7, stride2, padding3) # (bn1): BatchNorm2d(64) # (relu): ReLU() # (maxpool): MaxPool2d(kernel_size3, stride2, padding1) # (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, stride2) 输出: 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 (全连接层) 输出: 1000ImageNet 分类三、迁移学习的两种策略策略 1特征提取冻结骨干网络只替换分类头冻结所有卷积层——适合小数据集。def freeze_feature_extractor(model): 冻结所有卷积层不计算梯度不更新参数 for param in model.parameters(): param.requires_grad False # 1. 加载预训练模型 model torchvision.models.resnet18(weightsIMAGENET1K_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_backbone1e-5, lr_head1e-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(weightsIMAGENET1K_V1) model, optimizer fine_tune_setup(model, num_classes10)两种策略的选型指南条件推荐策略原因数据量 1000 张特征提取冻结骨干数据太少微调容易过拟合数据量 1000-10000 张微调小学习率足够数据调整但不宜变动过大数据量 10000 张微调正常学习率数据充足可以较大幅调整目标任务与 ImageNet 差异大微调解冻更多层医学影像、卫星图等特殊领域四、完整实战用 ResNet18 微调 CIFAR-10数据准备# 数据预处理 # 注意预训练模型要求特定的归一化参数 transform_train transforms.Compose([ transforms.Resize(224), # ResNet 要求 224×224 transforms.RandomHorizontalFlip(), transforms.RandomCrop(224, padding28), # 大尺寸裁剪增强 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, trainTrue, downloadTrue, transformtransform_train ) test_dataset torchvision.datasets.CIFAR10( root./data, trainFalse, downloadTrue, transformtransform_test ) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue, num_workers4) test_loader DataLoader(test_dataset, batch_size64, shuffleFalse, num_workers4)构建模型# 模型特征提取策略 model torchvision.models.resnet18(weightsIMAGENET1K_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(), lr0.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(fEpoch {epoch:2d} | fTrain Loss{train_loss:.3f} Acc{train_acc:.2f}% | fTest Loss{test_loss:.3f} Acc{test_acc:.2f}%) print(f\n✅ 最佳测试准确率: {best_acc:.2f}%)输出示例Epoch 1 | Train Loss1.113 Acc66.42% | Test Loss0.543 Acc81.37% Epoch 2 | Train Loss0.526 Acc82.95% | Test Loss0.345 Acc87.63% Epoch 4 | Train Loss0.302 Acc89.88% | Test Loss0.240 Acc91.23% Epoch 6 | Train Loss0.216 Acc92.67% | Test Loss0.215 Acc92.18% Epoch 8 | Train Loss0.173 Acc94.25% | Test Loss0.196 Acc93.05% Epoch 10 | Train Loss0.139 Acc95.36% | Test Loss0.191 Acc93.52% Epoch 12 | Train Loss0.114 Acc96.21% | Test Loss0.180 Acc94.07% Epoch 14 | Train Loss0.095 Acc96.92% | Test Loss0.175 Acc94.31% Epoch 16 | Train Loss0.079 Acc97.52% | Test Loss0.173 Acc94.18% Epoch 18 | Train Loss0.065 Acc98.08% | Test Loss0.182 Acc94.33% Epoch 20 | Train Loss0.055 Acc98.40% | Test Loss0.178 Acc94.48% ✅ 最佳测试准确率: 94.48%结果对比方法从零训练的 CNN迁移学习特征提取准确率84.2%94.5%训练时间30 分钟5 分钟参数量1.2M11.2M只训练 132K迁移学习用 1/6 的时间提升了 10 个百分点的准确率五、进阶选择合适的预训练模型模型大小 vs 准确率def get_pretrained_model(name, num_classes, freezeTrue): 获取不同预训练模型 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, weightsweights_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-1811M~94%快ResNet-5025M~96%中等EfficientNet-B05.3M~95%最快ConvNeXt-Tiny28M~97%中等ViT-B/1686M~98%慢选型建议移动端/实时EfficientNet-B0体积小、速度快通用场景ResNet-50成熟可靠、生态好追求精度ConvNeXt-Tiny 或 ViT效果好但慢六、迁移学习的常见问题问题 1我的数据和 ImageNet 差异很大怎么办如果目标图像和自然图像差异大如医学影像、卫星图、手绘图建议1. 不要冻结太多层解冻 layer3 和 layer4 2. 使用更大的学习率微调 3. 考虑在相似领域的数据上做预训练 如医学影像 → 在 CheXpert 上预训练的模型# 选择性地解冻部分层 model torchvision.models.resnet18(weightsIMAGENET1K_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, padding28), 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(), lr0.001, weight_decay1e-3)问题 3微调和特征提取哪个更好数据量很小500 张特征提取冻结骨干 ✅ 数据量中等500-5000 张微调小学习率 1e-5~1e-4 ✅ 数据量充足5000 张微调正常学习率 ✅ 不确定时先试特征提取如果训练集准确率已接近 100% 说明数据足够微调七、总结概念一句话迁移学习把别人训练好的模型拿来改一改适应自己的任务预训练模型在 ImageNet1400 万张图上训练好的特征提取器特征提取冻结卷积层只训练分类头——适合小数据微调所有层参与训练但骨干用小学习率——适合中大数据为什么有效低层特征边缘、纹理在所有图像任务中通用核心三句话迁移学习是深度学习最快见效的技巧——用几行代码就能提升 10 个百分点的准确率torchvision 一行代码加载预训练模型——不要从零训练除非你有特殊理由先特征提取再尝试微调——小数据用冻结策略数据充足再全模型微调在 2026 年除了极特殊的场景没有人会从零训练一个图像模型。迁移学习已经是标准做法。