AulaML Python · C++
Python avanzado · 50 min · Python para Machine Learning

NLP, embeddings y Transformers

Cómo un texto se convierte en vectores, cómo la atención los mezcla y cómo se ajusta un modelo preentrenado

Un modelo de lenguaje no lee palabras: recibe números. Aprenderás a tokenizar texto, a representarlo con embeddings, a medir similitud con coseno, a entender qué hace la atención dentro de un Transformer y a ajustar un modelo preentrenado con Hugging Face.

Lección 7 de 7 del módulo
Al terminar esta lección podrás
  • Explicar el recorrido de un texto hasta los ids y los vectores que consume el modelo
  • Tokenizar con `AutoTokenizer`, gestionar truncado, padding y máscara de atención
  • Calcular e interpretar similitud coseno entre embeddings
  • Describir el mecanismo de atención con queries, keys y values, y leer una matriz de atención
  • Distinguir preentrenamiento de fine-tuning y ejecutar un ajuste con `Trainer`

Antes necesitas: Redes neuronales con PyTorch

En esta lección
  1. ¿Qué es el procesamiento de lenguaje natural?
  2. ¿Para qué sirve?
  3. ¿Cómo funciona por dentro?
  4. Sintaxis: instalar y cargar un tokenizador
  5. Ejemplo básico: de texto a ids
  6. Ejemplo intermedio: similitud coseno
  7. Caso práctico: clasificar y ajustar un Transformer
  8. Visualización: una matriz de atención
  9. Visualización: cómo se tokeniza una frase
  10. Errores comunes
  11. Preentrenamiento y ajuste
  12. Buenas prácticas
  13. Ejercicios
  14. Soluciones explicadas
  15. Resumen
  16. Evaluación

Un modelo no lee: recibe números de tamaño fijo, y el lenguaje no es ni numérico ni de tamaño fijo. Una frase tiene ocho palabras y otra cuarenta, y «banco» significa cosas distintas según la oración.

La solución actual se apoya en dos ideas: partir el texto en unidades con vectores densos (embeddings) y dejar que cada unidad mire a las demás para decidir qué significa en contexto (atención). Esa combinación es el Transformer.

¿Qué es el procesamiento de lenguaje natural?

El NLP (Natural Language Processing) es la rama que estudia cómo hacer tareas útiles con texto: clasificar, extraer, traducir, resumir, responder preguntas. La dificultad no es solo la ambigüedad; también lo es la variabilidad: dos frases con el mismo sentido pueden compartir muy pocas palabras, y una sola palabra puede invertir el significado.

¿Para qué sirve?

  • Clasificación: detectar spam, medir el tono de un comentario o etiquetar tickets de soporte.
  • Búsqueda semántica: encontrar documentos por significado y no por coincidencia exacta de palabras.
  • Extracción: sacar nombres, fechas e importes de contratos y facturas.
  • Generación: resumir, redactar y responder preguntas con modelos tipo GPT.
  • Traducción y transcripción: convertir voz en texto y texto en otro idioma.
  • Deduplicación y agrupamiento: detectar noticias repetidas o agrupar reseñas por tema.

¿Cómo funciona por dentro?

El recorrido tiene cuatro etapas. El tokenizador parte el texto en subpalabras y devuelve enteros de un vocabulario fijo. La capa de embeddings convierte cada entero en un vector de 768 dimensiones. Las capas del Transformer mezclan esos vectores con atención, y la cabeza final produce la salida de la tarea.

La tokenización no trabaja con palabras completas sino con subpalabras (WordPiece en BERT, BPE en GPT). Así el vocabulario se mantiene en decenas de miles de entradas y las palabras raras se descomponen: tokenization puede partirse en token + ##ization. Cada secuencia se envuelve con tokens especiales: [CLS] al inicio y [SEP] al final en los modelos BERT.

Un embedding es un vector aprendido. Dos palabras con usos parecidos acaban con vectores parecidos, y la similitud se mide con el coseno del ángulo entre ellos, no con la distancia euclídea. En un modelo contextual como BERT el vector no es fijo por palabra: se calcula en cada frase, después de que la atención mezcle el contexto.

La atención proyecta cada token en query (qué busco), key (qué ofrezco) y value (qué aporto). El producto entre una query y todas las keys mide compatibilidad; se divide por la raíz de la dimensión, se aplica softmax y el resultado pesa los values.

Un Transformer apila esas capas con conexiones residuales, normalización y una red densa por token. Los modelos encoder como BERT ven la frase completa en ambas direcciones y sirven para clasificar o extraer. Los decoder como GPT solo miran hacia atrás, porque su tarea es predecir el siguiente token.

Sintaxis: instalar y cargar un tokenizador

Necesitas transformers y, para tensores y modelos, torch. La primera ejecución descarga el vocabulario y los pesos desde Hugging Face, así que requiere conexión; después quedan en la caché local.

SHELL Instalación
pip install transformers torch
pip install datasets sentence-transformers   # opcionales, para las secciones siguientes
PYTHON tokenizar.py
from transformers import AutoTokenizer

tok = AutoTokenizer.from_pretrained("distilbert-base-uncased")
lote = tok("Machine learning is fun", return_tensors="pt")

print(lote["input_ids"])                          # tensor([[101, 3698, 4083, 2003, 4569, 102]])
print(tok.convert_ids_to_tokens(lote["input_ids"][0]))
print(lote["attention_mask"].shape)

AutoTokenizer elige la clase correcta a partir del nombre del modelo. La salida es un diccionario con input_ids (los enteros), attention_mask (1 donde hay token real, 0 donde hay relleno) y, si el modelo lo usa, token_type_ids. La forma es (1, 6): un lote de una secuencia de seis tokens.

Ejemplo básico: de texto a ids

Al procesar muchos textos, las secuencias tienen longitudes distintas y un tensor no admite filas de tamaños diferentes. Se recortan con truncation=True y se rellenan con padding=True hasta la longitud máxima del lote.

PYTHON lote.py
from transformers import AutoTokenizer

tok = AutoTokenizer.from_pretrained("distilbert-base-uncased")
frases = ["the cat sat", "machine learning is fun and useful"]

lote = tok(frases, padding=True, truncation=True, max_length=512, return_tensors="pt")
print(lote["input_ids"].shape)       # torch.Size([2, 8])
print(lote["attention_mask"])
print(tok.decode(lote["input_ids"][0], skip_special_tokens=True))

La primera frase tiene cinco tokens y la segunda ocho, así que la primera se rellena con tres ceros y su máscara termina en 0, 0, 0. Sin máscara, el modelo trataría esos ceros como contenido. decode hace el camino inverso y devuelve el texto, útil para depurar tokenizaciones raras.

Ejemplo intermedio: similitud coseno

La similitud coseno compara direcciones e ignora la longitud: cos(a, b) = (a·b) / (‖a‖·‖b‖). Vale 1 para vectores paralelos, 0 para perpendiculares y −1 para opuestos. Los vectores siguientes son de cuatro dimensiones para que puedas comprobar las cuentas; un embedding real tiene 384 o 768.

PYTHON coseno.py
import math

def coseno(a, b):
    producto = sum(x * y for x, y in zip(a, b))
    norma_a = math.sqrt(sum(x * x for x in a))
    norma_b = math.sqrt(sum(y * y for y in b))
    return producto / (norma_a * norma_b)

rey = [0.8, 0.6, 0.0, 0.1]
reina = [0.75, 0.55, 0.2, 0.12]
manzana = [-0.2, 0.1, 0.9, -0.3]

print(round(coseno(rey, reina), 3))     # 0.978
print(round(coseno(rey, manzana), 3))   # -0.133

«Rey» y «reina» comparten dirección (0.978); «rey» y «manzana» apuntan a lados opuestos (−0.133). El coseno ya divide por las normas, así que no hace falta normalizar; el producto escalar y la distancia euclídea sí crecen con la magnitud, y por eso conviene normalizar si usas esos dos. Con sentence-transformers, normalize_embeddings=True deja los vectores listos para comparar.

PYTHON frases.py
from sentence_transformers import SentenceTransformer, util

modelo = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
frases = ["Un gato duerme en el sofá", "El perro descansa en el sillón"]
vectores = modelo.encode(frases, normalize_embeddings=True)

print(vectores.shape)                              # (2, 384)
print(util.cos_sim(vectores[0], vectores[1]).item())  # cercano a 1: mismo tema

Caso práctico: clasificar y ajustar un Transformer

Primero, la vía rápida: un pipeline descarga un modelo ya ajustado y hace todo el recorrido por ti. Es la forma más eficiente de comprobar si un modelo sirve antes de invertir en entrenamiento.

PYTHON pipeline.py
from transformers import pipeline

clasificador = pipeline("sentiment-analysis")   # distilbert afinado en SST-2
print(clasificador("I loved every chapter of this course"))
print(clasificador("The ending was a mess"))

La salida es una lista con label y score. Ese modelo está entrenado en inglés: con texto en español las etiquetas no son fiables. Para español existen alternativas como pysentimiento/robertuito-sentiment-analysis; cambiar el modelo es cambiar un argumento, porque la interfaz es la misma.

num_labels=2 sustituye la cabeza de preentrenamiento por una de dos clases; el resto de los pesos se conserva. DataCollatorWithPadding rellena cada lote a la longitud de su secuencia más larga, y load_best_model_at_end devuelve los pesos de la mejor época.

PYTHON fine_tuning.py
import numpy as np
from datasets import load_dataset
from sklearn.metrics import accuracy_score, f1_score
from transformers import (AutoModelForSequenceClassification, AutoTokenizer,
                          DataCollatorWithPadding, Trainer, TrainingArguments)

nombre = "distilbert-base-uncased"
tok = AutoTokenizer.from_pretrained(nombre)
datos = load_dataset("imdb", split={"train": "train[:2000]", "test": "test[:500]"})
datos = datos.map(lambda l: tok(l["text"], truncation=True, max_length=256), batched=True)
datos = datos.rename_column("label", "labels")

modelo = AutoModelForSequenceClassification.from_pretrained(nombre, num_labels=2)

def metricas(eval_pred):
    logits, etiquetas = eval_pred
    pred = np.argmax(logits, axis=-1)
    return {"accuracy": accuracy_score(etiquetas, pred),
            "f1": f1_score(etiquetas, pred, average="binary")}

args = TrainingArguments(
    output_dir="./resultados",
    learning_rate=2e-5,
    per_device_train_batch_size=16,
    num_train_epochs=2,
    eval_strategy="epoch",
    save_strategy="epoch",
    load_best_model_at_end=True,
    report_to="none",
)

entrenador = Trainer(
    model=modelo,
    args=args,
    train_dataset=datos["train"],
    eval_dataset=datos["test"],
    data_collator=DataCollatorWithPadding(tok),
    compute_metrics=metricas,
    processing_class=tok,          # transformers >= 4.46
)
entrenador.train()
print(entrenador.evaluate())

Visualización: una matriz de atención

Esta matriz es una cabeza de atención de juguete sobre seis tokens, con valores redondeados. Cada fila es un token que consulta y cada columna, un token consultado; la diagonal es la atención hacia sí mismo. En un modelo real las cifras salen de softmax(Q·Kᵀ/√d_k).

Mapa de datos ·Atención de una cabeza (6 tokens × 6 tokens)
6 × 6 rango [0.05, 0.55] Fila cruzó: dedica 0.45 a gato (quién cruza), 0.25 a no (la negación) y 0.10 a sí mismo. La suma de la fila es 1.
El
gato
no
cruzó
la
calle
El
0.1
0.55
0.05
0.1
0.1
0.1
gato
0.3
0.25
0.05
0.15
0.1
0.15
no
0.05
0.2
0.3
0.25
0.1
0.1
cruzó
0.05
0.45
0.25
0.1
0.05
0.1
la
0.05
0.1
0.05
0.2
0.15
0.45
calle
0.05
0.1
0.05
0.4
0.1
0.3

Recorre los pasos para ver cómo cambian las celdas.

Comprueba una fila con una calculadora: 0.05 + 0.45 + 0.25 + 0.10 + 0.05 + 0.10 = 1.00. Esa es la firma del softmax. En un decoder como GPT la mitad superior se enmascara con −∞ antes del softmax, de modo que un token no puede mirar al futuro; en un encoder como BERT no hay máscara y la matriz es completa.

Visualización: cómo se tokeniza una frase

El programa siguiente ejecuta el primer paso del recorrido y muestra los tres objetos que acompañan a cualquier texto: la forma del tensor, la máscara y la lista de tokens.

Ejecución visual ·De la frase al lote
1 / 6
1 from transformers import AutoTokenizer
2
3 tok = AutoTokenizer.from_pretrained("distilbert-base-uncased")
4 lote = tok("the cat sat", return_tensors="pt")
5 print(lote["input_ids"].shape)
6 print(lote["attention_mask"])
7 print(tok.convert_ids_to_tokens(lote["input_ids"][0]))
8 print(tok.model_max_length)
Se descarga el vocabulario de DistilBERT (30 522 entradas) y se construye el tokenizador WordPiece en minúsculas.
Variables
tok = DistilBertTokenizerFast (vocab 30522)
Salida
—

Avanza paso a paso: observa la línea resaltada, las variables y la salida.

Los ids dependen del vocabulario: 1996 es the, 4937 es cat y 2938 es sat. En otro modelo cambian por completo, y por eso nunca se reutiliza un tokenizador con los pesos de otro modelo. Si vieras [UNK], la palabra no estaba en el vocabulario y el modelo pierde esa información.

Errores comunes

Mensaje o síntomaCausaSolución
Token indices sequence length is longer than...La secuencia supera los 512 tokensActiva truncation=True con max_length
RuntimeError: The size of tensor a (7) must match...Filas de longitudes distintas sin paddingUsa padding=True o un collator
El modelo predice basura en españolSe usó un modelo entrenado en inglésElige un modelo en español o ajusta el tuyo
OSError: ... is not a local folderNombre de modelo mal escrito o sin conexiónRevisa el id exacto en huggingface.co
Las probabilidades no suman 1Se aplicó softmax dos veces o a la columna equivocadaAplica softmax sobre la última dimensión, una vez

Preentrenamiento y ajuste

AspectoPreentrenamientoFine-tuning
ObjetivoPredecir tokens ocultos o el siguiente tokenOptimizar la tarea concreta
DatosMiles de millones de tokens sin etiquetarCientos o miles de ejemplos etiquetados
CosteDías de muchas GPUMinutos u horas en una sola GPU
Qué cambiaTodos los pesos desde ceroTodos los pesos, con learning rate bajo

Buenas prácticas

  • Usa siempre el tokenizador que corresponde al modelo; no los mezcles.
  • Empieza con un pipeline o un modelo preentrenado antes de entrenar desde cero.
  • Con pocos datos, congela el encoder y entrena solo la cabeza (requires_grad=False).
  • Evalúa con F1 si las clases están desbalanceadas; la accuracy sola engaña.
  • Fija seed y guarda la versión del modelo base: los resultados cambian entre versiones.

Ejercicios

Ejercicio 1

Tokenizar y medir el desperdicio

Tokeniza estas tres frases con distilbert-base-uncased: «the cat sat», «machine learning is fun and useful» y «unbelievably». Imprime el número de ids de cada una (sin relleno) y explica por qué la tercera no produce un solo token.
Pista

Usa tok(frases)["input_ids"] sin padding, y cuenta con len. Recuerda que cada secuencia lleva [CLS] y [SEP].

Completar código Prepara un lote heterogéneo
lote = tok(frases, padding=, truncation=, max_length=512, return_tensors="pt")
print(lote["input_ids"].shape)

Ambos parámetros aceptan un booleano; son los que igualan longitudes y evitan el error de tamaño.

Ejercicio 2

Similitud entre tres frases

Con sentence-transformers/all-MiniLM-L6-v2, codifica tres frases: dos sobre el mismo tema y una sobre otro. Normaliza los vectores y calcula la matriz de similitud coseno 3×3. Identifica el par más parecido y explica por qué la diagonal vale 1.
Pista

modelo.encode(frases, normalize_embeddings=True) y luego util.cos_sim(v, v). La matriz será simétrica.

Soluciones explicadas

En el primer ejercicio, contar tokens y no palabras revela el coste real: la frase de seis palabras ocupa ocho ids. Ese dato decide si tu max_length pierde información; si el percentil 95 del corpus está en 300, truncar a 128 elimina la mitad del contenido.

En el segundo, normalizar antes de comparar evita que los vectores largos dominen. La similitud alta entre las dos primeras frases no depende de compartir palabras exactas —«duerme» frente a «descansa»—, sino de que el modelo las sitúa en direcciones parecidas.

Resumen

  • El texto se convierte en ids mediante un tokenizador de subpalabras y un vocabulario fijo.
  • [CLS] y [SEP] delimitan la secuencia; attention_mask distingue tokens reales de relleno.
  • truncation y padding igualan longitudes; el límite de BERT es 512 tokens.
  • Un embedding es un vector denso; la similitud se mide con coseno, que ignora la magnitud.
  • La atención calcula pesos con softmax(Q·Kᵀ/√d_k) y cada fila suma 1.
  • El fine-tuning parte de pesos preentrenados; el encoder (BERT) ve toda la frase y el decoder (GPT) solo el pasado.

Evaluación

Evaluación 1

¿Por qué un tokenizador parte «transformers» en varios tokens?

Evaluación 2

En una matriz de atención, ¿qué significa que una fila sume 1?

Evaluación 3

¿Cuál es la diferencia central entre preentrenamiento y fine-tuning?

Tus notas

¿Ya lo entendiste?

Márcala como completada para llevar tu progreso y desbloquear el siguiente paso de la ruta.