☰
YOLO垃圾分类检测系统:从数据标注到Flask落库与答辩验收
2026/10/26 3:40:20 网站建设 项目流程

简介:面向计算机视觉方向毕业设计与课程实践,这份内容围绕基于Python与YOLOv8的垃圾分类目标检测系统展开,配套PyQt5可视化界面、MySQL数据库脚本与完整工程源码,适合需要完成检测类课题、复现深度学习项目或准备答辩演示的本科与高职学生参考。包内共2000个文件,以txt文本为主,多为数据集标注与说明类内容,另含md说明、yaml配置文件及一份docx配套论文,压缩包约229.28MB。已有108人学习下载。内容覆盖数据采集标注与增强、YOLOv8迁移学习与超参数优化、PyTorch模型加载推理、PyQt5交互界面、分类统计与结果输出等模块,并给出mAP@0.5约94.2%、单帧约60FPS的实验指标。读者可据此梳理从数据集构建、模型训练到桌面端部署的完整链路,理解模块化目录组织与MySQL数据管理方式,为毕业设计撰写、系统复现和二次开发提供可落地的参考。

1. 从一次答辩现场说起:垃圾分类检测系统要交付的到底是什么

做 python 毕业设计的同学里,选 YOLO 目标检测这条线的特别多,但能把垃圾分类检测从数据集一路做到界面、再在答辩现场稳定跑完一轮的并不多。常见的情况是权重文件有了、脚本也能跑,被追问一句「厨余和其他这两类为什么老混」「这个统计数字是从哪张表查出来的」就接不下去了。

这套方案要解决的不是「模型能不能识图」,而是一条完整链路:一张生活垃圾照片进来,出去的是类别、置信度、框位置和一条可查询的统计记录。适合正在做垃圾分类 YOLO 课设、毕设,或者要给某个投放点做识别演示的人。后面按我实际交付的顺序拆——数据、训练、服务、界面、避坑、验收,每一环都能照着改。

2. 数据集构建与标注规范:从几千张原图攒出一份能训练的四分类标签

模型效果的上限在数据这一层就定死了。我见过太多人把时间全砸在调参上,最后发现标注里同一类物体出现了三种写法,或者验证集里的图和训练集来自同一天同一段视频,指标虚高得离谱。这一章讲三件事:类别体系怎么定、标注怎么清洗、训练集怎么划分才不骗自己。

2.1 先定死类别字典,再决定要不要细分类

垃圾分类的业务口径通常是四分类:可回收物、厨余垃圾、有害垃圾、其他垃圾。这个口径适合做成 YOLO 的四个检测类——注意,是「检测类」,不是「细分材质」。我见过有同学一上来就要分「塑料瓶、玻璃瓶、易拉罐、纸箱、果皮、菜叶、电池、灯管、烟头、纸巾」十类,数据集里每类勉强凑两三百张,训练出来每一类的 mAP 都在 0.4 上下徘徊,答辩时很难看。

我一般的处理是:主模型只做四分类,把细分类放到结果展示层做补充说明。比如检测到某个瓶子,界面上归到「可回收物」,后面的说明文案再写「建议投入可回收物桶」。这样既满足业务口径,又不至于让模型去学一个它学不动的东西。

类别 ID 一旦确定就不要改。YOLO 的标签文件里存的是数字 ID,0 0.53 0.41 0.22 0.31这样一行一个目标。如果你训到一半想插一个新类别到中间,前面所有标签的 ID 全部要重映射,而data.yaml里的names顺序是索引敏感的,错一位就全乱。通常的做法是:类别字典单独写一份classes.json,训练配置和数据清洗脚本都从这份文件读,只有一处源头。

样本量方面,四分类场景下每类不低于 500 张、总样本 2500~4000 张是个比较现实的起点。如果某一类特别少(比如有害垃圾,现实里本来就少见),就先做数据补齐:从公开数据集中筛、在拍摄时专门多拍,而不是靠复制粘贴同一张图凑数——复制出来的样本会让训练集和验证集出现完全相同的图,指标直接是假的。

还有一个容易被忽略的点:负样本。投放点场景里经常出现空桶、墙面、行人、地面污渍,这些不属于四类中任何一类。如果你的数据集里全是干净的目标特写,模型在现场会把任意一个圆形物体都判成瓶子。我一般会掺 5%~10% 的纯背景图(标签文件为空),让模型学会「什么都不检测」也是一种正确输出。

2.2 标注规范与一份真能跑起来的数据清洗脚本

标注工具用什么都行,导出时选 YOLO 格式(每张图一个同名.txt,一行一个目标,格式是class_id x_center y_center width height,坐标全部归一化到 0~1)。真正决定质量的是几条规范,写在项目文档里,标注的人(哪怕只有你自己)必须照着来:

  • 框要贴住目标外轮廓,不要留大片空白,也不要切掉目标的一部分;
  • 目标被遮挡超过一半时不标,低于 1/3 可以标但框住可见部分;
  • 单张图超过 15 个目标的不入训练集,这种图通常是俯拍或堆叠场景,标注成本高且容易漏标;
  • 极小目标(短边小于 12 像素)不标,YOLO 在这个尺度上的召回本来就很低,标了只会拉低 precision;
  • 同一张图里出现的同一类物体,有几个标几个,别只标最显眼的那一个。

标完不要直接拿去训练。归一化坐标越界、宽高为 0、类别 ID 超出字典范围,这三种错误在人工标注里出现频率相当高,而 YOLO 训练时对它们的报错并不总是直观——有时候是训练中途某个 batch 崩掉,报一个和根因毫无关系的维度错误,查起来很痛苦。血泪经验是:先在训练前跑一遍清洗。

import os, json from pathlib import Path # classes.json 是唯一的类别字典来源,形如 {"recyclable": 0, "kitchen": 1, ...} with open("configs/classes.json", encoding="utf-8") as f: CLASSES = json.load(f) VALID_IDS = set(CLASSES.values()) def check_label(txt_path: Path, min_side=12, max_objects=15): """返回 (是否合格, 该图的目标数, 问题描述列表)""" problems = [] lines = [l.strip() for l in txt_path.read_text(encoding="utf-8").splitlines() if l.strip()] if len(lines) > max_objects: problems.append(f"目标数过多:{len(lines)}") if not lines: return True, 0, problems # 空标签是合法的背景样本,保留 for i, line in enumerate(lines, 1): parts = line.split() if len(parts) != 5: problems.append(f"第{i}行字段数={len(parts)}") continue cid = int(float(parts[0])) x, y, w, h = map(float, parts[1:]) if cid not in VALID_IDS: problems.append(f"第{i}行类别越界:{cid}") if not (0 < w <= 1 and 0 < h <= 1): problems.append(f"第{i}行宽高异常:{w:.3f},{h:.3f}") if not (0 <= x - w / 2 and x + w / 2 <= 1 and 0 <= y - h / 2 and y + h / 2 <= 1): problems.append(f"第{i}行框越界") return len(problems) == 0, len(lines), problems def scan_dataset(img_dir, lbl_dir): bad, stats = [], {} for txt in Path(lbl_dir).glob("*.txt"): ok, n, probs = check_label(txt) if not ok: bad.append((txt.name, probs)) stats[n] = stats.get(n, 0) + 1 # 图片和标签必须成对,缺标签的图会被当成背景样本,容易误伤 imgs = {p.stem for p in Path(img_dir).glob("*.jpg")} lbls = {p.stem for p in Path(lbl_dir).glob("*.txt")} orphan_img, orphan_lbl = imgs - lbls, lbls - imgs return bad, stats, orphan_img, orphan_lbl if __name__ == "__main__": bad, stats, oi, ol = scan_dataset("dataset/images/train", "dataset/labels/train") print("异常标签:", len(bad)) for name, probs in bad[:20]: print(" ", name, probs) print("单图目标数分布:", dict(sorted(stats.items()))) print("有图无标签:", len(oi), "有标签无图:", len(ol))

这段脚本做四件事:读唯一类别字典、逐行校验标签、统计单图目标数分布、检查图片和标签的成对关系。min_side和max_objects先用默认值跑一遍看分布,再决定要不要按自己数据集的实际尺度调整。orphan_img那一项尤其要注意——有图无标签在 YOLO 里会被当作纯背景样本参与训练,如果你只是漏标了,等于在教模型「这里面什么都没有」,属于自己给自己下绊子。

跑完之后通常的做法是:把异常标签对应的图片移到一个rejected/目录,人工看一遍再决定补标还是丢弃,不要直接删。丢掉的每一张,后面想找回来都得重新翻原始素材。

2.3 数据配置文件与训练集划分顺序

data.yaml是训练入口,所有路径和类别都在这里声明。写成下面这样:

# configs/trash.yaml path: /data/trash_dataset train: images/train val: images/val test: images/test names: 0: recyclable 1: kitchen 2: hazardous 3: other

path用绝对路径,用相对路径在切换工作目录时极容易翻车,这个坑几乎每届都有人踩。names用英文 key、中文只在界面层映射,原因有两个:一是某些环境下非 ASCII 字符在 yaml 解析和日志输出里会出编码问题,二是模型返回的是 ID,中文名放在前端字典里更灵活,改文案不用重训。

划分比例 8:1:1 是常规做法,但划分方式比比例更重要。绝对不能随机打散——同一个投放点连拍几十张、同一段视频抽帧,这些近乎重复的图如果同时落进训练集和验证集,验证指标会虚高十几个点,等换了场景就原形毕露。我一般按「来源」分组划分:先把所有图按拍摄批次/视频来源分组,再整组分配到 train / val / test,保证同一批次只出现在一个集合里。这样得到的指标虽然低一些,但更接近真实表现。

最后是增强策略。YOLO 默认自带 mosaic、HSV 扰动、随机翻转、缩放。垃圾分类场景里,翻转和 HSV 可以保留(同一个瓶子在不同光线下颜色差异很大,色相扰动很有用),但大角度旋转要慎用——垃圾桶和瓶子在现实中基本是直立的,degrees: 180这种配置会让模型学到一堆现实中不存在的姿态。通常设degrees: 0或不超过 10,flipud: 0(上下翻转在俯拍场景外没有意义),mosaic: 1.0在训练后期关掉(close_mosaic: 10,最后 10 个 epoch 关),能让收敛更稳。

3. 模型训练与调优:从预训练权重到一份敢写进论文的指标

数据定型之后,训练本身反而是最省事的一步——YOLO 系列的默认配置已经足够好,真正花时间的是判断「这次训练到底行不行」。这一章讲训练命令怎么配、曲线怎么读、以及怎么把权重封装成一个别处能直接调用的检测类。

3.1 训练配置:几个真正影响结果的参数

以 YOLOv8 系列为例,一条完整的训练命令长这样:

yolo detect train \ data=configs/trash.yaml \ model=yolov8s.pt \ epochs=120 \ imgsz=640 \ batch=16 \ device=0 \ workers=4 \ patience=30 \ optimizer=SGD \ lr0=0.01 \ lrf=0.01 \ weight_decay=0.0005 \ warmup_epochs=3 \ close_mosaic=10 \ project=runs/trash \ name=v8s_baseline

逐个说清楚,因为答辩被问到的多半就是这些:

model=yolov8s.pt是预训练权重的起点。n / s / m 三档的区别是参数量和推理速度,s 档在 640 输入下落在一张消费级显卡能承受的范围内,精度也够,通常是毕设的默认选择。别一上来选 m 或 l,显存不够的时候调 batch 会调到你怀疑人生。

imgsz=640是训练分辨率,必须和推理时保持一致或成比例。如果改到 416 训练、640 推理,精度会掉得莫名其妙,这种「玄学掉点」有一半是分辨率不匹配造成的。

batch=16取决于显存。8G 显存跑 640 的 s 模型,16 一般能跑;爆了就降到 8,同时把lr0按比例稍微降一点,或者把nbs(名义 batch)固定住让框架自动累积梯度。

patience=30是早停,连续 30 个 epoch 验证指标没提升就停。设这个是因为训练日志里最常见的误判就是「再训一会儿说不定就好了」,结果过拟合到验证集掉点还在训。

lr0=0.01配optimizer=SGD是 YOLO 的经典组合。如果你换AdamW,初始学习率要降到0.001这个量级,直接套 0.01 会震荡到不收敛。lrf=0.01是最终学习率相对于初始值的比例,余弦退到 1%,别设 1.0。

close_mosaic=10前面提过,最后 10 个 epoch 关掉 mosaic 增强,让模型在接近真实的图像分布上收尾。

3.2 训练曲线怎么读:分辨过拟合、欠拟合和「数据本身有问题」

训练跑起来之后,runs/trash/v8s_baseline/下会有results.csv和若干曲线图。四分类、三千张样本、120 epoch,比较健康的结果通常是:mAP50 落在 0.82~0.90 区间,mAP50-95 在 0.60~0.72 之间,precision 和 recall 相差不超过 0.08。

看曲线的时候,我一般按下面这张表对号入座:

曲线表现判断处理方式
train loss 持续下降,val loss 先降后升过拟合加增强、减 epoch、加数据;先查 val 集是否太小
train loss 和 val loss 都很高且不降欠拟合或学习率过大提高 lr 或换 AdamW、检查标签是否加载成功
前 10 epoch mAP 就冲到 0.9 以上数据泄漏查 train/val 是否有同源重复图
box_loss 正常但 cls_loss 居高不下类别混淆看混淆矩阵,通常是某两类语义接近
precision 高、recall 很低置信度阈值偏保守推理时下调 conf,或检查是否有大量漏标

最容易被误判的是第一行和第三行。过拟合的处理不是「加 dropout」——YOLO 里没这个东西——而是加数据、加增强、减容量或提前停。而「前 10 个 epoch 就 0.9」听着像好消息,实际是警报,几乎一定是验证集里混进了训练集的近重复图,回头查 2.3 节的划分方式。

关于混淆:四分类里最容易混的是「厨余垃圾」和「其他垃圾」,比如沾了油的纸盒、带汤汁的餐盒,标注的人自己都会犹豫。这种情况别指望靠调参解决,正确做法是在标注规范里补一条明确的判定规则(我一般写「以是否可回收利用为准,餐盒按其他垃圾处理」),然后回头把这类样本重新过一遍。混淆矩阵里如果某一对类别的错分占了总错误的 30% 以上,那就是标注口径问题,不是模型问题。

3.3 推理封装:把权重变成一个能塞进业务代码的类

训练交付物是best.pt,但业务代码不该直接调用YOLO(...)。中间要有一层封装,负责加载、预处理、后处理、耗时统计和异常兜底。这样换权重、换阈值、换设备都只改一处。

import time import cv2 import numpy as np from ultralytics import YOLO CLASS_CN = {0: "可回收物", 1: "厨余垃圾", 2: "有害垃圾", 3: "其他垃圾"} class TrashDetector: def __init__(self, weights="weights/best.pt", conf=0.35, iou=0.45, device="cpu", imgsz=640): self.model = YOLO(weights) self.conf = conf # 置信度阈值,低于此值的框丢弃 self.iou = iou # NMS 的 IoU 阈值,重叠超过此值的框合并 self.device = device # "cpu" 或 "0"(第一块 GPU) self.imgsz = imgsz # 必须与训练分辨率一致或等比例 def detect_image(self, image_path): t0 = time.time() img = cv2.imread(image_path) if img is None: raise ValueError(f"图片读取失败: {image_path}") result = self.model.predict(img, conf=self.conf, iou=self.iou, imgsz=self.imgsz, device=self.device, verbose=False)[0] items = [] for box in result.boxes: cid = int(box.cls.item()) items.append({ "class_id": cid, "class_name": CLASS_CN.get(cid, "未知"), "conf": round(float(box.conf.item()), 4), "bbox": [round(v, 1) for v in box.xyxy[0].tolist()], # x1,y1,x2,y2 像素 }) items.sort(key=lambda x: x["conf"], reverse=True) return { "items": items, "count": len(items), "summary": self._summary(items), "cost_ms": int((time.time() - t0) * 1000), } @staticmethod def _summary(items): """按类别聚合,界面上的柱状图和入库统计都用这一份结果""" s = {} for it in items: s[it["class_name"]] = s.get(it["class_name"], 0) + 1 return s

三个参数值得单独解释。conf=0.35是交付时的常用值,比训练默认的 0.25 略高一点,用来压掉一批低置信度误检,演示时画面更干净;但如果你更在意召回(比如要统计「有没有垃圾落地」),就该降到 0.2 左右,宁可多报。iou=0.45控制 NMS 的合并强度,同一堆垃圾里紧挨着的两个瓶子,设太高会合并成一个,设太低会重复计数,0.45 是个折中。imgsz必须和训练一致,前面说过,这里再强调一次。

_summary单独抽出来的原因是:界面上的类别统计、入库的统计表、答辩时说的「这一张图里检出 3 个可回收物」,全都来自这一个函数,保证三处数字永远一致。血泪经验是不要让前端和后端各算一遍,一定对不上。

CPU 推理一张 640 的图大概 200~400ms,GPU 上 20~40ms。毕设演示如果跑在笔记本上,提前把device="cpu"的耗时测出来写进文档,答辩时被问「响应速度怎么样」能直接答,比现场卡住强。

4. 检测服务与数据落库:Flask 接口、MySQL 表结构与三种检测入口

到这一步模型已经能用了,剩下的问题是怎么让它变成一个「系统」而不是一个脚本。这一章的内容是 sql 脚本、接口和界面,也是答辩时唯一能体现你做了工程而不只是跑了个模型的部分。

4.1 表结构设计:三张表覆盖账号、记录和统计

sql 脚本至少要包含建库、建表、初始账号和类别字典。三张核心表够了:用户表、检测记录表、类别字典表。检测结果本身不进独立表,因为它是一对多的明细,查询频率低,存成 JSON 字段更划算。

CREATE DATABASE IF NOT EXISTS trash_detect DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE trash_detect; -- 用户表:毕设里通常只需要一个角色,字段别铺太开 CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(128) NOT NULL COMMENT '存哈希,不存明文', role TINYINT NOT NULL DEFAULT 0 COMMENT '0普通用户 1管理员', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; -- 检测记录表:一次检测一行,明细以 JSON 存 CREATE TABLE detect_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, source_type TINYINT NOT NULL COMMENT '1图片 2视频 3摄像头', file_name VARCHAR(255) DEFAULT NULL, total_count INT NOT NULL DEFAULT 0, result_json JSON DEFAULT NULL COMMENT '每个框的类别/置信度/坐标', cost_ms INT DEFAULT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_user_time (user_id, create_time), CONSTRAINT fk_record_user FOREIGN KEY (user_id) REFERENCES sys_user(id) ) ENGINE=InnoDB; -- 类别字典表:中文名和颜色都由前端从这里取,改文案不用动代码 CREATE TABLE class_dict ( class_id INT PRIMARY KEY, class_name VARCHAR(50) NOT NULL, color_hex CHAR(7) NOT NULL DEFAULT '#2E86DE' ) ENGINE=InnoDB; INSERT INTO class_dict VALUES (0,'可回收物','#2E86DE'),(1,'厨余垃圾','#27AE60'), (2,'有害垃圾','#E74C3C'),(3,'其他垃圾','#7F8C8D'); INSERT INTO sys_user (username, password, role) VALUES ('admin', '改为你的哈希值', 1);

几个设计取舍值得说明。detect_record里存result_json而不是拆明细表,是因为检测结果几乎没有按单个框查询的需求,拆表只会增加 join 成本;MySQL 5.7 以上原生支持 JSON 类型,直接存。idx_user_time这个联合索引是给「我的历史记录」页准备的,按用户 + 时间倒序分页,没有它数据上到几千条就会明显变慢。class_dict看起来可有可无,但它是让「中文类别名只在数据库里维护一份」的关键,前端着色、统计图例、文案全都读它,避免了三处硬编码。

密码存哈希,别存明文。这条不是安全问题,是答辩态度问题——被看一眼代码就发现的低级问题,会连累评委对你其他部分的信任。

4.2 检测接口:一条主链路加两处异步兜底

后端用 Flask 就够,不用上 Django。核心接口只有一个/api/detect,接收图片、跑推理、落库、返回结果。

import os, uuid, json, pymysql from flask import Flask, request, jsonify from detector import TrashDetector app = Flask(__name__) det = TrashDetector(weights="weights/best.pt", conf=0.35, device="cpu") UPLOAD_DIR = "static/uploads" os.makedirs(UPLOAD_DIR, exist_ok=True) def get_conn(): return pymysql.connect(host="127.0.0.1", user="root", password="***", database="trash_detect", charset="utf8mb4", cursorclass=pymysql.cursors.DictCursor) @app.post("/api/detect") def detect(): file = request.files.get("image") if file is None: return jsonify(code=400, msg="未收到图片"), 400 # 用 uuid 重命名,避免中文名和同名文件覆盖 ext = os.path.splitext(file.filename)[1].lower() or ".jpg" save_path = os.path.join(UPLOAD_DIR, f"{uuid.uuid4().hex}{ext}") file.save(save_path) try: res = det.detect_image(save_path) except Exception as e: return jsonify(code=500, msg=f"检测失败: {e}"), 500 # 落库失败不应该让整个请求失败,演示时更不能因为数据库挂了就白屏 try: with get_conn() as conn, conn.cursor() as cur: cur.execute( "INSERT INTO detect_record(user_id,source_type,file_name," "total_count,result_json,cost_ms) VALUES(%s,%s,%s,%s,%s,%s)", (1, 1, os.path.basename(save_path), res["count"], json.dumps(res["items"], ensure_ascii=False), res["cost_ms"])) conn.commit() except Exception as e: app.logger.warning("记录写入失败: %s", e) return jsonify(code=200, msg="ok", data={ "image_url": "/" + save_path.replace("\\", "/"), "items": res["items"], "summary": res["summary"], "cost_ms": res["cost_ms"]}) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=False)

关键点在try包住落库这一段。检测成功但写库失败,用户仍然应该看到结果,只是历史记录少一条。很多同学把落库和推理放在一个事务里,数据库一抖动整个接口 500,演示当场白屏——这是典型的把非核心链路当核心链路。debug=False也是必须的,调试模式下 Flask 的重载器会在推理进程里再起一个实例,显存直接翻倍,笔记本上很容易 OOM。

source_type这个字段是给三种检测模式预留的:图片上传、视频文件、摄像头帧。视频和摄像头的处理方式不一样——不能每一帧都跑检测再落库,那会把库撑爆。我一般的做法是抽帧检测(比如每 5 帧取 1 帧),一次「视频检测」任务落一条汇总记录,result_json里放按类别聚合的总数和置信度均值,明细不存。摄像头模式则只在界面上实时画框,用户点「保存快照」时才落一条记录。

4.3 界面三种检测模式与结果展示

界面用 PyQt5 或者 Flask 模板都行,取决于你更想让演示看起来像「桌面软件」还是「Web 系统」。两种我都做过,PyQt5 的好处是不用开浏览器、全屏演示更稳,坏处是摄像头和视频播放要自己处理线程,不然界面会卡死。核心是三块区域:左侧输入(图片/视频/摄像头切换)、中间画布(原图和检测结果切换显示)、右侧结果面板(类别计数、置信度列表、耗时)。

画框的时候,颜色从class_dict表里取color_hex,别在代码里写死一组颜色再去数据库里维护另一组,两处不一致在演示时特别显眼。框上要标类别名 + 置信度,字号随图像尺寸缩放,不然 4K 图上字小到看不见。

结果面板我一般做成两层:上面是四个类别的计数卡片(包括数量为 0 的类别也显示,显示 0 比不显示更有信息量),下面是明细列表,按置信度从高到低排。这个排序不是细节——答辩被问「哪个检测得最准」时,指第一行就够了。

历史记录页只做三件事:按时间倒序列出记录、点开看检测图、按类别筛选。别在上面加编辑和删除,毕设里没人会审这个功能,加了反而多一个出 bug 的地方。

5. 避坑与排查:五个最容易在验收现场翻车的点

这一章是我带过几轮之后攒下来的清单,每一条都真实发生过。按「现象 → 原因 → 解决」写,遇到对应症状的时候可以直接对号入座。

5.1 数据与训练阶段的三个坑

坑一:训练集 mAP 0.9,现场识别一塌糊涂。

现象是results.csv里指标漂亮,换成自己用手机拍的图,检出率不到一半。原因八成是数据泄漏或场景单一——训练集全是网络上下载的白底商品图,而现场是混杂在垃圾桶里的实拍图,域差异太大。解决:在验证集里强制掺入 20% 以上自己实拍的现场图,重新评估。如果现场图的指标掉到 0.5 以下,说明模型学到的是商品外观而不是垃圾特征,需要补拍实景数据重训,光调参救不回来。

坑二:训练跑到第 3 个 epoch 就报显存不足。

现象是CUDA out of memory,但显存看起来明明够。原因通常是workers开太大加上 mosaic 增强同时生成多张图,峰值显存比稳态高不少。解决:把batch减半、workers降到 2、cache=False,还不行就把imgsz从 640 降到 512 试一次。另外注意别同时开两个训练任务,重载器或者没退干净的进程会占着显存不放。

坑三:验证集 loss 正常,但某几类怎么都不收敛。

现象是other这一类 mAP 长期在 0.3 左右徘徊,其他三类都上 0.85。原因通常是这一类的定义本身太模糊——「其他垃圾」是个兜底类别,什么都能往里装,模型学不到共同特征。解决:不要试图让模型学会「其他的共同点」,而是把兜底逻辑挪到后处理:模型只学三个特征明确的类别,剩下的检测框统一归为「其他垃圾」。这样虽然召回口径变了,但指标会立刻正常,而且业务上完全说得通。这个改动我强烈建议在做数据之前就想清楚。

5.2 部署与演示阶段的两个坑

坑四:PyQt 界面点了「开始检测」直接无响应。

现象是窗口变灰、转圈、任务管理器里 CPU 拉满但画面不动。原因是推理跑在主线程里,YOLO 一次前向几百毫秒,UI 事件循环被完全堵死。解决:把推理放进QThread子类,通过信号把结果发回主线程再更新画布;摄像头模式还要额外用一个定时器控制帧率,不要用while True死循环读帧。这个坑几乎是 PyQt 项目的必经之路,早点改比演示前一晚改好。

坑五:换了台电脑,权重加载报错或者结果全乱。

现象是同一个best.pt在开发机上正常,拷到答辩用的机器上类别名全变成英文,或者干脆加载失败。原因是类别名从模型里读、没有和数据库字典对齐;或者打包时漏了weights目录、data.yaml里的绝对路径在新机器上不存在。解决:部署前跑一次自检脚本,检查权重文件存在、class_dict表里 ID 与模型names完全一致、上传目录可写、数据库能连通。这四项目前我每次交付都强制过一遍,能挡掉九成的现场事故。

注意:演示用的机器上提前装好依赖并离线跑通一遍全流程。临时装包遇到网络问题,是最冤的一种翻车。

6. 验收与答辩:把指标、演示和问答串成一条能走完的线

验收这件事,本质上不是证明你的模型多准,而是证明这套东西是可控的、你知道它哪里不行。我一般把验收拆成三块:一份能自查的指标清单、一段五分钟内跑完的演示路径、一组大概率会被问到的问题。

先做自检清单,答辩前一晚逐项打勾:

检查项合格标准怎么查
模型指标mAP50 ≥ 0.80,四类无明显短板看results.csv最后一行,再看混淆矩阵
单图耗时CPU ≤ 500ms,GPU ≤ 100ms界面右下角耗时字段,连续测 10 张取均值
三种模式图片、视频、摄像头各跑通一次现场用的那台机器上跑,不是开发机
数据落库每次检测后记录数 +1检测完刷新历史记录页
异常兜底传非图片文件、断网、库挂掉都不崩手动制造一次失败
类别一致性模型 names、数据库字典、界面文案三处一致逐类对一遍中文名

演示路径要提前排练成固定顺序,五分钟内走完:先上一张四类齐全的图片讲清检出结果,再放一段十几秒的短视频证明能处理连续帧,最后开摄像头做实时演示。顺序不能反——摄像头最容易出意外,放在最后,即使失败也已经把该讲的讲完了。如果摄像头确实不稳,提前录一段屏作为备用,说明「现场光照条件限制,这段是标准环境下的录制结果」,比硬撑着调设备体面得多。

问答环节,四类问题几乎必被问到,提前准备好答案:

「为什么选 YOLO 不选两阶段方法?」——答速度和部署成本,不要答精度,两阶段在精度上通常不输。

「数据集从哪来的,多少张,怎么划的?」——答来源构成、总数、按来源分组划分,重点讲你为什么这么划,这体现你懂数据泄漏。

「某一类识别不准怎么办?」——别答「再训练一下」,答具体动作:补该类实拍样本、查标注口径是否一致、必要时把兜底类挪到后处理。

「系统的完整流程讲一下。」——从上传到返回讲一遍,中间点出哪一步可能失败、你怎么兜的。这一问其实是在考你有没有真的跑通过。

最后一点,也是我自己踩过最狠的一次:答辩前把所有依赖的绝对路径、数据库连接、模型路径全部过一遍,然后在答辩用的那台机器上完整跑一次从登录到检测到查记录的流程。那次我以为拷过去就行,结果data.yaml里写的是开发机的路径,模型加载直接失败,站在台上现改配置,手心全是汗。从那以后我每次交付前都强制走一遍「换机自检」,一个清单六项,跑完才敢合上电脑。

希望这套流程帮到你,少走点弯路。

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

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

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

立即咨询