拓冰建站拓冰建站
首页 / 资讯中心 / 正文

PyTorch猫狗分类实战:数据增强、ResNet18与模型验证全解析

简介基于PyTorch与CNN的猫狗图片分类源码面向深度学习者与学生群体适用于课程设计、期末大作业或入门图像分类实践。项目以卷积神经网络完成猫狗图片二分类代码结构完整、带详细注释初次接触深度学习的用户也能读懂同时保留扩展余地适合在此基础上做调参或二次开发。压缩包共13个文件以8个Python脚本为主涵盖数据预处理、模型构建、训练与测试流程另含1个已训练好的pth权重文件、1份Docx说明手册及若干配置文件整体大小仅1.3MB轻量易用。已有274人学习下载。通过源码加文档的组合既能快速复现猫狗分类效果也可对照手册梳理项目思路是完成高分课设或期末作业的可靠参考。1. 期末大作业选猫狗分类拼的不是模型而是细节猫狗图片分类几乎是每个PyTorch学习者都会撞上的题目它出现在期末大作业里的频率远高于其他图像任务。原因不难理解数据集好找、类别只有两个、CNN做这个问题的技术路线成熟老师说“做出来不难”但高分通常属于那些能讲清楚每一步为什么这么做的人。一个常见的判断是用ResNet18做迁移学习并加上数据增强验证集准确率可以轻松超过95%真正拉开差距的是数据怎么组织、训练怎么调、结果怎么验证这三件事。这篇内容按一条完整可复现的路径走从目录整理和DataLoader开始到CNN的两种写法对比再到训练循环里容易踩的坑最后落在混淆矩阵和预测函数上。如果你正在被“网上找的源码跑不通”或者“代码能跑但解释不了”卡住这篇能直接用来交作业。2. 数据集整理与DataLoader分类工程的第一个分水岭2.1 ImageFolder能解决的没必要手写Dataset猫狗分类最常用的做法是先搭Anaconda配置PyTorch环境然后使用torchvision.datasets.ImageFolder。它的前提是数据目录按“类名/图片”结构组织。训练集目录建议直接命名为train/dog和train/cat验证集同理。ImageFolder会自动把dog和cat这两个文件夹名映射成类别索引0和1省去手写标签的麻烦。from torchvision import datasets, transforms from torch.utils.data import DataLoader train_transforms transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_dataset datasets.ImageFolder(train, transformtrain_transforms) valid_dataset datasets.ImageFolder(valid, transformtrain_transforms) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) valid_loader DataLoader(valid_dataset, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue) print(train_dataset.class_to_idx) # {cat: 0, dog: 1}这里没有手写__getitem__因为ImageFolder本身就完成了“按文件夹生成标签”的工作。逻辑说明shuffleTrue在每个epoch开始前打乱数据保证模型不会学习到样本顺序造成的虚假模式验证集不shuffle方便后续统计每个类别的准确率和混淆矩阵。num_workers这个参数在Windows上如果大于0报错多半是Python脚本没有放在if __name__ __main__:里改成0可以临时规避但会拖慢数据加载。2.2 数据增强组合变换的顺序与参数期末作业的数据量如果只有几千张不做过增强的网络很容易过拟合。常见做法是在Compose里加两个随机变换但顺序有讲究先做空间变换再做张量化和归一化否则归一化后图像的数值范围变了再旋转会引入大量无效插值。train_transforms transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale(0.8, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])先Resize(256)再RandomResizedCrop(224)是ImageNet训练时常用的思路相当于让模型每次看到不同构图而不是固定裁剪同一个位置。ColorJitter调节亮度对比度对猫狗这类拍摄环境差异大的任务很有效。注意验证集只需要Resize和CenterCrop不需要随机变换因为评估时要保证结果可复现。有一个容易忽略的细节如果在Windows上跑RandomResizedCrop加上num_workers大于0时偶尔会卡死这通常和PyTorch的数据加载机制有关把num_workers设为0或者换到Linux环境都能解决。2.3 批大小和线程数先看显存再看训练速度batch_size的选择直接影响训练能否跑起来。显卡显存8GB以下ResNet18加224像素输入batch_size32是比较安全的起点显存不足时报错CUDA out of memory优先把batch降到16或8。pin_memoryTrue配合GPU训练能减少CPU到显卡的拷贝时间但开了pin_memory后如果内存不够系统会变得极其卡顿这时关掉它反而更稳。train_loader DataLoader( train_dataset, batch_size16, shuffleTrue, num_workers2, pin_memoryTrue, drop_lastTrue )drop_lastTrue的用途是丢弃最后一个不足batch的批次。原因在于BatchNorm层统计的是当前batch的均值和方差最后一个batch如果只剩几张图统计出来的分布会偏离整体可能导致那一轮loss曲线出现突然下坠或上升。这个参数在验证集上不要开验证集最后一小批样本不需要参与BatchNorm统计一致性。3. 从零搭CNN到ResNet18迁移PyTorch模型的两种写法3.1 手写小CNN结构设计不再拍脑袋期末作业如果完全从零实现CNN网络结构不要过度追求深度。三层卷积加三层的结构已经足够应付二分类任务。一个典型且能跑出不错效果的结构如下import torch.nn as nn import torch.nn.functional as F class SimpleCNN(nn.Module): def __init__(self, num_classes2): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2) ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(128, 256), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(256, num_classes) ) def forward(self, x): x self.features(x) x self.classifier(x) return x这段代码里最关键的设计是AdaptiveAvgPool2d(1)。它把任意尺寸的特征图压缩成1×1这样前面的卷积层不需要关心输入图片是224还是256像素全连接层的输入维度也固定为128。Dropout(0.5)只在全连接层前加卷积层后面不加原因是卷积层的参数量小过拟合风险主要来自全连接层。BatchNorm放在ReLU之前是PyTorch官方示例里的常见顺序实际操作中放在之后也能收敛不要在这上面浪费太多时间。从零训练的代价是准确率上限有限。在不做数据增强时这个结构在Cat vs Dog数据集上大概能到85%~90%但训练耗时比迁移学习长很多。原因是浅层卷积网络需要自己学会边缘、纹理、形状这些低级特征而预训练模型一开始就具备这些能力。适合的场景是老师明确要求“不准用预训练权重”或者“必须手写CNN结构”。3.2 预训练模型一行代码换主干网络高分作业更稳妥的路线是使用torchvision.models里的ResNet18加载ImageNet预训练权重把最后一层全连接替换成2分类。这样做的技术依据是ImageNet学习到的通用视觉特征可以直接复用到猫狗分类任务只需要微调少量参数。from torchvision import models model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) num_features model.fc.in_features model.fc nn.Linear(num_features, 2) # 冻结前四层只训练最后的分类头和部分残差块 for name, param in model.named_parameters(): if layer4 not in name and fc not in name: param.requires_grad Falseweightsmodels.ResNet18_Weights.IMAGENET1K_V1是新版PyTorch推荐写法老代码里写pretrainedTrue会弹出弃用警告。冻结参数的原因是数据量只有几千张时反向传播更新整个网络的几千万参数很容易过拟合而且显存占用和训练时间都会成倍上升。常见做法是只解冻layer4和最后的fc层其他层的特征提取能力保持预训练状态。如果数据量超过两万张可以全部解冻让模型更充分地适配新数据。3.3 模型的可解释性高分作业要能讲出“为什么选它”老师问“为什么用ResNet18不用VGG16”时不能只回答“准确率高”。ResNet18在ImageNet上的top-1准确率比VGG16更高但参数量只有VGG16的十分之一左右。对小数据集来说参数量直接决定过拟合风险这也是ResNet家族在期末作业里比VGG更常见的原因。对比两个模型的实际差异模型参数量输入尺寸特点手写SimpleCNN约240万224×224结构简单好解释准确率上限低ResNet18约1170万224×224残差连接利于梯度传播轻量高效VGG16约1.38亿224×224特征提取强但参数量过大容易过拟合如果作业要求必须展示CNN原理手写的SimpleCNN更适合画结构图如果要求“分类准确率高”ResNet18几乎是不二选择。还有一个容易被忽视的点PyTorch自带模型输出的是未经过Softmax的logits训练时配合nn.CrossEntropyLoss使用内部已经集成了Softmax计算。不要在模型最后一层手动加torch.softmax否则反向传播时数值不稳定损失值可能不降反升。4. 训练循环、损失曲线与超参调优4.1 损失函数和优化器的搭配逻辑分类问题的标准组合是nn.CrossEntropyLoss加Adam优化器。CrossEntropyLoss把模型输出的logits和真实标签作为输入内部先做Softmax再算交叉熵损失数值稳定性比手动分开计算好得多。Adam在深度学习CNN任务里几乎不需要手动调整学习率就能收敛适合期末阶段快速出结果。import torch.optim as optim criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-4, weight_decay1e-4)weight_decay1e-4是L2正则化它的作用是限制权重不能过大、防止模型死记训练集噪声。对猫狗分类这种小数据集来说这个参数比调高dropout更有效。学习率从1e-4起步而不是常见的1e-3因为迁移学习时预训练权重的梯度绝对值偏大学习率太高会一步就把已经学好的特征破坏掉。手写的SimpleCNN可以放心用1e-3因为网络是从零开始训练不存在破坏预训练特征的问题。4.2 学习率衰减加早停比盲目加epochs靠谱固定学习率跑到底的问题在于训练后期loss会陷入平台期模型在最优点附近反复震荡。常见做法是用CosineAnnealingLR让学习率周期性下降或者用ReduceLROnPlateau监控验证集loss连续多轮不下降就自动调低学习率。from torch.optim.lr_scheduler import CosineAnnealingLR scheduler CosineAnnealingLR(optimizer, T_max20, eta_min1e-6)T_max代表半个余弦周期的轮数。实际训练30个epoch时把T_max设为30更合理这样学习率会从初始值平滑降到eta_min动态地“先大步探索再小步收敛”。除了学习率调度早停机制也值得写进代码期末答辩时这是加分项。它的逻辑是验证集准确率连续patience轮没有刷新记录就停止训练并恢复最好的模型权重。best_acc 0.0 patience 5 bad_epochs 0 for epoch in range(30): # 训练一个epoch后执行验证 val_acc run_validation() if val_acc best_acc: best_acc val_acc bad_epochs 0 torch.save(model.state_dict(), best_model.pth) else: bad_epochs 1 if bad_epochs patience: print(early stopping at epoch, epoch) break scheduler.step()早停的价值是节省时间更重要的是在代码里体现“防止过拟合”的思路。这里有个关键细节scheduler.step()的位置必须在每轮验证之后调用如果放在验证之前学习率的变化提前发生早停判断和准确率记录会出现一个周期的错位。torch.save保存的是state_dict而不是整个模型这样加载时更灵活也方便查看参数字典结构。4.3 训练循环里的模式切换model.train()和model.eval()这段代码几乎每个PyTorch实战教程都会强调但期末作业里依然有人出错。model.train()和model.eval()影响的是BatchNorm和Dropout的行为。BatchNorm在训练模式下使用当前batch的均值和方差在评估模式下使用训练阶段积累的全局统计量Dropout在训练模式下随机丢弃神经元在评估模式下不丢弃。def train_one_epoch(model, loader, criterion, optimizer): model.train() total_loss 0.0 for images, labels in loader: 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) return total_loss / len(loader.dataset)optimizer.zero_grad()必须放在前向传播之前。因为PyTorch的梯度是累积的如果不手动清零下一轮batch的梯度会叠加到当前梯度上参数更新方向完全跑偏。loss.item()从gradient tensor里取出Python数值目的是将损失值从计算图里分离出来否则它会一直保留整个反向传播的图内存占用越来越大。训练完做验证时记得用with torch.no_grad():包住前向传播过程。这个上下文管理器关闭了梯度计算推理时节省大量显存也能防止意外修改模型参数。如果不写这两行代码也能跑但用model.eval()却忘记no_grad()验证阶段显存会暴涨小白经常在这翻车。5. 验证阶段的三个加分动作混淆矩阵、分类报告和单图预测5.1 混淆矩阵比准确率更能说明问题准确率只能回答“整体好不好”混淆矩阵能看出“猫被误判成狗”和“狗被误判成猫”分别有多少。期末答辩时一张清晰的混淆矩阵图比口头说“准确率98%”有力得多。用sklearn.metrics.confusion_matrix和seaborn快速生成import numpy as np from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns import matplotlib.pyplot as plt all_preds [] all_labels [] model.eval() with torch.no_grad(): for images, labels in valid_loader: images images.to(device) outputs model(images) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) class_names list(valid_dataset.class_to_idx.keys()) plt.figure(figsize(6, 5)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabelsclass_names, yticklabelsclass_names) plt.xlabel(Predicted) plt.ylabel(True) plt.savefig(confusion_matrix.png, dpi150)torch.max(outputs, 1)中dim1表示在类别维度上取最大值返回的是两个张量第一个是最大值第二个是对应的索引这里只需要索引。把结果保存到两个列表后转换为NumPy数组用于后续sklearn的统计接口。注意类别顺序来自ImageFolder的class_to_idx映射所以猫对应0、狗对应1。同时打印一份classification_report它包含precision、recall、f1-score三项结构清晰。如果猫的recall比狗低说明模型更倾向把猫判成狗可以针对性多收集猫的训练数据或者提高猫类别的loss权重。5.2 单图预测函数期末答辩时的演示利器答辩现场老师常会随机抽一张网站图片或者手机照片要求预测。写一个轻量预测函数输入图片路径输出类别名和模型置信度from PIL import Image def predict_image(model, image_path, class_names, device): model.eval() image Image.open(image_path).convert(RGB) transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) tensor transform(image).unsqueeze(0).to(device) with torch.no_grad(): output model(tensor) prob torch.softmax(output, dim1) confidence, pred torch.max(prob, 1) return class_names[pred.item()], confidence.item() # 示例print(predict_image(model, test_dog.jpg, class_names, device))unsqueeze(0)把单张3×224×224的图片转换为1×3×224×224的batch形式因为模型输入要求是四维张量。softmax转换成概率后每个类别的概率和为1。这里取top-1类别和对应置信度。答辩时如果置信度低于0.6建议说“模型对这个样本不太确定”而不是强行给出结果这种技术诚实度反而会被认可。5.3 一个更完整的自查清单交作业前用这份清单检查一遍能避开绝大多数扣分点。检查项常见扣分原因自查方法训练和验证的transform是否一致验证集用了数据增强打印两者transform对比model.eval()是否调用Dropout评估模式影响结果在验证循环前加model.eval()随机种子是否固定每次运行结果不同torch.manual_seed(42)加np.random.seed(42)是否保存了最佳模型用了最后一轮参数对比best_model.pth和末尾模型准确率预测时是否做了归一化直接喂0~255像素检查ToTensor与Normalize是否存在随机种子固定值得单独说明。PyTorch的DataLoader在多线程下带有随机性不固定种子的话每次训练结果都会有波动老师抽查复现时会发现同一份代码跑出两个结果。在脚本开头加上torch.manual_seed(42)、np.random.seed(42)和random.seed(42)配合CUDNN的torch.backends.cudnn.benchmark False能最大程度保证可复现性。高分作业不一定是最高准确率但一定是能稳定复现最高准确率的那份。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门