이미 운영 중인 모델의 사전 레이블과 홀드아웃 점수
vLLM, Ollama, Hugging Face, Roboflow, CVAT, Label Studio ML 백엔드로 제공되는 모델의 점수를 연구의 레이블로 계산한 뒤, 모델의 답을 사전 레이블로 검토합니다.
v2.14.0의 새 기능
이 튜토리얼은 Potato 밖에서 이미 모델을 운영하고 있으며, 모델의 답을 누구에게든 보여 주기 전에 그 모델이 팀의 어노테이터와 얼마나 일치하는지 알고 싶은 팀을 위한 것입니다. Potato의 모델 학습에는 브리지라고 부르는, 아무것도 학습시키지 않는 트레이너 다섯 개가 있습니다. 브리지는 연구의 홀드아웃 항목을 다른 곳에서 제공되는 모델에 보내고, 그 답을 연구의 레이블과 비교해 점수를 매기며, 학습한 모델의 실행과 마찬가지로 답을 검토용 사전 레이블로 되씁니다. 아래 단계는 OpenAI 호환 API 뒤의 비전-언어 모델, Roboflow 모델, CVAT 서버리스 함수, Hugging Face 모델, Label Studio ML 백엔드를 각각 검증된 설정과 함께 다룹니다.
브리지 실행이 보내고 점수 매기는 것
브리지는 학습 단계가 서비스의 응답 여부만 확인하는 트레이너입니다. 그다음 실행은 실행의 val과 test 분할에 있는 항목을 서비스에 보내며, writeback.include_unlabeled: true이면 아무도 레이블을 달지 않은 항목도 보냅니다. Potato는 답을 스키마 고유의 형태, 즉 스팬이나 선택지로 바꾸고, 학습한 모델이 받는 것과 같은 홀드아웃 지표(박스는 mAP50, 스팬은 스팬 F1, 단일 선택은 macro-F1)로 홀드아웃 항목의 답을 통합된 사람 레이블과 비교해 점수를 매깁니다. 되쓰기, 검토 대기열, 각 사전 레이블의 실행 ID는 다른 실행 뒤와 똑같이 작동합니다.
| 트레이너 | 스키마 | 호출하는 모델 |
|---|---|---|
openai-vlm-boxes | image_annotation | OpenAI 호환 채팅 API 뒤의 비전-언어 모델 (vLLM, Ollama, LM Studio, llama.cpp 서버, OpenAI) |
roboflow | image_annotation | 호스팅되거나 자체 호스팅 Inference 서버에서 실행되는 Roboflow 탐지 또는 인스턴스 분할 모델 |
cvat-nuclio | image_annotation | Nuclio에서 실행되는 CVAT 서버리스 함수 |
hf-inference | radio, select, multiselect, span, image_annotation | Hugging Face Inference Endpoint 또는 서버리스 HF Inference API의 모델 |
label-studio-ml | radio, select, multiselect, span, image_annotation | 실행 중인 Label Studio ML 백엔드 |
항목의 분할은 항목의 ID와 model_training.split_seed에만 달려 있으므로, 시드가 고정되어 있는 동안 브리지 실행과 학습한 모델의 실행은 같은 val과 test 항목으로 점수를 매깁니다. 그러면 /admin/training의 실행 표는 이미 가지고 있는 모델을 연구의 레이블로 학습한 모델 옆에 놓아 줍니다. train 분할의 항목은 브리지에서 예측을 받지 못하며, 되쓰기는 train 분할 항목에 사전 레이블을 달지 않습니다. 따라서 기본 분할 70/15/15에서 브리지는 레이블이 달린 항목의 약 30%에 대해 점수를 매깁니다.
시작하기 전에
label-studio-ml을 제외한 네 브리지는 Potato 서버에 requests와 Pillow가 필요하며, 두 패키지가 모두 임포트될 때까지 Training 페이지는 그 브리지를 "not installed"로 표시합니다. label-studio-ml은 Potato 외에는 아무것도 필요하지 않습니다. 모델은 서비스에서 실행되므로 다섯 브리지 모두 모델, GPU 라이브러리, torch를 설치하지 않습니다.
이미지는 모든 이미지 트레이너와 마찬가지로 프로젝트 안의 로컬 파일이어야 합니다. 브리지는 이미지가 원격 http(s) URL인 항목을 제외하며, 레이블은 있으나 예측이 없는 항목은 홀드아웃 점수에서 틀린 것으로 셉니다. 실행은 예측을 받은 홀드아웃 항목의 비율을 val_coverage와 test_coverage로 보고하므로, 커버리지 수치가 낮으면 브리지가 보내지 못한 이미지가 있다는 뜻입니다.
키는 환경 변수에 둡니다
키는 설정에 절대 넣지 않습니다. 키를 보내는 각 브리지에는 키를 담은 환경 변수의 이름을 지정하는 매개변수(api_key_env, Hugging Face의 token_env, Label Studio의 두 basic_auth_*_env)가 있으며, 그 변수는 서버를 시작하기 전에 Potato 서버의 환경에 설정되어 있어야 합니다. cvat-nuclio는 키를 보내지 않으며 그런 매개변수도 없습니다. 키를 받는 네 브리지는 변수 이름이 아닌 값을 거부하며, 그 자리의 값은 키 자체일 가능성이 가장 높으므로 오류는 그 값을 반복하지 않습니다. 서비스의 오류 본문이나 연결 오류가 요청을 그대로 되돌려 보내면, 브리지는 오류를 보여 주기 전에 키의 값을 [redacted]로 바꿉니다. Label Studio ML 백엔드의 /predict에서 온 오류 본문도 같은 방식으로 정리됩니다. 쿼리 문자열이나 Authorization 헤더에서 키처럼 생긴 것은 무엇이든 마찬가지로 대체됩니다.
hf-inference와 roboflow 실행은 로컬 전용입니다. 두 브리지는 Potato 서버에서 키를 읽으며, ssh, SLURM, 에이전트 백엔드의 실행은 그 호스트로 키를 보내야 하므로 Potato는 그런 곳에서 실행을 시작하지 않습니다. openai-vlm-boxes, cvat-nuclio, label-studio-ml은 서비스에 닿을 수 있는 모든 백엔드에서 실행되며, 키의 변수는 그 백엔드의 호스트에 설정합니다.
1단계: 브리지 세 개로 이미지 연구 구성하기
아래 설정은 어노테이터가 모든 안전모와 모든 맨머리에 박스를 치는 현장 사진 연구를 구성합니다. model_training.trainer_params는 여러 트레이너의 기본값을 한꺼번에 담으므로 설정 하나에 이미지 브리지 세 개의 설정이 모두 들어가며, 각 실행은 자기 트레이너의 블록을 사용합니다. Training 페이지의 Parameters 필드는 트레이너를 고르면 이 기본값으로 채워집니다.
annotation_task_name: "Hard hats on site photos"
task_dir: .
media_directory: media
output_annotation_dir: annotation_output
admin_api_key: "replace-with-a-long-random-string"
data_files:
- data/photos.jsonl
item_properties:
id_key: id
text_key: image_url
annotation_schemes:
- annotation_type: image_annotation
name: ppe
description: "Box every hard hat and every bare head."
source_field: image_url
tools: [bbox, polygon]
labels:
- name: helmet
- name: head
model_training:
enabled: true
writeback:
schemas: [ppe]
include_unlabeled: true
trainer_params:
openai-vlm-boxes:
url: http://localhost:8000/v1
model: Qwen/Qwen2.5-VL-7B-Instruct
coordinate_format: pixels
roboflow:
model_id: site-safety/3 # your Roboflow project and version
url: https://detect.roboflow.com
api_key_env: ROBOFLOW_API_KEY
confidence: 0.4
label_map: {hardhat: helmet, no-hardhat: head} # the model's class names on the left
cvat-nuclio:
url: http://localhost:32768
threshold: 0.5
label_map: {person: null}data/photos.jsonl의 각 줄은 media/ 아래의 이미지 하나를 지정합니다.
{"id": "site_001", "image_url": "media/site_001.jpg"}writeback.schemas는 모델의 박스를 어노테이터에게 보이게 하는 설정이며, include_unlabeled는 아무도 레이블을 달지 않은 사진도 서비스에 보냅니다. 누구도 사전 레이블을 보기 전에 홀드아웃 점수를 먼저 얻고 싶다면 첫 실행에서는 두 키를 모두 빼십시오. label_map은 서비스의 클래스 이름을 스키마의 레이블에 대응시키며, null에 대응된 클래스는 버려집니다. 항목이 없는 클래스는 대소문자를 무시하고 같은 이름의 레이블로 갑니다. 그 밖의 클래스의 박스는 제외되며, 실행 로그가 제외된 박스의 수를 셉니다.
2단계: openai-vlm-boxes로 비전-언어 모델 사용하기
openai-vlm-boxes는 각 이미지를 긴 변 기준 최대 1,280픽셀(max_image_side)로 줄이고, 스키마의 레이블을 밝힌 프롬프트와 함께 채팅 완성 경로로 이미지를 보냅니다. Potato는 응답을 JSON 박스로 읽습니다. OpenAI 호환 API를 가진 어떤 서버든 작동합니다. 아래 명령은 vLLM으로 Qwen2.5-VL을 제공하며, vLLM 문서는 클라이언트를 http://localhost:8000/v1에 연결합니다.
vllm serve Qwen/Qwen2.5-VL-7B-InstructvLLM이 포트 8000에 있으면 Potato를 다른 포트에서 시작하십시오.
potato start config.yaml -p 9000비전-언어 모델은 프롬프트가 무엇을 요구하든 자기만의 좌표 관례로 답하며, 매개변수 두 개가 Potato에게 숫자를 읽는 법을 알려 줍니다. coordinate_format은 fraction(이미지의 01), 1,000), 또는 전송된 이미지의 per_mille(0pixels입니다. box_order는 [x0, y0, x1, y1]이면 xyxy, [y0, x0, y1, x1]이면 yxyx입니다. Qwen2.5-VL은 픽셀로 답하므로 위 설정은 coordinate_format: pixels를 지정합니다. auto에서 Potato는 응답마다 한 번 숫자의 크기로 형식을 추측합니다. 1.05 이하의 값은 비율로, 전송된 이미지의 크기를 넘는 값은 퍼밀로, 그 밖의 값은 픽셀로 읽습니다. 따라서 큰 이미지 안에 들어가는 퍼밀 박스는 픽셀로 읽히므로, 모델의 관례를 알고 있다면 항상 형식을 지정하십시오.
Gemma와 Gemini는 프롬프트가 비율을 요구해도 y가 먼저인 퍼밀 박스 [ymin, xmin, ymax, xmax]로 답합니다. 두 매개변수가 모두 auto이면 Potato는 이름에 gemma 또는 gemini가 들어간 모델의 박스를 y가 먼저인 퍼밀로 읽습니다. 빨간 사각형과 파란 사각형으로 만든 Gemma 4 12B 테스트 프로젝트에서 Potato 팀은 박스를 x 먼저로 읽을 때 mAP50이 0.0, y 먼저로 읽을 때 1.0임을 측정했습니다. Ollama는 Gemma 3를 gemma가 들어간 이름으로 제공하므로 auto가 적용됩니다.
ollama run gemma3:12b설정의 openai-vlm-boxes 블록을 아래 블록으로 바꾸십시오.
model_training:
trainer_params:
openai-vlm-boxes:
url: http://localhost:11434/v1
model: gemma3:12b
json_mode: object둘 중 어느 단어도 없는 이름으로 제공되는 Gemma나 Gemini 파인튜닝 모델은 auto에서 x 먼저 읽기를 받습니다. 그런 모델에는 coordinate_format: per_mille와 box_order: yxyx를 설정하십시오. json_mode: object는 Ollama에 response_format: json_object로 JSON을 요청합니다. 기본값인 schema는 레이블을 열거형으로 담은 JSON 스키마를 보냅니다. vLLM과 OpenAI는 그 스키마를 따릅니다. 어떤 응답도 JSON으로 파싱되지 않는 실행은 실패하며, 오류는 json_mode: object나 더 단순한 프롬프트(prompt, 여기서 {labels}는 스키마의 레이블이 됩니다)를 제안합니다.
비전-언어 모델은 박스와 함께 점수를 주지 않으므로 박스에는 신뢰도가 없습니다. writeback.min_confidence는 모든 openai-vlm-boxes 예측을 통과시키며, 모델 검토는 예측을 신뢰도가 낮은 것부터 높은 것까지 정렬할 수 없습니다. openai-vlm-boxes 참조 문서에는 호스팅 API를 위한 api_key_env와 토큰 및 타임아웃 설정이 나와 있습니다.
3단계: 실행을 시작하고 점수 읽기
http://localhost:9000/admin/training을 열어 openai-vlm-boxes와 ppe 스키마를 고른 다음 Train now를 클릭하십시오. 같은 실행을 API에서도 시작할 수 있습니다.
curl -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
http://localhost:9000/admin/training/api/runs \
-d '{"trainer": "openai-vlm-boxes", "schemas": ["ppe"]}'학습 단계는 /models를 통해 서버의 모델을 나열하고, 실행이 어느 모델에 묻는지와 모델의 박스를 어떻게 읽는지를 기록합니다. 그다음 예측 단계는 요청마다 이미지 하나를 보냅니다. 실행이 끝나면 홀드아웃 mAP50이 실행 표에 나타나며, 실행 페이지에는 val_map_50, test_map_50, 커버리지 수치, 그리고 JSON이 아니었던 응답, 스키마에 없는 레이블의 박스, 이미지 밖의 박스를 세는 "Not used" 경고가 나열됩니다. 커버리지가 높은데 점수가 0에 가깝고 경고가 비어 있으면 coordinate_format과 box_order를 먼저 확인하십시오.
Roboflow 모델
roboflow는 각 이미지를 <url>/<model_id>에 POST하며, model_id는 project/version 형태의 프로젝트와 버전입니다. 키는 기본적으로 ROBOFLOW_API_KEY에 넣으며(api_key_env), 키가 없으면 실행이 시작되지 않습니다.
export ROBOFLOW_API_KEY=... # in the shell that starts Potato
potato start config.yaml -p 9000Potato의 기본 url은 https://detect.roboflow.com이며, 위 설정은 기본값을 지정합니다. 인스턴스 분할 실행은 url: https://outline.roboflow.com을 설정합니다. url은 매개변수이므로 실행은 https://serverless.roboflow.com을 지정할 수도 있으며, 이는 Roboflow의 객체 탐지 참조 문서가 같은 project/version 경로에 대해 제시하는 호스트입니다. Potato 팀은 이 브리지로 그 호스트를 테스트하지 않았습니다. 자체 호스팅 Roboflow Inference 서버는 기본적으로 포트 9001에서 수신합니다(url: http://localhost:9001).
Roboflow의 API는 키를 쿼리 문자열로 받습니다. Potato는 요청 URL을 실행 로그나 오류 메시지에 쓰지 않습니다. Potato는 confidence와 overlap(기본값 0.4와 0.3)을 백분율로 API에 보내며, Roboflow의 참조 문서는 둘 다 0~100 척도로 설명합니다. shapes: auto는 모델이 윤곽선을 반환하고 스키마에 다각형 도구가 있으면 윤곽선을 쓰고, 그렇지 않으면 박스를 씁니다. 상태 확인은 8 × 8 흰색 이미지를 POST하며, Roboflow는 그 요청을 계정의 사용량으로 셉니다.
CVAT 서버리스 함수
cvat-nuclio는 CVAT에 이미 배포된 탐지기를 Nuclio 함수로 사용하며, cvat/serverless의 YOLOv7이나 Faster R-CNN 함수 같은 것이 해당됩니다. Potato는 CVAT를 거치지 않고 각 이미지를 {"image": <base64>, "threshold": t} 형태로 함수 자체의 URL에 POST합니다. nuctl get functions는 배포된 함수를 포트와 함께 나열하며, 아래 명령은 CVAT 문서가 제시하는 형태입니다.
nuctl get functions --platform local함수는 픽셀 단위의 도형으로 답합니다. rectangle은 박스가 되고, polygon은 다각형이 되거나, 스키마에 다각형 도구가 없으면 다각형의 경계 박스가 됩니다. mask 도형은 제외되고 집계됩니다. 함수는 점수가 threshold(기본값 0.5) 미만인 도형을 버립니다. 스키마에는 사람 레이블이 없으므로 설정의 label_map: {person: null}은 COCO 탐지기의 사람을 버립니다.
hf-inference로 Hugging Face 모델 사용하기
hf-inference는 Hugging Face에 배포된 모델을 호출하며, https:// URL로 지정하는 전용 Inference Endpoint(endpoint_url)이거나 서버리스 HF Inference API의 모델 ID(model)입니다. endpoint_url과 model 중 하나만 설정하고 둘 다 설정하지는 마십시오. 작업은 스키마의 유형을 따릅니다.
- 텍스트 분류는
radio,select,multiselect에 쓰입니다. 단일 선택 스키마는 모델의 최상위 레이블을 취합니다.label_map을 거친 뒤 그 레이블이 스키마의 레이블이 아니면 차순위로 가지 않고 항목은 예측을 받지 못합니다.multiselect스키마는 점수가threshold(기본값 0.5) 이상인 모든 레이블을 취합니다. - 토큰 분류는
span에 쓰이며, 워드피스는aggregation_strategy(simple,first,average,max)에 따라 엔티티로 묶이고 문자 오프셋은 모델에서 가져옵니다. - 객체 탐지는
image_annotation에 쓰입니다. 점수가threshold미만인 박스는 버려집니다.
아래 설정은 dslim/bert-base-NER에 스팬을 요청합니다. 모델 카드는 영어 CoNLL-2003으로 개체명에 맞춰 파인튜닝한 BERT 모델이라고 설명합니다. HF Inference는 Hub 모델의 일부만 제공하므로, 서버리스 API에 의존하기 전에 모델 페이지에 HF Inference가 나열되어 있는지 확인하십시오.
annotation_task_name: "Named entities in news sentences"
task_dir: .
output_annotation_dir: annotation_output
admin_api_key: "replace-with-a-long-random-string"
data_files:
- data/sentences.jsonl
item_properties:
id_key: id
text_key: text
annotation_schemes:
- annotation_type: span
name: entities
description: "Highlight every person, organization, and place."
labels: [person, organization, location]
model_training:
enabled: true
writeback:
schemas: [entities]
include_unlabeled: true
trainer_params:
hf-inference:
model: dslim/bert-base-NER
token_env: HF_TOKEN
aggregation_strategy: simple
label_map: {PER: person, ORG: organization, LOC: location, MISC: null}모델의 PER, ORG, LOC 엔티티는 스키마의 레이블에 대응되고, 스키마에 그런 레이블이 없으므로 MISC는 버려집니다. 엔티티의 전체 이름이 아무것과도 맞지 않으면 Potato는 B- 또는 I- 접두사를 떼고 다시 시도합니다. LABEL_0과 LABEL_1로 답하는 감성 분류기에는 같은 방식으로 label_map: {LABEL_0: negative, LABEL_1: positive}가 필요합니다. 상태 확인은 짧은 문장 또는 8 × 8 이미지로 하는 실제 요청 한 번이며, wait_for_model: true(기본값)는 서버리스 모델이 로드되는 동안 503으로 실패하지 않고 기다립니다.
Label Studio ML 백엔드
label-studio-ml은 실행 중인 Label Studio ML 백엔드를 사용합니다. 저장소의 예제에서 docker-compose up으로 시작한 백엔드는 http://localhost:9090에서 HTTP로 모델을 제공합니다. 실행은 백엔드의 GET /health를 확인하고, 항목을 16개 단위 배치(batch_size)로 POST /predict에 보내고, 반환된 영역을 potato import -f label_studio가 쓰는 것과 같은 변환기로 읽습니다. Label Studio의 UTF-16 스팬 오프셋은 코드 포인트가 되고, 퍼센트 좌표는 이미지 위젯의 좌표가 됩니다.
annotation_task_name: "Review sentiment"
task_dir: .
output_annotation_dir: annotation_output
admin_api_key: "replace-with-a-long-random-string"
data_files:
- data/reviews.jsonl
item_properties:
id_key: id
text_key: text
annotation_schemes:
- annotation_type: radio
name: sentiment
description: "Is this review positive or negative?"
labels: [positive, negative]
model_training:
enabled: true
writeback:
schemas: [sentiment]
include_unlabeled: true
min_confidence: 0.6
trainer_params:
label-studio-ml:
url: http://localhost:9090
basic_auth_user_env: LS_ML_USER
basic_auth_pass_env: LS_ML_PASS기본 백엔드의 유일한 인증은 HTTP Basic이며, 백엔드의 BASIC_AUTH_USER와 BASIC_AUTH_PASS로 설정합니다. basic_auth_user_env와 basic_auth_pass_env는 같은 두 값을 담은 Potato 서버의 변수를 지정하며, 두 매개변수는 함께 씁니다. api_key_env는 대신 Authorization: Token <value>를 보내며, 토큰을 기대하는 프록시 뒤의 백엔드를 위한 것입니다. 기본 백엔드는 그 헤더에 500으로 답합니다. Potato는 각 요청과 함께 보내는 레이블링 설정을 스키마에서 생성하며(Choices, Labels, RectangleLabels, PolygonLabels), label_config는 생성된 XML을 직접 작성한 것으로 대체합니다.
이미지 백엔드에는 설정이 하나 더 필요합니다. media_url_prefix가 없으면 Potato는 각 이미지를 Potato 머신의 절대 경로로 보내며, Label Studio SDK로 만든 백엔드는 data.image를 URL로 읽으므로 맨 경로에서 실패합니다. 같은 머신의 백엔드에는 아래 접두사를 추가하고 LOCAL_FILES_DOCUMENT_ROOT를 작업 디렉터리로 설정해 백엔드를 시작하십시오.
model_training:
trainer_params:
label-studio-ml:
url: http://localhost:9090
media_url_prefix: /data/local-files/?d=다른 머신의 백엔드에는 작업 디렉터리를 HTTP로 제공하고 그 서버의 URL을 접두사로 지정하십시오. 작업 디렉터리 밖의 이미지는 디렉터리 기준 상대 경로가 없으므로 Potato는 그 이미지를 보내지 않으며, 실행 로그가 몇 개가 제외되었는지 알려 줍니다.
Potato는 백엔드에 예측 요청만 보내고 어노테이션 이벤트는 보내지 않으므로, 백엔드의 /webhook 핸들러에 있는 자체 학습은 Potato에서 시작되지 않습니다. potato import -f label_studio는 Label Studio 프로젝트 전체를 프로젝트의 레이블링 설정, 어노테이터의 답, 저장된 예측과 함께 Potato로 옮깁니다(프로젝트 가져오기). Label Studio에서 Potato로 이전하기는 임포터가 제외하는 태그를 다룹니다.
사전 레이블 검토하기
브리지의 예측은 학습한 모델의 예측과 같은 되쓰기 검사를 거칩니다. 되쓰기는 train 분할 항목과 writeback.schemas에 없는 스키마를 건너뛰고, 검토자가 거부한 항목과 스키마 쌍을 건너뛰며, writeback.min_confidence 미만의 예측을 보류합니다. 박스와 스팬의 경우 예측의 신뢰도는 가장 신뢰도가 낮은 도형이나 스팬의 신뢰도이므로, 항목의 박스나 스팬 중 하나라도 점수가 더 낮으면 기준에 따라 그 항목의 예측 전체가 보류됩니다. Roboflow의 confidence와 CVAT의 threshold는 대신 서비스에서 박스마다 작용하며, hf-inference의 threshold는 박스마다 또는 multiselect 레이블마다 작용합니다. 모델 인 더 루프 튜토리얼은 검토 대기열, 수락과 거부 판정, 재학습을 다룹니다. 대기열과 판정은 브리지에서도 같은 방식으로 작동합니다.
항목과 스키마마다 예측 하나가 유지되므로, 다른 브리지나 학습한 모델의 이후 실행은 이전 실행의 사전 레이블을 대체합니다. 한 실행의 사전 레이블을 철회하려면 DELETE /admin/training/api/runs/<run_id>/predictions를 보내십시오. 추론 브리지 참조 문서에는 네 브리지의 모든 매개변수가 나와 있고, label-studio-ml 참조 문서에는 Label Studio 매개변수가 나와 있습니다.