Vai al contenuto

Pre-etichette e punteggi held-out da un modello che già esegui

Valuta sulle etichette del tuo studio un modello servito da vLLM, Ollama, Hugging Face, Roboflow, CVAT o un backend ML di Label Studio, poi rivedi le sue risposte come pre-etichette.

Novità nella v2.14.0

Questo tutorial è per i team che eseguono già un modello fuori da Potato e vogliono sapere quanto il modello concorda con i propri annotatori prima di mostrare a qualcuno le sue risposte. L'addestramento di modelli di Potato include cinque trainer, chiamati bridge, che non addestrano nulla. Un bridge invia gli item esclusi dall'addestramento dello studio a un modello servito altrove, valuta le risposte rispetto alle etichette dello studio e le riscrive come pre-etichette da rivedere, come fa l'esecuzione di un modello addestrato. I passaggi seguenti coprono un modello visione-linguaggio dietro un'API compatibile con OpenAI, un modello Roboflow, una funzione serverless di CVAT, un modello Hugging Face e un backend ML di Label Studio, con una configurazione validata per ciascuno.

Che cosa invia e valuta l'esecuzione di un bridge

Un bridge è un trainer il cui passaggio di adattamento verifica soltanto che il servizio risponda. L'esecuzione invia poi al servizio gli item delle partizioni val e test dell'esecuzione, e con writeback.include_unlabeled: true anche gli item che nessuno ha etichettato. Potato trasforma le risposte nelle forme proprie dello schema, cioè span o scelte, e le valuta sugli item esclusi dall'addestramento rispetto alle etichette umane risolte, con le stesse metriche held-out di un modello addestrato (mAP50 per i box, F1 sugli span, macro-F1 per le scelte singole). Il write-back, la coda di revisione e l'ID dell'esecuzione di ogni pre-etichetta funzionano come dopo qualsiasi altra esecuzione.

TrainerSchemiIl modello che chiama
openai-vlm-boxesimage_annotationUn modello visione-linguaggio dietro un'API di chat compatibile con OpenAI (vLLM, Ollama, LM Studio, il server di llama.cpp, OpenAI)
roboflowimage_annotationUn modello Roboflow di rilevamento o segmentazione di istanze, ospitato o su un server Inference self-hosted
cvat-nuclioimage_annotationUna funzione serverless di CVAT eseguita su Nuclio
hf-inferenceradio, select, multiselect, span, image_annotationUn Hugging Face Inference Endpoint o un modello sulla HF Inference API serverless
label-studio-mlradio, select, multiselect, span, image_annotationUn backend ML di Label Studio in esecuzione

La partizione di un item dipende solo dall'id dell'item e da model_training.split_seed, quindi l'esecuzione di un bridge e quella di un modello addestrato vengono valutate sugli stessi item val e test finché il seed resta fisso. La tabella Runs in /admin/training mette così un modello che già possiedi accanto a un modello addestrato sulle etichette dello studio. Gli item nella partizione train non ricevono alcuna predizione da un bridge, e il write-back non pre-etichetta mai un item della partizione train. Con la ripartizione predefinita 70/15/15, un bridge viene quindi valutato su circa il 30% degli item etichettati.

Prima di iniziare

I quattro bridge diversi da label-studio-ml richiedono requests e Pillow sul server Potato, e la pagina Training elenca un bridge come "not installed" finché entrambi i pacchetti non si importano. label-studio-ml non richiede nulla oltre a Potato. Nessuno dei cinque bridge installa un modello, una libreria GPU o torch, perché il modello gira nel servizio.

Le immagini devono essere file locali dentro il progetto, come per ogni trainer di immagini. Un bridge esclude un item la cui immagine è un URL http(s) remoto, e un item con un'etichetta e senza predizione conta come errato nel punteggio held-out. L'esecuzione riporta la quota di item esclusi dall'addestramento che hanno ricevuto una predizione come val_coverage e test_coverage, quindi una copertura bassa indica immagini che il bridge non ha potuto inviare.

Le chiavi restano nelle variabili d'ambiente

Una chiave non va mai nella configurazione. Ogni bridge che invia una chiave accetta un parametro che nomina la variabile d'ambiente contenente la chiave (api_key_env, token_env per Hugging Face, o i due nomi basic_auth_*_env per Label Studio), e la variabile deve essere impostata nell'ambiente del server Potato prima che il server parta. cvat-nuclio non invia alcuna chiave e non accetta un parametro del genere. I quattro bridge che accettano una chiave rifiutano un valore che non è il nome di una variabile, e l'errore non ripete il valore, perché un valore in quel punto è con ogni probabilità la chiave stessa. Quando il corpo di un errore del servizio o un errore di connessione riecheggia la richiesta, i bridge sostituiscono il valore della chiave con [redacted] prima di mostrare l'errore. I corpi di errore del /predict di un backend ML di Label Studio vengono ripuliti allo stesso modo. Viene sostituito anche tutto ciò che ha la forma di una chiave in una stringa di query o in un'intestazione Authorization.

Le esecuzioni di hf-inference e roboflow sono solo locali. Entrambi i bridge leggono la chiave sul server Potato, e un'esecuzione su un backend ssh, SLURM o agent dovrebbe spedire la chiave a quell'host, quindi Potato rifiuta di avviare un'esecuzione di questo tipo lì. openai-vlm-boxes, cvat-nuclio e label-studio-ml girano su qualsiasi backend che raggiunga il servizio, con la variabile della chiave impostata sull'host di quel backend.

Passaggio 1: uno studio di immagini con tre bridge

La configurazione seguente imposta uno studio di foto di cantiere in cui gli annotatori riquadrano ogni elmetto e ogni testa scoperta. model_training.trainer_params contiene i valori predefiniti per più trainer insieme, quindi un'unica configurazione porta le impostazioni di tutti e tre i bridge per immagini, e ogni esecuzione usa il blocco del proprio trainer. Il campo Parameters della pagina Training si compila con questi valori predefiniti quando scegli un trainer.

yaml
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}

Ogni riga di data/photos.jsonl indica un'immagine sotto media/:

json
{"id": "site_001", "image_url": "media/site_001.jpg"}

writeback.schemas è ciò che rende visibili agli annotatori i box del modello, e include_unlabeled invia al servizio anche le foto che nessuno ha etichettato. Lascia fuori entrambe le chiavi per la prima esecuzione se vuoi il punteggio held-out prima che qualcuno veda una pre-etichetta. label_map associa i nomi delle classi del servizio alle etichette dello schema, e una classe associata a null viene scartata. Una classe senza voce va all'etichetta con lo stesso nome, e il confronto ignora le maiuscole. I box con qualsiasi altra classe restano fuori, e il log dell'esecuzione conta i box esclusi.

Passaggio 2: un modello visione-linguaggio con openai-vlm-boxes

openai-vlm-boxes riduce ogni immagine a un massimo di 1.280 pixel sul lato più lungo (max_image_side) e la invia alla route delle chat completions con un prompt che nomina le etichette dello schema. Potato legge la risposta come box JSON. Funziona qualsiasi server con un'API compatibile con OpenAI. Il comando seguente serve Qwen2.5-VL con vLLM, la cui documentazione collega i client a http://localhost:8000/v1:

bash
vllm serve Qwen/Qwen2.5-VL-7B-Instruct

Con vLLM sulla porta 8000, avvia Potato su un'altra porta:

bash
potato start config.yaml -p 9000

I modelli visione-linguaggio rispondono con la propria convenzione di coordinate qualunque cosa chieda il prompt, e due parametri dicono a Potato come leggere i numeri. coordinate_format è fraction (da 0 a 1 dell'immagine), per_mille (da 0 a 1.000) o pixels dell'immagine come inviata. box_order è xyxy per [x0, y0, x1, y1] o yxyx per [y0, x0, y1, x1]. Qwen2.5-VL risponde in pixel, quindi la configurazione qui sopra imposta coordinate_format: pixels. Con auto, Potato indovina il formato una volta per risposta dalla dimensione dei numeri. I valori fino a 1.05 si leggono come frazioni, i valori oltre la dimensione dell'immagine inviata si leggono come per mille e tutto il resto si legge come pixel. Un box per mille che sta dentro un'immagine grande si legge quindi come pixel, perciò indica il formato ogni volta che conosci la convenzione del modello.

Gemma e Gemini rispondono con box per mille con la y per prima, [ymin, xmin, ymax, xmax], anche quando il prompt chiede frazioni. Con entrambi i parametri su auto, Potato legge i box di un modello il cui nome contiene gemma o gemini come per mille con la y per prima. Su un progetto di prova con Gemma 4 12B su quadrati rossi e blu, il team di Potato ha misurato una mAP50 di 0.0 con i box letti con la x per prima e di 1.0 con i box letti con la y per prima. Ollama serve Gemma 3 con un nome che contiene gemma, quindi auto si applica:

bash
ollama run gemma3:12b

Sostituisci il blocco openai-vlm-boxes nella configurazione con il blocco seguente:

yaml
model_training:
  trainer_params:
    openai-vlm-boxes:
      url: http://localhost:11434/v1
      model: gemma3:12b
      json_mode: object

Un fine-tune di Gemma o Gemini servito con un nome privo di entrambe le parole riceve la lettura con la x per prima sotto auto. Imposta coordinate_format: per_mille e box_order: yxyx per un modello di questo tipo. json_mode: object chiede a Ollama JSON con response_format: json_object. Il valore predefinito, schema, invia uno schema JSON con le etichette come enum. vLLM e OpenAI rispettano lo schema. Un'esecuzione in cui nessuna risposta si interpreta come JSON fallisce, e l'errore suggerisce json_mode: object o un prompt più semplice (prompt, dove {labels} diventa le etichette dello schema).

Un modello visione-linguaggio non dà alcun punteggio con i suoi box, quindi i box non hanno confidenza. writeback.min_confidence lascia passare ogni predizione di openai-vlm-boxes, e la revisione del modello non può ordinare le predizioni dalla meno alla più confidente. Il riferimento di openai-vlm-boxes elenca api_key_env per un'API ospitata e le impostazioni di token e timeout.

Passaggio 3: avviare un'esecuzione e leggere il punteggio

Apri http://localhost:9000/admin/training, scegli openai-vlm-boxes e lo schema ppe e fai clic su Train now. La stessa esecuzione parte dall'API:

bash
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"]}'

Il passaggio di adattamento elenca i modelli del server tramite /models e registra quale modello l'esecuzione interroga e come legge i box del modello. Il passaggio di predizione invia poi un'immagine per richiesta. Quando l'esecuzione termina, la mAP50 held-out compare nella tabella Runs, e la pagina dell'esecuzione elenca val_map_50, test_map_50, i valori di copertura e un avviso "Not used" che conta le risposte che non erano JSON, i box con etichette che lo schema non ha e i box fuori dall'immagine. Quando il punteggio è vicino a 0 mentre la copertura è alta e l'avviso è vuoto, controlla prima coordinate_format e box_order.

Modelli Roboflow

roboflow invia ogni immagine con POST a <url>/<model_id>, dove model_id è il progetto e la versione come project/version. La chiave va per impostazione predefinita in ROBOFLOW_API_KEY (api_key_env), e l'esecuzione rifiuta di partire senza la chiave:

bash
export ROBOFLOW_API_KEY=...   # in the shell that starts Potato
potato start config.yaml -p 9000

L'url predefinito di Potato è https://detect.roboflow.com, e la configurazione qui sopra indica quello predefinito. Un'esecuzione per la segmentazione di istanze imposta url: https://outline.roboflow.com. url è un parametro, quindi un'esecuzione può anche indicare https://serverless.roboflow.com, l'host che il riferimento sul rilevamento di oggetti di Roboflow riporta per la stessa route project/version. Il team di Potato non ha provato quell'host con questo bridge. Un server Roboflow Inference self-hosted ascolta per impostazione predefinita sulla porta 9001 (url: http://localhost:9001).

L'API di Roboflow riceve la chiave nella stringa di query. Potato non scrive l'URL della richiesta nel log dell'esecuzione né in un messaggio di errore. Potato invia confidence e overlap (valori predefiniti 0.4 e 0.3) all'API come percentuali, e il riferimento di Roboflow documenta entrambi su una scala da 0 a 100. shapes: auto scrive un contorno dove il modello restituisce un contorno e lo schema ha uno strumento poligono, e un box altrimenti. Il controllo di integrità invia un'immagine bianca di 8 × 8, e Roboflow conta la richiesta nell'utilizzo dell'account.

Funzioni serverless di CVAT

cvat-nuclio usa un detector già distribuito per CVAT come funzione Nuclio, come le funzioni YOLOv7 o Faster R-CNN in cvat/serverless. Potato invia ogni immagine con POST all'URL proprio della funzione come {"image": <base64>, "threshold": t}, senza passare da CVAT. nuctl get functions elenca le funzioni distribuite con le loro porte, e il comando seguente è la forma riportata dalla documentazione di CVAT:

bash
nuctl get functions --platform local

La funzione risponde con forme in pixel. Un rectangle diventa un box, e un polygon diventa un poligono, o il box di delimitazione del poligono quando lo schema non ha uno strumento poligono. Le forme mask vengono escluse e contate. La funzione scarta le forme con punteggio inferiore a threshold (predefinito 0.5). Lo schema non ha un'etichetta person, quindi label_map: {person: null} nella configurazione scarta le persone di un detector COCO.

Modelli Hugging Face con hf-inference

hf-inference chiama un modello distribuito su Hugging Face, sia un Inference Endpoint dedicato tramite il suo URL https:// (endpoint_url), sia l'id di un modello sulla HF Inference API serverless (model). Imposta endpoint_url o model, e non entrambi. Il compito segue il tipo dello schema:

  • Classificazione di testo per radio, select e multiselect. Uno schema a scelta singola prende l'etichetta migliore del modello. Quando quell'etichetta non è una delle etichette dello schema dopo label_map, l'item non riceve predizione anziché la seconda classificata. Uno schema multiselect prende ogni etichetta con punteggio di almeno threshold (predefinito 0.5).
  • Classificazione di token per span, con i word piece raggruppati in entità tramite aggregation_strategy (simple, first, average o max) e gli offset di carattere presi dal modello.
  • Rilevamento di oggetti per image_annotation. I box con punteggio inferiore a threshold vengono scartati.

La configurazione seguente chiede a dslim/bert-base-NER gli span. La model card descrive un modello BERT sottoposto a fine-tuning per le entità nominate sull'inglese di CoNLL-2003. HF Inference serve un sottoinsieme dei modelli dell'Hub, quindi controlla che la pagina del modello elenchi HF Inference prima di affidarti all'API serverless.

yaml
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}

Le entità PER, ORG e LOC del modello vengono associate alle etichette dello schema, e MISC viene scartata perché lo schema non ha un'etichetta simile. Quando il nome completo di un'entità non corrisponde a nulla, Potato elimina un prefisso B- o I- e riprova. Un classificatore di sentiment che risponde LABEL_0 e LABEL_1 richiede nello stesso modo label_map: {LABEL_0: negative, LABEL_1: positive}. Il controllo di integrità è una richiesta reale, una frase breve o un'immagine di 8 × 8, e wait_for_model: true (il valore predefinito) attende mentre un modello serverless si carica anziché fallire con un 503.

Un backend ML di Label Studio

label-studio-ml usa un backend ML di Label Studio in esecuzione. Un backend avviato dagli esempi del repository con docker-compose up serve un modello via HTTP su http://localhost:9090. L'esecuzione controlla il GET /health del backend, invia gli item a POST /predict in batch da 16 (batch_size) e legge le regioni restituite con lo stesso convertitore usato da potato import -f label_studio. Gli offset di span UTF-16 di Label Studio diventano code point, e le coordinate in percentuale diventano quelle del widget immagine.

yaml
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

L'unica autenticazione di un backend standard è HTTP Basic, impostata da BASIC_AUTH_USER e BASIC_AUTH_PASS sul backend. basic_auth_user_env e basic_auth_pass_env nominano le variabili sul server Potato che contengono gli stessi due valori, e i due parametri vanno insieme. api_key_env invia invece Authorization: Token <value>, per un backend dietro un proxy che si aspetta un token. Un backend standard risponde a quell'intestazione con un 500. Potato genera dallo schema la configurazione di etichettatura inviata con ogni richiesta (Choices, Labels, RectangleLabels o PolygonLabels), e label_config sostituisce l'XML generato con il tuo.

Un backend per immagini richiede un'altra impostazione. Senza media_url_prefix, Potato invia ogni immagine come percorso assoluto sulla macchina Potato, e un backend costruito sull'SDK di Label Studio legge data.image come un URL e fallisce con un percorso semplice. Per un backend sulla stessa macchina, aggiungi il prefisso seguente e avvia il backend con LOCAL_FILES_DOCUMENT_ROOT impostato alla cartella del task:

yaml
model_training:
  trainer_params:
    label-studio-ml:
      url: http://localhost:9090
      media_url_prefix: /data/local-files/?d=

Per un backend su un'altra macchina, servi la cartella del task via HTTP e indica l'URL di quel server come prefisso. Un'immagine fuori dalla cartella del task non ha un percorso relativo alla cartella, quindi Potato non invia l'immagine, e il log dell'esecuzione indica quante ne sono rimaste fuori.

Potato invia al backend richieste di predizione e nessun evento di annotazione, quindi l'addestramento proprio del backend nel suo gestore /webhook non parte da Potato. potato import -f label_studio sposta in Potato un intero progetto Label Studio con la configurazione di etichettatura del progetto, le risposte degli annotatori e le predizioni memorizzate (Importare progetti). Migrating from Label Studio to Potato tratta i tag che l'importatore lascia fuori.

Rivedere le pre-etichette

Le predizioni di un bridge passano attraverso gli stessi controlli di write-back di quelle di un modello addestrato. Il write-back salta gli item della partizione train e gli schemi assenti da writeback.schemas, salta le coppie item-schema che un revisore ha rifiutato e trattiene le predizioni sotto writeback.min_confidence. Per box e span, la confidenza di una predizione è la confidenza della forma o dello span meno confidente, quindi la soglia trattiene l'intera predizione di un item quando un qualsiasi box o span dell'item ottiene un punteggio inferiore. Il confidence di Roboflow e il threshold di CVAT agiscono invece per box, presso il servizio, e il threshold di hf-inference agisce per box o per etichetta multiselect. Il tutorial sul modello nel ciclo tratta la coda di revisione, i verdetti accept e reject e il riaddestramento. La coda e i verdetti funzionano allo stesso modo per un bridge.

Per ogni item e schema viene conservata una predizione, quindi un'esecuzione successiva di un altro bridge o di un modello addestrato sostituisce le pre-etichette dell'esecuzione precedente. Per ritirare le pre-etichette di un'esecuzione, invia DELETE /admin/training/api/runs/<run_id>/predictions. Il riferimento dei bridge di inferenza elenca ogni parametro dei quattro bridge, e il riferimento di label-studio-ml elenca i parametri di Label Studio.

Ulteriori letture