☰
基于Python卷积神经网络CNN的猫狗图像分类识别源码与详细报告
2026/10/12 18:40:41 网站建设 项目流程

简介:这份资源是面向高校学生的卷积神经网络课程实践项目,围绕Python实现猫狗图像二分类任务,适合作为深度学习入门练手、课程设计或期末大作业参考。压缩包共约2000个文件,整体218.59MB,其中1990张jpg图片构成训练与测试数据集,另含1份pdf详细报告、1个py源码文件及少量xml、md等辅助文件,覆盖数据、代码与文档三类内容。目前已有308人学习下载,说明该方案在同类作业中具备一定参考价值。读者可获取一份可直接运行的CNN分类源码,配合报告理解网络结构设计、数据预处理与训练评估流程,并借助现成数据集快速复现实验、对照修改参数,为撰写自己的课程报告或进一步尝试迁移学习提供清晰起点。

1. 从一份期末大作业说起:CNN 猫狗分类到底在练什么

Kaggle 那套猫狗数据集(Dogs vs. Cats)几乎是每个学深度学习的人绕不开的第一道坎。标题里说的「基于 Python 卷积神经网络 CNN 的猫狗图像分类识别源码 + 详细报告 PDF」,本质上就是一套完整的课程设计交付物:一份能跑通的训练脚本、一个能加载权重做推理的预测脚本,外加一份把实验过程、参数、结果图表写清楚的报告。很多人拿到这类源码第一反应是「跑起来看看准确率」,但真正决定这份作业能不能拿高分、能不能变成你自己简历上的项目,是你能不能讲清楚数据怎么进网络、卷积核在学什么、过拟合什么时候出现、以及为什么你的验证集准确率比训练集低一截。

这篇文章面向三类人:正在赶期末大作业、需要一份能复现的 CNN 图像分类源码的同学;想用猫狗分类入门深度学习卷积神经网络、但被各种教程绕晕的初学者;以及已经会调库、但想把「图像分类算法」这条链路从数据到部署完整走一遍的工程师。我会按「数据准备 → 模型搭建 → 训练调参 → 推理验证 → 报告怎么写」的顺序,把每一步的命令、参数和踩坑点摊开讲。你照着做,能拿到一个验证集准确率 90% 以上的二分类模型,也能理解源码里每一行为什么这么写。

2. 数据准备:把猫狗图片整理成 CNN 能吃的格式

2.1 目录结构与数据划分的常见做法

原始数据集通常是PetImages/Cat和PetImages/Dog两个文件夹,里面混着大量文件名重复、尺寸不一、甚至有几张损坏的图。直接丢给ImageFolder会报错,所以第一步是清洗和划分。我一般会建一个dataset目录,下面分train、val、test三个子目录,每个子目录再分cat、dog。划分比例按 8:1:1 走,训练集 20000 张、验证集 2500 张、测试集 2500 张,这是猫狗各 12500 张总量下的稳妥分法。

清洗时重点处理三类问题:一是尺寸为 0 的损坏文件,用PIL打开会抛异常;二是灰度图或 RGBA 四通道图,训练时统一转成 RGB;三是文件名大小写不一致导致的重复。下面这段脚本把清洗和划分一次做完。

import os, shutil, random from PIL import Image random.seed(42) SRC = "PetImages" DST = "dataset" for split in ["train", "val", "test"]: for cls in ["cat", "dog"]: os.makedirs(os.path.join(DST, split, cls), exist_ok=True) def is_valid(path): try: with Image.open(path) as im: im.verify() # 校验文件完整性 with Image.open(path) as im: im = im.convert("RGB") # 统一转三通道 return True except Exception: return False for cls in ["cat", "dog"]: files = [f for f in os.listdir(os.path.join(SRC, cls)) if is_valid(os.path.join(SRC, cls, f))] random.shuffle(files) n = len(files) n_train, n_val = int(n * 0.8), int(n * 0.1) splits = { "train": files[:n_train], "val": files[n_train:n_train + n_val], "test": files[n_train + n_val:] } for split, names in splits.items(): for name in names: shutil.copy(os.path.join(SRC, cls, name), os.path.join(DST, split, cls, name)) print(cls, "total:", n, "train:", len(splits["train"]))

这段代码的逻辑是先用verify()过滤损坏文件,再用convert("RGB")把可能存在的灰度图、四通道图统一成三通道,避免后续ToTensor()时通道数对不上。random.seed(42)保证每次划分结果一致,方便复现。参数上,0.8/0.1/0.1是经验值,如果你的数据量小于 5000 张,建议改成 7:1.5:1.5,让验证集和测试集更有统计意义。

2.2 用 Dataset 和 DataLoader 做在线增强

猫狗分类最容易翻车的地方是过拟合:训练集准确率冲到 99%,验证集卡在 80% 上不去。解决办法是在训练阶段做在线数据增强,让每轮看到的图都略有不同。常见做法是用torchvision.transforms组合随机裁剪、翻转、颜色抖动,验证和测试阶段只做 Resize 和归一化。

from torchvision import transforms from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder train_tf = transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale=(0.7, 1.0)), # 随机裁剪 transforms.RandomHorizontalFlip(p=0.5), # 水平翻转 transforms.ColorJitter(0.2, 0.2, 0.2), # 颜色抖动 transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], # ImageNet 均值 [0.229, 0.224, 0.225]) # ImageNet 标准差 ]) val_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) train_ds = ImageFolder("dataset/train", transform=train_tf) val_ds = ImageFolder("dataset/val", transform=val_tf) test_ds = ImageFolder("dataset/test", transform=val_tf) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4) val_loader = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=4) test_loader = DataLoader(test_ds, batch_size=32, shuffle=False, num_workers=4)

RandomResizedCrop(224, scale=(0.7, 1.0))表示随机裁出原图 70% 到 100% 的区域再缩放到 224,这是让模型学到「猫耳朵在角落也能认出来」的关键。Normalize用的均值和标准差是 ImageNet 统计值,如果你从零训练而不是用预训练权重,可以换成自己数据集的统计值,但差别通常不大。num_workers=4在 Windows 上如果报错,改成 0 即可,这是DataLoader多进程在 Windows 下的经典坑。

提示:验证集和测试集千万不要加随机增强,否则每次评估结果都在抖,你根本分不清是模型变好了还是数据变了。

3. 模型搭建:从零写 CNN 还是用预训练网络

3.1 一个能跑通的四层 CNN 结构

期末大作业通常要求「自己搭 CNN」,所以先给一个从零写的版本。结构是四组「卷积 + ReLU + 池化」,最后接全连接分类头。输入 224×224×3,输出 2 类。

import torch import torch.nn as nn class CatDogCNN(nn.Module): def __init__(self, num_classes=2): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 32, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), # 112 nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), # 56 nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), # 28 nn.Conv2d(128, 256, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),# 14 ) self.classifier = nn.Sequential( nn.AdaptiveAvgPool2d(1), # 把 14x14 压成 1x1 nn.Flatten(), nn.Dropout(0.5), nn.Linear(256, num_classes) ) def forward(self, x): return self.classifier(self.features(x)) model = CatDogCNN() print(sum(p.numel() for p in model.parameters())) # 约 40 万参数

padding=1配合kernel_size=3保证卷积后尺寸不变,尺寸缩小全靠MaxPool2d(2)。AdaptiveAvgPool2d(1)是替代大Linear层的常用技巧,它把任意空间尺寸压成 1×1,这样你换输入分辨率时不用改全连接层维度,也大幅减少了参数量。Dropout(0.5)放在分类头前,是抑制过拟合最直接的手段。这个模型参数量约 40 万,在 2 万张图上训练,CPU 也能跑,但 GPU 上单轮大约 30 秒。

3.2 迁移学习:把验证集准确率从 85% 拉到 95%

从零训练的 CNN 在猫狗任务上通常能到 85% 左右,想再往上走,最省力的办法是拿 ImageNet 预训练的 ResNet18 做微调。做法是冻结前面的卷积层,只训练最后的全连接层,等 loss 稳定后再解冻最后几个 block 一起微调。

import torchvision.models as models def build_resnet18(num_classes=2, freeze_backbone=True): model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT) if freeze_backbone: for p in model.parameters(): p.requires_grad = False in_features = model.fc.in_features model.fc = nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, num_classes) ) return model model = build_resnet18() # 只优化 fc 层参数 optimizer = torch.optim.Adam( filter(lambda p: p.requires_grad, model.parameters()), lr=1e-3)

weights=ResNet18_Weights.DEFAULT会下载官方预训练权重,第一次运行需要联网。冻结骨干后,filter(lambda p: p.requires_grad, ...)只把fc层参数交给优化器,这样训练快且不容易破坏预训练特征。等验证集准确率不再上升,把freeze_backbone设为 False,用更小的学习率(比如 1e-4)再跑几轮,通常还能涨 1 到 2 个百分点。这是「最新的图像分类模型」思路在课程作业里的落地方式:不追求 SOTA,追求在有限算力下拿到稳定高分。

注意:解冻微调时学习率一定要调小,否则预训练权重会被大梯度冲垮,验证集准确率会先掉一大截再慢慢爬回来,很多人以为模型坏了就放弃了。

4. 训练与调参:让 loss 曲线听话的几个关键参数

4.1 训练循环与学习率调度

训练循环本身不复杂,关键是每个 epoch 结束后在验证集上评估,并保存验证集准确率最高的权重,而不是最后一个 epoch 的权重。下面是一个完整的训练骨架。

import torch, time from torch.optim.lr_scheduler import CosineAnnealingLR device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = build_resnet18().to(device) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam( filter(lambda p: p.requires_grad, model.parameters()), lr=1e-3) scheduler = CosineAnnealingLR(optimizer, T_max=10) best_acc, best_path = 0.0, "best.pth" for epoch in range(10): model.train() running_loss = 0.0 for imgs, labels in train_loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() loss = criterion(model(imgs), labels) loss.backward() optimizer.step() running_loss += loss.item() * imgs.size(0) model.eval() correct, total = 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels = imgs.to(device), labels.to(device) preds = model(imgs).argmax(1) correct += (preds == labels).sum().item() total += labels.size(0) acc = correct / total scheduler.step() print(f"epoch {epoch+1} loss {running_loss/total:.4f} val_acc {acc:.4f}") if acc > best_acc: best_acc = acc torch.save(model.state_dict(), best_path) print("best val acc:", best_acc)

CosineAnnealingLR让学习率按余弦曲线从 1e-3 降到接近 0,前期大步走、后期小步收敛,比固定学习率稳。best.pth保存的是验证集最优权重,这是防止过拟合的最后一道保险。running_loss乘imgs.size(0)再除以总数,得到的是按样本加权的平均 loss,比直接平均每个 batch 的 loss 更准确。

4.2 三个必调参数与它们的合理区间

参数常用值调大后果调小后果
batch_size32 / 64显存吃紧,收敛变慢梯度噪声大,loss 抖
learning_rate1e-3(冻结)/ 1e-4(微调)loss 震荡不下降收敛极慢,10 轮没动静
weight_decay1e-4欠拟合,训练准确率上不去过拟合,验证准确率掉

batch_size受显存限制,8G 显存跑 ResNet18 + 224 输入,32 是安全值。learning_rate是最玄学的参数,冻结骨干时用 1e-3,解冻微调时降到 1e-4,这是血泪经验。weight_decay加在优化器里,Adam(..., weight_decay=1e-4),它相当于给权重加 L2 惩罚,能压住过拟合,但设太大模型会学不动。

提示:如果 loss 在前几个 epoch 就变成 nan,先检查学习率是不是太大,再检查数据里有没有标签越界或全黑图。

5. 避坑与排查:猫狗分类里最容易翻车的五件事

现象一:训练准确率 99%,验证准确率 75%,两条曲线越拉越开。原因:过拟合,模型把训练集背下来了。解决:加数据增强、加 Dropout、加 weight_decay,或者直接换预训练模型冻结骨干。最有效的是增加训练数据量,猫狗各 12500 张已经不算少,如果还过拟合,说明模型容量太大。

现象二:验证集准确率一直在 50% 附近,等于瞎猜。原因:标签和图片对不上,或者ImageFolder读到的类别顺序和你以为的不一样。解决:打印train_ds.classes和train_ds.class_to_idx,确认cat对应 0 还是 1;再随机抽几张图连同标签可视化出来看。这个坑我踩过,划分脚本里shutil.copy的目标路径写反了,猫的图进了 dog 文件夹。

现象三:Windows 上num_workers>0报BrokenPipeError或卡死。原因:Windows 多进程DataLoader的已知问题。解决:把num_workers设为 0,或者把训练代码放进if __name__ == "__main__":保护块里。Linux 上一般没这个问题。

现象四:GPU 显存够,但训练速度慢得离谱。原因:数据增强在 CPU 上做,num_workers太小,GPU 一直在等数据。解决:把num_workers调到 CPU 核数的 2 到 4 倍,或者把图片提前缩放到 256 存一份缓存,减少每轮解码开销。用nvidia-smi看 GPU 利用率,低于 50% 基本就是数据管道拖后腿。

现象五:推理时单张图预测结果和验证集对不上。原因:推理时的预处理和验证集不一致,比如忘了Normalize,或者Resize尺寸不对。解决:把验证集的val_tf单独存成一个变量,推理脚本直接复用,不要重新写一遍。另外model.eval()和torch.no_grad()一个都不能少,否则 Dropout 和 BatchNorm 会按训练模式走,结果每次都不一样。

6. 推理、报告与一个提分技巧

训练完拿到best.pth,推理脚本要做的就是加载权重、复用验证集预处理、输出类别和置信度。下面这段可以直接抄。

from PIL import Image import torch def predict(img_path, model, device): model.eval() tf = val_tf # 复用验证集预处理,别重写 img = Image.open(img_path).convert("RGB") x = tf(img).unsqueeze(0).to(device) # 加 batch 维度 with torch.no_grad(): logits = model(x) prob = torch.softmax(logits, dim=1) conf, pred = prob.max(1) classes = ["cat", "dog"] return classes[pred.item()], conf.item() model = build_resnet18(freeze_backbone=False).to(device) model.load_state_dict(torch.load("best.pth", map_location=device)) print(predict("dataset/test/cat/1.jpg", model, device))

unsqueeze(0)是把单张图的[3,224,224]变成[1,3,224,224],因为模型 forward 期望有 batch 维度。softmax把 logits 转成概率,max(1)同时拿到置信度和类别索引。map_location=device保证在 CPU 上也能加载 GPU 训出来的权重。

报告 PDF 怎么写才不像流水账?我的习惯是只放四样东西:一张数据分布表、一张 loss/acc 曲线图、一张混淆矩阵、一张错例图。混淆矩阵能看出模型是把猫认成狗多,还是狗认成猫多;错例图挑 8 张置信度低于 0.7 的样本,分析是遮挡、光照还是姿态问题。这四样东西一摆,报告的技术含量就出来了,比堆十页代码强。

最后一个提分技巧:测试时增强(TTA)。对同一张测试图做原图、水平翻转、中心裁剪三个版本,分别推理后把 softmax 概率平均,再取 argmax。通常能涨 0.5 到 1 个百分点,代码只多十几行,答辩时讲出来是个亮点。我自己的习惯是每次交作业前都跑一遍 TTA,确认比单次推理稳,再写进报告。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询