Ga naar hoofdinhoud

Beginners-tutorial: aanbevelingssystemen in Python

Bouw je aanbevelingsengine met Python, van basismodellen tot contentbased en collaborative filtering-aanbevelingssystemen.
· 15 min lezen

Verken met AI

aanbevelingssysteem
Bron</a

Het doel van deze tutorial is niet om je een expert te maken in het bouwen van aanbevelingssystemen. Het idee is juist om je op weg te helpen met een overzicht van de soorten aanbevelingssystemen die er zijn en hoe je er zelf eentje kunt bouwen.

In deze tutorial leer je hoe je een basismodel bouwt van eenvoudige en contentbased-aanbevelingssystemen. Hoewel deze modellen qua complexiteit, kwaliteit of nauwkeurigheid niet in de buurt komen van de industrienorm, helpen ze je wel om te starten met het bouwen van complexere modellen die nog betere resultaten opleveren.

Aanbevelingssystemen behoren tot de populairste toepassingen van data science. Ze worden gebruikt om de "rating" of "voorkeur" te voorspellen die een gebruiker aan een item zou geven. Bijna elk groot techbedrijf past ze op de een of andere manier toe. Amazon gebruikt ze om producten aan te bevelen, YouTube om te bepalen welke video automatisch als volgende wordt afgespeeld, en Facebook om pagina's en mensen aan te raden om te volgen.

Meer nog: voor bedrijven als Netflix, Amazon Prime, Hulu en Hotstar draait het businessmodel en het succes om de kracht van hun aanbevelingen. Netflix loofde in 2009 zelfs een miljoen dollar uit aan iedereen die het systeem met 10% kon verbeteren.

Er zijn ook populaire aanbevelingssystemen voor domeinen als restaurants, films en online daten. Daarnaast zijn aanbevelers ontwikkeld voor het verkennen van onderzoeksartikelen en experts, samenwerkingen en financiële diensten. YouTube gebruikt aanbevelingen op grote schaal om video's voor te stellen op basis van je geschiedenis. Kijk je bijvoorbeeld veel educatieve video's, dan stelt het platform je dat type content voor.

Maar wat zijn die aanbevelingssystemen precies?

Grofweg kun je aanbevelingssystemen indelen in 3 typen:

  • Eenvoudige aanbevelers: bieden algemene aanbevelingen aan elke gebruiker, gebaseerd op filmpopulariteit en/of genre. Het basisidee is dat populairdere en geprezen films met hogere waarschijnlijkheid in de smaak vallen bij het gemiddelde publiek. Een voorbeeld is de IMDB Top 250.
  • Contentbased-aanbevelers: raden vergelijkbare items aan op basis van een specifiek item. Dit systeem gebruikt metadata van items, zoals genre, regisseur, beschrijving, acteurs, enz. bij films, om aanbevelingen te doen. Het idee is dat als iemand een bepaald item leuk vindt, hij/zij ook iets zal waarderen dat erop lijkt. Om dat te kunnen aanraden, gebruikt het de metadata van de items die de gebruiker eerder leuk vond. Een goed voorbeeld is YouTube, dat op basis van je geschiedenis nieuwe video's voorstelt die je mogelijk wilt bekijken.
  • Collaborative-filtering-engines: deze systemen worden veel gebruikt en proberen te voorspellen welke rating of voorkeur een gebruiker aan een item zou geven op basis van eerdere ratings en voorkeuren van andere gebruikers. Collaborative filtering heeft geen itemmetadata nodig, in tegenstelling tot contentbased systemen.

Eenvoudige aanbevelers

Zoals hierboven beschreven, zijn eenvoudige aanbevelers basale systemen die topitems aanraden op basis van een bepaalde metriek of score. In deze sectie bouw je een vereenvoudigde kloon van IMDB Top 250 Movies met metadata verzameld van IMDB.

De stappen zijn als volgt:

  • Bepaal de metriek of score om films op te beoordelen.

  • Bereken de score voor elke film.

  • Sorteer de films op basis van de score en geef de toppers weer.

Over de dataset

De datasetbestanden bevatten metadata voor alle 45.000 films in de Full MovieLens Dataset. De dataset bestaat uit films uitgebracht op of vóór juli 2017. Ze bevat features als cast, crew, plotkeywords, budget, omzet, posters, releasedata, talen, productiebedrijven, landen, TMDB-stemaantallen en gemiddelde stemmen.

Deze features kunnen mogelijk worden gebruikt om je machinelearningmodellen te trainen voor content- en collaborative filtering.

Deze dataset bestaat uit de volgende bestanden:

  • movies_metadata.csv: Bevat informatie over ~45.000 films uit de Full MovieLens-dataset. Features omvatten posters, backdrops, budget, genre, omzet, releasedata, talen, productielanden en -bedrijven.
  • keywords.csv: Bevat de plotkeywords voor onze MovieLens-films. Beschikbaar als een ge-stringifieerde JSON.
  • credits.csv: Bestaat uit cast- en crewinformatie voor alle films. Beschikbaar als een ge-stringifieerde JSON.
  • links.csv: Bevat de TMDB- en IMDB-ID's van alle films uit de Full MovieLens-dataset.
  • links_small.csv: Bevat de TMDB- en IMDB-ID's van een kleine subset van 9.000 films uit de volledige dataset.
  • ratings_small.csv: De subset met 100.000 ratings van 700 gebruikers op 9.000 films.

De Full MovieLens Dataset omvat 26 miljoen ratings en 750.000 tag-toepassingen, van 270.000 gebruikers op alle 45.000 films in deze dataset. Je kunt deze vinden op de officiële GroupLens-website.

Let op: De subset die in deze tutorial wordt gebruikt, kun je hier downloaden.

Om je dataset te laden, gebruik je de pandas DataFrame-bibliotheek. De pandas-bibliotheek wordt vooral gebruikt voor datamanipulatie en -analyse. Ze stelt je data voor in een rij-kolomstructuur. De Pandas-bibliotheek is gebouwd op de NumPy-array voor de implementatie van pandas-dataobjecten. pandas biedt kant-en-klare datastructuren en bewerkingen voor het manipuleren van numerieke tabellen, tijdreeksen, afbeeldingen en NLP-datasets. Kortom, pandas is nuttig voor datasets die zich eenvoudig in tabelvorm laten weergeven.

Voordat je een van de bovenstaande stappen uitvoert, laden we eerst je movies-metadata in een pandas DataFrame:

# Import Pandas
import pandas as pd

# Load Movies Metadata
metadata = pd.read_csv('movies_metadata.csv', low_memory=False)

# Print the first three rows
metadata.head(3)
 adultbelongs_to_collectionbudgetgenreshomepageidimdb_idoriginal_languageoriginal_titleoverview...release_daterevenueruntimespoken_languagesstatustaglinetitlevideovote_averagevote_count
0False{'id': 10194, 'name': 'Toy Story Collection', ...30000000[{'id': 16, 'name': 'Animation'}, {'id': 35, '...http://toystory.disney.com/toy-story862tt0114709enToy StoryLed by Woody, Andy's toys live happily in his ......1995-10-30373554033.081.0[{'iso_639_1': 'en', 'name': 'English'}]ReleasedNaNToy StoryFalse7.75415.0
1FalseNaN65000000[{'id': 12, 'name': 'Adventure'}, {'id': 14, '...NaN8844tt0113497enJumanjiWhen siblings Judy and Peter discover an encha......1995-12-15262797249.0104.0[{'iso_639_1': 'en', 'name': 'English'}, {'iso...ReleasedRoll the dice and unleash the excitement!JumanjiFalse6.92413.0
2False{'id': 119050, 'name': 'Grumpy Old Men Collect...0[{'id': 10749, 'name': 'Romance'}, {'id': 35, ...NaN15602tt0113228enGrumpier Old MenA family wedding reignites the ancient feud be......1995-12-220.0101.0[{'iso_639_1': 'en', 'name': 'English'}]ReleasedStill Yelling. Still Fighting. Still Ready for...Grumpier Old MenFalse6.592.0

3 rijen × 24 kolommen

Een van de meest basale metrics is de ranking om te bepalen welke top 250 films er zijn op basis van hun ratings.

Maar een rating als metriek heeft een paar kanttekeningen:

  • Ten eerste houdt het geen rekening met de populariteit van een film. Een film met een rating van 9 van 10 stemmers wordt dus als 'beter' gezien dan een film met een rating van 8,9 van 10.000 stemmers.

    Stel dat je Chinees wilt bestellen en je hebt twee opties: het ene restaurant heeft 5 sterren van slechts 5 mensen, het andere 4,5 sterren van 1000 mensen. Welke kies je? Waarschijnlijk de tweede, toch?

    Natuurlijk kan het zijn dat het eerste restaurant pas net open is en dus minder stemmen heeft, terwijl het tweede al een jaar draait.

  • Gerelateerd hieraan zal deze metriek films met weinig stemmers en scheve en/of extreem hoge ratings bevoordelen. Naarmate het aantal stemmers toeneemt, stabiliseert de rating en benadert die een waarde die de kwaliteit beter weerspiegelt en de gebruiker een beter idee geeft welke film hij/zij moet kiezen. Bij extreem weinig stemmers is het lastig de kwaliteit te beoordelen en moet je mogelijk externe bronnen raadplegen.

Rekening houdend met deze tekortkomingen, moet je een gewogen rating bedenken die het gemiddelde en het aantal stemmen meeneemt. Zo krijgt een film met een 9 van 100.000 stemmers een (veel) hogere score dan een film met dezelfde rating maar slechts een paar honderd stemmers.

Omdat je een kloon van IMDB's Top 250 probeert te bouwen, gebruiken we hun gewogen ratingformule als metriek/score. Wiskundig weergegeven als volgt:

\begin{equation} \text Weighted Rating (\bf WR) = \left({{\bf v} \over {\bf v} + {\bf m}} \cdot R\right) + \left({{\bf m} \over {\bf v} + {\bf m}} \cdot C\right) \end{equation}

In de bovenstaande vergelijking:

  • v is het aantal stemmen voor de film;

  • m is het minimum aantal stemmen om in de lijst te komen;

  • R is de gemiddelde rating van de film;

  • C is de gemiddelde rating over de hele dataset.

Je hebt al de waarden voor v (vote_count) en R (vote_average) voor elke film in de dataset. Ook C kun je direct uit deze data berekenen.

Een passende waarde voor m bepalen is een hyperparameter die je zelf kunt kiezen; er is geen 'juiste' waarde. Je kunt het zien als een negatieve voorfilter die simpelweg films verwijdert met minder stemmen dan een bepaalde drempel m. Hoe selectief je filter is, bepaal je zelf.

In deze tutorial gebruik je als afkapwaarde m het 90e percentiel. Met andere woorden: om in de lijst te komen moet een film meer stemmen hebben dan minstens 90% van de films. (Had je bijvoorbeeld het 75e percentiel gekozen, dan had je de top 25% qua aantal stemmen meegenomen. Hoe lager het percentiel, hoe meer films worden overwogen).

Als eerste stap berekenen we C, de gemiddelde rating over alle films met de pandas-functie .mean():

# Calculate mean of vote average column
C = metadata['vote_average'].mean()
print(C)
5.618207215133889

Hieruit zie je dat de gemiddelde filmrating op IMDB rond de 5,6 op een schaal van 10 ligt.

Vervolgens berekenen we het aantal stemmen, m, dat een film in het 90e percentiel heeft. De pandas-bibliotheek maakt dit eenvoudig met de methode .quantile():

# Calculate the minimum number of votes required to be in the chart, m
m = metadata['vote_count'].quantile(0.90)
print(m)
160.0

Nu je m hebt, kun je eenvoudig filteren op films met minimaal 160 stemmen:

Gebruik de methode .copy() om ervoor te zorgen dat de nieuwe q_movies DataFrame onafhankelijk is van je oorspronkelijke metadata-DataFrame. Met andere woorden: wijzigingen in q_movies beïnvloeden het origineel niet.

# Filter out all qualified movies into a new DataFrame
q_movies = metadata.copy().loc[metadata['vote_count'] >= m]
q_movies.shape
(4555, 24)
metadata.shape
(45466, 24)

Hieruit blijkt dat ongeveer 10% van de films meer dan 160 stemmen heeft en in deze lijst komt.

De volgende, en belangrijkste, stap is het berekenen van de gewogen rating voor elke gekwalificeerde film. Hiervoor zul je:

  • Een functie weighted_rating() definiëren;
  • Aangezien je m en C al hebt berekend, geef je die door als argumenten;
  • Vervolgens selecteer je de kolommen vote_count (v) en vote_average (R) uit de q_movies-DataFrame;
  • Tot slot bereken je het gewogen gemiddelde en retourneer je het resultaat.

Je definieert een nieuwe feature score, waarvan je de waarde berekent door deze functie toe te passen op je DataFrame met gekwalificeerde films:

# Function that computes the weighted rating of each movie
def weighted_rating(x, m=m, C=C):
    v = x['vote_count']
    R = x['vote_average']
    # Calculation based on the IMDB formula
    return (v/(v+m) * R) + (m/(m+v) * C)
# Define a new feature 'score' and calculate its value with `weighted_rating()`
q_movies['score'] = q_movies.apply(weighted_rating, axis=1)

Tot slot sorteren we de DataFrame aflopend op de kolom score en geven we de titel, het aantal stemmen, het gemiddelde en de gewogen rating (score) van de top 20 films weer.

#Sort movies based on score calculated above
q_movies = q_movies.sort_values('score', ascending=False)

#Print the top 15 movies
q_movies[['title', 'vote_count', 'vote_average', 'score']].head(20)
 titlevote_countvote_averagescore
314The Shawshank Redemption8358.08.58.445869
834The Godfather6024.08.58.425439
10309Dilwale Dulhania Le Jayenge661.09.18.421453
12481The Dark Knight12269.08.38.265477
2843Fight Club9678.08.38.256385
292Pulp Fiction8670.08.38.251406
522Schindler's List4436.08.38.206639
23673Whiplash4376.08.38.205404
5481Spirited Away3968.08.38.196055
2211Life Is Beautiful3643.08.38.187171
1178The Godfather: Part II3418.08.38.180076
1152One Flew Over the Cuckoo's Nest3001.08.38.164256
351Forrest Gump8147.08.28.150272
1154The Empire Strikes Back5998.08.28.132919
1176Psycho2405.08.38.132715
18465The Intouchables5410.08.28.125837
40251Your Name.1030.08.58.112532
289Leon: The Professional4293.08.28.107234
3030The Green Mile4166.08.28.104511
1170GoodFellas3211.08.28.077459

Zoals je ziet heeft de eenvoudige aanbeveler het prima gedaan!

De grafiek heeft veel overlap met de IMDB Top 250: je top twee films, "Shawshank Redemption" en "The Godfather", zijn dezelfde als bij IMDB en we weten allemaal dat dat geweldige films zijn. Eigenlijk verdienen alle top 20-films het om in die lijst te staan, toch?

Contentbased-aanbeveler

Aanbeveler op basis van plotbeschrijving

In deze sectie leer je een systeem bouwen dat films aanbeveelt die lijken op een bepaalde film. Hiervoor bereken je paargewijze cosine-similariteitsscores voor alle films op basis van hun plotbeschrijvingen en raad je films aan op basis van die drempel voor similariteitsscore.

De plotbeschrijving is beschikbaar als de feature overview in je metadata-dataset. Laten we de plots van een paar films bekijken:

#Print plot overviews of the first 5 movies.
metadata['overview'].head()
0    Led by Woody, Andy's toys live happily in his ...
1    When siblings Judy and Peter discover an encha...
2    A family wedding reignites the ancient feud be...
3    Cheated on, mistreated and stepped on, the wom...
4    Just when George Banks has recovered from his ...
Name: overview, dtype: object

Het probleem is een NLP-vraagstuk. Je moet dus eerst features uit deze tekstdata extraheren voordat je similariteit/ongelijkheid tussen ze kunt berekenen. Simpel gezegd: je kunt de similariteit tussen twee overzichten niet in ruwe vorm berekenen. Je moet de woordvectoren van elk overzicht (of document) berekenen.

Zoals de naam zegt, zijn woordvectoren gevectoriseerde representaties van woorden in een document. De vectoren dragen semantiek met zich mee. Bijvoorbeeld: man & king zullen vectorrepresentaties dicht bij elkaar hebben, terwijl man & woman juist verder uit elkaar liggen.

Je berekent Term Frequency-Inverse Document Frequency (TF-IDF)-vectoren voor elk document. Dit levert een matrix op waarin elke kolom een woord in de overview-woordenschat vertegenwoordigt (alle woorden die in minstens één document voorkomen) en elke rij een film, zoals eerder.

In essentie is de TF-IDF-score de frequentie waarmee een woord in een document voorkomt, naar beneden gewogen door het aantal documenten waarin het voorkomt. Dit vermindert het belang van woorden die vaak in plotoverzichten voorkomen en dus minder betekenisvol zijn voor de uiteindelijke similariteitsscore.

Gelukkig biedt scikit-learn een ingebouwde TfIdfVectorizer-klasse die de TF-IDF-matrix in een paar regels produceert.

  • Importeer de Tfidf-module met scikit-learn;
  • Verwijder stopwoorden zoals 'the', 'an', etc., omdat ze geen nuttige informatie geven over het onderwerp;
  • Vervang not-a-number-waarden door een lege string;
  • Construeer vervolgens de TF-IDF-matrix op de data.
#Import TfIdfVectorizer from scikit-learn
from sklearn.feature_extraction.text import TfidfVectorizer

#Define a TF-IDF Vectorizer Object. Remove all english stop words such as 'the', 'a'
tfidf = TfidfVectorizer(stop_words='english')

#Replace NaN with an empty string
metadata['overview'] = metadata['overview'].fillna('')

#Construct the required TF-IDF matrix by fitting and transforming the data
tfidf_matrix = tfidf.fit_transform(metadata['overview'])

#Output the shape of tfidf_matrix
tfidf_matrix.shape
(45466, 75827)
#Array mapping from feature integer indices to feature name.
tfidf.get_feature_names()[5000:5010]
['avails',
 'avaks',
 'avalanche',
 'avalanches',
 'avallone',
 'avalon',
 'avant',
 'avanthika',
 'avanti',
 'avaracious']

Hieruit blijkt dat je dataset met 45.000 films 75.827 unieke woorden bevat.

Met deze matrix kun je nu een similariteitsscore berekenen. Er zijn verschillende metrics mogelijk, zoals manhattan, euclidisch, Pearson en cosine-similariteit. Er is geen eenduidig beste score. Verschillende metrics werken goed in verschillende scenario's; het is vaak goed om te experimenteren en de resultaten te bekijken.

Je gebruikt cosine similarity om een numerieke waarde te berekenen die de overeenkomst tussen twee films aangeeft. Je gebruikt cosine omdat die onafhankelijk is van de grootte en relatief eenvoudig en snel te berekenen is (zeker in combinatie met TF-IDF). Wiskundig gedefinieerd als volgt:

definitie

Omdat je de TF-IDF-vectorizer gebruikte, geeft het dotproduct tussen elk paar vectoren direct de cosine-similariteit. Daarom gebruik je sklearn's linear_kernel() in plaats van cosine_similarities(), omdat dat sneller is.

Dit levert een matrix van 45466x45466 op, wat betekent dat elk film-overview een cosine-similariteitsscore heeft met elk ander film-overview. Elke film is dus een 1x45466-kolomvector waarin elke kolom een similariteitsscore met een film is.

# Import linear_kernel
from sklearn.metrics.pairwise import linear_kernel

# Compute the cosine similarity matrix
cosine_sim = linear_kernel(tfidf_matrix, tfidf_matrix)
cosine_sim.shape
(45466, 45466)
cosine_sim[1]
array([0.01504121, 1.        , 0.04681953, ..., 0.        , 0.02198641,
       0.00929411])

Je gaat nu een functie definiëren die een filmtitel als invoer neemt en een lijst met de 10 meest vergelijkbare films geeft. Hiervoor heb je eerst een omgekeerde mapping nodig van filmtitels naar DataFrame-indexen. Met andere woorden: je hebt een mechanisme nodig om de index van een film in je metadata-DataFrame te vinden op basis van de titel.

#Construct a reverse map of indices and movie titles
indices = pd.Series(metadata.index, index=metadata['title']).drop_duplicates()
indices[:10]
title
Toy Story                      0
Jumanji                        1
Grumpier Old Men               2
Waiting to Exhale              3
Father of the Bride Part II    4
Heat                           5
Sabrina                        6
Tom and Huck                   7
Sudden Death                   8
GoldenEye                      9
dtype: int64

Je kunt nu de aanbevelingsfunctie definiëren. De stappen zijn:

  • Haal de index op van de film met de gegeven titel.

  • Haal de lijst met cosine-similariteitsscores op voor die film met alle films. Zet dit om in een lijst van tuples waarin het eerste element de positie is en het tweede de similariteitsscore.

  • Sorteer de lijst op de similariteitsscores (het tweede element).

  • Neem de top 10 uit deze lijst. Negeer het eerste element omdat dit de film zelf is.

  • Geef de titels terug die horen bij de indices van de topelementen.

# Function that takes in movie title as input and outputs most similar movies
def get_recommendations(title, cosine_sim=cosine_sim):
    # Get the index of the movie that matches the title
    idx = indices[title]

    # Get the pairwsie similarity scores of all movies with that movie
    sim_scores = list(enumerate(cosine_sim[idx]))

    # Sort the movies based on the similarity scores
    sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True)

    # Get the scores of the 10 most similar movies
    sim_scores = sim_scores[1:11]

    # Get the movie indices
    movie_indices = [i[0] for i in sim_scores]

    # Return the top 10 most similar movies
    return metadata['title'].iloc[movie_indices]
get_recommendations('The Dark Knight Rises')
12481                                      The Dark Knight
150                                         Batman Forever
1328                                        Batman Returns
15511                           Batman: Under the Red Hood
585                                                 Batman
21194    Batman Unmasked: The Psychology of the Dark Kn...
9230                    Batman Beyond: Return of the Joker
18035                                     Batman: Year One
19792              Batman: The Dark Knight Returns, Part 1
3095                          Batman: Mask of the Phantasm
Name: title, dtype: object
get_recommendations('The Godfather')
1178               The Godfather: Part II
44030    The Godfather Trilogy: 1972-1990
1914              The Godfather: Part III
23126                          Blood Ties
11297                    Household Saints
34717                   Start Liquidation
10821                            Election
38030            A Mother Should Be Loved
17729                   Short Sharp Shock
26293                  Beck 28 - Familjen
Name: title, dtype: object

Je ziet dat je systeem best aardige vergelijkbare films vindt op basis van de plotbeschrijving, maar de kwaliteit van de aanbevelingen is niet geweldig. "The Dark Knight Rises" levert alleen Batman-films op, terwijl het waarschijnlijker is dat mensen die die film leuk vonden ook andere films van Christopher Nolan waarderen. Dat kan je huidige systeem niet vangen.

Aanbeveler op basis van credits, genres en keywords

De kwaliteit van je aanbeveler wordt beter met rijkere metadata en fijnere details. Dat ga je hier doen. Je bouwt een aanbeveler op basis van de volgende metadata: de 3 belangrijkste acteurs, de regisseur, gerelateerde genres en de plotkeywords.

De keywords-, cast- en crewdata zitten niet in je huidige dataset, dus laad en merge die eerst in je hoofd-DataFrame metadata.

# Load keywords and credits
credits = pd.read_csv('credits.csv')
keywords = pd.read_csv('keywords.csv')

# Remove rows with bad IDs.
metadata = metadata.drop([19730, 29503, 35587])

# Convert IDs to int. Required for merging
keywords['id'] = keywords['id'].astype('int')
credits['id'] = credits['id'].astype('int')
metadata['id'] = metadata['id'].astype('int')

# Merge keywords and credits into your main metadata dataframe
metadata = metadata.merge(credits, on='id')
metadata = metadata.merge(keywords, on='id')
# Print the first two movies of your newly merged metadata
metadata.head(2)
 adultbelongs_to_collectionbudgetgenreshomepageidimdb_idoriginal_languageoriginal_titleoverview...spoken_languagesstatustaglinetitlevideovote_averagevote_countcastcrewkeywords
0False{'id': 10194, 'name': 'Toy Story Collection', ...30000000[{'id': 16, 'name': 'Animation'}, {'id': 35, '...http://toystory.disney.com/toy-story862tt0114709enToy StoryLed by Woody, Andy's toys live happily in his ......[{'iso_639_1': 'en', 'name': 'English'}]ReleasedNaNToy StoryFalse7.75415.0[{'cast_id': 14, 'character': 'Woody (voice)',...[{'credit_id': '52fe4284c3a36847f8024f49', 'de...[{'id': 931, 'name': 'jealousy'}, {'id': 4290,...
1FalseNaN65000000[{'id': 12, 'name': 'Adventure'}, {'id': 14, '...NaN8844tt0113497enJumanjiWhen siblings Judy and Peter discover an encha......[{'iso_639_1': 'en', 'name': 'English'}, {'iso...ReleasedRoll the dice and unleash the excitement!JumanjiFalse6.92413.0[{'cast_id': 1, 'character': 'Alan Parrish', '...[{'credit_id': '52fe44bfc3a36847f80a7cd1', 'de...[{'id': 10090, 'name': 'board game'}, {'id': 1...

2 rijen × 27 kolommen

Uit je nieuwe features cast, crew en keywords moet je de drie belangrijkste acteurs, de regisseur en de keywords bij de film extraheren.

Maar eerst: je data staat als "ge-stringifieerde" lijsten. Je moet die omzetten naar bruikbare objecten.

# Parse the stringified features into their corresponding python objects
from ast import literal_eval

features = ['cast', 'crew', 'keywords', 'genres']
for feature in features:
    metadata[feature] = metadata[feature].apply(literal_eval)

Vervolgens schrijf je functies die helpen de benodigde informatie uit elke feature te halen.

Eerst importeer je NumPy om toegang te krijgen tot de constante NaN. Daarna schrijf je de functie get_director():

# Import Numpy
import numpy as np

Haal de naam van de regisseur uit de crew-feature. Als er geen regisseur staat, retourneer NaN

def get_director(x):
    for i in x:
        if i['job'] == 'Director':
            return i['name']
    return np.nan

Daarna schrijf je een functie die de top 3 elementen of anders de hele lijst retourneert. De lijst slaat op cast, keywords en genres.

def get_list(x):
    if isinstance(x, list):
        names = [i['name'] for i in x]
        #Check if more than 3 elements exist. If yes, return only first three. If no, return entire list.
        if len(names) > 3:
            names = names[:3]
        return names

    #Return empty list in case of missing/malformed data
    return []
# Define new director, cast, genres and keywords features that are in a suitable form.
metadata['director'] = metadata['crew'].apply(get_director)

features = ['cast', 'keywords', 'genres']
for feature in features:
    metadata[feature] = metadata[feature].apply(get_list)
# Print the new features of the first 3 films
metadata[['title', 'cast', 'director', 'keywords', 'genres']].head(3)
 titlecastdirectorkeywordsgenres
0Toy Story[Tom Hanks, Tim Allen, Don Rickles]John Lasseter[jealousy, toy, boy][Animation, Comedy, Family]
1Jumanji[Robin Williams, Jonathan Hyde, Kirsten Dunst]Joe Johnston[board game, disappearance, based on children'...[Adventure, Fantasy, Family]
2Grumpier Old Men[Walter Matthau, Jack Lemmon, Ann-Margret]Howard Deutch[fishing, best friend, duringcreditsstinger][Romance, Comedy]

De volgende stap is om de namen en keywords naar kleine letters te brengen en alle spaties te verwijderen.

Spaties verwijderen is een belangrijke preprocessingstap. Zo telt de vectorizer de "Johnny" van "Johnny Depp" en "Johnny Galecki" niet als hetzelfde. Na deze stap worden de genoemde acteurs "johnnydepp" en "johnnygalecki" en dus verschillend voor je vectorizer.

Een ander voorbeeld waar het model dezelfde vector zou kunnen opleveren is "bread jam" en "traffic jam". Daarom is het beter eventuele spaties te strippen.

De onderstaande functie doet precies dat:

# Function to convert all strings to lower case and strip names of spaces
def clean_data(x):
    if isinstance(x, list):
        return [str.lower(i.replace(" ", "")) for i in x]
    else:
        #Check if director exists. If not, return empty string
        if isinstance(x, str):
            return str.lower(x.replace(" ", ""))
        else:
            return ''
# Apply clean_data function to your features.
features = ['cast', 'keywords', 'director', 'genres']

for feature in features:
    metadata[feature] = metadata[feature].apply(clean_data)

Je kunt nu je "metadata soup" maken: een string met alle metadata die je aan de vectorizer wilt voeren (acteurs, regisseur en keywords).

De functie create_soup voegt simpelweg alle vereiste kolommen samen met een spatie. Dit is de laatste preprocessingstap, en de output gaat je woordvectormodel in.

def create_soup(x):
    return ' '.join(x['keywords']) + ' ' + ' '.join(x['cast']) + ' ' + x['director'] + ' ' + ' '.join(x['genres'])
# Create a new soup feature
metadata['soup'] = metadata.apply(create_soup, axis=1)
metadata[['soup']].head(2)
 soup
0jealousy toy boy tomhanks timallen donrickles ...
1boardgame disappearance basedonchildren'sbook ...

De volgende stappen zijn hetzelfde als bij de aanbeveler op basis van plotbeschrijving. Een belangrijk verschil is dat je nu CountVectorizer() gebruikt in plaats van TF-IDF. Je wilt namelijk de aanwezigheid van een acteur/regisseur niet naar beneden wegen als diegene relatief veel films heeft gedaan. Dat is hier niet intuïtief.

Het grote verschil tussen CountVectorizer() en TF-IDF is de inverse documentfrequentie (IDF)-component, die wel in de laatste zit en niet in de eerste.

# Import CountVectorizer and create the count matrix
from sklearn.feature_extraction.text import CountVectorizer

count = CountVectorizer(stop_words='english')
count_matrix = count.fit_transform(metadata['soup'])
count_matrix.shape
(46628, 73881)

Je ziet dat er 73.881 woorden in de metadata zitten die je hebt gevoed.

Vervolgens gebruik je cosine_similarity om de afstand tussen de embeddings te meten.

# Compute the Cosine Similarity matrix based on the count_matrix
from sklearn.metrics.pairwise import cosine_similarity

cosine_sim2 = cosine_similarity(count_matrix, count_matrix)
# Reset index of your main DataFrame and construct reverse mapping as before
metadata = metadata.reset_index()
indices = pd.Series(metadata.index, index=metadata['title'])

Je kunt nu je functie get_recommendations() hergebruiken door de nieuwe matrix cosine_sim2 als tweede argument mee te geven.

get_recommendations('The Dark Knight Rises', cosine_sim2)
12589      The Dark Knight
10210        Batman Begins
9311                Shiner
9874       Amongst Friends
7772              Mitchell
516      Romeo Is Bleeding
11463         The Prestige
24090            Quicksand
25038             Deadfall
41063                 Sara
Name: title, dtype: object
get_recommendations('The Godfather', cosine_sim2)
1934            The Godfather: Part III
1199             The Godfather: Part II
15609                   The Rain People
18940                         Last Exit
34488                              Rege
35802            Manuscripts Don't Burn
35803            Manuscripts Don't Burn
8001     The Night of the Following Day
18261                 The Son of No One
28683            In the Name of the Law
Name: title, dtype: object

Mooi! Je ziet dat je aanbeveler meer informatie heeft weten te vangen dankzij rijkere metadata en betere aanbevelingen geeft. Er zijn uiteraard talloze manieren om hiermee te experimenteren en de aanbevelingen verder te verbeteren.

Enkele suggesties:

  • Voeg een populariteitsfilter toe: deze aanbeveler neemt de 30 meest vergelijkbare films, berekent de gewogen ratings (met de IMDB-formule), sorteert daarop en geeft de top 10 terug.

  • Andere crewleden: neem ook namen op van bijvoorbeeld scenarioschrijvers en producenten.

  • Meer gewicht voor de regisseur: noem de regisseur meerdere keren in de soup om de similariteitsscores van films met dezelfde regisseur te verhogen.

Collaborative filtering met Python

In deze tutorial heb je geleerd hoe je je eigen eenvoudige en contentbased film-aanbevelingssystemen bouwt. Er is ook nog een ander, zeer populair type: collaborative filtering.

Collaborative filters kun je verder indelen in twee typen:

  • User-based Filtering: deze systemen bevelen producten aan die vergelijkbare gebruikers leuk vonden. Stel dat Alice en Bob een vergelijkbare smaak in boeken hebben (ze vinden grotendeels dezelfde boeken goed of slecht). Er verschijnt een nieuw boek, Alice leest het en is enthousiast. Dan is de kans groot dat Bob het ook leuk vindt, dus raadt het systeem dit boek aan Bob aan.
  • Item-based Filtering: deze systemen lijken sterk op de content-aanbeveler die je bouwde. Ze identificeren vergelijkbare items op basis van hoe mensen ze eerder beoordeelden. Als Alice, Bob en Eve 5 sterren gaven aan The Lord of the Rings en The Hobbit, ziet het systeem die items als vergelijkbaar. Koopt iemand The Lord of the Rings, dan raadt het systeem ook The Hobbit aan.

Een voorbeeld van collaborative filtering op basis van een ratingsysteem:

grafiek

Je gaat deze systemen niet bouwen in deze tutorial, maar je kent inmiddels de meeste ideeën die je nodig hebt. Een goed startpunt voor collaborative filtering is de MovieLens-dataset, die je hier vindt.

Conclusie

Gefeliciteerd met het afronden van deze tutorial!

Je hebt met succes onze tutorial doorlopen over aanbevelingssystemen in Python. Je leerde hoe je eenvoudige en contentbased-aanbevelers bouwt.

Een goede oefening is om collaborative filtering te implementeren in Python met de subset van de MovieLens-dataset die je hier gebruikte voor eenvoudige en contentbased-aanbevelers.

Ben je net begonnen met Python en wil je meer leren? Volg dan DataCamps cursus Introduction to Data Science in Python.

Onderwerpen

Gerelateerd