Files
ModelTrainEval/ml_backend/README.md
T
Shaun ZhouandClaude Opus 5 6e746b7351 feat(scripts): 重训后同时维护 current.pt 与 best.pt
current.pt 指向最新一次训练的结果(后端与 push_predictions 都读它),
best.pt 指向历史上 mAP50 最高的那版,留作回退与对照。

数据集每次重训都在变,这个比较只是粗略参考而非严格的同条件对比,所以只用来
维护 best.pt 这个指针,不拦截 current.pt 的切换。

用服务器上三次真实训练验证过:首次建立、更好时更新、更差时保持不变。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 16:03:00 +08:00

107 lines
5.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Label Studio ML Backend:用训练好的 YOLO 做预标注
把 `runs/detect/<name>/weights/best.pt` 接进 Label Studio,打开任务时自动给出框,
人工只需修正而不是从零画。标注攒够数量还能自动触发重训,形成"边标边训"的闭环。
## 为什么要单独的虚拟环境
`label-studio-ml` 会把 jinja2 降到 3.0.3、带进 Flask 1.1.2,而 label-studio 1.23 需要
jinja2 3.1。装进主环境会直接弄坏 Label Studio。它本来就是个独立服务,单独建环境最省事。
另外 **PyPI 上的 `label-studio-ml` 是 2023 年的 1.0.9,不要用**,要装 GitHub 上的现行版本
(本文档基于 2.0.1.dev0 验证)。
```bash
uv venv .venv-ml --python 3.12
# --no-deps 装本体:它把 label-studio-sdk pin 到了 GitHub 某个提交,
# 那个仓库大、容易传输中断(服务器上实测失败过),依赖全部走 PyPI 更稳
VIRTUAL_ENV=.venv-ml uv pip install --no-deps \
"label-studio-ml @ git+https://github.com/HumanSignal/label-studio-ml-backend.git"
VIRTUAL_ENV=.venv-ml uv pip install \
"flask>2.3" "colorama~=0.4" "requests~=2.31" "semver~=3.0.2" \
"label-studio-sdk>=2.1" ultralytics
```
## 启动
```bash
bash scripts/start_ml_backend.sh
```
脚本会停掉旧进程、带齐环境变量重启,并打印权重、日志位置与网页端要填的 URL。
所有参数都能用环境变量覆盖,例如 `YOLO_WEIGHTS=... bash scripts/start_ml_backend.sh`。
它内部用 `python _wsgi.py` 而不是 `label-studio-ml start .`:后者用 PATH 上的 python
起子进程,在独立环境里会 `ModuleNotFoundError`。
`LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT` 必须和启动 Label Studio 时的值一致——任务里的图片是
`/data/local-files/?d=...`,后端要靠它把 URL 还原成磁盘路径。图片若来自上传或云存储,
则改为设置 `LABEL_STUDIO_URL` 与 `LABEL_STUDIO_API_KEY`,后端会自行下载。
健康检查:
```bash
curl http://localhost:9090/health
# {"model_class":"FilmDefectDetector","status":"UP"}
```
## 接入项目
Label Studio 里:Settings → **Model** → Connect Model,URL 填 `http://localhost:9090`。
- 勾选 **Interactive preannotations** 后,打开任务就会实时调用模型;
- 也可以在 Data Manager 里选中若干任务 → Actions → **Retrieve Predictions** 批量拉取。
**类别名必须和 labeling config 里的 `<Label value="...">` 完全一致。** 后端用的是权重里的
`model.names`(也就是训练时 `data.yaml` 的 names),对不上的话 Label Studio 会拒绝这条预测。
我们的模型是 9 类:black、blackdot、grease、mosquito、normal、obstacle、ripple、uneven、wrinkle。
## 环境变量
| 变量 | 默认 | 说明 |
| --- | --- | --- |
| `YOLO_WEIGHTS` | `yolo26n.pt` | 权重路径。版本号里带它的修改时间,换权重后 Label Studio 能区分新旧预测 |
| `YOLO_CONF` | `0.25` | 置信度阈值。预标注可以调低些,宁可多给几个框让人删 |
| `YOLO_IMGSZ` | `640` | 推理尺寸,与训练保持一致 |
| `YOLO_DEVICE` | `cuda` | `cuda` / `cpu` / `mps` |
| `YOLO_BATCH` | `16` | 一次推理几张。Retrieve Predictions 会一次发来上千个任务,不分批会撑爆显存 |
| `YOLO_TILE_SIZE` | `160` | 模型输入边长。按它切片推理再拼回,全程不缩放;0 = 整图推理(会 letterbox) |
| `YOLO_TILE_OVERLAP` | `32` | 切片重叠像素。大于缺陷尺寸时,总有一片能完整看到它 |
| `RETRAIN_AFTER` | `0` | 累计多少条新标注触发重训,0 = 不触发 |
| `RETRAIN_COMMAND` | 空 | 触发时执行的命令 |
## 边标边训
**在 Label Studio 里点 Settings → Model → Start Training**,后端收到 `START_TRAINING`
事件,起一个子进程跑 `scripts/retrain_from_label_studio.sh`,立刻返回——训练要几十分钟,
绝不能在这个请求里同步做,否则会把 Label Studio 卡住。
触发规则:
- **Start Training 按钮无条件触发**,不看攒了多少条;
- 新增标注累计到 `RETRAIN_AFTER` 也会自动触发(默认 0 = 只手动)。只数
`ANNOTATION_CREATED`,`UPDATED` / `DELETED` 是对已有标注的修改,不该把阈值顶到;
- 已有重训在跑时重复点击会被忽略,不会叠起多个训练。
重训脚本做五件事:导出标注 → `build_yolo_dataset.py` 装配 → `tile_dataset.py` 切成固定
边长的片 → `detect.py` 训练 → 更新软链接并重启本服务。
两个软链接:**`current.pt`** 指向最新一次训练的结果(本服务与 `push_predictions.py` 都读它),
**`best.pt`** 指向历史上 mAP50 最高的那版,留作回退与对照。换模型不用改任何脚本。
**权重只在进程启动时加载一次**,这也是重训末尾要重启服务的原因。
训练进度看 `$RETRAIN_LOG`(默认 `/tmp/retrain.log`);Label Studio 网页端不显示训练进度。
## 已验证的行为
本地用 COCO 预训练权重实测过:
- `/health` 返回 UP;
- `/predict` 用 Label Studio 的真实请求体(tasks + label_config + params)调用,
`bus.jpg` 返回 10 个框,坐标换算正确(百分比、`original_width/height` 与原图一致);
- 图片无检出时返回一条空 `result` 的预测,表示"这张图没问题";
- `/webhook` 连发 3 条 `ANNOTATION_CREATED`,在第 3 条触发 `RETRAIN_COMMAND` 并把计数清零。
转换逻辑的单元测试在 `tests/test_ml_backend_convert.py`,跑在主环境里
(`convert.py` 刻意不 import `label_studio_ml`,就是为了能这样测)。