Atención de múltiples cabezas y codificación posicional: Dándole al Transformer un sentido de dirección
En la Parte 7, Lina vio cómo la autoatención permite a un modelo ponderar la importancia de diferentes palabras — un foco en las partes más relevantes de una oración. El inconveniente: si desordenas esas palabras, la atención básica no se da cuenta. Para una computadora, “el cargo superó al reembolso” y “el reembolso superó al cargo” parecen idénticas si solo nos fijamos en las palabras en sí.
Hoy solucionaremos eso. Le daremos a nuestro modelo un “GPS” para que sepa dónde se ubica cada palabra, y le daremos múltiples “ojos” para que pueda rastrear diferentes patrones a la vez.
El problema: Un modelo sin noción del tiempo
Piense en la autoatención básica como un modelo de “bag-of-words”. Trata una oración como un montón de ropa en el suelo en lugar de una fila ordenada de prendas. Como la matemática detrás de la atención suma vectores ponderados, el orden de esos vectores no cambia el resultado. En términos matemáticos, esto se llama invariancia bajo permutaciones.
¿Qué pasa cuando intercambiamos el orden de las palabras en un cálculo sencillo de atención?
import torch
import torch.nn.functional as F
def simple_attention_check(words):
# Imagine these are 3-dimensional word embeddings
# Word A, Word B, Word C
embeddings = torch.tensor([
[1.0, 0.0, 0.0], # Word 1
[0.0, 1.0, 0.0], # Word 2
[0.0, 0.0, 1.0] # Word 3
])
# If we shuffle the order, does the output change?
# We'll just look at the raw dot-product sum for simplicity
scores = torch.matmul(embeddings, embeddings.T)
probs = F.softmax(scores, dim=-1)
output = torch.matmul(probs, embeddings)
return output
sentence_1 = [0, 1, 2] # Word 1, 2, 3
sentence_2 = [2, 1, 0] # Word 3, 2, 1
print("Output 1:\n", simple_attention_check(sentence_1))
print("Output 2:\n", simple_attention_check(sentence_2))
import torch/import torch.nn.functional as F— Importa PyTorch y su API funcional (que proporcionasoftmaxy otras operaciones matemáticas sin crear objetos de módulo).embeddings = torch.tensor([...])— Crea un tensor de 3×3 donde cada fila es un vector one-hot que actúa como un “word embedding” sustituto. La fila 0 representa la Palabra 1, la fila 1 representa la Palabra 2, etc.torch.matmul(embeddings, embeddings.T)— Calcula la matriz completa de 3×3 de productos punto por pares. Cada entrada[i, j]es la similitud entre la palabra i y la palabra j.F.softmax(scores, dim=-1)— Aplica softmax a lo largo de la última dimensión (a través de las columnas), convirtiendo cada fila de puntajes sin procesar en una distribución de probabilidad que suma 1.torch.matmul(probs, embeddings)— Multiplica la matriz de probabilidad por los embeddings, produciendo un promedio ponderado de los embeddings para cada palabra.sentence_1ysentence_2se pasan como argumentos pero el cuerpo de la función los ignora — el tensorembeddingsestá codificado de forma fija. Este es el punto central: no importa en qué orden digas que están las palabras, la función produce el mismo conjunto de vectores de salida (solo en filas diferentes), demostrando que la atención por sí sola es ciega al orden.
Los valores de salida siguen siendo los mismos — simplemente terminan en filas diferentes. El modelo no tiene una noción interna de que la “Palabra 1” vino primero; solo sabe que la “Palabra 1” está presente. Para un Transformer, “Charged, not refunded” y “Refunded, not charged” producen la misma representación interna. Necesitamos incorporar la “posición” en los datos sin romper la matemática.
Codificación posicional: El ‘GPS’ para tus palabras
Necesitamos decirle al modelo qué palabra se encuentra en el índice 0 y cuál en el índice 10. Pero no podemos simplemente añadir los números 0, 1, 2… a los embeddings. Si una oración llega a 1,000 palabras de longitud, el número 1,000 empequeñece nuestros valores de embedding. Esos valores suelen oscilar entre -1 y 1. La señal de posición ahogaría el significado real de la palabra.
Por eso recurrimos a la codificación posicional. Piensa en ello como una marca de tiempo o una coordenada GPS. Usamos ondas seno y coseno a diferentes frecuencias para estampar cada posición con una firma única.
Imagina un reloj con muchas manecillas. Una marca cada segundo, otra cada minuto, otra cada hora. Lee todas las manecillas a la vez y sabrás exactamente qué hora es. El Transformer hace algo similar con ondas.
import numpy as np
import matplotlib.pyplot as plt
def get_positional_encoding(max_seq_len, d_model):
# Create a matrix of zeros
pe = np.zeros((max_seq_len, d_model))
for pos in range(max_seq_len):
for i in range(0, d_model, 2):
# Use sine for even indices, cosine for odd
pe[pos, i] = np.sin(pos / (10000 ** (i / d_model)))
pe[pos, i + 1] = np.cos(pos / (10000 ** (i / d_model)))
return pe
# Let's visualize a small encoding for a 50-word sentence
pe_matrix = get_positional_encoding(50, 128)
plt.imshow(pe_matrix, cmap='RdBu')
plt.xlabel("Embedding Dimension")
plt.ylabel("Word Position")
plt.title("Positional Encoding 'GPS' Map")
plt.show()
pe = np.zeros((max_seq_len, d_model))— Inicializa una matriz donde cada fila es una posición (de 0 amax_seq_len - 1) y cada columna es una dimensión del vector de codificación posicional.for pos in range(max_seq_len)— Itera sobre cada posición en la secuencia.for i in range(0, d_model, 2)— Recorre las dimensiones del embedding de dos en dos (porque cada par obtiene un seno y un coseno).np.sin(pos / (10000 ** (i / d_model)))— Calcula el seno para una dimensión de índice par. El divisor10000 ** (i / d_model)controla la frecuencia: los índices más bajos producen ondas de alta frecuencia (posiciones que cambian rápidamente), los índices más altos producen ondas de baja frecuencia (posiciones que cambian lentamente). Esto imita un reloj con manecillas de distintas velocidades.np.cos(pos / (10000 ** (i / d_model)))— Calcula el coseno para la siguiente dimensión de índice impar, emparejado con el seno anterior.plt.imshow(pe_matrix, cmap='RdBu')— Representa la matriz de codificación como un mapa de calor donde el rojo y el azul muestran valores positivos y negativos, revelando visualmente los patrones de ondas.
Cada fila en ese mapa de calor es un “código de barras” único. Agrega ese código de barras a nuestros embeddings de palabras y el modelo puede “sentir” dónde se ubica en la oración. Dado que el seno y el coseno son periódicos, el modelo también capta las posiciones relativas. Puede deducir que la “Palabra A” siempre cae 3 lugares detrás de la “Palabra B”.
Fórmulas de codificación posicional
Para una palabra en la posición de la secuencia, la codificación posicional para la dimensión de embedding (par) y (impar) es:
| Lenguaje natural | Símbolo estadístico | Equivalente en Python |
|---|---|---|
| Posición de la palabra en la secuencia (0, 1, 2, …) | pos (variable de iteración) | |
| Índice de la dimensión de embedding (0, 1, 2, …) | i (variable de iteración) | |
| Tamaño total del vector de embedding | d_model | |
| Denominador de frecuencia (controla la velocidad de la onda) | 10000 ** (i / d_model) | |
| Codificación en dimensión par | pe[pos, i] = np.sin(...) | |
| Codificación en dimensión impar | pe[pos, i + 1] = np.cos(...) |
La constante es un hiperparámetro elegido de modo que las ondas de mayor frecuencia ciclen lo suficientemente rápido como para distinguir posiciones adyacentes, mientras que las ondas de menor frecuencia cambian lo suficientemente lento como para seguir siendo significativas a lo largo de secuencias largas. Cada dimensión actúa como una “manecilla” diferente en la analogía del reloj de múltiples manecillas del texto.
Por qué una sola cabeza no basta: El problema de ‘Los ciegos y el elefante’
Ahora que el modelo sabe dónde están las palabras, necesita entender qué están haciendo. Una palabra puede tener varios roles en una sola oración. Tomemos “The battery charge is low, so I called support about the billing charge” — la palabra “charge” tiene dos significados diferentes.
Un único head de atención es como una persona mirando un elefante en la oscuridad. Uno toca la trompa y dice “¡Es una serpiente!” Otro toca una pata y dice “¡Es un árbol!”
Multi-Head Attention es como tener un equipo de expertos. Un head podría enfocarse en gramática (concordancia sujeto-verbo). Otro podría rastrear patrones de rima. Un tercero podría captar relaciones fácticas.
Aquí está la parte complicada: en realidad no ejecutamos 8 modelos separados. Tomamos nuestro gran vector de embedding (digamos, tamaño 512) y lo dividimos en 8 fragmentos de 64 cada uno. Cada fragmento va a un “head” diferente.
# Conceptual split of a vector into 4 heads
d_model = 12 # Total size of our word vector
num_heads = 4
head_dim = d_model // num_heads
# A single word vector
word_vector = torch.randn(1, d_model)
# Split into 4 'expert' views
heads = word_vector.view(num_heads, head_dim)
print(f"Original vector size: {word_vector.shape}")
print(f"Split into {num_heads} heads, each with size: {heads.shape[1]}")
# Output: Split into 4 heads, each with size: 3
d_model = 12— La dimensión total de embedding. En los Transformers reales esto es típicamente 512 o 768; aquí se mantiene pequeño para legibilidad.num_heads = 4— El número de heads de atención. Cada head operará sobre una porción del embedding.head_dim = d_model // num_heads— División entera: cada head obtiene12 // 4 = 3dimensiones. Esto debe dividirse de manera exacta.torch.randn(1, d_model)— Genera un vector aleatorio 1×12 que representa el embedding de una sola palabra.word_vector.view(num_heads, head_dim)— Redimensiona el vector 1×12 en una matriz 4×3, donde cada fila es la “vista” de la palabra para un head. El método.view()reinterpreta la misma memoria sin copiar datos.heads.shape[1]— Devuelve la segunda dimensión del tensor redimensionado (3), confirmando que cada head ve 3 características.
Lo que esto significa en la práctica: el modelo analiza los datos en paralelo. En lugar de un cálculo grande y desordenado, obtienes 4 (u 8, o 16) cálculos enfocados ejecutándose al mismo tiempo.
Atención de una sola cabeza vs. Atención multicabeza
| Enfoque | Qué hace | Ideal para | Compromiso |
|---|---|---|---|
| Atención de una sola cabeza | Ejecuta un conjunto de proyecciones Q, K, V sobre el vector d_model completo. Cada dimensión del embedding participa en un cálculo de atención unificado. | Relaciones simples; modelos de juguete; depuración; entornos de bajo cómputo. | Solo puede aprender un patrón de relevancia a la vez. Si la oración requiere desambiguación gramatical y semántica simultánea (p. ej., “cargo” como sujeto y como término financiero), una sola cabeza debe comprimir ambos en la misma suma ponderada; no puede especializarse. |
| Atención multicabeza | Divide d_model en num_heads fragmentos, ejecuta atención independiente por fragmento y luego concatena y vuelve a proyectar. Cada cabeza tiene la libertad de especializarse en una relación diferente. | Lenguaje del mundo real; cualquier tarea donde múltiples patrones simultáneos importen (sintaxis, semántica, correferencia, rima, etc.). | Cada cabeza ve solo head_dim = d_model / num_heads características, por lo que la capacidad por cabeza es menor. Más cabezas significa más parámetros (la proyección de salida) y más cómputo. Demasiadas cabezas con un head_dim demasiado pequeño pueden tener un rendimiento inferior: la cabeza no tiene suficientes dimensiones para aprender un patrón significativo. |
En qué aprenden a especializarse las cabezas (ejemplos):
- Cabeza 1 — Sintaxis: Se enfoca en la concordancia entre sujeto y verbo (“cargo” ↔ “excedió”).
- Cabeza 2 — Semántica: Se enfoca en el significado de las palabras (“cargo” ↔ “facturación”, desambiguando lo financiero frente a lo eléctrico).
- Cabeza 3 — Correferencia: Se enfoca en la resolución de pronombres (“eso” ↔ el sustantivo más reciente).
- Cabeza 4 — Patrones posicionales: Se enfoca en tokens adyacentes o cercanos, aprovechando la codificación posicional para encontrar contexto local.
Regla general: Lina recurriría a la atención multicabeza siempre que la misma palabra participe en múltiples relaciones simultáneas que no puedan ser capturadas por una sola suma ponderada. El costo es más parámetros y la restricción de que d_model debe ser divisible por num_heads.
El truco de la concatenación: Reuniendo a los expertos
Una vez que cada cabeza ha hecho su trabajo y elegido qué palabras importan, nos quedamos con 8 respuestas diferentes. La siguiente capa de la red espera un único vector por palabra.
Resolvemos esto con el Truco de Concatenación. Pegamos las salidas de las cabezas nuevamente, una al lado de la otra. Luego ese vector largo pasa por una capa lineal final — una matriz de pesos — permitiendo que las cabezas mezclen sus hallazgos en una representación coherente.
class MultiHeadAttentionSimple(torch.nn.Module):
def __init__(self, d_model, num_heads):
super().__init__()
self.num_heads = num_heads
self.head_dim = d_model // num_heads
# Linear layers for Q, K, V
self.q_linear = torch.nn.Linear(d_model, d_model)
self.k_linear = torch.nn.Linear(d_model, d_model)
self.v_linear = torch.nn.Linear(d_model, d_model)
# Final output layer
self.out_proj = torch.nn.Linear(d_model, d_model)
def forward(self, x):
batch_size, seq_len, d_model = x.shape
# 1. Project and Split into heads
q = self.q_linear(x).view(batch_size, seq_len, self.num_heads, self.head_dim)
k = self.k_linear(x).view(batch_size, seq_len, self.num_heads, self.head_dim)
v = self.v_linear(x).view(batch_size, seq_len, self.num_heads, self.head_dim)
# (Simplified attention calculation here...)
# 2. Concatenate back together
combined = v.view(batch_size, seq_len, d_model)
# 3. Final summary report
return self.out_proj(combined)
class MultiHeadAttentionSimple(torch.nn.Module)— Define un módulo personalizado de PyTorch que hereda denn.Module, lo que le otorga__call__, seguimiento de parámetros y soporte para.to(device)de forma gratuita.super().__init__()— Llama al constructor padre para inicializar la contabilidad interna de PyTorch (registro de parámetros, hooks de gradiente, etc.).self.q_linear = torch.nn.Linear(d_model, d_model)— Una capa lineal aprendible que proyecta la entrada al espacio de Query. Lo mismo se hace para Key (k_linear) y Value (v_linear). Cada una tiene sus propios pesos separados.self.out_proj = torch.nn.Linear(d_model, d_model)— La proyección de salida final que permite que las cabezas concatenadas se “comuniquen” entre sí y fusionen sus hallazgos.batch_size, seq_len, d_model = x.shape— Desempaqueta las tres dimensiones del tensor de entrada: número de secuencias en el lote, longitud de cada secuencia y dimensión de embedding.self.q_linear(x).view(batch_size, seq_len, self.num_heads, self.head_dim)— Proyecta la entrada a través de la capa lineal de Query, luego redimensiona de(batch, seq, d_model)a(batch, seq, num_heads, head_dim), dividiendo el embedding en fragmentos por cabeza. Lo mismo para K y V.combined = v.view(batch_size, seq_len, d_model)— Redimensiona (concatena) los valores por cabeza de vuelta al ancho completo ded_model. En una implementación completa, la salida de attention (no elvsin procesar) se concatenaría aquí.return self.out_proj(combined)— Pasa el vector concatenado por la capa lineal de salida, produciendo la salida final de multi-head attention para la siguiente capa.
Tomamos las notas de todos nuestros expertos, las pegamos en un solo pergamino largo, y resumimos ese pergamino en un único informe para la siguiente capa.
Revisemos los datos: ¿Realmente funciona?
¿Agregar estas características realmente ayuda? Tomemos una tarea de inversión de secuencia. Pídele a un modelo que preste atención a la palabra “anterior”, y uno sin codificación posicional se queda estancado: no tiene noción de lo que significa “anterior”.
# Let's compare attention scores for the word 'charge'
# in 'the charge exceeded the refund' vs 'the refund exceeded the charge'
def check_attention_focus():
# Mock attention scores for 3 words
# Without PE, the scores for the middle word are identical
naive_scores = torch.tensor([0.33, 0.33, 0.33])
# With PE, the model can 'see' that index 0 is different from index 2
pe_aware_scores = torch.tensor([0.70, 0.20, 0.10])
print(f"Naive model focus: {naive_scores.max().item():.2f} (No clear winner)")
print(f"PE-aware model focus: {pe_aware_scores.max().item():.2f} (Strongly prefers the subject)")
check_attention_focus()
naive_scores = torch.tensor([0.33, 0.33, 0.33])— Pesos de atención simulados que representan lo que un modelo sin codificación posicional produciría: puntuaciones casi iguales para cada palabra porque no puede distinguir las posiciones.pe_aware_scores = torch.tensor([0.70, 0.20, 0.10])— Pesos de atención simulados que representan lo que un modelo con codificación posicional podría producir: asigna pesos marcadamente diferentes a diferentes posiciones, demostrando que ha aprendido que la posición importa..max().item()—.max()devuelve un tensor que contiene el valor máximo;.item()lo extrae como un flotante de Python estándar para su formato.- Las cadenas
f"..."— f-strings que incrustan el flotante calculado con 2 decimales (:.2f) y una cadena de anotación para legibilidad humana.
El modelo ingenuo falló por un amplio margen porque trató cada palabra como igualmente “distante”. El modelo con codificación posicional se enfocó de inmediato en la palabra al inicio de la oración. Combina esto con Atención Multicabezal, y el modelo puede rastrear “cargo” como el sujeto (Cabeza 1) y “reembolso” como el objeto (Cabeza 2) simultáneamente.
Resumen: Lo que aprendimos
- La auto-atención es ciega al orden: Sin ayuda, trata las oraciones como un montón de palabras.
- La codificación posicional es un GPS: Usa ondas senoidales para darle a cada palabra una dirección única.
- La atención multicabezal es un equipo de expertos: Divide el trabajo para que el modelo pueda ver gramática, significado y contexto al mismo tiempo.
- La concatenación fusiona las vistas: Unimos los hallazgos de los expertos nuevamente en una sola representación.
El modelo de Lina ahora tiene sentido de dirección y múltiples ojos. Tiene todas las piezas — atención, multicabezal y codificación posicional — y está lista para ensamblarlas en un modelo funcional. En la Parte 9, construiremos un Transformer en miniatura desde cero y veremos cómo encajan estas piezas.
Comprueba tu comprensión
Las siguientes preguntas van desde el simple recuerdo hasta el diseño abierto, siguiendo aproximadamente la Taxonomía de Bloom.
Recordar ¿Qué significa “invariancia de permutación” y por qué es un problema para la autoatención básica?
Comprender Con tus propias palabras, explica por qué la codificación posicional usa ondas seno y coseno en lugar de simplemente sumar el número de posición crudo (0, 1, 2, …) al embedding de cada palabra.
Aplicar
Usando la fórmula de división de cabezas del artículo (head_dim = d_model // num_heads), calcula head_dim para un modelo con d_model = 64 y num_heads = 8.
Analizar El artículo dice que una cabeza podría enfocarse en “quién realiza la acción” mientras que otra se enfoca en “el tono de la oración”. Explica por qué dividir el embedding en fragmentos separados por cabeza —en lugar de simplemente ejecutar el mismo cálculo de atención de tamaño completo varias veces— es lo que realmente permite que cada cabeza se especialice en algo diferente.
Evaluar El truco de concatenación une todas las salidas de las cabezas y las pasa por una última capa lineal. Critica esto: ¿qué probablemente fallaría si omitieras esa última capa lineal y simplemente usaras la salida concatenada directamente como la entrada de la siguiente capa?
Crear Diseña un escenario con una oración corta (5-6 palabras) en la que desees al menos 3 cabezas de atención distintas, y describe en qué debería especializarse cada una de las 3 cabezas para comprender completamente esa oración.
Artículos relacionados
- El mecanismo de atención, por fin explicado (sin álgebra matricial)
- Construyendo un Transformer en miniatura desde cero: El panorama completo
Referencias y lecturas adicionales
- Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., Kaiser, Ł., y Polosukhin, I. (2017). “Attention Is All You Need.” Advances in Neural Information Processing Systems (NeurIPS 2017). — El artículo que introdujo la arquitectura Transformer, incluyendo tanto la atención multicabeza (dividiendo los embeddings en subespacios de atención paralelos) como la codificación posicional sinusoidal (el esquema GPS de seno/coseno construido en este artículo).
Esta traducción fue generada automáticamente y puede contener errores. Si el idioma inglés es tu preferencia, puedes leer el artículo original en inglés .
«Aplica lo que aprendiste» es para suscriptores Supporter e Insider.
Suscríbete para desbloquear los ejercicios de este artículo.
Ver planesArtículos relacionados
- Aprendizaje Profundo En revisión
LSTMs y GRUs: Dándole memoria a las redes
Aprende cómo las LSTMs y GRUs usan memoria con compuertas para superar el gradiente desvaneciente, reteniendo señales tempranas a lo largo de secuencias largas para lograr mejores predicciones de secuencias.
- Aprendizaje Profundo En revisión
Retropropagación intuitiva: cómo las redes aprenden de sus errores
Aprende cómo funciona la retropropagación de forma intuitiva, sin necesidad de cálculo. Descubre cómo las redes neuronales asignan la culpa a los pesos mediante la regla de la cadena y aprenden de sus errores.
- Aprendizaje Profundo En revisión
Referencia: Optimizadores
Una referencia que cubre los optimizadores de redes neuronales desde GD hasta AdamW, con programación de la tasa de aprendizaje, árboles de decisión y guía práctica para cada arquitectura.
- Aprendizaje Profundo En revisión
¿Por qué mi modelo no aprende? Guía amigable para diagnosticar callejones sin salida en el deep learning
Aprende una lista de verificación práctica de 4 pasos para diagnosticar modelos de deep learning estancados: sobreajustar un lote, revisar los gradientes, escalar los datos y hacer un barrido de las tasas de aprendizaje.
¿Buscas otra cosa?
Busca en todos los artículos por título, resumen o tema.