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

Contextualización: Embeddings que Entienden Contexto

Descripción de la cápsula

La diferencia fundamental entre embeddings modernos (BERT, OpenAI) y legacy (Word2Vec) es la contextualización: la capacidad de generar embeddings diferentes para la misma palabra según su contexto.

En esta cápsula aprenderás qué son embeddings contextuales, por qué son superiores a embeddings estáticos, cómo self-attention logra contextualización, y verás ejemplos concretos de polysemy (palabras con múltiples significados). También escribirás código para demostrar que embeddings contextuales capturan matices que los estáticos no pueden.

Al final, entenderás por qué la revolución Transformer fue tan disruptiva para NLP.


Embeddings estáticos vs contextuales

Embeddings estáticos (Word2Vec, GloVe):

Un embedding fijo por palabra, independiente del contexto.

# Word2Vec (legacy - pre-2018):
vocab = {
    "banco": [0.5, 0.3, 0.8, -0.2, 0.6],  # SIEMPRE este vector
    "río": [0.2, 0.6, 0.1, 0.4, -0.3],
    "dinero": [0.9, 0.1, 0.7, -0.5, 0.2]
}

# Problema: "banco" es SIEMPRE el mismo embedding
text_1 = "El banco del río"
text_2 = "El banco está cerrado"

# En ambos casos:
embedding_banco = vocab["banco"]  # [0.5, 0.3, 0.8, -0.2, 0.6]
# ❌ No diferencia entre "orilla" y "financiero"

Embeddings contextuales (BERT, OpenAI):

Embedding dinámico que depende del contexto completo.

# Transformers (moderno - post-2018):
text_1 = "El banco del río está lleno"
text_2 = "El banco está cerrado hoy"

# Generar embeddings contextuales
emb_banco_1 = get_contextual_embedding(text_1, word="banco")
# → [0.2, 0.5, 0.1, ...]  (contextualizado con "río")

emb_banco_2 = get_contextual_embedding(text_2, word="banco")
# → [0.8, 0.3, 0.6, ...]  (contextualizado con "cerrado")

# ✅ Embeddings DIFERENTES según contexto
similarity = cosine_similarity(emb_banco_1, emb_banco_2)
print(f"Similarity: {similarity:.4f}")  # ~0.70 (relacionados pero distintos)

Ejemplo real: Polysemy con OpenAI

Demostración de contextualización:

from openai import OpenAI
import numpy as np
import os
from dotenv import load_dotenv

load_dotenv()
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

def get_embedding(text):
    """Generar embedding completo (frase)"""
    response = client.embeddings.create(
        model="text-embedding-3-small",
        input=text
    )
    return np.array(response.data[0].embedding)

def cosine_similarity(vec_a, vec_b):
    return np.dot(vec_a, vec_b) / (np.linalg.norm(vec_a) * np.linalg.norm(vec_b))

# Contextos diferentes de "banco"
context_1 = "El banco del río está contaminado"  # Orilla
context_2 = "El banco está cerrado los domingos"  # Financiero
context_3 = "Me senté en el banco de la plaza"   # Asiento

# Generar embeddings (frases completas)
emb_1 = get_embedding(context_1)
emb_2 = get_embedding(context_2)
emb_3 = get_embedding(context_3)

# Comparar similaridades
print("Contextualización de 'banco':")
print(f"Orilla vs Financiero: {cosine_similarity(emb_1, emb_2):.4f}")
print(f"Orilla vs Asiento: {cosine_similarity(emb_1, emb_3):.4f}")
print(f"Financiero vs Asiento: {cosine_similarity(emb_2, emb_3):.4f}")

Output esperado:

Contextualización de 'banco':
Orilla vs Financiero: 0.72  ← Relacionados pero diferentes
Orilla vs Asiento: 0.68     ← Algo relacionados
Financiero vs Asiento: 0.65 ← Menos relacionados

Interpretación: Embeddings capturan que son contextos diferentes (scores <0.80).


Cómo self-attention logra contextualización

Mecanismo:

Texto: "El banco del río"

Paso 1: Embedding inicial de "banco" (sin contexto)
banco_initial = [0.5, 0.3, 0.8, ...]  (genérico)

Paso 2: Self-attention calcula attention weights
- "banco" presta atención a "río" (weight = 0.8)
- "banco" presta atención a "del" (weight = 0.3)
- "banco" presta atención a "El" (weight = 0.1)

Paso 3: Weighted sum actualiza "banco"
banco_contextualized = 0.8 × río_emb + 0.3 × del_emb + 0.1 × el_emb
                      → [0.2, 0.5, 0.1, ...]  (ahora contextualizado con "río")

Resultado: "banco" incorpora información de "río" en su embedding.


Múltiples layers refinan contextualización:

Layer 1: "banco" ve "río" (contexto inmediato)
      ↓
Layer 2: "banco" ve "río" + "del" (contexto más amplio)
      ↓
Layer 3-6: Refinamiento semántico
      ↓
Layer 7-12: Contexto global (toda la frase)
      ↓
Embedding final: "banco" completamente contextualizado

Más layers → mejor contextualización.


Ejemplos de polysemy

1. "Apple" (polysemia en inglés):

contexts = [
    "I ate an apple for breakfast",           # Fruta
    "Apple released a new iPhone",            # Empresa
    "The apple tree is in the garden"         # Árbol
]

# Generar embeddings
embeddings = [get_embedding(ctx) for ctx in contexts]

# Comparar
print("Apple - Polysemy:")
print(f"Fruta vs Empresa: {cosine_similarity(embeddings[0], embeddings[1]):.4f}")
print(f"Fruta vs Árbol: {cosine_similarity(embeddings[0], embeddings[2]):.4f}")
print(f"Empresa vs Árbol: {cosine_similarity(embeddings[1], embeddings[2]):.4f}")

Output esperado:

Apple - Polysemy:
Fruta vs Empresa: 0.68  ← Diferentes dominios
Fruta vs Árbol: 0.82    ← Relacionados (botánica)
Empresa vs Árbol: 0.65  ← No relacionados

2. "Playing" (diferentes sentidos):

contexts = [
    "She is playing piano beautifully",       # Instrumento
    "Kids are playing in the park",           # Jugar
    "The movie is playing at the cinema"      # Reproducir
]

embeddings = [get_embedding(ctx) for ctx in contexts]

print("Playing - Polysemy:")
print(f"Instrumento vs Jugar: {cosine_similarity(embeddings[0], embeddings[1]):.4f}")
print(f"Instrumento vs Reproducir: {cosine_similarity(embeddings[0], embeddings[2]):.4f}")

Output esperado:

Playing - Polysemy:
Instrumento vs Jugar: 0.75      ← Algo relacionados (ambos "playing")
Instrumento vs Reproducir: 0.70 ← Menos relacionados

Ventajas de contextualización para AI Engineering

1. Semantic search más preciso:

# Usuario busca: "banco"

# Con Word2Vec (estático):
# Retorna: "banco del río", "banco financiero", "banco de plaza" (todos similares)

# Con BERT/OpenAI (contextual):
# Si query es "banco río" → Retorna "banco del río" (contexto coincide)
# Si query es "banco dinero" → Retorna "banco financiero" (contexto coincide)

2. RAG más efectivo:

# Usuario pregunta: "¿Cómo abrir cuenta de banco?"

# Sistema RAG busca docs relevantes:
# Con contextual embeddings:
# ✅ Encuentra: "Abrir cuenta en banco" (alta similaridad)
# ❌ NO encuentra: "Banco del río" (baja similaridad, contexto diferente)

# Con static embeddings:
# ⚠️ Encuentra ambos (no diferencia contextos)

3. Classification más precisa:

# Email: "El banco rechazó mi pago"

# Static embeddings:
# Categoría: ¿Financiero o Naturaleza? (ambiguo)

# Contextual embeddings:
# Categoría: Financiero ✅ (contexto "rechazó" + "pago" clarifica)

Limitaciones de contextualización

1. Requiere contexto suficiente:

# Poco contexto:
text_minimal = "banco"
emb = get_embedding(text_minimal)
# → Embedding genérico (no puede contextualizar sin más texto)

# Contexto suficiente:
text_full = "Voy al banco a depositar dinero"
emb = get_embedding(text_full)
# → Embedding contextualizado ✅

Recomendación: Mínimo 3-5 palabras de contexto.


2. Contexto global limitado:

# Transformers tienen límite de tokens (8191 para OpenAI)
# Texto muy largo:
long_doc = "..." * 10000  # 10,000 tokens

# Solo los primeros 8191 tokens se consideran
# El resto se pierde

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


Ejercicios

Ejercicio 1: Identificar polysemy

¿Cuál palabra tiene múltiples significados?

A: "Python" (lenguaje vs serpiente) B: "casa" C: "rápido"

Ver solución

Respuesta: A) "Python"

Contextos:

  1. "Python es un lenguaje de programación" (tecnología)
  2. "Python es una serpiente grande" (animal)

B y C: No tienen significados radicalmente diferentes.

Los embeddings contextuales capturan esta diferencia:

emb_tech = get_embedding("Python es un lenguaje de programación")
emb_animal = get_embedding("Python es una serpiente grande")

similarity = cosine_similarity(emb_tech, emb_animal)
# → ~0.75 (relacionados porque palabra común, pero contextos diferentes)

Ejercicio 2: Comparar contextos

Genera embeddings para estos 3 contextos y determina cuáles son más similares:

contexts = [
    "El gato duerme en el sofá",
    "El gato caza ratones",
    "El perro duerme en el sofá"
]

# ¿Cuáles pares son más similares?
Ver solución
# Generar embeddings
embs = [get_embedding(ctx) for ctx in contexts]

# Comparar pares
print("Comparación:")
print(f"Gato duerme vs Gato caza: {cosine_similarity(embs[0], embs[1]):.4f}")
print(f"Gato duerme vs Perro duerme: {cosine_similarity(embs[0], embs[2]):.4f}")
print(f"Gato caza vs Perro duerme: {cosine_similarity(embs[1], embs[2]):.4f}")

Output esperado:

Comparación:
Gato duerme vs Gato caza: 0.85   ← Mismo sujeto (gato), verbos diferentes
Gato duerme vs Perro duerme: 0.88 ← Sujetos diferentes, MISMO verbo + contexto
Gato caza vs Perro duerme: 0.78  ← Ambos diferentes

Más similar: "Gato duerme" vs "Perro duerme" (contexto estructural idéntico).


Ejercicio 3: Detectar polysemy

Implementa función que detecta si una palabra tiene significados muy diferentes en dos contextos:

def detect_polysemy(word, context_1, context_2, threshold=0.75):
    """
    Detectar si palabra tiene significados diferentes en 2 contextos
    
    Args:
        word: Palabra a analizar
        context_1: Primera frase con la palabra
        context_2: Segunda frase con la palabra
        threshold: Umbral de similaridad (< threshold = polysemy)
    
    Returns:
        True si tiene significados diferentes
    """
    # Implementa aquí
    pass

# Test
is_polysemy = detect_polysemy(
    "banco",
    "El banco del río",
    "El banco está cerrado"
)
print(f"¿Polysemy? {is_polysemy}")
Ver solución
def detect_polysemy(word, context_1, context_2, threshold=0.75):
    """Detectar polysemy comparando embeddings de contextos"""
    # Generar embeddings de contextos completos
    emb_1 = get_embedding(context_1)
    emb_2 = get_embedding(context_2)
    
    # Calcular similaridad
    similarity = cosine_similarity(emb_1, emb_2)
    
    # Si similaridad < threshold → Significados diferentes
    return similarity < threshold

# Test
is_polysemy = detect_polysemy(
    "banco",
    "El banco del río",
    "El banco está cerrado"
)

print(f"¿'banco' tiene polysemy? {is_polysemy}")  # True

# Verificar con palabra sin polysemy
is_polysemy_2 = detect_polysemy(
    "gato",
    "El gato duerme",
    "El gato come"
)
print(f"¿'gato' tiene polysemy? {is_polysemy_2}")  # False (mismo significado)

Output:

¿'banco' tiene polysemy? True   ← Contextos diferentes
¿'gato' tiene polysemy? False  ← Mismo contexto (animal)

Resumen

Qué aprendiste:

  • Embeddings estáticos: 1 vector por palabra (Word2Vec, GloVe)
  • Embeddings contextuales: Vector dinámico según contexto (BERT, OpenAI)
  • Polysemy: Palabras con múltiples significados
  • Self-attention: Mecanismo que contextualiza
  • Ventajas: Semantic search, RAG, classification más precisos

Conceptos clave:

  1. Contextualización = diferente embedding según contexto
  2. Self-attention permite que tokens se "vean" entre sí
  3. Múltiples layers refinan contextualización progresivamente

Recursos adicionales

  1. Contextualized Word Representations - ELMo paper
  2. BERT Paper - Bidirectional contextualization
  3. Word Senses and Embeddings - Polysemy handling
  4. Illustrated BERT - Visualización
  5. Contextual Embeddings Tutorial - SBERT

En la siguiente cápsula

Cápsula 05: Pooling Strategies

Aprenderás:

  • Mean pooling (código numpy)
  • CLS pooling (BERT-style)
  • Max pooling (raro)
  • Comparación de estrategias
  • Implementación manual de pooling

De contextualización a agregación.


Módulo 2 - Embeddings Deep Dive Guide Embeddings que entienden contexto: la revolución del NLP