
简介基于PyTorch打造的深度学习物体分类系统整合了数据集搜集与划分、模型构建、训练调优、测试评估和图形化界面演示等完整流程面向深度学习初学者、计算机视觉开发者以及需要快速搭建分类项目的工程师。压缩包共73个文件以Python脚本和图像样本为主另有预训练权重、说明文档与依赖清单整体体积94.72MB目录结构清晰便于按模块复用。系统内置ResNet等常见分类模型借助图形界面可直观查看训练损失曲线、准确率变化以及单张图片的预测结果和置信度并能调整参数实时观察性能变化免去繁琐的命令行操作。配套代码覆盖数据清洗、训练验证、模型导出与UI交互等环节同时给出数据集预处理思路如归一化、裁剪、缩放等帮助理解物体分类的完整数据流水线。目前已有778人学习适合作为课程设计、毕业设计或深度学习入门实践参考。1. 为什么说物体分类系统的瓶颈在数据管道和界面回调而不是模型很多人接到“基于 PyTorch 开发的深度学习物体分类系统图形化界面”这个题目第一反应是去排行榜上挑分类模型。实际写过一遍就会发现真正卡人的是三个点数据从哪来、怎么变干净训练过程怎么从黑盒变成进度条和 loss 曲线训练完的模型怎么被图形界面无感调用。模型反而是最不稀缺的部分PyTorch 的 torchvision 里已经内置了一批预训练权重你需要做的只是“换头改输出”。这类项目的正确打开方式是把它当成一条“数据管道 事件回调”来设计数据集搜集与整理是一段模型训练与测试是一段图形化界面通过回调订阅前两段的运行状态再负责展示结果和实时推理。下面按这个次序把每个环节的命令、参数和代码边界讲清楚。这套思路可以直接落到课程设计、团队内部标注工具或者小规模物体识别系统上避免 notebook 里能跑、一接界面就改得面目全非的尴尬。2. PyTorch 物体分类的项目结构、预训练底座与可配置参数2.1 先按数据、训练、界面拆目录再写第一行模型代码我一般这样组织工程结构而不是把所有代码堆进两三个 .py 文件object-classifier/ ├── configs/ │ └── config.yaml ├── data/ │ ├── raw/ # 脚本抓取/人工导入的原始图片 │ ├── processed/ # 清洗后按类别分好目录的图片集 │ └── checkpoints/ # 训练过程中保存的模型权重 ├── models/ # 模型构造逻辑 ├── utils/ # 数据清洗、日志、训练回调 ├── ui/ # PySide6 图形界面 ├── train.py # 训练入口 ├── test.py # 测试入口 └── requirements.txt这样拆的原因不只是为了好看数据搜集脚本只写 raw 到 processed训练脚本只读 processed界面脚本只调用训练函数与推理函数。任何一环出错替换对应目录就行不用重写其它代码。尤其是界面与训练解耦后面接进度条、接日志、接调试工具时都顺手。2.2 torchvision 里哪些预训练模型适合做物体分类底座物体分类是单标签任务对骨干网络的要求通常低于目标检测和图像分割因此优先考虑“预训练权重成熟、显存友好、推理快”的模型。这里有一张选型表按数据量和部署条件选模型参数量显存压力适用场景ResNet18约 11M很低小数据集、CPU 推理、快速验证ResNet50约 25M较低通用首选准确率与资源较均衡MobileNetV3-Small约 2.5M极低边缘设备、界面内置摄像头实时推理EfficientNetV2-S约 21M中等追求精度的中小规模数据集Swin-T约 28M较高数据量大、训练资源足时对比实验模型切换可以直接用 torchvision 的动态入口避免为每个模型写死一行代码import torch import torch.nn as nn import torchvision.models as models def build_model(backbone: str, num_classes: int, pretrained: bool True): weights_arg DEFAULT if pretrained else None model getattr(models, backbone)(weightsweights_arg) if hasattr(model, fc): in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes) elif hasattr(model, classifier): in_features model.classifier[-1].in_features model.classifier[-1] nn.Linear(in_features, num_classes) return model代码说明getattr(models, backbone)允许通过字符串在 ResNet、MobileNet、EfficientNet 等模型间切换新旧版 torchvision 的写法差异主要在weights参数上新版用权重枚举旧版用pretrainedTrue。hasattr(model, fc)和hasattr(model, classifier)分别覆盖了 ResNet 类和 MobileNet/EfficientNet 类两种分类头的位置。小数据集上我一般会先把model.fc之外的所有层requires_grad_(False)只训练分类头等收敛后再决定要不要解冻骨干网络。2.3 config.yaml 与 get_model 组合让超参不再散落在代码里训练脚本里最忌讳到处是魔法数字。把所有可变项放进一个 YAMLdata: train_dir: data/processed/train val_dir: data/processed/val num_workers: 8 input_size: 224 model: name: resnet50 num_classes: 10 pretrained: true freeze_backbone: true train: batch_size: 32 epochs: 50 lr: 0.0003 weight_decay: 0.0001 early_stop_patience: 7 mixed_precision: false配套加载代码即 Config 类用yaml.safe_load读取即可同时把训练入口的参数也统一从配置读取。这样做最直接的好处是一次训练一个 configs 目录下的实验配置换数据集和换模型时不用改动训练函数本体也方便对比多次实验结果。3. 数据集搜集与自动整理从原始图片到 DataLoader3.1 用目录结构表达标签先跑一遍清洗脚本PyTorch 的torchvision.datasets.ImageFolder约定一个子目录就是一个类别子目录里的图片归到该类。因此数据集搜集完成后的第一件事是整理成下面这种结构data/processed/train/ ├── apple/ ├── banana/ └── orange/ data/processed/val/ ├── apple/ ├── banana/ └── orange/网上抓下来的图片经常有损坏、重复和格式异常训练前必须洗一遍。下面这个脚本会遍历目录删除无法被 Pillow 打开的文件并把非 RGB 图片转换为 RGBfrom pathlib import Path from PIL import Image def clean_images(root_dir: str, min_size: int 32): root Path(root_dir) removed 0 for img_path in root.rglob(*.jpg): try: with Image.open(img_path) as im: im.verify() with Image.open(img_path) as im: if im.width min_size or im.height min_size: img_path.unlink() removed 1 continue if img_path.suffix.lower() .png: with Image.open(img_path) as im: im.convert(RGB).save(img_path) except Exception: img_path.unlink() removed 1 print(f清洗完成移除 {removed} 个异常文件)im.verify()只校验文件头真正要拿到像素数据还得再 open 一次。转 RGB 是为了避免训练数据里混入 RGBA 四通道图否则 DataLoader 里会因为通道数不一致报 batch 维度错误。文件过小时删除是因为这类图大概率是裁剪残留或无效截图保留只会放大噪声。3.2 合规搜集公开图片的最小脚本关于数据集搜集务必遵守目标站点的 robots 协议和版权声明图片仅用于个人学习与算法验证。下面给出一个只抓取页面中img标签图片的最小脚本它的作用是“把公开可访问的图片链接下载到本地”具体入口请替换成你拥有使用权的页面或开放图片站import requests from bs4 import BeautifulSoup def collect_images(page_url: str, save_dir: str, label: str, max_num: int 200): headers {User-Agent: Mozilla/5.0 (learning-research)} resp requests.get(page_url, headersheaders, timeout10) soup BeautifulSoup(resp.text, html.parser) count 0 for img in soup.find_all(img): src img.get(src) or img.get(data-src) if not src or count max_num: break try: r requests.get(src, headersheaders, timeout10) ext src.rsplit(., 1)[-1].lower() if ext not in (jpg, jpeg, png): continue with open(f{save_dir}/{label}_{count}.jpg, wb) as f: f.write(r.content) count 1 except Exception: continue参数说明src可能是相对路径抓取前建议用urllib.parse.urljoin补全>import torchvision.transforms as T train_transform T.Compose([ T.RandomResizedCrop(224, scale(0.3, 1.0)), T.RandomHorizontalFlip(p0.5), T.ColorJitter(brightness0.3, contrast0.3), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_transform T.Compose([ T.Resize(256), T.CenterCrop(224), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) dataset ImageFolder(data/processed/train, transformtrain_transform) dataloader DataLoader(dataset, batch_size32, shuffleTrue, num_workers8, pin_memoryTrue)参数说明RandomResizedCrop的scale(0.3, 1.0)控制裁剪面积占原图的比例小于 0.3 会让物体过小增大过拟合风险Normalize的均值方差是 ImageNet 统计量使用预训练权重时不要随意改。ColorJitter在真实物体数据上有效但在医学影像、文档图像这类色彩本身是语义一部分的数据上应该去掉。num_workers在 Windows 上有时会引起多进程死锁调成 0 可以规避代价是数据加载慢一些。4. 模型训练、测试与图形界面的进度桥接4.1 带断点续训和早停的训练循环图形界面项目里训练循环不能“一次跑完不管”用户希望能看到一个 epoch 结束后的实时反馈也要能手动中断后接着跑。下面的训练函数加入了保存断点与验证集早停import torch import torch.nn as nn from pathlib import Path def train_model(model, train_loader, val_loader, cfg, on_epoch_endNone): device cuda if torch.cuda.is_available() else cpu model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW(model.parameters(), lrcfg[train][lr], weight_decaycfg[train][weight_decay]) best_acc, no_improve 0.0, 0 ckpt_dir Path(data/checkpoints) ckpt_dir.mkdir(exist_okTrue) for epoch in range(cfg[train][epochs]): model.train() running_loss, correct, total 0.0, 0, 0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() logits model(images) loss criterion(logits, labels) loss.backward() optimizer.step() running_loss loss.item() correct (logits.argmax(1) labels).sum().item() total labels.size(0) acc correct / total val_acc evaluate(model, val_loader, device) if val_acc best_acc: best_acc val_acc no_improve 0 torch.save({model: model.state_dict(), epoch: epoch, best_acc: best_acc}, ckpt_dir / best.pt) else: no_improve 1 if on_epoch_end: on_epoch_end(epoch, running_loss, acc, val_acc, best_acc) if no_improve cfg[train][early_stop_patience]: print(早停触发验证集指标连续未提升) break训练循环的关键点AdamW 的weight_decay与 Adam 不同它是解耦的权重衰减通常设1e-4左右即可best.pt保存的是验证集最优状态而不是最后一个 epoch避免过拟合后的权重污染早停 patience 一般设为 710数据集小的时候可以放宽到 15。如果显存足够把cfg[train][mixed_precision]设为True并用torch.cuda.amp包住前向与反向训练速度在 30 系以上 NVIDIA 显卡上有明显提升但老显卡和 CPU 环境不要开。4.2 测试阶段除了 accuracy还要看混淆矩阵和错误样本只打印一个 top-1 acc 对图形界面项目来说远远不够。用户更关心“哪些类别总是分不清”。测试脚本里应该输出每类召回率和混淆矩阵并把错误样本的路径记录下来from sklearn.metrics import confusion_matrix, classification_report def test_and_collect(model, test_loader, class_names, device): model.eval() y_true, y_pred, wrong_files [], [], [] with torch.no_grad(): for images, labels, paths in test_loader: images images.to(device) labels labels.numpy() logits model(images) preds logits.argmax(1).cpu().numpy() y_true.extend(labels) y_pred.extend(preds) for path, label, pred in zip(paths, labels, preds): if label ! pred: wrong_files.append((path, class_names[label], class_names[pred])) cm confusion_matrix(y_true, y_pred) report classification_report(y_true, y_pred, target_namesclass_names) print(report) return cm, wrong_files这里有个容易踩的坑默认的测试数据集返回(image, label)拿不到文件路径。我一般写一个继承ImageFolder的 Dataset在__getitem__里额外返回self.samples[idx][0]这样错误样本可以直接在界面上用缩略图展示。分类报告里如果某个类别的召回率明显低于其它类常见原因有两个一个是该类样本量太少另一个是该类与某个近邻类别在视觉上高度相似比如不同品种的狗。前者加数据后者考虑改标签或增加细粒度特征。4.3 用回调把训练状态送到界面而不是在训练脚本里 import 界面最难改的地方就是这里如果训练函数里直接引用 QMainWindow训练脚本立刻变成“只能在图形界面里跑”的死代码。正确做法是定义回调接口from dataclasses import dataclass dataclass class TrainingState: epoch: int train_loss: float train_acc: float val_acc: float best_acc: float训练循环里在每轮 epoch 结束时调用on_epoch_end(TrainingState(...))。图形界面侧只需要提供一个适配器函数把 TrainingState 转换为 Qt 信号的数据命令行训练时则传入一个打印函数。这样训练脚本既可以被python train.py直接执行也可以被界面线程带着跑两条路径共用同一套核心代码。5. 用 PySide6 串起测试、推理与 ONNX 导出验证5.1 训练页的 QThread 与信号槽最小写法界面侧最核心的是 QThread 里跑训练通过信号更新进度条难度在于信号的参数不能是自定义 dataclass需要用基本类型或字典。这里给出最小骨架from PySide6.QtCore import QThread, Signal class TrainWorker(QThread): progress Signal(dict) def __init__(self, cfg): super().__init__() self.cfg cfg def run(self): def on_epoch_end(epoch, loss, acc, val_acc, best_acc): self.progress.emit({ epoch: epoch, loss: loss, train_acc: acc, val_acc: val_acc, best_acc: best_acc }) train_model(self.model, self.train_loader, self.val_loader, self.cfg, on_epoch_endon_epoch_end)界面里把progress信号连到槽函数槽函数更新 QProgressBar 和 loss 曲线即可。注意训练循环里要定期检查线程是否被取消可以在每个 epoch 开头判断if self.isInterruptionRequested(): torch.save(...); return并用requestInterruption()而不是强行 terminate否则权重文件会损坏。5.2 导出 ONNX 并用 onnxruntime 做输出一致性验证图形界面或后端服务更希望脱离 PyTorch 环境做推理常见做法是导出 ONNX。最后的验证技巧是固定一张图对比 PyTorch 与 onnxruntime 的 top-5 输出差异model.eval() dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export(model, dummy_input, model.onnx, input_names[input], output_names[logits], dynamic_axes{input: {0: batch}, logits: {0: batch}})import onnxruntime as ort import numpy as np sess ort.InferenceSession(model.onnx, providers[CPUExecutionProvider]) torch_logits model(dummy_input).detach().numpy() ort_logits sess.run([logits], {input: dummy_input.numpy()})[0] diff np.abs(torch_logits - ort_logits).max() print(最大输出差异:, diff)验证逻辑说明dynamic_axes允许推理时 batch 不为 1界面里单张图片推理时 batch1 也没关系比较的是 logits 而不用过 softmax因为 softmax 是单调映射最大差异出现在 logits 空间最直接。这个 diff 通常小于1e-4。界面推理页直接加载 onnx 文件即可与训练时使用的 CUDA 版本、PyTorch 环境彻底解耦即使换一台没装深度学习框架的机器也能跑。本文还有配套的精品资源点击获取