# Import Pandasimport pandas as pd
# Load Movies Metadata
metadata = pd.read_csv('movies_metadata.csv', low_memory=False)# Print the first three rows
metadata.head(3)技术支持
# Function that computes the weighted rating of each moviedefweighted_rating(x, m=m, C=C):
v = x['vote_count']
R = x['vote_average']# Calculation based on the IMDB formulareturn(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)技术支持
#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)技术支持
#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...2A 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
技术支持
#Import TfIdfVectorizer from scikit-learnfrom 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
技术支持
title
Toy Story 0
Jumanji 1
Grumpier Old Men 2
Waiting to Exhale 3
Father of the Bride Part II4
Heat 5
Sabrina 6
Tom and Huck 7
Sudden Death 8
GoldenEye 9dtype: int64
技术支持
# Function that takes in movie title as input and outputs most similar moviesdefget_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 moviesreturn metadata['title'].iloc[movie_indices]技术支持
12481 The Dark Knight
150 Batman Forever
1328 Batman Returns
15511Batman: Under the Red Hood
585 Batman
21194 Batman Unmasked: The Psychology of the Dark Kn...9230 Batman Beyond: Return of the Joker
18035Batman: Year One
19792Batman: The Dark Knight Returns, Part 13095Batman: Mask of the Phantasm
Name: title,dtype: object
技术支持
1178 The Godfather: Part II44030 The Godfather Trilogy:1972-19901914 The Godfather: Part III23126 Blood Ties
11297 Household Saints
34717 Start Liquidation
10821 Election
38030A Mother Should Be Loved
17729 Short Sharp Shock
26293 Beck 28- Familjen
Name: title,dtype: object
技术支持
您会发现,虽然系统能找到剧情简介相似的电影,但推荐质量并不算很高。“The Dark Knight Rises”返回了所有蝙蝠侠电影,而更有可能的是,喜欢这部电影的人也更倾向于喜欢克里斯托弗·诺兰的其他电影。这是当前系统无法捕捉到的信息。
# Parse the stringified features into their corresponding python objectsfrom ast import literal_eval
features =['cast','crew','keywords','genres']for feature in features:
metadata[feature]= metadata[feature].apply(literal_eval)技术支持
defget_list(x):ifisinstance(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.iflen(names)>3:
names = names[:3]return names
#Return empty list in case of missing/malformed datareturn[]技术支持
# 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)技术支持
# Function to convert all strings to lower case and strip names of spacesdefclean_data(x):ifisinstance(x,list):return[str.lower(i.replace(" ",""))for i in x]else:#Check if director exists. If not, return empty stringifisinstance(x,str):returnstr.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)技术支持
# Compute the Cosine Similarity matrix based on the count_matrixfrom 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'])技术支持
1934 The Godfather: Part III1199 The Godfather: Part II15609 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
技术支持
基于用户的过滤:向用户推荐相似用户喜欢的产品。比如,假设 Alice 与 Bob 在图书偏好上很相似(即他们大体上喜欢/不喜欢相同的书)。现在市场上新出了本书,Alice 已读且很喜欢,那么 Bob 很可能也会喜欢,于是系统向 Bob 推荐这本书。
基于物品的过滤:与您构建的内容推荐引擎非常相似。这类系统根据人们过去的评分来识别相似的物品。例如,如果 Alice、Bob 与 Eve 都给《The Lord of the Rings》与《The Hobbit》打了 5 星,系统就会认为这两本书相似。因此,如果有人购买了《The Lord of the Rings》,系统也会向他/她推荐《The Hobbit》。
本教程的目的并不是让您成为构建推荐系统模型的专家,而是帮助您起步:了解现有推荐系统的类型,以及如何亲手构建一个。
在本教程中,您将学习如何搭建简单推荐器与基于内容推荐器的基础模型。虽然这些模型在复杂度、质量或准确性上都远达不到业界水准,但它们将帮助您入门,并为后续构建更复杂、效果更好的模型打下基础。
推荐系统是当今数据科学最流行的应用之一。它们用于预测用户对某个条目的“评分”或“偏好”。几乎每一家大型科技公司都以某种形式在使用它们。Amazon 用它向顾客推荐商品,YouTube 用它在自动播放中决定下一条视频,Facebook 用它推荐赞的页面与关注的人。
更进一步,对 Netflix、Amazon Prime、Hulu 和 Hotstar 等公司而言,其商业模式与成功与否很大程度上取决于推荐的效果。Netflix 甚至在 2009 年悬赏 100 万美元给任何能将其系统提升 10% 的人。
在餐厅、电影、在线约会等领域也有非常流行的推荐系统。推荐系统还被用于探索学术文章与专家、合作者以及金融服务。YouTube 在大规模上使用推荐系统,基于您的历史为您推荐视频。例如,如果您观看了很多教育类视频,它就会推荐这类视频。
那么,这些推荐系统究竟是什么?
大体上,推荐系统可分为 3 类:
简单推荐器
如上一节所述,简单推荐器是根据某个度量或分数推荐排名靠前条目的基础系统。在本节中,您将使用从 IMDB 收集的元数据,构建一个简化版的 IMDB Top 250 电影榜单。
涉及的步骤如下:
确定用于给电影打分的度量或分数。
为每部电影计算分数。
按分数对电影排序并输出排名靠前的结果。
关于数据集
数据集文件包含
Full MovieLens Dataset中所有 45,000 部电影的元数据。该数据集包含截至 2017 年 7 月之前上映的电影。其特征点包括演职员、剧情关键词、预算、票房、海报、上映日期、语言、制片公司、国家/地区、TMDB 投票次数与平均得分等。这些特征点可用于训练您的基于内容与协同过滤的机器学习模型。
该数据集包括以下文件:
Full MovieLens 数据集包含 2,600 万条评分与 75 万次标签应用,覆盖该数据集中的全部 45,000 部电影、270,000 名用户。可从官方 GroupLens 网站获取。
注意:本教程所用的数据子集可从此处下载。
要加载数据集,您将使用
pandas的 DataFrame 库。pandas主要用于数据处理与分析,以行-列格式表示数据。Pandas 库由NumPy数组为后盾,实现 pandas 数据对象。pandas提供现成的数据结构与操作,用于处理数值表格、时间序列、图像与自然语言处理数据集。基本上,pandas适用于那些能够以表格形式轻松表示的数据集。在执行上述步骤之前,先将电影元数据数据集加载到一个
pandasDataFrame 中:3 行 × 24 列
您能想到的最基础的度量之一是用评分来决定前 250 部电影的排名。
不过,把评分作为度量有一些问题:
首先,它没有考虑电影的受欢迎程度。因此,一部只有 10 人打分、评分 9 的电影,会被视为“优于”一部有 10,000 人打分、评分 8.9 的电影。
举个例子,设想您想点一份中餐,有两个选择:一家餐厅只有 5 个人评了 5 星,另一家有 1000 个人评了 4.5 星。您会选哪家?大概率是第二家,对吧?
当然也有例外,比如第一家餐厅可能刚开业几天,因此评分人数少;相反,第二家可能已经经营了一年。
考虑到这些不足,您需要提出加权评分,既考虑平均评分,也考虑累计投票数。这样可确保一部有 100,000 人打出 9 分的电影获得(远)高于同为 9 分但只有几百人打分的电影的分数。
既然要仿照 IMDB Top 250,不妨使用其加权评分公式作为度量/分数。从数学上,它表示如下:
\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}
在上式中,
v 是该电影的投票数;
m 是进入榜单所需的最小投票数;
R 是该电影的平均评分;
C 是全站的平均评分。
数据集中每部电影的 v
(vote_count)与 R(vote_average)已经有了。C 也可以直接从数据中计算。为
m选择合适的值是一个可调超参数,因为并不存在唯一正确的m。您可以把它当作初步的负向筛选器,用来剔除投票数低于某阈值m的电影。筛选的苛刻程度由您决定。本教程中,我们将
m设为第 90 百分位。换言之,要上榜,电影的投票数至少要超过列表中 90% 的电影。(相对地,如果选择第 75 百分位,就会考虑投票数排名前 25% 的电影。百分位越低,被考虑的电影越多)。第一步,使用 pandas 的
.mean()函数计算所有电影的平均评分 C:从上面的输出可以看出,IMDB 上电影的平均评分约为 5.6(满分 10)。
接下来,计算第 90 百分位电影的投票数 m。
pandas的.quantile()方法可轻松完成:现在得到
m之后,可以直接用大于等于的条件筛选出投票数不小于 160 的电影:您可以使用
.copy()方法确保新创建的q_moviesDataFrame 独立于原始的 metadata DataFrame。换言之,对 q_movies 的修改不会影响原始的 metadata。从以上输出可见,大约 10% 的电影投票数超过 160,符合上榜条件。
下一步也是最重要的一步:为每部合格电影计算加权评分。为此您将:
weighted_rating();m与C已经计算好,将它们作为参数传入函数;q_movies数据框中取出vote_count(v)与vote_average(R)列;您将定义一个新特征
score,其值通过将该函数应用到合格电影的 DataFrame 来计算:最后,按
score特征列降序排序 DataFrame,并输出前 20 部电影的标题、投票数、平均评分与加权评分(score)。从上述输出可以看到,
简单推荐器表现不错!该榜单与 IMDB Top 250 有很多重合之处:例如您的前两部电影“Shawshank Redemption”和“The Godfather”与 IMDB 一致,而我们都知道它们确实是佳作。事实上,前 20 部电影都值得在这个榜单上,是不是?
基于内容的推荐器
基于剧情简介的推荐器
在本节中,您将学习如何构建一个系统:针对某部电影,推荐与其相似的电影。为实现这一点,您将基于剧情简介计算所有电影之间的成对
余弦相似度,并基于该相似度阈值进行推荐。剧情简介在
metadata数据集中的overview特征中可用。我们先查看几部电影的简介:当前问题属于自然语言处理问题。因此,在计算相似或不相似之前,您需要先从上述文本数据中提取某种特征。简单说,无法直接对两段原始简介计算相似度。您需要为每段简介(下文称为“文档”)计算词向量。
顾名思义,词向量是文档中词语的向量化表示。向量承载语义信息。例如,man 与 king 的向量表示会彼此接近,而 man 与 woman 的表示会相距较远。
您将为每个文档计算
词频-逆文档频率(TF-IDF)向量。这将得到一个矩阵,其中每一列代表简介词汇表中的一个词(至少在一个文档中出现过的所有词),每一列仍对应一部电影。本质上,TF-IDF 分数是某词在文档中出现的频率,按其在多少文档中出现进行降权。这样做是为了降低那些在剧情简介中频繁出现、对最终相似度计算意义不大的词的重要性。
幸运的是,scikit-learn 提供了内置的
TfIdfVectorizer类,只需几行代码即可生成 TF-IDF 矩阵。从上面的输出可以观察到,数据集中 45,000 部电影产生了 75,827 个不同的词汇。
有了这个矩阵后,就可以计算相似度分数了。可用的相似度度量有多种,例如曼哈顿距离、欧几里得距离、皮尔逊相关与余弦相似度等。并没有唯一正确答案,不同的度量在不同场景下表现更好,通常建议尝试多种度量并观察结果。
您将使用
余弦相似度来计算两个电影之间的数值化相似度。之所以使用余弦相似度,是因为它与向量的模无关,且计算相对简单快速(尤其与 TF-IDF 分数结合时,后文会解释)。其数学定义如下:由于使用了 TF-IDF 向量化器,各向量之间的点积即可直接得到余弦相似度。因此,您将使用
sklearn 的 linear_kernel()而不是cosine_similarities(),因为它更快。这将返回一个 45466×45466 的矩阵,表示每部电影的
overview与其他每部电影overview的余弦相似度。因而,每部电影将对应一个 1×45466 的列向量,其中每一列是与某部电影的相似度分数。接下来,您将定义一个函数,输入电影标题,输出最相似的 10 部电影。首先,您需要一个标题与 DataFrame 索引的反向映射。换言之,需要一种机制:给定电影标题,能在
metadataDataFrame 中找到其索引。现在可以着手定义推荐函数了。将遵循以下步骤:
根据标题获取电影的索引。
获取该电影与所有电影的余弦相似度列表。将其转换为元组列表,其中第一个元素是位置,第二个元素是相似度分数。
按相似度分数(即第二个元素)对上述元组列表排序。
取该列表的前 10 个元素。忽略第一个元素,因为它是电影自身(与自己最相似的就是自己)。
返回这些前列元素索引对应的电影标题。
您会发现,虽然系统能找到剧情简介相似的电影,但推荐质量并不算很高。“The Dark Knight Rises”返回了所有蝙蝠侠电影,而更有可能的是,喜欢这部电影的人也更倾向于喜欢克里斯托弗·诺兰的其他电影。这是当前系统无法捕捉到的信息。
基于演职员、类型与关键词的推荐器
利用更好的元数据并捕捉更多细节可以提升推荐质量。本节正是要这样做。您将基于以下元数据构建推荐系统:排名前 3 的演员、导演、相关类型与电影剧情关键词。
当前数据集中没有关键词、演员与剧组数据,因此第一步是将它们加载并合并进主 DataFrame
metadata。2 行 × 27 列
对于新增的特征 cast、crew 与 keywords,您需要提取最重要的三位演员、导演以及与该电影关联的关键词。
但首先,数据目前以“字符串化”的列表形式存在。您需要把它们转换为可用的结构。
接下来,编写一些函数,帮助您从每个特征中提取所需信息。
首先导入 NumPy 以获得
NaN常量。随后编写get_director()函数:从 crew 特征中获取导演姓名。如果未列出导演,则返回
NaN接下来,您将编写一个函数,返回前三个元素或整个列表(二者取其少)。这里的列表指
cast、keywords与genres。下一步是将姓名与关键词实例转为小写,并去除其间的空格。
去除词间空格是一个重要的预处理步骤。这样可避免向量化器将 “Johnny Depp” 与 “Johnny Galecki” 的 “Johnny” 视为相同。处理之后,上述演员将分别表示为“johnnydepp”和“johnnygalecki”,从而在向量化器看来是不同的。
另一个例子是 “bread jam” 与 “traffic jam”,模型可能会输出相同的向量表示,因此最好去掉其中的空格。
下面的函数正是为此而写:
现在可以创建“metadata soup”了——一个包含您要喂给向量化器的所有元数据(演员、导演、关键词)的字符串。
create_soup函数会将所需列用空格连接。这是最后的预处理步骤,该函数的输出将送入词向量模型。接下来的步骤与
基于剧情简介的推荐器相同。一个关键区别是,这里使用CountVectorizer()而不是TF-IDF。原因是:如果某位演员/导演参演或执导了更多电影,您并不希望降低其出现的权重。在此情境下对其降权并不符合直觉。CountVectorizer()与TF-IDF的主要区别在于后者包含逆文档频率(IDF)成分,而前者没有。从上面的输出可以看到,您提供的元数据中包含 73,881 个词汇。
接着,您将使用
cosine_similarity来衡量嵌入之间的距离。现在,您可以复用
get_recommendations()函数,将新的cosine_sim2矩阵作为第二个参数传入。很好!由于使用了更多元数据,您的推荐器捕捉的信息更多,推荐也更优。当然,还有很多方法可以在此基础上继续试验改进。
一些建议:
引入流行度过滤:该推荐器先取最相似的 30 部电影,用上文的 IMDB 公式计算加权评分,按该评分排序,并返回前 10 部。
加入其他剧组成员:可以包含编剧、制片等其他剧组成员的姓名。
增加导演权重:可在 soup 中多次提及导演,从而提升同一导演电影之间的相似度。
用 Python 实现协同过滤
在本教程中,您已经学会如何构建简单与基于内容的电影推荐系统。还有另一类非常流行的推荐方法,即协同过滤。
协同过滤还可以进一步分为两类:
基于用户的过滤:向用户推荐相似用户喜欢的产品。比如,假设 Alice 与 Bob 在图书偏好上很相似(即他们大体上喜欢/不喜欢相同的书)。现在市场上新出了本书,Alice 已读且很喜欢,那么 Bob 很可能也会喜欢,于是系统向 Bob 推荐这本书。基于物品的过滤:与您构建的内容推荐引擎非常相似。这类系统根据人们过去的评分来识别相似的物品。例如,如果 Alice、Bob 与 Eve 都给《The Lord of the Rings》与《The Hobbit》打了 5 星,系统就会认为这两本书相似。因此,如果有人购买了《The Lord of the Rings》,系统也会向他/她推荐《The Hobbit》。基于评分系统的协同过滤示例:
本教程不会实现这些系统,但您已经熟悉了实现它们所需的大多数思想。一个很好的起点是研究 MovieLens 数据集,可在此处找到。
总结
恭喜您完成本教程!
您已经顺利完成关于 Python 推荐系统的学习,掌握了如何构建简单与基于内容的推荐器。
一个不错的练习是:使用本教程中的 MovieLens 数据子集,在 Python 中实现协同过滤。
如果您刚开始学习 Python 并希望进一步进阶,欢迎参加 DataCamp 的Introduction to Data Science in Python 课程。