简介:面向高校计算机与人工智能方向毕业设计的一套完整项目源码,围绕基于Python与YOLOv8的垃圾分类目标检测系统展开,包含PyQt5界面与MySQL数据库,适合需要完成毕业设计、课程设计或进行深度学习项目实战的学生参考。压缩包共2000个文件、约229.28MB,其中1996个为txt数据与标注文本,另含2个md说明文档、1个yaml数据集配置文件和1份docx配套论文,链路覆盖从数据准备、模型训练到界面交互,已有108人学习下载。内容涉及多类别垃圾数据的采集标注与增强、C2f模块与动态检测头的结构改进、迁移学习与超参数调优,以及数据预处理、YOLOv8推理、界面交互与结果输出这四个模块的划分,界面支持图像上传、摄像头实时检测与分类统计。读者可据此复现mAP@0.5约94.2%、单帧60FPS的检测流程,理解PyTorch与PyQt5的工程组织方式,并在此基础上做轻量化或边缘部署的二次开发。
1. 从一只没洗的奶茶杯说起:这套系统到底在解决什么
一杯没洗干净的奶茶杯该扔进哪个桶,是这套系统最初的起点。做 python毕业设计 时选垃圾分类检测这个题目,很多人的第一反应是“拿 YOLO 跑个开源数据集就行”,但真跑起来才会发现:类别体系怎么定、置信度低的时候怎么给结论、多个检测框叠在一起时怎么输出一条投放建议,才是决定这套设计能不能立住的东西。这套基于 Python 的 YOLO 垃圾分类目标检测系统,做的是“图片进、类别与投放建议出”这件具体的事:单张识别、批量识别、摄像头实时三路入口,Python 后端服务,前端结果可视化,外加一份可直接导入的 sql 脚本,把每次检测沉淀成可统计的记录。它适合两类人:正在找 python毕业设计 选题、需要一个既能演示又能讲清原理的完整项目;以及想把检测模型接进小型管理系统的开发者。
2. 需求与技术选型:为什么最终落在 YOLO 这条线上
毕设选题最容易翻车的地方,不是模型不够新,而是需求边界没划清。开头就把“识别什么、输出什么、多快出结果”这三件事写死,后面的数据集、模型、接口才有依据可循,否则做到一半改类别体系,标注全废,这是血泪经验。
2.1 功能需求拆解:一张照片进来要吐出什么
把需求拆成“输入—处理—输出”三段来看,这套系统的功能边界其实很清晰。
输入端支持三种形态:本地上传单张图片、一次拖入多张图片做批量检测、调用本机摄像头做连续帧检测。三种形态在服务端走的是同一条推理链路,区别只在调用频率和返回方式,批量走同步循环,摄像头走定时抓帧,这样接口层不用写三套逻辑。
处理端要做的事情比想象中多。检测模型输出的是“类别 + 置信度 + 边界框”三元组,但用户要的是“这东西属于哪一类、该扔哪个桶”。中间需要一层类别映射:检测模型识别的是细分品类(塑料瓶、纸箱、电池、菜叶、陶瓷碎片等几十个细类),映射表再把这些细类归到可回收物、厨余垃圾、有害垃圾、其他垃圾四个大类上。这一层是整套设计里最容易被忽略、但在答辩时最容易被追问的地方——导师通常会问“你的模型识别的是瓶子,怎么给出‘可回收物’这个结论”,答案就在这张映射表上。
输出端除了类别和大类,还要给三样东西:置信度、检测框坐标、投放提示文案。置信度低于阈值的样本不硬给结论,统一返回“无法确定,请人工确认”,这比强行输出一个错误类别要负责得多。投放提示文案从数据库的类别字典表里读,方便后期改文案不用动代码。
非功能需求同样要提前写进开题报告:单张图片在纯 CPU 环境下推理耗时控制在 300 毫秒以内,模型权重文件控制在 50MB 以内,整套系统可以完全离线运行,不依赖任何在线接口。这三条决定了后面选模型时的取舍方向。
2.2 技术选型对比:两阶段检测、YOLO 系列与轻量骨干
目标检测这条路线上,主流方案分两派。两阶段检测器(以 Faster R-CNN 为代表)先出候选区域再分类,精度扎实但推理慢,一张图几百毫秒到一秒不等,做摄像头实时基本没戏,而且模型体积大,塞进一个毕设项目里显得头重脚轻。单阶段检测器把定位和分类合成一步,速度优势明显,YOLO 系列是这一派里工程化最成熟的一支。
在 YOLO 内部还要再选一次。现在主流是两条线:v5 系和 v8 系。v5 系的特点是资料多、配置文件直观、改网络结构门槛低,适合需要在论文里讲清楚“我改了什么”的场合;v8 系把训练、验证、导出整合成一套 API,写代码量少,但黑盒程度高一些,答辩时被问到细节容易卡壳。我的建议是:如果毕设的重点在“系统完整度”和“业务落地”,选 v5 系更稳,配置文件看得见摸得着;如果重点是“模型对比实验”,v8 系能省下大量胶水代码,把时间留给消融实验。
骨干网络用 CSPDarknet 加 PANet Neck 这套组合就够了,不要一上来就想着换骨干。轻量化版本(n 或 s 尺度)在垃圾分类这种类别区分度较高的任务上,精度损失通常在 2 到 4 个 mAP 点,但参数量能压到 300 万级别,权重文件 15MB 上下,CPU 推理也能跑到实时边缘,对毕设演示环境极其友好——你没法保证答辩机房里有一块像样的显卡。
推理框架这块,训练用 PyTorch,部署导出 ONNX 再用 onnxruntime 跑,是踩坑最少的一条路。有人会想直接上 TensorRT,速度快是快,但环境依赖重,换台机器就跑不起来,答辩当天出问题没有后悔药。
2.3 数据集与标注:类别体系是第一个要定死的东西
数据集这件事,公开数据集和自己标注各有取舍。公开的垃圾分类数据集类别数往往和你的需求对不上,且场景单一(多为白底摆拍),模型学出来在真实环境下泛化很差。比较务实的做法是:以公开数据集打底保证基础量,再自己补拍 1000 到 2000 张真实场景图,涵盖室内光线、桌面杂乱背景、多物体同框、部分遮挡这几种情况。
标注规范必须写进文档,而且要写成可执行的条款,不能只写“标注要准确”这种废话:
- 边界框贴合目标外轮廓,留白不超过 5 个像素,也不能切掉目标边缘。
- 遮挡面积超过 70% 的目标不标,30% 到 70% 之间按可见部分标。
- 同一物理目标被切成两半的情况(比如半个西瓜),合并成一个框。
- 小目标(边长小于 32 像素)不单独标,避免引入大量低质量样本。
- 类别名统一用小写英文加下划线,中文只出现在映射表里,防止训练时编码出问题。
数量上,每个细类至少 300 个实例,总数落在 3000 到 8000 张之间比较合适。低于 3000 张,长尾类别会严重欠拟合;高于 8000 张,单卡训练时间会拖到十几个小时,毕设周期内调参轮次不够。
划分训练集、验证集、测试集按 8:1:1,但有个细节必须注意:要按“拍摄场景”划分,而不是随机打散。同一张桌子、同一批物品拍出来的照片如果被分到训练集和测试集两边,测试指标会虚高十几个点,答辩演示时换个环境立刻原形毕露。这是我在类似项目里见过最多的自欺欺人。
3. 总体架构设计:五层结构与四张表的职责划分
架构设计这一章解决的是“代码怎么组织”的问题。毕设项目最容易写成一个大脚本,训练、推理、界面全塞在一个文件里,改一行动全身。分层不是为了好看,是为了让每一层都能单独调试、单独演示。
3.1 分层架构:从采集层到展示层的五层结构
整套系统自上而下分五层,每层只和相邻层打交道。
采集层负责拿到原始图像。三个入口分别是 Web 上传、批量文件夹读取、摄像头帧捕获。这一层的唯一职责是把各种来源的图片统一成 numpy 数组或临时文件路径,不做任何业务判断。
预处理层做尺寸归一、通道转换、均值方差归一化。检测模型的输入尺寸固定为 640×640,但原始图片比例各异,这里用 letterbox 方式缩放并填充灰边,保证目标不变形。这个细节很多初次做检测项目的人会忽略,直接用 resize 拉伸,结果模型对细长目标(比如筷子、瓶子)的定位一直不准。
推理层封装模型加载和单次前向计算。模型在服务启动时加载一次,常驻内存,不要每次请求都重新 load 权重——这是新手最常见的性能事故,一次加载几百毫秒,请求一多直接卡死。
业务层承担三件事:置信度过滤与非极大值抑制后的结果整理、细类到大类的映射、投放建议文案拼装。这一层是纯 Python 逻辑,不碰模型,单独写单元测试很容易。
展示层是 Web 前端,负责上传控件、结果图片渲染(在原图上画框)、统计图表。前后端用 JSON 交互,返回结构里带上原图尺寸和每个框的归一化坐标,前端按比例还原,这样后端不需要生成带框的图片文件,省掉一堆临时文件清理的麻烦。
数据流是单向的:采集 → 预处理 → 推理 → 业务 → 展示,任何一层都不回头调用上层。这条约束听起来教条,但它能避免循环依赖,也让每个模块可以单独拿出来做演示。
3.2 数据库设计:四张表撑起检测记录与统计口径
标题里提到的 sql 脚本,核心就是四张表。设计原则是:类别体系可配置、检测记录可追溯、统计口径统一。
-- 类别字典表:细类与大类的映射关系,改文案不用动代码 CREATE TABLE `t_category` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `class_key` VARCHAR(64) NOT NULL COMMENT '模型输出的英文类别名,如 plastic_bottle', `class_cn` VARCHAR(64) NOT NULL COMMENT '细类中文名,如 塑料瓶', `major_id` INT NOT NULL COMMENT '所属大类ID,关联 t_major', `tip` VARCHAR(255) DEFAULT NULL COMMENT '投放提示文案', UNIQUE KEY `uk_class_key` (`class_key`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 大类表:可回收物/厨余垃圾/有害垃圾/其他垃圾 CREATE TABLE `t_major` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `major_cn` VARCHAR(32) NOT NULL COMMENT '大类名称', `color` VARCHAR(16) DEFAULT NULL COMMENT '前端展示色值,如 #2E8B57' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 检测记录主表:一次请求一条,批量检测按图片条数拆开存 CREATE TABLE `t_detect_record` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `img_name` VARCHAR(255) NOT NULL COMMENT '原始文件名', `img_path` VARCHAR(500) DEFAULT NULL COMMENT '存储相对路径', `obj_count` INT NOT NULL DEFAULT 0 COMMENT '本图检出目标数', `cost_ms` INT DEFAULT NULL COMMENT '推理耗时(毫秒)', `source_type` TINYINT NOT NULL DEFAULT 1 COMMENT '1上传 2批量 3摄像头', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY `idx_create_time` (`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 检测明细表:一个框一条,支撑后续的类别分布统计 CREATE TABLE `t_detect_item` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `record_id` BIGINT NOT NULL, `class_key` VARCHAR(64) NOT NULL, `confidence` DECIMAL(5,4) NOT NULL COMMENT '置信度,0~1', `bbox_x` INT NOT NULL COMMENT '左上角x', `bbox_y` INT NOT NULL, `bbox_w` INT NOT NULL, `bbox_h` INT NOT NULL, KEY `idx_record` (`record_id`), KEY `idx_class` (`class_key`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;四张表的分工要说清楚。t_major和t_category是配置表,初始化时用 INSERT 灌入几十条映射数据,运行期只读。t_detect_record和t_detect_item是一对多的业务表,主表记一次请求的概况,明细表记每个检测框。
这里有个设计选择值得讲:为什么把检测框拆到明细表,而不是在主表里存一个 JSON 字段。原因是统计需求——“过去一周各类垃圾的检出次数排行”这种查询,如果框信息塞在 JSON 里,要么全表扫出来在 Python 里算,要么用数据库的 JSON 函数,两种写法都不如一条 GROUP BY 干脆。毕设答辩时展示统计图表,现场跑一条聚合查询出结果,说服力比放一张事先截好的图强得多。
置信度字段用 DECIMAL(5,4) 而不是 FLOAT,是为了排序和比较时不出现浮点误差导致的重复值,虽然实际影响很小,但这种细节在文档里写出来是加分的。
3.3 目录结构约定:让审阅的人三分钟看懂
目录结构不用花哨,按职责分文件夹即可,关键是把训练代码和业务代码彻底隔开:
project/ ├── configs/ # 模型与训练配置文件 │ └── garbage.yaml # 数据集路径、类别数、锚框 ├── data/ │ ├── images/ # 原图,按 train/val/test 分 │ ├── labels/ # YOLO 格式 txt 标注 │ └── dataset.yaml ├── models/ # 网络结构定义 ├── weights/ # 预训练权重与训练产出 ├── train.py # 训练入口 ├── val.py # 评估入口 ├── export.py # 导出 ONNX ├── service/ │ ├── app.py # Flask 入口 │ ├── detector.py # 推理封装 │ ├── mapper.py # 细类到大类映射 │ └── templates/ # 前端页面 ├── sql/ │ └── init.sql # 建表 + 初始数据 └── requirements.txtweights/和data/要加进.gitignore,不然仓库体积会失控。配置文件单独放configs/而不是散落在脚本里,是为了做对比实验时能快速切换,答辩演示“改一个类别数就能跑新数据集”这种操作会很加分。
4. 核心模块实现:数据加载、训练调参与推理服务
这一章落到能跑的代码上。所有代码按“能直接复制进项目改路径就跑”的标准写,注释说明关键参数为什么这么设。
4.1 数据加载与增强:把小数据集用出大数据集的效果
数据集配置文件是训练的入口,格式很固定:
# data/dataset.yaml path: ./data # 数据集根目录 train: images/train # 训练集图片相对路径 val: images/val test: images/test nc: 24 # 类别数,必须和标注文件里的 class_id 上限一致 names: # 顺序即 class_id,改动会导致标注错位,务必谨慎 0: plastic_bottle 1: glass_bottle 2: paper_box 3: battery # ... 其余类别省略nc和names是整套代码里最不能出错的两行。标注文件里写的是数字 id,模型按 id 输出,一旦 names 的顺序和标注时的顺序对不上,训练照样能收敛,但预测结果会整体错位——这种 bug 不看混淆矩阵很难发现。我的做法是把类别表单独存一份 JSON,标注脚本和 dataset.yaml 都从它生成,杜绝手写错位。
数据增强这块,垃圾分类任务有两个专属要点。第一,颜色增强要克制。翻转、缩放、马赛克都没问题,但 HSV 的饱和度扰动幅度不能太大,因为塑料瓶的透明感、纸箱的牛皮纸色、厨余的深色,本身就是强特征,把颜色改狠了等于把特征抹掉。第二,小目标密集场景(比如一桌子的外卖盒)要用 mosaic 增强,让模型习惯多目标同框。增强参数一般在配置文件里改hsv_s、hsv_v、mosaic、mixup这四项,mixup建议关掉或设很低,检测任务里 mixup 容易造出物理上不存在的半透明叠加目标。
自定义 Dataset 时要注意一个坑:__getitem__里读图用的是 OpenCV,读出来是 BGR 顺序,而模型期望 RGB。如果用的是官方封装好的 Dataset 类,内部已经处理了;自己写的话必须手动cv2.cvtColor(img, cv2.COLOR_BGR2RGB),否则训练损失能降下去,但预测时颜色全部反向,精度掉得莫名其妙。
4.2 训练配置与超参:把随机性关进笼子
训练脚本不用自己写,官方仓库的train.py足够用,关键是参数怎么给:
python train.py \ --img 640 \ --batch 16 \ --epochs 120 \ --data data/dataset.yaml \ --cfg models/yolov5s-garbage.yaml \ --weights weights/pretrained.pt \ --name exp_garbage_v1 \ --cache \ --seed 42逐个说。--img 640是输入分辨率,显卡显存小于 8G 就降到 512 或 416,宁可牺牲一点精度也要保证 batch 能跑起来,batch size 太小批归一化统计量会很不稳定。--batch 16是显存和收敛速度的平衡点,显存富余可以到 32。--epochs 120是经验值,垃圾分类这种 20 类左右的任务,80 到 150 轮之间基本能看到 mAP 曲线走平,轮数再多就开始过拟合。--weights一定要加载预训练权重,从零训练的收敛速度和最终精度完全不是一个量级,这是省时间最有效的一步。--cache把图片缓存到内存,数据集在 8000 张以内时能显著加快数据加载,前提是内存够。--seed 42固定随机种子,保证每次训练结果可复现,写论文时这一条很重要。
训练过程中要盯三条曲线:box_loss、cls_loss、obj_loss。正常情况下三条都单调下降并逐渐趋缓。如果cls_loss早早降到很低但 mAP 卡住不动,大概率是类别严重不均衡,某些类样本太少被模型放弃了;如果obj_loss剧烈震荡,多半是学习率给大了,把初始学习率从 0.01 降到 0.001 试一轮。
训练完会在weights/下产出best.pt和last.pt。best.pt是验证集指标最好的那一轮,演示和部署都用它;last.pt是最后一轮,用来做断点续训。这两个不要混用,有人拿last.pt去答辩,结果指标比论文里写的低好几个点,现场很尴尬。
4.3 推理封装与服务接口:让模型变成可调用的服务
推理封装的核心是“只加载一次模型”,并且把后处理逻辑收拢在一个函数里:
# service/detector.py import cv2 import numpy as np from ultralytics import YOLO # 或 torch.hub.load,取决于所用版本 class GarbageDetector: def __init__(self, weight_path, conf_thres=0.35, iou_thres=0.45): # 模型常驻内存,服务启动时加载一次 self.model = YOLO(weight_path) self.conf_thres = conf_thres # 置信度阈值,低于此值不输出结论 self.iou_thres = iou_thres # NMS 的 IoU 阈值,控制重叠框合并力度 def detect(self, img_bgr): # 统一转 RGB,避免通道顺序导致的精度异常 img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) results = self.model.predict( source=img_rgb, conf=self.conf_thres, iou=self.iou_thres, verbose=False )[0] boxes, confs, cls_ids = [], [], [] for box in results.boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() boxes.append([int(x1), int(y1), int(x2 - x1), int(y2 - y1)]) confs.append(float(box.conf[0])) cls_ids.append(int(box.cls[0])) return { "count": len(boxes), "boxes": boxes, "confs": confs, "cls_ids": cls_ids }三个参数的含义要说清楚。conf_thres设 0.35 而不是常见的 0.25,是因为垃圾分类里“看不清就别说”比“猜一个”更重要,阈值低会引入大量误检,演示时框一堆错的东西反而掉分。iou_thres控制 NMS 力度,0.45 是通用值,如果发现同一物体被框了两次,往下调到 0.35;如果相邻的两个物体只剩一个框,往上调到 0.55。img_bgr保持 OpenCV 原始格式入参,在函数内部转换,这样调用方不用关心通道顺序,减少出错点。
服务层用 Flask 包一层,接口设计成两个:单图检测和批量检测。返回体统一成{code, msg, data}结构,data里带原图宽高和每个框的归一化坐标,前端按比例还原绘制,后端不生成图片文件。
# service/app.py 关键片段 @app.route("/api/detect", methods=["POST"]) def api_detect(): file = request.files.get("file") if not file: return jsonify({"code": 400, "msg": "未收到图片", "data": None}) img = cv2.imdecode(np.frombuffer(file.read(), np.uint8), cv2.IMREAD_COLOR) h, w = img.shape[:2] t0 = time.time() raw = detector.detect(img) cost_ms = int((time.time() - t0) * 1000) items = mapper.to_business(raw, w, h) # 映射大类 + 拼投放提示 save_record(file.filename, items, cost_ms) # 落库,失败不影响返回 return jsonify({"code": 200, "msg": "ok", "data": {"width": w, "height": h, "cost_ms": cost_ms, "items": items}})这里有个必须做的容错:落库操作要放在 try 块里,数据库连不上时接口依然要正常返回检测结果。答辩现场数据库服务没起来是很常见的事故,接口直接 500 就等于演示失败,而检测结果能出来、只是统计页打不开,那还救得回来。
mapper.to_business这一层是纯查表逻辑,输入模型输出的 class_id,输出大类名称、颜色、投放提示。类别字典在服务启动时一次性读进内存做成 dict,不要每次请求都查库。
4.4 前端展示与结果渲染:把检测框画在用户看得懂的位置
前端不追求花哨,三块内容:上传区、结果画布、统计面板。
结果画布用 Canvas 绘制,后端返回归一化坐标(除以原图宽高得到 0 到 1 之间的值),前端乘以画布的显示尺寸,这样无论图片原始分辨率多大、画布缩放多少,框都能对准。用归一化坐标而不是绝对像素,是为了适配移动端和不同尺寸的展示容器,这个设计在做响应式页面时能省掉大量重算逻辑。
每个框上方标注“细类名 + 置信度百分比”,右侧结果列表按大类分组显示,每组用大类对应的颜色做左边框。同一张大类下的多个目标合并成一条“可回收物 × 3”,比列三条重复信息更清楚。
统计面板从/api/stats拉数据,返回最近七天的类别分布和每日检测次数,用柱状图渲染。这块的数据直接来自t_detect_item的 GROUP BY 查询,不做缓存,因为量小,实时算反而更简单。
5. 避坑与排查:训练不收敛、类别混淆与部署翻车的高频现场
这一章是按“现象 → 原因 → 解决”整理的高频问题清单,每一条都是在类似项目里真实出现过的,写在这里省下的是几天的试错时间。
5.1 训练阶段的三个坑
现象一:训练跑了三十轮,cls_loss一直在 2.0 上下震荡,降不下去。
原因大概率是标注里存在类别 id 越界或空标注文件。YOLO 格式的 txt 标注,每行是class_id x_center y_center width height,全部归一化。如果某个 txt 里写了class_id=24而nc=24(合法范围是 0 到 23),训练脚本不会报错,但这一行会被静默丢弃或产生异常梯度。空 txt 文件(图片里没有目标)是合法的,表示背景样本,但如果整个训练集里空文件占比超过 30%,模型会倾向于“什么都不预测”,表现为obj_loss很低但 mAP 接近零。
解决方式很直接:写一个校验脚本,遍历所有标注文件,统计每个 class_id 的出现次数,输出成表格。第一列是类别 id,第二列是实例数。任何一类的实例数低于 50,就要么补数据,要么从类别体系里删掉。这一步在开始训练前做,比训练两天后发现问题要划算得多。
现象二:验证集 mAP 到 0.75 就不动了,但训练集 mAP 已经 0.95,明显过拟合。
先别急着加正则。垃圾分类任务里,过拟合最常见的真实原因是数据增强太弱加上场景单一。所有训练图都在同一张桌子上拍,模型学到的是“这张桌子的纹理”而不是“瓶子的形状”。检查方法是把验证集的图片拿出来肉眼过一遍,看背景是否和训练集高度相似。
解决办法分两步。短期靠增强,把 mosaic 概率提到 0.8、缩放范围放宽到 0.5 到 1.5、加随机透视变换,让模型没法依赖固定背景。长期靠补数据,专门去不同光线、不同桌面、不同角度补拍,宁可总量少一点也要场景多样。两条一起上,一般两三轮就能看到验证集指标重新往上走。
现象三:某一类(比如“电池”)的检测精度始终很低,召回率不到 0.4。
这是典型的长尾问题。电池在真实照片里通常很小、容易被其他物体压住、外观还和口香糖盒、打火机撞脸。先看混淆矩阵,确认它被误判成了哪一类。如果主要被误判成“其他垃圾”里的小物件,说明特征区分度不够;如果大量漏检,说明小目标检测能力不足。
对策是分层处理。数据层面,对这类小目标做过采样,把包含电池的图片复制两到三份(注意每份要配不同的增强参数,不能完全一样,否则等于变相加权但没增加信息)。模型层面,把输入分辨率从 640 提到 800,小目标的特征响应会明显改善,代价是推理速度下降约 40%。这两个手段选一个用,别同时上,否则训练时间会失控。
5.2 推理与部署阶段的两个坑
现象四:训练时验证集 mAP 0.82,部署到服务里用同一批图测试,精度掉到 0.6 左右。
九成是预处理不一致。训练时的流程是:读图 → letterbox 缩放到 640 → 归一化 → 转 RGB。部署时如果直接 resize 到 640 而没有保持长宽比,或者忘了 BGR 转 RGB,模型看到的就是和训练分布不同的输入。还有一种隐蔽情况:训练时用了 mosaic 增强,验证时也误开了增强开关,导致验证指标虚高。
排查方法是对比法。从验证集里挑一张图,分别用训练脚本的 val 流程和服务的 detect 流程跑一遍,把两次的输入张量打印出来做逐元素比对,差值大于 1e-3 就说明预处理有分歧。定位到具体环节后统一到一份预处理代码里,训练和服务都调它,别再各写一遍。
现象五:本地跑得好好的,换到答辩用的笔记本上,模型加载报错或者推理要三四秒一张。
原因通常有三个。一是权重文件路径写成了绝对路径,换机器后找不到;二是环境里装的是 GPU 版本的框架,但答辩机没有显卡,回退到 CPU 时没做配置;三是没有导出成轻量格式,PyTorch 原生权重加载本身就慢。
解决办法:路径全部改成基于项目根目录的相对路径,用os.path.join拼接;在代码里显式判断设备,无 GPU 时自动切 CPU 并打印一行提示,不要让它静默切换;提前导出 ONNX 版本作为备用,服务启动时优先加载 ONNX,加载失败再回退到原生权重。导出命令就一行python export.py --weights weights/best.pt --include onnx --opset 12,导出的模型在纯 CPU 上跑 640 分辨率通常能压到 200 毫秒以内。
提示:答辩前一天一定要在一台从没装过项目环境的机器上完整走一遍“解压 → 装依赖 → 启动 → 检测”的流程,把缺的包和错的路径列出来补掉。这一步花半小时,能避免当天最难受的场面。
6. 验收与答辩:怎么证明这套系统真的能用
验收这件事,核心不是“展示功能”,而是“证明指标可信”。答辩老师最常问的三个问题是:你的模型精度到底多少、这个数字是怎么测的、换一批没见过的图片还准不准。前两个靠规范的评估流程,第三个靠现场演示的兜底设计。
评估要用独立的测试集,而且必须是划分时就留出来的那 10%,不能拿验证集凑数。跑一次val.py输出完整指标,重点看四个数:mAP@0.5 反映整体检测能力,垃圾分类这种类别区分度尚可的任务,轻量化模型做到 0.80 以上算合格;mAP@0.5:0.95 更严格,通常在 0.55 到 0.70 之间;各类别的精确率和召回率,用来发现长尾问题;混淆矩阵,用来定位具体哪些类别在互相误判。把这些整理成一张表放进论文,比只写一个总 mAP 更有说服力。
| 指标 | 数值 | 说明 |
|---|---|---|
| mAP@0.5 | 0.84 | 测试集 800 张,IoU 阈值 0.5 |
| mAP@0.5:0.95 | 0.61 | 严格指标,反映定位精度 |
| 精确率 / 召回率 | 0.86 / 0.81 | 置信度阈值 0.35 下统计 |
| 单张推理耗时 | 180 ms | 纯 CPU,640×640 输入,ONNX 运行时 |
| 权重文件体积 | 14.6 MB | 轻量化尺度模型 |
演示环节设计成三段递进。第一段用预置的十张典型图片走一遍,覆盖四个大类、多目标同框、轻微遮挡三种情况,让老师先建立“系统能干什么”的印象。第二段现场上传一张手机拍的图,证明不是只对测试集有效。第三段打开统计页面,展示历史检测记录的类别分布,说明数据链路是通的,不是个只会画框的玩具。
兜底方案要有。现场网络断了、摄像头权限没开、数据库没起来,这几种情况各准备一条退路:摄像头演示提前录一段三十秒的屏,数据库起不来就让接口走内存降级模式,统计页展示预置数据并口头说明。真出事的时候不慌,比技术本身更能体现工程素养。
答辩陈述的时间分配上,我的习惯是需求背景占两成、方案设计和模型选型占三成、演示占三成、指标与不足占两成。“不足”那部分一定要主动讲,比如“细类中的电池因为样本少,召回率只有 0.68,下一步计划补拍并做小目标增强”,主动暴露问题比被追问出来要好得多,也显得你真的理解自己的系统。
从那以后我每次带类似的项目,都会在动手写代码之前先把“验收表”填一遍——指标要测哪几项、演示走哪几步、兜底用什么方案,填不满就说明需求还没想清楚,先别急着开工。这个习惯省下的返工时间,比任何调参技巧都值钱。希望帮到你。
本文还有配套的精品资源,点击获取