ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ResNet-50猫狗分类训练全记录:预训练权重、数据增强与调参避坑

ResNet-50猫狗分类训练全记录:预训练权重、数据增强与调参避坑 很早以前我用VGG16跑过猫狗分类后来换到ResNet-50第一反应是这活儿真没想象中简单。表面上看只是二分类但实际训练中涉及预训练权重、数据增强、损失函数、学习率策略、显存占用这些问题任何一个环节偷懒模型就会用“loss降不下去”或者“验证集准确率在原地抖动”来回应你。这篇文章就把我自己的实操过程完整拆开围绕基于ResNet-50的猫狗分类训练从选型、数据准备、代码实现到调参避坑按真实项目里的推进顺序写出来给正准备动手训练图像分类模型的朋友一个可以直接落地的参考。1. 为什么ResNet-50是猫狗二分类的稳妥选择1.1 二分类任务也有隐藏难点猫狗分类看起来是“区分两张图片里是猫还是狗”但如果把任务拆细你会发现难点比预想的多猫和狗都有竖耳、圆眼、毛茸茸的特征背景可能包含其他动物玩具图片里可能出现两只动物同一品种不同姿势差异很大。二分类的类别数少不意味着网络可以随便乱配。目标是在测试集上做到高准确率而不是把训练集背下来。ResNet-50是图像分类领域被反复验证过的骨干网络。相比更深的ResNet-101、ResNet-152它的参数量适中在单卡训练时更容易控制显存相比VGG16它的结构在相同深度下计算量更小同时通过残差连接解决了深层网络退化问题。对猫狗分类这种“入门级迁移学习”场景ResNet-50的效果和训练成本处在非常舒服的平衡点。1.2 残差结构到底解决了什么问题ResNet-50的核心是残差模块每个模块学习的是“输入和输出的差值”而不是直接拟合完整的底层映射。这里我用一个不太严谨但很好理解的说法普通网络每一层都在努力把输入变换成输出层数深了以后梯度在反向传播中容易消失前几层基本学不到东西残差网络给数据开了一条“高速公路”梯度可以沿着恒等映射直接传回去所以即使堆到50层网络依然能稳定训练。在猫狗分类中图片的高层语义耳朵形状、脸型、毛色和低层纹理毛发方向、背景边缘都需要被同时捕捉。深层的ResNet-50有能力表达更复杂的特征组合这是它优于浅层网络的地方。同时得益于ImageNet预训练权重实际训练中我不会从零开始学习“如何看一张图”而是从“已经能识别1000类物体”的起点继续适应当前的猫狗数据。1.3 预训练模型决定了训练起点训练深度网络时最容易被忽略的一件事是初始权重。如果随机初始化ResNet-50靠一张单卡去训练猫狗分类通常需要几十个epoch才能看到可用效果而且容易陷入局部最优。使用在ImageNet上预训练过的resnet预训练模型相当于模型已经学会了通用的边缘、纹理、形状特征我们只需要微调最后的分类层和部分高级特征层。PyTorch的torchvision提供了现成的ResNet-50预训练权重代码里一句weightsResNet50_Weights.IMAGENET1K_V2就能拿到。这个V2版本比V1在ImageNet上准确率更高做迁移学习时的起点也更好。我第一次用的时候差点踩坑老代码写的是pretrainedTrue新版torchvision里已经标记为弃用虽然还能跑但建议直接用新写法避免以后升级依赖时突然报错。2. 训练前先想清楚三件事数据、环境与评估口径2.1 数据集目录结构直接影响DataLoader猫狗分类最常见的数据集是Kaggle的Dogs vs Cats原始目录往往是一堆图片文件名带cat.0.jpg和dog.0.jpg这种前缀。如果直接写一个自定义Dataset去解析文件名不是不行但后续增加标签、替换数据都要改代码。我建议先整理成标准目录结构data/ train/ cat/ cat.0.jpg cat.1.jpg dog/ dog.0.jpg dog.1.jpg val/ cat/ dog/torchvision.datasets.ImageFolder可以直接读取这种结构免去手写图片路径和标签映射的麻烦。整理数据这一步看似琐碎实际上能避免很多低级bug。曾经我把训练集和验证集放在同一个父目录下ImageFolder会把子文件夹都当成类别最后训练集里混入了验证集图片准确率虚高得一塌糊涂。2.2 数据增强按需选择不是越多越好训练集只有两万张左右图片时数据增强是防止过拟合的重要工具。但很多人喜欢把所有增强手段都堆上去随机裁剪、水平翻转、旋转、颜色抖动、随机擦除结果训练集和验证集的分布严重不一致模型学了一堆“变形图片”的规律真正预测时反而不稳。我自己的猫狗分类训练配置如下数据集增强方式说明训练集RandomResizedCrop(224)、RandomHorizontalFlip、ColorJitter模拟不同尺寸和拍摄角度避免模型依赖位置信息训练集Normalize(mean, std)ImageNet标准化配合预训练权重使用验证集Resize(256)、CenterCrop(224)、Normalize验证时不做随机增强保证评估结果稳定实际使用中我一般不轻易加旋转因为猫狗照片里倒置图片很少过度的旋转增强反而会引入不真实的样本。水平翻转是比较安全的增强方式因为手机拍猫狗时左右翻转并不改变类别语义。2.3 训练环境与可复现性训练环境这块我建议在项目一开始就固定好依赖版本。以PyTorch为例我用的版本组合是torch 2.1.0、torchvision 0.16.0、Python 3.10、CUDA 11.8。不同版本之间虽然差异不大但预训练权重的加载方式和部分API行为可能变化踩到坑后很难判断是代码问题还是环境问题。显存方面ResNet-50在batch_size32、输入尺寸224x224时训练显存占用大约在7到10GB之间取决于是否开启混合精度和梯度检查点。我的显卡是12GB显存开混合精度后可以轻松训练。如果显存只有8GB建议把batch_size降到16同时配合梯度累积效果上差别不会太大。conda create -n catdog python3.10 conda activate catdog pip install torch2.1.0 torchvision0.16.0 --index-url https://download.pytorch.org/whl/cu118 pip install matplotlib tensorboard tqdm另外强烈建议在代码开头固定随机种子否则每次训练结果都会有细微差异排错时会多出很多不确定因素。2.4 评估口径不能只看训练集准确率猫狗分类项目最常见的汇报指标是准确率但准确率只有在类别均衡时才有意义。如果训练集里猫比狗多20%模型全部预测成猫也能拿到较高的准确率但这显然不是我们想要的。我在验证时通常同时看三样东西总准确率、猫和狗各自的召回率、以及loss曲线。loss曲线尤其重要。训练集loss不断下降但验证集loss开始上升说明过拟合两者都不降说明学习率可能过高或模型没有收敛验证集loss震荡剧烈则可能是batch_size太小或学习率偏大。准确率是一个“结果数字”loss才是帮你定位问题的过程信号。3. 核心代码拆解从加载模型到训练循环3.1 加载ResNet-50预训练模型并改造分类头用PyTorch实现时核心逻辑是先加载预训练模型然后替换最后一层全连接层让输出变成2类。原来的ImageNet分类是1000类最后一层是(2048, 1000)的线性层我们把它换成(2048, 2)。import torch import torch.nn as nn from torchvision import models, transforms def create_model(num_classes2): weights models.ResNet50_Weights.IMAGENET1K_V2 model models.resnet50(weightsweights) # 替换分类头 in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes) return model这里有一个细节替换分类头后新初始化的全连接层权重是随机的而前面所有层还保留ImageNet的预训练权重。第一次反向传播时随机初始化的分类头会产生较大梯度如果学习率设得太高很容易把主干网络好不容易学到的特征破坏掉。所以迁移学习的经典做法是先冻结主干只训练分类头跑几个epoch后再解冻所有层进行微调。def freeze_backbone(model, freezeTrue): for param in model.parameters(): param.requires_grad not freeze # 最后一层保持可训练 for param in model.fc.parameters(): param.requires_grad True3.2 冻结训练与全量微调怎么选我在猫狗分类项目里实际采用了两阶段策略第一阶段冻结ResNet-50所有卷积层主干只训练新加的fc层学习率设为1e-3跑3个epoch。这个阶段模型训练速度很快显存占用也低主要目的是让分类头快速适应猫狗数据的特征分布。第二阶段解冻全部层使用较小的学习率1e-5到5e-5进行全量微调跑5到10个epoch。此时模型会针对猫狗任务微调底层特征但因为学习率很小不会大幅破坏预训练权重。为什么第二阶段的learning rate必须这么低因为预训练模型已经处于一个“很成熟”的参数空间你用大学习率去更新它相当于把一个经验丰富的质检员拉去重新培训并用高压手段强迫他改习惯效果往往适得其反。用小学习率微调是让他在原有知识基础上再学一点猫狗专属特征。3.3 完整训练循环训练循环本身不复杂复杂的地方在于正确处理模型训练/验证状态、梯度清零、反向传播和指标统计。下面我给出一个精简但完整的版本import torch.optim as optim from torch.utils.data import DataLoader from torchvision import datasets, transforms from tqdm import tqdm device torch.device(cuda if torch.cuda.is_available() else cpu) model create_model().to(device) # 数据加载 transform_train transforms.Compose([ transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), transforms.ColorJitter(0.4, 0.4, 0.4), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) transform_val transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) train_dataset datasets.ImageFolder(data/train, transformtransform_train) val_dataset datasets.ImageFolder(data/val, transformtransform_val) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue) criterion nn.CrossEntropyLoss() optimizer optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4) def train_one_epoch(model, loader, optimizer, criterion): model.train() total_loss, correct, total 0, 0, 0 for images, labels in tqdm(loader, desctrain): images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * images.size(0) correct (outputs.argmax(1) labels).sum().item() total labels.size(0) return total_loss / total, correct / total def validate(model, loader, criterion): model.eval() total_loss, correct, total 0, 0, 0 with torch.no_grad(): for images, labels in tqdm(loader, descval): images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) total_loss loss.item() * images.size(0) correct (outputs.argmax(1) labels).sum().item() total labels.size(0) return total_loss / total, correct / total这里我还用了torch.cuda.amp混合精度。显存吃紧时混合精度几乎是今天训练ResNet-50的默认选项只需要在forward和backward外面包一层autocast再用GradScaler缩放梯度。加上之后训练速度能提升30%以上显存占用也会下降。3.4 为什么损失函数默认用交叉熵对于猫狗二分类输出层有两个节点PyTorch的CrossEntropyLoss内部已经做了Softmax计算不需要在模型最后再手动加Softmax。这个细节经常被刚入门的人忽略如果模型输出层加了Softmax再用CrossEntropyLoss等于做了两次Softmax训练会变得奇怪预测概率也会被“压平”。交叉熵的梯度形式对概率分布非常敏感配合ResNet-50的最后一层线性输出能让模型在训练初期快速学会区分猫和狗而不是慢慢逼近。实际跑下来第一阶段3个epoch后验证集准确率就能到95%以上这就是预训练模型加交叉熵的威力。4. 实测调参经验与避坑记录4.1 loss不降时先排查数据还是模型我第一次训练时遇到过loss在0.69附近徘徊不上不下的情况。0.69正好是二分类交叉熵的“随机猜测”数值说明模型完全没学到有效信息。当时第一反应是换模型后来静下心排查发现是数据加载出了问题ImageFolder读取后发现train和val目录里图片几乎一样导致验证集“泄漏”到训练集模型虽然把训练集背下来了但验证时表现的loss完全不下降。所以遇到loss不降我建议按这个顺序排查先检查数据目录结构是否按预期划分训练集和验证集是否存在交叉打印一个batch的images和labels人工看图片是否正确、标签是否对应检查预处理有没有把图片“归一化坏”例如mean/std填错会让输入范围偏移检查模型是否加载了预训练权重随机初始化的ResNet-50收敛速度会慢很多检查学习率是否过高或过低过高会导致loss震荡不降过低会导致收敛极其缓慢。4.2 过拟合与欠拟合的判断方法在猫狗分类训练中我习惯画一张训练集和验证集的loss曲线图。如果训练集loss持续下降、验证集loss在第4个epoch后开始上升说明模型开始过拟合。应对手段按优先级排序手段适用时机我的设置建议数据增强验证集波动大RandAugment或ColorJitterDropout分类头过拟合在fc前加Dropout(0.2)权重衰减参数震荡weight_decay1e-4提前停止验证集loss连续3个epoch不降保存最佳模型不保存最后一轮降低学习率后期loss明显不降ReduceLROnPlateau或CosineAnnealing如果训练集和验证集准确率都很低则是欠拟合这时不要急着堆数据增强优先考虑训练更多epoch、适当调大学习率、解冻更多层。最常见的欠拟合原因是第一阶段冻结主干后只训练分类头但只跑了很少的epoch就急着下结论。分类头需要先收敛主干微调的效果才会体现。4.3 batch_size、显存与收敛速度的权衡我在训练ResNet-50时batch_size的选择是根据显卡显存动态调整的12GB显存batch_size32输入224x224开混合精度8GB显存batch_size16开混合精度必要时开梯度累积6GB显存batch_size8同时使用梯度累积让等效batch_size保持32。这里有个容易忽略的点batch_size过小时BatchNorm层的统计量会很不稳定因为每个batch的均值和方差波动大导致训练震荡。ResNet-50里有大量BatchNorm所以我不建议batch_size低于8。如果显存实在小也可以通过固定图片分辨率为192x192来降低显存但会损失部分精度。4.4 常见问题排查表把实际项目中遇到的坑整理成表格方便大家直接对照现象可能原因解决方法训练loss一直0.69附近数据标签错乱或数据加载有误打印batch检查验证准确率高但测试集上表现差验证集划分与训练集重叠重新划分数据集模型训练速度很慢未开混合精度、num_workers太小开启cuda.amp并行加载显存不足batch_size过大、输入尺寸过大降低batch_size、缩小输入尺寸训练后期准确率抖动学习率过大使用学习率衰减冻结主干时准确率上不去分类头训练epoch不足先多跑几个epoch再解冻5. 训练完成后的下一步优化思路5.1 从ResNet-50转换到轻量模型ResNet-50训练完成后如果要做移动端或边缘设备部署模型体积约98MB和推理速度都不算理想。这时可以把它当作“教师模型”用知识蒸馏去训练一个MobileNetV3或EfficientNet-Lite学生模型。实操上就是把教师模型在训练集上的softmax输出作为额外监督信号让学生模型不仅学习真实标签还学习教师模型的“软标签”。不过如果你的目标只是跑通一个课程项目或验证想法ResNet-50本身已经足够不需要额外上蒸馏。知识蒸馏的收益在数据量少、任务难度高时更明显猫狗二分类在预训练模型加持下已经能取得很高准确率蒸馏收益不会太大。5.2 数据质量永远比模型结构更值得投入我在多次实验后有个很深的体会与其花大量精力调模型结构不如先把数据清洗干净。猫狗分类数据集中经常存在标注错误、模糊图片、包含多种动物的图片。训练前我会人工抽样检查每个目录下的图片把明显标注错误的图片移除或重新标注。一个错误标注的数据点会让模型在对应区域产生混乱而错误标注比例超过1%时训练出的模型性能上限会被显著拉低。数据清洗完成后还有一个性价比很高的操作用训练好的ResNet-50对训练集再次预测找出预测置信度在0.5到0.7之间的“模糊样本”人工复查这些样本的标签。这些样本往往是标注错误或任务本身难以区分的数据修正它们对模型提升最明显。5.3 模型导出与后续使用训练完成后我通常会把PyTorch模型导出为TorchScript或ONNX格式方便部署和跨平台调用。导出时要注意把模型切到eval()模式并固定输入尺寸model.eval() dummy_input torch.randn(1, 3, 224, 224).to(device) traced_model torch.jit.trace(model, dummy_input) traced_model.save(resnet50_catdog.pt)这里有个细节如果训练时用了混合精度导出前最好把模型权重转为FP32避免推理时精度不一致。FP16版本的模型虽然体积更小、速度更快但如果你在CPU上推理FP16反而可能更慢需要根据实际平台选择。另外我建议把验证集上表现最好的权重保存下来而不是简单保存最后一个epoch。实际操作中我会在每个epoch结束后比较验证集准确率只在验证集指标变好时覆盖保存。这样即使后面训练过拟合也能从容地回退到最优模型不需要重新训练。有一次我帮朋友复现这个项目他跑完一个完整流程后问我“为什么准确率已经到98%了但自己拍的照片还是经常识别错”我让他把那几张照片发过来一看全是逆光、模糊、猫和狗在画面里同时出现。这问题不在模型而在拍摄场景和模型训练数据分布不一致。训练时模型见过的大多是主体清晰、光照正常的图片到了真实环境自然会有落差。后来我养成了一个习惯训练完不要只盯验证集数字而是从网上找几张没见过的猫狗照片甚至用手机随手拍几张直接送到模型里预测看看真实表现如何。这一步能帮你发现很多“指标完美但实际不可用”的问题。我希望这篇基于ResNet-50的猫狗分类训练记录能让你少走一点弯路也欢迎你按自己的数据把完整流程跑一遍踩过坑之后对图像分类的理解会完全不一样。
返回列表