ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

自研 YOLO 训练管理平台的 23 个踩坑教训:不报错、静默丢数据(FastAPI + subprocess)

自研 YOLO 训练管理平台的 23 个踩坑教训:不报错、静默丢数据(FastAPI + subprocess) 自研 YOLO 训练管理平台的 23 个踩坑教训不报错、静默丢数据FastAPI subprocess系列第 1 篇想自己搭一个管数据集 在线标注 一键训练的 YOLO 训练管理平台这篇讲我一个人用 FastAPI SQLite subprocess 调 ultralytics 做完之后真正踩过的 23 个坑——大半是不报错但结果错了的静默 bug查起来极其费时。正在做或打算做同类内部工具的人照着避雷即可。先说说这是个什么东西公司内部缺一个能管数据集、能在线标注、能一键训练的工具。市面上的方案CVAT、LabelStudio、各种 MLOps 平台要么太重要么只管标注不管训练索性自己写了一个后端FastAPI SQLite单进程托管 API、前端静态页和图片不装数据库、不装 nginx前端Vue 3 Vite ECharts自绘 CSS不套 UI 库训练subprocess 调 ultralytics 的yoloCLI逐行收日志、解析 results.csv 画实时曲线功能数据集导入YOLO/VOC 自动识别、在线标注、数据集版本快照、串行训练队列、模型版本管理、在线试模型部署Windows 优先做了个图形安装向导非技术同事双击就能装整个项目是一个人利用业余时间堆出来的后端约 7800 行 Python前端约 9600 行 Vue/JS配了 13 个 pytest 用例。下面 23 条全是实战里换来的每条都对应一个真实修过的 bug 或定下的规矩。本文目录一、数据集导入静默丢数据是最要命的坑 1~5二、标注系统一致性错了就是灾难坑 6~8三、训练进程管理坑最密集的地方坑 9~16四、多数据集合并训练细节全是雷坑 17~19五、Windows 平台被忽视的重灾区坑 20~23六、工程化的小决定省了大量麻烦什么时候这套做法不适用小结 / FAQ一、数据集导入静默丢数据是最要命的1. 图片和标注文件名匹配一定要统一小写图片叫IMG_001.JPG标注叫img_001.txt——在 Windows 上能配上部署到 Linux 服务器上大小写敏感静默丢掉全部标注不报任何错。训练照常跑只是 mAP 低得离谱你查三天都想不到是文件名大小写。# 两边都 .lower() 再匹配一行代码的事stem_map{p.stem.lower():pforpinimage_files}2. zip 里的中文文件名是 cp437 编码的Windows 上打的 zip 包中文文件名按 GBK 存但 zip 规范里的 UTF-8 标记位常常没设置Python 解出来是 cp437 乱码。要做 cp437→GBK 的兜底解码否则中文名的图片导入后全是乱码文件名在 Windows 上还可能直接写盘失败。defdecode_zip_name(raw:str)-str:zip 成员名 cp437→GBK 兜底解码Windows 中文文件名打的包。try:returnraw.encode(cp437).decode(gbk)except(UnicodeEncodeError,UnicodeDecodeError):returnraw3. 空标注文件不是错误是负样本一张没有任何目标的图片对应的 txt 就是空文件。早期版本把空文件当缺失标注跳过这批图片就没进训练集——实际上它们是非常重要的背景样本能显著降低误检。空 txt 要合法保留训练时写空标签。4. 脏数据容错越界、零宽、脏行外部拿到的数据集质量参差不齐class id 越界类别表只有 5 类标注里出现 7→ 检查并跳过计数坐标超出 [0,1] → clamp别让脏数据进库VOC 转 YOLO 时的零宽框、贴着图片右边界的框 → 转换后校验一遍导入结束给用户一句跳过了 N 张损坏图片、M 条异常标注比什么都重要。5. 解压 zip 一定拦一下../防别人发来的 zip 里藏../../路径穿越把文件写到系统目录。判断就是两行frompathlibimportPurePosixPathdefis_evil_name(name:str)-bool:pPurePosixPath(name)returnp.is_absolute()orany(part..forpartinp.parts)另外解压要按内容读成员再落盘到自己命名的路径不要信任 zip 内的原始文件名。二、标注系统一致性错了就是灾难6. 标注存像素坐标不要存归一化坐标YOLO 训练用的是归一化坐标0~1但数据库里一定要存像素 xywh。原因标注是要反复编辑的归一化值每次像素→归一化→像素往返都有浮点精度损耗框会越拖越歪。存储用像素只在训练导出的最后一刻转归一化。7. 类别顺序是数据的一部分一旦确定永远不可重排这是全项目最值钱的一条教训。标注里存的是 class id0、1、2……id 的含义完全由类别表的顺序决定。如果有人在中间插入一个类别或者重新排序历史标注的 class id 全部错位——猫变成狗狗变成背景而且同样是静默出错。规矩只有一条新类别只能追加到类别表尾部前端标注页加类别也一样。想删除类别就标记弃用别动顺序。8. 多边形标注先转外接矩形老系统里有齿形零件的多边形标注新系统一期只支持矩形框。导入时取多边形外接矩形即可别为了 5% 的场景把标注画布的复杂度翻三倍。三、训练进程管理坑最密集的地方9. yolo 的进度条是用\r刷新的别按行读日志ultralytics 训练时的进度条用\r回车刷新一个 epoch 可能只有一行但刷新了几百次。如果你按\n切分读日志要么读不到进度要么缓冲区炸掉。解决办法是逐字符读\r和\n都当分隔符deflog_reader(proc,log_path):逐字符读 stdoutyolo 进度条用 \r按 \r/\n 切分段落写文件。withopen(log_path,a,encodingutf-8,errorsreplace,buffering1)asf:segmentforchiniter(lambda:proc.stdout.read(1),):ifchin\r\n:ifsegment:f.write(segment\n)segmentelse:segmentch10. stderr 不读管道会死锁只读 stdout 不管 stderr 的话子进程 stderr 缓冲区写满后会阻塞整个训练卡住不动——表面看像训练 hang 了。最简单的做法是 Popen 时合并procsubprocess.Popen(cmd,stdoutsubprocess.PIPE,stderrsubprocess.STDOUT,# stderr 合并进 stdout避免管道写满死锁textTrue,encodingutf-8,# 子进程输出含 UTF-8 字符Windows 默认 GBK 解码会崩errorsreplace,bufsize1,)11. epoch 进度去数 results.csv 的行数别解析日志百分比日志里的百分比是给人看的格式随 ultralytics 版本说变就变。results.csv才是结构化数据一行 一个 epoch行数就是进度importcsvdefepoch_count(results_csv)-int:try:withopen(results_csv,encodingutf-8,errorsreplace)asf:rowslist(csv.reader(f))exceptOSError:return0returnsum(1forrowinrows[1:]ifrowandrow[0].strip().isdigit())前端拿到的percent 已完成的行数 * 100 / 总 epoch 数。顺便列名要做模糊匹配mAP50前缀匹配不同 ultralytics 版本的列名后缀不一样。12. 日志写文件 offset 增量读别存内存 dict老项目把训练日志存在内存 dict 里服务一重启日志全丢任务还在跑但页面一片空白。改成日志直接写run_dir/train.log前端轮询时带 offset 增量拉取服务重启、刷新页面都不丢。13. 服务启动时把残留的 running 任务批量标记为中断服务崩了/机器重启后数据库里还躺着一堆statusrunning的任务但进程早死了。不清理的话这些任务永远卡在运行中队列也被占死。启动时扫一遍全部标记 interrupted配上续训按钮ultralytics 原生支持从 last.pt 恢复体验直接拉满。14. 发起训练的检查 启动必须加锁两个人同时点开始训练检查队列时都看到空闲然后同时启动——GPU 直接爆显存。检查和启动要在一个锁里完成。15. Windows 下 terminate 杀不掉进程树用 psutil训练是 spawn 出来的独立进程yolo 自己还会起 dataloader 子进程。Windows 上proc.terminate()经常只杀了壳训练还在跑。用 psutil 拿到整棵进程树一起杀Windows/Linux 行为一致importpsutildefkill_tree(pid:int):try:procpsutil.Process(pid)exceptpsutil.NoSuchProcess:returnprocs[proc]proc.children(recursiveTrue)forpinprocs:try:p.terminate()exceptpsutil.Error:pass_,alivepsutil.wait_procs(procs,timeout1)forpinalive:# 1 秒还没死的强杀try:p.kill()exceptpsutil.Error:pass16. 推理显存不够try 一把 GPU失败就转 CPU别费劲做显存预估和状态判断直接 try GPU 推理OOM 异常就 fallback 到 CPU。几行代码比任何智能调度都可靠defpredict(model,source,conf):先尝试 GPURuntimeError/OOM 自动 fallback CPU 重试。primary0iftorch.cuda.is_available()elsecputry:returnmodel.predict(sourcesource,confconf,deviceprimary,verboseFalse)exceptRuntimeError:ifprimarycpu:raisereturnmodel.predict(sourcesource,confconf,devicecpu,verboseFalse)四、多数据集合并训练细节全是雷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 65001Windows 控制台默认 GBKPython 输出中文直接乱码。start.bat第一行切 UTF-8 代码页一行解决。21. 全程 pathlib禁止手拼路径、禁止 os.systemdata / 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/ 目录就是全部数据——备份 打包它迁移 拷走没有数据库导出导入那些破事什么时候这套做法不适用上面这套FastAPI 单进程 SQLite subprocess 串行队列是为小团队内部工具设计的有几个明确的失效边界多人同时训练 / 多 GPU 调度串行队列一次只跑一个任务机器多、任务多就得换 Celery/RQ 这类真队列加 worker 池。数据集特别大图片到几十万张级别SQLite 单文件和整个 data/ 目录打包备份都会开始吃力缩略图、分页、存储策略都得重做。强权限和审计要求这套只有简单的登录鉴权没有操作审计、细粒度角色权限过不了企业合规那一关。公网部署单进程托管 本地文件存储是内网信任环境的前提暴露到公网要补的东西太多不如直接上成熟平台。一句话它是几个人、一台带显卡的 Windows 机器、把训练流程跑顺的最省方案不是平台型产品。小结这类系统最大的敌人是静默失败所有跳过的地方都要计数并告诉用户别替用户做决定还不吱声。类别表顺序是数据的一部分只能尾部追加永远不能重排。训练进度看 results.csv 的行数别解析日志里给人看的百分比。服务启动第一件事清理数据库里残留的 running 状态。Windows 部署三关chcp 65001、全程 pathlib、程序自生成路径保持纯 ASCII。FAQQ为什么不直接用 CVAT / LabelStudio它们标注很强但不管训练、不管模型版本部署也重。我们要的是标完点一下就开始训、训完直接在线试的闭环市面上的开源方案拼起来比自己写一个还费劲。QSQLite 单进程真的扛得住吗内部工具、几个人并发绰绰有余而且备份就是拷目录。真有几十人并发或多机部署的需求再换 PostgreSQL 独立 worker 不迟——但那时候你该考虑的已经不是这个架构了。Q训练实时曲线具体怎么画的后端每 2 秒数一次 results.csv 的行数算进度、解析最后一行取 mAP 等指标前端 ECharts 轮询刷新。细节含断点续训和日志增量拉取在系列第 3 篇单独讲。Q没有 GPU 的机器能用吗管理、标注、导入都没问题在线试模型会自动 fallback 到 CPU单张推理能接受CPU 训练只够跑通流程做 smoke test正经训练还是要显卡。Q这套代码开源吗目前是公司内部项目没有开源。但上面所有代码片段都是从真实代码里精简出来的不依赖项目内部结构可以直接抄走改改用。这类内部 AI 工具的项目技术栈都不难真正的工作量全在上面这些边角细节里。回头看这 23 条至少 10 条属于同一个类型不报错、静默出错、重启后状态对不上、换台 Windows 机器就翻车。防它们的办法也不高级——统一约定、写死规则、启动时清理残留状态、所有静默失败的地方都改成跳过并计数。项目里还有一份 221 行的避坑清单PLAN.md开发时照着逐条检查少走了非常多弯路。如果你也在做类似的系统建议从第一天就维护一份这样的清单。技术栈FastAPI · SQLite · Vue 3 · ultralyticsWindows 优先部署本系列共 6 篇系列第 1 篇开发总览——23 个实践教训本篇系列第 2 篇需求设计与技术选型系列第 3 篇subprocess 训练进程管理系列第 4 篇标注数据一致性的 4 个设计系列第 5 篇Windows 双击即用与 PyInstaller 打包系列第 6 篇业余时间做内部工具不烂尾的心得有问题欢迎评论区交流。
返回列表