Ir al contenido

Preetiquetas y puntuaciones sobre ítems reservados de un modelo que ya ejecutas

Puntúa con las etiquetas de tu estudio un modelo servido con vLLM, Ollama, Hugging Face, Roboflow, CVAT o un backend de ML de Label Studio, y revisa después sus respuestas como preetiquetas.

Novedad de la v2.14.0

Este tutorial está dirigido a equipos que ya ejecutan un modelo fuera de Potato y quieren saber en qué medida el modelo coincide con los propios anotadores del equipo antes de mostrar sus respuestas a nadie. El entrenamiento de modelos de Potato incluye cinco entrenadores, llamados puentes, que no entrenan nada. Un puente envía los ítems reservados del estudio a un modelo servido en otro lugar, puntúa las respuestas con las etiquetas del estudio y escribe de vuelta las respuestas como preetiquetas para su revisión, como hace la ejecución de un modelo entrenado. Los pasos siguientes cubren un modelo de visión y lenguaje detrás de una API compatible con OpenAI, un modelo de Roboflow, una función sin servidor de CVAT, un modelo de Hugging Face y un backend de ML de Label Studio, con una configuración validada para cada uno.

Qué envía y puntúa la ejecución de un puente

Un puente es un entrenador cuyo paso de ajuste solo comprueba que el servicio responde. La ejecución envía entonces al servicio los ítems de las particiones val y test de la ejecución y, con writeback.include_unlabeled: true, también los ítems que nadie ha etiquetado. Potato convierte las respuestas en las formas propias del esquema, spans o elecciones, y puntúa las respuestas sobre los ítems reservados frente a las etiquetas humanas resueltas, con las mismas métricas sobre ítems reservados que recibe un modelo entrenado (mAP50 para cajas, F1 de spans para spans, macro-F1 para elecciones únicas). La escritura de vuelta, la cola de revisión y el identificador de ejecución de cada preetiqueta funcionan como tras cualquier otra ejecución.

EntrenadorEsquemasEl modelo al que llama
openai-vlm-boxesimage_annotationUn modelo de visión y lenguaje detrás de una API de chat compatible con OpenAI (vLLM, Ollama, LM Studio, el servidor de llama.cpp, OpenAI)
roboflowimage_annotationUn modelo de detección o de segmentación de instancias de Roboflow, alojado o en un servidor Inference autoalojado
cvat-nuclioimage_annotationUna función sin servidor de CVAT que se ejecuta en Nuclio
hf-inferenceradio, select, multiselect, span, image_annotationUn Hugging Face Inference Endpoint o un modelo en la API HF Inference sin servidor
label-studio-mlradio, select, multiselect, span, image_annotationUn backend de ML de Label Studio en ejecución

La partición de un ítem depende solo del identificador del ítem y de model_training.split_seed, así que la ejecución de un puente y la de un modelo entrenado puntúan sobre los mismos ítems val y test mientras la semilla se mantenga fija. La tabla de ejecuciones de /admin/training pone entonces un modelo que ya tienes junto a un modelo entrenado con las etiquetas del estudio. Los ítems de la partición train no reciben predicción de un puente, y la escritura de vuelta nunca preetiqueta un ítem de la partición train. Con la partición predeterminada de 70/15/15, un puente puntúa por tanto aproximadamente sobre el 30 % de los ítems etiquetados.

Antes de empezar

Los cuatro puentes distintos de label-studio-ml necesitan requests y Pillow en el servidor de Potato, y la página Training lista un puente como «not installed» hasta que ambos paquetes se importan. label-studio-ml no necesita nada más allá de Potato. Ninguno de los cinco puentes instala un modelo, una biblioteca de GPU ni torch, porque el modelo se ejecuta en el servicio.

Las imágenes deben ser archivos locales dentro del proyecto, como en todos los entrenadores de imágenes. Un puente deja fuera un ítem cuya imagen es una URL http(s) remota, y un ítem con etiqueta y sin predicción cuenta como erróneo en la puntuación sobre ítems reservados. La ejecución notifica la proporción de ítems reservados que recibieron una predicción como val_coverage y test_coverage, así que una cobertura baja apunta a imágenes que el puente no pudo enviar.

Las claves se quedan en variables de entorno

Una clave nunca va en la configuración. Cada puente que envía una clave admite un parámetro que nombra la variable de entorno que contiene la clave (api_key_env, token_env para Hugging Face o los dos nombres basic_auth_*_env para Label Studio), y la variable debe estar definida en el entorno del servidor de Potato antes de que el servidor arranque. cvat-nuclio no envía ninguna clave y no admite ese parámetro. Los cuatro puentes que admiten una clave rechazan un valor que no es un nombre de variable, y el error no repite el valor, ya que un valor en ese lugar es con toda probabilidad la propia clave. Cuando el cuerpo de error de un servicio o un error de conexión reproduce la solicitud, los puentes sustituyen el valor de la clave por [redacted] antes de mostrar el error. Los cuerpos de error del /predict de un backend de ML de Label Studio se limpian del mismo modo. También se sustituye cualquier cosa con forma de clave en una cadena de consulta o en una cabecera Authorization.

Las ejecuciones de hf-inference y roboflow son solo locales. Ambos puentes leen la clave en el servidor de Potato, y una ejecución en un backend ssh, SLURM o agent tendría que enviar la clave a ese host, así que Potato se niega a iniciar una ejecución así allí. openai-vlm-boxes, cvat-nuclio y label-studio-ml se ejecutan en cualquier backend que pueda alcanzar el servicio, con la variable de la clave definida en el host de ese backend.

Paso 1: un estudio de imágenes con tres puentes

La siguiente configuración prepara un estudio de fotos de obra en el que los anotadores enmarcan cada casco y cada cabeza descubierta. model_training.trainer_params contiene valores predeterminados para varios entrenadores a la vez, así que una sola configuración lleva los ajustes de los tres puentes de imágenes, y cada ejecución usa el bloque de su propio entrenador. El campo Parameters de la página Training se rellena con estos valores predeterminados cuando eliges un entrenador.

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}

Cada línea de data/photos.jsonl nombra una imagen bajo media/:

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

writeback.schemas es lo que hace visibles las cajas del modelo para los anotadores, y include_unlabeled envía también al servicio las fotos que nadie ha etiquetado. Omite ambas claves en la primera ejecución si quieres la puntuación sobre ítems reservados antes de que alguien vea una preetiqueta. label_map asigna los nombres de clase del servicio a las etiquetas del esquema, y una clase asignada a null se descarta. Una clase sin entrada va a la etiqueta del mismo nombre, y la coincidencia ignora las mayúsculas. Las cajas con cualquier otra clase quedan fuera, y el registro de la ejecución cuenta las cajas descartadas.

Paso 2: un modelo de visión y lenguaje con openai-vlm-boxes

openai-vlm-boxes reduce cada imagen a un máximo de 1280 píxeles en el lado más largo (max_image_side) y envía la imagen a la ruta de chat completions con un prompt que nombra las etiquetas del esquema. Potato lee la respuesta como cajas en JSON. Funciona cualquier servidor con una API compatible con OpenAI. El siguiente comando sirve Qwen2.5-VL con vLLM, cuya documentación conecta los clientes en http://localhost:8000/v1:

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

Con vLLM en el puerto 8000, inicia Potato en otro puerto:

bash
potato start config.yaml -p 9000

Los modelos de visión y lenguaje responden con su propia convención de coordenadas, sea cual sea lo que pida el prompt, y dos parámetros indican a Potato cómo leer los números. coordinate_format es fraction (de 0 a 1 de la imagen), per_mille (de 0 a 1000) o pixels de la imagen tal como se envió. box_order es xyxy para [x0, y0, x1, y1] o yxyx para [y0, x0, y1, x1]. Qwen2.5-VL responde en píxeles, así que la configuración anterior fija coordinate_format: pixels. Con auto, Potato adivina el formato una vez por respuesta a partir del tamaño de los números. Los valores de hasta 1,05 se leen como fracciones, los valores que superan el tamaño de la imagen enviada se leen como por mil y cualquier otro se lee como píxeles. Una caja por mil que cabe dentro de una imagen grande se lee por tanto como píxeles, así que nombra el formato siempre que conozcas la convención del modelo.

Gemma y Gemini responden con cajas por mil con la y primero, [ymin, xmin, ymax, xmax], aunque el prompt pida fracciones. Con ambos parámetros en auto, Potato lee las cajas de un modelo cuyo nombre contiene gemma o gemini como por mil con la y primero. En un proyecto de prueba con Gemma 4 12B y cuadrados rojos y azules, el equipo de Potato midió un mAP50 de 0,0 leyendo las cajas con la x primero y de 1,0 leyéndolas con la y primero. Ollama sirve Gemma 3 con un nombre que contiene gemma, así que se aplica auto:

bash
ollama run gemma3:12b

Sustituye el bloque openai-vlm-boxes de la configuración por el bloque siguiente:

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

Un ajuste fino de Gemma o de Gemini servido con un nombre sin ninguna de las dos palabras recibe la lectura con la x primero con auto. Fija coordinate_format: per_mille y box_order: yxyx para un modelo así. json_mode: object pide a Ollama JSON con response_format: json_object. El valor predeterminado, schema, envía un esquema JSON con las etiquetas como enumeración. vLLM y OpenAI respetan el esquema. Una ejecución en la que ninguna respuesta se analiza como JSON falla, y el error sugiere json_mode: object o un prompt más sencillo (prompt, donde {labels} se convierte en las etiquetas del esquema).

Un modelo de visión y lenguaje no da ninguna puntuación con sus cajas, así que las cajas no llevan confianza. writeback.min_confidence deja pasar todas las predicciones de openai-vlm-boxes, y la revisión del modelo no puede ordenar las predicciones de menor a mayor confianza. La referencia de openai-vlm-boxes lista api_key_env para una API alojada y los ajustes de token y de tiempo de espera.

Paso 3: iniciar una ejecución y leer la puntuación

Abre http://localhost:9000/admin/training, elige openai-vlm-boxes y el esquema ppe y pulsa Train now. La misma ejecución se inicia desde la 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"]}'

El paso de ajuste lista los modelos del servidor mediante /models y registra qué modelo solicita la ejecución y cómo lee las cajas del modelo. La pasada de predicción envía después una imagen por solicitud. Cuando la ejecución termina, el mAP50 sobre ítems reservados aparece en la tabla de ejecuciones, y la página de la ejecución lista val_map_50, test_map_50, las cifras de cobertura y una advertencia «Not used» que cuenta las respuestas que no eran JSON, las cajas con etiquetas que el esquema no tiene y las cajas fuera de la imagen. Cuando la puntuación es cercana a 0 mientras la cobertura es alta y la advertencia está vacía, comprueba primero coordinate_format y box_order.

Modelos de Roboflow

roboflow envía por POST cada imagen a <url>/<model_id>, donde model_id es el proyecto y la versión como project/version. La clave va por defecto en ROBOFLOW_API_KEY (api_key_env), y la ejecución se niega a iniciarse sin la clave:

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

La url predeterminada de Potato es https://detect.roboflow.com, y la configuración anterior nombra la predeterminada. Una ejecución de segmentación de instancias fija url: https://outline.roboflow.com. url es un parámetro, así que una ejecución también puede nombrar https://serverless.roboflow.com, el host que da la referencia de detección de objetos de Roboflow para la misma ruta project/version. El equipo de Potato no ha probado ese host con este puente. Un servidor Roboflow Inference autoalojado escucha por defecto en el puerto 9001 (url: http://localhost:9001).

La API de Roboflow recibe la clave en la cadena de consulta. Potato no escribe la URL de la solicitud en el registro de la ejecución ni en un mensaje de error. Potato envía confidence y overlap (valores predeterminados 0,4 y 0,3) a la API como porcentajes, y la referencia de Roboflow documenta ambos en una escala de 0 a 100. shapes: auto escribe un contorno cuando el modelo devuelve un contorno y el esquema tiene una herramienta de polígono, y una caja en caso contrario. La comprobación de estado envía una imagen blanca de 8 × 8, y Roboflow cuenta la solicitud en el uso de la cuenta.

Funciones sin servidor de CVAT

cvat-nuclio usa un detector ya desplegado para CVAT como función de Nuclio, como las funciones YOLOv7 o Faster R-CNN de cvat/serverless. Potato envía por POST cada imagen a la URL propia de la función como {"image": <base64>, "threshold": t}, sin pasar por CVAT. nuctl get functions lista las funciones desplegadas con sus puertos, y el siguiente comando es la forma que da la documentación de CVAT:

bash
nuctl get functions --platform local

La función responde con formas en píxeles. Un rectangle se convierte en una caja, y un polygon en un polígono, o en la caja delimitadora del polígono cuando el esquema no tiene herramienta de polígono. Las formas mask se dejan fuera y se cuentan. La función descarta las formas con puntuación inferior a threshold (0,5 por defecto). El esquema no tiene una etiqueta person, así que label_map: {person: null} en la configuración descarta las personas de un detector COCO.

Modelos de Hugging Face con hf-inference

hf-inference llama a un modelo desplegado en Hugging Face, ya sea un Inference Endpoint dedicado mediante su URL https:// (endpoint_url) o un identificador de modelo en la API HF Inference sin servidor (model). Fija endpoint_url o model, pero no ambos. La tarea sigue el tipo del esquema:

  • Clasificación de texto para radio, select y multiselect. Un esquema de elección única toma la etiqueta principal del modelo. Cuando esa etiqueta no es una de las etiquetas del esquema tras label_map, el ítem no recibe predicción en lugar de la segunda candidata. Un esquema multiselect toma cada etiqueta con una puntuación de al menos threshold (0,5 por defecto).
  • Clasificación de tokens para span, con las piezas de palabra agrupadas en entidades según aggregation_strategy (simple, first, average o max) y los desplazamientos de caracteres tomados del modelo.
  • Detección de objetos para image_annotation. Las cajas con puntuación inferior a threshold se descartan.

La siguiente configuración pide spans a dslim/bert-base-NER. La ficha del modelo describe un modelo BERT ajustado para entidades con nombre sobre CoNLL-2003 en inglés. HF Inference sirve un subconjunto de los modelos del Hub, así que comprueba que la página del modelo lista HF Inference antes de confiar en la API sin servidor.

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}

Las entidades PER, ORG y LOC del modelo se asignan a las etiquetas del esquema, y MISC se descarta porque el esquema no tiene esa etiqueta. Cuando el nombre completo de una entidad no coincide con nada, Potato quita un prefijo B- o I- y lo intenta de nuevo. Un clasificador de sentimiento que responde LABEL_0 y LABEL_1 necesita label_map: {LABEL_0: negative, LABEL_1: positive} del mismo modo. La comprobación de estado es una solicitud real, una frase corta o una imagen de 8 × 8, y wait_for_model: true (el valor predeterminado) espera mientras se carga un modelo sin servidor en lugar de fallar con un 503.

Un backend de ML de Label Studio

label-studio-ml usa un backend de ML de Label Studio en ejecución. Un backend iniciado a partir de los ejemplos del repositorio con docker-compose up sirve un modelo por HTTP en http://localhost:9090. La ejecución comprueba el GET /health del backend, envía los ítems a POST /predict en lotes de 16 (batch_size) y lee las regiones devueltas con el mismo conversor que usa potato import -f label_studio. Los desplazamientos de span UTF-16 de Label Studio se convierten en puntos de código, y las coordenadas en porcentaje se convierten en las del widget de imagen.

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 única autenticación de un backend estándar es HTTP Basic, fijada con BASIC_AUTH_USER y BASIC_AUTH_PASS en el backend. basic_auth_user_env y basic_auth_pass_env nombran las variables del servidor de Potato que contienen esos mismos dos valores, y los dos parámetros van juntos. api_key_env envía Authorization: Token <value> en su lugar, para un backend detrás de un proxy que espera un token. Un backend estándar responde a esa cabecera con un 500. Potato genera a partir del esquema la configuración de etiquetado que se envía con cada solicitud (Choices, Labels, RectangleLabels o PolygonLabels), y label_config sustituye el XML generado por el tuyo.

Un backend de imágenes necesita un ajuste más. Sin media_url_prefix, Potato envía cada imagen como una ruta absoluta en la máquina de Potato, y un backend construido sobre el SDK de Label Studio lee data.image como una URL y falla con una ruta simple. Para un backend en la misma máquina, añade el prefijo siguiente e inicia el backend con LOCAL_FILES_DOCUMENT_ROOT fijado en la carpeta de la tarea:

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

Para un backend en otra máquina, sirve la carpeta de la tarea por HTTP y da la URL de ese servidor como prefijo. Una imagen fuera de la carpeta de la tarea no tiene una ruta relativa a la carpeta, así que Potato no envía la imagen, y el registro de la ejecución indica cuántas quedaron fuera.

Potato envía al backend solicitudes de predicción y ningún evento de anotación, así que el entrenamiento propio del backend en su controlador /webhook no se inicia desde Potato. potato import -f label_studio traslada a Potato un proyecto completo de Label Studio con la configuración de etiquetado del proyecto, las respuestas de los anotadores y las predicciones almacenadas (Importar proyectos). Migrar de Label Studio a Potato cubre las etiquetas que el importador deja fuera.

Revisar las preetiquetas

Las predicciones de un puente pasan por las mismas comprobaciones de escritura de vuelta que las de un modelo entrenado. La escritura de vuelta omite los ítems de la partición train y los esquemas ausentes de writeback.schemas, omite los pares de ítem y esquema que un revisor rechazó y retiene las predicciones por debajo de writeback.min_confidence. Para cajas y spans, la confianza de una predicción es la confianza de la forma o del span menos seguro, así que el umbral retiene la predicción entera de un ítem cuando cualquier caja o span del ítem puntúa menos. El confidence de Roboflow y el threshold de CVAT actúan en cambio por caja, en el servicio, y el threshold de hf-inference actúa por caja o por etiqueta de multiselect. El tutorial del modelo en el bucle cubre la cola de revisión, los veredictos de aceptar y rechazar y el reentrenamiento. La cola y los veredictos funcionan igual para un puente.

Se conserva una predicción por ítem y esquema, así que una ejecución posterior de otro puente o de un modelo entrenado sustituye las preetiquetas de la ejecución anterior. Para retirar las preetiquetas de una ejecución, envía DELETE /admin/training/api/runs/<run_id>/predictions. La referencia de los puentes de inferencia lista todos los parámetros de los cuatro puentes, y la referencia de label-studio-ml lista los parámetros de Label Studio.

Más información