“¡Dije que quería una película clase B, maldita sea!”
¿Cansado de navegar sin parar por Netflix y sin saber qué ver a continuación? ¿Qué pasaría si pudieras crear tu propio sistema de recomendaciones personalizado, impulsado por IA, que prediga tu próxima película favorita con precisión?
En este tutorial, lo guiaremos a través del proceso de creación de un sistema de recomendación de películas utilizando bases de datos vectoriales (VectorDB) . Aprenderá cómo funcionan los motores de recomendación de IA modernos y obtendrá experiencia práctica en la creación de su propio sistema con Superlinked .
(¿Quieres ir directamente al código? Consulta nuestro repositorio en GitHub aquí . ¿Estás listo para probar los sistemas de recomendación para tu propio caso de uso? Obtén una demostración aquí ).
Seguiremos este cuaderno a lo largo del artículo. También puedes ejecutar el código directamente desde tu navegador usando Colab.
El algoritmo de recomendaciones de Netflix hace un buen trabajo al sugerir contenido relevante, dada la gran cantidad de opciones (~16.000 películas y programas de televisión en 2023) y la rapidez con la que tiene que proponer programas a los usuarios. ¿Cómo lo hace Netflix? En una palabra, mediante búsqueda semántica .
La búsqueda semántica comprende el significado y el contexto (tanto los atributos como los patrones de consumo) detrás de las consultas de los usuarios y las descripciones de películas y programas de televisión y, por lo tanto, puede proporcionar una mejor personalización en sus consultas y recomendaciones que los enfoques tradicionales basados en palabras clave.
Sin embargo, la búsqueda semántica plantea ciertos desafíos , entre los que destacan: 1) garantizar resultados de búsqueda precisos, 2) interpretabilidad y 3) escalabilidad, desafíos que cualquier estrategia de recomendación de contenido exitosa deberá abordar. Con la biblioteca de Superlinked, puede superar estas dificultades.
En este artículo, le mostraremos cómo usar la biblioteca Superlinked para configurar su propia búsqueda semántica y generar una lista de películas relevantes según sus preferencias.
La búsqueda semántica aporta mucho valor a la búsqueda vectorial, pero plantea tres desafíos importantes en la integración de vectores para los desarrolladores:
La biblioteca Superlinked le permite abordar estos desafíos. A continuación, crearemos un recomendador de contenido (específicamente para películas), comenzando con la información que tenemos sobre una película determinada, integrando esta información como un vector multimodal, creando un índice de vector que se pueda buscar para todas nuestras películas y luego utilizando ponderaciones de consulta para ajustar nuestros resultados y llegar a buenas recomendaciones de películas. Vamos a ello.
A continuación, realizará una búsqueda semántica en el conjunto de datos de películas de Netflix utilizando los siguientes elementos de la biblioteca Superlinked:
Recomendar películas con éxito es difícil, principalmente porque hay muchas opciones (más de 9000 títulos en 2023) y los usuarios quieren recomendaciones a pedido, de inmediato. Adoptemos un enfoque basado en datos para encontrar algo que queramos ver. En nuestro conjunto de datos de películas, sabemos lo siguiente:
Podemos incrustar estas entradas y crear un índice vectorial sobre nuestras incrustaciones, creando un espacio en el que podemos buscar semánticamente.
Una vez que tengamos nuestro espacio vectorial indexado, haremos lo siguiente:
El primer paso es instalar la biblioteca e importar las clases necesarias.
(Nota: A continuación, cambie alt.renderers.enable(“mimetype”)
a alt.renderers.enable('colab')
si está ejecutando esto en Google Colab . Mantenga “mimetype” si está ejecutando esto en GitHub ).
%pip install superlinked==5.3.0 from datetime import timedelta, datetime import altair as alt import os import pandas as pd from superlinked.evaluation.charts.recency_plotter import RecencyPlotter from superlinked.framework.common.dag.context import CONTEXT_COMMON, CONTEXT_COMMON_NOW from superlinked.framework.common.dag.period_time import PeriodTime from superlinked.framework.common.schema.schema import schema from superlinked.framework.common.schema.schema_object import String, Timestamp from superlinked.framework.common.schema.id_schema_object import IdField from superlinked.framework.common.parser.dataframe_parser import DataFrameParser from superlinked.framework.dsl.executor.in_memory.in_memory_executor import ( InMemoryExecutor, InMemoryApp, ) from superlinked.framework.dsl.index.index import Index from superlinked.framework.dsl.query.param import Param from superlinked.framework.dsl.query.query import Query from superlinked.framework.dsl.query.result import Result from superlinked.framework.dsl.source.in_memory_source import InMemorySource from superlinked.framework.dsl.space.text_similarity_space import TextSimilaritySpace from superlinked.framework.dsl.space.recency_space import RecencySpace alt.renderers.enable("mimetype") # NOTE: to render altair plots in colab, change 'mimetype' to 'colab' alt.data_transformers.disable_max_rows() pd.set_option("display.max_colwidth", 190)
También debemos preparar el conjunto de datos: definir constantes de tiempo, configurar la ubicación URL de los datos, crear un diccionario de almacenamiento de datos, leer el CSV en un DataFrame de pandas, limpiar el DataFrame y los datos para que se puedan buscar correctamente y hacer una verificación y una descripción general rápidas. (Consulte las celdas 3 y 4 para obtener más detalles).
Ahora que el conjunto de datos está preparado, puede optimizar su recuperación utilizando la biblioteca Superlinked.
La biblioteca de Superlinked contiene un conjunto de bloques de construcción básicos que utilizamos para construir un índice y gestionar la recuperación. Puede leer sobre estos bloques de construcción con más detalle aquí .
Primero, debes definir tu esquema para informar al sistema sobre tus datos.
# accommodate our inputs in a typed schema @schema class MovieSchema: description: String title: String release_timestamp: Timestamp genres: String id: IdField movie = MovieSchema()
A continuación, utilice espacios para indicar cómo desea tratar cada parte de los datos al incrustarlos. Los espacios que se utilicen dependerán del tipo de datos. Cada espacio está optimizado para incrustar los datos de modo de devolver la mayor calidad posible de resultados de recuperación.
En las definiciones de espacio, describimos cómo se deben integrar las entradas para reflejar las relaciones semánticas en nuestros datos.
# textual fields are embedded using a sentence-transformers model description_space = TextSimilaritySpace( text=movie.description, model="sentence-transformers/paraphrase-MiniLM-L3-v2" ) title_space = TextSimilaritySpace( text=movie.title, model="sentence-transformers/paraphrase-MiniLM-L3-v2" ) genre_space = TextSimilaritySpace( text=movie.genres, model="sentence-transformers/paraphrase-MiniLM-L3-v2" ) # release date are encoded using our recency space # periodtimes aim to reflect notable breaks in our scores recency_space = RecencySpace( timestamp=movie.release_timestamp, period_time_list=[ PeriodTime(timedelta(days=4 * YEAR_IN_DAYS)), PeriodTime(timedelta(days=10 * YEAR_IN_DAYS)), PeriodTime(timedelta(days=40 * YEAR_IN_DAYS)), ], negative_filter=-0.25, ) movie_index = Index(spaces=[description_space, title_space, genre_space, recency_space])
Una vez que haya configurado sus espacios y creado su índice, utilice las partes de origen y ejecutor de la biblioteca para configurar sus consultas. Consulte las celdas 10 a 13 en el cuaderno .
Ahora que las consultas están preparadas, pasemos a ejecutarlas y optimizar la recuperación ajustando los pesos.
El espacio de actualidad le permite modificar los resultados de su consulta al incluir preferentemente publicaciones más antiguas o más nuevas de su conjunto de datos. Usamos 4, 10 y 40 años como períodos de tiempo para poder dar más importancia a los años con más títulos (consulte la celda 5 ).
Observe las interrupciones en la puntuación a los 4, 10 y 40 años. Los títulos con más de 40 años obtienen una puntuación negative_filter
.
Definamos una función de utilidad rápida para presentar nuestros resultados en el cuaderno.
def present_result( result: Result, cols_to_keep: list[str] = ["description", "title", "genres", "release_year", "id"], ) -> pd.DataFrame: # parse result to dataframe df: pd.DataFrame = result.to_pandas() # transform timestamp back to release year df["release_year"] = [ datetime.fromtimestamp(timestamp).year for timestamp in df["release_timestamp"] ] return df[cols_to_keep]
La biblioteca Superlinked le permite realizar varios tipos de consultas; aquí definimos dos. Ambos tipos de consultas (simples y avanzadas) me permiten evaluar espacios individuales (descripción, título, género y, por supuesto, actualidad) según mis preferencias. La diferencia entre ellos es que con una consulta simple , establezco un texto de consulta y luego obtengo resultados similares en los espacios de descripción, título y género.
Con una consulta avanzada , tengo un control más preciso. Si quiero, puedo ingresar diferentes textos de consulta en cada uno de los espacios de descripción, título y género. Este es el código de consulta:
query_text_param = Param("query_text") simple_query = ( Query( movie_index, weights={ description_space: Param("description_weight"), title_space: Param("title_weight"), genre_space: Param("genre_weight"), recency_space: Param("recency_weight"), }, ) .find(movie) .similar(description_space.text, query_text_param) .similar(title_space.text, query_text_param) .similar(genre_space.text, query_text_param) .limit(Param("limit")) ) advanced_query = ( Query( movie_index, weights={ description_space: Param("description_weight"), title_space: Param("title_weight"), genre_space: Param("genre_weight"), recency_space: Param("recency_weight"), }, ) .find(movie) .similar(description_space.text, Param("description_query_text")) .similar(title_space.text, Param("title_query_text")) .similar(genre_space.text, Param("genre_query_text")) .limit(Param("limit")) )
En consultas simples, configuro el texto de mi consulta y aplico diferentes pesos según su importancia para mí.
result: Result = app.query( simple_query, query_text="Heartfelt romantic comedy", description_weight=1, title_weight=1, genre_weight=1, recency_weight=0, limit=TOP_N, ) present_result(result)
Nuestros resultados contienen algunos títulos que ya he visto. Puedo solucionar esto ponderando la actualidad para sesgar mis resultados hacia los títulos recientes. Las ponderaciones se normalizan para tener una suma unitaria (es decir, todas las ponderaciones se ajustan para que siempre sumen un total de 1), por lo que no tiene que preocuparse por cómo las configura.
result: Result = app.query( simple_query, query_text="Heartfelt romantic comedy", description_weight=1, title_weight=1, genre_weight=1, recency_weight=3, limit=TOP_N, ) present_result(result)
Mis resultados (arriba) ahora son todos posteriores a 2021.
Con la consulta simple, puedo ponderar cualquier espacio específico (descripción, título, género o actualidad) para que cuente más al devolver resultados. Experimentemos con esto. A continuación, le daremos más peso al género y le quitaremos peso al título: el texto de mi consulta es básicamente un género con un poco de contexto adicional. Mantengo la actualidad como está porque me gustaría que mis resultados estuvieran sesgados hacia las películas recientes.
result = app.query( simple_query, query_text="Heartfelt romantic comedy", description_weight=1, title_weight=0.1, genre_weight=2, recency_weight=1, limit=TOP_N, ) present_result(result)
Esta consulta retrasa un poco el año de lanzamiento para brindarme resultados más ponderados por género (abajo).
La consulta avanzada me brinda un control aún más preciso. Mantengo el control sobre la actualidad, pero también puedo especificar el texto de búsqueda para la descripción, el título y el género, y asignar a cada uno un peso específico según mis preferencias, como se muestra a continuación (y en las celdas 19 a 21 ).
result = app.query( advanced_query, description_query_text="Heartfelt lovely romantic comedy for a cold autumn evening.", title_query_text="love", genre_query_text="drama comedy romantic", description_weight=0.2, title_weight=3, genre_weight=1, recency_weight=5, limit=TOP_N, ) present_result(result)
Digamos que en mis últimos resultados de películas encontré una película que ya vi y me gustaría ver algo similar. Supongamos que me gusta White Christmas, una comedia romántica de 1954 (id = tm16479) sobre cantantes y bailarines que se juntan para un espectáculo teatral para atraer invitados a una posada en dificultades de Vermont. Al agregar una cláusula with_vector
adicional (con un parámetro movie_id
) a advanced_query, with_movie_query me permite buscar usando esta película (o cualquier película que me guste) y me brinda todo el control detallado de un texto de consulta de subbúsqueda independiente y ponderación.
Primero, agregamos nuestro parámetro movie_id:
with_movie_query = advanced_query.with_vector(movie, Param("movie_id"))
Y luego puedo configurar mis otras consultas de subbúsqueda como vacías o como sea más relevante, junto con cualquier ponderación que tenga sentido. Digamos que mi primera consulta devuelve resultados que reflejan el aspecto de actuación/banda en el escenario de White Christmas (ver celda 24 ), pero quiero ver una película más orientada a la familia. Puedo ingresar un description_query_text para sesgar mis resultados en la dirección deseada.
result = app.query( with_movie_query, description_query_text="family", title_query_text="", genre_query_text="", description_weight=1, title_weight=0, genre_weight=0, recency_weight=0, description_query_weight=1, movie_id="tm16479", limit=TOP_N, ) present_result(result)
Pero ahora que veo los resultados, me doy cuenta de que en realidad tengo más ganas de algo más ligero y divertido. Ajustemos mi consulta en consecuencia:
Result = app.query( with_movie_query, description_query_text="", title_query_text="", genre_query_text="comedy", description_weight=1, title_weight=0, genre_weight=2, recency_weight=0, description_query_weight=1, movie_id="tm16479", limit=TOP_N, ) present_result(result)
Bueno, esos resultados son mejores. Elegiré uno de estos. ¡Pon las palomitas de maíz a hervir!
Superlinked facilita la realización de pruebas, iteraciones y mejoras en la calidad de la recuperación. Más arriba, le mostramos cómo utilizar la biblioteca Superlinked para realizar una búsqueda semántica en un espacio vectorial, como lo hace Netflix, y devolver resultados de películas precisos y relevantes. También vimos cómo ajustar nuestros resultados, modificando los pesos y los términos de búsqueda hasta obtener el resultado correcto.
¡Ahora prueba el cuaderno tú mismo y descubre lo que puedes lograr!
Los motores de recomendación están cambiando la forma en que descubrimos contenido. Ya sean películas, música o productos, la búsqueda vectorial es el futuro y ahora tienes las herramientas para crear la tuya.
Autor: Mór Kapronczay