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.
Self-attention es el corazón del Transformer: permite que cada palabra se entienda en el contexto de las demás.
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.
Contexto. Convierte tokens aislados en tokens contextualizados: "perro" pasa a ser "perro en esta frase específica, donde 'come' es relevante".
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 ↓
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:
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?
Para entender self-attention, pensá en un buscador web. Cuando hacés una búsqueda, hay 3 elementos en juego:
Lo que estás buscando.
"recetas de pasta sin gluten"
Es la "pregunta" que hacés.
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.
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.
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.
Antes de meternos en self-attention, necesitamos entender softmax, una operación que vamos a usar varias veces en el algoritmo.
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:
Es decir: aplicás e^x a cada elemento, después dividís cada uno por la suma de todos los exponenciales.
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 ✓
Probá distintos valores y mirá cómo se distribuye la probabilidad:
Ahora sí, el algoritmo completo. Recordá que venimos del paso 3 con los siguientes vectores (los "inputs"):
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.
Vamos uno por uno.
Cada token de la frase genera tres vectores distintos a partir de su input x:
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.
Estos valores son inventados (en un modelo real se aprenden). Pero la mecánica es idéntica.
| 0.3 | -0.1 | 0.4 | 0.2 |
| 0.1 | 0.5 | -0.2 | 0.3 |
| -0.2 | 0.1 | 0.6 | -0.1 |
| 0.4 | 0.2 | 0.1 | 0.5 |
| 0.2 | 0.4 | -0.1 | 0.3 |
| -0.3 | 0.2 | 0.5 | 0.1 |
| 0.1 | -0.2 | 0.3 | 0.4 |
| 0.5 | 0.1 | 0.2 | -0.2 |
| 0.4 | 0.1 | 0.3 | -0.2 |
| 0.2 | 0.3 | -0.1 | 0.4 |
| -0.1 | 0.5 | 0.2 | 0.1 |
| 0.3 | -0.2 | 0.4 | 0.5 |
Como vimos en el paso 2b, multiplicar un vector x por una matriz W se hace así:
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.
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.
Repitiendo el proceso para cada token y cada matriz, obtenemos 9 vectores en total (3 tokens × 3 roles):
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.
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.
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.
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.
Cada fila es una Query (quién pregunta); cada columna es una Key (a quién mira).
Antes de aplicar softmax, dividimos todos los scores por √dk, donde dk es la dimensión de las Keys (en nuestro caso, 4).
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.
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
Buena pregunta. Vamos a entenderlo de a poco, sin fórmulas raras.
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.
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
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).
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 |
|---|---|---|---|
| 4 | 2.00 | 2.00 | 1.00 |
| 16 | 4.01 | 4.00 | 1.00 |
| 64 | 7.96 | 8.00 | 1.00 |
| 256 | 16.06 | 16.00 | 1.00 |
| 1024 | 31.94 | 32.00 | 1.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.
| 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. ✅ |
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.
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:
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%).
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.
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.
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.
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:
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.
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.
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]
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.
Hacé click en "Siguiente paso" para ver cada fase del algoritmo, una por una, con los números reales calculados en JavaScript:
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:
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.