简介:这份资源面向深度学习入门与计算机视觉实践者,提供一套基于卷积神经网络的猫狗图像分类完整源码与数据集,帮助读者理解从数据准备到模型训练、测试的全流程。包内共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,或者想快速验证流程,建议先从训练集中抽取 2000~4000 张做子集实验。我一般会按 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(parents=True, exist_ok=True) 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 预训练模型的标准输入,如果你打算用迁移学习,就选这个尺寸;如果从零训练一个小型 CNN,128×128 能显著降低计算量,精度损失通常在可接受范围内。
数据增强是防止过拟合的重要手段。对于猫狗分类,常用的增强方式包括:随机水平翻转、随机旋转 ±15 度、随机裁剪、颜色抖动。注意不要用垂直翻转,因为猫狗的正常姿态不会上下颠倒,垂直翻转会引入不合理的样本。下面是一个用torchvision做预处理的例子:
from torchvision import transforms train_transform = transforms.Compose([ transforms.Resize((128, 128)), # 统一尺寸 transforms.RandomHorizontalFlip(p=0.5), # 水平翻转 transforms.RandomRotation(15), # 随机旋转 transforms.ColorJitter(brightness=0.2, contrast=0.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 的结构设计
如果你刚入门,我建议先从零搭一个 4~5 层的 CNN,把前向传播、卷积核数量、池化方式这些概念跑通。下面是一个适合 128×128 输入的轻量结构:
import torch import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes=2): super(SimpleCNN, self).__init__() self.features = nn.Sequential( # 输入 3×128×128 nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 32×64×64 nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 64×32×32 nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 128×16×16 nn.Conv2d(128, 256, kernel_size=3, padding=1), nn.BatchNorm2d(256), nn.ReLU(inplace=True), 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_size=3, padding=1保证卷积后特征图尺寸不变,只有池化才降尺寸,这样结构更清晰。
3.2 迁移学习:用 ResNet18 做猫狗分类的快速方案
从零训练一个 CNN 在 25000 张图上大概需要几十个 epoch 才能收敛到 90% 左右的准确率。如果你想要更快出结果,或者数据量只有几千张,迁移学习是更务实的选择。ResNet18 在 ImageNet 上预训练过,已经学会了提取边缘、纹理、形状等通用特征,你只需要替换最后的全连接层,微调几轮就能达到很高的精度。
import torchvision.models as models import torch.nn as nn def build_resnet18(num_classes=2, freeze_backbone=True): model = models.resnet18(weights=models.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_backbone=True表示只训练最后的分类层,这在数据量少的时候能防止过拟合。如果你有足够的数据(比如完整的 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", transform=train_transform) val_dataset = ImageFolder("data/processed/val", transform=val_transform) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4) val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=4) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = SimpleCNN(num_classes=2).to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=1e-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(f"Epoch {epoch+1}, Loss: {running_loss/len(train_loader):.4f}, Val Acc: {val_acc:.4f}")batch_size=32是常见起点,显存不够就降到 16 或 8。num_workers=4在 Linux 上能加速数据加载,Windows 上如果报错就改成 0。验证阶段一定要加model.eval()和torch.no_grad(),前者关闭 Dropout 和 BatchNorm 的训练行为,后者节省显存。
4. 训练过程排查:loss 不降、过拟合、显存爆了怎么处理
4.1 现象:训练 loss 震荡不下降
原因通常有三种:学习率太大、数据标签有问题、BatchNorm 在小 batch 下统计量不准。先检查学习率,把 1e-3 降到 1e-4 试试。然后抽查几张图片和对应的标签是否匹配,用ImageFolder时如果文件夹名字写错,标签会静默出错。最后,如果batch_size小于 8,BatchNorm 的均值和方差估计会很不稳定,这时候要么增大 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_path="best_model.pth", device="cpu"): # 加载模型结构 model = SimpleCNN(num_classes=2) model.load_state_dict(torch.load(model_path, map_location=device)) 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, dim=1) 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", transform=val_transform) test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False) 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,推理时可能没有 GPU,map_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(), lr=base_lr) warmup_scheduler = LambdaLR(optimizer, lr_lambda=warmup_lambda) cosine_scheduler = CosineAnnealingLR(optimizer, T_max=total_epochs - warmup_epochs) for epoch in range(total_epochs): # 训练代码... if epoch < warmup_epochs: warmup_scheduler.step() else: cosine_scheduler.step()第二个技巧是测试时增强(TTA)。推理时对同一张图片做多次不同的变换(比如水平翻转、不同裁剪),把多次预测的概率取平均。这个技巧几乎不增加训练成本,但通常能带来 1~2 个百分点的精度提升。实现方式很简单:对每张图生成 5 个版本,分别推理后取平均。
第三个技巧是错误样本分析。把验证集中预测错误的图片单独拿出来看,你会发现模型翻车的往往是一些模糊、遮挡、姿态极端的样本。针对这些情况,可以在训练时加入 CutMix 或 MixUp 增强,让模型学会在部分信息缺失的情况下也能判断。CutMix 的实现稍微复杂一点,但 PyTorch 社区有很多现成的实现可以参考。
这三个技巧我一般会按顺序上:先加学习率调度,再加 TTA,最后根据错误分析决定要不要上 CutMix。不要一次性全加上,否则出了问题很难定位是哪个环节导致的。希望帮到你。
本文还有配套的精品资源,点击获取