ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于CNN的猫狗识别实战:从数据集到推理部署完整指南

基于CNN的猫狗识别实战:从数据集到推理部署完整指南 简介这份资源面向深度学习入门与计算机视觉实践者提供一套基于卷积神经网络的猫狗图像分类完整源码与数据集帮助读者理解从数据准备到模型训练、测试的全流程。包内共10个文件以7个Python脚本为主辅以2个txt说明与1个md文档压缩包约9KB涵盖AlexNet与VGG16两种可选模型实现、训练与测试入口、数据分类脚本及依赖清单结构清晰便于按模块学习。资源已吸引46人学习下载适合希望动手复现图像二分类任务、熟悉PyTorch模型搭建与数据加载流程的读者。通过阅读代码可掌握argparse参数配置、单通道图像清洗、旧版数据加载等实用技巧并借助data_classify.py完成训练集与测试集划分快速搭建自己的分类实验环境。1. 猫狗识别项目拆解从数据集到 CNN 推理的完整链路很多人第一次接触图像分类都是从猫狗识别这个经典任务开始的。它看起来简单——无非是把一张图分成猫或狗两类——但真正动手跑一遍就会发现从数据集组织、模型搭建、训练调参到最终推理部署每一步都有具体的工程决策要做。这个标题指向的是一套完整的 Python 深度学习方案基于卷积神经网络实现猫狗图像分类包含源码和数据集。它解决的核心问题是给定一批猫和狗的图片训练一个模型让它对没见过的图片也能正确分类。适合谁刚学完 Python 基础、想找一个能跑通的深度学习项目练手的开发者已经了解 CNN 原理但没完整走过一遍训练流程的算法初学者以及需要快速搭建图像二分类基线、再迁移到自己业务场景的工程师。下面我会按实际落地的顺序把这条链路拆开讲清楚。2. 数据集准备与目录结构猫狗图片怎么组织才能直接喂给模型2.1 猫狗数据集的常见来源与规模判断猫狗图像分类最常用的公开数据集来自 Kaggle 的 Dogs vs. Cats 竞赛训练集包含 25000 张图片猫和狗各 12500 张测试集 12500 张。这个规模对于从零训练一个中小型 CNN 是够用的但如果你的机器没有 GPU或者想快速验证流程建议先从训练集中抽取 20004000 张做子集实验。我一般会按 8:1:1 划分训练集、验证集和测试集确保每个类别在三个集合中的比例一致。数据集下载后通常是扁平的目录所有图片混在一起文件名类似cat.0.jpg、dog.1.jpg。这种命名方式虽然包含了类别信息但直接用来训练并不方便。常见做法是把它整理成按类别分文件夹的结构这样后续用ImageFolder或flow_from_directory就能自动读取标签。2.2 用 Python 脚本把扁平目录整理成训练结构下面这段脚本做三件事读取原始图片目录、按文件名前缀判断类别、按比例复制到train/val/test三个子目录下对应的cat和dog文件夹中。import os import shutil import random from pathlib import Path # 原始数据目录和输出目录 RAW_DIR Path(data/raw) # 存放 cat.0.jpg, dog.1.jpg 等 OUT_DIR Path(data/processed) # 输出结构化目录 SPLIT_RATIO {train: 0.8, val: 0.1, test: 0.1} random.seed(42) # 固定随机种子保证可复现 def get_label(filename): 根据文件名前缀返回类别标签 name filename.lower() if name.startswith(cat): return cat elif name.startswith(dog): return dog return None # 收集所有有效图片 all_images [] for f in RAW_DIR.iterdir(): if f.suffix.lower() in (.jpg, .jpeg, .png): label get_label(f.name) if label: all_images.append((f, label)) # 按类别分组后分别打乱避免某类集中在某个集合 cat_images [x for x in all_images if x[1] cat] dog_images [x for x in all_images if x[1] dog] random.shuffle(cat_images) random.shuffle(dog_images) def split_and_copy(images, label): n len(images) n_train int(n * SPLIT_RATIO[train]) n_val int(n * SPLIT_RATIO[val]) splits { train: images[:n_train], val: images[n_train:n_train n_val], test: images[n_train n_val:] } for split_name, items in splits.items(): target_dir OUT_DIR / split_name / label target_dir.mkdir(parentsTrue, exist_okTrue) for img_path, _ in items: shutil.copy2(img_path, target_dir / img_path.name) split_and_copy(cat_images, cat) split_and_copy(dog_images, dog) print(数据集划分完成)这段代码的关键点在于先按类别分组再分别打乱而不是把所有图片混在一起打乱。如果混在一起打乱某个类别可能因为随机性在验证集中占比过高或过低导致验证指标波动大。random.seed(42)是为了让每次运行结果一致方便调试。shutil.copy2会保留文件的元数据比shutil.copy更稳妥。参数方面SPLIT_RATIO可以根据数据量调整。如果总图片少于 2000 张验证集和测试集各取 10% 可能只有一两百张评估结果不够稳定这时候可以考虑用交叉验证或者把比例调成 7:1.5:1.5。另外如果原始文件名不是cat.或dog.开头需要改get_label函数的判断逻辑。2.3 图片尺寸统一与数据增强的预处理策略CNN 要求输入图片尺寸一致。猫狗图片的原始尺寸参差不齐常见做法是在训练时统一缩放到 224×224 或 128×128。224×224 是 ImageNet 预训练模型的标准输入如果你打算用迁移学习就选这个尺寸如果从零训练一个小型 CNN128×128 能显著降低计算量精度损失通常在可接受范围内。数据增强是防止过拟合的重要手段。对于猫狗分类常用的增强方式包括随机水平翻转、随机旋转 ±15 度、随机裁剪、颜色抖动。注意不要用垂直翻转因为猫狗的正常姿态不会上下颠倒垂直翻转会引入不合理的样本。下面是一个用torchvision做预处理的例子from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((128, 128)), # 统一尺寸 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]) # ImageNet 统计值 ]) val_transform transforms.Compose([ transforms.Resize((128, 128)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])Normalize里的均值和标准差是 ImageNet 的统计值即使你从零训练用这组值也没有坏处它能让输入分布更接近零均值。如果你自己算过数据集的均值和方差也可以替换成自己的值但差别通常不大。注意验证集和测试集只能用val_transform不能加随机增强。否则每次评估时图片都在变指标会不稳定无法判断模型是否真的在收敛。3. CNN 模型搭建从零写一个还是用预训练网络3.1 从零搭建一个轻量 CNN 的结构设计如果你刚入门我建议先从零搭一个 45 层的 CNN把前向传播、卷积核数量、池化方式这些概念跑通。下面是一个适合 128×128 输入的轻量结构import torch import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes2): super(SimpleCNN, self).__init__() self.features nn.Sequential( # 输入 3×128×128 nn.Conv2d(3, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 32×64×64 nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 64×32×32 nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 128×16×16 nn.Conv2d(128, 256, kernel_size3, padding1), nn.BatchNorm2d(256), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 256×8×8 ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d((1, 1)), # 256×1×1 nn.Flatten(), nn.Dropout(0.5), nn.Linear(256, num_classes) ) def forward(self, x): x self.features(x) x self.classifier(x) return x这个结构的设计逻辑每经过一次池化特征图尺寸减半通道数翻倍。BatchNorm2d放在卷积和激活之间能加速收敛、降低对初始化的敏感度。AdaptiveAvgPool2d((1,1))替代了全连接前的展平操作好处是不管输入尺寸怎么变输出都是固定维度而且参数量更少。Dropout(0.5)在全连接层前做正则化防止过拟合。参数方面卷积核数量从 32 起步逐层翻倍到 256这是比较经典的设置。如果你的 GPU 显存有限可以把每层的通道数减半。kernel_size3, padding1保证卷积后特征图尺寸不变只有池化才降尺寸这样结构更清晰。3.2 迁移学习用 ResNet18 做猫狗分类的快速方案从零训练一个 CNN 在 25000 张图上大概需要几十个 epoch 才能收敛到 90% 左右的准确率。如果你想要更快出结果或者数据量只有几千张迁移学习是更务实的选择。ResNet18 在 ImageNet 上预训练过已经学会了提取边缘、纹理、形状等通用特征你只需要替换最后的全连接层微调几轮就能达到很高的精度。import torchvision.models as models import torch.nn as nn def build_resnet18(num_classes2, freeze_backboneTrue): model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) if freeze_backbone: # 冻结除 fc 层以外的所有参数 for param in model.parameters(): param.requires_grad False # 替换最后的全连接层 in_features model.fc.in_features model.fc nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, num_classes) ) return modelfreeze_backboneTrue表示只训练最后的分类层这在数据量少的时候能防止过拟合。如果你有足够的数据比如完整的 25000 张可以把freeze_backbone设为False让整个网络都参与微调但学习率要设小一点比如 1e-4避免破坏预训练权重。注意用预训练模型时输入尺寸必须是 224×224预处理也要用 ImageNet 的均值和标准差。如果你前面按 128×128 做的预处理这里要改过来。3.3 训练循环里的损失函数与优化器选择猫狗分类是二分类任务损失函数用CrossEntropyLoss就行它内部包含了 Softmax。优化器方面从零训练时用Adam学习率 1e-3 起步比较稳微调预训练模型时用SGD配合动量 0.9、学习率 1e-4 更常见。下面是一个完整的训练循环骨架import torch.optim as optim from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder # 加载数据集 train_dataset ImageFolder(data/processed/train, transformtrain_transform) val_dataset ImageFolder(data/processed/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4) device torch.device(cuda if torch.cuda.is_available() else cpu) model SimpleCNN(num_classes2).to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3) for epoch in range(20): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() # 验证阶段 model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() val_acc correct / total print(fEpoch {epoch1}, Loss: {running_loss/len(train_loader):.4f}, Val Acc: {val_acc:.4f})batch_size32是常见起点显存不够就降到 16 或 8。num_workers4在 Linux 上能加速数据加载Windows 上如果报错就改成 0。验证阶段一定要加model.eval()和torch.no_grad()前者关闭 Dropout 和 BatchNorm 的训练行为后者节省显存。4. 训练过程排查loss 不降、过拟合、显存爆了怎么处理4.1 现象训练 loss 震荡不下降原因通常有三种学习率太大、数据标签有问题、BatchNorm 在小 batch 下统计量不准。先检查学习率把 1e-3 降到 1e-4 试试。然后抽查几张图片和对应的标签是否匹配用ImageFolder时如果文件夹名字写错标签会静默出错。最后如果batch_size小于 8BatchNorm 的均值和方差估计会很不稳定这时候要么增大 batch要么改用 GroupNorm。4.2 现象训练准确率很高但验证准确率很低这是典型的过拟合。解决手段按优先级排第一加数据增强尤其是随机裁剪和颜色抖动第二加 Dropout在全连接层前设 0.5第三用预训练模型冻结骨干网络第四如果数据量实在少考虑用 k 折交叉验证来更充分地利用数据。不要一上来就加 L2 正则化它的效果通常不如前几种明显。4.3 现象CUDA out of memory显存不够时先降batch_size这是最直接有效的。如果降到 1 还是爆说明模型本身太大可以把卷积通道数减半或者把输入尺寸从 224 降到 128。另外验证阶段记得加torch.no_grad()否则中间激活值会一直保留在显存里。还有一个容易忽略的点num_workers设得太高也会占用额外内存Linux 上 4 到 8 够用Windows 上建议设 0。4.4 现象验证集准确率波动很大如果每个 epoch 的验证准确率上下跳动超过 5%通常是验证集太小或者数据分布不均匀。检查验证集中猫和狗的比例是否接近 1:1如果偏差大重新划分数据集。另外验证时不要用随机增强确保每次评估的输入是一致的。如果验证集只有几百张可以考虑用测试集也参与评估取平均来降低波动。4.5 现象模型推理时对某些图片预测置信度很低这通常是因为训练数据中缺少类似场景的图片。比如训练集里都是室内猫狗突然来一张雪地里的狗模型可能就懵了。解决办法是在训练时加入更多样化的背景增强或者用 Grad-CAM 可视化模型的注意力区域看看它到底在关注图片的哪个部分。如果模型关注的是背景而不是主体说明数据增强还不够。5. 推理与部署把训练好的猫狗分类模型用起来5.1 单张图片推理的完整代码训练完成后保存模型权重推理时加载权重并对单张图片做预测。下面是一个完整的推理脚本import torch from PIL import Image from torchvision import transforms def predict(image_path, model_pathbest_model.pth, devicecpu): # 加载模型结构 model SimpleCNN(num_classes2) model.load_state_dict(torch.load(model_path, map_locationdevice)) model.to(device) model.eval() # 预处理 transform transforms.Compose([ transforms.Resize((128, 128)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) image Image.open(image_path).convert(RGB) tensor transform(image).unsqueeze(0).to(device) # 增加 batch 维度 with torch.no_grad(): outputs model(tensor) probabilities torch.softmax(outputs, dim1) confidence, predicted torch.max(probabilities, 1) classes [cat, dog] return classes[predicted.item()], confidence.item() # 使用示例 label, conf predict(test.jpg) print(f预测结果: {label}, 置信度: {conf:.4f})unsqueeze(0)是把单张图片从[3, 128, 128]变成[1, 3, 128, 128]因为模型要求输入有 batch 维度。torch.softmax把输出转成概率分布torch.max同时返回最大值和对应的索引。classes列表的顺序要和训练时ImageFolder的类别顺序一致通常是按文件夹名字母序排列cat在dog前面。5.2 批量推理与结果导出如果要对整个测试集做批量推理并导出结果可以用DataLoader加循环的方式import csv from torchvision.datasets import ImageFolder from torch.utils.data import DataLoader test_dataset ImageFolder(data/processed/test, transformval_transform) test_loader DataLoader(test_dataset, batch_size32, shuffleFalse) model.eval() results [] with torch.no_grad(): for images, labels in test_loader: images images.to(device) outputs model(images) _, predicted torch.max(outputs, 1) for i in range(len(predicted)): results.append({ label: test_dataset.classes[labels[i]], prediction: test_dataset.classes[predicted[i]], correct: labels[i].item() predicted[i].item() }) # 导出 CSV with open(predictions.csv, w, newline) as f: writer csv.DictWriter(f, fieldnames[label, prediction, correct]) writer.writeheader() writer.writerows(results) accuracy sum(r[correct] for r in results) / len(results) print(f测试集准确率: {accuracy:.4f})test_dataset.classes会自动返回类别名称列表顺序和训练时一致。导出 CSV 方便后续做错误分析比如找出所有预测错误的样本看看它们有什么共同特征。5.3 模型保存与加载的注意事项保存模型时推荐只保存state_dict而不是整个模型对象因为整个模型对象依赖具体的类定义换一个文件就加载不了。加载时先实例化模型结构再load_state_dict。如果训练时用了 GPU推理时可能没有 GPUmap_location参数就是用来处理这种设备不匹配的情况。注意如果你用了迁移学习保存的state_dict里包含的是 ResNet18 的结构推理时也要用同样的结构来加载不能换成 SimpleCNN。6. 把猫狗分类精度再提一截三个我反复用到的技巧第一个技巧是学习率预热和余弦退火。从零训练时前几个 epoch 用很小的学习率慢慢升到设定值然后再按余弦曲线衰减能明显减少训练初期的震荡。具体做法是用torch.optim.lr_scheduler.CosineAnnealingLR配合一个简单的线性预热from torch.optim.lr_scheduler import CosineAnnealingLR, LambdaLR warmup_epochs 3 total_epochs 30 base_lr 1e-3 def warmup_lambda(epoch): if epoch warmup_epochs: return (epoch 1) / warmup_epochs return 1.0 optimizer optim.Adam(model.parameters(), lrbase_lr) warmup_scheduler LambdaLR(optimizer, lr_lambdawarmup_lambda) cosine_scheduler CosineAnnealingLR(optimizer, T_maxtotal_epochs - warmup_epochs) for epoch in range(total_epochs): # 训练代码... if epoch warmup_epochs: warmup_scheduler.step() else: cosine_scheduler.step()第二个技巧是测试时增强TTA。推理时对同一张图片做多次不同的变换比如水平翻转、不同裁剪把多次预测的概率取平均。这个技巧几乎不增加训练成本但通常能带来 12 个百分点的精度提升。实现方式很简单对每张图生成 5 个版本分别推理后取平均。第三个技巧是错误样本分析。把验证集中预测错误的图片单独拿出来看你会发现模型翻车的往往是一些模糊、遮挡、姿态极端的样本。针对这些情况可以在训练时加入 CutMix 或 MixUp 增强让模型学会在部分信息缺失的情况下也能判断。CutMix 的实现稍微复杂一点但 PyTorch 社区有很多现成的实现可以参考。这三个技巧我一般会按顺序上先加学习率调度再加 TTA最后根据错误分析决定要不要上 CutMix。不要一次性全加上否则出了问题很难定位是哪个环节导致的。希望帮到你。本文还有配套的精品资源点击获取
返回列表