
简介基于Python与卷积神经网络的猫狗图片分类项目源代码适合计算机相关专业正在准备毕业设计或期末大作业的学生也适合需要图像分类实战练习的学习者。该项目获得导师认可评审分98分题目难度适中且经助教老师审定全部源码均通过本地编译和严格调试可稳定运行。资源zip压缩包约87.79MB共2000个文件其中含有1992张jpg格式的猫狗图像、7个Python脚本和1个Markdown说明文档脚本覆盖数据加载、CNN模型搭建、训练与准确率评估等核心流程文档便于快速理清项目结构与运行说明。学习者可直接使用现成数据集复现训练参考网络参数配置调整结构并结合代码注释理解卷积层、池化层、全连接层之间的关系从而完成课程设计、毕业设计或深度学习入门实战项目源码经过严格调试可按文档指引复现猫狗分类结果。目前已有74人学习/下载能够作为同类图像分类任务的选题与实现参考。1. 猫狗图片分类项目这个源代码到底能帮你省多少事如果只看 demo 截图猫狗图片分类给人的感觉是“深度学习入门而已”但你真把手伸进一个自称“高质量优秀作品”的 Python CNN 源代码项目看到的往往是两种极端要么是一堆别人跑不起来的半成品要么是封装过度让你改一个卷积核都要翻半天的“大作业”。这个项目要解决的正是这两件事——用 CNN 把猫狗分类这件事本身做扎实同时把源代码组织得能改、能续、能复现。对刚入门的读者它能当作第一个真正意义上的深度学习工程样板对已经写过分类模型的读者它的价值在数据处理、训练流程和保存加载策略这些容易被忽略的细节。我拿到这类项目第一件事不是读模型文件而是先把数据加载、目录结构和 checkpoint 逻辑看明白。CNN 模型结构是黑匣子但数据路径和训练循环不是后者才决定你能不能在一小时内把它跑起来。这篇笔记就按这个思路展开从环境准备、数据处理、模型构建、训练调参到避坑与验证把整个项目拆成你能直接上手复现的步骤。适合正在学 PyTorch、准备做课程设计或想建一个图片分类基线的读者。2. 环境准备与猫狗数据集的预处理把目录结构理顺训练就成功了一半2.1 用 venv 隔离 Python 环境避免把本机装乱这类深度学习项目最常见的翻车起点不是代码而是 Python 环境。猫狗图片分类用的依赖不算多但 PyTorch、torchvision、matplotlib、numpy、pillow 这几件套版本一旦互相打架你会浪费一下午在重装库上。我一般先在项目根目录建一个虚拟环境再用 requirements 一次装齐不做全局安装这是血泪经验换来的习惯。cd cat_dog_cnn python -m venv venv source venv/bin/activate pip install torch torchvision matplotlib numpy pillow建议用 PyTorch 官方推荐的安装命令区分 CPU 和 GPU 版本。上面这条默认装的是 CPU 版如果你机器有 NVIDIA 显卡并且想用小 batch 快速验证模型就去官网复制对应 CUDA 版本的命令例如安装带 CUDA 支持的 PyTorch。接下来的代码都假设你已经在 venv 里。环境这块有两个容易踩的细节一是 torch 和 torchvision 版本必须配套torchvision 的 API 在 0.15 之后有一些变化老代码直接跑会报ImportError二是pillow最好指定一个较新版本老版本对新格式图片的解码支持有问题训练中途报Image file is truncated的概率会明显升高。2.2 用 ImageFolder 数据加载目录名就是标签猫狗图片分类这类二分类任务最可靠的数据组织方式不是写一个自定义 Dataset 去读 CSV 标签而是用torchvision.datasets.ImageFolder。它的规则特别简单根目录下每个子文件夹的名字就是类别名文件夹里的所有图片都归到这一类。这样做的好处是你的标签零维护换新图片只需丢进对应文件夹而ImageFolder会自动枚举类别并建立索引。# prepare_data.py import os import shutil import random random.seed(42) src_root path/to/raw_images # 原始图片目录猫图狗图混在一起 train_root data/train val_root data/val # 手动准备两个类别文件夹 for split in [train_root, val_root]: os.makedirs(os.path.join(split, cat), exist_okTrue) os.makedirs(os.path.join(split, dog), exist_okTrue) # 假设原始数据结构为 {src_root}/cat/*.jpg 与 {src_root}/dog/*.jpg for cls in [cat, dog]: src_dir os.path.join(src_root, cls) images os.listdir(src_dir) random.shuffle(images) val_count int(len(images) * 0.2) # 留 20% 做验证集 for img in images[:val_count]: shutil.copy(os.path.join(src_dir, img), os.path.join(val_root, cls, img)) for img in images[val_count:]: shutil.copy(os.path.join(src_dir, img), os.path.join(train_root, cls, img)) print(train/val 目录生成完毕)这段脚本做的事就是分层抽样从每个类里随机抽 20% 作为验证集剩下进训练集。注意random.seed(42)保证每次执行划分结果一致这一点很重要否则你训练到一半发现验证集变了loss 曲线会突然跳变看起来像模型玄学实际是数据划分不稳定。接下来定义训练用的 transform。CNN 要求输入尺寸固定所以Resize和CenterCrop是必须的。常见做法是统一缩放到 256x256 再中心裁剪到 224x224这个尺寸是很多预训练模型的默认输入后续如果你想换成 ResNet 也方便。# transforms.py from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(), 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]) ])这里的Normalize用的是 ImageNet 的均值标准差如果你的猫狗图片风格和 ImageNet 差得很远理论上应该重新统计但实际上大多数情况下沿用这套参数就能正常收敛。数据增强方面我只用了随机裁剪和水平翻转不要一上来就加太多花样先保持样本分布稳定模型能跑通以后再加增强也不迟。数据加载器上batch_size我通常先给 32num_workers在 Windows 上给 0 最省事Linux 可以给 4 或 8。如果你训练时发现 CPU 占用异常低、GPU 在干等多半是num_workers没设对。from torch.utils.data import DataLoader train_dataset datasets.ImageFolder(train_root, transformtrain_transform) val_dataset datasets.ImageFolder(val_root, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue)3. CNN 模型构建从 LeNet 式结构到一个能自定义的卷积网络3.1 为什么卷积层组合比全连接层更适合图片分类猫狗图片分类本质上是一个二分类问题但如果你直接拉平一张 224x224x3 的图片送进全连接网络第一层就得有 150528 个输入神经元参数数量直接爆炸训练不但慢还容易过拟合。CNN 的核心思路是用卷积核在局部区域做特征提取参数共享让网络规模小了一个数量级。具体到这个项目里我一般先定义一个“卷积块 池化 展平 全连接”的经典骨架让新手能直观看到特征图怎么逐层变小、通道数怎么逐层变多。这里有一个经常被忽略的点卷积核数量和特征图尺寸的变化不是随便拍的。每次MaxPool2d(2)会让宽高减半你得大致估算一下最终展平向量的维度后面全连接层的输入维度才能对得上。与其心算不如写一行print查看中间维度。3.2 一个能直接跑通的 CNN 模型类代码下面这段代码是这类猫狗分类源代码里最核心的部分。我把模型封装成一个类__init__里定义卷积层、池化层和全连接层forward里描述数据流向。为了让你看清楚每层输出尺寸怎么变我在forward里留了注释训练时可以把print打开核对维度。# model.py import torch.nn as nn import torch.nn.functional as F class CatDogCNN(nn.Module): def __init__(self, num_classes2): super(CatDogCNN, self).__init__() # 第一组卷积3 - 32 通道特征图 224 - 224 self.conv1 nn.Conv2d(in_channels3, out_channels32, kernel_size3, padding1) self.bn1 nn.BatchNorm2d(32) # 第二组卷积32 - 64 通道 self.conv2 nn.Conv2d(in_channels32, out_channels64, kernel_size3, padding1) self.bn2 nn.BatchNorm2d(64) # 第三组卷积64 - 128 通道 self.conv3 nn.Conv2d(in_channels64, out_channels128, kernel_size3, padding1) self.bn3 nn.BatchNorm2d(128) self.pool nn.MaxPool2d(kernel_size2, stride2) # 经过 3 次池化后224 - 112 - 56 - 28 # 最后一层卷积输出 128 个通道所以展平维度为 128*28*28 self.fc1 nn.Linear(128 * 28 * 28, 256) self.fc2 nn.Linear(256, num_classes) self.dropout nn.Dropout(p0.5) def forward(self, x): x self.pool(F.relu(self.bn1(self.conv1(x)))) x self.pool(F.relu(self.bn2(self.conv2(x)))) x self.pool(F.relu(self.bn3(self.conv3(x)))) # 展平从 (batch, 128, 28, 28) 变为 (batch, 128*28*28) x x.view(x.size(0), -1) x F.relu(self.fc1(x)) x self.dropout(x) x self.fc2(x) return x关键参数都写清楚了卷积核大小统一为 3x3padding1保持宽高不变MaxPool2d尺寸减半。三次池化后 224 变成 28128 个通道展平得到 100352 维向量这个数字和fc1的输入维度必须一致。Dropout放在全连接层之间只在全连接部分做随机失活卷积层后面不接 Dropout 是常见做法因为卷积层本身参数少且有 BatchNorm 在控制激活分布。如果你的项目里用的不是 224x224 而是其他尺寸上面这段代码的fc1输入维度就要按新尺寸重新算。我每次改数据尺寸都会先跑一个随机输入张量过一遍模型而不是去心算维度。import torch model CatDogCNN(num_classes2) fake_input torch.randn(1, 3, 224, 224) output model(fake_input) print(output.shape) # torch.Size([1, 2])跑通这一步模型结构就算立住了。如果此处报错百分之八九十是fc1的输入维度不对去改常量就行。4. 训练循环与调参顺序损失函数、优化器和学习率怎么配合4.1 损失函数与优化器CrossEntropyLoss 和 Adam 的搭配理由猫狗分类是个二分类任务但输出层是 2 个神经元这里最合适的损失函数不是BCELoss而是CrossEntropyLoss它内部会自动做 softmax 并计算交叉熵。BCELoss需要你手动把输出压缩到 0 到 1 之间还要改标签编码方式纯属给自己找麻烦。优化器方面项目源代码里我常用 Adam学习率先给1e-4这个组合在中小型数据集上收敛稳定几乎不需要额外调参。如果你愿意多花时间SGD 配合 momentum 也能达到更好的收敛效果但新手阶段没必要在优化器上纠结。4.2 训练循环代码batch 迭代、loss 记录与 checkpoint 保存训练循环是这类源代码里最需要考虑工程细节的部分。下面这个版本我加了几个实用组件验证集评估、早停和 checkpoint 保存最后会打印每个 epoch 的准确率变化方便你判断模型是收敛、过拟合还是已经训练崩了。# train.py import torch import torch.nn as nn import torch.optim as optim device torch.device(cuda if torch.cuda.is_available() else cpu) model CatDogCNN(num_classes2).to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-4) scheduler optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.5) num_epochs 30 best_val_acc 0.0 patience 5 bad_epochs 0 for epoch in range(num_epochs): model.train() running_loss 0.0 correct 0 total 0 for inputs, labels in train_loader: inputs, labels inputs.to(device), labels.to(device) optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * inputs.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() train_loss running_loss / total train_acc 100.0 * correct / total model.eval() val_correct 0 val_total 0 with torch.no_grad(): for inputs, labels in val_loader: inputs, labels inputs.to(device), labels.to(device) outputs model(inputs) _, predicted torch.max(outputs, 1) val_total labels.size(0) val_correct (predicted labels).sum().item() val_acc 100.0 * val_correct / val_total print(fEpoch {epoch1}/{num_epochs} | fTrain Loss: {train_loss:.4f} | fTrain Acc: {train_acc:.2f}% | fVal Acc: {val_acc:.2f}%) scheduler.step() if val_acc best_val_acc: best_val_acc val_acc bad_epochs 0 torch.save({ epoch: epoch, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), best_val_acc: best_val_acc, }, best_model.pth) print( - 保存最佳模型) else: bad_epochs 1 if bad_epochs patience: print(f连续 {patience} 个 epoch 验证集未提升早停。) break几个参数的取舍逻辑说一下。StepLR(step_size10, gamma0.5)的意思是每 10 个 epoch 把学习率乘 0.5学习率从1e-4逐步降到5e-5、2.5e-5后期训练精细度更高。patience5是早停的耐心值如果验证集准确率连续 5 个 epoch 没创新高就停防止你浪费算力在过拟合阶段。这里还有一个细节scheduler.step()必须放在每个 epoch 结束时调用放错位置会严重影响学习率变化节奏。为什么要保存整个字典而不是只保存model.state_dict()因为恢复训练时需要把优化器状态、当前 epoch 都找回来。如果你之后想加载模型继续训练只保存权重是不够的Adam 的动量信息会丢失恢复后的前几个 epoch 会有明显震荡。4.3 调参顺序先让 loss 降下去再谈准确率我见过太多人一上来就调卷积核数量、加层加通道结果 loss 根本不降问题根本不出在模型容量上。正确的顺序应该是先用小数据集比如每类 500 张把整个流程跑通确认 loss 在下降、准确率在提升然后放大到全量数据训练。如果 loss 震荡得厉害优先调低学习率而不是改模型结构。学习率1e-4是个相对保守的起点Adam 对学习率不敏感但这个值在图片分类任务里基本不会翻车。如果 loss 一开始就不降检查数据加载是否正常、标签是否错位、model.train()有没有调用。5. 猫狗分类源代码的高频坑现象、原因、解决办法5.1 训练时 loss 剧烈震荡准确率在 50% 上下徘徊现象loss 曲线像锯齿一样上蹿下跳训练到十几个 epoch 准确率依然在 50% 附近和抛硬币没区别。原因学习率过大是最常见的原因Adam 在lr1e-3时对这类小数据集容易出这种问题另外如果数据加载没有shuffleTrue模型每个 batch 学到的全是同一个类别的图片梯度方向会被带偏。解决先把学习率降到1e-4或1e-5确认shuffleTrue然后观察前两个 epoch 的 loss 是否稳定下降。如果还不行检查标签是否对得上打印一个 batch 的inputs和labels看看猫对应 0、狗对应 1这个对应关系不能反。5.2 验证集准确率远低于训练集模型泛化失败现象训练集准确率已经 99%验证集却只有 75%两者差距越拉越大。原因过拟合而且数据量太少或者数据增强不够。猫狗分类很容易过拟合尤其是网络容量偏大、训练 epoch 又长时模型开始记住训练集细节而不是学习真正的分类特征。解决增加随机翻转、随机裁剪等增强手段加大Dropout概率到 0.5我这个模型里已经用了减少 epoch 数并配合早停必要时用预训练模型做迁移学习效果会立竿见影。还有一个细节验证集不要做增强只做Resize、CenterCrop、Normalize。5.3 加载 checkpoint 后训练准确率断崖式下降现象用torch.load加载保存的模型继续训练第一个 epoch 的 loss 奇高准确率掉到接近随机。原因保存和加载时模型定义不一致最常见是加载前模型类的结构变了或者你加载的是model.state_dict()而保存时保存的是整个字典导致键不匹配。另一种可能是保存时没写model.eval()如果你在评估时直接加载权重BatchNorm 和 Dropout 状态不对。解决保存时统一用我上面给出的字典格式加载时统一走model.load_state_dict(checkpoint[model_state_dict])。恢复训练前要把优化器状态也加载回来checkpoint torch.load(best_model.pth) model.load_state_dict(checkpoint[model_state_dict]) optimizer.load_state_dict(checkpoint[optimizer_state_dict])5.4 报错Image file is truncated或Found 0 images in subfolders现象数据加载阶段直接崩ImageFolder报找不到图片或者训练到一半突然报图片解码失败。原因数据集里混进了损坏的图片文件另一个坑是目录结构不对——ImageFolder要求根目录下必须有一层类别子目录你直接把图片全放在根目录下就会识别成 0 个类。解决检查目录结构是否严格符合data/train/cat/*.jpg和data/train/dog/*.jpg对损坏图片训练前做一次过滤用PIL.Image.open验证每张图能否正常打开不能打开的直接移到坏图文件夹。这一步虽然费时间但能避免训练到第 8 个小时突然中断的悲剧。5.5 GPU 显存充足但训练速度慢num_workers怎么调都上不去现象GPU 利用率只有 30%CPU 却满载训练时间比预期翻了一倍。原因数据加载成了瓶颈。num_workers太高会导致进程频繁切换太低又无法预先取数据在 Windows 上num_workers设大于 0 还可能触发多进程递归问题。解决Linux 上num_workers按 CPU 核心数的四分之一到一半来设Windows 上先设 0 跑通再逐步升到 2、4 观察变化。pin_memoryTrue对 GPU 训练有实际加速效果但只在 CUDA 可用时才有意义。6. 用混淆矩阵和单图预测核对模型真实水平别只盯着准确率训练结束后很多人只看验证集准确率就宣布“项目完成”这不够。猫狗二分类里准确率虽然直观但无法暴露类别偏好——比如模型可能把大部分猫都分对了狗却错了一半。我每个项目都会补上一张混淆矩阵和一次单图预测这两个验证手段能在五分钟内让你搞清楚模型到底哪里不行。# evaluate.py import torch import numpy as np import torchvision.datasets as datasets from torchvision import transforms from sklearn.metrics import confusion_matrix import matplotlib.pyplot as plt 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]) ]) val_dataset datasets.ImageFolder(data/val, transformval_transform) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4) device torch.device(cuda if torch.cuda.is_available() else cpu) model CatDogCNN(num_classes2).to(device) checkpoint torch.load(best_model.pth) model.load_state_dict(checkpoint[model_state_dict]) model.eval() all_preds, all_labels [], [] with torch.no_grad(): for inputs, labels in val_loader: inputs inputs.to(device) outputs model(inputs) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) print(混淆矩阵) print(cm)看混淆矩阵的时候我重点看非对角线数字。如果cm[0][1]明显大于cm[1][0]说明模型更倾向于把猫预测成狗这时候优先去补猫类样本的多样性而不是改网络结构。单图预测的代码更简单加载一张图走同一个 transform 流程输出类别索引和置信度分数from PIL import Image def predict_single(image_path): image Image.open(image_path).convert(RGB) tensor val_transform(image).unsqueeze(0).to(device) with torch.no_grad(): output model(tensor) prob torch.softmax(output, dim1) class_idx torch.argmax(prob, dim1).item() class_name val_dataset.classes[class_idx] confidence prob[0][class_idx].item() print(f预测类别: {class_name}, 置信度: {confidence:.4f}) return class_name, confidence我自己做这类项目时养成的习惯是每保存一次最佳模型就顺手跑十张验证集外的图片做单图预测看看真实图片上的置信度分布是否合理。如果一张猫图的置信度只有 0.55虽然分类对了也不敢说模型是稳健的。这类小验证花的时间不到两分钟但能让你对模型的泛化能力有比准确率更准确的判断。这个项目值得投入的深度其实比看上去大。把基础 CNN 跑通只是第一步后续你可以试着换成预训练的 ResNet 做迁移学习看看同样的数据量下准确率能提升多少也可以把模型导出为 ONNX 放到移动端推理。关键是先把这个最小闭环做扎实数据、模型、训练、验证四个环节每一处都要能解释明白再去谈更复杂的结构。希望这篇笔记能帮你在猫狗图片分类这个项目上少走弯路把源码玩转起来。本文还有配套的精品资源点击获取