Skip to content

English · Español

Lab 00 — Atención a mano

Objetivo: derivar a mano un cálculo de atención (attention) con 2 tokens y una sola cabeza, después implementar single-head attention en NumPy y verificar que ambos coinciden con tolerancia 1e-5.

Tiempo estimado: 90–120 minutos.

Requisito previo: leídos los cinco archivos de theory/.


Qué produces

Un directorio experiments/15-attention-by-hand/ que contiene:

  • paper_derivation.md — tu derivación paso a paso (manuscrita o tecleada) del ejemplo de juguete de abajo. Números, no símbolos.
  • attention.py — tu implementación en NumPy, importando desde src/minimodel/attention/attention.py.
  • verify.py — script que ejecuta tu implementación sobre el ejemplo de juguete y comprueba que coincide con los números del papel.
  • verify_output.txt — salida capturada que muestra ambos conjuntos de números y la diferencia por elemento.
  • manifest.json.
  • README.md (1–2 párrafos).

El ejemplo de juguete

Tokens: \(T = 2\). Dimensión de embedding: \(d = 2\). Dimensión por cabeza: \(d_k = d_v = 2\) (de modo que una sola cabeza ocupa toda la dimensión).

Entradas:

\[ X = \begin{pmatrix} 1 & 0 \\ 0 & 1 \end{pmatrix} \]

Pesos (escogidos para que los valores intermedios sean enteros cuando sea posible):

\[ W_Q = \begin{pmatrix} 1 & 0 \\ 0 & 1 \end{pmatrix}, \quad W_K = \begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix}, \quad W_V = \begin{pmatrix} 2 & 0 \\ 0 & 3 \end{pmatrix} \]

Sin máscara. Una sola cabeza. Scaled dot-product attention.

TODOs

Bloque A — derivar en papel

En paper_derivation.md, sin escribir nada de código, calcula paso a paso:

  1. \(Q = X W_Q\) — ¿qué matriz queda?
  2. \(K = X W_K\) — ¿qué matriz queda?
  3. \(V = X W_V\) — ¿qué matriz queda?
  4. \(S = Q K^\top\) — ¿qué matriz queda?
  5. \(S / \sqrt{d_k} = S / \sqrt{2}\) — divide elemento a elemento.
  6. Aplica softmax por filas. Para la fila 0: \(\text{softmax}((s_{00}, s_{01}) / \sqrt{2})\). Usa la reescritura estable (resta el máximo).
  7. Repite para la fila 1.
  8. Multiplica \(A V\). Muestra la matriz resultante.

Escribe cada matriz intermedia con sus cuatro entradas rellenas.

Bloque B — implementación en NumPy

Antes de escribir código, lee src/minimodel/attention/BLUEPRINT.md. Después, en src/minimodel/attention/attention.py:

  • Implementa single_head_attention(Q: np.ndarray, K: np.ndarray, V: np.ndarray, mask: np.ndarray | None = None) -> np.ndarray.
  • Usa el softmax estable (helper softmax_stable, o inlínealo).
  • Cinco líneas como mucho en el cuerpo. Si acabas escribiendo 20 líneas, te estás complicando.

Bloque C — verificar

En verify.py:

  • Configura las entradas y pesos exactamente como arriba.
  • Calcula Q, K, V usando NumPy.
  • Llama a single_head_attention(Q, K, V).
  • Compara elemento a elemento contra tus números del papel. La diferencia máxima por elemento debe ser < 1e-5.
  • Imprime ambas matrices lado a lado, con la diferencia por elemento. Captura la salida en verify_output.txt.

Bloque D — explorar: qué pasa sin el escalado

El argumento de varianza en theory/02-scaled-dot-product.md dice que dividimos por \(\sqrt{d_k}\) para evitar que el softmax sature cuando \(d_k\) es grande. Vamos a verlo.

  • En verify.py, ejecuta el ejemplo de juguete con \(d_k = 64\) en lugar de \(d_k = 2\) (usa \(X = \mathcal{N}(0, 1)\), \(W_*\) ortogonales aleatorias). Ejecútalo dos veces — una con el escalado \(/\sqrt{d_k}\), otra sin él.
  • Para cada caso, imprime la matriz de atención A. La entrada máxima de \(A\) por fila debería ser:
  • Con escalado: cerca de \(1/T\) si las queries son aproximadamente ortogonales — el softmax está haciendo su trabajo.
  • Sin escalado: muy cerca de 1.0 — una posición domina, el softmax ha saturado.
  • Confirma esto en la salida. Anótalo en README.md.

Bloque E — manifest

{
  "experiment": "15-attention-by-hand",
  "date": "YYYY-MM-DD",
  "seed": 42,
  "versions": { "python": "3.11.x", "numpy": "X.Y.Z" },
  "results_summary": {
    "max_abs_diff_paper_vs_code": null,
    "softmax_max_entry_scaled_d_k_64": null,
    "softmax_max_entry_unscaled_d_k_64": null
  }
}

Restricciones

  • Sin PyTorch. (Anti-meta §10.)
  • Primero papel, después código. Si escribes primero el NumPy y luego "derivas" la versión en papel, has desvirtuado el lab. El objetivo es saber qué respuesta predice la matemática antes de ejecutar nada.
  • Softmax estable. Usa resta del máximo. Nada de exp(x) / sum(exp(x)) ingenuo.

Condiciones de parada

Hecho cuando:

  1. Los seis archivos están commiteados.
  2. max_abs_diff_paper_vs_code < 1e-5.
  3. El caso sin escalar con \(d_k = 64\) muestra claramente la saturación del softmax (entrada máxima por fila > 0.95).
  4. README.md describe ambos hallazgos en 2–3 frases cada uno.

Trampas

  • El softmax de \((s_0, s_1)\) con \(s_0 = s_1\) debe dar \((0.5, 0.5)\), no \((1, 0)\). Compruébalo a mano.
  • np.sqrt(d_k) es un float de Python; puedes dividir una matriz numpy directamente por él. No construyas un array numpy para un escalar.
  • Precisión numérica en fp32. Tu diferencia máxima puede ser 1e-7 o 1e-6 según el orden de las operaciones. 1e-5 es el umbral fijado.
  • W_K no se transpone a nivel de pesos. No intentes "arreglar" la asimetría transponiendo — la asimetría es el quid de la cuestión (ver theory/01-query-key-value.md).

Cuándo consultar solutions/

Cuando los seis archivos estén commiteados y las aserciones pasen. Solución en solutions/00-attention-by-hand-ref.md.


Siguiente lab: 01-multi-head-attention.md.