Skip to content

English · Español

04 — Enmascarado: causal y de padding

La máscara es lo que evita que el modelo "haga trampa" mirando al futuro durante el entrenamiento (máscara causal), y lo que evita que mire al relleno cuando las secuencias en un batch tienen distinta longitud (máscara de padding). Lo importante: la máscara se suma como \(-\infty\) antes del softmax, no se multiplica por cero después. Multiplicar después deja al gradiente "filtrarse" — es un bug clásico que sigue apareciendo en código de producción.

Este archivo deriva las dos máscaras estándar de attention y muestra la manera correcta de aplicarlas.


Por qué enmascarar algo

Por defecto, cada posición en attention atiende a cada otra posición. Para algunas tareas eso está bien. Para dos casos importantes, no.

Caso 1 — Modelado de lenguaje causal (entrenamiento)

Entrenamos un modelo de lenguaje para predecir el token \(t+1\) dados los tokens \(1, \ldots, t\). Si la atención de la posición \(t\) pudiera leer de la posición \(t+1, t+2, \ldots\), el modelo tendría acceso a la respuesta durante el entrenamiento. Aprendería a copiar del futuro — trivialmente alcanzando 100% de precisión en entrenamiento y 0% en inferencia.

Solución: evitar que la posición \(i\) atienda a posiciones \(j > i\). Máscara causal (causal mask).

Caso 2 — Secuencias batched de longitud variable (padding)

En el bucle de entrenamiento de la Fase 18, agruparemos secuencias de diferentes longitudes en batches. Las secuencias más cortas se rellenan con un token <PAD> para igualar la más larga del batch. No queremos que attention atienda al padding — esas posiciones no llevan información.

Solución: evitar que cualquier posición atienda a las posiciones de padding. Máscara de padding.

Ambas máscaras se combinan en una única matriz \((T \times T)\) añadida pre-softmax.

La máscara causal

Para una secuencia de longitud \(T\), la máscara causal \(M^{\text{causal}}\) es una matriz \(T \times T\):

\[ M^{\text{causal}}_{ij} = \begin{cases} 0 & \text{si } j \leq i \\ -\infty & \text{si } j > i \end{cases} \]

Así que \(M^{\text{causal}}\) es triangular inferior con ceros en y debajo de la diagonal, \(-\infty\) por encima.

Aplicar pre-softmax:

\[ A = \text{softmax}\left(\frac{Q K^\top}{\sqrt{d_k}} + M^{\text{causal}}\right) \]

Las entradas \(-\infty\) empujan \(e^{s + (-\infty)} = 0\), así que las posiciones \(j > i\) obtienen peso de atención cero desde la query \(i\). No pueden influir en la salida de la posición \(i\).

¿Por qué \(j \leq i\), no \(j < i\)?

La posición \(i\) atiende a las posiciones \(0, 1, \ldots, i\). Incluyéndose a sí misma. Esto es esencial — la representación de un token debe depender de sí mismo; de lo contrario la capa solo enruta información desde otros sitios y descarta el propio contenido del token.

El off-by-one es un bug muy común. Verifícalo en el lab 02 con un test de perturbación.

Implementación numérica

No uses literalmente np.inf. Usa un número negativo grande — -1e9 es estándar. Esto evita NaN si alguna reducción sobre las entradas enmascaradas ocurre antes del softmax (lo cual no debería, pero codificación defensiva).

def causal_mask(T: int) -> np.ndarray:
    mask = np.triu(np.ones((T, T), dtype=np.float32), k=1) * -1e9
    return mask  # zeros on/below diag, -1e9 above

(np.triu con k=1 produce una matriz triangular superior con cero debajo y en la diagonal, uno por encima. Multiplica por \(-10^9\) y tenemos la máscara aditiva.)

La máscara de padding

Supón que tu batch tiene secuencias de longitudes \(T_1, T_2, \ldots, T_B\), rellenadas a \(T_{\max}\). Para la secuencia \(b\), las posiciones \(T_b, T_b + 1, \ldots, T_{\max} - 1\) son padding.

Para cada secuencia en el batch, define la máscara de padding:

\[ M^{\text{pad}}_{ij} = \begin{cases} 0 & \text{si } j \text{ no es posición de padding} \\ -\infty & \text{si } j \text{ es posición de padding} \end{cases} \]

Combina con la causal:

\[ M = M^{\text{causal}} + M^{\text{pad}} \]

(Suma de dos máscaras aditivas: una posición está enmascarada si cualquiera de las máscaras quiere enmascararla.)

La Fase 15 no usa batches — atendemos a secuencias individuales — así que no implementamos la máscara de padding aquí. Está documentada por completitud. El bucle de entrenamiento de la Fase 18 la añadirá.

El error crítico: enmascarado multiplicativo después del softmax

Una implementación ingenua podría calcular attention sin máscara, luego multiplicar por una máscara 0/1 después:

# WRONG
attn = softmax(scores)                 # full attention, all positions
attn = attn * mask_01                   # zero out forbidden positions
out = attn @ V

Esto está roto de dos maneras:

  1. Los pesos de atención restantes no suman 1. Después de poner a cero, las filas de attn suman algo \(< 1\). La salida es la suma ponderada con una "masa faltante". La salida del modelo está implícitamente escalada hacia abajo para posiciones que tienen muchos vecinos prohibidos. Numéricamente no catastrófico, semánticamente equivocado.

  2. Fuga de gradiente. Incluso después de poner a cero los pesos de atención, el gradiente \(\partial L / \partial \text{scores}_{ij}\) para las posiciones prohibidas no es cero. El softmax ve esas posiciones durante el forward; sus scores afectan la normalización de las posiciones no enmascaradas. La información de las posiciones prohibidas se filtra a través de la normalización. Para un LM causal, esto es información sobre tokens futuros filtrándose hacia atrás a predicciones pasadas — el fallo exacto que queríamos prevenir.

La máscara matemáticamente correcta es aditiva \(-\infty\) pre-softmax. Después del softmax, las entradas prohibidas son exactamente cero, y contribuyen gradiente cero (porque \(e^{-\infty} = 0\) tiene derivada cero respecto a cualquier cosa).

Regla: máscaras siempre aditivas, pre-softmax, con \(-\infty\) (o \(-10^9\)). Nunca multiplicativas post-softmax. Es un bug que aparece en código real con frecuencia preocupante.

Lab 02 — test de perturbación para la máscara causal

La forma de probar que tu máscara causal funciona:

  1. Genera una entrada aleatoria \(X\) de longitud \(T\).
  2. Calcula la salida de attention \(Y = \text{Attention}(X)\).
  3. Haz \(X' = X\) pero cambia la posición \(T - 1\) (la última posición) por algo completamente diferente.
  4. Calcula \(Y' = \text{Attention}(X')\).
  5. Verifica \(Y[0:T-1] = Y'[0:T-1]\) con la precisión numérica. (El cambio en la última posición no debe propagarse a las salidas anteriores.)

Si las posiciones antes de \(T - 1\) en \(Y\) difieren de \(Y'\), tu máscara está equivocada. El off-by-one es el culpable típico.

Caso especial: atención bidireccional (estilo BERT)

Para algunos modelos — encoders, BERT — cada posición atiende a cada posición. Sin máscara causal. Solo enmascaramos el padding.

El Mini-GPT en la Fase 17 es decoder-only y causal. Usamos la máscara causal. La atención bidireccional está documentada aquí para vocabulario; no la construimos.

Atención por ventana deslizante (un párrafo)

Algunos modelos modernos (Longformer, Mistral) reemplazan la máscara causal por una máscara causal con ventana: la posición \(i\) atiende a las posiciones \(\max(0, i - w), \ldots, i\) para algún tamaño de ventana \(w\). Esto reduce la complejidad de \(O(T^2)\) a \(O(T w)\).

La construcción de la máscara es la misma — aditiva, pre-softmax — solo con más ceros puestos a cero:

def windowed_causal_mask(T: int, window: int) -> np.ndarray:
    mask = np.full((T, T), -1e9, dtype=np.float32)
    for i in range(T):
        mask[i, max(0, i - window + 1):i + 1] = 0.0
    return mask

No se usa en la Fase 15. Se menciona porque el término aparecerá.

Lo que este archivo NO cubre

  • Implementación de la máscara de padding. Fase 18 (entrenamiento, donde llegan los batches).
  • Atención por ventana deslizante / local. Fase 27 o fuera de alcance. Solo esbozada.
  • Máscaras block-sparse (BigBird, etc.). Fuera de alcance.
  • Construcción de la máscara para inferencia con KV-cache. Fase 22+. La KV cache cambia la forma de la máscara.
  • Atención bidireccional / estilo BERT. Fuera de alcance. Currículo decoder-only.

Recapitulación

  • Máscara causal: ceros triangular inferior aditivos, \(-\infty\) por encima de la diagonal. La posición \(i\) atiende a \(0, \ldots, i\) inclusive.
  • Máscara de padding: \(-\infty\) en las posiciones de padding. Añadida a la máscara causal.
  • Siempre aditiva, siempre pre-softmax. La multiplicativa post-softmax filtra el gradiente.
  • El Lab 02 verifica con un test de perturbación.
  • La Fase 15 implementa solo la máscara causal. El padding espera a la Fase 18.

Ya has leído los cinco archivos de teoría de la Fase 15. Antes de abrir el lab:

  1. Escribe la ecuación completa de attention de memoria.
  2. Reproduce el argumento de varianza para \(\sqrt{d_k}\).
  3. Dibuja la superficie de API desde BLUEPRINT.md.
  4. Esboza la matriz de máscara causal para \(T = 4\).

Si alguno de estos se siente flojo, vuelve a leer el archivo relevante.


Siguiente: fin de la teoría. Procede a ../lab/00-attention-by-hand.md.