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
.gitignoreREADME.es.mdREADME.mdbackend/Dockerfilebackend/ai_gemini.pybackend/ai_openai.pybackend/main.pybackend/requirements.txtbootstrap-secrets.shdocker-compose.ymlfrontend/Dockerfilefrontend/app/layout.tsxfrontend/app/page.tsxfrontend/next.config.tsfrontend/package.jsonfrontend/tsconfig.json