第13篇:迁移学习实战——站在巨人肩膀上的模型训练捷径(项目实战)

张开发
2026/4/16 9:49:25 15 分钟阅读

分享文章

第13篇:迁移学习实战——站在巨人肩膀上的模型训练捷径(项目实战)
文章目录项目背景技术选型架构设计核心实现1. 环境准备与数据加载2. 模型改造加载预训练模型并“换头”3. 训练策略区别对待不同参数4. 训练与验证循环5. 进阶解冻部分层进行微调踩坑记录效果对比项目背景在之前的文章中我们都是从零开始训练模型。但做过几个实际项目后我发现一个痛点数据不够算力有限时间还紧。比如公司想做一个工业瑕疵检测系统但能收集到的合格品和瑕疵品图片只有几千张用传统方法训练一个复杂的CNN模型要么过拟合要么效果平平。这时候迁移学习Transfer Learning就成了我们的“救命稻草”。简单来说迁移学习就是利用在一个大型数据集如ImageNet上预训练好的模型将其学到的通用特征如边缘、纹理、形状迁移到我们自己的、数据量较小的新任务上。这就像一位经验丰富的医生已经掌握了大量基础医学知识再针对某个专科进行短期进修就能快速成为该领域的专家远比从头培养一个医学生要快得多、好得多。这次实战我们就用PyTorch基于预训练的ResNet模型快速构建一个猫狗图像二分类器。虽然猫狗数据集很经典但背后的方法论完全适用于工业检测、医疗影像、卫星图像分析等数据稀缺的领域。技术选型框架PyTorch。它的torchvision.models模块提供了丰富的预训练模型加载和修改极其方便动态图机制也让模型调整和调试更直观。预训练模型ResNet-18。在精度和速度之间取得了很好的平衡模型大小适中非常适合作为入门和实际部署的基线模型。ImageNet上预训练的ResNet已经学会了识别上千种物体的强大特征。数据集Kaggle上的Dogs vs. Cats数据集精简版。我们假设这是一个“小数据”场景只使用训练集中的一小部分例如每类1000张来模拟数据匮乏的情况。核心策略特征提取Feature Extraction与微调Fine-tuning。这是迁移学习最常用的两种方式本次实战我们会结合使用。架构设计我们的目标不是重新设计网络而是对现有的ResNet-18架构进行“外科手术式”的改造。保留特征提取器冻结ResNet-18除最终全连接层外的所有卷积层参数。这些层是模型的“骨干”已经包含了从低级到高级的通用图像特征。替换分类头移除ResNet-18原本为1000类ImageNet类别数设计的最后一个全连接层替换为一个新的、适合我们二分类任务的分类器。渐进式微调可选在特征提取训练稳定后可以解冻部分深层卷积层用较小的学习率进行微调让模型更好地适应我们任务的特有特征。整个流程的架构图可以简化为输入图像-预训练ResNet骨干参数冻结-全局平均池化-新的全连接分类层可训练-二分类输出核心实现1. 环境准备与数据加载首先确保安装了必要的库并按照PyTorch惯例组织数据。假设你的数据目录结构如下data/dogs-vs-cats/ ├── train/ │ ├── cat.0.jpg │ ├── dog.0.jpg │ └── ... └── valid/ ├── cat.2000.jpg ├── dog.2000.jpg └── ...importtorchimporttorch.nnasnnimporttorch.optimasoptimfromtorchvisionimportdatasets,models,transformsimportos# 数据增强和归一化必须与预训练模型使用的保持一致# 注意ImageNet预训练模型的均值和标准差是固定的data_transforms{train:transforms.Compose([transforms.RandomResizedCrop(224),# 模型输入尺寸transforms.RandomHorizontalFlip(),transforms.ToTensor(),transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])]),valid:transforms.Compose([transforms.Resize(256),transforms.CenterCrop(224),transforms.ToTensor(),transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])]),}data_dirdata/dogs-vs-catsimage_datasets{x:datasets.ImageFolder(os.path.join(data_dir,x),data_transforms[x])forxin[train,valid]}dataloaders{x:torch.utils.data.DataLoader(image_datasets[x],batch_size32,shuffleTrue,num_workers4)forxin[train,valid]}dataset_sizes{x:len(image_datasets[x])forxin[train,valid]}class_namesimage_datasets[train].classesprint(fClasses:{class_names})print(fTraining set size:{dataset_sizes[train]})print(fValidation set size:{dataset_sizes[valid]})2. 模型改造加载预训练模型并“换头”这是迁移学习的核心步骤。defget_model(use_pretrainedTrue,fine_tuneFalse): 加载预训练ResNet-18并替换分类器。 Args: use_pretrained: 是否加载ImageNet预训练权重 fine_tune: 是否微调卷积层参数。False表示只训练新添加的分类层。 # 加载预训练模型modelmodels.resnet18(weightsIMAGENET1K_V1ifuse_pretrainedelseNone)ifnotfine_tune:# 特征提取模式冻结所有卷积层参数forparaminmodel.parameters():param.requires_gradFalseelse:# 微调模式所有参数均可训练forparaminmodel.parameters():param.requires_gradTrue# 获取原始全连接层的输入特征数 (ResNet-18 是 512)num_ftrsmodel.fc.in_features# 替换为一个新的分类器输出为2猫/狗# 这里可以设计得更复杂例如添加Dropout层防止过拟合model.fcnn.Sequential(nn.Dropout(0.5),# 添加Dropout对小数据集尤其重要nn.Linear(num_ftrs,2))returnmodel# 初始化模型第一阶段先不微调卷积层modelget_model(use_pretrainedTrue,fine_tuneFalse)devicetorch.device(cuda:0iftorch.cuda.is_available()elsecpu)modelmodel.to(device)print(model)# 可以打印看看模型结构确认fc层已被替换3. 训练策略区别对待不同参数由于我们冻结了卷积层只需要对新添加的fc层进行较大学习率的训练。# 只收集需要梯度更新的参数params_to_update[]forname,paraminmodel.named_parameters():ifparam.requires_grad:params_to_update.append(param)print(f\t{name})# 打印出实际会被训练的参数# 优化器只作用于这些参数optimizeroptim.Adam(params_to_update,lr0.001)# 损失函数criterionnn.CrossEntropyLoss()4. 训练与验证循环这是一个标准的PyTorch训练循环但要注意模型处于train或eval模式时Dropout和BatchNorm层的行为不同。deftrain_model(model,criterion,optimizer,num_epochs10):forepochinrange(num_epochs):print(fEpoch{epoch}/{num_epochs-1})print(-*10)# 每个epoch都有训练和验证阶段forphasein[train,valid]:ifphasetrain:model.train()# 设置模型为训练模式else:model.eval()# 设置模型为评估模式running_loss0.0running_corrects0# 迭代数据forinputs,labelsindataloaders[phase]:inputsinputs.to(device)labelslabels.to(device)# 清零梯度optimizer.zero_grad()# 前向传播# 只在训练阶段跟踪历史计算图withtorch.set_grad_enabled(phasetrain):outputsmodel(inputs)_,predstorch.max(outputs,1)losscriterion(outputs,labels)# 反向传播 优化仅在训练阶段进行ifphasetrain:loss.backward()optimizer.step()# 统计running_lossloss.item()*inputs.size(0)running_correctstorch.sum(predslabels.data)epoch_lossrunning_loss/dataset_sizes[phase]epoch_accrunning_corrects.double()/dataset_sizes[phase]print(f{phase}Loss:{epoch_loss:.4f}Acc:{epoch_acc:.4f})print()returnmodel# 开始训练特征提取阶段modeltrain_model(model,criterion,optimizer,num_epochs5)5. 进阶解冻部分层进行微调当新分类头训练得差不多了我们可以解冻骨干网络的最后几层用更小的学习率进行微调使特征更适应我们的数据。# 微调最后两个卷积块layer3和layer4forname,paraminmodel.named_parameters():# 在ResNet中layer3和layer4是更深层的模块iflayer3innameorlayer4innameorfcinname:param.requires_gradTrueelse:param.requires_gradFalse# 微调时使用更小的学习率避免破坏已有的好特征optimizer_ftoptim.Adam(filter(lambdap:p.requires_grad,model.parameters()),lr1e-5)# 继续训练几个epochmodeltrain_model(model,criterion,optimizer_ft,num_epochs5)踩坑记录归一化参数不一致这是最容易掉进去的坑。ImageNet预训练模型有固定的均值和标准差[0.485, 0.456, 0.406],[0.229, 0.224, 0.225]。如果你用自己的均值和标准差做归一化相当于把模型扔进了一个它不认识的“颜色空间”效果会大打折扣。务必使用与预训练时一致的参数。学习率设置不当对于新添加的层应该使用相对较大的学习率如0.001对于微调的预训练层一定要用很小的学习率如1e-5。因为预训练权重本身已经很好我们只想对它做细微的调整。一个常见的策略是使用torch.optim中的param_groups为不同层设置不同的学习率。忘记切换模型模式训练前model.train()验证/测试前model.eval()。这不仅影响Dropout和BatchNorm的行为在某些情况下还会影响计算图的内存占用。我曾在验证时忘记eval()导致GPU内存暴涨百思不得其解。数据量太少导致过拟合即使用了迁移学习如果自己的数据量极少比如每类几十张新分类头也容易过拟合。务必使用数据增强如随机裁剪、翻转、色彩抖动并在新分类头中添加Dropout层。监控验证集精度如果训练集精度持续上升而验证集精度下降就是过拟合的典型信号。盲目微调所有层对于小数据集微调所有层反而可能导致模型“忘记”预训练时学到的通用特征从而在验证集上表现变差。建议采用渐进式解冻先冻结所有层训练新头然后解冻最后1-2个阶段进行微调观察验证集效果再决定是否解冻更多。效果对比为了直观感受迁移学习的威力我们可以做一个简单的对比实验实验A从头训练初始化一个没有预训练权重的ResNet-18用我们的小数据集训练20个epoch。实验B迁移学习-特征提取如上文所述加载预训练权重冻结卷积层只训练新分类头5个epoch。实验C迁移学习-微调在B的基础上解冻深层网络微调5个epoch。在我的实验中每类1000张训练图200张验证图结果大致如下实验A最终验证精度约70%~75%训练过程波动大收敛慢。实验B仅5个epoch验证精度轻松达到95%。这充分证明了预训练特征的强大。实验C精度可能提升到97%~98%提升幅度取决于数据集与ImageNet的相似度。这个对比清晰地展示了在数据有限的情况下迁移学习能以极小的代价训练时间和数据量获得接近甚至超越大数据训练的效果真正实现了“站在巨人肩膀上”。迁移学习是AI工程师工具箱中的必备利器。掌握它意味着你能在资源受限的真实业务场景中快速交付高质量的模型。希望这个实战项目能帮你打通任督二脉。接下来我们将探讨如何将这些模型部署到生产环境。如有问题欢迎评论区交流持续更新中…

更多文章