Módulo 1: ¿Qué son Embeddings?
Propiedades Vectoriales de Embeddings
Descripción de la cápsula
Los embeddings no son solo listas arbitrarias de números—son vectores con propiedades matemáticas específicas que los hacen increíblemente útiles para sistemas de AI.
En esta cápsula aprenderás las propiedades vectoriales clave de embeddings: similaridad semántica (qué tan "cerca" están dos vectores), direccionalidad (qué representa la dirección en el espacio), y magnitud vs dirección (cuál importa más para semantic search).
También verás código práctico usando numpy para calcular cosine similarity y entenderás por qué es la métrica estándar en AI.
Embeddings como vectores en espacio
Recordatorio: Un vector es una flecha en el espacio
Vector 2D (visualizable):
^
| (3, 4) ← Punto final
| /
| /
| /
└────────>
(0,0) Origen
Un embedding es un vector en espacio de alta dimensionalidad:
# Embedding = vector de 1536 dimensiones
embedding = [0.023, -0.145, 0.892, ..., 0.567]
└─────────────────────────────────┘
1536 dimensiones
No podemos visualizar 1536D, pero las matemáticas son las mismas que en 2D/3D.
Propiedad 1: Similaridad Semántica
Concepto central:
Textos con significado similar tienen embeddings cercanos en el espacio vectorial.
Visualización conceptual (proyectado a 2D):
Python ●
/ JavaScript ●
/
/
Ruby ● /
/
/
/
Perro ●
\
\ Gato ●
\
- "Python", "JavaScript", "Ruby" están agrupados (lenguajes de programación)
- "Perro", "Gato" están agrupados (animales)
- Los dos grupos están separados (dominios diferentes)
Midiendo distancia: Cosine Similarity
La métrica estándar en AI:
import numpy as np
def cosine_similarity(vec_a, vec_b):
"""
Calcula similaridad entre dos vectores usando coseno del ángulo entre ellos.
Retorna valor entre -1 y 1:
- 1.0 = idénticos (ángulo 0°)
- 0.0 = ortogonales (ángulo 90°)
- -1.0 = opuestos (ángulo 180°)
"""
dot_product = np.dot(vec_a, vec_b)
norm_a = np.linalg.norm(vec_a)
norm_b = np.linalg.norm(vec_b)
return dot_product / (norm_a * norm_b)
# Ejemplo con vectores pequeños (conceptual):
vec_python = np.array([0.5, 0.3, 0.8])
vec_javascript = np.array([0.4, 0.3, 0.7])
vec_gato = np.array([-0.2, 0.9, -0.1])
print(cosine_similarity(vec_python, vec_javascript)) # ~0.99 (muy similares)
print(cosine_similarity(vec_python, vec_gato)) # ~0.15 (no relacionados)
¿Por qué cosine similarity y no euclidean distance?
Comparación:
import numpy as np
vec_a = np.array([1.0, 0.0])
vec_b = np.array([2.0, 0.0]) # Misma dirección, doble magnitud
vec_c = np.array([0.0, 1.0]) # Dirección perpendicular
# Euclidean distance (distancia geométrica)
dist_ab = np.linalg.norm(vec_a - vec_b) # 1.0
dist_ac = np.linalg.norm(vec_a - vec_c) # 1.41
# Cosine similarity (ángulo)
cos_ab = cosine_similarity(vec_a, vec_b) # 1.0 ← Idénticos (mismo ángulo)
cos_ac = cosine_similarity(vec_a, vec_c) # 0.0 ← Ortogonales (90°)
Ventaja de cosine:
- Ignora magnitud (longitud del vector)
- Solo mide dirección (orientación en el espacio)
- Para embeddings, dirección = significado semántico
En semantic search, nos importa el significado (dirección), no la "intensidad" (magnitud).
Ejemplo real con OpenAI embeddings
Comparando textos similares:
from openai import OpenAI
import numpy as np
import os
from dotenv import load_dotenv
# Setup
load_dotenv()
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
def get_embedding(text):
"""Helper para generar embedding"""
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):
"""Calcula cosine similarity"""
return np.dot(vec_a, vec_b) / (np.linalg.norm(vec_a) * np.linalg.norm(vec_b))
# Textos para comparar
text_1 = "Python es un lenguaje de programación"
text_2 = "Python es un lenguaje para programar" # Paráfrasis
text_3 = "JavaScript es un lenguaje de programación" # Similar (otro lenguaje)
text_4 = "El gato duerme en el sofá" # No relacionado
# Generar embeddings
emb_1 = get_embedding(text_1)
emb_2 = get_embedding(text_2)
emb_3 = get_embedding(text_3)
emb_4 = get_embedding(text_4)
# Calcular similaridades
print(f"Python vs Python (paráfrasis): {cosine_similarity(emb_1, emb_2):.4f}")
# → ~0.95-0.98 (muy similares)
print(f"Python vs JavaScript: {cosine_similarity(emb_1, emb_3):.4f}")
# → ~0.85-0.90 (similares, ambos lenguajes)
print(f"Python vs Gato: {cosine_similarity(emb_1, emb_4):.4f}")
# → ~0.60-0.70 (no relacionados)
Interpretación de scores:
- 0.95-1.00: Paráfrasis o casi idénticos
- 0.85-0.95: Relacionados semánticamente (mismo dominio)
- 0.70-0.85: Algo relacionados
- <0.70: No relacionados o mínimamente relacionados
Propiedad 2: Direccionalidad
Concepto:
La dirección del vector en el espacio representa el significado semántico.
Visualización conceptual:
Tecnología
^
|
Python ●
| \ JavaScript ●
| \
| \ Ruby ●
────────────┼──────────────────> Formalidad
|
Pizza ●
|
v
Comida
Diferentes direcciones = diferentes significados:
- "Python", "JavaScript", "Ruby" → Dirección "Tecnología"
- "Pizza", "Hamburguesa" → Dirección "Comida"
Aritmética vectorial (conceptual):
Con word embeddings (Word2Vec) funcionaba:
# Conceptual (no funciona así con sentence embeddings modernos):
vec_rey = embed("rey")
vec_reina = embed("reina")
vec_hombre = embed("hombre")
vec_mujer = embed("mujer")
# "rey" - "hombre" + "mujer" ≈ "reina"
resultado = vec_rey - vec_hombre + vec_mujer
# cosine_similarity(resultado, vec_reina) → ~0.90
Con sentence embeddings modernos (OpenAI, SBERT):
- Esta aritmética es menos predecible
- Los modelos son más contextuales y complejos
- No confíes en aritmética de embeddings para producción
Takeaway: Dirección importa, pero no manipules embeddings aritméticamente en producción.
Propiedad 3: Magnitud vs Dirección
¿Qué importa más?
Para semantic search: DIRECCIÓN.
import numpy as np
# Dos vectores con misma dirección pero diferente magnitud
vec_a = np.array([1.0, 1.0]) # Magnitud = sqrt(2) ≈ 1.41
vec_b = np.array([2.0, 2.0]) # Magnitud = sqrt(8) ≈ 2.83 (doble)
# Cosine similarity ignora magnitud:
cosine = np.dot(vec_a, vec_b) / (np.linalg.norm(vec_a) * np.linalg.norm(vec_b))
print(cosine) # 1.0 (idénticos según cosine)
# Euclidean distance SÍ considera magnitud:
distance = np.linalg.norm(vec_a - vec_b)
print(distance) # ~1.41 (diferentes según euclidean)
Implicación: Normalizar embeddings (L2 normalization) es común para que solo importe la dirección.
Normalización de embeddings:
def normalize_embedding(embedding):
"""Normaliza embedding a longitud unitaria (magnitud = 1.0)"""
return embedding / np.linalg.norm(embedding)
# Ejemplo:
emb = np.array([3.0, 4.0]) # Magnitud = 5.0
emb_normalized = normalize_embedding(emb)
print(f"Original: {emb}") # [3.0, 4.0]
print(f"Normalizado: {emb_normalized}") # [0.6, 0.8]
print(f"Magnitud normalizada: {np.linalg.norm(emb_normalized)}") # 1.0
Ventaja: Con embeddings normalizados, dot product = cosine similarity (más eficiente).
# Con embeddings normalizados:
dot_product = np.dot(emb_a_normalized, emb_b_normalized)
# dot_product == cosine_similarity(emb_a, emb_b) ✅
Nota: OpenAI embeddings NO vienen normalizados por defecto. Sentence-BERT SÍ los normaliza.
Propiedad 4: Clustering Natural
Concepto:
Embeddings de textos relacionados se agrupan naturalmente en clusters.
Visualización conceptual (proyectado a 2D):
Cluster "Lenguajes"
┌─────────────────────────┐
│ Python ● JavaScript ● │
│ │
│ Ruby ● Java ● │
└─────────────────────────┘
Cluster "Animales"
┌─────────────────────────┐
│ Perro ● Gato ● │
│ │
│ Caballo ● Ratón ● │
└─────────────────────────┘
No necesitas etiquetar manualmente: Embeddings se agrupan solos por significado semántico.
Ejemplo práctico: Clustering simple
from sklearn.cluster import KMeans
import numpy as np
# Textos de dos dominios:
texts = [
# Tecnología (cluster 1):
"Python es un lenguaje de programación",
"JavaScript corre en el navegador",
"Ruby es bueno para web development",
# Animales (cluster 2):
"El perro ladra",
"El gato maúlla",
"El caballo galopa"
]
# Generar embeddings
embeddings = [get_embedding(text) for text in texts]
embeddings_array = np.array(embeddings)
# Clustering con K-Means (2 clusters)
kmeans = KMeans(n_clusters=2, random_state=42)
clusters = kmeans.fit_predict(embeddings_array)
# Mostrar resultados
for i, (text, cluster) in enumerate(zip(texts, clusters)):
print(f"Cluster {cluster}: {text}")
Output esperado:
Cluster 0: Python es un lenguaje de programación
Cluster 0: JavaScript corre en el navegador
Cluster 0: Ruby es bueno para web development
Cluster 1: El perro ladra
Cluster 1: El gato maúlla
Cluster 1: El caballo galopa
Clustering automático sin supervisión: embeddings agrupan por tema.
Propiedad 5: Transitividad de Similaridad
Concepto:
Si A es similar a B, y B es similar a C, entonces A es algo similar a C (aproximadamente).
# Supongamos:
similarity(A, B) = 0.95 # A y B muy similares
similarity(B, C) = 0.90 # B y C muy similares
# Podemos inferir:
similarity(A, C) ≈ 0.85-0.90 # A y C probablemente similares
Aplicación: Query expansion en search.
# Usuario busca "Python"
# Embeddings similares: "Python", "py", "python3", "CPython", "Jython"
# → Expande búsqueda automáticamente a sinónimos/variantes
Propiedad 6: Suavidad del Espacio
Concepto:
Pequeños cambios en el texto → pequeños cambios en el embedding.
# Textos con cambios mínimos:
texts = [
"El gato duerme",
"El gato descansa", # Cambio: duerme → descansa (sinónimo)
"El perro descansa", # Cambio: gato → perro
"La pizza es buena" # Cambio total (dominio diferente)
]
# Embeddings esperados:
# emb_1 vs emb_2: ~0.90 (cambio mínimo, sinónimo)
# emb_2 vs emb_3: ~0.85 (cambio mediano, mismo verbo)
# emb_3 vs emb_4: ~0.65 (cambio total, dominio diferente)
Propiedad útil: Permite encontrar textos "casi iguales" (paráfrasis, near-duplicates).
Comparando métricas de distancia
Cosine Similarity vs Euclidean Distance vs Dot Product:
import numpy as np
# Dos vectores ejemplo
vec_a = np.array([1.0, 2.0, 3.0])
vec_b = np.array([2.0, 4.0, 6.0]) # Misma dirección, doble magnitud
# 1. Cosine Similarity (ángulo)
cos_sim = np.dot(vec_a, vec_b) / (np.linalg.norm(vec_a) * np.linalg.norm(vec_b))
print(f"Cosine similarity: {cos_sim}") # 1.0 (idénticos en dirección)
# 2. Euclidean Distance (distancia geométrica)
euclidean = np.linalg.norm(vec_a - vec_b)
print(f"Euclidean distance: {euclidean}") # ~3.74 (diferentes en magnitud)
# 3. Dot Product (producto punto)
dot_prod = np.dot(vec_a, vec_b)
print(f"Dot product: {dot_prod}") # 28.0 (considera magnitud)
Interpretación:
| Métrica | Considera Dirección | Considera Magnitud | Uso en AI |
|---|---|---|---|
| Cosine Similarity | ✅ Sí | ❌ No | ✅ Semantic search (estándar) |
| Euclidean Distance | ✅ Sí | ✅ Sí | ⚠️ Clustering (K-Means) |
| Dot Product | ✅ Sí | ✅ Sí | ⚠️ Ranking (si embeddings normalizados) |
Recomendación: Cosine similarity para semantic search (99% de casos).
Dimensionalidad y compresión
Trade-off: Más dimensiones = más información
# OpenAI modelos:
# text-embedding-3-small: 1536 dims → Balance costo/calidad
# text-embedding-3-large: 3072 dims → Mejor calidad, más caro
¿Podemos reducir dimensiones?
from sklearn.decomposition import PCA
import numpy as np
# Embeddings originales (1536 dims)
embeddings = np.array([get_embedding(text) for text in texts])
# Reducir a 128 dims con PCA
pca = PCA(n_components=128)
embeddings_compressed = pca.fit_transform(embeddings)
print(f"Original: {embeddings.shape}") # (N, 1536)
print(f"Comprimido: {embeddings_compressed.shape}") # (N, 128)
# Trade-off: Pierdes ~10-20% de información semántica
Cuándo comprimir:
- Almacenar millones de embeddings (ahorrar memoria)
- Búsqueda más rápida (menos dimensiones = cálculo más rápido)
Cuándo NO comprimir:
- Precisión crítica
- Corpus pequeño (<100K docs)
Ejercicios
Ejercicio 1: Calcular cosine similarity
Calcula la cosine similarity entre estos dos vectores:
import numpy as np
vec_a = np.array([3.0, 4.0, 0.0])
vec_b = np.array([4.0, 3.0, 0.0])
# ¿Cuál es la cosine similarity?
Ver solución
def cosine_similarity(vec_a, vec_b):
dot_product = np.dot(vec_a, vec_b)
norm_a = np.linalg.norm(vec_a)
norm_b = np.linalg.norm(vec_b)
return dot_product / (norm_a * norm_b)
vec_a = np.array([3.0, 4.0, 0.0])
vec_b = np.array([4.0, 3.0, 0.0])
similarity = cosine_similarity(vec_a, vec_b)
print(f"Cosine similarity: {similarity}") # 0.96
Explicación:
- Dot product: (3×4) + (4×3) + (0×0) = 24
- Norm A: sqrt(3² + 4²) = 5.0
- Norm B: sqrt(4² + 3²) = 5.0
- Cosine: 24 / (5.0 × 5.0) = 0.96
Interpretación: 0.96 indica vectores muy similares (casi misma dirección).
Ejercicio 2: Normalizar embeddings
Normaliza este embedding a longitud unitaria:
import numpy as np
embedding = np.array([6.0, 8.0])
# Magnitud actual: sqrt(6² + 8²) = 10.0
# Normaliza a magnitud = 1.0
Ver solución
def normalize(vec):
return vec / np.linalg.norm(vec)
embedding = np.array([6.0, 8.0])
embedding_normalized = normalize(embedding)
print(f"Original: {embedding}")
print(f"Normalizado: {embedding_normalized}")
print(f"Magnitud normalizada: {np.linalg.norm(embedding_normalized)}")
Output:
Original: [6. 8.]
Normalizado: [0.6 0.8]
Magnitud normalizada: 1.0
Explicación:
- Magnitud original: sqrt(36 + 64) = 10.0
- Normalización: [6/10, 8/10] = [0.6, 0.8]
- Nueva magnitud: sqrt(0.36 + 0.64) = 1.0 ✅
Ejercicio 3: Encontrar más similar
Dado un query embedding, encuentra el documento más similar:
import numpy as np
query_emb = np.array([0.5, 0.3, 0.8])
docs_emb = {
"doc_1": np.array([0.5, 0.3, 0.7]),
"doc_2": np.array([0.1, 0.9, -0.2]),
"doc_3": np.array([0.4, 0.3, 0.8])
}
# ¿Cuál documento es más similar al query?
Ver solución
def cosine_similarity(vec_a, vec_b):
return np.dot(vec_a, vec_b) / (np.linalg.norm(vec_a) * np.linalg.norm(vec_b))
query_emb = np.array([0.5, 0.3, 0.8])
docs_emb = {
"doc_1": np.array([0.5, 0.3, 0.7]),
"doc_2": np.array([0.1, 0.9, -0.2]),
"doc_3": np.array([0.4, 0.3, 0.8])
}
# Calcular similaridades
similarities = {}
for doc_id, doc_emb in docs_emb.items():
sim = cosine_similarity(query_emb, doc_emb)
similarities[doc_id] = sim
print(f"{doc_id}: {sim:.4f}")
# Encontrar el más similar
most_similar = max(similarities, key=similarities.get)
print(f"\nMás similar: {most_similar}")
Output:
doc_1: 0.9960
doc_2: 0.3511
doc_3: 0.9980
Más similar: doc_3
Explicación: doc_3 tiene cosine similarity de 0.998 con el query (casi idénticos).
Ejercicio 4: Predecir similaridad
¿Cuál par debería tener mayor cosine similarity?
Par A:
- "El perro ladra fuerte"
- "El can ladra ruidosamente"
Par B:
- "Python es popular"
- "JavaScript es popular"
Ver solución
Respuesta: Par A debería tener mayor similaridad
Razones:
- Par A: Paráfrasis (perro=can, fuerte=ruidosamente)
- Similaridad esperada: ~0.92-0.96
- Par B: Mismo verbo ("es popular") pero sujetos diferentes (lenguajes distintos)
- Similaridad esperada: ~0.85-0.90
Los embeddings capturan:
- Sinónimos (perro/can)
- Paráfrasis (fuerte/ruidosamente)
- Estructura similar
Par A es más parecido que Par B (misma idea expresada diferente vs ideas relacionadas).
Troubleshooting común
Problema 1: Similaridades inesperadamente bajas
# Esperas 0.90+ pero obtienes 0.70
similarity = cosine_similarity(emb_1, emb_2) # → 0.70
Causas posibles:
- Textos NO son tan similares como crees (embeddings son correctos)
- Modelos diferentes usados (incomparables)
- Typos o ruido en texto (afecta embeddings)
Solución: Inspecciona los textos originales.
Problema 2: Todos los embeddings son similares
# Todos los scores entre 0.80-0.85
for doc_emb in docs:
print(cosine_similarity(query_emb, doc_emb)) # 0.82, 0.83, 0.84...
Causa: Corpus muy homogéneo (todos los docs del mismo tema).
Solución: Normal si tu corpus es especializado (e.g., todos docs técnicos de Python).
Problema 3: Similaridad negativa
similarity = cosine_similarity(emb_a, emb_b) # → -0.15
Interpretación: Vectores apuntan en direcciones opuestas (ángulo >90°).
En práctica: Raro con embeddings modernos (OpenAI, SBERT).
Si ocurre: Textos son MUY diferentes conceptualmente (e.g., "love" vs "hate").
Resumen
Qué aprendiste:
- ✅ Similaridad semántica: Textos similares → embeddings cercanos
- ✅ Cosine similarity: Métrica estándar (mide ángulo, ignora magnitud)
- ✅ Direccionalidad: Dirección del vector = significado semántico
- ✅ Magnitud vs Dirección: Para semantic search, dirección importa (normalizar embeddings)
- ✅ Clustering natural: Embeddings se agrupan solos por tema
- ✅ Métricas comparadas: Cosine > Euclidean para semantic search
Conceptos clave:
- Cosine similarity retorna valores entre -1 y 1 (típicamente 0.6-1.0 para textos relacionados)
- Normalizar embeddings hace dot product = cosine similarity (más eficiente)
- Embeddings crean clustering natural sin supervisión
Recursos adicionales
- Cosine Similarity Explained - Explicación visual
- Vector Space Models - Stanford NLP
- Similarity Metrics - Comparación completa
- Clustering with Embeddings - Sentence-BERT
- OpenAI Embeddings Best Practices - Casos de uso
En la siguiente cápsula
Cápsula 04: Espacios Vectoriales
Aprenderás:
- High-dimensional spaces (1536 dimensiones)
- Por qué embeddings necesitan tantas dimensiones
- Proximity en espacio vectorial = similaridad semántica
- Clustering natural en alta dimensionalidad
De propiedades vectoriales a geometría de espacios.
Módulo 1 - Embeddings Deep Dive Guide Vectores que capturan significado