## 1. 核心理念:什么是 ML Backend?
参考链接:[Introduction to Machine Learning with Label Studio](https://labelstud.io/blog/introduction-to-machine-learning-with-label-studio/#introduction)
在 Label Studio 的架构中,前端(界面)和后端(数据库)完全不包含机器学习逻辑。所有的模型推理(Inference)和训练(Training)逻辑都托管在 **ML Backend** 中。
ML Backend 本质上是一个 **Web 服务(Wrapper Service)**,它充当了 Label Studio 与你的模型之间的"翻译官":
1. **解析(In)**:接收 Label Studio 发送的标注任务(JSON),提取数据(如图片 URL 或文本内容)。
2. **推理(Run)**:调用你的模型(YOLO, BERT, GPT-4 等)进行预测。
3. **封装(Out)**:将模型输出转换为 Label Studio 能够识别的特定 JSON 格式并返回。
> 💡 **核心思想**:只要你能用 Python 写出这套"输入-输出"逻辑,你就可以接入任何模型。
### 1.1 架构总览
下图展示了 Label Studio 与 ML Backend 的交互关系:

**工作流程说明**:
1. 用户在 Label Studio 界面打开标注任务
2. Label Studio 向 ML Backend 发送 HTTP 请求(包含任务数据)
3. ML Backend 调用模型进行推理
4. 返回预测结果(JSON 格式)
5. Label Studio 在界面上展示预标注结果
---
## 2. 环境准备
官方提供了一个 Python SDK `label-studio-ml`,它封装了 Flask/FastAPI 和 Redis,简化了开发流程。
### 2.1 基础安装
```bash
# 1. 安装官方 SDK
pip install label-studio-ml
# 2. 创建一个后端项目模版
label-studio-ml init my_backend
cd my_backend
```
执行 `init` 后,你会得到一个目录,其中最核心的文件是 `model.py`。
### 2.2 项目结构
```plaintext
my_backend/
├── model.py # 核心:模型封装逻辑
├── requirements.txt # Python 依赖
├── Dockerfile # Docker 镜像构建文件
└── docker-compose.yml # 容器编排配置
```
---
## 3. 代码实现:封装模型的三个步骤
你需要修改 `model.py`,创建一个继承自 `LabelStudioMLBase` 的类。核心逻辑仅需实现 `setup` (或 `__init__`) 和 `predict` 两个方法。
### 3.1 第一步:初始化模型 (`__init__` / `setup`)
⚙️ **目标**:在这里加载你的模型文件(权重),避免每次请求都重新加载。
```python
from label_studio_ml.model import LabelStudioMLBase
import torch
# 引入你自己的模型库,例如 ultralytics (YOLO) 或 openai
# from ultralytics import YOLO
class MyCustomModel(LabelStudioMLBase):
def __init__(self, project_id=None, **kwargs):
super(MyCustomModel, self).__init__(project_id=project_id, **kwargs)
print("正在加载模型...")
# 示例1:加载本地 YOLO 模型
# self.model = YOLO('yolov8n.pt')
# 示例2:初始化 OpenAI 客户端
# self.client = OpenAI(api_key="sk-...")
# 获取 Label Studio 项目的标签配置 (Schema)
# self.parsed_label_config 包含了 XML 中的标签定义
# 可用于动态适配不同的标注任务类型
```
### 3.2 第二步:预测逻辑 (`predict`) —— ⭐️ 核心
这是 Label Studio 调用最频繁的接口。你需要遍历传入的任务列表,生成预测结果。
#### 输入 `tasks` 结构示例
```json
[{
"data": {
"image": "http://localhost:8080/data/upload/1.jpg",
"text": "这是一段测试文本"
},
"id": 100
}]
```
#### 代码逻辑框架
```python
def predict(self, tasks, **kwargs):
"""
核心预测方法
"""
predictions = []
for task in tasks:
# ========== 步骤1:解析输入数据 ==========
# 注意:对应 XML 配置 <Image name="img" value="$image"/>
# 这里获取 task['data'] 中的 key 通常对应 value="$image" 去掉 $ 后的名称
image_url = task['data'].get('image')
# ========== 步骤2:预处理 & 推理 ==========
# image_path = self.get_local_path(image_url) # SDK 内置下载方法
# model_output = self.model.predict(image_path)
# ========== 步骤3:格式转换 (参考第4节) ==========
# 这里的 format_output 需要你根据第4节的逻辑自己实现
# 例如:formatted_results = self.format_output(model_output)
# 假设这是转换后的结果
formatted_results = [{
"from_name": "label",
"to_name": "image",
"type": "rectanglelabels",
"value": { ... }
}]
# ========== 步骤4:构建返回结构 ==========
predictions.append({
"result": formatted_results,
"score": 0.95 # (可选) 仅在需要主动学习时填写
})
return predictions
```
### 3.3 第三步:训练逻辑 (`fit`) —— 可选
如果你只需要"预标注",可以不写这个方法。如果你希望在 Label Studio 点击"训练"按钮时触发模型更新,则需要实现。
```python
def fit(self, event, data, **kwargs):
"""
模型训练/微调逻辑
Args:
event: 触发事件类型
data: 用户提交的标注数据
Returns:
训练结果信息(如新模型路径)
"""
# 步骤1:保存 data 到本地 JSON
# 步骤2:触发后台训练脚本(建议异步执行,不要阻塞)
# 步骤3:训练完成后保存新模型权重
print("触发训练逻辑...")
return {'model_path': '/path/to/new/model.pt'}
```
### 3.4 完整执行流程
下图展示了 `predict` 方法的完整执行流程:

## 4. 关键协议:如何包装返回值 (Format Output)
⚠️ **难点所在**:这是封装"任意模型"的核心挑战。你必须严格遵守 Label Studio 的 JSON 结构。
### 4.1 通用返回结构
**字段说明**:
- `from_name`:XML 中工具标签的 name 属性(如 `<RectangleLabels name="label">`)
- `to_name`:XML 中数据标签的 name 属性(如 `<Image name="image">`)
- `type`:标注类型
- `value`:核心数据(根据类型不同而不同)
```json
{
"from_name": "label",
"to_name": "image",
"type": "rectanglelabels",
"value": { ... }
}
```
### 4.2 常见场景对比表
| 场景类型 | XML 标签 | type 字段 | value 关键字段 | 坐标系统 |
| ------------ | ------------------- | ----------------- | --------------------- | -------------- |
| 目标检测 | `<RectangleLabels>` | `rectanglelabels` | `x, y, width, height` | 百分比 (0-100) |
| 图像分类 | `<Choices>` | `choices` | `choices` (数组) | N/A |
| 文本生成 | `<TextArea>` | `textarea` | `text` (数组) | N/A |
| 命名实体识别 | `<Labels>` | `labels` | `start, end, text` | 字符索引 |
| 关键点标注 | `<KeyPointLabels>` | `keypointlabels` | `x, y, width (=0)` | 百分比 (0-100) |
### 4.3 场景 A:图像目标检测 (Object Detection)
- **XML**: `<RectangleLabels>`
- **难点**:Label Studio 使用 **0-100 的百分比坐标系**,而非像素坐标。
#### 转换逻辑
```python
# 假设模型返回像素坐标: [x_min, y_min, x_max, y_max]
# 必须知道原始图片宽高: img_w, img_h
value = {
"x": (x_min / img_w) * 100, # 左上角 x 坐标(百分比)
"y": (y_min / img_h) * 100, # 左上角 y 坐标(百分比)
"width": ((x_max - x_min) / img_w) * 100, # 宽度(百分比)
"height": ((y_max - y_min) / img_h) * 100, # 高度(百分比)
"rotation": 0, # 旋转角度
"rectanglelabels": ["Car"] # 预测类别(数组)
}
```
**完整示例**:
```json
{
"from_name": "label",
"to_name": "image",
"type": "rectanglelabels",
"value": {
"x": 25.5,
"y": 30.2,
"width": 15.8,
"height": 20.1,
"rotation": 0,
"rectanglelabels": ["Car"]
}
}
```
### 4.4 场景 B:图像分类 / 文本分类 (Classification)
- **XML**: `<Choices>`
#### 转换逻辑
```python
value = {
"choices": ["Positive"] # 必须是列表,即使只有一个类别
}
```
**完整示例**:
```json
{
"from_name": "sentiment",
"to_name": "text",
"type": "choices",
"value": {
"choices": ["Positive"]
}
}
```
### 4.5 场景 C:LLM 文本生成 / 问答 (Text Generation)
- **XML**: `<TextArea>`
#### 转换逻辑
```python
# 模型输出: "这是翻译后的内容"
value = {
"text": ["这是翻译后的内容"] # 预填充到输入框(必须是数组)
}
```
**完整示例**:
```json
{
"from_name": "answer",
"to_name": "question",
"type": "textarea",
"value": {
"text": ["这是翻译后的内容"]
}
}
```
### 4.6 场景 D:命名实体识别 (NER)
- **XML**: `<Labels>` (作用于 Text)
- **难点**:LLM 通常输出文本,但 Label Studio 需要**字符索引**。
#### 转换逻辑
你需要根据 LLM 提取的实体文本,去原文中查找 `start` 和 `end` 索引。
```python
# 原文: "联系张三先生"
# 模型提取: "张三"
# 必须计算出 start=2, end=4
value = {
"start": 2, # 起始字符索引
"end": 4, # 结束字符索引
"text": "张三", # 实体文本
"labels": ["Person"] # 实体类型
}
```
**完整示例**:
```json
{
"from_name": "ner",
"to_name": "text",
"type": "labels",
"value": {
"start": 2,
"end": 4,
"text": "张三",
"labels": ["Person"]
}
}
```
---
## 5. 部署与连接
代码写好后,需要将其运行起来并连接到 Label Studio。
### 5.1 本地启动后端服务
在 `model.py` 同级目录下运行:
```bash
label-studio-ml start my_backend
```
这会启动一个默认监听 **9090 端口** 的 Web 服务。
### 5.2 生产环境部署 (Docker Compose)
为了解决依赖问题(如 CUDA),强烈建议使用 Docker。`init` 命令生成的目录里已经包含了 `Dockerfile` 和 `docker-compose.yml`。
#### 修改 docker-compose.yml
确保将本地模型权重文件挂载进去:
```yaml
services:
ml-backend:
build: .
volumes:
- ./my_models:/app/my_models # 挂载模型文件
environment:
- MODEL_DIR=/app/my_models
- CUDA_VISIBLE_DEVICES=0 # 指定 GPU(如需要)
ports:
- "9090:9090"
restart: unless-stopped
```
#### 启动容器
```bash
docker-compose up -d
```
### 5.3 部署架构图
下图展示了完整的部署架构:

**架构说明**:
- **Label Studio 容器**:提供 Web 界面和任务管理
- **ML Backend 容器**:运行模型推理服务
- **共享存储**:挂载模型文件和数据集
- **网络通信**:通过 Docker 网络或宿主机网络通信
### 5.4 在界面连接
1. 打开 Label Studio 项目,进入 **Settings** → **Machine Learning**
2. 点击 **Add Model**
3. **📍 URL 填写指南**:
| 部署场景 | URL 示例 | 说明 |
| --------------------- | ---------------------------------- | ------------------------------------- |
| 本地开发(非 Docker) | `http://localhost:9090` | Label Studio 和 Backend 都在本机运行 |
| Docker Compose | `http://ml-backend:9090` | 使用服务名(需在同一网络) |
| 混合部署 (Mac/Win) | `http://host.docker.internal:9090` | Label Studio 在容器,Backend 在宿主机 |
| 混合部署 (Linux) | `http://172.17.0.1:9090` | 使用 Docker 桥接网络 IP |
| 生产环境 | `http://ml-backend.example.com` | 使用域名或负载均衡地址 |
4. 开启 **"Retrieve predictions when loading a task"** 开关,实现自动预标注
---
## 6. 常见问题排查 (Troubleshooting)
### 6.1 ❌ Connection Refused / Timeout
**症状**:Label Studio 无法连接到 ML Backend
**原因**:这是最常见的问题,99% 是因为 Docker 容器之间的网络不通。
**解决方案**:
| 检查项 | 操作 |
| ------------ | ----------------------------------------- |
| 端口是否开放 | `docker ps` 查看端口映射 |
| 网络是否连通 | `docker exec <container> ping ml-backend` |
| URL 是否正确 | ⚠️ 容器内不要用 `localhost` |
| 防火墙规则 | 检查主机防火墙配置 |
```bash
# 测试连接
curl http://localhost:9090/health
```
### 6.2 ❌ 界面没有显示预测结果
**症状**:连接成功但标注界面没有预标注
**排查步骤**:
1. **检查 Backend 日志**
```bash
docker logs ml-backend
```
2. **验证返回格式**
- `from_name` 和 `to_name` 是否与 XML 配置**完全一致**(区分大小写)
- 坐标是否归一化到了 0-100 之间
- `type` 字段是否正确
3. **手动测试 API**
```bash
curl -X POST http://localhost:9090/predict \
-H "Content-Type: application/json" \
-d '{"tasks": [{"data": {"image": "..."}}]}'
```
### 6.3 ❌ 图片无法下载
**症状**:Backend 无法获取图片内容
**原因分析**:
- 如果 Label Studio 设置了登录保护,ML Backend 下载图片时可能需要携带 `Authorization` 头
- 云存储链接(S3、OSS)需要确保 Backend 容器有网络访问权限
**解决方案**:
```python
# SDK 自带的方法会自动处理部分鉴权
image_path = self.get_local_path(image_url)
# 如果需要自定义鉴权
import requests
headers = {'Authorization': f'Token {self.label_studio_token}'}
response = requests.get(image_url, headers=headers)
```
### 6.4 📊 问题排查流程图
| 步骤 | 检查内容 | 工具 |
| ---- | ---------------- | ---------------------------- |
| 1️⃣ | 服务是否启动 | `docker ps` / `ps aux` |
| 2️⃣ | 端口是否监听 | `netstat -tulpn` |
| 3️⃣ | 网络是否连通 | `curl` / `ping` |
| 4️⃣ | 返回格式是否正确 | 查看日志 / 手动测试 |
| 5️⃣ | XML 配置是否匹配 | 对比 `from_name` / `to_name` |
---
## 7. 总结:万能公式
接入任意模型到 Label Studio 的万能公式:
> **🎯 ML Backend = HTTP 接口 + 数据预处理 + 模型推理 + JSON 格式化**
### 核心要点
| 要素 | 说明 |
| --------------- | -------------------------------------- |
| **HTTP 接口** | 基于 Flask/FastAPI,接收 POST 请求 |
| **数据预处理** | 解析 JSON,下载图片/文本,转换格式 |
| **模型推理** | 调用任意 ML 模型(YOLO、GPT、BERT...) |
| **JSON 格式化** | 将模型输出转换为 Label Studio 协议 |
### 关键认知
你不需要关心模型内部是 Transformer 还是 CNN,你只需要关心如何把 Label Studio 的 **Task JSON** 变成模型的 **Input**,再把模型的 **Output** 变成 Label Studio 的 **Result JSON**。
> 💡 **一句话概括**:ML Backend 是一个"翻译器",负责在 Label Studio 的数据格式和你的模型之间架起桥梁。