5 · Similitud coseno
¿Cómo mides 'cercano' para embeddings? La similitud coseno compara dirección, no magnitud — la métrica por defecto para vectores de texto, e igual al producto punto una vez normalizados los vectores.
La similitud coseno mide el ángulo entre dos vectores: es el producto punto dividido por ambas longitudes, de -1 (opuestos) a 1 (misma dirección). Para texto supera a la distancia euclidiana porque ignora la magnitud — solo importa la dirección (el significado). En vectores L2-normalizados, la similitud coseno es igual al producto punto simple.
Sin esto:
Elige la métrica equivocada y un documento largo parece 'lejano' de una consulta corta sobre el mismo tema solo porque tiene más palabras — tu búsqueda rankea por longitud en vez de por significado.
Una vez que el texto es un vector, "similar" necesita una definición precisa. Dos candidatas:
-
Distancia euclidiana
||a - b||— la distancia en línea recta entre los dos puntos. Distancia pequeña = similar. Pero es sensible a la magnitud: un vector del doble de largo está "lejos" aunque apunte exactamente igual. -
Similitud coseno
cos(a, b) = (a · b) / (||a|| · ||b||)— el coseno del ángulo entre los vectores. Va de +1 (misma dirección), pasando por 0 (perpendiculares / no relacionados), hasta -1 (opuestos). Es invariante a la magnitud: escalar un vector no cambia su dirección, así que no cambia el puntaje.
Para texto, la dirección codifica significado y la magnitud codifica mayormente longitud/énfasis — por eso el coseno es la elección estándar. Una consulta de 3 palabras y un documento de 300 palabras sobre el mismo tema apuntan igual; el coseno los ve similares, mientras que la distancia euclidiana penalizaría al documento solo por ser largo.
El truco elegante: si L2-normalizas cada vector primero (pones ||a|| = 1), entonces cos(a, b) = a · b — el denominador es solo 1 × 1. Por eso las bases de datos vectoriales en producción normalizan una vez al insertar y luego usan el producto punto barato en cada consulta.
Python (in browser)
cosine_similarity rankea los documentos de mascotas por encima de deportes/finanzas/clima, no cambia al escalar un vector 5×, y — en vectores normalizados — es igual al producto punto simple.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Una sutileza: la similitud coseno y la distancia euclidiana coinciden en el ranking una vez que los vectores están normalizados — para vectores unitarios, ||a - b||² = 2 - 2·cos(a, b), así que menor distancia ⇔ mayor coseno. La diferencia importa más en vectores sin normalizar, donde la euclidiana se deja llevar por la longitud. Por eso casi toda base de datos vectorial o normaliza por ti o expone el coseno como la métrica por defecto para texto.
Embeddings reales, misma métrica: normaliza, luego coseno (= producto punto). La versión NumPy desde cero que ejecutaste es exactamente lo que calcula `util.cos_sim`.
¿Por qué la similitud coseno suele preferirse sobre la distancia euclidiana para comparar embeddings de texto?
- Similitud coseno = (a·b)/(‖a‖‖b‖), el coseno del ángulo entre vectores, de -1 a 1.
- El coseno ignora la magnitud (compara dirección = significado), por eso supera a la distancia euclidiana para texto.
- En vectores L2-normalizados, el coseno es igual al producto punto — así las bases en producción normalizan una vez y rankean con un solo `vectors @ query`.
Toda base de datos vectorial (Pinecone, Weaviate, pgvector, FAISS) expone el coseno como métrica de texto por defecto; la búsqueda semántica y la recuperación de RAG son ranking por coseno por debajo.
Si lo quitas: Usa distancia euclidiana cruda sobre embeddings sin normalizar y tu búsqueda rankea en parte por longitud de documento, mostrando fragmentos largos irrelevantes por encima de cortos relevantes.