Vad jag Lärt mig av att Bygga Interaktiva Visualiseringar av Inbäddningar

Under de senaste åren har jag byggt flera olika interaktiva visualiseringar av inbäddningar. Jag har ett stort intresse av att utforska och förstå koncept på ett fritt och öppet sätt, och dessa typer av verktyg känns som ett mycket effektivt sätt att underlätta en sådan upplevelse. Mitt arbete inom detta område började som ett experiment med data jag samlade in för ett annat projekt. Sedan har jag upprepat processen för andra liknande projekt, justerat implementationen baserat på vad som fungerade bra och vad som inte gjorde det. Efter att ha avslutat min senaste insats, tror jag att jag har utvecklat en solid process för att bygga högkvalitativa interaktiva visualiseringar av inbäddningar för en mängd olika typer av enhetsrelationer. Jag har sammanställt detaljer om hela processen från början till slut tillsammans med mina personliga observationer om vad som fungerar och vad som inte gör det här. Min förhoppning är att det kommer att vara intressant eller användbart för någon som försöker bygga liknande verktyg själv.

Bakgrund om Inbäddningar + Visualiseringar av Inbäddningar

Inbäddningar är i grunden ett sätt att representera enheter som punkter i ett N-dimensionellt rum. Dessa enheter kan vara saker som ord, produkter, människor, tweets - egentligen vad som helst som kan relateras till något annat. Idén är att välja koordinater för varje enhet så att liknande/relaterade enheter är nära varandra och vice versa. Mycket av tiden när inbäddningar hänvisas till, är det inom kontexten av djupinlärning. Ordsinbäddningar är en kritisk del av språkmodeller och som sådana har de fått en hel del uppmärksamhet nyligen. Dessa inbäddningar är vanligtvis ganska högdimensionella med hundratals eller tusentals dimensioner, vilket innebär att varje enhet tilldelas en koordinat med upp till tusentals värden. Dessa stora antal dimensioner ger inbäddningarna möjligheten att koda komplexa relationer mellan enheter annat än bara liknande/olika. Om två saker delar någon abstrakt kvalitet med varandra men annars är olika, kan de vara nära varandra i vissa dimensioner medan de förblir avlägsna i andra. Detta möjliggör några av de mycket coola exempel som det berömda kungen + man - kvinna = drottning och gör inbäddningar så användbara för att koda rik information om saker.

Den mänskliga hjärnan har svårt att intuitivt förstå högdimensionella data som detta. Av denna anledning, när man visualiserar inbäddningar, är det vanligt att "projicera" inbäddningar ner till 3 eller 2 dimensioner för att kunna tolka deras struktur visuellt och förstå relationerna mellan de enheter det representerar. Genom mitt arbete och experiment inom detta område har jag upptäckt att det finns en hel del nyanser för att göra detta på ett sätt som producerar en användbar och tolkbar visualisering i slutändan.

Varför Visualiseringar av Inbäddningar

Begreppet "algoritm" är ett allomfattande koncept på den moderna internet. Allt runt oss online försöker optimera vår upplevelse (eller mer exakt vår engagemang) genom att ge oss innehåll som det anser att vi mest sannolikt kommer att vara intresserade av eller interagera med. Bakom kulisserna är det mycket troligt att rekommendationsmodellerna använder inbäddningar eller något mycket liknande för att åstadkomma detta. Visualiseringar av inbäddningar tillåter en självledd och utforskande metod för att hitta nya saker jämfört med det endimensionella och ogenomskinliga perspektiv som ges av "för dig" listor eller rekommendationsmotorer.

Jag har ett stort intresse av att bryta igenom "svarta lådan" av dessa typer av ogenomskinliga system och sätta ihop de dolda relationerna mellan saker - och jag har funnit att visualiseringar av inbäddningar är perfekta verktyg för detta. Förutom att vara användbart för riktade sökningar, tycker jag också bara att det är kul att bläddra runt och utforska visualiseringar av inbäddningar. Det känns väldigt naturligt och njutbart att röra sig runt och manipulera ett "rum". Det påminner mig mycket om att flyga runt Google Earth och bara kolla in och utforska slumpmässiga coola platser.

Mina Visualiseringsprojekt

För att ge lite bakgrund på de typer av saker som du kan få med denna teknik, här är vad jag har byggt hittills:

Spotify Music Galaxy

Detta var den första jag skapade, och det är i 3D! Jag byggde det med data jag samlade in från Spotifys offentliga API som tar formen av "Användare som lyssnade på artist X lyssnade också på artist Y". Det implementerades som en webbapplikation byggd med Three.JS. Jag ger användare möjlighet att flyga runt med videospel-liknande kontroller samt möjligheten att ansluta sitt Spotify-konto för att se vilka delar av galaxen deras favoritartister är i.

Anime Atlas

Detta var mest ett fall av att jag råkade komma över data och tyckte att det skulle vara coolt att prova. Jag laddade ner profiler från MyAnimeList av några hundratusen användare och använde dessa för att inbädda alla olika anime som är listade på sidan. Jag ville skapa något lite lättare att engagera sig i, så denna är i 2D. Jag inbäddade det (lol) inom en anime-statistik och rekommendationssida som jag byggde tillsammans med den.

osu! Beatmap Atlas

Den här avslutade jag nyligen, och den är med i rubrikbilden för detta inlägg. I nästan ett decennium nu (herregud), har jag drivit en statistikspårningssida för det populära rytmspelet osu!. I detta spel spelar du olika kartor för sånger (kallade beatmaps) och dina bästa spel kan hämtas från ett offentligt API. Jag använde den data jag samlat in från det projektet för att inbädda dessa beatmaps på ett mycket liknande sätt som Anime Atlas-projektet. Detta projekt är särskilt coolt för mig eftersom det finns mycket detaljerade numeriska mått associerade med var och en av dessa beatmaps som direkt relaterar till spelupplevelsen, vilket gör den resulterande visualiseringen mycket rik och intressant att utforska. Det är för närvarande värd på en fristående webbplats, men jag planerar att integrera det med osu!track på något sätt i framtiden.

Datainsamling + Förberedelse

Den huvudfråga du behöver kunna besvara med den data du samlar in är "hur lika är dessa två givna enheter?". För Spotify Music Galaxy kom detta direkt från Spotifys API där de gjorde det hårda arbetet själva. För de andra två kom dessa data i form av "Denna användare hade alla dessa enheter i sin profil". Varje enhet i den profilen behandlades som relaterad till varje annan enhet med en domänspecifik viktning. All data du arbetar med kommer förmodligen att ha sina egna förbehåll och särdrag, men så länge du har något sätt att ta det och skapa någon metrisk för hur relaterade/lika två enheter är, borde det fungera bra.

Förfiltrering

Beroende på hur mycket data du har kan du behöva göra en initial filtrering för att minska dess kardinalitet till en storlek som är hanterbar för visualisering. En vanlig sak jag gjorde när jag skapade mina var att ta bort mycket sällan sedda eller på annat sätt "ointressanta" enheter från uppsättningen helt innan något annat. Om du har en enorm mängd låggradiga, löst anslutna datapunkter kan det resultera i en rörig och oengagerad visualisering. Att ha ett massivt antal enheter kommer också att göra inbäddningsprocessen långsammare samt potentiellt göra själva visualiseringen långsammare för användare. Personligen har jag funnit att det optimala antalet ligger någonstans mellan ~10k-50k enheter för de typer av visualiseringar jag gör. Du kan förmodligen klara dig med mer om du använder en effektiv renderingsmetod och kanske tillhandahåller några sätt för användarna att skiva + filtrera visualiseringen själv.

Index vs. IDs

De flesta av verktygen och processerna du kommer att använda för att producera inbäddningen kommer att referera till enheter med index snarare än någon form av domänspecifika ID. När du har en samling av enheter du är redo att inbädda, skulle jag rekommendera att skapa en ID-till-index mappning så tidigt som möjligt som kan hänvisas till genom resten av processen. Detta gör det också enklare när man sammanfogar de inbäddade punkterna tillbaka med metadata i slutet av processen.

Bygga Samförekomstmatrisen

Grunden för inbäddningsbyggnadsprocessen innebär att skapa en samförekomstmatris från råkälldata. Detta är en kvadratisk matris där storleken är lika med antalet enheter du inbäddar. Idén är att varje gång du hittar entity_n och entity_m i samma samling, ökar du cooc_matrix[n][m]. För vissa typer av enheter kan du ha ytterligare data tillgängliga som kan användas för att avgöra i vilken utsträckning två enheter är relaterade. För min osu! beatmap-inbäddning till exempel, vägde jag mängden av tillagd värde baserat på hur nära de två beatmaps var i mängden av prestationer poäng belönade. Detta tjänade till att stratifiera inbäddningen och förhindra att nybörjare och avancerade kartor verkar relaterade till varandra.

Om dina enheter har ett associerat tidsstämpel för när de lades till i samlingen, kan du också väga likheten efter hur nära de är i tid. Även om inbäddningar sannolikt kodar några av dessa tidsmässiga relationer naturligt, kan detta vara användbart i vissa fall. Om du är intresserad av att titta på någon kod, kan du kolla in Python-notebooken som hanterar detta och de följande stegen för min osu! beatmap inbäddning här: embed.ipynb.

Minnesöverväganden

Eftersom samförekomstmatriser är kvadratiska, växer de exponentiellt med antalet enheter som inbäddas. För 50k enheter och ett 32-bitars dataformat, kommer en tät matris redan vara på 10GB. 100k enheter sätter den på 40GB. Om du försöker inbädda ännu fler enheter än så eller har begränsat RAM-minne tillgängligt, kan du behöva använda en sparsam representation av matrisen. Jag har haft god framgång med att använda coo_matrix och csc_matrix typer från scipy.sparse Python-biblioteket för detta. Som en extra bonus kan många av "downstream" bibliotek som används för att bygga inbäddningar arbeta med dessa glesa matriser direkt.

Prestandaöverväganden

Den huvudsakliga loopen för att konstruera denna samförekomstmatris kommer förmodligen att se ut något som detta:

cooc_matrix = np.ndarray((n_entities, n_entities))
for collection in collections:
for i in range(len(collection) - 1):
entity_i_ix = entity_ix_by_id[collection[i].id]
for j in range(i + 1, len(collection)):
entity_j_ix = entity_ix_by_id[collection[j].id]

Detta är O(n^2) med avseende på storleken på varje av de samlingar som bearbetas. På grund av detta, om du har ens måttligt stora samlingar, kan processen att generera samförekomstmatrisen vara ganska beräkningsintensiv. Eftersom Python inte är det snabbaste språket, betyder det att generera matrisen kan vara extremt långsamt. Medan jag itererade min process och experimenterade med olika data och uppsättningar, fann jag mig ofta vänta timmar på att matrisen skulle beräknas. Som en lösning för detta använde jag det utmärkta numba-biblioteket för Python. numba JIT-kompilerar Python på flygande fot för att tillåta det att köra många gånger snabbare. Om du har tur, kan det vara så enkelt som att bara lägga till en @jit(nopython=True) dekoratör ovanpå din funktion och njuta av mycket förbättrad prestanda. Det kan till och med automatiskt parallellisera loopar i vissa fall med parallel=True. numba är medveten om och kompatibel med några vanliga Python-datalager som numpy, men för andra (inklusive de sparsamma matriserna från scipy) fungerar det inte. För detta fall hamnade jag med att göra vissa manuella datachunking och omflyttning för att få det att fungera, men det var värt det i slutändan.

Bygga en Gles Entitetsrelationsgraf

När du väl har byggt din samförekomstmatris har du alla data du behöver för att skapa inbäddningen. Faktum är att det är möjligt att gå vidare och direkt inbädda den matrisen - behandla den som en viktad graf. Jag har försökt det - men resultaten var inte bra. De inbäddningar som kom ut såg ut som ellipsoida galaxer: Det finns en tät massa av höggradiga enheter grupperade i mitten och lite annan urskiljbar struktur. Det är inte intressant att titta på och överför inte mycket användbar information annat än vilka enheter som är mest populära. Jag tror att orsaken till detta är tvåfaldig:

Den viktade grafen är helt enkelt för tät

Kantvikterna för populära höggradiga noder är alldeles för stora i jämförelse med de allra flesta andra kanter

PyMDE Förbearbetning

Nu är det ett bra tillfälle att introducera PyMDE. PyMDE är ett Python-bibliotek som implementerar en algoritm kallad Minimum Distortion Embedding. Det är huvudmotorn i inbäddningsgenereringsprocessen och mycket kraftfullt och mångsidigt. Det kan inbädda högdimensionella vektorer eller grafer inbyggt, den senare av vilken vi kommer att använda. Vi kommer att gå in på mer senare, men en av de saker som detta bibliotek tillhandahåller är en uppsättning av förbearbetningsrutiner - en av vilka gör precis vad vi letar efter. Den kallas pymde.preserve_neighbors. Denna rutin tar en graf som indata och returnerar en annan glesare graf som utdata. Den åtgärdar båda punkterna ovan genom att glesa samt avväga alla kanterna. Internt uppnår den detta genom att beräkna k-nearest neighbors för varje nod i grafen. Den släpper alla utom de k bästa kanterna för varje nod och sätter deras vikter till 1. Den traverserar också grafen för att fylla ut kanter för noder med mindre än k kanter, vilket kan vara användbart för vissa datauppsättningar. En sak att notera är att denna funktion tolkar kantvikter som avstånd: större vikter behandlas som mer olikartade snarare än mer lika. För att arbeta runt detta tillämpar jag 1 / log(x) på alla värdena i samförekomstmatrisen innan jag skickar den till pymde.preserve_neighbors. Detta omvandlar det effektivt från en likhetsmatris till en olikhetmatris.

I tillägg till olikhetsmatrisen, pymde.preserve_neighbors accepterar några parametrar som är mycket inflytelserika för den genererade inbäddningen.

n_neighbors

Denna parameter definierar k som används för KNN-beräkningen. Ju större detta värde är, desto tätare blir den resulterande grafen. Det bästa värdet för denna variabel beror ganska mycket på dina data. Storleken på dina samlingar, antalet enheter du inbäddar och popularitetsdistributionen för dina enheter spelar alla en roll. Denna n_neighbors parameter är en av de saker jag starkt rekommenderar att experimentera med och prova olika värden för att se vad som fungerar bäst för dina data. Med det

Praktiska Tips för Att Skapa Effektiva Visualiseringar av Inbäddningar

När du skapar visualiseringar av inbäddningar är det viktigt att tänka på några praktiska aspekter för att optimera resultatet:

Optimera Prestanda och Användarvänlighet

För att skapa en smidig användarupplevelse, överväg följande:

  • Använd effektiva renderingstekniker som WebGL för stora datamängder
  • Implementera progressiv laddning av data för snabbare initial rendering
  • Erbjud zoom- och filtreringsfunktioner så användare kan utforska delar av visualiseringen

Välja Rätt Dimensionalitet

2D vs 3D visualiseringar har olika fördelar:

  • 2D är enklare att navigera och tolka för många användare
  • 3D kan visa mer komplex struktur men kan vara svårare att interagera med

Interaktiva Funktioner för Utforskning

Lägg till funktioner som:

  • Sökfunktion för att hitta specifika enheter
  • Klickbara noder som visar mer information
  • Möjlighet att markera och jämföra grupper av enheter

Tolkning och Analys av Resultaten

För att få insikter från visualiseringen:

  • Identifiera kluster och undersök vad de representerar
  • Analysera outliers och varför de är avlägsna från andra enheter
  • Jämför visualiseringen med domänkunskap för att validera resultaten

Genom att tillämpa dessa tips kan du skapa mer informativa och användbara visualiseringar av inbäddningar som ger djupare insikter i dina data.