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 desdesrc/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:
Pesos (escogidos para que los valores intermedios sean enteros cuando sea posible):
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:
- \(Q = X W_Q\) — ¿qué matriz queda?
- \(K = X W_K\) — ¿qué matriz queda?
- \(V = X W_V\) — ¿qué matriz queda?
- \(S = Q K^\top\) — ¿qué matriz queda?
- \(S / \sqrt{d_k} = S / \sqrt{2}\) — divide elemento a elemento.
- Aplica softmax por filas. Para la fila 0: \(\text{softmax}((s_{00}, s_{01}) / \sqrt{2})\). Usa la reescritura estable (resta el máximo).
- Repite para la fila 1.
- 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:
- Los seis archivos están commiteados.
max_abs_diff_paper_vs_code < 1e-5.- El caso sin escalar con \(d_k = 64\) muestra claramente la saturación del softmax (entrada máxima por fila > 0.95).
README.mddescribe 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_Kno se transpone a nivel de pesos. No intentes "arreglar" la asimetría transponiendo — la asimetría es el quid de la cuestión (vertheory/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.