
简介为希望掌握PyTorch图像分类流程的开发者提供一份完整实践项目以猫、狗、公鸡三分类为具体案例系统讲解从数据预处理、CNN网络搭建到训练验证的完整过程适合有一定深度学习基础、想通过实战系统上手PyTorch的初学者。压缩包内共1390个文件其中1362张jpg构成训练与验证图片数据集11个py脚本是核心训练与推理代码另含5个xml配置文件、3个txt说明文档、1个onnx模型文件等包体554.92MB目录结构清晰便于按步骤学习。已有1378人参与学习下载项目内容广受认可。通过这套实战项目可以动手实践数据读取与增强、Conv2d卷积层堆叠、CrossEntropyLoss损失函数与SGD/Adam优化器选择、DataLoader数据加载、模型保存与加载等关键操作并可借助TensorBoard或混淆矩阵分析分类效果从而快速掌握PyTorch在图像分类任务中的典型开发流程为后续更复杂的深度学习项目打下坚实基础。1. 用 PyTorch 做猫狗公鸡图片分类三分类比二分类更值得练手把猫狗分类换成猫狗公鸡三分类难度马上不一样。狗和公鸡的毛色、纹理与姿态有大量重叠网络稍微偷懒就会去记背景而不是动物本体。用 PyTorch 搭建一个猫狗公鸡图片分类网络核心不是套一个现成模型跑一遍而是把目录结构、数据增强、网络设计、训练循环和排错手段串成一条能复现的链路。这篇笔记适合刚学完 Python 与 PyTorch 基础、准备做第一个实战项目的人也适合已经跑过二分类但想给自己加一点难度的从业者。我们按“先跑通环境—准备数据—搭网络—训练—排坑—验证”的顺序走完每条命令和参数都能直接抄。2. 先跑通环境再谈模型PyTorch 安装与开发环境配置新手一开始就把精力花在挑网络结构上这是顺序问题。环境搭不稳后面每一个报错都会混在一起分不清是代码错了、包版本错了还是 GPU 没调用起来。我一般把环境配置控制在二十分钟内解决目标只有一个能跑通一个最简单的张量运算确认 torch 和 torchvision 都能正常 import。2.1 用 Anaconda 建独立环境避免依赖打架很多人第一次装 PyTorch 是在系统 Python 里直接 pip install torch装到后面发现图片分类代码一运行就报错。最常见的原因不是代码写错而是 torch 与 torchvision 的版本不匹配或者和系统里其他包冲突。PyTorch 的官方约束很明确torchvision 是配套 torch 单独发布的版本必须对应不能单独升级其中一个。推荐先用 Anaconda 建一个干净环境把这个项目的依赖和系统其他 Python 环境隔开。这个习惯在多个项目并行时能省下大量排错时间项目 A 升级了 torch不会影响项目 B 的依赖树。conda create -n catdog python3.10 -y conda activate catdogpython3.10 是目前兼容性比较稳的选择支持 torch 的同时也能满足大部分图像处理库的版本要求。如果你本机已经跑着 3.11 或 3.12 的现成项目不冲突时也可以用但建议按这个命令新起一个环境避免以后换显卡驱动或装新库时把 base 环境搞坏。如果你不想装 Anaconda用 Python 自带的 venv 也能做隔离只是后续切换环境和安装 CUDA 相关依赖时conda 处理起来更省心。为什么不直接 conda install torch常见原因是 conda 默认源在一些机器上很慢而且 conda 对 torch 这种大型二进制包的依赖解析比 pip 慢不少。pip 安装时 torch 与 torchvision 会校验彼此的版本约束装错会即时报错比跑到训练时才暴露问题好处理。2.2 CPU 版还是 GPU 版三个判断条件很多人上来就问“我该装 CPU 版还是 GPU 版”这个问题在猫狗公鸡三分类场景里没那么复杂。几十到几百张的小样本量CPU 版完全可以跑完整个训练流程只是慢一点。判断要不要花时间装 GPU 版只看三件事有没有 NVIDIA 显卡显卡驱动支持的 CUDA 版本能不能和 PyTorch 对上你后面是不是要反复训练调参。三条中有一条不确定就先装 CPU 版把逻辑跑通再换 GPU 版不要一上来就追 CUDA 配置。常见做法是 CPU 版直接 pip 安装pip install torch torchvision如果需要 GPU 版安装命令会指定一个带 CUDA 版本的下载源pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121cu121 对应 CUDA 12.1 的预编译版本PyTorch 官方安装页会给不同 CUDA 版本列出对应命令。新手不理解的坑是这里选哪个 CUDA 版本不是看 PyTorch 版本号而是看显卡驱动支持到什么级别。装完如果 import 正常但 GPU 不可用绝大多数情况是驱动版本高于或低于 PyTorch 打包的 CUDA 运行库先试官方推荐的默认版本不行再换低一档的 cu118。老显卡不确定时敲一下nvidia-smi看右上角显示的 CUDA 版本拿它减去 0.5 再向下取整基本就是能用的档位。2.3 安装完必须做的三个验证环境装完先别急着写网络用一段最简单的代码验证三件事PyTorch 版本能读到、CUDA 可用性返回值符合预期、torchvision 能正常导入。这个验证以后每次换机器都能用。import torch print(torch.__version__) print(torch.cuda.is_available()) from torchvision import transforms, datasets print(torchvision ok)如果 torch.cuda.is_available() 返回 False不一定是装错了。先看 pip list 里 torch 版本号是否带 cpu 后缀带就是 CPU 版不带再看驱动。很多人在这一步死磕 GPU结果发现自己的显卡其实不支持当前 CUDA 版本白折腾一小时。小样本三分类用 CPU 训练二十轮也就几分钟先把网络跑起来再回头优化算力成本性价比高得多。另外如果你在 WSL 里搭环境验证命令和普通 Linux/Windows 完全一样PyTorch 对 WSL 的支持已经很成熟。只有一个细节WSL 里的 conda 环境和 Windows 侧不共享别两边反复装。torch 和 torchvision 的版本对应关系可以用pip list | grep torch确认主版本号一致基本就没问题。装完这两个核心包后这个项目基本不需要再引入其他深度学习库。数据操作用 torchvision可视化用 matplotlib如果只是写脚本训练这两步就够了。如果想确认网络每层的输出尺寸也不用额外装包直接print(model)就能看到每一层的名字和参数形状新手阶段这个输出比任何工具都直观。3. 猫狗公鸡数据准备目录结构、图像预处理与数据增强数据准备是图片分类里最容易被低估的一步。很多新手把图片随便丢到一个文件夹里然后开始写模型最后训练时要么报“Found 0 images”要么模型在验证集上过拟合得一塌糊涂。数据这一步决定了后面所有环节的上限网络设计解决不了数据没整理好的问题。3.1 目录结构用 ImageFolder 按文件夹生成标签torchvision 里的datasets.ImageFolder就是为这类目录结构设计的。它把每个子目录当作一个类别根据图片文件自动生成标签不需要你手动维护一份标签表。对猫狗公鸡三分类来说这是最快、最不容易错的方案。data/ ├── train/ │ ├── cat/ # 猫图片 │ ├── dog/ # 狗图片 │ └── rooster/ # 公鸡图片 └── val/ ├── cat/ ├── dog/ └── rooster/train 和 val 必须分开而且 val 里不能混入 train 的图片否则验证结果会虚高后面说避坑时还会提到。ImageFolder 读取时会按字母顺序给子目录排序cat 对应索引 0、dog 对应 1、rooster 对应 2。这个顺序在推理阶段很重要保存模型时把 classes 输出记下来以后推理类别索引以它为准。from torchvision import datasets train_dataset datasets.ImageFolder(rootdata/train) print(train_dataset.classes) print(train_dataset.class_to_idx)如果发现 class_to_idx 的顺序和自己预期不一致不要改文件夹名去迎合因为排序规则是固定的改名字反而容易在换数据时翻车。直接用 classes 列表的索引解读预测结果即可。3.2 预处理管线Resize、ToTensor 与 Normalize 的顺序图片读进来是 PIL 对象取值范围 0 到 255形状是 HWC。PyTorch 网络要求输入是 CHW 顺序的浮点张量像素值最好落在 0 到 1 附近所以 transforms 的顺序有严格规定先做几何变换再 ToTensor最后 Normalize。from torchvision import transforms transform_train transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.5, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) transform_val transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])train 用了 RandomResizedCrop会在原图上随机裁剪一块区域再缩放成 224x224相当于同时完成了裁剪和缩放val 用 Resize(256) 再 CenterCrop(224)保证验证时每张图都用固定的中心区域结果可复现。mean 和 std 沿用 ImageNet 的统计值这是 torchvision 预训练模型的标准预处理自己从零训练时沿用也不会错。224 是 torchvision 预训练模型默认的输入尺寸也是显存和精度比较平衡的选择。如果你自己收集的图片尺寸差异很大Resize 时要注意直接用 Resize((224,224)) 会把长宽比压扁公鸡被拉宽后纹理形变会影响分类。训练阶段用 RandomResizedCrop 天然解决了比例问题验证阶段用先 Resize 短边再 CenterCrop 的固定流程比直接强压更稳。3.3 数据增强小数据集的三件套与边界猫狗公鸡三分类如果每个类别只有几十张图不加增强基本必过拟合。增强的本质是给模型制造合法扰动公鸡换个角度、光线变一点、颜色偏移一点分类结果不能变。但增强不是越多越好旋转超过 30 度连人眼都难分辨网络学到的就变成了“猜”。transform_train transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.5, 1.0)), 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]), ])RandomRotation(15) 的 15 表示角度范围正负 15 度。ColorJitter 里 brightness、contrast 各 0.2表示在 0.8 到 1.2 倍之间随机调整。对公鸡这类纹理敏感的类别旋转和颜色扰动比翻转更重要因为公鸡的鸡冠和尾羽在不同角度下变化很大网络容易把这些外观细节当成固定特征记死。这套增强对几十张的小数据集足够不用再加更复杂的 AutoAugment。如果公鸡类别实在只有三四十张图我会在 Normalize 之后再加一层 RandomErasing。它会在输入图上随机抹掉一块矩形区域让网络不要过度依赖某一个局部特征比如鸡冠。p 参数控制在 0.3 以内scale 设为 (0.02, 0.1)意思是抹除面积占原图 2% 到 10%。这块是血泪经验小数据集上不加遮挡类增强验证集只要带一点遮挡准确率就崩。3.4 DataLoaderbatch_size、num_workers 与样本不均衡数据准备好之后用 DataLoader 包起来训练循环每次从里面取一批图片。参数不多但每一个都直接影响训练速度和显存占用。from torch.utils.data import DataLoader train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue)batch_size32 表示每批 32 张图算一次梯度。显存有限时第一个调整的就是它降到 16 或 8训练变慢但不会爆显存。shuffleTrue 只在训练集开验证集保持固定顺序否则每个 epoch 的验证样本顺序一直在变不方便对比。num_workers 是读取图片的子进程数默认 0 表示用主进程读图数据稍微多点训练就卡在 IO 上设成 CPU 核数减一比较合理Windows 上建议不要超过 4偶发多进程兼容问题能少一点。pin_memoryTrue 在 GPU 训练时能把数据放到锁页内存减少 CPU 到 GPU 的拷贝开销CPU 训练也不会有副作用。还有一个容易被忽略的问题如果猫图片有 500 张公鸡只有 50 张ImageFolder 会按顺序喂数据模型会严重偏向猫。常见做法是给 DataLoader 加 WeightedRandomSampler按类别数量倒数的权重采样让小类别每个 epoch 也能被抽到足够多次。这个方向知道即可先把均衡数据跑通再回头看是否需要加权。4. 搭建图片分类网络卷积层、分类头与训练闭环网络结构是这类项目里最容易被神话的部分。实际上猫狗公鸡三分类任务用三层卷积已经能到 90% 以上准确率前提是数据流程正确、训练参数合理。先学会把网络每一层的输入输出算清楚再谈换 ResNet 是更稳的路线。4.1 手写一个三层卷积网络从 Conv2d 到 MaxPool2d输入是 3 通道的 224x224 图像输出是 3 个类别的得分向量。中间的处理逻辑是卷积层在局部窗口里提取纹理特征ReLU 做非线性变换池化层把特征图缩小一半最后用全连接层把特征映射成类别得分。这个结构的每一层尺寸变化是可以手算的写代码前先在纸上过一遍。import torch import torch.nn as nn class CatDogRoosterNet(nn.Module): def __init__(self, num_classes3): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 16, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(16, 32, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(64 * 28 * 28, 256), nn.ReLU(inplaceTrue), nn.Linear(256, num_classes), ) def forward(self, x): x self.features(x) x self.classifier(x) return x尺寸变化是理解这段代码的关键。224x224 的图像经过第一层卷积padding1 保证输出仍是 224x224然后 MaxPool2d(2) 后变成 112x112。第二层卷积后池化成 56x56第三层后是 28x28。通道数从 3 变成 16、32、64所以 Flatten 后传给全连接层的特征维度是 64 乘 28 乘 28也就是 50176。如果改了输入尺寸或加了池化层这个数字必须跟着改这是新手最常见的报错来源之一。ReLU 加 inplaceTrue 是省内存的写法意思是在原张量上直接修改不新建输出张量。BatchNorm 的位置有讲究常见的是 Conv 之后、ReLU 之前放一层 BatchNorm2d。但我们的数据已经做了归一化小网络里不写 BatchNorm 也能稳定训练还能少踩一个 eval 模式的坑所以我在这份代码里先不写 BatchNorm等你迁移到更深网络时再补。4.2 分类头设计为什么输出三个神经元最后的全连接层输出 3 个数值对应猫、狗、公鸡三个类别的得分。训练时用 CrossEntropyLoss它会把这些得分logits做 softmax 变成概率再和真实标签做交叉熵计算。这里有一个容易翻车的点CrossEntropyLoss 内部已经包含 LogSoftmax所以不要在最后一层手动加 Softmax 或 Sigmoid否则梯度路径会出问题训练半天 loss 不降。self.classifier nn.Sequential( nn.Flatten(), nn.Linear(64 * 28 * 28, 256), nn.ReLU(inplaceTrue), nn.Dropout(0.2), nn.Linear(256, 3), )Dropout(0.2) 会在训练时随机让 20% 的神经元输出变成 0强迫网络不依赖单个神经元。它对小数据集防过拟合很有效但带来的副产品是验证时必须调用model.eval()关闭 Dropout否则推理结果是随机的这一点稍后讲。如果只是做三分类中间隐藏层 256 已经够用加到 512 不会带来明显收益反而更慢。4.3 迁移学习用预训练模型替换分类头自己从零搭网络最大的好处是理解每一层在干什么但真实项目的生产力方案是迁移学习。torchvision 里带预训练权重在 ImageNet 上学过的纹理特征可以直接复用你只需要把最后一层换成输出为 3 的全连接层。import torchvision.models as models backbone models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT) backbone.fc nn.Linear(backbone.fc.in_features, 3)关键是以backbone.fc.in_features为准去替换不要写死 512不同模型的分类头输入维度不一样。预训练权重要求输入尺寸和归一化方式与 ImageNet 一致所以前面 transform 里的 Resize 224、mean/std 那套必须保持原样。如果数据集很小可以先冻结 backbone 的卷积层只训练分类头代码上就是过滤掉不需要更新参数的层for param in backbone.parameters(): param.requires_grad False for param in backbone.fc.parameters(): param.requires_grad True冻结之后再训练loss 下降会比全网络微调慢但不容易过拟合。当你试完手写网络、理解了卷积和全连接的配合再用迁移学习去对比一次精度提升比直接上 ResNet 的收获大得多。4.4 训练循环把 loss、梯度更新和学习率调度串起来网络写好后训练循环是固定套路但代码顺序不能错。完整看一下一个 epoch 的训练代码epochs 10 criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size3, gamma0.5) for epoch in range(epochs): model.train() total_loss 0 for images, labels in train_loader: optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() avg_loss total_loss / len(train_loader) scheduler.step() print(fepoch {epoch 1:02d}, train loss {avg_loss:.4f}, flr {scheduler.get_last_lr()[0]:.2e})optimizer.zero_grad() 必须写在 loss.backward() 之前作用是清空上一次迭代留下的梯度。如果漏了梯度会在每次迭代里累加模型参数更新方向越来越偏。loss.backward() 计算梯度后optimizer.step() 更新参数。model.train() 和 model.eval() 的切换也要注意每轮训练开始切 train验证前切 eval。学习率调度选 StepLR 是常见的保守做法每 3 个 epoch 学习率减半。Adam 的初始学习率 1e-3 对这个小网络是安全起点。如果你想用 SGD一般从 0.01 起步配 momentum0.9。判断学习率是否合理直接观察第一个 epoch 的 loss 变化如果第一个 epoch 结束 loss 从 1.1 降到 0.7 左右说明学得动如果纹丝不动大概率学习率太小或数据链路有问题不是模型问题。训练时还要同时关注验证集。每轮结束后跑一次验证记录 val loss 和准确率才能判断模型是正在拟合还是已经过拟合。验证代码用 model.eval() 加 torch.no_grad()是固定搭配少一个结果都不可信。5. 训练踩坑与常见问题排查5 个让模型翻车的问题训练跑起来只是开始真正的工程量在排错。下面这几类问题我在带新手做图片分类时反复见到每条都按现象、原因、解决来写你可以直接把日志和情况对号入座。这些问题单独看都不难但组合出现时特别费时间所以把边界条件也写清楚什么情况算正常什么情况必须停。5.1 Loss 停在 1.10 附近不动现象训练 loss 从初始值缓慢下降后停在 1.09 到 1.12 之间验证集准确率一直在 33% 左右。1.099 是三分类随机猜测的理论交叉熵值等于 -ln(1/3)。看到这个数字说明模型没在学东西而不是“学得慢”。原因最常见是学习率实在太小梯度更新幅度不足以改变权重其次是数据链路出错比如所有图片都被放进了同一个类别文件夹标签和图像对应不上还有一种常见情况是 Normalize 的 mean 和 std 填反了输入分布不在模型预期范围训练直接失效。解决先打印一个 batch 的 images.mean() 和 images.std()确认数值范围正常再把学习率从 1e-3 上调到 1e-2Adam或从 0.01 起调SGD各跑 5 个 epoch 对比最后用torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)限制梯度范数防止个别样本把梯度拉爆。5.2 验证准确率比训练还高现象训练集准确率在区间内波动验证集准确率反而稳稳高出十几个点看起来很反常。原因多数情况是验证循环里忘了加 model.eval()。网络里有 Dropout 或 BatchNorm 时训练模式会用当前 batch 的统计量做归一化验证时不切 evalBatchNorm 还在用训练状态计算输出并不是真正的模型表现。另一个原因确认一下目录val 集里混进了 train 的图片这是数据泄露。解决验证循环固定写成下面这个模式两个字都不能少model.eval() with torch.no_grad(): # 跑完整验证集只统计数据不更新梯度 correct 0 total 0 for images, labels in val_loader: outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item()每轮验证结束后记得把模型切回model.train()否则下一轮训练的 Dropout 不生效loss 曲线会突然出现毛刺。5.3 训练到中途报 CUDA out of memory现象第一个 epoch 顺利跑完第二个 epoch 或验证阶段报RuntimeError: CUDA out of memory。很多人以为是显存被占满实际是验证阶段把整个 val 集一次性全塞进去或者优化器的梯度、动量等辅助内存逐步累积。原因batch_size 设置过大224x224 的激活值占用高验证集没有分 batch 推理多个进程共享同一块显存但没有释放。解决优先把 batch_size 从 32 降到 16 或 8这一步通常立竿见影再把输入尺寸统一缩到 160对三分类精度影响很小epoch 结束时调用torch.cuda.empty_cache()主动释放缓存。如果还紧张把模型通道数从 64 降到 32这类小任务完全够用。5.4 公鸡总被认成狗现象整体准确率到 80% 以上但混淆矩阵里公鸡类只有 60%大部分错判成狗。整体 acc 掩盖了单类问题只看整体指标发现不了。原因公鸡的鸡冠、羽毛纹理在部分姿态下和狗的卷毛非常接近更常见的是公鸡图片来源单一全都带草地或栅栏背景模型学到的是“浅绿背景”而不是公鸡本身。测试场景一换准确率立刻下降。解决给 rooster 类增加多背景、多角度的图片这是最本质的解法增强里开启 RandomErasing 和 ColorJitter打掉颜色依赖如果训练图背景干扰明显先用检测框把动物主体裁出来再训练。想打印混淆矩阵用 sklearn.metrics.confusion_matrix把验证集的预测值和真实值传进去即可肉眼看一下错在哪一类比任何调参都有方向。调网络结构解决不了数据分布问题因为模型记住的是背景里稳定的颜色块不是纹理。5.5 ImageFolder 报 “Found 0 images”现象DataLoader 初始化直接抛RuntimeError: Found 0 images in subfolders of ...路径检查了好几遍看不出问题。原因路径不存在、子目录层级比预期多一层或者目录里混入了 .DS_Store、Thumbs.db 这类系统隐藏文件被 ImageFolder 当成了类别文件夹。分类网络代码本身没问题是数据目录不干净。解决先用os.listdir(data/train)打印顶层目录确认只有 cat、dog、rooster 三个文件夹进到每个类别目录里用ls | head -5看图片文件列表隐藏文件全部清理掉。Windows 上路径尽量别用中文和空格PyTorch 对 unicode 路径的处理偶尔会有兼容问题。Linux 上注意扩展名大小写敏感.JPG 和 .jpg 是不同文件ImageFolder 对 PIL 能打开的后缀都能识别但目录名的大小写必须和代码一致。6. 模型保存与推理验证把训练好的网络真正用起来训练完这一步几乎所有人都会做保存模型。但保存方式有讲究。推荐只保存 state_dict它只含权重字典体积小且换环境不依赖模型类定义文件。torch.save(model.state_dict(), cat_dog_rooster.pth)加载时先构建网络再填权重注意网络结构必须和训练时完全一致。模型文件拷到没有 GPU 的机器上时加 map_location 参数model CatDogRoosterNet(num_classes3) model.load_state_dict(torch.load(cat_dog_rooster.pth, map_locationcpu)) model.eval()接着写一个推理函数输入图片路径输出各类别概率。class_names 来自训练时的train_dataset.classes。这里有一个我一直会加的技巧打印 top-2 的概率而不是只打印最大概率。当第一和第二名的概率接近比如 0.45 对 0.42 时这张图就在类别边界上直接归为第一名会误导业务方不如返回“不确定”。from PIL import Image import torch class_names [cat, dog, rooster] def predict(img_path): img Image.open(img_path).convert(RGB) x transform_val(img).unsqueeze(0) with torch.no_grad(): probs torch.softmax(model(x), dim1)[0] top2 probs.argsort(descendingTrue)[:2] return [(class_names[i], probs[i].item()) for i in top2]最后用一张训练时没见过的真实照片验证而不是测试集里已经看过的图片。手机随手拍一只公鸡缩放到 224x224看输出概率分布是否合理。这一步能暴露训练数据和真实分布之间的差距比如背景干扰、光线偏色、拍摄角度都是训练集里容易缺的维度。我做图片分类项目吃过最大的亏是一上来就换网络结构。后来发现先把数据流程验收完九成翻车问题都不在模型结构上。这个猫狗公鸡三分类项目结构固定、流程完整很适合作为你的第一个 PyTorch 实战练习。顺着代码跑通一遍再用真实照片验证一下你会把“模型能训练”和“模型能用”这两件事分得清清楚楚。希望帮到你。本文还有配套的精品资源点击获取