Módulo / 8. NLP
0%
Módulo 8 · Avanzado

Procesamiento de lenguaje natural

El lenguaje es la interfaz más natural del conocimiento humano. Recorreremos el pipeline clásico de NLP hasta los modelos de lenguaje, entendiendo qué representa cada etapa y dónde están sus límites.

8 lecciones 18 h estimadas De TF-IDF a LLMs
0 de 8 lecciones completadas
Lección 8.1

El pipeline de NLP y la tokenización

18 minPipelineTexto → números

Un modelo no ve palabras: ve números. Todo sistema de NLP es una cadena de decisiones que transforma texto crudo en tensores. La calidad de esa cadena determina el techo del modelo mucho más de lo que suele admitirse.

Al terminar sabrás

Describir las etapas de un pipeline de NLP y qué decide cada una.
Comparar tokenización por palabras, caracteres y subpalabras.
Explicar por qué BPE y WordPiece dominan los modelos modernos.
Detectar errores de normalización que destruyen información útil.

El pipeline completo

Texto crudoHTML, ruido Normalizarunicode, case Tokenizarpalabras/subwords VectorizarIDs o embeddings Modeloclasificar/generar Cada etapa puede perder información; documenta y justifica cada decisión.
Las decisiones de normalización y tokenización forman parte del modelo: si cambian, el modelo deja de ser comparable.

Tokenización: la decisión fundamental

UnidadVentajaProblema
PalabraInterpretable y compactaVocabulario enorme, palabras fuera de vocabulario (OOV)
CarácterVocabulario pequeño, sin OOVSecuencias largas y costo alto
Subpalabra (BPE, WordPiece)Equilibrio: cubre morfología y limita vocabularioTokens menos legibles para humanos

La tokenización por subpalabras resuelve el problema de las palabras raras: «aprendizaje» puede dividirse en aprend + izaje. Así el modelo comparte estadística entre variantes morfológicas y puede representar palabras que nunca vio, porque están compuestas por piezas conocidas.

1

Por qué el vocabulario explota

Un corpus técnico en español puede contener 500 000 formas distintas si se incluyen flexiones, errores y nombres propios. Un vocabulario de palabras de ese tamaño hace que la matriz de embeddings sea enorme y que la mayoría de términos aparezca una sola vez. Con 32 000 subpalabras, cualquier texto se representa con piezas reutilizadas.

Consecuencia práctica: reutilizar el tokenizador de un modelo preentrenado no es un detalle: el modelo aprendió embeddings para esos IDs concretos. Cambiar el tokenizador invalida los pesos.

Código: tokenizar de tres formas

Pythontokenizar.py
import re
from collections import Counter

texto = "El aprendizaje automático no es magia; es estadística bien aplicada."

# 1) Tokenización por palabras con regex (mejor que split)
palabras = re.findall(r"\b\w+\b", texto.lower())
print(palabras)

# 2) Por caracteres
print(list(texto[:30]))

# 3) Subpalabras con un tokenizador real (requiere transformers)
from transformers import AutoTokenizer

tok = AutoTokenizer.from_pretrained("dccuchile/bert-base-spanish-wwm-cased")
ids = tok(texto)["input_ids"]
print(ids)
print(tok.convert_ids_to_tokens(ids))

# Ver el vocabulario: cuántas piezas empiezan con "##" (continuación)
vocab = tok.get_vocab()
print("tamaño del vocabulario:", len(vocab))
print("piezas de continuación:", sum(1 for k in vocab if k.startswith("##")))
Normalizar de más también destruye señal Pasar todo a minúsculas y quitar tildes mejora la cobertura en modelos clásicos, pero en análisis de sentimiento o detección de sarcasmo puede eliminar información. En modelos preentrenados, usa exactamente la normalización con la que fueron entrenados. Nunca normalices a mano antes de pasar por el tokenizador oficial.

Limpieza: qué quitar y qué conservar

  • HTML y entidades: eliminar etiquetas, conservar el texto.
  • URLs y correos: sustituir por un token especial o eliminarlos según la tarea.
  • Emojis: pueden ser señal fuerte de sentimiento; no los borres por defecto.
  • Stopwords: rara vez conviene eliminarlas en modelos modernos; sí puede ayudar en TF-IDF.
  • Duplicados: pueden inflar la evaluación si aparecen en train y test.
Regla de oro La limpieza debe decidirse con el problema, no por costumbre. Si dudas, mide: entrena dos versiones del pipeline y compara con validación. Cada transformación que no aporta es una fuente de error y de deuda técnica.
Ejercicio 8.1 · Diseñar el pipeline

Vas a clasificar reseñas de productos en positivas y negativas. Define el pipeline: qué limpiarías, qué tokenizador usarías, qué harías con emojis, negaciones y errores ortográficos. Justifica cada decisión con un experimento que podrías ejecutar.

Orientación: conservar emojis y negaciones (aportan sentimiento), corregir errores solo si tienes una herramienta fiable, usar un tokenizador preentrenado en español y comparar contra una versión con limpieza agresiva. La decisión debe basarse en F1 de validación, no en intuición.

Lección 8.2

Bolsa de palabras y TF-IDF

20 minRepresentación dispersaBaseline fuerte

Antes de los embeddings y los Transformers, el texto se representaba contando palabras. Estas representaciones ignoran el orden, pero siguen siendo baselines difíciles de superar en muchos problemas con pocos datos. Entenderlas es entender qué información captura y qué pierde un modelo de lenguaje.

Bolsa de palabras

Se construye un vocabulario con los términos del corpus y cada documento se representa como un vector de conteos. El orden y la gramática desaparecen: «el perro muerde al hombre» y «el hombre muerde al perro» producen el mismo vector.

Vector de conteos $$\mathbf{x}_d=(c_{d1},c_{d2},\dots,c_{dV}), \qquad c_{dj}=\text{número de veces que el término } j \text{ aparece en el documento } d$$ $V$ es el tamaño del vocabulario. La matriz documento–término es dispersa: casi todas las entradas son cero.

TF-IDF

Un término frecuente en un documento pero raro en el corpus es discriminativo. TF-IDF combina ambas señales:

Frecuencia de término e inversa de frecuencia de documento $$\text{tf}(t,d)=\frac{f_{t,d}}{\sum_{t'}f_{t',d}}, \qquad \text{idf}(t)=\log\frac{N}{1+\text{df}(t)}+1, \qquad w_{t,d}=\text{tf}(t,d)\cdot\text{idf}(t)$$ $N$: número de documentos; $\text{df}(t)$: documentos que contienen $t$. Scikit-learn usa $1+\text{df}$ y suavizado para evitar divisiones por cero y términos con idf 0.
1

TF-IDF con tres documentos

Documentos: D1 = «gato duerme», D2 = «perro duerme», D3 = «gato come». $N=3$.

«duerme» aparece en 2 documentos: $\text{idf}=1+\log(3/3)=1$. «gato» aparece en 2: mismo idf. «come» aparece en 1: $\text{idf}=1+\log(3/2)\approx1.405$.

En D3, «come» recibe más peso que «gato» porque es más raro en el corpus. En D1, «gato» y «duerme» pesan igual, pero «gato» aparece en 2 de 3 documentos y aun así no se penaliza mucho: con corpus pequeños, el IDF es poco estable. Usa corpus representativos.

Normalización de vectores Scikit-learn normaliza cada vector a norma 1 por defecto. Así la similitud coseno entre documentos no depende de la longitud del texto. Si comparas documentos de tamaños muy distintos, mantén esa normalización.

Implementación y baseline

Pythontfidf_baseline.py
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report

# X_texto: lista de documentos; y: etiquetas
X_train, X_test, y_train, y_test = train_test_split(
    X_texto, y, test_size=0.2, stratify=y, random_state=42
)

modelo = make_pipeline(
    TfidfVectorizer(
        ngram_range=(1, 2),      # unigramas y bigramas
        min_df=3,                # ignora términos muy raros
        max_df=0.9,              # ignora términos casi universales
        sublinear_tf=True,       # aplica 1 + log(tf)
        strip_accents="unicode",
        lowercase=True
    ),
    LogisticRegression(max_iter=2000, class_weight="balanced")
)

modelo.fit(X_train, y_train)
y_pred = modelo.predict(X_test)
print(classification_report(y_test, y_pred))

# Términos más importantes para la clase positiva
vectorizador = modelo.named_steps["tfidfvectorizer"]
clf = modelo.named_steps["logisticregression"]
import numpy as np
vocab = np.array(vectorizador.get_feature_names_out())
pesos = clf.coef_[0]
for i in np.argsort(pesos)[-12:][::-1]:
    print(f"{vocab[i]:>20s}  {pesos[i]:+.3f}")

Qué captura y qué pierde

Captura

Señal léxica

Presencia de términos discriminativos, n-gramas simples, estilo y vocabulario del dominio. Con suficientes datos es una baseline sólida y rapidísima.

Pierde

Orden y composición

Negaciones, ironía, dependencias largas y significado composicional. Los bigramas mitigan parte del problema, pero no lo resuelven.

Ventaja

Interpretabilidad

Puedes inspeccionar los pesos por término. Útil para depurar y para explicar decisiones a personas no técnicas.

Riesgo

Sesgo por vocabulario

El modelo puede aprender atajos del corpus: nombres propios, marcadores de fuente o palabras correlacionadas con la etiqueta sin relación causal.

Ajusta el vectorizador solo con train El vocabulario y el IDF se aprenden de los datos. Si los calculas con todo el corpus antes de dividir, el test filtra información y la métrica se vuelve optimista. Dentro de un Pipeline, esta separación ocurre automáticamente en cada fold de validación.
Ejercicio 8.2 · TF-IDF vs. conteos

Compara un clasificador con CountVectorizer y otro con TfidfVectorizer sobre el mismo dataset y partición. Prueba con y sin bigramas, y con min_df 1, 3 y 10. Reporta F1 macro y explica las diferencias.

Orientación: TF-IDF suele ayudar cuando hay documentos de longitudes muy distintas o términos muy frecuentes. Los bigramas capturan negaciones locales («no bueno») pero multiplican la dimensionalidad. min_df alto reduce ruido y sobreajuste, pero puede eliminar señal si el corpus es pequeño.

Lección 8.3

Embeddings y Word2Vec

22 minEspacios vectorialesSimilitud

Un embedding representa cada palabra con un vector denso de decenas o cientos de dimensiones, aprendido de modo que palabras con contextos similares queden cerca. Es la transición entre contar palabras y modelar significado.

La hipótesis distribucional

«Conocerás una palabra por las compañías que mantiene». Word2Vec aprende vectores prediciendo el contexto: si dos palabras aparecen en contextos parecidos, sus vectores se acercan. No hay definiciones ni diccionarios; solo regularidades de uso.

Skip-gram: predecir el contexto $$P(w_O \mid w_I)=\frac{\exp(\mathbf{v}_{w_O}^\top \mathbf{u}_{w_I})}{\sum_{w=1}^{V}\exp(\mathbf{v}_{w}^\top \mathbf{u}_{w_I})}, \qquad \mathcal{L}=-\sum_{(w_I,w_O)\in \mathcal{D}}\log P(w_O\mid w_I)$$ $\mathbf{u}$ es el vector de la palabra central y $\mathbf{v}$ el de la palabra de contexto. El denominador es caro; se aproxima con negative sampling.

Negative sampling en una frase

En lugar de normalizar sobre todo el vocabulario, se entrena un clasificador binario: pares reales (palabra, contexto) deben puntuar alto y pares falsos (palabra, contexto aleatorio) deben puntuar bajo. Con 5–20 negativos por positivo se obtienen embeddings útiles con una fracción del costo.

1

Similitud coseno e interpretación geométrica

Si $\mathbf{v}_{\text{rey}}=(0.8,0.2,0.1)$ y $\mathbf{v}_{\text{reina}}=(0.75,0.25,0.05)$, su similitud coseno es alta. La dirección captura el «significado» y la magnitud puede reflejar frecuencia de uso.

La analogía clásica $\mathbf{v}_{\text{rey}}-\mathbf{v}_{\text{hombre}}+\mathbf{v}_{\text{mujer}}\approx\mathbf{v}_{\text{reina}}$ funciona en algunos espacios, pero no es una propiedad universal. Depende del corpus, del tamaño y del entrenamiento. No la presentes como evidencia de que el modelo «entiende» género.

Pythonword2vec.py
from gensim.models import Word2Vec
from gensim.utils import simple_preprocess

# corpus: lista de documentos (strings)
frases = [simple_preprocess(doc, deacc=False, min_len=2) for doc in corpus]

modelo = Word2Vec(
    sentences=frases,
    vector_size=100,
    window=5,
    min_count=5,
    workers=4,
    sg=1,               # skip-gram; sg=0 usa CBOW
    negative=10,
    epochs=10,
    seed=42
)

print(modelo.wv.most_similar("aprendizaje", topn=8))
print(modelo.wv.similarity("modelo", "algoritmo"))

# Guardar solo los vectores (más ligero que el modelo completo)
modelo.wv.save("vectores.kv")

# Cargar con KeyedVectors
from gensim.models import KeyedVectors
kv = KeyedVectors.load("vectores.kv")
print(kv["datos"].shape)

Limitaciones que conviene nombrar

  • Un vector por palabra: «banco» tiene el mismo vector en «banco financiero» y «banco del parque». Los embeddings contextuales resuelven esto.
  • Sesgos del corpus: si el corpus asocia ciertas profesiones con un género, el espacio reproduce y amplifica esa asociación.
  • Vocabulario fijo: las palabras nuevas quedan fuera. Los modelos basados en subpalabras mitigan el problema.
  • No son interpretables dimensión a dimensión: cada eje no tiene un significado limpio; solo las relaciones geométricas son útiles.
Embeddings preentrenados Entrenar Word2Vec desde cero requiere un corpus grande. En la práctica se usan vectores preentrenados (FastText, GloVe, word2vec de Google News) o, mejor aún, representaciones contextuales de un Transformer. Para clasificación con pocos datos, un modelo preentrenado suele ganar.

De palabra a documento

Para clasificar un documento con embeddings de palabras se suele promediar los vectores (ponderados por TF-IDF) o usar una RNN/Transformer. El promedio es simple y sorprendentemente competitivo en tareas de tema, pero pierde orden y composición. En tareas de sentimiento con negaciones, esa pérdida importa.

Ejercicio 8.3 · Evaluar un espacio de embeddings

Entrena Word2Vec con ventanas 2, 5 y 10 sobre el mismo corpus. Inspecciona los vecinos de cinco palabras y calcula la similitud entre pares que tú consideres relacionados. ¿Qué ventana produce vecinos más coherentes? ¿Cómo lo medirías sin depender solo de tu intuición?

Orientación: ventanas pequeñas capturan relaciones sintácticas y funcionales; ventanas grandes capturan tema. Para evaluar, usa un conjunto de pares con juicio humano (word similarity) o evalúa los embeddings como features en una tarea downstream con validación. Reporta ambos.

Lección 8.4

Redes recurrentes para texto

18 minSecuenciasLSTM/GRU

Una red recurrente procesa el texto palabra por palabra, actualizando un estado que resume lo leído. Fue la arquitectura dominante en NLP hasta 2017 y todavía es útil para secuencias cortas y modelos ligeros.

Por qué recurrencia

Una red densa sobre una bolsa de palabras no distingue «no me gustó» de «me gustó, no». La recurrencia introduce orden: el estado en el paso $t$ depende del estado en $t-1$ y del token actual.

RNN simple y salida $$\mathbf{h}_t=\tanh(W_{xh}\mathbf{x}_t+W_{hh}\mathbf{h}_{t-1}+\mathbf{b}_h), \qquad \mathbf{y}=\text{softmax}(W_{hy}\mathbf{h}_T+\mathbf{b}_y)$$ Para clasificación se usa el último estado (o un pooling sobre todos). Para generación se usa cada estado para predecir el siguiente token.

El problema del gradiente que se desvanece

Al retropropagar a través de $T$ pasos se multiplican $T$ jacobianos. Si sus normas son menores que 1, el gradiente se vuelve exponencialmente pequeño; si son mayores, explota. LSTM y GRU introducen compuertas y una ruta de memoria que mitiga el problema.

ArquitecturaIdeaUso típico
RNN simpleEstado oculto recurrenteBases pedagógicas, secuencias cortas
LSTMCelda de memoria con compuertas de olvido, entrada y salidaSeries temporales, texto con dependencias largas
GRUDos compuertas, menos parámetrosAlternativa eficiente a LSTM
BiLSTMDos direcciones concatenadasEtiquetado de secuencias, NER
Pythonclasificador_lstm.py
import torch
import torch.nn as nn
from torch.utils.data import DataLoader

class ClasificadorTexto(nn.Module):
    def __init__(self, vocab_size, emb_dim=128, hidden=128, n_classes=2, pad_idx=0):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, emb_dim, padding_idx=pad_idx)
        self.lstm = nn.LSTM(
            emb_dim, hidden, batch_first=True, bidirectional=True, dropout=0.2
        )
        self.dropout = nn.Dropout(0.3)
        self.fc = nn.Linear(hidden * 2, n_classes)

    def forward(self, x, lengths):
        emb = self.embedding(x)
        packed = nn.utils.rnn.pack_padded_sequence(
            emb, lengths.cpu(), batch_first=True, enforce_sorted=False
        )
        packed_out, (h, _) = self.lstm(packed)
        # h: (2, batch, hidden) -> concatenar direcciones
        h = torch.cat([h[0], h[1]], dim=1)
        return self.fc(self.dropout(h))

modelo = ClasificadorTexto(vocab_size=30000)
criterio = nn.CrossEntropyLoss()
optimizador = torch.optim.AdamW(modelo.parameters(), lr=1e-3)
Padding y longitudes Si rellenas todas las secuencias al mismo largo, el modelo debe ignorar el padding con pack_padded_sequence (o una máscara). De lo contrario, el estado final se contamina con tokens artificiales y el rendimiento cae sin dar errores.

Cuándo usar una RNN hoy

  • Secuencias cortas y recursos limitados (dispositivos, latencia estricta).
  • Tareas de etiquetado token a token con dependencias locales (NER, POS).
  • Series temporales con patrones secuenciales claros y datasets pequeños.
  • Como baseline antes de probar un Transformer.
La comparación honesta Un Transformer preentrenado pequeño suele superar a una LSTM entrenada desde cero en clasificación de texto, incluso con pocos datos. La LSTM tiene sentido cuando no puedes usar modelos grandes o cuando el coste de inferencia es crítico.
Ejercicio 8.4 · RNN vs. bolsa de palabras

Compara una LSTM con un clasificador TF-IDF + regresión logística en un dataset de reseñas. Usa la misma partición y reporta F1. Analiza en qué casos la LSTM gana y en qué casos no.

Orientación: con datasets pequeños o medianos, TF-IDF suele ser competitivo y mucho más rápido. La LSTM puede ganar cuando el orden y las negaciones son decisivas y hay suficientes ejemplos. Documenta el coste computacional junto con la métrica.

Lección 8.5

Atención y Transformers aplicados a texto

24 minSelf-attentionContexto

La atención permite que cada token consulte a todos los demás y construya una representación dependiente del contexto. Es la pieza que hizo posible que una misma palabra tenga vectores distintos según la frase, y la base de BERT, GPT y los LLM actuales.

La intuición antes de la fórmula

En «El banco estaba junto al río», la palabra «banco» debe mirar a «río» para resolverse como orilla. En «Fui al banco a pedir un crédito», debe mirar a «crédito». La self-attention calcula, para cada token, cuánto debe pesar cada otro token al construir su nueva representación.

Scaled dot-product attention $$Q=XW_Q,\quad K=XW_K,\quad V=XW_V, \qquad \text{Attention}(Q,K,V)=\text{softmax}\left(\frac{QK^\top}{\sqrt{d_k}}\right)V$$ $QK^\top$ mide compatibilidad entre consultas y claves. La escala $1/\sqrt{d_k}$ evita que los productos crezcan y saturen el softmax. El resultado es una mezcla ponderada de valores.
1

Atención con tres tokens

Supón que para el token «banco» los puntajes de compatibilidad con «río», «crédito» y «el» son 3, 1 y 0. Al aplicar softmax con $d_k=1$: $e^3\approx20.1$, $e^1\approx2.72$, $e^0=1$. La suma es 23.8, así que los pesos son 0.844, 0.114 y 0.042.

La representación de «banco» será $0.844\,\mathbf{v}_{\text{río}}+0.114\,\mathbf{v}_{\text{crédito}}+0.042\,\mathbf{v}_{\text{el}}$. El contexto domina la interpretación. Con múltiples cabezas, otras cabezas pueden capturar relaciones distintas, como sujeto–verbo o correferencia.

Por qué reemplazó a la recurrencia

  • Paralelización: todos los tokens se procesan a la vez, no secuencialmente.
  • Dependencias largas: cualquier par de posiciones se conecta en un paso, sin recorrer toda la secuencia.
  • Escalabilidad: permite entrenar con grandes corpus y modelos de miles de millones de parámetros.
  • Costo: la atención es $O(n^2d)$; para secuencias muy largas hay variantes eficientes.

Encoder, decoder y máscaras

TipoAttentionEjemplosTareas
EncoderBidireccionalBERT, RoBERTaClasificación, NER, QA extractiva
DecoderCausal (mira solo el pasado)GPT, LLaMAGeneración, completar texto
Encoder–decoderCruzada + causalT5, BARTTraducción, resumen
La máscara causal En un decoder, cada token solo puede atender a los anteriores. Sin máscara, el modelo vería el futuro durante el entrenamiento y aprendería a copiar la respuesta, arruinando la generación. La máscara es una matriz triangular superior con $-\infty$ antes del softmax.
Pythonatencion.py
import torch
import torch.nn.functional as F

def atencion(Q, K, V, mask=None):
    d_k = Q.size(-1)
    scores = Q @ K.transpose(-2, -1) / d_k ** 0.5
    if mask is not None:
        scores = scores.masked_fill(mask == 0, float("-inf"))
    pesos = F.softmax(scores, dim=-1)
    return pesos @ V, pesos

# batch=2, tokens=4, dimensión=8
Q = torch.randn(2, 4, 8)
K = torch.randn(2, 4, 8)
V = torch.randn(2, 4, 8)
salida, pesos = atencion(Q, K, V)
print(salida.shape, pesos.shape)   # (2,4,8) y (2,4,4)

# Máscara causal para generación
n = 4
causal = torch.tril(torch.ones(n, n)).bool()
_, pesos_causal = atencion(Q, K, V, mask=causal)
print(pesos_causal[0].round(decimals=2))  # triangular: cada fila solo mira hasta su posición
Los pesos de atención no son explicaciones Es tentador interpretar los mapas de atención como «lo que el modelo mira». Diversos estudios muestran que pesos altos no implican causalidad ni importancia. Úsalos como herramienta exploratoria, no como prueba de por qué el modelo tomó una decisión.
Ejercicio 8.5 · Atención paso a paso

Con una secuencia de cuatro tokens y dimensión $d_k=4$, calcula a mano (o con NumPy) la matriz de atención para una máscara causal. Comprueba que la primera fila concentra todo el peso en el primer token y que la última puede mirar a todos.

Orientación: con la máscara triangular, la fila $i$ solo tiene entradas válidas en columnas $j\le i$; tras softmax, los pesos suman 1. La última fila es la única con cuatro valores positivos. Verifica también que sin escala $1/\sqrt{d_k}$ los pesos se vuelven casi one-hot cuando las dimensiones crecen.

Lección 8.6

BERT, modelos de lenguaje y fine-tuning

24 minPreentrenamientoHugging Face

El preentrenamiento cambió las reglas: en lugar de entrenar desde cero con tus pocos datos, partes de un modelo que ya aprendió lenguaje en un corpus enorme y lo adaptas a tu tarea. Es la práctica estándar en NLP aplicado.

Preentrenamiento y ajuste

Preentrenamiento corpus masivo aprender lenguaje Fine-tuning tus datos etiquetados adaptar a la tarea Despliegue inferencia monitoreo El modelo preentrenado aporta representaciones; tus datos aportan la decisión específica.
El fine-tuning reutiliza representaciones generales y aprende una cabeza (o ajusta todo el modelo) para la tarea concreta.

Objetivos de preentrenamiento

  • Masked Language Modeling (BERT): se enmascara un 15 % de tokens y el modelo debe predecirlos usando ambos contextos. Fuerza representaciones bidireccionales.
  • Next Sentence Prediction: se predice si dos frases son consecutivas. Hoy se considera poco útil y muchos modelos lo omiten.
  • Autoregresivo (GPT): predecir el siguiente token. Permite generar y escalar a modelos enormes.
  • SentencePiece / BPE: tokenización por subpalabras que hace posible un vocabulario manejable y multilingüe.

Fine-tuning con Hugging Face

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

# 1) Datos: dos columnas de texto y una etiqueta entera
dataset = load_dataset("csv", data_files={"train": "train.csv", "test": "test.csv"})
etiquetas = dataset["train"].features["label"].names

# 2) Tokenizador del modelo base
modelo_base = "dccuchile/bert-base-spanish-wwm-cased"
tok = AutoTokenizer.from_pretrained(modelo_base)

def tokenizar(batch):
    return tok(batch["texto"], truncation=True, padding="max_length", max_length=128)

dataset = dataset.map(tokenizar, batched=True)

# 3) Modelo con cabeza de clasificación
modelo = AutoModelForSequenceClassification.from_pretrained(
    modelo_base, num_labels=len(etiquetas)
)

def metricas(eval_pred):
    logits, labels = eval_pred
    preds = np.argmax(logits, axis=-1)
    return {"f1_macro": f1_score(labels, preds, average="macro")}

args = TrainingArguments(
    output_dir="modelo_sentimiento",
    eval_strategy="epoch",
    save_strategy="epoch",
    learning_rate=2e-5,
    per_device_train_batch_size=16,
    num_train_epochs=3,
    weight_decay=0.01,
    load_best_model_at_end=True,
    metric_for_best_model="f1_macro",
    logging_steps=50,
    seed=42
)

trainer = Trainer(
    model=modelo,
    args=args,
    train_dataset=dataset["train"],
    eval_dataset=dataset["test"],
    compute_metrics=metricas
)

trainer.train()
trainer.save_model("modelo_final")
tok.save_pretrained("modelo_final")
El fine-tuning no perdona la fuga de datos Si el mismo usuario, documento o plantilla aparece en train y test, el F1 será irreal. Agrupa por entidad antes de dividir. En datasets construidos por plantillas, verifica que las variantes no compartan casi todo el texto.

Elegir un modelo base

SituaciónOpción razonableMotivo
Español, pocos datosBERT/RoBERTa en españolTokenizador y vocabulario adecuados
MultilingüeXLM-RCubre muchos idiomas con un solo modelo
Recursos muy limitadosDistilBERT, MiniLMMenor latencia y tamaño
GeneraciónModelos causales (GPT, LLaMA)Entrenados para predecir el siguiente token
Tareas de texto largasLongformer, BigBirdAtención eficiente para secuencias largas
¿Fine-tuning o prompting? Con cientos o miles de ejemplos etiquetados, el fine-tuning suele dar mejor rendimiento y menor coste por consulta. Con pocos ejemplos o tareas muy variadas, un LLM con prompting puede ser suficiente para prototipar. La decisión depende de datos, latencia, privacidad y coste.
Ejercicio 8.6 · Diseñar un fine-tuning

Tienes 4 000 reseñas en español etiquetadas en cinco categorías y quieres desplegar el modelo en una API con latencia menor a 100 ms. Diseña el experimento: modelo base, longitud máxima, métrica, validación y criterio para elegir entre DistilBERT y BERT-base.

Orientación: usar validación estratificada y agrupada por producto si hay duplicados; comparar F1 macro y latencia; ajustar max_length al percentil 95 de longitudes para no truncar información; elegir el modelo más pequeño que no pierda más de 1–2 puntos de F1; medir latencia en el hardware de despliegue, no solo en el notebook.

Lección 8.7

Evaluación y errores en NLP

18 minMétricasAnálisis de errores

En NLP, una métrica alta puede esconder un modelo que aprendió el dataset en lugar del idioma. Evaluar bien exige mirar los datos, segmentar por subgrupos y analizar errores concretos.

Métricas por tarea

TareaMétricas habitualesCuidado con
Clasificación de textoF1 macro, precisión/recall por clase, AUCDesbalance y clases minoritarias
Etiquetado de secuencias (NER)F1 a nivel de entidad (no de token)Fronteras parciales y esquemas BIO
Generación / resumenROUGE, BLEU, BERTScore, evaluación humanaMétricas n-grama no capturan fidelidad
TraducciónBLEU, chrF, COMETCorpus y dominio de referencia
QA extractivaExact Match, F1 por solapamientoRespuestas equivalentes con otra forma
1

Por qué F1 macro y no accuracy

Un dataset de soporte técnico tiene 90 % consultas de «cuenta», 8 % de «pagos» y 2 % de «seguridad». Un modelo que predice siempre «cuenta» obtiene 90 % de accuracy, pero F1 macro es aproximadamente $(1+0+0)/3=0.33$. La clase crítica de seguridad nunca se detecta.

F1 macro promedia por clase, así que una clase minoritaria con mal rendimiento pesa igual que la mayoritaria. Si el coste de fallar seguridad es alto, además conviene reportar recall de esa clase por separado.

Análisis de errores sistemático

  1. Muestrea 100–200 errores de validación, estratificados por clase y confianza.
  2. Etiqueta el tipo de error: ambigüedad, etiqueta incorrecta, falta de contexto, sesgo, truncamiento, dominio.
  3. Cuantifica las categorías y prioriza las que más impactan.
  4. Convierte los hallazgos en acciones: más datos, mejor esquema, cambios de preprocessing o de modelo.
Pythonanalisis_errores.py
import numpy as np
import pandas as pd
from sklearn.metrics import classification_report, confusion_matrix

probs = modelo.predict_proba(X_test)
preds = probs.argmax(axis=1)
confianza = probs.max(axis=1)

print(classification_report(y_test, preds, digits=3))
print(pd.DataFrame(
    confusion_matrix(y_test, preds),
    index=[f"real_{c}" for c in clases],
    columns=[f"pred_{c}" for c in clases]
))

# Errores con alta confianza: los más peligrosos
errores = pd.DataFrame({
    "texto": X_test,
    "real": y_test,
    "pred": preds,
    "confianza": confianza
})
errores = errores[errores.real != errores.pred].sort_values("confianza", ascending=False)
print(errores.head(25).to_string())

# Rendimiento por longitud del texto (posible truncamiento)
longitudes = [len(t.split()) for t in X_test]
bins = pd.qcut(longitudes, q=4, labels=["corto", "medio", "largo", "muy largo"])
print(pd.crosstab(bins, y_test == preds, normalize="index"))
El test set no es infinito Si evalúas decenas de variantes sobre el mismo test y eliges la mejor, estás sobreajustando el test aunque no entrenes con él. Reserva un conjunto final que solo mires una vez, o usa validación cruzada para las decisiones.

Sesgos y subgrupos

Un modelo puede tener buen rendimiento global y fallar sistemáticamente en ciertos dialectos, géneros o dominios. Reporta métricas por subgrupo cuando tengas la información; si no la tienes, al menos analiza errores por fuente y por longitud. La equidad no se deduce de una métrica agregada.

Ejercicio 8.7 · Plan de evaluación

Diseña la evaluación de un modelo que clasifica tickets de soporte en cinco categorías. Incluye métrica principal, métricas por clase, análisis de errores y qué subgrupos analizarías. ¿Qué harías si una clase tiene recall 0.4 y el resto supera 0.85?

Orientación: F1 macro como principal, recall por clase y matriz de confusión; revisar errores de la clase débil, comprobar si está mal definida o si hay pocos ejemplos; considerar oversampling, ajuste de umbral por clase o reetiquetado. No ocultes la clase débil en el promedio.

Lección 8.8

Ética, sesgos y límites de los modelos de lenguaje

20 minRiesgosCierre del módulo

Un modelo de lenguaje no «sabe»: predice texto plausible. Esa diferencia es crucial cuando se usa para decisiones que afectan personas. Este cierre no es una lista de prohibiciones, sino un marco para usar estas herramientas con responsabilidad.

Riesgos que aparecen en producción

Alucinación

Afirmar sin fundamento

El modelo genera texto coherente que puede ser falso. En dominios médicos, legales o financieros, toda salida debe verificarse con una fuente.

Sesgo

Reproducir desigualdades

El corpus contiene estereotipos. El modelo puede asignar atributos por género, origen o dialecto, tanto en clasificación como en generación.

Privacidad

Memorizar datos

Los modelos grandes pueden reproducir fragmentos de entrenamiento. No introduzcas datos personales en servicios externos sin garantías contractuales.

Uso dual

Automatizar abuso

La misma capacidad que genera resúmenes puede generar desinformación a escala. El diseño debe incluir límites y trazabilidad.

Un protocolo mínimo de uso responsable

  1. Define el propósito y el daño posible: ¿qué decisión toma una persona a partir de la salida?
  2. Verifica con fuentes: en tareas factuales, exige citas o recuperación de documentos (RAG).
  3. Mide por subgrupos: no aceptes una métrica agregada como prueba de equidad.
  4. Documenta límites: idioma, dominio, longitud, tipo de texto y fecha de corte del modelo.
  5. Mantén supervisión humana: en decisiones de alto impacto, el modelo propone y una persona decide.
  6. Registra y audita: entradas, salidas, versiones y cambios. Sin trazabilidad no hay rendición de cuentas.
No confundas fluidez con verdad La coherencia gramatical es independiente de la corrección factual. Un modelo puede escribir un párrafo impecable sobre un artículo que no existe. En evaluación, separa siempre fidelidad (¿lo que dice está respaldado?) de fluidez (¿se lee bien?).

RAG: conectar el modelo con documentos

Una forma práctica de reducir alucinaciones es Retrieval-Augmented Generation: dado un documento o pregunta, se recuperan fragmentos relevantes de una base de conocimiento y se le pide al modelo responder solo con esa evidencia. La recuperación se evalúa por separado (recall de fragmentos) y la generación por fidelidad a las fuentes.

Pseudocódigorag.txt
1. Indexar documentos: dividir en fragmentos, calcular embeddings, guardar en un índice vectorial.
2. Recuperar: embedder la pregunta y buscar los k fragmentos más similares.
3. Construir el prompt: instrucción + fragmentos + pregunta + "si no está en el contexto, di que no lo sabes".
4. Generar y citar: pedir que cada afirmación incluya el fragmento de origen.
5. Evaluar: recall@k de recuperación, fidelidad de la respuesta y tasa de abstenciones correctas.
Cierre del módulo Has recorrido desde la tokenización hasta los modelos de lenguaje. La idea que deberías retener: cada representación captura algo y pierde algo. Elige la más simple que resuelva tu problema, mide con datos representativos y documenta los límites. La sofisticación no sustituye a un buen conjunto de evaluación.
Ejercicio 8.8 · Auditoría de un sistema de texto

Un banco quiere usar un LLM para resumir reclamaciones de clientes y priorizarlas. Enumera cinco riesgos concretos y una mitigación para cada uno. ¿Qué métricas exigirías antes de ponerlo en producción?

Orientación: alucinación en el resumen (verificación con el texto original y muestreo humano), sesgo por tipo de cliente (métricas por subgrupo), privacidad (anonimización y contrato), deriva del lenguaje (monitoreo de distribuciones), automatización excesiva (decisión final humana). Métricas: fidelidad, recall de reclamaciones críticas, tasa de abstención, latencia y coste.

Curso de Machine Learning · teoría, matemáticas y práctica en Python Progreso local · 0% completado · Hecho para estudiar en cualquier lugar