
浙大疏锦行预训练、迁移学习与ResNet微调实战前言在我们之前从零训练CNN的过程里发现模型参数初始化非常关键初始化不好收敛慢还容易卡在局部最优。预训练微调迁移学习就是用来解决这个痛点的核心方案。今天系统梳理预训练、迁移学习概念同时附带可直接运行的PyTorch代码基于ResNet18在CIFAR10上做阶段性冻结微调。配套环境PyTorch torchvision数据集CIFAR10骨干网络ResNet18上游预训练权重来自ImageNet。一、预训练是什么模型参数初始值对最终训练结果影响巨大初始参数好收敛更快需要的epoch更少初始参数差容易陷入局部最优最终效果差预训练思想在和目标任务相似的大规模公开数据集上提前训练好模型把这套训练好的参数拿来初始化我们自己的模型。自问自答理解核心✅ 为什么要选相似任务的预训练权重 任务相近网络学到的特征提取能力才可以复用任务差距过大学到的特征不匹配迁移效果很差。预训练本质复用模型已经学会的通用视觉特征再在我们自己数据集上继续适配下游任务。✅ 为什么预训练要用大规模数据集小规模数据集不行 预训练目标是学习通用特征。小数据集很难让复杂网络学到普适的特征。类比物理博士做小学数学题上手很快小学数学高手很难搞定物理前沿课题。术语区分上游任务大规模数据集上的预训练过程如ImageNet图像分类下游任务在自己数据集上基于预训练参数继续调参的过程这个过程叫做微调Finetune整套思想迁移学习一个反例CIFAR10不适合用来做预训练数据集规模小仅10万张图图像尺寸只有32×32类别少只有10个类别学到的特征泛化能力弱经典预训练数据集ImageNet1000类别上亿图像常用输入尺寸224×224是CV分类预训练最经典的数据集。二、经典预训练模型汇总2.1 CNN类预训练骨干网络模型预训练数据集核心特点CIFAR10适配要点AlexNetImageNet首次ReLU、LRN归一化参数量6000w修改首层卷积核适配32×32小图VGG16ImageNet纯3×3卷积堆叠结构简单规整冻结大部分卷积层只微调头部ResNet18ImageNet残差连接解决梯度消失1100w参数直接适配小图仅调整池化步长MobileNetV2ImageNet深度可分离卷积轻量化适合算力弱、移动端场景2.2 Transformer类预训练模型ViT、Swin Transformer、DeiT原生面向224×224大图放到CIFAR10这种32×32小图要么上采样放大图片要么调整patch大小。2.3 自监督预训练模型不需要人工标签依靠辅助任务对比学习、掩码重建学习特征标签稀缺场景很香。MoCo v3对比学习预训练BEiT掩码图像建模三、CNN预训练模型发展史梳理模型年份团队核心创新LeNet-51998Yann LeCun最早CNN卷积池化全连接AlexNet2012AlexReLU、Dropout、GPU训练开启深度学习时代VGGNet2014Oxford VGG统一3×3卷积结构简洁GoogLeNet2014GoogleInception多分支、1×1卷积降维ResNet2015何恺明残差连接解决深层网络退化CV里程碑DenseNet2017黄高密集连接特征复用小数据集友好MobileNet2017Google深度可分离卷积轻量化EfficientNet2019GoogleNAS复合缩放深度/宽度/分辨率一起优化补充指标说明ImageNet Top-5 Error1000分类任务预测概率前5个里面包含真实标签即算正确是当年经典评估指标。模型演进4条主线深度突破残差连接让上百层网络可以稳定训练计算效率Inception、MobileNet在精度和算力之间做平衡特征复用DenseNet密集连接适合小数据集抑制过拟合自动架构EfficientNet用NAS自动搜索网络配置模型选型建议需求推荐模型理由快速跑原型ResNet18/50生态成熟、预训练权重稳定、上手简单移动端部署MobileNetV3参数少、推理速度快追求高精度、算力充足EfficientNet-B7ImageNet精度高数据集很小DenseNet特征复用抗过拟合四、微调的训练策略加载预训练模型做微调不是直接全部参数一起训有几个关键点加载官方预训练权重输入图片尺寸、归一化参数要和预训练时匹配替换最后的分类头FC层适配我们自己数据集类别数CIFAR10是10类阶段训练策略先冻结backbone骨干只训练分类头稳定后解冻全部参数微调。术语Backbone骨干负责提取图像特征ResNet卷积层部分Head头最后的全连接分类层输出类别logitsFPN特征金字塔网络常用于检测、分割任务做多尺度特征融合常规方案前若干epoch冻结骨干只训head之后解冻全部参数使用更小学习率微调避免破坏学到的通用特征。五、完整可运行代码ResNet18 分阶段冻结微调 CIFAR10import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms, models from torch.utils.data import DataLoader import matplotlib.pyplot as plt import os # 设置中文字体支持 plt.rcParams[font.family] [SimHei] plt.rcParams[axes.unicode_minus] False # 解决负号显示问题 # 检查GPU是否可用 device torch.device(cuda if torch.cuda.is_available() else cpu) print(f使用设备: {device}) # 1. 数据预处理训练集增强测试集标准化 train_transform transforms.Compose([ transforms.RandomCrop(32, padding4), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2, hue0.1), transforms.RandomRotation(15), transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)) ]) test_transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)) ]) # 2. 加载CIFAR-10数据集 train_dataset datasets.CIFAR10( root./data, trainTrue, downloadTrue, transformtrain_transform ) test_dataset datasets.CIFAR10( root./data, trainFalse, transformtest_transform ) # 3. 创建数据加载器 batch_size 64 train_loader DataLoader(train_dataset, batch_sizebatch_size, shuffleTrue) test_loader DataLoader(test_dataset, batch_sizebatch_size, shuffleFalse) # 4. 定义ResNet18模型 def create_resnet18(pretrainedTrue, num_classes10): model models.resnet18(pretrainedpretrained) # 修改最后一层全连接层适配10分类 in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes) return model.to(device) # 5. 冻结/解冻骨干卷积层 def freeze_model(model, freezeTrue): 冻结或解冻除fc以外的所有参数 for name, param in model.named_parameters(): if fc not in name: param.requires_grad not freeze frozen_params sum(p.numel() for p in model.parameters() if not p.requires_grad) total_params sum(p.numel() for p in model.parameters()) if freeze: print(f已冻结骨干卷积层参数可训练参数{total_params - frozen_params}/{total_params}) else: print(f全部参数解冻所有{total_params}参数均可训练) return model # 6. 训练函数支持分阶段冻结 def train_with_freeze_schedule(model, train_loader, test_loader, criterion, optimizer, scheduler, device, epochs, freeze_epochs5): train_loss_history [] test_loss_history [] train_acc_history [] test_acc_history [] all_iter_losses [] iter_indices [] if freeze_epochs 0: model freeze_model(model, freezeTrue) for epoch in range(epochs): # 到达解冻轮次放开全部参数 if epoch freeze_epochs: model freeze_model(model, freezeFalse) optimizer.param_groups[0][lr] 1e-4 model.train() running_loss 0.0 correct_train 0 total_train 0 for batch_idx, (data, target) in enumerate(train_loader): data, target data.to(device), target.to(device) optimizer.zero_grad() output model(data) loss criterion(output, target) loss.backward() optimizer.step() iter_loss loss.item() all_iter_losses.append(iter_loss) iter_indices.append(epoch * len(train_loader) batch_idx 1) running_loss iter_loss _, predicted output.max(1) total_train target.size(0) correct_train predicted.eq(target).sum().item() if (batch_idx 1) % 100 0: print(fEpoch {epoch1}/{epochs} | Batch {batch_idx1}/{len(train_loader)} | 单Batch损失: {iter_loss:.4f}) epoch_train_loss running_loss / len(train_loader) epoch_train_acc 100. * correct_train / total_train # 测试 model.eval() correct_test 0 total_test 0 test_loss 0.0 with torch.no_grad(): for data, target in test_loader: data, target data.to(device), target.to(device) output model(data) test_loss criterion(output, target).item() _, predicted output.max(1) total_test target.size(0) correct_test predicted.eq(target).sum().item() epoch_test_loss test_loss / len(test_loader) epoch_test_acc 100. * correct_test / total_test train_loss_history.append(epoch_train_loss) test_loss_history.append(epoch_test_loss) train_acc_history.append(epoch_train_acc) test_acc_history.append(epoch_test_acc) if scheduler is not None: scheduler.step(epoch_test_loss) print(fEpoch {epoch1} 完成 | 训练损失: {epoch_train_loss:.4f} | 训练准确率: {epoch_train_acc:.2f}% | 测试准确率: {epoch_test_acc:.2f}%) plot_iter_losses(all_iter_losses, iter_indices) plot_epoch_metrics(train_acc_history, test_acc_history, train_loss_history, test_loss_history) return epoch_test_acc # 7. 绘图函数 def plot_iter_losses(losses, indices): plt.figure(figsize(10, 4)) plt.plot(indices, losses, b-, alpha0.7) plt.xlabel(IterationBatch序号) plt.ylabel(损失值) plt.title(训练过程Iter损失变化) plt.grid(True) plt.show() def plot_epoch_metrics(train_acc, test_acc, train_loss, test_loss): epochs range(1, len(train_acc) 1) plt.figure(figsize(12, 5)) plt.subplot(1, 2, 1) plt.plot(epochs, train_acc, b-, label训练准确率) plt.plot(epochs, test_acc, r-, label测试准确率) plt.xlabel(Epoch) plt.ylabel(准确率 (%)) plt.title(准确率变化) plt.legend() plt.grid(True) plt.subplot(1, 2, 2) plt.plot(epochs, train_loss, b-, label训练损失) plt.plot(epochs, test_loss, r-, label测试损失) plt.xlabel(Epoch) plt.ylabel(损失值) plt.title(损失变化) plt.legend() plt.grid(True) plt.tight_layout() plt.show() # 主训练入口 def main(): epochs 40 freeze_epochs 5 learning_rate 1e-3 weight_decay 1e-4 model create_resnet18(pretrainedTrue, num_classes10) optimizer optim.Adam(model.parameters(), lrlearning_rate, weight_decayweight_decay) criterion nn.CrossEntropyLoss() scheduler optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemin, factor0.5, patience2, verboseTrue) final_accuracy train_with_freeze_schedule(model, train_loader, test_loader, criterion, optimizer, scheduler, device, epochs, freeze_epochs) print(f训练完成最终测试准确率: {final_accuracy:.2f}%) # torch.save(model.state_dict(), resnet18_cifar10_finetuned.pth) if __name__ __main__: main()CIFAR10标签映射0:airplane飞机1:automobile汽车2:bird鸟3:cat猫4:deer鹿 5:dog狗6:frog青蛙7:horse马8:ship船9:truck卡车六、训练中观察到的现象收敛速度极快解冻骨干之后短短几个epoch效果就能超过从零训练CNN 20轮这就是预训练迁移学习最大优势前期训练acc可能低于测试acc因为训练集加了大量数据增强随机裁剪、翻转、色彩抖动、旋转训练图片难度更高测试图片无增强。前期很容易出现这个现象训练后期会慢慢缓解精度上限更高微调收敛后的准确率显著高于从零开始训练的CNN轻松突破80%。七、总结预训练微调是CV工程里最常用的范式核心就是用大规模数据集学到通用视觉先验再迁移到小数据集下游任务。 实操上记住两个关键点分类头一定要改成自己任务的类别数优先冻结骨干训头再小学习率全参数微调保护预训练学到的通用特征。后续可以拓展尝试冻结不同层、对比freeze和不freeze的差异、尝试ViT/MobileNet预训练权重迁移。class BasicBlock(nn.Module): def forward(self, x): identity x # 捷径分支shortcut保存原始输入 out self.conv1(x) out self.bn1(out) out self.relu(out) out self.conv2(out) out self.bn2(out) # 残差核心卷积学习到的特征 原始输入 out identity out self.relu(out) return out残差思想模型不去直接学习目标映射 \(H(x)\)改为学习残差 \(F(x)H(x)-x\)最终输出 \(H(x)F(x)x\)。x恒等捷径分支不需要额外学习\(F(x)\)卷积层需要拟合的残差部分若输入、输出通道或者尺寸不一致会启用downsample对identity做卷积变换保证张量形状匹配才能相加。 残差连接解决了深层 CNN 的网络退化问题让梯度可以沿着 shortcut 支路直接回传深层网络更容易训练收敛。实验结果与差异分析表格模型参数量核心结构特点CIFAR10 微调表现ResNet18约 11.2M残差 shortcut 连接结构简洁上手简单收敛稳定精度中等训练速度快DenseNet121约 8M密集连接每一层接收前面所有层的特征特征复用强精度最高参数量更少显存占用偏高训练慢一点MobileNetV2约 2.2M深度可分离卷积 倒残差轻量化设计推理最快、参数量最小精度略低适合移动端场景小结DenseNet 依靠密集特征复用在小数据集 CIFAR10 上微调上限更高MobileNetV2 主打轻量化精度换取速度和显存ResNet 属于均衡选择兼顾训练速度、精度、工程易用性预训练微调的核心逻辑不变冻结骨干先学分类头再全参数微调降低过拟合风险。