ML/AIMLE·AIMachine Learning Engineering
Deep Learning · Transformers · Artículo completo

Transformers

La arquitectura de los modelos de lenguaje: self-attention, multi-head, codificación posicional y bloques residuales, desde NumPy hasta un mini-GPT entrenado con el texto de este portal.

Resultados obtenidos con NumPy 2.4 y PyTorch 2.14 en CPU · torch.manual_seed(42) · Laboratorio relacionado

01Qué es

El Transformer es una arquitectura de red neuronal para secuencias presentada en 2017 en «Attention Is All You Need». Sustituye la recurrencia de las RNN por atención: cada elemento de la secuencia mira directamente a todos los demás y decide cuánto le importa cada uno. Así se procesa toda la secuencia en paralelo.

Es la base de BERT, GPT, Claude, Llama y de los modelos de visión y audio más recientes. Un LLM es, en esencia, una pila de bloques Transformer entrenada para predecir el siguiente token.

02Intuición

En la frase «el gato no cruzó la calle porque estaba cansado», para entender «cansado» hay que saber que se refiere al gato. La atención lo resuelve como una búsqueda: cada palabra lanza una pregunta (Query), cada palabra ofrece una etiqueta (Key) y un contenido (Value). Donde la pregunta encaja con la etiqueta, se toma más contenido.

Varias cabezas hacen búsquedas distintas en paralelo (una puede seguir la sintaxis, otra la posición, otra las referencias), y las capas se apilan para construir representaciones cada vez más abstractas.

Sigue las cuentas en el visualizador de atención y recorre las 13 etapas de un modelo completo en el Transformer Visualizer.

03Fundamento matemático

Atención de producto escalar escalado

Attention(Q,K,V)=softmax ⁣(QK⊤dk+M)V,Q=XWQ, K=XWK, V=XWV\text{Attention}(Q, K, V) = \text{softmax}\!\left(\frac{QK^{\top}}{\sqrt{d_k}} + M\right) V, \qquad Q = XW_Q,\ K = XW_K,\ V = XW_V

M es la máscara causal (−∞ por encima de la diagonal) en los modelos que generan texto. Dividir por dk\sqrt{d_k} evita que los productos escalares crezcan con la dimensión y saturen el softmax.

Multi-head

MultiHead(X)=Concat(head1,…,headh) WO\text{MultiHead}(X) = \text{Concat}(\text{head}_1, \dots, \text{head}_h)\,W_O

Bloque (variante pre-norm, la de GPT-2 en adelante)

X′=X+MultiHead(LN(X)),Y=X′+FFN(LN(X′))X' = X + \text{MultiHead}(\text{LN}(X)), \qquad Y = X' + \text{FFN}(\text{LN}(X'))

El coste de la atención es O(T2⋅d)O(T^2 \cdot d) en la longitud T de la secuencia: duplicar el contexto cuadruplica el cálculo de la atención. Por eso el tamaño de contexto es un recurso caro.

04Algoritmo paso a paso

  1. Tokeniza el texto y convierte cada token en un vector (embedding).
  2. Suma a cada vector información de su posición.
  3. Pasa la secuencia por N bloques: atención multi-cabeza y red feed-forward, cada una con normalización y conexión residual.
  4. Proyecta el vector de cada posición sobre el vocabulario para obtener logits.
  5. Entrena con entropía cruzada para que cada posición prediga el token siguiente.
  6. Para generar: elige un token de la distribución, añádelo y repite.

05Pseudocódigo

función TRANSFORMER(ids):
    X ← Embedding(ids) + Posición(0 … T−1)
    para cada bloque:
        H ← LayerNorm(X)
        X ← X + Concat(ATENCIÓN(H·W_Qᵢ, H·W_Kᵢ, H·W_Vᵢ) para cada cabeza i)·W_O
        X ← X + FFN(LayerNorm(X))
    devolver LayerNorm(X)·W_vocab          # logits

función ATENCIÓN(Q, K, V):
    S ← Q·Kᵀ / √d_k ;  S[j > i] ← −∞
    devolver softmax(S)·V

06Implementación desde cero en Python

Python
import numpy as np

def self_attention(X, Wq, Wk, Wv):
    Q, K, V = X @ Wq, X @ Wk, X @ Wv
    S = Q @ K.T / np.sqrt(K.shape[1])                          # puntuaciones escaladas
    S = np.where(np.tril(np.ones_like(S)) == 1, S, -np.inf)    # máscara causal
    A = np.exp(S - S.max(1, keepdims=True))
    A /= A.sum(1, keepdims=True)                               # softmax por filas
    return A @ V, A

# Coincide con torch.nn.functional.scaled_dot_product_attention(..., is_causal=True)

Con una secuencia de 4 elementos y d = 8, cada fila de la matriz de atención suma 1, la parte por encima de la diagonal es exactamente 0, y la salida coincide con scaled_dot_product_attention de PyTorch (comprobado con np.allclose).

07Implementación con PyTorch: un mini-GPT

PyTorch
import torch, torch.nn as nn

class Bloque(nn.Module):
    def __init__(s):
        super().__init__()
        s.ln1, s.ln2 = nn.LayerNorm(D), nn.LayerNorm(D)
        s.attn = nn.MultiheadAttention(D, H, dropout=0.2, batch_first=True)
        s.ffn = nn.Sequential(nn.Linear(D, 4 * D), nn.GELU(), nn.Linear(4 * D, D), nn.Dropout(0.2))
        s.register_buffer("mask", torch.triu(torch.ones(T, T, dtype=torch.bool), 1))

    def forward(s, x):
        t = x.shape[1]
        h = s.ln1(x)
        x = x + s.attn(h, h, h, attn_mask=s.mask[:t, :t], need_weights=False)[0]   # residual
        return x + s.ffn(s.ln2(x))                                                 # residual

class MiniGPT(nn.Module):
    def __init__(s):
        super().__init__()
        s.tok, s.pos = nn.Embedding(V, D), nn.Embedding(T, D)    # tokens + posiciones aprendidas
        s.bloques = nn.Sequential(*[Bloque() for _ in range(L)])
        s.ln, s.head = nn.LayerNorm(D), nn.Linear(D, V, bias=False)

    def forward(s, idx):
        x = s.tok(idx) + s.pos(torch.arange(idx.shape[1]))
        return s.head(s.ln(s.bloques(x)))                       # logits: [B, T, V]

Es la misma estructura que GPT-2 a escala mínima: 2 bloques, dmodel = 96, 4 cabezas, contexto de 64 caracteres y 248.064 parámetros. Para modelos reales se usan las clases de Hugging Face transformers.

08Dataset de ejemplo

El modelo se entrena con el propio texto de este portal: las definiciones de los temas, las descripciones de laboratorios y proyectos y los párrafos de los artículos, un total de 19.612 caracteres en castellano con un vocabulario de 94 caracteres distintos. El 90 % se usa para entrenar y el 10 % final para validar. Es un corpus minúsculo (GPT-3 se entrenó con cientos de miles de millones de tokens), elegido a propósito para que se vea lo que ocurre cuando faltan datos.

09Entrenamiento

Python
opt = torch.optim.AdamW(modelo.parameters(), lr=2e-3, weight_decay=0.1)
mejor = float("inf")
for paso in range(1, 1201):
    x, y = lote(entrenamiento)                   # y es x desplazado una posición
    perdida = F.cross_entropy(modelo(x).view(-1, V), y.view(-1))
    opt.zero_grad(); perdida.backward(); opt.step()
    if paso % 100 == 0:
        v = perdida_validacion()
        if v < mejor:                            # early stopping: guarda el mejor modelo
            mejor, estado = v, copia_de(modelo.state_dict())
0123451100200300400500600700800900100011001200paso de entrenamientomejor modelo
pérdida de entrenamientopérdida de validación
La validación toca fondo en el paso 600 (2,114) y después sube mientras el entrenamiento sigue bajando hasta 0,943: el modelo empieza a memorizar. El early stopping se queda con los pesos del paso 600. Entrenamiento completo: 100 segundos en CPU.

Una primera prueba con un modelo mayor (3 bloques, d = 128, 632.960 parámetros, sin dropout) sobre un corpus sin limpiar de 26.490 caracteres lo mostró de forma extrema: la validación llegó a su mínimo (2,81) en el paso 250 y acabó en 5,13 al paso 2.000, con la pérdida de entrenamiento en 0,22. Demasiados parámetros para tan pocos datos.

10Evaluación

Python
@torch.no_grad()
def generar(modelo, idx, n, temperatura=0.8):
    for _ in range(n):
        logits = modelo(idx[:, -T:])[:, -1] / temperatura
        siguiente = torch.multinomial(F.softmax(logits, -1), 1)
        idx = torch.cat([idx, siguiente], 1)
    return idx

Texto generado a partir de «El modelo » con temperatura 0,8, tal cual lo produjo el modelo:

El modelo con dista. / Elos pícioner y el de suprobusta de con error un con en allos emprendizar decer la conticación de contrones de la pantacionos proproductos sobre paci

Con 20.000 caracteres ha aprendido la ortografía del castellano, palabras frecuentes («con», «de», «error», «sobre»), terminaciones como «-ación» y la puntuación, pero no significado. Es el primer peldaño de lo que hace un LLM: la misma arquitectura, con miles de veces más parámetros y millones de veces más texto, produce frases coherentes.

11Métricas

perplejidad=e L,L=−1N∑tlog⁡p(xt∣x<t)\text{perplejidad} = e^{\,L}, \qquad L = -\frac{1}{N}\sum_{t} \log p(x_t \mid x_{<t})

Un modelo que no sabe nada reparte la probabilidad por igual entre los 94 caracteres: pérdida ln⁡94=4,54\ln 94 = 4{,}54, perplejidad 94. Al empezar, el modelo mide 4,70, cerca de ese valor teórico. El mejor modelo llega a 2,114 en validación, una perplejidad de 8,3: en cada posición duda, en promedio, como si eligiera entre unos 8 caracteres en lugar de 94.

12Visualización

Recuento de parámetros de GPT-2 small
vocab, ctx, d, capas = 50257, 1024, 768, 12        # GPT-2 small
por_capa = (2*d                    # LayerNorm 1
            + d*3*d + 3*d          # proyección Q, K, V
            + d*d + d              # proyección de salida de la atención
            + 2*d                  # LayerNorm 2
            + d*4*d + 4*d          # FFN: expansión
            + 4*d*d + d)           # FFN: compresión
total = vocab*d + ctx*d + capas*por_capa + 2*d
print(por_capa, total)             # 7087872  124439808
12 bloques Transformer85.1 M
Embeddings de tokens (50.257 × 768)38.6 M
Embeddings de posición (1.024 × 768)0.8 M
LayerNorm final0.0 M
Dónde están los 124.439.808 parámetros de GPT-2 small. Cada bloque tiene 7.087.872, de los que dos tercios están en la red feed-forward.

13Ventajas

  • Procesa toda la secuencia en paralelo: entrenamiento mucho más rápido que las RNN en GPU.
  • Conecta directamente elementos lejanos de la secuencia.
  • Escala de forma predecible: más datos, parámetros y cálculo dan mejores modelos.
  • Sirve para texto, imágenes (ViT), audio, código, proteínas y combinaciones multimodales.

14Limitaciones

  • Coste cuadrático de la atención con la longitud del contexto.
  • Necesita enormes cantidades de datos y cálculo para dar lo mejor.
  • Sin información de posición no distingue el orden: hay que añadirla.
  • Los modelos grandes son caros de servir y difíciles de interpretar.

15Hiperparámetros

ParámetroMini-GPT (este artículo)GPT-2 smallEfecto
d_model96768Dimensión de los vectores de cada token.
n_layers212Número de bloques apilados.
n_heads412Cabezas de atención; d_model se reparte entre ellas.
d_ff3843.072Ancho de la red feed-forward (normalmente 4 · d_model).
context64 caracteres1.024 tokensLongitud máxima de la secuencia.
vocab94 caracteres50.257 tokens BPETamaño del vocabulario.
dropout0.20.1Regularización durante el entrenamiento.
lr / weight_decay2e-3 / 0.1—Optimizador AdamW.

16Casos de uso

  • Modelos de lenguaje y asistentes conversacionales.
  • Traducción automática y resumen.
  • Búsqueda semántica y RAG con embeddings (BERT y derivados).
  • Visión (ViT), voz (Whisper) y generación de código.

17Aplicaciones industriales

  • Asistentes sobre documentación técnica con RAG: manuales, normativa, históricos de incidencias.
  • Clasificación y extracción de información de tickets, correos y partes de trabajo.
  • Series temporales: modelos fundacionales de previsión basados en Transformers.
  • Agentes que consultan sistemas internos y ejecutan tareas.

18Ejercicio práctico

Varía el número de cabezas y elimina la codificación posicional para ver qué aporta cada pieza.

Plantilla
# Ejercicio: mide el efecto del número de cabezas y de la codificación posicional.
# 1) Entrena con H = 1, 2, 4 y 8 (D = 96 se reparte entre las cabezas) y compara la mejor pérdida de validación.
# 2) Elimina s.pos del forward (sin información de posición). ¿Qué pasa con la pérdida? ¿Y con el texto generado?

19Código completo

La atención en NumPy, el mini-GPT completo con su entrenamiento y generación, y el recuento de GPT-2:

El entrenamiento tarda unos 2 minutos en CPU; en Colab con GPU, segundos.

20Referencias y papers

  1. A. Vaswani et al. (2017). «Attention Is All You Need». NeurIPS 2017. Ficha en la biblioteca.
  2. D. Bahdanau, K. Cho, Y. Bengio (2015). «Neural Machine Translation by Jointly Learning to Align and Translate». ICLR 2015.
  3. J. L. Ba, J. R. Kiros, G. E. Hinton (2016). «Layer Normalization». arXiv:1607.06450.
  4. A. Radford, J. Wu, R. Child, D. Luan, D. Amodei, I. Sutskever (2019). «Language Models are Unsupervised Multitask Learners». Informe técnico de OpenAI (GPT-2).
  5. J. Devlin, M.-W. Chang, K. Lee, K. Toutanova (2019). «BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding». NAACL 2019. Ficha en la biblioteca.
  6. A. Karpathy. nanoGPT (repositorio de código). github.com/karpathy/nanoGPT.