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