
简介这份资源是面向计算机相关专业学生与项目实战学习者的猫狗图像分类完整方案基于Python卷积神经网络CNN实现可直接用于毕业设计、期末大作业或课程设计场景难度适中适合希望掌握深度学习图像分类流程的初学者与进阶者。压缩包共2000个文件以1992张jpg猫狗图片构成训练与测试数据集另有7个py源码文件负责模型搭建、训练与预测以及1个md说明文档整体约86.82MB目录结构清晰便于按数据与代码模块分别查阅。目前已有207人学习下载可作为同类任务的参考范例。读者可从中获得一套经本地编译调试、导师认可且评审98分的可运行项目涵盖数据读取、CNN网络定义、训练调参、模型保存与推理预测等关键环节既能直接复现实验也能据此理解卷积、池化与全连接层的组织方式为后续迁移学习或更复杂视觉任务打下基础。1. 猫狗分类这个练手项目为什么值得你花一个周末跑通如果你正在找一个能写进简历、又能真正跑起来的深度学习入门项目基于 Python 卷积神经网络 CNN 的猫狗图像分类几乎是绕不开的选项。它不像 MNIST 手写数字那样简单到没有区分度也不像 ImageNet 千类竞赛那样需要多卡集群才能出结果。猫和狗这两类目标在视觉特征上有足够的差异——耳朵形状、面部比例、毛发纹理——但又不是靠颜色直方图就能轻松分开这恰好逼着你去理解卷积层到底在学什么。我见过不少同学拿这个项目当课程设计或面试作品问题往往不在模型本身而在于数据怎么组织、训练怎么监控、过拟合怎么判断。这篇笔记就按我实际做过的路径从数据准备到模型调参再到排错把每一步讲清楚让你能照着复现也能看懂边界在哪。2. 数据准备与目录结构别让脏数据毁掉你的第一个模型2.1 猫狗数据集从哪来、怎么划分才不翻车常见做法是从公开数据集中取猫狗两类图片总量通常在两万五千张左右猫狗各半。拿到手的第一件事不是写模型而是检查图片完整性。我一般会先跑一个脚本统计每张图的尺寸、通道数和是否能正常解码把损坏文件、灰度图、异常小图筛出来。这一步花十分钟能省掉后面训练时莫名其妙的 loss 震荡。划分训练集、验证集、测试集时比例建议 8:1:1。注意验证集和测试集必须从同一分布里抽不能把某几个品种的狗全塞进测试集否则准确率会虚高。目录结构按类别分文件夹这是 PyTorch 和 TensorFlow 的 ImageFolder 都能直接吃的格式dataset/ ├── train/ │ ├── cat/ │ └── dog/ ├── val/ │ ├── cat/ │ └── dog/ └── test/ ├── cat/ └── dog/每个类别文件夹下直接放图片不要嵌套子目录。如果原始数据是扁平的用下面这段脚本按比例搬移import os import random import shutil random.seed(42) src_dir raw_images dst_dir dataset classes [cat, dog] split_ratio {train: 0.8, val: 0.1, test: 0.1} for cls in classes: files os.listdir(os.path.join(src_dir, cls)) random.shuffle(files) n len(files) n_train int(n * split_ratio[train]) n_val int(n * split_ratio[val]) splits { train: files[:n_train], val: files[n_train:n_train n_val], test: files[n_train n_val:] } for split, flist in splits.items(): out_dir os.path.join(dst_dir, split, cls) os.makedirs(out_dir, exist_okTrue) for f in flist: shutil.copy(os.path.join(src_dir, cls, f), os.path.join(out_dir, f))这段代码的关键参数是random.seed(42)固定随机种子保证每次划分一致方便复现。split_ratio按 8:1:1 分配如果数据量少于五千张验证集比例可以提到 0.15避免验证指标抖动太大。2.2 图像预处理与增强哪些操作真正有用预处理分两步统一尺寸和归一化。CNN 要求输入尺寸一致猫狗图片原始分辨率参差不齐我一般缩放到 224×224这是 ResNet、VGG 等经典骨干网络的默认输入。归一化用 ImageNet 的均值和标准差即使你从零训练这个先验也能加速收敛。数据增强是防止过拟合的第一道防线。但不是什么增强都往上堆。随机水平翻转、随机裁剪、轻微旋转这三样对猫狗分类几乎无副作用。颜色抖动要谨慎猫狗毛色是重要特征过度抖动会让模型学偏。下面是一个可用的增强配置from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale(0.8, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])RandomResizedCrop的scale(0.8, 1.0)表示裁剪面积占原图的 80% 到 100%再缩放到 224。RandomRotation(15)是正负 15 度再大就可能把猫耳朵转成狗耳朵的形态。验证集只做中心裁剪不做任何随机增强保证评估结果稳定。提示增强只作用于训练集验证集和测试集必须用确定性变换否则每次评估结果都在变你根本不知道模型是变好了还是运气好。3. 从零搭建 CNN 还是用预训练骨干两条路线的取舍3.1 一个够用的自定义 CNN 结构长什么样如果你是为了理解卷积、池化、全连接的作用从零搭一个四层卷积网络就够了。结构不用花哨但每一层的通道数和卷积核大小要有依据。我的习惯是第一层用大核抓边缘和纹理后面逐层减小核尺寸、增加通道数让网络从低级特征过渡到高级语义。import torch.nn as nn class CatDogCNN(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), nn.Conv2d(128, 256, kernel_size3, padding1), nn.BatchNorm2d(256), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Dropout(0.5), nn.Linear(256, num_classes) ) def forward(self, x): x self.features(x) return self.classifier(x)BatchNorm2d放在卷积和激活之间能稳定训练、允许更大学习率。AdaptiveAvgPool2d(1)把任意空间尺寸压成 1×1这样你换输入尺寸时不用改全连接层。Dropout(0.5)只在分类头用卷积层里不加因为卷积本身有参数共享过拟合风险相对小。这个网络参数量大约在 110 万左右在单张消费级显卡上 batch size 设 32 能跑得很舒服。训练时用 Adam 优化器学习率 1e-3跑 30 个 epoch 通常能在验证集上到 85% 以上。如果低于 80%先检查数据划分和归一化而不是急着加层。3.2 预训练模型微调什么时候该换路线自定义 CNN 的天花板大概在 90% 左右再往上提很吃力。如果你要冲高分项目或者简历上写一个亮眼数字换预训练骨干是更务实的选择。ResNet18 是最常用的起点参数量 1100 万ImageNet 预训练权重已经把底层特征学得很好了。微调策略有两种只替换最后的全连接层冻结前面所有层或者解冻最后几个卷积块一起训。我一般先冻结训练 5 个 epoch让新分类头适应数据分布再解冻最后一个 stage 用更小的学习率跑 10 个 epoch。这样比一上来就全量微调稳定得多。import torchvision.models as models import torch.nn as nn model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) for param in model.parameters(): param.requires_grad False model.fc nn.Linear(model.fc.in_features, 2) # 第一阶段只训分类头 optimizer torch.optim.Adam(model.fc.parameters(), lr1e-3) # 第二阶段解冻 layer4 for param in model.layer4.parameters(): param.requires_grad True optimizer torch.optim.Adam([ {params: model.layer4.parameters(), lr: 1e-4}, {params: model.fc.parameters(), lr: 1e-3} ])注意第二阶段用了参数组卷积层学习率 1e-4分类头保持 1e-3。这是因为预训练权重已经很好大学习率会把它破坏掉。这个配置在猫狗数据集上通常能到 97% 以上但你要清楚这个数字里有 ImageNet 的先验功劳面试时被问到要能说清楚。4. 训练循环与监控loss 不降的时候先看这三个地方4.1 一个带验证和保存的完整训练模板训练循环本身不复杂但要把训练损失、验证损失、验证准确率都记下来还要在验证指标最好时保存权重。下面这个模板我用了很多次结构清晰改起来方便import torch from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder def train_one_epoch(model, loader, criterion, optimizer, device): model.train() running_loss 0.0 correct 0 total 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() running_loss loss.item() * images.size(0) _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) return running_loss / total, correct / total def evaluate(model, loader, criterion, device): model.eval() running_loss 0.0 correct 0 total 0 with torch.no_grad(): for images, labels in loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) running_loss loss.item() * images.size(0) _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) return running_loss / total, correct / total train_dataset ImageFolder(dataset/train, transformtrain_transform) val_dataset ImageFolder(dataset/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4) device torch.device(cuda if torch.cuda.is_available() else cpu) model CatDogCNN().to(device) criterion torch.nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) best_acc 0.0 for epoch in range(30): train_loss, train_acc train_one_epoch(model, train_loader, criterion, optimizer, device) val_loss, val_acc evaluate(model, val_loader, criterion, device) print(fEpoch {epoch1}: train_loss{train_loss:.4f} train_acc{train_acc:.4f} fval_loss{val_loss:.4f} val_acc{val_acc:.4f}) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth)num_workers4在 Linux 上能加速数据加载Windows 上如果报错就改成 0。shuffleTrue只对训练集开验证集必须关掉否则评估结果没有可比性。保存权重时用state_dict()而不是整个模型文件更小加载也更灵活。4.2 学习率调度与早停两个让训练更省心的技巧固定学习率在后期容易在最优解附近震荡。加一个余弦退火或者阶梯下降能让验证准确率再涨一两个点。我常用CosineAnnealingLR配合早停机制验证损失连续 5 个 epoch 不降就停scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) patience 5 counter 0 best_val_loss float(inf) for epoch in range(30): train_loss, train_acc train_one_epoch(model, train_loader, criterion, optimizer, device) val_loss, val_acc evaluate(model, val_loader, criterion, device) scheduler.step() if val_loss best_val_loss: best_val_loss val_loss counter 0 torch.save(model.state_dict(), best_model.pth) else: counter 1 if counter patience: print(fEarly stop at epoch {epoch1}) breakT_max30表示余弦周期覆盖 30 个 epoch学习率从初始值平滑降到接近零。早停的patience5是经验值数据量小可以设 3数据量大可以设 8。注意早停看的是验证损失而不是准确率损失更敏感能更早发现过拟合。5. 避坑与排查那些让我熬夜的翻车现场5.1 验证准确率远高于训练准确率现象训练到第 5 个 epoch训练准确率 70%验证准确率 85%。原因通常是训练时开了 Dropout 和强增强验证时全关掉模型在验证集上反而表现更好。这不是 bug是正常现象。解决方式是看训练损失是否在降如果损失正常下降就不用管准确率的倒挂。如果训练损失也不降检查数据标签是否错位。5.2 loss 变成 NaN现象训练几个 batch 后 loss 突然变成 nan。原因多半是学习率太大或者数据里有异常值。先检查输入归一化是否做了再检查有没有损坏图片产生极端像素值。解决方式是把学习率降到 1e-4 重跑同时在 DataLoader 里加drop_lastTrue避免最后一个 batch 只有一张图导致 BatchNorm 统计量不稳。5.3 显存不够用现象报 CUDA out of memory。原因可能是 batch size 太大、模型参数量太大、或者没有用torch.no_grad()包住验证循环。解决方式是先把 batch size 减半验证循环确认加了torch.no_grad()如果还不够就换更小的骨干网络或者用梯度累积模拟大 batch。5.4 测试集准确率比验证集低很多现象验证集 95%测试集只有 80%。原因通常是验证集和测试集分布不一致或者测试集里有训练时没见过的品种。解决方式是重新检查划分脚本确保随机种子固定且分层抽样。如果数据量允许做 5 折交叉验证看方差有多大方差大说明数据本身难度不均。5.5 模型把猫全预测成狗现象混淆矩阵显示某一类召回率接近零。原因可能是类别标签映射反了或者某一类图片在预处理时被错误增强。解决方式是打印前 10 个 batch 的标签和图片肉眼确认。另外检查ImageFolder的类别顺序它是按文件夹名字母序排的cat 在 dog 前面别搞反。6. 把准确率再往上推一个我常用的验证技巧当你已经跑通基线想冲更高分数时别急着换更大的模型。先做一件事用测试时增强TTA看看推理阶段的提升空间。TTA 的思路是对同一张测试图做多次不同变换把预测概率平均。这个技巧不增加训练成本通常能涨 0.5 到 1.5 个百分点。def predict_with_tta(model, image, device): model.eval() transforms_list [ val_transform, transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.RandomHorizontalFlip(p1.0), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) ] probs [] with torch.no_grad(): for t in transforms_list: inp t(image).unsqueeze(0).to(device) out torch.softmax(model(inp), dim1) probs.append(out) return torch.mean(torch.stack(probs), dim0)这里只用了原图和水平翻转两个视图因为猫狗分类对翻转不敏感再多视图收益递减。如果你用的是预训练模型还可以把不同 epoch 保存的权重做集成效果比单模型 TTA 更稳。另一个我常做的验证是画混淆矩阵和看错误样本。把测试集里预测错的图挑出来按置信度从高到低排看前 20 张。如果错的是模糊、遮挡、多目标同框的图说明模型已经学到该学的剩下的靠数据清洗解决。如果错的是清晰单目标图那就要回头查标签或者增强策略。这个习惯帮我省了很多盲目调参的时间也让我在写项目报告时能说清楚模型的边界在哪。希望帮到你。本文还有配套的精品资源点击获取