摘要: 本文教你如何构建属于你自己的推荐系统,不容错过。

clipboard.png
“empty brown theater chairs” by Tyler Callahan on Unsplash

你是否有过这样的疑惑:为什么Netflix,Amazon,Google总能推荐到你比较感兴趣的产品?我们有时会对互联网上的产品进行评分,以此体现我们对产品的偏好,同时,推荐系统会利用我们分享的数据,生成推荐结果。主流的推荐系统算法大致分为两类:基于用户历史数据的协同过滤算法和基于内容数据的过滤算法。两者的区别其实从名称上便可看出,但接下来我们将以电影推荐为例进一步阐述二者之间的不同。

协同过滤(Collaborative filters)

协同过滤依赖用户的历史评分数据,为用户推荐自己未曾看过,而与自己相似的用户已经观看过的电影。为了确定两个用户之间是否相似,协同过滤会结合用户所看过的电影以及他们对电影的评分。

clipboard.png
Collaborative-based filter.

协同过滤算法的准确性依赖于用户对产品的历史评分,但并非所有的用户都会持续不断的对产品进行评价,有一些用户甚至未曾评价过任何产品。协同过滤算法的另一个特点是能提供多样化的建议,根据应用场景的不同,对推荐系统的评价也不尽相同。举个例子,假设用户A非常喜欢反乌托邦电影和黑色喜剧,用户B也喜欢反乌托邦电影,但从来没有看过黑色喜剧。协同过滤算法将根据用户A和用户B对反乌托邦电影的喜爱,为用户B推荐黑色喜剧。这个推荐结果将会产生两种影响:用户B也非常喜欢黑色喜剧,则推荐成功;如若用户B喜欢轻喜剧,那么推荐是不成功的。

基于内容的过滤(Content-based filters)

clipboard.png
Content-based filter.

基于内容的推荐不再涉及其他用户,只根据我们自身的喜爱,简单的选择内容相似的项目进行推荐。

相比协同过滤算法,基于内容的推荐减少了推荐的多样性,但用户是否对项目进行了评分便不再影响推荐结果。还是前一个例子,也许用户B潜意识里也喜欢黑色喜剧,但除非他自己决定主动尝试,否则他永远也不会知道自己的这个喜好,因为基于内容的推荐只会继续推荐反乌托邦或同种类型的电影。以电影为例,在计算相似度的时候,除了考虑片名,还可以考虑导演,主要演员等因素。

到目前为止,我已多次提及到相似度(similarity)这个词,但是它究竟是什么呢?相似度是我们在计算用户之间或者项目之间的相似性时可以使用的度量标准之一。它虽然不可量化,但却是可以通过计算得到。在构建基于内容的推荐系统之前,我将简明地对相似度的概念做一个讲解。

余弦相似度(Cosine similarity)

向量可以是二维,三维甚至n维的。让我们以二维向量为例回顾一下点积(dot product)。两个向量之间的点积等于其中一个向量在另一个向量上的投影。因此,两个相同向量(即相同分量)之间的点积等于该向量模的平方,而如果这两个向量垂直,则点积为零。通常,对于n维向量,点积的计算公式如下所示。

clipboard.png
Dot product.

点积在计算相似度时非常重要,因为它与相似度直接相关。两个向量u和v之间相似度是由它们之间的点积和它们自身的模的比值定义的。

clipboard.png
Similarity.

透过相似度的定义我们可以看出,如果两个向量相同,相似度为1,如果两个向量是正交的,相似度为0。换句话说,相似度是一个在0和1之间有界的数,它反应了这两个向量的相似程度。

下面进入实战阶段。

1、数据收集

实验数据来自IMDB数据集,本次只选取了前250个高评分电影。数据集列表有250行(250部电影),38列。在构建模型的时候,我们只考虑了电影导演、主要演员、电影类型和电影情节这几类特征。

import pandas as pd
from rake_nltk import Rake
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.feature_extraction.text import CountVectorizer

df = pd.read_csv('https://query.data.world/s/uikepcpffyo2nhig52xxeevdialfl7')

df = df[['Title','Genre','Director','Actors','Plot']]
df.head()

部分数据如下图所示:

clipboard.png

我们将每一部包含上述特征的电影作为一列,以便能更好的进行向量化。我们还会使用到自然语言处理,将文字转换为向量能帮助我们更好的计算余弦相似度。

接下来,我们将对数据进行清洗。

2、数据清洗(Data cleaning)

nltk(natural language toolkit)是一套基于python的自然语言处理工具集,它能够帮助我们从文本中提取关键字,甚至可以为每个字打分。我们将使用Rake功能从Plot列中提取出关键字,相较于使用完整的句子对电影情节进行描述,我更青睐于使用一些与电影情节最相关的词语。为此,我在Plot列中,对每行都使用了Rake功能,将获取到的关键词独立作为新的一列,命名Key_words。

# initializing the new column
df['Key_words'] = ""
for index, row in df.iterrows():
    plot = row['Plot']
   
# instantiating Rake, by default it uses english stopwords from NLTK

# and discards all puntuation characters as well
    r = Rake()

# extracting the words by passing the text
    r.extract_keywords_from_text(plot)

# getting the dictionary whith key words as keys and their scores as values
    key_words_dict_scores = r.get_word_degrees()    

# assigning the key words to the new column for the corresponding movie
    row['Key_words'] = list(key_words_dict_scores.keys())
# dropping the Plot column
df.drop(columns = ['Plot'], inplace = True)

除了Plot列,还需要对其余列的数据进行清洗。同时,为了避免重复,需要对所有的内容进行小写转换,并且将所有的名和姓合并到一个单词中。试想:如果电影A的导演是Danny Boyle,而电影B的主要演员是Danny DeVito,那么电影A和B会因为Danny而拥有较高的相似度,但这并不是我们想要的。

在进行了所有的清理和合并之后,我将索引重新分配到movie title列,下图是为向量化准备的Dataframe。

clipboard.png

3、建模(Modeling)

为了充分利用NLP挖掘电影之间的相似性,我们需要将文字转为词向量。我更倾向于使用CountVecorizer而非TfIdfVecorizer,因为我只需要一个简单的频率计数器来统计bag_of_words列中的每个单词。Tf-Idf认为字词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降。这不适用于我们今天所讲的应用场景,毕竟每个单词对于相似度的衡量都非常重要。一旦我们得到了包含每个单词计数的矩阵,便可应用cosine_similarity函数对相似度进行计算。

# instantiating and generating the count matrix
count = CountVectorizer()
count_matrix = count.fit_transform(df['bag_of_words'])

# generating the cosine similarity matrix
cosine_sim = cosine_similarity(count_matrix, count_matrix)

相似度矩阵如下图所示:

clipboard.png

Similarity matrix.

对角线上的值都为1,因为每部电影在和自己比较时是完全相似的。同时,这是一个对称矩阵,因为电影A和B与电影B与A之间的相似度是相同的。

接下来,我们将电影标题作为输入,返回前10个类似的电影作为推荐结果。此外,我们还给电影标题加上了数字索引,以匹配相似矩阵到实际电影标题的索引。实际上,函数一旦接收到输入,就会检测出与所输入的电影相对应的行中最大的10个数字,获取相应的索引并将其与电影标题系列匹配,以返回推荐的电影列表。当函数选取10个最高的相似度值时,丢弃了单位值,这样就不会返回与输入相同的电影标题。

# creating a Series for the movie titles so they are associated to an ordered numerical
# list I will use in the function to match the indexes
indices = pd.Series(df.index)

#  defining the function that takes in movie title 
# as input and returns the top 10 recommended movies
def recommendations(title, cosine_sim = cosine_sim):
    
# initializing the empty list of recommended movies
    recommended_movies = []
    
# gettin the index of the movie that matches the title
    idx = indices[indices == title].index[0]

# creating a Series with the similarity scores in descending order
    score_series = pd.Series(cosine_sim[idx]).sort_values(ascending = False)

# getting the indexes of the 10 most similar movies
    top_10_indexes = list(score_series.iloc[1:11].index)
    
# populating the list with the titles of the best 10 matching movies
    for i in top_10_indexes:
        recommended_movies.append(list(df.index)[i])
        
    return recommended_movies

4、推荐系统测试(Testing the recommender)

由于我们只是用了包含250部电影的数据集,所构建的推荐系统性能有限。在测试的时候,输入了我喜爱的电影“Fargo”,下图为推荐的前10部电影。

clipboard.png

Movies recommended because I like Fargo.

我对推荐结果是满意的,从导演和情节上可以看出它们与我喜爱的电影有一些相似之处。上面列表中有我已经看过的电影,我喜欢它们就像我喜欢“Fargo”一样,接下来我会去看列表中我还没有看过的那几部电影。

本文作者:【方向】

阅读原文

本文为云栖社区原创内容,未经允许不得转载。


数据库知识分享者
27.8k 声望35.7k 粉丝

数据库知识分享