Saltar a contenido

Capítulo 7: Transfer Learning en NLP y el camino a los LLMs

Módulo 03 — Deep Learning · Capítulo 7 de 7 (cierre del módulo)

En el capítulo anterior construiste un mini-GPT desde cero: sabes qué es la atención, qué diferencia a BERT de GPT y cómo un Transformer predice el siguiente token. En el capítulo 4 viste transfer learning en visión: tomar una red preentrenada en ImageNet y adaptarla a tu problema con pocos datos. Este capítulo une ambas ideas y las lleva al terreno donde hoy se juega toda la IA moderna: preentrenar una vez, adaptar mil veces. Al terminar sabrás usar Hugging Face con soltura, afinar un modelo BERT en español para un problema real de empresa, entender LoRA, generar embeddings de producción... y tendrás el puente tendido hacia el módulo 04-LLM.


Índice

  1. El paradigma que domina la IA moderna
  2. 1.1 Preentrenar una vez, adaptar mil veces
  3. 1.2 Por qué funciona aún mejor en lenguaje que en visión
  4. 1.3 La jerarquía de adaptación: la tabla más importante del capítulo
  5. Hugging Face a fondo: el GitHub de los modelos
  6. 2.1 El Hub: modelos, datasets y spaces
  7. 2.2 Model cards: cómo leerlas como profesional
  8. 2.3 pipeline: inferencia en tres líneas
  9. 2.4 AutoTokenizer y AutoModel: abriendo el capó
  10. 2.5 La cabeza intercambiable: AutoModelForSequenceClassification
  11. Tokenizers en la práctica
  12. Fine-tuning completo paso a paso: clasificador de tickets
  13. 4.1 El problema y el dataset
  14. 4.2 Tokenizar el dataset y crear DataLoaders
  15. 4.3 Versión A: training loop manual en PyTorch
  16. 4.4 Versión B: Trainer de Hugging Face
  17. 4.5 Evaluación con sklearn
  18. 4.6 Guardar, cargar e inferencia final
  19. Fine-tuning eficiente: LoRA y PEFT
  20. Embeddings como producto: sentence-transformers
  21. Del Transformer al LLM moderno
  22. Decisiones profesionales: 4 escenarios de arquitecto
  23. Caso empresarial: la aseguradora de los 5.000 emails diarios
  24. Buenas prácticas
  25. Malas prácticas
  26. Errores comunes
  27. FAQ
  28. Resumen del capítulo y cierre del módulo
  29. Bibliografía y recursos

1. El paradigma que domina la IA moderna

1.1 Preentrenar una vez, adaptar mil veces

Piensa en lo que costó entrenar tu mini-GPT del capítulo 6: unos minutos en tu GPU (o en Colab) para un modelo de juguete con un corpus diminuto. Ahora escala mentalmente: los modelos de lenguaje que usan las empresas hoy se preentrenan con billones de tokens (buena parte del texto público de internet, libros, código) durante semanas o meses, en clústeres de miles de GPUs, con costes que se miden en decenas o cientos de millones de dólares.

Nadie —ni tú, ni tu empresa, ni el 99,9 % de las empresas del mundo— va a repetir ese entrenamiento. Y la gran noticia es que nadie necesita hacerlo, porque el conocimiento adquirido en el preentrenamiento se transfiere:

flowchart LR
    subgraph FASE1["FASE 1: Preentrenamiento (una vez)"]
        A["Todo internet<br/>billones de tokens"] --> B["Semanas de entrenamiento<br/>miles de GPUs<br/>millones de dólares"]
        B --> C["Modelo base<br/>(conocimiento general<br/>del lenguaje y del mundo)"]
    end
    subgraph FASE2["FASE 2: Adaptación (mil veces)"]
        C --> D["Prompting<br/>coste ~0"]
        C --> E["RAG<br/>horas de trabajo"]
        C --> F["LoRA<br/>1 GPU, horas"]
        C --> G["Fine-tuning completo<br/>varias GPUs, días"]
    end
    D --> H["Tu aplicación"]
    E --> H
    F --> H
    G --> H

Este es exactamente el mismo patrón que viste en visión en el capítulo 4 (ResNet preentrenada en ImageNet → cambias la última capa → afinas con 2.000 fotos de tu fábrica), pero elevado a paradigma universal. Hoy, prácticamente ningún proyecto profesional de NLP empieza desde cero. La primera pregunta ya no es "¿qué arquitectura diseño?", sino "¿qué modelo preentrenado tomo como punto de partida y cuánto lo adapto?".

Nota

A los modelos preentrenados gigantes que sirven de punto de partida para muchas tareas se les llama foundation models (modelos fundacionales), término acuñado por Stanford en 2021. BERT, GPT, Llama, Claude o Gemini son foundation models: la "materia prima" sobre la que se construyen los productos.

1.2 Por qué funciona aún mejor en lenguaje que en visión

En visión, el transfer learning funciona porque las primeras capas de una CNN aprenden detectores genéricos (bordes, texturas, formas) útiles para cualquier imagen. En lenguaje ocurre lo mismo... pero con tres ventajas adicionales que hacen que la transferencia sea todavía más potente:

  1. El preentrenamiento no necesita etiquetas. En visión clásica, ImageNet requirió millones de imágenes etiquetadas a mano. En lenguaje, la tarea de preentrenamiento es autosupervisada: predecir el siguiente token (GPT) o el token enmascarado (BERT). La etiqueta es el propio texto. Eso significa que el corpus de entrenamiento es, literalmente, todo el texto disponible en el mundo, sin coste de anotación.

  2. El lenguaje codifica conocimiento del mundo, no solo percepción. Una CNN preentrenada sabe "ver". Un modelo de lenguaje preentrenado sabe gramática, hechos, razonamiento superficial, estilos, jerga médica y legal, código... Para predecir bien la palabra siguiente en "El paciente presenta fiebre, tos y disnea, compatible con ___", el modelo tuvo que aprender medicina. La tarea de pretexto es tan exigente que obliga a comprimir conocimiento enciclopédico.

  3. Todas las tareas de NLP comparten la misma interfaz: texto. En visión, adaptar una red de clasificación a segmentación requiere cambiar la arquitectura. En lenguaje, clasificar, resumir, traducir, extraer entidades y responder preguntas son, en el fondo, transformaciones texto → texto (o texto → etiqueta). El mismo backbone sirve para todo con cambios mínimos.

Consejo profesional

Cuando expliques a un directivo por qué no vais a "entrenar vuestra propia IA desde cero", usa la analogía de la educación: preentrenar es pagar 25 años de educación general de una persona; adaptar es darle un curso de dos semanas sobre los productos de tu empresa. Nadie contrata bebés para formarlos desde cero.

1.3 La jerarquía de adaptación: la tabla más importante del capítulo

Si solo pudieras llevarte una cosa de este capítulo, que sea esta tabla. Ordena las formas de adaptar un modelo preentrenado de menor a mayor coste y compromiso. La regla profesional de oro: empieza siempre por el nivel más bajo que resuelva tu problema y sube solo si se demuestra insuficiente.

Nivel Técnica ¿Qué modificas? Datos necesarios Coste típico Cuándo usarla Ejemplo empresarial
1 Prompting / in-context learning Nada: solo la entrada (instrucciones + ejemplos en el prompt) 0–10 ejemplos Céntimos por llamada API Prototipos, tareas generales, poco volumen, arranque de cualquier proyecto Startup que clasifica 200 emails/día con un LLM vía API y 5 ejemplos en el prompt
2 RAG (retrieval-augmented generation) Nada del modelo: le inyectas contexto recuperado de tus documentos Tus documentos (sin etiquetar) Días de ingeniería + coste API + base vectorial Conocimiento privado o cambiante, respuestas con fuentes citables Asistente interno que responde sobre la documentación técnica de la empresa, actualizada a diario
3 Fine-tuning ligero (LoRA/PEFT) ~0,1–1 % de los parámetros (adaptadores de bajo rango) Cientos–decenas de miles de ejemplos etiquetados 1 GPU de consumo, horas Comportamiento/formato/dominio muy específico, presupuesto ajustado, modelo open source Bufete que adapta un Llama a redactar cláusulas con el estilo exacto de la firma
4 Fine-tuning completo Todos los parámetros del modelo Miles–millones de ejemplos etiquetados Varias GPUs, horas–días (en modelos tipo BERT); prohibitivo en modelos de decenas de miles de millones de parámetros Máximo rendimiento en una tarea cerrada, latencia mínima, privacidad total, gran volumen Banco que clasifica millones de transacciones/día con un BERT afinado, on-premise
5 Preentrenar desde cero Todo, desde pesos aleatorios Billones de tokens Decenas–cientos de millones de dólares Casi nunca. Solo labs de frontera o dominios radicalmente distintos (p. ej. proteínas, no lenguaje humano) Empresa farmacéutica preentrenando un modelo sobre secuencias de ADN
flowchart TD
    A["¿Un buen prompt con ejemplos<br/>resuelve tu problema?"] -->|Sí|B["Nivel 1: Prompting<br/>Quédate aquí. En serio."]
    A -->|No|C["¿El problema es que al modelo<br/>le falta TU información<br/>(documentos, datos privados, actualidad)?"]
    C -->|Sí|D["Nivel 2: RAG"]
    C -->|No|E["¿El problema es de COMPORTAMIENTO<br/>(formato, estilo, tarea muy específica)<br/>y tienes cientos o miles de ejemplos?"]
    E -->|Sí|F["¿Presupuesto/GPU limitados<br/>o modelo muy grande?"]
    F -->|Sí|G["Nivel 3: LoRA"]
    F -->|No|H["Nivel 4: Fine-tuning completo<br/>(típico en modelos tipo BERT)"]
    E -->|No|I["¿Tu dominio ni siquiera es<br/>lenguaje humano estándar?"]
    I -->|Sí|J["Nivel 5: Preentrenar<br/>(consulta con tu CFO primero)"]
    I -->|No|A

Advertencia

El error de arquitectura más caro que se comete hoy en la industria es saltar directamente al fine-tuning cuando un prompt bien escrito habría bastado. Antes de gastar una semana y una GPU, dedica dos horas a escribir un buen prompt con 5 ejemplos y mide. La mayoría de las veces te sorprenderá. El segundo error más caro es el inverso: intentar resolver con prompting heroico un problema que pide a gritos datos y fine-tuning.

Nota

RAG (nivel 2) se trata en profundidad en el módulo 06-RAG, y LoRA sobre modelos open source (nivel 3) en el módulo 16-OPEN-SOURCE-LLMS. En este capítulo dominarás el nivel 4 con un modelo tipo BERT —donde el fine-tuning completo sigue siendo perfectamente viable y muy usado en producción— y entenderás la intuición del nivel 3.

Ejercicio rápido 1

Tu empresa quiere un chatbot que responda preguntas sobre el manual del empleado (400 páginas que RR. HH. actualiza cada trimestre). Un compañero propone hacer fine-tuning de un modelo con el manual. ¿Qué nivel de la jerarquía recomendarías y por qué su propuesta es problemática?

Ver solución **Nivel 2: RAG.** Razones: 1. **El problema es de conocimiento, no de comportamiento.** El modelo ya sabe conversar; lo que le falta es el contenido del manual. Esa es exactamente la señal de RAG. 2. **El manual cambia cada trimestre.** Con fine-tuning tendrías que re-entrenar en cada actualización; con RAG basta con re-indexar los documentos (minutos). 3. **El fine-tuning no memoriza documentos de forma fiable.** Afinar con el texto del manual enseña al modelo el *estilo* del manual, pero no garantiza recuperación fiel de datos concretos (¿cuántos días de permiso por mudanza?). Puede alucinar respuestas plausibles pero incorrectas — inaceptable en RR. HH. 4. **RAG cita fuentes**: puedes mostrar el párrafo exacto del manual que respalda cada respuesta, clave para confianza y auditoría. El fine-tuning sería la herramienta correcta si el problema fuera, por ejemplo, que el bot debe responder siempre con un formato/tono corporativo muy estricto que el prompting no consigue.

2. Hugging Face a fondo: el GitHub de los modelos

2.1 El Hub: modelos, datasets y spaces

Hugging Face (huggingface.co) es a los modelos de IA lo que GitHub es al código: el punto de encuentro donde la comunidad publica, versiona, documenta y descarga. En 2026 aloja más de un millón de modelos públicos. Sus tres componentes principales:

  • Models (huggingface.co/models): repositorios de modelos preentrenados y afinados. Cada modelo tiene un identificador organizacion/nombre (p. ej. dccuchile/bert-base-spanish-wwm-cased). Se filtra por tarea, idioma, licencia, tamaño...
  • Datasets (huggingface.co/datasets): miles de datasets listos para cargar con la librería datasets en una línea.
  • Spaces (huggingface.co/spaces): demos interactivas alojadas (con Gradio o Streamlit). Perfectas para probar un modelo antes de integrarlo.

Alrededor del Hub, Hugging Face mantiene el ecosistema de librerías que usarás a diario:

Librería Para qué sirve La usarás en
transformers Cargar y usar modelos Transformer (BERT, GPT, T5, Llama...) Este capítulo, y siempre
datasets Cargar y procesar datasets de forma eficiente Este capítulo
tokenizers Tokenización ultrarrápida (backend en Rust de los tokenizers) Bajo el capó de transformers
peft Fine-tuning eficiente (LoRA y compañía) Sección 5 y módulo 16
accelerate Entrenamiento distribuido y en dispositivos mixtos sin cambiar código Módulo 16
sentence-transformers Embeddings de frases para búsqueda semántica Sección 6 y módulo 06-RAG

Instalación de lo que necesitas para este capítulo:

pip install transformers datasets torch scikit-learn sentence-transformers peft

2.2 Model cards: cómo leerlas como profesional

Cada modelo del Hub tiene una model card: su README. Aprender a leerla en 90 segundos es una habilidad profesional real, porque elegir mal un modelo cuesta semanas. Qué mirar y en qué orden:

Campo de la model card Qué buscar Señal de alarma
Licencia ¿Permite uso comercial? Apache 2.0 y MIT sí, sin restricciones. Las licencias "community" de algunos modelos (p. ej. familia Llama) permiten uso comercial con condiciones. CC-BY-NC = no comercial Usar en producción un modelo NC: riesgo legal directo para tu empresa
Tamaño (parámetros) ¿Cabe en tu GPU/presupuesto de latencia? 110M (BERT-base) corre en CPU; 7B necesita GPU decente; 70B, infraestructura seria Elegir un modelo 100 veces más grande de lo que la tarea necesita
Idiomas ¿Está entrenado con español? ¿Es monolingüe español o multilingüe? Modelos "English-only" evaluados solo en inglés que "parece que funcionan" en español
Datos de entrenamiento ¿Con qué corpus se entrenó? ¿Hay riesgo de sesgos o contaminación con tus datos de test? Model card sin sección de datos: opacidad = riesgo
Benchmarks / evaluación Métricas en tareas parecidas a la tuya, e idealmente en tu idioma Solo métricas en benchmarks ingleses para un producto en español
Descargas y última actualización Modelos con millones de descargas y mantenimiento reciente son apuestas más seguras Modelo sin actualizar en años, 40 descargas, sin card
Limitaciones y usos previstos Los autores serios documentan para qué NO sirve el modelo Ausencia total de esta sección

Consejo profesional

En el Hub, filtra siempre primero por licencia y por idioma. Es tentador ordenar por "trending" y coger el más famoso, pero el modelo perfecto para tu demo puede ser ilegal para tu producto. La licencia se revisa antes de escribir la primera línea de código, no la semana del lanzamiento.

2.3 pipeline: inferencia en tres líneas

pipeline() es la API de más alto nivel de transformers: le dices la tarea (y opcionalmente el modelo) y te devuelve un objeto listo para usar que encapsula tokenización → modelo → post-procesado. Es la herramienta perfecta para probar un modelo en 30 segundos antes de decidir nada.

Análisis de sentimiento en español (con un modelo entrenado específicamente con español de redes sociales):

from transformers import pipeline

# Creamos el pipeline indicando tarea y modelo concreto.
# pysentimiento/robertuito-sentiment-analysis: RoBERTa entrenada con
# tuits en español, afinada para sentimiento (POS/NEG/NEU).
clasificador = pipeline(
    "sentiment-analysis",                                  # la tarea
    model="pysentimiento/robertuito-sentiment-analysis"    # el modelo del Hub
)

# Inferencia inmediata sobre una lista de frases
resultados = clasificador([
    "Me ha encantado la atención al cliente, rapidísimos.",
    "Llevo tres semanas esperando el reembolso, es una vergüenza.",
    "El pedido llegó el martes."
])

for r in resultados:
    print(r)
# {'label': 'POS', 'score': 0.99...}
# {'label': 'NEG', 'score': 0.99...}
# {'label': 'NEU', 'score': 0.9...}

La primera vez que ejecutes esto, transformers descargará el modelo del Hub (cientos de MB) y lo cacheará en ~/.cache/huggingface/. Las siguientes veces carga desde disco.

Reconocimiento de entidades (NER) multilingüe:

from transformers import pipeline

# Babelscape/wikineural-multilingual-ner: NER multilingüe (9 idiomas,
# incluido español) entrenado sobre Wikipedia. Detecta PER, ORG, LOC, MISC.
ner = pipeline(
    "ner",
    model="Babelscape/wikineural-multilingual-ner",
    aggregation_strategy="simple"   # agrupa subpalabras en entidades completas
)

texto = "Ana García, directora de Iberdrola, se reunió en Bilbao con el equipo de Microsoft."
for entidad in ner(texto):
    print(f"{entidad['word']:<12}{entidad['entity_group']} (confianza {entidad['score']:.2f})")
# Ana García   → PER (confianza 0.99)
# Iberdrola    → ORG (confianza 0.99)
# Bilbao       → LOC (confianza 0.99)
# Microsoft    → ORG (confianza 0.99)

Nota

El argumento aggregation_strategy="simple" es importante: sin él, el pipeline devuelve entidades troceadas en subpalabras (Iber, ##drola), porque —como verás en la sección 3— el modelo trabaja internamente con subpalabras, no con palabras.

Resumen automático multilingüe:

from transformers import pipeline

# csebuetnlp/mT5_multilingual_XLSum: mT5 afinado para resumen
# en 45 idiomas, incluido español (dataset XL-Sum de la BBC).
resumidor = pipeline(
    "summarization",
    model="csebuetnlp/mT5_multilingual_XLSum"
)

articulo = """El consejo de administración aprobó ayer el plan estratégico 2026-2029,
que contempla una inversión de 200 millones de euros en digitalización, la apertura
de tres centros logísticos en Valencia, Sevilla y Oporto, y la contratación de 500
ingenieros. La compañía espera duplicar su facturación internacional en tres años,
apoyándose en la automatización de su cadena de suministro y en una nueva plataforma
de comercio electrónico que se lanzará el próximo trimestre."""

resumen = resumidor(articulo, max_length=60, min_length=15)
print(resumen[0]["summary_text"])

Advertencia

pipeline() es fantástico para explorar, pero en producción querrás controlar batching, dispositivo, precisión numérica y post-procesado. Considéralo el "modo demo": lo que te permite decidir qué modelo usar antes de industrializarlo.

2.4 AutoTokenizer y AutoModel: abriendo el capó

Debajo de cada pipeline hay dos objetos que ya conoces conceptualmente del capítulo 6:

  • El tokenizer: convierte texto ↔ ids de tokens. Cada modelo tiene su tokenizer, entrenado junto a él. Usar el tokenizer equivocado con un modelo es como darle a alguien un diccionario de otro idioma.
  • El modelo: la red Transformer que convierte ids de tokens en representaciones (y, con una cabeza encima, en predicciones).

Las clases Auto* leen la configuración del repositorio del Hub y eligen automáticamente la clase concreta correcta (BertTokenizer, RobertaModel...). Tú siempre escribes lo mismo:

from transformers import AutoTokenizer, AutoModel
import torch

# BETO: el BERT en español de la Universidad de Chile. Entrenado desde cero
# con un gran corpus en español. 110M de parámetros, licencia permisiva.
NOMBRE_MODELO = "dccuchile/bert-base-spanish-wwm-cased"

tokenizer = AutoTokenizer.from_pretrained(NOMBRE_MODELO)  # descarga vocab + reglas
modelo = AutoModel.from_pretrained(NOMBRE_MODELO)         # descarga pesos (~440 MB)

frase = "Los transformers revolucionaron el procesamiento del lenguaje."

# Paso 1: texto → tokens (subpalabras). Veamos qué hace de verdad:
tokens = tokenizer.tokenize(frase)
print(tokens)
# ['Los', 'transform', '##ers', 'revolucion', '##aron', 'el', 'procesamiento',
#  'del', 'lenguaje', '.']
# Fíjate: "transformers" no está en el vocabulario español de BETO,
# así que se parte en 'transform' + '##ers'. El prefijo ## significa
# "continúa la palabra anterior". "revolucionaron" también se parte:
# la raíz 'revolucion' es frecuente; la terminación '##aron' se añade aparte.

# Paso 2: tokens → ids (números que el modelo entiende)
entrada = tokenizer(frase, return_tensors="pt")  # "pt" = tensores de PyTorch
print(entrada["input_ids"])
# tensor([[    4, 1477, 28995, 10429, 24902,  1203,  ...,     5]])
# El 4 y el 5 son [CLS] y [SEP], tokens especiales que veremos en la sección 3.

# Paso 3: ids → representaciones contextuales (¡el Transformer del capítulo 6!)
with torch.no_grad():                     # inferencia: no necesitamos gradientes
    salida = modelo(**entrada)            # ** desempaqueta el dict como argumentos

print(salida.last_hidden_state.shape)
# torch.Size([1, 12, 768])
# 1 frase × 12 tokens × 768 dimensiones: un vector contextual por token,
# exactamente lo que producía el bloque Transformer que programaste.

Esto es literalmente lo mismo que construiste a mano en el capítulo 6 —embeddings, atención, capas apiladas— pero con 110 millones de parámetros ya entrenados con miles de millones de palabras en español. AutoModel te da el backbone desnudo: representaciones contextuales sin ninguna cabeza de tarea. Para hacer algo útil con ellas, necesitas la pieza siguiente.

2.5 La cabeza intercambiable: AutoModelForSequenceClassification

Recuerda transfer learning en visión (capítulo 4): tomabas la ResNet, tirabas la última capa (la que clasificaba entre las 1.000 clases de ImageNet) y ponías una capa nueva con tus clases, inicializada al azar. En NLP es idéntico:

from transformers import AutoModelForSequenceClassification

# Mismo backbone BETO, pero con una CABEZA DE CLASIFICACIÓN encima:
# una capa lineal que proyecta el vector del token [CLS] (768 dims)
# a num_labels logits (aquí 4 clases).
modelo_clf = AutoModelForSequenceClassification.from_pretrained(
    "dccuchile/bert-base-spanish-wwm-cased",
    num_labels=4                       # nuestras 4 categorías de tickets
)
# Al ejecutarlo verás este warning (¡y es BUENA señal!):
# "Some weights of BertForSequenceClassification were not initialized from
#  the model checkpoint ... and are newly initialized: ['classifier.weight',
#  'classifier.bias'] You should probably TRAIN this model..."
# Traducción: el backbone cargó sus pesos preentrenados; la cabeza nueva
# está en pesos aleatorios y hay que entrenarla. Exactamente el plan.

La familia AutoModelFor* cubre todas las tareas, siempre con el mismo patrón backbone + cabeza:

Clase Cabeza que añade Tarea
AutoModelForSequenceClassification Lineal sobre [CLS] → n clases Clasificar textos completos
AutoModelForTokenClassification Lineal sobre cada token → n clases NER, etiquetado gramatical
AutoModelForQuestionAnswering Dos lineales (inicio/fin de respuesta) QA extractivo
AutoModelForCausalLM Lineal → vocabulario (siguiente token) Generación tipo GPT
AutoModelForMaskedLM Lineal → vocabulario (token enmascarado) Preentrenamiento tipo BERT

Ejercicio rápido 2

Sin ejecutar código: ¿por qué el warning "newly initialized" de arriba sería una mala señal si apareciera al cargar un modelo que descargaste ya afinado para tu tarea (p. ej. el de sentimiento de la sección 2.3)?

Ver solución Porque significaría que la cabeza de clasificación **no se cargó del checkpoint** y está en pesos aleatorios: las predicciones serían ruido puro (accuracy ≈ azar). Si cargas un modelo supuestamente afinado y ves ese warning, algo va mal: o el identificador del modelo es incorrecto, o estás usando una clase `AutoModelFor*` distinta a la que usó el autor (p. ej. pediste 5 etiquetas y el modelo se entrenó con 3), o el repositorio no incluye la cabeza. El mismo mensaje es *esperado* al iniciar un fine-tuning y *alarmante* al cargar un modelo para inferencia. Aprender a leer los warnings de `transformers` te ahorrará tardes enteras de depuración.

3. Tokenizers en la práctica

En el capítulo 6 implementaste un tokenizer simple. Los modelos reales usan tokenizers de subpalabras (WordPiece en BERT, BPE en GPT/RoBERTa) que equilibran vocabulario manejable y cobertura total: cualquier palabra, incluso inventada, se puede representar como secuencia de trozos. Exploremos el de BETO de verdad:

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("dccuchile/bert-base-spanish-wwm-cased")

# --- 1. El vocabulario ---
print(len(tokenizer))            # ~31000: tamaño del vocabulario de BETO
print(tokenizer.vocab["hola"])   # id de la subpalabra 'hola' en el vocabulario

# --- 2. Palabras frecuentes vs raras ---
print(tokenizer.tokenize("gato"))
# ['gato']  → palabra frecuente: 1 token
print(tokenizer.tokenize("electroencefalografista"))
# ['electro', '##ence', '##fal', '##ograf', '##ista'] → palabra rara: 5 subpalabras
print(tokenizer.tokenize("criptomonedas"))
# ['cripto', '##mon', '##edas'] → el corpus de BETO (2019) apenas la contenía

# --- 3. Los tokens especiales ---
print(tokenizer.cls_token, tokenizer.sep_token, tokenizer.pad_token, tokenizer.unk_token)
# [CLS] [SEP] [PAD] [UNK]

Cada familia de modelos usa sus propios tokens especiales; conviene reconocerlos a simple vista:

Familia Inicio de secuencia Separador/fin Relleno Desconocido
BERT / BETO (WordPiece) [CLS] [SEP] [PAD] [UNK]
RoBERTa / muchos BPE <s> </s> <pad> <unk>
GPT-2 (BPE) — (no usa) <|endoftext|> — (no tiene, se le asigna) — (BPE cubre todo)
Llama y derivados <s> </s> según versión <unk>

El momento clave es entender qué diccionario entra exactamente al modelo cuando tokenizas un batch de frases de distinta longitud:

frases = [
    "No puedo acceder a mi cuenta.",
    "Quiero información sobre el plan empresarial y sus condiciones de facturación anual."
]

batch = tokenizer(
    frases,
    padding=True,          # rellena la frase corta con [PAD] hasta igualar la larga
    truncation=True,       # corta si alguna excede max_length
    max_length=32,         # longitud máxima permitida
    return_tensors="pt"    # devuelve tensores de PyTorch
)

print(batch.keys())
# dict_keys(['input_ids', 'token_type_ids', 'attention_mask'])

print(batch["input_ids"])
# tensor([[   4, 1139, 5031, 26625,   10,  1030, 5215,  ...,    5,    1,    1,    1, ...],
#         [   4, 4643, 3379,  1791, 1203, 3053, 26845,  ...,                        5]])
# La frase corta termina en [SEP] (5) y se rellena con [PAD] (id 1) hasta
# la longitud de la más larga del batch.

print(batch["attention_mask"])
# tensor([[1, 1, 1, 1, 1, 1, 1, ..., 1, 0, 0, 0, ...],
#         [1, 1, 1, 1, 1, 1, 1, ...,          1, 1]])
# 1 = token real, 0 = relleno. La atención IGNORA las posiciones con 0:
# sin esta máscara, el modelo "prestaría atención" al relleno y las
# representaciones se contaminarían con basura.

# token_type_ids distingue frase A de frase B en tareas de pares de frases
# (herencia del preentrenamiento de BERT); para una sola frase es todo ceros.

Estas tres claves —input_ids, attention_mask, token_type_ids— son exactamente lo que el modelo recibe en el forward. Cuando en la sección 4 escribas modelo(**batch), ya sabes qué está pasando.

Advertencia

truncation=True y max_length no son detalles: BERT/BETO tienen un máximo arquitectónico de 512 tokens (por los embeddings posicionales). Si le pasas un documento de 3 páginas sin truncar, obtendrás un error; si lo truncas sin pensar, el modelo solo verá el principio del documento y clasificará ignorando el resto. Para textos largos: trocear el documento y agregar predicciones, o usar modelos de contexto largo. Sé siempre consciente de qué parte del texto está viendo realmente tu modelo.

Consejo profesional

En producción, tokeniza con padding dinámico por batch (como arriba) y no con padding="max_length" fijo a 512: rellenar todas las frases de 20 tokens hasta 512 multiplica el cómputo por 25 para procesar ceros.

Ejercicio rápido 3

tokenizer.tokenize("WhatsApp") sobre BETO devuelve algo como ['Wh', '##ats', '##App']. ¿Por qué esto no es un problema para el modelo, mientras que en un tokenizer a nivel de palabra "WhatsApp" habría sido [UNK]?

Ver solución Con subpalabras, ninguna palabra queda fuera del vocabulario: cualquier cadena se descompone en trozos conocidos (en el peor caso, caracteres sueltos). El modelo aprende durante el preentrenamiento a **componer el significado** de secuencias de subpalabras mediante la atención: las representaciones contextuales de `Wh`, `##ats`, `##App` juntas acaban codificando "aplicación de mensajería" porque esa secuencia apareció miles de veces en contextos consistentes. Con un tokenizer a nivel de palabra, `[UNK]` destruye toda la información: el modelo ve el mismo token para "WhatsApp", "Xiaomi" y cualquier palabra nueva, y no puede distinguirlas. Esta es la razón por la que **todos** los modelos modernos usan subpalabras: vocabulario acotado (~30k–250k entradas) con cobertura infinita.

4. Fine-tuning completo paso a paso: clasificador de tickets

Llegamos al núcleo práctico del capítulo. Vamos a construir, de principio a fin, un sistema real de empresa: un clasificador automático de tickets de soporte en español que enruta cada mensaje a uno de 4 departamentos: facturacion, tecnico, ventas, otros.

flowchart LR
    A["Dataset<br/>tickets etiquetados"] --> B["Tokenizar<br/>texto → ids"]
    B --> C["DataLoader<br/>batches con padding"]
    C --> D["BETO preentrenado<br/>+ cabeza nueva de 4 clases"]
    D --> E["Entrenamiento<br/>manual o Trainer"]
    E --> F["Evaluación<br/>sklearn: F1 por clase"]
    F --> G["Guardar modelo"]
    G --> H["Inferencia<br/>en producción"]

4.1 El problema y el dataset

En una empresa real este dataset saldría de tu sistema de ticketing (Zendesk, Freshdesk...), etiquetado por el histórico de enrutamiento. Aquí usamos uno sintético inline y autocontenido de 100 tickets (25 por clase) para que todo el capítulo sea ejecutable sin descargas:

# =====================================================================
# DATASET SINTÉTICO: 100 tickets de soporte en español, 4 categorías
# 0 = facturacion | 1 = tecnico | 2 = ventas | 3 = otros
# =====================================================================

TICKETS_FACTURACION = [
    "Me habéis cobrado dos veces la cuota de este mes, solicito la devolución del cargo duplicado.",
    "La factura de junio no me ha llegado y la necesito para presentarla en contabilidad.",
    "Quiero cambiar los datos fiscales de mi cuenta, la empresa ha cambiado de CIF.",
    "¿Por qué ha subido el importe de mi suscripción sin previo aviso?",
    "Necesito una copia de todas las facturas del último trimestre para la auditoría.",
    "El recibo ha sido devuelto por el banco, ¿cómo puedo pagar de otra forma?",
    "Solicito el reembolso del pedido cancelado, ya han pasado quince días.",
    "Hay un cargo en mi tarjeta que no reconozco de vuestra plataforma.",
    "Quiero pasar de pago mensual a pago anual, ¿me aplicáis el descuento?",
    "La factura tiene un error, el IVA aplicado no corresponde a mi país.",
    "¿Cuándo se realiza el cobro mensual? Necesito saberlo para cuadrar tesorería.",
    "He cancelado la suscripción pero me habéis vuelto a cobrar este mes.",
    "Necesito cambiar la tarjeta de crédito asociada al pago de la cuenta.",
    "El descuento promocional no aparece reflejado en mi última factura.",
    "Quiero una factura rectificativa, la dirección fiscal está mal escrita.",
    "Me gustaría saber si ofrecéis pago por transferencia para empresas.",
    "El importe cobrado no coincide con el presupuesto que me enviasteis.",
    "Solicito el desglose de los cargos adicionales que aparecen en la factura.",
    "¿Puedo aplazar el pago de este mes? Tenemos un problema puntual de caja.",
    "No consigo descargar las facturas desde el panel de cliente, no aparecen.",
    "Me cobrasteis el plan premium pero solo contraté el plan básico.",
    "¿Cómo actualizo el número de cuenta bancaria para la domiciliación?",
    "Pedí la baja hace un mes y sigue apareciendo un cargo pendiente.",
    "Necesito que la factura salga a nombre de la nueva filial del grupo.",
    "El cupón de descuento del veinte por ciento no se aplicó en la renovación.",
]

TICKETS_TECNICO = [
    "La aplicación se cierra sola cada vez que intento subir un archivo grande.",
    "No puedo iniciar sesión, dice contraseña incorrecta aunque la acabo de restablecer.",
    "La API devuelve error quinientos desde esta mañana en todas las peticiones.",
    "El panel de control tarda más de un minuto en cargar, antes iba fluido.",
    "Los correos de notificación han dejado de llegar a todo mi equipo.",
    "La sincronización con Google Drive falla y no guarda los cambios.",
    "Al exportar el informe en PDF salen los gráficos en blanco.",
    "El sistema me ha cerrado la sesión y ahora no reconoce mi usuario.",
    "La integración con nuestro CRM dejó de funcionar tras la última actualización.",
    "Recibo un error de certificado SSL al acceder desde la red de la oficina.",
    "La app móvil no muestra los datos actualizados, la web sí funciona.",
    "No me llegan los códigos de verificación en dos pasos al teléfono.",
    "El buscador interno no encuentra documentos que sé que existen.",
    "Desde ayer la plataforma va lentísima y se queda cargando en cada clic.",
    "Al importar el CSV los acentos aparecen como símbolos raros.",
    "El webhook configurado no está enviando los eventos a nuestro servidor.",
    "La pantalla de informes muestra un error de JavaScript en la consola.",
    "No puedo restablecer la contraseña, el enlace del correo dice que ha caducado.",
    "Los permisos de usuario no se guardan, cada día tengo que reconfigurarlos.",
    "El widget del calendario aparece vacío aunque hay reuniones creadas.",
    "La subida de imágenes falla con archivos de más de cinco megas.",
    "Tras la migración no aparecen los proyectos antiguos en mi cuenta.",
    "El sistema duplica las tareas cuando las creo desde la aplicación móvil.",
    "No funciona el inicio de sesión con la cuenta corporativa de Microsoft.",
    "El backup automático lleva tres días fallando según los registros.",
]

TICKETS_VENTAS = [
    "¿Qué diferencias hay entre el plan profesional y el plan empresarial?",
    "Somos una empresa de doscientos empleados, ¿tenéis precios por volumen?",
    "Me gustaría agendar una demo del producto para mi equipo directivo.",
    "¿El plan básico incluye acceso a la API o es de pago aparte?",
    "Estamos comparando vuestra solución con la competencia, ¿tenéis algún comparativo?",
    "¿Ofrecéis descuentos para ONGs y centros educativos?",
    "Quiero ampliar mi licencia actual de diez a cincuenta usuarios.",
    "¿Cuál es el periodo de prueba gratuito y qué limitaciones tiene?",
    "Necesito un presupuesto formal para presentarlo a dirección esta semana.",
    "¿El precio anunciado incluye el soporte técnico prioritario?",
    "¿Tenéis partners o distribuidores oficiales en Latinoamérica?",
    "Me interesa el módulo de analítica avanzada, ¿se contrata por separado?",
    "¿Qué condiciones tiene el contrato anual? ¿Hay permanencia?",
    "Vengo de la competencia, ¿tenéis alguna oferta de migración?",
    "¿Podéis enviarme el catálogo completo de funcionalidades y precios?",
    "Queremos contratar el plan empresarial, ¿cuáles son los siguientes pasos?",
    "¿Hay descuento si pagamos dos años por adelantado?",
    "¿El onboarding y la formación inicial están incluidos en el precio?",
    "Soy consultor y quiero revender vuestro producto a mis clientes.",
    "¿Cuántos proyectos simultáneos permite cada uno de los planes?",
    "Mi startup está en fase inicial, ¿tenéis algún programa para emprendedores?",
    "¿Es posible contratar solo por tres meses para un proyecto puntual?",
    "Necesito hablar con un comercial sobre una implantación a gran escala.",
    "¿Qué método de facturación usáis para contratos corporativos?",
    "¿Puedo cambiar de plan a mitad de contrato si crecemos?",
]

TICKETS_OTROS = [
    "¿Dónde puedo consultar vuestra política de privacidad actualizada?",
    "Quiero ejercer mi derecho de supresión de datos según el RGPD.",
    "¿Tenéis oficinas físicas en Madrid? Me gustaría visitaros.",
    "Os escribo para proponer una colaboración con nuestro podcast de tecnología.",
    "¿Cómo puedo enviar mi currículum para las vacantes de ingeniería?",
    "Enhorabuena por el rediseño de la web, ha quedado genial.",
    "Soy periodista y me gustaría entrevistar a vuestro director general.",
    "¿En qué horario está disponible el soporte telefónico?",
    "Me gustaría daros feedback sobre la nueva interfaz, tengo varias ideas.",
    "¿Vais a asistir a la feria tecnológica de Barcelona este año?",
    "No encuentro el enlace para daros de baja de la newsletter.",
    "¿Tenéis programa de afiliados para creadores de contenido?",
    "Quisiera saber si la empresa tiene certificación ISO veintisiete mil uno.",
    "Estoy escribiendo un trabajo universitario sobre vuestro sector, ¿me ayudáis?",
    "El enlace de vuestro blog sobre novedades está roto.",
    "¿Puedo usar vuestro logo en una presentación interna de mi empresa?",
    "Me han hablado muy bien de vosotros en un evento, solo quería saludar.",
    "¿Dónde puedo ver los términos y condiciones del sorteo de redes sociales?",
    "Sugerencia: estaría bien que la web tuviera modo oscuro.",
    "¿Cuál es la dirección postal para enviaros una notificación formal?",
    "He recibido un correo sospechoso que suplanta a vuestra empresa.",
    "¿Ofrecéis visitas guiadas a estudiantes de formación profesional?",
    "Quería felicitar al equipo de soporte por la atención del otro día.",
    "¿Tenéis versión de la web en catalán o en inglés?",
    "Me gustaría suscribirme a las notas de prensa de la compañía.",
]

# Unimos todo en dos listas paralelas: textos y etiquetas
textos = TICKETS_FACTURACION + TICKETS_TECNICO + TICKETS_VENTAS + TICKETS_OTROS
etiquetas = [0] * 25 + [1] * 25 + [2] * 25 + [3] * 25

NOMBRES_CLASES = ["facturacion", "tecnico", "ventas", "otros"]
print(f"Total: {len(textos)} tickets, {len(set(etiquetas))} clases")

Nota

100 ejemplos es poquísimo para entrenar desde cero... pero es suficiente para afinar un modelo preentrenado. Esa es exactamente la magia del transfer learning: BETO ya sabe español; solo tiene que aprender a distinguir 4 categorías. En producción, con 500–2.000 ejemplos por clase obtendrás resultados sólidos.

4.2 Tokenizar el dataset y crear DataLoaders

import torch
from torch.utils.data import Dataset, DataLoader
from sklearn.model_selection import train_test_split
from transformers import AutoTokenizer

# --- 1. Separar train/test de forma estratificada ---
# stratify=etiquetas garantiza la misma proporción de clases en ambos splits.
textos_train, textos_test, y_train, y_test = train_test_split(
    textos, etiquetas,
    test_size=0.2,        # 80 tickets para entrenar, 20 para evaluar
    stratify=etiquetas,   # 5 tickets de cada clase en test
    random_state=42       # reproducibilidad
)

# --- 2. Tokenizer del modelo que vamos a afinar ---
tokenizer = AutoTokenizer.from_pretrained("dccuchile/bert-base-spanish-wwm-cased")

# --- 3. Dataset de PyTorch: la misma clase que escribiste en el capítulo 3 ---
class TicketsDataset(Dataset):
    """Envuelve textos + etiquetas y tokeniza al vuelo."""

    def __init__(self, textos, etiquetas, tokenizer, max_length=64):
        self.textos = textos
        self.etiquetas = etiquetas
        self.tokenizer = tokenizer
        self.max_length = max_length   # los tickets son cortos: 64 tokens sobran

    def __len__(self):
        return len(self.textos)        # cuántos ejemplos hay

    def __getitem__(self, idx):
        # Tokenizamos UN ticket con padding fijo (simplifica el batching)
        enc = self.tokenizer(
            self.textos[idx],
            padding="max_length",      # rellena hasta max_length
            truncation=True,           # corta si excede
            max_length=self.max_length,
            return_tensors="pt"
        )
        return {
            # squeeze(0) quita la dimensión de batch=1 que añade return_tensors
            "input_ids": enc["input_ids"].squeeze(0),
            "attention_mask": enc["attention_mask"].squeeze(0),
            "labels": torch.tensor(self.etiquetas[idx])   # la clave DEBE llamarse
        }                                                  # "labels" para HF

# --- 4. Instanciar datasets y DataLoaders ---
train_ds = TicketsDataset(textos_train, y_train, tokenizer)
test_ds = TicketsDataset(textos_test, y_test, tokenizer)

train_loader = DataLoader(train_ds, batch_size=8, shuffle=True)   # barajar en train
test_loader = DataLoader(test_ds, batch_size=8, shuffle=False)    # orden fijo en test

# Verificación: inspecciona SIEMPRE un batch antes de entrenar
batch = next(iter(train_loader))
print(batch["input_ids"].shape)      # torch.Size([8, 64])  → 8 tickets × 64 tokens
print(batch["attention_mask"].shape) # torch.Size([8, 64])
print(batch["labels"])               # tensor([2, 0, 3, 1, ...]) → las 4 clases

4.3 Versión A: training loop manual en PyTorch

Primero, el entrenamiento a mano, reutilizando el patrón exacto del capítulo 3 (forward → loss → backward → step). Objetivo pedagógico: que veas con tus propios ojos que afinar un Transformer no tiene ninguna magia nueva; es el mismo bucle con el que entrenaste tu primera red.

import torch
from transformers import AutoModelForSequenceClassification

# --- 1. Dispositivo: GPU si hay, CPU si no (este modelo corre bien en CPU) ---
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# --- 2. Modelo preentrenado + cabeza nueva de 4 clases ---
modelo = AutoModelForSequenceClassification.from_pretrained(
    "dccuchile/bert-base-spanish-wwm-cased",
    num_labels=4
).to(device)                       # mover los 110M de parámetros al dispositivo

# --- 3. Optimizador: AdamW con learning rate PEQUEÑO ---
# Regla de oro del fine-tuning: lr entre 1e-5 y 5e-5. Con lr "normales" (1e-3)
# destruirías en pocas iteraciones los pesos preentrenados que tanto valen.
optimizador = torch.optim.AdamW(modelo.parameters(), lr=2e-5)

EPOCHS = 4                         # con pocos datos, pocas épocas: el modelo
                                   # preentrenado converge rapidísimo (y sobreajusta
                                   # rápido también)

# --- 4. EL MISMO BUCLE DEL CAPÍTULO 3 ---
for epoch in range(EPOCHS):
    modelo.train()                             # modo entrenamiento (activa dropout)
    perdida_total = 0.0

    for batch in train_loader:
        # a) Mover el batch al dispositivo
        batch = {k: v.to(device) for k, v in batch.items()}

        # b) Forward. Detalle de HF: si pasas "labels", el modelo calcula
        #    la CrossEntropyLoss internamente y la devuelve en outputs.loss.
        #    (Podrías ignorarla y calcularla tú con nn.CrossEntropyLoss:
        #    daría exactamente lo mismo.)
        outputs = modelo(**batch)              # ** desempaqueta input_ids,
                                               # attention_mask y labels
        loss = outputs.loss

        # c) Backward: calcular gradientes
        optimizador.zero_grad()                # limpiar gradientes anteriores
        loss.backward()                        # backpropagation por TODO el modelo

        # d) (Recomendado en Transformers) recortar gradientes para estabilidad
        torch.nn.utils.clip_grad_norm_(modelo.parameters(), max_norm=1.0)

        # e) Step: actualizar los 110M de parámetros
        optimizador.step()

        perdida_total += loss.item()

    print(f"Época {epoch + 1}/{EPOCHS} — pérdida media: "
          f"{perdida_total / len(train_loader):.4f}")

# Salida típica (los números exactos varían):
# Época 1/4 — pérdida media: 1.31   ← cerca de ln(4)=1.39: aún adivina al azar
# Época 2/4 — pérdida media: 0.72   ← aprendiendo rápido
# Época 3/4 — pérdida media: 0.31
# Época 4/4 — pérdida media: 0.14   ← ha aprendido la tarea

Fíjate en la primera pérdida: ≈1.31, muy cerca de ln(4) ≈ 1.386, la pérdida de un clasificador de 4 clases que responde al azar. Es la cabeza aleatoria hablando. Cuatro épocas después, el backbone preentrenado ha "prestado" todo su conocimiento del español y la pérdida se desploma. Con 80 ejemplos. Intenta conseguir eso entrenando desde cero.

Consejo profesional

¿Cuándo congelar el backbone y entrenar solo la cabeza (como hacías a veces en visión)? Con datasets minúsculos (<100 ejemplos) o si no tienes GPU, congelar (for p in modelo.bert.parameters(): p.requires_grad = False) es una opción rápida y decente. Pero en NLP, si puedes permitirte afinarlo todo con lr bajo, casi siempre rinde mejor: el lenguaje de tus tickets ("no me llega la factura") se beneficia de ajustar también las capas intermedias.

4.4 Versión B: Trainer de Hugging Face

El bucle manual es idéntico en cada proyecto, así que Hugging Face lo empaquetó (con extras: logging, checkpoints, evaluación periódica, mixed precision, entrenamiento distribuido...) en la clase Trainer. La configuración vive en TrainingArguments. Vamos argumento a argumento:

import numpy as np
from transformers import (
    AutoModelForSequenceClassification,
    TrainingArguments,
    Trainer,
)
from sklearn.metrics import accuracy_score, f1_score

# --- 1. Modelo fresco (para comparar en igualdad de condiciones) ---
modelo = AutoModelForSequenceClassification.from_pretrained(
    "dccuchile/bert-base-spanish-wwm-cased",
    num_labels=4,
    # Mapeos opcionales pero MUY recomendables: viajan con el modelo guardado
    # y hacen que la inferencia devuelva nombres legibles en vez de LABEL_2.
    id2label={0: "facturacion", 1: "tecnico", 2: "ventas", 3: "otros"},
    label2id={"facturacion": 0, "tecnico": 1, "ventas": 2, "otros": 3},
)

# --- 2. Función de métricas: el Trainer la llama en cada evaluación ---
def calcular_metricas(eval_pred):
    logits, labels = eval_pred                 # predicciones crudas y verdad
    preds = np.argmax(logits, axis=-1)         # logit mayor → clase predicha
    return {
        "accuracy": accuracy_score(labels, preds),
        "f1_macro": f1_score(labels, preds, average="macro"),  # media por clase:
    }                                           # justa aunque haya desbalanceo

# --- 3. TrainingArguments: cada argumento explicado ---
args = TrainingArguments(
    output_dir="./resultados-tickets",   # dónde guardar checkpoints y logs
    num_train_epochs=4,                  # mismas 4 épocas que en la versión manual
    per_device_train_batch_size=8,       # batch de entrenamiento (por GPU/CPU)
    per_device_eval_batch_size=16,       # en eval no hay gradientes: cabe más
    learning_rate=2e-5,                  # el lr pequeño canónico de fine-tuning
    weight_decay=0.01,                   # regularización L2 (AdamW)
    warmup_ratio=0.1,                    # 10% inicial de pasos subiendo el lr
                                         # desde 0: estabiliza el arranque
    eval_strategy="epoch",               # evaluar al final de cada época
    save_strategy="epoch",               # guardar checkpoint en cada época
    load_best_model_at_end=True,         # al acabar, recargar el MEJOR checkpoint
    metric_for_best_model="f1_macro",    # ...según esta métrica (no la última época)
    logging_steps=5,                     # imprimir la pérdida cada 5 pasos
    report_to="none",                    # sin wandb/tensorboard en este ejemplo
    seed=42,                             # reproducibilidad
)

# --- 4. El Trainer une todas las piezas ---
trainer = Trainer(
    model=modelo,                # el modelo con cabeza nueva
    args=args,                   # la configuración de arriba
    train_dataset=train_ds,      # nuestro Dataset de PyTorch (¡sirve tal cual!)
    eval_dataset=test_ds,        # dataset de evaluación
    compute_metrics=calcular_metricas,   # nuestras métricas de sklearn
)

# --- 5. Entrenar: una línea que hace TODO el bucle de la versión A ---
trainer.train()

# --- 6. Evaluación final con el mejor checkpoint ---
print(trainer.evaluate())
# {'eval_loss': 0.18, 'eval_accuracy': 0.95, 'eval_f1_macro': 0.95, ...}

Nota

Trainer no es un algoritmo distinto: ejecuta internamente el mismo bucle de la versión A. Escribimos ambas versiones para inmunizarte contra el pensamiento mágico: cuando el Trainer falle o necesites algo exótico, sabrás bajar al metal. En el día a día profesional, usarás Trainer (o librerías equivalentes) por productividad y porque trae bien resueltos detalles como warmup, checkpoints y mixed precision.

4.5 Evaluación con sklearn

Accuracy global no basta: necesitas saber en qué clases falla el modelo. Informe completo:

import numpy as np
from sklearn.metrics import classification_report, confusion_matrix

# Predicciones del Trainer sobre el conjunto de test
predicciones = trainer.predict(test_ds)
y_pred = np.argmax(predicciones.predictions, axis=-1)  # logits → clase
y_true = predicciones.label_ids                        # etiquetas reales

# Informe por clase: precision, recall y F1 de cada categoría
print(classification_report(y_true, y_pred, target_names=NOMBRES_CLASES))
#               precision    recall  f1-score   support
#  facturacion       1.00      1.00      1.00         5
#      tecnico       1.00      0.80      0.89         5
#       ventas       1.00      1.00      1.00         5
#        otros       0.83      1.00      0.91         5

# Matriz de confusión: filas = clase real, columnas = clase predicha
print(confusion_matrix(y_true, y_pred))
# [[5 0 0 0]
#  [0 4 0 1]   ← un ticket técnico confundido con "otros"
#  [0 0 5 0]
#  [0 0 0 5]]

Advertencia

Con solo 20 ejemplos de test, estas métricas tienen una varianza enorme: cada ticket vale un 5 % de accuracy. En un proyecto real necesitas cientos de ejemplos de test como mínimo, y si el dataset es pequeño, validación cruzada. Nunca tomes decisiones de negocio con un test set de 20 ejemplos; aquí lo aceptamos porque el objetivo es aprender la mecánica.

4.6 Guardar, cargar e inferencia final

# --- Guardar: modelo Y tokenizer juntos (siempre juntos) ---
ruta = "./clasificador-tickets-es"
trainer.save_model(ruta)              # pesos + configuración + id2label
tokenizer.save_pretrained(ruta)       # vocabulario + reglas de tokenización
# El directorio resultante es autocontenido: puedes moverlo a otro servidor,
# subirlo al Hub privado de tu empresa o meterlo en una imagen Docker.

# --- Cargar en producción (otro proceso, otra máquina) ---
from transformers import pipeline

clasificador_tickets = pipeline(
    "text-classification",
    model=ruta,                       # carga desde el directorio local
    tokenizer=ruta,
)

# --- Inferencia final con tickets nuevos, jamás vistos ---
nuevos = [
    "No me deja pagar con la tarjeta nueva y me sale un cargo raro",
    "La página se queda en blanco al entrar al panel de administración",
    "¿Cuánto costaría el plan para un equipo de 80 personas?",
    "Quería felicitaros por el evento del jueves, estuvo genial",
]
for ticket, pred in zip(nuevos, clasificador_tickets(nuevos)):
    print(f"[{pred['label']:<12}] ({pred['score']:.2f}) {ticket}")
# [facturacion ] (0.97) No me deja pagar con la tarjeta nueva...
# [tecnico     ] (0.98) La página se queda en blanco al entrar...
# [ventas      ] (0.99) ¿Cuánto costaría el plan para un equipo...
# [otros       ] (0.95) Quería felicitaros por el evento...

Acabas de completar el ciclo profesional completo: datos → tokenización → modelo preentrenado + cabeza → entrenamiento → evaluación → serialización → inferencia. Este flujo, con más datos y más rigor en la evaluación, es exactamente el que se despliega en miles de empresas.

Ejercicio rápido 4

El clasificador confunde a veces "no me deja pagar" (¿facturación o técnico?). Propón dos mejoras al sistema (no al modelo) para manejar tickets ambiguos en producción.

Ver solución 1. **Umbral de confianza con cola humana:** si la probabilidad máxima (`score`) queda por debajo de un umbral (p. ej. 0,80), el ticket no se enruta automáticamente sino que va a una bandeja de revisión humana. Ajustas el umbral midiendo el compromiso entre porcentaje automatizado y tasa de error. Esto convierte un clasificador imperfecto en un sistema útil y seguro. 2. **Etiquetado multi-etiqueta o clase "mixta":** algunos tickets SON de dos departamentos a la vez ("no puedo pagar" = fallo técnico en el proceso de facturación). Puedes (a) reformular el problema como multi-label (sigmoid por clase en vez de softmax), o (b) crear reglas de negocio: si las dos clases top están muy próximas en probabilidad, enviar a ambos departamentos o al que tenga SLA más estricto. Bonus: registra estos casos ambiguos: son oro para re-etiquetar y re-entrenar (el "data flywheel" que verás en módulos posteriores).

5. Fine-tuning eficiente: LoRA y PEFT

5.1 El problema: afinar modelos gigantes no cabe en tu GPU

Nuestro BETO tiene 110M de parámetros: se afina en un portátil. Pero ¿y si quieres afinar un LLM open source de 7.000 millones de parámetros? Haz las cuentas del fine-tuning completo con el optimizador Adam:

  • Pesos en fp16: 7B × 2 bytes = 14 GB
  • Gradientes: otros 14 GB
  • Estados de Adam (momento y varianza, en fp32): ~56 GB
  • Total: más de 80 GB de VRAM solo para empezar, sin contar activaciones.

Eso es territorio de GPUs de datacenter (o varias). Además, cada tarea afinada produce una copia completa de 14 GB del modelo: afinar 10 tareas = almacenar y servir 10 modelos gigantes.

5.2 La idea de LoRA: congelar todo, aprender poco

LoRA (Low-Rank Adaptation, Hu et al., 2021) parte de una observación empírica: cuando afinas un modelo preentrenado, el cambio que necesitan los pesos (ΔW) es de bajo rango: la adaptación a una tarea concreta no requiere mover los pesos en 4.096 direcciones independientes; con unas pocas decenas de direcciones basta. Entonces, ¿para qué aprender la matriz de cambio completa?

En lugar de actualizar la matriz original W (por ejemplo, 4096 × 4096 ≈ 16,8M de parámetros), LoRA:

  1. Congela W por completo (no recibe gradientes, no se toca).
  2. Añade en paralelo dos matrices pequeñas: A (4096 × r) y B (r × 4096), con r = 8 o 16 típicamente.
  3. La salida de la capa pasa a ser h = W·x + B·A·x (el desvío de bajo rango se suma al camino original).
  4. Solo A y B se entrenan: 4096×8 + 8×4096 ≈ 65k parámetros en vez de 16,8M. Un 0,4 %.
flowchart TD
    X["Entrada x"] --> W["W original<br/>4096 × 4096<br/> CONGELADA"]
    X --> A["A: 4096 × r<br/> entrenable"]
    A --> B["B: r × 4096<br/> entrenable"]
    W --> S(("+"))
    B --> S
    S --> H["Salida h = Wx + BAx"]
    style W fill:#b3d9ff,stroke:#336
    style A fill:#ffcccc,stroke:#933
    style B fill:#ffcccc,stroke:#933

Órdenes de magnitud del ahorro (modelo tipo 7B, cifras aproximadas para fijar intuición):

Recurso Fine-tuning completo LoRA (r=16) Factor de ahorro
Parámetros entrenables 7.000M (100 %) ~10–40M (~0,1–0,5 %) ~200–1000×
VRAM de entrenamiento > 80 GB (multi-GPU) ~16–20 GB (1 GPU buena) ~4–5×
VRAM con QLoRA (base cuantizada a 4 bits) ~6–10 GB (GPU de consumo) ~10×
Almacenamiento por tarea afinada ~14 GB (copia completa) ~10–100 MB (solo adaptadores) ~100–1000×
¿Riesgo de "olvido catastrófico" del modelo base? Alto Bajo (la base está intacta)

La última fila esconde una ventaja operativa enorme: como el modelo base no se toca, puedes tener un solo modelo de 14 GB en memoria y cambiar de "personalidad" cargando adaptadores de 50 MB: uno para legal, otro para soporte, otro para marketing.

5.3 PEFT: la librería

PEFT (Parameter-Efficient Fine-Tuning) es la librería de Hugging Face que implementa LoRA y técnicas hermanas. Aplicarla a nuestro clasificador de tickets es sorprendentemente poco código:

from peft import LoraConfig, get_peft_model, TaskType
from transformers import AutoModelForSequenceClassification

# 1. Cargar el modelo base, igual que siempre
modelo_base = AutoModelForSequenceClassification.from_pretrained(
    "dccuchile/bert-base-spanish-wwm-cased", num_labels=4
)

# 2. Configurar LoRA
config_lora = LoraConfig(
    task_type=TaskType.SEQ_CLS,      # tarea: clasificación de secuencias
    r=8,                             # rango de las matrices A y B
    lora_alpha=16,                   # factor de escala del desvío (≈ 2·r es habitual)
    lora_dropout=0.1,                # dropout sobre el camino LoRA
    target_modules=["query", "value"],  # a qué matrices añadir adaptadores:
)                                    # las proyecciones Q y V de la atención
                                     # (lo que el paper original encontró suficiente)

# 3. Envolver el modelo: congela todo y añade los adaptadores
modelo_lora = get_peft_model(modelo_base, config_lora)
modelo_lora.print_trainable_parameters()
# trainable params: ~300k || all params: ~110M || trainable%: ~0.27
# ¡Entrenamos el 0,27 % del modelo!

# 4. A partir de aquí, TODO IGUAL: el mismo Trainer o el mismo bucle manual
#    de la sección 4 funcionan sin cambiar una línea. Al guardar:
modelo_lora.save_pretrained("./adaptador-tickets")   # guarda SOLO el adaptador (~MBs)

Nota

En un modelo de 110M como BETO, LoRA no aporta gran cosa (el fine-tuning completo ya es barato) y puede rendir un pelín por debajo. Su terreno natural son los modelos de miles de millones de parámetros, donde es la diferencia entre "imposible" y "una tarde con una GPU de consumo". QLoRA va un paso más allá: carga el modelo base cuantizado a 4 bits (los adaptadores siguen en precisión alta), bajando aún más la VRAM necesaria. Todo esto —elegir r, qué módulos adaptar, fusionar adaptadores, QLoRA en detalle— se practica a fondo en el módulo 16-OPEN-SOURCE-LLMS.

Ejercicio rápido 5

Tu empresa quiere adaptar el mismo modelo 7B a 12 departamentos distintos, cada uno con su estilo de respuesta. Compara el coste de almacenamiento y despliegue de "12 fine-tunings completos" vs "12 adaptadores LoRA".

Ver solución **12 fine-tunings completos:** 12 copias × ~14 GB = **~168 GB** de modelos. Para servirlos con baja latencia necesitarías o bien 12 instancias de GPU (una por modelo, carísimo) o cargar/descargar modelos de 14 GB bajo demanda (lentísimo, decenas de segundos por cambio). Cada actualización del modelo base obliga a repetir 12 entrenamientos completos. **12 adaptadores LoRA:** 1 copia del modelo base (~14 GB) + 12 adaptadores × ~50 MB = **~14,6 GB** en total. Una sola GPU sirve el modelo base y cambia de adaptador en milisegundos (hay servidores de inferencia que incluso sirven múltiples adaptadores LoRA simultáneamente sobre la misma base). Actualizar el modelo base requiere re-entrenar los adaptadores, pero cada uno es horas, no días. La decisión es tan desequilibrada que en la práctica los despliegues multi-tarea sobre LLMs open source son territorio LoRA casi por definición.

6. Embeddings como producto: sentence-transformers

6.1 Por qué no basta con promediar word embeddings

En el módulo 01 hiciste un buscador con bag-of-words y viste su límite: "no puedo entrar en mi cuenta" y "problemas para iniciar sesión" no comparten palabras, así que para BoW son documentos sin relación. Los word embeddings mejoran esto, y podrías pensar: "promedio los embeddings de las palabras de la frase y ya tengo el embedding de la frase". Funciona regular, por dos razones:

  1. El promedio destruye la composición. "El banco rechazó el pago" y "el pago rechazó el banco" promedian a lo mismo. Y la negación ("me gusta" / "no me gusta") apenas mueve el promedio.
  2. Los vectores de modelos tipo BERT sin ajustar no están optimizados para similitud. Sorpresa contraintuitiva: los embeddings crudos de BERT, promediados, funcionan peor que métodos clásicos en tareas de similitud de frases. BERT fue entrenado para rellenar huecos, no para que frases parecidas queden cerca en el espacio.

sentence-transformers (Reimers y Gurevych, 2019, con SBERT) resuelve esto afinando el Transformer con pares de frases (parecidas/no parecidas) y un objetivo contrastivo: acercar en el espacio vectorial las frases que significan lo mismo y alejar las que no. El resultado: un modelo cuyo output es un único vector por frase, diseñado específicamente para que la similitud coseno signifique similitud semántica.

6.2 Búsqueda semántica real en 30 líneas

Cerremos el círculo del módulo 01, esta vez con embeddings de verdad:

from sentence_transformers import SentenceTransformer
import numpy as np

# Modelo multilingüe real y veterano (50+ idiomas, incluido español),
# compacto (~470 MB) y suficiente para prototipos serios:
modelo = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2")

# 20 frases en español: nuestra "base de conocimiento" de ejemplo
frases = [
    "No puedo iniciar sesión en la plataforma.",
    "He olvidado mi contraseña de acceso.",
    "La aplicación se cierra inesperadamente.",
    "El programa falla al arrancar en Windows.",
    "¿Cuánto cuesta el plan para empresas?",
    "Quisiera conocer los precios de las licencias corporativas.",
    "La factura de este mes tiene un importe incorrecto.",
    "Me han cobrado de más en el último recibo.",
    "¿Cómo exporto mis datos a Excel?",
    "Necesito descargar mis informes en formato de hoja de cálculo.",
    "El gato duerme plácidamente al sol.",
    "Mi perro corre feliz por el parque.",
    "La reunión del lunes se pospone al miércoles.",
    "Cambiamos la fecha de la junta al jueves.",
    "El servidor no responde a las peticiones.",
    "La API devuelve errores de tiempo de espera.",
    "Quiero cancelar mi suscripción de inmediato.",
    "Deseo darme de baja del servicio.",
    "La paella valenciana lleva arroz y azafrán.",
    "Para hacer un buen gazpacho hacen falta tomates maduros.",
]

# 1. Embeber: cada frase → vector de 384 dimensiones
# normalize_embeddings=True: vectores de norma 1, así el producto punto
# ES directamente la similitud coseno.
embeddings = modelo.encode(frases, normalize_embeddings=True)
print(embeddings.shape)   # (20, 384)

# 2. Matriz de similitud: producto de la matriz por su traspuesta
similitud = embeddings @ embeddings.T   # (20, 20), valores en [-1, 1]

# 3. Veamos los pares más parecidos (fuera de la diagonal)
n = len(frases)
pares = [(similitud[i, j], i, j) for i in range(n) for j in range(i + 1, n)]
for sim, i, j in sorted(pares, reverse=True)[:5]:
    print(f"{sim:.2f} | {frases[i]}  <->  {frases[j]}")
# 0.8x | ¿Cuánto cuesta el plan para empresas? <-> Quisiera conocer los precios...
# 0.8x | Quiero cancelar mi suscripción... <-> Deseo darme de baja del servicio.
# 0.7x | La reunión del lunes se pospone... <-> Cambiamos la fecha de la junta...
# ...
# Fíjate: los pares correctos emergen SIN compartir apenas palabras.
# "cancelar suscripción" y "darme de baja" no comparten ni una palabra clave.

# 4. Mini búsqueda semántica: la esencia de un sistema RAG
def buscar(consulta, k=3):
    """Devuelve las k frases más parecidas semánticamente a la consulta."""
    q = modelo.encode([consulta], normalize_embeddings=True)  # embeber la consulta
    scores = (q @ embeddings.T)[0]          # similitud contra toda la base
    top = np.argsort(scores)[::-1][:k]      # índices de mayor a menor
    return [(frases[i], float(scores[i])) for i in top]

for frase, score in buscar("problemas para acceder a mi cuenta"):
    print(f"{score:.2f}  {frase}")
# 0.7x  No puedo iniciar sesión en la plataforma.
# 0.6x  He olvidado mi contraseña de acceso.
# 0.4x  El servidor no responde a las peticiones.

Compara con tu buscador bag-of-words del módulo 01: "problemas para acceder a mi cuenta" no comparte ninguna palabra significativa con "no puedo iniciar sesión en la plataforma", y aun así es el primer resultado. Esto es comprensión semántica como producto, y es el motor de recuperación que alimentará tus sistemas RAG en el módulo 06: sustituye las 20 frases por los 20.000 párrafos de la documentación de tu empresa, añade una base de datos vectorial para buscar eficientemente, y pásale los resultados a un LLM para que redacte la respuesta. Ese es, esencialmente, el pipeline RAG completo.

Consejo profesional

Para producción en español/multilingüe, evalúa siempre 2–3 modelos de embeddings sobre tus datos (el leaderboard MTEB en el Hub es el punto de partida para preseleccionar). Los modelos de embeddings son pequeños y baratos de cambiar; una mejora de 5 puntos en recuperación transforma la calidad de todo el sistema RAG que construyas encima.


7. Del Transformer al LLM moderno

Tu mini-GPT del capítulo 6 y los LLMs que usarás en el módulo 04 comparten el mismo ADN arquitectónico. ¿Qué los separa entonces? Cuatro ingredientes.

7.1 Escala: las scaling laws revisitadas

Las scaling laws (leyes de escala) son el descubrimiento empírico de que la pérdida de un modelo de lenguaje mejora de forma suave y predecible al aumentar tres cantidades a la vez: parámetros, datos y cómputo. No hay que inventar arquitecturas nuevas: la misma receta, multiplicada por mil, da resultados cualitativamente distintos. El refinamiento clave (trabajo conocido como "Chinchilla", 2022) demostró además que muchos modelos estaban infraentrenados: para un cómputo dado, conviene equilibrar tamaño del modelo y cantidad de tokens de entrenamiento, lo que llevó a la era de modelos "más pequeños pero entrenados con muchísimos más datos" que hoy permite que modelos abiertos de tamaño modesto sean sorprendentemente capaces.

Y con la escala llegó la sorpresa: capacidades emergentes. Habilidades que los modelos pequeños simplemente no tienen (aritmética de varios pasos, seguir cadenas de razonamiento, traducir idiomas poco vistos) aparecen al cruzar ciertos umbrales de escala, sin haber sido programadas ni entrenadas explícitamente. Tu mini-GPT completa texto plausible; un modelo mil veces mayor, entrenado con la misma tarea de "predecir el siguiente token", resuelve problemas.

7.2 Instruction tuning: de completar texto a seguir instrucciones

Un modelo base (como GPT-3 original, o tu mini-GPT) solo hace una cosa: continuar texto. Eso produce comportamientos desconcertantes:

Prompt del usuario Modelo base (solo completa) Modelo instruction-tuned
"Explica qué es la fotosíntesis" "Explica qué es la respiración celular. Explica qué es la mitosis. Explica..." (continúa la lista: parece un examen) "La fotosíntesis es el proceso por el cual las plantas convierten luz solar, agua y CO₂ en glucosa y oxígeno..."
"Traduce al inglés: buenos días" "Traduce al francés: buenas tardes. Traduce al alemán: buenas noches." (sigue el patrón del texto) "Good morning"
"¿Es seguro mezclar lejía y amoniaco?" Cualquier continuación estadísticamente plausible, incluida una receta "No. Mezclar lejía y amoniaco produce cloraminas, gases tóxicos..."

El instruction tuning es un fine-tuning (¡el mismo que hiciste en la sección 4, conceptualmente!) sobre decenas de miles de pares (instrucción → respuesta deseada) escritos o curados por humanos. Convierte un "autocompletador de internet" en un asistente que entiende que un prompt es una petición, no un texto a continuar.

7.3 RLHF y alineamiento: el entrenador con feedback humano

El instruction tuning enseña el formato, pero ¿cómo enseñar preferencias difusas como "sé útil, no inventes, rechaza peticiones dañinas, admite incertidumbre"? No puedes escribir la respuesta perfecta a todo, pero los humanos sí saben comparar: "de estas dos respuestas, esta es mejor".

RLHF (Reinforcement Learning from Human Feedback, la técnica central del paper de InstructGPT, 2022) explota eso en tres pasos, que puedes imaginar como un entrenador deportivo:

  1. Recoger comparaciones: el modelo genera varias respuestas al mismo prompt; anotadores humanos las ordenan de mejor a peor.
  2. Entrenar un modelo de recompensa: una red que aprende a predecir qué puntuación le daría un humano a una respuesta. Es el "criterio del entrenador" destilado en un modelo.
  3. Optimizar el LLM contra esa recompensa con aprendizaje por refuerzo: el modelo genera, el modelo de recompensa puntúa, y los pesos se ajustan para producir respuestas mejor puntuadas.

El resultado del paper de InstructGPT fue histórico: un modelo de 1.3B parámetros alineado con RLHF era preferido por los humanos frente al GPT-3 base de 175B. El alineamiento no es cosmética: multiplica la utilidad real. Desde entonces han surgido variantes y alternativas (aprendizaje por preferencias directas, feedback generado con ayuda de IA guiada por principios, como el enfoque Constitutional AI de Anthropic), pero la idea rectora es la misma: optimizar contra juicios de preferencia, no solo contra "predecir el siguiente token de internet".

7.4 La línea evolutiva completa

Generación Hito Qué demostró
GPT-2 (2019) Generación de texto coherente a escala de párrafos Predecir el siguiente token, con suficiente escala, produce texto fluido y algo de transferencia entre tareas
GPT-3 (2020) In-context learning con 175B parámetros Un modelo suficientemente grande aprende tareas nuevas desde el prompt, sin actualizar pesos: nace el prompting como paradigma
InstructGPT / ChatGPT (2022) Instruction tuning + RLHF Alinear el modelo con intenciones humanas lo convierte en asistente; la interfaz conversacional lo lleva al gran público
Modelos actuales: Claude, GPT, Gemini, Llama y ecosistema open source (2023 → hoy) Contextos largos, multimodalidad (texto, imagen, audio), uso de herramientas, razonamiento extendido, agentes El LLM deja de ser un generador de texto y pasa a ser una plataforma de cómputo general sobre lenguaje
flowchart LR
    A["Tu mini-GPT<br/>(capítulo 6)<br/>completa texto"] -->|"× escala<br/>(scaling laws)"|B["Modelo base grande<br/>in-context learning"]
    B -->|"instruction<br/>tuning"|C["Sigue<br/>instrucciones"]
    C -->|"RLHF /<br/>alineamiento"|D["Asistente útil,<br/>honesto e inocuo"]
    D -->|"herramientas, memoria,<br/>contexto largo"|E["LLMs y agentes actuales<br/>(módulo 04 en adelante)"]

Ejercicio rápido 6

Un compañero afirma: "los LLMs son una arquitectura completamente nueva; mi mini-GPT no tiene nada que ver con ellos". Rebátelo en tres frases usando lo aprendido en esta sección.

Ver solución 1. La arquitectura es esencialmente la misma: bloques Transformer con atención apilados que predicen el siguiente token, exactamente lo que implementaste en el capítulo 6 (con refinamientos incrementales: normalizaciones y funciones de activación mejoradas, codificaciones posicionales más modernas, atención optimizada en memoria). 2. Lo que separa tu mini-GPT de un LLM no es una idea arquitectónica nueva sino **escala** (parámetros, datos y cómputo multiplicados por órdenes de magnitud, siguiendo las scaling laws) más **dos fases de entrenamiento adicionales**: instruction tuning y alineamiento con feedback humano. 3. La prueba de que es el mismo linaje: las capacidades emergentes aparecen escalando la *misma* receta de "predecir el siguiente token", sin cambiar el objetivo de entrenamiento que tú ya programaste.

Qué significa esto para ti: en los capítulos 1–7 de este módulo construiste el motor: tensores, backprop, CNNs, transfer learning, redes recurrentes, atención, Transformers. Sabes exactamente qué hay dentro de la caja. A partir del módulo 04, dejas de construir el motor y aprendes a conducirlo profesionalmente: prompting sistemático, APIs, evaluación de LLMs, y después RAG, agentes y fine-tuning de modelos abiertos. La diferencia entre un "usuario de ChatGPT" y un ingeniero de IA es que tú sabrás por qué el modelo hace lo que hace, y por tanto sabrás cuándo confiar, cuándo dudar y cómo arreglarlo.


8. Decisiones profesionales: 4 escenarios de arquitecto

La jerarquía de adaptación de la sección 1.3 solo vale si sabes aplicarla. Cuatro escenarios reales, resueltos y argumentados:

Escenario 1 — Startup: clasificar 200 solicitudes/día con 50 ejemplos

Contexto: una startup de 8 personas quiere priorizar automáticamente las solicitudes que llegan por el formulario web. Tienen 50 ejemplos etiquetados y ni un solo perfil de ML dedicado.

Decisión: Nivel 1 — Prompting sobre un LLM vía API. - Por qué: con 50 ejemplos no puedes hacer fine-tuning serio (ni evaluar con rigor). Un prompt con instrucciones claras + 5–10 ejemplos bien elegidos (few-shot) rinde muy bien en clasificación sencilla. El volumen (200/día) hace el coste de API despreciable (céntimos o pocos euros al día). - Beneficio: en producción en 2 días. Cambiar las categorías cuesta editar el prompt, no re-entrenar. - Riesgos: dependencia del proveedor y latencia de red (asumibles a este volumen). Trampa a evitar: montar infraestructura de fine-tuning "para cuando crezcamos". Cuando crezcan, migrarán con datos reales acumulados; hoy sería sobreingeniería.

Escenario 2 — Empresa con 10.000 tickets históricos y requisitos de privacidad

Contexto: una empresa de salud tiene 10k tickets etiquetados por 6 categorías y una restricción dura: los datos no pueden salir de su infraestructura (datos médicos, cumplimiento normativo).

Decisión: Nivel 4 — Fine-tuning completo de un modelo open source tipo BERT, on-premise. - Por qué: la restricción de privacidad descarta APIs externas de entrada. Con 10k ejemplos etiquetados, un BERT/RoBERTa en español afinado (exactamente lo que hiciste en la sección 4) alcanzará métricas excelentes en una tarea cerrada de clasificación. El modelo resultante es pequeño (~110M), corre en CPU o GPU modesta, latencia de milisegundos, coste marginal por predicción ≈ 0. - Beneficio: control total, datos en casa, sin coste por llamada, auditable. - Riesgos: necesitan alguien que mantenga el modelo (reentrenos periódicos cuando cambie la distribución de tickets: drift). Es un coste de personal real que hay que presupuestar, no un "se entrena una vez y listo".

Escenario 3 — Corporación con documentación técnica que cambia cada semana

Contexto: una corporación industrial quiere que sus 3.000 técnicos consulten en lenguaje natural manuales y procedimientos que se actualizan semanalmente. Las respuestas deben citar el documento fuente (responsabilidad legal).

Decisión: Nivel 2 — RAG. - Por qué: el conocimiento cambia cada semana: cualquier fine-tuning estaría desactualizado antes de terminar de entrenarse. RAG separa el conocimiento (índice de documentos, re-indexable en minutos) del razonamiento (el LLM). Y la citación de fuentes es nativa en RAG: la respuesta se construye sobre pasajes recuperados que puedes mostrar. - Beneficio: actualización continua sin re-entrenar, trazabilidad de cada respuesta, y el mismo sistema sirve para nuevos tipos de documentos sin cambios de arquitectura. - Riesgos: la calidad depende críticamente de la recuperación (los embeddings de la sección 6). Si el retriever falla, el LLM responde con contexto equivocado. Hay que invertir en evaluación del pipeline completo, no solo del LLM. Se ve en detalle en el módulo 06-RAG.

Escenario 4 — Nicho ultraespecializado con presupuesto limitado

Contexto: una legaltech quiere un modelo que redacte borradores de un tipo de recurso administrativo muy específico, con estructura y fraseología rígidas. Tienen 3.000 ejemplos de calidad y presupuesto para una GPU, no para un clúster.

Decisión: Nivel 3 — LoRA (o QLoRA) sobre un LLM open source tipo Llama. - Por qué: es un problema de comportamiento y estilo, no de conocimiento consultable: el prompting se queda corto para clavar formato y fraseología en documentos largos, y RAG no aplica (no es búsqueda). El fine-tuning completo de un modelo generativo grande excede el presupuesto; LoRA sobre un modelo abierto de tamaño medio cabe en una GPU y captura el estilo con 3.000 ejemplos. - Beneficio: modelo propio, coste de inferencia controlado, adaptador de decenas de MB fácil de versionar (uno por tipo de documento si hace falta). - Riesgos: revisar la licencia del modelo base para uso comercial (sección 2.2); montar evaluación humana del output (en generación no hay accuracy que valga); y siempre con revisión de un abogado en el bucle: es un borrador asistido, no un firmante.

Resumen de los cuatro:

Escenario Señal decisiva Nivel elegido Coste dominante
Startup, 50 ejemplos Pocos datos, poco volumen, prisa 1 · Prompting API (céntimos/día)
Salud, 10k tickets, privacidad Datos etiquetados + datos no pueden salir 4 · Fine-tuning BERT propio Personal ML + infraestructura propia
Corporación, docs cambiantes Conocimiento vivo + citas obligatorias 2 · RAG Ingeniería del pipeline + API
Legaltech, estilo rígido, 1 GPU Comportamiento específico + presupuesto 3 · LoRA sobre open source 1 GPU + evaluación humana

9. Caso empresarial: la aseguradora de los 5.000 emails diarios

Caso empresarial

SegurPlus (nombre ficticio, patrón 100 % real) es una aseguradora mediana que recibe 5.000 emails diarios: partes de siniestro, consultas de pólizas, reclamaciones, solicitudes comerciales y spam. Cada email debe llegar al departamento correcto; cada hora de retraso en un parte de siniestro es un cliente furioso y un posible incumplimiento de SLA regulatorio.

Etapa 1 (hasta 2019): reglas y palabras clave. El sistema original: cientos de reglas tipo "si contiene 'siniestro' o 'accidente' → departamento de siniestros". Precisión ~70 %. Problemas crónicos: "quiero información por si tengo un accidente" iba a siniestros (falso positivo); "me chocaron el coche ayer" sin la palabra mágica iba al montón genérico. Mantener las reglas se convirtió en un trabajo a tiempo parcial de dos personas, y cada regla nueva rompía dos viejas.

Etapa 2 (2020–2023): BERT fine-tuneado. El equipo de datos tomó 60.000 emails históricos (etiquetados gratis por el propio historial de enrutamiento) y afinó un modelo tipo BERT en español —el proceso exacto de la sección 4 de este capítulo, con más datos—. Resultado: precisión ~93 %, latencia de decenas de milisegundos por email en un servidor propio con una GPU modesta, coste marginal por email prácticamente nulo. El coste real: un equipo que reentrenaba el modelo cada trimestre (nuevos productos → nuevas categorías → drift) y mantenía el pipeline. Las dos personas de las reglas pasaron a curar datos de entrenamiento: mejor trabajo, mejor resultado.

Etapa 3 (2024–2026): evaluación de migrar a un LLM vía API. Con la madurez de los LLMs, dirección pregunta: "¿tiramos el BERT y lo hacemos todo con un LLM?". El equipo hace números (órdenes de magnitud ilustrativos, no presupuesto real):

Criterio BERT afinado (statu quo) LLM vía API para todo
Precisión en clasificación pura ~93 % (entrenado en SU distribución) Similar, ~90–95 % con buen prompt, sin entrenar
Manejo de emails complejos/ambiguos Flojo: solo devuelve una etiqueta Excelente: extrae datos, resume, detecta urgencia y redacta respuesta
Coste mensual (~150.000 emails/mes) Cientos de €/mes (amortización servidor + energía) Miles de €/mes (orden de magnitud, según modelo y tamaño de email)
Latencia ~10–50 ms ~0,5–3 s (red + generación)
Privacidad Total: nada sale de casa Requiere contrato de tratamiento de datos con el proveedor, revisión legal
Mantenimiento Reentrenos trimestrales, equipo ML propio Sin entrenamientos, pero: versionado de prompts, cambios de modelo del proveedor, evaluación continua
Capacidades nuevas (extracción, resumen, respuesta) No, habría que construir cada una Incluidas en la misma llamada

Decisión final: arquitectura híbrida. El BERT afinado sigue clasificando el 100 % del tráfico (barato, rápido, probado). Los emails que el BERT marca con baja confianza (~8 % del total) más los de categorías de alto valor (reclamaciones complejas, ~4 %) pasan a un LLM que extrae los datos del parte, resume el caso y propone un borrador de respuesta para el gestor humano. Resultado: el 88 % del volumen se procesa por fracciones de céntimo, y el 12 % difícil recibe un tratamiento que antes era imposible. Coste total: una fracción de la opción "todo LLM", con mejor resultado que "todo BERT".

Consejo profesional

Esta arquitectura —modelo pequeño y barato para lo masivo y simple, LLM para lo complejo y de alto valor, con la confianza del modelo pequeño como enrutador— es uno de los patrones más rentables de la IA aplicada actual. Recuérdala: la volverás a ver (formalizada como cascading o routing) en los módulos de LLMs y de arquitectura de sistemas de IA. La pregunta profesional nunca es "¿cuál es el mejor modelo?", sino "¿cuál es el sistema con mejor coste-beneficio para cada segmento del problema?".


10. Buenas prácticas

  1. Sube la jerarquía de adaptación de abajo arriba. Prompt primero, siempre. Solo escala a RAG/LoRA/fine-tuning cuando midas que el nivel anterior no basta. Documenta esa medición: es tu justificación de arquitectura.
  2. Lee la model card entera antes de la primera línea de código. Licencia, idiomas, datos de entrenamiento, limitaciones. Cinco minutos que evitan desastres legales y técnicos.
  3. Fija versiones de modelos. Igual que congelas dependencias con pip, fija la revisión del modelo (revision= en from_pretrained, o un registro interno). "El modelo del Hub cambió" no puede ser causa de un incidente en producción.
  4. Guarda modelo y tokenizer siempre juntos. Son un matrimonio indisoluble: un modelo servido con otro tokenizer produce basura silenciosa (no da error: da predicciones absurdas).
  5. Learning rate pequeño (1e-5 a 5e-5) y pocas épocas al afinar. Los pesos preentrenados son el activo; el fine-tuning es cirugía fina, no demolición.
  6. Evalúa por clase (F1 macro, matriz de confusión), no solo accuracy global. El 95 % de accuracy puede esconder un 0 % en tu clase minoritaria más importante.
  7. Diseña el sistema con umbral de confianza y humano en el bucle. Un clasificador del 93 % con cola de revisión para el 7 % dudoso vale más que uno "del 96 %" sin red de seguridad.
  8. Monitoriza el drift. La distribución de textos de tu empresa cambia (productos nuevos, jerga nueva, temporada). Programa evaluaciones periódicas con datos frescos y presupuesta reentrenos.
  9. Prueba embeddings sobre TUS datos antes de elegir modelo. Los leaderboards orientan; tu dominio decide. El test cuesta una tarde; la elección afecta a todo lo que construyas encima.
  10. Registra los casos difíciles de producción. Los ejemplos donde el modelo duda o falla son el mejor dataset de mejora que existirá jamás: es tu data flywheel.

11. Malas prácticas

  1. "Vamos a entrenar nuestro propio LLM desde cero" sin ser un laboratorio con presupuesto de nueve cifras. Es el nivel 5 de la jerarquía por una razón.
  2. Hacer fine-tuning para inyectar conocimiento consultable (documentos, catálogos, FAQs). Para eso está RAG; el fine-tuning enseña comportamiento, no memoriza datos de forma fiable.
  3. Usar un modelo con licencia no comercial en un producto "porque de momento es una prueba". Las pruebas tienen la mala costumbre de llegar a producción intactas.
  4. Evaluar con 20 ejemplos y tomar decisiones de negocio. Con test sets diminutos, la diferencia entre 90 % y 95 % es ruido puro.
  5. Padding fijo a 512 tokens para frases de 15. Multiplicas el cómputo (y la factura) por procesar relleno.
  6. Ignorar los warnings de transformers. "Some weights were newly initialized" en inferencia significa que tu modelo tiene la cabeza en pesos aleatorios y estás sirviendo ruido con confianza.
  7. Promediar word embeddings para búsqueda semántica en producción existiendo sentence-transformers. Ya sabes por qué (sección 6.1).
  8. Elegir "el modelo más grande disponible" por defecto. Cada parámetro de más es latencia, coste y huella que pagas en cada una de los millones de inferencias. El tamaño correcto es el más pequeño que cumpla el objetivo de calidad.

12. Errores comunes

Error Síntoma Causa Solución
Tokenizer de un modelo con pesos de otro Predicciones absurdas sin ningún error ni warning Los ids de tokens no corresponden al vocabulario con que se entrenó Cargar SIEMPRE tokenizer y modelo del mismo identificador/directorio
Learning rate de "red normal" (1e-3) en fine-tuning La pérdida baja y luego explota, o el modelo rinde peor que congelado Pasos de gradiente enormes destruyen los pesos preentrenados lr entre 1e-5 y 5e-5, warmup, pocas épocas
Olvidar attention_mask al llamar al modelo a mano Métricas mediocres inexplicables con batches, correctas de una en una El modelo atiende a los tokens [PAD] como si fueran texto Pasar siempre el diccionario completo del tokenizer (modelo(**batch))
Texto largo truncado en silencio El modelo "ignora" información que está claramente en el documento truncation=True corta a max_length y solo ve el principio Trocear documentos largos y agregar, o modelo de contexto largo; comprobar longitudes tokenizadas
La clave del dataset no se llama labels Trainer entrena sin error pero la pérdida no baja, o KeyError Hugging Face busca la clave exacta labels para calcular la pérdida Nombrar la clave labels en el Dataset
Warning "newly initialized" ignorado en inferencia Accuracy a nivel de azar con un modelo "afinado" La cabeza de clasificación no se cargó del checkpoint Verificar identificador del modelo, clase AutoModelFor* y num_labels
Test set contaminado Métricas de laboratorio espectaculares, producción decepcionante Ejemplos duplicados o casi duplicados entre train y test Deduplicar antes del split; separar por cliente/fecha, no solo por fila
Comparar embeddings sin normalizar Similitudes coseno fuera de rango o rankings incoherentes Producto punto de vectores no normalizados ≠ coseno normalize_embeddings=True o normalizar antes de comparar

13. FAQ

1. ¿Sigue mereciendo la pena afinar un BERT en 2026, con LLMs tan capaces? Sí, rotundamente, para tareas cerradas de alto volumen (clasificación, NER, scoring). Un BERT afinado es cientos de veces más barato y rápido por predicción que un LLM, corre en tu infraestructura y, entrenado con tus datos, iguala o supera al LLM en SU tarea concreta. El caso de la aseguradora (sección 9) es la respuesta larga: la pregunta correcta no es "¿BERT o LLM?" sino "¿qué parte del problema merece cada herramienta?".

2. ¿Cuántos ejemplos necesito para hacer fine-tuning? Orientación práctica para clasificación con un modelo tipo BERT: con 100–200 por clase ya obtienes algo usable (lo has visto con menos); con 500–2.000 por clase, resultados sólidos; a partir de ahí, rendimientos decrecientes. Para fine-tuning generativo (estilo LoRA sobre un LLM), piensa en 500–5.000 ejemplos de alta calidad: en generación, la calidad de los ejemplos pesa más que la cantidad.

3. ¿Congelo el backbone o afino todo el modelo? Si tienes muy pocos datos (<100 ejemplos) o cero GPU, congela el backbone y entrena solo la cabeza: rápido y decente. Con datos y hardware normales, afinar todo con lr pequeño rinde mejor casi siempre. LoRA es el punto intermedio moderno: congela todo pero permite adaptación profunda con coste mínimo.

4. ¿Qué diferencia real hay entre RAG y fine-tuning? Me lo confunden constantemente. Regla mnemotécnica: RAG cambia lo que el modelo SABE en el momento de responder; el fine-tuning cambia cómo el modelo SE COMPORTA. ¿Necesita datos actualizados, privados o citables? RAG. ¿Necesita un formato, estilo, tono o habilidad específica que el prompting no consigue? Fine-tuning. ¿Ambas cosas? Se combinan sin problema (fine-tuning del comportamiento + RAG del conocimiento).

5. ¿Puedo usar cualquier modelo del Hub en mi empresa? No. La licencia manda: Apache 2.0/MIT, uso libre; licencias tipo "community" (p. ej. familia Llama), uso comercial con condiciones que hay que leer; CC-BY-NC y similares, prohibido uso comercial. Y además de la licencia del modelo, considera la de los datos con que se entrenó si tu sector es sensible. Ante la duda: legal primero, código después.

6. ¿Por qué mi modelo afinado en español funciona mal con textos de Latinoamérica? Distribución de entrenamiento. Si tu dataset de fine-tuning es 100 % español peninsular, el modelo sufrirá con léxico y giros de otras variantes ("computadora"/"ordenador", "manejar"/"conducir", voseo...). Soluciones: incluir datos de las variantes que atenderás, partir de un modelo preentrenado con español diverso o multilingüe, y —siempre— evaluar con datos de cada región antes de desplegar en ella.

7. ¿Qué es exactamente "in-context learning" y por qué se le llama "learning" si no se actualizan pesos? Es la capacidad de un LLM de ejecutar una tarea nueva a partir de ejemplos incluidos en el propio prompt, sin ningún entrenamiento. Se le llama "learning" porque el comportamiento observable es el de haber aprendido la tarea, aunque los pesos no cambian: la "adaptación" ocurre en las activaciones durante esa única inferencia y se esfuma al terminar. Es el nivel 1 de la jerarquía: la forma más barata de "aprender" jamás inventada, y por eso siempre se prueba primero.

8. ¿LoRA rinde peor que el fine-tuning completo? En la mayoría de tareas prácticas, la diferencia es pequeña o nula: el paper original ya mostraba paridad en muchos benchmarks. El fine-tuning completo conserva ventaja cuando la tarea exige cambios profundos y masivos de comportamiento (p. ej. adaptar a un dominio radicalmente distinto con muchísimos datos). Regla práctica: si el modelo es grande y el presupuesto finito, LoRA por defecto; si el modelo es pequeño (tipo BERT), el fine-tuning completo es tan barato que no hay motivo para no hacerlo.

9. ¿Cómo llevo el clasificador de tickets de este capítulo a producción de verdad? Lo esencial que faltaría: (1) más datos y test set serio (cientos de ejemplos, deduplicado); (2) servir el modelo tras una API (FastAPI + el pipeline guardado, o un servidor de inferencia dedicado); (3) umbral de confianza con cola humana; (4) logging de predicciones y monitorización de drift; (5) proceso de reentrenamiento periódico. Los módulos de MLOps y arquitectura de la academia cubren exactamente ese camino.

10. Con lo aprendido, ¿podría afinar mi mini-GPT del capítulo 6 para que siga instrucciones? Conceptualmente, ¡sí, y es un ejercicio buenísimo! Necesitarías un dataset de pares instrucción→respuesta y entrenarlo igual que siempre (predecir los tokens de la respuesta dada la instrucción). El resultado será humilde —tu mini-GPT no tiene la escala para capacidades serias—, pero verás con tus propios ojos el mecanismo del instruction tuning. La versión profesional de esto, sobre modelos abiertos de verdad, te espera en el módulo 16.

14. Resumen del capítulo y cierre del módulo

Lo esencial de este capítulo:

  • La IA moderna funciona con un patrón único: preentrenar una vez (carísimo) → adaptar mil veces (barato). En lenguaje funciona incluso mejor que en visión porque el preentrenamiento es autosupervisado (sin etiquetas), captura conocimiento del mundo y todas las tareas comparten la interfaz texto.
  • La jerarquía de adaptación —prompting < RAG < LoRA < fine-tuning completo < preentrenar— es tu marco de decisión de arquitecto: empieza abajo, sube solo con evidencia. Cada nivel tiene su escenario (sección 8) y su coste.
  • Hugging Face es la infraestructura de este paradigma: el Hub para encontrar modelos (leyendo model cards como profesional: licencia, idiomas, tamaño), pipeline() para probar en segundos, AutoTokenizer/AutoModel para abrir el capó y AutoModelForSequenceClassification para el patrón backbone congelable + cabeza intercambiable que ya conocías de visión.
  • Los tokenizers de subpalabras son el pegamento texto↔modelo: tokens especiales, attention_mask, padding dinámico y el peligro silencioso de la truncación.
  • Hiciste fine-tuning completo de BETO para clasificar tickets en español, dos veces: con el bucle manual del capítulo 3 (no hay magia) y con Trainer (hay productividad). Y evaluaste, guardaste y serviste el modelo: el ciclo profesional entero.
  • LoRA congela el modelo y aprende solo desvíos de bajo rango: ~0,1–0,5 % de parámetros entrenables, una GPU en vez de un clúster, adaptadores de MBs en vez de copias de GBs. Es la puerta al fine-tuning de LLMs de verdad (módulo 16).
  • sentence-transformers convierte frases en vectores hechos para la similitud semántica: cerraste el círculo abierto con bag-of-words en el módulo 01 y dejaste montado el motor de recuperación de tus futuros RAG (módulo 06).
  • Entre tu mini-GPT y un LLM moderno hay cuatro escalones: escala (scaling laws), instruction tuning (de completar a obedecer), RLHF/alineamiento (optimizar contra preferencias humanas) y las capacidades emergentes que aparecen por el camino.

Cierre del módulo 03 — mira lo que has construido:

Empezaste este módulo aprendiendo qué es un tensor y cómo fluye un gradiente (capítulo 1) y entrenando tus primeras redes con el bucle forward-loss-backward-step (capítulos 2–3): ese mismo bucle, sin cambiar una coma, es el que hoy usaste para afinar un Transformer de 110 millones de parámetros. Con las CNNs y el transfer learning en visión (capítulo 4) descubriste la idea más rentable del deep learning —reutilizar representaciones aprendidas—, que este capítulo elevó a paradigma universal. Las redes recurrentes (capítulo 5) te enseñaron por qué procesar secuencias es difícil, y la atención y los Transformers (capítulo 6) cómo se resolvió de verdad, hasta el punto de que construiste tu propio mini-GPT. Y hoy, capítulo 7, uniste todas las piezas: sabes qué hay dentro de un modelo de lenguaje, sabes adaptarlo a un problema real de empresa y sabes decidir cuándo adaptarlo y cuándo no.

Ya no eres una persona que "usa IA": eres alguien que entiende el motor. En el módulo 04-LLM empieza la segunda mitad del viaje: conducir ese motor profesionalmente —prompting sistemático, APIs, evaluación, y de ahí a RAG y agentes—. Nos vemos allí.

15. Bibliografía y recursos

  • Hugging Face Learn — NLP Course: huggingface.co/learn — el curso oficial y gratuito de transformers, datasets y tokenizers. La continuación natural de este capítulo, con ejercicios ejecutables.
  • Documentación de transformers: huggingface.co/docs/transformers — referencia de pipeline, clases Auto*, Trainer y TrainingArguments.
  • Hu, E. J., et al. (2021). "LoRA: Low-Rank Adaptation of Large Language Models". arxiv.org/abs/2106.09685 — el paper original de LoRA; la sección de análisis del rango intrínseco es sorprendentemente legible.
  • Dettmers, T., et al. (2023). "QLoRA: Efficient Finetuning of Quantized LLMs". arxiv.org/abs/2305.14314 — cuantización a 4 bits + LoRA: fine-tuning de modelos grandes en GPUs de consumo.
  • Ouyang, L., et al. (2022). "Training language models to follow instructions with human feedback" (InstructGPT). arxiv.org/abs/2203.02155 — el paper que definió la receta instruction tuning + RLHF.
  • Reimers, N. y Gurevych, I. (2019). "Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks". arxiv.org/abs/1908.10084 — por qué los embeddings de BERT crudos no sirven para similitud y cómo arreglarlo.
  • Documentación de sentence-transformers: sbert.net — guías de búsqueda semántica, modelos multilingües y entrenamiento de embeddings propios.
  • Documentación de PEFT: huggingface.co/docs/peft — LoRA y familia, con recetas por tipo de tarea.
  • Cañete, J., et al. (2020). "Spanish Pre-Trained BERT Model and Evaluation Data" (BETO). huggingface.co/dccuchile/bert-base-spanish-wwm-cased — la model card del modelo que afinaste en este capítulo: léela entera como ejercicio.
  • Kaplan, J., et al. (2020). "Scaling Laws for Neural Language Models". arxiv.org/abs/2001.08361 — el paper que mostró que la pérdida escala de forma predecible con parámetros, datos y cómputo.

Anterior Módulo Siguiente
Capítulo 6: Transformers 03-DEEP-LEARNING Ejercicios del módulo