DEV Community

Cover image for Logprobs: cómo clasificar imágenes con un LLM en un solo token
lu1tr0n
lu1tr0n

Posted on Originally published at elsolitario.org

Logprobs: cómo clasificar imágenes con un LLM en un solo token

Un script de código abierto acaba de demostrar que no hace falta entrenar un clasificador de imágenes para etiquetar una webcam en tiempo real: alcanza con pedirle a un LLM que responda con una sola letra y leer qué tan seguro estaba de esa letra. El desarrollador Allan Riordan Boll publicó el 25 de septiembre de 2026 en su blog personal un wrapper inspirado en Jev que extiende esta técnica, basada en logprobs, a modelos de visión. Con Gemma 4 12B corriendo local en una RTX 3090 logra cerca de 1 fotograma por segundo con tres preguntas por frame; la misma prueba contra GPT-6-luna de OpenAI cae a unos 0,2 fps.

La técnica en sí no es nueva: OpenAI la documenta desde hace años en su cookbook de logprobs, y proyectos como Jev, OpenJev y SemIf la usan para enrutar tickets o clasificar texto sin llamar a un modelo aparte. Lo que aporta este experimento es aplicarla a imágenes, agregando un campo attachments que el formato de Jev todavía no contempla de forma oficial.

TL;DR

  • Allan Riordan Boll publicó el 25 de septiembre de 2026 un wrapper estilo Jev que suma soporte de imágenes vía logprobs.- El truco fuerza al LLM a responder con una sola letra y lee la probabilidad logarítmica de cada opción posible.- Con Gemma 4 12B local en una RTX 3090 el script procesa cerca de 1 fotograma por segundo con tres preguntas por frame.- Contra la API de OpenAI con GPT-6-luna el rendimiento cae a unos 0,2 fps por la conexión separada de cada pregunta.- Los parámetros clave de la petición son max_completion_tokens=1, logprobs=true y top_logprobs=20.- El formato admite hasta 20 criterios por pregunta, uno por cada letra disponible entre A y T.- El código es un script Python independiente que usa OpenCV solo para capturar la webcam, sin visión por computadora dedicada.- Jev, OpenJev y SemIf ya usaban logprobs para clasificar texto antes de que esta prueba lo extendiera a video.

Qué pasó

Boll leyó sobre Jev y los proyectos que fueron apareciendo alrededor, como OpenJev y SemIf, y se topó con un truco que para muchos desarrolladores de backend resulta nuevo: leer las probabilidades logarítmicas que un LLM asigna a cada token posible en su respuesta, en vez de quedarse solo con el texto generado. La idea es simple. Se arma un prompt tipo test de opción múltiple, por ejemplo Estado: el pedido llegó roto. Pregunta: qué equipo debe atender esto? A) facturación B) envíos C) devoluciones, y se le pide al modelo que responda con una sola letra. Sumando dos parámetros a la petición, max_completion_tokens en 1 y logprobs en true, el LLM devuelve la letra elegida junto con las probabilidades de las otras opciones que consideró.

Repetir esa consulta pregunta por pregunta permite construir una clasificación completa sin depender de un modelo separado ni de fine-tuning. Boll fue un paso más allá: el formato de Jev documenta hoy solo estados de texto o JSON, así que agregó un campo attachments para pasar imágenes en base64. Con eso armó un script que captura frames de su webcam, los codifica y le pregunta al modelo si hay una persona visible, si la escena es interior o exterior y qué tan brillante está, todo con un token de respuesta por pregunta.

Contexto e historia

Leer logprobs para clasificar no es un hallazgo de Boll. OpenAI documenta el patrón en su cookbook como forma de obtener confianza calibrada en tareas de clasificación sin pagar por generar una respuesta larga. La ventaja frente a pedirle al modelo un párrafo de texto es doble: la respuesta es determinista en su formato, siempre una letra, y la probabilidad asociada funciona como un puntaje de confianza que se puede umbralizar.

Jev popularizó este enfoque como alternativa liviana a entrenar un clasificador dedicado para enrutar tickets de soporte, moderar contenido o etiquetar datos. Proyectos derivados como OpenJev y SemIf replicaron la idea de forma autohospedable, pensados para equipos que no quieren depender de una API externa para decisiones simples y repetitivas. Lo que ninguno documentaba de forma oficial, hasta este experimento, era extender el mismo prompt de opción múltiple a una entrada visual: la especificación de Jev contempla estado en texto o JSON, no adjuntos binarios.

Ahí está el aporte concreto de Boll: no inventa el truco de los logprobs, pero prueba que funciona igual de bien cuando el estado es una imagen en vez de una oración, siempre que el modelo detrás sea multimodal.
Jev y sus variantes autohospedables como OpenJev llevan años usando logprobs para clasificar texto sin fine-tuning.

Detalles técnicos: los logprobs y su rendimiento real

La petición HTTP es una llamada estándar de Chat Completions con tres parámetros extra. max_completion_tokens en 1 obliga al modelo a devolver un solo token, evitando que divague en una respuesta larga. logprobs en true activa que la API devuelva, además del token elegido, su log-probabilidad. top_logprobs, con un número entre 0 y 20, pide además las alternativas más probables que el modelo consideró, aunque no las haya elegido.

import requests

pregunta = (
    "Estado: el cliente dice que el pedido llego roto.\n"
    "Pregunta: que equipo debe atender esto?\n"
    "[A] facturacion\n[B] envios\n[C] devoluciones\n"
    "Responde solo con la letra."
)

payload = {
    "model": "gemma-4-12b",
    "messages": [{"role": "user", "content": pregunta}],
    "max_completion_tokens": 1,
    "logprobs": True,
    "top_logprobs": 3,
}

resp = requests.post("http://localhost:8080/v1/chat/completions", json=payload, timeout=30)
top = resp.json()["choices"][0]["logprobs"]["content"][0]["top_logprobs"]
print(top)
Enter fullscreen mode Exit fullscreen mode

La respuesta trae una lista de tokens candidatos con su log-probabilidad. Tomando el de mayor valor entre A, B y C se obtiene la clasificación, sin haber generado nunca una respuesta larga.

Para imágenes, Boll agrega el contenido como un bloque image_url con una URL de datos en base64, siguiendo el mismo formato multimodal que ya soportan las APIs compatibles con OpenAI. El resto de la lógica no cambia: se arma la pregunta, se listan las opciones como letras y se lee cuál tuvo la log-probabilidad más alta.

import base64, json, pathlib, urllib.request

def clasificar_frame(ruta_imagen, pregunta, opciones, url, modelo):
    imagen_b64 = base64.b64encode(pathlib.Path(ruta_imagen).read_bytes()).decode()
    letras = "ABCDEFGHIJKLMNOPQRST"[: len(opciones)]
    texto_opciones = "\n".join(f"[{l}] {o}" for l, o in zip(letras, opciones))
    body = {
        "model": modelo,
        "messages": [{
            "role": "user",
            "content": [
                {"type": "text", "text": f"Pregunta: {pregunta}\nOpciones:\n{texto_opciones}\nResponde solo con la letra."},
                {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{imagen_b64}"}},
            ],
        }],
        "max_completion_tokens": 1,
        "logprobs": True,
        "top_logprobs": len(opciones),
    }
    data = json.dumps(body).encode()
    req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=60) as resp:
        respuesta = json.loads(resp.read())
    top = respuesta["choices"][0]["logprobs"]["content"][0]["top_logprobs"]
    return sorted(top, key=lambda t: t["logprob"], reverse=True)[0]["token"]

letra = clasificar_frame(
    "frame_webcam.jpg",
    "Hay una persona visible en la imagen?",
    ["si", "no"],
    "http://localhost:8080/v1/chat/completions",
    "gemma-4-12b",
)
print(letra)
Enter fullscreen mode Exit fullscreen mode
sequenceDiagram
    participant W as Webcam
    participant S as Script Python
    participant L as Servidor LLM
    W->>S: captura frame JPEG
    S->>S: codifica base64
    S->>L: pide clasificacion con 1 token y logprobs activos
    L-->>S: letra elegida y top_logprobs
    S->>S: toma la letra con mayor log-probabilidad
    Note over S,L: se repite una vez por cada pregunta del frame
Enter fullscreen mode Exit fullscreen mode

En sus pruebas, con Gemma 4 12B corriendo en llama.cpp sobre una RTX 3090, el script sostiene cerca de 1 fotograma por segundo evaluando tres preguntas por frame: persona visible, interior o exterior, y nivel de brillo. Contra la API de OpenAI con GPT-6-luna, el rendimiento cae a unos 0,2 fps. Boll atribuye la diferencia a que no optimizó la conexión: cada pregunta abre una conexión separada contra la infraestructura de OpenAI en vez de reutilizar un canal ya abierto.
OpciónCuándo usarlaVentajaLimitaciónModelo local (llama.cpp + Gemma 4 12B)Prototipos, privacidad de la webcam, sin costo por llamada~1 fps con tres preguntas, sin salir de la red localDepende de tener una GPU decente en la máquinaAPI remota (OpenAI GPT-6-luna)Cuando no hay GPU local disponibleNo requiere infraestructura propiaCae a ~0,2 fps y cada pregunta se cobra por separadoModelo de visión especializado (CNN entrenada)Producción a gran escala con una tarea fijaMás eficiente y barato por inferenciaHay que entrenar y reentrenar si cambia la condición

💡 Tip: si tu backend soporta cache de prefijo (KV cache), el estado compartido entre las tres preguntas de un mismo frame se puede cachear una sola vez, evitando reprocesar la imagen para cada pregunta.
Con tres preguntas por frame, GPT-6-luna promedió unos 0,2 fotogramas por segundo en las pruebas publicadas el 25 de septiembre de 2026.

Cómo empezar o probarlo

Para reproducir el experimento hace falta un backend que hable el protocolo de Chat Completions con soporte de logprobs, como llama.cpp server o directamente la API de OpenAI, más un modelo de visión si vas a clasificar imágenes.

Linux y macOS, con uv:

curl -LsSf https://astral.sh/uv/install.sh | sh
uv run webcam.py
Enter fullscreen mode Exit fullscreen mode

Windows, en PowerShell:

powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
uv run webcam.py
Enter fullscreen mode Exit fullscreen mode

Alternativa con pip en las tres plataformas, cambiando solo el comando de activar el entorno:

python -m venv venv
source venv/bin/activate
pip install opencv-python requests
python webcam.py
Enter fullscreen mode Exit fullscreen mode

En Windows el paso de activación es venv\Scripts\activate en vez de source venv/bin/activate.

Para confirmar que tu backend efectivamente devuelve logprobs, antes de escribir el script completo, alcanza con una llamada suelta:

curl -s http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"gemma-4-12b","messages":[{"role":"user","content":"Responde A o B"}],"max_completion_tokens":1,"logprobs":true,"top_logprobs":5}' \
  | jq '.choices[0].logprobs.content[0].top_logprobs'
Enter fullscreen mode Exit fullscreen mode

Si el arreglo que devuelve jq no está vacío, el backend soporta logprobs y el truco funciona tal cual se describe arriba.

⚠️ Ojo: si usás una API remota, cada pregunta es una llamada facturable con la imagen completa en base64 adentro; tres preguntas por frame multiplican el costo y la latencia por tres, no dividen nada.

Impacto y análisis

Un modelo de visión especializado, entrenado para una tarea puntual, seguramente es más rápido y barato en producción a gran escala. La ventaja de este enfoque no es la velocidad: es la flexibilidad de cambiar una condición reescribiendo una frase en el prompt, sin reentrenar nada. Para un prototipo, una alerta puntual o un pipeline con pocos frames por segundo, esa flexibilidad puede pesar más que la eficiencia bruta.

El costo real está en la cantidad de llamadas. Clasificar un frame con tres preguntas son tres peticiones HTTP, aunque cada una devuelva apenas un token. Contra un backend local eso es casi gratis; contra una API remota, se multiplica el costo por cada pregunta y por cada frame que quieras procesar por segundo.

Qué sigue

El formato de Jev todavía no documenta attachments de forma oficial, así que el campo que agregó Boll sigue siendo una extensión personal, no un estándar. Si la comunidad alrededor de Jev, OpenJev y SemIf adopta esta idea, lo lógico sería verla aparecer en la especificación del proyecto en algún momento, con soporte explícito para imágenes y quizás audio.

Mientras tanto, cualquiera con un backend compatible con Chat Completions puede probar el mismo patrón hoy: no depende de que Jev lo estandarice, alcanza con que la API exponga logprobs y top_logprobs.

📖 Resumen en Telegram: Ver resumen

Probalo vos: levantá un servidor local con llama.cpp y un modelo multimodal, y mandale una imagen con logprobs: true para ver la confianza real detrás de la respuesta.

Preguntas frecuentes

Qué es un logprob y por qué sirve para clasificar?

Es el logaritmo de la probabilidad que el modelo le asigna a un token dado el contexto. Sirve porque convierte la elección del LLM en un número comparable entre opciones, en vez de quedarte solo con el texto que generó.

Hace falta reentrenar el modelo para usar esta técnica?

No. El truco funciona con cualquier modelo que ya sepa seguir instrucciones y cuya API exponga logprobs; no se ajustan pesos ni se agrega una capa nueva.

Funciona con modelos que no exponen logprobs en su API?

No directamente. Hace falta que el backend, sea llama.cpp, vLLM o la API de OpenAI, implemente los parámetros logprobs y top_logprobs en el endpoint de chat completions.

Por qué el límite está en 20 opciones por pregunta?

Porque el script de Boll mapea cada opción a una letra del alfabeto entre A y T, y top_logprobs en la mayoría de las APIs compatibles con OpenAI acepta como máximo 20 valores.

Conviene usarlo en vez de un modelo de visión especializado?

Depende del volumen. Para pocos frames por segundo y condiciones que cambian seguido, la flexibilidad de reescribir el prompt gana. Para producción a gran escala, un modelo entrenado para la tarea puntual suele ser más eficiente.

Se puede correr completamente local, sin mandar nada a una API externa?

Sí. El script habla el mismo protocolo de Chat Completions contra un servidor local como llama.cpp, así que no hace falta compartir la webcam con ningún proveedor externo.

Referencias

📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.

Top comments (0)