English · Español
01 — Query, Key, Value: por qué tres proyecciones¶
Tres matrices, no una. La Q dice "qué busco", la K dice "qué tengo", la V dice "qué entrego si me eligen". Separar las tres permite (a) atenciones asimétricas (i atiende a j ≠ j atiende a i) y (b) decoupling entre la representación que coincide y la información que se devuelve. Sin ese decoupling, el modelo solo podría devolver lo mismo que usa para coincidir, lo cual rompe la generalidad del mecanismo.
Este archivo deriva Q, K, V como proyecciones de la entrada, y responde a la pregunta "¿por qué tres matrices y no una o dos?". Esta es una de las preguntas más frecuentes sobre attention; la respuesta compensa el tiempo que cuesta aterrizarla.
Planteamiento¶
Tenemos una secuencia de entrada de \(T\) tokens, cada uno representado como un vector de dimensión \(d\). Los apilamos como una matriz \(X \in \mathbb{R}^{T \times d}\). (Cómo se produjo \(X\) — token embedding + codificación posicional — es una cuestión de la Fase 16; aquí lo tomamos como dado.)
Queremos producir una secuencia de salida de \(T\) vectores, donde cada salida sea una combinación ponderada de vectores "value" derivados de \(X\), con pesos calculados a partir de alguna similitud entre vectores "query" y "key" también derivados de \(X\).
La versión más simple sería:
Esto es no entrenable: no hay parámetros aprendidos en ninguna parte. La similitud y la recuperación del value están ambas fijadas en "producto escalar de la entrada en bruto". El modelo no tiene grados de libertad.
Para hacerlo aprendible, proyectamos \(X\) a través de tres matrices:
con \(W_Q \in \mathbb{R}^{d \times d_k}\), \(W_K \in \mathbb{R}^{d \times d_k}\), \(W_V \in \mathbb{R}^{d \times d_v}\). Entonces
Las tres matrices dan al modelo control independiente sobre tres cosas: qué pedir (vía \(W_Q\)), qué anunciar (vía \(W_K\)), qué entregar (vía \(W_V\)).
La analogía de búsqueda en diccionario¶
Una búsqueda en un dict de Python d[key] es:
- Proporcionas una query (la key que estás buscando).
- El dict guarda pares (key, value).
- Devuelve el value cuya key coincide exactamente con la query.
Attention es la generalización suave de esto:
- La posición actual emite un query \(Q_i\) — un vector que codifica "¿qué estoy buscando aquí?".
- Cada posición \(j\) tiene una key \(K_j\) — un vector que codifica "esto es lo que soy, a efectos de coincidencia".
- Cada posición \(j\) tiene un value \(V_j\) — un vector que codifica "esto es lo que devolveré si coincido".
- El producto escalar \(Q_i \cdot K_j\) mide cuán bien coincide el query con cada key. Softmax normaliza a pesos que suman 1. La salida es la suma ponderada de los values.
Concretamente, en la secuencia canónica de 8 tokens I work, you work, he ___, la posición 7 (el hueco a rellenar) necesita saber:
- "¿Cuál es el pronombre sujeto en esta cláusula?" — su query pide las características gramaticales del sujeto.
- La posición 6 (
he) tiene una key que anuncia "soy un pronombre de 3ª singular". - La posición 6 tiene un value que entrega "característica de concordancia: hace falta el sufijo -s".
El query en la posición 7 hace producto escalar con todas las keys; la key en la posición 6 coincide mejor; el softmax pone la mayor parte del peso en \(j=6\); la salida en la posición 7 hereda el value de la posición 6 — "usa la forma con -s del verbo".
La característica de coincidencia (lo que hay en \(K_6\): "pronombre de 3ª singular") es diferente de la información entregada (lo que hay en \(V_6\): "concordancia verbal = -s"). Ese es todo el sentido de tener \(K\) y \(V\) como proyecciones diferentes.
¿Por qué tres? Cuatro argumentos, en sutileza creciente¶
Argumento 1 — Atención asimétrica¶
Self-attention es direccional: la atención de la posición \(i\) a la posición \(j\) no es igual a la atención de la posición \(j\) a la posición \(i\).
Si usáramos solo una proyección (llamémosla \(W\)), entonces \(\text{sim}(W x_i, W x_j) = (W x_i) \cdot (W x_j) = x_i^\top W^\top W x_j\). Esto es simétrico en \(i \leftrightarrow j\): la posición \(i\) atiende a la posición \(j\) exactamente igual que la posición \(j\) atiende a la posición \(i\).
Con \(W_Q, W_K\) separadas: $$ Q_i \cdot K_j = x_i^\top W_Q^\top W_K x_j $$ La matriz \(W_Q^\top W_K\) no es simétrica en general. Así que la atención \(i\)-a-\(j\) difiere de la \(j\)-a-\(i\).
Esto importa lingüísticamente: la relación entre un verbo y su sujeto es asimétrica — el verbo depende de la persona/número del sujeto (así que el query del verbo pregunta "¿quién es mi sujeto?"), pero el sujeto no depende de la forma del verbo. En he works, la posición 7 (works) atiende fuertemente a la posición 6 (he) — pero la posición 6 (he) no necesita atender a la posición 7 para ser ella misma. Attention tiene que modelar esa asimetría, y solo puede hacerlo con Q y K separadas.
Punto: sin Q y K separadas, attention sería simétrica. La asimetría del lenguaje (verbo→sujeto, modificador→modificado) la pide.
Argumento 2 — V desacopla contenido coincidente de información devuelta¶
Si usáramos \(V = K\) (es decir, los values son las keys), cada posición entregaría el mismo vector que anuncia. Esto está bien si el value que quieres es idéntico a tu característica de coincidencia — pero normalmente no lo es.
Ejemplo de gramática verbal: en he ___, el token he necesita ser emparejado por su rol gramatical ("soy un pronombre de 3ª singular"), pero el value que debería entregar es la señal de concordancia que impone al verbo ("necesitas el sufijo -s"). El criterio de coincidencia (una característica del pronombre) es diferente del contenido entregado (una instrucción al verbo).
\(K\) y \(V\) separadas proyectan la misma entrada \(x_6\) en los dos espacios diferentes:
- \(K_6 = x_6 W_K\) codifica "pronombre de 3ª singular, aquí estoy".
- \(V_6 = x_6 W_V\) codifica "si coincidiste conmigo, aplica -s a tu verbo".
El modelo aprende ambas proyecciones durante el entrenamiento. \(W_K\) aprende a poner los pronombres de 3ª singular en una región del espacio de keys donde los queries de verbos de 3ª singular puedan encontrarlos. \(W_V\) aprende a poner "-s" en el espacio de values que entregan.
Este desacoplamiento es lo que convierte a attention en una primitiva de enrutamiento de información en lugar de un agrupamiento basado en similitud: el modelo puede emparejar por una característica y entregar por otra completamente distinta.
Sin V separado: la capa de atención solo podría devolver información desde posiciones que ya se parecen a lo que el query está buscando. Inútil para el enrutamiento entre características.
Argumento 3 — Flexibilidad dimensional¶
\(Q\) y \(K\) deben tener la misma dimensión (necesitas hacer producto escalar). \(V\) puede tener una dimensión diferente \(d_v\). En multi-head (próximo archivo de teoría), tener \(d_v\) independiente de \(d_k\) es conveniente — puedes variar "cuán detallado es el cálculo de similitud" independientemente de "cuánta información entrega cada posición".
En la práctica, \(d_k = d_v\) es el default. Pero la superficie de la API permite la separación, y frameworks como nn.MultiheadAttention de PyTorch lo exponen.
Argumento 4 — Los patrones de atención aprendidos requieren un \(W_Q W_K^\top\) suficientemente expresivo¶
Aquí está la razón más profunda. Los pesos de atención son \(\text{softmax}(QK^\top / \sqrt{d_k})\). Antes del softmax, los scores son \(Q K^\top = X W_Q W_K^\top X^\top\). La matriz \(W_Q W_K^\top \in \mathbb{R}^{d \times d}\) — llamémosla \(M\) — es la forma bilineal que el modelo usa para puntuar pares \((x_i, x_j)\).
Si \(W_Q, W_K \in \mathbb{R}^{d \times d_k}\) con \(d_k = d\), entonces \(M\) tiene rango completo en general. El modelo puede expresar cualquier función de scoring bilineal. Con \(d_k < d\), \(M\) tiene rango-\(d_k\) — restringida, pero a menudo suficiente.
Si colapsaras \(W_Q\) y \(W_K\) en una única matriz \(W\), obtendrías \(M = W^\top W\), que es PSD (semidefinida positiva). Eso es una restricción importante: las matrices PSD tienen una estructura específica (autovalores positivos, simétricas). Muchos patrones de atención útiles requieren \(M\) no-PSD. \(W_Q, W_K\) separadas levantan esta restricción.
Este argumento es el más riguroso; para un aprendiz se puede agitar con la mano como "matrices separadas = scoring más expresivo".
Ejemplo trabajado con \(T = 2, d = 2, d_k = 2\)¶
Sea
(dos tokens; sus embeddings son vectores unitarios en direcciones diferentes — piensa la fila 0 como "pronombre sujeto" y la fila 1 como "raíz verbal").
Sea
Entonces
Scores: \(Q K^\top = \begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix}\). Nota que no es simétrica en general — aquí resulta serlo, por la elección de juguete de \(W_K\).
Sin escalar: \(\text{softmax}(\text{fila 0}) = \text{softmax}(0, 1) = (0.27, 0.73)\).
Fila 0 de la salida = \(0.27 \cdot V_0 + 0.73 \cdot V_1 = 0.27 \cdot (2, 0) + 0.73 \cdot (0, 3) = (0.54, 2.19)\).
Este es el cálculo completo. El Lab 00 pide a Borja hacer exactamente esto a mano sobre un ejemplo ligeramente más grande, y luego contrastar contra NumPy.
Ejercicio mental: nota cómo Q decide a quién mira la fila 0 (en este caso "el segundo token") y V decide qué le devuelven ("el segundo token tiene \(V = (0, 3)\)"). Si hubiéramos puesto \(W_V = I\), la fila 0 habría devuelto simplemente \(V_1 = (0, 1)\) — el mismo input. La separación V vs K es lo que permite que el output sea una transformación, no una copia.
Conexión con la escala del Mini-GPT¶
En el Mini-GPT de la Fase 17 fijaremos \(d_\text{model} = 64, n_\text{heads} = 4\). La convención por defecto pone \(d_k = d_v = d_\text{model} / n_\text{heads} = 16\) por cabeza. Cada cabeza por tanto posee tres matrices:
- \(W_Q \in \mathbb{R}^{64 \times 16}\)
- \(W_K \in \mathbb{R}^{64 \times 16}\)
- \(W_V \in \mathbb{R}^{64 \times 16}\)
Eso son \(3 \cdot 64 \cdot 16 = 3072\) parámetros por cabeza, \(\times 4\) cabezas \(= 12{,}288\) parámetros totales para las proyecciones Q/K/V de una capa de atención. Añade la proyección de salida \(W_O \in \mathbb{R}^{64 \times 64}\) (4096 params) y tienes \(\sim 16{,}384\) parámetros por capa de atención. El inventario de parámetros de la Fase 17 revisitará esto.
En la práctica las cuatro matrices por cabeza se concatenan y la proyección se hace en un matmul con un tensor \(W_{QKV} \in \mathbb{R}^{64 \times 192}\) — misma matemática, más rápido. Implementaremos la forma concatenada en la lab 01 de la Fase 15.
¿Y si realmente quisieras compartir matrices?¶
Una simplificación natural — "tied weights" — sí existe en algunas arquitecturas:
- \(W_Q = W_K\): convierte attention en un agrupamiento basado en similitud. Limita la expresividad (como mostró el Argumento 4). Usado en algunos papers de attention eficiente. No es estándar.
- \(W_K = W_V\): ata la característica de coincidencia a la información entregada. Usado en memory networks (pre-transformer). No estándar en transformers modernos.
- \(W_Q = W_K = W_V\): convierte attention en "media ponderada de \(X\) por \(X^\top X\)". Mayormente inútil — la misma matriz solo puede enrutar información a donde ya se parezca a la entrada.
El default en cada transformer moderno es tres matrices independientes. La Fase 15 sigue este default.
Dónde aterriza esto en la API¶
En src/minimodel/attention/BLUEPRINT.md, la superficie de la API para MultiHeadAttention poseerá tres matrices de pesos (\(W_Q, W_K, W_V\)) más una proyección de salida \(W_O\) (cubierta en 03-multi-head.md). La firma del constructor es:
class MultiHeadAttention:
def __init__(self, d_model: int, n_heads: int, seed: int) -> None:
# internally allocates W_Q, W_K, W_V (each d_model × d_model)
# and W_O (d_model × d_model)
Esta elección de API está bloqueada por la Fase 17 (Mini-GPT) — cambiarla cae en cascada sobre cada fase aguas abajo. Lee el blueprint cuidadosamente antes de implementar.
Lo que este archivo NO cubre¶
- El softmax y el factor de escalado \(\sqrt{d_k}\). Próximo archivo (
02-scaled-dot-product.md). - Por qué cabezas en lugar de una gran proyección.
03-multi-head.md. - Máscara causal (causal mask).
04-masking.md. - Escala de inicialización de \(W_Q, W_K, W_V\). Fase 18 (entrenamiento). Por ahora, los labs usan inicialización gaussiana pequeña (p. ej. \(\sigma = 0.02\)) para verificación solo forward.
- Términos de sesgo en las proyecciones. Los transformers modernos (estilo GPT-2) eliminan los biases en las proyecciones Q/K/V; seguimos esa convención. La justificación es número-de-parámetros / diferencia-insignificante, no derivada desde primeros principios.
Recapitulación¶
- Tres matrices existen por cuatro razones: asimetría, desacoplamiento contenido-vs-coincidencia, flexibilidad dimensional, scoring de rango completo.
- La más importante pedagógicamente es la #2: V separado significa que attention es enrutamiento de información, no agrupamiento basado en similitud. En
he ___, la coincidencia es por "pronombre de 3ª singular" pero la entrega es "aplica -s al verbo" — dos funciones diferentes de la misma entrada. - El ejemplo de juguete muestra el mecanismo en 6 números. El Lab 00 expande esto.
- La superficie de la API vive en
src/minimodel/attention/BLUEPRINT.md. La estructura de tres matrices está bloqueada.
Siguiente: 02-scaled-dot-product.md.