开发 YOLO 训练管理平台的 23 个实践教训(系列第 1 篇)
本系列记录一个人从零开发 YOLO 训练管理平台的完整过程。这篇是系列的第 1 篇:不讲架构设计那些"正确废话",只讲真正踩过的坑——很多都是这类系统(数据管理 + 训练调度)的经典 bug,你大概率也会遇到。
先说说这是个什么东西
公司内部缺一个能管数据集、能在线标注、能一键训练的工具。市面上的方案(CVAT、LabelStudio、各种 MLOps 平台)要么太重,要么只管标注不管训练,索性自己写了一个:
- 后端:FastAPI + SQLite,单进程托管 API、前端静态页和图片,不装数据库、不装 nginx
- 前端:Vue 3 + Vite + ECharts,自绘 CSS,不套 UI 库
- 训练:subprocess 调 ultralytics 的
yoloCLI,逐行收日志、解析 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 个实践教训》(本篇)
有问题欢迎评论区交流。