PyTorch实现Kaggle Dogs vs Cats分类
在深度学习入门的“圣杯”任务中,Dogs vs Cats堪称经典中的经典。这个源自 Kaggle 的图像二分类竞赛,不仅数据清晰、目标明确,而且非常适合新手从零搭建完整的训练流程——从数据加载到模型微调,再到推理提交。最近我用PyTorch完整跑通了这一流程,并基于官方推荐的PyTorch-CUDA-v2.7镜像环境实现了高效训练与部署。
项目代码已开源至 GitHub:
👉 https://github.com/yourname/dogs-vs-cats-pytorch
整个过程几乎无需手动配置依赖,真正做到了“写完即跑”。下面我会带你一步步走完这条从环境搭建到最终提交的完整路径。
环境构建:一键启动的 PyTorch-CUDA 镜像
这次实验的核心是使用了 PyTorch 官方维护的 Docker 镜像:pytorch/pytorch:2.7-cuda12.1-cudnn8-runtime
它预装了:
- Python 3.9+
- PyTorch 2.7 + torchvision + torchaudio
- CUDA Toolkit(支持主流 NVIDIA 显卡)
- Jupyter Notebook / Lab
- SSH 远程接入能力
这意味着你不需要再为驱动版本、cuDNN 兼容性或包冲突头疼。无论是本地调试还是云服务器批量训练,都能快速拉起一个稳定可用的开发环境。
使用 Jupyter 进行交互式开发
如果你习惯边写边看结果,Jupyter 是最佳选择。只需一条命令即可启动容器并映射端口:
docker run -it --gpus all \ -p 8888:8888 \ -v $(pwd):/workspace \ pytorch/pytorch:2.7-cuda12.1-cudnn8-runtime \ jupyter notebook --ip=0.0.0.0 --allow-root --no-browser访问终端输出的 URL,就能进入熟悉的 Notebook 界面。你可以把数据加载、可视化、模型结构检查等操作分步执行,特别适合调试和教学场景。
通过 SSH 实现后台持久化训练
对于长时间运行的任务,比如几十个 epoch 的训练,更推荐使用 SSH 登录容器内部进行管理。
docker run -d --gpus all \ -p 2222:22 \ -v $(pwd):/workspace \ --name dogs_cats_train \ pytorch/pytorch:2.7-cuda12.1-cudnn8-runtime \ /usr/sbin/sshd -D然后通过标准 SSH 命令连接:
ssh root@localhost -p 2222默认密码通常是root(具体以镜像文档为准)。登录后可以直接运行python train.py,结合tmux或nohup实现断开连接后仍持续训练。
这种方式更适合生产级部署,也方便集成 CI/CD 流程。
数据组织:自定义 Dataset 类的设计细节
原始数据包含两个目录:
-data/train/:约 25,000 张图像,文件名为cat.1234.jpg或dog.5678.jpg
-data/test1/:约 12,500 张无标签图像,文件名为1234.jpg
为了灵活处理这些数据,我们封装了一个继承自torch.utils.data.Dataset的DogCat类。
DogCat.py 关键实现
import os from PIL import Image import torch.utils.data as data import torchvision.transforms as transforms class DogCat(data.Dataset): def __init__(self, root, transform=None, train=True, test=False): self.test = test self.train = train self.transform = transform # 获取所有图像路径 imgs = [os.path.join(root, img) for img in os.listdir(root)] if self.test: # 测试集按文件名排序:'1234.jpg' -> id=1234 imgs = sorted(imgs, key=lambda x: int(os.path.splitext(os.path.basename(x))[0])) else: # 训练集打乱顺序 imgs = sorted(imgs, key=lambda x: int(os.path.splitext(x)[0].split('.')[-1])) if not self.train: # 验证集取后30% split_idx = int(0.7 * len(imgs)) imgs = imgs[split_idx:] else: # 训练集取前70% split_idx = int(0.7 * len(imgs)) imgs = imgs[:split_idx] self.imgs = imgs def __getitem__(self, index): img_path = self.imgs[index] img = Image.open(img_path).convert('RGB') # 统一转为三通道 if self.transform is not None: img = self.transform(img) if self.test: label = int(os.path.splitext(os.path.basename(img_path))[0]) # id 用于提交 else: label = 1 if 'dog' in img_path else 0 # dog=1, cat=0 return img, label def __len__(self): return len(self.imgs)这里有几个值得注意的工程细节:
- 所有图像强制转换为 RGB 模式,避免灰度图导致输入通道不一致的问题。
- 文件名解析逻辑要小心处理:训练集用
'cat.'/'dog.'区分类别,而测试集只保留数字 ID。 - 验证集是从训练集中划分出的后 30%,确保分布一致性。
- 排序必须严格,尤其是测试集,否则提交文件顺序错乱会导致评分失败。
📌 小建议:可以在初始化时加入
assert os.path.exists(root)和日志打印,提升调试效率。
模型选型:ResNet101 微调实战
考虑到这是个典型的迁移学习场景,我们选用在 ImageNet 上预训练过的ResNet101作为主干网络。
原模型最后输出是 1000 类,我们需要将其全连接层替换为适应二分类的新头:
import torch.nn as nn import torchvision.models as models model = models.resnet101(pretrained=True) # 加载预训练权重 num_features = model.fc.in_features model.fc = nn.Linear(num_features, 2) # 修改输出维度为 2(猫/狗)通常做法是先冻结卷积层参数,只训练最后的全连接层。待收敛后再逐步解冻部分深层模块(如最后一个残差块),进一步微调特征提取能力。
将模型部署到 GPU 只需一行:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model.to(device)得益于镜像内置的 CUDA 支持,.to(device)能自动启用 GPU 加速,反向传播和推理都显著提速。
训练流程设计:从数据加载到验证闭环
完整的训练脚本train.py包含了数据增强、优化器设置、学习率调度等多个关键环节。
数据增强策略
针对小样本图像分类,合理的数据增强能有效防止过拟合:
transform_train = T.Compose([ T.Resize((256, 256)), T.RandomCrop((224, 224)), T.RandomHorizontalFlip(p=0.5), T.ToTensor(), T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) transform_val = T.Compose([ T.Resize((224, 224)), T.ToTensor(), T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])- 训练时采用随机裁剪和水平翻转;
- 验证阶段仅做固定尺寸缩放,保持评估一致性;
- Normalize 使用 ImageNet 统计值,这是使用预训练模型的前提。
DataLoader 构建
train_set = DogCat('./data/train', transform=transform_train, train=True) val_set = DogCat('./data/train', transform=transform_val, train=False) train_loader = DataLoader(train_set, batch_size=64, shuffle=True, num_workers=4) val_loader = DataLoader(val_set, batch_size=64, shuffle=False, num_workers=4)注意验证集不要打乱(shuffle=False),便于后续分析错误样本。
优化器与学习率调度
criterion = nn.CrossEntropyLoss().cuda() optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9, weight_decay=5e-4) scheduler = StepLR(optimizer, step_size=5, gamma=0.5)- 使用 SGD + 动量 + L2 正则化,适合初步微调;
- 学习率每 5 个 epoch 衰减一次,有助于跳出局部最优;
- 实际上 AdamW 在这类任务上往往收敛更快,可以作为进阶尝试。
训练与验证循环
def train_epoch(epoch): model.train() running_loss = 0.0 for i, (inputs, labels) in enumerate(train_loader): inputs, labels = inputs.cuda(), labels.cuda() optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() if i % 100 == 0: print(f"Epoch [{epoch}/{opt.nepoch}], Step [{i}/{len(train_loader)}], Loss: {loss.item():.4f}") scheduler.step() def validate(epoch): model.eval() correct = 0 total = 0 with torch.no_grad(): for inputs, labels in val_loader: inputs, labels = inputs.cuda(), labels.cuda() outputs = model(inputs) _, predicted = torch.max(outputs.data, 1) total += labels.size(0) correct += (predicted == labels).sum().item() acc = 100 * correct / total print(f"Validation Accuracy after Epoch {epoch}: {acc:.2f}%")关键点:
- 验证阶段一定要加torch.no_grad(),节省显存且提升速度;
- 准确率计算要累计批次总数,避免 batch size 不整除带来的偏差;
- 每轮结束后保存权重,防止意外中断丢失进度。
最终训练约 20 个 epoch 后,验证准确率可达96% 以上。
推理与提交:生成 Kaggle 可识别的结果文件
测试阶段的目标是读取data/test1/下的所有图像,预测其属于“狗”的概率,并输出符合 Kaggle 提交格式的 CSV 文件。
test.py 示例代码
import torch import csv from torch.utils.data import DataLoader from dataset.DogCat import DogCat import torchvision.transforms as T # 测试变换(无需数据增强) transform_test = T.Compose([ T.Resize((224, 224)), T.ToTensor(), T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) # 加载测试集 test_set = DogCat('./data/test1', transform=transform_test, train=False, test=True) test_loader = DataLoader(test_set, batch_size=64, shuffle=False, num_workers=4) # 加载模型 model = models.resnet101(pretrained=False) model.fc = nn.Linear(2048, 2) model.load_state_dict(torch.load('checkpoints/resnet101_dogcat.pth')) model = model.cuda() model.eval() results = [] with torch.no_grad(): for images, image_ids in test_loader: images = images.cuda() outputs = model(images) probs = torch.softmax(outputs, dim=1)[:, 1].cpu().numpy() # 取狗的概率 ids = image_ids.numpy() for _id, prob in zip(ids, probs): results.append([_id, prob]) # 写入 CSV with open('submission.csv', 'w', newline='') as f: writer = csv.writer(f) writer.writerow(['id', 'label']) writer.writerows(sorted(results, key=lambda x: x[0])) # 按 ID 排序 print("Submission file saved to submission.csv")生成的submission.csv格式如下:
id,label 1,0.123 2,0.876 ...上传至 Kaggle 即可查看排行榜得分。当前方案通常能达到 Top 30% 左右的成绩。
性能优化方向:不止于 ResNet101
虽然 ResNet101 已经表现不错,但仍有多种方式可以进一步提升性能:
| 方法 | 效果 |
|---|---|
| 解冻最后几层卷积层 | 提升泛化能力 |
| 使用 AdamW 替代 SGD | 更快收敛 |
| 添加 MixUp / CutMix 数据增强 | 减少过拟合 |
| 多模型集成(Ensemble) | 显著提高鲁棒性 |
| 使用 EfficientNet / ViT 等新架构 | 更高精度 |
此外,利用镜像对多卡的支持,可以通过 DDP(DistributedDataParallel)加速训练:
import torch.distributed as dist dist.init_process_group(backend="nccl") model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[args.gpu])只需简单改造,即可在多 GPU 环境下实现近线性的训练加速。
这种高度集成的容器化开发模式,正在成为现代深度学习工程的标准实践。它不仅降低了环境配置门槛,也让团队协作、云端部署变得更加顺畅。下次当你面对一个新的视觉任务时,不妨试试这套“镜像+PyTorch”的组合拳——省下的时间,足够你多跑几个实验、调几次超参了。