跳至主要内容

初学者教程:用 Python 构建推荐系统

在 Python 的帮助下构建您的推荐引擎:从基础模型到基于内容与协同过滤的推荐系统。
· 15分钟 阅读

使用 AI 探索

recommender system
来源</a

本教程的目的并不是让您成为构建推荐系统模型的专家,而是帮助您起步:了解现有推荐系统的类型,以及如何亲手构建一个。

在本教程中,您将学习如何搭建简单推荐器与基于内容推荐器的基础模型。虽然这些模型在复杂度、质量或准确性上都远达不到业界水准,但它们将帮助您入门,并为后续构建更复杂、效果更好的模型打下基础。

推荐系统是当今数据科学最流行的应用之一。它们用于预测用户对某个条目的“评分”或“偏好”。几乎每一家大型科技公司都以某种形式在使用它们。Amazon 用它向顾客推荐商品,YouTube 用它在自动播放中决定下一条视频,Facebook 用它推荐赞的页面与关注的人。

更进一步,对 Netflix、Amazon Prime、Hulu 和 Hotstar 等公司而言,其商业模式与成功与否很大程度上取决于推荐的效果。Netflix 甚至在 2009 年悬赏 100 万美元给任何能将其系统提升 10% 的人。

在餐厅、电影、在线约会等领域也有非常流行的推荐系统。推荐系统还被用于探索学术文章与专家、合作者以及金融服务。YouTube 在大规模上使用推荐系统,基于您的历史为您推荐视频。例如,如果您观看了很多教育类视频,它就会推荐这类视频。

那么,这些推荐系统究竟是什么?

大体上,推荐系统可分为 3 类:

  • 简单推荐器:基于电影热度和/或类型,为每位用户提供通用推荐。其基本思想是,更受欢迎、评价更高的电影更有可能被普通观众喜欢。一个例子是 IMDB Top 250。
  • 基于内容的推荐器:基于某个条目推荐相似条目。该系统使用条目元数据,例如对电影而言的类型、导演、简介、演员等。其一般思想是,如果某人喜欢某个条目,他/她也会喜欢与之相似的条目。为进行推荐,它会利用用户过去条目的元数据。一个很好的例子是 YouTube,它会基于您的历史向您推荐可能会看的新视频。
  • 协同过滤引擎:这类系统应用广泛,试图根据其他用户过去的评分与偏好,预测某用户对某个条目的评分或偏好。协同过滤不需要像基于内容方法那样的条目元数据。

简单推荐器

如上一节所述,简单推荐器是根据某个度量或分数推荐排名靠前条目的基础系统。在本节中,您将使用从 IMDB 收集的元数据,构建一个简化版的 IMDB Top 250 电影榜单。

涉及的步骤如下:

  • 确定用于给电影打分的度量或分数。

  • 为每部电影计算分数。

  • 按分数对电影排序并输出排名靠前的结果。

关于数据集

数据集文件包含 Full MovieLens Dataset 中所有 45,000 部电影的元数据。该数据集包含截至 2017 年 7 月之前上映的电影。其特征点包括演职员、剧情关键词、预算、票房、海报、上映日期、语言、制片公司、国家/地区、TMDB 投票次数与平均得分等。

这些特征点可用于训练您的基于内容与协同过滤的机器学习模型。

该数据集包括以下文件:

  • movies_metadata.csv:包含 Full MovieLens 数据集中约 45,000 部电影的信息。特征包括海报、背景图、预算、类型、票房、上映日期、语言、制作国家/地区与公司等。
  • keywords.csv:包含 MovieLens 电影的剧情关键词。以字符串化的 JSON 对象形式提供。
  • credits.csv:包含所有电影的演员与剧组信息。以字符串化的 JSON 对象形式提供。
  • links.csv:包含 Full MovieLens 数据集中所有电影的 TMDB 与 IMDB ID。
  • links_small.csv:包含完整数据集中 9,000 部电影的小子集的 TMDB 与 IMDB ID。
  • ratings_small.csv:700 位用户对 9,000 部电影的 100,000 条评分子集。

Full MovieLens 数据集包含 2,600 万条评分与 75 万次标签应用,覆盖该数据集中的全部 45,000 部电影、270,000 名用户。可从官方 GroupLens 网站获取。

注意:本教程所用的数据子集可从此处下载。

要加载数据集,您将使用 pandas 的 DataFrame 库。pandas 主要用于数据处理与分析,以行-列格式表示数据。Pandas 库由 NumPy 数组为后盾,实现 pandas 数据对象。pandas 提供现成的数据结构与操作,用于处理数值表格、时间序列、图像与自然语言处理数据集。基本上,pandas 适用于那些能够以表格形式轻松表示的数据集。

在执行上述步骤之前,先将电影元数据数据集加载到一个 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 行 × 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:

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

从上面的输出可以看出,IMDB 上电影的平均评分约为 5.6(满分 10)。

接下来,计算第 90 百分位电影的投票数 m。pandas 的 .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

现在得到 m 之后,可以直接用大于等于的条件筛选出投票数不小于 160 的电影:

您可以使用 .copy() 方法确保新创建的 q_movies DataFrame 独立于原始的 metadata DataFrame。换言之,对 q_movies 的修改不会影响原始的 metadata。

# 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)

从以上输出可见,大约 10% 的电影投票数超过 160,符合上榜条件。

下一步也是最重要的一步:为每部合格电影计算加权评分。为此您将:

  • 定义函数 weighted_rating();
  • 由于 m 与 C 已经计算好,将它们作为参数传入函数;
  • 从 q_movies 数据框中取出 vote_count(v)与 vote_average(R)列;
  • 计算加权平均并返回结果。

您将定义一个新特征 score,其值通过将该函数应用到合格电影的 DataFrame 来计算:

# 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)

最后,按 score 特征列降序排序 DataFrame,并输出前 20 部电影的标题、投票数、平均评分与加权评分(score)。

#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

从上述输出可以看到,简单推荐器表现不错!

该榜单与 IMDB Top 250 有很多重合之处:例如您的前两部电影“Shawshank Redemption”和“The Godfather”与 IMDB 一致,而我们都知道它们确实是佳作。事实上,前 20 部电影都值得在这个榜单上,是不是?

基于内容的推荐器

基于剧情简介的推荐器

在本节中,您将学习如何构建一个系统:针对某部电影,推荐与其相似的电影。为实现这一点,您将基于剧情简介计算所有电影之间的成对余弦相似度,并基于该相似度阈值进行推荐。

剧情简介在 metadata 数据集中的 overview 特征中可用。我们先查看几部电影的简介:

#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

当前问题属于自然语言处理问题。因此,在计算相似或不相似之前,您需要先从上述文本数据中提取某种特征。简单说,无法直接对两段原始简介计算相似度。您需要为每段简介(下文称为“文档”)计算词向量。

顾名思义,词向量是文档中词语的向量化表示。向量承载语义信息。例如,man 与 king 的向量表示会彼此接近,而 man 与 woman 的表示会相距较远。

您将为每个文档计算词频-逆文档频率(TF-IDF)向量。这将得到一个矩阵,其中每一列代表简介词汇表中的一个词(至少在一个文档中出现过的所有词),每一列仍对应一部电影。

本质上,TF-IDF 分数是某词在文档中出现的频率,按其在多少文档中出现进行降权。这样做是为了降低那些在剧情简介中频繁出现、对最终相似度计算意义不大的词的重要性。

幸运的是,scikit-learn 提供了内置的 TfIdfVectorizer 类,只需几行代码即可生成 TF-IDF 矩阵。

  • 通过 scikit-learn 导入 Tfidf 模块;
  • 移除如“the”“an”等停用词,因为它们对主题没有有效信息;
  • 将非数字(NaN)值替换为空字符串;
  • 最后在数据上构建 TF-IDF 矩阵。
#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']

从上面的输出可以观察到,数据集中 45,000 部电影产生了 75,827 个不同的词汇。

有了这个矩阵后,就可以计算相似度分数了。可用的相似度度量有多种,例如曼哈顿距离、欧几里得距离、皮尔逊相关与余弦相似度等。并没有唯一正确答案,不同的度量在不同场景下表现更好,通常建议尝试多种度量并观察结果。

您将使用余弦相似度来计算两个电影之间的数值化相似度。之所以使用余弦相似度,是因为它与向量的模无关,且计算相对简单快速(尤其与 TF-IDF 分数结合时,后文会解释)。其数学定义如下:

definition

由于使用了 TF-IDF 向量化器,各向量之间的点积即可直接得到余弦相似度。因此,您将使用 sklearn 的 linear_kernel() 而不是 cosine_similarities(),因为它更快。

这将返回一个 45466×45466 的矩阵,表示每部电影的 overview 与其他每部电影 overview 的余弦相似度。因而,每部电影将对应一个 1×45466 的列向量,其中每一列是与某部电影的相似度分数。

# 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])

接下来,您将定义一个函数,输入电影标题,输出最相似的 10 部电影。首先,您需要一个标题与 DataFrame 索引的反向映射。换言之,需要一种机制:给定电影标题,能在 metadata DataFrame 中找到其索引。

#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

现在可以着手定义推荐函数了。将遵循以下步骤:

  • 根据标题获取电影的索引。

  • 获取该电影与所有电影的余弦相似度列表。将其转换为元组列表,其中第一个元素是位置,第二个元素是相似度分数。

  • 按相似度分数(即第二个元素)对上述元组列表排序。

  • 取该列表的前 10 个元素。忽略第一个元素,因为它是电影自身(与自己最相似的就是自己)。

  • 返回这些前列元素索引对应的电影标题。

# 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

您会发现,虽然系统能找到剧情简介相似的电影,但推荐质量并不算很高。“The Dark Knight Rises”返回了所有蝙蝠侠电影,而更有可能的是,喜欢这部电影的人也更倾向于喜欢克里斯托弗·诺兰的其他电影。这是当前系统无法捕捉到的信息。

基于演职员、类型与关键词的推荐器

利用更好的元数据并捕捉更多细节可以提升推荐质量。本节正是要这样做。您将基于以下元数据构建推荐系统:排名前 3 的演员、导演、相关类型与电影剧情关键词。

当前数据集中没有关键词、演员与剧组数据,因此第一步是将它们加载并合并进主 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 行 × 27 列

对于新增的特征 cast、crew 与 keywords,您需要提取最重要的三位演员、导演以及与该电影关联的关键词。

但首先,数据目前以“字符串化”的列表形式存在。您需要把它们转换为可用的结构。

# 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)

接下来,编写一些函数,帮助您从每个特征中提取所需信息。

首先导入 NumPy 以获得 NaN 常量。随后编写 get_director() 函数:

# Import Numpy
import numpy as np

从 crew 特征中获取导演姓名。如果未列出导演,则返回 NaN

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

接下来,您将编写一个函数,返回前三个元素或整个列表(二者取其少)。这里的列表指 cast、keywords 与 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]

下一步是将姓名与关键词实例转为小写,并去除其间的空格。

去除词间空格是一个重要的预处理步骤。这样可避免向量化器将 “Johnny Depp” 与 “Johnny Galecki” 的 “Johnny” 视为相同。处理之后,上述演员将分别表示为“johnnydepp”和“johnnygalecki”,从而在向量化器看来是不同的。

另一个例子是 “bread jam” 与 “traffic jam”,模型可能会输出相同的向量表示,因此最好去掉其中的空格。

下面的函数正是为此而写:

# 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)

现在可以创建“metadata soup”了——一个包含您要喂给向量化器的所有元数据(演员、导演、关键词)的字符串。

create_soup 函数会将所需列用空格连接。这是最后的预处理步骤,该函数的输出将送入词向量模型。

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 ...

接下来的步骤与基于剧情简介的推荐器相同。一个关键区别是,这里使用 CountVectorizer() 而不是 TF-IDF。原因是:如果某位演员/导演参演或执导了更多电影,您并不希望降低其出现的权重。在此情境下对其降权并不符合直觉。

CountVectorizer() 与 TF-IDF 的主要区别在于后者包含逆文档频率(IDF)成分,而前者没有。

# 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)

从上面的输出可以看到,您提供的元数据中包含 73,881 个词汇。

接着,您将使用 cosine_similarity 来衡量嵌入之间的距离。

# 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'])

现在,您可以复用 get_recommendations() 函数,将新的 cosine_sim2 矩阵作为第二个参数传入。

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

很好!由于使用了更多元数据,您的推荐器捕捉的信息更多,推荐也更优。当然,还有很多方法可以在此基础上继续试验改进。

一些建议:

  • 引入流行度过滤:该推荐器先取最相似的 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》。

基于评分系统的协同过滤示例:

graphic

本教程不会实现这些系统,但您已经熟悉了实现它们所需的大多数思想。一个很好的起点是研究 MovieLens 数据集,可在此处找到。

总结

恭喜您完成本教程!

您已经顺利完成关于 Python 推荐系统的学习,掌握了如何构建简单与基于内容的推荐器。

一个不错的练习是:使用本教程中的 MovieLens 数据子集,在 Python 中实现协同过滤。

如果您刚开始学习 Python 并希望进一步进阶,欢迎参加 DataCamp 的Introduction to Data Science in Python 课程。

主题