基于PyTorch的猫狗图像分类:CNN模型训练与调优实践
发布时间:2026/9/20 23:57:45来源:尧图网络
简介基于PyTorch的猫狗二分类图像识别完整项目面向深度学习与计算机视觉初学者、算法练习者及竞赛入门用户提供从模型搭建、数据加载到训练评估的整套可运行代码。资源共647个文件以604张猫狗jpg图片数据集为主体另有11个Python源码、7个pth预训练权重、15个pyc编译文件、2个csv预测提交结果及Markdown说明文档压缩包整体大小约541.86MB文件类型清晰便于按需取用。已有679人学习下载。项目在config.py中集中管理参数配置可适配CPU/CUDA环境入口main.py封装了训练、验证、测试三个命令分别通过python main.py train/val/test调用支持一键运行与结果输出。数据集按猫狗类别整理配合示例提交文件适合快速上手二分类任务、进行模型调参对比也可作为课程设计或Kaggle入门练习的参考方案。1. 基于 PyTorch 的猫狗图片识别项目压缩包里最有价值的不是模型拿到一个叫「基于 PyTorch 卷积神经网络识别猫狗图片项目源码模型数据集使用说明.zip」的压缩包第一反应不该是解压后直接双击 train.py。这个标题已经把项目的最小闭环讲完了数据集、CNN 模型、训练好的权重、使用说明文档。它解决的是图像分类里最具代表性的一类问题——猫狗二分类。看起来只是「一张图判猫还是判狗」背后却是数据目录组织、张量形状推演、训练循环、模型持久化和推理管线这一整套 PyTorch 基础框架链路。适合课程设计、毕业设计和第一次完整跑通 CNN 流水线的工程师。说个反直觉的结论把准确率从 92% 拉到 95% 的关键往往不是网络结构有多深而是数据划分和增强策略是否规范。2. 数据集目录结构与 PyTorch 预处理管线先把输入部分做对任何图像分类项目代码量占比最大的部分在数据侧而不是模型侧。PyTorch 基础框架里和图片打交道的核心是两个接口Dataset 负责「怎么读一张图并给出标签」DataLoader 负责「怎么把一批图打包喂给模型」。很多压缩包在手环境上的问题又集中在 PyTorch 安装的版本配套Python 3.10 配 PyTorch 2.x 与 CUDA 12.x 是当前常见组合装错版本会在 import 阶段或第一次执行to(device)时报错。先把版本对齐再谈数据。2.1 数据集如何划分从文件名到 train/val 子目录经典 Dogs vs Cats 数据集的原始排列是同一个目录下放两万多张图文件名形如cat.0.jpg、dog.0.jpg。模型训练需要显式标签而torchvision.datasets.ImageFolder只认「主目录/类名/图片.jpg」这种结构所以第一步永远是写脚本把数据切分成 train 和 val 两套目录且每一套下面再按cat/、dog/分子目录。import os import random import shutil src raw/train # 原始目录文件名形如 cat.0.jpg / dog.1.jpg dst_root data_catdog # 目标根目录生成 data_catdog/train/cat 这类层级 val_ratio 0.2 random.seed(42) for cls in [cat, dog]: files [f for f in os.listdir(src) if f.startswith(cls .)] random.shuffle(files) # 打乱顺序防止图片按编号连号带来的分布偏差 split_idx int(len(files) * (1 - val_ratio)) # 前 80% 进训练后 20% 进验证 for phase, subset in [(train, files[:split_idx]), (val, files[split_idx:])]: out_dir os.path.join(dst_root, phase, cls) os.makedirs(out_dir, exist_okTrue) # 重复执行脚本不会因目录已存在而报错 for fname in subset: shutil.copy(os.path.join(src, fname), os.path.join(out_dir, fname)) print(f{cls}: train{split_idx}, val{len(files) - split_idx})这段脚本的逻辑是按文件名前缀区分类别random.seed(42)固定 shuffle 顺序保证任何机器上重跑得到的划分一致。val_ratio取 0.2 是图像分类任务里的常用默认值数据量小或类别不平衡时可以把验证集比例提到 0.3。我一般用shutil.copy而不是shutil.move因为划分脚本如果路径写错原始文件还在重新跑一遍即可恢复。最终目录结构如下data_catdog/ ├── train/ │ ├── cat/ (8000 张) │ └── dog/ (8000 张) └── val/ ├── cat/ (2000 张) └── dog/ (2000 张)如果数据源不是这种命名方式而是 CSV 标注文件就用 pandas 读取 label 列再按同样的思路搬文件核心不变最后一定要落成 ImageFolder 认得的树形结构。ImageFolder 会把cat和dog两个子目录名映射成 0 和 1这个映射顺序取决于目录名的字母序训练和推理阶段必须复用同一份映射不能猜。2.2 transform 预处理与 DataLoader 参数表图片读进来是 H×W×3 的 uint8 数组卷积神经网络期望的是经过归一化的浮点张量这一步由torchvision.transforms完成。常见做法是直接复用 ImageNet 数据集的 mean 和 std这两个数组是公开的常量很多项目把它写死在代码里。from torchvision import datasets, transforms mean [0.485, 0.456, 0.406] std [0.229, 0.224, 0.225] train_tf transforms.Compose([ transforms.Resize((224, 224)), # 统一输入尺寸后续换预训练网络时不用改 transforms.RandomHorizontalFlip(p0.5), # 水平翻转猫狗左右对称属于安全增强 transforms.ToTensor(), # uint8 - float32并把 HxWxC 转成 CxHxW transforms.Normalize(mean, std), ]) val_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean, std), # 验证集与训练集必须用同一套 mean/std ]) train_ds datasets.ImageFolder(data_catdog/train, transformtrain_tf) val_ds datasets.ImageFolder(data_catdog/val, transformval_tf) print(train_ds.class_to_idx) # 输出 {cat: 0, dog: 1}推理阶段要复用这个映射参数说明Resize((224, 224))是 ImageNet 系列网络的事实标准不管后面是手写 CNN 还是换成 ResNet18这个尺寸都不用再動。RandomHorizontalFlip的 p0.5 表示每个样本有一半概率被翻转狗和猫的左右翻转不会改变类别语义是成本最低、收益最稳的增强。ToTensor把 0~255 的像素缩放到 0~1Normalize再按通道标准化让数据分布围绕零点附近梯度更新更平顺。DataLoader 的参数直接影响训练速度和显存占用逐项说明如下表参数训练时推荐值说明batch_size32显存不足用 16224×224×3 的图32 张一批普通消费级显卡可承受shuffleTrue每个 epoch 重新打乱顺序防止模型记住固定样本顺序num_workers4Windows 建议 0 或 2子进程预取数据Windows 下 worker 过多容易触发递归报错pin_memoryTrueGPU 训练时把数据钉在页锁定内存减少 CPU 到 GPU 的复制耗时from torch.utils.data import DataLoader train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue)有一个平台细节容易忽略num_workers 在 Linux 上开到 8 都很正常Windows 上如果不写if __name__ __main__:保护直接运行会抛 RuntimeError。所以 Windows 下调参时把 num_workers 降到 0 或 2 最省心。shuffle 对验证集必须为 False否则每次评估的样本顺序都不同日志里的 val acc 曲线会带着随机噪声难以判断训练是否真的在变好。2.3 数据增强参数与过拟合的关系小模型的参数量其实不小相对两万张图来说纯 Resize 训练时验证准确率通常到 90% 出头就顶住了因为模型把大量能力用在了记忆训练集上。增强的本质是制造更多语义等价的样本参数不宜过度RandomRotation(10)给 10 度以内的轻微旋转ColorJitter模拟光照差异这些都是猫狗图片里真实的拍摄扰动。train_tf_aug transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomRotation(10), # 超过 10 度会把狗鼻子转出画面类别信息受损 transforms.ColorJitter(brightness0.2, contrast0.2), transforms.RandomHorizontalFlip(p0.5), transforms.ToTensor(), transforms.Normalize(mean, std), ])增强只挂在 train_loader 对应的 transform 上val_tf 保持确定性操作。验证集一旦加入随机翻转或旋转每次 evaluate 的指标都会抖动你很难判断一个 epoch 到底有没有变好。「训练增强、验证固定」这条规则无论项目多大多小都别破坏。3. 卷积神经网络结构与 CNN 模型代码从结构图到可运行模块卷积神经网络结构图在网上能搜到大量带箭头和色块的示意图落到 PyTorch 代码里其实只有三个核心算子卷积nn.Conv2d、池化nn.MaxPool2d、全连接nn.Linear外加激活函数与归一化层。能把它们之间的配合规则讲清楚比照着结构图抄一遍代码更有价值。3.1 卷积核、padding 与通道数的搭配逻辑浅层卷积核负责边角、色块这类底层特征深层卷积核负责纹理和局部形状这是 CNN 的经典认知。设计一个能跑的小网络三条约定足够3×3 卷积核配 padding1 保持空间尺寸不缩小通道数从 32 开始逐层翻倍控制计算量在入门级显卡可承受的范围池化层只负责减半尺寸、扩大感受野。下面这种结构用于猫狗二分类是可靠的起步配置层输出形状参数要点Conv2d(3, 32, 3, padding1) BatchNorm ReLU224×224×32输入通道 3 表示 RGBMaxPool2d(2)112×112×32空间尺寸减半Conv2d(32, 64, 3, padding1) BatchNorm ReLU112×112×64通道数翻倍MaxPool2d(2)56×56×64Conv2d(64, 128, 3, padding1) BatchNorm ReLU56×56×128MaxPool2d(2)28×28×128三次池化后 224→28通道数 32→64→128 是入门级 CNN 的常见规模。再往上加深就需要考虑显存占用和过拟合风险两万张图养一个 5 层卷积网络绰绰有余但养 ResNet50 就比较吃力。padding1 的关键作用在于用补零保持前后尺寸一致设计者只需要盯住池化层就能推算特征图尺寸变化后面计算全连接输入维度才不会错。3.2 定义 CatDogCNN 模型与前向传播形状推演手写 CNN 分类模型时最常见的报错是 size mismatch根源就是全连接层输入维度写错。推算方法很简单224 经过 3 次MaxPool2d(2)变成 28最后一层卷积输出 128 个通道于是全连接输入就是 128×28×28。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), ) self.classifier nn.Sequential( # 分类部分 nn.Dropout(0.3), nn.Linear(128 * 28 * 28, 256), nn.ReLU(inplaceTrue), nn.Linear(256, num_classes), # 二分类输出 logits不接 softmax ) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) # 展平保留 batch其余压成一维 return self.classifier(x)forward 里先做特征提取再用view(x.size(0), -1)把 128×28×28 展平成 100352 维向量-1由 PyTorch 自动推断。BatchNorm2d 放在卷积和 ReLU 之间是主流做法它能把每层输出统一到合理尺度但 batch_size 小于 8 时建议去掉 BN因为统计量依赖 batch 内样本数batch 太小反而引入抖动。Dropout 只在训练时生效model.eval()后自动关闭这正好配合推理阶段的确定性要求。训练开始前打印一次模型结构能立刻暴露形状问题from torchsummary import summary model CatDogCNN() summary(model, (3, 224, 224))torchsummary 不是 PyTorch 官方库它输出每一层的输出形状与参数量比人肉推算快得多。不想引入额外依赖就按上面的口诀手算本质一样。3.3 损失函数与优化器参数一个容易被忽略的 logits 细节二分类属于单标签分类标准损失函数是nn.CrossEntropyLoss。它内部已经做了 LogSoftmax 和负对数似然计算所以模型最后一层 Linear 输出的原始分数就是 logits不要在外面再接nn.Softmax。训练时在模型外套 Softmax再经交叉熵内部的 LogSoftmax属于双重归一化既拖慢收敛又影响数值精度。优化器方面从头训练这种小网络SGD 配 momentum 是可靠选择Adam 更适合快速验证网络能不能收敛。参数怎么给参考下表优化器学习率关键参数适用场景SGD0.01momentum0.9, weight_decay5e-4从头训练收敛更可靠调好参数后精度上限更高Adam0.001betas(0.9, 0.999)快速试错对学习率不敏感import torch model CatDogCNN() criterion nn.CrossEntropyLoss() optimizer torch.optim.SGD(model.parameters(), lr0.01, momentum0.9, weight_decay5e-4)lr0.01 是 SGD 在图像分类任务里的常见起点比 Adam 的 0.001 高一个量级momentum0.9 用前一步梯度方向加权当前梯度抑制方向震荡weight_decay5e-4 是 L2 正则在损失上给权重加惩罚正好匹配数据量不大、容易过拟合的猫狗任务。换 Adam 时 lr 保持默认 0.001照抄 SGD 的 0.01 大概率发散。4. 训练循环、验证评估与模型保存加载网上流传的源码里训练脚本能跑通但换个环境就断多半是设备判断、随机种子、模型保存这三处没处理干净。从训练循环到加载本地模型这里一次性打通。4.1 设备判断、随机种子与训练循环搭建训练脚本开头的固定动作是判断有没有 GPU并把随机种子固定下来。随机种子不固定每次运行初始权重都不同你根本判断不了调参是变好了还是随机波动。import torch import random import numpy as np random.seed(42) np.random.seed(42) torch.manual_seed(42) if torch.cuda.is_available(): torch.cuda.manual_seed_all(42) device torch.device(cuda if torch.cuda.is_available() else cpu) model CatDogCNN().to(device) print(running on, device)CPU 和 GPU 的随机数生成器是分开的seed 要分别设置。torch.cuda.manual_seed_all覆盖全部可见 GPU。固定 seed 后如果两次训练结果仍不一致就要考虑 DataLoader 的 num_workers 了多进程取数会引入系统级随机因素。训练循环本身按 epoch 组织每个 epoch 内遍历一遍 DataLoaderdef train_one_epoch(model, loader, criterion, optimizer, device): model.train() total_loss, correct, total 0.0, 0, 0 for images, labels in loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() # 梯度清零必须在 backward 之前 logits model(images) loss criterion(logits, labels) loss.backward() optimizer.step() total_loss loss.item() * images.size(0) correct (logits.argmax(dim1) labels).sum().item() total labels.size(0) return total_loss / total, correct / total训练循环里常犯的错有三个漏掉zero_grad梯度跨 batch 累加loss 越训越高样本数量不是 batch 整数倍时用total_loss / len(loader)而不是除以样本总数loss 曲线虚高忘记切model.train()BN 和 Dropout 一直在 eval 状态下工作。上面代码中argmax(dim1)取每个样本 logits 最大值的下标和 labels 逐元素比较correct 累计得到该 epoch 分类正确的样本数除以 total 得到准确率。4.2 验证评估与早停策略每个 epoch 结束后要跑一遍验证集。验证和训练最大的差异不在网络代码本身而在两个上下文torch.no_grad()与model.eval()。torch.no_grad() def evaluate(model, loader, device): model.eval() correct, total 0, 0 for images, labels in loader: images, labels images.to(device), labels.to(device) logits model(images) correct (logits.argmax(dim1) labels).sum().item() total labels.size(0) return correct / totaltorch.no_grad()告诉 PyTorch 不必为验证过程构建计算图显存占用和耗时都会明显下降。model.eval()关闭 Dropout、让 BatchNorm 使用运行均值而不是当前 batch 的统计量。这两个机制不同缺一不可。每个 epoch 里先 train 再 eval 的顺序也不要搞反。早停的常见做法是记录验证集 acc连续 N 个 epoch 不刷新最优值就降低学习率或停止。N 取 5~10 比较常用太小容易错过平台期后的二次上升太大则浪费训练时间。best_acc 0.0 for epoch in range(30): train_loss, train_acc train_one_epoch(model, train_loader, criterion, optimizer, device) val_acc evaluate(model, val_loader, device) print(fepoch {epoch1:02d} | loss {train_loss:.4f} | train acc {train_acc:.4f} | val acc {val_acc:.4f}) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), catdog_best.pth)这段伪训练流程把「验证集变好才保存」作为唯一保存条件避免最后一个 epoch 的权重不是最优。训练结束后的最优模型应该是 best 版本而不是最后一个 epoch 的版本这个细节经常被忽略。4.3 checkpoint 保存与加载本地模型推理模型保存有两个层次只存权重state_dict或把模型结构一并存成 checkpoint。对于赛后复用和部署存 state_dict 更干净把类别映射也存进去推理时就不用猜 index 0 到底是猫还是狗。扩展名 .pth 只是习惯压缩包里的模型文件也常见 .pt。ckpt { model_state: model.state_dict(), # 权重不携带网络结构 class_to_idx: train_ds.class_to_idx, # {cat: 0, dog: 1} best_acc: best_acc, } torch.save(ckpt, catdog_best.pth)加载时要先重新创建模型实例再把权重灌进去。提示PyTorch 2.6 之后 torch.load 默认 weights_onlyTrue。上面这种只含 state_dict 和基本类型映射的 checkpoint 可以直接加载如果历史上把整个 model 对象塞进了 checkpoint加载报错时先检查这里。def load_model(ckpt_path, device): model CatDogCNN() ckpt torch.load(ckpt_path, map_locationdevice) # GPU 权重在 CPU 机器上加载时这句必须有 model.load_state_dict(ckpt[model_state]) model.to(device).eval() return model, ckpt[class_to_idx]单张图片推理时必须走与验证集完全一致的预处理管线并把 224×224×3 的图片补上 batch 维度变成 1×3×224×224from PIL import Image import torchvision.transforms as T def predict_one(model, image_path, class_to_idx, device): idx_to_class {v: k for k, v in class_to_idx.items()} tf T.Compose([ T.Resize((224, 224)), T.ToTensor(), T.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) img Image.open(image_path).convert(RGB) # 灰度图、RGBA 图统一转成 RGB避免通道维度报错 x tf(img).unsqueeze(0).to(device) # unsqueeze(0) 增加 batch 维度 with torch.no_grad(): probs torch.softmax(model(x), dim1)[0] # 推理时手动接 softmax 拿概率 pred int(probs.argmax()) return idx_to_class[pred], probs[pred].item() model, class_to_idx load_model(catdog_best.pth, device) cls_name, prob predict_one(model, test_dog.jpg, class_to_idx, device) print(fpredicted: {cls_name}, confidence: {prob:.4f})注意推理与训练在 softmax 处理上的差异训练时 CrossEntropyLoss 内部完成归一化模型输出 logits推理时为了拿到置信度需要在模型外部调用torch.softmax。map_locationdevice解决「GPU 上训练的权重拿到 CPU 机器上加载」的经典报错不写这一句会看到 CUDA 相关的 RuntimeError。5. 卡在准确率上不去时训练曲线、混淆矩阵与错误样本定位模型能跑通只是第一个里程碑。当验证准确率停在 92% 上下不再动先画训练曲线再看混淆矩阵和错误样本这两件事比换网络结构更值得先做。5.1 训练曲线三种形态与对应调整每个 epoch 的 train loss 和 val acc 一起记录按 epoch 画两条曲线。三种典型形态对应三种不同问题曲线形态诊断对应动作train acc 接近 1.0val acc 停在低点过拟合模型在背训练集增强数据、Dropout 提到 0.5、减小网络容量train acc 和 val acc 同步偏低欠拟合容量不足或学习率过小加深网络、通道翻倍、lr 调大一档val acc 在某点后不再提升平台期用 StepLR 把 lr 降一个量级继续训练 10 个 epoch判断过拟合的基线是 train 和 val 的差距。差距 5 个百分点以内属于正常波动超过 10 个点基本就是过拟合。猫狗二分类这个小任务小网络配增强收敛到 94% 左右是正常水平不必死磕 99.9%。5.2 用混淆矩阵与错误样本定位系统性错误准确率只看整体混淆矩阵能看出模型在哪个方向上犯的错更多。猫狗图片里常见的系统性错误源是背景干扰白色墙前的白猫、毛发纹理接近大型犬的短毛狗这些从数字上看不出原因必须落到具体图片上看。from sklearn.metrics import confusion_matrix, classification_report preds, targets [], [] for images, labels in val_loader: images images.to(device) logits model(images) preds.extend(logits.argmax(dim1).cpu().tolist()) targets.extend(labels.tolist()) cm confusion_matrix(targets, preds) print(cm) print(classification_report(targets, preds, target_names[cat, dog]))再写一段收集错误样本的辅助代码遍历验证集记录预测概率低于 0.6 且类别错误的样本路径打印前 20 个。这批样本里通常有两种情况一是模糊到人眼都难分辨的图片二是标注错误。原始猫狗数据集里确实混有少量错标图把这些样本从 val 里清掉或修正标签val acc 会立刻上升。把错误样本收集脚本留在项目里当作常驻调试工具比反复盯终端日志高效得多。模型修到平台期后还有一个零成本技巧推理时对同一张图做水平翻转得到两组概率后取平均即 TTATest Time Augmentation。实现上只是把预测函数里的单次 forward 换成两次probs [] for tf in [val_tf, T.Compose([T.Resize((224, 224)), T.RandomHorizontalFlip(p1.0), T.ToTensor(), T.Normalize(mean, std)])]: x tf(img).unsqueeze(0).to(device) with torch.no_grad(): probs.append(torch.softmax(model(x), dim1)) final_prob (probs[0] probs[1]) / 2这个技巧实现成本极低通常在原有准确率上带来 0.3%~1% 的提升而且在置信度输出上更平滑。先把混淆矩阵、错误样本和 TTA 这三件事做完再去考虑引入 ResNet 预训练权重路径就清楚多了。本文还有配套的精品资源点击获取
网站建设高端定制企业官网