PASO 4 / 10 · EL CORAZÓN DEL TRANSFORMER

Self-Attention

Cómo cada palabra "mira" a las demás de la frase para entender su significado en contexto. Este es el paso más importante.

🎯 De un vistazo

Self-attention es el corazón del Transformer: permite que cada palabra se entienda en el contexto de las demás.

PROPÓSITO

¿Para qué sirve?

Que cada token "mire" a todos los demás de la frase y absorba información de los que son relevantes para entenderse a sí mismo.

APORTE

¿Qué aporta al modelo?

Contexto. Convierte tokens aislados en tokens contextualizados: "perro" pasa a ser "perro en esta frase específica, donde 'come' es relevante".

NECESIDAD

¿Por qué es indispensable?

Sin atención, cada token está aislado y no puede resolver ambigüedades ni captar relaciones entre palabras (¿a qué se refiere "tiene"? ¿quién "come"?). Es lo que da al modelo su capacidad de "entender".

↓ A continuación, el detalle con ejemplos y visualizaciones ↓

El problema que resuelve

Hasta ahora, en los pasos 1-3, cada token vive en su propio mundo: tiene un embedding (paso 2) más su info de posición (paso 3), pero no sabe nada de los otros tokens de la frase.

Eso es un problema serio. Considerá esta frase:

"El perro come carne porque tiene hambre."

Para entender "tiene", el modelo necesita saber que el sujeto es "perro" (no "carne" ni "hambre"). Pero "perro" está varias palabras atrás. ¿Cómo conecta esos dos tokens?

La idea de self-attention: para cada token, calcular cuánto debe "prestar atención" a cada uno de los demás tokens de la frase. Después, mezclar la información de los tokens relevantes en una nueva representación para ese token. Así, "tiene" puede absorber información de "perro" y entender su contexto.

La analogía clave: un buscador (tipo Google)

Para entender self-attention, pensá en un buscador web. Cuando hacés una búsqueda, hay 3 elementos en juego:

🔍 Query (Q)

Lo que estás buscando.

"recetas de pasta sin gluten"

Es la "pregunta" que hacés.

🏷️ Key (K)

Lo que cada documento "anuncia" como contenido.

Tags de cada página: "pasta", "italiana", "gluten-free", etc.

El "letrero" que el documento muestra al buscador.

💎 Value (V)

El contenido real que vas a recibir.

El texto, las imágenes, la receta completa.

Lo que efectivamente te da el buscador.

El buscador funciona así: compara tu Query con todas las Keys. Donde haya buen match, te devuelve los Values de esas páginas. Los Values de las páginas que no matchean los descarta.

En self-attention pasa exactamente lo mismo, pero con tokens:
  • Cada token genera una Query: "¿qué información necesito de los otros tokens?"
  • Cada token genera una Key: "¿qué tipo de información ofrezco?"
  • Cada token genera un Value: "si me elegís, esto es lo que aporto."
Y cada token mezcla los Values de los demás tokens, ponderados por qué tan bien matchea su Query con la Key de cada uno.

Lo confuso al principio: cada token tiene los TRES roles a la vez

A diferencia del buscador (donde vos sos el query y los documentos son keys/values), en self-attention cada token es simultáneamente query, key y value. Cuando "perro" hace su búsqueda (con su Query), está mirando las Keys de TODOS los tokens — incluida la suya propia. Y cuando otros tokens hacen su búsqueda, miran la Key de "perro" entre sus opciones.

Por eso se llama "self-attention": la frase se atiende a sí misma, sin necesidad de input externo.

📐 Repaso matemático: ¿qué es softmax?

Antes de meternos en self-attention, necesitamos entender softmax, una operación que vamos a usar varias veces en el algoritmo.

El problema que resuelve softmax

Imaginate que tenés una lista de "puntajes" o "scores" sin restricciones: por ejemplo [2.0, 5.0, 1.0]. Querés convertirlos en probabilidades — números que estén entre 0 y 1, y que sumen exactamente 1.

Naive intent: dividir cada número por la suma → [2/8, 5/8, 1/8] = [0.25, 0.625, 0.125]. Funciona pero tiene problemas con números negativos (¿cómo dividís por una suma negativa?) y no "amplifica" las diferencias.

Softmax resuelve esto usando la función exponencial:

softmax(xi) = exi / Σ exj

Es decir: aplicás e^x a cada elemento, después dividís cada uno por la suma de todos los exponenciales.

Ejemplo paso a paso

Vamos a aplicar softmax a [2.0, 5.0, 1.0]:

Paso 1: aplicar e^x a cada elemento
  e^2.0 ≈ 7.39
  e^5.0 ≈ 148.41
  e^1.0 ≈ 2.72

Paso 2: sumar todos
  total = 7.39 + 148.41 + 2.72 = 158.52

Paso 3: dividir cada uno por el total
  softmax(2.0) = 7.39 / 158.52   ≈ 0.047  (4.7%)
  softmax(5.0) = 148.41 / 158.52 ≈ 0.936  (93.6%)
  softmax(1.0) = 2.72 / 158.52   ≈ 0.017  (1.7%)

Verificación: 0.047 + 0.936 + 0.017 = 1.000 ✓
Lo importante de softmax:
  • Resultado entre 0 y 1, suman 1 — son probabilidades.
  • Amplifica las diferencias: el valor 5 (apenas 2.5× más grande que 2) terminó con 20× más probabilidad. Esto es porque la exponencial crece muy rápido.
  • El "ganador se lleva casi todo": en general, el valor más grande dominará la probabilidad resultante. Por eso se llama "soft-max" — es como un "max" suave (en lugar de elegir solo el más grande, le da casi toda la probabilidad).

🎮 Calculadora interactiva de softmax

Probá distintos valores y mirá cómo se distribuye la probabilidad:

El algoritmo de self-attention paso a paso

Ahora sí, el algoritmo completo. Recordá que venimos del paso 3 con los siguientes vectores (los "inputs"):

📥 Input desde el paso 3 ("el perro come" + positional encoding)

x_el    = [+0.21, +0.45, +0.08, +1.43]   (pos 0)
x_perro = [+0.50, +1.15, +0.78, +0.88]   (pos 1)
x_come  = [+1.36, -0.64, -0.28, +1.91]   (pos 2)

Son los mismos números que calculamos al final del paso 3. Cada vector tiene 4 dimensiones.

Vista general: 5 sub-pasos

  1. Generar Q, K, V para cada token (multiplicando por 3 matrices aprendidas).
  2. Calcular puntajes de atención: producto punto de Q con cada K.
  3. Escalar los puntajes dividiendo por √dk.
  4. Softmax para convertir los puntajes en probabilidades.
  5. Suma ponderada de los V: cada token toma una mezcla de los Values, ponderada por las probabilidades del paso 4.

Vamos uno por uno.

Sub-paso 1: generar Q, K, V para cada token

Cada token de la frase genera tres vectores distintos a partir de su input x:

Q = x · WQ
K = x · WK
V = x · WV

Donde WQ, WK, WV son matrices de pesos aprendidos (cada una de 4×4 en nuestro ejemplo). El modelo las aprende durante el entrenamiento. Son lo que le da "personalidad" a cada uno de los 3 roles.

¿Por qué 3 matrices distintas? Porque cada rol (Q, K, V) necesita "ver" el input de forma distinta. La Q de "perro" extrae las propiedades de "perro" que le sirven para hacer preguntas. Su K extrae las propiedades que sirven para responder preguntas de otros. Su V extrae el contenido que querría compartir. Son 3 perspectivas distintas del mismo token.

Las matrices WQ, WK, WV de nuestro ejemplo

Estos valores son inventados (en un modelo real se aprenden). Pero la mecánica es idéntica.

WQ (4×4)

0.3-0.10.40.2
0.10.5-0.20.3
-0.20.10.6-0.1
0.40.20.10.5

WK (4×4)

0.20.4-0.10.3
-0.30.20.50.1
0.1-0.20.30.4
0.50.10.2-0.2

WV (4×4)

0.40.10.3-0.2
0.20.3-0.10.4
-0.10.50.20.1
0.3-0.20.40.5

Mini-repaso: ¿cómo multiplicar un vector por una matriz?

Como vimos en el paso 2b, multiplicar un vector x por una matriz W se hace así:

  • El resultado es un vector con la misma cantidad de columnas que tiene W.
  • Cada elemento del resultado se calcula como el producto punto entre x y una columna de W.
  • Concretamente: resultado[j] = sumá sobre i de x[i] × W[i][j].

En nuestro caso, x tiene 4 elementos y W es 4×4, así que el resultado también tiene 4 elementos.

Calculando Q_perro paso a paso (con todos los detalles)

Vamos a calcular las 4 dimensiones de Q_perro, una por una. Recordá: Q_perro = x_perro · WQ.

x_perro = [+0.50, +1.15, +0.78, +0.88]

Q_perro[0] = producto punto entre x_perro y la COLUMNA 0 de W_Q:
             columna 0 de W_Q = [0.3, 0.1, -0.2, 0.4]
  = (0.50)(0.3) + (1.15)(0.1) + (0.78)(-0.2) + (0.88)(0.4)
  =     0.150  +     0.115  +    -0.156    +     0.352
  = +0.461

Q_perro[1] = producto punto entre x_perro y la COLUMNA 1 de W_Q:
             columna 1 de W_Q = [-0.1, 0.5, 0.1, 0.2]
  = (0.50)(-0.1) + (1.15)(0.5) + (0.78)(0.1) + (0.88)(0.2)
  =    -0.050   +     0.575  +     0.078  +     0.176
  = +0.779

Q_perro[2] = producto punto entre x_perro y la COLUMNA 2 de W_Q:
             columna 2 de W_Q = [0.4, -0.2, 0.6, 0.1]
  = (0.50)(0.4) + (1.15)(-0.2) + (0.78)(0.6) + (0.88)(0.1)
  =     0.200  +    -0.230   +     0.468  +     0.088
  = +0.526

Q_perro[3] = producto punto entre x_perro y la COLUMNA 3 de W_Q:
             columna 3 de W_Q = [0.2, 0.3, -0.1, 0.5]
  = (0.50)(0.2) + (1.15)(0.3) + (0.78)(-0.1) + (0.88)(0.5)
  =     0.100  +     0.345  +    -0.078    +     0.440
  = +0.807

Vector final:
Q_perro = [+0.461, +0.779, +0.526, +0.807]

Repetimos exactamente lo mismo cambiando WQ por WK para obtener K_perro, y por WV para V_perro. Y después hacemos los 3 cálculos para "el" y para "come". Total: 9 multiplicaciones de vector por matriz.

Lo que significa esto matemáticamente: cada matriz W es una "lente" que proyecta el vector input en un nuevo espacio. WQ lo proyecta al espacio de "queries", WK al de "keys", WV al de "values". Las 3 lentes ven el mismo input pero le sacan distintas características. Lo que se aprende durante el entrenamiento es QUÉ proyecciones son útiles.

Tabla completa de Q, K, V para "el perro come"

Repitiendo el proceso para cada token y cada matriz, obtenemos 9 vectores en total (3 tokens × 3 roles):

Sub-paso 2: calcular puntajes de atención

Ahora vamos a calcular cuánta "atención" le presta cada token a cada uno (incluido a sí mismo). Para eso, hacemos el producto punto entre cada Query y cada Key.

Recordá la intuición del producto punto: mide qué tan "alineados" están dos vectores. Si la Query de "perro" y la Key de "come" están bien alineadas (producto punto alto), significa que el rol que pregunta "perro" matchea bien con el rol que ofrece "come". Por lo tanto, "perro" le va a prestar mucha atención a "come".

La operación matemática en este sub-paso

Cada Query Q_i hace producto punto con CADA Key K_j. Como tenemos 3 tokens, tenemos 3 queries y 3 keys → 9 productos punto en total. Esto se organiza en una matriz de 3×3 que llamamos matriz de scores.

Si lo escribimos como matriz: S = Q · KT (donde KT es K traspuesta — convertimos filas en columnas y viceversa). Esto nos da las 9 combinaciones de un saque.

Calculando el score "perro → come" paso a paso

Primero, los dos vectores que vamos a usar (el Q se calculó en el sub-paso 1; el K_come se calcula igual pero con WK):

Q_perro = [+0.461, +0.779, +0.526, +0.807]    (lo que "perro" busca)
K_come  = [+1.391, +0.663, -0.158, -0.150]    (lo que "come" ofrece)

score("perro" → "come") = producto punto entre los dos vectores:
  = (+0.461)(+1.391) + (+0.779)(+0.663) + (+0.526)(-0.158) + (+0.807)(-0.150)
  =      +0.641     +      +0.517     +      -0.083    +      -0.121
  = +0.954

Interpretación: este es el "match" crudo entre la pregunta de "perro" y la
oferta de "come". Cuanto más alto, más relevante.

Lo mismo para los otros 8 scores

Aplicando la misma receta a las 9 combinaciones, obtenemos:

Desde "perro":
  score(perro → el)    = Q_perro · K_el    = +0.677
  score(perro → perro) = Q_perro · K_perro = +1.223
  score(perro → come)  = Q_perro · K_come  = +0.954

Lectura: la fila de "perro" muestra que su mejor match es consigo mismo (+1.223),
después con "come" (+0.954), y por último con "el" (+0.677).
Pero ojo: los números crudos NO son porcentajes todavía — falta escalar y softmax.

Matriz completa de scores

Cada fila es una Query (quién pregunta); cada columna es una Key (a quién mira).

Sub-paso 3: escalar los puntajes

Antes de aplicar softmax, dividimos todos los scores por √dk, donde dk es la dimensión de las Keys (en nuestro caso, 4).

score_escalado = score / √dk   =   score / √4   =   score / 2

¿Por qué dividir por √dk?

Es un truco práctico, no profundo. Cuando los vectores tienen muchas dimensiones, el producto punto tiende a generar números muy grandes (positivos o negativos). Si esos números entran al softmax así, la exponencial los amplifica tanto que todo se concentra en un solo token — el modelo se vuelve "rígido" y deja de aprender bien.

Dividir por √dk mantiene los scores en un rango razonable antes del softmax, lo cual produce distribuciones más "suaves" y entrenamiento más estable. Es un truco descubierto empíricamente por los autores del paper.

Ejemplo concreto del escalado

Vamos a ver cómo cambian los scores de la fila "perro" después de dividir cada uno por √4 = 2:

Antes (sin escalar):
  score(perro → el)    = +0.677
  score(perro → perro) = +1.223
  score(perro → come)  = +0.954

Después (dividir por √4 = 2):
  score_escalado(perro → el)    = 0.677 / 2 = +0.339
  score_escalado(perro → perro) = 1.223 / 2 = +0.611
  score_escalado(perro → come)  = 0.954 / 2 = +0.477

¿Por qué raíz cuadrada y no simplemente dk?

Buena pregunta. Vamos a entenderlo de a poco, sin fórmulas raras.

Paso 1 · El producto punto es una SUMA de muchos números

Recordá que el score es un producto punto: Q·K = q₀·k₀ + q₁·k₁ + ... . Es decir, sumamos dk términos (uno por cada dimensión). Si dk = 64, estamos sumando 64 numeritos.

Paso 2 · Esos números son aleatorios: unos suman, otros restan

Como Q y K arrancan con valores variados (positivos y negativos), los términos que sumamos también lo son. Algunos son positivos, otros negativos. Se cancelan entre sí, en parte.

Por eso la suma NO crece tan rápido como uno pensaría. Compará:

Si los 64 términos fueran TODOS +1:  suma = 64  (crece "rápido", lineal)
Pero como son aleatorios (+,-,+,-): se cancelan, la suma queda mucho menor

Paso 3 · El "paseo del borracho" (la clave)

Imaginá una persona que da pasos al azar: uno a la izquierda, otro a la derecha, sin rumbo. Después de n pasos, ¿qué tan lejos del punto de partida está?

No está a n pasos de distancia — está a ~√n pasos. Porque al ir y venir al azar, se cancela gran parte del avance. Esto es un hecho matemático famoso (el "random walk" o paseo aleatorio).

Nuestro producto punto Q·K es exactamente eso: una suma de términos que van y vienen al azar. Por eso su tamaño típico crece como √dk, no como dk.

Paso 4 · La demostración con números reales

Generamos vectores Q y K al azar de distintos tamaños y medimos la "magnitud típica" (desviación) de su producto punto. Mirá cómo coincide con √dk:

dk magnitud típica de Q·K √dk tras dividir por √dk
42.002.001.00
164.014.001.00
647.968.001.00
25616.0616.001.00
102431.9432.001.00

La columna del medio (magnitud de Q·K) es casi idéntica a √dk. Y al dividir por √dk, siempre queda en 1.00, sin importar el tamaño. Eso es lo que buscábamos: una escala neutra y constante.

Paso 5 · ¿Y si eligiéramos mal el divisor?

Divisor Qué pasa con los scores Resultado en el softmax
Nada (sin escalar) Quedan enormes (±8, ±20...) Softmax se satura: toda la atención a un token, los demás 0%. Casi no hay gradiente → no aprende.
÷ dk (de más) Quedan diminutos (±0.12...) Softmax queda plano: atención casi igual para todos → no distingue nada.
÷ √dk (justo) Quedan en escala ~1 Softmax en su zona ideal: distingue bien y entrena estable. ✅
En una frase: el producto punto crece como √dk (por el "paseo aleatorio" de sumar términos que se cancelan). Dividir por √dk cancela ese crecimiento y deja los scores en una escala constante (~1), sin importar cuántas dimensiones tengan los vectores. Así el softmax siempre funciona bien, ya sea con dk=4 o dk=12288.

Sub-paso 4: aplicar softmax a cada fila

Ahora aplicamos softmax a cada fila de la matriz de scores escalados. Cada fila se vuelve una distribución de probabilidad (suma 1) que indica cuánta atención presta ese token a cada uno de los demás.

¿Por qué softmax y no otra cosa?

Tenemos scores como [0.339, 0.611, 0.477] que no son probabilidades — pueden ser negativos, no suman 1. Necesitamos transformarlos en porcentajes de atención. Softmax es ideal porque:

  • Convierte cualquier número en uno positivo (gracias a la exponencial).
  • Hace que todos sumen 1 (porque dividimos por la suma total).
  • Amplifica las diferencias: scores altos se llevan la mayoría de la probabilidad.
  • Es diferenciable: indispensable para que el modelo pueda entrenarse con gradient descent.

Calculando softmax para la fila de "perro" paso a paso

Recordá: softmax(xi) = exi / Σ exj. Vamos con los 3 valores que sacamos del sub-paso 3:

Scores escalados de "perro": [+0.339, +0.611, +0.477]
(estos son los matches "perro→el", "perro→perro", "perro→come")

Paso 1: aplicar e^x a cada score:
  e^0.339 ≈ 1.404
  e^0.611 ≈ 1.842
  e^0.477 ≈ 1.611

Paso 2: sumar todos los exponenciales:
  total = 1.404 + 1.842 + 1.611 = 4.857

Paso 3: dividir cada uno por el total:
  softmax(0.339) = 1.404 / 4.857 ≈ 0.289  (28.9%)  ← atención a "el"
  softmax(0.611) = 1.842 / 4.857 ≈ 0.379  (37.9%)  ← atención a "perro"
  softmax(0.477) = 1.611 / 4.857 ≈ 0.332  (33.2%)  ← atención a "come"

Verificación: 0.289 + 0.379 + 0.332 = 1.000 ✓

Lectura: "perro" reparte su atención casi por igual entre los 3 tokens,
con leve preferencia por sí mismo (38%), después "come" (33%), y por último "el" (29%).

Comparación: ¿qué hubiera pasado SIN escalar?

Mirá cómo cambian las atenciones si no hubiéramos dividido por √dk:

SIN escalar (scores originales [+0.677, +1.223, +0.954]):
  softmax → [0.222, 0.382, 0.396]
  → atenciones: 22.2% / 38.2% / 39.6%   (más concentradas)

CON escalar (scores [+0.339, +0.611, +0.477]):
  softmax → [0.289, 0.379, 0.332]
  → atenciones: 28.9% / 37.9% / 33.2%   (más equilibradas)

Con scores chicos (como los nuestros), la diferencia es leve. Pero con vectores grandes (d=512), sin escalar el softmax pondría casi 100% en un solo token — el modelo no podría aprender a balancear su atención.

La matriz de atención completa (softmax aplicado a las 3 filas)

Esta matriz es lo más importante de self-attention. Si miramos la fila de "perro", vemos qué porcentaje de su atención va a cada token de la frase. La suma de cada fila es siempre 1 (100%).

Sub-paso 5: suma ponderada de los Values

El paso final. Para cada token, calculamos su nuevo vector de salida como una combinación ponderada de los Values de todos los tokens, usando los pesos de softmax como ponderaciones.

output_perro = αperro→el · V_el + αperro→perro · V_perro + αperro→come · V_come

Donde αperro→X es el peso de atención que "perro" le da al token X (sacado del softmax del paso anterior).

Suena técnico, pero la idea es la de hacer una mezcla. Vamos despacio.

Paso 1 · La idea con algo cotidiano: un licuado 🥤

Imaginá que hacés un licuado eligiendo proporciones:

  50% banana  +  30% frutilla  +  20% mango
  └─────────────── suman 100% ───────────────┘

El licuado final es una mezcla: tiene gusto principalmente a banana (porque puse más), algo de frutilla, y un toque de mango. Cuanto mayor la proporción, más domina ese ingrediente.

Eso es una "suma ponderada": cada cosa entra en la mezcla según su peso (su proporción). En self-attention:

  • Los porcentajes de atención son las proporciones (50%, 30%, 20%...). Vienen del softmax y suman 100%.
  • Los Values son los ingredientes (la info que aporta cada token).
  • El output es el licuado final: la mezcla de los Values según cuánta atención les dio.

Paso 2 · Con números simples (un solo número por token)

Antes de usar vectores, hagámoslo con un número por token, para ver la mecánica. Supongamos que cada token aporta UN número (su "Value" simplificado):

Values (un número por token):
  V_el    = 10
  V_perro = 20
  V_come  = 30

Pesos de atención de "perro" (las proporciones, suman 1):
  a "el":    30%  (0.30)
  a "perro": 40%  (0.40)
  a "come":  30%  (0.30)

Suma ponderada = cada Value × su peso, todo sumado:
  = (0.30 × 10) + (0.40 × 20) + (0.30 × 30)
  = 3 + 8 + 9
  = 20

El resultado (20) es una mezcla de 10, 20 y 30, "tirada" hacia los que tienen más peso. No es ni el más chico ni el más grande: es un promedio donde cada uno pesa según su atención.

Como los pesos suman 1 (100%), el resultado siempre queda "dentro" del rango de los Values — es una mezcla equilibrada, no una suma que explota. Por eso el softmax (que garantiza que sumen 1) es tan importante.

Paso 3 · ¿Qué significa "suma ponderada de vectores"?

Es una operación simple: tomás varios vectores, multiplicás cada uno por un "peso" (un número), y sumás los resultados. Tipo:

Si tenés tres vectores y tres pesos:

  vector_A = [1, 2, 3]    peso_A = 0.5
  vector_B = [4, 5, 6]    peso_B = 0.3
  vector_C = [7, 8, 9]    peso_C = 0.2

Resultado = 0.5 × [1,2,3] + 0.3 × [4,5,6] + 0.2 × [7,8,9]
          = [0.5, 1.0, 1.5] + [1.2, 1.5, 1.8] + [1.4, 1.6, 1.8]
          = [3.1, 4.1, 5.1]

→ El resultado es un vector "mezcla" de los 3 originales,
  pesado más por A (0.5) que por C (0.2).

En self-attention los pesos son los porcentajes de atención (que suman 1), y los vectores son los Values.

Calculando output_perro paso a paso (las 4 dimensiones)

Primero recopilamos los pesos (del sub-paso 4) y los V (del sub-paso 1):

Pesos de atención que "perro" reparte:
  α(perro → el)    = 0.289  (28.9%)
  α(perro → perro) = 0.379  (37.9%)
  α(perro → come)  = 0.332  (33.2%)

Vectores Value de cada token:
  V_el    = [+0.595, -0.090, +0.606, +0.861]
  V_perro = [+0.616, +0.609, +0.543, +0.878]
  V_come  = [+1.017, -0.578, +1.180, +0.399]

Ahora calculamos cada dimensión del output por separado:

output_perro[0] = 0.289 × (+0.595) + 0.379 × (+0.616) + 0.332 × (+1.017)
                =      +0.172   +     +0.233   +     +0.338
                = +0.743

output_perro[1] = 0.289 × (-0.090) + 0.379 × (+0.609) + 0.332 × (-0.578)
                =      -0.026   +     +0.231   +     -0.192
                = +0.013

output_perro[2] = 0.289 × (+0.606) + 0.379 × (+0.543) + 0.332 × (+1.180)
                =      +0.175   +     +0.206   +     +0.392
                = +0.773

output_perro[3] = 0.289 × (+0.861) + 0.379 × (+0.878) + 0.332 × (+0.399)
                =      +0.249   +     +0.333   +     +0.132
                = +0.714

Vector de salida final para "perro":
output_perro = [+0.743, +0.013, +0.773, +0.714]

Comparando "perro" antes y después de self-attention

Acá se ve la transformación clave del paso 4:

ANTES (input del paso 3): "perro" aislado, solo con su embedding + PE
  x_perro = [+0.50, +1.15, +0.78, +0.88]

DESPUÉS de self-attention: "perro" contextualizado con info de toda la frase
  output_perro = [+0.743, +0.013, +0.773, +0.714]

El vector cambió. Ya no representa solo a "perro" en abstracto, sino
a "perro en el contexto de 'el perro come' (donde 'come' es relevante)".

Esto es lo que self-attention agrega al modelo: contexto. Cada token sale "enriquecido" con información de los tokens relevantes de la frase.

Lo mismo para los otros 2 tokens (calculado por el demo)

Resultado final: tenemos 3 vectores nuevos (uno por token), cada uno conteniendo no solo información del token original sino también de los otros tokens, ponderada por relevancia. "perro" ahora sabe qué tan importante es "come" para entenderse, y absorbió un poquito de su Value.

🎮 Demo interactivo: el algoritmo completo

Hacé click en "Siguiente paso" para ver cada fase del algoritmo, una por una, con los números reales calculados en JavaScript:

0. Inputs
1. Generar Q, K, V
2. Scores (Q·K)
3. Escalar
4. Softmax
5. Salida (·V)
Listo para comenzar
Apretá "Siguiente paso" para arrancar.

¿Qué aprende exactamente el modelo?

Pregunta importante: si las matrices WQ, WK, WV son aprendidas, ¿qué es lo que efectivamente "se aprende" durante el entrenamiento?

El modelo aprende qué transformaciones aplicar al input para que el matching Query-Key produzca buenas atenciones. Por ejemplo:

Lo que NO está aprendido:
  • El algoritmo de self-attention en sí (es fijo: Q·K, softmax, ·V).
  • La estructura de bloques del Transformer (también fija).
Lo que SÍ está aprendido:
  • Los valores numéricos de las matrices WQ, WK, WV (y otras que veremos en pasos siguientes).
  • Los valores de la matriz de embeddings.

✅ Resumen: lo que entra al siguiente paso

  1. Empezamos con los inputs del paso 3: 3 vectores (uno por token de "el perro come").
  2. Cada vector se transformó en Q, K, V usando matrices aprendidas.
  3. Calculamos puntajes de atención entre todos los pares (Q · K).
  4. Escalamos por √dk y aplicamos softmax → matriz de probabilidades.
  5. Cada vector de salida = suma ponderada de los V usando las probabilidades.

Al final de self-attention, tenemos 3 vectores nuevos, pero ahora cada uno contiene información de TODOS los tokens, ponderada por relevancia. "perro" ya no es solo "perro" — es "perro en este contexto específico, atendiendo a 'el' y 'come' según corresponda."

En el Paso 5: Multi-Head Attention vamos a ver cómo en lugar de hacer self-attention una sola vez, el Transformer la hace varias veces en paralelo con matrices distintas, permitiendo capturar múltiples "tipos" de relaciones simultáneamente.