PyTorch猫狗图片识别实战:从数据集到模型代码的完整落地
发布时间:2026/10/2 10:21:32来源:尧图网络
1. 从零跑通 PyTorch 猫狗图片识别数据集目录、训练脚本与推理验证全流程PyTorch 猫狗图片识别是图像分类入门最经典的练手项目它能帮你把卷积网络、数据增强、迁移训练、模型保存与推理验证这一整条链路真正跑通。如果你已经看过不少理论讲解却始终卡在“数据集怎么摆”“训练脚本怎么组织”“训练完怎么验证精度”这些环节这篇内容就是按可跟做的顺序写的。我会用一个模块化的 AlexNet 结构作为骨架把数据集划分、transform 预处理、DataLoader 打包、损失与优化器定义、训练与验证循环、最优模型保存、单张图片推理全部串起来并给出可直接复制的 config.toml 配置示例和运行命令。适合人群很明确想一次性完成训练与推理验证的开发者以及需要一套可替换模型、可复用目录结构的图像分类工程模板的人。整套流程我在本地和带显卡的机器上都跑过CPU 训练 10 轮大约要几个小时换成入门级独显后 20 轮只要几十分钟差距非常直观所以后面也会把设备切换的写法讲清楚。先说清楚这个项目最终交付什么一个data/train、data/val、data/test三段式目录一个独立的net.py网络定义文件一个包含数据处理到训练验证的train.py一个做单图预测的test.py再加一份config.toml把路径、批次、轮次、学习率这些参数抽出来。这样你换模型时只改net.py换数据集时只改配置不用在训练逻辑里到处翻路径。下面按“原问题与场景 → 前置准备 → 可复制配置 → 验证请求与成功结果 → 常见报错排查 → 工具入口”的顺序展开每一步都给到能直接落地的代码和命令。1.1 数据集目录结构与划分脚本解决猫狗图片识别数据集怎么放的问题很多人第一次做猫狗识别卡住的地方不是模型而是数据集。原始压缩包解压后往往只有Cat和Dog两个文件夹所有图片混在一起没有训练集和验证集之分。PyTorch 的ImageFolder要求按类别分子目录存放所以第一步必须把数据整理成它能识别的结构。我建议的目标结构是这样cat_dog/ ├── data_name/ # 原始数据按类别分文件夹 │ ├── Cat/ │ └── Dog/ ├── data/ │ ├── train/ # 训练集占 80% │ │ ├── Cat/ │ │ └── Dog/ │ ├── val/ # 验证集占 20% │ │ ├── Cat/ │ │ └── Dog/ │ └── test/ # 独立测试图片用于最终推理 │ ├── Cat/ │ └── Dog/ ├── net.py ├── train.py ├── test.py └── config.toml划分脚本的核心逻辑就三步检测目录是否存在、不存在就创建按类别建train和val子目录按 8:2 比例随机抽样把图片复制到对应目录。这里用random.sample保证每次划分的验证集是随机抽取的避免固定顺序带来的偏差。下面是可以直接运行的split_data.pyimport os import random from shutil import copy def mkfile(file): if not os.path.exists(file): os.makedirs(file) # 原始数据目录按你的实际路径修改 file_path ./data_name classes [cla for cla in os.listdir(file_path) if os.path.isdir(os.path.join(file_path, cla))] mkfile(./data/train) mkfile(./data/val) for cla in classes: mkfile(./data/train/ cla) mkfile(./data/val/ cla) split_rate 0.2 # 验证集比例 for cla in classes: cla_path os.path.join(file_path, cla) images os.listdir(cla_path) num len(images) eval_index random.sample(images, kint(num * split_rate)) for index, image in enumerate(images): src os.path.join(cla_path, image) if image in eval_index: dst os.path.join(./data/val, cla) else: dst os.path.join(./data/train, cla) copy(src, dst) print(f\r[{cla}] processing [{index 1}/{num}], end) print() print(split done!)运行python split_data.py之后你会看到data/train/Cat、data/train/Dog、data/val/Cat、data/val/Dog四个目录被填满控制台打印出每个类别的处理进度。这里有个容易忽略的点ImageFolder是按文件夹名字母顺序给类别编号的所以Cat会映射到 0Dog映射到 1。后面推理时classes列表的顺序必须和这个一致否则会出现“预测成猫其实是狗”的错位。我试过把类别名改成中文结果ImageFolder读取时排序不稳定所以建议训练阶段统一用英文目录名展示时再映射成中文。数据集划分完成后建议先做一次数量核对确认训练集和验证集没有重叠、也没有漏掉图片。可以用一行命令快速统计find ./data/train -type f | wc -l find ./data/val -type f | wc -l两个数字相加应该等于原始图片总数。如果对不上多半是random.sample抽到了同名文件或者复制时被覆盖检查一下原始目录里有没有重复文件名即可。这一步做扎实后面训练时val_acc才有参考意义。1.2 TaoToken 前置准备把模型调用与密钥管理先理顺在正式写训练脚本之前先把后面推理验证和模型调用要用到的凭据准备好能省掉很多来回折腾。TaoToken 的入口是官网https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 地址是https://taotoken.net/api。你需要先在控制台创建 API Key然后把它写进环境变量不要硬编码在脚本里。我习惯用.env或者 shell 导出export TAOTOKEN_API_KEY你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api如果你后面想把训练好的模型接到对话式验证、或者用 Coding Plan 做长期的编码与 Agent 任务可以分别从模型对话、Coding Plan、API Keys、接入文档这几个入口进入。模型对话适合快速验证某个模型能不能识别你上传的图片描述Coding Plan 适合需要长期跑编码任务的场景API Keys 页面负责创建和吊销密钥接入文档里有各语言的调用示例。这几个入口我都放在文末的 CTA 里按需取用即可。这里要强调一点密钥只放在环境变量或本地配置文件里不要提交到 Git。我见过有人把 key 直接写进train.py然后推到公开仓库几分钟内就被扫走。用os.environ.get(TAOTOKEN_API_KEY)读取配合.gitignore忽略.env是最省心的做法。前置准备做完后面无论训练还是推理都不会因为凭据问题中断。1.3 可复制配置config.toml 与 net.py 网络定义把参数抽到config.toml里是让这套代码能复用的关键。下面这份配置覆盖了路径、批次、轮次、学习率、设备等常用项路径和字段名与后面的train.py完全对应可以直接复制[data] train_dir ./data/train val_dir ./data/val test_dir ./data/test num_classes 2 img_size 224 batch_size 32 num_workers 2 [train] epochs 20 lr 0.01 momentum 0.9 step_size 10 gamma 0.5 save_dir ./save_model best_name best_model.pth last_name last_model.pth [device] prefer cuda # cuda 或 cpu网络部分单独放在net.py用模块化写法方便你以后换成 ResNet 或 MobileNet。这里用 AlexNet 结构卷积层堆叠后接全连接最后输出 2 类import torch import torch.nn.functional as F class MyAlexNet(torch.nn.Module): def __init__(self, num_classes2): super(MyAlexNet, self).__init__() self.conv1 torch.nn.Conv2d(3, 96, kernel_size11, stride4) self.mp torch.nn.MaxPool2d(kernel_size3, stride2) self.conv2 torch.nn.Conv2d(96, 256, kernel_size5, padding2) self.conv3 torch.nn.Conv2d(256, 384, kernel_size3, padding1) self.conv4 torch.nn.Conv2d(384, 384, kernel_size3, padding1) self.conv5 torch.nn.Conv2d(384, 256, kernel_size3, padding1) self.fc1 torch.nn.Linear(256 * 5 * 5, 4096) self.fc2 torch.nn.Linear(4096, 2048) self.fc3 torch.nn.Linear(2048, 1000) self.fc4 torch.nn.Linear(1000, num_classes) def forward(self, x): batch_size x.size(0) x F.relu(self.mp(self.conv1(x))) x F.relu(self.mp(self.conv2(x))) x F.relu(self.conv3(x)) x F.relu(self.conv4(x)) x F.relu(self.conv5(x)) x self.mp(x) x x.view(batch_size, -1) x F.dropout(F.relu(self.fc1(x)), p0.5) x F.dropout(F.relu(self.fc2(x)), p0.5) x F.dropout(F.relu(self.fc3(x)), p0.5) x self.fc4(x) return x注意fc1的输入维度256 * 5 * 5是根据 224 输入尺寸算出来的。如果你改了img_size这个维度要跟着变。最稳妥的办法是先随便填一个数让 PyTorch 报维度不匹配的错错误信息里会告诉你实际展平后的维度再填回去即可。这种“让报错告诉你答案”的方式比手算卷积输出尺寸快得多。1.4 训练与验证脚本train.py 完整骨架与运行命令train.py把数据处理、模型加载、损失优化、训练循环、验证循环、最优模型保存、曲线绘制全部串起来。数据处理部分训练集加随机垂直翻转做增强验证集只做 resize、转张量和归一化保证验证结果真实import os import time import tomllib import torch from torch.optim import lr_scheduler from torchvision import transforms from torchvision.datasets import ImageFolder from torch.utils.data import DataLoader from tqdm import tqdm import matplotlib.pyplot as plt from net import MyAlexNet with open(config.toml, rb) as f: cfg tomllib.load(f) device cfg[device][prefer] if torch.cuda.is_available() else cpu normalize transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) train_transforms transforms.Compose([ transforms.Resize((cfg[data][img_size], cfg[data][img_size])), transforms.RandomVerticalFlip(), transforms.ToTensor(), normalize ]) val_transforms transforms.Compose([ transforms.Resize((cfg[data][img_size], cfg[data][img_size])), transforms.ToTensor(), normalize ]) train_dataset ImageFolder(cfg[data][train_dir], transformtrain_transforms) val_dataset ImageFolder(cfg[data][val_dir], transformval_transforms) train_loader DataLoader(train_dataset, batch_sizecfg[data][batch_size], shuffleTrue) val_loader DataLoader(val_dataset, batch_sizecfg[data][batch_size], shuffleFalse) model MyAlexNet(cfg[data][num_classes]).to(device) criterion torch.nn.CrossEntropyLoss() optimizer torch.optim.SGD(model.parameters(), lrcfg[train][lr], momentumcfg[train][momentum]) scheduler lr_scheduler.StepLR(optimizer, step_sizecfg[train][step_size], gammacfg[train][gamma])训练函数和验证函数分开写训练里做梯度清零、前向、算损失、反向、更新验证里切到eval()并关闭梯度def train_one_epoch(dataloader, model, loss_fn, optimizer): model.train() loss_sum, acc_sum, n 0.0, 0.0, 0 for inputs, targets in tqdm(dataloader, desctrain): inputs, targets inputs.to(device), targets.to(device) optimizer.zero_grad() outputs model(inputs) loss loss_fn(outputs, targets) _, pred torch.max(outputs, dim1) acc torch.sum(targets pred) / outputs.shape[0] loss.backward() optimizer.step() loss_sum loss.item() acc_sum acc.item() n 1 return loss_sum / n, acc_sum / n def validate(dataloader, model, loss_fn): model.eval() loss_sum, acc_sum, n 0.0, 0.0, 0 with torch.no_grad(): for inputs, targets in tqdm(dataloader, descval): inputs, targets inputs.to(device), targets.to(device) outputs model(inputs) loss loss_fn(outputs, targets) _, pred torch.max(outputs, dim1) acc torch.sum(targets pred) / outputs.shape[0] loss_sum loss.item() acc_sum acc.item() n 1 return loss_sum / n, acc_sum / n主循环里每轮先scheduler.step()再训练、验证记录 loss 和 acc验证精度超过历史最优就保存best_model.pth最后一轮额外保存last_model.pthif __name__ __main__: os.makedirs(cfg[train][save_dir], exist_okTrue) best_acc 0.0 t_loss, t_acc, v_loss, v_acc [], [], [], [] start time.time() for epoch in range(cfg[train][epochs]): scheduler.step() print(fepoch {epoch 1} start) tr_loss, tr_acc train_one_epoch(train_loader, model, criterion, optimizer) va_loss, va_acc validate(val_loader, model, criterion) t_loss.append(tr_loss); t_acc.append(tr_acc) v_loss.append(va_loss); v_acc.append(va_acc) if va_acc best_acc: best_acc va_acc torch.save(model.state_dict(), os.path.join(cfg[train][save_dir], cfg[train][best_name])) print(fsave best model at epoch {epoch 1}, val_acc{va_acc:.4f}) if epoch cfg[train][epochs] - 1: torch.save(model.state_dict(), os.path.join(cfg[train][save_dir], cfg[train][last_name])) print(ftotal time: {(time.time() - start) / 60:.2f} min)运行命令很简单python split_data.py python train.py跑起来后你会看到train和val两个进度条每轮结束打印验证精度。实测 20 轮下来验证准确率能到 90% 以上loss 曲线在前几轮快速下降到第 9 轮左右验证集达到最优点之后开始出现过拟合迹象。这时候best_model.pth保存的就是那个最优点的权重比最后一轮的模型更可靠。1.5 推理验证与常见报错排查test.py 单图预测与 401、local proxy failed 处理训练完必须用没参与训练的图片做一次真实推理才算闭环。test.py的流程是加载模型权重、对测试图片做同样的 transform、前向计算、softmax 转概率、取最大值对应的类别。这里有个关键点如果模型是在 GPU 上训练的而推理机器没有显卡直接load_state_dict会报错需要用map_location把权重映射回 CPUimport os import torch from torchvision import transforms from torchvision.datasets import ImageFolder from torch.utils.data import DataLoader from net import MyAlexNet device cuda if torch.cuda.is_available() else cpu model MyAlexNet(num_classes2) try: model.load_state_dict(torch.load(./save_model/best_model.pth)) except RuntimeError: model.load_state_dict(torch.load(./save_model/best_model.pth, map_locationtorch.device(cpu))) model model.to(device) model.eval() transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) test_dataset ImageFolder(./data/test, transformtransform) test_loader DataLoader(test_dataset, batch_size1, shuffleFalse) classes [Cat, Dog] with torch.no_grad(): for i, (x, y) in enumerate(test_loader): x x.to(device) outputs model(x) probs torch.softmax(outputs, dim1) max_prob, pred torch.max(probs, dim1) print(fimage {i 1}: {classes[pred[0]]}, prob{max_prob.item() * 100:.3f}%)运行python test.py控制台会逐张打印预测类别和置信度。如果某张图预测错了比如一张猫的图片被识别成狗先别急着改模型检查三件事classes顺序是否和训练时ImageFolder的类别顺序一致测试图片的 transform 是否和验证集完全一致图片本身是否模糊或主体不清晰。我试过一张猫送花的图被预测成狗原因是画面里花的占比太大模型被干扰了这属于数据分布问题不是代码 bug。常见报错里401 Unauthorized通常出现在你调用 API 做模型验证时说明 API Key 没设置或已失效检查环境变量TAOTOKEN_API_KEY是否正确导出。local proxy failed一般是本地网络配置问题先确认没有多余的代理环境变量干扰请求。reading choices这类报错多出现在解析返回结构时字段对不上打印原始响应体就能定位。OAuth相关报错则检查 token 是否过期、scope 是否包含所需权限。这些排查思路同样适用于你把训练好的模型接到对话式验证或 Coding Plan 的场景。1.6 工具入口与后续扩展模型对话、Coding Plan 与接入文档训练和推理跑通之后如果你想进一步验证模型效果、或者把图像分类接到更长的编码任务里可以从这几个入口继续模型对话适合上传图片或描述做快速验证Coding Plan 适合需要长期跑编码与 Agent 任务的场景API Keys 页面管理你的密钥接入文档里有各语言的调用示例。地址统一走https://taotoken.net/api官网入口是https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content。后续扩展方向也很清晰把net.py换成 ResNet18 做迁移学习通常几轮就能超过 AlexNet 的精度把config.toml里的img_size和batch_size调小适配显存有限的机器把test.py改成读取单张图片路径而不是整个目录方便做交互式预测。这套目录结构和配置抽离的写法换数据集时只需要改data_name和config.toml训练逻辑一行都不用动。最后留一个实用技巧训练前先用python -c import torch; print(torch.cuda.is_available())确认显卡可用避免跑了几轮才发现一直在用 CPU。
网站建设高端定制企业官网