简介这份资源面向深度学习入门与计算机视觉实践者提供一套基于卷积神经网络的猫狗图像分类完整源码与数据集帮助读者理解从数据准备到模型训练、测试的全流程。压缩包共10个文件约9KB以7个Python脚本为主辅以2个txt说明与1个md项目文档涵盖模型定义、训练、测试及数据划分等模块。其中包含自建AlexNet与基于PyTorch改写的VGG16两种可选模型并配有数据分类脚本可将按类别存放的图像自动划分为训练集与测试集同时提供单通道图像清除等数据清洗逻辑以及旧版数据加载示例便于学习图像读取与异常处理。代码中涉及argparse参数配置方便调整训练选项。目前已有46人学习适合希望快速跑通猫狗识别任务、对照源码理解CNN训练细节的读者参考。1. 猫狗图像分类从数据集到 CNN 模型一条能跑通的工程路径猫狗图像分类是深度学习入门最经典的实战场景之一也是很多人从 Python 基础语法跨入卷积神经网络的第一站。它解决的核心问题很具体给模型一张猫或狗的照片让它输出正确类别。听起来简单但真正动手时会遇到一堆工程问题——数据集怎么组织、图片尺寸不统一怎么办、模型为什么过拟合、训练多久才算收敛。这套源码加数据集的组合适合刚学完 Python 和深度学习基础、想找一个完整项目练手的开发者也适合需要快速搭建图像分类基线、再迁移到自己业务场景的工程师。下面按实际落地顺序把数据准备、模型搭建、训练调参和排错经验一次讲清楚。2. 数据集准备与目录结构别让路径问题浪费一晚上2.1 猫狗数据集的常见来源与清洗要点猫狗图像分类最常用的公开数据集是 Kaggle 上的 Dogs vs. Cats原始训练集有 25000 张图片猫狗各占一半测试集 12500 张没有标签。如果你拿到的数据集是压缩包解压后通常是一堆文件名类似cat.0.jpg、dog.1.jpg的图片混在一起。第一步不是急着写模型而是把数据整理成ImageFolder能直接读取的目录结构。常见做法是建两个顶层目录train和val每个目录下再分cats和dogs两个子目录。验证集从训练集里按 8:2 或 9:1 切分注意要按类别分层抽样避免某一类在验证集里过少导致评估指标失真。图片格式统一成 JPEG 或 PNG遇到损坏文件直接跳过并在日志里记录文件名不要让它中断整个训练流程。注意Windows 下路径分隔符和 Linux 不同写代码时统一用os.path.join或pathlib.Path避免硬编码反斜杠导致换环境就报错。2.2 用 Python 脚本完成训练集与验证集划分下面这段脚本做三件事读取原始图片目录、按类别分层切分、复制到目标结构。运行前把raw_dir改成你解压后的实际路径。import os import shutil import random from pathlib import Path from collections import defaultdict random.seed(42) raw_dir Path(data/raw) # 原始图片所在目录 train_dir Path(data/train) # 训练集输出目录 val_dir Path(data/val) # 验证集输出目录 val_ratio 0.2 # 验证集比例 # 按类别收集文件 buckets defaultdict(list) for img_path in raw_dir.glob(*.jpg): # 文件名形如 cat.123.jpg 或 dog.456.jpg label img_path.name.split(.)[0] if label in (cat, dog): buckets[label].append(img_path) for label, files in buckets.items(): random.shuffle(files) split int(len(files) * (1 - val_ratio)) train_files, val_files files[:split], files[split:] for target_dir, file_list in [(train_dir, train_files), (val_dir, val_files)]: out target_dir / label out.mkdir(parentsTrue, exist_okTrue) for f in file_list: shutil.copy2(f, out / f.name) print(划分完成, {k: len(v) for k, v in buckets.items()})逻辑说明defaultdict(list)按文件名前缀把猫狗分开random.shuffle打乱后按比例切分shutil.copy2保留原始文件元信息。参数方面val_ratio设 0.2 是常规选择数据量少于 5000 张时可以降到 0.1把更多样本留给训练。random.seed(42)保证每次运行划分结果一致方便复现实验。跑完后检查一下data/train/cats和data/train/dogs的文件数量是否接近如果差距超过 10%说明原始数据本身不均衡需要在训练时用WeightedRandomSampler或类别权重来补偿。3. 卷积神经网络模型搭建从 LeNet 到 ResNet 的选型逻辑3.1 为什么猫狗分类不需要从零设计网络卷积神经网络的核心优势是局部感受野和权值共享这两点让它天然适合图像任务。猫狗分类的难点不在于类别多而在于同一类内部的姿态、光照、背景差异极大。自己从零堆卷积层不是不行但很容易陷入“层数加了效果反而降”的玄学。更稳妥的做法是用预训练模型做迁移学习比如 ResNet18 或 VGG16在 ImageNet 上已经学到的边缘、纹理特征对猫狗分类同样有效。如果只是想理解卷积神经网络结构可以从一个简化版 CNN 开始两层卷积加两层全连接输入尺寸统一缩放到 128×128。这个规模在 CPU 上也能跑适合验证流程是否通畅。等流程跑通后再换 ResNet18把输入提到 224×224效果通常会有明显提升。3.2 用 PyTorch 定义一个可训练的 CNN 基线下面是一个四层卷积的基线模型结构清晰方便你逐层替换成预训练网络。import torch 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), # 128 - 64 nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 64 - 32 nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 32 - 16 nn.Conv2d(128, 256, kernel_size3, padding1), nn.BatchNorm2d(256), nn.ReLU(inplaceTrue), nn.AdaptiveAvgPool2d((1, 1)) # 输出 256x1x1 ) self.classifier nn.Sequential( nn.Flatten(), nn.Dropout(0.5), nn.Linear(256, num_classes) ) def forward(self, x): x self.features(x) return self.classifier(x)逻辑说明每个卷积块后面接BatchNorm2d和ReLUBatchNorm能加速收敛并起到一定正则作用。AdaptiveAvgPool2d((1,1))把任意空间尺寸压成 1×1这样输入图片尺寸变化时全连接层不用改。Dropout(0.5)放在分类头前面是防止过拟合的常规手段。参数方面卷积核统一用 3×3、padding1这是最通用的配置通道数从 32 翻到 256遵循“空间尺寸减半、通道数翻倍”的经验规律。3.3 数据增强与 DataLoader 配置猫狗分类最容易翻车的地方是过拟合训练集准确率冲到 99%验证集卡在 70% 不动。数据增强是性价比最高的缓解手段。常见组合是随机水平翻转、随机裁剪、颜色抖动验证集只做缩放和归一化。from torchvision import transforms, datasets from torch.utils.data import DataLoader train_tf 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]) ]) val_tf transforms.Compose([ transforms.Resize((128, 128)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_ds datasets.ImageFolder(data/train, transformtrain_tf) val_ds datasets.ImageFolder(data/val, transformval_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4)逻辑说明Normalize用的均值和标准差是 ImageNet 的统计值迁移学习时保持一致从零训练时也可以用这组值影响不大。batch_size32是显存和梯度稳定性的折中显存不够就降到 16 或 8。num_workers在 Windows 上设 0 或 2设太大有时会卡住这是血泪经验。RandomRotation(15)对猫狗这种姿态多变的类别有帮助但如果你做的是工业缺陷检测旋转增强可能反而不合理。4. 训练循环与参数调优学习率、优化器和早停怎么设4.1 训练循环的骨架与损失函数选择训练循环本身不复杂关键是每一步的日志要打清楚否则出了问题只能靠猜。下面是一个带验证和最佳模型保存的训练骨架。import torch.optim as optim from torch.optim.lr_scheduler import ReduceLROnPlateau device torch.device(cuda if torch.cuda.is_available() else cpu) model CatDogCNN().to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler ReduceLROnPlateau(optimizer, modemin, factor0.5, patience3) best_acc 0.0 for epoch in range(30): model.train() running_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() running_loss loss.item() * imgs.size(0) train_loss running_loss / len(train_ds) # 验证阶段 model.eval() correct, total 0, 0 val_loss 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) val_acc correct / total val_loss val_loss / len(val_ds) scheduler.step(val_loss) print(fEpoch {epoch1:02d} | train_loss{train_loss:.4f} | val_loss{val_loss:.4f} | val_acc{val_acc:.4f}) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_catdog.pth)逻辑说明CrossEntropyLoss内部包含 softmax所以模型输出不要加 softmax。Adam配合weight_decay1e-4是常见组合如果换成SGD需要把学习率调到 0.01 并加 momentum。ReduceLROnPlateau在验证损失连续 3 个 epoch 不下降时把学习率减半比手动调省心。保存最佳模型而不是最后一个 epoch 的模型是因为过拟合后验证准确率会回落。4.2 学习率、批大小和权重衰减的联动关系这三个参数不是独立的。学习率太大损失震荡不收敛太小训练慢到怀疑人生。批大小影响梯度估计的噪声批越大梯度越稳但泛化可能略差。权重衰减是正则项和数据增强、Dropout 一起控制过拟合程度。参数推荐起点调整方向常见现象学习率1e-3 (Adam)验证损失震荡则降 10 倍损失变 NaN 说明太大批大小32显存允许就加到 64太小导致 BN 统计不稳权重衰减1e-4过拟合严重加到 1e-3太大导致欠拟合Dropout0.5过拟合降 0.3欠拟合升 0.6只在全连接层用如果训练 10 个 epoch 后验证准确率还在 50% 附近先检查数据标签有没有错、图片有没有正常读取而不是急着调参。我一般会先拿 20 张图片过一遍模型确认输出维度是[batch, 2]且损失能下降再跑全量。4.3 迁移学习换 ResNet18 后怎么改代码把基线模型换成预训练 ResNet18只需要改模型定义和输入尺寸。torchvision.models.resnet18(pretrainedTrue)加载 ImageNet 权重把最后的全连接层替换成二分类输出。import torchvision.models as models model models.resnet18(pretrainedTrue) for param in model.parameters(): param.requires_grad False # 先冻结主干 model.fc nn.Linear(model.fc.in_features, 2) # 训练几个 epoch 后解冻最后两个 block 做微调 for name, param in model.named_parameters(): if layer3 in name or layer4 in name or fc in name: param.requires_grad True逻辑说明先冻结主干只训练分类头学习率可以设 1e-3等验证准确率稳定后再解冻layer3和layer4学习率降到 1e-4 做微调。输入尺寸要改成 224×224Normalize参数不变。这样在 25000 张图上通常 5 到 10 个 epoch 就能到 95% 以上的验证准确率比从零训练快得多。5. 避坑与排查猫狗分类训练中最容易翻车的 5 个点5.1 验证准确率远高于训练准确率现象每个 epoch 打印出来验证集准确率比训练集还高看起来像“模型太强了”。原因通常是训练时开了 Dropout 和强数据增强而验证时两者都关闭导致训练阶段评估偏难。解决在训练循环里单独跑一次model.eval()评估训练集或者接受这个现象只要验证准确率在涨就没问题。5.2 损失变成 NaN现象训练几个 batch 后 loss 显示nan后续所有输出都是 NaN。原因一般是学习率太大、数据里有损坏图片导致梯度爆炸或者Normalize用错。解决先把学习率降到 1e-4 试一轮用脚本遍历所有图片确认没有零字节文件检查Normalize的均值和标准差是否和输入通道匹配。5.3 显存不足报 CUDA out of memory现象跑几个 batch 后报显存不够。原因可能是批大小太大、图片分辨率太高或者验证阶段没有用torch.no_grad()导致计算图累积。解决把batch_size减半输入尺寸从 224 降到 128确认验证循环包在with torch.no_grad():里。如果还不行用torch.cuda.empty_cache()清理缓存。5.4 训练集和验证集准确率都卡在 50%现象模型输出始终偏向一个类别准确率等于多数类占比。原因通常是标签映射错了比如ImageFolder按字母顺序把cats映射成 0、dogs映射成 1但你在评估时把 0 当成了狗。解决打印train_ds.class_to_idx确认映射关系评估时用train_ds.classes反查类别名。5.5 换机器后 DataLoader 卡死现象在 Linux 上跑得好好的换到 Windows 或 Mac 后卡在第一个 batch 不动。原因是num_workers大于 0 时多进程启动方式不同。解决Windows 下把num_workers设为 0或者在if __name__ __main__:里包住训练代码。Mac 上如果用了 MPS 加速也要注意某些算子不支持导致回退到 CPU。6. 把模型用起来单张图片推理与批量预测的工程细节训练完保存的best_catdog.pth只是权重文件要真正用起来还需要一个推理脚本。推理和训练最大的区别是不需要计算梯度输入要按同样方式预处理输出要转成可读的类别名。下面这段代码处理单张图片也支持传入目录做批量预测。from PIL import Image import torch import torch.nn.functional as F from torchvision import transforms def predict(image_path, model, device): tf transforms.Compose([ transforms.Resize((128, 128)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) img Image.open(image_path).convert(RGB) tensor tf(img).unsqueeze(0).to(device) # 增加 batch 维度 model.eval() with torch.no_grad(): logits model(tensor) probs F.softmax(logits, dim1) conf, pred probs.max(dim1) label dog if pred.item() 1 else cat return label, conf.item() # 使用示例 model CatDogCNN().to(device) model.load_state_dict(torch.load(best_catdog.pth, map_locationdevice)) print(predict(test.jpg, model, device))逻辑说明unsqueeze(0)把单张图片变成[1, 3, 128, 128]因为模型 forward 期望有 batch 维度。F.softmax把 logits 转成概率max(dim1)同时拿到置信度和类别索引。map_locationdevice保证在 CPU 上也能加载 GPU 训练的权重。如果要做批量预测把多张图片堆成[N, 3, 128, 128]一次前向即可比循环单张快很多。一个容易忽略的细节是推理时的预处理必须和验证集完全一致。我见过有人训练时用了RandomHorizontalFlip推理时忘了去掉结果同一张图翻转后预测结果不同还以为是模型不稳定。另外置信度低于 0.7 的样本建议人工复核尤其是猫狗姿态特别奇怪或者背景干扰严重的情况。如果你打算把这个方案迁移到自己的业务比如识别特定品种或检测异常图像最省力的路径是保留数据加载和训练循环把ImageFolder的类别目录换成你的标签把ResNet18的最后一层输出维度改成你的类别数先用小学习率微调 3 个 epoch 看验证集表现。不要一上来就从头训练大模型那是在烧算力换教训。希望帮到你。本文还有配套的精品资源点击获取