简介这份资源是面向Python与深度学习入门者的CNN猫狗图像识别实战项目包适合想通过完整案例掌握卷积神经网络分类流程的学生与开发者。包内共26个文件以jpg与png图片示例、py源码脚本、zip数据压缩包为主另附PDF案例讲义、txt程序说明与md文档整体约49.3MB覆盖数据准备、模型构建、训练验证到测试部署的完整链路。项目围绕Keras与TensorFlow展开包含ImageDataGenerator数据增强、load_img与load_to_array等图像处理函数演示以及卷积层、池化层、全连接层与softmax分类结构说明并给出猫狗数据分类与识别两个可运行脚本。已有314人学习读者可借助源码、数据集与讲义快速复现实验理解超参数调整与泛化性能评估思路也可作为课程设计或毕业项目的参考模板。1. 从一份猫狗识别源码说起CNN 图像分类到底能跑多快很多人第一次接触深度学习都是从「猫狗分类」这个经典任务开始的。它不像目标检测那样要处理锚框和 NMS也不像语义分割那样逐像素标注输入一张图输出「猫」或「狗」两个类别干净利落。但恰恰是这种简单让它成为检验一套 CNN 工程链路是否完整的试金石数据怎么组织、模型怎么搭、训练怎么调、推理怎么用一个环节掉链子准确率就上不去。这份「基于 CNN 的猫狗图像识别检测分类项目」本质上是一套完整的监督学习流水线。它要解决的问题很具体给一张猫或狗的照片让模型判断它属于哪一类。适合谁适合刚学完卷积神经网络理论、想找一个能跑通全流程的实战项目练手的人也适合需要快速搭建图像二分类基线、再往业务场景迁移的工程师。下面我按自己落地时的顺序把这条链路拆开讲清楚。2. 数据准备与目录结构把猫狗图片喂进 CNN 之前要做的三件事2.1 数据集从哪来、怎么划分才不翻车猫狗图像识别最常用的公开数据集是 Kaggle 上的 Dogs vs. Cats原始训练集各 12500 张测试集 12500 张无标签。但实际做项目时你拿到的往往是一个压缩包里面图片混在一起文件名可能是cat.0.jpg、dog.1.jpg这种带标签前缀的也可能是哈希串。第一步不是急着写模型而是把数据整理成 CNN 能直接读的结构。常见做法是按 8:1:1 划分训练集、验证集、测试集。注意验证集和测试集必须来自不同批次或不同来源否则评估结果会虚高。我一般会先统计两个类别的样本数如果差距超过 1.5 倍就要做重采样或加类别权重不然模型会偏向多数类。import os import shutil import random # 原始图片目录文件名形如 cat.0.jpg / dog.1.jpg src_dir data/raw # 划分后的目标目录 dst_dir data/split random.seed(42) for split in [train, val, test]: for cls in [cat, dog]: os.makedirs(os.path.join(dst_dir, split, cls), exist_okTrue) files [f for f in os.listdir(src_dir) if f.endswith(.jpg)] random.shuffle(files) # 按 8:1:1 切分 n len(files) train_files files[:int(n * 0.8)] val_files files[int(n * 0.8):int(n * 0.9)] test_files files[int(n * 0.9):] def copy_files(file_list, split): for f in file_list: # 从文件名前缀判断类别 cls cat if f.startswith(cat) else dog shutil.copy(os.path.join(src_dir, f), os.path.join(dst_dir, split, cls, f)) copy_files(train_files, train) copy_files(val_files, val) copy_files(test_files, test)这段脚本做了三件事建目录、随机打乱、按比例复制。random.seed(42)保证每次划分一致方便复现。cls的判断依赖文件名前缀如果你的数据命名不同改成从文件夹名或 CSV 标签读取即可。划分完记得打印每个子集的样本数确认没有某个类别为空。2.2 图像预处理Resize、归一化和数据增强的取舍CNN 要求输入尺寸固定猫狗图片原始分辨率参差不齐必须统一 Resize。常见选择是 224×224因为 VGG、ResNet 这些经典骨干都在这个尺寸上预训练过。如果你自己从零搭一个小 CNN128×128 也能跑但太小会丢细节太大显存吃不消。归一化用 ImageNet 的均值和标准差是行业惯例mean[0.485, 0.456, 0.406]std[0.229, 0.224, 0.225]。即使你的数据不是 ImageNet用这套参数也不会出大问题因为 CNN 对输入分布的敏感度在浅层深层会自适应。数据增强是防止过拟合的关键。训练集上我一般加随机水平翻转、随机旋转 ±15 度、颜色抖动。验证集和测试集只做 Resize 和归一化不能加随机变换否则评估结果不可比。from torchvision import transforms train_tf transforms.Compose([ transforms.Resize((224, 224)), 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_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])RandomHorizontalFlip对猫狗都适用因为左右翻转不改变类别语义。RandomRotation(15)模拟拍摄角度偏差。ColorJitter让模型对光照变化更鲁棒。注意增强只在训练时开验证和测试必须关掉这是很多人第一次做项目时容易忽略的点。2.3 用 DataLoader 把图片变成批次张量PyTorch 的ImageFolder能自动按文件夹名生成标签配合DataLoader就能批量取数据。batch_size设 32 或 64取决于显存。num_workers设成 CPU 核心数能明显加快读取速度但在 Windows 上有时会报错设成 0 最稳。from torchvision.datasets import ImageFolder from torch.utils.data import DataLoader train_ds ImageFolder(data/split/train, transformtrain_tf) val_ds ImageFolder(data/split/val, transformval_tf) test_ds ImageFolder(data/split/test, transformval_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4) test_loader DataLoader(test_ds, batch_size32, shuffleFalse, num_workers4) print(类别映射:, train_ds.class_to_idx) print(训练集样本数:, len(train_ds))class_to_idx会输出{cat: 0, dog: 1}这个顺序决定了后面推理时索引对应的类别。shuffleTrue只在训练集开验证和测试保持顺序方便逐样本对比。如果显存不够把batch_size降到 16同时把学习率按比例调小。3. 搭一个能打的 CNN从三层卷积到迁移学习的选型对比3.1 自己搭 CNN 的最小结构三层卷积够不够如果只是跑通流程一个三层卷积加两层全连接的网络就能到 80% 左右的验证准确率。结构是Conv-ReLU-MaxPool 重复三次通道数从 32 到 64 再到 128然后展平接全连接。这个结构参数量小训练快适合验证数据管道是否正常。import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes2): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 224 - 112 nn.Conv2d(32, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 112 - 56 nn.Conv2d(64, 128, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 56 - 28 ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(128 * 28 * 28, 256), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(256, num_classes) ) def forward(self, x): x self.features(x) return self.classifier(x)padding1保证卷积后尺寸不变MaxPool2d(2)每次把宽高减半。三层之后特征图是 28×28展平后 128×28×28100352 维接 256 维全连接。Dropout(0.5)是防过拟合的后悔药训练时随机丢一半神经元推理时关闭。这个网络在 12500 张训练集上大概跑 20 个 epoch 能到 80% 验证准确率再往上就难了因为容量不够。3.2 迁移学习为什么几乎总是更优ResNet18 微调实战自己搭的 CNN 从随机初始化开始学需要大量数据和长时间训练。迁移学习用 ImageNet 上预训练的权重做起点浅层特征边缘、纹理直接复用只微调深层和分类头通常 5 个 epoch 就能到 95% 以上。这是猫狗分类项目里最值得投入的选型。import torchvision.models as models import torch.nn as nn # 加载预训练 ResNet18 model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) # 冻结浅层只训练 layer4 和 fc for name, param in model.named_parameters(): if layer4 not in name and fc not in name: param.requires_grad False # 替换分类头 model.fc nn.Linear(model.fc.in_features, 2) # 只把需要梯度的参数传给优化器 optimizer torch.optim.Adam( filter(lambda p: p.requires_grad, model.parameters()), lr1e-3 )weightsmodels.ResNet18_Weights.IMAGENET1K_V1是 torchvision 新版写法旧版用pretrainedTrue。冻结浅层能减少显存占用和训练时间layer4是最后一个残差块负责高层语义必须微调。fc换成 2 输出。优化器只接收requires_gradTrue的参数否则会报错或浪费计算。学习率设 1e-3 对微调合适如果全量微调就降到 1e-4。3.3 训练循环里必须盯住的三个量loss、acc 和 lr训练循环本身不复杂但有几个量必须每个 epoch 打印训练 loss、验证 loss、验证准确率。训练 loss 持续下降但验证 loss 开始上升就是过拟合信号该加正则或早停。验证准确率震荡大说明学习率太高或 batch_size 太小。import torch device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() for epoch in range(10): model.train() train_loss 0.0 for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() train_loss loss.item() * imgs.size(0) model.eval() val_loss, correct, total 0.0, 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) outputs model(imgs) loss criterion(outputs, labels) val_loss loss.item() * imgs.size(0) preds outputs.argmax(dim1) correct (preds labels).sum().item() total labels.size(0) print(fEpoch {epoch1}: ftrain_loss{train_loss/len(train_ds):.4f}, fval_loss{val_loss/len(val_ds):.4f}, fval_acc{correct/total:.4f})model.train()和model.eval()切换很重要影响 Dropout 和 BatchNorm 行为。torch.no_grad()在验证时关闭梯度计算省显存。loss.item() * imgs.size(0)是按样本数加权平均避免最后一个 batch 不满导致偏差。如果验证准确率连续 3 个 epoch 不升就停别硬跑。4. 推理、评估与踩坑排查模型训完之后真正要面对的事4.1 单张图片推理从 PIL 读到类别概率训练完保存权重推理时重新加载模型结构再载入权重。单张图片推理的流程是PIL 读图、Resize、转张量、归一化、加 batch 维度、过模型、softmax 取概率。from PIL import Image import torch import torch.nn.functional as F def predict(image_path, model, device): model.eval() img Image.open(image_path).convert(RGB) tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) x tf(img).unsqueeze(0).to(device) # 加 batch 维度 with torch.no_grad(): logits model(x) probs F.softmax(logits, dim1) idx probs.argmax(dim1).item() classes [cat, dog] return classes[idx], probs[0][idx].item() # 用法 label, conf predict(test.jpg, model, device) print(f预测: {label}, 置信度: {conf:.4f})convert(RGB)防止灰度图或 RGBA 图报错。unsqueeze(0)把 3×224×224 变成 1×3×224×224因为模型要求 batch 维度。softmax把 logits 转成概率argmax取最大索引。classes的顺序必须和class_to_idx一致否则猫狗会反。4.2 混淆矩阵和分类报告别只看准确率准确率在类别均衡时够用但猫狗数据如果某类偏多准确率会骗人。用sklearn的classification_report和confusion_matrix看每一类的 precision、recall、f1。from sklearn.metrics import classification_report, confusion_matrix import numpy as np model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in test_loader: imgs imgs.to(device) outputs model(imgs) preds outputs.argmax(dim1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) print(confusion_matrix(all_labels, all_preds)) print(classification_report(all_labels, all_preds, target_names[cat, dog]))混淆矩阵对角线是正确预测非对角线是错分。如果猫被大量判成狗看猫的 recall 是不是偏低。classification_report的 f1 是 precision 和 recall 的调和平均比准确率更能反映类别不均衡下的表现。4.3 避坑与排查五个血泪教训现象一训练 loss 不降一直在 0.69 附近。原因通常是标签没对上或者数据管道把图片和标签错位了。解决打印一个 batch 的图片和标签确认图片内容和标签一致检查ImageFolder的目录结构是不是train/cat/和train/dog/。现象二验证准确率比训练准确率高很多。这听起来反直觉但常见原因是验证集太小或者验证集和训练集有重叠。解决确认划分时没有重复文件验证集至少 1000 张以上。现象三显存爆了报 CUDA out of memory。原因可能是 batch_size 太大或者没加torch.no_grad()。解决把 batch_size 减半验证和推理时加with torch.no_grad():及时把 loss 转成 Python 标量释放计算图。现象四推理时预测结果全是同一类。原因可能是模型没加载权重或者归一化参数和训练时不一致。解决确认model.load_state_dict()成功检查推理的transforms和验证集是否完全一致。现象五训练准确率到 99% 但测试只有 70%。典型过拟合。解决加数据增强、加 Dropout、加权重衰减或者换迁移学习。自己搭的小 CNN 在猫狗任务上很容易过拟合因为参数量相对数据量还是偏大。5. 把准确率再往上推学习率调度与测试时增强的实战技巧迁移学习跑到 95% 之后想再往上走靠的不是换更大的模型而是调训练策略。我一般会加两个东西余弦退火学习率调度和测试时增强。余弦退火让学习率从初始值按余弦曲线降到接近零前期大步走后期小步微调。配合 Adam 使用通常能比固定学习率多涨 1 到 2 个百分点。from torch.optim.lr_scheduler import CosineAnnealingLR optimizer torch.optim.Adam( filter(lambda p: p.requires_grad, model.parameters()), lr1e-3 ) scheduler CosineAnnealingLR(optimizer, T_max10, eta_min1e-6) for epoch in range(10): # ... 训练和验证代码 ... scheduler.step() # 每个 epoch 后更新学习率 print(f当前学习率: {optimizer.param_groups[0][lr]:.6f})T_max10表示 10 个 epoch 完成一个余弦周期eta_min1e-6是学习率下限。scheduler.step()放在 epoch 末尾不是 batch 末尾。如果训练超过 10 个 epochT_max要相应调大。测试时增强是推理阶段对同一张图做多次变换取平均概率。比如原图、水平翻转、轻微旋转各推理一次softmax 概率平均后再取 argmax。这相当于免费涨点代价是推理时间翻几倍。def predict_tta(image_path, model, device): model.eval() img Image.open(image_path).convert(RGB) base_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) aug_tfs [ base_tf, transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p1.0), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]), ] probs_list [] with torch.no_grad(): for tf in aug_tfs: x tf(img).unsqueeze(0).to(device) probs F.softmax(model(x), dim1) probs_list.append(probs) avg_probs torch.stack(probs_list).mean(dim0) idx avg_probs.argmax(dim1).item() return [cat, dog][idx], avg_probs[0][idx].item()RandomHorizontalFlip(p1.0)在推理时强制翻转因为训练时见过翻转样本模型对翻转不变。两次推理取平均置信度更稳。如果还想再稳可以加RandomRotation(10)和CenterCrop但收益递减两到三次就够了。我自己的习惯是先用迁移学习跑到 95%再加余弦退火到 96%最后加 TTA 到 97% 左右。再往上就要换更大骨干或更多数据了性价比不高。这套流程在猫狗分类上足够稳迁移到其他二分类任务也基本不用改结构改数据管道和类别数就行。希望帮到你。本文还有配套的精品资源点击获取