Course EN
← back to chapter

Clasificador de etiquetas

Clasifica texto en categorías sin entrenamiento y sin ninguna llamada al modelo

Showcase — Clasificador de etiquetas

Clasifica texto en categorías sin entrenamiento y sin ninguna llamada al modelo en tiempo de inferencia más allá de un embedding. Cada categoría es una descripción corta, embebida una vez; un mensaje entrante se embebe y se asigna a la descripción más cercana por cosine similarity. Agregar una categoría es una oración — el "reentrenamiento" es instantáneo.

Córrelo

bash bootstrap-secrets.sh              # reads ../../../../.env, writes secrets/
docker compose up --build              # default: PROVIDER=openai

Abre http://localhost:3000.

Para correrlo contra Gemini en su lugar:

PROVIDER=gemini docker compose up --build

Qué hay aquí

  • backend/ai_openai.py / backend/ai_gemini.py — las descripciones de las etiquetas (embebidas una vez), y la asignación a la etiqueta más cercana por cosine similarity.
  • backend/main.py — loader idéntico de FastAPI; lee PROVIDER y despacha.
  • frontend/app/page.tsx — caja de mensaje + etiqueta predicha + todos los scores.

Este es un buen clasificador de baseline. Cuando las categorías se vuelven sutiles o el volumen se vuelve grande, ahí es cuando un fine-tune (más adelante en el curso) se gana su lugar.

Detenlo

docker compose down

Ejecútalo en tu máquina

Descarga el proyecto como ZIP y córrelo con Docker. Levanta un backend FastAPI y un frontend Next.js en localhost:3000.

Descargar label-classifier.zip

unzip label-classifier.zip
cd label-classifier
bash bootstrap-secrets.sh   # one-time: pulls API keys into ./secrets
docker compose up --build   # default provider: openai
# or:  PROVIDER=gemini docker compose up --build

Escribe algo, elige un proveedor y ejecuta el mismo código de Código contra la API real. Requiere iniciar sesión.


  

Los mismos módulos que ejecuta el botón Run. El proyecto completo (frontend, Dockerfile, compose) está en el ZIP, pestaña README.

backend/ai_openai.py

"""Showcase 2 (OpenAI): zero-shot classification with embeddings.

No training, no fine-tuning, no model call at inference beyond embedding. Embed
a set of label descriptions once, embed the incoming text, and assign whichever
label sits closest in cosine similarity. Add a new category by writing a
sentence — that's the whole "retraining" step.
"""
import math

from openai import OpenAI

_client = OpenAI()

_MODEL = "text-embedding-3-small"

# Label -> a short description. Richer descriptions embed to better anchors than
# bare one-word labels, so the nearest-neighbor decision is sharper.
_LABELS = {
    "billing": "A question or problem about payment, charges, invoices, or refunds.",
    "technical support": "Something is broken, erroring, crashing, or not working.",
    "feature request": "Asking for a new capability or an improvement.",
    "shipping": "About delivery, tracking, or the status of an order in transit.",
    "account access": "Login trouble, passwords, two-factor, or being locked out.",
    "praise": "Positive feedback or a compliment about the product or service.",
}

_names = list(_LABELS.keys())
_label_vecs: list[list[float]] | None = None


def _embed(texts: list[str]) -> list[list[float]]:
    return [item.embedding for item in _client.embeddings.create(model=_MODEL, input=texts).data]


def _cosine(a: list[float], b: list[float]) -> float:
    dot = sum(x * y for x, y in zip(a, b))
    na = math.sqrt(sum(x * x for x in a))
    nb = math.sqrt(sum(y * y for y in b))
    return dot / (na * nb) if na and nb else 0.0


def _labels() -> list[list[float]]:
    global _label_vecs
    if _label_vecs is None:  # embed the label descriptions once
        _label_vecs = _embed(list(_LABELS.values()))
    return _label_vecs


def run(text: str) -> str:
    tv = _embed([text.strip()])[0]
    scored = sorted(
        ((_cosine(tv, lv), name) for lv, name in zip(_labels(), _names)),
        reverse=True,
    )
    best_score, best_name = scored[0]
    rows = "\n".join(f"  {score:.3f}  {name}" for score, name in scored)
    return f"Predicted category: {best_name}  (cosine {best_score:.3f})\n\nAll labels:\n{rows}"

backend/ai_gemini.py

"""Showcase 2 (Gemini): zero-shot classification with embeddings.

Same nearest-label idea, Gemini's embedding model.
"""
import math
import os

from google import genai

_client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])

_MODEL = "gemini-embedding-001"

_LABELS = {
    "billing": "A question or problem about payment, charges, invoices, or refunds.",
    "technical support": "Something is broken, erroring, crashing, or not working.",
    "feature request": "Asking for a new capability or an improvement.",
    "shipping": "About delivery, tracking, or the status of an order in transit.",
    "account access": "Login trouble, passwords, two-factor, or being locked out.",
    "praise": "Positive feedback or a compliment about the product or service.",
}

_names = list(_LABELS.keys())
_label_vecs: list[list[float]] | None = None


def _embed(texts: list[str]) -> list[list[float]]:
    return [e.values for e in _client.models.embed_content(model=_MODEL, contents=texts).embeddings]


def _cosine(a: list[float], b: list[float]) -> float:
    dot = sum(x * y for x, y in zip(a, b))
    na = math.sqrt(sum(x * x for x in a))
    nb = math.sqrt(sum(y * y for y in b))
    return dot / (na * nb) if na and nb else 0.0


def _labels() -> list[list[float]]:
    global _label_vecs
    if _label_vecs is None:
        _label_vecs = _embed(list(_LABELS.values()))
    return _label_vecs


def run(text: str) -> str:
    tv = _embed([text.strip()])[0]
    scored = sorted(
        ((_cosine(tv, lv), name) for lv, name in zip(_labels(), _names)),
        reverse=True,
    )
    best_score, best_name = scored[0]
    rows = "\n".join(f"  {score:.3f}  {name}" for score, name in scored)
    return f"Predicted category: {best_name}  (cosine {best_score:.3f})\n\nAll labels:\n{rows}"

Archivos del proyecto

  • .gitignore
  • README.es.md
  • README.md
  • backend/Dockerfile
  • backend/ai_gemini.py
  • backend/ai_openai.py
  • backend/main.py
  • backend/requirements.txt
  • bootstrap-secrets.sh
  • docker-compose.yml
  • frontend/Dockerfile
  • frontend/app/layout.tsx
  • frontend/app/page.tsx
  • frontend/next.config.ts
  • frontend/package.json
  • frontend/tsconfig.json