Aller au contenu

Pré-étiquettes et scores sur items réservés à partir d'un modèle que vous exécutez déjà

Évaluez sur les étiquettes de votre étude un modèle servi par vLLM, Ollama, Hugging Face, Roboflow, CVAT ou un backend ML de Label Studio, puis relisez ses réponses sous forme de pré-étiquettes.

Nouveauté de la v2.14.0

Ce tutoriel s'adresse aux équipes qui exécutent déjà un modèle hors de Potato et veulent savoir dans quelle mesure ce modèle s'accorde avec leurs propres annotateurs avant de montrer ses réponses à qui que ce soit. L'entraînement de modèles de Potato comprend cinq modules d'entraînement, appelés passerelles, qui n'entraînent rien. Une passerelle envoie les items réservés de l'étude à un modèle servi ailleurs, évalue les réponses d'après les étiquettes de l'étude et les écrit en retour sous forme de pré-étiquettes à relire, comme le fait l'exécution d'un modèle entraîné. Les étapes ci-dessous couvrent un modèle vision-langage derrière une API compatible OpenAI, un modèle Roboflow, une fonction serverless CVAT, un modèle Hugging Face et un backend ML de Label Studio, avec une configuration validée pour chacun.

Ce qu'une exécution de passerelle envoie et évalue

Une passerelle est un module d'entraînement dont l'étape d'ajustement se borne à vérifier que le service répond. L'exécution envoie ensuite au service les items des partitions val et test de l'exécution, et avec writeback.include_unlabeled: true aussi les items que personne n'a étiquetés. Potato transforme les réponses en formes propres au schéma, empans ou choix, et les évalue sur les items réservés d'après les étiquettes humaines résolues, avec les mêmes métriques sur items réservés que celles d'un modèle entraîné (mAP50 pour les boîtes, F1 d'empans pour les empans, F1 macro pour les choix uniques). L'écriture en retour, la file de relecture et l'identifiant d'exécution de chaque pré-étiquette fonctionnent comme après toute autre exécution.

ModuleSchémasLe modèle qu'il appelle
openai-vlm-boxesimage_annotationUn modèle vision-langage derrière une API de chat compatible OpenAI (vLLM, Ollama, LM Studio, le serveur de llama.cpp, OpenAI)
roboflowimage_annotationUn modèle Roboflow de détection ou de segmentation d'instances, hébergé ou sur un serveur Inference auto-hébergé
cvat-nuclioimage_annotationUne fonction serverless CVAT qui tourne sur Nuclio
hf-inferenceradio, select, multiselect, span, image_annotationUn Inference Endpoint Hugging Face ou un modèle sur l'API HF Inference serverless
label-studio-mlradio, select, multiselect, span, image_annotationUn backend ML de Label Studio en cours d'exécution

La partition d'un item ne dépend que de l'identifiant de l'item et de model_training.split_seed, de sorte qu'une exécution de passerelle et l'exécution d'un modèle entraîné sont évaluées sur les mêmes items val et test tant que la graine reste fixe. Le tableau Runs de /admin/training place alors un modèle que vous avez déjà à côté d'un modèle entraîné sur les étiquettes de l'étude. Les items de la partition train ne reçoivent aucune prédiction d'une passerelle, et l'écriture en retour ne pré-étiquette jamais un item de la partition train. Avec la partition par défaut de 70/15/15, une passerelle est donc évaluée sur environ 30 % des items étiquetés.

Avant de commencer

Les quatre passerelles autres que label-studio-ml demandent requests et Pillow sur le serveur Potato, et la page Training liste une passerelle comme « not installed » tant que les deux paquets ne s'importent pas. label-studio-ml ne demande rien de plus que Potato. Aucune des cinq passerelles n'installe de modèle, de bibliothèque GPU ni torch, car le modèle tourne dans le service.

Les images doivent être des fichiers locaux dans le projet, comme pour tout module d'entraînement sur les images. Une passerelle exclut un item dont l'image est une URL http(s) distante, et un item qui a une étiquette et pas de prédiction compte comme faux dans le score sur items réservés. L'exécution rapporte la part des items réservés qui ont reçu une prédiction sous les noms val_coverage et test_coverage, de sorte qu'une faible couverture indique des images que la passerelle n'a pas pu envoyer.

Les clés restent dans des variables d'environnement

Une clé ne figure jamais dans la configuration. Chaque passerelle qui envoie une clé prend un paramètre qui nomme la variable d'environnement contenant la clé (api_key_env, token_env pour Hugging Face, ou les deux noms basic_auth_*_env pour Label Studio), et la variable doit être définie dans l'environnement du serveur Potato avant le démarrage du serveur. cvat-nuclio n'envoie aucune clé et ne prend pas de tel paramètre. Les quatre passerelles qui prennent une clé refusent une valeur qui n'est pas un nom de variable, et l'erreur ne répète pas la valeur, car une valeur à cet endroit est le plus probablement la clé elle-même. Lorsque le corps d'erreur d'un service ou une erreur de connexion répète la requête, les passerelles remplacent la valeur de la clé par [redacted] avant d'afficher l'erreur. Les corps d'erreur du /predict d'un backend ML de Label Studio sont nettoyés de la même façon. Tout ce qui a la forme d'une clé dans une chaîne de requête ou dans un en-tête Authorization est également remplacé.

Les exécutions hf-inference et roboflow sont uniquement locales. Les deux passerelles lisent la clé sur le serveur Potato, et une exécution sur un backend ssh, SLURM ou agent devrait envoyer la clé à cet hôte, de sorte que Potato refuse de lancer une telle exécution à cet endroit. openai-vlm-boxes, cvat-nuclio et label-studio-ml s'exécutent sur tout backend qui peut joindre le service, avec la variable de la clé définie sur l'hôte de ce backend.

Étape 1 : une étude d'images avec trois passerelles

La configuration ci-dessous met en place une étude de photos de chantier dans laquelle les annotateurs encadrent chaque casque de chantier et chaque tête nue. model_training.trainer_params contient les valeurs par défaut de plusieurs modules à la fois, de sorte qu'une seule configuration porte les réglages des trois passerelles d'images, et chaque exécution utilise le bloc de son propre module. Le champ Parameters de la page Training se remplit à partir de ces valeurs par défaut lorsque vous choisissez un module.

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}

Chaque ligne de data/photos.jsonl nomme une image sous media/ :

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

writeback.schemas est ce qui rend les boîtes du modèle visibles aux annotateurs, et include_unlabeled envoie aussi au service les photos que personne n'a étiquetées. Omettez les deux clés pour la première exécution si vous voulez le score sur items réservés avant que quiconque voie une pré-étiquette. label_map fait correspondre les noms de classes du service aux étiquettes du schéma, et une classe associée à null est écartée. Une classe sans entrée va vers l'étiquette du même nom, et la correspondance ignore la casse. Les boîtes de toute autre classe sont exclues, et le journal de l'exécution compte les boîtes exclues.

Étape 2 : un modèle vision-langage avec openai-vlm-boxes

openai-vlm-boxes réduit chaque image à 1 280 pixels au plus sur le côté le plus long (max_image_side) et envoie l'image à la route des complétions de chat avec une consigne qui nomme les étiquettes du schéma. Potato lit la réponse comme des boîtes en JSON. Tout serveur doté d'une API compatible OpenAI convient. La commande ci-dessous sert Qwen2.5-VL avec vLLM, dont la documentation connecte les clients à http://localhost:8000/v1 :

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

Avec vLLM sur le port 8000, démarrez Potato sur un autre port :

bash
potato start config.yaml -p 9000

Les modèles vision-langage répondent dans leur propre convention de coordonnées quelle que soit la demande de la consigne, et deux paramètres indiquent à Potato comment lire les nombres. coordinate_format vaut fraction (0 à 1 de l'image), per_mille (0 à 1 000) ou pixels de l'image telle qu'envoyée. box_order vaut xyxy pour [x0, y0, x1, y1] ou yxyx pour [y0, x0, y1, x1]. Qwen2.5-VL répond en pixels, de sorte que la configuration ci-dessus fixe coordinate_format: pixels. Avec auto, Potato devine le format une fois par réponse d'après la taille des nombres. Les valeurs jusqu'à 1,05 se lisent comme des fractions, les valeurs au-delà de la taille de l'image envoyée se lisent comme des pour-mille, et tout le reste se lit comme des pixels. Une boîte en pour-mille qui tient dans une grande image se lit donc comme des pixels, de sorte qu'il faut nommer le format dès que vous connaissez la convention du modèle.

Gemma et Gemini répondent par des boîtes en pour-mille avec y en premier, [ymin, xmin, ymax, xmax], même lorsque la consigne demande des fractions. Avec les deux paramètres sur auto, Potato lit les boîtes d'un modèle dont le nom contient gemma ou gemini comme des pour-mille avec y en premier. Sur un projet de test Gemma 4 12B de carrés rouges et bleus, l'équipe de Potato a mesuré un mAP50 de 0,0 avec les boîtes lues x en premier et de 1,0 avec les boîtes lues y en premier. Ollama sert Gemma 3 sous un nom qui contient gemma, de sorte que auto s'applique :

bash
ollama run gemma3:12b

Remplacez le bloc openai-vlm-boxes de la configuration par le bloc ci-dessous :

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

Un affinage de Gemma ou de Gemini servi sous un nom sans aucun de ces mots reçoit la lecture x en premier avec auto. Fixez coordinate_format: per_mille et box_order: yxyx pour un tel modèle. json_mode: object demande à Ollama du JSON avec response_format: json_object. La valeur par défaut, schema, envoie un schéma JSON avec les étiquettes comme énumération. vLLM et OpenAI respectent le schéma. Une exécution dans laquelle aucune réponse ne s'analyse comme du JSON échoue, et l'erreur suggère json_mode: object ou une consigne plus simple (prompt, où {labels} devient les étiquettes du schéma).

Un modèle vision-langage ne donne aucun score avec ses boîtes, de sorte que les boîtes n'ont aucune confiance. writeback.min_confidence laisse passer toutes les prédictions d'openai-vlm-boxes, et la relecture des modèles ne peut pas ordonner les prédictions de la moins sûre à la plus sûre. La référence d'openai-vlm-boxes liste api_key_env pour une API hébergée ainsi que les réglages de jeton et de délai.

Étape 3 : lancer une exécution et lire le score

Ouvrez http://localhost:9000/admin/training, choisissez openai-vlm-boxes et le schéma ppe, puis cliquez sur Train now. La même exécution se lance depuis l'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"]}'

L'étape d'ajustement liste les modèles du serveur par /models et consigne quel modèle l'exécution interroge et comment l'exécution lit les boîtes du modèle. La passe de prédiction envoie ensuite une image par requête. Lorsque l'exécution se termine, le mAP50 sur items réservés apparaît dans le tableau Runs, et la page de l'exécution liste val_map_50, test_map_50, les chiffres de couverture et un avertissement « Not used » qui compte les réponses qui n'étaient pas du JSON, les boîtes sous des étiquettes que le schéma n'a pas et les boîtes hors de l'image. Lorsque le score est proche de 0 alors que la couverture est élevée et que l'avertissement est vide, vérifiez d'abord coordinate_format et box_order.

Modèles Roboflow

roboflow envoie chaque image par POST à <url>/<model_id>, où model_id est le projet et la version sous la forme project/version. La clé se place par défaut dans ROBOFLOW_API_KEY (api_key_env), et l'exécution refuse de démarrer sans la clé :

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

L'url par défaut de Potato est https://detect.roboflow.com, et la configuration ci-dessus nomme la valeur par défaut. Une exécution de segmentation d'instances définit url: https://outline.roboflow.com. url est un paramètre, de sorte qu'une exécution peut aussi nommer https://serverless.roboflow.com, l'hôte que donne la référence de détection d'objets de Roboflow pour la même route project/version. L'équipe de Potato n'a pas testé cet hôte avec cette passerelle. Un serveur Roboflow Inference auto-hébergé écoute par défaut sur le port 9001 (url: http://localhost:9001).

L'API de Roboflow prend la clé dans la chaîne de requête. Potato n'écrit pas l'URL de la requête dans le journal de l'exécution ni dans un message d'erreur. Potato envoie confidence et overlap (valeurs par défaut 0,4 et 0,3) à l'API sous forme de pourcentages, et la référence de Roboflow documente les deux sur une échelle de 0 à 100. shapes: auto écrit un contour lorsque le modèle renvoie un contour et que le schéma a un outil polygone, et une boîte dans le cas contraire. La vérification d'état envoie une image blanche de 8 × 8, et Roboflow compte la requête dans l'utilisation du compte.

Fonctions serverless CVAT

cvat-nuclio utilise un détecteur déjà déployé pour CVAT comme fonction Nuclio, comme les fonctions YOLOv7 ou Faster R-CNN de cvat/serverless. Potato envoie chaque image à l'URL propre de la fonction sous la forme {"image": <base64>, "threshold": t}, sans passer par CVAT. nuctl get functions liste les fonctions déployées avec leurs ports, et la commande ci-dessous est la forme que donne la documentation de CVAT :

bash
nuctl get functions --platform local

La fonction répond avec des formes en pixels. Un rectangle devient une boîte, et un polygon devient un polygone, ou la boîte englobante du polygone lorsque le schéma n'a pas d'outil polygone. Les formes mask sont exclues et comptées. La fonction écarte les formes dont le score est inférieur à threshold (0,5 par défaut). Le schéma n'a pas d'étiquette de personne, de sorte que label_map: {person: null} dans la configuration écarte les personnes d'un détecteur COCO.

Modèles Hugging Face avec hf-inference

hf-inference appelle un modèle déployé sur Hugging Face, soit un Inference Endpoint dédié par son URL https:// (endpoint_url), soit un identifiant de modèle sur l'API HF Inference serverless (model). Définissez endpoint_url ou model, mais pas les deux. La tâche suit le type du schéma :

  • Classification de texte pour radio, select et multiselect. Un schéma à choix unique prend l'étiquette de tête du modèle. Lorsque cette étiquette ne figure pas parmi les étiquettes du schéma après label_map, l'item ne reçoit aucune prédiction plutôt que la deuxième. Un schéma multiselect prend chaque étiquette dont le score atteint au moins threshold (0,5 par défaut).
  • Classification de jetons pour span, avec les morceaux de mots regroupés en entités par aggregation_strategy (simple, first, average ou max) et les décalages de caractères pris au modèle.
  • Détection d'objets pour image_annotation. Les boîtes dont le score est inférieur à threshold sont écartées.

La configuration ci-dessous interroge dslim/bert-base-NER pour des empans. La fiche du modèle le décrit comme un modèle BERT affiné pour les entités nommées sur le CoNLL-2003 en anglais. HF Inference sert un sous-ensemble des modèles du Hub, de sorte qu'il faut vérifier que la page du modèle liste HF Inference avant de compter sur l'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}

Les entités PER, ORG et LOC du modèle correspondent aux étiquettes du schéma, et MISC est écartée car le schéma n'a pas une telle étiquette. Lorsque le nom complet d'une entité ne correspond à rien, Potato retire un préfixe B- ou I- et réessaie. Un classifieur de sentiment qui répond LABEL_0 et LABEL_1 demande de la même façon label_map: {LABEL_0: negative, LABEL_1: positive}. La vérification d'état est une vraie requête, une courte phrase ou une image de 8 × 8, et wait_for_model: true (la valeur par défaut) attend pendant le chargement d'un modèle serverless au lieu d'échouer avec une erreur 503.

Un backend ML de Label Studio

label-studio-ml utilise un backend ML de Label Studio en cours d'exécution. Un backend démarré à partir des exemples du dépôt avec docker-compose up sert un modèle en HTTP à http://localhost:9090. L'exécution vérifie le GET /health du backend, envoie les items à POST /predict par lots de 16 (batch_size) et lit les régions renvoyées avec le même convertisseur que celui de potato import -f label_studio. Les décalages d'empans en UTF-16 de Label Studio deviennent des points de code, et les coordonnées en pourcentage deviennent celles du widget d'image.

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

La seule authentification d'un backend standard est HTTP Basic, définie par BASIC_AUTH_USER et BASIC_AUTH_PASS sur le backend. basic_auth_user_env et basic_auth_pass_env nomment les variables du serveur Potato qui contiennent ces deux mêmes valeurs, et les deux paramètres vont ensemble. api_key_env envoie plutôt Authorization: Token <value>, pour un backend derrière un proxy qui attend un jeton. Un backend standard répond à cet en-tête par une erreur 500. Potato génère la configuration d'étiquetage envoyée avec chaque requête à partir du schéma (Choices, Labels, RectangleLabels ou PolygonLabels), et label_config remplace le XML généré par le vôtre.

Un backend d'images demande un réglage de plus. Sans media_url_prefix, Potato envoie chaque image comme un chemin absolu sur la machine de Potato, et un backend construit sur le SDK de Label Studio lit data.image comme une URL et échoue sur un chemin nu. Pour un backend sur la même machine, ajoutez le préfixe ci-dessous et démarrez le backend avec LOCAL_FILES_DOCUMENT_ROOT défini sur le répertoire de la tâche :

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

Pour un backend sur une autre machine, servez le répertoire de la tâche en HTTP et donnez l'URL de ce serveur comme préfixe. Une image hors du répertoire de la tâche n'a pas de chemin relatif au répertoire, de sorte que Potato n'envoie pas l'image, et le journal de l'exécution indique combien ont été exclues.

Potato envoie au backend des requêtes de prédiction et aucun événement d'annotation, de sorte que l'entraînement propre au backend dans son gestionnaire /webhook ne démarre pas depuis Potato. potato import -f label_studio déplace tout un projet Label Studio dans Potato avec la configuration d'étiquetage du projet, les réponses des annotateurs et les prédictions stockées (Importer des projets). Migrer de Label Studio vers Potato traite des balises que l'importateur laisse de côté.

Relire les pré-étiquettes

Les prédictions d'une passerelle passent par les mêmes vérifications d'écriture en retour que celles d'un modèle entraîné. L'écriture en retour ignore les items de la partition train et les schémas absents de writeback.schemas, ignore les paires item-schéma qu'un relecteur a rejetées et retient les prédictions sous writeback.min_confidence. Pour les boîtes et les empans, la confiance d'une prédiction est celle de la forme ou de l'empan le moins sûr, de sorte que le seuil retient toute la prédiction d'un item lorsqu'une seule boîte ou un seul empan de l'item obtient un score plus bas. Le confidence de Roboflow et le threshold de CVAT agissent plutôt par boîte, au niveau du service, et le threshold de hf-inference agit par boîte ou par étiquette multiselect. Le tutoriel du modèle dans la boucle traite de la file de relecture, des verdicts d'acceptation et de rejet et du réentraînement. La file et les verdicts fonctionnent de la même façon pour une passerelle.

Une seule prédiction est conservée par item et par schéma, de sorte que l'exécution ultérieure d'une autre passerelle ou d'un modèle entraîné remplace les pré-étiquettes de l'exécution précédente. Pour retirer les pré-étiquettes d'une exécution, envoyez DELETE /admin/training/api/runs/<run_id>/predictions. La référence des passerelles d'inférence liste chaque paramètre des quatre passerelles, et la référence de label-studio-ml liste les paramètres de Label Studio.

Pour aller plus loin