Similitud coseno y Jaccard: midiendo semejanzas en Python
Cuando trabajas con datos, a menudo quieres poder cuantificar qué tan similares o diferentes son dos objetos. Por ejemplo, si estás construyendo un sistema de recomendación, necesitas poder medir la similitud entre dos usuarios o entre un usuario y un elemento para hacer buenas recomendaciones.
Yo he estado durante las últimas semanas trabajando mucho con esto, comprendiendo como construir un buen sistema de recomendación para Deeditt, y aunque hay muchas librerias, todas tienen enfoques diferentes y se prestan para conjuntos de datos muy grandes, por lo que por esa razon, opte por un sistema muy básico utilizand los conceptos que explicará a continuación, dos de las cuales son el cosine similarity y el jaccard score del paquete scikit-learn.
¿Qué es un vector?
Un vector es una representación de datos en forma de magnitudes numéricas dentro de un espacio multidimensional. Por ejemplo, un vector de 10 dimensiones para representar características de una película podría ser:
[comedia: 0.7, acción: 0.3, drama: 0.1, duración: 2h, criticas_positivas: 8.5, presupuesto: 90M, ...)
Cada dimensión captura un atributo, característica o propiedad del ítem. Esta representación vectorial permite aplicar operaciones matriciales y de álgebra lineal para comparar similitudes de forma eficiente.
TF-IDF
TF-IDF (term frequency–inverse document frequency) es una representación vectorial muy popular en recuperación de información y machine learning.
Funciona vectorizando el texto de los documentos (por ejemplo, descripción de productos, artículos, etc.) en base a la frecuencia de las palabras, dandole más peso a aquellos términos más distintivos y menos frecuentes en el conjunto completo.
Permite transformar datos textuales en representaciones vectoriales numéricas sobre las que luego aplicar algoritmos, básicamente TF-IDF genera una representación vectorial a partir de texto:
TF-IDF (term frequency–inverse document frequency) es una técnica que transforma colecciones de documentos textuales en representaciones vectoriales numéricas.
Por ejemplo, imaginemos que tenemos dos descripciones de películas:
pelicula1 = "Una comedia romántica sobre dos amigos que se enamoran"
pelicula2 = "Una historia de superación personal sobre un deportista que busca éxito"
TF-IDF vectoriza estos textos convirtiendo las palabras (términos) en dimensiones de un vector. Cada dimensión contará el peso de ese término en el documento, basado en su frecuencia y relevancia en todo el conjunto.
Más formalmente, genera vectores así:
from sklearn.feature_extraction.text import TfidfVectorizer
descriptions = [
"Una comedia romántica sobre dos amigos",
"Una historia de superación personal sobre un deportista"
]
# Create TfidfVectorizer
vectorizer = TfidfVectorizer()
# Generate tf-idf vectors
tfidf_vectors = vectorizer.fit_transform(descriptions)
print(tfidf_vectors.shape)
# (2, 10) - 2 vectores de 10 dimensiones
Cada dimensión es un término relevante, con su peso TF-IDF asociado en los documentos.
Algo muy importante que no se debe mal interpretar es que el número de dimensiones del vector TF-IDF que se obtiene no está fijo a 10, esto dependerá de los datos y puede configurarse.
Al aplicar TF-IDF sobre un conjunto de documentos textuales, este analizará los textos para extraer los términos (palabras) más relevantes. Cada uno de estos términos se convertirá en una dimensión del vector.
Por tanto, el número total de dimensiones (en el ejemplo eran 10) viene dado por la cantidad de términos más relevantes que extraiga TF-IDF del conjunto de textos que se le aplique.
Algunos hiperparámetros que permiten controlar esto son:
vectorizer = TfidfVectorizer(max_features=100)
# Extraer máximo 100 términos
vectorizer = TfidfVectorizer(min_df=5, max_df=0.95)
# Términos con frecuencia entre 5 y 95% de los documentos
Y al hablar de "términos" me refiero a las palabras individuales que conforman el texto de los documentos, por ejemplo, si tenemos 2 documentos:
doc1 = "La película tenía una trama interesante"
doc2 = "La cinta cinematográfica poseía una narrativa cautivadora"
Algunos de los términos (palabras) relevantes que TF-IDF extraería serían:
- película
- cinta
- cinematográfica
- trama
- narrativa
- interesante
- cautivadora
Y cada uno de estos términos se convertiría en una dimensión/característica del vector final TF-IDF.
Así que cuando configuramos min_df=5
, le estamos diciendo a TF-IDF: "Incluye solo los términos (palabras) que aparezcan en al menos el 5% de los documentos". Esto ayuda a filtrar términos muy raros o poco frecuentes.
Y max_df=0.95
indicaría: "Incluye solo los términos que aparezcan como máximo en el 95% de los documentos". Esto filtra términos demasiado comunes que no aportan mucho.
Similitud coseno
La similitud coseno compara dos vectores de características y cuantifica su semejanza midiéndolos en un espacio multidimensional común.
Por ejemplo, imaginemos dos vectores que representan películas en un espacio de 3 dimensiones:
película1 = [comedia: 0.9, drama: 0.3, acción: 0.1]
película2 = [comedia: 0.7, drama: 0.5, acción: 0.8]
Cada dimensión mide el grado/peso de esa característica en la película.
Si proyectamos estos vectores en sus coordenadas dentro del espacio 3D de géneros, tendrían una orientación. La similitud coseno compara sus direcciones relativas midiendo el ángulo entre ellos.
Si los vectores apuntan en la misma dirección (ángulo 0 grados) la similitud es 1. Si son perpendiculares (90 grados) la similitud es 0. Valores intermedios indican semejanza parcial de acuerdo al ángulo.
La similitud coseno varía entre 0 y 1, donde:
- 1 significa máxima similitud (vectores idénticos)
- 0 significa mínima similitud (vectores totalmente diferentes)
Por tanto, cuanto MÁS cercana sea la similitud coseno a 1, MÁS parecidos serán los vectores.
Es decir, vectores más similares tendrán un ángulo más pequeño entre ellos y por tanto una puntuación de similitud coseno cercana a 1.
En cambio, cuanto MÁS cercana sea la similitud coseno a 0, MENOS parecidos son los vectores, indicando que el ángulo entre ellos es mayor.
Se calcula así:
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
vec1 = np.array([1, 2, 3])
vec2 = np.array([4, 5, 6])
cos_sim = cosine_similarity([vec1], [vec2])
print(cos_sim)
# [[0]]
En este ejemplo, tenemos dos vectores y sus dimensiones (1,2,3) vs (4,5,6), las cuales no coinciden, no tienen valores en común. Por tanto, sklearn correctamente calcula una similitud coseno de 0, indicando que son completamente distintos.
Aplicado a un caso diferente, podríamos vectorizar las descripciones de películas y las preferencias de los usuarios usando TF-IDF. Luego calcular la similitud coseno entre estos vectores encontraría películas similares a lo que le gusta al usuario.
Jaccard similarity
La similitud de Jaccard mide qué tan parecidos son dos conjuntos de elementos. Analiza cuántos elementos tienen en común entre los dos conjuntos.
Por ejemplo, dado:
Conjunto 1 = {1, 2, 3, 4, 5}
Conjunto 2 = {3, 4, 5, 6, 7}
Compara cuántos elementos se repiten en ambos conjuntos (la intersección):
Intersección = {3, 4, 5}
Luego compara cuántos elementos hay en total entre los dos conjuntos (la unión):
Unión = {1, 2, 3, 4, 5, 6, 7}
Finalmente, divide el tamaño de la intersección sobre la unión para calcular el puntaje de similitud Jaccard.
Así que básicamente cuantifica la proporción de elementos que tienen en común entre los conjuntos.
similitud_jaccard = tamaño_intersección / tamaño_union
Por ejemplo:
from sklearn.metrics import jaccard_score
set1 = {1, 2, 3}
set2 = {2, 3, 4}
jaccard_sim = jaccard_score(set1, set2)
print(jaccard_sim)
# 0.667
Valores más cercanos a 1 indican mayor similitud. Se utiliza normalmente para comparar similitud basada en etiquetas, palabras clave, u otros atributos categóricos.
Un caso de uso es comparar los tags que a los usuarios les gustan versus los tags de los ítems para hacer recomendaciones:
user_tags = {'comedia', 'acción', 'aventura'}
movie1_tags = {'comedia', 'romance'}
movie2_tags = {'comedia', 'aventura'}
sim_movie1 = jaccard_score(user_tags, movie1_tags)
sim_movie2 = jaccard_score(user_tags, movie2_tags)
print(sim_movie1)
# 0.33
print(sim_movie2)
# 0.66
Aquí vemos que movie2 es más similar al usuario en base a tags compartidos.
Usos en sistemas de recomendación
Tanto la similitud coseno como Jaccard son muy útiles en varios tipos de sistemas de recomendación:
-
Recomendación basado en contenido: se calcula la similitud entre los atributos/contenidos de los ítems comparados con lo que le gusta al usuario. Por ejemplo, para películas sería la descripción, género, director, actores, etc.
-
Recomendación colaborativa filtrada: se compara al usuario activo contra otros usuarios en base a calificaciones o preferencias. Los usuarios más similares formarían su vecindario.
-
Recomendación híbrida: se construye un score final combinando diferentes similitudes, tanto de contenido como colaborativas.
Por ejemplo, un sistema híbrido de películas podría calcular:
- Similitud coseno entre vector de preferencias del usuario y vectores TF-IDF de datos de películas.
- Similitud Jaccard de los géneros preferredos por el usuario versus géneros de las películas.
- Similitud coseno contra usuarios con gustos similares.
Y combinar estas métricas en un score final para recomendación.
Conclusión
La similitud coseno y Jaccard son dos formas populares de medir que tan parecidos o diferentes son dos vectores o conjuntos de datos.
Como lo mencione al principio, tengo ya algunas semanas trabajando con esto y ha sido un viaje lleno de aprendizaje porque es aun un campo nuevo para mi, pero lo estoy disfrutando, y para mi suerte, Python tiene muchas librerias que encapsulan muchos de estos conceptos, los cuales se pueden aplicar de forma sencilla, aunque en terminos de fiabilidad en el retorno de los datos, ese ya es otro punto en que hay que hacer mucha prueba y error hasta encontrar los parametros que mejor se adapten a la solucion.
Yo por ahora les puedo adelantar que he utilizado Cosine Similarity, Jaccard Score, LightFM y TextBlob para crear mi propio sistema, todavia no esta publicado y su lanzamiendo dependera de la cantidad de muestras que tenga, pero al menos un borrador ya esta escrito.
Happy coding! :D
Photo by Olav Ahrens Røtne on Unsplash