简介一份面向深度学习初学者与技术文章写作者的 PyTorch 猫狗图像分类实战教程资源围绕“从需求到部署”的完整项目展开涵盖项目背景、数据准备与数据增强、轻量级 CNN 模型构建、训练评估及部署步骤能够帮助读者建立图像分类任务的完整认知。压缩包内容为 1 个 docx 文档大小仅 19KB文档中内置了可直接复制运行的 Python 代码片段并对随机裁剪、水平翻转、归一化、Dropout、Adam 优化等关键细节做了说明方便边学边练。目前已有 152 人学习下载过程中穿插了提升准确率的实用技巧结尾总结了常见陷阱与改进方向非常适合自学入门、课堂辅助教学或撰写技术博客时作为模板参考。跟随教程逐步操作即可掌握利用 PyTorch 构建轻量级 CNN 解决猫狗二分类问题的基本思路与实现方法。1. 猫狗分类这个经典项目为什么值得用 PyTorch 从零跑一遍如果你正在入门深度学习想找一个“跑通就有成就感、跑完又不至于被劝退”的项目猫狗图像分类几乎是最合适的选择。它不涉及复杂的目标检测或语义分割只需要判断一张图里是猫还是狗但背后却完整覆盖了图像读取、数据增强、模型定义、训练循环、验证逻辑和模型保存这一整套深度学习流水线。用 PyTorch 来做这件事因为它的动态图和“所见即所得”的调试体验对新手特别友好改一行代码立刻能看效果这也是它这几年在深度学习实战里越来越主流的原因。下文我把完整流程拆开讲包含可直接复制的代码、参数取值依据以及我反复踩过的一些坑。2. 环境与数据准备拿什么喂给模型以及 PyTorch 安装的取舍Deep Learning 项目第一步不是写模型而是把环境、数据和数据读取管道搞定。很多新手在这一步就卡住后面全乱。2.1 用 Anaconda 创建虚拟环境CPU 版还是 GPU 版我一般用 Anaconda 给每个项目单独建一个虚拟环境避免不同项目之间的包依赖打架。创建 PyTorch 环境常见做法是conda create -n catdog python3.8 -y conda activate catdog然后是安装 PyTorch。这里最大的分叉是 CPU 版还是 GPU 版。如果你的机器有 NVIDIA 显卡并且想真正体验深度学习模型训练的速度建议装 CUDA 版本没有独立显卡或者显卡是 A 卡就装 CPU 版先跑通流程猫狗分类这种小项目CPU 训练几十个 epoch 也能在十几分钟内完成不至于等不下去。GPU 版安装常见命令长这样pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118CPU 版则更简单pip install torch torchvision torchaudio装完以后务必验证一下可用性import torch print(torch.__version__) print(torch.cuda.is_available())如果torch.cuda.is_available()返回False不要急着怀疑人生。先检查 PyTorch 版本和 CUDA 版本是否匹配再看显卡驱动是否正常。这个“匹配问题”是 PyTorch 环境搭建里出现频率最高的坑后文专门说。另外注意 Python 版本和 PyTorch 版本有对应关系Python 3.8 目前兼容性最稳新一点的 PyTorch 2.x 也支持 3.10/3.11但避免直接用最新 Python否则容易遇到依赖库还没适配的情况。2.2 数据集从哪里来猫狗分类数据集与目录结构猫狗图像分类最经典的数据集是 Kaggle 上的 Dogs vs Cats。它包含 25000 张猫狗图片训练集和测试集分开。通常做法是把原始图片整理成下面这种目录结构data/ train/ cat/ cat.0.jpg cat.1.jpg ... dog/ dog.0.jpg ... val/ cat/ ... dog/ ...为什么要手动整理成这种按类别分文件夹的结构因为 PyTorch 的torchvision.datasets.ImageFolder直接就能读这种目录结构自动按子文件夹名生成类别标签省去自己写标签解析的麻烦。从原始数据集整理目录的时候要注意原始文件名里带有类别信息比如cat.0.jpg写脚本切分时别把文件名切错。import os import shutil import random source_dir raw/kaggle_cats_dogs train_dir data/train val_dir data/val split_ratio 0.8 for cls in [cat, dog]: os.makedirs(os.path.join(train_dir, cls), exist_okTrue) os.makedirs(os.path.join(val_dir, cls), exist_okTrue) files [f for f in os.listdir(os.path.join(source_dir, cls)) if f.endswith(.jpg)] random.shuffle(files) split_idx int(len(files) * split_ratio) for f in files[:split_idx]: shutil.copy(os.path.join(source_dir, cls, f), os.path.join(train_dir, cls, f)) for f in files[split_idx:]: shutil.copy(os.path.join(source_dir, cls, f), os.path.join(val_dir, cls, f))这段脚本按 8:2 把每类数据分成训练和验证集。注意一定要先random.shuffle否则原始文件的顺序可能按某种规则排列导致验证集分布失衡。另外我习惯复制而不是移动因为原始数据要保留备查万一后面想调整划分比例还有后悔药。2.3 自定义 Dataset 和 transforms把图片变成张量的标准模板数据进入模型之前要经过“读取图片 → 解码 → 缩放 → 转张量 → 归一化”这几步。PyTorch 中最省事的方案是使用torchvision.datasets.ImageFolder配合transforms。一个常用的训练变换组合长这样from torchvision import datasets, transforms train_transforms transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), 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_transforms transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_dataset datasets.ImageFolder(rootdata/train, transformtrain_transforms) val_dataset datasets.ImageFolder(rootdata/val, transformval_transforms)每个变换的作用我简单解释一下。Resize((224, 224))是把所有图片统一到相同尺寸因为卷积神经网络要求输入固定形状RandomHorizontalFlip和RandomRotation属于数据增强让模型看到更多样化的猫狗姿势对防止过拟合很有帮助ColorJitter调整亮度对比度模拟不同光线环境ToTensor()会把 PIL 图像从 HWC 转为 CHW 排列并将像素值从 0-255 缩放到 0.0-1.0Normalize则用 ImageNet 的均值和标准差做标准化让每个通道的数据分布接近标准正态分布。这里容易踩一个坑验证集和测试集不要做随机增强只做 Resize、转张量和归一化即可。如果验证集也随便翻转旋转那验证准确率就不是模型真实水平的稳定估计每次跑出来的指标会抖得很厉害。2.4 DataLoader 的 batch 与 shuffle 参数为什么重要数据集建好之后还要交给DataLoader才能批量喂给模型。常见写法from torch.utils.data import DataLoader train_loader DataLoader(train_dataset, batch_size64, shuffleTrue, num_workers2) val_loader DataLoader(val_dataset, batch_size64, shuffleFalse, num_workers2)batch_size64表示每次迭代取 64 张图片送入模型。这个值不是随便定的它受显存大小影响太大容易 OOM太小训练不稳定且耗时。在 8GB 显存的 GPU 上ResNet18 输入 224x224batch_size 64 基本是上限CPU 训练的话建议调到 16 或 32避免内存爆掉。shuffleTrue只在训练集使用作用是每个 epoch 重新打乱样本顺序避免模型学习到样本的排列顺序。验证集不需要 shuffle因为验证只看最终准确率顺序无关紧要。num_workers决定用几个子进程加载数据Windows 下建议设为 0否则容易报BrokenPipeErrorLinux 下可以开到 2 或 4。如果你的机器 CPU 核心不多num_workers0最保险就是加载速度慢一点。到这里数据和数据管道就准备好了。这个阶段最常见的现象是“代码运行时报FileNotFoundError”或“图片读取失败”究其原因多半是目录结构不对、图片本身损坏或者文件名里有特殊字符。建议在跑训练之前先单独打印几批数据看看data_iter iter(train_loader) images, labels next(data_iter) print(images.shape, labels.shape)能正常打印出torch.Size([64, 3, 224, 224])和torch.Size([64])再进入模型搭建环节。3. 模型搭建CNN 手写与迁移学习怎么选代码怎么落地数据准备好之后核心部分就是搭模型。猫狗分类本质上是一个二分类图像任务动手之前先想清楚是手写一个小型 CNN还是直接用预训练模型做迁移学习。这个选择直接决定训练时间和最终效果上限。3.1 手写一个 CNN结构设计与参数含义神经网络在图像分类中动辄几十上百层并不意味着新手起步也要堆那么深。手写一个能完成猫狗分类的小型 CNN三四个卷积块就够用。下面是我常用的一个结构import torch import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self): super(SimpleCNN, self).__init__() self.features nn.Sequential( nn.Conv2d(3, 16, kernel_size3, padding1), nn.BatchNorm2d(16), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), nn.Conv2d(16, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2) ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(64 * 28 * 28, 256), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(256, 2) ) def forward(self, x): return self.classifier(self.features(x))输入是 3x224x224 的图片。每个卷积块都遵循“卷积 → 批归一化 → ReLU → 最大池化”的组合。Conv2d(3, 16, kernel_size3, padding1)表示输入通道 3输出特征图 16 个3x3 卷积核padding 为 1 保持尺寸不变。BatchNorm2d的作用是让小批次内的特征分布保持稳定能显著加速收敛算是训练深度网络的标准配置。MaxPool2d(2)把特征图高宽各减半三次池化后224x224 变成 28x28最后一个卷积块输出 64 个通道所以全连接层输入维度是 64x28x28。这里有个关键点最后全连接层输出是 2对应猫和狗两个类别。损失函数用的是交叉熵模型输出的是未经过 softmax 的原始 logits所以后面计算概率时要手动加softmax这个后文会说到。3.2 用 torchvision 加载预训练模型迁移学习的两条路手写 CNN 虽然能跑但效果通常一般尤其是数据量只有一两万张时。一个更务实的选择是加载在 ImageNet 上预训练好的模型比如 ResNet18、ResNet34 或 MobileNetV3然后针对猫狗二分类做微调。这个思路就是迁移学习它可以让你站在大规模预训练模型的肩膀上而不是从零学起。常见代码import torchvision.models as models model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) num_features model.fc.in_features model.fc nn.Linear(num_features, 2)老版本的写法是pretrainedTrue但新版 PyTorch 推荐用weights参数因为旧的写法已经不再建议。替换model.fc是因为 ResNet 的最后一层全连接原本是 1000 类输出改成 2 类输出就适配了猫狗分类任务。迁移学习有两种常见路线第一种是“只训练新加的分类头”。也就是把主干网络全部冻结只让最后一层全连接层学习。这种方式训练速度极快数据量少也不容易过拟合但效果上限受限于预训练特征的可区分性。第二种是“微调全部或部分层”。也就是不冻结参数或者只冻结前面若干层让模型在猫狗数据上继续更新权重。这种方式能更好地适配猫狗图像的特殊分布一般准确率会更高但需要更长训练时间和更多数据也更容易过拟合。3.3 冻结与解冻到底改哪些层的参数如果你选择“只训练分类头”代码这样做for param in model.parameters(): param.requires_grad False for param in model.fc.parameters(): param.requires_grad True optimizer optim.Adam(model.fc.parameters(), lr1e-3)注意这里传给优化器的是model.fc.parameters()而不是model.parameters()否则会报错“参数没有梯度”或者白占内存。如果你选择微调全部层一般把学习率调低一点比如1e-4因为预训练参数已经很好学习率太大会把这些参数破坏掉。还有一个折中做法先冻结主干训练几轮分类头再解冻主干用较小学习率继续训练。我在实际项目里常常用这个两阶段策略既避免了前期训练不稳定又能拿到更高的最终精度。判断当前该用哪种策略主要看你的数据量数据量小于 5000 张建议只训分类头数据量在 2 万张以上可以考虑微调全部层。4. 训练全流程损失、优化器、batch size 和 epoch 怎么配合模型定义好接下来就是训练。训练这块也是深度学习中套路最多的环节很多参数看起来都有默认值但不同项目不同参数带来的效果差距很大。4.1 训练循环和验证循环的标准写法PyTorch 的训练循环核心就三件事前向计算、反向传播、参数更新。但如果不加上验证逻辑你完全不知道模型到底学得怎么样。下面是我常用的标准训练框架import torch import torch.nn as nn import torch.optim as optim device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-4) epochs 30 best_acc 0.0 for epoch in range(epochs): model.train() running_loss 0.0 for inputs, labels in train_loader: inputs, labels inputs.to(device), labels.to(device) optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * inputs.size(0) model.eval() correct 0 total 0 val_loss 0.0 with torch.no_grad(): for inputs, labels in val_loader: inputs, labels inputs.to(device), labels.to(device) outputs model(inputs) loss criterion(outputs, labels) val_loss loss.item() * inputs.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() epoch_loss running_loss / len(train_dataset) epoch_val_loss val_loss / len(val_dataset) epoch_acc correct / total print(fEpoch {epoch1}/{epochs}, Loss: {epoch_loss:.4f}, Val Loss: {epoch_val_loss:.4f}, Val Acc: {epoch_acc:.4f}) if epoch_acc best_acc: best_acc epoch_acc torch.save(model.state_dict(), best_model.pth)逐段解释一下。model.train()和model.eval()是切换模型模式的关键前者让 Dropout 和 BatchNorm 处于训练状态后者让它们使用固定的统计量验证时不调用这两行模型结果会非常不稳定。optimizer.zero_grad()每一轮都要清空梯度否则梯度会累积。loss.backward()算梯度optimizer.step()更新参数这是深度学习中 CNN 训练的标准节奏。with torch.no_grad()表示验证阶段不计算梯度既省内存又防止误更新参数。同时注意outputs, predicted torch.max(outputs, 1)是沿着类别维度取最大值索引也就是模型预测的类别。计算验证准确率时用(predicted labels).sum()统计正确数再除以总数。4.2 超参数表lr、batch size、epoch 的常见取值新手最容易困惑的就是这些超参数到底怎么设。我给出一份基于猫狗分类的常用参数表记在下面遇到具体问题时按这个范围调整超参数常见取值范围选型依据学习率 lr1e-3 到 1e-5微调模型用 1e-4 左右手写小 CNN 可以用 1e-3 起步batch size16 到 64取决于 GPU 显存显存小就减半epochs20 到 50观察验证集准确率不再上升就提前停止优化器Adam 或 SGDAdam 收敛快新手先用 Adam损失函数CrossEntropyLoss几乎不用换这就是分类任务的标准选择关于学习率经验是“一上来不要太大”。1e-3对 Adam 来说是比较激进的值如果发现 loss 震荡或者不下降立刻降一个数量级到1e-4。微调预训练模型我一般直接用1e-4很少超过这个值。学习率这个参数看起来像玄学其实是深度学习模型训练中影响最大的一个旋钮多试几个值记录训练曲线很快就能找到手感。关于 epoch并不是越大越好。模型可能在某个 epoch 达到最高验证准确率之后就过拟合了。所以训练时最好每轮都保存验证准确率最高的那个模型也就是代码里best_acc对应的best_model.pth这就是“后悔药”机制避免最后训练完发现模型已经过拟合。4.3 记录训练曲线如何判断过拟合与欠拟合如果你只是打印数字很难直观判断模型状态。我一般用matplotlib画出训练 loss 和验证 loss 的曲线每次训练结束扫一眼就能定位问题。做法是在训练循环里保存每轮的 loss 值最后统一绘图。import matplotlib.pyplot as plt train_losses [] val_losses [] # 在训练循环中 append 对应值 # train_losses.append(epoch_loss) # val_losses.append(epoch_val_loss) plt.plot(train_losses, labeltrain_loss) plt.plot(val_losses, labelval_loss) plt.xlabel(epoch) plt.ylabel(loss) plt.legend() plt.show()判断标准很简单。训练 loss 不断下降验证 loss 也开始下降说明模型在学习训练 loss 下降但验证 loss 上升说明过拟合两者都高并且几乎不降说明欠拟合可能是学习率太小、模型容量不够或者数据预处理出了问题。训练曲线是深度学习中最重要的可视化工具这个环节一定要养成习惯不然就是在撞大运。5. 实操避坑5 个让猫狗分类翻车的典型问题这部分内容都是我在实际跑猫狗分类项目时遇到过的具体问题按照“现象 → 原因 → 解决”的顺序写每条都很短但都是血泪经验。5.1 验证准确率一直在 50% 左右徘徊现象模型训练了好几轮loss 在下降但验证准确率始终在 50% 上下也就是瞎猜水平。原因最常见的是标签和图片没有正确对齐。ImageFolder按文件夹名排序如果训练集和验证集在整理目录时脚本有 bug猫的图片打到狗的标签上模型当然学不到任何东西。另一个常见原因是数据增强太猛比如RandomRotation角度超过 90 度本身猫狗图片已经快认不出来了模型学到的是旋转不变特征而不是类别特征。解决先别急着调参。打印一批(图片, 标签)对照检查确认标签是否正确然后暂时去掉随机旋转和 ColorJitter用最小变换验证模型能过拟合一个批次的数据。如果连训练集都过不了那问题出在模型或者数据管道上不在数据增强。5.2 迁移学习模型准确率反而低于手写 CNN现象明明加载了 ResNet18 预训练模型准确率却不如自己手写的简单模型。原因很可能是输入尺寸不匹配。ResNet 默认输入是 224x224如果你的数据变换里写的是Resize((128, 128))预训练权重虽然能跑但特征提取能力没有充分发挥出来。另外一个原因是学习率过大微调阶段用1e-3直接把预训练参数冲坏了。解决严格控制输入尺寸为 224x224微调时学习率降到1e-4或更低并且建议先冻结主干只训练分类头两个 epoch等 loss 稳定后再解冻全部层。记住迁移学习的效果不是模型越深就一定越好和输入处理方式关系很大。5.3 训练时报错“CUDA out of memory”现象程序跑到中途直接报RuntimeError: CUDA out of memory. Tried to allocate ...原因batch size 太大或者模型太大显存不够。也可能是因为训练和验证阶段同时把大量中间变量留在显存里没释放。解决最直接的方法是把 batch_size 从 64 降到 32 或 16同时把输入图片Resize到 160x160 而不是 224x224显存占用会明显减少。再一个技巧是验证阶段使用with torch.no_grad()这个已经写在前面的标准代码里了。还有一点如果用了 DataLoader 的num_workers过大多个子进程会复制模型副本显存占用会翻倍降低num_workers或设为 0 也能缓解。5.4 训练到后期验证 loss 反弹准确率下降现象前 20 个 epoch 准确率一路升高之后验证 loss 突然反弹准确率下降。原因这就是典型的过拟合模型开始记住训练集中的噪声失去了泛化能力。数据量不够、模型容量太大、训练时间太长三者叠加就会出现。解决把保存的best_model.pth拿出来用那是验证准确率最高点的参数。然后考虑增加数据增强强度裁剪、颜色抖动、翻转或者给全连接层加更大的 Dropout 比率比如从 0.5 调到 0.7。如果已经用了迁移学习可以重新冻结前面若干层减少可训练参数数量。不要试图靠 early stopping 硬撑根本办法是让模型更简单或者数据更丰富。5.5 PyTorch 环境装完但torch.cuda.is_available()为 False现象装的是 CUDA 版 PyTorch但运行代码发现torch.cuda.is_available()返回False。原因PyTorch 的 CUDA 版本和本机显卡驱动不匹配或者装了 CPU 版。典型的例子是显卡驱动支持 CUDA 12但 PyTorch 装的是 cu118某些情况下也能用但版本差太多就会失效。再有一种坑是用 pip 装的时候--index-url参数拼写错误实际装成了 CPU 版。解决先执行nvidia-smi看驱动支持的 CUDA 版本再到 PyTorch 官网主页选择完全匹配的安装命令不要从第三方博客复制命令。装完后用python -c import torch; print(torch.version.cuda)确认如果输出的是cpu毫无疑问就是装错了。注意PyTorch 环境搭建在 Windows 和 Linux 下的表现不完全一样Linux 下相对顺畅Windows 下建议优先用 WSL 环境能少遇到很多底层兼容兼容问题。6. 验证模型单张预测、混淆矩阵与模型导出训练结束后模型能不能在你的业务场景里真正干活还需要最后几步验证。6.1 单张图片预测的完整流程拿一张新的猫或狗图片走一遍和验证集相同的预处理然后送入模型from PIL import Image img Image.open(new_dog.jpg).convert(RGB) img_tensor val_transforms(img).unsqueeze(0).to(device) model.eval() with torch.no_grad(): output model(img_tensor) prob torch.softmax(output, dim1) pred torch.argmax(prob, dim1).item() class_names [cat, dog] print(f预测结果: {class_names[pred]}, 概率: {prob[0][pred].item():.4f})注意.unsqueeze(0)因为模型接收的输入是 4 维张量(batch, channel, height, width)单张图片只有 3 维需要补上一个批次维度。val_transforms必须和验证集用的完全一致尤其Normalize的均值和标准差不能改。输出概率前要加softmax因为训练时用的是交叉熵损失模型最后一层输出的是未归一化的 logits。6.2 混淆矩阵看错误模式准确率只能告诉你总体水平却看不出模型到底把猫认成狗更频繁还是把狗认成猫更频繁。画一个混淆矩阵问题一目了然。from sklearn.metrics import confusion_matrix import numpy as np all_labels [] all_preds [] model.eval() with torch.no_grad(): for inputs, labels in val_loader: inputs inputs.to(device) outputs model(inputs) _, preds torch.max(outputs, 1) all_labels.extend(labels.cpu().numpy()) all_preds.extend(preds.cpu().numpy()) cm confusion_matrix(all_labels, all_preds) print(cm)对角线数值大、非对角线小说明模型表现健康。如果猫的误判率明显高于狗可能因为训练集中猫的图片姿态更多样或者猫和某些背景纹理相似这时候针对误判样本做数据补充比盲目调模型结构更有效。6.3 模型保存与加载两种方式与坑最后把模型保存下来。常见有两种方式第一种保存state_dict只存参数字典加载时要有模型结构第二种保存完整模型结构和参数在一起但占用空间更大跨 PyTorch 版本加载容易出问题。# 保存 state_dict torch.save(model.state_dict(), catdog.pth) # 加载 state_dict model SimpleCNN() # 先手动构建结构 model.load_state_dict(torch.load(catdog.pth, map_locationdevice)) model.to(device)加载时有个坑如果你的模型原来是 GPU 上训练的加载到一台没有 GPU 的机器上需要在torch.load里加map_locationcpu否则会报错找不到 CUDA 设备。load_state_dict之前模型结构必须和保存时完全一致哪怕最后一层全连接的名字或者维数差一点都会报错所以最好把模型定义和训练写在同一个脚本里否则容易手忙脚乱。我自己在项目里习惯只存state_dict因为结构变更时能清楚知道自己加载的是什么。猫狗分类这条链路走到这算是真正闭环了。从环境搭建到数据整理从模型设计到训练调参最后还能对单张图片做出预测并保存模型这一套流程覆盖了图像分类任务的大部分核心细节。以后遇到其他二分类或者多分类图像任务你只需要改数据集目录、调整最后一层输出维度其他代码几乎可以原样复用。如果非要我总结一条经验遇到问题时不要先怀疑模型结构先检查数据和数据管道至少我掉进去的坑里七成以上出在数据环节。希望帮到你。本文还有配套的精品资源点击获取