
简介这份资源是面向深度学习初学者与希望深入理解 PyTorch 的开发者打造的猫狗图像分类实战教程以 docx 文档形式呈现聚焦二分类任务的全流程落地。内容从项目背景与技术选型切入依次讲解数据准备与增强策略、轻量级 CNN 模型构建、训练与评估方法并延伸至部署步骤各阶段均配有可直接复制运行的 Python 代码片段同时融入提升性能与准确率的实用技巧最后总结常见陷阱与改进方向。压缩包内共 1 个 docx 文件约 19KB结构紧凑便于快速通读与对照实践。该教程既适合个人自学时作为图像分类的入门参考也可供教师辅助课堂教学或作为撰写技术博客的内容模板。目前已有 152 人学习数据源地址等信息一并给出方便读者直接上手复现完整项目。1. 猫狗分类看着简单为什么你的第一个模型可能连 60% 都跑不到很多人第一次接触深度学习图像分类都是从猫狗数据集开始的。打开 PyTorch 教程照着写几行DataLoader、搭一个卷积网络、跑几个 epoch看起来一切顺理成章。但真到自己动手把几千张猫狗图片丢进去训练结果往往很尴尬训练集准确率冲到 90% 以上验证集却在 55% 到 60% 之间反复横跳甚至不如抛硬币。这不是玄学而是典型的过拟合加上数据管道没搭对。这个实战项目的核心就是用 PyTorch 把猫狗图像分类从「能跑」做到「能看」。它适合已经装好 PyTorch 环境、知道张量是什么、但还没完整走过一遍图像分类流程的人。你需要的不只是模型定义而是从数据划分、增强、训练循环、验证策略到推理部署的一整条链路。下面我会按实际落地顺序把每个环节的参数和坑讲清楚让你能照着复现出一个验证集准确率稳定在 90% 以上的猫狗分类器。2. 数据管道决定上限猫狗图片怎么读、怎么分、怎么增强2.1 先搞清楚你的数据长什么样猫狗数据集常见有两种来源一种是 Kaggle 的dogs-vs-cats训练集 25000 张猫狗各半文件名带标签另一种是自己爬的或者从其他渠道凑的目录结构混乱图片尺寸不一甚至混入非猫非狗的噪声图。不管哪种第一步都是统一成ImageFolder能识别的结构data/ ├── train/ │ ├── cat/ │ │ ├── cat.0.jpg │ │ └── ... │ └── dog/ │ ├── dog.0.jpg │ └── ... └── val/ ├── cat/ └── dog/如果你拿到的是 Kaggle 原始训练集所有图片平铺在一个目录里需要先按文件名拆分。下面这个脚本做三件事按 8:2 划分训练集和验证集、把图片复制到对应子目录、顺便过滤掉损坏文件。import os import shutil import random from PIL import Image random.seed(42) src_dir dogs-vs-cats/train dst_dir data split_ratio 0.8 for phase in [train, val]: for cls in [cat, dog]: os.makedirs(os.path.join(dst_dir, phase, cls), exist_okTrue) files [f for f in os.listdir(src_dir) if f.endswith(.jpg)] random.shuffle(files) for fname in files: label cat if fname.startswith(cat) else dog src_path os.path.join(src_dir, fname) try: img Image.open(src_path) img.verify() # 验证文件完整性 except Exception: print(f跳过损坏文件: {fname}) continue phase train if random.random() split_ratio else val dst_path os.path.join(dst_dir, phase, label, fname) shutil.copy(src_path, dst_path)逻辑说明random.seed(42)保证每次划分结果一致方便复现。img.verify()会检查图片是否损坏损坏文件直接跳过否则训练时DataLoader会直接报错中断。split_ratio设为 0.8 是常见起点如果数据量少于 5000 张建议改成 0.7 并配合交叉验证。参数说明src_dir指向原始图片目录dst_dir是输出根目录。注意verify()之后如果还要用img对象需要重新open因为verify()会改变文件指针位置。2.2 训练集增强和验证集处理的区别数据增强是防止过拟合最直接的手段但很多人翻车在「验证集也做了随机增强」。验证集的目的是评估模型真实能力必须用确定性的预处理只做 resize 和归一化。训练集才用随机翻转、随机裁剪、颜色抖动这些操作。from torchvision import transforms train_transform transforms.Compose([ transforms.Resize(256), transforms.RandomResizedCrop(224, scale(0.8, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform 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]) ])逻辑说明RandomResizedCrop(224, scale(0.8, 1.0))表示随机裁剪原图 80% 到 100% 的区域再缩放到 224这样模型能看到不同尺度的猫狗。RandomHorizontalFlip对猫狗分类是安全的因为左右翻转不改变类别。ColorJitter模拟不同光照条件。归一化的均值和标准差是 ImageNet 的统计值如果你从零训练可以用自己数据集的统计值但用预训练模型就必须保持一致。参数说明scale(0.8, 1.0)不要设得太激进比如(0.5, 1.0)会把猫耳朵或狗尾巴裁掉导致标签噪声。p0.5是翻转概率猫狗分类不需要设更高。验证集的CenterCrop(224)和训练集的RandomResizedCrop(224)输出尺寸一致保证模型输入维度不变。2.3 DataLoader 的 batch_size 和 num_workers 怎么定DataLoader有两个参数直接影响训练效率和稳定性batch_size和num_workers。batch_size太小会导致梯度噪声大、训练震荡太大则显存吃紧而且可能泛化变差。num_workers设不好会出现「GPU 等数据」的情况利用率上不去。from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder train_dataset ImageFolder(data/train, transformtrain_transform) val_dataset ImageFolder(data/val, transformval_transform) train_loader DataLoader( train_dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue, drop_lastTrue ) val_loader DataLoader( val_dataset, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue )逻辑说明shuffleTrue只在训练集开启验证集必须关闭否则评估结果没有可比性。pin_memoryTrue在 GPU 训练时能加速 CPU 到 GPU 的数据传输。drop_lastTrue丢弃最后一个不完整的 batch避免 BatchNorm 在 batch size 为 1 时出问题。参数说明batch_size32是 8GB 显存下的安全值如果显存更大可以试 64 或 128。num_workers4适合 4 核以上 CPUWindows 下如果报错就改成 0。验证集的batch_size可以和训练集不同通常可以设大一点比如 64因为不需要反向传播。3. 模型选型从零搭 CNN 还是用预训练模型3.1 自己搭一个四层 CNN 能跑到多少如果你坚持从零训练一个经典的四层卷积网络在猫狗数据集上大概能到 75% 到 82% 的验证集准确率前提是数据增强到位、训练足够久。下面是一个可用的基线结构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), nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(128, 256, kernel_size3, padding1), nn.BatchNorm2d(256), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d(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和ReLUBatchNorm能加速收敛并允许更大的学习率。AdaptiveAvgPool2d(1)把任意空间尺寸压成 1x1避免全连接层参数爆炸。Dropout(0.5)在分类头之前做正则化。参数说明卷积核统一用 3x3padding1保持空间尺寸不变池化层负责降采样。通道数从 32 翻倍到 256这是常见设计。如果显存不够可以把通道数减半。3.2 为什么我建议你用预训练 ResNet18 起步从零训练猫狗分类你需要至少 50 个 epoch 才能看到像样的结果而且对学习率、权重衰减很敏感。用 ImageNet 预训练的 ResNet18只需要微调 5 到 10 个 epoch验证集准确率就能到 95% 以上。这不是偷懒而是工程上更合理的选择。import torchvision.models as models import torch.nn as nn model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) num_features model.fc.in_features model.fc nn.Linear(num_features, 2) # 替换分类头 # 冻结前面的层只训练分类头 for param in model.parameters(): param.requires_grad False for param in model.fc.parameters(): param.requires_grad True逻辑说明weightsResNet18_Weights.IMAGENET1K_V1加载 ImageNet 预训练权重。替换model.fc为二分类输出。冻结所有层再解冻分类头这是「特征提取」模式适合数据量少于 10000 张的情况。如果数据量更大可以解冻最后几个卷积块做微调。参数说明num_features对 ResNet18 是 512。如果你用 ResNet50这个值是 2048。冻结策略不是固定的可以先用特征提取模式跑几个 epoch再解冻layer4做微调。3.3 微调时学习率怎么设微调阶段的学习率要比从零训练小一个数量级。如果你用 SGD从零训练常用 0.01 到 0.1微调就用 0.001 到 0.01。如果用 Adam从零训练 1e-3微调用 1e-4 到 1e-3。import torch.optim as optim # 特征提取阶段 optimizer optim.Adam(model.fc.parameters(), lr1e-3) # 微调阶段解冻 layer4 后 optimizer optim.Adam([ {params: model.layer4.parameters(), lr: 1e-4}, {params: model.fc.parameters(), lr: 1e-3} ])逻辑说明分组设置学习率卷积层用更小的学习率避免破坏预训练特征分类头用正常学习率。model.layer4是 ResNet18 最后一个卷积块解冻它能让模型适应猫狗数据的特定纹理。参数说明lr1e-4是微调卷积层的保守值如果训练损失下降太慢可以调到 5e-4。分类头的lr1e-3是 Adam 的常用起点。4. 训练循环里最容易翻车的四个地方4.1 设备切换和混合精度训练循环第一件事是把模型和数据放到同一个设备上。如果你用 GPU还要考虑混合精度训练来省显存、加速。import torch from torch.cuda.amp import autocast, GradScaler device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) scaler GradScaler() for epoch in range(num_epochs): model.train() for images, labels in train_loader: images images.to(device, non_blockingTrue) labels labels.to(device, non_blockingTrue) optimizer.zero_grad() with autocast(): outputs model(images) loss criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()逻辑说明autocast()自动把部分运算转成 float16GradScaler防止梯度下溢。non_blockingTrue配合pin_memoryTrue实现异步传输。scaler.update()调整缩放因子。参数说明混合精度在 RTX 系列和 A100 上效果明显老显卡可能不支持。如果报错就关掉autocast和GradScaler用纯 float32 训练。4.2 验证集评估必须切 eval 模式这是血泪经验忘记写model.eval()会导致 Dropout 和 BatchNorm 在验证时仍然生效验证准确率会莫名其妙低 5 到 10 个百分点。def evaluate(model, loader, device): model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in loader: images images.to(device) labels labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() return correct / total逻辑说明model.eval()关闭 Dropout 并把 BatchNorm 切换到使用运行统计量。torch.no_grad()关闭梯度计算省显存。torch.max(outputs, 1)取最大 logit 对应的类别。参数说明验证集不需要optimizer.zero_grad()因为不更新参数。如果验证集很大可以分批累加正确数最后再算比例。4.3 学习率调度器的选择固定学习率在后期会导致损失震荡加一个调度器能让模型收敛得更稳。常见的有StepLR、CosineAnnealingLR和ReduceLROnPlateau。from torch.optim.lr_scheduler import CosineAnnealingLR scheduler CosineAnnealingLR(optimizer, T_maxnum_epochs, eta_min1e-6) for epoch in range(num_epochs): train_one_epoch() val_acc evaluate() scheduler.step()逻辑说明CosineAnnealingLR让学习率按余弦曲线从初始值降到eta_min适合总 epoch 数确定的情况。ReduceLROnPlateau则根据验证指标动态调整适合不确定训练多久的场景。参数说明T_max通常设为总 epoch 数。eta_min1e-6是学习率下限不要设 0否则后期完全不更新。4.4 保存最佳模型而不是最后一个训练到最后最后一个 epoch 的模型不一定是最好的。用验证准确率做标准保存最佳模型。best_acc 0.0 for epoch in range(num_epochs): train_one_epoch() val_acc evaluate(model, val_loader, device) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth) print(fEpoch {epoch}: 保存最佳模型验证准确率 {val_acc:.4f})逻辑说明model.state_dict()只保存参数不保存模型结构。加载时需要先实例化模型再load_state_dict。best_acc初始化为 0第一个 epoch 只要准确率大于 0 就会保存。参数说明如果验证准确率波动大可以加一个patience参数连续几个 epoch 不提升就停止训练这就是早停。5. 避坑与排查猫狗分类实战中的五个常见问题5.1 训练损失下降但验证准确率不动现象训练集 loss 从 0.6 降到 0.1验证集准确率卡在 60% 左右。原因典型过拟合。模型记住了训练集的猫狗纹理但没有学到泛化特征。也可能是数据增强太弱或者训练集和验证集分布不一致。解决加强数据增强加RandomRotation、RandomAffine。增加Dropout或weight_decay。如果用了预训练模型检查是否错误地冻结了所有层导致只有分类头在学。5.2 验证准确率比训练准确率高很多现象训练准确率 70%验证准确率 85%。原因训练时 Dropout 和 BatchNorm 在起作用验证时关闭了所以验证表现更好。如果差距太大说明训练集增强过猛或者训练集太小。解决检查训练集的transform是否过于激进比如RandomResizedCrop的scale下限太低。适当减小增强强度或者增加训练数据量。5.3 CUDA out of memory现象训练几个 batch 后报RuntimeError: CUDA out of memory。原因batch_size太大或者没有用torch.no_grad()导致验证时也存梯度或者中间变量没有释放。解决减小batch_size验证时加torch.no_grad()用del删除不用的张量。如果还不行用混合精度训练。最后手段是减小模型输入尺寸比如从 224 降到 192。5.4 验证集准确率波动超过 5%现象每个 epoch 验证准确率在 88% 到 94% 之间跳。原因验证集太小或者batch_size太小导致 BatchNorm 统计量不稳定。也可能是学习率太大。解决增大验证集比例或者用交叉验证。增大验证集的batch_size。降低学习率加学习率调度器。5.5 推理时单张图片预测结果不对现象训练时验证准确率 95%但用model.predict()预测单张图片总是错。原因推理时的预处理和验证集不一致。比如忘了ToTensor()或者归一化参数用错或者图片没有转成 RGB。解决把验证集的val_transform单独封装成一个函数推理时直接调用。检查图片模式用Image.open().convert(RGB)强制转成三通道。6. 把模型推到 95% 以上微调策略与推理验证6.1 分层解冻的实操节奏用预训练 ResNet18 做猫狗分类我一般分三个阶段推进。第一阶段只训练分类头5 个 epoch学习率 1e-3。第二阶段解冻layer4再训 5 个 epochlayer4学习率 1e-4分类头 1e-3。第三阶段如果验证准确率还在涨解冻layer3学习率再降一个数量级。# 阶段一只训练分类头 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) train(model, train_loader, val_loader, optimizer, epochs5) # 阶段二解冻 layer4 for param in model.layer4.parameters(): param.requires_grad True optimizer optim.Adam([ {params: model.layer4.parameters(), lr: 1e-4}, {params: model.fc.parameters(), lr: 1e-3} ]) train(model, train_loader, val_loader, optimizer, epochs5)逻辑说明分层解冻的核心是让底层保持通用特征高层适应特定任务。layer4是 ResNet18 最后一个卷积块负责高层语义解冻它能显著提升猫狗这种细粒度分类的表现。参数说明每个阶段结束后都评估验证集如果准确率不再提升就停止。阶段二的学习率不要超过 1e-3否则会破坏预训练权重。6.2 用混淆矩阵看模型到底错在哪准确率只告诉你「错了多少」不告诉你「错成什么样」。猫狗分类虽然只有两类但混淆矩阵能看出模型是偏向猫还是偏向狗。from sklearn.metrics import confusion_matrix, classification_report import numpy as np model.eval() all_preds [] all_labels [] with torch.no_grad(): for images, labels in val_loader: images images.to(device) outputs model(images) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) print(confusion_matrix(all_labels, all_preds)) print(classification_report(all_labels, all_preds, target_names[cat, dog]))逻辑说明confusion_matrix输出 2x2 矩阵对角线是正确分类非对角线是误判。classification_report给出精确率、召回率和 F1。如果猫的召回率明显低于狗说明模型对猫的特征学得不好可以增加猫的样本或调整类别权重。参数说明target_names的顺序要和ImageFolder的类别索引一致通常是按字母顺序cat是 0dog是 1。6.3 导出 ONNX 做推理验证训练完的 PyTorch 模型可以导出成 ONNX方便在其他框架或硬件上部署。导出后一定要用onnxruntime跑一遍确认输出和 PyTorch 一致。import torch.onnx dummy_input torch.randn(1, 3, 224, 224).to(device) torch.onnx.export( model, dummy_input, cat_dog_model.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}}, opset_version11 )逻辑说明dynamic_axes让 batch 维度可变推理时可以一次处理多张图。opset_version11是兼容性较好的版本。导出后可以用onnxruntime.InferenceSession加载对比 PyTorch 和 ONNX 的输出差异如果误差超过 1e-4 就要检查导出过程。参数说明dummy_input的尺寸必须和训练时的输入一致。如果模型有 Dropout 或 BatchNorm导出前要model.eval()。6.4 一个我常用的验证习惯每次训练完我不会只看验证集准确率。我会从验证集里随机抽 20 张图用模型预测一遍把预测错误的图单独存到一个文件夹里肉眼看一下。很多时候模型错的图人也要愣一下比如猫在暗处只露出眼睛或者狗的背影和猫很像。这些图如果太多说明数据集的标注或者分布有问题不是模型的问题。这个习惯帮我省了很多「调参调到怀疑人生」的时间。如果错误图里大部分是模糊、遮挡、非猫非狗的噪声图那该做的是清洗数据而不是换模型。希望帮到你。本文还有配套的精品资源点击获取