开发 YOLO 训练管理平台的 23 个实践教训

3 阅读9分钟

开发 YOLO 训练管理平台的 23 个实践教训(系列第 1 篇)

本系列记录一个人从零开发 YOLO 训练管理平台的完整过程。这篇是系列的第 1 篇:不讲架构设计那些"正确废话",只讲真正踩过的坑——很多都是这类系统(数据管理 + 训练调度)的经典 bug,你大概率也会遇到。

先说说这是个什么东西

公司内部缺一个能管数据集、能在线标注、能一键训练的工具。市面上的方案(CVAT、LabelStudio、各种 MLOps 平台)要么太重,要么只管标注不管训练,索性自己写了一个:

  • 后端:FastAPI + SQLite,单进程托管 API、前端静态页和图片,不装数据库、不装 nginx
  • 前端:Vue 3 + Vite + ECharts,自绘 CSS,不套 UI 库
  • 训练:subprocess 调 ultralytics 的 yolo CLI,逐行收日志、解析 results.csv 画实时曲线
  • 功能:数据集导入(YOLO/VOC 自动识别)、在线标注、数据集版本快照、串行训练队列、模型版本管理、在线试模型
  • 部署:Windows 优先,做了个图形安装向导,非技术同事双击就能装

整个项目是一个人利用业余时间一点一点堆出来的,下面 23 条全是实战里换来的。

本文目录

  • 一、数据集导入:静默丢数据是最要命的(坑 1~5)
  • 二、标注系统:一致性错了就是灾难(坑 6~8)
  • 三、训练进程管理:坑最密集的地方(坑 9~16)
  • 四、多数据集合并训练:细节全是雷(坑 17~19)
  • 五、Windows 平台:被忽视的重灾区(坑 20~23)
  • 六、工程化的小决定,省了大量麻烦

一、数据集导入:静默丢数据是最要命的

1. 图片和标注文件名匹配,一定要统一小写

图片叫 IMG_001.JPG,标注叫 img_001.txt——在 Windows 上能配上,部署到 Linux 服务器上大小写敏感,静默丢掉全部标注,不报任何错。训练照常跑,只是 mAP 低得离谱,你查三天都想不到是文件名大小写。

# 两边都 .lower() 再匹配,一行代码的事
stem_map = {p.stem.lower(): p for p in image_files}

2. zip 里的中文文件名是 cp437 编码的

Windows 上打的 zip 包,中文文件名按 GBK 存,但 zip 规范里标记位常常没设置,Python 解出来是 cp437 乱码。要做 cp437→GBK 的兜底解码,否则中文名的图片导入后全是乱码文件名,在 Windows 上还可能直接写盘失败。

3. 空标注文件不是错误,是负样本

一张没有任何目标的图片,对应的 txt 就是空文件。早期版本把空文件当"缺失标注"跳过,这批图片就没进训练集——实际上它们是非常重要的背景样本,能显著降低误检。空 txt 要合法保留,训练时写空标签。

4. 脏数据容错:越界、零宽、脏行

外部拿到的数据集质量参差不齐:

  • class id 越界(类别表只有 5 类,标注里出现 7)→ 检查并跳过计数
  • 坐标超出 [0,1] → clamp,别让脏数据进库
  • VOC 转 YOLO 时的零宽框、贴着图片右边界的框 → 转换后校验一遍

导入结束给用户一句"跳过了 N 张损坏图片、M 条异常标注",比什么都重要。

5. 解压 zip 一定拦一下 ../

两三行代码的事,防别人发来的 zip 里藏 ../../ 路径穿越把文件写到系统目录。另外解压要按内容读成员再落盘到自己命名的路径,不要信任 zip 内的原始文件名。


二、标注系统:一致性错了就是灾难

6. 标注存像素坐标,不要存归一化坐标

YOLO 训练用的是归一化坐标(0~1),但数据库里一定要存像素 xywh。原因:标注是要反复编辑的,归一化值每次"像素→归一化→像素"往返都有浮点精度损耗,框会越拖越歪。存储用像素,只在训练导出的最后一刻转归一化。

7. 类别顺序是数据的一部分,一旦确定永远不可重排

这是全项目最值钱的一条教训。标注里存的是 class id(0、1、2……),id 的含义完全由类别表的顺序决定。如果有人在中间插入一个类别或者重新排序,历史标注的 class id 全部错位——猫变成狗,狗变成背景,而且同样是静默出错。

规矩只有一条:新类别只能追加到类别表尾部,前端标注页加类别也一样。想"删除"类别就标记弃用,别动顺序。

8. 多边形标注先转外接矩形

老系统里有齿形零件的多边形标注,新系统一期只支持矩形框。导入时取多边形外接矩形即可,别为了 5% 的场景把标注画布的复杂度翻三倍。


三、训练进程管理:坑最密集的地方

9. yolo 的进度条是用 \r 刷新的,别按行读日志

ultralytics 训练时的进度条 \r 回车刷新,一个 epoch 可能只有一行但刷新了几百次。如果你按 \n 切分读日志,要么读不到进度,要么缓冲区炸掉。按 \r 和 \n 都要切分。

10. stderr 不读,管道会死锁

只读 stdout 不管 stderr 的话,子进程 stderr 缓冲区写满后会阻塞,整个训练卡住不动——表面看像"训练 hang 了"。要么 stderr=subprocess.STDOUT 合并,要么单独开线程读。

11. epoch 进度去数 results.csv 的行数,别解析日志百分比

日志里的百分比是给人看的,格式随 ultralytics 版本说变就变。results.csv 才是结构化数据:一行 = 一个 epoch,行数就是进度。顺便,列名要做模糊匹配(mAP50 前缀匹配),不同版本列名后缀不一样。

12. 日志写文件 + offset 增量读,别存内存 dict

老项目把训练日志存在内存 dict 里,服务一重启日志全丢,任务还在跑但页面一片空白。改成:日志直接写 run_dir/train.log,前端轮询时带 offset 增量拉取,服务重启、刷新页面都不丢。

13. 服务启动时,把残留的 running 任务批量标记为"中断"

服务崩了/机器重启后,数据库里还躺着一堆 status=running 的任务,但进程早死了。不清理的话这些任务永远卡在"运行中",队列也被占死。启动时扫一遍,全部标记 interrupted,配上"续训"按钮(ultralytics 原生支持从 last.pt 恢复),体验直接拉满。

14. 发起训练的"检查 + 启动"必须加锁

两个人同时点"开始训练",检查队列时都看到"空闲",然后同时启动——GPU 直接爆显存。检查和启动要在一个锁里完成。

15. Windows 下 terminate 杀不掉进程树,用 psutil

训练是 spawn 出来的独立进程,yolo 自己还会起 dataloader 子进程。Windows 上 proc.terminate() 经常只杀了壳,训练还在跑。用 psutil 拿到整棵进程树一起杀,Windows/Linux 行为一致。

16. 推理显存不够?try 一把 GPU,失败就转 CPU

别费劲做显存预估和状态判断,直接 try GPU 推理,OOM 异常就 fallback 到 CPU。三行代码,比任何"智能调度"都可靠。


四、多数据集合并训练:细节全是雷

17. 不同数据集图片重名,合并时必须重命名

两个数据集里都有 0001.jpg,拷到同一个训练目录直接互相覆盖。按数据集 id 分子目录,或者统一重命名。

18. label 里的 class id 必须按新类别表重写

数据集 A 的 class 0 是"划痕",数据集 B 的 class 0 是"凹陷",合并训练用统一的类别表后,所有 txt 里的 id 都要重写。漏了这一步,训出来的模型类别完全是乱的——又是静默出错。

19. 合并结果拷到独立 staging 目录再训

直接在数据集目录里拼凑训练数据,会和其他人正在进行的导入任务读写冲突。拷贝到独立的 data/runs/task_<id>/ 再训,任务结束后保留(断点续训还要用)。


五、Windows 平台:被忽视的重灾区

20. 批处理第一行先 chcp 65001

Windows 控制台默认 GBK,Python 输出中文直接乱码。start.bat 第一行切 UTF-8 代码页,一行解决。

21. 全程 pathlib,禁止手拼路径、禁止 os.system

"data" + "/" + name 这种代码在 Windows 上早晚出事。统一 pathlib,命令调用全部用参数列表形式的 subprocess,不经过 shell。

22. 训练输出目录用纯 ASCII 路径

中文用户名 + 中文安装路径 + 深度学习框架,是 Windows 上的经典翻车组合。所有程序自己生成的路径(data/runs/task_123/)保持纯 ASCII,用户数据爱叫什么叫什么。

23. 文件下载 URL 用 id,别用中文文件名

/api/images/123/file 永远不会有编码问题;/api/files/零件照片.jpg 在 Windows + 各种浏览器的组合下迟早乱码。


六、工程化的小决定,省了大量麻烦

最后几条不是 bug,是几个事后看特别正确的小决定:

  • 训练必须引用数据集版本快照,而不是"当前数据"——这样"这个模型到底是哪版标注训出来的"永远可追溯,改完标注旧版本还能回滚查看
  • 被训练任务用过的数据集禁止删除——一行引用检查,防止误删后模型变成"孤儿"
  • best.pt 不存在就把任务标记失败并写明原因——全空标注是训不出模型的,别让任务显示"成功"但库里没有模型
  • 整个 data/ 目录就是全部数据——备份 = 打包它,迁移 = 拷走,没有数据库导出导入那些破事

写在最后

这类"内部 AI 工具"的项目,技术栈都不难,真正的工作量全在上面这些边角细节里。坑的共同特征很明显:不报错、静默出错、重启后状态对不上、换台 Windows 机器就翻车。防它们的办法也不高级——统一约定、写死规则、启动时清理残留状态、所有静默失败的地方都改成"跳过并计数"。

项目代码里的 PLAN.md 攒了一份更长的避坑清单,开发时照着逐条检查,少走了非常多弯路。如果你也在做类似的系统,建议从第一天就维护一份这样的清单。


技术栈:FastAPI · SQLite · Vue 3 · ultralytics(Windows 优先部署)

系列目录

  • 第 1 篇《开发 YOLO 训练管理平台的 23 个实践教训》(本篇)

有问题欢迎评论区交流。