原文:
annas-archive.org/md5/8575b367f198fddfeb9dd19627d153b8
译者:飞龙
前言
推荐系统是几乎所有互联网业务的核心,从 Facebook 到 Netflix 再到 Amazon。提供好的推荐,无论是朋友、电影还是食品,都能在定义用户体验和吸引客户使用和购买平台上的商品方面发挥重要作用。
本书将向您展示如何做到这一点。您将了解行业中使用的不同类型的推荐系统,并学习如何从零开始使用 Python 构建它们。不需要翻阅大量的线性代数和机器学习理论,您将尽快开始构建并了解推荐系统。
在本书中,您将构建一个 IMDB Top 250 克隆,基于电影元数据的内容推荐引擎,利用客户行为数据的协同过滤器,以及一个结合了基于内容和协同过滤技术的混合推荐系统。
通过本书,您只需具备 Python 基础知识,就可以开始构建推荐系统,完成后,您将深入理解推荐系统的工作原理,并能够将所学的技术应用到自己的问题领域。
本书适合的人群
如果您是 Python 开发者,想要开发社交网络、新闻个性化或智能广告应用,那么这本书适合您。机器学习技术的基础知识会有所帮助,但并非必需。
本书内容概述
第一章,推荐系统入门,介绍了推荐问题及其常用的解决模型。
第二章,使用 Pandas 库处理数据,展示了使用 Pandas 库进行各种数据清洗技术的应用。
第三章,使用 Pandas 构建 IMDB Top 250 克隆,带领您完成构建热门电影榜单和明确考虑用户偏好的基于知识的推荐系统的过程。
第四章,构建基于内容的推荐系统,描述了如何构建利用电影情节和其他元数据提供推荐的模型。
第五章,数据挖掘技术入门,介绍了构建和评估协同过滤推荐模型时使用的各种相似度评分、机器学习技术和评估指标。
第六章,构建协同过滤器,带领您构建各种利用用户评分数据进行推荐的协同过滤器。
第七章,混合推荐系统,概述了实践中使用的各种混合推荐系统,并带您了解如何构建一个结合内容和协同过滤的模型。
为了充分利用本书
本书将为您提供最大益处,前提是您有一定的 Python 开发经验,或者您只是希望开发社交网络、新闻个性化或智能广告应用程序的读者,那么这本书就是为您量身定做的。如果您对机器学习(ML)有一些了解,会有所帮助,但不是必需的。
下载示例代码文件
您可以从您的账户中下载本书的示例代码文件,网址为www.packtpub.com。如果您是在其他地方购买的本书,可以访问www.packtpub.com/support,并注册以直接将文件通过邮件发送给您。
您可以按照以下步骤下载代码文件:
-
登录或注册至www.packtpub.com。
-
选择“支持”标签。
-
点击“代码下载 & 勘误”。
-
在搜索框中输入书名,并按照屏幕上的说明操作。
下载文件后,请确保使用最新版本的工具解压或提取文件夹:
-
适用于 Windows 的 WinRAR/7-Zip
-
适用于 Mac 的 Zipeg/iZip/UnRarX
-
适用于 Linux 的 7-Zip/PeaZip
本书的代码包也托管在 GitHub 上,网址是github.com/PacktPublishing/Hands-On-Recommendation-Systems-with-Python
。如果代码有更新,将会在现有的 GitHub 仓库中进行更新。
我们还提供了来自我们丰富的书籍和视频目录中的其他代码包,您可以在**github.com/PacktPublishing/
**找到它们。快去看看吧!
下载彩色图像
我们还提供了一个包含本书中使用的截图/图表的彩色图像的 PDF 文件。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/HandsOnRecommendationSystemswithPython_ColorImages.pdf
。
代码示例
访问以下链接,查看代码运行的视频:
使用的规范
本书中使用了多种文本规范。
CodeInText
:表示文本中的代码词语、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。例如:“现在让我们使用surprise
包实现 SVD 过滤器。”
一段代码如下所示:
#Import SVD
from surprise import SVD
#Define the SVD algorithm object
svd = SVD()
#Evaluate the performance in terms of RMSE
evaluate(svd, data, measures=['RMSE'])
当我们希望您注意到代码块中的特定部分时,相关的行或项目会加粗显示:
else:
#Default to a rating of 3.0 in the absence of any information
wmean_rating = 3.0
return wmean_rating
score(cf_user_wmean)
OUTPUT:
1.0174483808407588
任何命令行输入或输出格式如下所示:
sudo pip3 install scikit-surprise
粗体:表示新术语、重要单词或您在屏幕上看到的文字。例如,菜单或对话框中的单词会以这种方式显示在文本中。这里是一个例子:“我们看到 u.user
文件包含有关用户的统计信息,如他们的年龄、性别、职业和邮政编码。”
警告或重要说明以这种方式展示。
提示和技巧以这种方式展示。
联系我们
我们始终欢迎读者的反馈。
一般反馈:请发送邮件至 feedback@packtpub.com
,并在邮件主题中提及书籍标题。如果您有任何关于本书的问题,请通过 questions@packtpub.com
与我们联系。
勘误:尽管我们已尽力确保内容的准确性,但错误仍然可能发生。如果您在本书中发现错误,我们将不胜感激,如果您能向我们报告。请访问 www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表格链接并输入相关信息。
盗版:如果您在互联网上遇到我们作品的任何非法复制版本,我们将非常感激您能提供该文件的位置或网站名称。请通过 copyright@packtpub.com
联系我们,并附上相关材料的链接。
如果您有兴趣成为作者:如果您在某个领域具有专业知识并且有兴趣撰写或贡献书籍内容,请访问 authors.packtpub.com。
评价
请留下您的评价。阅读并使用本书后,为什么不在您购买书籍的网站上留下评价呢?潜在读者可以通过您的公正评价做出购买决策,我们 Packt 也能了解您对我们产品的看法,我们的作者也能看到您对他们书籍的反馈。谢谢!
更多关于 Packt 的信息,请访问 packtpub.com。
第一章:推荐系统入门
几乎我们今天购买或消费的每样东西,都受到了某种形式的推荐的影响;无论是来自朋友、家人、外部评论,还是更近期的,来自卖方的推荐。当你登录 Netflix 或 Amazon Prime 时,例如,你会看到服务根据你过去的观看(和评分)历史,认为你会喜欢的电影和电视节目列表。Facebook 会推荐它认为你可能认识并可能想加为好友的人。它还会根据你喜欢的帖子、你交的朋友以及你关注的页面,为你精选新闻动态。Amazon 会在你浏览特定产品时推荐商品。它会展示来自竞争商家的类似商品,并建议与该商品 常一起购买 的附加商品。
因此,不言而喻,为这些公司提供一个好的推荐系统是其成功商业的核心。Netflix 最希望通过你喜欢的内容吸引你,这样你就会继续订阅其服务;Amazon 向你展示的项目越相关,你的购买几率和数量就越大,这直接转化为更高的利润。同样,建立 友谊 对于 Facebook 作为一个几乎无所不能的社交网络的力量和影响力至关重要,Facebook 利用这一点从广告中获取大量收入。
在本章的介绍中,我们将了解推荐系统的世界,涵盖以下主题:
-
什么是推荐系统?它能做什么,不能做什么?
-
推荐系统的不同类型
技术要求
你需要在系统中安装 Python。最后,为了使用本书的 Git 仓库,用户需要安装 Git。
本章的代码文件可以在 GitHub 上找到:
github.com/PacktPublishing/Hands-On-Recommendation-Systems-with-Python
.
查看以下视频,看看代码如何实际运行:
什么是推荐系统?
推荐系统很容易理解;顾名思义,它们是推荐或建议特定产品、服务或实体的系统或技术。然而,这些系统可以根据其提供推荐的方法,分为以下两类。
预测问题
在这个问题版本中,我们给定了一个 m 用户和 n 项目的矩阵。矩阵的每一行代表一个用户,每一列代表一个项目。矩阵中第 i^(行) 和 j^(列) 位置的值表示用户 i 给项目 j 的评分。这个值通常表示为 r[ij]。
例如,考虑下图中的矩阵:
这个矩阵包含了七个用户对六个项目的评分。因此,m = 7 和 n = 6。用户 1 给项目 1 的评分是 4。因此,r[11] = 4。
现在让我们考虑一个更具体的例子。假设你是 Netflix,拥有一个包含 20,000 部电影和 5,000 名用户的库。你有一个系统记录每个用户给特定电影的评分。换句话说,你拥有一个评分矩阵(形状为 5,000 × 20,000)。
然而,你的所有用户只能看到你网站上部分电影的内容;因此,你拥有的矩阵是稀疏的。换句话说,你的矩阵中大部分条目是空的,因为大多数用户没有对大部分电影进行评分。
因此,预测问题旨在使用所有可用的信息(已记录的评分、电影数据、用户数据等)来预测这些缺失的值。如果它能够准确地预测缺失的值,就能够提供很好的推荐。例如,如果用户 i 没有使用项目 j,但我们的系统预测出一个非常高的评分(表示为 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-rec-sys-py/img/57a418b0-5fe0-41be-a202-275bc4162f83.png[ij]),那么用户 i 很可能会喜欢 j,只要他们通过系统发现它。
排名问题
排名是推荐问题的更直观的表述。给定一组 n 个项目,排名问题试图辨别出推荐给特定用户的前 k 个项目,利用所有可用的信息。
假设你是 Airbnb,就像前面的例子一样。你的用户已经输入了他们寻找房东和空间的具体要求(比如位置和预算)。你希望展示符合这些条件的前 10 个结果。这将是一个排名问题的例子。
很容易看出,预测问题通常可以简化为排名问题。如果我们能够预测缺失的值,就可以提取出最好的值并将其显示为我们的结果。
在本书中,我们将探讨这两种方法,并构建能够有效解决这些问题的系统。
推荐系统的类型
在推荐系统中,正如几乎所有其他机器学习问题一样,你使用的技术和模型(以及你取得的成功)在很大程度上依赖于你拥有的数据的数量和质量。在本节中,我们将概览三种最受欢迎的推荐系统类型,按照它们所需的数据量递减的顺序来展示。
协同过滤
协同过滤利用社区的力量来提供推荐。协同过滤器是业界最受欢迎的推荐模型之一,已经为像亚马逊这样的公司带来了巨大的成功。协同过滤可以大致分为两种类型。
基于用户的过滤
用户基于过滤的主要思想是,如果我们能够找到过去购买并喜欢相似物品的用户,那么他们未来也更可能购买相似的物品。因此,这些模型会根据类似用户的喜好推荐物品。亚马逊的购买此商品的客户还购买了就是这一过滤器的例子,如下图所示:
假设艾丽丝和鲍勃大多数时候喜欢和不喜欢相同的视频游戏。现在,假设市场上推出了一款新的视频游戏。假设艾丽丝购买了这款游戏并且非常喜欢。因为我们已经辨别出他们在视频游戏上的品味极其相似,所以鲍勃也很可能会喜欢这款游戏;因此,系统会将这款新游戏推荐给鲍勃。
基于物品的过滤
如果一群人对两件物品的评价相似,那么这两件物品必须是相似的。因此,如果一个人喜欢某个特定的物品,他们也很可能对另一个物品感兴趣。这就是基于物品的过滤方法的原理。亚马逊通过根据你的浏览和购买历史推荐产品,很好地利用了这一模型,如下图所示:
基于物品的过滤器因此是根据用户过去的评分来推荐物品。例如,假设艾丽丝、鲍勃和伊芙都给*《战争与和平》和《道林·格雷的画像》打了“极好”的分。那么,当有人购买《卡拉马佐夫兄弟》时,系统会推荐《战争与和平》*,因为它识别到,在大多数情况下,如果有人喜欢其中一本书,他们也会喜欢另一本书。
缺点
协同过滤系统的最大前提之一是过去活动数据的可用性。亚马逊之所以能够如此有效地利用协同过滤,是因为它可以访问数百万用户的购买数据。
因此,协同过滤存在我们所说的冷启动问题。*假设你刚刚启动了一个电子商务网站——要建立一个好的协同过滤系统,你需要大量用户的购买数据。然而,你没有这些数据,因此很难从零开始构建这样的系统。
基于内容的系统
与协同过滤不同,基于内容的系统不需要过去活动的数据。相反,它们根据用户的个人资料和对特定物品的元数据提供推荐。
Netflix 是上述系统的一个优秀例子。你第一次登录 Netflix 时,它并不知道你喜欢或不喜欢什么,因此无法找到与你相似的用户并推荐他们喜欢的电影和节目。
如前所示,Netflix 所做的是要求你评分一些你曾经看过的电影。基于这些信息以及它已经拥有的电影元数据,它为你创建了一个观影清单。例如,如果你喜欢哈利·波特和纳尼亚传奇电影,基于内容的系统可以识别出你喜欢基于奇幻小说的电影,并会推荐像指环王这样的电影给你。
然而,由于基于内容的系统没有利用社区的力量,它们往往会给出一些不如协同过滤系统所提供的结果那么令人印象深刻或相关。换句话说,基于内容的系统通常会提供显而易见的推荐。如果哈利·波特是你最喜欢的电影,那么指环王的推荐就缺乏新意。
基于知识的推荐系统
基于知识的推荐系统适用于那些极少被购买的商品。仅仅依靠过去的购买活动或通过建立用户档案来推荐此类商品是不可能的。以房地产为例,房地产通常是家庭一次性的大宗购买。现有用户没有足够的房地产购买历史来进行协同过滤,也并不总是可行去询问用户他们的房地产购买历史。
在这种情况下,你构建一个系统,询问用户一些具体要求和偏好,然后提供符合这些条件的推荐。例如,在房地产的例子中,你可以询问用户关于房子的需求,比如位置、预算、房间数量、楼层数等。根据这些信息,你可以推荐符合上述条件的房产。
基于知识的推荐系统也面临着低新颖性的问题。用户通常知道结果会是什么,因此很少会感到惊讶。
混合推荐系统
如其名所示,混合推荐系统是强大的系统,它结合了多种推荐模型,包括我们之前已经解释过的那些模型。正如我们在前面几节中看到的,每个模型都有其优缺点。混合系统试图将一个模型的劣势与另一个模型的优势相抵消。
再次考虑 Netflix 的例子。当你第一次登录时,Netflix 通过使用基于内容的推荐系统克服了协同过滤的冷启动问题,并且随着你逐渐开始观看和评分电影,它会启动协同过滤机制。这种方法更为成功,因此大多数实际的推荐系统本质上是混合型的。
在本书中,我们将构建每种类型的推荐系统,并将检查前面部分描述的所有优缺点。
总结
在本章中,我们概述了推荐系统的世界。我们看到了两种解决推荐问题的方法:即预测和排序。最后,我们考察了各种类型的推荐系统,并讨论了它们的优缺点。
在下一章中,我们将学习如何使用 pandas 来处理数据,pandas 是 Python 中首选的数据分析库。这将帮助我们构建我们介绍过的各种推荐系统。
第二章:使用 pandas 库操作数据
在接下来的章节中,我们将通过构建第一章中介绍的各种推荐系统来动手实践。不过,在此之前,我们需要了解如何高效地在 Python 中处理、操作和分析数据。
我们将使用的数据集将有几兆字节大。历史上,Python 一直以执行速度较慢而闻名。因此,使用原生 Python 及其内置的数据结构来分析如此庞大的数据量几乎是不可能的。
在本章中,我们将熟悉 pandas 库,它旨在克服前面提到的限制,使得在 Python 中进行数据分析变得极为高效且用户友好。我们还将介绍我们将用来构建推荐系统的电影数据集,并利用 pandas 提取一些有趣的事实,通过数据叙述电影的历史。
免责声明: 如果你已经熟悉 pandas 库,可以跳过本章,直接进入下一章,使用 pandas 构建 IMDB Top 250 克隆。
技术要求
你需要在系统上安装 Python。最后,为了使用本书的 Git 仓库,用户需要安装 Git。
本章的代码文件可以在 GitHub 上找到:
github.com/PacktPublishing/Hands-On-Recommendation-Systems-with-Python
.
查看以下视频,看看代码如何运行:
设置环境
在开始编码之前,我们可能需要设置开发环境。对于使用 Python 的数据科学家和分析师来说,Jupyter Notebook 是迄今为止最流行的开发工具。因此,我们强烈建议你使用这个环境。
我们还需要下载 pandas 库。获取这两个库最简单的方式是下载 Anaconda。Anaconda 是一个发行版,包含了 Jupyter 软件和 SciPy 包(其中包括 pandas)。
你可以在此下载发行版**😗* www.anaconda.com/download/
。
下一步是在你希望的位置创建一个新文件夹(我将其命名为 RecoSys
)。这将是包含我们在本书中编写的所有代码的主文件夹。在这个文件夹内,创建一个名为 Chapter2
的子文件夹,它将包含我们在本章中编写的所有代码。
接下来,打开你的终端应用程序,导航到 Chapter2
文件夹,并运行 jupyter notebook
命令。如果你在 Mac 或 Linux 上,命令应该如下所示(Windows 中的 cd 路径会有所不同):
[rounakbanik:~]$ cd RecoSys/Chapter2
[rounakbanik:~/RecoSys/Chapter2]$ jupyter notebook
Jupyter Notebook 运行在本地浏览器中。因此,它们与操作系统无关。换句话说,无论你是在 Mac、PC 还是 Linux 系统上,体验都将是一样的。
执行 jupyter notebook
命令后,你的默认浏览器应该会打开并显示 localhost:8888/tree
的 URL,窗口看起来如下所示:
在窗口的右侧,你应该能看到一个 New 下拉菜单。点击它并创建一个新的 Python 3(或 Python 2)Notebook。这样做会打开一个新标签页,其中包含一个未命名的 Notebook。你还可以看到一个带有指针的输入单元格。这是我们编写代码(和 markdown)的空间。接下来,请键入以下几行代码:
import pandas as pd
pd.__version__
要执行此单元格中的代码,请按 Shift + Enter。如果一切顺利,你应该会看到一个新的输出单元格,显示 Pandas 库的版本(对于我们来说是 0.20.3):
恭喜!
你现在已经成功设置了开发环境。当然,Jupyter Notebook 的功能远不止于运行单元格。当我们使用这些其他功能时,我们会进行讲解。由于这不是一本关于 Jupyter 的书籍,如果你有兴趣首先学习 Jupyter Notebook 的基础知识,我们会将你引导到网上的免费教程。DataCamp 上有一篇关于这个主题的权威文章。
你可以在这里找到 DataCamp Jupyter Notebook 教程:www.datacamp.com/community/tutorials/tutorial-jupyter-notebook
。
如果你在设置环境时遇到问题,Google 错误信息应该能引导你找到提供合适解决方案的页面。像 Stack Overflow 这样的网站上有成千上万的关于 Anaconda 设置的问题,你遇到的问题极有可能是别人之前也遇到过的。
Pandas 库
Pandas 是一个让我们能够访问高性能、易于使用的数据分析工具和数据结构的 Python 包。
如我们在介绍中所述,Python 是一种较慢的语言。Pandas 通过使用 C 编程语言进行大量优化来克服这一点。它还为我们提供了 Series 和 DataFrame 这两种极其强大且用户友好的数据结构,它们来自 R 统计包。
Pandas 还使得从外部文件导入数据到 Python 环境变得轻而易举。它支持多种格式,如 JSON、CSV、HDF5、SQL、NPY 和 XLSX。
作为使用 Pandas 的第一步,首先让我们将电影数据导入到我们的 Jupyter Notebook 中。为此,我们需要知道数据集所在的位置路径。这个路径可以是互联网上的 URL,也可以是你本地计算机上的路径。我们强烈建议将数据下载到本地计算机并通过本地路径访问,而不是通过网络 URL。
访问以下网址下载所需的 CSV 文件**😗* www.kaggle.com/rounakbanik/the-movies-dataset/downloads/movies_metadata.csv/7.
在RecoSys
目录下创建一个名为data
的新文件夹,并将刚下载的movies_metadata.csv
文件移动到该文件夹中。现在,让我们见证一些 pandas 的魔法。在你之前运行的 Jupyter Notebook 中,转到第二个单元格并输入以下代码:
#Read the CSV File into df
df = pd.read_csv('../data/movies_metadata.csv')
#We will find out what the following code does a little later!
df.head()
Voilà! 你应该能看到一个类似表格的结构,包含五行数据,每行代表一部电影。你还会看到表格有 24 列,尽管这些列被截断以适应显示。
那么,这个结构到底是什么呢?让我们通过运行熟悉的type
命令来了解:
#Output the type of df
type(df)
你应该会看到输出内容显示 df 是一个pandas.core.frame.DataFrame
。 换句话说,我们的代码已经将 CSV 文件读取到一个 pandas DataFrame 对象中。但 DataFrame 到底是什么?让我们在下一节中解答这个问题。
Pandas DataFrame
正如我们在上一节中看到的,df.head()
代码输出了一个类似表格的结构。本质上,DataFrame 就是这样:一个二维数据结构,包含了不同数据类型的列。你可以将它看作一个 SQL 表格。当然,仅仅是行和列的表格并不是 DataFrame 特别之处。DataFrame 为我们提供了丰富的功能,部分功能我们将在这一节中探讨。
我们的 DataFrame 中的每一行代表一部电影。那么有多少部电影呢?我们可以通过运行以下代码来找出答案:
#Output the shape of df
df.shape
OUTPUT:
(45466, 24)
结果给出了 df 中的行数和列数。我们可以看到,数据中包含了 45,466 部电影的信息。
我们还可以看到共有 24 列,每列代表一个特征或关于电影的元数据。当我们运行df.head()
*时,*我们看到大多数列被截断,以适应显示空间。为了查看所有的列(以下简称为特征),我们可以运行以下代码:
#Output the columns of df
df.columns
OUTPUT:
Index(['adult', 'belongs_to_collection', 'budget', 'genres', 'homepage', 'id',
'imdb_id', 'original_language', 'original_title', 'overview',
'popularity', 'poster_path', 'production_companies',
'production_countries', 'release_date', 'revenue', 'runtime',
'spoken_languages', 'status', 'tagline', 'title', 'video',
'vote_average', 'vote_count'],
dtype='object')
我们可以看到这些电影包含了很多信息,包括它们的标题、预算、类型、发布日期和收入等。
接下来,让我们了解如何访问特定的电影(或行)。第一种方法是使用.iloc
方法。通过这种方式,我们可以根据数值位置选择行,从零开始。例如,如果我们想访问 DataFrame 中的第二部电影,可以运行以下代码:
#Select the second movie in df
second = df.iloc[1]
second
输出将会给你关于电影的 24 个特征的信息。我们可以看到电影的标题是Jumanji,并且它是在 1995 年 12 月 15 日上映的,除此之外还有其他信息。
每个单元格都会输出最后一行代码的结果。因此,我们不需要在print
函数中显式写出它。
第二种方法是通过访问 DataFrame 的索引。由于在读取 CSV 文件时我们没有显式设置索引,pandas 默认将其设置为零基索引。我们可以轻松地更改 df 的索引。让我们将索引更改为电影标题,并尝试使用该索引访问Jumanji
:
#Change the index to the title
df = df.set_index('title')
#Access the movie with title 'Jumanji'
jum = df.loc['Jumanji']
jum
你应该看到与之前单元格完全相同的输出。让我们恢复为零基数字索引:
#Revert back to the previous zero-based indexing
df = df.reset_index()
还可以创建一个新的、更小的 DataFrame,包含更少的列。让我们创建一个只包含以下特征的新 DataFrame:title
、release_date
、budget
、revenue
、runtime
和genres
:
#Create a smaller dataframe with a subset of all features
small_df = df[['title', 'release_date', 'budget', 'revenue', 'runtime', 'genres']]
#Output only the first 5 rows of small_df
small_df.head()
你应该看到一个包含五部电影的表格,并且只有我们提到的特征。.head()
方法仅显示 DataFrame 的前五行。你可以通过将行数作为参数传递给.head()
来显示任意数量的行:
#Display the first 15 rows
small_df.head(15)
接下来,让我们查看各种特征的数据类型:
#Get information of the data types of each feature
small_df.info()
OUTPUT:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 45466 entries, 0 to 45465
Data columns (total 6 columns):
title 45460 non-null object
release_date 45379 non-null object
budget 45466 non-null object
revenue 45460 non-null float64
runtime 45203 non-null float64
genres 45466 non-null object
dtypes: float64(2), object(4)
memory usage: 2.1+ MB
一个有趣的观察是,pandas 正确地将revenue
和runtime
解读为 float 类型数据,但将budget
分配为通用的 object 数据类型*。*
然而,pandas 允许我们手动转换特征的数据类型。让我们尝试将budget
特征转换为float
:
#Convert budget to float
df['budget'] = df['budget'].astype('float')
OUTPUT: ...
...
ValueError: could not convert string to float: '/zaSf5OG7V8X8gqFvly88zDdRm46.jpg'
运行此单元格会抛出ValueError
。很容易猜测,某个预算字段的值是'/zaSf...'
这样的字符串,pandas 无法将其转换为浮动数字。
为了解决这个问题,我们将使用apply()
方法。这将允许我们对特定列的每个字段应用一个函数,并将其转换为返回值。我们将把budget
中的每个数字字段转换为 float,如果失败,则转换为NaN
:
#Import the numpy library
import numpy as np
#Function to convert to float manually
def to_float(x):
try:
x = float(x)
except:
x = np.nan
return x
#Apply the to_float function to all values in the budget column
small_df['budget'] = small_df['budget'].apply(to_float)
#Try converting to float using pandas astype
small_df['budget'] = small_df['budget'].astype('float')
#Get the data types for all features
small_df.info()
这次没有抛出任何错误。我们还注意到,budget
特征现在是float64
类型。
现在,让我们尝试定义一个新特征,叫做year
,表示发行年份。推荐的做法是使用 pandas 提供的datetime
功能:
#Convert release_date into pandas datetime format
small_df['release_date'] = pd.to_datetime(small_df['release_date'], errors='coerce')
#Extract year from the datetime
small_df['year'] = small_df['release_date'].apply(lambda x: str(x).split('-')[0] if x != np.nan else np.nan)
#Display the DataFrame with the new 'year' feature
small_df.head()
数据集中有哪些最古老的电影?为了回答这个问题,我们可以根据发行年份对 DataFrame 进行排序:
#Sort DataFrame based on release year
small_df = small_df.sort_values('year')
small_df.head()
我们看到从 1870 年代开始就有电影,其中*《金星的通道》*是已记录的最古老电影。接下来,让我们找出史上最成功的电影。为此,我们将再次使用sort_values()
方法,但加上额外的ascending=False
参数来按降序排序DataFrame
:
#Sort Movies based on revenue (in descending order)
small_df = small_df.sort_values('revenue', ascending=False)
small_df.head()
从我们的结果来看,我们观察到*《阿凡达》*是史上最成功的电影,收入超过 27.8 亿美元。
假设我们想创建一个符合某个条件的新电影 DataFrame。例如,我们只想要那些收入超过 10 亿美元的电影。pandas 通过其布尔索引功能使这成为可能。让我们来看一下操作:
#Select only those movies which earned more than 1 billion
new = small_df[small_df['revenue'] > 1e9]
new
也可以应用多个条件。例如,假设我们只想要那些收入超过 10 亿美元但开销少于 1.5 亿美元的电影,我们可以如下操作:
#Select only those movies which earned more than 1 billion and spent less than 150 million
new2 = small_df[(small_df['revenue'] > 1e9) & (small_df['budget'] < 1.5e8)]
new2
只有四部电影进入了这个榜单。
当然,DataFrame 还有很多其他功能(比如处理缺失数据),但我们先暂停对它的探索。接下来,我们将介绍在本节中我们无意间广泛使用的数据结构:Pandas Series。
Pandas Series
当我们使用.loc
和.iloc
访问 Jumanji 电影时,返回的数据结构是 Pandas Series 对象。你可能还注意到,我们通过df[column_name]
访问整列数据,这同样是一个 Pandas Series 对象:
type(small_df['year'])
OUTPUT:
pandas.core.series.Series
Pandas Series 是一种一维标签数组,可以包含任何类型的数据。你可以把它看作是一个增强版的 Python 列表。当我们在前一部分使用.apply()
和.astype()
方法时,实际上我们是在对这些 Series 对象进行操作。
因此,像 DataFrame 一样,Series 对象也具有一组极其有用的方法,使得数据分析变得轻松自如。
首先,让我们看看史上最短和最长的电影时长。我们将通过访问 DataFrame 中的runtime
列,将其作为 Series 对象,并应用其方法来实现:
#Get the runtime Series object
runtime = small_df['runtime']
#Print the longest runtime of any movie
print(runtime.max())
#Print the shortest runtime of any movie
print(runtime.min())
我们看到,最长的电影长度超过 1256 分钟,而最短的竟然是 0 分钟!当然,这样的异常结果需要对数据进行更深入的检查,但我们现在先跳过这一部分。
同样,也可以通过这种方式计算 Series 的均值和中位数。让我们对电影预算进行计算:
#Get the budget Series object
budget = small_df['budget']
#Print the mean budget of the movies
print(budget.mean())
#Print the median budget of the movies
print(budget.median())
一部电影的平均预算是 420 万美元,中位预算是 0!这表明我们数据集中的至少一半电影没有预算!和前面的情况一样,这种异常结果需要进一步检查。在这种情况下,很可能是预算为零表明数据不可用。
第 90 百分位电影的收入是多少?我们可以使用quantile
函数来发现这个值:
#Get the revenue Series object
revenue = small_df['revenue']
#Revenue generated by the 90th percentile movie
revenue.quantile(0.90)
我们得到的结果是 826 万美元。这意味着我们数据集中的 10%的电影收入超过了 826 万美元。
最后,我们来找出每年上映的电影数量。我们可以使用year
系列上的value_counts()
方法来实现:
#Get number of movies released each year
small_df['year'].value_counts()
我们发现 2014 年是电影上映最多的一年。我们的数据集中还有六年(包括 2020 年)只有一部电影记录。
我们的 pandas 库之旅到此为止。如我之前所提到的,pandas 远不止我们在本章中所涵盖的内容。然而,这些内容足以应对我们在构建推荐系统时所遇到的数据整理和分析任务。
你可以通过点击“Untitled”并将笔记本重命名为Chapter2
,然后关闭它。下一章我们将创建一个新的笔记本。
总结
在本章中,我们了解了使用原生 Python 及其内置数据结构的局限性。我们熟悉了 Pandas 库,并学习了它如何通过提供极其强大且易于使用的数据结构来克服上述困难。然后,我们通过分析我们的电影元数据集,探索了两个主要数据结构——Series 和 DataFrame。
在下一章,我们将运用新学到的技能来构建一个 IMDB Top 250 克隆及其变体,一种基于知识的推荐系统。
第三章:使用 Pandas 构建 IMDB 前 250 名克隆
互联网电影数据库(IMDB)维护着一个名为 IMDB Top 250 的榜单,这是根据某种评分标准对前 250 部电影的排名。榜单中的所有电影都是非纪录片、影院上映且时长至少为 45 分钟,并且有超过 25 万条评分:
这个图表可以被认为是最简单的推荐系统。它没有考虑特定用户的口味,也没有尝试推断不同电影之间的相似性。它只是根据预定义的指标为每部电影计算分数,并根据该分数输出排序后的电影列表。
在本章中,我们将涵盖以下内容:
-
构建 IMDB 前 250 名图表的克隆(以下简称为简单推荐系统)。
-
将图表的功能向前推进一步,构建一个基于知识的推荐系统。该模型考虑用户对电影的偏好,如类型、时间段、时长、语言等,并推荐符合所有条件的电影。
技术要求
你需要在系统上安装 Python。最后,为了使用本书的 Git 仓库,用户还需要安装 Git。
本章的代码文件可以在 GitHub 上找到:
github.com/PacktPublishing/Hands-On-Recommendation-Systems-with-Python
。
查看以下视频,看看代码的实际效果:
简单的推荐系统
构建我们简单推荐系统的第一步是设置我们的工作空间。让我们在名为Chapter3
的目录中创建一个新的 Jupyter Notebook,命名为Simple Recommender
,并在浏览器中打开它。
现在让我们加载在上一章中使用的数据集到我们的笔记本中。
如果你还没有下载,数据集可以在以下位置获取:
www.kaggle.com/rounakbanik/the-movies-dataset/downloads/movies_metadata.csv/7
。
import pandas as pd
import numpy as np
#Load the dataset into a pandas dataframe
df = pd.read_csv('../data/movies_')
#Display the first five movies in the dataframe
df.head()
运行该单元格后,你应该会在笔记本中看到一个熟悉的类似表格的结构输出。
构建简单的推荐系统相当简单。步骤如下:
-
选择一个指标(或分数)来为电影评分
-
决定电影出现在图表中的先决条件
-
计算每部符合条件的电影的分数
-
输出按分数递减顺序排列的电影列表
指标
指标是根据其数值来对电影进行排名的量化标准。如果一部电影的指标分数高于另一部电影,那么它就被认为比另一部电影更好。确保我们拥有一个强大且可靠的指标来构建我们的图表,这对确保推荐的高质量至关重要。
指标的选择是任意的。可以使用的最简单的指标之一是电影评分。然而,这样做有许多缺点。首先,电影评分没有考虑到电影的受欢迎程度。因此,一部由 10 万人评分为 9 的电影将排在一部由 100 人评分为 9.5 的电影之后。
这样做并不可取,因为一部仅有 100 人观看并评分的电影,很可能迎合的是一个非常特定的小众群体,可能不像前者那样对普通观众有吸引力。
还有一个众所周知的事实是,随着投票人数的增加,电影的评分会趋于正常化,接近反映电影质量和普及度的值。换句话说,评分很少的电影并不太可靠。五个人打 10 分的电影不一定是好电影。
因此,我们需要的是一个可以在一定程度上兼顾电影评分和它获得的投票数(作为受欢迎度的替代指标)的指标。这样一来,它会更倾向于选择一部由 10 万人评分为 8 的大片,而不是由 100 人评分为 9 的艺术片。
幸运的是,我们不必为这个指标构思数学公式。正如本章标题所示,我们正在构建一个 IMDB 前 250 的克隆。因此,我们将使用 IMDB 的加权评分公式作为我们的指标。从数学角度来看,它可以表示为以下形式:
以下内容适用:
-
v 是电影获得的评分次数
-
m 是电影进入排行榜所需的最低投票数(前提条件)
-
R 是电影的平均评分
-
C 是数据集中所有电影的平均评分
我们已经有了每部电影的 v 和 R 值,分别以 vote_count
和 vote_average
形式呈现。计算 C 非常简单,正如我们在上一章中所见。
前提条件
IMDB 加权公式还包含一个变量 m,它用于计算评分。这个变量的存在是为了确保只有超过某一受欢迎度阈值的电影才能进入排名。因此,m 的值决定了哪些电影有资格进入排行榜,并且通过成为公式的一部分,决定了最终的评分值。
就像这个指标一样,m 的值选择是任意的。换句话说,m 没有正确的值。建议尝试不同的 m 值,然后选择你(和你的受众)认为能给出最佳推荐的值。唯一需要记住的是,m 的值越高,电影的受欢迎程度所占的比重越大,因此选择性也越高。
对于我们的推荐系统,我们将使用第 80 百分位电影获得的投票数作为 m 的值。换句话说,为了使电影出现在排行榜上,它必须获得比数据集中至少 80% 的电影更多的投票。此外,第 80 百分位电影获得的投票数将用于先前描述的加权公式中,来计算评分值。
现在让我们计算 m 的值:
#Calculate the number of votes garnered by the 80th percentile movie
m = df['vote_count'].quantile(0.80)
m
OUTPUT: 50.0
我们可以看到,只有 20% 的电影获得了超过 50 次投票。因此,我们的 m 值是 50
。
我们希望具备的另一个前提条件是运行时间。我们将只考虑时长超过 45 分钟
且小于 300 分钟
的电影。我们将定义一个新的数据框 q_movies
,它将包含所有符合条件的电影,以便出现在图表中:
#Only consider movies longer than 45 minutes and shorter than 300 minutes
q_movies = df[(df['runtime'] >= 45) & (df['runtime'] <= 300)]
#Only consider movies that have garnered more than m votes
q_movies = q_movies[q_movies['vote_count'] >= m]
#Inspect the number of movies that made the cut
q_movies.shape
OUTPUT:
(8963, 24)
我们看到,在 45,000 部电影的数据集中,大约 9,000 部电影(即 20%)达到了标准。
计算评分
在计算评分之前,我们需要发现的最后一个值是 C,即数据集中所有电影的平均评分:
# Calculate C
C = df['vote_average'].mean()
C
OUTPUT:
5.6182072151341851
我们可以看到,电影的平均评分大约是 5.6/10。看起来 IMDB 对其评分的标准非常严格。现在我们已经有了 C 的值,接下来可以计算每部电影的评分。
首先,让我们定义一个函数,根据电影的特征以及 m 和 C 的值来计算评分:
# Function to compute the IMDB weighted rating for each movie
def weighted_rating(x, m=m, C=C):
v = x['vote_count']
R = x['vote_average']
# Compute the weighted score
return (v/(v+m) * R) + (m/(m+v) * C)
接下来,我们将在 q_movies
数据框上使用熟悉的 apply
函数来构建一个新的特征 score。由于计算是对每一行进行的,我们将设置轴 1
来表示按行操作:
# Compute the score using the weighted_rating function defined above
q_movies['score'] = q_movies.apply(weighted_rating, axis=1)
排序与输出
只剩下一步。现在我们需要根据刚才计算的评分对数据框进行排序,并输出前几名电影的列表:
完成了!你刚刚构建了你的第一个推荐系统。恭喜你!
我们可以看到,宝莱坞电影 Dilwale Dulhania Le Jayenge 排名榜单的顶部。我们还可以看到,它的投票数明显少于其他前 25 名电影。这强烈暗示我们应该探索更高的 m 值。这部分留给读者作为练习;尝试不同的 m 值,并观察图表中电影的变化。
基于知识的推荐系统
在本节中,我们将继续在 IMDB Top 250 克隆的基础上构建一个基于知识的推荐系统。这将是一个简单的函数,执行以下任务:
-
询问用户他/她想要的电影类型
-
询问用户电影的时长
-
询问用户推荐电影的时间范围
-
使用收集到的信息,向用户推荐评分较高(根据 IMDB 公式)且符合前述条件的电影:
我们拥有的数据包含时长、类型和时间线的信息,但目前的形式并不直接可用。换句话说,我们的数据需要进行整理,才能用于构建推荐系统。
在我们的Chapter3
文件夹中,让我们创建一个新的 Jupyter Notebook,命名为Knowledge Recommender
*。*这个笔记本将包含我们在这一部分编写的所有代码。
和往常一样,让我们加载所需的包和数据到笔记本中。我们还可以查看我们拥有的特性,并决定哪些对这个任务有用:
import pandas as pd
import numpy as np
df = pd.read_csv('../data/movies_metadata.csv')
#Print all the features (or columns) of the DataFrame
df.columns
OUTPUT:
Index(['adult', 'belongs_to_collection', 'budget', 'genres', 'homepage', 'id',
'imdb_id', 'original_language', 'original_title', 'overview',
'popularity', 'poster_path', 'production_companies',
'production_countries', 'release_date', 'revenue', 'runtime',
'spoken_languages', 'status', 'tagline', 'title', 'video',
'vote_average', 'vote_count'],
dtype='object')
从我们的输出中,可以很清楚地看到我们需要哪些特性,哪些不需要。现在,让我们将数据框缩减到只包含我们模型所需的特性:
#Only keep those features that we require
df = df[['title','genres', 'release_date', 'runtime', 'vote_average', 'vote_count']]
df.head()
接下来,让我们从release_date
特性中提取出发行年份:
#Convert release_date into pandas datetime format
df['release_date'] = pd.to_datetime(df['release_date'], errors='coerce')
#Extract year from the datetime
df['year'] = df['release_date'].apply(lambda x: str(x).split('-')[0] if x != np.nan else np.nan)
我们的year
特性仍然是object
类型,并且充满了NaT
值,这是 Pandas 使用的一种空值类型。让我们将这些值转换为整数0
,并将year
特性的类型转换为int
。
为此,我们将定义一个辅助函数convert_int
,并将其应用到year
特性上:
#Helper function to convert NaT to 0 and all other years to integers.
def convert_int(x):
try:
return int(x)
except:
return 0
#Apply convert_int to the year feature
df['year'] = df['year'].apply(convert_int)
我们不再需要release_date
特性。所以让我们去掉它:
#Drop the release_date column
df = df.drop('release_date', axis=1)
#Display the dataframe
df.head()
runtime
特性已经是可用的格式,不需要做额外的处理。现在让我们把注意力转向genres
*。
类型
初步检查后,我们可以观察到类型的格式看起来像是 JSON 对象(或 Python 字典)。让我们查看一下我们某部电影的genres
对象:
#Print genres of the first movie
df.iloc[0]['genres']
OUTPUT:
"[{'id': 16, 'name': 'Animation'}, {'id': 35, 'name': 'Comedy'}, {'id': 10751, 'name': 'Family'}]"
我们可以观察到输出是一个字符串化的字典。为了使这个功能可用,重要的是我们将这个字符串转换为本地的 Python 字典。幸运的是,Python 提供了一个名为literal_eval
(在ast
库中)的函数,它正是完成这个操作的。literal_eval
解析传递给它的任何字符串,并将其转换为相应的 Python 对象:
#Import the literal_eval function from ast
from ast import literal_eval
#Define a stringified list and output its type
a = "[1,2,3]"
print(type(a))
#Apply literal_eval and output type
b = literal_eval(a)
print(type(b))
OUTPUT:
<class 'str'>
<class 'list'>
现在我们拥有了所有必需的工具来将genres特性转换为 Python 字典格式。
此外,每个字典代表一个类型,并具有两个键:id
和name
*。然而,对于这个练习(以及所有后续的练习),我们只需要name
。*因此,我们将把字典列表转换成字符串列表,其中每个字符串是一个类型名称:
#Convert all NaN into stringified empty lists
df['genres'] = df['genres'].fillna('[]')
#Apply literal_eval to convert to the list object
df['genres'] = df['genres'].apply(literal_eval)
#Convert list of dictionaries to a list of strings
df['genres'] = df['genres'].apply(lambda x: [i['name'] for i in x] if isinstance(x, list) else [])
df.head()
打印数据框的头部应该会显示一个新的genres
特性,它是一个类型名称的列表。然而,我们还没有完成。最后一步是explode
类型列。换句话说,如果某部电影有多个类型,我们将创建多行每行代表该电影的一个类型。
例如,如果有一部电影叫做Just Go With It,其类型为romance和comedy,我们将通过explode
将这部电影拆分成两行。一行将是Just Go With It作为一部romance电影,另一行则是comedy电影:
#Create a new feature by exploding genres
s = df.apply(lambda x: pd.Series(x['genres']),axis=1).stack().reset_index(level=1, drop=True)
#Name the new feature as 'genre'
s.name = 'genre'
#Create a new dataframe gen_df which by dropping the old 'genres' feature and adding the new 'genre'.
gen_df = df.drop('genres', axis=1).join(s)
#Print the head of the new gen_df
gen_df.head()
现在你应该能够看到三行Toy Story数据;每一行代表一个类别:动画、家庭和喜剧。这个gen_df
数据框就是我们将用来构建基于知识的推荐系统的数据。
build_chart
函数
我们终于可以编写作为推荐系统的函数了。我们不能使用之前计算的m和C值,因为我们并不是考虑所有的电影,而仅仅是符合条件的电影。换句话说,这有三个主要步骤:
-
获取用户关于他们偏好的输入
-
提取所有符合用户设定条件的电影
-
仅针对这些电影计算m和C的值,然后按照上一节中的方式构建图表
因此,build_chart
函数将仅接受两个输入:我们的gen_df
数据框和用于计算m值的百分位数。默认情况下,我们将其设置为 80%,即0.8
:
def build_chart(gen_df, percentile=0.8):
#Ask for preferred genres
print("Input preferred genre")
genre = input()
#Ask for lower limit of duration
print("Input shortest duration")
low_time = int(input())
#Ask for upper limit of duration
print("Input longest duration")
high_time = int(input())
#Ask for lower limit of timeline
print("Input earliest year")
low_year = int(input())
#Ask for upper limit of timeline
print("Input latest year")
high_year = int(input())
#Define a new movies variable to store the preferred movies. Copy the contents of gen_df to movies
movies = gen_df.copy()
#Filter based on the condition
movies = movies[(movies['genre'] == genre) &
(movies['runtime'] >= low_time) &
(movies['runtime'] <= high_time) &
(movies['year'] >= low_year) &
(movies['year'] <= high_year)]
#Compute the values of C and m for the filtered movies
C = movies['vote_average'].mean()
m = movies['vote_count'].quantile(percentile)
#Only consider movies that have higher than m votes. Save this in a new dataframe q_movies
q_movies = movies.copy().loc[movies['vote_count'] >= m]
#Calculate score using the IMDB formula
q_movies['score'] = q_movies.apply(lambda x: (x['vote_count']/(x['vote_count']+m) * x['vote_average'])
+ (m/(m+x['vote_count']) * C)
,axis=1)
#Sort movies in descending order of their scores
q_movies = q_movies.sort_values('score', ascending=False)
return q_movies
是时候让我们的模型投入实际使用了!
我们希望推荐的动画电影时长在 30 分钟到 2 小时之间,并且上映时间介于 1990 年到 2005 年之间。让我们来看看结果:
我们可以看到,输出的电影满足我们作为输入传递的所有条件。由于我们应用了 IMDB 的评分标准,我们还可以观察到我们的电影在评分上非常高,而且同时也很受欢迎。前五名还包括狮子王,这是我最喜欢的动画电影!就我个人而言,我会非常满意这个列表的结果。
总结
在这一章中,我们构建了一个简单的推荐系统,它是 IMDB Top 250 排行榜的克隆。然后我们继续构建了一个改进的基于知识的推荐系统,该系统要求用户提供他们偏好的类型、时长和上映时间。在构建这些模型的过程中,我们还学习了如何使用 Pandas 库进行一些高级的数据整理。
在下一章中,我们将使用更高级的特性和技术来构建一个基于内容的推荐系统。该模型将能够根据电影的情节来检测相似的电影,并通过识别类型、演员、导演、情节等方面的相似性来推荐电影。
第四章:构建基于内容的推荐系统
在上一章中,我们建立了一个 IMDB Top 250 克隆(一个简单推荐系统)和一个基于知识的推荐系统,根据时间线、类型和时长建议电影。然而,这些系统都非常原始。简单推荐系统没有考虑个别用户的偏好。基于知识的推荐系统考虑了用户对类型、时间线和时长的偏好,但模型及其推荐仍然非常通用。
假设 Alice 喜欢的电影有 《黑暗骑士》、《钢铁侠》 和 《超人:钢铁之躯》。很显然,Alice 喜欢超级英雄电影。然而,我们在上一章建立的模型无法捕捉到这一细节。它们能做的最好的事情就是建议 动作 电影(通过让 Alice 输入 动作 作为首选类型),而动作电影是超级英雄电影的超集。
也有可能两部电影拥有相同的类型、时间线和时长特征,但在受众上有很大的不同。例如,考虑 《宿醉》 和 《遗忘萨拉·马歇尔》。这两部电影都是 21 世纪第一个十年上映的,都持续了大约两个小时,都是喜剧片。然而,喜欢这两部电影的观众类型却截然不同。
解决这个问题的一个显而易见的办法是要求用户提供更多的元数据作为输入。例如,如果我们增加 子类型 输入,用户就能输入如 超级英雄、黑色幽默 和 浪漫喜剧 等值,从而获得更合适的推荐结果,但这种解决方案在可用性方面存在很大问题。
第一个问题是我们没有 子类型 数据。其次,即使有数据,用户极不可能了解他们喜欢的电影的元数据。最后,即使他们知道这些,他们也肯定不会有耐心将这些信息输入到一个长表单中。相反,他们更愿意做的是告诉你他们喜欢/不喜欢的电影,并期待得到与他们口味相符的推荐。
正如我们在第一章中讨论的那样,这正是 Netflix 等网站所做的。当你第一次注册 Netflix 时,它没有任何关于你口味的信息来建立个人档案,利用社区的力量为你推荐电影(这是我们在后续章节中会探讨的概念)。相反,它会要求你提供一些你喜欢的电影,并显示与你喜欢的电影最相似的结果。
在本章中,我们将构建两种类型的基于内容的推荐系统:
-
基于情节描述的推荐系统: 该模型比较不同电影的描述和标语,提供情节描述最相似的推荐结果。
-
**基于元数据的推荐系统:**该模型考虑了大量特征,例如类型、关键词、演员和制作人员,并提供最相似的推荐,基于上述特征。
技术要求
您需要在系统上安装 Python。最后,要使用本书的 Git 仓库,用户需要安装 Git。
本章的代码文件可以在 GitHub 上找到:
github.com/PacktPublishing/Hands-On-Recommendation-Systems-with-Python
.
查看以下视频,看看代码如何执行:
导出清理后的 DataFrame
在上一章中,我们对元数据进行了系列数据整理和清理,以将其转换为更易用的形式。为了避免再次执行这些步骤,我们将清理后的 DataFrame 保存为 CSV 文件。像往常一样,使用 pandas 来做这一切非常简单。
在第四章的知识推荐器 notebook 中,请输入以下代码:
#Convert the cleaned (non-exploded) dataframe df into a CSV file and save it in the data folder
#Set parameter index to False as the index of the DataFrame has no inherent meaning.
df.to_csv('../data/metadata_clean.csv', index=False)
您的data
文件夹现在应该包含一个新文件,metadata_clean.csv
。
让我们创建一个新的文件夹,Chapter 4
*,*并在该文件夹内打开一个新的 Jupyter Notebook。现在让我们将新文件导入这个 Notebook:
import pandas as pd
import numpy as np
#Import data from the clean file
df = pd.read_csv('../data/metadata_clean.csv')
#Print the head of the cleaned DataFrame
df.head()
该单元应输出一个已经清理并符合所需格式的 DataFrame。
文档向量
本质上,我们正在构建的模型计算文本之间的成对相似度。那么,我们如何用数字量化两篇文本之间的相似度呢?
换句话说,考虑三部电影:A、B 和 C。我们如何在数学上证明 A 的情节比 B 更像 C 的情节(或反之)?
解答这些问题的第一步是将文本体(以下简称为文档)表示为数学量。这是通过将这些文档表示为向量来实现的*。换句话说,每个文档都被描绘为一系列n*个数字,其中每个数字代表一个维度,n是所有文档词汇的总大小。
但是这些向量的值是多少?这个问题的答案取决于我们使用的向量化工具,即将文档转换为向量的工具。最受欢迎的两种向量化工具是 CountVectorizer 和 TF-IDFVectorizer*。
CountVectorizer
CountVectorizer 是最简单的向量化工具,最好的解释方法是通过一个示例。假设我们有三篇文档,A、B 和 C,如下所示:
-
A:太阳是一颗恒星。
-
B:我的爱像一朵红红的玫瑰
-
C:玛丽有只小羊
我们现在需要使用 CountVectorizer 将这些文档转换为向量形式。第一步是计算词汇表的大小。词汇表是指所有文档中出现的唯一词汇数量。因此,这三份文档的词汇表如下:the, sun, is, a, star, my, love, like, red, rose, mary, had, little, lamb。结果,词汇表的大小是 14。
通常的做法是不会将极为常见的单词(如 a, the, is, had, my 等)包含在词汇表中,这些单词也被称为停用词。因此,在去除停用词后,我们的词汇表 V 如下:
V:like, little, lamb, love, mary, red, rose, sun, star
现在我们的词汇表大小是九。因此,我们的文档将被表示为九维向量,每个维度代表某个特定单词在文档中出现的次数。换句话说,第一个维度表示 “like” 出现的次数,第二个维度表示 “little” 出现的次数,以此类推。
因此,使用 CountVectorizer 方法,A、B 和 C 将现在表示如下:
-
A:(0, 0, 0, 0, 0, 0, 0, 1, 1)
-
B:(1, 0, 0, 1, 0, 2, 1, 0, 0)
-
C:(0, 1, 1, 0, 1, 0, 0, 0, 0)
TF-IDF Vectorizer
不是文档中的所有词汇都具有相同的权重。当我们完全去除停用词时,我们已经观察到了这一点。但词汇中存在的词都赋予了相等的权重。
但这总是正确的吗?
例如,考虑一个关于狗的文档集合。现在,很明显,这些文档中将频繁出现 “dog” 这个词。因此,“dog” 的出现并不像其他只在少数文档中出现的词那样重要。
TF-IDF Vectorizer(词频-逆文档频率)考虑到了上述因素,并根据以下公式为每个单词分配权重。对于文档 j 中的每个单词 i,适用以下公式:
在这个公式中,以下是成立的:
-
w[i, j] 是词汇 i 在文档 j 中的权重
-
df[i] 是包含词汇 i 的文档数量
-
N 是文档的总数
我们不会深入探讨公式及相关计算。只需记住,文档中一个词的权重越大,如果它在该文档中出现的频率越高,并且出现在较少的文档中。权重 w[i,j]的取值范围是0
到1
:
我们将使用 TF-IDF Vectorizer,因为某些词(如前面词云中的词汇)在描述图表时的出现频率远高于其他词。因此,根据 TF-IDF 公式为文档中的每个单词分配权重是一个好主意。
使用 TF-IDF 的另一个原因是它加速了计算文档对之间的余弦相似度得分。当我们在代码中实现时,将会更详细地讨论这一点。
余弦相似度得分
我们将在第五章《数据挖掘技术入门》中详细讨论相似度得分。现在,我们将使用余弦相似度度量来构建我们的模型。余弦得分非常稳健且容易计算(尤其是当与 TF-IDF 向量化器一起使用时)。
两个文档x和y之间的余弦相似度得分计算公式如下:
余弦得分可以取从-1 到 1 之间的任何值。余弦得分越高,文档之间的相似度就越大。现在,我们有了一个良好的理论基础,可以开始使用 Python 构建基于内容的推荐系统了。
基于剧情描述的推荐系统
我们的基于剧情描述的推荐系统将以电影标题作为输入,并输出一份基于电影剧情最相似的电影列表。以下是我们将在构建该模型时执行的步骤:
-
获取构建模型所需的数据
-
为每部电影的剧情描述(或概述)创建 TF-IDF 向量
-
计算每对电影的余弦相似度得分
-
编写推荐函数,该函数接受电影标题作为输入,并基于剧情输出与其最相似的电影
准备数据
目前,虽然数据框(DataFrame)已经清理完毕,但它并不包含构建基于剧情描述的推荐系统所需的特征。幸运的是,这些必要的特征可以在原始元数据文件中找到。
我们需要做的就是导入它们并将其添加到我们的数据框中:
#Import the original file
orig_df = pd.read_csv('../data/movies_metadata.csv', low_memory=False)
#Add the useful features into the cleaned dataframe
df['overview'], df['id'] = orig_df['overview'], orig_df['id']
df.head()
现在数据框应该包含两个新特征:overview
和id
。我们将在构建这个模型时使用overview
,而使用id
来构建下一个模型。
overview
特征由字符串组成,理想情况下,我们应该通过去除所有标点符号并将所有单词转换为小写来清理它们。然而,正如我们接下来会看到的,scikit-learn
库会自动为我们完成这一切工作,这个库将在本章中大规模应用于构建模型。
创建 TF-IDF 矩阵
下一步是创建一个数据框,其中每一行表示我们主数据框中对应电影的overview
特征的 TF-IDF 向量。为此,我们将使用scikit-learn
库,它为我们提供了一个 TfidfVectorizer 对象,能够轻松地完成这一过程:
#Import TfIdfVectorizer from the scikit-learn library
from sklearn.feature_extraction.text import TfidfVectorizer
#Define a TF-IDF Vectorizer Object. Remove all english stopwords
tfidf = TfidfVectorizer(stop_words='english')
#Replace NaN with an empty string
df['overview'] = df['overview'].fillna('')
#Construct the required TF-IDF matrix by applying the fit_transform method on the overview feature
tfidf_matrix = tfidf.fit_transform(df['overview'])
#Output the shape of tfidf_matrix
tfidf_matrix.shape
OUTPUT:
(45466, 75827)
我们看到,向量化器为每部电影的概述创建了一个 75,827 维的向量。
计算余弦相似度得分
下一步是计算每部电影的成对余弦相似度分数。换句话说,我们将创建一个 45,466 × 45,466 的矩阵,其中第i(th)*行和第*j(th)列的单元格表示电影i和j之间的相似度分数。我们可以很容易地看到,这个矩阵具有对称性,且对角线上的每个元素都是 1,因为它表示电影与自身的相似度分数。
和 TF-IDFVectorizer 一样,scikit-learn
也有计算上述相似度矩阵的功能。然而,计算余弦相似度是一个计算上昂贵的过程。幸运的是,由于我们的电影情节是以 TF-IDF 向量表示的,因此它们的幅度始终为 1。因此,我们无需计算余弦相似度公式中的分母,因为它始终是 1。我们的工作现在简化为计算一个更简单、更计算廉价的点积(这也是scikit-learn
提供的功能):
# Import linear_kernel to compute the dot product
from sklearn.metrics.pairwise import linear_kernel
# Compute the cosine similarity matrix
cosine_sim = linear_kernel(tfidf_matrix, tfidf_matrix)
尽管我们在计算更便宜的点积,整个过程仍然需要几分钟才能完成。通过计算每部电影与其他电影的相似度分数,我们现在处于一个非常有利的位置,可以编写我们的最终推荐函数。
构建推荐函数
最后一步是创建我们的推荐函数。但是,在此之前,让我们先创建一个电影标题及其对应索引的反向映射。换句话说,让我们创建一个 pandas 系列,将电影标题作为索引,电影在主 DataFrame 中的索引作为值:
#Construct a reverse mapping of indices and movie titles, and drop duplicate titles, if any
indices = pd.Series(df.index, index=df['title']).drop_duplicates()
我们将在构建推荐函数时执行以下步骤:
-
将电影标题声明为一个参数。
-
从
indices
反向映射中获取电影的索引。 -
使用
cosine_sim
获取该电影与所有其他电影的余弦相似度分数列表。将其转换为一个元组列表,其中第一个元素是位置,第二个元素是相似度分数。 -
根据余弦相似度分数对这个元组列表进行排序。
-
获取这个列表的前 10 个元素。忽略第一个元素,因为它表示与自身的相似度分数(与某部电影最相似的电影显然就是它自己)。
-
返回与前 10 个元素索引对应的标题,排除第一个:
# Function that takes in movie title as input and gives recommendations
def content_recommender(title, cosine_sim=cosine_sim, df=df, indices=indices):
# Obtain the index of the movie that matches the title
idx = indices[title]
# Get the pairwsie similarity scores of all movies with that movie
# And convert it into a list of tuples as described above
sim_scores = list(enumerate(cosine_sim[idx]))
# Sort the movies based on the cosine similarity scores
sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True)
# Get the scores of the 10 most similar movies. Ignore the first movie.
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 df['title'].iloc[movie_indices]
恭喜!你已经构建了你的第一个基于内容的推荐系统。现在是时候让我们的推荐系统实际运作了!让我们请求它推荐类似于《狮子王》
的电影:*
#Get recommendations for The Lion King
content_recommender('The Lion King')
我们看到我们的推荐系统在其前 10 名列表中建议了所有*《狮子王》*的续集。我们还注意到,列表中的大多数电影都与狮子有关。
不言而喻,喜欢*《狮子王》*的人很可能对迪士尼电影情有独钟。他们也许还更倾向于观看动画片。不幸的是,我们的情节描述推荐器无法捕捉到所有这些信息。
因此,在下一部分中,我们将构建一个使用更高级元数据的推荐系统,如类型、演员、工作人员和关键词(或子类型)。这个推荐系统将能够更好地识别个人对特定导演、演员、子类型等的喜好。
基于元数据的推荐系统
我们将大致按照基于剧情描述的推荐系统的步骤来构建我们的基于元数据的模型。当然,主要的区别在于我们用来构建模型的数据类型。
数据准备
为了构建这个模型,我们将使用以下元数据:
-
电影的类型。
-
电影的导演。此人是工作人员的一部分。
-
电影的三大主演。他们是演员阵容的一部分。
-
子类型或 关键词。
除了类型之外,我们的 DataFrame(无论是原始的还是清洗过的)并未包含我们所需要的数据。因此,在这次练习中,我们需要下载两个附加文件:credits.csv
*,其中包含电影演员和工作人员的信息,以及 keywords.csv
,*其中包含子类型的信息。
你可以从以下网址下载所需的文件:www.kaggle.com/rounakbanik/the-movies-dataset/data
。
将两个文件放入你的 data
文件夹中。在将数据转换为可用格式之前,我们需要进行大量的清洗工作。让我们开始吧!
关键词和演员阵容数据集
让我们开始加载新数据到现有的 Jupyter Notebook 中:
# Load the keywords and credits files
cred_df = pd.read_csv('../data/credits.csv')
key_df = pd.read_csv('../data/keywords.csv')
#Print the head of the credit dataframe
cred_df.head()
#Print the head of the keywords dataframe
key_df.head()
我们可以看到演员阵容、工作人员和关键词都采用了我们熟悉的字典列表
格式。就像 genres
*,*我们必须将它们简化为字符串或字符串列表。
然而,在此之前,我们将联合三个 DataFrame,以便将所有特征合并到一个 DataFrame 中。连接 pandas DataFrame 与在 SQL 中连接表格是相同的。我们将用于连接 DataFrame 的键是 id
特征。然而,为了使用这个,我们首先需要明确地将其转换为 ID 格式。这显然是错误的数据。因此,我们应该查找
转换为整数。我们已经知道如何做到这一点:
#Convert the IDs of df into int
df['id'] = df['id'].astype('int')
运行上面的代码会导致ValueError
。仔细检查后,我们发现1997-08-20 被列为 ID。这显然是错误的数据。因此,我们应该找到所有 ID 错误的行并删除它们,以确保代码执行成功:
# Function to convert all non-integer IDs to NaN
def clean_ids(x):
try:
return int(x)
except:
return np.nan
#Clean the ids of df
df['id'] = df['id'].apply(clean_ids)
#Filter all rows that have a null ID
df = df[df['id'].notnull()]
现在我们已经准备好将所有三个 DataFrame 的 ID 转换为整数,并将它们合并成一个 DataFrame:
# Convert IDs into integer
df['id'] = df['id'].astype('int')
key_df['id'] = key_df['id'].astype('int')
cred_df['id'] = cred_df['id'].astype('int')
# Merge keywords and credits into your main metadata dataframe
df = df.merge(cred_df, on='id')
df = df.merge(key_df, on='id')
#Display the head of the merged df
df.head()
处理关键词、演员和工作人员
现在我们已经将所有需要的特征合并到一个 DataFrame 中,让我们将它们转换为更易于使用的格式。更具体地说,我们将进行以下几项转换:
-
将
keywords
转换为字符串列表,其中每个字符串是一个关键词(类似于类型)。我们只会包括前三个关键词。因此,这个列表最多可以包含三个元素。 -
将
cast
转换为字符串列表,其中每个字符串都是一位明星。像keywords
一样,我们只会包括我们演员阵容中的前三位明星。 -
将
crew
转换为director
。换句话说,我们只提取电影的导演,忽略其他所有工作人员。
第一步是将这些字符串化的对象转换为原生的 Python 对象:
# Convert the stringified objects into the native python objects
from ast import literal_eval
features = ['cast', 'crew', 'keywords', 'genres']
for feature in features:
df[feature] = df[feature].apply(literal_eval)
接下来,我们从crew
列表中提取导演。为此,我们首先检查crew
列表中字典的结构:
#Print the first cast member of the first movie in df
df.iloc[0]['crew'][0]
OUTPUT:
{'credit_id': '52fe4284c3a36847f8024f49',
'department': 'Directing',
'gender': 2,
'id': 7879,
'job': 'Director',
'name': 'John Lasseter',
'profile_path': '/7EdqiNbr4FRjIhKHyPPdFfEEEFG.jpg'}
我们看到这个字典由job
和name
键组成。由于我们只对导演感兴趣,我们将循环遍历特定列表中的所有工作人员,并在job
为Director
时提取name
。让我们编写一个函数来实现这一点:
# Extract the director's name. If director is not listed, return NaN
def get_director(x):
for crew_member in x:
if crew_member['job'] == 'Director':
return crew_member['name']
return np.nan
既然我们已经有了get_director
函数,我们可以定义新的director
特性:
#Define the new director feature
df['director'] = df['crew'].apply(get_director)
#Print the directors of the first five movies
df['director'].head()
OUTPUT:
0 John Lasseter
1 Joe Johnston
2 Howard Deutch
3 Forest Whitaker
4 Charles Shyer
Name: director, dtype: object
keywords
和cast
都是字典列表。在这两种情况下,我们需要提取每个列表中的前三个name
属性。因此,我们可以编写一个单一的函数来处理这两个特性。另外,和keywords
以及cast
一样,我们只会考虑每部电影的前三个类型:
# Returns the list top 3 elements or entire list; whichever is more.
def generate_list(x):
if isinstance(x, list):
names = [ele['name'] for ele 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 []
我们将使用这个函数来处理我们的cast
和keywords
特性。我们也只会考虑前三个列出的genres
:
#Apply the generate_list function to cast and keywords
df['cast'] = df['cast'].apply(generate_list)
df['keywords'] = df['keywords'].apply(generate_list)
#Only consider a maximum of 3 genres
df['genres'] = df['genres'].apply(lambda x: x[:3])
现在,让我们来看看我们处理后的数据样本:
# Print the new features of the first 5 movies along with title
df[['title', 'cast', 'director', 'keywords', 'genres']].head(3)
在后续步骤中,我们将使用向量化器来构建文档向量。如果两个演员有相同的名字(比如 Ryan Reynolds 和 Ryan Gosling),向量化器会将这两个 Ryan 视为相同的人,尽管它们显然是不同的个体。这会影响我们获得的推荐质量。如果一个人喜欢 Ryan Reynolds 的电影,并不意味着他也喜欢所有 Ryan 的电影。
因此,最后一步是去除关键词、演员和导演姓名之间的空格,并将它们全部转换为小写字母。因此,前面例子中的两个 Ryan 将变成ryangosling和ryanreynolds,我们的向量化器现在能够区分它们:
# Function to sanitize data to prevent ambiguity.
# Removes spaces and converts to lowercase
def sanitize(x):
if isinstance(x, list):
#Strip spaces and convert to lowercase
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 the generate_list function to cast, keywords, director and genres
for feature in ['cast', 'director', 'genres', 'keywords']:
df[feature] = df[feature].apply(sanitize)
创建元数据 soup
在基于剧情描述的推荐系统中,我们只处理了一个overview特性,这是一个文本体。因此,我们能够直接应用我们的向量化器。
然而,在基于元数据的推荐系统中情况并非如此。我们有四个特性需要处理,其中三个是列表,一个是字符串。我们需要做的是创建一个包含演员、导演、关键词和类型的soup
。这样,我们就可以将这个 soup 输入到我们的向量化器中,并执行类似之前的后续步骤:
#Function that creates a soup out of the desired metadata
def create_soup(x):
return ' '.join(x['keywords']) + ' ' + ' '.join(x['cast']) + ' ' + x['director'] + ' ' + ' '.join(x['genres'])
有了这个功能,我们创建了soup
特性:
# Create the new soup feature
df['soup'] = df.apply(create_soup, axis=1)
现在,让我们看一下其中一个 soup
值。它应该是一个包含表示电影类型、演员和关键词的字符串:
#Display the soup of the first movie
df.iloc[0]['soup']
OUTPUT:
'jealousy toy boy tomhanks timallen donrickles johnlasseter animation comedy family'
创建好 soup
后,我们现在处于一个良好的位置,可以创建文档向量、计算相似度得分,并构建基于元数据的推荐函数。
生成推荐
接下来的步骤几乎与前一部分的相应步骤相同。
我们将使用 CountVectorizer,而不是 TF-IDFVectorizer。这是因为使用 TF-IDFVectorizer 会对在较多电影中担任演员和导演的人员赋予较低的权重。
这是不理想的,因为我们不希望因为艺术家参与或执导更多电影而给予惩罚:
#Define a new CountVectorizer object and create vectors for the soup
count = CountVectorizer(stop_words='english')
count_matrix = count.fit_transform(df['soup'])
不幸的是,使用 CountVectorizer 意味着我们必须使用计算开销更大的 cosine_similarity
函数来计算相似度得分:
#Import cosine_similarity function
from sklearn.metrics.pairwise import cosine_similarity
#Compute the cosine similarity score (equivalent to dot product for tf-idf vectors)
cosine_sim2 = cosine_similarity(count_matrix, count_matrix)
由于我们剔除了几个索引不良的电影,我们需要重新构建反向映射。让我们在下一步中完成这项工作:
# Reset index of your df and construct reverse mapping again
df = df.reset_index()
indices2 = pd.Series(df.index, index=df['title'])
在构建好新的反向映射并计算出相似度得分后,我们可以通过传入 cosine_sim2
作为参数,重用上一部分定义的 content_recommender
函数。现在,让我们通过请求同一部电影《狮子王》的推荐来测试我们的新模型:
content_recommender('The Lion King', cosine_sim2, df, indices2)
该案例给出的推荐与我们基于剧情描述的推荐系统提供的推荐截然不同。我们看到,它能够捕捉到比“狮子”更多的信息。列表中的大多数电影都是动画片,且包含拟人化角色。
就个人而言,我觉得 《宝可梦:阿尔宙斯与生命之珠》 的推荐尤其有趣。这部电影与 《狮子王》 都 featuring 动画拟人化角色,这些角色几年后回来报复那些曾经伤害过他们的人。
改进建议
本章构建的基于内容的推荐系统,当然,远远不及行业中使用的强大模型。仍有许多提升空间。在这一部分,我将提出一些关于如何升级您已经构建的推荐系统的建议:
-
尝试不同数量的关键词、类型和演员:在我们构建的模型中,我们最多考虑了三个关键词、类型和演员。这是一个随意的决定。实验不同数量的这些特征,看看是否能有效地为元数据“汤”贡献更多信息。
-
提出更多明确的子类型:我们的模型只考虑了关键词列表中出现的前三个关键词。然而,这样做并没有充分的理由。事实上,某些关键词可能只出现在一部电影中(从而使它们变得无用)。一个更有效的技术是,像定义电影类型一样,定义一个明确的子类型数量,并只将这些子类型分配给电影。
-
给予导演更多的权重:我们的模型给导演和演员同等的权重。然而,你可以认为,电影的特色更多是由导演决定的。我们可以通过在推荐模型中多次提到导演,而不仅仅是一次,来给导演更多的关注。可以尝试调整导演在推荐中的出现次数。
-
考虑其他工作人员成员:导演并不是唯一赋予电影其特色的人物。你也可以考虑加入其他工作人员成员,比如制作人和编剧,来丰富你的推荐模型。
-
尝试其他元数据:在构建我们的元数据模型时,我们只考虑了类型、关键词和演员名单。然而,我们的数据集中还有很多其他特征,比如制作公司、国家和语言。你也可以考虑这些数据点,因为它们可能能够捕捉到重要的信息(比如两部电影是否由皮克斯制作)。
-
引入流行度过滤器:有可能两部电影有相同的类型和子类型,但在质量和受欢迎程度上却相差甚远。在这种情况下,你可能希望引入一个流行度过滤器,考虑n最相似的电影,计算加权评分,并展示前五个结果。你已经在上一章学习了如何做到这一点。
总结
在这一章中,我们已经走了很长一段路。我们首先学习了文档向量,并简要介绍了余弦相似度评分。接下来,我们构建了一个推荐系统,能够识别具有相似剧情描述的电影。然后,我们构建了一个更高级的模型,利用了其他元数据的力量,比如类型、关键词和演员名单。最后,我们讨论了几种可以改进现有系统的方法。
至此,我们正式结束了基于内容的推荐系统的学习。在接下来的章节中,我们将讨论目前业界最流行的推荐模型之一:协同过滤。
第五章:入门数据挖掘技术
2003 年,亚马逊的 Linden、Smith 和 York 发布了一篇名为《项目与项目协同过滤》的论文,解释了亚马逊如何进行产品推荐。从那时起,这类算法已经主导了推荐行业的标准。每一个拥有大量用户的的网站或应用,无论是 Netflix、Amazon 还是 Facebook,都使用某种形式的协同过滤来推荐项目(这些项目可能是电影、商品或朋友):
正如第一章所描述的,协同过滤试图利用社区的力量来提供可靠、相关的,甚至有时是令人惊讶的推荐。如果 Alice 和 Bob 大多数情况下喜欢相同的电影(比如《狮子王》、《阿拉丁》和《玩具总动员》),而且 Alice 还喜欢《海底总动员》,那么 Bob 很可能也会喜欢这部电影,尽管他还没有看过。
我们将在下一章构建强大的协同过滤器。然而,在此之前,了解构建协同过滤器所涉及的基础技术、原理和算法是非常重要的。
因此,在本章中,我们将涵盖以下主题:
-
相似度度量:给定两个项目,我们如何数学地量化它们之间的差异或相似性?相似度度量帮助我们解答这个问题。
在构建内容推荐引擎时,我们已经使用了相似度度量(余弦相似度)。在本章中,我们将介绍一些其他流行的相似度度量。
-
降维:在构建协同过滤时,我们通常会处理数百万用户对数百万项目的评分。在这种情况下,我们的用户和项目向量的维度往往达到数百万。为了提高性能,加速计算并避免维度灾难,通常建议大幅减少维度的数量,同时保留大部分信息。本章这一部分将描述一些能够实现这一目标的技术。
-
监督学习:监督学习是一类利用标注数据推断映射函数的机器学习算法,这个映射函数可以用来预测未标注数据的标签(或类别)。我们将学习一些最流行的监督学习算法,如支持向量机、逻辑回归、决策树和集成方法。
-
聚类:聚类是一种无监督学习方法,算法尝试将所有数据点划分为若干个簇。因此,在没有标签数据集的情况下,聚类算法能够为所有未标注的数据点分配类别。在本节中,我们将学习 k-means 聚类算法,这是一种简单但功能强大的算法,广泛应用于协同过滤中。
-
评估方法和指标:我们将介绍几种用于评估这些算法性能的评估指标,包括准确率、精确度和召回率。
本章所涉及的主题足以成为一本完整的教材。由于这是一个动手的推荐引擎教程,我们不会过多深入探讨大多数算法的工作原理,也不会从零开始编写它们。我们要做的是理解它们的工作方式及适用时机,它们的优缺点,以及如何利用 scikit-learn 库轻松实现它们。
问题陈述
协同过滤算法试图解决预测问题(如第一章《推荐系统入门》中所述)。换句话说,我们给定了一个 i 个用户和 j 个项目的矩阵。矩阵中第 i 行第 j 列的值(记作 rij)表示用户 i 对项目 j 给出的评分:
i 个用户和 j 个项目的矩阵
我们的任务是完成这个矩阵。换句话说,我们需要预测矩阵中没有数据的所有单元格。例如,在前面的图示中,我们需要预测用户 E 是否会喜欢音乐播放器项。为完成这一任务,一些评分是已知的(例如用户 A 喜欢音乐播放器和视频游戏),而另一些则未知(例如我们不知道用户 C 和 D 是否喜欢视频游戏)。
相似度度量
从上一节中的评分矩阵来看,每个用户可以用一个 j 维的向量表示,其中第 k 维表示该用户对第 k 项的评分。例如,设 1 表示喜欢,-1 表示不喜欢,0 表示没有评分。因此,用户 B 可以表示为 (0, 1, -1, -1)。类似地,每个项目也可以用一个 i 维的向量表示,其中第 k 维表示第 k 用户对该项目的评分。因此,视频游戏项目可以表示为 (1, -1, 0, 0, -1)。
我们在构建基于内容的推荐引擎时,已经计算了相似度得分,针对相同维度的向量。在本节中,我们将讨论其他相似度度量,并重新审视在其他得分背景下的余弦相似度得分。
欧几里得距离
欧几里得距离可以定义为连接在 n 维笛卡尔平面上绘制的两个数据点的线段长度。例如,考虑在二维平面上绘制的两个点:
欧几里得距离
两个点之间的距离 d 给出了欧几里得距离,其在二维空间中的公式如前图所示。
更一般地,考虑两个 n 维的点(或向量):
-
v1: (q1, q2,…, qn)
-
v2: (r1, r2,…, rn)
然后,欧几里得得分在数学上定义为:
欧几里得分数可以取从 0 到无穷大的任何值。欧几里得分数(或距离)越低,两个向量就越相似。现在,我们来定义一个简单的函数,使用 NumPy 来计算两个 n 维向量之间的欧几里得距离,公式如下:
#Function to compute Euclidean Distance.
def euclidean(v1, v2):
#Convert 1-D Python lists to numpy vectors
v1 = np.array(v1)
v2 = np.array(v2)
#Compute vector which is the element wise square of the difference
diff = np.power(np.array(v1)- np.array(v2), 2)
#Perform summation of the elements of the above vector
sigma_val = np.sum(diff)
#Compute square root and return final Euclidean score
euclid_score = np.sqrt(sigma_val)
return euclid_score
接下来,我们定义三个对五部不同电影进行了评分的用户:
#Define 3 users with ratings for 5 movies
u1 = [5,1,2,4,5]
u2 = [1,5,4,2,1]
u3 = [5,2,2,4,4]
从评分来看,我们可以看到用户 1 和 2 的口味极为不同,而用户 1 和 3 的口味大致相似。我们来看看欧几里得距离是否能够捕捉到这一点:
euclidean(u1, u2)
OUTPUT:
7.4833147735478827
用户 1 和 2 之间的欧几里得距离约为 7.48:
euclidean(u1, u3)
OUTPUT:
1.4142135623730951
用户 1 和 3 之间的欧几里得距离明显小于用户 1 和 2 之间的距离。因此,在这种情况下,欧几里得距离能够令人满意地捕捉到我们用户之间的关系。
皮尔逊相关系数
考虑两位用户 Alice 和 Bob,他们对同五部电影进行了评分。Alice 对评分非常苛刻,任何电影的评分都不超过 4。另一方面,Bob 比较宽松,评分从不低于 2。我们来定义代表 Alice 和 Bob 的矩阵,并计算他们的欧几里得距离:
alice = [1,1,3,2,4]
bob = [2,2,4,3,5]
euclidean(alice, bob)
OUTPUT:
2.2360679774997898
我们得到了约 2.23 的欧几里得距离。然而,经过更仔细的检查,我们发现 Bob 总是给出比 Alice 高 1 分的评分。因此,我们可以说 Alice 和 Bob 的评分是极其相关的。换句话说,如果我们知道 Alice 对一部电影的评分,我们可以通过简单地加 1 来高精度地计算 Bob 对同一部电影的评分。
考虑另一个用户 Eve,她的口味与 Alice 完全相反:
eve = [5,5,3,4,2]
euclidean(eve, alice)
OUTPUT:
6.324555320336759
我们得到了一个非常高的得分 6.32,这表明这两个人的差异很大。如果我们使用欧几里得距离,我们无法做出更多的分析。然而,经过检查,我们发现 Alice 和 Eve 对一部电影的评分总是加起来等于 6。因此,尽管两个人非常不同,但一个人的评分可以用来准确预测另一个人的对应评分。从数学角度来说,我们说 Alice 和 Eve 的评分是强烈的负相关。
欧几里得距离强调的是大小,而在这个过程中,它无法很好地衡量相似性或差异性。这就是皮尔逊相关系数发挥作用的地方。皮尔逊相关系数是一个介于 -1 和 1 之间的分数,其中 -1 表示完全负相关(如 Alice 和 Eve 的情况),1 表示完全正相关(如 Alice 和 Bob 的情况),而 0 表示两者之间没有任何相关性(或彼此独立)。
数学上,皮尔逊相关系数的定义如下:
这里,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-rec-sys-py/img/cf6be388-6b92-4140-9b16-06c11f0d25d7.png 表示向量 i 中所有元素的平均值。
SciPy 包提供了一个计算皮尔逊相似度分数的函数:
from scipy.stats import pearsonr
pearsonr(alice, bob)
OUTPUT:
(1.0, 0.0)
pearsonr(alice, eve)
OUTPUT:
(-1.0, 0.0)
我们列表输出的第一个元素是皮尔逊分数。我们看到 Alice 和 Bob 有最高可能的相似度分数,而 Alice 和 Eve 有最低可能的分数。
你能猜出 Bob 和 Eve 的相似度分数吗?
余弦相似度
在前一章中,我们数学上定义了余弦相似度分数,并在构建基于内容的推荐系统时广泛使用它:
数学上,余弦相似度定义如下:
余弦相似度分数计算在 n 维空间中两个向量之间的夹角余弦值。当余弦分数为 1(或角度为 0)时,向量完全相似。另一方面,余弦分数为-1(或角度为 180 度)表示两个向量完全不相似。
现在考虑两个均值为零的向量 x 和 y。我们看到在这种情况下,皮尔逊相关分数与余弦相似度分数完全相同。换句话说,对于均值为零的中心化向量,皮尔逊相关性即是余弦相似度分数。
在不同的场景中,适合使用不同的相似度分数。对于重视幅度的情况,欧几里得距离是一个适当的度量标准。然而,正如我们在皮尔逊相关子节中所看到的那样,对我们而言幅度并不像相关性重要。因此,在构建过滤器时,我们将使用皮尔逊和余弦相似度分数。
聚类
协同过滤背后的主要思想之一是,如果用户 A 对某个产品的意见与用户 B 相同,则 A 在另一个产品上与 B 的意见相同的可能性也比随机选择的用户更高。
聚类是协同过滤算法中最流行的技术之一。它是一种无监督学习,根据数据点之间的相似性将数据点分组到不同的类中:
例如,假设我们所有的用户都被绘制在一个二维笛卡尔平面上,如前图所示。聚类算法的任务是为平面上的每个点分配类别。就像相似性度量一样,没有一种聚类算法可以解决所有问题。每种算法都有其特定的使用场景,只适用于某些问题。在本节中,我们只会讨论 k-means 聚类算法,它能够很好地为前述的点集分配类别。我们还将看到一个 k-means 不适用的案例。
k-means 聚类
k-means 算法是最简单也是最流行的机器学习算法之一。它将数据点和聚类的数量(k)作为输入。
接下来,它会随机绘制 k 个不同的点在平面上(称为质心)。在随机绘制了 k 个质心后,以下两个步骤会反复执行,直到 k 个质心的集合不再发生变化:
-
点到质心的分配:每个数据点会被分配给离它最近的质心。被分配给某个特定质心的数据点集合称为一个聚类。因此,将点分配给 k 个质心会形成 k 个聚类。
-
质心的重新分配:在下一步中,每个聚类的质心会重新计算,作为该聚类的中心(或者是该聚类中所有点的平均值)。然后,所有数据点将重新分配到新的质心:
上面的截图展示了 k-means 聚类算法步骤的可视化,已分配的聚类数量为两个。
本章的某些部分使用了 Matplotlib 和 Seaborn 库进行可视化。你不需要理解书中编写的绘图代码,但如果你仍然感兴趣,可以在matplotlib.org/users/pyplot_tutorial.html
找到官方的 Matplotlib 教程,在seaborn.pydata.org/tutorial.html
找到官方的 Seaborn 教程。
我们不会从头实现 k-means 算法,而是使用 scikit-learn 提供的实现。作为第一步,让我们访问本节开头绘制的数据点:
#Import the function that enables us to plot clusters
from sklearn.datasets.samples_generator import make_blobs
#Get points such that they form 3 visually separable clusters
X, y = make_blobs(n_samples=300, centers=3,
cluster_std=0.50, random_state=0)
#Plot the points on a scatterplot
plt.scatter(X[:, 0], X[:, 1], s=50)
使用 k-means 算法时,最重要的步骤之一是确定聚类的数量。在这种情况下,从图中(以及代码中)可以清楚地看出,我们已经将点绘制成三个明显可分的聚类。现在,让我们通过 scikit-learn 应用 k-means 算法并评估其性能:
#Import the K-Means Class
from sklearn.cluster import KMeans
#Initializr the K-Means object. Set number of clusters to 3,
#centroid initilalization as 'random' and maximum iterations to 10
kmeans = KMeans(n_clusters=3, init='random', max_iter=10)
#Compute the K-Means clustering
kmeans.fit(X)
#Predict the classes for every point
y_pred = kmeans.predict(X)
#Plot the data points again but with different colors for different classes
plt.scatter(X[:, 0], X[:, 1], c=y_pred, s=50)
#Get the list of the final centroids
centroids = kmeans.cluster_centers_
#Plot the centroids onto the same scatterplot.
plt.scatter(centroids[:, 0], centroids[:, 1], c='black', s=100, marker='X')
我们看到该算法在识别三个聚类方面表现非常成功。三个最终的质心也在图中标记为 X:
选择 k
如前一小节所述,选择合适的 k 值对 k-means 聚类算法的成功至关重要。聚类数可以在 1 到数据点总数之间的任何数值(每个点被分配到自己的聚类中)。
现实世界中的数据很少像之前探讨的那样,其中数据点在二维平面上形成了明确定义、视觉上可分的聚类。为了确定一个合适的 K 值,有几种方法可供选择。在本节中,我们将探讨通过肘部法则来确定 k 值的方法。
肘部法则计算每个 k 值的平方和,并选择平方和与 K 的关系图中的肘部点作为 k 的最佳值。肘部点定义为 k 值,在该点之后,所有后续 k 的平方和值开始显著减缓下降。
平方和值被定义为每个数据点到其所属聚类质心的距离的平方和。数学表达式如下:
这里,Ck 是第 k 个聚类,uk 是 Ck 对应的质心。
幸运的是,scikit-learn 的 k-means 实现会在计算聚类时自动计算平方和。现在我们来可视化数据的肘部图,并确定最佳的 K 值:
#List that will hold the sum of square values for different cluster sizes
ss = []
#We will compute SS for cluster sizes between 1 and 8.
for i in range(1,9):
#Initialize the KMeans object and call the fit method to compute clusters
kmeans = KMeans(n_clusters=i, random_state=0, max_iter=10, init='random').fit(X)
#Append the value of SS for a particular iteration into the ss list
ss.append(kmeans.inertia_)
#Plot the Elbow Plot of SS v/s K
sns.pointplot(x=[j for j in range(1,9)], y=ss)
从图中可以看出,肘部出现在 K=3 处。根据我们之前的可视化结果,我们知道这确实是该数据集的最佳聚类数。
其他聚类算法
尽管 k-means 算法非常强大,但并不适用于每一种情况。
为了说明这一点,我们构造了一个包含两个半月形状的图。与之前的簇形图相似,scikit-learn 为我们提供了一个便捷的函数来绘制半月形聚类:
#Import the half moon function from scikit-learn
from sklearn.datasets import make_moons
#Get access to points using the make_moons function
X_m, y_m = make_moons(200, noise=.05, random_state=0)
#Plot the two half moon clusters
plt.scatter(X_m[:, 0], X_m[:, 1], s=50)
k-means 算法能否正确识别两个半月形状的聚类?我们来看看:
#Initialize K-Means Object with K=2 (for two half moons) and fit it to our data
kmm = KMeans(n_clusters=2, init='random', max_iter=10)
kmm.fit(X_m)
#Predict the classes for the data points
y_m_pred = kmm.predict(X_m)
#Plot the colored clusters as identified by K-Means
plt.scatter(X_m[:, 0], X_m[:, 1], c=y_m_pred, s=50)
现在我们来可视化 k-means 认为这组数据点存在的两个聚类:
我们看到,k-means 算法在识别正确的聚类上做得不好。对于像这些半月形状的聚类,另一种名为谱聚类的算法,结合最近邻和相似度,表现得要好得多。
我们不会深入探讨谱聚类的原理。相反,我们将使用其在 scikit-learn 中的实现,并直接评估其性能:
#Import Spectral Clustering from scikit-learn
from sklearn.cluster import SpectralClustering
#Define the Spectral Clustering Model
model = SpectralClustering(n_clusters=2, affinity='nearest_neighbors')
#Fit and predict the labels
y_m_sc = model.fit_predict(X_m)
#Plot the colored clusters as identified by Spectral Clustering
plt.scatter(X_m[:, 0], X_m[:, 1], c=y_m_sc, s=50)
我们看到,谱聚类在识别半月形聚类方面表现得非常好。
我们已经看到,不同的聚类算法适用于不同的情况。协同过滤的情况也一样。例如,我们将在下一章讨论的 surprise 包,它实现了一种协同过滤方法,使用了另一种聚类算法,叫做共聚类。我们将总结聚类的讨论,并转向另一种重要的数据挖掘技术:降维。
降维
大多数机器学习算法随着数据维度数量的增加而表现不佳。这种现象通常被称为维度灾难。因此,减少数据中可用特征的数量,同时尽可能保留最多的信息,是一个好主意。实现这一目标有两种方式:
-
特征选择:这种方法涉及识别出最少预测能力的特征,并将其完全删除。因此,特征选择涉及识别出对特定用例最重要的特征子集。特征选择的一个重要特点是,它保持每个保留特征的原始意义。例如,假设我们有一个包含价格、面积和房间数量的住房数据集。如果我们删除了面积特征,那么剩下的价格和房间数量特征仍然会保留其原本的意义。
-
特征提取:特征提取将 m 维数据转化为 n 维输出空间(通常 m >> n),同时尽可能保留大部分信息。然而,在此过程中,它创建了没有内在意义的新特征。例如,如果我们使用特征提取将同一个住房数据集输出为二维空间,新的特征将不再表示价格、面积或房间数量。它们将不再具有任何意义。
在本节中,我们将介绍一个重要的特征提取方法:主成分分析(或PCA)。
主成分分析
主成分分析是一种无监督的特征提取算法,它将 m 维输入转化为一组 n (m >> n)线性不相关的变量(称为主成分),以尽可能减少由于丢失 (m - n) 维度而导致的方差(或信息)损失。
PCA 中的线性变换是以这样的方式进行的:第一个主成分具有最大方差(或信息)。它通过考虑那些彼此高度相关的变量来实现这一点。每个主成分的方差都大于后续成分,且与前一个成分正交。
考虑一个三维空间,其中两个特征高度相关,而与第三个特征的相关性较低:
假设我们希望将其转换为二维空间。为此,PCA 试图识别第一个主成分,该主成分将包含最大可能的方差。它通过定义一个新维度,使用这两个高度相关的变量来实现。接下来,它试图定义下一个维度,使其具有最大方差,与第一个主成分正交,并且与其不相关。前面的图展示了这两个新维度(或主成分),PC 1 和 PC 2。
要理解 PCA 算法,需要一些超出本书范围的线性代数概念。相反,我们将使用scikit-learn
提供的 PCA 黑盒实现,并以著名的 Iris 数据集为例来考虑一个实际案例。
第一步是将 Iris 数据集从 UCI 机器学习库加载到 pandas DataFrame 中:
# Load the Iris dataset into Pandas DataFrame
iris = pd.read_csv("https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data",
names=['sepal_length','sepal_width','petal_length','petal_width','class'])
#Display the head of the dataframe
iris.head()
PCA 算法对数据的尺度非常敏感。因此,我们将对所有特征进行缩放,使其具有均值为 0、方差为 1 的分布:
#Import Standard Scaler from scikit-learn
from sklearn.preprocessing import StandardScaler
#Separate the features and the class
X = iris.drop('class', axis=1)
y = iris['class']
# Scale the features of X
X = pd.DataFrame(StandardScaler().fit_transform(X),
columns = ['sepal_length','sepal_width','petal_length','petal_width'])
X.head()
现在我们已经准备好应用 PCA 算法了。让我们将数据转换到二维空间中:
#Import PCA
from sklearn.decomposition import PCA
#Intialize a PCA object to transform into the 2D Space.
pca = PCA(n_components=2)
#Apply PCA
pca_iris = pca.fit_transform(X)
pca_iris = pd.DataFrame(data = pca_iris, columns = ['PC1', 'PC2'])
pca_iris.head()
scikit-learn
的 PCA 实现还会为我们提供每个主成分所包含的方差比率信息:
pca.explained_variance_ratio
OUTPUT:
array([ 0.72770452, 0.23030523])
我们看到,第一个主成分包含大约 72.8%的信息,而第二个主成分包含大约 23.3%的信息。总的来说,保留了 95.8%的信息,而去除两个维度则损失了 4.2%的信息。
最后,让我们在新的二维平面中按类别可视化我们的数据点:
#Concatenate the class variable
pca_iris = pd.concat([pca_iris, y], axis = 1)
#Display the scatterplot
sns.lmplot(x='PC1', y='PC2', data=pca_iris, hue='class', fit_reg=False)
其他降维技术
线性判别分析
与 PCA 类似,线性判别分析是一种线性变换方法,旨在将m维数据转换为n维输出空间。
然而,与 PCA 不同,PCA 旨在保留最大的信息量,而 LDA 则旨在识别一组n个特征,从而使类之间的分离度(或判别度)最大化。由于 LDA 需要标注的数据来确定其成分,因此它是一种监督学习算法。
现在让我们对 Iris 数据集应用 LDA 算法:
#Import LDA
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
#Define the LDA Object to have two components
lda = LinearDiscriminantAnalysis(n_components = 2)
#Apply LDA
lda_iris = lda.fit_transform(X, y)
lda_iris = pd.DataFrame(data = lda_iris, columns = ['C1', 'C2'])
#Concatenate the class variable
lda_iris = pd.concat([lda_iris, y], axis = 1)
#Display the scatterplot
sns.lmplot(x='C1', y='C2', data=lda_iris, hue='class', fit_reg=False)
我们看到,类之间的可分性比 PCA 方法下要好得多。
奇异值分解
奇异值分解(SVD)是一种矩阵分析技术,它允许我们将一个高维矩阵表示为低维矩阵。SVD 通过识别并去除矩阵中不重要的部分来实现这一点,从而在所需的维度数中生成一个近似值。
协同过滤的 SVD 方法最早由 Simon Funk 提出,并在 Netflix 奖竞赛中证明了其极高的受欢迎度和有效性。不幸的是,理解 SVD 需要掌握超出本书范围的线性代数知识。不过,在下一章中,我们将使用 surprise
包提供的黑箱实现来进行 SVD 协同过滤。
有监督学习
有监督学习是一类机器学习算法,它接收一系列向量及其对应的输出(一个连续值或一个类别)作为输入,生成一个推断函数,该函数可用于映射新的例子。
使用有监督学习的一个重要前提是需要有标注数据。换句话说,我们必须能够访问已经知道正确输出的输入数据。
有监督学习可以分为两种类型:分类和回归。分类问题的目标变量是一个离散值集(例如,喜欢和不喜欢),而回归问题的目标是一个连续值(例如,介于一到五之间的平均评分)。
考虑前面定义的评分矩阵。可以将(m-1)列作为输入,而第 m^(th) 列作为目标变量。通过这种方式,我们应该能够通过传入相应的(m-1)维向量来预测 m^(th) 列中不可用的值。
有监督学习是机器学习中最成熟的子领域之一,因此,现有许多强大的算法可以用于进行准确的预测。在这一部分,我们将探讨一些在多种应用中成功使用的最流行算法(包括协同过滤器)。
k-最近邻
k-最近邻 (k-NN) 可能是最简单的机器学习算法。在分类问题中,它通过其 k 个最近邻的多数投票为特定数据点分配一个类别。换句话说,数据点被分配给在其 k 个最近邻中最常见的类别。在回归问题中,它基于 k 个最近邻计算目标变量的平均值。
与大多数机器学习算法不同,k-NN 是非参数且懒惰的。前者意味着 k-NN 不对数据的分布做任何假设,换句话说,模型结构是由数据决定的。后者意味着 k-NN 几乎不进行任何训练,它只在预测阶段计算特定点的 k 个最近邻。这也意味着 k-NN 模型在预测过程中需要始终访问训练数据,不能像其他算法那样在预测时丢弃训练数据。
分类
k-NN 分类最好通过一个示例来解释。考虑一个具有二进制类的数据集(表示为蓝色方块和红色三角形)。k-NN 现在将其绘制到n维空间中(在这种情况下是二维)。
假设我们想预测绿色圆圈的类。在 k-NN 算法做出预测之前,它需要知道需要考虑的最近邻的数量(即k的值)。k通常是奇数(以避免在二进制分类的情况下出现平局)。
假设k=3。
k-NN 计算绿色圆圈与训练数据集中每个其他点的距离度量(通常是欧几里得距离),并选择与其最接近的三个数据点。在这种情况下,这些点位于实心内圈内。
下一步是确定三点中的多数类。这里有两个红色三角形和一个蓝色方块。因此,绿色圆圈被分配为红色三角形类。
现在,假设k=5。
在这种情况下,最近邻是所有包含在虚线外圈内的点。这次,我们有两个红色三角形和三个蓝色方块。因此,绿色圆圈被分配为蓝色方块类。
从前面的例子可以看出,k的值在确定数据点最终分配的类时至关重要。通常最好测试不同的k值,并使用交叉验证和测试数据集评估其性能。
回归
k-NN 回归几乎以相同的方式工作。不同之处在于,我们计算的是 k-NN 的属性值,而不是类。
假设我们有一个住房数据集,并且我们正在尝试预测一栋房子的价格。因此,特定房子的价格将由其k个最近邻房子的平均价格来决定。与分类一样,最终的目标值可能会根据k的值有所不同。
对于本节中的其余算法,我们将只介绍分类过程。然而,像 k-NN 一样,大多数算法只需要做出非常小的修改,就可以适用于回归问题。
支持向量机
支持向量机是行业中最流行的分类算法之一。它将一个n维数据集作为输入,并构建一个(n-1)维超平面,使得类之间的分离最大化。
请查看下面截图中二进制数据集的可视化:
前面的图展示了三个可能的超平面(直线),它们将两个类别分开。然而,实线是具有最大间隔的超平面。换句话说,它是最大程度上将两个类别分开的超平面。同时,它将整个平面分为两个区域。任何位于超平面下方的点将被分类为红色方块,而任何位于上方的点将被分类为蓝色圆圈。
SVM 模型仅依赖于support vectors
*;*这些是确定两个类别之间最大间隔的点。在上面的图中,填充的方块和圆圈是支持向量。其余的点对 SVM 的运作没有影响:
SVM 也能够分离那些非线性可分的类别(如前面的图所示)。它通过特殊的工具——径向核函数,来完成这一点,这些函数将数据点绘制到更高的维度,并尝试在那里构建最大间隔超平面。
决策树
决策树是非常快速且简单的基于树的算法,它根据信息增益最大的特征进行分支。决策树虽然不够准确,但具有极高的可解释性。
我们不会深入探讨决策树的内部工作原理,但我们会通过可视化展示它的实际操作:
假设我们想使用决策树对鸢尾花数据集进行分类。前面的图中展示了执行分类的决策树。我们从顶部开始,一直到树的深处,直到到达叶节点。
例如,如果花的花瓣宽度小于 0.8 厘米,我们就会到达一个叶节点,并将其分类为 setosa*。*如果不满足条件,它会进入另一个分支,直到到达一个叶节点。
决策树在其运作中具有随机性,并且在不同的迭代中会得出不同的条件。正如前面所说,它们的预测精度也不高。然而,它们的随机性和快速执行使得它们在集成模型中非常流行,接下来的部分将对此进行解释。
集成
集成的主要思想是多个算法的预测能力远大于单一算法。在构建集成模型时,决策树是最常用的基础算法。
装袋法与随机森林
装袋法是自助法聚合的简称。像大多数其他集成方法一样,它会对大量的基础分类模型进行平均,并将它们的结果平均化以提供最终预测。
构建装袋模型的步骤如下:
-
一定比例的数据点会被抽样(假设为 10%)。抽样是有放回的。换句话说,一个特定的数据点可以在多个迭代中出现。
-
基线分类模型(通常是决策树)在这些抽样数据上进行训练。
-
这个过程会一直重复,直到训练出n个模型为止。Bagging 模型的最终预测是所有基模型预测的平均值。
对 Bagging 模型的改进是随机森林模型。除了对数据点进行采样外,随机森林集成方法还强制每个基准模型随机选择一部分特征(通常是等于特征总数平方根的数目):
选择一部分样本和特征来构建基准决策树,大大增强了每棵树的随机性。这反过来提高了随机森林的鲁棒性,使其能够在噪声数据上表现得非常好。
此外,从特征子集构建基准模型并分析其对最终预测的贡献,还使得随机森林能够确定每个特征的重要性。因此,通过随机森林进行特征选择是可能的(回想一下,特征选择是一种降维方法)。
提升算法
Bagging 和随机森林模型训练出的基准模型是完全相互独立的。因此,它们不会从每个学习器所犯的错误中学习。这就是提升算法发挥作用的地方。
和随机森林一样,提升模型通过使用样本和特征的子集来构建基准模型。然而,在构建下一个学习器时,提升模型试图纠正前一个学习器所犯的错误。不同的提升算法以不同的方式实现这一点。
例如,原始的提升算法只是将 50%的错误分类样本加入第二个学习器,并将前两个学习器不同意见的所有样本加入,构建第三个最终学习器。然后,使用这三种学习器的集成进行预测。
提升算法极其鲁棒,通常能够提供高性能。这使得它们在数据科学竞赛中非常流行,而就我们而言,它们在构建强大的协同过滤器时也非常有用。
scikit-learn
为我们提供了本节所描述的所有算法的实现。每个算法的使用方式几乎相同。作为示例,让我们应用梯度提升算法对鸢尾花数据集进行分类:
#Divide the dataset into the feature dataframe and the target class series.
X, y = iris.drop('class', axis=1), iris['class']
#Split the data into training and test datasets.
#We will train on 75% of the data and assess our performance on 25% of the data
#Import the splitting function
from sklearn.model_selection import train_test_split
#Split the data into training and test sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=0)
#Import the Gradient Boosting Classifier
from sklearn.ensemble import GradientBoostingClassifier
#Apply Gradient Boosting to the training data
gbc = GradientBoostingClassifier()
gbc.fit(X_train, y_train)
#Compute the accuracy on the test set
gbc.score(X_test, y_test)
OUTPUT:
0.97368421052631582
我们看到,分类器在未见过的测试数据上达到了 97.3%的准确率。像随机森林一样,梯度提升机能够衡量每个特征的预测能力。让我们绘制一下鸢尾花数据集的特征重要性:
#Display a bar plot of feature importances
sns.barplot(x= ['sepal_length','sepal_width','petal_length','petal_width'], y=gbc.feature_importances_)
评估指标
在本节中,我们将查看一些指标,这些指标可以让我们从数学角度量化分类器、回归器和过滤器的性能。
准确率
准确率是最常用的分类模型性能衡量指标。它是正确预测的案例数与模型预测的总案例数之间的比值:
均方根误差
均方根误差 (RMSE) 是一种广泛用于衡量回归模型表现的指标。从数学上讲,它可以表示为:
这里,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-rec-sys-py/img/7ca4e738-dbfe-4c82-9f9c-d6ac7ff621a0.png 是第 i^(个)实际目标值,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-rec-sys-py/img/7ae9ea8f-9150-4c9a-8028-df08658eff7e.png 是第 i^(个)预测目标值。
二分类指标
有时,准确率无法很好地估计模型的表现。
举个例子,考虑一个二分类数据集,其中 99%的数据属于一个类别,只有 1%的数据属于另一个类别。如果一个分类器总是预测多数类,那么它的准确率会是 99%。但这并不意味着分类器表现得好。
对于这种情况,我们需要使用其他指标。为了理解这些指标,我们首先需要定义一些术语:
-
真正例 (TP):真正例指的是实际类别和预测类别都为正类的所有情况。
-
真负例 (TN):真负例指的是实际类别和预测类别都为负类的所有情况。
-
假正例 (FP):假正例是指实际类别为负类,但预测类别为正类的所有情况。
-
假负例 (FN):假负例是指实际类别为正类,但预测类别为负类的所有情况。
举例来说,假设有一项测试试图确定某人是否患有癌症。如果测试预测某人患有癌症,而实际并未患病,那么就是假正例。相反,如果测试未能检测到某人实际上患有癌症,那么就是假负例。
精确率
精确率是指正确识别出的正类案例数与所有被识别为正类的案例数的比值。从数学上看,它如下所示:
召回率
召回率是指识别出的正类案例数与数据集中所有正类案例数的比值:
F1 分数
F1 分数是一个衡量精确率和召回率之间平衡的指标。它是精确率和召回率的调和平均数。F1 分数为 1 表示精确率和召回率完美,F1 分数为 0 则表示精确率和召回率都无法达到:
总结
在本章中,我们涵盖了许多主题,这些主题将帮助我们构建强大的协同过滤器。我们看到了聚类,它是一种无监督学习算法,能够帮助我们将用户划分为明确的群体。接下来,我们通过了一些降维技术,以克服维度灾难并提升学习算法的性能。
接下来的章节讨论了监督学习算法,最后我们以各种评估指标的简要概述结束了本章。
本章涵盖的主题足以成为一本书的内容,我们并没有像通常的机器学习工程师那样深入分析这些技术。然而,我们在本章中学到的内容足以帮助我们构建和理解协同过滤器,这也是本书的主要目标之一。如果你有兴趣,关于本章所呈现主题的更详细处理,可以参考 Sebastian Thrun 的优秀书籍《Python 机器学习》。
第六章:构建协同过滤器
在前一章节中,我们通过数学方式定义了协同过滤问题,并了解了我们认为在解决这个问题时有用的各种数据挖掘技术。
终于到了我们将技能付诸实践的时候了。在第一节中,我们将构建一个明确定义的框架,允许我们轻松构建和测试我们的协同过滤模型。这个框架将包括数据、评估指标和相应的函数,用于计算给定模型的指标。
技术要求
你需要在系统中安装 Python。最后,为了使用本书的 Git 仓库,用户还需要安装 Git。
本章的代码文件可以在 GitHub 上找到:
github.com/PacktPublishing/Hands-On-Recommendation-Systems-with-Python
。
查看以下视频,看看代码如何运行:
框架
就像基于知识和基于内容的推荐系统一样,我们将在电影的背景下构建我们的协同过滤模型。由于协同过滤要求有用户行为数据,我们将使用一个不同的数据集,称为 MovieLens。
MovieLens 数据集
MovieLens 数据集由 GroupLens Research 提供,GroupLens 是明尼苏达大学的一个计算机科学实验室。它是最流行的基准数据集之一,用于测试各种协同过滤模型的效果,通常可以在大多数推荐库和包中找到:
MovieLens 提供了关于各种电影的用户评分,且有多个版本可用。完整版本包含超过 26,000,000 条评分,涉及 45,000 部电影,由 270,000 用户评分。然而,为了快速计算,我们将使用一个更小的 100,000 数据集,该数据集包含 100,000 条评分,由 1,000 个用户对 1,700 部电影进行评分。
下载数据集
不再废话,让我们直接下载 100,000 数据集。官方 GroupLens 网站上提供的数据集已经不再包含用户人口统计信息。因此,我们将使用一个由 Prajit Datta 在 Kaggle 上发布的旧版数据集。
下载 MovieLens 100,000 数据集,请访问www.kaggle.com/prajitdatta/movielens-100k-dataset/data
。
解压文件夹并将其重命名为movielens
。*接下来,将该文件夹移动到RecoSys
中的data
文件夹内。*MovieLens 数据集应该包含大约 23 个文件。然而,我们只关心u.data
、u.user
和u.item
这几个文件。*让我们在下一节中探讨这些文件。
探索数据
正如前一节所提到的,我们只对movielens
文件夹中的三个文件感兴趣:u.data
、u.user
和u.item
。尽管这些文件不是 CSV 格式,但加载它们到 Pandas 数据框中的代码几乎是相同的。
我们从u.user
开始*:*
#Load the u.user file into a dataframe
u_cols = ['user_id', 'age', 'sex', 'occupation', 'zip_code']
users = pd.read_csv('../data/movielens/u.user', sep='|', names=u_cols,
encoding='latin-1')
users.head()
这是它的输出**:**
我们看到u.user
文件包含了关于用户的 demographic(人口统计)信息,如年龄、性别、职业和邮政编码。
接下来,让我们看看u.item
文件,它提供了关于用户已评分电影的信息:
#Load the u.items file into a dataframe
i_cols = ['movie_id', 'title' ,'release date','video release date', 'IMDb URL', 'unknown', 'Action', 'Adventure',
'Animation', 'Children\'s', 'Comedy', 'Crime', 'Documentary', 'Drama', 'Fantasy',
'Film-Noir', 'Horror', 'Musical', 'Mystery', 'Romance', 'Sci-Fi', 'Thriller', 'War', 'Western']
movies = pd.read_csv('../data/movielens/u.item', sep='|', names=i_cols, encoding='latin-1')
movies.head()
这是它的输出**:**
我们看到这个文件提供了关于电影的标题、上映日期、IMDb 网址以及它的类型等信息。由于我们本章专注于构建协同过滤,因此除了电影标题和对应的 ID 外,我们不需要其他任何信息:
#Remove all information except Movie ID and title
movies = movies[['movie_id', 'title']]
最后,让我们将u.data
文件导入到我们的笔记本中。这个文件可以说是最重要的,因为它包含了每个用户对电影的评分。正是从这个文件中,我们将构建我们的评分矩阵:
#Load the u.data file into a dataframe
r_cols = ['user_id', 'movie_id', 'rating', 'timestamp']
ratings = pd.read_csv('../data/movielens/u.data', sep='\t', names=r_cols,
encoding='latin-1')
ratings.head()
这是它的输出**:**
我们看到,在新的ratings
数据框中,每一行代表一个用户在某一时间给特定电影的评分。然而,对于本章的练习,我们并不关心评分给出的具体时间。因此,我们将直接去掉这一列:
#Drop the timestamp column
ratings = ratings.drop('timestamp', axis=1)
训练和测试数据
ratings
数据框包含了电影的用户评分,评分范围从 1 到 5。因此,我们可以将这个问题建模为一个监督学习问题,其中我们需要预测一个用户对一部电影的评分。尽管评分只有五个离散值,我们仍将其建模为回归问题。
假设一个用户给某电影的真实评分是 5。一个分类模型无法区分预测的评分是 1 还是 4,它会将两者都视为错误分类。然而,一个回归模型会对前者给予更多的惩罚,这正是我们希望的行为。
正如我们在第五章中所看到的,数据挖掘技术入门,构建监督学习模型的第一步是构造训练集和测试集。模型将使用训练集进行学习,并使用测试集评估其效能。
现在让我们将评分数据集拆分成训练集和测试集,其中 75%的评分用于训练数据集,25%用于测试数据集。我们将使用一种稍微有点“hacky”的方式来完成:我们假设user_id
字段是目标变量(或y
),而我们的ratings
数据框则由预测变量(或X
)组成**.**然后我们将这两个变量传递给 scikit-learn 的train_test_split
函数,并沿着y
进行stratify
处理。这样可以确保训练集和测试集中的每个类别的比例相同:
#Import the train_test_split function
from sklearn.model_selection import train_test_split
#Assign X as the original ratings dataframe and y as the user_id column of ratings.
X = ratings.copy()
y = ratings['user_id']
#Split into training and test datasets, stratified along user_id
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.25, stratify=y, random_state=42)
评估
我们从第五章,数据挖掘技术入门中得知,RMSE(均方根误差)是回归模型中最常用的性能评估指标。我们也将使用 RMSE 来评估我们的模型表现。scikit-learn
已经提供了均方误差的实现。因此,我们所要做的就是定义一个函数,返回mean_squared_error
函数返回值的平方根**😗*
#Import the mean_squared_error function
from sklearn.metrics import mean_squared_error
#Function that computes the root mean squared error (or RMSE)
def rmse(y_true, y_pred):
return np.sqrt(mean_squared_error(y_true, y_pred))
接下来,让我们定义我们的基准协同过滤模型。我们所有的协同过滤(或CF)模型都将以user_id
和movie_id
作为输入,并输出一个介于 1 和 5 之间的浮动值。我们将基准模型定义为无论user_id
或movie_id
如何,都返回3
:
#Define the baseline model to always return 3.
def baseline(user_id, movie_id):
return 3.0
为了测试模型的效果,我们计算该特定模型在测试数据集中的所有用户-电影对所得到的 RMSE:
#Function to compute the RMSE score obtained on the testing set by a model
def score(cf_model):
#Construct a list of user-movie tuples from the testing dataset
id_pairs = zip(X_test['user_id'], X_test['movie_id'])
#Predict the rating for every user-movie tuple
y_pred = np.array([cf_model(user, movie) for (user, movie) in id_pairs])
#Extract the actual ratings given by the users in the test data
y_true = np.array(X_test['rating'])
#Return the final RMSE score
return rmse(y_true, y_pred)
一切准备就绪。现在让我们计算一下基准模型得到的 RMSE:
score(baseline)
OUTPUT:
1.2470926188539486
我们得到了一个1.247
的分数。对于接下来构建的模型,我们将尽力使得 RMSE 低于基准模型的 RMSE。
基于用户的协同过滤
在第一章,推荐系统入门中,我们了解了基于用户的协同过滤器的工作原理:它们找到与特定用户相似的用户,然后向第一个用户推荐那些用户喜欢的产品。
在本节中,我们将用代码实现这一思路。我们将构建逐渐复杂的过滤器,并使用前一节中构建的框架来评估它们的表现。
为了帮助我们完成这一过程,让我们首先构建一个评分矩阵(在第一章,推荐系统入门和第五章,数据挖掘技术入门中有所描述),其中每一行代表一个用户,每一列代表一部电影。因此,第 i^(行)和 j^(列)的值表示用户i
对电影j
的评分。像往常一样,pandas 提供了一个非常有用的函数,叫做pivot_table
,可以从我们的ratings
数据框中构建该矩阵:
#Build the ratings matrix using pivot_table function
r_matrix = X_train.pivot_table(values='rating', index='user_id', columns='movie_id')
r_matrix.head()
这是它的输出**😗*
现在我们有了一个新的r_matrix
DataFrame,其中每一行代表一个用户,每一列代表一部电影。另外,请注意,DataFrame 中的大多数值是未指定的。这给我们提供了矩阵稀疏程度的一个图景。
平均值
让我们首先构建最简单的协同过滤器之一。它只是输入user_id
和movie_id
,并输出所有评分该电影的用户的平均评分。用户之间没有区分。换句话说,每个用户的评分都赋予相同的权重。
可能有些电影只在测试集而不在训练集中(因此不在我们的评分矩阵中)。在这种情况下,我们将像基线模型一样默认评分为3.0
:
#User Based Collaborative Filter using Mean Ratings
def cf_user_mean(user_id, movie_id):
#Check if movie_id exists in r_matrix
if movie_id in r_matrix:
#Compute the mean of all the ratings given to the movie
mean_rating = r_matrix[movie_id].mean()
else:
#Default to a rating of 3.0 in the absence of any information
mean_rating = 3.0
return mean_rating
#Compute RMSE for the Mean model
score(cf_user_mean)
OUTPUT:
1.0234701463131335
我们看到这个模型的得分较低,因此比基线模型更好。
加权平均
在之前的模型中,我们给所有用户赋予了相等的权重。然而,从直觉上讲,给那些评分与当前用户相似的用户更多的权重,而不是那些评分不相似的用户,是有道理的。
因此,让我们通过引入一个权重系数来修改我们之前的模型。这个系数将是我们在上一章中计算的相似度度量之一。从数学上讲,它表示如下:
在这个公式中,r[u,m]表示用户u对电影m的评分。
为了本次练习,我们将使用余弦分数作为我们的相似度函数(或 sim)。回想一下,我们在构建基于内容的引擎时是如何构建电影余弦相似度矩阵的。在本节中,我们将为我们的用户构建一个非常相似的余弦相似度矩阵。
然而,scikit-learn 的cosine_similarity
函数无法处理NaN
值。因此,我们将把所有缺失值转换为零,以便计算我们的余弦相似度矩阵:
#Create a dummy ratings matrix with all null values imputed to 0
r_matrix_dummy = r_matrix.copy().fillna(0)
# Import cosine_score
from sklearn.metrics.pairwise import cosine_similarity
#Compute the cosine similarity matrix using the dummy ratings matrix
cosine_sim = cosine_similarity(r_matrix_dummy, r_matrix_dummy)
#Convert into pandas dataframe
cosine_sim = pd.DataFrame(cosine_sim, index=r_matrix.index, columns=r_matrix.index)
cosine_sim.head(10)
这是它的输出**😗*
有了用户余弦相似度矩阵,我们现在可以有效地计算该模型的加权平均评分。然而,在代码中实现这个模型比其简单的均值模型要复杂一些。这是因为我们只需要考虑那些具有相应非空评分的余弦相似度分数。换句话说,我们需要避免所有没有对电影m进行评分的用户:
#User Based Collaborative Filter using Weighted Mean Ratings
def cf_user_wmean(user_id, movie_id):
#Check if movie_id exists in r_matrix
if movie_id in r_matrix:
#Get the similarity scores for the user in question with every other user
sim_scores = cosine_sim[user_id]
#Get the user ratings for the movie in question
m_ratings = r_matrix[movie_id]
#Extract the indices containing NaN in the m_ratings series
idx = m_ratings[m_ratings.isnull()].index
#Drop the NaN values from the m_ratings Series
m_ratings = m_ratings.dropna()
#Drop the corresponding cosine scores from the sim_scores series
sim_scores = sim_scores.drop(idx)
#Compute the final weighted mean
wmean_rating = np.dot(sim_scores, m_ratings)/ sim_scores.sum()
else:
#Default to a rating of 3.0 in the absence of any information
wmean_rating = 3.0
return wmean_rating
score(cf_user_wmean)
OUTPUT:
1.0174483808407588
由于我们处理的是正向评分,余弦相似度分数将始终为正。因此,我们在计算归一化因子时(即确保最终评分被缩放回 1 到 5 之间的方程的分母)不需要显式地添加模值函数。
然而,如果你正在使用一个可能在此场景中为负的相似度度量(例如,皮尔逊相关系数),那么我们必须考虑模值。
运行这段代码的时间明显比之前的模型要长。然而,我们在 RMSE 评分上取得了(非常小的)改进。
用户人口统计
最后,让我们看看利用用户人口统计信息的过滤器。这些过滤器的基本直觉是,相同人口统计的用户往往有相似的口味。因此,它们的有效性依赖于这样一个假设:女性、青少年或来自同一地区的人会有相同的电影口味。
与之前的模型不同,这些过滤器并不考虑所有用户对特定电影的评分。而是只看那些符合特定人口统计的用户。
现在让我们构建一个性别人口统计过滤器。这个过滤器的作用是识别用户的性别,计算该性别对电影的(加权)平均评分,并返回该值作为预测结果。
我们的ratings
DataFrame 不包含用户的人口统计信息。我们将通过将users
DataFrame 导入并合并它们来获取这些信息(像往常一样使用 pandas)。熟悉 SQL 的读者会看到,这与 JOIN 功能非常相似:
#Merge the original users dataframe with the training set
merged_df = pd.merge(X_train, users)
merged_df.head()
这是它的输出**:**
接下来,我们需要按性别计算每部电影的mean
评分。Pandas 通过groupby
方法使这变得可能:
#Compute the mean rating of every movie by gender
gender_mean = merged_df[['movie_id', 'sex', 'rating']].groupby(['movie_id', 'sex']) ['rating'].mean()
我们现在可以定义一个函数,识别用户的性别,提取该性别对特定电影的平均评分,并返回该值作为输出:
#Set the index of the users dataframe to the user_id
users = users.set_index('user_id')
#Gender Based Collaborative Filter using Mean Ratings
def cf_gender(user_id, movie_id):
#Check if movie_id exists in r_matrix (or training set)
if movie_id in r_matrix:
#Identify the gender of the user
gender = users.loc[user_id]['sex']
#Check if the gender has rated the movie
if gender in gender_mean[movie_id]:
#Compute the mean rating given by that gender to the movie
gender_rating = gender_mean[movie_id][gender]
else:
gender_rating = 3.0
else:
#Default to a rating of 3.0 in the absence of any information
gender_rating = 3.0
return gender_rating
score(cf_gender)
OUTPUT:
1.0330308800874282
我们看到,这个模型实际上比标准的均值评分协同过滤器表现更差。这表明,用户的性别并不是其电影口味的最强指示因素。
让我们再试构建一个人口统计过滤器,这次使用性别和职业:
#Compute the mean rating by gender and occupation
gen_occ_mean = merged_df[['sex', 'rating', 'movie_id', 'occupation']].pivot_table(
values='rating', index='movie_id', columns=['occupation', 'sex'], aggfunc='mean')
gen_occ_mean.head()
我们看到pivot_table
方法为我们提供了所需的 DataFrame。然而,这本可以通过groupby
来完成。pivot_table
只是groupby
方法的一种更紧凑、更易于使用的接口:
#Gender and Occupation Based Collaborative Filter using Mean Ratings
def cf_gen_occ(user_id, movie_id):
#Check if movie_id exists in gen_occ_mean
if movie_id in gen_occ_mean.index:
#Identify the user
user = users.loc[user_id]
#Identify the gender and occupation
gender = user['sex']
occ = user['occupation']
#Check if the occupation has rated the movie
if occ in gen_occ_mean.loc[movie_id]:
#Check if the gender has rated the movie
if gender in gen_occ_mean.loc[movie_id][occ]:
#Extract the required rating
rating = gen_occ_mean.loc[movie_id][occ][gender]
#Default to 3.0 if the rating is null
if np.isnan(rating):
rating = 3.0
return rating
#Return the default rating
return 3.0
score(cf_gen_occ)
OUTPUT:
1.1391976012043645
我们看到这个模型的表现是所有我们构建的过滤器中最差的,仅仅比基准模型好一点。这强烈暗示,修改用户人口统计数据可能不是我们当前使用的数据的最佳处理方式。然而,鼓励你尝试不同的用户人口统计数据的排列组合,看看哪些表现最好。你也可以尝试其他改进模型的技术,例如使用加权平均数来作为pivot_table
的aggfunc
,并尝试不同(或许更有依据的)默认评分。
基于物品的协同过滤
基于物品的协同过滤本质上是基于用户的协同过滤,其中用户扮演了物品所扮演的角色,反之亦然。
在基于物品的协同过滤中,我们计算库存中每个物品的两两相似度。然后,给定user_id
和movie_id
*,*我们计算用户对其评级的所有物品的加权平均值。该模型背后的基本思想是,特定用户可能会类似地评价两个相似的物品。
构建基于物品的协同过滤器留给读者作为练习。所涉及的步骤与前述完全相同,只是现在电影和用户位置交换了。
基于模型的方法
到目前为止,我们构建的协同过滤器被称为内存型过滤器。这是因为它们只利用相似性度量来得出结果。它们从数据中不学习任何参数,也不为数据分配类别/簇。换句话说,它们不使用机器学习算法。
在本节中,我们将看一些这样的过滤器。我们花了一整章的时间来研究各种监督和无监督学习技术。现在终于是时候看到它们的实际应用并测试它们的效力了。
聚类
在我们的加权平均滤波器中,当试图预测最终评分时,我们考虑了每位用户。相比之下,我们的基于人口统计的过滤器只考虑符合特定人口统计的用户。我们发现,与加权平均滤波器相比,人口统计过滤器表现不佳。
但这是否必然意味着我们需要考虑所有用户才能取得更好的结果呢?
人口统计过滤器的一个主要缺点是,它们基于这样一种假设:来自某一特定人口统计的人们有相似的思想和评分。然而,我们可以肯定地说,这是一个过于牵强的假设。并非所有男性都喜欢动作片,也不是所有儿童都喜欢动画片。同样,假设来自特定地区或职业的人们会有相同的口味是非常牵强的。
我们需要想出一种比人口统计更强大的方式来分组用户。从第五章,《开始使用数据挖掘技术》,我们已经知道一种非常强大的工具:clustering
。
可以使用聚类算法,如 k-means,将用户分组成一个簇,然后在预测评分时只考虑同一簇中的用户。
在本节中,我们将使用 k-means 的姐妹算法,kNN*,来构建基于聚类的协同过滤器。简而言之,给定一个用户u和一个电影m*,涉及以下步骤:
-
找到评价过电影m的u的 k 个最近邻居
-
输出m的k个用户的平均评分
就这样。这种极其简单的算法恰好是最广泛使用的之一。
和 kNN 一样,我们不会从头开始实现基于 kNN 的协同过滤器。相反,我们将使用一个非常流行且强大的库——surprise
:
Surprise 是一个用于构建推荐系统的 Python 科学工具包(scikit)。你可以把它看作是 scikit-learn 在推荐系统方面的对等物。根据其文档,surprise
代表简单的 Python 推荐系统引擎。短短时间内,surprise
已经成为最流行的推荐库之一。这是因为它非常健壮且易于使用。它为我们提供了大多数流行的协同过滤算法的现成实现,并且还允许我们将自己的算法集成到框架中。
要下载surprise
,像其他 Python 库一样,打开终端并输入以下命令:
sudo pip3 install scikit-surprise
现在,让我们构建并评估基于 kNN 的协同过滤器。尽管surprise库中已有 MovieLens 数据集,我们仍然会使用我们手头的外部数据,以便体验如何使用该库处理外部数据集:
#Import the required classes and methods from the surprise library
from surprise import Reader, Dataset, KNNBasic, evaluate
#Define a Reader object
#The Reader object helps in parsing the file or dataframe containing ratings
reader = Reader()
#Create the dataset to be used for building the filter
data = Dataset.load_from_df(ratings, reader)
#Define the algorithm object; in this case kNN
knn = KNNBasic()
#Evaluate the performance in terms of RMSE
evaluate(knn, data, measures=['RMSE'])
这是它的输出**😗*
输出结果表明,过滤器采用了一种称为五重交叉验证
的技术。简而言之,这意味着surprise
将数据分为五个相等的部分。然后,它使用其中四部分作为训练数据,并在第五部分上进行测试。这个过程会进行五次,每次都确保每一部分都会充当一次测试数据。
我们看到该模型得到的 RMSE 为 0.9784,这是目前为止我们取得的最好结果。
现在,让我们看看其他一些基于模型的协同过滤方法,并使用surprise库实现其中一些方法。
监督学习与降维
再次考虑我们的评分矩阵。它的形状是m × n,每一行代表m个用户之一,每一列代表n个物品之一。
现在,让我们删除其中一列(比如 n[j])。我们现在得到的是一个m × (n-1)的矩阵。如果我们将m × (n-1)的矩阵视为预测变量,并将 n[j]视为目标变量,就可以使用监督学习算法训练 n[j]中已有的值,以预测其中缺失的值。对于每一列,我们可以重复这个过程 n 次,最终完成我们的矩阵。
一个大问题是,大多数监督学习算法不能处理缺失数据。在标准问题中,通常会用所属列的均值或中位数来填充缺失值。
然而,我们的矩阵存在严重的数据稀疏问题。矩阵中超过 99%的数据不可用。因此,简单地用均值或中位数填充缺失值是行不通的,因为这会引入大量偏差。
可能想到的一种解决方案是以某种方式压缩预测矩阵,以便所有的值都可以获得。不幸的是,像 SVD 和 PCA 这样的降维技术,在缺失值的环境中同样无法奏效。
在为 Netflix 问题寻找解决方案的过程中,Simon Funk 提出了一个可以将 m × (n-1) 矩阵降维成一个低维的 m × d 矩阵,其中 d << n。他使用了标准的降维技术(在他这里是 SVD),但是做了一些小的调整。解释这种技术超出了本书的范围,但它已在附录中为进阶读者介绍。为了本章的目的,我们将把这项技术视作一个黑盒,它将 m × n 稀疏矩阵转换为 m × d 稠密矩阵,其中 d << n,并称之为 SVD-like
*。
现在让我们把注意力转向或许是有史以来最著名的推荐算法:奇异值分解*。*
奇异值分解
在第五章,数据挖掘技术入门 中,我们提到过奇异值分解背后的数学超出了本书的范围。然而,让我们尝试从外行的角度理解它是如何工作的。
回想一下第五章,数据挖掘技术入门,PCA(主成分分析)将一个 m × n 矩阵转化为 n 个 m 维的向量(称为主成分),使得每个分量与下一个分量正交。它还构造这些分量的方式是,使得第一个分量包含最多的方差(或信息),接下来是第二个分量,以此类推。
让我们将评分矩阵表示为 A*。* 该矩阵的转置为 A*^T*,它的形状为 n × m,每一行将表示一部电影(而不是一个用户)。
现在,我们可以使用 PCA 从 A 和 A*^T* 分别构建出两个新的矩阵,U 和 V。
奇异值分解使我们能够一次性从 A 计算出 U 和 V:
从本质上讲,奇异值分解是一种矩阵分解技术。它接收一个输入 A,并输出 U 和 V,使得:
其中 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-rec-sys-py/img/d4a5f097-a8e4-4459-9b20-2b07a31b5713.png 是一个对角矩阵。它用于缩放目的,在本示例中可以假设它与 U 或 V 合并。因此,我们现在有:
U 矩阵,本质上由用户的主成分组成,通常称为用户嵌入矩阵。它的对应矩阵 V 被称为电影嵌入矩阵。
SVD 的经典版本和大多数其他机器学习算法一样,不适用于稀疏矩阵。然而,Simon Funk 找到了一个解决该问题的方法,他的解决方案成为了推荐系统领域最著名的解决方案之一。
Funk 的系统将稀疏的评分矩阵 A 输入,并分别构建了两个稠密的用户和物品嵌入矩阵 U 和 V。这些稠密矩阵直接为我们提供了原始矩阵 A 中所有缺失值的预测。
现在让我们使用 surprise
包来实现 SVD 滤波器:
#Import SVD
from surprise import SVD
#Define the SVD algorithm object
svd = SVD()
#Evaluate the performance in terms of RMSE
evaluate(svd, data, measures=['RMSE'])
这是它的输出**:**
SVD 滤波器的表现优于所有其他滤波器,RMSE 得分为 0.9367。
总结
这也标志着我们对协同过滤讨论的结束。在这一章中,我们构建了各种基于用户的协同过滤器,并由此学会了构建基于物品的协同过滤器。
然后我们将注意力转向了基于模型的方法,这些方法依赖于机器学习算法来生成预测。我们介绍了 surprise 库,并使用它实现了基于 kNN 的聚类模型。接着我们看了一种使用监督学习算法预测评分矩阵中缺失值的方法。最后,我们以外行的视角理解了奇异值分解算法,并使用 surprise
库实现了该算法*。*
到目前为止,我们构建的所有推荐系统仅存在于我们的 Jupyter Notebook 中。在下一章,我们将学习如何将我们的模型部署到网络上,让任何人都能在互联网上使用它们。