"Jag sa att jag ville ha en B-film för fan!"
Trött på att oändligt rulla igenom Netflix, osäker på vad du ska se härnäst? Tänk om du kunde bygga ditt eget anpassade, AI-drivna rekommendationssystem som förutsäger din nästa favoritfilm med precision?
I den här handledningen guidar vi dig genom processen att skapa ett filmrekommendationssystem med hjälp av vektordatabaser (VectorDBs) . Du lär dig hur moderna AI-rekommendationsmotorer fungerar och får praktisk erfarenhet av att bygga ditt eget system med Superlinked .
(Vill du hoppa direkt till koden? Kolla in vår repo på GitHub här . Är du redo att prova rekommendatorsystem för ditt eget bruk? Skaffa en demo här .)
Vi kommer att följa den här anteckningsboken genom hela artikeln. Du kan också köra koden direkt från din webbläsare med hjälp av Colab.
Netflix rekommendationsalgoritm gör ett ganska bra jobb med att föreslå relevant innehåll - med tanke på den stora mängden alternativ (~16 000 filmer och TV-program 2023) och hur snabbt den måste föreslå program till användare. Hur gör Netflix det? Med ett ord, semantisk sökning .
Semantisk sökning förstår innebörden och sammanhanget (både attribut och konsumtionsmönster) bakom användarfrågor och beskrivningar av filmer/TV-program, och kan därför ge bättre personalisering i sina frågor och rekommendationer än traditionella sökordsbaserade tillvägagångssätt.
Men semantisk sökning innebär vissa utmaningar - främst bland dem: 1) säkerställa korrekta sökresultat, 2) tolkningsbarhet och 3) skalbarhet - utmaningar som alla framgångsrika innehållsrekommendationer måste hantera. Genom att använda Superlinkeds bibliotek kan du övervinna dessa svårigheter.
I den här artikeln visar vi dig hur du använder Superlinked-biblioteket för att ställa in din egen semantiska sökning och generera en lista över relevanta filmer baserat på dina preferenser.
Semantisk sökning förmedlar mycket värde i vektorsökning men innebär tre betydande utmaningar för inbäddning av vektorer för utvecklare:
Superlinked-biblioteket gör att du kan hantera dessa utmaningar. Nedan bygger vi en innehållsrekommendator (specifikt för filmer), börjar med information vi har om en given film, bäddar in denna information som en multimodal vektor, bygger ut ett sökbart vektorindex för alla våra filmer och använder sedan frågevikter för att justera våra resultat och komma fram till bra filmrekommendationer. Låt oss gå in i det.
Nedan utför du en semantisk sökning på Netflix-filmdataset med hjälp av följande element i Superlinked-biblioteket:
Att framgångsrikt rekommendera filmer är svårt, främst för att det finns så många alternativ (>9000 titlar 2023), och användare vill ha rekommendationer på begäran, omedelbart. Låt oss ta ett datadrivet tillvägagångssätt för att hitta något vi vill titta på. I vår datauppsättning av filmer känner vi till:
Vi kan bädda in dessa ingångar och sätta ihop ett vektorindex ovanpå våra inbäddningar, vilket skapar ett utrymme som vi kan söka semantiskt.
När vi har vårt indexerade vektorutrymme kommer vi:
Ditt första steg är att installera biblioteket och importera de erforderliga klasserna.
(Obs: Nedan ändrar du alt.renderers.enable(“mimetype”)
till alt.renderers.enable('colab')
om du kör detta i google colab . Behåll "mimetype" om du kör i 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)
Vi behöver också förbereda datamängden - definiera tidskonstanter, ställa in URL-platsen för datan, skapa en datalagringsordbok, läsa in CSV:en i en pandas DataFrame, rensa dataramen och data så att den kan sökas ordentligt, och gör en snabb verifiering och översikt. (Se cell 3 och 4 för detaljer.)
Nu när datasetet är förberett kan du optimera din hämtning med hjälp av Superlinked-biblioteket.
Superlinkeds bibliotek innehåller en uppsättning kärnbyggstenar som vi använder för att konstruera ett index och hantera hämtning. Du kan läsa mer om dessa byggstenar här .
Först måste du definiera ditt Schema för att berätta för systemet om dina data.
# accommodate our inputs in a typed schema @schema class MovieSchema: description: String title: String release_timestamp: Timestamp genres: String id: IdField movie = MovieSchema()
Därefter använder du Spaces för att säga hur du vill behandla varje del av data när du bäddar in. Vilka utrymmen som används beror på din datatyp. Varje utrymme är optimerat för att bädda in data för att ge högsta möjliga kvalitet på hämtningsresultaten.
I rymddefinitioner beskriver vi hur indata ska vara inbäddade för att spegla de semantiska sambanden i vår data.
# 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])
När du har ställt in dina utrymmen och skapat ditt index använder du käll- och exekveringsdelen av biblioteket för att ställa in dina frågor. Se cellerna 10-13 i anteckningsboken .
Nu när frågorna är förberedda, låt oss gå vidare till att köra frågor och optimera hämtning genom att justera vikter.
Nyhetsutrymmet låter dig ändra resultaten av din fråga genom att i första hand dra in äldre eller nyare versioner från din datauppsättning. Vi använder 4, 10 och 40 år som våra periodtider så att vi kan ge år med fler titlar mer fokus - se cell 5 ).
Lägg märke till pauserna i poängen vid 4, 10 och 40 år. Titlar äldre än 40 år får en negative_filter
-poäng.
Låt oss definiera en snabbfunktion för att presentera våra resultat i anteckningsboken.
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]
Superlinked-biblioteket låter dig utföra olika typer av frågor; här definierar vi två. Båda våra frågetyper av frågeställningar (enkla och avancerade) låter mig väga individuella utrymmen (beskrivning, titel, genre och naturligtvis nycitet) enligt mina preferenser. Skillnaden mellan dem är att med en enkel fråga ställer jag in en frågetext och visar sedan liknande resultat i beskrivningen, titeln och genren.
Med en avancerad fråga har jag mer finkornig kontroll. Om jag vill kan jag ange olika frågetexter i var och en av beskrivnings-, titel- och genreutrymmen. Här är frågekoden:
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")) )
I enkla frågor ställer jag in min frågetext och lägger olika vikter beroende på deras betydelse för mig.
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)
Våra resultat innehåller några titlar som jag redan har sett. Jag kan hantera detta genom att vikta nycitet för att påverka mina resultat mot de senaste titlarna. Vikter är normaliserade till att ha en enhetssumma (dvs alla vikter justeras så att de alltid summeras till totalt 1), så du behöver inte oroa dig för hur du ställer in dem.
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)
Mina resultat (ovan) är nu alla efter 2021.
Med den enkla frågan kan jag vikta vilket specifikt utrymme som helst (beskrivning, titel, genre eller senaste nytt) för att få det att räknas mer när jag returnerar resultat. Låt oss experimentera med detta. Nedan kommer vi att ge mer tyngd åt genren och nedviktstiteln - min frågetext är i princip bara en genre med lite extra sammanhang. Jag behåller min uppdatering som den är eftersom jag fortfarande vill att mina resultat ska vara partiska mot de senaste filmerna.
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)
Den här frågan skjuter tillbaka releaseåret lite för att ge mig mer genreviktade resultat (nedan).
Den avancerade frågan ger mig ännu mer finkornig kontroll. Jag behåller kontrollen över senaste tiden, men kan också ange söktext för beskrivning, titel och genre, och tilldela var och en en specifik vikt enligt mina preferenser, enligt nedan (och celler 19-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)
Säg i mina senaste filmresultat, jag hittade en film som jag redan har sett och skulle vilja se något liknande. Låt oss anta att jag gillar White Christmas, en romantisk komedi från 1954 (id = tm16479) om sångare-dansare som samlas för en scenshow för att locka gäster till ett kämpande värdshus i Vermont. Genom att lägga till en extra with_vector
-sats (med en movie_id
parameter) till advanced_query, låter with_movie_query mig söka med den här filmen (eller vilken film jag gillar), och ger mig all den finkorniga kontrollen av separat undersökningsfrågetext och viktning.
Först lägger vi till vår movie_id-parameter:
with_movie_query = advanced_query.with_vector(movie, Param("movie_id"))
Och sedan kan jag ställa in mina andra undersökningar till antingen tomma eller vad som är mest relevant, tillsammans med alla viktningar som är vettiga. Låt oss säga att min första fråga ger resultat som återspeglar scenframträdandet/bandaspekten av White Christmas (se cell 24 ), men jag vill se en film som är mer familjeorienterad. Jag kan ange en description_query_text för att snedställa mina resultat i önskad riktning.
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)
Men nu när jag ser mina resultat inser jag att jag faktiskt är mer på humör för något lättsamt och roligt. Låt oss justera min fråga därefter:
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)
Okej, de här resultaten är bättre. Jag väljer en av dessa. Sätt på popcornen!
Superlinked gör det enkelt att testa, iterera och förbättra din hämtningskvalitet. Ovan har vi gått igenom hur du använder Superlinked-biblioteket för att göra en semantisk sökning på ett vektorutrymme, som Netflix gör, och returnera korrekta, relevanta filmresultat. Vi har också sett hur vi finjusterar våra resultat, justerar vikter och söktermer tills vi kommer till precis rätt resultat.
Prova nu anteckningsboken själv och se vad du kan uppnå!
Rekommendationsmotorer formar hur vi upptäcker innehåll. Oavsett om det är filmer, musik eller produkter är vektorsökning framtiden – och nu har du verktygen för att bygga din egen.
Författare: Mór Kapronczay