在本章中,我们将介绍以下食谱:
- 建模偏好表达式
- 了解数据
- 摄取电影评论数据
- 寻找得分最高的电影
- 改善电影分级系统
- 测量偏好空间中用户之间的距离
- 计算用户之间的相关性
- 为用户找到最佳批评家
- 预测用户的电影收视率
- 逐项协作过滤
- 建立非负矩阵分解模型
- 将整个数据集加载到内存中
- 将基于 SVD 的模型转储到磁盘
- 训练基于 SVD 的模型
- 测试基于 SVD 的模型
从书籍,电影到人们在 Twitter 上关注,推荐系统将互联网上的大量信息雕刻成更加个性化的信息流,从而提高了电子商务,Web 和社交应用的性能。 鉴于亚马逊货币化推荐的成功和 Netflix 奖的获得,毫不奇怪,任何有关个性化或数据理论预测的讨论都将涉及推荐者。 令人惊讶的是,推荐者实施起来多么简单,却容易受到稀疏数据和过度拟合的影响。
考虑一种采用非算法的方法来提出建议:获得建议的最简单方法之一就是查看我们信任的人的偏好。 我们将我们的偏好与他们的偏好进行隐式比较,并且您共享的相似性越多,您就越有可能发现新颖的共享偏好。 但是,每个人都是独一无二的,我们的偏好存在于各种类别和领域中。 如果您可以利用很多人的偏好,而不仅是您信任的人的偏好,该怎么办? 总体而言,您不仅可以看到像您这样的人的模式,还可以看到反建议-远离您的事物,那些不喜欢您的人要注意的事情。 希望您还会看到在共享您自己独特体验的部分人群的共享偏好空间中的微妙划界。
在此基本前提下,使用了一组称为协同过滤的技术来提出建议。 简而言之,可以将这一前提归结为以下假设:那些具有相似的过去偏好的人将来会共享相同的偏好。 当然,这是从人类的角度出发,并且这种假设的典型推论是从事物的优先选择角度出发-同一个人喜欢的项目集在将来更可能被一起优先使用-以及 这是文献中通常所说的以用户为中心的协作过滤与以项目为中心的协作过滤的基础。
协作过滤是由 David Goldberg 在名为的论文中提出的。该论文使用协作过滤来编织信息挂毯,ACM 和 他提出了一个名为 Tapestry 的系统,该系统于 1992 年在 Xerox PARC 上进行了设计,目的是为文档添加有趣或不感兴趣的注释,并向寻求良好阅读的人们提供文档建议。
协作过滤算法搜索大量的偏好表达式组,以查找与某些输入偏好或某些偏好的相似性。 这些算法的输出是建议的排序列表,这些建议是所有可能的首选项的子集,因此,它被称为过滤。 协作来自使用许多其他人的偏好来为自己找到建议。 可以将其视为对偏好空间的搜索(针对蛮力技术),聚类问题(对相似的首选项目进行分组),或者甚至是其他一些预测模型。 为了在稀疏或大型数据集上优化或解决此问题,已经进行了许多算法尝试,本章将讨论其中的一些算法。
本章的目标如下:
- 了解如何从各种来源对偏好进行建模
- 学习如何使用距离量度来计算相似度
- 使用矩阵分解对星级进行推荐的建模
这两个不同的模型将使用网络上随时可用的数据集在 Python 中实现。 为了说明本章中的技术,我们将使用明尼苏达大学经常引用的 MovieLens 数据库,该数据库包含喜欢电影的观影者的星级。
请注意,本章被认为是高级章节,与以前的章节相比,完成本章的时间可能要多得多。
我们已经指出,像 Amazon 这样的公司会跟踪购买情况和页面浏览量以提出建议,Goodreads 和 Yelp 使用五星级评级和文字评论,而 Reddit 或 Stack Overflow 这样的网站都使用简单的上下投票。 您可以看到,偏好可以在数据中以不同的方式表示,从布尔标志,投票到评分。 但是,这些首选项是通过尝试在首选项表达式中找到相似性组来表达的,在这些表达式中,您正在利用协作过滤的核心假设。
更正式地说,我们了解到,鲍勃(Bob)和爱丽丝(Alice)这两个人共享对特定商品或小部件的偏好。 如果爱丽丝也对链轮等其他物品有偏爱,那么鲍勃也比随机地也有机会分享对链轮的偏爱。 我们相信,鲍勃和爱丽丝的口味相似性可以通过大量偏好来综合表达,并且通过利用团体的协作性质,我们可以过滤产品世界。
我们将在接下来的几个配方中对首选项表达式进行建模,包括以下内容:
- 了解数据
- 摄取电影评论数据
- 寻找收视率最高的电影
- 完善电影分级系统
偏好表达式是可证明的相对选择模型的实例。 也就是说,偏好表达是用于显示一个人的一组项目之间的主观排名的数据点。 更正式地说,我们应该说偏好表达不仅是相对的,而且是时间的-例如,偏好陈述还具有固定的时间相对性和项目相对性。
偏好表达是可证明的相对选择模型的一个实例。
可以认为我们可以在全球范围内主观和准确地表达自己的偏好(例如,将一部电影与其他所有电影进行比较),这很高兴,但事实上,我们的品味会随着时间而改变,而且我们只能 考虑一下我们如何对项目进行相对排名。 偏好模型必须考虑到这一点,并尝试减轻由偏好引起的偏差。 最常见的偏好表达模型类型通过使表达式在数值上变得模糊来简化排名问题,例如:
- 布尔表达式(是或否)
- 上下投票(如弃权,不喜欢)
- 加权信号(点击或操作数)
- 广泛的分类(明星,讨厌或喜爱)
想法是为个人用户创建偏好模型,为特定个人创建偏好表达集合的数值模型。 模型将各个首选项表达式构建到可以针对其进行计算的有用的特定于用户的上下文中。 可以对模型执行进一步的推理,以减轻基于时间的偏差或执行本体论推理或其他分类。
随着实体之间的关系变得越来越复杂,您可以通过将行为权重分配给每种类型的语义连接来表达它们的相对偏好。 然而,选择权重是困难的,并且需要研究来确定相对权重,这就是为什么优选模糊泛化的原因。 例如,下表显示了一些著名的排名首选项系统:
在本章的其余部分中,我们将仅考虑一个非常常见的偏好表达:星级等级(从 1 到 5)。
了解您的数据对于所有与数据相关的工作都是至关重要的。 在本食谱中,我们将获取并首先查看用于构建推荐引擎的数据。
为了准备本食谱以及本章的其余部分,请从明尼苏达大学的 GroupLens 网站下载 MovieLens 数据。 您可以在这个页面中找到数据。
在本章中,我们将使用较小的 MoveLens 100k 数据集(大小为 4.7 MB),以便轻松地将整个模型加载到内存中。
执行以下步骤,以更好地理解本章将要使用的数据:
- 从这个页面下载数据。 100K 数据集是您想要的数据集(
ml-100k.zip
):
- 将下载的数据解压缩到您选择的目录中。
- 我们主要关注的两个文件是
u.data
和u.item
,其中包含用户电影分级,而u.item
包含电影信息和详细信息。 要了解每个文件,在 Mac 和 Linux 的命令提示符下使用head
命令,在 Windows 的情况下使用more
命令:
head -n 5 u.item
请注意,如果您在运行 Microsoft Windows 操作系统且未使用虚拟机的计算机上工作(不建议使用),则您无权访问head
命令; 而是使用以下命令:more u.item 2 n
- 前面的命令为您提供以下输出:
1|Toy Story (1995)|01-Jan-1995||http://us.imdb.com/M/title- exact?Toy%20Story%20(1995)|0|0|0|1|1|1|0|0|0|0|0|0|0|0|0|0|0|0|02|GoldenEye (1995)|01-Jan-1995||http://us.imdb.com/M/title-
exact?GoldenEye%20(1995)|0|1|1|0|0|0|0|0|0|0|0|0|0|0|0|0|1|0|03|Four Rooms (1995)|01-Jan-1995||http://us.imdb.com/M/title- exact?Four%20Rooms%20(1995)|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1|0|04|Get Shorty (1995)|01-Jan-1995||http://us.imdb.com/M/title- exact?Get%20Shorty%20(1995)|0|1|0|0|0|1|0|0|1|0|0|0|0|0|0|0|0|0|05|Copycat (1995)|01-Jan-1995||http://us.imdb.com/M/title-
exact?Copycat%20(1995)|0|0|0|0|0|0|1|0|1|0|0|0|0|0|0|0|1|0|0
以下命令将产生给定的输出:
head -n 5 u.data
对于 Windows,可以使用以下命令:
more u.item 2 n196 242 3 881250949186 302 3 89171774222 377 1 878887116244 51 2 880606923166 346 1 886397596
我们将使用的两个主要文件如下:
u.data
:包含用户移动等级u.item
:包含电影信息和其他详细信息
两者都是字符分隔文件; 主文件u.data
由制表符分隔,u.item
由管道分隔。
对于u.data
,第一列是用户 ID,第二列是电影 ID,第三列是星级,最后一列是时间戳。 u.item
文件包含更多信息,包括 ID,标题,发布日期,甚至是指向 IMDb 的 URL。 有趣的是,此文件还具有一个布尔数组,指示每个电影的流派,包括(按顺序)动作,冒险,动画,儿童,喜剧,犯罪,纪录片,戏剧,幻想,黑色电影,恐怖片,音乐剧, 神秘,浪漫,科幻,惊悚,战争和西方。
适用于构建推荐引擎的免费的,网络规模的数据集非常少。 结果,电影镜头数据集是执行此任务的非常受欢迎的选择,但还有其他选择。 著名的 Netflix 奖数据集已被 Netflix 下拉。 但是,可以通过 Internet 存档从 Stack Exchange 网络(包括 Stack Overflow)中转储所有用户贡献的内容。 此外,还有一个跨书数据集,其中包含一百万个(一百万个不同的书籍中约四分之一)分级。
推荐引擎需要大量的培训数据才能做好工作,这就是为什么它们经常被降级到大数据项目。 但是,要构建推荐引擎,我们必须首先将所需的数据存储到内存中,并且由于数据的大小,必须以一种内存安全且有效的方式进行存储。 幸运的是,Python 拥有完成任务的所有工具,此食谱向您展示了如何进行。
您将需要下载适当的电影镜头数据集,如前面的配方中所述。 如果跳过了第 1 章和“准备数据科学环境”中的设置,则需要返回并确保正确安装了 NumPy。
以下步骤将指导您创建将数据集加载到内存中所需的功能:
- 打开您喜欢的 Python 编辑器或 IDE。 有很多代码,因此将其直接输入文本文件而不是 Read-Eval-Print Loop(REPL)应该更简单。
- 我们创建了一个导入电影评论的功能:
In [1]: import csv
...: import datetime
In [2]: def load_reviews(path, **kwargs):
...: """
...: Loads MovieLens reviews
...: """
...: options = {
...: 'fieldnames': ('userid', 'movieid', 'rating', 'timestamp'),
...: 'delimiter': '\t',
...: }
...: options.update(kwargs)
...:
...: parse_date = lambda r,k: datetime.fromtimestamp(float(r[k]))
...: parse_int = lambda r,k: int(r[k])
...:
...: with open(path, 'rb') as reviews:
...: reader = csv.DictReader(reviews, **options)
...: for row in reader:
...: row['movieid'] = parse_int(row, 'movieid')
...: row['userid'] = parse_int(row, 'userid')
...: row['rating'] = parse_int(row, 'rating')
...: row['timestamp'] = parse_date(row, 'timestamp')
...: yield row
- 我们创建一个辅助函数来帮助导入数据:
In [3]: import os
...: def relative_path(path):
...: """
...: Returns a path relative from this code file
...: """
...: dirname = os.path.dirname(os.path.realpath('__file__'))
...: path = os.path.join(dirname, path)
...: return os.path.normpath(path)
- 我们创建另一个函数来加载电影信息:
In [4]: def load_movies(path, **kwargs):
...:
...: options = {
...: 'fieldnames': ('movieid', 'title', 'release', 'video', 'url'),
...: 'delimiter': '|',...: 'restkey': 'genre',
...: }
...: options.update(kwargs)
...:
...: parse_int = lambda r,k: int(r[k])
...: parse_date = lambda r,k: datetime.strptime(r[k], '%d-%b-%Y') if r[k] else None
...:
...: with open(path, 'rb') as movies:
...: reader = csv.DictReader(movies, **options)
...: for row in reader:
...: row['movieid'] = parse_int(row, 'movieid')
...: row['release'] = parse_date(row, 'release')
...: row['video'] = parse_date(row, 'video')
...: yield row
- 最后,我们开始创建一个
MovieLens
类,该类将在以后的食谱中得到增强:
In [5]: from collections import defaultdict
In [6]: class MovieLens(object):
...: """
...: Data structure to build our recommender model on.
...: """
...:
...: def __init__(self, udata, uitem):
...: """
...: Instantiate with a path to u.data and u.item
...: """
...: self.udata = udata
...: self.uitem = uitem
...: self.movies = {}
...: self.reviews = defaultdict(dict)
...: self.load_dataset()
...:
...: def load_dataset(self):
...: """
...: Loads the two datasets into memory, indexed on the ID.
...: """
...: for movie in load_movies(self.uitem):
...: self.movies[movie['movieid']] = movie
...:
...: for review in load_reviews(self.udata):...: self.reviews[review['userid']][review['movieid']] = review
- 确保已将功能导入到 REPL 或 Jupyter 工作区中,然后键入以下内容,确保数据文件的路径适合您的系统:
In [7]: data = relative_path('../data/ml-100k/u.data')
...: item = relative_path('../data/ml-100k/u.item')
...: model = MovieLens(data, item)
我们用于两个数据加载功能(load_reviews
和load_movies
)的方法很简单,但是它照顾了从磁盘解析数据的细节。 我们创建了一个函数,该函数首先获取数据集的路径,然后获取所有可选关键字的路径。 我们知道我们需要与csv
模块进行交互的特定方式,因此我们创建了默认选项,将行的字段名称以及定界符\t
传递进来。 options.update(kwargs)
行表示我们将接受传递给此功能的所有用户。
然后,我们使用 Python 中的lambda
函数创建了内部解析函数。 这些简单的解析器将一行和一个键作为输入,并返回转换后的输入。 这是将lambda
用作内部可重复使用的代码块的示例,并且是 Python 中的常用技术。 最后,我们打开文件并使用我们的选项创建一个csv.DictReader
函数。 遍历阅读器中的行,我们分别解析我们想成为int
和datetime
的字段,然后产生该行。
请注意,由于我们不确定输入文件的实际大小,因此我们使用 Python 生成器以内存安全的方式执行此操作。 使用yield
而不是return
可确保 Python 在后台创建生成器,并且不会将整个数据集加载到内存中。
我们将使用这些方法中的每一种,通过使用该数据集的计算在不同时间加载数据集。 我们需要始终知道这些文件在哪里,这可能会很麻烦,尤其是在较大的代码库中。 在中还有更多... 部分,我们将讨论减轻该问题的 Python 专业提示。
最后,我们创建了一个数据结构,即MovieLens
类,通过该结构可以保存我们的审阅数据。 此结构采用udata
和uitem
路径,然后将电影和评论加载到两个 Python 字典中,分别由movieid
和userid
索引。 要实例化此对象,您将执行以下操作:
data = relative_path('../data/ml-100k/u.data')
item = relative_path('../data/ml-100k/u.item')
model = MovieLens(data, item)
请注意,上述命令假定您将数据保存在名为data
的文件夹中。 现在,我们可以将整个数据集加载到内存中,并在数据集中指定的各种 ID 上建立索引。
您是否注意到relative_path
功能的使用? 当使用诸如此类的夹具来构建模型时,数据通常包含在代码中。 当您在 Python 中指定路径时,例如data/ml-100k/u.data
,它将相对于您运行脚本的当前工作目录进行查找。 为了帮助减轻这种麻烦,您可以指定相对于代码本身的路径:
import os def relative_path(path): """ Returns a path relative from this code file """ dirname = os.path.dirname(os.path.realpath('__file__')) path = os.path.join(dirname, path) return os.path.normpath(path)
请记住,这会将整个数据结构保存在内存中。 对于 100k 数据集,这将需要 54.1 MB,对于现代计算机来说还算不错。 但是,我们还应该牢记,我们通常会使用远远超过 100,000 条评论的方式来建立推荐者。 这就是为什么我们以与数据库非常相似的方式配置数据结构的原因。 为了扩展系统,您将reviews
和movies
属性替换为数据库访问函数或属性,这将产生我们的方法所期望的数据类型。
如果您正在寻找一部好电影,通常会希望看到整体上最受欢迎或评分最高的电影。 最初,我们将采用天真的方法,通过平均每部电影的用户评论来计算电影的综合评分。 该技术还将演示如何访问MovieLens
类中的数据。
这些配方本质上是顺序的。 因此,在开始本章之前,您应该已经完成了本章中的先前食谱。
请按照以下步骤为数据集中的所有电影输出数字分数,并计算前 10 名列表:
- 使用新方法增强
MovieLens
类,以获取特定电影的所有评论:
In [8]: class MovieLens(object):
...:
...:
...: def reviews_for_movie(self, movieid):
...: """
...: Yields the reviews for a given movie
...: """
...: for review in self.reviews.values():
...: if movieid in review:
...: yield review[movieid]
...:
- 然后,添加其他方法来计算用户评论的前十部电影:
In [9]: import heapq
...: from operator import itemgetter
...: class MovieLens(object):
...:
...: def average_reviews(self):
...: """
...: Averages the star rating for all movies. Yields a tuple of movieid,
...: the average rating, and the number of reviews.
...: """
...: for movieid in self.movies:
...: reviews = list(r['rating'] for r in self.reviews_for_movie(movieid))
...: average = sum(reviews) / float(len(reviews))
...: yield (movieid, average, len(reviews))
...:
...: def top_rated(self, n=10):
...: """
...: Yields the n top rated movies
...: """
...: return heapq.nlargest(n, self.bayesian_average(), key=itemgetter(1))
...:
请注意,class MovieLens(object):
下方的...
表示我们将average_reviews
方法附加到现有的MovieLens
类上。
- 现在,让我们打印最受好评的结果:
In [10]: for mid, avg, num in model.top_rated(10):
...: title = model.movies[mid]['title']
...: print "[%0.3f average rating (%i reviews)] %s" % (avg, num,title)
- 在 REPL 中执行上述命令应产生以下输出:
Out [10]: [5.000 average rating (1 reviews)] Entertaining Angels: The Dorothy Day Story (1996)[5.000 average rating (2 reviews)] Santa with Muscles (1996)[5.000 average rating (1 reviews)] Great Day in Harlem, A (1994)[5.000 average rating (1 reviews)] They Made Me a Criminal (1939)[5.000 average rating (1 reviews)] Aiqing wansui (1994)[5.000 average rating (1 reviews)] Someone Else's America (1995)[5.000 average rating (2 reviews)] Saint of Fort Washington, The (1993) [5.000 average rating (3 reviews)] Prefontaine (1997)[5.000 average rating (3 reviews)] Star Kid (1997)[5.000 average rating (1 reviews)] Marlene Dietrich: Shadow and Light (1996)
添加到MovieLens
类的新reviews_for_movie()
方法遍历我们的审阅字典值(由userid
参数索引),检查movieid
值是否已由用户审阅,然后显示 那个评论字典。 下一个方法将需要此类功能。
使用average_review()
方法,我们创建了另一个生成器功能,该功能遍历我们所有的电影及其所有评论,并提供电影 ID,平均评分和评论数量。 top_rated
功能使用heapq
模块根据平均值快速对评论进行排序。
heapq
数据结构,也称为优先级队列算法,是具有有趣和有用属性的抽象数据结构的 Python 实现。 堆是构建的二叉树,因此每个父节点的值都小于或等于其任何子节点的值。 因此,最小的元素是树的根,可以在恒定时间内对其进行访问,这是非常理想的属性。 使用heapq
,Python 开发人员可以有效地在有序数据结构中插入新值,并返回排序后的值。
在这里,我们遇到了第一个问题,一些收视率最高的电影只有一个评论(反之,收视率最低的电影也是如此)。 您如何将卡萨布兰卡的平均评分为 4.457(243 条评论)与圣诞老人与肌肉的的平均评分为 5.000(2 条评论)? 我们确定那两位评论者确实喜欢圣诞老人和肌肉,但是卡萨布兰卡的高评价可能更有意义,因为更多的人喜欢它。 大多数具有星级的推荐者将仅输出平均评分以及评论者的数量,从而使用户可以确定其质量。 但是,作为数据科学家,我们可以在下一个食谱中做得更好。
heapq
文档可从这个页面获得。
我们不希望使用这样一种系统来构建推荐引擎,该系统认为可能带有肌肉的圣诞老人*,*通常比 Casablanca 更好。 因此,先前使用的天真的评分方法必须加以改进,并且是本食谱的重点。
确保首先完成本章中的先前食谱。
以下步骤实现并测试了一种新的电影评分算法:
- 让我们实现一个新的贝叶斯电影评分算法,如以下函数所示,将其添加到
MovieLens
类中:
In [11]: def bayesian_average(self, c=59, m=3):
...: """
...: Reports the Bayesian average with parameters c and m.
...: """
...: for movieid in self.movies:
...: reviews = list(r['rating'] for r in self.reviews_for_movie(movieid))
...: average = ((c * m) + sum(reviews)) / float(c + len(reviews))
...: yield (movieid, average, len(reviews))
- 接下来,我们将用以下命令中的版本替换
MovieLens
类中的top_rated
方法,该命令使用上一步中的新Bayesian_average
方法:
In [12]: def top_rated(self, n=10):
...: """
...: Yields the n top rated movies
...: """
...: return heapq.nlargest(n, self.bayesian_average(), key=itemgetter(1))
- 打印新的前十名列表对我们来说似乎有些熟悉,现在
Casablanca
的评级为4
:
[4.234 average rating (583 reviews)] Star Wars (1977)[4.224 average rating (298 reviews)] Schindler's List (1993)[4.196 average rating (283 reviews)] Shawshank Redemption, The (1994)[4.172 average rating (243 reviews)] Casablanca (1942)[4.135 average rating (267 reviews)] Usual Suspects, The (1995)[4.123 average rating (413 reviews)] Godfather, The (1972)[4.120 average rating (390 reviews)] Silence of the Lambs, The (1991)[4.098 average rating (420 reviews)] Raiders of the Lost Ark (1981)[4.082 average rating (209 reviews)] Rear Window (1954)[4.066 average rating (350 reviews)] Titanic (1997)
如上一个食谱所示,对电影评论进行平均,根本就行不通,因为某些电影没有足够的评分,无法与具有更高评分的电影进行有意义的比较。 我们真正想要的是让每位电影评论家给每部电影评分。 鉴于这是不可能的,我们可以得出一个估计,如果有无数的人给电影定级,电影将如何定级。 从一个数据点很难推断出这一点,因此我们应该说,如果有相同人数的人给它一个平均评分(例如,根据评论数对结果进行过滤),我们希望对电影的评分进行估算 。
可以使用在bayesian_average()
函数中实现的贝叶斯平均值来计算此估算值,以根据以下公式推断这些额定值:
在这里,m
是我们求星星平均值的先验,C
是与我们后验观测数量相等的置信度参数。
确定先验可能是一件复杂而神奇的艺术。 无需采用将 Dirichlet 分布拟合到我们的数据的复杂路径,我们可以使用五星级评级系统简单地选择 3 的m
优先级,这意味着我们的先验假设星级趋向于 在中值附近进行审查。 在选择C
时,您表示要离开先前的知识,需要进行多次评论; 我们可以通过查看每部电影的平均评论数来计算:
print float(sum(num for mid, avg, num in model.average_reviews())) /
len(model.movies)
这给我们提供了59.4
的平均值,我们将其用作函数定义中的默认值。
播放C
参数。 您应该发现,如果更改参数以使 C = 50,则前 10 个列表将巧妙地移动; 在这种情况下,迅达的清单和星球大战的排名会互换,失落奇兵的突袭者和后窗-注意 这两个被交换的电影都比前一个电影具有更多的评论,这意味着较高的C
参数平衡了其他电影的较少收视率。
- 请在这个页面了解 Yelp 如何应对这一挑战。
两种最可识别的协作过滤系统类型是基于用户的推荐者和基于项目的推荐者。 如果想像一下偏好空间是n
维特征空间,其中绘制了用户或商品,那么我们可以说相似的用户或商品在此偏好空间中倾向于彼此靠近。 因此,这种类型的协作过滤的替代名称是最近邻居推荐器。
在此过程中,至关重要的一步是提出一个相似性或距离度量标准,通过该度量标准,我们可以将批评者彼此之间或相互喜欢的项目进行比较。 然后,该度量用于将特定用户与所有其他用户进行成对比较,或者反之,将要与所有其他项目进行比较的项目进行比较。 然后使用规范化的比较来确定建议。 尽管计算空间可能会变得非常大,但是距离度量本身并不难计算,在本食谱中,我们将探索一些并实现我们的第一个推荐系统。
在本食谱中,我们将测量用户之间的距离; 在此之后的食谱中,我们将看看另一个相似距离指示器。
我们将从建模偏好表达式秒和的MovieLens
类继续构建。 如果您没有机会复习本节,请准备该课程的代码。 重要的是,我们将要访问从磁盘上的 CSV 文件加载的数据结构MovieLens.movies
和MovieLens.reviews
。
以下步骤集提供了有关如何计算用户之间的欧几里得距离的说明:
- 使用新方法
shared_preferences
增强MovieLens
类,以提取已被A
和B
两位评论家评价的电影:
In [13]: class MovieLens(object):
...:
...: def shared_preferences(self, criticA, criticB):
...: """
...: Returns the intersection of ratings for two critics, A and B.
...: """
...: if criticA not in self.reviews:
...: raise KeyError("Couldn't find critic '%s' in data" % criticA)
...: if criticB not in self.reviews:
...: raise KeyError("Couldn't find critic '%s' in data" % criticB)
...:
...: moviesA = set(self.reviews[criticA].keys())
...: moviesB = set(self.reviews[criticB].keys())
...: shared = moviesA & moviesB # Intersection operator
...:
...: # Create a reviews dictionary to return
...: reviews = {}
...: for movieid in shared:
...: reviews[movieid] = (
...: self.reviews[criticA][movieid]['rating'],
...: self.reviews[criticB][movieid]['rating'],
...: )
...: return reviews
...:
- 然后,实现一个函数,使用两个评论员的共享电影首选项作为计算的向量来计算两个评论员之间的欧几里得距离。 此方法也将是
MovieLens
类的一部分:
In [14]: from math import sqrt
...:
...: def euclidean_distance(self, criticA, criticB, prefs='users'):
...: """
...: Reports the Euclidean distance of two critics, A and B by
...: performing a J-dimensional Euclidean calculation of each of their
...: preference vectors for the intersection of books the critics have
...: rated.
...: """
...:
...: # Get the intersection of the rated titles in the data.
...:
...: if prefs == 'users':
...: preferences = self.shared_preferences(criticA, criticB)
...: elif prefs == 'movies':
...: preferences = self.shared_critics(criticA, criticB)
...: else:
...: raise Exception("No preferences of type '%s'." % prefs)
...:
...: # If they have no rankings in common, return 0.
...: if len(preferences) == 0: return 0
...:
...: # Sum the squares of the differences
...: sum_of_squares = sum([pow(a-b, 2) for a, b in preferences.values()])
...:
...: # Return the inverse of the distance to give a higher score to
...: # folks who are more similar (e.g. less distance) add 1 to prevent
...: # division by zero errors and normalize ranks in [0, 1]
...: return 1 / (1 + sqrt(sum_of_squares))
- 实施上述代码后,请在 REPL 中对其进行测试:
>>> data = relative_path('data/ml-100k/u.data')>>> item = relative_path('data/ml-100k/u.item') >>> model = MovieLens(data, item)>>> print model.euclidean_distance(232, 532)0.1023021629920016
MovieLens
类的新shared_preferences()
方法确定两个用户的共享首选项空间。 至关重要的是,我们只能根据他们都对用户(criticA
和criticB
输入参数)进行的评估来进行比较。 此函数使用 Python 集来确定A
和B
均已查看的电影列表(电影A
已评级和电影B
已评级)。 然后,该函数对该集合进行迭代,返回一个字典,该字典的键是电影 ID,并且值是收视率元组,例如,对于两个用户都已收视的每部电影,其收视率是[ratingA
和ratingB
。 现在,我们可以使用该数据集来计算相似性得分,这是由第二个函数完成的。
euclidean_distance()
函数将两个注释符作为输入A
和B
,并计算偏好空间中用户之间的距离。 在这里,我们选择实现欧几里得距离度量标准(对于记住毕达哥拉斯定理的人来说,二维变化是众所周知的),但是我们也可以实现其他度量标准。 此函数会将实数从0
返回到1
,其中0
的批评者相似度较小(相距较远),而1
的批评者相似度较高(相近)。
曼哈顿距离是另一种非常流行的度量标准,也是一个非常简单易懂的度量标准。 它可以简单地将每个向量的元素之间的成对差的绝对值求和。 或者,在代码中,可以按以下方式执行:
manhattan = sum([abs(a-b) for a, b in preferences.values()])
此度量标准也称为城市街区距离,因为从概念上讲,这好像是在计算要在城市的两点之间行走的北/南和东/西街区的数量。 在针对本配方实现它之前,您还希望以某种方式反转和标准化该值,以返回 [0,1] 范围内的值。
在上一个食谱中,我们使用了许多可能的距离度量中的一个来捕获用户的电影评论之间的距离。 即使有五个或五百万个其他用户,两个特定用户之间的距离也不会更改。
在此配方中,我们将计算偏好空间中用户之间的相关性。 像距离度量一样,有许多相关度量。 其中最受欢迎的是 Pearson 或 Spearman 相关或余弦距离。 与距离度量不同,相关性将根据用户和电影的数量而变化。
我们将继续继续先前食谱的工作,因此请确保您理解每一个食谱。
以下函数为两个评论者criticA
和criticB
实现pearson_correlation
函数的计算,并将其添加到MovieLens
类中:
In [15]: def pearson_correlation(self, criticA, criticB, prefs='users'):
...: """
...: Returns the Pearson Correlation of two critics, A and B by
...: performing the PPMC calculation on the scatter plot of (a, b)
...: ratings on the shared set of critiqued titles.
...: """
...:
...: # Get the set of mutually rated items
...: if prefs == 'users':
...: preferences = self.shared_preferences(criticA, criticB)
...: elif prefs == 'movies':
...: preferences = self.shared_critics(criticA, criticB)
...: else:
...: raise Exception("No preferences of type '%s'." % prefs)
...:
...: # Store the length to save traversals of the len computation.
...: # If they have no rankings in common, return 0.
...: length = len(preferences)
...: if length == 0: return 0
...:
...: # Loop through the preferences of each critic once and compute the
...: # various summations that are required for our final calculation.
...: sumA = sumB = sumSquareA = sumSquareB = sumProducts = 0
...: for a, b in preferences.values():
...: sumA += a
...: sumB += b
...: sumSquareA += pow(a, 2)
...: sumSquareB += pow(b, 2)
...: sumProducts += a*b
...:
...: # Calculate Pearson Score
...: numerator = (sumProducts*length) - (sumA*sumB)
...: denominator = sqrt(((sumSquareA*length) - pow(sumA, 2))
...: * ((sumSquareB*length) - pow(sumB, 2)))
...:
...: # Prevent division by zero.
...: if denominator == 0: return 0
...:
...: return abs(numerator / denominator)
...:
皮尔逊相关系数计算乘积矩,乘积矩是均值调整后的随机变量乘积的平均值,并且定义为两个变量(在我们的情况下为a
和b
)的协方差除以标准差的乘积 a
的标准偏差和b
的标准偏差。 作为公式,它看起来像下面的样子:
对于我们拥有的有限样本,在上一个函数中实现的详细公式如下:
思考皮尔逊相关性的另一种方法是度量两个变量之间的线性相关性。 它返回-1
到1
的得分,其中接近-1
的负得分表示更强的负相关性,接近 1 的正得分。 表示更强的正相关。 分数0
表示两个变量不相关。
为了使我们能够进行比较,我们想在[0,1]的空间中归一化我们的相似性指标,以便0
意味着不那么相似,1
意味着更不相似,因此我们 返回绝对值:
>>> print model.pearson_correlation(232, 532)0.06025793538385047
我们探索了两个距离度量:欧几里得距离和皮尔森相关性。 还有更多内容,包括 Spearman 相关性,Tantimoto 得分,Jaccard 距离,余弦相似度和 Manhattan 距离,仅举几例。 为推荐器的数据集选择正确的距离度量以及所使用的偏好表达的类型,对于确保这种推荐器成功至关重要。 读者可以根据自己的兴趣和特定的数据集来进一步探索该空间。
现在,我们有两种不同的方法来计算用户之间的相似距离,我们可以确定特定用户的最佳评论者,并查看它们与个人偏好的相似程度。
在解决此问题之前,请确保已完成之前的食谱。
为MovieLens
类实现一个新方法similar_critics()
,该方法为用户找到最佳匹配项:
In [16]: import heapq
...:
...: def similar_critics(self, user, metric='euclidean', n=None):
...: """
...: Finds and ranks similar critics for the user according to the
...: specified distance metric. Returns the top n similar critics.
...: """
...:
...: # Metric jump table
...: metrics = {
...: 'euclidean': self.euclidean_distance,
...: 'pearson': self.pearson_correlation,
...: }
...:
...: distance = metrics.get(metric, None)
...:
...: # Handle problems that might occur
...: if user not in self.reviews:
...: raise KeyError("Unknown user, '%s'." % user)
...: if not distance or not callable(distance):
...: raise KeyError("Unknown or unprogrammed distance metric '%s'." % metric)
...:
...: # Compute user to critic similarities for all critics
...: critics = {}
...: for critic in self.reviews:
...: # Don't compare against yourself!
...: if critic == user:
...: continue
...:
...: critics[critic] = distance(user, critic)
...:
...: if n:
...: return heapq.nlargest(n, critics.items(), key=itemgetter(1))
...: return critics
MovieLens
类中添加的similar_critics
方法是此食谱的核心。 它以目标用户和两个可选参数作为参数:要使用的指标(默认为euclidean
)和要返回的结果数(默认为None
)。 如您所见,这种灵活的方法使用跳转表来确定要使用的算法(您可以传入euclidean
或pearson
以选择距离度量)。 将每个其他评论者与当前用户进行比较(用户与自己的比较除外)。 然后使用灵活的heapq
模块对结果进行排序,并返回顶部的n
结果。
要测试我们的实现,请打印出两个相似距离的运行结果:
>>> for item in model.similar_critics(232, 'euclidean', n=10): print "%4i: %0.3f" % item 688: 1.000 914: 1.00047: 0.50078: 0.500170: 0.500335: 0.500341: 0.500101: 0.414155: 0.414309: 0.414
>>> for item in model.similar_critics(232, 'pearson', n=10): print "%4i: %0.3f" % item33: 1.00036: 1.000155: 1.000260: 1.000289: 1.000302: 1.000309: 1.000317: 1.000511: 1.000769: 1.000
这些分数显然有很大的不同,而且皮尔逊似乎认为比欧几里得距离度量标准拥有更多相似用户。 欧几里得距离度量标准倾向于偏爱那些评分相同的项目较少的用户。 皮尔逊相关性倾向于更多的线性拟合分数,因此,皮尔逊校正了等级膨胀率,两名评论家可能对电影的评分非常相似,但一个用户对它们的评价始终如一。
如果算出每个评论家有多少个共享排名,您会发现数据非常稀疏。 这是前面的数据,其中附加了排名数量:
Euclidean scores: 688: 1.000 (1 shared rankings) 914: 1.000 (2 shared rankings) 47: 0.500 (5 shared rankings) 78: 0.500 (3 shared rankings) 170: 0.500 (1 shared rankings)Pearson scores: 33: 1.000 (2 shared rankings) 36: 1.000 (3 shared rankings) 155: 1.000 (2 shared rankings) 260: 1.000 (3 shared rankings) 289: 1.000 (3 shared rankings)
因此,仅找到相似的评论家并使用他们的评分来预测我们用户的得分是不够的; 相反,无论相似度如何,我们都必须汇总所有评论家的得分,并预测未评级电影的评级。
要预测我们如何评价特定电影,我们可以计算评论家的加权平均数,这些评论家也对与用户相同的电影进行了评级。 如果评论者未对电影评分,则权重将是评论者与用户的相似性,那么他们的相似性将不会对电影的整体排名有所贡献。
确保您已经完成了这一大篇累积的章节中的先前食谱。
以下步骤将引导您预测用户的电影收视率:
- 首先,将
predict_ranking
函数添加到MovieLens
类中,以预测用户可能会给具有类似评论家的特定电影的排名:
In [17]: def predict_ranking(self, user, movie, metric='euclidean', critics=None):
...: """
...: Predicts the ranking a user might give a movie according to the
...: weighted average of the critics that are similar to the that user.
...: """
...:
...: critics = critics or self.similar_critics(user, metric=metric)
...: total = 0.0
...: simsum = 0.0
...:
...: for critic, similarity in critics.items():
...: if movie in self.reviews[critic]:
...: total += similarity * self.reviews[critic][movie]['rating']
...: simsum += similarity
...:
...: if simsum == 0.0: return 0.0
...: return total / simsum
- 接下来,将
predict_all_rankings
方法添加到MovieLens
类中:
In [18]: def predict_all_rankings(self, user, metric='euclidean', n=None):
...: """
...: Predicts all rankings for all movies, if n is specified returns
...: the top n movies and their predicted ranking.
...: """
...: critics = self.similar_critics(user, metric=metric)
...: movies = {
...: movie: self.predict_ranking(user, movie, metric, critics)
...: for movie in self.movies
...: }
...:
...: if n:
...: return heapq.nlargest(n, movies.items(), key=itemgetter(1))
...: return movies
predict_ranking
方法将用户和电影以及指定距离度量的字符串一起使用,并为该特定用户返回该电影的预测收视率。 第四个参数critics
旨在对predict_all_rankings
方法进行优化,我们将在稍后讨论。 该预测会收集与用户相似的所有评论者,并计算评论者的加权总评分,并根据实际对相关电影进行评分的人进行过滤。 权重只是它们与用户的相似性,由距离量度计算得出。 然后,通过相似度的总和对总和进行归一化,以将等级移回 1 到 5 星的空间:
>>> print model.predict_ranking(422, 50, 'euclidean') 4.35413151722>>> print model.predict_ranking(422, 50, 'pearson') 4.3566797826
在这里,我们可以看到用户422
对星球大战(在我们的 MovieLens 数据集中的 ID 50
)的预测。 欧几里得和皮尔逊的计算非常接近(不一定是期望值),但是预测也非常接近用户的实际评分4
。
predict_all_rankings
方法根据传入的指标为特定用户计算所有电影的排名预测。 可以选择使用n
值来返回前n
个最佳匹配项。 此功能仅执行一次,然后将发现的评论者传递给predict_ranking
函数,以优化性能,从而优化了类似评论者的查找。 但是,此方法必须在数据集中的每个电影上运行:
>>> for mid, rating in model.predict_all_rankings(578, 'pearson', 10): ... print "%0.3f: %s" % (rating, model.movies[mid]['title'])5.000: Prefontaine (1997)5.000: Santa with Muscles (1996)5.000: Marlene Dietrich: Shadow and Light (1996) 5.000: Star Kid (1997)5.000: Aiqing wansui (1994)5.000: Someone Else's America (1995)5.000: Great Day in Harlem, A (1994)5.000: Saint of Fort Washington, The (1993)4.954: Anna (1996)4.817: Innocents, The (1961)
如您所见,我们现在已经计算出推荐人认为该特定用户的热门电影的内容,以及我们认为该用户将对电影进行评分的内容! 平均电影收视率排名前 10 位的清单在这里起着巨大的作用,可能的改进可能是除了相似权重之外还使用贝叶斯平均法,但这留给读者来实现。
到目前为止,我们已经将用户与其他用户进行了比较,以便做出我们的预测。 但是,相似性空间可以通过两种方式进行划分。 以用户为中心的协作过滤可在偏好空间中绘制用户的图,并发现用户之间的相似程度。 然后,将这些相似性用于预测排名,使用户与相似的评论家保持一致。 以项目为中心的协作过滤则相反。 它在首选项空间中将各个项目绘制在一起,并根据一组项目与另一组的相似程度提出建议。
基于项目的协作过滤是一项常见的优化,因为项目的相似性变化缓慢。 一旦收集到足够的数据,评论者添加评论并不一定会改变以下事实:玩具总动员与贝贝比 The Terminator 更相似,并且用户更喜欢 玩具总动员可能更喜欢前者。 因此,您只需在单个脱机流程中计算一次项目相似度,然后将其用作建议的静态映射,即可半定期更新结果。
此食谱将引导您完成逐项协作筛选。
此食谱要求完成本章中的先前食谱。
构造以下函数以执行逐项协作过滤:
In [19]: def shared_critics(self, movieA, movieB):
...: """
...: Returns the intersection of critics for two items, A and B
...: """
...:
...: if movieA not in self.movies:
...: raise KeyError("Couldn't find movie '%s' in data" % movieA)
...: if movieB not in self.movies:
...: raise KeyError("Couldn't find movie '%s' in data" % movieB)
...:
...: criticsA = set(critic for critic in self.reviews if movieA in self.reviews[critic])
...: criticsB = set(critic for critic in self.reviews if movieB in self.reviews[critic])
...: shared = criticsA & criticsB # Intersection operator
...:
...: # Create the reviews dictionary to return
...: reviews = {}
...: for critic in shared:
...: reviews[critic] = (
...: self.reviews[critic][movieA]['rating'],
...: self.reviews[critic][movieB]['rating'],
...: )
...: return reviews
In [20]: def similar_items(self, movie, metric='euclidean', n=None):
...: # Metric jump table
...: metrics = {
...: 'euclidean': self.euclidean_distance,
...: 'pearson': self.pearson_correlation,
...: }
...:
...: distance = metrics.get(metric, None)
...:
...: # Handle problems that might occur
...: if movie not in self.reviews:
...: raise KeyError("Unknown movie, '%s'." % movie)
...: if not distance or not callable(distance):
...: raise KeyError("Unknown or unprogrammed distance metric '%s'." % metric)
...:
...: items = {}
...: for item in self.movies:
...: if item == movie:
...: continue
...:
...: items[item] = distance(item, movie, prefs='movies')
...:
...: if n:
...: return heapq.nlargest(n, items.items(), key=itemgetter(1))
...: return items
...:
为了执行逐项协作过滤,可以使用相同的距离度量,但是必须对其进行更新以使用来自shared_critics
而不是shared_preferences
的首选项(例如,项目相似性与用户相似性)。 更新函数以接受prefs
参数,该参数确定要使用的首选项,但是我将其留给读者,因为它只有两行代码(请注意,答案包含在sim.py
源文件中) 在包含第 7 章,“使用社交图谱(Python)”的代码的目录中)。
如果打印出特定电影的类似项目列表,则可以看到一些有趣的结果。 例如,查看 ID 为631
的 The Crying Game (1992)的相似性结果:
for movie, similarity in model.similar_items(631, 'pearson').items(): print "%0.3f: %s" % (similarity, model.movies[movie]['title']) 0.127: Toy Story (1995) 0.209: GoldenEye (1995) 0.069: Four Rooms (1995) 0.039: Get Shorty (1995) 0.340: Copycat (1995) 0.225: Shanghai Triad (Yao a yao yao dao waipo qiao) (1995) 0.232: Twelve Monkeys (1995) ...
这部犯罪惊悚片与儿童电影《玩具总动员》的玩具总动员不太相似,但与另一部犯罪惊悚片的 Copycat 更相似。 当然,对许多电影进行评级的评论家会使结果歪曲,因此需要更多的电影评论才能使结果正常化。
假定项目相似性分数是定期运行的,但是不需要实时计算它们。 给定一组计算的项目相似性,计算建议如下:
In [21]: def predict_ranking(self, user, movie, metric='euclidean', critics=None):
...: """
...: Predicts the ranking a user might give a movie according to the
...: weighted average of the critics that are similar to the that user.
...: """
...:
...: critics = critics or self.similar_critics(user, metric=metric)
...: total = 0.0
...: simsum = 0.0
...:
...: for critic, similarity in critics.items():
...: if movie in self.reviews[critic]:
...: total += similarity * self.reviews[critic][movie]['rating']
...: simsum += similarity
...:
...: if simsum == 0.0: return 0.0
...: return total / simsum
该方法仅使用倒置的项间相似度评分,而不是用户间相似度评分。 由于类似项目可以离线计算,因此通过self.similar_items
方法对电影的查找应该是数据库查找,而不是实时计算:
>>> print model.predict_ranking(232, 52, 'pearson') 3.980443976
然后,您可以按照与用户到用户的建议类似的方式计算所有可能建议的排名列表。
协同过滤的基本横向最近邻相似性评分的一般改进是矩阵分解方法,该方法也称为奇异值分解(SVD)。 矩阵分解方法试图通过发现潜在特征来解释评级,而这些潜在特征是分析师难以识别的。 例如,此技术可以在电影数据集中显示可能的功能,例如动作量,家庭友善或微调的类型发现。
这些功能特别有趣的是,它们是连续的而不是离散的值,并且可以代表个人在连续体上的偏好。 从这个意义上讲,该模型可以探索特征的阴影,例如电影评论数据集中的评论家,例如在欧洲国家/地区具有强烈女性主导的动作轻弹。 詹姆斯·邦德(James Bond)的电影可能只是那种电影的阴影,尽管它只是在欧洲国家和动作类型框中打勾。 根据评论者对电影的评价程度,女性詹姆斯·邦德的实力将决定他们对这部电影的喜欢程度。
同样,非常有用的是,矩阵分解模型在稀疏数据(即很少有推荐和电影对的数据)上表现良好。 评论数据特别稀疏,因为并非每个人都对同一部电影进行评级,并且有大量可用的电影。 SVD 也可以并行执行,因此对于更大的数据集而言,它是一个不错的选择。
在本章的其余部分中,我们将建立一个非负矩阵分解模型,以改善我们的推荐引擎:
- 将整个数据集加载到内存中
- 将基于 SVD 的模型转储到磁盘
- 训练基于 SVD 的模型
- 测试基于 SVD 的模型
矩阵分解或 SVD 通过找到两个矩阵来工作,这样当您获取它们的点积(也称为内积或标量积)时,您将获得原始矩阵的近似值。 我们已将训练矩阵表示为电影的用户的稀疏 N x M 矩阵,其中该值是 5 星评级(如果存在),否则为空白或0
。 通过用我们拥有的值对模型进行因子分解,然后取因式分解产生的两个矩阵的点积,我们希望在原始矩阵中填充空白点,从而预测用户如何评价电影的质量。 那一列。
直觉是应该有一些潜在特征来确定用户对项目的评分方式,而这些潜在特征是通过其先前评分的语义来表达的。 如果我们可以发现潜在功能,就可以预测新的评分。 此外,功能应该少于用户和电影(否则,每个电影或用户都是唯一的功能)。 这就是为什么我们在提取点乘积之前,将特征矩阵按某些特征长度进行组合。
在数学上,此任务表示如下。 如果我们有一组 U 个用户和 M 个电影,则让R
的大小为 | U |。 x | M | 是包含用户评分的矩阵。 假设我们具有K
潜在特征,请找到两个矩阵P
和Q
,其中P
为 | U |。 x K 和Q
是 | M | x K 使得P
和Q
的点积转置为R
。P
,因此表示用户与功能之间的关联强度,Q
表示电影与功能之间的关联:
有几种方法可以进行因式分解,但是我们所做的选择是执行梯度下降。 梯度下降可初始化两个随机P
和Q
矩阵,计算它们的点积,然后通过沿误差函数的斜率(梯度)向下移动来将误差与原始矩阵相比最小化 。 这样,算法希望找到误差在可接受阈值内的局部最小值。
我们的函数将误差计算为预测值和实际值之间的平方差:
为了使误差最小,我们通过沿当前误差斜率的斜率下降来修改值 p [ik] 和 q [kj] ,相对于p
产量:
然后,我们根据以下公式对变量q
产生的误差方程进行微分:
然后,我们可以得出学习规则,该规则以恒定的学习率α更新P
和Q
中的值。 该学习率α不应太大,因为它确定了我们迈向最小值的步长,并且有可能越过误差曲线的另一侧。 它也不应太小; 否则,将需要永远收敛:
我们继续更新P
和Q
矩阵,将误差最小化,直到误差平方和低于某个阈值,代码中的0.001
或执行了 最大迭代次数。
矩阵分解已成为推荐系统的一项重要技术,尤其是那些利用 Likert 标度样的偏好表达式(尤其是星级)的系统。 Netflix 奖挑战赛向我们展示了矩阵分解方法对于收视率预测任务的执行具有很高的准确性。 此外,矩阵分解是模型参数空间的紧凑,内存高效的表示形式,可以并行训练,可以支持多个特征向量,并且可以通过置信度进行改进。 通常,它们用于解决稀疏评论的冷启动问题,并且在具有更复杂混合动力的合奏中建议同时计算基于内容的推荐物。
- Wikipedia 对点积的概述可从这个页面获得。
建立非负因式分解模型的第一步是将整个数据集加载到内存中。 对于此任务,我们将充分利用 NumPy。
为了完成此食谱,您必须从明尼苏达大学的 GroupLens 页面下载 MovieLens 数据库并解压缩 将其放在您的代码所在的工作目录中。 我们还将在此代码中大量使用 NumPy,因此请确保已下载并准备好此数值分析包。 此外,我们将使用先前食谱中的load_reviews
功能。 如果您没有机会查看相应的部分,请准备该功能的代码。
要构建矩阵分解模型,我们需要为预测变量创建一个包装器,以将整个数据集加载到内存中。 我们将执行以下步骤:
- 如图所示,我们创建以下
Recommender
类。 请注意,此类取决于先前创建和讨论的load_reviews
函数:
In [22]: import numpy as np
...: import csv
...:
...: class Recommender(object):
...:
...: def __init__(self, udata):
...: self.udata = udata
...: self.users = None
...: self.movies = None
...: self.reviews = None
...: self.load_dataset()
...:
...: def load_dataset(self):
...: """
...: Loads an index of users and movies as a heap and a reviews table
...: as a N x M array where N is the number of users and M is the number
...: of movies. Note that order matters so that we can look up values
...: outside of the matrix!
...: """
...: self.users = set([])
...: self.movies = set([])
...: for review in load_reviews(self.udata):
...: self.users.add(review['userid'])
...: self.movies.add(review['movieid'])
...:
...: self.users = sorted(self.users)
...: self.movies = sorted(self.movies)
...:
...: self.reviews = np.zeros(shape=(len(self.users), len(self.movies)))
...: for review in load_reviews(self.udata):
...: uid = self.users.index(review['userid'])
...: mid = self.movies.index(review['movieid'])
...: self.reviews[uid, mid] = review['rating']
- 定义好这个后,我们可以通过键入以下命令来实例化模型:
data_path = '../data/ml-100k/u.data'model = Recommender(data_path)
让我们逐行浏览此代码。 推荐程序的实例化需要u.data
文件的路径; 为我们的用户,电影和评论列表创建所有者; 然后加载数据集。 我们需要将整个数据集保存在内存中,其原因将在以后看到。
执行矩阵分解的基本数据结构是 N x M 矩阵,其中 N 是用户数,M 是电影数。 为此,我们首先将所有电影和用户加载到有序列表中,以便我们可以通过 ID 来查找用户或电影的索引。 在MovieLens
的情况下,所有 ID 与1
是连续的; 但是,并非总是如此。 拥有索引查找表是一种很好的做法。 否则,您将无法从我们的计算中获取建议!
有了索引查找列表后,我们将创建一个 NumPy 数组,该数组的全零是用户列表的长度乘以电影列表的长度。 请记住,行是用户,列是电影! 然后,我们第二次浏览收视率数据,然后将收视率的值添加到矩阵的uid
和mid
索引位置。 请注意,如果用户尚未给电影评分,则他们的评分为0
。 这个很重要! 通过输入model.reviews
打印出阵列,您应该看到类似以下的内容:
[[ 5\. 3\. 4\. ..., 0\. 0\. 0.][ 4\. 0\. 0\. ..., 0\. 0\. 0.][ 0\. 0\. 0\. ..., 0\. 0\. 0.]..., [ 5\. 0\. 0\. ..., 0\. 0\. 0.][ 0\. 0\. 0\. ..., 0\. 0\. 0.]
[ 0\. 5\. 0\. ..., 0\. 0\. 0.]]
通过向Recommender
类添加以下两种方法,让我们了解数据集的稀疏程度:
In [23]: def sparsity(self):
...: """
...: Report the percent of elements that are zero in the array
...: """
...: return 1 - self.density()
...:
In [24]: def density(self):
...: """
...: Return the percent of elements that are nonzero in the array
...: """
...: nonzero = float(np.count_nonzero(self.reviews))
...: return nonzero / self.reviews.size
将这些方法添加到Recommender
类中将有助于我们评估推荐者,也将有助于我们将来确定推荐者。
打印结果:
print "%0.3f%% sparse" % model.sparsity()print "%0.3f%% dense" % model.density()
您应该看到 MovieLens 100k 数据集的稀疏率为 0.937%,密度为 0.063%。
记住评论数据集的大小非常重要。 稀疏性在大多数推荐系统中很常见,这意味着我们也许可以使用稀疏矩阵算法和优化。 另外,当我们开始保存模型时,这将有助于我们从磁盘上的序列化文件加载模型时识别模型。
在构建模型(这将花费很长时间进行训练)之前,我们应该创建一种机制以供我们将模型加载和转储到磁盘上。 如果我们有保存因子矩阵矩阵参数化的方法,那么我们可以重复使用我们的模型而不必每次都要使用它时就进行训练,因为这需要花费数小时的训练时间,所以这是非常重要的! 幸运的是,Python 有一个用于对 Python 对象进行序列化和反序列化的内置工具-pickle
模块。
如下更新Recommender
类:
In [26]: import pickle
...: class Recommender(object):
...: @classmethod
...: def load(klass, pickle_path):
...: """
...: Instantiates the class by deserializing the pickle.
...: Note that the object returned may not be an exact match
...: to the code in this class (if it was saved
...: before updates).
...: """
...: with open(pickle_path, 'rb') as pkl:
...: return pickle.load(pkl)
...:
...: def __init__(self, udata, description=None):
...: self.udata = udata
...: self.users = None
...: self.movies = None
...: self.reviews = None
...:
...: # Descriptive properties
...: self.build_start = None
...: self.build_finish = None
...: self.description = None
...:
...: # Model properties
...: self.model = None
...: self.features = 2
...: self.steps = 5000
...: self.alpha = 0.0002
...: self.beta = 0.02
...: self.load_dataset()
...:
...: def dump(self, pickle_path):
...: """
...: Dump the object into a serialized file using the pickle module.
...: This will allow us to quickly reload our model in the future.
...: """
...: with open(pickle_path, 'wb') as pkl:
...: pickle.dump(self, pkl)
@classmethod
功能是 Python 中的装饰器,用于声明类方法而不是实例方法。 传入的第一个参数是类型而不是实例(我们通常将其称为self
)。 load
类方法采用磁盘上包含串行化pickle
对象的文件的路径,然后使用pickle
模块加载该文件。 请注意,在您运行代码时,返回的类可能与Recommender
类不完全匹配,这是因为pickle
模块保存的类(包括方法和属性)与转储时的完全相同。 它。
说到转储,dump
方法提供了相反的功能,允许您将方法,属性和数据序列化到磁盘上,以便将来再次加载。 为了帮助我们识别要从磁盘转储和加载的对象,我们还为__init__
函数添加了一些描述性属性,包括描述,一些构建参数和一些时间戳。
现在,我们准备编写我们的函数,这些函数可以将我们的训练数据集纳入考虑范围并构建推荐模型。 您可以在此食谱中看到所需的功能。
我们构造以下函数来训练我们的模型。 请注意,这些功能不是Recommender
类的一部分:
In [27]: def initialize(R, K):
...: """
...: Returns initial matrices for an N X M matrix, R and K features.
...:
...: :param R: the matrix to be factorized
...: :param K: the number of latent features
...:
...: :returns: P, Q initial matrices of N x K and M x K sizes
...: """
...: N, M = R.shape
...: P = np.random.rand(N,K)
...: Q = np.random.rand(M,K)
...:
...: return P, Q
In [28]: def factor(R, P=None, Q=None, K=2, steps=5000, alpha=0.0002, beta=0.02):
...: """
...: Performs matrix factorization on R with given parameters.
...:
...: :param R: A matrix to be factorized, dimension N x M
...: :param P: an initial matrix of dimension N x K
...: :param Q: an initial matrix of dimension M x K
...: :param K: the number of latent features
...: :param steps: the maximum number of iterations to optimize in
...: :param alpha: the learning rate for gradient descent
...: :param beta: the regularization parameter
...:
...: :returns: final matrices P and Q
...: """
...:
...: if not P or not Q:
...: P, Q = initialize(R, K)
...: Q = Q.T
...:
...: rows, cols = R.shape
...: for step in xrange(steps):
...: for i in xrange(rows):
...: for j in xrange(cols):
...: if R[i,j] > 0:
...: eij = R[i,j] - np.dot(P[i,:], Q[:,j])
...: for k in xrange(K):
...: P[i,k] = P[i,k] + alpha * (2 * eij * Q[k,j] - beta * P[i,k])
...: Q[k,j] = Q[k,j] + alpha * (2 * eij * P[i,k] - beta * Q[k,j])
...:
...: e = 0
...: for i in xrange(rows):
...: for j in xrange(cols):
...: if R[i,j] > 0:
...: e = e + pow(R[i,j] - np.dot(P[i,:], Q[:,j]), 2)
...: for k in xrange(K):
...: e = e + (beta/2) * (pow(P[i,k], 2) + pow(Q[k,j], 2))
...: if e < 0.001:
...: break
...:
...: return P, Q.T
我们讨论了先前食谱建立非负矩阵分解模型的理论和数学方法,下面让我们讨论一下代码。 initialize
函数创建两个矩阵P
和Q
,它们的大小与评论矩阵和特征数量有关,即N
x K
和M
x K
,其中 N
是用户数量,M
是电影数量。 它们的值被初始化为0.0
和1.0
之间的随机数。 factor
函数使用梯度下降来计算P
和Q
,以使P
和Q
的点积在小于0.001
或5000
步长的均方误差内 , 以先到者为准。 特别注意,仅计算大于0
的值。 这些是我们试图预测的价值; 因此,我们不想在我们的代码中尝试匹配它们(否则,模型将在零评级下进行训练)! 这也是不能使用 NumPy 内置的奇异值分解(SVD)函数(即np.linalg.svd
或np.linalg.solve
)的原因。
让我们使用这些分解函数来构建模型并将模型构建后保存到磁盘中。通过这种方式,我们可以使用类中的dump
和load
方法在方便时加载模型。 将以下方法添加到Recommender
类:
In [29]: def build(self, output=None, alternate=False):
...: """
...: Trains the model by employing matrix factorization on our training
...: data set, the sparse reviews matrix. The model is the dot product
...: of the P and Q decomposed matrices from the factorization.
...: """
...: options = {
...: 'K': self.features,
...: 'steps': self.steps,
...: 'alpha': self.alpha,
...: 'beta': self.beta,
...: }
...:
...: self.build_start = time.time()
...: nnmf = factor2 if alternate else factor
...: self.P, self.Q = nnmf(self.reviews, **options)
...: self.model = np.dot(self.P, self.Q.T)
...: self.build_finish = time.time()
...:
...: if output:
...: self.dump(output)
此辅助函数将使我们能够快速构建模型。 请注意,我们还保存了P
和Q
-我们潜在功能的参数。 这不是必需的,因为我们的预测模型是两个因子矩阵的点积。 决定是否在模型中保存此信息是重新训练时间(尽管您必须提防过大的可能,但可能要从当前的P
和Q
参数开始)和磁盘空间之间的权衡,例如[HTG4 保存这些矩阵的磁盘上的]会更大。 要构建此模型并将数据转储到磁盘,请运行以下代码:
model = Recommender(relative_path('../data/ml-100k/u.data'))model.build('reccod.pickle')
警告! 这将需要很长时间才能完成! 在配备 2.8 GHz 处理器的 2013 MacBook Pro 上,此过程花费了大约 9 个小时 15 分钟,并且需要 23.1 MB 的内存。 对于您可能习惯于编写的大多数 Python 脚本来说,这并不是无关紧要的! 在构建模型之前,继续完成本食谱的其余部分,这不是一个坏主意。 在继续进行整个过程之前,在 100 条记录的较小测试集中测试代码也可能不是一个坏主意! 此外,如果您没有时间训练模型,则可以在本书的勘误表中找到我们模型的pickle
模块。
本食谱使有关推荐引擎的这一章结束。 现在,我们将使用基于的新的基于非负矩阵分解的模型,并查看一些预测的评论。
利用我们的模型的最后一步是访问基于我们模型的电影的预测评论:
In [30]: def predict_ranking(self, user, movie):
...: uidx = self.users.index(user)
...: midx = self.movies.index(movie)
...: if self.reviews[uidx, midx] > 0:
...: return None
...: return self.model[uidx, midx]
计算排名相对容易; 我们只需要查找用户的索引和电影的索引,并在模型中查找预测的收视率。 这就是为什么在pickle
模块中保存用户和电影的有序列表如此重要的原因; 这样,如果数据发生更改(我们添加了用户或电影),但是更改未反映在我们的模型中,则会引发异常。 由于模型是历史预测,并且对时间的变化不敏感,因此我们需要确保不断用新数据对模型进行重新训练。 如果我们知道用户的排名(例如,这不是预测),则此方法还会返回None
;否则,此方法将返回None
。 我们将在下一步中利用它。
要预测排名最高的电影,我们可以利用先前的功能为用户订购预测的最高排名:
In [31]: import heapq
...: from operator import itemgetter
...:
...: def top_rated(self, user, n=12):
...: movies = [(mid, self.predict_ranking(user, mid)) for mid in self.movies]
...: return heapq.nlargest(n, movies, key=itemgetter(1))
现在,我们可以打印出尚未被用户评价的顶级电影:
>>> rec = Recommender.load('reccod.pickle')>>> for item in rec.top_rated(234):... print "%i: %0.3f" % item 814: 4.4371642: 4.3621491: 4.3611599: 4.3431536: 4.3241500: 4.3231449: 4.2811650: 4.1471645: 4.1351467: 4.1331636: 4.1331651: 4.132
然后,只需使用电影 ID 在我们的电影数据库中查找电影即可。