机器学习猫狗识别项目实战:从源码到可运行模型的完整指南 简介这份资源是基于机器学习的猫狗识别算法完整源码包面向计算机、人工智能、大数据、数学及电子信息等相关专业的学生与开发者可用于课程设计、期末大作业、毕业设计或自学参考。压缩包共12个文件约359KB包含2个Python脚本作为核心算法实现4张jpg与4张png图片用于数据集样本及训练过程可视化另有README说明文档与gitignore配置文件结构精简、便于快速上手。内容涵盖模型训练、预测流程及损失曲线、结果对比等可视化输出能帮助读者理解图像分类任务的完整实现思路与调试方法。目前已有329人学习下载适合具备一定编程基础、希望借助现成代码完成项目实践或对照学习机器学习流程的读者参考使用。1. 猫狗识别源码拆开看一份能跑通的机器学习项目该长什么样很多人拿到「基于机器学习的猫狗识别算法源码.zip」这类压缩包第一反应是解压、找train.py、直接python train.py然后被一堆ModuleNotFoundError和路径报错劝退。我见过太多这样的场景一个在机器学习课程设计选题里被反复选中的题目最后卡在环境配置而不是算法本身。猫狗识别本质是一个二分类图像任务输入是猫或狗的照片输出是 0/1 标签核心链路是「数据读取 → 特征提取 → 模型训练 → 推理验证」。它适合刚入门机器学习、想找一个完整项目练手的人也适合需要交课程设计、想快速复现一份可解释代码的人。但源码包不等于能跑通的项目真正决定成败的是数据组织方式、模型选型和训练参数这三件事。下面我按自己复现这类项目的顺序把每个环节拆开讲清楚。2. 数据准备与目录结构源码跑不起来八成卡在这一步2.1 猫狗数据集的组织方式与常见坑猫狗识别最常用的公开数据集是 Kaggle 的 Dogs vs. Cats原始结构是train/下混放cat.0.jpg、dog.0.jpg这种命名没有按类别分文件夹。而绝大多数源码默认的读取方式是ImageFolder风格要求目录长这样dataset/ ├── train/ │ ├── cat/ │ │ ├── cat.001.jpg │ │ └── ... │ └── dog/ │ ├── dog.001.jpg │ └── ... ├── val/ │ ├── cat/ │ └── dog/如果你直接拿原始 Kaggle 数据丢进去ImageFolder会把train/当成一个类别训练出来的模型永远输出同一个结果准确率卡在 50% 左右——这是最隐蔽的翻车方式因为代码不报错只是学不到东西。我一般会先写一个整理脚本把混放的文件按文件名前缀分到对应类别目录同时切出验证集import os import shutil import random src_dir raw_train # 原始混放目录 dst_dir dataset # 整理后目录 val_ratio 0.2 # 验证集比例 random.seed(42) # 固定随机种子保证可复现 for split in [train, val]: 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) split_idx int(len(files) * (1 - val_ratio)) for i, fname in enumerate(files): cls cat if fname.startswith(cat) else dog split train if i split_idx else val shutil.copy(os.path.join(src_dir, fname), os.path.join(dst_dir, split, cls, fname))这段脚本做了三件事建目录、打乱顺序、按比例复制。val_ratio设 0.2 是常见做法数据量小于 5000 张时可以降到 0.1避免验证集太小导致指标抖动。random.seed(42)不是可有可无的没有它每次跑出来的划分不同你没法判断模型变好是因为改了参数还是换了数据。提示整理完后用find dataset -type f | wc -l确认文件总数和原始数据一致少文件通常是文件名大小写或扩展名不统一导致的。2.2 图像预处理与数据增强的参数怎么设数据整理好之后读取环节要统一尺寸和归一化。猫狗图片原始尺寸参差不齐直接送进网络会报维度错误。常见做法是缩放到 224×224 或 128×128前者适配 ResNet 这类预训练模型后者适合自己搭的小 CNN训练更快。from torchvision import transforms train_tf transforms.Compose([ transforms.Resize((128, 128)), # 统一尺寸 transforms.RandomHorizontalFlip(), # 随机水平翻转增强泛化 transforms.RandomRotation(10), # 小角度旋转模拟拍摄角度差异 transforms.ToTensor(), # 转成张量像素值归到 [0,1] transforms.Normalize([0.5]*3, [0.5]*3) # 归一化到 [-1,1] ]) val_tf transforms.Compose([ transforms.Resize((128, 128)), transforms.ToTensor(), transforms.Normalize([0.5]*3, [0.5]*3) ])训练集用增强验证集不用这是铁律。RandomHorizontalFlip对猫狗识别几乎无副作用因为左右翻转后猫还是猫。RandomRotation(10)角度别开太大超过 20 度会出现大量黑边反而引入噪声。Normalize的均值和方差设 0.5 是简化写法如果追求更高精度可以先用脚本统计训练集的真实均值和方差再填进去。3. 模型选型与训练从自己搭 CNN 到迁移学习3.1 自己搭一个小 CNN 的最小可用结构如果课程设计要求「自己实现」通常不允许直接调预训练模型。这时候搭一个 3 层卷积的小网络就够了猫狗二分类在几千张图上能到 80% 以上准确率。import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(128 * 16 * 16, 256), nn.ReLU(), nn.Dropout(0.5), nn.Linear(256, 2) # 二分类输出 2 个 logits ) def forward(self, x): return self.classifier(self.features(x))输入 128×128 经过三次池化变成 16×16通道数 128所以全连接层输入是128*16*16。Dropout(0.5)放在全连接之间是防止过拟合最省事的手段。输出维度写 2 而不是 1配合CrossEntropyLoss使用比用 Sigmoid BCELoss 更稳定梯度不容易消失。3.2 迁移学习什么时候该用预训练权重自己搭的 CNN 在数据量少于 2000 张时容易过拟合验证准确率上不去。这时候换成 ResNet18 加载 ImageNet 预训练权重只训练最后的全连接层通常能把准确率拉高 5 到 10 个百分点。import torchvision.models as models import torch.nn as nn model models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT) for param in model.parameters(): param.requires_grad False # 冻结主干 model.fc nn.Linear(model.fc.in_features, 2) # 替换分类头冻结主干后只训练fc层显存占用小、收敛快。如果数据量超过 5000 张可以解冻最后两个 stage 做微调学习率调到 1e-4 量级。注意weights参数在新版 torchvision 里替代了旧的pretrainedTrue写错会直接报 TypeError。3.3 训练循环里必须盯住的三个量训练脚本写完后真正决定结果的是 loss、学习率和 batch size 的配合。我一般用 Adam学习率 1e-3batch size 32跑 20 个 epoch 看趋势。import torch from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder train_ds ImageFolder(dataset/train, transformtrain_tf) val_ds ImageFolder(dataset/val, transformval_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers2) val_loader DataLoader(val_ds, batch_size32, shuffleFalse) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.fc.parameters(), lr1e-3) for epoch in range(20): model.train() for imgs, labels in train_loader: optimizer.zero_grad() loss criterion(model(imgs), labels) loss.backward() optimizer.step() # 每个 epoch 后在验证集上评估 model.eval() correct total 0 with torch.no_grad(): for imgs, labels in val_loader: preds model(imgs).argmax(dim1) correct (preds labels).sum().item() total labels.size(0) print(fepoch {epoch}, val_acc {correct/total:.4f})shuffleTrue只给训练集验证集必须关掉否则评估结果不可复现。num_workers在 Windows 上设 0 更稳设大了容易卡死。如果训练 loss 一直不降先检查学习率是不是太大把 1e-3 降到 1e-4 试一轮如果训练 loss 降但验证 loss 上升说明过拟合加 Dropout 或减少全连接层参数量。4. 推理与验证模型训完之后怎么确认它真的能用4.1 单张图片推理的完整流程训练完保存权重后推理脚本要保证和训练时用完全相同的预处理否则预测结果会莫名其妙地差。from PIL import Image import torch model.load_state_dict(torch.load(best.pth, map_locationcpu)) model.eval() img Image.open(test.jpg).convert(RGB) tensor val_tf(img).unsqueeze(0) # 加 batch 维度 with torch.no_grad(): logits model(tensor) prob torch.softmax(logits, dim1) pred logits.argmax(dim1).item() print(f预测类别: {[cat,dog][pred]}, 置信度: {prob[0][pred]:.4f})convert(RGB)不能省有些图片是 RGBA 或灰度直接送进网络会报通道数不匹配。unsqueeze(0)是补 batch 维度单张图也要凑成[1,3,128,128]。map_locationcpu在没 GPU 的机器上加载权重时必须加否则会报 CUDA 相关错误。4.2 用混淆矩阵判断模型是不是偏科只看准确率容易被骗。如果验证集里猫 500 张、狗 500 张模型全预测成狗也有 50% 准确率。这时候要看混淆矩阵真实 \ 预测猫狗猫46040狗55445对角线是预测对的非对角线是错的。如果猫那一行错得明显多说明模型对猫的特征学得不够可能是猫的图片里背景干扰大或者训练集里猫的样本偏少。我一般会把这个矩阵打印出来比准确率更能说明问题。5. 避坑与排查源码复现时最容易踩的五个坑5.1 报错CUDA out of memory但显存明明够现象batch size 设 32 就爆显存降到 8 才能跑。原因通常是验证阶段没有加torch.no_grad()导致计算图一直累积。解决所有推理代码块外面套with torch.no_grad():验证和测试阶段都要加。5.2 训练准确率 99% 但验证准确率 50%现象训练集上几乎全对验证集上跟瞎猜一样。原因一般是数据泄漏——整理数据时把同一张图的副本分到了训练集和验证集或者验证集用了训练集的增强变换。解决检查文件是否有重名验证集只用Resize ToTensor Normalize不加任何随机变换。5.3 预测结果永远是同一个类别现象不管输入什么图输出都是 cat 或都是 dog。原因通常是ImageFolder读到的目录结构不对所有图片被当成一个类。解决用print(train_ds.classes)确认类别列表是[cat,dog]而不是[train]用print(len(train_ds))确认样本数正常。5.4 加载权重时报Missing key(s) in state_dict现象load_state_dict报一堆 key 不匹配。原因一般是保存时用了torch.save(model, path)存整个模型加载时又用load_state_dict。解决统一用torch.save(model.state_dict(), path)保存加载时先实例化同结构模型再load_state_dict。如果换了模型结构strictFalse可以跳过不匹配的层但要确认跳过的不是你需要的层。5.5 推理速度慢到无法接受现象单张图推理要好几秒。原因通常是没设model.eval()Dropout 和 BatchNorm 还在训练模式或者每次推理都重新加载模型。解决推理前调model.eval()模型只加载一次循环里复用。如果还慢检查是不是在 CPU 上跑了大模型换成 ResNet18 或缩小输入尺寸。6. 把准确率再往上推一档三个我常用的调优技巧第一个技巧是学习率预热加余弦退火。直接上 1e-3 有时候前期震荡大前 3 个 epoch 用 1e-4 预热之后按余弦曲线降到 1e-6验证准确率通常能稳 1 到 2 个点。代码上就是在 epoch 循环里手动改optimizer.param_groups[0][lr]不需要额外库。第二个技巧是测试时增强TTA。推理时把同一张图水平翻转一次两次预测概率取平均对猫狗这种左右对称的类别几乎稳赚。实现上就是把val_tf和翻转后的val_tf各跑一遍softmax 结果相加再 argmax。代价是推理时间翻倍但离线评估值得做。第三个技巧是错误样本分析。把验证集里预测错的图单独存到一个文件夹肉眼过一遍你会发现很多是标注本身就模棱两可的图或者猫狗同框的图。把这些图从验证集里剔除或重新标注比调参更能提升指标。我自己的习惯是每轮训练后都导出错误样本看三遍再决定下一步改什么。这套流程跑下来猫狗识别从源码到可用模型快的话一个下午就能走通慢的话卡在数据整理上但坑就那几个踩过一次就记住了。希望帮到你。本文还有配套的精品资源点击获取