Saltar al contenido principal

Capítulo 3: Codificación Posicional y Embeddings

Introducción​

A diferencia de las RNN, que procesan las secuencias token por token, los Transformers procesan todas las posiciones simultáneamente. Esto crea un desafío fundamental: ¿cómo sabe el modelo el orden de los tokens? Este capítulo explica la codificación posicional y el proceso más amplio de embedding de tokens.

El Problema de la Posición​

Por Qué la Posición Importa​

Considere estas oraciones:

  • "El perro mordió al hombre"
  • "El hombre mordió al perro"

Mismos tokens, significados completamente diferentes. La posición es crucial para entender el lenguaje.

Transformers y la Invariancia a la Permutación​

Sin información posicional, la auto-atención es invariante a la permutación:

SelfAttention(π(X))=π(SelfAttention(X))\text{SelfAttention}(\pi(X)) = \pi(\text{SelfAttention}(X))

Donde π\pi es cualquier permutación. ¡El modelo trata "perro mordió hombre" y "hombre mordió perro" de manera idéntica!

# Demostración: los scores de atención son independientes de la posición
import torch

def attention_scores(Q, K):
return torch.softmax(Q @ K.T, dim=-1)

# Secuencia original
x1 = torch.tensor([[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]])

# Secuencia permutada (intercambia posiciones 0 y 1)
x2 = torch.tensor([[0.0, 1.0], [1.0, 0.0], [1.0, 1.0]])

# ¡Mismo patrón de atención (solo permutado)!
print(attention_scores(x1, x1))
print(attention_scores(x2, x2))

Embeddings de Tokens​

De Tokens a Vectores​

Antes de agregar información posicional, primero convertimos los tokens en vectores densos:

E=Embedding(token_ids)E = \text{Embedding}(\text{token\_ids})

Implementación​

import torch
import torch.nn as nn

class TokenEmbedding(nn.Module):
"""
Convierte IDs de tokens en vectores densos.
"""
def __init__(self, vocab_size, d_model):
super().__init__()
self.embedding = nn.Embedding(vocab_size, d_model)
self.d_model = d_model

def forward(self, token_ids):
"""
token_ids: (batch, seq_len) de IDs de tokens enteros
Devuelve: embeddings (batch, seq_len, d_model)
"""
# Escala los embeddings (del paper original de Transformer)
return self.embedding(token_ids) * (self.d_model ** 0.5)

# Ejemplo
vocab_size = 50000
d_model = 512
embedding_layer = TokenEmbedding(vocab_size, d_model)

tokens = torch.tensor([[101, 7592, 2088, 102]]) # [CLS] Hello World [SEP]
embeddings = embedding_layer(tokens)
print(f"Shape: {embeddings.shape}") # (1, 4, 512)

Codificación Posicional Sinusoidal​

El Enfoque Original​

El paper original de Transformer utilizó funciones sinusoidales fijas:

PE(pos,2i)=sin⁡(pos100002i/dmodel)PE_{(pos, 2i)} = \sin\left(\frac{pos}{10000^{2i/d_{model}}}\right) PE(pos,2i+1)=cos⁡(pos100002i/dmodel)PE_{(pos, 2i+1)} = \cos\left(\frac{pos}{10000^{2i/d_{model}}}\right)

Donde:

  • pospos = posición en la secuencia
  • ii = índice de dimensión
  • dmodeld_{model} = dimensión del modelo

Intuición​

Cada dimensión oscila a una frecuencia diferente:

  • Dimensiones bajas: oscilación lenta (captura posiciones de largo alcance)
  • Dimensiones altas: oscilación rápida (captura posiciones de detalle fino)

Implementación​

import numpy as np

def get_sinusoidal_encoding(max_len, d_model):
"""
Genera codificaciones posicionales sinusoidales.

Args:
max_len: Longitud máxima de la secuencia
d_model: Dimensión del modelo

Returns:
Matriz de codificación posicional (max_len, d_model)
"""
position = np.arange(max_len)[:, np.newaxis]
div_term = np.exp(np.arange(0, d_model, 2) * -(np.log(10000.0) / d_model))

pe = np.zeros((max_len, d_model))
pe[:, 0::2] = np.sin(position * div_term) # Índices pares
pe[:, 1::2] = np.cos(position * div_term) # Índices impares

return pe

# Visualización
import matplotlib.pyplot as plt

pe = get_sinusoidal_encoding(100, 512)
plt.figure(figsize=(12, 4))
plt.imshow(pe.T, aspect='auto', cmap='RdBu')
plt.xlabel('Posición')
plt.ylabel('Dimensión')
plt.colorbar(label='Valor')
plt.title('Codificación Posicional Sinusoidal')
plt.show()

Propiedades de la Codificación Sinusoidal​

  1. Única: Cada posición tiene una codificación única
  2. Acotada: Los valores siempre están en [-1, 1]
  3. Posiciones relativas: La relación lineal permite aprender posiciones relativas
  4. Extrapolación: Puede generalizar a secuencias más largas que las vistas en entrenamiento

Implementación en PyTorch​

class SinusoidalPositionalEncoding(nn.Module):
"""
Codificación posicional sinusoidal de 'Attention Is All You Need'.
"""
def __init__(self, d_model, max_len=5000, dropout=0.1):
super().__init__()
self.dropout = nn.Dropout(p=dropout)

# Crear matriz de codificación posicional
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(
torch.arange(0, d_model, 2).float() * (-np.log(10000.0) / d_model)
)

pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = pe.unsqueeze(0) # Añade dimensión de batch

# Registrar como buffer (no es un parámetro, pero se guarda con el modelo)
self.register_buffer('pe', pe)

def forward(self, x):
"""
x: (batch, seq_len, d_model)
"""
x = x + self.pe[:, :x.size(1)]
return self.dropout(x)

Embeddings Posicionales Aprendidos​

Un Enfoque Alternativo​

Muchos LLM modernos usan embeddings posicionales aprendidos:

class LearnedPositionalEmbedding(nn.Module):
"""
Embeddings posicionales aprendidos (por ejemplo, usados en GPT-2).
"""
def __init__(self, max_len, d_model):
super().__init__()
self.position_embedding = nn.Embedding(max_len, d_model)

def forward(self, x):
"""
x: (batch, seq_len, d_model)
"""
positions = torch.arange(x.size(1), device=x.device)
return x + self.position_embedding(positions)

Comparación​

AspectoSinusoidalAprendido
ParámetrosNingunomax_len × d_model
ExtrapolaciónMejorLimitado a max_len
RendimientoComparableComparable
FlexibilidadFijoSe adapta a los datos

Rotary Position Embedding (RoPE)​

Innovación Moderna​

RoPE, utilizado en LLaMA, Mistral y muchos modelos modernos, codifica la posición mediante rotación:

fq(xm,m)=xmeimθf_q(x_m, m) = x_m e^{im\theta}

Beneficios Clave​

  1. Posiciones relativas: Captura naturalmente la información de posición relativa
  2. Extrapolación: Mejor manejo de secuencias largas
  3. Eficiencia: Se aplica directamente a Q, K sin parámetros adicionales

Implementación Simplificada​

def rotary_embedding(x, seq_len, d_model):
"""
Implementación simplificada de RoPE.
x: (batch, seq_len, d_model)
"""
# Genera ángulos de rotación
position = torch.arange(seq_len).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2) * -(np.log(10000.0) / d_model))
angles = position * div_term

# Aplica rotación a pares de dimensiones
x_rotated = x.clone()
x_rotated[..., 0::2] = x[..., 0::2] * torch.cos(angles) - x[..., 1::2] * torch.sin(angles)
x_rotated[..., 1::2] = x[..., 0::2] * torch.sin(angles) + x[..., 1::2] * torch.cos(angles)

return x_rotated

ALiBi: Atención con Sesgos Lineales​

Otro Enfoque​

ALiBi (Attention with Linear Biases) añade una penalización lineal basada en la distancia:

softmax(qi⋅kj−m⋅∣i−j∣)\text{softmax}(q_i \cdot k_j - m \cdot |i - j|)

Donde mm es una pendiente específica de cada cabeza.

Beneficios​

  1. Sin parámetros adicionales
  2. Excelente extrapolación en longitud
  3. Implementación simple

Capa de Embedding Completa​

Integrando Todo​

class TransformerEmbedding(nn.Module):
"""
Capa de embedding completa: token + posición.
"""
def __init__(self, vocab_size, d_model, max_len=5000,
pos_encoding='sinusoidal', dropout=0.1):
super().__init__()

self.token_embedding = nn.Embedding(vocab_size, d_model)
self.d_model = d_model
self.dropout = nn.Dropout(dropout)

if pos_encoding == 'sinusoidal':
self.pos_encoding = SinusoidalPositionalEncoding(d_model, max_len)
elif pos_encoding == 'learned':
self.pos_encoding = LearnedPositionalEmbedding(max_len, d_model)
else:
raise ValueError(f"Unknown pos_encoding: {pos_encoding}")

def forward(self, token_ids):
"""
token_ids: (batch, seq_len)
Devuelve: (batch, seq_len, d_model)
"""
# Embeddings de tokens (escalados)
x = self.token_embedding(token_ids) * (self.d_model ** 0.5)

# Añade codificación posicional
x = self.pos_encoding(x)

return self.dropout(x)

# Uso
embedding = TransformerEmbedding(
vocab_size=50000,
d_model=512,
max_len=2048,
pos_encoding='sinusoidal'
)

tokens = torch.randint(0, 50000, (32, 100)) # Lote de 32, seq_len 100
embedded = embedding(tokens)
print(f"Output shape: {embedded.shape}") # (32, 100, 512)

El Panorama Completo​

Así es como los tokens fluyen a través de los embeddings hasta la atención:

IDs de tokens: [101, 7592, 2088, 102]
↓
Embedding de tokens: búsqueda en la tabla de embeddings
↓
[0.23, -0.45, ...] [0.89, 0.12, ...] [0.45, -0.23, ...] [0.67, 0.34, ...]
↓
Agregar codificación posicional
↓
[0.23+PE_0, -0.45+PE_0, ...] [0.89+PE_1, 0.12+PE_1, ...] ...
↓
Atención multi-cabeza
↓
Salida con contexto sensible a la posición

Puntos Clave​

Summary
  1. La información de posición es esencial—de lo contrario, los Transformers son invariantes a la permutación
  2. Los embeddings de tokens convierten tokens discretos en vectores continuos
  3. La codificación sinusoidal usa patrones de frecuencia fija (Transformer original)
  4. Los embeddings aprendidos se adaptan a los datos pero tienen longitud máxima fija
  5. RoPE codifica posiciones relativas mediante rotación (LLMs modernos)
  6. ALiBi usa sesgos de atención basados en la distancia

Mirando Hacia Adelante​

Ahora que entendemos la arquitectura completa de Transformer—atención, atención multi-cabeza y codificación posicional—estamos listos para avanzar al Módulo 3, donde exploraremos cómo se entrenan estos modelos masivos.