资讯动态

图像分类实战:基于ResNet18迁移学习的全流程优化复盘

发布时间:2026/10/18 5:02:00 来源:尧图企业网站定制
20260120这周的人工智能课下发了第二次作业要求在一个十类图像分类数据集上完成从数据准备、模型训练到结果分析的全流程。第一次提交的v1版本我图省事直接搭了一个四层卷积网络训练30轮后训练集准确率刷到98%验证集却只有78.3%连85%的合格线都没碰到被助教打回重做。20260121我重新提交的v2版本验证集准确率提到89.6%测试集88.6%。这篇就把v2从数据划分、增强策略到训练调参、结果分析的整个过程完整复盘重点讲清楚每一步“为什么这么做”。如果你也在做类似的图像分类作业或者刚接触深度学习想搞明白一套规范训练流程这里的内容应该能帮你少踩几个坑。1. 作业v2的项目目标与技术选型1.1 v1提交为什么被打了回来先复盘v1失败的原因。当时作业要求有三条用卷积神经网络完成分类、训练轮次不少于30轮、提交训练曲线、混淆矩阵和1500字分析。v1我直接手写了一个conv-conv-pool-conv-conv-pool-fc-fc结构的网络没有做任何数据标准化也没有数据增强优化器用的还是固定学习率的SGD。结果训练集loss很快降到接近0训练准确率一路冲到98%但验证集准确率稳定在78%附近。这是最典型的“把训练集背下来了却没见过验证集”的过拟合状态。深入复盘后v1有三个致命问题。第一没有把图像像素归一化到合适范围原始0到255的像素值直接进入网络输入分布漂移大模型收敛慢第二训练样本根本没有做随机裁剪、翻转之类的增强模型把每张图的精确位置、颜色、角度全记死了第三学习率恒定是0.001训练后期既不衰减也不做任何调度loss在低位来回震荡模型跳不出局部平缓区。助教反馈也直指过拟合要求针对这个点上交新方案。所以v2的目标非常明确在单张8GB显存显卡、单次训练不超过40分钟的前提下把验证集准确率稳定做到85%以上同时能清晰说明模型在哪些类别上犯错、为什么会犯错。这个目标决定了我不可能再花时间从零设计网络结构而是直接把迁移学习作为核心方案。1.2 为什么选ResNet18做骨干网络当时备选的预训练模型有三个ResNet18、ResNet50和MobileNetV3。ResNet50在ImageNet上的分类准确率确实高出一两个点但参数量达到2500万左右训练时间和显存开销都明显上涨在40分钟的训练时长限制下会很紧张MobileNetV3轻量、推理快但它的结构相对特殊预训练权重的泛化表现也不如ResNet稳定写作业分析时解释起来也不够直观。综合下来我选了ResNet18。ResNet18参数量只有约1100万8GB显存跑起来很从容。torchvision里有现成的ImageNet预训练权重加载一行代码就能搞定。残差结构让梯度在深层网络中传播得很顺畅不容易出现训练不收敛的问题。更重要的是迁移学习在小样本图像分类上是已经被反复验证的正解预训练模型已经掌握了边缘、纹理、形状、颜色渐变的通用表征我们只需要在具体类别上做“微调”不需要从零学习什么是叶子、什么是病斑。这套思路让v2的起点比v1高了一大截。2. 数据处理与增强策略2.1 数据集划分不要只按目录顺序切拿到数据之后的第一件事永远不是写模型而是先把数据分布搞清楚。这个十类数据集总共约7000张图片类别分布很不均匀最多的两个类各有900张最少的两类只有450张左右。如果按文件夹顺序直接取前70%做训练验证集和测试集的类别分布会严重失真有可能一整批样本里都缺某一类。这种“验证集被污染”的情况会让后续所有调参结论都失去意义。我在代码里用sklearn的train_test_split并且加了stratify参数按类别比例进行分层采样。划分比例是训练集70%、验证集15%、测试集15%算下来训练集约4900张验证集和测试集各约1050张。划分完成后我把三个子集的类别分布直方图都打印出来确认和全量数据的分布一致才算通过。整个过程代码量不大但这一步能避免后面好几个小时的无效调参。还有一个原则验证集和测试集必须严格分开测试集只能最后评估一次不能中途拿测试集结果来调任何参数。所有“看结果、调学习率、改增强策略”的动作都只盯着验证集。v1里我没有单独拆测试集全用验证集调参导致最后提交的“测试集准确率”其实是被验证过的结果说服力大打折扣。2.2 数据增强的具体配置v2在数据增强上花的时间最多。训练集用的transforms组合是这样的先把图片随机缩放到比224略大的尺寸再随机裁剪到224乘224这一步模拟同一物体在不同距离和不同构图下的拍摄情况再做随机水平翻转模拟镜像视角接着以一定概率做随机旋转15度同时调整亮度、对比度和饱和度。这些增强操作每一项都有实际意义不是随手堆叠。这里尤其要注意归一化参数的选择。我直接用了ImageNet预训练权重对应的标准值mean为[0.485, 0.456, 0.406]std为[0.229, 0.224, 0.225]。为什么不用自己数据集算出来的均值和方差因为预训练模型的权重是在这套归一化参数下训练出来的沿用相同的统计值能让输入分布和预训练时对齐迁移效果最好。只有当你自定义数据和ImageNet的自然图像差异极大时才需要考虑重新统计mean和std。训练后期我还加了Mixup增强。具体做法是按lambda比例把两张输入图像逐像素叠加标签也按同样比例混合。这个操作表面上会让模型看到“不像真实图片”的输入但实际效果是强正则化模型不再依赖单张图片的精确特征而必须从混合内容中学到更本质的模式。在v2里加上Mixup后验证集准确率涨了约1.2个百分点。代价是训练初期loss下降变慢这是预期中的不需要额外干预。2.3 类别不均衡的应对类别不均衡在这个数据集里客观存在我用了WeightedRandomSampler来处理。先计算每个类别样本数倒数作为采样权重样本少的类别被抽中的概率更高这样每个batch里各类别比例趋于均衡模型不会因为某个类别样本太少而放弃学习它。实现时就是把DataLoader的sampler参数指向这个采样器。不过WeightedRandomSampler不是万能的。它本质上是让少数类反复出现、多数类有概率被跳过加大了训练数据的冗余。这个数据集里最少的类与最多的类样本量比值是450比900差距不算极端所以我把权重压缩了一下没有让少数类被极端放大只是保证每个epoch下来最少类别能被完整过一遍。如果你的任务里类别差距达到1比10以上应该优先考虑补数据或者做针对性的类别级增强而不是完全依赖采样器。3. 模型训练与调参实现3.1 模型改造与冻结策略从torchvision加载ResNet18预训练权重后原来最后一层全连接是输出1000类需要改成10类。我在替换fc层的同时加了一点小改动把后接的线性层维度改为512到10并在中间插入一个Dropout层概率设为0.2。原因很简单fc层集中了整个网络相当一部分参数量在小数据集微调时是最容易过拟合的位置加Dropout能强制特征表达不过度依赖某一个神经元的响应。冻结策略同样是基于迁移学习的常规做法。前9个epoch我把整个backbone的requires_grad设为False只训练fc层以及最后一个layer4残差块。这样做让随机初始化的分类头先在稳定特征上找到合适的方向避免backbone在一开始就被反向传播里偏大的梯度“带偏”。等分类头基本收敛后第10个epoch开始再解冻全网络用更小的学习率做整体微调。这个两阶段策略在ResNet上经过大量验证效果稳定也是很多开源训练库的默认做法。代码上需要注意优化器参数分组。前9个epoch直接构建AdamW优化器只把fc参数传进去第10个epoch重新构建优化器把backbone和fc参数都放进去并给backbone一个更低的学习率。如果中途直接在原优化器上追加参数组需要手动管理参数组的顺序和调度范围容易出问题重新构建优化器反而是最清晰的做法。import torch import torch.nn as nn from torchvision import models model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) in_features model.fc.in_features model.fc nn.Sequential( nn.Dropout(0.2), nn.Linear(in_features, 10) ) # 阶段1冻结backbone只训练fc和layer4 for name, param in model.named_parameters(): if not name.startswith(fc) and layer4 not in name: param.requires_grad False optimizer_stage1 torch.optim.AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr1e-3, weight_decay0.05 ) # 训练到第9个epoch后执行阶段2 for p in model.parameters(): p.requires_grad True optimizer_stage2 torch.optim.AdamW([ {params: model.fc.parameters(), lr: 1e-3}, {params: [p for n, p in model.named_parameters() if fc not in n], lr: 1e-4} ], weight_decay0.05)3.2 优化器与学习率调度细节决定上限v2的优化器我选了AdamW而不是传统的Adam。AdamW和Adam的核心差异在于权重衰减的实现位置AdamW把权重衰减从梯度更新步骤中解耦出来避免Adam在自适应学习率机制下对权重衰减的缩放产生偏差。简单理解AdamW让正则化更“给力”在CNN微调场景下也更稳。这里的weight_decay我设置为0.05初始学习率0.001。有一点特别值得注意不同参数组不能共用一个学习率。fc层是随机初始化的需要比较快的学习率去适应新任务我给的是0.001backbone本身已经在大规模数据上预训练过再用0.001去刷很容易把学到的通用特征“洗掉”所以backbone部分用0.0001。这个经验是从实际对比里来的我第一次v2实验里全部参数都用0.001跑了12个epoch发现验证集准确率涨到82%就再也不动了改成分组学习率之后才突破85%。学习率调度选的是CosineAnnealingLRT_max设为30正好覆盖整个训练周期eta_min设为5e-6。余弦退火的思路是让学习率从一个较高值平滑下降到一个很低的值整个过程中的“探索”和“收敛”阶段分配得比较自然。相对固定学习率来说这套调度让验证loss在末端更平稳不再频繁跳变。实际操作中我没有用ReduceLROnPlateau这种按指标表现来降低学习率的策略因为按步数调度的余弦退火不需要人工介入复现性也更好。3.3 混合精度、EMA与训练循环训练过程里我全程启用了PyTorch的自动混合精度。所谓混合精度就是让前向传播和反向传播中能使用fp16的部分用fp16计算关键权重更新等部分仍用fp32从而在几乎不损失精度的情况下大幅节省显存和时间。我在GTX 2080S这张8GB显存的卡上实测开启amp后单轮训练时间压缩约35%显存占用下降约1.5GB。这直接决定了batch size能不能从32提到64。EMA指数移动平均是我在v2里加上的另一个细节。思路是维护一份模型参数的滑动平均版本每个epoch把历史参数和新参数按0.999与0.001的比例做加权平均推理时使用这份平均后的参数而不是当前epoch的参数。EMA相当于把一个训练轨迹上的多个状态叠加起来减少了单次权重更新带来的抖动。在v2里EMA版本的模型在验证集上比当前epoch版本又涨了约0.6%。代价是实现代码多二十行但换来稳定提升非常划算。训练循环的主流程并不复杂每个epoch内部遍历训练集DataLoader前向得到loss反向传播更新梯度再更新EMA参数每个epoch结束后在验证集上跑一次评估保留验证准确率最高的那一次权重作为最终提交候选。由于用了amp这里必须注意梯度缩放器的调用顺序这个细节放在第4节重点讲。scaler torch.cuda.amp.GradScaler() best_acc 0 for epoch in range(total_epochs): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() with torch.cuda.amp.autocast(): outputs model(images) loss criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() ema_model.update(model) # 每个epoch后验证 val_acc evaluate(ema_model if use_ema else model, val_loader) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth)4. 训练过程异常与排查记录4.1 验证loss先降后升别被准确率骗了训练中有个阶段让我印象特别深从第8个epoch开始训练loss持续下降验证集准确率也在缓慢上升但验证集的loss曲线却出现了反弹。这时候如果只看准确率指标很容易误判模型还在正常收敛。实际上验证loss上升是过拟合开始的信号说明模型开始对训练集细节过度拟合泛化能力到达瓶颈。我当时的处理是把选择模型的依据从“最后epoch的权重”改成“验证集上性能最好的权重”也就是早停的核心逻辑。同时我开始观察EMA版本模型的表现发现EMA的验证loss曲线比当前权重平滑很多这也是为什么最终提交使用的是EMA版本。以后你在训练时也建议同时打印训练loss和验证loss不要只看准确率两条曲线的相对走势才是最诚实的指标。4.2 显存溢出从直接爆掉到平稳训练v2刚把batch size提到64的时候我正好还没开amp一上来就报CUDA out of memory。解决思路是先确认显存占用来自哪里输入图片尺寸是224乘224ResNet18的中间特征图在中层会较大batch size乘以通道数再乘以分辨率显存占用是线性上升的。我当时给了两个方案一是把batch size降到32二是开启amp混合精度。实际都试了amp效果更好同一个batch size下既能训练又能节省时间。如果你也遇到显存不足排查顺序建议是这样先看有没有开amp再看PIN MEMORY是不是设成了True导致CPU侧占用过多最后才降batch size。同时把DataLoader里的num_workers调整到合适数值避免每个epoch开始时内存突然飙升。v2里我把batch size定在64num_workers设4这个组合在8GB显存上运行稳定整个训练大概35分钟跑完30个epoch。4.3 准确率卡在85%的突破路径v2第一版改动后验证集准确率其实也就85%出头和合格线贴得很近很不保险。我做了三件事才把分数推到89.6%。第一件事是加强数据增强从原本只有随机裁剪和水平翻转升级到加入随机旋转、颜色抖动、Mixup这一步大约提升1.8个百分点。第二件事是解除冻结做全网络微调相比只训练fc层整体微调让模型能针对这个数据集调整中高层的语义特征这一步提升了2.3个百分点左右。第三件事是TTA也就是测试时增强推理阶段把每张图同时使用原图和水平翻转后的版本分别预测再把两部分概率取平均。TTA几乎没有成本最后带来约0.4个百分点的提升。这里想特别说一句如果你看到准确率卡在一个平台不要第一反应就是换更大的预训练模型。我一开始也试过换成ResNet50参数量上去了但训练时间接近阈值准确率提升不到1%完全不如优化数据和微调策略来得划算。先检查数据增强强度、是否解冻、学习率调度是否合理这些低成本改动往往才是突破平台的关键。5. 评估分析与结果可视化5.1 混淆矩阵和错误模式v2的评估阶段我重点做了三张可视化训练与验证的loss曲线、准确率曲线、以及测试集上的混淆矩阵热力图。前两张曲线用来直观展示模型收敛过程混淆矩阵则用来做细粒度错误分析。打印出来之后发现模型在“某A类病斑”和“某B类病斑”两个类别之间的混淆程度明显高于其他类别约20%的A类样本被错认成B类B类样本也有约12%被错认成A类。去看具体误判图片之后原因很清楚这两类病斑在外观上本来就很接近早期病斑的纹理和颜色差异极小连人眼都很难区分。这属于数据集的“语义重叠”问题不是单纯靠加深网络就能解决的。我做的调整是增加ColorJitter里对比度和饱和度的扰动范围让模型在训练时被迫关注这类样本中相对稳定的形状和纹理特征而不是颜色这种容易被扰动干扰的低层线索。最后这两类互相误判的比例下降了几个点。5.2 v1与v2的消融对比为了给作业分析报告提供数据支撑我顺带做了一组简单的消融对比把v1、v2以及v2去掉某一项关键配置后的验证集结果列在一起。v1自建网络是78.3%v2完整方案是89.6%。单独去掉Mixup之后是88.4%去掉EMA后是89.0%去掉解冻微调后是87.1%。表格放在分析文档里非常直观比空口说“我加了数据增强所以更好了”有说服力得多。配置验证集准确率测试集准确率v1 自建CNN78.3%77.5%v2 冻结训练fc87.1%86.8%v2 全网络微调89.6%88.6%v2 全网络微调 无Mixup88.4%87.7%v2 完整方案89.6%88.6%这里有个做实验的细节每次消融要用相同的随机种子和相同的训练流程否则结果差异会混入随机性得出的结论不干净。torch里的manual_seed、numpy的seed还有DataLoader的worker初始化种子都设置一遍确保对比实验可复现。5.3 模型导出与提交作业提交前我额外把Pytorch模型导出成了TorchScript格式。TorchScript的好处是推理时不依赖Python环境可以在生产环境里直接加载运行也方便给作业里“实际部署”这部分加分。导出的方式很简单用torch.jit.trace对模型做一次输入追踪再把轨迹保存成.pt文件。同时我把测试时增强的逻辑封装进推理函数提交附件里除了训练代码、最佳权重、TorchScript模型还包括训练曲线图、混淆矩阵图以及1500字分析文档。提交前最后一步是把测试集整体跑一遍输出每张图片的预测类别、真实类别、置信度并把其中预测置信度最低的20张图保存成一个错误case文件夹。这样如果助教问“哪些样本让模型最困惑”我能直接翻出具体例子和数据而不是只给一个准确率数字。整个作业v2做完我最大的体会是深度学习项目里工程化思考和数据意识的重要性完全不亚于看懂模型结构。拿网络结构硬套和把数据、训练策略、评估链路都当成一个整体来对待最终呈现出来的结果差距是巨大的。如果你也在类似作业里卡着先从验证集划分、增强强度和嵌入式的实验记录开始改善比单纯换一个更大的模型要划算得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑