PyTorch迁移学习实战:猫狗分类准确率突破99%
2026/9/11 21:44:10 网站建设 项目流程

简介:基于 PyTorch 框架实现的 Kaggle 猫狗分类完整源码,适合深度学习初学者、课程设计以及期末大作业参考。项目采用迁移学习思路,选用 GoogLeNet、ResNet 与 ResNeXt 三个预训练模型提取图像特征,将各自全局平均池化层输出的特征拼合为高维向量,再通过单层全连接网络完成二分类,最终准确率超过百分之九十九。资源本身共包含四个文件,其中两个 Python 脚本分别承担特征提取和模型训练任务,一个说明文档用于辅助理解代码流程,一个提交样例文件可以对照提交格式。压缩包整体只有三十四 KB,结构简洁,适合快速阅读与二次修改。特征提取脚本会自动加载预训练权重并生成特征,训练脚本直接使用处理好的特征进行训练,同时应用随机失活防止过拟合,在中央处理器上数秒即可跑完,没有图形处理器也能快速验证。目前已有三百零四人学习,对于希望了解迁移学习、特征融合并完成类似图像分类项目的开发者很有帮助。

1. 猫狗分类是 Kaggle 最经典的二分类入门赛:25000 张训练图、12500 张测试图,判断每张图是猫还是狗。真正有意思的地方在“准确率超过 99%”——从零训练的 CNN 通常卡在 93% 到 96%,缺口全在毛发纹理、光照、遮挡和犬种差异上。要做到 99%,可靠路径是 PyTorch + ImageNet 预训练权重 + 数据增强 + 分层学习率微调,而不是自己堆更深的网络。

下面沿这条路径拆:环境配套、数据集整理、模型替换、训练调参、推理提交,每步给可直接复制的命令和代码。适合刚做完课程作业、想从 95% 推到 99% 的同学,也适合想一周内把图像分类 pipeline 从训练跑到上线的工程师。

2. PyTorch 环境配套与 Kaggle 猫狗数据集的本地化

拿到任何一套 PyTorch 图像分类代码,第一件事不是看模型定义,而是把环境钉在同一套版本上。PyTorch 的 CUDA 预编译 wheel 与 torchvision 强绑定,版本错位最常见的表现是import torch直接报错,或者训练时静默落到 CPU 上跑;数据集的目录结构如果不是train/类别/图片这种层级,ImageFolder 接口第一步就会抛异常。这两件事在猫狗分类这种入门项目里反而是翻车率最高的地方。

2.1 Python / PyTorch / CUDA 版本匹配与安装

一套被反复验证过的组合是:Python 3.10.11、PyTorch 2.8.0、torchvision 0.23.0、CUDA 12.1。这套组合的二进制依赖齐全,numpy、pandas、Pillow 都有对应 wheel,装完不需要再编译任何东西。

conda create -n dogscats python=3.10.11 -y conda activate dogscats pip install torch==2.8.0 torchvision==0.23.0 --index-url https://download.pytorch.org/whl/cu121 pip install numpy pandas matplotlib tqdm pillow kaggle

--index-url指向 PyTorch 官方 cu121 预编译源,装的是带 CUDA 算子的版本,体积约 2.5GB。torch==2.8.0必须和torchvision==0.23.0配套,版本拆开装会导致后面torchvision.models接口不兼容。如果下载速度不理想,先把 torch 那行装完、把官方源切回默认 PyPI,再装其余依赖,不要中途换源去混装。

import torch, torchvision print(torch.__version__, torchvision.__version__) # 期望 2.8.0 0.23.0 print(torch.cuda.is_available()) # 期望 True print(torch.cuda.get_device_name(0)) # 期望显示你的显卡型号

torch.cuda.is_available()返回 False 时,先执行nvidia-smi确认驱动版本,再检查当前环境里是否残留了 CPU 版 torch,pip list里看 torch 构建标识最直接。显卡显存低于 6GB 时,后面章节里 batch size 和输入分辨率都要跟着下调,环境这一层不用强求统一。

2.2 用 Kaggle API 下载数据集并重排目录

原竞赛名是 dogs-vs-cats,官方数据分 train 和 test1 两个目录。下载最快的方式是 Kaggle API,前提是已经在账户后台生成了 kaggle.json。

pip install kaggle mkdir -p ~/.kaggle cp ~/Downloads/kaggle.json ~/.kaggle/kaggle.json chmod 600 ~/.kaggle/kaggle.json kaggle competitions download -c dogs-vs-cats unzip -q dogs-vs-cats.zip

解压后的原始结构必须搞清楚,它决定了后面所有脚本的路径假设:

dogs-vs-cats/ ├── train/ │ ├── cat.0.jpg ... cat.12499.jpg │ └── dog.0.jpg ... dog.12499.jpg └── test1/ ├── 1.jpg └── ...

train 里的文件名是类别.编号.jpg,test1 里是纯数字编号,两类编号不存在对应关系。这里有两个坑要提前处理:原始 train 目录按类别连续排列,不随机打乱的话验证集的类别分布会偏;测试集不带标签,只能按文件名全量推理。第一步把数据重排成 PyTorch ImageFolder 要求的train/val两级结构:

import os, random, shutil src = "dogs-vs-cats/train" for split in ("data/train", "data/val"): for cls in ("cat", "dog"): os.makedirs(os.path.join(split, cls), exist_ok=True) for fn in os.listdir(src): cls = fn.split(".")[0] split = "train" if random.random() < 0.9 else "val" shutil.copy(os.path.join(src, fn), os.path.join("data", split, cls, fn)) print("train 猫:", len(os.listdir("data/train/cat"))) print("val 猫:", len(os.listdir("data/val/cat")))

random.random() < 0.9形成 90/10 划分,边拷贝边随机等价于全局 shuffle,避免连续读取造成验证集全是同一类别。拷贝而不是移动,是为了保留原始数据,后面做全量重训时还能用。chmod 600那步不能省,Kaggle API 对密钥文件权限有硬校验。

2.3 数据增强配置:训练集与验证集必须分开

99% 准确率的第二个关键在增强。训练集的 transform 要做足,验证集的 transform 要收敛,两者不能共用。很多人直接复用训练增强做验证,val_acc 凭空掉 0.5 到 1 个点,然后误判模型过拟合去加正则,越调越偏。

参数训练集验证集作用
RandomResizedCrop(224, scale=(0.7,1.0))使用Resize(256)+CenterCrop(224)模拟目标大小变化
RandomHorizontalFlip(p=0.5)使用不使用等价扩大两倍样本
RandomRotation(15)使用不使用容忍拍摄角度偏差
ColorJitter(0.3,0.3,0.3)使用不使用抗光照和色调差异
from torchvision import transforms mean, std = [0.485, 0.456, 0.406], [0.229, 0.224, 0.225] train_tf = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.7, 1.0)), transforms.RandomHorizontalFlip(p=0.5), transforms.RandomRotation(15), transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.3), transforms.ToTensor(), transforms.Normalize(mean, std), ]) val_tf = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean, std), ])

Normalize 必须使用 ImageNet 的 mean/std,因为预训练权重就是在这个分布上学出来的,换任何自定义统计量都会让权重失效。scale=(0.7,1.0)不要用 torchvision 默认的 0.08,猫狗主体占画面比例大,裁剪比例过狠会把判别部位裁没。验证集只做 Resize 加 CenterCrop,保证每次评估口径一致,精度的浮动才能真实反映训练状态。

3. 模型设计与预训练权重:99% 准确率靠迁移学习,不靠改网络

3.1 从零训练的 CNN 为什么上不了 99%

25000 张图对于一个二分类任务不算少,但从零训练时,网络底层卷积核要从噪声里重新学边缘、纹理、形状的层级表示。数据量和可用算力都不足以支撑模型在毛发走向、犬种差异这些细粒度特征上达到判别级精度。实际表现是训练损失能压到很低,验证准确率却趴在 95% 附近,调 dropout、换激活函数都难以突破。

问题不在网络结构,而在初始化。ImageNet 预训练权重在 128 万张图上已经学到了通用的视觉特征表示,迁移学习把这份先验直接继承下来,微调阶段只做局部适配,把最后一层从 1000 类换成 2 类。这也是 Kaggle 猫狗竞赛历届前排方案从早期就清一色使用预训练模型的根本原因——比赛比的是数据利用效率,不是结构创新。

3.2 预训练模型怎么选:ResNet50、EfficientNet-B4 还是 ConvNeXt-Tiny

模型参数量单卡 224 推理耗时适用场景
ResNet5025.6M约 10ms显存小,先跑通流程
EfficientNet-B419M约 25ms精度与算力平衡,99% 的主力
ConvNeXt-Tiny28.6M约 30ms结构现代,微调收敛快

默认选择是 EfficientNet-B4。它参数量比 ResNet50 更少,但 depthwise 卷积的算力利用率高,同等 224 输入下精度更好;BN 层对批次大小的敏感度也比 ResNet 低。显存低于 6GB 就先用 ResNet50 把全套流程跑通,记录验证集基线,再换 B4 提点。不要在入门阶段直接追 ConvNeXt,它的收益要在更大数据规模上才明显。

3.3 加载 torchvision 预训练模型并替换分类头

import torch import torchvision.models as models model = models.efficientnet_b4( weights=models.EfficientNet_B4_Weights.IMAGENET1K_V1 ) in_features = model.classifier[1].in_features model.classifier[1] = torch.nn.Linear(in_features, 2) total = sum(p.numel() for p in model.parameters()) print(f"参数量: {total / 1e6:.1f}M")

EfficientNet 的分类头是 Sequential(Dropout, Linear),下标 1 才是线性层;ResNet 对应的是model.fc = torch.nn.Linear(2048, 2)weights=IMAGENET1K_V1表示加载官方预训练权重,首次运行自动下载到TORCH_HOME缓存目录,网络慢时可以先手动下载再设置这个环境变量指向本地。替换分类头后参数量不变,变的只是最后输出维度。

3.4 冻结主干与解冻微调:分两阶段做

一下把所有层解冻、学习率统一给到 1e-3,预训练特征会被快速冲掉,模型事实上在从零重学。常见做法是分两阶段:

# 阶段一:只训练分类头 for p in model.features.parameters(): p.requires_grad = False optimizer = torch.optim.AdamW(model.classifier.parameters(), lr=1e-3) # 阶段二:解冻主干,分层设置学习率 for p in model.features.parameters(): p.requires_grad = True optimizer = torch.optim.AdamW([ {"params": model.features.parameters(), "lr": 3e-5}, {"params": model.classifier.parameters(), "lr": 1e-4}, ])

阶段一跑 2 个 epoch,让随机初始化的分类头先适配预训练特征;阶段二解冻整网,主干用 3e-5、分类头用 1e-4 的分层学习率继续训练。很多人一上来就全量微调,loss 下降很快,验证集却停在 97% 附近,问题多数出在这里——低层卷积的预训练特征被大步长更新破坏了,前半程学到的 128 万张图的通用表示在前 1000 步迭代里就被覆盖掉。

4. 训练与调参:把猫狗分类验证准确率推到 99% 的 8 组参数

4.1 优化器、学习率调度与损失函数

训练部分真正决定上限的是一组参数组合,单独调任何一个都出不来 99%。下面这张表是验证过的一整套配置:

参数取值设置理由
optimizerAdamW带权重衰减的 Adam,微调场景表现更稳
主干 lr / 分类头 lr3e-5 / 1e-4预训练特征更新幅度必须小于新分类头
weight_decay1e-4不加会掉 0.3 个点左右,B4 尤其明显
batch_size328GB 显存的安全值,不够降 16
schedulerCosineAnnealingLR余弦退火,末段 lr 逼近 0 时精度还会涨一截
T_max / eta_min15 / 1e-6与 epoch 数对齐,lr 平滑降到 1e-6
lossCrossEntropyLoss(label_smoothing=0.05)软化标签,减少过拟合
epochs2 + 152 个 epoch 冻结微调,15 个 epoch 全量微调

label smoothing 在二分类里等价于把 0/1 目标改成 0.025/0.975,模型不再追求把输出概率推向极端值,验证集上通常能多拿 0.1 到 0.2 个点。CosineAnnealingLR 的 T_max 必须和实际训练 epoch 数保持一致,否则学习率曲线会在中途出现跳变,末段精度抬不上去。

4.2 训练循环与混合精度

from torch.cuda.amp import GradScaler, autocast model.to(device) criterion = torch.nn.CrossEntropyLoss(label_smoothing=0.05) optimizer = torch.optim.AdamW([ {"params": model.features.parameters(), "lr": 3e-5}, {"params": model.classifier.parameters(), "lr": 1e-4}, ], weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_max=15, eta_min=1e-6 ) scaler = GradScaler() for epoch in range(15): model.train() for x, y in train_loader: x, y = x.to(device), y.to(device) with autocast(): loss = criterion(model(x), y) optimizer.zero_grad() scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() scheduler.step()

autocast在 RTX 20 系及以上显卡上默认走 fp16 计算,显存占用近乎减半,训练速度提升明显;GradScaler负责梯度放大,避免 fp16 下的梯度下溢。label 不需要转 half,CrossEntropyLoss 内部会自己处理精度。scheduler.step()放在每个 epoch 结束后调用,对应 CosineAnnealingLR 按 epoch 步进,不要放到每个 batch 里。

DataLoader 记得设置num_workers=4, pin_memory=True, shuffle=True。shuffle 对训练集的随机性影响直接反映在 val_acc 曲线平滑度上,pin_memory 能减少 CPU 到 GPU 的拷贝等待。每次 epoch 结束跑一次 val_tf 评估,记录 val_acc,把最优权重存成best.pt。上述配置在 8GB 显存的显卡上,EfficientNet-B4 单 epoch 约 20 分钟,15 个 epoch 内能看到 val_acc 从 95% 爬到 99% 以上。

4.3 三个容易踩的准确率杀手

第一个是学习率给得太统一。主干直接用 1e-3 更新,低层卷积很快脱离 ImageNet 特征空间,val_acc 永远差一口气。检查办法是加载 best.pt 后看层间梯度范数,正常情况主干层梯度范数应该显著小于分类头,如果两者同量级说明主干更新过猛。

第二个是验证集误用训练增强。共用 train_tf 会让 val_acc 比真实水平低 0.5 到 1 个点,于是误判模型能力不够,盲目加大正则和 dropout,实际是在带偏的系统上做无用功。验证集只用 Resize 和 CenterCrop,保持口径干净。

第三个是 batch size 与学习率不同步。batch 从 32 调到 64 时学习率没跟着调,BN 统计量在两个尺度之间跳动,val_acc 曲线出现锯齿。经验规则:batch size 翻倍则 lr 翻倍,batch size 减半则 lr 减半,改完 batch 必须同时改 lr 和 T_max 的对应关系。

5. 推理、TTA 与 Kaggle 提交格式的最后一公里

训练结束到最终提交,还有三个动作能再捞 0.2 到 0.4 个点的收益:测试时增强、全量数据重训、TorchScript 导出。顺序不能乱——先确认验证集准确率达到预期,再做 TTA 打分测试,最后决定导出格式。

5.1 TTA 推理代码

from PIL import Image tta_tf = transforms.Compose([ transforms.Resize(256), transforms.RandomHorizontalFlip(p=0.5), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean, std), ]) def predict_tta(model, img_path, device, n_times=4): model.eval() probs = [] for _ in range(n_times): x = tta_tf(Image.open(img_path)).unsqueeze(0).to(device) with torch.no_grad(): probs.append(torch.softmax(model(x), dim=1).cpu()) return torch.mean(torch.stack(probs), dim=0)

TTA 的增强只保留随机翻转和中心裁切,不加旋转、颜色扰动,后者的方差大于收益,会拉低均值投票的效果。二分类任务 4 次 TTA 通常能提升 0.1 到 0.2 个百分点,代价是推理时间乘以 4,在 12500 张测试图上属于可接受范围。

5.2 提交文件的 id 与 label 列

import pandas as pd ids, preds = [], [] for fn in sorted(os.listdir("dogs-vs-cats/test1")): p_dog = predict_tta(model, os.path.join("dogs-vs-cats/test1", fn), device)[0, 1] ids.append(fn.split(".")[0]) preds.append(float(p_dog)) sub = pd.DataFrame({"id": ids, "label": preds}) sub.to_csv("submission.csv", index=False) print(sub.shape) # (12500, 2)

提交格式固定两列:id 是测试图片文件名去掉 .jpg,label 是该图属于狗的概率,0 到 1 之间的浮点数。test1 目录里是1.jpg2.jpg这样的纯数字编号,与 train 里的 cat/dog 编号没有重叠,按文件名排序推理即可,Kaggle 会按 id 字段自动匹配。

5.3 上生产环境的导出方式

提交之外,模型要脱离训练脚本运行,推荐导出 TorchScript。先加载训练好的权重,再用一张真实尺寸的输入做 trace,固定输入 shape 后推理吞吐更高:

model.load_state_dict(torch.load("best.pt")) model.eval() example = torch.rand(1, 3, 224, 224) traced = torch.jit.trace(model.cpu(), example) traced.save("dogscat_ts.pt")

Trace 要在model.eval()状态下进行,否则 BN 层会记录推理时的统计量导致导出的模型输出漂移。最后一步是出分前用全部 25000 张图按同一组参数重训一次,再做 TTA 打分和提交;验证集只用来选超参和早停,最终模型不该浪费那 10% 的数据。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询