current.pt 指向最新一次训练的结果(后端与 push_predictions 都读它), best.pt 指向历史上 mAP50 最高的那版,留作回退与对照。 数据集每次重训都在变,这个比较只是粗略参考而非严格的同条件对比,所以只用来 维护 best.pt 这个指针,不拦截 current.pt 的切换。 用服务器上三次真实训练验证过:首次建立、更好时更新、更差时保持不变。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
107 lines
5.5 KiB
Markdown
107 lines
5.5 KiB
Markdown
# 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`,就是为了能这样测)。
|