Módulo 2: ¿Cómo funcionan Embeddings?

Transformer Encoders: La Arquitectura de Embeddings Modernos

Descripción de la cápsula

Los Transformers revolucionaron el procesamiento de lenguaje natural en 2017 y son la base de todos los modelos de embeddings modernos (BERT, GPT, OpenAI embeddings, Sentence-BERT).

En esta cápsula aprenderás la arquitectura encoder-only (BERT-style) que genera embeddings, cómo funciona self-attention para capturar contexto, y por qué Transformers superaron a RNNs/LSTMs. No implementarás un Transformer desde cero (eso es ML Engineering), pero entenderás conceptualmente cada componente para tomar decisiones informadas como AI Engineer.

También verás comparaciones concretas de embeddings pre-Transformer (Word2Vec) vs post-Transformer (BERT) y entenderás por qué el cambio fue tan disruptivo.


La revolución Transformer (2017)

Antes de Transformers (pre-2017):

Word2Vec, GloVe (2013-2015):

# Word embeddings estáticos:
embed("bank") = [0.5, 0.3, 0.8, ...]  # SIEMPRE el mismo vector

# Problema: NO captura contexto
"Voy al banco del río""banco" = [0.5, 0.3, 0.8]
"Voy al banco a sacar dinero""banco" = [0.5, 0.3, 0.8]  # ❌ Mismo vector

RNNs/LSTMs (2015-2017):

# Sí capturaban contexto, pero:
# ❌ Lentos (procesamiento secuencial, no paralelizable)
# ❌ Problemas con secuencias largas (vanishing gradients)
# ❌ Difíciles de entrenar

Después de Transformers (2017+):

BERT, GPT, OpenAI embeddings (2018+):

# Embeddings contextuales:
embed("Voy al banco del río") → "banco" = [0.2, 0.5, 0.1, ...]  # Orilla
embed("Voy al banco a sacar dinero") → "banco" = [0.8, 0.3, 0.6, ...]  # Financiero

# ✅ Contexto diferente → embedding diferente
# ✅ Rápido (procesamiento paralelo)
# ✅ Escalable (billones de parámetros)

Cambio fundamental: De embeddings estáticos a embeddings contextuales.


Arquitectura Transformer: Encoder-Decoder

Paper original (2017): "Attention Is All You Need"

Arquitectura completa:

┌─────────────────────────────────┐
│         INPUT TOKENS            │
└─────────────────────────────────┘
              ↓
┌─────────────────────────────────┐
│    ENCODER (N layers)           │  ← Para embeddings usamos SOLO esto
│  - Self-Attention               │
│  - Feed-Forward                 │
└─────────────────────────────────┘
              ↓
┌─────────────────────────────────┐
│    DECODER (N layers)           │  ← Para generación de texto (GPT)
│  - Self-Attention               │
│  - Cross-Attention              │
│  - Feed-Forward                 │
└─────────────────────────────────┘
              ↓
┌─────────────────────────────────┐
│         OUTPUT TOKENS           │
└─────────────────────────────────┘

Para embeddings: Solo usamos ENCODER (encoder-only models).


Encoder-Only Models (BERT-style)

Arquitectura encoder-only:

Input: "Python es un lenguaje de programación"
         ↓
┌─────────────────────────────────────┐
│   Tokenización + Embedding Inicial  │
└─────────────────────────────────────┘
         ↓
┌─────────────────────────────────────┐
│   Encoder Layer 1                   │
│   - Multi-Head Self-Attention       │
│   - Add & Norm                      │
│   - Feed-Forward Network            │
│   - Add & Norm                      │
└─────────────────────────────────────┘
         ↓
┌─────────────────────────────────────┐
│   Encoder Layer 2                   │
│   (misma estructura)                │
└─────────────────────────────────────┘
         ↓
         ... (N layers)
         ↓
┌─────────────────────────────────────┐
│   Hidden States (output)            │
│   Un vector por token               │
└─────────────────────────────────────┘
         ↓
┌─────────────────────────────────────┐
│   Pooling                           │
│   (reduce a 1 vector)               │
└─────────────────────────────────────┘
         ↓
    Embedding Final

Cada encoder layer procesa todos los tokens en paralelo (no secuencialmente como RNN).


Self-Attention: El corazón del Transformer

Qué es self-attention:

Mecanismo que permite a cada token "prestar atención" a todos los otros tokens en la secuencia.

Ejemplo visual:

Input: "El banco del río"

Self-attention para "banco":
- "El" → 0.1 (poca atención)
- "banco" → 1.0 (atención a sí mismo, siempre alto)
- "del" → 0.3 (algo de atención)
- "río" → 0.8 (MUCHA atención) ✅

Resultado: "banco" entiende que está cerca de "río" 
→ Embedding refleja "orilla" (no "financiero")

Visualización de attention weights:

# Conceptual (no ejecutable sin modelo cargado)

text = "El banco del río está lleno"

# Matriz de attention (simplificada):
#           El    banco   del    río   está  lleno
# El       1.0    0.2    0.1    0.0   0.1   0.0
# banco    0.2    1.0    0.3    0.8   0.1   0.0  ← "banco" presta atención a "río"
# del      0.1    0.3    1.0    0.6   0.0   0.0
# río      0.0    0.8    0.6    1.0   0.2   0.3  ← "río" presta atención a "banco"
# está     0.1    0.1    0.0    0.2   1.0   0.5
# lleno    0.0    0.0    0.0    0.3   0.5   1.0

Los valores altos indican relación semántica entre tokens.


Comparación con arquitecturas previas:

Arquitectura¿Captura contexto?¿Paralelizable?Complejidad
Word2Vec❌ No (estático)✅ Sí (entrenamiento)O(1) per token
RNN/LSTM✅ Sí (secuencial)❌ NoO(n) secuencial
Transformer✅ Sí (self-attention)✅ SíO(n²) pero paralelo

Trade-off: Transformers son O(n²) en memoria pero paralelizables (GPUs).


Componentes de un Encoder Layer

1. Multi-Head Self-Attention

Qué hace: Calcula attention weights entre todos los pares de tokens.

"Multi-Head": Múltiples attention mechanisms en paralelo (e.g., 12 heads en BERT-base).

┌─────────────────────────────────────┐
│   Multi-Head Self-Attention         │
│                                     │
│   Head 1: Atención a relaciones     │
│            sintácticas              │
│   Head 2: Atención a relaciones     │
│            semánticas               │
│   Head 3: Atención a contexto       │
│            local                    │
│   ...                               │
│   Head 12: Atención a contexto      │
│             global                  │
└─────────────────────────────────────┘

Cada head aprende patrones diferentes automáticamente durante entrenamiento.


2. Add & Normalize (Residual Connection)

Qué hace: Suma input original + output de attention, luego normaliza.

# Pseudocódigo
output_attention = self_attention(input)
output = layer_norm(input + output_attention)  # Residual connection

Por qué: Evita vanishing gradients en redes profundas (12-24 layers).


3. Feed-Forward Network

Qué hace: Transformación no-linear aplicada a cada token independientemente.

# Pseudocódigo
def feed_forward(x):
    # Expansión
    hidden = linear_1(x)  # [hidden_size] → [4 * hidden_size]
    hidden = relu(hidden)  # Activación
    
    # Contracción
    output = linear_2(hidden)  # [4 * hidden_size] → [hidden_size]
    return output

# Aplicar a cada token
for token_vector in hidden_states:
    token_vector = feed_forward(token_vector)

Intuición: Proyecta a espacio más grande (4x), aplica no-linearidad, proyecta de vuelta.


4. Add & Normalize (segunda vez)

# Pseudocódigo
output_ffn = feed_forward(input)
output = layer_norm(input + output_ffn)  # Residual connection

Resultado: Output de un encoder layer completo.


Encoder Stack (N layers)

BERT-base: 12 encoder layers

Input tokens
    ↓
┌────────────┐
│ Layer 1    │ → Captura patrones locales (bigrams, trigrams)
├────────────┤
│ Layer 2    │
├────────────┤
│ Layer 3    │ → Captura relaciones sintácticas
├────────────┤
│ Layer 4-6  │ → Captura semántica básica
├────────────┤
│ Layer 7-9  │ → Captura semántica profunda
├────────────┤
│ Layer 10-12│ → Captura contexto global
└────────────┘
    ↓
Hidden states (contextualizados)

Más layers = más profundidad contextual.


Self-Attention: Matemáticas (simplificado)

Conceptos clave:

Para cada token, calculamos:

  1. Query (Q): "¿Qué busco?"
  2. Key (K): "¿Qué ofrezco?"
  3. Value (V): "Mi contenido"

Intuición: Token A hace query, compara con keys de todos los tokens, obtiene attention weights, y calcula weighted sum de values.


Fórmula de attention (simplificada):

Attention(Q, K, V) = softmax(Q · K^T / √d_k) · V

Donde:
- Q, K, V: Matrices de queries, keys, values
- Q · K^T: Dot product (similaridad)
- √d_k: Scaling factor (estabilidad numérica)
- softmax: Normaliza a probabilidades (suma = 1)
- · V: Weighted sum de values

No necesitas memorizar esto. Importante: self-attention calcula similaridad entre tokens.


Ejemplo conceptual:

# Token "banco" en "El banco del río"

# Query de "banco": [0.5, 0.3, 0.8]
# Keys de todos los tokens:
#   "El": [0.1, 0.2, 0.1]
#   "banco": [0.5, 0.3, 0.8]  (sí mismo)
#   "del": [0.2, 0.4, 0.3]
#   "río": [0.6, 0.4, 0.9]

# Dot products (similaridad):
#   "banco" · "El" = 0.19 (bajo)
#   "banco" · "banco" = 0.98 (alto - sí mismo)
#   "banco" · "del" = 0.46 (medio)
#   "banco" · "río" = 1.02 (alto - relacionado) ✅

# Softmax (normalizar):
#   "El": 0.1
#   "banco": 0.3
#   "del": 0.2
#   "río": 0.4  ← Mayor atención

# Weighted sum de values → Nuevo vector de "banco" (contextualizado)

Resultado: "banco" actualizado con información de "río".


Positional Encoding

Problema:

Self-attention no tiene noción de orden (permutation-invariant).

# Sin positional encoding:
embed("El banco del río") ≈ embed("río del banco El")  # ❌ Orden no importa

Solución: Agregar información de posición.


Positional Encoding (sinusoidal):

# Pseudocódigo (OpenAI/BERT usan learned embeddings)
def positional_encoding(position, d_model):
    """
    Encoding único para cada posición
    
    Args:
        position: Posición del token (0, 1, 2, ...)
        d_model: Dimensiones del embedding
    """
    encoding = []
    for i in range(d_model):
        if i % 2 == 0:
            encoding.append(sin(position / 10000^(i/d_model)))
        else:
            encoding.append(cos(position / 10000^(i/d_model)))
    return encoding

# Agregar a token embeddings
token_embedding = [0.5, 0.3, 0.8, ...]
position_encoding = positional_encoding(position=2, d_model=768)
final_embedding = token_embedding + position_encoding

Ahora el modelo sabe que "banco" está en posición 2.


Comparación: Word2Vec vs Transformer Embeddings

Word2Vec (2013):

# Embeddings estáticos - NO contextuales
vocab = {
    "banco": [0.5, 0.3, 0.8],  # SIEMPRE este vector
    "río": [0.2, 0.6, 0.1],
    "dinero": [0.9, 0.1, 0.7]
}

# Problema:
text_1 = "banco del río"
text_2 = "banco de dinero"

# "banco" tiene el MISMO embedding en ambos contextos ❌

BERT / OpenAI Embeddings (2018+):

# Embeddings contextuales
def get_contextual_embedding(text, word):
    # Tokenizar texto completo
    tokens = tokenize(text)
    
    # Procesar con Transformer
    hidden_states = transformer_encoder(tokens)
    
    # Embedding de "word" depende de contexto
    word_idx = tokens.index(word)
    return hidden_states[word_idx]

# Ejemplo:
emb_1 = get_contextual_embedding("banco del río", "banco")
# → [0.2, 0.5, 0.1, ...]  (orilla)

emb_2 = get_contextual_embedding("banco de dinero", "banco")
# → [0.8, 0.3, 0.6, ...]  (financiero)

# Embeddings DIFERENTES según contexto ✅

Modelos encoder-only populares

1. BERT (2018) - Google

Arquitectura: 12 layers, 768 hidden, 12 attention heads
Parámetros: 110M (base), 340M (large)
Embeddings: 768 dims (base), 1024 dims (large)
Training: Masked Language Modeling (MLM) + Next Sentence Prediction

Uso: Base para fine-tuning en tareas específicas.


2. Sentence-BERT (2019)

Arquitectura: BERT fine-tuned para sentence embeddings
Parámetros: 22M (MiniLM-L6), 110M (base)
Embeddings: 384 dims (MiniLM), 768 dims (base)
Training: Siamese network con pares similares
Pooling: Mean pooling (default)

Uso: Sentence embeddings out-of-the-box (mejor que BERT vanilla).


3. OpenAI text-embedding-3-small (2024)

Arquitectura: Transformer encoder (detalles no públicos)
Parámetros: No público
Embeddings: 1536 dims
Training: Contrastive learning a escala masiva
Pooling: No documentado (probablemente mean)

Uso: API, no local. Excelente calidad out-of-the-box.


4. BGE (BAAI General Embedding) - 2023

Arquitectura: BERT-large fine-tuned
Parámetros: 340M (large)
Embeddings: 1024 dims
Training: Contrastive learning + hard negatives
Pooling: CLS token

Uso: State-of-the-art open-source, local.


Ventajas de Transformers sobre arquitecturas previas

vs RNN/LSTM:

AspectoRNN/LSTMTransformer
Paralelización❌ No (secuencial)✅ Sí (todos los tokens en paralelo)
Secuencias largas❌ Vanishing gradients✅ Self-attention captura largo alcance
Velocidad❌ Lento (GPU subutilizada)✅ Rápido (GPU fully utilizada)
Escalabilidad❌ Difícil escalar✅ Escala a billones de parámetros

vs Word2Vec/GloVe:

AspectoWord2Vec/GloVeTransformer
Contextual❌ No (estático)✅ Sí (contexto dinámico)
Polysemy❌ No (1 vector por palabra)✅ Sí (diferentes embeddings según contexto)
Calidad⚠️ Básico✅ State-of-the-art

Limitaciones de Transformers

1. Complejidad cuadrática (O(n²)):

# Attention calcula similitud entre TODOS los pares de tokens
n_tokens = 1000
attention_computations = n_tokens ** 2  # 1,000,000 operaciones

# Para textos largos (10K tokens):
n_tokens = 10000
attention_computations = n_tokens ** 2  # 100,000,000 operaciones 😱

Límite: OpenAI embeddings = 8191 tokens max.


2. Memoria intensiva:

# Matriz de attention: [batch_size, n_heads, seq_len, seq_len]
batch_size = 32
n_heads = 12
seq_len = 512

attention_matrix_size = batch_size * n_heads * seq_len * seq_len * 4  # bytes (float32)
# = 32 * 12 * 512 * 512 * 4 ≈ 402 MB (solo attention!)

Requiere GPUs con mucha VRAM.


3. No entiende matemáticas/lógica perfectamente:

# Transformers son buenos en lenguaje natural, no en lógica exacta:
query = "¿Cuánto es 123456 × 789012?"
embedding = get_embedding(query)

# Embedding captura "pregunta de multiplicación" pero NO el resultado exacto

Para cálculos: usa herramientas (tool use), no embeddings.


Ejercicios

Ejercicio 1: Identificar arquitectura

¿Qué arquitectura es mejor para cada caso?

Caso A: Generar embeddings de frases Caso B: Generar texto (chatbot) Caso C: Traducción automática

Ver solución
  • Caso A: Encoder-only (BERT, Sentence-BERT, OpenAI embeddings)

    • Solo necesitas representación vectorial, no generación
  • Caso B: Decoder-only (GPT, Claude)

    • Generación de texto token por token
  • Caso C: Encoder-Decoder (T5, BART)

    • Encoder procesa idioma origen, decoder genera idioma destino

Embeddings = Encoder-only.


Ejercicio 2: Predecir attention

Dado el texto "El gato persigue al ratón", ¿qué token debería recibir MÁS atención de "persigue"?

Opciones: A) "El" B) "gato" C) "al" D) "ratón"

Ver solución

Respuesta: B) "gato" y D) "ratón"

Razón:

  • "persigue" es un verbo
  • Verbo presta atención a sujeto ("gato") y objeto ("ratón")
  • "El" y "al" son artículos (menos relevantes semánticamente)

Self-attention aprende relaciones sintácticas automáticamente.


Ejercicio 3: Comparar Word2Vec vs Transformer

¿Qué approach captura mejor la diferencia de "banco"?

text_1 = "Voy al banco del río a pescar"
text_2 = "Voy al banco a depositar dinero"

Opciones: A) Word2Vec (embeddings estáticos) B) Transformer (embeddings contextuales)

Ver solución

Respuesta: B) Transformer

Word2Vec:

  • "banco" = [0.5, 0.3, 0.8] en AMBOS contextos ❌
  • No diferencia entre "orilla" y "financiero"

Transformer:

  • Text 1: "banco" con contexto de "río" + "pescar" → Embedding A (orilla)
  • Text 2: "banco" con contexto de "depositar" + "dinero" → Embedding B (financiero)
  • Embeddings A y B son DIFERENTES ✅

Transformers resuelven polysemy (múltiples significados).


Ejercicio 4: Calcular complejidad

¿Cuántas operaciones de attention para 512 tokens?

n_tokens = 512
# ¿Cuántas operaciones?
Ver solución
n_tokens = 512
attention_operations = n_tokens ** 2
print(f"Operaciones: {attention_operations}")
# → 262,144 operaciones

# Con 12 attention heads (BERT-base):
total_operations = attention_operations * 12
print(f"Total: {total_operations}")
# → 3,145,728 operaciones por layer
# × 12 layers = ~37M operaciones total

Por eso Transformers requieren GPUs potentes.


Troubleshooting común

Problema 1: "Token limit exceeded"

# Error: Texto > 8191 tokens (OpenAI)
long_text = "..." * 10000
embedding = get_embedding(long_text)  # ❌ Error

Solución: Chunkear texto (Módulo 4).


Problema 2: Embeddings idénticos para contextos diferentes

# Esperabas diferentes, pero son casi idénticos:
emb_1 = embed("banco del río")
emb_2 = embed("banco de dinero")

similarity = cosine_similarity(emb_1, emb_2)  # 0.95 (muy similar)

Causa: Modelo no es lo suficientemente contextual (e.g., Word2Vec legacy).

Solución: Usa modelo moderno (OpenAI, SBERT, BGE).


Resumen

Qué aprendiste:

  • Transformers: Arquitectura encoder-only para embeddings
  • Self-attention: Captura contexto entre todos los tokens
  • Encoder layers: Stack de 12-24 layers procesa en paralelo
  • Embeddings contextuales: "banco" tiene diferentes embeddings según contexto
  • Ventajas: Paralelo, escalable, captura largo alcance
  • Limitaciones: O(n²) complejidad, límite de tokens

Conceptos clave:

  1. Self-attention permite que cada token vea todos los otros tokens
  2. Encoder stack de N layers captura contexto progresivamente
  3. Embeddings contextuales > embeddings estáticos (Word2Vec)

Recursos adicionales

  1. Illustrated Transformer - Visualización excelente
  2. Attention Is All You Need - Paper original
  3. BERT Explained - Encoder-only architecture
  4. The Annotated Transformer - Implementación comentada
  5. Transformers from Scratch - Deep dive técnico

En la siguiente cápsula

Cápsula 03: Tokenización

Aprenderás:

  • BPE (Byte Pair Encoding) algoritmo
  • WordPiece tokenization (BERT)
  • tiktoken código (OpenAI)
  • Por qué sub-palabras > palabras completas
  • Vocabulario y out-of-vocabulary handling

De arquitectura a primer paso del pipeline.


Módulo 2 - Embeddings Deep Dive Guide Entendiendo la arquitectura que revolucionó NLP